@dorsk/tsumikit 0.38.0 → 0.39.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/dist/autoresize.d.ts +2 -0
- package/dist/autoresize.js +15 -1
- package/dist/components/atoms/Artwork.svelte +19 -18
- package/dist/components/atoms/Artwork.svelte.d.ts +3 -2
- package/dist/components/atoms/Badge.svelte +60 -26
- package/dist/components/atoms/Badge.svelte.d.ts +14 -3
- package/dist/components/atoms/Button.svelte +30 -12
- package/dist/components/atoms/Button.svelte.d.ts +4 -2
- package/dist/components/atoms/Card.svelte +47 -30
- package/dist/components/atoms/Card.svelte.d.ts +12 -3
- package/dist/components/atoms/Checkbox.svelte +16 -2
- package/dist/components/atoms/Checkbox.svelte.d.ts +2 -0
- package/dist/components/atoms/Dot.svelte +17 -11
- package/dist/components/atoms/Dot.svelte.d.ts +5 -2
- package/dist/components/atoms/Gauge.svelte +19 -18
- package/dist/components/atoms/Gauge.svelte.d.ts +3 -2
- package/dist/components/atoms/Heading.svelte +20 -19
- package/dist/components/atoms/Heading.svelte.d.ts +3 -2
- package/dist/components/atoms/Icon.svelte +33 -13
- package/dist/components/atoms/Icon.svelte.d.ts +17 -2
- package/dist/components/atoms/Input.svelte +17 -3
- package/dist/components/atoms/Input.svelte.d.ts +2 -1
- package/dist/components/atoms/Kbd.svelte +25 -3
- package/dist/components/atoms/Kbd.svelte.d.ts +5 -1
- package/dist/components/atoms/Link.svelte +16 -12
- package/dist/components/atoms/Link.svelte.d.ts +6 -2
- package/dist/components/atoms/Progress.svelte +119 -4
- package/dist/components/atoms/Progress.svelte.d.ts +28 -2
- package/dist/components/atoms/Scrim.svelte +7 -2
- package/dist/components/atoms/Scrim.svelte.d.ts +2 -0
- package/dist/components/atoms/SegmentedProgress.svelte +14 -2
- package/dist/components/atoms/SegmentedProgress.svelte.d.ts +5 -2
- package/dist/components/atoms/Select.svelte +18 -6
- package/dist/components/atoms/Select.svelte.d.ts +3 -3
- package/dist/components/atoms/Skeleton.svelte +12 -11
- package/dist/components/atoms/Skeleton.svelte.d.ts +3 -2
- package/dist/components/atoms/Slider.svelte +45 -3
- package/dist/components/atoms/Slider.svelte.d.ts +7 -0
- package/dist/components/atoms/Spinner.svelte +2 -0
- package/dist/components/atoms/Spinner.svelte.d.ts +2 -0
- package/dist/components/atoms/Switch.svelte +70 -18
- package/dist/components/atoms/Switch.svelte.d.ts +5 -1
- package/dist/components/atoms/Text.svelte +39 -26
- package/dist/components/atoms/Text.svelte.d.ts +7 -3
- package/dist/components/atoms/Textarea.svelte +47 -10
- package/dist/components/atoms/Textarea.svelte.d.ts +10 -3
- package/dist/components/layouts/AppShell.svelte +7 -3
- package/dist/components/layouts/AppShell.svelte.d.ts +2 -0
- package/dist/components/layouts/AutoGrid.svelte +4 -0
- package/dist/components/layouts/AutoGrid.svelte.d.ts +2 -0
- package/dist/components/layouts/Cluster.svelte +62 -1
- package/dist/components/layouts/Cluster.svelte.d.ts +9 -0
- package/dist/components/layouts/NavItem.svelte +22 -1
- package/dist/components/layouts/NavItem.svelte.d.ts +5 -0
- package/dist/components/layouts/NavSection.svelte +4 -1
- package/dist/components/layouts/NavSection.svelte.d.ts +2 -0
- package/dist/components/layouts/ResizablePanel.svelte +14 -3
- package/dist/components/layouts/ResizablePanel.svelte.d.ts +5 -0
- package/dist/components/layouts/Stack.svelte +41 -0
- package/dist/components/layouts/Stack.svelte.d.ts +9 -0
- package/dist/components/layouts/Toolbar.svelte +122 -0
- package/dist/components/layouts/Toolbar.svelte.d.ts +22 -0
- package/dist/components/molecules/AttachmentList.svelte +59 -0
- package/dist/components/molecules/AttachmentList.svelte.d.ts +13 -0
- package/dist/components/molecules/Breadcrumb.svelte +6 -2
- package/dist/components/molecules/Breadcrumb.svelte.d.ts +2 -0
- package/dist/components/molecules/Callout.svelte +10 -4
- package/dist/components/molecules/Callout.svelte.d.ts +1 -1
- package/dist/components/molecules/CapBar.svelte +5 -1
- package/dist/components/molecules/ChatBubble.svelte +158 -0
- package/dist/components/molecules/ChatBubble.svelte.d.ts +26 -0
- package/dist/components/molecules/CodeBlock.svelte +36 -3
- package/dist/components/molecules/CodeBlock.svelte.d.ts +8 -0
- package/dist/components/molecules/Composer.svelte +195 -0
- package/dist/components/molecules/Composer.svelte.d.ts +27 -0
- package/dist/components/molecules/ConfirmModal.svelte +15 -5
- package/dist/components/molecules/ConfirmModal.svelte.d.ts +5 -1
- package/dist/components/molecules/Dropzone.svelte +9 -3
- package/dist/components/molecules/Dropzone.svelte.d.ts +2 -0
- package/dist/components/molecules/EmptyState.svelte +9 -5
- package/dist/components/molecules/EmptyState.svelte.d.ts +3 -1
- package/dist/components/molecules/Field.svelte +7 -1
- package/dist/components/molecules/Field.svelte.d.ts +2 -0
- package/dist/components/molecules/FileButton.svelte +2 -1
- package/dist/components/molecules/FileButton.svelte.d.ts +2 -1
- package/dist/components/molecules/FilterInput.svelte +13 -3
- package/dist/components/molecules/FilterInput.svelte.d.ts +4 -1
- package/dist/components/molecules/FontScalePicker.svelte +9 -4
- package/dist/components/molecules/IconButton.svelte +32 -7
- package/dist/components/molecules/IconButton.svelte.d.ts +9 -1
- package/dist/components/molecules/KeyValue.svelte +4 -1
- package/dist/components/molecules/KeyValue.svelte.d.ts +2 -1
- package/dist/components/molecules/Menu.svelte +50 -5
- package/dist/components/molecules/Menu.svelte.d.ts +10 -1
- package/dist/components/molecules/Meter.svelte +91 -0
- package/dist/components/molecules/Meter.svelte.d.ts +29 -0
- package/dist/components/molecules/Metric.svelte +134 -8
- package/dist/components/molecules/Metric.svelte.d.ts +20 -2
- package/dist/components/molecules/Modal.svelte +26 -12
- package/dist/components/molecules/Modal.svelte.d.ts +7 -1
- package/dist/components/molecules/Pagination.svelte +25 -1
- package/dist/components/molecules/Pagination.svelte.d.ts +9 -2
- package/dist/components/molecules/Popover.svelte +14 -3
- package/dist/components/molecules/Popover.svelte.d.ts +5 -0
- package/dist/components/molecules/Prose.svelte +140 -0
- package/dist/components/molecules/Prose.svelte.d.ts +13 -0
- package/dist/components/molecules/RadioGroup.svelte +42 -2
- package/dist/components/molecules/RadioGroup.svelte.d.ts +4 -1
- package/dist/components/molecules/SectionHeader.svelte +5 -2
- package/dist/components/molecules/SectionHeader.svelte.d.ts +1 -1
- package/dist/components/molecules/SegmentedControl.svelte +36 -1
- package/dist/components/molecules/SegmentedControl.svelte.d.ts +11 -1
- package/dist/components/molecules/SelectButton.svelte +64 -4
- package/dist/components/molecules/SelectButton.svelte.d.ts +18 -1
- package/dist/components/molecules/Tabs.svelte +21 -3
- package/dist/components/molecules/Tabs.svelte.d.ts +6 -0
- package/dist/components/molecules/Timestamp.svelte +13 -6
- package/dist/components/molecules/Timestamp.svelte.d.ts +4 -0
- package/dist/components/molecules/Toaster.svelte +54 -38
- package/dist/components/molecules/Toaster.svelte.d.ts +5 -1
- package/dist/components/molecules/Toggle.svelte +2 -1
- package/dist/components/molecules/Toggle.svelte.d.ts +2 -1
- package/dist/components/molecules/Tooltip.svelte +32 -5
- package/dist/components/molecules/Tooltip.svelte.d.ts +9 -0
- package/dist/components/molecules/Truncate.svelte +156 -18
- package/dist/components/molecules/Truncate.svelte.d.ts +17 -1
- package/dist/components/molecules/WorkingDir.svelte +6 -0
- package/dist/components/molecules/WorkingDir.svelte.d.ts +2 -0
- package/dist/components/organisms/DataTable.svelte +38 -10
- package/dist/components/organisms/DataTable.svelte.d.ts +4 -0
- package/dist/components/organisms/FilterSearchBar.svelte +9 -2
- package/dist/components/organisms/FilterSearchBar.svelte.d.ts +4 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.js +8 -1
- package/dist/size.d.ts +2 -0
- package/dist/size.js +1 -0
- package/dist/styles/tokens.css +3 -0
- package/dist/timestamp.d.ts +2 -2
- package/dist/timestamp.js +4 -4
- package/dist/tone.d.ts +1 -1
- package/dist/tone.js +1 -1
- package/dist/truncate.d.ts +13 -1
- package/dist/truncate.js +39 -0
- package/package.json +5 -3
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
description?: string;
|
|
7
7
|
note?: string;
|
|
8
8
|
disabled?: boolean;
|
|
9
|
+
/** `swatch` variant: any CSS colour (or var()). */
|
|
10
|
+
color?: string;
|
|
9
11
|
}
|
|
10
12
|
</script>
|
|
11
13
|
|
|
@@ -30,7 +32,8 @@
|
|
|
30
32
|
value?: string;
|
|
31
33
|
name?: string;
|
|
32
34
|
label: string;
|
|
33
|
-
|
|
35
|
+
/** `swatch` renders each option as a colour chip with the label as its name. */
|
|
36
|
+
variant?: 'list' | 'rows' | 'swatch';
|
|
34
37
|
action?: Snippet<[RadioOption]>;
|
|
35
38
|
below?: Snippet<[RadioOption]>;
|
|
36
39
|
class?: string;
|
|
@@ -42,11 +45,17 @@
|
|
|
42
45
|
aria-label={label}
|
|
43
46
|
class="radio-group {klass}"
|
|
44
47
|
class:rows={variant === 'rows'}
|
|
48
|
+
class:swatches={variant === 'swatch'}
|
|
45
49
|
data-tsu="RadioGroup"
|
|
46
50
|
data-variant={variant}
|
|
47
51
|
>
|
|
48
52
|
{#each options as o (o.value)}
|
|
49
|
-
{#if variant === '
|
|
53
|
+
{#if variant === 'swatch'}
|
|
54
|
+
<label class="swatch" class:disabled={o.disabled} title={o.label}>
|
|
55
|
+
<input type="radio" {name} value={o.value} bind:group={value} disabled={o.disabled} aria-label={o.label} />
|
|
56
|
+
<span class="swatch-chip" aria-hidden="true" style="--swatch: {o.color ?? 'var(--accent)'}"></span>
|
|
57
|
+
</label>
|
|
58
|
+
{:else if variant === 'rows'}
|
|
50
59
|
<div class="row-wrap">
|
|
51
60
|
<div class="row" class:selected={o.value === value} class:disabled={o.disabled}>
|
|
52
61
|
<label class="radio">
|
|
@@ -215,4 +224,35 @@
|
|
|
215
224
|
.below:empty {
|
|
216
225
|
display: none;
|
|
217
226
|
}
|
|
227
|
+
.swatches {
|
|
228
|
+
flex-direction: row;
|
|
229
|
+
flex-wrap: wrap;
|
|
230
|
+
gap: var(--sp-2);
|
|
231
|
+
}
|
|
232
|
+
.swatch {
|
|
233
|
+
position: relative;
|
|
234
|
+
display: inline-flex;
|
|
235
|
+
cursor: pointer;
|
|
236
|
+
}
|
|
237
|
+
.swatch.disabled {
|
|
238
|
+
opacity: 0.45;
|
|
239
|
+
cursor: not-allowed;
|
|
240
|
+
}
|
|
241
|
+
.swatch-chip {
|
|
242
|
+
width: 1.6rem;
|
|
243
|
+
height: 1.6rem;
|
|
244
|
+
border-radius: var(--r-pill);
|
|
245
|
+
background: var(--swatch);
|
|
246
|
+
border: 2px solid transparent;
|
|
247
|
+
box-shadow: 0 0 0 1px var(--border-strong);
|
|
248
|
+
transition: box-shadow 0.12s var(--ease);
|
|
249
|
+
}
|
|
250
|
+
.swatch input:checked + .swatch-chip {
|
|
251
|
+
border-color: var(--bg);
|
|
252
|
+
box-shadow: 0 0 0 2px var(--accent);
|
|
253
|
+
}
|
|
254
|
+
.swatch input:focus-visible + .swatch-chip {
|
|
255
|
+
outline: var(--focus-ring);
|
|
256
|
+
outline-offset: 2px;
|
|
257
|
+
}
|
|
218
258
|
</style>
|
|
@@ -5,6 +5,8 @@ export interface RadioOption {
|
|
|
5
5
|
description?: string;
|
|
6
6
|
note?: string;
|
|
7
7
|
disabled?: boolean;
|
|
8
|
+
/** `swatch` variant: any CSS colour (or var()). */
|
|
9
|
+
color?: string;
|
|
8
10
|
}
|
|
9
11
|
import type { Snippet } from 'svelte';
|
|
10
12
|
type $$ComponentProps = {
|
|
@@ -12,7 +14,8 @@ type $$ComponentProps = {
|
|
|
12
14
|
value?: string;
|
|
13
15
|
name?: string;
|
|
14
16
|
label: string;
|
|
15
|
-
|
|
17
|
+
/** `swatch` renders each option as a colour chip with the label as its name. */
|
|
18
|
+
variant?: 'list' | 'rows' | 'swatch';
|
|
16
19
|
action?: Snippet<[RadioOption]>;
|
|
17
20
|
below?: Snippet<[RadioOption]>;
|
|
18
21
|
class?: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { canonicalTone, type Tone } from '../../tone';
|
|
2
3
|
// Section / group header: heading + optional subtitle, icon, count, hue
|
|
3
4
|
// swatch and right-aligned actions on one wrapping row. `divider` underlines
|
|
4
5
|
// it, `sticky` pins it under the app header and publishes its height as
|
|
@@ -11,7 +12,6 @@
|
|
|
11
12
|
import Icon, { type IconName } from '../atoms/Icon.svelte';
|
|
12
13
|
import Text from '../atoms/Text.svelte';
|
|
13
14
|
|
|
14
|
-
type Tone = 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
|
|
15
15
|
type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
|
16
16
|
|
|
17
17
|
let {
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
bind:this={el}
|
|
81
81
|
bind:offsetHeight={height}
|
|
82
82
|
data-tsu="SectionHeader"
|
|
83
|
-
class="section-header sh-{tone} {klass}"
|
|
83
|
+
class="section-header sh-{canonicalTone(tone)} {klass}"
|
|
84
84
|
class:sh-divider={divider}
|
|
85
85
|
class:sh-group={variant === 'group'}
|
|
86
86
|
class:sh-sticky={sticky}
|
|
@@ -150,6 +150,9 @@
|
|
|
150
150
|
.sh-info {
|
|
151
151
|
--sh-tone: var(--info);
|
|
152
152
|
}
|
|
153
|
+
.sh-accent {
|
|
154
|
+
--sh-tone: var(--accent);
|
|
155
|
+
}
|
|
153
156
|
.sh-row {
|
|
154
157
|
display: flex;
|
|
155
158
|
align-items: center;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { type Tone } from '../../tone';
|
|
1
2
|
import type { Snippet } from 'svelte';
|
|
2
3
|
import { type IconName } from '../atoms/Icon.svelte';
|
|
3
|
-
type Tone = 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
|
|
4
4
|
type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
|
5
5
|
type $$ComponentProps = {
|
|
6
6
|
title?: string;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
</script>
|
|
14
14
|
|
|
15
15
|
<script lang="ts">
|
|
16
|
+
import type { ControlSize } from '../../size';
|
|
16
17
|
// Single-select segmented control. One token-driven row of options covering
|
|
17
18
|
// the two repeated redesign controls:
|
|
18
19
|
// • filter pills — label + optional count badge ("All 742 / Missing 120")
|
|
@@ -27,11 +28,15 @@
|
|
|
27
28
|
let {
|
|
28
29
|
options,
|
|
29
30
|
value = $bindable(),
|
|
31
|
+
onchange,
|
|
32
|
+
justify,
|
|
33
|
+
wrap = false,
|
|
30
34
|
// `icon` packs the segments into compact square buttons (label hidden as an
|
|
31
35
|
// accessible name only); `pill` keeps the labelled filter-pill layout.
|
|
32
36
|
variant = 'pill',
|
|
33
37
|
size = 'md',
|
|
34
38
|
control = false,
|
|
39
|
+
box = false,
|
|
35
40
|
label = 'Options',
|
|
36
41
|
// `mobile` hides segment labels below the mobile breakpoint, collapsing
|
|
37
42
|
// labelled segments to centered icon-only squares (aria-label preserved).
|
|
@@ -46,11 +51,20 @@
|
|
|
46
51
|
}: {
|
|
47
52
|
options: SegmentOption[];
|
|
48
53
|
value?: string;
|
|
54
|
+
/** Fires after a selection (click or keyboard). */
|
|
55
|
+
onchange?: (value: string) => void;
|
|
56
|
+
/** Pin the control to the start/end of a flex row. */
|
|
57
|
+
justify?: 'start' | 'end';
|
|
58
|
+
/** Allow segments to wrap onto multiple lines. */
|
|
59
|
+
wrap?: boolean;
|
|
49
60
|
variant?: 'pill' | 'icon';
|
|
50
|
-
size?:
|
|
61
|
+
size?: ControlSize;
|
|
51
62
|
/** Adopt the shared `--control-height` toolbar contract (like Popover/Button
|
|
52
63
|
* `control`) so the whole segmented control height-matches its siblings. */
|
|
53
64
|
control?: boolean;
|
|
65
|
+
/** Adopt `--control-height-default` so an icon toggle lines up with
|
|
66
|
+
* `Button square` / `box` siblings. */
|
|
67
|
+
box?: boolean;
|
|
54
68
|
label?: string;
|
|
55
69
|
collapseLabels?: 'never' | 'mobile' | 'container';
|
|
56
70
|
/** Single non-wrapping row that scrolls horizontally (scrollbar hidden). */
|
|
@@ -75,6 +89,7 @@
|
|
|
75
89
|
function select(val: string, focus = false) {
|
|
76
90
|
if (options.find((o) => o.value === val)?.disabled) return;
|
|
77
91
|
value = val;
|
|
92
|
+
onchange?.(val);
|
|
78
93
|
if (focus) {
|
|
79
94
|
queueMicrotask(() =>
|
|
80
95
|
listEl?.querySelector<HTMLButtonElement>(`#${baseId}-${val}`)?.focus()
|
|
@@ -97,6 +112,10 @@
|
|
|
97
112
|
tabindex="-1"
|
|
98
113
|
class="seg seg-{variant} seg-{size} {klass}"
|
|
99
114
|
class:seg-control={control}
|
|
115
|
+
class:seg-box={box}
|
|
116
|
+
class:seg-wrap={wrap}
|
|
117
|
+
class:justify-start={justify === 'start'}
|
|
118
|
+
class:justify-end={justify === 'end'}
|
|
100
119
|
class:seg-collapse-mobile={collapseLabels === 'mobile'}
|
|
101
120
|
class:collapse-container={collapseLabels === 'container'}
|
|
102
121
|
class:seg-scroll={scroll}
|
|
@@ -143,6 +162,15 @@
|
|
|
143
162
|
.seg-icon {
|
|
144
163
|
border-radius: var(--r-md);
|
|
145
164
|
}
|
|
165
|
+
.seg-wrap {
|
|
166
|
+
flex-wrap: wrap;
|
|
167
|
+
}
|
|
168
|
+
.justify-start {
|
|
169
|
+
margin-inline-end: auto;
|
|
170
|
+
}
|
|
171
|
+
.justify-end {
|
|
172
|
+
margin-inline-start: auto;
|
|
173
|
+
}
|
|
146
174
|
.seg-scroll {
|
|
147
175
|
flex: 1 0 100%;
|
|
148
176
|
flex-wrap: nowrap;
|
|
@@ -206,6 +234,13 @@
|
|
|
206
234
|
.seg-control .seg-item {
|
|
207
235
|
height: 100%;
|
|
208
236
|
}
|
|
237
|
+
.seg-box {
|
|
238
|
+
height: var(--control-height-default);
|
|
239
|
+
min-height: var(--control-height-default);
|
|
240
|
+
}
|
|
241
|
+
.seg-box .seg-item {
|
|
242
|
+
height: 100%;
|
|
243
|
+
}
|
|
209
244
|
.seg-icon .seg-item {
|
|
210
245
|
gap: 0;
|
|
211
246
|
padding: 0.35rem;
|
|
@@ -9,15 +9,25 @@ export interface SegmentOption {
|
|
|
9
9
|
/** Greyed out, not selectable, skipped by keyboard navigation. */
|
|
10
10
|
disabled?: boolean;
|
|
11
11
|
}
|
|
12
|
+
import type { ControlSize } from '../../size';
|
|
12
13
|
import type { Snippet } from 'svelte';
|
|
13
14
|
type $$ComponentProps = {
|
|
14
15
|
options: SegmentOption[];
|
|
15
16
|
value?: string;
|
|
17
|
+
/** Fires after a selection (click or keyboard). */
|
|
18
|
+
onchange?: (value: string) => void;
|
|
19
|
+
/** Pin the control to the start/end of a flex row. */
|
|
20
|
+
justify?: 'start' | 'end';
|
|
21
|
+
/** Allow segments to wrap onto multiple lines. */
|
|
22
|
+
wrap?: boolean;
|
|
16
23
|
variant?: 'pill' | 'icon';
|
|
17
|
-
size?:
|
|
24
|
+
size?: ControlSize;
|
|
18
25
|
/** Adopt the shared `--control-height` toolbar contract (like Popover/Button
|
|
19
26
|
* `control`) so the whole segmented control height-matches its siblings. */
|
|
20
27
|
control?: boolean;
|
|
28
|
+
/** Adopt `--control-height-default` so an icon toggle lines up with
|
|
29
|
+
* `Button square` / `box` siblings. */
|
|
30
|
+
box?: boolean;
|
|
21
31
|
label?: string;
|
|
22
32
|
collapseLabels?: 'never' | 'mobile' | 'container';
|
|
23
33
|
/** Single non-wrapping row that scrolls horizontally (scrollbar hidden). */
|
|
@@ -4,13 +4,19 @@
|
|
|
4
4
|
// affordance. Used by the header font-size + theme pickers and the drawer
|
|
5
5
|
// font-size picker (CCT-250 #5 / CCT-297 #11). Composes the Button primitive
|
|
6
6
|
// for the base look.
|
|
7
|
+
import type { Snippet } from 'svelte';
|
|
7
8
|
import Button from '../atoms/Button.svelte';
|
|
9
|
+
import Icon, { type IconName } from '../atoms/Icon.svelte';
|
|
8
10
|
import Select from '../atoms/Select.svelte';
|
|
9
11
|
|
|
10
12
|
type Option = { value: string; label: string };
|
|
11
13
|
|
|
12
14
|
let {
|
|
13
15
|
glyph,
|
|
16
|
+
icon,
|
|
17
|
+
iconChildren,
|
|
18
|
+
variant = 'square',
|
|
19
|
+
active = false,
|
|
14
20
|
label,
|
|
15
21
|
title,
|
|
16
22
|
value,
|
|
@@ -19,12 +25,30 @@
|
|
|
19
25
|
onchange,
|
|
20
26
|
box,
|
|
21
27
|
hitArea = 'auto',
|
|
28
|
+
grow = false,
|
|
29
|
+
shrink = true,
|
|
30
|
+
block = false,
|
|
22
31
|
class: klass = '',
|
|
23
32
|
...rest
|
|
24
33
|
}: {
|
|
25
34
|
// Visible content of the button (a letter like "A" or an emoji icon).
|
|
26
|
-
glyph
|
|
35
|
+
glyph?: string;
|
|
36
|
+
/** Registry glyph, used instead of `glyph`. */
|
|
37
|
+
icon?: IconName;
|
|
38
|
+
/** Raw SVG children for an off-registry glyph. */
|
|
39
|
+
iconChildren?: Snippet;
|
|
40
|
+
/** `row` is a full-width, left-aligned row showing `label` beside the glyph
|
|
41
|
+
* (overflow menus); `square` is the compact icon box. */
|
|
42
|
+
variant?: 'square' | 'row';
|
|
43
|
+
/** Accent tint when the picker's current value is "on". */
|
|
44
|
+
active?: boolean;
|
|
27
45
|
label: string;
|
|
46
|
+
/** Fill the free space of a flex row (`flex: 1 1 0`). */
|
|
47
|
+
grow?: boolean;
|
|
48
|
+
/** `false` pins the box (`flex: none`) so a flex row cannot squeeze it. */
|
|
49
|
+
shrink?: boolean;
|
|
50
|
+
/** Full-width block. */
|
|
51
|
+
block?: boolean;
|
|
28
52
|
title?: string;
|
|
29
53
|
value: string;
|
|
30
54
|
// Flat option list. Ignored when `groups` is given.
|
|
@@ -43,9 +67,21 @@
|
|
|
43
67
|
|
|
44
68
|
<!-- The wrapper (owned here, so styled scoped) is the positioning context that
|
|
45
69
|
clips the transparent overlaid <select>; the icon Button shows through. -->
|
|
46
|
-
<span
|
|
47
|
-
|
|
48
|
-
|
|
70
|
+
<span
|
|
71
|
+
class="select-button {klass}"
|
|
72
|
+
class:row={variant === 'row'}
|
|
73
|
+
class:active
|
|
74
|
+
data-tsu="SelectButton"
|
|
75
|
+
{...rest}
|
|
76
|
+
class:grow={grow}
|
|
77
|
+
class:no-shrink={!shrink}
|
|
78
|
+
class:block={block || variant === 'row'}
|
|
79
|
+
>
|
|
80
|
+
<Button variant="ghost" icon={variant === 'square'} block={variant === 'row'} {box} {hitArea} {title} aria-label={label}>
|
|
81
|
+
<span aria-hidden="true" class="sb-glyph">
|
|
82
|
+
{#if iconChildren}<Icon>{@render iconChildren()}</Icon>{:else if icon}<Icon name={icon} />{:else}{glyph}{/if}
|
|
83
|
+
</span>
|
|
84
|
+
{#if variant === 'row'}<span aria-hidden="true" class="sb-label">{label}</span>{/if}
|
|
49
85
|
<Select
|
|
50
86
|
variant="ghost"
|
|
51
87
|
aria-label={label}
|
|
@@ -70,6 +106,17 @@
|
|
|
70
106
|
</span>
|
|
71
107
|
|
|
72
108
|
<style>
|
|
109
|
+
.grow {
|
|
110
|
+
flex: 1 1 0;
|
|
111
|
+
min-width: 0;
|
|
112
|
+
}
|
|
113
|
+
.no-shrink {
|
|
114
|
+
flex: none;
|
|
115
|
+
}
|
|
116
|
+
.block {
|
|
117
|
+
display: flex;
|
|
118
|
+
width: 100%;
|
|
119
|
+
}
|
|
73
120
|
.select-button {
|
|
74
121
|
position: relative;
|
|
75
122
|
display: inline-flex;
|
|
@@ -77,4 +124,17 @@
|
|
|
77
124
|
border-radius: var(--r-md);
|
|
78
125
|
font-weight: var(--fw-bold);
|
|
79
126
|
}
|
|
127
|
+
.select-button.active {
|
|
128
|
+
color: var(--accent);
|
|
129
|
+
}
|
|
130
|
+
.select-button.row :global(.btn) {
|
|
131
|
+
justify-content: flex-start;
|
|
132
|
+
gap: var(--sp-2);
|
|
133
|
+
width: 100%;
|
|
134
|
+
font-weight: var(--fw-medium);
|
|
135
|
+
}
|
|
136
|
+
.sb-glyph {
|
|
137
|
+
display: inline-flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
}
|
|
80
140
|
</style>
|
|
@@ -1,10 +1,27 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import { type IconName } from '../atoms/Icon.svelte';
|
|
1
3
|
type Option = {
|
|
2
4
|
value: string;
|
|
3
5
|
label: string;
|
|
4
6
|
};
|
|
5
7
|
type $$ComponentProps = {
|
|
6
|
-
glyph
|
|
8
|
+
glyph?: string;
|
|
9
|
+
/** Registry glyph, used instead of `glyph`. */
|
|
10
|
+
icon?: IconName;
|
|
11
|
+
/** Raw SVG children for an off-registry glyph. */
|
|
12
|
+
iconChildren?: Snippet;
|
|
13
|
+
/** `row` is a full-width, left-aligned row showing `label` beside the glyph
|
|
14
|
+
* (overflow menus); `square` is the compact icon box. */
|
|
15
|
+
variant?: 'square' | 'row';
|
|
16
|
+
/** Accent tint when the picker's current value is "on". */
|
|
17
|
+
active?: boolean;
|
|
7
18
|
label: string;
|
|
19
|
+
/** Fill the free space of a flex row (`flex: 1 1 0`). */
|
|
20
|
+
grow?: boolean;
|
|
21
|
+
/** `false` pins the box (`flex: none`) so a flex row cannot squeeze it. */
|
|
22
|
+
shrink?: boolean;
|
|
23
|
+
/** Full-width block. */
|
|
24
|
+
block?: boolean;
|
|
8
25
|
title?: string;
|
|
9
26
|
value: string;
|
|
10
27
|
options?: Option[];
|
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
icon?: import('../atoms/Icon.svelte').IconName;
|
|
6
6
|
/** Greyed out, not selectable, skipped by keyboard navigation. */
|
|
7
7
|
disabled?: boolean;
|
|
8
|
+
/** Trailing count badge. */
|
|
9
|
+
count?: number | string;
|
|
8
10
|
}
|
|
9
11
|
</script>
|
|
10
12
|
|
|
11
13
|
<script lang="ts">
|
|
14
|
+
import Badge from '../atoms/Badge.svelte';
|
|
12
15
|
// WAI-ARIA tabs. A `tablist` of `tab`s controlling a single `tabpanel`.
|
|
13
16
|
// Follows the automatic-activation pattern: ←/→ (and Home/End) move selection
|
|
14
17
|
// and reveal the panel in one step; roving tabindex keeps exactly one tab in
|
|
@@ -21,12 +24,20 @@
|
|
|
21
24
|
tabs,
|
|
22
25
|
value = $bindable(),
|
|
23
26
|
label = 'Tabs',
|
|
24
|
-
panel
|
|
27
|
+
panel,
|
|
28
|
+
class: klass = '',
|
|
29
|
+
style: styleProp = '',
|
|
30
|
+
panelClass = '',
|
|
31
|
+
panelPadding = 'md',
|
|
25
32
|
}: {
|
|
26
33
|
tabs: TabItem[];
|
|
27
34
|
value?: string;
|
|
28
35
|
label?: string;
|
|
29
36
|
panel: Snippet<[string]>;
|
|
37
|
+
class?: string;
|
|
38
|
+
style?: string;
|
|
39
|
+
panelClass?: string;
|
|
40
|
+
panelPadding?: 'none' | 'sm' | 'md';
|
|
30
41
|
} = $props();
|
|
31
42
|
|
|
32
43
|
// Default to the first selectable tab when no value is supplied.
|
|
@@ -73,7 +84,7 @@
|
|
|
73
84
|
}
|
|
74
85
|
</script>
|
|
75
86
|
|
|
76
|
-
<div class="tabs" data-tsu="Tabs">
|
|
87
|
+
<div class="tabs {klass}" style={styleProp} data-tsu="Tabs">
|
|
77
88
|
<div bind:this={listEl} role="tablist" aria-label={label} tabindex="-1" class="tablist" {onkeydown}>
|
|
78
89
|
{#each tabs as t (t.id)}
|
|
79
90
|
<button
|
|
@@ -90,10 +101,11 @@
|
|
|
90
101
|
>
|
|
91
102
|
{#if t.icon}<Icon name={t.icon} />{/if}
|
|
92
103
|
<span>{t.label}</span>
|
|
104
|
+
{#if t.count !== undefined}<Badge size="sm" tone={value === t.id ? 'accent' : 'neutral'}>{t.count}</Badge>{/if}
|
|
93
105
|
</button>
|
|
94
106
|
{/each}
|
|
95
107
|
</div>
|
|
96
|
-
<div role="tabpanel" id="{baseId}-panel" tabindex="0" class="tabpanel">
|
|
108
|
+
<div role="tabpanel" id="{baseId}-panel" tabindex="0" class="tabpanel {panelClass}" class:pad-none={panelPadding === 'none'} class:pad-sm={panelPadding === 'sm'}>
|
|
97
109
|
{#if value !== undefined}{@render panel(value)}{/if}
|
|
98
110
|
</div>
|
|
99
111
|
</div>
|
|
@@ -139,6 +151,12 @@
|
|
|
139
151
|
.tabpanel {
|
|
140
152
|
padding-top: var(--sp-4);
|
|
141
153
|
}
|
|
154
|
+
.tabpanel.pad-sm {
|
|
155
|
+
padding-top: var(--sp-2);
|
|
156
|
+
}
|
|
157
|
+
.tabpanel.pad-none {
|
|
158
|
+
padding-top: 0;
|
|
159
|
+
}
|
|
142
160
|
.tabpanel:focus-visible {
|
|
143
161
|
outline: 2px solid var(--accent);
|
|
144
162
|
outline-offset: 2px;
|
|
@@ -4,6 +4,8 @@ export interface TabItem {
|
|
|
4
4
|
icon?: import('../atoms/Icon.svelte').IconName;
|
|
5
5
|
/** Greyed out, not selectable, skipped by keyboard navigation. */
|
|
6
6
|
disabled?: boolean;
|
|
7
|
+
/** Trailing count badge. */
|
|
8
|
+
count?: number | string;
|
|
7
9
|
}
|
|
8
10
|
import type { Snippet } from 'svelte';
|
|
9
11
|
type $$ComponentProps = {
|
|
@@ -11,6 +13,10 @@ type $$ComponentProps = {
|
|
|
11
13
|
value?: string;
|
|
12
14
|
label?: string;
|
|
13
15
|
panel: Snippet<[string]>;
|
|
16
|
+
class?: string;
|
|
17
|
+
style?: string;
|
|
18
|
+
panelClass?: string;
|
|
19
|
+
panelPadding?: 'none' | 'sm' | 'md';
|
|
14
20
|
};
|
|
15
21
|
declare const Tabs: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
16
22
|
type Tabs = ReturnType<typeof Tabs>;
|
|
@@ -39,7 +39,10 @@
|
|
|
39
39
|
mono = false,
|
|
40
40
|
tone = 'muted',
|
|
41
41
|
size,
|
|
42
|
-
|
|
42
|
+
short = false,
|
|
43
|
+
tickMs = 30_000,
|
|
44
|
+
class: klass = '',
|
|
45
|
+
style: styleProp = '',
|
|
43
46
|
}: {
|
|
44
47
|
/** The instant: a Date, epoch milliseconds, or an ISO/parseable string.
|
|
45
48
|
* null/undefined (or unparseable input) renders an inert "—". */
|
|
@@ -68,8 +71,12 @@
|
|
|
68
71
|
/** Font size, mirroring <Text> sizes (maps to --fs-* tokens). Omit to
|
|
69
72
|
* inherit the surrounding size. */
|
|
70
73
|
size?: Size;
|
|
74
|
+
/** Relative mode without the "ago" suffix ("6m", "2h", "3d"). */
|
|
75
|
+
short?: boolean;
|
|
71
76
|
/** How often relative mode re-renders so "3m ago" stays fresh. */
|
|
72
77
|
tickMs?: number;
|
|
78
|
+
class?: string;
|
|
79
|
+
style?: string;
|
|
73
80
|
} = $props();
|
|
74
81
|
|
|
75
82
|
// User's in-popover choice overrides the `mode` prop; until they pick, we
|
|
@@ -88,11 +95,11 @@
|
|
|
88
95
|
});
|
|
89
96
|
|
|
90
97
|
const date = $derived(toDate(value));
|
|
91
|
-
const label = $derived(formatTimestamp(value, current, now, utc, precision));
|
|
98
|
+
const label = $derived(formatTimestamp(value, current, now, utc, precision, short));
|
|
92
99
|
// datetime= wants a valid ISO string; omit it entirely on bad input.
|
|
93
100
|
const machine = $derived(date ? toISO(date) : undefined);
|
|
94
101
|
const showPopover = $derived(details || selectable);
|
|
95
|
-
const cls = $derived(`ts tone-${tone}${mono ? ' mono' : ''}`);
|
|
102
|
+
const cls = $derived(`ts tone-${tone}${mono ? ' mono' : ''} ${klass}`);
|
|
96
103
|
// Size maps straight onto the --fs-* scale; null leaves font-size unset so the
|
|
97
104
|
// timestamp inherits its surrounding run.
|
|
98
105
|
const sizeVar = $derived(size ? `var(--fs-${size})` : null);
|
|
@@ -121,11 +128,11 @@
|
|
|
121
128
|
|
|
122
129
|
{#if !date}
|
|
123
130
|
<!-- Unparseable input: degrade to an inert dash rather than empty text. -->
|
|
124
|
-
<time class="{cls} ts-invalid" data-tsu="Timestamp" style:font-size={sizeVar}>—</time>
|
|
131
|
+
<time class="{cls} ts-invalid" data-tsu="Timestamp" style={styleProp} style:font-size={sizeVar}>—</time>
|
|
125
132
|
{:else if showPopover}
|
|
126
133
|
<Popover label="Timestamp details" placement="bottom-start" bare>
|
|
127
134
|
{#snippet trigger()}
|
|
128
|
-
<time class="{cls} ts-trigger" data-tsu="Timestamp" style:font-size={sizeVar} datetime={machine}>{label}</time>
|
|
135
|
+
<time class="{cls} ts-trigger" data-tsu="Timestamp" style={styleProp} style:font-size={sizeVar} datetime={machine}>{label}</time>
|
|
129
136
|
{/snippet}
|
|
130
137
|
<div class="ts-panel">
|
|
131
138
|
{#if selectable}
|
|
@@ -152,7 +159,7 @@
|
|
|
152
159
|
</div>
|
|
153
160
|
</Popover>
|
|
154
161
|
{:else}
|
|
155
|
-
<time class={cls} data-tsu="Timestamp" style:font-size={sizeVar} datetime={machine}>{label}</time>
|
|
162
|
+
<time class={cls} data-tsu="Timestamp" style={styleProp} style:font-size={sizeVar} datetime={machine}>{label}</time>
|
|
156
163
|
{/if}
|
|
157
164
|
|
|
158
165
|
<style>
|
|
@@ -29,8 +29,12 @@ type $$ComponentProps = {
|
|
|
29
29
|
/** Font size, mirroring <Text> sizes (maps to --fs-* tokens). Omit to
|
|
30
30
|
* inherit the surrounding size. */
|
|
31
31
|
size?: Size;
|
|
32
|
+
/** Relative mode without the "ago" suffix ("6m", "2h", "3d"). */
|
|
33
|
+
short?: boolean;
|
|
32
34
|
/** How often relative mode re-renders so "3m ago" stays fresh. */
|
|
33
35
|
tickMs?: number;
|
|
36
|
+
class?: string;
|
|
37
|
+
style?: string;
|
|
34
38
|
};
|
|
35
39
|
declare const Timestamp: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
36
40
|
type Timestamp = ReturnType<typeof Timestamp>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
// Renders the toast queue
|
|
3
|
-
//
|
|
2
|
+
// Renders the toast queue bottom-centered in two live regions: polite for
|
|
3
|
+
// ok/info/neutral, assertive (role=alert) for errors. Mount once near the
|
|
4
|
+
// app root. The stack lives in a manual popover so it paints in the
|
|
4
5
|
// top layer above an open Modal <dialog>; without the Popover API the
|
|
5
6
|
// attribute is ignored and the fixed/z-index positioning applies instead.
|
|
6
7
|
import { toasts } from '../../stores/toast.svelte';
|
|
@@ -8,7 +9,11 @@
|
|
|
8
9
|
import Card from '../atoms/Card.svelte';
|
|
9
10
|
import Icon from '../atoms/Icon.svelte';
|
|
10
11
|
|
|
12
|
+
let { class: klass = '', style: styleProp = '' }: { class?: string; style?: string } = $props();
|
|
13
|
+
|
|
11
14
|
let el: HTMLDivElement | undefined = $state();
|
|
15
|
+
const polite = $derived(toasts.items.filter((t) => t.tone !== 'error'));
|
|
16
|
+
const assertive = $derived(toasts.items.filter((t) => t.tone === 'error'));
|
|
12
17
|
|
|
13
18
|
$effect(() => {
|
|
14
19
|
if (!el || typeof el.showPopover !== 'function') return;
|
|
@@ -19,46 +24,51 @@
|
|
|
19
24
|
});
|
|
20
25
|
</script>
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
aria-live="polite"
|
|
28
|
-
aria-relevant="additions"
|
|
29
|
-
data-tsu="Toaster"
|
|
30
|
-
>
|
|
31
|
-
{#each toasts.items as t (t.id)}
|
|
32
|
-
{#if t.action}
|
|
33
|
-
<Card
|
|
34
|
-
as="div"
|
|
35
|
-
surface="raised"
|
|
36
|
-
class="toast"
|
|
37
|
-
tone={t.tone === 'ok' ? 'ok' : t.tone === 'error' ? 'danger' : t.tone === 'info' ? 'info' : undefined}
|
|
38
|
-
>
|
|
39
|
-
<button type="button" class="toast-text" onclick={() => toasts.dismiss(t.id)}>
|
|
40
|
-
{#if t.tone === 'ok'}<Icon name="check" />{:else if t.tone === 'error'}<Icon name="warning" />{:else if t.tone === 'info'}<Icon name="info" />{/if}
|
|
41
|
-
<span class="msg">{t.message}</span>
|
|
42
|
-
</button>
|
|
43
|
-
<Button size="sm" variant="ghost" loading={t.pending} onclick={() => toasts.act(t.id)}>
|
|
44
|
-
{t.action.label}
|
|
45
|
-
</Button>
|
|
46
|
-
</Card>
|
|
47
|
-
{:else}
|
|
48
|
-
<Card
|
|
49
|
-
as="button"
|
|
50
|
-
surface="raised"
|
|
51
|
-
class="toast"
|
|
52
|
-
tone={t.tone === 'ok' ? 'ok' : t.tone === 'error' ? 'danger' : t.tone === 'info' ? 'info' : undefined}
|
|
27
|
+
{#snippet toast(t: (typeof toasts.items)[number])}
|
|
28
|
+
{@const tone = t.tone === 'ok' ? 'ok' : t.tone === 'error' ? 'danger' : t.tone === 'info' ? 'info' : undefined}
|
|
29
|
+
{#if t.action}
|
|
30
|
+
<Card as="div" surface="raised" class="toast" {tone}>
|
|
31
|
+
<button
|
|
53
32
|
type="button"
|
|
33
|
+
class="toast-text"
|
|
34
|
+
aria-label="Dismiss: {t.message}"
|
|
54
35
|
onclick={() => toasts.dismiss(t.id)}
|
|
55
36
|
>
|
|
56
|
-
|
|
37
|
+
<span aria-hidden="true" class="toast-icon">
|
|
38
|
+
{#if t.tone === 'ok'}<Icon name="check" />{:else if t.tone === 'error'}<Icon name="warning" />{:else if t.tone === 'info'}<Icon name="info" />{/if}
|
|
39
|
+
</span>
|
|
57
40
|
<span class="msg">{t.message}</span>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
41
|
+
</button>
|
|
42
|
+
<Button size="sm" variant="ghost" loading={t.pending} onclick={() => toasts.act(t.id)}>
|
|
43
|
+
{t.action.label}
|
|
44
|
+
</Button>
|
|
45
|
+
</Card>
|
|
46
|
+
{:else}
|
|
47
|
+
<Card
|
|
48
|
+
as="button"
|
|
49
|
+
surface="raised"
|
|
50
|
+
class="toast"
|
|
51
|
+
{tone}
|
|
52
|
+
type="button"
|
|
53
|
+
aria-label="Dismiss: {t.message}"
|
|
54
|
+
onclick={() => toasts.dismiss(t.id)}
|
|
55
|
+
>
|
|
56
|
+
<span aria-hidden="true" class="toast-icon">
|
|
57
|
+
{#if t.tone === 'ok'}<Icon name="check" />{:else if t.tone === 'error'}<Icon name="warning" />{:else if t.tone === 'info'}<Icon name="info" />{/if}
|
|
58
|
+
</span>
|
|
59
|
+
<span class="msg">{t.message}</span>
|
|
60
|
+
<span aria-hidden="true" class="toast-icon"><Icon name="x" /></span>
|
|
61
|
+
</Card>
|
|
62
|
+
{/if}
|
|
63
|
+
{/snippet}
|
|
64
|
+
|
|
65
|
+
<div class="toaster {klass}" style={styleProp} bind:this={el} popover="manual" data-tsu="Toaster">
|
|
66
|
+
<div class="region" role="status" aria-live="polite">
|
|
67
|
+
{#each polite as t (t.id)}{@render toast(t)}{/each}
|
|
68
|
+
</div>
|
|
69
|
+
<div class="region" role="alert" aria-live="assertive">
|
|
70
|
+
{#each assertive as t (t.id)}{@render toast(t)}{/each}
|
|
71
|
+
</div>
|
|
62
72
|
</div>
|
|
63
73
|
|
|
64
74
|
<style>
|
|
@@ -84,6 +94,12 @@
|
|
|
84
94
|
.toaster:not(:popover-open) {
|
|
85
95
|
display: none;
|
|
86
96
|
}
|
|
97
|
+
.region {
|
|
98
|
+
display: contents;
|
|
99
|
+
}
|
|
100
|
+
.toast-icon {
|
|
101
|
+
display: inline-flex;
|
|
102
|
+
}
|
|
87
103
|
.toaster :global(.toast) {
|
|
88
104
|
pointer-events: auto;
|
|
89
105
|
display: flex;
|