@sableui/react 0.0.0-stage → 0.1.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.
- package/AGENTS.md +157 -0
- package/CHANGELOG.md +50 -0
- package/CONTRIBUTING.md +221 -0
- package/DESIGN.md +548 -0
- package/LICENSE +21 -0
- package/README.md +194 -2
- package/THIRD_PARTY_NOTICES.md +44 -0
- package/bin/sable-lint.mjs +0 -0
- package/bin/sable.mjs +1223 -0
- package/dist/_contract-CYScOdf5.d.ts +150 -0
- package/dist/chunk-3IWZYBGH.js +398 -0
- package/dist/chunk-5Z5KLOAU.js +167 -0
- package/dist/chunk-7MG4SHYX.js +15 -0
- package/dist/chunk-ADLLDWLW.js +15 -0
- package/dist/chunk-EP55KSYA.js +14 -0
- package/dist/chunk-J23ELQQZ.js +9396 -0
- package/dist/chunk-JI7WJP2L.js +286 -0
- package/dist/chunk-K7RUHUGC.js +14 -0
- package/dist/chunk-KO3NAEBF.js +15 -0
- package/dist/chunk-L4IEIQQA.js +14 -0
- package/dist/chunk-L6Y46C62.js +270 -0
- package/dist/chunk-NEQNXI2G.js +25 -0
- package/dist/chunk-PAOZBZME.js +40 -0
- package/dist/chunk-RWP4XBSK.js +139 -0
- package/dist/chunk-SRQWQUTW.js +63 -0
- package/dist/chunk-YXBOYWBN.js +15 -0
- package/dist/foundations/index.d.ts +88 -0
- package/dist/foundations/index.js +792 -0
- package/dist/icon/index.d.ts +60 -0
- package/dist/icon/index.js +2 -0
- package/dist/icon-sets/brands.d.ts +44 -0
- package/dist/icon-sets/brands.js +147 -0
- package/dist/icon-sets/reicon.d.ts +8 -0
- package/dist/icon-sets/reicon.js +1 -0
- package/dist/index.d.ts +7013 -0
- package/dist/index.js +15462 -0
- package/dist/ios/chrome.css +83 -0
- package/dist/ios/frame.css +5 -0
- package/dist/ios/index.d.ts +70 -0
- package/dist/ios/index.js +313 -0
- package/dist/ios/preview.d.ts +64 -0
- package/dist/ios/preview.js +133 -0
- package/dist/ios/styles.css +77 -0
- package/dist/layout/index.d.ts +280 -0
- package/dist/layout/index.js +2 -0
- package/dist/meta-6dUzgC6M.d.ts +205 -0
- package/dist/patterns/index.d.ts +36 -0
- package/dist/patterns/index.js +934 -0
- package/dist/presets/index.d.ts +37 -0
- package/dist/presets/index.js +117 -0
- package/dist/registry-CyL28OzJ.d.ts +28 -0
- package/dist/styles.css +1893 -0
- package/dist/theme-provider-BWcizhAt.d.ts +105 -0
- package/dist/themes/atelier/atelier.css +213 -0
- package/dist/themes/atelier/index.d.ts +9 -0
- package/dist/themes/atelier/index.js +1 -0
- package/dist/themes/base/base.css +443 -0
- package/dist/themes/base/index.d.ts +9 -0
- package/dist/themes/base/index.js +1 -0
- package/dist/themes/ember/ember.css +270 -0
- package/dist/themes/ember/index.d.ts +9 -0
- package/dist/themes/ember/index.js +1 -0
- package/dist/themes/index.d.ts +10 -0
- package/dist/themes/index.js +8 -0
- package/dist/themes/meridian/index.d.ts +9 -0
- package/dist/themes/meridian/index.js +1 -0
- package/dist/themes/meridian/meridian.css +255 -0
- package/dist/themes/poppy/index.d.ts +6 -0
- package/dist/themes/poppy/index.js +1 -0
- package/dist/themes/poppy/poppy.css +174 -0
- package/dist/themes/prism/index.d.ts +6 -0
- package/dist/themes/prism/index.js +1 -0
- package/dist/themes/prism/prism.css +183 -0
- package/dist/themes/verdant/index.d.ts +9 -0
- package/dist/themes/verdant/index.js +1 -0
- package/dist/themes/verdant/verdant.css +210 -0
- package/dist/themes.css +30 -0
- package/llms/components/accordion.md +93 -0
- package/llms/components/app-header.md +70 -0
- package/llms/components/app-shell.md +117 -0
- package/llms/components/attachment.md +105 -0
- package/llms/components/avatar.md +141 -0
- package/llms/components/badge.md +117 -0
- package/llms/components/banner.md +119 -0
- package/llms/components/bottom-nav.md +80 -0
- package/llms/components/breadcrumbs.md +75 -0
- package/llms/components/browser.md +67 -0
- package/llms/components/bubble.md +119 -0
- package/llms/components/button.md +199 -0
- package/llms/components/calendar.md +75 -0
- package/llms/components/card.md +135 -0
- package/llms/components/carousel.md +87 -0
- package/llms/components/chat-composer.md +126 -0
- package/llms/components/checkbox-card.md +98 -0
- package/llms/components/checkbox.md +90 -0
- package/llms/components/code-block.md +81 -0
- package/llms/components/code.md +62 -0
- package/llms/components/collapsible.md +83 -0
- package/llms/components/color-field.md +100 -0
- package/llms/components/combobox.md +119 -0
- package/llms/components/command-menu.md +158 -0
- package/llms/components/context-card.md +73 -0
- package/llms/components/context-menu.md +79 -0
- package/llms/components/data-table.md +99 -0
- package/llms/components/date-picker.md +97 -0
- package/llms/components/description.md +96 -0
- package/llms/components/destructive-action-modal.md +76 -0
- package/llms/components/dialog.md +127 -0
- package/llms/components/dots-menu.md +64 -0
- package/llms/components/empty-state.md +113 -0
- package/llms/components/entity.md +67 -0
- package/llms/components/feedback.md +63 -0
- package/llms/components/fieldset.md +106 -0
- package/llms/components/file-dropzone.md +103 -0
- package/llms/components/form.md +111 -0
- package/llms/components/gliding-highlight.md +84 -0
- package/llms/components/inline-status.md +62 -0
- package/llms/components/input.md +127 -0
- package/llms/components/kbd.md +57 -0
- package/llms/components/label.md +73 -0
- package/llms/components/marker.md +99 -0
- package/llms/components/marquee.md +66 -0
- package/llms/components/menu.md +122 -0
- package/llms/components/message-scroller.md +110 -0
- package/llms/components/message.md +114 -0
- package/llms/components/meter.md +124 -0
- package/llms/components/middle-truncate.md +62 -0
- package/llms/components/note.md +81 -0
- package/llms/components/notification-badge.md +67 -0
- package/llms/components/number-field.md +70 -0
- package/llms/components/otp-field.md +101 -0
- package/llms/components/page-header.md +76 -0
- package/llms/components/page-loader.md +67 -0
- package/llms/components/pagination.md +92 -0
- package/llms/components/panel-group.md +120 -0
- package/llms/components/panel-section.md +122 -0
- package/llms/components/phone.md +70 -0
- package/llms/components/pill.md +73 -0
- package/llms/components/popover.md +79 -0
- package/llms/components/progress.md +98 -0
- package/llms/components/radio-card.md +90 -0
- package/llms/components/radio.md +86 -0
- package/llms/components/relative-time.md +60 -0
- package/llms/components/scroller.md +88 -0
- package/llms/components/scrub-field.md +97 -0
- package/llms/components/search-input.md +84 -0
- package/llms/components/select.md +106 -0
- package/llms/components/separator.md +70 -0
- package/llms/components/settings-menu.md +139 -0
- package/llms/components/sheet.md +97 -0
- package/llms/components/show-more.md +59 -0
- package/llms/components/side-nav.md +106 -0
- package/llms/components/sidebar.md +87 -0
- package/llms/components/skeleton.md +62 -0
- package/llms/components/slider.md +114 -0
- package/llms/components/spinner.md +87 -0
- package/llms/components/status-bar.md +91 -0
- package/llms/components/status-dot.md +70 -0
- package/llms/components/stepper.md +125 -0
- package/llms/components/streaming-text.md +86 -0
- package/llms/components/switch.md +87 -0
- package/llms/components/table.md +112 -0
- package/llms/components/tabs.md +84 -0
- package/llms/components/textarea.md +88 -0
- package/llms/components/theme-selector.md +95 -0
- package/llms/components/theme-switcher.md +71 -0
- package/llms/components/thinking-orb.md +91 -0
- package/llms/components/toast.md +130 -0
- package/llms/components/toggle.md +95 -0
- package/llms/components/toolbar.md +168 -0
- package/llms/components/tooltip.md +93 -0
- package/llms/components/tree.md +227 -0
- package/llms/components/video.md +66 -0
- package/llms/components/workspace.md +138 -0
- package/llms/foundations/accessibility.md +60 -0
- package/llms/foundations/motion.md +96 -0
- package/llms/foundations/scrolling.md +41 -0
- package/llms/foundations/shape.md +73 -0
- package/llms/foundations/spacing.md +82 -0
- package/llms/foundations/surfaces.md +69 -0
- package/llms/foundations/typography.md +120 -0
- package/llms/layout/aspect-box.md +37 -0
- package/llms/layout/bleed.md +31 -0
- package/llms/layout/center.md +37 -0
- package/llms/layout/columns.md +51 -0
- package/llms/layout/grid-overlay.md +40 -0
- package/llms/layout/grid.md +47 -0
- package/llms/layout/page.md +39 -0
- package/llms/layout/region.md +49 -0
- package/llms/layout/row.md +40 -0
- package/llms/layout/safe-area.md +37 -0
- package/llms/layout/spacer.md +39 -0
- package/llms/layout/split-pane.md +41 -0
- package/llms/layout/stack.md +39 -0
- package/llms/patterns/auth-page.md +68 -0
- package/llms/patterns/brand-landing-page.md +423 -0
- package/llms/patterns/chat-panel.md +210 -0
- package/llms/patterns/dashboard-page.md +170 -0
- package/llms/patterns/feed-page.md +103 -0
- package/llms/patterns/landing-page.md +122 -0
- package/llms/patterns/list-detail-page.md +100 -0
- package/llms/patterns/settings-page.md +121 -0
- package/llms/reference/token-intent.md +324 -0
- package/llms/reference/utilities.md +129 -0
- package/llms/tasks/adding-a-component.md +113 -0
- package/llms/tasks/auditing-an-app.md +216 -0
- package/llms/tasks/building-a-page.md +109 -0
- package/llms/tasks/building-a-tool.md +141 -0
- package/llms/tasks/building-an-ios-prototype.md +105 -0
- package/llms/tasks/building-with-sable.md +73 -0
- package/llms/tasks/choosing-a-component.md +109 -0
- package/llms/tasks/converting-an-app.md +139 -0
- package/llms/tasks/creating-a-preset.md +43 -0
- package/llms/tasks/creating-design-assets.md +96 -0
- package/llms/tasks/designing-a-surface.md +115 -0
- package/llms/tasks/developing-taste.md +157 -0
- package/llms/tasks/getting-started.md +63 -0
- package/llms/tasks/learning-from-builds.md +74 -0
- package/llms/tasks/reviewing-a-design.md +114 -0
- package/llms/tasks/reviewing-a-journey.md +84 -0
- package/llms/tasks/reviewing-motion.md +92 -0
- package/llms/tasks/using-presets.md +87 -0
- package/llms/taste/asset-plan.template.json +53 -0
- package/llms/taste/composition-follows-content.md +20 -0
- package/llms/taste/hierarchy-is-a-relationship.md +22 -0
- package/llms/taste/lessons.json +57 -0
- package/llms/taste/motion-needs-a-reason.md +21 -0
- package/llms/taste/surface-states-travel-together.md +20 -0
- package/llms-full.txt +14906 -0
- package/llms.txt +405 -0
- package/package.json +185 -3
- package/registry.json +18812 -0
- package/scripts/agent-setup.mjs +54 -0
- package/scripts/design-atlas.mjs +221 -0
- package/scripts/design-directions.mjs +123 -0
- package/scripts/design-memory.mjs +166 -0
- package/scripts/design-preferences.mjs +227 -0
- package/scripts/design-presets.mjs +48 -0
- package/scripts/grade.mjs +509 -0
- package/scripts/lint-rules.mjs +1065 -0
- package/scripts/review-evidence.mjs +120 -0
- package/scripts/skeleton.mjs +248 -0
- package/skills/entry/SKILL.md +34 -0
- package/skills/sable/SKILL.md +187 -0
- package/skills/sable/agents/openai.yaml +6 -0
- package/targets/ios.json +982 -0
- package/third-party/reicon-LICENSE.txt +20 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# DashboardPage
|
|
2
|
+
|
|
3
|
+
App-shell dashboard: sidebar chrome (AppHeader + BottomNav below lg), a wide content region, a stat-card row that breathes, and a data table — the default shape for operator/analytics home routes.
|
|
4
|
+
|
|
5
|
+
**Intent:** Reach for this when a route's job is monitoring or managing many records (metrics, deployments, orders, posts). Not for reading-focused routes (use FeedPage) or single-record routes (use ListDetailPage's detail side).
|
|
6
|
+
|
|
7
|
+
## Notes
|
|
8
|
+
|
|
9
|
+
- Region cap is `wide` (1280px): dashboards hold tables and multi-column grids, never a reading column.
|
|
10
|
+
- Stat cards: CardHeader carries label (CardDescription) + value + trend badge (CardAction pins it top-right); KPI values are `text-mono-xl` — the metric mono tier, tabular figures — never a heading tier.
|
|
11
|
+
- Stat cards use an explicit cols map (1 → 2 → 4) — a 4-stat row must fold to 2×2, never strand an orphan the way auto-fit can.
|
|
12
|
+
- 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.
|
|
13
|
+
- The Activity list is Entity rows in CardList, inset to the card padding and divided by hairlines — the same list shape as ListDetailPage, never CardContent with hand-tuned px.
|
|
14
|
+
- Believable density is part of the pattern: 7 rows, commit hashes in mono-caption, RelativeTime muted — never 3 rows of filler.
|
|
15
|
+
- The page has exactly one heading-1 (PageHeader); card titles are Title tier, not headings.
|
|
16
|
+
- Chrome (Sidebar) is outside the content grid; the Page region caps only content.
|
|
17
|
+
- Sidebar hides below lg, so the shell's header carries an AppHeader and its footer a BottomNav with the same destinations — never strand mobile users without navigation.
|
|
18
|
+
|
|
19
|
+
## Accessibility
|
|
20
|
+
|
|
21
|
+
- AppShell provides the skip link and <main id="main-content"> landmark.
|
|
22
|
+
- The active SidebarItem and BottomNavItem carry aria-current via `active`.
|
|
23
|
+
- DataTable headers are real <th>; sortable columns are announced.
|
|
24
|
+
|
|
25
|
+
## Source
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { AppHeader, AppShell, Avatar, Badge, BottomNav, BottomNavItem, Button, Card, CardAction, CardContent, CardDescription, CardHeader, CardList, CardTitle, DataTable, Entity, Grid, Icon, Page, PageHeader, RelativeTime, Row, Sidebar, SidebarHeader, SidebarItem, SidebarNav } from "@sableui/react"
|
|
29
|
+
|
|
30
|
+
const MIN = 60 * 1000
|
|
31
|
+
const HOUR = 60 * MIN
|
|
32
|
+
|
|
33
|
+
const stats = [
|
|
34
|
+
{ label: "Revenue", value: "$12,480", delta: "+8.2%", up: true, note: "vs. previous 30 days" },
|
|
35
|
+
{ label: "Collects", value: "1,284", delta: "+3.1%", up: true, note: "182 from new collectors" },
|
|
36
|
+
{ label: "New collectors", value: "312", delta: "+12.4%", up: true, note: "best month this year" },
|
|
37
|
+
{ label: "Sell-through", value: "64%", delta: "−1.8%", up: false, note: "across 48 active editions" },
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
const deployments = [
|
|
41
|
+
{ id: "dp_01", project: "storefront", commit: "a41f2c9", status: "ready" as const, duration: "42s", updated: 2 * MIN },
|
|
42
|
+
{ id: "dp_02", project: "api", commit: "7c03bd1", status: "building" as const, duration: "1m 08s", updated: 9 * MIN },
|
|
43
|
+
{ id: "dp_03", project: "docs", commit: "e992a70", status: "queued" as const, duration: "—", updated: 12 * MIN },
|
|
44
|
+
{ id: "dp_04", project: "worker", commit: "51b8e04", status: "failed" as const, duration: "18s", updated: 1 * HOUR },
|
|
45
|
+
{ id: "dp_05", project: "storefront", commit: "90cc41a", status: "ready" as const, duration: "39s", updated: 3 * HOUR },
|
|
46
|
+
{ id: "dp_06", project: "renderer", commit: "b7714dd", status: "ready" as const, duration: "2m 31s", updated: 5 * HOUR },
|
|
47
|
+
{ id: "dp_07", project: "api", commit: "f30a9e2", status: "ready" as const, duration: "1m 02s", updated: 8 * HOUR },
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
const activity = [
|
|
51
|
+
{ id: "ac_01", who: "Nina Simone", initials: "NS", what: "collected Gold Rush #42", when: 2 * MIN },
|
|
52
|
+
{ id: "ac_02", who: "Grace Hopper", initials: "GH", what: "minted a 100-run edition", when: 26 * MIN },
|
|
53
|
+
{ id: "ac_03", who: "Ada Lovelace", initials: "AL", what: "followed you", when: 1 * HOUR },
|
|
54
|
+
{ id: "ac_04", who: "Mary Shelley", initials: "MS", what: "collected Quiet Harbor", when: 3 * HOUR },
|
|
55
|
+
{ id: "ac_05", who: "Frida Kahlo", initials: "FK", what: "tipped 0.2 SOL on Process", when: 6 * HOUR },
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
export function DashboardPage() {
|
|
59
|
+
return (
|
|
60
|
+
<AppShell
|
|
61
|
+
// Sidebar hides below lg, so the same destinations ride in the shell's
|
|
62
|
+
// mobile slots — without them the route has no navigation under 1024px.
|
|
63
|
+
header={<AppHeader title={<span className="text-title-sm">Atelier</span>} />}
|
|
64
|
+
bottomNav={
|
|
65
|
+
<BottomNav aria-label="Primary">
|
|
66
|
+
<BottomNavItem icon={<Icon name="grid" />} label="Overview" showLabel active render={<a href="/" />} />
|
|
67
|
+
<BottomNavItem icon={<Icon name="image-stack" />} label="Editions" showLabel render={<a href="/editions" />} />
|
|
68
|
+
<BottomNavItem icon={<Icon name="user" />} label="Collectors" showLabel render={<a href="/collectors" />} />
|
|
69
|
+
<BottomNavItem icon={<Icon name="bell" />} label="Activity" showLabel render={<a href="/activity" />} />
|
|
70
|
+
<BottomNavItem icon={<Icon name="gear" />} label="Settings" showLabel render={<a href="/settings" />} />
|
|
71
|
+
</BottomNav>
|
|
72
|
+
}
|
|
73
|
+
sidebar={
|
|
74
|
+
<Sidebar>
|
|
75
|
+
<SidebarHeader>
|
|
76
|
+
<span className="text-title-sm">Atelier</span>
|
|
77
|
+
</SidebarHeader>
|
|
78
|
+
<SidebarNav>
|
|
79
|
+
<SidebarItem icon={<Icon name="grid" />} label="Overview" active render={<a href="/" />} />
|
|
80
|
+
<SidebarItem icon={<Icon name="image-stack" />} label="Editions" render={<a href="/editions" />} />
|
|
81
|
+
<SidebarItem icon={<Icon name="user" />} label="Collectors" render={<a href="/collectors" />} />
|
|
82
|
+
<SidebarItem icon={<Icon name="bell" />} label="Activity" render={<a href="/activity" />} />
|
|
83
|
+
<SidebarItem icon={<Icon name="gear" />} label="Settings" render={<a href="/settings" />} />
|
|
84
|
+
</SidebarNav>
|
|
85
|
+
</Sidebar>
|
|
86
|
+
}
|
|
87
|
+
>
|
|
88
|
+
<Page max="wide" gap={4} className="py-6">
|
|
89
|
+
<PageHeader
|
|
90
|
+
title="Overview"
|
|
91
|
+
description="Sales, collects, and edition activity for the last 30 days."
|
|
92
|
+
actions={
|
|
93
|
+
<Row gap={2}>
|
|
94
|
+
<Button variant="outline">Export</Button>
|
|
95
|
+
<Button>New edition</Button>
|
|
96
|
+
</Row>
|
|
97
|
+
}
|
|
98
|
+
/>
|
|
99
|
+
<Grid cols={{ base: 1, sm: 2, xl: 4 }} gap={3}>
|
|
100
|
+
{stats.map((stat) => (
|
|
101
|
+
<Card key={stat.label}>
|
|
102
|
+
<CardHeader>
|
|
103
|
+
<CardDescription>{stat.label}</CardDescription>
|
|
104
|
+
<CardTitle className="text-mono-xl">{stat.value}</CardTitle>
|
|
105
|
+
<CardAction>
|
|
106
|
+
<Badge tone={stat.up ? "success" : "neutral"} size="sm">
|
|
107
|
+
<Icon name={stat.up ? "chevron-up" : "chevron-down"} />
|
|
108
|
+
{stat.delta}
|
|
109
|
+
</Badge>
|
|
110
|
+
</CardAction>
|
|
111
|
+
</CardHeader>
|
|
112
|
+
<CardContent>
|
|
113
|
+
<p className="text-caption text-muted-foreground">{stat.note}</p>
|
|
114
|
+
</CardContent>
|
|
115
|
+
</Card>
|
|
116
|
+
))}
|
|
117
|
+
</Grid>
|
|
118
|
+
<Grid cols={{ base: 1, xl: 3 }} gap={3}>
|
|
119
|
+
<Card className="xl:col-span-2">
|
|
120
|
+
<CardHeader>
|
|
121
|
+
<CardTitle>Recent deployments</CardTitle>
|
|
122
|
+
<CardDescription>Builds across every project, newest first.</CardDescription>
|
|
123
|
+
<CardAction>
|
|
124
|
+
<Button variant="ghost" size="sm" render={<a href="/deployments" />}>
|
|
125
|
+
View all
|
|
126
|
+
</Button>
|
|
127
|
+
</CardAction>
|
|
128
|
+
</CardHeader>
|
|
129
|
+
<CardContent flush>
|
|
130
|
+
<DataTable
|
|
131
|
+
data={deployments}
|
|
132
|
+
rowKey={(row) => row.id}
|
|
133
|
+
columns={[
|
|
134
|
+
{ key: "project", header: "Project", sortable: true, className: "text-label-md" },
|
|
135
|
+
{ key: "commit", header: "Commit", cell: (row) => <span className="text-mono-caption text-muted-foreground">{row.commit}</span> },
|
|
136
|
+
{ key: "status", header: "Status", cell: (row) => <Badge appearance="plain" tone={statusTone(row.status)} dot>{row.status}</Badge> },
|
|
137
|
+
{ key: "duration", header: "Duration", align: "end", cell: (row) => <span className="text-mono-caption">{row.duration}</span> },
|
|
138
|
+
{ key: "updated", header: "Updated", align: "end", cell: (row) => <RelativeTime date={Date.now() - row.updated} className="text-body-xs text-muted-foreground" /> },
|
|
139
|
+
]}
|
|
140
|
+
/>
|
|
141
|
+
</CardContent>
|
|
142
|
+
</Card>
|
|
143
|
+
<Card>
|
|
144
|
+
<CardHeader>
|
|
145
|
+
<CardTitle>Activity</CardTitle>
|
|
146
|
+
<CardDescription>Collects, mints, and follows.</CardDescription>
|
|
147
|
+
</CardHeader>
|
|
148
|
+
<CardList>
|
|
149
|
+
{activity.map((item) => (
|
|
150
|
+
<Entity
|
|
151
|
+
key={item.id}
|
|
152
|
+
density="compact"
|
|
153
|
+
leading={<Avatar size="sm" fallback={item.initials} alt={item.who} />}
|
|
154
|
+
title={
|
|
155
|
+
<span className="text-body-sm">
|
|
156
|
+
<span className="text-label-md">{item.who}</span>{" "}
|
|
157
|
+
<span className="text-muted-foreground">{item.what}</span>
|
|
158
|
+
</span>
|
|
159
|
+
}
|
|
160
|
+
action={<RelativeTime date={Date.now() - item.when} className="text-caption text-muted-foreground" />}
|
|
161
|
+
/>
|
|
162
|
+
))}
|
|
163
|
+
</CardList>
|
|
164
|
+
</Card>
|
|
165
|
+
</Grid>
|
|
166
|
+
</Page>
|
|
167
|
+
</AppShell>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
```
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# FeedPage
|
|
2
|
+
|
|
3
|
+
Single-column reading feed: a feed-width region, one Card per post (Entity byline, zero-shift media, body copy, quiet action row), and explicit load-more pagination.
|
|
4
|
+
|
|
5
|
+
**Intent:** Reach for this for timelines, activity streams, and any reverse-chronological reading surface. Not for record management (DashboardPage) or side-by-side browsing (ListDetailPage).
|
|
6
|
+
|
|
7
|
+
## Notes
|
|
8
|
+
|
|
9
|
+
- Region cap is `feed` (640px) — a single reading column; media and copy share one measure.
|
|
10
|
+
- Post media sits in an AspectBox so late-loading images never shift the column.
|
|
11
|
+
- The byline is Entity (Avatar + name + handle) in a CardList — the same row primitive as everywhere else, inset to the card padding by CardList rather than by zeroing paddings on CardHeader and Entity.
|
|
12
|
+
- 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: quiet chrome under content, not competing pills.
|
|
13
|
+
- Pagination is an explicit load-more Button (secondary; set `loading` while the next page fetches — `loadingLabel` names that state), not infinite scroll — predictable for keyboard and AT users.
|
|
14
|
+
|
|
15
|
+
## Accessibility
|
|
16
|
+
|
|
17
|
+
- Each post is an <article>, so AT users can jump between posts.
|
|
18
|
+
- Icon-only action buttons carry aria-labels.
|
|
19
|
+
- Media images get meaningful alt text from the post, empty alt only if decorative.
|
|
20
|
+
|
|
21
|
+
## Source
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { AspectBox, Avatar, Button, Card, CardContent, CardList, Entity, Icon, Page, PageHeader, Row, Stack } from "@sableui/react"
|
|
25
|
+
|
|
26
|
+
const posts = [
|
|
27
|
+
{
|
|
28
|
+
id: "p_01",
|
|
29
|
+
author: "Ada Lovelace",
|
|
30
|
+
handle: "@ada · 2h",
|
|
31
|
+
initials: "AL",
|
|
32
|
+
body: "New edition drops Friday — 100 signed prints, each with the on-chain original.",
|
|
33
|
+
image: "/media/edition-cover.svg",
|
|
34
|
+
alt: "Edition cover — Gold Rush series",
|
|
35
|
+
likes: 128,
|
|
36
|
+
comments: 24,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "p_02",
|
|
40
|
+
author: "Grace Hopper",
|
|
41
|
+
handle: "@grace · 5h",
|
|
42
|
+
initials: "GH",
|
|
43
|
+
body: "Process shot from this week's series. Compilers and copper ink.",
|
|
44
|
+
image: "/media/process-shot.svg",
|
|
45
|
+
alt: "Work-in-progress plotter drawing in copper ink",
|
|
46
|
+
likes: 86,
|
|
47
|
+
comments: 9,
|
|
48
|
+
},
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
export function FeedPage() {
|
|
52
|
+
return (
|
|
53
|
+
<Page as="main" max="feed" gap={4} className="py-6">
|
|
54
|
+
<PageHeader title="Home" description="Latest posts from artists you follow." />
|
|
55
|
+
<Stack gap={3}>
|
|
56
|
+
{posts.map((post) => (
|
|
57
|
+
<article key={post.id}>
|
|
58
|
+
<Card className="overflow-hidden">
|
|
59
|
+
{/* The byline is an Entity row in CardList, which insets it to the
|
|
60
|
+
card's own padding on every side — no hand-zeroed paddings. */}
|
|
61
|
+
<CardList>
|
|
62
|
+
<Entity
|
|
63
|
+
leading={<Avatar fallback={post.initials} alt={post.author} />}
|
|
64
|
+
title={post.author}
|
|
65
|
+
subtitle={post.handle}
|
|
66
|
+
action={
|
|
67
|
+
<Button variant="ghost" size="icon-sm" aria-label="Post options">
|
|
68
|
+
<Icon name="ellipsis" />
|
|
69
|
+
</Button>
|
|
70
|
+
}
|
|
71
|
+
/>
|
|
72
|
+
</CardList>
|
|
73
|
+
<AspectBox ratio={16 / 9}>
|
|
74
|
+
<img src={post.image} alt={post.alt} className="size-full object-cover" />
|
|
75
|
+
</AspectBox>
|
|
76
|
+
<CardContent className="flex flex-col gap-2">
|
|
77
|
+
<p className="text-body-md text-foreground">{post.body}</p>
|
|
78
|
+
{/* Pulled out by exactly a sm Button's own padding (px-3), so the
|
|
79
|
+
first icon lands on the text edge above it. Both are on the
|
|
80
|
+
spacing scale, so they stay aligned at every density. */}
|
|
81
|
+
<Row gap={1} className="-ms-3">
|
|
82
|
+
<Button variant="ghost" size="sm" aria-label={`Like — ${post.likes}`}>
|
|
83
|
+
<Icon name="heart" />
|
|
84
|
+
{post.likes}
|
|
85
|
+
</Button>
|
|
86
|
+
<Button variant="ghost" size="sm" aria-label={`Comments — ${post.comments}`}>
|
|
87
|
+
<Icon name="comment" />
|
|
88
|
+
{post.comments}
|
|
89
|
+
</Button>
|
|
90
|
+
<Button variant="ghost" size="sm" aria-label="Share">
|
|
91
|
+
<Icon name="share" />
|
|
92
|
+
</Button>
|
|
93
|
+
</Row>
|
|
94
|
+
</CardContent>
|
|
95
|
+
</Card>
|
|
96
|
+
</article>
|
|
97
|
+
))}
|
|
98
|
+
</Stack>
|
|
99
|
+
<Button variant="secondary" loadingLabel="Loading…">Load more posts</Button>
|
|
100
|
+
</Page>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
```
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# LandingPage
|
|
2
|
+
|
|
3
|
+
Marketing hero + feature grid: display-tier headline as the page's single loud moment, an eyebrow kicker, a two-action CTA row, and a three-up feature grid that reveals on scroll.
|
|
4
|
+
|
|
5
|
+
**Intent:** Reach for this for the marketing/landing route of a product — the one page where Display type and scroll reveals belong. Not for product UI (display type and one-per-view rules forbid it inside the app).
|
|
6
|
+
|
|
7
|
+
## Notes
|
|
8
|
+
|
|
9
|
+
- Display type is the marketing voice: exactly one display headline per page, tight leading, weight 700+.
|
|
10
|
+
- The eyebrow is a small outline Badge (`size="sm"`) — the badge's own size, never a type class forced onto its label; hierarchy above the headline is eyebrow → display → lede.
|
|
11
|
+
- The lede is body-lg capped at the prose measure — the headline may be wide, reading text may not.
|
|
12
|
+
- Two CTAs max: one primary, one quiet outline. More actions dilute the hero.
|
|
13
|
+
- Reveal wraps below-the-fold sections only; it no-ops under reduced motion via the motion policy.
|
|
14
|
+
- Feature cards are Cards in a Grid with minItemWidth (three-up that reflows on its own width), each icon + title + body-sm copy.
|
|
15
|
+
|
|
16
|
+
## Accessibility
|
|
17
|
+
|
|
18
|
+
- The hero headline is the page h1; feature titles are h2s restyled with the Title tier.
|
|
19
|
+
- Reveal renders content immediately (no animation) when prefers-reduced-motion is set.
|
|
20
|
+
- CTA buttons are real links or buttons with visible text — no icon-only CTAs.
|
|
21
|
+
|
|
22
|
+
## Source
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { Badge, Button, Card, CardContent, Grid, Icon, Region, Reveal, Row, Stack } from "@sableui/react"
|
|
26
|
+
|
|
27
|
+
const features = [
|
|
28
|
+
{
|
|
29
|
+
icon: "sparkles" as const,
|
|
30
|
+
title: "Own the surface",
|
|
31
|
+
body: "Semantic tokens end-to-end — re-theme the whole product without touching a component.",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
icon: "component" as const,
|
|
35
|
+
title: "Behavior included",
|
|
36
|
+
body: "Focus, keyboard nav, and ARIA ride on Base UI primitives under every surface.",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
icon: "layout" as const,
|
|
40
|
+
title: "Pages, not just parts",
|
|
41
|
+
body: "Purpose-named regions, an 8px rhythm, and canonical page shells for the routes every product needs.",
|
|
42
|
+
},
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
export function LandingPage() {
|
|
46
|
+
return (
|
|
47
|
+
<div className="min-h-dvh bg-background">
|
|
48
|
+
<header>
|
|
49
|
+
<Region as="nav" aria-label="Main" className="grid h-16 grid-cols-[1fr_auto_1fr] items-center">
|
|
50
|
+
<span className="justify-self-start text-title-sm">Sable</span>
|
|
51
|
+
<Row gap={1} className="justify-self-center max-sm:hidden">
|
|
52
|
+
<Button variant="ghost" size="sm" render={<a href="/docs" />}>
|
|
53
|
+
Docs
|
|
54
|
+
</Button>
|
|
55
|
+
<Button variant="ghost" size="sm" render={<a href="/patterns" />}>
|
|
56
|
+
Patterns
|
|
57
|
+
</Button>
|
|
58
|
+
<Button variant="ghost" size="sm" render={<a href="/themes" />}>
|
|
59
|
+
Themes
|
|
60
|
+
</Button>
|
|
61
|
+
</Row>
|
|
62
|
+
<Button size="sm" className="justify-self-end" render={<a href="/signup" />}>
|
|
63
|
+
Get started
|
|
64
|
+
</Button>
|
|
65
|
+
</Region>
|
|
66
|
+
</header>
|
|
67
|
+
<main>
|
|
68
|
+
<Region as="section" max="wide" className="py-24">
|
|
69
|
+
<Stack gap={4} align="center" className="text-center">
|
|
70
|
+
<Badge appearance="outline" size="sm">
|
|
71
|
+
Now in beta
|
|
72
|
+
</Badge>
|
|
73
|
+
<h1 className="max-w-4xl text-balance text-display-2xl">
|
|
74
|
+
Ship product UI your agents can't get wrong
|
|
75
|
+
</h1>
|
|
76
|
+
<p className="max-w-(--measure-prose) text-body-lg text-muted-foreground">
|
|
77
|
+
An owned, themeable component system with enforced tokens, typography,
|
|
78
|
+
and motion — documented for machines as carefully as for people.
|
|
79
|
+
</p>
|
|
80
|
+
<Row gap={2} justify="center">
|
|
81
|
+
<Button size="cta">Get started</Button>
|
|
82
|
+
<Button size="cta" variant="outline">
|
|
83
|
+
Read the docs
|
|
84
|
+
</Button>
|
|
85
|
+
</Row>
|
|
86
|
+
</Stack>
|
|
87
|
+
</Region>
|
|
88
|
+
<Region as="section" max="wide" className="pb-24">
|
|
89
|
+
<Reveal>
|
|
90
|
+
{/* Three-up that reflows on its own width (minItemWidth), one Card
|
|
91
|
+
per feature — never a hand-drawn bordered grid. */}
|
|
92
|
+
<Grid minItemWidth="calc(var(--spacing) * 64)">
|
|
93
|
+
{features.map((feature) => (
|
|
94
|
+
<Card key={feature.title}>
|
|
95
|
+
<CardContent className="flex flex-col gap-2">
|
|
96
|
+
<Icon name={feature.icon} className="text-xl text-muted-foreground" />
|
|
97
|
+
<h2 className="text-title-lg">{feature.title}</h2>
|
|
98
|
+
<p className="text-body-sm text-muted-foreground">{feature.body}</p>
|
|
99
|
+
</CardContent>
|
|
100
|
+
</Card>
|
|
101
|
+
))}
|
|
102
|
+
</Grid>
|
|
103
|
+
</Reveal>
|
|
104
|
+
</Region>
|
|
105
|
+
</main>
|
|
106
|
+
<footer className="border-t border-border">
|
|
107
|
+
<Region className="flex h-16 items-center justify-between">
|
|
108
|
+
<span className="text-caption text-muted-foreground">© 2026 Sable</span>
|
|
109
|
+
<Row gap={3}>
|
|
110
|
+
<a href="/github" className="text-caption text-muted-foreground motion-interactive hover:text-foreground">
|
|
111
|
+
GitHub
|
|
112
|
+
</a>
|
|
113
|
+
<a href="/changelog" className="text-caption text-muted-foreground motion-interactive hover:text-foreground">
|
|
114
|
+
Changelog
|
|
115
|
+
</a>
|
|
116
|
+
</Row>
|
|
117
|
+
</Region>
|
|
118
|
+
</footer>
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
```
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# ListDetailPage
|
|
2
|
+
|
|
3
|
+
Two-column list-detail: a wide region split 8/4, the record list as Entity rows in the main column, and a sticky supporting rail (detail, filters, or summary) beside it.
|
|
4
|
+
|
|
5
|
+
**Intent:** Reach for this when a route lists records AND keeps one selected/summarized in view (members + member detail, inbox + message, orders + order). For pure browsing with no rail, a single-column FeedPage or DashboardPage table is simpler.
|
|
6
|
+
|
|
7
|
+
## Notes
|
|
8
|
+
|
|
9
|
+
- Region cap is `wide`; the split is Columns 12 → Col 8 + Col 4, collapsing to stacked full-width columns below lg via breakpoint maps.
|
|
10
|
+
- List rows are Entity inside one bordered Card, in CardList: it insets each row to the card padding and divides them with hairlines — on a dark canvas rows need an anchoring surface, not a flat float.
|
|
11
|
+
- Pending records live in the same list, muted (muted title + `Invited` badge), instead of a separate section.
|
|
12
|
+
- The rail card holds label/value pairs as a Description list — muted terms, mono (tabular) values — plus one quiet action. Never hand-built `justify-between` rows.
|
|
13
|
+
- The rail is sticky (top on the 4px scale) so the summary stays in view while the list scrolls.
|
|
14
|
+
|
|
15
|
+
## Accessibility
|
|
16
|
+
|
|
17
|
+
- Entity rows given `interactive` + `onSelect` (to open a member's detail) render as real buttons; keyboard focus follows list order. Rows here are presentational.
|
|
18
|
+
- The rail is an <aside> landmark; the list column is the main content.
|
|
19
|
+
|
|
20
|
+
## Source
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { Avatar, Badge, Button, Card, CardContent, CardHeader, CardList, CardTitle, Col, Columns, Description, DescriptionItem, Entity, Page, PageHeader, SearchInput, Stack } from "@sableui/react"
|
|
24
|
+
|
|
25
|
+
const members = [
|
|
26
|
+
{ id: "u_01", name: "Ada Lovelace", handle: "@ada", role: "Owner", initials: "AL", pending: false },
|
|
27
|
+
{ id: "u_02", name: "Grace Hopper", handle: "@grace", role: "Admin", initials: "GH", pending: false },
|
|
28
|
+
{ id: "u_03", name: "Mary Shelley", handle: "@mary", role: "Member", initials: "MS", pending: false },
|
|
29
|
+
{ id: "u_04", name: "Nina Simone", handle: "@nina", role: "Member", initials: "NS", pending: false },
|
|
30
|
+
{ id: "u_05", name: "Frida Kahlo", handle: "@frida", role: "Member", initials: "FK", pending: false },
|
|
31
|
+
{ id: "u_06", name: "hopper@navy.mil", handle: "invited 2 days ago", role: "Member", initials: "?", pending: true },
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
export function ListDetailPage() {
|
|
35
|
+
return (
|
|
36
|
+
<Page as="main" max="wide" gap={4} className="py-6">
|
|
37
|
+
<PageHeader
|
|
38
|
+
title="Members"
|
|
39
|
+
description="Everyone with access to this workspace."
|
|
40
|
+
actions={<Button>Invite member</Button>}
|
|
41
|
+
/>
|
|
42
|
+
<Columns count={{ base: 1, lg: 12 }}>
|
|
43
|
+
<Col span={{ base: 1, lg: 8 }}>
|
|
44
|
+
<Stack gap={3}>
|
|
45
|
+
<SearchInput placeholder="Search members" aria-label="Search members" />
|
|
46
|
+
<Card className="overflow-hidden">
|
|
47
|
+
<CardList>
|
|
48
|
+
{members.map((member) => (
|
|
49
|
+
<Entity
|
|
50
|
+
key={member.id}
|
|
51
|
+
leading={
|
|
52
|
+
<Avatar
|
|
53
|
+
fallback={member.pending ? "•" : member.initials}
|
|
54
|
+
alt={member.pending ? "" : member.name}
|
|
55
|
+
/>
|
|
56
|
+
}
|
|
57
|
+
title={
|
|
58
|
+
<span className={member.pending ? "text-muted-foreground" : undefined}>
|
|
59
|
+
{member.name}
|
|
60
|
+
</span>
|
|
61
|
+
}
|
|
62
|
+
subtitle={member.handle}
|
|
63
|
+
action={
|
|
64
|
+
member.pending ? (
|
|
65
|
+
<Badge>Invited</Badge>
|
|
66
|
+
) : (
|
|
67
|
+
<Badge appearance="outline">{member.role}</Badge>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
/>
|
|
71
|
+
))}
|
|
72
|
+
</CardList>
|
|
73
|
+
</Card>
|
|
74
|
+
</Stack>
|
|
75
|
+
</Col>
|
|
76
|
+
<Col span={{ base: 1, lg: 4 }}>
|
|
77
|
+
<aside className="sticky top-6">
|
|
78
|
+
<Card>
|
|
79
|
+
<CardHeader>
|
|
80
|
+
<CardTitle>Workspace</CardTitle>
|
|
81
|
+
</CardHeader>
|
|
82
|
+
<CardContent className="flex flex-col gap-3">
|
|
83
|
+
{/* Label/value pairs are a Description list, not hand-built rows. */}
|
|
84
|
+
<Description>
|
|
85
|
+
<DescriptionItem term="Members" detail={<span className="text-mono-md">5</span>} />
|
|
86
|
+
<DescriptionItem term="Pending invites" detail={<span className="text-mono-md">1</span>} />
|
|
87
|
+
<DescriptionItem term="Seats used" detail={<span className="text-mono-md">6 / 10</span>} />
|
|
88
|
+
</Description>
|
|
89
|
+
<Button variant="outline" size="sm" className="mt-1 self-start">
|
|
90
|
+
Manage plan
|
|
91
|
+
</Button>
|
|
92
|
+
</CardContent>
|
|
93
|
+
</Card>
|
|
94
|
+
</aside>
|
|
95
|
+
</Col>
|
|
96
|
+
</Columns>
|
|
97
|
+
</Page>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
```
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# SettingsPage
|
|
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.
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
## Notes
|
|
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.
|
|
14
|
+
- Lives in the same AppShell as DashboardPage: Sidebar from lg up, AppHeader + BottomNav below it, with Settings marked active in both.
|
|
15
|
+
|
|
16
|
+
## Accessibility
|
|
17
|
+
|
|
18
|
+
- 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).
|
|
20
|
+
- 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.
|
|
22
|
+
|
|
23
|
+
## Source
|
|
24
|
+
|
|
25
|
+
```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"
|
|
27
|
+
|
|
28
|
+
export function SettingsPage() {
|
|
29
|
+
return (
|
|
30
|
+
<AppShell
|
|
31
|
+
// Sidebar hides below lg, so the same destinations ride in the shell's
|
|
32
|
+
// mobile slots — without them the route has no navigation under 1024px.
|
|
33
|
+
header={<AppHeader title={<span className="text-title-sm">Atelier</span>} />}
|
|
34
|
+
bottomNav={
|
|
35
|
+
<BottomNav aria-label="Primary">
|
|
36
|
+
<BottomNavItem icon={<Icon name="grid" />} label="Overview" showLabel render={<a href="/" />} />
|
|
37
|
+
<BottomNavItem icon={<Icon name="image-stack" />} label="Editions" showLabel render={<a href="/editions" />} />
|
|
38
|
+
<BottomNavItem icon={<Icon name="user" />} label="Collectors" showLabel render={<a href="/collectors" />} />
|
|
39
|
+
<BottomNavItem icon={<Icon name="bell" />} label="Activity" showLabel render={<a href="/activity" />} />
|
|
40
|
+
<BottomNavItem icon={<Icon name="gear" />} label="Settings" showLabel active render={<a href="/settings" />} />
|
|
41
|
+
</BottomNav>
|
|
42
|
+
}
|
|
43
|
+
sidebar={
|
|
44
|
+
<Sidebar>
|
|
45
|
+
<SidebarHeader>
|
|
46
|
+
<span className="text-title-sm">Atelier</span>
|
|
47
|
+
</SidebarHeader>
|
|
48
|
+
<SidebarNav>
|
|
49
|
+
<SidebarItem icon={<Icon name="grid" />} label="Overview" render={<a href="/" />} />
|
|
50
|
+
<SidebarItem icon={<Icon name="image-stack" />} label="Editions" render={<a href="/editions" />} />
|
|
51
|
+
<SidebarItem icon={<Icon name="user" />} label="Collectors" render={<a href="/collectors" />} />
|
|
52
|
+
<SidebarItem icon={<Icon name="bell" />} label="Activity" render={<a href="/activity" />} />
|
|
53
|
+
<SidebarItem icon={<Icon name="gear" />} label="Settings" active render={<a href="/settings" />} />
|
|
54
|
+
</SidebarNav>
|
|
55
|
+
</Sidebar>
|
|
56
|
+
}
|
|
57
|
+
>
|
|
58
|
+
{/* No `as="main"`: AppShell already renders <main id="main-content">. */}
|
|
59
|
+
<Page max="content" gap={4} className="py-6">
|
|
60
|
+
<PageHeader
|
|
61
|
+
title="Settings"
|
|
62
|
+
description="Your profile, notifications, and account controls."
|
|
63
|
+
/>
|
|
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>
|
|
117
|
+
</Page>
|
|
118
|
+
</AppShell>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
```
|