@lyeve-labs/ui-kit 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Alert.svelte +109 -41
- package/dist/components/Alert.svelte.d.ts +10 -0
- package/dist/components/AppShell.svelte +5 -1
- package/dist/components/Badge.svelte +9 -2
- package/dist/components/Toggle.svelte +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/layout.d.ts +7 -1
- package/dist/internal/layout.js +7 -1
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { HIT_AREA } from '../internal/touch.js';
|
|
4
|
+
import * as motion from '../motion.js';
|
|
4
5
|
import {
|
|
5
6
|
TONE_GLYPH,
|
|
6
7
|
statusTone,
|
|
@@ -8,10 +9,30 @@
|
|
|
8
9
|
type StatusToneInput,
|
|
9
10
|
} from '../internal/tone.js';
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* How long a confirmation holds before it clears itself.
|
|
14
|
+
*
|
|
15
|
+
* A toast waits 4000ms. This waits longer because it carries more: a toast is
|
|
16
|
+
* one line floating over the page, and an alert is a title and a sentence
|
|
17
|
+
* sitting in the reading order, which takes a beat to find and a beat to
|
|
18
|
+
* read.
|
|
19
|
+
*/
|
|
20
|
+
const DISMISS_AFTER = 5000;
|
|
21
|
+
|
|
11
22
|
interface Props {
|
|
12
23
|
tone?: StatusToneInput;
|
|
13
24
|
title?: string;
|
|
14
25
|
dismissible?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Milliseconds before the alert closes itself, or `true` for the default.
|
|
28
|
+
*
|
|
29
|
+
* A confirmation clears itself. A warning and a failure stay until
|
|
30
|
+
* dismissed, so this is off by default and a danger alert never takes it.
|
|
31
|
+
* Pass it wherever the alert reports the outcome of a submit; leave it off
|
|
32
|
+
* where a success tone states a standing condition, which does not stop
|
|
33
|
+
* being true after five seconds.
|
|
34
|
+
*/
|
|
35
|
+
autoDismiss?: number | boolean;
|
|
15
36
|
ondismiss?: () => void;
|
|
16
37
|
class?: string;
|
|
17
38
|
children?: Snippet;
|
|
@@ -21,6 +42,7 @@
|
|
|
21
42
|
tone = 'brand',
|
|
22
43
|
title = undefined,
|
|
23
44
|
dismissible = false,
|
|
45
|
+
autoDismiss = false,
|
|
24
46
|
ondismiss = undefined,
|
|
25
47
|
class: klass = '',
|
|
26
48
|
children,
|
|
@@ -35,54 +57,100 @@
|
|
|
35
57
|
};
|
|
36
58
|
|
|
37
59
|
const t = $derived(statusTone(tone));
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A failure and a warning interrupt, because the reader has to act. Anything
|
|
63
|
+
* else waits for a pause. FormMessage already splits them this way; Alert
|
|
64
|
+
* announced every tone assertively, so a green confirmation cut across
|
|
65
|
+
* whatever was being read. An alert that also clears itself must not be
|
|
66
|
+
* assertive: it would interrupt to say something and then take it away.
|
|
67
|
+
*/
|
|
68
|
+
const live = $derived(t === 'danger' || t === 'warn');
|
|
69
|
+
|
|
70
|
+
const delay = $derived(autoDismiss === true ? DISMISS_AFTER : Number(autoDismiss) || 0);
|
|
71
|
+
|
|
72
|
+
/** An alert that can close itself can also be closed by hand. */
|
|
73
|
+
const closable = $derived(dismissible || delay > 0);
|
|
74
|
+
|
|
75
|
+
let closed = $state(false);
|
|
76
|
+
/** The pointer is over the alert, or focus is inside it. */
|
|
77
|
+
let held = $state(false);
|
|
78
|
+
|
|
79
|
+
function close(): void {
|
|
80
|
+
closed = true;
|
|
81
|
+
ondismiss?.();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The alert owns whether it is on screen, rather than asking the page to
|
|
86
|
+
* un-render it. An outcome alert lives inside `{#if form}` in the page that
|
|
87
|
+
* submitted, and nothing clears a form result short of a navigation, so a
|
|
88
|
+
* callback has nothing to act on.
|
|
89
|
+
*/
|
|
90
|
+
$effect(() => {
|
|
91
|
+
if (closed || held || delay <= 0) return;
|
|
92
|
+
const timer = setTimeout(close, delay);
|
|
93
|
+
return () => clearTimeout(timer);
|
|
94
|
+
});
|
|
38
95
|
</script>
|
|
39
96
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
aria-
|
|
97
|
+
{#if !closed}
|
|
98
|
+
<!-- The timer stops while the pointer is over the alert or focus is inside
|
|
99
|
+
it, and starts again when they leave. Text is never pulled away from
|
|
100
|
+
somebody reading it. -->
|
|
101
|
+
<div
|
|
102
|
+
transition:motion.toast|global
|
|
103
|
+
class="flex items-start gap-3 rounded-lg border px-4 py-3 {tones[t].wrap} {klass}"
|
|
104
|
+
role={live ? 'alert' : 'status'}
|
|
105
|
+
aria-live={live ? 'assertive' : 'polite'}
|
|
106
|
+
onmouseenter={closable ? () => (held = true) : undefined}
|
|
107
|
+
onmouseleave={closable ? () => (held = false) : undefined}
|
|
108
|
+
onfocusin={closable ? () => (held = true) : undefined}
|
|
109
|
+
onfocusout={closable ? () => (held = false) : undefined}
|
|
49
110
|
>
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
stroke="currentColor"
|
|
56
|
-
stroke-width="2.5"
|
|
57
|
-
stroke-linecap="round"
|
|
58
|
-
stroke-linejoin="round"
|
|
59
|
-
>
|
|
60
|
-
<path d={TONE_GLYPH[t]} />
|
|
61
|
-
</svg>
|
|
62
|
-
</span>
|
|
63
|
-
<div class="flex-1 min-w-0">
|
|
64
|
-
{#if title}<p class="text-sm font-semibold text-fg">{title}</p>{/if}
|
|
65
|
-
{#if children}<div class="text-sm text-muted {title ? 'mt-0.5' : ''}">
|
|
66
|
-
{@render children()}
|
|
67
|
-
</div>{/if}
|
|
68
|
-
</div>
|
|
69
|
-
{#if dismissible}
|
|
70
|
-
<button
|
|
71
|
-
type="button"
|
|
72
|
-
onclick={ondismiss}
|
|
73
|
-
class="{HIT_AREA} shrink-0 text-faint transition-colors hover:text-fg"
|
|
74
|
-
aria-label="Dismiss"
|
|
111
|
+
<span
|
|
112
|
+
class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-current {tones[
|
|
113
|
+
t
|
|
114
|
+
].icon}"
|
|
115
|
+
aria-hidden="true"
|
|
75
116
|
>
|
|
76
117
|
<svg
|
|
77
|
-
width="
|
|
78
|
-
height="
|
|
118
|
+
width="11"
|
|
119
|
+
height="11"
|
|
79
120
|
viewBox="0 0 24 24"
|
|
80
121
|
fill="none"
|
|
81
122
|
stroke="currentColor"
|
|
82
|
-
stroke-width="2"
|
|
123
|
+
stroke-width="2.5"
|
|
83
124
|
stroke-linecap="round"
|
|
84
|
-
|
|
125
|
+
stroke-linejoin="round"
|
|
126
|
+
>
|
|
127
|
+
<path d={TONE_GLYPH[t]} />
|
|
128
|
+
</svg>
|
|
129
|
+
</span>
|
|
130
|
+
<div class="flex-1 min-w-0">
|
|
131
|
+
{#if title}<p class="text-sm font-semibold text-fg">{title}</p>{/if}
|
|
132
|
+
{#if children}<div class="text-sm text-muted {title ? 'mt-0.5' : ''}">
|
|
133
|
+
{@render children()}
|
|
134
|
+
</div>{/if}
|
|
135
|
+
</div>
|
|
136
|
+
{#if closable}
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
onclick={close}
|
|
140
|
+
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"
|
|
141
|
+
aria-label="Dismiss"
|
|
85
142
|
>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
143
|
+
<svg
|
|
144
|
+
width="16"
|
|
145
|
+
height="16"
|
|
146
|
+
viewBox="0 0 24 24"
|
|
147
|
+
fill="none"
|
|
148
|
+
stroke="currentColor"
|
|
149
|
+
stroke-width="2"
|
|
150
|
+
stroke-linecap="round"
|
|
151
|
+
aria-hidden="true"><path d="M18 6L6 18M6 6l12 12" /></svg
|
|
152
|
+
>
|
|
153
|
+
</button>
|
|
154
|
+
{/if}
|
|
155
|
+
</div>
|
|
156
|
+
{/if}
|
|
@@ -4,6 +4,16 @@ interface Props {
|
|
|
4
4
|
tone?: StatusToneInput;
|
|
5
5
|
title?: string;
|
|
6
6
|
dismissible?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Milliseconds before the alert closes itself, or `true` for the default.
|
|
9
|
+
*
|
|
10
|
+
* A confirmation clears itself. A warning and a failure stay until
|
|
11
|
+
* dismissed, so this is off by default and a danger alert never takes it.
|
|
12
|
+
* Pass it wherever the alert reports the outcome of a submit; leave it off
|
|
13
|
+
* where a success tone states a standing condition, which does not stop
|
|
14
|
+
* being true after five seconds.
|
|
15
|
+
*/
|
|
16
|
+
autoDismiss?: number | boolean;
|
|
7
17
|
ondismiss?: () => void;
|
|
8
18
|
class?: string;
|
|
9
19
|
children?: Snippet;
|
|
@@ -256,7 +256,11 @@
|
|
|
256
256
|
onfocusin={() => (railOpen = true)}
|
|
257
257
|
onfocusout={onRailFocusOut}
|
|
258
258
|
>
|
|
259
|
-
<div
|
|
259
|
+
<div
|
|
260
|
+
class="absolute inset-y-0 start-0 z-dropdown flex transition-shadow {railOpen
|
|
261
|
+
? 'shadow-2xl'
|
|
262
|
+
: ''}"
|
|
263
|
+
>
|
|
260
264
|
{@render sidebar(isMobile, railIcons)}
|
|
261
265
|
</div>
|
|
262
266
|
</div>
|
|
@@ -42,12 +42,19 @@
|
|
|
42
42
|
Under a Table's default `overflow-wrap: anywhere` a status rendered as
|
|
43
43
|
`dra ft` and a role as `sup er_a dmi n`. The label sits in a truncating
|
|
44
44
|
span so a badge whose caller caps its width ends in an ellipsis instead of
|
|
45
|
-
painting past its own border; with no cap the span never shrinks.
|
|
45
|
+
painting past its own border; with no cap the span never shrinks.
|
|
46
|
+
|
|
47
|
+
The svg rules are what let a caller put an icon in the label. Preflight
|
|
48
|
+
makes every svg a block, and a block inside the label takes a line of its
|
|
49
|
+
own, so an icon and its text stacked and the badge rendered two rows tall
|
|
50
|
+
inside a pill. A flex row here would fix the stacking and lose the
|
|
51
|
+
ellipsis, which is the thing the span exists for, so the icon goes back to
|
|
52
|
+
being inline and sits on the text's optical centre. -->
|
|
46
53
|
<span
|
|
47
54
|
class="inline-flex max-w-full items-center font-medium whitespace-nowrap rounded-full border {tones[
|
|
48
55
|
tone
|
|
49
56
|
]} {sizes[size]} {klass}"
|
|
50
57
|
>
|
|
51
58
|
{#if dot}<span class="w-1.5 h-1.5 shrink-0 rounded-full {dotColor[tone]}"></span>{/if}
|
|
52
|
-
<span class="min-w-0 truncate">{@render children()}</span>
|
|
59
|
+
<span class="min-w-0 truncate [&>svg]:inline [&>svg]:align-middle">{@render children()}</span>
|
|
53
60
|
</span>
|
|
@@ -67,7 +67,11 @@
|
|
|
67
67
|
}
|
|
68
68
|
</script>
|
|
69
69
|
|
|
70
|
+
<!-- The marker is on the label because the label is the whole control: the
|
|
71
|
+
switch and its text are one field, and an overlay sizing itself to a form
|
|
72
|
+
counts what it can see. -->
|
|
70
73
|
<label
|
|
74
|
+
data-field
|
|
71
75
|
class="inline-flex items-start gap-2.5 cursor-pointer select-none {disabled
|
|
72
76
|
? 'opacity-50 cursor-not-allowed'
|
|
73
77
|
: ''} {cls}"
|
package/dist/index.d.ts
CHANGED
|
@@ -93,4 +93,4 @@ export { cn, type ClassValue } from './utils/cn.js';
|
|
|
93
93
|
export { getTheme, getThemePreference, nextThemePreference, resolveTheme, setTheme, setThemePreference, systemTheme, themeBootScript, toggleTheme, watchSystemTheme, THEME_PREFERENCES, type Theme, type ThemePreference, } from './utils/theme.js';
|
|
94
94
|
export * as motion from './motion.js';
|
|
95
95
|
export type { Rung as MotionRung, Curve as MotionCurve } from './motion.js';
|
|
96
|
-
export declare const VERSION = "0.
|
|
96
|
+
export declare const VERSION = "0.27.0";
|
package/dist/index.js
CHANGED
|
@@ -103,4 +103,4 @@ export * as motion from './motion.js';
|
|
|
103
103
|
// ── Version ────────────────────────────────────────────────────────────────
|
|
104
104
|
// Generated from package.json by `pnpm version:sync`. Bump package.json, never
|
|
105
105
|
// this line; the build and the test suite fail when the two disagree.
|
|
106
|
-
export const VERSION = '0.
|
|
106
|
+
export const VERSION = '0.27.0';
|
|
@@ -173,13 +173,19 @@ export declare function sectionHeading(level: 2 | 3, variant?: SectionVariant):
|
|
|
173
173
|
* `border-e` and not `border-r`: the shell is the one place a right-to-left
|
|
174
174
|
* locale flips, and a physical border leaves the rule on the wrong edge.
|
|
175
175
|
*/
|
|
176
|
-
export declare const APP_SIDEBAR = "h-full shrink-0 flex-col border-e border-line bg-surface";
|
|
176
|
+
export declare const APP_SIDEBAR = "h-full shrink-0 flex-col border-e border-line bg-surface transition-[width] duration-base ease-move";
|
|
177
177
|
/**
|
|
178
178
|
* The sidebar's two widths. Expanded is the 224px column every authed screen
|
|
179
179
|
* had. The rail is the 56px icon column the theme had named and nothing used:
|
|
180
180
|
* at 768px the expanded column left 496px for the page, and a flow editor with
|
|
181
181
|
* two docked panes had no canvas at all. The shell picks between them; the
|
|
182
182
|
* width is not part of APP_SIDEBAR so the aside cannot carry both.
|
|
183
|
+
*
|
|
184
|
+
* The travel between them is on APP_SIDEBAR, which is the one class both
|
|
185
|
+
* widths share. It is a disclosure opening sideways, which is the same thing
|
|
186
|
+
* an accordion does downwards, so it takes the accordion's rung and curve:
|
|
187
|
+
* base and move. It was the most frequent state change in the product and the
|
|
188
|
+
* only one that happened in a single frame.
|
|
183
189
|
*/
|
|
184
190
|
export declare const APP_SIDEBAR_WIDE = "w-sidebar";
|
|
185
191
|
export declare const APP_SIDEBAR_RAIL = "w-nav-rail";
|
package/dist/internal/layout.js
CHANGED
|
@@ -207,13 +207,19 @@ export function sectionHeading(level, variant = 'default') {
|
|
|
207
207
|
* `border-e` and not `border-r`: the shell is the one place a right-to-left
|
|
208
208
|
* locale flips, and a physical border leaves the rule on the wrong edge.
|
|
209
209
|
*/
|
|
210
|
-
export const APP_SIDEBAR = 'h-full shrink-0 flex-col border-e border-line bg-surface';
|
|
210
|
+
export const APP_SIDEBAR = 'h-full shrink-0 flex-col border-e border-line bg-surface transition-[width] duration-base ease-move';
|
|
211
211
|
/**
|
|
212
212
|
* The sidebar's two widths. Expanded is the 224px column every authed screen
|
|
213
213
|
* had. The rail is the 56px icon column the theme had named and nothing used:
|
|
214
214
|
* at 768px the expanded column left 496px for the page, and a flow editor with
|
|
215
215
|
* two docked panes had no canvas at all. The shell picks between them; the
|
|
216
216
|
* width is not part of APP_SIDEBAR so the aside cannot carry both.
|
|
217
|
+
*
|
|
218
|
+
* The travel between them is on APP_SIDEBAR, which is the one class both
|
|
219
|
+
* widths share. It is a disclosure opening sideways, which is the same thing
|
|
220
|
+
* an accordion does downwards, so it takes the accordion's rung and curve:
|
|
221
|
+
* base and move. It was the most frequent state change in the product and the
|
|
222
|
+
* only one that happened in a single frame.
|
|
217
223
|
*/
|
|
218
224
|
export const APP_SIDEBAR_WIDE = 'w-sidebar';
|
|
219
225
|
export const APP_SIDEBAR_RAIL = 'w-nav-rail';
|
package/package.json
CHANGED