fix(dify-ui): standardize story focus indicators (#38417)

This commit is contained in:
yyh 2026-07-04 22:59:24 +08:00 committed by GitHub
parent a0f8347513
commit 5d7ae56deb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 273 additions and 146 deletions

View File

@ -13,7 +13,7 @@ import {
} 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 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 meta = {
title: 'Base/UI/AlertDialog',

View File

@ -97,7 +97,7 @@ export const Controlled: Story = {
<div className="flex flex-col items-start gap-3">
<button
type="button"
className="rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-1.5 system-sm-medium text-components-button-secondary-text shadow-xs shadow-shadow-shadow-3 hover:bg-state-base-hover focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid"
className="rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-1.5 system-sm-medium text-components-button-secondary-text shadow-xs shadow-shadow-shadow-3 outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid"
onClick={() => setOpen(value => !value)}
>
{open ? 'Close panel' : 'Open panel'}

View File

@ -22,7 +22,7 @@ import {
const TriggerArea = ({ label = 'Right-click inside this area' }: { label?: string }) => (
<ContextMenuTrigger
aria-label="context menu trigger area"
render={<button type="button" className="flex h-44 w-80 items-center justify-center rounded-xl border border-divider-subtle bg-background-default-subtle px-6 text-center text-sm text-text-tertiary select-none" />}
render={<button type="button" className="flex h-44 w-80 items-center justify-center rounded-xl border border-divider-subtle bg-background-default-subtle px-6 text-center text-sm text-text-tertiary outline-hidden select-none focus-visible:ring-2 focus-visible:ring-state-accent-solid" />}
>
{label}
</ContextMenuTrigger>

View File

@ -3,17 +3,27 @@ import * as React from 'react'
import { expect, waitFor, within } from 'storybook/test'
import {
Dialog,
DialogBackdrop,
DialogCloseButton,
DialogContent,
DialogDescription,
DialogPopup,
DialogPortal,
DialogTitle,
DialogTrigger,
DialogViewport,
} from '.'
import { Button } from '../button'
import { FieldControl, FieldDescription, FieldError, FieldLabel, FieldRoot } from '../field'
import { Form } from '../form'
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'
import { Input } from '../input'
import {
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '../scroll-area'
const meta = {
title: 'Base/UI/Dialog',
@ -32,11 +42,65 @@ const meta = {
export default meta
type Story = StoryObj<typeof meta>
const releaseNoteItems = Array.from({ length: 24 }, (_, index) => ({
id: `improvement-${index + 1}`,
title: `Improvement #${index + 1}`,
body: 'Refined a workflow behavior so long content naturally overflows and scrolls inside the dialog.',
}))
function ReleaseNoteHeader({
title,
description,
}: {
title: string
description: string
}) {
return (
<div className="flex shrink-0 flex-col gap-2 p-6 pr-12 pb-4">
<DialogTitle className="text-lg leading-7 font-semibold text-text-primary">
{title}
</DialogTitle>
<DialogDescription className="text-sm leading-5 text-text-secondary">
{description}
</DialogDescription>
</div>
)
}
function ReleaseNoteSections() {
return (
<div className="flex flex-col gap-3 px-6 py-2 text-sm leading-5 text-text-secondary">
{releaseNoteItems.map(item => (
<section key={item.id} className="rounded-lg bg-background-default-subtle px-3 py-2">
<h3 className="font-medium text-text-primary">
{item.title}
</h3>
<p>{item.body}</p>
</section>
))}
</div>
)
}
function ReleaseNoteFooter({
onClose,
}: {
onClose: () => void
}) {
return (
<div className="flex shrink-0 justify-end border-t border-divider-subtle p-4">
<Button onClick={onClose}>
Close
</Button>
</div>
)
}
export const Default: Story = {
render: () => (
<Dialog>
<DialogTrigger
render={<button type="button" className={triggerButtonClassName} />}
render={<Button />}
>
Open dialog
</DialogTrigger>
@ -54,11 +118,10 @@ export const Default: Story = {
<label className="text-xs font-medium text-text-tertiary" htmlFor="invite-email">
Email address
</label>
<input
<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">
@ -89,7 +152,7 @@ export const WithoutCloseButton: Story = {
render: () => (
<Dialog>
<DialogTrigger
render={<button type="button" className={triggerButtonClassName} />}
render={<Button />}
>
Start onboarding
</DialogTrigger>
@ -135,10 +198,10 @@ const ControlledDemo = () => {
The workspace URL will stay the same, but the display name updates everywhere.
</DialogDescription>
</div>
<input
<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"
className="mt-4"
/>
<div className="mt-6 flex items-center justify-end gap-2">
<Button variant="secondary" onClick={() => setOpen(false)}>
@ -170,7 +233,7 @@ const FormDialogDemo = () => {
return (
<Dialog open={open} onOpenChange={setOpen} disablePointerDismissal>
<DialogTrigger
render={<button type="button" className={triggerButtonClassName} />}
render={<Button />}
>
Configure API extension
</DialogTrigger>
@ -201,7 +264,7 @@ const FormDialogDemo = () => {
href="https://docs.dify.ai/use-dify/workspace/api-extension/api-extension"
target="_blank"
rel="noopener noreferrer"
className="inline-flex w-fit items-center text-text-accent focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
className="inline-flex w-fit items-center text-text-accent outline-hidden focus-visible:ring-1 focus-visible:ring-components-input-border-active"
>
View API extension docs
</a>
@ -241,38 +304,128 @@ export const FormDialog: Story = {
render: () => <FormDialogDemo />,
}
export const ScrollingContent: Story = {
const OutsideScrollingContentDemo = () => {
const [open, setOpen] = React.useState(false)
const popupRef = React.useRef<HTMLDivElement>(null)
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
render={<Button />}
>
Review long release notes
</DialogTrigger>
<DialogPortal>
<DialogBackdrop className="duration-[600ms] ease-[cubic-bezier(0.22,1,0.36,1)] data-ending-style:duration-[350ms] data-ending-style:ease-[cubic-bezier(0.375,0.015,0.545,0.455)]" />
<DialogViewport className="group/dialog">
<ScrollAreaRoot className="h-full overscroll-contain group-data-ending-style/dialog:pointer-events-none">
<ScrollAreaViewport aria-label="Scrollable dialog viewport" role="region" className="h-full max-h-full max-w-full overscroll-contain group-data-ending-style/dialog:pointer-events-none">
<ScrollAreaContent className="flex min-h-full items-center justify-center px-4 py-16">
<DialogPopup
ref={popupRef}
initialFocus={popupRef}
className="relative mx-auto flex w-120 max-w-[calc(100vw-2rem)] flex-col overflow-hidden outline-hidden transition-[translate] duration-[700ms] ease-[cubic-bezier(0.45,1.005,0,1.005)] data-starting-style:translate-y-[100dvh] data-starting-style:scale-100 data-starting-style:opacity-100 data-ending-style:translate-y-[max(100dvh,100%)] data-ending-style:scale-100 data-ending-style:opacity-100 data-ending-style:duration-[350ms] data-ending-style:ease-[cubic-bezier(0.375,0.015,0.545,0.455)]"
>
<DialogCloseButton />
<ReleaseNoteHeader
title="Long release notes"
description="This layout lets the outer dialog viewport scroll while the popup keeps its natural height."
/>
<ReleaseNoteSections />
<ReleaseNoteFooter onClose={() => setOpen(false)} />
</DialogPopup>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</DialogViewport>
</DialogPortal>
</Dialog>
)
}
export const OutsideScrollingContent: Story = {
render: () => <OutsideScrollingContentDemo />,
}
export const OutsidePopupElements: Story = {
render: () => (
<Dialog>
<DialogTrigger
render={<button type="button" className={triggerButtonClassName} />}
render={<Button />}
>
Review release notes
Open uncontained dialog
</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>
<DialogPortal>
<DialogBackdrop className="min-h-dvh" />
<DialogViewport className="grid place-items-center px-4 py-12 xl:py-6">
<DialogPopup className="group/popup relative flex h-full w-full max-w-[70rem] justify-center border-0 bg-transparent shadow-none pointer-events-none transition-opacity data-starting-style:scale-100 data-starting-style:opacity-0 data-ending-style:scale-100 data-ending-style:opacity-0">
<DialogCloseButton
aria-label="Close"
className="pointer-events-auto absolute right-0 -top-10 z-10 flex size-8 items-center justify-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg text-text-tertiary shadow-xs outline-hidden hover:bg-components-button-secondary-bg-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid xl:top-0"
>
</DialogCloseButton>
<div className="pointer-events-auto flex h-full w-full max-w-[70rem] flex-col overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-6 shadow-xl transition-[scale] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] group-data-starting-style/popup:scale-105">
<div className="grid gap-2">
<DialogTitle className="text-lg leading-7 font-semibold text-text-primary">
Knowledge review
</DialogTitle>
<DialogDescription className="text-sm leading-5 text-text-secondary">
The close button is visually outside this panel but remains inside the popup for focus order and screen reader context.
</DialogDescription>
</div>
<div className="mt-6 grid flex-1 place-items-center rounded-xl bg-background-default-subtle text-sm text-text-tertiary">
Panel content
</div>
</div>
</DialogPopup>
</DialogViewport>
</DialogPortal>
</Dialog>
),
}
const InsideScrollingContentDemo = () => {
const [open, setOpen] = React.useState(false)
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
render={<Button />}
>
Review release notes
</DialogTrigger>
<DialogPortal>
<DialogBackdrop />
<DialogViewport className="flex items-center justify-center overflow-hidden p-4">
<DialogPopup
className="relative flex h-[min(44rem,calc(100dvh-2rem))] w-120 max-w-[calc(100vw-2rem)] min-h-0 flex-col overflow-hidden"
>
<DialogCloseButton />
<ReleaseNoteHeader
title="Release notes"
description="Highlights from the latest workspace update."
/>
<ScrollAreaRoot className="relative flex min-h-0 flex-auto overflow-hidden">
<ScrollAreaViewport aria-label="Release note improvements" role="region" className="h-full max-h-full max-w-full overflow-y-auto overscroll-contain">
<ScrollAreaContent>
<ReleaseNoteSections />
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
<ReleaseNoteFooter onClose={() => setOpen(false)} />
</DialogPopup>
</DialogViewport>
</DialogPortal>
</Dialog>
)
}
export const InsideScrollingContent: Story = {
render: () => <InsideScrollingContentDemo />,
}

View File

@ -8,6 +8,63 @@ export const Dialog = BaseDialog.Root
export const DialogTrigger = BaseDialog.Trigger
export const DialogTitle = BaseDialog.Title
export const DialogDescription = BaseDialog.Description
export const DialogPortal = BaseDialog.Portal
type DialogBackdropProps = Omit<BaseDialog.Backdrop.Props, 'className'> & {
className?: string
}
export function DialogBackdrop({
className,
...props
}: DialogBackdropProps) {
return (
<BaseDialog.Backdrop
{...props}
className={cn(
'absolute inset-0 z-50 bg-background-overlay',
'transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 motion-reduce:transition-none',
className,
)}
/>
)
}
type DialogViewportProps = Omit<BaseDialog.Viewport.Props, 'className'> & {
className?: string
}
export function DialogViewport({
className,
...props
}: DialogViewportProps) {
return (
<BaseDialog.Viewport
className={cn('fixed inset-0 z-50', className)}
{...props}
/>
)
}
type DialogPopupProps = Omit<BaseDialog.Popup.Props, 'className'> & {
className?: string
}
export function DialogPopup({
className,
...props
}: DialogPopupProps) {
return (
<BaseDialog.Popup
className={cn(
'z-50 rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl',
'transition-[transform,scale,opacity] duration-150 data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0 motion-reduce:transition-none',
className,
)}
{...props}
/>
)
}
type DialogCloseButtonProps = Omit<BaseDialog.Close.Props, 'children'>
@ -21,7 +78,7 @@ export function DialogCloseButton({
aria-label={ariaLabel}
{...props}
className={cn(
'absolute top-6 end-6 z-10 flex h-5 w-5 cursor-pointer items-center justify-center rounded-2xl hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
'absolute top-6 end-6 z-10 flex h-5 w-5 cursor-pointer items-center justify-center rounded-2xl hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
>
@ -44,24 +101,16 @@ export function DialogContent({
backdropProps,
}: DialogContentProps) {
return (
<BaseDialog.Portal>
<BaseDialog.Backdrop
{...backdropProps}
<DialogPortal>
<DialogBackdrop {...backdropProps} className={backdropClassName} />
<DialogPopup
className={cn(
'absolute inset-0 z-50 bg-background-overlay',
'transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 motion-reduce:transition-none',
backdropClassName,
)}
/>
<BaseDialog.Popup
className={cn(
'fixed top-1/2 left-1/2 z-50 max-h-[80dvh] w-120 max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-6 shadow-xl',
'transition-[transform,scale,opacity] duration-150 data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0 motion-reduce:transition-none',
'fixed top-1/2 left-1/2 max-h-[80dvh] w-120 max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain p-6',
className,
)}
>
{children}
</BaseDialog.Popup>
</BaseDialog.Portal>
</DialogPopup>
</DialogPortal>
)
}

View File

@ -21,7 +21,7 @@ import {
const TriggerButton = ({ label = 'Open Menu' }: { label?: string }) => (
<DropdownMenuTrigger
render={<button type="button" className="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" />}
render={<button type="button" className="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" />}
>
{label}
</DropdownMenuTrigger>

View File

@ -170,7 +170,7 @@ export const InTooltip: Story = {
<button
type="button"
aria-label="Collapse sidebar"
className="inline-flex size-8 items-center justify-center rounded-lg border border-divider-subtle bg-components-button-secondary-bg text-text-secondary shadow-xs"
className="inline-flex size-8 items-center justify-center rounded-lg border border-divider-subtle bg-components-button-secondary-bg text-text-secondary shadow-xs outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid"
>
<span aria-hidden className="i-ri-sidebar-fold-line size-4" />
</button>
@ -204,7 +204,7 @@ export const InContextMenu: Story = {
render={(
<button
type="button"
className="flex h-28 w-60 items-center justify-center rounded-xl border border-divider-subtle bg-background-default-subtle px-6 text-center system-sm-regular text-text-tertiary"
className="flex h-28 w-60 items-center justify-center rounded-xl border border-divider-subtle bg-background-default-subtle px-6 text-center system-sm-regular text-text-tertiary outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid"
/>
)}
>

View File

@ -12,7 +12,7 @@ import {
} from '.'
import { Kbd, KbdGroup } from '../kbd'
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 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 meta = {
title: 'Base/UI/Popover',
@ -81,7 +81,7 @@ export const WithActions: Story = {
<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"
className="h-8 rounded-md border border-transparent bg-components-input-bg-normal px-2 text-sm text-components-input-text-filled outline-hidden placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs"
/>
<div className="flex items-center justify-end gap-2">
<PopoverClose
@ -169,7 +169,7 @@ const PlacementsDemo = () => {
key={value}
type="button"
onClick={() => setPlacement(value)}
className={`rounded-md border border-divider-subtle px-2 py-1 text-text-secondary ${
className={`rounded-md border border-divider-subtle px-2 py-1 text-text-secondary outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid ${
placement === value ? 'bg-state-base-hover' : 'bg-components-button-secondary-bg'
}`}
>

View File

@ -9,13 +9,13 @@ import {
} from '.'
const rowButtonClassName
= 'flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm text-text-secondary hover:bg-state-base-hover'
= 'flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm text-text-secondary outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid'
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'
= '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 inlineLinkClassName
= 'text-text-accent underline decoration-text-accent/60 decoration-1 underline-offset-2 outline-hidden hover:decoration-text-accent focus-visible:rounded-xs focus-visible:no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-text-accent data-[popup-open]:decoration-text-accent'
= 'text-text-accent underline decoration-text-accent/60 decoration-1 underline-offset-2 outline-hidden hover:decoration-text-accent focus-visible:rounded-xs focus-visible:no-underline focus-visible:ring-1 focus-visible:ring-components-input-border-active data-[popup-open]:decoration-text-accent'
const meta = {
title: 'Base/UI/PreviewCard',
@ -154,7 +154,7 @@ const PlacementsDemo = () => {
key={value}
type="button"
onClick={() => setPlacement(value)}
className={`rounded-md border border-divider-subtle px-2 py-1 text-text-secondary ${
className={`rounded-md border border-divider-subtle px-2 py-1 text-text-secondary outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid ${
placement === value ? 'bg-state-base-hover' : 'bg-components-button-secondary-bg'
}`}
>

View File

@ -136,7 +136,7 @@ function OptionCardsDemo() {
variant="unstyled"
nativeButton
render={<button type="button" />}
className="w-full rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg p-4 text-left transition-colors hover:bg-state-base-hover data-checked:border-components-option-card-option-selected-border data-checked:bg-components-option-card-option-selected-bg"
className="w-full rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg p-4 text-left outline-hidden transition-colors hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid data-checked:border-components-option-card-option-selected-border data-checked:bg-components-option-card-option-selected-bg"
>
<div className="flex items-start justify-between gap-3">
<div>

View File

@ -29,21 +29,6 @@ type Story = StoryObj<typeof meta>
const verticalContentStyle = { minWidth: 0 } satisfies React.CSSProperties
const appRows = [
{ name: 'Invoice Copilot', meta: 'Pinned', icon: 'i-ri-file-list-3-line', selected: true, pinned: true },
{ name: 'RAG Ops Console', meta: 'Ops', icon: 'i-ri-database-2-line', selected: false, pinned: true },
{ name: 'Knowledge Studio', meta: 'Docs', icon: 'i-ri-book-open-line', selected: false, pinned: true },
{ name: 'Workflow Studio', meta: 'Build', icon: 'i-ri-flow-chart', selected: false, pinned: true },
{ name: 'Agent Playground', meta: 'Lab', icon: 'i-ri-robot-2-line', selected: false, pinned: false },
{ name: 'Sales Briefing', meta: 'Team', icon: 'i-ri-presentation-line', selected: false, pinned: false },
{ name: 'Support Triage', meta: 'Queue', icon: 'i-ri-customer-service-2-line', selected: false, pinned: false },
{ name: 'Legal Review', meta: 'Beta', icon: 'i-ri-scales-3-line', selected: false, pinned: false },
{ name: 'Release Watcher', meta: 'Feed', icon: 'i-ri-rocket-line', selected: false, pinned: false },
{ name: 'Security Radar', meta: 'Risk', icon: 'i-ri-shield-check-line', selected: false, pinned: false },
{ name: 'Partner Portal', meta: 'Ext', icon: 'i-ri-handshake-line', selected: false, pinned: false },
{ name: 'QA Replays', meta: 'Debug', icon: 'i-ri-replay-line', selected: false, pinned: false },
] as const
const articleParagraphs = [
'Vernacular architecture is building done outside any academic tradition, and without professional guidance. It is not a particular architectural movement or style, but rather a broad category, encompassing a wide range and variety of building types, with differing methods of construction, from around the world, both historical and extant and classical and modern.',
'This type of architecture usually serves immediate, local needs, is constrained by the materials available in its particular region, and reflects local traditions and cultural practices. The study of vernacular architecture does not examine formally schooled architects, but instead the design skills and tradition of local builders.',
@ -292,63 +277,3 @@ export const BothAxes: Story = {
</StorySection>
),
}
export const AppSidebar: Story = {
render: () => {
const pinnedCount = appRows.filter(row => row.pinned).length
return (
<StorySection
eyebrow="Application"
title="Main navigation list"
description="A Dify-like sidebar keeps business UI outside the primitive while preserving the same Root, Viewport, Content, Scrollbar anatomy."
>
<div className="w-full max-w-70 rounded-xl bg-background-default-subtle p-3">
<div className="mb-4 flex h-8 items-center gap-2 rounded-lg bg-state-base-active px-2 text-text-accent">
<span className="i-ri-apps-fill size-4 shrink-0" aria-hidden />
<span className="min-w-0 truncate system-sm-semibold">Explore</span>
</div>
<div className="mb-1.5 flex items-center justify-between px-2">
<span className="system-xs-medium-uppercase text-text-tertiary">Web apps</span>
<span className="system-xs-medium text-text-quaternary">{appRows.length}</span>
</div>
<ScrollAreaRoot className="relative h-76 min-w-0">
<ScrollAreaViewport aria-label="Web apps" role="region" className="h-full max-h-full max-w-full rounded-lg bg-transparent">
<VerticalContent className="space-y-0.5 pr-3">
{appRows.map((row, index) => (
<div key={row.name} className="space-y-0.5">
<button
type="button"
className={cn(
'flex h-8 w-full min-w-0 items-center justify-between gap-2 rounded-lg px-2 text-left transition-colors outline-none focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-solid focus-visible:outline-state-accent-solid',
row.selected
? 'bg-state-base-active text-components-menu-item-text-active'
: 'text-components-menu-item-text hover:bg-state-base-hover hover:text-components-menu-item-text-hover',
)}
>
<span className="flex min-w-0 items-center gap-2">
<span className="flex size-5 shrink-0 items-center justify-center rounded-md bg-components-icon-bg-blue-solid text-components-avatar-shape-fill-stop-100">
<span aria-hidden className={cn(row.icon, 'size-3.5')} />
</span>
<span className="min-w-0 truncate system-sm-regular">{row.name}</span>
</span>
<span className="shrink-0 rounded-md border border-divider-subtle bg-components-panel-bg-alt px-1.5 py-0.5 system-2xs-medium-uppercase text-text-quaternary">
{row.meta}
</span>
</button>
{index === pinnedCount - 1 && index !== appRows.length - 1 && (
<div className="my-1 h-px bg-divider-subtle" />
)}
</div>
))}
</VerticalContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</div>
</StorySection>
)
},
}

View File

@ -225,7 +225,7 @@ const LoadingDemo = () => {
return (
<div className="flex flex-col items-center space-y-4">
<button
className="rounded-sm border px-2 py-1 text-xs"
className="rounded-sm border px-2 py-1 text-xs outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid"
onClick={() => setLoading(!loading)}
>
{loading ? 'Stop Loading' : 'Start Loading'}

View File

@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
import * as React from 'react'
import { toast, ToastHost } from '.'
const buttonClassName = 'rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-2 text-sm text-text-secondary shadow-xs transition-colors hover:bg-state-base-hover'
const buttonClassName = 'rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-2 text-sm text-text-secondary shadow-xs outline-hidden transition-colors hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid'
const cardClassName = 'flex min-h-[220px] flex-col gap-4 rounded-2xl border border-divider-subtle bg-components-panel-bg p-6 shadow-sm shadow-shadow-shadow-3'
const ExampleCard = ({

View File

@ -8,8 +8,8 @@ import {
TooltipTrigger,
} from '.'
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 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 outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid'
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 meta = {
title: 'Base/UI/Tooltip',
@ -117,7 +117,7 @@ const PlacementsDemo = () => {
key={value}
type="button"
onClick={() => setPlacement(value)}
className={`rounded-md border border-divider-subtle px-2 py-1 text-text-secondary ${
className={`rounded-md border border-divider-subtle px-2 py-1 text-text-secondary outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid ${
placement === value ? 'bg-state-base-hover' : 'bg-components-button-secondary-bg'
}`}
>