@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
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<script lang="ts">
|
|
6
|
-
|
|
7
|
-
|
|
8
6
|
interface Props {
|
|
9
7
|
label?: string | undefined;
|
|
10
8
|
length?: number;
|
|
@@ -73,16 +71,24 @@
|
|
|
73
71
|
commit(next);
|
|
74
72
|
setTimeout(() => focusAt(Math.min(text.length, length - 1)), 0);
|
|
75
73
|
}
|
|
74
|
+
|
|
75
|
+
const DIGIT_CLS =
|
|
76
|
+
'h-9 w-9 rounded-md border border-border-2 bg-bg-page text-center font-mono text-15 text-text-1 outline-none transition-colors duration-100 focus:border-brand-05 focus:shadow-[0_0_0_2px_rgba(0,111,255,0.2)]';
|
|
76
77
|
</script>
|
|
77
78
|
|
|
78
|
-
<div class=
|
|
79
|
+
<div class={className}>
|
|
79
80
|
{#if label}
|
|
80
|
-
<label for="{uid}-0" class="
|
|
81
|
+
<label for="{uid}-0" class="text-12 text-text-4">{label}</label>
|
|
81
82
|
{/if}
|
|
82
|
-
<div
|
|
83
|
+
<div
|
|
84
|
+
role="group"
|
|
85
|
+
aria-label={label ?? 'One-time password'}
|
|
86
|
+
data-disabled={disabled ? 'true' : undefined}
|
|
87
|
+
class="inline-flex items-center gap-1.5 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-40"
|
|
88
|
+
>
|
|
83
89
|
{#each digits as d, i}
|
|
84
90
|
{#if i === half}
|
|
85
|
-
<span class="
|
|
91
|
+
<span aria-hidden="true" class="select-none text-13 text-text-4">–</span>
|
|
86
92
|
{/if}
|
|
87
93
|
<input
|
|
88
94
|
bind:this={inputs[i]}
|
|
@@ -91,10 +97,10 @@
|
|
|
91
97
|
inputmode="numeric"
|
|
92
98
|
pattern="[0-9]*"
|
|
93
99
|
maxlength={1}
|
|
94
|
-
aria-label=
|
|
100
|
+
aria-label={`Digit ${i + 1} of ${length}`}
|
|
95
101
|
value={d}
|
|
96
102
|
{disabled}
|
|
97
|
-
class=
|
|
103
|
+
class={DIGIT_CLS}
|
|
98
104
|
autocomplete={i === 0 ? 'one-time-code' : 'off'}
|
|
99
105
|
oninput={(e) => handleInput(i, e.currentTarget.value)}
|
|
100
106
|
onkeydown={(e) => handleKey(i, e)}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
title: string;
|
|
4
|
+
sticky?: boolean;
|
|
5
|
+
class?: string;
|
|
6
|
+
actions?: import('svelte').Snippet;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
title,
|
|
11
|
+
sticky = true,
|
|
12
|
+
class: className = '',
|
|
13
|
+
actions,
|
|
14
|
+
}: Props = $props();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<div
|
|
18
|
+
class="ph-bar {className}"
|
|
19
|
+
style={sticky ? undefined : 'position: static'}
|
|
20
|
+
>
|
|
21
|
+
<h2 class="ph-title">{title}</h2>
|
|
22
|
+
{#if actions}
|
|
23
|
+
<div class="ph-actions">{@render actions?.()}</div>
|
|
24
|
+
{/if}
|
|
25
|
+
</div>
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
2
|
interface Props {
|
|
5
3
|
page?: number;
|
|
6
4
|
pageSize?: number;
|
|
@@ -8,9 +6,8 @@
|
|
|
8
6
|
onchange?: (payload: number) => void;
|
|
9
7
|
}
|
|
10
8
|
|
|
11
|
-
let { page = 1, pageSize = 10, total = 0,
|
|
12
|
-
|
|
13
|
-
}: Props = $props();
|
|
9
|
+
let { page = 1, pageSize = 10, total = 0, onchange }: Props = $props();
|
|
10
|
+
|
|
14
11
|
function getPageNumbers(p: number, tp: number): (number | '...')[] {
|
|
15
12
|
if (tp <= 7) return Array.from({ length: tp }, (_, i) => i + 1);
|
|
16
13
|
const items: (number | '...')[] = [1];
|
|
@@ -23,38 +20,45 @@
|
|
|
23
20
|
return items;
|
|
24
21
|
}
|
|
25
22
|
|
|
26
|
-
function go(p: number) {
|
|
23
|
+
function go(p: number) {
|
|
24
|
+
onchange?.(p);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
27
|
let totalPages = $derived(Math.max(1, Math.ceil(total / pageSize)));
|
|
28
28
|
let pages = $derived(getPageNumbers(page, totalPages));
|
|
29
|
+
|
|
30
|
+
const BTN_BASE =
|
|
31
|
+
'inline-flex h-[30px] min-w-[30px] cursor-pointer items-center justify-center rounded-md border border-transparent bg-transparent px-1.5 text-13 font-medium leading-none text-text-3 transition-[background-color,color,border-color] duration-100 hover:bg-row-hover hover:text-text-1 hover:border-border-2 disabled:cursor-not-allowed disabled:opacity-35 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05';
|
|
32
|
+
const BTN_ACTIVE = 'bg-brand-05 text-white border-brand-05 hover:bg-brand-06 hover:border-brand-06';
|
|
29
33
|
</script>
|
|
30
34
|
|
|
31
35
|
{#if totalPages > 1}
|
|
32
|
-
<nav class="
|
|
36
|
+
<nav class="flex items-center gap-0.5" aria-label="Pagination">
|
|
33
37
|
<button
|
|
34
38
|
type="button"
|
|
35
|
-
class="pagination-btn"
|
|
36
39
|
aria-label="Previous page"
|
|
37
40
|
disabled={page <= 1}
|
|
41
|
+
class={BTN_BASE}
|
|
38
42
|
onclick={() => go(page - 1)}
|
|
39
43
|
>‹</button>
|
|
40
44
|
{#each pages as p, i (typeof p === 'number' ? p : `e${i}`)}
|
|
41
45
|
{#if p === '...'}
|
|
42
|
-
<span class="
|
|
46
|
+
<span aria-hidden="true" class="inline-flex h-[30px] min-w-[30px] items-center justify-center text-13 text-text-4">…</span>
|
|
43
47
|
{:else}
|
|
44
48
|
<button
|
|
45
49
|
type="button"
|
|
46
|
-
|
|
47
|
-
aria-label="Page {p}"
|
|
50
|
+
aria-label={`Page ${p}`}
|
|
48
51
|
aria-current={p === page ? 'page' : undefined}
|
|
52
|
+
class="{BTN_BASE} {p === page ? BTN_ACTIVE : ''}"
|
|
49
53
|
onclick={() => go(p)}
|
|
50
54
|
>{p}</button>
|
|
51
55
|
{/if}
|
|
52
56
|
{/each}
|
|
53
57
|
<button
|
|
54
58
|
type="button"
|
|
55
|
-
class="pagination-btn"
|
|
56
59
|
aria-label="Next page"
|
|
57
60
|
disabled={page >= totalPages}
|
|
61
|
+
class={BTN_BASE}
|
|
58
62
|
onclick={() => go(page + 1)}
|
|
59
63
|
>›</button>
|
|
60
64
|
</nav>
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<script lang="ts">
|
|
6
|
-
|
|
7
|
-
|
|
8
6
|
interface Props {
|
|
9
7
|
label?: string | undefined;
|
|
10
8
|
value?: string;
|
|
@@ -34,15 +32,18 @@
|
|
|
34
32
|
}
|
|
35
33
|
</script>
|
|
36
34
|
|
|
37
|
-
<div class=
|
|
35
|
+
<div class={className}>
|
|
38
36
|
{#if label}
|
|
39
|
-
<label for={uid} class="
|
|
37
|
+
<label for={uid} class="text-12 text-text-4">{label}</label>
|
|
40
38
|
{/if}
|
|
41
|
-
<div
|
|
39
|
+
<div
|
|
40
|
+
data-disabled={disabled ? 'true' : undefined}
|
|
41
|
+
class="flex h-[34px] items-stretch overflow-hidden rounded-md border border-border-2 bg-bg-page transition-colors duration-100 focus-within:border-brand-05 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-40"
|
|
42
|
+
>
|
|
42
43
|
<input
|
|
43
44
|
id={uid}
|
|
44
45
|
type={shown ? 'text' : 'password'}
|
|
45
|
-
class="
|
|
46
|
+
class="min-w-0 flex-1 border-0 bg-transparent px-3 text-13 text-text-1 outline-none placeholder:text-text-4"
|
|
46
47
|
{value}
|
|
47
48
|
{placeholder}
|
|
48
49
|
{disabled}
|
|
@@ -51,10 +52,10 @@
|
|
|
51
52
|
/>
|
|
52
53
|
<button
|
|
53
54
|
type="button"
|
|
54
|
-
class="pwd-input__toggle"
|
|
55
55
|
aria-pressed={shown}
|
|
56
56
|
aria-label={shown ? 'Hide password' : 'Show password'}
|
|
57
57
|
{disabled}
|
|
58
|
+
class="inline-flex h-full w-9 shrink-0 cursor-pointer items-center justify-center border-0 border-l border-l-white/[0.08] bg-transparent text-text-4 transition-colors duration-100 hover:bg-row-hover hover:text-text-1 focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-brand-05"
|
|
58
59
|
onclick={() => (shown = !shown)}
|
|
59
60
|
>
|
|
60
61
|
{#if shown}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
export type PillVariant = 'success' | 'warn' | 'danger' | 'info' | 'neutral';
|
|
5
|
+
|
|
3
6
|
interface Props {
|
|
4
|
-
variant?:
|
|
7
|
+
variant?: PillVariant;
|
|
5
8
|
showDot?: boolean;
|
|
6
9
|
class?: string;
|
|
7
10
|
style?: string;
|
|
8
|
-
children?:
|
|
11
|
+
children?: Snippet;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
let {
|
|
@@ -13,11 +16,36 @@
|
|
|
13
16
|
showDot = true,
|
|
14
17
|
class: className = '',
|
|
15
18
|
style = '',
|
|
16
|
-
children
|
|
19
|
+
children,
|
|
17
20
|
}: Props = $props();
|
|
21
|
+
|
|
22
|
+
// Pre-compose so Tailwind's static scanner picks up every variant.
|
|
23
|
+
// Backgrounds use the literal 14% opacity from `dashboard.css`; text
|
|
24
|
+
// colours resolve to the semantic status tokens so they stay legible
|
|
25
|
+
// against the 14% tinted background in both motifs. The neutral variant
|
|
26
|
+
// sits on `--row-active` (brand-blue tint) so it stays visible on a
|
|
27
|
+
// white page bg.
|
|
28
|
+
const PILL: Record<PillVariant, string> = {
|
|
29
|
+
success: 'bg-status-success/14 text-status-success',
|
|
30
|
+
warn: 'bg-status-warning/14 text-status-warning',
|
|
31
|
+
danger: 'bg-status-danger/14 text-status-danger',
|
|
32
|
+
info: 'bg-status-info/14 text-status-info',
|
|
33
|
+
neutral: 'bg-row-active text-text-2',
|
|
34
|
+
};
|
|
35
|
+
const DOT: Record<PillVariant, string> = {
|
|
36
|
+
success: 'bg-status-success',
|
|
37
|
+
warn: 'bg-status-warning',
|
|
38
|
+
danger: 'bg-status-danger',
|
|
39
|
+
info: 'bg-status-info',
|
|
40
|
+
neutral: 'bg-status-neutral',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
let pillClass = $derived(
|
|
44
|
+
`inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-medium leading-[1.4] ${PILL[variant]} ${className}`,
|
|
45
|
+
);
|
|
18
46
|
</script>
|
|
19
47
|
|
|
20
|
-
<span class=
|
|
21
|
-
{#if showDot}<span class="
|
|
48
|
+
<span class={pillClass} {style}>
|
|
49
|
+
{#if showDot}<span class="h-1.5 w-1.5 rounded-full {DOT[variant]}" aria-hidden="true"></span>{/if}
|
|
22
50
|
{@render children?.()}
|
|
23
51
|
</span>
|
|
@@ -4,12 +4,34 @@
|
|
|
4
4
|
|
|
5
5
|
<script lang="ts">
|
|
6
6
|
import { onMount, onDestroy, tick } from 'svelte';
|
|
7
|
+
import Button from './Button.svelte';
|
|
8
|
+
import { portal } from '../actions/portal.ts';
|
|
7
9
|
|
|
8
10
|
interface Props {
|
|
9
11
|
label: string;
|
|
10
12
|
variant?: 'ghost' | 'primary';
|
|
11
13
|
title?: string | undefined;
|
|
12
14
|
placement?: 'bottom-start' | 'bottom-end' | 'bottom';
|
|
15
|
+
/**
|
|
16
|
+
* Controlled open state. Default uncontrolled (`$bindable(false)`):
|
|
17
|
+
* the trigger button toggles it and outside-clicks close it.
|
|
18
|
+
* Callers can `bind:open` to drive it programmatically (e.g. close
|
|
19
|
+
* the popover after a form submit) or to coordinate with other
|
|
20
|
+
* UI. When the trigger button is used, the bound variable updates
|
|
21
|
+
* automatically.
|
|
22
|
+
*/
|
|
23
|
+
open?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Optional custom trigger snippet. When provided, renders inline
|
|
26
|
+
* with the rest of the popover root and receives a `toggle` fn so
|
|
27
|
+
* the snippet can drive open/close. The default `label`-Button
|
|
28
|
+
* trigger renders only when no `trigger` snippet is passed.
|
|
29
|
+
*
|
|
30
|
+
* Use for icon-only triggers (eye-icon view-options menus, gear
|
|
31
|
+
* icons, etc.) where the upstream `<Button label>` chrome doesn't
|
|
32
|
+
* fit the surrounding UI.
|
|
33
|
+
*/
|
|
34
|
+
trigger?: import('svelte').Snippet<[{ toggle: () => void; open: boolean }]>;
|
|
13
35
|
children?: import('svelte').Snippet;
|
|
14
36
|
}
|
|
15
37
|
|
|
@@ -18,7 +40,9 @@
|
|
|
18
40
|
variant = 'ghost',
|
|
19
41
|
title = undefined,
|
|
20
42
|
placement = 'bottom-start',
|
|
21
|
-
|
|
43
|
+
open = $bindable(false),
|
|
44
|
+
trigger,
|
|
45
|
+
children,
|
|
22
46
|
}: Props = $props();
|
|
23
47
|
|
|
24
48
|
const uid = `dash-ui-pop-${++counter}`;
|
|
@@ -27,15 +51,40 @@
|
|
|
27
51
|
const FOCUSABLE =
|
|
28
52
|
'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
|
|
29
53
|
|
|
30
|
-
let open = $state(false);
|
|
31
54
|
let rootEl = $state<HTMLDivElement | undefined>(undefined);
|
|
32
|
-
let triggerEl = $state<
|
|
55
|
+
let triggerEl = $state<HTMLElement | undefined>(undefined);
|
|
33
56
|
let panelEl = $state<HTMLDivElement | undefined>(undefined);
|
|
57
|
+
/** Top + left in viewport coordinates. Computed from the trigger's
|
|
58
|
+
* bounding rect every time the panel opens / window resizes /
|
|
59
|
+
* the user scrolls. The panel uses `position: fixed` so we never
|
|
60
|
+
* get clipped by an ancestor's `overflow: hidden`. */
|
|
61
|
+
let panelTop = $state(0);
|
|
62
|
+
let panelLeft = $state(0);
|
|
63
|
+
|
|
64
|
+
/** Recompute the panel's viewport coords. The panel itself must be
|
|
65
|
+
* rendered (size > 0) for accurate measurement; call after the
|
|
66
|
+
* open + tick + paint. */
|
|
67
|
+
function recomputePosition() {
|
|
68
|
+
if (!triggerEl || !panelEl) return;
|
|
69
|
+
const tr = triggerEl.getBoundingClientRect();
|
|
70
|
+
const margin = 8;
|
|
71
|
+
const offset = 6; // gap between trigger bottom and panel top
|
|
72
|
+
const panelWidth = panelEl.offsetWidth;
|
|
73
|
+
let left: number;
|
|
74
|
+
if (placement === 'bottom-start') left = tr.left;
|
|
75
|
+
else if (placement === 'bottom-end') left = tr.right - panelWidth;
|
|
76
|
+
else left = tr.left + tr.width / 2 - panelWidth / 2; // 'bottom'
|
|
77
|
+
// Clamp into the visible viewport with the configured gutter.
|
|
78
|
+
left = Math.max(margin, Math.min(left, window.innerWidth - panelWidth - margin));
|
|
79
|
+
panelLeft = left;
|
|
80
|
+
panelTop = tr.bottom + offset;
|
|
81
|
+
}
|
|
34
82
|
|
|
35
83
|
async function toggle() {
|
|
36
84
|
if (!open) {
|
|
37
85
|
open = true;
|
|
38
86
|
await tick();
|
|
87
|
+
recomputePosition();
|
|
39
88
|
const first = panelEl?.querySelector<HTMLElement>(FOCUSABLE);
|
|
40
89
|
(first ?? panelEl)?.focus();
|
|
41
90
|
} else {
|
|
@@ -63,38 +112,73 @@
|
|
|
63
112
|
}
|
|
64
113
|
|
|
65
114
|
function onPointer(e: PointerEvent) {
|
|
66
|
-
if (open
|
|
115
|
+
if (!open) return;
|
|
116
|
+
const target = e.target as Node;
|
|
117
|
+
// The panel is portalled to <body> so it's no longer a descendant
|
|
118
|
+
// of rootEl. Check both so clicks INSIDE the panel don't count as
|
|
119
|
+
// outside-clicks and immediately close the popover.
|
|
120
|
+
if (rootEl?.contains(target)) return;
|
|
121
|
+
if (panelEl?.contains(target)) return;
|
|
122
|
+
open = false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function onResize() {
|
|
126
|
+
if (open) recomputePosition();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function onScroll() {
|
|
130
|
+
if (open) recomputePosition();
|
|
67
131
|
}
|
|
68
132
|
|
|
69
133
|
onMount(() => {
|
|
70
134
|
window.addEventListener('keydown', onKey);
|
|
135
|
+
window.addEventListener('resize', onResize);
|
|
136
|
+
// Capture-phase scroll listener picks up scrolls in ANY ancestor
|
|
137
|
+
// (the sidebar's own overflow, the page itself, etc.) so the
|
|
138
|
+
// fixed-positioned panel tracks the trigger as it moves.
|
|
139
|
+
window.addEventListener('scroll', onScroll, true);
|
|
71
140
|
document.addEventListener('pointerdown', onPointer);
|
|
141
|
+
triggerEl =
|
|
142
|
+
rootEl?.querySelector<HTMLButtonElement>('button[aria-haspopup="dialog"]') ?? undefined;
|
|
72
143
|
});
|
|
73
144
|
onDestroy(() => {
|
|
74
145
|
window.removeEventListener('keydown', onKey);
|
|
146
|
+
window.removeEventListener('resize', onResize);
|
|
147
|
+
window.removeEventListener('scroll', onScroll, true);
|
|
75
148
|
document.removeEventListener('pointerdown', onPointer);
|
|
76
149
|
});
|
|
77
150
|
</script>
|
|
78
151
|
|
|
79
|
-
<div class="
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
>{label}</button>
|
|
152
|
+
<div class="relative inline-flex items-center" bind:this={rootEl}>
|
|
153
|
+
{#if trigger}
|
|
154
|
+
{@render trigger({ toggle, open })}
|
|
155
|
+
{:else}
|
|
156
|
+
<Button {variant} aria-label={label} onclick={toggle}>
|
|
157
|
+
{label}
|
|
158
|
+
</Button>
|
|
159
|
+
{/if}
|
|
88
160
|
{#if open}
|
|
161
|
+
<!--
|
|
162
|
+
`position: fixed` so the panel can extend outside any ancestor
|
|
163
|
+
with `overflow: hidden` (the sidebar's session-list scroll, the
|
|
164
|
+
app's main grid columns, etc.). Coords are computed from the
|
|
165
|
+
trigger's `getBoundingClientRect()` in `recomputePosition`.
|
|
166
|
+
-->
|
|
89
167
|
<div
|
|
90
168
|
bind:this={panelEl}
|
|
169
|
+
use:portal
|
|
91
170
|
role="dialog"
|
|
92
171
|
aria-labelledby={title ? titleId : undefined}
|
|
93
|
-
class="
|
|
94
|
-
|
|
172
|
+
class="fixed z-[9100] min-w-[200px] max-w-[320px] rounded-[10px] border border-border-3 bg-bg-2 shadow-[0_8px_32px_rgba(0,0,0,0.5)] focus:outline-none"
|
|
173
|
+
style={`top: ${panelTop}px; left: ${panelLeft}px`}
|
|
174
|
+
tabindex={-1}
|
|
95
175
|
>
|
|
96
|
-
{#if title}
|
|
97
|
-
|
|
176
|
+
{#if title}
|
|
177
|
+
<div id={titleId} class="border-b border-border-1 px-3.5 pb-2 pt-2.5 text-11 font-semibold uppercase tracking-[0.06em] text-text-4">
|
|
178
|
+
{title}
|
|
179
|
+
</div>
|
|
180
|
+
{/if}
|
|
181
|
+
<div class="flex flex-col gap-2.5 px-3.5 py-3">{@render children?.()}</div>
|
|
98
182
|
</div>
|
|
99
183
|
{/if}
|
|
100
184
|
</div>
|
|
@@ -10,22 +10,22 @@
|
|
|
10
10
|
value,
|
|
11
11
|
label = undefined,
|
|
12
12
|
valueText = undefined,
|
|
13
|
-
color = '#006FFF'
|
|
13
|
+
color = '#006FFF',
|
|
14
14
|
}: Props = $props();
|
|
15
15
|
let clamped = $derived(Math.max(0, Math.min(100, value)));
|
|
16
16
|
let displayed = $derived(valueText ?? `${Math.round(clamped)}%`);
|
|
17
17
|
let hasHeader = $derived(label !== undefined || valueText !== undefined);
|
|
18
18
|
</script>
|
|
19
19
|
|
|
20
|
-
<div class="
|
|
20
|
+
<div class="flex flex-col gap-1">
|
|
21
21
|
{#if hasHeader}
|
|
22
|
-
<div class="
|
|
23
|
-
{#if label}<span class="
|
|
24
|
-
<span class="
|
|
22
|
+
<div class="flex justify-between text-12">
|
|
23
|
+
{#if label}<span class="text-text-2">{label}</span>{/if}
|
|
24
|
+
<span class="font-mono text-text-3">{displayed}</span>
|
|
25
25
|
</div>
|
|
26
26
|
{/if}
|
|
27
27
|
<div
|
|
28
|
-
class="
|
|
28
|
+
class="h-1.5 overflow-hidden rounded-[3px] bg-row-active"
|
|
29
29
|
role="progressbar"
|
|
30
30
|
aria-valuenow={Math.round(clamped)}
|
|
31
31
|
aria-valuemin={0}
|
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
aria-label={label}
|
|
34
34
|
aria-valuetext={displayed}
|
|
35
35
|
>
|
|
36
|
-
<div
|
|
36
|
+
<div
|
|
37
|
+
class="h-full rounded-[3px] transition-[width] duration-300 ease-out motion-reduce:transition-none"
|
|
38
|
+
style="width:{clamped}%;background:{color};"
|
|
39
|
+
></div>
|
|
37
40
|
</div>
|
|
38
41
|
</div>
|
|
@@ -24,20 +24,34 @@
|
|
|
24
24
|
options = [],
|
|
25
25
|
value = $bindable(undefined),
|
|
26
26
|
srOnlyLegend = false,
|
|
27
|
-
horizontal = false
|
|
27
|
+
horizontal = false,
|
|
28
28
|
}: Props = $props();
|
|
29
29
|
|
|
30
30
|
// svelte-ignore state_referenced_locally
|
|
31
31
|
const groupName = name ?? `dash-ui-rg-${++counter}`;
|
|
32
|
+
|
|
33
|
+
// Native radio input drawn with Tailwind utilities. The selected dot is
|
|
34
|
+
// an inline-SVG bg-image (matches the legacy checkbox approach).
|
|
35
|
+
const DOT_SVG = `url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='8' cy='8' r='3' fill='%23fff'/%3E%3C/svg%3E")`;
|
|
36
|
+
|
|
37
|
+
const INPUT_CLS =
|
|
38
|
+
'mt-px relative inline-block h-4 w-4 min-w-4 shrink-0 cursor-pointer appearance-none rounded-full border-[1.5px] border-border-3 bg-bg-1 align-middle bg-center bg-no-repeat transition-[border-color,background-color] duration-100 hover:border-text-3 checked:border-brand-05 checked:bg-brand-05 disabled:cursor-not-allowed focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05';
|
|
39
|
+
|
|
40
|
+
// Horizontal layout uses flex-wrap row gap; vertical stacks the options.
|
|
41
|
+
let groupCls = $derived(
|
|
42
|
+
horizontal ? 'flex flex-wrap gap-x-5 gap-y-1 border-0 m-0 p-0' : 'flex flex-col border-0 m-0 p-0',
|
|
43
|
+
);
|
|
32
44
|
</script>
|
|
33
45
|
|
|
34
|
-
<fieldset class=
|
|
35
|
-
<legend class={srOnlyLegend ? 'sr-only' : '
|
|
46
|
+
<fieldset class={groupCls}>
|
|
47
|
+
<legend class={srOnlyLegend ? 'sr-only' : 'block p-0 mb-2 text-12 text-text-3'}>{legend}</legend>
|
|
36
48
|
{#each options as opt}
|
|
37
49
|
{@const optId = `${groupName}-${opt.value}`}
|
|
38
50
|
<label
|
|
39
51
|
for={optId}
|
|
40
|
-
class="
|
|
52
|
+
class="flex cursor-pointer select-none items-start gap-2 py-1.5 text-13 text-text-2 {opt.disabled
|
|
53
|
+
? 'cursor-not-allowed opacity-40'
|
|
54
|
+
: ''}"
|
|
41
55
|
>
|
|
42
56
|
<input
|
|
43
57
|
type="radio"
|
|
@@ -46,13 +60,14 @@
|
|
|
46
60
|
value={opt.value}
|
|
47
61
|
checked={value === opt.value}
|
|
48
62
|
disabled={opt.disabled}
|
|
49
|
-
class=
|
|
63
|
+
class={INPUT_CLS}
|
|
64
|
+
style={value === opt.value ? `background-image: ${DOT_SVG};` : ''}
|
|
50
65
|
onchange={() => { value = opt.value; }}
|
|
51
66
|
/>
|
|
52
|
-
<span class="
|
|
67
|
+
<span class="flex flex-col gap-px">
|
|
53
68
|
<span>{opt.label}</span>
|
|
54
69
|
{#if opt.description}
|
|
55
|
-
<span class="
|
|
70
|
+
<span class="text-12 text-text-3">{opt.description}</span>
|
|
56
71
|
{/if}
|
|
57
72
|
</span>
|
|
58
73
|
</label>
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
2
|
interface Props {
|
|
5
3
|
label?: string | undefined;
|
|
6
4
|
min?: number;
|
|
@@ -37,26 +35,33 @@
|
|
|
37
35
|
high = Math.max(Number((e.target as HTMLInputElement).value), low + step);
|
|
38
36
|
onchange?.([low, high]);
|
|
39
37
|
}
|
|
38
|
+
|
|
39
|
+
// Native dual-thumb range slider. Both `<input type="range">` overlap
|
|
40
|
+
// visually; we toggle z-index so the focused one is on top.
|
|
41
|
+
const INPUT_CLS =
|
|
42
|
+
'pointer-events-auto absolute inset-x-0 top-0 m-0 h-4 w-full cursor-pointer appearance-none bg-transparent outline-none [&::-webkit-slider-thumb]:[-webkit-appearance:none] [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-brand-05 [&::-webkit-slider-thumb]:bg-white [&::-moz-range-thumb]:h-3.5 [&::-moz-range-thumb]:w-3.5 [&::-moz-range-thumb]:cursor-pointer [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-brand-05 [&::-moz-range-thumb]:bg-white';
|
|
40
43
|
</script>
|
|
41
44
|
|
|
42
|
-
<div class="
|
|
45
|
+
<div class="flex flex-col gap-1.5">
|
|
43
46
|
{#if label}
|
|
44
|
-
<div class="
|
|
45
|
-
<span class="
|
|
46
|
-
<span class="
|
|
47
|
+
<div class="flex items-center justify-between gap-2">
|
|
48
|
+
<span class="text-12 text-text-4">{label}</span>
|
|
49
|
+
<span class="text-12 font-medium text-text-1 tabular-nums" aria-live="polite">{low}{suffix ?? ''} – {high}{suffix ?? ''}</span>
|
|
47
50
|
</div>
|
|
48
51
|
{/if}
|
|
49
52
|
<div
|
|
50
|
-
class="
|
|
51
|
-
style="--rs-low:{lowPct}%;--rs-high:{highPct}%"
|
|
53
|
+
class="relative h-4"
|
|
52
54
|
role="group"
|
|
53
55
|
aria-label={label}
|
|
54
56
|
>
|
|
55
|
-
<div class="
|
|
56
|
-
<div
|
|
57
|
+
<div class="absolute left-0 right-0 top-1/2 h-1 -translate-y-1/2 rounded-sm bg-bg-3"></div>
|
|
58
|
+
<div
|
|
59
|
+
class="absolute top-1/2 h-1 -translate-y-1/2 rounded-sm bg-brand-05"
|
|
60
|
+
style="left:{lowPct}%; right:{100 - highPct}%"
|
|
61
|
+
></div>
|
|
57
62
|
<input
|
|
58
63
|
type="range"
|
|
59
|
-
class=
|
|
64
|
+
class={INPUT_CLS}
|
|
60
65
|
{min} {max} {step}
|
|
61
66
|
value={low}
|
|
62
67
|
{disabled}
|
|
@@ -67,7 +72,7 @@
|
|
|
67
72
|
/>
|
|
68
73
|
<input
|
|
69
74
|
type="range"
|
|
70
|
-
class=
|
|
75
|
+
class={INPUT_CLS}
|
|
71
76
|
{min} {max} {step}
|
|
72
77
|
value={high}
|
|
73
78
|
{disabled}
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
</script>
|
|
9
9
|
|
|
10
10
|
<script lang="ts">
|
|
11
|
-
|
|
12
11
|
interface Props {
|
|
13
12
|
items: RankedItem[];
|
|
14
13
|
unit?: string | undefined;
|
|
@@ -16,17 +15,12 @@
|
|
|
16
15
|
ariaLabel?: string;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
let {
|
|
20
|
-
items,
|
|
21
|
-
unit = undefined,
|
|
22
|
-
max = undefined,
|
|
23
|
-
ariaLabel = 'Ranked list'
|
|
24
|
-
}: Props = $props();
|
|
18
|
+
let { items, unit = undefined, max = undefined, ariaLabel = 'Ranked list' }: Props = $props();
|
|
25
19
|
|
|
26
20
|
let resolvedMax = $derived(max ?? Math.max(...items.map((i) => i.value), 1));
|
|
27
21
|
</script>
|
|
28
22
|
|
|
29
|
-
<table class="
|
|
23
|
+
<table class="w-full border-collapse text-13" aria-label={ariaLabel}>
|
|
30
24
|
<caption class="sr-only">{ariaLabel}</caption>
|
|
31
25
|
<thead class="sr-only">
|
|
32
26
|
<tr>
|
|
@@ -38,22 +32,22 @@
|
|
|
38
32
|
</thead>
|
|
39
33
|
<tbody>
|
|
40
34
|
{#each items as item, i}
|
|
41
|
-
<tr class="
|
|
42
|
-
<td class="
|
|
43
|
-
<td class="
|
|
44
|
-
<div class="
|
|
45
|
-
{#if item.sublabel}<div class="
|
|
35
|
+
<tr class="border-b border-border-1 last:border-b-0">
|
|
36
|
+
<td class="w-8 py-1.5 text-12 text-text-4 tabular-nums">{i + 1}</td>
|
|
37
|
+
<td class="py-1.5 pr-3">
|
|
38
|
+
<div class="text-text-2">{item.label}</div>
|
|
39
|
+
{#if item.sublabel}<div class="text-11 text-text-4">{item.sublabel}</div>{/if}
|
|
46
40
|
</td>
|
|
47
|
-
<td class="
|
|
48
|
-
<div class="
|
|
41
|
+
<td class="w-1/3 py-1.5 pr-3" aria-hidden="true">
|
|
42
|
+
<div class="h-1.5 w-full overflow-hidden rounded-sm bg-row-active">
|
|
49
43
|
<div
|
|
50
|
-
class="
|
|
44
|
+
class="h-full rounded-sm"
|
|
51
45
|
style="width:{((item.value / resolvedMax) * 100).toFixed(1)}%;background:{item.color ?? '#006FFF'}"
|
|
52
46
|
></div>
|
|
53
47
|
</div>
|
|
54
48
|
</td>
|
|
55
|
-
<td class="
|
|
56
|
-
{item.value.toLocaleString()}{#if unit}<span class="
|
|
49
|
+
<td class="py-1.5 text-right font-medium text-text-1 tabular-nums">
|
|
50
|
+
{item.value.toLocaleString()}{#if unit}<span class="ml-1 text-11 text-text-4"> {unit}</span>{/if}
|
|
57
51
|
</td>
|
|
58
52
|
</tr>
|
|
59
53
|
{/each}
|