@runwisp/ui 1.4.0 → 1.5.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/package.json +4 -5
- package/src/lib/actions/floating.ts +73 -0
- 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/CodeBlock.svelte +84 -35
- package/src/lib/components/CronInput.svelte +5 -2
- package/src/lib/components/DataGrid.svelte +95 -133
- 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/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 +30 -32
- 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/Select.svelte +13 -45
- 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/accordion.svelte.ts +19 -0
- package/src/lib/components/dashboard/RunDetailPanel.svelte +91 -63
- package/src/lib/components/dashboard/RunFilterPopover.svelte +7 -16
- package/src/lib/components/dashboard/RunsList.svelte +9 -18
- package/src/lib/components/dashboard/cron-format.ts +1 -1
- package/src/lib/components/dashboard/run-filters.test.ts +2 -58
- package/src/lib/components/dashboard/run-filters.ts +5 -53
- package/src/lib/components/dashboard/run-helpers.test.ts +17 -7
- package/src/lib/components/dashboard/run-helpers.ts +12 -1
- package/src/lib/components/dashboard/status-config.ts +38 -83
- package/src/lib/components/dashboard/types.ts +0 -26
- package/src/lib/components/radio-group.ts +11 -0
- package/src/lib/index.ts +8 -43
- package/src/lib/log-console/LogFetcher.svelte.ts +3 -14
- 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.css +10 -4
- package/src/lib/utils/clipboard.svelte.test.ts +92 -0
- package/src/lib/utils/clipboard.svelte.ts +73 -0
- 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 +3 -11
- 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/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;
|
|
@@ -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
|
});
|
|
@@ -220,7 +194,7 @@
|
|
|
220
194
|
let someSelected = $derived(selection.someSelected);
|
|
221
195
|
|
|
222
196
|
// Cell rhythm — airy by default, tight when compact. Header + body share it.
|
|
223
|
-
const
|
|
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
|
|
|
@@ -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>
|
|
@@ -2,23 +2,34 @@
|
|
|
2
2
|
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
3
3
|
|
|
4
4
|
<script lang="ts">
|
|
5
|
+
type KbdSize = "xs" | "sm";
|
|
6
|
+
/** `faint` sits quietly inside an input; `console` rides dark terminal chrome and inherits its text color. */
|
|
7
|
+
type KbdTone = "default" | "faint" | "console";
|
|
8
|
+
|
|
5
9
|
interface Props {
|
|
6
10
|
keys: string | string[];
|
|
11
|
+
size?: KbdSize;
|
|
12
|
+
tone?: KbdTone;
|
|
7
13
|
class?: string;
|
|
8
14
|
}
|
|
9
15
|
|
|
10
|
-
let { keys, class: className = "" }: Props = $props();
|
|
16
|
+
let { keys, size = "sm", tone = "default", class: className = "" }: Props = $props();
|
|
11
17
|
|
|
12
18
|
const keyList = $derived(Array.isArray(keys) ? keys : [keys]);
|
|
13
19
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
min-w-[1.5em] px-1.5
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const sizeClasses: Record<KbdSize, string> = {
|
|
21
|
+
xs: "px-1.5 text-2xs",
|
|
22
|
+
sm: "min-w-[1.5em] px-1.5 text-xs",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Vertical padding lives with the tone: console keys sit inline in toolbar
|
|
26
|
+
// text and must not grow the row.
|
|
27
|
+
const toneClasses: Record<KbdTone, string> = {
|
|
28
|
+
default:
|
|
29
|
+
"py-0.5 font-medium bg-surface-sunken text-on-surface-muted border-outline shadow-sm",
|
|
30
|
+
faint: "py-0.5 font-medium bg-surface-raised text-on-surface-faint border-outline-faint shadow-sm",
|
|
31
|
+
console: "tracking-wide border-[var(--rw-con-gutter)]",
|
|
32
|
+
};
|
|
22
33
|
</script>
|
|
23
34
|
|
|
24
35
|
<span class="inline-flex items-center gap-1 {className}">
|
|
@@ -26,6 +37,10 @@
|
|
|
26
37
|
{#if i > 0}
|
|
27
38
|
<span class="text-xs text-on-surface-faint select-none">+</span>
|
|
28
39
|
{/if}
|
|
29
|
-
<kbd
|
|
40
|
+
<kbd
|
|
41
|
+
class="inline-flex items-center justify-center rounded-[3px] border font-mono {sizeClasses[
|
|
42
|
+
size
|
|
43
|
+
]} {toneClasses[tone]}">{key}</kbd
|
|
44
|
+
>
|
|
30
45
|
{/each}
|
|
31
46
|
</span>
|