@lyeve-labs/ui-kit 0.24.0 → 0.25.1
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 +43 -0
- package/dist/components/AccordionItem.svelte +3 -5
- package/dist/components/AccountMenu.svelte +4 -3
- package/dist/components/Alert.svelte +2 -1
- package/dist/components/AppShell.svelte +166 -54
- package/dist/components/AppShell.svelte.d.ts +41 -6
- package/dist/components/Autocomplete.svelte +3 -2
- package/dist/components/Badge.svelte +10 -5
- package/dist/components/Banner.svelte +2 -1
- package/dist/components/Breadcrumb.svelte +7 -2
- package/dist/components/Button.svelte +15 -2
- package/dist/components/Card.svelte +1 -1
- package/dist/components/Collapsible.svelte +5 -4
- package/dist/components/CopyButton.svelte +2 -1
- package/dist/components/DatePicker.svelte +77 -62
- package/dist/components/Drawer.svelte +6 -5
- package/dist/components/Dropdown.svelte +5 -2
- package/dist/components/FileInput.svelte +1 -1
- package/dist/components/Modal.svelte +6 -3
- package/dist/components/MultiSelect.svelte +8 -6
- package/dist/components/NumberInput.svelte +2 -2
- package/dist/components/PageHeader.svelte +13 -1
- package/dist/components/PageShell.svelte +37 -19
- package/dist/components/PageShell.svelte.d.ts +12 -0
- package/dist/components/Pagination.svelte +7 -2
- package/dist/components/PasswordInput.svelte +2 -1
- package/dist/components/Progress.svelte +1 -1
- package/dist/components/SearchInput.svelte +3 -2
- package/dist/components/SectionHeading.svelte +10 -1
- package/dist/components/SegmentedControl.svelte +101 -27
- package/dist/components/SegmentedControl.svelte.d.ts +9 -0
- package/dist/components/Select.svelte +4 -3
- package/dist/components/SidebarNav.svelte +26 -17
- package/dist/components/StepIndicator.svelte +7 -9
- package/dist/components/Table.svelte +39 -29
- package/dist/components/Tabs.svelte +128 -24
- package/dist/components/Tag.svelte +2 -1
- package/dist/components/ThemeToggle.svelte +2 -1
- package/dist/components/Toaster.svelte +6 -2
- package/dist/components/Toggle.svelte +3 -2
- package/dist/components/Tooltip.svelte +10 -5
- package/dist/components/TreeView.svelte +2 -2
- package/dist/components/dialog/Dialog.svelte +15 -32
- package/dist/index.d.ts +4 -1
- package/dist/index.js +7 -1
- package/dist/internal/choice.js +10 -3
- package/dist/internal/field.js +3 -3
- package/dist/internal/layout.d.ts +10 -1
- package/dist/internal/layout.js +10 -1
- package/dist/internal/panel.d.ts +13 -3
- package/dist/internal/panel.js +16 -5
- package/dist/internal/scroll-edges.d.ts +33 -0
- package/dist/internal/scroll-edges.js +33 -0
- package/dist/internal/touch.d.ts +28 -0
- package/dist/internal/touch.js +28 -0
- package/dist/motion.d.ts +63 -0
- package/dist/motion.js +170 -0
- package/dist/styles/theme.css +131 -63
- package/package.json +1 -1
- package/src/lib/styles/theme.css +131 -63
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
SCROLL_EDGE,
|
|
4
|
+
SCROLL_EDGE_END,
|
|
5
|
+
SCROLL_EDGE_START,
|
|
6
|
+
scrollEdges,
|
|
7
|
+
} from '../internal/scroll-edges.js';
|
|
8
|
+
import { TOUCH_GROW } from '../internal/touch.js';
|
|
9
|
+
|
|
2
10
|
let {
|
|
3
11
|
items,
|
|
4
12
|
active,
|
|
@@ -10,30 +18,126 @@
|
|
|
10
18
|
class?: string;
|
|
11
19
|
onchange: (id: string) => void;
|
|
12
20
|
} = $props();
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
* The strip scrolls sideways when the tabs do not fit, and says so.
|
|
24
|
+
*
|
|
25
|
+
* It neither wrapped nor scrolled: five tabs are 480px, and at 400px the
|
|
26
|
+
* fifth was past the edge with nothing on screen to say a fifth existed.
|
|
27
|
+
* Wrapping was the workaround, and a wrapped strip breaks the one line the
|
|
28
|
+
* underline runs along. A tab never breaks inside its own label.
|
|
29
|
+
*
|
|
30
|
+
* The scrollbar is not drawn. A strip of tabs with a bar under it reads as
|
|
31
|
+
* a second rule under the real one; the fades carry the same message, the
|
|
32
|
+
* tabs are buttons so the keyboard reaches every one, and the active tab
|
|
33
|
+
* is scrolled into view when it changes.
|
|
34
|
+
*/
|
|
35
|
+
let scroller: HTMLElement | undefined = $state();
|
|
36
|
+
let moreBefore = $state(false);
|
|
37
|
+
let moreAfter = $state(false);
|
|
38
|
+
|
|
39
|
+
function measure() {
|
|
40
|
+
const el = scroller;
|
|
41
|
+
if (!el) return;
|
|
42
|
+
const edges = scrollEdges(el);
|
|
43
|
+
moreBefore = edges.before;
|
|
44
|
+
moreAfter = edges.after;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
$effect(() => {
|
|
48
|
+
const el = scroller;
|
|
49
|
+
if (!el) return;
|
|
50
|
+
measure();
|
|
51
|
+
if (typeof ResizeObserver === 'undefined') return;
|
|
52
|
+
const observer = new ResizeObserver(measure);
|
|
53
|
+
observer.observe(el);
|
|
54
|
+
return () => observer.disconnect();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
* The tab for the active id is brought into view when the id changes, so a
|
|
59
|
+
* page that opens on its fifth tab does not open on a strip that hides it.
|
|
60
|
+
* `nearest` on both axes moves the strip and nothing else; a test DOM has
|
|
61
|
+
* no layout and no scrollIntoView, and neither is an error here.
|
|
62
|
+
*/
|
|
63
|
+
$effect(() => {
|
|
64
|
+
const el = scroller;
|
|
65
|
+
const id = active;
|
|
66
|
+
if (!el) return;
|
|
67
|
+
const tab = [...el.querySelectorAll<HTMLElement>('[role="tab"]')].find(
|
|
68
|
+
(t) => t.dataset.tab === id,
|
|
69
|
+
);
|
|
70
|
+
if (tab && typeof tab.scrollIntoView === 'function') {
|
|
71
|
+
tab.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/*
|
|
76
|
+
* The focus ring is inset: the tabs sit in a box that clips to scroll, and
|
|
77
|
+
* the global outline sits 2px outside its element, so on the strip's top
|
|
78
|
+
* edge it was cut off. min-h-control under a finger is the same height the
|
|
79
|
+
* buttons and fields beside the strip take there.
|
|
80
|
+
*/
|
|
81
|
+
const TAB =
|
|
82
|
+
`${TOUCH_GROW} relative shrink-0 px-4 py-2.5 text-sm font-medium whitespace-nowrap ` +
|
|
83
|
+
'transition-colors outline-none ' +
|
|
84
|
+
'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand';
|
|
13
85
|
</script>
|
|
14
86
|
|
|
15
|
-
<div
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
87
|
+
<div class="relative {cls}">
|
|
88
|
+
<div
|
|
89
|
+
bind:this={scroller}
|
|
90
|
+
onscroll={measure}
|
|
91
|
+
role="tablist"
|
|
92
|
+
data-testid="tabs-scroll"
|
|
93
|
+
class="flex gap-0.5 overflow-x-auto border-b border-line [scrollbar-width:none]"
|
|
94
|
+
>
|
|
95
|
+
{#each items as tab (tab.id)}
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
role="tab"
|
|
99
|
+
data-tab={tab.id}
|
|
100
|
+
aria-selected={active === tab.id}
|
|
101
|
+
onclick={() => onchange(tab.id)}
|
|
102
|
+
class="{TAB}
|
|
103
|
+
{active === tab.id
|
|
104
|
+
? 'text-fg after:absolute after:bottom-0 after:inset-x-0 after:h-0.5 after:bg-brand after:rounded-t'
|
|
105
|
+
: 'text-faint hover:text-fg'}"
|
|
106
|
+
>
|
|
107
|
+
{tab.label}
|
|
108
|
+
{#if tab.count !== undefined}
|
|
109
|
+
<!-- text-xs, not 10px: the count was the one text on a measured
|
|
110
|
+
page under the 12px floor, and it is the one figure a tab
|
|
111
|
+
carries. -->
|
|
112
|
+
<span
|
|
113
|
+
class="ms-1.5 inline-flex items-center justify-center min-w-4.5 h-4.5 px-1
|
|
114
|
+
rounded-full text-xs font-semibold
|
|
115
|
+
{active === tab.id ? 'bg-brand/40 text-brand' : 'bg-surface-2 text-faint'}"
|
|
116
|
+
>
|
|
117
|
+
{tab.count}
|
|
118
|
+
</span>
|
|
119
|
+
{/if}
|
|
120
|
+
</button>
|
|
121
|
+
{/each}
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<!-- Outside the scroll box and out of the accessibility tree: a picture of
|
|
125
|
+
the scrollbar the phone will not draw, saying nothing the tablist has
|
|
126
|
+
not already said. -->
|
|
127
|
+
{#if moreBefore}
|
|
128
|
+
<div
|
|
129
|
+
aria-hidden="true"
|
|
130
|
+
data-print="hide"
|
|
131
|
+
data-testid="tabs-more-before"
|
|
132
|
+
class="{SCROLL_EDGE} {SCROLL_EDGE_START}"
|
|
133
|
+
></div>
|
|
134
|
+
{/if}
|
|
135
|
+
{#if moreAfter}
|
|
136
|
+
<div
|
|
137
|
+
aria-hidden="true"
|
|
138
|
+
data-print="hide"
|
|
139
|
+
data-testid="tabs-more-after"
|
|
140
|
+
class="{SCROLL_EDGE} {SCROLL_EDGE_END}"
|
|
141
|
+
></div>
|
|
142
|
+
{/if}
|
|
39
143
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { AccentTone } from '../internal/tone.js';
|
|
3
|
+
import { HIT_AREA } from '../internal/touch.js';
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
label: string;
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
type="button"
|
|
38
39
|
onclick={onremove}
|
|
39
40
|
aria-label="Remove {label}"
|
|
40
|
-
class="ms-0.5 rounded transition-opacity hover:opacity-60 leading-none"
|
|
41
|
+
class="{HIT_AREA} ms-0.5 rounded transition-opacity hover:opacity-60 leading-none"
|
|
41
42
|
>
|
|
42
43
|
<svg
|
|
43
44
|
width="10"
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* implies the state.
|
|
16
16
|
*/
|
|
17
17
|
import { Monitor, Moon, Sun } from '@lucide/svelte';
|
|
18
|
+
import { TOUCH_GROW } from '../internal/touch.js';
|
|
18
19
|
import {
|
|
19
20
|
THEME_PREFERENCES,
|
|
20
21
|
getThemePreference,
|
|
@@ -81,7 +82,7 @@
|
|
|
81
82
|
data-print="hide"
|
|
82
83
|
data-theme-preference={preference}
|
|
83
84
|
aria-label="Theme: {LABEL[preference]}. Switch to {LABEL[upcoming]}."
|
|
84
|
-
class="rounded-lg p-2 text-faint transition-colors
|
|
85
|
+
class="{TOUCH_GROW} rounded-lg p-2 text-faint transition-colors hover:bg-surface-2 hover:text-fg active:bg-line active:text-fg {klass}"
|
|
85
86
|
>
|
|
86
87
|
<!-- The icon shows the state the control is in, not the one it moves to. It
|
|
87
88
|
showed the destination while the label named the same destination, which
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { toast } from '../stores/toast.svelte';
|
|
3
|
+
import { HIT_AREA } from '../internal/touch.js';
|
|
3
4
|
import { TONE_GLYPH, statusTone, type StatusTone } from '../internal/tone.js';
|
|
5
|
+
import * as motion from '../motion.js';
|
|
4
6
|
|
|
5
7
|
const styles: Record<StatusTone, { bar: string; icon: string }> = {
|
|
6
8
|
neutral: { bar: 'bg-muted', icon: 'text-muted' },
|
|
@@ -25,8 +27,10 @@
|
|
|
25
27
|
>
|
|
26
28
|
{#each toast.items as t (t.id)}
|
|
27
29
|
<div
|
|
30
|
+
transition:motion.toast|global
|
|
31
|
+
animate:motion.reorder
|
|
28
32
|
class="pointer-events-auto flex items-start gap-3 overflow-hidden rounded-lg border border-line
|
|
29
|
-
bg-surface ps-0 pe-3 py-3 shadow-xl
|
|
33
|
+
bg-surface ps-0 pe-3 py-3 shadow-xl"
|
|
30
34
|
>
|
|
31
35
|
<span class="w-1 self-stretch shrink-0 {styles[statusTone(t.tone)].bar}"></span>
|
|
32
36
|
<span
|
|
@@ -51,7 +55,7 @@
|
|
|
51
55
|
<p class="flex-1 text-sm text-fg leading-snug">{t.message}</p>
|
|
52
56
|
<button
|
|
53
57
|
type="button"
|
|
54
|
-
class="shrink-0 rounded text-faint transition-colors
|
|
58
|
+
class="{HIT_AREA} shrink-0 rounded text-faint transition-colors hover:text-fg outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand"
|
|
55
59
|
aria-label="Dismiss"
|
|
56
60
|
onclick={() => toast.dismiss(t.id)}
|
|
57
61
|
>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { FIELD_HINT } from '../internal/field.js';
|
|
3
|
+
import { HIT_AREA } from '../internal/touch.js';
|
|
3
4
|
let {
|
|
4
5
|
checked = $bindable(false),
|
|
5
6
|
label,
|
|
@@ -79,14 +80,14 @@
|
|
|
79
80
|
aria-label={label ?? 'Toggle'}
|
|
80
81
|
{disabled}
|
|
81
82
|
onclick={handleClick}
|
|
82
|
-
class="
|
|
83
|
+
class="{HIT_AREA} shrink-0 rounded-full border transition-colors outline-none
|
|
83
84
|
focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-1
|
|
84
85
|
focus-visible:ring-offset-ink mt-0.5
|
|
85
86
|
{track[size]}
|
|
86
87
|
{checked ? trackPaint.on : trackPaint.off}"
|
|
87
88
|
>
|
|
88
89
|
<span
|
|
89
|
-
class="absolute rounded-full bg-fg shadow transition-transform
|
|
90
|
+
class="absolute rounded-full bg-fg shadow transition-transform duration-base
|
|
90
91
|
{thumb[size]}
|
|
91
92
|
{checked ? thumbOn[size] : 'translate-x-0'}"
|
|
92
93
|
></span>
|
|
@@ -125,18 +125,23 @@
|
|
|
125
125
|
{onkeydown}
|
|
126
126
|
>
|
|
127
127
|
{@render children()}
|
|
128
|
-
<!-- Always rendered,
|
|
128
|
+
<!-- Always rendered, and shut with visibility rather than removed:
|
|
129
129
|
aria-describedby resolves the text of a hidden element, so the trigger
|
|
130
|
-
keeps a stable target.
|
|
131
|
-
|
|
130
|
+
keeps a stable target. Visibility is one of the properties Tailwind's
|
|
131
|
+
`transition` moves, discretely, so the hint fades and settles on the way
|
|
132
|
+
in and is invisible only once it has faded out. Left hoverable rather
|
|
133
|
+
than pointer-events-none, so moving onto it does not dismiss it, which
|
|
134
|
+
SC 1.4.13 also requires. -->
|
|
132
135
|
<span
|
|
133
136
|
{id}
|
|
134
137
|
role={describe ? 'tooltip' : undefined}
|
|
135
138
|
aria-hidden={describe ? undefined : 'true'}
|
|
136
|
-
hidden={!visible}
|
|
137
139
|
style="top: {anchor.top}px; left: {anchor.left}px"
|
|
138
140
|
class="fixed {pos[position]} z-tooltip w-max max-w-[min(16rem,calc(100vw-2rem))]
|
|
139
|
-
rounded-lg border border-line bg-surface-2 px-2.5 py-1.5 text-xs text-fg shadow-xl
|
|
141
|
+
rounded-lg border border-line bg-surface-2 px-2.5 py-1.5 text-xs text-fg shadow-xl
|
|
142
|
+
transition {visible
|
|
143
|
+
? 'duration-base ease-enter'
|
|
144
|
+
: 'invisible scale-[0.96] opacity-0 duration-fast ease-exit'}"
|
|
140
145
|
>
|
|
141
146
|
{text}
|
|
142
147
|
</span>
|
|
@@ -270,7 +270,7 @@
|
|
|
270
270
|
scan, so no rule is generated and every row draws flush left.
|
|
271
271
|
-->
|
|
272
272
|
<div
|
|
273
|
-
class="flex w-full items-center gap-2 rounded-md py-1.5 pe-2 text-sm transition-colors
|
|
273
|
+
class="flex w-full items-center gap-2 rounded-md py-1.5 pe-2 text-sm transition-colors {rowPaint(
|
|
274
274
|
selected === node.id,
|
|
275
275
|
!!node.disabled,
|
|
276
276
|
)}"
|
|
@@ -283,7 +283,7 @@
|
|
|
283
283
|
>
|
|
284
284
|
<ChevronRight
|
|
285
285
|
size={14}
|
|
286
|
-
class="transition-transform duration-
|
|
286
|
+
class="transition-transform duration-base {open ? 'rotate-90' : 'rtl:rotate-180'}"
|
|
287
287
|
/>
|
|
288
288
|
</span>
|
|
289
289
|
{:else}
|
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Dialog - modal frame component.
|
|
4
4
|
*
|
|
5
|
-
* Handles: focus trap, ESC to close, click-outside to close,
|
|
6
|
-
*
|
|
5
|
+
* Handles: focus trap, ESC to close, click-outside to close, the entrance
|
|
6
|
+
* and exit through the kit's motion presets, stacked z-indexing, portal to
|
|
7
|
+
* body.
|
|
7
8
|
*
|
|
8
9
|
* Rendered by DialogContainer for each entry in the dialog stack.
|
|
9
10
|
* Not meant to be used directly - use openDialog() from dialog-manager.
|
|
10
11
|
*/
|
|
11
12
|
import { X } from '@lucide/svelte';
|
|
12
|
-
import { onMount } from 'svelte';
|
|
13
13
|
import type { Snippet } from 'svelte';
|
|
14
14
|
import type { DialogEntry } from './types.js';
|
|
15
15
|
import { sizeClass } from './types.js';
|
|
16
16
|
import { overlay } from '../../internal/overlay.js';
|
|
17
|
+
import * as motion from '../../motion.js';
|
|
17
18
|
import { closeDialog, dismissDialog } from './dialog-manager.svelte';
|
|
18
19
|
import ConfirmDialog from './ConfirmDialog.svelte';
|
|
19
20
|
|
|
@@ -25,13 +26,6 @@
|
|
|
25
26
|
children?: Snippet;
|
|
26
27
|
} = $props();
|
|
27
28
|
|
|
28
|
-
// ──────────────────────────────────────────────────────
|
|
29
|
-
// Animation state
|
|
30
|
-
// ──────────────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
let visible = $state(false);
|
|
33
|
-
let exiting = $state(false);
|
|
34
|
-
|
|
35
29
|
// Stacked offset: each deeper dialog shrinks, shifts back and rises
|
|
36
30
|
let offset = $derived(entry.depth);
|
|
37
31
|
|
|
@@ -47,12 +41,9 @@
|
|
|
47
41
|
|
|
48
42
|
// Focus entry, the Tab trap, the body scroll lock and focus restore all come
|
|
49
43
|
// from `use:overlay` on the panel below, which is the same implementation
|
|
50
|
-
// Modal and Drawer use.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
visible = true;
|
|
54
|
-
});
|
|
55
|
-
});
|
|
44
|
+
// Modal and Drawer use. The entrance and the exit are the same presets too:
|
|
45
|
+
// removing the entry from the stack plays the exit before the element goes,
|
|
46
|
+
// so nothing here waits on a timer to match a duration written elsewhere.
|
|
56
47
|
|
|
57
48
|
// ──────────────────────────────────────────────────────
|
|
58
49
|
// Handlers
|
|
@@ -74,16 +65,11 @@
|
|
|
74
65
|
}
|
|
75
66
|
}
|
|
76
67
|
|
|
77
|
-
|
|
78
|
-
exiting = true;
|
|
79
|
-
// Wait for exit animation
|
|
80
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
68
|
+
function handleDismiss(): void {
|
|
81
69
|
dismissDialog(entry.id);
|
|
82
70
|
}
|
|
83
71
|
|
|
84
|
-
|
|
85
|
-
exiting = true;
|
|
86
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
72
|
+
function handleClose(value?: unknown): void {
|
|
87
73
|
closeDialog(value, entry.id);
|
|
88
74
|
}
|
|
89
75
|
</script>
|
|
@@ -107,9 +93,8 @@
|
|
|
107
93
|
<!-- Backdrop -->
|
|
108
94
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
109
95
|
<div
|
|
110
|
-
|
|
111
|
-
class
|
|
112
|
-
class:opacity-100={visible && !exiting}
|
|
96
|
+
transition:motion.scrim|global
|
|
97
|
+
class="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
|
113
98
|
onclick={handleBackdropClick}
|
|
114
99
|
onkeydown={(e: KeyboardEvent) => {
|
|
115
100
|
if (e.key === 'Escape' && !entry.options.persistent) handleDismiss();
|
|
@@ -122,14 +107,12 @@
|
|
|
122
107
|
<div
|
|
123
108
|
bind:this={dialogEl}
|
|
124
109
|
use:overlay
|
|
110
|
+
transition:motion.dialog|global
|
|
125
111
|
class="relative w-full {sizeClass(entry.options.size ?? 'md')} mx-4
|
|
126
112
|
bg-surface border border-line rounded-xl shadow-2xl
|
|
127
|
-
transition-
|
|
113
|
+
transition-transform duration-slow
|
|
128
114
|
focus:outline-none focus-visible:ring-2 focus-visible:ring-brand"
|
|
129
|
-
|
|
130
|
-
class:opacity-100={visible && !exiting}
|
|
131
|
-
style="transform: scale({visible && !exiting ? 1 - offset * 0.03 : 0.95}) translateY({offset *
|
|
132
|
-
16}px);
|
|
115
|
+
style="transform: scale({1 - offset * 0.03}) translateY({offset * 16}px);
|
|
133
116
|
transform-origin: center;"
|
|
134
117
|
role="dialog"
|
|
135
118
|
aria-modal="true"
|
|
@@ -158,7 +141,7 @@
|
|
|
158
141
|
{#if !entry.options.persistent}
|
|
159
142
|
<button
|
|
160
143
|
class="inline-flex items-center justify-center w-8 h-8 -me-2 rounded-lg
|
|
161
|
-
text-muted hover:text-fg hover:bg-surface-2 transition-colors
|
|
144
|
+
text-muted hover:text-fg hover:bg-surface-2 transition-colors shrink-0"
|
|
162
145
|
onclick={() => handleClose()}
|
|
163
146
|
aria-label="Close"
|
|
164
147
|
>
|
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export { default as DescriptionList } from './components/DescriptionList.svelte'
|
|
|
21
21
|
export { default as Toolbar } from './components/Toolbar.svelte';
|
|
22
22
|
export { default as TreeView } from './components/TreeView.svelte';
|
|
23
23
|
export type { CellFit } from './components/Table.svelte';
|
|
24
|
+
export type { SidebarState } from './components/AppShell.svelte';
|
|
24
25
|
export type { PageWidth, SectionVariant } from './internal/layout.js';
|
|
25
26
|
export type { TreeNode, TriState } from './internal/tree.js';
|
|
26
27
|
export { default as Button } from './components/Button.svelte';
|
|
@@ -89,4 +90,6 @@ export type { ConfirmOptions } from './components/dialog/dialog-manager.svelte.j
|
|
|
89
90
|
export type { DialogOptions, DialogEntry, DialogSize } from './components/dialog/types.js';
|
|
90
91
|
export { cn, type ClassValue } from './utils/cn.js';
|
|
91
92
|
export { getTheme, getThemePreference, nextThemePreference, resolveTheme, setTheme, setThemePreference, systemTheme, themeBootScript, toggleTheme, watchSystemTheme, THEME_PREFERENCES, type Theme, type ThemePreference, } from './utils/theme.js';
|
|
92
|
-
export
|
|
93
|
+
export * as motion from './motion.js';
|
|
94
|
+
export type { Rung as MotionRung, Curve as MotionCurve } from './motion.js';
|
|
95
|
+
export declare const VERSION = "0.25.1";
|
package/dist/index.js
CHANGED
|
@@ -93,7 +93,13 @@ export { openDialog, closeDialog, dismissDialog, dismissAllDialogs, confirm, set
|
|
|
93
93
|
// ── Utilities ──────────────────────────────────────────────────────────────
|
|
94
94
|
export { cn } from './utils/cn.js';
|
|
95
95
|
export { getTheme, getThemePreference, nextThemePreference, resolveTheme, setTheme, setThemePreference, systemTheme, themeBootScript, toggleTheme, watchSystemTheme, THEME_PREFERENCES, } from './utils/theme.js';
|
|
96
|
+
// ── Motion ─────────────────────────────────────────────────────────────────
|
|
97
|
+
// The entrances and exits as Svelte transitions, for a surface a consumer
|
|
98
|
+
// builds that the kit does not ship: `transition:motion.popover`,
|
|
99
|
+
// `animate:motion.reorder`. They read the duration and easing tokens, so a
|
|
100
|
+
// page that uses them cannot drift from the components beside it.
|
|
101
|
+
export * as motion from './motion.js';
|
|
96
102
|
// ── Version ────────────────────────────────────────────────────────────────
|
|
97
103
|
// Generated from package.json by `pnpm version:sync`. Bump package.json, never
|
|
98
104
|
// this line; the build and the test suite fail when the two disagree.
|
|
99
|
-
export const VERSION = '0.
|
|
105
|
+
export const VERSION = '0.25.1';
|
package/dist/internal/choice.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* Not exported from the package entry point - this is an implementation detail.
|
|
16
16
|
*/
|
|
17
17
|
import { FIELD_HINT, FIELD_LABEL } from './field.js';
|
|
18
|
+
import { HIT_AREA } from './touch.js';
|
|
18
19
|
/**
|
|
19
20
|
* The box and the icon slot share one size map, so the label text starts at the
|
|
20
21
|
* same x whether or not an option carries an icon. 3.5 is 14px, 4 is 16px and 5
|
|
@@ -25,8 +26,14 @@ const CHOICE_SQUARE = {
|
|
|
25
26
|
md: 'h-4 w-4',
|
|
26
27
|
lg: 'h-5 w-5',
|
|
27
28
|
};
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The inline wrapper carries the hit box, not the box inside it. A click
|
|
31
|
+
* anywhere on the label reaches the input, so growing the label's hit area
|
|
32
|
+
* under a finger is what makes a 16px checkbox a 44px target without drawing
|
|
33
|
+
* it any larger. A card is its own surface and already clears the size.
|
|
34
|
+
*/
|
|
35
|
+
const WRAP_INLINE = `${HIT_AREA} inline-flex select-none items-start gap-2.5`;
|
|
36
|
+
const WRAP_CARD = 'flex select-none items-start gap-2.5 rounded-lg border p-3 transition-colors';
|
|
30
37
|
/**
|
|
31
38
|
* The clickable wrapper. Card is a bordered option the whole surface of which
|
|
32
39
|
* is the target.
|
|
@@ -55,7 +62,7 @@ export function choiceWrap(variant, checked, disabled) {
|
|
|
55
62
|
* instead, and `peer` is what lets the box below read its focus state.
|
|
56
63
|
*/
|
|
57
64
|
export const CHOICE_INPUT = 'peer absolute inset-0 m-0 h-full w-full cursor-pointer opacity-0 disabled:cursor-not-allowed';
|
|
58
|
-
const BOX_BASE = 'pointer-events-none flex shrink-0 items-center justify-center transition-colors
|
|
65
|
+
const BOX_BASE = 'pointer-events-none flex shrink-0 items-center justify-center transition-colors';
|
|
59
66
|
/**
|
|
60
67
|
* The painted box or circle. Square for a checkbox, round for a radio, sized by
|
|
61
68
|
* ChoiceSize.
|
package/dist/internal/field.js
CHANGED
|
@@ -34,12 +34,12 @@ export const FIELD_ERROR = 'text-xs text-danger';
|
|
|
34
34
|
* CONTROL_SEGMENT already pair the two correctly; these did half of it.
|
|
35
35
|
*/
|
|
36
36
|
export const CONTROL_BASE = 'w-full h-control rounded-lg bg-surface-2 border px-3 text-sm text-fg ' +
|
|
37
|
-
'placeholder:text-faint transition-colors
|
|
37
|
+
'placeholder:text-faint transition-colors outline-none ' +
|
|
38
38
|
'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand ' +
|
|
39
39
|
'disabled:opacity-50 disabled:cursor-not-allowed';
|
|
40
40
|
/** As CONTROL_BASE, for controls whose height is driven by their content. */
|
|
41
41
|
export const CONTROL_MULTILINE = 'w-full min-h-control rounded-lg bg-surface-2 border px-3 py-2 text-sm text-fg ' +
|
|
42
|
-
'placeholder:text-faint transition-colors
|
|
42
|
+
'placeholder:text-faint transition-colors outline-none ' +
|
|
43
43
|
'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand ' +
|
|
44
44
|
'disabled:opacity-50 disabled:cursor-not-allowed';
|
|
45
45
|
/**
|
|
@@ -88,7 +88,7 @@ export function describedBy(id, error, hint) {
|
|
|
88
88
|
* held the caret.
|
|
89
89
|
*/
|
|
90
90
|
export const CONTROL_SEGMENTED = 'inline-flex h-control items-center gap-0.5 rounded-lg bg-surface-2 border px-3 ' +
|
|
91
|
-
'text-sm text-fg transition-colors
|
|
91
|
+
'text-sm text-fg transition-colors';
|
|
92
92
|
/** As controlBorder, for a wrapper that is only ever focused through a child. */
|
|
93
93
|
export function segmentedBorder(error) {
|
|
94
94
|
return error
|
|
@@ -139,7 +139,16 @@ export declare function sectionHeading(level: 2 | 3, variant?: SectionVariant):
|
|
|
139
139
|
* `border-e` and not `border-r`: the shell is the one place a right-to-left
|
|
140
140
|
* locale flips, and a physical border leaves the rule on the wrong edge.
|
|
141
141
|
*/
|
|
142
|
-
export declare const APP_SIDEBAR = "h-full
|
|
142
|
+
export declare const APP_SIDEBAR = "h-full shrink-0 flex-col border-e border-line bg-surface";
|
|
143
|
+
/**
|
|
144
|
+
* The sidebar's two widths. Expanded is the 224px column every authed screen
|
|
145
|
+
* had. The rail is the 56px icon column the theme had named and nothing used:
|
|
146
|
+
* at 768px the expanded column left 496px for the page, and a flow editor with
|
|
147
|
+
* two docked panes had no canvas at all. The shell picks between them; the
|
|
148
|
+
* width is not part of APP_SIDEBAR so the aside cannot carry both.
|
|
149
|
+
*/
|
|
150
|
+
export declare const APP_SIDEBAR_WIDE = "w-sidebar";
|
|
151
|
+
export declare const APP_SIDEBAR_RAIL = "w-nav-rail";
|
|
143
152
|
/**
|
|
144
153
|
* The header bar. Its gutter is the page gutter, so the section name in the
|
|
145
154
|
* header sits directly above the page title under it rather than 8px off.
|
package/dist/internal/layout.js
CHANGED
|
@@ -163,7 +163,16 @@ export function sectionHeading(level, variant = 'default') {
|
|
|
163
163
|
* `border-e` and not `border-r`: the shell is the one place a right-to-left
|
|
164
164
|
* locale flips, and a physical border leaves the rule on the wrong edge.
|
|
165
165
|
*/
|
|
166
|
-
export const APP_SIDEBAR = 'h-full
|
|
166
|
+
export const APP_SIDEBAR = 'h-full shrink-0 flex-col border-e border-line bg-surface';
|
|
167
|
+
/**
|
|
168
|
+
* The sidebar's two widths. Expanded is the 224px column every authed screen
|
|
169
|
+
* had. The rail is the 56px icon column the theme had named and nothing used:
|
|
170
|
+
* at 768px the expanded column left 496px for the page, and a flow editor with
|
|
171
|
+
* two docked panes had no canvas at all. The shell picks between them; the
|
|
172
|
+
* width is not part of APP_SIDEBAR so the aside cannot carry both.
|
|
173
|
+
*/
|
|
174
|
+
export const APP_SIDEBAR_WIDE = 'w-sidebar';
|
|
175
|
+
export const APP_SIDEBAR_RAIL = 'w-nav-rail';
|
|
167
176
|
/**
|
|
168
177
|
* The header bar. Its gutter is the page gutter, so the section name in the
|
|
169
178
|
* header sits directly above the page title under it rather than 8px off.
|
package/dist/internal/panel.d.ts
CHANGED
|
@@ -125,10 +125,19 @@ export declare function panelOption(state: {
|
|
|
125
125
|
}): string;
|
|
126
126
|
/** Marks the scrolling region inside a surface, so placePanel can find it. */
|
|
127
127
|
export declare const PANEL_LIST_ATTR = "data-panel-list";
|
|
128
|
+
/**
|
|
129
|
+
* The attribute's value when the region is capped by the room alone.
|
|
130
|
+
*
|
|
131
|
+
* The token cap is sized for a list of rows, and a calendar is not one: six
|
|
132
|
+
* weeks of days stand taller than the token, so under it every month would
|
|
133
|
+
* scroll by a few rows. Such a region still takes the room as its cap, so
|
|
134
|
+
* inside a short modal it scrolls rather than being cut.
|
|
135
|
+
*/
|
|
136
|
+
export declare const PANEL_LIST_UNCAPPED = "uncapped";
|
|
128
137
|
/** The classes that put the surface under its anchor. */
|
|
129
|
-
export declare const PANEL_BELOW = "top-full mt-1";
|
|
138
|
+
export declare const PANEL_BELOW = "top-full mt-1 origin-top";
|
|
130
139
|
/** The classes that put the surface over its anchor. */
|
|
131
|
-
export declare const PANEL_ABOVE = "bottom-full mb-1";
|
|
140
|
+
export declare const PANEL_ABOVE = "bottom-full mb-1 origin-bottom";
|
|
132
141
|
/**
|
|
133
142
|
* Svelte action for a panel surface. Puts it on the side of its anchor that
|
|
134
143
|
* has room, and stops its list running past whatever clips it.
|
|
@@ -145,7 +154,8 @@ export declare const PANEL_ABOVE = "bottom-full mb-1";
|
|
|
145
154
|
* under the modal's scroll edge that nobody can reach without scrolling
|
|
146
155
|
* first. Whichever side wins, the list's max-height is the smaller of the
|
|
147
156
|
* token cap and the room on that side, written as a CSS min so the token stays
|
|
148
|
-
* the cap without this code knowing its value.
|
|
157
|
+
* the cap without this code knowing its value. A region marked uncapped takes
|
|
158
|
+
* the room alone.
|
|
149
159
|
*
|
|
150
160
|
* Measured on mount, on resize and on scroll of the clipping ancestor, since
|
|
151
161
|
* each moves the anchor relative to the edge. Scroll does not bubble, so the
|
package/dist/internal/panel.js
CHANGED
|
@@ -73,7 +73,7 @@ export const PANEL_LIST = 'max-h-panel-max overflow-y-auto overscroll-contain px
|
|
|
73
73
|
* the active ring follow the row's own corners.
|
|
74
74
|
*/
|
|
75
75
|
export const PANEL_OPTION = 'flex w-full items-center gap-2.5 rounded-lg px-2 py-2 text-start text-sm ' +
|
|
76
|
-
'transition-colors
|
|
76
|
+
'transition-colors outline-none hover:bg-surface-2 ' +
|
|
77
77
|
'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand';
|
|
78
78
|
/**
|
|
79
79
|
* The active descendant.
|
|
@@ -134,10 +134,19 @@ export function panelOption(state) {
|
|
|
134
134
|
}
|
|
135
135
|
/** Marks the scrolling region inside a surface, so placePanel can find it. */
|
|
136
136
|
export const PANEL_LIST_ATTR = 'data-panel-list';
|
|
137
|
+
/**
|
|
138
|
+
* The attribute's value when the region is capped by the room alone.
|
|
139
|
+
*
|
|
140
|
+
* The token cap is sized for a list of rows, and a calendar is not one: six
|
|
141
|
+
* weeks of days stand taller than the token, so under it every month would
|
|
142
|
+
* scroll by a few rows. Such a region still takes the room as its cap, so
|
|
143
|
+
* inside a short modal it scrolls rather than being cut.
|
|
144
|
+
*/
|
|
145
|
+
export const PANEL_LIST_UNCAPPED = 'uncapped';
|
|
137
146
|
/** The classes that put the surface under its anchor. */
|
|
138
|
-
export const PANEL_BELOW = 'top-full mt-1';
|
|
147
|
+
export const PANEL_BELOW = 'top-full mt-1 origin-top';
|
|
139
148
|
/** The classes that put the surface over its anchor. */
|
|
140
|
-
export const PANEL_ABOVE = 'bottom-full mb-1';
|
|
149
|
+
export const PANEL_ABOVE = 'bottom-full mb-1 origin-bottom';
|
|
141
150
|
/**
|
|
142
151
|
* Breathing room between the panel and the edge it is measured against, so a
|
|
143
152
|
* panel that fits exactly does not sit on the edge of the window or the modal.
|
|
@@ -183,7 +192,8 @@ function swap(node, remove, add) {
|
|
|
183
192
|
* under the modal's scroll edge that nobody can reach without scrolling
|
|
184
193
|
* first. Whichever side wins, the list's max-height is the smaller of the
|
|
185
194
|
* token cap and the room on that side, written as a CSS min so the token stays
|
|
186
|
-
* the cap without this code knowing its value.
|
|
195
|
+
* the cap without this code knowing its value. A region marked uncapped takes
|
|
196
|
+
* the room alone.
|
|
187
197
|
*
|
|
188
198
|
* Measured on mount, on resize and on scroll of the clipping ancestor, since
|
|
189
199
|
* each moves the anchor relative to the edge. Scroll does not bubble, so the
|
|
@@ -221,7 +231,8 @@ export function placePanel(node) {
|
|
|
221
231
|
// taken out of the room so the whole panel fits and not only its list.
|
|
222
232
|
const chrome = Math.max(0, node.offsetHeight - list.offsetHeight);
|
|
223
233
|
const room = Math.max(0, Math.round((flip ? above : below) - chrome));
|
|
224
|
-
|
|
234
|
+
const capped = list.getAttribute(PANEL_LIST_ATTR) !== PANEL_LIST_UNCAPPED;
|
|
235
|
+
list.style.maxHeight = capped ? `min(var(--spacing-panel-max), ${room}px)` : `${room}px`;
|
|
225
236
|
}
|
|
226
237
|
measure();
|
|
227
238
|
const scroller = clip ?? window;
|