@rozie-ui/toast-solid 0.1.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/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/index.cjs +482 -0
- package/dist/index.d.cts +51 -0
- package/dist/index.d.mts +51 -0
- package/dist/index.mjs +477 -0
- package/package.json +66 -0
- package/src/Toaster.tsx +683 -0
- package/src/index.ts +5 -0
- package/src/themes/base.css +57 -0
- package/src/themes/bootstrap.css +27 -0
- package/src/themes/material.css +28 -0
- package/src/themes/shadcn.css +29 -0
package/src/Toaster.tsx
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
import type { JSX } from 'solid-js';
|
|
2
|
+
import { Show, createSignal, mergeProps, onCleanup, onMount, splitProps } from 'solid-js';
|
|
3
|
+
import { Key } from '@solid-primitives/keyed';
|
|
4
|
+
import { __rozieInjectStyle, mergeListeners, parseInlineStyle, rozieAttr, rozieClass, rozieDisplay } from '@rozie/runtime-solid';
|
|
5
|
+
|
|
6
|
+
__rozieInjectStyle('Toaster-12d4265c', `@media (prefers-reduced-motion: reduce) {
|
|
7
|
+
.rozie-toast[data-rozie-s-12d4265c] {
|
|
8
|
+
animation-name: rozie-toast-fade-in;
|
|
9
|
+
animation-duration: 1ms;
|
|
10
|
+
}
|
|
11
|
+
.rozie-toast--exiting[data-rozie-s-12d4265c] {
|
|
12
|
+
animation-name: rozie-toast-fade-out;
|
|
13
|
+
animation-duration: 1ms;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
.rozie-toaster[data-rozie-s-12d4265c] {
|
|
17
|
+
position: fixed;
|
|
18
|
+
z-index: var(--rozie-toast-z, 9999);
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
gap: var(--rozie-toast-gap, 0.5rem);
|
|
22
|
+
padding: var(--rozie-toast-region-padding, 1rem);
|
|
23
|
+
max-width: var(--rozie-toast-max-width, calc(100vw - 2rem));
|
|
24
|
+
pointer-events: none;
|
|
25
|
+
font: var(--rozie-toast-font, inherit);
|
|
26
|
+
}
|
|
27
|
+
.rozie-toaster[data-rozie-s-12d4265c] > *[data-rozie-s-12d4265c] {
|
|
28
|
+
pointer-events: auto;
|
|
29
|
+
}
|
|
30
|
+
.rozie-toaster--top-left[data-rozie-s-12d4265c] { top: 0; left: 0; align-items: flex-start; }
|
|
31
|
+
.rozie-toaster--top-right[data-rozie-s-12d4265c] { top: 0; right: 0; align-items: flex-end; }
|
|
32
|
+
.rozie-toaster--top-center[data-rozie-s-12d4265c] { top: 0; left: 50%; transform: translateX(-50%); align-items: center; }
|
|
33
|
+
.rozie-toaster--bottom-left[data-rozie-s-12d4265c] { bottom: 0; left: 0; align-items: flex-start; flex-direction: column-reverse; }
|
|
34
|
+
.rozie-toaster--bottom-right[data-rozie-s-12d4265c] { bottom: 0; right: 0; align-items: flex-end; flex-direction: column-reverse; }
|
|
35
|
+
.rozie-toaster--bottom-center[data-rozie-s-12d4265c] { bottom: 0; left: 50%; transform: translateX(-50%); align-items: center; flex-direction: column-reverse; }
|
|
36
|
+
.rozie-toaster--stacked[data-rozie-s-12d4265c] .rozie-toast[data-rozie-s-12d4265c] {
|
|
37
|
+
grid-area: 1 / 1;
|
|
38
|
+
z-index: calc(100 - var(--rozie-toast-depth, 0));
|
|
39
|
+
}
|
|
40
|
+
.rozie-toaster--stacked[data-rozie-s-12d4265c]:not([data-rozie-s-12d4265c]:hover):not([data-rozie-s-12d4265c]:focus-within) {
|
|
41
|
+
display: grid;
|
|
42
|
+
}
|
|
43
|
+
.rozie-toaster--stacked[data-rozie-s-12d4265c]:not([data-rozie-s-12d4265c]:hover):not([data-rozie-s-12d4265c]:focus-within) .rozie-toast[data-rozie-s-12d4265c] {
|
|
44
|
+
transform:
|
|
45
|
+
translateY(calc(var(--rozie-toast-depth, 0) * var(--rozie-toast-stack-offset, 8px)))
|
|
46
|
+
scale(calc(1 - var(--rozie-toast-depth, 0) * var(--rozie-toast-stack-scale-step, 0.05)));
|
|
47
|
+
opacity: calc(1 - min(1, max(0, var(--rozie-toast-depth, 0) - 2)));
|
|
48
|
+
}
|
|
49
|
+
.rozie-toaster--stacked.rozie-toaster--bottom-left[data-rozie-s-12d4265c]:not([data-rozie-s-12d4265c]:hover):not([data-rozie-s-12d4265c]:focus-within) .rozie-toast[data-rozie-s-12d4265c],
|
|
50
|
+
.rozie-toaster--stacked.rozie-toaster--bottom-right[data-rozie-s-12d4265c]:not([data-rozie-s-12d4265c]:hover):not([data-rozie-s-12d4265c]:focus-within) .rozie-toast[data-rozie-s-12d4265c],
|
|
51
|
+
.rozie-toaster--stacked.rozie-toaster--bottom-center[data-rozie-s-12d4265c]:not([data-rozie-s-12d4265c]:hover):not([data-rozie-s-12d4265c]:focus-within) .rozie-toast[data-rozie-s-12d4265c] {
|
|
52
|
+
transform:
|
|
53
|
+
translateY(calc(var(--rozie-toast-depth, 0) * var(--rozie-toast-stack-offset, 8px) * -1))
|
|
54
|
+
scale(calc(1 - var(--rozie-toast-depth, 0) * var(--rozie-toast-stack-scale-step, 0.05)));
|
|
55
|
+
}
|
|
56
|
+
.rozie-toast[data-rozie-s-12d4265c] {
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
gap: var(--rozie-toast-content-gap, 0.75rem);
|
|
60
|
+
min-width: var(--rozie-toast-min-width, 16rem);
|
|
61
|
+
max-width: var(--rozie-toast-toast-max-width, 24rem);
|
|
62
|
+
padding: var(--rozie-toast-padding, 0.75rem 1rem);
|
|
63
|
+
color: var(--rozie-toast-color, #fff);
|
|
64
|
+
background: var(--rozie-toast-bg, #333);
|
|
65
|
+
border-radius: var(--rozie-toast-radius, 0.5rem);
|
|
66
|
+
box-shadow: var(--rozie-toast-shadow, 0 6px 20px rgba(0, 0, 0, 0.25));
|
|
67
|
+
/* Swipe: page scroll stays alive on touch along the axis the toast does
|
|
68
|
+
NOT move on. The transition here drives the spring-back (the active-drag
|
|
69
|
+
:style sets an inline \`transition: none\` to track the finger 1:1;
|
|
70
|
+
releasing it without a further gesture falls back to this transition). */
|
|
71
|
+
touch-action: pan-y;
|
|
72
|
+
transition: transform 200ms ease, opacity 200ms ease;
|
|
73
|
+
}
|
|
74
|
+
.rozie-toaster--top-center[data-rozie-s-12d4265c] .rozie-toast[data-rozie-s-12d4265c],
|
|
75
|
+
.rozie-toaster--bottom-center[data-rozie-s-12d4265c] .rozie-toast[data-rozie-s-12d4265c] {
|
|
76
|
+
touch-action: pan-x;
|
|
77
|
+
}
|
|
78
|
+
.rozie-toast--success[data-rozie-s-12d4265c] { background: var(--rozie-toast-success-bg, #16a34a); }
|
|
79
|
+
.rozie-toast--error[data-rozie-s-12d4265c] { background: var(--rozie-toast-error-bg, #dc2626); }
|
|
80
|
+
.rozie-toast--warning[data-rozie-s-12d4265c] { background: var(--rozie-toast-warning-bg, #ca8a04); }
|
|
81
|
+
.rozie-toast--info[data-rozie-s-12d4265c] { background: var(--rozie-toast-info-bg, var(--rozie-toast-bg, #333)); }
|
|
82
|
+
from[data-rozie-s-12d4265c] { opacity: 0; transform: translateY(-0.5rem); }
|
|
83
|
+
to[data-rozie-s-12d4265c] { opacity: 1; transform: translateY(0); }
|
|
84
|
+
from[data-rozie-s-12d4265c] { opacity: 0; transform: translateY(0.5rem); }
|
|
85
|
+
to[data-rozie-s-12d4265c] { opacity: 1; transform: translateY(0); }
|
|
86
|
+
from[data-rozie-s-12d4265c] { opacity: 1; transform: translateY(0); }
|
|
87
|
+
to[data-rozie-s-12d4265c] { opacity: 0; transform: translateY(-0.5rem); }
|
|
88
|
+
from[data-rozie-s-12d4265c] { opacity: 1; transform: translateY(0); }
|
|
89
|
+
to[data-rozie-s-12d4265c] { opacity: 0; transform: translateY(0.5rem); }
|
|
90
|
+
.rozie-toast[data-rozie-s-12d4265c] {
|
|
91
|
+
animation: rozie-toast-enter var(--rozie-toast-enter-duration, 200ms) ease-out;
|
|
92
|
+
}
|
|
93
|
+
.rozie-toaster--bottom-left[data-rozie-s-12d4265c] .rozie-toast[data-rozie-s-12d4265c],
|
|
94
|
+
.rozie-toaster--bottom-right[data-rozie-s-12d4265c] .rozie-toast[data-rozie-s-12d4265c],
|
|
95
|
+
.rozie-toaster--bottom-center[data-rozie-s-12d4265c] .rozie-toast[data-rozie-s-12d4265c] {
|
|
96
|
+
animation-name: rozie-toast-enter-from-bottom;
|
|
97
|
+
}
|
|
98
|
+
.rozie-toast--exiting[data-rozie-s-12d4265c] {
|
|
99
|
+
animation: rozie-toast-exit var(--rozie-toast-exit-duration, 200ms) ease-in forwards;
|
|
100
|
+
}
|
|
101
|
+
.rozie-toaster--bottom-left[data-rozie-s-12d4265c] .rozie-toast--exiting[data-rozie-s-12d4265c],
|
|
102
|
+
.rozie-toaster--bottom-right[data-rozie-s-12d4265c] .rozie-toast--exiting[data-rozie-s-12d4265c],
|
|
103
|
+
.rozie-toaster--bottom-center[data-rozie-s-12d4265c] .rozie-toast--exiting[data-rozie-s-12d4265c] {
|
|
104
|
+
animation-name: rozie-toast-exit-to-bottom;
|
|
105
|
+
}
|
|
106
|
+
from[data-rozie-s-12d4265c] { opacity: 0; }
|
|
107
|
+
to[data-rozie-s-12d4265c] { opacity: 1; }
|
|
108
|
+
from[data-rozie-s-12d4265c] { opacity: 1; }
|
|
109
|
+
to[data-rozie-s-12d4265c] { opacity: 0; }
|
|
110
|
+
from[data-rozie-s-12d4265c] { opacity: 1; transform: translateX(0); }
|
|
111
|
+
to[data-rozie-s-12d4265c] { opacity: 0; transform: translateX(calc(var(--rozie-toast-swipe-exit, 1) * 100%)); }
|
|
112
|
+
from[data-rozie-s-12d4265c] { opacity: 1; transform: translateY(0); }
|
|
113
|
+
to[data-rozie-s-12d4265c] { opacity: 0; transform: translateY(calc(var(--rozie-toast-swipe-exit, 1) * 100%)); }
|
|
114
|
+
.rozie-toast--exiting.rozie-toast--swipe-exit[data-rozie-s-12d4265c] {
|
|
115
|
+
animation-name: rozie-toast-swipe-exit-x;
|
|
116
|
+
}
|
|
117
|
+
.rozie-toaster--top-center[data-rozie-s-12d4265c] .rozie-toast--exiting.rozie-toast--swipe-exit[data-rozie-s-12d4265c],
|
|
118
|
+
.rozie-toaster--bottom-center[data-rozie-s-12d4265c] .rozie-toast--exiting.rozie-toast--swipe-exit[data-rozie-s-12d4265c] {
|
|
119
|
+
animation-name: rozie-toast-swipe-exit-y;
|
|
120
|
+
}
|
|
121
|
+
.rozie-toast-spinner[data-rozie-s-12d4265c] {
|
|
122
|
+
flex: 0 0 auto;
|
|
123
|
+
width: var(--rozie-toast-spinner-size, 1em);
|
|
124
|
+
height: var(--rozie-toast-spinner-size, 1em);
|
|
125
|
+
border: 2px solid color-mix(in srgb, var(--rozie-toast-spinner-color, currentColor) 25%, transparent);
|
|
126
|
+
border-top-color: var(--rozie-toast-spinner-color, currentColor);
|
|
127
|
+
border-radius: 50%;
|
|
128
|
+
animation: rozie-toast-spin 0.75s linear infinite;
|
|
129
|
+
}
|
|
130
|
+
to[data-rozie-s-12d4265c] { transform: rotate(360deg); }
|
|
131
|
+
.rozie-toast-message[data-rozie-s-12d4265c] {
|
|
132
|
+
flex: 1 1 auto;
|
|
133
|
+
font-size: var(--rozie-toast-font-size, 0.9rem);
|
|
134
|
+
}
|
|
135
|
+
.rozie-toast-close[data-rozie-s-12d4265c] {
|
|
136
|
+
flex: 0 0 auto;
|
|
137
|
+
display: inline-flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
justify-content: center;
|
|
140
|
+
width: var(--rozie-toast-close-size, 1.25rem);
|
|
141
|
+
height: var(--rozie-toast-close-size, 1.25rem);
|
|
142
|
+
padding: 0;
|
|
143
|
+
font-size: 1.1rem;
|
|
144
|
+
line-height: 1;
|
|
145
|
+
color: inherit;
|
|
146
|
+
background: transparent;
|
|
147
|
+
border: none;
|
|
148
|
+
border-radius: 0.25rem;
|
|
149
|
+
opacity: var(--rozie-toast-close-opacity, 0.75);
|
|
150
|
+
cursor: pointer;
|
|
151
|
+
}
|
|
152
|
+
.rozie-toast-close[data-rozie-s-12d4265c]:hover {
|
|
153
|
+
opacity: 1;
|
|
154
|
+
}`);
|
|
155
|
+
|
|
156
|
+
interface ToastSlotCtx { toast: any; dismiss: any; }
|
|
157
|
+
|
|
158
|
+
interface ToasterProps {
|
|
159
|
+
/**
|
|
160
|
+
* Which corner the toast stack renders in: `'top-left'`, `'top-right'`, `'top-center'`, `'bottom-left'`, `'bottom-right'`, or `'bottom-center'`. Drives the fixed-position layout and the stack direction.
|
|
161
|
+
*/
|
|
162
|
+
position?: string;
|
|
163
|
+
/**
|
|
164
|
+
* Default auto-dismiss time in milliseconds, applied to any toast that does not pass its own `duration`. `0` (or a per-toast `duration` of `0`) makes the toast sticky — it stays until explicitly dismissed.
|
|
165
|
+
*/
|
|
166
|
+
duration?: number;
|
|
167
|
+
/**
|
|
168
|
+
* Maximum number of visible toasts (`0` = unlimited). When the queue exceeds this, the oldest toasts drop off the stack.
|
|
169
|
+
*/
|
|
170
|
+
max?: number;
|
|
171
|
+
/**
|
|
172
|
+
* Opt **out** of pausing the auto-dismiss timers while the pointer is over the stack. By default hovering pauses every timer and leaving restarts them; set this to keep toasts dismissing on schedule regardless of hover.
|
|
173
|
+
*/
|
|
174
|
+
disablePauseOnHover?: boolean;
|
|
175
|
+
/**
|
|
176
|
+
* Accessible name for the live region (`role="region"`), applied as its `aria-label`. Defaults to `'Notifications'` when not set, so assistive tech can navigate to the toast stack as a landmark.
|
|
177
|
+
*/
|
|
178
|
+
ariaLabel?: (string) | null;
|
|
179
|
+
/**
|
|
180
|
+
* Opt **out** of pointer swipe-to-dismiss. By default, dragging a toast past 45% of its own width/height (direction auto-derived from `position`) or a fast flick dismisses it with reason `'swipe'`; a short drag springs back. A drag starting on the close button (or any button/link) never swipes.
|
|
181
|
+
*/
|
|
182
|
+
disableSwipe?: boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Opt **in** to a sonner-style collapsed stack: a single-cell grid overlay with depth-driven transforms (toasts at depth 3+ fade to invisible), newest on top. Hovering the region or moving keyboard focus into it expands to the normal flex-column stack; leaving re-collapses. `false` (default) renders the plain flex column at all times.
|
|
185
|
+
*/
|
|
186
|
+
stacked?: boolean;
|
|
187
|
+
onDismissed?: (...args: unknown[]) => void;
|
|
188
|
+
toastSlot?: (ctx: ToastSlotCtx) => JSX.Element;
|
|
189
|
+
slots?: Record<string, (ctx: any) => JSX.Element>;
|
|
190
|
+
ref?: (h: ToasterHandle) => void;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface ToasterHandle {
|
|
194
|
+
show: (...args: any[]) => any;
|
|
195
|
+
dismiss: (...args: any[]) => any;
|
|
196
|
+
clear: (...args: any[]) => any;
|
|
197
|
+
patch: (...args: any[]) => any;
|
|
198
|
+
promise: (...args: any[]) => any;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export default function Toaster(_props: ToasterProps): JSX.Element {
|
|
202
|
+
const _merged = mergeProps({ position: 'bottom-right', duration: 4000, max: 0, disablePauseOnHover: false, ariaLabel: null, disableSwipe: false, stacked: false }, _props);
|
|
203
|
+
const [local, attrs] = splitProps(_merged, ['position', 'duration', 'max', 'disablePauseOnHover', 'ariaLabel', 'disableSwipe', 'stacked', 'ref']);
|
|
204
|
+
onMount(() => { local.ref?.({ show, dismiss, clear, patch, promise }); });
|
|
205
|
+
|
|
206
|
+
const [toasts, setToasts] = createSignal<any[]>([]);
|
|
207
|
+
const [seq, setSeq] = createSignal(0);
|
|
208
|
+
const [swipe, setSwipe] = createSignal<any>(null);
|
|
209
|
+
const [swipeGesture, setSwipeGesture] = createSignal<any>(null);
|
|
210
|
+
onCleanup(() => {
|
|
211
|
+
unmounted = true;
|
|
212
|
+
teardownTimers();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Mutable cross-render scratch (NOT reactive): per-id timer bookkeeping. A
|
|
216
|
+
// top-level `let` → React useRef (it escapes into $onUnmount's effect, so the
|
|
217
|
+
// emitter hoists it). The id counter lives in $data.seq instead (see <data>).
|
|
218
|
+
//
|
|
219
|
+
// Shape: { [id]: { handle, startedAt, remaining } }. `pauseTimers` clears the
|
|
220
|
+
// live setTimeout handle but KEEPS the entry with a decremented `remaining` —
|
|
221
|
+
// the remainder IS the state (this is what makes the hover pause PRECISE
|
|
222
|
+
// instead of a full restart). `resumeTimers` re-arms with exactly that
|
|
223
|
+
// remainder. `clearTimer`/the full-teardown helper below are the only ways an
|
|
224
|
+
// entry is actually removed from the map.
|
|
225
|
+
let timers = {};
|
|
226
|
+
|
|
227
|
+
// Per-id handles for the ~350ms exit-removal failsafe (the fallback that
|
|
228
|
+
// removes a toast if its @animationend never fires). Tracked in a module map
|
|
229
|
+
// — NOT an anonymous window.setTimeout — so teardownTimers ($onUnmount /
|
|
230
|
+
// clear()) can cancel a pending failsafe (else it fires post-unmount and
|
|
231
|
+
// writes $data on a torn-down instance) and removeToast can cancel it
|
|
232
|
+
// first-wins when @animationend beats it. Escapes into $onUnmount's effect →
|
|
233
|
+
// React hoists it to useRef alongside `timers`.
|
|
234
|
+
let exitFailsafes = {};
|
|
235
|
+
|
|
236
|
+
// Set true in $onUnmount; read by promise()'s settle guard (never-resurrect
|
|
237
|
+
// a toast after the host itself is gone). A top-level `let` → React useRef
|
|
238
|
+
// (it escapes into $onUnmount's effect).
|
|
239
|
+
let unmounted = false;
|
|
240
|
+
|
|
241
|
+
// Same-tick id-uniqueness guard for React. The id counter lives in reactive
|
|
242
|
+
// $data.seq (persists across renders), but React batches setState so within a
|
|
243
|
+
// SINGLE synchronous tick two show() calls read the SAME stale $data.seq →
|
|
244
|
+
// duplicate ids. `seqLocal` is a plain counter incremented SYNCHRONOUSLY in
|
|
245
|
+
// show(); it survives the same tick (and, because show() is an $expose verb,
|
|
246
|
+
// the emitter hoists it to a persistent useRef on React too — but the design
|
|
247
|
+
// does NOT depend on that: `Math.max($data.seq, seqLocal)` is correct whether
|
|
248
|
+
// seqLocal persists OR resets per render, since the monotonic $data.seq
|
|
249
|
+
// carries the high-water mark across any reset). On the other five targets
|
|
250
|
+
// $data.seq is synchronous, so the two simply stay in lockstep. Result:
|
|
251
|
+
// strictly-increasing, collision-free ids on all six with NO randomness.
|
|
252
|
+
let seqLocal = 0;
|
|
253
|
+
|
|
254
|
+
// Hover-pause flag: true while the pointer is over the stack (set by
|
|
255
|
+
// pauseTimers, cleared by resumeTimers). Read by patch() so a duration change
|
|
256
|
+
// arriving mid-hover stores the new remainder WITHOUT arming a live timer
|
|
257
|
+
// (which would dismiss the toast while it is still hovered) — resume arms it
|
|
258
|
+
// on leave. A top-level `let` reachable from the $expose verbs (patch/show →
|
|
259
|
+
// startTimer) and the @mouseenter/@mouseleave handlers, so React hoists it to
|
|
260
|
+
// useRef (persistent) like `timers`.
|
|
261
|
+
let paused = false;
|
|
262
|
+
|
|
263
|
+
// ---- timers ------------------------------------------------------------
|
|
264
|
+
function startTimer(toast: any) {
|
|
265
|
+
if (!toast || !toast.duration || toast.duration <= 0) return;
|
|
266
|
+
if (typeof window === 'undefined') return;
|
|
267
|
+
// Belt-and-braces: clear any pre-existing live handle for this id before
|
|
268
|
+
// overwriting the entry, so a re-arm never orphans a running timeout.
|
|
269
|
+
const existing = timers[toast.id];
|
|
270
|
+
if (existing && existing.handle != null) window.clearTimeout(existing.handle);
|
|
271
|
+
const remaining = toast.duration;
|
|
272
|
+
const handle = window.setTimeout(() => dismissBegin(toast.id, 'timeout'), remaining);
|
|
273
|
+
timers[toast.id] = {
|
|
274
|
+
handle,
|
|
275
|
+
startedAt: Date.now(),
|
|
276
|
+
remaining
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function clearTimer(id: any) {
|
|
280
|
+
const entry = timers[id];
|
|
281
|
+
if (entry && entry.handle != null && typeof window !== 'undefined') window.clearTimeout(entry.handle);
|
|
282
|
+
delete timers[id];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Pauses every live timer WITHOUT losing the remainder: clears the handle,
|
|
286
|
+
// decrements `remaining` by the elapsed time, and KEEPS the entry (does NOT
|
|
287
|
+
// delete it — the old v1 shortcut deleted entries here, which is why leave
|
|
288
|
+
// had to do a full restart).
|
|
289
|
+
function pauseTimers() {
|
|
290
|
+
paused = true;
|
|
291
|
+
if (typeof window === 'undefined') return;
|
|
292
|
+
for (const id in timers) {
|
|
293
|
+
const entry = timers[id];
|
|
294
|
+
// Idempotent: an entry already paused (handle cleared) keeps its stored
|
|
295
|
+
// remainder. A second pause must NOT re-subtract elapsed against the
|
|
296
|
+
// original startedAt — that drove `remaining` negative and stranded the
|
|
297
|
+
// toast forever once resume saw the non-positive value.
|
|
298
|
+
if (entry.handle == null) continue;
|
|
299
|
+
window.clearTimeout(entry.handle);
|
|
300
|
+
const elapsed = Date.now() - entry.startedAt;
|
|
301
|
+
// Clamp so a late pause (e.g. a background-tab timer that overran) can
|
|
302
|
+
// never store a negative remainder.
|
|
303
|
+
const remaining = Math.max(0, entry.remaining - elapsed);
|
|
304
|
+
timers[id] = {
|
|
305
|
+
handle: null,
|
|
306
|
+
startedAt: entry.startedAt,
|
|
307
|
+
remaining
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Re-arms every paused timer with EXACTLY its stored remainder (called on
|
|
313
|
+
// mouse leave). An entry with a non-positive remainder is left un-armed
|
|
314
|
+
// (it will be cleaned up by the next dismiss/clear pass) rather than firing
|
|
315
|
+
// immediately from inside this loop.
|
|
316
|
+
function resumeTimers() {
|
|
317
|
+
paused = false;
|
|
318
|
+
if (typeof window === 'undefined') return;
|
|
319
|
+
for (const id in timers) {
|
|
320
|
+
const entry = timers[id];
|
|
321
|
+
// Only re-arm entries that are actually paused (handle cleared). A live
|
|
322
|
+
// handle is left alone — re-arming it would orphan the running timeout.
|
|
323
|
+
if (entry.handle != null) continue;
|
|
324
|
+
if (entry.remaining == null || entry.remaining <= 0) {
|
|
325
|
+
// Its deadline elapsed while paused (a background-tab overrun, or a
|
|
326
|
+
// remainder clamped to 0): treat as EXPIRED and dismiss now — its time
|
|
327
|
+
// is up — rather than leaving it un-armed and stranded forever.
|
|
328
|
+
dismissBegin(id, 'timeout');
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const remaining = entry.remaining;
|
|
332
|
+
const handle = window.setTimeout(() => dismissBegin(id, 'timeout'), remaining);
|
|
333
|
+
timers[id] = {
|
|
334
|
+
handle,
|
|
335
|
+
startedAt: Date.now(),
|
|
336
|
+
remaining
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// FULL teardown: clears every live handle AND drops every entry (unlike
|
|
342
|
+
// pauseTimers, which deliberately keeps entries to hold their remainders).
|
|
343
|
+
// clear() and $onUnmount can no longer reuse pauseTimers for this reason.
|
|
344
|
+
function teardownTimers() {
|
|
345
|
+
if (typeof window !== 'undefined') {
|
|
346
|
+
for (const id in timers) {
|
|
347
|
+
const entry = timers[id];
|
|
348
|
+
if (entry.handle != null) window.clearTimeout(entry.handle);
|
|
349
|
+
}
|
|
350
|
+
// Also cancel every pending exit failsafe — otherwise a removal timeout
|
|
351
|
+
// scheduled just before unmount/clear() fires afterward and writes $data.
|
|
352
|
+
for (const id in exitFailsafes) {
|
|
353
|
+
if (exitFailsafes[id] != null) window.clearTimeout(exitFailsafes[id]);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
timers = {};
|
|
357
|
+
exitFailsafes = {};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ---- queue (imperative handle implementations) -------------------------
|
|
361
|
+
function show(input: any) {
|
|
362
|
+
const t = input || {};
|
|
363
|
+
let id;
|
|
364
|
+
if (t.id != null) {
|
|
365
|
+
// Coerce a consumer-supplied id to a String once, at the single entry
|
|
366
|
+
// point. Ids flow through the `timers` map (whose `for (const id in …)`
|
|
367
|
+
// keys are ALWAYS strings) and every downstream `t.id === id` strict
|
|
368
|
+
// comparison; a numeric consumer id (`show({ id: 42 })`) would otherwise
|
|
369
|
+
// stop matching after a hover pause/resume re-arms with the string key.
|
|
370
|
+
id = String(t.id);
|
|
371
|
+
} else {
|
|
372
|
+
// Take the high-water mark of the persistent-but-tick-stale $data.seq and
|
|
373
|
+
// the synchronous-but-maybe-per-render seqLocal (see the <script> comment)
|
|
374
|
+
// so same-tick multi-show yields DISTINCT ids on React too. Read both
|
|
375
|
+
// BEFORE writing either (no read-after-write of $data.seq → ROZ138-safe).
|
|
376
|
+
const s = Math.max(seq(), seqLocal);
|
|
377
|
+
id = 't' + s;
|
|
378
|
+
seqLocal = s + 1;
|
|
379
|
+
setSeq(s + 1);
|
|
380
|
+
}
|
|
381
|
+
const toast = {
|
|
382
|
+
id,
|
|
383
|
+
message: t.message != null ? t.message : '',
|
|
384
|
+
type: t.type || 'info',
|
|
385
|
+
duration: t.duration != null ? t.duration : local.duration
|
|
386
|
+
};
|
|
387
|
+
// ONE self-referential assignment so the React emitter lowers it to the
|
|
388
|
+
// concurrent-safe functional updater `setToasts(prev => …)` (it only does so
|
|
389
|
+
// when the RHS reads $data.toasts DIRECTLY — a via-a-local form lowered to a
|
|
390
|
+
// stale-closure `setToasts(<value>)`, losing the first of two same-tick
|
|
391
|
+
// toasts). slice() start: keep the newest `max` when over the cap
|
|
392
|
+
// (Math.max(0, len+1-max)), else slice(0) = the whole fresh array.
|
|
393
|
+
setToasts(toasts().concat([toast]).slice(local.max > 0 ? Math.max(0, toasts().length + 1 - local.max) : 0));
|
|
394
|
+
startTimer(toast);
|
|
395
|
+
return id;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ---- exit lifecycle ------------------------------------------------------
|
|
399
|
+
// Deliberately exceeds the 200ms default --rozie-toast-exit-duration token
|
|
400
|
+
// comfortably; a consumer overriding the exit duration beyond ~350ms gets cut
|
|
401
|
+
// short by this failsafe (documented in docs/components/toast.md).
|
|
402
|
+
const EXIT_FAILSAFE_MS = 350;
|
|
403
|
+
|
|
404
|
+
// Idempotent removal: filters the entry out of $data.toasts. Safe to call
|
|
405
|
+
// twice (from the inline @animationend binding AND the failsafe) — the
|
|
406
|
+
// second call is a harmless no-op filter over an already-absent id.
|
|
407
|
+
function removeToast(id: any) {
|
|
408
|
+
// Cancel any pending exit failsafe for this id (first-wins: @animationend
|
|
409
|
+
// beating the ~350ms timeout, or vice-versa — either way, only one removal).
|
|
410
|
+
if (typeof window !== 'undefined' && exitFailsafes[id] != null) {
|
|
411
|
+
window.clearTimeout(exitFailsafes[id]);
|
|
412
|
+
}
|
|
413
|
+
delete exitFailsafes[id];
|
|
414
|
+
setToasts(toasts().filter((t: any) => t.id !== id));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// The single dismissal funnel every path routes through: the `dismiss(id)`
|
|
418
|
+
// verb ('api'), the built-in close button ('close'), a timer expiry
|
|
419
|
+
// ('timeout'), and a swipe past threshold ('swipe'). Idempotent via the
|
|
420
|
+
// entry's `exiting` flag — a second call on an id already exiting (or
|
|
421
|
+
// already gone) is a no-op, so a stray timeout firing mid-exit never
|
|
422
|
+
// double-emits. `extra` (swipe only) carries `{ swipeExitSign }` so the
|
|
423
|
+
// template can apply the direction-matched swipe-exit animation.
|
|
424
|
+
function dismissBegin(id: any, reason: any, extra?: {
|
|
425
|
+
swipeExitSign?: number;
|
|
426
|
+
}) {
|
|
427
|
+
const entry = toasts().find((t: any) => t.id === id);
|
|
428
|
+
if (!entry || entry.exiting) return;
|
|
429
|
+
clearTimer(id);
|
|
430
|
+
_props.onDismissed?.({
|
|
431
|
+
toast: entry,
|
|
432
|
+
reason
|
|
433
|
+
});
|
|
434
|
+
setToasts(toasts().map((t: any) => t.id === id ? {
|
|
435
|
+
...t,
|
|
436
|
+
exiting: true,
|
|
437
|
+
...(extra || {})
|
|
438
|
+
} : t));
|
|
439
|
+
if (typeof window === 'undefined') {
|
|
440
|
+
removeToast(id);
|
|
441
|
+
} else {
|
|
442
|
+
exitFailsafes[id] = window.setTimeout(() => removeToast(id), EXIT_FAILSAFE_MS);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function dismiss(id: any) {
|
|
446
|
+
dismissBegin(id, 'api');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// clear() is bulk: immediate full teardown, NO per-toast exit animation and
|
|
450
|
+
// NO emit (documented — see docs/components/toast.md).
|
|
451
|
+
function clear() {
|
|
452
|
+
teardownTimers();
|
|
453
|
+
setToasts([]);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ---- patch / promise ------------------------------------------------------
|
|
457
|
+
// Update-in-place primitive: merges ONLY the present `{message,type,duration}`
|
|
458
|
+
// keys into the matching entry via a fresh-array map (never in-place
|
|
459
|
+
// mutation). Returns whether the id existed. A `duration` key clears+restarts
|
|
460
|
+
// the timer (0 → sticky/no-arm; positive → arm); any other key leaves a
|
|
461
|
+
// running timer untouched.
|
|
462
|
+
function patch(id: any, changes: any) {
|
|
463
|
+
const c = changes || {};
|
|
464
|
+
let existed = false;
|
|
465
|
+
const next = toasts().map((t: any) => {
|
|
466
|
+
if (t.id !== id) return t;
|
|
467
|
+
// Treat an EXITING entry as absent — never resurrect a toast whose
|
|
468
|
+
// dismissal is already in flight (removal deferred to @animationend / the
|
|
469
|
+
// failsafe). `existed` stays false → patch returns false, writes nothing,
|
|
470
|
+
// arms no timer.
|
|
471
|
+
if (t.exiting) return t;
|
|
472
|
+
existed = true;
|
|
473
|
+
const merged = {
|
|
474
|
+
...t
|
|
475
|
+
};
|
|
476
|
+
if (c.message !== undefined) merged.message = c.message;
|
|
477
|
+
if (c.type !== undefined) merged.type = c.type;
|
|
478
|
+
if (c.duration !== undefined) merged.duration = c.duration;
|
|
479
|
+
return merged;
|
|
480
|
+
});
|
|
481
|
+
if (!existed) return false;
|
|
482
|
+
setToasts(next);
|
|
483
|
+
if (c.duration !== undefined) {
|
|
484
|
+
clearTimer(id);
|
|
485
|
+
const patched = next.find((t: any) => t.id === id);
|
|
486
|
+
if (paused) {
|
|
487
|
+
// Hovered: store the new duration as the pending remainder WITHOUT
|
|
488
|
+
// arming a live timer (which would dismiss the toast while the pointer
|
|
489
|
+
// is still over the stack). resumeTimers() arms it on leave.
|
|
490
|
+
if (patched && patched.duration > 0 && typeof window !== 'undefined') {
|
|
491
|
+
timers[id] = {
|
|
492
|
+
handle: null,
|
|
493
|
+
startedAt: Date.now(),
|
|
494
|
+
remaining: patched.duration
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
startTimer(patched);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// The settle guard: a no-op if the host unmounted OR the toast was already
|
|
505
|
+
// dismissed while the promise was still pending (never-resurrect).
|
|
506
|
+
function settlePromise(id: any, type: any, messageOrFn: any, value: any) {
|
|
507
|
+
if (unmounted) return;
|
|
508
|
+
// Never-resurrect: no-op if the toast is gone OR already exiting (its
|
|
509
|
+
// dismissal is in flight — settling now would flip it back to a live
|
|
510
|
+
// success/error toast and re-arm a timer).
|
|
511
|
+
const entry = toasts().find((t: any) => t.id === id);
|
|
512
|
+
if (!entry || entry.exiting) return;
|
|
513
|
+
const message = typeof messageOrFn === 'function' ? messageOrFn(value) : messageOrFn;
|
|
514
|
+
patch(id, {
|
|
515
|
+
type,
|
|
516
|
+
message,
|
|
517
|
+
duration: local.duration
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Sugar over show()+patch(): shows a sticky loading toast synchronously
|
|
522
|
+
// (returns its id immediately — the consumer already holds `p`), then patches
|
|
523
|
+
// the SAME entry to success/error on settle (the auto-dismiss timer starts AT
|
|
524
|
+
// SETTLE, via patch's duration-key restart). Never returns/derives a new
|
|
525
|
+
// promise — `p`'s own .then/.catch still fire for the consumer untouched.
|
|
526
|
+
function promise(p: any, opts: any) {
|
|
527
|
+
const o = opts || {};
|
|
528
|
+
const id = show({
|
|
529
|
+
type: 'loading',
|
|
530
|
+
duration: 0,
|
|
531
|
+
message: o.loading
|
|
532
|
+
});
|
|
533
|
+
if (p && typeof p.then === 'function') {
|
|
534
|
+
p.then((value: any) => settlePromise(id, 'success', o.success, value)).catch((err: any) => settlePromise(id, 'error', o.error, err));
|
|
535
|
+
}
|
|
536
|
+
return id;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ---- swipe-to-dismiss ------------------------------------------------------
|
|
540
|
+
// Axis + dismiss-direction sign, purely derived from the corner (no per-
|
|
541
|
+
// gesture state needed for these two — they only depend on $props.position).
|
|
542
|
+
function swipeAxisFor(position: any) {
|
|
543
|
+
return position === 'top-center' || position === 'bottom-center' ? 'y' : 'x';
|
|
544
|
+
}
|
|
545
|
+
function swipeSignFor(position: any) {
|
|
546
|
+
if (position === 'top-right' || position === 'bottom-right') return 1;
|
|
547
|
+
if (position === 'top-left' || position === 'bottom-left') return -1;
|
|
548
|
+
if (position === 'bottom-center') return 1;
|
|
549
|
+
return -1; // top-center
|
|
550
|
+
}
|
|
551
|
+
function onToastPointerDown(t: any, event: any) {
|
|
552
|
+
if (local.disableSwipe) return;
|
|
553
|
+
if (event.button != null && event.button !== 0) return;
|
|
554
|
+
// Ignore drags starting on the close button / any button-or-link chrome.
|
|
555
|
+
const chrome = event.target && event.target.closest ? event.target.closest('button, a') : null;
|
|
556
|
+
if (chrome) return;
|
|
557
|
+
const axis = swipeAxisFor(local.position);
|
|
558
|
+
const sign = swipeSignFor(local.position);
|
|
559
|
+
const el = event.currentTarget;
|
|
560
|
+
const size = axis === 'x' ? el.offsetWidth : el.offsetHeight;
|
|
561
|
+
setSwipeGesture({
|
|
562
|
+
id: t.id,
|
|
563
|
+
axis,
|
|
564
|
+
sign,
|
|
565
|
+
size,
|
|
566
|
+
startX: event.clientX,
|
|
567
|
+
startY: event.clientY,
|
|
568
|
+
startTime: Date.now()
|
|
569
|
+
});
|
|
570
|
+
if (el && el.setPointerCapture) {
|
|
571
|
+
try {
|
|
572
|
+
el.setPointerCapture(event.pointerId);
|
|
573
|
+
} catch (e: any) {
|
|
574
|
+
// Some embedded contexts throw on setPointerCapture — swipe still
|
|
575
|
+
// works without capture (just loses "keeps tracking off-element").
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function onToastPointerMove(t: any, event: any) {
|
|
580
|
+
if (local.disableSwipe) return;
|
|
581
|
+
const gesture = swipeGesture();
|
|
582
|
+
if (!gesture || gesture.id !== t.id) return;
|
|
583
|
+
const raw = gesture.axis === 'x' ? event.clientX - gesture.startX : event.clientY - gesture.startY;
|
|
584
|
+
const towardDismiss = raw * gesture.sign > 0;
|
|
585
|
+
const d = towardDismiss ? raw : raw * 0.15;
|
|
586
|
+
setSwipe({
|
|
587
|
+
id: t.id,
|
|
588
|
+
d,
|
|
589
|
+
axis: gesture.axis,
|
|
590
|
+
sign: gesture.sign,
|
|
591
|
+
size: gesture.size
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
function onToastPointerUp(t: any, event: any) {
|
|
595
|
+
if (local.disableSwipe) return;
|
|
596
|
+
const gesture = swipeGesture();
|
|
597
|
+
setSwipeGesture(null);
|
|
598
|
+
// Local named `dragState`, NOT `swipe` — a local `swipe` would shadow the
|
|
599
|
+
// reactive `$data.swipe` key on Svelte 5 (top-level `let swipe = $state(…)`
|
|
600
|
+
// self-shadow TDZ: `const swipe = swipe` then `swipe = null` throws
|
|
601
|
+
// "Cannot assign to constant"). Same collision class as the documented
|
|
602
|
+
// $refs/$props self-shadow, just for a $data key.
|
|
603
|
+
const dragState = swipe();
|
|
604
|
+
setSwipe(null);
|
|
605
|
+
if (!gesture || gesture.id !== t.id || !dragState) return;
|
|
606
|
+
const elapsed = Math.max(1, Date.now() - gesture.startTime);
|
|
607
|
+
const magnitude = dragState.d * gesture.sign;
|
|
608
|
+
const velocity = magnitude / elapsed;
|
|
609
|
+
if (magnitude > 0 && (magnitude > gesture.size * 0.45 || velocity > 0.11)) {
|
|
610
|
+
dismissBegin(t.id, 'swipe', {
|
|
611
|
+
swipeExitSign: gesture.sign
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function onToastPointerCancel(t: any) {
|
|
616
|
+
if (local.disableSwipe) return;
|
|
617
|
+
if (swipeGesture() && swipeGesture().id === t.id) setSwipeGesture(null);
|
|
618
|
+
if (swipe() && swipe().id === t.id) setSwipe(null);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ---- stacked mode ----------------------------------------------------------
|
|
622
|
+
// Depth from newest: the newest toast (last in the array — show() appends)
|
|
623
|
+
// is depth 0; each older toast is one deeper. Corner-independent — the
|
|
624
|
+
// collapsed grid overlay ignores flex-direction/column-reverse entirely, so
|
|
625
|
+
// this needs no position-aware math.
|
|
626
|
+
function depth(t: any) {
|
|
627
|
+
const idx = toasts().findIndex((x: any) => x.id === t.id);
|
|
628
|
+
return idx === -1 ? 0 : toasts().length - 1 - idx;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// String-form `:style` for the toast row. ALWAYS carries `--rozie-toast-depth`
|
|
632
|
+
// (a no-op unless `stacked` is on — CSS reads it only inside
|
|
633
|
+
// `.rozie-toaster--stacked`), plus EITHER the active drag transform (while
|
|
634
|
+
// $data.swipe tracks this id) OR the swipe-exit sign custom property (once
|
|
635
|
+
// `dismissBegin('swipe')` flipped `t.swipeExitSign`). Drag/exit never overlap.
|
|
636
|
+
function toastStyle(t: any) {
|
|
637
|
+
const depthDecl = '--rozie-toast-depth: ' + depth(t) + ';';
|
|
638
|
+
if (t.exiting) {
|
|
639
|
+
return t.swipeExitSign != null ? depthDecl + ' --rozie-toast-swipe-exit: ' + t.swipeExitSign + ';' : depthDecl;
|
|
640
|
+
}
|
|
641
|
+
// Local named `dragState`, NOT `swipe` — see the onToastPointerUp comment
|
|
642
|
+
// above (Svelte 5 $data-key self-shadow).
|
|
643
|
+
const dragState = swipe();
|
|
644
|
+
if (!dragState || dragState.id !== t.id) return depthDecl;
|
|
645
|
+
const translate = dragState.axis === 'x' ? 'translateX(' + dragState.d + 'px)' : 'translateY(' + dragState.d + 'px)';
|
|
646
|
+
const magnitude = dragState.d * dragState.sign;
|
|
647
|
+
const opacity = magnitude > 0 && dragState.size > 0 ? Math.max(0.3, 1 - magnitude / dragState.size) : 1;
|
|
648
|
+
return depthDecl + ' transform: ' + translate + '; opacity: ' + opacity + '; transition: none;';
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// ---- hover pause -------------------------------------------------------
|
|
652
|
+
function onMouseEnter() {
|
|
653
|
+
if (local.disablePauseOnHover) return;
|
|
654
|
+
pauseTimers();
|
|
655
|
+
}
|
|
656
|
+
function onMouseLeave() {
|
|
657
|
+
if (local.disablePauseOnHover) return;
|
|
658
|
+
resumeTimers();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ---- helpers -----------------------------------------------------------
|
|
662
|
+
function regionLabel() {
|
|
663
|
+
return local.ariaLabel != null ? local.ariaLabel : 'Notifications';
|
|
664
|
+
}
|
|
665
|
+
// Type union: 'info' | 'success' | 'error' | 'warning' | 'loading'. Only
|
|
666
|
+
// error/warning interrupt (assertive); loading (like info/success) is polite.
|
|
667
|
+
function liveFor(type: any) {
|
|
668
|
+
return type === 'error' || type === 'warning' ? 'assertive' : 'polite';
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ---- lifecycle + handle ------------------------------------------------
|
|
672
|
+
|
|
673
|
+
return (
|
|
674
|
+
<>
|
|
675
|
+
<div role="region" aria-label={rozieAttr(regionLabel())} {...attrs} class={"rozie-toaster" + " " + rozieClass('rozie-toaster--' + local.position + (local.stacked ? ' rozie-toaster--stacked' : '')) + (((attrs as unknown as Record<string, unknown>).class as string | undefined) ? " " + ((attrs as unknown as Record<string, unknown>).class as string | undefined) : "")} {...mergeListeners({ onMouseEnter: ($event: MouseEvent & { currentTarget: HTMLDivElement; target: Element }) => { onMouseEnter(); }, onMouseLeave: ($event: MouseEvent & { currentTarget: HTMLDivElement; target: Element }) => { onMouseLeave(); } }, attrs)} data-rozie-s-12d4265c="">
|
|
676
|
+
|
|
677
|
+
<Key each={toasts() as readonly any[]} by={(t) => t.id}>{(t) => <div role="status" aria-live={rozieAttr(liveFor(t().type))} class={"rozie-toast" + " " + rozieClass('rozie-toast--' + t().type + (t().exiting ? ' rozie-toast--exiting' : '') + (t().swipeExitSign != null ? ' rozie-toast--swipe-exit' : ''))} style={parseInlineStyle(toastStyle(t()))} onAnimationEnd={($event: AnimationEvent & { currentTarget: HTMLDivElement; target: Element }) => { t().exiting && removeToast(t().id); }} onPointerDown={($event: PointerEvent & { currentTarget: HTMLDivElement; target: Element }) => { onToastPointerDown(t(), $event); }} onPointerMove={($event: PointerEvent & { currentTarget: HTMLDivElement; target: Element }) => { onToastPointerMove(t(), $event); }} onPointerUp={($event: PointerEvent & { currentTarget: HTMLDivElement; target: Element }) => { onToastPointerUp(t(), $event); }} onPointerCancel={($event: PointerEvent & { currentTarget: HTMLDivElement; target: Element }) => { onToastPointerCancel(t()); }} data-rozie-s-12d4265c="">
|
|
678
|
+
{(_props.toastSlot ?? _props.slots?.['toast'])?.({ toast: t(), dismiss }) ?? <>{<Show when={t().type === 'loading'}><span class={"rozie-toast-spinner"} aria-hidden="true" data-rozie-s-12d4265c="" /></Show>}<span class={"rozie-toast-message"} data-rozie-s-12d4265c="">{rozieDisplay(t().message)}</span><button type="button" aria-label="Dismiss" class={"rozie-toast-close"} onClick={($event: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }) => { dismissBegin(t().id, 'close'); }} data-rozie-s-12d4265c="">×</button></>}
|
|
679
|
+
</div>}</Key>
|
|
680
|
+
</div>
|
|
681
|
+
</>
|
|
682
|
+
);
|
|
683
|
+
}
|