@mystravil/hardpoint 0.0.0-stage → 0.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.
Files changed (112) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +25 -0
  3. package/README.md +64 -3
  4. package/dist/base.css +346 -0
  5. package/dist/components/AppShell.svelte +698 -0
  6. package/dist/components/AppShell.svelte.d.ts +77 -0
  7. package/dist/components/Button.svelte +174 -0
  8. package/dist/components/Button.svelte.d.ts +20 -0
  9. package/dist/components/Checkbox.svelte +198 -0
  10. package/dist/components/Checkbox.svelte.d.ts +16 -0
  11. package/dist/components/CheckboxGroup.svelte +99 -0
  12. package/dist/components/CheckboxGroup.svelte.d.ts +25 -0
  13. package/dist/components/Combobox.svelte +824 -0
  14. package/dist/components/Combobox.svelte.d.ts +63 -0
  15. package/dist/components/Dialog.svelte +356 -0
  16. package/dist/components/Dialog.svelte.d.ts +43 -0
  17. package/dist/components/Disclosure.svelte +201 -0
  18. package/dist/components/Disclosure.svelte.d.ts +27 -0
  19. package/dist/components/EmptyState.svelte +88 -0
  20. package/dist/components/EmptyState.svelte.d.ts +22 -0
  21. package/dist/components/ErrorState.svelte +135 -0
  22. package/dist/components/ErrorState.svelte.d.ts +26 -0
  23. package/dist/components/KeyValueList.svelte +114 -0
  24. package/dist/components/KeyValueList.svelte.d.ts +43 -0
  25. package/dist/components/LoadRegion.svelte +219 -0
  26. package/dist/components/LoadRegion.svelte.d.ts +27 -0
  27. package/dist/components/MediaFrame.svelte +110 -0
  28. package/dist/components/MediaFrame.svelte.d.ts +20 -0
  29. package/dist/components/MediaImage.svelte +256 -0
  30. package/dist/components/MediaImage.svelte.d.ts +16 -0
  31. package/dist/components/Menu.svelte +258 -0
  32. package/dist/components/Menu.svelte.d.ts +36 -0
  33. package/dist/components/MenuCheckboxItem.svelte +119 -0
  34. package/dist/components/MenuCheckboxItem.svelte.d.ts +15 -0
  35. package/dist/components/MenuGroup.svelte +50 -0
  36. package/dist/components/MenuGroup.svelte.d.ts +15 -0
  37. package/dist/components/MenuItem.svelte +94 -0
  38. package/dist/components/MenuItem.svelte.d.ts +19 -0
  39. package/dist/components/MenuRadioGroup.svelte +62 -0
  40. package/dist/components/MenuRadioGroup.svelte.d.ts +20 -0
  41. package/dist/components/MenuRadioItem.svelte +110 -0
  42. package/dist/components/MenuRadioItem.svelte.d.ts +17 -0
  43. package/dist/components/MenuSeparator.svelte +28 -0
  44. package/dist/components/MenuSeparator.svelte.d.ts +6 -0
  45. package/dist/components/NativeSelect.svelte +119 -0
  46. package/dist/components/NativeSelect.svelte.d.ts +46 -0
  47. package/dist/components/Pagination.svelte +132 -0
  48. package/dist/components/Pagination.svelte.d.ts +18 -0
  49. package/dist/components/Popover.svelte +222 -0
  50. package/dist/components/Popover.svelte.d.ts +35 -0
  51. package/dist/components/Progress.svelte +157 -0
  52. package/dist/components/Progress.svelte.d.ts +34 -0
  53. package/dist/components/Select.svelte +405 -0
  54. package/dist/components/Select.svelte.d.ts +42 -0
  55. package/dist/components/Skeleton.svelte +149 -0
  56. package/dist/components/Skeleton.svelte.d.ts +24 -0
  57. package/dist/components/Spinner.svelte +117 -0
  58. package/dist/components/Spinner.svelte.d.ts +13 -0
  59. package/dist/components/Stat.svelte +253 -0
  60. package/dist/components/Stat.svelte.d.ts +46 -0
  61. package/dist/components/Steps.svelte +189 -0
  62. package/dist/components/Steps.svelte.d.ts +31 -0
  63. package/dist/components/Switch.svelte +222 -0
  64. package/dist/components/Switch.svelte.d.ts +18 -0
  65. package/dist/components/Table.svelte +504 -0
  66. package/dist/components/Table.svelte.d.ts +92 -0
  67. package/dist/components/Tabs.svelte +246 -0
  68. package/dist/components/Tabs.svelte.d.ts +44 -0
  69. package/dist/components/Tag.svelte +40 -0
  70. package/dist/components/Tag.svelte.d.ts +15 -0
  71. package/dist/components/TextField.svelte +256 -0
  72. package/dist/components/TextField.svelte.d.ts +32 -0
  73. package/dist/components/Timeline.svelte +156 -0
  74. package/dist/components/Timeline.svelte.d.ts +30 -0
  75. package/dist/components/Toaster.svelte +411 -0
  76. package/dist/components/Toaster.svelte.d.ts +11 -0
  77. package/dist/components/Tooltip.svelte +200 -0
  78. package/dist/components/Tooltip.svelte.d.ts +38 -0
  79. package/dist/components/TooltipGroup.svelte +46 -0
  80. package/dist/components/TooltipGroup.svelte.d.ts +29 -0
  81. package/dist/components/internal/Scrim.svelte +52 -0
  82. package/dist/components/internal/Scrim.svelte.d.ts +15 -0
  83. package/dist/components/internal/Sheet.svelte +263 -0
  84. package/dist/components/internal/Sheet.svelte.d.ts +36 -0
  85. package/dist/components/internal/fade-in.d.ts +10 -0
  86. package/dist/components/internal/fade-in.js +20 -0
  87. package/dist/components/internal/resolve.d.ts +8 -0
  88. package/dist/components/internal/resolve.js +247 -0
  89. package/dist/components/internal/swipe-dismiss.d.ts +47 -0
  90. package/dist/components/internal/swipe-dismiss.js +220 -0
  91. package/dist/components/internal/visual-viewport.svelte.d.ts +35 -0
  92. package/dist/components/internal/visual-viewport.svelte.js +79 -0
  93. package/dist/components/toast.svelte.d.ts +57 -0
  94. package/dist/components/toast.svelte.js +84 -0
  95. package/dist/fonts/OFL-geist-pixel.txt +92 -0
  96. package/dist/fonts/OFL.txt +93 -0
  97. package/dist/fonts/geist-latin-variable.woff2 +0 -0
  98. package/dist/fonts/geist-mono-latin-variable.woff2 +0 -0
  99. package/dist/fonts/geist-pixel-square.woff2 +0 -0
  100. package/dist/fonts.css +71 -0
  101. package/dist/index.d.ts +39 -0
  102. package/dist/index.js +39 -0
  103. package/dist/motion/Motion.svelte +15 -0
  104. package/dist/motion/Motion.svelte.d.ts +9 -0
  105. package/dist/motion/context.d.ts +18 -0
  106. package/dist/motion/context.js +26 -0
  107. package/dist/motion/index.d.ts +3 -0
  108. package/dist/motion/index.js +3 -0
  109. package/dist/motion/transitions.d.ts +31 -0
  110. package/dist/motion/transitions.js +112 -0
  111. package/dist/tokens.css +279 -0
  112. package/package.json +76 -6
@@ -0,0 +1,504 @@
1
+ <script lang="ts" module>
2
+ /** A status hue: `ok` done, `warn` waiting or needs you, `danger` failed. */
3
+ export type TableMark = 'ok' | 'warn' | 'danger';
4
+
5
+ /** `plain` (default): no row dividers. `ruled`: 1px dividers, for long dense tables. */
6
+ export type TableVariant = 'plain' | 'ruled';
7
+
8
+ /** Breakpoints a column can hide below: 900px (`expanded`) or 1200px (`large`). */
9
+ export type TableBreakpoint = 'expanded' | 'large';
10
+
11
+ export type Column<Row> = {
12
+ /** The row field this column shows. Also the column's identity. */
13
+ key: keyof Row & string;
14
+ /** The caps column label in the header band and in the stacked list. */
15
+ label: string;
16
+ /** Text alignment. Defaults to `start`. */
17
+ align?: 'start' | 'end';
18
+ /** Right-aligned tabular numbers. */
19
+ numeric?: boolean;
20
+ /**
21
+ * Any CSS width for the column's `<col>`, such as `calc(10ch + 24px)` or
22
+ * `12%`. Columns without one share the rest. To change a width by
23
+ * breakpoint, pass a custom property (`var(--w-name)`) and set it in
24
+ * the page's media queries.
25
+ */
26
+ width?: string;
27
+ /** Hide the column below 900px (`expanded`) or 1200px (`large`). */
28
+ hideBelow?: TableBreakpoint;
29
+ /**
30
+ * A square status mark before the value, in that status's hue. The
31
+ * mark hangs left of the text, so marked and unmarked values keep the
32
+ * same text edge. Colour only where the value means that status.
33
+ */
34
+ mark?: (row: Row) => TableMark | undefined;
35
+ };
36
+
37
+ /** The stacked list on compact widths: one title, two or three key-value lines. */
38
+ export type TableStack<Row> = {
39
+ title: keyof Row & string;
40
+ fields: (keyof Row & string)[];
41
+ };
42
+ </script>
43
+
44
+ <script lang="ts" generics="Row">
45
+ import { onMount, untrack, type Snippet } from 'svelte';
46
+ import type { HTMLAttributes } from 'svelte/elements';
47
+ import { useMotion } from '../motion/index.js';
48
+
49
+ type Key = keyof Row & string;
50
+
51
+ let {
52
+ rows,
53
+ columns,
54
+ key,
55
+ label,
56
+ variant = 'plain',
57
+ selected,
58
+ rowHref,
59
+ stack,
60
+ total,
61
+ totalLabel = 'Total',
62
+ cell,
63
+ empty,
64
+ footer,
65
+ class: className,
66
+ ...rest
67
+ }: Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
68
+ rows: Row[];
69
+ columns: Column<Row>[];
70
+ /** The row field that identifies a row: keys the rows and matches `selected`. */
71
+ key: Key;
72
+ /** The table's accessible name. */
73
+ label: string;
74
+ variant?: TableVariant;
75
+ /** The key of the current row: a 3px titanium edge and the selected tint. */
76
+ selected?: Row[Key];
77
+ /** Makes each whole row one link to this address. */
78
+ rowHref?: (row: Row) => string;
79
+ /**
80
+ * Compact widths show a stacked list. Defaults to the first column as
81
+ * the title and the next three as key-value lines.
82
+ */
83
+ stack?: TableStack<Row>;
84
+ /** The count after the caps TOTAL label in the footer row. */
85
+ total?: number;
86
+ totalLabel?: string;
87
+ /** Renders one cell, in the table and in the stacked list. Default: the value. */
88
+ cell?: Snippet<[{ row: Row; column: Column<Row>; value: Row[Key] }]>;
89
+ /** Shown under the header band when there are no rows. */
90
+ empty?: Snippet;
91
+ /** The footer row's end, beside the total: pagination goes here. */
92
+ footer?: Snippet;
93
+ } = $props();
94
+
95
+ const stackTitle = $derived(
96
+ columns.find((column) => column.key === (stack?.title ?? columns[0]?.key)) ?? columns[0]
97
+ );
98
+ const stackFields = $derived(
99
+ stack
100
+ ? stack.fields
101
+ .map((field) => columns.find((column) => column.key === field))
102
+ .filter((column) => column !== undefined)
103
+ : columns.slice(1, 4)
104
+ );
105
+
106
+ const isSelected = (row: Row) => selected !== undefined && row[key] === selected;
107
+
108
+ const cellAttrs = (column: Column<Row>) => ({
109
+ 'data-align': column.numeric || column.align === 'end' ? 'end' : undefined,
110
+ 'data-numeric': column.numeric ? '' : undefined,
111
+ 'data-mark': column.mark ? '' : undefined,
112
+ 'data-hide-below': column.hideBelow
113
+ });
114
+
115
+ // Expressive stagger (motion rule 7): only the rows of the first render
116
+ // enter, by their index. New rows from filtering, sorting or paging never
117
+ // do, so any change to `rows` ends the entrance, and so does its last
118
+ // animation (a kept row would replay it when a sort moves it).
119
+ const motion = useMotion();
120
+ const firstRows = untrack(() => rows);
121
+ let entering = $state.raw<Map<Row[Key], number> | null>(
122
+ untrack(() => (motion.expressive ? new Map(rows.map((row, i) => [row[key], i])) : null))
123
+ );
124
+ let root: HTMLDivElement;
125
+
126
+ $effect.pre(() => {
127
+ if (rows !== firstRows) entering = null;
128
+ });
129
+
130
+ onMount(() => {
131
+ if (!entering) return;
132
+ const running = root
133
+ .getAnimations({ subtree: true })
134
+ .filter((a) => a instanceof CSSAnimation && a.animationName === 'hp-enter');
135
+ Promise.allSettled(running.map((a) => a.finished)).then(() => {
136
+ entering = null;
137
+ });
138
+ });
139
+ </script>
140
+
141
+ {#snippet content(row: Row, column: Column<Row>)}
142
+ {#if column.mark}
143
+ <span class="marked" data-tone={column.mark(row)}>{@render cellValue(row, column)}</span>
144
+ {:else}
145
+ {@render cellValue(row, column)}
146
+ {/if}
147
+ {/snippet}
148
+
149
+ {#snippet cellValue(row: Row, column: Column<Row>)}
150
+ {#if cell}{@render cell({ row, column, value: row[column.key] })}{:else}{row[column.key]}{/if}
151
+ {/snippet}
152
+
153
+ {#snippet item(row: Row)}
154
+ <span class="item-title">{@render content(row, stackTitle)}</span>
155
+ <dl class="item-fields">
156
+ {#each stackFields as column (column.key)}
157
+ <div>
158
+ <dt class="hp-label">{column.label}</dt>
159
+ <dd data-mark={column.mark ? '' : undefined}>{@render content(row, column)}</dd>
160
+ </div>
161
+ {/each}
162
+ </dl>
163
+ {/snippet}
164
+
165
+ <div {...rest} bind:this={root} class={['hp-table', className]} data-variant={variant}>
166
+ <!-- Medium and up: one header band, mono rows. -->
167
+ <table class="grid" aria-label={label}>
168
+ <colgroup>
169
+ {#each columns as column (column.key)}
170
+ <col style:width={column.width} data-hide-below={column.hideBelow} />
171
+ {/each}
172
+ </colgroup>
173
+ <thead>
174
+ <tr>
175
+ {#each columns as column (column.key)}
176
+ <th class="hp-label" scope="col" {...cellAttrs(column)}>{column.label}</th>
177
+ {/each}
178
+ </tr>
179
+ </thead>
180
+ <tbody>
181
+ {#each rows as row (row[key])}
182
+ <tr
183
+ class={[entering?.has(row[key]) && 'hp-enter']}
184
+ style:--i={entering?.get(row[key])}
185
+ data-selected={isSelected(row) ? '' : undefined}
186
+ aria-current={isSelected(row) ? 'true' : undefined}
187
+ >
188
+ {#each columns as column, c (column.key)}
189
+ <td {...cellAttrs(column)}>
190
+ {#if c === 0 && rowHref}
191
+ <a class="row-link" href={rowHref(row)}>{@render content(row, column)}</a>
192
+ {:else}
193
+ {@render content(row, column)}
194
+ {/if}
195
+ </td>
196
+ {/each}
197
+ </tr>
198
+ {/each}
199
+ </tbody>
200
+ </table>
201
+
202
+ <!-- Compact: a stacked list; one title, key-value lines, the whole item the target. -->
203
+ {#if rows.length > 0}
204
+ <ul class="stack" aria-label={label}>
205
+ {#each rows as row (row[key])}
206
+ <li
207
+ class={[entering?.has(row[key]) && 'hp-enter']}
208
+ style:--i={entering?.get(row[key])}
209
+ data-selected={isSelected(row) ? '' : undefined}
210
+ aria-current={isSelected(row) ? 'true' : undefined}
211
+ >
212
+ {#if rowHref}
213
+ <a class="item" href={rowHref(row)}>{@render item(row)}</a>
214
+ {:else}
215
+ <div class="item">{@render item(row)}</div>
216
+ {/if}
217
+ </li>
218
+ {/each}
219
+ </ul>
220
+ {:else if empty}
221
+ <div class="empty">{@render empty()}</div>
222
+ {/if}
223
+
224
+ {#if total !== undefined || footer}
225
+ <div class="foot">
226
+ {#if total !== undefined}
227
+ <p class="total">
228
+ <span class="hp-label">{totalLabel}</span>
229
+ <span class="figure">{total}</span>
230
+ </p>
231
+ {/if}
232
+ {@render footer?.()}
233
+ </div>
234
+ {/if}
235
+ </div>
236
+
237
+ <style>
238
+ /* ---- Table: medium and up ---------------------------------------- */
239
+
240
+ /* The band and the row tints reach --bleed past the text edge. */
241
+ .grid {
242
+ width: calc(100% + 2 * var(--bleed));
243
+ margin-inline: calc(-1 * var(--bleed));
244
+ border-collapse: collapse;
245
+ table-layout: fixed;
246
+ font-family: var(--mono);
247
+ font-size: var(--type-body);
248
+ line-height: 24px;
249
+ }
250
+ @media (pointer: fine) {
251
+ .grid {
252
+ font-size: var(--type-dense);
253
+ line-height: 20px;
254
+ }
255
+ }
256
+
257
+ th,
258
+ td {
259
+ padding: 0 var(--space-6) 0 0;
260
+ overflow: hidden;
261
+ text-align: start;
262
+ text-overflow: ellipsis;
263
+ white-space: nowrap;
264
+ }
265
+ th:first-child,
266
+ td:first-child {
267
+ padding-left: var(--bleed);
268
+ }
269
+ th:last-child,
270
+ td:last-child {
271
+ padding-right: var(--bleed);
272
+ }
273
+ [data-align='end'] {
274
+ text-align: end;
275
+ }
276
+ [data-numeric] {
277
+ font-variant-numeric: tabular-nums;
278
+ }
279
+
280
+ /* The one band. It pins under the shell's header row. */
281
+ thead th {
282
+ position: sticky;
283
+ top: calc(var(--header-height) + env(safe-area-inset-top));
284
+ z-index: var(--layer-sticky);
285
+ height: 36px;
286
+ background: var(--surface-band);
287
+ color: var(--text);
288
+ }
289
+ /* Space between the band and the first row, not a line. */
290
+ tbody::before {
291
+ content: '';
292
+ display: table-row;
293
+ height: var(--space-2);
294
+ }
295
+
296
+ tbody tr {
297
+ position: relative;
298
+ }
299
+ tbody td {
300
+ height: var(--control-height);
301
+ }
302
+
303
+ /* The first cell's link covers the row, so the whole row is the
304
+ target; its focus contour is drawn inside the row. */
305
+ .row-link {
306
+ text-decoration: none;
307
+ -webkit-tap-highlight-color: transparent;
308
+ }
309
+ .row-link::after {
310
+ content: '';
311
+ position: absolute;
312
+ inset: 0;
313
+ }
314
+ .row-link:focus-visible {
315
+ outline: none;
316
+ }
317
+ .row-link:focus-visible::after {
318
+ outline: 2px solid var(--focus);
319
+ outline-offset: -2px;
320
+ }
321
+
322
+ /* Hover on pointer devices, press for links. Hover and press never
323
+ darken a selected row. */
324
+ @media (hover: hover) and (pointer: fine) {
325
+ tbody tr:not([data-selected]):hover {
326
+ background: var(--surface-hover);
327
+ }
328
+ }
329
+ tbody tr:not([data-selected]):has(.row-link):active {
330
+ background: var(--surface-hover);
331
+ }
332
+
333
+ /* Selection: a 3px titanium edge and the tint, text unchanged. */
334
+ tbody tr[data-selected] {
335
+ background: var(--surface-selected);
336
+ }
337
+ tr[data-selected] td:first-child {
338
+ box-shadow: inset 3px 0 0 var(--accent);
339
+ }
340
+
341
+ /* Ruled: 1px dividers between rows, drawn as shadows so the pitch
342
+ stays even. */
343
+ [data-variant='ruled'] tbody tr + tr td {
344
+ box-shadow: inset 0 1px 0 var(--border);
345
+ }
346
+ [data-variant='ruled'] tbody tr + tr[data-selected] td:first-child {
347
+ box-shadow:
348
+ inset 3px 0 0 var(--accent),
349
+ inset 0 1px 0 var(--border);
350
+ }
351
+
352
+ /* A marked column leaves room before its text for the mark. */
353
+ th[data-mark],
354
+ td[data-mark] {
355
+ padding-left: var(--space-4);
356
+ }
357
+
358
+ @media (max-width: 899px) {
359
+ [data-hide-below='expanded'] {
360
+ display: none;
361
+ }
362
+ }
363
+ @media (max-width: 1199px) {
364
+ [data-hide-below='large'] {
365
+ display: none;
366
+ }
367
+ }
368
+
369
+ /* ---- Status mark ---------------------------------------------------- */
370
+
371
+ /* An 8px square in the status hue, hanging left of the word so every
372
+ value keeps the column's text edge. */
373
+ .marked[data-tone]::before {
374
+ content: '';
375
+ display: inline-block;
376
+ width: 8px;
377
+ height: 8px;
378
+ margin: 0 var(--space-2) 0 calc(-1 * var(--space-4));
379
+ vertical-align: 1px;
380
+ }
381
+ .marked[data-tone='ok']::before {
382
+ background: var(--ok);
383
+ }
384
+ .marked[data-tone='warn']::before {
385
+ background: var(--warn);
386
+ }
387
+ .marked[data-tone='danger']::before {
388
+ background: var(--danger);
389
+ }
390
+
391
+ /* ---- Stacked list: compact ---------------------------------------- */
392
+
393
+ .stack {
394
+ display: none;
395
+ margin: 0 calc(-1 * var(--bleed));
396
+ padding: 0;
397
+ list-style: none;
398
+ }
399
+ @media (max-width: 599px) {
400
+ .grid {
401
+ display: none;
402
+ }
403
+ .stack {
404
+ display: grid;
405
+ }
406
+ }
407
+
408
+ .item {
409
+ display: grid;
410
+ gap: var(--space-2);
411
+ min-height: var(--hit-target);
412
+ padding: var(--space-3) var(--bleed);
413
+ text-decoration: none;
414
+ -webkit-tap-highlight-color: transparent;
415
+ }
416
+ .item:focus-visible {
417
+ outline-offset: -2px;
418
+ }
419
+ .item-title {
420
+ font-weight: 500;
421
+ }
422
+ /* One label column per item, at least 72px; a longer label widens it
423
+ for every line of the item, so the values keep one edge. */
424
+ .item-fields {
425
+ display: grid;
426
+ grid-template-columns: minmax(72px, max-content) minmax(0, 1fr);
427
+ gap: 2px var(--space-3);
428
+ font-family: var(--mono);
429
+ }
430
+ .item-fields div {
431
+ display: grid;
432
+ grid-column: 1 / -1;
433
+ grid-template-columns: subgrid;
434
+ align-items: baseline;
435
+ }
436
+ .item-fields dt {
437
+ color: var(--muted);
438
+ }
439
+ .item-fields dd {
440
+ min-width: 0;
441
+ overflow: hidden;
442
+ text-overflow: ellipsis;
443
+ white-space: nowrap;
444
+ }
445
+ /* The mark hangs into the gap before the value. */
446
+ .item-fields dd[data-mark] {
447
+ overflow: visible;
448
+ }
449
+
450
+ @media (hover: hover) and (pointer: fine) {
451
+ .stack li:not([data-selected]) .item:hover {
452
+ background: var(--surface-hover);
453
+ }
454
+ }
455
+ .stack li:not([data-selected]) a.item:active {
456
+ background: var(--surface-hover);
457
+ }
458
+ .stack li[data-selected] {
459
+ background: var(--surface-selected);
460
+ box-shadow: inset 3px 0 0 var(--accent);
461
+ }
462
+
463
+ [data-variant='ruled'] .stack li + li {
464
+ box-shadow: inset 0 1px 0 var(--border);
465
+ }
466
+ [data-variant='ruled'] .stack li + li[data-selected] {
467
+ box-shadow:
468
+ inset 3px 0 0 var(--accent),
469
+ inset 0 1px 0 var(--border);
470
+ }
471
+
472
+ /* ---- Empty state ---------------------------------------------------- */
473
+
474
+ .empty {
475
+ display: grid;
476
+ justify-items: start;
477
+ gap: var(--space-2);
478
+ padding: var(--space-8) 0;
479
+ }
480
+
481
+ /* ---- Footer row: total, then the page's footer (pagination) -------- */
482
+
483
+ .foot {
484
+ display: flex;
485
+ flex-wrap: wrap;
486
+ align-items: center;
487
+ justify-content: space-between;
488
+ gap: var(--space-4) var(--space-8);
489
+ margin-top: var(--space-8);
490
+ }
491
+ /* Without a total, the footer (pagination) still ends the row. */
492
+ .foot:not(:has(> .total)) {
493
+ justify-content: flex-end;
494
+ }
495
+ .total {
496
+ display: flex;
497
+ align-items: center;
498
+ gap: var(--space-4);
499
+ }
500
+ .figure {
501
+ font-family: var(--mono);
502
+ font-variant-numeric: tabular-nums;
503
+ }
504
+ </style>
@@ -0,0 +1,92 @@
1
+ /** A status hue: `ok` done, `warn` waiting or needs you, `danger` failed. */
2
+ export type TableMark = 'ok' | 'warn' | 'danger';
3
+ /** `plain` (default): no row dividers. `ruled`: 1px dividers, for long dense tables. */
4
+ export type TableVariant = 'plain' | 'ruled';
5
+ /** Breakpoints a column can hide below: 900px (`expanded`) or 1200px (`large`). */
6
+ export type TableBreakpoint = 'expanded' | 'large';
7
+ export type Column<Row> = {
8
+ /** The row field this column shows. Also the column's identity. */
9
+ key: keyof Row & string;
10
+ /** The caps column label in the header band and in the stacked list. */
11
+ label: string;
12
+ /** Text alignment. Defaults to `start`. */
13
+ align?: 'start' | 'end';
14
+ /** Right-aligned tabular numbers. */
15
+ numeric?: boolean;
16
+ /**
17
+ * Any CSS width for the column's `<col>`, such as `calc(10ch + 24px)` or
18
+ * `12%`. Columns without one share the rest. To change a width by
19
+ * breakpoint, pass a custom property (`var(--w-name)`) and set it in
20
+ * the page's media queries.
21
+ */
22
+ width?: string;
23
+ /** Hide the column below 900px (`expanded`) or 1200px (`large`). */
24
+ hideBelow?: TableBreakpoint;
25
+ /**
26
+ * A square status mark before the value, in that status's hue. The
27
+ * mark hangs left of the text, so marked and unmarked values keep the
28
+ * same text edge. Colour only where the value means that status.
29
+ */
30
+ mark?: (row: Row) => TableMark | undefined;
31
+ };
32
+ /** The stacked list on compact widths: one title, two or three key-value lines. */
33
+ export type TableStack<Row> = {
34
+ title: keyof Row & string;
35
+ fields: (keyof Row & string)[];
36
+ };
37
+ import { type Snippet } from 'svelte';
38
+ import type { HTMLAttributes } from 'svelte/elements';
39
+ declare function $$render<Row>(): {
40
+ props: Omit<HTMLAttributes<HTMLDivElement>, "children"> & {
41
+ rows: Row[];
42
+ columns: Column<Row>[];
43
+ /** The row field that identifies a row: keys the rows and matches `selected`. */
44
+ key: keyof Row & string;
45
+ /** The table's accessible name. */
46
+ label: string;
47
+ variant?: TableVariant;
48
+ /** The key of the current row: a 3px titanium edge and the selected tint. */
49
+ selected?: Row[keyof Row & string];
50
+ /** Makes each whole row one link to this address. */
51
+ rowHref?: (row: Row) => string;
52
+ /**
53
+ * Compact widths show a stacked list. Defaults to the first column as
54
+ * the title and the next three as key-value lines.
55
+ */
56
+ stack?: TableStack<Row>;
57
+ /** The count after the caps TOTAL label in the footer row. */
58
+ total?: number;
59
+ totalLabel?: string;
60
+ /** Renders one cell, in the table and in the stacked list. Default: the value. */
61
+ cell?: Snippet<[{
62
+ row: Row;
63
+ column: Column<Row>;
64
+ value: Row[keyof Row & string];
65
+ }]>;
66
+ /** Shown under the header band when there are no rows. */
67
+ empty?: Snippet;
68
+ /** The footer row's end, beside the total: pagination goes here. */
69
+ footer?: Snippet;
70
+ };
71
+ exports: {};
72
+ bindings: "";
73
+ slots: {};
74
+ events: {};
75
+ };
76
+ declare class __sveltets_Render<Row> {
77
+ props(): ReturnType<typeof $$render<Row>>['props'];
78
+ events(): ReturnType<typeof $$render<Row>>['events'];
79
+ slots(): ReturnType<typeof $$render<Row>>['slots'];
80
+ bindings(): "";
81
+ exports(): {};
82
+ }
83
+ interface $$IsomorphicComponent {
84
+ new <Row>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Row>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Row>['props']>, ReturnType<__sveltets_Render<Row>['events']>, ReturnType<__sveltets_Render<Row>['slots']>> & {
85
+ $$bindings?: ReturnType<__sveltets_Render<Row>['bindings']>;
86
+ } & ReturnType<__sveltets_Render<Row>['exports']>;
87
+ <Row>(internal: unknown, props: ReturnType<__sveltets_Render<Row>['props']> & {}): ReturnType<__sveltets_Render<Row>['exports']>;
88
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
89
+ }
90
+ declare const Table: $$IsomorphicComponent;
91
+ type Table<Row> = InstanceType<typeof Table<Row>>;
92
+ export default Table;