@happyvertical/smrt-ui 0.52.0 → 0.53.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/AGENTS.md +11 -1
- package/README.md +300 -2
- package/dist/components/data/DataTable.svelte +60 -15
- package/dist/components/data/DataTable.svelte.d.ts.map +1 -1
- package/dist/components/data/DataTableResponsive.d.ts +4 -0
- package/dist/components/data/DataTableResponsive.d.ts.map +1 -0
- package/dist/components/data/DataTableResponsive.js +22 -0
- package/dist/components/data/__tests__/DataTableResponsive.test.js +80 -0
- package/dist/components/data/__tests__/responsive-table.fixture.svelte +29 -0
- package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts +7 -0
- package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts.map +1 -0
- package/dist/components/data/types.d.ts +8 -0
- package/dist/components/data/types.d.ts.map +1 -1
- package/dist/components/display/DateDisplay.svelte +76 -14
- package/dist/components/display/DateDisplay.svelte.d.ts +2 -0
- package/dist/components/display/DateDisplay.svelte.d.ts.map +1 -1
- package/dist/components/display/Icon.svelte +3 -16
- package/dist/components/display/Icon.svelte.d.ts +0 -9
- package/dist/components/display/Icon.svelte.d.ts.map +1 -1
- package/dist/components/display/StatusBadge.svelte +29 -2
- package/dist/components/display/StatusBadge.svelte.d.ts +4 -2
- package/dist/components/display/StatusBadge.svelte.d.ts.map +1 -1
- package/dist/components/display/__tests__/DateDisplay.test.js +82 -1
- package/dist/components/display/__tests__/Icon.test.js +132 -2
- package/dist/components/display/__tests__/StatusBadge.test.js +78 -0
- package/dist/components/display/icons.svelte.d.ts +11 -0
- package/dist/components/display/icons.svelte.d.ts.map +1 -0
- package/dist/components/display/icons.svelte.js +60 -0
- package/dist/components/display/index.d.ts +3 -1
- package/dist/components/display/index.d.ts.map +1 -1
- package/dist/components/display/index.js +1 -0
- package/dist/components/display/types.d.ts +2 -0
- package/dist/components/display/types.d.ts.map +1 -1
- package/dist/components/feedback/ConfirmDialog.svelte +59 -48
- package/dist/components/feedback/ConfirmDialog.svelte.d.ts +14 -2
- package/dist/components/feedback/ConfirmDialog.svelte.d.ts.map +1 -1
- package/dist/components/feedback/ToastViewport.svelte +156 -8
- package/dist/components/feedback/ToastViewport.svelte.d.ts +5 -1
- package/dist/components/feedback/ToastViewport.svelte.d.ts.map +1 -1
- package/dist/components/feedback/__tests__/ConfirmDialog.test.js +38 -20
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte +9 -0
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts +19 -0
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts.map +1 -0
- package/dist/components/feedback/__tests__/toast.test.js +142 -1
- package/dist/components/feedback/index.d.ts +1 -1
- package/dist/components/feedback/index.d.ts.map +1 -1
- package/dist/components/feedback/toast.d.ts +13 -1
- package/dist/components/feedback/toast.d.ts.map +1 -1
- package/dist/components/feedback/toast.js +51 -10
- package/dist/components/forms/CameraCapture.svelte +619 -0
- package/dist/components/forms/CameraCapture.svelte.d.ts +42 -0
- package/dist/components/forms/CameraCapture.svelte.d.ts.map +1 -0
- package/dist/components/forms/Checkbox.svelte +8 -1
- package/dist/components/forms/Checkbox.svelte.d.ts +2 -0
- package/dist/components/forms/Checkbox.svelte.d.ts.map +1 -1
- package/dist/components/forms/Combobox.svelte +40 -0
- package/dist/components/forms/Combobox.svelte.d.ts.map +1 -1
- package/dist/components/forms/Form.svelte +5 -0
- package/dist/components/forms/Form.svelte.d.ts +1 -0
- package/dist/components/forms/Form.svelte.d.ts.map +1 -1
- package/dist/components/forms/Input.svelte +8 -1
- package/dist/components/forms/Input.svelte.d.ts +2 -0
- package/dist/components/forms/Input.svelte.d.ts.map +1 -1
- package/dist/components/forms/Listbox.svelte +39 -1
- package/dist/components/forms/Listbox.svelte.d.ts.map +1 -1
- package/dist/components/forms/MultiSelect.svelte +41 -1
- package/dist/components/forms/MultiSelect.svelte.d.ts.map +1 -1
- package/dist/components/forms/Radio.svelte +1 -0
- package/dist/components/forms/RadioGroup.svelte +7 -1
- package/dist/components/forms/RadioGroup.svelte.d.ts +2 -0
- package/dist/components/forms/RadioGroup.svelte.d.ts.map +1 -1
- package/dist/components/forms/SegmentedControl.svelte +152 -11
- package/dist/components/forms/SegmentedControl.svelte.d.ts +4 -0
- package/dist/components/forms/SegmentedControl.svelte.d.ts.map +1 -1
- package/dist/components/forms/Select.svelte +8 -1
- package/dist/components/forms/Select.svelte.d.ts +2 -0
- package/dist/components/forms/Select.svelte.d.ts.map +1 -1
- package/dist/components/forms/SignaturePad.svelte +436 -0
- package/dist/components/forms/SignaturePad.svelte.d.ts +32 -0
- package/dist/components/forms/SignaturePad.svelte.d.ts.map +1 -0
- package/dist/components/forms/Switch.svelte +8 -1
- package/dist/components/forms/Switch.svelte.d.ts +2 -0
- package/dist/components/forms/Switch.svelte.d.ts.map +1 -1
- package/dist/components/forms/Textarea.svelte +8 -1
- package/dist/components/forms/Textarea.svelte.d.ts +2 -0
- package/dist/components/forms/Textarea.svelte.d.ts.map +1 -1
- package/dist/components/forms/__tests__/CameraCapture.test.js +504 -0
- package/dist/components/forms/__tests__/Combobox.test.js +279 -12
- package/dist/components/forms/__tests__/Form.test.js +46 -0
- package/dist/components/forms/__tests__/Listbox.test.js +303 -0
- package/dist/components/forms/__tests__/MultiSelect.test.js +281 -0
- package/dist/components/forms/__tests__/SignaturePad.test.js +338 -0
- package/dist/components/forms/__tests__/camera-capture-session.test.js +207 -0
- package/dist/components/forms/__tests__/capture-form-retry.test.js +166 -0
- package/dist/components/forms/__tests__/capture-form.fixture.svelte +38 -0
- package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts +10 -0
- package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/capture-test-env.d.ts +70 -0
- package/dist/components/forms/__tests__/capture-test-env.d.ts.map +1 -0
- package/dist/components/forms/__tests__/capture-test-env.js +122 -0
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte +15 -0
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte +29 -0
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts +14 -0
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte +28 -0
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts +11 -0
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte +31 -0
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts +13 -0
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-retry-form.test.js +107 -0
- package/dist/components/forms/__tests__/hidden-controls-form-retry.test.js +69 -0
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte +29 -0
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts +9 -0
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte +27 -0
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts +13 -0
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte +27 -0
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts +13 -0
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/native-file-field.test.js +177 -0
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte +28 -0
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts +12 -0
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/segmented-posting.test.js +92 -0
- package/dist/components/forms/__tests__/signature-pad-logic.test.js +54 -0
- package/dist/components/forms/__tests__/touch-density.fixture.svelte +34 -0
- package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/touch-density.test.js +66 -0
- package/dist/components/forms/camera-capture-session.d.ts +81 -0
- package/dist/components/forms/camera-capture-session.d.ts.map +1 -0
- package/dist/components/forms/camera-capture-session.js +130 -0
- package/dist/components/forms/form-retry/__tests__/boundaries.test.js +23 -0
- package/dist/components/forms/form-retry/__tests__/content-digest.test.js +70 -0
- package/dist/components/forms/form-retry/__tests__/controller.test.js +802 -0
- package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts +82 -0
- package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts.map +1 -0
- package/dist/components/forms/form-retry/__tests__/fake-kit.js +180 -0
- package/dist/components/forms/form-retry/__tests__/form-draft.test.js +169 -0
- package/dist/components/forms/form-retry/__tests__/ssr.test.js +37 -0
- package/dist/components/forms/form-retry/__tests__/storage.d.ts +26 -0
- package/dist/components/forms/form-retry/__tests__/storage.d.ts.map +1 -0
- package/dist/components/forms/form-retry/__tests__/storage.js +63 -0
- package/dist/components/forms/form-retry/__tests__/submission-key.test.js +236 -0
- package/dist/components/forms/form-retry/__tests__/submitted-draft.test.js +334 -0
- package/dist/components/forms/form-retry/content-digest.d.ts +54 -0
- package/dist/components/forms/form-retry/content-digest.d.ts.map +1 -0
- package/dist/components/forms/form-retry/content-digest.js +169 -0
- package/dist/components/forms/form-retry/controller.d.ts +214 -0
- package/dist/components/forms/form-retry/controller.d.ts.map +1 -0
- package/dist/components/forms/form-retry/controller.js +404 -0
- package/dist/components/forms/form-retry/form-draft.d.ts +68 -0
- package/dist/components/forms/form-retry/form-draft.d.ts.map +1 -0
- package/dist/components/forms/form-retry/form-draft.js +161 -0
- package/dist/components/forms/form-retry/index.d.ts +19 -0
- package/dist/components/forms/form-retry/index.d.ts.map +1 -0
- package/dist/components/forms/form-retry/index.js +16 -0
- package/dist/components/forms/form-retry/submission-key.d.ts +244 -0
- package/dist/components/forms/form-retry/submission-key.d.ts.map +1 -0
- package/dist/components/forms/form-retry/submission-key.js +342 -0
- package/dist/components/forms/form-retry/submitted-draft.d.ts +112 -0
- package/dist/components/forms/form-retry/submitted-draft.d.ts.map +1 -0
- package/dist/components/forms/form-retry/submitted-draft.js +270 -0
- package/dist/components/forms/form-retry/types.d.ts +84 -0
- package/dist/components/forms/form-retry/types.d.ts.map +1 -0
- package/dist/components/forms/form-retry/types.js +13 -0
- package/dist/components/forms/index.d.ts +7 -1
- package/dist/components/forms/index.d.ts.map +1 -1
- package/dist/components/forms/index.js +8 -0
- package/dist/components/forms/native-file-field.d.ts +94 -0
- package/dist/components/forms/native-file-field.d.ts.map +1 -0
- package/dist/components/forms/native-file-field.js +147 -0
- package/dist/components/forms/signature-pad-logic.d.ts +50 -0
- package/dist/components/forms/signature-pad-logic.d.ts.map +1 -0
- package/dist/components/forms/signature-pad-logic.js +48 -0
- package/dist/components/forms/types.d.ts +41 -0
- package/dist/components/forms/types.d.ts.map +1 -1
- package/dist/components/nav/FilterChips.svelte +8 -1
- package/dist/components/nav/FilterChips.svelte.d.ts +2 -0
- package/dist/components/nav/FilterChips.svelte.d.ts.map +1 -1
- package/dist/components/ui/Button.svelte +9 -2
- package/dist/components/ui/Button.svelte.d.ts +2 -0
- package/dist/components/ui/Button.svelte.d.ts.map +1 -1
- package/dist/i18n/strings.ui.d.ts +24 -0
- package/dist/i18n/strings.ui.d.ts.map +1 -1
- package/dist/i18n/strings.ui.js +26 -0
- package/dist/styles/tokens.css +2 -0
- package/dist/svelte/__tests__/playground.test.js +2 -1
- package/dist/svelte/playground/CapturePreview.svelte +56 -0
- package/dist/svelte/playground/CapturePreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/CapturePreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground.d.ts.map +1 -1
- package/dist/svelte/playground.js +10 -0
- package/dist/themes/ThemeProvider.svelte +9 -0
- package/dist/themes/ThemeProvider.svelte.d.ts +2 -0
- package/dist/themes/ThemeProvider.svelte.d.ts.map +1 -1
- package/dist/themes/css-generator.d.ts.map +1 -1
- package/dist/themes/css-generator.js +3 -0
- package/dist/themes/styles/glass.css +4 -0
- package/dist/themes/styles/happyvertical.css +4 -0
- package/dist/themes/styles/material.css +4 -0
- package/dist/themes/styles/smrt.css +4 -0
- package/dist/themes/styles/studio.css +4 -0
- package/package.json +8 -2
|
@@ -10,7 +10,17 @@ export interface Props {
|
|
|
10
10
|
/** Toaster instance that manages the toast queue. */
|
|
11
11
|
toaster?: Toaster;
|
|
12
12
|
/** Screen corner where toasts appear. */
|
|
13
|
-
position?:
|
|
13
|
+
position?:
|
|
14
|
+
| 'top-start'
|
|
15
|
+
| 'top-center'
|
|
16
|
+
| 'top-end'
|
|
17
|
+
| 'bottom-start'
|
|
18
|
+
| 'bottom-center'
|
|
19
|
+
| 'bottom-end';
|
|
20
|
+
/** CSS length inset from the window or anchor region. */
|
|
21
|
+
inset?: string;
|
|
22
|
+
/** Optional content region used for toast placement. */
|
|
23
|
+
anchor?: HTMLElement | null;
|
|
14
24
|
/** CSS class to apply to the viewport container. */
|
|
15
25
|
class?: string;
|
|
16
26
|
}
|
|
@@ -18,10 +28,131 @@ let {
|
|
|
18
28
|
toaster = defaultToaster,
|
|
19
29
|
position = 'bottom-end',
|
|
20
30
|
class: className = '',
|
|
31
|
+
inset = 'var(--smrt-spacing-4, 1rem)',
|
|
32
|
+
anchor = null,
|
|
21
33
|
}: Props = $props();
|
|
34
|
+
const instanceId = $props.id();
|
|
35
|
+
const hoverReason = `${instanceId}-hover`;
|
|
36
|
+
const focusReason = `${instanceId}-focus`;
|
|
22
37
|
let toasts = $state<Toast[]>([]);
|
|
38
|
+
let bounds = $state<{
|
|
39
|
+
top: number;
|
|
40
|
+
right: number;
|
|
41
|
+
bottom: number;
|
|
42
|
+
left: number;
|
|
43
|
+
width: number;
|
|
44
|
+
} | null>(null);
|
|
23
45
|
|
|
24
|
-
$effect(() =>
|
|
46
|
+
$effect(() => {
|
|
47
|
+
if (!anchor) {
|
|
48
|
+
bounds = null;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const region = anchor;
|
|
52
|
+
function measure() {
|
|
53
|
+
const rect = region.getBoundingClientRect();
|
|
54
|
+
bounds = {
|
|
55
|
+
top: rect.top,
|
|
56
|
+
right: window.innerWidth - rect.right,
|
|
57
|
+
bottom: window.innerHeight - rect.bottom,
|
|
58
|
+
left: rect.left,
|
|
59
|
+
width: rect.width,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
measure();
|
|
63
|
+
const observer = new ResizeObserver(measure);
|
|
64
|
+
observer.observe(region);
|
|
65
|
+
window.addEventListener('resize', measure);
|
|
66
|
+
document.addEventListener('scroll', measure, true);
|
|
67
|
+
return () => {
|
|
68
|
+
observer.disconnect();
|
|
69
|
+
window.removeEventListener('resize', measure);
|
|
70
|
+
document.removeEventListener('scroll', measure, true);
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// A popover outside the topmost modal remains inert. Keep the live region and
|
|
75
|
+
// its controls inside the active dialog instead, preserving their Svelte owner.
|
|
76
|
+
function followModal(node: HTMLElement) {
|
|
77
|
+
const marker = document.createComment('toast viewport host');
|
|
78
|
+
node.before(marker);
|
|
79
|
+
let stack: HTMLDialogElement[] = [];
|
|
80
|
+
function update(records: MutationRecord[] = []) {
|
|
81
|
+
const dialogs = Array.from(
|
|
82
|
+
document.querySelectorAll<HTMLDialogElement>('dialog[open]'),
|
|
83
|
+
).filter((dialog) => dialog.matches(':modal'));
|
|
84
|
+
stack = stack.filter((dialog) => dialogs.includes(dialog));
|
|
85
|
+
for (const dialog of dialogs)
|
|
86
|
+
if (!stack.includes(dialog)) stack.push(dialog);
|
|
87
|
+
for (const record of records) {
|
|
88
|
+
if (record.type !== 'attributes' || record.attributeName !== 'open')
|
|
89
|
+
continue;
|
|
90
|
+
const dialog = record.target as HTMLDialogElement;
|
|
91
|
+
if (!dialogs.includes(dialog)) continue;
|
|
92
|
+
stack = [...stack.filter((item) => item !== dialog), dialog];
|
|
93
|
+
}
|
|
94
|
+
const focused = dialogs
|
|
95
|
+
.filter((dialog) => dialog.contains(document.activeElement))
|
|
96
|
+
.at(-1);
|
|
97
|
+
if (focused)
|
|
98
|
+
stack = [...stack.filter((dialog) => dialog !== focused), focused];
|
|
99
|
+
const host = stack.at(-1) ?? marker.parentElement;
|
|
100
|
+
if (!host || node.parentElement === host) return;
|
|
101
|
+
const active = node.contains(document.activeElement)
|
|
102
|
+
? (document.activeElement as HTMLElement)
|
|
103
|
+
: null;
|
|
104
|
+
if (host === marker.parentElement) marker.after(node);
|
|
105
|
+
else host.append(node);
|
|
106
|
+
active?.focus();
|
|
107
|
+
if (!node.contains(document.activeElement)) {
|
|
108
|
+
for (const toast of toasts) toaster.resume?.(toast.id, focusReason);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
update();
|
|
112
|
+
const observer = new MutationObserver(update);
|
|
113
|
+
observer.observe(document.documentElement, {
|
|
114
|
+
subtree: true,
|
|
115
|
+
childList: true,
|
|
116
|
+
attributes: true,
|
|
117
|
+
attributeFilter: ['open'],
|
|
118
|
+
});
|
|
119
|
+
const focus = () => update();
|
|
120
|
+
document.addEventListener('focusin', focus);
|
|
121
|
+
return {
|
|
122
|
+
destroy() {
|
|
123
|
+
observer.disconnect();
|
|
124
|
+
document.removeEventListener('focusin', focus);
|
|
125
|
+
marker.remove();
|
|
126
|
+
node.remove();
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function handleFocusOut(event: FocusEvent, id: string) {
|
|
132
|
+
if (
|
|
133
|
+
event.currentTarget instanceof HTMLElement &&
|
|
134
|
+
event.relatedTarget instanceof Node &&
|
|
135
|
+
event.currentTarget.contains(event.relatedTarget)
|
|
136
|
+
)
|
|
137
|
+
return;
|
|
138
|
+
toaster.resume?.(id, focusReason);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
$effect(() => {
|
|
142
|
+
const queue = toaster;
|
|
143
|
+
let ids: string[] = [];
|
|
144
|
+
const unsubscribe = queue.subscribe((next) => {
|
|
145
|
+
ids = next.map((toast) => toast.id);
|
|
146
|
+
toasts = next;
|
|
147
|
+
});
|
|
148
|
+
return () => {
|
|
149
|
+
unsubscribe();
|
|
150
|
+
for (const id of ids) {
|
|
151
|
+
queue.resume?.(id, hoverReason);
|
|
152
|
+
queue.resume?.(id, focusReason);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
});
|
|
25
156
|
|
|
26
157
|
async function runAction(toast: Toast) {
|
|
27
158
|
await toast.action?.run();
|
|
@@ -29,23 +160,40 @@ async function runAction(toast: Toast) {
|
|
|
29
160
|
}
|
|
30
161
|
</script>
|
|
31
162
|
|
|
32
|
-
<section class="viewport viewport--{position} {className}" aria-label="Notifications" aria-live="polite"
|
|
163
|
+
<section use:followModal class="viewport viewport--{position} {className}" aria-label="Notifications" aria-live="polite"
|
|
164
|
+
style:--toast-inset={inset}
|
|
165
|
+
style:--toast-top={bounds ? `${bounds.top}px` : undefined}
|
|
166
|
+
style:--toast-right={bounds ? `${bounds.right}px` : undefined}
|
|
167
|
+
style:--toast-bottom={bounds ? `${bounds.bottom}px` : undefined}
|
|
168
|
+
style:--toast-left={bounds ? `${bounds.left}px` : undefined}
|
|
169
|
+
style:--toast-region-width={bounds ? `${bounds.width}px` : undefined}
|
|
170
|
+
>
|
|
33
171
|
{#each toasts as toast (toast.id)}
|
|
172
|
+
<div role="group" aria-label={toast.title ?? 'Notification'}
|
|
173
|
+
onmouseenter={() => toaster.pause?.(toast.id, hoverReason)}
|
|
174
|
+
onmouseleave={() => toaster.resume?.(toast.id, hoverReason)}
|
|
175
|
+
onfocusin={() => toaster.pause?.(toast.id, focusReason)}
|
|
176
|
+
onfocusout={(event) => handleFocusOut(event, toast.id)}
|
|
177
|
+
>
|
|
34
178
|
<Alert variant={toast.variant} title={toast.title} dismissible ondismiss={() => toaster.dismiss(toast.id)}>
|
|
35
179
|
{toast.message}
|
|
36
180
|
{#snippet action()}
|
|
37
181
|
{#if toast.action}<button type="button" onclick={() => runAction(toast)}>{toast.action.label}</button>{/if}
|
|
38
182
|
{/snippet}
|
|
39
183
|
</Alert>
|
|
184
|
+
</div>
|
|
40
185
|
{/each}
|
|
41
186
|
</section>
|
|
42
187
|
|
|
43
188
|
<style>
|
|
44
|
-
.viewport { position: fixed; z-index: var(--smrt-z-index-toast, 1200); display: grid; width: min(24rem, calc(100vw - 2 * var(--
|
|
189
|
+
.viewport { position: fixed; z-index: var(--smrt-z-index-toast, 1200); display: grid; width: min(24rem, calc(var(--toast-region-width, 100vw) - 2 * var(--toast-inset))); gap: var(--smrt-spacing-2); pointer-events: none; }
|
|
45
190
|
.viewport :global(.alert) { pointer-events: auto; box-shadow: var(--smrt-elevation-3); }
|
|
46
|
-
.viewport--top-start { top: var(--
|
|
47
|
-
.viewport--top-end { top: var(--
|
|
48
|
-
.viewport--bottom-start { bottom: var(--
|
|
49
|
-
.viewport--bottom-end { right: var(--
|
|
191
|
+
.viewport--top-start { top: calc(var(--toast-top, 0px) + var(--toast-inset)); left: calc(var(--toast-left, 0px) + var(--toast-inset)); }
|
|
192
|
+
.viewport--top-end { top: calc(var(--toast-top, 0px) + var(--toast-inset)); right: calc(var(--toast-right, 0px) + var(--toast-inset)); }
|
|
193
|
+
.viewport--bottom-start { bottom: calc(var(--toast-bottom, 0px) + var(--toast-inset)); left: calc(var(--toast-left, 0px) + var(--toast-inset)); }
|
|
194
|
+
.viewport--bottom-end { right: calc(var(--toast-right, 0px) + var(--toast-inset)); bottom: calc(var(--toast-bottom, 0px) + var(--toast-inset)); }
|
|
195
|
+
.viewport--top-center, .viewport--bottom-center { left: calc(var(--toast-left, 0px) + var(--toast-region-width, 100vw) / 2); transform: translateX(-50%); }
|
|
196
|
+
.viewport--top-center { top: calc(var(--toast-top, 0px) + var(--toast-inset)); }
|
|
197
|
+
.viewport--bottom-center { bottom: calc(var(--toast-bottom, 0px) + var(--toast-inset)); }
|
|
50
198
|
button { padding: var(--smrt-spacing-1) var(--smrt-spacing-2); border: 1px solid currentColor; border-radius: var(--smrt-radius-small); background: transparent; color: inherit; font: inherit; cursor: pointer; }
|
|
51
199
|
</style>
|
|
@@ -3,7 +3,11 @@ export interface Props {
|
|
|
3
3
|
/** Toaster instance that manages the toast queue. */
|
|
4
4
|
toaster?: Toaster;
|
|
5
5
|
/** Screen corner where toasts appear. */
|
|
6
|
-
position?: 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end';
|
|
6
|
+
position?: 'top-start' | 'top-center' | 'top-end' | 'bottom-start' | 'bottom-center' | 'bottom-end';
|
|
7
|
+
/** CSS length inset from the window or anchor region. */
|
|
8
|
+
inset?: string;
|
|
9
|
+
/** Optional content region used for toast placement. */
|
|
10
|
+
anchor?: HTMLElement | null;
|
|
7
11
|
/** CSS class to apply to the viewport container. */
|
|
8
12
|
class?: string;
|
|
9
13
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ToastViewport.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/feedback/ToastViewport.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,OAAO,EACb,MAAM,YAAY,CAAC;AAGpB,MAAM,WAAW,KAAK;IACpB,qDAAqD;IACrD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yCAAyC;IACzC,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"ToastViewport.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/feedback/ToastViewport.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,OAAO,EACb,MAAM,YAAY,CAAC;AAGpB,MAAM,WAAW,KAAK;IACpB,qDAAqD;IACrD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yCAAyC;IACzC,QAAQ,CAAC,EACL,WAAW,GACX,YAAY,GACZ,SAAS,GACT,cAAc,GACd,eAAe,GACf,YAAY,CAAC;IACjB,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC5B,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA8JD,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* ConfirmDialog renders a backdrop `<div role="dialog" aria-modal="true">` only
|
|
5
|
-
* while `open` is true, with a title, message, and cancel/confirm buttons. It
|
|
6
|
-
* exposes `onconfirm`/`oncancel` callbacks; Escape (handled at the document
|
|
7
|
-
* level) and backdrop clicks both route to `oncancel`. On open it manages focus:
|
|
8
|
-
* focus moves to the confirm button, Tab is trapped within the dialog, and focus
|
|
9
|
-
* is restored to the opener on close (#1586).
|
|
10
|
-
*/
|
|
11
|
-
import { render, screen, waitFor } from '@testing-library/svelte';
|
|
1
|
+
/** Native confirmation rendering, actions, focus and instance identity. */
|
|
2
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/svelte';
|
|
12
3
|
import userEvent from '@testing-library/user-event';
|
|
13
4
|
import { describe, expect, it, vi } from 'vitest';
|
|
14
5
|
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
15
6
|
import ConfirmDialog from '../ConfirmDialog.svelte';
|
|
7
|
+
import RichMessage from './confirm-message.fixture.svelte';
|
|
16
8
|
const baseProps = {
|
|
17
9
|
open: true,
|
|
18
10
|
title: 'Delete item',
|
|
@@ -27,6 +19,38 @@ describe('ConfirmDialog', () => {
|
|
|
27
19
|
expect(dialog).toHaveAccessibleName('Delete item');
|
|
28
20
|
expect(screen.getByText('This action cannot be undone.')).toBeInTheDocument();
|
|
29
21
|
});
|
|
22
|
+
it('uses native dialog and unique instance description ids', () => {
|
|
23
|
+
render(ConfirmDialog, { props: baseProps });
|
|
24
|
+
render(ConfirmDialog, {
|
|
25
|
+
props: { ...baseProps, title: 'Second', message: 'Other' },
|
|
26
|
+
});
|
|
27
|
+
const dialogs = screen.getAllByRole('dialog');
|
|
28
|
+
expect(dialogs.every((dialog) => dialog instanceof HTMLDialogElement)).toBe(true);
|
|
29
|
+
for (const attribute of ['aria-labelledby', 'aria-describedby']) {
|
|
30
|
+
const ids = dialogs.map((dialog) => dialog.getAttribute(attribute));
|
|
31
|
+
expect(new Set(ids).size).toBe(2);
|
|
32
|
+
for (let index = 0; index < dialogs.length; index++) {
|
|
33
|
+
expect(dialogs[index].contains(document.getElementById(ids[index]))).toBe(true);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
it('handles native cancel without closing outside controlled state', async () => {
|
|
38
|
+
const oncancel = vi.fn();
|
|
39
|
+
render(ConfirmDialog, { props: { ...baseProps, oncancel } });
|
|
40
|
+
const dialog = screen.getByRole('dialog');
|
|
41
|
+
const event = new Event('cancel', { cancelable: true });
|
|
42
|
+
await fireEvent(dialog, event);
|
|
43
|
+
expect(event.defaultPrevented).toBe(true);
|
|
44
|
+
expect(oncancel).toHaveBeenCalledTimes(1);
|
|
45
|
+
expect(dialog).toHaveAttribute('open');
|
|
46
|
+
});
|
|
47
|
+
it('renders a snippet message as accessible rich content', () => {
|
|
48
|
+
render(RichMessage);
|
|
49
|
+
const dialog = screen.getByRole('dialog', { name: 'Rich confirmation' });
|
|
50
|
+
expect(dialog.querySelector('strong')).toHaveTextContent('Record name');
|
|
51
|
+
expect(screen.getAllByRole('listitem')).toHaveLength(2);
|
|
52
|
+
expect(dialog).toHaveAccessibleDescription('Remove Record name? Notes Attachments');
|
|
53
|
+
});
|
|
30
54
|
it('renders nothing when closed', () => {
|
|
31
55
|
render(ConfirmDialog, { props: { ...baseProps, open: false } });
|
|
32
56
|
expect(screen.queryByRole('dialog')).toBeNull();
|
|
@@ -104,17 +128,11 @@ describe('ConfirmDialog', () => {
|
|
|
104
128
|
expect(opener).toHaveFocus();
|
|
105
129
|
opener.remove();
|
|
106
130
|
});
|
|
107
|
-
it('
|
|
131
|
+
it('does not consume Escape from outside its dialog', async () => {
|
|
108
132
|
const oncancel = vi.fn();
|
|
109
|
-
const outside = document.createElement('input');
|
|
110
|
-
document.body.appendChild(outside);
|
|
111
133
|
render(ConfirmDialog, { props: { ...baseProps, oncancel } });
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
outside.focus();
|
|
115
|
-
await userEvent.keyboard('{Escape}');
|
|
116
|
-
expect(oncancel).toHaveBeenCalledTimes(1);
|
|
117
|
-
outside.remove();
|
|
134
|
+
await fireEvent.keyDown(window, { key: 'Escape' });
|
|
135
|
+
expect(oncancel).not.toHaveBeenCalled();
|
|
118
136
|
});
|
|
119
137
|
it('focuses the dialog container when all controls are disabled (loading)', async () => {
|
|
120
138
|
// Both buttons are disabled while loading, so there is no focusable
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import ConfirmDialog from '../ConfirmDialog.svelte';
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
{#snippet message()}
|
|
6
|
+
Remove <strong>Record name</strong>?
|
|
7
|
+
<ul><li>Notes</li><li>Attachments</li></ul>
|
|
8
|
+
{/snippet}
|
|
9
|
+
<ConfirmDialog open title="Rich confirmation" {message} />
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const ConfirmMessage: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type ConfirmMessage = InstanceType<typeof ConfirmMessage>;
|
|
18
|
+
export default ConfirmMessage;
|
|
19
|
+
//# sourceMappingURL=confirm-message.fixture.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"confirm-message.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/feedback/__tests__/confirm-message.fixture.svelte.ts"],"names":[],"mappings":"AAgBA,UAAU,kCAAkC,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,OAAO,GAAG,EAAE,EAAE,QAAQ,GAAG,MAAM;IACpM,KAAK,OAAO,EAAE,OAAO,QAAQ,EAAE,2BAA2B,CAAC,KAAK,CAAC,GAAG,OAAO,QAAQ,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACjK,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,KAAK,CAAA;KAAC,GAAG,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,GAAG,CAAC;QAAC,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;IACtG,YAAY,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAKD,QAAA,MAAM,cAAc;;kBAA+E,CAAC;AAClF,KAAK,cAAc,GAAG,YAAY,CAAC,OAAO,cAAc,CAAC,CAAC;AAC5D,eAAe,cAAc,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render, screen } from '@testing-library/svelte';
|
|
1
|
+
import { act, fireEvent, render, screen } from '@testing-library/svelte';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import ToastViewport from '../ToastViewport.svelte';
|
|
@@ -47,3 +47,144 @@ describe('toast', () => {
|
|
|
47
47
|
}
|
|
48
48
|
});
|
|
49
49
|
});
|
|
50
|
+
describe('toast dismissal interactions', () => {
|
|
51
|
+
it('preserves the remaining duration until every pause reason clears', () => {
|
|
52
|
+
vi.useFakeTimers();
|
|
53
|
+
try {
|
|
54
|
+
const toaster = createToaster();
|
|
55
|
+
let visible = 0;
|
|
56
|
+
toaster.subscribe((toasts) => {
|
|
57
|
+
visible = toasts.length;
|
|
58
|
+
});
|
|
59
|
+
const id = toaster.show({ message: 'Saved', duration: 1000 });
|
|
60
|
+
vi.advanceTimersByTime(300);
|
|
61
|
+
toaster.pause?.(id, 'hover');
|
|
62
|
+
toaster.pause?.(id, 'focus');
|
|
63
|
+
vi.advanceTimersByTime(5000);
|
|
64
|
+
expect(visible).toBe(1);
|
|
65
|
+
toaster.resume?.(id, 'hover');
|
|
66
|
+
vi.advanceTimersByTime(5000);
|
|
67
|
+
expect(visible).toBe(1);
|
|
68
|
+
toaster.resume?.(id, 'focus');
|
|
69
|
+
toaster.resume?.(id, 'focus');
|
|
70
|
+
vi.advanceTimersByTime(699);
|
|
71
|
+
expect(visible).toBe(1);
|
|
72
|
+
vi.advanceTimersByTime(1);
|
|
73
|
+
expect(visible).toBe(0);
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
vi.useRealTimers();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
it('replacing a paused id resets its duration without losing interaction pause', () => {
|
|
80
|
+
vi.useFakeTimers();
|
|
81
|
+
try {
|
|
82
|
+
const toaster = createToaster();
|
|
83
|
+
let message = '';
|
|
84
|
+
toaster.subscribe((toasts) => {
|
|
85
|
+
message = toasts[0]?.message ?? '';
|
|
86
|
+
});
|
|
87
|
+
toaster.show({ id: 'save', message: 'Saving', duration: 1000 });
|
|
88
|
+
toaster.pause?.('save', 'hover');
|
|
89
|
+
toaster.show({ id: 'save', message: 'Saved', duration: 2000 });
|
|
90
|
+
vi.advanceTimersByTime(5000);
|
|
91
|
+
expect(message).toBe('Saved');
|
|
92
|
+
toaster.resume?.('save', 'hover');
|
|
93
|
+
vi.advanceTimersByTime(1999);
|
|
94
|
+
expect(message).toBe('Saved');
|
|
95
|
+
vi.advanceTimersByTime(1);
|
|
96
|
+
expect(message).toBe('');
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
vi.useRealTimers();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
it('can keep success notifications persistent while other notifications expire', () => {
|
|
103
|
+
vi.useFakeTimers();
|
|
104
|
+
try {
|
|
105
|
+
const toaster = createToaster({ successDuration: 0, duration: 1000 });
|
|
106
|
+
let messages = [];
|
|
107
|
+
toaster.subscribe((toasts) => {
|
|
108
|
+
messages = toasts.map((toast) => toast.message);
|
|
109
|
+
});
|
|
110
|
+
const id = toaster.success('Only record of approval');
|
|
111
|
+
toaster.show('Temporary');
|
|
112
|
+
toaster.success('Explicit duration wins', { duration: 500 });
|
|
113
|
+
vi.advanceTimersByTime(10000);
|
|
114
|
+
expect(messages).toEqual(['Only record of approval']);
|
|
115
|
+
toaster.dismiss(id);
|
|
116
|
+
expect(messages).toEqual([]);
|
|
117
|
+
toaster.show('Cleared');
|
|
118
|
+
toaster.clear();
|
|
119
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
vi.useRealTimers();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
it('viewport hover and focus pause independently, including focus movement within a toast', async () => {
|
|
126
|
+
vi.useFakeTimers();
|
|
127
|
+
try {
|
|
128
|
+
const toaster = createToaster();
|
|
129
|
+
render(ToastViewport, { props: { toaster } });
|
|
130
|
+
await act(() => {
|
|
131
|
+
toaster.show({
|
|
132
|
+
message: 'Stay visible',
|
|
133
|
+
duration: 1000,
|
|
134
|
+
action: { label: 'Undo', run: () => { } },
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
const group = screen.getByRole('group', { name: 'Notification' });
|
|
138
|
+
await fireEvent.mouseEnter(group);
|
|
139
|
+
await fireEvent.focusIn(screen.getByRole('button', { name: 'Undo' }));
|
|
140
|
+
await fireEvent.mouseLeave(group);
|
|
141
|
+
await fireEvent.focusOut(screen.getByRole('button', { name: 'Undo' }), {
|
|
142
|
+
relatedTarget: screen.getByRole('button', { name: 'Dismiss' }),
|
|
143
|
+
});
|
|
144
|
+
await act(() => vi.advanceTimersByTime(5000));
|
|
145
|
+
expect(screen.getByText('Stay visible')).toBeInTheDocument();
|
|
146
|
+
await fireEvent.focusOut(group, { relatedTarget: document.body });
|
|
147
|
+
await act(() => vi.advanceTimersByTime(1000));
|
|
148
|
+
expect(screen.queryByText('Stay visible')).not.toBeInTheDocument();
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
vi.useRealTimers();
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
it('releases its interaction pauses when the viewport unmounts', async () => {
|
|
155
|
+
vi.useFakeTimers();
|
|
156
|
+
try {
|
|
157
|
+
const toaster = createToaster();
|
|
158
|
+
let count = 0;
|
|
159
|
+
toaster.subscribe((toasts) => {
|
|
160
|
+
count = toasts.length;
|
|
161
|
+
});
|
|
162
|
+
const { unmount } = render(ToastViewport, { props: { toaster } });
|
|
163
|
+
await act(() => {
|
|
164
|
+
toaster.show({ message: 'Paused', duration: 1000 });
|
|
165
|
+
});
|
|
166
|
+
await fireEvent.mouseEnter(screen.getByRole('group', { name: 'Notification' }));
|
|
167
|
+
await act(() => vi.advanceTimersByTime(5000));
|
|
168
|
+
expect(count).toBe(1);
|
|
169
|
+
await unmount();
|
|
170
|
+
vi.advanceTimersByTime(1000);
|
|
171
|
+
expect(count).toBe(0);
|
|
172
|
+
expect(document.querySelector('.viewport')).toBeNull();
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
vi.useRealTimers();
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
it('retains compatibility with injected toasters without pause/resume methods', async () => {
|
|
179
|
+
const legacy = { ...createToaster(), pause: undefined, resume: undefined };
|
|
180
|
+
const { unmount } = render(ToastViewport, { props: { toaster: legacy } });
|
|
181
|
+
legacy.success('Legacy', { duration: 0 });
|
|
182
|
+
const group = await screen.findByRole('group', { name: 'Notification' });
|
|
183
|
+
await fireEvent.mouseEnter(group);
|
|
184
|
+
await fireEvent.mouseLeave(group);
|
|
185
|
+
await userEvent.click(screen.getByRole('button', { name: 'Dismiss' }));
|
|
186
|
+
expect(screen.queryByText('Legacy')).not.toBeInTheDocument();
|
|
187
|
+
await unmount();
|
|
188
|
+
expect(document.querySelector('.viewport')).toBeNull();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -13,7 +13,7 @@ export { default as Progress } from './Progress.svelte';
|
|
|
13
13
|
export { default as ProgressBar } from './ProgressBar.svelte';
|
|
14
14
|
export { default as Spinner } from './Spinner.svelte';
|
|
15
15
|
export { default as ToastViewport } from './ToastViewport.svelte';
|
|
16
|
-
export type { Toast, ToastAction, Toaster, ToastInput, ToastVariant, } from './toast.js';
|
|
16
|
+
export type { Toast, ToastAction, Toaster, ToasterOptions, ToastInput, ToastVariant, } from './toast.js';
|
|
17
17
|
export { createToaster, toaster } from './toast.js';
|
|
18
18
|
export { default as WorkingStrip } from './WorkingStrip.svelte';
|
|
19
19
|
export { WORKING_ASSERTIVE_PHASES, type WorkingPhase, type WorkingStatus, } from './working-status.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/feedback/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EACL,uBAAuB,EACvB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,YAAY,EACZ,cAAc,GACf,MAAM,gCAAgC,CAAC;AAIxC,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,YAAY,EACV,KAAK,EACL,WAAW,EACX,OAAO,EACP,UAAU,EACV,YAAY,GACb,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EACL,wBAAwB,EACxB,KAAK,YAAY,EACjB,KAAK,aAAa,GACnB,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/feedback/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EACL,uBAAuB,EACvB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,YAAY,EACZ,cAAc,GACf,MAAM,gCAAgC,CAAC;AAIxC,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,YAAY,EACV,KAAK,EACL,WAAW,EACX,OAAO,EACP,cAAc,EACd,UAAU,EACV,YAAY,GACb,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EACL,wBAAwB,EACxB,KAAK,YAAY,EACjB,KAAK,aAAa,GACnB,MAAM,qBAAqB,CAAC"}
|
|
@@ -19,10 +19,22 @@ export interface Toaster {
|
|
|
19
19
|
show(input: string | ToastInput): string;
|
|
20
20
|
success(message: string, input?: Omit<ToastInput, 'message' | 'variant'>): string;
|
|
21
21
|
error(message: string, input?: Omit<ToastInput, 'message' | 'variant'>): string;
|
|
22
|
+
/** Pause dismissal for a named interaction; legacy injected toasters may omit it. */
|
|
23
|
+
pause?(id: string, reason?: string): void;
|
|
24
|
+
/** Resume dismissal when all named interactions have ended. */
|
|
25
|
+
resume?(id: string, reason?: string): void;
|
|
22
26
|
dismiss(id: string): void;
|
|
23
27
|
clear(): void;
|
|
24
28
|
subscribe(listener: (toasts: Toast[]) => void): () => void;
|
|
25
29
|
}
|
|
26
|
-
|
|
30
|
+
/** Default durations; zero keeps notifications until explicitly dismissed. */
|
|
31
|
+
export interface ToasterOptions {
|
|
32
|
+
/** Default dismissal duration in milliseconds (5000 by default). */
|
|
33
|
+
duration?: number;
|
|
34
|
+
/** Success dismissal duration; zero preserves the only record of an action. */
|
|
35
|
+
successDuration?: number;
|
|
36
|
+
}
|
|
37
|
+
/** Create an isolated notification queue with interaction-aware dismissal timers. */
|
|
38
|
+
export declare function createToaster(options?: ToasterOptions): Toaster;
|
|
27
39
|
export declare const toaster: Toaster;
|
|
28
40
|
//# sourceMappingURL=toast.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toast.d.ts","sourceRoot":"","sources":["../../../src/components/feedback/toast.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAEpE,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,YAAY,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,UAAW,SAAQ,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,GAAG,SAAS,CAAC,CAAC;IACxE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;IACzC,OAAO,CACL,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,GAAG,SAAS,CAAC,GAC9C,MAAM,CAAC;IACV,KAAK,CACH,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,GAAG,SAAS,CAAC,GAC9C,MAAM,CAAC;IACV,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,IAAI,IAAI,CAAC;IACd,SAAS,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CAC5D;AAID,wBAAgB,aAAa,
|
|
1
|
+
{"version":3,"file":"toast.d.ts","sourceRoot":"","sources":["../../../src/components/feedback/toast.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAEpE,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,YAAY,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,UAAW,SAAQ,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,GAAG,SAAS,CAAC,CAAC;IACxE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;IACzC,OAAO,CACL,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,GAAG,SAAS,CAAC,GAC9C,MAAM,CAAC;IACV,KAAK,CACH,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,GAAG,SAAS,CAAC,GAC9C,MAAM,CAAC;IACV,qFAAqF;IACrF,KAAK,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,+DAA+D;IAC/D,MAAM,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,IAAI,IAAI,CAAC;IACd,SAAS,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CAC5D;AAID,8EAA8E;AAC9E,MAAM,WAAW,cAAc;IAC7B,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,qFAAqF;AACrF,wBAAgB,aAAa,CAAC,OAAO,GAAE,cAAmB,GAAG,OAAO,CAwGnE;AAED,eAAO,MAAM,OAAO,SAAkB,CAAC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
let nextToastId = 0;
|
|
2
|
-
|
|
2
|
+
/** Create an isolated notification queue with interaction-aware dismissal timers. */
|
|
3
|
+
export function createToaster(options = {}) {
|
|
3
4
|
let toasts = [];
|
|
4
5
|
const listeners = new Set();
|
|
5
6
|
const timers = new Map();
|
|
@@ -9,8 +10,8 @@ export function createToaster() {
|
|
|
9
10
|
};
|
|
10
11
|
function dismiss(id) {
|
|
11
12
|
const timer = timers.get(id);
|
|
12
|
-
if (timer)
|
|
13
|
-
clearTimeout(timer);
|
|
13
|
+
if (timer?.handle)
|
|
14
|
+
clearTimeout(timer.handle);
|
|
14
15
|
timers.delete(id);
|
|
15
16
|
toasts = toasts.filter((toast) => toast.id !== id);
|
|
16
17
|
publish();
|
|
@@ -19,31 +20,71 @@ export function createToaster() {
|
|
|
19
20
|
const normalized = typeof input === 'string' ? { message: input } : input;
|
|
20
21
|
const id = normalized.id ?? `toast-${++nextToastId}`;
|
|
21
22
|
const existingTimer = timers.get(id);
|
|
22
|
-
if (existingTimer)
|
|
23
|
-
clearTimeout(existingTimer);
|
|
23
|
+
if (existingTimer?.handle)
|
|
24
|
+
clearTimeout(existingTimer.handle);
|
|
24
25
|
timers.delete(id);
|
|
25
26
|
const toast = {
|
|
26
27
|
id,
|
|
27
28
|
message: normalized.message,
|
|
28
29
|
title: normalized.title,
|
|
29
30
|
variant: normalized.variant ?? 'info',
|
|
30
|
-
duration: normalized.duration ??
|
|
31
|
+
duration: normalized.duration ??
|
|
32
|
+
(normalized.variant === 'success'
|
|
33
|
+
? options.successDuration
|
|
34
|
+
: undefined) ??
|
|
35
|
+
options.duration ??
|
|
36
|
+
5000,
|
|
31
37
|
action: normalized.action,
|
|
32
38
|
};
|
|
33
39
|
toasts = [...toasts.filter((item) => item.id !== id), toast];
|
|
34
40
|
publish();
|
|
35
|
-
if (toast.duration > 0)
|
|
36
|
-
|
|
41
|
+
if (toast.duration > 0) {
|
|
42
|
+
const timer = {
|
|
43
|
+
remaining: toast.duration,
|
|
44
|
+
started: Date.now(),
|
|
45
|
+
reasons: existingTimer?.reasons ?? new Set(),
|
|
46
|
+
};
|
|
47
|
+
timers.set(id, timer);
|
|
48
|
+
startTimer(id, timer);
|
|
49
|
+
}
|
|
37
50
|
return id;
|
|
38
51
|
}
|
|
52
|
+
function startTimer(id, timer) {
|
|
53
|
+
if (timer.reasons.size)
|
|
54
|
+
return;
|
|
55
|
+
timer.started = Date.now();
|
|
56
|
+
timer.handle = setTimeout(() => dismiss(id), timer.remaining);
|
|
57
|
+
}
|
|
58
|
+
function pause(id, reason = 'manual') {
|
|
59
|
+
const timer = timers.get(id);
|
|
60
|
+
if (!timer || timer.reasons.has(reason))
|
|
61
|
+
return;
|
|
62
|
+
if (!timer.reasons.size) {
|
|
63
|
+
if (timer.handle)
|
|
64
|
+
clearTimeout(timer.handle);
|
|
65
|
+
timer.handle = undefined;
|
|
66
|
+
timer.remaining = Math.max(0, timer.remaining - (Date.now() - timer.started));
|
|
67
|
+
}
|
|
68
|
+
timer.reasons.add(reason);
|
|
69
|
+
}
|
|
70
|
+
function resume(id, reason = 'manual') {
|
|
71
|
+
const timer = timers.get(id);
|
|
72
|
+
if (!timer || !timer.reasons.delete(reason))
|
|
73
|
+
return;
|
|
74
|
+
startTimer(id, timer);
|
|
75
|
+
}
|
|
39
76
|
return {
|
|
40
77
|
show,
|
|
78
|
+
pause,
|
|
79
|
+
resume,
|
|
41
80
|
success: (message, input) => show({ ...input, message, variant: 'success' }),
|
|
42
81
|
error: (message, input) => show({ ...input, message, variant: 'error' }),
|
|
43
82
|
dismiss,
|
|
44
83
|
clear() {
|
|
45
|
-
for (const timer of timers.values())
|
|
46
|
-
|
|
84
|
+
for (const timer of timers.values()) {
|
|
85
|
+
if (timer.handle)
|
|
86
|
+
clearTimeout(timer.handle);
|
|
87
|
+
}
|
|
47
88
|
timers.clear();
|
|
48
89
|
toasts = [];
|
|
49
90
|
publish();
|