@tangle-network/ui 11.11.1 → 11.11.3

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +9 -1
  3. package/dist/{chat-D4jAfEmj.js → chat-BUJhNpx2.js} +11 -6
  4. package/dist/chat.js +1 -1
  5. package/dist/{document-editor-pane-BcoABlhn.d.ts → document-editor-pane-DHM8DDhb.d.ts} +4 -1
  6. package/dist/{document-editor-pane-BxlPjaTP.js → document-editor-pane-LyanNypc.js} +6 -14
  7. package/dist/editor.d.ts +1 -1
  8. package/dist/editor.js +1 -1
  9. package/dist/{files-Bug33qCl.js → files-DgPc3vI2.js} +16 -7
  10. package/dist/files.d.ts +1 -1
  11. package/dist/files.js +1 -1
  12. package/dist/{index-DhYmSaLF.d.ts → index-CyGSLGVF.d.ts} +6 -17
  13. package/dist/index.d.ts +3 -2
  14. package/dist/index.js +6 -5
  15. package/dist/{openui-artifact-renderer-DrU1AdVk.js → openui-artifact-renderer-C3bmqfjX.js} +15 -20
  16. package/dist/openui-schema.d.ts +3 -0
  17. package/dist/openui-schema.js +2 -0
  18. package/dist/openui.d.ts +2 -1
  19. package/dist/openui.js +3 -2
  20. package/dist/{run-group-BACLbCFl.js → run-group-CxNskmHd.js} +1 -1
  21. package/dist/run.js +1 -1
  22. package/dist/schema-BFKgCGH-.d.ts +228 -0
  23. package/dist/schema-BpW9rSSG.js +9754 -0
  24. package/package.json +9 -2
  25. package/src/chat/agent-timeline.stories.tsx +15 -0
  26. package/src/chat/agent-timeline.tsx +10 -5
  27. package/src/editor/document-editor-pane.tsx +5 -1
  28. package/src/files/file-artifact-pane.test.tsx +41 -0
  29. package/src/files/file-artifact-pane.tsx +3 -0
  30. package/src/files/file-preview.tsx +16 -3
  31. package/src/openui/index.ts +10 -0
  32. package/src/openui/openui-artifact-renderer.stories.tsx +36 -0
  33. package/src/openui/openui-artifact-renderer.tsx +19 -27
  34. package/src/openui/schema-compiled.ts +3 -0
  35. package/src/openui/schema-data.ts +95 -0
  36. package/src/openui/schema.test.tsx +74 -0
  37. package/src/openui/schema.ts +136 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/ui",
3
- "version": "11.11.1",
3
+ "version": "11.11.3",
4
4
  "description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -47,6 +47,11 @@
47
47
  "import": "./dist/openui.js",
48
48
  "default": "./dist/openui.js"
49
49
  },
50
+ "./openui-schema": {
51
+ "types": "./dist/openui-schema.d.ts",
52
+ "import": "./dist/openui-schema.js",
53
+ "default": "./dist/openui-schema.js"
54
+ },
50
55
  "./files": {
51
56
  "types": "./dist/files.d.ts",
52
57
  "import": "./dist/files.js",
@@ -191,6 +196,7 @@
191
196
  "@types/react": "^19.3.0",
192
197
  "@types/react-syntax-highlighter": "^15.5.13",
193
198
  "@types/turndown": "^5.0.6",
199
+ "ajv": "8.17.1",
194
200
  "nanostores": "^1.5.4",
195
201
  "react": "^19.3.0",
196
202
  "react-dom": "^19.3.0",
@@ -207,7 +213,8 @@
207
213
  "design-system"
208
214
  ],
209
215
  "scripts": {
210
- "build": "tsdown && node ../../scripts/validate-dist.mjs",
216
+ "build": "node scripts/generate-openui-validators.mjs --check && tsdown && node ../../scripts/validate-dist.mjs && node scripts/probe-openui-worker.mjs",
217
+ "gen:openui-schema": "node scripts/generate-openui-validators.mjs",
211
218
  "dev": "tsdown --watch"
212
219
  }
213
220
  }
@@ -450,6 +450,21 @@ export const StatusTones: Story = {
450
450
  },
451
451
  }
452
452
 
453
+ export const ErrorArtifact: Story = {
454
+ args: {
455
+ items: [
456
+ {
457
+ id: 'artifact-error',
458
+ kind: 'artifact',
459
+ title: 'Build output',
460
+ description: 'The compiler stopped before writing the bundle.',
461
+ meta: 'Exit code 1',
462
+ tone: 'error',
463
+ },
464
+ ],
465
+ },
466
+ }
467
+
453
468
  // ---------------------------------------------------------------------------
454
469
  // Full transcript through ChatContainer — the real consumer path. Tool titles
455
470
  // and descriptions come from `getToolDisplayMetadata`, so this story shows the
@@ -101,30 +101,35 @@ export interface AgentTimelineProps {
101
101
  collapseAfter?: number;
102
102
  }
103
103
 
104
- const TONE_STYLES: Record<AgentTimelineTone, { card: string; text: string; icon: typeof Info }> = {
104
+ const TONE_STYLES: Record<AgentTimelineTone, { card: string; text: string; supportingText: string; icon: typeof Info }> = {
105
105
  default: {
106
106
  card: "border-border bg-card",
107
107
  text: "text-foreground",
108
+ supportingText: "text-muted-foreground",
108
109
  icon: CircleDot,
109
110
  },
110
111
  info: {
111
112
  card: "border-[var(--surface-info-border)] bg-[var(--surface-info-bg)]",
112
113
  text: "text-[var(--surface-info-text)]",
114
+ supportingText: "text-[var(--surface-info-text)]",
113
115
  icon: Info,
114
116
  },
115
117
  success: {
116
118
  card: "border-[var(--surface-success-border)] bg-[var(--surface-success-bg)]",
117
119
  text: "text-[var(--surface-success-text)]",
120
+ supportingText: "text-[var(--surface-success-text)]",
118
121
  icon: CheckCircle2,
119
122
  },
120
123
  warning: {
121
124
  card: "border-[var(--surface-warning-border)] bg-[var(--surface-warning-bg)]",
122
125
  text: "text-[var(--surface-warning-text)]",
126
+ supportingText: "text-[var(--surface-warning-text)]",
123
127
  icon: AlertTriangle,
124
128
  },
125
129
  error: {
126
130
  card: "border-[var(--surface-danger-border)] bg-[var(--surface-danger-bg)]",
127
131
  text: "text-[var(--surface-danger-text)]",
132
+ supportingText: "text-[var(--surface-danger-text)]",
128
133
  icon: AlertTriangle,
129
134
  },
130
135
  };
@@ -191,7 +196,7 @@ function StatusCard({ item }: { item: AgentTimelineStatusItem }) {
191
196
  <div className="min-w-0">
192
197
  <div className={cn("text-sm font-medium", tone.text)}>{item.label}</div>
193
198
  {item.detail && (
194
- <div className="mt-0.5 text-sm text-muted-foreground">{item.detail}</div>
199
+ <div className={cn("mt-0.5 text-sm", tone.supportingText)}>{item.detail}</div>
195
200
  )}
196
201
  </div>
197
202
  </div>
@@ -208,12 +213,12 @@ function ArtifactCard({ item }: { item: AgentTimelineArtifactItem }) {
208
213
  {item.icon ?? <FileText className="h-4 w-4" />}
209
214
  </div>
210
215
  <div className="min-w-0 flex-1">
211
- <div className="text-sm font-medium text-foreground">{item.title}</div>
216
+ <div className={cn("text-sm font-medium", tone.text)}>{item.title}</div>
212
217
  {item.description && (
213
- <div className="mt-1 text-sm text-muted-foreground">{item.description}</div>
218
+ <div className={cn("mt-1 text-sm", tone.supportingText)}>{item.description}</div>
214
219
  )}
215
220
  {item.meta && (
216
- <div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
221
+ <div className={cn("mt-2 flex flex-wrap items-center gap-2 text-xs", tone.supportingText)}>
217
222
  {item.meta}
218
223
  </div>
219
224
  )}
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { type ReactNode, useEffect, useMemo, useState } from "react";
4
+ import type { UrlTransform } from "react-markdown";
4
5
  import { PencilLine, Save, Users, Wifi, WifiOff } from "lucide-react";
5
6
  import { Markdown } from "../markdown/markdown";
6
7
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../primitives/tabs";
@@ -36,6 +37,8 @@ export interface DocumentEditorPaneProps
36
37
  tabs?: ArtifactPaneProps["tabs"];
37
38
  toolbar?: ReactNode;
38
39
  markdown?: string;
40
+ /** Transform Markdown links and images through the shared URL policy. */
41
+ urlTransform?: UrlTransform;
39
42
  mode?: DocumentEditorMode;
40
43
  defaultMode?: DocumentEditorMode;
41
44
  onModeChange?: (mode: DocumentEditorMode) => void;
@@ -188,6 +191,7 @@ export function DocumentEditorPane({
188
191
  tabs,
189
192
  toolbar,
190
193
  markdown = "",
194
+ urlTransform,
191
195
  mode,
192
196
  defaultMode = "preview",
193
197
  onModeChange,
@@ -300,7 +304,7 @@ export function DocumentEditorPane({
300
304
  previewClassName,
301
305
  )}
302
306
  >
303
- <Markdown>{draft}</Markdown>
307
+ <Markdown urlTransform={urlTransform}>{draft}</Markdown>
304
308
  </div>
305
309
  );
306
310
 
@@ -113,6 +113,47 @@ describe("FileArtifactPane previews", () => {
113
113
  expect(container.querySelector("strong")).toHaveTextContent("bold");
114
114
  });
115
115
 
116
+ it("forwards Markdown URL transforms to file and rich editor previews", async () => {
117
+ const urlTransform = vi.fn((url: string, key: string) => (
118
+ key === "href" && url === "sibling.md"
119
+ ? "/app/ws-1/vault?file=docs%2Fsibling.md"
120
+ : url
121
+ ));
122
+ const readOnly = render(
123
+ <FileArtifactPane
124
+ filename="README.md"
125
+ content={"[Sibling](sibling.md)"}
126
+ urlTransform={urlTransform}
127
+ />,
128
+ );
129
+
130
+ expect(screen.getByRole("link", { name: "Sibling" })).toHaveAttribute(
131
+ "href",
132
+ "/app/ws-1/vault?file=docs%2Fsibling.md",
133
+ );
134
+ readOnly.unmount();
135
+
136
+ render(
137
+ <FileArtifactPane
138
+ path="docs/README.md"
139
+ filename="README.md"
140
+ content={"[Sibling](sibling.md)"}
141
+ urlTransform={urlTransform}
142
+ editor={{ enabled: true, defaultMode: "preview" }}
143
+ />,
144
+ );
145
+
146
+ expect(await screen.findByRole("link", { name: "Sibling" })).toHaveAttribute(
147
+ "href",
148
+ "/app/ws-1/vault?file=docs%2Fsibling.md",
149
+ );
150
+ expect(urlTransform).toHaveBeenCalledWith(
151
+ "sibling.md",
152
+ "href",
153
+ expect.objectContaining({ tagName: "a" }),
154
+ );
155
+ });
156
+
116
157
  it("renders a binary file with no download link as one muted sentence", () => {
117
158
  const onDownload = vi.fn();
118
159
  const withDownload = render(
@@ -64,6 +64,7 @@ export function FileArtifactPane({
64
64
  size,
65
65
  onClose,
66
66
  onDownload,
67
+ urlTransform,
67
68
  path,
68
69
  tabs = [],
69
70
  activeTabId,
@@ -154,6 +155,7 @@ export function FileArtifactPane({
154
155
  headerClassName={headerClassName}
155
156
  hideTitleBlock={hideTitleBlock}
156
157
  markdown={content ?? ""}
158
+ urlTransform={urlTransform}
157
159
  mode={editor.mode}
158
160
  defaultMode={editor.defaultMode}
159
161
  onModeChange={editor.onModeChange}
@@ -193,6 +195,7 @@ export function FileArtifactPane({
193
195
  blobUrl={blobUrl}
194
196
  mimeType={mimeType}
195
197
  size={size}
198
+ urlTransform={urlTransform}
196
199
  onDownload={onDownload}
197
200
  hideHeader={true}
198
201
  />
@@ -22,6 +22,7 @@ import {
22
22
  X,
23
23
  type LucideIcon,
24
24
  } from "lucide-react";
25
+ import type { UrlTransform } from "react-markdown";
25
26
  import { focusRingInset } from "../lib/focus";
26
27
  import { cn } from "../lib/utils";
27
28
  import { Button } from "../primitives/button";
@@ -44,6 +45,8 @@ export interface FilePreviewProps {
44
45
  mimeType?: string;
45
46
  /** Size in bytes; download cards show it when known. */
46
47
  size?: number;
48
+ /** Transform Markdown links and images through the shared URL policy. */
49
+ urlTransform?: UrlTransform;
47
50
  onClose?: () => void;
48
51
  onDownload?: () => void;
49
52
  hideHeader?: boolean;
@@ -301,10 +304,16 @@ function TextPreview({ content }: { content: string }) {
301
304
  );
302
305
  }
303
306
 
304
- function MarkdownPreview({ content }: { content: string }) {
307
+ function MarkdownPreview({
308
+ content,
309
+ urlTransform,
310
+ }: {
311
+ content: string;
312
+ urlTransform?: UrlTransform;
313
+ }) {
305
314
  return (
306
315
  <div className="rounded-[var(--radius-md)] border border-border bg-background p-5">
307
- <Markdown>{content}</Markdown>
316
+ <Markdown urlTransform={urlTransform}>{content}</Markdown>
308
317
  </div>
309
318
  );
310
319
  }
@@ -358,6 +367,7 @@ function PreviewBody({
358
367
  content,
359
368
  blobUrl,
360
369
  size,
370
+ urlTransform,
361
371
  onDownload,
362
372
  }: {
363
373
  kind: FilePreviewKind;
@@ -365,6 +375,7 @@ function PreviewBody({
365
375
  content?: string;
366
376
  blobUrl?: string;
367
377
  size?: number;
378
+ urlTransform?: UrlTransform;
368
379
  onDownload?: () => void;
369
380
  }) {
370
381
  const hasText = typeof content === "string";
@@ -404,7 +415,7 @@ function PreviewBody({
404
415
  if (hasText) return <CodePreview content={content} filename={filename} kind={kind} />;
405
416
  return card(NO_INLINE_CONTENT);
406
417
  case "markdown":
407
- if (hasText) return <MarkdownPreview content={content} />;
418
+ if (hasText) return <MarkdownPreview content={content} urlTransform={urlTransform} />;
408
419
  return card(NO_INLINE_CONTENT);
409
420
  case "text":
410
421
  if (hasText) return <TextPreview content={content} />;
@@ -423,6 +434,7 @@ export function FilePreview({
423
434
  blobUrl,
424
435
  mimeType,
425
436
  size,
437
+ urlTransform,
426
438
  onClose,
427
439
  onDownload,
428
440
  hideHeader = false,
@@ -473,6 +485,7 @@ export function FilePreview({
473
485
  content={content}
474
486
  blobUrl={blobUrl}
475
487
  size={size}
488
+ urlTransform={urlTransform}
476
489
  onDownload={onDownload}
477
490
  />
478
491
  </div>
@@ -18,3 +18,13 @@ export {
18
18
  type OpenUIGridNode,
19
19
  type OpenUICardNode,
20
20
  } from "./openui-artifact-renderer";
21
+
22
+ export {
23
+ OPENUI_NODE_TYPES,
24
+ OPENUI_NODE_JSON_SCHEMA,
25
+ findUnsupportedOpenUINode,
26
+ validateOpenUIJsonNode,
27
+ validateOpenUIJsonArtifact,
28
+ type OpenUISchemaIssue,
29
+ type OpenUISchemaValidation,
30
+ } from "./schema";
@@ -0,0 +1,36 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { OpenUIArtifactRenderer } from "./openui-artifact-renderer";
3
+
4
+ const meta: Meta<typeof OpenUIArtifactRenderer> = {
5
+ title: "OpenUI/ArtifactRenderer",
6
+ component: OpenUIArtifactRenderer,
7
+ decorators: [(Story) => <div className="mx-auto max-w-2xl p-6"><Story /></div>],
8
+ };
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof OpenUIArtifactRenderer>;
12
+
13
+ export const NestedCard: Story = {
14
+ args: {
15
+ schema: {
16
+ type: "card",
17
+ title: "Release proof",
18
+ children: [
19
+ { type: "heading", text: "Result" },
20
+ { type: "stack", children: [
21
+ { type: "stat", label: "Checks", value: "3" },
22
+ { type: "text", text: "The saved result remains visible after reload." },
23
+ ] },
24
+ ],
25
+ },
26
+ },
27
+ };
28
+
29
+ export const UnsupportedSavedNode: Story = {
30
+ args: {
31
+ schema: {
32
+ type: "card",
33
+ children: [{ type: "section", children: [{ type: "text", text: "This content must not disappear silently." }] }],
34
+ } as never,
35
+ },
36
+ };
@@ -13,6 +13,7 @@ import {
13
13
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../primitives/table";
14
14
  import { CodeBlock } from "../primitives/code-block";
15
15
  import { Markdown } from "../markdown/markdown";
16
+ import { findUnsupportedOpenUINode, type OPENUI_NODE_TYPES } from "./schema";
16
17
 
17
18
  export type OpenUIPrimitive = string | number | boolean | null | undefined;
18
19
 
@@ -148,21 +149,14 @@ export interface OpenUIArtifactRendererProps {
148
149
  className?: string;
149
150
  }
150
151
 
151
- const NODE_TYPES = new Set([
152
- "heading",
153
- "text",
154
- "badge",
155
- "stat",
156
- "key_value",
157
- "code",
158
- "markdown",
159
- "table",
160
- "actions",
161
- "separator",
162
- "stack",
163
- "grid",
164
- "card",
165
- ]);
152
+ type RendererNodeTypesMatchSchema =
153
+ Exclude<OpenUIComponentNode["type"], (typeof OPENUI_NODE_TYPES)[number]> extends never
154
+ ? Exclude<(typeof OPENUI_NODE_TYPES)[number], OpenUIComponentNode["type"]> extends never
155
+ ? true
156
+ : never
157
+ : never;
158
+ const rendererNodeTypesMatchSchema: RendererNodeTypesMatchSchema = true;
159
+ void rendererNodeTypesMatchSchema;
166
160
 
167
161
  const GAP_STYLES = {
168
162
  sm: "gap-2",
@@ -200,16 +194,6 @@ function asArray<T>(value: unknown): T[] {
200
194
  return Array.isArray(value) ? (value as T[]) : [];
201
195
  }
202
196
 
203
- function isOpenUIComponentNode(value: unknown): value is OpenUIComponentNode {
204
- return (
205
- typeof value === "object" &&
206
- value !== null &&
207
- "type" in value &&
208
- typeof (value as { type?: unknown }).type === "string" &&
209
- NODE_TYPES.has((value as { type: string }).type)
210
- );
211
- }
212
-
213
197
  function renderActions(actions: OpenUIAction[], onAction?: (action: OpenUIAction) => void) {
214
198
  if (actions.length === 0) return null;
215
199
 
@@ -394,7 +378,7 @@ function renderNode(node: OpenUIComponentNode, onAction?: (action: OpenUIAction)
394
378
  column.align === "right" && "text-right tabular-nums",
395
379
  )}
396
380
  >
397
- {formatValue(row[column.key])}
381
+ {formatValue(Object.prototype.hasOwnProperty.call(row, column.key) ? row[column.key] : undefined)}
398
382
  </TableCell>
399
383
  ))}
400
384
  </TableRow>
@@ -510,7 +494,15 @@ export function OpenUIArtifactRenderer({
510
494
  onAction,
511
495
  className,
512
496
  }: OpenUIArtifactRendererProps) {
513
- const nodes = (Array.isArray(schema) ? schema : [schema]).filter(isOpenUIComponentNode);
497
+ const issue = findUnsupportedOpenUINode(schema);
498
+ if (issue) {
499
+ return (
500
+ <div role="alert" className={cn("rounded-[var(--radius-xl)] border border-border bg-card p-5 text-sm text-foreground", className)}>
501
+ Cannot display this saved view. {issue.message} Path: {issue.path}.
502
+ </div>
503
+ );
504
+ }
505
+ const nodes = Array.isArray(schema) ? schema : [schema];
514
506
 
515
507
  if (nodes.length === 0) {
516
508
  return (