@sableui/react 0.1.1 → 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 (137) hide show
  1. package/AGENTS.md +3 -1
  2. package/CHANGELOG.md +120 -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/{_contract-CYScOdf5.d.ts → _contract-o3tUc8Ig.d.ts} +1 -1
  8. package/dist/blocks/index.d.ts +396 -0
  9. package/dist/blocks/index.js +1997 -0
  10. package/dist/chunk-6LCIBMMG.js +4908 -0
  11. package/dist/{chunk-J23ELQQZ.js → chunk-CKNM7KSB.js} +1664 -4270
  12. package/dist/chunk-K6LII3OE.js +5093 -0
  13. package/dist/{chunk-RWP4XBSK.js → chunk-KFZ43RKU.js} +1 -1
  14. package/dist/{chunk-PAOZBZME.js → chunk-NGBDP3BM.js} +1 -1
  15. package/dist/{chunk-5Z5KLOAU.js → chunk-TH563WLW.js} +14 -1
  16. package/dist/{chunk-JI7WJP2L.js → chunk-X276MF2W.js} +5 -0
  17. package/dist/foundations/index.js +1 -1
  18. package/dist/icon/index.js +2 -2
  19. package/dist/icon-sets/reicon.js +1 -1
  20. package/dist/index.d.ts +624 -133
  21. package/dist/index.js +767 -4964
  22. package/dist/ios/index.d.ts +14 -2
  23. package/dist/ios/index.js +10 -9
  24. package/dist/ios/preview.d.ts +11 -1
  25. package/dist/ios/preview.js +14 -9
  26. package/dist/ios/styles.css +26 -6
  27. package/dist/{meta-6dUzgC6M.d.ts → meta-CcE_SeFu.d.ts} +37 -1
  28. package/dist/patterns/index.d.ts +1 -1
  29. package/dist/patterns/index.js +1684 -471
  30. package/dist/presets/index.d.ts +1 -1
  31. package/dist/presets/index.js +1 -1
  32. package/dist/styles.css +238 -40
  33. package/dist/{theme-provider-BWcizhAt.d.ts → theme-provider-BE-KqUG-.d.ts} +1 -1
  34. package/dist/themes/atelier/atelier.css +3 -0
  35. package/dist/themes/atelier/index.d.ts +1 -1
  36. package/dist/themes/base/base.css +34 -4
  37. package/dist/themes/base/index.d.ts +1 -1
  38. package/dist/themes/ember/ember.css +3 -0
  39. package/dist/themes/ember/index.d.ts +1 -1
  40. package/dist/themes/index.d.ts +2 -2
  41. package/dist/themes/index.js +2 -2
  42. package/dist/themes/meridian/index.d.ts +1 -1
  43. package/dist/themes/meridian/meridian.css +10 -5
  44. package/dist/themes/poppy/index.d.ts +1 -1
  45. package/dist/themes/poppy/poppy.css +16 -0
  46. package/dist/themes/prism/index.d.ts +1 -1
  47. package/dist/themes/prism/prism.css +4 -1
  48. package/dist/themes/verdant/index.d.ts +1 -1
  49. package/dist/themes/verdant/verdant.css +7 -2
  50. package/llms/blocks/api-keys-block.md +79 -0
  51. package/llms/blocks/chat-thread-block.md +51 -0
  52. package/llms/blocks/command-palette-block.md +48 -0
  53. package/llms/blocks/cta-band-block.md +62 -0
  54. package/llms/blocks/danger-zone-block.md +69 -0
  55. package/llms/blocks/device-comparison-block.md +91 -0
  56. package/llms/blocks/event-card-block.md +84 -0
  57. package/llms/blocks/faq-block.md +72 -0
  58. package/llms/blocks/feature-grid-block.md +75 -0
  59. package/llms/blocks/feedback-form-block.md +66 -0
  60. package/llms/blocks/goal-progress-block.md +54 -0
  61. package/llms/blocks/hero-block.md +65 -0
  62. package/llms/blocks/invite-team-block.md +61 -0
  63. package/llms/blocks/invoice-history-block.md +84 -0
  64. package/llms/blocks/kpi-sparkline-block.md +57 -0
  65. package/llms/blocks/media-player-block.md +100 -0
  66. package/llms/blocks/newsletter-block.md +64 -0
  67. package/llms/blocks/not-found-block.md +56 -0
  68. package/llms/blocks/notification-list-block.md +63 -0
  69. package/llms/blocks/notification-settings-block.md +54 -0
  70. package/llms/blocks/order-tracking-block.md +62 -0
  71. package/llms/blocks/pricing-card-block.md +82 -0
  72. package/llms/blocks/pricing-table-block.md +129 -0
  73. package/llms/blocks/product-card-block.md +69 -0
  74. package/llms/blocks/profile-card-block.md +103 -0
  75. package/llms/blocks/profile-form-block.md +114 -0
  76. package/llms/blocks/revenue-breakdown-block.md +70 -0
  77. package/llms/blocks/sign-in-block.md +64 -0
  78. package/llms/blocks/sign-up-block.md +78 -0
  79. package/llms/blocks/site-footer-block.md +96 -0
  80. package/llms/blocks/stat-row-block.md +59 -0
  81. package/llms/blocks/steps-block.md +73 -0
  82. package/llms/blocks/task-list-block.md +81 -0
  83. package/llms/blocks/team-members-block.md +91 -0
  84. package/llms/blocks/testimonial-block.md +78 -0
  85. package/llms/blocks/traffic-sources-block.md +63 -0
  86. package/llms/blocks/traffic-trend-block.md +95 -0
  87. package/llms/blocks/upload-list-block.md +66 -0
  88. package/llms/blocks/verify-code-block.md +59 -0
  89. package/llms/blocks/wallet-block.md +84 -0
  90. package/llms/components/area-chart.md +112 -0
  91. package/llms/components/bar-chart.md +131 -0
  92. package/llms/components/bubble.md +5 -5
  93. package/llms/components/button.md +1 -1
  94. package/llms/components/card.md +2 -2
  95. package/llms/components/checkbox-card.md +1 -1
  96. package/llms/components/context-card.md +2 -2
  97. package/llms/components/context-menu.md +1 -1
  98. package/llms/components/date-tile.md +79 -0
  99. package/llms/components/dialog.md +1 -1
  100. package/llms/components/empty-state.md +4 -4
  101. package/llms/components/entity.md +5 -3
  102. package/llms/components/fieldset.md +1 -1
  103. package/llms/components/kbd.md +18 -6
  104. package/llms/components/line-chart.md +101 -0
  105. package/llms/components/menu.md +1 -1
  106. package/llms/components/{dots-menu.md → overflow-menu.md} +9 -9
  107. package/llms/components/pie-chart.md +103 -0
  108. package/llms/components/pill.md +1 -1
  109. package/llms/components/radar-chart.md +106 -0
  110. package/llms/components/radial-chart.md +102 -0
  111. package/llms/components/segmented-bar.md +102 -0
  112. package/llms/components/settings-menu.md +2 -2
  113. package/llms/components/side-nav.md +1 -1
  114. package/llms/components/skeleton.md +1 -1
  115. package/llms/components/sparkline.md +80 -0
  116. package/llms/components/table.md +1 -1
  117. package/llms/components/theme-selector.md +1 -1
  118. package/llms/foundations/shape.md +1 -1
  119. package/llms/patterns/auth-page.md +132 -42
  120. package/llms/patterns/brand-landing-page.md +271 -175
  121. package/llms/patterns/chat-panel.md +120 -39
  122. package/llms/patterns/dashboard-page.md +276 -65
  123. package/llms/patterns/feed-page.md +245 -64
  124. package/llms/patterns/landing-page.md +473 -75
  125. package/llms/patterns/list-detail-page.md +377 -68
  126. package/llms/patterns/settings-page.md +268 -67
  127. package/llms/reference/token-intent.md +4 -2
  128. package/llms/reference/utilities.md +20 -5
  129. package/llms/tasks/building-an-ios-prototype.md +4 -1
  130. package/llms/tasks/choosing-a-component.md +2 -2
  131. package/llms-full.txt +6428 -892
  132. package/llms.txt +67 -16
  133. package/package.json +5 -1
  134. package/registry.json +3217 -552
  135. package/targets/ios.json +88 -18
  136. package/llms/components/description.md +0 -96
  137. package/dist/{chunk-L6Y46C62.js → chunk-MXJX3NZI.js} +1 -1
@@ -1,36 +1,83 @@
1
1
  # SettingsPage
2
2
 
3
- Stacked settings sections inside the app shell: a content-width region, one Fieldset card per concern (each with its own save), auto-wired Form/Field rows, and a destructive zone last.
3
+ Account settings inside the app shell: a sticky SideNav section rail beside a column of section Cards — a Profile form with the page's one explicit save, Card + CardList + Entity rows with Switches that apply instantly, a read-only payouts summary, account rows, and a delete card confirmed through DestructiveActionModal.
4
4
 
5
5
  **Intent:** Reach for this for account/profile/workspace settings and any 'edit a handful of grouped preferences' route. Not for a single short form (use AuthPage's centered form shape) or bulk record editing (use DashboardPage + DataTable).
6
6
 
7
7
  ## Notes
8
8
 
9
- - Region cap is `content` (896px): settings panes hold labeled rows and the occasional 2-up field, never a full grid.
10
- - One Fieldset per concern, each with its own footer save — per-section saves beat one page-wide submit for settings.
11
- - Field wires label/description/error a11y automatically; never hand-roll <label> + <input> pairs here.
12
- - The destructive section is visually last, isolated in its own Fieldset with a destructive-variant action.
13
- - Toggles use orientation="horizontal" Fields so the label sits beside the Switch.
9
+ - Region cap is `wide`, split on a 12-column track: a 3-column SideNav rail and a 9-column section column. The rail fills the space a lone content-width column would leave empty and gives the page its table of contents.
10
+ - The rail is `SideNav` (the documented settings sub-nav), sticky, with in-page anchor items; it hides below lg, where the sections simply stack and the shell's BottomNav carries app navigation.
11
+ - One save model, stated in the copy: drafted text (the Profile form) commits through its single footer save; toggles apply the moment they change, and the Notifications description says so. Never pair a Switch with a Save button.
12
+ - Settings rows are Card + CardList + Entity with the Switch in `action` — the row owns its title and one-line description; keep descriptions short, because Entity truncates its subtitle.
13
+ - Each section is a Card whose CardTitle holds an h2, so the page has a real heading outline under PageHeader's h1; Cards carry the anchor id and `scroll-mt-*` so anchor jumps clear the top edge.
14
+ - Field wires label/description/error a11y automatically; never hand-roll <label> + <input> pairs. Pair fields side by side with Grid cols={{ base: 1, sm: 2 }}.
15
+ - The destructive action is last and isolated in its own Card, and it never acts directly: it opens DestructiveActionModal with type-to-confirm on the handle.
14
16
  - Lives in the same AppShell as DashboardPage: Sidebar from lg up, AppHeader + BottomNav below it, with Settings marked active in both.
15
17
 
16
18
  ## Accessibility
17
19
 
18
20
  - AppShell provides the skip link and <main id="main-content"> landmark; the active SidebarItem and BottomNavItem carry aria-current.
19
- - Each section is a real <fieldset> with a <legend> (FieldsetLegend).
21
+ - The section rail is a labelled <nav> ("Settings sections") distinct from the primary navigation; its active item carries aria-current.
22
+ - Every section has an h2, so screen-reader heading navigation mirrors the rail.
23
+ - Each Switch takes its row title as aria-label, since the Entity title is not a <label>.
20
24
  - Field auto-wires htmlFor/id, aria-describedby (description + error), aria-invalid, and required.
21
- - The danger action is a real button labeled by its visible text — no icon-only destructive controls.
25
+ - DestructiveActionModal is an alertdialog: focus opens on Cancel, the backdrop does not dismiss, and the confirm stays disabled until the handle is typed.
22
26
 
23
27
  ## Source
24
28
 
25
29
  ```tsx
26
- import { AppHeader, AppShell, BottomNav, BottomNavItem, Button, Field, Fieldset, FieldsetContent, FieldsetDescription, FieldsetFooter, FieldsetLegend, Form, Icon, Input, Page, PageHeader, Select, SelectContent, SelectItem, SelectTrigger, Sidebar, SidebarHeader, SidebarItem, SidebarNav, Switch, Textarea } from "@sableui/react"
30
+ import { useState } from "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
+
33
+ const sections = [
34
+ { id: "profile", label: "Profile" },
35
+ { id: "notifications", label: "Notifications" },
36
+ { id: "payouts", label: "Payouts" },
37
+ { id: "account", label: "Account" },
38
+ ]
39
+
40
+ const timezones = [
41
+ { value: "europe-london", label: "London (GMT+1)" },
42
+ { value: "europe-paris", label: "Paris (GMT+2)" },
43
+ { value: "america-new-york", label: "New York (GMT−4)" },
44
+ { value: "america-los-angeles", label: "Los Angeles (GMT−7)" },
45
+ { value: "asia-tokyo", label: "Tokyo (GMT+9)" },
46
+ ]
47
+
48
+ const notifications = [
49
+ { name: "notifyCollects", title: "New collects", description: "When someone collects your work", on: true },
50
+ { name: "notifySoldOut", title: "Sold out", description: "When an edition sells its last copy", on: true },
51
+ { name: "notifyFollowers", title: "New followers", description: "When a collector follows you", on: false },
52
+ { name: "notifyDigest", title: "Weekly digest", description: "Sales and collectors, every Monday", on: true },
53
+ { name: "notifyProduct", title: "Product updates", description: "New Atelier features", on: false },
54
+ ]
55
+
56
+ const payouts = [
57
+ { label: "Next payout", value: "$1,284.60", note: "Friday, Oct 16" },
58
+ { label: "Pending", value: "$412.00", note: "Clears in 7 days" },
59
+ { label: "Paid this year", value: "$18,920.00", note: "Across 214 collects" },
60
+ ]
27
61
 
28
62
  export function SettingsPage() {
63
+ const [activeSection, setActiveSection] = useState("profile")
64
+ const [deleteOpen, setDeleteOpen] = useState(false)
65
+
29
66
  return (
30
67
  <AppShell
31
68
  // Sidebar hides below lg, so the same destinations ride in the shell's
32
69
  // mobile slots — without them the route has no navigation under 1024px.
33
- header={<AppHeader title={<span className="text-title-sm">Atelier</span>} />}
70
+ header={
71
+ <AppHeader
72
+ title={
73
+ <Row gap={1} align="center">
74
+ <Icon name="gem" variant="solid" className="text-xl" />
75
+ <span className="text-title-sm">Atelier</span>
76
+ </Row>
77
+ }
78
+ actions={<Avatar size="sm" fallback="AL" alt="Ada Lovelace" />}
79
+ />
80
+ }
34
81
  bottomNav={
35
82
  <BottomNav aria-label="Primary">
36
83
  <BottomNavItem icon={<Icon name="grid" />} label="Overview" showLabel render={<a href="/" />} />
@@ -43,78 +90,232 @@ export function SettingsPage() {
43
90
  sidebar={
44
91
  <Sidebar>
45
92
  <SidebarHeader>
46
- <span className="text-title-sm">Atelier</span>
93
+ <Row gap={1} align="center" className="px-3">
94
+ <Icon name="gem" variant="solid" className="text-xl" />
95
+ <span className="text-title-sm">Atelier</span>
96
+ </Row>
47
97
  </SidebarHeader>
48
98
  <SidebarNav>
49
99
  <SidebarItem icon={<Icon name="grid" />} label="Overview" render={<a href="/" />} />
50
100
  <SidebarItem icon={<Icon name="image-stack" />} label="Editions" render={<a href="/editions" />} />
51
101
  <SidebarItem icon={<Icon name="user" />} label="Collectors" render={<a href="/collectors" />} />
52
- <SidebarItem icon={<Icon name="bell" />} label="Activity" render={<a href="/activity" />} />
102
+ <SidebarItem
103
+ icon={<Icon name="bell" />}
104
+ label="Activity"
105
+ badge={<NotificationBadge count={4} tone="neutral" />}
106
+ render={<a href="/activity" />}
107
+ />
53
108
  <SidebarItem icon={<Icon name="gear" />} label="Settings" active render={<a href="/settings" />} />
54
109
  </SidebarNav>
110
+ <SidebarFooter>
111
+ <Entity
112
+ className="[--entity-py:0]"
113
+ leading={<Avatar size="sm" fallback="AL" alt="" />}
114
+ title="Ada Lovelace"
115
+ subtitle="@ada"
116
+ action={
117
+ <OverflowMenu label="Account" size="icon-sm">
118
+ <MenuItem render={<a href="/@ada" />}>View profile</MenuItem>
119
+ <MenuItem render={<a href="/settings" />}>Settings</MenuItem>
120
+ <MenuSeparator />
121
+ <MenuItem>Sign out</MenuItem>
122
+ </OverflowMenu>
123
+ }
124
+ />
125
+ </SidebarFooter>
55
126
  </Sidebar>
56
127
  }
57
128
  >
58
129
  {/* No `as="main"`: AppShell already renders <main id="main-content">. */}
59
- <Page max="content" gap={4} className="py-6">
130
+ <Page max="wide" gap={4} className="py-6">
60
131
  <PageHeader
61
132
  title="Settings"
62
- description="Your profile, notifications, and account controls."
133
+ description="Manage your profile, notifications, and payouts."
63
134
  />
64
- <Form onSubmit={(event) => event.preventDefault()}>
65
- <Fieldset>
66
- <FieldsetLegend>Profile</FieldsetLegend>
67
- <FieldsetDescription>How you appear across the app.</FieldsetDescription>
68
- <FieldsetContent>
69
- <Field label="Display name" required>
70
- <Input name="displayName" defaultValue="ada" />
71
- </Field>
72
- <Field label="Bio" description="Shown on your public profile.">
73
- <Textarea name="bio" rows={3} placeholder="Tell collectors who you are" />
74
- </Field>
75
- <Field label="Timezone">
76
- <Select defaultValue="utc" name="timezone">
77
- <SelectTrigger />
78
- <SelectContent>
79
- <SelectItem value="utc">UTC</SelectItem>
80
- <SelectItem value="est">Eastern (UTC−5)</SelectItem>
81
- <SelectItem value="pst">Pacific (UTC−8)</SelectItem>
82
- </SelectContent>
83
- </Select>
84
- </Field>
85
- </FieldsetContent>
86
- <FieldsetFooter>
87
- <Button type="submit">Save profile</Button>
88
- </FieldsetFooter>
89
- </Fieldset>
90
- </Form>
91
- <Form onSubmit={(event) => event.preventDefault()}>
92
- <Fieldset>
93
- <FieldsetLegend>Notifications</FieldsetLegend>
94
- <FieldsetDescription>What we email you about.</FieldsetDescription>
95
- <FieldsetContent>
96
- <Field label="New collector alerts" orientation="horizontal">
97
- <Switch name="collectorAlerts" defaultChecked />
98
- </Field>
99
- <Field label="Weekly digest" orientation="horizontal">
100
- <Switch name="weeklyDigest" />
101
- </Field>
102
- </FieldsetContent>
103
- <FieldsetFooter>
104
- <Button type="submit">Save notifications</Button>
105
- </FieldsetFooter>
106
- </Fieldset>
107
- </Form>
108
- <Fieldset>
109
- <FieldsetLegend>Danger zone</FieldsetLegend>
110
- <FieldsetDescription>
111
- Deleting your account removes your posts and collections permanently.
112
- </FieldsetDescription>
113
- <FieldsetFooter>
114
- <Button variant="destructive">Delete account</Button>
115
- </FieldsetFooter>
116
- </Fieldset>
135
+ <Columns gap={4}>
136
+ {/* The section rail is a desktop aid; on mobile the sections simply stack. */}
137
+ <Col span={{ base: 12, lg: 3 }} className="hidden lg:block">
138
+ <SideNav aria-label="Settings sections" className="sticky top-6">
139
+ {sections.map((section) => (
140
+ <SideNavItem
141
+ key={section.id}
142
+ label={section.label}
143
+ active={activeSection === section.id}
144
+ onClick={() => setActiveSection(section.id)}
145
+ render={<a href={`#${section.id}`} />}
146
+ />
147
+ ))}
148
+ </SideNav>
149
+ </Col>
150
+ <Col span={{ base: 12, lg: 9 }}>
151
+ <Stack gap={3}>
152
+ <Card id="profile" className="scroll-mt-6">
153
+ <CardHeader>
154
+ <CardTitle render={<h2 />}>Profile</CardTitle>
155
+ <CardDescription>How collectors see you on Atelier.</CardDescription>
156
+ </CardHeader>
157
+ {/* The one explicit save on the page: profile text is drafted, then
158
+ committed. Toggles below apply the moment they change. */}
159
+ <Form onSubmit={(event) => event.preventDefault()} className="gap-0">
160
+ <CardContent>
161
+ <Stack gap={3}>
162
+ <Row gap={2}>
163
+ <Avatar size="lg" fallback="AL" alt="Ada Lovelace" />
164
+ <Stack gap={0.5} align="start">
165
+ <Row gap={1}>
166
+ <Button type="button" variant="outline" size="sm">
167
+ Upload photo
168
+ </Button>
169
+ <Button type="button" variant="ghost" size="sm">
170
+ Remove
171
+ </Button>
172
+ </Row>
173
+ <p className="text-caption text-muted-foreground">
174
+ Square PNG or JPG, at least 400 px.
175
+ </p>
176
+ </Stack>
177
+ </Row>
178
+ <Grid cols={{ base: 1, sm: 2 }} gap={3}>
179
+ <Field label="Display name" name="displayName" required>
180
+ <Input defaultValue="Ada Lovelace" autoComplete="name" />
181
+ </Field>
182
+ <Field label="Handle" name="handle" description="atelier.com/@ada" required>
183
+ <Input defaultValue="ada" autoComplete="username" spellCheck={false} />
184
+ </Field>
185
+ </Grid>
186
+ <Field label="Bio" name="bio" description="Shown on your profile and edition pages.">
187
+ <Textarea
188
+ rows={3}
189
+ defaultValue="Generative artist working in plotted ink and code. New editions of 25 to 100 every month."
190
+ />
191
+ </Field>
192
+ <Grid cols={{ base: 1, sm: 2 }} gap={3}>
193
+ <Field label="Timezone" name="timezone" description="Release times use this zone.">
194
+ <Select defaultValue="europe-london" items={timezones}>
195
+ <SelectTrigger />
196
+ <SelectContent>
197
+ {timezones.map((zone) => (
198
+ <SelectItem key={zone.value} value={zone.value}>
199
+ {zone.label}
200
+ </SelectItem>
201
+ ))}
202
+ </SelectContent>
203
+ </Select>
204
+ </Field>
205
+ </Grid>
206
+ </Stack>
207
+ </CardContent>
208
+ <CardFooter className="justify-end">
209
+ <Button type="submit">Save profile</Button>
210
+ </CardFooter>
211
+ </Form>
212
+ </Card>
213
+
214
+ <Card id="notifications" className="scroll-mt-6">
215
+ <CardHeader>
216
+ <CardTitle render={<h2 />}>Notifications</CardTitle>
217
+ <CardDescription>
218
+ Email updates about your editions. Changes save automatically.
219
+ </CardDescription>
220
+ </CardHeader>
221
+ <CardList>
222
+ {notifications.map((item) => (
223
+ <Entity
224
+ key={item.name}
225
+ title={item.title}
226
+ subtitle={item.description}
227
+ action={
228
+ <Switch
229
+ name={item.name}
230
+ defaultChecked={item.on}
231
+ aria-label={item.title}
232
+ />
233
+ }
234
+ />
235
+ ))}
236
+ </CardList>
237
+ </Card>
238
+
239
+ <Card id="payouts" className="scroll-mt-6">
240
+ <CardHeader>
241
+ <CardTitle render={<h2 />}>Payouts</CardTitle>
242
+ <CardDescription>Paid in USD every Friday to Chase checking ••4821.</CardDescription>
243
+ <CardAction>
244
+ <Button variant="outline" size="sm" render={<a href="/settings/payouts" />}>
245
+ Manage payouts
246
+ </Button>
247
+ </CardAction>
248
+ </CardHeader>
249
+ <CardContent>
250
+ <Grid cols={{ base: 1, sm: 3 }} gap={3}>
251
+ {payouts.map((stat) => (
252
+ <Stack key={stat.label} gap={0.5}>
253
+ <p className="text-body-sm text-muted-foreground">{stat.label}</p>
254
+ <p className="text-mono-lg text-foreground">{stat.value}</p>
255
+ <p className="text-caption text-muted-foreground">{stat.note}</p>
256
+ </Stack>
257
+ ))}
258
+ </Grid>
259
+ </CardContent>
260
+ </Card>
261
+
262
+ <Card id="account" className="scroll-mt-6">
263
+ <CardHeader>
264
+ <CardTitle render={<h2 />}>Account</CardTitle>
265
+ <CardDescription>Sign-in details and your data.</CardDescription>
266
+ </CardHeader>
267
+ <CardList>
268
+ <Entity
269
+ leading={<Icon name="mail" className="text-lg text-muted-foreground" />}
270
+ title="Email"
271
+ subtitle="ada@atelier.art"
272
+ action={
273
+ <Button variant="outline" size="sm">
274
+ Change
275
+ </Button>
276
+ }
277
+ />
278
+ <Entity
279
+ leading={<Icon name="download" className="text-lg text-muted-foreground" />}
280
+ title="Export data"
281
+ subtitle="Editions, sales, and collectors"
282
+ action={
283
+ <Button variant="outline" size="sm">
284
+ Export
285
+ </Button>
286
+ }
287
+ />
288
+ </CardList>
289
+ </Card>
290
+
291
+ <Card>
292
+ <CardHeader>
293
+ <CardTitle render={<h2 />}>Delete account</CardTitle>
294
+ <CardDescription>
295
+ Permanently remove your profile, editions, and sales history. Collectors keep
296
+ the editions they own.
297
+ </CardDescription>
298
+ </CardHeader>
299
+ <CardFooter className="justify-end">
300
+ <Button variant="destructive" onClick={() => setDeleteOpen(true)}>
301
+ Delete account
302
+ </Button>
303
+ </CardFooter>
304
+ </Card>
305
+ </Stack>
306
+ </Col>
307
+ </Columns>
117
308
  </Page>
309
+ <DestructiveActionModal
310
+ open={deleteOpen}
311
+ onOpenChange={setDeleteOpen}
312
+ title="Delete your account?"
313
+ description="This permanently removes your profile, 12 editions, and sales history. Pending payouts are still sent. This can't be undone."
314
+ confirmLabel="Delete account"
315
+ confirmingLabel="Deleting…"
316
+ confirmText="ada"
317
+ onConfirm={() => setDeleteOpen(false)}
318
+ />
118
319
  </AppShell>
119
320
  )
120
321
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  Which token to reach for, and what it is most often confused with. Generated from `src/themes/token-docs.ts`; the full data is `registry.json → tokens`.
4
4
 
5
- ### semantic (44)
5
+ ### semantic (45)
6
6
 
7
7
  - **`background`** — The page. The furthest-back surface in the page register. *Use for:* The document body, and any surface meant to read as 'the page itself'. Never anything that floats: background is the BOTTOM of the surface stack (background < card < popover in every dark mode), so a floating panel painted with it reads as a hole beneath the cards it hovers over.
8
8
  - **Not** `popover` for anything floating, `card` for a bounded block on the page. `canvas`. `canvas` is the *work* surface in a tool — the artboard being edited — which is not the page behind the UI. Using `background` for an artboard makes the thing being edited indistinguishable from the chrome around it.
@@ -24,8 +24,10 @@ Which token to reach for, and what it is most often confused with. Generated fro
24
24
  - **Not** `border-strong`. Reach for `border-strong` when the line carries *structure* (a grid rule, a table divider, a section separator) and for `border` when it merely bounds a control.
25
25
  - **`border-strong`** — A structural divider. Visibly stronger than `border`. *Use for:* Grid lines, table rules, separators between major sections.
26
26
  - **Not** `border`. If removing the line would make the layout ambiguous, it is structural and belongs here.
27
- - **`input`** — The outline of a form control specifically. *Use for:* Text inputs, textareas, selects, and the resting edge of a checkbox or radio.
27
+ - **`input`** — The outline of a form control specifically. *Use for:* Text inputs, textareas and selects. Not the edge of a checkbox, radio or switch — that is `control-border`.
28
28
  - **Not** `border`. They are often the same value, but a theme is entitled to make form controls read differently from other chrome, and only this token gives it the hook.
29
+ - **`control-border`** — The resting edge of a control whose outline is the whole control. Held to 3:1 (WCAG 1.4.11) on every surface. *Use for:* An unchecked checkbox, an unselected radio, an off switch track, a selection card's check or radio mark — anywhere removing the edge would make the control disappear.
30
+ - **Not** `input`. A text field is also found by its fill, caret and placeholder, so its outline may stay quiet; a checkbox has nothing else, and at `input`'s strength it measures about 1.4:1.
29
31
  - **`ring`** — The focus indicator colour. *Use for:* Only for `focus-visible`. Nothing else.
30
32
  - **Not** `primary`. Focus must remain distinguishable from selection; if the ring is the same colour as a selected state, a keyboard user cannot tell where focus is.
31
33
  - **`primary`** — The one high-emphasis action colour. *Use for:* The single most important action on a surface — one per view. A submit button, the primary CTA.
@@ -26,7 +26,7 @@ The `@utility` and `@custom-variant` layer in `styles.css`: what each one is, wh
26
26
  - **Instead of** `bg-muted`, which is fixed: inside a popover it can sit within a step of the panel in some dark modes. And never a hand-mixed `color-mix(… var(--foreground) …)` or an outline on every well to compensate — that turns a panel into a stack of boxes. For a segmented control itself, use `ToggleGroup`; the well is for what no component draws.
27
27
  - `<div className="sable-well rounded-md p-1">…</div>`
28
28
 
29
- ### frame (6)
29
+ ### frame (7)
30
30
 
31
31
  - **`sable-frame`** — A bordered, padded, rounded container that computes the radius its children need: `--sable-frame-inner-radius` = radius − padding − border width, floored at `radius-sm`. *Use for:* A container whose children have their own rounded backgrounds (rows, tiles, a nested preview), so the corners nest concentrically and keep doing so when the radius ladder is rescaled. Defaults: `radius-lg`, `--spacing(4)` padding (1rem at the default scale, and it follows the density control), a 1px edge from the surface (`--sable-surface-edge`, else `border`). Size it with `sable-frame-r-*` / `sable-frame-p-*`; children take `sable-frame-inner`.
32
32
  - **Instead of** `rounded-lg p-4 border` on the parent plus a guessed `rounded-sm` on each child — the guess is concentric only at the default ladder. For a one-off nested corner without a frame, `rounded-concentric-*`.
@@ -34,6 +34,9 @@ The `@utility` and `@custom-variant` layer in `styles.css`: what each one is, wh
34
34
  - **`sable-frame-popover`** — The popup frame recipe: `radius-md`, `--spacing(1)` padding, the `border-floating` edge and `shadow-lg`, applied over `sable-frame`. *Use for:* A hand-composed floating list or panel that must match Sable's menus (with `sable-frame sable-surface sable-surface-popover`); rows inside take `sable-frame-inner`.
35
35
  - **Instead of** Copying a Menu's class string. And use `Menu`, `Select`, `Popover` or `Combobox` first — they own focus and dismissal; this is only for floating chrome no component covers.
36
36
  - `<div className="sable-frame sable-surface sable-surface-popover sable-frame-popover">…</div>`
37
+ - **`sable-frame-tool`** — The frame for edge-to-edge tool chrome at the theme's radius, with no padding. It publishes concentric geometry for what's inside: `--panel-inset` (row side padding, grown so rows clear the corner), `--panel-field-radius` (compact fields curve parallel to the frame) and `--panel-edge` (the first and last section's extra top and bottom). PanelSection and the xs field shell read them. Applied over `sable-frame`. *Use for:* Framing PanelSections, ScrubFields, a Tree, a StatusBar or a PanelGroup outside a Workspace (a docs demo, a floating inspector). Add `overflow-hidden` to clip to the corners.
38
+ - **Instead of** `rounded-lg border overflow-hidden` by hand: the panel keeps its square-column 8px inset and small field radius, so a rounded preset crowds the first label and last field into the curve.
39
+ - `<div className="sable-frame sable-frame-tool overflow-hidden bg-panel" data-density="compact"><PanelSection title="Layer">…</PanelSection></div>`
37
40
  - **`sable-frame-borderless`** — Sets a `sable-frame`'s border width to 0 while keeping the inner-radius math correct. *Use for:* A frame that separates by fill or space rather than an edge (a well, a tinted group) but still needs concentric children.
38
41
  - **Instead of** `border-0` on a `sable-frame`, which removes the border but leaves 1px in the inner-radius calculation.
39
42
  - `<div className="sable-frame sable-frame-borderless sable-well sable-frame-r-md sable-frame-p-1">…</div>`
@@ -49,14 +52,14 @@ The `@utility` and `@custom-variant` layer in `styles.css`: what each one is, wh
49
52
 
50
53
  ### geometry (4)
51
54
 
52
- - **`rounded-concentric-*`** — A border radius computed as an outer radius minus an inset: `rounded-concentric-<radius>/<spacing>`, clamped at zero. `rounded-concentric-md/1` = `radius-md` − `spacing-1`. *Use for:* A one-off rounded element nested inside another — a tab trigger inside a tab list, a thumb inside a track, a row inside a well — where a `sable-frame` would be overkill. The outer radius and the inset are the parent's.
55
+ - **`rounded-concentric-*`** — A border radius computed as an outer radius minus an inset: `rounded-concentric-<radius>/<spacing>`, floored at `radius-xs` so a deep inset never leaves a 1-2px sliver. `rounded-concentric-md/1` = `radius-md` − `spacing-1`. *Use for:* A one-off rounded element nested inside another — a tab trigger inside a tab list, a thumb inside a track, a row inside a well — where a `sable-frame` would be overkill. The outer radius and the inset are the parent's.
53
56
  - **Instead of** Picking the next ladder step down by eye (`rounded-sm` inside `rounded-md p-1`). That is concentric only at the default ladder; the utility recomputes from live tokens. Registered in `cn()` as a radius, so it correctly overrides a component's own `rounded-*`.
54
57
  - `<div className="sable-well rounded-md p-1"><button className="rounded-concentric-md/1 px-2 py-1">…</button></div>`
55
58
  - **`popup-inset`** — The padding of a popup list, derived from the popup's radius: a third of `radius-md`, at least `--spacing(1)`. A rounder popup gets more room, and the floor is on the spacing scale, so the density control moves it. *Use for:* On a hand-composed rounded-md popup that holds a list of rows. Sable's Menu, Select, Combobox and ContextMenu already use it.
56
59
  - **Instead of** `p-1`. A fixed inset at a larger radius preset leaves the rows' corners approaching half their height, so every row turns into a pill.
57
60
  - `<div className="rounded-md bg-popover popup-inset shadow-md">…</div>`
58
61
  - **`rounded-popup-item`** — The row radius that nests inside a `popup-inset` popup: `radius-md` minus the popup inset, clamped at zero. *Use for:* On every row inside a `popup-inset` container.
59
- - **Instead of** `rounded-sm` on menu rows, which matches only the default ladder (12px popup, 4px inset, 8px rows).
62
+ - **Instead of** `rounded-sm` on menu rows, which matches no ladder exactly (a 12px popup with a 4px inset and 1px border wants 7px rows).
60
63
  - `<div role="option" className="rounded-popup-item px-2 py-1.5 sable-highlighted:bg-accent">…</div>`
61
64
  - **`popup-bleed`** — A negative inline margin of exactly one `popup-inset`, so a separator inside a padded popup runs edge to edge at every radius and density. *Use for:* On a hand-composed separator (a 1px rule) inside a `popup-inset` popup. Sable's MenuSeparator, ContextMenuSeparator and SelectSeparator already use it.
62
65
  - **Instead of** `-mx-1`, which matches the inset only at the default ladder and stops short of both edges once a rounder preset or a roomier density grows it. design-lint flags it (`popup-separator-bleed`).
@@ -89,8 +92,20 @@ The `@utility` and `@custom-variant` layer in `styles.css`: what each one is, wh
89
92
  - **Instead of** `scrollbar-quiet` on a vertical list. A vertical region with no bar and no fade tells a reader nothing about how far through they are.
90
93
  - `<div className="flex gap-3 overflow-x-auto scrollbar-hidden">…</div>`
91
94
 
92
- ### text (8)
93
-
95
+ ### text (12)
96
+
97
+ - **`sable-list`** — A bulleted list as typography: body type, a hanging muted marker, and item rhythm. Put it on a native `<ul>`. *Use for:* Running content with bullets: release notes, a feature list, steps that are not ordered.
98
+ - **Instead of** A stack of `<div>`s with a hand-drawn dot, or an unstyled `<ul>` that inherits the reset (no markers, no indent).
99
+ - `<ul className="sable-list"><li>Faster sync</li><li>Offline drafts</li></ul>`
100
+ - **`sable-list-numbered`** — A numbered list as typography: `sable-list` with decimal markers in tabular figures. Put it on a native `<ol>`. *Use for:* Ordered content in running text: setup steps, a ranked list. For a stepped flow with state, use Stepper.
101
+ - **Instead of** Numbers typed into the text, which neither renumber nor read as a list to assistive tech.
102
+ - `<ol className="sable-list-numbered"><li>Install</li><li>Import the styles</li></ol>`
103
+ - **`sable-list-dl`** — Label/value pairs on a native `<dl>`: muted label-tier `<dt>`s and body-tier `<dd>`s on a baseline-aligned two-column grid; long values wrap inside their column. Colour a status value with a `text-*` utility on its `<dd>`. *Use for:* Read-only details: an order summary, a record's metadata, a settings summary, profile stats.
104
+ - **Instead of** A grid of `<div>`s (screen readers lose the term/value pairing), or Table for a single record. For editable values use Form and Field.
105
+ - `<dl className="sable-list-dl"><dt>Network</dt><dd>Solana</dd><dt>Status</dt><dd className="text-success">Active</dd></dl>`
106
+ - **`sable-list-dl-2`** — Two label/value pairs a row once the nearest `@container` ancestor is at least 28rem wide. Add to `sable-list-dl`. *Use for:* A compact metadata block in a wide card or panel. Put `@container` on a parent so it measures the room it actually has.
107
+ - **Instead of** A viewport breakpoint, which gives a narrow card on a wide screen four columns it cannot fit.
108
+ - `<div className="@container"><dl className="sable-list-dl sable-list-dl-2">…</dl></div>`
94
109
  - **`text-overline`** — The eyebrow / overline / kicker tier: small, letter-spaced, uppercase by default (a theme may set sentence case via `--type-overline-transform`). *Use for:* A section eyebrow above a heading, a stat's label, a badge-style category line. Sits ABOVE and categorises.
95
110
  - **Instead of** `text-label-xs uppercase tracking-wider` assembled by hand, or `text-caption`, which sits BELOW and explains. In tool chrome, panel headers use `text-ui-section` instead.
96
111
  - `<p className="text-overline text-muted-foreground">Latest news</p>`
@@ -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