@starwind-ui/core 1.12.4 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +16 -12
- package/dist/index.js.map +1 -1
- package/dist/src/components/accordion/Accordion.astro +10 -3
- package/dist/src/components/alert/AlertTitle.astro +1 -1
- package/dist/src/components/alert-dialog/AlertDialog.astro +4 -2
- package/dist/src/components/alert-dialog/AlertDialogTitle.astro +1 -1
- package/dist/src/components/badge/Badge.astro +1 -3
- package/dist/src/components/button/Button.astro +2 -1
- package/dist/src/components/dialog/Dialog.astro +101 -9
- package/dist/src/components/dialog/DialogContent.astro +13 -2
- package/dist/src/components/dialog/DialogTitle.astro +3 -1
- package/dist/src/components/dropdown/Dropdown.astro +4 -2
- package/dist/src/components/dropzone/Dropzone.astro +5 -3
- package/dist/src/components/image/Image.astro +24 -0
- package/dist/src/components/image/index.ts +9 -0
- package/dist/src/components/progress/Progress.astro +1 -0
- package/dist/src/components/radio-group/RadioGroup.astro +7 -2
- package/dist/src/components/select/Select.astro +4 -2
- package/dist/src/components/sheet/SheetTitle.astro +1 -1
- package/dist/src/components/slider/Slider.astro +411 -0
- package/dist/src/components/slider/index.ts +9 -0
- package/dist/src/components/switch/Switch.astro +1 -0
- package/dist/src/components/tabs/Tabs.astro +4 -2
- package/dist/src/components/toast/ToastDescription.astro +21 -0
- package/dist/src/components/toast/ToastItem.astro +54 -0
- package/dist/src/components/toast/ToastTemplate.astro +25 -0
- package/dist/src/components/toast/ToastTitle.astro +57 -0
- package/dist/src/components/toast/Toaster.astro +982 -0
- package/dist/src/components/toast/index.ts +29 -0
- package/dist/src/components/toast/toast-manager.ts +216 -0
- package/dist/src/components/toggle/Toggle.astro +4 -2
- package/dist/src/components/tooltip/Tooltip.astro +4 -2
- package/dist/src/components/tooltip/TooltipContent.astro +1 -1
- package/dist/src/components/video/Video.astro +120 -0
- package/dist/src/components/video/index.ts +9 -0
- package/package.json +5 -3
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from "astro/types";
|
|
3
|
+
import { tv } from "tailwind-variants";
|
|
4
|
+
|
|
5
|
+
import ToastTemplate from "./ToastTemplate.astro";
|
|
6
|
+
|
|
7
|
+
export const toastViewport = tv({
|
|
8
|
+
base: [
|
|
9
|
+
"starwind-toast-viewport fixed z-50 flex w-80 outline-none",
|
|
10
|
+
"data-[position=bottom-center]:bottom-4 data-[position=bottom-center]:left-1/2 data-[position=bottom-center]:-translate-x-1/2",
|
|
11
|
+
"data-[position=bottom-left]:bottom-4 data-[position=bottom-left]:left-4",
|
|
12
|
+
"data-[position=bottom-right]:right-4 data-[position=bottom-right]:bottom-4",
|
|
13
|
+
"data-[position=top-center]:top-4 data-[position=top-center]:left-1/2 data-[position=top-center]:-translate-x-1/2",
|
|
14
|
+
"data-[position=top-left]:top-4 data-[position=top-left]:left-4",
|
|
15
|
+
"data-[position=top-right]:top-4 data-[position=top-right]:right-4",
|
|
16
|
+
],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
type Position =
|
|
20
|
+
| "top-left"
|
|
21
|
+
| "top-center"
|
|
22
|
+
| "top-right"
|
|
23
|
+
| "bottom-left"
|
|
24
|
+
| "bottom-center"
|
|
25
|
+
| "bottom-right";
|
|
26
|
+
|
|
27
|
+
type Props = HTMLAttributes<"div"> & {
|
|
28
|
+
position?: Position;
|
|
29
|
+
limit?: number;
|
|
30
|
+
gap?: string;
|
|
31
|
+
peek?: string;
|
|
32
|
+
duration?: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const {
|
|
36
|
+
class: className,
|
|
37
|
+
position = "bottom-right",
|
|
38
|
+
gap = "0.5rem",
|
|
39
|
+
peek = "1rem",
|
|
40
|
+
limit = 3,
|
|
41
|
+
duration = 5000,
|
|
42
|
+
...rest
|
|
43
|
+
} = Astro.props;
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
<div
|
|
47
|
+
class={toastViewport({ class: className })}
|
|
48
|
+
data-slot="toast-viewport"
|
|
49
|
+
data-position={position}
|
|
50
|
+
data-limit={limit}
|
|
51
|
+
data-duration={duration}
|
|
52
|
+
role="region"
|
|
53
|
+
aria-live="polite"
|
|
54
|
+
aria-atomic="false"
|
|
55
|
+
aria-relevant="additions text"
|
|
56
|
+
aria-label="Notifications"
|
|
57
|
+
tabindex={-1}
|
|
58
|
+
style={`--gap: ${gap}; --peek: ${peek};`}
|
|
59
|
+
{...rest}
|
|
60
|
+
>
|
|
61
|
+
<!-- Hidden templates for each variant - JS will clone these -->
|
|
62
|
+
<slot>
|
|
63
|
+
<ToastTemplate variant="default" />
|
|
64
|
+
<ToastTemplate variant="success" />
|
|
65
|
+
<ToastTemplate variant="error" />
|
|
66
|
+
<ToastTemplate variant="warning" />
|
|
67
|
+
<ToastTemplate variant="info" />
|
|
68
|
+
<ToastTemplate variant="loading" />
|
|
69
|
+
</slot>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<style is:global>
|
|
73
|
+
.starwind-toast-viewport [data-slot="toast"] {
|
|
74
|
+
--scale: max(0.9, 1 - (var(--toast-index, 0) * 0.05));
|
|
75
|
+
|
|
76
|
+
z-index: calc(1000 - var(--toast-index, 0));
|
|
77
|
+
transform: translateX(var(--toast-swipe-movement-x, 0px))
|
|
78
|
+
translateY(calc(var(--toast-swipe-movement-y, 0px) - (var(--toast-index, 0) * var(--peek))))
|
|
79
|
+
scale(var(--scale));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.starwind-toast-viewport [data-slot="toast"][data-swiping] {
|
|
83
|
+
transition: none;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.starwind-toast-viewport [data-slot="toast"][data-expanded] {
|
|
87
|
+
--scale: 1;
|
|
88
|
+
|
|
89
|
+
transform: translateX(var(--toast-swipe-movement-x, 0px))
|
|
90
|
+
translateY(calc(var(--toast-swipe-movement-y, 0px) - var(--toast-offset-y, 0px)));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Exit animations by swipe direction */
|
|
94
|
+
.starwind-toast-viewport [data-slot="toast"][data-state="closed"][data-swipe-direction="down"] {
|
|
95
|
+
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) + 150%));
|
|
96
|
+
opacity: 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.starwind-toast-viewport [data-slot="toast"][data-state="closed"][data-swipe-direction="up"] {
|
|
100
|
+
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) - 150%));
|
|
101
|
+
opacity: 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.starwind-toast-viewport [data-slot="toast"][data-state="closed"][data-swipe-direction="right"] {
|
|
105
|
+
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) + 150%))
|
|
106
|
+
translateY(calc(var(--toast-offset-y, 0px) * -1));
|
|
107
|
+
opacity: 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.starwind-toast-viewport [data-slot="toast"][data-state="closed"][data-swipe-direction="left"] {
|
|
111
|
+
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) - 150%))
|
|
112
|
+
translateY(calc(var(--toast-offset-y, 0px) * -1));
|
|
113
|
+
opacity: 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Position-specific overrides for top positions */
|
|
117
|
+
.starwind-toast-viewport[data-position^="top"] [data-slot="toast"] {
|
|
118
|
+
top: 0;
|
|
119
|
+
bottom: auto;
|
|
120
|
+
transform-origin: top center;
|
|
121
|
+
transform: translateX(var(--toast-swipe-movement-x, 0px))
|
|
122
|
+
translateY(calc(var(--toast-swipe-movement-y, 0px) + (var(--toast-index, 0) * var(--peek))))
|
|
123
|
+
scale(var(--scale));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.starwind-toast-viewport[data-position^="top"] [data-slot="toast"][data-expanded] {
|
|
127
|
+
transform: translateX(var(--toast-swipe-movement-x, 0px))
|
|
128
|
+
translateY(calc(var(--toast-swipe-movement-y, 0px) + var(--toast-offset-y, 0px)));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.starwind-toast-viewport[data-position^="top"]
|
|
132
|
+
[data-slot="toast"][data-state="closed"][data-swipe-direction="up"] {
|
|
133
|
+
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) - 150%));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.starwind-toast-viewport[data-position^="top"]
|
|
137
|
+
[data-slot="toast"][data-state="closed"][data-swipe-direction="down"] {
|
|
138
|
+
transform: translateY(calc(var(--toast-swipe-movement-y, 0px) + 150%));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.starwind-toast-viewport[data-position^="top"]
|
|
142
|
+
[data-slot="toast"][data-state="closed"][data-swipe-direction="left"] {
|
|
143
|
+
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) - 150%))
|
|
144
|
+
translateY(var(--toast-offset-y, 0px));
|
|
145
|
+
opacity: 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.starwind-toast-viewport[data-position^="top"]
|
|
149
|
+
[data-slot="toast"][data-state="closed"][data-swipe-direction="right"] {
|
|
150
|
+
transform: translateX(calc(var(--toast-swipe-movement-x, 0px) + 150%))
|
|
151
|
+
translateY(var(--toast-offset-y, 0px));
|
|
152
|
+
opacity: 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* Entry animation - starting state (must come after base transforms) */
|
|
156
|
+
.starwind-toast-viewport [data-slot="toast"][data-starting-style] {
|
|
157
|
+
transform: translateY(150%);
|
|
158
|
+
opacity: 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* Default exit animation - slide down (for non-swipe closes) */
|
|
162
|
+
.starwind-toast-viewport [data-slot="toast"][data-state="closed"]:not([data-swipe-direction]) {
|
|
163
|
+
transform: translateY(150%);
|
|
164
|
+
opacity: 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Top position entry - slide from top */
|
|
168
|
+
.starwind-toast-viewport[data-position^="top"] [data-slot="toast"][data-starting-style] {
|
|
169
|
+
transform: translateY(-150%);
|
|
170
|
+
opacity: 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* Top position default exit - slide up */
|
|
174
|
+
.starwind-toast-viewport[data-position^="top"]
|
|
175
|
+
[data-slot="toast"][data-state="closed"]:not([data-swipe-direction]) {
|
|
176
|
+
transform: translateY(-150%);
|
|
177
|
+
opacity: 0;
|
|
178
|
+
}
|
|
179
|
+
</style>
|
|
180
|
+
|
|
181
|
+
<script>
|
|
182
|
+
type Variant = "default" | "success" | "error" | "warning" | "info" | "loading";
|
|
183
|
+
|
|
184
|
+
interface ToastOptions {
|
|
185
|
+
id?: string;
|
|
186
|
+
title?: string;
|
|
187
|
+
description?: string;
|
|
188
|
+
variant?: Variant;
|
|
189
|
+
duration?: number;
|
|
190
|
+
action?: {
|
|
191
|
+
label: string;
|
|
192
|
+
onClick: () => void;
|
|
193
|
+
};
|
|
194
|
+
onClose?: () => void;
|
|
195
|
+
onRemove?: () => void;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
interface ToastState extends ToastOptions {
|
|
199
|
+
id: string;
|
|
200
|
+
element?: HTMLElement;
|
|
201
|
+
timeoutId?: number;
|
|
202
|
+
height?: number;
|
|
203
|
+
closing?: boolean;
|
|
204
|
+
// Swipe state
|
|
205
|
+
swiping?: boolean;
|
|
206
|
+
swipeX?: number;
|
|
207
|
+
swipeY?: number;
|
|
208
|
+
swipeDirection?: "up" | "down" | "left" | "right";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Swipe constants
|
|
212
|
+
const SWIPE_THRESHOLD = 40;
|
|
213
|
+
const DAMPING_FACTOR = 0.1;
|
|
214
|
+
const LONG_PRESS_DURATION = 300;
|
|
215
|
+
|
|
216
|
+
type SwipeDirection = "up" | "down" | "left" | "right";
|
|
217
|
+
|
|
218
|
+
class StarwindToastManager {
|
|
219
|
+
readonly viewport: HTMLElement;
|
|
220
|
+
private toasts: ToastState[] = [];
|
|
221
|
+
private queue: ToastState[] = [];
|
|
222
|
+
private limit: number;
|
|
223
|
+
private defaultDuration: number;
|
|
224
|
+
private expanded: boolean = false;
|
|
225
|
+
private expandedByTouch: boolean = false;
|
|
226
|
+
private counter: number = 0;
|
|
227
|
+
private swipeDirections: SwipeDirection[] = ["down", "right"];
|
|
228
|
+
private destroyed: boolean = false;
|
|
229
|
+
|
|
230
|
+
// Swipe tracking
|
|
231
|
+
private activeSwipe: {
|
|
232
|
+
toast: ToastState;
|
|
233
|
+
startX: number;
|
|
234
|
+
startY: number;
|
|
235
|
+
startTime: number;
|
|
236
|
+
didMove: boolean;
|
|
237
|
+
lockedDirection: "horizontal" | "vertical" | null;
|
|
238
|
+
} | null = null;
|
|
239
|
+
|
|
240
|
+
// Bound event handlers for cleanup
|
|
241
|
+
private boundHandlers: {
|
|
242
|
+
mouseenter: () => void;
|
|
243
|
+
mouseleave: (e: MouseEvent) => void;
|
|
244
|
+
focusin: () => void;
|
|
245
|
+
focusout: (e: FocusEvent) => void;
|
|
246
|
+
documentMousemove: (e: MouseEvent) => void;
|
|
247
|
+
documentPointerdown: (e: PointerEvent) => void;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
constructor(viewport: HTMLElement) {
|
|
251
|
+
this.viewport = viewport;
|
|
252
|
+
this.limit = Number(viewport.dataset.limit) || 3;
|
|
253
|
+
this.defaultDuration = Number(viewport.dataset.duration) || 5000;
|
|
254
|
+
|
|
255
|
+
// Determine swipe directions based on position
|
|
256
|
+
const position = viewport.dataset.position || "bottom-right";
|
|
257
|
+
if (position.startsWith("top")) {
|
|
258
|
+
this.swipeDirections = ["up", "right"];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Initialize bound handlers
|
|
262
|
+
this.boundHandlers = {
|
|
263
|
+
mouseenter: this.handleMouseenter.bind(this),
|
|
264
|
+
mouseleave: this.handleMouseleave.bind(this),
|
|
265
|
+
focusin: this.handleFocusin.bind(this),
|
|
266
|
+
focusout: this.handleFocusout.bind(this),
|
|
267
|
+
documentMousemove: this.handleDocumentMousemove.bind(this),
|
|
268
|
+
documentPointerdown: this.handleDocumentPointerdown.bind(this),
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
this.setupEvents();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private handleMouseenter(): void {
|
|
275
|
+
this.expanded = true;
|
|
276
|
+
this.updateExpanded();
|
|
277
|
+
this.pauseAllTimers();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private handleMouseleave(e: MouseEvent): void {
|
|
281
|
+
// Don't collapse if expanded by touch - let the document pointerdown handle it
|
|
282
|
+
if (this.expandedByTouch) return;
|
|
283
|
+
|
|
284
|
+
// Check if mouse is still within viewport bounds (handles case where toast is removed)
|
|
285
|
+
const rect = this.viewport.getBoundingClientRect();
|
|
286
|
+
const isStillInside =
|
|
287
|
+
e.clientX >= rect.left &&
|
|
288
|
+
e.clientX <= rect.right &&
|
|
289
|
+
e.clientY >= rect.top &&
|
|
290
|
+
e.clientY <= rect.bottom;
|
|
291
|
+
|
|
292
|
+
if (!isStillInside) {
|
|
293
|
+
this.expanded = false;
|
|
294
|
+
this.updateExpanded();
|
|
295
|
+
this.resumeAllTimers();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private handleFocusin(): void {
|
|
300
|
+
this.expanded = true;
|
|
301
|
+
this.updateExpanded();
|
|
302
|
+
this.pauseAllTimers();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private handleFocusout(e: FocusEvent): void {
|
|
306
|
+
// Don't collapse if expanded by touch - let the document pointerdown handle it
|
|
307
|
+
if (this.expandedByTouch) return;
|
|
308
|
+
|
|
309
|
+
// Check if mouse is still over viewport before collapsing
|
|
310
|
+
// This handles the case where focus leaves because a toast was closed
|
|
311
|
+
const rect = this.viewport.getBoundingClientRect();
|
|
312
|
+
const mouseEvent = (window as any).__lastMousePosition;
|
|
313
|
+
const isMouseInside =
|
|
314
|
+
mouseEvent &&
|
|
315
|
+
mouseEvent.x >= rect.left &&
|
|
316
|
+
mouseEvent.x <= rect.right &&
|
|
317
|
+
mouseEvent.y >= rect.top &&
|
|
318
|
+
mouseEvent.y <= rect.bottom;
|
|
319
|
+
|
|
320
|
+
if (!this.viewport.contains(e.relatedTarget as Node) && !isMouseInside) {
|
|
321
|
+
this.expanded = false;
|
|
322
|
+
this.updateExpanded();
|
|
323
|
+
this.resumeAllTimers();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private handleDocumentMousemove(e: MouseEvent): void {
|
|
328
|
+
(window as any).__lastMousePosition = { x: e.clientX, y: e.clientY };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private handleDocumentPointerdown(e: PointerEvent): void {
|
|
332
|
+
if (!this.expandedByTouch || !this.expanded) return;
|
|
333
|
+
|
|
334
|
+
const target = e.target as HTMLElement;
|
|
335
|
+
if (!this.viewport.contains(target)) {
|
|
336
|
+
this.expandedByTouch = false;
|
|
337
|
+
this.expanded = false;
|
|
338
|
+
this.updateExpanded();
|
|
339
|
+
this.resumeAllTimers();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private setupEvents(): void {
|
|
344
|
+
this.viewport.addEventListener("mouseenter", this.boundHandlers.mouseenter);
|
|
345
|
+
this.viewport.addEventListener("mouseleave", this.boundHandlers.mouseleave);
|
|
346
|
+
this.viewport.addEventListener("focusin", this.boundHandlers.focusin);
|
|
347
|
+
this.viewport.addEventListener("focusout", this.boundHandlers.focusout);
|
|
348
|
+
document.addEventListener("mousemove", this.boundHandlers.documentMousemove);
|
|
349
|
+
document.addEventListener("pointerdown", this.boundHandlers.documentPointerdown);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private removeEvents(): void {
|
|
353
|
+
this.viewport.removeEventListener("mouseenter", this.boundHandlers.mouseenter);
|
|
354
|
+
this.viewport.removeEventListener("mouseleave", this.boundHandlers.mouseleave);
|
|
355
|
+
this.viewport.removeEventListener("focusin", this.boundHandlers.focusin);
|
|
356
|
+
this.viewport.removeEventListener("focusout", this.boundHandlers.focusout);
|
|
357
|
+
document.removeEventListener("mousemove", this.boundHandlers.documentMousemove);
|
|
358
|
+
document.removeEventListener("pointerdown", this.boundHandlers.documentPointerdown);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Destroy the manager and clean up all resources
|
|
363
|
+
*/
|
|
364
|
+
public destroy(): void {
|
|
365
|
+
if (this.destroyed) return;
|
|
366
|
+
this.destroyed = true;
|
|
367
|
+
|
|
368
|
+
// Clear all toasts and their timers
|
|
369
|
+
this.toasts.forEach((toast) => {
|
|
370
|
+
if (toast.timeoutId) {
|
|
371
|
+
clearTimeout(toast.timeoutId);
|
|
372
|
+
}
|
|
373
|
+
toast.element?.remove();
|
|
374
|
+
});
|
|
375
|
+
this.toasts = [];
|
|
376
|
+
this.queue = [];
|
|
377
|
+
|
|
378
|
+
// Remove all event listeners
|
|
379
|
+
this.removeEvents();
|
|
380
|
+
|
|
381
|
+
// Clear active swipe state
|
|
382
|
+
this.activeSwipe = null;
|
|
383
|
+
|
|
384
|
+
// Null out the scoped namespace entry
|
|
385
|
+
if ((window as any).__starwind__?.toast === this) {
|
|
386
|
+
(window as any).__starwind__.toast = null;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private updateExpanded(): void {
|
|
391
|
+
// Set expanded state on viewport
|
|
392
|
+
if (this.expanded) {
|
|
393
|
+
this.viewport.setAttribute("data-expanded", "");
|
|
394
|
+
// Calculate total height needed for expanded state
|
|
395
|
+
this.updateViewportHeight();
|
|
396
|
+
} else {
|
|
397
|
+
this.viewport.removeAttribute("data-expanded");
|
|
398
|
+
this.viewport.style.removeProperty("height");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
this.toasts.forEach((toast) => {
|
|
402
|
+
if (toast.element) {
|
|
403
|
+
const contentEl = toast.element.querySelector('[data-slot="toast-content"]');
|
|
404
|
+
if (this.expanded) {
|
|
405
|
+
toast.element.setAttribute("data-expanded", "");
|
|
406
|
+
contentEl?.setAttribute("data-expanded", "");
|
|
407
|
+
} else {
|
|
408
|
+
toast.element.removeAttribute("data-expanded");
|
|
409
|
+
contentEl?.removeAttribute("data-expanded");
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
this.updatePositions();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private updateViewportHeight(): void {
|
|
417
|
+
if (!this.expanded || this.toasts.length === 0) return;
|
|
418
|
+
|
|
419
|
+
const gapValue = this.getCssPixelValue("--gap");
|
|
420
|
+
const visibleToasts = this.toasts.slice(0, this.limit);
|
|
421
|
+
const totalHeight = visibleToasts.reduce((sum, toast) => sum + (toast.height || 0), 0);
|
|
422
|
+
const totalGaps = (visibleToasts.length - 1) * gapValue;
|
|
423
|
+
|
|
424
|
+
this.viewport.style.height = `${totalHeight + totalGaps}px`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private getCssPixelValue(property: string): number {
|
|
428
|
+
const value = getComputedStyle(this.viewport).getPropertyValue(property).trim();
|
|
429
|
+
if (!value) return 16;
|
|
430
|
+
|
|
431
|
+
// Create a temporary element to convert CSS value to pixels
|
|
432
|
+
const temp = document.createElement("div");
|
|
433
|
+
temp.style.position = "absolute";
|
|
434
|
+
temp.style.visibility = "hidden";
|
|
435
|
+
temp.style.width = value;
|
|
436
|
+
document.body.appendChild(temp);
|
|
437
|
+
const pixels = temp.offsetWidth;
|
|
438
|
+
document.body.removeChild(temp);
|
|
439
|
+
return pixels || 16;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private pauseAllTimers(): void {
|
|
443
|
+
this.toasts.forEach((toast) => {
|
|
444
|
+
if (toast.timeoutId) {
|
|
445
|
+
clearTimeout(toast.timeoutId);
|
|
446
|
+
toast.timeoutId = undefined;
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private resumeAllTimers(): void {
|
|
452
|
+
this.toasts.forEach((toast) => {
|
|
453
|
+
const duration = toast.duration ?? this.defaultDuration;
|
|
454
|
+
if (duration > 0) {
|
|
455
|
+
toast.timeoutId = window.setTimeout(() => {
|
|
456
|
+
this.close(toast.id);
|
|
457
|
+
}, duration);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Add a new toast
|
|
464
|
+
*/
|
|
465
|
+
public add(options: ToastOptions): string {
|
|
466
|
+
const id = options.id || `toast-${++this.counter}`;
|
|
467
|
+
const duration = options.duration ?? this.defaultDuration;
|
|
468
|
+
|
|
469
|
+
const toastState: ToastState = {
|
|
470
|
+
...options,
|
|
471
|
+
id,
|
|
472
|
+
duration,
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// Always add the new toast at the front (newest first)
|
|
476
|
+
this.renderToast(toastState);
|
|
477
|
+
this.toasts.unshift(toastState);
|
|
478
|
+
|
|
479
|
+
// updatePositions will handle hiding toasts beyond the limit via data-limited
|
|
480
|
+
// They'll become visible again when other toasts close
|
|
481
|
+
|
|
482
|
+
return id;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Update an existing toast
|
|
487
|
+
*/
|
|
488
|
+
public update(id: string, options: Partial<ToastOptions>): void {
|
|
489
|
+
const toastIndex = this.toasts.findIndex((t) => t.id === id);
|
|
490
|
+
if (toastIndex === -1) return;
|
|
491
|
+
|
|
492
|
+
const toast = this.toasts[toastIndex];
|
|
493
|
+
const oldVariant = toast.variant;
|
|
494
|
+
Object.assign(toast, options);
|
|
495
|
+
|
|
496
|
+
// If variant changed, we need to re-render the toast with new template
|
|
497
|
+
if (options.variant && options.variant !== oldVariant && toast.element) {
|
|
498
|
+
this.rerenderToast(toast);
|
|
499
|
+
} else if (toast.element) {
|
|
500
|
+
this.updateToastContent(toast);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Restart timer if duration changed or variant changed (new default duration)
|
|
504
|
+
if (toast.element && (options.duration !== undefined || options.variant)) {
|
|
505
|
+
if (toast.timeoutId) {
|
|
506
|
+
clearTimeout(toast.timeoutId);
|
|
507
|
+
toast.timeoutId = undefined;
|
|
508
|
+
}
|
|
509
|
+
const duration = toast.duration ?? this.defaultDuration;
|
|
510
|
+
if (duration > 0 && !this.expanded) {
|
|
511
|
+
toast.timeoutId = window.setTimeout(() => {
|
|
512
|
+
this.close(toast.id);
|
|
513
|
+
}, duration);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Close a toast
|
|
520
|
+
*/
|
|
521
|
+
public close(id: string): void {
|
|
522
|
+
const toast = this.toasts.find((t) => t.id === id);
|
|
523
|
+
if (!toast) return;
|
|
524
|
+
|
|
525
|
+
// Guard against closing a toast that's already closing
|
|
526
|
+
if (toast.closing) return;
|
|
527
|
+
toast.closing = true;
|
|
528
|
+
|
|
529
|
+
if (toast.timeoutId) {
|
|
530
|
+
clearTimeout(toast.timeoutId);
|
|
531
|
+
toast.timeoutId = undefined;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
toast.onClose?.();
|
|
535
|
+
|
|
536
|
+
if (toast.element) {
|
|
537
|
+
toast.element.setAttribute("data-state", "closed");
|
|
538
|
+
|
|
539
|
+
setTimeout(() => {
|
|
540
|
+
toast.element?.remove();
|
|
541
|
+
toast.onRemove?.();
|
|
542
|
+
|
|
543
|
+
// Find current index (may have changed since close was called)
|
|
544
|
+
const currentIndex = this.toasts.findIndex((t) => t.id === id);
|
|
545
|
+
if (currentIndex !== -1) {
|
|
546
|
+
this.toasts.splice(currentIndex, 1);
|
|
547
|
+
}
|
|
548
|
+
this.updatePositions();
|
|
549
|
+
}, 200);
|
|
550
|
+
} else {
|
|
551
|
+
// No element, just remove from array immediately
|
|
552
|
+
const index = this.toasts.findIndex((t) => t.id === id);
|
|
553
|
+
if (index !== -1) {
|
|
554
|
+
this.toasts.splice(index, 1);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Close all toasts
|
|
561
|
+
*/
|
|
562
|
+
public closeAll(): void {
|
|
563
|
+
[...this.toasts].forEach((toast) => this.close(toast.id));
|
|
564
|
+
this.queue = [];
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private renderToast(toast: ToastState): void {
|
|
568
|
+
const variant = toast.variant || "default";
|
|
569
|
+
const template = this.viewport.querySelector(
|
|
570
|
+
`template[data-toast-template="${variant}"]`,
|
|
571
|
+
) as HTMLTemplateElement;
|
|
572
|
+
|
|
573
|
+
if (!template) {
|
|
574
|
+
console.error(`Toast template for variant "${variant}" not found`);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const clone = template.content.cloneNode(true) as DocumentFragment;
|
|
579
|
+
const element = clone.firstElementChild as HTMLElement;
|
|
580
|
+
|
|
581
|
+
if (!element) return;
|
|
582
|
+
|
|
583
|
+
element.setAttribute("data-toast-id", toast.id);
|
|
584
|
+
element.setAttribute("data-starting-style", "");
|
|
585
|
+
|
|
586
|
+
// Update title
|
|
587
|
+
const titleEl = element.querySelector("[data-toast-title]");
|
|
588
|
+
const titleTextEl = element.querySelector("[data-toast-title-text]");
|
|
589
|
+
if (titleEl) {
|
|
590
|
+
if (toast.title) {
|
|
591
|
+
if (titleTextEl) titleTextEl.textContent = toast.title;
|
|
592
|
+
} else {
|
|
593
|
+
titleEl.remove();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Update description
|
|
598
|
+
const descEl = element.querySelector("[data-toast-description]");
|
|
599
|
+
if (descEl) {
|
|
600
|
+
if (toast.description) {
|
|
601
|
+
descEl.textContent = toast.description;
|
|
602
|
+
} else {
|
|
603
|
+
descEl.remove();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Setup close button
|
|
608
|
+
const closeBtn = element.querySelector('[data-slot="toast-close"]');
|
|
609
|
+
closeBtn?.addEventListener("click", () => this.close(toast.id));
|
|
610
|
+
|
|
611
|
+
// Setup swipe handlers
|
|
612
|
+
this.setupSwipeHandlers(element, toast);
|
|
613
|
+
|
|
614
|
+
// Initialize swipe CSS variables
|
|
615
|
+
element.style.setProperty("--toast-swipe-movement-x", "0px");
|
|
616
|
+
element.style.setProperty("--toast-swipe-movement-y", "0px");
|
|
617
|
+
|
|
618
|
+
// Insert at beginning (newest on top visually)
|
|
619
|
+
this.viewport.insertBefore(element, this.viewport.firstElementChild);
|
|
620
|
+
toast.element = element;
|
|
621
|
+
|
|
622
|
+
// Trigger entry animation
|
|
623
|
+
requestAnimationFrame(() => {
|
|
624
|
+
toast.height = element.offsetHeight;
|
|
625
|
+
this.updatePositions();
|
|
626
|
+
|
|
627
|
+
// Remove starting style after a frame to trigger animation
|
|
628
|
+
requestAnimationFrame(() => {
|
|
629
|
+
element.removeAttribute("data-starting-style");
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// Start auto-dismiss timer
|
|
634
|
+
const duration = toast.duration ?? this.defaultDuration;
|
|
635
|
+
if (duration > 0 && !this.expanded) {
|
|
636
|
+
toast.timeoutId = window.setTimeout(() => {
|
|
637
|
+
this.close(toast.id);
|
|
638
|
+
}, duration);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
private updateToastContent(toast: ToastState): void {
|
|
643
|
+
if (!toast.element) return;
|
|
644
|
+
|
|
645
|
+
const titleEl = toast.element.querySelector("[data-toast-title]");
|
|
646
|
+
const descEl = toast.element.querySelector("[data-toast-description]");
|
|
647
|
+
|
|
648
|
+
if (titleEl && toast.title) {
|
|
649
|
+
const titleTextEl = titleEl.querySelector("[data-toast-title-text]");
|
|
650
|
+
if (titleTextEl) titleTextEl.textContent = toast.title;
|
|
651
|
+
}
|
|
652
|
+
if (descEl && toast.description) {
|
|
653
|
+
descEl.textContent = toast.description;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Re-render a toast with a new template (for variant changes)
|
|
659
|
+
*/
|
|
660
|
+
private rerenderToast(toast: ToastState): void {
|
|
661
|
+
if (!toast.element) return;
|
|
662
|
+
|
|
663
|
+
const variant = toast.variant || "default";
|
|
664
|
+
const template = this.viewport.querySelector(
|
|
665
|
+
`template[data-toast-template="${variant}"]`,
|
|
666
|
+
) as HTMLTemplateElement;
|
|
667
|
+
|
|
668
|
+
if (!template) {
|
|
669
|
+
console.error(`Toast template for variant "${variant}" not found`);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const clone = template.content.cloneNode(true) as DocumentFragment;
|
|
674
|
+
const newElement = clone.firstElementChild as HTMLElement;
|
|
675
|
+
|
|
676
|
+
if (!newElement) return;
|
|
677
|
+
|
|
678
|
+
// Copy over attributes and state from old element
|
|
679
|
+
newElement.setAttribute("data-toast-id", toast.id);
|
|
680
|
+
newElement.setAttribute("data-state", "open");
|
|
681
|
+
if (this.expanded) {
|
|
682
|
+
newElement.setAttribute("data-expanded", "");
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Copy CSS variables
|
|
686
|
+
const oldStyle = toast.element.style;
|
|
687
|
+
newElement.style.setProperty("--toast-index", oldStyle.getPropertyValue("--toast-index"));
|
|
688
|
+
newElement.style.setProperty(
|
|
689
|
+
"--toast-offset-y",
|
|
690
|
+
oldStyle.getPropertyValue("--toast-offset-y"),
|
|
691
|
+
);
|
|
692
|
+
newElement.style.setProperty("--toast-swipe-movement-x", "0px");
|
|
693
|
+
newElement.style.setProperty("--toast-swipe-movement-y", "0px");
|
|
694
|
+
|
|
695
|
+
// Update title
|
|
696
|
+
const titleTextEl = newElement.querySelector("[data-toast-title-text]");
|
|
697
|
+
if (titleTextEl && toast.title) {
|
|
698
|
+
titleTextEl.textContent = toast.title;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Update description
|
|
702
|
+
const descEl = newElement.querySelector("[data-toast-description]");
|
|
703
|
+
if (descEl && toast.description) {
|
|
704
|
+
descEl.textContent = toast.description;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Setup close button
|
|
708
|
+
const closeBtn = newElement.querySelector('[data-slot="toast-close"]');
|
|
709
|
+
closeBtn?.addEventListener("click", () => this.close(toast.id));
|
|
710
|
+
|
|
711
|
+
// Setup swipe handlers
|
|
712
|
+
this.setupSwipeHandlers(newElement, toast);
|
|
713
|
+
|
|
714
|
+
// Replace old element with new one
|
|
715
|
+
toast.element.replaceWith(newElement);
|
|
716
|
+
toast.element = newElement;
|
|
717
|
+
|
|
718
|
+
// Update height
|
|
719
|
+
toast.height = newElement.offsetHeight;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
private updatePositions(): void {
|
|
723
|
+
let cumulativeOffset = 0;
|
|
724
|
+
|
|
725
|
+
// Iterate through toasts - index 0 is newest (front)
|
|
726
|
+
this.toasts.forEach((toast, index) => {
|
|
727
|
+
if (!toast.element) return;
|
|
728
|
+
|
|
729
|
+
const contentEl = toast.element.querySelector('[data-slot="toast-content"]');
|
|
730
|
+
|
|
731
|
+
// Set CSS variables for this toast
|
|
732
|
+
toast.element.style.setProperty("--toast-index", String(index));
|
|
733
|
+
toast.element.style.setProperty("--toast-offset-y", `${cumulativeOffset}px`);
|
|
734
|
+
|
|
735
|
+
// Set data-behind on content for toasts behind the frontmost
|
|
736
|
+
if (index > 0) {
|
|
737
|
+
contentEl?.setAttribute("data-behind", "");
|
|
738
|
+
} else {
|
|
739
|
+
contentEl?.removeAttribute("data-behind");
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Mark as limited if beyond limit
|
|
743
|
+
if (index >= this.limit) {
|
|
744
|
+
toast.element.setAttribute("data-limited", "");
|
|
745
|
+
} else {
|
|
746
|
+
toast.element.removeAttribute("data-limited");
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Accumulate offset for expanded state (height + gap)
|
|
750
|
+
const gapValue = this.getCssPixelValue("--gap");
|
|
751
|
+
cumulativeOffset += (toast.height || 0) + gapValue;
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Swipe handlers
|
|
756
|
+
private handlePointerDown(e: PointerEvent, toast: ToastState): void {
|
|
757
|
+
if (e.button !== 0) return;
|
|
758
|
+
|
|
759
|
+
const target = e.target as HTMLElement;
|
|
760
|
+
const isInteractive = target.closest(
|
|
761
|
+
'button, a, input, textarea, [role="button"], [data-swipe-ignore]',
|
|
762
|
+
);
|
|
763
|
+
if (isInteractive) return;
|
|
764
|
+
|
|
765
|
+
// Treat touch/pointer down as hover to expand stacked toasts
|
|
766
|
+
// Always mark as touch-expanded for long press detection
|
|
767
|
+
this.expandedByTouch = true;
|
|
768
|
+
if (!this.expanded) {
|
|
769
|
+
this.expanded = true;
|
|
770
|
+
this.updateExpanded();
|
|
771
|
+
this.pauseAllTimers();
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
this.activeSwipe = {
|
|
775
|
+
toast,
|
|
776
|
+
startX: e.clientX,
|
|
777
|
+
startY: e.clientY,
|
|
778
|
+
startTime: Date.now(),
|
|
779
|
+
didMove: false,
|
|
780
|
+
lockedDirection: null,
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
toast.swiping = true;
|
|
784
|
+
toast.swipeX = 0;
|
|
785
|
+
toast.swipeY = 0;
|
|
786
|
+
toast.element?.setAttribute("data-swiping", "");
|
|
787
|
+
toast.element?.setPointerCapture(e.pointerId);
|
|
788
|
+
|
|
789
|
+
// Pause timer while swiping
|
|
790
|
+
if (toast.timeoutId) {
|
|
791
|
+
clearTimeout(toast.timeoutId);
|
|
792
|
+
toast.timeoutId = undefined;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
private handlePointerMove(e: PointerEvent): void {
|
|
797
|
+
if (!this.activeSwipe) return;
|
|
798
|
+
|
|
799
|
+
e.preventDefault();
|
|
800
|
+
|
|
801
|
+
const { toast, startX, startY } = this.activeSwipe;
|
|
802
|
+
const deltaX = e.clientX - startX;
|
|
803
|
+
const deltaY = e.clientY - startY;
|
|
804
|
+
|
|
805
|
+
// Lock direction on first significant movement
|
|
806
|
+
if (!this.activeSwipe.lockedDirection) {
|
|
807
|
+
const absX = Math.abs(deltaX);
|
|
808
|
+
const absY = Math.abs(deltaY);
|
|
809
|
+
if (absX > 5 || absY > 5) {
|
|
810
|
+
this.activeSwipe.didMove = true;
|
|
811
|
+
const hasHorizontal =
|
|
812
|
+
this.swipeDirections.includes("left") || this.swipeDirections.includes("right");
|
|
813
|
+
const hasVertical =
|
|
814
|
+
this.swipeDirections.includes("up") || this.swipeDirections.includes("down");
|
|
815
|
+
if (hasHorizontal && hasVertical) {
|
|
816
|
+
this.activeSwipe.lockedDirection = absX > absY ? "horizontal" : "vertical";
|
|
817
|
+
} else if (hasHorizontal) {
|
|
818
|
+
this.activeSwipe.lockedDirection = "horizontal";
|
|
819
|
+
} else {
|
|
820
|
+
this.activeSwipe.lockedDirection = "vertical";
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Apply damping for non-allowed directions
|
|
826
|
+
let swipeX = 0;
|
|
827
|
+
let swipeY = 0;
|
|
828
|
+
|
|
829
|
+
if (this.activeSwipe.lockedDirection === "horizontal") {
|
|
830
|
+
if (this.swipeDirections.includes("right") && deltaX > 0) {
|
|
831
|
+
swipeX = deltaX;
|
|
832
|
+
} else if (this.swipeDirections.includes("left") && deltaX < 0) {
|
|
833
|
+
swipeX = deltaX;
|
|
834
|
+
} else {
|
|
835
|
+
swipeX = deltaX * DAMPING_FACTOR;
|
|
836
|
+
}
|
|
837
|
+
} else if (this.activeSwipe.lockedDirection === "vertical") {
|
|
838
|
+
if (this.swipeDirections.includes("down") && deltaY > 0) {
|
|
839
|
+
swipeY = deltaY;
|
|
840
|
+
} else if (this.swipeDirections.includes("up") && deltaY < 0) {
|
|
841
|
+
swipeY = deltaY;
|
|
842
|
+
} else {
|
|
843
|
+
swipeY = deltaY * DAMPING_FACTOR;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
toast.swipeX = swipeX;
|
|
848
|
+
toast.swipeY = swipeY;
|
|
849
|
+
toast.element?.style.setProperty("--toast-swipe-movement-x", `${swipeX}px`);
|
|
850
|
+
toast.element?.style.setProperty("--toast-swipe-movement-y", `${swipeY}px`);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
private handlePointerUp(e: PointerEvent): void {
|
|
854
|
+
if (!this.activeSwipe) return;
|
|
855
|
+
|
|
856
|
+
const { toast } = this.activeSwipe;
|
|
857
|
+
toast.element?.releasePointerCapture(e.pointerId);
|
|
858
|
+
toast.element?.removeAttribute("data-swiping");
|
|
859
|
+
toast.swiping = false;
|
|
860
|
+
|
|
861
|
+
const swipeX = toast.swipeX || 0;
|
|
862
|
+
const swipeY = toast.swipeY || 0;
|
|
863
|
+
|
|
864
|
+
// Check if swipe exceeds threshold based on locked direction
|
|
865
|
+
let shouldClose = false;
|
|
866
|
+
let direction: SwipeDirection | undefined;
|
|
867
|
+
const lockedDir = this.activeSwipe.lockedDirection;
|
|
868
|
+
|
|
869
|
+
if (lockedDir === "horizontal") {
|
|
870
|
+
if (swipeX > SWIPE_THRESHOLD && this.swipeDirections.includes("right")) {
|
|
871
|
+
shouldClose = true;
|
|
872
|
+
direction = "right";
|
|
873
|
+
} else if (swipeX < -SWIPE_THRESHOLD && this.swipeDirections.includes("left")) {
|
|
874
|
+
shouldClose = true;
|
|
875
|
+
direction = "left";
|
|
876
|
+
}
|
|
877
|
+
} else if (lockedDir === "vertical") {
|
|
878
|
+
if (swipeY > SWIPE_THRESHOLD && this.swipeDirections.includes("down")) {
|
|
879
|
+
shouldClose = true;
|
|
880
|
+
direction = "down";
|
|
881
|
+
} else if (swipeY < -SWIPE_THRESHOLD && this.swipeDirections.includes("up")) {
|
|
882
|
+
shouldClose = true;
|
|
883
|
+
direction = "up";
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Check if this was a long press or if user moved (swiped)
|
|
888
|
+
const pressDuration = Date.now() - this.activeSwipe.startTime;
|
|
889
|
+
const isLongPress = pressDuration >= LONG_PRESS_DURATION;
|
|
890
|
+
const didMove = this.activeSwipe.didMove;
|
|
891
|
+
|
|
892
|
+
// Check if pointer is outside viewport bounds
|
|
893
|
+
const rect = this.viewport.getBoundingClientRect();
|
|
894
|
+
const isOutsideViewport =
|
|
895
|
+
e.clientX < rect.left ||
|
|
896
|
+
e.clientX > rect.right ||
|
|
897
|
+
e.clientY < rect.top ||
|
|
898
|
+
e.clientY > rect.bottom;
|
|
899
|
+
|
|
900
|
+
if (shouldClose && direction) {
|
|
901
|
+
toast.swipeDirection = direction;
|
|
902
|
+
toast.element?.setAttribute("data-swipe-direction", direction);
|
|
903
|
+
this.close(toast.id);
|
|
904
|
+
} else {
|
|
905
|
+
// Reset position
|
|
906
|
+
toast.swipeX = 0;
|
|
907
|
+
toast.swipeY = 0;
|
|
908
|
+
toast.element?.style.setProperty("--toast-swipe-movement-x", "0px");
|
|
909
|
+
toast.element?.style.setProperty("--toast-swipe-movement-y", "0px");
|
|
910
|
+
|
|
911
|
+
// Collapse if: short tap without movement, OR pointer ended outside viewport
|
|
912
|
+
if ((!isLongPress && !didMove && this.expandedByTouch) || isOutsideViewport) {
|
|
913
|
+
this.expandedByTouch = false;
|
|
914
|
+
this.expanded = false;
|
|
915
|
+
this.updateExpanded();
|
|
916
|
+
this.resumeAllTimers();
|
|
917
|
+
} else {
|
|
918
|
+
// Resume timer for this toast only
|
|
919
|
+
const duration = toast.duration ?? this.defaultDuration;
|
|
920
|
+
if (duration > 0 && !this.expanded) {
|
|
921
|
+
toast.timeoutId = window.setTimeout(() => {
|
|
922
|
+
this.close(toast.id);
|
|
923
|
+
}, duration);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
this.activeSwipe = null;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
private setupSwipeHandlers(element: HTMLElement, toast: ToastState): void {
|
|
932
|
+
element.addEventListener("pointerdown", (e) => this.handlePointerDown(e, toast));
|
|
933
|
+
element.addEventListener("pointermove", (e) => this.handlePointerMove(e));
|
|
934
|
+
element.addEventListener("pointerup", (e) => this.handlePointerUp(e));
|
|
935
|
+
element.addEventListener("pointercancel", (e) => this.handlePointerUp(e));
|
|
936
|
+
|
|
937
|
+
// Prevent default touch behavior to avoid scroll interference
|
|
938
|
+
element.addEventListener(
|
|
939
|
+
"touchmove",
|
|
940
|
+
(e) => {
|
|
941
|
+
if (this.activeSwipe?.toast === toast) {
|
|
942
|
+
e.preventDefault();
|
|
943
|
+
}
|
|
944
|
+
},
|
|
945
|
+
{ passive: false },
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Initialize scoped namespace
|
|
951
|
+
(window as any).__starwind__ = (window as any).__starwind__ || {};
|
|
952
|
+
|
|
953
|
+
let currentManager: StarwindToastManager | null = null;
|
|
954
|
+
|
|
955
|
+
const setupToasts = () => {
|
|
956
|
+
// Find the viewport in the current DOM
|
|
957
|
+
const viewport = document.querySelector(".starwind-toast-viewport") as HTMLElement | null;
|
|
958
|
+
|
|
959
|
+
if (!viewport) {
|
|
960
|
+
// No viewport on this page, destroy existing manager and clear references
|
|
961
|
+
currentManager?.destroy();
|
|
962
|
+
currentManager = null;
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Check if we already have a manager for this exact element
|
|
967
|
+
if (currentManager && currentManager.viewport === viewport) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Destroy previous manager before creating new one (viewport differs)
|
|
972
|
+
currentManager?.destroy();
|
|
973
|
+
|
|
974
|
+
// Create new manager for the viewport
|
|
975
|
+
currentManager = new StarwindToastManager(viewport);
|
|
976
|
+
(window as any).__starwind__.toast = currentManager;
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
setupToasts();
|
|
980
|
+
document.addEventListener("astro:after-swap", setupToasts);
|
|
981
|
+
document.addEventListener("starwind:init", setupToasts);
|
|
982
|
+
</script>
|