6.6 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. - Props pattern:
Omit<BaseXxx.Root.Props, 'className' | ...> & VariantProps<typeof xxxVariants> & { /* custom */ }. - 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.
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-1002onPositioner, matching the overlay contract inREADME.md. - Use
w-(--anchor-width)with viewport-aware max-width forAutocompleteandComboboxpopups. Do not addmin-w-(--anchor-width)when it would defeat available-width clamping.