@sableui/react 0.9.0 → 0.10.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 +1 -1
- package/CHANGELOG.md +124 -0
- package/CONTRIBUTING.md +2 -2
- package/DESIGN.md +55 -16
- package/README.md +12 -0
- package/dist/{_contract-C2M1SjBf.d.ts → _contract-CkqXDVww.d.ts} +25 -2
- package/dist/blocks/danger-zone-block.js +3 -3
- package/dist/blocks/danger-zone-block.meta.js +1 -1
- package/dist/blocks/faq-block.js +3 -3
- package/dist/blocks/goal-progress-block.js +1 -1
- package/dist/blocks/notification-list-block.js +1 -1
- package/dist/blocks/pricing-table-block.js +12 -12
- package/dist/blocks/product-card-block.js +3 -3
- package/dist/blocks/product-card-block.meta.js +3 -3
- package/dist/blocks/profile-card-block.js +1 -1
- package/dist/blocks/profile-form-block.js +3 -3
- package/dist/blocks/revenue-breakdown-block.js +5 -5
- package/dist/blocks/stat-row-block.js +3 -3
- package/dist/blocks/task-list-block.js +2 -2
- package/dist/blocks/team-members-block.js +1 -1
- package/dist/blocks/wallet-block.js +4 -4
- package/dist/blocks/wallet-block.meta.js +3 -3
- package/dist/components/accordion/accordion.meta.js +2 -2
- package/dist/components/app-header/app-header.meta.js +1 -1
- package/dist/components/app-shell/app-shell.meta.js +2 -2
- package/dist/components/area-chart/area-chart.meta.js +3 -3
- package/dist/components/badge/badge.js +13 -4
- package/dist/components/badge/badge.meta.js +24 -13
- package/dist/components/badge/badge.variants.js +37 -23
- package/dist/components/banner/banner.meta.js +8 -8
- package/dist/components/banner/banner.variants.js +3 -3
- package/dist/components/bar-chart/bar-chart.meta.js +4 -4
- package/dist/components/breadcrumbs/breadcrumbs.meta.js +1 -1
- package/dist/components/button/button.js +13 -2
- package/dist/components/button/button.meta.js +3 -3
- package/dist/components/card/card.js +16 -7
- package/dist/components/card/card.meta.js +4 -4
- package/dist/components/checkbox-card/checkbox-card.meta.js +10 -10
- package/dist/components/checkbox-card/checkbox-card.variants.js +15 -15
- package/dist/components/code-block/code-block.meta.js +1 -1
- package/dist/components/command-menu/command-menu.js +2 -0
- package/dist/components/command-menu/command-menu.meta.js +1 -1
- package/dist/components/context-card/context-card.meta.js +2 -2
- package/dist/components/destructive-action-modal/destructive-action-modal.meta.js +1 -1
- package/dist/components/dialog/dialog.meta.js +2 -2
- package/dist/components/disclosure-picker/disclosure-picker.js +48 -2
- package/dist/components/disclosure-picker/disclosure-picker.meta.js +3 -2
- package/dist/components/empty-state/empty-state.meta.js +4 -4
- package/dist/components/form/form.js +10 -5
- package/dist/components/gradient-art/gradient-art.meta.js +3 -3
- package/dist/components/label/label.meta.js +1 -1
- package/dist/components/line-chart/line-chart.meta.js +3 -3
- package/dist/components/meter/meter.js +1 -1
- package/dist/components/meter/meter.meta.js +2 -2
- package/dist/components/meter/meter.variants.js +1 -1
- package/dist/components/middle-truncate/middle-truncate.meta.js +4 -4
- package/dist/components/note/note.meta.js +3 -3
- package/dist/components/page-header/page-header.js +1 -1
- package/dist/components/pagination/pagination.meta.js +2 -2
- package/dist/components/phone/phone.meta.js +1 -1
- package/dist/components/pie-chart/pie-chart.meta.js +3 -3
- package/dist/components/pill/pill.js +4 -2
- package/dist/components/pill/pill.meta.js +19 -2
- package/dist/components/pill/pill.variants.js +23 -2
- package/dist/components/popover/popover.meta.js +2 -2
- package/dist/components/radar-chart/radar-chart.meta.js +3 -3
- package/dist/components/radial-chart/radial-chart.meta.js +3 -3
- package/dist/components/radio/radio.meta.js +2 -2
- package/dist/components/radio-card/radio-card.meta.js +6 -6
- package/dist/components/radio-card/radio-card.variants.js +15 -15
- package/dist/components/scroller/scroller.meta.js +1 -1
- package/dist/components/search-input/search-input.meta.js +1 -1
- package/dist/components/section-header/section-header.js +1 -1
- package/dist/components/segmented-bar/segmented-bar.js +1 -1
- package/dist/components/segmented-bar/segmented-bar.meta.js +6 -6
- package/dist/components/segmented-bar/segmented-bar.variants.js +4 -4
- package/dist/components/select/select.meta.js +2 -2
- package/dist/components/sheet/sheet.js +15 -1
- package/dist/components/sheet/sheet.meta.js +1 -1
- package/dist/components/side-nav/side-nav.meta.js +1 -1
- package/dist/components/sidebar/sidebar.meta.js +1 -1
- package/dist/components/sparkline/sparkline.meta.js +4 -4
- package/dist/components/spinner/spinner.meta.js +1 -1
- package/dist/components/status-dot/status-dot.js +4 -1
- package/dist/components/status-dot/status-dot.meta.js +8 -16
- package/dist/components/status-dot/status-dot.variants.js +13 -4
- package/dist/components/stepper/stepper.js +11 -2
- package/dist/components/table/table.meta.js +4 -4
- package/dist/components/tabs/tabs.meta.js +1 -1
- package/dist/components/textarea/textarea.meta.js +2 -2
- package/dist/components/toast/toast.js +1 -0
- package/dist/components/toast/toast.meta.js +1 -1
- package/dist/components/tooltip/tooltip.meta.js +2 -2
- package/dist/file-hashes.json +127 -124
- package/dist/foundations/spacing.js +8 -8
- package/dist/index.d.ts +138 -73
- package/dist/index.js +12 -1
- package/dist/lib/chart/types.js +3 -3
- package/dist/lib/is-promise.js +5 -0
- package/dist/lib/palette.js +103 -0
- package/dist/lib/selection-card.js +4 -4
- package/dist/palette.css +669 -0
- package/dist/patterns/auth-page.js +6 -6
- package/dist/patterns/auth-page.meta.js +3 -3
- package/dist/patterns/brand-landing-page.js +2 -2
- package/dist/patterns/chat-panel.js +2 -2
- package/dist/patterns/dashboard-page.js +71 -71
- package/dist/patterns/dashboard-page.meta.js +5 -5
- package/dist/patterns/feed-page.js +25 -25
- package/dist/patterns/feed-page.meta.js +3 -3
- package/dist/patterns/landing-page.js +1 -1
- package/dist/patterns/list-detail-page.js +17 -17
- package/dist/patterns/settings-page.js +17 -17
- package/dist/presets/index.d.ts +1 -1
- package/dist/presets/index.js +17 -2
- package/dist/styles.css +163 -55
- package/dist/{theme-provider-S0NVJs2Y.d.ts → theme-provider-CdSicAad.d.ts} +1 -1
- package/dist/themes/_contract.js +22 -8
- package/dist/themes/atelier/atelier.css +13 -13
- package/dist/themes/atelier/index.d.ts +1 -1
- package/dist/themes/ember/ember.css +13 -13
- 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 +13 -13
- package/dist/themes/poppy/index.d.ts +1 -1
- package/dist/themes/poppy/poppy.css +19 -16
- package/dist/themes/prism/index.d.ts +1 -1
- package/dist/themes/prism/prism.css +18 -15
- package/dist/themes/sable/index.d.ts +1 -1
- package/dist/themes/sable/sable.css +18 -22
- package/dist/themes/verdant/index.d.ts +1 -1
- package/dist/themes/verdant/verdant.css +15 -15
- package/llms/blocks/danger-zone-block.md +4 -4
- package/llms/blocks/faq-block.md +3 -3
- package/llms/blocks/goal-progress-block.md +1 -1
- package/llms/blocks/notification-list-block.md +1 -1
- package/llms/blocks/pricing-table-block.md +12 -12
- package/llms/blocks/product-card-block.md +6 -6
- package/llms/blocks/profile-card-block.md +1 -1
- package/llms/blocks/profile-form-block.md +3 -3
- package/llms/blocks/revenue-breakdown-block.md +5 -5
- package/llms/blocks/stat-row-block.md +3 -3
- package/llms/blocks/task-list-block.md +2 -2
- package/llms/blocks/team-members-block.md +1 -1
- package/llms/blocks/wallet-block.md +7 -7
- package/llms/components/accordion.md +2 -2
- package/llms/components/app-header.md +1 -1
- package/llms/components/app-shell.md +2 -2
- package/llms/components/area-chart.md +1 -1
- package/llms/components/badge.md +15 -7
- package/llms/components/banner.md +6 -6
- package/llms/components/bar-chart.md +2 -2
- package/llms/components/breadcrumbs.md +1 -1
- package/llms/components/button.md +3 -3
- package/llms/components/card.md +4 -4
- package/llms/components/checkbox-card.md +5 -5
- package/llms/components/code-block.md +1 -1
- package/llms/components/command-menu.md +1 -1
- package/llms/components/context-card.md +2 -2
- package/llms/components/destructive-action-modal.md +1 -1
- package/llms/components/dialog.md +2 -2
- package/llms/components/disclosure-picker.md +2 -1
- package/llms/components/empty-state.md +4 -4
- package/llms/components/gradient-art.md +3 -3
- package/llms/components/label.md +1 -1
- package/llms/components/line-chart.md +1 -1
- package/llms/components/meter.md +2 -2
- package/llms/components/middle-truncate.md +5 -5
- package/llms/components/note.md +3 -3
- package/llms/components/pagination.md +2 -2
- package/llms/components/phone.md +1 -1
- package/llms/components/pie-chart.md +1 -1
- package/llms/components/pill.md +12 -2
- package/llms/components/popover.md +2 -2
- package/llms/components/radar-chart.md +1 -1
- package/llms/components/radial-chart.md +1 -1
- package/llms/components/radio-card.md +4 -4
- package/llms/components/radio.md +2 -2
- package/llms/components/scroller.md +1 -1
- package/llms/components/search-input.md +1 -1
- package/llms/components/segmented-bar.md +7 -7
- package/llms/components/select.md +2 -2
- package/llms/components/sheet.md +1 -1
- package/llms/components/side-nav.md +1 -1
- package/llms/components/sidebar.md +1 -1
- package/llms/components/sparkline.md +2 -2
- package/llms/components/spinner.md +1 -1
- package/llms/components/status-dot.md +4 -4
- package/llms/components/table.md +8 -8
- package/llms/components/tabs.md +1 -1
- package/llms/components/textarea.md +2 -2
- package/llms/components/toast.md +1 -1
- package/llms/components/tooltip.md +2 -2
- package/llms/foundations/spacing.md +7 -7
- package/llms/layout/grid.md +1 -1
- package/llms/layout/page.md +1 -1
- package/llms/layout/region.md +1 -1
- package/llms/layout/split-pane.md +1 -1
- package/llms/patterns/auth-page.md +11 -11
- package/llms/patterns/brand-landing-page.md +2 -2
- package/llms/patterns/chat-panel.md +2 -2
- package/llms/patterns/dashboard-page.md +78 -78
- package/llms/patterns/feed-page.md +31 -31
- package/llms/patterns/landing-page.md +1 -1
- package/llms/patterns/list-detail-page.md +18 -18
- package/llms/patterns/settings-page.md +18 -18
- package/llms/reference/token-intent.md +25 -11
- package/llms/reference/utilities.md +1 -1
- package/llms/tasks/adding-a-component.md +4 -1
- package/llms/tasks/building-a-page.md +40 -0
- package/llms/tasks/building-with-sable.md +4 -0
- package/llms/tasks/converting-an-app.md +3 -1
- package/llms/tasks/using-presets.md +4 -0
- package/llms-full.txt +433 -347
- package/llms.txt +7 -7
- package/package.json +4 -4
- package/registry.json +369 -233
- package/scripts/lint-rules.mjs +26 -5
- package/scripts/sources.mjs +4 -1
- package/skills/sable/SKILL.md +14 -3
|
@@ -73,7 +73,7 @@ Use Tooltip for brief nonessential hints tied to a specific control or piece of
|
|
|
73
73
|
```tsx
|
|
74
74
|
import { Button, Icon, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@sableui/react"
|
|
75
75
|
|
|
76
|
-
<TooltipProvider><Tooltip><TooltipTrigger render={<Button variant="ghost" size="icon-sm" aria-label="Info"><Icon name="info" /></Button>} /><TooltipContent>
|
|
76
|
+
<TooltipProvider><Tooltip><TooltipTrigger render={<Button variant="ghost" size="icon-sm" aria-label="Info"><Icon name="info" /></Button>} /><TooltipContent>Saved to your workspace.</TooltipContent></Tooltip></TooltipProvider>
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
### Sides
|
|
@@ -89,5 +89,5 @@ import { Button, Tooltip, TooltipContent, TooltipTrigger } from "@sableui/react"
|
|
|
89
89
|
```tsx
|
|
90
90
|
import { Button, Icon, SimpleTooltip } from "@sableui/react"
|
|
91
91
|
|
|
92
|
-
<SimpleTooltip content="
|
|
92
|
+
<SimpleTooltip content="Saved to your workspace."><Button variant="ghost" size="icon-sm" aria-label="Info"><Icon name="info" /></Button></SimpleTooltip>
|
|
93
93
|
```
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Spacing & sizing
|
|
2
2
|
|
|
3
|
-
One
|
|
3
|
+
One scale, derived from the type: the baseline is one line of body text, split into steps every gap, inset and line height lands on. Space is assigned by role. Controls read a height ladder that a density scope can retune all at once.
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Baseline from the type · 8px steps · 3 roles · 1 control ladder**
|
|
6
6
|
|
|
7
7
|
## Spacing by role
|
|
8
8
|
|
|
@@ -11,8 +11,8 @@ Steps below 2 (8px) are optical correction only.
|
|
|
11
11
|
| Name | Value | Use |
|
|
12
12
|
|---|---|---|
|
|
13
13
|
| `Inset p-4` | 16px | Padding inside a surface. p-3 for a dense card, px-3 py-2 for a row. |
|
|
14
|
-
| `Stack, within a group gap-
|
|
15
|
-
| `Stack, between groups gap-4 – gap-6` | 16–24px | Field to field, card to card. |
|
|
14
|
+
| `Stack, within a group gap-2` | 1 step · 8px | Label to field, title to description. |
|
|
15
|
+
| `Stack, between groups gap-4 – gap-6` | 2–3 steps · 16–24px | Field to field, card to card. |
|
|
16
16
|
| `Stack, between sections gap-12+` | 48px+ | Major regions of a page. |
|
|
17
17
|
| `Inline gap-2 – gap-3` | 8–12px | Icon to label, control to control. |
|
|
18
18
|
|
|
@@ -38,8 +38,8 @@ Comfortable values; data-density="compact" (or <Density>) redefines every step f
|
|
|
38
38
|
|
|
39
39
|
## Rules
|
|
40
40
|
|
|
41
|
-
1. **Every margin, padding and gap is a step on the
|
|
42
|
-
2. **Padding, gaps and insets in a component sit on the spacing scale: a step (p-4, gap-2) or --spacing(n) inside an arbitrary value (p-[var(--card-p,--spacing(6))]).**
|
|
41
|
+
1. **Every margin, padding and gap is a step on the scale, and vertical space between blocks is whole baseline steps: an even Tailwind step (2, 4, 6, 8, 12) or a Stack/Row/Grid gap number.** The baseline is one line of body text split into steps; line heights round to it. An odd step (3, 5) is half a baseline step. It is a rhythm to keep, not a grid to force: components stay as tall as their content.
|
|
42
|
+
2. **Padding, gaps and insets in a component sit on the spacing scale: a step (p-4, gap-2) or --spacing(n) inside an arbitrary value (p-[var(--card-p,--spacing(6))]).** --spacing derives from the baseline step and the density (Character) control moves the leading it comes from, so anything on the scale moves with it. A literal p-[1.5rem] or padding: "12px" stays put while everything around it tightens. Keep fixed rem/px for what must not scale: a 1px hairline, a ring width. *Enforced by design-lint: fixed-spacing (library source).*
|
|
43
43
|
3. **Related things sit closer than unrelated things.** Proximity groups content without a divider line. Use space first; reach for a border only when space is not enough.
|
|
44
44
|
4. **Space lives on the container (gap), not on children (margins).** gap survives reordering, conditional children and RTL; per-child margins do not.
|
|
45
45
|
5. **Control heights read the --control-* ladder.** A density scope retunes every control at once. A hardcoded height has to be found and overridden by hand. *Enforced by sable-lint: raw-control-height.*
|
|
@@ -77,6 +77,6 @@ Every control, menu and popup inside retunes, including portaled overlays.
|
|
|
77
77
|
<Stack gap={3}> between groups (24px), <Stack gap={1}> inside a group (8px)
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
Spacing steps do the work of every divider, with less ink. Stack's numeric gap counts --rhythm-unit — one baseline step — so it follows the type and the density control like every other spacing value.
|
|
81
81
|
|
|
82
82
|
**Related:** `sable docs shape` · `sable docs typography` · `sable docs building-a-tool` · `sable token layout`
|
package/llms/layout/grid.md
CHANGED
|
@@ -64,5 +64,5 @@ import { Card, CardDescription, CardHeader, Grid } from "@sableui/react"
|
|
|
64
64
|
```tsx
|
|
65
65
|
import { Badge, Grid } from "@sableui/react"
|
|
66
66
|
|
|
67
|
-
<Grid cols={{ base: 1, md: 2, lg: 4 }}><Badge appearance="plain" tone="
|
|
67
|
+
<Grid cols={{ base: 1, md: 2, lg: 4 }}><Badge appearance="plain" tone="3" dot /><Badge appearance="plain" tone="warning" dot /><Badge appearance="plain" tone="neutral" dot /><Badge appearance="plain" tone="destructive" dot /></Grid>
|
|
68
68
|
```
|
package/llms/layout/page.md
CHANGED
|
@@ -35,5 +35,5 @@ Use Page as a route content shell that combines Region sizing with vertical rhyt
|
|
|
35
35
|
```tsx
|
|
36
36
|
import { Card, CardContent, Page, PageHeader } from "@sableui/react"
|
|
37
37
|
|
|
38
|
-
<Page as="main" max="feed"><PageHeader title="Home" description="Latest posts from
|
|
38
|
+
<Page as="main" max="feed"><PageHeader title="Home" description="Latest posts from people you follow." /><Card><CardContent><p className="text-body-md">First post.</p></CardContent></Card></Page>
|
|
39
39
|
```
|
package/llms/layout/region.md
CHANGED
|
@@ -50,7 +50,7 @@ import { PageHeader, Region } from "@sableui/react"
|
|
|
50
50
|
```tsx
|
|
51
51
|
import { Card, CardHeader, CardTitle, Grid, Region } from "@sableui/react"
|
|
52
52
|
|
|
53
|
-
<Region max="wide"><Grid minItemWidth="16rem"><Card><CardHeader><CardTitle>Revenue</CardTitle></CardHeader></Card><Card><CardHeader><CardTitle>
|
|
53
|
+
<Region max="wide"><Grid minItemWidth="16rem"><Card><CardHeader><CardTitle>Revenue</CardTitle></CardHeader></Card><Card><CardHeader><CardTitle>Orders</CardTitle></CardHeader></Card></Grid></Region>
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
### Fluid workspace: the queue's table takes the extra width
|
|
@@ -37,5 +37,5 @@ Use SplitPane for responsive two-pane layouts where a primary pane and supportin
|
|
|
37
37
|
```tsx
|
|
38
38
|
import { AspectBox, Button, SplitPane, Stack } from "@sableui/react"
|
|
39
39
|
|
|
40
|
-
<SplitPane ratio="2/1" primary={<AspectBox ratio={1}><img src={post.imageUrl} alt={post.title} className="size-full object-cover" /></AspectBox>} secondary={<Stack gap={2}><h1 className="text-heading-1">{post.title}</h1><p className="text-body-md text-muted-foreground">{post.description}</p><Button>
|
|
40
|
+
<SplitPane ratio="2/1" primary={<AspectBox ratio={1}><img src={post.imageUrl} alt={post.title} className="size-full object-cover" /></AspectBox>} secondary={<Stack gap={2}><h1 className="text-heading-1">{post.title}</h1><p className="text-body-md text-muted-foreground">{post.description}</p><Button>Add to cart</Button></Stack>} />
|
|
41
41
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AuthPage
|
|
2
2
|
|
|
3
|
-
Split sign-in page: a form column (brand mark, heading, email + password, primary sign-in, passkey and
|
|
3
|
+
Split sign-in page: a form column (brand mark, heading, email + password, primary sign-in, passkey and email-link alternatives, sign-up switch, terms) beside a brand panel painted from the theme's tones with a customer story card. Below lg the panel drops away and the form is the whole screen.
|
|
4
4
|
|
|
5
5
|
**Intent:** Reach for this for sign-in, sign-up, forgot-password, and any single-purpose one-column form route (invite accept, unsubscribe). Not for multi-section forms (use SettingsPage).
|
|
6
6
|
|
|
@@ -11,14 +11,14 @@ Split sign-in page: a form column (brand mark, heading, email + password, primar
|
|
|
11
11
|
- The heading is `text-heading-1` in an h1 — it is the page outline, so it takes a heading tier, not a card title.
|
|
12
12
|
- Brand mark, form and terms are a three-part column: mark pinned to the top, form vertically centred in the remaining space, terms pinned to the bottom.
|
|
13
13
|
- Forgot password sits under the password field, right-aligned, before the submit button — so it comes right after the field in tab order. Field has no label-row action slot yet.
|
|
14
|
-
- One primary action. Passkey and
|
|
14
|
+
- One primary action. Passkey and email link are outline buttons under an `or` Separator; switching to sign-up is a quiet `link-underline` link.
|
|
15
15
|
- Validation uses Field `validate` for product copy instead of the browser's generic required message. A rejected sign-in comes back through Form `errors` on the password field; the submit Button shows `loading` while the request is in flight.
|
|
16
16
|
- The brand panel artwork is a mesh of the theme's decorative tones over `--primary`, so it re-paints for every theme and mode. The relative-colour `oklch(from var(--tone-*) …)` lift needs an `inline-color` disable with a reason.
|
|
17
17
|
- The panel is `hidden lg:flex`: on a phone the form is the whole screen, with no decorative scroll above it.
|
|
18
18
|
|
|
19
19
|
## Accessibility
|
|
20
20
|
|
|
21
|
-
- The route renders inside <main> with one h1; the brand panel is an <aside> labelled "
|
|
21
|
+
- The route renders inside <main> with one h1; the brand panel is an <aside> labelled "Customer story".
|
|
22
22
|
- Field auto-wires labels, describedby, and invalid state onto the inputs; on submit, the first invalid field is focused.
|
|
23
23
|
- Autocomplete attributes (`email webauthn`, `current-password`) keep password managers and passkey autofill working.
|
|
24
24
|
- The Forgot password and Create one links use `hit-target` to meet the 44px touch floor without changing layout.
|
|
@@ -49,7 +49,7 @@ export function AuthPage() {
|
|
|
49
49
|
<Stack gap={1.5}>
|
|
50
50
|
<h1 className="text-heading-1">Welcome back</h1>
|
|
51
51
|
<p className="text-body-lg text-muted-foreground">
|
|
52
|
-
Sign in to
|
|
52
|
+
Sign in to pick up where you and your team left off.
|
|
53
53
|
</p>
|
|
54
54
|
</Stack>
|
|
55
55
|
|
|
@@ -87,8 +87,8 @@ export function AuthPage() {
|
|
|
87
87
|
<Button variant="outline" size="cta" className="w-full" iconStart={<Icon name="key" />}>
|
|
88
88
|
Sign in with a passkey
|
|
89
89
|
</Button>
|
|
90
|
-
<Button variant="outline" size="cta" className="w-full" iconStart={<Icon name="
|
|
91
|
-
|
|
90
|
+
<Button variant="outline" size="cta" className="w-full" iconStart={<Icon name="mail" />}>
|
|
91
|
+
Email me a sign-in link
|
|
92
92
|
</Button>
|
|
93
93
|
</Stack>
|
|
94
94
|
|
|
@@ -115,23 +115,23 @@ export function AuthPage() {
|
|
|
115
115
|
</Region>
|
|
116
116
|
|
|
117
117
|
{/* The brand panel is decoration: on a phone the form is the whole screen. */}
|
|
118
|
-
<aside aria-label="
|
|
118
|
+
<aside aria-label="Customer story" className="hidden p-3 lg:flex">
|
|
119
119
|
{/* Art painted from the theme's tones over its brand, so it follows every look. */}
|
|
120
120
|
<GradientArt seed="Untitled (gold rush)" className="flex flex-1 flex-col justify-end rounded-xl p-8">
|
|
121
121
|
<Card className="max-w-md">
|
|
122
122
|
<CardContent>
|
|
123
123
|
<Stack gap={3}>
|
|
124
|
-
<Badge tone="
|
|
125
|
-
|
|
124
|
+
<Badge tone="1" className="self-start">
|
|
125
|
+
Customer story
|
|
126
126
|
</Badge>
|
|
127
127
|
<p className="text-title-lg">
|
|
128
|
-
“
|
|
128
|
+
“We moved three teams over in a week, and nobody asked for the old tool back.”
|
|
129
129
|
</p>
|
|
130
130
|
<Entity
|
|
131
131
|
className="p-0"
|
|
132
132
|
leading={<Avatar variant="art" fallback="MO" />}
|
|
133
133
|
title="Mara Okafor"
|
|
134
|
-
subtitle="
|
|
134
|
+
subtitle="Head of Design, Northwind"
|
|
135
135
|
/>
|
|
136
136
|
</Stack>
|
|
137
137
|
</CardContent>
|
|
@@ -45,7 +45,7 @@ const spaces = [
|
|
|
45
45
|
|
|
46
46
|
const routes = [
|
|
47
47
|
{ label: "Intake", state: "Open", tone: "info" as const },
|
|
48
|
-
{ label: "Review", state: "Live", tone: "
|
|
48
|
+
{ label: "Review", state: "Live", tone: "1" as const },
|
|
49
49
|
{ label: "Archive", state: "Safe", tone: "success" as const },
|
|
50
50
|
]
|
|
51
51
|
|
|
@@ -223,7 +223,7 @@ export function BrandLandingPage() {
|
|
|
223
223
|
<div className="flex items-center justify-between border-b border-border px-4 py-3">
|
|
224
224
|
<Row gap={1}>
|
|
225
225
|
<StatusDot tone="warning" />
|
|
226
|
-
<StatusDot tone="
|
|
226
|
+
<StatusDot tone="1" />
|
|
227
227
|
<StatusDot tone="success" />
|
|
228
228
|
</Row>
|
|
229
229
|
<span className="text-label-sm text-muted-foreground">Live workspace</span>
|
|
@@ -95,7 +95,7 @@ export function ChatPanel() {
|
|
|
95
95
|
render={<section aria-labelledby="chat-title" />}
|
|
96
96
|
className="flex h-[calc(100dvh_-_--spacing(12))] flex-col overflow-hidden"
|
|
97
97
|
>
|
|
98
|
-
<CardHeader className="
|
|
98
|
+
<CardHeader className="pb-(--card-p) shadow-[inset_0_-1px_0_var(--border)]">
|
|
99
99
|
{/* CardTitle carries the type; the h1 inside it is the page heading. */}
|
|
100
100
|
<CardTitle>
|
|
101
101
|
<h1 id="chat-title">Scroll behaviour</h1>
|
|
@@ -234,7 +234,7 @@ export function ChatPanel() {
|
|
|
234
234
|
</Message>
|
|
235
235
|
</MessageScroller>
|
|
236
236
|
|
|
237
|
-
<CardFooter className="
|
|
237
|
+
<CardFooter className="pt-(--card-p) shadow-[inset_0_1px_0_var(--border)]">
|
|
238
238
|
<ChatComposer
|
|
239
239
|
className="flex-1"
|
|
240
240
|
placeholder="Ask a follow-up…"
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# DashboardPage
|
|
2
2
|
|
|
3
|
-
App-shell
|
|
3
|
+
App-shell admin dashboard: a branded Sidebar with an account footer (AppHeader + BottomNav below lg), a KPI row, an edge-to-edge accounts table with seat-usage Meters, and supporting payout, top-customer, 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 (sales, orders,
|
|
5
|
+
**Intent:** Reach for this when a route's job is monitoring or managing many records (sales, orders, accounts, 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
|
-
- Hierarchy runs headline → records → context: KPI row, then the primary table beside its two closest companions (payout, top
|
|
10
|
+
- Hierarchy runs headline → records → context: KPI row, then the primary table beside its two closest companions (payout, top customers), then the lower-priority lists (upcoming, activity). Every row of the grid is filled so the route ends on content, not empty canvas.
|
|
11
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
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
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
|
-
-
|
|
14
|
+
- Seats used / seats 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
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
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.
|
|
17
17
|
- The page has exactly one heading-1 (PageHeader); card titles are Title tier, not headings.
|
|
@@ -23,7 +23,7 @@ App-shell creator dashboard: a branded Sidebar with an account footer (AppHeader
|
|
|
23
23
|
- AppShell provides the skip link and <main id="main-content"> landmark.
|
|
24
24
|
- The active SidebarItem and BottomNavItem carry aria-current via `active`.
|
|
25
25
|
- DataTable headers are real <th>; sortable columns are announced.
|
|
26
|
-
- Each
|
|
26
|
+
- Each seat-usage Meter has an aria-label naming the account and aria-valuetext reading "184 of 250 seats used".
|
|
27
27
|
- Avatars beside a visible name use an empty alt so the name is not read twice.
|
|
28
28
|
|
|
29
29
|
## Source
|
|
@@ -31,11 +31,11 @@ App-shell creator dashboard: a branded Sidebar with an account footer (AppHeader
|
|
|
31
31
|
```tsx
|
|
32
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, SidebarBrand, SidebarFooter, SidebarHeader, SidebarItem, SidebarNav, Stack } from "@sableui/react"
|
|
33
33
|
|
|
34
|
-
//
|
|
34
|
+
// Account status → Badge tone. Sable has no status vocabulary of its own;
|
|
35
35
|
// an app maps ITS states onto tones once, here, and every badge follows.
|
|
36
|
-
const STATUS_TONE = {
|
|
37
|
-
const STATUS_LABEL = {
|
|
38
|
-
type
|
|
36
|
+
const STATUS_TONE = { active: "success", onboarding: "info", full: "1", cancelled: "neutral" } as const
|
|
37
|
+
const STATUS_LABEL = { active: "Active", onboarding: "Onboarding", full: "Seats full", cancelled: "Cancelled" } as const
|
|
38
|
+
type AccountStatus = keyof typeof STATUS_TONE
|
|
39
39
|
|
|
40
40
|
const MIN = 60 * 1000
|
|
41
41
|
const HOUR = 60 * MIN
|
|
@@ -48,54 +48,54 @@ const shortDate = (offset: number) =>
|
|
|
48
48
|
|
|
49
49
|
const stats = [
|
|
50
50
|
{ label: "Revenue", value: "$18,240", delta: "+8.2%", up: true, note: "vs. $16,857" },
|
|
51
|
-
{ label: "
|
|
52
|
-
{ label: "
|
|
53
|
-
{ label: "
|
|
51
|
+
{ label: "Active seats", value: "1,284", delta: "+3.1%", up: true, note: "182 added" },
|
|
52
|
+
{ label: "Accounts", value: "312", delta: "+12.4%", up: true, note: "Best month yet" },
|
|
53
|
+
{ label: "Seat usage", value: "64%", delta: "−1.8%", up: false, note: "6 near their limit" },
|
|
54
54
|
]
|
|
55
55
|
|
|
56
|
-
const
|
|
57
|
-
{ id: "
|
|
58
|
-
{ id: "
|
|
59
|
-
{ id: "
|
|
60
|
-
{ id: "
|
|
61
|
-
{ id: "
|
|
62
|
-
{ id: "
|
|
63
|
-
{ id: "
|
|
64
|
-
{ id: "
|
|
65
|
-
{ id: "
|
|
56
|
+
const accounts = [
|
|
57
|
+
{ id: "ac_01", title: "Northwind", status: "active" as const, used: 184, seats: 250, price: 16, started: -6 * DAY },
|
|
58
|
+
{ id: "ac_02", title: "Harbor Labs", status: "full" as const, used: 100, seats: 100, price: 20, started: -19 * DAY },
|
|
59
|
+
{ id: "ac_03", title: "Brightline", status: "active" as const, used: 61, seats: 150, price: 16, started: -2 * DAY },
|
|
60
|
+
{ id: "ac_04", title: "Tidewater Co.", status: "active" as const, used: 212, seats: 300, price: 12, started: -27 * DAY },
|
|
61
|
+
{ id: "ac_05", title: "Saltworks", status: "cancelled" as const, used: 38, seats: 75, price: 20, started: -44 * DAY },
|
|
62
|
+
{ id: "ac_06", title: "Paper Moon", status: "full" as const, used: 50, seats: 50, price: 24, started: -58 * DAY },
|
|
63
|
+
{ id: "ac_07", title: "Signal Studio", status: "onboarding" as const, used: 0, seats: 200, price: 16, started: 3 * DAY },
|
|
64
|
+
{ id: "ac_08", title: "Bluewater", status: "cancelled" as const, used: 72, seats: 120, price: 12, started: -71 * DAY },
|
|
65
|
+
{ id: "ac_09", title: "Ember & Field", status: "full" as const, used: 80, seats: 80, price: 20, started: -86 * DAY },
|
|
66
66
|
]
|
|
67
|
-
const
|
|
67
|
+
const accountCount = 312
|
|
68
68
|
|
|
69
69
|
const payout = {
|
|
70
|
-
amount:
|
|
70
|
+
amount: 8862.64,
|
|
71
71
|
date: 5 * DAY,
|
|
72
72
|
lines: [
|
|
73
|
-
{ label: "
|
|
74
|
-
{ label: "
|
|
73
|
+
{ label: "Subscriptions", value: 9127.33 },
|
|
74
|
+
{ label: "Processing (2.9%)", value: -264.69 },
|
|
75
75
|
{ label: "Deposits to", value: "Checking ••4821" },
|
|
76
76
|
],
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
const
|
|
80
|
-
{ id: "
|
|
81
|
-
{ id: "
|
|
82
|
-
{ id: "
|
|
83
|
-
{ id: "
|
|
79
|
+
const customers = [
|
|
80
|
+
{ id: "cu_01", name: "Northwind", handle: "northwind.io", initials: "NW", seats: 184, spent: 2944 },
|
|
81
|
+
{ id: "cu_02", name: "Tidewater Co.", handle: "tidewater.co", initials: "TC", seats: 212, spent: 2544 },
|
|
82
|
+
{ id: "cu_03", name: "Harbor Labs", handle: "harborlabs.com", initials: "HL", seats: 100, spent: 2000 },
|
|
83
|
+
{ id: "cu_04", name: "Ember & Field", handle: "emberfield.com", initials: "EF", seats: 80, spent: 1600 },
|
|
84
84
|
]
|
|
85
85
|
|
|
86
86
|
const upcoming = [
|
|
87
|
-
{ id: "up_01", title: "
|
|
88
|
-
{ id: "up_02", title: "
|
|
89
|
-
{ id: "up_03", title: "
|
|
90
|
-
{ id: "up_04", title: "
|
|
87
|
+
{ id: "up_01", title: "Version 2.4", detail: "Shared reports · All plans", at: 3 * DAY },
|
|
88
|
+
{ id: "up_02", title: "Audit log export", detail: "Business plan · Beta", at: 12 * DAY },
|
|
89
|
+
{ id: "up_03", title: "Version 2.5", detail: "Offline mode · All plans", at: 26 * DAY },
|
|
90
|
+
{ id: "up_04", title: "Usage-based billing", detail: "Studio and Business · Beta", at: 34 * DAY },
|
|
91
91
|
]
|
|
92
92
|
|
|
93
93
|
const activity = [
|
|
94
|
-
{ id: "
|
|
95
|
-
{ id: "
|
|
96
|
-
{ id: "
|
|
97
|
-
{ id: "
|
|
98
|
-
{ id: "
|
|
94
|
+
{ id: "ev_01", who: "Nina Simone", initials: "NS", what: "added 12 seats to Northwind · 184 of 250", when: 2 * MIN },
|
|
95
|
+
{ id: "ev_02", who: "Grace Hopper", initials: "GH", what: "upgraded Brightline to Studio", when: 26 * MIN },
|
|
96
|
+
{ id: "ev_03", who: "Frida Kahlo", initials: "FK", what: "paid invoice INV-0042 for Tidewater Co.", when: 1 * HOUR },
|
|
97
|
+
{ id: "ev_04", who: "Mary Shelley", initials: "MS", what: "joined the Harbor Labs workspace", when: 3 * HOUR },
|
|
98
|
+
{ id: "ev_05", who: "Nina Simone", initials: "NS", what: "booked onboarding for Signal Studio", when: 6 * HOUR },
|
|
99
99
|
]
|
|
100
100
|
|
|
101
101
|
export function DashboardPage() {
|
|
@@ -117,8 +117,8 @@ export function DashboardPage() {
|
|
|
117
117
|
bottomNav={
|
|
118
118
|
<BottomNav aria-label="Primary">
|
|
119
119
|
<BottomNavItem icon={<Icon name="grid" />} label="Overview" showLabel active render={<a href="/" />} />
|
|
120
|
-
<BottomNavItem icon={<Icon name="
|
|
121
|
-
<BottomNavItem icon={<Icon name="user" />} label="
|
|
120
|
+
<BottomNavItem icon={<Icon name="folder" />} label="Accounts" showLabel render={<a href="/accounts" />} />
|
|
121
|
+
<BottomNavItem icon={<Icon name="user" />} label="Customers" showLabel render={<a href="/customers" />} />
|
|
122
122
|
<BottomNavItem icon={<Icon name="bell" />} label="Activity" showLabel render={<a href="/activity" />} />
|
|
123
123
|
<BottomNavItem icon={<Icon name="gear" />} label="Settings" showLabel render={<a href="/settings" />} />
|
|
124
124
|
</BottomNav>
|
|
@@ -130,8 +130,8 @@ export function DashboardPage() {
|
|
|
130
130
|
</SidebarHeader>
|
|
131
131
|
<SidebarNav>
|
|
132
132
|
<SidebarItem icon={<Icon name="grid" />} label="Overview" active render={<a href="/" />} />
|
|
133
|
-
<SidebarItem icon={<Icon name="
|
|
134
|
-
<SidebarItem icon={<Icon name="user" />} label="
|
|
133
|
+
<SidebarItem icon={<Icon name="folder" />} label="Accounts" render={<a href="/accounts" />} />
|
|
134
|
+
<SidebarItem icon={<Icon name="user" />} label="Customers" render={<a href="/customers" />} />
|
|
135
135
|
<SidebarItem
|
|
136
136
|
icon={<Icon name="bell" />}
|
|
137
137
|
label="Activity"
|
|
@@ -162,7 +162,7 @@ export function DashboardPage() {
|
|
|
162
162
|
<Page max="wide" gap={4} className="py-6">
|
|
163
163
|
<PageHeader
|
|
164
164
|
title="Overview"
|
|
165
|
-
description="
|
|
165
|
+
description="Revenue and account activity for the last 30 days."
|
|
166
166
|
actions={
|
|
167
167
|
<Row gap={2}>
|
|
168
168
|
<Button variant="outline">
|
|
@@ -171,7 +171,7 @@ export function DashboardPage() {
|
|
|
171
171
|
</Button>
|
|
172
172
|
<Button>
|
|
173
173
|
<Icon name="plus" />
|
|
174
|
-
New
|
|
174
|
+
New account
|
|
175
175
|
</Button>
|
|
176
176
|
</Row>
|
|
177
177
|
}
|
|
@@ -201,43 +201,43 @@ export function DashboardPage() {
|
|
|
201
201
|
<Grid cols={{ base: 1, xl: 3 }} gap={3}>
|
|
202
202
|
<Card className="flex flex-col xl:col-span-2">
|
|
203
203
|
<CardHeader>
|
|
204
|
-
<CardTitle>
|
|
205
|
-
<CardDescription>
|
|
204
|
+
<CardTitle>Accounts</CardTitle>
|
|
205
|
+
<CardDescription>Seats across your customers, newest first.</CardDescription>
|
|
206
206
|
</CardHeader>
|
|
207
207
|
<CardContent flush className="mt-4 flex-1">
|
|
208
208
|
<DataTable
|
|
209
|
-
data={
|
|
209
|
+
data={accounts}
|
|
210
210
|
rowKey={(row) => row.id}
|
|
211
|
-
defaultSort={{ key: "
|
|
211
|
+
defaultSort={{ key: "started", direction: "desc" }}
|
|
212
212
|
columns={[
|
|
213
|
-
{ key: "title", header: "
|
|
213
|
+
{ key: "title", header: "Account", sortable: true, className: "text-label-md whitespace-nowrap" },
|
|
214
214
|
{
|
|
215
215
|
key: "status",
|
|
216
216
|
header: "Status",
|
|
217
217
|
cell: (row) => (
|
|
218
|
-
<Badge appearance="plain" tone={STATUS_TONE[row.status as
|
|
219
|
-
{STATUS_LABEL[row.status as
|
|
218
|
+
<Badge appearance="plain" tone={STATUS_TONE[row.status as AccountStatus]} dot>
|
|
219
|
+
{STATUS_LABEL[row.status as AccountStatus]}
|
|
220
220
|
</Badge>
|
|
221
221
|
),
|
|
222
222
|
},
|
|
223
223
|
{
|
|
224
|
-
key: "
|
|
225
|
-
header: "
|
|
224
|
+
key: "used",
|
|
225
|
+
header: "Seats",
|
|
226
226
|
sortable: true,
|
|
227
|
-
sortValue: (row) => row.
|
|
227
|
+
sortValue: (row) => row.used / row.seats,
|
|
228
228
|
cell: (row) => (
|
|
229
229
|
<Row gap={3} align="center" className="min-w-40">
|
|
230
230
|
<Meter
|
|
231
|
-
value={row.
|
|
232
|
-
max={row.
|
|
231
|
+
value={row.used}
|
|
232
|
+
max={row.seats}
|
|
233
233
|
size="sm"
|
|
234
|
-
tone={row.
|
|
235
|
-
aria-label={`${row.title}
|
|
236
|
-
getAriaValueText={(_, v) => `${v} of ${row.
|
|
234
|
+
tone={row.used === row.seats ? "success" : "primary"}
|
|
235
|
+
aria-label={`${row.title} seats used`}
|
|
236
|
+
getAriaValueText={(_, v) => `${v} of ${row.seats} seats used`}
|
|
237
237
|
className="flex-1"
|
|
238
238
|
/>
|
|
239
239
|
<span className="text-mono-caption whitespace-nowrap text-muted-foreground">
|
|
240
|
-
{row.
|
|
240
|
+
{row.used}/{row.seats}
|
|
241
241
|
</span>
|
|
242
242
|
</Row>
|
|
243
243
|
),
|
|
@@ -247,21 +247,21 @@ export function DashboardPage() {
|
|
|
247
247
|
header: "Revenue",
|
|
248
248
|
align: "end",
|
|
249
249
|
sortable: true,
|
|
250
|
-
sortValue: (row) => row.
|
|
250
|
+
sortValue: (row) => row.used * row.price,
|
|
251
251
|
cell: (row) =>
|
|
252
|
-
row.
|
|
253
|
-
<span className="text-mono-sm">{usd.format(row.
|
|
252
|
+
row.used > 0 ? (
|
|
253
|
+
<span className="text-mono-sm">{usd.format(row.used * row.price)}</span>
|
|
254
254
|
) : (
|
|
255
255
|
<span className="text-mono-sm text-muted-foreground">—</span>
|
|
256
256
|
),
|
|
257
257
|
},
|
|
258
258
|
{
|
|
259
|
-
key: "
|
|
260
|
-
header: "
|
|
259
|
+
key: "started",
|
|
260
|
+
header: "Started",
|
|
261
261
|
align: "end",
|
|
262
262
|
sortable: true,
|
|
263
263
|
cell: (row) => (
|
|
264
|
-
<span className="text-body-sm whitespace-nowrap text-muted-foreground">{shortDate(row.
|
|
264
|
+
<span className="text-body-sm whitespace-nowrap text-muted-foreground">{shortDate(row.started)}</span>
|
|
265
265
|
),
|
|
266
266
|
},
|
|
267
267
|
]}
|
|
@@ -269,10 +269,10 @@ export function DashboardPage() {
|
|
|
269
269
|
</CardContent>
|
|
270
270
|
<CardFooter className="justify-between">
|
|
271
271
|
<span className="text-caption text-muted-foreground">
|
|
272
|
-
Showing {
|
|
272
|
+
Showing {accounts.length} of {accountCount} accounts
|
|
273
273
|
</span>
|
|
274
|
-
<Button variant="ghost" size="sm" bleed="end" render={<a href="/
|
|
275
|
-
View all
|
|
274
|
+
<Button variant="ghost" size="sm" bleed="end" render={<a href="/accounts" />}>
|
|
275
|
+
View all accounts
|
|
276
276
|
<Icon name="arrow-right" />
|
|
277
277
|
</Button>
|
|
278
278
|
</CardFooter>
|
|
@@ -303,21 +303,21 @@ export function DashboardPage() {
|
|
|
303
303
|
</Card>
|
|
304
304
|
<Card>
|
|
305
305
|
<CardHeader>
|
|
306
|
-
<CardTitle>Top
|
|
306
|
+
<CardTitle>Top customers</CardTitle>
|
|
307
307
|
<CardDescription>By spend this month.</CardDescription>
|
|
308
308
|
<CardAction>
|
|
309
|
-
<Button variant="ghost" size="sm" render={<a href="/
|
|
309
|
+
<Button variant="ghost" size="sm" render={<a href="/customers" />}>
|
|
310
310
|
View all
|
|
311
311
|
</Button>
|
|
312
312
|
</CardAction>
|
|
313
313
|
</CardHeader>
|
|
314
314
|
<CardList>
|
|
315
|
-
{
|
|
315
|
+
{customers.map((person) => (
|
|
316
316
|
<Entity
|
|
317
317
|
key={person.id}
|
|
318
318
|
leading={<Avatar variant="art" size="sm" fallback={person.initials} alt="" />}
|
|
319
319
|
title={person.name}
|
|
320
|
-
subtitle={`${person.handle} · ${person.
|
|
320
|
+
subtitle={`${person.handle} · ${person.seats} seats`}
|
|
321
321
|
action={<span className="text-mono-sm">{usd.format(person.spent)}</span>}
|
|
322
322
|
/>
|
|
323
323
|
))}
|
|
@@ -329,7 +329,7 @@ export function DashboardPage() {
|
|
|
329
329
|
<Card className="flex flex-col">
|
|
330
330
|
<CardHeader>
|
|
331
331
|
<CardTitle>Upcoming releases</CardTitle>
|
|
332
|
-
<CardDescription>Scheduled to
|
|
332
|
+
<CardDescription>Scheduled to ship to customers.</CardDescription>
|
|
333
333
|
</CardHeader>
|
|
334
334
|
<CardList>
|
|
335
335
|
{upcoming.map((release) => (
|
|
@@ -342,7 +342,7 @@ export function DashboardPage() {
|
|
|
342
342
|
))}
|
|
343
343
|
</CardList>
|
|
344
344
|
<CardFooter className="mt-auto">
|
|
345
|
-
<Button variant="outline" size="sm" render={<a href="/
|
|
345
|
+
<Button variant="outline" size="sm" render={<a href="/releases/new" />}>
|
|
346
346
|
<Icon name="calendar" />
|
|
347
347
|
Schedule a release
|
|
348
348
|
</Button>
|
|
@@ -351,7 +351,7 @@ export function DashboardPage() {
|
|
|
351
351
|
<Card className="xl:col-span-2">
|
|
352
352
|
<CardHeader>
|
|
353
353
|
<CardTitle>Activity</CardTitle>
|
|
354
|
-
<CardDescription>
|
|
354
|
+
<CardDescription>Seats, upgrades, and invoices.</CardDescription>
|
|
355
355
|
<CardAction>
|
|
356
356
|
<Button variant="ghost" size="sm" render={<a href="/activity" />}>
|
|
357
357
|
View all
|