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