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