@dorsk/tsumikit 0.47.0 → 0.49.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 +5 -1
- package/dist/cap-bar.d.ts +6 -1
- package/dist/cap-bar.js +20 -1
- package/dist/components/atoms/Card.svelte +14 -2
- package/dist/components/atoms/Card.svelte.d.ts +2 -0
- package/dist/components/atoms/Gauge.svelte +10 -5
- package/dist/components/atoms/Gauge.svelte.d.ts +1 -0
- package/dist/components/molecules/CapBar.svelte +150 -16
- package/dist/components/molecules/CapBar.svelte.d.ts +20 -4
- package/dist/components/molecules/Carousel.svelte +276 -0
- package/dist/components/molecules/Carousel.svelte.d.ts +59 -0
- package/dist/components/molecules/Fieldset.svelte +15 -5
- package/dist/components/molecules/Fieldset.svelte.d.ts +7 -1
- package/dist/components/molecules/Popover.svelte +99 -7
- package/dist/components/molecules/Popover.svelte.d.ts +9 -0
- package/dist/components/molecules/carousel-keyboard.d.ts +44 -0
- package/dist/components/molecules/carousel-keyboard.js +63 -0
- package/dist/components/molecules/popover-hover.d.ts +27 -0
- package/dist/components/molecules/popover-hover.js +62 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -181,7 +181,11 @@ EmojiPicker (popover with a searchable EN+FR glyph catalogue, grouped tabs, rovi
|
|
|
181
181
|
GitRef (branch chip + PR link tinted by state + `+N −N` diff; `collapse`
|
|
182
182
|
auto/never/glyph degrades to icons inside a narrow `.cq` container),
|
|
183
183
|
CapBar (consumption track with a draggable, keyboard-steppable cap handle that
|
|
184
|
-
shows a `N%` bubble while it is moved; `oninput` live, `onchange` on commit
|
|
184
|
+
shows a `N%` bubble while it is moved; `oninput` live, `onchange` on commit;
|
|
185
|
+
`marker`/`markerLabel`/`markerTone` draw a pace rule, `cap: null` plus
|
|
186
|
+
`clearAtMax`/`uncappedLabel` express "no cap", `layout`/`stackBelow` put the
|
|
187
|
+
readout above a full-width track, `capLabel`/`capValueText` localise the ARIA
|
|
188
|
+
strings),
|
|
185
189
|
Drawer (side-panel `<dialog>`: `side`, `width` clamped to the viewport, full-screen
|
|
186
190
|
under 48rem; `nav` page column that turns into a horizontal strip on narrow
|
|
187
191
|
screens, or `navMobile`; sticky `footer`; Escape / scrim / close button all close),
|
package/dist/cap-bar.d.ts
CHANGED
|
@@ -5,4 +5,9 @@ export declare function capFromPointer(clientX: number, rect: {
|
|
|
5
5
|
width: number;
|
|
6
6
|
}, step?: number, min?: number, max?: number): number;
|
|
7
7
|
export declare function capKeyStep(key: string, shift: boolean, current: number, step?: number, min?: number, max?: number): number | null;
|
|
8
|
-
export declare function capTone(value: number, cap: number, warnAt?: number): 'ok' | 'warn' | 'danger';
|
|
8
|
+
export declare function capTone(value: number, cap: number | null, warnAt?: number): 'ok' | 'warn' | 'danger';
|
|
9
|
+
export declare function capPercent(cap: number | null, min?: number, max?: number): number;
|
|
10
|
+
export declare function markerPercent(marker: number | null | undefined, min?: number, max?: number): number | null;
|
|
11
|
+
/** With `clearAtMax`, a cap dragged to `max` means "no cap" rather than a cap of `max`. */
|
|
12
|
+
export declare function resolveCap(next: number, clearAtMax?: boolean, max?: number): number | null;
|
|
13
|
+
export declare function isStacked(layout: 'row' | 'stacked', width?: number, limit?: number): boolean;
|
package/dist/cap-bar.js
CHANGED
|
@@ -35,9 +35,28 @@ export function capKeyStep(key, shift, current, step = 5, min = 0, max = 100) {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
export function capTone(value, cap, warnAt = 75) {
|
|
38
|
-
if (value >= cap)
|
|
38
|
+
if (cap !== null && value >= cap)
|
|
39
39
|
return 'danger';
|
|
40
40
|
if (value >= warnAt)
|
|
41
41
|
return 'warn';
|
|
42
42
|
return 'ok';
|
|
43
43
|
}
|
|
44
|
+
export function capPercent(cap, min = 0, max = 100) {
|
|
45
|
+
if (cap === null)
|
|
46
|
+
return 100;
|
|
47
|
+
return clampCap(((cap - min) / (max - min || 1)) * 100, 0, 100);
|
|
48
|
+
}
|
|
49
|
+
export function markerPercent(marker, min = 0, max = 100) {
|
|
50
|
+
if (marker == null || Number.isNaN(marker))
|
|
51
|
+
return null;
|
|
52
|
+
return clampCap(((marker - min) / (max - min || 1)) * 100, 0, 100);
|
|
53
|
+
}
|
|
54
|
+
/** With `clearAtMax`, a cap dragged to `max` means "no cap" rather than a cap of `max`. */
|
|
55
|
+
export function resolveCap(next, clearAtMax = false, max = 100) {
|
|
56
|
+
return clearAtMax && next >= max ? null : next;
|
|
57
|
+
}
|
|
58
|
+
export function isStacked(layout, width = 0, limit = 0) {
|
|
59
|
+
if (layout === 'stacked')
|
|
60
|
+
return true;
|
|
61
|
+
return limit > 0 && width > 0 && width < limit;
|
|
62
|
+
}
|
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
as?: 'div' | 'button' | 'a' | 'li' | 'section' | 'form';
|
|
41
41
|
padding?: 'none' | 'sm' | 'md' | 'lg';
|
|
42
42
|
surface?: 'base' | 'raised' | 'sunken';
|
|
43
|
+
/** Frame border style; `none` drops the frame. Applies to the stacked layers too. */
|
|
44
|
+
border?: 'solid' | 'dashed' | 'none';
|
|
43
45
|
tone?: Tone;
|
|
44
46
|
stacked?: boolean;
|
|
45
47
|
stackTone?: Tone;
|
|
@@ -65,6 +67,7 @@
|
|
|
65
67
|
as = 'div',
|
|
66
68
|
padding = 'md',
|
|
67
69
|
surface = 'base',
|
|
70
|
+
border = 'solid',
|
|
68
71
|
tone = 'neutral',
|
|
69
72
|
stacked = false,
|
|
70
73
|
stackTone = 'neutral',
|
|
@@ -121,6 +124,8 @@
|
|
|
121
124
|
class:pad-lg={padding === 'lg'}
|
|
122
125
|
class:surface-raised={surface === 'raised'}
|
|
123
126
|
class:surface-sunken={surface === 'sunken'}
|
|
127
|
+
class:border-dashed={border === 'dashed'}
|
|
128
|
+
class:border-none={border === 'none'}
|
|
124
129
|
class:card-tap={tap || interactive}
|
|
125
130
|
class:card-max={maxWidth !== undefined}
|
|
126
131
|
class:card-ok={t === 'ok'}
|
|
@@ -168,7 +173,8 @@
|
|
|
168
173
|
--card-pad: var(--sp-4);
|
|
169
174
|
min-width: 0;
|
|
170
175
|
background: var(--bg-elevated);
|
|
171
|
-
border:
|
|
176
|
+
--card-border-style: solid;
|
|
177
|
+
border: 1px var(--card-border-style) var(--border);
|
|
172
178
|
border-radius: var(--r-lg);
|
|
173
179
|
padding: var(--card-pad);
|
|
174
180
|
}
|
|
@@ -200,6 +206,12 @@
|
|
|
200
206
|
.surface-sunken {
|
|
201
207
|
background: var(--bg-elevated-2);
|
|
202
208
|
}
|
|
209
|
+
.border-dashed {
|
|
210
|
+
--card-border-style: dashed;
|
|
211
|
+
}
|
|
212
|
+
.border-none {
|
|
213
|
+
--card-border-style: none;
|
|
214
|
+
}
|
|
203
215
|
.pad-none {
|
|
204
216
|
--card-pad: 0;
|
|
205
217
|
padding: 0;
|
|
@@ -286,7 +298,7 @@
|
|
|
286
298
|
/* Opaque fill so each layer fully hides the one behind it — only the
|
|
287
299
|
bottom-right peek (and its single border line) stays visible. */
|
|
288
300
|
background: var(--stack-bg);
|
|
289
|
-
border: 1px
|
|
301
|
+
border: 1px var(--card-border-style) var(--stack-border);
|
|
290
302
|
}
|
|
291
303
|
/* Nearest back layer — sits just under the front surface. */
|
|
292
304
|
.card-stacked::before {
|
|
@@ -13,6 +13,8 @@ type Own = {
|
|
|
13
13
|
as?: 'div' | 'button' | 'a' | 'li' | 'section' | 'form';
|
|
14
14
|
padding?: 'none' | 'sm' | 'md' | 'lg';
|
|
15
15
|
surface?: 'base' | 'raised' | 'sunken';
|
|
16
|
+
/** Frame border style; `none` drops the frame. Applies to the stacked layers too. */
|
|
17
|
+
border?: 'solid' | 'dashed' | 'none';
|
|
16
18
|
tone?: Tone;
|
|
17
19
|
stacked?: boolean;
|
|
18
20
|
stackTone?: Tone;
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
dangerAt?: number;
|
|
35
35
|
label?: string;
|
|
36
36
|
as?: 'div' | 'a' | 'button';
|
|
37
|
+
href?: string;
|
|
37
38
|
width?: string;
|
|
38
39
|
height?: string;
|
|
39
40
|
corner?: Snippet;
|
|
@@ -61,6 +62,10 @@
|
|
|
61
62
|
const resolvedTone = $derived(tone ?? gaugeTone(pct, warnAt, dangerAt));
|
|
62
63
|
const lit = $derived(litSegments(pct, segments));
|
|
63
64
|
const segmentCount = $derived(Math.max(0, Math.floor(segments)));
|
|
65
|
+
const isMeter = $derived(as === 'div');
|
|
66
|
+
const ariaLabel = $derived(
|
|
67
|
+
isMeter ? label : label ? `${label} (${Math.round(pct)}%)` : `${Math.round(pct)}%`
|
|
68
|
+
);
|
|
64
69
|
</script>
|
|
65
70
|
|
|
66
71
|
<svelte:element
|
|
@@ -69,11 +74,11 @@
|
|
|
69
74
|
class="gauge tone-{resolvedTone} {klass}"
|
|
70
75
|
class:interactive={as !== 'div'}
|
|
71
76
|
type={as === 'button' ? 'button' : undefined}
|
|
72
|
-
role=
|
|
73
|
-
aria-label={
|
|
74
|
-
aria-valuemin={0}
|
|
75
|
-
aria-valuemax={100}
|
|
76
|
-
aria-valuenow={pct}
|
|
77
|
+
role={isMeter ? 'meter' : undefined}
|
|
78
|
+
aria-label={ariaLabel}
|
|
79
|
+
aria-valuemin={isMeter ? 0 : undefined}
|
|
80
|
+
aria-valuemax={isMeter ? 100 : undefined}
|
|
81
|
+
aria-valuenow={isMeter ? pct : undefined}
|
|
77
82
|
style:--gauge-w={width}
|
|
78
83
|
style:--gauge-h={height}
|
|
79
84
|
{style}
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
capFromPointer,
|
|
5
|
+
capKeyStep,
|
|
6
|
+
capPercent,
|
|
7
|
+
capTone,
|
|
8
|
+
clampCap,
|
|
9
|
+
isStacked,
|
|
10
|
+
markerPercent,
|
|
11
|
+
resolveCap,
|
|
12
|
+
snapCap
|
|
13
|
+
} from '../../cap-bar';
|
|
14
|
+
import { browser } from '../../env';
|
|
4
15
|
|
|
5
16
|
let {
|
|
6
17
|
value = 0,
|
|
@@ -10,6 +21,15 @@
|
|
|
10
21
|
step = 5,
|
|
11
22
|
warnAt = 75,
|
|
12
23
|
size = 'md',
|
|
24
|
+
layout = 'row',
|
|
25
|
+
stackBelow,
|
|
26
|
+
marker,
|
|
27
|
+
markerLabel,
|
|
28
|
+
markerTone = 'neutral',
|
|
29
|
+
uncappedLabel = 'no cap',
|
|
30
|
+
clearAtMax = false,
|
|
31
|
+
capLabel,
|
|
32
|
+
capValueText,
|
|
13
33
|
label,
|
|
14
34
|
labelWidth = '96px',
|
|
15
35
|
readout,
|
|
@@ -25,8 +45,8 @@
|
|
|
25
45
|
}: {
|
|
26
46
|
/** Consumption, in percent of `max`. */
|
|
27
47
|
value?: number;
|
|
28
|
-
/** Cap position; bindable, moved by drag/keyboard. */
|
|
29
|
-
cap?: number;
|
|
48
|
+
/** Cap position; bindable, moved by drag/keyboard. `null` is uncapped. */
|
|
49
|
+
cap?: number | null;
|
|
30
50
|
min?: number;
|
|
31
51
|
max?: number;
|
|
32
52
|
/** Snap increment for drag and keyboard. */
|
|
@@ -34,6 +54,22 @@
|
|
|
34
54
|
/** Consumption at which the fill turns `--warn`. */
|
|
35
55
|
warnAt?: number;
|
|
36
56
|
size?: 'md' | 'lg';
|
|
57
|
+
/** `'stacked'` puts label + readout on a line above a full-width track. */
|
|
58
|
+
layout?: 'row' | 'stacked';
|
|
59
|
+
/** Own-width threshold (px/em/rem) below which `layout` becomes `'stacked'`. */
|
|
60
|
+
stackBelow?: string;
|
|
61
|
+
/** Reference position on the track, in the same units as `value`. */
|
|
62
|
+
marker?: number | null;
|
|
63
|
+
markerLabel?: string;
|
|
64
|
+
markerTone?: 'neutral' | 'ok' | 'warn' | 'danger';
|
|
65
|
+
/** Spoken and readout text for `cap === null`. */
|
|
66
|
+
uncappedLabel?: string;
|
|
67
|
+
/** Moving the cap to `max` yields `null` instead of `max`. */
|
|
68
|
+
clearAtMax?: boolean;
|
|
69
|
+
/** Accessible name of the cap handle. Defaults to `${label} cap`. */
|
|
70
|
+
capLabel?: string;
|
|
71
|
+
/** Spoken value of the cap handle. Defaults to `cap ${cap}%`. */
|
|
72
|
+
capValueText?: string | ((cap: number | null) => string);
|
|
37
73
|
label?: string | Snippet;
|
|
38
74
|
labelWidth?: string;
|
|
39
75
|
/** Overrides the default `{value}% · {hint}` readout. */
|
|
@@ -45,30 +81,64 @@
|
|
|
45
81
|
tooltip?: string;
|
|
46
82
|
readonly?: boolean;
|
|
47
83
|
/** Fires live while dragging or stepping. */
|
|
48
|
-
oninput?: (cap: number) => void;
|
|
84
|
+
oninput?: (cap: number | null) => void;
|
|
49
85
|
/** Fires once the cap is committed (pointer release / key-up). */
|
|
50
|
-
onchange?: (cap: number) => void;
|
|
86
|
+
onchange?: (cap: number | null) => void;
|
|
51
87
|
class?: string;
|
|
52
88
|
style?: string;
|
|
53
89
|
} = $props();
|
|
54
90
|
|
|
91
|
+
const uncapped = $derived(cap === null);
|
|
55
92
|
const range = $derived(max - min || 1);
|
|
56
93
|
const pct = $derived(clampCap(((value - min) / range) * 100, 0, 100));
|
|
57
|
-
const capPct = $derived(
|
|
94
|
+
const capPct = $derived(capPercent(cap, min, max));
|
|
95
|
+
const markerPct = $derived(markerPercent(marker, min, max));
|
|
58
96
|
const tone = $derived(capTone(value, cap, warnAt));
|
|
59
97
|
const readoutText = $derived(hint ? `${value}% · ${hint}` : `${value}%`);
|
|
60
|
-
const
|
|
98
|
+
const capText = $derived(uncapped ? uncappedLabel : `cap ${cap}%`);
|
|
99
|
+
const tip = $derived(tooltip ?? `${capText} — drag the bar`);
|
|
61
100
|
const readoutTitle = $derived(readout === undefined ? readoutText : typeof readout === 'string' ? readout : undefined);
|
|
62
|
-
const ariaLabel = $derived(typeof label === 'string' ? `${label} cap` : 'cap');
|
|
101
|
+
const ariaLabel = $derived(capLabel ?? (typeof label === 'string' ? `${label} cap` : 'cap'));
|
|
102
|
+
const ariaValueText = $derived(
|
|
103
|
+
capValueText === undefined ? capText : typeof capValueText === 'string' ? capValueText : capValueText(cap)
|
|
104
|
+
);
|
|
63
105
|
|
|
106
|
+
const uid = $props.id();
|
|
107
|
+
const markerId = $derived(markerLabel ? `${uid}-marker` : undefined);
|
|
108
|
+
|
|
109
|
+
let root = $state<HTMLElement | null>(null);
|
|
64
110
|
let trackEl = $state<HTMLDivElement | null>(null);
|
|
111
|
+
let width = $state(0);
|
|
112
|
+
let limit = $state(0);
|
|
65
113
|
let dragging = $state(false);
|
|
66
114
|
let keying = $state(false);
|
|
67
115
|
let committed = cap;
|
|
68
116
|
|
|
117
|
+
const stacked = $derived(isStacked(layout, width, limit));
|
|
118
|
+
|
|
119
|
+
function toPx(length: string, el: HTMLElement): number {
|
|
120
|
+
const n = Number.parseFloat(length);
|
|
121
|
+
const fontSize = (node: Element) => Number.parseFloat(getComputedStyle(node).fontSize);
|
|
122
|
+
if (length.endsWith('rem')) return n * fontSize(document.documentElement);
|
|
123
|
+
if (length.endsWith('em')) return n * fontSize(el);
|
|
124
|
+
return n;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
$effect(() => {
|
|
128
|
+
if (!browser || !root || !stackBelow) return;
|
|
129
|
+
const el = root;
|
|
130
|
+
limit = toPx(stackBelow, el);
|
|
131
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
132
|
+
width = entry.contentRect.width;
|
|
133
|
+
});
|
|
134
|
+
observer.observe(el);
|
|
135
|
+
return () => observer.disconnect();
|
|
136
|
+
});
|
|
137
|
+
|
|
69
138
|
function setCap(next: number) {
|
|
70
|
-
|
|
71
|
-
cap
|
|
139
|
+
const resolved = resolveCap(next, clearAtMax, max);
|
|
140
|
+
if (resolved === cap) return;
|
|
141
|
+
cap = resolved;
|
|
72
142
|
oninput?.(cap);
|
|
73
143
|
}
|
|
74
144
|
function commit() {
|
|
@@ -96,25 +166,29 @@
|
|
|
96
166
|
}
|
|
97
167
|
function keyDown(e: KeyboardEvent) {
|
|
98
168
|
if (readonly) return;
|
|
99
|
-
const next = capKeyStep(e.key, e.shiftKey, snapCap(cap, step, min, max), step, min, max);
|
|
169
|
+
const next = capKeyStep(e.key, e.shiftKey, snapCap(cap ?? max, step, min, max), step, min, max);
|
|
100
170
|
if (next === null) return;
|
|
101
171
|
e.preventDefault();
|
|
102
172
|
keying = true;
|
|
103
173
|
setCap(next);
|
|
104
174
|
}
|
|
105
175
|
function keyUp(e: KeyboardEvent) {
|
|
106
|
-
if (readonly || capKeyStep(e.key, e.shiftKey, cap, step, min, max) === null) return;
|
|
176
|
+
if (readonly || capKeyStep(e.key, e.shiftKey, cap ?? max, step, min, max) === null) return;
|
|
107
177
|
commit();
|
|
108
178
|
}
|
|
109
179
|
</script>
|
|
110
180
|
|
|
111
181
|
<div
|
|
112
182
|
data-tsu="CapBar"
|
|
183
|
+
bind:this={root}
|
|
113
184
|
class="cap-bar size-{size} tone-{tone} {klass}"
|
|
114
185
|
class:readonly
|
|
115
186
|
class:dragging
|
|
116
187
|
class:keying
|
|
117
|
-
|
|
188
|
+
class:stacked
|
|
189
|
+
class:uncapped
|
|
190
|
+
style="--label-w: {labelWidth}; --readout-w: {readoutWidth}; --pct: {pct}%; --cap: {capPct}%; --marker: {markerPct ??
|
|
191
|
+
0}%; {styleProp}"
|
|
118
192
|
>
|
|
119
193
|
{#if label}
|
|
120
194
|
<div class="label">
|
|
@@ -132,6 +206,12 @@
|
|
|
132
206
|
onpointercancel={pointerUp}
|
|
133
207
|
>
|
|
134
208
|
<div class="fill"></div>
|
|
209
|
+
{#if markerPct !== null}
|
|
210
|
+
<div class="marker marker-{markerTone}" title={markerLabel} aria-hidden="true"></div>
|
|
211
|
+
{#if markerLabel}
|
|
212
|
+
<span class="sr-only" id={markerId}>{markerLabel}</span>
|
|
213
|
+
{/if}
|
|
214
|
+
{/if}
|
|
135
215
|
<div
|
|
136
216
|
class="handle"
|
|
137
217
|
role="slider"
|
|
@@ -139,14 +219,15 @@
|
|
|
139
219
|
aria-label={ariaLabel}
|
|
140
220
|
aria-valuemin={min}
|
|
141
221
|
aria-valuemax={max}
|
|
142
|
-
aria-valuenow={cap}
|
|
143
|
-
aria-valuetext=
|
|
222
|
+
aria-valuenow={cap ?? max}
|
|
223
|
+
aria-valuetext={ariaValueText}
|
|
224
|
+
aria-describedby={markerId}
|
|
144
225
|
aria-disabled={readonly ? 'true' : undefined}
|
|
145
226
|
onkeydown={keyDown}
|
|
146
227
|
onkeyup={keyUp}
|
|
147
228
|
onblur={() => (keying = false)}
|
|
148
229
|
></div>
|
|
149
|
-
<div class="bubble" aria-hidden="true">{
|
|
230
|
+
<div class="bubble" aria-hidden="true">{capText}</div>
|
|
150
231
|
</div>
|
|
151
232
|
<div class="readout" title={readoutTitle}>
|
|
152
233
|
{#if readout === undefined}{readoutText}{:else if typeof readout === 'string'}{readout}{:else}{@render readout()}{/if}
|
|
@@ -213,6 +294,41 @@
|
|
|
213
294
|
transition: width 0.2s var(--ease);
|
|
214
295
|
z-index: 1;
|
|
215
296
|
}
|
|
297
|
+
.uncapped .track::after {
|
|
298
|
+
display: none;
|
|
299
|
+
}
|
|
300
|
+
.marker {
|
|
301
|
+
position: absolute;
|
|
302
|
+
top: 50%;
|
|
303
|
+
left: var(--marker);
|
|
304
|
+
width: 2px;
|
|
305
|
+
height: calc(var(--track-h) + 6px);
|
|
306
|
+
transform: translate(-50%, -50%);
|
|
307
|
+
border-radius: 1px;
|
|
308
|
+
background: var(--marker-color, var(--text-muted));
|
|
309
|
+
pointer-events: none;
|
|
310
|
+
z-index: 2;
|
|
311
|
+
}
|
|
312
|
+
.marker-ok {
|
|
313
|
+
--marker-color: var(--ok);
|
|
314
|
+
}
|
|
315
|
+
.marker-warn {
|
|
316
|
+
--marker-color: var(--warn);
|
|
317
|
+
}
|
|
318
|
+
.marker-danger {
|
|
319
|
+
--marker-color: var(--danger);
|
|
320
|
+
}
|
|
321
|
+
.sr-only {
|
|
322
|
+
position: absolute;
|
|
323
|
+
width: 1px;
|
|
324
|
+
height: 1px;
|
|
325
|
+
margin: -1px;
|
|
326
|
+
padding: 0;
|
|
327
|
+
overflow: hidden;
|
|
328
|
+
clip-path: inset(50%);
|
|
329
|
+
white-space: nowrap;
|
|
330
|
+
border: 0;
|
|
331
|
+
}
|
|
216
332
|
.handle {
|
|
217
333
|
position: absolute;
|
|
218
334
|
top: 50%;
|
|
@@ -226,6 +342,9 @@
|
|
|
226
342
|
cursor: ew-resize;
|
|
227
343
|
z-index: 2;
|
|
228
344
|
}
|
|
345
|
+
.uncapped .handle {
|
|
346
|
+
opacity: 0.4;
|
|
347
|
+
}
|
|
229
348
|
.handle:focus-visible {
|
|
230
349
|
outline: 2px solid var(--accent);
|
|
231
350
|
outline-offset: 2px;
|
|
@@ -283,6 +402,21 @@
|
|
|
283
402
|
.cap-bar:not(:has(.label)) .caption {
|
|
284
403
|
grid-column: 1 / 3;
|
|
285
404
|
}
|
|
405
|
+
.cap-bar.stacked {
|
|
406
|
+
grid-template-columns: 1fr auto;
|
|
407
|
+
}
|
|
408
|
+
.cap-bar.stacked .label {
|
|
409
|
+
grid-area: 1 / 1;
|
|
410
|
+
}
|
|
411
|
+
.cap-bar.stacked .readout {
|
|
412
|
+
grid-area: 1 / 2;
|
|
413
|
+
}
|
|
414
|
+
.cap-bar.stacked .track {
|
|
415
|
+
grid-area: 2 / 1 / 3 / -1;
|
|
416
|
+
}
|
|
417
|
+
.cap-bar.stacked .caption {
|
|
418
|
+
grid-column: 1 / -1;
|
|
419
|
+
}
|
|
286
420
|
@media (prefers-reduced-motion: reduce) {
|
|
287
421
|
.fill,
|
|
288
422
|
.bubble {
|
|
@@ -2,8 +2,8 @@ import type { Snippet } from 'svelte';
|
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
/** Consumption, in percent of `max`. */
|
|
4
4
|
value?: number;
|
|
5
|
-
/** Cap position; bindable, moved by drag/keyboard. */
|
|
6
|
-
cap?: number;
|
|
5
|
+
/** Cap position; bindable, moved by drag/keyboard. `null` is uncapped. */
|
|
6
|
+
cap?: number | null;
|
|
7
7
|
min?: number;
|
|
8
8
|
max?: number;
|
|
9
9
|
/** Snap increment for drag and keyboard. */
|
|
@@ -11,6 +11,22 @@ type $$ComponentProps = {
|
|
|
11
11
|
/** Consumption at which the fill turns `--warn`. */
|
|
12
12
|
warnAt?: number;
|
|
13
13
|
size?: 'md' | 'lg';
|
|
14
|
+
/** `'stacked'` puts label + readout on a line above a full-width track. */
|
|
15
|
+
layout?: 'row' | 'stacked';
|
|
16
|
+
/** Own-width threshold (px/em/rem) below which `layout` becomes `'stacked'`. */
|
|
17
|
+
stackBelow?: string;
|
|
18
|
+
/** Reference position on the track, in the same units as `value`. */
|
|
19
|
+
marker?: number | null;
|
|
20
|
+
markerLabel?: string;
|
|
21
|
+
markerTone?: 'neutral' | 'ok' | 'warn' | 'danger';
|
|
22
|
+
/** Spoken and readout text for `cap === null`. */
|
|
23
|
+
uncappedLabel?: string;
|
|
24
|
+
/** Moving the cap to `max` yields `null` instead of `max`. */
|
|
25
|
+
clearAtMax?: boolean;
|
|
26
|
+
/** Accessible name of the cap handle. Defaults to `${label} cap`. */
|
|
27
|
+
capLabel?: string;
|
|
28
|
+
/** Spoken value of the cap handle. Defaults to `cap ${cap}%`. */
|
|
29
|
+
capValueText?: string | ((cap: number | null) => string);
|
|
14
30
|
label?: string | Snippet;
|
|
15
31
|
labelWidth?: string;
|
|
16
32
|
/** Overrides the default `{value}% · {hint}` readout. */
|
|
@@ -22,9 +38,9 @@ type $$ComponentProps = {
|
|
|
22
38
|
tooltip?: string;
|
|
23
39
|
readonly?: boolean;
|
|
24
40
|
/** Fires live while dragging or stepping. */
|
|
25
|
-
oninput?: (cap: number) => void;
|
|
41
|
+
oninput?: (cap: number | null) => void;
|
|
26
42
|
/** Fires once the cap is committed (pointer release / key-up). */
|
|
27
|
-
onchange?: (cap: number) => void;
|
|
43
|
+
onchange?: (cap: number | null) => void;
|
|
28
44
|
class?: string;
|
|
29
45
|
style?: string;
|
|
30
46
|
};
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
<script lang="ts" generics="T">
|
|
2
|
+
/**
|
|
3
|
+
* Ordered full-bleed panels stepped with prev/next, dot indicators, arrow
|
|
4
|
+
* keys and horizontal swipe.
|
|
5
|
+
*
|
|
6
|
+
* ARIA follows the WAI-ARIA carousel pattern in its *group* flavour: the
|
|
7
|
+
* root is a region with `aria-roledescription="carousel"`, every slide is a
|
|
8
|
+
* `group` with `aria-roledescription="slide"` named "N of M", and the dots
|
|
9
|
+
* are plain buttons. The tablist flavour was rejected because it makes the
|
|
10
|
+
* dots the primary navigation and turns each slide into a `tabpanel`, which
|
|
11
|
+
* is the wrong model when prev/next is the main affordance and the panels
|
|
12
|
+
* are unlabelled content rather than named destinations. The slide container
|
|
13
|
+
* is a polite live region while the carousel is not auto-rotating.
|
|
14
|
+
*/
|
|
15
|
+
import type { Snippet } from 'svelte';
|
|
16
|
+
import IconButton from './IconButton.svelte';
|
|
17
|
+
import { clampSlide, nextSlideForKey, stepSlide, swipeStep } from './carousel-keyboard.js';
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
slides,
|
|
21
|
+
slide,
|
|
22
|
+
index = $bindable(0),
|
|
23
|
+
onchange,
|
|
24
|
+
loop = false,
|
|
25
|
+
autoplay = 0,
|
|
26
|
+
dots = true,
|
|
27
|
+
controls = true,
|
|
28
|
+
counter = false,
|
|
29
|
+
label = 'Carousel',
|
|
30
|
+
class: klass = '',
|
|
31
|
+
style: styleProp = '',
|
|
32
|
+
slideClass = ''
|
|
33
|
+
}: {
|
|
34
|
+
/** One entry per panel. Entries may themselves be snippets when no `slide` renderer is given. */
|
|
35
|
+
slides: T[];
|
|
36
|
+
/** Renders one panel from its entry and position. */
|
|
37
|
+
slide?: Snippet<[T, number]>;
|
|
38
|
+
/** Active panel; bindable. Clamped (or wrapped with `loop`) into range. */
|
|
39
|
+
index?: number;
|
|
40
|
+
onchange?: (index: number) => void;
|
|
41
|
+
/** Wrap from the last panel to the first and back. */
|
|
42
|
+
loop?: boolean;
|
|
43
|
+
/** Milliseconds between automatic advances. `0` (default) disables it. When
|
|
44
|
+
* on, rotation pauses on hover and focus and a play/pause control appears. */
|
|
45
|
+
autoplay?: number;
|
|
46
|
+
dots?: boolean;
|
|
47
|
+
controls?: boolean;
|
|
48
|
+
/** Show an `N / M` position counter. */
|
|
49
|
+
counter?: boolean;
|
|
50
|
+
label?: string;
|
|
51
|
+
class?: string;
|
|
52
|
+
style?: string;
|
|
53
|
+
slideClass?: string;
|
|
54
|
+
} = $props();
|
|
55
|
+
|
|
56
|
+
const count = $derived(slides.length);
|
|
57
|
+
const baseId = `carousel-${Math.random().toString(36).slice(2, 8)}`;
|
|
58
|
+
|
|
59
|
+
$effect(() => {
|
|
60
|
+
const clamped = clampSlide(index, count, false);
|
|
61
|
+
if (clamped !== index) index = clamped;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
let rootEl = $state<HTMLElement | null>(null);
|
|
65
|
+
|
|
66
|
+
function go(next: number, focusDot = false) {
|
|
67
|
+
const target = clampSlide(next, count, loop);
|
|
68
|
+
if (target === index) return;
|
|
69
|
+
index = target;
|
|
70
|
+
onchange?.(target);
|
|
71
|
+
if (focusDot) {
|
|
72
|
+
queueMicrotask(() =>
|
|
73
|
+
rootEl?.querySelector<HTMLButtonElement>(`#${baseId}-dot-${target}`)?.focus()
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const prev = () => go(stepSlide(index, count, -1, loop));
|
|
78
|
+
const next = () => go(stepSlide(index, count, 1, loop));
|
|
79
|
+
const atStart = $derived(!loop && index <= 0);
|
|
80
|
+
const atEnd = $derived(!loop && index >= count - 1);
|
|
81
|
+
|
|
82
|
+
function onkeydown(e: KeyboardEvent) {
|
|
83
|
+
const tag = (e.target as HTMLElement | null)?.tagName;
|
|
84
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
|
|
85
|
+
const target = nextSlideForKey(index, count, e.key, loop);
|
|
86
|
+
if (target === undefined) return;
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
const inDots = (e.target as HTMLElement | null)?.closest('.dots') !== null;
|
|
89
|
+
go(target, inDots);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let startX = 0;
|
|
93
|
+
let startY = 0;
|
|
94
|
+
let pointerId: number | null = null;
|
|
95
|
+
function onpointerdown(e: PointerEvent) {
|
|
96
|
+
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
|
97
|
+
pointerId = e.pointerId;
|
|
98
|
+
startX = e.clientX;
|
|
99
|
+
startY = e.clientY;
|
|
100
|
+
}
|
|
101
|
+
function onpointerup(e: PointerEvent) {
|
|
102
|
+
if (e.pointerId !== pointerId) return;
|
|
103
|
+
pointerId = null;
|
|
104
|
+
const step = swipeStep(e.clientX - startX, e.clientY - startY);
|
|
105
|
+
if (step) go(stepSlide(index, count, step, loop));
|
|
106
|
+
}
|
|
107
|
+
function onpointercancel() {
|
|
108
|
+
pointerId = null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let playing = $derived(autoplay > 0);
|
|
112
|
+
let hovered = $state(false);
|
|
113
|
+
let focused = $state(false);
|
|
114
|
+
const rotating = $derived(playing && autoplay > 0 && count > 1 && !hovered && !focused);
|
|
115
|
+
$effect(() => {
|
|
116
|
+
if (!rotating) return;
|
|
117
|
+
const id = setInterval(() => go(stepSlide(index, count, 1, true)), autoplay);
|
|
118
|
+
return () => clearInterval(id);
|
|
119
|
+
});
|
|
120
|
+
</script>
|
|
121
|
+
|
|
122
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
123
|
+
<section
|
|
124
|
+
bind:this={rootEl}
|
|
125
|
+
class="carousel {klass}"
|
|
126
|
+
style={styleProp}
|
|
127
|
+
aria-roledescription="carousel"
|
|
128
|
+
aria-label={label}
|
|
129
|
+
data-tsu="Carousel"
|
|
130
|
+
{onkeydown}
|
|
131
|
+
onpointerenter={() => (hovered = true)}
|
|
132
|
+
onpointerleave={() => (hovered = false)}
|
|
133
|
+
onfocusin={() => (focused = true)}
|
|
134
|
+
onfocusout={(e) => {
|
|
135
|
+
if (!rootEl?.contains(e.relatedTarget as Node | null)) focused = false;
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
139
|
+
<div
|
|
140
|
+
class="viewport"
|
|
141
|
+
aria-live={rotating ? 'off' : 'polite'}
|
|
142
|
+
{onpointerdown}
|
|
143
|
+
{onpointerup}
|
|
144
|
+
{onpointercancel}
|
|
145
|
+
>
|
|
146
|
+
<div class="track" style:transform="translateX(-{index * 100}%)">
|
|
147
|
+
{#each slides as item, i (i)}
|
|
148
|
+
<div
|
|
149
|
+
class="slide {slideClass}"
|
|
150
|
+
role="group"
|
|
151
|
+
aria-roledescription="slide"
|
|
152
|
+
aria-label="{i + 1} of {count}"
|
|
153
|
+
aria-hidden={i !== index}
|
|
154
|
+
inert={i !== index}
|
|
155
|
+
id="{baseId}-slide-{i}"
|
|
156
|
+
>
|
|
157
|
+
{#if slide}
|
|
158
|
+
{@render slide(item, i)}
|
|
159
|
+
{:else}
|
|
160
|
+
{@render (item as unknown as Snippet)()}
|
|
161
|
+
{/if}
|
|
162
|
+
</div>
|
|
163
|
+
{/each}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
{#if count > 1 && (controls || dots || counter || autoplay > 0)}
|
|
168
|
+
<div class="bar">
|
|
169
|
+
{#if controls}
|
|
170
|
+
<IconButton icon="chevron-left" label="Previous slide" variant="ghost" box="sm" disabled={atStart} onclick={prev} />
|
|
171
|
+
{/if}
|
|
172
|
+
{#if dots}
|
|
173
|
+
<div class="dots" role="group" aria-label="Choose slide">
|
|
174
|
+
{#each slides as _, i (i)}
|
|
175
|
+
<button
|
|
176
|
+
type="button"
|
|
177
|
+
class="dot"
|
|
178
|
+
class:active={i === index}
|
|
179
|
+
id="{baseId}-dot-{i}"
|
|
180
|
+
aria-label="Slide {i + 1}"
|
|
181
|
+
aria-current={i === index ? 'true' : undefined}
|
|
182
|
+
aria-controls="{baseId}-slide-{i}"
|
|
183
|
+
tabindex={i === index ? 0 : -1}
|
|
184
|
+
onclick={() => go(i)}
|
|
185
|
+
></button>
|
|
186
|
+
{/each}
|
|
187
|
+
</div>
|
|
188
|
+
{/if}
|
|
189
|
+
{#if counter}
|
|
190
|
+
<span class="counter">{index + 1} / {count}</span>
|
|
191
|
+
{/if}
|
|
192
|
+
{#if autoplay > 0}
|
|
193
|
+
<IconButton
|
|
194
|
+
icon={playing ? 'pause' : 'play'}
|
|
195
|
+
label={playing ? 'Stop automatic rotation' : 'Start automatic rotation'}
|
|
196
|
+
variant="ghost"
|
|
197
|
+
box="sm"
|
|
198
|
+
pressed={playing}
|
|
199
|
+
onclick={() => (playing = !playing)}
|
|
200
|
+
/>
|
|
201
|
+
{/if}
|
|
202
|
+
{#if controls}
|
|
203
|
+
<IconButton icon="chevron-right" label="Next slide" variant="ghost" box="sm" disabled={atEnd} onclick={next} />
|
|
204
|
+
{/if}
|
|
205
|
+
</div>
|
|
206
|
+
{/if}
|
|
207
|
+
</section>
|
|
208
|
+
|
|
209
|
+
<style>
|
|
210
|
+
.carousel {
|
|
211
|
+
display: flex;
|
|
212
|
+
flex-direction: column;
|
|
213
|
+
gap: var(--sp-3);
|
|
214
|
+
min-width: 0;
|
|
215
|
+
}
|
|
216
|
+
.viewport {
|
|
217
|
+
overflow: hidden;
|
|
218
|
+
border-radius: var(--r-md);
|
|
219
|
+
touch-action: pan-y;
|
|
220
|
+
user-select: none;
|
|
221
|
+
}
|
|
222
|
+
.track {
|
|
223
|
+
display: flex;
|
|
224
|
+
width: 100%;
|
|
225
|
+
transition: transform 0.35s var(--ease);
|
|
226
|
+
}
|
|
227
|
+
.slide {
|
|
228
|
+
flex: 0 0 100%;
|
|
229
|
+
min-width: 0;
|
|
230
|
+
}
|
|
231
|
+
.bar {
|
|
232
|
+
display: flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
justify-content: center;
|
|
235
|
+
gap: var(--sp-2);
|
|
236
|
+
}
|
|
237
|
+
.dots {
|
|
238
|
+
display: flex;
|
|
239
|
+
align-items: center;
|
|
240
|
+
gap: var(--sp-2);
|
|
241
|
+
}
|
|
242
|
+
.dot {
|
|
243
|
+
width: 0.5rem;
|
|
244
|
+
height: 0.5rem;
|
|
245
|
+
padding: 0;
|
|
246
|
+
border: 0;
|
|
247
|
+
border-radius: var(--r-pill);
|
|
248
|
+
background: var(--border);
|
|
249
|
+
cursor: pointer;
|
|
250
|
+
transition:
|
|
251
|
+
background 0.12s var(--ease),
|
|
252
|
+
transform 0.12s var(--ease);
|
|
253
|
+
}
|
|
254
|
+
.dot:hover {
|
|
255
|
+
background: var(--text-faint);
|
|
256
|
+
}
|
|
257
|
+
.dot.active {
|
|
258
|
+
background: var(--accent);
|
|
259
|
+
transform: scale(1.25);
|
|
260
|
+
}
|
|
261
|
+
.dot:focus-visible {
|
|
262
|
+
outline: 2px solid var(--accent);
|
|
263
|
+
outline-offset: 2px;
|
|
264
|
+
}
|
|
265
|
+
.counter {
|
|
266
|
+
color: var(--text-muted);
|
|
267
|
+
font-size: var(--fs-sm);
|
|
268
|
+
font-variant-numeric: tabular-nums;
|
|
269
|
+
}
|
|
270
|
+
@media (prefers-reduced-motion: reduce) {
|
|
271
|
+
.track,
|
|
272
|
+
.dot {
|
|
273
|
+
transition: none;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
</style>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordered full-bleed panels stepped with prev/next, dot indicators, arrow
|
|
3
|
+
* keys and horizontal swipe.
|
|
4
|
+
*
|
|
5
|
+
* ARIA follows the WAI-ARIA carousel pattern in its *group* flavour: the
|
|
6
|
+
* root is a region with `aria-roledescription="carousel"`, every slide is a
|
|
7
|
+
* `group` with `aria-roledescription="slide"` named "N of M", and the dots
|
|
8
|
+
* are plain buttons. The tablist flavour was rejected because it makes the
|
|
9
|
+
* dots the primary navigation and turns each slide into a `tabpanel`, which
|
|
10
|
+
* is the wrong model when prev/next is the main affordance and the panels
|
|
11
|
+
* are unlabelled content rather than named destinations. The slide container
|
|
12
|
+
* is a polite live region while the carousel is not auto-rotating.
|
|
13
|
+
*/
|
|
14
|
+
import type { Snippet } from 'svelte';
|
|
15
|
+
declare function $$render<T>(): {
|
|
16
|
+
props: {
|
|
17
|
+
/** One entry per panel. Entries may themselves be snippets when no `slide` renderer is given. */
|
|
18
|
+
slides: T[];
|
|
19
|
+
/** Renders one panel from its entry and position. */
|
|
20
|
+
slide?: Snippet<[T, number]>;
|
|
21
|
+
/** Active panel; bindable. Clamped (or wrapped with `loop`) into range. */
|
|
22
|
+
index?: number;
|
|
23
|
+
onchange?: (index: number) => void;
|
|
24
|
+
/** Wrap from the last panel to the first and back. */
|
|
25
|
+
loop?: boolean;
|
|
26
|
+
/** Milliseconds between automatic advances. `0` (default) disables it. When
|
|
27
|
+
* on, rotation pauses on hover and focus and a play/pause control appears. */
|
|
28
|
+
autoplay?: number;
|
|
29
|
+
dots?: boolean;
|
|
30
|
+
controls?: boolean;
|
|
31
|
+
/** Show an `N / M` position counter. */
|
|
32
|
+
counter?: boolean;
|
|
33
|
+
label?: string;
|
|
34
|
+
class?: string;
|
|
35
|
+
style?: string;
|
|
36
|
+
slideClass?: string;
|
|
37
|
+
};
|
|
38
|
+
exports: {};
|
|
39
|
+
bindings: "index";
|
|
40
|
+
slots: {};
|
|
41
|
+
events: {};
|
|
42
|
+
};
|
|
43
|
+
declare class __sveltets_Render<T> {
|
|
44
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
45
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
46
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
47
|
+
bindings(): "index";
|
|
48
|
+
exports(): {};
|
|
49
|
+
}
|
|
50
|
+
interface $$IsomorphicComponent {
|
|
51
|
+
new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
52
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
53
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
54
|
+
<T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
55
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
56
|
+
}
|
|
57
|
+
declare const Carousel: $$IsomorphicComponent;
|
|
58
|
+
type Carousel<T> = InstanceType<typeof Carousel<T>>;
|
|
59
|
+
export default Carousel;
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// Bordered zone whose legend rides the top border. Real <fieldset>/<legend>
|
|
3
3
|
// for semantics. `droppable` turns it into an HTML5 drop target: a hovering
|
|
4
4
|
// drag that passes `accepts` highlights the zone and shows `dropHint`;
|
|
5
|
-
// `ondrop` receives the payload read from `dataTransfer` under `mime`.
|
|
5
|
+
// `ondrop` receives the payload read from `dataTransfer` under `mime`. That payload
|
|
6
|
+
// is unreadable while hovering (HTML protected mode), so per-item acceptance rules
|
|
7
|
+
// need `dragPayload` or the `types` handed to `accepts`.
|
|
6
8
|
// Keyboard users cannot drag — the consumer must offer a non-drag path
|
|
7
9
|
// (a move menu, a select) to the same action. Pointer-based DnD can drive
|
|
8
10
|
// the highlight through the bindable `over` prop.
|
|
@@ -18,6 +20,7 @@
|
|
|
18
20
|
droppable = false,
|
|
19
21
|
mime = 'text/plain',
|
|
20
22
|
accepts,
|
|
23
|
+
dragPayload = '',
|
|
21
24
|
ondrop,
|
|
22
25
|
dropHint,
|
|
23
26
|
over = $bindable(false),
|
|
@@ -33,7 +36,13 @@
|
|
|
33
36
|
padding?: 'sm' | 'md' | 'lg';
|
|
34
37
|
droppable?: boolean;
|
|
35
38
|
mime?: string;
|
|
36
|
-
|
|
39
|
+
/** `data` is the dataTransfer payload on drop, but during dragenter/dragover the
|
|
40
|
+
* dataTransfer is in protected mode and `getData` returns `''` — there `data` is
|
|
41
|
+
* `dragPayload` when given, otherwise `''`, and `types` is the only other signal. */
|
|
42
|
+
accepts?: (data: string, e: DragEvent, types: readonly string[]) => boolean;
|
|
43
|
+
/** Payload of the drag in flight when the app already knows it (a dragstart handler
|
|
44
|
+
* or a pointer-DnD store); stands in for the unreadable dataTransfer during dragover. */
|
|
45
|
+
dragPayload?: string;
|
|
37
46
|
ondrop?: (data: string, e: DragEvent) => void;
|
|
38
47
|
dropHint?: string | Snippet;
|
|
39
48
|
over?: boolean;
|
|
@@ -47,12 +56,13 @@
|
|
|
47
56
|
let depth = 0;
|
|
48
57
|
|
|
49
58
|
function payload(e: DragEvent): string {
|
|
50
|
-
return e.dataTransfer?.getData(mime)
|
|
59
|
+
return e.dataTransfer?.getData(mime) || dragPayload;
|
|
51
60
|
}
|
|
52
61
|
function valid(e: DragEvent): boolean {
|
|
53
62
|
if (!droppable || disabled) return false;
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
const types = Array.from(e.dataTransfer?.types ?? []);
|
|
64
|
+
if (!types.includes(mime)) return false;
|
|
65
|
+
return accepts ? accepts(payload(e), e, types) : true;
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
function onDragEnter(e: DragEvent) {
|
|
@@ -7,7 +7,13 @@ type $$ComponentProps = {
|
|
|
7
7
|
padding?: 'sm' | 'md' | 'lg';
|
|
8
8
|
droppable?: boolean;
|
|
9
9
|
mime?: string;
|
|
10
|
-
|
|
10
|
+
/** `data` is the dataTransfer payload on drop, but during dragenter/dragover the
|
|
11
|
+
* dataTransfer is in protected mode and `getData` returns `''` — there `data` is
|
|
12
|
+
* `dragPayload` when given, otherwise `''`, and `types` is the only other signal. */
|
|
13
|
+
accepts?: (data: string, e: DragEvent, types: readonly string[]) => boolean;
|
|
14
|
+
/** Payload of the drag in flight when the app already knows it (a dragstart handler
|
|
15
|
+
* or a pointer-DnD store); stands in for the unreadable dataTransfer during dragover. */
|
|
16
|
+
dragPayload?: string;
|
|
11
17
|
ondrop?: (data: string, e: DragEvent) => void;
|
|
12
18
|
dropHint?: string | Snippet;
|
|
13
19
|
over?: boolean;
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// are broadly supported today.)
|
|
12
12
|
import { tick, type Snippet } from 'svelte';
|
|
13
13
|
import { place } from '../../floating';
|
|
14
|
+
import { HOVER_CLOSE_GRACE, HOVER_OPEN_DELAY, createHoverIntent, opensOnHover } from './popover-hover.js';
|
|
14
15
|
|
|
15
16
|
type Placement = 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
|
|
16
17
|
type TriggerVariant = 'default' | 'primary' | 'ghost' | 'danger';
|
|
@@ -35,6 +36,10 @@
|
|
|
35
36
|
block = false,
|
|
36
37
|
hitArea = 'auto',
|
|
37
38
|
disabled = false,
|
|
39
|
+
openOn = 'click',
|
|
40
|
+
hoverDelay = HOVER_OPEN_DELAY,
|
|
41
|
+
as = 'button',
|
|
42
|
+
href,
|
|
38
43
|
role = 'dialog',
|
|
39
44
|
haspopup = 'dialog',
|
|
40
45
|
onopen,
|
|
@@ -75,6 +80,15 @@
|
|
|
75
80
|
/** Icon-only triggers grow a 44px hit slab on coarse pointers; `compact` opts out. */
|
|
76
81
|
hitArea?: 'auto' | 'compact';
|
|
77
82
|
disabled?: boolean;
|
|
83
|
+
/** `hover` adds pointer-driven opening on fine pointers; click, Enter and
|
|
84
|
+
* touch keep working so keyboard and touch users reach the same panel. */
|
|
85
|
+
openOn?: 'click' | 'hover';
|
|
86
|
+
/** Delay before a hover opens the panel. */
|
|
87
|
+
hoverDelay?: number;
|
|
88
|
+
/** Render the trigger as a link so it can navigate while still revealing
|
|
89
|
+
* the panel; an `a` without `href` toggles the panel like a button. */
|
|
90
|
+
as?: 'button' | 'a';
|
|
91
|
+
href?: string;
|
|
78
92
|
/** ARIA role of the panel. */
|
|
79
93
|
role?: PanelRole;
|
|
80
94
|
/** `aria-haspopup` announced on the trigger. */
|
|
@@ -93,7 +107,7 @@
|
|
|
93
107
|
);
|
|
94
108
|
|
|
95
109
|
const id = `pop-${Math.random().toString(36).slice(2, 8)}`;
|
|
96
|
-
let triggerEl = $state<
|
|
110
|
+
let triggerEl = $state<HTMLElement | null>(null);
|
|
97
111
|
let panelEl = $state<HTMLDivElement | null>(null);
|
|
98
112
|
// Panel content is mounted only after the first open, then kept alive so
|
|
99
113
|
// reopening is instant. The panel element itself always renders — the native
|
|
@@ -107,6 +121,70 @@
|
|
|
107
121
|
} catch {}
|
|
108
122
|
}
|
|
109
123
|
|
|
124
|
+
function show() {
|
|
125
|
+
try {
|
|
126
|
+
panelEl?.showPopover();
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function toggle() {
|
|
131
|
+
if (open) close();
|
|
132
|
+
else show();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const hover = createHoverIntent({ open: show, close });
|
|
136
|
+
$effect(() => () => hover.cancel());
|
|
137
|
+
|
|
138
|
+
function onPointerEnter(e: PointerEvent) {
|
|
139
|
+
if (disabled || !opensOnHover(openOn, e.pointerType)) return;
|
|
140
|
+
if (open) hover.cancel();
|
|
141
|
+
else hover.enter(hoverDelay);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function onPointerLeave() {
|
|
145
|
+
if (openOn !== 'hover') return;
|
|
146
|
+
hover.leave(HOVER_CLOSE_GRACE);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function onPanelPointerEnter(e: PointerEvent) {
|
|
150
|
+
if (opensOnHover(openOn, e.pointerType)) hover.cancel();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// A link trigger cannot carry `popovertarget`, so it toggles by hand — unless
|
|
154
|
+
// it has an href, where the click belongs to the navigation.
|
|
155
|
+
function onTriggerClick(e: MouseEvent) {
|
|
156
|
+
if (as !== 'a') return;
|
|
157
|
+
if (disabled) {
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
hover.cancel();
|
|
162
|
+
if (href === undefined) {
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
toggle();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function onTriggerKeydown(e: KeyboardEvent) {
|
|
169
|
+
if (as !== 'a' || href !== undefined || disabled) return;
|
|
170
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
hover.cancel();
|
|
173
|
+
toggle();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const triggerAttrs = $derived(
|
|
178
|
+
as === 'a'
|
|
179
|
+
? {
|
|
180
|
+
href: disabled ? undefined : href,
|
|
181
|
+
tabindex: 0,
|
|
182
|
+
role: href === undefined ? 'button' : undefined,
|
|
183
|
+
'aria-disabled': disabled ? ('true' as const) : undefined,
|
|
184
|
+
}
|
|
185
|
+
: { type: 'button' as const, popovertarget: id, disabled }
|
|
186
|
+
);
|
|
187
|
+
|
|
110
188
|
function reposition() {
|
|
111
189
|
if (triggerEl && panelEl) place(triggerEl, panelEl, placement, gap);
|
|
112
190
|
}
|
|
@@ -132,10 +210,10 @@
|
|
|
132
210
|
}
|
|
133
211
|
</script>
|
|
134
212
|
|
|
135
|
-
<
|
|
213
|
+
<svelte:element
|
|
214
|
+
this={as}
|
|
136
215
|
bind:this={triggerEl}
|
|
137
216
|
data-tsu="Popover"
|
|
138
|
-
type="button"
|
|
139
217
|
class="pop-trigger {triggerClass} {klass}"
|
|
140
218
|
style={styleProp}
|
|
141
219
|
class:bare
|
|
@@ -155,14 +233,18 @@
|
|
|
155
233
|
class:trigger-tone-info={tone === 'info'}
|
|
156
234
|
class:trigger-tone-warn={tone === 'warn'}
|
|
157
235
|
class:trigger-tone-danger={tone === 'danger'}
|
|
158
|
-
|
|
236
|
+
class:is-disabled={disabled && as === 'a'}
|
|
159
237
|
aria-label={label}
|
|
160
238
|
aria-haspopup={haspopup}
|
|
161
239
|
aria-expanded={open}
|
|
162
|
-
{
|
|
240
|
+
{...triggerAttrs}
|
|
241
|
+
onpointerenter={onPointerEnter}
|
|
242
|
+
onpointerleave={onPointerLeave}
|
|
243
|
+
onclick={onTriggerClick}
|
|
244
|
+
onkeydown={onTriggerKeydown}
|
|
163
245
|
>
|
|
164
246
|
{@render trigger()}
|
|
165
|
-
</
|
|
247
|
+
</svelte:element>
|
|
166
248
|
|
|
167
249
|
<div
|
|
168
250
|
bind:this={panelEl}
|
|
@@ -173,6 +255,8 @@
|
|
|
173
255
|
{role}
|
|
174
256
|
aria-label={label}
|
|
175
257
|
ontoggle={onToggle}
|
|
258
|
+
onpointerenter={onPanelPointerEnter}
|
|
259
|
+
onpointerleave={onPointerLeave}
|
|
176
260
|
>
|
|
177
261
|
{#if opened}
|
|
178
262
|
{@render children({ close })}
|
|
@@ -332,10 +416,18 @@
|
|
|
332
416
|
border-color: var(--ok);
|
|
333
417
|
filter: brightness(1.08);
|
|
334
418
|
}
|
|
335
|
-
.pop-trigger:disabled
|
|
419
|
+
.pop-trigger:disabled,
|
|
420
|
+
.pop-trigger.is-disabled {
|
|
336
421
|
opacity: 0.45;
|
|
337
422
|
cursor: not-allowed;
|
|
338
423
|
}
|
|
424
|
+
.pop-trigger.is-disabled {
|
|
425
|
+
pointer-events: none;
|
|
426
|
+
}
|
|
427
|
+
:where(a.pop-trigger) {
|
|
428
|
+
cursor: pointer;
|
|
429
|
+
text-decoration: none;
|
|
430
|
+
}
|
|
339
431
|
/* `bare`: strip the chrome down to a plain button the consumer styles. */
|
|
340
432
|
:where(.pop-trigger.bare) {
|
|
341
433
|
display: inline;
|
|
@@ -39,6 +39,15 @@ type $$ComponentProps = {
|
|
|
39
39
|
/** Icon-only triggers grow a 44px hit slab on coarse pointers; `compact` opts out. */
|
|
40
40
|
hitArea?: 'auto' | 'compact';
|
|
41
41
|
disabled?: boolean;
|
|
42
|
+
/** `hover` adds pointer-driven opening on fine pointers; click, Enter and
|
|
43
|
+
* touch keep working so keyboard and touch users reach the same panel. */
|
|
44
|
+
openOn?: 'click' | 'hover';
|
|
45
|
+
/** Delay before a hover opens the panel. */
|
|
46
|
+
hoverDelay?: number;
|
|
47
|
+
/** Render the trigger as a link so it can navigate while still revealing
|
|
48
|
+
* the panel; an `a` without `href` toggles the panel like a button. */
|
|
49
|
+
as?: 'button' | 'a';
|
|
50
|
+
href?: string;
|
|
42
51
|
/** ARIA role of the panel. */
|
|
43
52
|
role?: PanelRole;
|
|
44
53
|
/** `aria-haspopup` announced on the trigger. */
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clamp a slide index into `[0, count)`, wrapping when `loop` is set.
|
|
3
|
+
*
|
|
4
|
+
* @param {number} index
|
|
5
|
+
* @param {number} count
|
|
6
|
+
* @param {boolean} [loop]
|
|
7
|
+
* @returns {number}
|
|
8
|
+
*/
|
|
9
|
+
export function clampSlide(index: number, count: number, loop?: boolean): number;
|
|
10
|
+
/**
|
|
11
|
+
* Step from `current` by `delta` slides. Without `loop` the edges absorb the
|
|
12
|
+
* step, so stepping past the last slide stays on it.
|
|
13
|
+
*
|
|
14
|
+
* @param {number} current
|
|
15
|
+
* @param {number} count
|
|
16
|
+
* @param {number} delta
|
|
17
|
+
* @param {boolean} [loop]
|
|
18
|
+
* @returns {number}
|
|
19
|
+
*/
|
|
20
|
+
export function stepSlide(current: number, count: number, delta: number, loop?: boolean): number;
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a Carousel navigation key to the slide it selects. Left/Right step by
|
|
23
|
+
* one (wrapping only with `loop`), Home/End jump to the first/last slide.
|
|
24
|
+
* Unhandled keys return `undefined` so the browser keeps its default.
|
|
25
|
+
*
|
|
26
|
+
* @param {number} current
|
|
27
|
+
* @param {number} count
|
|
28
|
+
* @param {string} key
|
|
29
|
+
* @param {boolean} [loop]
|
|
30
|
+
* @returns {number | undefined}
|
|
31
|
+
*/
|
|
32
|
+
export function nextSlideForKey(current: number, count: number, key: string, loop?: boolean): number | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Interpret a completed pointer gesture as a slide step. A gesture counts as a
|
|
35
|
+
* horizontal swipe only when it travels at least `threshold` px on the x axis
|
|
36
|
+
* and further on x than on y; anything else is left to the page (vertical
|
|
37
|
+
* scroll, taps). Swiping left reveals the next slide, so the result is +1.
|
|
38
|
+
*
|
|
39
|
+
* @param {number} dx
|
|
40
|
+
* @param {number} dy
|
|
41
|
+
* @param {number} [threshold]
|
|
42
|
+
* @returns {-1 | 0 | 1}
|
|
43
|
+
*/
|
|
44
|
+
export function swipeStep(dx: number, dy: number, threshold?: number): -1 | 0 | 1;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clamp a slide index into `[0, count)`, wrapping when `loop` is set.
|
|
3
|
+
*
|
|
4
|
+
* @param {number} index
|
|
5
|
+
* @param {number} count
|
|
6
|
+
* @param {boolean} [loop]
|
|
7
|
+
* @returns {number}
|
|
8
|
+
*/
|
|
9
|
+
export function clampSlide(index, count, loop = false) {
|
|
10
|
+
if (count <= 0 || !Number.isFinite(index)) return 0;
|
|
11
|
+
if (loop) return ((Math.trunc(index) % count) + count) % count;
|
|
12
|
+
return Math.min(Math.max(Math.trunc(index), 0), count - 1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Step from `current` by `delta` slides. Without `loop` the edges absorb the
|
|
17
|
+
* step, so stepping past the last slide stays on it.
|
|
18
|
+
*
|
|
19
|
+
* @param {number} current
|
|
20
|
+
* @param {number} count
|
|
21
|
+
* @param {number} delta
|
|
22
|
+
* @param {boolean} [loop]
|
|
23
|
+
* @returns {number}
|
|
24
|
+
*/
|
|
25
|
+
export function stepSlide(current, count, delta, loop = false) {
|
|
26
|
+
return clampSlide(current + delta, count, loop);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a Carousel navigation key to the slide it selects. Left/Right step by
|
|
31
|
+
* one (wrapping only with `loop`), Home/End jump to the first/last slide.
|
|
32
|
+
* Unhandled keys return `undefined` so the browser keeps its default.
|
|
33
|
+
*
|
|
34
|
+
* @param {number} current
|
|
35
|
+
* @param {number} count
|
|
36
|
+
* @param {string} key
|
|
37
|
+
* @param {boolean} [loop]
|
|
38
|
+
* @returns {number | undefined}
|
|
39
|
+
*/
|
|
40
|
+
export function nextSlideForKey(current, count, key, loop = false) {
|
|
41
|
+
if (count <= 0) return undefined;
|
|
42
|
+
if (key === 'ArrowRight') return stepSlide(current, count, 1, loop);
|
|
43
|
+
if (key === 'ArrowLeft') return stepSlide(current, count, -1, loop);
|
|
44
|
+
if (key === 'Home') return 0;
|
|
45
|
+
if (key === 'End') return count - 1;
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Interpret a completed pointer gesture as a slide step. A gesture counts as a
|
|
51
|
+
* horizontal swipe only when it travels at least `threshold` px on the x axis
|
|
52
|
+
* and further on x than on y; anything else is left to the page (vertical
|
|
53
|
+
* scroll, taps). Swiping left reveals the next slide, so the result is +1.
|
|
54
|
+
*
|
|
55
|
+
* @param {number} dx
|
|
56
|
+
* @param {number} dy
|
|
57
|
+
* @param {number} [threshold]
|
|
58
|
+
* @returns {-1 | 0 | 1}
|
|
59
|
+
*/
|
|
60
|
+
export function swipeStep(dx, dy, threshold = 40) {
|
|
61
|
+
if (Math.abs(dx) < threshold || Math.abs(dx) <= Math.abs(dy)) return 0;
|
|
62
|
+
return dx < 0 ? 1 : -1;
|
|
63
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {'click' | 'hover'} openOn
|
|
3
|
+
* @param {string | undefined} pointerType
|
|
4
|
+
*/
|
|
5
|
+
export function opensOnHover(openOn: "click" | "hover", pointerType: string | undefined): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* @param {object} o
|
|
8
|
+
* @param {() => void} o.open
|
|
9
|
+
* @param {() => void} o.close
|
|
10
|
+
* @param {(fn: () => void, ms: number) => any} [o.schedule]
|
|
11
|
+
* @param {(handle: any) => void} [o.cancel]
|
|
12
|
+
*/
|
|
13
|
+
export function createHoverIntent({ open, close, schedule, cancel }: {
|
|
14
|
+
open: () => void;
|
|
15
|
+
close: () => void;
|
|
16
|
+
schedule?: ((fn: () => void, ms: number) => any) | undefined;
|
|
17
|
+
cancel?: ((handle: any) => void) | undefined;
|
|
18
|
+
}): {
|
|
19
|
+
/** @param {number} [delay] */
|
|
20
|
+
enter(delay?: number): void;
|
|
21
|
+
/** @param {number} [grace] */
|
|
22
|
+
leave(grace?: number): void;
|
|
23
|
+
cancel: () => void;
|
|
24
|
+
readonly pending: boolean;
|
|
25
|
+
};
|
|
26
|
+
export const HOVER_OPEN_DELAY: 150;
|
|
27
|
+
export const HOVER_CLOSE_GRACE: 120;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Hover-open intent for Popover: fine pointers only (a touch "pointerenter"
|
|
2
|
+
// fires on tap and would fight the click), with a delay before opening and a
|
|
3
|
+
// grace period before closing so the pointer can travel trigger -> panel.
|
|
4
|
+
|
|
5
|
+
export const HOVER_OPEN_DELAY = 150;
|
|
6
|
+
export const HOVER_CLOSE_GRACE = 120;
|
|
7
|
+
|
|
8
|
+
const FINE_POINTERS = new Set(['mouse', 'pen']);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {'click' | 'hover'} openOn
|
|
12
|
+
* @param {string | undefined} pointerType
|
|
13
|
+
*/
|
|
14
|
+
export function opensOnHover(openOn, pointerType) {
|
|
15
|
+
return openOn === 'hover' && FINE_POINTERS.has(pointerType ?? '');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} o
|
|
20
|
+
* @param {() => void} o.open
|
|
21
|
+
* @param {() => void} o.close
|
|
22
|
+
* @param {(fn: () => void, ms: number) => any} [o.schedule]
|
|
23
|
+
* @param {(handle: any) => void} [o.cancel]
|
|
24
|
+
*/
|
|
25
|
+
export function createHoverIntent({ open, close, schedule = setTimeout, cancel = clearTimeout }) {
|
|
26
|
+
/** @type {any} */
|
|
27
|
+
let timer = null;
|
|
28
|
+
|
|
29
|
+
function clear() {
|
|
30
|
+
if (timer !== null) {
|
|
31
|
+
cancel(timer);
|
|
32
|
+
timer = null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {() => void} fn
|
|
38
|
+
* @param {number} ms
|
|
39
|
+
*/
|
|
40
|
+
function later(fn, ms) {
|
|
41
|
+
clear();
|
|
42
|
+
timer = schedule(() => {
|
|
43
|
+
timer = null;
|
|
44
|
+
fn();
|
|
45
|
+
}, ms);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
/** @param {number} [delay] */
|
|
50
|
+
enter(delay = HOVER_OPEN_DELAY) {
|
|
51
|
+
later(open, Math.max(0, delay));
|
|
52
|
+
},
|
|
53
|
+
/** @param {number} [grace] */
|
|
54
|
+
leave(grace = HOVER_CLOSE_GRACE) {
|
|
55
|
+
later(close, Math.max(0, grace));
|
|
56
|
+
},
|
|
57
|
+
cancel: clear,
|
|
58
|
+
get pending() {
|
|
59
|
+
return timer !== null;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export { type Attachment, default as AttachmentList, } from './components/molecu
|
|
|
41
41
|
export { type BreadcrumbItem, default as Breadcrumb, } from './components/molecules/Breadcrumb.svelte';
|
|
42
42
|
export { default as Callout } from './components/molecules/Callout.svelte';
|
|
43
43
|
export { default as CapBar } from './components/molecules/CapBar.svelte';
|
|
44
|
+
export { default as Carousel } from './components/molecules/Carousel.svelte';
|
|
44
45
|
export { default as ChatBubble } from './components/molecules/ChatBubble.svelte';
|
|
45
46
|
export { default as CodeBlock } from './components/molecules/CodeBlock.svelte';
|
|
46
47
|
export { default as Composer } from './components/molecules/Composer.svelte';
|
package/dist/index.js
CHANGED
|
@@ -47,6 +47,7 @@ export { default as AttachmentList, } from './components/molecules/AttachmentLis
|
|
|
47
47
|
export { default as Breadcrumb, } from './components/molecules/Breadcrumb.svelte';
|
|
48
48
|
export { default as Callout } from './components/molecules/Callout.svelte';
|
|
49
49
|
export { default as CapBar } from './components/molecules/CapBar.svelte';
|
|
50
|
+
export { default as Carousel } from './components/molecules/Carousel.svelte';
|
|
50
51
|
export { default as ChatBubble } from './components/molecules/ChatBubble.svelte';
|
|
51
52
|
export { default as CodeBlock } from './components/molecules/CodeBlock.svelte';
|
|
52
53
|
export { default as Composer } from './components/molecules/Composer.svelte';
|
package/package.json
CHANGED