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