8.0 KiB
@langgenius/dify-ui
Shared design tokens, the cn() utility, CSS-first Tailwind styles, and headless primitive components consumed by web/.
Component Authoring Rules
- Use
@base-ui/reactprimitives +cva+cn. - Inside dify-ui, cross-component imports use relative paths (
../button). External consumers use subpath exports (@langgenius/dify-ui/button). - No imports from
web/. No dependencies on next / i18next / ky / jotai / zustand. - One component per folder:
src/<name>/index.tsx, optionalindex.stories.tsxand__tests__/index.spec.tsx. Add a matching./<name>subpath topackage.json#exports. - Name the canonical public boundary and its associated public types after the primitive without a
Rootsuffix (Select/SelectProps). KeepRootonly when the subpath also exports a higher-level convenience component that must be distinguished from the low-level anatomy root (CheckboxRoot/Checkbox). Preserve the upstream anatomy in implementation type sources such asBaseSelect.Root.Props. - Props pattern:
Omit<BaseXxx.Root.Props, 'className' | ...> & VariantProps<typeof xxxVariants> & { /* custom */ }. - Use plain
Omit<...>only for non-union Base UI props. When a prop changes the valid shape of related props (for examplevalue/defaultValue,multiple/value, orclearable/onChange), model that relationship with an explicit discriminated union or a distributive helper instead of flattening the props. - Preserve Base UI generic value contracts in wrappers. If the upstream primitive is generic, expose the same generic parameters and pass them through to the Base UI part, such as
Select.Root<Value, Multiple>,RadioGroup<Value>, orRadio.Root<Value>. - Do not hard-code selection wrappers to
stringunless the upstream primitive is string-only. Select and radio wrappers that carry selected values should preserve the primitive's generic value contract. - When a component accepts a prop typed from a shared internal module,
export typeit from that component so consumers import it from the component subpath. - Prefer Base UI data attributes and CSS variables for visual states; do not mirror state in React solely to add classes.
- When a Base UI API or selector contract is unclear, read the docs linked from
README.mdand the local@base-ui/react.d.tsfiles before coding.
Overlay Primitive Selection: Tooltip vs PreviewCard vs Popover
Pick by the trigger's purpose and a11y reach, not visual richness.
| Primitive | Opens on | Trigger's purpose | Content | Reachable on touch / SR? |
|---|---|---|---|---|
Tooltip |
hover / focus | has its own action | short plain-text label | ❌ (label only) |
PreviewCard |
hover / focus | has a primary click target | supplementary preview | ❌ (via click target) |
Popover |
click / tap (+ hover) | to open the popup | anything, incl. long text | ✅ |
Base UI decision rule (docs):
"If the trigger's purpose is to open the popup itself, it's a popover. If the trigger's purpose is unrelated to opening the popup, it's a tooltip."
Apply this first, then narrow:
Tooltip— ephemeral visual label. Trigger must already carry its ownaria-label/ visible text; tooltip mirrors it for sighted mouse/keyboard users. No interactive UI, no multi-line prose. Not dwell-able.PreviewCard— hover-revealed rich supplementary preview anchored to a trigger whose click goes somewhere (link, selectable row, jumpable chip). Hard contract: the popup MUST NOT contain information or actions unreachable from the trigger's click destination — touch and SR users can't open it. If the info is unique to the popup, switch toPopover(click oropenOnHover) or move it to the click destination. Do not hand-roll "hover to open" on top ofPopoverto evade this split.Popover— any popup with its own interactions, or any "infotip" (?/(i)glyph whose sole purpose is to reveal help text). PassopenOnHoveronPopoverTriggerfor the infotip case — unlikeTooltip/PreviewCard, this stays accessible to touch and SR users because the popover still opens on tap and focus.
Border Radius: Figma Token → Tailwind Class Mapping
The Figma design system uses --radius/* tokens whose scale is offset by one step from Tailwind CSS v4 defaults. When translating Figma specs to code, always use this mapping — never use radius-* as a CSS class, and never extend borderRadius in the preset.
| Figma Token | Value | Tailwind Class |
|---|---|---|
--radius/2xs |
2px | rounded-xs |
--radius/xs |
4px | rounded-sm |
--radius/sm |
6px | rounded-md |
--radius/md |
8px | rounded-lg |
--radius/lg |
10px | rounded-[10px] |
--radius/xl |
12px | rounded-xl |
--radius/2xl |
16px | rounded-2xl |
--radius/3xl |
20px | rounded-[20px] |
--radius/6xl |
28px | rounded-[28px] |
--radius/full |
999px | rounded-full |
Rules
- Do not add custom
borderRadiustheme values. We use Tailwind v4 defaults and arbitrary values (rounded-[Npx]) for sizes without a standard equivalent. - Do not use
radius-*as CSS class names. The old@utility radius-*definitions have been removed. - When the Figma MCP returns
rounded-[var(--radius/sm, 6px)], convert it to the standard Tailwind class from the table above (e.g.rounded-md). - For values without a standard Tailwind equivalent (10px, 20px, 28px), use arbitrary values like
rounded-[10px].
Search / Picker Primitive Selection: Autocomplete vs Combobox vs Select
Pick by whether the user is entering free-form text, choosing a remembered value, or selecting from a closed list.
Base UI decision rules:
- Autocomplete docs: use
Comboboxinstead ofAutocompleteif the selection should be remembered and the input value cannot be custom. - Combobox docs: do not use
Comboboxfor simple search widgets that require unrestricted text entry; useAutocompleteinstead.
Apply this split in Dify UI:
Autocomplete— free-form text input with optional suggestions or completions. The input value may be custom and does not necessarily become a selected option. Use for search boxes, command-style suggestions, tag suggestions, and async text completion.Combobox— searchable picker whose value is one or more selected items from a collection. The chosen value is remembered by the root, and free-form text is not the final value. Use for model pickers, user pickers, dataset/document pickers, and multi-select chips.Select— closed-list picker without text entry. Use when the option set is small or already scannable and filtering is unnecessary.
Composition rules:
- Keep Base UI primitive semantics visible in the public API. Export compound parts such as
ComboboxInputGroup,ComboboxInput,ComboboxContent,ComboboxList,ComboboxItem, andComboboxItemIndicatorinstead of wrapping them into one business component. - For
Comboboxmultiple selection, follow the official chips pattern:ComboboxInputGroupcontainsComboboxChips,ComboboxValuerendersComboboxChipitems, andComboboxInputremains inside the chips row. Chips should wrap and let the input group grow vertically instead of forcing horizontal overflow. - Content primitives must own their Base UI
Portaland usez-50onPositioner, matching the overlay contract inREADME.md. Toast ownsz-60. - Use
w-(--anchor-width)with viewport-aware max-width forAutocompleteandComboboxpopups. Do not addmin-w-(--anchor-width)when it would defeat available-width clamping.