@usememos/ui 26.10.0 → 26.10.2

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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +67 -92
  3. package/dist/index.js +3 -48
  4. package/dist/index.js.map +7 -0
  5. package/dist/styles.css +1 -0
  6. package/dist/tokens.css +1 -0
  7. package/dist/types/components/avatar/index.d.ts +16 -0
  8. package/dist/types/components/badge/index.d.ts +10 -0
  9. package/dist/types/components/button/index.d.ts +29 -0
  10. package/dist/types/components/checkbox/index.d.ts +11 -0
  11. package/dist/types/components/collapsible/index.d.ts +15 -0
  12. package/dist/types/components/combobox/index.d.ts +66 -0
  13. package/dist/types/components/dialog/alert-dialog.d.ts +19 -0
  14. package/dist/types/components/dialog/index.d.ts +22 -0
  15. package/dist/types/components/dialog/parts.d.ts +34 -0
  16. package/dist/types/components/field/fieldset.d.ts +11 -0
  17. package/dist/types/components/field/index.d.ts +24 -0
  18. package/dist/types/components/input/index.d.ts +19 -0
  19. package/dist/types/components/kbd/index.d.ts +5 -0
  20. package/dist/types/components/link/index.d.ts +11 -0
  21. package/dist/types/components/menu/index.d.ts +67 -0
  22. package/dist/types/components/popover/index.d.ts +28 -0
  23. package/dist/types/components/prose/index.d.ts +11 -0
  24. package/dist/types/components/radio-group/index.d.ts +15 -0
  25. package/dist/types/components/select/index.d.ts +46 -0
  26. package/dist/types/components/separator/index.d.ts +5 -0
  27. package/dist/types/components/skeleton/index.d.ts +13 -0
  28. package/dist/types/components/spinner/index.d.ts +10 -0
  29. package/dist/types/components/switch/index.d.ts +11 -0
  30. package/dist/types/components/table/index.d.ts +42 -0
  31. package/dist/types/components/tabs/index.d.ts +22 -0
  32. package/dist/types/components/textarea/index.d.ts +9 -0
  33. package/dist/types/components/toast/index.d.ts +19 -0
  34. package/dist/types/components/toggle/index.d.ts +11 -0
  35. package/dist/types/components/toggle-group/index.d.ts +13 -0
  36. package/dist/types/components/tooltip/index.d.ts +24 -0
  37. package/dist/types/index.d.ts +30 -0
  38. package/dist/types/internal/class-name.d.ts +7 -0
  39. package/dist/types/internal/popup.d.ts +9 -0
  40. package/dist/types/internal/size.d.ts +5 -0
  41. package/dist/types/types.d.ts +7 -0
  42. package/llms-full.txt +2181 -0
  43. package/llms.txt +53 -0
  44. package/package.json +34 -157
  45. package/dist/button.cjs +0 -74
  46. package/dist/button.d.cts +0 -15
  47. package/dist/button.d.ts +0 -15
  48. package/dist/button.js +0 -7
  49. package/dist/checkbox.cjs +0 -98
  50. package/dist/checkbox.d.cts +0 -16
  51. package/dist/checkbox.d.ts +0 -16
  52. package/dist/checkbox.js +0 -7
  53. package/dist/chunk-3DNMGJFC.js +0 -216
  54. package/dist/chunk-3VYNJ6UM.js +0 -27
  55. package/dist/chunk-3YY2G3JK.js +0 -125
  56. package/dist/chunk-5GQEJQKE.js +0 -60
  57. package/dist/chunk-6TNEFWVX.js +0 -9
  58. package/dist/chunk-BH4MPO4N.js +0 -25
  59. package/dist/chunk-F6GH4X7N.js +0 -34
  60. package/dist/chunk-HYN2P4OY.js +0 -84
  61. package/dist/chunk-OIUVLPTE.js +0 -54
  62. package/dist/chunk-S6LVYCE6.js +0 -157
  63. package/dist/chunk-SNSDJKJP.js +0 -44
  64. package/dist/chunk-WPO54YPG.js +0 -328
  65. package/dist/chunk-Z3ME5VRQ.js +0 -98
  66. package/dist/control-size-Bz2gyuyO.d.cts +0 -3
  67. package/dist/control-size-Bz2gyuyO.d.ts +0 -3
  68. package/dist/dialog.cjs +0 -203
  69. package/dist/dialog.d.cts +0 -34
  70. package/dist/dialog.d.ts +0 -34
  71. package/dist/dialog.js +0 -8
  72. package/dist/dropdown-menu.cjs +0 -378
  73. package/dist/dropdown-menu.d.cts +0 -60
  74. package/dist/dropdown-menu.d.ts +0 -60
  75. package/dist/dropdown-menu.js +0 -8
  76. package/dist/index.cjs +0 -1223
  77. package/dist/index.d.cts +0 -21
  78. package/dist/index.d.ts +0 -21
  79. package/dist/input.cjs +0 -67
  80. package/dist/input.d.cts +0 -11
  81. package/dist/input.d.ts +0 -11
  82. package/dist/input.js +0 -7
  83. package/dist/popover.cjs +0 -175
  84. package/dist/popover.d.cts +0 -53
  85. package/dist/popover.d.ts +0 -53
  86. package/dist/popover.js +0 -8
  87. package/dist/radio-group.cjs +0 -122
  88. package/dist/radio-group.d.cts +0 -25
  89. package/dist/radio-group.d.ts +0 -25
  90. package/dist/radio-group.js +0 -7
  91. package/dist/select.cjs +0 -266
  92. package/dist/select.d.cts +0 -45
  93. package/dist/select.d.ts +0 -45
  94. package/dist/select.js +0 -8
  95. package/dist/separator.cjs +0 -82
  96. package/dist/separator.d.cts +0 -12
  97. package/dist/separator.d.ts +0 -12
  98. package/dist/separator.js +0 -7
  99. package/dist/switch.cjs +0 -92
  100. package/dist/switch.d.cts +0 -16
  101. package/dist/switch.d.ts +0 -16
  102. package/dist/switch.js +0 -7
  103. package/dist/theme.css +0 -1097
  104. package/dist/tooltip.cjs +0 -148
  105. package/dist/tooltip.d.cts +0 -24
  106. package/dist/tooltip.d.ts +0 -24
  107. package/dist/tooltip.js +0 -8
package/llms-full.txt ADDED
@@ -0,0 +1,2181 @@
1
+ <!-- docs/README.md -->
2
+
3
+ # Memos UI design system
4
+
5
+ Memos UI is one calm, dense and tactile system for every product we build: memos (the app), the website, the web clipper and whatever comes next. It reads like ink on paper: pure grays, black ink for the one thing that matters, and no brand color competing with what people write. Controls have a little physical depth, so you can tell at a glance what holds a value and what you can press.
6
+
7
+ These docs describe the target system. They are the source of truth for `@usememos/ui`. Where the code in `packages/` differs from them, the code is out of date and should move toward the docs.
8
+
9
+ ## Who it is for
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.
12
+ - **People and coding agents** who build or change the package. Start with [`AGENTS.md`](../AGENTS.md).
13
+
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.
15
+
16
+ ## Contents
17
+
18
+ | Doc | What it covers |
19
+ | --- | --- |
20
+ | [Principles and voice](principles.md) | The taste in six rules, and how interface text is written |
21
+ | [Color](color.md) | Color roles, every color token in Light, Dark and High contrast, and how themes are chosen |
22
+ | [Depth](depth.md) | The tactile model: recessed, key, raised, popup and flat; shadow tokens |
23
+ | [Typography](typography.md) | The system font, the type scale and text rules |
24
+ | [Size, spacing and layout](layout.md) | Control sizes, the 4px scale, radius, layout widths, breakpoints and layers |
25
+ | [Motion](motion.md) | Press, settle and fade timing, and reduced motion |
26
+ | [States](states.md) | Hover, pressed, loading, disabled, invalid and focus |
27
+ | [Icons](icons.md) | Lucide, sizes, fixed stroke widths and accessibility |
28
+ | [Components](components/README.md) | The 28 components, what was left out, and one page per component |
29
+ | [Patterns](patterns.md) | Page, form, entry, confirmation, loading, empty state and mobile |
30
+ | [Building on it](building.md) | Base UI, loading the CSS, scoping inside a host page, theme selection and API conventions |
31
+
32
+ ## Decisions at a glance
33
+
34
+ - **Behavior from Base UI.** Focus management, keyboard support, ARIA, portals and positioning come from [Base UI](https://base-ui.com). Styling, tokens and the component API are our own.
35
+ - **No shadcn.** No shadcn dependency, no copied shadcn code, and no shadcn-compatible component or token names (`card`, `muted`, `destructive`, `foreground` and so on are not used).
36
+ - **Light, tactile by default.** Restrained depth that shows behavior. Light is the default theme; Dark and High contrast ship with it.
37
+ - **Ink is the accent.** The primary action and every on state use `primary`: near-black in Light, near-white in Dark.
38
+ - **One quiet hue.** `accent` is a quiet blue used only for focus rings, links and text selection, never as a fill.
39
+ - **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
+ - **The system UI font** for all text. The package ships no font files.
42
+ - **Dense, on a 4px grid.** Controls are 32px tall by default.
43
+
44
+ <!-- docs/principles.md -->
45
+
46
+ # Principles and voice
47
+
48
+ ## Principles
49
+
50
+ 1. **Content first.** The interface stays gray, so people's words, tags and images are the color on the page.
51
+ 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
+ 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.
54
+ 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
+ 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
+
57
+ ## Voice and interface text
58
+
59
+ - Sentence case everywhere: "Save note", not "Save Note".
60
+ - Control labels are one or two words. Buttons start with a verb.
61
+ - Address the reader as "you". Say what happened: "Note archived", not "Success!".
62
+ - Destructive confirmations name the object and the outcome: "Delete this note?" and "This can't be undone."
63
+ - Name an action with its verb, never "OK".
64
+ - Errors say how to fix the problem, not only what went wrong.
65
+ - No emoji in the interface. Numbers use digits ("3 notes").
66
+
67
+ ## What to avoid
68
+
69
+ - A brand hue on controls, or blue anywhere at rest except links.
70
+ - Cards and boxes around content. Group with spacing, a heading and a `Separator`.
71
+ - Textures, glass or blur behind text, and neumorphism.
72
+ - Pills for buttons, inputs or badges.
73
+ - Color as the only signal. Status colors always come with a word or an icon.
74
+ - Illustrations in empty states.
75
+
76
+ <!-- docs/color.md -->
77
+
78
+ # Color
79
+
80
+ Memos UI is ink on paper: pure-gray neutrals, `primary` ink for emphasis, one quiet blue for focus, links and selection, and status colors only when there is a status.
81
+
82
+ ## Roles
83
+
84
+ | Role | Tokens | Used for |
85
+ | --- | --- | --- |
86
+ | Surfaces | `canvas`, `surface`, `overlay` | The page; then inputs, menus, dialogs and toasts one step up. `overlay` dims the page behind dialogs |
87
+ | Wells | `well`, `edge` | Switch, tab, checkbox and radio wells, pressed toggles, inline code. `edge` is the 1px border of wells and inputs |
88
+ | Hover | `hover` | Rows, ghost buttons, the highlighted menu row. About 4% ink over surface |
89
+ | Keys | `key`, `key-hover`, `key-sheen` | Neutral buttons, select triggers, the selected tab, switch knobs, kbd |
90
+ | Text | `ink`, `ink-muted` | Primary and secondary text. Both pass 4.5:1 on every surface, well and key in every theme |
91
+ | Emphasis | `primary`, `primary-hover`, `on-primary` | Primary button, checked checkbox and radio, switch when on, tooltip |
92
+ | Meta | `accent`, `selection`, `focus` | Links, focus ring, text selection. Never a fill |
93
+ | Status | `success`, `warning`, `danger` and their `-soft` pairs; `danger-key`, `on-danger` | Toasts, badges, field errors, danger buttons |
94
+ | Lines | `line` | Separators, table rules, menu dividers |
95
+
96
+ ## Rules
97
+
98
+ - Neutrals are pure gray, never tinted.
99
+ - `accent` is a quiet blue used only for links, focus and selection. If a screen has blue on it at rest, apart from links, something is wrong.
100
+ - Status colors appear only when there is a status. Red is for destructive actions and errors, never for emphasis.
101
+ - `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.
102
+ - Always pair a status color with a word or an icon.
103
+ - 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.
104
+
105
+ ## Themes
106
+
107
+ There are three themes: **Light** (the default), **Dark** and **High contrast**.
108
+
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.
110
+ - 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
+ - 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
+
113
+ ### Choosing a theme
114
+
115
+ Set `data-theme="light" | "dark" | "contrast"` on the root element, or on any `.mu-root` element. Without the attribute the tokens are Light.
116
+
117
+ 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
+
119
+ ### App themes
120
+
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:
122
+
123
+ ```css
124
+ [data-theme="paper"] {
125
+ color-scheme: light;
126
+ --mu-canvas: #f5ede4;
127
+ --mu-surface: #fbf6f0;
128
+ /* every other color token, and the five shadow tokens */
129
+ }
130
+ ```
131
+
132
+ - 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.
133
+ - 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
+ - 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
+ - The tokens sit in the `mu` [cascade layer](building.md#cascade-layer), so an unlayered app theme wins over them at any specificity.
136
+ - An app theme is a choice the user makes. It is not a way to restyle the package per product: an app's default theme is Light or Dark.
137
+
138
+ ## Color tokens
139
+
140
+ | Token | Light | Dark | High contrast | Usage |
141
+ | --- | --- | --- | --- | --- |
142
+ | `canvas` | `#f9f9f9` | `#0f0f0f` | `#ffffff` | Page background |
143
+ | `surface` | `#ffffff` | `#1a1a1a` | `#ffffff` | One lightness step above canvas: inputs, menus, popovers, dialogs, toasts, table headers |
144
+ | `well` | `#e4e4e4` | `#262626` | `#e6e6e6` | 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 |
145
+ | `hover` | `#ededed` | `#242424` | `#d9d9d9` | Hover on rows, ghost buttons and wells; the highlighted row in menus and selects |
146
+ | `key` | `#ffffff` | `#343434` | `#ffffff` | Raised parts you press: neutral buttons, select triggers, the selected tab, switch knobs, kbd. Paired with `shadow-key` or `shadow-raised` |
147
+ | `key-hover` | `#fbfbfb` | `#3c3c3c` | `#f2f2f2` | Hover on a neutral key |
148
+ | `key-sheen` | `rgba(255,255,255,0.18)` | `rgba(255,255,255,0.08)` | `#ffffff00` | Top light on keys: a gradient from `key-sheen` at the top edge to clear at 60% of the height |
149
+ | `edge` | `#d0d0d0` | `#353535` | `#6b6b6b` | 1px border of inputs and wells. About 1.5:1 on canvas in Light and Dark; 5.3:1 in High contrast, where it also outlines keys |
150
+ | `line` | `#e0e0e0` | `#262626` | `#6b6b6b` | Separators, table rules and menu dividers. 1.25:1 in Light, so structural lines also need spacing or a heading |
151
+ | `overlay` | `rgba(0,0,0,0.32)` | `rgba(0,0,0,0.6)` | `rgba(0,0,0,0.6)` | Backdrop behind dialogs and sheets |
152
+ | `ink` | `#1a1a1a` | `#ededed` | `#000000` | Primary text and icons on canvas, surface, well and key (13:1 or more in every theme) |
153
+ | `ink-muted` | `#5e5e5e` | `#adadad` | `#333333` | Secondary text: descriptions, placeholders, unselected tabs and toggles, shortcuts, timestamps (4.9:1 or more on every surface) |
154
+ | `primary` | `#1f1f1f` | `#e6e6e6` | `#000000` | The one emphasized action and every on state: primary buttons, checked boxes and radios, switches when on, tooltips. Ink, not a hue |
155
+ | `primary-hover` | `#333333` | `#f5f5f5` | `#262626` | Hover on a primary key |
156
+ | `on-primary` | `#ffffff` | `#111111` | `#ffffff` | Text, icons and the switch knob on `primary` (16:1 Light, 15:1 Dark) |
157
+ | `accent` | `oklch(0.5 0.13 255)` | `oklch(0.78 0.11 255)` | `oklch(0.42 0.14 255)` | The only hue at rest: focus rings, links, the text cursor and selection. 5.8:1 on canvas in Light. Never a fill |
158
+ | `selection` | `rgba(40,99,171,0.18)` | `rgba(135,186,253,0.28)` | `rgba(0,75,151,0.25)` | Text selection and the selected range in an editor |
159
+ | `focus` | `var(--mu-accent)` | `var(--mu-accent)` | `var(--mu-accent)` | Focus ring: 2px outline, 2px offset, on `:focus-visible` only |
160
+ | `success` | `oklch(0.52 0.13 150)` | `oklch(0.78 0.14 150)` | `oklch(0.42 0.12 150)` | Success text and icons on `surface` or `success-soft` (4.6:1 or more). Always with a word or icon |
161
+ | `success-soft` | `#e9f4ec` | `#14261a` | `#e9f4ec` | Background of success badges and toasts |
162
+ | `warning` | `oklch(0.5 0.11 70)` | `oklch(0.8 0.13 75)` | `oklch(0.45 0.11 70)` | Warning text and icons on `surface` or `warning-soft` (5.5:1 or more) |
163
+ | `warning-soft` | `#fbf1df` | `#2a2112` | `#fbf1df` | Background of warning badges and toasts |
164
+ | `danger` | `oklch(0.53 0.19 27)` | `oklch(0.72 0.16 25)` | `oklch(0.45 0.18 27)` | Danger text and icons: field errors, delete menu items, danger badges (5.0:1 or more on `surface` and `danger-soft`) |
165
+ | `danger-key` | `oklch(0.53 0.19 27)` | `oklch(0.55 0.17 25)` | `oklch(0.45 0.18 27)` | Fill of a danger button. 5.3:1 or more with `on-danger` |
166
+ | `danger-soft` | `#fbeaea` | `#2c1616` | `#fbeaea` | Background of danger badges and error toasts |
167
+ | `on-danger` | `#ffffff` | `#ffffff` | `#ffffff` | Text on a danger key |
168
+
169
+ Every text and background pair in this table keeps at least 4.5:1 in every theme. Depth (see [Depth](depth.md)) is added on top and never lowers contrast.
170
+
171
+ <!-- docs/depth.md -->
172
+
173
+ # Depth
174
+
175
+ 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.
176
+
177
+ ## Roles
178
+
179
+ | Role | Class | Used for | Background | Shadow |
180
+ | --- | --- | --- | --- | --- |
181
+ | 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 |
182
+ | Key in a well | `mu-key` | The selected tab or segment, switch knobs | `key` with the `key-sheen` gradient | `shadow-key` |
183
+ | Raised | `mu-raised` | Buttons, select triggers, kbd | `key` or `primary` with `key-sheen` | `shadow-raised`; `shadow-pressed` while held |
184
+ | Popup | `mu-popup` | Menus, popovers, dialogs, sheets, toasts | `surface` | `shadow-pop` |
185
+ | Flat | none | Inputs and textareas, checked boxes and radios, content | `surface` with `edge`, or `primary` | None |
186
+
187
+ Each depth class applies a whole role in one class. Use them for new parts before writing new shadows.
188
+
189
+ ```css
190
+ .mu-recessed { background-color: var(--mu-well); box-shadow: inset 0 0 0 1px var(--mu-edge), var(--mu-shadow-track); }
191
+ .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); }
192
+ .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); }
193
+ .mu-popup { background: var(--mu-surface); box-shadow: var(--mu-shadow-pop); border-radius: var(--mu-radius-xl); }
194
+ ```
195
+
196
+ ## Rules
197
+
198
+ - Light always comes from the top.
199
+ - 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.
200
+ - 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.
201
+ - **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.
202
+ - Content areas are flat. There is no card component: group content with spacing, a heading and a `Separator`.
203
+ - 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.
204
+ - In High contrast, depth turns into outlines on every well and key, and `shadow-pressed` becomes an inner ring.
205
+
206
+ ## Shadow tokens
207
+
208
+ | Token | Usage |
209
+ | --- | --- |
210
+ | `shadow-track` | Recessed parts: wells and the faint top shadow inside inputs. The border comes from `edge` |
211
+ | `shadow-key` | Keys inside a well: the switch knob, the selected tab or segment |
212
+ | `shadow-raised` | Free-standing keys on canvas or surface: buttons, select triggers, kbd |
213
+ | `shadow-pressed` | A key while held |
214
+ | `shadow-pop` | Menus, selects, popovers, dialogs, sheets and toasts: the floating layer |
215
+
216
+ ### Light
217
+
218
+ ```css
219
+ --mu-shadow-track: inset 0 1px 2px rgba(0,0,0,0.10);
220
+ --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);
221
+ --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);
222
+ --mu-shadow-pressed: inset 0 1px 3px rgba(0,0,0,0.22), 0 0 0 0.5px rgba(0,0,0,0.12);
223
+ --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);
224
+ ```
225
+
226
+ ### Dark
227
+
228
+ ```css
229
+ --mu-shadow-track: inset 0 1px 2px rgba(0,0,0,0.55), inset 0 -0.5px 0 rgba(255,255,255,0.06);
230
+ --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);
231
+ --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);
232
+ --mu-shadow-pressed: inset 0 1px 3px rgba(0,0,0,0.6), 0 0 0 0.5px rgba(0,0,0,0.6);
233
+ --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);
234
+ ```
235
+
236
+ ### High contrast
237
+
238
+ ```css
239
+ --mu-shadow-track: 0 0 0 0 rgba(0,0,0,0);
240
+ --mu-shadow-key: 0 0 0 1px #6b6b6b;
241
+ --mu-shadow-raised: 0 0 0 1px #000000;
242
+ --mu-shadow-pressed: inset 0 0 0 2px #000000;
243
+ --mu-shadow-pop: 0 0 0 1px #000000;
244
+ ```
245
+
246
+ <!-- docs/typography.md -->
247
+
248
+ # Typography
249
+
250
+ ## Rules
251
+
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`.
258
+ - 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.
260
+
261
+ ## Fonts
262
+
263
+ | Family | Stack |
264
+ | --- | --- |
265
+ | `--mu-font-sans` | `system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif` |
266
+ | `--mu-font-display` | `system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif` |
267
+ | `--mu-font-mono` | `ui-monospace, "SF Mono", Menlo, Consolas, monospace` |
268
+
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.
270
+
271
+ ## Scale
272
+
273
+ ### Text (`--mu-font-sans`)
274
+
275
+ | Style | Size / line | Weight | Usage |
276
+ | --- | --- | --- | --- |
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 |
285
+
286
+ ### Headings (`--mu-font-display`)
287
+
288
+ | Style | Size / line | Weight | Letter spacing | Usage |
289
+ | --- | --- | --- | --- | --- |
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 |
295
+
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.
297
+
298
+ ## Prose
299
+
300
+ Rendered content (notes, comments, changelogs, website pages) uses the `Prose` styles, not the scale above. See [Prose](components/prose.md).
301
+
302
+ <!-- docs/layout.md -->
303
+
304
+ # Size, spacing and layout
305
+
306
+ ## Control sizes
307
+
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.
309
+
310
+ | Size | Height | Padding | Icon | Icon gap | Text | Radius | Check box |
311
+ | --- | --- | --- | --- | --- | --- | --- | --- |
312
+ | sm | `control-sm` 24px | 8px | 14px | 4px | 12 / 16px | `radius-sm` 4px | 16px |
313
+ | 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
+
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.
317
+ - The icon side of a control has 2px less padding, so text and icon look centered.
318
+ - On touch screens (`pointer: coarse`), every control gets an invisible hit area of at least `hit-area` (44px) without changing its visual size.
319
+
320
+ ## Spacing
321
+
322
+ 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
+
324
+ - 0 inside a well.
325
+ - 2px between items inside a cluster (a toolbar).
326
+ - 8px between controls.
327
+ - 16px between fields.
328
+ - 24px between groups.
329
+
330
+ | Token | Value | Usage |
331
+ | --- | --- | --- |
332
+ | `space-0_5` | 2px | Only for insets: the gap between a well and the key inside it |
333
+ | `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 |
335
+ | `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 |
337
+ | `space-5` | 20px | Padding inside dialogs; space above Prose headings |
338
+ | `space-6` | 24px | Gap between sections or entries inside a page; page padding on desktop |
339
+ | `space-8` | 32px | Gap between page regions |
340
+ | `space-12` | 48px | Section rhythm on a website |
341
+ | `space-16` | 64px | Hero and footer padding on a website |
342
+
343
+ ## Radius
344
+
345
+ Inner radius = outer radius − `inset`, so nested corners stay concentric.
346
+
347
+ | Token | Value | Usage |
348
+ | --- | --- | --- |
349
+ | `radius-xs` | 2px | Keys inside sm wells (`radius-sm` minus the 2px inset); inline code |
350
+ | `radius-sm` | 4px | sm controls; badges |
351
+ | `radius-md` | 6px | md controls, the default |
352
+ | `radius-lg` | 8px | lg controls; images in Prose |
353
+ | `radius-xl` | 12px | Menus, select lists, popovers and toasts |
354
+ | `radius-2xl` | 16px | Dialogs and sheets |
355
+ | `radius-full` | 9999px | Avatars, radios, switches and their knobs only |
356
+
357
+ Buttons, inputs and badges are never pills.
358
+
359
+ ## Sizes
360
+
361
+ | Token | Value | Usage |
362
+ | --- | --- | --- |
363
+ | `control-sm` | 24px | sm control height |
364
+ | `control-md` | 32px | Default control height |
365
+ | `control-lg` | 40px | lg control height |
366
+ | `inset` | 2px | Well-to-key inset at every size |
367
+ | `hit-area` | 44px | Minimum touch target on `pointer: coarse`, added invisibly around a control |
368
+ | `focus-width` | 2px | Width and offset of the focus ring |
369
+ | `icon-sm` | 14px | Icons in sm controls |
370
+ | `icon-md` | 16px | Icons in md controls and nav rows |
371
+ | `icon-lg` | 20px | Icons in lg controls |
372
+
373
+ ## Layout
374
+
375
+ | Token | Value | Usage |
376
+ | --- | --- | --- |
377
+ | `content-width` | 720px | Maximum width of a reading column: a timeline, a settings form, an article |
378
+ | `sidebar-width` | 240px | Width of a fixed navigation sidebar from `breakpoint-md` up |
379
+ | `menu-width` | 200px | Minimum width of a menu or select list |
380
+ | `list-height` | 240px | Maximum height of a combobox list before it scrolls |
381
+ | `popover-width` | 280px | Default width of a popover |
382
+ | `tooltip-width` | 240px | Maximum width of a tooltip |
383
+ | `toast-width` | 360px | Width of a toast from `breakpoint-sm` up |
384
+ | `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
+
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.
389
+
390
+ ## Layers
391
+
392
+ | Token | Value | Usage |
393
+ | --- | --- | --- |
394
+ | `layer-popup` | 50 | Menus, select lists, popovers, tooltips. Equal to `layer-overlay` on purpose: the one opened last is on top, so a menu opened inside a dialog shows above it |
395
+ | `layer-overlay` | 50 | Dialogs, sheets and their backdrops |
396
+ | `layer-toast` | 60 | Toasts, above everything |
397
+
398
+ Use these tokens for every `z-index`. Don't invent new layers in product code.
399
+
400
+ <!-- docs/motion.md -->
401
+
402
+ # Motion
403
+
404
+ 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.
405
+
406
+ ## Rules
407
+
408
+ - **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%.
409
+ - **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.
410
+ - **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.
411
+ - **Tooltips** wait 400ms on hover before they open, and open at once on keyboard focus.
412
+ - **Collapsible content** fades and rises in. There is no height animation, so long content never stutters.
413
+ - **Spinners** turn once every 0.8s. **Skeletons** shimmer once every 1.4s.
414
+ - **Reduced motion.** Under `prefers-reduced-motion`, keep color, opacity and shadow changes, and drop slides, scaling and shimmer.
415
+
416
+ ## Tokens
417
+
418
+ | Token | Value | Usage |
419
+ | --- | --- | --- |
420
+ | `duration-press` | 80ms | Pointer down: keys sink, knobs grow |
421
+ | `duration-settle` | 150ms | Knobs and keys slide on release; dialogs open |
422
+ | `duration-fade` | 100ms | Popups open; content inside a control fades in after the key stops |
423
+ | `duration-spin` | 800ms | One turn of a Spinner |
424
+ | `duration-shimmer` | 1400ms | One pass of a Skeleton's shimmer |
425
+ | `ease-settle` | `cubic-bezier(0.2, 0.9, 0.3, 1)` | Slides: hard deceleration at the end, like a light spring |
426
+ | `ease-out` | `cubic-bezier(0.2, 0, 0, 1)` | Press and fades |
427
+
428
+ <!-- docs/states.md -->
429
+
430
+ # States
431
+
432
+ Every interactive component handles these states the same way.
433
+
434
+ | State | Looks like |
435
+ | --- | --- |
436
+ | 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` |
437
+ | Pressed (while held) | Keys move down 0.5px and use `shadow-pressed`. A ghost button sinks into a `well` with `shadow-track` |
438
+ | Pressed toggle (on) | Sinks into a `well` with an `edge` and `ink` text |
439
+ | Selected (one of several) | A raised key in a well that slides to the chosen item |
440
+ | Checked | A flat `primary` fill with an `on-primary` mark |
441
+ | Loading | The control keeps its size, hides its label and shows a Spinner. It is inert while loading |
442
+ | Disabled | 45% opacity, `cursor: not-allowed`, and no hover or press response |
443
+ | Invalid | A `danger` edge on the field, plus an error line with an icon under it |
444
+ | Focus | A 2px `focus` outline with a 2px offset, on `:focus-visible` only |
445
+
446
+ ## Rules
447
+
448
+ - Hover never changes size, weight or layout.
449
+ - An input never changes its background on hover or focus, so it never looks disabled.
450
+ - Focus rings show for keyboard focus only (`:focus-visible`), never on mouse click.
451
+ - Disabled controls stay in place. Prefer explaining why something is unavailable over hiding it.
452
+ - Loading never shows a spinner for less than about 300ms; short waits pass silently.
453
+
454
+ <!-- docs/icons.md -->
455
+
456
+ # Icons
457
+
458
+ Memos UI uses [Lucide](https://lucide.dev) (ISC license) for every icon. Apps import icons from `lucide-react` and pass them to components as elements. The design system does not wrap or re-export the set.
459
+
460
+ ```tsx
461
+ import { Pin, Trash2 } from "lucide-react";
462
+
463
+ <Button icon={<Pin />}>Pin</Button>
464
+ <Menu items={[{ value: "delete", label: "Delete", icon: <Trash2 />, danger: true }]} />
465
+ ```
466
+
467
+ ## Rules
468
+
469
+ - **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`.
472
+ - **Color is `currentColor`.** Icons follow the text color: `ink` in controls, `ink-muted` in menu rows, the tone color in toasts.
473
+ - **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
+ - **Accessibility.** Icons are `aria-hidden`. Every icon-only Button or Toggle has an `aria-label`, and a Tooltip with the same text.
475
+ - **Standalone icons.** Outside a component, add `className="mu-icon"` to get the same size and stroke rules.
476
+
477
+ ## Built-in glyphs
478
+
479
+ These components bring their own Lucide icons, so the app doesn't pass them:
480
+
481
+ | Component | Icons |
482
+ | --- | --- |
483
+ | Checkbox | `Check` |
484
+ | Select | `ChevronDown`, `Check` |
485
+ | Combobox | `Search`, `ChevronDown`, `Check` |
486
+ | Menu | `Ellipsis` (default trigger), `Check` |
487
+ | Collapsible | `ChevronRight` (turns 90° when open) |
488
+ | Toast | `Info`, `CircleCheck`, `TriangleAlert`, `CircleAlert`, `X` |
489
+ | Field | `CircleAlert` (errors) |
490
+ | Link | `ArrowUpRight` (external links) |
491
+
492
+ ## Missing icons
493
+
494
+ When Lucide doesn't have an icon, draw it on Lucide's 24px grid with 2px round strokes and round joins, then export it from the package under a Lucide-style name (`PascalCase`, describing the shape, not the feature).
495
+
496
+ <!-- docs/components/README.md -->
497
+
498
+ # Components
499
+
500
+ 28 components in five groups, chosen from what Base UI and other popular libraries provide and what our products need today. Every component is generic: it works the same in memos, the website, the web clipper and any future product.
501
+
502
+ | Group | Components |
503
+ | --- | --- |
504
+ | Actions | [Button](button.md), [Toggle](toggle.md), [ToggleGroup](toggle-group.md), [Menu](menu.md), [Link](link.md) |
505
+ | Forms | [Field and Fieldset](field.md), [Input](input.md), [Textarea](textarea.md), [Select](select.md), [Combobox](combobox.md), [Checkbox](checkbox.md), [Switch](switch.md), [RadioGroup](radio-group.md) |
506
+ | Navigation | [Tabs](tabs.md), [Collapsible](collapsible.md) |
507
+ | Overlays | [Dialog](dialog.md) (and its sheet form), [Popover](popover.md), [Tooltip](tooltip.md), [Toast](toast.md) |
508
+ | Display | [Avatar](avatar.md), [Badge](badge.md), [Kbd](kbd.md), [Separator](separator.md), [Spinner](spinner.md), [Skeleton](skeleton.md), [Table](table.md), [Prose](prose.md), plus [Lucide icons](../icons.md) |
509
+
510
+ ## Page template
511
+
512
+ Pages are reference for agents: short, imperative, no narrative. Every page has these sections, in this order:
513
+
514
+ 1. **Title and purpose.** `# Name`, then one line: what it is for.
515
+ 2. **Example.** One complete, copyable example of the common case.
516
+ 3. **Parts.** One row per part: name, the Base UI part it wraps, what it renders. Skip for single-element components.
517
+ 4. **Props.** A TypeScript block per part, only for props we add or change, then one line on which Base UI props pass through.
518
+ 5. **Rules.** Imperative bullets: when to use it, what to use instead, required labels, keyboard behavior.
519
+ 6. **Style.** Bullets for building the component: tokens, sizes, states and motion.
520
+
521
+ Components follow the [API conventions](../building.md#api-conventions): parts with Base UI's names, no data props.
522
+
523
+ ## Left out on purpose
524
+
525
+ - **Card.** Content is grouped by spacing, headings and separators, not boxes.
526
+ - **Slider, Stepper.** No setting in our products needs a continuous range. Use an Input with `type="number"` or a Select.
527
+ - **Chip, SegmentedControl, Accordion.** A Toggle covers filter chips, a single-choice ToggleGroup covers segmented choices, and stacked Collapsibles cover an accordion.
528
+ - **Later, when a screen needs them:** multi-select Combobox, ContextMenu, ScrollArea, Progress, Calendar.
529
+
530
+ ## Choosing between similar components
531
+
532
+ | Need | Use |
533
+ | --- | --- |
534
+ | Start an action | Button |
535
+ | Go somewhere | Link, or an anchor with `buttonClasses()` when it should look like an action |
536
+ | A setting that saves at once | Switch |
537
+ | An option saved with a form | Checkbox |
538
+ | One of 2 or 3 options, with room to show them | RadioGroup |
539
+ | One of a short list | Select |
540
+ | One of a long list, or typing to find | Combobox |
541
+ | On/off in a toolbar, or stacking filters | Toggle, or ToggleGroup with `multiple` |
542
+ | One of several views or filters on the same page | ToggleGroup without `multiple` |
543
+ | Sections of a page | Tabs |
544
+ | A list of actions for one thing | Menu |
545
+ | A few controls that belong to one thing | Popover |
546
+ | A decision that blocks the page | Dialog |
547
+ | Name an icon-only control | Tooltip |
548
+ | Confirm something happened | Toast |
549
+ | A status or tag label | Badge |
550
+ | A clickable tag filter | Toggle |
551
+ | Content still loading | Skeleton |
552
+ | A control waiting on a server | the control's `loading` prop |
553
+
554
+ ## Shared types
555
+
556
+ ```ts
557
+ type Size = "sm" | "md" | "lg";
558
+ type Tone = "neutral" | "success" | "warning" | "danger";
559
+ /** A Lucide icon element, such as <Pin /> from lucide-react. Size and stroke come from the component. */
560
+ type IconElement = React.ReactElement;
561
+ ```
562
+
563
+ See [Building on it](../building.md#api-conventions) for the conventions every component follows.
564
+
565
+ <!-- docs/components/button.md -->
566
+
567
+ # Button
568
+
569
+ Starts an action.
570
+
571
+ ## Example
572
+
573
+ ```tsx
574
+ import { Button } from "@usememos/ui";
575
+ import { Plus } from "lucide-react";
576
+
577
+ <Button variant="primary" icon={<Plus />}>New note</Button>
578
+ ```
579
+
580
+ ## Props
581
+
582
+ ```ts
583
+ interface ButtonProps extends BaseButton.Props {
584
+ variant?: "primary" | "neutral" | "ghost" | "danger"; // default "neutral"
585
+ size?: Size;
586
+ icon?: IconElement;
587
+ loading?: boolean;
588
+ loadingLabel?: string; // accessible name while loading, such as "Saving"
589
+ }
590
+
591
+ function buttonClasses(options?: { variant?: ButtonVariant; size?: Size; iconOnly?: boolean }): string;
592
+ ```
593
+
594
+ Wraps Base UI's [Button](https://base-ui.com/react/components/button): `disabled`, `focusableWhenDisabled`, `render`, `nativeButton` and native button attributes pass through. Renders `<button type="button">`.
595
+
596
+ ## Rules
597
+
598
+ - Label with a verb in sentence case ("Save", "Copy link"). With only an `icon`, pass `aria-label` and add a [Tooltip](tooltip.md) with the same text.
599
+ - `primary`: the one action a view exists for, at most one per view. `neutral`: every other action. `ghost`: toolbars, dense rows, the "More actions" trigger. `danger`: confirming a destructive action inside a dialog, never as a first click.
600
+ - Use `loading` for any action that waits on a server.
601
+ - A link that should look like a button stays an `<a>` with `buttonClasses()`; Base UI doesn't render links through Button:
602
+
603
+ ```tsx
604
+ <a className={buttonClasses({ variant: "primary" })} href="/start">Get started</a>
605
+ ```
606
+
607
+ - Prefer saying why an action is unavailable over disabling it.
608
+
609
+ ## Style
610
+
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.
612
+ - Only an icon: square. The icon side has 2px less padding.
613
+ - 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
+ - `loading` keeps the width, shows a Spinner over the label, sets `aria-busy` and `aria-disabled`, ignores presses and stays focusable.
615
+ - Disabled: 45% opacity.
616
+
617
+ <!-- docs/components/toggle.md -->
618
+
619
+ # Toggle
620
+
621
+ Turns one option on or off and stays pressed while on.
622
+
623
+ ## Example
624
+
625
+ ```tsx
626
+ import { Toggle } from "@usememos/ui";
627
+ import { Pin } from "lucide-react";
628
+
629
+ <Toggle icon={<Pin />} pressed={pinnedOnly} onPressedChange={setPinnedOnly}>Pinned</Toggle>
630
+ ```
631
+
632
+ ## Props
633
+
634
+ ```ts
635
+ interface ToggleProps extends BaseToggle.Props {
636
+ size?: Size;
637
+ icon?: IconElement; // before the label; alone, the toggle is square
638
+ }
639
+ ```
640
+
641
+ Wraps Base UI's [Toggle](https://base-ui.com/react/components/toggle): `pressed`, `defaultPressed`, `onPressedChange`, `disabled`, `value` pass through. Renders a `<button aria-pressed>`.
642
+
643
+ ## Rules
644
+
645
+ - Provide `children`, or an `icon` with `aria-label` and a [Tooltip](tooltip.md) with the same text.
646
+ - Use it for toolbar formatting and stacking filters ("Pinned", "Has tasks"). A clickable tag filter is a Toggle, not a Badge.
647
+ - Inside a [ToggleGroup](toggle-group.md), `value` names the item in the group's value.
648
+ - For a setting that saves at once, use [Switch](switch.md). For one choice among several, use a ToggleGroup without `multiple`.
649
+
650
+ ## Style
651
+
652
+ - Off: flat `ink-muted`. On: sits in a `well` with `ink` text.
653
+
654
+ <!-- docs/components/toggle-group.md -->
655
+
656
+ # ToggleGroup
657
+
658
+ A row of related [Toggles](toggle.md). With `multiple`: a toolbar of independent on/off toggles. Without: one choice among peers, the same control as [Tabs](tabs.md) without a panel.
659
+
660
+ ## Example
661
+
662
+ ```tsx
663
+ import { Separator, Toggle, ToggleGroup } from "@usememos/ui";
664
+ import { Bold, Code, Italic } from "lucide-react";
665
+
666
+ <ToggleGroup value={[filter]} onValueChange={([next]) => next && setFilter(next)} aria-label="Show">
667
+ <Toggle value="all">All</Toggle>
668
+ <Toggle value="pinned">Pinned</Toggle>
669
+ <Toggle value="archived">Archived</Toggle>
670
+ </ToggleGroup>
671
+
672
+ <ToggleGroup multiple value={marks} onValueChange={setMarks} aria-label="Formatting">
673
+ <Toggle value="bold" icon={<Bold />} aria-label="Bold" />
674
+ <Toggle value="italic" icon={<Italic />} aria-label="Italic" />
675
+ <Separator orientation="vertical" />
676
+ <Toggle value="code" icon={<Code />} aria-label="Code" />
677
+ </ToggleGroup>
678
+ ```
679
+
680
+ ## Parts
681
+
682
+ | Part | Base UI part | Renders |
683
+ | --- | --- | --- |
684
+ | `ToggleGroup` | `ToggleGroup` | A `<div role="group">`. Takes `size`. Without `multiple`, adds the sliding key |
685
+ | `Toggle` | `Toggle` | One item, a `<button aria-pressed>` |
686
+ | `Separator` | `Separator` | A line between clusters, `multiple` groups only |
687
+
688
+ ## Props
689
+
690
+ ```ts
691
+ interface ToggleGroupProps extends BaseToggleGroup.Props {
692
+ size?: Size; // passed to every Toggle inside
693
+ }
694
+ ```
695
+
696
+ Base UI's props pass through: `value`, `defaultValue`, `onValueChange`, `multiple`, `disabled`, `orientation`, `loopFocus`. The value is always an array of pressed `value`s, even without `multiple`.
697
+
698
+ ## Rules
699
+
700
+ - Give the group an `aria-label`.
701
+ - 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.
702
+ - Without `multiple`: filters and view switches that don't change the whole page. For sections of a page, use Tabs.
703
+ - 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.
704
+ - Arrow keys move focus between items; Space or Enter presses one.
705
+
706
+ ## Style
707
+
708
+ - `multiple`: items 2px apart.
709
+ - Without `multiple`: a segmented well; the key slides to the chosen item over `duration-settle` with `ease-settle` and re-measures when an item changes width. Under reduced motion it jumps.
710
+
711
+ <!-- docs/components/menu.md -->
712
+
713
+ # Menu
714
+
715
+ Lists actions for one thing, opened from a trigger.
716
+
717
+ ## Example
718
+
719
+ ```tsx
720
+ import { Button, Menu } from "@usememos/ui";
721
+ import { Ellipsis, FolderInput, Pin, Trash2 } from "lucide-react";
722
+
723
+ <Menu.Root>
724
+ <Menu.Trigger render={<Button variant="ghost" icon={<Ellipsis />} aria-label="More actions" />} />
725
+ <Menu.Popup align="end">
726
+ <Menu.Item icon={<Pin />} shortcut="P" onClick={pin}>Pin</Menu.Item>
727
+ <Menu.SubmenuRoot>
728
+ <Menu.SubmenuTrigger icon={<FolderInput />}>Move to</Menu.SubmenuTrigger>
729
+ <Menu.Popup>
730
+ <Menu.Item onClick={() => move("inbox")}>Inbox</Menu.Item>
731
+ </Menu.Popup>
732
+ </Menu.SubmenuRoot>
733
+ <Menu.Separator />
734
+ <Menu.Item icon={<Trash2 />} danger onClick={confirmDelete}>Delete</Menu.Item>
735
+ </Menu.Popup>
736
+ </Menu.Root>
737
+ ```
738
+
739
+ ## Parts
740
+
741
+ | Part | Base UI part | Renders |
742
+ | --- | --- | --- |
743
+ | `Menu.Root` | `Menu.Root` | Nothing. Groups a trigger and its popup. Takes `size` |
744
+ | `Menu.Trigger` | `Menu.Trigger` | The control that opens it, usually a Button through `render` |
745
+ | `Menu.Popup` | `Menu.Portal`, `Positioner`, `Popup` | The list, a `<div role="menu">` portaled to the body. Also a submenu's list |
746
+ | `Menu.Item` | `Menu.Item` | One action. Takes `icon`, `shortcut`, `danger` |
747
+ | `Menu.CheckboxItem` | `Menu.CheckboxItem` with its indicator | An option that turns on or off, with a check at the end |
748
+ | `Menu.RadioGroup` | `Menu.RadioGroup` | Groups RadioItems; holds the value |
749
+ | `Menu.RadioItem` | `Menu.RadioItem` with its indicator | One choice, with a check at the end when chosen |
750
+ | `Menu.Group` | `Menu.Group` | A labeled set of items |
751
+ | `Menu.GroupLabel` | `Menu.GroupLabel` | The name of a Group or RadioGroup |
752
+ | `Menu.Separator` | `Menu.Separator` | A line between groups |
753
+ | `Menu.SubmenuRoot` | `Menu.SubmenuRoot` | Nothing. Holds a SubmenuTrigger and a Popup |
754
+ | `Menu.SubmenuTrigger` | `Menu.SubmenuTrigger` | The row that opens a submenu, with a chevron at the end. Takes `icon` |
755
+
756
+ ## Props
757
+
758
+ ```ts
759
+ interface Menu.RootProps extends BaseMenu.Root.Props {
760
+ size?: Size; // row height: 24, 32, 40px
761
+ }
762
+ 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
764
+ container?: HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null; // portal target
765
+ }
766
+ interface Menu.ItemProps extends BaseMenu.Item.Props {
767
+ icon?: IconElement;
768
+ shortcut?: React.ReactNode; // at the end of the row, "⌘ D"
769
+ danger?: boolean; // destructive: red text
770
+ }
771
+ // CheckboxItem and RadioItem take icon and shortcut too. SubmenuTrigger takes icon.
772
+ ```
773
+
774
+ 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.
775
+
776
+ ## Rules
777
+
778
+ - Act in `onClick`. Labels are verbs in sentence case.
779
+ - Put destructive items last, after a Separator, with `danger`. Confirm them with an [AlertDialog](dialog.md).
780
+ - CheckboxItems and RadioItems stay open on click, so people can change several.
781
+ - For picking a value saved with a form, use [Select](select.md).
782
+ - Arrow keys move the highlight; typing jumps to an item; Escape and an outside click close it; focus returns to the trigger.
783
+
784
+ ## Style
785
+
786
+ - `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`.
788
+ - The highlighted row uses `hover`. Disabled rows fade to 45%. `danger` rows use `danger`.
789
+ - A ghost trigger keeps its `hover` background while the menu is open.
790
+ - Fades in and rises `space-1` over `duration-fade`; no rise under reduced motion.
791
+
792
+ <!-- docs/components/link.md -->
793
+
794
+ # Link
795
+
796
+ Goes somewhere.
797
+
798
+ ## Example
799
+
800
+ ```tsx
801
+ import { Link } from "@usememos/ui";
802
+
803
+ <Link href="/notes">View all</Link>
804
+ <p>Read the <Link href="/changelog" underline="always">release notes</Link> first.</p>
805
+ <Link href="https://www.usememos.com" external>usememos.com</Link>
806
+ ```
807
+
808
+ ## Props
809
+
810
+ ```ts
811
+ interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
812
+ muted?: boolean; // navigation that shouldn't draw the eye
813
+ external?: boolean; // opens a new tab and shows an arrow
814
+ underline?: "hover" | "always"; // default "hover"
815
+ render?: React.ReactElement; // a router's link: render={<RouterLink to="/notes" />}
816
+ }
817
+ ```
818
+
819
+ Provide `href` and `children`.
820
+
821
+ ## Rules
822
+
823
+ - A link navigates; a button acts. A "Get started" that goes to a page is an anchor with [`buttonClasses()`](button.md#rules). "View all" is a link.
824
+ - Inside running text, use `underline="always"`, so the link isn't told apart by color alone.
825
+ - Inside [Prose](prose.md), plain anchors get this style without the component.
826
+
827
+ ## Style
828
+
829
+ - `accent` text that underlines on hover: the one place the hue appears at rest. `muted` uses `ink-muted`.
830
+ - High contrast underlines every link.
831
+
832
+ <!-- docs/components/field.md -->
833
+
834
+ # Field and Fieldset
835
+
836
+ Field gives one control its label, description and error, wired for screen readers. Fieldset groups checkboxes, radios or switches under one legend.
837
+
838
+ ## Example
839
+
840
+ ```tsx
841
+ import { Checkbox, Field, Fieldset, Input } from "@usememos/ui";
842
+
843
+ <Field.Root invalid={!!error}>
844
+ <Field.Label>Email</Field.Label>
845
+ <Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
846
+ <Field.Description>We only use it to sign you in.</Field.Description>
847
+ <Field.Error match>{error}</Field.Error>
848
+ </Field.Root>
849
+
850
+ <Fieldset.Root>
851
+ <Fieldset.Legend>Notifications</Fieldset.Legend>
852
+ <Field.Root>
853
+ <Field.Label><Checkbox defaultChecked /> Mentions</Field.Label>
854
+ </Field.Root>
855
+ <Field.Root>
856
+ <Field.Label><Checkbox /> Weekly digest</Field.Label>
857
+ </Field.Root>
858
+ </Fieldset.Root>
859
+ ```
860
+
861
+ ## Parts
862
+
863
+ | Part | Base UI part | Renders |
864
+ | --- | --- | --- |
865
+ | `Field.Root` | `Field.Root` | A `<div>` around one control. Takes `size` and passes it to the control |
866
+ | `Field.Label` | `Field.Label` | A `<label>`: above a text control, or around a Checkbox, Switch or Radio |
867
+ | `Field.Item` | `Field.Item` | A `<div>` around one option of a radio or checkbox group in one field |
868
+ | `Field.Description` | `Field.Description` | A `<p>` of help under the control |
869
+ | `Field.Error` | `Field.Error` | A `<div>` with a `CircleAlert` icon and the message, while the field is invalid |
870
+ | `Fieldset.Root` | `Fieldset.Root` | A `<fieldset>`. Takes `size` |
871
+ | `Fieldset.Legend` | `Fieldset.Legend` | The group's name |
872
+
873
+ ## Props
874
+
875
+ ```ts
876
+ interface Field.RootProps extends BaseField.Root.Props {
877
+ size?: Size; // passed to the control inside
878
+ }
879
+ interface Fieldset.RootProps extends BaseFieldset.Root.Props {
880
+ size?: Size; // passed to every control inside
881
+ }
882
+ ```
883
+
884
+ Base UI's props pass through: `name`, `invalid`, `disabled`, `validate`, `validationMode` on `Field.Root`; `match` on `Field.Error` (`true` shows it whenever the field is invalid, or a `ValidityState` key such as `"valueMissing"`).
885
+
886
+ ## Rules
887
+
888
+ - Set `invalid` from the app when the server rejects a value; use `validate` for client checks.
889
+ - Write the error as how to fix the problem.
890
+ - Put a Checkbox, Switch or Radio inside its `Field.Label`, so clicking the text toggles it.
891
+ - A radio group is one field: `Field.Root` holds `Fieldset.Root render={<RadioGroup />}`, and each radio sits in a `Field.Item`. See [RadioGroup](radio-group.md).
892
+ - Stack fields `space-4` apart. Group them with a heading or a Fieldset, not boxes.
893
+ - Never use a placeholder as the only label.
894
+
895
+ ## Style
896
+
897
+ - 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`.
899
+
900
+ <!-- docs/components/input.md -->
901
+
902
+ # Input
903
+
904
+ Takes one line of text.
905
+
906
+ ## Example
907
+
908
+ ```tsx
909
+ import { Input } from "@usememos/ui";
910
+ import { Search } from "lucide-react";
911
+
912
+ <Input prefix={<Search />} placeholder="Search notes" aria-label="Search notes" />
913
+ ```
914
+
915
+ ## Props
916
+
917
+ ```ts
918
+ interface InputProps extends Omit<BaseInput.Props, "size" | "prefix"> {
919
+ size?: Size;
920
+ prefix?: React.ReactNode; // says what the field is: a search or link icon
921
+ suffix?: React.ReactNode; // one action on the value (copy, clear), or a unit
922
+ className?: string; // on the visible box, not the <input>
923
+ style?: React.CSSProperties; // on the visible box, not the <input>
924
+ }
925
+ ```
926
+
927
+ 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.
928
+
929
+ ## Rules
930
+
931
+ - Provide a `Field.Label` or `aria-label`. Never a placeholder alone.
932
+ - `prefix` and `suffix` hold an icon, a short word or a small ghost Button.
933
+ - `sm` for filters inside panels, `md` everywhere else.
934
+
935
+ ## Style
936
+
937
+ - Flat `surface` with a 1px `edge` and a faint inner shadow from the top. Never a gray well, so it never reads as disabled.
938
+ - Typed text weight 400; placeholder `ink-muted`.
939
+ - Hover darkens the edge to `ink-muted`; focus adds the ring. The background never changes.
940
+ - The side with a prefix or suffix has 2px less padding.
941
+
942
+ <!-- docs/components/textarea.md -->
943
+
944
+ # Textarea
945
+
946
+ Takes a few lines of plain text.
947
+
948
+ ## Example
949
+
950
+ ```tsx
951
+ import { Field, Textarea } from "@usememos/ui";
952
+
953
+ <Field.Root>
954
+ <Field.Label>About</Field.Label>
955
+ <Textarea placeholder="A line or two about you" />
956
+ </Field.Root>
957
+ ```
958
+
959
+ ## Props
960
+
961
+ ```ts
962
+ interface TextareaProps extends Omit<BaseField.Control.Props, "size" | "render"> {
963
+ size?: Size;
964
+ rows?: number; // visible lines, default 3
965
+ }
966
+ ```
967
+
968
+ 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.
969
+
970
+ ## Rules
971
+
972
+ - Provide a `Field.Label` or `aria-label`.
973
+ - Rich content is not a textarea: use the editor (`@usememos/editor`) with [Prose](prose.md).
974
+
975
+ ## Style
976
+
977
+ - Looks like [Input](input.md). Resizes vertically only.
978
+
979
+ <!-- docs/components/select.md -->
980
+
981
+ # Select
982
+
983
+ Picks one value from a short list.
984
+
985
+ ## Example
986
+
987
+ ```tsx
988
+ import { Field, Select } from "@usememos/ui";
989
+
990
+ const visibility = { private: "Private", workspace: "Workspace", public: "Public" };
991
+
992
+ <Field.Root>
993
+ <Field.Label>Visibility</Field.Label>
994
+ <Select.Root items={visibility} value={value} onValueChange={setValue}>
995
+ <Select.Trigger>
996
+ <Select.Value placeholder="Choose who can see it" />
997
+ </Select.Trigger>
998
+ <Select.Popup>
999
+ <Select.Item value="private">Private</Select.Item>
1000
+ <Select.Item value="workspace">Workspace</Select.Item>
1001
+ <Select.Item value="public">Public</Select.Item>
1002
+ </Select.Popup>
1003
+ </Select.Root>
1004
+ </Field.Root>
1005
+ ```
1006
+
1007
+ ## Parts
1008
+
1009
+ | Part | Base UI part | Renders |
1010
+ | --- | --- | --- |
1011
+ | `Select.Root` | `Select.Root` | Nothing. Holds the value and a hidden input for forms. Takes `size` |
1012
+ | `Select.Trigger` | `Select.Trigger` with `Select.Icon` | A neutral key with a chevron at the end, a `<button role="combobox">` |
1013
+ | `Select.Value` | `Select.Value` | The chosen item's label, or the placeholder |
1014
+ | `Select.Popup` | `Select.Portal`, `Positioner`, `Popup`, `List`, scroll arrows | The list, portaled to the body |
1015
+ | `Select.Item` | `Select.Item` with `ItemText` and `ItemIndicator` | One option, with a check at the end when chosen. Takes `icon` |
1016
+ | `Select.Group` | `Select.Group` | A labeled set of items |
1017
+ | `Select.GroupLabel` | `Select.GroupLabel` | The name of a Group |
1018
+ | `Select.Separator` | `Select.Separator` | A line between groups |
1019
+
1020
+ ## Props
1021
+
1022
+ ```ts
1023
+ type Select.RootProps<Value, Multiple> = BaseSelect.Root.Props<Value, Multiple> & {
1024
+ size?: Size;
1025
+ };
1026
+ interface Select.PopupProps extends BaseSelect.Popup.Props {
1027
+ 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
1029
+ container?: HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null; // portal target
1030
+ }
1031
+ interface Select.ItemProps extends BaseSelect.Item.Props {
1032
+ icon?: IconElement;
1033
+ }
1034
+ ```
1035
+
1036
+ Base UI's props pass through: `items`, `value`, `defaultValue`, `onValueChange`, `name`, `disabled`, `required`, `multiple` on the root; `placeholder` on Value; `value`, `disabled`, `label` on Item.
1037
+
1038
+ ## Rules
1039
+
1040
+ - Pass `items` (value to label) to the root, so `Select.Value` shows the label, not the raw value.
1041
+ - Label it with a [Field](field.md), or `aria-label` on the trigger.
1042
+ - Use [RadioGroup](radio-group.md) for two or three options with room to show them, and [Combobox](combobox.md) for more than about 10 or when people type to find.
1043
+ - For actions, use [Menu](menu.md).
1044
+
1045
+ ## Style
1046
+
1047
+ - Trigger: a neutral key ([Button](button.md) `neutral`), value left-aligned, `ChevronsUpDown` in `ink-muted`. Placeholder in `ink-muted`, weight 400. Invalid: a `danger` edge.
1048
+ - List: `mu-popup`, `space-1` padding, at least `menu-width` and the trigger's width. Rows like [Menu](menu.md) items; the highlighted row uses `hover`.
1049
+ - Fades in and rises `space-1` over `duration-fade`; no rise under reduced motion.
1050
+
1051
+ <!-- docs/components/combobox.md -->
1052
+
1053
+ # Combobox
1054
+
1055
+ Picks one value from a long list by typing to filter it.
1056
+
1057
+ ## Example
1058
+
1059
+ ```tsx
1060
+ import { Combobox, Field } from "@usememos/ui";
1061
+
1062
+ const tags = ["book", "design", "travel"];
1063
+
1064
+ <Field.Root>
1065
+ <Field.Label>Tag</Field.Label>
1066
+ <Combobox.Root items={tags} value={tag} onValueChange={setTag}>
1067
+ <Combobox.Input placeholder="Find a tag" />
1068
+ <Combobox.Popup>
1069
+ <Combobox.Empty>No tags found</Combobox.Empty>
1070
+ <Combobox.List>
1071
+ {(tag: string) => (
1072
+ <Combobox.Item key={tag} value={tag}>{tag}</Combobox.Item>
1073
+ )}
1074
+ </Combobox.List>
1075
+ </Combobox.Popup>
1076
+ </Combobox.Root>
1077
+ </Field.Root>
1078
+ ```
1079
+
1080
+ ## Parts
1081
+
1082
+ | Part | Base UI part | Renders |
1083
+ | --- | --- | --- |
1084
+ | `Combobox.Root` | `Combobox.Root` | Nothing. Holds the value, the input text and a hidden input for forms. Takes `size` |
1085
+ | `Combobox.Input` | `Combobox.InputGroup`, `Input`, `Trigger` | An [Input](input.md) box: a search icon, the `<input role="combobox">` and a chevron button that opens the list. Takes `icon` |
1086
+ | `Combobox.Popup` | `Combobox.Portal`, `Positioner`, `Popup` | The panel under the input, as wide as the input, portaled to the body |
1087
+ | `Combobox.List` | `Combobox.List` | The items, a `<div role="listbox">`. A function child renders each filtered item |
1088
+ | `Combobox.Item` | `Combobox.Item` with `ItemIndicator` | One option, with a check at the end when chosen. Takes `icon` |
1089
+ | `Combobox.Empty` | `Combobox.Empty` | The text shown when nothing matches. Stays mounted, so screen readers announce it |
1090
+ | `Combobox.Group` | `Combobox.Group` | A labeled set of items |
1091
+ | `Combobox.GroupLabel` | `Combobox.GroupLabel` | The name of a Group |
1092
+ | `Combobox.Separator` | `Combobox.Separator` | A line between groups |
1093
+ | `Combobox.Collection` | `Combobox.Collection` | Base UI's part, unstyled: renders one group's items from grouped `items` |
1094
+ | `Combobox.useFilter` | `Combobox.useFilter` | Base UI's hook: locale-aware `contains`, `startsWith`, `endsWith` for `filter` |
1095
+
1096
+ ## Props
1097
+
1098
+ ```ts
1099
+ type Combobox.RootProps<Value, Multiple, Item> = BaseCombobox.Root.Props<Value, Multiple, Item> & {
1100
+ size?: Size;
1101
+ };
1102
+ interface Combobox.InputProps extends BaseCombobox.Input.Props {
1103
+ icon?: IconElement | null; // default <Search />; null drops it
1104
+ triggerLabel?: string; // the chevron's name outside a Field; default "Show options"
1105
+ className?: string; // on the box, not the <input>
1106
+ style?: React.CSSProperties; // on the box
1107
+ }
1108
+ 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
1110
+ container?: HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null; // portal target
1111
+ }
1112
+ interface Combobox.ItemProps extends BaseCombobox.Item.Props {
1113
+ icon?: IconElement;
1114
+ }
1115
+ ```
1116
+
1117
+ Base UI's props pass through: `items`, `value`, `defaultValue`, `onValueChange`, `inputValue`, `onInputValueChange`, `filter`, `itemToStringLabel`, `isItemEqualToValue`, `open`, `onOpenChange`, `name`, `disabled`, `required` on the root; `placeholder`, `disabled` on Input; `value`, `disabled` on Item.
1118
+
1119
+ ## Rules
1120
+
1121
+ - Pass `items` to the root and render them with a function child of `List`. The root filters them as people type; `Empty` needs `items` to know when nothing matches.
1122
+ - For object items, pass `itemToStringLabel` so the input shows the label, and give each Item the object as `value`.
1123
+ - Label it with a [Field](field.md); the label names the input and the chevron. Outside a Field, give Input an `aria-label`.
1124
+ - Use it for more than about 10 options, or when people type to find one. For fewer, use [Select](select.md).
1125
+ - Typing opens the list. Up and Down move the highlight, Enter picks, Escape closes; focus stays in the input.
1126
+ - Multi-select (chips) is not built.
1127
+
1128
+ ## Style
1129
+
1130
+ - The box is an [Input](input.md): flat `surface`, `edge`, focus ring on the box. The search icon and chevron are `ink-muted`; the chevron turns `ink` on hover.
1131
+ - List: `mu-popup`, `space-1` padding, as wide as the input and at least `menu-width`, at most `list-height` (240px) tall, then it scrolls.
1132
+ - Rows are a control height, text weight 400; the highlighted row uses `hover`; the chosen row has a check at the end.
1133
+ - Empty text: `body` in `ink-muted`, `space-3` by `space-4` padding.
1134
+ - Fades in and rises `space-1` over `duration-fade`; no rise under reduced motion.
1135
+
1136
+ <!-- docs/components/checkbox.md -->
1137
+
1138
+ # Checkbox
1139
+
1140
+ Includes or excludes one item, saved with a form.
1141
+
1142
+ ## Example
1143
+
1144
+ ```tsx
1145
+ import { Checkbox, Field } from "@usememos/ui";
1146
+
1147
+ <Field.Root>
1148
+ <Field.Label>
1149
+ <Checkbox checked={digest} onCheckedChange={setDigest} /> Weekly digest
1150
+ </Field.Label>
1151
+ </Field.Root>
1152
+ ```
1153
+
1154
+ ## Props
1155
+
1156
+ ```ts
1157
+ interface CheckboxProps extends BaseCheckbox.Root.Props {
1158
+ size?: Size; // box: 16, 18, 20px
1159
+ }
1160
+ ```
1161
+
1162
+ Wraps Base UI's [Checkbox](https://base-ui.com/react/components/checkbox) root with its indicator built in: `checked`, `defaultChecked`, `onCheckedChange`, `indeterminate`, `disabled`, `name`, `value` pass through. Renders a `<span role="checkbox">` and a hidden `<input>` for forms.
1163
+
1164
+ ## Rules
1165
+
1166
+ - Name it with a `Field.Label` around it, or `aria-label`.
1167
+ - For a setting that applies at once, use [Switch](switch.md). For a labeled group, wrap the boxes in a [Fieldset](field.md).
1168
+ - Task lists in [Prose](prose.md) use this component at 16px.
1169
+
1170
+ ## Style
1171
+
1172
+ - Unchecked: an empty well with an edge. Checked: a flat `primary` square (holds a value, so not raised).
1173
+ - Lucide `Check` at a 2px stroke; `Minus` when indeterminate. It fades and scales in after the fill.
1174
+ - Shrinks 6% while pressed.
1175
+
1176
+ <!-- docs/components/switch.md -->
1177
+
1178
+ # Switch
1179
+
1180
+ Turns a setting on or off at once.
1181
+
1182
+ ## Example
1183
+
1184
+ ```tsx
1185
+ import { Field, Switch } from "@usememos/ui";
1186
+
1187
+ <Field.Root>
1188
+ <Field.Label>
1189
+ <Switch checked={publicByDefault} onCheckedChange={save} /> Make new notes public
1190
+ </Field.Label>
1191
+ </Field.Root>
1192
+ ```
1193
+
1194
+ ## Props
1195
+
1196
+ ```ts
1197
+ interface SwitchProps extends BaseSwitch.Root.Props {
1198
+ size?: Size; // 18, 20, 22px tall
1199
+ }
1200
+ ```
1201
+
1202
+ Wraps Base UI's [Switch](https://base-ui.com/react/components/switch) root with its thumb built in: `checked`, `defaultChecked`, `onCheckedChange`, `disabled`, `name` pass through.
1203
+
1204
+ ## Rules
1205
+
1206
+ - Name it with a `Field.Label` around it, or `aria-label`.
1207
+ - Use it where each change saves at once (settings pages). In a form with a Save button, use [Checkbox](checkbox.md).
1208
+
1209
+ ## Style
1210
+
1211
+ - A pill well with a round knob; the well fills with `primary` when on.
1212
+ - Height = checkbox box + 2px; width = height × 1.7.
1213
+ - 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.
1214
+
1215
+ <!-- docs/components/radio-group.md -->
1216
+
1217
+ # RadioGroup
1218
+
1219
+ Picks exactly one option from a short list that is always visible.
1220
+
1221
+ ## Example
1222
+
1223
+ ```tsx
1224
+ import { Field, Fieldset, Radio, RadioGroup } from "@usememos/ui";
1225
+
1226
+ <Field.Root name="visibility">
1227
+ <Fieldset.Root render={<RadioGroup value={visibility} onValueChange={setVisibility} />}>
1228
+ <Fieldset.Legend>Visibility</Fieldset.Legend>
1229
+ <Field.Item>
1230
+ <Field.Label><Radio value="private" /> Private</Field.Label>
1231
+ </Field.Item>
1232
+ <Field.Item>
1233
+ <Field.Label><Radio value="public" /> Public</Field.Label>
1234
+ </Field.Item>
1235
+ </Fieldset.Root>
1236
+ </Field.Root>
1237
+ ```
1238
+
1239
+ ## Parts
1240
+
1241
+ | Part | Base UI part | Renders |
1242
+ | --- | --- | --- |
1243
+ | `RadioGroup` | `RadioGroup` | A `<div role="radiogroup">`. Takes `size` and `orientation` |
1244
+ | `Radio` | `Radio.Root` with `Radio.Indicator` | One option, a `<span role="radio">` |
1245
+
1246
+ ## Props
1247
+
1248
+ ```ts
1249
+ interface RadioGroupProps extends BaseRadioGroup.Props {
1250
+ size?: Size;
1251
+ orientation?: "vertical" | "horizontal"; // default "vertical"
1252
+ }
1253
+ interface RadioProps extends BaseRadio.Root.Props {
1254
+ size?: Size; // usually set by the group
1255
+ }
1256
+ ```
1257
+
1258
+ Base UI's props pass through: `value`, `defaultValue`, `onValueChange`, `disabled`, `name` on the group; `value`, `disabled` on each radio.
1259
+
1260
+ ## Rules
1261
+
1262
+ - Use the pattern in the example: one field, a fieldset that names it, each radio labeled in a `Field.Item`.
1263
+ - Use it for two or three options, or options that need a description. Otherwise use [Select](select.md).
1264
+ - Arrow keys move the selection; only the selected radio is in the tab order.
1265
+
1266
+ ## Style
1267
+
1268
+ - Each radio is a round well; the chosen one is a flat `primary` circle with an `on-primary` dot.
1269
+ - Vertical: `space-3` apart. Horizontal: `space-4` apart, with the legend on its own line above.
1270
+
1271
+ <!-- docs/components/tabs.md -->
1272
+
1273
+ # Tabs
1274
+
1275
+ Switches between sections of the same page.
1276
+
1277
+ ## Example
1278
+
1279
+ ```tsx
1280
+ import { Tabs } from "@usememos/ui";
1281
+ import { Bell, User } from "lucide-react";
1282
+
1283
+ <Tabs.Root defaultValue="account">
1284
+ <Tabs.List aria-label="Settings sections">
1285
+ <Tabs.Tab value="account" icon={<User />}>Account</Tabs.Tab>
1286
+ <Tabs.Tab value="notifications" icon={<Bell />}>Notifications</Tabs.Tab>
1287
+ </Tabs.List>
1288
+ <Tabs.Panel value="account">…</Tabs.Panel>
1289
+ <Tabs.Panel value="notifications">…</Tabs.Panel>
1290
+ </Tabs.Root>
1291
+ ```
1292
+
1293
+ ## Parts
1294
+
1295
+ | Part | Base UI part | Renders |
1296
+ | --- | --- | --- |
1297
+ | `Tabs.Root` | `Tabs.Root` | A `<div>` around the list and panels. Takes `size` |
1298
+ | `Tabs.List` | `Tabs.List` with `Tabs.Indicator` | The well, a `<div role="tablist">`, with the sliding key built in |
1299
+ | `Tabs.Tab` | `Tabs.Tab` | One tab, a `<button role="tab">`. Takes `icon` |
1300
+ | `Tabs.Panel` | `Tabs.Panel` | One tab's content, a `<div role="tabpanel">` |
1301
+
1302
+ ## Props
1303
+
1304
+ ```ts
1305
+ interface Tabs.RootProps extends BaseTabs.Root.Props {
1306
+ size?: Size;
1307
+ }
1308
+ interface Tabs.TabProps extends BaseTabs.Tab.Props {
1309
+ icon?: IconElement; // before the label
1310
+ }
1311
+ ```
1312
+
1313
+ Base UI's props pass through: `value`, `defaultValue`, `onValueChange` on the root; `activateOnFocus`, `loopFocus` on the list; `value`, `disabled` on each tab; `keepMounted` on each panel.
1314
+
1315
+ ## Rules
1316
+
1317
+ - Give the list an `aria-label`.
1318
+ - Use tabs for sections of a page, not for site navigation. For a filter or view switch, use a [ToggleGroup](toggle-group.md).
1319
+ - Left and Right arrow keys move between tabs.
1320
+
1321
+ ## Style
1322
+
1323
+ - Shares the segmented well and sliding key with a single-choice ToggleGroup.
1324
+ - Tabs are sized to their labels. The key re-measures when a label changes width.
1325
+ - Selected and hovered labels `ink`, others `ink-muted`. Weight never changes, so labels don't shift.
1326
+
1327
+ <!-- docs/components/collapsible.md -->
1328
+
1329
+ # Collapsible
1330
+
1331
+ Hides content behind one trigger until people ask for it.
1332
+
1333
+ ## Example
1334
+
1335
+ ```tsx
1336
+ import { Collapsible } from "@usememos/ui";
1337
+
1338
+ <Collapsible.Root>
1339
+ <Collapsible.Trigger>Advanced settings</Collapsible.Trigger>
1340
+ <Collapsible.Panel>…</Collapsible.Panel>
1341
+ </Collapsible.Root>
1342
+ ```
1343
+
1344
+ ## Parts
1345
+
1346
+ | Part | Base UI part | Renders |
1347
+ | --- | --- | --- |
1348
+ | `Collapsible.Root` | `Collapsible.Root` | A `<div>` around the trigger and panel. Takes `size` |
1349
+ | `Collapsible.Trigger` | `Collapsible.Trigger` | A ghost [Button](button.md) with a chevron at the end that turns while open |
1350
+ | `Collapsible.Panel` | `Collapsible.Panel` | The content, a `<div>`; unmounted while closed unless `keepMounted` |
1351
+
1352
+ ## Props
1353
+
1354
+ ```ts
1355
+ interface Collapsible.RootProps extends BaseCollapsible.Root.Props {
1356
+ size?: Size; // the trigger's size
1357
+ }
1358
+ ```
1359
+
1360
+ Base UI's props pass through: `open`, `defaultOpen`, `onOpenChange`, `disabled` on the root; `keepMounted`, `hiddenUntilFound` on the panel.
1361
+
1362
+ ## Rules
1363
+
1364
+ - Use it for "Show more" on long content, advanced settings and secondary details.
1365
+ - The trigger label names what is hidden. To change it while open ("Show less"), control `open` and swap the label.
1366
+ - For several sections that open one at a time, stack Collapsibles and control them. There is no Accordion.
1367
+ - Enter and Space toggle it; the trigger has `aria-expanded`.
1368
+
1369
+ ## Style
1370
+
1371
+ - The trigger is a ghost key that bleeds `px` to the left, so its label lines up with the content below.
1372
+ - The chevron is `ink-muted` and turns 180° over `duration-settle` with `ease-settle`.
1373
+ - The panel fades in and rises `space-1` over `duration-fade`. Its height never animates. No rise or turn under reduced motion.
1374
+
1375
+ <!-- docs/components/dialog.md -->
1376
+
1377
+ # Dialog and AlertDialog
1378
+
1379
+ Dialog asks for a short form or a decision on top of the page. AlertDialog asks to confirm an action and closes only through its buttons.
1380
+
1381
+ ## Example
1382
+
1383
+ ```tsx
1384
+ import { AlertDialog, Button, Dialog, Field, Input } from "@usememos/ui";
1385
+
1386
+ <Dialog.Root>
1387
+ <Dialog.Trigger render={<Button>Create access token</Button>} />
1388
+ <Dialog.Popup>
1389
+ <Dialog.Title>Create access token</Dialog.Title>
1390
+ <Dialog.Description>Use it to call the API from scripts.</Dialog.Description>
1391
+ <Field.Root>
1392
+ <Field.Label>Description</Field.Label>
1393
+ <Input />
1394
+ </Field.Root>
1395
+ <Dialog.Footer>
1396
+ <Dialog.Close render={<Button variant="ghost">Cancel</Button>} />
1397
+ <Button variant="primary" onClick={create}>Create</Button>
1398
+ </Dialog.Footer>
1399
+ </Dialog.Popup>
1400
+ </Dialog.Root>
1401
+
1402
+ <AlertDialog.Root open={open} onOpenChange={setOpen}>
1403
+ <AlertDialog.Popup>
1404
+ <AlertDialog.Title>Delete this note?</AlertDialog.Title>
1405
+ <AlertDialog.Description>It moves to the trash for 30 days.</AlertDialog.Description>
1406
+ <AlertDialog.Footer>
1407
+ <AlertDialog.Close render={<Button variant="ghost">Cancel</Button>} />
1408
+ <Button variant="danger" onClick={remove}>Delete</Button>
1409
+ </AlertDialog.Footer>
1410
+ </AlertDialog.Popup>
1411
+ </AlertDialog.Root>
1412
+ ```
1413
+
1414
+ ## Parts
1415
+
1416
+ Dialog and AlertDialog have the same parts.
1417
+
1418
+ | Part | Base UI part | Renders |
1419
+ | --- | --- | --- |
1420
+ | `Root` | `Root` | Nothing. Holds the open state |
1421
+ | `Trigger` | `Trigger` | The control that opens it, usually a Button through `render` |
1422
+ | `Popup` | `Portal`, `Backdrop`, `Viewport`, `Popup` | The panel over a dimmed backdrop, portaled to the body. Dialog adds an X at the top right |
1423
+ | `Title` | `Title` | An `<h2>` that names the dialog |
1424
+ | `Description` | `Description` | A `<p>` under the title |
1425
+ | `Close` | `Close` | A button that closes it, usually a Button through `render` |
1426
+ | `Footer` | none (ours) | A `<div>` that right-aligns the buttons |
1427
+
1428
+ ## Props
1429
+
1430
+ ```ts
1431
+ interface Dialog.PopupProps extends BaseDialog.Popup.Props {
1432
+ sheet?: boolean; // below breakpoint-sm, a sheet from the bottom; default true
1433
+ closeLabel?: string; // accessible name of the X; default "Close"
1434
+ container?: HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null; // portal target
1435
+ }
1436
+ // AlertDialog.Popup takes sheet and container; it has no X.
1437
+ ```
1438
+
1439
+ Base UI's props pass through: `open`, `defaultOpen`, `onOpenChange`, `modal`, `disablePointerDismissal` on the root; `initialFocus`, `finalFocus` on the popup.
1440
+
1441
+ ## Rules
1442
+
1443
+ - Title: a question or a task ("Delete this note?", "Create access token"). Description: what will happen.
1444
+ - Footer: Cancel as a `ghost` Button, then the action. Name the action with its verb, never "OK". Use `danger` only for destructive confirmations.
1445
+ - Use AlertDialog for confirmations; see the [confirmation pattern](../patterns.md#confirmation). Open it from a Menu item with `open` and `onOpenChange`.
1446
+ - Escape and a backdrop click close a Dialog. While a form has unsaved input, keep it open from `onOpenChange`.
1447
+ - Focus moves into the dialog and returns to the trigger on close.
1448
+
1449
+ ## Style
1450
+
1451
+ - `mu-popup` with `radius-2xl`, `space-5` padding, `dialog-width` wide; children stack `space-4` apart. The backdrop is `overlay`.
1452
+ - 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.
1453
+ - 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.
1454
+ - Long content scrolls the viewport around the panel.
1455
+
1456
+ <!-- docs/components/popover.md -->
1457
+
1458
+ # Popover
1459
+
1460
+ A small floating panel of controls next to its trigger, such as a share panel or a link editor.
1461
+
1462
+ ## Example
1463
+
1464
+ ```tsx
1465
+ import { Button, Input, Popover } from "@usememos/ui";
1466
+
1467
+ <Popover.Root>
1468
+ <Popover.Trigger render={<Button>Share</Button>} />
1469
+ <Popover.Popup align="start">
1470
+ <Popover.Title>Share this note</Popover.Title>
1471
+ <Popover.Description>Anyone with the link can read it.</Popover.Description>
1472
+ <Input readOnly value={link} aria-label="Link" />
1473
+ </Popover.Popup>
1474
+ </Popover.Root>
1475
+ ```
1476
+
1477
+ ## Parts
1478
+
1479
+ | Part | Base UI part | Renders |
1480
+ | --- | --- | --- |
1481
+ | `Popover.Root` | `Popover.Root` | Nothing. Groups a trigger and its popup |
1482
+ | `Popover.Trigger` | `Popover.Trigger` | The control that opens it, usually a Button through `render` |
1483
+ | `Popover.Popup` | `Popover.Portal`, `Positioner`, `Popup` | The panel, a `<div>` portaled to the body |
1484
+ | `Popover.Title` | `Popover.Title` | An `<h2>` that names the popup |
1485
+ | `Popover.Description` | `Popover.Description` | A `<p>` under the title |
1486
+ | `Popover.Close` | `Popover.Close` | A button that closes it, usually a Button through `render` |
1487
+
1488
+ ## Props
1489
+
1490
+ ```ts
1491
+ 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
1493
+ container?: HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null; // portal target
1494
+ }
1495
+ ```
1496
+
1497
+ Base UI's props pass through: `open`, `defaultOpen`, `onOpenChange`, `modal` on the root; `openOnHover`, `delay` on the trigger; `initialFocus`, `finalFocus` on the popup.
1498
+
1499
+ ## Rules
1500
+
1501
+ - 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
+ - Give it a `Popover.Title`, or `aria-label` on the popup.
1503
+ - It closes on Escape and on a click outside; focus returns to the trigger.
1504
+
1505
+ ## Style
1506
+
1507
+ - `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.
1509
+ - Fades in and rises `space-1` over `duration-fade`; no rise under reduced motion.
1510
+
1511
+ <!-- docs/components/tooltip.md -->
1512
+
1513
+ # Tooltip
1514
+
1515
+ Names an icon-only control and shows its shortcut.
1516
+
1517
+ ## Example
1518
+
1519
+ ```tsx
1520
+ import { Button, Tooltip } from "@usememos/ui";
1521
+ import { Archive } from "lucide-react";
1522
+
1523
+ <Tooltip.Provider>
1524
+ <Tooltip.Root>
1525
+ <Tooltip.Trigger render={<Button variant="ghost" icon={<Archive />} aria-label="Archive" />} />
1526
+ <Tooltip.Popup shortcut="⌘ E">Archive</Tooltip.Popup>
1527
+ </Tooltip.Root>
1528
+ </Tooltip.Provider>
1529
+ ```
1530
+
1531
+ ## Parts
1532
+
1533
+ | Part | Base UI part | Renders |
1534
+ | --- | --- | --- |
1535
+ | `Tooltip.Provider` | `Tooltip.Provider` | Nothing. Shares the hover delay, so the next tooltip in a toolbar opens at once |
1536
+ | `Tooltip.Root` | `Tooltip.Root` | Nothing. Groups a trigger and its popup |
1537
+ | `Tooltip.Trigger` | `Tooltip.Trigger` | The control, usually a Button through `render` |
1538
+ | `Tooltip.Popup` | `Tooltip.Portal`, `Positioner`, `Popup` | The label, a `<div>` portaled to the body |
1539
+
1540
+ ## Props
1541
+
1542
+ ```ts
1543
+ interface Tooltip.TriggerProps extends BaseTooltip.Trigger.Props {
1544
+ delay?: number; // hover time before opening, ms; default 400
1545
+ }
1546
+ interface Tooltip.PopupProps extends BaseTooltip.Popup.Props {
1547
+ 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
1549
+ container?: HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null; // portal target
1550
+ }
1551
+ ```
1552
+
1553
+ Base UI's props pass through: `open`, `defaultOpen`, `onOpenChange`, `disabled` on the root; `closeOnClick` on the trigger; `delay`, `closeDelay`, `timeout` on the provider.
1554
+
1555
+ ## Rules
1556
+
1557
+ - Give every icon-only Button and Toggle a tooltip with the same text as its `aria-label`. Two or three words.
1558
+ - Never put essential information only in a tooltip: it doesn't open on touch screens.
1559
+ - For text that explains something, use a [Popover](popover.md) instead.
1560
+ - Wrap a toolbar, or the whole app, in one `Tooltip.Provider`.
1561
+
1562
+ ## Style
1563
+
1564
+ - `primary` background, `on-primary` text, `label-sm`, `radius-md`, `space-1` by `space-2` padding, at most `tooltip-width` wide.
1565
+ - Opens after 400ms of hover, or at once on keyboard focus. Fades in and rises `space-1`; no rise under reduced motion.
1566
+
1567
+ <!-- docs/components/toast.md -->
1568
+
1569
+ # Toast
1570
+
1571
+ Confirms that something happened, or that it failed, without blocking the page.
1572
+
1573
+ ## Example
1574
+
1575
+ ```tsx
1576
+ import { Toast } from "@usememos/ui";
1577
+
1578
+ // Once, around the app:
1579
+ <Toast.Provider>
1580
+ <App />
1581
+ <Toast.Viewport />
1582
+ </Toast.Provider>
1583
+
1584
+ // Anywhere inside:
1585
+ const toast = Toast.useToastManager();
1586
+ toast.add({ title: "Note archived", actionProps: { children: "Undo", onClick: unarchive } });
1587
+ toast.add({ title: "Settings saved", type: "success" });
1588
+ toast.add({ title: "Couldn't sync", description: "Check your connection.", type: "danger", timeout: 0 });
1589
+ ```
1590
+
1591
+ ## Parts
1592
+
1593
+ | Part | Base UI part | Renders |
1594
+ | --- | --- | --- |
1595
+ | `Toast.Provider` | `Toast.Provider` | Nothing. Holds the toasts |
1596
+ | `Toast.Viewport` | `Toast.Portal`, `Viewport`, and each toast's `Root`, `Content`, `Title`, `Description`, `Action`, `Close` | The stack of toasts, portaled to the body |
1597
+ | `Toast.useToastManager` | `Toast.useToastManager` | A hook: `add`, `update`, `close`, `promise`, `toasts` |
1598
+ | `Toast.createToastManager` | `Toast.createToastManager` | A manager for code outside React; pass it to the Provider as `toastManager` |
1599
+
1600
+ ## Props
1601
+
1602
+ ```ts
1603
+ interface Toast.ViewportProps extends BaseToast.Viewport.Props {
1604
+ closeLabel?: string; // accessible name of each X; default "Close"
1605
+ container?: HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null; // portal target
1606
+ }
1607
+ // toast.add() options (Base UI): title, description, type, timeout, priority, actionProps, onClose, data.
1608
+ // type sets the tone: "success", "warning", "danger", or none for neutral.
1609
+ ```
1610
+
1611
+ Base UI's props pass through: `timeout` (default 5000ms) and `limit` (default 3) on the Provider.
1612
+
1613
+ ## Rules
1614
+
1615
+ - Title: what happened, past tense ("Note archived"). Add a description only when it helps.
1616
+ - At most one action, such as "Undo", through `actionProps`.
1617
+ - Errors stay until dismissed: pass `timeout: 0` with `type: "danger"`, and `priority: "high"` when people must hear it at once.
1618
+ - Color only marks the icon; the words carry the meaning.
1619
+ - Render one Viewport per app. F6 moves focus into the toasts.
1620
+
1621
+ ## Style
1622
+
1623
+ - `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.
1625
+ - 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
+
1627
+ <!-- docs/components/avatar.md -->
1628
+
1629
+ # Avatar
1630
+
1631
+ Shows a person.
1632
+
1633
+ ## Example
1634
+
1635
+ ```tsx
1636
+ import { Avatar } from "@usememos/ui";
1637
+
1638
+ <Avatar.Root size="sm">
1639
+ <Avatar.Image src={user.avatarUrl} alt={user.name} />
1640
+ <Avatar.Fallback>{initials(user.name)}</Avatar.Fallback>
1641
+ </Avatar.Root>
1642
+ ```
1643
+
1644
+ ## Parts
1645
+
1646
+ | Part | Base UI part | Renders |
1647
+ | --- | --- | --- |
1648
+ | `Avatar.Root` | `Avatar.Root` | The circle, a `<span>`. Takes `size` |
1649
+ | `Avatar.Image` | `Avatar.Image` | The photo, an `<img>`, shown once loaded |
1650
+ | `Avatar.Fallback` | `Avatar.Fallback` | The initials, shown while there is no photo or it fails to load |
1651
+
1652
+ ## Props
1653
+
1654
+ ```ts
1655
+ interface Avatar.RootProps extends BaseAvatar.Root.Props {
1656
+ size?: Size; // 24, 32, 40px
1657
+ }
1658
+ ```
1659
+
1660
+ `Avatar.Image` and `Avatar.Fallback` take Base UI's props (`src`, `alt`, `delay`, `render`).
1661
+
1662
+ ## Rules
1663
+
1664
+ - 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.
1666
+
1667
+ ## Style
1668
+
1669
+ - A circle (one of three round shapes, with radios and switches). Initials in `ink-muted` on `well`.
1670
+
1671
+ <!-- docs/components/badge.md -->
1672
+
1673
+ # Badge
1674
+
1675
+ Labels a thing with a tag or a status.
1676
+
1677
+ ## Example
1678
+
1679
+ ```tsx
1680
+ import { Badge } from "@usememos/ui";
1681
+ import { Hash, Lock } from "lucide-react";
1682
+
1683
+ <Badge icon={<Hash />}>reading</Badge>
1684
+ <Badge tone="outline" icon={<Lock />}>Private</Badge>
1685
+ <Badge tone="success">Synced</Badge>
1686
+ ```
1687
+
1688
+ ## Props
1689
+
1690
+ ```ts
1691
+ interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
1692
+ tone?: Tone | "outline"; // default "neutral"
1693
+ icon?: IconElement;
1694
+ children: React.ReactNode; // one or two words
1695
+ render?: React.ReactElement;
1696
+ }
1697
+ ```
1698
+
1699
+ ## Rules
1700
+
1701
+ - Tags: `neutral` with a `Hash` icon. Visibility: `outline` with its icon. Status: `success`, `warning` or `danger`, always with a word, never color alone.
1702
+ - A badge is not a button. A clickable tag filter is a [Toggle](toggle.md).
1703
+
1704
+ ## Style
1705
+
1706
+ - 20px tall, `radius-sm` (never a pill), 12px medium text, optional 12px icon at a 1.25px stroke.
1707
+ - `neutral`: `ink` on `well`. Status tones: their `-soft` background with full-strength text.
1708
+
1709
+ <!-- docs/components/kbd.md -->
1710
+
1711
+ # Kbd
1712
+
1713
+ Shows one keyboard key in help text, menus and empty states.
1714
+
1715
+ ## Example
1716
+
1717
+ ```tsx
1718
+ import { Kbd } from "@usememos/ui";
1719
+
1720
+ <p>Press <Kbd>⌘</Kbd> <Kbd>K</Kbd> to search</p>
1721
+ ```
1722
+
1723
+ ## Props
1724
+
1725
+ ```ts
1726
+ interface KbdProps extends React.HTMLAttributes<HTMLElement> {
1727
+ children: React.ReactNode; // one key: "⌘", "Enter", "/"
1728
+ render?: React.ReactElement;
1729
+ }
1730
+ ```
1731
+
1732
+ Renders a `<kbd>`.
1733
+
1734
+ ## Rules
1735
+
1736
+ - One key per Kbd. Put a space between the keys of a shortcut.
1737
+ - Use symbols for modifiers on macOS (⌘ ⌥ ⇧) and words elsewhere (Ctrl, Alt, Shift).
1738
+
1739
+ ## Style
1740
+
1741
+ - A tiny raised key, 20px tall, `label-xs` text in `ink-muted`.
1742
+
1743
+ <!-- docs/components/separator.md -->
1744
+
1745
+ # Separator
1746
+
1747
+ A line between groups of content, horizontal or vertical.
1748
+
1749
+ ## Example
1750
+
1751
+ ```tsx
1752
+ import { Separator } from "@usememos/ui";
1753
+
1754
+ <Separator />
1755
+ <Separator orientation="vertical" />
1756
+ ```
1757
+
1758
+ ## Props
1759
+
1760
+ ```ts
1761
+ interface SeparatorProps extends BaseSeparator.Props {
1762
+ orientation?: "horizontal" | "vertical"; // default "horizontal"
1763
+ }
1764
+ ```
1765
+
1766
+ Wraps Base UI's [Separator](https://base-ui.com/react/components/separator): a `<div role="separator">` with `aria-orientation`.
1767
+
1768
+ ## Rules
1769
+
1770
+ - Prefer space over lines. Use a separator only where `space-6` alone doesn't make the groups clear.
1771
+ - In a [ToggleGroup](toggle-group.md), a vertical Separator splits clusters. Menus use `Menu.Separator`.
1772
+
1773
+ ## Style
1774
+
1775
+ - 1px `line`. A vertical separator stretches to the height of its row.
1776
+
1777
+ <!-- docs/components/spinner.md -->
1778
+
1779
+ # Spinner
1780
+
1781
+ Shows that something short is in progress.
1782
+
1783
+ ## Example
1784
+
1785
+ ```tsx
1786
+ import { Spinner } from "@usememos/ui";
1787
+
1788
+ <Spinner label="Loading notes" />
1789
+ ```
1790
+
1791
+ ## Props
1792
+
1793
+ ```ts
1794
+ interface SpinnerProps extends React.HTMLAttributes<HTMLSpanElement> {
1795
+ size?: Size; // 14, 16, 20px; default: the surrounding Root's size, else md
1796
+ label?: string; // for screen readers, default "Loading"
1797
+ render?: React.ReactElement;
1798
+ }
1799
+ ```
1800
+
1801
+ Renders a `<span role="status">` named by `label`.
1802
+
1803
+ ## Rules
1804
+
1805
+ - For a wait inside a control, use the control's `loading` prop.
1806
+ - For content that is loading, use [Skeleton](skeleton.md), so the layout doesn't jump.
1807
+ - Don't show a spinner for waits under about 300ms.
1808
+
1809
+ ## Style
1810
+
1811
+ - A ring in `currentColor` with one gap, at the icon stroke width of its size, turning once every `duration-spin`.
1812
+
1813
+ <!-- docs/components/skeleton.md -->
1814
+
1815
+ # Skeleton
1816
+
1817
+ Holds the place of content that is still loading.
1818
+
1819
+ ## Example
1820
+
1821
+ ```tsx
1822
+ import { Skeleton } from "@usememos/ui";
1823
+
1824
+ <Skeleton round />
1825
+ <Skeleton width="40%" />
1826
+ <Skeleton />
1827
+ ```
1828
+
1829
+ ## Props
1830
+
1831
+ ```ts
1832
+ interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {
1833
+ width?: number | string; // default: full width (round: its height)
1834
+ height?: number | string; // default: one line of text at `size` (round: the control height)
1835
+ round?: boolean; // for avatars
1836
+ size?: Size;
1837
+ render?: React.ReactElement;
1838
+ }
1839
+ ```
1840
+
1841
+ ## Rules
1842
+
1843
+ - Match the shape of what is coming: one bar per line of text, a circle for an avatar, a block for an image. The layout must not jump when content arrives.
1844
+ - Use skeletons for lists and pages, and [Spinner](spinner.md) for a control that is waiting.
1845
+ - Skeletons are hidden from screen readers. Set `aria-busy` on the loading region.
1846
+
1847
+ ## Style
1848
+
1849
+ - A `well` block that shimmers once every `duration-shimmer`. The shimmer stops under reduced motion.
1850
+
1851
+ <!-- docs/components/table.md -->
1852
+
1853
+ # Table
1854
+
1855
+ Lists things with the same attributes, such as tokens, users or webhooks.
1856
+
1857
+ ## Example
1858
+
1859
+ ```tsx
1860
+ import { Button, Menu, Table } from "@usememos/ui";
1861
+ import { Ellipsis } from "lucide-react";
1862
+
1863
+ <Table.Root>
1864
+ <Table.Caption>Access tokens</Table.Caption>
1865
+ <Table.Header>
1866
+ <Table.Row>
1867
+ <Table.Head>Name</Table.Head>
1868
+ <Table.Head>Created</Table.Head>
1869
+ <Table.Head num>Uses</Table.Head>
1870
+ <Table.Head><span className="mu-visually-hidden">Actions</span></Table.Head>
1871
+ </Table.Row>
1872
+ </Table.Header>
1873
+ <Table.Body>
1874
+ {tokens.map((token) => (
1875
+ <Table.Row key={token.id}>
1876
+ <Table.Cell>{token.name}</Table.Cell>
1877
+ <Table.Cell muted>{token.created}</Table.Cell>
1878
+ <Table.Cell num>{token.uses}</Table.Cell>
1879
+ <Table.Cell num>
1880
+ <Menu.Root>
1881
+ <Menu.Trigger render={<Button variant="ghost" icon={<Ellipsis />} aria-label={`Actions for ${token.name}`} />} />
1882
+ <Menu.Popup align="end">…</Menu.Popup>
1883
+ </Menu.Root>
1884
+ </Table.Cell>
1885
+ </Table.Row>
1886
+ ))}
1887
+ </Table.Body>
1888
+ </Table.Root>
1889
+ ```
1890
+
1891
+ ## Parts
1892
+
1893
+ No Base UI part: these are styled native table elements.
1894
+
1895
+ | Part | Renders |
1896
+ | --- | --- |
1897
+ | `Table.Root` | A `<table>` inside a `<div>` that scrolls sideways when the table is too wide. Takes `size`. The ref and props go to the `<table>` |
1898
+ | `Table.Caption` | `<caption>`, under the table. Names it for screen readers |
1899
+ | `Table.Header` | `<thead>` |
1900
+ | `Table.Body` | `<tbody>` |
1901
+ | `Table.Row` | `<tr>` |
1902
+ | `Table.Head` | `<th scope="col">`. Takes `num` |
1903
+ | `Table.Cell` | `<td>`. Takes `num` and `muted` |
1904
+
1905
+ ## Props
1906
+
1907
+ ```ts
1908
+ interface Table.RootProps extends React.ComponentProps<"table"> {
1909
+ size?: Size; // sm for dense admin lists
1910
+ }
1911
+ interface Table.HeadProps extends React.ComponentProps<"th"> {
1912
+ num?: boolean; // right-aligned, for a column of numbers
1913
+ }
1914
+ interface Table.CellProps extends React.ComponentProps<"td"> {
1915
+ num?: boolean; // right-aligned
1916
+ muted?: boolean; // ink-muted, for secondary values such as dates
1917
+ }
1918
+ ```
1919
+
1920
+ Every native attribute passes through.
1921
+
1922
+ ## Rules
1923
+
1924
+ - Use it for data with columns. Lists of content (notes, comments) are [entries](../patterns.md#entry), not rows.
1925
+ - Set `num` on a numeric column's Head and on its Cells.
1926
+ - Put row actions in the last column as a ghost [Menu](menu.md) trigger, named for its row. Give that column's Head text in `mu-visually-hidden`.
1927
+ - Name the table with a Caption, or `aria-label` on the root.
1928
+ - Sort, select and paginate in the app. The table has no data props.
1929
+
1930
+ ## Style
1931
+
1932
+ - 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.
1935
+ - Cells pad `px` at the sides. Caption: `caption` in `ink-muted`, `space-2` above.
1936
+
1937
+ <!-- docs/components/prose.md -->
1938
+
1939
+ # Prose
1940
+
1941
+ Styles rendered Markdown: the body of a note, a comment, a changelog, a page on the website.
1942
+
1943
+ ## Example
1944
+
1945
+ ```tsx
1946
+ import { Prose } from "@usememos/ui";
1947
+
1948
+ <Prose dangerouslySetInnerHTML={{ __html: renderMarkdown(memo.content) }} />
1949
+
1950
+ // or, with a React renderer
1951
+ <Prose>
1952
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{memo.content}</ReactMarkdown>
1953
+ </Prose>
1954
+ ```
1955
+
1956
+ Without React: `<div class="mu-prose">…</div>`, and `mu-prose mu-prose-sm` for the small size.
1957
+
1958
+ ## Props
1959
+
1960
+ ```ts
1961
+ interface ProseProps extends React.ComponentProps<"div"> {
1962
+ size?: "sm" | "md"; // md: 15/24px for reading; sm: 14/22px for dense lists
1963
+ }
1964
+ ```
1965
+
1966
+ Every native attribute passes through.
1967
+
1968
+ ## Rules
1969
+
1970
+ - Put plain HTML from a Markdown renderer inside it. It styles elements (`h1`–`h6`, `p`, `ul`, `ol`, `li`, `a`, `code`, `pre`, `blockquote`, `table`, `img`, `video`, `hr`), so the renderer needs no classes.
1971
+ - Task lists are `<li>` elements that start with `<input type="checkbox">`, as GFM renders them. Give each checkbox a name, such as `aria-label` with the item text. Drop `disabled` when people can tick them.
1972
+ - Mark tags with `<span class="mu-prose-tag">#tag</span>`.
1973
+ - Do not put components that are not content (forms, menus) inside it.
1974
+ - The editor (`@usememos/editor`) uses the same stylesheet in edit mode, so content does not shift between editing and reading.
1975
+
1976
+ ## Style
1977
+
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.
1980
+ - Links: `accent`, underlined at rest, because color alone does not set them apart from the text.
1981
+ - 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
+ - Quotes: a 2px `edge` bar, text in `ink-muted`. Rules: a 1px `line` with `space-5` around it.
1983
+ - 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
+ - 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.
1986
+ - Tags: `ink-muted` on `well`, `radius-xs`. They read as words, not badges.
1987
+ - Sizes are custom properties set on `.mu-prose` in the foundation (`--prose-fs`, `--prose-lh`, heading sizes).
1988
+
1989
+ <!-- docs/patterns.md -->
1990
+
1991
+ # Patterns
1992
+
1993
+ How the components go together. Each product builds its screens from these patterns. None of them is specific to one product.
1994
+
1995
+ ## Page
1996
+
1997
+ - 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
+ - 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.
2000
+
2001
+ ## Form
2002
+
2003
+ - Fields stack `space-4` apart with the label above the control.
2004
+ - Related checkboxes or switches sit in a `Fieldset`.
2005
+ - Actions go at the end, right-aligned: a ghost Cancel, then the primary.
2006
+ - A form that saves at once (settings) uses switches and has no Save button. A form with a Save button uses checkboxes.
2007
+ - Errors appear under the field, in words that say how to fix it.
2008
+
2009
+ ## Entry
2010
+
2011
+ A repeated unit of content in a list: a note, a comment, a changelog item.
2012
+
2013
+ - **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.
2014
+ - **Body** in `Prose`.
2015
+ - **Footer line:** tags as Prose tags, attachments, reactions.
2016
+ - Entries are separated by `space-6` of whitespace or a `Separator`, never by boxes.
2017
+
2018
+ ## Confirmation
2019
+
2020
+ - A destructive action opens an alert Dialog that names the object and the outcome.
2021
+ - It has a ghost Cancel and a danger Button that repeats the verb ("Delete note").
2022
+ - On phones it is a sheet with stacked, full-width buttons.
2023
+ - After the action runs, a neutral Toast confirms it, with Undo where undo is possible.
2024
+
2025
+ ## Loading
2026
+
2027
+ - A list or page that is loading shows Skeletons in the shape of what is coming. The layout must not jump when content arrives.
2028
+ - A control that is waiting uses its `loading` prop.
2029
+ - Nothing spins for less than about 300ms.
2030
+ - Mark the loading region with `aria-busy`.
2031
+
2032
+ ## Empty state
2033
+
2034
+ Centered in the content column:
2035
+
2036
+ - a `title` line that says what is missing ("No notes yet"),
2037
+ - a `body` line that says what to do,
2038
+ - one primary Button.
2039
+
2040
+ No illustration.
2041
+
2042
+ ## Mobile
2043
+
2044
+ Under `breakpoint-sm` (640px):
2045
+
2046
+ - One column.
2047
+ - Dialogs and menus become sheets.
2048
+ - Toasts sit bottom-center.
2049
+ - Every control keeps its 44px hit area.
2050
+ - Controls stay `md`. `lg` is for marketing pages and primary actions in sheets.
2051
+
2052
+ <!-- docs/building.md -->
2053
+
2054
+ # Building on it
2055
+
2056
+ How the package is put together and how products use it.
2057
+
2058
+ ## Stack
2059
+
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.
2061
+ - **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.
2063
+ - **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
+ - **Lucide icons** from `lucide-react`. See [Icons](icons.md).
2065
+ - **No fonts.** Text uses the system UI font; nothing is downloaded.
2066
+
2067
+ ## Loading the CSS
2068
+
2069
+ Load the tokens first, then the styles:
2070
+
2071
+ ```ts
2072
+ import "@usememos/ui/tokens.css";
2073
+ import "@usememos/ui/styles.css";
2074
+ ```
2075
+
2076
+ `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
+
2078
+ 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.
2079
+
2080
+ ### Cascade layer
2081
+
2082
+ Both stylesheets sit in one [cascade layer](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer) named `mu`. Unlayered CSS always wins over layered CSS, so an app's own rules override a component without a specificity fight: a `className` on a `Button` that sets `width` wins over the button's own rule.
2083
+
2084
+ When the app also puts its CSS in layers, declare the order once, before any import, so that `mu` comes after resets and before the app's components and utilities. For an app on Tailwind CSS v4:
2085
+
2086
+ ```css
2087
+ @layer theme, base, mu, components, utilities;
2088
+ @import "tailwindcss";
2089
+ @import "@usememos/ui/tokens.css";
2090
+ @import "@usememos/ui/styles.css";
2091
+ ```
2092
+
2093
+ Without that line, the layer seen first is the weakest. A reset loaded after `styles.css` would then override the components.
2094
+
2095
+ ## Scoping inside a host page
2096
+
2097
+ Inside another page (the web clipper, an embed), mount the UI under a `.mu-root` element or inside a shadow root, so neither side's styles leak into the other. The package has no global selectors beyond `:root`, `:host` and `.mu-root`.
2098
+
2099
+ ## Choosing a theme
2100
+
2101
+ 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
+
2103
+ ## Class names
2104
+
2105
+ - Every class the package ships starts with `mu-`.
2106
+ - `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
+ - 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
+ - `mu-prose` styles rendered content. See [Prose](components/prose.md).
2109
+ - `mu-icon` applies icon size and stroke rules outside a component.
2110
+ - `mu-visually-hidden` hides text visually but keeps it for screen readers.
2111
+
2112
+ ## API conventions
2113
+
2114
+ Components follow the conventions of established React libraries (Base UI, Radix, React Aria), not the needs of one product. Each [component page](components/README.md) lists its parts and their props.
2115
+
2116
+ ### Parts
2117
+
2118
+ A component made of more than one element is a namespace of styled parts. Part names are Base UI's, so anyone who knows Base UI knows the anatomy:
2119
+
2120
+ ```tsx
2121
+ import { Menu, Button } from "@usememos/ui";
2122
+ import { Ellipsis, Pin, Trash2 } from "lucide-react";
2123
+
2124
+ <Menu.Root>
2125
+ <Menu.Trigger render={<Button variant="ghost" icon={<Ellipsis />} aria-label="More actions" />} />
2126
+ <Menu.Popup align="end">
2127
+ <Menu.Item icon={<Pin />}>Pin</Menu.Item>
2128
+ <Menu.Separator />
2129
+ <Menu.Item icon={<Trash2 />} danger>Delete</Menu.Item>
2130
+ </Menu.Popup>
2131
+ </Menu.Root>
2132
+ ```
2133
+
2134
+ - **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).
2136
+ - **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
+ - **One element, one component.** Button, Input, Textarea, Badge, Kbd, Separator, Spinner, Skeleton, Link, Checkbox, Switch and Toggle are single components, not namespaces.
2138
+ - **No data props.** Collections are built from parts (`Menu.Item`, `Select.Item`, `Tabs.Tab`), not passed as `items` or `options` arrays.
2139
+
2140
+ ### Props
2141
+
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.
2143
+ - **Controlled or uncontrolled**, with Base UI's prop names:
2144
+ - `value` + `onValueChange`, or `defaultValue` (Select, Combobox, Tabs, ToggleGroup, RadioGroup)
2145
+ - `checked` + `onCheckedChange`, or `defaultChecked` (Checkbox, Switch, Menu.CheckboxItem)
2146
+ - `pressed` + `onPressedChange`, or `defaultPressed` (Toggle)
2147
+ - `open` + `onOpenChange`, or `defaultOpen` (Menu, Dialog, AlertDialog, Popover, Tooltip, Collapsible, Select, Combobox)
2148
+ - **`icon`** takes a Lucide element (`icon={<Pin />}`), never a string name.
2149
+ - **`render`** on every component and part swaps the rendered element, as in Base UI: a router's link in `Link`, a Button as a `Menu.Trigger`. Base UI doesn't render links through Button; a link that looks like a button is an anchor with `buttonClasses()` (see [Button](components/button.md#rules)).
2150
+ - **`className` and `style`** pass through to the part's element, after our classes.
2151
+ - **Accessible names.** A control without a visible label takes `aria-label`. Icon-only controls always do.
2152
+ - **`tone`**: `"neutral" | "success" | "warning" | "danger"` on status components (Badge, toasts).
2153
+ - **`variant`** is for Button only: `"primary" | "neutral" | "ghost" | "danger"`.
2154
+ - **Native props pass through** to the element a component or part renders.
2155
+
2156
+ ### Styling hooks
2157
+
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.
2159
+ - 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
+
2161
+ ### Toasts
2162
+
2163
+ Wrap the app in `Toast.Provider`, render `Toast.Viewport` once, and create toasts with Base UI's manager:
2164
+
2165
+ ```tsx
2166
+ const toast = Toast.useToastManager();
2167
+ toast.add({ title: "Note archived", type: "success" });
2168
+ ```
2169
+
2170
+ ## Shared styles for content
2171
+
2172
+ `Prose` is one stylesheet shared by every place content is rendered or edited, including the editor (`@usememos/editor`). Content never shifts between editing and reading.
2173
+
2174
+ ## What every new component needs
2175
+
2176
+ 1. A reason: a screen in a product needs it, and no existing component or pattern covers it.
2177
+ 2. A generic name and API that would make sense in any of our products.
2178
+ 3. Tokens only: no raw colors, sizes or shadows.
2179
+ 4. All three sizes, all [states](states.md), all three themes, and reduced motion.
2180
+ 5. Keyboard and screen reader behavior from Base UI.
2181
+ 6. A page in `docs/components/` and an entry in the [inventory](components/README.md).