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
2 changes: 1 addition & 1 deletion packages/core/src/client/webcomponents/.generated/css.ts

Large diffs are not rendered by default.

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,79 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { h } from 'vue'
import Button from './Button.vue'

const variants = ['primary', 'soft', 'secondary', 'ghost', 'danger'] as const
const sizes = ['sm', 'md', 'lg'] as const

function row(children: any, label?: string) {
return h('div', { class: 'flex items-center gap-3 flex-wrap' }, [
label ? h('div', { class: 'w-20 text-xs op-mute font-mono' }, label) : null,
children,
])
}

const meta = {
title: 'Display/Button',
component: Button,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component: 'The unified DevTools button, shared by the confirm modal, the auth OTP form, the launcher view and json-render. Variants (`primary`, `soft`, `secondary`, `ghost`, `danger`), sizes (`sm`/`md`/`lg`), plus `block`, `loading`, `disabled` and an optional leading `#icon` slot.',
},
},
},
} satisfies Meta<typeof Button>

export default meta
type Story = StoryObj<typeof meta>

/** Every variant at the default size. */
export const Variants: Story = {
render: () => ({
setup: () => () => h('div', { class: 'flex flex-col gap-3 p8 bg-base color-base font-sans' }, variants.map(variant => row(
h(Button, { variant }, { default: () => variant[0]!.toUpperCase() + variant.slice(1) }),
variant,
))),
}),
}

/** The three sizes (shown with the primary variant). */
export const Sizes: Story = {
render: () => ({
setup: () => () => h('div', { class: 'flex items-center gap-3 p8 bg-base color-base font-sans' }, sizes.map(size => h(Button, { variant: 'primary', size }, { default: () => size }))),
}),
}

/** With a leading icon (via the `#icon` slot). */
export const WithIcon: Story = {
render: () => ({
setup: () => () => h('div', { class: 'flex items-center gap-3 p8 bg-base color-base font-sans' }, [
h(Button, { variant: 'primary' }, {
icon: () => h('div', { class: 'i-ph-shield-check-duotone w-4.5 h-4.5' }),
default: () => 'Authorize',
}),
h(Button, { variant: 'danger' }, {
icon: () => h('div', { class: 'i-ph-trash-duotone w-4.5 h-4.5' }),
default: () => 'Delete',
}),
]),
}),
}

/** Loading (spinner + disabled) and disabled states. */
export const States: Story = {
render: () => ({
setup: () => () => h('div', { class: 'flex items-center gap-3 p8 bg-base color-base font-sans' }, [
h(Button, { variant: 'primary', loading: true }, { default: () => 'Authorizing' }),
h(Button, { variant: 'primary', disabled: true }, { default: () => 'Disabled' }),
]),
}),
}

/** Full-width block button, as used in the auth form. */
export const Block: Story = {
render: () => ({
setup: () => () => h('div', { class: 'max-w-80 p8 bg-base color-base font-sans' }, h(Button, { variant: 'primary', size: 'lg', block: true }, { default: () => 'Authorize' })),
}),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<script setup lang="ts">
import { useTemplateRef } from 'vue'

type Variant = 'primary' | 'secondary' | 'ghost' | 'danger' | 'soft'
type Size = 'sm' | 'md' | 'lg'

withDefaults(defineProps<{
/** Visual style. */
variant?: Variant
size?: Size
/** Stretch to the full width of the container. */
block?: boolean
/** Show a spinner and disable interaction. */
loading?: boolean
disabled?: boolean
type?: 'button' | 'submit' | 'reset'
}>(), {
variant: 'secondary',
size: 'md',
block: false,
loading: false,
disabled: false,
type: 'button',
})

// The single source of truth for how a DevTools button looks — shared by the
// confirm modal, the auth OTP form, the launcher view and json-render.
const variantClass: Record<Variant, string> = {
primary: 'bg-primary-600 text-white shadow-sm hover:bg-primary-700 focus-visible:ring-primary-500/40',
soft: 'bg-primary/10 text-primary-700 dark:text-primary-300 hover:bg-primary/20 focus-visible:ring-primary-500/30',
secondary: 'bg-secondary color-base hover:bg-[#8883] focus-visible:ring-primary-500/30',
ghost: 'color-base op70 hover:op100 hover:bg-active focus-visible:ring-primary-500/30',
danger: 'bg-red-500/12 text-red-600 dark:text-red-400 hover:bg-red-500/20 focus-visible:ring-red-500/30',
}

const sizeClass: Record<Size, string> = {
sm: 'text-xs px3 py1.5 gap-1.5 rounded-md',
md: 'text-sm px4 py2 gap-2 rounded-lg',
lg: 'text-sm px4 py2.5 gap-2 rounded-xl',
}

const iconSizeClass: Record<Size, string> = {
sm: 'w-3.5 h-3.5',
md: 'w-4.5 h-4.5',
lg: 'w-4.5 h-4.5',
}

const button = useTemplateRef<HTMLButtonElement>('button')

defineExpose({
focus: (options?: FocusOptions) => button.value?.focus(options),
})
</script>

<template>
<button
ref="button"
:type="type"
:disabled="disabled || loading"
:aria-busy="loading || undefined"
class="inline-flex items-center justify-center font-medium transition-all outline-none focus-visible:ring-3 disabled:op50 disabled:pointer-events-none"
:class="[variantClass[variant], sizeClass[size], block ? 'w-full' : '']"
>
<div v-if="loading" class="i-ph-spinner-gap-duotone animate-spin flex-none" :class="iconSizeClass[size]" />
<slot v-else name="icon" />
<slot />
</button>
</template>
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
@@ -1,8 +1,9 @@
<script setup lang="ts">
import { useTemplateRef, watchPostEffect } from 'vue'
import { ConfirmPromise } from '../../state/confirm'
import Button from './Button.vue'

const confirmButton = useTemplateRef<HTMLButtonElement>('confirmButton')
const confirmButton = useTemplateRef<InstanceType<typeof Button>>('confirmButton')

watchPostEffect(() => {
confirmButton.value?.focus({ preventScroll: true })
Expand Down Expand Up @@ -42,21 +43,12 @@ function resolveConfirm(resolve: (value: boolean) => void, value: boolean) {
</p>

<div class="flex items-center justify-end gap-2 mt-6">
<button
type="button"
class="px-3 py-1.5 rounded text-xs op60 hover:op100 hover:bg-gray/10 transition-colors"
@click="resolveConfirm(resolve, false)"
>
<Button variant="ghost" size="sm" @click="resolveConfirm(resolve, false)">
{{ options.cancelText ?? 'Cancel' }}
</button>
<button
ref="confirmButton"
type="button"
class="px-3 py-1.5 rounded text-xs transition-colors bg-primary/15 text-primary hover:bg-primary/25"
@click="resolveConfirm(resolve, true)"
>
</Button>
<Button ref="confirmButton" variant="soft" size="sm" @click="resolveConfirm(resolve, true)">
{{ options.confirmText ?? 'OK' }}
</button>
</Button>
</div>
</div>
</div>
Expand Down
Loading
Loading