@tanishqkancharla/maui 0.0.21 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/skills/maui/SKILL.md +64 -57
  3. package/skills/maui/references/apps/ai-chat.md +32 -0
  4. package/skills/maui/references/apps/calendar.md +46 -0
  5. package/skills/maui/references/apps/email-client.md +27 -0
  6. package/skills/maui/references/components/avatar.md +9 -0
  7. package/skills/maui/references/components/badge.md +9 -0
  8. package/skills/maui/references/components/buttons.md +57 -0
  9. package/skills/maui/references/components/code.md +11 -0
  10. package/skills/maui/references/components/crossfade.md +16 -0
  11. package/skills/maui/references/components/editor.md +22 -0
  12. package/skills/maui/references/components/form-controls.md +49 -0
  13. package/skills/maui/references/components/fuzzy-string.md +13 -0
  14. package/skills/maui/references/components/icons.md +16 -0
  15. package/skills/maui/references/components/layout-utilities.md +50 -0
  16. package/skills/maui/references/components/list-box.md +22 -0
  17. package/skills/maui/references/components/loading-screen.md +17 -0
  18. package/skills/maui/references/components/menu.md +17 -0
  19. package/skills/maui/references/components/prose.md +39 -0
  20. package/skills/maui/references/components/select.md +23 -0
  21. package/skills/maui/references/components/table.md +41 -0
  22. package/skills/maui/references/components/text.md +14 -0
  23. package/skills/maui/references/components/thinking.md +12 -0
  24. package/skills/maui/references/components/tooltip.md +15 -0
  25. package/skills/maui/references/patterns/assistant-message.md +23 -0
  26. package/skills/maui/references/patterns/inbox.md +36 -0
  27. package/skills/maui/references/patterns/message-list.md +23 -0
  28. package/skills/maui/references/patterns/sidebar.md +38 -0
  29. package/skills/maui/references/tokens.md +229 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanishqkancharla/maui",
3
- "version": "0.0.21",
3
+ "version": "0.0.22",
4
4
  "description": "Maui design system",
5
5
  "type": "module",
6
6
  "main": "./dist/maui.js",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: maui
3
- description: Conventions and design constraints for consuming the Maui design system. Use when building UI with Maui tokens, components, or purse-styles in an app that depends on Maui. Before designing or implementing new UI, read the closest example under src/apps/ or src/patterns/.
3
+ description: Conventions and design constraints for consuming the Maui design system. Use when building UI with Maui tokens, components, patterns, or purse-styles. Prefer this skill and its references for composition knowledge.
4
4
  ---
5
5
 
6
6
  # Maui
@@ -34,14 +34,32 @@ The published package exposes:
34
34
  - `"maui/src"` — TypeScript source barrel
35
35
  - `"maui/src/*"` — TypeScript source for deep imports
36
36
  - `"maui/skills/maui"` — this skill file
37
+ - `"maui/skills/maui/*"` — reference files next to this skill
37
38
 
38
39
  `MauiProvider` sets up theme (`data-theme` / `color-scheme`), `PurseProvider`, design-system globals, and the focus UI database used by Button/Dialog.
39
40
 
41
+ ## How to learn Maui
42
+
43
+ Prefer this skill for composition knowledge — how to assemble tokens, components, patterns, and apps. Reach for package source (`"maui/src/patterns"`, `"maui/src/apps"`, or an install under `node_modules`) when you need a detail the skill does not cover.
44
+
45
+ Before designing or implementing new UI:
46
+
47
+ 1. Read the constraints in this file.
48
+ 2. Open the matching reference (same grouping as the [gallery nav](https://maui.tanishqkancharla.dev)) and reuse its structure, tokens, and interactions.
49
+
50
+ | Need | Read |
51
+ | --- | --- |
52
+ | Tokens, `purse-styles`, motion, layout, theme | [references/tokens.md](references/tokens.md) |
53
+ | A component | [Components](#components) |
54
+ | A pattern | [Patterns](#patterns) |
55
+ | An app | [Apps](#apps) |
56
+
57
+ Patterns, demo apps, and the gallery `Panel` preview surface are **not** part of the `"maui"` package barrel. Prefer the recipes in those references and rebuild with barrel exports (`Button`, `Flex`, `text(...)`, …).
58
+
40
59
  ## Design constraints
41
60
 
42
- - Before designing or implementing new UI with Maui components, read the closest example under `src/apps/` or `src/patterns/`. Reuse its structure, components, tokens, and interactions.
43
61
  - Hover backgrounds have no transitions. Hover fills (`backgroundColor.elementHover`, quiet-button washes, list/row highlights) snap instantly. Do not animate `background` / `background-color` on hover with `motion.standard(...)` or a CSS `transition`. Other motion (tooltips, transforms) is fine.
44
- - Simple apps default to a `proseMaxWidth` column (`72ch`) centered in their container: `width: "100%"`, `maxWidth: proseMaxWidth`, `marginInline: "auto"`. `sizingTokens.contentWidth` is the same measure. Use this for single-column tools, settings, forms, and reading layouts. Multi-pane or full-bleed apps (inbox, calendar, IDE) are the exception.
62
+ - Simple apps default to a `proseMaxWidth` column (`72ch`) centered in their container: `width: "100%"`, `maxWidth: proseMaxWidth`, `marginInline: "auto"`. `sizing.contentWidth` is the same measure. Use this for single-column tools, settings, forms, and reading layouts. Multi-pane or full-bleed apps (inbox, calendar, IDE) are the exception.
45
63
  - Always design empty states. Every list, inbox, search result, or collection needs an intentional empty composition (copy and an optional action), never a blank panel.
46
64
 
47
65
  ## Theme FOUC
@@ -84,65 +102,54 @@ import { Text as TextIcon } from "maui/icons"
84
102
  <TextIcon size="md" />
85
103
  ```
86
104
 
87
- `size` uses the same t-shirt scale as `text(...)` (`2xs`–`xl`, default `sm`). Stroke and fill use `currentColor`. Icons that share a root export name (`Text`, `Badge`, `Switch`, …) are `TextIcon` / `BadgeIcon` / `SwitchIcon` from `"maui"`, or the original name from `"maui/icons"` / `Icons.Text`.
105
+ `size` uses the same t-shirt scale as `text(...)` (`2xs`–`xl`, default `sm`). Stroke and fill use `currentColor`. Icons that share a root export name (`Text`, `Badge`, `Switch`, `H1`, `H2`, `H3`, `Link`, `Menu`, `Code`, `Blockquote`, `Padding`, `SearchField`) are `TextIcon` / `BadgeIcon` / … from `"maui"`, or the original name from `"maui/icons"` / `Icons.Text`.
88
106
 
89
107
  ## Components
90
108
 
91
- ### Typography and reading
92
-
93
- - `Text` — size / weight / color / `monospace` span
94
- - `H1`–`H4`, `P`, `Label`, `Blockquote`, `Ul`, `Ol`, `Li`, `Link`
95
- - `Prose` — long-form rhythm; headings switch to the prose scale inside it
96
- - `Editor` — TipTap markdown surface (CommonMark shortcuts, `proseHtml` type) with no chrome; wrap it for padding, elevation, and actions
97
-
98
- ### Form controls
99
-
100
- - `Button` — `variant` is `"default"` | `"quiet"` | `"primary"`; `variantColor` is a palette name, or an opaque hex / `rgb()` string (`#${string}` or `rgb(`). Primary palettes fill step 9 (hover 10) with light text (`onAccent`, or step 12 on amber/lime/mint/sky/yellow). A CSS color is used as the fill (alpha is dropped; hover is `l - 0.04`; text is white or near-black from lightness). The edge is `tintedSubtle`. Quiet + color uses a 3.5% wash of the fill (hover 7%). Disable with React Aria’s `isDisabled` (maps to the native `disabled` attribute; no parallel `disabled` React prop).
101
- - `TextField`, `SearchField`, `NumberField`, `QuietTextField`
102
- - `Checkbox`, `Switch`, `Slider`
103
- - `RadioOptionGroup` / `RadioOption`
104
- - `Select` / `SelectItem`
105
-
106
- ### Collections and overlays
107
-
108
- - `ListBox` / `ListBoxItem`
109
- - `MenuTrigger` / `Menu` / `MenuItem`
110
- - `Tooltip`
111
- - `CollectionPopover` — shared popover used by Select and Menu
112
- - `Overlay`, `Dialog`
113
-
114
- ### Display
115
-
116
- - `Avatar`
117
- - `Badge`
118
- - `Code`, `Kbd`, `CodeBlock`
119
- - `Table` — React Aria table. `TableHeader` contains `TableHead` columns directly (no `TableRow`). Mark the identifying column with `isRowHeader` (required; usually the name/id column, not a leading checkbox or drag handle). When `selectionMode` is `"multiple"`, `TableHeader` and `TableRow` insert a leading checkbox column (`Checkbox slot="selection"`). `align` on `TableHead` / `TableCell` is `"start"` | `"center"` | `"end"`. `TableFooter` fills with `colors.gray[2]`. Place `TableCaption` after `Table`. `TableBody` renders “No results.” when empty; pass `renderEmptyState` to replace it.
120
- - `FuzzyString` — highlight segments; takes a match result, not a plain string
121
- - `Thinking` — 3×3 Game of Life indicator; reseeds when the board dies or loops
122
- - `Crossfade` — when `contentKey` changes, the previous view exits in `direction` (`up` | `down` | `left` | `right`) and the next view enters from the opposite side. `contentKey` is required. Do not put `key` on `Crossfade` itself or the exit is skipped.
123
- - `LoadingScreen` — fills available width and height. Optional `progressLabel`. Label at start fades Thinking (accent, small / `0.8em`) and the label (accent, weight 500, trailing `...`) in together; later label changes Crossfade up. With no label at start, Thinking waits 2s before fading in; a label before 2s fades both in immediately; a label after 2s animates in and shifts Thinking so the pair stays centered.
124
-
125
- ## Reference: patterns and apps
126
-
127
- Patterns, demo apps, and the gallery `Panel` preview surface are not part of the `"maui"` package barrel. Read the closest example before designing or implementing new UI, and reuse its structure, components, tokens, and interactions (also available via `"maui/src/..."`):
128
-
129
- ### Patterns — `src/patterns/`
130
-
131
- | Path | Role |
132
- | --- | --- |
133
- | `src/patterns/AssistantMessage.tsx` | Streaming markdown reply (Streamdown + Maui prose) |
134
- | `src/patterns/Sidebar.tsx` | App sidebar chrome |
135
- | `src/patterns/Inbox.tsx` | Mail inbox layout |
136
- | `src/patterns/MessageList.tsx` | Message list rows |
137
-
138
- ### Apps — `src/apps/`
109
+ Gallery order. Props and composition live in the linked file.
139
110
 
140
- | Path | Role |
111
+ | Page | Reference |
141
112
  | --- | --- |
142
- | `src/apps/AiChat/` | Mock streaming AI chat (Editor + AssistantMessage) |
143
- | `src/apps/EmailClient/` | Email client demo composing inbox patterns |
144
- | `src/apps/Calendar/` | Three-pane schedule (mini month, week grid, event details) |
145
- | `src/apps/JsxEditor/` | Live JSX playground (CodeMirror + Maui catalog) |
113
+ | Avatar | [avatar.md](references/components/avatar.md) |
114
+ | Badge | [badge.md](references/components/badge.md) |
115
+ | Buttons | [buttons.md](references/components/buttons.md) (`Button`, `Overlay`, `Dialog`) |
116
+ | Prose | [prose.md](references/components/prose.md) (`Prose`, `H1`–`H4`, `P`, lists, `Label`, `Link`) |
117
+ | Editor | [editor.md](references/components/editor.md) |
118
+ | Thinking | [thinking.md](references/components/thinking.md) |
119
+ | Crossfade | [crossfade.md](references/components/crossfade.md) |
120
+ | Loading screen | [loading-screen.md](references/components/loading-screen.md) |
121
+ | Text | [text.md](references/components/text.md) |
122
+ | Form controls | [form-controls.md](references/components/form-controls.md) |
123
+ | Select | [select.md](references/components/select.md) |
124
+ | List box | [list-box.md](references/components/list-box.md) |
125
+ | Table | [table.md](references/components/table.md) |
126
+ | Menu | [menu.md](references/components/menu.md) |
127
+ | Tooltip | [tooltip.md](references/components/tooltip.md) |
128
+ | Layout utilities | [layout-utilities.md](references/components/layout-utilities.md) |
129
+ | FuzzyString | [fuzzy-string.md](references/components/fuzzy-string.md) |
130
+ | Icons | [icons.md](references/components/icons.md) |
131
+ | Code | [code.md](references/components/code.md) |
132
+
133
+ ## Patterns
134
+
135
+ Not on the `"maui"` barrel. Prefer the recipes; rebuild with barrel exports in consuming apps. Hover fills snap; empty collections get copy.
136
+
137
+ | Page | Role | Reference |
138
+ | --- | --- | --- |
139
+ | Inbox | Mail thread list with unread dot, hover actions, selection | [inbox.md](references/patterns/inbox.md) |
140
+ | Message list | Thread of raised message cards (avatar + Prose body) | [message-list.md](references/patterns/message-list.md) |
141
+ | Assistant message | Streaming markdown reply (Streamdown + Maui prose + CodeBlock) | [assistant-message.md](references/patterns/assistant-message.md) |
142
+ | Sidebar | 240px nav: sections, active item, optional icon and trailing badge | [sidebar.md](references/patterns/sidebar.md) |
143
+
144
+ ## Apps
145
+
146
+ Not on the `"maui"` barrel. Prefer these layouts; rebuild with barrel exports.
147
+
148
+ | Page | Role | Reference |
149
+ | --- | --- | --- |
150
+ | Email client | Two-pane inbox + reading pane | [email-client.md](references/apps/email-client.md) |
151
+ | AI chat | Mock streaming chat (Editor + AssistantMessage + Thinking) | [ai-chat.md](references/apps/ai-chat.md) |
152
+ | Calendar | Three-pane schedule (mini month, week grid, event details) | [calendar.md](references/apps/calendar.md) |
146
153
 
147
154
  ## License
148
155
 
@@ -0,0 +1,32 @@
1
+ # AI chat
2
+
3
+ Gallery: `/apps/ai-chat`. Not on the `"maui"` barrel. Prefer this layout; rebuild with barrel components. Package source: `"maui/src/apps/AiChat/"` if you need a detail this file does not cover.
4
+
5
+ Uses [Editor](../components/editor.md), [Assistant message](../patterns/assistant-message.md), [Thinking](../components/thinking.md).
6
+
7
+ Column shell: outline border, `radius.lg`, `minHeight` ~560px / `maxHeight` ~720px, `overflow: hidden`.
8
+
9
+ ```
10
+ [ scrollable feed: user bubbles + assistant rows ]
11
+ [ composer: Editor in a raised shell + send ]
12
+ ```
13
+
14
+ **Feed** (`role="log"` `aria-label="Conversation"` `aria-relevant="additions"`)
15
+
16
+ - Padding `x: 8, y: 6`, column `gap` 6, `flex: 1`, `overflowY: auto`. Scroll to bottom when messages change.
17
+ - **User**: `justify: end`, bubble `maxWidth: 80%`, `prose("sm")` paragraph, `radius.md`, `shadow.subtle`, `background.element`, `pre-wrap`.
18
+ - **Assistant**: full-width column.
19
+ 1. Optional tool-call lines (lowContrast, ellipsis): `Read path`, `Wrote path`, `$ command` (command in `monospace`).
20
+ 2. AssistantMessage `size="sm"` `isAnimating={streaming}` — override `maxWidth: none` so it fills the pane.
21
+ 3. While streaming, a muted `Thinking` + “Thinking” (`xs` / lowContrast, row `gap` 4).
22
+
23
+ **Composer**
24
+
25
+ - Outer padding `x: 6, top: 4, bottom: 6`.
26
+ - Inner shell: `radius.lg`, `shadow.subtle`, `background.element`, padding `x: 4, y: 3`, column.
27
+ - `<Editor size="sm" onSubmit={send} editable={!streaming} placeholder="Message the assistant…" />`
28
+ - Send: circular quiet-ish `Button` (`radius.circle`, no box-shadow, `gray[3]` fill so it reads on `element`), icon `ArrowUp`, `aria-label="Send"`, disabled while streaming or empty. ⌘/Ctrl+Enter also sends (`Editor onSubmit`).
29
+
30
+ Mock streaming (no model required for a demo): wait ~3s (Thinking), optionally emit tool-call rows, then append markdown in small chunks. Keep `animated` on Streamdown the whole time; only `isAnimating` flips off when the last chunk lands.
31
+
32
+ Empty conversation: still show a welcome **assistant** markdown message, not a blank feed.
@@ -0,0 +1,46 @@
1
+ # Calendar
2
+
3
+ Gallery: `/apps/calendar`. Not on the `"maui"` barrel. Prefer this layout; rebuild with barrel components. Package source: `"maui/src/apps/Calendar/"` if you need a detail this file does not cover.
4
+
5
+ **Three panes** (sidebar can collapse):
6
+
7
+ ```
8
+ [ 196px sidebar ] [ flexible week grid ] [ ~200–220px details ]
9
+ ```
10
+
11
+ Shell: `radius.lg`, `shadow.subtle`, `minHeight` ~640px, `backgroundColor.app`, `overflow: hidden`. Without sidebar: `minmax(0, 1fr) minmax(200px, 220px)`.
12
+
13
+ This is a **full-bleed schedule**, not a prose column.
14
+
15
+ ## Sidebar
16
+
17
+ Raised `background.element` + `shadow.subtle`, column `gap` 6, padding 4.
18
+
19
+ - Toolbar: quiet icon buttons — hide sidebar (`Sidebar` icon), new event (`Plus`). Wrap in `Tooltip`.
20
+ - **Mini month**: 7-column weekday initials (`2xs`), 24px circular day buttons. Today = `accent[9]` + `onAccent` text. Selected (not today) = `grayAlpha[4]`. Outside-month = `gray[8]`. Hover = `elementHover` (no background transition on the chrome; day buttons may use `motion.standard` on color only). Prev/next month chevrons.
21
+ - `TextField` placeholder “Meet with...”.
22
+ - Account groups: xs/500/lowContrast email, then source rows (10px color swatch + name + hover-revealed quiet `Eye` to hide the calendar). Hidden sources drop to 45% opacity.
23
+ - Footer: quiet “Add calendar account” / “Add Notion database”.
24
+
25
+ Event colors: `accent` | `green` | `orange` | `pink` — fill step 3, text 11, selected fill 9 + `onAccent`.
26
+
27
+ ## Week grid
28
+
29
+ - Header: month title (`xl`/700) + `Avatar` + `Select` for 1/3/5/7-day view + `Today` + prev/next quiet chevrons (`Tooltip`).
30
+ - Sticky **all-day** row, then a vertically scrolling 24h grid (`HOUR_HEIGHT` 52px).
31
+ - Timed events: absolutely positioned blocks (`radius.sm`, padding 3/1, title + time range `2xs` lowContrast). Click a block to select; click empty grid to **create** a 30-minute event (snap 15 minutes).
32
+ - Today column: `accentAlpha[2]` wash. Now line: 2px `accent[9]` + circle on the today column.
33
+ - Time gutter ~36px, `2xs` mono labels.
34
+
35
+ ## Details pane
36
+
37
+ Raised column, padding 8.
38
+
39
+ - `SearchField` “Search events”. Matches render as a list of title (`FuzzyString`) + meta; click jumps to that event and date.
40
+ - Selected event: title `lg`/600, time range or “All day”, duration, calendar swatch + name, `Button` “Add meeting note”.
41
+ - None selected: “Select an event to see details, or click the grid to create one.”
42
+ - Footer: “Useful shortcuts” with `Kbd` (`T` today, `←`/`→` range, `` ` `` sidebar). Ignore shortcuts when focus is in an input.
43
+
44
+ Hotkeys (no modifiers): `t` today, arrows shift the visible range by `viewDays`, backtick toggles sidebar.
45
+
46
+ Empty search: show nothing extra (the selected-event block and shortcuts remain). Empty calendar: still show the grid + the details empty copy.
@@ -0,0 +1,27 @@
1
+ # Email client
2
+
3
+ Gallery: `/apps/email-client`. Not on the `"maui"` barrel. Prefer this layout; rebuild with barrel components. Package source: `"maui/src/apps/EmailClient/"` if you need a detail this file does not cover.
4
+
5
+ Patterns: [Inbox](../patterns/inbox.md), [Message list](../patterns/message-list.md).
6
+
7
+ **Two panes**, full-bleed (not a 72ch column).
8
+
9
+ ```
10
+ [ 240px inbox ] [ flexible reading pane ]
11
+ ```
12
+
13
+ Shell: `radius.lg`, `shadow.subtle`, `minHeight` ~640px, `overflow: hidden`, `backgroundColor.app`, grid `240px minmax(0, 1fr)`.
14
+
15
+ **Inbox pane**
16
+
17
+ - Column, padding step 2, `minHeight: 0`, list `overflowY: auto`.
18
+ - Header: `H3` “Inbox” (or mailbox name), padded to match multi-line row text (`pt` 6, `px` 4).
19
+ - Body: InboxMultiLine with `threads`, `selectedId`, `onSelectThread`. Reset the pattern’s default `marginTop` so it sits flush under the header.
20
+
21
+ **Reading pane**
22
+
23
+ - Padding `x: 16, y: 8`, column `gap` 6, `overflowY: auto`.
24
+ - Selected: `H2` subject, then MessageList of that thread’s messages (`sender`, `timestamp`, optional `edited`, `P` children).
25
+ - None selected: lowContrast “Select a thread to read messages.”
26
+
27
+ State: one `selectedThreadId`. Threads carry `id / senders / subject / snippet / time / unread?` plus a `messages[]` array. Unread dots and hover toolbars come from the inbox pattern.
@@ -0,0 +1,9 @@
1
+ # Avatar
2
+
3
+ Gallery: `/components/avatar`. Import from `"maui"`. Tokens: [tokens.md](../tokens.md).
4
+
5
+ Initials from `name` (first letters of up to two words). `size` matches `TextSize` (default `sm` = 18px). Color is hashed from the name (accent / green / orange / pink). `aria-hidden`.
6
+
7
+ ```tsx
8
+ <Avatar name="Maya Chen" size="lg" />
9
+ ```
@@ -0,0 +1,9 @@
1
+ # Badge
2
+
3
+ Gallery: `/components/badge`. Import from `"maui"`.
4
+
5
+ Compact pill count/status (`grayAlpha[3]`, 18px tall, tabular nums). Children are the label.
6
+
7
+ ```tsx
8
+ <Badge>12</Badge>
9
+ ```
@@ -0,0 +1,57 @@
1
+ # Buttons
2
+
3
+ Gallery: `/components/buttons`. Import from `"maui"`. Overlay and Dialog live on this page too.
4
+
5
+ ## `Button`
6
+
7
+ Native `<button>` (default `type="button"`). Height 28px, `shadow.subtle`, `radius` 4px.
8
+
9
+ ```tsx
10
+ <Button>Save</Button>
11
+ <Button variant="quiet">Cancel</Button>
12
+ <Button variant="primary" variantColor="blue">Create</Button>
13
+ <Button variant="primary" variantColor="#6366f1">Indigo</Button>
14
+ <Button isDisabled>Wait</Button>
15
+ <Button aria-label="Search"><Search size="sm" /></Button>
16
+ <Button>
17
+ <Plus size="sm" />
18
+ Create
19
+ </Button>
20
+ ```
21
+
22
+ | Prop | Notes |
23
+ | --- | --- |
24
+ | `variant` | `"default"` (raised element) \| `"quiet"` (no shadow, transparent) \| `"primary"` (solid fill) |
25
+ | `variantColor` | Palette name or opaque `#hex` / `rgb(...)`. Primary default is `"accent"`. Quiet ignores color unless set |
26
+ | `isDisabled` | React Aria name. Maps to native `disabled`. **No `disabled` React prop** |
27
+ | `children` | Text is wrapped for cap-height trim; SVG icons sit beside text. Icon-only needs `aria-label` |
28
+
29
+ Primary: fill step 9, hover 10, light text (`onAccent`, or step 12 on amber/lime/mint/sky/yellow). Custom CSS fill drops alpha; hover is `l - 0.04`; text is white or near-black from lightness. Edge is `tintedSubtle`. Quiet + color: 3.5% wash (hover 7%).
30
+
31
+ `useButton(props)` is exported for custom focus-tracked buttons; prefer `Button`.
32
+
33
+ ## `Overlay`
34
+
35
+ Full-viewport portal. `onClickOutside` fires when the backdrop itself is the mousedown target.
36
+
37
+ ```tsx
38
+ {open && (
39
+ <Overlay onClickOutside={() => setOpen(false)}>
40
+ {/* centered panel */}
41
+ </Overlay>
42
+ )}
43
+ ```
44
+
45
+ ## `Dialog`
46
+
47
+ `Overlay` + focus lock + scale/fade in. Children are the dialog body (padding 32px, `background.element`, 4px radius). Not React Aria Dialog — you own title and close.
48
+
49
+ ```tsx
50
+ {open && (
51
+ <Dialog onClickOutside={() => setOpen(false)}>
52
+ <H3>Confirm</H3>
53
+ <P>This cannot be undone.</P>
54
+ <Button onClick={() => setOpen(false)}>Close</Button>
55
+ </Dialog>
56
+ )}
57
+ ```
@@ -0,0 +1,11 @@
1
+ # Code
2
+
3
+ Gallery: `/components/code`. Import from `"maui"`.
4
+
5
+ ```tsx
6
+ <Code>proseMaxWidth</Code>
7
+ <Kbd>⌘</Kbd><Kbd>K</Kbd>
8
+ <CodeBlock lang="ts">{`const n = 1`}</CodeBlock>
9
+ ```
10
+
11
+ `CodeBlock` highlights with Shiki using the resolved theme. `lang` is required. Unsupported langs / pending highlight render a plain `<pre>`.
@@ -0,0 +1,16 @@
1
+ # Crossfade
2
+
3
+ Gallery: `/components/crossfade`. Import from `"maui"`.
4
+
5
+ When `contentKey` changes, the previous view exits in `direction` while the next view enters from the opposite side. Views overlap (`AnimatePresence` `mode="sync"`). Offset is spacing step 6. The root clips overflow.
6
+
7
+ ```tsx
8
+ <Crossfade direction="left" contentKey={slide.id}>
9
+ <Text size="lg">{slide.title}</Text>
10
+ </Crossfade>
11
+ ```
12
+
13
+ - `direction` is required: `"up" | "down" | "left" | "right"`. Gallery playground starts on `"left"`.
14
+ - `contentKey` is required. **Do not** put `key` on `Crossfade` itself or the exit is skipped.
15
+ - Timing is internal: enter is a spring (`visualDuration` 0.3, `bounce` 0.2); exit uses `motionDurationMs` (80ms) and ease-in-out. There are no public motion props.
16
+ - Reduced motion: opacity only.
@@ -0,0 +1,22 @@
1
+ # Editor
2
+
3
+ Gallery: `/components/editor`. Import from `"maui"`. Composer chrome: [AI chat](../apps/ai-chat.md).
4
+
5
+ Unchromed TipTap markdown surface. Wrap it for padding, elevation, and actions.
6
+
7
+ ```tsx
8
+ <Editor
9
+ content={draft}
10
+ onChange={setDraft}
11
+ onSubmit={send} // ⌘/Ctrl+Enter
12
+ placeholder="Write…"
13
+ size="sm" // ProseSize: sm | md | lg
14
+ editable={!streaming}
15
+ aria-label="Compose message"
16
+ />
17
+ ```
18
+
19
+ - `content` is markdown; updates apply when the string changes.
20
+ - `onChange` receives markdown.
21
+ - CommonMark shortcuts (`#`, `**`, `-`, `>`).
22
+ - No chrome, no submit button — parent owns those.
@@ -0,0 +1,49 @@
1
+ # Form controls
2
+
3
+ Gallery: `/components/form-controls`. Import from `"maui"`. [Select](select.md) has its own page.
4
+
5
+ All fields are 28px tall, full width of the parent, `shadow.subtle` except `QuietTextField`. They take React Aria field props (`value`, `onChange`, `placeholder`, `isDisabled`, `isInvalid`, `aria-label`, `id`, …).
6
+
7
+ ```tsx
8
+ <TextField aria-label="Title" placeholder="Title" value={v} onChange={setV} />
9
+ <SearchField aria-label="Search" value={q} onChange={setQ} /> // clear button when non-empty
10
+ <NumberField aria-label="Count" value={n} onChange={setN} minValue={0} maxValue={10} />
11
+ <QuietTextField aria-label="Filter" placeholder="Filter" value={f} onChange={setF} />
12
+ ```
13
+
14
+ Invalid (unfocused) adds a red 1px ring on top of `shadow.subtle`. Placeholders are italic `gray[8]`. Cap width in the parent (`maxWidth: 240px` is the gallery default).
15
+
16
+ ## `Checkbox`
17
+
18
+ Controlled only: `checked` + `setChecked`. Optional `label`.
19
+
20
+ ```tsx
21
+ <Checkbox label="Subscribe" checked={on} setChecked={setOn} />
22
+ ```
23
+
24
+ ## `Switch`
25
+
26
+ Controlled: `selected` + `onChange`. **`label` is required.**
27
+
28
+ ```tsx
29
+ <Switch label="Enable notifications" selected={on} onChange={setOn} />
30
+ ```
31
+
32
+ ## `Slider`
33
+
34
+ `label` required. React Aria slider props: `value`, `onChange`, `minValue`, `maxValue`, `step`. Default width 240px. Shows the formatted value as `<output>`.
35
+
36
+ ```tsx
37
+ <Slider label="Volume" value={v} onChange={setV} minValue={0} maxValue={100} />
38
+ ```
39
+
40
+ ## `RadioOptionGroup` / `RadioOption`
41
+
42
+ ```tsx
43
+ <RadioOptionGroup label="Plan" value={plan} onChange={setPlan}>
44
+ <RadioOption value="free">Free</RadioOption>
45
+ <RadioOption value="pro">Pro</RadioOption>
46
+ </RadioOptionGroup>
47
+ ```
48
+
49
+ `RadioOption` must be nested. Group also accepts React Aria radio-group props (`isDisabled`, …).
@@ -0,0 +1,13 @@
1
+ # FuzzyString
2
+
3
+ Gallery: `/components/fuzzy-string`. Import from `"maui"`. Search UI example: [Calendar](../apps/calendar.md).
4
+
5
+ Renders highlight segments. **Not a plain string.**
6
+
7
+ ```tsx
8
+ type FuzzyMatch = Array<{ match: string } | { skip: string }>
9
+
10
+ <FuzzyString match={[{ skip: "Em" }, { match: "ail" }, { skip: " client" }]} />
11
+ ```
12
+
13
+ Matched spans use `accent[11]`. The matcher is **not** on the package barrel — build `{ match | skip }[]` in the app (first matching subsequence is enough). Drop items that do not match; sort by a score if you have one.
@@ -0,0 +1,16 @@
1
+ # Icons
2
+
3
+ Gallery: `/components/icons`. Import named icons from `"maui"` or `"maui/icons"`.
4
+
5
+ ```ts
6
+ import { Search, Plus, ChevronLeft } from "maui"
7
+ import { Text as TextIcon, Badge as BadgeIcon } from "maui/icons"
8
+ ```
9
+
10
+ Root-export collisions (use the `*Icon` alias from `"maui"`, or the original name from `"maui/icons"`):
11
+
12
+ `BadgeIcon`, `BlockquoteIcon`, `CodeIcon`, `H1Icon`, `H2Icon`, `H3Icon`, `LinkIcon`, `MenuIcon`, `PaddingIcon`, `SearchFieldIcon`, `SwitchIcon`, `TextIcon`.
13
+
14
+ Avoid `import { Icons } from "maui"` in app code — it pulls the full set. The JSX editor catalog uses `Icons.*` because every icon is in scope.
15
+
16
+ `IconProps`: SVG props + `size?: TextSize` (default `sm`). Size matches the text t-shirt scale (`2xs`–`xl`). Stroke and fill use `currentColor`.
@@ -0,0 +1,50 @@
1
+ # Layout utilities
2
+
3
+ Gallery: `/components/layout-utilities`. Import from `"maui"`. Layout tokens: [tokens.md](../tokens.md).
4
+
5
+ Gallery-only: `Panel` is a preview frame in the docs site. It is **not** exported. Use `Flex` + `shadow` + `radius` instead.
6
+
7
+ ## `Flex`
8
+
9
+ Row **or** column (exactly one of `row` / `column` is required).
10
+
11
+ ```tsx
12
+ <Flex row gap={4} alignItems="center">
13
+ <Avatar name="Ada Lovelace" size="md" />
14
+ <Text size="lg" fontWeight={600}>Ada Lovelace</Text>
15
+ <Spacer />
16
+ <Badge>Active</Badge>
17
+ </Flex>
18
+ ```
19
+
20
+ | Prop | Notes |
21
+ | --- | --- |
22
+ | `row` / `column` | Required, mutually exclusive |
23
+ | `gap` | Spacing step |
24
+ | `p` / `padding` | All-side padding step |
25
+ | `px` `py` `pt` `pb` | Axis / side padding |
26
+ | `alignItems` | CSS `align-items` |
27
+ | `border` | `true` (outline) or `"border" \| "outline" \| "accent"`. **Skipped when `shadow` is set** |
28
+ | `shadow` | `"subtle" \| "medium" \| "strong"` — includes a ring; do not also set `border` |
29
+ | `radius` | Token key (`sm`, `lg`, `pill`, …) |
30
+ | `style` | React style object |
31
+
32
+ ## `Padding`
33
+
34
+ `xy` (all), `x` / `y`, or `top` `right` `bottom` `left`. Spacing steps.
35
+
36
+ ## `Gap`
37
+
38
+ Fixed spacer: `{ width: Space }` **or** `{ height: Space }`. Does not grow.
39
+
40
+ ## `Spacer`
41
+
42
+ `flex: 1 1 auto` — fills leftover space in a `Flex`.
43
+
44
+ ## `Divider`
45
+
46
+ Full-width `<hr>` (`gray[5]`, 1.5px). Has vertical margin (~1.5rem). For tight lists, prefer `gap: 1px` between rows instead.
47
+
48
+ ## `navigationItem`
49
+
50
+ Style object (not a component) for current-page nav rows: sm text, `radius.sm`, hover `elementHover`, `[aria-current="page"]` accent + weight 500. Used by the [Sidebar](../patterns/sidebar.md) recipe.
@@ -0,0 +1,22 @@
1
+ # List box
2
+
3
+ Gallery: `/components/list-box`. Import from `"maui"`.
4
+
5
+ Persistent selectable list (not a popup). Wrap in a raised, padded panel.
6
+
7
+ ```tsx
8
+ <ListBox
9
+ aria-label="Inboxes"
10
+ selectionMode="single"
11
+ selectedKeys={keys}
12
+ onSelectionChange={setKeys}
13
+ disallowEmptySelection
14
+ >
15
+ <ListBoxItem id="inbox">Inbox</ListBoxItem>
16
+ <ListBoxItem id="sent">Sent</ListBoxItem>
17
+ </ListBox>
18
+ ```
19
+
20
+ Hover/focus uses `elementHover` with **no** background transition. Selected: accent text + trailing ✓.
21
+
22
+ `textValue` is inferred from string children; pass it when children are not a string.
@@ -0,0 +1,17 @@
1
+ # Loading screen
2
+
3
+ Gallery: `/components/loading-screen`. Import from `"maui"`. Uses [Thinking](thinking.md) and [Crossfade](crossfade.md).
4
+
5
+ Fills available width and height (`flex: 1`, `width/height: 100%`). Centered `Thinking` (accent, `0.8em`).
6
+
7
+ ```tsx
8
+ <LoadingScreen />
9
+ <LoadingScreen progressLabel="Indexing files" />
10
+ ```
11
+
12
+ | First paint | Later `progressLabel` |
13
+ | --- | --- |
14
+ | Label present | Thinking + label fade in together. Later labels Crossfade `up`. Missing `...` / `…` is appended |
15
+ | No label | Thinking waits **2s**, then fades in. A label before 2s fades both in immediately. A label after 2s animates in and shifts Thinking so the pair stays centered |
16
+
17
+ `role="status"` `aria-busy` `aria-live="polite"`.
@@ -0,0 +1,17 @@
1
+ # Menu
2
+
3
+ Gallery: `/components/menu`. Import from `"maui"`. Related: [select.md](select.md).
4
+
5
+ Exactly two children on `MenuTrigger`: **trigger**, then **menu**. The trigger should be focusable.
6
+
7
+ ```tsx
8
+ <MenuTrigger placement="bottom start">
9
+ <Button>Actions</Button>
10
+ <Menu onAction={(key) => doAction(String(key))}>
11
+ <MenuItem id="rename">Rename</MenuItem>
12
+ <MenuItem id="delete">Delete</MenuItem>
13
+ </Menu>
14
+ </MenuTrigger>
15
+ ```
16
+
17
+ `placement` defaults to `"bottom start"`. Popover uses `shadow.strong` via `CollectionPopover`.
@@ -0,0 +1,39 @@
1
+ # Prose
2
+
3
+ Gallery: `/components/prose`. Import from `"maui"`. Type tokens: [tokens.md](../tokens.md).
4
+
5
+ Typography components have **no margin and no max-width**. Spacing belongs to the parent (`Flex` gap or `Prose`).
6
+
7
+ ## `Prose`
8
+
9
+ Long-form column: `maxWidth: 72ch` (`proseMaxWidth`) plus vertical rhythm. `size?: "sm" | "md" | "lg"` (default `md`) is inherited via `useProseSize()`.
10
+
11
+ ```tsx
12
+ <Prose size="md">
13
+ <H2>Shipping notes</H2>
14
+ <P>Body copy with <Link href="/docs">a link</Link>.</P>
15
+ <Ul>
16
+ <Li>First</Li>
17
+ <Li>Second</Li>
18
+ </Ul>
19
+ </Prose>
20
+ ```
21
+
22
+ Do not wrap app chrome (sidebars, toolbars, forms) in `Prose`.
23
+
24
+ ## `H1` `H2` `H3` `H4` `Blockquote` `Link`
25
+
26
+ **Children must be a `string`.** `Link` also needs `href`.
27
+
28
+ Outside `Prose` they use the app `text` scale (`H1` xl/700, `H2` lg/600, `H3`/`H4` md/600). Inside `Prose` they switch to the prose scale.
29
+
30
+ ## `P` `Ul` `Ol` `Li` `Label`
31
+
32
+ `P` / lists / `Li` take `ReactNode`. `Label` forwards `<label>` attributes (`htmlFor`, …) and uses xs/500/lowContrast, non-selectable (`labelText`).
33
+
34
+ ```tsx
35
+ <Flex column gap={3}>
36
+ <Label htmlFor="name">Name</Label>
37
+ <TextField id="name" aria-label="Name" />
38
+ </Flex>
39
+ ```
@@ -0,0 +1,23 @@
1
+ # Select
2
+
3
+ Gallery: `/components/select`. Import from `"maui"`. Related: [list-box.md](list-box.md), [menu.md](menu.md).
4
+
5
+ Trigger + `CollectionPopover` + `ListBox`. Width 100% of parent.
6
+
7
+ ```tsx
8
+ <Select
9
+ label="Favorite fruit"
10
+ placeholder="Choose a fruit"
11
+ selectedKey={key}
12
+ onSelectionChange={(k) => setKey(String(k))}
13
+ >
14
+ <SelectItem id="apple">Apple</SelectItem>
15
+ <SelectItem id="banana">Banana</SelectItem>
16
+ </Select>
17
+ ```
18
+
19
+ React Aria also accepts `value` / `onChange` on some versions; `selectedKey` / `onSelectionChange` is the usual collection API. Optional `description`, `errorMessage`, `items` + child render function.
20
+
21
+ Selected items show an accent checkmark on the right (`ListBoxItem`).
22
+
23
+ `CollectionPopover` is the shared RAC `Popover` for Select and Menu. Default `placement="bottom start"`, `offset={6}`, min-width = trigger width, max-height 280px, `shadow.strong`. Use it for a custom collection overlay.
@@ -0,0 +1,41 @@
1
+ # Table
2
+
3
+ Gallery: `/components/table`. Import from `"maui"`.
4
+
5
+ React Aria table. Columns live **directly** in `TableHeader` (no header `TableRow`).
6
+
7
+ ```tsx
8
+ <Table aria-label="Invoices">
9
+ <TableHeader>
10
+ <TableHead isRowHeader>Invoice</TableHead>
11
+ <TableHead>Status</TableHead>
12
+ <TableHead align="end">Amount</TableHead>
13
+ </TableHeader>
14
+ <TableBody>
15
+ {rows.map((row) => (
16
+ <TableRow key={row.id} id={row.id}>
17
+ <TableCell>{row.id}</TableCell>
18
+ <TableCell>{row.status}</TableCell>
19
+ <TableCell align="end">{row.amount}</TableCell>
20
+ </TableRow>
21
+ ))}
22
+ </TableBody>
23
+ <TableFooter>
24
+ <TableRow id="total">
25
+ <TableCell colSpan={2}>Total</TableCell>
26
+ <TableCell align="end">$2,500.00</TableCell>
27
+ </TableRow>
28
+ </TableFooter>
29
+ </Table>
30
+ <TableCaption>Recent invoices.</TableCaption>
31
+ ```
32
+
33
+ Rules:
34
+
35
+ - Mark the identifying column with `isRowHeader` (name/id, not a checkbox).
36
+ - `align` on `TableHead` / `TableCell`: `"start"` | `"center"` | `"end"`.
37
+ - Selection is **opt-in**. Without `selectionMode`, rows do not highlight on hover.
38
+ - `selectionMode="multiple"` inserts a leading checkbox column (select-all in the header). Hover/selected washes follow `data-selection-mode`.
39
+ - `TableBody` default empty state is “No results.” Pass `renderEmptyState`.
40
+ - Put `TableCaption` **after** `Table` (often inside a `<figure>`).
41
+ - `TableFooter` uses `gray[2]`.
@@ -0,0 +1,14 @@
1
+ # Text
2
+
3
+ Gallery: `/components/text`. Import from `"maui"`. Type tokens: [tokens.md](../tokens.md). Headings and `Prose`: [prose.md](prose.md).
4
+
5
+ Inline `<span>`. Defaults: `size="md"`, `fontWeight={400}`, `color="highContrast"`.
6
+
7
+ ```tsx
8
+ <Text size="sm" color="lowContrast">Secondary</Text>
9
+ <Text size="lg" fontWeight={600} monospace>src/maui.ts</Text>
10
+ ```
11
+
12
+ Also accepts native span props except `color` (that axis is the token).
13
+
14
+ `size`: `2xs` | `xs` | `sm` | `md` | `lg` | `xl`. `fontWeight`: `400` | `500` | `600` | `700`. `color`: `lowContrast` | `highContrast` | `accent` | `onAccent`.
@@ -0,0 +1,12 @@
1
+ # Thinking
2
+
3
+ Gallery: `/components/thinking`. Import from `"maui"`.
4
+
5
+ 3×3 Game of Life, text-sized. `size` is a CSS length (default `1em`). `variant`: `"primary"` (currentColor) | `"accent"` | `"muted"`.
6
+
7
+ ```tsx
8
+ <Flex row alignItems="center" gap={4}>
9
+ <Thinking size="0.75em" variant="muted" />
10
+ <Text size="xs" color="lowContrast">Thinking</Text>
11
+ </Flex>
12
+ ```
@@ -0,0 +1,15 @@
1
+ # Tooltip
2
+
3
+ Gallery: `/components/tooltip`. Import from `"maui"`.
4
+
5
+ Wraps the trigger in an inline-block `<span>`. Child should contain something focusable.
6
+
7
+ ```tsx
8
+ <Tooltip content="Previous" placement="top" delay={500}>
9
+ <Button variant="quiet" aria-label="Previous">
10
+ <ChevronLeft size="sm" />
11
+ </Button>
12
+ </Tooltip>
13
+ ```
14
+
15
+ `placement`: `top` | `bottom` | `left` | `right`. Adjacent tooltips skip enter animation (warm group); only the first/last animate.
@@ -0,0 +1,23 @@
1
+ # Assistant message
2
+
3
+ Gallery: `/patterns/assistant-message`. Not on the `"maui"` barrel. Prefer this recipe; rebuild with barrel components. Package source: `"maui/src/patterns/AssistantMessage.tsx"` if you need a detail this file does not cover.
4
+
5
+ `AssistantMessage`: streaming **markdown reply**. Same reading scale as `Prose` (`proseHtml(size)`), max width `proseMaxWidth`.
6
+
7
+ ```tsx
8
+ <AssistantMessage size="sm" isAnimating={streaming}>
9
+ {markdown}
10
+ </AssistantMessage>
11
+ ```
12
+
13
+ Rebuild with Streamdown + Maui tokens (Streamdown is a Maui dependency):
14
+
15
+ 1. Root: `maxWidth: proseMaxWidth`, `minWidth: 0`. `aria-live="polite"` and `aria-busy` while `isAnimating`.
16
+ 2. `<Streamdown>` with `proseHtml(size)` on the root (flex column, Maui type, **drop Streamdown’s Tailwind classNames** on `h1`/`p`/`ul`/… so markers and gaps stay under Maui).
17
+ 3. Fenced code: while the fence is incomplete, render a plain `<pre>` (`radius.md`, `shadow.subtle`, `backgroundColor.app`, padding 12px) so the highlighter does not remount every chunk. When complete, `<CodeBlock lang={lang}>{text}</CodeBlock>`.
18
+ 4. Animation: keep Streamdown `animated` **stably on**; toggle `isAnimating` only. Flipping `animated` resets stagger and makes new blocks pop. Word fade: `duration: motionStreamDurationMs`, `easing: motionEasing`, `stagger: 16`. While animating, also apply `proseStreamingMarkers` so list bullets fade with words.
19
+ 5. `controls={false}`, `lineNumbers={false}`, `mode="streaming"`.
20
+
21
+ Do not wrap AssistantMessage in `Prose` — `proseHtml` already owns rhythm.
22
+
23
+ User turns in a chat are **not** this pattern: they are a right-aligned bubble (`prose("sm").paragraph`, `radius.md`, `shadow.subtle`, `background.element`, max-width ~80%). See [AI chat](../apps/ai-chat.md).
@@ -0,0 +1,36 @@
1
+ # Inbox
2
+
3
+ Gallery: `/patterns/inbox`. Not on the `"maui"` barrel. Prefer this recipe; rebuild with barrel components. Package source: `"maui/src/patterns/Inbox.tsx"` if you need a detail this file does not cover.
4
+
5
+ `Inbox` / `InboxMultiLine`: mail **thread list**, not a table. Two densities:
6
+
7
+ | | `Inbox` (single line) | `InboxMultiLine` |
8
+ | --- | --- | --- |
9
+ | Grid | `minmax(180px, 24%) minmax(0, 1fr) 92px` — sender · subject+snippet · time | Column: sender+time row, subject, 2-line snippet |
10
+ | Height | ~40px | Padded `y: 6, x: 4` |
11
+ | Unread | 6px accent dot before sender | Same, aligned to the first line |
12
+
13
+ Row model:
14
+
15
+ ```ts
16
+ type EmailThread = {
17
+ id: string
18
+ senders: string
19
+ subject: string
20
+ snippet: string
21
+ time: string
22
+ unread?: boolean
23
+ }
24
+ ```
25
+
26
+ Behavior:
27
+
28
+ - `selectedId` + `onSelectThread(id)`. Selected row uses `backgroundColor.elementActive`. Hover uses `elementHover` (instant).
29
+ - `radius.md`, `userSelect: none`, list `gap: 1px`.
30
+ - Subject: highContrast, ellipsis. Snippet: lowContrast, ellipsis (multi-line clamps to 2).
31
+ - Time sits on the right. **On hover**, time fades out and a quiet icon toolbar (`Star`, `Archive`, `Trash`, `Envelope`, `Clock`) fades in at the right — `shadow.subtle` chip, `Button variant="quiet"` with `aria-label`, `stopPropagation` on click.
32
+ - Always include an empty state when `threads.length === 0` (“No messages” + optional compose action).
33
+
34
+ Single-line sender column is highContrast with the unread dot. Multi-line puts the dot in a leading column (`align: start`).
35
+
36
+ [Email client](../apps/email-client.md) uses **InboxMultiLine** in a 240px pane.
@@ -0,0 +1,23 @@
1
+ # Message list
2
+
3
+ Gallery: `/patterns/message-list`. Not on the `"maui"` barrel. Prefer this recipe; rebuild with barrel components. Package source: `"maui/src/patterns/MessageList.tsx"` if you need a detail this file does not cover.
4
+
5
+ `MessageList` + `Message`: raised **cards** in a column (`gap` 6, max width ~760px), `role="feed"`.
6
+
7
+ Each `Message`:
8
+
9
+ ```tsx
10
+ <article> {/* radius.lg, shadow.subtle, padding 8, column gap 4, background.element */}
11
+ <header> {/* row, wrap, gap 2 */}
12
+ <Avatar name={sender} size="sm" />
13
+ <Text size="sm" fontWeight={500}>{sender}</Text>
14
+ <time><Text size="xs" color="lowContrast">{timestamp}</Text></time>
15
+ {edited ? <Text size="xs" color="lowContrast">(edited)</Text> : null}
16
+ </header>
17
+ <Prose>{/* body: P / lists, not a raw string */}</Prose>
18
+ </article>
19
+ ```
20
+
21
+ - `sender` + `timestamp` required; `edited?` appends “(edited)”.
22
+ - Body is React children inside `Prose` (so headings/paragraphs get reading rhythm). For email, keep the inner `Prose` unconstrained (`minWidth: 0`) so it fills the card.
23
+ - Empty thread: “Select a thread to read messages.” See [Email client](../apps/email-client.md).
@@ -0,0 +1,38 @@
1
+ # Sidebar
2
+
3
+ Gallery: `/patterns/sidebar`. Not on the `"maui"` barrel. Prefer this recipe; rebuild with barrel components. Package source: `"maui/src/patterns/Sidebar.tsx"` if you need a detail this file does not cover.
4
+
5
+ `Sidebar` / `SidebarSection` / `SidebarItem`: fixed **240px** nav: raised `background.element` + `shadow.subtle` + `radius.lg`, column `gap={8}`, padding step 2.
6
+
7
+ Uses `navigationItem` from [layout utilities](../components/layout-utilities.md).
8
+
9
+ Composition:
10
+
11
+ 1. Optional brand block (not a special component).
12
+ 2. One or more sections: xs/500/lowContrast label, then a list of items.
13
+ 3. Each item is a full-width quiet row: 16px icon column, label, optional trailing (`Badge`).
14
+
15
+ ```tsx
16
+ import { Flex, Text } from "maui"
17
+
18
+ <nav aria-label="Workspace">
19
+ {/* 240px column, padding step 2, radius.lg, shadow.subtle, background.element, gap 8 */}
20
+ <Flex column gap={2} px={4} pt={4}>
21
+ <Text size="sm" fontWeight={600}>Maui Cloud</Text>
22
+ <Text size="xs" color="lowContrast">Production</Text>
23
+ </Flex>
24
+ {/* sections + items — see layout rules below */}
25
+ </nav>
26
+ ```
27
+
28
+ Item layout (rebuild, do not invent a different nav row):
29
+
30
+ - Grid: `16px minmax(0, 1fr) auto`, column gap spacing 3.
31
+ - Style from `navigationItem` + `focusRing("&:focus-visible")`.
32
+ - `aria-current="page"` when `active` — accent color + weight 500, **no** filled background.
33
+ - Icon wrap is 16×16, `gray[11]`; active icon uses `accent[9]`.
34
+ - Label: 13/20, ellipsis. Section label is padded to align with the text column (padding-start = item padding 4 + 16px icon + gap 3).
35
+ - Native `<button type="button">` inside `<li>`, not `Button` — so the row can be a grid without the 28px control chrome.
36
+ - List: no bullets, `gap: 1px`.
37
+
38
+ Hover: `backgroundColor.elementHover`, instant.
@@ -0,0 +1,229 @@
1
+ # Tokens, theme, and styling
2
+
3
+ Use this file when choosing color, type, space, motion, or elevation, or when writing custom `purse-styles` around Maui components.
4
+
5
+ Import tokens from `"maui"`. Import `style` / `useStyles` from `"purse-styles"`. Prefer semantic tokens over one-off CSS variables.
6
+
7
+ ## Theme
8
+
9
+ `MauiProvider` wraps `ThemeProvider` + `PurseProvider` + global CSS + the focus UI database.
10
+
11
+ ```tsx
12
+ import { MauiProvider, themeFoucScript, useTheme } from "maui"
13
+
14
+ // In <head>, before React:
15
+ // <script>{themeFoucScript}</script>
16
+
17
+ const { preference, resolvedTheme, setPreference } = useTheme()
18
+ // preference: "system" | "light" | "dark"
19
+ // resolvedTheme: "light" | "dark"
20
+ ```
21
+
22
+ - Storage key: `themeStorageKey` (`"maui-theme"`).
23
+ - Dark CSS: `DARK_THEME` is `:root[data-theme="dark"]`. `defineVars` dark values use that selector.
24
+ - FOUC: inline `themeFoucScript` in a classic `<script>` in `<head>` so `data-theme` and `color-scheme` exist before first paint.
25
+
26
+ `MauiProvider` also sets `box-sizing: border-box`, zeros `html`/`body` margin, paints `backgroundColor.app`, applies `baseTextStyle` (md / 400 / highContrast) on `html, body`, and resets heading/paragraph margins.
27
+
28
+ ## purse-styles
29
+
30
+ Maui styles are `purse-styles` objects. `useStyles(...)` turns one or more of them into a class name.
31
+
32
+ ```tsx
33
+ import { style, useStyles } from "purse-styles"
34
+ import { flex, spacing, text, backgroundColor, radius, shadow } from "maui"
35
+
36
+ function Row({ children }: { children: React.ReactNode }) {
37
+ const className = useStyles(
38
+ flex({ direction: "row", align: "center", gap: 4 }),
39
+ radius.sm,
40
+ shadow.subtle,
41
+ style({
42
+ minWidth: 0,
43
+ backgroundColor: backgroundColor.element,
44
+ }),
45
+ )
46
+ return <div className={className}>{children}</div>
47
+ }
48
+ ```
49
+
50
+ - `text({...})`, `shadow.subtle`, `background.element`, `radius.md`, `flex({...})`, `spacing.padding({...})` are already style objects — pass them to `useStyles`.
51
+ - `style({ ... })` is for leftover CSS that tokens do not cover (`minWidth`, grid templates, absolute positioning).
52
+ - Compose several objects in one `useStyles` call. Falsy entries are skipped.
53
+ - For one-off layout, prefer the `Flex` / `Padding` components (see [layout-utilities.md](components/layout-utilities.md)). For repeated custom chrome, prefer tokens + `useStyles`.
54
+
55
+ ## Color
56
+
57
+ `colors` is Radix Scales with a brand `accent` (teal in light, violet in dark). Each scale has steps **1–12**. Alpha scales are `colors.grayAlpha`, `colors.accentAlpha`, `colors.blueAlpha`, …
58
+
59
+ | Steps | Use |
60
+ | --- | --- |
61
+ | 1–2 | App / subtle surfaces |
62
+ | 3–5 | Interactive washes |
63
+ | 6–8 | Borders / strong lines |
64
+ | 9–10 | Solid fills (9 rest, 10 hover) |
65
+ | 11–12 | Text (11 secondary, 12 primary) |
66
+
67
+ ```ts
68
+ import { colors, colorNames, paletteNames } from "maui"
69
+
70
+ colors.accent[9] // solid brand fill
71
+ colors.gray[12] // high-contrast text
72
+ colors.blueAlpha[8]
73
+ ```
74
+
75
+ `colorNames` is `"accent"` plus every Radix palette name. `paletteNames` omits `"accent"`. `"blue"` as a `variantColor` is the Radix blue scale, not CSS `blue`.
76
+
77
+ Prefer semantic surfaces over raw scale steps for chrome:
78
+
79
+ ```ts
80
+ import { background, backgroundColor } from "maui"
81
+
82
+ backgroundColor.app // page
83
+ backgroundColor.element // raised control / card
84
+ backgroundColor.elementHover // 3.5% gray-12 wash over element
85
+ backgroundColor.elementActive // 7% wash
86
+ background.app // style objects of the same values
87
+ background.element
88
+ background.accent // accent[9]
89
+ ```
90
+
91
+ Hover/active washes use `color-mix` in oklch. **Do not transition `background` / `background-color` on hover.**
92
+
93
+ ## Text
94
+
95
+ ```ts
96
+ import { text, monospace, fontFamily, monoFontFamily } from "maui"
97
+
98
+ text({
99
+ size: "sm", // 2xs | xs | sm | md | lg | xl (default md)
100
+ fontWeight: 500, // 400 | 500 | 600 | 700 (default 400)
101
+ color: "lowContrast", // lowContrast | highContrast | accent | onAccent
102
+ monospace: true,
103
+ })
104
+ ```
105
+
106
+ | Size | Font | Line |
107
+ | --- | --- | --- |
108
+ | `2xs` | 10px | 14px |
109
+ | `xs` | 12px | 18px |
110
+ | `sm` | 13px | 20px |
111
+ | `md` | 14px | 22px |
112
+ | `lg` | 16px | 24px |
113
+ | `xl` | 22px | 30px |
114
+
115
+ - `lowContrast` → `gray[11]`, `highContrast` → `gray[12]`, `accent` → `accent[11]`, `onAccent` → white.
116
+ - UI sans: `fontFamily` (system ui-sans-serif stack). Mono: Commit Mono with `ss05` smart kerning (`monoFontStyle` / `monospace`).
117
+ - `baseTextStyle` is md / 400 / highContrast — already on `html, body`.
118
+ - Inside `Prose`, `H1`–`H4` / `P` / lists switch to the **prose** scale (`sm` 14px, `md` 16px, `lg` 18px), which is larger and has reading rhythm. Do not put app chrome inside `Prose`.
119
+
120
+ `proseMaxWidth` is `"72ch"`. `sizing.contentWidth` is `maxWidth: 72ch`. `sizing.fullWidth` is `width: 100%`.
121
+
122
+ ## Spacing
123
+
124
+ Scale steps are **not pixels**:
125
+
126
+ | Step | px |
127
+ | --- | --- |
128
+ | 1 | 2 |
129
+ | 2 | 4 |
130
+ | 3 | 6 |
131
+ | 4 | 9 |
132
+ | 6 | 12 |
133
+ | 8 | 16 |
134
+ | 12 | 24 |
135
+ | 16 | 32 |
136
+
137
+ ```ts
138
+ spacing.padding({ all: 4, x: 6, y: 2, top: 8 })
139
+ spacing.gap[4] // style { gap: 9px }
140
+ spacing.value(4) // "9px" — only when gap/padding tokens cannot apply
141
+ ```
142
+
143
+ `Flex` / `Padding` / `Gap` / `flex({ gap })` all take these steps.
144
+
145
+ ## Layout tokens
146
+
147
+ ```ts
148
+ flex({ direction: "row" | "column", align, justify, gap, wrap })
149
+ flexItem({ size: "hug" | "fill" | "auto", align, order })
150
+ grid({ columns: "one" | "two" | "three" | "autoFit" | "sidebarContent", align, justify, gap })
151
+ gridItem({ area: "sidebar" | "content", span: "full" | 1 | 2 | 3, align, justify })
152
+ ```
153
+
154
+ - `align`: `start` | `center` | `end` | `stretch` | `baseline`
155
+ - `justify`: `start` | `center` | `end` | `between` | `around` | `evenly`
156
+ - `gap` on these token helpers also accepts `0`.
157
+ - `sidebarContent` is `180px minmax(0, 1fr)` with areas `"sidebar content"`.
158
+
159
+ Prefer `<Flex row gap={4}>` for one-off JSX. Use `flex()` / `grid()` when composing with other tokens.
160
+
161
+ ## Radius
162
+
163
+ `radius.none` | `2xs` (2px) | `xs` (3) | `sm` (4) | `md` (6) | `lg` (8) | `xl` (12) | `pill` | `circle`.
164
+
165
+ Controls typically use `sm`. Cards / sidebars use `lg`. Avatars use `circle`.
166
+
167
+ ## Shadows and rings
168
+
169
+ ```ts
170
+ shadow.subtle | shadow.medium | shadow.strong // style objects
171
+ shadowVars.subtle | .medium | .strong // raw box-shadow strings
172
+ tintedSubtle(color) // subtle ring+blur tinted from a fill
173
+ ```
174
+
175
+ Every elevation already includes a 1px ring. **Do not also `border()` the same element.**
176
+
177
+ `border(sides, color)`:
178
+
179
+ - `sides`: `[]` for all sides, or `"top" | "right" | "bottom" | "left"`
180
+ - `color`: `"border"` (gray-12 @ 5%) | `"outline"` (10%) | `"accent"` (`accent[8]`)
181
+
182
+ `borderColor.border` / `borderColor.outline` are the raw color strings (hairlines, `borderBottom`).
183
+
184
+ `focusRing(selector = "&:focus-visible", existingShadow?)` — Radix blue ring. Pass `shadowVars.subtle` as the second argument so a raised control keeps its elevation while focused.
185
+
186
+ `visuallyHidden` — clip an input and keep the styled sibling visible (Checkbox / Switch / Radio).
187
+
188
+ ## Motion
189
+
190
+ ```ts
191
+ motionDurationMs // 80
192
+ motionStreamDurationMs // 80 — Streamdown word fade
193
+ motionEasing // "ease-in-out"
194
+ motion.standard("opacity", "transform") // 80ms ease-in-out on those properties
195
+ ```
196
+
197
+ Use `motion.standard` for opacity, transform, box-shadow, color. **Never** for hover `background` / `background-color`.
198
+
199
+ Tooltips use a snappy spring (not `motion.standard`). Crossfade enter is a 0.3s spring (`bounce` 0.2); exit uses `motionDurationMs` / ease-in-out. Streaming markdown uses `motionStreamDurationMs`.
200
+
201
+ ## Sizing and icons
202
+
203
+ `iconSizeValues` pairs t-shirt sizes to icon boxes (slightly above the matching font-size):
204
+
205
+ | Size | Box |
206
+ | --- | --- |
207
+ | `2xs` | 12px |
208
+ | `xs` | 14px |
209
+ | `sm` | 16px (default) |
210
+ | `md` | 18px |
211
+ | `lg` | 20px |
212
+ | `xl` | 24px (intrinsic SVG) |
213
+
214
+ Stroke/fill is `currentColor`. Pass `size` on the icon component; optional `width` / `height` override the box.
215
+
216
+ ## Avatar tokens
217
+
218
+ `avatar.green` / `avatar.orange` / `avatar.pink` each have `background` and `foreground`. The `Avatar` component also uses `accent[4]`/`[11]`. You rarely need these tokens directly.
219
+
220
+ ## Prose tokens
221
+
222
+ For HTML that is **not** React typography (`Editor` ProseMirror tree, Streamdown output):
223
+
224
+ - `prose(size)` — style objects for paragraph, h1–h4, link, blockquote, list (`size`: `sm` | `md` | `lg`)
225
+ - `proseRhythm(size)` — margin-top-only rhythm for `Prose` children
226
+ - `proseHtml(size)` — flex-column + gap styles for a markdown HTML tree
227
+ - `proseStreamingMarkers` — fade list markers with Streamdown’s word animation
228
+
229
+ Prefer `<Prose>` + `H1`/`P`/… for React trees. Use `proseHtml` only when rendering markdown HTML.