mirror of
https://github.com/langgenius/dify.git
synced 2026-04-27 00:00:15 -04:00
docs(web): add Storybook stories for overlay and select primitives (#35334)
This commit is contained in:
179
web/app/components/base/ui/alert-dialog/index.stories.tsx
Normal file
179
web/app/components/base/ui/alert-dialog/index.stories.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogActions,
|
||||
AlertDialogCancelButton,
|
||||
AlertDialogConfirmButton,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '.'
|
||||
import { Button } from '../button'
|
||||
|
||||
const triggerButtonClassName = 'rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-1.5 text-sm text-text-secondary shadow-xs hover:bg-state-base-hover'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/UI/AlertDialog',
|
||||
component: AlertDialog,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Compound alert dialog built on Base UI AlertDialog. Use it for destructive or high-risk confirmations that require an explicit user decision. Compose title, description, and actions via `AlertDialogActions`, `AlertDialogCancelButton`, and `AlertDialogConfirmButton`.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof AlertDialog>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger
|
||||
render={<button type="button" className={triggerButtonClassName} />}
|
||||
>
|
||||
Delete project
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<div className="flex flex-col gap-2 p-6 pb-4">
|
||||
<AlertDialogTitle className="text-lg leading-7 font-semibold text-text-primary">
|
||||
Delete project?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-sm leading-5 text-text-secondary">
|
||||
This action cannot be undone. All workflows, datasets, and API keys will be permanently removed.
|
||||
</AlertDialogDescription>
|
||||
</div>
|
||||
<AlertDialogActions>
|
||||
<AlertDialogCancelButton variant="secondary">Cancel</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton>Delete</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
),
|
||||
}
|
||||
|
||||
export const NonDestructive: Story = {
|
||||
render: () => (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger
|
||||
render={<button type="button" className={triggerButtonClassName} />}
|
||||
>
|
||||
Publish changes
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<div className="flex flex-col gap-2 p-6 pb-4">
|
||||
<AlertDialogTitle className="text-lg leading-7 font-semibold text-text-primary">
|
||||
Publish the latest draft?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-sm leading-5 text-text-secondary">
|
||||
Collaborators will immediately see the new workflow. You can still revert from version history.
|
||||
</AlertDialogDescription>
|
||||
</div>
|
||||
<AlertDialogActions>
|
||||
<AlertDialogCancelButton variant="secondary">Not now</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton tone="default">Publish</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
),
|
||||
}
|
||||
|
||||
const ControlledDemo = () => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Button variant="secondary" onClick={() => setOpen(true)}>
|
||||
Open controlled dialog
|
||||
</Button>
|
||||
<span className="text-xs text-text-tertiary">
|
||||
Confirmed
|
||||
{count}
|
||||
{' '}
|
||||
times
|
||||
</span>
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogContent>
|
||||
<div className="flex flex-col gap-2 p-6 pb-4">
|
||||
<AlertDialogTitle className="text-lg leading-7 font-semibold text-text-primary">
|
||||
Revoke API token?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-sm leading-5 text-text-secondary">
|
||||
Any integration using this token will immediately stop working. You can issue a new token afterwards.
|
||||
</AlertDialogDescription>
|
||||
</div>
|
||||
<AlertDialogActions>
|
||||
<AlertDialogCancelButton variant="secondary">Keep token</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton
|
||||
onClick={() => {
|
||||
setCount(prev => prev + 1)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
Revoke
|
||||
</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Controlled: Story = {
|
||||
render: () => <ControlledDemo />,
|
||||
}
|
||||
|
||||
const LoadingConfirmDemo = () => {
|
||||
const [pending, setPending] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const handleConfirm = () => {
|
||||
setPending(true)
|
||||
window.setTimeout(() => {
|
||||
setPending(false)
|
||||
setOpen(false)
|
||||
}, 1200)
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogTrigger
|
||||
render={<button type="button" className={triggerButtonClassName} />}
|
||||
>
|
||||
Archive workspace
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<div className="flex flex-col gap-2 p-6 pb-4">
|
||||
<AlertDialogTitle className="text-lg leading-7 font-semibold text-text-primary">
|
||||
Archive this workspace?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-sm leading-5 text-text-secondary">
|
||||
Members will lose access until the workspace is restored. This may take a moment to finalize.
|
||||
</AlertDialogDescription>
|
||||
</div>
|
||||
<AlertDialogActions>
|
||||
<AlertDialogCancelButton variant="secondary" disabled={pending}>
|
||||
Cancel
|
||||
</AlertDialogCancelButton>
|
||||
<AlertDialogConfirmButton
|
||||
tone="default"
|
||||
loading={pending}
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
{pending ? 'Archiving…' : 'Archive'}
|
||||
</AlertDialogConfirmButton>
|
||||
</AlertDialogActions>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export const LoadingConfirm: Story = {
|
||||
render: () => <LoadingConfirmDemo />,
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Avatar, AvatarFallback, AvatarRoot } from '.'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Data Display/Avatar',
|
||||
title: 'Base/UI/Avatar',
|
||||
component: Avatar,
|
||||
parameters: {
|
||||
docs: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { Button } from '.'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/General/Button',
|
||||
title: 'Base/UI/Button',
|
||||
component: Button,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
|
||||
@@ -29,7 +29,7 @@ const TriggerArea = ({ label = 'Right-click inside this area' }: { label?: strin
|
||||
)
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Navigation/ContextMenu',
|
||||
title: 'Base/UI/ContextMenu',
|
||||
component: ContextMenu,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
|
||||
176
web/app/components/base/ui/dialog/index.stories.tsx
Normal file
176
web/app/components/base/ui/dialog/index.stories.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogCloseButton,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '.'
|
||||
import { Button } from '../button'
|
||||
|
||||
const triggerButtonClassName = 'rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-1.5 text-sm text-text-secondary shadow-xs hover:bg-state-base-hover'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/UI/Dialog',
|
||||
component: Dialog,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Compound modal dialog built on Base UI Dialog. Use it for focused flows that interrupt the user, such as editing settings, confirming non-destructive actions, or collecting short-form input. Compose `DialogTitle`, `DialogDescription`, and optional `DialogCloseButton` inside `DialogContent`.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof Dialog>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<Dialog>
|
||||
<DialogTrigger
|
||||
render={<button type="button" className={triggerButtonClassName} />}
|
||||
>
|
||||
Open dialog
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogCloseButton />
|
||||
<div className="flex flex-col gap-2 pr-8">
|
||||
<DialogTitle className="text-lg leading-7 font-semibold text-text-primary">
|
||||
Invite collaborators
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-sm leading-5 text-text-secondary">
|
||||
Add teammates by email to share this workspace. They will receive an invitation link.
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<div className="mt-5 flex flex-col gap-2">
|
||||
<label className="text-xs font-medium text-text-tertiary" htmlFor="invite-email">
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
id="invite-email"
|
||||
type="email"
|
||||
placeholder="teammate@example.com"
|
||||
className="h-9 w-full rounded-lg border-[0.5px] border-components-input-border-active bg-components-input-bg-normal px-3 text-sm text-components-input-text-filled outline-hidden placeholder:text-components-input-text-placeholder focus:border-components-input-border-hover"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6 flex items-center justify-end">
|
||||
<Button variant="primary">Send invite</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
),
|
||||
}
|
||||
|
||||
export const WithoutCloseButton: Story = {
|
||||
render: () => (
|
||||
<Dialog>
|
||||
<DialogTrigger
|
||||
render={<button type="button" className={triggerButtonClassName} />}
|
||||
>
|
||||
Start onboarding
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<div className="flex flex-col gap-2">
|
||||
<DialogTitle className="text-lg leading-7 font-semibold text-text-primary">
|
||||
Welcome to Dify
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-sm leading-5 text-text-secondary">
|
||||
Let's get your workspace ready. This takes about a minute and sets up your default models, datasets, and API keys.
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<div className="mt-6 flex items-center justify-end gap-2">
|
||||
<Button variant="secondary">Skip for now</Button>
|
||||
<Button variant="primary">Start setup</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
),
|
||||
}
|
||||
|
||||
const ControlledDemo = () => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Button variant="secondary" onClick={() => setOpen(true)}>
|
||||
Open controlled dialog
|
||||
</Button>
|
||||
<span className="text-xs text-text-tertiary">
|
||||
State:
|
||||
{' '}
|
||||
{open ? 'open' : 'closed'}
|
||||
</span>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogCloseButton />
|
||||
<div className="flex flex-col gap-2 pr-8">
|
||||
<DialogTitle className="text-lg leading-7 font-semibold text-text-primary">
|
||||
Rename workspace
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-sm leading-5 text-text-secondary">
|
||||
The workspace URL will stay the same, but the display name updates everywhere.
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue="Acme Workspace"
|
||||
className="mt-4 h-9 w-full rounded-lg border-[0.5px] border-components-input-border-active bg-components-input-bg-normal px-3 text-sm text-components-input-text-filled outline-hidden focus:border-components-input-border-hover"
|
||||
/>
|
||||
<div className="mt-6 flex items-center justify-end gap-2">
|
||||
<Button variant="secondary" onClick={() => setOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="primary" onClick={() => setOpen(false)}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Controlled: Story = {
|
||||
render: () => <ControlledDemo />,
|
||||
}
|
||||
|
||||
export const ScrollingContent: Story = {
|
||||
render: () => (
|
||||
<Dialog>
|
||||
<DialogTrigger
|
||||
render={<button type="button" className={triggerButtonClassName} />}
|
||||
>
|
||||
Review release notes
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogCloseButton />
|
||||
<div className="flex flex-col gap-2 pr-8">
|
||||
<DialogTitle className="text-lg leading-7 font-semibold text-text-primary">
|
||||
Release notes
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-sm leading-5 text-text-secondary">
|
||||
Highlights from the latest workspace update.
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<ul className="mt-4 flex flex-col gap-3 text-sm leading-5 text-text-secondary">
|
||||
{Array.from({ length: 24 }, (_, index) => `improvement-${index + 1}`).map((id, index) => (
|
||||
<li key={id} className="rounded-lg bg-background-default-subtle px-3 py-2">
|
||||
<span className="font-medium text-text-primary">
|
||||
Improvement #
|
||||
{index + 1}
|
||||
:
|
||||
</span>
|
||||
{' '}
|
||||
Refined a workflow behavior so long content naturally overflows and scrolls inside the dialog.
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
),
|
||||
}
|
||||
@@ -28,7 +28,7 @@ const TriggerButton = ({ label = 'Open Menu' }: { label?: string }) => (
|
||||
)
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Navigation/DropdownMenu',
|
||||
title: 'Base/UI/DropdownMenu',
|
||||
component: DropdownMenu,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
|
||||
@@ -108,7 +108,7 @@ const DemoField = ({
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Form/NumberField',
|
||||
title: 'Base/UI/NumberField',
|
||||
component: NumberField,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
|
||||
199
web/app/components/base/ui/popover/index.stories.tsx
Normal file
199
web/app/components/base/ui/popover/index.stories.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import type { Placement } from '../placement'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Popover,
|
||||
PopoverClose,
|
||||
PopoverContent,
|
||||
PopoverDescription,
|
||||
PopoverTitle,
|
||||
PopoverTrigger,
|
||||
} from '.'
|
||||
import { Button } from '../button'
|
||||
|
||||
const triggerButtonClassName = 'rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-1.5 text-sm text-text-secondary shadow-xs hover:bg-state-base-hover'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/UI/Popover',
|
||||
component: Popover,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Compound popover built on Base UI Popover. Use it for contextual affordances, overflow menus, filters, and forms that anchor to a trigger. Control placement via the `placement` prop on `PopoverContent` and compose arbitrary children inside the popup.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof Popover>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
render={<button type="button" className={triggerButtonClassName} />}
|
||||
>
|
||||
Open popover
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<div className="flex w-72 flex-col gap-2 p-4">
|
||||
<PopoverTitle className="text-sm font-semibold text-text-primary">
|
||||
Keyboard shortcuts
|
||||
</PopoverTitle>
|
||||
<PopoverDescription className="text-xs text-text-secondary">
|
||||
Press
|
||||
{' '}
|
||||
<kbd className="rounded bg-background-default-subtle px-1 py-0.5 font-mono text-[11px]">⌘</kbd>
|
||||
{' '}
|
||||
+
|
||||
{' '}
|
||||
<kbd className="rounded bg-background-default-subtle px-1 py-0.5 font-mono text-[11px]">K</kbd>
|
||||
{' '}
|
||||
to open the command palette anywhere in the app.
|
||||
</PopoverDescription>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
),
|
||||
}
|
||||
|
||||
export const WithActions: Story = {
|
||||
render: () => (
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
render={<button type="button" className={triggerButtonClassName} />}
|
||||
>
|
||||
Share workspace
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<div className="flex w-80 flex-col gap-3 p-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<PopoverTitle className="text-sm font-semibold text-text-primary">
|
||||
Share workspace
|
||||
</PopoverTitle>
|
||||
<PopoverDescription className="text-xs text-text-secondary">
|
||||
Invite collaborators by email. They will get a pending invitation in their inbox.
|
||||
</PopoverDescription>
|
||||
</div>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="teammate@example.com"
|
||||
className="h-8 rounded-md border-[0.5px] border-components-input-border-active bg-components-input-bg-normal px-2 text-sm text-components-input-text-filled outline-hidden placeholder:text-components-input-text-placeholder focus:border-components-input-border-hover"
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<PopoverClose
|
||||
render={<Button variant="secondary" size="small" />}
|
||||
>
|
||||
Cancel
|
||||
</PopoverClose>
|
||||
<PopoverClose
|
||||
render={<Button variant="primary" size="small" />}
|
||||
>
|
||||
Send invite
|
||||
</PopoverClose>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
),
|
||||
}
|
||||
|
||||
const PLACEMENTS: Placement[] = [
|
||||
'top-start',
|
||||
'top',
|
||||
'top-end',
|
||||
'right-start',
|
||||
'right',
|
||||
'right-end',
|
||||
'bottom-start',
|
||||
'bottom',
|
||||
'bottom-end',
|
||||
'left-start',
|
||||
'left',
|
||||
'left-end',
|
||||
]
|
||||
|
||||
const PlacementsDemo = () => {
|
||||
const [placement, setPlacement] = useState<Placement>('bottom')
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4 p-20">
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
{PLACEMENTS.map(value => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
onClick={() => setPlacement(value)}
|
||||
className={`rounded-md border border-divider-subtle px-2 py-1 text-text-secondary ${
|
||||
placement === value ? 'bg-state-base-hover' : 'bg-components-button-secondary-bg'
|
||||
}`}
|
||||
>
|
||||
{value}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Popover open>
|
||||
<PopoverTrigger
|
||||
render={<button type="button" className={triggerButtonClassName} />}
|
||||
>
|
||||
Anchored trigger
|
||||
</PopoverTrigger>
|
||||
<PopoverContent placement={placement}>
|
||||
<div className="flex w-56 flex-col gap-1 p-3">
|
||||
<PopoverTitle className="text-sm font-semibold text-text-primary">
|
||||
placement="
|
||||
{placement}
|
||||
"
|
||||
</PopoverTitle>
|
||||
<PopoverDescription className="text-xs text-text-secondary">
|
||||
Popover positions itself relative to the trigger using the selected placement.
|
||||
</PopoverDescription>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Placements: Story = {
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
render: () => <PlacementsDemo />,
|
||||
}
|
||||
|
||||
const ControlledDemo = () => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="secondary" onClick={() => setOpen(prev => !prev)}>
|
||||
{open ? 'Close from outside' : 'Open from outside'}
|
||||
</Button>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
render={<button type="button" className={triggerButtonClassName} />}
|
||||
>
|
||||
Anchor
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<div className="flex w-64 flex-col gap-2 p-4">
|
||||
<PopoverTitle className="text-sm font-semibold text-text-primary">
|
||||
Controlled popover
|
||||
</PopoverTitle>
|
||||
<PopoverDescription className="text-xs text-text-secondary">
|
||||
Open state is owned by the parent. The trigger and the external button both toggle it.
|
||||
</PopoverDescription>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Controlled: Story = {
|
||||
render: () => <ControlledDemo />,
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from '.'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Layout/ScrollArea',
|
||||
title: 'Base/UI/ScrollArea',
|
||||
component: ScrollAreaRoot,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
|
||||
312
web/app/components/base/ui/select/index.stories.tsx
Normal file
312
web/app/components/base/ui/select/index.stories.tsx
Normal file
@@ -0,0 +1,312 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectItemIndicator,
|
||||
SelectItemText,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '.'
|
||||
|
||||
const triggerWidth = 'w-64'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/UI/Select',
|
||||
component: Select,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Compound select built on Base UI Select. Compose `SelectTrigger`, `SelectContent`, and `SelectItem` to build accessible single-value pickers with groups, labels, separators, and keyboard selection.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof Select>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<div className={triggerWidth}>
|
||||
<Select defaultValue="seattle">
|
||||
<SelectTrigger aria-label="City">
|
||||
<SelectValue placeholder="Select a city" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="seattle">
|
||||
<SelectItemText>Seattle</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
<SelectItem value="new-york">
|
||||
<SelectItemText>New York</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
<SelectItem value="tokyo">
|
||||
<SelectItemText>Tokyo</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
<SelectItem value="paris">
|
||||
<SelectItemText>Paris</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const WithPlaceholder: Story = {
|
||||
render: () => (
|
||||
<div className={triggerWidth}>
|
||||
<Select>
|
||||
<SelectTrigger aria-label="Model">
|
||||
<SelectValue placeholder="Choose a model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="gpt-5">
|
||||
<SelectItemText>GPT-5</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
<SelectItem value="claude-opus">
|
||||
<SelectItemText>Claude Opus</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
<SelectItem value="gemini-25">
|
||||
<SelectItemText>Gemini 2.5</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Sizes: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-3">
|
||||
{(['small', 'medium', 'large'] as const).map(size => (
|
||||
<div key={size} className={triggerWidth}>
|
||||
<Select defaultValue="seattle">
|
||||
<SelectTrigger aria-label={`${size} select`} size={size}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="seattle">
|
||||
<SelectItemText>Seattle</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
<SelectItem value="new-york">
|
||||
<SelectItemText>New York</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const WithGroupsAndSeparator: Story = {
|
||||
render: () => (
|
||||
<div className={triggerWidth}>
|
||||
<Select defaultValue="gpt-5">
|
||||
<SelectTrigger aria-label="Model">
|
||||
<SelectValue placeholder="Choose a model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>OpenAI</SelectLabel>
|
||||
<SelectItem value="gpt-5">
|
||||
<SelectItemText>GPT-5</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
<SelectItem value="gpt-4o">
|
||||
<SelectItemText>GPT-4o</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
<SelectSeparator />
|
||||
<SelectGroup>
|
||||
<SelectLabel>Anthropic</SelectLabel>
|
||||
<SelectItem value="claude-opus">
|
||||
<SelectItemText>Claude Opus</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
<SelectItem value="claude-sonnet">
|
||||
<SelectItemText>Claude Sonnet</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
<SelectSeparator />
|
||||
<SelectGroup>
|
||||
<SelectLabel>Google</SelectLabel>
|
||||
<SelectItem value="gemini-25">
|
||||
<SelectItemText>Gemini 2.5</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
<SelectItem value="gemini-flash">
|
||||
<SelectItemText>Gemini Flash</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const WithDisabledItem: Story = {
|
||||
render: () => (
|
||||
<div className={triggerWidth}>
|
||||
<Select defaultValue="free">
|
||||
<SelectTrigger aria-label="Plan">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="free">
|
||||
<SelectItemText>Free</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
<SelectItem value="team">
|
||||
<SelectItemText>Team</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
<SelectItem value="enterprise" disabled>
|
||||
<SelectItemText>Enterprise (contact sales)</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
render: () => (
|
||||
<div className={triggerWidth}>
|
||||
<Select defaultValue="seattle">
|
||||
<SelectTrigger aria-label="City" disabled>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="seattle">
|
||||
<SelectItemText>Seattle</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
<SelectItem value="new-york">
|
||||
<SelectItemText>New York</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const ReadOnly: Story = {
|
||||
render: () => (
|
||||
<div className={triggerWidth}>
|
||||
<Select defaultValue="seattle" readOnly>
|
||||
<SelectTrigger aria-label="City">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="seattle">
|
||||
<SelectItemText>Seattle</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
<SelectItem value="new-york">
|
||||
<SelectItemText>New York</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
const ControlledDemo = () => {
|
||||
const [value, setValue] = useState<string | null>('balanced')
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-3">
|
||||
<div className={triggerWidth}>
|
||||
<Select value={value} onValueChange={setValue}>
|
||||
<SelectTrigger aria-label="Routing strategy">
|
||||
<SelectValue placeholder="Choose a strategy" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="low-latency">
|
||||
<SelectItemText>Low latency</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
<SelectItem value="balanced">
|
||||
<SelectItemText>Balanced</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
<SelectItem value="high-quality">
|
||||
<SelectItemText>High quality</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<span className="text-xs text-text-tertiary">
|
||||
Selected:
|
||||
{value ?? '—'}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Controlled: Story = {
|
||||
render: () => <ControlledDemo />,
|
||||
}
|
||||
|
||||
export const InForm: Story = {
|
||||
render: () => (
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
}}
|
||||
className="flex w-72 flex-col gap-3"
|
||||
>
|
||||
<label className="text-xs font-medium text-text-tertiary" htmlFor="timezone">
|
||||
Timezone
|
||||
</label>
|
||||
<Select name="timezone" defaultValue="utc">
|
||||
<SelectTrigger id="timezone" aria-label="Timezone">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="utc">
|
||||
<SelectItemText>UTC</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
<SelectItem value="pst">
|
||||
<SelectItemText>Pacific (PST)</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
<SelectItem value="jst">
|
||||
<SelectItemText>Japan (JST)</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<button
|
||||
type="submit"
|
||||
className="h-8 rounded-lg bg-components-button-primary-bg px-3 text-sm text-components-button-primary-text"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
),
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { useState } from 'react'
|
||||
import { Slider } from '.'
|
||||
|
||||
const meta = {
|
||||
title: 'Base UI/Data Entry/Slider',
|
||||
title: 'Base/UI/Slider',
|
||||
component: Slider,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
|
||||
@@ -298,7 +298,7 @@ const ToastDocsDemo = () => {
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Feedback/UI Toast',
|
||||
title: 'Base/UI/Toast',
|
||||
component: ToastDocsDemo,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
|
||||
247
web/app/components/base/ui/tooltip/index.stories.tsx
Normal file
247
web/app/components/base/ui/tooltip/index.stories.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import type { Placement } from '../placement'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '.'
|
||||
|
||||
const triggerButtonClassName = 'rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-1.5 text-sm text-text-secondary shadow-xs hover:bg-state-base-hover'
|
||||
const iconButtonClassName = 'inline-flex h-8 w-8 items-center justify-center rounded-lg border border-divider-subtle bg-components-button-secondary-bg text-text-secondary shadow-xs hover:bg-state-base-hover'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/UI/Tooltip',
|
||||
component: Tooltip,
|
||||
decorators: [
|
||||
Story => (
|
||||
<TooltipProvider>
|
||||
<Story />
|
||||
</TooltipProvider>
|
||||
),
|
||||
],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Compound tooltip built on Base UI Tooltip. Wrap the app in `TooltipProvider` (done automatically in these stories) so multiple tooltips share open/close delays. Each tooltip pairs a `TooltipTrigger` with a `TooltipContent` and supports placement, offsets, and two style variants.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
} satisfies Meta<typeof Tooltip>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<button type="button" className={triggerButtonClassName} />}
|
||||
>
|
||||
Hover me
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Tooltips describe interactive elements without a click.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
),
|
||||
}
|
||||
|
||||
export const Plain: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Use `variant="plain"` to render the popup without default chrome (background, padding, typography). Apply your own styling via `className` on `TooltipContent`.',
|
||||
},
|
||||
},
|
||||
},
|
||||
render: () => (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<button type="button" className={triggerButtonClassName} />}
|
||||
>
|
||||
Preview details
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
variant="plain"
|
||||
className="rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-4 shadow-lg"
|
||||
>
|
||||
<div className="flex w-64 flex-col gap-1">
|
||||
<span className="text-sm font-semibold text-text-primary">Dataset preview</span>
|
||||
<span className="text-xs text-text-secondary">
|
||||
32 documents • Last indexed 2 minutes ago
|
||||
</span>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
),
|
||||
}
|
||||
|
||||
const PLACEMENTS: Placement[] = [
|
||||
'top-start',
|
||||
'top',
|
||||
'top-end',
|
||||
'right-start',
|
||||
'right',
|
||||
'right-end',
|
||||
'bottom-start',
|
||||
'bottom',
|
||||
'bottom-end',
|
||||
'left-start',
|
||||
'left',
|
||||
'left-end',
|
||||
]
|
||||
|
||||
const PlacementsDemo = () => {
|
||||
const [placement, setPlacement] = useState<Placement>('top')
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4 p-24">
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
{PLACEMENTS.map(value => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
onClick={() => setPlacement(value)}
|
||||
className={`rounded-md border border-divider-subtle px-2 py-1 text-text-secondary ${
|
||||
placement === value ? 'bg-state-base-hover' : 'bg-components-button-secondary-bg'
|
||||
}`}
|
||||
>
|
||||
{value}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Tooltip open>
|
||||
<TooltipTrigger
|
||||
render={<button type="button" className={triggerButtonClassName} />}
|
||||
>
|
||||
Anchor
|
||||
</TooltipTrigger>
|
||||
<TooltipContent placement={placement}>
|
||||
placement="
|
||||
{placement}
|
||||
"
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Placements: Story = {
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
render: () => <PlacementsDemo />,
|
||||
}
|
||||
|
||||
export const OnIconButtons: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Tooltips are essential for icon-only buttons. The trigger is the button; the tooltip provides the accessible label and hover hint.',
|
||||
},
|
||||
},
|
||||
},
|
||||
render: () => (
|
||||
<div className="flex items-center gap-3">
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<button type="button" aria-label="Edit" className={iconButtonClassName}>
|
||||
<span aria-hidden className="i-ri-pencil-line h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>Edit</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<button type="button" aria-label="Duplicate" className={iconButtonClassName}>
|
||||
<span aria-hidden className="i-ri-file-copy-line h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>Duplicate</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<button type="button" aria-label="Archive" className={iconButtonClassName}>
|
||||
<span aria-hidden className="i-ri-archive-line h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>Archive</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<button type="button" aria-label="Delete" className={iconButtonClassName}>
|
||||
<span aria-hidden className="i-ri-delete-bin-line h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>Delete</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
}
|
||||
|
||||
export const LongContent: Story = {
|
||||
render: () => (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<button type="button" className={triggerButtonClassName} />}
|
||||
>
|
||||
What are tokens?
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Tokens are the basic units a model reads. English text averages ~4 characters per token; non-Latin scripts often use more tokens per character. Both input and output count toward your quota.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
),
|
||||
}
|
||||
|
||||
const DELAY_PRESETS: Array<{ label: string, delay: number }> = [
|
||||
{ label: 'Instant (0ms)', delay: 0 },
|
||||
{ label: 'Fast (150ms)', delay: 150 },
|
||||
{ label: 'Default (600ms)', delay: 600 },
|
||||
]
|
||||
|
||||
const DelayDemo = () => {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
{DELAY_PRESETS.map(({ label, delay }) => (
|
||||
<TooltipProvider key={delay} delay={delay}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={<button type="button" className={triggerButtonClassName} />}
|
||||
>
|
||||
{label}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Appeared after
|
||||
{delay}
|
||||
ms hover delay.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const WithDelay: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: '`TooltipProvider` controls hover `delay` (and `closeDelay`) for the tooltips nested inside it. Adjacent tooltips under the same provider open instantly after the first has been shown.',
|
||||
},
|
||||
},
|
||||
},
|
||||
render: () => <DelayDemo />,
|
||||
}
|
||||
Reference in New Issue
Block a user