@dbx-tools/ui 0.9.111 → 0.9.116

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // Hand edits are overwritten on the next watch; this file is read-only.
4
4
 
5
5
  export const PACKAGE_IDENTIFIER = "@dbx-tools/ui";
6
- export const PACKAGE_VERSION = "0.9.111";
6
+ export const PACKAGE_VERSION = "0.9.116";
7
7
  export * as authReactAuthClient from "./src/auth/react/auth-client.ts";
8
8
  export * as authReactAuthGate from "./src/auth/react/auth-gate.tsx";
9
9
  export * as authReactPasskeyManager from "./src/auth/react/passkey-manager.tsx";
@@ -16,8 +16,10 @@ export * as emailReactEmailCompose from "./src/email/react/email-compose.tsx";
16
16
  export * as emailReactFields from "./src/email/react/fields.ts";
17
17
  export * as reactAppkitUi from "./src/react/appkit-ui.ts";
18
18
  export * as reactBrandPicker from "./src/react/brand-picker.tsx";
19
+ export * as reactHighlightedCode from "./src/react/highlighted-code.tsx";
19
20
  export * as reactRecordPreview from "./src/react/record-preview.tsx";
20
21
  export * as reactRecordPreviewData from "./src/react/record-preview-data.ts";
22
+ export * as reactShikiPlugin from "./src/react/shiki-plugin.ts";
21
23
  export * as searchReactSearchBox from "./src/search/react/search-box.tsx";
22
24
  export * as searchReactSearchResults from "./src/search/react/search-results.tsx";
23
25
  export * as searchReactUseSearch from "./src/search/react/use-search.ts";
@@ -36,6 +38,8 @@ export type { EmailComposeProps } from "./src/email/react/email-compose.tsx";
36
38
  export { joinAddresses, parseAddresses, attachmentNames } from "./src/email/react/fields.ts";
37
39
  export type { EmailDraft } from "./src/email/react/fields.ts";
38
40
  export type { BrandPreset, BrandPickerProps } from "./src/react/brand-picker.tsx";
41
+ export { HighlightedCodeBlock, JsonBlock } from "./src/react/highlighted-code.tsx";
42
+ export type { DetectedCodeLanguage } from "./src/react/highlighted-code.tsx";
39
43
  export type { RecordPreviewProps } from "./src/react/record-preview.tsx";
40
44
  export type { RecordPreviewRow } from "./src/react/record-preview-data.ts";
41
45
  export type { SearchBoxProps } from "./src/search/react/search-box.tsx";
package/package.json CHANGED
@@ -30,12 +30,13 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@databricks/appkit-ui": "0.81.0",
33
- "@dbx-tools/shared-auth": "^0.9.111",
34
- "@dbx-tools/shared-core": "^0.9.111",
35
- "@dbx-tools/shared-email": "^0.9.111",
36
- "@dbx-tools/shared-email-template": "^0.9.111",
37
- "@dbx-tools/shared-search": "^0.9.111",
33
+ "@dbx-tools/shared-auth": "^0.9.116",
34
+ "@dbx-tools/shared-core": "^0.9.116",
35
+ "@dbx-tools/shared-email": "^0.9.116",
36
+ "@dbx-tools/shared-email-template": "^0.9.116",
37
+ "@dbx-tools/shared-search": "^0.9.116",
38
38
  "lucide-react": "^0.554.0",
39
+ "shiki": "^3.0.0",
39
40
  "streamdown": "^2.5.0",
40
41
  "tailwindcss": "^4.3.2"
41
42
  },
@@ -43,7 +44,7 @@
43
44
  "publishConfig": {
44
45
  "access": "public"
45
46
  },
46
- "version": "0.9.111",
47
+ "version": "0.9.116",
47
48
  "dbxToolsConfig": {
48
49
  "tags": [
49
50
  "ui"
@@ -53,6 +54,7 @@
53
54
  "exports": {
54
55
  "./react": "./src/react/index.ts",
55
56
  "./styles.css": "./src/styles.css",
57
+ "./*": "./src/*/index.ts",
56
58
  "./branding/react": "./src/branding/react/index.ts",
57
59
  "./branding/browser": "./src/branding/browser.ts",
58
60
  "./branding/assets": "./src/branding/generated/assets.ts",
@@ -0,0 +1,6 @@
1
+ // GENERATED by projen watch - DO NOT EDIT.
2
+ // Regenerated from the exporting modules in ./src.
3
+ // Hand edits are overwritten on the next watch; this file is read-only.
4
+
5
+ export * as assets from "./assets.ts";
6
+ export { dbxToolsAssetSvg, dbxToolsAssetDataUrls } from "./assets.ts";
@@ -0,0 +1,8 @@
1
+ // GENERATED by projen watch - DO NOT EDIT.
2
+ // Regenerated from the exporting modules in ./src.
3
+ // Hand edits are overwritten on the next watch; this file is read-only.
4
+
5
+ export * as browser from "./browser.ts";
6
+ export * as generated from "./generated/index.ts";
7
+ export { resolveBrandAsset } from "./browser.ts";
8
+ export type { BrandAssetResolver, ApplyBrandContextOptions } from "./browser.ts";
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Fine-grained Shiki runtime containing only the supported UI languages.
3
+ *
4
+ * This module is itself dynamically imported, so its grammar, theme, and WASM
5
+ * dependencies remain off the network until a supported code block appears.
6
+ */
7
+ import { createHighlighterCore } from "shiki/core";
8
+ import { createOnigurumaEngine } from "shiki/engine/oniguruma";
9
+ import bash from "shiki/langs/bash.mjs";
10
+ import javascript from "shiki/langs/javascript.mjs";
11
+ import json from "shiki/langs/json.mjs";
12
+ import markdown from "shiki/langs/markdown.mjs";
13
+ import python from "shiki/langs/python.mjs";
14
+ import sql from "shiki/langs/sql.mjs";
15
+ import typescript from "shiki/langs/typescript.mjs";
16
+ import yaml from "shiki/langs/yaml.mjs";
17
+ import githubDarkHighContrast from "shiki/themes/github-dark-high-contrast.mjs";
18
+ import githubLightHighContrast from "shiki/themes/github-light-high-contrast.mjs";
19
+
20
+ /** Create the shared highlighter with the exact configured language set. */
21
+ export function createHighlighter() {
22
+ return createHighlighterCore({
23
+ themes: [githubLightHighContrast, githubDarkHighContrast],
24
+ langs: [sql, python, typescript, javascript, json, bash, yaml, markdown],
25
+ engine: createOnigurumaEngine(import("shiki/wasm")),
26
+ });
27
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Shared syntax-highlighted code surfaces for AppKit-oriented UI packages.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { json } from "@dbx-tools/shared-core";
8
+ import { useEffect, useState } from "react";
9
+
10
+ import { cn } from "./appkit-ui.ts";
11
+ import { highlightToHtml } from "./shiki-plugin.ts";
12
+
13
+ export { createShikiPlugin, highlightToHtml } from "./shiki-plugin.ts";
14
+
15
+ /** Code languages detected automatically in free-form text fields. */
16
+ export type DetectedCodeLanguage = "sql" | "python" | "typescript" | "bash" | "json";
17
+
18
+ /** Detect a supported code language from a complete free-form text value. */
19
+ export function detectCodeLanguage(source: string): DetectedCodeLanguage | undefined {
20
+ const trimmed = source.trim();
21
+ if (/^(?:select|with|insert|update|delete|merge|create|alter)\b/i.test(trimmed)) return "sql";
22
+ if (/^(?:from\s+\S+\s+import|import\s+\S+|def\s+\w+|class\s+\w+):?/m.test(trimmed)) {
23
+ return "python";
24
+ }
25
+ if (/^(?:const|let|var|interface|type|export|import)\b/m.test(trimmed)) return "typescript";
26
+ if (/^(?:#!.*\b(?:ba|z|k)?sh\b|(?:cd|ls|cat|grep|find|bun|npm|pnpm|yarn)\s)/m.test(trimmed)) {
27
+ return "bash";
28
+ }
29
+ if (
30
+ (trimmed.startsWith("{") || trimmed.startsWith("[")) &&
31
+ trimmed.length > 1 &&
32
+ json.parse(trimmed) !== undefined
33
+ ) {
34
+ return "json";
35
+ }
36
+ return undefined;
37
+ }
38
+
39
+ /** Resolve syntax-highlighted HTML while preserving plaintext during lazy load. */
40
+ export function useHighlightedHtml(source: string, language: string): string | null {
41
+ const [highlighted, setHighlighted] = useState<{
42
+ source: string;
43
+ language: string;
44
+ html: string;
45
+ } | null>(null);
46
+ useEffect(() => {
47
+ let active = true;
48
+ void highlightToHtml(source, language).then((result) => {
49
+ if (active) setHighlighted({ source, language, html: result });
50
+ });
51
+ return () => {
52
+ active = false;
53
+ };
54
+ }, [language, source]);
55
+ return highlighted?.source === source && highlighted.language === language
56
+ ? highlighted.html
57
+ : null;
58
+ }
59
+
60
+ /** Render code with the shared lazily loaded Shiki runtime. */
61
+ export const HighlightedCodeBlock = ({
62
+ source,
63
+ language,
64
+ className,
65
+ }: {
66
+ source: string;
67
+ language: string;
68
+ className?: string;
69
+ }) => {
70
+ const html = useHighlightedHtml(source, language);
71
+ return (
72
+ <pre
73
+ data-language={language}
74
+ className={cn(
75
+ "max-w-full overflow-auto whitespace-pre-wrap break-words font-mono text-xs leading-relaxed",
76
+ className,
77
+ )}
78
+ >
79
+ {html === null ? (
80
+ <code>{source}</code>
81
+ ) : (
82
+ <code data-dbx-highlighted-code dangerouslySetInnerHTML={{ __html: html }} />
83
+ )}
84
+ </pre>
85
+ );
86
+ };
87
+
88
+ /** Render formatted JSON with the shared lazily loaded Shiki runtime. */
89
+ export const JsonBlock = ({ json, className }: { json: string; className?: string }) => (
90
+ <HighlightedCodeBlock source={json} language="json" className={className} />
91
+ );
@@ -1,3 +1,4 @@
1
1
  export * from "./appkit-ui.ts";
2
2
  export * from "./brand-picker.tsx";
3
+ export * from "./highlighted-code.tsx";
3
4
  export * from "./record-preview.tsx";
@@ -4,7 +4,7 @@
4
4
  * @module
5
5
  */
6
6
 
7
- import { stringUtils } from "@dbx-tools/shared-core";
7
+ import { json, object as objectUtils, stringUtils } from "@dbx-tools/shared-core";
8
8
 
9
9
  /** One field shown in the table view of {@link RecordPreview}. */
10
10
  export interface RecordPreviewRow {
@@ -39,6 +39,18 @@ export function formatRecordPreviewJson(value: unknown): string {
39
39
  }
40
40
  }
41
41
 
42
+ /** Parse a string whose complete value is a serialized JSON object or array. */
43
+ export function parseNestedJsonText(
44
+ value: unknown,
45
+ ): Record<string, unknown> | unknown[] | undefined {
46
+ if (typeof value !== "string") return undefined;
47
+ const trimmed = value.trim();
48
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return undefined;
49
+ const parsed = json.parse(trimmed);
50
+ if (Array.isArray(parsed)) return parsed;
51
+ return objectUtils.isRecord(parsed) ? parsed : undefined;
52
+ }
53
+
42
54
  /** YAML document fence at the start of a markdown file (`---` ... `---`). */
43
55
  const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
44
56
 
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { BracesIcon, Table2Icon } from "lucide-react";
9
- import { useState } from "react";
9
+ import { Fragment, useState } from "react";
10
10
  import { Streamdown } from "streamdown";
11
11
 
12
12
  import {
@@ -21,10 +21,12 @@ import {
21
21
  TooltipTrigger,
22
22
  cn,
23
23
  } from "./appkit-ui.ts";
24
+ import { detectCodeLanguage, HighlightedCodeBlock, JsonBlock } from "./highlighted-code.tsx";
24
25
  import {
25
26
  formatRecordPreviewJson,
26
27
  looksLikeMarkdown,
27
28
  markdownForPreview,
29
+ parseNestedJsonText,
28
30
  recordPreviewRows,
29
31
  type RecordPreviewRow,
30
32
  } from "./record-preview-data.ts";
@@ -34,6 +36,7 @@ export {
34
36
  formatRecordPreviewJson,
35
37
  looksLikeMarkdown,
36
38
  markdownForPreview,
39
+ parseNestedJsonText,
37
40
  recordPreviewRows,
38
41
  } from "./record-preview-data.ts";
39
42
 
@@ -41,7 +44,8 @@ export {
41
44
  const MARKDOWN_CELL_CLASSES =
42
45
  "min-w-0 max-w-full text-[11px] leading-snug [&_p]:my-0.5 [&_p]:leading-snug [&_ul]:my-0.5 [&_ol]:my-0.5 [&_li]:my-0 [&_h1]:my-1 [&_h1]:text-[11px] [&_h1]:font-semibold [&_h2]:my-1 [&_h2]:text-[11px] [&_h2]:font-semibold [&_h3]:my-1 [&_h3]:text-[11px] [&_h3]:font-semibold [&_pre]:my-1 [&_pre]:overflow-x-auto [&_pre]:rounded [&_pre]:bg-background/60 [&_pre]:p-1.5 [&_pre]:text-[10px] [&_code]:text-[10px]";
43
46
 
44
- const NESTED_DEPTH = 2;
47
+ const MAX_NESTED_DEPTH = 12;
48
+ const MAX_INDENT_DEPTH = 4;
45
49
 
46
50
  /** Props for {@link RecordPreview}. */
47
51
  export interface RecordPreviewProps {
@@ -55,55 +59,276 @@ export interface RecordPreviewProps {
55
59
  export function RecordPreview({ value, className }: RecordPreviewProps) {
56
60
  const [raw, setRaw] = useState(false);
57
61
  const rows = recordPreviewRows(value);
62
+ const rawJson = formatRecordPreviewJson(value);
58
63
  return (
59
- <div className={cn("relative min-w-0", className)}>
60
- <Tooltip>
61
- <TooltipTrigger asChild>
62
- <Button
63
- type="button"
64
- size="icon"
65
- variant="ghost"
66
- aria-pressed={raw}
67
- aria-label={raw ? "Show table" : "Show JSON"}
68
- className="absolute right-1 top-1 z-10 size-6"
69
- onClick={() => setRaw((current) => !current)}
70
- >
71
- {raw ? <Table2Icon className="size-3" /> : <BracesIcon className="size-3" />}
72
- </Button>
73
- </TooltipTrigger>
74
- <TooltipContent>{raw ? "Table" : "JSON"}</TooltipContent>
75
- </Tooltip>
64
+ <div
65
+ className={cn(
66
+ "relative min-w-0 [&_[data-slot=table-container]]:overflow-x-hidden",
67
+ className,
68
+ )}
69
+ >
70
+ <div className="sticky top-1 z-10 float-right mr-1 h-0">
71
+ <Tooltip>
72
+ <TooltipTrigger asChild>
73
+ <Button
74
+ type="button"
75
+ size="icon"
76
+ variant="ghost"
77
+ aria-pressed={raw}
78
+ aria-label={raw ? "Show table" : "Show JSON"}
79
+ className="size-6 bg-background/85 backdrop-blur-sm"
80
+ onClick={() => setRaw((current) => !current)}
81
+ >
82
+ {raw ? <Table2Icon className="size-3" /> : <BracesIcon className="size-3" />}
83
+ </Button>
84
+ </TooltipTrigger>
85
+ <TooltipContent>{raw ? "Table" : "JSON"}</TooltipContent>
86
+ </Tooltip>
87
+ </div>
76
88
  {raw ? (
77
- <pre className="max-h-80 overflow-auto whitespace-pre-wrap break-words rounded bg-background/40 p-2 pr-8 font-mono text-[11px] leading-relaxed">
78
- {formatRecordPreviewJson(value)}
89
+ <JsonBlock json={rawJson} className="rounded bg-background/40 p-2 pr-8 text-[11px]" />
90
+ ) : rows.length === 0 ? (
91
+ <pre className="whitespace-pre-wrap break-words rounded bg-background/40 p-2 pr-8 font-mono text-[11px] leading-relaxed">
92
+ {rawJson}
79
93
  </pre>
80
94
  ) : (
81
- <RecordTable rows={rows} depth={0} />
95
+ <RecordTable rows={rows} />
82
96
  )}
83
97
  </div>
84
98
  );
85
99
  }
86
100
 
87
- function RecordTable({ rows, depth }: { rows: RecordPreviewRow[]; depth: number }) {
101
+ function RecordTable({ rows }: { rows: RecordPreviewRow[] }) {
88
102
  if (rows.length === 0) return null;
89
103
  return (
90
- <Table className="text-[11px]">
91
- <TableBody>
104
+ <div className="min-w-0 px-2 pb-2">
105
+ <Table className="table-fixed text-[11px]">
106
+ <TableBody>
107
+ {rows.map((row) => (
108
+ <RecordRow key={row.key} row={row} depth={0} path={row.key} />
109
+ ))}
110
+ </TableBody>
111
+ </Table>
112
+ </div>
113
+ );
114
+ }
115
+
116
+ function RecordRow({
117
+ row,
118
+ depth,
119
+ path,
120
+ divided,
121
+ }: {
122
+ row: RecordPreviewRow;
123
+ depth: number;
124
+ path: string;
125
+ divided?: boolean;
126
+ }) {
127
+ const nested = nestedRecordValue(row.value);
128
+ if (!nested) {
129
+ return <FieldRow row={row} depth={depth} divided={divided} />;
130
+ }
131
+ return (
132
+ <Fragment>
133
+ <SectionRow
134
+ label={row.label}
135
+ depth={depth}
136
+ divided={divided}
137
+ parsedFromText={nested.parsedFromText}
138
+ />
139
+ <NestedRows value={nested.value} depth={depth + 1} path={path} />
140
+ </Fragment>
141
+ );
142
+ }
143
+
144
+ function FieldRow({
145
+ row,
146
+ depth,
147
+ divided,
148
+ }: {
149
+ row: RecordPreviewRow;
150
+ depth: number;
151
+ divided?: boolean;
152
+ }) {
153
+ return (
154
+ <TableRow className={cn("hover:bg-transparent", divided && "border-t-2 border-border/50")}>
155
+ <TableHead
156
+ className="h-auto w-28 align-top whitespace-normal px-0 py-1.5 pr-2 font-medium text-muted-foreground sm:w-[8.5rem]"
157
+ style={{ paddingLeft: rowIndent(depth) }}
158
+ >
159
+ {row.label}
160
+ </TableHead>
161
+ <TableCell className="min-w-0 whitespace-normal px-0 py-1.5 pr-8 align-top [overflow-wrap:anywhere]">
162
+ <RecordValue value={row.value} depth={depth} />
163
+ </TableCell>
164
+ </TableRow>
165
+ );
166
+ }
167
+
168
+ function SectionRow({
169
+ label,
170
+ depth,
171
+ divided,
172
+ parsedFromText,
173
+ }: {
174
+ label: string;
175
+ depth: number;
176
+ divided?: boolean;
177
+ parsedFromText: boolean;
178
+ }) {
179
+ return (
180
+ <TableRow
181
+ className={cn("border-b-0 hover:bg-transparent", divided && "border-t-2 border-border/50")}
182
+ >
183
+ <TableHead
184
+ colSpan={2}
185
+ className="h-auto px-0 pb-1 pt-3 text-[10px] font-semibold uppercase tracking-wide text-foreground/85"
186
+ style={{ paddingLeft: rowIndent(depth) }}
187
+ >
188
+ <span className="inline-flex items-center gap-1">
189
+ {label}
190
+ {parsedFromText ? (
191
+ <BracesIcon
192
+ className="size-3 text-muted-foreground"
193
+ aria-label="JSON parsed from text"
194
+ />
195
+ ) : null}
196
+ </span>
197
+ </TableHead>
198
+ </TableRow>
199
+ );
200
+ }
201
+
202
+ function NestedRows({ value, depth, path }: { value: unknown; depth: number; path: string }) {
203
+ if (depth > MAX_NESTED_DEPTH) {
204
+ return <ValueRow value={value} depth={depth} />;
205
+ }
206
+ if (Array.isArray(value)) {
207
+ return (
208
+ <Fragment>
209
+ {value.map((item, index) => (
210
+ <ArrayItemRows
211
+ key={`${path}.${index}`}
212
+ value={item}
213
+ depth={depth}
214
+ path={`${path}.${index}`}
215
+ divided={index > 0}
216
+ />
217
+ ))}
218
+ </Fragment>
219
+ );
220
+ }
221
+ if (value != null && typeof value === "object") {
222
+ const rows = recordPreviewRows(value);
223
+ if (rows.length === 0) return <ValueRow value={value} depth={depth} />;
224
+ return (
225
+ <Fragment>
92
226
  {rows.map((row) => (
93
- <TableRow key={row.key} className="hover:bg-transparent">
94
- <TableHead className="w-[8.5rem] align-top whitespace-nowrap px-2 py-1.5 font-medium text-muted-foreground">
95
- {row.label}
96
- </TableHead>
97
- <TableCell className="align-top break-words px-2 py-1.5 pr-8">
98
- <RecordValue value={row.value} depth={depth} />
99
- </TableCell>
100
- </TableRow>
227
+ <RecordRow
228
+ key={`${path}.${row.key}`}
229
+ row={row}
230
+ depth={depth}
231
+ path={`${path}.${row.key}`}
232
+ />
101
233
  ))}
102
- </TableBody>
103
- </Table>
234
+ </Fragment>
235
+ );
236
+ }
237
+ return <ValueRow value={value} depth={depth} />;
238
+ }
239
+
240
+ function ArrayItemRows({
241
+ value,
242
+ depth,
243
+ path,
244
+ divided,
245
+ }: {
246
+ value: unknown;
247
+ depth: number;
248
+ path: string;
249
+ divided: boolean;
250
+ }) {
251
+ const nested = nestedRecordValue(value);
252
+ if (nested?.parsedFromText) {
253
+ return (
254
+ <Fragment>
255
+ <NestedTextMarkerRow depth={depth} divided={divided} />
256
+ <NestedRows value={nested.value} depth={depth + 1} path={path} />
257
+ </Fragment>
258
+ );
259
+ }
260
+ if (value != null && typeof value === "object") {
261
+ const rows = recordPreviewRows(value);
262
+ if (rows.length === 0) return <ValueRow value={value} depth={depth} divided={divided} />;
263
+ return (
264
+ <Fragment>
265
+ {rows.map((row, index) => (
266
+ <RecordRow
267
+ key={`${path}.${row.key}`}
268
+ row={row}
269
+ depth={depth}
270
+ path={`${path}.${row.key}`}
271
+ divided={divided && index === 0}
272
+ />
273
+ ))}
274
+ </Fragment>
275
+ );
276
+ }
277
+ return <ValueRow value={value} depth={depth} divided={divided} />;
278
+ }
279
+
280
+ function NestedTextMarkerRow({ depth, divided }: { depth: number; divided: boolean }) {
281
+ return (
282
+ <TableRow className={cn("border-b-0 hover:bg-transparent", divided && "border-t-2")}>
283
+ <TableHead
284
+ colSpan={2}
285
+ className="h-auto px-0 pb-0.5 pt-2 text-muted-foreground"
286
+ style={{ paddingLeft: rowIndent(depth) }}
287
+ >
288
+ <BracesIcon className="size-3" aria-label="JSON parsed from text" />
289
+ </TableHead>
290
+ </TableRow>
104
291
  );
105
292
  }
106
293
 
294
+ function ValueRow({ value, depth, divided }: { value: unknown; depth: number; divided?: boolean }) {
295
+ return (
296
+ <TableRow className={cn("hover:bg-transparent", divided && "border-t-2 border-border/50")}>
297
+ <TableHead
298
+ aria-hidden
299
+ className="h-auto w-28 px-0 py-1.5 pr-2 sm:w-[8.5rem]"
300
+ style={{ paddingLeft: rowIndent(depth) }}
301
+ />
302
+ <TableCell className="min-w-0 whitespace-normal px-0 py-1.5 pr-8 align-top [overflow-wrap:anywhere]">
303
+ <RecordValue value={value} depth={depth} />
304
+ </TableCell>
305
+ </TableRow>
306
+ );
307
+ }
308
+
309
+ function rowIndent(depth: number): string {
310
+ return `${Math.min(depth, MAX_INDENT_DEPTH) * 0.9}rem`;
311
+ }
312
+
313
+ function nestedRecordValue(
314
+ value: unknown,
315
+ ): { value: Record<string, unknown> | unknown[]; parsedFromText: boolean } | undefined {
316
+ const parsed = parseNestedJsonText(value);
317
+ if (parsed) return { value: parsed, parsedFromText: true };
318
+ if (value == null || typeof value !== "object") return undefined;
319
+ if (Array.isArray(value)) {
320
+ const hasNestedValue = value.some(
321
+ (item) =>
322
+ (item != null && typeof item === "object") || parseNestedJsonText(item) !== undefined,
323
+ );
324
+ if (!hasNestedValue) return undefined;
325
+ }
326
+ return {
327
+ value: value as Record<string, unknown> | unknown[],
328
+ parsedFromText: false,
329
+ };
330
+ }
331
+
107
332
  function RecordValue({ value, depth }: { value: unknown; depth: number }) {
108
333
  if (value == null) {
109
334
  return <span className="text-muted-foreground">null</span>;
@@ -112,6 +337,16 @@ function RecordValue({ value, depth }: { value: unknown; depth: number }) {
112
337
  return <span className="tabular-nums">{String(value)}</span>;
113
338
  }
114
339
  if (typeof value === "string") {
340
+ const language = detectCodeLanguage(value);
341
+ if (language === "sql") {
342
+ return (
343
+ <HighlightedCodeBlock
344
+ source={value}
345
+ language={language}
346
+ className="rounded bg-background/60 p-1.5 text-[10px]"
347
+ />
348
+ );
349
+ }
115
350
  if (looksLikeMarkdown(value)) {
116
351
  return (
117
352
  <div className={MARKDOWN_CELL_CLASSES}>
@@ -121,11 +356,23 @@ function RecordValue({ value, depth }: { value: unknown; depth: number }) {
121
356
  }
122
357
  return <span className="whitespace-pre-wrap break-words">{value}</span>;
123
358
  }
124
- if (typeof value === "object" && !Array.isArray(value) && depth < NESTED_DEPTH) {
125
- return <RecordTable rows={recordPreviewRows(value)} depth={depth + 1} />;
126
- }
127
- if (Array.isArray(value) && value.every((item) => item == null || typeof item !== "object")) {
128
- return <span className="whitespace-pre-wrap break-words">{value.map(String).join(", ")}</span>;
359
+ if (Array.isArray(value)) {
360
+ if (value.every((item) => item == null || typeof item !== "object")) {
361
+ return (
362
+ <span className="whitespace-pre-wrap break-words">{value.map(String).join(", ")}</span>
363
+ );
364
+ }
365
+ if (depth < MAX_NESTED_DEPTH) {
366
+ return (
367
+ <div className="min-w-0 divide-y divide-border/60">
368
+ {value.map((item, index) => (
369
+ <div key={index} className="min-w-0 py-1 first:pt-0 last:pb-0">
370
+ <RecordValue value={item} depth={depth + 1} />
371
+ </div>
372
+ ))}
373
+ </div>
374
+ );
375
+ }
129
376
  }
130
377
  return (
131
378
  <pre className="whitespace-pre-wrap break-words font-mono text-[11px] leading-relaxed">
@@ -0,0 +1,158 @@
1
+ import { stringUtils } from "@dbx-tools/shared-core";
2
+ import type { BundledLanguage, BundledTheme } from "shiki";
3
+ import type { HighlighterCore } from "shiki/core";
4
+ import type { CodeHighlighterPlugin } from "streamdown";
5
+
6
+ // Streamdown 2.x ships syntax highlighting as an opt-in plugin and has
7
+ // no shiki dependency of its own; without a `code` plugin every fenced
8
+ // block renders as uncolored plaintext. This module provides a small
9
+ // shiki-backed highlighter to wire in via `plugins={{ code }}`.
10
+
11
+ /**
12
+ * Languages we highlight in the chat. SQL is the primary one (Genie
13
+ * query previews); the rest cover code the assistant might emit. Kept
14
+ * to a curated set so shiki only bundles these grammars.
15
+ */
16
+ const LANGUAGES = [
17
+ "text",
18
+ "plaintext",
19
+ "txt",
20
+ "sql",
21
+ "python",
22
+ "typescript",
23
+ "javascript",
24
+ "json",
25
+ "bash",
26
+ "yaml",
27
+ "markdown",
28
+ ] as const;
29
+
30
+ /** High-contrast theme pair used for every syntax surface. */
31
+ const THEMES = [
32
+ "github-light-high-contrast",
33
+ "github-dark-high-contrast",
34
+ ] as const satisfies readonly [BundledTheme, BundledTheme];
35
+
36
+ /** Languages we can tokenize, as a set for O(1) support checks. */
37
+ const SUPPORTED = new Set<string>(LANGUAGES);
38
+
39
+ let _highlighter: HighlighterCore | null = null;
40
+ let _loading: Promise<HighlighterCore> | null = null;
41
+
42
+ /** Lazily create the shared shiki highlighter (singleton, loaded once). */
43
+ function loadHighlighter(): Promise<HighlighterCore> {
44
+ _loading ??= import("./_shiki-runtime.ts")
45
+ .then(({ createHighlighter }) => createHighlighter())
46
+ .then((highlighter) => {
47
+ _highlighter = highlighter;
48
+ return highlighter;
49
+ });
50
+ return _loading;
51
+ }
52
+
53
+ function splitThemeColor(
54
+ value: string | undefined,
55
+ darkVariable: "--shiki-dark" | "--shiki-dark-bg",
56
+ ): { light?: string; dark?: string } {
57
+ if (!value) return {};
58
+ const marker = `;${darkVariable}:`;
59
+ const index = value.indexOf(marker);
60
+ if (index < 0) return { light: value };
61
+ return {
62
+ light: value.slice(0, index),
63
+ dark: value.slice(index + marker.length),
64
+ };
65
+ }
66
+
67
+ /** Tokenize `code` with the active themes into Streamdown's result shape. */
68
+ function highlightTokens(h: HighlighterCore, code: string, language: string) {
69
+ const { tokens, fg, bg, rootStyle } = h.codeToTokens(code, {
70
+ lang: language as BundledLanguage,
71
+ themes: { light: THEMES[0], dark: THEMES[1] },
72
+ });
73
+ const foreground = splitThemeColor(fg, "--shiki-dark");
74
+ const background = splitThemeColor(bg, "--shiki-dark-bg");
75
+ const darkStyles = [
76
+ foreground.dark ? `--shiki-dark:${foreground.dark}` : "",
77
+ background.dark ? `--shiki-dark-bg:${background.dark}` : "",
78
+ ]
79
+ .filter(Boolean)
80
+ .join(";");
81
+ return {
82
+ tokens,
83
+ fg: foreground.light,
84
+ bg: background.light,
85
+ rootStyle: [rootStyle, darkStyles].filter(Boolean).join(";"),
86
+ };
87
+ }
88
+
89
+ /** Escape HTML-significant characters (from the shared string utils). */
90
+ const escapeHtml = stringUtils.escapeHtml;
91
+
92
+ /**
93
+ * Highlight `code` into minimal inline HTML: one colored `<span>` per
94
+ * token, lines joined by real newlines, with no line-number gutter or
95
+ * per-line wrapper elements. Meant to drop straight into a
96
+ * `<pre><code>` so the rendered text stays cleanly selectable and
97
+ * copyable. Falls back to plain escaped text when the language isn't
98
+ * supported or shiki fails to parse the snippet.
99
+ */
100
+ export async function highlightToHtml(code: string, language: string): Promise<string> {
101
+ if (!SUPPORTED.has(language)) return escapeHtml(code);
102
+ const h = await loadHighlighter();
103
+ try {
104
+ const { tokens } = highlightTokens(h, code, language);
105
+ return tokens
106
+ .map((line) =>
107
+ line
108
+ .map((token) => {
109
+ const light = token.htmlStyle?.color ?? token.color ?? "inherit";
110
+ const dark = token.htmlStyle?.["--shiki-dark"] ?? light;
111
+ const style = `--sdm-c:${light};--shiki-dark:${dark}`;
112
+ return `<span style="${escapeHtml(style)}">${escapeHtml(token.content)}</span>`;
113
+ })
114
+ .join(""),
115
+ )
116
+ .join("\n");
117
+ } catch {
118
+ return escapeHtml(code);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * shiki-backed code highlighter plugin for Streamdown. The highlighter
124
+ * loads asynchronously: the first call for any block returns `null`
125
+ * and resolves through the `callback` once shiki is ready, after which
126
+ * results are synchronous. Unsupported languages return `null` so the
127
+ * block stays plaintext rather than throwing.
128
+ */
129
+ export function createShikiPlugin(): CodeHighlighterPlugin {
130
+ const isSupported = (language: string): boolean => SUPPORTED.has(language);
131
+
132
+ return {
133
+ name: "shiki",
134
+ type: "code-highlighter",
135
+ getSupportedLanguages: () => [...LANGUAGES],
136
+ getThemes: () => [...THEMES],
137
+ supportsLanguage: (language) => isSupported(language),
138
+ highlight: (options, callback) => {
139
+ if (!isSupported(options.language)) return null;
140
+ const language = options.language;
141
+ if (_highlighter) {
142
+ try {
143
+ return highlightTokens(_highlighter, options.code, language);
144
+ } catch {
145
+ return null;
146
+ }
147
+ }
148
+ void loadHighlighter().then((h) => {
149
+ try {
150
+ callback?.(highlightTokens(h, options.code, language));
151
+ } catch {
152
+ // Parse failure for this block - leave it as plaintext.
153
+ }
154
+ });
155
+ return null;
156
+ },
157
+ };
158
+ }