@jovertical/mise-design-system 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,291 @@
1
+ <!-- Generated by pnpm build:skill. Do not edit by hand. -->
2
+
3
+ # Overlays
4
+
5
+ ## AlertDialog
6
+
7
+ `import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogMedia, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger } from '@jovertical/mise-design-system'`
8
+
9
+ For a confirmation the operator cannot dismiss by clicking outside it: `Dialog` closes on an outside click, this does not. Reach for it when the action is destructive or hard to undo; everything else wants `Dialog`.
10
+
11
+ Also takes: bits-ui `AlertDialog.Root` props.
12
+
13
+ ### `AlertDialogAction`
14
+
15
+ Also takes: bits-ui `AlertDialog.Action` props except `child`; `variant`, `tone`, `size` from `Button`.
16
+
17
+ ### `AlertDialogCancel`
18
+
19
+ Also takes: bits-ui `AlertDialog.Cancel` props except `child`; `variant`, `size` from `Button`.
20
+
21
+ ### `AlertDialogContent`
22
+
23
+ The card's 24px radius, a ring and `shadow-xl`: it is a dialog, and was drawn like a menu. `size="sm"` for a short confirmation with no description.
24
+
25
+ | Prop | Type | Default | Description |
26
+ | --- | --- | --- | --- |
27
+ | `size` | `'default' \| 'sm'` | `'default'` | |
28
+ | `portalProps` | `AlertDialogPrimitive.PortalProps` | | |
29
+
30
+ Also takes: bits-ui `AlertDialog.Content` props.
31
+
32
+ ### `AlertDialogDescription`
33
+
34
+ Also takes: bits-ui `AlertDialog.Description` props.
35
+
36
+ ### `AlertDialogFooter`
37
+
38
+ Also takes: every `<div>` attribute.
39
+
40
+ ### `AlertDialogHeader`
41
+
42
+ Also takes: every `<div>` attribute.
43
+
44
+ ### `AlertDialogMedia`
45
+
46
+ Also takes: every `<div>` attribute.
47
+
48
+ ### `AlertDialogOverlay`
49
+
50
+ Also takes: bits-ui `AlertDialog.Overlay` props.
51
+
52
+ ### `AlertDialogPortal`
53
+
54
+ Also takes: bits-ui `AlertDialog.Portal` props.
55
+
56
+ ### `AlertDialogTitle`
57
+
58
+ Also takes: bits-ui `AlertDialog.Title` props.
59
+
60
+ ### `AlertDialogTrigger`
61
+
62
+ Also takes: bits-ui `AlertDialog.Trigger` props.
63
+
64
+ ## Dialog
65
+
66
+ `import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from '@jovertical/mise-design-system'`
67
+
68
+ Also takes: bits-ui `Dialog.Root` props.
69
+
70
+ ### `DialogClose`
71
+
72
+ Also takes: bits-ui `Dialog.Close` props.
73
+
74
+ ### `DialogContent`
75
+
76
+ A ring rather than a border, and `shadow-xl` rather than `shadow-lg`: a dialog sits above everything, so it casts the deeper shadow.
77
+
78
+ | Prop | Type | Default | Description |
79
+ | --- | --- | --- | --- |
80
+ | `portalProps` | `WithoutChildrenOrChild<DialogPrimitive.PortalProps>` | | |
81
+ | `children` (required) | `Snippet` | | |
82
+ | `showCloseButton` | `boolean` | `true` | |
83
+ | `size` | `DialogSize` | `'lg'` | |
84
+
85
+ Also takes: bits-ui `Dialog.Content` props except `children`, `child`.
86
+
87
+ ### `DialogDescription`
88
+
89
+ Also takes: bits-ui `Dialog.Description` props.
90
+
91
+ ### `DialogFooter`
92
+
93
+ Also takes: every `<div>` attribute.
94
+
95
+ ### `DialogHeader`
96
+
97
+ Also takes: every `<div>` attribute.
98
+
99
+ ### `DialogOverlay`
100
+
101
+ Also takes: bits-ui `Dialog.Overlay` props.
102
+
103
+ ### `DialogPortal`
104
+
105
+ Also takes: bits-ui `Dialog.Portal` props.
106
+
107
+ ### `DialogTitle`
108
+
109
+ Also takes: bits-ui `Dialog.Title` props.
110
+
111
+ ### `DialogTrigger`
112
+
113
+ Also takes: bits-ui `Dialog.Trigger` props.
114
+
115
+ Types: `DialogSize`.
116
+
117
+ ## Drawer
118
+
119
+ `import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger } from '@jovertical/mise-design-system'`
120
+
121
+ A panel dragged in from an edge. Slides from the bottom by default: the phone sheet, for a screen that wants a bottom sheet rather than a centred `Dialog`. `direction="right"` makes it a side panel, though `Sheet` is the usual one there.
122
+
123
+ Also takes: bits-ui `Drawer.Root` props.
124
+
125
+ ### `DrawerClose`
126
+
127
+ Also takes: bits-ui `Drawer.Close` props.
128
+
129
+ ### `DrawerContent`
130
+
131
+ The panel, its overlay and, from the bottom, the drag handle.
132
+
133
+ | Prop | Type | Default | Description |
134
+ | --- | --- | --- | --- |
135
+ | `portalProps` | `DrawerPrimitive.PortalProps` | | |
136
+
137
+ Also takes: bits-ui `Drawer.Content` props.
138
+
139
+ ### `DrawerDescription`
140
+
141
+ Also takes: bits-ui `Drawer.Description` props.
142
+
143
+ ### `DrawerFooter`
144
+
145
+ Also takes: every `<div>` attribute.
146
+
147
+ ### `DrawerHeader`
148
+
149
+ Also takes: every `<div>` attribute.
150
+
151
+ ### `DrawerOverlay`
152
+
153
+ Also takes: bits-ui `Drawer.Overlay` props.
154
+
155
+ ### `DrawerPortal`
156
+
157
+ Also takes: bits-ui `Drawer.Portal` props.
158
+
159
+ ### `DrawerTitle`
160
+
161
+ Also takes: bits-ui `Drawer.Title` props.
162
+
163
+ ### `DrawerTrigger`
164
+
165
+ Also takes: bits-ui `Drawer.Trigger` props.
166
+
167
+ ## Hint
168
+
169
+ `import { Hint } from '@jovertical/mise-design-system'`
170
+
171
+ The `?` that explains the thing it sits beside.
172
+
173
+ | Prop | Type | Default | Description |
174
+ | --- | --- | --- | --- |
175
+ | `label` | `string` | | Announced in place of the glyph, so name what is being explained ("How stock health is calculated", not "Help"). |
176
+ | `children` (required) | `Snippet` | | The explanation, shown in the tooltip. |
177
+
178
+ Also takes: every `<button>` attribute except `children`.
179
+
180
+ ## HoverCard
181
+
182
+ `import { HoverCard, HoverCardContent, HoverCardTrigger } from '@jovertical/mise-design-system'`
183
+
184
+ A preview on hover or focus, for a fact worth a glance without leaving the page: a person's details behind their name, a linked record's summary. Reach for `Tooltip` instead when the content is a single short label.
185
+
186
+ Also takes: bits-ui `HoverCard.Root` props.
187
+
188
+ ### `HoverCardContent`
189
+
190
+ The floating card. A ring rather than a border, and `rounded-2xl` rather than shadcn's `rounded-md`: 6px was the smallest radius in a system whose cards are 24px.
191
+
192
+ | Prop | Type | Default | Description |
193
+ | --- | --- | --- | --- |
194
+ | `portalProps` | `HoverCardPrimitive.PortalProps` | | |
195
+
196
+ Also takes: bits-ui `HoverCard.Content` props.
197
+
198
+ ### `HoverCardTrigger`
199
+
200
+ Also takes: bits-ui `HoverCard.Trigger` props.
201
+
202
+ ## Popover
203
+
204
+ `import { Popover, PopoverContent, PopoverTrigger } from '@jovertical/mise-design-system'`
205
+
206
+ A positioned panel with no semantics of its own; whatever goes in it brings its meaning. Reach for it when the content is not a list of commands: a filter, a form, a preview, a feed. Commands want `DropdownMenu`.
207
+
208
+ Also takes: bits-ui `Popover.Root` props.
209
+
210
+ ### `PopoverContent`
211
+
212
+ The floating panel. It is not a `flex flex-col gap-4` like the reference's settings panel: most popovers are menus, filters or list panels that pass `p-0` and own their layout, and `p-0` cancels padding but not a gap. Add `flex flex-col gap-4` at the call site for the form case.
213
+
214
+ | Prop | Type | Default | Description |
215
+ | --- | --- | --- | --- |
216
+ | `matchTriggerWidth` | `boolean` | `false` | The panel takes the trigger's exact width instead of its own `w-72`, and drops the padding: the shape a Select-like popover wants, where the content inside owns its own padding. |
217
+ | `portalProps` | `PopoverPrimitive.PortalProps` | | |
218
+
219
+ Also takes: bits-ui `Popover.Content` props.
220
+
221
+ ### `PopoverTrigger`
222
+
223
+ Also takes: bits-ui `Popover.Trigger` props.
224
+
225
+ ## Sheet
226
+
227
+ `import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from '@jovertical/mise-design-system'`
228
+
229
+ A panel anchored to the viewport's edge, `side="right"` by default: the desktop side panel for a quick view without leaving the list.
230
+
231
+ Also takes: bits-ui `Sheet.Root` props.
232
+
233
+ ### `SheetClose`
234
+
235
+ Also takes: bits-ui `Sheet.Close` props.
236
+
237
+ ### `SheetContent`
238
+
239
+ | Prop | Type | Default | Description |
240
+ | --- | --- | --- | --- |
241
+ | `side` | `SheetSide` | `'right'` | |
242
+ | `portalProps` | `SheetPrimitive.PortalProps` | | |
243
+ | `children` | `Snippet` | | |
244
+
245
+ Also takes: bits-ui `Sheet.Content` props except `children`, `child`.
246
+
247
+ ### `SheetDescription`
248
+
249
+ Also takes: bits-ui `Sheet.Description` props.
250
+
251
+ ### `SheetFooter`
252
+
253
+ Also takes: every `<div>` attribute.
254
+
255
+ ### `SheetHeader`
256
+
257
+ Also takes: every `<div>` attribute.
258
+
259
+ ### `SheetTitle`
260
+
261
+ Also takes: bits-ui `Sheet.Title` props.
262
+
263
+ ### `SheetTrigger`
264
+
265
+ Also takes: bits-ui `Sheet.Trigger` props.
266
+
267
+ Types: `SheetSide`.
268
+
269
+ ## Tooltip
270
+
271
+ `import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@jovertical/mise-design-system'`
272
+
273
+ Also takes: bits-ui `Tooltip.Root` props.
274
+
275
+ ### `TooltipContent`
276
+
277
+ The panel sits on `bg-popover` with the ring and shadow every floating panel here carries, not the inverted `bg-foreground text-background` tooltip idiom. The inversion looks deliberate in light mode and renders a page-bright block over a dark UI in dark mode. The arrow follows the surface and carries no ring of its own.
278
+
279
+ | Prop | Type | Default | Description |
280
+ | --- | --- | --- | --- |
281
+ | `portalProps` | `TooltipPrimitive.PortalProps` | | |
282
+
283
+ Also takes: bits-ui `Tooltip.Content` props.
284
+
285
+ ### `TooltipProvider`
286
+
287
+ Also takes: bits-ui `Tooltip.Provider` props.
288
+
289
+ ### `TooltipTrigger`
290
+
291
+ Also takes: bits-ui `Tooltip.Trigger` props.
@@ -0,0 +1,36 @@
1
+ <!-- Generated by pnpm build:skill. Do not edit by hand. -->
2
+
3
+ # Typography
4
+
5
+ ## Heading
6
+
7
+ `import { Heading } from '@jovertical/mise-design-system'`
8
+
9
+ A heading that is not a card's, a dialog's or a page's: those have `CardTitle`, `DialogTitle` and `PageHeader`. This is a section inside a page body, so the face, the weight and the step come from one place. A figure is not a heading.
10
+
11
+ | Prop | Type | Default | Description |
12
+ | --- | --- | --- | --- |
13
+ | `as` | `'h1' \| 'h2' \| 'h3' \| 'h4' \| 'h5' \| 'h6'` | `'h2'` | The level, for the document outline. `h2` by default; a page's `h1` is `PageHeader`'s. Choose it by where the heading sits, never by how big it should look; that is `size`. |
14
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | `sm` 16px (a card's title), `md` 18px (a dialog's), `lg` 20px (a section opening a page body). |
15
+
16
+ Also takes: every `<h1>`–`<h6>` attribute.
17
+
18
+ ## Text
19
+
20
+ `import { Text } from '@jovertical/mise-design-system'`
21
+
22
+ Body text, set by what it is doing rather than by the classes that draw it: the size, the muting, the weight, the figures, and nothing else. No face, no arbitrary size, no colour outside `muted` and the shared `Tone`. `as` rather than `asChild`, because the element is the one semantic choice text makes.
23
+
24
+ | Prop | Type | Default | Description |
25
+ | --- | --- | --- | --- |
26
+ | `as` | `'span' \| 'p' \| 'div'` | `'span'` | `span` by default; `p` for a sentence that stands as a paragraph, `div` for text holding a block. |
27
+ | `size` | `'xs' \| 'sm'` | | `xs` 12px, `sm` 14px. Omit it to inherit the line's size. A larger step is a heading. |
28
+ | `muted` | `boolean` | `false` | De-emphasis, `text-muted-foreground`: a hint, a timestamp. Not a status. |
29
+ | `tone` | `'neutral' \| 'primary' \| 'info' \| 'success' \| 'warning' \| 'critical'` | | Status ink, for text that carries a state. Wins over `muted`. |
30
+ | `weight` | `'normal' \| 'medium' \| 'semibold'` | | `medium` for a name or a value, `semibold` for a total, `normal` to undo an inherited weight. |
31
+ | `numeric` | `boolean` | `false` | `tabular-nums`, so a column of figures lines up. |
32
+ | `truncate` | `boolean` | `false` | One line, ellipsised. Needs a width to truncate against. |
33
+
34
+ Also takes: every HTML attribute.
35
+
36
+ Types: `TextSize` (`'xs' | 'sm'`), `TextWeight` (`'normal' | 'medium' | 'semibold'`).
@@ -0,0 +1,40 @@
1
+ # Recipe: a dashboard page
2
+
3
+ A day's dashboard for one location: four headline figures, orders by hour, and sales by
4
+ location ranked against target. The code is [dashboard-page.svelte](dashboard-page.svelte), a
5
+ page component that type-checks against the current mise.
6
+
7
+ ## What it is made of
8
+
9
+ - `PageLayout` and `PageHeader` ([layout](../components/layout.md)): the page frame, the title,
10
+ the description and an action on the right.
11
+ - `StatTile` ([data display](../components/data-display.md)), four of them in a plain grid. One
12
+ figure per tile; `delta` takes a `DeltaBadge`, `visual` a `Sparkline`.
13
+ - `Card` with `ChartContainer` and a LayerChart `AreaChart`
14
+ ([data display](../components/data-display.md)) for the trend.
15
+ - `Card` with `MetricBar`s, `rank` numbering them, for the ranking.
16
+
17
+ ## Decisions worth copying
18
+
19
+ - **`StatTile`, not `MetricRow`, for the headline row.** A tile states one figure and qualifies
20
+ it; `MetricRow` is one subject with several figures and a bar under them.
21
+ - **The grid is the app's one class.** `grid gap-4 sm:grid-cols-2 lg:grid-cols-4` is layout,
22
+ which is what `class` is for. The tiles bring their own surface; do not wrap them in `Card`.
23
+ - **Figures arrive formatted.** `value="$14,527"` with `exactValue="$14,527.40"` on hover; mise
24
+ never formats money.
25
+ - **`invert` on a delta where falling is good.** Refunds up 3.1% is bad news, so the refunds
26
+ delta is `invert` and reads as a warning.
27
+ - **Chart colours come from the config**: `--chart-1` for the series, read back as
28
+ `--color-orders`. The chart follows the theme and dark mode with no extra work.
29
+
30
+ ## The app shell
31
+
32
+ The `Sidebar` belongs to the app's layout, not to each page: a layout component renders
33
+ `SidebarProvider`, `Sidebar` and an inset where pages like this one go. See `Sidebar` in
34
+ [navigation](../components/navigation.md) for its parts.
35
+
36
+ ## Dependencies
37
+
38
+ The chart imports `layerchart`, `d3-scale` and `d3-shape` directly. mise depends on them, but a
39
+ strict package manager (pnpm) only lets the app import what it declares, so add them to the app:
40
+ `pnpm add layerchart d3-scale d3-shape`.
@@ -0,0 +1,132 @@
1
+ <!--
2
+ A day's dashboard for one location: headline figures, the trend, and a
3
+ ranking. Everything is a mise component; the app's only classes are layout
4
+ (the tile grid). Figures arrive formatted: mise has no opinion about
5
+ currencies or locales.
6
+ -->
7
+ <script lang="ts">
8
+ import {
9
+ Button,
10
+ Card,
11
+ CardContent,
12
+ CardDescription,
13
+ CardHeader,
14
+ CardTitle,
15
+ ChartContainer,
16
+ type ChartConfig,
17
+ ChartTooltip,
18
+ chartAreaFillOpacity,
19
+ chartGrid,
20
+ chartLineStrokeWidth,
21
+ DeltaBadge,
22
+ MetricBar,
23
+ PageHeader,
24
+ PageLayout,
25
+ Sparkline,
26
+ StatTile,
27
+ } from '@jovertical/mise-design-system'
28
+ import { scalePoint } from 'd3-scale'
29
+ import { curveMonotoneX } from 'd3-shape'
30
+ import { AreaChart } from 'layerchart'
31
+
32
+ const byHour = [
33
+ { hour: '8am', orders: 12 },
34
+ { hour: '10am', orders: 31 },
35
+ { hour: '12pm', orders: 58 },
36
+ { hour: '2pm', orders: 44 },
37
+ { hour: '4pm', orders: 37 },
38
+ { hour: '6pm', orders: 52 },
39
+ ]
40
+
41
+ const config = {
42
+ orders: { label: 'Orders', color: 'var(--chart-1)' },
43
+ } satisfies ChartConfig
44
+
45
+ const locations = [
46
+ { name: 'Riverside', sales: '$4,812', percent: 96 },
47
+ { name: 'Harbour', sales: '$4,140', percent: 83 },
48
+ { name: 'North yard', sales: '$3,365', percent: 67 },
49
+ { name: 'Market street', sales: '$2,210', percent: 44 },
50
+ ]
51
+ </script>
52
+
53
+ {#snippet salesDelta()}
54
+ <DeltaBadge value={12.4} />
55
+ {/snippet}
56
+ {#snippet salesTrend()}
57
+ <Sparkline data={[18, 22, 21, 27, 25, 31, 34]} tone="success" height={36} />
58
+ {/snippet}
59
+ {#snippet refundsDelta()}
60
+ <DeltaBadge value={3.1} invert />
61
+ {/snippet}
62
+ {#snippet exportButton()}
63
+ <Button variant="outline">Export</Button>
64
+ {/snippet}
65
+ {#snippet tooltip()}
66
+ <ChartTooltip indicator="dot" />
67
+ {/snippet}
68
+
69
+ <PageLayout>
70
+ <PageHeader
71
+ title="Today"
72
+ description="Riverside, since the register opened at 7:30am."
73
+ actions={exportButton}
74
+ />
75
+
76
+ <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
77
+ <StatTile
78
+ label="Sales"
79
+ value="$14,527"
80
+ exactValue="$14,527.40"
81
+ delta={salesDelta}
82
+ visual={salesTrend}
83
+ />
84
+ <StatTile label="Orders" value={234} unit="orders" />
85
+ <StatTile label="Average basket" value="$62.08" unit="per order" />
86
+ <StatTile label="Refunds" value="$318" delta={refundsDelta} tone="warning" />
87
+ </div>
88
+
89
+ <Card>
90
+ <CardHeader>
91
+ <CardTitle>Orders by hour</CardTitle>
92
+ <CardDescription>Placed at every register, all channels.</CardDescription>
93
+ </CardHeader>
94
+ <CardContent>
95
+ <ChartContainer class="aspect-auto h-[240px] w-full" {config}>
96
+ <AreaChart
97
+ data={byHour}
98
+ x="hour"
99
+ xScale={scalePoint().padding(0.2)}
100
+ grid={chartGrid}
101
+ yNice={4}
102
+ series={[{ key: 'orders', label: 'Orders', color: 'var(--color-orders)' }]}
103
+ props={{
104
+ area: {
105
+ curve: curveMonotoneX,
106
+ fillOpacity: chartAreaFillOpacity,
107
+ line: { strokeWidth: chartLineStrokeWidth },
108
+ },
109
+ }}
110
+ {tooltip}
111
+ />
112
+ </ChartContainer>
113
+ </CardContent>
114
+ </Card>
115
+
116
+ <Card>
117
+ <CardHeader>
118
+ <CardTitle>Sales by location</CardTitle>
119
+ <CardDescription>Against each location's daily target.</CardDescription>
120
+ </CardHeader>
121
+ <CardContent class="flex flex-col gap-4">
122
+ {#each locations as location, index (location.name)}
123
+ <MetricBar
124
+ rank={index + 1}
125
+ label={location.name}
126
+ value={location.sales}
127
+ percent={location.percent}
128
+ />
129
+ {/each}
130
+ </CardContent>
131
+ </Card>
132
+ </PageLayout>
@@ -0,0 +1,39 @@
1
+ # Recipe: a data table
2
+
3
+ Today's orders, with search, a status filter, sortable columns, row selection for a bulk action,
4
+ and a menu of actions on each row. The code is [data-table.svelte](data-table.svelte), which
5
+ type-checks against the current mise.
6
+
7
+ ## What it is made of
8
+
9
+ - `DataTable` ([data display](../components/data-display.md)): the card, the table, the empty
10
+ state and the closing strip with the row count.
11
+ - `dataTableColumns<Order>()` to build typed columns, `DataTableColumnHeader` for a sortable
12
+ header, and `renderSnippet` or `renderComponent` from `@tanstack/svelte-table` for cells.
13
+ - `DataTableToolbar` above the rows: the search, the faceted status filter, the column toggle,
14
+ and the app's bulk actions in `actions`.
15
+ - `DataTableRowActions` with `DropdownMenuItem`s ([actions](../components/actions.md)): the ⋯
16
+ menu at the end of each row.
17
+ - `Badge` with a `tone` per status, and `Empty` for "no orders match".
18
+
19
+ ## Decisions worth copying
20
+
21
+ - **`getRowId` whenever rows can be selected.** Without it rows are keyed by index, and a
22
+ selection survives neither a sort nor a new page.
23
+ - **A faceted filter needs a `filterFn`.** The status filter sets a list of values; the column's
24
+ `filterFn` keeps a row whose status is in it.
25
+ - **Status is data, tone is presentation.** A `STATUS_TONE` map turns each status into a `tone`,
26
+ in one place.
27
+ - **Name the row in its actions label** ("Actions for order #1042"), so a screen reader moving
28
+ down the column hears which row the menu belongs to.
29
+ - **The destructive action is last** and uses `variant="destructive"`; it is disabled, not
30
+ hidden, where it does not apply.
31
+ - **Server data:** for a long list, keep the rows on the server. Pass `filters` and
32
+ `onFiltersChange` to send the search, filters and sort with the request, and `hasNextPage`
33
+ with `fetchNextPage` for load-more.
34
+
35
+ ## Dependencies
36
+
37
+ The columns import `renderSnippet` and `renderComponent` from `@tanstack/svelte-table`. mise
38
+ depends on it, but a strict package manager only lets the app import what it declares:
39
+ `pnpm add @tanstack/svelte-table`.
@@ -0,0 +1,130 @@
1
+ <!--
2
+ Today's orders: search, a status filter, sortable columns, row selection for
3
+ a bulk action, and a menu of actions on every row. DataTable owns the table
4
+ (TanStack Table underneath); the app owns the rows, the columns and what each
5
+ action does.
6
+ -->
7
+ <script lang="ts">
8
+ import {
9
+ Badge,
10
+ Button,
11
+ DataTable,
12
+ DataTableColumnHeader,
13
+ DataTableRowActions,
14
+ DataTableToolbar,
15
+ DropdownMenuItem,
16
+ DropdownMenuSeparator,
17
+ dataTableColumns,
18
+ Empty,
19
+ EmptyDescription,
20
+ EmptyHeader,
21
+ EmptyTitle,
22
+ type Tone,
23
+ } from '@jovertical/mise-design-system'
24
+ import { type RowSelectionState, renderComponent, renderSnippet } from '@tanstack/svelte-table'
25
+
26
+ type Order = {
27
+ id: string
28
+ number: string
29
+ location: string
30
+ status: 'open' | 'ready' | 'collected' | 'refunded'
31
+ /** Formatted by the app; mise has no opinion about currencies. */
32
+ total: string
33
+ }
34
+
35
+ const orders: Order[] = [
36
+ { id: '1', number: '#1042', location: 'Riverside', status: 'open', total: '$48.20' },
37
+ { id: '2', number: '#1043', location: 'Riverside', status: 'ready', total: '$12.75' },
38
+ { id: '3', number: '#1044', location: 'Harbour', status: 'collected', total: '$96.00' },
39
+ { id: '4', number: '#1045', location: 'North yard', status: 'refunded', total: '$22.10' },
40
+ ]
41
+
42
+ const STATUS_TONE: Record<Order['status'], Tone> = {
43
+ open: 'info',
44
+ ready: 'success',
45
+ collected: 'neutral',
46
+ refunded: 'warning',
47
+ }
48
+
49
+ const STATUS_OPTIONS = [
50
+ { label: 'Open', value: 'open' },
51
+ { label: 'Ready', value: 'ready' },
52
+ { label: 'Collected', value: 'collected' },
53
+ { label: 'Refunded', value: 'refunded' },
54
+ ]
55
+
56
+ let selection = $state<RowSelectionState>({})
57
+ const selectedCount = $derived(Object.keys(selection).length)
58
+
59
+ const column = dataTableColumns<Order>()
60
+
61
+ const columns = column.columns([
62
+ column.accessor('number', {
63
+ header: ({ column }) => renderComponent(DataTableColumnHeader, { column, title: 'Order' }),
64
+ }),
65
+ column.accessor('location', {
66
+ header: ({ column }) => renderComponent(DataTableColumnHeader, { column, title: 'Location' }),
67
+ }),
68
+ column.accessor('status', {
69
+ header: ({ column }) => renderComponent(DataTableColumnHeader, { column, title: 'Status' }),
70
+ cell: ({ row }) => renderSnippet(statusCell, row.original),
71
+ // The status filter passes a list of values; keep rows whose status is in it.
72
+ filterFn: (row, id, values: string[]) => values.includes(row.getValue(id)),
73
+ }),
74
+ column.accessor('total', { header: 'Total', enableSorting: false }),
75
+ column.display({
76
+ id: 'actions',
77
+ cell: ({ row }) => renderSnippet(actionsCell, row.original),
78
+ }),
79
+ ])
80
+ </script>
81
+
82
+ {#snippet statusCell(order: Order)}
83
+ <Badge tone={STATUS_TONE[order.status]} class="capitalize">{order.status}</Badge>
84
+ {/snippet}
85
+
86
+ {#snippet actionsCell(order: Order)}
87
+ <DataTableRowActions label={`Actions for order ${order.number}`}>
88
+ <DropdownMenuItem>View order</DropdownMenuItem>
89
+ <DropdownMenuItem>Print receipt</DropdownMenuItem>
90
+ <DropdownMenuSeparator />
91
+ <DropdownMenuItem variant="destructive" disabled={order.status === 'refunded'}>
92
+ Refund
93
+ </DropdownMenuItem>
94
+ </DataTableRowActions>
95
+ {/snippet}
96
+
97
+ {#snippet noOrders()}
98
+ <Empty>
99
+ <EmptyHeader>
100
+ <EmptyTitle>No orders match</EmptyTitle>
101
+ <EmptyDescription>Clear the search or the status filter.</EmptyDescription>
102
+ </EmptyHeader>
103
+ </Empty>
104
+ {/snippet}
105
+
106
+ {#snippet bulkActions()}
107
+ {#if selectedCount > 0}
108
+ <Button variant="outline" size="sm">Print {selectedCount} receipts</Button>
109
+ {/if}
110
+ {/snippet}
111
+
112
+ <DataTable
113
+ title="Today's orders"
114
+ {columns}
115
+ data={orders}
116
+ defaultSort="-number"
117
+ getRowId={(order) => order.id}
118
+ enableRowSelection
119
+ bind:rowSelection={selection}
120
+ emptyState={noOrders}
121
+ >
122
+ {#snippet toolbar(table)}
123
+ <DataTableToolbar
124
+ {table}
125
+ search={{ placeholder: 'Search orders…' }}
126
+ filters={[{ columnId: 'status', title: 'Status', options: STATUS_OPTIONS }]}
127
+ actions={bulkActions}
128
+ />
129
+ {/snippet}
130
+ </DataTable>