@urbicon-ui/blocks 6.22.0 → 6.24.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.
Files changed (30) hide show
  1. package/dist/components/Calendar/CalendarWeekdayHeader.svelte +4 -1
  2. package/dist/components/Calendar/calendar.variants.d.ts +63 -63
  3. package/dist/components/Sparkline/Sparkline.svelte +10 -4
  4. package/dist/components/Sparkline/index.d.ts +9 -1
  5. package/dist/i18n/index.d.ts +2 -378
  6. package/dist/primitives/Alert/alert.variants.d.ts +8 -8
  7. package/dist/primitives/Badge/Badge.svelte +8 -2
  8. package/dist/primitives/Badge/index.d.ts +67 -32
  9. package/dist/primitives/ButtonGroup/ButtonGroup.svelte +120 -3
  10. package/dist/primitives/Card/card.variants.js +2 -2
  11. package/dist/primitives/Checkbox/Checkbox.svelte +11 -2
  12. package/dist/primitives/Combobox/Combobox.svelte +35 -3
  13. package/dist/primitives/ConfirmDialog/index.d.ts +2 -1
  14. package/dist/primitives/Dialog/index.d.ts +4 -4
  15. package/dist/primitives/JourneyTimeline/journey-timeline.variants.d.ts +22 -22
  16. package/dist/primitives/Pagination/Pagination.svelte +28 -15
  17. package/dist/primitives/RadioGroup/RadioGroup.svelte +16 -3
  18. package/dist/primitives/RadioGroup/RadioItem.svelte +1 -1
  19. package/dist/primitives/Select/Select.svelte +20 -5
  20. package/dist/primitives/Spinner/spinner.variants.d.ts +16 -16
  21. package/dist/primitives/Stepper/stepper.variants.d.ts +11 -11
  22. package/dist/primitives/Textarea/Textarea.svelte +10 -1
  23. package/dist/primitives/Toast/Toaster.svelte +122 -4
  24. package/dist/primitives/Toast/toast.store.svelte.d.ts +32 -1
  25. package/dist/primitives/Toast/toast.store.svelte.js +84 -19
  26. package/dist/primitives/Toast/toast.variants.d.ts +12 -12
  27. package/dist/primitives/Toggle/Toggle.svelte +16 -3
  28. package/dist/primitives/Tooltip/tooltip.variants.d.ts +3 -3
  29. package/dist/style/semantic.css +1 -1
  30. package/package.json +3 -3
@@ -4,7 +4,7 @@ export declare const stepperVariants: ((props?: {
4
4
  orientation?: "horizontal" | "vertical" | undefined;
5
5
  size?: "sm" | "md" | "lg" | undefined;
6
6
  variant?: "default" | "outlined" | "minimal" | undefined;
7
- stepState?: "complete" | "active" | "error" | "warning" | "inactive" | undefined;
7
+ stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
8
8
  clickable?: boolean | undefined;
9
9
  stepDisabled?: boolean | undefined;
10
10
  separatorComplete?: boolean | undefined;
@@ -14,7 +14,7 @@ export declare const stepperVariants: ((props?: {
14
14
  orientation?: "horizontal" | "vertical" | undefined;
15
15
  size?: "sm" | "md" | "lg" | undefined;
16
16
  variant?: "default" | "outlined" | "minimal" | undefined;
17
- stepState?: "complete" | "active" | "error" | "warning" | "inactive" | undefined;
17
+ stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
18
18
  clickable?: boolean | undefined;
19
19
  stepDisabled?: boolean | undefined;
20
20
  separatorComplete?: boolean | undefined;
@@ -26,7 +26,7 @@ export declare const stepperVariants: ((props?: {
26
26
  orientation?: "horizontal" | "vertical" | undefined;
27
27
  size?: "sm" | "md" | "lg" | undefined;
28
28
  variant?: "default" | "outlined" | "minimal" | undefined;
29
- stepState?: "complete" | "active" | "error" | "warning" | "inactive" | undefined;
29
+ stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
30
30
  clickable?: boolean | undefined;
31
31
  stepDisabled?: boolean | undefined;
32
32
  separatorComplete?: boolean | undefined;
@@ -38,7 +38,7 @@ export declare const stepperVariants: ((props?: {
38
38
  orientation?: "horizontal" | "vertical" | undefined;
39
39
  size?: "sm" | "md" | "lg" | undefined;
40
40
  variant?: "default" | "outlined" | "minimal" | undefined;
41
- stepState?: "complete" | "active" | "error" | "warning" | "inactive" | undefined;
41
+ stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
42
42
  clickable?: boolean | undefined;
43
43
  stepDisabled?: boolean | undefined;
44
44
  separatorComplete?: boolean | undefined;
@@ -50,7 +50,7 @@ export declare const stepperVariants: ((props?: {
50
50
  orientation?: "horizontal" | "vertical" | undefined;
51
51
  size?: "sm" | "md" | "lg" | undefined;
52
52
  variant?: "default" | "outlined" | "minimal" | undefined;
53
- stepState?: "complete" | "active" | "error" | "warning" | "inactive" | undefined;
53
+ stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
54
54
  clickable?: boolean | undefined;
55
55
  stepDisabled?: boolean | undefined;
56
56
  separatorComplete?: boolean | undefined;
@@ -62,7 +62,7 @@ export declare const stepperVariants: ((props?: {
62
62
  orientation?: "horizontal" | "vertical" | undefined;
63
63
  size?: "sm" | "md" | "lg" | undefined;
64
64
  variant?: "default" | "outlined" | "minimal" | undefined;
65
- stepState?: "complete" | "active" | "error" | "warning" | "inactive" | undefined;
65
+ stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
66
66
  clickable?: boolean | undefined;
67
67
  stepDisabled?: boolean | undefined;
68
68
  separatorComplete?: boolean | undefined;
@@ -74,7 +74,7 @@ export declare const stepperVariants: ((props?: {
74
74
  orientation?: "horizontal" | "vertical" | undefined;
75
75
  size?: "sm" | "md" | "lg" | undefined;
76
76
  variant?: "default" | "outlined" | "minimal" | undefined;
77
- stepState?: "complete" | "active" | "error" | "warning" | "inactive" | undefined;
77
+ stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
78
78
  clickable?: boolean | undefined;
79
79
  stepDisabled?: boolean | undefined;
80
80
  separatorComplete?: boolean | undefined;
@@ -86,7 +86,7 @@ export declare const stepperVariants: ((props?: {
86
86
  orientation?: "horizontal" | "vertical" | undefined;
87
87
  size?: "sm" | "md" | "lg" | undefined;
88
88
  variant?: "default" | "outlined" | "minimal" | undefined;
89
- stepState?: "complete" | "active" | "error" | "warning" | "inactive" | undefined;
89
+ stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
90
90
  clickable?: boolean | undefined;
91
91
  stepDisabled?: boolean | undefined;
92
92
  separatorComplete?: boolean | undefined;
@@ -98,7 +98,7 @@ export declare const stepperVariants: ((props?: {
98
98
  orientation?: "horizontal" | "vertical" | undefined;
99
99
  size?: "sm" | "md" | "lg" | undefined;
100
100
  variant?: "default" | "outlined" | "minimal" | undefined;
101
- stepState?: "complete" | "active" | "error" | "warning" | "inactive" | undefined;
101
+ stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
102
102
  clickable?: boolean | undefined;
103
103
  stepDisabled?: boolean | undefined;
104
104
  separatorComplete?: boolean | undefined;
@@ -110,7 +110,7 @@ export declare const stepperVariants: ((props?: {
110
110
  orientation?: "horizontal" | "vertical" | undefined;
111
111
  size?: "sm" | "md" | "lg" | undefined;
112
112
  variant?: "default" | "outlined" | "minimal" | undefined;
113
- stepState?: "complete" | "active" | "error" | "warning" | "inactive" | undefined;
113
+ stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
114
114
  clickable?: boolean | undefined;
115
115
  stepDisabled?: boolean | undefined;
116
116
  separatorComplete?: boolean | undefined;
@@ -122,7 +122,7 @@ export declare const stepperVariants: ((props?: {
122
122
  orientation?: "horizontal" | "vertical" | undefined;
123
123
  size?: "sm" | "md" | "lg" | undefined;
124
124
  variant?: "default" | "outlined" | "minimal" | undefined;
125
- stepState?: "complete" | "active" | "error" | "warning" | "inactive" | undefined;
125
+ stepState?: "error" | "warning" | "active" | "inactive" | "complete" | undefined;
126
126
  clickable?: boolean | undefined;
127
127
  stepDisabled?: boolean | undefined;
128
128
  separatorComplete?: boolean | undefined;
@@ -29,6 +29,7 @@
29
29
  slotClasses: slotClassesProp = {},
30
30
  preset,
31
31
  oninput: userOnInput,
32
+ 'aria-describedby': ariaDescribedby,
32
33
  ...restProps
33
34
  }: TextareaProps = $props();
34
35
 
@@ -50,6 +51,14 @@
50
51
  disabled
51
52
  }));
52
53
 
54
+ // Consumer-supplied `aria-describedby` (e.g. an external hint rendered
55
+ // outside the component) merges with the internal error/helper chain instead
56
+ // of replacing it — internal descriptions first, the consumer's supplemental
57
+ // one last (mirrors the Input role model, XC-2).
58
+ const describedBy = $derived(
59
+ [ff.describedBy, ariaDescribedby].filter(Boolean).join(' ') || undefined
60
+ );
61
+
53
62
  const charCount = $derived(typeof value === 'string' ? value.length : 0);
54
63
  const counterState = $derived.by(() => {
55
64
  if (!maxlength) return 'normal' as const;
@@ -145,7 +154,7 @@
145
154
  {readonly}
146
155
  {required}
147
156
  aria-invalid={ff.invalid ? 'true' : undefined}
148
- aria-describedby={ff.describedBy}
157
+ aria-describedby={describedBy}
149
158
  oninput={handleInput}></textarea>
150
159
 
151
160
  {#if showFooter}
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { useBlocksI18n } from '../..';
3
+ import { untrack } from 'svelte';
3
4
  import { fly } from 'svelte/transition';
4
5
  import { getOverlayMotion } from '../../utils';
5
6
  import { toastVariants, type ToastVariants } from './toast.variants';
@@ -56,8 +57,74 @@
56
57
  return toaster.registerSubscriber();
57
58
  });
58
59
 
60
+ // Sonner-style hover-to-pause. The pointer or keyboard focus being anywhere in
61
+ // the toaster region freezes the whole visible stack (all auto-dismiss timers
62
+ // + the progress bars) and leaving resumes each from its remaining time.
63
+ //
64
+ // These are bubbling over/out + focusin/focusout, NOT pointerenter/leave: the
65
+ // container is `pointer-events-none` (so clicks fall through the gaps between
66
+ // toasts to the page), which means it never becomes a pointer target itself —
67
+ // only its `pointer-events-auto` toast children do. enter/leave don't bubble
68
+ // and so would never fire here; over/out bubble up from the toasts. The
69
+ // `relatedTarget` containment check collapses the noisy per-descendant out/blur
70
+ // events into a single "the pointer/focus actually left the region" signal.
71
+ let pointerInside = $state(false);
72
+ let focusInside = $state(false);
73
+
74
+ $effect(() => {
75
+ if (pointerInside || focusInside) toaster.pause();
76
+ else toaster.resume();
77
+ });
78
+
79
+ // Safety net: if the Toaster unmounts while the stack is frozen (pointer/focus
80
+ // was still inside), un-freeze the singleton store so its timers aren't stranded.
81
+ $effect(() => {
82
+ return () => toaster.resume();
83
+ });
84
+
85
+ function leftRegion(event: PointerEvent | FocusEvent) {
86
+ const next = event.relatedTarget as Node | null;
87
+ return !(event.currentTarget as HTMLElement).contains(next);
88
+ }
89
+
59
90
  const visibleToasts = $derived(toaster.toasts.slice(-max));
60
91
 
92
+ // Stranded-pause guard. A toast removed from under the cursor (e.g. its own
93
+ // close button) does not reliably fire `pointerout`, so `pointerInside` can
94
+ // stay stuck `true` and freeze every remaining timer — worst case a fresh
95
+ // toast is then born paused and never counts down. Rather than trust the flag,
96
+ // when the rendered stack SHRINKS we re-derive containment from the real cursor
97
+ // position. The cursor is tracked from the region's own bubbling pointer events
98
+ // (the container is `pointer-events-none`, so these fire only over a toast
99
+ // child, which is exactly when the coords matter).
100
+ let containerEl: HTMLElement | undefined;
101
+ let pointerX = 0;
102
+ let pointerY = 0;
103
+ let prevVisibleCount = 0;
104
+
105
+ function reconcilePointerInside() {
106
+ // Only a stranded `true` needs rescuing; SSR-safe — effects/handlers never
107
+ // run on the server, and `document` is guarded regardless.
108
+ if (typeof document === 'undefined' || !pointerInside || !containerEl) return;
109
+ const hit = document.elementFromPoint(pointerX, pointerY);
110
+ const toastEl = (hit?.closest('[data-toast-id]') ?? null) as HTMLElement | null;
111
+ // Inside only if the cursor sits over a toast that is (a) in THIS region and
112
+ // (b) still live — not the just-removed one, whose node lingers through its
113
+ // fly-out outro.
114
+ pointerInside =
115
+ !!toastEl &&
116
+ containerEl.contains(toastEl) &&
117
+ visibleToasts.some((t) => t.id === toastEl.dataset.toastId);
118
+ }
119
+
120
+ $effect(() => {
121
+ const count = visibleToasts.length;
122
+ untrack(() => {
123
+ if (count < prevVisibleCount) reconcilePointerInside();
124
+ prevVisibleCount = count;
125
+ });
126
+ });
127
+
61
128
  const styles = $derived(
62
129
  unstyled
63
130
  ? {
@@ -121,14 +188,37 @@
121
188
  </script>
122
189
 
123
190
  <div
191
+ bind:this={containerEl}
124
192
  class={[slot('container'), className].filter(Boolean).join(' ')}
125
193
  aria-live="polite"
126
194
  aria-relevant="additions removals"
195
+ onpointerover={(e) => {
196
+ pointerX = e.clientX;
197
+ pointerY = e.clientY;
198
+ pointerInside = true;
199
+ }}
200
+ onpointermove={(e) => {
201
+ pointerX = e.clientX;
202
+ pointerY = e.clientY;
203
+ }}
204
+ onpointerout={(e) => {
205
+ if (leftRegion(e)) pointerInside = false;
206
+ }}
207
+ onfocusin={() => (focusInside = true)}
208
+ onfocusout={(e) => {
209
+ if (leftRegion(e)) focusInside = false;
210
+ }}
127
211
  {...restProps}
128
212
  >
129
213
  {#each visibleToasts as toast (toast.id)}
130
214
  {@const IntentIcon = INTENT_ICON_MAP[toast.intent] ?? INTENT_ICON_MAP.neutral}
131
- <div class={slot('toast', toast.intent)} role="alert" transition:fly={flyParams()}>
215
+ <div
216
+ class={slot('toast', toast.intent)}
217
+ role="alert"
218
+ aria-atomic="true"
219
+ data-toast-id={toast.id}
220
+ transition:fly={flyParams()}
221
+ >
132
222
  {#if toast.loading}
133
223
  <span class={slot('icon', toast.intent)}><Spinner size="sm" /></span>
134
224
  {:else}
@@ -177,10 +267,20 @@
177
267
  </button>
178
268
  {/if}
179
269
 
180
- {#if toast.showProgress && toast.duration > 0}
270
+ {#if toast.showProgress && Number.isFinite(toast.duration) && toast.duration > 0}
271
+ <!-- The countdown animation lives in the `<style>` rule below (not inline)
272
+ so `@media (prefers-reduced-motion: reduce)` can reach it — an inline
273
+ `animation` shorthand is invisible to media queries. Only the real
274
+ per-toast duration is passed inline, as a custom property.
275
+ `animation-play-state` (also inline, bound to the store's paused flag)
276
+ freezes the bar with the timer on hover/focus and resumes it in place;
277
+ inline wins over the rule's implicit `running`, and toggling only the
278
+ play-state never restarts the animation. -->
181
279
  <div
182
- class={slot('progress', toast.intent)}
183
- style="animation: blocks-toast-progress {toast.duration}ms linear forwards;"
280
+ class={[slot('progress', toast.intent), 'blocks-toast-progress-bar']}
281
+ style="--toast-progress-duration: {toast.duration}ms; animation-play-state: {toaster.paused
282
+ ? 'paused'
283
+ : 'running'};"
184
284
  ></div>
185
285
  {/if}
186
286
  </div>
@@ -196,4 +296,22 @@
196
296
  width: 0%;
197
297
  }
198
298
  }
299
+
300
+ /* The countdown bar animates via this class (not an inline `animation`) so the
301
+ reduced-motion query below can reach it; the per-toast duration arrives as
302
+ the `--toast-progress-duration` custom property. `:global` keeps it off
303
+ Svelte's scoping, consistent with the global keyframe above. */
304
+ :global(.blocks-toast-progress-bar) {
305
+ animation: blocks-toast-progress var(--toast-progress-duration) linear forwards;
306
+ }
307
+
308
+ /* A countdown bar carries no information once it can't animate, so under
309
+ reduced motion we hide it outright rather than leave it frozen. This mirrors
310
+ interaction.css collapsing the motion tokens; the auto-dismiss timing itself
311
+ is unaffected. */
312
+ @media (prefers-reduced-motion: reduce) {
313
+ :global(.blocks-toast-progress-bar) {
314
+ display: none;
315
+ }
316
+ }
199
317
  </style>
@@ -14,6 +14,13 @@ import type { ToastPlacement } from './toast.variants.js';
14
14
  declare class ToastStore {
15
15
  toasts: ToastData[];
16
16
  placement: ToastPlacement;
17
+ /**
18
+ * Whether the whole visible stack is currently frozen because the pointer or
19
+ * keyboard focus is inside the toaster region. Drives each progress bar's
20
+ * `animation-play-state`; the auto-dismiss timers freeze together with it. Set
21
+ * by `<Toaster>` via {@link pause} / {@link resume}.
22
+ */
23
+ paused: boolean;
17
24
  private timers;
18
25
  private subscriberCount;
19
26
  private hasWarnedNoSubscriber;
@@ -22,6 +29,15 @@ declare class ToastStore {
22
29
  * the store knows whether a renderer is present. Returns an unsubscribe fn.
23
30
  */
24
31
  registerSubscriber(): () => void;
32
+ /**
33
+ * Arm the auto-dismiss timer for `id` — or, if the stack is currently paused,
34
+ * pre-register it frozen so it starts counting when the stack resumes.
35
+ * Persistent toasts (`duration <= 0` or non-finite, e.g. `Infinity`, and the
36
+ * loading leg of a promise toast) get no timer at all.
37
+ */
38
+ private armTimer;
39
+ /** Cancel and forget the timer for `id` (if any). */
40
+ private clearTimer;
25
41
  /** Create a toast with full control over all options. Returns the toast ID for programmatic dismissal. */
26
42
  add(input: ToastInput): string;
27
43
  /**
@@ -50,8 +66,23 @@ declare class ToastStore {
50
66
  promise<T>(promise: Promise<T>, opts: ToastPromiseOptions<T>): string;
51
67
  /** Remove a single toast by ID. Clears its auto-dismiss timer. */
52
68
  dismiss(id: string): void;
53
- /** Remove all toasts and cancel their timers. */
69
+ /** Remove all toasts, cancel their timers, and un-freeze the stack. */
54
70
  clear(): void;
71
+ /**
72
+ * Freeze the whole visible stack (Sonner-style hover-to-pause): cancel every
73
+ * running auto-dismiss timer, banking the time each has left, and flag
74
+ * {@link paused} so the progress bars stop. Idempotent — `<Toaster>` calls it
75
+ * on pointer-enter / focus-in of the region. Persistent toasts and a promise's
76
+ * loading leg have no timer and are untouched; the banked remainders are
77
+ * restarted by {@link resume}.
78
+ */
79
+ pause(): void;
80
+ /**
81
+ * Resume the stack: restart every frozen timer from its banked remaining time
82
+ * (not from the full duration) and clear {@link paused} so the progress bars
83
+ * run again. Idempotent — called on pointer-leave / focus-out of the region.
84
+ */
85
+ resume(): void;
55
86
  /** Shorthand for `add()` with `intent: 'info'`. Shows an info icon. */
56
87
  info(title: string, opts?: ToastShorthandOpts): string;
57
88
  /** Shorthand for `add()` with `intent: 'success'`. Shows a check icon. */
@@ -16,6 +16,13 @@ function uid() {
16
16
  class ToastStore {
17
17
  toasts = $state([]);
18
18
  placement = $state('bottom-right');
19
+ /**
20
+ * Whether the whole visible stack is currently frozen because the pointer or
21
+ * keyboard focus is inside the toaster region. Drives each progress bar's
22
+ * `animation-play-state`; the auto-dismiss timers freeze together with it. Set
23
+ * by `<Toaster>` via {@link pause} / {@link resume}.
24
+ */
25
+ paused = $state(false);
19
26
  timers = new Map();
20
27
  subscriberCount = 0;
21
28
  hasWarnedNoSubscriber = false;
@@ -31,6 +38,34 @@ class ToastStore {
31
38
  this.subscriberCount = Math.max(0, this.subscriberCount - 1);
32
39
  };
33
40
  }
41
+ /**
42
+ * Arm the auto-dismiss timer for `id` — or, if the stack is currently paused,
43
+ * pre-register it frozen so it starts counting when the stack resumes.
44
+ * Persistent toasts (`duration <= 0` or non-finite, e.g. `Infinity`, and the
45
+ * loading leg of a promise toast) get no timer at all.
46
+ */
47
+ armTimer(id, duration) {
48
+ if (!Number.isFinite(duration) || duration <= 0)
49
+ return;
50
+ if (this.paused) {
51
+ // A toast that arrives while the user is hovering the stack starts frozen;
52
+ // resume() lights its timer once the pointer/focus leaves.
53
+ this.timers.set(id, { handle: undefined, remaining: duration, startedAt: 0 });
54
+ return;
55
+ }
56
+ this.timers.set(id, {
57
+ handle: setTimeout(() => this.dismiss(id), duration),
58
+ remaining: duration,
59
+ startedAt: Date.now()
60
+ });
61
+ }
62
+ /** Cancel and forget the timer for `id` (if any). */
63
+ clearTimer(id) {
64
+ const entry = this.timers.get(id);
65
+ if (entry?.handle !== undefined)
66
+ clearTimeout(entry.handle);
67
+ this.timers.delete(id);
68
+ }
34
69
  /** Create a toast with full control over all options. Returns the toast ID for programmatic dismissal. */
35
70
  add(input) {
36
71
  if (this.subscriberCount === 0 &&
@@ -56,9 +91,7 @@ class ToastStore {
56
91
  loading: input.loading ?? false
57
92
  };
58
93
  this.toasts = [...this.toasts, toast];
59
- if (toast.duration > 0) {
60
- this.timers.set(id, setTimeout(() => this.dismiss(id), toast.duration));
61
- }
94
+ this.armTimer(id, toast.duration);
62
95
  return id;
63
96
  }
64
97
  /**
@@ -73,14 +106,9 @@ class ToastStore {
73
106
  return;
74
107
  const next = { ...this.toasts[idx], ...input };
75
108
  this.toasts = [...this.toasts.slice(0, idx), next, ...this.toasts.slice(idx + 1)];
76
- const timer = this.timers.get(id);
77
- if (timer) {
78
- clearTimeout(timer);
79
- this.timers.delete(id);
80
- }
81
- if (next.duration > 0) {
82
- this.timers.set(id, setTimeout(() => this.dismiss(id), next.duration));
83
- }
109
+ // Reset the auto-dismiss clock to the new duration (respecting a current pause).
110
+ this.clearTimer(id);
111
+ this.armTimer(id, next.duration);
84
112
  }
85
113
  /**
86
114
  * Drive a toast through a promise's lifecycle (Sonner-style). Shows a
@@ -150,19 +178,56 @@ class ToastStore {
150
178
  }
151
179
  /** Remove a single toast by ID. Clears its auto-dismiss timer. */
152
180
  dismiss(id) {
153
- const timer = this.timers.get(id);
154
- if (timer) {
155
- clearTimeout(timer);
156
- this.timers.delete(id);
157
- }
181
+ this.clearTimer(id);
158
182
  this.toasts = this.toasts.filter((t) => t.id !== id);
159
183
  }
160
- /** Remove all toasts and cancel their timers. */
184
+ /** Remove all toasts, cancel their timers, and un-freeze the stack. */
161
185
  clear() {
162
- for (const timer of this.timers.values())
163
- clearTimeout(timer);
186
+ for (const entry of this.timers.values()) {
187
+ if (entry.handle !== undefined)
188
+ clearTimeout(entry.handle);
189
+ }
164
190
  this.timers.clear();
165
191
  this.toasts = [];
192
+ this.paused = false;
193
+ }
194
+ /**
195
+ * Freeze the whole visible stack (Sonner-style hover-to-pause): cancel every
196
+ * running auto-dismiss timer, banking the time each has left, and flag
197
+ * {@link paused} so the progress bars stop. Idempotent — `<Toaster>` calls it
198
+ * on pointer-enter / focus-in of the region. Persistent toasts and a promise's
199
+ * loading leg have no timer and are untouched; the banked remainders are
200
+ * restarted by {@link resume}.
201
+ */
202
+ pause() {
203
+ if (this.paused)
204
+ return;
205
+ this.paused = true;
206
+ const now = Date.now();
207
+ for (const entry of this.timers.values()) {
208
+ if (entry.handle === undefined)
209
+ continue;
210
+ clearTimeout(entry.handle);
211
+ entry.remaining = Math.max(0, entry.remaining - (now - entry.startedAt));
212
+ entry.handle = undefined;
213
+ }
214
+ }
215
+ /**
216
+ * Resume the stack: restart every frozen timer from its banked remaining time
217
+ * (not from the full duration) and clear {@link paused} so the progress bars
218
+ * run again. Idempotent — called on pointer-leave / focus-out of the region.
219
+ */
220
+ resume() {
221
+ if (!this.paused)
222
+ return;
223
+ this.paused = false;
224
+ const now = Date.now();
225
+ for (const [id, entry] of this.timers) {
226
+ if (entry.handle !== undefined)
227
+ continue;
228
+ entry.startedAt = now;
229
+ entry.handle = setTimeout(() => this.dismiss(id), entry.remaining);
230
+ }
166
231
  }
167
232
  /** Shorthand for `add()` with `intent: 'info'`. Shows an info icon. */
168
233
  info(title, opts) {
@@ -1,70 +1,70 @@
1
1
  import { type SlotNames, type VariantProps } from '../../utils/variants.js';
2
2
  export declare const toastVariants: ((props?: {
3
- intent?: "info" | "primary" | "success" | "warning" | "danger" | "neutral" | undefined;
3
+ intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
4
4
  placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
5
5
  } | undefined) => {
6
6
  container: (props?: ({
7
- intent?: "info" | "primary" | "success" | "warning" | "danger" | "neutral" | undefined;
7
+ intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
8
8
  placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
9
9
  } & {
10
10
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
11
11
  }) | undefined) => string;
12
12
  toast: (props?: ({
13
- intent?: "info" | "primary" | "success" | "warning" | "danger" | "neutral" | undefined;
13
+ intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
14
14
  placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
15
15
  } & {
16
16
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
17
17
  }) | undefined) => string;
18
18
  icon: (props?: ({
19
- intent?: "info" | "primary" | "success" | "warning" | "danger" | "neutral" | undefined;
19
+ intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
20
20
  placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
21
21
  } & {
22
22
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
23
23
  }) | undefined) => string;
24
24
  content: (props?: ({
25
- intent?: "info" | "primary" | "success" | "warning" | "danger" | "neutral" | undefined;
25
+ intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
26
26
  placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
27
27
  } & {
28
28
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
29
29
  }) | undefined) => string;
30
30
  title: (props?: ({
31
- intent?: "info" | "primary" | "success" | "warning" | "danger" | "neutral" | undefined;
31
+ intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
32
32
  placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
33
33
  } & {
34
34
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
35
35
  }) | undefined) => string;
36
36
  description: (props?: ({
37
- intent?: "info" | "primary" | "success" | "warning" | "danger" | "neutral" | undefined;
37
+ intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
38
38
  placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
39
39
  } & {
40
40
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
41
41
  }) | undefined) => string;
42
42
  actions: (props?: ({
43
- intent?: "info" | "primary" | "success" | "warning" | "danger" | "neutral" | undefined;
43
+ intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
44
44
  placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
45
45
  } & {
46
46
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
47
47
  }) | undefined) => string;
48
48
  actionButton: (props?: ({
49
- intent?: "info" | "primary" | "success" | "warning" | "danger" | "neutral" | undefined;
49
+ intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
50
50
  placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
51
51
  } & {
52
52
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
53
53
  }) | undefined) => string;
54
54
  cancelButton: (props?: ({
55
- intent?: "info" | "primary" | "success" | "warning" | "danger" | "neutral" | undefined;
55
+ intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
56
56
  placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
57
57
  } & {
58
58
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
59
59
  }) | undefined) => string;
60
60
  dismissButton: (props?: ({
61
- intent?: "info" | "primary" | "success" | "warning" | "danger" | "neutral" | undefined;
61
+ intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
62
62
  placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
63
63
  } & {
64
64
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
65
65
  }) | undefined) => string;
66
66
  progress: (props?: ({
67
- intent?: "info" | "primary" | "success" | "warning" | "danger" | "neutral" | undefined;
67
+ intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
68
68
  placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
69
69
  } & {
70
70
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
@@ -28,6 +28,8 @@
28
28
  slotClasses: slotClassesProp = {},
29
29
  preset,
30
30
  withBorder = false,
31
+ 'aria-describedby': ariaDescribedby,
32
+ 'aria-label': ariaLabel,
31
33
  ...restProps
32
34
  }: ToggleProps = $props();
33
35
 
@@ -42,6 +44,14 @@
42
44
  disabled
43
45
  }));
44
46
 
47
+ // Consumer-supplied `aria-describedby` (e.g. an external hint rendered
48
+ // outside the component) merges with the internal error/helper chain instead
49
+ // of replacing it — internal descriptions first, the consumer's supplemental
50
+ // one last (mirrors the Input role model, XC-2).
51
+ const describedBy = $derived(
52
+ [ff.describedBy, ariaDescribedby].filter(Boolean).join(' ') || undefined
53
+ );
54
+
45
55
  const blocksConfig = getBlocksConfig();
46
56
  const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
47
57
 
@@ -101,7 +111,11 @@
101
111
  : styles.control({ class: slotClasses?.control })}
102
112
  for={ff.fieldId}
103
113
  >
114
+ <!-- aria-label: a visible `label` wins (stays undefined); with no label a
115
+ consumer-supplied aria-label is preferred over the generic i18n fallback
116
+ so external labelling survives the restProps-first spread. -->
104
117
  <input
118
+ {...restProps}
105
119
  id={ff.fieldId}
106
120
  type="checkbox"
107
121
  role="switch"
@@ -112,11 +126,10 @@
112
126
  {required}
113
127
  class="peer sr-only"
114
128
  aria-checked={checked}
115
- aria-label={label ? undefined : bt('accessibility.toggle') || 'Toggle'}
116
- aria-describedby={ff.describedBy}
129
+ aria-label={label ? undefined : (ariaLabel ?? (bt('accessibility.toggle') || 'Toggle'))}
130
+ aria-describedby={describedBy}
117
131
  aria-invalid={ff.invalid ? 'true' : undefined}
118
132
  onchange={handleChange}
119
- {...restProps}
120
133
  />
121
134
 
122
135
  <span