From 8f6a2a686fda122d9b31a01a7671680a3043c93b Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Tue, 19 May 2026 23:09:41 +0200 Subject: [PATCH] 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). --- docs-site/app/global.css | 113 +++++-- docs-site/components/code-block.tsx | 334 ++++++++++++++++++- docs-site/components/semantic-layer-flow.tsx | 206 ++++++++++-- 3 files changed, 586 insertions(+), 67 deletions(-) diff --git a/docs-site/app/global.css b/docs-site/app/global.css index e7e2c5b2..d0f7ac21 100644 --- a/docs-site/app/global.css +++ b/docs-site/app/global.css @@ -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; } diff --git a/docs-site/components/code-block.tsx b/docs-site/components/code-block.tsx index 9c9d71ec..80f29bdc 100644 --- a/docs-site/components/code-block.tsx +++ b/docs-site/components/code-block.tsx @@ -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( + + {token} + , + ); +} + +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 (
output - +
           {children}
         
@@ -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 (
- - {title} {language && {language}} + {headerTitle}
-          {children}
+          {highlightedCode}
         
); @@ -111,7 +419,7 @@ export function CodeBlock(props: Props) {
-        {children}
+        {highlightedCode}
       
); diff --git a/docs-site/components/semantic-layer-flow.tsx b/docs-site/components/semantic-layer-flow.tsx index 27e3634f..1a755384 100644 --- a/docs-site/components/semantic-layer-flow.tsx +++ b/docs-site/components/semantic-layer-flow.tsx @@ -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) { return (
+-]/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( + + {token} + , + ); + 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( + + {token} + , + ); + 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 (
@@ -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}
); @@ -576,10 +668,11 @@ function ManualSqlNodeView({ data }: NodeProps) { className="flex items-start gap-1.5 text-[11.5px] leading-4 text-fd-muted-foreground" > {note} ))} @@ -707,7 +800,20 @@ function WarehouseNodeView({ data }: NodeProps) { 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" > - + +
>((instance) => { + requestAnimationFrame(() => { + void instance.fitView(FIT_VIEW_OPTIONS).then(() => { + setMinZoom(instance.getZoom()); + }); + }); + }, []); + return (
+
+ Pan / zoom +
+ );