@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,58 +1,101 @@
1
1
  # DashboardPage
2
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.
3
+ App-shell creator dashboard: a branded Sidebar with an account footer (AppHeader + BottomNav below lg), a KPI row, an edge-to-edge editions table with sell-through Meters, and supporting payout, collector, release and activity cards — the default shape for an operator or analytics home route.
4
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).
5
+ **Intent:** Reach for this when a route's job is monitoring or managing many records (sales, orders, editions, posts) with a few headline numbers. Not for reading-focused routes (use FeedPage) or single-record routes (use ListDetailPage's detail side).
6
6
 
7
7
  ## Notes
8
8
 
9
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.
10
+ - Hierarchy runs headline → records → context: KPI row, then the primary table beside its two closest companions (payout, top collectors), then the lower-priority lists (upcoming, activity). Every row of the grid is filled so the route ends on content, not empty canvas.
11
+ - Stat cards: CardHeader carries the label (CardDescription) and the value; KPI values are `text-mono-xl` — the metric mono tier, tabular figures — never a heading tier. The trend Badge sits with its comparison note in CardContent, so the value keeps the full card width when the row folds to 2×2 on a phone.
12
+ - Stat cards use an explicit cols map (2 → 4) — a 4-stat row must fold to 2×2, never strand an orphan the way auto-fit can.
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
+ - 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
+ - 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 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.
15
17
  - The page has exactly one heading-1 (PageHeader); card titles are Title tier, not headings.
16
18
  - 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.
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.
18
20
 
19
21
  ## Accessibility
20
22
 
21
23
  - AppShell provides the skip link and <main id="main-content"> landmark.
22
24
  - The active SidebarItem and BottomNavItem carry aria-current via `active`.
23
25
  - DataTable headers are real <th>; sortable columns are announced.
26
+ - Each sell-through Meter has an aria-label naming the edition and aria-valuetext reading "184 of 250 sold".
27
+ - Avatars beside a visible name use an empty alt so the name is not read twice.
24
28
 
25
29
  ## Source
26
30
 
27
31
  ```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"
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
+
34
+ // Edition status → Badge tone. Sable has no status vocabulary of its own;
35
+ // an app maps ITS states onto tones once, here, and every badge follows.
36
+ const STATUS_TONE = { live: "success", scheduled: "info", "sold-out": "edition", closed: "neutral" } as const
37
+ const STATUS_LABEL = { live: "Live", scheduled: "Scheduled", "sold-out": "Sold out", closed: "Closed" } as const
38
+ type EditionStatus = keyof typeof STATUS_TONE
29
39
 
30
40
  const MIN = 60 * 1000
31
41
  const HOUR = 60 * MIN
42
+ const DAY = 24 * HOUR
43
+
44
+ const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 })
45
+ const usdCents = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" })
46
+ const shortDate = (offset: number) =>
47
+ new Date(Date.now() + offset).toLocaleDateString("en-US", { month: "short", day: "numeric" })
32
48
 
33
49
  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" },
50
+ { label: "Revenue", value: "$18,240", delta: "+8.2%", up: true, note: "vs. $16,857" },
51
+ { label: "Collects", value: "1,284", delta: "+3.1%", up: true, note: "182 first-time" },
52
+ { label: "Collectors", value: "312", delta: "+12.4%", up: true, note: "Best month yet" },
53
+ { label: "Sell-through", value: "64%", delta: "−1.8%", up: false, note: "6 open editions" },
38
54
  ]
39
55
 
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 },
56
+ const editions = [
57
+ { id: "ed_01", title: "Gold Rush", status: "live" as const, sold: 184, supply: 250, price: 48, released: -6 * DAY },
58
+ { id: "ed_02", title: "Quiet Harbor", status: "sold-out" as const, sold: 100, supply: 100, price: 120, released: -19 * DAY },
59
+ { id: "ed_03", title: "Dusk, no. 4", status: "live" as const, sold: 61, supply: 150, price: 64, released: -2 * DAY },
60
+ { id: "ed_04", title: "Tidewater", status: "live" as const, sold: 212, supply: 300, price: 36, released: -27 * DAY },
61
+ { id: "ed_05", title: "Salt Study", status: "closed" as const, sold: 38, supply: 75, price: 90, released: -44 * DAY },
62
+ { id: "ed_06", title: "Paper Moon", status: "sold-out" as const, sold: 50, supply: 50, price: 220, released: -58 * DAY },
63
+ { id: "ed_07", title: "Night Signal", status: "scheduled" as const, sold: 0, supply: 200, price: 56, released: 3 * DAY },
64
+ { id: "ed_08", title: "Low Tide, Blue", status: "closed" as const, sold: 72, supply: 120, price: 42, released: -71 * DAY },
65
+ { id: "ed_09", title: "Ember Field", status: "sold-out" as const, sold: 80, supply: 80, price: 140, released: -86 * DAY },
66
+ ]
67
+ const editionCount = 24
68
+
69
+ const payout = {
70
+ amount: 8214.6,
71
+ date: 5 * DAY,
72
+ lines: [
73
+ { label: "Sales", value: 9127.33 },
74
+ { label: "Platform fee (10%)", value: -912.73 },
75
+ { label: "Deposits to", value: "Checking ••4821" },
76
+ ],
77
+ }
78
+
79
+ const collectors = [
80
+ { id: "co_01", name: "Nina Simone", handle: "@nina", initials: "NS", pieces: 14, spent: 1460 },
81
+ { id: "co_02", name: "Grace Hopper", handle: "@grace", initials: "GH", pieces: 11, spent: 1128 },
82
+ { id: "co_03", name: "Frida Kahlo", handle: "@frida", initials: "FK", pieces: 9, spent: 942 },
83
+ { id: "co_04", name: "Mary Shelley", handle: "@mary", initials: "MS", pieces: 6, spent: 588 },
84
+ ]
85
+
86
+ const upcoming = [
87
+ { id: "up_01", title: "Night Signal", detail: "Edition of 200 · $56", at: 3 * DAY },
88
+ { id: "up_02", title: "Tidewater, second printing", detail: "Edition of 100 · $40", at: 12 * DAY },
89
+ { id: "up_03", title: "Dusk, no. 5", detail: "Edition of 150 · $64", at: 26 * DAY },
90
+ { id: "up_04", title: "Quiet Harbor, artist proofs", detail: "Edition of 10 · $480", at: 34 * DAY },
48
91
  ]
49
92
 
50
93
  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 },
94
+ { id: "ac_01", who: "Nina Simone", initials: "NS", what: "collected Gold Rush · 185 of 250", when: 2 * MIN },
95
+ { id: "ac_02", who: "Grace Hopper", initials: "GH", what: "collected Dusk, no. 4 · 61 of 150", when: 26 * MIN },
96
+ { id: "ac_03", who: "Frida Kahlo", initials: "FK", what: "tipped $20 on Tidewater", when: 1 * HOUR },
97
+ { id: "ac_04", who: "Mary Shelley", initials: "MS", what: "started following you", when: 3 * HOUR },
98
+ { id: "ac_05", who: "Nina Simone", initials: "NS", what: "set a reminder for Night Signal", when: 6 * HOUR },
56
99
  ]
57
100
 
58
101
  export function DashboardPage() {
@@ -60,7 +103,17 @@ export function DashboardPage() {
60
103
  <AppShell
61
104
  // Sidebar hides below lg, so the same destinations ride in the shell's
62
105
  // mobile slots — without them the route has no navigation under 1024px.
63
- header={<AppHeader title={<span className="text-title-sm">Atelier</span>} />}
106
+ header={
107
+ <AppHeader
108
+ title={
109
+ <Row gap={1} align="center">
110
+ <Icon name="gem" variant="solid" className="text-xl" />
111
+ <span className="text-title-sm">Atelier</span>
112
+ </Row>
113
+ }
114
+ actions={<Avatar size="sm" fallback="AL" alt="Ada Lovelace" />}
115
+ />
116
+ }
64
117
  bottomNav={
65
118
  <BottomNav aria-label="Primary">
66
119
  <BottomNavItem icon={<Icon name="grid" />} label="Overview" showLabel active render={<a href="/" />} />
@@ -73,90 +126,248 @@ export function DashboardPage() {
73
126
  sidebar={
74
127
  <Sidebar>
75
128
  <SidebarHeader>
76
- <span className="text-title-sm">Atelier</span>
129
+ <Row gap={1} align="center" className="px-3">
130
+ <Icon name="gem" variant="solid" className="text-xl" />
131
+ <span className="text-title-sm">Atelier</span>
132
+ </Row>
77
133
  </SidebarHeader>
78
134
  <SidebarNav>
79
135
  <SidebarItem icon={<Icon name="grid" />} label="Overview" active render={<a href="/" />} />
80
136
  <SidebarItem icon={<Icon name="image-stack" />} label="Editions" render={<a href="/editions" />} />
81
137
  <SidebarItem icon={<Icon name="user" />} label="Collectors" render={<a href="/collectors" />} />
82
- <SidebarItem icon={<Icon name="bell" />} label="Activity" render={<a href="/activity" />} />
138
+ <SidebarItem
139
+ icon={<Icon name="bell" />}
140
+ label="Activity"
141
+ badge={<NotificationBadge count={4} tone="neutral" />}
142
+ render={<a href="/activity" />}
143
+ />
83
144
  <SidebarItem icon={<Icon name="gear" />} label="Settings" render={<a href="/settings" />} />
84
145
  </SidebarNav>
146
+ <SidebarFooter>
147
+ <Entity
148
+ className="[--entity-py:0]"
149
+ leading={<Avatar size="sm" fallback="AL" alt="" />}
150
+ title="Ada Lovelace"
151
+ subtitle="@ada"
152
+ action={
153
+ <OverflowMenu label="Account" size="icon-sm">
154
+ <MenuItem render={<a href="/@ada" />}>View profile</MenuItem>
155
+ <MenuItem render={<a href="/settings" />}>Settings</MenuItem>
156
+ <MenuSeparator />
157
+ <MenuItem>Sign out</MenuItem>
158
+ </OverflowMenu>
159
+ }
160
+ />
161
+ </SidebarFooter>
85
162
  </Sidebar>
86
163
  }
87
164
  >
88
165
  <Page max="wide" gap={4} className="py-6">
89
166
  <PageHeader
90
167
  title="Overview"
91
- description="Sales, collects, and edition activity for the last 30 days."
168
+ description="Sales and collector activity for the last 30 days."
92
169
  actions={
93
170
  <Row gap={2}>
94
- <Button variant="outline">Export</Button>
95
- <Button>New edition</Button>
171
+ <Button variant="outline">
172
+ <Icon name="download" />
173
+ Export
174
+ </Button>
175
+ <Button>
176
+ <Icon name="plus" />
177
+ New edition
178
+ </Button>
96
179
  </Row>
97
180
  }
98
181
  />
99
- <Grid cols={{ base: 1, sm: 2, xl: 4 }} gap={3}>
182
+ <Grid cols={{ base: 2, xl: 4 }} gap={3}>
100
183
  {stats.map((stat) => (
101
184
  <Card key={stat.label}>
102
185
  <CardHeader>
103
186
  <CardDescription>{stat.label}</CardDescription>
104
187
  <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
188
  </CardHeader>
112
189
  <CardContent>
113
- <p className="text-caption text-muted-foreground">{stat.note}</p>
190
+ <Row gap={1} wrap>
191
+ <Badge
192
+ tone={stat.up ? "success" : "warning"}
193
+ size="sm"
194
+ icon={<Icon name={stat.up ? "arrow-up" : "arrow-down"} />}
195
+ >
196
+ {stat.delta}
197
+ </Badge>
198
+ <span className="text-caption text-muted-foreground">{stat.note}</span>
199
+ </Row>
114
200
  </CardContent>
115
201
  </Card>
116
202
  ))}
117
203
  </Grid>
118
204
  <Grid cols={{ base: 1, xl: 3 }} gap={3}>
119
- <Card className="xl:col-span-2">
205
+ <Card className="flex flex-col xl:col-span-2">
120
206
  <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>
207
+ <CardTitle>Editions</CardTitle>
208
+ <CardDescription>Sales across your releases, newest first.</CardDescription>
128
209
  </CardHeader>
129
- <CardContent flush>
210
+ <CardContent flush className="mt-4 flex-1">
130
211
  <DataTable
131
- data={deployments}
212
+ data={editions}
132
213
  rowKey={(row) => row.id}
214
+ defaultSort={{ key: "released", direction: "desc" }}
133
215
  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" /> },
216
+ { key: "title", header: "Edition", sortable: true, className: "text-label-md whitespace-nowrap" },
217
+ {
218
+ key: "status",
219
+ header: "Status",
220
+ cell: (row) => (
221
+ <Badge appearance="plain" tone={STATUS_TONE[row.status as EditionStatus]} dot>
222
+ {STATUS_LABEL[row.status as EditionStatus]}
223
+ </Badge>
224
+ ),
225
+ },
226
+ {
227
+ key: "sold",
228
+ header: "Sold",
229
+ sortable: true,
230
+ sortValue: (row) => row.sold / row.supply,
231
+ cell: (row) => (
232
+ <Row gap={3} align="center" className="min-w-40">
233
+ <Meter
234
+ value={row.sold}
235
+ max={row.supply}
236
+ size="sm"
237
+ tone={row.sold === row.supply ? "success" : "primary"}
238
+ aria-label={`${row.title} sold`}
239
+ getAriaValueText={(_, v) => `${v} of ${row.supply} sold`}
240
+ className="flex-1"
241
+ />
242
+ <span className="text-mono-caption whitespace-nowrap text-muted-foreground">
243
+ {row.sold}/{row.supply}
244
+ </span>
245
+ </Row>
246
+ ),
247
+ },
248
+ {
249
+ key: "revenue",
250
+ header: "Revenue",
251
+ align: "end",
252
+ sortable: true,
253
+ sortValue: (row) => row.sold * row.price,
254
+ cell: (row) =>
255
+ row.sold > 0 ? (
256
+ <span className="text-mono-sm">{usd.format(row.sold * row.price)}</span>
257
+ ) : (
258
+ <span className="text-mono-sm text-muted-foreground">—</span>
259
+ ),
260
+ },
261
+ {
262
+ key: "released",
263
+ header: "Release",
264
+ align: "end",
265
+ sortable: true,
266
+ cell: (row) => (
267
+ <span className="text-body-sm whitespace-nowrap text-muted-foreground">{shortDate(row.released)}</span>
268
+ ),
269
+ },
139
270
  ]}
140
271
  />
141
272
  </CardContent>
273
+ <CardFooter className="justify-between">
274
+ <span className="text-caption text-muted-foreground">
275
+ Showing {editions.length} of {editionCount} editions
276
+ </span>
277
+ <Button variant="ghost" size="sm" render={<a href="/editions" />}>
278
+ View all editions
279
+ <Icon name="arrow-right" />
280
+ </Button>
281
+ </CardFooter>
282
+ </Card>
283
+ <Stack gap={3}>
284
+ <Card>
285
+ <CardHeader>
286
+ <CardDescription>Next payout</CardDescription>
287
+ <CardTitle className="text-mono-xl">{usdCents.format(payout.amount)}</CardTitle>
288
+ <CardAction>
289
+ <Badge tone="info" size="sm" icon={<Icon name="calendar" />}>
290
+ {shortDate(payout.date)}
291
+ </Badge>
292
+ </CardAction>
293
+ </CardHeader>
294
+ <CardContent>
295
+ <dl className="sable-list-dl text-body-sm">
296
+ {payout.lines.map((line) => (
297
+ <div key={line.label} className="contents">
298
+ <dt>{line.label}</dt>
299
+ <dd className={typeof line.value === "number" ? "text-end text-mono-sm" : "text-end"}>
300
+ {typeof line.value === "number" ? usdCents.format(line.value) : line.value}
301
+ </dd>
302
+ </div>
303
+ ))}
304
+ </dl>
305
+ </CardContent>
306
+ </Card>
307
+ <Card>
308
+ <CardHeader>
309
+ <CardTitle>Top collectors</CardTitle>
310
+ <CardDescription>By spend this month.</CardDescription>
311
+ <CardAction>
312
+ <Button variant="ghost" size="sm" render={<a href="/collectors" />}>
313
+ View all
314
+ </Button>
315
+ </CardAction>
316
+ </CardHeader>
317
+ <CardList>
318
+ {collectors.map((person) => (
319
+ <Entity
320
+ key={person.id}
321
+ leading={<Avatar size="sm" fallback={person.initials} alt="" />}
322
+ title={person.name}
323
+ subtitle={`${person.handle} · ${person.pieces} pieces`}
324
+ action={<span className="text-mono-sm">{usd.format(person.spent)}</span>}
325
+ />
326
+ ))}
327
+ </CardList>
328
+ </Card>
329
+ </Stack>
330
+ </Grid>
331
+ <Grid cols={{ base: 1, xl: 3 }} gap={3}>
332
+ <Card className="flex flex-col">
333
+ <CardHeader>
334
+ <CardTitle>Upcoming releases</CardTitle>
335
+ <CardDescription>Scheduled to open to collectors.</CardDescription>
336
+ </CardHeader>
337
+ <CardList>
338
+ {upcoming.map((release) => (
339
+ <Entity
340
+ key={release.id}
341
+ title={release.title}
342
+ subtitle={release.detail}
343
+ action={<RelativeTime date={Date.now() + release.at} className="text-caption text-muted-foreground" />}
344
+ />
345
+ ))}
346
+ </CardList>
347
+ <CardFooter className="mt-auto">
348
+ <Button variant="outline" size="sm" render={<a href="/editions/new" />}>
349
+ <Icon name="calendar" />
350
+ Schedule a release
351
+ </Button>
352
+ </CardFooter>
142
353
  </Card>
143
- <Card>
354
+ <Card className="xl:col-span-2">
144
355
  <CardHeader>
145
356
  <CardTitle>Activity</CardTitle>
146
- <CardDescription>Collects, mints, and follows.</CardDescription>
357
+ <CardDescription>Collects, tips, and follows.</CardDescription>
358
+ <CardAction>
359
+ <Button variant="ghost" size="sm" render={<a href="/activity" />}>
360
+ View all
361
+ </Button>
362
+ </CardAction>
147
363
  </CardHeader>
148
364
  <CardList>
149
365
  {activity.map((item) => (
150
366
  <Entity
151
367
  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
- }
368
+ leading={<Avatar size="sm" fallback={item.initials} alt="" />}
369
+ title={item.who}
370
+ subtitle={item.what}
160
371
  action={<RelativeTime date={Date.now() - item.when} className="text-caption text-muted-foreground" />}
161
372
  />
162
373
  ))}