@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
|
@@ -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
|
+
Inline monospace label for MAC addresses, IPs, SKUs, serial numbers and
|
|
12
|
+
other fixed-width identifiers that read inside a table cell or a property
|
|
13
|
+
row. Renders a <span class="mac"> so the .mac rule in
|
|
14
|
+
@w5-ui/tokens/dashboard.css applies the canonical JetBrains Mono family,
|
|
15
|
+
12 px size, and muted colour. Stops userland reaching for inline
|
|
16
|
+
font-family / font-size overrides on bare identifiers. Typed equivalent of
|
|
17
|
+
<span class="mac"> (apply the class directly to a <td> when the entire
|
|
18
|
+
cell is the identifier, or wrap the cell contents with <Mac> when the cell
|
|
19
|
+
carries other markup too).
|
|
20
|
+
-->
|
|
21
|
+
<span class="mac {className}">
|
|
22
|
+
{@render children?.()}
|
|
23
|
+
</span>
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
<script module lang="ts">
|
|
2
2
|
let counter = 0;
|
|
3
|
-
export interface MenubarItem {
|
|
4
|
-
|
|
3
|
+
export interface MenubarItem {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
separator?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface MenubarMenu {
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
items: MenubarItem[];
|
|
13
|
+
}
|
|
5
14
|
</script>
|
|
6
15
|
|
|
7
16
|
<script lang="ts">
|
|
@@ -27,9 +36,9 @@
|
|
|
27
36
|
let triggerEls: HTMLButtonElement[] = $state([]);
|
|
28
37
|
let rootEl = $state<HTMLDivElement | undefined>(undefined);
|
|
29
38
|
|
|
30
|
-
let currentEligible = $derived(
|
|
31
|
-
? menus[openIdx].items.filter((i) => !i.separator && !i.disabled)
|
|
32
|
-
|
|
39
|
+
let currentEligible = $derived(
|
|
40
|
+
openIdx !== null ? menus[openIdx].items.filter((i) => !i.separator && !i.disabled) : [],
|
|
41
|
+
);
|
|
33
42
|
|
|
34
43
|
function openMenu(idx: number, focusLast = false) {
|
|
35
44
|
const items = menus[idx].items.filter((i) => !i.separator && !i.disabled);
|
|
@@ -37,7 +46,9 @@
|
|
|
37
46
|
openIdx = idx;
|
|
38
47
|
}
|
|
39
48
|
|
|
40
|
-
function closeMenu() {
|
|
49
|
+
function closeMenu() {
|
|
50
|
+
openIdx = null;
|
|
51
|
+
}
|
|
41
52
|
|
|
42
53
|
function activate(menuId: string, itemId: string) {
|
|
43
54
|
onaction?.({ menuId, itemId });
|
|
@@ -93,13 +104,13 @@
|
|
|
93
104
|
bind:this={rootEl}
|
|
94
105
|
role="menubar"
|
|
95
106
|
aria-label={label}
|
|
96
|
-
class=
|
|
107
|
+
class="inline-flex items-center gap-0.5 rounded-md border border-border-1 bg-divider p-0.5 {klass}"
|
|
97
108
|
>
|
|
98
109
|
{#each menus as menu, idx (menu.id)}
|
|
99
110
|
{@const isOpen = openIdx === idx}
|
|
100
111
|
{@const menuId = `${uid}-menu-${idx}`}
|
|
101
112
|
{@const elig = menu.items.filter((i) => !i.separator && !i.disabled)}
|
|
102
|
-
<div class="
|
|
113
|
+
<div class="relative">
|
|
103
114
|
<button
|
|
104
115
|
bind:this={triggerEls[idx]}
|
|
105
116
|
type="button"
|
|
@@ -107,25 +118,31 @@
|
|
|
107
118
|
aria-haspopup="menu"
|
|
108
119
|
aria-expanded={isOpen}
|
|
109
120
|
aria-controls={isOpen ? menuId : undefined}
|
|
110
|
-
|
|
121
|
+
data-open={isOpen ? 'true' : undefined}
|
|
122
|
+
class="inline-flex cursor-pointer items-center rounded border-0 bg-transparent px-2.5 py-1.5 text-13 leading-none text-text-3 transition-[background-color,color] duration-100 hover:bg-row-active hover:text-text-1 focus-visible:outline-2 focus-visible:outline-offset-[1px] focus-visible:outline-brand-05 data-[open=true]:bg-row-active data-[open=true]:text-text-1"
|
|
111
123
|
onclick={() => { if (isOpen) closeMenu(); else openMenu(idx); }}
|
|
112
124
|
onkeydown={(e) => handleTriggerKeyDown(e, idx)}
|
|
113
125
|
>
|
|
114
126
|
{menu.label}
|
|
115
127
|
</button>
|
|
116
128
|
{#if isOpen}
|
|
117
|
-
<ul
|
|
129
|
+
<ul
|
|
130
|
+
id={menuId}
|
|
131
|
+
role="menu"
|
|
132
|
+
aria-label={menu.label}
|
|
133
|
+
class="absolute left-0 top-[calc(100%+2px)] z-[200] m-0 min-w-[180px] list-none rounded-md border border-border-3 bg-bg-2 p-1 shadow-[0_8px_24px_rgba(0,0,0,0.4)]"
|
|
134
|
+
>
|
|
118
135
|
{#each menu.items as item (item.id)}
|
|
119
136
|
{#if item.separator}
|
|
120
|
-
<li role="separator" class="
|
|
137
|
+
<li role="separator" aria-hidden="true" class="my-[3px] h-px list-none bg-row-active"></li>
|
|
121
138
|
{:else}
|
|
122
139
|
{@const eligIdx = elig.indexOf(item)}
|
|
123
140
|
<li
|
|
124
141
|
role="menuitem"
|
|
125
|
-
tabindex=
|
|
142
|
+
tabindex={-1}
|
|
126
143
|
aria-disabled={item.disabled}
|
|
127
144
|
data-active={eligIdx === activeItemIdx && !item.disabled ? 'true' : undefined}
|
|
128
|
-
class="
|
|
145
|
+
class="flex cursor-pointer select-none items-center whitespace-nowrap rounded px-2.5 py-1.5 text-13 text-text-3 transition-colors duration-75 hover:bg-row-active hover:text-text-1 data-[active=true]:bg-row-active data-[active=true]:text-text-1 aria-disabled:cursor-default aria-disabled:text-white/25 aria-disabled:pointer-events-none"
|
|
129
146
|
onmouseenter={() => { if (!item.disabled) activeItemIdx = eligIdx; }}
|
|
130
147
|
onmousedown={(e) => { e.preventDefault(); (() => { if (!item.disabled) activate(menu.id, item.id); })(); }}
|
|
131
148
|
>{item.label}</li>
|
|
@@ -72,8 +72,15 @@
|
|
|
72
72
|
|
|
73
73
|
<svelte:window onkeydown={onKeydown} />
|
|
74
74
|
|
|
75
|
+
<!--
|
|
76
|
+
Modal chrome matches dashboard.css literal values for cross-motif
|
|
77
|
+
consistency: backdrop is solid 55% black with 8px blur, modal panel is
|
|
78
|
+
bg #141415 (neutral-09) with rgba(255,255,255,0.1) border, header +
|
|
79
|
+
footer separators at rgba(255,255,255,0.06), footer surface at #0f0f10.
|
|
80
|
+
-->
|
|
75
81
|
<div
|
|
76
|
-
class="
|
|
82
|
+
class="fixed inset-0 z-[60] items-center justify-center bg-black/55 backdrop-blur-lg
|
|
83
|
+
{open ? 'flex' : 'hidden'}"
|
|
77
84
|
onmousedown={(e) => (downOnBackdrop = e.target === e.currentTarget)}
|
|
78
85
|
onclick={(e) => {
|
|
79
86
|
if (downOnBackdrop && e.target === e.currentTarget) open = false;
|
|
@@ -83,21 +90,23 @@
|
|
|
83
90
|
>
|
|
84
91
|
<div
|
|
85
92
|
bind:this={modalEl}
|
|
86
|
-
class="modal"
|
|
93
|
+
class="w-[520px] max-w-[90vw] overflow-hidden rounded-xl border border-border-2 bg-bg-1 shadow-modal"
|
|
87
94
|
role="dialog"
|
|
88
95
|
aria-modal="true"
|
|
89
96
|
aria-labelledby={titleId}
|
|
90
97
|
tabindex="-1"
|
|
91
98
|
>
|
|
92
|
-
<div class="
|
|
93
|
-
<h2 id={titleId}>{title}</h2>
|
|
99
|
+
<div class="flex items-center justify-between border-b border-border-1 px-5 py-4">
|
|
100
|
+
<h2 id={titleId} class="m-0 text-16 font-semibold">{title}</h2>
|
|
94
101
|
<IconButton title="Close" onclick={() => (open = false)}>
|
|
95
102
|
<CloseIcon />
|
|
96
103
|
</IconButton>
|
|
97
104
|
</div>
|
|
98
|
-
<div class="
|
|
105
|
+
<div class="px-5 py-4">{@render children?.()}</div>
|
|
99
106
|
{#if footer}
|
|
100
|
-
<div
|
|
107
|
+
<div
|
|
108
|
+
class="flex justify-end gap-2 border-t border-border-1 bg-bg-0 px-5 py-3.5"
|
|
109
|
+
>{@render footer?.()}</div>
|
|
101
110
|
{/if}
|
|
102
111
|
</div>
|
|
103
112
|
</div>
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
Inline monospace identifier for MAC addresses, IPs, hex IDs, ports,
|
|
12
|
+
firmware versions, config keys, and other machine-shaped values that
|
|
13
|
+
read inside body copy or a property row. Renders a
|
|
14
|
+
<span class="dash-ui-mono"> so the .dash-ui-mono rule in
|
|
15
|
+
@w5-ui/tokens/tokens.css applies the canonical JetBrains Mono family,
|
|
16
|
+
13 px size, regular weight, 1.4 line-height, and --text-2 colour. The
|
|
17
|
+
class also enables font-variant-numeric: tabular-nums so a column of
|
|
18
|
+
MACs / IPs / versions aligns vertically. Stops userland reaching for
|
|
19
|
+
bare font-family: monospace (which falls back to a serif Courier with
|
|
20
|
+
proportional digits) or one-off font-size overrides on identifiers.
|
|
21
|
+
Typed equivalent of <span class="dash-ui-mono"> (the userland-facing
|
|
22
|
+
semantic class documented in Foundations / Type). Prefer <Mono> for
|
|
23
|
+
new userland code; reach for <Mac> only when you need the dashboard's
|
|
24
|
+
denser 12 px .mac variant (reference Devices / Clients table cells).
|
|
25
|
+
-->
|
|
26
|
+
<span class="dash-ui-mono {className}">
|
|
27
|
+
{@render children?.()}
|
|
28
|
+
</span>
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
<script module lang="ts">
|
|
2
2
|
let counter = 0;
|
|
3
|
-
export interface MultiSelectOption {
|
|
3
|
+
export interface MultiSelectOption {
|
|
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-ms-${++counter}`;
|
|
33
35
|
let inputId = $derived(id ?? uid);
|
|
@@ -40,13 +42,15 @@
|
|
|
40
42
|
|
|
41
43
|
let selectedSet = $derived(new Set(value));
|
|
42
44
|
|
|
43
|
-
let filtered =
|
|
44
|
-
|
|
45
|
+
let filtered = $derived(
|
|
46
|
+
query.trim() === ''
|
|
45
47
|
? options
|
|
46
|
-
: options.filter((o) => o.label.toLowerCase().includes(query.toLowerCase()))
|
|
48
|
+
: options.filter((o) => o.label.toLowerCase().includes(query.toLowerCase())),
|
|
49
|
+
);
|
|
47
50
|
|
|
48
|
-
let activeOptionId =
|
|
49
|
-
|
|
51
|
+
let activeOptionId = $derived(
|
|
52
|
+
activeIdx >= 0 && filtered[activeIdx] ? `${listboxId}-opt-${activeIdx}` : undefined,
|
|
53
|
+
);
|
|
50
54
|
|
|
51
55
|
function toggle(optValue: string) {
|
|
52
56
|
const next = new Set(selectedSet);
|
|
@@ -88,22 +92,23 @@
|
|
|
88
92
|
onDestroy(() => document.removeEventListener('mousedown', handleOutside));
|
|
89
93
|
</script>
|
|
90
94
|
|
|
91
|
-
<div class="
|
|
95
|
+
<div class="relative block {className}" bind:this={wrapperEl}>
|
|
92
96
|
{#if label}<label for={inputId} class="sr-only">{label}</label>{/if}
|
|
93
97
|
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
94
98
|
<div
|
|
95
|
-
|
|
99
|
+
data-open={open ? 'true' : undefined}
|
|
100
|
+
class="relative flex min-h-[34px] cursor-text flex-wrap items-center gap-1 rounded-md border border-border-2 bg-bg-page py-[3px] pl-2 pr-9 transition-colors duration-100 hover:border-border-3 data-[open=true]:border-brand-05 data-[open=true]:shadow-[0_0_0_2px_rgba(0,111,255,0.2)]"
|
|
96
101
|
onclick={() => { if (!disabled) inputEl?.focus(); }}
|
|
97
102
|
>
|
|
98
103
|
{#each value as v (v)}
|
|
99
104
|
{@const opt = options.find((o) => o.value === v)}
|
|
100
105
|
{#if opt}
|
|
101
|
-
<span class="
|
|
102
|
-
<span class="
|
|
106
|
+
<span class="inline-flex h-6 items-center gap-1 whitespace-nowrap rounded-full border border-brand-05/25 bg-brand-05/[0.14] pl-2.5 pr-1 text-12 font-medium text-status-info">
|
|
107
|
+
<span class="leading-none">{opt.label}</span>
|
|
103
108
|
<button
|
|
104
109
|
type="button"
|
|
105
|
-
|
|
106
|
-
|
|
110
|
+
aria-label={`Remove ${opt.label} filter`}
|
|
111
|
+
class="inline-flex h-[18px] w-[18px] shrink-0 cursor-pointer items-center justify-center rounded-full border-0 bg-transparent p-0 text-status-info transition-colors duration-100 hover:bg-brand-05/25 hover:text-text-1 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-brand-05"
|
|
107
112
|
onclick={(e) => { e.stopPropagation(); (() => toggle(v))(); }}
|
|
108
113
|
>
|
|
109
114
|
<svg viewBox="0 0 10 10" width="10" height="10" fill="none" aria-hidden="true" focusable="false">
|
|
@@ -130,38 +135,53 @@
|
|
|
130
135
|
oninput={onInput}
|
|
131
136
|
onfocus={() => { if (!disabled) open = true; }}
|
|
132
137
|
onkeydown={onKeyDown}
|
|
133
|
-
class="
|
|
138
|
+
class="h-6 min-w-[80px] flex-1 border-0 bg-transparent px-1 text-13 leading-none text-text-1 outline-none placeholder:text-text-4"
|
|
134
139
|
/>
|
|
135
140
|
<button
|
|
136
141
|
type="button"
|
|
137
|
-
tabindex=
|
|
142
|
+
tabindex={-1}
|
|
138
143
|
aria-hidden="true"
|
|
139
|
-
class="
|
|
144
|
+
class="absolute right-2 flex cursor-pointer items-center border-0 bg-transparent p-0.5 leading-none text-text-4"
|
|
140
145
|
onclick={(e) => { e.stopPropagation(); (() => { if (open) { open = false; query = ''; } else { inputEl?.focus(); open = true; } })(); }}
|
|
141
146
|
>
|
|
142
|
-
<svg
|
|
147
|
+
<svg
|
|
148
|
+
viewBox="0 0 16 16"
|
|
149
|
+
aria-hidden="true"
|
|
150
|
+
focusable="false"
|
|
151
|
+
class="h-3.5 w-3.5 transition-transform duration-100 motion-reduce:transition-none {open ? 'rotate-180' : ''}"
|
|
152
|
+
>
|
|
143
153
|
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none" />
|
|
144
154
|
</svg>
|
|
145
155
|
</button>
|
|
146
156
|
</div>
|
|
147
157
|
{#if open}
|
|
148
|
-
<ul
|
|
158
|
+
<ul
|
|
159
|
+
id={listboxId}
|
|
160
|
+
role="listbox"
|
|
161
|
+
aria-label={label}
|
|
162
|
+
aria-multiselectable="true"
|
|
163
|
+
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)]"
|
|
164
|
+
>
|
|
149
165
|
{#if filtered.length === 0}
|
|
150
|
-
<li class="
|
|
166
|
+
<li class="list-none p-2.5 text-center text-13 text-text-4">No results</li>
|
|
151
167
|
{:else}
|
|
152
168
|
{#each filtered as opt, idx (opt.value)}
|
|
169
|
+
{@const selected = selectedSet.has(opt.value)}
|
|
153
170
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
154
171
|
<li
|
|
155
172
|
id="{listboxId}-opt-{idx}"
|
|
156
173
|
role="option"
|
|
157
|
-
aria-selected={
|
|
174
|
+
aria-selected={selected}
|
|
158
175
|
data-active={idx === activeIdx ? 'true' : undefined}
|
|
159
|
-
class="
|
|
176
|
+
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 data-[active=true]:bg-row-active data-[active=true]:text-text-1"
|
|
160
177
|
onmousedown={(e) => { e.preventDefault(); (() => toggle(opt.value))(); }}
|
|
161
178
|
onmouseenter={() => { activeIdx = idx; }}
|
|
162
179
|
>
|
|
163
|
-
<span
|
|
164
|
-
|
|
180
|
+
<span
|
|
181
|
+
aria-hidden="true"
|
|
182
|
+
class="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-sm border border-border-3 {selected ? 'border-brand-05 bg-brand-05 text-white' : 'text-transparent'}"
|
|
183
|
+
>
|
|
184
|
+
{#if selected}
|
|
165
185
|
<svg viewBox="0 0 12 12" width="12" height="12" fill="none" aria-hidden="true">
|
|
166
186
|
<path d="M2 6l3 3 5-5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
|
167
187
|
</svg>
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
Inline span that drops a phrase to the tertiary text colour
|
|
12
|
+
(--text-3). Renders <span class="t-muted"> so the .t-muted rule in
|
|
13
|
+
@w5-ui/tokens/dashboard.css applies the canonical muted colour used by
|
|
14
|
+
.submeta, .stat .unit and the .card h3 title chrome. Use for
|
|
15
|
+
"X active" counters next to an icon, caption-style metadata sitting
|
|
16
|
+
beside a primary label, or any sub-line that would otherwise tempt
|
|
17
|
+
userland to reach for a one-off color: #... override. Typed equivalent
|
|
18
|
+
of <span class="t-muted"> (the contrast pair is AA against every motif
|
|
19
|
+
background, verified for --text-3 over --depthBg-0/--depthBg-1/--depthBg-2
|
|
20
|
+
in both dark and light motifs).
|
|
21
|
+
-->
|
|
22
|
+
<span class="t-muted {className}">
|
|
23
|
+
{@render children?.()}
|
|
24
|
+
</span>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
class?: string;
|
|
4
|
+
/**
|
|
5
|
+
* Extra class for the inner .nc-thumb (e.g. "cam" for the camera
|
|
6
|
+
* gradient variant). Ignored when the thumb snippet is not supplied.
|
|
7
|
+
*/
|
|
8
|
+
thumbClass?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Square 28px avatar / initial that sits to the left of the name. Pass
|
|
11
|
+
* a snippet rendering a letter, emoji, or small SVG. Omit if the row
|
|
12
|
+
* has no leading affordance.
|
|
13
|
+
*/
|
|
14
|
+
thumb?: import('svelte').Snippet;
|
|
15
|
+
children?: import('svelte').Snippet;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let { class: className = '', thumbClass = '', thumb, children }: Props = $props();
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<!--
|
|
22
|
+
First-column row in a list / table: a 28px square thumbnail or initial
|
|
23
|
+
followed by the device / client / alert name. Renders the canonical
|
|
24
|
+
<div class="name-cell"><span class="nc-thumb">...</span>...</div> markup
|
|
25
|
+
so the .name-cell and .nc-thumb rules from @w5-ui/tokens/dashboard.css
|
|
26
|
+
apply (28px thumb, 10px gap, white name colour, JetBrains Mono initial).
|
|
27
|
+
Typed equivalent of the manual <div class="name-cell">... pattern used
|
|
28
|
+
across the Dashboard, Devices, Wi-Fi, and Alarms reference pages.
|
|
29
|
+
-->
|
|
30
|
+
<div class="name-cell {className}">
|
|
31
|
+
{#if thumb}
|
|
32
|
+
<span class="nc-thumb {thumbClass}">{@render thumb()}</span>
|
|
33
|
+
{/if}
|
|
34
|
+
{@render children?.()}
|
|
35
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
class?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
let { class: className = '' }: Props = $props();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<!--
|
|
10
|
+
Tiny red dot that sits on top of a bell / mail / inbox icon to flag unread
|
|
11
|
+
activity. Renders <span aria-hidden class="topbar-notif-dot"> so the
|
|
12
|
+
.topbar-notif-dot rule in @w5-ui/tokens/dashboard.css paints the canonical
|
|
13
|
+
6 px circle with the danger colour and a neutral-10 ring (the ring is what
|
|
14
|
+
keeps the dot legible over the deep topbar background). The dot is
|
|
15
|
+
presentational — wrap your bell <IconButton> and put the live count in its
|
|
16
|
+
aria-label (e.g. aria-label="Notifications, 3 new") so screen readers
|
|
17
|
+
announce the number while sighted users see the indicator. The hosting
|
|
18
|
+
button must be position: relative for the dot to anchor in the top-right
|
|
19
|
+
corner. Typed equivalent of <span aria-hidden class="topbar-notif-dot" />
|
|
20
|
+
for userland that slots a custom <Topbar actions> block and wants to keep
|
|
21
|
+
the unread indicator wired up without copying the class name.
|
|
22
|
+
-->
|
|
23
|
+
<span aria-hidden="true" class="topbar-notif-dot {className}"></span>
|
|
@@ -15,8 +15,6 @@
|
|
|
15
15
|
<script lang="ts">
|
|
16
16
|
import { onDestroy } from 'svelte';
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
18
|
interface Props {
|
|
21
19
|
open?: boolean;
|
|
22
20
|
notifications?: Notification[];
|
|
@@ -30,7 +28,7 @@
|
|
|
30
28
|
notifications = [],
|
|
31
29
|
onClose = () => {},
|
|
32
30
|
onMarkRead = undefined,
|
|
33
|
-
onMarkAllRead = undefined
|
|
31
|
+
onMarkAllRead = undefined,
|
|
34
32
|
}: Props = $props();
|
|
35
33
|
|
|
36
34
|
let filter: 'all' | NotifType = $state('all');
|
|
@@ -51,7 +49,9 @@
|
|
|
51
49
|
update: 'Update',
|
|
52
50
|
};
|
|
53
51
|
|
|
54
|
-
function setFilter(f: string) {
|
|
52
|
+
function setFilter(f: string) {
|
|
53
|
+
filter = f as 'all' | NotifType;
|
|
54
|
+
}
|
|
55
55
|
|
|
56
56
|
let unread = $derived(notifications.filter((n) => !n.read).length);
|
|
57
57
|
let filtered = $derived(filter === 'all' ? notifications : notifications.filter((n) => n.type === filter));
|
|
@@ -95,46 +95,56 @@
|
|
|
95
95
|
|
|
96
96
|
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
97
97
|
<div
|
|
98
|
-
|
|
98
|
+
data-open={open ? 'true' : undefined}
|
|
99
|
+
class="fixed inset-0 z-[9100] hidden bg-black/45 backdrop-blur-sm data-[open=true]:block"
|
|
99
100
|
onclick={onClose}
|
|
100
101
|
aria-hidden="true"
|
|
101
102
|
></div>
|
|
102
103
|
<div
|
|
103
104
|
bind:this={panelEl}
|
|
104
|
-
|
|
105
|
+
data-open={open ? 'true' : undefined}
|
|
106
|
+
class="fixed right-0 top-0 z-[9200] flex h-full w-[380px] max-w-[90vw] translate-x-full flex-col overflow-hidden border-l border-border-1 bg-bg-1 shadow-[0_0_40px_rgba(0,0,0,0.6)] transition-transform duration-200 data-[open=true]:translate-x-0 motion-reduce:transition-none"
|
|
105
107
|
role="dialog"
|
|
106
108
|
aria-modal="true"
|
|
107
109
|
aria-labelledby={titleId}
|
|
108
|
-
tabindex=
|
|
110
|
+
tabindex={-1}
|
|
109
111
|
>
|
|
110
|
-
<div class="
|
|
111
|
-
<div class="
|
|
112
|
-
<h2 id={titleId} class="
|
|
112
|
+
<div class="border-b border-border-1 px-4 py-3">
|
|
113
|
+
<div class="flex items-center justify-between gap-2">
|
|
114
|
+
<h2 id={titleId} class="m-0 flex items-center gap-2 text-15 font-semibold text-text-1">
|
|
113
115
|
Notifications
|
|
114
116
|
{#if unread > 0}
|
|
115
|
-
<span class="
|
|
117
|
+
<span class="inline-flex h-5 min-w-[20px] items-center justify-center rounded-full bg-brand-05 px-1.5 text-11 font-bold text-white tabular-nums" aria-label={`${unread} unread`}>{unread}</span>
|
|
116
118
|
{/if}
|
|
117
119
|
</h2>
|
|
118
|
-
<div class="
|
|
120
|
+
<div class="flex items-center gap-1.5">
|
|
119
121
|
{#if unread > 0 && onMarkAllRead}
|
|
120
|
-
<button
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
<button
|
|
123
|
+
type="button"
|
|
124
|
+
class="cursor-pointer rounded border-0 bg-transparent px-1.5 py-0.5 text-11 text-brand-05 hover:text-status-info focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
|
|
125
|
+
onclick={onMarkAllRead}
|
|
126
|
+
>Mark all read</button>
|
|
123
127
|
{/if}
|
|
124
|
-
<button
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
aria-label="Close notifications"
|
|
131
|
+
class="inline-flex h-7 w-7 cursor-pointer items-center justify-center rounded border-0 bg-transparent text-text-4 hover:bg-row-hover hover:text-text-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
|
|
132
|
+
onclick={onClose}
|
|
133
|
+
>
|
|
125
134
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
126
135
|
<path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.75.75 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.75.75 0 1 1-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 0 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06z"/>
|
|
127
136
|
</svg>
|
|
128
137
|
</button>
|
|
129
138
|
</div>
|
|
130
139
|
</div>
|
|
131
|
-
<div class="
|
|
140
|
+
<div class="mt-2 flex gap-1" role="tablist" aria-label="Filter by type">
|
|
132
141
|
{#each ['all', 'alarm', 'system', 'update'] as f}
|
|
133
142
|
<button
|
|
134
143
|
type="button"
|
|
135
144
|
role="tab"
|
|
136
145
|
aria-selected={filter === f}
|
|
137
|
-
|
|
146
|
+
data-active={filter === f ? 'true' : undefined}
|
|
147
|
+
class="cursor-pointer rounded border border-border-2 bg-transparent px-2.5 py-0.5 text-11 text-text-3 hover:bg-row-hover hover:text-text-1 data-[active=true]:border-brand-05 data-[active=true]:bg-brand-05/[0.10] data-[active=true]:text-brand-05 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
|
|
138
148
|
onclick={() => setFilter(f)}
|
|
139
149
|
>
|
|
140
150
|
{TYPE_LABEL[f] ?? f}
|
|
@@ -143,39 +153,42 @@
|
|
|
143
153
|
</div>
|
|
144
154
|
</div>
|
|
145
155
|
<div
|
|
146
|
-
class="
|
|
156
|
+
class="flex-1 overflow-y-auto"
|
|
147
157
|
role="log"
|
|
148
158
|
aria-live="polite"
|
|
149
159
|
aria-label="Notifications"
|
|
150
160
|
aria-relevant="additions"
|
|
151
161
|
>
|
|
152
162
|
{#if filtered.length === 0}
|
|
153
|
-
<div class="
|
|
163
|
+
<div class="p-6 text-center text-13 text-text-4">No notifications</div>
|
|
154
164
|
{:else}
|
|
155
165
|
{#each filtered as n (n.id)}
|
|
156
|
-
<div
|
|
166
|
+
<div
|
|
167
|
+
data-read={n.read ? 'true' : undefined}
|
|
168
|
+
class="flex items-start gap-2 border-b border-border-1 px-4 py-3 last:border-b-0 data-[read=true]:opacity-60"
|
|
169
|
+
>
|
|
157
170
|
<span
|
|
158
|
-
class="
|
|
171
|
+
class="mt-1.5 inline-block h-2 w-2 shrink-0 rounded-full"
|
|
159
172
|
style="background:{SEV_COLOR[n.severity ?? 'info']};"
|
|
160
173
|
aria-hidden="true"
|
|
161
174
|
></span>
|
|
162
|
-
<div class="
|
|
163
|
-
<div class="
|
|
164
|
-
<span class="
|
|
165
|
-
<span class="
|
|
175
|
+
<div class="min-w-0 flex-1">
|
|
176
|
+
<div class="flex items-baseline justify-between gap-2">
|
|
177
|
+
<span class="truncate text-13 font-medium text-text-1">{n.title}</span>
|
|
178
|
+
<span class="shrink-0 text-11 text-text-4 tabular-nums">{n.time}</span>
|
|
166
179
|
</div>
|
|
167
180
|
{#if n.description}
|
|
168
|
-
<p class="
|
|
181
|
+
<p class="m-0 mt-0.5 text-12 text-text-3">{n.description}</p>
|
|
169
182
|
{/if}
|
|
170
|
-
<span class="
|
|
183
|
+
<span class="mt-1 inline-block rounded bg-row-active px-1.5 py-0.5 text-[10px] uppercase tracking-[0.05em] text-text-4">{TYPE_LABEL[n.type]}</span>
|
|
171
184
|
</div>
|
|
172
185
|
{#if !n.read && onMarkRead}
|
|
173
186
|
<button
|
|
174
187
|
type="button"
|
|
175
|
-
|
|
176
|
-
onclick={() => onMarkRead?.(n.id)}
|
|
177
|
-
aria-label="Mark as read: {n.title}"
|
|
188
|
+
aria-label={`Mark as read: ${n.title}`}
|
|
178
189
|
title="Mark as read"
|
|
190
|
+
class="inline-flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center rounded border-0 bg-transparent text-status-success hover:bg-status-success/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
|
|
191
|
+
onclick={() => onMarkRead?.(n.id)}
|
|
179
192
|
>✓</button>
|
|
180
193
|
{/if}
|
|
181
194
|
</div>
|
|
@@ -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?: number;
|
|
@@ -30,10 +28,10 @@
|
|
|
30
28
|
class: className = '',
|
|
31
29
|
onchange,
|
|
32
30
|
}: Props = $props();
|
|
33
|
-
|
|
34
31
|
|
|
35
32
|
const uid = `dash-ui-ni-${++counter}`;
|
|
36
33
|
let inputId = $derived(id ?? uid);
|
|
34
|
+
|
|
37
35
|
function clamp(n: number): number {
|
|
38
36
|
let v = n;
|
|
39
37
|
if (min !== undefined) v = Math.max(min, v);
|
|
@@ -62,19 +60,22 @@
|
|
|
62
60
|
|
|
63
61
|
let canDecrement = $derived(!disabled && (min === undefined || value > min));
|
|
64
62
|
let canIncrement = $derived(!disabled && (max === undefined || value < max));
|
|
63
|
+
|
|
64
|
+
const BTN_CLS =
|
|
65
|
+
'inline-flex h-full w-[30px] shrink-0 cursor-pointer items-center justify-center border-0 bg-transparent text-base text-text-4 transition-[background-color,color] duration-100 hover:bg-row-hover hover:text-text-1 disabled:cursor-not-allowed disabled:opacity-40 focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-brand-05';
|
|
65
66
|
</script>
|
|
66
67
|
|
|
67
|
-
<div class="
|
|
68
|
+
<div class="inline-flex flex-col gap-1 {className}">
|
|
68
69
|
{#if label}
|
|
69
|
-
<label for={inputId} class="
|
|
70
|
+
<label for={inputId} class="text-12 text-text-4">{label}</label>
|
|
70
71
|
{/if}
|
|
71
|
-
<div class="
|
|
72
|
+
<div class="inline-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">
|
|
72
73
|
<button
|
|
73
74
|
type="button"
|
|
74
|
-
class="number-input__btn"
|
|
75
75
|
aria-label="Decrement"
|
|
76
|
-
tabindex=
|
|
76
|
+
tabindex={-1}
|
|
77
77
|
disabled={!canDecrement}
|
|
78
|
+
class={BTN_CLS}
|
|
78
79
|
onclick={() => commit(value - step)}
|
|
79
80
|
>−</button>
|
|
80
81
|
<!-- svelte-ignore a11y_no_redundant_roles - role kept explicit for screen reader compat -->
|
|
@@ -88,19 +89,19 @@
|
|
|
88
89
|
aria-valuetext={suffix ? `${value} ${suffix}` : undefined}
|
|
89
90
|
{value}
|
|
90
91
|
{disabled}
|
|
91
|
-
class="
|
|
92
|
+
class="w-[6ch] min-w-[40px] border-0 bg-transparent px-1 text-center text-13 text-text-1 outline-none [appearance:textfield] [-moz-appearance:textfield] [&::-webkit-outer-spin-button]:m-0 [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:m-0 [&::-webkit-inner-spin-button]:appearance-none"
|
|
92
93
|
onchange={handleChange}
|
|
93
94
|
onkeydown={handleKey}
|
|
94
95
|
/>
|
|
95
96
|
{#if suffix}
|
|
96
|
-
<span class="
|
|
97
|
+
<span aria-hidden="true" class="flex items-center whitespace-nowrap pl-0.5 pr-2 text-12 text-text-4">{suffix}</span>
|
|
97
98
|
{/if}
|
|
98
99
|
<button
|
|
99
100
|
type="button"
|
|
100
|
-
class="number-input__btn"
|
|
101
101
|
aria-label="Increment"
|
|
102
|
-
tabindex=
|
|
102
|
+
tabindex={-1}
|
|
103
103
|
disabled={!canIncrement}
|
|
104
|
+
class={BTN_CLS}
|
|
104
105
|
onclick={() => commit(value + step)}
|
|
105
106
|
>+</button>
|
|
106
107
|
</div>
|