@sableui/react 0.2.0 → 0.3.0

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 (97) hide show
  1. package/AGENTS.md +3 -1
  2. package/CHANGELOG.md +59 -0
  3. package/CONTRIBUTING.md +8 -0
  4. package/README.md +1 -0
  5. package/bin/sable-lint.mjs +0 -0
  6. package/bin/sable.mjs +68 -26
  7. package/dist/blocks/index.d.ts +396 -0
  8. package/dist/blocks/index.js +1997 -0
  9. package/dist/chunk-6LCIBMMG.js +4908 -0
  10. package/dist/{chunk-AOWBYEQ6.js → chunk-CKNM7KSB.js} +1598 -6464
  11. package/dist/chunk-K6LII3OE.js +5093 -0
  12. package/dist/{chunk-PAOZBZME.js → chunk-NGBDP3BM.js} +1 -1
  13. package/dist/{chunk-5Z5KLOAU.js → chunk-TH563WLW.js} +14 -1
  14. package/dist/icon/index.js +2 -2
  15. package/dist/icon-sets/reicon.js +1 -1
  16. package/dist/index.d.ts +471 -34
  17. package/dist/index.js +588 -2589
  18. package/dist/ios/index.d.ts +8 -1
  19. package/dist/ios/index.js +6 -5
  20. package/dist/ios/preview.d.ts +11 -1
  21. package/dist/ios/preview.js +13 -8
  22. package/dist/ios/styles.css +6 -0
  23. package/dist/{meta-6dUzgC6M.d.ts → meta-CcE_SeFu.d.ts} +37 -1
  24. package/dist/patterns/index.d.ts +1 -1
  25. package/dist/patterns/index.js +29 -30
  26. package/dist/themes/base/base.css +23 -0
  27. package/dist/themes/index.js +2 -2
  28. package/llms/blocks/api-keys-block.md +79 -0
  29. package/llms/blocks/chat-thread-block.md +51 -0
  30. package/llms/blocks/command-palette-block.md +48 -0
  31. package/llms/blocks/cta-band-block.md +62 -0
  32. package/llms/blocks/danger-zone-block.md +69 -0
  33. package/llms/blocks/device-comparison-block.md +91 -0
  34. package/llms/blocks/event-card-block.md +84 -0
  35. package/llms/blocks/faq-block.md +72 -0
  36. package/llms/blocks/feature-grid-block.md +75 -0
  37. package/llms/blocks/feedback-form-block.md +66 -0
  38. package/llms/blocks/goal-progress-block.md +54 -0
  39. package/llms/blocks/hero-block.md +65 -0
  40. package/llms/blocks/invite-team-block.md +61 -0
  41. package/llms/blocks/invoice-history-block.md +84 -0
  42. package/llms/blocks/kpi-sparkline-block.md +57 -0
  43. package/llms/blocks/media-player-block.md +100 -0
  44. package/llms/blocks/newsletter-block.md +64 -0
  45. package/llms/blocks/not-found-block.md +56 -0
  46. package/llms/blocks/notification-list-block.md +63 -0
  47. package/llms/blocks/notification-settings-block.md +54 -0
  48. package/llms/blocks/order-tracking-block.md +62 -0
  49. package/llms/blocks/pricing-card-block.md +82 -0
  50. package/llms/blocks/pricing-table-block.md +129 -0
  51. package/llms/blocks/product-card-block.md +69 -0
  52. package/llms/blocks/profile-card-block.md +103 -0
  53. package/llms/blocks/profile-form-block.md +114 -0
  54. package/llms/blocks/revenue-breakdown-block.md +70 -0
  55. package/llms/blocks/sign-in-block.md +64 -0
  56. package/llms/blocks/sign-up-block.md +78 -0
  57. package/llms/blocks/site-footer-block.md +96 -0
  58. package/llms/blocks/stat-row-block.md +59 -0
  59. package/llms/blocks/steps-block.md +73 -0
  60. package/llms/blocks/task-list-block.md +81 -0
  61. package/llms/blocks/team-members-block.md +91 -0
  62. package/llms/blocks/testimonial-block.md +78 -0
  63. package/llms/blocks/traffic-sources-block.md +63 -0
  64. package/llms/blocks/traffic-trend-block.md +95 -0
  65. package/llms/blocks/upload-list-block.md +66 -0
  66. package/llms/blocks/verify-code-block.md +59 -0
  67. package/llms/blocks/wallet-block.md +84 -0
  68. package/llms/components/area-chart.md +112 -0
  69. package/llms/components/bar-chart.md +131 -0
  70. package/llms/components/button.md +1 -1
  71. package/llms/components/card.md +2 -2
  72. package/llms/components/context-menu.md +1 -1
  73. package/llms/components/date-tile.md +79 -0
  74. package/llms/components/entity.md +4 -2
  75. package/llms/components/line-chart.md +101 -0
  76. package/llms/components/menu.md +1 -1
  77. package/llms/components/{dots-menu.md → overflow-menu.md} +9 -9
  78. package/llms/components/pie-chart.md +103 -0
  79. package/llms/components/radar-chart.md +106 -0
  80. package/llms/components/radial-chart.md +102 -0
  81. package/llms/components/segmented-bar.md +102 -0
  82. package/llms/components/settings-menu.md +2 -2
  83. package/llms/components/sparkline.md +80 -0
  84. package/llms/components/table.md +1 -1
  85. package/llms/patterns/auth-page.md +2 -2
  86. package/llms/patterns/dashboard-page.md +6 -7
  87. package/llms/patterns/feed-page.md +7 -7
  88. package/llms/patterns/list-detail-page.md +7 -15
  89. package/llms/patterns/settings-page.md +10 -21
  90. package/llms/tasks/building-an-ios-prototype.md +4 -1
  91. package/llms/tasks/choosing-a-component.md +2 -2
  92. package/llms-full.txt +4296 -277
  93. package/llms.txt +55 -3
  94. package/package.json +5 -1
  95. package/registry.json +6383 -3741
  96. package/targets/ios.json +90 -9
  97. package/dist/{chunk-L6Y46C62.js → chunk-MXJX3NZI.js} +1 -1
@@ -0,0 +1,80 @@
1
+ # Sparkline
2
+
3
+ **Status:** beta
4
+
5
+ A word-sized trend with no axes — a line, a line over a soft fill, or columns — sized by its container, for stat cards, table cells and sentences. Draws on as it scrolls into view.
6
+
7
+ **Keywords:** sparkline, trend, mini chart, inline chart, micro chart, kpi, stat
8
+
9
+ ## Usage
10
+
11
+ Use Sparkline next to a number to show where it has been — a KPI's last 30 days in a stat card, a row's trend in a table. It has no axes, tooltip or values to read off; when the reader needs those, use AreaChart or LineChart.
12
+
13
+ ### Best practices
14
+
15
+ - Do: Pair it with the current value and its change in text; the sparkline shows only the shape.
16
+ - Do: Give it a label that names the measure and the period — the spoken summary adds the range and latest value.
17
+ - Don't: Put two sparklines on one shared axis to compare them — each scales to its own data; use LineChart.
18
+ - Don't: Use it as the only place a number appears; a shape without a value cannot be read off.
19
+
20
+ ## Props
21
+
22
+ | Name | Type | Default | Description |
23
+ | --- | --- | --- | --- |
24
+ | data | number[] | — | The values, oldest first. |
25
+ | label | string | — | What the line tracks ("Revenue, last 30 days"). The spoken summary adds the first and last values, low and high. |
26
+ | variant | "line" \| "area" \| "bar" | "line" | A line, a line over a soft fill, or columns. |
27
+ | tone | "edition" \| "collectible" \| "standard" \| "info" \| "warning" \| "primary" \| "neutral" \| "success" \| "destructive" | "primary" | Colour by role. |
28
+ | className | string | "h-8 w-24" | Sizes it; the drawing stretches to fit. |
29
+ | animate | boolean | true | Draw on (or rise) when it scrolls into view. |
30
+ | locale | string | runtime locale | BCP 47 locale for default number and date formatting. |
31
+
32
+ ## Variants
33
+
34
+ - **variant**: line, area, bar
35
+
36
+ ## Tokens used
37
+
38
+ `primary`, `tone-edition`, `tone-collectible`, `tone-standard`, `tone-info`, `tone-warning`, `muted-foreground`, `success`, `destructive`
39
+
40
+ ## Motion
41
+
42
+ - **arrival** · Line / bars · _custom (outside the recipe layer)_ — Once on screen the line draws on (or the bars rise, in sequence). On the motion tokens; reduced motion shows the finished drawing.
43
+ - **response** · Sparkline · _no motion_ — Not interactive: it has no tooltip or focus.
44
+ - **transition** · Sparkline · _no motion_ — New data redraws in place; a word-sized chart animating on every update would distract from the number beside it.
45
+ - **activity** · Sparkline · _no motion_ — No indeterminate state.
46
+
47
+ ## Accessibility
48
+
49
+ - Rendered role=img with an aria-label built from `label` and the data: "Revenue, last 30 days: from 120 to 184, low 96, high 190".
50
+ - It is not focusable and has no tooltip, so the number it accompanies must be in text beside it.
51
+
52
+ ## Related
53
+
54
+ - Components: AreaChart, LineChart, Card, Table
55
+
56
+ ## Examples
57
+
58
+ ### In a stat card
59
+
60
+ ```tsx
61
+ import { Card, CardAction, CardDescription, CardHeader, CardTitle, Sparkline } from "@sableui/react"
62
+
63
+ <Card>
64
+ <CardHeader>
65
+ <CardDescription>Revenue</CardDescription>
66
+ <CardTitle className="text-mono-xl">$18,240</CardTitle>
67
+ <CardAction>
68
+ <Sparkline label="Revenue, last 12 weeks" data={[12, 14, 13, 17, 16, 19, 18, 21, 20, 24, 23, 26]} variant="area" tone="success" />
69
+ </CardAction>
70
+ </CardHeader>
71
+ </Card>
72
+ ```
73
+
74
+ ### Columns in a table cell
75
+
76
+ ```tsx
77
+ import { Sparkline } from "@sableui/react"
78
+
79
+ <Sparkline label="Daily signups, last 14 days" data={[4, 6, 3, 8, 7, 9, 5, 6, 10, 8, 7, 11, 9, 12]} variant="bar" className="h-6 w-20" />
80
+ ```
@@ -17,7 +17,7 @@ Use Table for structured data with consistent columns and rows. Use DataTable wh
17
17
  - Do: Use the built-in horizontal frame for overflow instead of squeezing columns until content becomes unreadable.
18
18
  - Do: Put a table in a card with `<CardContent flush>`. The first and last columns then take the card's own padding (`--table-edge-px`), so they line up with the CardHeader at any density. Never hand-set `pl-6`/`pr-6` on edge cells.
19
19
  - Don't: Use Table for heterogeneous items with no stable columns. Use Card, Entity, or a list composition instead.
20
- - Don't: Put every possible action in every row. Use one primary row action, a DotsMenu, or row detail navigation.
20
+ - Don't: Put every possible action in every row. Use one primary row action, an OverflowMenu, or row detail navigation.
21
21
 
22
22
  ### Anatomy
23
23
 
@@ -99,10 +99,10 @@ export function AuthPage() {
99
99
  <Separator label="or" />
100
100
 
101
101
  <Stack gap={1.5}>
102
- <Button variant="outline" size="cta" className="w-full">
102
+ <Button variant="outline" size="cta" className="w-full" iconStart={<Icon name="key" />}>
103
103
  Sign in with a passkey
104
104
  </Button>
105
- <Button variant="outline" size="cta" className="w-full">
105
+ <Button variant="outline" size="cta" className="w-full" iconStart={<Icon name="wallet" />}>
106
106
  Connect a wallet
107
107
  </Button>
108
108
  </Stack>
@@ -13,7 +13,7 @@ App-shell creator dashboard: a branded Sidebar with an account footer (AppHeader
13
13
  - The table runs flush to the card edges with `<CardContent flush>`, which hands the card's padding to the table (`--table-edge-px`), so the first and last columns line up with the CardHeader at any card density — never hand-set pl-6/pr-6. Numeric columns align end in mono; Table scrolls horizontally inside the card on narrow screens.
14
14
  - Sold / supply is a `Meter` (how full a known range is), not `Progress` (task progress). Status is `Badge appearance="plain" dot`, with the app's own states mapped to tones once at the top of the file.
15
15
  - Lists are Entity rows in CardList, inset to the card padding and divided by hairlines — never CardContent with hand-tuned px. A card that shares a grid row with a taller one becomes `flex flex-col` and pins its CardFooter with `mt-auto`.
16
- - Brand and account live in the Sidebar: SidebarHeader holds the mark + wordmark, SidebarFooter holds the signed-in Entity with a DotsMenu. The footer rebinds `--foreground` to `--sidebar-foreground`, because Entity's title and the ghost trigger ink with page tokens and a theme may paint the rail dark on a light page; it keeps to a title line, since there is no sidebar-muted ink for a subtitle.
16
+ - Brand and account live in the Sidebar: SidebarHeader holds the mark + wordmark, SidebarFooter holds the signed-in Entity with an OverflowMenu. Sidebar rebinds the page ink tokens to its own, so the Entity and OverflowMenu read correctly even when a theme paints the rail dark on a light page.
17
17
  - The page has exactly one heading-1 (PageHeader); card titles are Title tier, not headings.
18
18
  - Chrome (Sidebar) is outside the content grid; the Page region caps only content.
19
19
  - Sidebar hides below lg, so the shell's header carries an AppHeader (brand + account) and its footer a BottomNav with the same destinations — never strand mobile users without navigation.
@@ -29,7 +29,7 @@ App-shell creator dashboard: a branded Sidebar with an account footer (AppHeader
29
29
  ## Source
30
30
 
31
31
  ```tsx
32
- import { AppHeader, AppShell, Avatar, Badge, BottomNav, BottomNavItem, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, DataTable, DotsMenu, Entity, Grid, Icon, MenuItem, MenuSeparator, Meter, NotificationBadge, Page, PageHeader, RelativeTime, Row, Sidebar, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav, Stack } from "@sableui/react"
32
+ import { AppHeader, AppShell, Avatar, Badge, BottomNav, BottomNavItem, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, DataTable, Entity, Grid, Icon, MenuItem, MenuSeparator, Meter, NotificationBadge, OverflowMenu, Page, PageHeader, RelativeTime, Row, Sidebar, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav, Stack } from "@sableui/react"
33
33
 
34
34
  // Edition status → Badge tone. Sable has no status vocabulary of its own;
35
35
  // an app maps ITS states onto tones once, here, and every badge follows.
@@ -143,20 +143,19 @@ export function DashboardPage() {
143
143
  />
144
144
  <SidebarItem icon={<Icon name="gear" />} label="Settings" render={<a href="/settings" />} />
145
145
  </SidebarNav>
146
- {/* Entity and DotsMenu ink with page tokens; a theme can paint the rail
147
- dark on a light page, so this subtree reads the rail's ink instead. */}
148
- <SidebarFooter className="[--foreground:var(--sidebar-foreground)]">
146
+ <SidebarFooter>
149
147
  <Entity
150
148
  className="[--entity-py:0]"
151
149
  leading={<Avatar size="sm" fallback="AL" alt="" />}
152
150
  title="Ada Lovelace"
151
+ subtitle="@ada"
153
152
  action={
154
- <DotsMenu label="Account" size="icon-sm">
153
+ <OverflowMenu label="Account" size="icon-sm">
155
154
  <MenuItem render={<a href="/@ada" />}>View profile</MenuItem>
156
155
  <MenuItem render={<a href="/settings" />}>Settings</MenuItem>
157
156
  <MenuSeparator />
158
157
  <MenuItem>Sign out</MenuItem>
159
- </DotsMenu>
158
+ </OverflowMenu>
160
159
  }
161
160
  />
162
161
  </SidebarFooter>
@@ -9,7 +9,7 @@ Social reading feed with a side rail: a feed-width main column (composer, For yo
9
9
  - The main column is capped at `--region-feed` (640px) inside a `wide` page, so posts keep a reading measure while the rail fills the desktop width. The rail is a fixed `w-80` aside, `sticky` at lg and `hidden` below it — its content is supplementary, so nothing is lost on mobile.
10
10
  - Feed scope is `Tabs` (underline, stretched to the column with `flex`): peer views of the same feed. The composer sits above them because posting applies to both.
11
11
  - The composer is a Card holding `Textarea autoResize` plus ghost icon Buttons and a secondary `Post` — not ChatComposer, which submits on Enter and is for conversations.
12
- - Each post is a Card: an Entity byline in a CardList (with a DotsMenu for post actions), body copy, then media in an AspectBox at the asset's own ratio, inset with `rounded-concentric-lg/6` so its corners follow the card's in every theme.
12
+ - Each post is a Card: an Entity byline in a CardList (with an OverflowMenu for post actions), body copy, then media in an AspectBox at the asset's own ratio, inset with `rounded-concentric-lg/6` so its corners follow the card's in every theme.
13
13
  - An edition drop adds a `Badge appearance="overlay"` on the artwork, title and price, the one primary Button on the page (`Collect`), and a `Meter` for supply — collected against a known maximum is a level, not task progress.
14
14
  - Actions are ghost sm Buttons (icon + count) in a Row pulled out by the button's own padding (`-ms-3`) so the first icon sits on the text edge.
15
15
  - Loading the next page shows a Skeleton post in the shape the post will land in, above the explicit load-more Button in its `loading` state — predictable for keyboard and AT users, unlike infinite scroll.
@@ -17,7 +17,7 @@ Social reading feed with a side rail: a feed-width main column (composer, For yo
17
17
  ## Accessibility
18
18
 
19
19
  - Each post is an <article>, so AT users can jump between posts; the rail is a labelled <aside> landmark.
20
- - Rail card titles carry role="heading" aria-level=2 under the page's one h1.
20
+ - Rail card titles render as h2 (CardTitle `render={<h2 />}`) under the page's one h1.
21
21
  - Icon-only buttons and every Follow / Remind button carry aria-labels naming their target; counts stay in the accessible name.
22
22
  - The supply Meter is role=meter with a getAriaValueText reading ("62 of 100 collected").
23
23
  - The Skeleton card is aria-busy; the loading Button keeps its label ("Loading posts").
@@ -25,7 +25,7 @@ Social reading feed with a side rail: a feed-width main column (composer, For yo
25
25
  ## Source
26
26
 
27
27
  ```tsx
28
- import { AspectBox, Avatar, Badge, Button, Card, CardContent, CardHeader, CardList, CardTitle, DotsMenu, Entity, Icon, MenuItem, MenuSeparator, Meter, MeterLabel, MeterTrack, MeterValue, Page, PageHeader, Row, Skeleton, Stack, Tabs, TabsList, TabsPanel, TabsTrigger, Textarea } from "@sableui/react"
28
+ import { AspectBox, Avatar, Badge, Button, Card, CardContent, CardHeader, CardList, CardTitle, Entity, Icon, MenuItem, MenuSeparator, Meter, MeterLabel, MeterTrack, MeterValue, OverflowMenu, Page, PageHeader, Row, Skeleton, Stack, Tabs, TabsList, TabsPanel, TabsTrigger, Textarea } from "@sableui/react"
29
29
 
30
30
  type Post = {
31
31
  id: string
@@ -99,12 +99,12 @@ const renderPost = (post: Post) => (
99
99
  title={post.author}
100
100
  subtitle={`${post.handle} · ${post.time}`}
101
101
  action={
102
- <DotsMenu label={`Post options for ${post.author}`} size="icon-sm">
102
+ <OverflowMenu label={`Post options for ${post.author}`} size="icon-sm">
103
103
  <MenuItem>Copy link</MenuItem>
104
104
  <MenuItem>Mute {post.handle}</MenuItem>
105
105
  <MenuSeparator />
106
106
  <MenuItem variant="destructive">Report post</MenuItem>
107
- </DotsMenu>
107
+ </OverflowMenu>
108
108
  }
109
109
  />
110
110
  </CardList>
@@ -237,7 +237,7 @@ export function FeedPage() {
237
237
  <Stack gap={3}>
238
238
  <Card>
239
239
  <CardHeader>
240
- <CardTitle role="heading" aria-level={2}>Who to follow</CardTitle>
240
+ <CardTitle render={<h2 />}>Who to follow</CardTitle>
241
241
  </CardHeader>
242
242
  <CardList>
243
243
  {suggestions.map((person) => (
@@ -257,7 +257,7 @@ export function FeedPage() {
257
257
  </Card>
258
258
  <Card>
259
259
  <CardHeader>
260
- <CardTitle role="heading" aria-level={2}>Upcoming drops</CardTitle>
260
+ <CardTitle render={<h2 />}>Upcoming drops</CardTitle>
261
261
  </CardHeader>
262
262
  <CardList>
263
263
  {upcoming.map((drop) => (
@@ -7,7 +7,7 @@ Searchable list beside the selected record's detail: interactive Entity rows in
7
7
  ## Notes
8
8
 
9
9
  - Region cap is `wide`; Columns 12 → Col 5 (list) + Col 7 (detail). Below lg the list and detail share one column as a drill-in: selecting a row shows the detail with an `All members` back button (lg:hidden), and the back button returns to the list. The panes swap with `max-lg:hidden`, so desktop always shows both.
10
- - Rows are interactive Entity inside one Card + CardList. Selection is `data-selected` on the row painted by `sable-surface-interactive`; `sable-surface` on the list card makes that step 12% ink off the card, so a selected row stays distinct from its muted avatar.
10
+ - Rows are interactive Entity inside one Card + CardList. Selection is Entity `selected`; `sable-surface` on the list card makes its selected step 12% ink off the card, so a selected row stays distinct from its muted avatar.
11
11
  - Search filters the list in place (name, email, role). No matches swap the CardList for an EmptyState with a Clear search action; the selected detail stays open.
12
12
  - A pending invite lives in the same list: a mail glyph in the Avatar, an `Invited` Badge, and an invite-specific detail (role on joining, sent, expires, invited by; Resend and Revoke actions).
13
13
  - The role Select edits the record in place; its Field description states what the role can do. The owner's role is read-only and the owner has no remove action.
@@ -17,14 +17,14 @@ Searchable list beside the selected record's detail: interactive Entity rows in
17
17
 
18
18
  ## Accessibility
19
19
 
20
- - Each row is a real button (Entity `interactive` + `onSelect`): Tab moves through rows, Enter/Space selects. The selected row carries visually hidden `(selected)` text in its name; list avatars are aria-hidden so initials are not read before the name.
20
+ - Each row is a real button (Entity `interactive` + `onSelect`): Tab moves through rows, Enter/Space selects. The selected row is Entity `selected`, which paints the selected fill and sets aria-current; list avatars are aria-hidden so initials are not read before the name.
21
21
  - The detail pane is a labelled region (`Member details`) whose title is a level-2 heading.
22
22
  - On a phone, drilling in moves focus to the back button; going back returns focus to the row that was open. After a remove, focus lands on the newly selected row; after Clear search, on the search field.
23
23
 
24
24
  ## Source
25
25
 
26
26
  ```tsx
27
- import { useState, useEffect } from "react"
27
+ import { useState, useEffect, useRef } from "react"
28
28
  import { Avatar, Badge, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, Col, Columns, DestructiveActionModal, EmptyState, Entity, Field, Icon, Page, PageHeader, Row, SearchInput, Select, SelectContent, SelectItem, SelectTrigger, Stack, StatusDot } from "@sableui/react"
29
29
 
30
30
  type Role = "owner" | "admin" | "editor" | "viewer"
@@ -208,11 +208,8 @@ export function ListDetailPage() {
208
208
  <Entity
209
209
  key={m.id}
210
210
  data-member-id={m.id}
211
- data-selected={isSelected ? "" : undefined}
212
- // No selected variant on Entity yet: the relative-surface recipe paints
213
- // data-selected one step off the card.
214
- className="sable-surface-interactive"
215
211
  interactive
212
+ selected={isSelected}
216
213
  onSelect={() => select(m.id)}
217
214
  leading={
218
215
  <Avatar
@@ -222,12 +219,7 @@ export function ListDetailPage() {
222
219
  aria-hidden
223
220
  />
224
221
  }
225
- title={
226
- <>
227
- {m.name}
228
- {isSelected ? <span className="sr-only"> (selected)</span> : null}
229
- </>
230
- }
222
+ title={m.name}
231
223
  subtitle={
232
224
  m.pending
233
225
  ? `${roleLabel[m.role]} · invite sent ${m.sent?.toLowerCase()}`
@@ -289,7 +281,7 @@ export function ListDetailPage() {
289
281
  alt={selected.pending ? "Pending invite" : selected.name}
290
282
  />
291
283
  <Stack gap={0} className="min-w-0">
292
- <CardTitle role="heading" aria-level={2} className="truncate">
284
+ <CardTitle render={<h2 />} className="truncate">
293
285
  {selected.name}
294
286
  </CardTitle>
295
287
  <CardDescription className="truncate">
@@ -378,7 +370,7 @@ export function ListDetailPage() {
378
370
  {selected.activity.length > 0 ? (
379
371
  <Card>
380
372
  <CardHeader>
381
- <CardTitle role="heading" aria-level={3}>
373
+ <CardTitle render={<h3 />}>
382
374
  Recent activity
383
375
  </CardTitle>
384
376
  </CardHeader>
@@ -28,7 +28,7 @@ Account settings inside the app shell: a sticky SideNav section rail beside a co
28
28
 
29
29
  ```tsx
30
30
  import { useState } from "react"
31
- import { AppHeader, AppShell, Avatar, BottomNav, BottomNavItem, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, Col, Columns, DestructiveActionModal, DotsMenu, Entity, Field, Form, Grid, Icon, Input, MenuItem, MenuSeparator, NotificationBadge, Page, PageHeader, Row, Select, SelectContent, SelectItem, SelectTrigger, Sidebar, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav, SideNav, SideNavItem, Stack, Switch, Textarea } from "@sableui/react"
31
+ import { AppHeader, AppShell, Avatar, BottomNav, BottomNavItem, Button, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardList, CardTitle, Col, Columns, DestructiveActionModal, Entity, Field, Form, Grid, Icon, Input, MenuItem, MenuSeparator, NotificationBadge, OverflowMenu, Page, PageHeader, Row, Select, SelectContent, SelectItem, SelectTrigger, Sidebar, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav, SideNav, SideNavItem, Stack, Switch, Textarea } from "@sableui/react"
32
32
 
33
33
  const sections = [
34
34
  { id: "profile", label: "Profile" },
@@ -107,20 +107,19 @@ export function SettingsPage() {
107
107
  />
108
108
  <SidebarItem icon={<Icon name="gear" />} label="Settings" active render={<a href="/settings" />} />
109
109
  </SidebarNav>
110
- {/* Entity and DotsMenu ink with page tokens; a theme can paint the rail
111
- dark on a light page, so this subtree reads the rail's ink instead. */}
112
- <SidebarFooter className="[--foreground:var(--sidebar-foreground)]">
110
+ <SidebarFooter>
113
111
  <Entity
114
112
  className="[--entity-py:0]"
115
113
  leading={<Avatar size="sm" fallback="AL" alt="" />}
116
114
  title="Ada Lovelace"
115
+ subtitle="@ada"
117
116
  action={
118
- <DotsMenu label="Account" size="icon-sm">
117
+ <OverflowMenu label="Account" size="icon-sm">
119
118
  <MenuItem render={<a href="/@ada" />}>View profile</MenuItem>
120
119
  <MenuItem render={<a href="/settings" />}>Settings</MenuItem>
121
120
  <MenuSeparator />
122
121
  <MenuItem>Sign out</MenuItem>
123
- </DotsMenu>
122
+ </OverflowMenu>
124
123
  }
125
124
  />
126
125
  </SidebarFooter>
@@ -152,9 +151,7 @@ export function SettingsPage() {
152
151
  <Stack gap={3}>
153
152
  <Card id="profile" className="scroll-mt-6">
154
153
  <CardHeader>
155
- <CardTitle>
156
- <h2>Profile</h2>
157
- </CardTitle>
154
+ <CardTitle render={<h2 />}>Profile</CardTitle>
158
155
  <CardDescription>How collectors see you on Atelier.</CardDescription>
159
156
  </CardHeader>
160
157
  {/* The one explicit save on the page: profile text is drafted, then
@@ -216,9 +213,7 @@ export function SettingsPage() {
216
213
 
217
214
  <Card id="notifications" className="scroll-mt-6">
218
215
  <CardHeader>
219
- <CardTitle>
220
- <h2>Notifications</h2>
221
- </CardTitle>
216
+ <CardTitle render={<h2 />}>Notifications</CardTitle>
222
217
  <CardDescription>
223
218
  Email updates about your editions. Changes save automatically.
224
219
  </CardDescription>
@@ -243,9 +238,7 @@ export function SettingsPage() {
243
238
 
244
239
  <Card id="payouts" className="scroll-mt-6">
245
240
  <CardHeader>
246
- <CardTitle>
247
- <h2>Payouts</h2>
248
- </CardTitle>
241
+ <CardTitle render={<h2 />}>Payouts</CardTitle>
249
242
  <CardDescription>Paid in USD every Friday to Chase checking ••4821.</CardDescription>
250
243
  <CardAction>
251
244
  <Button variant="outline" size="sm" render={<a href="/settings/payouts" />}>
@@ -268,9 +261,7 @@ export function SettingsPage() {
268
261
 
269
262
  <Card id="account" className="scroll-mt-6">
270
263
  <CardHeader>
271
- <CardTitle>
272
- <h2>Account</h2>
273
- </CardTitle>
264
+ <CardTitle render={<h2 />}>Account</CardTitle>
274
265
  <CardDescription>Sign-in details and your data.</CardDescription>
275
266
  </CardHeader>
276
267
  <CardList>
@@ -299,9 +290,7 @@ export function SettingsPage() {
299
290
 
300
291
  <Card>
301
292
  <CardHeader>
302
- <CardTitle>
303
- <h2>Delete account</h2>
304
- </CardTitle>
293
+ <CardTitle render={<h2 />}>Delete account</CardTitle>
305
294
  <CardDescription>
306
295
  Permanently remove your profile, editions, and sales history. Collectors keep
307
296
  the editions they own.
@@ -74,7 +74,10 @@ Entity rows in a Card) or `redesign` (desktop workspace tools).
74
74
  Keep the app's `IconProvider`.
75
75
  4. Frame it on a host page with `PhoneFrame` from `@sableui/react/ios/preview`
76
76
  and `@sableui/react/ios/frame.css` (Sable's Phone around an iframe at
77
- 402×874, scaled as a whole device). To follow a host's live look, call
77
+ 402×874, scaled as a whole device). `fit="contain"` with a height in
78
+ `className` keeps the whole device on screen. Shown on a real phone with no
79
+ frame, pass `systemChrome={false}` to `NativeApp`: the device draws its own
80
+ status bar and home indicator, and the safe areas come from it. To follow a host's live look, call
78
81
  `useIosPreviewBridge(frameRef, { mode })` on the host and
79
82
  `useIosPreviewTheme()` in the prototype.
80
83
  5. Verify light and dark, keyboard focus, reduced motion, back and edge swipe,
@@ -28,8 +28,8 @@ The question to ask is always **what the user is doing**, not what it looks like
28
28
  | The user is… | Use | Not |
29
29
  |---|---|---|
30
30
  | Choosing one action from a list under a button | `Menu` | `Select` (that picks a *value*) |
31
- | Reaching secondary actions for a row or card | `DotsMenu` | a bare `Menu` with a hand-made icon trigger |
32
- | Acting on the thing under the pointer (right-click or long press) | `ContextMenu`, always mirrored in a visible `DotsMenu` | `ContextMenu` alone: it is undiscoverable |
31
+ | Reaching secondary actions for a row or card | `OverflowMenu` | a bare `Menu` with a hand-made icon trigger |
32
+ | Acting on the thing under the pointer (right-click or long press) | `ContextMenu`, always mirrored in a visible `OverflowMenu` | `ContextMenu` alone: it is undiscoverable |
33
33
  | Changing a few settings that each show their current value | `SettingsMenu` | a `Menu` of toggles that hides each current state |
34
34
  | Running a main action that has close variations | a `Button` beside an icon `Button` that opens a `Menu` | a hand-fused split control |
35
35