diff --git a/packages/dify-ui/README.md b/packages/dify-ui/README.md index 71e632fc05f..5160053904a 100644 --- a/packages/dify-ui/README.md +++ b/packages/dify-ui/README.md @@ -37,6 +37,7 @@ import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { SegmentedControl, SegmentedControlItem } from '@langgenius/dify-ui/segmented-control' import { Textarea } from '@langgenius/dify-ui/textarea' +import { Toggle } from '@langgenius/dify-ui/toggle' import '@langgenius/dify-ui/styles.css' // once, in the app root ``` @@ -64,7 +65,7 @@ Keep implementation-only render helpers, context values, styling helpers, and up | Category | Subpath | Notes | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | -| Actions | `./button`, `./icon-button` | Visible-label actions and icon-only commands. | +| Actions | `./button`, `./icon-button`, `./toggle` | Visible-label actions, icon-only commands, and persistent toggles. | | Controls | `./segmented-control` | SegmentedControl for mode, filter, and view selection. | | Display | `./collapsible`, `./kbd` | Collapsible disclosure primitive; keyboard input and shortcut keycap primitives. | | Feedback | `./meter`, `./toast` | Meter is inline status; Toast owns the `z-60` layer. | @@ -95,7 +96,7 @@ When `loading` is true, `Button` defaults `focusableWhenDisabled` to true. Loadi Use `IconButton` for a command represented by one icon and no visible text. Use `Button` when the control has a visible label, including buttons with leading or trailing icons. -Pass exactly one React element as its child. React SVG components and CSS icons are both supported without an icon-specific prop or wrapper: +Pass exactly one CSS or React SVG icon and provide either `aria-label` or `aria-labelledby`: ```tsx @@ -107,10 +108,6 @@ Every icon button must have an `aria-label` or `aria-labelledby`; a tooltip is o `IconButton` preserves Base UI Button's `render`, `nativeButton`, event, and ref composition. -Do not layer `IconButton` over compound controls that already own button behavior and a specialized recipe, such as Number Field increment/decrement, Combobox clear, or Pagination navigation. Those controls keep their Base UI owner and domain-specific styling. - -Dify-authored icon-only compound parts, such as dialog, drawer, and toast close controls, also keep their own Base UI semantic part. They reuse the private icon-button visual recipe directly instead of rendering the public `IconButton` component, so dependency direction and DOM ownership stay unchanged. - ## Segmented control contract `SegmentedControl` is Dify's design-system primitive for mode, filter, and view selection. It is built on Base UI `ToggleGroup` + `Toggle`, so use `Tabs` instead when the UI needs `tablist` / `tabpanel` semantics. diff --git a/packages/dify-ui/package.json b/packages/dify-ui/package.json index 7e9b0b01cc0..2bac3fc93a8 100644 --- a/packages/dify-ui/package.json +++ b/packages/dify-ui/package.json @@ -29,6 +29,10 @@ "types": "./src/icon-button/index.tsx", "import": "./src/icon-button/index.tsx" }, + "./toggle": { + "types": "./src/toggle/index.tsx", + "import": "./src/toggle/index.tsx" + }, "./checkbox": { "types": "./src/checkbox/index.tsx", "import": "./src/checkbox/index.tsx" diff --git a/packages/dify-ui/src/toggle/__tests__/index.spec.tsx b/packages/dify-ui/src/toggle/__tests__/index.spec.tsx new file mode 100644 index 00000000000..ee828a762d3 --- /dev/null +++ b/packages/dify-ui/src/toggle/__tests__/index.spec.tsx @@ -0,0 +1,26 @@ +import { userEvent } from 'vite-plus/test/browser' +import { render } from 'vitest-browser-react' +import { IconButton } from '../../icon-button' +import { Toggle } from '../index' + +describe('Toggle', () => { + it('composes pressed semantics into one IconButton DOM element', async () => { + const screen = await render( + + + } + />, + ) + + const toggle = screen.getByRole('button', { name: 'Favorite' }) + await expect.element(toggle).toHaveAttribute('aria-pressed', 'true') + expect(document.querySelectorAll('button')).toHaveLength(1) + + await userEvent.click(toggle) + await expect.element(toggle).toHaveAttribute('aria-pressed', 'false') + }) +}) diff --git a/packages/dify-ui/src/toggle/index.stories.tsx b/packages/dify-ui/src/toggle/index.stories.tsx new file mode 100644 index 00000000000..ab6f0e13a3c --- /dev/null +++ b/packages/dify-ui/src/toggle/index.stories.tsx @@ -0,0 +1,65 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import { Toggle } from '.' +import { Button } from '../button' +import { IconButton } from '../icon-button' + +const accentPressedClassName = + 'data-pressed:bg-state-accent-active data-pressed:text-text-accent data-pressed:hover:bg-state-accent-active-alt' +const destructivePressedClassName = + 'data-pressed:bg-state-destructive-hover data-pressed:text-text-destructive data-pressed:hover:bg-state-destructive-hover data-pressed:hover:text-text-destructive' + +const meta = { + title: 'Base/UI/Toggle', + component: Toggle, + parameters: { + layout: 'centered', + docs: { + description: { + component: + 'Thin re-export of Base UI Toggle. Compose it with Button or IconButton through render and style its data-pressed state at the owning feature.', + }, + }, + }, + tags: ['autodocs'], +} satisfies Meta + +export default meta +type Story = StoryObj + +export const IconToggle: Story = { + render: () => ( + +