@vegastack/design 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -54,33 +54,53 @@ rg -n 'style=\{\{' --glob '!components/ui/**'
54
54
  - **hardcoded colour** — a hex literal used as a style value. Use a semantic token. **error**
55
55
  - **raw palette** — a Tailwind palette class. Use `bg-primary`, `text-muted-foreground`,
56
56
  `border-border`, or a status family. **error**
57
- - **hardcoded dimension** — an arbitrary px/rem value. Use the size, spacing, or radius scale.
58
- **error**
57
+ - **hardcoded dimension** — an arbitrary px/rem value where a stock utility says the same thing
58
+ (`h-[32px]` for `h-8`, `rounded-[10px]` for `rounded-lg`). An arbitrary value that no utility
59
+ expresses is not a finding; upstream writes several itself. **warning**
59
60
  - **inline style** — allowed only when every key is a `--*` custom property. Any direct visual
60
61
  property is a finding. **error**
61
62
 
62
63
  ## 3. Off-system utilities
63
64
 
64
65
  ```bash
65
- rg -n '\b(rounded-xl|rounded-2xl|rounded-3xl|text-4xl|text-5xl|text-6xl|font-bold|font-semibold|transition-all|transition-colors|z-[0-9]+|opacity-[0-9]+|tracking-[a-z]+|shadow-[a-z]+|blur-[a-z]+)\b' --glob '!components/ui/**'
66
+ rg -n '#[0-9a-fA-F]{3,8}\b|\b(bg|text|border|ring|fill|stroke)-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-[0-9]{2,3}\b' --glob '!components/ui/**'
67
+ rg -n 'ring-3\b|ring-\[3px\]|ring-ring/[0-9]+|focus-visible:ring-|shadow-\[0_0_0_' --glob '!components/ui/**'
66
68
  ```
67
69
 
68
- - `rounded-xl` and larger do not exist — the scale caps at `rounded-lg`. **error**
69
- - `text-4xl` and larger are off-scale — use `text-display-sm/md/lg/xl`. **error**
70
- - `font-bold`/`font-semibold` — the weight ladder is 400/500, owned by the type roles. **error**
71
- - `transition-all` / `transition-colors` — colour changes are immediate; enumerate the causal
72
- opacity, transform, or geometry properties. **error**
73
- - a raw `z-N` — three token bands only: `z-(--z-raised)` (local raises), `z-(--z-overlay)` (portaled
74
- surfaces), `z-(--z-toast)` (the toast stack alone). DOM order resolves nesting within a band.
75
- **error**
76
- - a raw `opacity-NN` — use an `--opacity-*` role (`opacity-0`/`opacity-100` are exempt). **warning**
77
- - raw `tracking-*`, `shadow-*`, `blur-*` — owned by the type and effect roles. **warning**
78
- - a raw `/NN` colour-alpha step — use an `--alpha-*` role. Alpha and opacity are different roles and
79
- are not interchangeable. **warning**
80
- - `uppercase` on non-mono type, or above 14px. Uppercase is mono-exclusive. **warning**
81
-
82
- Every `transition*` utility must pair a `duration-*` **and** an `ease-*` token in the same class
83
- string, or it silently inherits a default curve. **warning**
70
+ - a hex literal or a numbered Tailwind palette utility — use a semantic token. **error**
71
+ - **a focus-ring glow** — `ring-3`, `ring-[3px]`, `ring-ring/NN`, `focus-visible:ring-*` or a
72
+ `0 0 0` box-shadow ring. The system has ONE focus affordance, the global `:focus-visible` outline;
73
+ text entry tints its border instead. A glow usually means a component was pasted from upstream's
74
+ docs without the patch. **error**
75
+ - a status FILL used as a text ink on that family's own tint — `bg-destructive/10 text-destructive`
76
+ measures 3.98:1. The readable half is `text-destructive-text`. **error**
77
+ - `text-brand` used as a label — `brand` is a 3:1 marker; labels take `text-brand-text`. **error**
78
+ - a raw `<svg>` used as an icon — use lucide or `Icon`/`BrandIcon`. **error**
79
+ - `React.forwardRef` — React 19 takes `ref` as a normal prop. **error**
80
+
81
+ **Things that are NOT findings any more**, and reporting them is noise: `rounded-xl`, `shadow-md`,
82
+ `text-4xl`, `font-semibold`, `tracking-tight`, `transition-all`, `transition-colors`,
83
+ `duration-100`, `ease-in-out`, `z-50`, `opacity-50`, a raw `/NN` alpha, `h-8`/`size-4`,
84
+ `cursor-default` on a menu row, an arbitrary `h-[18.4px]`, and a `hover:` with no `active:` beside
85
+ it. Every one of those is upstream's own vocabulary, which this system now adopts.
86
+
87
+ ## 3b. Names and tokens the shadcn reset removed
88
+
89
+ A project upgrading across the reset carries these until someone changes them, and **there is no
90
+ compatibility layer** — an import resolves to nothing and a deleted token silently compiles to
91
+ nothing, which is the worse half. Both searches are mechanical:
92
+
93
+ ```bash
94
+ rg -n 'IconButton|OTPInput|PasswordInput|CheckboxGroup|FieldInline|Segmented|SplitButton|ProgressIndicator|OnboardingChecklist|FloatingSurface|MarketingSurface|ComparisonMatrix|FigureFrame|LogoRow|ParticleField|PricingSection|RuledBand|SectionHeader|Testimonial|StaggeredTextReveal' --glob '!components/ui/**'
95
+ rg -n 'surface-(1|2|3|raised)|--alpha-|--opacity-|--size-|--icon-|--panel-width-|--z-(raised|overlay|toast)|shadow-overlay|text-(h[1-4]|label|label-sm|code|mono-label|display-)|muted-foreground-faint|surfaceInteractive|fillInteractive|fieldControl|selectedChipVariants' --glob '!components/ui/**'
96
+ ```
97
+
98
+ - **a retired component name** — each has a replacement, listed in the shadcn-reset migration guide;
99
+ the marketing ten have none and their markup is the app's now. **error**
100
+ - **a deleted token or utility** — a `bg-surface-2` or a `text-h1` resolves to nothing and paints the
101
+ inherited value, so the page looks subtly wrong rather than broken. **error**
102
+ - **a deleted `@vegastack/design` export** — `surfaceInteractive`, `fillInteractive`, `fieldControl`,
103
+ `fieldControlGroup`, `selectedChipVariants`. Replace each with the literal it expanded to. **error**
84
104
 
85
105
  ## 4. Component substitution and accessibility
86
106
 
@@ -91,7 +111,8 @@ string, or it silently inherits a default curve. **warning**
91
111
  and `Icon`/`BrandIcon` from `@vegastack/design/icons` are sanctioned. **error**
92
112
  - **`outline-none` with no replacement focus affordance** anywhere in the file. **error**
93
113
  - **Icon-only controls with no accessible name** — a button with no visible text needs `aria-label`
94
- or `aria-labelledby`. Prefer `IconButton`, which requires it at the type level. **error**
114
+ or `aria-labelledby`. `<Button size="icon">` (and `icon-xs`/`icon-sm`/`icon-lg`) is the one
115
+ icon-only control; the name is never optional. **error**
95
116
  - **Missing states** — a surface that fetches data needs loading, empty, and error states, not just
96
117
  the success path. **warning**
97
118
  - **Truncation** — `truncate`/`line-clamp-*` on the same element as `flex`/`inline-flex` silently
@@ -5,12 +5,32 @@ description: Build product UI with the VegaStack design system — which compone
5
5
 
6
6
  # VegaStack design system
7
7
 
8
- Base UI + Tailwind v4 + OKLCH semantic tokens. Components are copy-in via a private shadcn registry;
9
- the runtime and token layer are public npm.
8
+ Base UI + Tailwind v4 + OKLCH semantic tokens, on shadcn's `base-nova` style. Components are copy-in
9
+ via a private shadcn registry; the runtime and token layer are public npm.
10
10
 
11
11
  Load this before writing UI code. For first-time project setup (installing packages, wiring the
12
12
  provider, configuring registry access), use the `vegastack-consume` skill instead.
13
13
 
14
+ **The shadcn reset is a clean break, with no compatibility layer** — no aliases, no deprecation
15
+ shims, no re-exports. It ships as an ordinary minor release, so the version number does not warn you;
16
+ this section does. Every component shadcn ships is now upstream's own file, so its API is upstream's
17
+ API. The complete break is in the shadcn-reset migration guide that ships with the release notes; the
18
+ live contract for any single component is its page at
19
+ <https://design.vegastack.com/docs/components>. The headlines, because they decide most code an agent
20
+ writes:
21
+
22
+ - **Retired, with no drop-in:** `IconButton` → `Button size="icon*"` · `OTPInput` → `InputOTP` ·
23
+ `PasswordInput` → an `InputGroup` composition · `CheckboxGroup` → `FieldSet` + `Checkbox` ·
24
+ `FieldInline` → `EditableCell` · `Segmented` → a joined `ToggleGroup` · `SplitButton` → a
25
+ `ButtonGroup` composition · `ProgressIndicator` → `Progress` + `Spinner` · `OnboardingChecklist` →
26
+ the `onboarding-01` block. The ten marketing components were deleted outright.
27
+ - **Gone from the token layer:** the surface ladder (`surface-1/2/3`), every `--alpha-*` and
28
+ `--opacity-*`, `--size-*`, `--icon-*`, `--z-*`, `--shadow-overlay`, and the role type scale
29
+ (`text-h1`, `text-label`, `text-code`, `text-mono-label`, `text-display-*`).
30
+ - **Gone from `@vegastack/design`:** `surfaceInteractive`, `surfaceInteractiveGroup`,
31
+ `fillInteractive`, `FillTone`, `fieldControl`, `fieldControlGroup`, `selectedChipVariants`.
32
+ `cn`, `TIMINGS`, `FLOATING`, `mergeRefs`, `prose`/`proseClassName` and the icon runtime all stay.
33
+
14
34
  ## Pick a component
15
35
 
16
36
  [references/components.md](references/components.md) is the complete roster, grouped by family, with
@@ -25,23 +45,28 @@ pnpm dlx shadcn@latest list @vegastack
25
45
 
26
46
  Rules that decide most component questions:
27
47
 
28
- - **`Button` is two axes** — `variant` is the shape (`solid · soft · outline · ghost · link · cta`),
29
- `tone` is the hue (`neutral · destructive · success · warning · info`). A destructive action is
30
- `variant="soft" tone="destructive"`; a solid red button does not type-check. Icon-only actions are
31
- **`IconButton`** (`shape="square" | "round"`) — `Button` has no icon size, and an icon in a bare
32
- `<button>` is off-system. An icon-only LINK stays an `<a>`, styled with
33
- `buttonVariants(...) + iconButtonGeometry(size)` — never an `IconButton`, which would put
34
- `role="button"` on navigation.
35
- - **One size vocabulary everywhere** — `xs · sm · md · lg`, with `md` the default. No component has a
36
- size called `default`.
48
+ - **`Button` is one axis** — `variant` is `default · outline · secondary · ghost · destructive ·
49
+ link` (upstream's set, verbatim). `destructive` is a soft tint, not a solid red fill. Icon-only
50
+ actions are `<Button size="icon">` (or `icon-xs` / `icon-sm` / `icon-lg`) with an `aria-label`;
51
+ an icon in a bare `<button>` is off-system. An icon-only LINK stays an `<a>` styled with
52
+ `buttonVariants({ variant, size: "icon" })` — never a `Button`, which would put `role="button"`
53
+ on navigation. `loading` is ours: it holds the label's box and sets `aria-busy`.
54
+ - **Control sizes are upstream's names: `default · xs · sm · lg`**, plus
55
+ `icon · icon-xs · icon-sm · icon-lg` where a square tier exists. The old `md` default is gone, and
56
+ most of the components that are ours dropped their `size` prop entirely and take their height from
57
+ what they compose. Four keepers still carry a small private axis over something that is not a
58
+ control height — `Chip` (`sm`/`md`, the inline and control pill scales), `StatusIcon`, `Stat` and
59
+ `Image`'s corner — and they say so on their own pages.
37
60
  - **Compose `app-shell`** for a sidebar + header + main layout — never hand-roll the landmark trio.
38
61
  - **`select`** for a short fixed option set; **`searchable-select`** when the list is long enough to
39
62
  need a search field (it is the preset `country-select` and `region-select` are built from — reach
40
63
  for it before composing `combobox` by hand); **`combobox`** directly only for free text,
41
64
  suggestions or multi-select chips.
42
- - **`segmented`** for 2–5 exclusive options inline; **`tabs`** when the choice switches page regions.
43
- - **`alert` variant=strip** for in-content notices and plan/trial rows; **`announcement-banner`** only
44
- for the full-width inverse strip at the very top of the page.
65
+ - **`toggle-group`** with `spacing={0}` for 2–5 exclusive options inline; **`tabs`** when the
66
+ choice switches page regions.
67
+ - **`alert`** for an in-content notice — `variant` is `default · destructive · success · warning ·
68
+ info`, each an ink on the `card` surface with a required icon; **`announcement-banner`** only for
69
+ the full-width inverse strip at the very top of the page.
45
70
  - **`chip` is the ONE pill** — `hue` × `size` (`sm` inline · `md` control-scale) × `active`, with
46
71
  `onRemove` giving a real 24×24 remove control. `Tag`, `FilterChip` and `ComboboxChip` are that
47
72
  primitive composed through `render`; never hand-roll a pill with its own height, radius, or a
@@ -52,49 +77,47 @@ Rules that decide most component questions:
52
77
  `role="status"` node with a `{text, seq}` counter.
53
78
  - **`code-block`** for static syntax-highlighted source; **`terminal`** for command sessions.
54
79
  - **`navigation-menu`** is top-level site navigation with panels, not a menu inside a page.
55
- - **Marketing components** (`marketing-surface`, `section-header`, `figure-frame`, `terminal`,
56
- `logo-row`, `testimonial`, `staggered-text-reveal`, `particle-field`, `pricing-section`, and
57
- Button's `cta` variant) are scoped to `.vs-marketing` and must never appear in product UI.
58
80
 
59
81
  ## Tokens
60
82
 
61
- Semantic CSS custom properties from `@vegastack/design-tokens/theme.css` (OKLCH, `:root` + `.dark`).
62
- Always use the utility, never a raw value.
63
-
64
- | Role | Utilities |
65
- | -------- | ------------------------------------------------------------------------------------------------ |
66
- | Surface | `bg-background` (page) · `bg-card` (every surface; `popover`/`sidebar` ARE `card`) |
67
- | Ladder | `bg-surface-1` (rest fill / well) · `bg-surface-2` (hover) · `bg-surface-3` (pressed / selected) |
68
- | Text | `text-foreground` `text-muted-foreground` `text-{primary,accent,popover}-foreground` |
69
- | Status | `bg-{destructive,success,warning,info}` + `-subtle` / `-hover` / `-text` / `-foreground` |
70
- | Border | `border-border` `border-input` — there are no rings; focus is the native outline |
71
- | Radius | `rounded-{xs,sm,md,lg}` — `lg` is the cap, `xl` does not exist |
72
- | Type | `text-{xs…3xl}` · `text-h1…h4` · `text-label` · `text-mono-label` · `text-display-{sm,md,lg,xl}` |
73
- | Font | `font-sans` `font-mono` `font-serif` |
74
- | Motion | `duration-{fast,base,slow}` paired with `ease-{standard,emphasized,exit,spring}` |
75
- | Entrance | `motion-pop-in` `motion-enter-up` `motion-shake` `motion-flash` |
76
- | Docked | `motion-dock-in` / `motion-dock-out` — a control parked at a viewport edge, 150ms in / 100ms out |
77
- | Prose | `proseClassName` from `@vegastack/design` — the whole rendered-rich-text recipe, one class |
78
-
79
- **Hover and pressed come from a recipe, never a literal.** Import the two class strings rather than
80
- writing `hover:bg-*` by hand — that is how a control gets both steps and stays on the ladder:
83
+ Semantic CSS custom properties from `@vegastack/design-tokens/theme.css` (OKLCH, `:root` + `.dark`),
84
+ on shadcn's `neutral` base. Always use the utility, never a raw value.
85
+
86
+ | Role | Utilities |
87
+ | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
88
+ | Surface | `bg-background` (page) · `bg-card` · `bg-popover` · `bg-sidebar` |
89
+ | Fill | `bg-primary` (solid action, every checked control) · `bg-secondary` (soft) · `bg-muted` (well, track, skeleton) · `bg-accent` (hover) |
90
+ | Text | `text-foreground` · `text-muted-foreground` · `text-{primary,secondary,accent,card,popover}-foreground` |
91
+ | Status | `bg-{destructive,success,warning,info}` · `-foreground` (ink ON the fill) · `-text` (ink on the page or on the family's own tint) |
92
+ | Border | `border-border` · `border-input` — there are no rings; focus is one global outline |
93
+ | Radius | `rounded-{sm,md,lg,xl,2xl}` — all derived from the single `--radius` |
94
+ | Type | Tailwind's own `text-{xs…7xl}`. `text-sm` is 14px, `text-base` is 16px |
95
+ | Font | `font-sans` `font-mono` `font-serif` `font-heading` |
96
+ | Motion | `duration-{fast,base,slow}` · `ease-{standard,emphasized,exit,spring}` — or Tailwind's own steps |
97
+ | Entrance | `motion-pop-in` `motion-enter-up` `motion-shake` `motion-flash` |
98
+ | Docked | `motion-dock-in` / `motion-dock-out` — a control parked at a viewport edge, 150ms in / 100ms out |
99
+ | Prose | `proseClassName` from `@vegastack/design` — the whole rendered-rich-text recipe, one class |
100
+
101
+ **Hover and pressed are written, not imported.** A component owns its own interaction chrome, the
102
+ way shadcn writes it:
81
103
 
82
104
  ```tsx
83
- import { cn, surfaceInteractive, fillInteractive } from "@vegastack/design";
84
-
85
105
  // A transparent control on a known surface.
86
- <button className={cn("rounded-md px-2", surfaceInteractive)} />;
87
- // hover:bg-surface-2 active:bg-surface-3
106
+ <button className="rounded-md px-2 hover:bg-accent hover:text-accent-foreground" />;
107
+
108
+ // A solid.
109
+ <button className="bg-primary text-primary-foreground hover:bg-primary/80" />;
88
110
 
89
- // A control on an unknown backdrop, or one that hovers in its own hue.
90
- <button className={cn("bg-destructive-subtle", fillInteractive.destructive)} />;
91
- // hover:bg-destructive/(--alpha-hover) active:bg-destructive/(--alpha-pressed)
111
+ // A tinted status control. The ink on a tint is `-text`, never the fill.
112
+ <button className="bg-destructive/10 text-destructive-text hover:bg-destructive/20" />;
92
113
  ```
93
114
 
94
- A **solid** fill uses neither — it steps through its own darker `-hover` / `-active` tokens.
115
+ A pressed step is optional. `surfaceInteractive`, `fillInteractive`, `fieldControl`,
116
+ `fieldControlGroup` and `selectedChipVariants` were **deleted** from `@vegastack/design` with no
117
+ alias; if you are upgrading, replace each with the literal it expanded to.
95
118
 
96
- **Rendered rich text comes from a recipe too.** Anything the system did not author element by element
97
- — markdown, a contenteditable, CMS copy — wears one class on its root:
119
+ **Rendered rich text comes from a recipe.** Anything the system did not author element by element —
120
+ markdown, a contenteditable, CMS copy — wears one class on its root:
98
121
 
99
122
  ```tsx
100
123
  import { cn, proseClassName } from "@vegastack/design";
@@ -110,40 +133,15 @@ descendant rules (`[&_h1]:…`), which means an element-level class on a child *
110
133
  (specificity (0,1,0) against (0,1,1)) — restyle by composing `prose` (the per-element record), never
111
134
  by setting a class on the rendered element.
112
135
 
113
- **A selected chip on a muted track has a third recipe.** If you are building a view switcher, a
114
- segmented control or chip-shaped tabs of your own, take `selectedChipVariants` rather than inventing
115
- a selected look — it is the same formula `Tabs`, `Segmented` and `Toggle` use:
116
-
117
- ```tsx
118
- import { cn, selectedChipVariants } from "@vegastack/design";
119
-
120
- <div className={cn("rounded-md p-0.5", selectedChipVariants.track)}>
121
- <Toggle
122
- className={cn(
123
- "rounded-sm",
124
- selectedChipVariants.item,
125
- selectedChipVariants.pressed,
126
- )}
127
- />
128
- </div>;
129
- ```
130
-
131
- Use `.pressed` for a control whose selected state is Base UI's `data-pressed` and `.active` for one
132
- using `data-active`. The selected chip keeps its own hover and pressed steps — never guard them off.
133
-
134
- `secondary`, `muted`, `accent` and the `sidebar-*` family are **aliases** of ladder rungs
135
- (`secondary` = `muted` = `surface-1`, `accent` = `sidebar-accent` = `surface-2`, `sidebar` = `card`).
136
- They still compile; name the rung in new code.
137
-
138
- `border` is one translucent hairline — `foreground` at `--alpha-border` — so it reads on the page, on
139
- a card and inside a well alike. `info` is **links and informational UI only**: promotion and
140
- selection take a ladder rung or `primary`.
136
+ `muted`, `accent` and `secondary` share one value in this base, and all three are kept: name the one
137
+ whose ROLE you mean, so a consumer can retune one without moving the others.
141
138
 
142
- Alpha and opacity are **different roles**: colour compositing takes an `--alpha-*` token
143
- (`bg-foreground/(--alpha-ink-tint)`), whole-element opacity takes an `--opacity-*` token
144
- (`opacity-(--opacity-dim)`). A raw `/20` or `opacity-50` is wrong in both cases.
139
+ **Status colour has two inks.** `-foreground` is the ink on the solid fill; `-text` is the ink on the
140
+ page and on the family's own `/10`-`/30` tint. Using the fill itself as text on a tint measures
141
+ 3.98-4.35:1, which the contrast gate rejects. `info` is links and informational UI only.
145
142
 
146
- `--brand` is a marker-role accent only — never a functional state colour.
143
+ `--brand` is a marker-role accent only — never a functional state colour, and never a text ink
144
+ (`--brand-text` is the readable half).
147
145
 
148
146
  **Overriding tokens:** redefine one runtime variable in your global CSS and every component repaints
149
147
  in both themes:
@@ -159,18 +157,19 @@ contract.
159
157
 
160
158
  ## Composition patterns
161
159
 
162
- - **Forms** — Base UI `Field` + react-hook-form `Controller` + Zod 4 (`z.email()`). `Field.Control`
163
- emits `onValueChange`, not a DOM `onChange` event. **`Field` owns the feedback layer**: helper text
164
- renders below the control, the error below that as a polite `role="status"`, and the invalid shake
165
- belongs to the field — wrap a control in a `Field` to get it, and pass `shakeSignal` (a
166
- submit-attempt counter) there to re-shake a field that never stopped being invalid. A bare
167
- `<Input aria-invalid>` outside a `Field` tints its border and does not move.
168
- - **A set of related checkboxes is a `CheckboxGroup`** — pass `allValues` and mark one child
169
- `parent` to get select-all with the mixed state, rather than computing checked/indeterminate in
170
- your own state. Name the group with a `FieldSet`/`FieldLegend` or `aria-labelledby`.
160
+ - **Forms are composed, not configured** — `Field` is layout and copy: `FieldLabel` bound with
161
+ `htmlFor`, the control, then `FieldDescription` and `FieldError` as CHILDREN. There is no `label`,
162
+ `description` or `error` prop, and no context that reaches into the control. State is written where
163
+ it belongs: `aria-invalid` on the control (for assistive tech), `data-invalid` / `data-disabled` on
164
+ the `Field` (for the block's styling). `FieldError` is `role="alert"`, carries a leading icon so an
165
+ error is never colour alone, and takes either children or an `errors` array it de-duplicates.
166
+ react-hook-form's `register` wires straight to the control; there is no `Controller` indirection.
167
+ - **A set of related checkboxes is a `FieldSet` + `FieldLegend` + one `Field` per option** — that is
168
+ the composition upstream documents, and it is what `Checkbox`'s own docs page shows. Compute
169
+ `checked` / `indeterminate` for a select-all parent in your own state, as the Table example does.
171
170
  - **Click-to-edit is `useInlineEdit`** — draft, commit, cancel, focus restoration and the
172
- double-commit guard, with no opinion about the editor or the display. `FieldInline` and
173
- `EditableCell` are built on it.
171
+ double-commit guard, with no opinion about the editor or the display. `EditableCell` is built
172
+ on it.
174
173
  - **Overlays** — enter/exit is driven by `data-starting-style`/`data-ending-style` on the popup root,
175
174
  inside a portal + positioner. Theme, toast, tooltip, and direction providers all come from
176
175
  `<VegaStackProvider>`; your app root needs `isolation: isolate` or portaled popups can render under
@@ -192,20 +191,25 @@ contract.
192
191
  chrome.
193
192
  - Implement every applicable state: default, hover, focus, loading, empty, error, success, disabled.
194
193
  - Put `truncate` on an inner span, with `min-w-0` on the flex container.
195
- - Let the parent decide a form control's width — every control is `w-full` and takes its height from
196
- the `--size-*` scale.
194
+ - Let the parent decide a form control's width — every control is `w-full`.
195
+ - Reach for a plain Tailwind utility for size, radius, shadow, z-index, alpha, weight and motion:
196
+ `h-8`, `size-4`, `rounded-xl`, `shadow-md`, `z-50`, `bg-foreground/10`, `opacity-50`,
197
+ `font-semibold`, `transition-colors duration-100 ease-in-out` are all on-system now.
197
198
 
198
199
  **Don't**
199
200
 
200
201
  - Hardcode a hex, a px value, or a raw Tailwind palette class (`bg-neutral-900`, `text-red-500`).
201
- - Use `font-bold`/`font-semibold` — the weight ladder is 400/500, owned by the type roles.
202
- - Use `rounded-xl`, `text-4xl` or larger, a raw `z-N`, or `transition-all`/`transition-colors`.
202
+ - Add a focus ring or glow. Focus is one global outline, and text entry tints its border instead;
203
+ `ring-3`, `ring-ring/50` and `focus-visible:ring-*` are rejected by lint.
203
204
  - Set `outline-none` without providing another focus affordance.
204
- - Pull in a second icon library or hand-write an inline `<svg>` as an icon.
205
- - Put `uppercase` on non-mono type, or on anything above 14px.
205
+ - Use a status FILL as ink on its own tint — `bg-destructive/10 text-destructive` measures 3.99:1.
206
+ The ink on a tint is `-text`.
207
+ - Pull in a second icon library, hand-write an inline `<svg>` as an icon, or pass
208
+ `size`/`width`/`height` to a lucide component.
206
209
  - Hand-roll a removable pill, or a `role="status"` live region with its own sequence counter.
207
210
  - Give a form control a fixed width (`w-56`, `w-64`) — it reads fine on the page it was tuned for
208
211
  and overflows at 320px. Constrain the parent instead.
212
+ - Expect a compatibility shim from before the reset. There is none — see the migration guide.
209
213
 
210
214
  ## Reference
211
215