chore: storybook stories and peripheral updates

Amp-Thread-ID: https://ampcode.com/threads/T-019d3012-26c4-70ff-984c-913b12217454
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Koshi
2026-03-27 17:33:44 +01:00
parent b40fb33e7a
commit 8d95cd8bbc
10 changed files with 488 additions and 29 deletions

View File

@@ -15,7 +15,7 @@ type ValidationState = InstallValidation['basePath']
type IndexedUpdate = InstallValidation & Record<string, ValidationState>
/** State of a maintenance task, managed by the maintenance task store. */
export class MaintenanceTaskRunner {
class MaintenanceTaskRunner {
constructor(readonly task: MaintenanceTask) {}
private _state?: MaintenanceTaskState

View File

@@ -42,7 +42,10 @@ const config: KnipConfig = {
'@primeuix/utils',
'@primevue/icons',
// Used by lucideStrokePlugin.js (CSS @plugin)
'@iconify/utils'
'@iconify/utils',
// Used in design-system CSS — knip can't trace CSS @plugin usage
'tailwindcss-primeui',
'tw-animate-css'
],
ignore: [
// Auto generated API types

View File

@@ -0,0 +1,136 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import Button from '@/components/ui/button/Button.vue'
import Dialogue from './Dialogue.vue'
const meta = {
title: 'UI/Dialog',
component: Dialogue,
tags: ['autodocs'],
parameters: {
layout: 'centered'
}
} satisfies Meta<typeof Dialogue>
export default meta
type Story = StoryObj<typeof meta>
export const WithTitle: Story = {
render: (args) => ({
components: { Dialogue, Button },
setup: () => ({ args }),
template: `
<Dialogue v-bind="args">
<template #button>
<Button>Open dialog</Button>
</template>
<template #default="{ close }">
<div class="flex flex-col gap-6 p-4">
<p class="text-sm text-muted-foreground">
A more descriptive lorem ipsum text...
</p>
<div class="flex items-center justify-end gap-4">
<Button variant="muted-textonly" size="sm" @click="close">
Cancel
</Button>
<Button variant="secondary" size="lg" @click="close">
Ok
</Button>
</div>
</div>
</template>
</Dialogue>
`
}),
args: {
title: 'Modal Title'
}
}
export const WithoutTitle: Story = {
render: () => ({
components: { Dialogue, Button },
template: `
<Dialogue>
<template #button>
<Button>Open dialog</Button>
</template>
<template #default="{ close }">
<div class="flex flex-col gap-4 p-4">
<p class="text-sm text-muted-foreground">
This dialog has no title header.
</p>
<div class="flex justify-end">
<Button variant="secondary" size="lg" @click="close">
Got it
</Button>
</div>
</div>
</template>
</Dialogue>
`
})
}
export const Confirmation: Story = {
render: () => ({
components: { Dialogue, Button },
template: `
<Dialogue title="Delete this item?">
<template #button>
<Button variant="destructive">Delete</Button>
</template>
<template #default="{ close }">
<div class="flex flex-col gap-6 p-4">
<p class="text-sm text-muted-foreground">
This action cannot be undone. The item will be permanently removed.
</p>
<div class="flex items-center justify-end gap-4">
<Button variant="muted-textonly" size="sm" @click="close">
Cancel
</Button>
<Button variant="destructive" size="lg" @click="close">
Delete
</Button>
</div>
</div>
</template>
</Dialogue>
`
})
}
export const WithLink: Story = {
render: () => ({
components: { Dialogue, Button },
template: `
<Dialogue title="Modal Title">
<template #button>
<Button>Open dialog</Button>
</template>
<template #default="{ close }">
<div class="flex flex-col gap-6 p-4">
<p class="text-sm text-muted-foreground">
A more descriptive lorem ipsum text...
</p>
<div class="flex items-center justify-between">
<button class="flex items-center gap-2 text-sm text-muted-foreground hover:text-base-foreground">
<i class="icon-[lucide--external-link] size-4" />
See what's new
</button>
<div class="flex items-center gap-4">
<Button variant="muted-textonly" size="sm" @click="close">
Cancel
</Button>
<Button variant="secondary" size="lg" @click="close">
Ok
</Button>
</div>
</div>
</div>
</template>
</Dialogue>
`
})
}

View File

@@ -0,0 +1,214 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import Button from '@/components/ui/button/Button.vue'
import Popover from './Popover.vue'
const meta = {
title: 'UI/Popover',
component: Popover,
tags: ['autodocs'],
parameters: {
layout: 'centered',
backgrounds: {
default: 'dark',
values: [
{ name: 'dark', value: '#1a1a1b' },
{ name: 'light', value: '#ffffff' },
{ name: 'sidebar', value: '#232326' }
]
}
}
} satisfies Meta<typeof Popover>
export default meta
type Story = StoryObj<typeof meta>
/** Default: menu-style popover with action entries. */
export const Default: Story = {
render: () => ({
components: { Popover },
template: `
<Popover
:entries="[
{ label: 'Rename', icon: 'icon-[lucide--pencil]', command: () => {} },
{ label: 'Duplicate', icon: 'icon-[lucide--copy]', command: () => {} },
{ separator: true },
{ label: 'Delete', icon: 'icon-[lucide--trash-2]', command: () => {} }
]"
/>
`
})
}
/** Custom trigger button. */
export const CustomTrigger: Story = {
render: () => ({
components: { Popover, Button },
template: `
<Popover
:entries="[
{ label: 'Option A', command: () => {} },
{ label: 'Option B', command: () => {} }
]"
>
<template #button>
<Button variant="outline">Click me</Button>
</template>
</Popover>
`
})
}
/** Action prompt: small inline confirmation bubble.
* Use this pattern for contextual Yes/No prompts like
* "Group these?", "Align to bottom?", etc. */
export const ActionPrompt: Story = {
render: () => ({
components: { Popover, Button },
template: `
<Popover>
<template #button>
<Button variant="outline" size="sm">
<i class="icon-[lucide--layout-grid] mr-1 size-3.5" />
Group
</Button>
</template>
<template #default="{ close }">
<div class="flex flex-col gap-2 p-1">
<p class="text-sm text-muted-foreground">Group into a row?</p>
<div class="flex gap-2">
<Button
size="sm"
variant="primary"
class="flex-1"
@click="close()"
>
Yes
</Button>
<Button
size="sm"
variant="ghost"
class="flex-1"
@click="close()"
>
No
</Button>
</div>
</div>
</template>
</Popover>
`
})
}
/** Alignment prompt: contextual bubble for zone actions. */
export const AlignPrompt: Story = {
render: () => ({
components: { Popover, Button },
template: `
<Popover>
<template #button>
<Button variant="ghost" size="sm">
<i class="icon-[lucide--align-vertical-justify-end] size-4" />
</Button>
</template>
<template #default="{ close }">
<div class="flex flex-col gap-1.5 p-1">
<button
class="flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-secondary-background"
@click="close()"
>
<i class="icon-[lucide--arrow-down-to-line] size-4" />
Align to bottom
</button>
<button
class="flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-secondary-background"
@click="close()"
>
<i class="icon-[lucide--columns-2] size-4" />
Group into row
</button>
</div>
</template>
</Popover>
`
})
}
/** On light background — verify popover visibility. */
export const OnLightBackground: Story = {
parameters: {
backgrounds: { default: 'light' }
},
render: () => ({
components: { Popover, Button },
template: `
<Popover>
<template #button>
<Button>Open popover</Button>
</template>
<template #default="{ close }">
<div class="p-2">
<p class="text-sm">Popover on light background</p>
<Button size="sm" class="mt-2" @click="close()">Close</Button>
</div>
</template>
</Popover>
`
})
}
/** On sidebar background — verify contrast against dark sidebar. */
export const OnSidebarBackground: Story = {
parameters: {
backgrounds: { default: 'sidebar' }
},
render: () => ({
components: { Popover, Button },
template: `
<Popover>
<template #button>
<Button>Open popover</Button>
</template>
<template #default="{ close }">
<div class="p-2">
<p class="text-sm">Popover on sidebar background</p>
<Button size="sm" class="mt-2" @click="close()">Close</Button>
</div>
</template>
</Popover>
`
})
}
/** No arrow variant. */
export const NoArrow: Story = {
render: () => ({
components: { Popover },
template: `
<Popover
:show-arrow="false"
:entries="[
{ label: 'Settings', icon: 'icon-[lucide--settings]', command: () => {} },
{ label: 'Help', icon: 'icon-[lucide--circle-help]', command: () => {} }
]"
/>
`
})
}
/** Disabled entry. */
export const WithDisabled: Story = {
render: () => ({
components: { Popover },
template: `
<Popover
:entries="[
{ label: 'Available', command: () => {} },
{ label: 'Coming soon', disabled: true }
]"
/>
`
})
}

View File

@@ -0,0 +1,31 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import TypeformPopoverButton from './TypeformPopoverButton.vue'
const meta = {
title: 'UI/TypeformPopoverButton',
component: TypeformPopoverButton,
tags: ['autodocs'],
parameters: {
layout: 'centered'
}
} satisfies Meta<typeof TypeformPopoverButton>
export default meta
type Story = StoryObj<typeof meta>
/** Default: help button that opens an embedded Typeform survey. */
export const Default: Story = {
args: {
dataTfWidget: 'example123',
active: true
}
}
/** Inactive: popover content is hidden. */
export const Inactive: Story = {
args: {
dataTfWidget: 'example123',
active: false
}
}

View File

@@ -15,6 +15,10 @@ const meta = {
side: {
control: 'select',
options: ['top', 'bottom', 'left', 'right']
},
size: {
control: 'select',
options: ['sm', 'lg']
}
}
} satisfies Meta<typeof Tooltip>
@@ -34,10 +38,74 @@ export const Default: Story = {
}),
args: {
text: 'This is a tooltip',
side: 'top'
side: 'top',
size: 'sm'
}
}
export const Small: Story = {
render: () => ({
components: { Tooltip, Button },
template: `
<div class="flex gap-12 p-20">
<Tooltip text="Tool tip left aligned" side="top" size="sm">
<Button>Top</Button>
</Tooltip>
<Tooltip text="Tool tip center aligned" side="bottom" size="sm">
<Button>Bottom</Button>
</Tooltip>
<Tooltip text="Tool tip right aligned" side="left" size="sm">
<Button>Left</Button>
</Tooltip>
<Tooltip text="Tool tip pointing left" side="right" size="sm">
<Button>Right</Button>
</Tooltip>
</div>
`
})
}
export const Large: Story = {
render: () => ({
components: { Tooltip, Button },
template: `
<div class="flex gap-12 p-20">
<Tooltip text="Lorem ipsum dolor sit amet, consectetur dolor si adipiscing elit. Proin maximus nisl nec posuere mattis." side="top" size="lg">
<Button>Top</Button>
</Tooltip>
<Tooltip text="Lorem ipsum dolor sit amet, consectetur dolor si adipiscing elit. Proin maximus nisl nec posuere mattis." side="bottom" size="lg">
<Button>Bottom</Button>
</Tooltip>
<Tooltip text="Lorem ipsum dolor sit amet, consectetur dolor si adipiscing elit. Proin maximus nisl nec posuere mattis." side="left" size="lg">
<Button>Left</Button>
</Tooltip>
<Tooltip text="Lorem ipsum dolor sit amet, consectetur dolor si adipiscing elit. Proin maximus nisl nec posuere mattis." side="right" size="lg">
<Button>Right</Button>
</Tooltip>
</div>
`
})
}
export const WithKeybind: Story = {
render: () => ({
components: { Tooltip, Button },
template: `
<div class="flex gap-12 p-20">
<Tooltip text="Select all" keybind="Ctrl+A" side="top" size="sm">
<Button>With keybind</Button>
</Tooltip>
<Tooltip text="Save" keybind="Ctrl+S" side="bottom" size="sm">
<Button>Save</Button>
</Tooltip>
<Tooltip text="Undo" keybind="Ctrl+Z" side="right" size="sm">
<Button>Undo</Button>
</Tooltip>
</div>
`
})
}
export const AllSides: Story = {
render: () => ({
components: { Tooltip, Button },
@@ -62,13 +130,21 @@ export const AllSides: Story = {
})
}
export const LongText: Story = {
export const WithOffset: Story = {
render: () => ({
components: { Tooltip, Button },
template: `
<Tooltip text="The random seed used for creating the noise. This controls the reproducibility of generated images." side="top">
<Button>Hover for details</Button>
</Tooltip>
<div class="flex gap-12 p-20">
<Tooltip text="20px offset" side="left" :side-offset="20" size="sm">
<Button>Left 20px</Button>
</Tooltip>
<Tooltip text="20px offset" side="top" :side-offset="20" size="sm">
<Button>Top 20px</Button>
</Tooltip>
<Tooltip text="Default offset" side="left" size="sm">
<Button>Left default</Button>
</Tooltip>
</div>
`
})
}

View File

@@ -2,6 +2,7 @@ import { app } from '../../scripts/app'
import { ComfyApp } from '../../scripts/app'
import { $el, ComfyDialog } from '../../scripts/ui'
/** @knipIgnoreUnusedButUsedByCustomNodes - Used by custom nodes via ComfyApp.clipspace */
export class ClipspaceDialog extends ComfyDialog {
static items: Array<
HTMLButtonElement & {

View File

@@ -22,6 +22,21 @@ export interface WidgetOverride {
displayType?: WidgetDisplayType
}
/** An item within an input group. */
export interface InputGroupItem {
key: string
pairId?: string
}
/** A named group of inputs that renders as a collapsible accordion. */
export interface InputGroup {
id: string
name: string | null
items: InputGroupItem[]
/** Optional color name from LGraphCanvas.node_colors (e.g. 'red', 'blue'). */
color?: string | null
}
/** Scope determines which widgets a preset targets. */
type PresetScope = 'app' | 'graph'
@@ -63,7 +78,6 @@ export interface LinearData {
>
runControlsZoneIdPerTemplate?: Record<string, string>
zoneItemOrderPerTemplate?: Record<string, Record<string, string[]>>
zoneAlignPerTemplate?: Record<string, Record<string, 'top' | 'bottom'>>
presetStripZoneIdPerTemplate?: Record<string, string>
/** Per-widget overrides (min/max constraints, display type). Keyed by `nodeId:widgetName`. */
widgetOverrides?: Record<string, WidgetOverride>
@@ -73,6 +87,10 @@ export interface LinearData {
presetDisplayMode?: PresetDisplayMode
/** Whether the preset strip is visible. Defaults to true. */
presetsEnabled?: boolean
/** Collapsible input groups per layout template. */
inputGroupsPerTemplate?: Record<string, InputGroup[]>
/** Per-input color names. Keyed by `nodeId:widgetName`. */
inputColors?: Record<string, string>
}
export interface PendingWarnings {

View File

@@ -110,7 +110,7 @@ interface Load3DNode extends LGraphNode {
const viewerInstances = new Map<NodeId, ReturnType<UseLoad3dViewerFn>>()
export class Load3dService {
class Load3dService {
private static instance: Load3dService
private constructor() {}

View File

@@ -3,7 +3,6 @@ import { resolvePromotedWidgetSource } from '@/core/graph/subgraph/resolvePromot
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { useDialogService } from '@/services/dialogService'
@@ -92,22 +91,3 @@ export async function promptWidgetLabel(
placeholder: widget.name
})
}
export async function promptRenameWidget(
widget: IBaseWidget,
node: LGraphNode,
t: (key: string) => string,
parents?: SubgraphNode[]
): Promise<string | null> {
const rawLabel = await promptWidgetLabel(widget, t)
if (rawLabel === null) return null
const normalizedLabel = rawLabel.trim()
if (!normalizedLabel) return null
if (!renameWidget(widget, node, normalizedLabel, parents)) return null
widget.callback?.(widget.value)
useCanvasStore().canvas?.setDirty(true)
return normalizedLabel
}