@mystravil/hardpoint 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/LICENSE +25 -0
- package/README.md +64 -3
- package/dist/base.css +625 -0
- package/dist/components/AppShell.svelte +698 -0
- package/dist/components/AppShell.svelte.d.ts +77 -0
- package/dist/components/Button.svelte +174 -0
- package/dist/components/Button.svelte.d.ts +20 -0
- package/dist/components/Checkbox.svelte +235 -0
- package/dist/components/Checkbox.svelte.d.ts +23 -0
- package/dist/components/CheckboxGroup.svelte +207 -0
- package/dist/components/CheckboxGroup.svelte.d.ts +44 -0
- package/dist/components/Combobox.svelte +901 -0
- package/dist/components/Combobox.svelte.d.ts +72 -0
- package/dist/components/Dialog.svelte +359 -0
- package/dist/components/Dialog.svelte.d.ts +43 -0
- package/dist/components/Disclosure.svelte +201 -0
- package/dist/components/Disclosure.svelte.d.ts +27 -0
- package/dist/components/EmptyState.svelte +88 -0
- package/dist/components/EmptyState.svelte.d.ts +22 -0
- package/dist/components/ErrorState.svelte +135 -0
- package/dist/components/ErrorState.svelte.d.ts +26 -0
- package/dist/components/ImageViewer.svelte +764 -0
- package/dist/components/ImageViewer.svelte.d.ts +55 -0
- package/dist/components/KeyValueList.svelte +114 -0
- package/dist/components/KeyValueList.svelte.d.ts +43 -0
- package/dist/components/ListDetail.svelte +218 -0
- package/dist/components/ListDetail.svelte.d.ts +37 -0
- package/dist/components/LoadRegion.svelte +219 -0
- package/dist/components/LoadRegion.svelte.d.ts +27 -0
- package/dist/components/MediaFrame.svelte +110 -0
- package/dist/components/MediaFrame.svelte.d.ts +20 -0
- package/dist/components/MediaImage.svelte +256 -0
- package/dist/components/MediaImage.svelte.d.ts +16 -0
- package/dist/components/Menu.svelte +258 -0
- package/dist/components/Menu.svelte.d.ts +36 -0
- package/dist/components/MenuCheckboxItem.svelte +119 -0
- package/dist/components/MenuCheckboxItem.svelte.d.ts +15 -0
- package/dist/components/MenuGroup.svelte +50 -0
- package/dist/components/MenuGroup.svelte.d.ts +15 -0
- package/dist/components/MenuItem.svelte +94 -0
- package/dist/components/MenuItem.svelte.d.ts +19 -0
- package/dist/components/MenuRadioGroup.svelte +62 -0
- package/dist/components/MenuRadioGroup.svelte.d.ts +20 -0
- package/dist/components/MenuRadioItem.svelte +110 -0
- package/dist/components/MenuRadioItem.svelte.d.ts +17 -0
- package/dist/components/MenuSeparator.svelte +28 -0
- package/dist/components/MenuSeparator.svelte.d.ts +6 -0
- package/dist/components/Meter.svelte +283 -0
- package/dist/components/Meter.svelte.d.ts +66 -0
- package/dist/components/NativeSelect.svelte +135 -0
- package/dist/components/NativeSelect.svelte.d.ts +52 -0
- package/dist/components/Notice.svelte +262 -0
- package/dist/components/Notice.svelte.d.ts +30 -0
- package/dist/components/Pagination.svelte +132 -0
- package/dist/components/Pagination.svelte.d.ts +18 -0
- package/dist/components/Popover.svelte +222 -0
- package/dist/components/Popover.svelte.d.ts +35 -0
- package/dist/components/Progress.svelte +157 -0
- package/dist/components/Progress.svelte.d.ts +34 -0
- package/dist/components/RadioGroup.svelte +440 -0
- package/dist/components/RadioGroup.svelte.d.ts +50 -0
- package/dist/components/Select.svelte +471 -0
- package/dist/components/Select.svelte.d.ts +48 -0
- package/dist/components/Skeleton.svelte +149 -0
- package/dist/components/Skeleton.svelte.d.ts +24 -0
- package/dist/components/Spinner.svelte +118 -0
- package/dist/components/Spinner.svelte.d.ts +13 -0
- package/dist/components/Stat.svelte +253 -0
- package/dist/components/Stat.svelte.d.ts +46 -0
- package/dist/components/Steps.svelte +189 -0
- package/dist/components/Steps.svelte.d.ts +31 -0
- package/dist/components/Switch.svelte +222 -0
- package/dist/components/Switch.svelte.d.ts +18 -0
- package/dist/components/Table.svelte +504 -0
- package/dist/components/Table.svelte.d.ts +92 -0
- package/dist/components/Tabs.svelte +246 -0
- package/dist/components/Tabs.svelte.d.ts +44 -0
- package/dist/components/Tag.svelte +40 -0
- package/dist/components/Tag.svelte.d.ts +15 -0
- package/dist/components/TextArea.svelte +272 -0
- package/dist/components/TextArea.svelte.d.ts +28 -0
- package/dist/components/TextField.svelte +256 -0
- package/dist/components/TextField.svelte.d.ts +32 -0
- package/dist/components/Timeline.svelte +156 -0
- package/dist/components/Timeline.svelte.d.ts +30 -0
- package/dist/components/Toaster.svelte +411 -0
- package/dist/components/Toaster.svelte.d.ts +11 -0
- package/dist/components/Tooltip.svelte +200 -0
- package/dist/components/Tooltip.svelte.d.ts +38 -0
- package/dist/components/TooltipGroup.svelte +46 -0
- package/dist/components/TooltipGroup.svelte.d.ts +29 -0
- package/dist/components/internal/Scrim.svelte +52 -0
- package/dist/components/internal/Scrim.svelte.d.ts +15 -0
- package/dist/components/internal/Sheet.svelte +263 -0
- package/dist/components/internal/Sheet.svelte.d.ts +36 -0
- package/dist/components/internal/fade-in.d.ts +10 -0
- package/dist/components/internal/fade-in.js +20 -0
- package/dist/components/internal/groups.d.ts +14 -0
- package/dist/components/internal/groups.js +23 -0
- package/dist/components/internal/resolve.d.ts +8 -0
- package/dist/components/internal/resolve.js +247 -0
- package/dist/components/internal/swipe-dismiss.d.ts +47 -0
- package/dist/components/internal/swipe-dismiss.js +220 -0
- package/dist/components/internal/visual-viewport.svelte.d.ts +35 -0
- package/dist/components/internal/visual-viewport.svelte.js +79 -0
- package/dist/components/toast.svelte.d.ts +57 -0
- package/dist/components/toast.svelte.js +84 -0
- package/dist/fonts/OFL-geist-pixel.txt +92 -0
- package/dist/fonts/OFL.txt +93 -0
- package/dist/fonts/geist-latin-variable.woff2 +0 -0
- package/dist/fonts/geist-mono-latin-variable.woff2 +0 -0
- package/dist/fonts/geist-pixel-square.woff2 +0 -0
- package/dist/fonts.css +71 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +45 -0
- package/dist/motion/Motion.svelte +15 -0
- package/dist/motion/Motion.svelte.d.ts +9 -0
- package/dist/motion/context.d.ts +18 -0
- package/dist/motion/context.js +26 -0
- package/dist/motion/index.d.ts +3 -0
- package/dist/motion/index.js +3 -0
- package/dist/motion/transitions.d.ts +31 -0
- package/dist/motion/transitions.js +112 -0
- package/dist/tokens.css +283 -0
- 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;
|