@usememos/ui 26.11.9 → 26.11.10

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.
Files changed (40) hide show
  1. package/README.md +4 -4
  2. package/dist/components/button/index.js +1 -1
  3. package/dist/components/button/index.js.map +3 -3
  4. package/dist/components/dialog/parts.js.map +1 -1
  5. package/dist/components/form/index.js.map +1 -1
  6. package/dist/components/kbd/index.js.map +1 -1
  7. package/dist/components/link/index.js.map +1 -1
  8. package/dist/components/menu/classes.js.map +1 -1
  9. package/dist/components/menu/index.js +1 -1
  10. package/dist/components/menu/index.js.map +3 -3
  11. package/dist/components/popover/classes.js.map +1 -1
  12. package/dist/components/popover/index.js.map +1 -1
  13. package/dist/components/select/index.js.map +1 -1
  14. package/dist/components/table/index.js.map +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/index.js.map +3 -3
  17. package/dist/spacing.js +3 -0
  18. package/dist/spacing.js.map +7 -0
  19. package/dist/styles.css +1 -1
  20. package/dist/tailwind.css +8 -13
  21. package/dist/theme-tokens.js +1 -1
  22. package/dist/theme-tokens.js.map +2 -2
  23. package/dist/tokens.css +1 -1
  24. package/dist/types/components/button/index.d.ts +2 -2
  25. package/dist/types/components/dialog/parts.d.ts +1 -1
  26. package/dist/types/components/form/index.d.ts +1 -1
  27. package/dist/types/components/kbd/index.d.ts +1 -1
  28. package/dist/types/components/link/index.d.ts +1 -1
  29. package/dist/types/components/menu/classes.d.ts +1 -1
  30. package/dist/types/components/menu/index.d.ts +2 -2
  31. package/dist/types/components/popover/classes.d.ts +1 -1
  32. package/dist/types/components/popover/index.d.ts +1 -1
  33. package/dist/types/components/select/index.d.ts +2 -2
  34. package/dist/types/components/table/index.d.ts +2 -2
  35. package/dist/types/index.d.ts +1 -0
  36. package/dist/types/spacing.d.ts +18 -0
  37. package/dist/types/theme-tokens.d.ts +3 -4
  38. package/llms-full.txt +239 -219
  39. package/llms.txt +1 -1
  40. package/package.json +1 -1
package/llms-full.txt CHANGED
@@ -19,7 +19,7 @@ It is not a general-purpose UI library for outside developers. Components are ge
19
19
  | --- | --- |
20
20
  | [Principles and voice](principles.md) | The taste in six rules, and how interface text is written |
21
21
  | [Color](color.md) | Color roles, every color token in Light and Dark, and how themes are chosen |
22
- | [Depth](depth.md) | The tactile model: recessed, key, raised, popup and flat; shadow tokens |
22
+ | [Depth](depth.md) | Flat surface roles, borders and focus without shadows or outlines |
23
23
  | [Typography](typography.md) | The system font, the type scale and text rules |
24
24
  | [Size, spacing and layout](layout.md) | Control sizes, the 4px scale, radius, layout widths, breakpoints and layers |
25
25
  | [Motion](motion.md) | Press, settle and fade timing, and reduced motion |
@@ -52,7 +52,7 @@ It is not a general-purpose UI library for outside developers. Components are ge
52
52
 
53
53
  1. **Content first.** The interface stays gray, so people's words, tags and images are the color on the page.
54
54
  2. **Ink is the emphasis.** The primary action, a checked box and a switch that is on use `primary` (near-black in Light, near-white in Dark). There is no brand hue in controls.
55
- 3. **Depth means behavior.** Recessed parts hold a state (wells, tracks). Raised parts can be pressed (buttons, keys, knobs). Flat parts are content, including text fields. Never add depth for decoration. See [Depth](depth.md).
55
+ 3. **Flat surfaces, clear states.** Fills show controls and state; borders separate surfaces. No shadows, sheen, focus rings or visible outlines. Content stays unboxed. See [Surfaces and boundaries](depth.md).
56
56
  4. **Calm by default.** Hierarchy comes from text color, lightness steps and spacing, not from weight, size or extra color. Few sizes and two weights: see [Typography](typography.md).
57
57
  5. **Dense, not cramped.** Controls are 32px tall by default, and every gap comes from the 4px scale. See [Size, spacing and layout](layout.md).
58
58
  6. **One system, many products.** Components are generic. A product's screens are built from them and from the [patterns](patterns.md); nothing in the package knows what a note or a memo is.
@@ -87,33 +87,34 @@ Memos UI is ink on paper: pure-gray neutrals, `primary` ink for emphasis, one qu
87
87
  | Role | Tokens | Used for |
88
88
  | --- | --- | --- |
89
89
  | Surfaces | `canvas`, `surface`, `overlay` | The page; then inputs, menus, dialogs and toasts one step up. `overlay` dims the page behind dialogs |
90
- | Wells | `well`, `edge` | Switch, tab, checkbox and radio wells, pressed toggles, inline code. `edge` is the 1px border of wells and inputs |
90
+ | Wells | `well`, `control-edge` | Switch, tab, checkbox and radio wells, pressed toggles, inline code. `control-edge` is the 1px boundary of controls |
91
91
  | Hover | `hover` | Rows, ghost buttons, the highlighted menu row. About 4% ink over surface |
92
- | Keys | `key`, `key-hover`, `key-sheen` | Neutral buttons, select triggers, the selected tab, switch knobs, kbd |
93
- | Text | `ink`, `ink-muted` | Primary and secondary text. Both pass 4.5:1 on every surface, well and key in both themes |
92
+ | Keys | `key`, `key-hover` | Neutral buttons, select triggers, the selected tab, switch knobs, kbd |
93
+ | Text | `text`, `text-secondary`, `text-disabled` | Primary, secondary and disabled text. Each keeps 4.5:1 on the backgrounds named in the token table below |
94
94
  | Emphasis | `primary`, `primary-hover`, `on-primary` | Primary button, checked checkbox and radio, switch when on, tooltip |
95
- | Meta | `accent`, `selection`, `focus`, `focus-edge` | Links and text selection (`accent`); the keyboard focus ring and the focused field edge (`focus`, `focus-edge`, both ink). Never a fill |
95
+ | Meta | `accent`, `selection`, `focus`, `focus-edge` | Links and text selection (`accent`); the keyboard focus edge and the focused field edge (`focus`, `focus-edge`, both ink). Never a fill |
96
96
  | Status | `success`, `warning`, `danger` and their `-soft` pairs; `danger-key`, `on-danger` | Toasts, badges, field errors, danger buttons |
97
- | Lines | `line` | Separators, table rules, menu dividers |
97
+ | Lines | `edge`, `line` | Popup and badge borders; separators, table rules and menu dividers |
98
98
 
99
99
  ## Rules
100
100
 
101
101
  - Neutrals are pure gray, never tinted.
102
102
  - `accent` is a quiet blue used only for links and selection. If a screen has blue on it at rest, apart from links, something is wrong.
103
- - Focus is ink, not blue. A focused field darkens its own edge to `focus-edge`; a control reached by keyboard gets a 2px `focus` ring 1px outside its edge. See [States](states.md).
103
+ - Focus is ink, not blue. A focused field darkens its own edge to `focus-edge`; a control reached by keyboard thickens its existing border to 2px `focus`. On primary and checked fills, the border uses `on-primary`; danger buttons use `on-danger`. See [States](states.md).
104
104
  - Status colors appear only when there is a status. Red is for destructive actions and errors, never for emphasis.
105
105
  - `danger` is tuned for text. `danger-key` is the danger button's fill; it is darker in Dark so the button is not the loudest thing on screen.
106
106
  - Always pair a status color with a word or an icon.
107
- - A color the user chooses, such as a tag's, is their content, not a role. It appears only as a tint behind `ink` text (Badge's `color`), never as text or as a fill under light text, because the package can't know its contrast.
107
+ - A color the user chooses, such as a tag's, is their content, not a role. It appears only as a tint behind the `text` color (Badge's `color`), never as text or as a fill under light text, because the package can't know its contrast.
108
108
  - Use tokens through their CSS variables. Every token is a custom property with the `--mu-` prefix: the `well` token is `var(--mu-well)`. The prefix keeps the tokens apart from an app's own variables and from Tailwind's. Never copy raw values into component or product code.
109
109
 
110
110
  ## Themes
111
111
 
112
112
  There are two themes: **Light** (the default) and **Dark**.
113
113
 
114
- - Each theme sets every color token and every shadow token to an explicit value. Nothing is derived at runtime. The package's tests check the values against the tables on this page and in [Depth](depth.md), and check the contrast promised here.
115
- - Dark keeps the same depth model. Wells go darker than `canvas`, keys go lighter, `key-sheen` drops to 8%, and `primary` flips to near-white with a dark `on-primary`.
116
- - Non-text contrast: `edge` is about 1.5:1 in Light and Dark. That is the soft border people expect from a calm interface, and it is below WCAG's 3:1 for control boundaries. An app that needs 3:1 boundaries defines its own [app theme](#app-themes) with a darker `edge` and outlined keys.
114
+ - `text`, `text-secondary` and `text-disabled` name content roles for text and icons, not fixed colors. App themes can set each one; they are not derived from `primary`.
115
+ - Each theme sets every color token explicitly. The package's tests check the values against the table on this page and check the contrast promised here.
116
+ - Dark keeps the same flat surface roles. Wells and keys are distinct gray fills, and `primary` flips to near-white with a dark `on-primary`.
117
+ - Control boundaries use `control-edge`, at least 3:1 against `canvas` and `surface` in both themes. `edge` stays softer for popup and badge borders; `line` separates content. Never substitute a structural line for a control boundary.
117
118
 
118
119
  ### Choosing a theme
119
120
 
@@ -125,8 +126,7 @@ The package does not read `prefers-color-scheme` itself. The app does, so that a
125
126
 
126
127
  An app sets only these tokens:
127
128
 
128
- - the 27 color tokens in the [table below](#color-tokens),
129
- - the five shadow tokens in [Depth](depth.md#shadow-tokens),
129
+ - the 28 color tokens in the [table below](#color-tokens),
130
130
  - the three font tokens in [Typography](typography.md#fonts): `font-sans`, `font-display` and `font-mono`.
131
131
 
132
132
  Every other token (space, radius, sizes, layout widths, layers, motion) and every `mu-*` class rule is internal. Read those tokens in app CSS (`var(--mu-space-4)`), but don't override them, even though the cascade layer lets an app rule win: the products would drift apart, and the package's tests and screenshots cover only its own values. To change one element, pass `className` or `style` to that component (see [API conventions](building.md#api-conventions)).
@@ -135,19 +135,19 @@ Internal rules are not private classes. An app applies the [public classes](cust
135
135
 
136
136
  ### App themes
137
137
 
138
- An app can offer more themes than the two the package ships, such as a warm Paper theme in memos. An app theme is a rule that sets the color and shadow tokens under its own `data-theme` value:
138
+ An app can offer more themes than the two the package ships, such as a warm Paper theme in memos. An app theme is a rule that sets the color tokens under its own `data-theme` value:
139
139
 
140
140
  ```css
141
141
  [data-theme="paper"] {
142
142
  color-scheme: light;
143
143
  --mu-canvas: #f5ede4;
144
144
  --mu-surface: #fbf6f0;
145
- /* every other color token, and the five shadow tokens */
145
+ /* every other color token */
146
146
  }
147
147
  ```
148
148
 
149
- - Set every color token in the table below and the five shadow tokens in [Depth](depth.md). A token the theme leaves out keeps its Light value, so a partial theme mixes two palettes.
150
- - The package exports `themeTokens`, the custom property names of those 32 tokens (`--mu-canvas` to `--mu-shadow-pop`). Check in the app's tests that its theme sets each one:
149
+ - Set every color token in the table below. A token the theme leaves out keeps its Light value, so a partial theme mixes two palettes.
150
+ - The package exports `themeTokens`, the custom property names of those 28 tokens (`--mu-canvas` to `--mu-on-danger`). Check in the app's tests that its theme sets each one (Vitest reads a stylesheet with `?raw` only when `test.css` is `true` in its config; without it the file comes back empty):
151
151
 
152
152
  ```ts
153
153
  import { themeTokens } from "@usememos/ui";
@@ -168,23 +168,24 @@ An app can offer more themes than the two the package ships, such as a warm Pape
168
168
  | --- | --- | --- | --- |
169
169
  | `canvas` | `#fafafa` | `#0f0f0f` | Page background |
170
170
  | `surface` | `#ffffff` | `#1a1a1a` | One lightness step above canvas: inputs, menus, popovers, dialogs, toasts, table headers |
171
- | `well` | `#e4e4e4` | `#252525` | Recessed parts that hold a state: switch, tab, checkbox and radio wells, pressed toggles, inline code. Paired with `edge` and `shadow-track`. Not for text entry |
171
+ | `well` | `#e4e4e4` | `#252525` | Flat parts that hold a state: switch, tab, checkbox and radio wells, pressed toggles, inline code. Paired with `control-edge`. Not for enabled text entry |
172
172
  | `hover` | `#ececec` | `#252525` | Hover on rows, ghost buttons and wells; the highlighted row in menus and selects |
173
- | `key` | `#ffffff` | `#343434` | Raised parts you press: neutral buttons, select triggers, the selected tab, switch knobs, kbd. Paired with `shadow-key` or `shadow-raised` |
173
+ | `key` | `#ffffff` | `#343434` | Controls and selected parts: neutral buttons, select triggers, the selected tab, switch knobs, kbd. Flat, with `control-edge` where a boundary is needed |
174
174
  | `key-hover` | `#fafafa` | `#3c3c3c` | Hover on a neutral key |
175
- | `key-sheen` | `rgba(255,255,255,0.18)` | `rgba(255,255,255,0.08)` | Top light on keys: a gradient from `key-sheen` at the top edge to clear at 60% of the height |
176
- | `edge` | `#d0d0d0` | `#343434` | 1px border of inputs and wells. About 1.5:1 on canvas |
175
+ | `edge` | `#d0d0d0` | `#343434` | Quiet borders of popups, badges and disabled controls |
176
+ | `control-edge` | `#909090` | `#777777` | Boundaries of fields, buttons and selection controls; at least 3:1 on `canvas` and `surface` |
177
177
  | `line` | `#dedede` | `#252525` | Separators, table rules and menu dividers. 1.25:1 in Light, so structural lines also need spacing or a heading |
178
178
  | `overlay` | `rgba(0,0,0,0.32)` | `rgba(0,0,0,0.6)` | Backdrop behind dialogs and sheets |
179
- | `ink` | `#1a1a1a` | `#ededed` | Primary text and icons on canvas, surface, well and key (13:1 or more in both themes) |
180
- | `ink-muted` | `#5e5e5e` | `#adadad` | Secondary text: descriptions, placeholders, unselected tabs and toggles, shortcuts, timestamps (4.9:1 or more on every surface) |
179
+ | `text` | `#1a1a1a` | `#ededed` | Primary text and icons on canvas, surface, well and key (13:1 or more in both themes) |
180
+ | `text-secondary` | `#5e5e5e` | `#adadad` | Secondary text: descriptions, placeholders, unselected tabs and toggles, shortcuts, timestamps (4.9:1 or more on every surface) |
181
+ | `text-disabled` | `#666666` | `#9e9e9e` | Disabled text and icons, at least 4.5:1 on `canvas`, `surface`, `well` and `key`; never fade the whole control |
181
182
  | `primary` | `#1a1a1a` | `#e6e6e6` | The one emphasized action and every on state: primary buttons, checked boxes and radios, switches when on, tooltips. Ink, not a hue |
182
183
  | `primary-hover` | `#333333` | `#ffffff` | Hover on a primary key |
183
184
  | `on-primary` | `#ffffff` | `#0f0f0f` | Text, icons and the switch knob on `primary` (16:1 Light, 15:1 Dark) |
184
185
  | `accent` | `oklch(0.5 0.13 255)` | `oklch(0.78 0.11 255)` | The only hue at rest: links, the text cursor and selection. 5.8:1 on canvas in Light. Never a fill |
185
186
  | `selection` | `rgba(40,99,171,0.18)` | `rgba(135,186,253,0.28)` | Text selection and the selected range in an editor |
186
- | `focus` | `var(--mu-primary)` | `var(--mu-primary)` | Keyboard focus ring: 2px outline, 1px offset, on `:focus-visible` only |
187
- | `focus-edge` | `var(--mu-ink)` | `var(--mu-ink)` | Edge of a focused field (Input, Textarea, Combobox): 1.5px on any focus, in place of the 1px `edge` |
187
+ | `focus` | `var(--mu-primary)` | `var(--mu-primary)` | 2px keyboard focus border, on `:focus-visible` only; contrasting text color on filled controls |
188
+ | `focus-edge` | `var(--mu-text)` | `var(--mu-text)` | Edge of a focused field (Input, Textarea, Combobox): 2px on any focus, in place of the 1px `control-edge` |
188
189
  | `success` | `oklch(0.52 0.13 150)` | `oklch(0.78 0.14 150)` | Success text and icons on `surface` or `success-soft` (4.6:1 or more). Always with a word or icon |
189
190
  | `success-soft` | `#e9f4ec` | `#14261a` | Background of success badges and toasts |
190
191
  | `warning` | `oklch(0.5 0.11 70)` | `oklch(0.8 0.13 75)` | Warning text and icons on `surface` or `warning-soft` (5.5:1 or more) |
@@ -194,71 +195,43 @@ An app can offer more themes than the two the package ships, such as a warm Pape
194
195
  | `danger-soft` | `#fbeaea` | `#2c1616` | Background of danger badges and error toasts |
195
196
  | `on-danger` | `#ffffff` | `#ffffff` | Text on a danger key |
196
197
 
197
- Every text and background pair in this table keeps at least 4.5:1 in both themes. Depth (see [Depth](depth.md)) is added on top and never lowers contrast.
198
+ Every text and background pair in this table keeps at least 4.5:1 in both themes. [Surface borders](depth.md) never lower text contrast.
198
199
 
199
200
  <!-- docs/depth.md -->
200
201
 
201
- # Depth
202
+ # Surfaces and boundaries
202
203
 
203
- The default taste is "Light, tactile": restrained depth where depth shows behavior. Recessed parts hold a value. Raised parts can be pressed. Popups float. Everything else, including text fields and content, is flat.
204
+ The interface is flat. Fills identify controls and state; borders separate adjacent or overlapping surfaces. No component uses shadows, key sheen, focus rings or visible CSS outlines.
204
205
 
205
206
  ## Roles
206
207
 
207
- | Role | Class | Used for | Background | Shadow |
208
- | --- | --- | --- | --- | --- |
209
- | Recessed | `mu-recessed` | Switch, tab, checkbox and radio wells, pressed toggles | `well` with a 1px `edge` | `shadow-track`, a soft inner shadow from the top |
210
- | Key in a well | `mu-key` | The selected tab or segment, switch knobs | `key` with the `key-sheen` gradient | `shadow-key` |
211
- | Raised | `mu-raised` | Buttons, select triggers, kbd | `key` or `primary` with `key-sheen` | `shadow-raised`; `shadow-pressed` while held |
212
- | Popup | `mu-popup` | Menus, popovers, dialogs, sheets, toasts | `surface` | `shadow-pop` |
213
- | Flat | none | Inputs and textareas, checked boxes and radios, content | `surface` with `edge`, or `primary` | None |
214
-
215
- Each depth class applies a whole role in one class. Use them for new parts before writing new shadows.
208
+ | Role | Class | Used for | Treatment |
209
+ | --- | --- | --- | --- |
210
+ | Well | `mu-well` | Switch tracks, tab groups, unchecked boxes and radios, pressed toggles | `well` with a 1px `control-edge` |
211
+ | Key | `mu-key` | Neutral buttons, select triggers, selected tabs, switch knobs, kbd | `key` with a 1px `control-edge` where a boundary is needed |
212
+ | Popup | `mu-popup` | Menus, popovers, dialogs and toasts | `surface`, a 1px `edge` and `radius-xl` |
213
+ | Content | none | Text fields and content | Fields use `surface` and `control-edge`; content has no enclosing box |
216
214
 
217
- ```css
218
- .mu-recessed { background-color: var(--mu-well); box-shadow: inset 0 0 0 1px var(--mu-edge), var(--mu-shadow-track); }
219
- .mu-raised { background-color: var(--mu-key); background-image: linear-gradient(180deg, var(--mu-key-sheen), rgba(255,255,255,0) 60%); box-shadow: var(--mu-shadow-raised); }
220
- .mu-key { background-color: var(--mu-key); background-image: linear-gradient(180deg, var(--mu-key-sheen), rgba(255,255,255,0) 60%); box-shadow: var(--mu-shadow-key); }
221
- .mu-popup { background: var(--mu-surface); box-shadow: var(--mu-shadow-pop); border-radius: var(--mu-radius-xl); }
222
- ```
215
+ Use these classes before writing new surface styles. `mu-key` covers both a control and a selected choice; neither is raised. Dialogs use `radius-2xl`, and Drawers have an edge facing the page.
223
216
 
224
217
  ## Rules
225
218
 
226
- - Light always comes from the top.
227
- - Text entry is flat: an input is `surface` with an `edge`, never a gray well, so it never reads as disabled next to a key.
228
- - A checked box or radio holds a value, so it is a flat `primary` fill, not a raised key. Only things you press are raised.
229
- - **One rule for "selected".** A single choice among peers (tabs, a segmented filter) is a raised key sliding in a well. An on/off state (a toolbar toggle) is pressed into a well. Never mix the two in one row.
230
- - Content areas are flat. There is no card component: group content with spacing, a heading and a `Separator`.
231
- - No textures, no glass or blur behind text, and no neumorphism. Every fill and text pair keeps its contrast, and depth is added on top.
232
-
233
- ## Shadow tokens
234
-
235
- | Token | Usage |
236
- | --- | --- |
237
- | `shadow-track` | Recessed parts: wells and the faint top shadow inside inputs. The border comes from `edge` |
238
- | `shadow-key` | Keys inside a well: the switch knob, the selected tab or segment |
239
- | `shadow-raised` | Free-standing keys on canvas or surface: buttons, select triggers, kbd |
240
- | `shadow-pressed` | A key while held |
241
- | `shadow-pop` | Menus, selects, popovers, dialogs, sheets and toasts: the floating layer |
242
-
243
- ### Light
219
+ - No `box-shadow`, `text-shadow`, sheen gradients, focus rings or visible `outline`. `outline: none` or `0` only suppresses a browser outline when another visible focus treatment exists.
220
+ - Text entry keeps its `surface` fill on hover and focus. Enabled fields never use a gray well; disabled fields use `well` with `text-disabled` text.
221
+ - A checked checkbox or radio uses `primary` with an `on-primary` mark. A switch also moves its knob.
222
+ - A single choice among peers uses a contrasting selected fill in a well. An independent on/off toggle uses `well` and an `edge` when on.
223
+ - Floating surfaces have a border so they remain distinct over the same surface color. Modal backdrops still use `overlay`.
224
+ - Content is grouped by spacing, headings and separators. The boxed exceptions are List, bordered Table and bordered Form.
225
+ - Borders count toward the documented padding. Reserve their space and compensate inner padding so states never move text or change a control's size.
244
226
 
245
- ```css
246
- --mu-shadow-track: inset 0 1px 2px rgba(0,0,0,0.10);
247
- --mu-shadow-key: 0 1px 2px rgba(0,0,0,0.16), 0 2px 6px rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.9), inset 0 -1px 0 rgba(0,0,0,0.05);
248
- --mu-shadow-raised: inset 0 1px 0 rgba(255,255,255,0.25), inset 0 -1px 0 rgba(0,0,0,0.10), 0 0 0 0.5px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.12);
249
- --mu-shadow-pressed: inset 0 1px 3px rgba(0,0,0,0.22), 0 0 0 0.5px rgba(0,0,0,0.12);
250
- --mu-shadow-pop: 0 8px 24px rgba(0,0,0,0.10), 0 1px 3px rgba(0,0,0,0.08), 0 0 0 0.5px rgba(0,0,0,0.08), inset 0 1px 0 rgba(255,255,255,0.8);
251
- ```
252
-
253
- ### Dark
227
+ ## Focus
254
228
 
255
- ```css
256
- --mu-shadow-track: inset 0 1px 2px rgba(0,0,0,0.55), inset 0 -0.5px 0 rgba(255,255,255,0.06);
257
- --mu-shadow-key: 0 1px 2px rgba(0,0,0,0.6), 0 2px 6px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.10);
258
- --mu-shadow-raised: inset 0 1px 0 rgba(255,255,255,0.10), inset 0 -1px 0 rgba(0,0,0,0.25), 0 0 0 0.5px rgba(0,0,0,0.6), 0 1px 2px rgba(0,0,0,0.5);
259
- --mu-shadow-pressed: inset 0 1px 3px rgba(0,0,0,0.6), 0 0 0 0.5px rgba(0,0,0,0.6);
260
- --mu-shadow-pop: 0 8px 24px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.06), inset 0 1px 0 rgba(255,255,255,0.06);
261
- ```
229
+ - `mu-focus` thickens the existing border from 1px to `focus-edge-width` on `:focus-visible`. It provides the internal `--mu-control-border-width` variable. A custom control reads that variable for its border and subtracts it from its padding, so its text and size stay fixed. Text-only links use an underline instead.
230
+ - The edge uses `focus` ink. On primary and checked fills it uses `on-primary`; danger buttons use `on-danger`, so the edge contrasts with the fill.
231
+ - Fields darken and thicken their own border from 1px to `focus-edge-width` on any focus. Invalid fields keep `danger` and thicken the same way.
232
+ - Text links use a double underline on keyboard focus, including links already underlined at rest.
233
+ - Menu items and options use their highlighted fill. Opening a popup never draws a focus boundary around the popup.
234
+ - In forced-colors mode, focused borders use the system `Highlight` color. Real borders remain visible without a shadow or outline fallback.
262
235
 
263
236
  <!-- docs/typography.md -->
264
237
 
@@ -266,14 +239,15 @@ Each depth class applies a whole role in one class. Use them for new parts befor
266
239
 
267
240
  ## Rules
268
241
 
269
- - Use few sizes and few weights. App text uses four sizes (13, 14, 16 and 20px) and two weights (400 and 500); 12px is only for the text of 20px parts (see `label-xs` and `caption-xs`). Hierarchy comes from text color, spacing and grouping first, then from weight, and from size last.
242
+ - Font decisions follow Apple's [typography guidelines](https://developer.apple.com/design/human-interface-guidelines/typography): the system font, no weight lighter than Regular, hierarchy from color, weight and size, no text under its 11pt minimum for iPhone, and no added letter spacing. The package follows these rules; it does not copy Apple's size tables, which differ per platform (13pt text on Mac, 17pt on iPhone). One set of sizes serves every device, and text does not grow on touch screens, except typed text in fields (see the last rule).
243
+ - Use few sizes and few weights. App text uses four sizes (13, 14, 16 and 20px) and two weights (400 and 500); 11px is only for the text of 20px parts (see `label-xs` and `caption-xs`). Hierarchy comes from text color, spacing and grouping first, then from weight, and from size last.
270
244
  - Weight 400 is for running text, values people typed or chose, and badge text (a badge's tone and fill carry its weight). Weight 500 is for labels, controls, titles and headings. The UI uses no 600 or 700. The one exception is [Prose](components/prose.md): bold text and headings that people wrote are 600, because 500 does not read as bold in system fonts.
271
245
  - 20px (`heading`) is for the page title only, once per page. 32px (`display`) is for the hero headline on a website only, once per page. Nothing else in the UI is larger than 16px, and 16px is only for `title` and Prose text. Headings that people write inside Prose have their own sizes (see [Prose](#prose)).
272
246
  - A section title inside a page or panel is `label` (14px, 500), set apart by space above it, not by a larger size. See [Page](patterns.md#page).
273
- - Secondary text (descriptions, helper text, metadata) is `caption` (13px) in `ink-muted`. No text is smaller than 13px, except `label-xs` and `caption-xs` inside 20px parts.
274
- - Control text scales with the control size: 12px at xs, 13px at sm, 14px at md.
247
+ - Secondary text (descriptions, helper text, metadata) is `caption` (13px) in `text-secondary`. No text is smaller than 13px, except `label-xs` and `caption-xs` inside 20px parts.
248
+ - Control text scales with the control size: 11px at xs, 13px at sm, 14px at md.
275
249
  - Reading text (`Prose`) is 16px on a 24px line. `sm` prose is 14px on a 22px line, for dense lists.
276
- - Letter spacing is 0, except −0.02em for `display`.
250
+ - Letter spacing is 0 for every style, `display` included. The system fonts set their own letter spacing at each size, so adding more makes text tighter or looser than the platform intends.
277
251
  - Use `font-variant-numeric: tabular-nums` wherever numbers change or line up: counts, times, tables.
278
252
  - Selected and unselected states never change weight, so labels don't shift.
279
253
  - Adding a size, a weight or a style needs a written reason in this page and the maintainer's sign-off.
@@ -293,32 +267,32 @@ The package ships no font files and loads no remote font: text uses the system U
293
267
 
294
268
  ## Scale
295
269
 
296
- Nine styles over six sizes. 12, 13 and 14px each have a 400 style for text and a 500 style for labels; the three headings are 500. Body text on a website is `body`, or `Prose` for long-form copy.
270
+ Nine styles over six sizes. 11, 13 and 14px each have a 400 style for text and a 500 style for labels; the three headings are 500. Body text on a website is `body`, or `Prose` for long-form copy.
297
271
 
298
272
  ### Text (`--mu-font-sans`)
299
273
 
300
274
  | Style | Size / line | Weight | Usage |
301
275
  | --- | --- | --- | --- |
302
- | `label-xs` | 12 / 16px | 500 | Labels inside xs controls and kbd: 20px parts you press. Never for running or secondary text. The reason for the size: xs controls must read as tiny, and at 13px they read as sm controls with less padding |
303
- | `caption-xs` | 12 / 16px | 400 | Text inside badges: 20px labels. Never for running or secondary text. The reason for the style: a badge is a label, not a heading, and at 500 every tag and status read heavier than the 13px `caption` metadata around it. Badges get their hierarchy from tone and fill |
304
- | `caption` | 13 / 18px | 400 | Secondary text: field descriptions and errors, metadata, timestamps, toast and popover descriptions, table captions, sm table cells. Usually in `ink-muted` |
276
+ | `label-xs` | 11 / 16px | 500 | Labels inside xs controls and kbd: 20px parts you press. Never for running or secondary text. The reason for the size: xs parts must read as tiny. At 12px their text was 1px (8%) under the 13px `caption` beside it, a step people can't see, so an xs control read as an sm control with less padding. 11px is a visible step, and it matches the smallest sizes in platform guidelines (the 11pt minimum on iOS, the 11sp label in Material). Nothing is smaller |
277
+ | `caption-xs` | 11 / 16px | 400 | Text inside badges: 20px labels. Never for running or secondary text. The reason for the style: a badge is a label, not a heading, and at 500 every tag and status read heavier than the 13px `caption` metadata around it. Badges get their hierarchy from tone and fill |
278
+ | `caption` | 13 / 18px | 400 | Secondary text: field descriptions and errors, metadata, timestamps, toast and popover descriptions, table captions, sm table cells. Usually in `text-secondary` |
305
279
  | `label-sm` | 13 / 18px | 500 | Labels inside sm controls, tooltips, group labels in menus and lists, table heads |
306
280
  | `body` | 14 / 20px | 400 | Default running text in app UI; dialog descriptions; the names in rows people scan: Menu and Select items, settings row names, sidebar navigation rows |
307
281
  | `label` | 14 / 20px | 500 | Labels inside md controls, field labels, buttons, section titles, popover and toast titles. The default control text |
308
282
 
309
283
  ### Headings (`--mu-font-display`)
310
284
 
311
- | Style | Size / line | Weight | Letter spacing | Usage |
312
- | --- | --- | --- | --- | --- |
313
- | `title` | 16 / 24px | 500 | 0 | Dialog titles; empty-state titles |
314
- | `heading` | 20 / 28px | 500 | 0 | The page title in an app or on a website. One per page |
315
- | `display` | 32 / 40px | 500 | −0.02em | The hero headline on a website. Never in an app |
285
+ | Style | Size / line | Weight | Usage |
286
+ | --- | --- | --- | --- |
287
+ | `title` | 16 / 24px | 500 | Dialog titles; empty-state titles |
288
+ | `heading` | 20 / 28px | 500 | The page title in an app or on a website. One per page |
289
+ | `display` | 32 / 40px | 500 | The hero headline on a website. Never in an app |
316
290
 
317
- Each style is also a class with the `mu-` prefix (`.mu-caption`, `.mu-heading`) in `@usememos/ui/styles.css`, which sets the family, size, line height, weight and, where listed, letter spacing.
291
+ Each style is also a class with the `mu-` prefix (`.mu-caption`, `.mu-heading`) in `@usememos/ui/styles.css`, which sets the family, size, line height and weight.
318
292
 
319
293
  ### Removed styles
320
294
 
321
- The scale used to have 12 sizes and 3 weights. These styles were removed; use the replacement. (`label-xs` was 11px before it came back at 12px for the xs row.)
295
+ The scale used to have 12 sizes and 3 weights. These styles were removed; use the replacement. (`label-xs` and `caption-xs` were 12px until November 2026; 12px is no longer in the scale.)
322
296
 
323
297
  | Removed | Was | Use |
324
298
  | --- | --- | --- |
@@ -343,7 +317,7 @@ Every control takes one `size` prop: `xs`, `sm` or `md` (the default). There is
343
317
 
344
318
  | Size | Height | Padding | Icon | Icon gap | Text | Radius | Check box |
345
319
  | --- | --- | --- | --- | --- | --- | --- | --- |
346
- | xs | `control-xs` 20px | 6px | 12px | 4px | 12 / 16px | `radius-sm` 4px | 14px |
320
+ | xs | `control-xs` 20px | 6px | 12px | 4px | 11 / 16px | `radius-sm` 4px | 14px |
347
321
  | sm | `control-sm` 24px | 8px | 14px | 4px | 13 / 18px | `radius-sm` 4px | 16px |
348
322
  | md (default) | `control-md` 32px | 12px | 16px | 6px | 14 / 20px | `radius-md` 6px | 18px |
349
323
 
@@ -355,27 +329,70 @@ Every control takes one `size` prop: `xs`, `sm` or `md` (the default). There is
355
329
 
356
330
  ## Spacing
357
331
 
358
- Every spacing value comes from the 4px scale. The only exception is `inset` (2px), the gap between a well and the key inside it.
332
+ Layout spacing comes from one ladder of nine steps, and each step has a job. Two rules decide every gap:
359
333
 
360
- - 0 inside a well.
361
- - 2px (`inset`) between items inside a cluster (a toolbar).
362
- - 8px between controls.
363
- - 16px between fields.
364
- - 24px between groups.
334
+ - Closer means more related. A title is nearer to its own text than to the block before it.
335
+ - A container's padding is never smaller than the gap between its blocks.
365
336
 
366
- | Token | Value | Usage |
367
- | --- | --- | --- |
368
- | `space-1` | 4px | Gap between sm controls; icon to text in sm; the label-to-control gap in a field |
369
- | `space-2` | 8px | Gap between md controls; sm horizontal padding |
370
- | `space-3` | 12px | md horizontal padding |
371
- | `space-4` | 16px | Padding inside popovers and toasts; gap between fields; page padding on phones |
372
- | `space-5` | 20px | Padding inside dialogs; space above Prose headings |
373
- | `space-6` | 24px | Gap between sections or entries inside a page; page padding on desktop |
374
- | `space-8` | 32px | Gap between page regions |
375
- | `space-12` | 48px | Section rhythm on a website |
376
- | `space-16` | 64px | Hero and footer padding on a website |
377
-
378
- `space-8`, `space-12` and `space-16` are for an app's page layout. No component uses them.
337
+ | Token | Value | Job | Where |
338
+ | --- | --- | --- | --- |
339
+ | `space-1` | 4px | Parts of one thing | Label to control, title to description, control to hint; the gap between sm controls |
340
+ | `space-2` | 8px | Controls side by side; a title to its list | Buttons in a row, tags, a section title to its List, a Fieldset legend to its options, the lines of an entry |
341
+ | `space-3` | 12px | Rows in a set; blocks on a small surface | Options in a Fieldset or RadioGroup, blocks in a Popover or Toast |
342
+ | `space-4` | 16px | Blocks in a container | Fields in a form, two blocks of one section; padding of popovers and boxed forms; page padding on phones |
343
+ | `space-5` | 20px | Padding of big surfaces; above a Prose heading | Padding of dialogs and drawers; the space above a heading and around a rule in Prose. Nowhere else: between other things use 16 or 24 |
344
+ | `space-6` | 24px | Groups | Sections and entries on a page, content to its footer; page padding on desktop |
345
+ | `space-8` | 32px | Regions of a page | A page header band to the content, one column to the next; above and below an empty state |
346
+ | `space-12` | 48px | Website sections | The rhythm between sections of a marketing page |
347
+ | `space-16` | 64px | Website hero and footer | The padding of a hero or a footer |
348
+
349
+ - `inset` (2px) is not a layout step. It is the gap between a well and the key inside it, and between items inside one cluster (a toolbar). Inside a well the gap is 0.
350
+ - Inside a control the size row sets the spacing (see [Control sizes](#control-sizes)): 6px between an md icon and its text, 6px of padding at xs and in a menu, 2px less padding on a control's icon side. These are optical values of the control, not layout steps. Don't use 6px or 10px between things.
351
+ - `space-8`, `space-12` and `space-16` are for an app's page layout. No component uses them.
352
+
353
+ Every surface follows the same two tiers. Big surfaces pad `space-5` and stack `space-4`; small ones pad `space-4` and stack `space-3`.
354
+
355
+ | Surface | Padding | Between blocks | Title to description | Content to footer |
356
+ | --- | --- | --- | --- | --- |
357
+ | Dialog, Drawer | 20px | 16px | 4px | 24px |
358
+ | Form, `bordered` | 16px | 16px | 4px | 16px, then a footer bar |
359
+ | Popover | 16px | 12px | 4px | |
360
+ | Toast | 12px, 16px at the start | 12px | 0: one block of text | |
361
+ | Fieldset | | 12px | 8px from the legend to its options | |
362
+ | Field | | 4px between label, control and hint | | |
363
+
364
+ The playground has three reference screens built only from these steps: a settings page, a timeline and an empty page (`#screen-settings`, `#screen-timeline` and `#screen-empty`, in `playground/screens.tsx`). The screenshot tests cover them, so a spacing change is judged on a whole screen, not only on one component. An app's pages copy their spacing.
365
+
366
+ ### Spacing in an app
367
+
368
+ An app's own layout uses the same steps for the same jobs.
369
+
370
+ - In CSS, read the token: `gap: var(--mu-space-4)`.
371
+ - In Tailwind CSS, the class number is the token number, so `gap-4` is `space-4` (16px). The ladder is `1`, `2`, `3`, `4`, `5`, `6`, `8`, `12` and `16`. `0` and `px` are fine, and `gap-0.5` (2px) is `inset`, for the items of one cluster only.
372
+ - `p-1.5`, `p-2.5`, `p-7`, `p-10` and lengths such as `gap-[6px]` are not steps.
373
+ - Pick a step by its job, not by the nearest number. 6px between two buttons becomes `gap-2`, because they are controls side by side. 6px between a label and its field becomes `gap-1`, because they are parts of one thing.
374
+
375
+ The package exports the ladder, and a check for an app's tests:
376
+
377
+ - `spacingSteps` is the ladder in px: `[4, 8, 12, 16, 20, 24, 32, 48, 64]`.
378
+ - `offLadderSpacing(source)` returns the Tailwind CSS padding, margin and gap classes in a text whose value is not a step, as written: `["gap-1.5", "px-[10px]"]`. It reads the `p`, `m`, `gap` and `space` classes that have a number or a px or rem length, with or without a variant or a minus sign. It does not check widths, heights or positions, and it skips a value it cannot measure (`auto`, a variable, a `calc()`).
379
+
380
+ Run it over the app's source files:
381
+
382
+ ```ts
383
+ import { offLadderSpacing } from "@usememos/ui";
384
+
385
+ const sources = import.meta.glob<string>("/src/**/*.{ts,tsx,css}", { query: "?raw", import: "default", eager: true });
386
+
387
+ it("uses only ladder steps for padding, margin and gap", () => {
388
+ const off = Object.entries(sources).flatMap(([file, text]) => offLadderSpacing(text).map((name) => `${file}: ${name}`));
389
+ expect(off).toEqual([]);
390
+ });
391
+ ```
392
+
393
+ - Vitest reads a stylesheet with `?raw` only when `test.css` is `true` in its config. Without it every `.css` file comes back empty, so its `@apply` classes are skipped without an error.
394
+ - An app that is still moving to the ladder compares the count with a number, and lowers the number as it goes: `expect(off.length).toBeLessThanOrEqual(40)`.
395
+ - A real exception, such as padding that makes room for a side panel of a fixed width, is filtered out by name in the app's test, with a comment that says why.
379
396
 
380
397
  ## Radius
381
398
 
@@ -403,9 +420,7 @@ Buttons and inputs are never pills; badges always are, so a label is never mista
403
420
  | `control-compact` | 28px | md height inside `mu-compact`, for an app's sidebar; sm row height in menus, select lists and combobox lists |
404
421
  | `inset` | 2px | Well-to-key inset at every size |
405
422
  | `hit-area` | 44px | Minimum touch target on `pointer: coarse`, added invisibly around a control |
406
- | `focus-width` | 2px | Width of the keyboard focus ring |
407
- | `focus-offset` | 1px | Gap between a control's edge and its focus ring, so the ring stays visible on ink fills |
408
- | `focus-edge-width` | 1.5px | Edge width of a focused field |
423
+ | `focus-edge-width` | 2px | Edge width of a focused control or field |
409
424
  | `icon-xs` | 12px | Icons in xs controls and badges |
410
425
  | `icon-sm` | 14px | Icons in sm controls |
411
426
  | `icon-md` | 16px | Icons in md controls and rows |
@@ -457,17 +472,17 @@ Use these tokens for every `z-index`. Don't invent new layers in product code.
457
472
 
458
473
  # Motion
459
474
 
460
- Motion makes depth feel physical: keys sink when pressed, knobs and keys settle with a light spring, and popups fade in. It is quick and never decorative.
475
+ Motion confirms interaction: buttons respond when pressed, knobs and selections settle with a light spring, and popups fade in. It is quick and never decorative.
461
476
 
462
477
  ## Rules
463
478
 
464
- - **Press.** Keys move down 0.5px and switch to `shadow-pressed` over `duration-press` (80ms). Switch knobs grow about 10%. Checkboxes and radios shrink 6%.
479
+ - **Press.** Buttons move down 0.5px and change fill over `duration-press` (80ms). Switch knobs grow about 10%. Checkboxes and radios shrink 6%.
465
480
  - **Release and selection.** Knobs and the segment key slide over `duration-settle` (150ms) with `ease-settle`, which stops hard like a light spring. Content inside a control (a check mark, a value) fades in after the key stops.
466
481
  - **Popups.** Menus, popovers and collapsible content fade in and rise 4px over `duration-fade` (100ms). Dialogs do the same over `duration-settle`. Sheets slide up 24px.
467
482
  - **Tooltips** wait 400ms on hover before they open, and open at once on keyboard focus.
468
483
  - **Collapsible content** fades and rises in. There is no height animation, so long content never stutters.
469
484
  - **Spinners** turn once every 0.8s. **Skeletons** shimmer once every 1.4s.
470
- - **Reduced motion.** Under `prefers-reduced-motion`, keep color, opacity and shadow changes, and drop slides, scaling and shimmer.
485
+ - **Reduced motion.** Under `prefers-reduced-motion`, keep color and opacity changes, and drop slides, scaling and shimmer.
471
486
 
472
487
  ## Tokens
473
488
 
@@ -489,28 +504,28 @@ Every interactive component handles these states the same way.
489
504
 
490
505
  | State | Looks like |
491
506
  | --- | --- |
492
- | Hover | Keys go to `key-hover`. Rows, ghost buttons and wells go to `hover`. An input's edge goes to `ink-muted`. Muted labels go to `ink`. Primary keys go to `primary-hover` |
493
- | Pressed (while held) | Keys move down 0.5px and use `shadow-pressed`. A ghost button sinks into a `well` with `shadow-track` |
494
- | Pressed toggle (on) | Sinks into a `well` with an `edge` and `ink` text |
495
- | Selected (one of several) | A raised key in a well that slides to the chosen item |
507
+ | Hover | Keys go to `key-hover`. Rows, ghost buttons and wells go to `hover`. An input's edge goes to `text-secondary`. Muted labels go to `text`. Primary keys go to `primary-hover` |
508
+ | Pressed (while held) | Buttons move down 0.5px and change fill; neutral and ghost buttons use `well`, primary uses `primary-hover`, danger darkens its fill |
509
+ | Pressed toggle (on) | Uses `well` with an `edge` and the `text` color |
510
+ | Selected (one of several) | A flat `key` fill with an edge in a well that slides to the chosen item |
496
511
  | Checked | A flat `primary` fill with an `on-primary` mark |
497
- | Loading | The control keeps its size, hides its label and shows a Spinner. It is inert while loading |
498
- | Disabled | 45% opacity, `cursor: not-allowed`, and no hover or press response |
512
+ | Loading | The control becomes inert at once. After 300ms it hides its label and shows a Spinner without changing size; short waits keep the label |
513
+ | Disabled | `text-disabled` text, quiet borders and `well` on filled controls; no whole-control opacity, hover or press response |
499
514
  | Invalid | A `danger` edge on the field, plus an error line with an icon under it |
500
- | Focus (field) | The edge darkens from 1px `edge` to 1.5px `focus-edge` (ink), on click and keyboard alike. No ring around it. An invalid field's `danger` edge thickens the same way |
501
- | Focus (everything else) | A 2px ink `focus` ring 1px outside the edge, on `:focus-visible` (keyboard) only |
515
+ | Focus (field) | The edge darkens from 1px `control-edge` to 2px `focus-edge` (ink), on click and keyboard alike. No ring around it. An invalid field's `danger` edge thickens the same way |
516
+ | Focus (everything else) | The existing border thickens to 2px `focus` on `:focus-visible`; primary/checked fills use `on-primary`, danger buttons use `on-danger`; text links get a double underline |
502
517
  | Focus (inside a widget) | Menu items and options take the `hover` fill. No ring |
503
518
 
504
519
  ## Rules
505
520
 
506
521
  - Hover never changes size, weight or layout.
507
522
  - An input never changes its background on hover or focus, so it never looks disabled.
508
- - Focus rings show for keyboard focus only (`:focus-visible`), never on mouse click. Fields are the exception: a clicked field is focused for typing, so its edge darkens, but it still gets no ring.
509
- - Focus is ink, never blue, and never a ring floating away from the control.
523
+ - Control focus borders show for keyboard focus only (`:focus-visible`), never on mouse click. Fields are the exception: a clicked field is focused for typing, so its edge darkens, but it still gets no ring.
524
+ - Focus changes the existing edge, never adds a ring or visible outline. Filled controls use their contrasting text color at the edge.
510
525
  - Popups, dialogs and panels never show a focus ring when they open and take focus.
511
- - Every focus style survives forced-colors mode: rings are outlines, and fields add a transparent outline that the system paints.
526
+ - Focused borders use the system `Highlight` color in forced-colors mode. No shadow or outline fallback is needed.
512
527
  - Disabled controls stay in place. Prefer explaining why something is unavailable over hiding it.
513
- - Loading never shows a spinner for less than about 300ms; short waits pass silently.
528
+ - Button waits 300ms before showing its spinner. It ignores presses and sets `aria-busy` immediately, cancels the delay when loading ends, and removes the spinner as soon as work finishes. Other loading indicators follow the same delay policy.
514
529
 
515
530
  <!-- docs/icons.md -->
516
531
 
@@ -530,7 +545,7 @@ import { Pin, Trash2 } from "lucide-react";
530
545
  - **Icons are elements, not names.** Components take `icon={<Pin />}`, never `icon="pin"`.
531
546
  - **Size comes from the control.** An icon inside a component is 12px at xs, 14px at sm and 16px at md (`icon-xs`, `icon-sm`, `icon-md`). Don't pass `size`.
532
547
  - **One stroke, fixed in screen pixels.** Every icon, at every size and in badges, draws the `icon-stroke` token: 1.25px. The check in a checkbox is the one exception, at 2px. Apps don't change the token. At 1.25px a 16px outline glyph (calendar, paperclip, search) reads no heavier than the 14px weight-400 text beside it: an icon puts more ink in its box than a letter does, so matching the letters' stem width would read heavier. CSS applies `vector-effect: non-scaling-stroke`, which matches Lucide's `absoluteStrokeWidth`, so no props are needed. Don't pass `strokeWidth`.
533
- - **Color is `currentColor`.** Icons follow the text color: `ink` in controls, `ink-muted` in menu rows, the tone color in toasts.
548
+ - **Color is `currentColor`.** Icons follow the text color: `text` in controls, `text-secondary` in menu rows, the tone color in toasts.
534
549
  - **Icons sit in a fixed square.** In a menu row or a control, the icon square keeps labels aligned whether or not a row has an icon.
535
550
  - **Accessibility.** Icons are `aria-hidden`. Every icon-only Button or Toggle has an `aria-label`, and a Tooltip with the same text.
536
551
  - **Standalone icons.** Outside a component, give the icon a size class: `mu-icon-xs` (12px), `mu-icon-sm` (14px) or `mu-icon-md` (16px), all with the 1.25px stroke. Each sets the box and the stroke together, so a heading's icon or an empty state's icon matches the icons in controls. `mu-icon` alone takes the size of the nearest size row (`mu-s-*`), or 16px outside one. Don't size an icon with a utility such as `size-4`: it changes the box but not the stroke.
@@ -683,11 +698,11 @@ Wraps Base UI's [Button](https://base-ui.com/react/components/button): `disabled
683
698
 
684
699
  ## Style
685
700
 
686
- - A raised key: 20, 24 or 32px tall. `primary` is an ink key with `on-primary` text; `neutral` a `key` with a soft edge; `ghost` flat `ink-muted` with `hover` on hover; `danger` a `danger-key` with `on-danger` text.
701
+ - A flat control: 20, 24 or 32px tall. `primary` is an ink key with `on-primary` text; `neutral` a `key` with a `control-edge` boundary; `ghost` flat `text-secondary` with `hover` on hover; `danger` a `danger-key` with `on-danger` text.
687
702
  - Only an icon: square. The side with an icon (`icon` at the start, `iconEnd` at the end) has 2px less padding.
688
- - Press moves the key down 0.5px and swaps its shadow for `shadow-pressed`; a ghost button sinks into a `well`. Under reduced motion the key doesn't move; the shadow still changes.
689
- - `loading` keeps the width, shows a Spinner over the label, sets `aria-busy` and `aria-disabled`, ignores presses and stays focusable.
690
- - Disabled: 45% opacity.
703
+ - Press moves the control down 0.5px and changes its fill: `well` for neutral and ghost, `primary-hover` for primary, a darker fill for danger. Under reduced motion only the fill changes.
704
+ - `loading` immediately sets `aria-busy` and `aria-disabled`, ignores presses and stays focusable. The label stays visible for 300ms; if still loading, a Spinner replaces it without changing width. Finishing cancels the delay and restores the label immediately.
705
+ - Disabled: `text-disabled` text, a `well` fill and a quiet `edge`; ghost buttons keep their transparent fill and border. Opacity stays 1.
691
706
 
692
707
  <!-- docs/components/toggle.md -->
693
708
 
@@ -724,7 +739,7 @@ Wraps Base UI's [Toggle](https://base-ui.com/react/components/toggle): `pressed`
724
739
 
725
740
  ## Style
726
741
 
727
- - Off: flat `ink-muted`. On: sits in a `well` with `ink` text.
742
+ - Off: flat `text-secondary`. On: sits in a `well` with the `text` color.
728
743
  - A label shrinks with an ellipsis when its row runs out of room, as Select's value does; an icon-only toggle never shrinks. A ToggleGroup is at most as wide as its container, so long translated labels truncate instead of overflowing.
729
744
 
730
745
  <!-- docs/components/toggle-group.md -->
@@ -775,7 +790,7 @@ Base UI's props pass through: `value`, `defaultValue`, `onValueChange`, `multipl
775
790
  ## Rules
776
791
 
777
792
  - Give the group an `aria-label`.
778
- - A single choice among peers is a raised key in a well; an on/off state is a pressed toggle. Never mix the two in one row.
793
+ - A single choice among peers is a flat selected fill in a well; an on/off state is a pressed toggle. Never mix the two in one row.
779
794
  - Without `multiple`: filters and view switches that don't change the whole page. For sections of a page, use Tabs.
780
795
  - Base UI lets people press the chosen item to clear it. When one item must stay chosen, ignore an empty value, as in the example.
781
796
  - Arrow keys move focus between items; Space or Enter presses one.
@@ -845,7 +860,7 @@ interface Menu.PopupProps extends BaseMenu.Popup.Props {
845
860
  interface Menu.ItemProps extends BaseMenu.Item.Props {
846
861
  icon?: IconElement;
847
862
  shortcut?: React.ReactNode; // at the end of the row, "⌘ D"
848
- description?: React.ReactNode; // a line of ink-muted text under the label; the row grows to two lines
863
+ description?: React.ReactNode; // a line of text-secondary text under the label; the row grows to two lines
849
864
  danger?: boolean; // destructive: red text
850
865
  }
851
866
  // CheckboxItem and RadioItem take icon, shortcut and description too. SubmenuTrigger takes icon.
@@ -872,10 +887,10 @@ Base UI's props pass through: `open`, `defaultOpen`, `onOpenChange`, `modal` on
872
887
  ## Style
873
888
 
874
889
  - `mu-popup` with 6px padding (`radius-xl` − `radius-md`, so md highlights are concentric with the corners). As wide as its longest row, at least `menu-width` (160px) and at most `menu-max-width` (280px), both clamped to the space available; labels truncate and descriptions wrap at the maximum. Scrolls when taller than the space available.
875
- - Rows are 28px at sm (`control-compact`, with sm text and icons) and 32px at md, text weight 400; icons in `ink-muted`; the shortcut right-aligned in `ink-muted`. Text starts at the start edge.
876
- - A row with a description grows: the label, then the description in `caption` and `ink-muted`, with the row's vertical padding above and below. The icon and the check stay on the label's line.
890
+ - Rows are 28px at sm (`control-compact`, with sm text and icons) and 32px at md, text weight 400; icons in `text-secondary`; the shortcut right-aligned in `text-secondary`. Text starts at the start edge.
891
+ - A row with a description grows: the label, then the description in `caption` and `text-secondary`, with the row's vertical padding above and below. The icon and the check stay on the label's line.
877
892
  - A Separator has 6px above and below, the same as the popup padding, and runs edge to edge.
878
- - The highlighted row uses `hover` with the row's radius (`radius-sm` at sm, `radius-md` at md), and so does a row whose menu is open (a SubmenuTrigger, or a `menuClasses().item` row that is a `Menu.Trigger`). Disabled rows fade to 45%. `danger` rows use `danger`.
893
+ - The highlighted row uses `hover` with the row's radius (`radius-sm` at sm, `radius-md` at md), and so does a row whose menu is open (a SubmenuTrigger, or a `menuClasses().item` row that is a `Menu.Trigger`). Disabled rows and their icons use `text-disabled` without fading the whole row. `danger` rows use `danger`.
879
894
  - A ghost trigger keeps its `hover` background while the menu is open.
880
895
  - Fades in and rises `space-1` over `duration-fade`; no rise under reduced motion.
881
896
  - A menu below or above its trigger sits 4px from it. A submenu opens beside its trigger row, its own padding (6px) from the row. The row sits the same 6px inside the parent menu, so the submenu's edge touches the parent's edge.
@@ -918,7 +933,7 @@ Provide `href` and `children`.
918
933
 
919
934
  ## Style
920
935
 
921
- - `accent` text that underlines on hover: the one place the hue appears at rest. `muted` uses `ink-muted`.
936
+ - `accent` text that underlines on hover: the one place the hue appears at rest. `muted` uses `text-secondary`.
922
937
 
923
938
  <!-- docs/components/form.md -->
924
939
 
@@ -982,7 +997,7 @@ Base UI's props pass through: `onFormSubmit`, `errors`, `validationMode` and `ac
982
997
 
983
998
  ## Style
984
999
 
985
- - Children stack `space-4` apart. Title: `label` (14px, 500); Description: `caption` in `ink-muted`, `space-1` under the title. Footer: buttons `space-2` apart at the end.
1000
+ - Children stack `space-4` apart. Title: `label` (14px, 500); Description: `caption` in `text-secondary`, `space-1` under the title. Footer: buttons `space-2` apart at the end.
986
1001
  - `bordered`: `surface` fill, a 1px `line` edge, `radius-lg`, `space-4` padding, no shadow, as a List. The Footer runs to the edges as a `canvas` bar under a `line`, padded `space-3` × `space-4`, rounded with the box at the bottom.
987
1002
  - The form is always md: controls inside it ignore the size of a parent Root.
988
1003
 
@@ -1063,7 +1078,8 @@ Base UI's props pass through: `name`, `invalid`, `disabled`, `validate`, `valida
1063
1078
 
1064
1079
  - Label: `label` style (14px, 500), `space-1` above the control; weight 400 when it wraps a Checkbox, Switch or Radio.
1065
1080
  - Horizontal: a two-column grid. The label (weight 400, as every settings row name is; see [Page](../patterns.md#page)) and description fill the first column; the control sits at the end, centered on them, `space-4` from the text. The error spans the row under them.
1066
- - Description and error: `caption` (13px), below the control. The error hides the description and turns the control's edge `danger`.
1081
+ - Description and error: `caption` (13px), `space-1` below the control. The error hides the description and turns the control's edge `danger`.
1082
+ - Fieldset: its options stack `space-3` apart. The legend (`label`) sits `space-2` above the first one, closer than the options are to each other, so it reads as their name.
1067
1083
 
1068
1084
  <!-- docs/components/input.md -->
1069
1085
 
@@ -1105,9 +1121,9 @@ Wraps Base UI's [Input](https://base-ui.com/react/components/input), so inside a
1105
1121
 
1106
1122
  ## Style
1107
1123
 
1108
- - Flat `surface` with a 1px `edge` and a faint inner shadow from the top. Never a gray well, so it never reads as disabled.
1109
- - Typed text weight 400; placeholder `ink-muted`. On touch screens typed text is 16px at every size, so iOS Safari does not zoom in on focus; the field keeps its height.
1110
- - Hover darkens the edge to `ink-muted`; focus turns it into a 1.5px `focus-edge` (ink), on click and keyboard alike. There is no ring around the field. The background never changes.
1124
+ - Enabled fields use flat `surface` with a real 1px `control-edge` border. A gray well is reserved for the disabled state.
1125
+ - Typed text weight 400; placeholder `text-secondary`. On touch screens typed text is 16px at every size, so iOS Safari does not zoom in on focus; the field keeps its height.
1126
+ - Hover darkens the edge to `text-secondary`; focus turns it into a 2px `focus-edge` (ink), on click and keyboard alike. There is no ring around the field. The background stays fixed on hover and focus. Disabled fields use `well`, a quiet `edge` and `text-disabled` text.
1111
1127
  - The side with a prefix or suffix has 2px less padding.
1112
1128
 
1113
1129
  <!-- docs/components/textarea.md -->
@@ -1223,7 +1239,7 @@ Base UI's props pass through: `items`, `value`, `defaultValue`, `onValueChange`,
1223
1239
 
1224
1240
  ## Style
1225
1241
 
1226
- - Trigger: a neutral key ([Button](button.md) `neutral`), value left-aligned, `ChevronsUpDown` in `ink-muted`. `ghost`: a ghost Button with `ink` text, `hover` on hover and while open. Placeholder in `ink-muted`, weight 400. Invalid: a `danger` edge.
1242
+ - Trigger: a neutral key ([Button](button.md) `neutral`), value left-aligned, `ChevronsUpDown` in `text-secondary`. `ghost`: a ghost Button with the `text` color, `hover` on hover and while open. Placeholder in `text-secondary`, weight 400. Invalid: a `danger` edge.
1227
1243
  - List: `mu-popup`, 6px padding, at least `menu-width` and the trigger's width, at most `menu-max-width` or the trigger's width when that is wider. Rows like [Menu](menu.md) items: 28px at sm, 32px at md, highlight in the row's radius; the highlighted row uses `hover`.
1228
1244
  - Fades in and rises `space-1` over `duration-fade`; no rise under reduced motion.
1229
1245
 
@@ -1327,10 +1343,10 @@ Base UI's props pass through: `items`, `value`, `defaultValue`, `onValueChange`,
1327
1343
 
1328
1344
  ## Style
1329
1345
 
1330
- - The box is an [Input](input.md): flat `surface`, `edge`, and the focused edge on the box. The search icon and chevron are `ink-muted`; the chevron turns `ink` on hover.
1346
+ - The box is an [Input](input.md): flat `surface`, `edge`, and the focused edge on the box. The search icon and chevron are `text-secondary`; the chevron turns `text` on hover.
1331
1347
  - List: `mu-popup`, 6px padding, as wide as the input and at least `menu-width`. Rows like [Menu](menu.md) items: 28px at sm, 32px at md, highlight in the row's radius. The list is at most `list-height` (240px) tall, then it scrolls.
1332
1348
  - Rows are text weight 400; the highlighted row uses `hover`; the chosen row has a check at the end.
1333
- - Empty text: `body` in `ink-muted`, `space-3` by `space-4` padding.
1349
+ - Empty text: `body` in `text-secondary`, `space-3` by `space-4` padding.
1334
1350
  - Grid cells: square, a control height, `inset` apart, `row-radius`; the highlighted cell uses `hover`, the chosen cell sits in a `well` like a pressed Toggle. No check.
1335
1351
  - Fades in and rises `space-1` over `duration-fade`; no rise under reduced motion.
1336
1352
 
@@ -1370,7 +1386,7 @@ Wraps Base UI's [Checkbox](https://base-ui.com/react/components/checkbox) root w
1370
1386
 
1371
1387
  ## Style
1372
1388
 
1373
- - Unchecked: an empty well with an edge. Checked: a flat `primary` square (holds a value, so not raised).
1389
+ - Unchecked: an empty well with an edge. Checked: a flat `primary` square with a contrasting mark.
1374
1390
  - Lucide `Check` at a 2px stroke; `Minus` when indeterminate. It fades and scales in after the fill.
1375
1391
  - Shrinks 6% while pressed.
1376
1392
 
@@ -1411,7 +1427,7 @@ Wraps Base UI's [Switch](https://base-ui.com/react/components/switch) root with
1411
1427
 
1412
1428
  - A pill well with a round knob; the well fills with `primary` when on.
1413
1429
  - Height = checkbox box + 2px; width = height × 1.7.
1414
- - The knob is a `key` with `shadow-key`, inset 2px; `on-primary` when on. It grows 10% on press and slides with `ease-settle`. Under reduced motion it doesn't slide or grow.
1430
+ - The knob is a flat `key`, inset 2px; `on-primary` when on. It grows 10% on press and slides with `ease-settle`. Under reduced motion it doesn't slide or grow.
1415
1431
 
1416
1432
  <!-- docs/components/radio-group.md -->
1417
1433
 
@@ -1467,7 +1483,7 @@ Base UI's props pass through: `value`, `defaultValue`, `onValueChange`, `disable
1467
1483
  ## Style
1468
1484
 
1469
1485
  - Each radio is a round well; the chosen one is a flat `primary` circle with an `on-primary` dot.
1470
- - Vertical: `space-3` apart. Horizontal: `space-4` apart, with the legend on its own line above.
1486
+ - Vertical: `space-3` apart. Horizontal: `space-4` apart, with the legend on its own line above. The legend sits `space-2` above the radios.
1471
1487
 
1472
1488
  <!-- docs/components/tabs.md -->
1473
1489
 
@@ -1523,7 +1539,7 @@ Base UI's props pass through: `value`, `defaultValue`, `onValueChange` on the ro
1523
1539
 
1524
1540
  - Shares the segmented well and sliding key with a single-choice ToggleGroup.
1525
1541
  - Tabs are sized to their labels. The key re-measures when a label changes width.
1526
- - Selected and hovered labels `ink`, others `ink-muted`. Weight never changes, so labels don't shift.
1542
+ - Selected and hovered labels `text`, others `text-secondary`. Weight never changes, so labels don't shift.
1527
1543
 
1528
1544
  <!-- docs/components/collapsible.md -->
1529
1545
 
@@ -1570,7 +1586,7 @@ Base UI's props pass through: `open`, `defaultOpen`, `onOpenChange`, `disabled`
1570
1586
  ## Style
1571
1587
 
1572
1588
  - The trigger is a ghost key that bleeds `px` to the left, so its label lines up with the content below.
1573
- - The chevron is `ink-muted` and turns 180° over `duration-settle` with `ease-settle`.
1589
+ - The chevron is `text-secondary` and turns 180° over `duration-settle` with `ease-settle`.
1574
1590
  - The panel fades in and rises `space-1` over `duration-fade`. Its height never animates. No rise or turn under reduced motion.
1575
1591
 
1576
1592
  <!-- docs/components/dialog.md -->
@@ -1655,8 +1671,8 @@ Base UI's props pass through: `open`, `defaultOpen`, `onOpenChange`, `modal`, `d
1655
1671
 
1656
1672
  ## Style
1657
1673
 
1658
- - `mu-popup` with `radius-2xl`, `space-5` padding, `dialog-width` wide; children stack `space-4` apart. The backdrop is `overlay`.
1659
- - Title: `title`. Description: `body` in `ink-muted`, `space-2` under the title. The X is a small ghost Button at `space-4` from the corner.
1674
+ - `mu-popup` with `radius-2xl`, `space-5` padding, `dialog-width` wide; children stack `space-4` apart, and the Footer sits `space-6` under them. The backdrop is `overlay`.
1675
+ - Title: `title`. Description: `body` in `text-secondary`, `space-1` under the title, so the two read as one header. The X is a small ghost Button at `space-4` from the corner.
1660
1676
  - Fades in and rises `space-1` over `duration-settle`. A sheet slides up `space-6`, with square bottom corners and full-width buttons stacked with the action on top. No movement under reduced motion.
1661
1677
  - Long content scrolls the viewport around the panel.
1662
1678
  - `bare`: no padding, no gap and no X; the content is clipped to the radius. Set the width with `className` when it differs from `dialog-width`.
@@ -1720,8 +1736,8 @@ Base UI's props pass through: `open`, `defaultOpen`, `onOpenChange`, `onOpenChan
1720
1736
 
1721
1737
  ## Style
1722
1738
 
1723
- - `surface` with `shadow-pop`, full height and `drawer-width` (320px) wide from the start or end edge, with a 1px `line` edge on its inner side (the side facing the page), as a fixed sidebar has, at most the screen width minus `space-12`. Square corners; from the bottom it is full width with `radius-2xl` top corners and at most the screen height minus `space-6`.
1724
- - `panel`: content `space-5` in, stacked `space-4` apart; the X is a small ghost Button at `space-4` from the top end corner. Title: `title`. Description: `body` in `ink-muted`.
1739
+ - Flat `surface`, full height and `drawer-width` (320px) wide from the start or end edge, with a 1px `line` edge on its inner side (the side facing the page), as a fixed sidebar has, at most the screen width minus `space-12`. Square corners; from the bottom it has a 1px `edge` at the top and is full width with `radius-2xl` top corners and at most the screen height minus `space-6`.
1740
+ - `panel`: content `space-5` in, stacked `space-4` apart, as in a Dialog; the X is a small ghost Button at `space-4` from the top end corner. Title: `title`. Description: `body` in `text-secondary`, `space-1` under the title.
1725
1741
  - `bare`: no padding, gap or X. A visually hidden `Drawer.Close` named `closeLabel` follows the content, so screen readers on touch screens can close it.
1726
1742
  - The backdrop is `overlay`; it fades as the drawer is swiped away.
1727
1743
  - Slides in from its edge over `duration-settle` with `ease-settle` and follows the finger while swiped. Under reduced motion it fades instead.
@@ -1784,8 +1800,8 @@ Base UI's props pass through: `open`, `defaultOpen`, `onOpenChange`, `modal` on
1784
1800
 
1785
1801
  ## Style
1786
1802
 
1787
- - `mu-popup` (`surface`, `shadow-pop`, `radius-xl`), `space-4` padding, `popover-width` wide. Children stack `space-3` apart.
1788
- - Title: `label`. Description: `caption` in `ink-muted`, `space-1` under the title.
1803
+ - `mu-popup` (`surface`, a 1px `edge`, `radius-xl`), `space-4` padding, `popover-width` wide. Children stack `space-3` apart.
1804
+ - Title: `label`. Description: `caption` in `text-secondary`, `space-1` under the title.
1789
1805
  - `list`: 6px padding and no gap, like a Menu. A title and description inside line up with the rows' text, with `space-2` above the title and below the description.
1790
1806
  - Fades in and rises `space-1` over `duration-fade`; no rise under reduced motion.
1791
1807
 
@@ -1903,7 +1919,7 @@ Base UI's props pass through: `timeout` (default 5000ms) and `limit` (default 3)
1903
1919
  ## Style
1904
1920
 
1905
1921
  - `mu-popup`, `toast-width` wide, top-right at `space-6` from the edges; below `breakpoint-sm`, full width across the top at `space-4`, below the status bar.
1906
- - A tone icon (`Info`, `CircleCheck`, `TriangleAlert`, `CircleAlert`) in `ink-muted`, `success`, `warning` or `danger`; title in `label`; description in `caption` `ink-muted`; the action as a small neutral Button; the X as a small ghost Button.
1922
+ - A tone icon (`Info`, `CircleCheck`, `TriangleAlert`, `CircleAlert`) in `text-secondary`, `success`, `warning` or `danger`; title in `label`; description in `caption` `text-secondary`; the action as a small neutral Button; the X as a small ghost Button.
1907
1923
  - Newest in front; the ones behind peek out `space-2` below and shrink. A toast behind hides its text at once and gets a 1px `edge`, so its text never shows below a shorter front toast and the peeks stay visible in Dark. Hover or focus fans them out downward, `space-3` apart. Toasts slide in from above and can be swiped up or to the right. Under reduced motion they only fade.
1908
1924
 
1909
1925
  <!-- docs/components/avatar.md -->
@@ -1948,7 +1964,7 @@ interface Avatar.RootProps extends BaseAvatar.Root.Props {
1948
1964
 
1949
1965
  ## Style
1950
1966
 
1951
- - A circle (one of three round shapes, with radios and switches). Initials in `ink-muted` on `well`.
1967
+ - A circle (one of three round shapes, with radios and switches). Initials in `text-secondary` on `well`.
1952
1968
 
1953
1969
  <!-- docs/components/badge.md -->
1954
1970
 
@@ -1986,8 +2002,8 @@ function badgeClasses(options?: { tone?: Tone | "outline" | "primary" }): string
1986
2002
  ## Rules
1987
2003
 
1988
2004
  - Tags: `neutral` with a `Hash` icon. Visibility: `outline` with its icon. Status: `success`, `warning` or `danger`, always with a word, never color alone. A label that should stand out, such as New on a feature: `primary`, in ink, the system's one emphasis. Use it for one badge in a view at most, and never for a status.
1989
- - A tag in an entry's footer, or anywhere outside running text, is a `neutral` Badge with a `Hash` icon, in `ink`: it is the user's own word, not secondary text, so it is never muted. A tag inside text is a Prose tag (see [Prose](prose.md)).
1990
- - When people can give a tag a color, pass it as `color`. The color tints the fill; the text and the icon stay `ink`, so every color the user can pick stays readable in both themes. Never set the text or the icon in the user's color: on its own tint it falls below 4.5:1 for most colors in one theme or the other. A tag without a color keeps `well`.
2005
+ - A tag in an entry's footer, or anywhere outside running text, is a `neutral` Badge with a `Hash` icon, in `text`: it is the user's own word, not secondary text, so it is never muted. A tag inside text is a Prose tag (see [Prose](prose.md)).
2006
+ - When people can give a tag a color, pass it as `color`. The color tints the fill; the text and the icon stay `text`, so every color the user can pick stays readable in both themes. Never set the text or the icon in the user's color: on its own tint it falls below 4.5:1 for most colors in one theme or the other. A tag without a color keeps `well`.
1991
2007
  - `color` is for colors people choose. The package's own meanings use tones: status is `success`, `warning` or `danger`, emphasis is `primary`.
1992
2008
  - A badge is not a button. A clickable tag filter is a [Toggle](toggle.md).
1993
2009
  - A badge is round and a control is square. Kbd and Toggle stay square.
@@ -1996,10 +2012,10 @@ function badgeClasses(options?: { tone?: Tone | "outline" | "primary" }): string
1996
2012
 
1997
2013
  ## Style
1998
2014
 
1999
- - `control-xs` (20px) tall, `radius-full` (a pill: round marks a label, square marks a control), `caption-xs` text (12px, 400), optional `icon-xs` (12px) icon at a 1.25px stroke. It lines up with xs controls.
2015
+ - `control-xs` (20px) tall, `radius-full` (a pill: round marks a label, square marks a control), `caption-xs` text (11px, 400), optional `icon-xs` (12px) icon at a 1.25px stroke. It lines up with xs controls.
2000
2016
  - Inline padding is the xs row's `--mu-row-padding` plus 2px (half of `space-1`) on each side, so the text clears the round ends. The gap between icon and text is `space-1`.
2001
- - Regular weight, because a badge is a label, not a heading: hierarchy among badges comes from tone and fill, not weight. `outline` text is `ink-muted`, 5.1:1 or more on `canvas`, `surface` and `well` in both themes.
2002
- - `neutral`: `ink` on `well`. With `color`, the fill is an 18% tint of that color over the background, the same amount in both themes, and the text and icon stay `ink`. The color's lightness is first kept between 50% and 75% (OKLCH), so an ink-like color (black in Dark, white in Light) still gives a visible gray chip instead of vanishing into the background; hues in that range are unchanged. The prop sets `--mu-badge-color` on the badge; an app's own element with `badgeClasses()` sets that variable itself (see [Class functions](../custom-components.md#class-functions)). `primary`: `on-primary` on `primary`, the colors of a primary Button. Status tones: their `-soft` background with full-strength text.
2017
+ - Regular weight, because a badge is a label, not a heading: hierarchy among badges comes from tone and fill, not weight. `outline` text is `text-secondary`, 5.1:1 or more on `canvas`, `surface` and `well` in both themes.
2018
+ - `neutral`: `text` on `well`. With `color`, the fill is an 18% tint of that color over the background, the same amount in both themes, and the text and icon stay `text`. The color's lightness is first kept between 50% and 75% (OKLCH), so an ink-like color (black in Dark, white in Light) still gives a visible gray chip instead of vanishing into the background; hues in that range are unchanged. The prop sets `--mu-badge-color` on the badge; an app's own element with `badgeClasses()` sets that variable itself (see [Class functions](../custom-components.md#class-functions)). `primary`: `on-primary` on `primary`, the colors of a primary Button. Status tones: their `-soft` background with full-strength text.
2003
2019
 
2004
2020
  <!-- docs/components/kbd.md -->
2005
2021
 
@@ -2033,7 +2049,7 @@ Renders a `<kbd>`.
2033
2049
 
2034
2050
  ## Style
2035
2051
 
2036
- - A tiny raised key, `control-xs` (20px) tall, `label-xs` text (12px) in `ink-muted`.
2052
+ - A tiny flat key with an `edge` border, `control-xs` (20px) tall, `label-xs` text (11px) in `text-secondary`.
2037
2053
 
2038
2054
  <!-- docs/components/separator.md -->
2039
2055
 
@@ -2210,7 +2226,7 @@ interface Table.HeadProps extends React.ComponentProps<"th"> {
2210
2226
  }
2211
2227
  interface Table.CellProps extends React.ComponentProps<"td"> {
2212
2228
  num?: boolean; // right-aligned
2213
- muted?: boolean; // ink-muted, for secondary values such as dates
2229
+ muted?: boolean; // text-secondary, for secondary values such as dates
2214
2230
  }
2215
2231
  ```
2216
2232
 
@@ -2239,9 +2255,9 @@ Every native attribute passes through.
2239
2255
 
2240
2256
  - No stripes: a `line` under the head row and between body rows. Body rows highlight with `hover`.
2241
2257
  - `bordered`: `surface` fill, a 1px `line` edge and `radius-lg` on the scrolling wrapper; the head row has a `canvas` fill, so it reads as the head inside the box; the Caption sits inside the edge, under a `line`. The outer columns pad `space-1` more, so the text lines up with a [List](list.md) row's.
2242
- - Heads: `label-sm` (13px, 500) in `ink-muted`, a control height tall.
2258
+ - Heads: `label-sm` (13px, 500) in `text-secondary`, a control height tall.
2243
2259
  - Cells: `caption` at sm and `body` at md, with tabular numbers. Rows are 34 or 40px; a Button in a cell, or a `Table.Actions` group of them (`space-1` apart), overlaps the padding, so rows keep that height.
2244
- - Cells pad `px` at the sides. Caption: `caption` in `ink-muted`, `space-2` above.
2260
+ - Cells pad `px` at the sides. Caption: `caption` in `text-secondary`, `space-2` above.
2245
2261
 
2246
2262
  <!-- docs/components/list.md -->
2247
2263
 
@@ -2318,8 +2334,8 @@ Every native attribute passes through.
2318
2334
  ## Style
2319
2335
 
2320
2336
  - `surface` fill, a 1px `line` edge, `radius-lg`. No shadow: it is flat, like a field.
2321
- - Rows pad `space-3` × `space-4` at md and `space-2` × `space-3` at sm, divided by a 1px `line`. No hover on a plain row. An ItemButton fills its row, padded as the row is, lays out its children in a row `space-3` apart, highlights with `hover`, and rounds with the box at the first and last row; keyboard focus shows the ink ring.
2322
- - Text is `body` in `ink`.
2337
+ - Rows pad `space-3` × `space-4` at md and `space-2` × `space-3` at sm, divided by a 1px `line`. No hover on a plain row. An ItemButton fills its row, padded as the row is, lays out its children in a row `space-3` apart, highlights with `hover`, and rounds with the box at the first and last row; keyboard focus darkens the row's reserved border.
2338
+ - Text is `body` in `text`.
2323
2339
 
2324
2340
  <!-- docs/components/prose.md -->
2325
2341
 
@@ -2362,15 +2378,15 @@ Every native attribute passes through.
2362
2378
 
2363
2379
  ## Style
2364
2380
 
2365
- - Text: 16px on a 24px line (`sm`: 14 on 22), weight 400, `ink`. Paragraphs and list items use `text-wrap: pretty`, so a last line is rarely one word. Blocks are `space-3` apart.
2381
+ - Text: 16px on a 24px line (`sm`: 14 on 22), weight 400, `text`. Paragraphs and list items use `text-wrap: pretty`, so a last line is rarely one word. Blocks are `space-3` apart.
2366
2382
  - Headings: `h1` 24/32px, `h2` 20/28px, `h3`–`h6` 16/24px (`sm`: `h1` 16/24px, `h2`–`h6` 14/22px); all weight 600, balanced over their lines (`text-wrap: balance`), with `space-5` above and `space-2` below. Bold (`strong`, `b`) is 600 too. These are the only 600s in the system: people wrote them as bold, and 500 does not read as bold in system fonts.
2367
2383
  - Links: `accent`, underlined at rest, because color alone does not set them apart from the text.
2368
2384
  - Code: `--mu-font-mono` at 0.875em on `well`. Inline code has `radius-xs`; blocks have `radius-md`, `space-3` by `space-4` padding, and scroll sideways.
2369
- - Quotes: a 2px `edge` bar, text in `ink-muted`. Rules: a 1px `line` with `space-5` around it.
2370
- - Lists indent `space-6`; markers are `ink-muted`. Task checkboxes are 16px, drawn like [Checkbox](checkbox.md); done items go `ink-muted` with a strike-through.
2385
+ - Quotes: a 2px `edge` bar, text in `text-secondary`. Rules: a 1px `line` with `space-5` around it.
2386
+ - Lists indent `space-6`; markers are `text-secondary`. Task checkboxes are 16px, drawn like [Checkbox](checkbox.md); done items go `text-secondary` with a strike-through.
2371
2387
  - Images and video: never wider than the column, `radius-lg`.
2372
- - Tables follow [Table](table.md): `line` between rows, 13px `ink-muted` heads (weight 500), tabular numbers; they scroll sideways when too wide.
2373
- - Tags: `ink-muted` on `well`, `radius-xs`. They read as words, not badges.
2388
+ - Tables follow [Table](table.md): `line` between rows, 13px `text-secondary` heads (weight 500), tabular numbers; they scroll sideways when too wide.
2389
+ - Tags: `text-secondary` on `well`, `radius-xs`. They read as words, not badges.
2374
2390
  - Sizes are custom properties set on `.mu-prose` in the foundation (`--prose-fs`, `--prose-lh`, `--prose-h1`, `--prose-h2`, `--prose-h3` and their line heights).
2375
2391
 
2376
2392
  <!-- docs/usage.md -->
@@ -2384,7 +2400,7 @@ How to pick the right component for a screen and put components together. Read i
2384
2400
  1. Find the [pattern](patterns.md) the screen follows: page, form, entry, confirmation, loading, empty state, mobile. Follow it before choosing components.
2385
2401
  2. Pick each component with the guides below. When two seem to fit, the first row that matches wins.
2386
2402
  3. Import components from `@usememos/ui` and icons from `lucide-react`. Don't wrap a component to restyle it.
2387
- 4. Use `className` and `style` for layout only: width, margin, grid or flex placement. Don't change a component's color, height, padding, radius, shadow, font size or focus style. If the look is wrong, the component or its page is wrong; change the package, not the screen.
2403
+ 4. Use `className` and `style` for layout only: width, margin, grid or flex placement. Don't change a component's color, height, padding, radius, border, font size or focus style. If the look is wrong, the component or its page is wrong; change the package, not the screen.
2388
2404
  5. A part the package doesn't ship is built with [Building your own component](custom-components.md), never by copying a component's CSS.
2389
2405
 
2390
2406
  ## Picking something that floats
@@ -2664,19 +2680,21 @@ The package ships no navigation component: the sidebar's rows are the app's link
2664
2680
  align-items: center;
2665
2681
  gap: var(--mu-row-gap);
2666
2682
  height: var(--mu-row-height); /* 28px inside mu-compact, 32px without */
2667
- padding: 0 var(--mu-row-padding-icon);
2683
+ box-sizing: border-box;
2684
+ padding: 0 calc(var(--mu-row-padding-icon) - var(--mu-control-border-width));
2685
+ border: var(--mu-control-border-width) solid transparent;
2668
2686
  border-radius: var(--mu-row-radius);
2669
- color: var(--mu-ink-muted);
2687
+ color: var(--mu-text-secondary);
2670
2688
  font-weight: 400; /* mu-base sets 500, for control labels */
2671
2689
  text-decoration: none;
2672
2690
  }
2673
- .app-nav-row:hover, .app-nav-row[aria-current="page"] { color: var(--mu-ink); background-color: var(--mu-hover); }
2691
+ .app-nav-row:hover, .app-nav-row[aria-current="page"] { color: var(--mu-text); background-color: var(--mu-hover); }
2674
2692
  ```
2675
2693
 
2676
2694
  - `mu-compact` changes only md. sm (24px) and xs (20px) controls inside keep their heights, so an xs ghost Button for a row action still fits.
2677
2695
  - Popups opened from the sidebar (the Select's list, a Menu) mount outside it, at the normal 32px. To give a panel of rows the same 28px, put `mu-compact` on its popup: `<Popover.Popup variant="list" className="mu-compact">`.
2678
2696
  - `mu-s-md` gives the rows 14px text, 16px icons with a 1.25px stroke, and md padding and radius; `mu-base` sets the font. Never set the `--mu-row-*` variables or the size tokens yourself; `mu-compact` is the only way to change a row's height.
2679
- - Rows are weight 400 (`body`), like Menu items, Select items and settings row names. A column of rows is content people scan; at 500 every row competes and the sidebar reads as bold. 500 is for what names a group: a section title in the sidebar and the "New" Button's label. The current row stands out by its fill and `ink` color, not by weight, so the text doesn't shift when the current page changes.
2697
+ - Rows are weight 400 (`body`), like Menu items, Select items and settings row names. A column of rows is content people scan; at 500 every row competes and the sidebar reads as bold. 500 is for what names a group: a section title in the sidebar and the "New" Button's label. The current row stands out by its fill and `text` color, not by weight, so the text doesn't shift when the current page changes.
2680
2698
  - Mark the current page with `aria-current="page"`, not with color alone; the fill shows it too.
2681
2699
  - Rows stack `inset` (2px) apart. Don't add `mu-hit` to stacked rows: their 44px touch areas would overlap. 28px is above the 24px minimum target size.
2682
2700
  - A row with a count or a menu at its end adds it after the label with `margin-inline-start: auto`.
@@ -2697,7 +2715,7 @@ Controls over an image or a video sit in a dark-theme scope, so they read on any
2697
2715
  ```
2698
2716
 
2699
2717
  - `data-theme="dark"` on any element sets the Dark tokens for everything inside it, so the Buttons are dark `neutral` keys with light icons. Don't use `ghost` there: it has no fill and disappears on a busy picture.
2700
- - When the scope holds only Buttons, leave its own background transparent: the keys carry their own fill and shadow. Text in the scope, such as a counter or a file name, has no fill of its own and needs the pill in [Text over a photo](#text-over-a-photo).
2718
+ - When the scope holds only Buttons, leave its own background transparent: the keys carry their own fill and border. Text in the scope, such as a counter or a file name, has no fill of its own and needs the pill in [Text over a photo](#text-over-a-photo).
2701
2719
  - A full-screen viewer is a Dialog with `variant="bare" fullScreen`; put the scope inside its Popup.
2702
2720
 
2703
2721
  ### Text over a photo
@@ -2718,16 +2736,16 @@ Plain text over a picture, such as a viewer's "3 / 12" counter or its zoom level
2718
2736
  height: var(--mu-control-md);
2719
2737
  padding: 0 var(--mu-space-3);
2720
2738
  border-radius: var(--mu-radius-md);
2721
- color: var(--mu-ink);
2739
+ color: var(--mu-text);
2722
2740
  background-color: var(--mu-surface);
2723
- box-shadow: var(--mu-shadow-pop);
2741
+ border: 1px solid var(--mu-edge);
2724
2742
  font-variant-numeric: tabular-nums;
2725
2743
  }
2726
2744
  ```
2727
2745
 
2728
- - The pill is opaque, as tall as the Buttons beside it, with their radius. Dark `surface` with `ink` text keeps 4.5:1 on any picture.
2746
+ - The pill is opaque, as tall as the Buttons beside it, with their radius. Dark `surface` with the `text` color keeps 4.5:1 on any picture.
2729
2747
  - No blur and no translucent fill: glass behind text is not in the system (see [Depth](depth.md)), and it fails contrast on bright pictures.
2730
- - Several labels and Buttons in one row can share one pill instead: put the `surface`, radius, padding and shadow on the scope, and the Buttons become `ghost` inside it, since the pill is their fill.
2748
+ - Several labels and Buttons in one row can share one pill instead: put the `surface`, radius, padding and border on the scope, and the Buttons become `ghost` inside it, since the pill is their fill.
2731
2749
 
2732
2750
  ### Inside a shadow root or a host page
2733
2751
 
@@ -2813,6 +2831,7 @@ Run through it for every screen.
2813
2831
  | A tag in the user's color as text, or on a fill you mixed yourself | `<Badge color={tag.color}>`: a tint, with ink text |
2814
2832
  | Text over a photo on a translucent, blurred fill | An opaque `surface` pill in a `data-theme="dark"` scope |
2815
2833
  | Tailwind `size-4` on a standalone icon | `mu-icon-sm` or `mu-icon-md`, which set the stroke too |
2834
+ | `gap-1.5`, `px-2.5`, `py-7` or `gap-[6px]` between things | A step of the [spacing ladder](layout.md#spacing), picked by its job. `offLadderSpacing()` finds these in an app's tests |
2816
2835
  | A Spinner where a list will appear | Skeletons in the shape of the list |
2817
2836
  | `size="md"` or `"lg"` on a Button to emphasize it | `variant="primary"` at the row's size |
2818
2837
  | `size` on every part of a Menu or Select | `size` on its Root |
@@ -2869,16 +2888,16 @@ Names and parts from other libraries and where they go.
2869
2888
 
2870
2889
  # Patterns
2871
2890
 
2872
- How the components go together. Each product builds its screens from these patterns. None of them is specific to one product.
2891
+ How the components go together. Each product builds its screens from these patterns. None of them is specific to one product. The gaps come from the [spacing ladder](layout.md#spacing), and the playground shows a settings page, a timeline and an empty page built this way (`playground/screens.tsx`).
2873
2892
 
2874
2893
  ## Page
2875
2894
 
2876
2895
  - From `breakpoint-md` (1024px) up: a fixed sidebar of `sidebar-width` (240px). Below it, the sidebar is a [Drawer](components/drawer.md) from the start edge (`side="start"`, `variant="bare"`) opened from a ghost button.
2877
2896
  - A sidebar may use 28px rows: put `mu-compact` on it and every md control and row inside is `control-compact` (28px) tall, the package's (a ghost Select switcher, the "New" Button) and the app's own navigation rows alike. See [A sidebar navigation row](usage.md#a-sidebar-navigation-row). Without it, sidebar rows are md (32px).
2878
- - Sidebar navigation rows (pages, views, tags) are `body` text, weight 400, `ink-muted`, and the current row is `ink` on the `hover` fill. A sidebar section title ("Tags") is `label-sm` or `label`, 500, and the "New" Button keeps its 500 label. Rows are scanned content; weight is for the few things that name them.
2897
+ - Sidebar navigation rows (pages, views, tags) are `body` text, weight 400, `text-secondary`, and the current row is `text` on the `hover` fill. A sidebar section title ("Tags") is `label-sm` or `label`, 500, and the "New" Button keeps its 500 label. Rows are scanned content; weight is for the few things that name them.
2879
2898
  - Content sits in a column of at most `content-width` (720px), padded `space-4` on phones and `space-6` on desktop.
2880
- - A page header is a `heading` with an optional `body` line under it, and the page's one primary action at the right. `heading` appears once per page.
2881
- - Sections inside a page have a `label` title (14px, 500) with `space-6` above it and `space-2` below. Section titles are never larger than the text around them; space and grouping set them apart. A section's rows are horizontal Fields: the name at weight 400 (`body`) and the description in `caption`, `ink-muted`. The section title above is the only `label` (500); the row names stay 400 so the title stands out from them.
2899
+ - A page header is a `heading` with an optional `body` line `space-1` under it, and the page's one primary action at the right. `heading` appears once per page.
2900
+ - Sections inside a page have a `label` title (14px, 500) with `space-6` above it and `space-2` below. Section titles are never larger than the text around them; space and grouping set them apart. A section's rows are horizontal Fields: the name at weight 400 (`body`) and the description in `caption`, `text-secondary`. The section title above is the only `label` (500); the row names stay 400 so the title stands out from them. Two blocks of one section (a List and the Form that adds to it) are `space-4` apart.
2882
2901
 
2883
2902
  ## Form
2884
2903
 
@@ -2894,9 +2913,10 @@ How the components go together. Each product builds its screens from these patte
2894
2913
 
2895
2914
  A repeated unit of content in a list: a note, a comment, a changelog item.
2896
2915
 
2897
- - **Header line** in `caption` (`ink-muted`): who or when, visibility as an outline Badge, and a ghost Menu trigger at the right that appears on hover. On touch screens the menu trigger is always visible.
2916
+ - **Header line** in `caption` (`text-secondary`): who or when, visibility as an outline Badge, and a ghost Menu trigger at the right that appears on hover. On touch screens the menu trigger is always visible.
2898
2917
  - **Body** in `Prose`.
2899
- - **Footer line:** tags as `neutral` [Badge](components/badge.md)s with a `Hash` icon, in `ink` (a tag is the user's own word, not metadata); a tag's own color goes in the Badge's `color`. Then attachments and reactions. Prose tags are only for tags inside the body's text.
2918
+ - **Footer line:** tags as `neutral` [Badge](components/badge.md)s with a `Hash` icon, in `text` (a tag is the user's own word, not metadata); a tag's own color goes in the Badge's `color`. Then attachments and reactions. Prose tags are only for tags inside the body's text.
2919
+ - The three lines of an entry are `space-2` apart.
2900
2920
  - Entries are separated by `space-6` of whitespace or a `Separator`, never by boxes.
2901
2921
 
2902
2922
  ## Confirmation
@@ -2910,7 +2930,7 @@ A repeated unit of content in a list: a note, a comment, a changelog item.
2910
2930
 
2911
2931
  - A list or page that is loading shows Skeletons in the shape of what is coming. The layout must not jump when content arrives.
2912
2932
  - A control that is waiting uses its `loading` prop.
2913
- - Nothing spins for less than about 300ms.
2933
+ - Delay a spinner for 300ms; if the work finishes before then, show no spinner.
2914
2934
  - Mark the loading region with `aria-busy`.
2915
2935
 
2916
2936
  ## Empty state
@@ -2918,8 +2938,10 @@ A repeated unit of content in a list: a note, a comment, a changelog item.
2918
2938
  Centered in the content column:
2919
2939
 
2920
2940
  - a `title` line that says what is missing ("No notes yet"),
2921
- - a `body` line that says what to do,
2922
- - one primary Button.
2941
+ - a `body` line in `text-secondary` that says what to do, `space-1` under the title,
2942
+ - one primary Button, `space-4` under the text.
2943
+
2944
+ The block has `space-8` above and below it, so it reads as a region of its own under the page header. The Button is the page's one primary action: a page that shows an empty state has no second primary in its header.
2923
2945
 
2924
2946
  No illustration.
2925
2947
 
@@ -2986,14 +3008,14 @@ Inside another page (the web clipper, an embed), mount the UI under a `.mu-root`
2986
3008
 
2987
3009
  Set `data-theme="light" | "dark"` on the root element or on any `.mu-root`. Without the attribute the tokens are Light. The app, not the package, reads `prefers-color-scheme` and the user's saved choice. See [Color](color.md#themes). An app can add its own themes on top of these two; see [App themes](color.md#app-themes).
2988
3010
 
2989
- An app changes only the color, shadow and font tokens. Every other token and every `mu-*` class rule is internal. See [What an app can change](color.md#what-an-app-can-change).
3011
+ An app changes only the color and font tokens. Every other token and every `mu-*` class rule is internal. See [What an app can change](color.md#what-an-app-can-change).
2990
3012
 
2991
3013
  ## Class names
2992
3014
 
2993
3015
  - Every class the package ships starts with `mu-`.
2994
- - The [public classes](custom-components.md#public-classes) are for apps too: size rows (`mu-s-md`), depth roles (`mu-raised`), focus, touch targets, icons and the type scale. Apps apply them to their own parts and never override them.
3016
+ - The [public classes](custom-components.md#public-classes) are for apps too: size rows (`mu-s-md`), surface roles (`mu-key`), focus, touch targets, icons and the type scale. Apps apply them to their own parts and never override them.
2995
3017
  - `mu-positioner` puts a popup's positioner on `layer-popup`, and `mu-popup-motion` makes a popup fade in and rise `space-1` (no rise under reduced motion).
2996
- - The depth classes `mu-recessed`, `mu-key`, `mu-raised` and `mu-popup` apply a whole depth role. Use them for new parts before writing new shadows. See [Depth](depth.md).
3018
+ - The surface classes `mu-well`, `mu-key` and `mu-popup` apply flat fills and borders. Use them for new parts. See [Surfaces and boundaries](depth.md).
2997
3019
  - `mu-prose` styles rendered content. See [Prose](components/prose.md).
2998
3020
  - `mu-icon` applies icon size and stroke rules outside a component, at the size of the nearest size row; `mu-icon-xs`, `mu-icon-sm` and `mu-icon-md` set a fixed size on their own. See [Icons](icons.md).
2999
3021
  - `mu-visually-hidden` hides text visually but keeps it for screen readers.
@@ -3076,7 +3098,7 @@ toast.add({ title: "Note archived", type: "success" });
3076
3098
 
3077
3099
  1. A reason: a screen in a product needs it, and no existing component or pattern covers it.
3078
3100
  2. A generic name and API that would make sense in any of our products.
3079
- 3. Tokens only: no raw colors, sizes or shadows.
3101
+ 3. Tokens only: no raw colors or sizes; no shadows, sheen or visible outlines.
3080
3102
  4. Both sizes, all [states](states.md), both themes, and reduced motion.
3081
3103
  5. Keyboard and screen reader behavior from Base UI.
3082
3104
  6. A page in `docs/components/` and an entry in the [inventory](components/README.md).
@@ -3094,7 +3116,7 @@ An app always has parts the package doesn't ship and shouldn't: product parts (a
3094
3116
  1. **Use a component if one fits.** Check the [inventory](components/README.md). Compose components, swap a part's element with `render`, or adjust one instance with `className` before building anything.
3095
3117
  2. **Behavior from Base UI.** Build anything with focus, keyboard, an open state or a popup on [`@base-ui/react`](https://base-ui.com), as the package does. It is a peer dependency of the package at one exact version, so the app's components and the package's share one copy. Upgrade it only with the package.
3096
3118
  3. **Looks from a class function** when the part should look like a component: an anchor that looks like a Button, an editor that looks like a Textarea, a suggestion list that looks like a Menu. See [Class functions](#class-functions).
3097
- 4. **Otherwise, from the public classes and tokens.** Size the part with a size row class and read the [row variables](#row-variables); give it depth with a [depth class](depth.md); set text with a [type class](typography.md); color it with `var(--mu-<token>)` or the [Tailwind utilities](#tailwind-css).
3119
+ 4. **Otherwise, from the public classes and tokens.** Size the part with a size row class and read the [row variables](#row-variables); give it a surface with a [surface class](depth.md); set text with a [type class](typography.md); color it with `var(--mu-<token>)` or the [Tailwind utilities](#tailwind-css).
3098
3120
  5. **Check it like a component.** It works in Light and Dark and under reduced motion; text keeps 4.5:1 contrast; focus shows on `:focus-visible` (`mu-focus`); touch targets reach 44px (`mu-hit`); icon-only controls have an `aria-label` and a [Tooltip](components/tooltip.md).
3099
3121
 
3100
3122
  ## Class functions
@@ -3148,22 +3170,21 @@ Apply these classes to an app's own elements. Don't override their rules: the pr
3148
3170
  | `mu-s-sm` | The sm size row: 24px | Dense toolbars, table rows, filters |
3149
3171
  | `mu-s-md` | The md size row: 32px | Everything else |
3150
3172
  | `mu-compact` | md controls and rows inside are `control-compact` (28px) tall; text, icons, padding and radius stay md | An app's sidebar with 28px rows, or a list Popover beside it |
3151
- | `mu-base` | The system font, the row's text size and weight 500, `ink` text, and icon sizing for Lucide icons inside | The root of a control |
3152
- | `mu-recessed` | `well`, a 1px `edge` and `shadow-track` | A part that holds a value |
3153
- | `mu-key` | A `key` with the sheen and `shadow-key` | A key inside a well |
3154
- | `mu-raised` | A `key` with the sheen and `shadow-raised` | A part you press |
3155
- | `mu-popup` | `surface`, `shadow-pop` and `radius-xl` | A part that floats |
3173
+ | `mu-base` | The system font, the row's text size and weight 500, the `text` color, and icon sizing for Lucide icons inside | The root of a control |
3174
+ | `mu-well` | `well` and a 1px `control-edge` | A part that holds a value |
3175
+ | `mu-key` | `key` and a 1px `control-edge` | A control or a selected choice |
3176
+ | `mu-popup` | `surface`, a 1px `edge` and `radius-xl` | A part that floats |
3156
3177
  | `mu-popup-motion` | The popup fade and rise, none under reduced motion | A popup Base UI opens |
3157
3178
  | `mu-positioner` | The popup layer | A popup's positioner |
3158
- | `mu-focus` | The ink focus ring on `:focus-visible`, 1px outside the edge | Anything focusable |
3179
+ | `mu-focus` | Thickens the existing border on `:focus-visible`; use its `--mu-control-border-width` for border and compensating padding | Anything focusable |
3159
3180
  | `mu-hit` | An invisible 44px touch target on `pointer: coarse` | Anything pressable smaller than 44px |
3160
3181
  | `mu-icon` | Icon size and stroke from the nearest size row, 16px outside one | A Lucide icon inside an app's own row |
3161
- | `mu-icon-xs` | A 12px icon with a 1.25px stroke | A Lucide icon beside 12px or 13px text, outside a control |
3182
+ | `mu-icon-xs` | A 12px icon with a 1.25px stroke | A Lucide icon beside 11px or 13px text, outside a control |
3162
3183
  | `mu-icon-sm` | A 14px icon with a 1.25px stroke | A Lucide icon beside 13px or 14px text, outside a control |
3163
3184
  | `mu-icon-md` | A 16px icon with a 1.25px stroke | A Lucide icon beside 14px or 16px text, outside a control |
3164
3185
  | `mu-visually-hidden` | Hidden on screen, read by screen readers | Text for screen readers only |
3165
- | `mu-label-xs` | 12 / 16px, 500 | Labels of 20px controls and kbd |
3166
- | `mu-caption-xs` | 12 / 16px, 400 | Text of 20px labels (badges) |
3186
+ | `mu-label-xs` | 11 / 16px, 500 | Labels of 20px controls and kbd |
3187
+ | `mu-caption-xs` | 11 / 16px, 400 | Text of 20px labels (badges) |
3167
3188
  | `mu-caption` | 13 / 18px, 400 | Secondary text |
3168
3189
  | `mu-label-sm` | 13 / 18px, 500 | Small labels |
3169
3190
  | `mu-body` | 14 / 20px, 400 | Body text |
@@ -3181,9 +3202,9 @@ A size row class sets these variables on its element, and children inherit them.
3181
3202
  | `--mu-row-height` | 20px | 24px | 32px |
3182
3203
  | `--mu-row-padding` | 6px | 8px | 12px |
3183
3204
  | `--mu-row-padding-icon` | 4px | 6px | 10px |
3184
- | `--mu-row-font-size` | 12px | 13px | 14px |
3205
+ | `--mu-row-font-size` | 11px | 13px | 14px |
3185
3206
  | `--mu-row-line-height` | 16px | 18px | 20px |
3186
- | `--mu-row-field-font-size` | 12px | 13px | 14px |
3207
+ | `--mu-row-field-font-size` | 11px | 13px | 14px |
3187
3208
  | `--mu-row-field-line-height` | 16px | 18px | 20px |
3188
3209
  | `--mu-row-radius` | 4px | 4px | 6px |
3189
3210
  | `--mu-row-gap` | 4px | 4px | 6px |
@@ -3213,16 +3234,15 @@ An app on Tailwind CSS v4 loads `tailwind.css` after the package's other stylesh
3213
3234
  @import "@usememos/ui/tailwind.css";
3214
3235
  ```
3215
3236
 
3216
- It maps the 27 color tokens, five shadow tokens, seven radius tokens and three font tokens to utilities with an `mu-` prefix, so they never clash with an app's own colors:
3237
+ It maps the 28 color tokens, seven radius tokens and three font tokens to utilities with an `mu-` prefix, so they never clash with an app's own colors:
3217
3238
 
3218
3239
  | Token | Utility |
3219
3240
  | --- | --- |
3220
- | A color, such as `surface` | `bg-mu-surface`, `text-mu-ink-muted`, `border-mu-edge`, and every other color utility |
3221
- | A shadow, such as `shadow-raised` | `shadow-mu-raised` |
3241
+ | A color, such as `surface` | `bg-mu-surface`, `text-mu-text-secondary`, `border-mu-edge`, and every other color utility |
3222
3242
  | A radius, such as `radius-md` | `rounded-mu-md` |
3223
3243
  | A font, such as `font-mono` | `font-mu-mono` |
3224
3244
 
3225
3245
  - Each utility reads the token where it is used, so a `.mu-root` with its own `data-theme` recolors the utilities inside it.
3226
- - Spacing needs no mapping: Tailwind's step is 0.25rem, which is the 4px grid, so `p-3` is `space-3`. Don't use half steps such as `p-0.5`; the only 2px value is `inset`.
3246
+ - Spacing needs no mapping: Tailwind's step is 0.25rem (4px), so `p-3` is `space-3`. Use only the numbers of the [spacing ladder](layout.md#spacing): `1`, `2`, `3`, `4`, `5`, `6`, `8`, `12` and `16`. `gap-0.5` (2px) is `inset`, for the items of one cluster only; the other half steps (`p-1.5`, `p-2.5`) are not steps. `offLadderSpacing()` finds the classes that miss the ladder: see [Spacing in an app](layout.md#spacing-in-an-app).
3227
3247
  - Text uses the type classes (`mu-body`), not Tailwind's text sizes.
3228
3248
  - `tailwind-merge` doesn't know `mu-` classes and keeps them next to a utility. The utility still wins, because the package's styles sit in the `mu` [cascade layer](building.md#cascade-layer).