From 8a34649bc86010edb962fdaeb4779973e23bff66 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Wed, 22 Jul 2026 06:02:10 +0000 Subject: [PATCH 1/3] feat(kit): adopt @devframes/json-render for the json-render layer devframe 0.7.6 extracts json-render out of @devframes/hub into the opt-in @devframes/json-render package, so the hub no longer ships createJsonRenderer, defineJsonRenderSpec, or the JsonRender* types. Rewire the kit onto the new package while preserving the kit's public API: - ctx.createJsonRenderer(spec) is now backed by createJsonRenderView; the returned handle keeps updateSpec/updateState/_stateKey and defines its methods non-enumerably so it stays serializable on a json-render dock entry. - Re-map the JsonRenderSpec/JsonRenderElement types onto @devframes/json-render and register the kit's json-render dock entry into the hub dock registry. - Provide the client-side dock-renderer registry now required by the hub 0.7.6 DocksContext. The in-house webcomponents registry stays (no @devframes/json-render-ui), but its components now speak the @devframes/json-render base-catalog vocabulary, since createJsonRenderView validates element props against that catalog at ingress. Update the git-ui example, docs, and stories to the base prop names. Created with the help of an agent. --- docs/kit/dock-system.md | 12 +- docs/kit/json-render.md | 114 +++++++------ examples/plugin-git-ui/src/node/spec.ts | 36 ++--- .../json-render/JsonRender.stories.ts | 28 ++-- .../webcomponents/json-render/catalog.ts | 153 ------------------ .../json-render/components/Badge.ts | 4 +- .../json-render/components/CodeBlock.ts | 16 +- .../json-render/components/DataTable.ts | 10 +- .../json-render/components/KeyValueTable.ts | 12 +- .../json-render/components/Stack.ts | 13 +- .../json-render/components/Text.ts | 11 +- .../json-render/components/Tree.ts | 5 +- .../json-render/components/tokens.ts | 2 +- .../client/webcomponents/json-render/index.ts | 2 - .../src/client/webcomponents/state/context.ts | 2 + .../client/webcomponents/state/renderers.ts | 51 ++++++ packages/kit/package.json | 1 + packages/kit/src/client/docks.ts | 4 + packages/kit/src/define.ts | 14 +- packages/kit/src/node/context.ts | 62 ++++++- packages/kit/src/types/docks.ts | 26 ++- packages/kit/src/types/json-render.ts | 49 +++++- pnpm-workspace.yaml | 11 +- .../@vitejs/devtools-kit/client.snapshot.d.ts | 4 + .../@vitejs/devtools-kit/index.snapshot.d.ts | 26 ++- .../@vitejs/devtools-kit/index.snapshot.js | 5 +- .../@vitejs/devtools-kit/node.snapshot.d.ts | 1 + 27 files changed, 380 insertions(+), 294 deletions(-) delete mode 100644 packages/core/src/client/webcomponents/json-render/catalog.ts delete mode 100644 packages/core/src/client/webcomponents/json-render/index.ts create mode 100644 packages/core/src/client/webcomponents/state/renderers.ts diff --git a/docs/kit/dock-system.md b/docs/kit/dock-system.md index b2b86daff..7c94e18f2 100644 --- a/docs/kit/dock-system.md +++ b/docs/kit/dock-system.md @@ -407,20 +407,20 @@ const ui = ctx.createJsonRenderer({ elements: { root: { type: 'Stack', - props: { direction: 'vertical', gap: 12 }, + props: { direction: 'column', gap: 12 }, children: ['heading', 'info'], }, heading: { type: 'Text', - props: { content: 'Hello from JSON!', variant: 'heading' }, + props: { text: 'Hello from JSON!', variant: 'heading' }, }, info: { type: 'KeyValueTable', props: { - entries: [ - { key: 'Version', value: '1.0.0' }, - { key: 'Status', value: 'Running' }, - ], + data: { + Version: '1.0.0', + Status: 'Running', + }, }, }, }, diff --git a/docs/kit/json-render.md b/docs/kit/json-render.md index 619961847..5a0c20990 100644 --- a/docs/kit/json-render.md +++ b/docs/kit/json-render.md @@ -23,20 +23,20 @@ export function MyPlugin(): PluginWithDevTools { elements: { root: { type: 'Stack', - props: { direction: 'vertical', gap: 12 }, + props: { direction: 'column', gap: 12 }, children: ['heading', 'info'], }, heading: { type: 'Text', - props: { content: 'Hello from JSON!', variant: 'heading' }, + props: { text: 'Hello from JSON!', variant: 'heading' }, }, info: { type: 'KeyValueTable', props: { - entries: [ - { key: 'Version', value: '1.0.0' }, - { key: 'Status', value: 'Running' }, - ], + data: { + Version: '1.0.0', + Status: 'Running', + }, }, }, }, @@ -68,16 +68,16 @@ ctx.createJsonRenderer({ elements: { root: { type: 'Stack', - props: { direction: 'vertical', gap: 12 }, + props: { direction: 'column', gap: 12 }, children: ['title', 'content'], }, title: { type: 'Text', - props: { content: 'My Panel', variant: 'heading' }, + props: { text: 'My Panel', variant: 'heading' }, }, content: { type: 'Text', - props: { content: 'Hello world' }, + props: { text: 'Hello world' }, }, }, }) @@ -175,7 +175,7 @@ const ui = ctx.createJsonRenderer({ elements: { root: { type: 'Stack', - props: { direction: 'horizontal', gap: 8 }, + props: { direction: 'row', gap: 8 }, children: ['input', 'submit'], }, input: { @@ -223,10 +223,12 @@ Flex layout container. Arranges children vertically or horizontally. | Prop | Type | Default | Description | |------|------|---------|-------------| -| `direction` | `'vertical' \| 'horizontal'` | `'vertical'` | Layout direction | +| `direction` | `'row' \| 'column'` | `'column'` | Layout direction | | `gap` | `number` | `8` | Gap between children in pixels | | `align` | `'start' \| 'center' \| 'end' \| 'stretch'` | — | Cross-axis alignment | -| `justify` | `'start' \| 'center' \| 'end' \| 'space-between' \| 'space-around'` | — | Main-axis alignment | +| `justify` | `'start' \| 'center' \| 'end' \| 'between' \| 'around'` | — | Main-axis alignment | +| `wrap` | `boolean` | `false` | Allow children to wrap onto multiple lines | +| `flex` | `number \| string` | — | `flex` shorthand for the container | | `padding` | `number` | — | Padding in pixels | @@ -234,7 +236,7 @@ Flex layout container. Arranges children vertically or horizontally. // Horizontal toolbar with items spaced apart { type: 'Stack', - props: { direction: 'horizontal', gap: 8, justify: 'space-between', align: 'center' }, + props: { direction: 'row', gap: 8, justify: 'between', align: 'center' }, children: ['title', 'actions'], } ``` @@ -244,7 +246,7 @@ Flex layout container. Arranges children vertically or horizontally. // Vertical form layout { type: 'Stack', - props: { direction: 'vertical', gap: 12, padding: 16 }, + props: { direction: 'column', gap: 12, padding: 16 }, children: ['name-input', 'email-input', 'submit-btn'], } ``` @@ -257,6 +259,8 @@ Container with an optional title and collapsible behavior. |------|------|---------|-------------| | `title` | `string` | — | Header title | | `collapsible` | `boolean` | `false` | Whether the card can be collapsed | +| `defaultCollapsed` | `boolean` | `false` | Start collapsed (when `collapsible`) | +| `loading` | `boolean` | `false` | Show a loading state | ```ts @@ -291,22 +295,24 @@ Display text with different visual styles. | Prop | Type | Default | Description | |------|------|---------|-------------| -| `content` | `string` | — | Text content | -| `variant` | `'heading' \| 'body' \| 'caption' \| 'code'` | `'body'` | Visual style | +| `text` | `string` | — | Text content | +| `variant` | `'heading' \| 'subheading' \| 'body' \| 'caption' \| 'code'` | `'body'` | Visual style | +| `weight` | `'normal' \| 'medium' \| 'bold'` | — | Font weight | +| `color` | `'base' \| 'muted' \| 'faint' \| 'primary' \| 'success' \| 'warning' \| 'danger'` | — | Text color | ```ts // heading — 16px bold -{ type: 'Text', props: { content: 'Module Graph', variant: 'heading' } } +{ type: 'Text', props: { text: 'Module Graph', variant: 'heading' } } // body (default) — 13px -{ type: 'Text', props: { content: 'Visualize module dependencies' } } +{ type: 'Text', props: { text: 'Visualize module dependencies' } } // caption — 12px, muted -{ type: 'Text', props: { content: 'Click a node to inspect', variant: 'caption' } } +{ type: 'Text', props: { text: 'Click a node to inspect', variant: 'caption' } } // code — monospace with background -{ type: 'Text', props: { content: 'src/index.ts', variant: 'code' } } +{ type: 'Text', props: { text: 'src/index.ts', variant: 'code' } } ``` #### Icon @@ -330,13 +336,14 @@ Status label with semantic color variants. | Prop | Type | Default | Description | |------|------|---------|-------------| | `text` | `string` | — | Badge text | -| `variant` | `'info' \| 'success' \| 'warning' \| 'error' \| 'default'` | `'default'` | Color variant | +| `variant` | `'default' \| 'info' \| 'success' \| 'warning' \| 'danger'` | `'default'` | Color variant | +| `minWidth` | `number` | — | Minimum width in pixels | ```ts { type: 'Badge', props: { text: 'Ready', variant: 'success' } } { type: 'Badge', props: { text: '3 warnings', variant: 'warning' } } -{ type: 'Badge', props: { text: 'Failed', variant: 'error' } } +{ type: 'Badge', props: { text: 'Failed', variant: 'danger' } } ``` ### Inputs @@ -351,6 +358,7 @@ Clickable button that triggers an action via the `press` event. | `icon` | `string` | — | Iconify icon name | | `variant` | `'primary' \| 'secondary' \| 'ghost' \| 'danger'` | `'secondary'` | Visual style | | `disabled` | `boolean` | `false` | Disable interaction | +| `loading` | `boolean` | `false` | Show a loading state | **Event**: `press` — fires when the button is clicked. @@ -375,7 +383,9 @@ Text input field with optional two-way state binding. | `placeholder` | `string` | — | Placeholder text | | `value` | `string` | — | Current value (use `$bindState` for two-way binding) | | `label` | `string` | — | Label shown above the input | +| `type` | `'text' \| 'search' \| 'number' \| 'password' \| 'email'` | `'text'` | Input type | | `disabled` | `boolean` | `false` | Disable interaction | +| `loading` | `boolean` | `false` | Show a loading state | ```ts @@ -398,21 +408,20 @@ Display key-value pairs in a two-column table. | Prop | Type | Default | Description | |------|------|---------|-------------| -| `title` | `string` | — | Optional header | -| `entries` | `Array<{ key: string, value: string }>` | — | Key-value pairs | +| `data` | `Record` | — | Key-value pairs to display | +| `loading` | `boolean` | `false` | Show a loading state | ```ts { type: 'KeyValueTable', props: { - title: 'Build Info', - entries: [ - { key: 'Mode', value: 'production' }, - { key: 'Duration', value: '1.2s' }, - { key: 'Modules', value: '142' }, - { key: 'Output', value: 'dist/' }, - ], + data: { + Mode: 'production', + Duration: '1.2s', + Modules: '142', + Output: 'dist/', + }, }, } ``` @@ -423,9 +432,10 @@ Tabular data with configurable columns and scroll support. | Prop | Type | Default | Description | |------|------|---------|-------------| -| `columns` | `Array<{ key: string, label: string, width?: string }>` | — | Column definitions | +| `columns` | `Array` | — | Column definitions (a bare string uses the key as its label) | | `rows` | `Array>` | — | Row data | -| `maxHeight` | `string` | — | Scrollable max height (e.g. `'300px'`) | +| `height` | `number` | — | Scrollable max height in pixels | +| `loading` | `boolean` | `false` | Show a loading state | ```ts @@ -433,16 +443,16 @@ Tabular data with configurable columns and scroll support. type: 'DataTable', props: { columns: [ - { key: 'id', label: 'Module', width: '200px' }, - { key: 'size', label: 'Size', width: '80px' }, - { key: 'time', label: 'Transform', width: '100px' }, + { key: 'id', label: 'Module' }, + { key: 'size', label: 'Size' }, + { key: 'time', label: 'Transform' }, ], rows: [ { id: 'src/index.ts', size: '2.1 KB', time: '12ms' }, { id: 'src/utils.ts', size: '0.8 KB', time: '3ms' }, { id: 'src/app.vue', size: '4.5 KB', time: '45ms' }, ], - maxHeight: '400px', + height: 400, }, } ``` @@ -454,9 +464,9 @@ Display a code snippet with an optional filename header. | Prop | Type | Default | Description | |------|------|---------|-------------| | `code` | `string` | — | Code content | -| `language` | `string` | `'text'` | Language identifier | +| `language` | `string` | — | Language identifier | | `filename` | `string` | — | Filename shown as header | -| `maxHeight` | `string` | — | Scrollable max height | +| `height` | `number` | — | Scrollable max height in pixels | ```ts @@ -466,7 +476,7 @@ Display a code snippet with an optional filename header. code: 'export default defineConfig({\n plugins: [vue()],\n})', language: 'ts', filename: 'vite.config.ts', - maxHeight: '200px', + height: 200, }, } ``` @@ -493,7 +503,7 @@ Expandable tree view for inspecting nested objects. | Prop | Type | Default | Description | |------|------|---------|-------------| | `data` | `unknown` | — | Any JSON-serializable value | -| `expandLevel` | `number` | `1` | How many levels to auto-expand | +| `defaultExpanded` | `boolean` | `true` | Whether nodes start expanded | ```ts @@ -508,7 +518,7 @@ Expandable tree view for inspecting nested objects. }, server: { port: 3000, hmr: true }, }, - expandLevel: 2, + defaultExpanded: true, }, } ``` @@ -528,17 +538,17 @@ function buildSpec(data: { modules: number, time: string, size: string }): JsonR elements: { 'root': { type: 'Stack', - props: { direction: 'vertical', gap: 12, padding: 8 }, + props: { direction: 'column', gap: 12, padding: 8 }, children: ['header', 'divider', 'stats', 'modules'], }, 'header': { type: 'Stack', - props: { direction: 'horizontal', gap: 8, align: 'center', justify: 'space-between' }, + props: { direction: 'row', gap: 8, align: 'center', justify: 'between' }, children: ['title', 'refresh-btn'], }, 'title': { type: 'Text', - props: { content: 'Build Report', variant: 'heading' }, + props: { text: 'Build Report', variant: 'heading' }, }, 'refresh-btn': { type: 'Button', @@ -557,11 +567,11 @@ function buildSpec(data: { modules: number, time: string, size: string }): JsonR 'stats-table': { type: 'KeyValueTable', props: { - entries: [ - { key: 'Total Modules', value: String(data.modules) }, - { key: 'Build Time', value: data.time }, - { key: 'Output Size', value: data.size }, - ], + data: { + 'Total Modules': String(data.modules), + 'Build Time': data.time, + 'Output Size': data.size, + }, }, }, 'modules': { @@ -574,13 +584,13 @@ function buildSpec(data: { modules: number, time: string, size: string }): JsonR props: { columns: [ { key: 'name', label: 'Module' }, - { key: 'size', label: 'Size', width: '80px' }, + { key: 'size', label: 'Size' }, ], rows: [ { name: 'src/index.ts', size: '2.1 KB' }, { name: 'src/app.vue', size: '4.5 KB' }, ], - maxHeight: '300px', + height: 300, }, }, }, diff --git a/examples/plugin-git-ui/src/node/spec.ts b/examples/plugin-git-ui/src/node/spec.ts index 3c5f774b2..8b20ae04d 100644 --- a/examples/plugin-git-ui/src/node/spec.ts +++ b/examples/plugin-git-ui/src/node/spec.ts @@ -4,10 +4,10 @@ import type { GitState } from './git' const statusMeta: Record = { 'M': { label: 'M', title: 'Modified', variant: 'warning' }, 'A': { label: 'A', title: 'Added', variant: 'success' }, - 'D': { label: 'D', title: 'Deleted', variant: 'error' }, + 'D': { label: 'D', title: 'Deleted', variant: 'danger' }, 'R': { label: 'R', title: 'Renamed', variant: 'info' }, 'C': { label: 'C', title: 'Copied', variant: 'info' }, - 'U': { label: 'U', title: 'Unmerged', variant: 'error' }, + 'U': { label: 'U', title: 'Unmerged', variant: 'danger' }, '?': { label: '?', title: 'Untracked', variant: 'default' }, } @@ -41,7 +41,7 @@ function buildFileRows( rowChildren.push(spacerId, btnId) elements[spacerId] = { type: 'Stack', - props: { direction: 'horizontal', flex: 1 }, + props: { direction: 'row', flex: 1 }, } elements[btnId] = { type: 'Button', @@ -52,7 +52,7 @@ function buildFileRows( elements[rowId] = { type: 'Stack', - props: { direction: 'horizontal', gap: 8, align: 'center' }, + props: { direction: 'row', gap: 8, align: 'center' }, children: rowChildren, } elements[statusId] = { @@ -61,12 +61,12 @@ function buildFileRows( text: meta.label, title: meta.title, variant: meta.variant, - minWidth: '24px', + minWidth: 24, }, } elements[fileId] = { type: 'Text', - props: { content: file, variant: 'code' }, + props: { text: file, variant: 'code' }, } } @@ -92,17 +92,17 @@ export function buildSpec(gitState: GitState, options?: { interactive?: boolean elements: { 'root': { type: 'Stack', - props: { direction: 'vertical', gap: 12, padding: 4 }, + props: { direction: 'column', gap: 12, padding: 4 }, children: rootChildren, }, 'header': { type: 'Stack', - props: { direction: 'horizontal', gap: 8, align: 'center', justify: 'space-between' }, + props: { direction: 'row', gap: 8, align: 'center', justify: 'between' }, children: interactive ? ['title', 'refresh-btn'] : ['title'], }, 'title': { type: 'Text', - props: { content: 'Git', variant: 'heading' }, + props: { text: 'Git', variant: 'heading' }, }, 'refresh-btn': { type: 'Button', @@ -111,7 +111,7 @@ export function buildSpec(gitState: GitState, options?: { interactive?: boolean }, 'branch-info': { type: 'Stack', - props: { direction: 'horizontal', gap: 8, align: 'center' }, + props: { direction: 'row', gap: 8, align: 'center' }, children: ['branch-icon', 'branch-text', 'changes-badge'], }, 'branch-icon': { @@ -120,7 +120,7 @@ export function buildSpec(gitState: GitState, options?: { interactive?: boolean }, 'branch-text': { type: 'Text', - props: { content: gitState.branch || '(detached)', variant: 'code' }, + props: { text: gitState.branch || '(detached)', variant: 'code' }, }, 'changes-badge': { type: 'Badge', @@ -131,7 +131,7 @@ export function buildSpec(gitState: GitState, options?: { interactive?: boolean }, 'commit-section': { type: 'Stack', - props: { direction: 'horizontal', gap: 8 }, + props: { direction: 'row', gap: 8 }, children: ['commit-input', 'commit-btn'], }, 'amend-switch': { @@ -174,13 +174,13 @@ export function buildSpec(gitState: GitState, options?: { interactive?: boolean }, 'staged-files': { type: 'Stack', - props: { direction: 'vertical', gap: 4 }, + props: { direction: 'column', gap: 4 }, children: stagedRows.children, }, ...stagedRows.elements, 'staged-empty': { type: 'Text', - props: { content: 'No staged files', variant: 'caption' }, + props: { text: 'No staged files', variant: 'caption' }, }, // Unstaged files @@ -193,7 +193,7 @@ export function buildSpec(gitState: GitState, options?: { interactive?: boolean }, 'unstaged-header': { type: 'Stack', - props: { direction: 'horizontal', justify: 'end' }, + props: { direction: 'row', justify: 'end' }, children: ['stage-all-btn'], }, 'stage-all-btn': { @@ -203,13 +203,13 @@ export function buildSpec(gitState: GitState, options?: { interactive?: boolean }, 'unstaged-files': { type: 'Stack', - props: { direction: 'vertical', gap: 4 }, + props: { direction: 'column', gap: 4 }, children: unstagedRows.children, }, ...unstagedRows.elements, 'unstaged-empty': { type: 'Text', - props: { content: 'No unstaged files', variant: 'caption' }, + props: { text: 'No unstaged files', variant: 'caption' }, }, // Commits @@ -228,7 +228,7 @@ export function buildSpec(gitState: GitState, options?: { interactive?: boolean { key: 'date', label: 'Date', width: '100px' }, ], rows: gitState.commits, - maxHeight: '300px', + height: 300, }, }, }, diff --git a/packages/core/src/client/webcomponents/json-render/JsonRender.stories.ts b/packages/core/src/client/webcomponents/json-render/JsonRender.stories.ts index e7eabe0fd..01c85ea1d 100644 --- a/packages/core/src/client/webcomponents/json-render/JsonRender.stories.ts +++ b/packages/core/src/client/webcomponents/json-render/JsonRender.stories.ts @@ -44,33 +44,33 @@ export const Gallery: Story = { root: 'root', state: { notifications: true }, elements: { - root: { type: 'Stack', props: { direction: 'vertical', gap: 16, padding: 4 }, children: ['heading', 'badges', 'buttons', 'progress', 'toggle', 'divider', 'kv', 'table', 'code'] }, - heading: { type: 'Text', props: { content: 'Build summary', variant: 'heading' } }, - badges: { type: 'Stack', props: { direction: 'horizontal', gap: 8, align: 'center' }, children: ['b1', 'b2', 'b3', 'b4'] }, + root: { type: 'Stack', props: { direction: 'column', gap: 16, padding: 4 }, children: ['heading', 'badges', 'buttons', 'progress', 'toggle', 'divider', 'kv', 'table', 'code'] }, + heading: { type: 'Text', props: { text: 'Build summary', variant: 'heading' } }, + badges: { type: 'Stack', props: { direction: 'row', gap: 8, align: 'center' }, children: ['b1', 'b2', 'b3', 'b4'] }, b1: { type: 'Badge', props: { text: 'passing', variant: 'success' } }, b2: { type: 'Badge', props: { text: '3 warnings', variant: 'warning' } }, - b3: { type: 'Badge', props: { text: '1 error', variant: 'error' } }, + b3: { type: 'Badge', props: { text: '1 error', variant: 'danger' } }, b4: { type: 'Badge', props: { text: 'v0.3.4', variant: 'default' } }, - buttons: { type: 'Stack', props: { direction: 'horizontal', gap: 8 }, children: ['btn1', 'btn2', 'btn3'] }, + buttons: { type: 'Stack', props: { direction: 'row', gap: 8 }, children: ['btn1', 'btn2', 'btn3'] }, btn1: { type: 'Button', props: { label: 'Rebuild', variant: 'primary', icon: 'ph:arrows-clockwise' } }, btn2: { type: 'Button', props: { label: 'Open', variant: 'secondary', icon: 'ph:arrow-square-out' } }, btn3: { type: 'Button', props: { label: 'Delete', variant: 'danger', icon: 'ph:trash' } }, progress: { type: 'Progress', props: { value: 68, max: 100, label: 'Bundling' } }, toggle: { type: 'Switch', props: { label: 'Notifications', value: '{{notifications}}' } }, divider: { type: 'Divider', props: { label: 'Details' } }, - kv: { type: 'KeyValueTable', props: { title: 'Environment', entries: [ - { key: 'Vite', value: '8.1.2' }, - { key: 'Node', value: '24.17.0' }, - { key: 'Mode', value: 'production' }, - ] } }, + kv: { type: 'KeyValueTable', props: { data: { + Vite: '8.1.2', + Node: '24.17.0', + Mode: 'production', + } } }, table: { type: 'DataTable', props: { - columns: [{ key: 'file', label: 'File', width: '60%' }, { key: 'size', label: 'Size' }], + columns: [{ key: 'file', label: 'File' }, { key: 'size', label: 'Size' }], rows: [ { file: 'index.js', size: '124 kB' }, { file: 'vendor.js', size: '612 kB' }, { file: 'style.css', size: '18 kB' }, ], - maxHeight: '160px', + height: 160, } }, code: { type: 'CodeBlock', props: { filename: 'vite.config.ts', code: 'export default defineConfig({\n plugins: [DevTools()],\n})' } }, }, @@ -84,8 +84,8 @@ export const Card: Story = { state: {}, elements: { root: { type: 'Card', props: { title: 'Plugin', collapsible: false }, children: ['body'] }, - body: { type: 'Stack', props: { direction: 'vertical', gap: 8, padding: 4 }, children: ['t', 'badge'] }, - t: { type: 'Text', props: { content: 'vite-plugin-inspect', variant: 'code' } }, + body: { type: 'Stack', props: { direction: 'column', gap: 8, padding: 4 }, children: ['t', 'badge'] }, + t: { type: 'Text', props: { text: 'vite-plugin-inspect', variant: 'code' } }, badge: { type: 'Badge', props: { text: 'enabled', variant: 'success' } }, }, } as unknown as Spec), diff --git a/packages/core/src/client/webcomponents/json-render/catalog.ts b/packages/core/src/client/webcomponents/json-render/catalog.ts deleted file mode 100644 index 313bb16cb..000000000 --- a/packages/core/src/client/webcomponents/json-render/catalog.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { defineCatalog, defineSchema } from '@json-render/core' -import { z } from 'zod' - -/** - * DevTools json-render schema — defines the spec shape. - * Uses the default json-render schema. - */ -export const devtoolsSchema = defineSchema(s => ({ - spec: s.object({ - root: s.string(), - elements: s.map({ - type: s.ref('catalog.components'), - props: s.propsOf('catalog.components'), - children: s.array(s.string()), - }), - }), - catalog: s.object({ - components: s.map({ - props: s.zod(), - description: s.string(), - }), - actions: s.map({ - params: s.zod(), - description: s.string(), - }), - }), -})) - -/** - * DevTools component catalog. - * These are the built-in components available to plugins via json-render specs. - */ -export const devtoolsCatalog = defineCatalog(devtoolsSchema, { - components: { - Stack: { - props: z.object({ - direction: z.enum(['vertical', 'horizontal']).default('vertical'), - gap: z.number().default(8), - align: z.enum(['start', 'center', 'end', 'stretch']).optional(), - justify: z.enum(['start', 'center', 'end', 'space-between', 'space-around']).optional(), - padding: z.number().optional(), - }), - description: 'Layout container that arranges children vertically or horizontally', - }, - Card: { - props: z.object({ - title: z.string().optional(), - collapsible: z.boolean().default(false), - }), - description: 'Container with optional title and collapsible behavior', - }, - Text: { - props: z.object({ - content: z.string(), - variant: z.enum(['heading', 'body', 'caption', 'code']).default('body'), - }), - description: 'Display text with different visual styles', - }, - Badge: { - props: z.object({ - text: z.string(), - variant: z.enum(['info', 'success', 'warning', 'error', 'default']).default('default'), - }), - description: 'Status badge label', - }, - Button: { - props: z.object({ - label: z.string(), - icon: z.string().optional(), - variant: z.enum(['primary', 'secondary', 'ghost', 'danger']).default('secondary'), - disabled: z.boolean().default(false), - }), - description: 'Clickable button that triggers an action via the "press" event', - }, - Icon: { - props: z.object({ - name: z.string(), - size: z.number().default(20), - }), - description: 'Iconify icon by name', - }, - Divider: { - props: z.object({ - label: z.string().optional(), - }), - description: 'Visual separator line', - }, - TextInput: { - props: z.object({ - placeholder: z.string().optional(), - value: z.string().optional(), - label: z.string().optional(), - disabled: z.boolean().default(false), - }), - description: 'Text input field. Use $bindState on the value prop for two-way binding.', - }, - Switch: { - props: z.object({ - value: z.boolean().default(false), - label: z.string().optional(), - disabled: z.boolean().default(false), - }), - description: 'Toggle switch. Use $bindState on the value prop for two-way binding. Bind "change" event to react to toggles.', - }, - KeyValueTable: { - props: z.object({ - title: z.string().optional(), - entries: z.array(z.object({ - key: z.string(), - value: z.string(), - })), - }), - description: 'Display key-value pairs in a table', - }, - DataTable: { - props: z.object({ - columns: z.array(z.object({ - key: z.string(), - label: z.string(), - width: z.string().optional(), - })), - rows: z.array(z.record(z.string(), z.unknown())), - maxHeight: z.string().optional(), - }), - description: 'Tabular data display with columns and rows. Bind "rowClick" event for row selection.', - }, - CodeBlock: { - props: z.object({ - code: z.string(), - language: z.string().default('text'), - filename: z.string().optional(), - maxHeight: z.string().optional(), - }), - description: 'Display code with syntax highlighting', - }, - Progress: { - props: z.object({ - value: z.number(), - max: z.number().default(100), - label: z.string().optional(), - }), - description: 'Progress bar with label', - }, - Tree: { - props: z.object({ - data: z.unknown(), - expandLevel: z.number().default(1), - }), - description: 'Expandable tree view for nested objects', - }, - }, - actions: {}, -}) diff --git a/packages/core/src/client/webcomponents/json-render/components/Badge.ts b/packages/core/src/client/webcomponents/json-render/components/Badge.ts index 3f38d3727..e2bfd0bae 100644 --- a/packages/core/src/client/webcomponents/json-render/components/Badge.ts +++ b/packages/core/src/client/webcomponents/json-render/components/Badge.ts @@ -9,6 +9,8 @@ export const Badge = defineComponent({ return () => { const { text, variant = 'default', title, minWidth } = ctx.element.props const c = colors[variant as keyof typeof colors] || colors.default + // Base catalog `minWidth` is a number of pixels. + const minWidthCss = minWidth == null ? undefined : (typeof minWidth === 'number' ? `${minWidth}px` : minWidth) return h('span', { class: `jr-badge jr-badge-${variant}`, title, @@ -21,7 +23,7 @@ export const Badge = defineComponent({ textAlign: 'center' as const, backgroundColor: c.bg, color: c.fg, - minWidth: minWidth || undefined, + minWidth: minWidthCss, }, }, text) } diff --git a/packages/core/src/client/webcomponents/json-render/components/CodeBlock.ts b/packages/core/src/client/webcomponents/json-render/components/CodeBlock.ts index a0f1e1197..00491b86f 100644 --- a/packages/core/src/client/webcomponents/json-render/components/CodeBlock.ts +++ b/packages/core/src/client/webcomponents/json-render/components/CodeBlock.ts @@ -7,17 +7,23 @@ export const CodeBlock = defineComponent({ props: ['element', 'emit', 'on', 'bindings', 'loading'], setup(ctx: RegistryComponentProps) { return () => { - const { code, filename, maxHeight } = ctx.element.props + const { code, filename, language, height } = ctx.element.props + const header = filename || language return h('div', { class: 'jr-code-block' }, [ - filename && h('div', { + header && h('div', { style: { + display: 'flex', + justifyContent: 'space-between', padding: '4px 12px', fontSize: '11px', opacity: '0.6', borderBottom: borderSolid(), fontFamily: 'monospace', }, - }, filename), + }, [ + h('span', filename ?? ''), + language && h('span', { style: { textTransform: 'uppercase' } }, language), + ]), h('pre', { style: { margin: 0, @@ -26,10 +32,10 @@ export const CodeBlock = defineComponent({ lineHeight: '1.5', fontFamily: 'monospace', backgroundColor: surfaceMuted, - borderRadius: filename ? '0 0 4px 4px' : '4px', + borderRadius: header ? '0 0 4px 4px' : '4px', overflow: 'auto', scrollbarGutter: 'stable', - maxHeight, + maxHeight: height != null ? `${height}px` : undefined, }, }, [h('code', code)]), ]) diff --git a/packages/core/src/client/webcomponents/json-render/components/DataTable.ts b/packages/core/src/client/webcomponents/json-render/components/DataTable.ts index b3fd90dd0..b4455a9a9 100644 --- a/packages/core/src/client/webcomponents/json-render/components/DataTable.ts +++ b/packages/core/src/client/webcomponents/json-render/components/DataTable.ts @@ -7,15 +7,19 @@ export const DataTable = defineComponent({ props: ['element', 'emit', 'on', 'bindings', 'loading'], setup(ctx: RegistryComponentProps) { return () => { - const { columns = [], rows = [], maxHeight } = ctx.element.props + const { columns: rawColumns = [], rows = [], height } = ctx.element.props const rowClick = ctx.on('rowClick') + // Base catalog columns are `string | { key, label? }`; normalize to objects. + const columns = (rawColumns as (string | { key: string, label?: string, width?: string })[]) + .map(col => (typeof col === 'string' ? { key: col } : col)) + return h('div', { class: 'jr-data-table', style: { overflow: 'auto', scrollbarGutter: 'stable', - maxHeight, + maxHeight: height != null ? `${height}px` : undefined, border: borderSolid(border), borderRadius: '4px', }, @@ -40,7 +44,7 @@ export const DataTable = defineComponent({ backgroundColor: `color-mix(in oklab,${bg} 75%,transparent)`, backdropFilter: 'blur(8px)', }, - }, col.label), + }, col.label ?? col.key), )), ]), h('tbody', rows.map((row: any, index: number) => diff --git a/packages/core/src/client/webcomponents/json-render/components/KeyValueTable.ts b/packages/core/src/client/webcomponents/json-render/components/KeyValueTable.ts index c03d29a6c..e4159dbaf 100644 --- a/packages/core/src/client/webcomponents/json-render/components/KeyValueTable.ts +++ b/packages/core/src/client/webcomponents/json-render/components/KeyValueTable.ts @@ -7,23 +7,21 @@ export const KeyValueTable = defineComponent({ props: ['element', 'emit', 'on', 'bindings', 'loading'], setup(ctx: RegistryComponentProps) { return () => { - const { title, entries = [] } = ctx.element.props + const { data = {} } = ctx.element.props + const entries = Object.entries(data as Record) return h('div', { class: 'jr-kv-table' }, [ - title && h('div', { - style: { fontSize: '13px', fontWeight: '600', marginBottom: '8px' }, - }, title), h('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: '12px' }, - }, entries.map((entry: any) => + }, entries.map(([key, value]) => h('tr', { style: { borderBottom: borderSolid(borderSubtle) }, }, [ h('td', { style: { padding: '6px 8px', opacity: '0.7', whiteSpace: 'nowrap', verticalAlign: 'top' }, - }, entry.key), + }, key), h('td', { style: { padding: '6px 8px', fontFamily: 'monospace', wordBreak: 'break-all' }, - }, entry.value), + }, typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value ?? '')), ]), )), ]) diff --git a/packages/core/src/client/webcomponents/json-render/components/Stack.ts b/packages/core/src/client/webcomponents/json-render/components/Stack.ts index 4d264c3a9..e1a256ea6 100644 --- a/packages/core/src/client/webcomponents/json-render/components/Stack.ts +++ b/packages/core/src/client/webcomponents/json-render/components/Stack.ts @@ -1,21 +1,26 @@ import type { RegistryComponentProps } from './types' import { defineComponent, h } from 'vue' +// Map the base catalog's `align` / `justify` enums onto CSS flexbox values. +const alignMap: Record = { start: 'flex-start', center: 'center', end: 'flex-end', stretch: 'stretch' } +const justifyMap: Record = { start: 'flex-start', center: 'center', end: 'flex-end', between: 'space-between', around: 'space-around' } + export const Stack = defineComponent({ name: 'JrStack', props: ['element', 'emit', 'on', 'bindings', 'loading'], setup(ctx: RegistryComponentProps, { slots }) { return () => { - const { direction = 'vertical', gap = 8, align, justify, padding, flex } = ctx.element.props - const isHorizontal = direction === 'horizontal' + const { direction = 'column', gap = 8, align, justify, padding, wrap, flex } = ctx.element.props + const isHorizontal = direction === 'row' return h('div', { class: 'jr-stack', style: { display: 'flex', flexDirection: isHorizontal ? 'row' : 'column', gap: `${gap}px`, - alignItems: align || (isHorizontal ? 'center' : 'stretch'), - justifyContent: justify, + alignItems: align ? (alignMap[align] ?? align) : (isHorizontal ? 'center' : 'stretch'), + justifyContent: justify ? (justifyMap[justify] ?? justify) : undefined, + flexWrap: wrap ? 'wrap' : undefined, padding: padding ? `${padding}px` : undefined, flex: flex != null ? String(flex) : undefined, }, diff --git a/packages/core/src/client/webcomponents/json-render/components/Text.ts b/packages/core/src/client/webcomponents/json-render/components/Text.ts index 836c39951..e06572c7e 100644 --- a/packages/core/src/client/webcomponents/json-render/components/Text.ts +++ b/packages/core/src/client/webcomponents/json-render/components/Text.ts @@ -7,15 +7,20 @@ export const Text = defineComponent({ props: ['element', 'emit', 'on', 'bindings', 'loading'], setup(ctx: RegistryComponentProps) { return () => { - const { content, variant = 'body' } = ctx.element.props + const { text, variant = 'body', weight } = ctx.element.props const styles: Record> = { heading: { fontSize: '16px', fontWeight: '600', lineHeight: '1.4' }, + subheading: { fontSize: '14px', fontWeight: '500', lineHeight: '1.4' }, body: { fontSize: '13px', lineHeight: '1.5' }, caption: { fontSize: '12px', opacity: '0.7', lineHeight: '1.4' }, code: { fontSize: '12px', fontFamily: 'monospace', backgroundColor: surfaceSubtle, padding: '2px 6px', borderRadius: '3px' }, } - const tag = variant === 'heading' ? 'h3' : variant === 'code' ? 'code' : 'p' - return h(tag, { class: `jr-text jr-text-${variant}`, style: { margin: 0, ...styles[variant] } }, content) + const weights: Record = { bold: '700', medium: '500', normal: '400' } + const tag = variant === 'heading' || variant === 'subheading' ? 'h3' : variant === 'code' ? 'code' : 'p' + return h(tag, { + class: `jr-text jr-text-${variant}`, + style: { margin: 0, ...styles[variant] ?? styles.body, ...(weight ? { fontWeight: weights[weight] ?? weights.normal } : {}) }, + }, text) } }, }) diff --git a/packages/core/src/client/webcomponents/json-render/components/Tree.ts b/packages/core/src/client/webcomponents/json-render/components/Tree.ts index be670601e..702c166d7 100644 --- a/packages/core/src/client/webcomponents/json-render/components/Tree.ts +++ b/packages/core/src/client/webcomponents/json-render/components/Tree.ts @@ -36,7 +36,10 @@ export const Tree = defineComponent({ } return () => { - const { data, expandLevel = 1 } = ctx.element.props + const { data, defaultExpanded } = ctx.element.props + // Base catalog exposes a boolean `defaultExpanded` (default true); map it + // to the recursive expand depth this renderer uses. + const expandLevel = defaultExpanded === false ? 0 : Infinity if (data === null || data === undefined) { return h('span', { style: { fontSize: '12px', opacity: 0.5 } }, 'null') } diff --git a/packages/core/src/client/webcomponents/json-render/components/tokens.ts b/packages/core/src/client/webcomponents/json-render/components/tokens.ts index 539ec05d8..7df89a8df 100644 --- a/packages/core/src/client/webcomponents/json-render/components/tokens.ts +++ b/packages/core/src/client/webcomponents/json-render/components/tokens.ts @@ -18,7 +18,7 @@ export const colors = { info: { bg: 'rgba(59,130,246,0.15)', fg: 'rgb(59,130,246)' }, success: { bg: 'rgba(34,197,94,0.15)', fg: 'rgb(34,197,94)' }, warning: { bg: 'rgba(234,179,8,0.15)', fg: 'rgb(234,179,8)' }, - error: { bg: 'rgba(239,68,68,0.15)', fg: 'rgb(239,68,68)' }, + danger: { bg: 'rgba(239,68,68,0.15)', fg: 'rgb(239,68,68)' }, default: { bg: 'rgba(128,128,128,0.15)', fg: 'inherit' }, } as const diff --git a/packages/core/src/client/webcomponents/json-render/index.ts b/packages/core/src/client/webcomponents/json-render/index.ts deleted file mode 100644 index 53adc1568..000000000 --- a/packages/core/src/client/webcomponents/json-render/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { devtoolsCatalog, devtoolsSchema } from './catalog' -export { devtoolsRegistry } from './registry' diff --git a/packages/core/src/client/webcomponents/state/context.ts b/packages/core/src/client/webcomponents/state/context.ts index f86a736a0..1aeec15a4 100644 --- a/packages/core/src/client/webcomponents/state/context.ts +++ b/packages/core/src/client/webcomponents/state/context.ts @@ -12,6 +12,7 @@ import { docksGroupByCategories, getGroupMembers, getRegisteredGroupIds, resolve import { createDockEntryState, DEFAULT_DOCK_PANEL_STORE, sharedStateToRef, useDocksEntries } from './docks' import { createClientMessagesClient } from './messages-client' import { registerMainFrameDockActionHandler, triggerMainFrameDockAction } from './popup' +import { createDockRenderers } from './renderers' import { executeSetupScript } from './setup-script' const docksContextByRpc = new WeakMap() @@ -299,6 +300,7 @@ export async function createDocksContext( }, events: rpc.events, }, + renderers: markRaw(createDockRenderers(() => docksContext)), rpc: markRaw(rpc), clientType, }) diff --git a/packages/core/src/client/webcomponents/state/renderers.ts b/packages/core/src/client/webcomponents/state/renderers.ts new file mode 100644 index 000000000..95ab7e586 --- /dev/null +++ b/packages/core/src/client/webcomponents/state/renderers.ts @@ -0,0 +1,51 @@ +import type { DevToolsDockEntry } from '@vitejs/devtools-kit' +import type { + DevToolsClientContext, + DockRenderer, + DockRenderersContext, + DocksContext, +} from '@vitejs/devtools-kit/client' + +/** + * The client-side dock-renderer registry required by the hub's `DocksContext` + * (devframe 0.7.6). The headless hub routes a dock `type` to a host-registered + * renderer through this registry — e.g. a host could register + * `@devframes/json-render-ui` for `'json-render'`. + * + * Vite DevTools renders its built-in dock types directly in `ViewEntry.vue` + * (it owns its Vue shell rather than delegating to the hub's client host), so + * nothing is registered here by default. The registry still exists so the + * context satisfies the hub contract and third-party dock types can plug in. + */ +export function createDockRenderers(getContext: () => DocksContext): DockRenderersContext { + const renderers = new Map() + + return { + register(type, renderer) { + renderers.set(type, renderer) + return () => { + if (renderers.get(type) === renderer) + renderers.delete(type) + } + }, + get(type) { + return renderers.get(type) + }, + has(type) { + return renderers.has(type) + }, + async mount(entry: DevToolsDockEntry, container: HTMLElement) { + const renderer = renderers.get(entry.type) + if (!renderer) { + console.warn(`[vite-devtools] no dock renderer registered for type "${entry.type}"`) + return () => {} + } + const instance = await renderer({ + entry, + container, + context: getContext() as DevToolsClientContext, + }) + return () => instance.dispose?.() + }, + } +} diff --git a/packages/kit/package.json b/packages/kit/package.json index b26b8150f..a0579fbc3 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -45,6 +45,7 @@ }, "dependencies": { "@devframes/hub": "catalog:deps", + "@devframes/json-render": "catalog:deps", "devframe": "catalog:deps", "local-pkg": "catalog:deps", "mlly": "catalog:deps", diff --git a/packages/kit/src/client/docks.ts b/packages/kit/src/client/docks.ts index 27aea0e34..bc0f14674 100644 --- a/packages/kit/src/client/docks.ts +++ b/packages/kit/src/client/docks.ts @@ -6,6 +6,10 @@ export type { DockEntryState, DockEntryStateEvents, DockPanelStorage, + DockRenderer, + DockRendererInstance, + DockRendererMountOptions, + DockRenderersContext, DocksContext, DocksEntriesContext, DocksPanelContext, diff --git a/packages/kit/src/define.ts b/packages/kit/src/define.ts index e7173c5c3..7a37c2765 100644 --- a/packages/kit/src/define.ts +++ b/packages/kit/src/define.ts @@ -1,6 +1,16 @@ -import type { ViteDevToolsNodeContext } from './types' +import type { JsonRenderSpec, ViteDevToolsNodeContext } from './types' import { createDefineWrapperWithContext } from 'devframe/rpc' -export { defineCommand, defineDockEntry, defineJsonRenderSpec } from '@devframes/hub' +export { defineCommand, defineDockEntry } from '@devframes/hub' + +/** + * Identity helper that types a json-render spec literal. `@devframes/hub` no + * longer ships this (json-render moved to the opt-in `@devframes/json-render` + * package, whose spec is a plain `@json-render/core` `Spec`), so the kit keeps + * the convenience helper for authoring specs with inference. + */ +export function defineJsonRenderSpec(spec: JsonRenderSpec): JsonRenderSpec { + return spec +} export const defineRpcFunction = createDefineWrapperWithContext() diff --git a/packages/kit/src/node/context.ts b/packages/kit/src/node/context.ts index 37d44455a..9bd3aefe0 100644 --- a/packages/kit/src/node/context.ts +++ b/packages/kit/src/node/context.ts @@ -1,15 +1,26 @@ import type { CreateHubContextOptions, DevframeHubContext } from '@devframes/hub/node' import type { ResolvedConfig, ViteDevServer } from 'vite' +import type { JsonRenderer, JsonRenderSpec } from '../types/json-render' import { createHubContext } from '@devframes/hub/node' +import { createJsonRenderView } from '@devframes/json-render/node' +import { nanoid } from '../utils/nanoid' /** * Kit-augmented node context — the framework-neutral hub context from * `@devframes/hub`, plus the Vite-specific slots surfaced when kit hosts - * the devtool inside Vite DevTools. + * the devtool inside Vite DevTools, and the kit's `createJsonRenderer` + * factory (json-render is the opt-in `@devframes/json-render` package, so + * the kit — not the hub — surfaces it on the context). */ export interface KitNodeContext extends DevframeHubContext { readonly viteConfig?: ResolvedConfig readonly viteServer?: ViteDevServer + /** + * Create a json-render handle for building declarative, server-driven + * panels. Register the returned handle on a `json-render` dock entry's `ui` + * field and call `updateSpec` / `updateState` to drive it reactively. + */ + createJsonRenderer: (spec: JsonRenderSpec) => JsonRenderer } export interface CreateKitContextOptions extends CreateHubContextOptions { @@ -22,9 +33,9 @@ export interface CreateKitContextOptions extends CreateHubContextOptions { /** * Create a kit-level node context: wraps `@devframes/hub`'s * `createHubContext` (which itself wraps devframe's `createHostContext`) - * and attaches the Vite-specific slots. The hub layer owns the - * docks/terminals/messages/commands subsystems and seeds the shared-state - * sync the unified client UI consumes. + * and attaches the Vite-specific slots plus the `createJsonRenderer` + * factory. The hub layer owns the docks/terminals/messages/commands + * subsystems and seeds the shared-state sync the unified client UI consumes. */ export async function createKitContext(options: CreateKitContextOptions): Promise { const context = await createHubContext(options) as KitNodeContext @@ -34,5 +45,48 @@ export async function createKitContext(options: CreateKitContextOptions): Promis if (options.viteServer) Object.defineProperty(context, 'viteServer', { value: options.viteServer, enumerable: true }) + Object.defineProperty(context, 'createJsonRenderer', { + value: (spec: JsonRenderSpec) => createJsonRenderer(context, spec), + enumerable: true, + }) + return context } + +/** + * Build a kit {@link JsonRenderer} handle over a devframe json-render view. + * The handle's methods are defined non-enumerably so the whole object stays + * serializable when carried on a dock entry's `ui` field — the docks + * shared-state projection walks only enumerable own keys, so the live + * closures never reach the wire while `_stateKey` / `upstreamVersion` do. + */ +function createJsonRenderer(context: KitNodeContext, spec: JsonRenderSpec): JsonRenderer { + const view = createJsonRenderView(context, { id: `kit-${nanoid()}`, spec }) + + const handle = { + _stateKey: view.ref.stateKey, + upstreamVersion: view.ref.upstreamVersion, + view: view.ref, + } as JsonRenderer + + Object.defineProperties(handle, { + updateSpec: { + value: (next: JsonRenderSpec) => view.update(next), + enumerable: false, + }, + updateState: { + value: (state: Record) => { + view.patchState( + Object.entries(state).map(([key, value]) => ({ op: 'add' as const, path: `/${key}`, value })), + ) + }, + enumerable: false, + }, + dispose: { + value: () => view.dispose(), + enumerable: false, + }, + }) + + return handle +} diff --git a/packages/kit/src/types/docks.ts b/packages/kit/src/types/docks.ts index e3253a40a..93d783a3a 100644 --- a/packages/kit/src/types/docks.ts +++ b/packages/kit/src/types/docks.ts @@ -1,4 +1,5 @@ -import type { DevframeDockEntryCategory, DevframeViewLauncher } from '@devframes/hub/types' +import type { DevframeDockEntryBase, DevframeDockEntryCategory, DevframeViewLauncher } from '@devframes/hub/types' +import type { JsonRenderer } from './json-render' export type { ClientScriptEntry, @@ -15,12 +16,33 @@ export type { DevframeViewCustomRender as DevToolsViewCustomRender, DevframeViewGroup as DevToolsViewGroup, DevframeViewIframe as DevToolsViewIframe, - DevframeViewJsonRender as DevToolsViewJsonRender, DevframeViewLauncherStatus as DevToolsViewLauncherStatus, RemoteConnectionInfo, RemoteDockOptions, } from '@devframes/hub/types' +/** + * A `json-render` dock entry. `@devframes/hub` ships no json-render variant of + * its own (json-render is the opt-in `@devframes/json-render` package), so the + * kit contributes this Vite-flavored entry to the hub's open dock union. + * + * It carries the {@link JsonRenderer} handle from `ctx.createJsonRenderer()` on + * `ui`; the handle's methods are non-enumerable, so only its serializable + * metadata survives dock projection into shared state, where the client reads + * `ui._stateKey` to subscribe to the live spec. + */ +export interface DevToolsViewJsonRender extends DevframeDockEntryBase { + type: 'json-render' + /** The renderer handle created by `ctx.createJsonRenderer()`. */ + ui: JsonRenderer +} + +declare module '@devframes/hub/types' { + interface DevframeDockEntryRegistry { + 'json-render': DevToolsViewJsonRender + } +} + /** * A selectable launch root offered by a launcher dock entry. * diff --git a/packages/kit/src/types/json-render.ts b/packages/kit/src/types/json-render.ts index 55005631b..45645279d 100644 --- a/packages/kit/src/types/json-render.ts +++ b/packages/kit/src/types/json-render.ts @@ -1,5 +1,44 @@ -export type { - JsonRenderElement, - JsonRenderer, - JsonRenderSpec, -} from '@devframes/hub/types' +import type { + DevframeJsonRenderSpec, + JsonRenderView, + JsonRenderViewRef, + UIElement, +} from '@devframes/json-render' + +// A Devframes json-render spec is an `@json-render/core` `Spec` (flat `root` +// + `elements` map, plus optional initial `state`). These aliases keep the +// kit's historical `JsonRender*` names pointing at the extracted +// `@devframes/json-render` package. + +/** A json-render spec — the declarative UI description a plugin authors. */ +export type JsonRenderSpec = DevframeJsonRenderSpec + +/** A single element within a spec's `elements` map. */ +export type JsonRenderElement = UIElement + +export type { JsonRenderView, JsonRenderViewRef } + +/** + * The handle returned by `ctx.createJsonRenderer()`. It wraps a devframe + * {@link JsonRenderView} (created via `@devframes/json-render`'s + * `createJsonRenderView`) and exposes the kit's back-compat method names. + * + * Its methods are defined non-enumerably so the handle stays fully + * serializable when carried on a `json-render` dock entry's `ui` field — + * only the plain string metadata (`_stateKey`, `upstreamVersion`) crosses + * the wire to the client, which subscribes through `_stateKey`. + */ +export interface JsonRenderer { + /** Replace the entire spec. */ + updateSpec: (spec: JsonRenderSpec) => void + /** Shallow-merge values into the view's `state`. */ + updateState: (state: Record) => void + /** Unregister the underlying view's shared state and listeners. */ + dispose: () => void + /** Shared-state key the client subscribes to for the live spec + state. */ + readonly _stateKey: string + /** Upstream `@json-render/*` version the view was authored against. */ + readonly upstreamVersion: string + /** The serializable reference to the underlying view. */ + readonly view: JsonRenderViewRef +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index fb9f0944a..6afb6dae3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -59,15 +59,16 @@ catalogs: unrun: ^0.3.1 vite: ^8.1.5 deps: - '@devframes/hub': ^0.7.8 - '@devframes/plugin-inspect': ^0.7.8 - '@devframes/plugin-messages': ^0.7.8 - '@devframes/plugin-terminals': ^0.7.8 + '@devframes/hub': ^0.7.9 + '@devframes/json-render': ^0.7.8 + '@devframes/plugin-inspect': ^0.7.9 + '@devframes/plugin-messages': ^0.7.9 + '@devframes/plugin-terminals': ^0.7.9 '@rolldown/debug': ^1.2.0 actionspack: ^0.1.5 birpc: ^4.0.0 cac: ^7.0.0 - devframe: ^0.7.8 + devframe: ^0.7.9 diff: ^9.0.0 envinfo: ^7.21.0 get-port-please: ^3.2.0 diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.d.ts index 8d6d8a464..2aa93a6ed 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.d.ts @@ -28,6 +28,10 @@ export { DockClientType } export { DockEntryState } export { DockEntryStateEvents } export { DockPanelStorage } +export { DockRenderer } +export { DockRendererInstance } +export { DockRendererMountOptions } +export { DockRenderersContext } export { DocksContext } export { DocksEntriesContext } export { DocksPanelContext } diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.d.ts index d8561db45..e71206736 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.d.ts @@ -21,14 +21,27 @@ export interface DevToolsPluginOptions { }; setup: (_: ViteDevToolsNodeContext) => void | Promise; } +export interface DevToolsViewJsonRender extends DevframeDockEntryBase { + type: 'json-render'; + ui: JsonRenderer; +} export interface DevToolsViewLauncher extends DevframeViewLauncher { launcher: DevframeViewLauncher['launcher'] & { roots?: DevToolsLaunchRoot[]; }; } +export interface JsonRenderer { + updateSpec: (_: JsonRenderSpec) => void; + updateState: (_: Record) => void; + dispose: () => void; + readonly _stateKey: string; + readonly upstreamVersion: string; + readonly view: JsonRenderViewRef; +} export interface KitNodeContext extends DevframeHubContext { readonly viteConfig?: ResolvedConfig; readonly viteServer?: ViteDevServer; + createJsonRenderer: (_: JsonRenderSpec) => JsonRenderer; } export interface PluginWithDevTools extends Plugin { devtools?: DevToolsPluginOptions; @@ -41,6 +54,12 @@ export interface ViteDevToolsNodeContext extends KitNodeContext { // #region Types export type DevToolsDockEntryCategory = DevframeDockEntryCategory; +export type JsonRenderElement = UIElement; +export type JsonRenderSpec = DevframeJsonRenderSpec; +// #endregion + +// #region Functions +export declare function defineJsonRenderSpec(_: JsonRenderSpec): JsonRenderSpec; // #endregion // #region Variables @@ -52,7 +71,6 @@ export { ClientScriptEntry } export { ConnectionMeta } export { defineCommand } export { defineDockEntry } -export { defineJsonRenderSpec } export { DevToolsCapabilities } export { DevToolsChildProcessExecuteOptions } export { DevToolsChildProcessTerminalSession } @@ -103,15 +121,13 @@ export { DevToolsViewCustomRender } export { DevToolsViewGroup } export { DevToolsViewHost } export { DevToolsViewIframe } -export { DevToolsViewJsonRender } export { DevToolsViewLauncherStatus } export { EntriesToObject } export { EventEmitter } export { EventsMap } export { EventUnsubscribe } -export { JsonRenderElement } -export { JsonRenderer } -export { JsonRenderSpec } +export { JsonRenderView } +export { JsonRenderViewRef } export { PartialWithoutId } export { RemoteConnectionInfo } export { RemoteDockOptions } diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.js b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.js index 85f303ba7..220844316 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.js +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.js @@ -1,6 +1,10 @@ /** * Generated by tsnapi — public API snapshot of `@vitejs/devtools-kit` */ +// #region Functions +export function defineJsonRenderSpec(_) {} +// #endregion + // #region Variables export var defineRpcFunction /* const */ // #endregion @@ -8,5 +12,4 @@ export var defineRpcFunction /* const */ // #region Other export { defineCommand } export { defineDockEntry } -export { defineJsonRenderSpec } // #endregion \ No newline at end of file diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/node.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/node.snapshot.d.ts index 37ab4b767..9eccec616 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/node.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/node.snapshot.d.ts @@ -34,6 +34,7 @@ export interface InstallLauncherOptions { export interface KitNodeContext extends DevframeHubContext { readonly viteConfig?: ResolvedConfig; readonly viteServer?: ViteDevServer; + createJsonRenderer: (_: JsonRenderSpec) => JsonRenderer; } export interface ProcessLauncherOptions { id: string; From b685ee5f7415fa98204066e5ebd65c5c4724fc10 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Wed, 22 Jul 2026 08:01:59 +0000 Subject: [PATCH 2/3] chore(deps): upgrade devframe to v0.7.9 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump devframe, @devframes/hub, @devframes/json-render, and the built-in @devframes/plugin-* integrations to 0.7.9. devframe/hub 0.7.6 → 0.7.9 has no functional change to the public API we consume — the dist output is otherwise identical (the zod → valibot migration mentioned in devframe's own dependency list is an internal implementation detail that doesn't cross the public surface). The one adaptation required: hub 0.7.9 re-added its own deprecated `createJsonRenderer` / `JsonRenderSpec` / `JsonRenderer` on `DevframeHubContext` as a back-compat shim for pre-0.7 call sites ("kept working... will be removed in 0.8"), typed against the hub's own looser (optional `props`) spec shape. That collides with the kit's `createJsonRenderer`, which is intentionally typed against `@devframes/json-render`'s stricter `Spec` (the non-deprecated, recommended surface we already adopted): - `KitNodeContext` now extends `Omit` before redeclaring its own factory, since the hub's deprecated property makes the plain `extends` an invalid override. - `createPluginFromDevframe` casts through `DevframeHubContext` at the `mountDevframe` call site (which never calls `createJsonRenderer`) rather than widening the kit's own factory's type. `@devframes/json-render` itself only gained an optional per-view `title` (published to a new opt-in shared-state "view index" for multi-view frontends) — additive and irrelevant to the kit's single-ref-per-dock usage. --- packages/kit/src/node/context.ts | 11 +- .../src/node/create-plugin-from-devframe.ts | 11 +- pnpm-lock.yaml | 236 ++++++------------ pnpm-workspace.yaml | 2 +- .../@vitejs/devtools-kit/index.snapshot.d.ts | 2 +- .../@vitejs/devtools-kit/node.snapshot.d.ts | 3 +- 6 files changed, 96 insertions(+), 169 deletions(-) diff --git a/packages/kit/src/node/context.ts b/packages/kit/src/node/context.ts index 9bd3aefe0..779f92412 100644 --- a/packages/kit/src/node/context.ts +++ b/packages/kit/src/node/context.ts @@ -11,8 +11,17 @@ import { nanoid } from '../utils/nanoid' * the devtool inside Vite DevTools, and the kit's `createJsonRenderer` * factory (json-render is the opt-in `@devframes/json-render` package, so * the kit — not the hub — surfaces it on the context). + * + * `Omit`: hub 0.7.9 re-added its own + * `createJsonRenderer` as a **deprecated** back-compat factory (removed in + * 0.8) typed against the hub's own pre-0.7 `JsonRenderSpec` (whose element + * `props` is optional). The kit's factory is typed against + * `@devframes/json-render`'s `Spec` (`props` required) instead — the + * currently-recommended, non-deprecated surface — so the property must be + * omitted from the base before it's redeclared here, or the narrower + * parameter type makes this an invalid override. */ -export interface KitNodeContext extends DevframeHubContext { +export interface KitNodeContext extends Omit { readonly viteConfig?: ResolvedConfig readonly viteServer?: ViteDevServer /** diff --git a/packages/kit/src/node/create-plugin-from-devframe.ts b/packages/kit/src/node/create-plugin-from-devframe.ts index 41fba98fe..58b3d7788 100644 --- a/packages/kit/src/node/create-plugin-from-devframe.ts +++ b/packages/kit/src/node/create-plugin-from-devframe.ts @@ -1,3 +1,4 @@ +import type { DevframeHubContext } from '@devframes/hub/node' import type { DevframeCapabilities, DevframeViewIframe } from '@devframes/hub/types' import type { DevframeDefinition } from 'devframe/types' import type { PluginWithDevTools } from '../types/vite-augment' @@ -53,7 +54,15 @@ export function createPluginFromDevframe( capabilities: options.capabilities ?? (d.capabilities as any), async setup(rawCtx) { const ctx = rawCtx as KitNodeContext - await mountDevframe(ctx, d, { + // `mountDevframe` is typed against the hub's own `DevframeHubContext`, + // whose `createJsonRenderer` is a deprecated back-compat shim (hub + // 0.7.9) typed against the hub's pre-0.7 `JsonRenderSpec` — looser + // (optional `props`) than the kit's own factory, which is typed + // against `@devframes/json-render`'s `Spec` (`props` required). The + // mismatch is a type-only artifact of the deprecated property: at + // runtime `ctx` is a fully valid `DevframeHubContext`, and + // `mountDevframe` never calls `createJsonRenderer`. + await mountDevframe(ctx as unknown as DevframeHubContext, d, { base: options.base, dock: options.dock, }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a9b99a76..49f598fa2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,17 +44,20 @@ catalogs: version: 0.3.1 deps: '@devframes/hub': - specifier: ^0.7.8 - version: 0.7.8 + specifier: ^0.7.9 + version: 0.7.9 + '@devframes/json-render': + specifier: ^0.7.9 + version: 0.7.9 '@devframes/plugin-inspect': - specifier: ^0.7.8 - version: 0.7.8 + specifier: ^0.7.9 + version: 0.7.9 '@devframes/plugin-messages': - specifier: ^0.7.8 - version: 0.7.8 + specifier: ^0.7.9 + version: 0.7.9 '@devframes/plugin-terminals': - specifier: ^0.7.8 - version: 0.7.8 + specifier: ^0.7.9 + version: 0.7.9 actionspack: specifier: ^0.1.5 version: 0.1.5 @@ -62,8 +65,8 @@ catalogs: specifier: ^4.0.0 version: 4.0.0 devframe: - specifier: ^0.7.8 - version: 0.7.8 + specifier: ^0.7.9 + version: 0.7.9 diff: specifier: ^9.0.0 version: 9.0.0 @@ -652,7 +655,7 @@ importers: version: 5.0.2(change-case@5.4.4)(focus-trap@8.2.2)(fuse.js@7.5.0)(idb-keyval@6.3.0)(vite@8.1.5(@types/node@25.0.3)(@vitejs/devtools@packages+core)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.1)(yaml@2.9.0))(vitepress@2.0.0-alpha.18(@types/node@25.0.3)(@vitejs/devtools@packages+core)(change-case@5.4.4)(esbuild@0.28.1)(fuse.js@7.5.0)(idb-keyval@6.3.0)(jiti@2.7.0)(postcss@8.5.20)(terser@5.44.1)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(vue@3.5.40(typescript@6.0.3)) devframe: specifier: catalog:deps - version: 0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) + version: 0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) mermaid: specifier: catalog:docs version: 11.16.0 @@ -796,16 +799,16 @@ importers: dependencies: '@devframes/hub': specifier: catalog:deps - version: 0.7.8(devframe@0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)) + version: 0.7.9(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)) '@devframes/plugin-inspect': specifier: catalog:deps - version: 0.7.8(devframe@0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))(valibot@1.4.2(typescript@6.0.3))(vite@8.1.5) + version: 0.7.9(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))(valibot@1.4.2(typescript@6.0.3))(vite@8.1.5) '@devframes/plugin-messages': specifier: catalog:deps - version: 0.7.8(@devframes/hub@0.7.8(devframe@0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)))(devframe@0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))(vite@8.1.5) + version: 0.7.9(@devframes/hub@0.7.9(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)))(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))(vite@8.1.5) '@devframes/plugin-terminals': specifier: catalog:deps - version: 0.7.8(devframe@0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))(typescript@6.0.3)(vite@8.1.5) + version: 0.7.9(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))(typescript@6.0.3)(vite@8.1.5) '@vitejs/devtools-kit': specifier: workspace:* version: link:../kit @@ -820,7 +823,7 @@ importers: version: 7.0.0 devframe: specifier: catalog:deps - version: 0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) + version: 0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) h3: specifier: catalog:deps version: 2.0.1-rc.22(crossws@0.4.10(srvx@0.11.22)) @@ -899,10 +902,13 @@ importers: dependencies: '@devframes/hub': specifier: catalog:deps - version: 0.7.8(devframe@0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)) + version: 0.7.9(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)) + '@devframes/json-render': + specifier: catalog:deps + version: 0.7.9(@devframes/hub@0.7.9(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)))(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)) devframe: specifier: catalog:deps - version: 0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) + version: 0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) local-pkg: specifier: catalog:deps version: 1.2.1 @@ -939,7 +945,7 @@ importers: version: 7.0.0 devframe: specifier: catalog:deps - version: 0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) + version: 0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) local-pkg: specifier: catalog:deps version: 1.2.1 @@ -1079,7 +1085,7 @@ importers: version: 3.1.2 devframe: specifier: catalog:deps - version: 0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) + version: 0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) diff-match-patch-es: specifier: catalog:frontend version: 2.0.0 @@ -1209,7 +1215,7 @@ importers: version: 3.1.2 devframe: specifier: catalog:deps - version: 0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) + version: 0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) diff-match-patch-es: specifier: catalog:frontend version: 2.0.0 @@ -1319,7 +1325,7 @@ importers: version: 6.0.8(vite@8.1.5(@types/node@25.0.3)(@vitejs/devtools@packages+core)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.1)(yaml@2.9.0))(vue@3.5.40(typescript@5.9.3)) devframe: specifier: catalog:deps - version: 0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@5.9.3) + version: 0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@5.9.3) storybook: specifier: catalog:storybook version: 10.5.3(@types/react@19.2.17)(prettier@2.8.8)(react@19.2.8) @@ -1642,73 +1648,46 @@ packages: peerDependencies: devframe: 0.6.2 - '@devframes/hub@0.7.7': - resolution: {integrity: sha512-fl+T0rdxeK5R39oRodLkR+9ucBqoMi7pQJQS6scks+l09BLA9QTErtSzhkNa+qhf1EJpq7JFQNyPUFz2We9QVQ==} - peerDependencies: - devframe: 0.7.7 - - '@devframes/hub@0.7.8': - resolution: {integrity: sha512-ZOnTAn/SW891LMdhB+nFiS2GCfyFNVEBF7ZiB+JAG9ryb+j5KJREVLhTWDTIzlLuRFXFr84x7xQ8lhFFQhei+g==} - peerDependencies: - devframe: 0.7.8 - - '@devframes/plugin-inspect@0.7.7': - resolution: {integrity: sha512-IZp0X2eURa36oOvHvyotCrRzRO4fJcz7CX4UgiZamKoHTdamF2CfoOlCPlx21qrGTb7L14TRoyupafmWdXLTpw==} - hasBin: true - peerDependencies: - devframe: 0.7.7 - vite: ^8.1.5 - peerDependenciesMeta: - vite: - optional: true - - '@devframes/plugin-inspect@0.7.8': - resolution: {integrity: sha512-tYv/vrU51b1mXZzcIOkKRWKSXPR1uQPENfdxoCDi0kIICuwM6FXtl+Ve9PpcuPfvtOCOs+XC8P9d8rcNLvijLQ==} - hasBin: true + '@devframes/hub@0.7.9': + resolution: {integrity: sha512-+tARSZfwkzez6RKA8ShlntAmfKTn7Z+IM2xwDilNMze5YO+E4n/WmaJBYZL2VvXdDiWB0cJbbwHto+vsrEQB9g==} peerDependencies: - devframe: 0.7.8 - vite: ^8.1.5 - peerDependenciesMeta: - vite: - optional: true + devframe: 0.7.9 - '@devframes/plugin-messages@0.7.7': - resolution: {integrity: sha512-jzNK5+u07fmn1+GQI95UMdq8KX/yPjtD1n55bPJSZcjCmCVUm8fubx/0KaABYl7lXR+3N7zY3JAoLn4ygnSKwQ==} - hasBin: true + '@devframes/json-render@0.7.9': + resolution: {integrity: sha512-VgUBkxyCG60wTMvihZpGwX9RZwhmhNf+znh/R0Dlwz602E74HtCD2J8jdWqKje4aia4KmmHFLLwWC/itmkfJZg==} peerDependencies: - '@devframes/hub': 0.7.7 - devframe: 0.7.7 - vite: ^8.1.5 + '@devframes/hub': 0.7.9 + devframe: 0.7.9 peerDependenciesMeta: - vite: + '@devframes/hub': optional: true - '@devframes/plugin-messages@0.7.8': - resolution: {integrity: sha512-hVBQWgxiCP96qb7YkVw65xeFGJ2MdBZpF22y2OSBJCo68BBHqS4qYcXxy/Y+OEGju/GC5/GNo6OzEdBFoqc2dw==} + '@devframes/plugin-inspect@0.7.9': + resolution: {integrity: sha512-pDt/cjOvc8hF0Qj4WwBo+drnGD9yy1ck/CCFr2wno+d6AlM+00rxTgP1cCecAckVRNPxeVzSmQ7rFo8Ep/zPIw==} hasBin: true peerDependencies: - '@devframes/hub': 0.7.8 - devframe: 0.7.8 + devframe: 0.7.9 vite: ^8.1.5 peerDependenciesMeta: vite: optional: true - '@devframes/plugin-terminals@0.7.7': - resolution: {integrity: sha512-jx4zmDXBVFFV/1s2ghrU3R5udQBpRbrzrEL/v0ubsnzXPZqLOYJrVZEguaytSMvTVIPObnX1GlHQlCCvud71fg==} + '@devframes/plugin-messages@0.7.9': + resolution: {integrity: sha512-WcKRQEPw+VAjbubEZx7gHZdaKMkN4FwGkGBHCoAfe/4rbY2Sl2l/KEIW6+og0y8luoxHWxmnmrus1Jkc9mRZow==} hasBin: true peerDependencies: - devframe: 0.7.7 + '@devframes/hub': 0.7.9 + devframe: 0.7.9 vite: ^8.1.5 peerDependenciesMeta: vite: optional: true - '@devframes/plugin-terminals@0.7.8': - resolution: {integrity: sha512-3SxPoLldlzcrYmX9hb9NQcHMFKX6iOHD4fWtWoLJe1Zz/ELyEbQ5fQ+v1fuzoG3IiLHNSH+dMwgDVPr8a+9Iug==} + '@devframes/plugin-terminals@0.7.9': + resolution: {integrity: sha512-rvDtNCzZyMRvtd89IV8UHImI0fHGqa88DzP87zHVAUoscL1WSHv9bE8zXW5yKj/tJMpJziVIeGJKTbto07tIrQ==} hasBin: true peerDependencies: - devframe: 0.7.8 + devframe: 0.7.9 vite: ^8.1.5 peerDependenciesMeta: vite: @@ -6023,19 +6002,8 @@ packages: '@modelcontextprotocol/sdk': optional: true - devframe@0.7.7: - resolution: {integrity: sha512-n8Sruz+6bNAZhR+NhUUUPNtz2tvEuwIHpJh8If1RQTgYGxeUoQoIEfsW0e5xVr1BewN4EfKVLTb+CycLcExcqw==} - peerDependencies: - '@modelcontextprotocol/sdk': ^1.0.0 - cac: ^7.0.0 - peerDependenciesMeta: - '@modelcontextprotocol/sdk': - optional: true - cac: - optional: true - - devframe@0.7.8: - resolution: {integrity: sha512-NewdglfH7C3vufTY4ipsyIptfz2l8fhIsuHEmsaNhp+fLqwi1gzXhnmncnympUwIYCQ+Io03BAFnnC5rNIvTCQ==} + devframe@0.7.9: + resolution: {integrity: sha512-ZWSHN5uk6KwCsrCXhu373kdBnk5kxkgxmGpD8tvv5DablgUOfWz+JrP4ufufTrns6PtQFP1Nh5S7sffWoLLp8w==} peerDependencies: '@modelcontextprotocol/sdk': ^1.0.0 cac: ^7.0.0 @@ -10197,93 +10165,52 @@ snapshots: tinyexec: 1.2.4 zigpty: 0.2.1 - '@devframes/hub@0.7.7(devframe@0.7.7(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))': - dependencies: - birpc: 4.0.0 - destr: 2.0.5 - devframe: 0.7.7(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) - nostics: 1.2.0 - pathe: 2.0.3 - perfect-debounce: 2.1.0 - tinyexec: 1.2.4 - zigpty: 0.2.1 - optional: true - - '@devframes/hub@0.7.8(devframe@0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))': + '@devframes/hub@0.7.9(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))': dependencies: birpc: 4.0.0 destr: 2.0.5 - devframe: 0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) + devframe: 0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) nostics: 1.2.0 pathe: 2.0.3 perfect-debounce: 2.1.0 tinyexec: 1.2.4 zigpty: 0.2.1 - '@devframes/plugin-inspect@0.7.7(devframe@0.7.7(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))(valibot@1.4.2(typescript@6.0.3))(vite@8.1.5)': + '@devframes/json-render@0.7.9(@devframes/hub@0.7.9(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)))(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))': dependencies: - '@valibot/to-json-schema': 1.7.1(valibot@1.4.2(typescript@6.0.3)) - cac: 7.0.0 - devframe: 0.7.7(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) + '@json-render/core': 0.19.0(zod@4.3.6) + devframe: 0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) nostics: 1.2.0 + zod: 4.3.6 optionalDependencies: - vite: 8.1.5(@types/node@25.0.3)(@vitejs/devtools@0.4.2)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.1)(yaml@2.9.0) - transitivePeerDependencies: - - valibot - optional: true + '@devframes/hub': 0.7.9(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)) - '@devframes/plugin-inspect@0.7.8(devframe@0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))(valibot@1.4.2(typescript@6.0.3))(vite@8.1.5)': + '@devframes/plugin-inspect@0.7.9(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))(valibot@1.4.2(typescript@6.0.3))(vite@8.1.5)': dependencies: '@valibot/to-json-schema': 1.7.1(valibot@1.4.2(typescript@6.0.3)) cac: 7.0.0 - devframe: 0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) + devframe: 0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) nostics: 1.2.0 optionalDependencies: vite: 8.1.5(@types/node@25.0.3)(@vitejs/devtools@0.3.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.1)(yaml@2.9.0) transitivePeerDependencies: - valibot - '@devframes/plugin-messages@0.7.7(@devframes/hub@0.7.7(devframe@0.7.7(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)))(devframe@0.7.7(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))(vite@8.1.5)': - dependencies: - '@devframes/hub': 0.7.7(devframe@0.7.7(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)) - cac: 7.0.0 - devframe: 0.7.7(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) - nostics: 1.2.0 - optionalDependencies: - vite: 8.1.5(@types/node@25.0.3)(@vitejs/devtools@0.4.2)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.1)(yaml@2.9.0) - optional: true - - '@devframes/plugin-messages@0.7.8(@devframes/hub@0.7.8(devframe@0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)))(devframe@0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))(vite@8.1.5)': + '@devframes/plugin-messages@0.7.9(@devframes/hub@0.7.9(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)))(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))(vite@8.1.5)': dependencies: - '@devframes/hub': 0.7.8(devframe@0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)) + '@devframes/hub': 0.7.9(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)) cac: 7.0.0 - devframe: 0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) + devframe: 0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) nostics: 1.2.0 optionalDependencies: vite: 8.1.5(@types/node@25.0.3)(@vitejs/devtools@0.3.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.1)(yaml@2.9.0) - '@devframes/plugin-terminals@0.7.7(devframe@0.7.7(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))(typescript@6.0.3)(vite@8.1.5)': + '@devframes/plugin-terminals@0.7.9(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))(typescript@6.0.3)(vite@8.1.5)': dependencies: '@xterm/addon-fit': 0.11.0 '@xterm/xterm': 6.0.0 cac: 7.0.0 - devframe: 0.7.7(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) - nostics: 1.2.0 - pathe: 2.0.3 - valibot: 1.4.2(typescript@6.0.3) - zigpty: 0.2.1 - optionalDependencies: - vite: 8.1.5(@types/node@25.0.3)(@vitejs/devtools@0.4.2)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.44.1)(tsx@4.23.1)(yaml@2.9.0) - transitivePeerDependencies: - - typescript - optional: true - - '@devframes/plugin-terminals@0.7.8(devframe@0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))(typescript@6.0.3)(vite@8.1.5)': - dependencies: - '@xterm/addon-fit': 0.11.0 - '@xterm/xterm': 6.0.0 - cac: 7.0.0 - devframe: 0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) + devframe: 0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) nostics: 1.2.0 pathe: 2.0.3 valibot: 1.4.2(typescript@6.0.3) @@ -13352,8 +13279,8 @@ snapshots: '@vitejs/devtools-kit@0.4.2(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)(vite@8.1.5)': dependencies: - '@devframes/hub': 0.7.7(devframe@0.7.7(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)) - devframe: 0.7.7(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) + '@devframes/hub': 0.7.9(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)) + devframe: 0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) local-pkg: 1.2.1 mlly: 1.8.2 nostics: 1.2.0 @@ -13370,7 +13297,7 @@ snapshots: dependencies: '@vitejs/devtools-kit': 0.4.2(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)(vite@8.1.5) cac: 7.0.0 - devframe: 0.7.7(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) + devframe: 0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) local-pkg: 1.2.1 nostics: 1.2.0 pathe: 2.0.3 @@ -13550,14 +13477,14 @@ snapshots: '@vitejs/devtools@0.4.2(@vitejs/devtools-oxc@0.4.2)(@vitejs/devtools-rolldown@0.4.2)(@vitejs/devtools-vite@0.4.2)(@vitejs/devtools-vitest@0.4.2)(crossws@0.4.10(srvx@0.11.22))(srvx@0.11.22)(typescript@6.0.3)(valibot@1.4.2(typescript@6.0.3))(vite@8.1.5)': dependencies: - '@devframes/hub': 0.7.7(devframe@0.7.7(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)) - '@devframes/plugin-inspect': 0.7.7(devframe@0.7.7(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))(valibot@1.4.2(typescript@6.0.3))(vite@8.1.5) - '@devframes/plugin-messages': 0.7.7(@devframes/hub@0.7.7(devframe@0.7.7(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)))(devframe@0.7.7(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))(vite@8.1.5) - '@devframes/plugin-terminals': 0.7.7(devframe@0.7.7(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))(typescript@6.0.3)(vite@8.1.5) + '@devframes/hub': 0.7.9(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)) + '@devframes/plugin-inspect': 0.7.9(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))(valibot@1.4.2(typescript@6.0.3))(vite@8.1.5) + '@devframes/plugin-messages': 0.7.9(@devframes/hub@0.7.9(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)))(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))(vite@8.1.5) + '@devframes/plugin-terminals': 0.7.9(devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3))(typescript@6.0.3)(vite@8.1.5) '@vitejs/devtools-kit': 0.4.2(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3)(vite@8.1.5) birpc: 4.0.0 cac: 7.0.0 - devframe: 0.7.7(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) + devframe: 0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3) h3: 2.0.1-rc.22(crossws@0.4.10(srvx@0.11.22)) local-pkg: 1.2.1 mlly: 1.8.2 @@ -15035,26 +14962,7 @@ snapshots: - srvx - typescript - devframe@0.7.7(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3): - dependencies: - '@valibot/to-json-schema': 1.7.1(valibot@1.4.2(typescript@6.0.3)) - birpc: 4.0.0 - crossws: 0.4.10(srvx@0.11.22) - destr: 2.0.5 - h3: 2.0.1-rc.22(crossws@0.4.10(srvx@0.11.22)) - mrmime: 2.0.1 - nostics: 1.2.0 - pathe: 2.0.3 - ufo: 1.6.4 - valibot: 1.4.2(typescript@6.0.3) - optionalDependencies: - cac: 7.0.0 - transitivePeerDependencies: - - srvx - - typescript - optional: true - - devframe@0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@5.9.3): + devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@5.9.3): dependencies: '@valibot/to-json-schema': 1.7.1(valibot@1.4.2(typescript@6.0.3)) birpc: 4.0.0 @@ -15072,7 +14980,7 @@ snapshots: - srvx - typescript - devframe@0.7.8(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3): + devframe@0.7.9(cac@7.0.0)(srvx@0.11.22)(typescript@6.0.3): dependencies: '@valibot/to-json-schema': 1.7.1(valibot@1.4.2(typescript@6.0.3)) birpc: 4.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6afb6dae3..fd9858681 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -60,7 +60,7 @@ catalogs: vite: ^8.1.5 deps: '@devframes/hub': ^0.7.9 - '@devframes/json-render': ^0.7.8 + '@devframes/json-render': ^0.7.9 '@devframes/plugin-inspect': ^0.7.9 '@devframes/plugin-messages': ^0.7.9 '@devframes/plugin-terminals': ^0.7.9 diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.d.ts index e71206736..0ad0cb43b 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.d.ts @@ -38,7 +38,7 @@ export interface JsonRenderer { readonly upstreamVersion: string; readonly view: JsonRenderViewRef; } -export interface KitNodeContext extends DevframeHubContext { +export interface KitNodeContext extends Omit { readonly viteConfig?: ResolvedConfig; readonly viteServer?: ViteDevServer; createJsonRenderer: (_: JsonRenderSpec) => JsonRenderer; diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/node.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/node.snapshot.d.ts index 9eccec616..6dfb36268 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/node.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/node.snapshot.d.ts @@ -28,10 +28,11 @@ export interface InstallLauncherOptions { groupId?: string; label?: string; name?: string; + pkg?: string; install: string[]; dev?: boolean; } -export interface KitNodeContext extends DevframeHubContext { +export interface KitNodeContext extends Omit { readonly viteConfig?: ResolvedConfig; readonly viteServer?: ViteDevServer; createJsonRenderer: (_: JsonRenderSpec) => JsonRenderer; From 199bbef0af71301d9d823db2f5b1d9348af8bab1 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Wed, 22 Jul 2026 08:02:16 +0000 Subject: [PATCH 3/3] feat(core): render an "unsupported component" fallback in json-render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An element whose `type` has no entry in the in-house registry — a spec authored against a newer base-catalog version than this client implements, a third-party catalog extension, or a plain typo — silently rendered nothing: upstream `@json-render/vue` only logs a console warning and returns `null` when `registry[type]` is missing. Add `UnsupportedComponent`, wired as ``'s upstream `fallback` prop (already supported by `@json-render/vue`, just previously unused here) in both `ViewJsonRender.vue` and the Storybook gallery. An unrenderable element now degrades to a visible, inspectable placeholder — the element's `type` name in a dashed warning-colored box — instead of disappearing. Also adds a dedicated `UnsupportedComponentFallback` story and a short note in the json-render docs. --- docs/kit/json-render.md | 2 + .../components/views/ViewJsonRender.vue | 4 +- .../json-render/JsonRender.stories.ts | 25 +++++++++-- .../components/UnsupportedComponent.ts | 41 +++++++++++++++++++ .../webcomponents/json-render/registry.ts | 8 ++++ 5 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 packages/core/src/client/webcomponents/json-render/components/UnsupportedComponent.ts diff --git a/docs/kit/json-render.md b/docs/kit/json-render.md index 5a0c20990..1c546c5c2 100644 --- a/docs/kit/json-render.md +++ b/docs/kit/json-render.md @@ -215,6 +215,8 @@ ctx.rpc.register(defineRpcFunction({ ## Built-in components +An element whose `type` doesn't match any of the components below — e.g. a spec authored against a newer base-catalog version than the connected client implements, or a plain typo — renders as a visible "Unsupported component" placeholder instead of disappearing silently, so a mismatch is easy to spot during development. + ### Layout #### Stack diff --git a/packages/core/src/client/webcomponents/components/views/ViewJsonRender.vue b/packages/core/src/client/webcomponents/components/views/ViewJsonRender.vue index a192b7d9a..723e93d57 100644 --- a/packages/core/src/client/webcomponents/components/views/ViewJsonRender.vue +++ b/packages/core/src/client/webcomponents/components/views/ViewJsonRender.vue @@ -4,7 +4,7 @@ import type { DevToolsViewJsonRender } from '@vitejs/devtools-kit' import type { DocksContext } from '@vitejs/devtools-kit/client' import { JSONUIProvider, Renderer } from '@json-render/vue' import { computed, markRaw, onMounted, ref, shallowRef, watch } from 'vue' -import { devtoolsRegistry } from '../../json-render/registry' +import { devtoolsRegistry, UnsupportedComponent } from '../../json-render/registry' const props = defineProps<{ context: DocksContext @@ -100,7 +100,7 @@ watch(() => props.entry.ui?._stateKey, loadSpec) :handlers="actionHandlers" :initial-state="initialState" > - +
No spec provided diff --git a/packages/core/src/client/webcomponents/json-render/JsonRender.stories.ts b/packages/core/src/client/webcomponents/json-render/JsonRender.stories.ts index 01c85ea1d..656646255 100644 --- a/packages/core/src/client/webcomponents/json-render/JsonRender.stories.ts +++ b/packages/core/src/client/webcomponents/json-render/JsonRender.stories.ts @@ -2,11 +2,12 @@ import type { Spec } from '@json-render/core' import type { Meta, StoryObj } from '@storybook/vue3-vite' import { JSONUIProvider, Renderer } from '@json-render/vue' import { defineComponent, h } from 'vue' -import { devtoolsRegistry } from './registry' +import { devtoolsRegistry, UnsupportedComponent } from './registry' /** * Render a json-render `Spec` with the DevTools registry, the same way - * `ViewJsonRender` does at runtime. + * `ViewJsonRender` does at runtime — including the `UnsupportedComponent` + * fallback for any element `type` absent from the registry. */ function renderSpec(spec: Spec) { return defineComponent({ @@ -16,7 +17,7 @@ function renderSpec(spec: Spec) { 'div', { class: 'max-w-160 p6 bg-base color-base font-sans' }, h(JSONUIProvider, { registry: devtoolsRegistry, handlers: {}, initialState }, { - default: () => h(Renderer, { spec, registry: devtoolsRegistry }), + default: () => h(Renderer, { spec, registry: devtoolsRegistry, fallback: UnsupportedComponent }), }), ) }, @@ -90,3 +91,21 @@ export const Card: Story = { }, } as unknown as Spec), } + +/** + * An element whose `type` has no entry in the registry — e.g. authored + * against a newer base-catalog version than this client implements, or a + * plain typo — falls back to a visible, inspectable placeholder instead of + * silently rendering nothing. + */ +export const UnsupportedComponentFallback: Story = { + render: () => renderSpec({ + root: 'root', + state: {}, + elements: { + root: { type: 'Stack', props: { direction: 'column', gap: 8, padding: 4 }, children: ['label', 'unknown'] }, + label: { type: 'Text', props: { text: 'The next element uses an unrecognized component type:', variant: 'caption' } }, + unknown: { type: 'FutureChart', props: { series: [1, 2, 3] } }, + }, + } as unknown as Spec), +} diff --git a/packages/core/src/client/webcomponents/json-render/components/UnsupportedComponent.ts b/packages/core/src/client/webcomponents/json-render/components/UnsupportedComponent.ts new file mode 100644 index 000000000..e228e4dd0 --- /dev/null +++ b/packages/core/src/client/webcomponents/json-render/components/UnsupportedComponent.ts @@ -0,0 +1,41 @@ +import type { RegistryComponentProps } from './types' +import { defineComponent, h } from 'vue' +import { colors } from './tokens' + +/** + * Fallback rendered in place of any spec element whose `type` has no entry in + * {@link devtoolsRegistry}. Passed as `Renderer`'s `fallback` prop (an + * upstream `@json-render/vue` feature) so an unrecognized component type — + * e.g. a spec authored against a newer base-catalog version than this + * client's registry implements, or a plain typo — degrades to a visible, + * inspectable placeholder instead of silently rendering nothing. + */ +export const UnsupportedComponent = defineComponent({ + name: 'JrUnsupportedComponent', + props: ['element', 'emit', 'on', 'bindings', 'loading'], + setup(ctx: RegistryComponentProps) { + return () => { + const type = ctx.element?.type ?? 'unknown' + return h('div', { + class: 'jr-unsupported', + role: 'alert', + title: `No renderer is registered for component type "${type}". It may belong to a newer base-catalog version than this client supports.`, + style: { + display: 'inline-flex', + alignItems: 'center', + gap: '6px', + padding: '4px 8px', + borderRadius: '4px', + fontSize: '11px', + fontFamily: 'monospace', + border: `1px dashed ${colors.warning.fg}`, + backgroundColor: colors.warning.bg, + color: colors.warning.fg, + }, + }, [ + h('span', { 'aria-hidden': 'true' }, '⚠'), + h('span', `Unsupported component: "${type}"`), + ]) + } + }, +}) diff --git a/packages/core/src/client/webcomponents/json-render/registry.ts b/packages/core/src/client/webcomponents/json-render/registry.ts index 3fad4259a..06824684b 100644 --- a/packages/core/src/client/webcomponents/json-render/registry.ts +++ b/packages/core/src/client/webcomponents/json-render/registry.ts @@ -13,6 +13,14 @@ import { Switch } from './components/Switch' import { Text } from './components/Text' import { TextInput } from './components/TextInput' import { Tree } from './components/Tree' +import { UnsupportedComponent } from './components/UnsupportedComponent' + +/** + * Fallback for any spec element whose `type` isn't a key in + * {@link devtoolsRegistry} — pass as `` so unrecognized + * component types render a visible placeholder instead of nothing. + */ +export { UnsupportedComponent } export const devtoolsRegistry: Record = { Stack,