@w5-ui/svelte 0.2.0 → 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 +4 -4
- 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
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
let { label, class: className = '', onclick }: Props = $props();
|
|
9
9
|
</script>
|
|
10
10
|
|
|
11
|
-
<span
|
|
12
|
-
|
|
11
|
+
<span
|
|
12
|
+
class="inline-flex h-6 items-center gap-1 whitespace-nowrap rounded-full border border-brand-05/25 bg-brand-05/14 pl-2.5 pr-1 text-12 font-medium text-status-info {className}"
|
|
13
|
+
>
|
|
14
|
+
<span class="leading-none">{label}</span>
|
|
13
15
|
<button
|
|
14
16
|
type="button"
|
|
15
|
-
class="
|
|
17
|
+
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-[background,color] duration-100 hover:bg-brand-05/25 hover:text-text-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
|
|
16
18
|
aria-label="Remove {label} filter"
|
|
17
19
|
{onclick}
|
|
18
20
|
>
|
|
@@ -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[];
|
|
@@ -56,26 +54,27 @@
|
|
|
56
54
|
}
|
|
57
55
|
</script>
|
|
58
56
|
|
|
59
|
-
<div class=
|
|
57
|
+
<div class={klass}>
|
|
60
58
|
{#if label}
|
|
61
|
-
<label for={uid} class="
|
|
59
|
+
<label for={uid} class="text-12 text-text-4">{label}</label>
|
|
62
60
|
{/if}
|
|
63
61
|
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
64
62
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions - group container focuses input on click -->
|
|
65
63
|
<div
|
|
66
64
|
role="group"
|
|
67
65
|
aria-label={label ?? 'Tags'}
|
|
68
|
-
|
|
66
|
+
data-disabled={disabled ? 'true' : undefined}
|
|
67
|
+
class="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-2 transition-colors duration-100 hover:border-border-3 focus-within:border-brand-05 focus-within:shadow-[0_0_0_2px_rgba(0,111,255,0.2)] data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-40"
|
|
69
68
|
onclick={() => inputEl?.focus()}
|
|
70
69
|
>
|
|
71
70
|
{#each value as tag, i}
|
|
72
|
-
<span class="
|
|
73
|
-
<span class="
|
|
71
|
+
<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">
|
|
72
|
+
<span class="leading-none">{tag}</span>
|
|
74
73
|
{#if !disabled}
|
|
75
74
|
<button
|
|
76
75
|
type="button"
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
aria-label={`Remove ${tag}`}
|
|
77
|
+
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"
|
|
79
78
|
onclick={(e) => { e.stopPropagation(); (() => removeTag(i))(); }}
|
|
80
79
|
>
|
|
81
80
|
<svg viewBox="0 0 10 10" width="10" height="10" fill="none" aria-hidden="true" focusable="false">
|
|
@@ -89,12 +88,12 @@
|
|
|
89
88
|
bind:this={inputEl}
|
|
90
89
|
id={uid}
|
|
91
90
|
type="text"
|
|
92
|
-
class="tag-input__field"
|
|
93
91
|
bind:value={draft}
|
|
94
92
|
{disabled}
|
|
95
93
|
placeholder={value.length === 0 ? placeholder : ''}
|
|
96
94
|
aria-label={label ?? 'Add tag'}
|
|
97
95
|
autocomplete="off"
|
|
96
|
+
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"
|
|
98
97
|
onkeydown={handleKeyDown}
|
|
99
98
|
onblur={handleBlur}
|
|
100
99
|
/>
|
|
@@ -37,9 +37,15 @@
|
|
|
37
37
|
let inputId = $derived(id ?? uid);
|
|
38
38
|
</script>
|
|
39
39
|
|
|
40
|
+
<!--
|
|
41
|
+
Textarea reuses the dashboard.css/Input layout (.input + .textarea sit
|
|
42
|
+
together; the `auto` height + min-height: 80px + resize: vertical + 8px
|
|
43
|
+
vertical padding + 1.5 line-height overrides come from .textarea). All
|
|
44
|
+
inline now.
|
|
45
|
+
-->
|
|
40
46
|
<textarea
|
|
41
47
|
id={inputId}
|
|
42
|
-
class="
|
|
48
|
+
class="block min-h-20 w-full resize-y rounded-md border border-border-2 bg-bg-page px-3 py-2 font-[inherit] text-13 leading-[1.5] text-text-1 outline-none transition-[border-color] duration-100 focus:border-brand-05 focus:[box-shadow:0_0_0_2px_rgba(0,111,255,0.2)] aria-[invalid='true']:border-status-danger aria-[invalid='true']:focus:[box-shadow:0_0_0_2px_rgba(240,58,58,0.12)] {className}"
|
|
43
49
|
{style}
|
|
44
50
|
{rows}
|
|
45
51
|
{placeholder}
|
|
@@ -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;
|
|
@@ -73,13 +71,21 @@
|
|
|
73
71
|
field === 0 ? commit(clamped, minutes) : commit(hours, clamped);
|
|
74
72
|
if (raw.length === 2 && field === 0) setTimeout(() => inputs[1]?.focus(), 0);
|
|
75
73
|
}
|
|
74
|
+
|
|
75
|
+
const FIELD_CLS =
|
|
76
|
+
'w-9 border-0 bg-transparent p-0 text-center font-mono text-13 text-text-1 outline-none';
|
|
76
77
|
</script>
|
|
77
78
|
|
|
78
|
-
<div class=
|
|
79
|
+
<div class={className}>
|
|
79
80
|
{#if label}
|
|
80
|
-
<label for="{uid}-h" class="
|
|
81
|
+
<label for="{uid}-h" class="text-12 text-text-4">{label}</label>
|
|
81
82
|
{/if}
|
|
82
|
-
<div
|
|
83
|
+
<div
|
|
84
|
+
role="group"
|
|
85
|
+
aria-label={label ?? 'Time'}
|
|
86
|
+
data-disabled={disabled ? 'true' : undefined}
|
|
87
|
+
class="inline-flex h-[34px] items-center gap-0.5 rounded-md border border-border-2 bg-bg-page px-2 transition-colors duration-100 focus-within:border-brand-05 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-40"
|
|
88
|
+
>
|
|
83
89
|
<input
|
|
84
90
|
bind:this={inputs[0]}
|
|
85
91
|
id="{uid}-h"
|
|
@@ -95,12 +101,12 @@
|
|
|
95
101
|
value={pad(hours)}
|
|
96
102
|
{disabled}
|
|
97
103
|
maxlength={2}
|
|
98
|
-
class=
|
|
104
|
+
class={FIELD_CLS}
|
|
99
105
|
oninput={(e) => handleInput(0, e)}
|
|
100
106
|
onkeydown={(e) => handleKey(0, e)}
|
|
101
107
|
onfocus={(e) => e.currentTarget.select()}
|
|
102
108
|
/>
|
|
103
|
-
<span class="
|
|
109
|
+
<span aria-hidden="true" class="select-none text-13 leading-none text-text-4">:</span>
|
|
104
110
|
<input
|
|
105
111
|
bind:this={inputs[1]}
|
|
106
112
|
type="text"
|
|
@@ -115,7 +121,7 @@
|
|
|
115
121
|
value={pad(minutes)}
|
|
116
122
|
{disabled}
|
|
117
123
|
maxlength={2}
|
|
118
|
-
class=
|
|
124
|
+
class={FIELD_CLS}
|
|
119
125
|
oninput={(e) => handleInput(1, e)}
|
|
120
126
|
onkeydown={(e) => handleKey(1, e)}
|
|
121
127
|
onfocus={(e) => e.currentTarget.select()}
|
|
@@ -12,21 +12,19 @@
|
|
|
12
12
|
|
|
13
13
|
<script lang="ts">
|
|
14
14
|
import { onMount, onDestroy } from 'svelte';
|
|
15
|
+
import Button from './Button.svelte';
|
|
15
16
|
|
|
16
17
|
interface Props {
|
|
17
18
|
value?: TimeRangeId;
|
|
18
19
|
onchange?: (payload: TimeRangeId) => void;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
let { value = '1h',
|
|
22
|
-
onchange,
|
|
23
|
-
}: Props = $props();
|
|
22
|
+
let { value = '1h', onchange }: Props = $props();
|
|
24
23
|
const uid = `dash-ui-tr-${++counter}`;
|
|
25
24
|
const listId = `${uid}-list`;
|
|
26
25
|
|
|
27
26
|
let open = $state(false);
|
|
28
27
|
let activeIdx = $state(0);
|
|
29
|
-
let triggerEl = $state<HTMLButtonElement | undefined>(undefined);
|
|
30
28
|
let wrapperEl = $state<HTMLDivElement | undefined>(undefined);
|
|
31
29
|
|
|
32
30
|
let selected = $derived(PRESETS.find((p) => p.id === value) ?? PRESETS[0]);
|
|
@@ -39,7 +37,6 @@
|
|
|
39
37
|
function select(id: TimeRangeId) {
|
|
40
38
|
onchange?.(id);
|
|
41
39
|
open = false;
|
|
42
|
-
triggerEl?.focus();
|
|
43
40
|
}
|
|
44
41
|
|
|
45
42
|
function onKeyDown(e: KeyboardEvent) {
|
|
@@ -51,7 +48,7 @@
|
|
|
51
48
|
}
|
|
52
49
|
return;
|
|
53
50
|
}
|
|
54
|
-
if (e.key === 'Escape') { e.preventDefault(); open = false;
|
|
51
|
+
if (e.key === 'Escape') { e.preventDefault(); open = false; }
|
|
55
52
|
else if (e.key === 'ArrowDown') { e.preventDefault(); activeIdx = Math.min(activeIdx + 1, PRESETS.length - 1); }
|
|
56
53
|
else if (e.key === 'ArrowUp') { e.preventDefault(); activeIdx = Math.max(activeIdx - 1, 0); }
|
|
57
54
|
else if (e.key === 'Home') { e.preventDefault(); activeIdx = 0; }
|
|
@@ -68,31 +65,32 @@
|
|
|
68
65
|
onDestroy(() => document.removeEventListener('mousedown', handleOutside));
|
|
69
66
|
</script>
|
|
70
67
|
|
|
71
|
-
<div
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
aria-controls={open ? listId : undefined}
|
|
79
|
-
onclick={toggle}
|
|
80
|
-
onkeydown={onKeyDown}
|
|
81
|
-
>
|
|
68
|
+
<div
|
|
69
|
+
class="relative inline-block"
|
|
70
|
+
bind:this={wrapperEl}
|
|
71
|
+
onkeydown={onKeyDown}
|
|
72
|
+
role="presentation"
|
|
73
|
+
>
|
|
74
|
+
<Button variant="ghost" aria-haspopup="listbox" aria-expanded={open} aria-controls={open ? listId : undefined} onclick={toggle}>
|
|
82
75
|
{selected.short}
|
|
83
|
-
<svg
|
|
76
|
+
<svg width="10" height="6" viewBox="0 0 10 6" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="shrink-0 transition-transform duration-100 motion-reduce:transition-none {open ? 'rotate-180' : ''}">
|
|
84
77
|
<path d="M1 1l4 4 4-4" />
|
|
85
78
|
</svg>
|
|
86
|
-
</
|
|
79
|
+
</Button>
|
|
87
80
|
{#if open}
|
|
88
|
-
<ul
|
|
81
|
+
<ul
|
|
82
|
+
id={listId}
|
|
83
|
+
role="listbox"
|
|
84
|
+
aria-label="Time range"
|
|
85
|
+
class="absolute right-0 top-[calc(100%+4px)] z-[9100] m-0 min-w-[152px] list-none rounded-lg border border-border-3 bg-bg-2 p-1 shadow-[0_8px_24px_rgba(0,0,0,0.5)]"
|
|
86
|
+
>
|
|
89
87
|
{#each PRESETS as p, idx (p.id)}
|
|
90
88
|
<li
|
|
91
89
|
role="option"
|
|
92
90
|
aria-selected={p.id === value}
|
|
93
|
-
tabindex=
|
|
91
|
+
tabindex={-1}
|
|
94
92
|
data-active={idx === activeIdx ? 'true' : undefined}
|
|
95
|
-
class="
|
|
93
|
+
class="flex cursor-pointer items-center justify-between gap-2 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-selected:text-status-info"
|
|
96
94
|
onmouseenter={() => { activeIdx = idx; }}
|
|
97
95
|
onmousedown={(e) => { e.preventDefault(); (() => select(p.id))(); }}
|
|
98
96
|
>
|
|
@@ -15,20 +15,31 @@
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
let { events, class: className = '' }: Props = $props();
|
|
18
|
-
|
|
18
|
+
|
|
19
|
+
type V = NonNullable<TimelineEvent['variant']>;
|
|
20
|
+
const VARIANT: Record<V, string> = {
|
|
21
|
+
info: 'bg-brand-05',
|
|
22
|
+
success: 'bg-status-success',
|
|
23
|
+
warn: 'bg-status-warning',
|
|
24
|
+
danger: 'bg-status-danger',
|
|
25
|
+
neutral: 'bg-status-neutral',
|
|
26
|
+
};
|
|
19
27
|
</script>
|
|
20
28
|
|
|
21
|
-
<ol class="
|
|
29
|
+
<ol class="relative m-0 list-none p-0 {className}" aria-label="Event timeline">
|
|
22
30
|
{#each events as e (e.id)}
|
|
23
|
-
<li class="
|
|
24
|
-
<span
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
<li class="relative flex gap-3 pb-3 pl-4 last:pb-0 [&:not(:last-child)]:before:absolute [&:not(:last-child)]:before:bottom-0 [&:not(:last-child)]:before:left-[5px] [&:not(:last-child)]:before:top-4 [&:not(:last-child)]:before:w-px [&:not(:last-child)]:before:bg-row-active [&:not(:last-child)]:before:content-['']">
|
|
32
|
+
<span
|
|
33
|
+
class="absolute left-0 top-1.5 inline-block h-2.5 w-2.5 shrink-0 rounded-full {VARIANT[e.variant ?? 'neutral']}"
|
|
34
|
+
aria-hidden="true"
|
|
35
|
+
></span>
|
|
36
|
+
<div class="min-w-0 flex-1">
|
|
37
|
+
<div class="flex items-baseline justify-between gap-2">
|
|
38
|
+
<span class="font-medium text-text-1">{e.title}</span>
|
|
39
|
+
<time class="shrink-0 text-11 text-text-4 tabular-nums">{e.time}</time>
|
|
29
40
|
</div>
|
|
30
41
|
{#if e.description}
|
|
31
|
-
<p class="
|
|
42
|
+
<p class="m-0 mt-0.5 text-12 text-text-3">{e.description}</p>
|
|
32
43
|
{/if}
|
|
33
44
|
</div>
|
|
34
45
|
</li>
|
|
@@ -30,16 +30,35 @@
|
|
|
30
30
|
</script>
|
|
31
31
|
|
|
32
32
|
<script lang="ts">
|
|
33
|
-
// store
|
|
33
|
+
// store + dismiss live in module scope; template uses $store
|
|
34
|
+
// Pre-composed variant classes for Tailwind's static scanner.
|
|
35
|
+
// Backgrounds at 16% (matches dashboard.css), left border solid, text
|
|
36
|
+
// colour the legibility-tuned literal hue.
|
|
37
|
+
const TOAST: Record<ToastVariant, string> = {
|
|
38
|
+
success: 'bg-status-success/16 border-l-status-success text-status-success',
|
|
39
|
+
warn: 'bg-status-warning/16 border-l-status-warning text-status-warning',
|
|
40
|
+
danger: 'bg-status-danger/16 border-l-status-danger text-status-danger',
|
|
41
|
+
info: 'bg-status-info/16 border-l-brand-05 text-status-info',
|
|
42
|
+
};
|
|
34
43
|
</script>
|
|
35
44
|
|
|
36
|
-
<div
|
|
45
|
+
<div
|
|
46
|
+
class="pointer-events-none fixed bottom-6 right-6 z-[9900] flex min-w-[240px] max-w-[360px] flex-col gap-2"
|
|
47
|
+
role="log"
|
|
48
|
+
aria-label="Notifications"
|
|
49
|
+
aria-live="polite"
|
|
50
|
+
aria-atomic="false"
|
|
51
|
+
>
|
|
37
52
|
{#each $store as item (item.id)}
|
|
38
|
-
<div
|
|
39
|
-
|
|
53
|
+
<div
|
|
54
|
+
class="pointer-events-auto flex items-center gap-2.5 rounded-lg border-l-[3px] px-3.5 py-2.5 text-13 leading-[1.4] [animation:toast-in_150ms_ease] [box-shadow:0_4px_16px_rgba(0,0,0,0.5)]
|
|
55
|
+
{TOAST[item.variant]}"
|
|
56
|
+
role={item.variant === 'danger' ? 'alert' : 'status'}
|
|
57
|
+
>
|
|
58
|
+
<span class="flex-1">{item.message}</span>
|
|
40
59
|
<button
|
|
41
60
|
type="button"
|
|
42
|
-
class="
|
|
61
|
+
class="ml-auto inline-flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent text-current hover:bg-row-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
|
|
43
62
|
onclick={() => dismiss(item.id)}
|
|
44
63
|
aria-label="Dismiss notification"
|
|
45
64
|
>
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
2
|
interface Props {
|
|
4
3
|
on?: boolean;
|
|
5
4
|
ariaLabel?: string | undefined;
|
|
@@ -7,7 +6,10 @@
|
|
|
7
6
|
ontoggle?: (payload: boolean) => void;
|
|
8
7
|
}
|
|
9
8
|
|
|
10
|
-
let {
|
|
9
|
+
let {
|
|
10
|
+
on = $bindable(false),
|
|
11
|
+
ariaLabel = undefined,
|
|
12
|
+
ariaDescribedBy = undefined,
|
|
11
13
|
ontoggle,
|
|
12
14
|
}: Props = $props();
|
|
13
15
|
function flip() {
|
|
@@ -22,8 +24,12 @@
|
|
|
22
24
|
aria-checked={on}
|
|
23
25
|
aria-label={ariaLabel}
|
|
24
26
|
aria-describedby={ariaDescribedBy}
|
|
25
|
-
class="
|
|
27
|
+
class="relative inline-block h-[18px] w-8 shrink-0 cursor-pointer appearance-none rounded-full border-0 p-0 font-[inherit] text-[inherit] transition-colors duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05
|
|
28
|
+
{on ? 'bg-brand-05' : 'bg-text-4'}"
|
|
26
29
|
onclick={flip}
|
|
27
30
|
>
|
|
28
|
-
<span
|
|
31
|
+
<span
|
|
32
|
+
class="absolute top-0.5 h-3.5 w-3.5 rounded-full bg-white transition-[left] duration-150
|
|
33
|
+
{on ? 'left-4' : 'left-0.5'}"
|
|
34
|
+
></span>
|
|
29
35
|
</button>
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
2
|
interface Props {
|
|
5
3
|
options?: { value: string; label: string; disabled?: boolean }[];
|
|
6
4
|
value?: string[];
|
|
@@ -18,20 +16,32 @@
|
|
|
18
16
|
class: klass = '',
|
|
19
17
|
onchange,
|
|
20
18
|
}: Props = $props();
|
|
19
|
+
|
|
21
20
|
function toggle(v: string) {
|
|
22
21
|
const next = value.includes(v) ? value.filter((x) => x !== v) : [...value, v];
|
|
23
22
|
value = next;
|
|
24
23
|
onchange?.(next);
|
|
25
24
|
}
|
|
25
|
+
|
|
26
|
+
// Pre-composed size strings — Tailwind's static scanner must see literals.
|
|
27
|
+
const SIZE: Record<NonNullable<Props['size']>, string> = {
|
|
28
|
+
sm: 'h-6 px-[9px] text-11',
|
|
29
|
+
md: 'h-7 px-3 text-12',
|
|
30
|
+
};
|
|
26
31
|
</script>
|
|
27
32
|
|
|
28
|
-
<div
|
|
33
|
+
<div
|
|
34
|
+
role="group"
|
|
35
|
+
aria-label={ariaLabel}
|
|
36
|
+
class="inline-flex flex-wrap gap-px rounded-lg bg-row-hover p-0.5 {klass}"
|
|
37
|
+
>
|
|
29
38
|
{#each options as opt}
|
|
39
|
+
{@const on = value.includes(opt.value)}
|
|
30
40
|
<button
|
|
31
41
|
type="button"
|
|
32
|
-
aria-pressed={
|
|
42
|
+
aria-pressed={on}
|
|
33
43
|
disabled={opt.disabled}
|
|
34
|
-
class="
|
|
44
|
+
class="flex-shrink-0 grow-0 inline-flex cursor-pointer select-none items-center justify-center whitespace-nowrap rounded-md border-0 bg-transparent font-medium leading-none text-text-3 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 {SIZE[size]} {on ? 'bg-bg-3 text-text-1 shadow-[0_1px_3px_rgba(0,0,0,0.18)] dark:shadow-[0_1px_3px_rgba(0,0,0,0.4)]' : ''}"
|
|
35
45
|
onclick={() => toggle(opt.value)}
|
|
36
46
|
>
|
|
37
47
|
{opt.label}
|
|
@@ -14,13 +14,28 @@
|
|
|
14
14
|
label,
|
|
15
15
|
placement = 'top',
|
|
16
16
|
class: className = '',
|
|
17
|
-
children
|
|
17
|
+
children,
|
|
18
18
|
}: Props = $props();
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
const id = `dash-ui-tooltip-${++counter}`;
|
|
21
|
+
|
|
22
|
+
// Pre-composed placement strings so Tailwind's scanner picks each up.
|
|
23
|
+
// The wrapper carries `group` so the content shows on group-hover or
|
|
24
|
+
// group-focus-within (replaces dashboard.css's `:hover .tooltip-content`).
|
|
25
|
+
const PLACEMENT: Record<NonNullable<Props['placement']>, string> = {
|
|
26
|
+
top: 'bottom-[calc(100%+6px)] left-1/2 -translate-x-1/2',
|
|
27
|
+
bottom: 'top-[calc(100%+6px)] left-1/2 -translate-x-1/2',
|
|
28
|
+
left: 'right-[calc(100%+6px)] top-1/2 -translate-y-1/2',
|
|
29
|
+
right: 'left-[calc(100%+6px)] top-1/2 -translate-y-1/2',
|
|
30
|
+
};
|
|
21
31
|
</script>
|
|
22
32
|
|
|
23
|
-
<span class="
|
|
24
|
-
<span class="
|
|
25
|
-
<span
|
|
33
|
+
<span class="group relative inline-flex {className}">
|
|
34
|
+
<span class="inline-flex" aria-describedby={id}>{@render children?.()}</span>
|
|
35
|
+
<span
|
|
36
|
+
{id}
|
|
37
|
+
role="tooltip"
|
|
38
|
+
class="pointer-events-none absolute z-[9999] whitespace-nowrap rounded border border-border-1 bg-bg-2 px-2 py-1 text-[11px] leading-[1.3] text-text-2 opacity-0 transition-opacity duration-100 group-hover:opacity-100 group-focus-within:opacity-100 motion-reduce:transition-none
|
|
39
|
+
{PLACEMENT[placement]}"
|
|
40
|
+
>{label}</span>
|
|
26
41
|
</span>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script module lang="ts">
|
|
2
2
|
export interface AppDef { id: string; label: string; logo: AppLogo }
|
|
3
|
+
export type TopbarStatus = 'ok' | 'warn' | 'danger' | 'neutral';
|
|
3
4
|
</script>
|
|
4
5
|
|
|
5
6
|
<script lang="ts">
|
|
6
|
-
|
|
7
|
+
import type { Snippet } from 'svelte';
|
|
7
8
|
import { appLogos, type AppLogoKey as AppLogo } from '@w5-ui/assets';
|
|
8
9
|
import IconButton from './IconButton.svelte';
|
|
9
10
|
import Avatar from './Avatar.svelte';
|
|
@@ -25,6 +26,15 @@
|
|
|
25
26
|
apps?: AppDef[];
|
|
26
27
|
initials?: string;
|
|
27
28
|
notificationCount?: number;
|
|
29
|
+
/** Health colour of the site-name status ring. Defaults to 'ok' (green). */
|
|
30
|
+
status?: TopbarStatus;
|
|
31
|
+
/** When false, the site label renders as a static element instead of a
|
|
32
|
+
* site-switcher dropdown trigger. Use for single-site dashboards. */
|
|
33
|
+
siteSwitchable?: boolean;
|
|
34
|
+
/** Custom right-side actions snippet. When provided, replaces the default
|
|
35
|
+
* Search/Updates/Notifications/Help/Avatar block in full. Use to slot in
|
|
36
|
+
* app-specific controls (theme toggle, status pill, etc.). */
|
|
37
|
+
actions?: Snippet;
|
|
28
38
|
onappchange?: (payload: string) => void;
|
|
29
39
|
}
|
|
30
40
|
|
|
@@ -34,44 +44,115 @@
|
|
|
34
44
|
apps = DEFAULT_APPS,
|
|
35
45
|
initials = 'MS',
|
|
36
46
|
notificationCount = 1,
|
|
47
|
+
status = 'ok',
|
|
48
|
+
siteSwitchable = true,
|
|
49
|
+
actions,
|
|
37
50
|
onappchange,
|
|
38
51
|
}: Props = $props();
|
|
52
|
+
|
|
53
|
+
// Status-ring colour mapping. We pre-compose the Tailwind class strings here
|
|
54
|
+
// (rather than inline) so the consumer's Tailwind compiler picks them up
|
|
55
|
+
// unambiguously via static-string scanning.
|
|
56
|
+
// Ring background opacities chosen to match dashboard.css exactly
|
|
57
|
+
// (`.status-ring { background: rgba(<colour>, 0.18) }`).
|
|
58
|
+
const RING_BG: Record<TopbarStatus, string> = {
|
|
59
|
+
ok: 'bg-status-success/18',
|
|
60
|
+
warn: 'bg-status-warning/18',
|
|
61
|
+
danger: 'bg-status-danger/18',
|
|
62
|
+
neutral: 'bg-status-neutral/18',
|
|
63
|
+
};
|
|
64
|
+
const RING_DOT: Record<TopbarStatus, string> = {
|
|
65
|
+
ok: 'bg-status-success',
|
|
66
|
+
warn: 'bg-status-warning',
|
|
67
|
+
danger: 'bg-status-danger',
|
|
68
|
+
neutral: 'bg-status-neutral',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
let ringWrapClass = $derived(
|
|
72
|
+
`inline-flex h-[18px] w-[18px] items-center justify-center rounded-full ${RING_BG[status]}`,
|
|
73
|
+
);
|
|
74
|
+
let ringDotClass = $derived(`h-2 w-2 rounded-full ${RING_DOT[status]}`);
|
|
39
75
|
</script>
|
|
40
76
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
77
|
+
<!--
|
|
78
|
+
Motif-aware chrome: surfaces resolve via `var(--bg-page)`, text via
|
|
79
|
+
`var(--text-1)`, borders via `var(--border-1)`. In dark motif the
|
|
80
|
+
values still resolve to the original `#0a0a0b` / `#ffffff` /
|
|
81
|
+
rgba(255,255,255,0.06) so the baseline look is unchanged. Light motif
|
|
82
|
+
flips them to `#ffffff` / `#0a0a0b` / `#ececee`.
|
|
83
|
+
-->
|
|
84
|
+
<header
|
|
85
|
+
class="flex h-12 shrink-0 items-center gap-0 bg-bg-page px-2"
|
|
86
|
+
>
|
|
87
|
+
{#if siteSwitchable}
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
class="flex cursor-pointer items-center gap-2 rounded-md px-2.5 py-1.5 hover:bg-row-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
|
|
91
|
+
aria-label="Switch site: {siteName}"
|
|
92
|
+
aria-haspopup="menu"
|
|
93
|
+
>
|
|
94
|
+
<span class={ringWrapClass}><span class={ringDotClass}></span></span>
|
|
95
|
+
<span class="text-13 font-medium text-text-1" aria-hidden="true">{siteName}</span>
|
|
96
|
+
<CaretIcon class="h-3.5 w-3.5 text-text-3" />
|
|
97
|
+
</button>
|
|
98
|
+
{:else}
|
|
99
|
+
<div class="flex items-center gap-2 px-2.5 py-1.5" role="presentation">
|
|
100
|
+
<span class={ringWrapClass}><span class={ringDotClass}></span></span>
|
|
101
|
+
<span class="text-13 font-medium text-text-1">{siteName}</span>
|
|
102
|
+
</div>
|
|
103
|
+
{/if}
|
|
104
|
+
|
|
105
|
+
<nav class="ml-3.5 flex h-12 items-center gap-0.5" aria-label="Apps">
|
|
48
106
|
{#each apps as a (a.id)}
|
|
49
107
|
<button
|
|
50
108
|
type="button"
|
|
51
|
-
class="
|
|
109
|
+
class="flex h-12 w-16 cursor-pointer flex-col items-center justify-center py-1.5 transition-colors duration-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05
|
|
110
|
+
{a.id === activeApp
|
|
111
|
+
? 'bg-bg-1 text-text-1'
|
|
112
|
+
: 'bg-transparent text-text-3 hover:text-text-1'}"
|
|
52
113
|
aria-current={a.id === activeApp ? 'page' : undefined}
|
|
53
114
|
onclick={() => { activeApp = a.id; onappchange?.(a.id); }}
|
|
54
115
|
>
|
|
55
|
-
<img
|
|
56
|
-
|
|
116
|
+
<img
|
|
117
|
+
src={appLogos[a.logo]}
|
|
118
|
+
alt=""
|
|
119
|
+
width="24"
|
|
120
|
+
height="24"
|
|
121
|
+
class="transition-[opacity,filter] duration-150
|
|
122
|
+
{a.id === activeApp
|
|
123
|
+
? 'opacity-100 dark:brightness-[1.6]'
|
|
124
|
+
: 'opacity-65 hover:opacity-90 dark:hover:brightness-[1.4]'}"
|
|
125
|
+
/>
|
|
126
|
+
<span class="mt-0.5 text-[10px] font-medium leading-none">{a.label}</span>
|
|
57
127
|
</button>
|
|
58
128
|
{/each}
|
|
59
129
|
</nav>
|
|
60
|
-
|
|
61
|
-
<div class="
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
130
|
+
|
|
131
|
+
<div class="flex-1"></div>
|
|
132
|
+
|
|
133
|
+
<div class="flex items-center gap-1 pr-2">
|
|
134
|
+
{#if actions}
|
|
135
|
+
{@render actions()}
|
|
136
|
+
{:else}
|
|
137
|
+
<IconButton aria-label="Search" title="Search"><SearchIcon /></IconButton>
|
|
138
|
+
<IconButton aria-label="Updates" title="Updates"><UpdatesIcon /></IconButton>
|
|
139
|
+
<IconButton
|
|
140
|
+
aria-label={notificationCount > 0
|
|
141
|
+
? `Notifications, ${notificationCount} new`
|
|
142
|
+
: 'Notifications'}
|
|
143
|
+
title="Notifications"
|
|
144
|
+
class="relative"
|
|
145
|
+
>
|
|
146
|
+
<BellIcon />
|
|
147
|
+
{#if notificationCount > 0}
|
|
148
|
+
<span
|
|
149
|
+
aria-hidden="true"
|
|
150
|
+
class="absolute right-1.5 top-1.5 h-1.5 w-1.5 rounded-full border-[1.5px] border-bg-page bg-status-danger"
|
|
151
|
+
></span>
|
|
152
|
+
{/if}
|
|
153
|
+
</IconButton>
|
|
154
|
+
<IconButton aria-label="Help" title="Help"><HelpIcon /></IconButton>
|
|
155
|
+
<Avatar initials={initials} size="sm" alt="Account, {initials}" class="ml-1" />
|
|
156
|
+
{/if}
|
|
76
157
|
</div>
|
|
77
158
|
</header>
|