docs-site: polish semantic-layer-internals code blocks and flow diagram

- Make CodeBlock a server component so children traverse synchronously
  under React 19 RSC streaming; previously extractText returned "" in
  dev SSR, leaving code blocks empty.
- Add custom JSON/YAML/SQL/code-like tokenizers with theme-aware token
  classes; drop the colored file-glyph dot and gradient tab-head.
- Tighten tab-head: subtle grey background, smaller monospace filename
  in muted grey, smaller rectangular language pill placed to the left
  of the filename.
- Polish the React Flow semantic-layer diagram (controls, fit-view
  padding, edge types).
This commit is contained in:
Andrey Avtomonov 2026-05-19 23:09:41 +02:00
parent c3dc488934
commit 8f6a2a686f
3 changed files with 586 additions and 67 deletions

View file

@ -221,6 +221,72 @@ pre code,
padding-inline: 0 !important;
}
.ktx-code .ktx-token-key {
color: #0f766e;
}
.ktx-code .ktx-token-keyword {
color: #0e7490;
font-weight: 650;
}
.ktx-code .ktx-token-function {
color: #7c3aed;
font-weight: 650;
}
.ktx-code .ktx-token-flag {
color: #0369a1;
}
.ktx-code .ktx-token-string {
color: #b45309;
}
.ktx-code .ktx-token-number,
.ktx-code .ktx-token-constant {
color: #be123c;
}
.ktx-code .ktx-token-comment {
color: #64748b;
font-style: italic;
}
.ktx-code .ktx-token-punctuation {
color: #64748b;
}
.dark .ktx-code .ktx-token-key {
color: #5eead4;
}
.dark .ktx-code .ktx-token-keyword {
color: #67e8f9;
}
.dark .ktx-code .ktx-token-function {
color: #c4b5fd;
}
.dark .ktx-code .ktx-token-flag {
color: #7dd3fc;
}
.dark .ktx-code .ktx-token-string {
color: #fbbf24;
}
.dark .ktx-code .ktx-token-number,
.dark .ktx-code .ktx-token-constant {
color: #fb7185;
}
.dark .ktx-code .ktx-token-comment,
.dark .ktx-code .ktx-token-punctuation {
color: #94a3b8;
}
/* Neutralize the outer figure styling that our wrapper now owns */
figure:has(> .ktx-code),
figure[data-rehype-pretty-code-figure]:has(.ktx-code) {
@ -327,55 +393,32 @@ figure[data-rehype-pretty-code-figure]:has(.ktx-code) {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px 8px 14px;
padding: 5px 8px 5px 12px;
border-bottom: 1px solid var(--color-fd-border);
background: linear-gradient(180deg, var(--color-fd-muted), transparent);
background: rgba(0, 0, 0, 0.025);
}
.dark .ktx-code-tab-head {
border-bottom-color: rgba(255, 255, 255, 0.05);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent);
background: rgba(255, 255, 255, 0.02);
}
.ktx-file-glyph {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 999px;
background: var(--color-fd-muted-foreground);
flex-shrink: 0;
}
.ktx-file-glyph[data-lang="yaml"],
.ktx-file-glyph[data-lang="yml"] { background: #fbbf24; }
.ktx-file-glyph[data-lang="ts"],
.ktx-file-glyph[data-lang="tsx"],
.ktx-file-glyph[data-lang="typescript"] { background: #3b82f6; }
.ktx-file-glyph[data-lang="js"],
.ktx-file-glyph[data-lang="jsx"],
.ktx-file-glyph[data-lang="javascript"] { background: #facc15; }
.ktx-file-glyph[data-lang="json"] { background: #84cc16; }
.ktx-file-glyph[data-lang="md"],
.ktx-file-glyph[data-lang="mdx"] { background: #a3a3a3; }
.ktx-file-glyph[data-lang="sql"] { background: #f97316; }
.ktx-file-glyph[data-lang="py"],
.ktx-file-glyph[data-lang="python"] { background: #22d3ee; }
.ktx-code-tab-filename {
font-family: var(--font-mono), ui-monospace, monospace;
font-size: 12.5px;
color: var(--color-fd-foreground);
font-size: 11.5px;
color: #6b7280;
}
.ktx-lang-pill {
margin-left: 4px;
padding: 1px 6px;
font-size: 10px;
font-weight: 600;
margin-right: 4px;
padding: 0 7px;
font-size: 9px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--color-fd-muted-foreground);
letter-spacing: 0.06em;
color: #9ca3af;
border: 1px solid var(--color-fd-border);
border-radius: 4px;
border-radius: 3px;
background: var(--color-fd-card);
font-family: var(--font-display), var(--font-sans), sans-serif;
}

View file

@ -1,5 +1,3 @@
"use client";
import {
type ComponentPropsWithoutRef,
type ReactNode,
@ -15,6 +13,55 @@ type Props = ComponentPropsWithoutRef<"pre"> & {
const OUTPUT_LANGS = new Set(["text", "plain", "plaintext", "console", "output"]);
const WIZARD_GLYPHS = /^\s*[◆◇◯◐○●]/;
const JSON_TOKEN_PATTERN =
/"(?:\\.|[^"\\])*"|-?\b\d+(?:\.\d+)?\b|\b(?:true|false|null)\b|[{}[\],:]/g;
const SQL_TOKEN_PATTERN =
/--[^\n]*|'(?:''|[^'])*'|\b\d+(?:\.\d+)?\b|\b(?:select|from|join|left|right|inner|outer|on|where|group|by|order|limit|as|sum|avg|min|max|count|coalesce|date_trunc|case|when|then|else|end|and|or|is|not|null|false|true|with|having|over|partition|insert|update|delete|create|alter|drop|table|view)\b|[(),.;=*<>+-]/gi;
const CODE_LIKE_TOKEN_PATTERN =
/\/\/[^\n]*|\/\*[\s\S]*?\*\/|#(?![{\w-]+:)[^\n]*|`(?:\\.|[^`\\])*`|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|-?\b\d+(?:\.\d+)?\b|\b(?:const|let|var|function|return|import|export|from|type|interface|extends|async|await|if|else|for|while|switch|case|break|continue|try|catch|throw|new|class|public|private|protected|readonly|true|false|null|undefined|pnpm|uv|ktx|node|npx|curl|git)\b|--?[\w-]+|[{}[\](),.;:=*<>|&+-]/g;
const SQL_FUNCTIONS = new Set([
"sum",
"avg",
"min",
"max",
"count",
"coalesce",
"date_trunc",
]);
const CODE_KEYWORDS = new Set([
"const",
"let",
"var",
"function",
"return",
"import",
"export",
"from",
"type",
"interface",
"extends",
"async",
"await",
"if",
"else",
"for",
"while",
"switch",
"case",
"break",
"continue",
"try",
"catch",
"throw",
"new",
"class",
"public",
"private",
"protected",
"readonly",
]);
const COMMAND_KEYWORDS = new Set(["pnpm", "uv", "ktx", "node", "npx", "curl", "git"]);
const CODE_CONSTANTS = new Set(["true", "false", "null", "undefined"]);
function extractText(node: ReactNode): string {
if (typeof node === "string") return node;
@ -65,15 +112,277 @@ function detectLanguage(props: Props, children: ReactNode): string | null {
return findLanguageInNode(children);
}
function stripOneLeadingBlankLine(text: string) {
return text.startsWith("\n") ? text.slice(1) : text;
}
function extractCodeHeader(language: string | null, code: string) {
const normalized = normalizeLanguage(language);
const firstLineEnd = code.indexOf("\n");
const firstLine = firstLineEnd === -1 ? code : code.slice(0, firstLineEnd);
const rest = firstLineEnd === -1 ? "" : code.slice(firstLineEnd + 1);
const commentPrefix =
normalized === "sql"
? "--"
: normalized === "javascript" ||
normalized === "js" ||
normalized === "jsx" ||
normalized === "typescript" ||
normalized === "ts" ||
normalized === "tsx"
? "//"
: "#";
if (!firstLine.trimStart().startsWith(commentPrefix)) {
return { header: null, code };
}
const candidate = firstLine
.trim()
.slice(commentPrefix.length)
.trim();
const looksLikePath =
candidate.includes("/") &&
/\.[A-Za-z0-9]+(?:["'`)]*)?$/.test(candidate);
if (!looksLikePath) return { header: null, code };
return {
header: candidate,
code: stripOneLeadingBlankLine(rest),
};
}
function normalizeLanguage(language: string | null) {
return language?.toLowerCase() ?? "";
}
function pushMatchedToken(
parts: ReactNode[],
token: string,
className: string,
key: string,
) {
parts.push(
<span key={key} className={className}>
{token}
</span>,
);
}
function highlightJson(code: string) {
const parts: ReactNode[] = [];
let lastIndex = 0;
let tokenIndex = 0;
for (const match of code.matchAll(JSON_TOKEN_PATTERN)) {
const token = match[0];
const index = match.index ?? 0;
if (index > lastIndex) parts.push(code.slice(lastIndex, index));
const nextText = code.slice(index + token.length);
const className = token.startsWith('"')
? /^\s*:/.test(nextText)
? "ktx-token-key"
: "ktx-token-string"
: /^-?\d/.test(token)
? "ktx-token-number"
: /^(true|false|null)$/.test(token)
? "ktx-token-constant"
: "ktx-token-punctuation";
pushMatchedToken(parts, token, className, `json-${tokenIndex}`);
lastIndex = index + token.length;
tokenIndex += 1;
}
if (lastIndex < code.length) parts.push(code.slice(lastIndex));
return parts;
}
function highlightYaml(code: string) {
const parts: ReactNode[] = [];
const lines = code.split(/(\n)/);
let tokenIndex = 0;
for (const line of lines) {
if (line === "\n") {
parts.push(line);
continue;
}
const commentIndex = line.search(/\s#/);
const fullLineComment = line.trimStart().startsWith("#");
const contentEnd =
fullLineComment || commentIndex === -1 ? line.length : commentIndex + 1;
const content = fullLineComment ? "" : line.slice(0, contentEnd);
const comment = fullLineComment ? line : line.slice(contentEnd);
const keyMatch = content.match(/^(\s*(?:-\s*)?)([A-Za-z_][\w.-]*)(\s*:)/);
if (keyMatch) {
parts.push(keyMatch[1]);
pushMatchedToken(parts, keyMatch[2], "ktx-token-key", `yaml-key-${tokenIndex}`);
pushMatchedToken(
parts,
keyMatch[3],
"ktx-token-punctuation",
`yaml-colon-${tokenIndex}`,
);
const rest = content.slice(keyMatch[0].length);
if (rest) parts.push(...highlightInlineValue(rest, `yaml-${tokenIndex}`));
} else if (content) {
parts.push(...highlightInlineValue(content, `yaml-${tokenIndex}`));
}
if (comment) {
pushMatchedToken(parts, comment, "ktx-token-comment", `yaml-comment-${tokenIndex}`);
}
tokenIndex += 1;
}
return parts;
}
function highlightInlineValue(value: string, keyPrefix: string) {
const parts: ReactNode[] = [];
let lastIndex = 0;
let tokenIndex = 0;
const pattern = /'(?:''|[^'])*'|"(?:\\.|[^"\\])*"|-?\b\d+(?:\.\d+)?\b|\b(?:true|false|null)\b|[()[\]{},:=!<>+-]/g;
for (const match of value.matchAll(pattern)) {
const token = match[0];
const index = match.index ?? 0;
if (index > lastIndex) parts.push(value.slice(lastIndex, index));
const className =
token.startsWith("'") || token.startsWith('"')
? "ktx-token-string"
: /^-?\d/.test(token)
? "ktx-token-number"
: /^(true|false|null)$/.test(token)
? "ktx-token-constant"
: "ktx-token-punctuation";
pushMatchedToken(parts, token, className, `${keyPrefix}-value-${tokenIndex}`);
lastIndex = index + token.length;
tokenIndex += 1;
}
if (lastIndex < value.length) parts.push(value.slice(lastIndex));
return parts;
}
function highlightSql(code: string) {
const parts: ReactNode[] = [];
let lastIndex = 0;
let tokenIndex = 0;
for (const match of code.matchAll(SQL_TOKEN_PATTERN)) {
const token = match[0];
const index = match.index ?? 0;
if (index > lastIndex) parts.push(code.slice(lastIndex, index));
const lowerToken = token.toLowerCase();
const className = token.startsWith("--")
? "ktx-token-comment"
: token.startsWith("'")
? "ktx-token-string"
: /^\d/.test(token)
? "ktx-token-number"
: SQL_FUNCTIONS.has(lowerToken)
? "ktx-token-function"
: /^[a-z_]+$/i.test(token)
? "ktx-token-keyword"
: "ktx-token-punctuation";
pushMatchedToken(parts, token, className, `sql-${tokenIndex}`);
lastIndex = index + token.length;
tokenIndex += 1;
}
if (lastIndex < code.length) parts.push(code.slice(lastIndex));
return parts;
}
function highlightCodeLike(code: string) {
const parts: ReactNode[] = [];
let lastIndex = 0;
let tokenIndex = 0;
for (const match of code.matchAll(CODE_LIKE_TOKEN_PATTERN)) {
const token = match[0];
const index = match.index ?? 0;
if (index > lastIndex) parts.push(code.slice(lastIndex, index));
const lowerToken = token.toLowerCase();
const className =
token.startsWith("//") || token.startsWith("/*") || token.startsWith("#")
? "ktx-token-comment"
: token.startsWith("'") || token.startsWith('"') || token.startsWith("`")
? "ktx-token-string"
: /^-?\d/.test(token)
? "ktx-token-number"
: CODE_CONSTANTS.has(lowerToken)
? "ktx-token-constant"
: CODE_KEYWORDS.has(lowerToken)
? "ktx-token-keyword"
: COMMAND_KEYWORDS.has(lowerToken)
? "ktx-token-function"
: token.startsWith("-")
? "ktx-token-flag"
: "ktx-token-punctuation";
pushMatchedToken(parts, token, className, `code-${tokenIndex}`);
lastIndex = index + token.length;
tokenIndex += 1;
}
if (lastIndex < code.length) parts.push(code.slice(lastIndex));
return parts;
}
function highlightCode(language: string | null, code: string) {
const normalized = normalizeLanguage(language);
if (normalized === "json" || normalized === "jsonc") return highlightJson(code);
if (normalized === "yaml" || normalized === "yml") return highlightYaml(code);
if (normalized === "sql") return highlightSql(code);
if (
[
"bash",
"sh",
"shell",
"zsh",
"javascript",
"js",
"jsx",
"typescript",
"ts",
"tsx",
"python",
"py",
].includes(normalized)
) {
return highlightCodeLike(code);
}
return code;
}
export function CodeBlock(props: Props) {
const { children, title, className: _ignored, ...rest } = props;
const language = detectLanguage(props, children);
const codeText = extractText(children);
const rawCodeText = extractText(children);
const extractedHeader = extractCodeHeader(language, rawCodeText);
const codeText = extractedHeader.code;
const headerTitle =
typeof title === "string" && title.length > 0
? title
: extractedHeader.header;
const highlightedCode = highlightCode(language, codeText);
const hasTitle = typeof title === "string" && title.length > 0;
const hasHeader = typeof headerTitle === "string" && headerTitle.length > 0;
const isOutput =
!hasTitle &&
(WIZARD_GLYPHS.test(codeText) ||
!hasHeader &&
(WIZARD_GLYPHS.test(rawCodeText) ||
(language !== null && OUTPUT_LANGS.has(language)));
// Mode D - Output preview (wizard prompts, terminal output)
@ -81,7 +390,7 @@ export function CodeBlock(props: Props) {
return (
<div className="not-prose ktx-code ktx-code-output group relative">
<span className="ktx-code-output-label">output</span>
<CopyButton text={codeText} className="ktx-code-output-copy" />
<CopyButton text={rawCodeText} className="ktx-code-output-copy" />
<pre {...rest} className="ktx-code-body ktx-code-body-output">
{children}
</pre>
@ -89,18 +398,17 @@ export function CodeBlock(props: Props) {
);
}
// Mode B - VS Code tab (filename present)
if (hasTitle) {
// Mode B - Header (filename present)
if (hasHeader) {
return (
<div className="not-prose ktx-code ktx-code-tab group">
<div className="ktx-code-tab-head">
<span className="ktx-file-glyph" data-lang={language ?? ""} />
<span className="ktx-code-tab-filename">{title}</span>
{language && <span className="ktx-lang-pill">{language}</span>}
<span className="ktx-code-tab-filename">{headerTitle}</span>
<CopyButton text={codeText} className="ml-auto" />
</div>
<pre {...rest} className="ktx-code-body ktx-code-body-tab">
{children}
{highlightedCode}
</pre>
</div>
);
@ -111,7 +419,7 @@ export function CodeBlock(props: Props) {
<div className="not-prose ktx-code ktx-code-minimal group relative">
<CopyButton text={codeText} className="ktx-code-minimal-copy" />
<pre {...rest} className="ktx-code-body ktx-code-body-minimal">
{children}
{highlightedCode}
</pre>
</div>
);

View file

@ -1,12 +1,15 @@
"use client";
import { useCallback, useState } from "react";
import {
Background,
BackgroundVariant,
Controls,
Handle,
MarkerType,
type Node,
type NodeProps,
type OnInit,
Position,
ReactFlow,
} from "@xyflow/react";
@ -108,6 +111,7 @@ const WAREHOUSE_Y = LANES_BOTTOM_Y + 56;
const MANUAL_STROKE = "#94a3b8";
const KTX_STROKE = "#0891b2";
const FIT_VIEW_OPTIONS = { padding: 0.05 };
const agent: AgentNode = {
id: "agent",
@ -363,7 +367,7 @@ const edges = [
id: "agent-manual",
source: "agent",
target: "manual-sql",
type: "smoothstep" as const,
type: "default" as const,
label: "writes raw SQL",
labelBgPadding: [6, 3] as [number, number],
labelBgBorderRadius: 4,
@ -388,7 +392,8 @@ const edges = [
id: "manual-warehouse",
source: "manual-sql",
target: "warehouse",
type: "smoothstep" as const,
targetHandle: "warehouse-manual",
type: "default" as const,
style: {
stroke: MANUAL_STROKE,
strokeWidth: 1.5,
@ -400,7 +405,7 @@ const edges = [
id: "agent-slquery",
source: "agent",
target: "sl-query",
type: "smoothstep" as const,
type: "default" as const,
label: "sends Semantic Query",
labelBgPadding: [6, 3] as [number, number],
labelBgBorderRadius: 4,
@ -437,12 +442,15 @@ const edges = [
id: "compiled-warehouse",
source: "compiled-sql",
target: "warehouse",
type: "smoothstep" as const,
targetHandle: "warehouse-compiled",
type: "straight" as const,
style: { stroke: KTX_STROKE, strokeWidth: 1.75 },
markerEnd: arrowMarker(KTX_STROKE),
},
];
type FlowEdge = (typeof edges)[number];
function AgentNodeView({ data }: NodeProps<AgentNode>) {
return (
<div
@ -507,6 +515,88 @@ function LaneBadge({
);
}
const JSON_TOKEN_PATTERN =
/"(?:\\.|[^"\\])*"|-?\b\d+(?:\.\d+)?\b|\b(?:true|false|null)\b|[{}[\],:]/g;
const SQL_TOKEN_PATTERN =
/--[^\n]*|'(?:''|[^'])*'|\b\d+(?:\.\d+)?\b|\b(?:select|from|join|left|right|inner|outer|on|where|group|by|order|limit|as|sum|count|coalesce|date_trunc|case|when|then|else|end|and|or|is|not|null|false|true|with|having|over|partition)\b|[(),.;=*<>+-]/gi;
const SQL_FUNCTIONS = new Set(["sum", "count", "coalesce", "date_trunc"]);
function highlightJson(code: string) {
const parts = [];
let lastIndex = 0;
let tokenIndex = 0;
for (const match of code.matchAll(JSON_TOKEN_PATTERN)) {
const token = match[0];
const index = match.index ?? 0;
if (index > lastIndex) parts.push(code.slice(lastIndex, index));
const nextText = code.slice(index + token.length);
const className = token.startsWith('"')
? /^\s*:/.test(nextText)
? "syntax-json-key"
: "syntax-string"
: /^-?\d/.test(token)
? "syntax-number"
: /^(true|false|null)$/.test(token)
? "syntax-constant"
: "syntax-punctuation";
parts.push(
<span key={`json-${tokenIndex}`} className={className}>
{token}
</span>,
);
lastIndex = index + token.length;
tokenIndex += 1;
}
if (lastIndex < code.length) parts.push(code.slice(lastIndex));
return parts;
}
function highlightSql(code: string) {
const parts = [];
let lastIndex = 0;
let tokenIndex = 0;
for (const match of code.matchAll(SQL_TOKEN_PATTERN)) {
const token = match[0];
const index = match.index ?? 0;
if (index > lastIndex) parts.push(code.slice(lastIndex, index));
const lowerToken = token.toLowerCase();
const className = token.startsWith("--")
? "syntax-comment"
: token.startsWith("'")
? "syntax-string"
: /^\d/.test(token)
? "syntax-number"
: SQL_FUNCTIONS.has(lowerToken)
? "syntax-function"
: /^[a-z_]+$/i.test(token)
? "syntax-keyword"
: "syntax-punctuation";
parts.push(
<span key={`sql-${tokenIndex}`} className={className}>
{token}
</span>,
);
lastIndex = index + token.length;
tokenIndex += 1;
}
if (lastIndex < code.length) parts.push(code.slice(lastIndex));
return parts;
}
function highlightCode(language: string, code: string) {
if (language === "json") return highlightJson(code);
if (language === "sql") return highlightSql(code);
return code;
}
function CodeBlock({
language,
code,
@ -522,6 +612,8 @@ function CodeBlock({
: tone === "slQuery"
? "text-fd-primary"
: "text-fd-primary/90";
const highlightedCode = highlightCode(language, code);
return (
<div className="flex h-full flex-col overflow-hidden rounded-md border border-fd-border bg-[#fbfaf6] dark:bg-[#0c1417]">
<div className="flex flex-none items-center justify-between border-b border-fd-border bg-fd-muted/40 px-3 py-1.5">
@ -542,7 +634,7 @@ function CodeBlock({
className="m-0 flex-1 overflow-auto px-3 py-2 font-mono text-fd-foreground"
style={{ fontSize: "11.5px", lineHeight: "17.5px" }}
>
{code}
{highlightedCode}
</pre>
</div>
);
@ -576,10 +668,11 @@ function ManualSqlNodeView({ data }: NodeProps<ManualSqlNode>) {
className="flex items-start gap-1.5 text-[11.5px] leading-4 text-fd-muted-foreground"
>
<span
className="mt-1 h-1 w-1 flex-none rounded-full"
style={{ background: MANUAL_STROKE }}
className="mt-[3px] flex h-2.5 w-2.5 flex-none items-center justify-center text-[11px] font-semibold leading-none text-red-500"
aria-hidden="true"
/>
>
×
</span>
<span>{note}</span>
</li>
))}
@ -707,7 +800,20 @@ function WarehouseNodeView({ data }: NodeProps<WarehouseNode>) {
style={{ width: WAREHOUSE_W, height: WAREHOUSE_H }}
className="flex items-center gap-3 rounded-md border border-fd-border bg-fd-card px-4 py-3 shadow-sm"
>
<Handle type="target" position={Position.Top} className="!opacity-0" />
<Handle
id="warehouse-manual"
type="target"
position={Position.Top}
className="!opacity-0"
style={{ left: "42%" }}
/>
<Handle
id="warehouse-compiled"
type="target"
position={Position.Top}
className="!opacity-0"
style={{ left: "58%" }}
/>
<div className="flex h-10 w-10 flex-none items-center justify-center rounded-md bg-fd-primary/12 text-fd-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
@ -748,6 +854,15 @@ const nodeTypes = {
};
export function SemanticLayerFlow() {
const [minZoom, setMinZoom] = useState(0.2);
const handleFlowInit = useCallback<OnInit<FlowNode, FlowEdge>>((instance) => {
requestAnimationFrame(() => {
void instance.fitView(FIT_VIEW_OPTIONS).then(() => {
setMinZoom(instance.getZoom());
});
});
}, []);
return (
<section
className="not-prose my-10 w-full max-w-full min-w-0 space-y-4"
@ -777,33 +892,36 @@ export function SemanticLayerFlow() {
</div>
<div
className="sl-flow-canvas bg-fd-background"
className="sl-flow-canvas relative bg-fd-background"
style={{
height: "min(2340px, 290vw)",
minHeight: 1780,
}}
>
<div className="pointer-events-none absolute right-2.5 top-2.5 z-10 rounded border border-fd-border/50 bg-white/30 px-1.5 py-px font-mono text-[9.5px] font-medium uppercase tracking-[0.06em] text-fd-muted-foreground shadow-sm backdrop-blur-sm dark:bg-white/10">
Pan / zoom
</div>
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
fitView
fitViewOptions={{ padding: 0.05 }}
onInit={handleFlowInit}
nodesDraggable={false}
nodesConnectable={false}
nodesFocusable={false}
edgesFocusable={false}
elementsSelectable={false}
panOnDrag={false}
panOnDrag
panOnScroll={false}
zoomOnScroll={false}
zoomOnPinch={false}
zoomOnDoubleClick={false}
preventScrolling={false}
minZoom={0.2}
zoomOnScroll
zoomOnPinch
zoomOnDoubleClick
preventScrolling
minZoom={minZoom}
maxZoom={1.5}
proOptions={{ hideAttribution: true }}
>
<Controls position="bottom-right" showInteractive={false} />
<Background
variant={BackgroundVariant.Dots}
gap={18}
@ -839,7 +957,10 @@ export function SemanticLayerFlow() {
box-shadow: none;
}
.sl-flow-canvas .react-flow__pane {
cursor: default;
cursor: grab;
}
.sl-flow-canvas .react-flow__pane:active {
cursor: grabbing;
}
.sl-flow-canvas .react-flow__handle {
width: 1px;
@ -864,6 +985,53 @@ export function SemanticLayerFlow() {
font-size: inherit !important;
line-height: inherit !important;
}
.sl-flow-canvas .syntax-json-key {
color: #0f766e;
}
.sl-flow-canvas .syntax-keyword {
color: #0e7490;
font-weight: 650;
}
.sl-flow-canvas .syntax-function {
color: #7c3aed;
font-weight: 650;
}
.sl-flow-canvas .syntax-string {
color: #b45309;
}
.sl-flow-canvas .syntax-number,
.sl-flow-canvas .syntax-constant {
color: #be123c;
}
.sl-flow-canvas .syntax-comment {
color: #64748b;
font-style: italic;
}
.sl-flow-canvas .syntax-punctuation {
color: #64748b;
}
.dark .sl-flow-canvas .syntax-json-key {
color: #5eead4;
}
.dark .sl-flow-canvas .syntax-keyword {
color: #67e8f9;
}
.dark .sl-flow-canvas .syntax-function {
color: #c4b5fd;
}
.dark .sl-flow-canvas .syntax-string {
color: #fbbf24;
}
.dark .sl-flow-canvas .syntax-number,
.dark .sl-flow-canvas .syntax-constant {
color: #fb7185;
}
.dark .sl-flow-canvas .syntax-comment {
color: #94a3b8;
}
.dark .sl-flow-canvas .syntax-punctuation {
color: #94a3b8;
}
`}</style>
</section>
);