From e2b61afcf655ed2fcebdbdb4b69c3dfca8dca400 Mon Sep 17 00:00:00 2001 From: yyh Date: Sat, 13 Jun 2026 18:38:38 +0800 Subject: [PATCH] docs: drawer stories --- packages/dify-ui/src/drawer/index.stories.tsx | 646 ++++++++++++++++++ 1 file changed, 646 insertions(+) create mode 100644 packages/dify-ui/src/drawer/index.stories.tsx diff --git a/packages/dify-ui/src/drawer/index.stories.tsx b/packages/dify-ui/src/drawer/index.stories.tsx new file mode 100644 index 00000000000..523a7b26b3a --- /dev/null +++ b/packages/dify-ui/src/drawer/index.stories.tsx @@ -0,0 +1,646 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import type { DrawerRootSnapPoint } from '.' +import * as React from 'react' +import { + createDrawerHandle, + Drawer, + DrawerBackdrop, + DrawerClose, + DrawerCloseButton, + DrawerContent, + DrawerDescription, + DrawerIndent, + DrawerIndentBackground, + DrawerPopup, + DrawerPortal, + DrawerProvider, + DrawerSwipeArea, + DrawerTitle, + DrawerTrigger, + DrawerViewport, +} from '.' +import { Button } from '../button' +import { cn } from '../cn' +import { Input } from '../input' +import { ScrollArea } from '../scroll-area' + +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 outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid' +const textCloseClassName = 'inline-flex h-8 items-center justify-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-3.5 text-[13px] font-medium text-components-button-secondary-text shadow-xs outline-hidden hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid' +const primaryCloseClassName = 'inline-flex h-8 items-center justify-center rounded-lg border-components-button-primary-border bg-components-button-primary-bg px-3.5 text-[13px] font-medium text-components-button-primary-text shadow outline-hidden hover:border-components-button-primary-border-hover hover:bg-components-button-primary-bg-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid' +const destructiveCloseClassName = 'inline-flex h-8 items-center justify-center rounded-lg border-components-button-destructive-secondary-border bg-components-button-destructive-secondary-bg px-3.5 text-[13px] font-medium text-components-button-destructive-secondary-text outline-hidden hover:border-components-button-destructive-secondary-border-hover hover:bg-components-button-destructive-secondary-bg-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid' +const handleClassName = 'mx-auto mt-3 h-1 w-10 shrink-0 rounded-full bg-state-base-handle' + +const meta = { + title: 'Base/UI/Drawer', + component: Drawer, + parameters: { + layout: 'centered', + docs: { + description: { + component: 'Compound drawer built on Base UI Drawer. Use it for side panels, bottom sheets, nested editor panels, snap-point sheets, and mobile navigation surfaces that need swipe gestures. If the panel only needs modal focus management without gestures, use Dialog instead.', + }, + }, + }, + tags: ['autodocs'], +} satisfies Meta + +export default meta +type Story = StoryObj + +type DrawerFrameProps = { + title: string + description: string + children?: React.ReactNode + footer?: React.ReactNode + showHandle?: boolean + titleId?: string + contentClassName?: string +} + +function DrawerFrame({ + title, + description, + children, + footer, + showHandle, + titleId, + contentClassName, +}: DrawerFrameProps) { + return ( + + {showHandle &&
} +
+
+ + {title} + + + {description} + +
+ +
+ {children && ( +
+ {children} +
+ )} + {footer && ( +
+ {footer} +
+ )} + + ) +} + +function DrawerParts({ + children, + popupClassName, + backdropClassName, +}: { + children: React.ReactNode + popupClassName?: string + backdropClassName?: string +}) { + return ( + + + + + {children} + + + + ) +} + +const settingRows = [ + ['Production model', 'gpt-4.1'], + ['Retrieval source', 'Customer knowledge base'], + ['Response mode', 'Streaming'], +] as const + +export const Default: Story = { + render: () => ( + + }> + Open drawer + + + + Cancel + Save changes + + )} + > +
+ {settingRows.map(([label, value]) => ( +
+
{label}
+
{value}
+
+ ))} +
+
+
+
+ ), +} + +function ControlledDemo() { + const [open, setOpen] = React.useState(false) + + return ( +
+ + + State: + {' '} + {open ? 'open' : 'closed'} + + + + + Dismiss + Done + + )} + > + + + + +
+ ) +} + +export const Controlled: Story = { + render: () => , +} + +const directions = [ + { value: 'right', label: 'Right panel' }, + { value: 'left', label: 'Left panel' }, + { value: 'down', label: 'Bottom sheet' }, + { value: 'up', label: 'Top sheet' }, +] as const + +function PositionDrawer({ direction, label }: { + direction: typeof directions[number]['value'] + label: string +}) { + return ( + + }> + {label} + + + Close} + > +
+ Position is controlled by Base UI data attributes and the drawer popup classes, not by a separate wrapper component. +
+
+
+
+ ) +} + +export const Positions: Story = { + render: () => ( +
+ {directions.map(item => ( + + ))} +
+ ), +} + +const initialSnapPoint: DrawerRootSnapPoint = '18rem' +const snapPoints = [initialSnapPoint, 1] satisfies DrawerRootSnapPoint[] + +function SnapPointsDemo() { + const [snapPoint, setSnapPoint] = React.useState(initialSnapPoint) + + return ( + + }> + Open snap drawer + + + Done} + > +
+ Current snap point + {String(snapPoint)} +
+
+ {Array.from({ length: 12 }, (_, index) => ( +
+ + {index + 1} + +
+
Activity checkpoint
+
Visible in the scrollable content area.
+
+
+ ))} +
+
+
+
+ ) +} + +export const SnapPoints: Story = { + render: () => , +} + +const nestedPopupClassName = cn( + 'data-[swipe-direction=down]:max-h-[82dvh]', + 'data-nested-drawer-open:overflow-hidden data-nested-drawer-open:shadow-md', + 'data-nested-drawer-open:[filter:brightness(0.96)]', +) + +export const NestedDrawers: Story = { + render: () => ( + + }> + Open drawer stack + + + + + }> + Security settings + + + + + }> + Advanced options + + + Done} + > + + + + + Close security + + )} + > +
    +
  • Passkeys enabled
  • +
  • 2FA via authenticator app
  • +
  • 3 signed-in devices
  • +
+
+
+
+ Close + + )} + /> +
+
+ ), +} + +function IndentEffectDemo() { + const [portalContainer, setPortalContainer] = React.useState(null) + + return ( + +
+ + +
+

Indent provider surface

+

+ The background and app shell respond when any drawer inside the provider opens. +

+
+ + }> + Open indent drawer + + + + + + Close} + /> + + + + +
+
+
+ ) +} + +export const IndentEffect: Story = { + parameters: { + layout: 'centered', + }, + render: () => , +} + +export const NonModal: Story = { + render: () => ( + + }> + Open non-modal drawer + + + Close} + > + + + + + ), +} + +const navItems = ['Explore', 'Apps', 'Datasets', 'Workflows'] as const +const componentItems = [ + 'Autocomplete', + 'Button', + 'Combobox', + 'Dialog', + 'Drawer', + 'Field', + 'Form', + 'Popover', + 'Select', + 'Tabs', + 'Tooltip', + 'Toast', +] as const + +export const MobileNavigation: Story = { + render: () => ( + + }> + Open mobile menu + + + + + + ), +} + +function SwipeToOpenDemo() { + const [portalContainer, setPortalContainer] = React.useState(null) + + return ( +
+ + + Swipe + +
+
+
Swipe area
+
Drag from the highlighted right edge to open the drawer.
+
+
+ + + + + Close} + /> + + + +
+
+ ) +} + +export const SwipeToOpen: Story = { + render: () => , +} + +const actionItems = [ + ['Duplicate app', 'Create a copy in the same workspace.'], + ['Export DSL', 'Download the workflow definition.'], + ['Move to folder', 'Organize this app with related work.'], +] as const + +export const ActionSheet: Story = { + render: () => ( + + }> + Open action sheet + + + +
+ + App actions + + + Choose an action for Customer support assistant. + +
+ {actionItems.map(([label, description]) => ( + + {label} + {description} + + ))} +
+ + Delete app + + + Cancel + + + + + ), +} + +type DetachedPayload = { + title: string + description: string + fields: readonly string[] +} + +const detachedPayloads = [ + { + title: 'Profile', + description: 'Update identity fields for the current member.', + fields: ['Display name', 'Role', 'Location'], + }, + { + title: 'Billing', + description: 'Review workspace billing contacts and usage limits.', + fields: ['Plan', 'Billing email', 'Monthly usage'], + }, +] as const satisfies readonly DetachedPayload[] + +function DetachedTriggersDemo() { + const [drawerHandle] = React.useState(() => createDrawerHandle()) + + return ( +
+
+ {detachedPayloads.map(payload => ( + } + > + {payload.title} + + ))} +
+ + {({ payload }) => ( + + Done} + > +
+ {(payload?.fields ?? ['Detached trigger']).map(field => ( +
+ {field} +
+ ))} +
+
+
+ )} +
+
+ ) +} + +export const DetachedTriggers: Story = { + render: () => , +} + +export const StackingAndAnimations: Story = { + render: () => ( + + }> + Open animated drawer + + + Close} + > +
+ {['data-starting-style', 'data-ending-style', 'data-swiping', 'data-nested-drawer-open'].map(attribute => ( +
+ {attribute} +
+ ))} +
+
+
+
+ ), +}