Skip to content

Latest commit

 

History

History
585 lines (419 loc) · 35.7 KB

Usage.docs.mdx

File metadata and controls

585 lines (419 loc) · 35.7 KB

import { Meta } from '@storybook/addon-docs/blocks';

Usage Guide

This guide covers the design system foundations — tokens, presets, colors, units, and patterns — needed to build and maintain components in Cube UI Kit.

For styling engine internals see the Tasty documentation. Tasty docs are also bundled with this package in docs/tasty/:

  • usage.md — tasty() component creation, state mappings, sub-elements, variants, extending components, hooks.
  • configuration.md — configure() options: tokens, recipes, custom units, style handlers, TypeScript module augmentation.
  • styles.md — Complete reference for all enhanced style properties (fill, flow, preset, border, radius, transition, etc.).
  • debug.md — tastyDebug runtime API for inspecting injected CSS and troubleshooting.
  • injector.md — Low-level style injector: inject(), injectGlobal(), deduplication, SSR.
  • tasty-static.md — Zero-runtime tastyStatic(), Babel plugin, Next.js/Vite integration.

Navigation

Pass your router's useHref and useNavigate hooks through Root's navigation adapter. Menu.Item, like Button, ItemButton, and Link, accepts to destinations. Its label and description expose native anchors for modified-click, middle-click and link context-menu commands; the complete row supports ordinary click and keyboard activation. Use navigationOptions for router options and target, rel, or download for native link behavior. Nested providers and overlays inherit the adapter unless they explicitly supply another one.

Overlay sizing

DialogContainer accepts type="tray" and styles for its overlay content shell. Its child Dialog has separate styles and size bounds. Layout.Panel exposes those same channels through dialogProps.styles and dialogStyles in dialog mode; inline panel styles and stored width remain independent. Keep the tray type and change dimensions to expand it without remounting its content.

React Compiler

UI Kit ships components optimized with React Compiler and requires React and React DOM 19.3 or newer. Your application does not need to enable Compiler to use this build. Adoption is incremental: functions with known compatibility diagnostics remain unoptimized, and the legacy form's mutable render paths are explicitly excluded.

Compiling UI Kit does not make mutable legacy form reads in your application reactive. When enabling Compiler in an application, migrate those consumers to the modern Form subscriptions (Form.useValue, Form.useSelector, or Form.Subscribe) or keep the affected consumer components uncompiled with "use no memo". Forms without an explicit instance still use the legacy implementation.

Units

Tasty provides custom units that map to CSS custom properties defined on the Root component. Use these instead of raw pixel values to keep styling consistent.

Unit CSS Variable Example Computed (defaults)
x var(--gap) 2x → calc(var(--gap) * 2) 16px
r var(--radius) 1r → var(--radius) 6px
cr var(--card-radius) 1cr → var(--card-radius) 10px
bw var(--border-width) 1bw → var(--border-width) 1px
ow var(--outline-width) 1ow → var(--outline-width) 3px

Base Tokens

These CSS custom properties are defined in src/tokens/base.ts and applied globally.

Token Default Description
$gap 8px Base spacing unit (used with x multiplier)
$radius 6px Base border radius (used with r multiplier)
$border-width 1px Base border width (used with bw multiplier)
$outline-width 3px Width of a field's thick focus ring (used with ow multiplier)
$outline-offset 1px Gap between a focus outline and its element
$ring-width 1px Width of a hairline focus or selection ring
$item-gap 1px Hairline gap between adjacent items in a list, menu or action group
$stroke-width 1.5 Icon stroke width
$disabled-opacity 0.4 Opacity for disabled elements
$transition 80ms Default transition duration
$disclosure-transition 120ms Disclosure animation duration
$tab-transition 120ms Tab animation duration
$fade-transition 200ms Fade animation duration
$overlay-transition 250ms Modal, tray and underlay enter/exit duration
$scrollbar-transition 150ms Auto-hiding (tiny) scrollbar fade duration
$large-radius (1r + .5x) Larger radius for cards/dialogs
$card-radius (1r + .5x) Card border radius
$dialog-max-size calc(100dvw - 8x), calc(100dvw - 4x) up to 700px Maximum dialog width
$min-dialog-size min(288px, $dialog-max-size) Minimum dialog width

Spacing Tokens

Defined in src/tokens/spacing.ts. Values use the x unit.

Token Value Computed (gap=8px)
$space-xs 0.5x 4px
$space-sm 0.75x 6px
$space-md 1x 8px
$space-lg 1.5x 12px
$space-xl 2x 16px

Size Tokens

Defined in src/tokens/sizes.ts. Used for component heights and icon sizes.

Token Value
$size-xs 24px
$size-sm 28px
$size-md 32px
$size-lg 40px
$size-xl 48px

Shadow Tokens

Defined in src/tokens/shadows.ts. Shadow colors (#shadow-sm, #shadow-md, #shadow-lg) are generated by Glaze and adapt automatically to dark / high-contrast schemes.

Token Value
$item-shadow 0 1px 0.375x #shadow-sm
$card-shadow 0 0.5x 2x #shadow-md
$shadow 0 0.5x 2x #shadow-md
$dialog-shadow 0 1x 4x #shadow-lg

Layout Tokens

Defined in src/tokens/layout.ts.

Token Value
$max-content-width 1440px
$topbar-height 48px
$sidebar-width 200px

Color Tokens

All color tokens use #name syntax in styles. Opacity variants use #name.NN (e.g. #surface-text.06 for 6% opacity).

The palette is generated by @tenphi/glaze (see src/tokens/palette.ts). Every color token is emitted as a state map with light, @dark, and @hc (high-contrast) variants, so the entire UI Kit adapts to dark / high-contrast schemes automatically.

The seeds it is generated from — brand hue, saturation, per-status hues, pastel, contrastLevel — are tunable at runtime. See Theming.

Color Schemes

The @dark and @hc predefined states are wired up in src/components/Root.tsx to support both an attribute opt-in and the user's system preference:

<!-- Force dark scheme -->
<html data-scheme="dark">

<!-- Force high-contrast scheme -->
<html data-contrast="high">

<!-- Both can be combined -->
<html data-scheme="dark" data-contrast="high">

When the attribute is absent, the scheme falls back to @media (prefers-color-scheme: dark) and @media (prefers-contrast: more).

Reading the scheme from JS

For styling, use the state map — { '': light, '@dark': dark, '@hc': hc }. Two cases it cannot serve: a surface the stylesheet never reaches (a Vega spec, a CodeMirror / Monaco theme, an iframe), and a control whose value is the ambient condition (a Light/Dark preview selector). For those, read the same two conditions from JS:

import { useHighContrast, useScheme } from '@cube-dev/ui-kit';

const scheme = useScheme(); // 'light' | 'dark' — the JS answer to `@dark`
const isHighContrast = useHighContrast(); // boolean — the JS answer to `@hc`

Both follow the attribute opt-in first and the media query second, exactly as the states do, and re-render when either changes. Outside React: resolveScheme(), resolveHighContrast() and subscribeScheme(listener) (fires on either condition; re-read in the listener). This is the conditions half of the gap resolveTokenValue() closes for token values.

Root-level only — there is no element-scoped or generic state reader, on purpose. Tasty's state vocabulary mixes element-local states (hovered, pressed, disabled) with ambient ones (@media(…), @root(…)), and only the ambient half is answerable without an element.

Surface & Text Vocabulary

The neutral palette uses a surface / surface-text model. Each surface depth has its own AAA text color, plus a soft (AA) variant for secondary copy:

Token Role
#surface Primary background
#surface-2 Slightly recessed background (cards, inputs)
#surface-3 Deepest background (panels, code blocks)
#surface-4 Subtle recessed background for radio-tab containers
#surface-text AAA text on #surface
#surface-text-soft AA text on #surface (secondary copy, body text)
#surface-text-soft-2 AA-large text on #surface (placeholders, captions)
#surface-2-text, #surface-2-text-soft Same scale, anchored to #surface-2
#surface-3-text, #surface-3-text-soft Same scale, anchored to #surface-3

Other Neutral Tokens

Token Role
#border Default border color
#placeholder Input placeholder text
#focus Focus ring color
#disabled Disabled state fill / text
#pink Accent pink (independent hue)
#shadow-sm / #shadow-md / #shadow-lg Tinted shadow colors (used by $*-shadow tokens)
#overlay Dim backdrop (50% transparent)
#clear transparent

Context Hooks

Tokens a container sets to steer a component that cannot resolve the color on its own. They ship with a default, so nothing has to be set unless the default is wrong.

Token Default Role
#current-fill #surface The label color current.primary punches out of its currentcolor chip — read by that one flavour and nothing else

theme="current" type="primary" fills with the color it inherits, so its label cannot come from that color too and reads #current-fill instead. The page (#surface) is the right answer almost everywhere, and wrong in exactly one place: a container whose own text color is the page paints #white, which is #surface in light mode, so the label would vanish into its own chip. Such a container sets the token to its own fill, which contrasts with its own text by construction:

<Block fill="#fixed-dark" color="#white" styles={{ '#current-fill': '#fixed-dark' }}>
  <Button theme="current" type="primary">Legible in both schemes</Button>
</Block>

Setting it moves the label, the icon slots and the chip's rim together. It also takes the alpha suffix like any other token (#current-fill.5 is the disabled label), because it is a declared token rather than a bare custom property.

Accent System

Each themed color (default, primary, success, danger, warning, note) provides an accent-* family anchored to a fixed white "accent surface text". These tokens stay recognizable across schemes (used for branded buttons, CTAs, etc.):

Token (default theme; prefixed in others) Role
#accent-surface Saturated brand fill (button bg)
#accent-surface-2, #accent-surface-3 Darker / more saturated variants
#accent-surface-text Near-white text on accent fill
#accent-text Saturated colored text (used on neutral surface)
#accent-icon Saturated colored icon

In the default theme these tokens are unprefixed (#accent-surface). In colored themes they are prefixed (#success-accent-surface, #danger-accent-text, …).

Themed Surfaces

Each colored theme (primary, purple, success, danger, warning, note) also provides its own tinted <theme>-surface (banner / chip background) and <theme>-surface-text (banner text).

The hues below are the defaults; each is tunable at runtime (see Theming).

Group Default hue Purpose
#primary-* / #purple-* 280.3° Primary actions, brand color
#success-* 156.9° Success states, confirmations
#danger-* 23.1° Errors, destructive actions
#warning-* 84.3° Warning states, caution
#note-* 302.3° Informational notes

For example, a success banner uses fill: '#success-surface' + color: '#success-surface-text', while a primary button uses fill: '#primary-accent-surface' + color: '#primary-accent-surface-text'.

The four status themes (success, danger, warning, note) add one more pair, <theme>-alert-surface and <theme>-alert-border, which Alert uses. They carry three times the saturation of <theme>-surface (twice in dark), so an alert's theme reads at a glance, and they keep #dark-02 text at AA (AAA in high contrast). primary, purple and the unprefixed default theme do not have them. The shared <theme>-surface / <theme>-border stay quieter because the Item family and table tints sit on them.

Runtime Theming

setPaletteConfig() re-seeds the palette after the kit has mounted; every token re-resolves and <Root> re-injects the token block. No component re-render is involved, because every color compiles to a CSS custom property.

import { resetPaletteConfig, setPaletteConfig } from '@cube-dev/ui-kit';

setPaletteConfig({ accent: { hue: 210 }, themes: { danger: '#b91c1c' } });
resetPaletteConfig();

Also available: getPaletteConfig(), subscribePaletteConfig(), the usePaletteConfig() hook, a <Root palette={…}> prop, DEFAULT_PALETTE_CONFIG, and invalidatePaletteTokens() for when you drive glaze.configure(...) yourself after mount. The palette is process-global — one palette per process, not per tree or per request.

To theme a region instead of the document — a theme preview, or a dark panel inside a light page — use renderColorTokens(). It resolves one config and one scheme to flat values you apply via a tokens prop, without touching the live palette:

<Block tokens={renderColorTokens({ accent: { hue: 210 }, scheme: 'dark' })} fill="#surface">
  …
</Block>

Full contract, caveats and a live playground: Theming.

Legacy Aliases

The previous token names continue to work as aliases (defined at the bottom of src/tokens/colors.ts). New code should prefer the Glaze tokens, but existing components do not need to be migrated:

Legacy Resolves to
#text #surface-text-soft
#dark / #dark-01 #surface-text
#dark-02 #surface-text-soft
#dark-03 #surface-text-soft-2
#dark-04 #placeholder
#dark-05 #border
#minor #surface-text-soft.65
#shadow #shadow-md
#light #surface-3
#dark-bg #surface-2
#primary / #purple #primary-accent-surface
#primary-text / #purple-text #primary-accent-text
#primary-bg / #purple-bg #primary-surface
#primary-icon / #purple-icon #primary-accent-icon
#<status> #<status>-accent-surface
#<status>-text #<status>-accent-text
#<status>-bg #<status>-surface
#<status>-icon #<status>-accent-icon
#<status>-disabled #disabled

#white, #black, and #clear are not defined in the palette — #white and #black are tasty's built-in named colors, and #clear is an alias for transparent.

Typography Presets

Use via the preset style property. Modifiers are appended with a / separator: 't1 / strong', 'h2 / italic'. Modifier-only shorthand like 'bold' is also supported (equivalent to 'inherit / bold').

Defined in src/tokens/typography.ts.

Headings

Preset Size Line Height Weight
h1 36px 44px 600
h2 24px 32px 600
h3 20px 28px 600
h4 18px 24px 600
h5 16px 22px 600
h6 14px 20px 600

Text

Preset Size Line Height Weight Notes
t1 18px 24px 400
t2 16px 22px 400
t2m 16px 22px 500 Medium weight variant
t3 14px 20px 400 Base text size
t3m 14px 20px 500 Medium weight variant
t4 12px 18px 500 Small text
t4m 12px 18px 600 Small text, semi-bold
t4n 12px 18px 400 Small text, normal weight — for secondary metadata beside t4

Paragraph

Paragraph presets have slightly increased line heights compared to text presets at the same size, optimized for multi-line reading.

Preset Size Line Height Weight
p1 18px 28px 400
p2 16px 24px 400
p3 14px 22px 400
p4 12px 20px 500

Markdown / Prose

Largest line heights, designed for long-form content blocks.

Preset Size Line Height Weight
m1 18px 32px 400
m2 16px 28px 400
m3 14px 24px 400

Snippet / Monospace

Sized to parallel t2–t4. Each preset sets font-family to var(--font-mono) (the design-system monospace stack from Root).

Preset Size Line Height Weight Notes
s2 16px 22px 400 Larger code / snippet text
s3 14px 20px 400 Default snippet size (matches Prism)
s4 12px 18px 400 Compact code / snippet text

Special Presets

Preset Description
c1 14px, weight 600, uppercase — section captions
c2 12px, weight 600, uppercase — small captions
tag 12px, weight 600 — tag/badge text
strong Inherits everything, applies bold weight
em Inherits everything, applies italic style
default Aliases t3 values — the base text style

Resolving Tokens Outside CSS

Some surfaces cannot take a var(--…) reference — a third-party iframe (Stripe Elements), a CodeMirror or Monaco theme object, a Vega spec. Those take colors, lengths and font descriptors as values, so they need the token resolved. resolveTokenValue() and its siblings read that value out of the DOM, from the element Root declares the tokens on, and refuse to hand back a placeholder.

import { resolvePresetValues, resolveTokenValue, resolveTokenValues } from '@cube-dev/ui-kit';

resolveTokenValue('#purple'); // 'oklch(0.571 0.0971 280.3)'
resolveTokenValue('$space-md'); // '8px'
resolveTokenValues(['#surface', '#surface-text', '#border']); // keyed by the tokens as written
resolvePresetValues('s3'); // { fontFamily: 'ui-monospace, …', fontSize: '14px', lineHeight: '20px', … }

useTokenValue(), useTokenValues() and usePresetValues() are the same three readers as hooks, re-resolved whenever the palette is re-seeded or the scheme / contrast tier flips. The object useTokenValues() returns keeps its identity while the values do, so it is safe as a dependency of the theme object built from it.

Colors a Legacy Parser Will Accept

Every color token computes to oklch(...), and a large class of consumers validates colors against "HEX, rgb(), or hsl()" and silently drops anything else — Stripe's Appearance API falls back to its own light theme, d3-color (so Vega) throws, mapbox-gl rejects even an #rrggbbaa tail. Nothing warns, and a round trip through the DOM does not help: Chrome serializes oklch back as oklch. So resolveTokenValue() hands back a value that is correct and unusable at the same time, and toLegacyColor() is the step between them.

import { resolveTokenValue, toLegacyColor } from '@cube-dev/ui-kit';

toLegacyColor(resolveTokenValue('#purple')); // '#6b53e4'
toLegacyColor('oklch(0.55 0.21 285 / 0.4)'); // 'rgba(107, 83, 228, 0.4)'
toLegacyColor('oklch(0.55 0.21 285 / 0.4)', { alpha: 'hex' }); // '#6b53e466'

An opaque color is always #rrggbb, which everything accepts. Only the translucent case has a choice, and alpha picks it, because the acceptable form differs per consumer: 'rgba' (the default) is the widest — Stripe, mapbox-gl and CSS all take it — while 'hex' gives the shorter #rrggbbaa that Vega and plain CSS prefer and mapbox-gl drops.

It takes null so it composes with resolveTokenValue() directly, and reads every notation the kit emits or accepts: oklch(), okhsl(), okhst(), rgb(), hsl(), hex (3/4/6/8 digits) and transparent, in either the slash or the legacy comma alpha syntax, including the components a hand-rolled regex misses — a negative hue, any angle unit, scientific notation, a none component. A chroma outside the sRGB gamut is clipped to the gamut boundary rather than allowed to overflow a channel.

A value it cannot read returns fallback ?? null rather than the input, which is the whole point: color-mix(), relative syntax (oklch(from …)) and a bare var() reference only have a value inside a CSS engine, and handing one back unchanged is exactly what makes the consumer drop the color and revert to its own theme.

These resolve token values. For the ambient conditions themselves — which scheme and contrast tier the document is in — see Reading the scheme from JS.

So these helpers do not inspect the value. They read $tokens-applied, a marker the token block declares alongside the tokens, which answers the question that matters — are the kit's tokens in effect on this element? Off that surface every read returns null (or fallback) and warns once in development; on it, the computed value is taken as the truth.

All six take { element, fallback }. Pass element to read a local override instead of the document — a subtree carrying its own tokens prop (renderColorTokens()), or one under a data-scheme / data-contrast that differs from the document's. Pass fallback for the value to use when a token cannot be resolved, including during SSR, where there is no DOM to read.

Why getComputedStyle by hand does not work. Root declares the token block on <body>, so <html>, a detached node, and a tree that has not mounted Root yet are all outside it — and a token read from outside does not come back empty. Tasty registers @property rules with real initial-values, so --gap reads 8px there, --radius reads 6px, --transition reads 80ms. Those are tasty's defaults, matching the kit's own values only by coincidence: the value looks entirely ordinary, and for a token the kit sets differently it is simply wrong. Inspecting it cannot help in the other direction either: #clear really is transparent and $h2-letter-spacing really is 0px, indistinguishable from an unset property of the same type.

Themes

Component themes are set via the theme prop or theme=name modifier:

Theme Purpose
default Standard appearance
danger Destructive / error actions
special Highlighted / promoted actions
success Success / positive states
warning Warning / caution states
note Informational / note states
current No palette of its own — every color is mixed from the inherited text color (#current), so the component adopts whatever its container paints with. Supported by Button, Item (and ItemButton, Item.Action, ItemBadge)

Customizing components

Most components are a design contract: customize them through their own props (type, theme, size, …), tokens and mods rather than by restyling them. The layout and typography primitives (Element, Block, Flex, Flow, Grid, Space, Prefix, Suffix, Text, TextItem, Title, Paragraph, Typography, ButtonGroup and Button.Group), the Action base and the icons (including IconSwitch) are the exception: their styles are their API.

The package's tasty.config.ts lists the other components as protectedComponents, and tasty/no-protected-restyle from @tenphi/eslint-plugin-tasty 1.12.0 or newer reports a restyle of one in your code:

  • instance styles, and every *Styles prop such as inputStyles;
  • style props other than the ones that place or size the component (width, height, gridArea, margin, inset, …). Containers (Card, Panel, ResizablePanel, Content, Header, Footer, and Layout and GridLayout with their sub-components) also keep the ones that lay out their content: padding, gap, flow, place*, align*, justify*, grid*, overflow and scrollbar. Root styles that sets only allowed props passes too;
  • tasty() wrappers that restyle a protected component, and wrappers of those wrappers.

On an input, the style props that place or size it act on its outer element: the field wrapper when it renders (see Field properties), the input itself otherwise. Its styles always style the input, so a root styles that only sizes it passes the rule but sizes the input inside the field, not the field. To size the input alone, beside a side label for example, use its controlWidth prop. Input components take no other style props.

A component's own prop that shares a name with a style passes, such as a field's direction and filter, a text field's resize, ColorSwatch's color or AlertDialog's content.

A sub-component counts as the component it hangs off, so Button.Split is protected like Button, and Layout.Content is a container like Layout. The list names the exceptions, such as Button.Group.

To turn it on, extend the package's config and enable the rule:

// tasty.config.ts
export default { extends: '@cube-dev/ui-kit' };
// .oxlintrc.json
{
  "jsPlugins": [{ "name": "tasty", "specifier": "@tenphi/eslint-plugin-tasty" }],
  "rules": { "tasty/no-protected-restyle": "warn" },
}

An intended override takes a disable comment with its reason on the line before it. Among JSX children, only the {/* … */} form works:

<Space>
  {/* oxlint-disable-next-line tasty/no-protected-restyle -- the empty state's call to action takes the brand fill */}
  <Button fill="#purple">Get started</Button>
</Space>

With this rule on, tasty/no-styles-prop can stay off, so the primitives keep their styles.

The same extends gives the plugin's other rules the names UI Kit defines: its tokens, presets and recipes, and the @dark, @hc and @reduced-motion state aliases. tasty/valid-color-token, tasty/valid-custom-property, tasty/valid-preset, tasty/valid-recipe, tasty/valid-state-key and tasty/no-unknown-state-alias then report only names that don't exist. The tokens include the ones a component's docs offer you, to read inside that component or set on it, such as a dialog's $dialog-padding-h or a layout's $content-padding. A component's other custom properties are internal and can change, so the rules report them too. List what your app defines itself, in its own configure() or tokens, in its own config, which adds to the lists it extends:

// tasty.config.ts
export default {
  extends: '@cube-dev/ui-kit',
  states: ['@mobile'],
  tokens: ['#brand'],
};

Recipes

Reusable style presets applied via the recipe style property. Defined in src/components/Root.tsx.

Recipe Purpose
reset Zero out margin, padding, border, outline; set box-sizing: border-box
button Base button appearance: no native styling, touch-action: manipulation
input Base input appearance: no native styling, inherit color
input-autofill Handle browser autofill styling (:autofill, :-webkit-autofill, :-internal-autofill-*)
input-placeholder Placeholder text styling via -webkit-text-fill-color
input-search-cancel-button Hide native search cancel button

Common Modifiers

These are data-attribute modifiers used in component style maps (via the mods prop):

Modifier Description
disabled Disabled state
readonly Read-only state: the value shows but cannot change
hovered Hover state
focused Focus state
pressed Active/pressed state
selected Selected state
checked Checked state (checkbox, switch)
invalid Invalid validation state
valid Valid validation state
indeterminate Indeterminate state
collapsed Collapsed state
type=primary Primary variant
type=outline Outline variant
size=small / size=medium / size=large Size modifiers

State Syntax

In style value maps, keys define when styles apply. States can be combined with boolean logic:

fill: {
  '': '#surface',              // default
  ':hover': '#surface-hover',  // pseudo-class
  disabled: '#disabled-surface', // data-attribute modifier
  'type=danger': '#danger',    // data-attribute with value
  '@media(w < 768px)': '...',  // media query
  '@(panel, w >= 300px)': '...', // container query
  'disabled & :hover': '...',  // combined with AND
  'a | b': '...',              // combined with OR
  '!disabled': '...',          // negated
}

Reduced Motion

<Root> registers a global @reduced-motion state beside @dark and @hc. It matches @media (prefers-reduced-motion: reduce). Use it to stop or shorten motion for users who ask for less:

const Spinner = tasty({
  styles: {
    animation: 'spin 1s linear infinite',
    animationPlayState: { '': 'running', '@reduced-motion': 'paused' },
  },
});

It follows the system preference alone, with no attribute opt-in.

Icons

Icons live in src/icons/. Use existing icons when possible; otherwise use @tabler/icons-react.

  • <Icon> — wrapper component for sizing and coloring icons. Props: size, stroke, plus style props.
  • wrapIcon(name, icon) — creates a Cube icon component from a tabler icon.
  • Always wrap tabler icons with <Icon> for custom size/color. Never add props directly to tabler icons.
  • To add new icons run: pnpm add-icons

Illustrations are not icons and do not live in src/icons/: CubeLogo and NoDataIcon are Icon-based components under src/components/other/. They size like an icon, but NoDataIcon is a three-tone drawing on the shared cube-face tokens (#loading-face-1 … #loading-face-3) rather than a currentColor glyph, so it ignores color and belongs at illustration sizes (size="8x" and up).

Hierarchical Table Rows

ItemTable and DataTable opt into native treegrid behavior with getRowChildren. Pass roots in data; the callback returns already-loaded children, and row keys must be globally unique. Expansion is uncontrolled with defaultExpandedKeys or controlled with expandedKeys / onExpand.

The shared table engine recursively sorts siblings, paginates roots with their complete subtrees, and flattens only expanded rows for rendering and virtualization. ItemTable client search preserves ancestor paths and temporarily opens branches containing matches. Server modes do not transform nested data.

Use treeColumnKey to choose the disclosure/indent column. Its hierarchy chrome is customizable through the TreeContent, TreeToggle, and TreeValue sub-elements. ItemTable multiple selection cascades by default; DataTable cell ranges contain visible rows only. See each component's docs for pagination, numbering, selection, and drag/drop limits.

DataTable also accepts nested column definitions for already-shaped pivot results. Group nodes create accessible multi-row headers with native column spans, while sorting, resizing, ranges and copying continue to operate on their leaf columns. Both tables fill an available flex pane by default; use isAutoHeight for a compact result whose frame should end at its last rendered row. Auto-height renders every row, so bounded tables remain the right choice for large results.

Form System

UI Kit provides modern and legacy implementations. Use the modern API for new forms. Existing callers remain on the legacy implementation until explicitly migrated.

Modern forms

For a new modern form, create a typed controller, put defaults on the controller, and bind inputs with name. Modern names accept strings and nested tuples; field is an optional typed binding:

const form = Form.useController<{ email: string | null }>({
  defaultValues: { email: null },
});

<Form form={form} onSubmit={(values, { signal }) => save(values, signal)}>
  <TextInput name="email" label="Email" isRequired />
  <Form.SubmitError />
  <Form.Submit>Save</Form.Submit>
</Form>

Inputs subscribe to their own values and errors. For other reactive UI, use Form.useValue(form, path) for one value, Form.useFieldState(form, path) for field metadata, or a selector for derived state. <Form.Subscribe> keeps an inline region's subscription separate from its parent. Keep imperative getters in handlers and effects.

Modern Form explains which API to choose for server data, nullable values, nested fields, conditional sections, cross-field and remote validation, errors, and external actions. Use that guide for implementation recipes and the migration checklist when converting an existing form.

Legacy forms

Form.useForm() and roots without a modern controller keep the legacy backend. DialogForm also supports modern controllers; Form.Item remains legacy-only. See FormInstance (legacy) for legacy instance methods, defaults, and dot notation.

Shared field integration

Built-in inputs have field support; they do not need a Field wrapper. Modern name and field bindings, and legacy string name bindings, connect through useFieldProps. Custom controls use that hook and wrapWithField. Field Properties documents shared labels, validation, state, and presentation props.

Fields render their chrome when there is something to show: a label, a form binding, or a description, message, field styles or props, or isHidden, and on TextInput, TextArea, PasswordInput and NumberInput a validate. A field with none of them renders only the control. Use isBare for an inline control with no room for its chrome, or when a parent owns the field layout and feedback; a supplied label remains accessible but visually hidden. isBare={false} renders the chrome anyway. Message content never chooses whether the wrapper exists. errorMessage={null} suppresses error text and its fallback while preserving the description and invalid state.