From 9e3b192a04bb3d71cf0de1a8367a0bb257e781a6 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Fri, 10 Jul 2026 00:37:05 +0000 Subject: [PATCH 1/6] feat(core): add Storybook coverage for dock and floating-panel components Adds a top-level `storybook/` workspace (theme-synced manager + preview, autodocs, a generated Overview page) and co-located stories for the webcomponents dock UI: the floating dock bar, edge/float dock modes, panel, groups (button/popover/sidebar), overflow, entries and the floating popover. Stories boot the real dock context over an in-memory mock RPC, so grouping, selection, commands and when-clauses behave as they do at runtime. Makes the shells controllable for isolation without changing runtime behavior: a `#view` slot on DockPanel/DockEdge, and a `useIsRpcTrusted` composable that replaces three uncleaned trust subscriptions. Story-only utility classes are kept out of the shipped shadow-DOM stylesheet. --- .gitignore | 1 + .../components/dock/Dock.stories.ts | 103 ++ .../webcomponents/components/dock/Dock.vue | 5 +- .../components/dock/DockEdge.stories.ts | 81 ++ .../components/dock/DockEdge.vue | 26 +- .../components/dock/DockEmbedded.stories.ts | 71 ++ .../components/dock/DockEmbedded.vue | 8 +- .../dock/DockEntriesWithCategories.stories.ts | 77 ++ .../components/dock/DockEntry.stories.ts | 87 ++ .../dock/DockGroupButton.stories.ts | 93 ++ .../dock/DockGroupPopover.stories.ts | 86 ++ .../dock/DockGroupSidebar.stories.ts | 56 + .../components/dock/DockIcon.stories.ts | 48 + .../dock/DockOverflowButton.stories.ts | 49 + .../components/dock/DockPanel.stories.ts | 73 ++ .../components/dock/DockPanel.vue | 34 +- .../components/dock/DockStandalone.vue | 7 +- .../floating/FloatingPopover.stories.ts | 57 + .../client/webcomponents/scripts/build-css.ts | 4 + .../stories/__tests__/mock-context.test.ts | 54 + .../client/webcomponents/stories/fixtures.ts | 77 ++ .../webcomponents/stories/mock-context.ts | 110 ++ .../webcomponents/stories/story-helpers.ts | 58 + .../webcomponents/utils/useIsRpcTrusted.ts | 25 + pnpm-lock.yaml | 1061 ++++++++++++++++- pnpm-workspace.yaml | 5 + storybook/.storybook/docs-dark.css | 20 + storybook/.storybook/main.ts | 37 + storybook/.storybook/manager.ts | 9 + storybook/.storybook/preview.ts | 42 + storybook/package.json | 30 + storybook/scripts/gen-overview.mts | 82 ++ storybook/stories/Overview.mdx | 71 ++ storybook/tsconfig.json | 16 + storybook/uno.config.ts | 54 + 35 files changed, 2656 insertions(+), 61 deletions(-) create mode 100644 packages/core/src/client/webcomponents/components/dock/Dock.stories.ts create mode 100644 packages/core/src/client/webcomponents/components/dock/DockEdge.stories.ts create mode 100644 packages/core/src/client/webcomponents/components/dock/DockEmbedded.stories.ts create mode 100644 packages/core/src/client/webcomponents/components/dock/DockEntriesWithCategories.stories.ts create mode 100644 packages/core/src/client/webcomponents/components/dock/DockEntry.stories.ts create mode 100644 packages/core/src/client/webcomponents/components/dock/DockGroupButton.stories.ts create mode 100644 packages/core/src/client/webcomponents/components/dock/DockGroupPopover.stories.ts create mode 100644 packages/core/src/client/webcomponents/components/dock/DockGroupSidebar.stories.ts create mode 100644 packages/core/src/client/webcomponents/components/dock/DockIcon.stories.ts create mode 100644 packages/core/src/client/webcomponents/components/dock/DockOverflowButton.stories.ts create mode 100644 packages/core/src/client/webcomponents/components/dock/DockPanel.stories.ts create mode 100644 packages/core/src/client/webcomponents/components/floating/FloatingPopover.stories.ts create mode 100644 packages/core/src/client/webcomponents/stories/__tests__/mock-context.test.ts create mode 100644 packages/core/src/client/webcomponents/stories/fixtures.ts create mode 100644 packages/core/src/client/webcomponents/stories/mock-context.ts create mode 100644 packages/core/src/client/webcomponents/stories/story-helpers.ts create mode 100644 packages/core/src/client/webcomponents/utils/useIsRpcTrusted.ts create mode 100644 storybook/.storybook/docs-dark.css create mode 100644 storybook/.storybook/main.ts create mode 100644 storybook/.storybook/manager.ts create mode 100644 storybook/.storybook/preview.ts create mode 100644 storybook/package.json create mode 100644 storybook/scripts/gen-overview.mts create mode 100644 storybook/stories/Overview.mdx create mode 100644 storybook/tsconfig.json create mode 100644 storybook/uno.config.ts diff --git a/.gitignore b/.gitignore index 0f9cf2a7..b547a88f 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ packages/kit/skills *.tsbuildinfo **/.vitepress/cache .turbo +storybook-static .context .ghfs diff --git a/packages/core/src/client/webcomponents/components/dock/Dock.stories.ts b/packages/core/src/client/webcomponents/components/dock/Dock.stories.ts new file mode 100644 index 00000000..3c81671f --- /dev/null +++ b/packages/core/src/client/webcomponents/components/dock/Dock.stories.ts @@ -0,0 +1,103 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { h } from 'vue' +import { categorizedEntries, groupedEntries, overflowEntries } from '../../stories/fixtures' +import { mountWithContext } from '../../stories/story-helpers' +import FloatingElements from '../floating/FloatingElements.vue' +import Dock from './Dock.vue' + +const meta = { + title: 'Dock/Shell/Float Bar', + component: Dock, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: 'The floating dock bar (float mode). It anchors to an edge of the viewport and can be dragged around. These stories pin `inactiveTimeout: -1` so the bar stays expanded.', + }, + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +/** Default: docked to the bottom-center of the viewport. */ +export const Bottom: Story = { + render: () => ({ + setup: () => mountWithContext( + { entries: categorizedEntries, panel: { position: 'bottom', left: 50, top: 100, inactiveTimeout: -1 } }, + ctx => [h(Dock, { context: ctx }), h(FloatingElements)], + ), + }), +} + +/** Docked to the top edge. */ +export const Top: Story = { + render: () => ({ + setup: () => mountWithContext( + { entries: categorizedEntries, panel: { position: 'top', left: 50, top: 0, inactiveTimeout: -1 } }, + ctx => [h(Dock, { context: ctx }), h(FloatingElements)], + ), + }), +} + +/** Docked to the left edge — the bar rotates to vertical. */ +export const Left: Story = { + render: () => ({ + setup: () => mountWithContext( + { entries: categorizedEntries, panel: { position: 'left', left: 0, top: 50, inactiveTimeout: -1 } }, + ctx => [h(Dock, { context: ctx }), h(FloatingElements)], + ), + }), +} + +/** Docked to the right edge. */ +export const Right: Story = { + render: () => ({ + setup: () => mountWithContext( + { entries: categorizedEntries, panel: { position: 'right', left: 100, top: 50, inactiveTimeout: -1 } }, + ctx => [h(Dock, { context: ctx }), h(FloatingElements)], + ), + }), +} + +/** With collapsed groups on the bar. */ +export const WithGroups: Story = { + render: () => ({ + setup: () => mountWithContext( + { entries: groupedEntries, panel: { position: 'bottom', left: 50, top: 100, inactiveTimeout: -1 } }, + ctx => [h(Dock, { context: ctx }), h(FloatingElements)], + ), + }), +} + +/** Past capacity: an overflow button appears at the end of the bar. */ +export const WithOverflow: Story = { + render: () => ({ + setup: () => mountWithContext( + { entries: overflowEntries, panel: { position: 'bottom', left: 50, top: 100, inactiveTimeout: -1 } }, + ctx => [h(Dock, { context: ctx }), h(FloatingElements)], + ), + }), +} + +/** Collapsed to the minimized nub (`inactiveTimeout: 0`). */ +export const Minimized: Story = { + render: () => ({ + setup: () => mountWithContext( + { entries: categorizedEntries, panel: { position: 'bottom', left: 50, top: 100, inactiveTimeout: 0 } }, + ctx => [h(Dock, { context: ctx }), h(FloatingElements)], + ), + }), +} + +/** Unauthorized: the bar shows the warning affordance instead of the tools. */ +export const Unauthorized: Story = { + render: () => ({ + setup: () => mountWithContext( + { entries: categorizedEntries, isTrusted: false, panel: { position: 'bottom', left: 50, top: 100, inactiveTimeout: -1 } }, + ctx => [h(Dock, { context: ctx }), h(FloatingElements)], + ), + }), +} diff --git a/packages/core/src/client/webcomponents/components/dock/Dock.vue b/packages/core/src/client/webcomponents/components/dock/Dock.vue index 1067d044..7a6c96bb 100644 --- a/packages/core/src/client/webcomponents/components/dock/Dock.vue +++ b/packages/core/src/client/webcomponents/components/dock/Dock.vue @@ -5,6 +5,7 @@ import { computed, onMounted, reactive, ref, useTemplateRef, watchEffect } from import { BUILTIN_ENTRY_CLIENT_AUTH_NOTICE } from '../../constants' import { docksSplitGroupsWithCapacity } from '../../state/dock-settings' import { setDocksOverflowPanel } from '../../state/floating-tooltip' +import { useIsRpcTrusted } from '../../utils/useIsRpcTrusted' import BracketLeft from '../icons/BracketLeft.vue' import BracketRight from '../icons/BracketRight.vue' import VitePlusCore from '../icons/VitePlusCore.vue' @@ -68,9 +69,7 @@ function onPointerDown(e: PointerEvent) { draggingOffset.y = e.clientY - top - height / 2 } -const isRpcTrusted = ref(context.rpc.isTrusted) -context.rpc.events.on('rpc:is-trusted:updated', (isTrusted) => { - isRpcTrusted.value = isTrusted +const isRpcTrusted = useIsRpcTrusted(context, (isTrusted) => { if (isTrusted && context.docks.selected?.id === BUILTIN_ENTRY_CLIENT_AUTH_NOTICE.id) { context.docks.switchEntry(null) } diff --git a/packages/core/src/client/webcomponents/components/dock/DockEdge.stories.ts b/packages/core/src/client/webcomponents/components/dock/DockEdge.stories.ts new file mode 100644 index 00000000..d2198ed9 --- /dev/null +++ b/packages/core/src/client/webcomponents/components/dock/DockEdge.stories.ts @@ -0,0 +1,81 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { h } from 'vue' +import { categorizedEntries, groupedEntries } from '../../stories/fixtures' +import { mountWithContext } from '../../stories/story-helpers' +import FloatingElements from '../floating/FloatingElements.vue' +import DockEdge from './DockEdge.vue' + +/** A stand-in panel body (the real one mounts iframe/custom views). */ +function body(entry: any) { + return h('div', { class: 'w-full h-full p6 font-sans color-base of-auto' }, [ + h('div', { class: 'text-lg font-medium mb2' }, entry?.title ?? 'No selection'), + h('div', { class: 'op60 text-sm' }, `Panel content for "${entry?.id ?? '—'}"`), + ]) +} + +const meta = { + title: 'Dock/Shell/Edge Panel', + component: DockEdge, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: 'The edge-docked shell (edge mode): a toolbar pinned to one viewport edge with a resizable panel. The `#view` slot is stubbed here in place of the live view renderer.', + }, + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +function edgeStory(position: 'top' | 'right' | 'bottom' | 'left', open = true) { + return { + render: () => ({ + setup: () => mountWithContext( + { + entries: categorizedEntries, + selectedId: open ? 'overview' : null, + panel: { mode: 'edge', position, open, height: 40, width: 30 }, + }, + ctx => [ + h(DockEdge, { context: ctx }, { view: ({ entry }: any) => body(entry) }), + h(FloatingElements), + ], + ), + }), + } satisfies Story +} + +/** Bottom edge with the panel open. */ +export const Bottom: Story = edgeStory('bottom') + +/** Top edge. */ +export const Top: Story = edgeStory('top') + +/** Left edge — toolbar runs vertically. */ +export const Left: Story = edgeStory('left') + +/** Right edge. */ +export const Right: Story = edgeStory('right') + +/** Toolbar only — nothing selected, so the panel body is collapsed away. */ +export const ToolbarOnly: Story = edgeStory('bottom', false) + +/** Edge dock hosting a group — the group rail shows inside the panel. */ +export const WithGroup: Story = { + render: () => ({ + setup: () => mountWithContext( + { + entries: groupedEntries, + selectedId: 'nuxt:overview', + panel: { mode: 'edge', position: 'bottom', open: true, height: 45 }, + }, + ctx => [ + h(DockEdge, { context: ctx }, { view: ({ entry }: any) => body(entry) }), + h(FloatingElements), + ], + ), + }), +} diff --git a/packages/core/src/client/webcomponents/components/dock/DockEdge.vue b/packages/core/src/client/webcomponents/components/dock/DockEdge.vue index 7ffacb80..a469a3d7 100644 --- a/packages/core/src/client/webcomponents/components/dock/DockEdge.vue +++ b/packages/core/src/client/webcomponents/components/dock/DockEdge.vue @@ -263,18 +263,20 @@ const contentClass = computed(() => { :selected-id="selectedEntry?.id ?? null" />
- -
+ + +
+
diff --git a/packages/core/src/client/webcomponents/components/dock/DockEmbedded.stories.ts b/packages/core/src/client/webcomponents/components/dock/DockEmbedded.stories.ts new file mode 100644 index 00000000..9d66b5a7 --- /dev/null +++ b/packages/core/src/client/webcomponents/components/dock/DockEmbedded.stories.ts @@ -0,0 +1,71 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import type { DevToolsDockEntry } from '@vitejs/devtools-kit' +import { h } from 'vue' +import { categorizedEntries } from '../../stories/fixtures' +import { mountWithContext } from '../../stories/story-helpers' +import DockEmbedded from './DockEmbedded.vue' + +// Entries whose iframes load `about:blank`, so the full shell (address bar + +// iframe pane) renders without reaching for a dev server. +const blankEntries: DevToolsDockEntry[] = [ + { id: 'overview', type: 'iframe', url: 'about:blank', title: 'Overview', icon: 'ph:gauge-duotone' }, + { id: 'inspect', type: 'iframe', url: 'about:blank', title: 'Inspect', icon: 'ph:stethoscope-duotone' }, + { id: 'assets', type: 'iframe', url: 'about:blank', title: 'Assets', icon: 'ph:images-duotone' }, +] as DevToolsDockEntry[] + +const meta = { + title: 'Dock/Shell/Embedded', + component: DockEmbedded, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: 'The full embedded shell as injected into a host app: the dock bar plus panel (float or edge), floating overlays, command palette, toasts and confirm dialog — switched by `panel.store.mode`.', + }, + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +/** Float mode, panel closed — just the resting dock bar. */ +export const FloatClosed: Story = { + render: () => ({ + setup: () => mountWithContext( + { entries: categorizedEntries, panel: { mode: 'float', position: 'bottom', left: 50, top: 100, inactiveTimeout: -1 } }, + ctx => h(DockEmbedded, { context: ctx }), + ), + }), +} + +/** Float mode with the panel open over an `about:blank` iframe. */ +export const FloatOpen: Story = { + render: () => ({ + setup: () => mountWithContext( + { entries: blankEntries, selectedId: 'overview', panel: { mode: 'float', position: 'bottom', left: 50, top: 100, inactiveTimeout: -1, width: 60, height: 60 } }, + ctx => h(DockEmbedded, { context: ctx }), + ), + }), +} + +/** Edge mode, docked to the bottom with the panel open. */ +export const Edge: Story = { + render: () => ({ + setup: () => mountWithContext( + { entries: blankEntries, selectedId: 'overview', panel: { mode: 'edge', position: 'bottom', open: true, height: 45 } }, + ctx => h(DockEmbedded, { context: ctx }), + ), + }), +} + +/** Unauthorized — the shell forces float mode and shows the warning. */ +export const Unauthorized: Story = { + render: () => ({ + setup: () => mountWithContext( + { entries: categorizedEntries, isTrusted: false, panel: { mode: 'edge', position: 'bottom', inactiveTimeout: -1 } }, + ctx => h(DockEmbedded, { context: ctx }), + ), + }), +} diff --git a/packages/core/src/client/webcomponents/components/dock/DockEmbedded.vue b/packages/core/src/client/webcomponents/components/dock/DockEmbedded.vue index c0bbc98b..2d7d04fa 100644 --- a/packages/core/src/client/webcomponents/components/dock/DockEmbedded.vue +++ b/packages/core/src/client/webcomponents/components/dock/DockEmbedded.vue @@ -1,9 +1,10 @@ diff --git a/packages/core/src/client/webcomponents/components/message/MessageItem.stories.ts b/packages/core/src/client/webcomponents/components/message/MessageItem.stories.ts new file mode 100644 index 00000000..3504b460 --- /dev/null +++ b/packages/core/src/client/webcomponents/components/message/MessageItem.stories.ts @@ -0,0 +1,41 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { h } from 'vue' +import { message, sampleMessages } from '../../stories/fixtures' +import MessageItem from './MessageItem.vue' + +const meta = { + title: 'Messages/MessageItem', + component: MessageItem, + tags: ['autodocs'], + decorators: [() => ({ template: '
' })], + parameters: { + docs: { + description: { + component: 'A single log/message row: level accent, icon, title, description, relative time and category/label chips.', + }, + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Error: Story = { + render: () => ({ setup: () => () => h(MessageItem, { entry: sampleMessages[0]! }) }), +} + +export const Loading: Story = { + render: () => ({ setup: () => () => h(MessageItem, { entry: message({ level: 'warn', message: 'Running checks…', status: 'loading', category: 'a11y' }) }) }), +} + +/** The compact variant used inside toasts (no timestamp / chips). */ +export const Compact: Story = { + render: () => ({ setup: () => () => h(MessageItem, { entry: sampleMessages[2]!, compact: true }) }), +} + +/** Every level stacked together. */ +export const AllLevels: Story = { + render: () => ({ + setup: () => () => h('div', { class: 'flex flex-col gap-3' }, sampleMessages.map(entry => h(MessageItem, { key: entry.id, entry }))), + }), +} diff --git a/packages/core/src/client/webcomponents/components/views-builtin/ViewBuiltinClientAuthNotice.stories.ts b/packages/core/src/client/webcomponents/components/views-builtin/ViewBuiltinClientAuthNotice.stories.ts new file mode 100644 index 00000000..dddb5b14 --- /dev/null +++ b/packages/core/src/client/webcomponents/components/views-builtin/ViewBuiltinClientAuthNotice.stories.ts @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { h } from 'vue' +import { mountWithContext } from '../../stories/story-helpers' +import ViewBuiltinClientAuthNotice from './ViewBuiltinClientAuthNotice.vue' + +const meta = { + title: 'Views/Builtin/AuthNotice', + component: ViewBuiltinClientAuthNotice, + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: 'The authorization prompt shown when the client is not yet trusted — explains the risk and takes a one-time code.', + }, + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: () => ({ + setup: () => mountWithContext( + { isTrusted: false }, + ctx => h('div', { class: 'h-140 bg-base color-base border border-base rounded-lg overflow-auto' }, h(ViewBuiltinClientAuthNotice, { context: ctx })), + ), + }), +} diff --git a/packages/core/src/client/webcomponents/components/views-builtin/ViewBuiltinSettings.stories.ts b/packages/core/src/client/webcomponents/components/views-builtin/ViewBuiltinSettings.stories.ts new file mode 100644 index 00000000..a4a777fc --- /dev/null +++ b/packages/core/src/client/webcomponents/components/views-builtin/ViewBuiltinSettings.stories.ts @@ -0,0 +1,53 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import type { DevToolsViewBuiltin } from '@vitejs/devtools-kit' +import { h } from 'vue' +import { groupedEntries } from '../../stories/fixtures' +import { mountWithContext } from '../../stories/story-helpers' +import ViewBuiltinSettings from './ViewBuiltinSettings.vue' + +const entry: DevToolsViewBuiltin = { + type: '~builtin', + id: '~settings', + title: 'Settings', + icon: 'ph:gear-duotone', +} + +function stage(children: any) { + return h('div', { class: 'h-140 bg-base color-base border border-base rounded-lg overflow-hidden font-sans' }, children) +} + +const meta = { + title: 'Views/Builtin/Settings', + component: ViewBuiltinSettings, + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: 'The built-in settings view: Appearance, Docks, Shortcuts and Advanced tabs. Click the tabs to switch.', + }, + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +/** Embedded client — the Appearance tab shows dock-mode options. */ +export const Embedded: Story = { + render: () => ({ + setup: () => mountWithContext( + { entries: groupedEntries, clientType: 'embedded' }, + ctx => stage(h(ViewBuiltinSettings, { context: ctx, entry })), + ), + }), +} + +/** Standalone client — dock-mode options are hidden. */ +export const Standalone: Story = { + render: () => ({ + setup: () => mountWithContext( + { entries: groupedEntries, clientType: 'standalone' }, + ctx => stage(h(ViewBuiltinSettings, { context: ctx, entry })), + ), + }), +} diff --git a/packages/core/src/client/webcomponents/components/views/ViewLauncher.stories.ts b/packages/core/src/client/webcomponents/components/views/ViewLauncher.stories.ts new file mode 100644 index 00000000..e74439ad --- /dev/null +++ b/packages/core/src/client/webcomponents/components/views/ViewLauncher.stories.ts @@ -0,0 +1,52 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import type { DevToolsViewLauncher } from '@vitejs/devtools-kit' +import { h } from 'vue' +import { mountWithContext } from '../../stories/story-helpers' +import ViewLauncher from './ViewLauncher.vue' + +function launcher(status: 'idle' | 'loading' | 'error' | 'success'): DevToolsViewLauncher { + return { + id: 'launcher', + type: 'launcher', + title: 'Launcher', + icon: 'ph:rocket-launch-duotone', + launcher: { + title: 'Launch My Cool App', + description: 'Start the dev server and open it here.', + status, + }, + } as DevToolsViewLauncher +} + +function stage(children: any) { + return h('div', { class: 'h-100 bg-base color-base border border-base rounded-lg overflow-hidden font-sans' }, children) +} + +const meta = { + title: 'Views/Launcher', + component: ViewLauncher, + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: 'A launcher view: a call-to-action that runs the entry\'s `onLaunch` and reflects idle / loading / success / error status.', + }, + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +function launcherStory(status: 'idle' | 'loading' | 'error' | 'success'): Story { + return { + render: () => ({ + setup: () => mountWithContext({}, ctx => stage(h(ViewLauncher, { context: ctx, entry: launcher(status) }))), + }), + } +} + +export const Idle: Story = launcherStory('idle') +export const Loading: Story = launcherStory('loading') +export const Success: Story = launcherStory('success') +export const Error: Story = launcherStory('error') diff --git a/packages/core/src/client/webcomponents/json-render/JsonRender.stories.ts b/packages/core/src/client/webcomponents/json-render/JsonRender.stories.ts new file mode 100644 index 00000000..e7eabe0f --- /dev/null +++ b/packages/core/src/client/webcomponents/json-render/JsonRender.stories.ts @@ -0,0 +1,92 @@ +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' + +/** + * Render a json-render `Spec` with the DevTools registry, the same way + * `ViewJsonRender` does at runtime. + */ +function renderSpec(spec: Spec) { + return defineComponent({ + setup() { + const initialState = (spec as any).state ?? {} + return () => h( + 'div', + { class: 'max-w-160 p6 bg-base color-base font-sans' }, + h(JSONUIProvider, { registry: devtoolsRegistry, handlers: {}, initialState }, { + default: () => h(Renderer, { spec, registry: devtoolsRegistry }), + }), + ) + }, + }) +} + +const meta = { + title: 'JsonRender/Gallery', + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: 'The json-render primitive registry (`Stack`, `Card`, `Text`, `Badge`, `Button`, `Icon`, `Divider`, `Switch`, `KeyValueTable`, `DataTable`, `CodeBlock`, `Progress`) rendered from a declarative spec — the same renderer plugins use to build panels without shipping Vue.', + }, + }, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +/** Every common primitive laid out in one spec. */ +export const Gallery: Story = { + render: () => renderSpec({ + 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'] }, + 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' } }, + b4: { type: 'Badge', props: { text: 'v0.3.4', variant: 'default' } }, + buttons: { type: 'Stack', props: { direction: 'horizontal', 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' }, + ] } }, + table: { type: 'DataTable', props: { + columns: [{ key: 'file', label: 'File', width: '60%' }, { 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', + } }, + code: { type: 'CodeBlock', props: { filename: 'vite.config.ts', code: 'export default defineConfig({\n plugins: [DevTools()],\n})' } }, + }, + } as unknown as Spec), +} + +/** A `Card` grouping content under a titled, bordered surface. */ +export const Card: Story = { + render: () => renderSpec({ + root: 'root', + 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' } }, + badge: { type: 'Badge', props: { text: 'enabled', variant: 'success' } }, + }, + } as unknown as Spec), +} diff --git a/packages/core/src/client/webcomponents/stories/fixtures.ts b/packages/core/src/client/webcomponents/stories/fixtures.ts index ee66779a..497e8955 100644 --- a/packages/core/src/client/webcomponents/stories/fixtures.ts +++ b/packages/core/src/client/webcomponents/stories/fixtures.ts @@ -1,4 +1,4 @@ -import type { DevToolsDockEntry, DevToolsViewGroup } from '@vitejs/devtools-kit' +import type { DevToolsDockEntry, DevToolsMessageEntry, DevToolsViewGroup } from '@vitejs/devtools-kit' type Extra = Partial @@ -75,3 +75,24 @@ export const overflowEntries: DevToolsDockEntry[] = [ /** The Nuxt group entry on its own (for group-component stories). */ export const nuxtGroup = groupedEntries.find(e => e.id === 'nuxt') as DevToolsViewGroup + +/** A message/log entry for the messages panel and toasts. */ +export function message(extra: Partial = {}): DevToolsMessageEntry { + return { + id: `msg-${Math.random().toString(36).slice(2, 8)}`, + message: 'Something happened', + level: 'info', + from: 'server', + timestamp: Date.now(), + ...extra, + } as DevToolsMessageEntry +} + +/** One message per level, covering the common shapes. */ +export const sampleMessages: DevToolsMessageEntry[] = [ + message({ level: 'error', message: 'Failed to resolve import "./missing"', description: 'in src/main.ts', category: 'runtime', labels: ['vite'] }), + message({ level: 'warn', message: 'Large chunk detected (612 kB)', category: 'build' }), + message({ level: 'success', message: 'HMR update applied', category: 'hmr' }), + message({ level: 'info', message: 'Server restarted', description: 'config change detected', category: 'server' }), + message({ level: 'warn', message: 'Checking accessibility…', status: 'loading', category: 'a11y' }), +] diff --git a/packages/core/src/client/webcomponents/stories/mock-context.ts b/packages/core/src/client/webcomponents/stories/mock-context.ts index 0aa4a4e5..ec5c664a 100644 --- a/packages/core/src/client/webcomponents/stories/mock-context.ts +++ b/packages/core/src/client/webcomponents/stories/mock-context.ts @@ -63,6 +63,8 @@ function createMockRpc( }, ensureTrusted: async () => trusted === true, requestTrust: async () => trusted === true, + requestTrustWithCode: async () => trusted === true, + requestTrustWithToken: async () => trusted === true, call, callEvent: async () => undefined, callOptional: async () => undefined, diff --git a/packages/core/src/client/webcomponents/utils/iconify.ts b/packages/core/src/client/webcomponents/utils/iconify.ts index a161942d..29bd116d 100644 --- a/packages/core/src/client/webcomponents/utils/iconify.ts +++ b/packages/core/src/client/webcomponents/utils/iconify.ts @@ -14,6 +14,11 @@ export async function getIconifySvg(collection: string, icon: string) { getIconifySvgMap.set(id, svg) return svg }) + .catch((err) => { + // Don't cache failures — drop the entry so a later render can retry. + getIconifySvgMap.delete(id) + throw err + }) getIconifySvgMap.set(id, promise) return promise diff --git a/storybook/stories/Overview.mdx b/storybook/stories/Overview.mdx index 25879c45..dea90a5d 100644 --- a/storybook/stories/Overview.mdx +++ b/storybook/stories/Overview.mdx @@ -1,6 +1,12 @@ {/* AUTO-GENERATED by scripts/gen-overview.mts — run `pnpm run docs:overview` to refresh. */} import { Canvas, Meta } from '@storybook/addon-docs/blocks' +import * as SCommandsPalette from '../../packages/core/src/client/webcomponents/components/command-palette/CommandPalette.stories' +import * as SCommandsPaletteItem from '../../packages/core/src/client/webcomponents/components/command-palette/CommandPaletteItem.stories' +import * as SCommandsKeybindingBadge from '../../packages/core/src/client/webcomponents/components/command-palette/KeybindingBadge.stories' +import * as SDisplayConfirm from '../../packages/core/src/client/webcomponents/components/display/Confirm.stories' +import * as SDisplayHashBadge from '../../packages/core/src/client/webcomponents/components/display/HashBadge.stories' +import * as SMessagesToastOverlay from '../../packages/core/src/client/webcomponents/components/display/ToastOverlay.stories' import * as SDockShellFloatBar from '../../packages/core/src/client/webcomponents/components/dock/Dock.stories' import * as SDockShellEdgePanel from '../../packages/core/src/client/webcomponents/components/dock/DockEdge.stories' import * as SDockShellEmbedded from '../../packages/core/src/client/webcomponents/components/dock/DockEmbedded.stories' @@ -12,7 +18,14 @@ import * as SDockGroupSidebar from '../../packages/core/src/client/webcomponents import * as SDockIcon from '../../packages/core/src/client/webcomponents/components/dock/DockIcon.stories' import * as SDockBarOverflowButton from '../../packages/core/src/client/webcomponents/components/dock/DockOverflowButton.stories' import * as SDockShellFloatPanel from '../../packages/core/src/client/webcomponents/components/dock/DockPanel.stories' +import * as SDockShellStandalone from '../../packages/core/src/client/webcomponents/components/dock/DockStandalone.stories' import * as SDockFloatingPopover from '../../packages/core/src/client/webcomponents/components/floating/FloatingPopover.stories' +import * as SBrandLogos from '../../packages/core/src/client/webcomponents/components/icons/Brand.stories' +import * as SMessagesMessageItem from '../../packages/core/src/client/webcomponents/components/message/MessageItem.stories' +import * as SViewsBuiltinAuthNotice from '../../packages/core/src/client/webcomponents/components/views-builtin/ViewBuiltinClientAuthNotice.stories' +import * as SViewsBuiltinSettings from '../../packages/core/src/client/webcomponents/components/views-builtin/ViewBuiltinSettings.stories' +import * as SViewsLauncher from '../../packages/core/src/client/webcomponents/components/views/ViewLauncher.stories' +import * as SJsonRenderGallery from '../../packages/core/src/client/webcomponents/json-render/JsonRender.stories' @@ -22,6 +35,30 @@ A gallery of the dock and floating-panel components that make up the embedded and standalone DevTools client. Each heading links to that component's own docs page; use the theme toggle in the toolbar to preview light and dark. +### [Commands/Palette](/?path=/docs/commands-palette--docs) + + + +### [Commands/PaletteItem](/?path=/docs/commands-paletteitem--docs) + + + +### [Commands/KeybindingBadge](/?path=/docs/commands-keybindingbadge--docs) + + + +### [Display/Confirm](/?path=/docs/display-confirm--docs) + + + +### [Display/HashBadge](/?path=/docs/display-hashbadge--docs) + + + +### [Messages/ToastOverlay](/?path=/docs/messages-toastoverlay--docs) + + + ### [Dock/Shell/Float Bar](/?path=/docs/dock-shell-float-bar--docs) @@ -66,6 +103,34 @@ page; use the theme toggle in the toolbar to preview light and dark. +### [Dock/Shell/Standalone](/?path=/docs/dock-shell-standalone--docs) + + + ### [Dock/Floating/Popover](/?path=/docs/dock-floating-popover--docs) + +### [Brand/Logos](/?path=/docs/brand-logos--docs) + + + +### [Messages/MessageItem](/?path=/docs/messages-messageitem--docs) + + + +### [Views/Builtin/AuthNotice](/?path=/docs/views-builtin-authnotice--docs) + + + +### [Views/Builtin/Settings](/?path=/docs/views-builtin-settings--docs) + + + +### [Views/Launcher](/?path=/docs/views-launcher--docs) + + + +### [JsonRender/Gallery](/?path=/docs/jsonrender-gallery--docs) + +