# @sub-design/ui

Portable React controls for GoodSync and NOKL. Twenty families today: Badge,
Button, Card, Checkbox, Context Menu, Dialog, Input, Label, Menu, Notice,
Radio Group, Select, Separator, Sheet, Skeleton, Status Mark, Switch, Tabs,
Textarea, Tooltip.

## What makes a component belong here

Core UI owns portable controls and accessible interaction patterns. File rows,
filter bars, sortable admin tables and marketing sections are not core
primitives — see `docs/adr-0001-platform-foundation.md`.

Two independent consumers is the bar, and it is necessary rather than
sufficient — Card's own history is the cautionary tale for how to apply it.
It was rejected once as unshareable because its two implementations shared a
name and nothing else; promoted later, 2026-08-11, once the same comparison
was redone property by property instead of as a totalled judgment.
Notice/Alert followed the same day: two real consumers whose colours already
converged, blocked instead by a composition-model difference — prop slots
vs. a compound component — that no token could carry, resolved by adopting
Enterprise's compound shape and adding the action slot it lacked. See
`docs/roadmap.md`'s Delivery 3 section for both.

The complete promotion and API-evolution rules live in
[`docs/evolution-policy.md`](../../docs/evolution-policy.md).

## Installing

```sh
npm install @sub-design/ui @sub-design/tokens @sub-design/themes
```

`@base-ui/react`, `react` and `react-dom` are peers.

The package ships **source**, not a build: `.tsx` files that a consumer's own
bundler compiles. That keeps one React instance and lets each product's Tailwind
generate only the classes it uses — but it means the classes are invisible to
Tailwind unless each adopted file is scanned explicitly:

```css
@import "@sub-design/tokens";
@import "@sub-design/themes/goodsync";

@source "../node_modules/@sub-design/ui/src/button.tsx";
```

Add a `@source` line per component adopted. Without it the component renders
unstyled, and nothing warns.

## How it stays brand-neutral

Every class resolves through a token. No component contains a palette value, a
radius literal, or a product name — swapping `data-brand` is the only thing that
changes how it looks. Colours use the `(color:--token)` form and lengths
`(length:--token)`, because Tailwind cannot otherwise tell what a bare custom
property holds.

Base UI is the primitive engine and stays behind this package; products import
from here, never from `@base-ui/react`, so the engine can be replaced without
touching product code.

## Accessibility

Focus geometry, pointer target sizes and contrast are treated as guarantees
rather than style. `--focus-ring-width` and `--switch-hit-area-min` live in the
token contract for that reason, and a product that redeclares them silently
changes adopted components too.

Theme contrast is asserted in `test/contrast.test.ts` — eight pairs across both
brands and both colour schemes, read from the real CSS. Three GoodSync values
shipped below AA before it existed.
