@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.
- package/AGENTS.md +3 -1
- package/CHANGELOG.md +120 -0
- package/CONTRIBUTING.md +8 -0
- package/README.md +1 -0
- package/bin/sable-lint.mjs +0 -0
- package/bin/sable.mjs +68 -26
- package/dist/{_contract-CYScOdf5.d.ts → _contract-o3tUc8Ig.d.ts} +1 -1
- package/dist/blocks/index.d.ts +396 -0
- package/dist/blocks/index.js +1997 -0
- package/dist/chunk-6LCIBMMG.js +4908 -0
- package/dist/{chunk-J23ELQQZ.js → chunk-CKNM7KSB.js} +1664 -4270
- package/dist/chunk-K6LII3OE.js +5093 -0
- package/dist/{chunk-RWP4XBSK.js → chunk-KFZ43RKU.js} +1 -1
- package/dist/{chunk-PAOZBZME.js → chunk-NGBDP3BM.js} +1 -1
- package/dist/{chunk-5Z5KLOAU.js → chunk-TH563WLW.js} +14 -1
- package/dist/{chunk-JI7WJP2L.js → chunk-X276MF2W.js} +5 -0
- package/dist/foundations/index.js +1 -1
- package/dist/icon/index.js +2 -2
- package/dist/icon-sets/reicon.js +1 -1
- package/dist/index.d.ts +624 -133
- package/dist/index.js +767 -4964
- package/dist/ios/index.d.ts +14 -2
- package/dist/ios/index.js +10 -9
- package/dist/ios/preview.d.ts +11 -1
- package/dist/ios/preview.js +14 -9
- package/dist/ios/styles.css +26 -6
- package/dist/{meta-6dUzgC6M.d.ts → meta-CcE_SeFu.d.ts} +37 -1
- package/dist/patterns/index.d.ts +1 -1
- package/dist/patterns/index.js +1684 -471
- package/dist/presets/index.d.ts +1 -1
- package/dist/presets/index.js +1 -1
- package/dist/styles.css +238 -40
- package/dist/{theme-provider-BWcizhAt.d.ts → theme-provider-BE-KqUG-.d.ts} +1 -1
- package/dist/themes/atelier/atelier.css +3 -0
- package/dist/themes/atelier/index.d.ts +1 -1
- package/dist/themes/base/base.css +34 -4
- package/dist/themes/base/index.d.ts +1 -1
- package/dist/themes/ember/ember.css +3 -0
- package/dist/themes/ember/index.d.ts +1 -1
- package/dist/themes/index.d.ts +2 -2
- package/dist/themes/index.js +2 -2
- package/dist/themes/meridian/index.d.ts +1 -1
- package/dist/themes/meridian/meridian.css +10 -5
- package/dist/themes/poppy/index.d.ts +1 -1
- package/dist/themes/poppy/poppy.css +16 -0
- package/dist/themes/prism/index.d.ts +1 -1
- package/dist/themes/prism/prism.css +4 -1
- package/dist/themes/verdant/index.d.ts +1 -1
- package/dist/themes/verdant/verdant.css +7 -2
- package/llms/blocks/api-keys-block.md +79 -0
- package/llms/blocks/chat-thread-block.md +51 -0
- package/llms/blocks/command-palette-block.md +48 -0
- package/llms/blocks/cta-band-block.md +62 -0
- package/llms/blocks/danger-zone-block.md +69 -0
- package/llms/blocks/device-comparison-block.md +91 -0
- package/llms/blocks/event-card-block.md +84 -0
- package/llms/blocks/faq-block.md +72 -0
- package/llms/blocks/feature-grid-block.md +75 -0
- package/llms/blocks/feedback-form-block.md +66 -0
- package/llms/blocks/goal-progress-block.md +54 -0
- package/llms/blocks/hero-block.md +65 -0
- package/llms/blocks/invite-team-block.md +61 -0
- package/llms/blocks/invoice-history-block.md +84 -0
- package/llms/blocks/kpi-sparkline-block.md +57 -0
- package/llms/blocks/media-player-block.md +100 -0
- package/llms/blocks/newsletter-block.md +64 -0
- package/llms/blocks/not-found-block.md +56 -0
- package/llms/blocks/notification-list-block.md +63 -0
- package/llms/blocks/notification-settings-block.md +54 -0
- package/llms/blocks/order-tracking-block.md +62 -0
- package/llms/blocks/pricing-card-block.md +82 -0
- package/llms/blocks/pricing-table-block.md +129 -0
- package/llms/blocks/product-card-block.md +69 -0
- package/llms/blocks/profile-card-block.md +103 -0
- package/llms/blocks/profile-form-block.md +114 -0
- package/llms/blocks/revenue-breakdown-block.md +70 -0
- package/llms/blocks/sign-in-block.md +64 -0
- package/llms/blocks/sign-up-block.md +78 -0
- package/llms/blocks/site-footer-block.md +96 -0
- package/llms/blocks/stat-row-block.md +59 -0
- package/llms/blocks/steps-block.md +73 -0
- package/llms/blocks/task-list-block.md +81 -0
- package/llms/blocks/team-members-block.md +91 -0
- package/llms/blocks/testimonial-block.md +78 -0
- package/llms/blocks/traffic-sources-block.md +63 -0
- package/llms/blocks/traffic-trend-block.md +95 -0
- package/llms/blocks/upload-list-block.md +66 -0
- package/llms/blocks/verify-code-block.md +59 -0
- package/llms/blocks/wallet-block.md +84 -0
- package/llms/components/area-chart.md +112 -0
- package/llms/components/bar-chart.md +131 -0
- package/llms/components/bubble.md +5 -5
- package/llms/components/button.md +1 -1
- package/llms/components/card.md +2 -2
- package/llms/components/checkbox-card.md +1 -1
- package/llms/components/context-card.md +2 -2
- package/llms/components/context-menu.md +1 -1
- package/llms/components/date-tile.md +79 -0
- package/llms/components/dialog.md +1 -1
- package/llms/components/empty-state.md +4 -4
- package/llms/components/entity.md +5 -3
- package/llms/components/fieldset.md +1 -1
- package/llms/components/kbd.md +18 -6
- package/llms/components/line-chart.md +101 -0
- package/llms/components/menu.md +1 -1
- package/llms/components/{dots-menu.md → overflow-menu.md} +9 -9
- package/llms/components/pie-chart.md +103 -0
- package/llms/components/pill.md +1 -1
- package/llms/components/radar-chart.md +106 -0
- package/llms/components/radial-chart.md +102 -0
- package/llms/components/segmented-bar.md +102 -0
- package/llms/components/settings-menu.md +2 -2
- package/llms/components/side-nav.md +1 -1
- package/llms/components/skeleton.md +1 -1
- package/llms/components/sparkline.md +80 -0
- package/llms/components/table.md +1 -1
- package/llms/components/theme-selector.md +1 -1
- package/llms/foundations/shape.md +1 -1
- package/llms/patterns/auth-page.md +132 -42
- package/llms/patterns/brand-landing-page.md +271 -175
- package/llms/patterns/chat-panel.md +120 -39
- package/llms/patterns/dashboard-page.md +276 -65
- package/llms/patterns/feed-page.md +245 -64
- package/llms/patterns/landing-page.md +473 -75
- package/llms/patterns/list-detail-page.md +377 -68
- package/llms/patterns/settings-page.md +268 -67
- package/llms/reference/token-intent.md +4 -2
- package/llms/reference/utilities.md +20 -5
- package/llms/tasks/building-an-ios-prototype.md +4 -1
- package/llms/tasks/choosing-a-component.md +2 -2
- package/llms-full.txt +6428 -892
- package/llms.txt +67 -16
- package/package.json +5 -1
- package/registry.json +3217 -552
- package/targets/ios.json +88 -18
- package/llms/components/description.md +0 -96
- package/dist/{chunk-L6Y46C62.js → chunk-MXJX3NZI.js} +1 -1
|
@@ -1,58 +1,101 @@
|
|
|
1
1
|
# DashboardPage
|
|
2
2
|
|
|
3
|
-
App-shell dashboard:
|
|
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 (
|
|
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
|
-
-
|
|
11
|
-
- Stat cards
|
|
12
|
-
-
|
|
13
|
-
- The
|
|
14
|
-
-
|
|
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: "$
|
|
35
|
-
{ label: "Collects", value: "1,284", delta: "+3.1%", up: true, note: "182
|
|
36
|
-
{ label: "
|
|
37
|
-
{ label: "Sell-through", value: "64%", delta: "−1.8%", up: false, note: "
|
|
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
|
|
41
|
-
{ id: "
|
|
42
|
-
{ id: "
|
|
43
|
-
{ id: "
|
|
44
|
-
{ id: "
|
|
45
|
-
{ id: "
|
|
46
|
-
{ id: "
|
|
47
|
-
{ id: "
|
|
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
|
|
52
|
-
{ id: "ac_02", who: "Grace Hopper", initials: "GH", what: "
|
|
53
|
-
{ id: "ac_03", who: "
|
|
54
|
-
{ id: "ac_04", who: "Mary Shelley", initials: "MS", what: "
|
|
55
|
-
{ id: "ac_05", who: "
|
|
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={
|
|
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
|
-
<
|
|
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
|
|
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
|
|
168
|
+
description="Sales and collector activity for the last 30 days."
|
|
92
169
|
actions={
|
|
93
170
|
<Row gap={2}>
|
|
94
|
-
<Button variant="outline">
|
|
95
|
-
|
|
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:
|
|
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
|
-
<
|
|
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>
|
|
122
|
-
<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={
|
|
212
|
+
data={editions}
|
|
132
213
|
rowKey={(row) => row.id}
|
|
214
|
+
defaultSort={{ key: "released", direction: "desc" }}
|
|
133
215
|
columns={[
|
|
134
|
-
{ key: "
|
|
135
|
-
{
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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,
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
))}
|