@runwisp/ui 1.4.0 → 1.5.1
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/package.json +4 -5
- package/src/lib/actions/floating.ts +73 -0
- package/src/lib/actions/focusTrap.ts +3 -3
- package/src/lib/actions/portal.ts +5 -1
- package/src/lib/components/Accordion.svelte +6 -2
- package/src/lib/components/AccordionItem.svelte +80 -23
- package/src/lib/components/Avatar.svelte +0 -1
- package/src/lib/components/Badge.svelte +1 -1
- package/src/lib/components/CodeBlock.svelte +84 -35
- package/src/lib/components/CronInput.svelte +5 -2
- package/src/lib/components/DataGrid.svelte +100 -138
- package/src/lib/components/Drawer.svelte +2 -6
- package/src/lib/components/Dropdown.svelte +19 -57
- package/src/lib/components/EmptyState.svelte +2 -1
- package/src/lib/components/ErrorState.svelte +2 -1
- package/src/lib/components/FilterBar.svelte +1 -1
- package/src/lib/components/FilterPill.svelte +2 -2
- package/src/lib/components/FormField.svelte +1 -1
- package/src/lib/components/Heading.svelte +19 -12
- package/src/lib/components/Kbd.svelte +25 -10
- package/src/lib/components/Link.svelte +7 -1
- package/src/lib/components/LogConsole.svelte +40 -42
- package/src/lib/components/Modal.svelte +44 -33
- package/src/lib/components/NotificationBell.svelte +2 -1
- package/src/lib/components/PageContainer.svelte +1 -10
- package/src/lib/components/PageHeader.svelte +8 -4
- package/src/lib/components/Popover.svelte +9 -51
- package/src/lib/components/Progress.svelte +19 -6
- package/src/lib/components/Radio.svelte +16 -4
- package/src/lib/components/RadioGroup.svelte +15 -2
- package/src/lib/components/SearchInput.svelte +1 -1
- package/src/lib/components/Select.svelte +16 -47
- package/src/lib/components/Sparkline.svelte +2 -2
- package/src/lib/components/StatusDot.svelte +1 -1
- package/src/lib/components/Table.svelte +13 -4
- package/src/lib/components/TableBody.svelte +2 -1
- package/src/lib/components/TableCell.svelte +38 -8
- package/src/lib/components/TableHead.svelte +12 -4
- package/src/lib/components/TableRow.svelte +23 -3
- package/src/lib/components/TaskCard.svelte +1 -1
- package/src/lib/components/TimezoneSelect.svelte +2 -2
- package/src/lib/components/Tooltip.svelte +1 -1
- package/src/lib/components/accordion.svelte.ts +19 -0
- package/src/lib/components/dashboard/RunDetailPanel.svelte +106 -79
- package/src/lib/components/dashboard/RunFilterPopover.svelte +12 -21
- package/src/lib/components/dashboard/RunsList.svelte +17 -26
- package/src/lib/components/dashboard/cron-format.test.ts +1 -1
- package/src/lib/components/dashboard/cron-format.ts +3 -3
- package/src/lib/components/dashboard/run-filters.test.ts +4 -60
- package/src/lib/components/dashboard/run-filters.ts +29 -144
- package/src/lib/components/dashboard/run-helpers.test.ts +25 -6
- package/src/lib/components/dashboard/run-helpers.ts +40 -39
- package/src/lib/components/dashboard/status-config.ts +38 -83
- package/src/lib/components/dashboard/types.ts +0 -26
- package/src/lib/components/data-grid-selection.ts +2 -9
- package/src/lib/components/filter-spec.ts +3 -3
- package/src/lib/components/radio-group.ts +11 -0
- package/src/lib/index.ts +8 -43
- package/src/lib/log-console/LogCache.svelte.ts +1 -1
- package/src/lib/log-console/LogFetcher.svelte.ts +3 -14
- package/src/lib/log-console/ansi.ts +1 -1
- package/src/lib/log-console/highlight-scroll.test.ts +59 -0
- package/src/lib/log-console/highlight-scroll.ts +41 -0
- package/src/lib/theme-tokens.css +21 -21
- package/src/lib/theme.css +22 -16
- package/src/lib/utils/clipboard.svelte.test.ts +92 -0
- package/src/lib/utils/clipboard.svelte.ts +73 -0
- package/src/lib/utils/format.test.ts +3 -3
- package/src/lib/utils/format.ts +65 -94
- package/src/lib/utils/id.test.ts +3 -7
- package/src/lib/utils/id.ts +3 -2
- package/src/lib/utils/theme.svelte.test.ts +0 -6
- package/src/lib/utils/theme.svelte.ts +3 -5
- package/src/lib/utils/toast.svelte.ts +5 -14
- package/src/stories/Accordion.stories.svelte +61 -0
- package/src/stories/CodeBlock.stories.svelte +55 -0
- package/src/stories/DataGrid.stories.svelte +0 -5
- package/src/stories/Heading.stories.svelte +31 -0
- package/src/stories/Kbd.stories.svelte +36 -0
- package/src/stories/Link.stories.svelte +38 -0
- package/src/stories/LogConsole.stories.svelte +2 -2
- package/src/stories/PageHeader.stories.svelte +31 -0
- package/src/stories/Progress.stories.svelte +26 -0
- package/src/stories/RadioGroup.stories.svelte +43 -0
- package/src/stories/Table.stories.svelte +69 -0
- package/src/svelte-shims.d.ts +0 -5
- package/src/lib/components/dashboard/status-config.test.ts +0 -18
|
@@ -25,18 +25,13 @@
|
|
|
25
25
|
/** Take over sorting (e.g. server-side). When set, the grid only reports
|
|
26
26
|
* the clicked key and never reorders `data` itself. */
|
|
27
27
|
onSort?: (key: string) => void;
|
|
28
|
-
/** Built-in fuzzy search box above the table. For typed filters use
|
|
29
|
-
* `FilterBar` + `applyFilters` outside the grid instead. */
|
|
30
|
-
filterable?: boolean;
|
|
31
|
-
filterPlaceholder?: string;
|
|
32
|
-
filterKeys?: string[];
|
|
33
|
-
filterQuery?: string;
|
|
34
28
|
paginate?: boolean;
|
|
35
29
|
page?: number;
|
|
36
30
|
pageSize?: number;
|
|
37
31
|
pageSizeOptions?: number[];
|
|
38
32
|
/** Server mode: the full row count. `data` is then the current page as
|
|
39
|
-
* delivered, so the grid skips its own
|
|
33
|
+
* delivered, so the grid skips its own sort and slicing. Filter with
|
|
34
|
+
* `FilterBar` + `applyFilters` before handing rows to the grid. */
|
|
40
35
|
total?: number;
|
|
41
36
|
onPageChange?: (page: number) => void;
|
|
42
37
|
onPageSizeChange?: (pageSize: number) => void;
|
|
@@ -51,16 +46,16 @@
|
|
|
51
46
|
onRowClick?: (row: T) => void;
|
|
52
47
|
loading?: boolean;
|
|
53
48
|
stickyHeader?: boolean;
|
|
54
|
-
/** Hide the header row entirely
|
|
49
|
+
/** Hide the header row entirely, for single-column feeds (e.g. an
|
|
55
50
|
* inbox) where a column label would be noise. */
|
|
56
51
|
showHeader?: boolean;
|
|
57
|
-
/** Rendered below the table body, inside the frame
|
|
52
|
+
/** Rendered below the table body, inside the frame, e.g. a cursor
|
|
58
53
|
* "Load more" button for feeds that don't use offset pagination. */
|
|
59
54
|
footer?: Snippet;
|
|
60
55
|
/** Drop the outer border/shadow/bg so the grid sits flush inside a Card
|
|
61
56
|
* or panel that already supplies its own frame. */
|
|
62
57
|
bare?: boolean;
|
|
63
|
-
/** Extra classes per row
|
|
58
|
+
/** Extra classes per row, e.g. a left accent bar for a failing row.
|
|
64
59
|
* Rows always carry `group`, so a `render`/`rowAction` snippet can use
|
|
65
60
|
* `group-hover:` to reveal on-hover controls. */
|
|
66
61
|
rowClass?: (row: T) => string;
|
|
@@ -71,7 +66,7 @@
|
|
|
71
66
|
// Type-aware comparator. Nullish sorts last; numbers numerically; dates by
|
|
72
67
|
// epoch; everything else by locale with numeric-aware string compare.
|
|
73
68
|
// Returns 0 for non-comparable/equal, so a column whose key maps to no real
|
|
74
|
-
// field (a display-only key) leaves order untouched
|
|
69
|
+
// field (a display-only key) leaves order untouched, sort stays inert there.
|
|
75
70
|
function compareValues(a: unknown, b: unknown): number {
|
|
76
71
|
if (a == null && b == null) return 0;
|
|
77
72
|
if (a == null) return 1;
|
|
@@ -85,10 +80,13 @@
|
|
|
85
80
|
<script lang="ts" generics="T extends object">
|
|
86
81
|
import { ArrowUp, ArrowDown, ArrowUpDown } from "@lucide/svelte";
|
|
87
82
|
import Checkbox from "./Checkbox.svelte";
|
|
88
|
-
import Input from "./Input.svelte";
|
|
89
83
|
import Pagination from "./Pagination.svelte";
|
|
90
84
|
import Spinner from "./Spinner.svelte";
|
|
91
|
-
import
|
|
85
|
+
import Table from "./Table.svelte";
|
|
86
|
+
import TableBody from "./TableBody.svelte";
|
|
87
|
+
import TableCell from "./TableCell.svelte";
|
|
88
|
+
import TableHead from "./TableHead.svelte";
|
|
89
|
+
import TableRow from "./TableRow.svelte";
|
|
92
90
|
import { selectionState } from "./data-grid-selection.js";
|
|
93
91
|
|
|
94
92
|
let {
|
|
@@ -99,10 +97,6 @@
|
|
|
99
97
|
sortKey = $bindable(undefined),
|
|
100
98
|
sortDirection = $bindable(undefined),
|
|
101
99
|
onSort,
|
|
102
|
-
filterable = false,
|
|
103
|
-
filterPlaceholder = "Filter…",
|
|
104
|
-
filterKeys,
|
|
105
|
-
filterQuery = $bindable(""),
|
|
106
100
|
paginate = false,
|
|
107
101
|
page = $bindable(1),
|
|
108
102
|
pageSize = $bindable(20),
|
|
@@ -166,36 +160,16 @@
|
|
|
166
160
|
}
|
|
167
161
|
}
|
|
168
162
|
|
|
169
|
-
const searchableKeys = $derived(
|
|
170
|
-
filterKeys?.length ? filterKeys : columns.map((c) => String(c.key)),
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
const fuse = $derived(
|
|
174
|
-
new Fuse(data, {
|
|
175
|
-
keys: searchableKeys,
|
|
176
|
-
threshold: 0.3,
|
|
177
|
-
ignoreLocation: true,
|
|
178
|
-
}),
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
const filteredData = $derived.by(() => {
|
|
182
|
-
if (typeof total === "number") return data;
|
|
183
|
-
|
|
184
|
-
if (!filterable || !filterQuery.trim()) return data;
|
|
185
|
-
|
|
186
|
-
return fuse.search(filterQuery).map((r) => r.item);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
163
|
// Client-side sort. Off in server mode (`total` set) or when the parent
|
|
190
164
|
// drives sorting via `onSort`; otherwise reorder by the active key so a
|
|
191
165
|
// sortable header actually sorts. Stable + inert on display-only keys.
|
|
192
166
|
const sortedData = $derived.by(() => {
|
|
193
167
|
if (onSort || typeof total === "number" || !sortKey || !sortDirection) {
|
|
194
|
-
return
|
|
168
|
+
return data;
|
|
195
169
|
}
|
|
196
170
|
const dir = sortDirection === "desc" ? -1 : 1;
|
|
197
171
|
const key = sortKey;
|
|
198
|
-
return [...
|
|
172
|
+
return [...data].sort(
|
|
199
173
|
(a, b) => dir * compareValues(resolvePath(a, key), resolvePath(b, key)),
|
|
200
174
|
);
|
|
201
175
|
});
|
|
@@ -219,8 +193,8 @@
|
|
|
219
193
|
let allSelected = $derived(selection.allSelected);
|
|
220
194
|
let someSelected = $derived(selection.someSelected);
|
|
221
195
|
|
|
222
|
-
// Cell rhythm
|
|
223
|
-
const
|
|
196
|
+
// Cell rhythm, airy by default, tight when compact. Header + body share it.
|
|
197
|
+
const density = $derived(compact ? "compact" : "comfortable");
|
|
224
198
|
// Hover reveals a teal accent rail on the leftmost cell (see .group on <tr>).
|
|
225
199
|
const railHover = "group-hover:shadow-[inset_3px_0_0_var(--color-primary)]";
|
|
226
200
|
</script>
|
|
@@ -230,22 +204,6 @@
|
|
|
230
204
|
? ''
|
|
231
205
|
: 'rounded-[4px] border border-outline bg-surface-raised shadow-sm'} {className}"
|
|
232
206
|
>
|
|
233
|
-
{#if filterable}
|
|
234
|
-
<div class="border-b border-outline p-3">
|
|
235
|
-
<Input
|
|
236
|
-
value={filterQuery}
|
|
237
|
-
oninput={(e) => {
|
|
238
|
-
filterQuery = e.currentTarget.value;
|
|
239
|
-
if (paginate && !total) page = 1;
|
|
240
|
-
}}
|
|
241
|
-
type="search"
|
|
242
|
-
placeholder={filterPlaceholder}
|
|
243
|
-
size="sm"
|
|
244
|
-
class="max-w-sm"
|
|
245
|
-
/>
|
|
246
|
-
</div>
|
|
247
|
-
{/if}
|
|
248
|
-
|
|
249
207
|
<div
|
|
250
208
|
class="relative min-h-0 flex-1 rounded-[4px] {wrapperClass ??
|
|
251
209
|
(stickyHeader ? '' : 'overflow-x-auto')}"
|
|
@@ -263,91 +221,93 @@
|
|
|
263
221
|
</div>
|
|
264
222
|
{/if}
|
|
265
223
|
|
|
266
|
-
<
|
|
224
|
+
<Table bare>
|
|
267
225
|
{#if showHeader}
|
|
268
|
-
<
|
|
226
|
+
<TableHead
|
|
269
227
|
class={stickyHeader
|
|
270
228
|
? "sticky top-0 z-10 bg-surface-raised/95 backdrop-blur-sm"
|
|
271
229
|
: ""}
|
|
272
230
|
>
|
|
273
|
-
|
|
274
|
-
{
|
|
275
|
-
<
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
{
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
{:else}
|
|
310
|
-
<ArrowDown size={14} class="text-primary" />
|
|
311
|
-
{/if}
|
|
231
|
+
{#if selectable}
|
|
232
|
+
<TableCell header {density} class="w-12">
|
|
233
|
+
<Checkbox
|
|
234
|
+
checked={allSelected}
|
|
235
|
+
indeterminate={someSelected}
|
|
236
|
+
onchange={toggleAll}
|
|
237
|
+
size="sm"
|
|
238
|
+
/>
|
|
239
|
+
</TableCell>
|
|
240
|
+
{/if}
|
|
241
|
+
{#each columns as column (column.key)}
|
|
242
|
+
{@const sorted = sortKey === column.key}
|
|
243
|
+
<TableCell
|
|
244
|
+
header
|
|
245
|
+
{density}
|
|
246
|
+
align={column.align}
|
|
247
|
+
aria-sort={sorted
|
|
248
|
+
? sortDirection === "asc"
|
|
249
|
+
? "ascending"
|
|
250
|
+
: "descending"
|
|
251
|
+
: undefined}
|
|
252
|
+
class="group"
|
|
253
|
+
style={column.width ? `width: ${column.width}` : undefined}
|
|
254
|
+
>
|
|
255
|
+
{#if column.sortable}
|
|
256
|
+
<button
|
|
257
|
+
onclick={() => handleSort(String(column.key))}
|
|
258
|
+
class="
|
|
259
|
+
inline-flex items-center gap-1 group-hover:text-on-surface
|
|
260
|
+
{sorted ? 'text-on-surface' : ''}
|
|
261
|
+
"
|
|
262
|
+
>
|
|
263
|
+
{column.label}
|
|
264
|
+
{#if sorted}
|
|
265
|
+
{#if sortDirection === "asc"}
|
|
266
|
+
<ArrowUp size={14} class="text-primary" />
|
|
312
267
|
{:else}
|
|
313
|
-
<
|
|
314
|
-
size={14}
|
|
315
|
-
class="text-on-surface-faint opacity-0 group-hover:opacity-100"
|
|
316
|
-
/>
|
|
268
|
+
<ArrowDown size={14} class="text-primary" />
|
|
317
269
|
{/if}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
270
|
+
{:else}
|
|
271
|
+
<ArrowUpDown
|
|
272
|
+
size={14}
|
|
273
|
+
class="text-on-surface-faint opacity-0 group-hover:opacity-100"
|
|
274
|
+
/>
|
|
275
|
+
{/if}
|
|
276
|
+
</button>
|
|
277
|
+
{:else}
|
|
278
|
+
{column.label}
|
|
279
|
+
{/if}
|
|
280
|
+
</TableCell>
|
|
281
|
+
{/each}
|
|
282
|
+
{#if rowAction}
|
|
283
|
+
<TableCell header {density} class="w-16" />
|
|
284
|
+
{/if}
|
|
285
|
+
</TableHead>
|
|
329
286
|
{/if}
|
|
330
|
-
<
|
|
287
|
+
<TableBody>
|
|
331
288
|
{#if pagedData.length === 0 && !loading}
|
|
332
|
-
<
|
|
333
|
-
<
|
|
289
|
+
<TableRow border="none">
|
|
290
|
+
<TableCell
|
|
334
291
|
colspan={columns.length + (selectable ? 1 : 0) + (rowAction ? 1 : 0)}
|
|
335
|
-
|
|
292
|
+
density="none"
|
|
293
|
+
align="center"
|
|
294
|
+
class="px-4 py-12 text-on-surface-muted"
|
|
336
295
|
>
|
|
337
296
|
{#if emptyState}
|
|
338
297
|
{@render emptyState()}
|
|
339
298
|
{:else}
|
|
340
299
|
{emptyMessage}
|
|
341
300
|
{/if}
|
|
342
|
-
</
|
|
343
|
-
</
|
|
301
|
+
</TableCell>
|
|
302
|
+
</TableRow>
|
|
344
303
|
{:else}
|
|
345
304
|
{#each pagedData as row, idx (row[rowKey])}
|
|
346
|
-
<
|
|
305
|
+
<TableRow
|
|
306
|
+
border="faint"
|
|
307
|
+
{hoverable}
|
|
347
308
|
class="
|
|
348
|
-
group
|
|
309
|
+
group
|
|
349
310
|
{striped && idx % 2 === 1 ? 'bg-surface-sunken/30' : ''}
|
|
350
|
-
{hoverable ? 'hover:bg-surface-sunken' : ''}
|
|
351
311
|
{isSelected(row) ? 'bg-primary-soft/30' : ''}
|
|
352
312
|
{onRowClick ? 'cursor-pointer' : ''}
|
|
353
313
|
{rowClass ? rowClass(row) : ''}
|
|
@@ -357,8 +317,9 @@
|
|
|
357
317
|
{#if selectable}
|
|
358
318
|
<!-- stopPropagation: a selection click must never also
|
|
359
319
|
fire onRowClick (which usually navigates away). -->
|
|
360
|
-
<
|
|
361
|
-
|
|
320
|
+
<TableCell
|
|
321
|
+
{density}
|
|
322
|
+
class="cursor-pointer {hoverable ? railHover : ''}"
|
|
362
323
|
onclick={(e) => {
|
|
363
324
|
e.stopPropagation();
|
|
364
325
|
if (!(e.target instanceof HTMLInputElement)) toggleRow(row);
|
|
@@ -369,16 +330,17 @@
|
|
|
369
330
|
size="sm"
|
|
370
331
|
class="pointer-events-none"
|
|
371
332
|
/>
|
|
372
|
-
</
|
|
333
|
+
</TableCell>
|
|
373
334
|
{/if}
|
|
374
335
|
{#each columns as column, ci (column.key)}
|
|
375
|
-
<
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
336
|
+
<TableCell
|
|
337
|
+
{density}
|
|
338
|
+
align={column.align}
|
|
339
|
+
class="text-on-surface-muted {ci === 0 &&
|
|
340
|
+
!selectable &&
|
|
341
|
+
hoverable
|
|
342
|
+
? railHover
|
|
343
|
+
: ''}"
|
|
382
344
|
>
|
|
383
345
|
{#if column.render}
|
|
384
346
|
{@render column.render(row)}
|
|
@@ -389,18 +351,18 @@
|
|
|
389
351
|
>{resolvePath(row, String(column.key)) ?? "-"}</span
|
|
390
352
|
>
|
|
391
353
|
{/if}
|
|
392
|
-
</
|
|
354
|
+
</TableCell>
|
|
393
355
|
{/each}
|
|
394
356
|
{#if rowAction}
|
|
395
|
-
<
|
|
357
|
+
<TableCell {density} align="right">
|
|
396
358
|
{@render rowAction(row)}
|
|
397
|
-
</
|
|
359
|
+
</TableCell>
|
|
398
360
|
{/if}
|
|
399
|
-
</
|
|
361
|
+
</TableRow>
|
|
400
362
|
{/each}
|
|
401
363
|
{/if}
|
|
402
|
-
</
|
|
403
|
-
</
|
|
364
|
+
</TableBody>
|
|
365
|
+
</Table>
|
|
404
366
|
</div>
|
|
405
367
|
|
|
406
368
|
{#if footer}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<script lang="ts">
|
|
5
5
|
import type { Snippet } from "svelte";
|
|
6
6
|
import { X } from "@lucide/svelte";
|
|
7
|
+
import Heading from "./Heading.svelte";
|
|
7
8
|
import { portal } from "../actions/portal.js";
|
|
8
9
|
import { trapFocus } from "../actions/focusTrap.js";
|
|
9
10
|
import { fly } from "svelte/transition";
|
|
@@ -122,12 +123,7 @@
|
|
|
122
123
|
>
|
|
123
124
|
<div>
|
|
124
125
|
{#if title}
|
|
125
|
-
<
|
|
126
|
-
id="drawer-title"
|
|
127
|
-
class="font-mono text-lg font-semibold text-on-surface"
|
|
128
|
-
>
|
|
129
|
-
{title}
|
|
130
|
-
</h2>
|
|
126
|
+
<Heading level={2} size="lg" id="drawer-title">{title}</Heading>
|
|
131
127
|
{/if}
|
|
132
128
|
</div>
|
|
133
129
|
{#if closable}
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
<script lang="ts">
|
|
5
5
|
import type { Snippet } from "svelte";
|
|
6
6
|
import type { Component } from "svelte";
|
|
7
|
-
import { tick, onDestroy } from "svelte";
|
|
8
7
|
import { EllipsisVertical, Check } from "@lucide/svelte";
|
|
9
|
-
import {
|
|
8
|
+
import { floating, isOutsideClick } from "../actions/floating.js";
|
|
9
|
+
import { portal } from "../actions/portal.js";
|
|
10
|
+
import { trapFocus } from "../actions/focusTrap.js";
|
|
11
|
+
import Separator from "./Separator.svelte";
|
|
10
12
|
|
|
11
13
|
interface MenuItem {
|
|
12
14
|
label?: string;
|
|
@@ -41,74 +43,27 @@
|
|
|
41
43
|
let open = $state(false);
|
|
42
44
|
let triggerEl = $state<HTMLElement | null>(null);
|
|
43
45
|
let menuEl = $state<HTMLElement | null>(null);
|
|
44
|
-
let cleanupFloating = $state<(() => void) | null>(null);
|
|
45
46
|
let transformOrigin = $state("top right");
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
async function setupFloating() {
|
|
51
|
-
if (!triggerEl || !menuEl) return;
|
|
52
|
-
|
|
53
|
-
cleanupFloating = autoUpdate(triggerEl, menuEl, () => {
|
|
54
|
-
if (!triggerEl || !menuEl) return;
|
|
55
|
-
|
|
56
|
-
const placement = align === "right" ? "bottom-end" : "bottom-start";
|
|
57
|
-
|
|
58
|
-
void computePosition(triggerEl, menuEl, {
|
|
59
|
-
placement,
|
|
60
|
-
middleware: [offset(4), flip(), shift({ padding: 8 })],
|
|
61
|
-
}).then(({ x, y, placement }) => {
|
|
62
|
-
if (menuEl) {
|
|
63
|
-
Object.assign(menuEl.style, {
|
|
64
|
-
left: `${x}px`,
|
|
65
|
-
top: `${y}px`,
|
|
66
|
-
position: "absolute",
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// Flip transform origin vertically when the menu flips above the trigger
|
|
70
|
-
const isTop = placement.startsWith("top");
|
|
71
|
-
transformOrigin = `${isTop ? "bottom" : "top"} ${align}`;
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
});
|
|
48
|
+
// Flip the transform origin vertically when the menu flips above the trigger.
|
|
49
|
+
function placed(placement: string) {
|
|
50
|
+
transformOrigin = `${placement.startsWith("top") ? "bottom" : "top"} ${align}`;
|
|
75
51
|
}
|
|
76
52
|
|
|
77
53
|
function handleItemClick(item: MenuItem) {
|
|
78
54
|
if (item.divider) return;
|
|
79
55
|
if (item.disabled) return;
|
|
80
56
|
item.onClick?.();
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function toggle(value: boolean) {
|
|
85
|
-
open = value;
|
|
86
|
-
if (open) {
|
|
87
|
-
void tick().then(() => {
|
|
88
|
-
void setupFloating();
|
|
89
|
-
});
|
|
90
|
-
} else {
|
|
91
|
-
cleanupFloating?.();
|
|
92
|
-
cleanupFloating = null;
|
|
93
|
-
}
|
|
57
|
+
open = false;
|
|
94
58
|
}
|
|
95
59
|
|
|
96
60
|
function handleOutsideClick(e: MouseEvent) {
|
|
97
|
-
|
|
98
|
-
if (triggerEl && path.includes(triggerEl)) return;
|
|
99
|
-
if (menuEl && path.includes(menuEl)) return;
|
|
100
|
-
if (open) toggle(false);
|
|
61
|
+
if (open && isOutsideClick(e, triggerEl, menuEl)) open = false;
|
|
101
62
|
}
|
|
102
63
|
|
|
103
64
|
function handleKeyDown(e: KeyboardEvent) {
|
|
104
|
-
if (e.key === "Escape" && open)
|
|
105
|
-
toggle(false);
|
|
106
|
-
}
|
|
65
|
+
if (e.key === "Escape" && open) open = false;
|
|
107
66
|
}
|
|
108
|
-
|
|
109
|
-
onDestroy(() => {
|
|
110
|
-
cleanupFloating?.();
|
|
111
|
-
});
|
|
112
67
|
</script>
|
|
113
68
|
|
|
114
69
|
<svelte:window onclick={handleOutsideClick} onkeydown={handleKeyDown} />
|
|
@@ -117,7 +72,7 @@
|
|
|
117
72
|
<button
|
|
118
73
|
onclick={(e) => {
|
|
119
74
|
e.stopPropagation();
|
|
120
|
-
|
|
75
|
+
open = !open;
|
|
121
76
|
}}
|
|
122
77
|
bind:this={triggerEl}
|
|
123
78
|
aria-label={triggerLabel}
|
|
@@ -135,6 +90,13 @@
|
|
|
135
90
|
<div
|
|
136
91
|
use:portal
|
|
137
92
|
use:trapFocus
|
|
93
|
+
use:floating={{
|
|
94
|
+
reference: triggerEl,
|
|
95
|
+
placement: align === "right" ? "bottom-end" : "bottom-start",
|
|
96
|
+
offset: 4,
|
|
97
|
+
padding: 8,
|
|
98
|
+
onPlace: placed,
|
|
99
|
+
}}
|
|
138
100
|
bind:this={menuEl}
|
|
139
101
|
role="menu"
|
|
140
102
|
class="
|
|
@@ -150,7 +112,7 @@
|
|
|
150
112
|
{:else}
|
|
151
113
|
{#each items as item, idx (idx)}
|
|
152
114
|
{#if item.divider}
|
|
153
|
-
<
|
|
115
|
+
<Separator class="my-1" />
|
|
154
116
|
{:else if item.href}
|
|
155
117
|
<a
|
|
156
118
|
href={item.href}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { Snippet } from "svelte";
|
|
6
6
|
import type { Component } from "svelte";
|
|
7
7
|
import { Inbox } from "@lucide/svelte";
|
|
8
|
+
import Heading from "./Heading.svelte";
|
|
8
9
|
|
|
9
10
|
interface Props {
|
|
10
11
|
title?: string;
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
<Icon size={iconSize} class="text-on-surface-faint" />
|
|
33
34
|
</div>
|
|
34
35
|
|
|
35
|
-
<
|
|
36
|
+
<Heading level={3} size="lg" class="mb-1">{title}</Heading>
|
|
36
37
|
|
|
37
38
|
{#if description}
|
|
38
39
|
<p class="mb-6 max-w-sm text-sm text-on-surface-muted">{description}</p>
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { Snippet, Component } from "svelte";
|
|
6
6
|
import { TriangleAlert, RefreshCw, WifiOff } from "@lucide/svelte";
|
|
7
7
|
import Button from "./Button.svelte";
|
|
8
|
+
import Heading from "./Heading.svelte";
|
|
8
9
|
|
|
9
10
|
interface Props {
|
|
10
11
|
/** Error message to display */
|
|
@@ -56,7 +57,7 @@
|
|
|
56
57
|
</div>
|
|
57
58
|
|
|
58
59
|
<div class="space-y-1">
|
|
59
|
-
<
|
|
60
|
+
<Heading level={3} size="lg">{defaultTitle}</Heading>
|
|
60
61
|
<p class="max-w-sm text-sm text-on-surface-muted">{message}</p>
|
|
61
62
|
</div>
|
|
62
63
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
shown as pills; every other field stays behind "+ Add filter" until the user
|
|
12
12
|
adds it, then lives as a pill. Each pill is self-contained (label + value +
|
|
13
13
|
clear in one element, see FilterPill) so activating a filter never adds a
|
|
14
|
-
second row
|
|
14
|
+
second row, the table below never jumps. Data-mode agnostic: the parent feeds
|
|
15
15
|
`values` to applyFilters (client) or maps it to query params (server).
|
|
16
16
|
-->
|
|
17
17
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<!--
|
|
5
5
|
One filter, one element. The pill is both the control and its own "chip":
|
|
6
6
|
inactive it reads `Label ▾` (muted); active it reads `Label: value ✕` (accent)
|
|
7
|
-
with an inline clear. Active vs inactive is a restyle of the SAME element
|
|
7
|
+
with an inline clear. Active vs inactive is a restyle of the SAME element, it
|
|
8
8
|
never spawns a second row, so the table below never jumps. Clicking the body
|
|
9
9
|
opens a type-specific editor popover (select menu / number / date range).
|
|
10
10
|
-->
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
let {
|
|
27
27
|
field,
|
|
28
28
|
values = $bindable({}),
|
|
29
|
-
// Open the editor on mount
|
|
29
|
+
// Open the editor on mount, set when the pill was just added from the
|
|
30
30
|
// "+ Add filter" menu, so the user lands straight in the value picker.
|
|
31
31
|
autoOpen = false,
|
|
32
32
|
onClose,
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
children: Snippet;
|
|
14
14
|
class?: string;
|
|
15
15
|
// The id of the control this field labels. Pass the SAME id to the wrapped
|
|
16
|
-
// input/select so the <label for> actually points at it
|
|
16
|
+
// input/select so the <label for> actually points at it, otherwise the
|
|
17
17
|
// label is dead (clicking it does nothing; screen readers can't associate
|
|
18
18
|
// it). Falls back to a generated id when the field has no focusable target.
|
|
19
19
|
id?: string;
|
|
@@ -3,18 +3,19 @@
|
|
|
3
3
|
|
|
4
4
|
<script lang="ts">
|
|
5
5
|
import type { Snippet } from "svelte";
|
|
6
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
6
7
|
|
|
7
8
|
type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
8
9
|
type HeadingSize = "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
|
|
9
10
|
|
|
10
|
-
interface Props {
|
|
11
|
+
interface Props extends Omit<HTMLAttributes<HTMLHeadingElement>, "children" | "class"> {
|
|
11
12
|
level: HeadingLevel;
|
|
12
13
|
size?: HeadingSize;
|
|
13
14
|
children: Snippet;
|
|
14
15
|
class?: string;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
let { level, size, children, class: className = "" }: Props = $props();
|
|
18
|
+
let { level, size, children, class: className = "", ...restProps }: Props = $props();
|
|
18
19
|
|
|
19
20
|
const defaultSizeMap: Record<HeadingLevel, HeadingSize> = {
|
|
20
21
|
1: "4xl",
|
|
@@ -25,24 +26,30 @@
|
|
|
25
26
|
6: "md",
|
|
26
27
|
};
|
|
27
28
|
|
|
28
|
-
const sizeClasses: Record<
|
|
29
|
-
xs: "text-xs
|
|
30
|
-
sm: "text-sm
|
|
31
|
-
md: "text-base
|
|
32
|
-
lg: "text-lg
|
|
33
|
-
xl: "text-xl
|
|
34
|
-
"2xl": "text-2xl
|
|
35
|
-
"3xl": "text-3xl
|
|
36
|
-
"4xl": "text-4xl
|
|
29
|
+
const sizeClasses: Record<HeadingSize, string> = {
|
|
30
|
+
xs: "text-xs",
|
|
31
|
+
sm: "text-sm",
|
|
32
|
+
md: "text-base",
|
|
33
|
+
lg: "text-lg",
|
|
34
|
+
xl: "text-xl",
|
|
35
|
+
"2xl": "text-2xl",
|
|
36
|
+
"3xl": "text-3xl",
|
|
37
|
+
"4xl": "text-4xl",
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
const resolvedSize = $derived(size ?? defaultSizeMap[level]);
|
|
40
41
|
const tag = $derived(`h${level}` as const);
|
|
41
42
|
</script>
|
|
42
43
|
|
|
44
|
+
<!-- theme.css styles raw h1-h3 globally; data-rw-heading opts out of that rule
|
|
45
|
+
so these classes (the same runbook look: mono, 800, tight) decide. -->
|
|
43
46
|
<svelte:element
|
|
44
47
|
this={tag}
|
|
45
|
-
|
|
48
|
+
data-rw-heading
|
|
49
|
+
class="font-mono leading-[1.1] font-extrabold tracking-[-0.02em] text-on-surface {sizeClasses[
|
|
50
|
+
resolvedSize
|
|
51
|
+
]} {className}"
|
|
52
|
+
{...restProps}
|
|
46
53
|
>
|
|
47
54
|
{@render children()}
|
|
48
55
|
</svelte:element>
|