@usememos/ui 26.10.2 → 26.10.4

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 (104) hide show
  1. package/README.md +9 -1
  2. package/dist/components/avatar/index.js +3 -0
  3. package/dist/components/avatar/index.js.map +7 -0
  4. package/dist/components/badge/index.js +3 -0
  5. package/dist/components/badge/index.js.map +7 -0
  6. package/dist/components/button/index.js +3 -0
  7. package/dist/components/button/index.js.map +7 -0
  8. package/dist/components/checkbox/index.js +3 -0
  9. package/dist/components/checkbox/index.js.map +7 -0
  10. package/dist/components/collapsible/index.js +3 -0
  11. package/dist/components/collapsible/index.js.map +7 -0
  12. package/dist/components/combobox/index.js +3 -0
  13. package/dist/components/combobox/index.js.map +7 -0
  14. package/dist/components/dialog/alert-dialog.js +3 -0
  15. package/dist/components/dialog/alert-dialog.js.map +7 -0
  16. package/dist/components/dialog/index.js +3 -0
  17. package/dist/components/dialog/index.js.map +7 -0
  18. package/dist/components/dialog/parts.js +3 -0
  19. package/dist/components/dialog/parts.js.map +7 -0
  20. package/dist/components/field/fieldset.js +3 -0
  21. package/dist/components/field/fieldset.js.map +7 -0
  22. package/dist/components/field/index.js +3 -0
  23. package/dist/components/field/index.js.map +7 -0
  24. package/dist/components/input/index.js +3 -0
  25. package/dist/components/input/index.js.map +7 -0
  26. package/dist/components/kbd/index.js +3 -0
  27. package/dist/components/kbd/index.js.map +7 -0
  28. package/dist/components/link/index.js +3 -0
  29. package/dist/components/link/index.js.map +7 -0
  30. package/dist/components/menu/classes.js +3 -0
  31. package/dist/components/menu/classes.js.map +7 -0
  32. package/dist/components/menu/index.js +3 -0
  33. package/dist/components/menu/index.js.map +7 -0
  34. package/dist/components/popover/classes.js +3 -0
  35. package/dist/components/popover/classes.js.map +7 -0
  36. package/dist/components/popover/index.js +3 -0
  37. package/dist/components/popover/index.js.map +7 -0
  38. package/dist/components/prose/index.js +3 -0
  39. package/dist/components/prose/index.js.map +7 -0
  40. package/dist/components/radio-group/index.js +3 -0
  41. package/dist/components/radio-group/index.js.map +7 -0
  42. package/dist/components/select/index.js +3 -0
  43. package/dist/components/select/index.js.map +7 -0
  44. package/dist/components/separator/index.js +3 -0
  45. package/dist/components/separator/index.js.map +7 -0
  46. package/dist/components/skeleton/index.js +3 -0
  47. package/dist/components/skeleton/index.js.map +7 -0
  48. package/dist/components/spinner/index.js +3 -0
  49. package/dist/components/spinner/index.js.map +7 -0
  50. package/dist/components/switch/index.js +3 -0
  51. package/dist/components/switch/index.js.map +7 -0
  52. package/dist/components/table/index.js +3 -0
  53. package/dist/components/table/index.js.map +7 -0
  54. package/dist/components/tabs/index.js +3 -0
  55. package/dist/components/tabs/index.js.map +7 -0
  56. package/dist/components/textarea/index.js +3 -0
  57. package/dist/components/textarea/index.js.map +7 -0
  58. package/dist/components/toast/index.js +3 -0
  59. package/dist/components/toast/index.js.map +7 -0
  60. package/dist/components/toggle/index.js +3 -0
  61. package/dist/components/toggle/index.js.map +7 -0
  62. package/dist/components/toggle-group/index.js +3 -0
  63. package/dist/components/toggle-group/index.js.map +7 -0
  64. package/dist/components/tooltip/index.js +3 -0
  65. package/dist/components/tooltip/index.js.map +7 -0
  66. package/dist/index.js +1 -1
  67. package/dist/index.js.map +4 -4
  68. package/dist/internal/class-name.js +3 -0
  69. package/dist/internal/class-name.js.map +7 -0
  70. package/dist/internal/popup.js +3 -0
  71. package/dist/internal/popup.js.map +7 -0
  72. package/dist/internal/size.js +3 -0
  73. package/dist/internal/size.js.map +7 -0
  74. package/dist/styles.css +1 -1
  75. package/dist/tailwind.css +66 -0
  76. package/dist/theme-tokens.js +3 -0
  77. package/dist/theme-tokens.js.map +7 -0
  78. package/dist/tokens.css +1 -1
  79. package/dist/types/components/badge/index.d.ts +9 -0
  80. package/dist/types/components/collapsible/index.d.ts +1 -1
  81. package/dist/types/components/combobox/index.d.ts +2 -2
  82. package/dist/types/components/field/fieldset.d.ts +1 -1
  83. package/dist/types/components/field/index.d.ts +1 -1
  84. package/dist/types/components/input/index.d.ts +11 -1
  85. package/dist/types/components/menu/classes.d.ts +28 -0
  86. package/dist/types/components/menu/index.d.ts +2 -2
  87. package/dist/types/components/popover/classes.d.ts +14 -0
  88. package/dist/types/components/popover/index.d.ts +1 -1
  89. package/dist/types/components/select/index.d.ts +2 -2
  90. package/dist/types/components/spinner/index.d.ts +1 -1
  91. package/dist/types/components/table/index.d.ts +1 -1
  92. package/dist/types/components/tabs/index.d.ts +1 -1
  93. package/dist/types/components/textarea/index.d.ts +11 -1
  94. package/dist/types/components/tooltip/index.d.ts +1 -1
  95. package/dist/types/index.d.ts +6 -3
  96. package/dist/types/internal/popup.d.ts +2 -2
  97. package/dist/types/internal/size.d.ts +2 -0
  98. package/dist/types/theme-tokens.d.ts +7 -0
  99. package/dist/types/types.d.ts +6 -2
  100. package/dist/types.js +2 -0
  101. package/dist/types.js.map +7 -0
  102. package/llms-full.txt +305 -87
  103. package/llms.txt +1 -0
  104. package/package.json +3 -4
package/llms-full.txt CHANGED
@@ -8,7 +8,7 @@ These docs describe the target system. They are the source of truth for `@usemem
8
8
 
9
9
  ## Who it is for
10
10
 
11
- - **Product code** in memos, the website, the web clipper and future usememos projects. Each product builds its screens from these components and patterns, so the products look and feel like one family.
11
+ - **Product code** in memos, the website, the web clipper and future usememos projects. Each product builds its screens from these components and patterns, so the products look and feel like one family. Parts a product needs beyond them follow [Building your own component](custom-components.md).
12
12
  - **People and coding agents** who build or change the package. Start with [`AGENTS.md`](../AGENTS.md).
13
13
 
14
14
  It is not a general-purpose UI library for outside developers. Components are generic: nothing in the package knows what a note, a memo or a clip is.
@@ -28,6 +28,7 @@ It is not a general-purpose UI library for outside developers. Components are ge
28
28
  | [Components](components/README.md) | The 28 components, what was left out, and one page per component |
29
29
  | [Patterns](patterns.md) | Page, form, entry, confirmation, loading, empty state and mobile |
30
30
  | [Building on it](building.md) | Base UI, loading the CSS, scoping inside a host page, theme selection and API conventions |
31
+ | [Building your own component](custom-components.md) | How an app builds a part the package doesn't ship: Base UI, class functions, public classes, row variables and Tailwind CSS |
31
32
 
32
33
  ## Decisions at a glance
33
34
 
@@ -37,8 +38,9 @@ It is not a general-purpose UI library for outside developers. Components are ge
37
38
  - **Ink is the accent.** The primary action and every on state use `primary`: near-black in Light, near-white in Dark.
38
39
  - **One quiet hue.** `accent` is a quiet blue used only for focus rings, links and text selection, never as a fill.
39
40
  - **Status colors only for status.** Green means success, amber means warning, red means danger or error. None of them is used for emphasis.
40
- - **Lucide icons** at a fixed screen-pixel stroke: 1.5px at md and lg, 1.25px at sm and in badges.
41
+ - **Lucide icons** at a fixed screen-pixel stroke: 1.5px at md, 1.25px at xs, sm and in badges.
41
42
  - **The system UI font** for all text. The package ships no font files.
43
+ - **Few sizes, two weights.** App UI uses 13, 14, 16 and 20px text at weights 400 and 500. 20px is for the page title only.
42
44
  - **Dense, on a 4px grid.** Controls are 32px tall by default.
43
45
 
44
46
  <!-- docs/principles.md -->
@@ -50,7 +52,7 @@ It is not a general-purpose UI library for outside developers. Components are ge
50
52
  1. **Content first.** The interface stays gray, so people's words, tags and images are the color on the page.
51
53
  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.
52
54
  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).
53
- 4. **Calm by default.** Hierarchy comes from text color and lightness steps, not from weight, size or extra color.
55
+ 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).
54
56
  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).
55
57
  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.
56
58
 
@@ -106,7 +108,7 @@ Memos UI is ink on paper: pure-gray neutrals, `primary` ink for emphasis, one qu
106
108
 
107
109
  There are three themes: **Light** (the default), **Dark** and **High contrast**.
108
110
 
109
- - Colors are generated in OKLCH from a neutral base, the ink and a contrast level. A new theme means new inputs, not hand-picked values.
111
+ - 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.
110
112
  - 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`.
111
113
  - 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. High contrast raises `edge` to 5.3:1 and outlines every key. Offer High contrast wherever the theme is chosen.
112
114
 
@@ -116,9 +118,21 @@ Set `data-theme="light" | "dark" | "contrast"` on the root element, or on any `.
116
118
 
117
119
  The package does not read `prefers-color-scheme` itself. The app does, so that a saved preference wins over the system setting. Dark sets `color-scheme: dark`, so native scrollbars and form controls follow.
118
120
 
121
+ ### What an app can change
122
+
123
+ An app sets only these tokens:
124
+
125
+ - the 26 color tokens in the [table below](#color-tokens),
126
+ - the five shadow tokens in [Depth](depth.md#shadow-tokens),
127
+ - the three font tokens in [Typography](typography.md#fonts): `font-sans`, `font-display` and `font-mono`.
128
+
129
+ 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)).
130
+
131
+ Internal rules are not private classes. An app applies the [public classes](custom-components.md#public-classes) and the class functions to its own elements; it only never changes what they do.
132
+
119
133
  ### App themes
120
134
 
121
- An app can offer more themes than the three the package ships, such as a warm Paper theme in memos. An app theme is a rule that sets the tokens under its own `data-theme` value:
135
+ An app can offer more themes than the three 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:
122
136
 
123
137
  ```css
124
138
  [data-theme="paper"] {
@@ -130,6 +144,16 @@ An app can offer more themes than the three the package ships, such as a warm Pa
130
144
  ```
131
145
 
132
146
  - 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.
147
+ - The package exports `themeTokens`, the custom property names of those 31 tokens (`--mu-canvas` to `--mu-shadow-pop`). Check in the app's tests that its theme sets each one:
148
+
149
+ ```ts
150
+ import { themeTokens } from "@usememos/ui";
151
+ import paper from "./themes/paper.css?raw";
152
+
153
+ it("sets every theme token", () => {
154
+ expect(themeTokens.filter((name) => !paper.includes(`${name}:`))).toEqual([]);
155
+ });
156
+ ```
133
157
  - Keep the rules on this page: `primary` stays ink, not a brand hue; `accent` is only for focus, links and selection; status colors only for status.
134
158
  - The app owns the theme's contrast. Check in the app's own tests that every text and background pair named in the Usage column below keeps 4.5:1 or more. The package's tests and screenshots cover only Light, Dark and High contrast.
135
159
  - The tokens sit in the `mu` [cascade layer](building.md#cascade-layer), so an unlayered app theme wins over them at any specificity.
@@ -249,14 +273,18 @@ Each depth class applies a whole role in one class. Use them for new parts befor
249
273
 
250
274
  ## Rules
251
275
 
252
- - The platform's UI font for everything (San Francisco on Apple, Segoe UI on Windows, Roboto on Android), smoothed with antialiasing. The package ships no font files.
253
- - Controls and labels are weight 500. Running text is 400. Headings are 600.
254
- - Control text scales with the control size: 12px at sm, 14px at md, 16px at lg.
255
- - `body-sm` (13px) is for dense secondary text. `label-xs` (11px) is for kbd and counts inside badges only.
256
- - Reading text (`Prose`) is 15px on a 24px line. `sm` prose is 14px on a 22px line, for dense lists.
257
- - Letter spacing is 0 up to 20px, −0.01em for `heading-2` and `heading-1`, and −0.02em for `display`.
276
+ - 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 labels of 20px parts (see `label-xs`). Hierarchy comes from text color, spacing and grouping first, then from weight, and from size last.
277
+ - Weight 400 is for running text and values people typed or chose. 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.
278
+ - 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 is larger than 16px, and 16px is only for `title` and Prose text.
279
+ - 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).
280
+ - Secondary text (descriptions, helper text, metadata) is `caption` (13px) in `ink-muted`. No text is smaller than 13px, except `label-xs` inside 20px parts.
281
+ - Control text scales with the control size: 12px at xs, 13px at sm, 14px at md.
282
+ - Reading text (`Prose`) is 16px on a 24px line. `sm` prose is 14px on a 22px line, for dense lists.
283
+ - Letter spacing is 0, except −0.02em for `display`.
258
284
  - Use `font-variant-numeric: tabular-nums` wherever numbers change or line up: counts, times, tables.
259
- - Hierarchy comes from text color (`ink`, `ink-muted`) before size or weight. Selected and unselected states never change weight, so labels don't shift.
285
+ - Selected and unselected states never change weight, so labels don't shift.
286
+ - Adding a size, a weight or a style needs a written reason in this page and the maintainer's sign-off.
287
+ - The platform's UI font for everything (San Francisco on Apple, Segoe UI on Windows, Roboto on Android), smoothed with antialiasing. The package ships no font files.
260
288
 
261
289
  ## Fonts
262
290
 
@@ -266,38 +294,48 @@ Each depth class applies a whole role in one class. Use them for new parts befor
266
294
  | `--mu-font-display` | `system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif` |
267
295
  | `--mu-font-mono` | `ui-monospace, "SF Mono", Menlo, Consolas, monospace` |
268
296
 
269
- The package ships no font files and loads no remote font: text uses the system UI font, so it looks native on each platform and costs no download. An app that wants another font sets `--mu-font-sans` and `--mu-font-display` after loading `tokens.css`, and loads the font itself. Code in [Prose](components/prose.md) uses the platform's monospace font.
297
+ The package ships no font files and loads no remote font: text uses the system UI font, so it looks native on each platform and costs no download. An app that wants another font sets `--mu-font-sans`, `--mu-font-display` or `--mu-font-mono` after loading `tokens.css`, and loads the font itself. These are the only type tokens an app may set: sizes, line heights, weights and letter spacing are fixed (see [What an app can change](color.md#what-an-app-can-change)). Code in [Prose](components/prose.md) uses `font-mono`, the platform's monospace font by default.
270
298
 
271
299
  ## Scale
272
300
 
301
+ Eight styles over six sizes. 13 and 14px each have a 400 style for text and a 500 style for labels; 12px has only a label style; the three headings are 500. Body text on a website is `body`, or `Prose` for long-form copy.
302
+
273
303
  ### Text (`--mu-font-sans`)
274
304
 
275
305
  | Style | Size / line | Weight | Usage |
276
306
  | --- | --- | --- | --- |
277
- | `label-xs` | 11 / 16px | 500 | Kbd and counts inside badges only. Never running text |
278
- | `caption` | 12 / 16px | 400 | Metadata, timestamps, helper text. In `ink-muted` |
279
- | `label-sm` | 12 / 16px | 500 | Labels inside sm controls |
280
- | `body-sm` | 13 / 18px | 400 | Dense secondary text: toast descriptions, sm menu rows, dense table cells |
281
- | `body` | 14 / 20px | 400 | Default running text in app UI |
282
- | `label` | 14 / 20px | 500 | Labels inside md controls, nav rows, buttons. The default control text |
283
- | `body-lg` | 16 / 24px | 400 | Reading text on marketing pages and other long-form copy |
284
- | `label-lg` | 16 / 24px | 500 | Labels inside lg controls |
307
+ | `label-xs` | 12 / 16px | 500 | Labels inside xs controls, badges and kbd: the parts that are 20px tall. 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 |
308
+ | `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` |
309
+ | `label-sm` | 13 / 18px | 500 | Labels inside sm controls, tooltips, group labels in menus and lists, table heads |
310
+ | `body` | 14 / 20px | 400 | Default running text in app UI; dialog descriptions |
311
+ | `label` | 14 / 20px | 500 | Labels inside md controls, field labels, nav rows, buttons, section titles, popover and toast titles. The default control text |
285
312
 
286
313
  ### Headings (`--mu-font-display`)
287
314
 
288
315
  | Style | Size / line | Weight | Letter spacing | Usage |
289
316
  | --- | --- | --- | --- | --- |
290
- | `title` | 18 / 28px | 600 | 0 | Section titles inside panels; dialog titles; empty-state titles |
291
- | `heading-3` | 20 / 28px | 600 | 0 | Page sub-heads |
292
- | `heading-2` | 24 / 32px | 600 | −0.01em | Page titles in an app |
293
- | `heading-1` | 32 / 40px | 600 | −0.01em | Marketing and onboarding headings |
294
- | `display` | 40 / 48px | 600 | −0.02em | Hero headline on a website only |
317
+ | `title` | 16 / 24px | 500 | 0 | Dialog titles; empty-state titles |
318
+ | `heading` | 20 / 28px | 500 | 0 | The page title in an app or on a website. One per page |
319
+ | `display` | 32 / 40px | 500 | −0.02em | The hero headline on a website. Never in an app |
320
+
321
+ 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.
322
+
323
+ ### Removed styles
295
324
 
296
- Each style is also a class with the `mu-` prefix (`.mu-caption`, `.mu-heading-2`) in `@usememos/ui/styles.css`, which sets the family, size, line height, weight and, where listed, letter spacing.
325
+ 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.)
326
+
327
+ | Removed | Was | Use |
328
+ | --- | --- | --- |
329
+ | `body-sm` | 13 / 18px, 400 | `caption` |
330
+ | `heading-3` | 20 / 28px, 600 | `heading`, or `label` for a section title |
331
+ | `heading-2` | 24 / 32px, 600 | `heading` |
332
+ | `heading-1` | 32 / 40px, 600 | `display` on a website; `heading` in an app |
333
+ | `body-lg` | 16 / 24px, 400 | `body`, or `Prose` for long-form copy |
334
+ | `label-lg` | 16 / 24px, 500 | `label` (there is no `lg` control size) |
297
335
 
298
336
  ## Prose
299
337
 
300
- Rendered content (notes, comments, changelogs, website pages) uses the `Prose` styles, not the scale above. See [Prose](components/prose.md).
338
+ Rendered content (notes, comments, changelogs, website pages) uses the `Prose` styles, not the scale above. It uses the same sizes: 16px text (14px at `sm`), 20px `h1`, 16px `h2`–`h6`, and 13px table heads. See [Prose](components/prose.md).
301
339
 
302
340
  <!-- docs/layout.md -->
303
341
 
@@ -305,15 +343,17 @@ Rendered content (notes, comments, changelogs, website pages) uses the `Prose` s
305
343
 
306
344
  ## Control sizes
307
345
 
308
- Every control takes one `size` prop: `sm`, `md` (the default) or `lg`. The size sets every value in its row together. Never mix rows in one component.
346
+ Every control takes one `size` prop: `xs`, `sm` or `md` (the default). There is no `lg`. The size sets every value in its row together. Never mix rows in one component.
309
347
 
310
348
  | Size | Height | Padding | Icon | Icon gap | Text | Radius | Check box |
311
349
  | --- | --- | --- | --- | --- | --- | --- | --- |
312
- | sm | `control-sm` 24px | 8px | 14px | 4px | 12 / 16px | `radius-sm` 4px | 16px |
350
+ | xs | `control-xs` 20px | 6px | 12px | 4px | 12 / 16px | `radius-sm` 4px | 14px |
351
+ | sm | `control-sm` 24px | 8px | 14px | 4px | 13 / 18px | `radius-sm` 4px | 16px |
313
352
  | md (default) | `control-md` 32px | 12px | 16px | 6px | 14 / 20px | `radius-md` 6px | 18px |
314
- | lg | `control-lg` 40px | 16px | 20px | 8px | 16 / 24px | `radius-lg` 8px | 20px |
315
353
 
316
- - Use `sm` for dense toolbars, table rows and filters inside panels; `md` everywhere else in an app; `lg` for marketing pages, onboarding and primary actions in sheets.
354
+ - Use `sm` for dense toolbars, table rows and filters inside panels, and `md` everywhere else, including marketing pages, onboarding and sheets. Emphasis comes from the `primary` variant, not from a bigger control. On touch screens the hit area, not the visual size, grows.
355
+ - Use `xs` for secondary actions beside 13px text: actions on a row or an entry, inline formatting, filters in a tight header, checkboxes in a dense list. It is the height of a Badge or a Kbd, so they line up. Don't use `xs` for forms, for navigation, or for the main action of a screen.
356
+ - Only Button, Toggle, ToggleGroup, Checkbox, RadioGroup and Radio, Switch, Avatar, Spinner and Skeleton take `xs`. Text entry, menus, tabs and tables are too cramped at 20px, so their `size` is `sm | md`, and an `xs` inherited from a parent renders them at `sm`.
317
357
  - The icon side of a control has 2px less padding, so text and icon look centered.
318
358
  - On touch screens (`pointer: coarse`), every control gets an invisible hit area of at least `hit-area` (44px) without changing its visual size.
319
359
 
@@ -322,24 +362,25 @@ Every control takes one `size` prop: `sm`, `md` (the default) or `lg`. The size
322
362
  Every spacing value comes from the 4px scale. The only exception is `inset` (2px), the gap between a well and the key inside it.
323
363
 
324
364
  - 0 inside a well.
325
- - 2px between items inside a cluster (a toolbar).
365
+ - 2px (`inset`) between items inside a cluster (a toolbar).
326
366
  - 8px between controls.
327
367
  - 16px between fields.
328
368
  - 24px between groups.
329
369
 
330
370
  | Token | Value | Usage |
331
371
  | --- | --- | --- |
332
- | `space-0_5` | 2px | Only for insets: the gap between a well and the key inside it |
333
372
  | `space-1` | 4px | Gap between sm controls; icon to text in sm; the label-to-control gap in a field |
334
- | `space-2` | 8px | Gap between md and lg controls; sm horizontal padding |
373
+ | `space-2` | 8px | Gap between md controls; sm horizontal padding |
335
374
  | `space-3` | 12px | md horizontal padding |
336
- | `space-4` | 16px | Padding inside popovers and toasts; lg horizontal padding; gap between fields; page padding on phones |
375
+ | `space-4` | 16px | Padding inside popovers and toasts; gap between fields; page padding on phones |
337
376
  | `space-5` | 20px | Padding inside dialogs; space above Prose headings |
338
377
  | `space-6` | 24px | Gap between sections or entries inside a page; page padding on desktop |
339
378
  | `space-8` | 32px | Gap between page regions |
340
379
  | `space-12` | 48px | Section rhythm on a website |
341
380
  | `space-16` | 64px | Hero and footer padding on a website |
342
381
 
382
+ `space-8`, `space-12` and `space-16` are for an app's page layout. No component uses them.
383
+
343
384
  ## Radius
344
385
 
345
386
  Inner radius = outer radius − `inset`, so nested corners stay concentric.
@@ -349,7 +390,7 @@ Inner radius = outer radius − `inset`, so nested corners stay concentric.
349
390
  | `radius-xs` | 2px | Keys inside sm wells (`radius-sm` minus the 2px inset); inline code |
350
391
  | `radius-sm` | 4px | sm controls; badges |
351
392
  | `radius-md` | 6px | md controls, the default |
352
- | `radius-lg` | 8px | lg controls; images in Prose |
393
+ | `radius-lg` | 8px | Images in Prose |
353
394
  | `radius-xl` | 12px | Menus, select lists, popovers and toasts |
354
395
  | `radius-2xl` | 16px | Dialogs and sheets |
355
396
  | `radius-full` | 9999px | Avatars, radios, switches and their knobs only |
@@ -360,15 +401,15 @@ Buttons, inputs and badges are never pills.
360
401
 
361
402
  | Token | Value | Usage |
362
403
  | --- | --- | --- |
404
+ | `control-xs` | 20px | xs control height; Badge and Kbd |
363
405
  | `control-sm` | 24px | sm control height |
364
406
  | `control-md` | 32px | Default control height |
365
- | `control-lg` | 40px | lg control height |
366
407
  | `inset` | 2px | Well-to-key inset at every size |
367
408
  | `hit-area` | 44px | Minimum touch target on `pointer: coarse`, added invisibly around a control |
368
409
  | `focus-width` | 2px | Width and offset of the focus ring |
410
+ | `icon-xs` | 12px | Icons in xs controls and badges |
369
411
  | `icon-sm` | 14px | Icons in sm controls |
370
412
  | `icon-md` | 16px | Icons in md controls and nav rows |
371
- | `icon-lg` | 20px | Icons in lg controls |
372
413
 
373
414
  ## Layout
374
415
 
@@ -382,10 +423,23 @@ Buttons, inputs and badges are never pills.
382
423
  | `tooltip-width` | 240px | Maximum width of a tooltip |
383
424
  | `toast-width` | 360px | Width of a toast from `breakpoint-sm` up |
384
425
  | `dialog-width` | 440px | Default width of a dialog |
385
- | `breakpoint-sm` | 640px | Below this: one column, dialogs and menus become sheets, toasts sit bottom-center |
386
- | `breakpoint-md` | 1024px | From this: the sidebar is fixed and the content column is centered |
387
426
 
388
- CSS custom properties can't be used in media queries, so media queries use the values (`@media (min-width: 1024px)`) and name the token in a comment.
427
+ - `content-width` and `sidebar-width` are suggested values for an app's page layout. No component uses them. An app that needs another width uses its own value instead of overriding the token.
428
+ - The other widths are component defaults. To change one instance, pass `className` or `style` to it.
429
+
430
+ ## Breakpoints
431
+
432
+ Two breakpoints. They are values, not tokens: CSS custom properties can't be used in media queries, so there is no `--mu-breakpoint-*` variable and nothing to override. Write the value in the query and name the breakpoint in a comment.
433
+
434
+ | Breakpoint | Value | Usage |
435
+ | --- | --- | --- |
436
+ | breakpoint-sm | 640px | Below this: one column, dialogs and menus become sheets, toasts sit bottom-center |
437
+ | breakpoint-md | 1024px | From this: the sidebar is fixed and the content column is centered |
438
+
439
+ ```css
440
+ /* Below breakpoint-sm */
441
+ @media (max-width: 639.98px) { … }
442
+ ```
389
443
 
390
444
  ## Layers
391
445
 
@@ -461,14 +515,14 @@ Memos UI uses [Lucide](https://lucide.dev) (ISC license) for every icon. Apps im
461
515
  import { Pin, Trash2 } from "lucide-react";
462
516
 
463
517
  <Button icon={<Pin />}>Pin</Button>
464
- <Menu items={[{ value: "delete", label: "Delete", icon: <Trash2 />, danger: true }]} />
518
+ <Menu.Item icon={<Trash2 />} danger>Delete</Menu.Item>
465
519
  ```
466
520
 
467
521
  ## Rules
468
522
 
469
523
  - **Icons are elements, not names.** Components take `icon={<Pin />}`, never `icon="pin"`.
470
- - **Size comes from the control.** An icon inside a component is 14px at sm, 16px at md and 20px at lg (`icon-sm`, `icon-md`, `icon-lg`). Don't pass `size`.
471
- - **Stroke is fixed in screen pixels.** 1.25px at sm and in badges, 1.5px at md and lg, and 2px for the check in a checkbox. At 1.5px an icon matches the stem weight of the label text beside it. CSS applies `vector-effect: non-scaling-stroke`, which matches Lucide's `absoluteStrokeWidth`, so no props are needed. Don't pass `strokeWidth`.
524
+ - **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`.
525
+ - **Stroke is fixed in screen pixels.** 1.25px at xs, at sm and in badges, 1.5px at md, and 2px for the check in a checkbox. At 1.5px an icon matches the stem weight of the label text beside it. CSS applies `vector-effect: non-scaling-stroke`, which matches Lucide's `absoluteStrokeWidth`, so no props are needed. Don't pass `strokeWidth`.
472
526
  - **Color is `currentColor`.** Icons follow the text color: `ink` in controls, `ink-muted` in menu rows, the tone color in toasts.
473
527
  - **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.
474
528
  - **Accessibility.** Icons are `aria-hidden`. Every icon-only Button or Toggle has an `aria-label`, and a Tooltip with the same text.
@@ -554,7 +608,7 @@ Components follow the [API conventions](../building.md#api-conventions): parts w
554
608
  ## Shared types
555
609
 
556
610
  ```ts
557
- type Size = "sm" | "md" | "lg";
611
+ type Size = "xs" | "sm" | "md"; // xs only where a page says so; other components take "sm" | "md"
558
612
  type Tone = "neutral" | "success" | "warning" | "danger";
559
613
  /** A Lucide icon element, such as <Pin /> from lucide-react. Size and stroke come from the component. */
560
614
  type IconElement = React.ReactElement;
@@ -608,7 +662,7 @@ Wraps Base UI's [Button](https://base-ui.com/react/components/button): `disabled
608
662
 
609
663
  ## Style
610
664
 
611
- - A raised key: 24, 32, 40px 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.
665
+ - 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.
612
666
  - Only an icon: square. The icon side has 2px less padding.
613
667
  - 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.
614
668
  - `loading` keeps the width, shows a Spinner over the label, sets `aria-busy` and `aria-disabled`, ignores presses and stays focusable.
@@ -757,10 +811,10 @@ import { Ellipsis, FolderInput, Pin, Trash2 } from "lucide-react";
757
811
 
758
812
  ```ts
759
813
  interface Menu.RootProps extends BaseMenu.Root.Props {
760
- size?: Size; // row height: 24, 32, 40px
814
+ size?: "sm" | "md"; // row height: 24, 32px. An xs trigger still opens sm rows
761
815
  }
762
816
  interface Menu.PopupProps extends BaseMenu.Popup.Props {
763
- side?: Side; align?: Align; sideOffset?: number; alignOffset?: number; collisionPadding?: Padding; // Positioner props; default side "bottom", sideOffset 4
817
+ side?: Side; align?: Align; // Positioner props; default side "bottom"
764
818
  container?: HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null; // portal target
765
819
  }
766
820
  interface Menu.ItemProps extends BaseMenu.Item.Props {
@@ -769,6 +823,11 @@ interface Menu.ItemProps extends BaseMenu.Item.Props {
769
823
  danger?: boolean; // destructive: red text
770
824
  }
771
825
  // CheckboxItem and RadioItem take icon and shortcut too. SubmenuTrigger takes icon.
826
+
827
+ function menuClasses(options?: { size?: "sm" | "md" }): {
828
+ popup: string; item: string; dangerItem: string; label: string;
829
+ shortcut: string; separator: string; groupLabel: string;
830
+ };
772
831
  ```
773
832
 
774
833
  Base UI's props pass through: `open`, `defaultOpen`, `onOpenChange`, `modal` on the root; `onClick`, `closeOnClick`, `disabled`, `label` on items; `checked`, `defaultChecked`, `onCheckedChange` on CheckboxItem; `value`, `defaultValue`, `onValueChange` on RadioGroup; `value` on RadioItem.
@@ -780,11 +839,12 @@ Base UI's props pass through: `open`, `defaultOpen`, `onOpenChange`, `modal` on
780
839
  - CheckboxItems and RadioItems stay open on click, so people can change several.
781
840
  - For picking a value saved with a form, use [Select](select.md).
782
841
  - Arrow keys move the highlight; typing jumps to an item; Escape and an outside click close it; focus returns to the trigger.
842
+ - An app's own floating list, such as an editor's suggestion list, takes `menuClasses()`: `popup` on the list, `item` on each row, and `data-highlighted` on the active row. See [Class functions](../custom-components.md#class-functions).
783
843
 
784
844
  ## Style
785
845
 
786
846
  - `mu-popup` with `space-1` padding, at least `menu-width` wide, scrolls when taller than the space available.
787
- - Rows are a control height (24, 32, 40px), text weight 400; icons in `ink-muted`; the shortcut right-aligned in `ink-muted`.
847
+ - Rows are a control height (24 or 32px), text weight 400; icons in `ink-muted`; the shortcut right-aligned in `ink-muted`.
788
848
  - The highlighted row uses `hover`. Disabled rows fade to 45%. `danger` rows use `danger`.
789
849
  - A ghost trigger keeps its `hover` background while the menu is open.
790
850
  - Fades in and rises `space-1` over `duration-fade`; no rise under reduced motion.
@@ -874,10 +934,10 @@ import { Checkbox, Field, Fieldset, Input } from "@usememos/ui";
874
934
 
875
935
  ```ts
876
936
  interface Field.RootProps extends BaseField.Root.Props {
877
- size?: Size; // passed to the control inside
937
+ size?: "sm" | "md"; // passed to the control inside
878
938
  }
879
939
  interface Fieldset.RootProps extends BaseFieldset.Root.Props {
880
- size?: Size; // passed to every control inside
940
+ size?: "sm" | "md"; // passed to every control inside
881
941
  }
882
942
  ```
883
943
 
@@ -895,7 +955,7 @@ Base UI's props pass through: `name`, `invalid`, `disabled`, `validate`, `valida
895
955
  ## Style
896
956
 
897
957
  - Label: `label` style (14px, 500), `space-1` above the control; weight 400 when it wraps a Checkbox, Switch or Radio.
898
- - Description and error: 12px, below the control. The error hides the description and turns the control's edge `danger`.
958
+ - Description and error: `caption` (13px), below the control. The error hides the description and turns the control's edge `danger`.
899
959
 
900
960
  <!-- docs/components/input.md -->
901
961
 
@@ -916,12 +976,14 @@ import { Search } from "lucide-react";
916
976
 
917
977
  ```ts
918
978
  interface InputProps extends Omit<BaseInput.Props, "size" | "prefix"> {
919
- size?: Size;
979
+ size?: "sm" | "md";
920
980
  prefix?: React.ReactNode; // says what the field is: a search or link icon
921
981
  suffix?: React.ReactNode; // one action on the value (copy, clear), or a unit
922
982
  className?: string; // on the visible box, not the <input>
923
983
  style?: React.CSSProperties; // on the visible box, not the <input>
924
984
  }
985
+
986
+ function inputClasses(options?: { size?: "sm" | "md" }): string; // the visible box
925
987
  ```
926
988
 
927
989
  Wraps Base UI's [Input](https://base-ui.com/react/components/input), so inside a [Field](field.md) it gets its label, description and validity. The ref and every other prop (`value`, `onValueChange`, `type`, `disabled`, native attributes) go to the `<input>`. The visible box is a `<span>` around it that fills its container's width.
@@ -931,6 +993,7 @@ Wraps Base UI's [Input](https://base-ui.com/react/components/input), so inside a
931
993
  - Provide a `Field.Label` or `aria-label`. Never a placeholder alone.
932
994
  - `prefix` and `suffix` hold an icon, a short word or a small ghost Button.
933
995
  - `sm` for filters inside panels, `md` everywhere else.
996
+ - An app's own one-line field, such as a tag input around a native `<input>`, takes `inputClasses()` on its box: focus anywhere inside shows the ring, and `aria-invalid="true"` or `aria-disabled="true"` on the box show those states. See [Class functions](../custom-components.md#class-functions).
934
997
 
935
998
  ## Style
936
999
 
@@ -960,9 +1023,11 @@ import { Field, Textarea } from "@usememos/ui";
960
1023
 
961
1024
  ```ts
962
1025
  interface TextareaProps extends Omit<BaseField.Control.Props, "size" | "render"> {
963
- size?: Size;
1026
+ size?: "sm" | "md";
964
1027
  rows?: number; // visible lines, default 3
965
1028
  }
1029
+
1030
+ function textareaClasses(options?: { size?: "sm" | "md" }): string;
966
1031
  ```
967
1032
 
968
1033
  Base UI has no textarea part, so this renders Base UI's `Field.Control` as a `<textarea>`. Inside a [Field](field.md) it gets its label, description and validity. Native textarea props (`value`, `onChange`, `maxLength`) pass through.
@@ -971,6 +1036,7 @@ Base UI has no textarea part, so this renders Base UI's `Field.Control` as a `<t
971
1036
 
972
1037
  - Provide a `Field.Label` or `aria-label`.
973
1038
  - Rich content is not a textarea: use the editor (`@usememos/editor`) with [Prose](prose.md).
1039
+ - An app's own multi-line field, such as a content-editable editor, takes `textareaClasses()`: focus shows the ring, and `aria-invalid="true"` or `aria-disabled="true"` show those states. See [Class functions](../custom-components.md#class-functions).
974
1040
 
975
1041
  ## Style
976
1042
 
@@ -1021,11 +1087,11 @@ const visibility = { private: "Private", workspace: "Workspace", public: "Public
1021
1087
 
1022
1088
  ```ts
1023
1089
  type Select.RootProps<Value, Multiple> = BaseSelect.Root.Props<Value, Multiple> & {
1024
- size?: Size;
1090
+ size?: "sm" | "md";
1025
1091
  };
1026
1092
  interface Select.PopupProps extends BaseSelect.Popup.Props {
1027
1093
  alignItemWithTrigger?: boolean; // default true: the list opens over the trigger with the chosen item on the value
1028
- side?: Side; align?: Align; sideOffset?: number; alignOffset?: number; collisionPadding?: Padding; // used when alignItemWithTrigger is false
1094
+ side?: Side; align?: Align; // used when alignItemWithTrigger is false
1029
1095
  container?: HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null; // portal target
1030
1096
  }
1031
1097
  interface Select.ItemProps extends BaseSelect.Item.Props {
@@ -1097,7 +1163,7 @@ const tags = ["book", "design", "travel"];
1097
1163
 
1098
1164
  ```ts
1099
1165
  type Combobox.RootProps<Value, Multiple, Item> = BaseCombobox.Root.Props<Value, Multiple, Item> & {
1100
- size?: Size;
1166
+ size?: "sm" | "md";
1101
1167
  };
1102
1168
  interface Combobox.InputProps extends BaseCombobox.Input.Props {
1103
1169
  icon?: IconElement | null; // default <Search />; null drops it
@@ -1106,7 +1172,7 @@ interface Combobox.InputProps extends BaseCombobox.Input.Props {
1106
1172
  style?: React.CSSProperties; // on the box
1107
1173
  }
1108
1174
  interface Combobox.PopupProps extends BaseCombobox.Popup.Props {
1109
- side?: Side; align?: Align; sideOffset?: number; alignOffset?: number; collisionPadding?: Padding; // Positioner props; default side "bottom", align "start", sideOffset 4
1175
+ side?: Side; align?: Align; // Positioner props; default side "bottom", align "start"
1110
1176
  container?: HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null; // portal target
1111
1177
  }
1112
1178
  interface Combobox.ItemProps extends BaseCombobox.Item.Props {
@@ -1155,7 +1221,7 @@ import { Checkbox, Field } from "@usememos/ui";
1155
1221
 
1156
1222
  ```ts
1157
1223
  interface CheckboxProps extends BaseCheckbox.Root.Props {
1158
- size?: Size; // box: 16, 18, 20px
1224
+ size?: Size; // box: 14, 16, 18px
1159
1225
  }
1160
1226
  ```
1161
1227
 
@@ -1195,7 +1261,7 @@ import { Field, Switch } from "@usememos/ui";
1195
1261
 
1196
1262
  ```ts
1197
1263
  interface SwitchProps extends BaseSwitch.Root.Props {
1198
- size?: Size; // 18, 20, 22px tall
1264
+ size?: Size; // 16, 18, 20px tall
1199
1265
  }
1200
1266
  ```
1201
1267
 
@@ -1303,7 +1369,7 @@ import { Bell, User } from "lucide-react";
1303
1369
 
1304
1370
  ```ts
1305
1371
  interface Tabs.RootProps extends BaseTabs.Root.Props {
1306
- size?: Size;
1372
+ size?: "sm" | "md";
1307
1373
  }
1308
1374
  interface Tabs.TabProps extends BaseTabs.Tab.Props {
1309
1375
  icon?: IconElement; // before the label
@@ -1353,7 +1419,7 @@ import { Collapsible } from "@usememos/ui";
1353
1419
 
1354
1420
  ```ts
1355
1421
  interface Collapsible.RootProps extends BaseCollapsible.Root.Props {
1356
- size?: Size; // the trigger's size
1422
+ size?: "sm" | "md"; // the trigger's size
1357
1423
  }
1358
1424
  ```
1359
1425
 
@@ -1489,9 +1555,11 @@ import { Button, Input, Popover } from "@usememos/ui";
1489
1555
 
1490
1556
  ```ts
1491
1557
  interface Popover.PopupProps extends BasePopover.Popup.Props {
1492
- side?: Side; align?: Align; sideOffset?: number; alignOffset?: number; collisionPadding?: Padding; // Positioner props; default side "bottom", sideOffset 6
1558
+ side?: Side; align?: Align; // Positioner props; default side "bottom"
1493
1559
  container?: HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null; // portal target
1494
1560
  }
1561
+
1562
+ function popoverClasses(): { popup: string; title: string; description: string };
1495
1563
  ```
1496
1564
 
1497
1565
  Base UI's props pass through: `open`, `defaultOpen`, `onOpenChange`, `modal` on the root; `openOnHover`, `delay` on the trigger; `initialFocus`, `finalFocus` on the popup.
@@ -1501,11 +1569,12 @@ Base UI's props pass through: `open`, `defaultOpen`, `onOpenChange`, `modal` on
1501
1569
  - Use it for a few controls that belong to one thing. For a list of actions, use [Menu](menu.md). For a decision that blocks the page, use [Dialog](dialog.md).
1502
1570
  - Give it a `Popover.Title`, or `aria-label` on the popup.
1503
1571
  - It closes on Escape and on a click outside; focus returns to the trigger.
1572
+ - An app's own floating panel, such as an emoji picker, takes `popoverClasses()`. See [Class functions](../custom-components.md#class-functions).
1504
1573
 
1505
1574
  ## Style
1506
1575
 
1507
1576
  - `mu-popup` (`surface`, `shadow-pop`, `radius-xl`), `space-4` padding, `popover-width` wide. Children stack `space-3` apart.
1508
- - Title: `label`. Description: `body-sm` in `ink-muted`, `space-1` under the title.
1577
+ - Title: `label`. Description: `caption` in `ink-muted`, `space-1` under the title.
1509
1578
  - Fades in and rises `space-1` over `duration-fade`; no rise under reduced motion.
1510
1579
 
1511
1580
  <!-- docs/components/tooltip.md -->
@@ -1545,7 +1614,7 @@ interface Tooltip.TriggerProps extends BaseTooltip.Trigger.Props {
1545
1614
  }
1546
1615
  interface Tooltip.PopupProps extends BaseTooltip.Popup.Props {
1547
1616
  shortcut?: React.ReactNode; // after the label, at 65% opacity
1548
- side?: Side; align?: Align; sideOffset?: number; alignOffset?: number; collisionPadding?: Padding; // Positioner props; default side "top", sideOffset 6
1617
+ side?: Side; align?: Align; // Positioner props; default side "top"
1549
1618
  container?: HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null; // portal target
1550
1619
  }
1551
1620
  ```
@@ -1621,7 +1690,7 @@ Base UI's props pass through: `timeout` (default 5000ms) and `limit` (default 3)
1621
1690
  ## Style
1622
1691
 
1623
1692
  - `mu-popup`, `toast-width` wide, bottom-right at `space-6` from the edges; below `breakpoint-sm`, full width at `space-4`.
1624
- - A tone icon (`Info`, `CircleCheck`, `TriangleAlert`, `CircleAlert`) in `ink-muted`, `success`, `warning` or `danger`; title in `label`; description in `body-sm` `ink-muted`; the action as a small neutral Button; the X as a small ghost Button.
1693
+ - 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.
1625
1694
  - Newest in front; the ones behind peek out `space-2` above and shrink. Hover or focus fans them out `space-3` apart. Toasts slide in from below and can be swiped away. Under reduced motion they only fade.
1626
1695
 
1627
1696
  <!-- docs/components/avatar.md -->
@@ -1653,7 +1722,7 @@ import { Avatar } from "@usememos/ui";
1653
1722
 
1654
1723
  ```ts
1655
1724
  interface Avatar.RootProps extends BaseAvatar.Root.Props {
1656
- size?: Size; // 24, 32, 40px
1725
+ size?: Size; // 20, 24, 32px
1657
1726
  }
1658
1727
  ```
1659
1728
 
@@ -1662,7 +1731,7 @@ interface Avatar.RootProps extends BaseAvatar.Root.Props {
1662
1731
  ## Rules
1663
1732
 
1664
1733
  - Give the image an `alt` with the person's name. Initials are one or two letters, computed by the app.
1665
- - `sm` in lists and comments, `md` in a sidebar, `lg` on profile pages.
1734
+ - `xs` inline with a name in 13px text, `sm` in lists and comments, `md` in a sidebar and on profile pages.
1666
1735
 
1667
1736
  ## Style
1668
1737
 
@@ -1694,16 +1763,19 @@ interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
1694
1763
  children: React.ReactNode; // one or two words
1695
1764
  render?: React.ReactElement;
1696
1765
  }
1766
+
1767
+ function badgeClasses(options?: { tone?: Tone | "outline" }): string;
1697
1768
  ```
1698
1769
 
1699
1770
  ## Rules
1700
1771
 
1701
1772
  - Tags: `neutral` with a `Hash` icon. Visibility: `outline` with its icon. Status: `success`, `warning` or `danger`, always with a word, never color alone.
1702
1773
  - A badge is not a button. A clickable tag filter is a [Toggle](toggle.md).
1774
+ - An app's own element that should look like a badge, such as a tag that is also a link, takes `badgeClasses()`. See [Class functions](../custom-components.md#class-functions).
1703
1775
 
1704
1776
  ## Style
1705
1777
 
1706
- - 20px tall, `radius-sm` (never a pill), 12px medium text, optional 12px icon at a 1.25px stroke.
1778
+ - `control-xs` (20px) tall, `radius-sm` (never a pill), `label-xs` text (12px, 500), optional `icon-xs` (12px) icon at a 1.25px stroke. It lines up with xs controls.
1707
1779
  - `neutral`: `ink` on `well`. Status tones: their `-soft` background with full-strength text.
1708
1780
 
1709
1781
  <!-- docs/components/kbd.md -->
@@ -1738,7 +1810,7 @@ Renders a `<kbd>`.
1738
1810
 
1739
1811
  ## Style
1740
1812
 
1741
- - A tiny raised key, 20px tall, `label-xs` text in `ink-muted`.
1813
+ - A tiny raised key, `control-xs` (20px) tall, `label-xs` text (12px) in `ink-muted`.
1742
1814
 
1743
1815
  <!-- docs/components/separator.md -->
1744
1816
 
@@ -1792,7 +1864,7 @@ import { Spinner } from "@usememos/ui";
1792
1864
 
1793
1865
  ```ts
1794
1866
  interface SpinnerProps extends React.HTMLAttributes<HTMLSpanElement> {
1795
- size?: Size; // 14, 16, 20px; default: the surrounding Root's size, else md
1867
+ size?: Size; // 12, 14, 16px; default: the surrounding Root's size, else md
1796
1868
  label?: string; // for screen readers, default "Loading"
1797
1869
  render?: React.ReactElement;
1798
1870
  }
@@ -1906,7 +1978,7 @@ No Base UI part: these are styled native table elements.
1906
1978
 
1907
1979
  ```ts
1908
1980
  interface Table.RootProps extends React.ComponentProps<"table"> {
1909
- size?: Size; // sm for dense admin lists
1981
+ size?: "sm" | "md"; // sm for dense admin lists
1910
1982
  }
1911
1983
  interface Table.HeadProps extends React.ComponentProps<"th"> {
1912
1984
  num?: boolean; // right-aligned, for a column of numbers
@@ -1930,8 +2002,8 @@ Every native attribute passes through.
1930
2002
  ## Style
1931
2003
 
1932
2004
  - No boxes or stripes: a `line` under the head row and between body rows. Body rows highlight with `hover`.
1933
- - Heads: `label-sm` (12px, 500) in `ink-muted`, a control height tall.
1934
- - Cells: `body-sm`, `body` or `body-lg` at sm, md and lg, with tabular numbers. Rows are 34, 40 or 48px; a Button in a cell overlaps the padding, so rows keep that height.
2005
+ - Heads: `label-sm` (13px, 500) in `ink-muted`, a control height tall.
2006
+ - Cells: `caption` at sm and `body` at md, with tabular numbers. Rows are 34 or 40px; a Button in a cell overlaps the padding, so rows keep that height.
1935
2007
  - Cells pad `px` at the sides. Caption: `caption` in `ink-muted`, `space-2` above.
1936
2008
 
1937
2009
  <!-- docs/components/prose.md -->
@@ -1959,7 +2031,7 @@ Without React: `<div class="mu-prose">…</div>`, and `mu-prose mu-prose-sm` for
1959
2031
 
1960
2032
  ```ts
1961
2033
  interface ProseProps extends React.ComponentProps<"div"> {
1962
- size?: "sm" | "md"; // md: 15/24px for reading; sm: 14/22px for dense lists
2034
+ size?: "sm" | "md"; // md: 16/24px for reading; sm: 14/22px for dense lists
1963
2035
  }
1964
2036
  ```
1965
2037
 
@@ -1975,14 +2047,14 @@ Every native attribute passes through.
1975
2047
 
1976
2048
  ## Style
1977
2049
 
1978
- - Text: 15px on a 24px line (`sm`: 14 on 22), weight 400, `ink`. Blocks are `space-3` apart.
1979
- - Headings: `h1` 20/28px, `h2` 17/24px, `h3`–`h6` the text size; all weight 600, with `space-5` above and `space-2` below.
2050
+ - Text: 16px on a 24px line (`sm`: 14 on 22), weight 400, `ink`. Blocks are `space-3` apart.
2051
+ - Headings: `h1` 20/28px, `h2`–`h6` 16/24px; all weight 600, 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.
1980
2052
  - Links: `accent`, underlined at rest, because color alone does not set them apart from the text.
1981
2053
  - 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.
1982
2054
  - Quotes: a 2px `edge` bar, text in `ink-muted`. Rules: a 1px `line` with `space-5` around it.
1983
2055
  - 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.
1984
2056
  - Images and video: never wider than the column, `radius-lg`.
1985
- - Tables follow [Table](table.md): `line` between rows, 12px `ink-muted` heads, tabular numbers; they scroll sideways when too wide.
2057
+ - Tables follow [Table](table.md): `line` between rows, 13px `ink-muted` heads (weight 500), tabular numbers; they scroll sideways when too wide.
1986
2058
  - Tags: `ink-muted` on `well`, `radius-xs`. They read as words, not badges.
1987
2059
  - Sizes are custom properties set on `.mu-prose` in the foundation (`--prose-fs`, `--prose-lh`, heading sizes).
1988
2060
 
@@ -1996,7 +2068,8 @@ How the components go together. Each product builds its screens from these patte
1996
2068
 
1997
2069
  - From `breakpoint-md` (1024px) up: a fixed sidebar of `sidebar-width` (240px). Below it, the sidebar is a sheet opened from a ghost button.
1998
2070
  - Content sits in a column of at most `content-width` (720px), padded `space-4` on phones and `space-6` on desktop.
1999
- - A page header is a `heading-2` with an optional `body` line under it, and the page's one primary action at the right.
2071
+ - 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.
2072
+ - 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 use `label` for the name and `caption` in `ink-muted` for the description.
2000
2073
 
2001
2074
  ## Form
2002
2075
 
@@ -2047,7 +2120,7 @@ Under `breakpoint-sm` (640px):
2047
2120
  - Dialogs and menus become sheets.
2048
2121
  - Toasts sit bottom-center.
2049
2122
  - Every control keeps its 44px hit area.
2050
- - Controls stay `md`. `lg` is for marketing pages and primary actions in sheets.
2123
+ - Controls stay `md`.
2051
2124
 
2052
2125
  <!-- docs/building.md -->
2053
2126
 
@@ -2057,9 +2130,9 @@ How the package is put together and how products use it.
2057
2130
 
2058
2131
  ## Stack
2059
2132
 
2060
- - **React components on [Base UI](https://base-ui.com).** Base UI provides behavior: focus management, keyboard support, ARIA, portals and positioning. Memos UI provides the styling, the tokens and the component API.
2133
+ - **React components on [Base UI](https://base-ui.com).** Base UI provides behavior: focus management, keyboard support, ARIA, portals and positioning. Memos UI provides the styling, the tokens and the component API. `@base-ui/react` is a peer dependency at one exact version, so an app's own Base UI components share the package's copy.
2061
2134
  - **No shadcn.** Don't depend on shadcn, don't copy its code, and don't use shadcn-compatible names for components or tokens. Following a similar layering (unstyled behavior below, styled components above) is fine.
2062
- - **One npm package**, `@usememos/ui`, used by every product. ESM only, React 18 and 19, one JS entry point and two stylesheets.
2135
+ - **One npm package**, `@usememos/ui`, used by every product. ESM only, React 18 and 19, one JS entry point, two stylesheets and a Tailwind CSS theme file.
2063
2136
  - **Plain CSS on tokens.** Components are styled with CSS that reads token variables. Product code uses tokens through their CSS variables (`var(--mu-well)`) and never copies raw values.
2064
2137
  - **Lucide icons** from `lucide-react`. See [Icons](icons.md).
2065
2138
  - **No fonts.** Text uses the system UI font; nothing is downloaded.
@@ -2073,6 +2146,8 @@ import "@usememos/ui/tokens.css";
2073
2146
  import "@usememos/ui/styles.css";
2074
2147
  ```
2075
2148
 
2149
+ An app on Tailwind CSS v4 also loads `@usememos/ui/tailwind.css`, which maps the tokens to `mu-` utilities (`bg-mu-surface`). See [Tailwind CSS](custom-components.md#tailwind-css).
2150
+
2076
2151
  `tokens.css` defines every token on `:root`, on a shadow root's `:host`, and on any element with the `mu-root` class. `styles.css` holds the foundation classes and every component's styles.
2077
2152
 
2078
2153
  Every token is a custom property named `--mu-<token>`, so the `surface` token is `var(--mu-surface)`. The prefix keeps the tokens apart from an app's own variables and from those of a CSS framework.
@@ -2100,14 +2175,19 @@ Inside another page (the web clipper, an embed), mount the UI under a `.mu-root`
2100
2175
 
2101
2176
  Set `data-theme="light" | "dark" | "contrast"` 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 three; see [App themes](color.md#app-themes).
2102
2177
 
2178
+ 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).
2179
+
2103
2180
  ## Class names
2104
2181
 
2105
2182
  - Every class the package ships starts with `mu-`.
2183
+ - 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.
2106
2184
  - `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).
2107
2185
  - 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).
2108
2186
  - `mu-prose` styles rendered content. See [Prose](components/prose.md).
2109
2187
  - `mu-icon` applies icon size and stroke rules outside a component.
2110
2188
  - `mu-visually-hidden` hides text visually but keeps it for screen readers.
2189
+ - A size row class sets the `--mu-row-*` variables (height, padding, text, radius, gap, icon) that every part inside reads. See [Row variables](custom-components.md#row-variables).
2190
+ - Component part classes (`mu-menu-item`) are internal. An app reaches a component's look through its class function (`menuClasses()`). See [Class functions](custom-components.md#class-functions).
2111
2191
 
2112
2192
  ## API conventions
2113
2193
 
@@ -2132,14 +2212,14 @@ import { Ellipsis, Pin, Trash2 } from "lucide-react";
2132
2212
  ```
2133
2213
 
2134
2214
  - **Base UI's names.** `Root`, `Trigger`, `Popup`, `Item`, `Title`, `Description`, `Close`, `Panel` and so on. No shadcn names (`DropdownMenuContent`, `CardHeader`).
2135
- - **One `Popup` per overlay.** Base UI's `Portal`, `Positioner` and `Backdrop` are folded into our `Popup`. It takes the positioning props (`side`, `align`, `sideOffset`, `alignOffset`, `collisionPadding`) and Base UI's portal `container`, and draws the backdrop where the component has one (Dialog).
2215
+ - **One `Popup` per overlay.** Base UI's `Portal`, `Positioner` and `Backdrop` are folded into our `Popup`. It takes `side`, `align`, Base UI's other placement behavior (`anchor`, `sticky`, `collisionAvoidance`, `collisionBoundary`, `positionMethod`, `disableAnchorTracking`) and its portal `container`, and draws the backdrop where the component has one (Dialog). The gap to the trigger and the distance kept from the viewport edge are fixed by the system (4px for menus and lists, 6px for popovers and tooltips; Base UI's default edge distance), so `sideOffset`, `alignOffset` and `collisionPadding` are not props.
2136
2216
  - **Indicators are built in.** The check in a menu or select item, the chevron in a select trigger and the knob in a switch render by themselves. See [built-in glyphs](icons.md#built-in-glyphs).
2137
2217
  - **One element, one component.** Button, Input, Textarea, Badge, Kbd, Separator, Spinner, Skeleton, Link, Checkbox, Switch and Toggle are single components, not namespaces.
2138
2218
  - **No data props.** Collections are built from parts (`Menu.Item`, `Select.Item`, `Tabs.Tab`), not passed as `items` or `options` arrays.
2139
2219
 
2140
2220
  ### Props
2141
2221
 
2142
- - **`size`**: `"sm" | "md" | "lg"`, default `"md"`. One prop sets height, padding, text, icon size and radius together. On a namespace, set it on `Root`; every part reads it from context.
2222
+ - **`size`**: `"xs" | "sm" | "md"`, default `"md"`. `xs` only on the components listed in [Control sizes](layout.md#control-sizes); the rest take `"sm" | "md"`. There is no `lg`: a bigger control is not a stronger one, and apps hardly need it. One prop sets height, padding, text, icon size and radius together. On a namespace, set it on `Root`; every part reads it from context.
2143
2223
  - **Controlled or uncontrolled**, with Base UI's prop names:
2144
2224
  - `value` + `onValueChange`, or `defaultValue` (Select, Combobox, Tabs, ToggleGroup, RadioGroup)
2145
2225
  - `checked` + `onCheckedChange`, or `defaultChecked` (Checkbox, Switch, Menu.CheckboxItem)
@@ -2155,7 +2235,8 @@ import { Ellipsis, Pin, Trash2 } from "lucide-react";
2155
2235
 
2156
2236
  ### Styling hooks
2157
2237
 
2158
- - Every part has a class `mu-<component>-<part>` (`mu-menu-popup`, `mu-menu-item`), plus `mu-<component>` on single components. Variants are classes too (`mu-button-primary`, `mu-badge-success`), so `buttonClasses()` can style any element.
2238
+ - Every part has a class `mu-<component>-<part>` (`mu-menu-popup`, `mu-menu-item`), plus `mu-<component>` on single components. Variants are classes too (`mu-button-primary`, `mu-badge-success`), so a class function can style any element.
2239
+ - **Class functions.** A component whose look apps put on their own elements exports `<component>Classes()`: `buttonClasses`, `inputClasses`, `textareaClasses`, `badgeClasses`, `menuClasses` and `popoverClasses`. It returns a string for a single component and one string per part for a namespace. The component builds its own classes with it, so the two can't drift. See [Class functions](custom-components.md#class-functions).
2159
2240
  - State is styled through Base UI's data attributes (`data-highlighted`, `data-checked`, `data-pressed`, `data-disabled`, `data-open`, `data-starting-style`, `data-ending-style`), never through extra classes.
2160
2241
 
2161
2242
  ### Toasts
@@ -2176,6 +2257,143 @@ toast.add({ title: "Note archived", type: "success" });
2176
2257
  1. A reason: a screen in a product needs it, and no existing component or pattern covers it.
2177
2258
  2. A generic name and API that would make sense in any of our products.
2178
2259
  3. Tokens only: no raw colors, sizes or shadows.
2179
- 4. All three sizes, all [states](states.md), all three themes, and reduced motion.
2260
+ 4. Both sizes, all [states](states.md), all three themes, and reduced motion.
2180
2261
  5. Keyboard and screen reader behavior from Base UI.
2181
2262
  6. A page in `docs/components/` and an entry in the [inventory](components/README.md).
2263
+
2264
+ <!-- docs/custom-components.md -->
2265
+
2266
+ # Building your own component
2267
+
2268
+ How an app builds a component the package doesn't ship, so that it looks and behaves like the package's own: Base UI for behavior, the package's class functions and public classes for looks, and tokens for everything else.
2269
+
2270
+ An app always has parts the package doesn't ship and shouldn't: product parts (an entry card, a tag pill, an editor's suggestion list) and wrappers around third-party widgets (a date picker, a code editor, a chart). Build them with the steps on this page. Never copy a value out of the package's CSS: copied values don't follow the themes.
2271
+
2272
+ ## Steps
2273
+
2274
+ 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.
2275
+ 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.
2276
+ 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).
2277
+ 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).
2278
+ 5. **Check it like a component.** It works in Light, Dark and High contrast 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).
2279
+
2280
+ ## Class functions
2281
+
2282
+ A class function returns the classes that make any element look like a component, the same classes the component uses, so the two can't drift apart. Functions for single components return a string; functions for components made of parts return one string per part. Add the app's own classes after them.
2283
+
2284
+ | Function | Returns | For an app's own |
2285
+ | --- | --- | --- |
2286
+ | `buttonClasses({ variant, size, iconOnly })` | A string | Anchor that looks like an action |
2287
+ | `inputClasses({ size })` | A string | One-line field, such as a tag input around a native `<input>` |
2288
+ | `textareaClasses({ size })` | A string | Multi-line field, such as a content-editable editor |
2289
+ | `badgeClasses({ tone })` | A string | Label, such as a tag that is also a link |
2290
+ | `menuClasses({ size })` | `popup`, `item`, `dangerItem`, `label`, `shortcut`, `separator`, `groupLabel` | Floating list, such as an editor's suggestion list |
2291
+ | `popoverClasses()` | `popup`, `title`, `description` | Floating panel, such as an emoji picker |
2292
+
2293
+ State is set with attributes, never with extra classes:
2294
+
2295
+ - A field shows the focus ring when it or anything inside it has focus. `aria-invalid="true"` shows the error edge, and `aria-disabled="true"` dims it.
2296
+ - A menu row is highlighted while it has `data-highlighted`, and dimmed with `data-disabled`. Base UI sets both; a list the app drives itself sets them on the active row.
2297
+ - A popup fades in with `data-starting-style` and out with `data-ending-style`, which Base UI's popups set.
2298
+
2299
+ ```tsx
2300
+ import { badgeClasses, menuClasses, textareaClasses } from "@usememos/ui";
2301
+
2302
+ <a className={badgeClasses()} href="/tags/work">work</a>
2303
+
2304
+ <div className={textareaClasses()} contentEditable role="textbox" aria-multiline="true" aria-label="Note" />
2305
+
2306
+ const menu = menuClasses({ size: "sm" });
2307
+
2308
+ <div className={menu.popup} role="listbox">
2309
+ {matches.map((tag, index) => (
2310
+ <div key={tag} className={menu.item} role="option" aria-selected={index === active} data-highlighted={index === active ? "" : undefined}>
2311
+ <span className={menu.label}>{tag}</span>
2312
+ </div>
2313
+ ))}
2314
+ </div>
2315
+ ```
2316
+
2317
+ A popup built this way still needs its behavior from Base UI or the editor it belongs to: positioning, dismissal and focus. Put the positioner on the popup layer with `mu-positioner`. Inside a host page or a shadow root, render it inside the `.mu-root` element or the shadow root, or it loses the tokens (see [Scoping inside a host page](building.md#scoping-inside-a-host-page)).
2318
+
2319
+ ## Public classes
2320
+
2321
+ Apply these classes to an app's own elements. Don't override their rules: the products would drift apart, and the package's tests cover only its own values. Every other `mu-*` class, such as a component part's `mu-menu-item`, is internal; reach it through a class function.
2322
+
2323
+ | Class | What it applies | Use it for |
2324
+ | --- | --- | --- |
2325
+ | `mu-s-xs` | The xs size row: 20px | Secondary actions beside 13px text |
2326
+ | `mu-s-sm` | The sm size row: 24px | Dense toolbars, table rows, filters |
2327
+ | `mu-s-md` | The md size row: 32px | Everything else |
2328
+ | `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 |
2329
+ | `mu-recessed` | `well`, a 1px `edge` and `shadow-track` | A part that holds a value |
2330
+ | `mu-key` | A `key` with the sheen and `shadow-key` | A key inside a well |
2331
+ | `mu-raised` | A `key` with the sheen and `shadow-raised` | A part you press |
2332
+ | `mu-popup` | `surface`, `shadow-pop` and `radius-xl` | A part that floats |
2333
+ | `mu-popup-motion` | The popup fade and rise, none under reduced motion | A popup Base UI opens |
2334
+ | `mu-positioner` | The popup layer | A popup's positioner |
2335
+ | `mu-focus` | The focus ring on `:focus-visible` | Anything focusable |
2336
+ | `mu-hit` | An invisible 44px touch target on `pointer: coarse` | Anything pressable smaller than 44px |
2337
+ | `mu-icon` | Icon size and stroke | A Lucide icon outside a control |
2338
+ | `mu-visually-hidden` | Hidden on screen, read by screen readers | Text for screen readers only |
2339
+ | `mu-label-xs` | 12 / 16px, 500 | Labels of 20px parts |
2340
+ | `mu-caption` | 13 / 18px, 400 | Secondary text |
2341
+ | `mu-label-sm` | 13 / 18px, 500 | Small labels |
2342
+ | `mu-body` | 14 / 20px, 400 | Body text |
2343
+ | `mu-label` | 14 / 20px, 500 | Labels and buttons |
2344
+ | `mu-title` | 16 / 24px, 500 | Section titles |
2345
+ | `mu-heading` | 20 / 28px, 500 | The page title |
2346
+ | `mu-display` | 32 / 40px, 500 | A website hero only |
2347
+
2348
+ ## Row variables
2349
+
2350
+ A size row class sets these variables on its element, and children inherit them. Read them in the app's CSS to match the package's rows; never set them. The values are those in [Control sizes](layout.md#control-sizes).
2351
+
2352
+ | Variable | xs | sm | md |
2353
+ | --- | --- | --- | --- |
2354
+ | `--mu-row-height` | 20px | 24px | 32px |
2355
+ | `--mu-row-padding` | 6px | 8px | 12px |
2356
+ | `--mu-row-padding-icon` | 4px | 6px | 10px |
2357
+ | `--mu-row-font-size` | 12px | 13px | 14px |
2358
+ | `--mu-row-line-height` | 16px | 18px | 20px |
2359
+ | `--mu-row-radius` | 4px | 4px | 6px |
2360
+ | `--mu-row-gap` | 4px | 4px | 6px |
2361
+ | `--mu-row-icon` | 12px | 14px | 16px |
2362
+ | `--mu-row-icon-stroke` | 1.25px | 1.25px | 1.5px |
2363
+ | `--mu-row-check-box` | 14px | 16px | 18px |
2364
+
2365
+ `--mu-row-padding-icon` is the padding on the side of a control that starts or ends with an icon.
2366
+
2367
+ ```css
2368
+ .app-tag-input-chip {
2369
+ height: calc(var(--mu-row-height) - 2 * var(--mu-inset));
2370
+ padding: 0 var(--mu-row-padding-icon);
2371
+ border-radius: var(--mu-row-radius);
2372
+ }
2373
+ ```
2374
+
2375
+ ## Tailwind CSS
2376
+
2377
+ An app on Tailwind CSS v4 loads `tailwind.css` after the package's other stylesheets:
2378
+
2379
+ ```css
2380
+ @layer theme, base, mu, components, utilities;
2381
+ @import "tailwindcss";
2382
+ @import "@usememos/ui/tokens.css";
2383
+ @import "@usememos/ui/styles.css";
2384
+ @import "@usememos/ui/tailwind.css";
2385
+ ```
2386
+
2387
+ It maps the 26 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:
2388
+
2389
+ | Token | Utility |
2390
+ | --- | --- |
2391
+ | A color, such as `surface` | `bg-mu-surface`, `text-mu-ink-muted`, `border-mu-edge`, and every other color utility |
2392
+ | A shadow, such as `shadow-raised` | `shadow-mu-raised` |
2393
+ | A radius, such as `radius-md` | `rounded-mu-md` |
2394
+ | A font, such as `font-mono` | `font-mu-mono` |
2395
+
2396
+ - Each utility reads the token where it is used, so a `.mu-root` with its own `data-theme` recolors the utilities inside it.
2397
+ - 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`.
2398
+ - Text uses the type classes (`mu-body`), not Tailwind's text sizes.
2399
+ - `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).