@happyvertical/smrt-ui 0.52.0 → 0.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/AGENTS.md +11 -1
  2. package/README.md +300 -2
  3. package/dist/components/data/DataTable.svelte +60 -15
  4. package/dist/components/data/DataTable.svelte.d.ts.map +1 -1
  5. package/dist/components/data/DataTableResponsive.d.ts +4 -0
  6. package/dist/components/data/DataTableResponsive.d.ts.map +1 -0
  7. package/dist/components/data/DataTableResponsive.js +22 -0
  8. package/dist/components/data/__tests__/DataTableResponsive.test.js +80 -0
  9. package/dist/components/data/__tests__/responsive-table.fixture.svelte +29 -0
  10. package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts +7 -0
  11. package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts.map +1 -0
  12. package/dist/components/data/types.d.ts +8 -0
  13. package/dist/components/data/types.d.ts.map +1 -1
  14. package/dist/components/display/DateDisplay.svelte +76 -14
  15. package/dist/components/display/DateDisplay.svelte.d.ts +2 -0
  16. package/dist/components/display/DateDisplay.svelte.d.ts.map +1 -1
  17. package/dist/components/display/Icon.svelte +3 -16
  18. package/dist/components/display/Icon.svelte.d.ts +0 -9
  19. package/dist/components/display/Icon.svelte.d.ts.map +1 -1
  20. package/dist/components/display/StatusBadge.svelte +29 -2
  21. package/dist/components/display/StatusBadge.svelte.d.ts +4 -2
  22. package/dist/components/display/StatusBadge.svelte.d.ts.map +1 -1
  23. package/dist/components/display/__tests__/DateDisplay.test.js +82 -1
  24. package/dist/components/display/__tests__/Icon.test.js +132 -2
  25. package/dist/components/display/__tests__/StatusBadge.test.js +78 -0
  26. package/dist/components/display/icons.svelte.d.ts +11 -0
  27. package/dist/components/display/icons.svelte.d.ts.map +1 -0
  28. package/dist/components/display/icons.svelte.js +60 -0
  29. package/dist/components/display/index.d.ts +3 -1
  30. package/dist/components/display/index.d.ts.map +1 -1
  31. package/dist/components/display/index.js +1 -0
  32. package/dist/components/display/types.d.ts +2 -0
  33. package/dist/components/display/types.d.ts.map +1 -1
  34. package/dist/components/feedback/ConfirmDialog.svelte +59 -48
  35. package/dist/components/feedback/ConfirmDialog.svelte.d.ts +14 -2
  36. package/dist/components/feedback/ConfirmDialog.svelte.d.ts.map +1 -1
  37. package/dist/components/feedback/ToastViewport.svelte +156 -8
  38. package/dist/components/feedback/ToastViewport.svelte.d.ts +5 -1
  39. package/dist/components/feedback/ToastViewport.svelte.d.ts.map +1 -1
  40. package/dist/components/feedback/__tests__/ConfirmDialog.test.js +38 -20
  41. package/dist/components/feedback/__tests__/confirm-message.fixture.svelte +9 -0
  42. package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts +19 -0
  43. package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts.map +1 -0
  44. package/dist/components/feedback/__tests__/toast.test.js +142 -1
  45. package/dist/components/feedback/index.d.ts +1 -1
  46. package/dist/components/feedback/index.d.ts.map +1 -1
  47. package/dist/components/feedback/toast.d.ts +13 -1
  48. package/dist/components/feedback/toast.d.ts.map +1 -1
  49. package/dist/components/feedback/toast.js +51 -10
  50. package/dist/components/forms/CameraCapture.svelte +619 -0
  51. package/dist/components/forms/CameraCapture.svelte.d.ts +42 -0
  52. package/dist/components/forms/CameraCapture.svelte.d.ts.map +1 -0
  53. package/dist/components/forms/Checkbox.svelte +8 -1
  54. package/dist/components/forms/Checkbox.svelte.d.ts +2 -0
  55. package/dist/components/forms/Checkbox.svelte.d.ts.map +1 -1
  56. package/dist/components/forms/Combobox.svelte +40 -0
  57. package/dist/components/forms/Combobox.svelte.d.ts.map +1 -1
  58. package/dist/components/forms/Form.svelte +5 -0
  59. package/dist/components/forms/Form.svelte.d.ts +1 -0
  60. package/dist/components/forms/Form.svelte.d.ts.map +1 -1
  61. package/dist/components/forms/Input.svelte +8 -1
  62. package/dist/components/forms/Input.svelte.d.ts +2 -0
  63. package/dist/components/forms/Input.svelte.d.ts.map +1 -1
  64. package/dist/components/forms/Listbox.svelte +39 -1
  65. package/dist/components/forms/Listbox.svelte.d.ts.map +1 -1
  66. package/dist/components/forms/MultiSelect.svelte +41 -1
  67. package/dist/components/forms/MultiSelect.svelte.d.ts.map +1 -1
  68. package/dist/components/forms/Radio.svelte +1 -0
  69. package/dist/components/forms/RadioGroup.svelte +7 -1
  70. package/dist/components/forms/RadioGroup.svelte.d.ts +2 -0
  71. package/dist/components/forms/RadioGroup.svelte.d.ts.map +1 -1
  72. package/dist/components/forms/SegmentedControl.svelte +152 -11
  73. package/dist/components/forms/SegmentedControl.svelte.d.ts +4 -0
  74. package/dist/components/forms/SegmentedControl.svelte.d.ts.map +1 -1
  75. package/dist/components/forms/Select.svelte +8 -1
  76. package/dist/components/forms/Select.svelte.d.ts +2 -0
  77. package/dist/components/forms/Select.svelte.d.ts.map +1 -1
  78. package/dist/components/forms/SignaturePad.svelte +436 -0
  79. package/dist/components/forms/SignaturePad.svelte.d.ts +32 -0
  80. package/dist/components/forms/SignaturePad.svelte.d.ts.map +1 -0
  81. package/dist/components/forms/Switch.svelte +8 -1
  82. package/dist/components/forms/Switch.svelte.d.ts +2 -0
  83. package/dist/components/forms/Switch.svelte.d.ts.map +1 -1
  84. package/dist/components/forms/Textarea.svelte +8 -1
  85. package/dist/components/forms/Textarea.svelte.d.ts +2 -0
  86. package/dist/components/forms/Textarea.svelte.d.ts.map +1 -1
  87. package/dist/components/forms/__tests__/CameraCapture.test.js +504 -0
  88. package/dist/components/forms/__tests__/Combobox.test.js +279 -12
  89. package/dist/components/forms/__tests__/Form.test.js +46 -0
  90. package/dist/components/forms/__tests__/Listbox.test.js +303 -0
  91. package/dist/components/forms/__tests__/MultiSelect.test.js +281 -0
  92. package/dist/components/forms/__tests__/SignaturePad.test.js +338 -0
  93. package/dist/components/forms/__tests__/camera-capture-session.test.js +207 -0
  94. package/dist/components/forms/__tests__/capture-form-retry.test.js +166 -0
  95. package/dist/components/forms/__tests__/capture-form.fixture.svelte +38 -0
  96. package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts +10 -0
  97. package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts.map +1 -0
  98. package/dist/components/forms/__tests__/capture-test-env.d.ts +70 -0
  99. package/dist/components/forms/__tests__/capture-test-env.d.ts.map +1 -0
  100. package/dist/components/forms/__tests__/capture-test-env.js +122 -0
  101. package/dist/components/forms/__tests__/capture-touch.fixture.svelte +15 -0
  102. package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts +8 -0
  103. package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts.map +1 -0
  104. package/dist/components/forms/__tests__/combobox-form.fixture.svelte +29 -0
  105. package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts +14 -0
  106. package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts.map +1 -0
  107. package/dist/components/forms/__tests__/form-attachment.fixture.svelte +28 -0
  108. package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts +11 -0
  109. package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts.map +1 -0
  110. package/dist/components/forms/__tests__/form-retry-form.fixture.svelte +31 -0
  111. package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts +13 -0
  112. package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts.map +1 -0
  113. package/dist/components/forms/__tests__/form-retry-form.test.js +107 -0
  114. package/dist/components/forms/__tests__/hidden-controls-form-retry.test.js +69 -0
  115. package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte +29 -0
  116. package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts +9 -0
  117. package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts.map +1 -0
  118. package/dist/components/forms/__tests__/listbox-form.fixture.svelte +27 -0
  119. package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts +13 -0
  120. package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts.map +1 -0
  121. package/dist/components/forms/__tests__/multiselect-form.fixture.svelte +27 -0
  122. package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts +13 -0
  123. package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts.map +1 -0
  124. package/dist/components/forms/__tests__/native-file-field.test.js +177 -0
  125. package/dist/components/forms/__tests__/segmented-posting.fixture.svelte +28 -0
  126. package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts +12 -0
  127. package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts.map +1 -0
  128. package/dist/components/forms/__tests__/segmented-posting.test.js +92 -0
  129. package/dist/components/forms/__tests__/signature-pad-logic.test.js +54 -0
  130. package/dist/components/forms/__tests__/touch-density.fixture.svelte +34 -0
  131. package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts +8 -0
  132. package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts.map +1 -0
  133. package/dist/components/forms/__tests__/touch-density.test.js +66 -0
  134. package/dist/components/forms/camera-capture-session.d.ts +81 -0
  135. package/dist/components/forms/camera-capture-session.d.ts.map +1 -0
  136. package/dist/components/forms/camera-capture-session.js +130 -0
  137. package/dist/components/forms/form-retry/__tests__/boundaries.test.js +23 -0
  138. package/dist/components/forms/form-retry/__tests__/content-digest.test.js +70 -0
  139. package/dist/components/forms/form-retry/__tests__/controller.test.js +802 -0
  140. package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts +82 -0
  141. package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts.map +1 -0
  142. package/dist/components/forms/form-retry/__tests__/fake-kit.js +180 -0
  143. package/dist/components/forms/form-retry/__tests__/form-draft.test.js +169 -0
  144. package/dist/components/forms/form-retry/__tests__/ssr.test.js +37 -0
  145. package/dist/components/forms/form-retry/__tests__/storage.d.ts +26 -0
  146. package/dist/components/forms/form-retry/__tests__/storage.d.ts.map +1 -0
  147. package/dist/components/forms/form-retry/__tests__/storage.js +63 -0
  148. package/dist/components/forms/form-retry/__tests__/submission-key.test.js +236 -0
  149. package/dist/components/forms/form-retry/__tests__/submitted-draft.test.js +334 -0
  150. package/dist/components/forms/form-retry/content-digest.d.ts +54 -0
  151. package/dist/components/forms/form-retry/content-digest.d.ts.map +1 -0
  152. package/dist/components/forms/form-retry/content-digest.js +169 -0
  153. package/dist/components/forms/form-retry/controller.d.ts +214 -0
  154. package/dist/components/forms/form-retry/controller.d.ts.map +1 -0
  155. package/dist/components/forms/form-retry/controller.js +404 -0
  156. package/dist/components/forms/form-retry/form-draft.d.ts +68 -0
  157. package/dist/components/forms/form-retry/form-draft.d.ts.map +1 -0
  158. package/dist/components/forms/form-retry/form-draft.js +161 -0
  159. package/dist/components/forms/form-retry/index.d.ts +19 -0
  160. package/dist/components/forms/form-retry/index.d.ts.map +1 -0
  161. package/dist/components/forms/form-retry/index.js +16 -0
  162. package/dist/components/forms/form-retry/submission-key.d.ts +244 -0
  163. package/dist/components/forms/form-retry/submission-key.d.ts.map +1 -0
  164. package/dist/components/forms/form-retry/submission-key.js +342 -0
  165. package/dist/components/forms/form-retry/submitted-draft.d.ts +112 -0
  166. package/dist/components/forms/form-retry/submitted-draft.d.ts.map +1 -0
  167. package/dist/components/forms/form-retry/submitted-draft.js +270 -0
  168. package/dist/components/forms/form-retry/types.d.ts +84 -0
  169. package/dist/components/forms/form-retry/types.d.ts.map +1 -0
  170. package/dist/components/forms/form-retry/types.js +13 -0
  171. package/dist/components/forms/index.d.ts +7 -1
  172. package/dist/components/forms/index.d.ts.map +1 -1
  173. package/dist/components/forms/index.js +8 -0
  174. package/dist/components/forms/native-file-field.d.ts +94 -0
  175. package/dist/components/forms/native-file-field.d.ts.map +1 -0
  176. package/dist/components/forms/native-file-field.js +147 -0
  177. package/dist/components/forms/signature-pad-logic.d.ts +50 -0
  178. package/dist/components/forms/signature-pad-logic.d.ts.map +1 -0
  179. package/dist/components/forms/signature-pad-logic.js +48 -0
  180. package/dist/components/forms/types.d.ts +41 -0
  181. package/dist/components/forms/types.d.ts.map +1 -1
  182. package/dist/components/nav/FilterChips.svelte +8 -1
  183. package/dist/components/nav/FilterChips.svelte.d.ts +2 -0
  184. package/dist/components/nav/FilterChips.svelte.d.ts.map +1 -1
  185. package/dist/components/ui/Button.svelte +9 -2
  186. package/dist/components/ui/Button.svelte.d.ts +2 -0
  187. package/dist/components/ui/Button.svelte.d.ts.map +1 -1
  188. package/dist/i18n/strings.ui.d.ts +24 -0
  189. package/dist/i18n/strings.ui.d.ts.map +1 -1
  190. package/dist/i18n/strings.ui.js +26 -0
  191. package/dist/styles/tokens.css +2 -0
  192. package/dist/svelte/__tests__/playground.test.js +2 -1
  193. package/dist/svelte/playground/CapturePreview.svelte +56 -0
  194. package/dist/svelte/playground/CapturePreview.svelte.d.ts +4 -0
  195. package/dist/svelte/playground/CapturePreview.svelte.d.ts.map +1 -0
  196. package/dist/svelte/playground.d.ts.map +1 -1
  197. package/dist/svelte/playground.js +10 -0
  198. package/dist/themes/ThemeProvider.svelte +9 -0
  199. package/dist/themes/ThemeProvider.svelte.d.ts +2 -0
  200. package/dist/themes/ThemeProvider.svelte.d.ts.map +1 -1
  201. package/dist/themes/css-generator.d.ts.map +1 -1
  202. package/dist/themes/css-generator.js +3 -0
  203. package/dist/themes/styles/glass.css +4 -0
  204. package/dist/themes/styles/happyvertical.css +4 -0
  205. package/dist/themes/styles/material.css +4 -0
  206. package/dist/themes/styles/smrt.css +4 -0
  207. package/dist/themes/styles/studio.css +4 -0
  208. 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?: 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end';
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(() => toaster.subscribe((next) => (toasts = next)));
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(--smrt-spacing-4))); gap: var(--smrt-spacing-2); pointer-events: none; }
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(--smrt-spacing-4); left: var(--smrt-spacing-4); }
47
- .viewport--top-end { top: var(--smrt-spacing-4); right: var(--smrt-spacing-4); }
48
- .viewport--bottom-start { bottom: var(--smrt-spacing-4); left: var(--smrt-spacing-4); }
49
- .viewport--bottom-end { right: var(--smrt-spacing-4); bottom: var(--smrt-spacing-4); }
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,EAAE,WAAW,GAAG,SAAS,GAAG,cAAc,GAAG,YAAY,CAAC;IACnE,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAmCD,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,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
- * Golden test for ConfirmDialog (Sweep S11, #1416).
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('cancels via Escape even when focus is outside the dialog', async () => {
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
- // Move focus out of the dialog, then press Escape: the document-level
113
- // handler still fires because the dialog is open.
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
- export declare function createToaster(): Toaster;
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,IAAI,OAAO,CA0DvC;AAED,eAAO,MAAM,OAAO,SAAkB,CAAC"}
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
- export function createToaster() {
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 ?? 5000,
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
- timers.set(id, setTimeout(() => dismiss(id), toast.duration));
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
- clearTimeout(timer);
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();