mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 00:31:19 +08:00
docs(dify-ui): establish documentation hierarchy (#41044)
This commit is contained in:
parent
a5e284e6b1
commit
ca6f4e11f9
@ -27,7 +27,7 @@ Read only the packs matched by the diff:
|
||||
- Stable Dify runtime invariants in the named paths: [`references/dify-invariants.md`][dify-invariants]
|
||||
- General TypeScript or styling quality not owned above: [`references/code-quality.md`][code-quality]
|
||||
|
||||
Read `packages/dify-ui/README.md`, `packages/dify-ui/AGENTS.md`, `web/docs/overlay.md`, or `web/docs/test.md` only when the reviewed code falls under that contract. Check current official documentation when local code and bundled references do not settle a framework, browser, or accessibility behavior.
|
||||
Read `packages/dify-ui/README.md`, `packages/dify-ui/AGENTS.md`, `packages/dify-ui/docs/overlays.md`, or `web/docs/test.md` only when the reviewed code falls under that contract. Check current official documentation when local code and bundled references do not settle a framework, browser, or accessibility behavior.
|
||||
|
||||
## Severity And Output
|
||||
|
||||
|
||||
@ -1,142 +1,29 @@
|
||||
# Dify UI Rules
|
||||
# Dify UI Review Routing
|
||||
|
||||
Use these rules whenever a review touches `packages/dify-ui/` or code consuming `@langgenius/dify-ui/*`.
|
||||
Use this reference when a review touches `packages/dify-ui/` or consumes
|
||||
`@langgenius/dify-ui/*`. It routes to owner documentation; it does not redefine package
|
||||
contracts.
|
||||
|
||||
Before finalizing findings for those files, read the current local docs that apply:
|
||||
Read `packages/dify-ui/AGENTS.md` and the primitive implementation first, then only the matching
|
||||
owner:
|
||||
|
||||
- `packages/dify-ui/README.md`
|
||||
- `packages/dify-ui/AGENTS.md`
|
||||
- `web/docs/overlay.md` for floating UI
|
||||
- `packages/dify-ui/src/<primitive>/index.tsx` for the primitive being changed or consumed
|
||||
| Review area | Canonical owner |
|
||||
| ------------------------------------------ | ------------------------------------------------------------------- |
|
||||
| Package boundary and document routing | `packages/dify-ui/AGENTS.md` |
|
||||
| Imports, exports, public types, and anatomy | `packages/dify-ui/docs/authoring.md` |
|
||||
| Button or icon-only actions | `packages/dify-ui/src/button/README.md`, `packages/dify-ui/src/icon-button/README.md` |
|
||||
| Compound inputs | `packages/dify-ui/src/input-group/README.md` |
|
||||
| Forms and field semantics | `packages/dify-ui/docs/forms.md` |
|
||||
| Selection and typed values | `packages/dify-ui/docs/selection.md` |
|
||||
| Portals, layers, and floating surfaces | `packages/dify-ui/docs/overlays.md` |
|
||||
| Tailwind and radius tokens | `packages/dify-ui/docs/styling.md` |
|
||||
| Package tests and Storybook | `packages/dify-ui/docs/testing.md` |
|
||||
|
||||
## Package Boundary
|
||||
For consumer code under `web/`, also read `web/AGENTS.md` for application-owned reuse policy and
|
||||
`packages/dify-ui/README.md` for the available public subpaths.
|
||||
|
||||
Flag in `packages/dify-ui`:
|
||||
|
||||
- Imports from `web/`.
|
||||
- Dependencies on Next.js, i18n, ky, Jotai, Zustand, TanStack Query, oRPC, or business APIs.
|
||||
- Business-specific component behavior that belongs in `web/`.
|
||||
- Multiple unrelated primitives in one component folder.
|
||||
|
||||
`packages/dify-ui` is a primitive layer: Base UI headless components + `cva` + `cn` + Dify design tokens.
|
||||
|
||||
## Imports And Exports
|
||||
|
||||
Flag:
|
||||
|
||||
- Consumer imports from `@langgenius/dify-ui` without a subpath.
|
||||
- Missing `package.json#exports` entry for a new primitive.
|
||||
- Internal package imports using workspace subpaths instead of relative paths.
|
||||
- Exported props using internal-only types that consumers cannot import from the component subpath.
|
||||
- Canonical primitive boundaries or their associated public types using a redundant `Root` suffix when no higher-level convenience component exists in the same subpath.
|
||||
|
||||
Consumers use subpath exports such as `@langgenius/dify-ui/button`.
|
||||
|
||||
Canonical boundaries use the primitive name and matching public types (`Select` / `SelectProps`). Keep `Root` only to distinguish a low-level anatomy root from a higher-level convenience component (`CheckboxRoot` / `Checkbox`); implementation aliases should still show their Base UI source (`BaseSelect.Root.Props`).
|
||||
|
||||
## Props And State
|
||||
|
||||
Flag:
|
||||
|
||||
- Flattened props where related values need a discriminated union, such as `value` / `defaultValue`, `multiple` / `value`, or `clearable` / `onChange`.
|
||||
- React state used only to mirror Base UI state for class names.
|
||||
- JavaScript conditional class logic for visual states that the Dify UI/Base UI primitive already exposes through `data-*` attributes or CSS variables.
|
||||
- Controlled props added when uncontrolled DOM state or CSS variables would be enough.
|
||||
- Thin wrappers that rename Base UI parts without adding semantics.
|
||||
- Generic Base UI selection primitives wrapped without preserving their value generics, such as `Select.Root<Value, Multiple>`, `RadioGroup<Value>`, or `Radio.Root<Value>`.
|
||||
- Shared select/radio option components that type selected values as `string` while callers pass enums, unions, booleans, numbers, objects, or nullable placeholder values.
|
||||
|
||||
Prefer Base UI/Dify UI data attributes and CSS variables for visual state: `data-open`, `data-checked`, `data-disabled`, `data-highlighted`, `data-popup-open`, `group-data-*`, `peer-data-*`, `has-[:focus-visible]`, and primitive CSS variables such as anchor width or transform origin. Use JS conditional classes for product/business state that the primitive does not expose.
|
||||
|
||||
For non-string `Select` and `RadioGroup` values, prefer explicit domain generics at the root and at child value carriers. JSX children do not inherit the parent generic, so `RadioGroup<PromptMode>` should compose with `Radio<PromptMode>`, `RadioItem<PromptMode>`, or option values from a typed collection. For `Select`, prefer the Base UI `items` collection pattern for typed value-to-label rendering, and flag string coercion helpers used only to recover display labels.
|
||||
|
||||
## Forms
|
||||
|
||||
Flag:
|
||||
|
||||
- Form-like UI using unrelated `Input` and `Button` pieces without a submit boundary.
|
||||
- Text-like fields not composed through `Field`, `FieldLabel`, and `Input` or `InputGroupInput` when using Dify UI form semantics.
|
||||
- Prefixes, suffixes, or actions manually layered over `Input` instead of using the canonical `InputGroup` composition.
|
||||
- Select fields using `FieldLabel` instead of `SelectLabel`.
|
||||
- Slider fields using a generic label instead of `SliderLabel`.
|
||||
- Checkbox/radio groups missing `Fieldset` and `FieldsetLegend`.
|
||||
- Field errors or descriptions rendered without `FieldDescription` / `FieldError` relationships.
|
||||
|
||||
`Form` is the submit boundary. Dify UI form primitives are not a form state-management framework; business validation and schema-driven behavior belong in `web/`.
|
||||
|
||||
## Overlay Contract
|
||||
|
||||
Flag:
|
||||
|
||||
- Legacy web overlay imports in new or modified code.
|
||||
- Manual portals around Dify UI overlay primitives.
|
||||
- Call-site `z-*` overrides on overlays.
|
||||
- Missing root `isolation: isolate` assumptions when debugging overlay stacking.
|
||||
- Repeated backdrop, z-index, or portal chrome at call sites.
|
||||
- Tooltip used for infotips, long text, or interactive content.
|
||||
|
||||
All Dify UI body-portalled overlays use `z-50`. Toast uses `z-60`. DOM order handles stacking between overlays.
|
||||
|
||||
## Primitive Selection
|
||||
|
||||
Flag:
|
||||
|
||||
- `Tabs` used for simple mode/filter/view selection where `SegmentedControl` is the semantic primitive.
|
||||
- `SegmentedControl` used where `tablist` / `tabpanel` semantics are required.
|
||||
- `Select` used for searchable or free-form input.
|
||||
- `Combobox` used for unrestricted search text where no selected option is remembered.
|
||||
- `Autocomplete` used for closed-list selection.
|
||||
- Tooltip or PreviewCard used for content that must be reachable on touch or by screen readers.
|
||||
|
||||
Use:
|
||||
|
||||
- `Autocomplete` for free-form text with optional suggestions.
|
||||
- `Combobox` for searchable selected values from a collection.
|
||||
- `Select` for closed, scannable option sets.
|
||||
- `Popover` for infotips, help text, rich content, or interactions.
|
||||
|
||||
## Bad Usage Patterns To Flag
|
||||
|
||||
Flag:
|
||||
|
||||
- Manually recreating UI behavior or chrome already owned by `@langgenius/dify-ui/*` or `web/app/components/base/*`, such as buttons, inputs, toggle groups, popovers, dropdown menus, alert dialogs, switches, avatars, scroll areas, toasts, borders, focus states, disabled states, segmented controls, or existing feature components.
|
||||
- Styling a raw Base UI primitive directly in `web/` when a Dify UI primitive exists.
|
||||
- Wrapping a Dify UI primitive in a feature component that hides its label, error, disabled, or focus contract.
|
||||
- Replacing a semantic primitive with a generic `div` plus classes to match a screenshot.
|
||||
- Using `Tooltip` because it is visually convenient when the content is actually help text or needs touch access.
|
||||
- Adding a `z-*` override to make a child popup appear over a parent dialog.
|
||||
- Adding a new app-level wrapper around Dialog, Drawer, Popover, Select, or Combobox that repeats portal/backdrop/positioner logic.
|
||||
- Using dify-ui `Input` as a drop-in replacement for legacy inputs that include search, clear, copy, unit, localized placeholder, or number normalization behavior.
|
||||
- Building a form row from loose text and controls instead of the matching Field/Form primitives.
|
||||
- Adding component state only to style `data-open`, `data-checked`, `data-disabled`, or highlighted states that Base UI already exposes.
|
||||
- Passing booleans down only so children can toggle classes already expressible with primitive `data-*` selectors.
|
||||
|
||||
## Tokens, Radius, And Styling
|
||||
|
||||
Flag:
|
||||
|
||||
- `radius-*` class names.
|
||||
- Custom Tailwind `borderRadius` extension for Figma radius values.
|
||||
- Generic colors where semantic Dify tokens exist.
|
||||
- Hardcoded design values where Dify tokens, component variants, or documented Figma radius mappings exist.
|
||||
- `!` important modifiers used to fight primitive styles instead of fixing the variant, selector, or component composition.
|
||||
- Manual class strings that duplicate primitive variants.
|
||||
- `min-w-(--anchor-width)` on picker popups when it defeats viewport clamping.
|
||||
|
||||
Use the Figma radius mapping from `packages/dify-ui/README.md`; for example `--radius/sm` maps to `rounded-md`, and `--radius/md` maps to `rounded-lg`.
|
||||
|
||||
Use `!` only for a tightly scoped compatibility override after confirming the primitive API, data attributes, and selector structure cannot express the state.
|
||||
|
||||
## Focus Details
|
||||
|
||||
Flag focus rings attached to the wrong element. For example, Base UI `Slider.Thumb` focuses an internal `input[type=range]`, so the visible thumb wrapper needs `has-[:focus-visible]` rather than direct wrapper `focus-visible`.
|
||||
|
||||
## Custom SVG Icons
|
||||
|
||||
Flag:
|
||||
|
||||
- New generated React icon components or JSON files under `web/app/components/base/icons/src/...` for custom SVG icons.
|
||||
- Custom SVG icons consumed outside the Tailwind `i-custom-*` icon class pipeline.
|
||||
- Generated `packages/iconify-collections/custom-*/icons.json` diffs where unrelated existing icons lost or changed intrinsic `width` or `height`.
|
||||
|
||||
New custom SVG icons belong in `packages/iconify-collections/assets/...`. Regenerate with `pnpm --filter @dify/iconify-collections generate`, validate with `pnpm --filter @dify/iconify-collections check:dimensions`, and consume the generated icon with Tailwind `i-custom-*` classes.
|
||||
Treat the implementation, public types, tests, and stories as evidence for the documented
|
||||
contract. If they disagree, identify the actual owner before reporting a finding. Read current
|
||||
official Base UI documentation and installed type declarations for upstream-derived behavior.
|
||||
Report only a reproducible contract violation or observable defect, not a preference inferred from
|
||||
this routing file.
|
||||
|
||||
@ -13,4 +13,4 @@ description: Use when writing or changing Vitest or React Testing Library tests
|
||||
4. Run the focused spec before the affected suite and relevant static checks.
|
||||
5. Report the behavior verified and any remaining browser, visual, or end-to-end risk.
|
||||
|
||||
Recommend deleting low-value tests as readily as adding missing behavior coverage. Use `web/docs/test.md` for policy and Web commands; use the `packages/dify-ui/README.md` Development section for Dify UI commands.
|
||||
Recommend deleting low-value tests as readily as adding missing behavior coverage. Use `web/docs/test.md` for policy and Web commands; use `packages/dify-ui/docs/testing.md` for Dify UI commands.
|
||||
|
||||
@ -32,7 +32,7 @@ Use this skill to route component architecture decisions to its bundled referenc
|
||||
1. Identify the behavior owner and the public contract being changed.
|
||||
2. Read the nearby implementation, tests, and only the routed skill references.
|
||||
3. Implement one coherent vertical slice. Do not expand into equivalent patterns elsewhere unless the current contract cannot be completed without them.
|
||||
4. Verify observable behavior at the narrowest sufficient boundary, then run the checks documented by the owning package: `web/docs/test.md` or `web/docs/lint.md` for Web, and the `packages/dify-ui/README.md` Development section for Dify UI.
|
||||
4. Verify observable behavior at the narrowest sufficient boundary, then run the checks documented by the owning package: `web/docs/test.md` or `web/docs/lint.md` for Web, and `packages/dify-ui/docs/testing.md` for Dify UI.
|
||||
|
||||
[data]: references/data.md
|
||||
[interactions]: references/interactions.md
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Component Interactions And Overlays
|
||||
|
||||
Read this document when a change involves application hotkeys, focus, dialogs, menus, popovers, or other secondary surfaces. Overlay primitive selection and layering are owned by the [overlay guide].
|
||||
Read this document when a change involves application hotkeys, focus, dialogs, menus, popovers, or other secondary surfaces. Overlay primitive selection and layering are owned by the [overlay contract].
|
||||
|
||||
## Focus And Semantics
|
||||
|
||||
@ -20,12 +20,11 @@ Read this document when a change involves application hotkeys, focus, dialogs, m
|
||||
|
||||
## Secondary Surfaces
|
||||
|
||||
- Follow `web/docs/overlay.md` for primitive choice. Dify UI primitives are the default, with package-approved Web wrappers such as `Infotip` where the overlay guide allows them.
|
||||
- Follow the [overlay contract] for primitive choice and shared mechanics. The nearest consumer `AGENTS.md` owns application-specific composite reuse policy.
|
||||
- Separate behavior ownership from placement ownership: the action may own trigger, open state, and menu content while the caller owns slots, offsets, and alignment.
|
||||
- Keep menu and dialog surfaces as siblings when a menu command opens a dialog. Mount the dialog outside popup content.
|
||||
- Mount controlled overlays unconditionally unless unmounting is required for performance or reset semantics. Prefer keyed or owner-local reset over conditional wrappers.
|
||||
- Put query and mutation work inside dialog or alert-dialog content when it should mount only after opening.
|
||||
- Prefer uncontrolled roots when the primitive can own open state. Use controlled state only for business coordination, analytics, cleanup, or explicit reset behavior.
|
||||
- Do not add manual portals or call-site z-index escalation. Fix ownership and stacking structure at the shared boundary.
|
||||
|
||||
[overlay guide]: ../../../../web/docs/overlay.md
|
||||
[overlay contract]: ../../../../packages/dify-ui/docs/overlays.md
|
||||
|
||||
@ -1,35 +1,41 @@
|
||||
# @langgenius/dify-ui
|
||||
|
||||
This package owns shared design tokens, CSS-first Tailwind styles, the `cn()` utility, and headless primitives consumed by `web/`. Read only the matching topic in [`README.md`] for public imports, forms, typed values, pickers, overlays, tokens, or tests.
|
||||
This file owns the package boundary and routes detailed contracts. Start from the [package index],
|
||||
then read only the guide for the contract being changed.
|
||||
|
||||
## Component Authoring Rules
|
||||
## Package boundary
|
||||
|
||||
- Build primitives from `@base-ui/react`, `cva`, and `cn`.
|
||||
- Use relative cross-component imports inside the package and subpath exports such as `@langgenius/dify-ui/button` from consumers. Add a matching `package.json#exports` entry for each public primitive.
|
||||
- Keep one primitive per `src/<name>/` folder with optional colocated stories and tests.
|
||||
- Do not import from `web/` or depend on Next.js, i18n, application state, or data-fetching libraries.
|
||||
- Preserve upstream Base UI anatomy and generic value contracts. Use discriminated unions when one prop changes the valid shape of related props; do not flatten those relationships or hard-code selectable values to `string`.
|
||||
- Export shared public types from the owning component subpath.
|
||||
- Prefer Base UI data attributes and CSS variables for visual states; do not mirror primitive state in React solely to add classes.
|
||||
- When a Base UI API or selector contract is unclear, read the current official documentation and local `@base-ui/react` type declarations before coding.
|
||||
- Keep this an independent primitive package. Do not import from application packages or depend on
|
||||
routing, i18n, application state, schemas, data fetching, or business APIs.
|
||||
- Prefer `@base-ui/react` when it owns the required headless behavior. Style primitives with `cva`,
|
||||
`cn`, and Dify design tokens. Keep one primitive per `src/<name>/` folder with optional colocated
|
||||
stories and tests.
|
||||
- Prefer Base UI data attributes and CSS variables for visual states. Do not mirror primitive state
|
||||
in React solely to add classes.
|
||||
- When an upstream API or selector contract is unclear, read the current official Base UI
|
||||
documentation and installed `@base-ui/react` declarations before coding.
|
||||
|
||||
Use the README sections as the detailed owners:
|
||||
## Contract owners
|
||||
|
||||
- [Button and icon-button contracts]
|
||||
- [Form and input composition]
|
||||
- [Imports and public boundaries]
|
||||
- [Typed value contracts]
|
||||
- [Search and picker selection]
|
||||
- [Tailwind and Figma radius mapping]
|
||||
- [Overlay and portal contracts]
|
||||
- [Development and test boundaries]
|
||||
- Imports, exports, naming, public types, generics, and anatomy: [Public API authoring]
|
||||
- Button and icon-only action behavior: [Button contract] and [Icon Button contract]
|
||||
- Compound input behavior: [Input Group contract]
|
||||
- Form structure and labels: [Forms]
|
||||
- Picker choice and typed values: [Selection]
|
||||
- Portals, layering, and floating-surface semantics: [Overlays]
|
||||
- Tailwind integration and radius mapping: [Styling]
|
||||
- Package test ownership and setup: [Testing and development]
|
||||
|
||||
[Button and icon-button contracts]: README.md#button-loading-and-disabled-contract
|
||||
[Development and test boundaries]: README.md#development
|
||||
[Form and input composition]: README.md#form-contract
|
||||
[Imports and public boundaries]: README.md#imports
|
||||
[Overlay and portal contracts]: README.md#overlay--portal-contract
|
||||
[Search and picker selection]: README.md#search-and-picker-selection
|
||||
[Tailwind and Figma radius mapping]: README.md#tailwind-css-v4-integration
|
||||
[Typed value contracts]: README.md#typed-value-contracts
|
||||
[`README.md`]: README.md
|
||||
A component needs a local README only when it owns a substantial Dify-specific contract that its
|
||||
types, stories, and upstream documentation do not express. Do not create one for completeness.
|
||||
|
||||
[Button contract]: src/button/README.md
|
||||
[Forms]: docs/forms.md
|
||||
[Icon Button contract]: src/icon-button/README.md
|
||||
[Input Group contract]: src/input-group/README.md
|
||||
[Overlays]: docs/overlays.md
|
||||
[Public API authoring]: docs/authoring.md
|
||||
[Selection]: docs/selection.md
|
||||
[Styling]: docs/styling.md
|
||||
[Testing and development]: docs/testing.md
|
||||
[package index]: README.md
|
||||
|
||||
@ -1,17 +1,15 @@
|
||||
# @langgenius/dify-ui
|
||||
|
||||
Shared UI primitives, design tokens, CSS-first Tailwind styles, and the `cn()` utility consumed by Dify's `web/` app.
|
||||
Independent UI primitives, design tokens, CSS-first Tailwind styles, and the `cn()` utility for
|
||||
Dify products.
|
||||
|
||||
The primitives are thin, opinionated wrappers around [Base UI] headless components, styled with `cva` + `cn` and Dify design tokens.
|
||||
For upstream component docs, start from the [Base UI docs index].
|
||||
Most interactive primitives are thin, opinionated wrappers around [Base UI] headless components.
|
||||
Dify-authored primitives use semantic HTML, `cva`, `cn`, and Dify design tokens. The package is
|
||||
private to the workspace, but its public subpaths are treated as stable package boundaries.
|
||||
|
||||
> `private: true` — this package is consumed by `web/` via the pnpm workspace and is not published to npm. Treat the API as internal to Dify, but stable within the workspace.
|
||||
## Usage
|
||||
|
||||
## Installation
|
||||
|
||||
Already wired as a workspace dependency in `web/package.json`. Nothing to install.
|
||||
|
||||
For a new workspace consumer, add:
|
||||
Add the workspace dependency:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@ -21,317 +19,78 @@ For a new workspace consumer, add:
|
||||
}
|
||||
```
|
||||
|
||||
## Imports
|
||||
Import from a public subpath. The package intentionally has no root barrel:
|
||||
|
||||
Always import from a **subpath export** — there is no barrel:
|
||||
|
||||
```ts
|
||||
```tsx
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogContent, DialogTrigger } from '@langgenius/dify-ui/dialog'
|
||||
import { Drawer, DrawerPopup, DrawerTrigger } from '@langgenius/dify-ui/drawer'
|
||||
import { Field, FieldLabel } from '@langgenius/dify-ui/field'
|
||||
import { Form } from '@langgenius/dify-ui/form'
|
||||
import { IconButton } from '@langgenius/dify-ui/icon-button'
|
||||
import { InputGroup, InputGroupAddon, InputGroupInput } from '@langgenius/dify-ui/input-group'
|
||||
import { Input } from '@langgenius/dify-ui/input'
|
||||
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
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import '@langgenius/dify-ui/styles.css'
|
||||
```
|
||||
|
||||
Importing from `@langgenius/dify-ui` (no subpath) is intentionally not supported — it keeps tree-shaking trivial and makes Storybook / test coverage attribution per-primitive.
|
||||
|
||||
The canonical boundary exported from a primitive subpath uses the primitive name without a `Root` suffix, and its public types follow the same name (`Select` / `SelectProps`, `Drawer` / `DrawerProps`). Keep `Root` only when the subpath exposes both a low-level anatomy root and a higher-level convenience component, such as `CheckboxRoot` / `Checkbox` or `PaginationRoot` / `Pagination`. Implementation code should continue to reference the upstream Base UI anatomy explicitly through names such as `BaseSelect.Root.Props`.
|
||||
|
||||
### Public type contracts
|
||||
|
||||
Every runtime component exported from a primitive subpath must have an accurate, importable props type with the matching name (`DialogContent` / `DialogContentProps`). Define Dify-authored composite props at the Dify UI boundary; use direct aliases for unchanged Base UI parts instead of copying their shapes.
|
||||
|
||||
Treat each `src/<primitive>/index.tsx` as an explicit public API boundary. Keep component, factory, hook, and type declarations module-local, then publish the complete surface through separate `export { ... }` and `export type { ... }` manifests at the bottom of the file. Do not mix scattered inline exports with the manifest or use a wildcard export; omission from the manifest keeps implementation helpers private.
|
||||
|
||||
Preserve generic relationships end to end. Generic public components and their props must carry the same caller-owned type parameters, including picker `Value` / `Multiple`, form values, radio values, slider values, and overlay payloads and handles. Never erase those relationships with `any` or a hard-coded `string`; use `unknown` only as the safe default for an independently consumed anatomy part whose value cannot be inferred from its parent through JSX.
|
||||
|
||||
Do not add a root-only generic when independently rendered JSX anatomy can produce values outside that root's inferred type. Preserve the upstream contract until the complete component family can enforce one value type; otherwise the generic gives callbacks a narrower type than the runtime can guarantee. `Tabs` intentionally follows Base UI's value contract for this reason.
|
||||
|
||||
Keep the public type surface smaller than the upstream Base UI namespace. A type is not public merely because Base UI provides a name for it, because Dify gives an internal contract a descriptive alias, or because an earlier implementation happened to export it. In addition to matching component props, export a type only when it pairs with a public factory or has a concrete workspace consumer that cannot express the contract clearly through the matching props type. Remove legacy aliases that have no matching runtime API or real consumer; package-local tests and stories can derive narrow values from canonical props instead of preserving an otherwise unused export. Dify-authored options, states, and controlled/uncontrolled branches stay private when the matching props type already expresses them.
|
||||
|
||||
State, event details and reasons, actions, and controlled/uncontrolled composition helpers are private by default. Public props already provide contextual typing for inline render and event callbacks. Export one of these narrower contracts only when Dify defines the state or event itself, a public factory requires the named type, or an external consumer needs to name it independently. Apply the same rule to Dify-authored components such as FileTree, Pagination, ProgressCircle, StatusDot, and Toast; being implemented locally does not justify a broader API.
|
||||
|
||||
Keep implementation-only render helpers, context values, styling helpers, and upstream passthrough aliases private. When a wrapper consumes `className` through `cn()`, omit the upstream state-callback form and expose `className?: string`; public types must describe behavior the wrapper actually implements.
|
||||
Import `styles.css` once from the consumer's root stylesheet or entrypoint.
|
||||
|
||||
## Primitives
|
||||
|
||||
| Category | Subpath | Notes |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
|
||||
| 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`, `./progress`, `./status-dot`, `./toast` | Inline and asynchronous status primitives; Toast owns the `z-60` layer. |
|
||||
| Form | `./form`, `./field`, `./fieldset`, `./input`, `./input-group`, `./textarea`, `./checkbox`, `./checkbox-group`, `./radio`, `./number-field`, `./select`, `./slider`, `./switch` | Native form boundary, field semantics, and controls. |
|
||||
| Layout | `./scroll-area` | Custom-styled scrollbar over the host viewport. |
|
||||
| Media | `./avatar` | Avatar root, image, and fallback primitives. |
|
||||
| Navigation | `./file-tree`, `./pagination`, `./tabs` | FileTree for preview-oriented file disclosure lists; Pagination for page navigation; Tabs for panels. |
|
||||
| Overlay / menu | `./alert-dialog`, `./context-menu`, `./dialog`, `./drawer`, `./dropdown-menu`, `./popover`, `./preview-card`, `./tooltip` | Portalled. See [Overlay & portal contract] below. |
|
||||
| Search / pickers | `./autocomplete`, `./combobox`, `./select` | Search input, searchable picker, and closed picker. |
|
||||
| Category | Public subpaths |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Actions | `./button`, `./icon-button`, `./toggle` |
|
||||
| Controls | `./segmented-control` |
|
||||
| Display | `./collapsible`, `./kbd` |
|
||||
| Feedback | `./meter`, `./progress`, `./status-dot`, `./toast` |
|
||||
| Form | `./form`, `./field`, `./fieldset`, `./input`, `./input-group`, `./textarea`, `./checkbox`, `./checkbox-group`, `./radio`, `./number-field`, `./select`, `./slider`, `./switch` |
|
||||
| Layout | `./scroll-area` |
|
||||
| Media | `./avatar` |
|
||||
| Navigation | `./file-tree`, `./pagination`, `./tabs` |
|
||||
| Overlay and menu | `./alert-dialog`, `./context-menu`, `./dialog`, `./drawer`, `./dropdown-menu`, `./popover`, `./preview-card`, `./tooltip` |
|
||||
| Search and pick | `./autocomplete`, `./combobox`, `./select` |
|
||||
|
||||
Utilities:
|
||||
|
||||
- `./cn` — `clsx` + `tailwind-merge` wrapper. Use this for conditional class composition.
|
||||
- `./styles.css` — the one CSS entry that ships the design tokens, theme variables, and project utilities/components. Import it once from the app root.
|
||||
- `./cn` composes conditional classes with `clsx` and `tailwind-merge`.
|
||||
- `./styles.css` provides design tokens, theme variables, and shared utilities.
|
||||
|
||||
## Button loading and disabled contract
|
||||
## Guides
|
||||
|
||||
`Button` owns the spacing between direct children. Regular (`medium`) and Large sizes use
|
||||
4px and 6px gaps. Small uses 3px for Primary and 4px for the other variants. Do not add icon
|
||||
margins or a standard `gap-*` at call sites; use a Button `className` override only for a
|
||||
documented layout exception.
|
||||
Start here, then open only the guide for the contract being changed. Component-specific Dify
|
||||
behavior lives beside the component. Contracts shared by several primitives live in `docs/`.
|
||||
Upstream behavior remains owned by the [Base UI documentation].
|
||||
|
||||
`Button` keeps normal `disabled` controls native-disabled by default so unavailable actions are removed from the keyboard focus order.
|
||||
### Component guides
|
||||
|
||||
When `loading` is true, `Button` defaults `focusableWhenDisabled` to true. Loading represents an action that has already been triggered and is temporarily pending, so the button remains focusable while Base UI still suppresses click, pointer, keyboard activation, and submit-button activation. Pass `focusableWhenDisabled={false}` only when a loading button should use native disabled behavior.
|
||||
| Guide | Dify-owned contract |
|
||||
| ------------- | ------------------------------------------------------------------------------------------- |
|
||||
| [Button] | Action semantics, submit and link choices, loading versus disabled, and content spacing. |
|
||||
| [Icon Button] | Accessible names, decorative glyphs, appearance ownership, and primitive composition. |
|
||||
| [Input Group] | Compound input anatomy, shared-surface ownership, DOM order, focus, and interactive addons. |
|
||||
|
||||
## Icon button contract
|
||||
### Cross-component guides
|
||||
|
||||
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.
|
||||
| Guide | Scope |
|
||||
| ------------------------- | ------------------------------------------------------------------------ |
|
||||
| [Forms] | Native submit boundaries, fields, labels, grouped controls, and errors. |
|
||||
| [Selection] | Typed values and choosing among segmented controls, pickers, and radios. |
|
||||
| [Overlays] | Portals, root isolation, layering, trigger composition, and semantics. |
|
||||
| [Styling] | Tailwind CSS integration and the Figma radius mapping. |
|
||||
| [Public API authoring] | Subpath exports, naming, public types, generics, and private helpers. |
|
||||
| [Testing and development] | Package commands, test ownership, accessibility, and animation setup. |
|
||||
|
||||
Pass exactly one React element containing the decorative glyph and provide either `aria-label` or `aria-labelledby`. Mark the glyph or its decorative wrapper `aria-hidden="true"`; `IconButton` does not add that attribute to its child:
|
||||
## Contributing
|
||||
|
||||
```tsx
|
||||
<IconButton aria-label="Close">
|
||||
<span aria-hidden="true" className="i-ri-close-line size-4" />
|
||||
</IconButton>
|
||||
```
|
||||
Read [component authoring rules] before modifying the package, then open only the matching owner
|
||||
guide. This index intentionally does not duplicate those contracts.
|
||||
|
||||
Every icon button must have an `aria-label` or `aria-labelledby`; a tooltip is only a visual enhancement. The child chooses the glyph and its optical size. Omit `variant` for the IconButton-specific neutral appearance; the other appearance names align with `Button`. Use `tone="destructive"` for destructive intent.
|
||||
|
||||
Size, radius, colors, hover, disabled, and focus-visible styles belong to `IconButton`. Use `className` for external layout or selectors driven by the composed primitive or business-state owner; do not recreate an existing appearance variant.
|
||||
|
||||
When Toggle, Menu, Popover, Tooltip, or Collapsible owns the interaction state, keep that primitive outside and use its `render` prop to render `IconButton` as the final button. `IconButton` preserves Base UI Button's `render`, `nativeButton`, event, and ref composition.
|
||||
|
||||
## Segmented control contract
|
||||
|
||||
`SegmentedControl` is Dify's required single-choice primitive for mode, filter, and view selection. It is built on Base UI `RadioGroup` + `Radio`, so `value`, `defaultValue`, and `onValueChange` use the caller's scalar domain value. Provide either `value` or `defaultValue`; an active item cannot be toggled off. Use `Tabs` instead when the UI needs `tablist` / `tabpanel` semantics.
|
||||
|
||||
Keyboard interaction follows the radio-group model: `Tab` enters on the selected item, and an arrow key moves focus and immediately selects the next enabled item.
|
||||
|
||||
## Form contract
|
||||
|
||||
Dify UI's form primitives are a Base UI composition layer for native form semantics, field accessibility, and design-system styling. They are intentionally not a form state-management framework. See the upstream [Base UI forms handbook], [Base UI Form], [Base UI Field], and [Base UI Fieldset] docs for the underlying component contracts.
|
||||
|
||||
Use `Form` for the submit boundary. It renders a native `<form>`, preserves Enter-to-submit and submit-button behavior, and adds Base UI's `onFormSubmit`, `errors`, `actionsRef`, and `validationMode` APIs for structured values and consolidated field validation. Prefer it over a bare `<form>` when the form is composed with Dify UI fields.
|
||||
|
||||
Use `Field` when a text control needs Base UI field semantics such as shared name, label, validation, description, or error state. A standalone `Input` may instead use a native `<label htmlFor>` relationship or an accessible name on the input itself. Prefer a visible label for normal form rows. `FieldDescription` and `FieldError` provide the message relationships that screen readers need, while the Dify wrapper adds the default Form Input Set styling from the design system.
|
||||
|
||||
`Input` is the standalone Dify UI text-input control. When `Field` is present, it owns the shared name, label, validation, and messages. For a prefix, suffix, or action, use the [Input Group composition contract].
|
||||
|
||||
Choose the label primitive by the control semantics. Text-like inputs, `Textarea`, input-based `Combobox` / `Autocomplete`, single `Checkbox` / `Radio`, `Switch`, and `NumberField` use `FieldLabel`. Trigger-based `Select` fields use `SelectLabel`; `Slider` fields use `SliderLabel`, with per-thumb `aria-label` only when the thumbs need distinct names. `SelectGroupLabel` and `AutocompleteGroupLabel` only label grouped options inside their popup content; they are not field labels.
|
||||
|
||||
Use `Fieldset` and `FieldsetLegend` when one field is represented by a group of related controls, such as checkbox groups, radio groups, multi-thumb sliders, or a section that combines several inputs. For checkbox and radio groups, wrap each option with `FieldItem` and give each option its own label:
|
||||
|
||||
```tsx
|
||||
<Field name="allowedNetworkProtocols">
|
||||
<Fieldset render={<CheckboxGroup />}>
|
||||
<FieldsetLegend>Allowed network protocols</FieldsetLegend>
|
||||
<FieldItem>
|
||||
<FieldLabel className="flex items-center gap-2">
|
||||
<Checkbox value="https" />
|
||||
HTTPS
|
||||
</FieldLabel>
|
||||
</FieldItem>
|
||||
</Fieldset>
|
||||
</Field>
|
||||
```
|
||||
|
||||
`Fieldset` provides the group semantics and legend relationship. It does not own the interactive state of the grouped control. Pass `disabled`, `value`, `defaultValue`, and change handlers to the actual group primitive (`CheckboxGroup`, radio group, slider root, etc.) instead of relying on the fieldset wrapper to manage them.
|
||||
|
||||
## Typed value contracts
|
||||
|
||||
Selection primitives should preserve the caller's domain value type instead of widening values to `string`. Use `Select<Value, Multiple>`, `RadioGroup<Value>`, `Radio<Value>`, and `RadioItem<Value>` when the selected value is an enum, union, boolean, number, object, or nullable placeholder value.
|
||||
|
||||
Root-level generics type `value`, `defaultValue`, and value-dependent callbacks such as `onValueChange`. Upstream collection props such as `items` can still be broader than the root value type, and JSX children do not automatically inherit the parent generic. Type independently consumed anatomy parts when their value cannot be inferred locally; for non-string radio groups, type the child item too:
|
||||
|
||||
```tsx
|
||||
<RadioGroup<PromptMode> value={promptMode} onValueChange={setPromptMode}>
|
||||
<Radio<PromptMode> value={PROMPT_MODE.default} />
|
||||
<RadioItem<PromptMode> value={PROMPT_MODE.custom}>
|
||||
<RadioControl />
|
||||
Custom prompt
|
||||
</RadioItem>
|
||||
</RadioGroup>
|
||||
```
|
||||
|
||||
Use `Radio` for the default Dify control. Use `RadioItem` when custom UI should be the radio item; place `RadioControl` inside it for the standard visual dot. `RadioControl` is a Dify visual part, not a Base UI anatomy export.
|
||||
|
||||
For `Select` and `Combobox`, a literal multiple-value contract must match the runtime mode: `<Combobox<Subject, true> multiple>`. Their value display can still receive `null` before a value is selected, including in multiple mode. Because JSX does not pass the root's generic to its children, repeat the domain type on independently consumed render anatomy instead of annotating callback parameters:
|
||||
|
||||
```tsx
|
||||
<Combobox<Subject, true> multiple value={subjects} onValueChange={setSubjects}>
|
||||
<ComboboxValue<Subject, true>>
|
||||
{(selectedSubjects) => selectedSubjects?.map((subject) => subject.name).join(', ') ?? 'Anyone'}
|
||||
</ComboboxValue>
|
||||
<ComboboxList<Subject>>
|
||||
{(subject) => <ComboboxItem value={subject}>{subject.name}</ComboboxItem>}
|
||||
</ComboboxList>
|
||||
</Combobox>
|
||||
```
|
||||
|
||||
`AutocompleteList` follows the same rule. A `ComboboxGroup` or `AutocompleteGroup` can infer its local value type from `items`; a nested `Collection` is a separate JSX boundary and should use `<Value>` when its render callback needs the domain type. Dynamic `multiple={condition}` remains supported and gives callbacks the corresponding single-or-multiple union.
|
||||
|
||||
For select labels and display values, prefer the Base UI `items` collection pattern so the root, value display, and item list share one runtime source of truth for values and labels. Avoid helpers that stringify values only to recover labels later; convert values to strings only at real boundaries such as form submission, URL/search params, or legacy APIs that require strings.
|
||||
|
||||
`CheckboxGroup` follows the Base UI contract and uses `string[]`. Do not add a generic checkbox-group wrapper unless the underlying primitive contract changes; if different business IDs need stronger separation, model that at the feature/domain type boundary.
|
||||
|
||||
For complex business forms, keep state ownership outside these primitives. TanStack Form, zod, server validation, dialog reset behavior, and schema-driven rendering belong to the feature layer in `web/`; they should pass `name`, `invalid`, `dirty`, `touched`, `value`, `onValueChange`, and errors into these primitives rather than replacing the field semantics. In this repo, `web/app/components/base/form` is the TanStack/schema runtime adapter; `packages/dify-ui` remains the primitive layer.
|
||||
|
||||
Migration rule for `web/`: if a UI has a save/submit action, do not leave it as unrelated `Input` and `Button` pieces. Give it a real submit boundary with `Form` or a native `<form>`, attach visible field names through the appropriate label primitive (`FieldLabel`, `SelectLabel`, `SliderLabel`, or `FieldsetLegend`), expose helper/error text through `FieldDescription` / `FieldError`, and keep non-submit buttons as `type="button"`.
|
||||
|
||||
## Search and picker selection
|
||||
|
||||
Choose the primitive by its value contract:
|
||||
|
||||
- `Autocomplete` accepts free-form text with optional suggestions or completions.
|
||||
- `Combobox` selects and remembers one or more values from a searchable collection.
|
||||
- `Select` chooses from a closed, scannable list without text entry.
|
||||
|
||||
Keep Base UI anatomy visible in public APIs instead of wrapping a picker into one business component. Multiple-selection comboboxes follow the official chips composition: chips and input share the input group, chips wrap, and the group grows vertically. Autocomplete and Combobox popups own their portals, use the package overlay layer, and size from `--anchor-width` with viewport-aware maximum width; do not force a minimum width that defeats viewport clamping.
|
||||
|
||||
## Tailwind CSS v4 integration
|
||||
|
||||
This package uses Tailwind CSS v4's CSS-first configuration model. Consumers should import Tailwind from their own root stylesheet, then import this package's CSS entry:
|
||||
|
||||
```css
|
||||
@import 'tailwindcss';
|
||||
@import '@langgenius/dify-ui/styles.css';
|
||||
```
|
||||
|
||||
If a consumer uses Dify UI source files through the workspace, add an explicit source so Tailwind can detect utility classes:
|
||||
|
||||
```css
|
||||
@source '../packages/dify-ui/src';
|
||||
```
|
||||
|
||||
Figma radius tokens are offset by one step from Tailwind CSS v4 defaults. Use this mapping rather than adding custom theme values or `radius-*` utilities:
|
||||
|
||||
| Figma token | Tailwind class |
|
||||
| --------------- | ---------------- |
|
||||
| `--radius/2xs` | `rounded-xs` |
|
||||
| `--radius/xs` | `rounded-sm` |
|
||||
| `--radius/sm` | `rounded-md` |
|
||||
| `--radius/md` | `rounded-lg` |
|
||||
| `--radius/lg` | `rounded-[10px]` |
|
||||
| `--radius/xl` | `rounded-xl` |
|
||||
| `--radius/2xl` | `rounded-2xl` |
|
||||
| `--radius/3xl` | `rounded-[20px]` |
|
||||
| `--radius/6xl` | `rounded-[28px]` |
|
||||
| `--radius/full` | `rounded-full` |
|
||||
|
||||
Convert Figma output such as `rounded-[var(--radius/sm, 6px)]` to the mapped Tailwind class. Use an arbitrary value only when no standard class matches.
|
||||
|
||||
## Overlay & portal contract
|
||||
|
||||
Overlay primitives render their floating surfaces inside a [Base UI Portal] attached to `document.body`. This is the Base UI default — see the upstream [Portals][Base UI Portal] docs for the underlying behavior. Convenience content components such as `DialogContent`, `PopoverContent`, and `SelectContent` own their portal internally. Primitives with explicit anatomy expose their constituent parts so consumers can compose the Base UI structure and put behavior on its owner.
|
||||
|
||||
### Root isolation requirement
|
||||
|
||||
The host app **must** establish an isolated stacking context at its root so the portalled overlay layer is not clipped or re-ordered by ancestor `transform` / `filter` / `contain` styles. In the Dify web app this is done in `web/app/layout.tsx`:
|
||||
|
||||
```tsx
|
||||
<body>
|
||||
<div className="isolate h-full">{children}</div>
|
||||
</body>
|
||||
```
|
||||
|
||||
Equivalent: any root element with `isolation: isolate` in CSS. Without it, overlays can be visually clipped on Safari when a descendant creates a new stacking context.
|
||||
|
||||
### z-index layering
|
||||
|
||||
Every overlay primitive uses a single, shared z-index. Do **not** override it at call sites.
|
||||
|
||||
| Layer | z-index | Where |
|
||||
| -------------------------------------------------------------------------------------------------------------------------------- | ------- | -------------------------------------------------------------------------- |
|
||||
| Overlays (Dialog, AlertDialog, Autocomplete, Combobox, Drawer, Popover, PreviewCard, DropdownMenu, ContextMenu, Select, Tooltip) | `z-50` | Positioner / Backdrop |
|
||||
| Toast viewport | `z-60` | One layer above overlays so notifications are never hidden under a dialog. |
|
||||
|
||||
Rationale: Dify UI owns the normal application overlay layer. Overlay primitives share `z-50` and **rely on DOM order** for stacking — the portal mounted later wins. Toast owns `z-60` so notifications remain visible above dialogs, popovers, and other portalled surfaces without falling back to `z-9999`.
|
||||
|
||||
See the [web overlay guide] for the web app overlay best practices.
|
||||
|
||||
### Rules
|
||||
|
||||
- Never add ad hoc `z-*` overrides on primitives from this package. If something is getting clipped, fix the parent overlay structure instead of raising the child primitive.
|
||||
- Never create an extra manual portal on top of our primitives. Use the exported content or portal parts such as `DialogContent`, `PopoverContent`, and `DrawerPortal`, and preserve each primitive's own focus, modal, and dismissal contract.
|
||||
- When a primitive needs additional presentation chrome (e.g. a custom backdrop), add it **inside** the exported component, not at call sites.
|
||||
|
||||
### Tooltip, preview card, infotip, and popover semantics
|
||||
|
||||
- Use `Tooltip` only for short, non-interactive visual labels. The trigger must already have visible text or an `aria-label`; the tooltip is not the accessible name and must not contain links, buttons, forms, or structured prose.
|
||||
- Use `PreviewCard` as a visual enhancement for a link that previews its destination. Its popup must remain non-interactive and must not contain unique or essential information unless that information is also available at the linked destination. Use `Popover` when opening the popup is the trigger's purpose or when users need to access its content on touch or with assistive technology.
|
||||
- Use `Popover` for explanatory content, long text, rich layout, or anything users may need to reach on touch or with assistive technology. In `web/`, the `Infotip` wrapper is the preferred pattern for a `?` help glyph backed by `Popover`.
|
||||
- Pick a `placement` and let the primitive own spacing. Avoid per-call-site offsets unless the component API explicitly needs a measured layout exception.
|
||||
- When passing a Base UI trigger `render` prop, render a real `<button type="button">` for button-like triggers. If a Popover trigger must render a `div`, `span`, or another non-button element, pass `nativeButton={false}`.
|
||||
|
||||
## Development
|
||||
|
||||
Run `vp check packages/dify-ui` from the repository root for package formatting, lint, and repository-wide TypeScript diagnostics. Run the remaining commands from `packages/dify-ui/`:
|
||||
|
||||
- `vp test --project unit` — Vitest unit tests for primitives.
|
||||
- `vp run storybook` — Storybook on the default port. Each primitive has `index.stories.tsx`.
|
||||
- `vp test --project storybook --run` — Storybook component tests in Vitest browser mode. Stories without `play` are render and a11y smoke tests; stories with `play` should cover public UI contracts such as opening overlays, keyboard navigation, disabled/loading guards, form submission, and controlled state updates.
|
||||
|
||||
Both test projects run in Playwright Chromium Browser Mode; choose `unit` or `storybook` by behavior owner, not runtime. Bare `vp test` runs both projects.
|
||||
|
||||
### Test Boundary
|
||||
|
||||
Use Storybook tests for behavior that belongs to the documented component example:
|
||||
visible state changes, user interaction, keyboard paths, overlay open/close flows,
|
||||
and accessibility-facing semantics. Keep regular Vitest unit tests for lower-level
|
||||
wrapper contracts such as class variants, Base UI passthrough props, hidden input
|
||||
serialization, data attribute hooks, store behavior, and edge cases that do not
|
||||
need a full story.
|
||||
|
||||
Storybook accessibility testing uses `a11y.test = 'error'` for enabled rules.
|
||||
Color contrast is a known design-token gap and is currently excluded globally;
|
||||
do not add another global exclusion. Keep other temporary exceptions local to
|
||||
the affected story and do not use a `play` test in place of an accessibility fix.
|
||||
|
||||
### Disabling Animations In Tests
|
||||
|
||||
Base UI can wait for `element.getAnimations()` to finish before it unmounts overlays, panels, and transition-driven components. Browser-based test runners can make that timing unstable, especially when tests assert final DOM state rather than animation behavior.
|
||||
|
||||
Set the Base UI test flag in a Vitest setup file to skip those waits:
|
||||
|
||||
```ts
|
||||
;(
|
||||
globalThis as typeof globalThis & {
|
||||
BASE_UI_ANIMATIONS_DISABLED: boolean
|
||||
}
|
||||
).BASE_UI_ANIMATIONS_DISABLED = true
|
||||
```
|
||||
|
||||
`packages/dify-ui/vitest.setup.ts` already applies this for primitive tests.
|
||||
The Storybook project intentionally uses its preview setup instead; do not disable animation lifecycles globally there.
|
||||
|
||||
See [component authoring rules] for:
|
||||
|
||||
- Component authoring rules (one-component-per-folder, `cva` + `cn`, relative imports inside the package, subpath imports from consumers).
|
||||
|
||||
## Not part of this package
|
||||
|
||||
- Application state (`jotai`, `zustand`), data fetching (`ky`, `@tanstack/react-query`, `@orpc/*`), i18n (`next-i18next` / `react-i18next`), and routing (`next`) all live in `web/`. This package has zero dependencies on them and must stay that way so it can eventually be consumed by other apps or extracted.
|
||||
- Business components (chat, workflow, dataset views, etc.). Those belong in `web/app/components/...`.
|
||||
|
||||
[Base UI Field]: https://base-ui.com/react/components/field
|
||||
[Base UI Fieldset]: https://base-ui.com/react/components/fieldset
|
||||
[Base UI Form]: https://base-ui.com/react/components/form
|
||||
[Base UI Portal]: https://base-ui.com/react/overview/quick-start#portals
|
||||
[Base UI docs index]: https://base-ui.com/llms.txt
|
||||
[Base UI forms handbook]: https://base-ui.com/react/handbook/forms
|
||||
[Base UI documentation]: https://base-ui.com/llms.txt
|
||||
[Base UI]: https://base-ui.com/react
|
||||
[Input Group composition contract]: ./src/input-group/README.md
|
||||
[Overlay & portal contract]: #overlay--portal-contract
|
||||
[Button]: ./src/button/README.md
|
||||
[Forms]: ./docs/forms.md
|
||||
[Icon Button]: ./src/icon-button/README.md
|
||||
[Input Group]: ./src/input-group/README.md
|
||||
[Overlays]: ./docs/overlays.md
|
||||
[Public API authoring]: ./docs/authoring.md
|
||||
[Selection]: ./docs/selection.md
|
||||
[Styling]: ./docs/styling.md
|
||||
[Testing and development]: ./docs/testing.md
|
||||
[component authoring rules]: ./AGENTS.md
|
||||
[web overlay guide]: ../../web/docs/overlay.md
|
||||
|
||||
59
packages/dify-ui/docs/authoring.md
Normal file
59
packages/dify-ui/docs/authoring.md
Normal file
@ -0,0 +1,59 @@
|
||||
# Public API Authoring
|
||||
|
||||
Each `src/<primitive>/index.tsx` is an explicit public API boundary. Keep implementation details
|
||||
module-local and publish the complete surface through separate `export { ... }` and
|
||||
`export type { ... }` manifests at the bottom of the file. Do not mix scattered inline exports
|
||||
with the manifest or use wildcard exports.
|
||||
|
||||
## Subpaths and names
|
||||
|
||||
Every public primitive needs a matching `package.json#exports` subpath. Import relatively between
|
||||
package components; consumers import only through public subpaths.
|
||||
|
||||
Use the primitive name without a `Root` suffix for the canonical boundary and matching props type:
|
||||
`Select` and `SelectProps`, `Drawer` and `DrawerProps`. Keep `Root` only when the same subpath
|
||||
exports both low-level anatomy and a higher-level convenience component, such as `CheckboxRoot`
|
||||
and `Checkbox`.
|
||||
|
||||
Every runtime component must have an accurate, importable props type with the matching name. Use a
|
||||
direct alias for an unchanged Base UI part. Define Dify-authored composite props at the Dify UI
|
||||
boundary instead of copying upstream shapes.
|
||||
|
||||
Use a discriminated union when one prop changes the valid shape of related props, such as
|
||||
controlled versus uncontrolled state or single versus multiple selection.
|
||||
|
||||
## Generic contracts
|
||||
|
||||
Preserve generic relationships end to end, including picker `Value` and `Multiple`, form values,
|
||||
radio and slider values, and overlay payloads or handles. Do not erase caller-owned types with
|
||||
`any` or a hard-coded `string`. Use `unknown` only as the safe default for independently consumed
|
||||
anatomy whose value JSX cannot infer from its parent.
|
||||
|
||||
Do not add a root-only generic when separately rendered anatomy can produce values outside the
|
||||
root's inferred type. Preserve the upstream contract until the whole component family can enforce
|
||||
one value type. `Tabs` intentionally follows Base UI's non-generic root because its current tab
|
||||
value type is `any | null`; do not advertise a type relationship the complete anatomy cannot
|
||||
enforce.
|
||||
|
||||
Preserve upstream anatomy when its parts own distinct semantics, interaction, or positioning.
|
||||
Create a Dify-authored convenience component only when the package adds a shared contract; do not
|
||||
hide primitive parts merely to shorten a consumer call site.
|
||||
|
||||
## Keep the public surface small
|
||||
|
||||
A type is not public merely because Base UI names it or an implementation once exported it. In
|
||||
addition to matching component props, export a type only when it pairs with a public factory or a
|
||||
real consumer must name it independently.
|
||||
|
||||
State, event details and reasons, actions, controlled-state helpers, context values, render
|
||||
helpers, styling helpers, and upstream passthrough aliases are private by default. Public props
|
||||
already provide contextual typing for inline render and event callbacks.
|
||||
|
||||
When a wrapper consumes `className` through `cn()`, omit the upstream state-callback form and
|
||||
expose `className?: string`. Public types must describe behavior the wrapper actually implements.
|
||||
|
||||
## Evidence
|
||||
|
||||
Use local public-subpath type tests to protect generic inference, required relationships, and
|
||||
intentional errors. Read current official Base UI documentation and installed type declarations
|
||||
before changing an upstream-derived contract.
|
||||
67
packages/dify-ui/docs/forms.md
Normal file
67
packages/dify-ui/docs/forms.md
Normal file
@ -0,0 +1,67 @@
|
||||
# Forms
|
||||
|
||||
Dify UI form primitives compose Base UI's native form semantics, field accessibility, and Dify
|
||||
styling. They are not a form state-management or schema framework. See the [Base UI forms
|
||||
handbook] for the upstream model.
|
||||
|
||||
## Submit boundary
|
||||
|
||||
Every group of controls that saves or submits together needs a real `<form>` boundary. Do not wire
|
||||
an `Input` and a click-only `Button` together as an informal form.
|
||||
|
||||
Use `Form` when the Dify UI boundary should own Base UI's structured `onFormSubmit` values,
|
||||
consolidated errors, `actionsRef`, or `validationMode`. It renders a native `<form>`. A native form
|
||||
remains correct when another form library owns submission and validation; do not nest form owners.
|
||||
|
||||
Set [`Button`] submit buttons to `type="submit"` explicitly. Keep every other button inside a form
|
||||
at `type="button"`.
|
||||
|
||||
## Fields and labels
|
||||
|
||||
Use `Field` when a control needs a shared name, label, validation, description, or error state. A
|
||||
standalone `Input` may use a native `<label htmlFor>` relationship, but normal form rows should
|
||||
prefer a visible label. `FieldDescription` and `FieldError` provide the corresponding accessible
|
||||
message relationships.
|
||||
|
||||
Choose the label primitive by the control:
|
||||
|
||||
- Text-like inputs, `Textarea`, input-based `Combobox` and `Autocomplete`, a single `Checkbox` or
|
||||
`Radio`, `Switch`, and `NumberField` use `FieldLabel`.
|
||||
- Trigger-based `Select` fields use `SelectLabel`.
|
||||
- `Slider` fields use `SliderLabel`; use per-thumb `aria-label` only when the thumbs need distinct
|
||||
names.
|
||||
- `SelectGroupLabel` and `AutocompleteGroupLabel` label option groups inside popup content. They
|
||||
are not field labels.
|
||||
|
||||
Use [`InputGroup`] when a prefix, suffix, or action shares the input's visual surface.
|
||||
|
||||
## Grouped controls
|
||||
|
||||
Use `Fieldset` and `FieldsetLegend` when one field contains related controls, such as checkbox or
|
||||
radio groups, multi-thumb sliders, or a section of related inputs. Wrap each checkbox or radio
|
||||
option with `FieldItem` and give it its own label:
|
||||
|
||||
```tsx
|
||||
<Field name="allowedNetworkProtocols">
|
||||
<Fieldset render={<CheckboxGroup />}>
|
||||
<FieldsetLegend>Allowed network protocols</FieldsetLegend>
|
||||
<FieldItem>
|
||||
<FieldLabel className="flex items-center gap-2">
|
||||
<Checkbox value="https" />
|
||||
HTTPS
|
||||
</FieldLabel>
|
||||
</FieldItem>
|
||||
</Fieldset>
|
||||
</Field>
|
||||
```
|
||||
|
||||
`Fieldset` owns group semantics and the legend relationship, not interactive state. Pass
|
||||
`disabled`, `value`, `defaultValue`, and change handlers to the group primitive.
|
||||
|
||||
Keep form state, schemas, server validation, and reset behavior outside these primitives. Pass
|
||||
their observable state through the public field and control props instead of replacing the
|
||||
semantic structure.
|
||||
|
||||
[Base UI forms handbook]: https://base-ui.com/react/handbook/forms
|
||||
[`Button`]: ../src/button/README.md
|
||||
[`InputGroup`]: ../src/input-group/README.md
|
||||
63
packages/dify-ui/docs/overlays.md
Normal file
63
packages/dify-ui/docs/overlays.md
Normal file
@ -0,0 +1,63 @@
|
||||
# Overlays
|
||||
|
||||
Dify UI overlay primitives own their Base UI portals, normal application layer, positioning,
|
||||
focus, modal behavior, and dismissal contracts. Consumers choose the semantic primitive and
|
||||
compose its exported anatomy instead of recreating those responsibilities.
|
||||
|
||||
## Portals and root isolation
|
||||
|
||||
Floating surfaces render through [Base UI Portal] into `document.body`. Convenience components
|
||||
such as `DialogContent`, `PopoverContent`, and `SelectContent` own their portals internally;
|
||||
primitives with explicit anatomy expose the constituent portal and content parts.
|
||||
|
||||
The host must establish an isolated stacking context at its application root:
|
||||
|
||||
```tsx
|
||||
<body>
|
||||
<div className="isolate h-full">{children}</div>
|
||||
</body>
|
||||
```
|
||||
|
||||
Equivalent CSS is [`isolation: isolate`][MDN `isolation`]. It creates a stacking context so
|
||||
application descendants cannot use high z-index values to compete with sibling surfaces portalled
|
||||
to `body`. Portalling avoids clipping by application ancestors; isolation itself does not fix
|
||||
clipping.
|
||||
|
||||
## Layering
|
||||
|
||||
| Layer | z-index |
|
||||
| ----------------------------------------------------------------------- | ------- |
|
||||
| Dialogs, pickers, drawers, menus, popovers, preview cards, and tooltips | `z-50` |
|
||||
| Toast viewport | `z-60` |
|
||||
|
||||
Overlays at `z-50` rely on portal DOM order; the portal mounted later appears above earlier ones.
|
||||
Toast remains one layer above ordinary overlays.
|
||||
|
||||
- Do not add call-site `z-*` overrides. Fix the owning overlay structure when content is clipped
|
||||
or hidden.
|
||||
- Do not wrap a Dify UI overlay in another manual portal.
|
||||
- Add shared backdrop or presentation chrome inside the owning exported component, not at call
|
||||
sites.
|
||||
|
||||
## Primitive semantics
|
||||
|
||||
- Use `Dialog` for modal content that needs focus containment and scroll locking.
|
||||
- Use `AlertDialog` only for a destructive or must-confirm decision requiring an explicit answer.
|
||||
- Use `Drawer` for side-panel interactions that follow the drawer model.
|
||||
- Use `DropdownMenu` for button-triggered action lists and `ContextMenu` for context actions.
|
||||
- Use [`Tooltip`] only for a short, non-interactive visual label. The trigger already needs an
|
||||
accessible name; use Popover for information users must reach on touch.
|
||||
- Use [`PreviewCard`] as a non-interactive enhancement for a link destination. Essential
|
||||
information must also exist without the preview.
|
||||
- Use [`Popover`] for explanatory, structured, or interactive content that users must reach on
|
||||
touch or with assistive technology.
|
||||
|
||||
Use a real `<button type="button">` for button-like triggers. If a Base UI trigger intentionally
|
||||
renders a non-button element, set `nativeButton={false}`. Let the primitive own placement and
|
||||
spacing unless its API documents a measured exception.
|
||||
|
||||
[Base UI Portal]: https://base-ui.com/react/overview/quick-start#portals
|
||||
[MDN `isolation`]: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/isolation
|
||||
[`Popover`]: https://base-ui.com/react/components/popover
|
||||
[`PreviewCard`]: https://base-ui.com/react/components/preview-card
|
||||
[`Tooltip`]: https://base-ui.com/react/components/tooltip
|
||||
74
packages/dify-ui/docs/selection.md
Normal file
74
packages/dify-ui/docs/selection.md
Normal file
@ -0,0 +1,74 @@
|
||||
# Selection
|
||||
|
||||
Choose a selection primitive from the value and interaction contract, then preserve the caller's
|
||||
domain value type through its public API.
|
||||
|
||||
## Primitive choice
|
||||
|
||||
- `SegmentedControl` selects one mode, filter, or view. It follows radio-group semantics: an active
|
||||
item cannot be toggled off, `Tab` enters on the selected item, and arrow keys move and select.
|
||||
- `Tabs` selects a panel and provides `tablist` and `tabpanel` semantics.
|
||||
- [`Autocomplete`] accepts free-form text with optional suggestions.
|
||||
- [`Combobox`] selects and remembers one or more values from a searchable collection.
|
||||
- [`Select`] chooses from a closed, scannable list without text entry.
|
||||
|
||||
Multiple-selection comboboxes follow the Base UI chips composition: chips and the input share the
|
||||
input group, chips wrap, and the group grows vertically.
|
||||
|
||||
Autocomplete, Combobox, and Select popups use Base UI's `--anchor-width` and `--available-width`
|
||||
variables to follow their trigger while clamping to the viewport. Do not replace that sizing with
|
||||
a fixed width or an unclamped minimum width.
|
||||
|
||||
Use `Radio` for the default radio appearance. Use `RadioItem` when custom content itself is a
|
||||
radio item, and put `RadioControl` inside it for Dify UI's visual indicator. `RadioControl` is a
|
||||
visual part, not a standalone radio.
|
||||
|
||||
## Typed values
|
||||
|
||||
Do not widen domain values to `string`. Use `Select<Value, Multiple>`, `RadioGroup<Value>`,
|
||||
`Radio<Value>`, and `RadioItem<Value>` for enums, unions, booleans, numbers, objects, or nullable
|
||||
placeholder values.
|
||||
|
||||
Root generics type `value`, `defaultValue`, and value-dependent callbacks. JSX children do not
|
||||
inherit the parent's generic, so type independently consumed anatomy when its value cannot be
|
||||
inferred locally:
|
||||
|
||||
```tsx
|
||||
<RadioGroup<PromptMode> value={promptMode} onValueChange={setPromptMode}>
|
||||
<Radio<PromptMode> value={PROMPT_MODE.default} />
|
||||
<RadioItem<PromptMode> value={PROMPT_MODE.custom}>
|
||||
<RadioControl />
|
||||
Custom prompt
|
||||
</RadioItem>
|
||||
</RadioGroup>
|
||||
```
|
||||
|
||||
For `Select` and `Combobox`, the literal multiple-value type must match runtime mode:
|
||||
`<Combobox<Subject, true> multiple>`. Value display can still receive `null` before selection.
|
||||
Repeat the domain type on independently consumed render anatomy rather than annotating callback
|
||||
parameters:
|
||||
|
||||
```tsx
|
||||
<Combobox<Subject, true> multiple value={subjects} onValueChange={setSubjects}>
|
||||
<ComboboxValue<Subject, true>>
|
||||
{(selected) => selected?.map((subject) => subject.name).join(', ') ?? 'Anyone'}
|
||||
</ComboboxValue>
|
||||
<ComboboxList<Subject>>
|
||||
{(subject) => <ComboboxItem value={subject}>{subject.name}</ComboboxItem>}
|
||||
</ComboboxList>
|
||||
</Combobox>
|
||||
```
|
||||
|
||||
`AutocompleteList` follows the same rule. Groups may infer their local type from `items`; a nested
|
||||
`Collection` is a separate JSX boundary. Dynamic `multiple={condition}` produces the corresponding
|
||||
single-or-multiple union.
|
||||
|
||||
Prefer the Base UI `items` collection pattern so the root, value display, and item list share one
|
||||
runtime source of truth. Convert values to strings only at real serialization boundaries.
|
||||
|
||||
`CheckboxGroup` follows Base UI and uses `string[]`. Model stronger business ID distinctions at
|
||||
the domain boundary unless the upstream primitive contract changes.
|
||||
|
||||
[`Autocomplete`]: https://base-ui.com/react/components/autocomplete
|
||||
[`Combobox`]: https://base-ui.com/react/components/combobox
|
||||
[`Select`]: https://base-ui.com/react/components/select
|
||||
54
packages/dify-ui/docs/styling.md
Normal file
54
packages/dify-ui/docs/styling.md
Normal file
@ -0,0 +1,54 @@
|
||||
# Styling
|
||||
|
||||
## Tailwind CSS v4
|
||||
|
||||
Import Tailwind from the consumer's root stylesheet as described in the [Tailwind CSS v4 upgrade
|
||||
guide], then import the Dify UI CSS entry:
|
||||
|
||||
```css
|
||||
@import 'tailwindcss';
|
||||
@import '@langgenius/dify-ui/styles.css';
|
||||
```
|
||||
|
||||
When a workspace consumer scans Dify UI source directly, add an `@source` entry for the package's
|
||||
`src/` directory using [Tailwind CSS functions and directives], resolved from that consumer
|
||||
stylesheet:
|
||||
|
||||
```css
|
||||
/* Example only: resolve paths from this stylesheet. */
|
||||
@source '../../../packages/dify-ui/src';
|
||||
@source not '../../../packages/dify-ui/src/**/*.{spec,test}.{ts,tsx}';
|
||||
@source not '../../../packages/dify-ui/src/**/*.stories.{ts,tsx}';
|
||||
```
|
||||
|
||||
## Figma radius mapping
|
||||
|
||||
Figma radius tokens are offset by one step from Tailwind CSS v4 defaults. Use this mapping instead
|
||||
of adding custom theme values or `radius-*` utilities:
|
||||
|
||||
| Figma token | Tailwind class |
|
||||
| --------------- | ---------------- |
|
||||
| `--radius/2xs` | `rounded-xs` |
|
||||
| `--radius/xs` | `rounded-sm` |
|
||||
| `--radius/sm` | `rounded-md` |
|
||||
| `--radius/md` | `rounded-lg` |
|
||||
| `--radius/lg` | `rounded-[10px]` |
|
||||
| `--radius/xl` | `rounded-xl` |
|
||||
| `--radius/2xl` | `rounded-2xl` |
|
||||
| `--radius/3xl` | `rounded-[20px]` |
|
||||
| `--radius/6xl` | `rounded-[28px]` |
|
||||
| `--radius/full` | `rounded-full` |
|
||||
|
||||
Convert Figma output such as `rounded-[var(--radius/sm, 6px)]` to the mapped Tailwind class. Use an
|
||||
arbitrary value only when no standard class matches.
|
||||
|
||||
Use semantic Dify tokens and existing component variants before hard-coded values or repeated
|
||||
primitive classes. Use an important modifier only for a tightly scoped compatibility override
|
||||
after the owning variant, data attribute, and selector structure cannot express the state.
|
||||
|
||||
Attach focus-visible styling to the element that visually represents focus. If a visible wrapper
|
||||
contains the native focus target, select that descendant state from the wrapper; for example,
|
||||
`SliderThumb` uses `has-[:focus-visible]` because its internal range input receives focus.
|
||||
|
||||
[Tailwind CSS functions and directives]: https://tailwindcss.com/docs/functions-and-directives
|
||||
[Tailwind CSS v4 upgrade guide]: https://tailwindcss.com/docs/upgrade-guide
|
||||
51
packages/dify-ui/docs/testing.md
Normal file
51
packages/dify-ui/docs/testing.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Testing and Development
|
||||
|
||||
Run `vp check packages/dify-ui` from the repository root for formatting, lint, and TypeScript
|
||||
diagnostics. Run the remaining commands from `packages/dify-ui/`:
|
||||
|
||||
- `vp test --project unit` runs primitive unit tests.
|
||||
- `vp run storybook` starts Storybook.
|
||||
- `vp test --project storybook --run` runs Storybook component tests in browser mode.
|
||||
- `vp test` runs both test projects.
|
||||
|
||||
## Test boundary
|
||||
|
||||
The package has two [Vitest projects]. Both run in Playwright Chromium [Browser Mode]; the project
|
||||
name identifies the behavior owner, not a different runtime.
|
||||
|
||||
Use Storybook for a documented component example. Every story is a render contract and runs the
|
||||
configured accessibility checks through the [Storybook Vitest addon]. Add `play` when the example
|
||||
also owns visible state changes, user interaction, keyboard paths, overlay flows, form behavior,
|
||||
loading behavior, or controlled-state coordination.
|
||||
|
||||
Use regular Vitest tests for lower-level wrapper contracts such as class variants, Base UI
|
||||
passthrough props, hidden-input serialization, data-attribute hooks, stores, and edge cases that
|
||||
do not need a documented example.
|
||||
|
||||
Storybook [accessibility testing] uses `a11y.test = 'error'`, so enabled violations fail the test.
|
||||
Color contrast is the only globally disabled rule because it is a known design-token gap. Do not
|
||||
add another global exception. Keep a temporary exception local to the affected story, and do not
|
||||
use a `play` test in place of an accessibility fix.
|
||||
|
||||
## Animation setup
|
||||
|
||||
Base UI can wait for `element.getAnimations()` before unmounting transition-driven components.
|
||||
Set its test flag in a Vitest setup file when a test asserts final DOM state rather than animation
|
||||
behavior:
|
||||
|
||||
```ts
|
||||
;(
|
||||
globalThis as typeof globalThis & {
|
||||
BASE_UI_ANIMATIONS_DISABLED: boolean
|
||||
}
|
||||
).BASE_UI_ANIMATIONS_DISABLED = true
|
||||
```
|
||||
|
||||
`vitest.setup.ts` already applies this for primitive tests. Storybook uses its preview setup and
|
||||
must retain real animation lifecycles. A unit test that intentionally asserts animation behavior
|
||||
may restore the flag to `false` locally, but must restore the previous value during cleanup.
|
||||
|
||||
[Browser Mode]: https://vitest.dev/guide/browser
|
||||
[Storybook Vitest addon]: https://storybook.js.org/docs/writing-tests/integrations/vitest-addon/index
|
||||
[Vitest projects]: https://vitest.dev/guide/projects.html
|
||||
[accessibility testing]: https://storybook.js.org/docs/writing-tests/accessibility-testing
|
||||
128
packages/dify-ui/src/button/README.md
Normal file
128
packages/dify-ui/src/button/README.md
Normal file
@ -0,0 +1,128 @@
|
||||
# Button
|
||||
|
||||
Use `Button` for an action with a visible text label. Use [`IconButton`] for an icon-only
|
||||
command, `Toggle` for a persistent pressed state, and a native link when activation navigates to
|
||||
a URL.
|
||||
|
||||
Dify UI `Button` is an opinionated wrapper around [Base UI Button]. It adds Dify variants,
|
||||
sizes, content spacing, and a `loading` state while preserving the upstream button, focus, and
|
||||
composition behavior.
|
||||
|
||||
## Button semantics
|
||||
|
||||
`Button` renders a native `<button type="button">` by default. Set `type="submit"` explicitly
|
||||
when the button submits a form:
|
||||
|
||||
```tsx
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Button type="submit">Save</Button>
|
||||
</form>
|
||||
```
|
||||
|
||||
Do not render a link through `Button`. Base UI applies button semantics, keyboard interaction,
|
||||
and disabled behavior to the rendered element. Keep navigation on a native anchor or routing
|
||||
link and reuse only the visual variants:
|
||||
|
||||
```tsx
|
||||
<a className={buttonVariants({ variant: 'secondary' })} href="/settings">
|
||||
Settings
|
||||
</a>
|
||||
```
|
||||
|
||||
Use `render` with `nativeButton={false}` only when a non-button element intentionally needs
|
||||
button semantics. It is not a link mode.
|
||||
|
||||
## Loading and disabled states
|
||||
|
||||
`disabled` and `loading` describe different facts:
|
||||
|
||||
| Prop | Meaning | Default focus behavior |
|
||||
| ---------- | -------------------------------------------- | ----------------------------------------------------- |
|
||||
| `disabled` | The action is unavailable. | Native-disabled and removed from the tab order. |
|
||||
| `loading` | The action was triggered and is now pending. | Activation is blocked while the button retains focus. |
|
||||
|
||||
Internally, Dify UI maps these states to Base UI's interaction contract:
|
||||
|
||||
```tsx
|
||||
disabled={disabled || loading}
|
||||
focusableWhenDisabled={focusableWhenDisabled ?? loading}
|
||||
```
|
||||
|
||||
Base UI recommends disabling a loading button while setting `focusableWhenDisabled` so that an
|
||||
action does not lose focus after it is triggered. Dify UI's `loading` prop owns that wiring and
|
||||
adds the visible spinner. The loading button remains in the tab order with `aria-disabled`
|
||||
instead of the native [`disabled`] attribute. Unlike native disabled, [`aria-disabled`] preserves
|
||||
focusability but requires the component to suppress activation. Callers should pass the pending
|
||||
state only to `loading`:
|
||||
|
||||
```tsx
|
||||
<Button loading={isSaving}>Save</Button>
|
||||
```
|
||||
|
||||
Keep independent availability conditions in `disabled`:
|
||||
|
||||
```tsx
|
||||
<Button loading={isSaving} disabled={!canSave}>
|
||||
Save
|
||||
</Button>
|
||||
```
|
||||
|
||||
Do not repeat the same pending state in `disabled`:
|
||||
|
||||
```tsx
|
||||
// Incorrect: loading already blocks activation.
|
||||
<Button loading={isSaving} disabled={isSaving}>
|
||||
Save
|
||||
</Button>
|
||||
|
||||
// Incorrect: keep only the independent availability condition in disabled.
|
||||
<Button loading={isSaving} disabled={isSaving || !canSave}>
|
||||
Save
|
||||
</Button>
|
||||
|
||||
// Correct.
|
||||
<Button loading={isSaving} disabled={!canSave}>
|
||||
Save
|
||||
</Button>
|
||||
```
|
||||
|
||||
It is valid for `loading` and an independent `disabled` condition to both evaluate to `true`.
|
||||
The loading focus policy applies while the action is pending; when loading ends, the remaining
|
||||
availability condition still determines whether the button is disabled.
|
||||
|
||||
Pass `focusableWhenDisabled={false}` only when a loading button should opt into native disabled
|
||||
behavior and may leave the tab order.
|
||||
|
||||
### Accessible loading feedback
|
||||
|
||||
The loading spinner is decorative and does not replace the button's visible label. `Button` does
|
||||
not add `aria-busy`: [WAI-ARIA `aria-busy`] defines it for an element being modified whose
|
||||
content changes may be deferred by assistive technology, not as a generic substitute for a
|
||||
pending action state. When a long-running operation needs an announcement or progress updates,
|
||||
the feature owns the corresponding status, live region, or progress component.
|
||||
|
||||
## Content and spacing
|
||||
|
||||
`Button` owns spacing between its direct children. Do not add icon margins or a standard `gap-*`
|
||||
at call sites:
|
||||
|
||||
```tsx
|
||||
<Button>
|
||||
<span aria-hidden="true" className="i-ri-rocket-line size-4" />
|
||||
Launch
|
||||
</Button>
|
||||
```
|
||||
|
||||
Regular (`medium`) and `large` sizes use 4px and 6px gaps. `small` uses 3px for `primary` and 4px
|
||||
for the other variants. Use a `className` override only for a documented layout exception.
|
||||
|
||||
## Related guides
|
||||
|
||||
- Read [`IconButton`] for icon-only actions.
|
||||
- Read [Base UI Button] for the upstream interaction and composition contract.
|
||||
|
||||
[Base UI Button]: https://base-ui.com/react/components/button
|
||||
[WAI-ARIA `aria-busy`]: https://www.w3.org/TR/wai-aria#aria-busy
|
||||
[`IconButton`]: ../icon-button/README.md
|
||||
[`aria-disabled`]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-disabled
|
||||
[`disabled`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/disabled
|
||||
47
packages/dify-ui/src/icon-button/README.md
Normal file
47
packages/dify-ui/src/icon-button/README.md
Normal file
@ -0,0 +1,47 @@
|
||||
# Icon Button
|
||||
|
||||
Use `IconButton` for a command represented by one icon and no visible text. Use [`Button`] when
|
||||
the action has a visible label, including buttons with leading or trailing icons. Keep URL
|
||||
navigation on a native link.
|
||||
|
||||
Dify UI `IconButton` is an opinionated [Base UI Button] with an accessible-name type contract and
|
||||
icon-specific appearance, size, and tone variants.
|
||||
|
||||
## Accessible name and glyph
|
||||
|
||||
Every icon button must provide exactly one accessible-name source: `aria-label` or
|
||||
`aria-labelledby`, preserving its [name, role, and value]. A tooltip is a visual enhancement, not
|
||||
the button's accessible name.
|
||||
|
||||
Pass exactly one React element containing the decorative glyph and hide that glyph from the
|
||||
accessibility tree:
|
||||
|
||||
```tsx
|
||||
<IconButton aria-label="Close">
|
||||
<span aria-hidden="true" className="i-ri-close-line size-4" />
|
||||
</IconButton>
|
||||
```
|
||||
|
||||
The child owns the glyph and its optical size. `IconButton` owns the button size, radius,
|
||||
colors, hover, disabled, and focus-visible styles. Use `className` for external layout or for
|
||||
selectors driven by the composed primitive or business-state owner; do not recreate an existing
|
||||
appearance variant.
|
||||
|
||||
Omit `variant` for the IconButton-specific neutral appearance. The other appearance names align
|
||||
with `Button`. Use `tone="destructive"` for destructive intent.
|
||||
|
||||
## Composition
|
||||
|
||||
When Toggle, Menu, Popover, Tooltip, or Collapsible owns the interaction state, keep that
|
||||
primitive outside and compose `IconButton` through its `render` prop. This preserves the owning
|
||||
primitive's pressed, open, expanded, event, and ref behavior instead of mirroring that state on
|
||||
the icon button.
|
||||
|
||||
## Related guides
|
||||
|
||||
- Read [`Button`] for visible-label actions, submit semantics, and loading state.
|
||||
- Read [Base UI Button] for the upstream interaction and composition contract.
|
||||
|
||||
[Base UI Button]: https://base-ui.com/react/components/button
|
||||
[`Button`]: ../button/README.md
|
||||
[name, role, and value]: https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html
|
||||
@ -1,11 +1,58 @@
|
||||
# Input Group
|
||||
|
||||
Use `InputGroup` when one text input shares a visual surface with a prefix, suffix, or action. Otherwise, use the standalone `Input` component.
|
||||
Use `InputGroup` when one text input shares a visual surface with a prefix, suffix, or action. Use
|
||||
the standalone `Input` when no content shares that surface.
|
||||
|
||||
In `web/`, reuse the existing `SearchInput` composite when its search, clear, and IME behavior matches the feature. For other compound inputs, compose this primitive instead of absolutely positioning content over `Input`.
|
||||
`InputGroup` is Dify UI's compound wrapper around [Base UI Input]. It owns the combined visual and
|
||||
pointer surface without changing the input's native semantics.
|
||||
|
||||
Compose exactly one direct `InputGroupInput` with direct `InputGroupAddon` children. `InputGroup` owns the shared border, background, focus state, and non-interactive pointer surface; `InputGroupInput` owns the native input and its value; addons own layout. Put interactive content in a semantic `Button`, `IconButton`, or link instead of adding interaction to the addon; those controls keep their own focus and events.
|
||||
## Anatomy
|
||||
|
||||
Place `InputGroupInput` before every `InputGroupAddon` in the DOM. The input is the primary control and addons are read or reached after it. Use `align="inline-start"` or `align="inline-end"` to choose visual placement without changing that semantic and focus order.
|
||||
Compose exactly one direct `InputGroupInput` and one or more direct `InputGroupAddon` children:
|
||||
|
||||
The initial contract supports the default input size. If another size is needed, add it to `InputGroup`, which owns the shared surface and addon layout.
|
||||
```tsx
|
||||
<InputGroup>
|
||||
<InputGroupInput aria-label="Repository URL" />
|
||||
<InputGroupAddon>https://</InputGroupAddon>
|
||||
</InputGroup>
|
||||
```
|
||||
|
||||
`InputGroup` owns the border, background, focus state, and non-interactive pointer surface.
|
||||
`InputGroupInput` owns the native input and its value. Addons own layout and supporting content.
|
||||
Do not absolutely position content over a standalone `Input` to recreate this shared surface.
|
||||
|
||||
Place `InputGroupInput` before every addon in the DOM. The input is the primary control and addons
|
||||
are read or reached after it. Use `align="inline-start"` or `align="inline-end"` for visual
|
||||
placement without changing semantic or focus order.
|
||||
|
||||
## Accessibility and interaction
|
||||
|
||||
Every `InputGroupInput` needs an accessible name from a visible label, `aria-label`, or
|
||||
`aria-labelledby`, following the [Base UI Input] contract.
|
||||
|
||||
Wrap the group in `Field` when it needs a shared name, label, validation state, description, or
|
||||
error. Field state propagates to `InputGroupInput`, and `InputGroup` derives the shared invalid,
|
||||
disabled, and focus visuals from its direct input. Do not duplicate those states on addons or
|
||||
recreate their styles on the group.
|
||||
|
||||
Treat plain addon text and icons as supporting content; decorative icons should be `aria-hidden`.
|
||||
Put interactive content in a semantic `Button`, `IconButton`, or link instead of adding click or
|
||||
keyboard behavior to the addon itself. Those controls keep their own focus and events.
|
||||
|
||||
Pressing the group's non-interactive surface focuses its direct input. Pressing an interactive
|
||||
addon targets that control and does not move focus to the input. Portalled addon content is outside
|
||||
the group's event path and does not refocus the input. A consumer can cancel the shared-surface
|
||||
behavior by calling `preventDefault()` from `InputGroup`'s `onMouseDown`.
|
||||
|
||||
## Sizing
|
||||
|
||||
The current contract supports the default input size. Add a new size to `InputGroup` rather than
|
||||
resizing the input and addons independently; the group owns the shared surface and addon layout.
|
||||
|
||||
## Related guides
|
||||
|
||||
- Read [Forms] for labels, validation, descriptions, and grouped controls.
|
||||
- Read [Base UI Input] for the upstream native input contract.
|
||||
|
||||
[Base UI Input]: https://base-ui.com/react/components/input
|
||||
[Forms]: ../../docs/forms.md
|
||||
|
||||
@ -8,12 +8,13 @@
|
||||
|
||||
- User-facing strings must use `web/i18n/en-US/` keys. When adding or renaming a key, update every supported locale with the correct localized value.
|
||||
- For new backend calls and migrated surfaces, use generated `consoleQuery` / `consoleClient` APIs from `@/service/client`. Do not add handwritten REST helpers or DTO mirrors, mock-backed app state, or direct edits to generated contracts.
|
||||
- Prefer `@langgenius/dify-ui/*` primitives, data attributes, and design tokens. Preserve a visible focus indicator on the final focusable element.
|
||||
- Use Dify UI `Input` for a standalone text control and `InputGroup` when a prefix, suffix, or action shares its visual surface. Reuse the Web `SearchInput` composite for its standard search, clear, and IME behavior instead of rebuilding it. See the canonical [form and input composition contract].
|
||||
- Use `Button` for actions with visible text and `IconButton` for icon-only actions. Every `IconButton` needs an `aria-label` or `aria-labelledby`, and its decorative glyph must be `aria-hidden`. Compose Menu, Popover, Toggle, and Collapsible through `render` so those primitives keep ownership of their state. See the canonical [IconButton contract].
|
||||
- Follow `docs/overlay.md` for overlay selection. Do not introduce a generic web wrapper that recreates Dify UI portal, backdrop, positioning, or z-index behavior.
|
||||
- Prefer `@langgenius/dify-ui/*` primitives, data attributes, and design tokens. Start from the [Dify UI package index] when choosing a primitive or shared contract. Preserve a visible focus indicator on the final focusable element.
|
||||
- Reuse the Web `SearchInput` composite when its search, clear, and IME contract matches the feature; otherwise follow the canonical [Input Group contract].
|
||||
- Give save and submit flows a real form boundary with visible labels and accessible errors. Use Dify UI `Form` when its structured submission and validation contract is the owner; otherwise use a native form. Follow the canonical [form contract].
|
||||
- Follow the canonical [Button contract] and [IconButton contract] for action semantics, loading, accessible names, and primitive composition. Do not add a Web wrapper that hides those contracts.
|
||||
- Follow the [Dify UI overlay contract] for primitive selection, portals, focus, and layering. Reuse the Web `Infotip` composite for an info glyph that opens explanatory content. Do not introduce a generic Web wrapper that recreates Dify UI overlay behavior.
|
||||
- For custom SVG icons, follow `../packages/iconify-collections/README.md`; do not add generated React icons under `app/components/base/icons/src/`.
|
||||
- `docs/test.md` is the single source of truth for frontend automated-test policy. Skills may route and execute that policy but must not redefine it.
|
||||
- `docs/test.md` is the single source of truth for Web automated-test policy. Skills may route and execute that policy but must not redefine it.
|
||||
|
||||
<!-- BEGIN:nextjs-agent-rules -->
|
||||
|
||||
@ -25,5 +26,9 @@ This block is written and re-added by `next dev` — verify at `node_modules/nex
|
||||
|
||||
<!-- END:nextjs-agent-rules -->
|
||||
|
||||
[IconButton contract]: ../packages/dify-ui/README.md#icon-button-contract
|
||||
[form and input composition contract]: ../packages/dify-ui/README.md#form-contract
|
||||
[Button contract]: ../packages/dify-ui/src/button/README.md
|
||||
[Dify UI overlay contract]: ../packages/dify-ui/docs/overlays.md
|
||||
[Dify UI package index]: ../packages/dify-ui/README.md
|
||||
[IconButton contract]: ../packages/dify-ui/src/icon-button/README.md
|
||||
[Input Group contract]: ../packages/dify-ui/src/input-group/README.md
|
||||
[form contract]: ../packages/dify-ui/docs/forms.md
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
# Overlay Best Practices
|
||||
|
||||
Use `@langgenius/dify-ui/*` primitives for ordinary overlays in new and modified web code. Choose by interaction semantics, not visual shape. A feature may compose business content around a primitive, but it must not recreate generic portal, backdrop, positioning, focus, or z-index behavior.
|
||||
|
||||
## Primitive choice
|
||||
|
||||
- Use `Dialog` for modal surfaces that need focus containment, scroll locking, Escape handling, and outside-press dismissal.
|
||||
- Use `AlertDialog` only when a destructive or must-confirm decision requires an explicit response.
|
||||
- Use `Drawer` for side panels and setup or editor surfaces that follow the drawer interaction model.
|
||||
- Use `DropdownMenu` for a button-triggered action list and `ContextMenu` for pointer-context actions.
|
||||
- Choose `Select`, `Combobox`, or `Autocomplete` by the [Dify UI search and picker contract], not because each renders a popup.
|
||||
- Use `Popover` or the web `Infotip` wrapper for explanatory content, long help text, rich layout, or interactive content.
|
||||
- Use `Tooltip` only for a short, non-interactive visual label. Its trigger must already have an accessible name.
|
||||
- Use `PreviewCard` only as a non-interactive visual preview of a link destination. Essential information must remain available without the preview.
|
||||
|
||||
## Composition
|
||||
|
||||
- Prefer the most specific semantic primitive over styling a generic `Dialog`.
|
||||
- Use controlled `open` and `onOpenChange` when business state, analytics, or cleanup reacts to visibility; otherwise let the primitive own its state.
|
||||
- For a button-like overlay trigger, keep the state-owning primitive outside and use its `render` prop to render the final `Button` or `IconButton`. Do not mirror open, pressed, or expanded state in the button.
|
||||
- Use the primitive-owned content or portal part. Do not wrap a Dify UI overlay in another manual portal.
|
||||
- Keep shared overlay chrome in Dify UI and feature-specific content in the feature owner.
|
||||
|
||||
## Feature-owned exception
|
||||
|
||||
The Step-by-step Tour coachmark is a deliberate feature-owned overlay because it targets arbitrary route content and owns spotlight geometry, pointer blockers, and target interaction policy. Its manual portal belongs to `web/app/components/step-by-step-tour/coachmark.tsx`; it is not a general overlay primitive or a pattern for ordinary dialogs and popovers.
|
||||
|
||||
## Layering
|
||||
|
||||
Body-portalled Dify UI overlays use `z-50`; Toast uses `z-60`. The app root keeps an isolated stacking context, and overlays at the same layer rely on DOM order.
|
||||
|
||||
Do not add call-site z-index overrides such as `z-9999`. If an overlay is clipped or hidden, fix the owning overlay structure instead of raising a child primitive.
|
||||
|
||||
[Dify UI search and picker contract]: ../../packages/dify-ui/README.md#search-and-picker-selection
|
||||
@ -1,6 +1,6 @@
|
||||
# Frontend Testing Guide
|
||||
|
||||
This document is the single source of truth for automated frontend tests under `web/` and `packages/dify-ui/`. Tests should protect product behavior and make refactoring safer. They are not a file-by-file completion exercise.
|
||||
This document is the single source of truth for automated frontend tests under `web/`. Tests should protect product behavior and make refactoring safer. They are not a file-by-file completion exercise. Dify UI owns its package-specific test boundary in the [Dify UI testing contract].
|
||||
|
||||
## Testing Mindset
|
||||
|
||||
@ -37,7 +37,7 @@ Use the smallest boundary that includes the behavior owner and proves the produc
|
||||
- Use React Testing Library for component and feature behavior visible through the DOM or external side effects.
|
||||
- Use integration tests for behavior that crosses meaningful module boundaries.
|
||||
- Use a real browser for layout, responsive behavior, browser-specific APIs, animation, and focus behavior that `happy-dom` cannot represent faithfully.
|
||||
- Follow `packages/dify-ui/README.md` for the Storybook and Vitest boundary of Dify UI primitives.
|
||||
- Follow the [Dify UI testing contract] for the Storybook and Vitest boundary of Dify UI primitives.
|
||||
|
||||
Test the behavior owner. Barrel exports, pass-through wrappers, and purely presentational children do not need separate tests when the owning feature already proves their contract. Do not repeat generic behavior already owned by Base UI, React Aria, or the browser; test Dify's integration, overrides, and known regressions.
|
||||
|
||||
@ -117,7 +117,6 @@ Mocks must preserve the public contract needed by the test. Do not mock interact
|
||||
|
||||
- Following [Vite+ testing configuration], tests under `web/` use two explicit projects in `web/vite.config.ts`. Supported commands and CI select one project explicitly: `unit` runs in `happy-dom` and loads `web/vitest.setup.ts`, while `browser` runs matching `app/**/*.browser.spec.{ts,tsx}` files in Playwright Chromium and loads `web/vitest.browser.setup.ts`. Bare `vp test` runs both registered projects.
|
||||
- Browser failures keep screenshots and Playwright traces under `web/.vitest-browser/`. CI uploads that directory only when failure artifacts exist; Browser Mode does not own coverage or report merging.
|
||||
- Tests under `packages/dify-ui/` use two Chromium Browser Mode projects: `unit` owns focused primitive contracts and loads the package styles through `vitest.setup.ts`; `storybook` owns story render, play, and accessibility contracts through `@storybook/addon-vitest`. The names identify behavior owners, not different runtimes.
|
||||
- New component and feature specs should generally use a sibling `__tests__/` directory. Existing colocated utility and hook specs may follow their owning module's convention. Cross-feature integration specs belong in `web/__tests__/`.
|
||||
- The shared `react-i18next` mock is loaded globally. Use `createReactI18nextMock` from `web/test/i18n-mock` only when a test needs custom translations.
|
||||
- For `nuqs` behavior, use the helpers in `web/test/nuqs-testing.tsx` and assert URL updates. Mock `nuqs` only when URL synchronization is explicitly outside the test contract.
|
||||
@ -180,6 +179,7 @@ Always pass `--project unit` or `--project browser`. Bare `vp test` runs both re
|
||||
- [Testing Library query guidance]
|
||||
- [Testing Library user-event guidance]
|
||||
|
||||
[Dify UI testing contract]: ../../packages/dify-ui/docs/testing.md
|
||||
[React Testing Library documentation]: https://testing-library.com/docs/react-testing-library/intro
|
||||
[Storybook Vitest addon]: https://storybook.js.org/docs/writing-tests/integrations/vitest-addon
|
||||
[Testing Library guiding principles]: https://testing-library.com/docs/guiding-principles
|
||||
|
||||
Loading…
Reference in New Issue
Block a user