mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
136
src/components/common/Dialogue.stories.ts
Normal file
136
src/components/common/Dialogue.stories.ts
Normal 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>
|
||||
`
|
||||
})
|
||||
}
|
||||
214
src/components/ui/Popover.stories.ts
Normal file
214
src/components/ui/Popover.stories.ts
Normal 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 }
|
||||
]"
|
||||
/>
|
||||
`
|
||||
})
|
||||
}
|
||||
31
src/components/ui/TypeformPopoverButton.stories.ts
Normal file
31
src/components/ui/TypeformPopoverButton.stories.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 & {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user