@dbx-tools/ui 0.9.79 → 0.9.95

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.79";
6
+ export const PACKAGE_VERSION = "0.9.95";
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,6 +16,8 @@ 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 reactRecordPreview from "./src/react/record-preview.tsx";
20
+ export * as reactRecordPreviewData from "./src/react/record-preview-data.ts";
19
21
  export * as searchReactSearchBox from "./src/search/react/search-box.tsx";
20
22
  export * as searchReactSearchResults from "./src/search/react/search-results.tsx";
21
23
  export * as searchReactUseSearch from "./src/search/react/use-search.ts";
@@ -34,6 +36,8 @@ export type { EmailComposeProps } from "./src/email/react/email-compose.tsx";
34
36
  export { joinAddresses, parseAddresses, attachmentNames } from "./src/email/react/fields.ts";
35
37
  export type { EmailDraft } from "./src/email/react/fields.ts";
36
38
  export type { BrandPreset, BrandPickerProps } from "./src/react/brand-picker.tsx";
39
+ export type { RecordPreviewProps } from "./src/react/record-preview.tsx";
40
+ export type { RecordPreviewRow } from "./src/react/record-preview-data.ts";
37
41
  export type { SearchBoxProps } from "./src/search/react/search-box.tsx";
38
42
  export type { SearchResultsProps } from "./src/search/react/search-results.tsx";
39
43
  export type { UseSearchOptions, UseSearchState } from "./src/search/react/use-search.ts";
package/package.json CHANGED
@@ -30,11 +30,11 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@databricks/appkit-ui": "0.81.0",
33
- "@dbx-tools/shared-auth": "^0.9.79",
34
- "@dbx-tools/shared-core": "^0.9.79",
35
- "@dbx-tools/shared-email": "^0.9.79",
36
- "@dbx-tools/shared-email-template": "^0.9.79",
37
- "@dbx-tools/shared-search": "^0.9.79",
33
+ "@dbx-tools/shared-auth": "^0.9.95",
34
+ "@dbx-tools/shared-core": "^0.9.95",
35
+ "@dbx-tools/shared-email": "^0.9.95",
36
+ "@dbx-tools/shared-email-template": "^0.9.95",
37
+ "@dbx-tools/shared-search": "^0.9.95",
38
38
  "lucide-react": "^0.554.0",
39
39
  "streamdown": "^2.5.0",
40
40
  "tailwindcss": "^4.3.2"
@@ -43,7 +43,7 @@
43
43
  "publishConfig": {
44
44
  "access": "public"
45
45
  },
46
- "version": "0.9.79",
46
+ "version": "0.9.95",
47
47
  "dbxToolsConfig": {
48
48
  "tags": [
49
49
  "ui"
@@ -1,2 +1,3 @@
1
1
  export * from "./appkit-ui.ts";
2
2
  export * from "./brand-picker.tsx";
3
+ export * from "./record-preview.tsx";
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Flatten unknown JSON into labeled rows for {@link RecordPreview}.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { stringUtils } from "@dbx-tools/shared-core";
8
+
9
+ /** One field shown in the table view of {@link RecordPreview}. */
10
+ export interface RecordPreviewRow {
11
+ /** Original object key, or `value` for a non-object payload. */
12
+ key: string;
13
+ /** Title-cased label from {@link stringUtils.toLabel}. */
14
+ label: string;
15
+ /** Raw field value, including nested objects and arrays. */
16
+ value: unknown;
17
+ }
18
+
19
+ /** Walk a JSON value into humanized key/value rows. */
20
+ export function recordPreviewRows(value: unknown): RecordPreviewRow[] {
21
+ if (value == null) return [];
22
+ if (typeof value !== "object" || Array.isArray(value)) {
23
+ return [{ key: "value", label: "Value", value }];
24
+ }
25
+ return Object.entries(value as Record<string, unknown>).map(([key, nested]) => ({
26
+ key,
27
+ label: stringUtils.toLabel(key),
28
+ value: nested,
29
+ }));
30
+ }
31
+
32
+ /** Pretty-print JSON for the raw view; strings stay as authored. */
33
+ export function formatRecordPreviewJson(value: unknown): string {
34
+ if (typeof value === "string") return value;
35
+ try {
36
+ return JSON.stringify(value, null, 2) ?? String(value);
37
+ } catch {
38
+ return String(value);
39
+ }
40
+ }
41
+
42
+ /** YAML document fence at the start of a markdown file (`---` ... `---`). */
43
+ const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
44
+
45
+ /**
46
+ * Structural markdown markers: ATX headings, fences, quotes, lists, and
47
+ * `[text](url)` links. Requires a space after `#` so paths and hex colors
48
+ * are not treated as headings.
49
+ */
50
+ const MARKDOWN_MARKERS = [
51
+ /^#{1,6}\s+\S/m,
52
+ /^```/m,
53
+ /^>\s+\S/m,
54
+ /^\d+\.\s+\S/m,
55
+ /^[-*+]\s+\S/m,
56
+ /\[[^\]]+\]\([^)\s]+\)/,
57
+ ] as const;
58
+
59
+ /** True when a string looks like markdown rather than a plain label or path. */
60
+ export function looksLikeMarkdown(value: string): boolean {
61
+ const text = value.trim();
62
+ if (text.length < 8) return false;
63
+ if (FRONTMATTER.test(text)) return true;
64
+ return MARKDOWN_MARKERS.some((pattern) => pattern.test(text));
65
+ }
66
+
67
+ /**
68
+ * Rewrite leading YAML frontmatter as a fenced `yaml` block so Streamdown
69
+ * does not turn the `---` fences into horizontal rules.
70
+ */
71
+ export function markdownForPreview(value: string): string {
72
+ const trimmed = value.trimStart();
73
+ const match = FRONTMATTER.exec(trimmed);
74
+ if (!match) return value;
75
+ const yaml = match[1]?.trimEnd() ?? "";
76
+ const rest = trimmed.slice(match[0].length);
77
+ return `\`\`\`yaml\n${yaml}\n\`\`\`\n\n${rest}`;
78
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Humanized key/value table for arbitrary JSON, with an icon toggle to
3
+ * the raw document.
4
+ *
5
+ * @module
6
+ */
7
+
8
+ import { BracesIcon, Table2Icon } from "lucide-react";
9
+ import { useState } from "react";
10
+ import { Streamdown } from "streamdown";
11
+
12
+ import {
13
+ Button,
14
+ Table,
15
+ TableBody,
16
+ TableCell,
17
+ TableHead,
18
+ TableRow,
19
+ Tooltip,
20
+ TooltipContent,
21
+ TooltipTrigger,
22
+ cn,
23
+ } from "./appkit-ui.ts";
24
+ import {
25
+ formatRecordPreviewJson,
26
+ looksLikeMarkdown,
27
+ markdownForPreview,
28
+ recordPreviewRows,
29
+ type RecordPreviewRow,
30
+ } from "./record-preview-data.ts";
31
+
32
+ export type { RecordPreviewRow } from "./record-preview-data.ts";
33
+ export {
34
+ formatRecordPreviewJson,
35
+ looksLikeMarkdown,
36
+ markdownForPreview,
37
+ recordPreviewRows,
38
+ } from "./record-preview-data.ts";
39
+
40
+ /** Compact Streamdown in a table cell: match 11px rows, keep lists tight. */
41
+ const MARKDOWN_CELL_CLASSES =
42
+ "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
+
44
+ const NESTED_DEPTH = 2;
45
+
46
+ /** Props for {@link RecordPreview}. */
47
+ export interface RecordPreviewProps {
48
+ /** JSON-shaped value to display. */
49
+ value: unknown;
50
+ /** Extra classes on the outer frame. */
51
+ className?: string;
52
+ }
53
+
54
+ /** Compact labeled table of a JSON object, with a JSON/raw icon toggle. */
55
+ export function RecordPreview({ value, className }: RecordPreviewProps) {
56
+ const [raw, setRaw] = useState(false);
57
+ const rows = recordPreviewRows(value);
58
+ 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>
76
+ {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)}
79
+ </pre>
80
+ ) : (
81
+ <RecordTable rows={rows} depth={0} />
82
+ )}
83
+ </div>
84
+ );
85
+ }
86
+
87
+ function RecordTable({ rows, depth }: { rows: RecordPreviewRow[]; depth: number }) {
88
+ if (rows.length === 0) return null;
89
+ return (
90
+ <Table className="text-[11px]">
91
+ <TableBody>
92
+ {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>
101
+ ))}
102
+ </TableBody>
103
+ </Table>
104
+ );
105
+ }
106
+
107
+ function RecordValue({ value, depth }: { value: unknown; depth: number }) {
108
+ if (value == null) {
109
+ return <span className="text-muted-foreground">null</span>;
110
+ }
111
+ if (typeof value === "boolean" || typeof value === "number") {
112
+ return <span className="tabular-nums">{String(value)}</span>;
113
+ }
114
+ if (typeof value === "string") {
115
+ if (looksLikeMarkdown(value)) {
116
+ return (
117
+ <div className={MARKDOWN_CELL_CLASSES}>
118
+ <Streamdown controls={false}>{markdownForPreview(value)}</Streamdown>
119
+ </div>
120
+ );
121
+ }
122
+ return <span className="whitespace-pre-wrap break-words">{value}</span>;
123
+ }
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>;
129
+ }
130
+ return (
131
+ <pre className="whitespace-pre-wrap break-words font-mono text-[11px] leading-relaxed">
132
+ {formatRecordPreviewJson(value)}
133
+ </pre>
134
+ );
135
+ }