import type { Meta, StoryObj } from '@storybook/nextjs-vite' import type { ReactNode } from 'react' import * as React from 'react' import AppIcon from '@/app/components/base/app-icon' import { cn } from '@/utils/classnames' import { ScrollAreaContent, ScrollAreaCorner, ScrollAreaRoot, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport, } from '.' const meta = { title: 'Base/Layout/ScrollArea', component: ScrollAreaRoot, parameters: { layout: 'padded', docs: { description: { component: 'Compound scroll container built on Base UI ScrollArea. These stories focus on panel-style compositions that already exist throughout Dify: dense sidebars, sticky list headers, multi-pane workbenches, horizontal rails, and overlay surfaces. Scrollbar placement should be adjusted by consumer spacing classes such as margin-based overrides instead of right/bottom positioning utilities.', }, }, }, tags: ['autodocs'], } satisfies Meta export default meta type Story = StoryObj const panelClassName = 'overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg shadow-shadow-shadow-5' const blurPanelClassName = 'overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-xl shadow-shadow-shadow-7 backdrop-blur-[6px]' const labelClassName = 'text-text-tertiary system-xs-medium-uppercase tracking-[0.14em]' const titleClassName = 'text-text-primary system-sm-semibold' const bodyClassName = 'text-text-secondary system-sm-regular' const insetScrollAreaClassName = 'h-full p-1' const insetViewportClassName = 'rounded-[20px] bg-components-panel-bg' const insetScrollbarClassName = 'data-[orientation=vertical]:my-1 data-[orientation=vertical]:[margin-inline-end:0.25rem] data-[orientation=horizontal]:mx-1 data-[orientation=horizontal]:mb-1' const storyButtonClassName = 'flex w-full items-center justify-between gap-3 rounded-xl border border-divider-subtle bg-components-panel-bg-alt px-3 py-2.5 text-left text-text-secondary transition-colors hover:bg-state-base-hover focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-components-input-border-hover motion-reduce:transition-none' const sidebarScrollAreaClassName = 'h-full' const sidebarViewportClassName = 'overscroll-contain' const sidebarContentClassName = 'space-y-0.5' const sidebarScrollbarClassName = 'data-[orientation=vertical]:my-2 data-[orientation=vertical]:[margin-inline-end:-0.75rem]' const appNavButtonClassName = 'group flex h-8 w-full items-center justify-between gap-3 rounded-lg px-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-components-input-border-hover motion-reduce:transition-none' const appNavMetaClassName = 'shrink-0 rounded-md border border-divider-subtle bg-components-panel-bg-alt px-1.5 py-0.5 text-text-quaternary system-2xs-medium-uppercase tracking-[0.08em]' const releaseRows = [ { title: 'Agent refactor', meta: 'Updated 2 hours ago', status: 'Ready' }, { title: 'Retriever tuning', meta: 'Updated yesterday', status: 'Review' }, { title: 'Workflow replay', meta: 'Updated 3 days ago', status: 'Draft' }, { title: 'Sandbox policy', meta: 'Updated this week', status: 'Ready' }, { title: 'SSE diagnostics', meta: 'Updated last week', status: 'Blocked' }, { title: 'Model routing', meta: 'Updated 9 days ago', status: 'Review' }, { title: 'Chunk overlap', meta: 'Updated 11 days ago', status: 'Draft' }, { title: 'Vector warmup', meta: 'Updated 2 weeks ago', status: 'Ready' }, ] as const const queueRows = [ { id: 'PLG-142', title: 'Plugin catalog sync', note: 'Waiting for moderation result' }, { id: 'OPS-088', title: 'Billing alert fallback', note: 'Last retry finished 12 minutes ago' }, { id: 'RAG-511', title: 'Embedding migration', note: '16 datasets still pending' }, { id: 'AGT-204', title: 'Multi-agent tracing', note: 'QA is verifying edge cases' }, { id: 'UI-390', title: 'Prompt editor polish', note: 'Needs token density pass' }, { id: 'WEB-072', title: 'Marketplace empty state', note: 'Waiting for design review' }, ] as const const horizontalCards = [ { title: 'Claude Opus', detail: 'Reasoning-heavy preset' }, { title: 'GPT-5.4', detail: 'Balanced orchestration lane' }, { title: 'Gemini 2.5', detail: 'Multimodal fallback' }, { title: 'Qwen Max', detail: 'Regional deployment' }, { title: 'DeepSeek R1', detail: 'High-throughput analysis' }, { title: 'Llama 4', detail: 'Cost-sensitive routing' }, ] as const const activityRows = Array.from({ length: 14 }, (_, index) => ({ title: `Workspace activity ${index + 1}`, body: 'A short line of copy to mimic dense operational feeds in settings and debug panels.', })) const scrollbarShowcaseRows = Array.from({ length: 18 }, (_, index) => ({ title: `Scroll checkpoint ${index + 1}`, body: 'Dedicated story content so the scrollbar can be inspected without sticky headers, masks, or clipped shells.', })) const horizontalShowcaseCards = Array.from({ length: 8 }, (_, index) => ({ title: `Lane ${index + 1}`, body: 'Horizontal scrollbar reference without edge hints.', })) const webAppsRows = [ { id: 'invoice-copilot', name: 'Invoice Copilot', meta: 'Pinned', icon: '🧾', iconBackground: '#FFEAD5', selected: true, pinned: true }, { id: 'rag-ops', name: 'RAG Ops Console', meta: 'Ops', icon: 'πŸ›°οΈ', iconBackground: '#E0F2FE', selected: false, pinned: true }, { id: 'knowledge-studio', name: 'Knowledge Studio', meta: 'Docs', icon: 'πŸ“š', iconBackground: '#FEF3C7', selected: false, pinned: true }, { id: 'workflow-studio', name: 'Workflow Studio', meta: 'Build', icon: '🧩', iconBackground: '#E0E7FF', selected: false, pinned: true }, { id: 'growth-briefs', name: 'Growth Briefs', meta: 'Brief', icon: 'πŸ“£', iconBackground: '#FCE7F3', selected: false, pinned: true }, { id: 'agent-playground', name: 'Agent Playground', meta: 'Lab', icon: 'πŸ§ͺ', iconBackground: '#DCFCE7', selected: false, pinned: false }, { id: 'sales-briefing', name: 'Sales Briefing', meta: 'Team', icon: 'πŸ“ˆ', iconBackground: '#FCE7F3', selected: false, pinned: false }, { id: 'support-triage', name: 'Support Triage', meta: 'Queue', icon: '🎧', iconBackground: '#EDE9FE', selected: false, pinned: false }, { id: 'legal-review', name: 'Legal Review', meta: 'Beta', icon: 'βš–οΈ', iconBackground: '#FDE68A', selected: false, pinned: false }, { id: 'release-watcher', name: 'Release Watcher', meta: 'Feed', icon: 'πŸš€', iconBackground: '#DBEAFE', selected: false, pinned: false }, { id: 'research-hub', name: 'Research Hub', meta: 'Notes', icon: 'πŸ”Ž', iconBackground: '#E0F2FE', selected: false, pinned: false }, { id: 'field-enablement', name: 'Field Enablement', meta: 'Team', icon: '🧭', iconBackground: '#DCFCE7', selected: false, pinned: false }, { id: 'brand-monitor', name: 'Brand Monitor', meta: 'Watch', icon: 'πŸͺ„', iconBackground: '#F3E8FF', selected: false, pinned: false }, { id: 'finance-ops', name: 'Finance Ops Desk', meta: 'Ops', icon: 'πŸ’³', iconBackground: '#FEF3C7', selected: false, pinned: false }, { id: 'security-radar', name: 'Security Radar', meta: 'Risk', icon: 'πŸ›‘οΈ', iconBackground: '#FEE2E2', selected: false, pinned: false }, { id: 'partner-portal', name: 'Partner Portal', meta: 'Ext', icon: '🀝', iconBackground: '#DBEAFE', selected: false, pinned: false }, { id: 'qa-replays', name: 'QA Replays', meta: 'Debug', icon: '🎞️', iconBackground: '#EDE9FE', selected: false, pinned: false }, { id: 'roadmap-notes', name: 'Roadmap Notes', meta: 'Plan', icon: 'πŸ—ΊοΈ', iconBackground: '#FFEAD5', selected: false, pinned: false }, ] as const const StoryCard = ({ eyebrow, title, description, className, children, }: { eyebrow: string title: string description: string className?: string children: ReactNode }) => (
{eyebrow}

{title}

{description}

{children}
) const VerticalPanelPane = () => (
Release board
Weekly checkpoints

A simple vertical panel with the default scrollbar skin and no business-specific overrides.

{releaseRows.map(item => (

{item.title}

{item.meta}

{item.status}
))}
) const StickyListPane = () => (
Sticky header
Operational queue

The scrollbar is still the shared base/ui primitive, while the pane adds sticky structure and a viewport mask.

24 items
{queueRows.map(item => (
{item.title}
{item.note}
{item.id}
))}
) const WorkbenchPane = ({ title, eyebrow, children, className, }: { title: string eyebrow: string children: ReactNode className?: string }) => (
{eyebrow}
{title}
{children}
) const HorizontalRailPane = () => (
Horizontal rail
Model lanes

This pane keeps the default track behavior and only changes the surface layout around it.

{horizontalCards.map(card => (
{card.title}
{card.detail}
Drag cards into orchestration groups.
))}
) const ScrollbarStatePane = ({ eyebrow, title, description, initialPosition, }: { eyebrow: string title: string description: string initialPosition: 'top' | 'middle' | 'bottom' }) => { const viewportId = React.useId() React.useEffect(() => { let frameA = 0 let frameB = 0 const syncScrollPosition = () => { const viewport = document.getElementById(viewportId) if (!(viewport instanceof HTMLDivElement)) return const maxScrollTop = Math.max(0, viewport.scrollHeight - viewport.clientHeight) if (initialPosition === 'top') viewport.scrollTop = 0 if (initialPosition === 'middle') viewport.scrollTop = maxScrollTop / 2 if (initialPosition === 'bottom') viewport.scrollTop = maxScrollTop } frameA = requestAnimationFrame(() => { frameB = requestAnimationFrame(syncScrollPosition) }) return () => { cancelAnimationFrame(frameA) cancelAnimationFrame(frameB) } }, [initialPosition, viewportId]) return (
{eyebrow}
{title}

{description}

{scrollbarShowcaseRows.map(item => (
{item.title}
{item.body}
))}
) } const HorizontalScrollbarShowcasePane = () => (
Horizontal
Horizontal track reference

Current design delivery defines the horizontal scrollbar body, but not a horizontal edge hint.

Horizontal scrollbar
A clean horizontal pane to inspect thickness, padding, and thumb behavior without extra masks.
{horizontalShowcaseCards.map(card => (
{card.title}
{card.body}
))}
) const OverlayPane = () => (
Overlay palette
Quick actions
{activityRows.map(item => (
{item.title}
{item.body}
))}
) const CornerPane = () => (
Corner surface
Bi-directional inspector canvas

Both axes overflow here so the corner becomes visible as a deliberate seam between the two tracks.

Always visible
{Array.from({ length: 12 }, (_, index) => (
Cell {' '} {index + 1}

Wide-and-tall content to force both scrollbars and show the corner treatment clearly.

))}
) const ExploreSidebarWebAppsPane = () => { const pinnedAppsCount = webAppsRows.filter(item => item.pinned).length return (
Explore

Web Apps

{webAppsRows.length}
{webAppsRows.map((item, index) => (
{index === pinnedAppsCount - 1 && index !== webAppsRows.length - 1 && (
)}
))}
) } export const VerticalPanels: Story = { render: () => (
), } export const ThreePaneWorkbench: Story = { render: () => (
{releaseRows.map(item => ( ))}
{Array.from({ length: 7 }, (_, index) => (
Section {' '} {index + 1}
Active

This pane is intentionally long so the default vertical scrollbar sits over a larger editorial surface.

))}
{queueRows.map(item => (
{item.id}
{item.title}
{item.note}
))}
), } export const HorizontalAndOverlay: Story = { render: () => (
), } export const CornerSurface: Story = { render: () => (
), } export const ExploreSidebarWebApps: Story = { render: () => (
), } export const PrimitiveComposition: Story = { render: () => (
{Array.from({ length: 8 }, (_, index) => (
Primitive row {' '} {index + 1}
))}
), } export const ScrollbarDelivery: Story = { render: () => (
), }