@w5-ui/svelte 0.2.1 → 0.4.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/README.md +116 -4
- package/package.json +2 -2
- package/src/lib/actions/portal.ts +55 -0
- package/src/lib/components/Accordion.svelte +1 -1
- package/src/lib/components/AccordionItem.svelte +6 -7
- package/src/lib/components/ActionMenu.svelte +17 -9
- package/src/lib/components/ActivityFeed.svelte +20 -14
- package/src/lib/components/Alert.svelte +22 -4
- package/src/lib/components/AppLayout.svelte +34 -0
- package/src/lib/components/Avatar.svelte +28 -11
- package/src/lib/components/AvatarGroup.svelte +21 -4
- package/src/lib/components/Badge.svelte +22 -6
- package/src/lib/components/Banner.svelte +36 -7
- package/src/lib/components/Body.svelte +29 -0
- package/src/lib/components/BodyLg.svelte +26 -0
- package/src/lib/components/Breadcrumb.svelte +19 -12
- package/src/lib/components/Button.svelte +30 -1
- package/src/lib/components/CIDRInput.svelte +17 -9
- package/src/lib/components/Callout.svelte +29 -8
- package/src/lib/components/Caption.svelte +26 -0
- package/src/lib/components/Card.svelte +11 -3
- package/src/lib/components/CardMore.svelte +20 -0
- package/src/lib/components/CardTitle.svelte +20 -0
- package/src/lib/components/Carousel.svelte +27 -20
- package/src/lib/components/Checkbox.svelte +24 -3
- package/src/lib/components/Code.svelte +24 -0
- package/src/lib/components/CodeBlock.svelte +8 -7
- package/src/lib/components/ColorPicker.svelte +13 -13
- package/src/lib/components/ColumnToggle.svelte +10 -9
- package/src/lib/components/Combobox.svelte +31 -17
- package/src/lib/components/CommandPalette.svelte +42 -26
- package/src/lib/components/ConfirmDialog.svelte +21 -15
- package/src/lib/components/ContextMenu.svelte +12 -12
- package/src/lib/components/ContextualHelp.svelte +11 -6
- package/src/lib/components/CopyButton.svelte +2 -1
- package/src/lib/components/DatePicker.svelte +27 -23
- package/src/lib/components/DateRangePicker.svelte +46 -39
- package/src/lib/components/Delta.svelte +25 -0
- package/src/lib/components/Display1.svelte +29 -0
- package/src/lib/components/Display2.svelte +29 -0
- package/src/lib/components/Drawer.svelte +6 -5
- package/src/lib/components/DurationInput.svelte +16 -8
- package/src/lib/components/EmptyState.svelte +16 -7
- package/src/lib/components/ExpandableRow.svelte +8 -15
- package/src/lib/components/Eyebrow.svelte +25 -0
- package/src/lib/components/Field.svelte +7 -7
- package/src/lib/components/FieldStack.svelte +18 -0
- package/src/lib/components/FileUpload.svelte +8 -10
- package/src/lib/components/FilterBuilder.svelte +52 -28
- package/src/lib/components/FormActions.svelte +18 -0
- package/src/lib/components/FormRow.svelte +17 -0
- package/src/lib/components/Grid.svelte +17 -0
- package/src/lib/components/GroupedList.svelte +35 -21
- package/src/lib/components/H1.svelte +29 -0
- package/src/lib/components/H2.svelte +29 -0
- package/src/lib/components/H3.svelte +29 -0
- package/src/lib/components/H4.svelte +29 -0
- package/src/lib/components/HealthBar.svelte +6 -3
- package/src/lib/components/HoverCard.svelte +16 -5
- package/src/lib/components/IPInput.svelte +14 -8
- package/src/lib/components/IconButton.svelte +1 -2
- package/src/lib/components/InlineEdit.svelte +25 -14
- package/src/lib/components/Input.svelte +13 -4
- package/src/lib/components/InputGroup.svelte +14 -3
- package/src/lib/components/JsonViewer.svelte +7 -2
- package/src/lib/components/JsonViewerNode.svelte +38 -32
- package/src/lib/components/KVTable.svelte +10 -4
- package/src/lib/components/KanbanBoard.svelte +29 -27
- package/src/lib/components/Kbd.svelte +8 -5
- package/src/lib/components/Label.svelte +28 -0
- package/src/lib/components/LogViewer.svelte +24 -20
- package/src/lib/components/MACInput.svelte +14 -8
- package/src/lib/components/Mac.svelte +23 -0
- package/src/lib/components/Menubar.svelte +30 -13
- package/src/lib/components/Modal.svelte +15 -6
- package/src/lib/components/Mono.svelte +28 -0
- package/src/lib/components/MultiSelect.svelte +43 -23
- package/src/lib/components/Muted.svelte +24 -0
- package/src/lib/components/NameCell.svelte +35 -0
- package/src/lib/components/NotifDot.svelte +23 -0
- package/src/lib/components/NotificationPanel.svelte +44 -31
- package/src/lib/components/NumberInput.svelte +13 -12
- package/src/lib/components/OTPInput.svelte +14 -8
- package/src/lib/components/PageHeader.svelte +25 -0
- package/src/lib/components/Pagination.svelte +16 -12
- package/src/lib/components/PasswordInput.svelte +8 -7
- package/src/lib/components/Pill.svelte +34 -6
- package/src/lib/components/Popover.svelte +101 -17
- package/src/lib/components/ProgressBar.svelte +10 -7
- package/src/lib/components/RadioGroup.svelte +22 -7
- package/src/lib/components/RangeSlider.svelte +17 -12
- package/src/lib/components/RankedList.svelte +12 -18
- package/src/lib/components/ResizablePanel.svelte +25 -14
- package/src/lib/components/RowToggle.svelte +6 -4
- package/src/lib/components/SearchBox.svelte +6 -2
- package/src/lib/components/SegmentedControl.svelte +16 -2
- package/src/lib/components/Select.svelte +20 -8
- package/src/lib/components/SelectionToolbar.svelte +13 -9
- package/src/lib/components/Sidebar.svelte +32 -11
- package/src/lib/components/Signal.svelte +3 -2
- package/src/lib/components/Skeleton.svelte +21 -9
- package/src/lib/components/SkipLink.svelte +5 -2
- package/src/lib/components/Slider.svelte +16 -9
- package/src/lib/components/SortHeader.svelte +13 -4
- package/src/lib/components/SortableList.svelte +17 -18
- package/src/lib/components/Sparkline.svelte +20 -14
- package/src/lib/components/Spinner.svelte +16 -1
- package/src/lib/components/SplitButton.svelte +27 -8
- package/src/lib/components/Spoiler.svelte +8 -6
- package/src/lib/components/SrOnly.svelte +23 -0
- package/src/lib/components/StackedProgress.svelte +17 -9
- package/src/lib/components/StarRating.svelte +35 -10
- package/src/lib/components/Stat.svelte +19 -5
- package/src/lib/components/StatNumber.svelte +22 -0
- package/src/lib/components/StatusIndicator.svelte +9 -2
- package/src/lib/components/StatusRing.svelte +36 -0
- package/src/lib/components/Stepper.svelte +30 -8
- package/src/lib/components/Submeta.svelte +19 -0
- package/src/lib/components/SwitchPortGrid.svelte +20 -7
- package/src/lib/components/TablePager.svelte +22 -0
- package/src/lib/components/TableShell.svelte +19 -0
- package/src/lib/components/Tabs.svelte +16 -5
- package/src/lib/components/Tag.svelte +5 -3
- package/src/lib/components/TagInput.svelte +9 -10
- package/src/lib/components/Textarea.svelte +7 -1
- package/src/lib/components/TimePicker.svelte +14 -8
- package/src/lib/components/TimeRange.svelte +20 -22
- package/src/lib/components/Timeline.svelte +20 -9
- package/src/lib/components/Toast.svelte +24 -5
- package/src/lib/components/Toggle.svelte +10 -4
- package/src/lib/components/ToggleGroup.svelte +15 -5
- package/src/lib/components/Tooltip.svelte +20 -5
- package/src/lib/components/Topbar.svelte +161 -28
- package/src/lib/components/TopbarActions.svelte +52 -0
- package/src/lib/components/TransferList.svelte +30 -22
- package/src/lib/components/TreeItem.svelte +11 -13
- package/src/lib/components/Unit.svelte +22 -0
- package/src/lib/components/VirtualList.svelte +12 -9
- package/src/lib/index.ts +37 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onDestroy } from 'svelte';
|
|
3
|
+
|
|
3
4
|
interface Props {
|
|
4
5
|
code?: string;
|
|
5
6
|
label?: string;
|
|
@@ -11,9 +12,8 @@
|
|
|
11
12
|
code = '',
|
|
12
13
|
label = 'Code block',
|
|
13
14
|
language = '',
|
|
14
|
-
class: className = ''
|
|
15
|
+
class: className = '',
|
|
15
16
|
}: Props = $props();
|
|
16
|
-
|
|
17
17
|
|
|
18
18
|
let copied = $state(false);
|
|
19
19
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -29,17 +29,18 @@
|
|
|
29
29
|
onDestroy(() => { if (timer) clearTimeout(timer); });
|
|
30
30
|
</script>
|
|
31
31
|
|
|
32
|
-
<div role="region" aria-label={label} class="
|
|
33
|
-
<div class="
|
|
32
|
+
<div role="region" aria-label={label} class="overflow-hidden rounded-lg border border-border-2 bg-bg-page {className}">
|
|
33
|
+
<div class="flex min-h-8 items-center justify-between border-b border-border-1 bg-bg-page py-1.5 pl-3 pr-2">
|
|
34
34
|
{#if language}
|
|
35
|
-
<span class="
|
|
35
|
+
<span class="font-mono text-11 uppercase tracking-[0.05em] text-text-4">{language}</span>
|
|
36
36
|
{:else}
|
|
37
37
|
<span></span>
|
|
38
38
|
{/if}
|
|
39
39
|
<button
|
|
40
40
|
type="button"
|
|
41
|
-
|
|
41
|
+
data-copied={copied ? 'true' : undefined}
|
|
42
42
|
aria-label={copied ? 'Copied!' : 'Copy code'}
|
|
43
|
+
class="inline-flex h-[22px] w-[22px] shrink-0 cursor-pointer items-center justify-center rounded border-0 bg-transparent text-text-4 transition-[color,background-color] duration-100 hover:bg-row-hover hover:text-text-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05 data-[copied=true]:text-status-success data-[copied=true]:hover:bg-status-success/10 data-[copied=true]:hover:text-status-success"
|
|
43
44
|
onclick={handleCopy}
|
|
44
45
|
>
|
|
45
46
|
{#if copied}
|
|
@@ -54,5 +55,5 @@
|
|
|
54
55
|
{/if}
|
|
55
56
|
</button>
|
|
56
57
|
</div>
|
|
57
|
-
<pre class="
|
|
58
|
+
<pre class="m-0 overflow-x-auto px-3.5 py-3"><code class="whitespace-pre font-mono text-12 leading-[1.6] text-text-2">{code}</code></pre>
|
|
58
59
|
</div>
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
value = $bindable(COLOR_SWATCHES[0].value),
|
|
36
36
|
swatches = COLOR_SWATCHES,
|
|
37
37
|
disabled = false,
|
|
38
|
-
onChange = undefined
|
|
38
|
+
onChange = undefined,
|
|
39
39
|
}: Props = $props();
|
|
40
40
|
|
|
41
41
|
const groupId = `dash-ui-cp-${++counter}`;
|
|
@@ -48,8 +48,7 @@
|
|
|
48
48
|
const idx = cells.findIndex((c) => c.value === value);
|
|
49
49
|
let next = idx;
|
|
50
50
|
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') next = (idx + 1) % cells.length;
|
|
51
|
-
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp')
|
|
52
|
-
next = (idx - 1 + cells.length) % cells.length;
|
|
51
|
+
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') next = (idx - 1 + cells.length) % cells.length;
|
|
53
52
|
else if (e.key === 'Home') next = 0;
|
|
54
53
|
else if (e.key === 'End') next = cells.length - 1;
|
|
55
54
|
else return;
|
|
@@ -62,28 +61,29 @@
|
|
|
62
61
|
let hasMatch = $derived(swatches.some((s) => s.value === value));
|
|
63
62
|
</script>
|
|
64
63
|
|
|
65
|
-
<div
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
<div
|
|
65
|
+
data-disabled={disabled ? 'true' : undefined}
|
|
66
|
+
class="inline-flex flex-col gap-1.5 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-40"
|
|
67
|
+
>
|
|
68
|
+
<div id="{groupId}-label" class={srOnlyLabel ? 'sr-only' : 'text-12 text-text-4'}>{label}</div>
|
|
69
69
|
<div
|
|
70
70
|
role="radiogroup"
|
|
71
71
|
aria-labelledby="{groupId}-label"
|
|
72
|
-
class="
|
|
73
|
-
tabindex=
|
|
72
|
+
class="flex flex-wrap gap-1.5"
|
|
73
|
+
tabindex={-1}
|
|
74
74
|
onkeydown={handleKeyDown}
|
|
75
75
|
>
|
|
76
76
|
{#each swatches as sw, i}
|
|
77
77
|
{@const checked = sw.value === value}
|
|
78
78
|
{@const tabIdx = checked || (i === 0 && !hasMatch) ? 0 : -1}
|
|
79
|
-
<label class="
|
|
79
|
+
<label class="group/sw inline-flex cursor-pointer" title={sw.label}>
|
|
80
80
|
<input
|
|
81
81
|
type="radio"
|
|
82
82
|
name={groupId}
|
|
83
83
|
value={sw.value}
|
|
84
84
|
{checked}
|
|
85
85
|
{disabled}
|
|
86
|
-
class="sr-only"
|
|
86
|
+
class="peer sr-only"
|
|
87
87
|
tabindex={tabIdx}
|
|
88
88
|
aria-label={sw.label}
|
|
89
89
|
onchange={() => {
|
|
@@ -92,10 +92,10 @@
|
|
|
92
92
|
}}
|
|
93
93
|
/>
|
|
94
94
|
<span
|
|
95
|
-
class="
|
|
95
|
+
class="inline-block h-5 w-5 rounded-full transition-[transform,box-shadow] duration-100 hover:scale-110 motion-reduce:hover:scale-100 peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-brand-05 {checked ? 'shadow-[0_0_0_2px_#0a0a0b,0_0_0_4px_#fff]' : ''}"
|
|
96
96
|
style="background:{sw.color}"
|
|
97
97
|
aria-hidden="true"
|
|
98
|
-
></span>
|
|
98
|
+
></span>
|
|
99
99
|
</label>
|
|
100
100
|
{/each}
|
|
101
101
|
</div>
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
<script lang="ts">
|
|
10
10
|
import { onMount } from 'svelte';
|
|
11
11
|
|
|
12
|
-
|
|
13
12
|
interface Props {
|
|
14
13
|
columns?: ColumnDef[];
|
|
15
14
|
visible?: Set<string>;
|
|
@@ -56,14 +55,15 @@
|
|
|
56
55
|
});
|
|
57
56
|
</script>
|
|
58
57
|
|
|
59
|
-
<div class="
|
|
58
|
+
<div class="relative inline-block">
|
|
60
59
|
<button
|
|
61
60
|
bind:this={btnEl}
|
|
62
61
|
type="button"
|
|
63
|
-
|
|
62
|
+
data-open={open ? 'true' : undefined}
|
|
64
63
|
aria-haspopup="dialog"
|
|
65
64
|
aria-expanded={open}
|
|
66
65
|
aria-controls={panelId}
|
|
66
|
+
class="inline-flex h-7 cursor-pointer items-center gap-1.5 rounded-md border border-border-2 bg-transparent px-2.5 text-12 font-medium text-text-3 transition-colors duration-100 hover:bg-row-hover hover:text-text-1 data-[open=true]:border-brand-05 data-[open=true]:bg-brand-05/[0.10] data-[open=true]:text-brand-05 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
|
|
67
67
|
onclick={() => (open = !open)}
|
|
68
68
|
>
|
|
69
69
|
<svg viewBox="0 0 14 14" width="14" height="14" fill="none" aria-hidden="true" focusable="false">
|
|
@@ -77,18 +77,19 @@
|
|
|
77
77
|
<div
|
|
78
78
|
bind:this={panelEl}
|
|
79
79
|
id={panelId}
|
|
80
|
-
class="col-toggle__panel"
|
|
81
80
|
role="dialog"
|
|
82
81
|
aria-label="Toggle columns"
|
|
82
|
+
class="absolute right-0 top-[calc(100%+4px)] z-[9100] min-w-[200px] rounded-lg border border-border-3 bg-bg-2 p-1 shadow-[0_8px_24px_rgba(0,0,0,0.5)]"
|
|
83
83
|
>
|
|
84
|
-
<div class="
|
|
85
|
-
<ul class="
|
|
84
|
+
<div class="px-2.5 pb-1.5 pt-2 text-11 font-semibold uppercase tracking-[0.06em] text-text-4">Columns</div>
|
|
85
|
+
<ul class="m-0 list-none p-0" role="list">
|
|
86
86
|
{#each columns as col (col.key)}
|
|
87
|
-
<li
|
|
88
|
-
<label class="
|
|
87
|
+
<li>
|
|
88
|
+
<label class="flex cursor-pointer items-center gap-2 rounded-[5px] px-2.5 py-1.5 text-13 text-text-2 hover:bg-row-active hover:text-text-1 has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50">
|
|
89
89
|
<input
|
|
90
90
|
type="checkbox"
|
|
91
|
-
class="
|
|
91
|
+
class="h-3.5 w-3.5 cursor-pointer appearance-none rounded-sm border-[1.5px] border-border-3 bg-bg-1 checked:border-brand-05 checked:bg-brand-05 disabled:cursor-not-allowed"
|
|
92
|
+
style="background-image: var(--check-img, none);"
|
|
92
93
|
checked={visible.has(col.key)}
|
|
93
94
|
disabled={col.required}
|
|
94
95
|
onchange={() => { if (!col.required) toggle(col.key); }}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
<script module lang="ts">
|
|
2
2
|
let counter = 0;
|
|
3
|
-
export interface ComboboxOption {
|
|
3
|
+
export interface ComboboxOption {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
4
7
|
</script>
|
|
5
8
|
|
|
6
9
|
<script lang="ts">
|
|
@@ -27,7 +30,6 @@
|
|
|
27
30
|
class: className = '',
|
|
28
31
|
onchange,
|
|
29
32
|
}: Props = $props();
|
|
30
|
-
|
|
31
33
|
|
|
32
34
|
const uid = `dash-ui-cb-${++counter}`;
|
|
33
35
|
let inputId = $derived(id ?? uid);
|
|
@@ -40,13 +42,15 @@
|
|
|
40
42
|
|
|
41
43
|
let selectedOption = $derived(options.find((o) => o.value === value));
|
|
42
44
|
|
|
43
|
-
let filtered = $derived(
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
let filtered = $derived(
|
|
46
|
+
query.trim() === ''
|
|
47
|
+
? options
|
|
48
|
+
: options.filter((o) => o.label.toLowerCase().includes(query.toLowerCase())),
|
|
49
|
+
);
|
|
46
50
|
|
|
47
|
-
let activeOptionId = $derived(
|
|
48
|
-
? `${listboxId}-opt-${activeIdx}`
|
|
49
|
-
|
|
51
|
+
let activeOptionId = $derived(
|
|
52
|
+
activeIdx >= 0 && filtered[activeIdx] ? `${listboxId}-opt-${activeIdx}` : undefined,
|
|
53
|
+
);
|
|
50
54
|
|
|
51
55
|
let displayValue = $derived(open ? query : (selectedOption?.label ?? ''));
|
|
52
56
|
|
|
@@ -93,9 +97,9 @@
|
|
|
93
97
|
onDestroy(() => document.removeEventListener('mousedown', handleOutside));
|
|
94
98
|
</script>
|
|
95
99
|
|
|
96
|
-
<div class="
|
|
100
|
+
<div class="relative block {className}" bind:this={wrapperEl}>
|
|
97
101
|
{#if label}<label for={inputId} class="sr-only">{label}</label>{/if}
|
|
98
|
-
<div class="
|
|
102
|
+
<div class="relative flex items-center">
|
|
99
103
|
<input
|
|
100
104
|
bind:this={inputEl}
|
|
101
105
|
id={inputId}
|
|
@@ -113,24 +117,34 @@
|
|
|
113
117
|
oninput={onInputChange}
|
|
114
118
|
onfocus={openList}
|
|
115
119
|
onkeydown={onKeyDown}
|
|
116
|
-
class="
|
|
120
|
+
class="box-border h-[34px] w-full rounded-md border border-border-2 bg-bg-page py-0 pl-3 pr-9 text-13 leading-none text-text-1 transition-colors duration-100 hover:border-border-3 focus:border-brand-05 focus:outline-none focus:shadow-[0_0_0_2px_rgba(0,111,255,0.2)] disabled:cursor-not-allowed disabled:opacity-40"
|
|
117
121
|
/>
|
|
118
122
|
<button
|
|
119
123
|
type="button"
|
|
120
|
-
tabindex=
|
|
124
|
+
tabindex={-1}
|
|
121
125
|
aria-hidden="true"
|
|
122
|
-
class="
|
|
126
|
+
class="absolute right-2 flex cursor-pointer items-center border-0 bg-transparent p-0.5 leading-none text-text-4"
|
|
123
127
|
onclick={() => { if (open) { open = false; query = ''; } else { inputEl?.focus(); openList(); } }}
|
|
124
128
|
>
|
|
125
|
-
<svg
|
|
129
|
+
<svg
|
|
130
|
+
viewBox="0 0 16 16"
|
|
131
|
+
aria-hidden="true"
|
|
132
|
+
focusable="false"
|
|
133
|
+
class="h-3.5 w-3.5 transition-transform duration-100 motion-reduce:transition-none {open ? 'rotate-180' : ''}"
|
|
134
|
+
>
|
|
126
135
|
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none" />
|
|
127
136
|
</svg>
|
|
128
137
|
</button>
|
|
129
138
|
</div>
|
|
130
139
|
{#if open}
|
|
131
|
-
<ul
|
|
140
|
+
<ul
|
|
141
|
+
id={listboxId}
|
|
142
|
+
role="listbox"
|
|
143
|
+
aria-label={label}
|
|
144
|
+
class="absolute left-0 top-[calc(100%+4px)] z-[9000] m-0 max-h-60 min-w-full list-none overflow-y-auto rounded-lg border border-border-3 bg-bg-2 p-1 shadow-[0_8px_24px_rgba(0,0,0,0.5)]"
|
|
145
|
+
>
|
|
132
146
|
{#if filtered.length === 0}
|
|
133
|
-
<li class="
|
|
147
|
+
<li class="list-none p-2.5 text-center text-13 text-text-4">No results</li>
|
|
134
148
|
{:else}
|
|
135
149
|
{#each filtered as opt, idx (opt.value)}
|
|
136
150
|
<li
|
|
@@ -138,7 +152,7 @@
|
|
|
138
152
|
role="option"
|
|
139
153
|
aria-selected={opt.value === value}
|
|
140
154
|
data-active={idx === activeIdx ? 'true' : undefined}
|
|
141
|
-
class="
|
|
155
|
+
class="flex cursor-pointer items-center rounded-[5px] px-2.5 py-1.5 text-13 text-text-2 hover:bg-row-active hover:text-text-1 data-[active=true]:bg-row-active data-[active=true]:text-text-1 aria-selected:font-medium aria-selected:text-brand-05"
|
|
142
156
|
onmousedown={(e) => { e.preventDefault(); (() => pick(opt))(); }}
|
|
143
157
|
onmouseenter={() => { activeIdx = idx; }}
|
|
144
158
|
>{opt.label}</li>
|
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
onclose?: () => void;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
let {
|
|
22
|
+
let {
|
|
23
|
+
open = false,
|
|
24
|
+
items = [],
|
|
25
|
+
placeholder = 'Search pages and actions…',
|
|
23
26
|
onselect,
|
|
24
27
|
onclose,
|
|
25
28
|
}: Props = $props();
|
|
@@ -28,21 +31,24 @@
|
|
|
28
31
|
let inputEl = $state<HTMLInputElement | undefined>(undefined);
|
|
29
32
|
const listboxId = `cp-list-${Math.random().toString(36).slice(2)}`;
|
|
30
33
|
|
|
31
|
-
let filtered =
|
|
32
|
-
|
|
34
|
+
let filtered = $derived(
|
|
35
|
+
query.trim() === ''
|
|
33
36
|
? items
|
|
34
|
-
: items.filter((i) => i.label.toLowerCase().includes(query.toLowerCase()))
|
|
37
|
+
: items.filter((i) => i.label.toLowerCase().includes(query.toLowerCase())),
|
|
38
|
+
);
|
|
35
39
|
|
|
36
|
-
let grouped = $derived(
|
|
37
|
-
(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
let grouped = $derived(
|
|
41
|
+
filtered.reduce(
|
|
42
|
+
(acc, item) => {
|
|
43
|
+
const g = item.group ?? '';
|
|
44
|
+
const found = acc.find((a: { group: string }) => a.group === g);
|
|
45
|
+
if (found) found.items.push(item);
|
|
46
|
+
else acc.push({ group: g, items: [item] });
|
|
47
|
+
return acc;
|
|
48
|
+
},
|
|
49
|
+
[] as { group: string; items: CommandItem[] }[],
|
|
50
|
+
),
|
|
51
|
+
);
|
|
46
52
|
|
|
47
53
|
$effect(() => {
|
|
48
54
|
if (open) {
|
|
@@ -86,16 +92,21 @@
|
|
|
86
92
|
|
|
87
93
|
{#if open}
|
|
88
94
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
89
|
-
<div
|
|
95
|
+
<div
|
|
96
|
+
class="fixed inset-0 z-[9300] flex items-start justify-center bg-black/60 pt-[15vh] backdrop-blur-md"
|
|
97
|
+
role="presentation"
|
|
98
|
+
onclick={handleBackdrop}
|
|
99
|
+
onkeydown={handleKey}
|
|
100
|
+
>
|
|
90
101
|
<div
|
|
91
102
|
role="dialog"
|
|
92
103
|
aria-modal="true"
|
|
93
104
|
aria-label="Command palette"
|
|
94
|
-
class="
|
|
105
|
+
class="w-[520px] max-w-[90vw] overflow-hidden rounded-xl border border-border-3 bg-bg-1 shadow-[0_24px_64px_rgba(0,0,0,0.6)]"
|
|
95
106
|
>
|
|
96
|
-
<div class="
|
|
107
|
+
<div class="flex items-center gap-2.5 border-b border-border-1 px-4 py-3">
|
|
97
108
|
<svg
|
|
98
|
-
class="
|
|
109
|
+
class="shrink-0 text-text-4"
|
|
99
110
|
width="16"
|
|
100
111
|
height="16"
|
|
101
112
|
viewBox="0 0 16 16"
|
|
@@ -112,7 +123,6 @@
|
|
|
112
123
|
</svg>
|
|
113
124
|
<input
|
|
114
125
|
bind:this={inputEl}
|
|
115
|
-
class="cp-input"
|
|
116
126
|
type="text"
|
|
117
127
|
role="combobox"
|
|
118
128
|
aria-expanded={filtered.length > 0}
|
|
@@ -121,33 +131,39 @@
|
|
|
121
131
|
{placeholder}
|
|
122
132
|
bind:value={query}
|
|
123
133
|
onkeydown={handleKey}
|
|
134
|
+
class="min-w-0 flex-1 border-0 bg-transparent text-[15px] leading-none text-text-1 outline-none placeholder:text-text-4"
|
|
124
135
|
/>
|
|
125
|
-
<span class="
|
|
136
|
+
<span class="shrink-0 whitespace-nowrap rounded border border-border-2 bg-row-active px-1.5 py-0.5 text-[10px] text-text-4">Esc</span>
|
|
126
137
|
</div>
|
|
127
|
-
<ul
|
|
138
|
+
<ul
|
|
139
|
+
id={listboxId}
|
|
140
|
+
role="listbox"
|
|
141
|
+
aria-label="Results"
|
|
142
|
+
class="m-0 max-h-[360px] list-none overflow-y-auto p-1.5"
|
|
143
|
+
>
|
|
128
144
|
{#if filtered.length === 0}
|
|
129
|
-
<li class="
|
|
145
|
+
<li class="list-none p-6 text-center text-13 text-text-4">No results for “{query}”</li>
|
|
130
146
|
{:else}
|
|
131
147
|
{#each grouped as { group, items: groupItems }}
|
|
132
148
|
<li role="presentation">
|
|
133
149
|
{#if group}
|
|
134
|
-
<div class="
|
|
150
|
+
<div class="px-2.5 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-[0.06em] text-text-4">{group}</div>
|
|
135
151
|
{/if}
|
|
136
152
|
{#each groupItems as item}
|
|
137
153
|
{@const idx = filtered.indexOf(item)}
|
|
138
154
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
139
155
|
<div
|
|
140
156
|
role="option"
|
|
141
|
-
tabindex=
|
|
157
|
+
tabindex={-1}
|
|
142
158
|
aria-selected={idx === activeIdx}
|
|
143
159
|
data-active={idx === activeIdx ? 'true' : undefined}
|
|
144
|
-
class="
|
|
160
|
+
class="flex cursor-pointer list-none items-center gap-2.5 rounded-md px-2.5 py-2 text-13 text-text-2 outline-none data-[active=true]:bg-brand-05/15 data-[active=true]:text-text-1"
|
|
145
161
|
onclick={() => commit(item.id)}
|
|
146
162
|
onmouseenter={() => (activeIdx = idx)}
|
|
147
163
|
>
|
|
148
164
|
{item.label}
|
|
149
165
|
{#if item.shortcut}
|
|
150
|
-
<span class="
|
|
166
|
+
<span class="ml-auto shrink-0" aria-label={item.shortcut.replace(/\+/g, ' then ')}>
|
|
151
167
|
<Kbd keys={item.shortcut} />
|
|
152
168
|
</span>
|
|
153
169
|
{/if}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onDestroy } from 'svelte';
|
|
3
|
+
import Button from './Button.svelte';
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
open?: boolean;
|
|
@@ -26,10 +27,17 @@
|
|
|
26
27
|
const FOCUSABLE =
|
|
27
28
|
'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
// Map confirm-variant to the migrated `Button` variants. `warning` doesn't
|
|
31
|
+
// exist on `Button`, so we mark it as primary + override colours inline.
|
|
32
|
+
const CONFIRM_VARIANT: Record<NonNullable<Props['variant']>, 'primary' | 'danger'> = {
|
|
33
|
+
danger: 'danger',
|
|
34
|
+
warning: 'primary',
|
|
35
|
+
info: 'primary',
|
|
36
|
+
};
|
|
37
|
+
const CONFIRM_EXTRA: Record<NonNullable<Props['variant']>, string> = {
|
|
38
|
+
danger: '!bg-status-danger !text-white hover:!bg-[#ff5e5e] !border-status-danger',
|
|
39
|
+
warning: '!bg-status-warning !text-neutral-00 hover:!bg-[#ffba42] !border-status-warning',
|
|
40
|
+
info: '',
|
|
33
41
|
};
|
|
34
42
|
|
|
35
43
|
let dialogEl = $state<HTMLDivElement | undefined>(undefined);
|
|
@@ -59,8 +67,6 @@
|
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
// Replaces the old `$: if (open) { ... }` + `afterUpdate` combo. `$effect`
|
|
63
|
-
// runs after the DOM commits, so `dialogEl` is bound before we query it.
|
|
64
70
|
$effect(() => {
|
|
65
71
|
if (open) {
|
|
66
72
|
prev = document.activeElement as HTMLElement | null;
|
|
@@ -86,7 +92,7 @@
|
|
|
86
92
|
</script>
|
|
87
93
|
|
|
88
94
|
<div
|
|
89
|
-
class="backdrop {open ? '
|
|
95
|
+
class="fixed inset-0 z-[60] items-center justify-center bg-black/55 backdrop-blur-lg {open ? 'flex' : 'hidden'}"
|
|
90
96
|
role="presentation"
|
|
91
97
|
onclick={(e) => {
|
|
92
98
|
if (e.target === e.currentTarget) onCancel();
|
|
@@ -94,22 +100,22 @@
|
|
|
94
100
|
>
|
|
95
101
|
<div
|
|
96
102
|
bind:this={dialogEl}
|
|
97
|
-
class="
|
|
103
|
+
class="w-[400px] max-w-[90vw] overflow-hidden rounded-xl border border-border-2 bg-bg-1 shadow-modal"
|
|
98
104
|
role="alertdialog"
|
|
99
105
|
aria-modal="true"
|
|
100
106
|
aria-labelledby="cd-title"
|
|
101
107
|
aria-describedby={description ? 'cd-desc' : undefined}
|
|
102
|
-
tabindex=
|
|
108
|
+
tabindex={-1}
|
|
103
109
|
>
|
|
104
|
-
<div class="
|
|
105
|
-
<h2 id="cd-title">{title}</h2>
|
|
110
|
+
<div class="flex items-center justify-between border-b border-border-1 px-5 py-4">
|
|
111
|
+
<h2 id="cd-title" class="m-0 text-16 font-semibold">{title}</h2>
|
|
106
112
|
</div>
|
|
107
113
|
{#if description}
|
|
108
|
-
<div id="cd-desc" class="
|
|
114
|
+
<div id="cd-desc" class="px-5 py-3.5 text-13 leading-[1.5] text-text-3">{description}</div>
|
|
109
115
|
{/if}
|
|
110
|
-
<div class="
|
|
111
|
-
<
|
|
112
|
-
<
|
|
116
|
+
<div class="flex justify-end gap-2 border-t border-border-1 bg-bg-0 px-5 py-3.5">
|
|
117
|
+
<Button variant="ghost" onclick={onCancel}>{cancelLabel}</Button>
|
|
118
|
+
<Button variant={CONFIRM_VARIANT[variant]} class={CONFIRM_EXTRA[variant]} onclick={onConfirm}>{confirmLabel}</Button>
|
|
113
119
|
</div>
|
|
114
120
|
</div>
|
|
115
121
|
</div>
|
|
@@ -33,10 +33,12 @@
|
|
|
33
33
|
let menuEl = $state<HTMLUListElement | undefined>(undefined);
|
|
34
34
|
let activeIdx = $state(0);
|
|
35
35
|
|
|
36
|
-
let actionItems = $derived(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
let actionItems = $derived(
|
|
37
|
+
items.reduce<ContextMenuItem[]>((acc, e) => {
|
|
38
|
+
if (!('separator' in e)) acc.push(e);
|
|
39
|
+
return acc;
|
|
40
|
+
}, []),
|
|
41
|
+
);
|
|
40
42
|
|
|
41
43
|
const menuW = 168;
|
|
42
44
|
let menuH = $derived(items.length * 30 + 8);
|
|
@@ -92,28 +94,26 @@
|
|
|
92
94
|
bind:this={menuEl}
|
|
93
95
|
role="menu"
|
|
94
96
|
aria-label={label}
|
|
95
|
-
tabindex=
|
|
96
|
-
class="
|
|
97
|
+
tabindex={-1}
|
|
98
|
+
class="fixed z-[9200] m-0 min-w-[168px] list-none rounded-lg border border-border-3 bg-bg-2 p-1 shadow-[0_8px_24px_rgba(0,0,0,0.5)] focus:outline-none"
|
|
97
99
|
style="left:{cx}px;top:{cy}px"
|
|
98
100
|
onkeydown={handleKeyDown}
|
|
99
101
|
>
|
|
100
102
|
{#each items as entry, i (i)}
|
|
101
103
|
{#if 'separator' in entry}
|
|
102
|
-
<li role="separator" class="
|
|
104
|
+
<li role="separator" class="my-1 h-px bg-row-active"></li>
|
|
103
105
|
{:else}
|
|
104
106
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
105
107
|
<li
|
|
106
108
|
role="menuitem"
|
|
107
|
-
tabindex=
|
|
109
|
+
tabindex={-1}
|
|
108
110
|
aria-disabled={entry.disabled || undefined}
|
|
109
111
|
data-active={actionIndex(entry) === activeIdx ? 'true' : undefined}
|
|
110
112
|
data-danger={entry.danger || undefined}
|
|
111
|
-
class="
|
|
113
|
+
class="flex cursor-pointer items-center whitespace-nowrap rounded-[5px] px-3 py-1.5 text-13 text-text-2 outline-none hover:bg-row-active hover:text-text-1 data-[active=true]:bg-row-active data-[active=true]:text-text-1 data-[danger=true]:text-status-danger data-[danger=true]:hover:bg-status-danger/10 aria-disabled:cursor-not-allowed aria-disabled:opacity-40"
|
|
112
114
|
onmouseenter={() => (activeIdx = actionIndex(entry))}
|
|
113
115
|
onmousedown={(e) => { e.preventDefault(); (() => { if (!entry.disabled) activate(entry.id); })(); }}
|
|
114
|
-
>
|
|
115
|
-
{entry.label}
|
|
116
|
-
</li>
|
|
116
|
+
>{entry.label}</li>
|
|
117
117
|
{/if}
|
|
118
118
|
{/each}
|
|
119
119
|
</ul>
|
|
@@ -50,16 +50,21 @@
|
|
|
50
50
|
window.removeEventListener('keydown', onKey);
|
|
51
51
|
document.removeEventListener('pointerdown', onPointer);
|
|
52
52
|
});
|
|
53
|
+
|
|
54
|
+
const PLACEMENT: Record<NonNullable<Props['placement']>, string> = {
|
|
55
|
+
top: 'bottom-[calc(100%+8px)] left-1/2 -translate-x-1/2',
|
|
56
|
+
bottom: 'top-[calc(100%+8px)] left-1/2 -translate-x-1/2',
|
|
57
|
+
};
|
|
53
58
|
</script>
|
|
54
59
|
|
|
55
|
-
<span class="
|
|
60
|
+
<span class="relative inline-flex items-center" bind:this={rootEl}>
|
|
56
61
|
<button
|
|
57
62
|
bind:this={triggerEl}
|
|
58
63
|
type="button"
|
|
59
|
-
class="ch-trigger"
|
|
60
64
|
aria-label="Help"
|
|
61
65
|
aria-haspopup="dialog"
|
|
62
66
|
aria-expanded={open}
|
|
67
|
+
class="inline-flex h-4 w-4 shrink-0 cursor-pointer items-center justify-center rounded-full border border-border-3 bg-transparent p-0 text-[10px] font-bold leading-none text-text-4 transition-[border-color,color] duration-100 hover:border-text-3 hover:text-text-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
|
|
63
68
|
onclick={toggle}
|
|
64
69
|
>?</button>
|
|
65
70
|
{#if open}
|
|
@@ -67,11 +72,11 @@
|
|
|
67
72
|
bind:this={panelEl}
|
|
68
73
|
role="dialog"
|
|
69
74
|
aria-labelledby={titleId}
|
|
70
|
-
class="
|
|
71
|
-
tabindex=
|
|
75
|
+
class="absolute z-[9200] min-w-[220px] max-w-[300px] rounded-[10px] border border-border-3 bg-bg-2 shadow-[0_8px_32px_rgba(0,0,0,0.5)] focus:outline-none {PLACEMENT[placement]}"
|
|
76
|
+
tabindex={-1}
|
|
72
77
|
>
|
|
73
|
-
<div id={titleId} class="
|
|
74
|
-
<div class="
|
|
78
|
+
<div id={titleId} class="border-b border-border-1 px-3.5 pb-2 pt-2.5 text-12 font-semibold text-text-2">{title}</div>
|
|
79
|
+
<div class="px-3.5 py-2.5 text-12 leading-[1.5] text-text-3">{body}</div>
|
|
75
80
|
</div>
|
|
76
81
|
{/if}
|
|
77
82
|
</span>
|
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
|
|
21
21
|
<button
|
|
22
22
|
type="button"
|
|
23
|
-
|
|
23
|
+
data-copied={copied ? 'true' : undefined}
|
|
24
24
|
aria-label={copied ? 'Copied!' : label}
|
|
25
|
+
class="inline-flex h-[22px] w-[22px] shrink-0 cursor-pointer items-center justify-center rounded border-0 bg-transparent text-text-4 transition-[color,background-color] duration-100 hover:bg-row-hover hover:text-text-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05 data-[copied=true]:text-status-success data-[copied=true]:hover:bg-status-success/10"
|
|
25
26
|
onclick={handleCopy}
|
|
26
27
|
>
|
|
27
28
|
{#if copied}
|