@sableui/react 0.1.1 → 0.2.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/CHANGELOG.md +61 -0
- package/dist/{_contract-CYScOdf5.d.ts → _contract-o3tUc8Ig.d.ts} +1 -1
- package/dist/{chunk-J23ELQQZ.js → chunk-AOWBYEQ6.js} +4177 -1917
- package/dist/{chunk-RWP4XBSK.js → chunk-KFZ43RKU.js} +1 -1
- package/dist/{chunk-JI7WJP2L.js → chunk-X276MF2W.js} +5 -0
- package/dist/foundations/index.js +1 -1
- package/dist/index.d.ts +155 -101
- package/dist/index.js +582 -2778
- package/dist/ios/index.d.ts +6 -1
- package/dist/ios/index.js +4 -4
- package/dist/ios/preview.js +1 -1
- package/dist/ios/styles.css +20 -6
- package/dist/patterns/index.js +1683 -469
- 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 +11 -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/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/components/bubble.md +5 -5
- package/llms/components/checkbox-card.md +1 -1
- package/llms/components/context-card.md +2 -2
- package/llms/components/dialog.md +1 -1
- package/llms/components/empty-state.md +4 -4
- package/llms/components/entity.md +1 -1
- package/llms/components/fieldset.md +1 -1
- package/llms/components/kbd.md +18 -6
- package/llms/components/pill.md +1 -1
- package/llms/components/side-nav.md +1 -1
- package/llms/components/skeleton.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 +277 -65
- package/llms/patterns/feed-page.md +245 -64
- package/llms/patterns/landing-page.md +473 -75
- package/llms/patterns/list-detail-page.md +385 -68
- package/llms/patterns/settings-page.md +279 -67
- package/llms/reference/token-intent.md +4 -2
- package/llms/reference/utilities.md +20 -5
- package/llms-full.txt +2244 -727
- package/llms.txt +14 -15
- package/package.json +1 -1
- package/registry.json +261 -238
- package/targets/ios.json +4 -15
- package/llms/components/description.md +0 -96
|
@@ -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 a DotsMenu. The footer rebinds `--foreground` to `--sidebar-foreground`, because Entity's title and the ghost trigger ink with page tokens and a theme may paint the rail dark on a light page; it keeps to a title line, since there is no sidebar-muted ink for a subtitle.
|
|
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, DotsMenu, Entity, Grid, Icon, MenuItem, MenuSeparator, Meter, NotificationBadge, 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,249 @@ 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
|
+
{/* Entity and DotsMenu ink with page tokens; a theme can paint the rail
|
|
147
|
+
dark on a light page, so this subtree reads the rail's ink instead. */}
|
|
148
|
+
<SidebarFooter className="[--foreground:var(--sidebar-foreground)]">
|
|
149
|
+
<Entity
|
|
150
|
+
className="[--entity-py:0]"
|
|
151
|
+
leading={<Avatar size="sm" fallback="AL" alt="" />}
|
|
152
|
+
title="Ada Lovelace"
|
|
153
|
+
action={
|
|
154
|
+
<DotsMenu label="Account" size="icon-sm">
|
|
155
|
+
<MenuItem render={<a href="/@ada" />}>View profile</MenuItem>
|
|
156
|
+
<MenuItem render={<a href="/settings" />}>Settings</MenuItem>
|
|
157
|
+
<MenuSeparator />
|
|
158
|
+
<MenuItem>Sign out</MenuItem>
|
|
159
|
+
</DotsMenu>
|
|
160
|
+
}
|
|
161
|
+
/>
|
|
162
|
+
</SidebarFooter>
|
|
85
163
|
</Sidebar>
|
|
86
164
|
}
|
|
87
165
|
>
|
|
88
166
|
<Page max="wide" gap={4} className="py-6">
|
|
89
167
|
<PageHeader
|
|
90
168
|
title="Overview"
|
|
91
|
-
description="Sales
|
|
169
|
+
description="Sales and collector activity for the last 30 days."
|
|
92
170
|
actions={
|
|
93
171
|
<Row gap={2}>
|
|
94
|
-
<Button variant="outline">
|
|
95
|
-
|
|
172
|
+
<Button variant="outline">
|
|
173
|
+
<Icon name="download" />
|
|
174
|
+
Export
|
|
175
|
+
</Button>
|
|
176
|
+
<Button>
|
|
177
|
+
<Icon name="plus" />
|
|
178
|
+
New edition
|
|
179
|
+
</Button>
|
|
96
180
|
</Row>
|
|
97
181
|
}
|
|
98
182
|
/>
|
|
99
|
-
<Grid cols={{ base:
|
|
183
|
+
<Grid cols={{ base: 2, xl: 4 }} gap={3}>
|
|
100
184
|
{stats.map((stat) => (
|
|
101
185
|
<Card key={stat.label}>
|
|
102
186
|
<CardHeader>
|
|
103
187
|
<CardDescription>{stat.label}</CardDescription>
|
|
104
188
|
<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
189
|
</CardHeader>
|
|
112
190
|
<CardContent>
|
|
113
|
-
<
|
|
191
|
+
<Row gap={1} wrap>
|
|
192
|
+
<Badge
|
|
193
|
+
tone={stat.up ? "success" : "warning"}
|
|
194
|
+
size="sm"
|
|
195
|
+
icon={<Icon name={stat.up ? "arrow-up" : "arrow-down"} />}
|
|
196
|
+
>
|
|
197
|
+
{stat.delta}
|
|
198
|
+
</Badge>
|
|
199
|
+
<span className="text-caption text-muted-foreground">{stat.note}</span>
|
|
200
|
+
</Row>
|
|
114
201
|
</CardContent>
|
|
115
202
|
</Card>
|
|
116
203
|
))}
|
|
117
204
|
</Grid>
|
|
118
205
|
<Grid cols={{ base: 1, xl: 3 }} gap={3}>
|
|
119
|
-
<Card className="xl:col-span-2">
|
|
206
|
+
<Card className="flex flex-col xl:col-span-2">
|
|
120
207
|
<CardHeader>
|
|
121
|
-
<CardTitle>
|
|
122
|
-
<CardDescription>
|
|
123
|
-
<CardAction>
|
|
124
|
-
<Button variant="ghost" size="sm" render={<a href="/deployments" />}>
|
|
125
|
-
View all
|
|
126
|
-
</Button>
|
|
127
|
-
</CardAction>
|
|
208
|
+
<CardTitle>Editions</CardTitle>
|
|
209
|
+
<CardDescription>Sales across your releases, newest first.</CardDescription>
|
|
128
210
|
</CardHeader>
|
|
129
|
-
<CardContent flush>
|
|
211
|
+
<CardContent flush className="mt-4 flex-1">
|
|
130
212
|
<DataTable
|
|
131
|
-
data={
|
|
213
|
+
data={editions}
|
|
132
214
|
rowKey={(row) => row.id}
|
|
215
|
+
defaultSort={{ key: "released", direction: "desc" }}
|
|
133
216
|
columns={[
|
|
134
|
-
{ key: "
|
|
135
|
-
{
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
217
|
+
{ key: "title", header: "Edition", sortable: true, className: "text-label-md whitespace-nowrap" },
|
|
218
|
+
{
|
|
219
|
+
key: "status",
|
|
220
|
+
header: "Status",
|
|
221
|
+
cell: (row) => (
|
|
222
|
+
<Badge appearance="plain" tone={STATUS_TONE[row.status as EditionStatus]} dot>
|
|
223
|
+
{STATUS_LABEL[row.status as EditionStatus]}
|
|
224
|
+
</Badge>
|
|
225
|
+
),
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
key: "sold",
|
|
229
|
+
header: "Sold",
|
|
230
|
+
sortable: true,
|
|
231
|
+
sortValue: (row) => row.sold / row.supply,
|
|
232
|
+
cell: (row) => (
|
|
233
|
+
<Row gap={3} align="center" className="min-w-40">
|
|
234
|
+
<Meter
|
|
235
|
+
value={row.sold}
|
|
236
|
+
max={row.supply}
|
|
237
|
+
size="sm"
|
|
238
|
+
tone={row.sold === row.supply ? "success" : "primary"}
|
|
239
|
+
aria-label={`${row.title} sold`}
|
|
240
|
+
getAriaValueText={(_, v) => `${v} of ${row.supply} sold`}
|
|
241
|
+
className="flex-1"
|
|
242
|
+
/>
|
|
243
|
+
<span className="text-mono-caption whitespace-nowrap text-muted-foreground">
|
|
244
|
+
{row.sold}/{row.supply}
|
|
245
|
+
</span>
|
|
246
|
+
</Row>
|
|
247
|
+
),
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
key: "revenue",
|
|
251
|
+
header: "Revenue",
|
|
252
|
+
align: "end",
|
|
253
|
+
sortable: true,
|
|
254
|
+
sortValue: (row) => row.sold * row.price,
|
|
255
|
+
cell: (row) =>
|
|
256
|
+
row.sold > 0 ? (
|
|
257
|
+
<span className="text-mono-sm">{usd.format(row.sold * row.price)}</span>
|
|
258
|
+
) : (
|
|
259
|
+
<span className="text-mono-sm text-muted-foreground">—</span>
|
|
260
|
+
),
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
key: "released",
|
|
264
|
+
header: "Release",
|
|
265
|
+
align: "end",
|
|
266
|
+
sortable: true,
|
|
267
|
+
cell: (row) => (
|
|
268
|
+
<span className="text-body-sm whitespace-nowrap text-muted-foreground">{shortDate(row.released)}</span>
|
|
269
|
+
),
|
|
270
|
+
},
|
|
139
271
|
]}
|
|
140
272
|
/>
|
|
141
273
|
</CardContent>
|
|
274
|
+
<CardFooter className="justify-between">
|
|
275
|
+
<span className="text-caption text-muted-foreground">
|
|
276
|
+
Showing {editions.length} of {editionCount} editions
|
|
277
|
+
</span>
|
|
278
|
+
<Button variant="ghost" size="sm" render={<a href="/editions" />}>
|
|
279
|
+
View all editions
|
|
280
|
+
<Icon name="arrow-right" />
|
|
281
|
+
</Button>
|
|
282
|
+
</CardFooter>
|
|
283
|
+
</Card>
|
|
284
|
+
<Stack gap={3}>
|
|
285
|
+
<Card>
|
|
286
|
+
<CardHeader>
|
|
287
|
+
<CardDescription>Next payout</CardDescription>
|
|
288
|
+
<CardTitle className="text-mono-xl">{usdCents.format(payout.amount)}</CardTitle>
|
|
289
|
+
<CardAction>
|
|
290
|
+
<Badge tone="info" size="sm" icon={<Icon name="calendar" />}>
|
|
291
|
+
{shortDate(payout.date)}
|
|
292
|
+
</Badge>
|
|
293
|
+
</CardAction>
|
|
294
|
+
</CardHeader>
|
|
295
|
+
<CardContent>
|
|
296
|
+
<dl className="sable-list-dl text-body-sm">
|
|
297
|
+
{payout.lines.map((line) => (
|
|
298
|
+
<div key={line.label} className="contents">
|
|
299
|
+
<dt>{line.label}</dt>
|
|
300
|
+
<dd className={typeof line.value === "number" ? "text-end text-mono-sm" : "text-end"}>
|
|
301
|
+
{typeof line.value === "number" ? usdCents.format(line.value) : line.value}
|
|
302
|
+
</dd>
|
|
303
|
+
</div>
|
|
304
|
+
))}
|
|
305
|
+
</dl>
|
|
306
|
+
</CardContent>
|
|
307
|
+
</Card>
|
|
308
|
+
<Card>
|
|
309
|
+
<CardHeader>
|
|
310
|
+
<CardTitle>Top collectors</CardTitle>
|
|
311
|
+
<CardDescription>By spend this month.</CardDescription>
|
|
312
|
+
<CardAction>
|
|
313
|
+
<Button variant="ghost" size="sm" render={<a href="/collectors" />}>
|
|
314
|
+
View all
|
|
315
|
+
</Button>
|
|
316
|
+
</CardAction>
|
|
317
|
+
</CardHeader>
|
|
318
|
+
<CardList>
|
|
319
|
+
{collectors.map((person) => (
|
|
320
|
+
<Entity
|
|
321
|
+
key={person.id}
|
|
322
|
+
leading={<Avatar size="sm" fallback={person.initials} alt="" />}
|
|
323
|
+
title={person.name}
|
|
324
|
+
subtitle={`${person.handle} · ${person.pieces} pieces`}
|
|
325
|
+
action={<span className="text-mono-sm">{usd.format(person.spent)}</span>}
|
|
326
|
+
/>
|
|
327
|
+
))}
|
|
328
|
+
</CardList>
|
|
329
|
+
</Card>
|
|
330
|
+
</Stack>
|
|
331
|
+
</Grid>
|
|
332
|
+
<Grid cols={{ base: 1, xl: 3 }} gap={3}>
|
|
333
|
+
<Card className="flex flex-col">
|
|
334
|
+
<CardHeader>
|
|
335
|
+
<CardTitle>Upcoming releases</CardTitle>
|
|
336
|
+
<CardDescription>Scheduled to open to collectors.</CardDescription>
|
|
337
|
+
</CardHeader>
|
|
338
|
+
<CardList>
|
|
339
|
+
{upcoming.map((release) => (
|
|
340
|
+
<Entity
|
|
341
|
+
key={release.id}
|
|
342
|
+
title={release.title}
|
|
343
|
+
subtitle={release.detail}
|
|
344
|
+
action={<RelativeTime date={Date.now() + release.at} className="text-caption text-muted-foreground" />}
|
|
345
|
+
/>
|
|
346
|
+
))}
|
|
347
|
+
</CardList>
|
|
348
|
+
<CardFooter className="mt-auto">
|
|
349
|
+
<Button variant="outline" size="sm" render={<a href="/editions/new" />}>
|
|
350
|
+
<Icon name="calendar" />
|
|
351
|
+
Schedule a release
|
|
352
|
+
</Button>
|
|
353
|
+
</CardFooter>
|
|
142
354
|
</Card>
|
|
143
|
-
<Card>
|
|
355
|
+
<Card className="xl:col-span-2">
|
|
144
356
|
<CardHeader>
|
|
145
357
|
<CardTitle>Activity</CardTitle>
|
|
146
|
-
<CardDescription>Collects,
|
|
358
|
+
<CardDescription>Collects, tips, and follows.</CardDescription>
|
|
359
|
+
<CardAction>
|
|
360
|
+
<Button variant="ghost" size="sm" render={<a href="/activity" />}>
|
|
361
|
+
View all
|
|
362
|
+
</Button>
|
|
363
|
+
</CardAction>
|
|
147
364
|
</CardHeader>
|
|
148
365
|
<CardList>
|
|
149
366
|
{activity.map((item) => (
|
|
150
367
|
<Entity
|
|
151
368
|
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
|
-
}
|
|
369
|
+
leading={<Avatar size="sm" fallback={item.initials} alt="" />}
|
|
370
|
+
title={item.who}
|
|
371
|
+
subtitle={item.what}
|
|
160
372
|
action={<RelativeTime date={Date.now() - item.when} className="text-caption text-muted-foreground" />}
|
|
161
373
|
/>
|
|
162
374
|
))}
|