@lyeve-labs/ui-kit 0.17.1 → 0.18.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 +16 -13
- package/dist/components/AccordionItem.svelte +1 -1
- package/dist/components/AccountMenu.svelte +1 -1
- package/dist/components/AppShell.svelte +20 -6
- package/dist/components/Autocomplete.svelte +10 -1
- package/dist/components/Card.svelte +1 -0
- package/dist/components/Collapsible.svelte +1 -1
- package/dist/components/DatePicker.svelte +1 -1
- package/dist/components/Drawer.svelte +6 -26
- package/dist/components/Dropdown.svelte +196 -10
- package/dist/components/Dropdown.svelte.d.ts +12 -0
- package/dist/components/Logo.svelte +77 -0
- package/dist/components/Logo.svelte.d.ts +35 -0
- package/dist/components/Modal.svelte +2 -15
- package/dist/components/MultiSelect.svelte +12 -3
- package/dist/components/PageHeader.svelte +23 -1
- package/dist/components/Panel.svelte +1 -1
- package/dist/components/SearchInput.svelte +10 -1
- package/dist/components/Table.svelte +143 -10
- package/dist/components/Table.svelte.d.ts +7 -0
- package/dist/components/ThemeToggle.svelte +75 -12
- package/dist/components/ThemeToggle.svelte.d.ts +9 -0
- package/dist/components/Toaster.svelte +3 -15
- package/dist/components/Tooltip.svelte +1 -1
- package/dist/components/dialog/Dialog.svelte +9 -4
- package/dist/components/dialog/dialog-manager.svelte.d.ts +20 -1
- package/dist/components/dialog/dialog-manager.svelte.js +13 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +6 -2
- package/dist/internal/layout.js +11 -2
- package/dist/internal/panel.d.ts +1 -1
- package/dist/internal/panel.js +1 -1
- package/dist/styles/theme.css +235 -0
- package/dist/utils/theme.d.ts +68 -3
- package/dist/utils/theme.js +118 -3
- package/package.json +18 -19
- package/src/lib/styles/theme.css +235 -0
|
@@ -199,11 +199,20 @@
|
|
|
199
199
|
event.stopPropagation();
|
|
200
200
|
remove(option.value);
|
|
201
201
|
}}
|
|
202
|
-
class="transition-colors duration-150 hover:text-brand-light"
|
|
202
|
+
class="-m-1.5 p-1.5 transition-colors duration-150 hover:text-brand-light"
|
|
203
203
|
>
|
|
204
|
+
<!--
|
|
205
|
+
12px inside 6px of padding is a 24px target, which is the floor
|
|
206
|
+
SC 2.5.8 puts on a control that cannot claim the spacing
|
|
207
|
+
exception, and this one sits inside the trigger. The padding is
|
|
208
|
+
taken straight back off as margin, so the chip is laid out
|
|
209
|
+
exactly as it was and only the reachable area grew. The glyph
|
|
210
|
+
is the same 12px cross Autocomplete clears its value with; it
|
|
211
|
+
was drawn a pixel smaller here for no reason anyone recorded.
|
|
212
|
+
-->
|
|
204
213
|
<svg
|
|
205
|
-
width="
|
|
206
|
-
height="
|
|
214
|
+
width="12"
|
|
215
|
+
height="12"
|
|
207
216
|
viewBox="0 0 24 24"
|
|
208
217
|
fill="none"
|
|
209
218
|
stroke="currentColor"
|
|
@@ -43,9 +43,31 @@
|
|
|
43
43
|
<div class="min-w-0">
|
|
44
44
|
<!-- The app shells render their own h1 for the current route, so a heading
|
|
45
45
|
lookup by name matches two elements. This names the page's own title. -->
|
|
46
|
+
<!--
|
|
47
|
+
The page title is the one place the brand type scale had a component to
|
|
48
|
+
land on and did not: eighteen type tokens shipped and nothing referenced
|
|
49
|
+
one of them, while this rendered at 24px, a size the ramp does not
|
|
50
|
+
contain.
|
|
51
|
+
|
|
52
|
+
text-h2 and not text-h1. The brand ramp is a marketing ramp - it runs
|
|
53
|
+
64 / 44 / 32 / 22 / 16 and holds nothing between 16 and 22, which is
|
|
54
|
+
where a console's own type lives. A 44px page title over a 14px table on
|
|
55
|
+
a 390px screen is the ramp applied rather than adopted. 32px is the step
|
|
56
|
+
the ramp does hold and the step this was already reaching for.
|
|
57
|
+
|
|
58
|
+
The weight stays at 700, which is the ramp's H1 weight, because this is
|
|
59
|
+
still the page's h1. Taking H2's 600 along with its size would move the
|
|
60
|
+
title twice for one decision.
|
|
61
|
+
|
|
62
|
+
Each size token names its own leading and tracking. A `text-*` token
|
|
63
|
+
built from `--text-*` alone sets font-size and leaves line-height to
|
|
64
|
+
whatever it inherits, which is not the ratio the ramp specifies.
|
|
65
|
+
-->
|
|
46
66
|
<h1
|
|
47
67
|
data-testid="page-title"
|
|
48
|
-
class={compact
|
|
68
|
+
class={compact
|
|
69
|
+
? 'text-sm font-semibold text-fg'
|
|
70
|
+
: 'text-h2 leading-h2 tracking-h2 font-bold text-fg'}
|
|
49
71
|
>
|
|
50
72
|
{title}
|
|
51
73
|
</h1>
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
const surface = $derived(`${SURFACE[variant]} ${CARD_PAD[pad]}`.trim());
|
|
69
69
|
</script>
|
|
70
70
|
|
|
71
|
-
<div class="flex flex-col gap-stack {surface} {klass}">
|
|
71
|
+
<div data-print="keep" class="flex flex-col gap-stack {surface} {klass}">
|
|
72
72
|
{#snippet headingText()}
|
|
73
73
|
{#if icon}
|
|
74
74
|
{@const Icon = icon}
|
|
@@ -71,11 +71,20 @@
|
|
|
71
71
|
/>
|
|
72
72
|
|
|
73
73
|
{#if value}
|
|
74
|
+
<!--
|
|
75
|
+
14px inside 6px of padding is a 26px target, past the 24px floor SC
|
|
76
|
+
2.5.8 puts on a control with no spacing exception to claim, and this one
|
|
77
|
+
sits inside the field. The padding is taken back off the horizontal axis
|
|
78
|
+
only: the box is positioned from its right edge, so a negative right
|
|
79
|
+
margin holds the glyph exactly where it was, while a negative top margin
|
|
80
|
+
would fight the translate that centres it and lift it 6px off the middle
|
|
81
|
+
of the field.
|
|
82
|
+
-->
|
|
74
83
|
<button
|
|
75
84
|
type="button"
|
|
76
85
|
onclick={clear}
|
|
77
86
|
aria-label="Clear search"
|
|
78
|
-
class="absolute right-2.5 top-1/2 -translate-y-1/2 text-faint
|
|
87
|
+
class="absolute right-2.5 top-1/2 -mx-1.5 -translate-y-1/2 p-1.5 text-faint
|
|
79
88
|
transition-colors duration-150 hover:text-fg"
|
|
80
89
|
>
|
|
81
90
|
<svg
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
striped?: boolean;
|
|
6
6
|
hoverable?: boolean;
|
|
7
7
|
fixed?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Names the scroll box, so a screen reader announces what the region it
|
|
10
|
+
* just landed in holds. Without it the box is still focusable and simply
|
|
11
|
+
* carries no role: an unnamed region is announced as "region" and nothing
|
|
12
|
+
* else, which is a landmark the reader has to enter to identify.
|
|
13
|
+
*/
|
|
14
|
+
label?: string;
|
|
8
15
|
class?: string;
|
|
9
16
|
children: Snippet;
|
|
10
17
|
}
|
|
@@ -13,26 +20,152 @@
|
|
|
13
20
|
striped = false,
|
|
14
21
|
hoverable = true,
|
|
15
22
|
fixed = false,
|
|
23
|
+
label = undefined,
|
|
16
24
|
class: cls = '',
|
|
17
25
|
children,
|
|
18
26
|
}: Props = $props();
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
* The scroll box is a tab stop. A wide table scrolls sideways, and a mouse is
|
|
30
|
+
* the only thing that could reach the columns past the right edge: nothing
|
|
31
|
+
* inside a plain table takes focus, so the arrow keys never had an element to
|
|
32
|
+
* act on (SC 2.1.1).
|
|
33
|
+
*
|
|
34
|
+
* Spread rather than written on the element. The compiler reads a literal
|
|
35
|
+
* tabindex on any element it does not consider interactive as a defect, and
|
|
36
|
+
* a focusable scroll container is the one case where that is the fix rather
|
|
37
|
+
* than the fault. The rule cannot express it and the gate stays on for
|
|
38
|
+
* everything else, so the attributes are assembled here.
|
|
39
|
+
*/
|
|
40
|
+
const scrollAttrs = $derived(
|
|
41
|
+
label === undefined ? { tabindex: 0 } : { tabindex: 0, role: 'region', 'aria-label': label },
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
* Reachable is not the same as discoverable.
|
|
46
|
+
*
|
|
47
|
+
* Making the box a tab stop answered SC 2.1.1 and told nobody the columns
|
|
48
|
+
* were there. A desktop has a scrollbar to give it away; a phone has an
|
|
49
|
+
* overlay scrollbar that is painted while a finger is moving and is invisible
|
|
50
|
+
* the rest of the time. On the admin's plugin list at 390px the column that
|
|
51
|
+
* falls off the edge is ACTIONS, which is the only thing on the page a reader
|
|
52
|
+
* came to do, and nothing on screen says so.
|
|
53
|
+
*
|
|
54
|
+
* A fade on the edge that has more behind it. Only when the box really
|
|
55
|
+
* overflows, and only on the side the content continues on, so it reads as a
|
|
56
|
+
* property of this table rather than as decoration every table wears.
|
|
57
|
+
*/
|
|
58
|
+
let scroller: HTMLElement | undefined = $state();
|
|
59
|
+
let moreBefore = $state(false);
|
|
60
|
+
let moreAfter = $state(false);
|
|
61
|
+
|
|
62
|
+
function measure() {
|
|
63
|
+
const el = scroller;
|
|
64
|
+
if (!el) return;
|
|
65
|
+
const slack = el.scrollWidth - el.clientWidth;
|
|
66
|
+
// A sub-pixel layout leaves scrollLeft a fraction short of its own maximum
|
|
67
|
+
// at the far end, so an exact comparison never reports the end as reached.
|
|
68
|
+
if (slack <= 1) {
|
|
69
|
+
moreBefore = false;
|
|
70
|
+
moreAfter = false;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// A right-to-left box reports the offset as a negative number, and the two
|
|
74
|
+
// edges are the same two edges either way round.
|
|
75
|
+
const travelled = Math.abs(el.scrollLeft);
|
|
76
|
+
moreBefore = travelled > 1;
|
|
77
|
+
moreAfter = travelled < slack - 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
$effect(() => {
|
|
81
|
+
const el = scroller;
|
|
82
|
+
if (!el) return;
|
|
83
|
+
measure();
|
|
84
|
+
// The width that matters changes without a scroll and without a resize of
|
|
85
|
+
// the window: a column added, a filter applied, a sidebar collapsed. Both
|
|
86
|
+
// boxes are watched because either one moving changes whether the other
|
|
87
|
+
// overflows.
|
|
88
|
+
if (typeof ResizeObserver === 'undefined') return;
|
|
89
|
+
const observer = new ResizeObserver(measure);
|
|
90
|
+
observer.observe(el);
|
|
91
|
+
if (el.firstElementChild) observer.observe(el.firstElementChild);
|
|
92
|
+
return () => observer.disconnect();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/*
|
|
96
|
+
* A dark fade rather than a palette colour. The box has no background of its
|
|
97
|
+
* own and sits on the page in one place and inside a card in another, so no
|
|
98
|
+
* surface token is the colour to fade to. The scrims use black at an alpha
|
|
99
|
+
* for the same reason and it reads in both themes.
|
|
100
|
+
*/
|
|
101
|
+
const EDGE = 'pointer-events-none absolute inset-y-px w-6';
|
|
19
102
|
</script>
|
|
20
103
|
|
|
21
|
-
<div class="w-full
|
|
22
|
-
|
|
23
|
-
|
|
104
|
+
<div data-testid="table-frame" class="relative w-full rounded-xl border border-line {cls}">
|
|
105
|
+
<!--
|
|
106
|
+
data-print: the box exists to clip. On paper there is no viewport to clip
|
|
107
|
+
to, and every column past the edge would simply not be printed.
|
|
108
|
+
-->
|
|
109
|
+
<div
|
|
110
|
+
bind:this={scroller}
|
|
111
|
+
onscroll={measure}
|
|
112
|
+
data-print="unclip"
|
|
113
|
+
data-testid="table-scroll"
|
|
114
|
+
class="w-full overflow-x-auto rounded-xl"
|
|
115
|
+
{...scrollAttrs}
|
|
116
|
+
>
|
|
117
|
+
<!--
|
|
118
|
+
border-separate with no spacing, and the row separators on the cells.
|
|
119
|
+
|
|
120
|
+
The hovered row draws a ring, and a ring is a box-shadow: a browser paints
|
|
121
|
+
no box-shadow on a table row while the table collapses its borders, so the
|
|
122
|
+
same row treatment the panels use would have rendered nothing at all here.
|
|
123
|
+
The separate model paints it. Borders on a row are ignored in that model,
|
|
124
|
+
which is why the lines moved down to the cells they were already drawn
|
|
125
|
+
between; with border-spacing at zero the result is the same line in the
|
|
126
|
+
same place.
|
|
127
|
+
-->
|
|
128
|
+
<table
|
|
129
|
+
class="w-full border-separate border-spacing-0 text-sm text-left
|
|
24
130
|
{fixed ? 'table-fixed' : ''}
|
|
25
|
-
[&
|
|
131
|
+
[&_thead_th]:border-b [&_thead_th]:border-line [&_thead]:bg-surface-2/40
|
|
26
132
|
[&_thead_th]:px-4 [&_thead_th]:py-3 [&_thead_th]:text-xs
|
|
27
133
|
[&_thead_th]:font-medium [&_thead_th]:text-faint
|
|
28
134
|
[&_thead_th]:uppercase [&_thead_th]:tracking-wider [&_thead_th]:whitespace-nowrap
|
|
29
135
|
[&_tbody_td]:px-4 [&_tbody_td]:py-3 [&_tbody_td]:text-fg [&_tbody_td]:align-middle
|
|
30
|
-
[&
|
|
136
|
+
[&_tbody_td]:border-b [&_tbody_td]:border-line [&_tbody_tr:last-child_td]:border-0
|
|
31
137
|
{striped ? '[&_tbody_tr:nth-child(even)]:bg-surface-2/40' : ''}
|
|
32
138
|
{hoverable
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
139
|
+
? '[&_tbody_tr:hover]:bg-surface-2/60 [&_tbody_tr:hover]:ring-1 [&_tbody_tr:hover]:ring-inset [&_tbody_tr:hover]:ring-brand [&_tbody_tr]:transition-colors [&_tbody_tr]:duration-150'
|
|
140
|
+
: ''}"
|
|
141
|
+
>
|
|
142
|
+
{@render children()}
|
|
143
|
+
</table>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<!--
|
|
147
|
+
aria-hidden and outside the scroll box. It is a picture of the scrollbar
|
|
148
|
+
the phone will not draw, it says nothing a screen reader has not already
|
|
149
|
+
been told by the region, and a child of the box would scroll away with the
|
|
150
|
+
content it is describing.
|
|
151
|
+
|
|
152
|
+
The gradient direction is logical: in a right-to-left locale the start edge
|
|
153
|
+
is the right one and the fade has to run the other way with it.
|
|
154
|
+
-->
|
|
155
|
+
{#if moreBefore}
|
|
156
|
+
<div
|
|
157
|
+
aria-hidden="true"
|
|
158
|
+
data-print="hide"
|
|
159
|
+
data-testid="table-more-before"
|
|
160
|
+
class="{EDGE} start-0 rounded-s-xl bg-linear-to-r rtl:bg-linear-to-l from-black/25 to-transparent"
|
|
161
|
+
></div>
|
|
162
|
+
{/if}
|
|
163
|
+
{#if moreAfter}
|
|
164
|
+
<div
|
|
165
|
+
aria-hidden="true"
|
|
166
|
+
data-print="hide"
|
|
167
|
+
data-testid="table-more-after"
|
|
168
|
+
class="{EDGE} end-0 rounded-e-xl bg-linear-to-l rtl:bg-linear-to-r from-black/25 to-transparent"
|
|
169
|
+
></div>
|
|
170
|
+
{/if}
|
|
38
171
|
</div>
|
|
@@ -3,6 +3,13 @@ interface Props {
|
|
|
3
3
|
striped?: boolean;
|
|
4
4
|
hoverable?: boolean;
|
|
5
5
|
fixed?: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Names the scroll box, so a screen reader announces what the region it
|
|
8
|
+
* just landed in holds. Without it the box is still focusable and simply
|
|
9
|
+
* carries no role: an unnamed region is announced as "region" and nothing
|
|
10
|
+
* else, which is a landmark the reader has to enter to identify.
|
|
11
|
+
*/
|
|
12
|
+
label?: string;
|
|
6
13
|
class?: string;
|
|
7
14
|
children: Snippet;
|
|
8
15
|
}
|
|
@@ -1,33 +1,96 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The control that picks the palette.
|
|
4
|
+
*
|
|
5
|
+
* Three states, not two. It offered `dark` and `light` and had no way to say
|
|
6
|
+
* "whatever the operating system is on", which is the state a reader who
|
|
7
|
+
* already answered that question at the OS level expects to find, and the
|
|
8
|
+
* reason one console shipped its own copy of this file rather than adopt it.
|
|
9
|
+
*
|
|
10
|
+
* One button rather than three radios: it sits in the header bar of every
|
|
11
|
+
* authed screen in three applications, next to the account menu, and a
|
|
12
|
+
* segmented control there costs the width of the section name beside it. The
|
|
13
|
+
* cost of the cycle is that the button's name has to carry the current state
|
|
14
|
+
* as well as the action, because with three states the action no longer
|
|
15
|
+
* implies the state.
|
|
16
|
+
*/
|
|
17
|
+
import { Monitor, Moon, Sun } from '@lucide/svelte';
|
|
18
|
+
import {
|
|
19
|
+
THEME_PREFERENCES,
|
|
20
|
+
getThemePreference,
|
|
21
|
+
nextThemePreference,
|
|
22
|
+
setThemePreference,
|
|
23
|
+
watchSystemTheme,
|
|
24
|
+
type ThemePreference,
|
|
25
|
+
} from '../utils/theme.js';
|
|
4
26
|
|
|
5
27
|
interface Props {
|
|
28
|
+
/**
|
|
29
|
+
* Which preferences to offer, in the order the button cycles through them.
|
|
30
|
+
*
|
|
31
|
+
* A surface that must not offer `system` - a screen printed to a fixed
|
|
32
|
+
* palette, a kiosk - passes `['light', 'dark']` and gets the two-state
|
|
33
|
+
* control back with no other change.
|
|
34
|
+
*/
|
|
35
|
+
order?: readonly ThemePreference[];
|
|
6
36
|
class?: string;
|
|
7
37
|
}
|
|
8
38
|
|
|
9
|
-
let { class: klass = '' }: Props = $props();
|
|
39
|
+
let { order = THEME_PREFERENCES, class: klass = '' }: Props = $props();
|
|
10
40
|
|
|
11
|
-
|
|
41
|
+
/**
|
|
42
|
+
* `system` until the browser says otherwise, which is what an unanswered
|
|
43
|
+
* question resolves to. The server cannot read storage or the OS, so a
|
|
44
|
+
* hardcoded `dark` here would render the wrong icon on every light-OS
|
|
45
|
+
* reader's first paint.
|
|
46
|
+
*/
|
|
47
|
+
let preference = $state<ThemePreference>('system');
|
|
12
48
|
|
|
13
49
|
$effect(() => {
|
|
14
|
-
|
|
50
|
+
preference = getThemePreference();
|
|
15
51
|
});
|
|
16
52
|
|
|
17
|
-
|
|
18
|
-
|
|
53
|
+
/**
|
|
54
|
+
* A reader on `system` whose OS flips at dusk gets the new palette without
|
|
55
|
+
* touching anything. Without this the preference is honoured once, at load,
|
|
56
|
+
* and reads as ignored for the rest of the session.
|
|
57
|
+
*/
|
|
58
|
+
$effect(() =>
|
|
59
|
+
watchSystemTheme(() => {
|
|
60
|
+
if (preference === 'system') setThemePreference('system');
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const upcoming = $derived(nextThemePreference(preference, order));
|
|
65
|
+
|
|
66
|
+
const LABEL: Record<ThemePreference, string> = {
|
|
67
|
+
light: 'light',
|
|
68
|
+
dark: 'dark',
|
|
69
|
+
system: 'system',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function advance() {
|
|
73
|
+
preference = upcoming;
|
|
74
|
+
setThemePreference(preference);
|
|
19
75
|
}
|
|
20
76
|
</script>
|
|
21
77
|
|
|
22
78
|
<button
|
|
23
79
|
type="button"
|
|
24
|
-
onclick={
|
|
25
|
-
|
|
80
|
+
onclick={advance}
|
|
81
|
+
data-print="hide"
|
|
82
|
+
data-theme-preference={preference}
|
|
83
|
+
aria-label="Theme: {LABEL[preference]}. Switch to {LABEL[upcoming]}."
|
|
26
84
|
class="rounded-lg p-2 text-faint transition-colors duration-150 hover:bg-surface-2 hover:text-fg {klass}"
|
|
27
85
|
>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
86
|
+
<!-- The icon shows the state the control is in, not the one it moves to. It
|
|
87
|
+
showed the destination while the label named the same destination, which
|
|
88
|
+
said the action twice and the state not at all. -->
|
|
89
|
+
{#if preference === 'system'}
|
|
90
|
+
<Monitor size={16} aria-hidden="true" />
|
|
91
|
+
{:else if preference === 'dark'}
|
|
31
92
|
<Moon size={16} aria-hidden="true" />
|
|
93
|
+
{:else}
|
|
94
|
+
<Sun size={16} aria-hidden="true" />
|
|
32
95
|
{/if}
|
|
33
96
|
</button>
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
+
import { type ThemePreference } from '../utils/theme.js';
|
|
1
2
|
interface Props {
|
|
3
|
+
/**
|
|
4
|
+
* Which preferences to offer, in the order the button cycles through them.
|
|
5
|
+
*
|
|
6
|
+
* A surface that must not offer `system` - a screen printed to a fixed
|
|
7
|
+
* palette, a kiosk - passes `['light', 'dark']` and gets the two-state
|
|
8
|
+
* control back with no other change.
|
|
9
|
+
*/
|
|
10
|
+
order?: readonly ThemePreference[];
|
|
2
11
|
class?: string;
|
|
3
12
|
}
|
|
4
13
|
declare const ThemeToggle: import("svelte").Component<Props, {}, "">;
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
changes, and a region that arrives already holding its text is not a
|
|
18
18
|
change. -->
|
|
19
19
|
<div
|
|
20
|
-
|
|
20
|
+
data-print="hide"
|
|
21
|
+
class="pointer-events-none fixed bottom-4 right-4 z-toast flex w-80 max-w-[calc(100vw-2rem)] flex-col gap-2"
|
|
21
22
|
role="status"
|
|
22
23
|
aria-live="polite"
|
|
23
24
|
aria-atomic="false"
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
{#each toast.items as t (t.id)}
|
|
26
27
|
<div
|
|
27
28
|
class="pointer-events-auto flex items-start gap-3 overflow-hidden rounded-lg border border-line
|
|
28
|
-
bg-surface pl-0 pr-3 py-3 shadow-xl animate-[toast-
|
|
29
|
+
bg-surface pl-0 pr-3 py-3 shadow-xl animate-[toast-in_var(--duration-toast-in)_ease-out]"
|
|
29
30
|
>
|
|
30
31
|
<span class="w-1 self-stretch shrink-0 {styles[statusTone(t.tone)].bar}"></span>
|
|
31
32
|
<span
|
|
@@ -68,16 +69,3 @@
|
|
|
68
69
|
</div>
|
|
69
70
|
{/each}
|
|
70
71
|
</div>
|
|
71
|
-
|
|
72
|
-
<style>
|
|
73
|
-
@keyframes toast-in {
|
|
74
|
-
from {
|
|
75
|
-
opacity: 0;
|
|
76
|
-
transform: translateX(12px);
|
|
77
|
-
}
|
|
78
|
-
to {
|
|
79
|
-
opacity: 1;
|
|
80
|
-
transform: translateX(0);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
</style>
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
{id}
|
|
73
73
|
role="tooltip"
|
|
74
74
|
hidden={!visible}
|
|
75
|
-
class="absolute {pos[position]} z-
|
|
75
|
+
class="absolute {pos[position]} z-tooltip max-w-[min(16rem,calc(100vw-2rem))]
|
|
76
76
|
rounded-lg border border-line bg-surface-2 px-2.5 py-1.5 text-xs text-fg shadow-xl"
|
|
77
77
|
>
|
|
78
78
|
{text}
|
|
@@ -32,9 +32,8 @@
|
|
|
32
32
|
let visible = $state(false);
|
|
33
33
|
let exiting = $state(false);
|
|
34
34
|
|
|
35
|
-
// Stacked offset: each deeper dialog shrinks
|
|
35
|
+
// Stacked offset: each deeper dialog shrinks, shifts back and rises
|
|
36
36
|
let offset = $derived(entry.depth);
|
|
37
|
-
let zIndex = $derived(50 + offset);
|
|
38
37
|
|
|
39
38
|
// ──────────────────────────────────────────────────────
|
|
40
39
|
// DOM refs
|
|
@@ -93,10 +92,16 @@
|
|
|
93
92
|
<!-- z-index is an inline style, not a `z-[...]` class. Tailwind scans source text
|
|
94
93
|
for complete class names, so a class built from a runtime value matches no
|
|
95
94
|
candidate and no rule is ever generated: every stacked dialog rendered at
|
|
96
|
-
`z-index: auto` and the stacking order came down to DOM order.
|
|
95
|
+
`z-index: auto` and the stacking order came down to DOM order.
|
|
96
|
+
|
|
97
|
+
The base is the modal layer rather than the literal 50 it used to be. 50
|
|
98
|
+
was the number every floating surface in the kit happened to hold, so a
|
|
99
|
+
dialog neither sat above a drawer nor below a tooltip on purpose; it sat
|
|
100
|
+
wherever the document put it. The offset climbs from there, and the layer
|
|
101
|
+
above is 100 clear of this one. -->
|
|
97
102
|
<div
|
|
98
103
|
class="fixed inset-0 flex items-center justify-center"
|
|
99
|
-
style="z-index: {
|
|
104
|
+
style="z-index: calc(var(--z-index-modal) + {offset})"
|
|
100
105
|
role="presentation"
|
|
101
106
|
>
|
|
102
107
|
<!-- Backdrop -->
|
|
@@ -40,13 +40,32 @@ export declare function dismissDialog(id?: string): void;
|
|
|
40
40
|
* Close all open dialogs, rejecting their promises.
|
|
41
41
|
*/
|
|
42
42
|
export declare function dismissAllDialogs(): void;
|
|
43
|
+
/** The labels a caller may override on a confirm dialog. */
|
|
44
|
+
export interface ConfirmOptions {
|
|
45
|
+
/**
|
|
46
|
+
* The verb on the accepting button. Defaults to `Confirm`.
|
|
47
|
+
*
|
|
48
|
+
* Name the action rather than accepting the default wherever the caller has
|
|
49
|
+
* a verb of its own: a button reading `Delete` says what happens, and one
|
|
50
|
+
* reading `Confirm` only says that something does. ConfirmDialog has always
|
|
51
|
+
* read this, but until it was reachable from here a caller had to drop to
|
|
52
|
+
* openDialog and setDialogMeta to reach it, so every call site took the
|
|
53
|
+
* generic label instead.
|
|
54
|
+
*/
|
|
55
|
+
confirmLabel?: string;
|
|
56
|
+
/** The verb on the dismissing button. Defaults to `Cancel`. */
|
|
57
|
+
cancelLabel?: string;
|
|
58
|
+
}
|
|
43
59
|
/**
|
|
44
60
|
* Convenience: confirm dialog.
|
|
45
61
|
* Returns `true` if user confirmed, `false` if cancelled.
|
|
46
62
|
*
|
|
47
63
|
* const ok = await confirm('Delete item?', 'This cannot be undone.');
|
|
64
|
+
* const ok = await confirm('Delete item?', 'This cannot be undone.', {
|
|
65
|
+
* confirmLabel: 'Delete',
|
|
66
|
+
* });
|
|
48
67
|
*/
|
|
49
|
-
export declare function confirm(title: string, message?: string): Promise<boolean>;
|
|
68
|
+
export declare function confirm(title: string, message?: string, options?: ConfirmOptions): Promise<boolean>;
|
|
50
69
|
/**
|
|
51
70
|
* Set metadata on a dialog entry after it's opened.
|
|
52
71
|
* Used by convenience wrappers to pass extra data without polluting DialogOptions.
|
|
@@ -136,11 +136,22 @@ export function dismissAllDialogs() {
|
|
|
136
136
|
* Returns `true` if user confirmed, `false` if cancelled.
|
|
137
137
|
*
|
|
138
138
|
* const ok = await confirm('Delete item?', 'This cannot be undone.');
|
|
139
|
+
* const ok = await confirm('Delete item?', 'This cannot be undone.', {
|
|
140
|
+
* confirmLabel: 'Delete',
|
|
141
|
+
* });
|
|
139
142
|
*/
|
|
140
|
-
export function confirm(title, message) {
|
|
143
|
+
export function confirm(title, message, options) {
|
|
141
144
|
const id = `confirm-${++idCounter}`;
|
|
142
145
|
const promise = openDialog({ id, size: 'sm', title });
|
|
143
|
-
setDialogMeta(id, {
|
|
146
|
+
setDialogMeta(id, {
|
|
147
|
+
confirmTitle: title,
|
|
148
|
+
confirmMessage: message ?? '',
|
|
149
|
+
// Undefined keys are dropped rather than written, because ConfirmDialog
|
|
150
|
+
// decides its default by type-checking the meta value: writing undefined
|
|
151
|
+
// would be indistinguishable from an override to the empty case.
|
|
152
|
+
...(options?.confirmLabel !== undefined ? { confirmLabel: options.confirmLabel } : {}),
|
|
153
|
+
...(options?.cancelLabel !== undefined ? { cancelLabel: options.cancelLabel } : {}),
|
|
154
|
+
});
|
|
144
155
|
// Cancelling a dialog dismisses it, and dismissal rejects. Callers write
|
|
145
156
|
// `if (await confirm(...))`, so a rejection on Cancel is an unhandled
|
|
146
157
|
// rejection on the ordinary path rather than an error anyone meant to
|
package/dist/index.d.ts
CHANGED
|
@@ -75,6 +75,7 @@ export { default as Kbd } from './components/Kbd.svelte';
|
|
|
75
75
|
export { default as CopyButton } from './components/CopyButton.svelte';
|
|
76
76
|
export { default as Avatar } from './components/Avatar.svelte';
|
|
77
77
|
export { default as AvatarGroup } from './components/AvatarGroup.svelte';
|
|
78
|
+
export { default as Logo } from './components/Logo.svelte';
|
|
78
79
|
export { default as ThemeToggle } from './components/ThemeToggle.svelte';
|
|
79
80
|
export { default as Toaster } from './components/Toaster.svelte';
|
|
80
81
|
export { toast } from './stores/toast.svelte.js';
|
|
@@ -83,7 +84,8 @@ export { default as DialogContainer } from './components/dialog/DialogContainer.
|
|
|
83
84
|
export { default as Dialog } from './components/dialog/Dialog.svelte';
|
|
84
85
|
export { default as ConfirmDialog } from './components/dialog/ConfirmDialog.svelte';
|
|
85
86
|
export { openDialog, closeDialog, dismissDialog, dismissAllDialogs, confirm, setDialogMeta, getDialogStack, } from './components/dialog/dialog-manager.svelte.js';
|
|
87
|
+
export type { ConfirmOptions } from './components/dialog/dialog-manager.svelte.js';
|
|
86
88
|
export type { DialogOptions, DialogEntry, DialogSize } from './components/dialog/types.js';
|
|
87
89
|
export { cn, type ClassValue } from './utils/cn.js';
|
|
88
|
-
export { getTheme, setTheme,
|
|
89
|
-
export declare const VERSION = "0.
|
|
90
|
+
export { getTheme, getThemePreference, nextThemePreference, resolveTheme, setTheme, setThemePreference, systemTheme, themeBootScript, toggleTheme, watchSystemTheme, THEME_PREFERENCES, type Theme, type ThemePreference, } from './utils/theme.js';
|
|
91
|
+
export declare const VERSION = "0.18.0";
|
package/dist/index.js
CHANGED
|
@@ -76,6 +76,10 @@ export { default as CopyButton } from './components/CopyButton.svelte';
|
|
|
76
76
|
// ── Media ──────────────────────────────────────────────────────────────────
|
|
77
77
|
export { default as Avatar } from './components/Avatar.svelte';
|
|
78
78
|
export { default as AvatarGroup } from './components/AvatarGroup.svelte';
|
|
79
|
+
// The product mark. Two applications carried their own copy of the same lockup
|
|
80
|
+
// and the two had drifted apart in size, in weight and in how they answered the
|
|
81
|
+
// theme; the kit owns it now.
|
|
82
|
+
export { default as Logo } from './components/Logo.svelte';
|
|
79
83
|
// ── Theming & toasts ───────────────────────────────────────────────────────
|
|
80
84
|
export { default as ThemeToggle } from './components/ThemeToggle.svelte';
|
|
81
85
|
export { default as Toaster } from './components/Toaster.svelte';
|
|
@@ -88,8 +92,8 @@ export { default as ConfirmDialog } from './components/dialog/ConfirmDialog.svel
|
|
|
88
92
|
export { openDialog, closeDialog, dismissDialog, dismissAllDialogs, confirm, setDialogMeta, getDialogStack, } from './components/dialog/dialog-manager.svelte.js';
|
|
89
93
|
// ── Utilities ──────────────────────────────────────────────────────────────
|
|
90
94
|
export { cn } from './utils/cn.js';
|
|
91
|
-
export { getTheme, setTheme,
|
|
95
|
+
export { getTheme, getThemePreference, nextThemePreference, resolveTheme, setTheme, setThemePreference, systemTheme, themeBootScript, toggleTheme, watchSystemTheme, THEME_PREFERENCES, } from './utils/theme.js';
|
|
92
96
|
// ── Version ────────────────────────────────────────────────────────────────
|
|
93
97
|
// Generated from package.json by `pnpm version:sync`. Bump package.json, never
|
|
94
98
|
// this line; the build and the test suite fail when the two disagree.
|
|
95
|
-
export const VERSION = '0.
|
|
99
|
+
export const VERSION = '0.18.0';
|
package/dist/internal/layout.js
CHANGED
|
@@ -137,10 +137,19 @@ export function sectionHeading(level, variant = 'default') {
|
|
|
137
137
|
// it rather than a title, so it does not take the level's size at all.
|
|
138
138
|
if (variant === 'eyebrow')
|
|
139
139
|
return 'text-xs font-medium uppercase tracking-wide text-faint';
|
|
140
|
+
// Level 2 is the brand ramp's H3, which the guideline describes as the card
|
|
141
|
+
// and group head this is. Its leading and tracking come with it: a --text-*
|
|
142
|
+
// token sets font-size and nothing else, so the ratio has to be asked for by
|
|
143
|
+
// name or the heading inherits whatever line-height its parent had.
|
|
144
|
+
//
|
|
140
145
|
// Level 3 drops a size rather than a weight. Inside a card it sits under the
|
|
141
146
|
// card's own semibold title, and two semibold lines at the same size read as
|
|
142
|
-
// one heading broken in half.
|
|
143
|
-
|
|
147
|
+
// one heading broken in half. It stays at text-sm: the ramp holds nothing
|
|
148
|
+
// between 16px and 22px, and 16px is exactly the size of the card title it
|
|
149
|
+
// has to sit below.
|
|
150
|
+
return level === 2
|
|
151
|
+
? 'text-h3 leading-h3 tracking-h3 font-semibold text-fg'
|
|
152
|
+
: 'text-sm font-semibold text-fg';
|
|
144
153
|
}
|
|
145
154
|
/**
|
|
146
155
|
* The authed app frame, stated once for every app that has one.
|
package/dist/internal/panel.d.ts
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
* needs a boundary that clears 3:1, which line does not. It carries the
|
|
41
41
|
* resting text colour so a row can override it with a single utility.
|
|
42
42
|
*/
|
|
43
|
-
export declare const PANEL_SURFACE = "absolute z-
|
|
43
|
+
export declare const PANEL_SURFACE = "absolute z-dropdown mt-1 rounded-xl border border-line-strong bg-surface text-fg shadow-2xl";
|
|
44
44
|
/**
|
|
45
45
|
* The scrolling region inside it.
|
|
46
46
|
*
|
package/dist/internal/panel.js
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
* needs a boundary that clears 3:1, which line does not. It carries the
|
|
41
41
|
* resting text colour so a row can override it with a single utility.
|
|
42
42
|
*/
|
|
43
|
-
export const PANEL_SURFACE = 'absolute z-
|
|
43
|
+
export const PANEL_SURFACE = 'absolute z-dropdown mt-1 rounded-xl border border-line-strong bg-surface text-fg shadow-2xl';
|
|
44
44
|
/**
|
|
45
45
|
* The scrolling region inside it.
|
|
46
46
|
*
|