8.2 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 | navigate through link | link destination preview | ❌ (visual enhancement) |
Popover |
click / tap (+ hover) | 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— a visual enhancement for a link that previews its destination. Prefer the canonical anchor trigger and keep the popup non-interactive. Do not place unique or essential information or actions in the popup unless they are also available at the linked destination; touch and screen reader users cannot access the preview. If opening the popup is itself the trigger's purpose, or its content must be accessible across input modes, usePopoverinstead.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.
Product-level polymorphic trigger compositions are local feature decisions. Document them in the owning feature and do not broaden or weaken the shared primitive contract to match one business workflow.
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.