@lyeve-labs/ui-kit 0.22.1 → 0.23.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/Button.svelte +54 -22
- package/dist/components/Button.svelte.d.ts +12 -0
- package/dist/components/Tooltip.svelte +14 -2
- package/dist/components/Tooltip.svelte.d.ts +10 -0
- package/dist/components/dialog/ConfirmDialog.svelte +9 -2
- package/dist/components/dialog/Dialog.svelte +28 -23
- package/dist/components/dialog/dialog-manager.svelte.d.ts +6 -0
- package/dist/components/dialog/dialog-manager.svelte.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/overlay.js +7 -3
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
|
4
4
|
import Spinner from './Spinner.svelte';
|
|
5
|
+
import Tooltip from './Tooltip.svelte';
|
|
5
6
|
import { safeHref } from '../internal/href.js';
|
|
6
7
|
|
|
7
8
|
type Variant = 'primary' | 'secondary' | 'danger' | 'ghost' | 'outline' | 'violet';
|
|
@@ -25,6 +26,18 @@
|
|
|
25
26
|
type?: 'button' | 'submit' | 'reset';
|
|
26
27
|
href?: string;
|
|
27
28
|
full?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The words shown on hover and focus. Defaults to the button's own
|
|
31
|
+
* aria-label, so an icon-only button names itself to a sighted reader the
|
|
32
|
+
* way it already does to a screen reader; a string replaces those words,
|
|
33
|
+
* and `false` keeps the button silent.
|
|
34
|
+
*
|
|
35
|
+
* Thirty-five icon-only actions across one console had a name and no
|
|
36
|
+
* hint, so a pointer user learned what the trash can did by pressing it.
|
|
37
|
+
* The hint lives here rather than on each caller because the rule that
|
|
38
|
+
* makes a row action icon-only is the kit's, and so is the fix.
|
|
39
|
+
*/
|
|
40
|
+
hint?: string | false;
|
|
28
41
|
class?: string;
|
|
29
42
|
onclick?: (e: MouseEvent) => void;
|
|
30
43
|
children: Snippet;
|
|
@@ -38,12 +51,18 @@
|
|
|
38
51
|
type = 'button',
|
|
39
52
|
href = undefined,
|
|
40
53
|
full = false,
|
|
54
|
+
hint = undefined,
|
|
41
55
|
class: klass = '',
|
|
42
56
|
onclick,
|
|
43
57
|
children,
|
|
44
58
|
...rest
|
|
45
59
|
}: Props = $props();
|
|
46
60
|
|
|
61
|
+
/** The hint text, or nothing: an explicit string, else the aria-label, unless switched off. */
|
|
62
|
+
const hintText = $derived(
|
|
63
|
+
hint === false ? undefined : (hint ?? (rest['aria-label'] || undefined)),
|
|
64
|
+
);
|
|
65
|
+
|
|
47
66
|
/**
|
|
48
67
|
* Colour per variant, in three states: at rest, under a pointer, and held.
|
|
49
68
|
*
|
|
@@ -104,27 +123,40 @@
|
|
|
104
123
|
const inert = $derived(disabled || loading);
|
|
105
124
|
</script>
|
|
106
125
|
|
|
107
|
-
{#
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
126
|
+
{#snippet control()}
|
|
127
|
+
{#if href}
|
|
128
|
+
<a
|
|
129
|
+
href={inert ? undefined : resolvedHref}
|
|
130
|
+
class="{cls} {inert ? 'pointer-events-none opacity-50' : ''}"
|
|
131
|
+
aria-disabled={inert ? 'true' : undefined}
|
|
132
|
+
tabindex={inert ? -1 : undefined}
|
|
133
|
+
{...rest}
|
|
134
|
+
>
|
|
135
|
+
{#if loading}<Spinner size={spinnerSize} />{/if}
|
|
136
|
+
{@render children()}
|
|
137
|
+
</a>
|
|
138
|
+
{:else}
|
|
139
|
+
<button
|
|
140
|
+
{type}
|
|
141
|
+
disabled={disabled || loading}
|
|
142
|
+
aria-busy={loading ? 'true' : undefined}
|
|
143
|
+
{onclick}
|
|
144
|
+
class={cls}
|
|
145
|
+
{...rest}
|
|
146
|
+
>
|
|
147
|
+
{#if loading}<Spinner size={spinnerSize} />{/if}
|
|
148
|
+
{@render children()}
|
|
149
|
+
</button>
|
|
150
|
+
{/if}
|
|
151
|
+
{/snippet}
|
|
152
|
+
|
|
153
|
+
<!-- The hint repeats the name, so it does not describe the control a second
|
|
154
|
+
time; the aria-label already announces it. The wrapper takes the full
|
|
155
|
+
width along with the button, or a full button would shrink to its text. -->
|
|
156
|
+
{#if hintText}
|
|
157
|
+
<Tooltip text={hintText} describe={false} class={full ? 'w-full' : ''}>
|
|
158
|
+
{@render control()}
|
|
159
|
+
</Tooltip>
|
|
118
160
|
{:else}
|
|
119
|
-
|
|
120
|
-
{type}
|
|
121
|
-
disabled={disabled || loading}
|
|
122
|
-
aria-busy={loading ? 'true' : undefined}
|
|
123
|
-
{onclick}
|
|
124
|
-
class={cls}
|
|
125
|
-
{...rest}
|
|
126
|
-
>
|
|
127
|
-
{#if loading}<Spinner size={spinnerSize} />{/if}
|
|
128
|
-
{@render children()}
|
|
129
|
-
</button>
|
|
161
|
+
{@render control()}
|
|
130
162
|
{/if}
|
|
@@ -16,6 +16,18 @@ interface Props extends Forwarded {
|
|
|
16
16
|
type?: 'button' | 'submit' | 'reset';
|
|
17
17
|
href?: string;
|
|
18
18
|
full?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The words shown on hover and focus. Defaults to the button's own
|
|
21
|
+
* aria-label, so an icon-only button names itself to a sighted reader the
|
|
22
|
+
* way it already does to a screen reader; a string replaces those words,
|
|
23
|
+
* and `false` keeps the button silent.
|
|
24
|
+
*
|
|
25
|
+
* Thirty-five icon-only actions across one console had a name and no
|
|
26
|
+
* hint, so a pointer user learned what the trash can did by pressing it.
|
|
27
|
+
* The hint lives here rather than on each caller because the rule that
|
|
28
|
+
* makes a row action icon-only is the kit's, and so is the fix.
|
|
29
|
+
*/
|
|
30
|
+
hint?: string | false;
|
|
19
31
|
class?: string;
|
|
20
32
|
onclick?: (e: MouseEvent) => void;
|
|
21
33
|
children: Snippet;
|
|
@@ -6,11 +6,21 @@
|
|
|
6
6
|
interface Props {
|
|
7
7
|
text: string;
|
|
8
8
|
position?: Position;
|
|
9
|
+
/**
|
|
10
|
+
* Whether the text is the trigger's accessible description.
|
|
11
|
+
*
|
|
12
|
+
* On by default, because a tooltip that nothing points at is read by
|
|
13
|
+
* nobody. Off for a trigger that already carries the same words as its
|
|
14
|
+
* name: an icon-only button named by aria-label would otherwise be
|
|
15
|
+
* announced as "Delete, Delete", so the hint stays visual there and the
|
|
16
|
+
* name does the announcing.
|
|
17
|
+
*/
|
|
18
|
+
describe?: boolean;
|
|
9
19
|
class?: string;
|
|
10
20
|
children: Snippet;
|
|
11
21
|
}
|
|
12
22
|
|
|
13
|
-
let { text, position = 'top', class: cls = '', children }: Props = $props();
|
|
23
|
+
let { text, position = 'top', describe = true, class: cls = '', children }: Props = $props();
|
|
14
24
|
|
|
15
25
|
let visible = $state(false);
|
|
16
26
|
let wrapper = $state<HTMLElement>();
|
|
@@ -32,6 +42,7 @@
|
|
|
32
42
|
* text was visible to a sighted mouse user and to nobody else.
|
|
33
43
|
*/
|
|
34
44
|
$effect(() => {
|
|
45
|
+
if (!describe) return;
|
|
35
46
|
const trigger = wrapper?.querySelector<HTMLElement>(
|
|
36
47
|
'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
|
37
48
|
);
|
|
@@ -70,7 +81,8 @@
|
|
|
70
81
|
moving onto it does not dismiss it, which SC 1.4.13 also requires. -->
|
|
71
82
|
<span
|
|
72
83
|
{id}
|
|
73
|
-
role=
|
|
84
|
+
role={describe ? 'tooltip' : undefined}
|
|
85
|
+
aria-hidden={describe ? undefined : 'true'}
|
|
74
86
|
hidden={!visible}
|
|
75
87
|
class="absolute {pos[position]} z-tooltip max-w-[min(16rem,calc(100vw-2rem))]
|
|
76
88
|
rounded-lg border border-line bg-surface-2 px-2.5 py-1.5 text-xs text-fg shadow-xl"
|
|
@@ -3,6 +3,16 @@ type Position = 'top' | 'bottom' | 'left' | 'right';
|
|
|
3
3
|
interface Props {
|
|
4
4
|
text: string;
|
|
5
5
|
position?: Position;
|
|
6
|
+
/**
|
|
7
|
+
* Whether the text is the trigger's accessible description.
|
|
8
|
+
*
|
|
9
|
+
* On by default, because a tooltip that nothing points at is read by
|
|
10
|
+
* nobody. Off for a trigger that already carries the same words as its
|
|
11
|
+
* name: an icon-only button named by aria-label would otherwise be
|
|
12
|
+
* announced as "Delete, Delete", so the hint stays visual there and the
|
|
13
|
+
* name does the announcing.
|
|
14
|
+
*/
|
|
15
|
+
describe?: boolean;
|
|
6
16
|
class?: string;
|
|
7
17
|
children: Snippet;
|
|
8
18
|
}
|
|
@@ -42,6 +42,9 @@
|
|
|
42
42
|
let confirmLabel = $derived(
|
|
43
43
|
typeof entry.meta?.confirmLabel === 'string' ? entry.meta.confirmLabel : 'Confirm',
|
|
44
44
|
);
|
|
45
|
+
// An identifier the message refers to. Set apart in monospace on its own
|
|
46
|
+
// line rather than inside the sentence, where a UUID wraps mid-string.
|
|
47
|
+
let detail = $derived(typeof entry.meta?.confirmDetail === 'string' ? entry.meta.confirmDetail : '');
|
|
45
48
|
|
|
46
49
|
async function handleConfirm() {
|
|
47
50
|
if (loading) return;
|
|
@@ -80,12 +83,16 @@
|
|
|
80
83
|
{#if message}
|
|
81
84
|
<p class="text-sm text-muted mt-1">{message}</p>
|
|
82
85
|
{/if}
|
|
86
|
+
{#if detail}
|
|
87
|
+
<p class="mt-2 break-all font-mono text-xs text-faint">{detail}</p>
|
|
88
|
+
{/if}
|
|
83
89
|
</div>
|
|
84
90
|
</div>
|
|
85
91
|
|
|
86
|
-
<!-- Actions
|
|
92
|
+
<!-- Actions. Cancel takes focus first: a destructive dialog must not
|
|
93
|
+
accept an Enter pressed before it was read. -->
|
|
87
94
|
<div class="flex items-center justify-end gap-3 pt-2">
|
|
88
|
-
<Button variant="secondary" size="sm" onclick={handleCancel}>
|
|
95
|
+
<Button variant="secondary" size="sm" onclick={handleCancel} data-initial-focus>
|
|
89
96
|
{cancelLabel}
|
|
90
97
|
</Button>
|
|
91
98
|
<Button variant="danger" size="sm" onclick={handleConfirm} {loading}>
|
|
@@ -137,34 +137,39 @@
|
|
|
137
137
|
tabindex="-1"
|
|
138
138
|
onkeydown={handleKeydown}
|
|
139
139
|
>
|
|
140
|
-
<!-- Header
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
140
|
+
<!-- Header. A confirm dialog has none: it renders its own title beside
|
|
141
|
+
the warning icon and offers Cancel, so a header here was an empty row
|
|
142
|
+
with a close control in it, and that control was the first thing
|
|
143
|
+
focused. Escape still dismisses. -->
|
|
144
|
+
{#if entry.meta?.confirmTitle === undefined}
|
|
145
|
+
<div class="flex items-center justify-between px-6 pt-6 pb-2">
|
|
146
|
+
<div class="flex-1 min-w-0">
|
|
147
|
+
{#if entry.options.title}
|
|
148
|
+
{#if typeof entry.options.title === 'string'}
|
|
149
|
+
<h2 class="text-lg font-semibold text-fg truncate">
|
|
150
|
+
{entry.options.title}
|
|
151
|
+
</h2>
|
|
152
|
+
{:else}
|
|
153
|
+
{@render entry.options.title()}
|
|
154
|
+
{/if}
|
|
150
155
|
{/if}
|
|
151
|
-
|
|
152
|
-
</div>
|
|
156
|
+
</div>
|
|
153
157
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
158
|
+
{#if !entry.options.persistent}
|
|
159
|
+
<button
|
|
160
|
+
class="inline-flex items-center justify-center w-8 h-8 -me-2 rounded-lg
|
|
157
161
|
text-muted hover:text-fg hover:bg-surface-2 transition-colors duration-150 shrink-0"
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
onclick={() => handleClose()}
|
|
163
|
+
aria-label="Close"
|
|
164
|
+
>
|
|
165
|
+
<X class="w-4 h-4" />
|
|
166
|
+
</button>
|
|
167
|
+
{/if}
|
|
168
|
+
</div>
|
|
169
|
+
{/if}
|
|
165
170
|
|
|
166
171
|
<!-- Body -->
|
|
167
|
-
<div class=
|
|
172
|
+
<div class={entry.meta?.confirmTitle === undefined ? 'px-6 py-2' : 'px-6 pt-6 pb-2'}>
|
|
168
173
|
{#if entry.options.body}
|
|
169
174
|
{@render entry.options.body()}
|
|
170
175
|
{:else if entry.meta?.confirmTitle !== undefined}
|
|
@@ -55,6 +55,12 @@ export interface ConfirmOptions {
|
|
|
55
55
|
confirmLabel?: string;
|
|
56
56
|
/** The verb on the dismissing button. Defaults to `Cancel`. */
|
|
57
57
|
cancelLabel?: string;
|
|
58
|
+
/**
|
|
59
|
+
* An identifier the message refers to, such as the id of the record about
|
|
60
|
+
* to be deleted. Rendered on its own line in monospace, so a UUID never
|
|
61
|
+
* wraps inside the sentence.
|
|
62
|
+
*/
|
|
63
|
+
detail?: string;
|
|
58
64
|
}
|
|
59
65
|
/**
|
|
60
66
|
* Convenience: confirm dialog.
|
|
@@ -151,6 +151,7 @@ export function confirm(title, message, options) {
|
|
|
151
151
|
// would be indistinguishable from an override to the empty case.
|
|
152
152
|
...(options?.confirmLabel !== undefined ? { confirmLabel: options.confirmLabel } : {}),
|
|
153
153
|
...(options?.cancelLabel !== undefined ? { cancelLabel: options.cancelLabel } : {}),
|
|
154
|
+
...(options?.detail !== undefined ? { confirmDetail: options.detail } : {}),
|
|
154
155
|
});
|
|
155
156
|
// Cancelling a dialog dismisses it, and dismissal rejects. Callers write
|
|
156
157
|
// `if (await confirm(...))`, so a rejection on Cancel is an unhandled
|
package/dist/index.d.ts
CHANGED
|
@@ -89,4 +89,4 @@ export type { ConfirmOptions } from './components/dialog/dialog-manager.svelte.j
|
|
|
89
89
|
export type { DialogOptions, DialogEntry, DialogSize } from './components/dialog/types.js';
|
|
90
90
|
export { cn, type ClassValue } from './utils/cn.js';
|
|
91
91
|
export { getTheme, getThemePreference, nextThemePreference, resolveTheme, setTheme, setThemePreference, systemTheme, themeBootScript, toggleTheme, watchSystemTheme, THEME_PREFERENCES, type Theme, type ThemePreference, } from './utils/theme.js';
|
|
92
|
-
export declare const VERSION = "0.
|
|
92
|
+
export declare const VERSION = "0.23.0";
|
package/dist/index.js
CHANGED
|
@@ -96,4 +96,4 @@ export { getTheme, getThemePreference, nextThemePreference, resolveTheme, setThe
|
|
|
96
96
|
// ── Version ────────────────────────────────────────────────────────────────
|
|
97
97
|
// Generated from package.json by `pnpm version:sync`. Bump package.json, never
|
|
98
98
|
// this line; the build and the test suite fail when the two disagree.
|
|
99
|
-
export const VERSION = '0.
|
|
99
|
+
export const VERSION = '0.23.0';
|
package/dist/internal/overlay.js
CHANGED
|
@@ -50,9 +50,13 @@ function focusable(node) {
|
|
|
50
50
|
export function overlay(node) {
|
|
51
51
|
const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
52
52
|
lockBodyScroll();
|
|
53
|
-
// A panel
|
|
54
|
-
//
|
|
55
|
-
|
|
53
|
+
// A panel may name where focus lands with data-initial-focus: a confirm
|
|
54
|
+
// dialog puts it on Cancel, so that Enter on a dialog a person has not read
|
|
55
|
+
// yet does nothing, and the close control is never the first thing focused.
|
|
56
|
+
// Otherwise the first focusable element takes it. A panel with nothing
|
|
57
|
+
// focusable still has to receive focus, or the screen reader stays on the
|
|
58
|
+
// element behind the overlay and reads the wrong thing.
|
|
59
|
+
const first = node.querySelector('[data-initial-focus]') ?? focusable(node)[0];
|
|
56
60
|
if (first) {
|
|
57
61
|
first.focus();
|
|
58
62
|
}
|
package/package.json
CHANGED