Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ packages/kit/skills
*.tsbuildinfo
**/.vitepress/cache
.turbo
storybook-static
.context
.ghfs

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"docs": "pnpm -C docs run docs",
"docs:build": "pnpm build && pnpm -C docs run docs:build",
"docs:serve": "pnpm -C docs run docs:serve",
"storybook": "pnpm -C storybook run dev",
"play": "pnpm -C packages/core run play",
"play:debug": "pnpm -C packages/core run play:debug",
"play:standalone": "pnpm -C packages/core run play:standalone",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { defineComponent, h, onMounted } from 'vue'
import { groupedEntries } from '../../stories/fixtures'
import { mountWithContext } from '../../stories/story-helpers'
import CommandPalette from './CommandPalette.vue'

const meta = {
title: 'Commands/Palette',
component: CommandPalette,
tags: ['autodocs'],
parameters: {
layout: 'fullscreen',
// Fixed, full-screen overlay — render in an iframe for the docs canvas.
docs: {
story: { inline: false, height: '480px' },
description: {
component: 'The ⌘K command palette. Lists client + server commands (and dock navigation), supports fuzzy search and drill-down into grouped commands.',
},
},
},
} satisfies Meta

export default meta
type Story = StoryObj

/**
* The palette opened over the built-in and dock-navigation commands.
*
* `paletteOpen` is flipped in `onMounted` (not before mount): the palette's
* open transition keys off the `show` change, so a value that's already `true`
* at mount would leave it stuck at `opacity-0`.
*/
export const Open: Story = {
render: () => ({
setup: () => mountWithContext(
{ entries: groupedEntries },
ctx => h(defineComponent({
setup() {
onMounted(() => {
ctx.commands.paletteOpen = true
})
return () => h(CommandPalette, { context: ctx })
},
})),
),
}),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import type { DevToolsCommandEntry } from '@vitejs/devtools-kit'
import CommandPaletteItem from './CommandPaletteItem.vue'

const entry: DevToolsCommandEntry = {
id: 'devtools:open-settings',
source: 'client',
title: 'Open Settings',
icon: 'ph:gear-duotone',
} as DevToolsCommandEntry

const meta = {
title: 'Commands/PaletteItem',
component: CommandPaletteItem,
tags: ['autodocs'],
decorators: [() => ({ template: '<div class="max-w-120 p4 bg-base color-base font-sans"><story /></div>' })],
args: {
entry,
showParentTitle: false,
selected: false,
loading: false,
keybindings: [],
},
} satisfies Meta<typeof CommandPaletteItem>

export default meta
type Story = StoryObj<typeof meta>

/** A resting command row. */
export const Default: Story = {}

/** The highlighted (keyboard-focused) row. */
export const Selected: Story = { args: { selected: true } }

/** With a keybinding badge on the right. */
export const WithKeybinding: Story = {
args: { keybindings: [{ key: 'Mod+,' }] },
}

/** A child command showing its parent group as a prefix. */
export const WithParentTitle: Story = {
args: { parentTitle: 'Docks', showParentTitle: true },
}

/** Mid-execution: the row shows a spinner. */
export const Loading: Story = { args: { loading: true, selected: true } }
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { h } from 'vue'
import KeybindingBadge from './KeybindingBadge.vue'

const meta = {
title: 'Commands/KeybindingBadge',
component: KeybindingBadge,
tags: ['autodocs'],
decorators: [() => ({ template: '<div class="p6 font-sans"><story /></div>' })],
args: { keyString: 'Mod+K' },
} satisfies Meta<typeof KeybindingBadge>

export default meta
type Story = StoryObj<typeof meta>

/** A single chord, formatted for the current platform (⌘ on macOS, Ctrl elsewhere). */
export const Default: Story = {}

/** A shortcut with a modifier and shift. */
export const WithShift: Story = { args: { keyString: 'Mod+Shift+P' } }

/** A plain key. */
export const SingleKey: Story = { args: { keyString: 'Escape' } }

/** Several badges together, as the palette lists them. */
export const Gallery: Story = {
render: () => ({
setup: () => () => h('div', { class: 'flex flex-col gap-2 items-start p6 font-sans' }, [
h(KeybindingBadge, { keyString: 'Mod+K' }),
h(KeybindingBadge, { keyString: 'Mod+Shift+P' }),
h(KeybindingBadge, { keyString: 'Escape' }),
h(KeybindingBadge, { keyString: 'Alt+ArrowUp' }),
]),
}),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { h, onMounted } from 'vue'
import { useConfirm } from '../../state/confirm'
import Confirm from './Confirm.vue'

const meta = {
title: 'Display/Confirm',
component: Confirm,
tags: ['autodocs'],
parameters: {
layout: 'fullscreen',
// Fixed, full-screen modal — render in an iframe for the docs canvas.
docs: {
story: { inline: false, height: '360px' },
description: {
component: 'The confirmation dialog, driven by the `useConfirm()` template-promise. These stories trigger it on mount.',
},
},
},
} satisfies Meta<typeof Confirm>

export default meta
type Story = StoryObj<typeof meta>

function confirmStory(options: { title?: string, message: string, confirmText?: string, cancelText?: string }): Story {
return {
render: () => ({
setup() {
const confirm = useConfirm()
onMounted(() => {
confirm(options)
})
return () => h(Confirm)
},
}),
}
}

/** A titled destructive confirmation. */
export const Default: Story = confirmStory({
title: 'Remove workspace?',
message: 'This deletes the worktree and branch. This cannot be undone.',
confirmText: 'Remove',
cancelText: 'Cancel',
})

/** A message-only prompt (no title). */
export const MessageOnly: Story = confirmStory({
message: 'Reload the page to apply the new settings?',
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { h } from 'vue'
import HashBadge from './HashBadge.vue'

const meta = {
title: 'Display/HashBadge',
component: HashBadge,
tags: ['autodocs'],
decorators: [() => ({ template: '<div class="p6 font-sans"><story /></div>' })],
args: { label: 'a11y' },
parameters: {
docs: {
description: {
component: 'A category/label chip whose colour is derived deterministically from the label text.',
},
},
},
} satisfies Meta<typeof HashBadge>

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {}

/** The colour is stable per string — the same label always gets the same hue. */
export const Palette: Story = {
render: () => ({
setup: () => () => h('div', { class: 'flex flex-wrap gap-1.5 max-w-100 p6 font-sans' }, ['a11y', 'lint', 'runtime', 'test', 'network', 'build', 'hmr', 'plugin', 'vite', 'rolldown']
.map(label => h(HashBadge, { key: label, label }))),
}),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { h, onMounted } from 'vue'
import { addToast } from '../../state/toasts'
import { message } from '../../stories/fixtures'
import ToastOverlay from './ToastOverlay.vue'

const meta = {
title: 'Messages/ToastOverlay',
component: ToastOverlay,
tags: ['autodocs'],
parameters: {
layout: 'fullscreen',
// Fixed bottom-right overlay — render in an iframe for the docs canvas.
docs: {
story: { inline: false, height: '360px' },
description: {
component: 'The toast stack shown bottom-right for `notify` messages. These stories push toasts on mount (with a long auto-dismiss so they stay visible).',
},
},
},
} satisfies Meta<typeof ToastOverlay>

export default meta
type Story = StoryObj<typeof meta>

/** A few stacked toasts across levels. */
export const Stack: Story = {
render: () => ({
setup() {
onMounted(() => {
addToast(message({ level: 'error', message: 'Build failed', description: '2 errors in 1 file', notify: true, autoDismiss: 10 ** 7 }))
addToast(message({ level: 'warn', message: 'Slow HMR update (820ms)', notify: true, autoDismiss: 10 ** 7 }))
addToast(message({ level: 'success', message: 'Server ready on :5173', notify: true, autoDismiss: 10 ** 7 }))
})
return () => h(ToastOverlay)
},
}),
}
106 changes: 106 additions & 0 deletions packages/core/src/client/webcomponents/components/dock/Dock.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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',
// The shell is `position: fixed` to the viewport, so it escapes the small
// inline docs preview block — render it in an iframe of a fixed height.
docs: {
story: { inline: false, height: '520px' },
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)],
),
}),
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
}
Expand Down
Loading
Loading