@dorsk/tsumikit 0.48.0 → 0.50.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 +15 -4
- 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/Divider.svelte +107 -0
- package/dist/components/atoms/Divider.svelte.d.ts +17 -0
- package/dist/components/atoms/Gauge.svelte +10 -5
- package/dist/components/atoms/Gauge.svelte.d.ts +1 -0
- package/dist/components/layouts/MasterDetail.svelte +12 -2
- package/dist/components/layouts/MasterDetail.svelte.d.ts +4 -0
- package/dist/components/layouts/NavBar.svelte +170 -0
- package/dist/components/layouts/NavBar.svelte.d.ts +28 -0
- package/dist/components/molecules/CapBar.svelte +150 -16
- package/dist/components/molecules/CapBar.svelte.d.ts +20 -4
- 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/popover-hover.d.ts +27 -0
- package/dist/components/molecules/popover-hover.js +62 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -166,7 +166,9 @@ threshold tones via `warnAt`/`dangerAt`, `corner` snippet), Artwork (lazy cover
|
|
|
166
166
|
fallback, `aspect`, `status` overlay), Card (`tone` tints the surface for inline banners), Badge
|
|
167
167
|
(`tone` semantic palette or `color` for any CSS colour, `size` xs/sm/md, `dot`,
|
|
168
168
|
`icon`, `numeric`, `truncate`, `variant="text"`; all tints derive from
|
|
169
|
-
`--badge-tone`), Dot (`ring` dark halo over artwork),
|
|
169
|
+
`--badge-tone`), Dot (`ring` dark halo over artwork), Divider (standalone
|
|
170
|
+
horizontal/vertical hairline, `tone`, `spacing`, optional centred label,
|
|
171
|
+
`decorative` to drop the `separator` role), Link (`tone`, `underline`
|
|
170
172
|
always/hover/none, `align`), Icon (open registry — pass a `children` snippet for
|
|
171
173
|
any custom SVG).
|
|
172
174
|
|
|
@@ -181,7 +183,11 @@ EmojiPicker (popover with a searchable EN+FR glyph catalogue, grouped tabs, rovi
|
|
|
181
183
|
GitRef (branch chip + PR link tinted by state + `+N −N` diff; `collapse`
|
|
182
184
|
auto/never/glyph degrades to icons inside a narrow `.cq` container),
|
|
183
185
|
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
|
|
186
|
+
shows a `N%` bubble while it is moved; `oninput` live, `onchange` on commit;
|
|
187
|
+
`marker`/`markerLabel`/`markerTone` draw a pace rule, `cap: null` plus
|
|
188
|
+
`clearAtMax`/`uncappedLabel` express "no cap", `layout`/`stackBelow` put the
|
|
189
|
+
readout above a full-width track, `capLabel`/`capValueText` localise the ARIA
|
|
190
|
+
strings),
|
|
185
191
|
Drawer (side-panel `<dialog>`: `side`, `width` clamped to the viewport, full-screen
|
|
186
192
|
under 48rem; `nav` page column that turns into a horizontal strip on narrow
|
|
187
193
|
screens, or `navMobile`; sticky `footer`; Escape / scrim / close button all close),
|
|
@@ -215,12 +221,17 @@ sidebar on desktop, overlay drawer on mobile, optionally resizable;
|
|
|
215
221
|
the content column only, `stickySidebar` pins it to the viewport, and
|
|
216
222
|
`sidebarPadding="none" | "sm" | "md"` sets the aside gutter — the header and its
|
|
217
223
|
children are `min-width: 0` so a wide title/actions row can't widen the grid on
|
|
218
|
-
mobile),
|
|
224
|
+
mobile), NavBar (horizontal route bar — `<nav>` + `<a aria-current="page">`,
|
|
225
|
+
icon/emoji + label + count badge; `placement="bar"` pins it to the bottom edge
|
|
226
|
+
over `--z-nav` with `--safe-bottom` padding, `orientation` stacks or inlines the
|
|
227
|
+
glyph, `maxWidth` centres the row), NavItem
|
|
219
228
|
(collapses to an icon rail when the sidebar is narrow; `icon` from the
|
|
220
229
|
registry, `iconPath` / `iconChildren` for custom glyphs, `activeStyle="bar"`
|
|
221
230
|
for the inset-bar active look), MasterDetail (list + detail columns that
|
|
222
231
|
become two pages — one pane at a time with a sticky 44px back header — below
|
|
223
|
-
their own `breakpoint`, so `selected` can be driven from the URL
|
|
232
|
+
their own `breakpoint`, so `selected` can be driven from the URL; `gap` spaces
|
|
233
|
+
the columns and `divider="none"` drops the seam, both also settable as the
|
|
234
|
+
`--md-gap` / `--md-divider` custom properties), Container, Stack
|
|
224
235
|
(vertical), Cluster (wrapping row; `stackAt="xs|sm|md|lg"` makes it its own
|
|
225
236
|
query container and stacks children full-width below 18/30/40/48rem — phone
|
|
226
237
|
action rows without a viewport query), AutoGrid (intrinsically responsive
|
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;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
// Standalone hairline between two siblings. Horizontal by default; the
|
|
5
|
+
// vertical form stretches to the flex parent's cross size. With `children`
|
|
6
|
+
// the rule splits around a centred label ("or", "3 older messages") — a
|
|
7
|
+
// labelled divider is never decorative, so it keeps role="separator".
|
|
8
|
+
type Own = {
|
|
9
|
+
orientation?: 'horizontal' | 'vertical';
|
|
10
|
+
/** Margin along the axis (any CSS length). */
|
|
11
|
+
spacing?: string;
|
|
12
|
+
tone?: 'default' | 'strong' | 'faint';
|
|
13
|
+
/** Centred label turning the rule into rule + text. */
|
|
14
|
+
children?: Snippet;
|
|
15
|
+
/** Purely visual (aria-hidden) rather than a semantic separator. */
|
|
16
|
+
decorative?: boolean;
|
|
17
|
+
class?: string;
|
|
18
|
+
};
|
|
19
|
+
let {
|
|
20
|
+
orientation = 'horizontal',
|
|
21
|
+
spacing,
|
|
22
|
+
tone = 'default',
|
|
23
|
+
children,
|
|
24
|
+
decorative = false,
|
|
25
|
+
class: klass = '',
|
|
26
|
+
...rest
|
|
27
|
+
}: Omit<HTMLAttributes<HTMLDivElement>, keyof Own> & Own = $props();
|
|
28
|
+
|
|
29
|
+
const vertical = $derived(orientation === 'vertical');
|
|
30
|
+
const semantic = $derived(!decorative || !!children);
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<div
|
|
34
|
+
class="divider {orientation} tone-{tone} {klass}"
|
|
35
|
+
class:labelled={!!children}
|
|
36
|
+
data-tsu="Divider"
|
|
37
|
+
role={semantic ? 'separator' : undefined}
|
|
38
|
+
aria-orientation={semantic ? orientation : undefined}
|
|
39
|
+
aria-hidden={semantic ? undefined : 'true'}
|
|
40
|
+
style:--divider-spacing={spacing}
|
|
41
|
+
{...rest}
|
|
42
|
+
>
|
|
43
|
+
{#if children}
|
|
44
|
+
<span class="divider-label">{@render children()}</span>
|
|
45
|
+
{/if}
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<style>
|
|
49
|
+
.divider {
|
|
50
|
+
--divider-color: var(--border);
|
|
51
|
+
--divider-spacing: var(--sp-3);
|
|
52
|
+
flex: none;
|
|
53
|
+
border: 0;
|
|
54
|
+
}
|
|
55
|
+
.tone-strong {
|
|
56
|
+
--divider-color: var(--border-strong);
|
|
57
|
+
}
|
|
58
|
+
.tone-faint {
|
|
59
|
+
--divider-color: color-mix(in srgb, var(--border) 45%, transparent);
|
|
60
|
+
}
|
|
61
|
+
.horizontal {
|
|
62
|
+
width: 100%;
|
|
63
|
+
height: 1px;
|
|
64
|
+
background: var(--divider-color);
|
|
65
|
+
margin-block: var(--divider-spacing);
|
|
66
|
+
}
|
|
67
|
+
.vertical {
|
|
68
|
+
align-self: stretch;
|
|
69
|
+
width: 1px;
|
|
70
|
+
min-height: 1em;
|
|
71
|
+
background: var(--divider-color);
|
|
72
|
+
margin-inline: var(--divider-spacing);
|
|
73
|
+
}
|
|
74
|
+
.horizontal.labelled,
|
|
75
|
+
.vertical.labelled {
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
gap: var(--sp-2);
|
|
79
|
+
background: none;
|
|
80
|
+
}
|
|
81
|
+
.horizontal.labelled {
|
|
82
|
+
height: auto;
|
|
83
|
+
}
|
|
84
|
+
.vertical.labelled {
|
|
85
|
+
flex-direction: column;
|
|
86
|
+
width: auto;
|
|
87
|
+
}
|
|
88
|
+
.horizontal.labelled::before,
|
|
89
|
+
.horizontal.labelled::after {
|
|
90
|
+
content: '';
|
|
91
|
+
flex: 1;
|
|
92
|
+
height: 1px;
|
|
93
|
+
background: var(--divider-color);
|
|
94
|
+
}
|
|
95
|
+
.vertical.labelled::before,
|
|
96
|
+
.vertical.labelled::after {
|
|
97
|
+
content: '';
|
|
98
|
+
flex: 1;
|
|
99
|
+
width: 1px;
|
|
100
|
+
background: var(--divider-color);
|
|
101
|
+
}
|
|
102
|
+
.divider-label {
|
|
103
|
+
color: var(--text-muted);
|
|
104
|
+
font-size: var(--fs-sm);
|
|
105
|
+
white-space: nowrap;
|
|
106
|
+
}
|
|
107
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
type Own = {
|
|
4
|
+
orientation?: 'horizontal' | 'vertical';
|
|
5
|
+
/** Margin along the axis (any CSS length). */
|
|
6
|
+
spacing?: string;
|
|
7
|
+
tone?: 'default' | 'strong' | 'faint';
|
|
8
|
+
/** Centred label turning the rule into rule + text. */
|
|
9
|
+
children?: Snippet;
|
|
10
|
+
/** Purely visual (aria-hidden) rather than a semantic separator. */
|
|
11
|
+
decorative?: boolean;
|
|
12
|
+
class?: string;
|
|
13
|
+
};
|
|
14
|
+
type $$ComponentProps = Omit<HTMLAttributes<HTMLDivElement>, keyof Own> & Own;
|
|
15
|
+
declare const Divider: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
16
|
+
type Divider = ReturnType<typeof Divider>;
|
|
17
|
+
export default Divider;
|
|
@@ -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}
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
empty,
|
|
16
16
|
listWidth = '17rem',
|
|
17
17
|
breakpoint = '48rem',
|
|
18
|
+
gap = '0',
|
|
19
|
+
divider = 'border',
|
|
18
20
|
selected = false,
|
|
19
21
|
onback,
|
|
20
22
|
backLabel = 'Back',
|
|
@@ -32,6 +34,10 @@
|
|
|
32
34
|
listWidth?: string;
|
|
33
35
|
/** Own-width threshold (px/em/rem) below which the panes become two pages. */
|
|
34
36
|
breakpoint?: string;
|
|
37
|
+
/** Space between the two columns (any CSS length). */
|
|
38
|
+
gap?: string;
|
|
39
|
+
/** Seam between the panes; `--md-divider` overrides it with any border shorthand. */
|
|
40
|
+
divider?: 'border' | 'none';
|
|
35
41
|
selected?: boolean;
|
|
36
42
|
onback?: () => void;
|
|
37
43
|
backLabel?: string;
|
|
@@ -72,7 +78,9 @@
|
|
|
72
78
|
class="md {klass}"
|
|
73
79
|
class:mobile
|
|
74
80
|
class:selected
|
|
75
|
-
style="--md-list-w: {listWidth};
|
|
81
|
+
style="--md-list-w: {listWidth}; --md-gap: {gap}; --md-divider: {divider === 'none'
|
|
82
|
+
? 'none'
|
|
83
|
+
: '1px solid var(--border)'};{style ? ` ${style}` : ''}"
|
|
76
84
|
data-tsu="MasterDetail"
|
|
77
85
|
{...rest}
|
|
78
86
|
>
|
|
@@ -113,6 +121,7 @@
|
|
|
113
121
|
.md {
|
|
114
122
|
display: grid;
|
|
115
123
|
grid-template-columns: var(--md-list-w) minmax(0, 1fr);
|
|
124
|
+
gap: var(--md-gap, 0);
|
|
116
125
|
min-width: 0;
|
|
117
126
|
min-height: 0;
|
|
118
127
|
height: 100%;
|
|
@@ -127,7 +136,7 @@
|
|
|
127
136
|
-webkit-overflow-scrolling: touch;
|
|
128
137
|
}
|
|
129
138
|
.md-list {
|
|
130
|
-
border-right: 1px solid var(--border);
|
|
139
|
+
border-right: var(--md-divider, 1px solid var(--border));
|
|
131
140
|
}
|
|
132
141
|
.md-detail {
|
|
133
142
|
display: flex;
|
|
@@ -183,6 +192,7 @@
|
|
|
183
192
|
|
|
184
193
|
.md.mobile {
|
|
185
194
|
grid-template-columns: minmax(0, 1fr);
|
|
195
|
+
gap: 0;
|
|
186
196
|
}
|
|
187
197
|
.md.mobile .md-list {
|
|
188
198
|
border-right: 0;
|
|
@@ -9,6 +9,10 @@ type $$ComponentProps = {
|
|
|
9
9
|
listWidth?: string;
|
|
10
10
|
/** Own-width threshold (px/em/rem) below which the panes become two pages. */
|
|
11
11
|
breakpoint?: string;
|
|
12
|
+
/** Space between the two columns (any CSS length). */
|
|
13
|
+
gap?: string;
|
|
14
|
+
/** Seam between the panes; `--md-divider` overrides it with any border shorthand. */
|
|
15
|
+
divider?: 'border' | 'none';
|
|
12
16
|
selected?: boolean;
|
|
13
17
|
onback?: () => void;
|
|
14
18
|
backLabel?: string;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Horizontal route navigation: a `<nav>` landmark of `<a>` links, one marked
|
|
3
|
+
// `aria-current="page"`. `placement="bar"` pins it to the bottom edge over
|
|
4
|
+
// `--z-nav` and pads for the iOS home indicator with `--safe-bottom`;
|
|
5
|
+
// `placement="inline"` is a plain row for a header. `orientation` stacks the
|
|
6
|
+
// icon over the label (phone bar) or sits it beside (header).
|
|
7
|
+
import Icon from '../atoms/Icon.svelte';
|
|
8
|
+
import type { IconName } from '../atoms/Icon.svelte';
|
|
9
|
+
|
|
10
|
+
export interface NavBarItem {
|
|
11
|
+
href: string;
|
|
12
|
+
label: string;
|
|
13
|
+
icon?: IconName;
|
|
14
|
+
/** Rendered instead of `icon` when set. */
|
|
15
|
+
emoji?: string;
|
|
16
|
+
/** Count or short text pinned to the glyph. 0 / '' render nothing. */
|
|
17
|
+
badge?: number | string;
|
|
18
|
+
current?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
items,
|
|
23
|
+
label,
|
|
24
|
+
placement = 'inline',
|
|
25
|
+
orientation = 'stacked',
|
|
26
|
+
maxWidth,
|
|
27
|
+
class: klass = '',
|
|
28
|
+
style: styleProp = '',
|
|
29
|
+
...rest
|
|
30
|
+
}: {
|
|
31
|
+
items: NavBarItem[];
|
|
32
|
+
/** Accessible name of the `<nav>` landmark. */
|
|
33
|
+
label: string;
|
|
34
|
+
/** 'bar' = fixed to the bottom edge with safe-area padding. */
|
|
35
|
+
placement?: 'bar' | 'inline';
|
|
36
|
+
/** Icon over label vs icon beside label. */
|
|
37
|
+
orientation?: 'stacked' | 'inline';
|
|
38
|
+
/** Caps and centres the row of links. */
|
|
39
|
+
maxWidth?: string;
|
|
40
|
+
class?: string;
|
|
41
|
+
style?: string;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
} = $props();
|
|
44
|
+
|
|
45
|
+
const hasBadge = (badge: NavBarItem['badge']) =>
|
|
46
|
+
badge !== undefined && badge !== null && badge !== '' && badge !== 0;
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<nav
|
|
50
|
+
data-tsu="NavBar"
|
|
51
|
+
aria-label={label}
|
|
52
|
+
class="navbar {klass}"
|
|
53
|
+
class:bar={placement === 'bar'}
|
|
54
|
+
class:stacked={orientation === 'stacked'}
|
|
55
|
+
style="{maxWidth ? `--navbar-max: ${maxWidth};` : ''}{styleProp}"
|
|
56
|
+
{...rest}
|
|
57
|
+
>
|
|
58
|
+
<div class="navbar-inner">
|
|
59
|
+
{#each items as item (item.href)}
|
|
60
|
+
<a
|
|
61
|
+
href={item.href}
|
|
62
|
+
class="navbar-link"
|
|
63
|
+
class:current={item.current}
|
|
64
|
+
aria-current={item.current ? 'page' : undefined}
|
|
65
|
+
>
|
|
66
|
+
<span class="navbar-glyph">
|
|
67
|
+
{#if item.emoji}
|
|
68
|
+
<span class="navbar-emoji" aria-hidden="true">{item.emoji}</span>
|
|
69
|
+
{:else if item.icon}
|
|
70
|
+
<Icon name={item.icon} size={20} />
|
|
71
|
+
{/if}
|
|
72
|
+
{#if hasBadge(item.badge)}
|
|
73
|
+
<span class="navbar-badge">{item.badge}</span>
|
|
74
|
+
{/if}
|
|
75
|
+
</span>
|
|
76
|
+
<span class="navbar-label">{item.label}</span>
|
|
77
|
+
</a>
|
|
78
|
+
{/each}
|
|
79
|
+
</div>
|
|
80
|
+
</nav>
|
|
81
|
+
|
|
82
|
+
<style>
|
|
83
|
+
.navbar {
|
|
84
|
+
--navbar-max: none;
|
|
85
|
+
min-width: 0;
|
|
86
|
+
}
|
|
87
|
+
.navbar-inner {
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: stretch;
|
|
90
|
+
justify-content: space-around;
|
|
91
|
+
gap: var(--sp-1);
|
|
92
|
+
min-width: 0;
|
|
93
|
+
max-width: var(--navbar-max);
|
|
94
|
+
margin-inline: auto;
|
|
95
|
+
min-height: var(--nav-h);
|
|
96
|
+
}
|
|
97
|
+
.bar {
|
|
98
|
+
position: fixed;
|
|
99
|
+
inset-inline: 0;
|
|
100
|
+
bottom: 0;
|
|
101
|
+
z-index: var(--z-nav);
|
|
102
|
+
padding-bottom: var(--safe-bottom);
|
|
103
|
+
padding-inline: max(var(--safe-left), var(--sp-1)) max(var(--safe-right), var(--sp-1));
|
|
104
|
+
background: color-mix(in srgb, var(--bg-elevated) 88%, transparent);
|
|
105
|
+
backdrop-filter: blur(12px);
|
|
106
|
+
border-top: 1px solid var(--border);
|
|
107
|
+
}
|
|
108
|
+
.navbar-link {
|
|
109
|
+
position: relative;
|
|
110
|
+
display: flex;
|
|
111
|
+
flex: 1 1 0;
|
|
112
|
+
min-width: 0;
|
|
113
|
+
flex-direction: row;
|
|
114
|
+
align-items: center;
|
|
115
|
+
justify-content: center;
|
|
116
|
+
gap: var(--sp-2);
|
|
117
|
+
padding: var(--sp-2);
|
|
118
|
+
border-radius: var(--r-md);
|
|
119
|
+
color: var(--text-muted);
|
|
120
|
+
font-size: var(--fs-sm);
|
|
121
|
+
font-weight: var(--fw-medium);
|
|
122
|
+
text-decoration: none;
|
|
123
|
+
transition:
|
|
124
|
+
background 0.12s var(--ease),
|
|
125
|
+
color 0.12s var(--ease);
|
|
126
|
+
}
|
|
127
|
+
.stacked .navbar-link {
|
|
128
|
+
flex-direction: column;
|
|
129
|
+
gap: 2px;
|
|
130
|
+
font-size: var(--fs-xs);
|
|
131
|
+
}
|
|
132
|
+
.navbar-link:hover {
|
|
133
|
+
background: var(--bg-elevated-2);
|
|
134
|
+
color: var(--text);
|
|
135
|
+
text-decoration: none;
|
|
136
|
+
}
|
|
137
|
+
.navbar-link.current {
|
|
138
|
+
color: var(--accent);
|
|
139
|
+
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
|
140
|
+
}
|
|
141
|
+
.navbar-glyph {
|
|
142
|
+
position: relative;
|
|
143
|
+
display: inline-flex;
|
|
144
|
+
flex: none;
|
|
145
|
+
align-items: center;
|
|
146
|
+
}
|
|
147
|
+
.navbar-emoji {
|
|
148
|
+
font-size: 1.15em;
|
|
149
|
+
line-height: 1;
|
|
150
|
+
}
|
|
151
|
+
.navbar-label {
|
|
152
|
+
min-width: 0;
|
|
153
|
+
overflow: hidden;
|
|
154
|
+
text-overflow: ellipsis;
|
|
155
|
+
white-space: nowrap;
|
|
156
|
+
}
|
|
157
|
+
.navbar-badge {
|
|
158
|
+
position: absolute;
|
|
159
|
+
top: -0.35rem;
|
|
160
|
+
left: calc(100% - 0.35rem);
|
|
161
|
+
padding-inline: 0.25rem;
|
|
162
|
+
min-width: 1rem;
|
|
163
|
+
border-radius: var(--r-pill);
|
|
164
|
+
background: var(--accent);
|
|
165
|
+
color: var(--text-on-accent);
|
|
166
|
+
font-size: var(--fs-xs);
|
|
167
|
+
line-height: 1rem;
|
|
168
|
+
text-align: center;
|
|
169
|
+
}
|
|
170
|
+
</style>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { IconName } from '../atoms/Icon.svelte';
|
|
2
|
+
export interface NavBarItem {
|
|
3
|
+
href: string;
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: IconName;
|
|
6
|
+
/** Rendered instead of `icon` when set. */
|
|
7
|
+
emoji?: string;
|
|
8
|
+
/** Count or short text pinned to the glyph. 0 / '' render nothing. */
|
|
9
|
+
badge?: number | string;
|
|
10
|
+
current?: boolean;
|
|
11
|
+
}
|
|
12
|
+
type $$ComponentProps = {
|
|
13
|
+
items: NavBarItem[];
|
|
14
|
+
/** Accessible name of the `<nav>` landmark. */
|
|
15
|
+
label: string;
|
|
16
|
+
/** 'bar' = fixed to the bottom edge with safe-area padding. */
|
|
17
|
+
placement?: 'bar' | 'inline';
|
|
18
|
+
/** Icon over label vs icon beside label. */
|
|
19
|
+
orientation?: 'stacked' | 'inline';
|
|
20
|
+
/** Caps and centres the row of links. */
|
|
21
|
+
maxWidth?: string;
|
|
22
|
+
class?: string;
|
|
23
|
+
style?: string;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
};
|
|
26
|
+
declare const NavBar: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
27
|
+
type NavBar = ReturnType<typeof NavBar>;
|
|
28
|
+
export default NavBar;
|
|
@@ -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
|
};
|
|
@@ -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,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
|
@@ -6,6 +6,7 @@ export { default as Badge } from './components/atoms/Badge.svelte';
|
|
|
6
6
|
export { default as Button } from './components/atoms/Button.svelte';
|
|
7
7
|
export { default as Card } from './components/atoms/Card.svelte';
|
|
8
8
|
export { default as Checkbox } from './components/atoms/Checkbox.svelte';
|
|
9
|
+
export { default as Divider } from './components/atoms/Divider.svelte';
|
|
9
10
|
export { default as Dot } from './components/atoms/Dot.svelte';
|
|
10
11
|
export { default as Gauge, type GaugeTone } from './components/atoms/Gauge.svelte';
|
|
11
12
|
export { default as Heading } from './components/atoms/Heading.svelte';
|
|
@@ -30,6 +31,7 @@ export { default as AutoGrid } from './components/layouts/AutoGrid.svelte';
|
|
|
30
31
|
export { default as Cluster } from './components/layouts/Cluster.svelte';
|
|
31
32
|
export { default as Container } from './components/layouts/Container.svelte';
|
|
32
33
|
export { default as MasterDetail } from './components/layouts/MasterDetail.svelte';
|
|
34
|
+
export { default as NavBar, type NavBarItem } from './components/layouts/NavBar.svelte';
|
|
33
35
|
export { default as NavItem } from './components/layouts/NavItem.svelte';
|
|
34
36
|
export { default as NavSection } from './components/layouts/NavSection.svelte';
|
|
35
37
|
export { default as ResizablePanel } from './components/layouts/ResizablePanel.svelte';
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export { default as Badge } from './components/atoms/Badge.svelte';
|
|
|
12
12
|
export { default as Button } from './components/atoms/Button.svelte';
|
|
13
13
|
export { default as Card } from './components/atoms/Card.svelte';
|
|
14
14
|
export { default as Checkbox } from './components/atoms/Checkbox.svelte';
|
|
15
|
+
export { default as Divider } from './components/atoms/Divider.svelte';
|
|
15
16
|
export { default as Dot } from './components/atoms/Dot.svelte';
|
|
16
17
|
export { default as Gauge } from './components/atoms/Gauge.svelte';
|
|
17
18
|
export { default as Heading } from './components/atoms/Heading.svelte';
|
|
@@ -36,6 +37,7 @@ export { default as AutoGrid } from './components/layouts/AutoGrid.svelte';
|
|
|
36
37
|
export { default as Cluster } from './components/layouts/Cluster.svelte';
|
|
37
38
|
export { default as Container } from './components/layouts/Container.svelte';
|
|
38
39
|
export { default as MasterDetail } from './components/layouts/MasterDetail.svelte';
|
|
40
|
+
export { default as NavBar } from './components/layouts/NavBar.svelte';
|
|
39
41
|
export { default as NavItem } from './components/layouts/NavItem.svelte';
|
|
40
42
|
export { default as NavSection } from './components/layouts/NavSection.svelte';
|
|
41
43
|
export { default as ResizablePanel } from './components/layouts/ResizablePanel.svelte';
|
package/package.json
CHANGED