@w5-ui/svelte 0.2.1 → 0.3.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/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 +91 -16
- 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 +108 -27
- 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 +35 -0
|
@@ -5,6 +5,21 @@
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
let { size = 'md', label = 'Loading' }: Props = $props();
|
|
8
|
+
|
|
9
|
+
// Pre-compose so Tailwind's scanner picks up every variant. Tailwind's
|
|
10
|
+
// `animate-spin` is `animation: spin 1s linear infinite` (rotate 360deg);
|
|
11
|
+
// dashboard.css used 0.65s — we mirror that with arbitrary-value
|
|
12
|
+
// `[animation-duration:0.65s]` to stay pixel-equivalent.
|
|
13
|
+
const SIZE: Record<NonNullable<Props['size']>, string> = {
|
|
14
|
+
sm: 'h-2.5 w-2.5 border-[1.5px]',
|
|
15
|
+
md: 'h-3.5 w-3.5 border-2',
|
|
16
|
+
lg: 'h-5 w-5 border-2',
|
|
17
|
+
};
|
|
8
18
|
</script>
|
|
9
19
|
|
|
10
|
-
<span
|
|
20
|
+
<span
|
|
21
|
+
role="status"
|
|
22
|
+
aria-label={label}
|
|
23
|
+
class="inline-block shrink-0 animate-spin rounded-full border-border-3 border-t-current [animation-duration:0.65s] motion-reduce:[animation-duration:10s]
|
|
24
|
+
{SIZE[size]}"
|
|
25
|
+
></span>
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
<script module lang="ts">
|
|
2
2
|
let counter = 0;
|
|
3
|
-
export interface SplitButtonItem {
|
|
3
|
+
export interface SplitButtonItem {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
4
8
|
</script>
|
|
5
9
|
|
|
6
10
|
<script lang="ts">
|
|
@@ -70,24 +74,34 @@
|
|
|
70
74
|
|
|
71
75
|
onMount(() => document.addEventListener('mousedown', handleOutside));
|
|
72
76
|
onDestroy(() => document.removeEventListener('mousedown', handleOutside));
|
|
77
|
+
|
|
78
|
+
// Pre-composed variant chrome so Tailwind's static scanner sees the literals.
|
|
79
|
+
const VARIANT: Record<NonNullable<Props['variant']>, string> = {
|
|
80
|
+
primary: 'bg-brand-05 text-white hover:bg-brand-06 border-brand-05',
|
|
81
|
+
ghost: 'bg-transparent text-text-2 border-border-2 hover:bg-row-hover hover:text-text-1',
|
|
82
|
+
danger: 'bg-transparent text-status-danger border-status-danger/30',
|
|
83
|
+
};
|
|
84
|
+
// Shared button base — same metrics as the migrated Button.
|
|
85
|
+
const BTN_BASE =
|
|
86
|
+
'inline-flex h-[30px] cursor-pointer items-center gap-1.5 whitespace-nowrap border px-3 text-13 font-medium transition-all duration-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05 disabled:cursor-not-allowed disabled:opacity-55';
|
|
73
87
|
</script>
|
|
74
88
|
|
|
75
|
-
<div class="
|
|
89
|
+
<div class="relative inline-flex items-stretch" bind:this={wrapperEl}>
|
|
76
90
|
<button
|
|
77
91
|
type="button"
|
|
78
|
-
class="btn btn-{variant} split-btn-primary"
|
|
79
92
|
{disabled}
|
|
93
|
+
class="{BTN_BASE} {VARIANT[variant]} rounded-l-md border-r-0 rounded-r-none"
|
|
80
94
|
onclick={() => onprimary?.()}
|
|
81
95
|
>{label}</button>
|
|
82
96
|
<button
|
|
83
97
|
bind:this={caretEl}
|
|
84
98
|
type="button"
|
|
85
|
-
class="btn btn-{variant} split-btn-caret"
|
|
86
99
|
{disabled}
|
|
87
|
-
aria-label=
|
|
100
|
+
aria-label={`${label} options`}
|
|
88
101
|
aria-haspopup="menu"
|
|
89
102
|
aria-expanded={open}
|
|
90
103
|
aria-controls={open ? menuId : undefined}
|
|
104
|
+
class="{BTN_BASE} {VARIANT[variant]} w-[26px] shrink-0 justify-center rounded-r-md rounded-l-none px-0 border-l border-l-white/15"
|
|
91
105
|
onclick={toggleMenu}
|
|
92
106
|
onkeydown={onKeyDown}
|
|
93
107
|
>
|
|
@@ -96,14 +110,19 @@
|
|
|
96
110
|
</svg>
|
|
97
111
|
</button>
|
|
98
112
|
{#if open}
|
|
99
|
-
<ul
|
|
113
|
+
<ul
|
|
114
|
+
id={menuId}
|
|
115
|
+
role="menu"
|
|
116
|
+
aria-label={`${label} options`}
|
|
117
|
+
class="absolute right-0 top-[calc(100%+4px)] z-[9100] m-0 min-w-[148px] list-none rounded-lg border border-border-3 bg-bg-2 p-1 shadow-[0_8px_24px_rgba(0,0,0,0.5)]"
|
|
118
|
+
>
|
|
100
119
|
{#each items as item, idx (item.id)}
|
|
101
120
|
<li
|
|
102
121
|
role="menuitem"
|
|
103
|
-
tabindex=
|
|
122
|
+
tabindex={-1}
|
|
104
123
|
aria-disabled={item.disabled}
|
|
105
124
|
data-active={idx === activeIdx ? 'true' : undefined}
|
|
106
|
-
class="
|
|
125
|
+
class="flex cursor-pointer items-center whitespace-nowrap rounded-[5px] px-2.5 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 aria-disabled:cursor-not-allowed aria-disabled:opacity-40"
|
|
107
126
|
onmouseenter={() => { activeIdx = idx; }}
|
|
108
127
|
onmousedown={(e) => { e.preventDefault(); (() => { if (!item.disabled) activate(item.id); })(); }}
|
|
109
128
|
>{item.label}</li>
|
|
@@ -14,9 +14,8 @@
|
|
|
14
14
|
showLabel = 'Show more',
|
|
15
15
|
hideLabel = 'Show less',
|
|
16
16
|
class: klass = '',
|
|
17
|
-
children
|
|
17
|
+
children,
|
|
18
18
|
}: Props = $props();
|
|
19
|
-
|
|
20
19
|
|
|
21
20
|
let expanded = $state(false);
|
|
22
21
|
let clipped = $state(false);
|
|
@@ -27,8 +26,8 @@
|
|
|
27
26
|
});
|
|
28
27
|
</script>
|
|
29
28
|
|
|
30
|
-
<div class="
|
|
31
|
-
<div class="
|
|
29
|
+
<div class="flex flex-col {klass}">
|
|
30
|
+
<div class="relative">
|
|
32
31
|
<div
|
|
33
32
|
bind:this={bodyEl}
|
|
34
33
|
style={!expanded && clipped ? `max-height:${maxHeight}px;overflow:hidden` : undefined}
|
|
@@ -36,14 +35,17 @@
|
|
|
36
35
|
{@render children?.()}
|
|
37
36
|
</div>
|
|
38
37
|
{#if !expanded && clipped}
|
|
39
|
-
<div
|
|
38
|
+
<div
|
|
39
|
+
class="pointer-events-none absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-b from-transparent to-[#1a1d23]"
|
|
40
|
+
aria-hidden="true"
|
|
41
|
+
></div>
|
|
40
42
|
{/if}
|
|
41
43
|
</div>
|
|
42
44
|
{#if clipped}
|
|
43
45
|
<button
|
|
44
46
|
type="button"
|
|
45
|
-
class="spoiler-toggle"
|
|
46
47
|
aria-expanded={expanded}
|
|
48
|
+
class="mt-1 inline-flex items-center border-0 bg-transparent px-0 py-1 text-12 leading-none text-brand-05 cursor-pointer hover:text-status-info focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
|
|
47
49
|
onclick={() => (expanded = !expanded)}
|
|
48
50
|
>
|
|
49
51
|
{expanded ? hideLabel : showLabel}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
class?: string;
|
|
4
|
+
children?: import('svelte').Snippet;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
let { class: className = '', children }: Props = $props();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<!--
|
|
11
|
+
Visually-hidden text that stays available to assistive tech. Renders a
|
|
12
|
+
<span class="sr-only"> so the .sr-only rule in @w5-ui/tokens/dashboard.css
|
|
13
|
+
clips it from the visual layout while keeping it announceable by screen
|
|
14
|
+
readers. Use it to name regions that the design intentionally leaves
|
|
15
|
+
unlabelled, the way the reference Tables recipe wires
|
|
16
|
+
<caption class="sr-only"> onto every <table> (WCAG 1.3.1). Typed equivalent
|
|
17
|
+
of <span class="sr-only"> (apply the class directly to a <caption> /
|
|
18
|
+
<label> when the whole element is the hidden text, or wrap an inline
|
|
19
|
+
phrase with <SrOnly> when it sits alongside visible markup).
|
|
20
|
+
-->
|
|
21
|
+
<span class="sr-only {className}">
|
|
22
|
+
{@render children?.()}
|
|
23
|
+
</span>
|
|
@@ -18,25 +18,33 @@
|
|
|
18
18
|
segments,
|
|
19
19
|
total = undefined,
|
|
20
20
|
ariaLabel = undefined,
|
|
21
|
-
showLegend = true
|
|
21
|
+
showLegend = true,
|
|
22
22
|
}: Props = $props();
|
|
23
23
|
let sum = $derived(total ?? segments.reduce((a, s) => a + s.value, 0));
|
|
24
|
-
let text = $derived(
|
|
24
|
+
let text = $derived(
|
|
25
|
+
ariaLabel ?? segments.map((s) => `${s.label} ${Math.round((s.value / sum) * 100)}%`).join(', '),
|
|
26
|
+
);
|
|
25
27
|
</script>
|
|
26
28
|
|
|
27
|
-
<div class="
|
|
28
|
-
<div
|
|
29
|
+
<div class="flex w-full flex-col gap-2" role="img" aria-label={text}>
|
|
30
|
+
<div
|
|
31
|
+
class="flex h-2 w-full overflow-hidden rounded-sm bg-row-active"
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
>
|
|
29
34
|
{#each segments as { label, value, color }}
|
|
30
|
-
<div
|
|
35
|
+
<div
|
|
36
|
+
class="h-full first:rounded-l-sm last:rounded-r-sm"
|
|
37
|
+
style="width:{((value / sum) * 100).toFixed(2)}%;background:{color}"
|
|
38
|
+
></div>
|
|
31
39
|
{/each}
|
|
32
40
|
</div>
|
|
33
41
|
{#if showLegend}
|
|
34
|
-
<div class="
|
|
42
|
+
<div class="flex flex-wrap items-center gap-3" aria-hidden="true">
|
|
35
43
|
{#each segments as { label, value, color }}
|
|
36
|
-
<span class="
|
|
37
|
-
<span class="
|
|
44
|
+
<span class="inline-flex items-center gap-1.5 text-12 text-text-2">
|
|
45
|
+
<span class="inline-block h-2 w-2 shrink-0 rounded-full" style="background:{color}"></span>
|
|
38
46
|
{label}
|
|
39
|
-
<span class="
|
|
47
|
+
<span class="text-text-4 tabular-nums">{Math.round((value / sum) * 100)}%</span>
|
|
40
48
|
</span>
|
|
41
49
|
{/each}
|
|
42
50
|
</div>
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
max = 5,
|
|
19
19
|
readOnly = false,
|
|
20
20
|
size = 'md',
|
|
21
|
-
onChange = undefined
|
|
21
|
+
onChange = undefined,
|
|
22
22
|
}: Props = $props();
|
|
23
23
|
|
|
24
24
|
const gid = `dash-ui-sr-${++counter}`;
|
|
@@ -28,31 +28,48 @@
|
|
|
28
28
|
value = star;
|
|
29
29
|
onChange?.(star);
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
const STAR_SIZE: Record<NonNullable<Props['size']>, string> = {
|
|
33
|
+
sm: 'h-[15px] w-[15px]',
|
|
34
|
+
md: 'h-5 w-5',
|
|
35
|
+
};
|
|
31
36
|
</script>
|
|
32
37
|
|
|
33
38
|
{#if readOnly}
|
|
34
39
|
<span
|
|
35
|
-
class="
|
|
40
|
+
class="m-0 inline-flex items-center gap-0.5 border-0 bg-transparent p-0"
|
|
36
41
|
role="img"
|
|
37
|
-
aria-label=
|
|
42
|
+
aria-label={`${value} out of ${max} stars`}
|
|
38
43
|
>
|
|
39
44
|
{#each Array(max) as _, i}
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
{@const on = i < value}
|
|
46
|
+
<span
|
|
47
|
+
aria-hidden="true"
|
|
48
|
+
class="flex items-center {on ? 'text-status-warning' : 'text-white/[0.18]'}"
|
|
49
|
+
>
|
|
50
|
+
<svg
|
|
51
|
+
class={STAR_SIZE[size]}
|
|
52
|
+
viewBox="0 0 24 24"
|
|
53
|
+
fill={on ? 'currentColor' : 'none'}
|
|
54
|
+
stroke="currentColor"
|
|
55
|
+
stroke-width="1.5"
|
|
56
|
+
aria-hidden="true"
|
|
57
|
+
focusable="false"
|
|
58
|
+
>
|
|
42
59
|
<polygon points="12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02 12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26" />
|
|
43
60
|
</svg>
|
|
44
61
|
</span>
|
|
45
62
|
{/each}
|
|
46
63
|
</span>
|
|
47
64
|
{:else}
|
|
48
|
-
<fieldset class="
|
|
65
|
+
<fieldset class="m-0 inline-flex items-center gap-0.5 border-0 bg-transparent p-0">
|
|
49
66
|
<legend class="sr-only">{label}</legend>
|
|
50
67
|
{#each Array(max) as _, i}
|
|
51
68
|
{@const star = i + 1}
|
|
52
69
|
{@const on = star <= (hovered || value)}
|
|
53
70
|
<label
|
|
54
71
|
for="{gid}-{star}"
|
|
55
|
-
class="star-
|
|
72
|
+
class="group/star relative flex cursor-pointer items-center rounded-sm p-px transition-colors duration-100 {on ? 'text-status-warning' : 'text-white/[0.18]'} has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-brand-05"
|
|
56
73
|
onmouseenter={() => (hovered = star)}
|
|
57
74
|
onmouseleave={() => (hovered = 0)}
|
|
58
75
|
>
|
|
@@ -62,11 +79,19 @@
|
|
|
62
79
|
name={gid}
|
|
63
80
|
value={String(star)}
|
|
64
81
|
checked={value === star}
|
|
65
|
-
class="
|
|
66
|
-
aria-label=
|
|
82
|
+
class="sr-only"
|
|
83
|
+
aria-label={`${star} out of ${max}`}
|
|
67
84
|
onchange={() => pick(star)}
|
|
68
85
|
/>
|
|
69
|
-
<svg
|
|
86
|
+
<svg
|
|
87
|
+
class={STAR_SIZE[size]}
|
|
88
|
+
viewBox="0 0 24 24"
|
|
89
|
+
fill={on ? 'currentColor' : 'none'}
|
|
90
|
+
stroke="currentColor"
|
|
91
|
+
stroke-width="1.5"
|
|
92
|
+
aria-hidden="true"
|
|
93
|
+
focusable="false"
|
|
94
|
+
>
|
|
70
95
|
<polygon points="12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02 12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26" />
|
|
71
96
|
</svg>
|
|
72
97
|
</label>
|
|
@@ -29,18 +29,32 @@
|
|
|
29
29
|
}: Props = $props();
|
|
30
30
|
|
|
31
31
|
let deltaClass = $derived(
|
|
32
|
-
deltaDir === 'up'
|
|
32
|
+
deltaDir === 'up'
|
|
33
|
+
? 'text-status-success'
|
|
34
|
+
: deltaDir === 'down'
|
|
35
|
+
? 'text-status-danger'
|
|
36
|
+
: '',
|
|
33
37
|
);
|
|
34
38
|
</script>
|
|
35
39
|
|
|
36
40
|
<Card {span}>
|
|
41
|
+
<!--
|
|
42
|
+
`.card h3` rules in dashboard.css still target this <h3> for now;
|
|
43
|
+
leaving them there until every Card consumer migrates its heading
|
|
44
|
+
too. Stat's value + submeta are Tailwind utilities below.
|
|
45
|
+
-->
|
|
37
46
|
<h3>{label}</h3>
|
|
38
|
-
<div
|
|
39
|
-
|
|
47
|
+
<div
|
|
48
|
+
class="text-[28px] font-semibold leading-[1.05] tracking-[-0.01em] tabular-nums text-text-1"
|
|
49
|
+
style={color ? `color:${color};` : ''}
|
|
50
|
+
>
|
|
51
|
+
{#if valueSlot}{@render valueSlot()}{:else}{value}{/if}{#if unit}<span
|
|
52
|
+
class="ml-1 text-13 font-normal text-text-4"
|
|
53
|
+
>{unit}</span>{/if}
|
|
40
54
|
</div>
|
|
41
55
|
{#if sub || delta}
|
|
42
|
-
<div class="
|
|
43
|
-
{#if sub}{sub}{/if}{#if sub && delta}
|
|
56
|
+
<div class="text-[11px] tabular-nums text-text-4">
|
|
57
|
+
{#if sub}{sub}{/if}{#if sub && delta} \u00b7 {/if}{#if delta}<span class="sr-only">Trend: </span><span class={deltaClass}>{delta}</span>{/if}
|
|
44
58
|
</div>
|
|
45
59
|
{/if}
|
|
46
60
|
</Card>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
class?: string;
|
|
4
|
+
children?: import('svelte').Snippet;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
let { class: className = '', children }: Props = $props();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<!--
|
|
11
|
+
Hero-number row inside a <Card>: the big tabular-nums value (optionally
|
|
12
|
+
followed by a trailing <Unit>). Renders a <div class="stat"> so the .stat
|
|
13
|
+
rule in @w5-ui/tokens/dashboard.css sets the canonical 30 px size, semibold
|
|
14
|
+
weight, tabular figures, and primary text colour. Stops userland reaching
|
|
15
|
+
for inline font-size / font-weight overrides on bare numbers. Pair with
|
|
16
|
+
<Unit> for the suffix label and <Submeta> for the muted line underneath;
|
|
17
|
+
<Stat> already wires all three internally, so reach for <StatNumber> when
|
|
18
|
+
you compose .stat by hand (e.g. number above a chart inside a <Card>).
|
|
19
|
+
-->
|
|
20
|
+
<div class="stat {className}">
|
|
21
|
+
{@render children?.()}
|
|
22
|
+
</div>
|
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
let { color, text, textColor = undefined }: Props = $props();
|
|
9
9
|
</script>
|
|
10
10
|
|
|
11
|
-
<span
|
|
12
|
-
|
|
11
|
+
<span
|
|
12
|
+
class="inline-flex items-center gap-1.5 text-12"
|
|
13
|
+
style="color:{textColor ?? color};"
|
|
14
|
+
>
|
|
15
|
+
<span
|
|
16
|
+
class="h-1.5 w-1.5 rounded-full"
|
|
17
|
+
aria-hidden="true"
|
|
18
|
+
style="background:{color};"
|
|
19
|
+
></span>
|
|
13
20
|
{text}
|
|
14
21
|
</span>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export type StatusRingStatus = 'ok' | 'warn' | 'danger' | 'neutral';
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
interface Props {
|
|
7
|
+
class?: string;
|
|
8
|
+
style?: string;
|
|
9
|
+
/** Health colour of the ring. Defaults to `'ok'` (success green). */
|
|
10
|
+
status?: StatusRingStatus;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { class: className = '', style = '', status = 'ok' }: Props = $props();
|
|
14
|
+
|
|
15
|
+
const MODIFIER: Record<StatusRingStatus, string> = {
|
|
16
|
+
ok: '',
|
|
17
|
+
warn: 'status-ring--warn',
|
|
18
|
+
danger: 'status-ring--danger',
|
|
19
|
+
neutral: 'status-ring--neutral',
|
|
20
|
+
};
|
|
21
|
+
let modifier = $derived(MODIFIER[status]);
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<!--
|
|
25
|
+
Pulsing status indicator: a soft translucent ring around a solid coloured
|
|
26
|
+
inner dot. Renders <span aria-hidden class="status-ring"> so the .status-ring
|
|
27
|
+
rule in @w5-ui/tokens/dashboard.css paints the canonical 18 px ring + 8 px
|
|
28
|
+
inner dot without userland having to hand-roll the two layers. The `status`
|
|
29
|
+
prop swaps the ring + dot colour: 'ok' (default, success green), 'warn'
|
|
30
|
+
(amber), 'danger' (red), 'neutral' (grey). Used by <Topbar> next to the site
|
|
31
|
+
name to signal site health; also suitable for tunnel / session / device
|
|
32
|
+
tiles where a small live-state dot sits at the leading edge of a row.
|
|
33
|
+
Decorative — the typed wrapper sets aria-hidden; pair with a label so
|
|
34
|
+
screen readers announce the state.
|
|
35
|
+
-->
|
|
36
|
+
<span aria-hidden="true" class="status-ring {modifier} {className}" {style}></span>
|
|
@@ -13,19 +13,41 @@
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
let { steps, active, class: className = '' }: Props = $props();
|
|
16
|
-
|
|
16
|
+
let activeIdx = $derived(steps.findIndex((s) => s.id === active));
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
// Per-state ramp:
|
|
19
|
+
// - num circle: ring + bg + text colour
|
|
20
|
+
// - label: colour + weight
|
|
21
|
+
// - connector line (::before via Tailwind arbitrary): bg colour
|
|
22
|
+
const NUM: Record<'active' | 'done' | 'upcoming', string> = {
|
|
23
|
+
active: 'border-brand-05 bg-brand-05 text-white',
|
|
24
|
+
done: 'border-brand-05 bg-brand-05/[0.18] text-status-info',
|
|
25
|
+
upcoming: 'border-bg-3 bg-bg-1 text-text-4',
|
|
26
|
+
};
|
|
27
|
+
const LABEL: Record<'active' | 'done' | 'upcoming', string> = {
|
|
28
|
+
active: 'text-text-1 font-medium',
|
|
29
|
+
done: 'text-status-info',
|
|
30
|
+
upcoming: 'text-text-4',
|
|
31
|
+
};
|
|
32
|
+
// Connector tail to the left, drawn from item to item.
|
|
33
|
+
const TAIL: Record<'active' | 'done' | 'upcoming', string> = {
|
|
34
|
+
active: 'before:bg-brand-05',
|
|
35
|
+
done: 'before:bg-brand-05',
|
|
36
|
+
upcoming: 'before:bg-bg-3',
|
|
37
|
+
};
|
|
19
38
|
</script>
|
|
20
39
|
|
|
21
|
-
<nav aria-label="Progress" class="
|
|
22
|
-
<ol class="
|
|
40
|
+
<nav aria-label="Progress" class="w-full {className}">
|
|
41
|
+
<ol class="m-0 flex list-none items-start p-0">
|
|
23
42
|
{#each steps as step, idx}
|
|
24
43
|
{@const isActive = step.id === active}
|
|
25
44
|
{@const isDone = idx < activeIdx}
|
|
26
|
-
{@const state = isActive ? 'active' : isDone ? 'done' : 'upcoming'}
|
|
27
|
-
<li class="
|
|
28
|
-
<span
|
|
45
|
+
{@const state = (isActive ? 'active' : isDone ? 'done' : 'upcoming') as 'active' | 'done' | 'upcoming'}
|
|
46
|
+
<li class="relative flex flex-1 flex-col items-center gap-1.5 [&:not(:first-child)]:before:absolute [&:not(:first-child)]:before:left-[-50%] [&:not(:first-child)]:before:right-1/2 [&:not(:first-child)]:before:top-[13px] [&:not(:first-child)]:before:h-px [&:not(:first-child)]:before:content-[''] {TAIL[state]}">
|
|
47
|
+
<span
|
|
48
|
+
class="relative z-10 flex h-[26px] w-[26px] shrink-0 items-center justify-center rounded-full border-[1.5px] text-11 font-semibold {NUM[state]}"
|
|
49
|
+
aria-hidden="true"
|
|
50
|
+
>
|
|
29
51
|
{#if isDone}
|
|
30
52
|
<svg viewBox="0 0 12 12" width="12" height="12" fill="none">
|
|
31
53
|
<path d="M2 6l3 3 5-5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
@@ -35,7 +57,7 @@
|
|
|
35
57
|
{/if}
|
|
36
58
|
</span>
|
|
37
59
|
{#if isDone}<span class="sr-only">completed: </span>{/if}
|
|
38
|
-
<span class="
|
|
60
|
+
<span class="text-center text-11 leading-[1.3] {LABEL[state]}" aria-current={isActive ? 'step' : undefined}>
|
|
39
61
|
{step.label}
|
|
40
62
|
</span>
|
|
41
63
|
</li>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
class?: string;
|
|
4
|
+
children?: import('svelte').Snippet;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
let { class: className = '', children }: Props = $props();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<!--
|
|
11
|
+
Secondary one-line caption inside a <Card> tile: the muted row that sits
|
|
12
|
+
under the headline .stat number and carries supporting metrics, deltas, or
|
|
13
|
+
status. Typed equivalent of `<div class="submeta">` from
|
|
14
|
+
@w5-ui/tokens/dashboard.css. Pair with `.delta-up` / `.delta-down` spans
|
|
15
|
+
for trend text so screen readers announce the direction.
|
|
16
|
+
-->
|
|
17
|
+
<div class="submeta {className}">
|
|
18
|
+
{@render children?.()}
|
|
19
|
+
</div>
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
</script>
|
|
14
14
|
|
|
15
15
|
<script lang="ts">
|
|
16
|
-
|
|
17
16
|
interface Props {
|
|
18
17
|
ports?: SwitchPort[];
|
|
19
18
|
columns?: number;
|
|
@@ -21,29 +20,43 @@
|
|
|
21
20
|
onportclick?: (payload: number) => void;
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
let {
|
|
23
|
+
let {
|
|
24
|
+
ports = [],
|
|
25
|
+
columns = 12,
|
|
26
|
+
ariaLabel = 'Switch port panel',
|
|
25
27
|
onportclick,
|
|
26
28
|
}: Props = $props();
|
|
29
|
+
|
|
30
|
+
const STATUS_BTN: Record<SwitchPortStatus, string> = {
|
|
31
|
+
up: 'border-status-success/40 bg-status-success/[0.08] text-status-success hover:bg-status-success/15',
|
|
32
|
+
poe: 'border-status-warning/40 bg-status-warning/[0.10] text-status-warning hover:bg-status-warning/20',
|
|
33
|
+
down: 'border-border-1 bg-divider text-text-4 hover:bg-row-hover',
|
|
34
|
+
};
|
|
27
35
|
</script>
|
|
28
36
|
|
|
29
|
-
<ul
|
|
37
|
+
<ul
|
|
38
|
+
class="m-0 grid list-none gap-1 p-0"
|
|
39
|
+
role="list"
|
|
40
|
+
aria-label={ariaLabel}
|
|
41
|
+
style="grid-template-columns: repeat({columns}, 1fr);"
|
|
42
|
+
>
|
|
30
43
|
{#each ports as port, i}
|
|
31
44
|
{@const n = i + 1}
|
|
32
45
|
{@const desc = `Port ${n}: ${STATUS_LABEL[port.status]}${port.speed ? ` · ${port.speed}` : ''}${port.label ? ` · ${port.label}` : ''}`}
|
|
33
46
|
<li>
|
|
34
47
|
<button
|
|
35
48
|
type="button"
|
|
36
|
-
class="spg__btn"
|
|
37
49
|
data-status={port.status}
|
|
38
50
|
aria-label={desc}
|
|
51
|
+
class="flex aspect-square w-full cursor-pointer flex-col items-center justify-center gap-0.5 rounded border p-1 text-11 font-medium transition-colors duration-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05 {STATUS_BTN[port.status]}"
|
|
39
52
|
onclick={() => onportclick?.(i)}
|
|
40
53
|
>
|
|
41
|
-
<span class="
|
|
54
|
+
<span class="font-bold leading-none">{n}</span>
|
|
42
55
|
{#if port.speed}
|
|
43
|
-
<span class="
|
|
56
|
+
<span class="text-[9px] leading-none opacity-75">{port.speed}</span>
|
|
44
57
|
{/if}
|
|
45
58
|
{#if port.status === 'poe'}
|
|
46
|
-
<span class="
|
|
59
|
+
<span class="text-[9px] font-bold leading-none" aria-hidden="true">PoE</span>
|
|
47
60
|
{/if}
|
|
48
61
|
</button>
|
|
49
62
|
</li>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
class?: string;
|
|
4
|
+
children?: import('svelte').Snippet;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
let { class: className = '', children }: Props = $props();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<!--
|
|
11
|
+
Sibling row that sits directly under a <TableShell> (or
|
|
12
|
+
<div class="table-shell">) and carries the rows-per-page control on the
|
|
13
|
+
left and the <Pagination> nav on the right. Renders the canonical
|
|
14
|
+
<div class="table-pager"> markup so the flex / space-between / 12px top
|
|
15
|
+
margin from @w5-ui/tokens/dashboard.css applies and userland drops the
|
|
16
|
+
inline flex / space-between wrapper that every reference page used to ship.
|
|
17
|
+
Render as a sibling of <TableShell>, never inside it, so focus rings on the
|
|
18
|
+
pager buttons aren't clipped.
|
|
19
|
+
-->
|
|
20
|
+
<div class="table-pager {className}">
|
|
21
|
+
{@render children?.()}
|
|
22
|
+
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
class?: string;
|
|
4
|
+
children?: import('svelte').Snippet;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
let { class: className = '', children }: Props = $props();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<!--
|
|
11
|
+
Rounded, bordered wrapper for any full-width <table> on a page. Carries the
|
|
12
|
+
8px radius, --neutral-09 background, single 1px border and overflow: hidden
|
|
13
|
+
clip. Typed equivalent of `<div class="table-shell">` from
|
|
14
|
+
@w5-ui/tokens/dashboard.css. Render <Pagination> as a sibling below, not
|
|
15
|
+
inside, so focus rings aren't clipped.
|
|
16
|
+
-->
|
|
17
|
+
<div class="table-shell {className}">
|
|
18
|
+
{@render children?.()}
|
|
19
|
+
</div>
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<script lang="ts">
|
|
6
|
-
|
|
7
6
|
interface Props {
|
|
8
7
|
items?: TabItem[];
|
|
9
8
|
active: string;
|
|
@@ -11,7 +10,10 @@
|
|
|
11
10
|
onchange?: (payload: string) => void;
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
let {
|
|
13
|
+
let {
|
|
14
|
+
items = [],
|
|
15
|
+
active = $bindable(),
|
|
16
|
+
ariaLabel = undefined,
|
|
15
17
|
onchange,
|
|
16
18
|
}: Props = $props();
|
|
17
19
|
let buttons: (HTMLButtonElement | null)[] = $state([]);
|
|
@@ -32,12 +34,19 @@
|
|
|
32
34
|
}
|
|
33
35
|
</script>
|
|
34
36
|
|
|
35
|
-
<div
|
|
37
|
+
<div
|
|
38
|
+
class="flex gap-0 overflow-x-auto border-b border-border-1 px-6"
|
|
39
|
+
role="tablist"
|
|
40
|
+
aria-label={ariaLabel}
|
|
41
|
+
>
|
|
36
42
|
{#each items as t, i (t.id)}
|
|
37
43
|
<button
|
|
38
44
|
bind:this={buttons[i]}
|
|
39
45
|
type="button"
|
|
40
|
-
class="
|
|
46
|
+
class="-mb-px cursor-pointer select-none whitespace-nowrap border-0 border-b-2 bg-transparent px-3.5 py-[11px] text-13 leading-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-brand-05
|
|
47
|
+
{t.id === active
|
|
48
|
+
? 'border-b-brand-05 text-text-1'
|
|
49
|
+
: 'border-b-transparent text-text-3 hover:text-text-1'}"
|
|
41
50
|
role="tab"
|
|
42
51
|
id="tab-{t.id}"
|
|
43
52
|
aria-selected={t.id === active}
|
|
@@ -47,7 +56,9 @@
|
|
|
47
56
|
onkeydown={(e) => onKey(e, i)}
|
|
48
57
|
>
|
|
49
58
|
{t.label}
|
|
50
|
-
{#if t.badge !== undefined}
|
|
59
|
+
{#if t.badge !== undefined}
|
|
60
|
+
<span class="ml-1.5 tabular-nums text-text-4">{t.badge}</span>
|
|
61
|
+
{/if}
|
|
51
62
|
</button>
|
|
52
63
|
{/each}
|
|
53
64
|
</div>
|