@getmicdrop/svelte-components 5.21.2 → 5.22.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/components/Toast/ToastItem.svelte +695 -0
- package/dist/components/Toast/ToastItem.svelte.d.ts +8 -0
- package/dist/components/Toast/ToastItem.svelte.d.ts.map +1 -0
- package/dist/components/Toast/Toaster.svelte +184 -0
- package/dist/components/Toast/Toaster.svelte.d.ts +11 -0
- package/dist/components/Toast/Toaster.svelte.d.ts.map +1 -0
- package/dist/components/Toast/index.d.ts +9 -0
- package/dist/components/Toast/index.d.ts.map +1 -0
- package/dist/components/Toast/index.js +13 -0
- package/dist/components/Toast/toast.svelte.d.ts +93 -0
- package/dist/components/Toast/toast.svelte.d.ts.map +1 -0
- package/dist/components/Toast/toast.svelte.js +386 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +3 -0
- package/dist/primitives/Icons/CancelledIcon.svelte +8 -0
- package/dist/primitives/Icons/CancelledIcon.svelte.d.ts +16 -0
- package/dist/primitives/Icons/CancelledIcon.svelte.d.ts.map +1 -0
- package/dist/primitives/Icons/CartIcon.svelte +12 -0
- package/dist/primitives/Icons/CartIcon.svelte.d.ts +16 -0
- package/dist/primitives/Icons/CartIcon.svelte.d.ts.map +1 -0
- package/dist/primitives/Icons/ConfirmedIcon.svelte +8 -0
- package/dist/primitives/Icons/ConfirmedIcon.svelte.d.ts +16 -0
- package/dist/primitives/Icons/ConfirmedIcon.svelte.d.ts.map +1 -0
- package/dist/primitives/Icons/InvitedIcon.svelte +7 -0
- package/dist/primitives/Icons/InvitedIcon.svelte.d.ts +16 -0
- package/dist/primitives/Icons/InvitedIcon.svelte.d.ts.map +1 -0
- package/dist/primitives/Icons/TicketIcon.svelte +12 -0
- package/dist/primitives/Icons/TicketIcon.svelte.d.ts +16 -0
- package/dist/primitives/Icons/TicketIcon.svelte.d.ts.map +1 -0
- package/dist/primitives/Icons/ToastErrorIcon.svelte +9 -0
- package/dist/primitives/Icons/ToastErrorIcon.svelte.d.ts +16 -0
- package/dist/primitives/Icons/ToastErrorIcon.svelte.d.ts.map +1 -0
- package/dist/primitives/Icons/ToastInfoIcon.svelte +9 -0
- package/dist/primitives/Icons/ToastInfoIcon.svelte.d.ts +16 -0
- package/dist/primitives/Icons/ToastInfoIcon.svelte.d.ts.map +1 -0
- package/dist/primitives/Icons/ToastLoadingIcon.svelte +14 -0
- package/dist/primitives/Icons/ToastLoadingIcon.svelte.d.ts +16 -0
- package/dist/primitives/Icons/ToastLoadingIcon.svelte.d.ts.map +1 -0
- package/dist/primitives/Icons/ToastSuccessIcon.svelte +8 -0
- package/dist/primitives/Icons/ToastSuccessIcon.svelte.d.ts +16 -0
- package/dist/primitives/Icons/ToastSuccessIcon.svelte.d.ts.map +1 -0
- package/dist/primitives/Icons/ToastWarningIcon.svelte +9 -0
- package/dist/primitives/Icons/ToastWarningIcon.svelte.d.ts +16 -0
- package/dist/primitives/Icons/ToastWarningIcon.svelte.d.ts.map +1 -0
- package/dist/primitives/Icons/index.d.ts +10 -0
- package/dist/primitives/Icons/index.d.ts.map +1 -1
- package/dist/primitives/Icons/index.js +12 -0
- package/dist/recipes/Toaster/Toaster.stories.svelte +9 -28
- package/dist/recipes/Toaster/Toaster.stories.svelte.d.ts +1 -1
- package/dist/recipes/Toaster/Toaster.stories.svelte.d.ts.map +1 -1
- package/dist/schemas/auth.d.ts +17 -107
- package/dist/schemas/auth.d.ts.map +1 -1
- package/dist/schemas/common.d.ts +13 -41
- package/dist/schemas/common.d.ts.map +1 -1
- package/dist/schemas/event.d.ts +41 -147
- package/dist/schemas/event.d.ts.map +1 -1
- package/dist/schemas/order.d.ts +51 -208
- package/dist/schemas/order.d.ts.map +1 -1
- package/dist/schemas/performer.d.ts +44 -199
- package/dist/schemas/performer.d.ts.map +1 -1
- package/dist/schemas/promo.d.ts +55 -221
- package/dist/schemas/promo.d.ts.map +1 -1
- package/dist/schemas/ticket.d.ts +61 -187
- package/dist/schemas/ticket.d.ts.map +1 -1
- package/dist/schemas/user.d.ts +54 -114
- package/dist/schemas/user.d.ts.map +1 -1
- package/dist/schemas/venue.d.ts +20 -238
- package/dist/schemas/venue.d.ts.map +1 -1
- package/dist/stores/formSave.svelte.js +4 -4
- package/dist/stores/formSave.svelte.spec.js +10 -6
- package/dist/stores/index.d.ts +0 -1
- package/dist/stores/index.js +0 -1
- package/dist/tokens/utilities.css +2 -2
- package/package.json +5 -4
- package/dist/stores/toaster.d.ts +0 -4
- package/dist/stores/toaster.d.ts.map +0 -1
- package/dist/stores/toaster.js +0 -13
- package/dist/stores/toaster.spec.d.ts +0 -2
- package/dist/stores/toaster.spec.d.ts.map +0 -1
- package/dist/stores/toaster.spec.js +0 -59
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
<!-- native-element-exception: Toast action button needs inline styling incompatible with Button component -->
|
|
2
|
+
<!-- Micdrop Toast Item - macOS style with swipe-to-dismiss -->
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { onMount } from 'svelte';
|
|
5
|
+
|
|
6
|
+
import Button from '../../primitives/Button/Button.svelte';
|
|
7
|
+
import CloseOutline from '../../primitives/Icons/CloseOutline.svelte';
|
|
8
|
+
|
|
9
|
+
// Notification status icons
|
|
10
|
+
import CancelledIcon from '../../primitives/Icons/CancelledIcon.svelte';
|
|
11
|
+
import CartIcon from '../../primitives/Icons/CartIcon.svelte';
|
|
12
|
+
import ConfirmedIcon from '../../primitives/Icons/ConfirmedIcon.svelte';
|
|
13
|
+
import InvitedIcon from '../../primitives/Icons/InvitedIcon.svelte';
|
|
14
|
+
import TicketIcon from '../../primitives/Icons/TicketIcon.svelte';
|
|
15
|
+
|
|
16
|
+
// Toast type icons
|
|
17
|
+
import ToastErrorIcon from '../../primitives/Icons/ToastErrorIcon.svelte';
|
|
18
|
+
import ToastInfoIcon from '../../primitives/Icons/ToastInfoIcon.svelte';
|
|
19
|
+
import ToastLoadingIcon from '../../primitives/Icons/ToastLoadingIcon.svelte';
|
|
20
|
+
import ToastSuccessIcon from '../../primitives/Icons/ToastSuccessIcon.svelte';
|
|
21
|
+
import ToastWarningIcon from '../../primitives/Icons/ToastWarningIcon.svelte';
|
|
22
|
+
|
|
23
|
+
import { triggerHaptic } from '../../utils/haptic';
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// SWIPE-TO-DISMISS CONFIGURATION
|
|
27
|
+
// =============================================================================
|
|
28
|
+
const SWIPE_THRESHOLD = 0.3; // 30% of toast width to dismiss
|
|
29
|
+
const VELOCITY_THRESHOLD = 0.5; // px/ms for quick flicks
|
|
30
|
+
const DRAG_RESISTANCE = 0.8; // Slight resistance during drag
|
|
31
|
+
|
|
32
|
+
// Map toast types to notification icon components
|
|
33
|
+
const typeIconComponents: Record<string, typeof ConfirmedIcon> = {
|
|
34
|
+
success: ConfirmedIcon,
|
|
35
|
+
error: CancelledIcon,
|
|
36
|
+
warning: CancelledIcon, // Uses same icon, styled differently if needed
|
|
37
|
+
info: InvitedIcon,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
import { toast } from './toast.svelte';
|
|
41
|
+
|
|
42
|
+
import type { Toast } from './toast.svelte';
|
|
43
|
+
|
|
44
|
+
interface Props {
|
|
45
|
+
data: Toast;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let { data }: Props = $props();
|
|
49
|
+
|
|
50
|
+
let progressEl: HTMLDivElement | undefined = $state();
|
|
51
|
+
let isPaused = $derived(!!data.pausedAt);
|
|
52
|
+
let animationFrameId: number | undefined;
|
|
53
|
+
|
|
54
|
+
// Toast type icon components
|
|
55
|
+
const iconComponents: Record<string, typeof ToastSuccessIcon> = {
|
|
56
|
+
success: ToastSuccessIcon,
|
|
57
|
+
error: ToastErrorIcon,
|
|
58
|
+
warning: ToastWarningIcon,
|
|
59
|
+
info: ToastInfoIcon,
|
|
60
|
+
loading: ToastLoadingIcon,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Notification status config with consistent IA
|
|
64
|
+
// Pattern A: Performer-centric (has name) - Line 1: Name, Line 2: Status · Event
|
|
65
|
+
// Pattern B: Event-centric (no name) - Line 1: Event, Line 2: Status, Line 3: Details
|
|
66
|
+
function getNotificationConfig(status: string, hasPerformer: boolean) {
|
|
67
|
+
const normalizedStatus = (status || '').trim().toLowerCase();
|
|
68
|
+
|
|
69
|
+
// Pattern A - Performer notifications
|
|
70
|
+
if (
|
|
71
|
+
normalizedStatus === 'performer confirmed' ||
|
|
72
|
+
normalizedStatus === 'confirmed'
|
|
73
|
+
) {
|
|
74
|
+
return {
|
|
75
|
+
statusText: 'Confirmed',
|
|
76
|
+
statusColor: 'text-green-600',
|
|
77
|
+
IconComponent: ConfirmedIcon,
|
|
78
|
+
pattern: 'performer',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (
|
|
82
|
+
normalizedStatus === 'performer declined' ||
|
|
83
|
+
normalizedStatus === 'declined'
|
|
84
|
+
) {
|
|
85
|
+
return {
|
|
86
|
+
statusText: 'Declined',
|
|
87
|
+
statusColor: 'text-red-600',
|
|
88
|
+
IconComponent: CancelledIcon,
|
|
89
|
+
pattern: 'performer',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (
|
|
93
|
+
normalizedStatus === 'cancelled' ||
|
|
94
|
+
normalizedStatus === 'performer left' ||
|
|
95
|
+
normalizedStatus === 'performer left the roster'
|
|
96
|
+
) {
|
|
97
|
+
return {
|
|
98
|
+
statusText: 'Canceled',
|
|
99
|
+
statusColor: 'text-red-600',
|
|
100
|
+
IconComponent: CancelledIcon,
|
|
101
|
+
pattern: 'performer',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (
|
|
105
|
+
normalizedStatus === 'message received' ||
|
|
106
|
+
normalizedStatus === 'message sent'
|
|
107
|
+
) {
|
|
108
|
+
return {
|
|
109
|
+
statusText: 'New message',
|
|
110
|
+
statusColor: 'text-blue-600',
|
|
111
|
+
IconComponent: InvitedIcon,
|
|
112
|
+
pattern: 'performer',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Pattern B - Sales notifications (event-centric)
|
|
117
|
+
if (
|
|
118
|
+
normalizedStatus === 'ticket sold' ||
|
|
119
|
+
normalizedStatus === 'order completed'
|
|
120
|
+
) {
|
|
121
|
+
return {
|
|
122
|
+
statusText: 'Order completed',
|
|
123
|
+
statusColor: 'text-green-600',
|
|
124
|
+
IconComponent: CartIcon,
|
|
125
|
+
pattern: 'event',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (
|
|
129
|
+
normalizedStatus === 'event sold out' ||
|
|
130
|
+
normalizedStatus === 'sold out' ||
|
|
131
|
+
normalizedStatus === 'show sold out'
|
|
132
|
+
) {
|
|
133
|
+
return {
|
|
134
|
+
statusText: 'Sold out',
|
|
135
|
+
statusColor: 'text-green-600',
|
|
136
|
+
IconComponent: TicketIcon,
|
|
137
|
+
pattern: 'event',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (normalizedStatus === 'section sold out') {
|
|
141
|
+
return {
|
|
142
|
+
statusText: 'Section sold out',
|
|
143
|
+
statusColor: 'text-green-600',
|
|
144
|
+
IconComponent: TicketIcon,
|
|
145
|
+
pattern: 'event',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (
|
|
149
|
+
normalizedStatus === 'ticket type sold out' ||
|
|
150
|
+
normalizedStatus === 'vip tickets sold out'
|
|
151
|
+
) {
|
|
152
|
+
return {
|
|
153
|
+
statusText: 'Ticket type sold out',
|
|
154
|
+
statusColor: 'text-green-600',
|
|
155
|
+
IconComponent: TicketIcon,
|
|
156
|
+
pattern: 'event',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Pattern C - Event status notifications
|
|
161
|
+
if (
|
|
162
|
+
normalizedStatus === 'event published' ||
|
|
163
|
+
normalizedStatus === 'event is now published'
|
|
164
|
+
) {
|
|
165
|
+
return {
|
|
166
|
+
statusText: 'Published',
|
|
167
|
+
statusColor: 'text-green-600',
|
|
168
|
+
IconComponent: ConfirmedIcon,
|
|
169
|
+
pattern: 'event',
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (normalizedStatus === 'event rescheduled') {
|
|
173
|
+
return {
|
|
174
|
+
statusText: 'Rescheduled',
|
|
175
|
+
statusColor: 'text-amber-600',
|
|
176
|
+
IconComponent: null,
|
|
177
|
+
pattern: 'event',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (
|
|
181
|
+
normalizedStatus === 'event fully booked' ||
|
|
182
|
+
normalizedStatus === 'event is fully booked'
|
|
183
|
+
) {
|
|
184
|
+
return {
|
|
185
|
+
statusText: 'Fully booked',
|
|
186
|
+
statusColor: 'text-green-600',
|
|
187
|
+
IconComponent: ConfirmedIcon,
|
|
188
|
+
pattern: 'event',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Pattern D - Admin notifications
|
|
193
|
+
if (
|
|
194
|
+
normalizedStatus === 'avails sent' ||
|
|
195
|
+
normalizedStatus === 'availability request sent'
|
|
196
|
+
) {
|
|
197
|
+
return {
|
|
198
|
+
statusText: 'Availability sent',
|
|
199
|
+
statusColor: 'text-blue-600',
|
|
200
|
+
IconComponent: InvitedIcon,
|
|
201
|
+
pattern: hasPerformer ? 'performer' : 'event',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (
|
|
205
|
+
normalizedStatus === 'avails sent to all' ||
|
|
206
|
+
normalizedStatus === 'scheduled avails sent' ||
|
|
207
|
+
normalizedStatus === 'availability request sent bulk'
|
|
208
|
+
) {
|
|
209
|
+
return {
|
|
210
|
+
statusText: 'Availability sent',
|
|
211
|
+
statusColor: 'text-blue-600',
|
|
212
|
+
IconComponent: InvitedIcon,
|
|
213
|
+
pattern: 'event',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Fallback - determine pattern based on whether we have a performer name
|
|
218
|
+
return {
|
|
219
|
+
statusText: status || 'Update',
|
|
220
|
+
statusColor: 'text-gray-600',
|
|
221
|
+
IconComponent: InvitedIcon,
|
|
222
|
+
pattern: hasPerformer ? 'performer' : 'event',
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Determine if this is a performer-centric notification
|
|
227
|
+
let hasPerformer = $derived(
|
|
228
|
+
!!(data.notification?.name || data.notification?.displayName)
|
|
229
|
+
);
|
|
230
|
+
let notificationConfig = $derived(
|
|
231
|
+
data.notification
|
|
232
|
+
? getNotificationConfig(data.notification.status, hasPerformer)
|
|
233
|
+
: null
|
|
234
|
+
);
|
|
235
|
+
let displayName = $derived(
|
|
236
|
+
data.notification?.displayName || data.notification?.name || ''
|
|
237
|
+
);
|
|
238
|
+
// For event-centric notifications, use event as primary display
|
|
239
|
+
let primaryText = $derived(
|
|
240
|
+
notificationConfig?.pattern === 'event'
|
|
241
|
+
? data.notification?.event || ''
|
|
242
|
+
: displayName
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// Colors for each type - unified dark styling matching notification toasts
|
|
246
|
+
const typeStyles: Record<string, { icon: string; text: string }> = {
|
|
247
|
+
success: {
|
|
248
|
+
icon: 'text-green-600 dark:text-green-400',
|
|
249
|
+
text: 'text-gray-900 dark:text-white',
|
|
250
|
+
},
|
|
251
|
+
error: {
|
|
252
|
+
icon: 'text-red-500',
|
|
253
|
+
text: 'text-gray-900 dark:text-white',
|
|
254
|
+
},
|
|
255
|
+
warning: {
|
|
256
|
+
icon: 'text-amber-500',
|
|
257
|
+
text: 'text-gray-900 dark:text-white',
|
|
258
|
+
},
|
|
259
|
+
info: {
|
|
260
|
+
icon: 'text-blue-500',
|
|
261
|
+
text: 'text-gray-900 dark:text-white',
|
|
262
|
+
},
|
|
263
|
+
loading: {
|
|
264
|
+
icon: 'text-gray-400',
|
|
265
|
+
text: 'text-gray-900 dark:text-white',
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
let styles = $derived(typeStyles[data.type] || typeStyles.info);
|
|
270
|
+
let isClickable = $derived(!!data.onClick);
|
|
271
|
+
let isNotification = $derived(data.type === 'notification');
|
|
272
|
+
|
|
273
|
+
function handleMouseEnter() {
|
|
274
|
+
if (data.duration !== Infinity) {
|
|
275
|
+
// Stop progress animation — pause/resume is handled by Toaster container
|
|
276
|
+
if (animationFrameId) {
|
|
277
|
+
cancelAnimationFrame(animationFrameId);
|
|
278
|
+
animationFrameId = undefined;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function handleMouseLeave() {
|
|
284
|
+
if (data.duration !== Infinity) {
|
|
285
|
+
// Restart progress animation — pause/resume is handled by Toaster container
|
|
286
|
+
if (data.showProgress) {
|
|
287
|
+
animationFrameId = requestAnimationFrame(updateProgress);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function handleClick() {
|
|
293
|
+
if (data.onClick) {
|
|
294
|
+
data.onClick();
|
|
295
|
+
toast.dismiss(data.id);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function handleDismiss(e: MouseEvent) {
|
|
300
|
+
e.stopPropagation();
|
|
301
|
+
toast.dismiss(data.id);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Animate progress bar
|
|
305
|
+
function updateProgress() {
|
|
306
|
+
if (!progressEl || data.duration === Infinity || isPaused) return;
|
|
307
|
+
|
|
308
|
+
const elapsed = Date.now() - data.createdAt;
|
|
309
|
+
const progress = Math.max(0, 1 - elapsed / data.duration);
|
|
310
|
+
|
|
311
|
+
progressEl.style.transform = `scaleX(${progress})`;
|
|
312
|
+
|
|
313
|
+
if (progress > 0) {
|
|
314
|
+
animationFrameId = requestAnimationFrame(updateProgress);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
onMount(() => {
|
|
319
|
+
if (data.showProgress && data.duration !== Infinity) {
|
|
320
|
+
animationFrameId = requestAnimationFrame(updateProgress);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Add non-passive touchmove listener to allow preventDefault during swipe
|
|
324
|
+
// Svelte's ontouchmove is passive by default, which prevents preventDefault
|
|
325
|
+
function handleTouchMoveNonPassive(e: TouchEvent) {
|
|
326
|
+
if (isSwiping) {
|
|
327
|
+
e.preventDefault();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
toastElement?.addEventListener('touchmove', handleTouchMoveNonPassive, {
|
|
332
|
+
passive: false,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
return () => {
|
|
336
|
+
if (animationFrameId) {
|
|
337
|
+
cancelAnimationFrame(animationFrameId);
|
|
338
|
+
}
|
|
339
|
+
toastElement?.removeEventListener('touchmove', handleTouchMoveNonPassive);
|
|
340
|
+
};
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Resume progress animation when unpaused
|
|
344
|
+
$effect(() => {
|
|
345
|
+
if (!isPaused && data.showProgress && data.duration !== Infinity) {
|
|
346
|
+
animationFrameId = requestAnimationFrame(updateProgress);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// =============================================================================
|
|
351
|
+
// SWIPE-TO-DISMISS (mobile)
|
|
352
|
+
// =============================================================================
|
|
353
|
+
|
|
354
|
+
let toastElement: HTMLElement | undefined = $state();
|
|
355
|
+
let swipeOffset = $state(0);
|
|
356
|
+
let isSwiping = $state(false);
|
|
357
|
+
let touchStartX = 0;
|
|
358
|
+
let touchStartY = 0;
|
|
359
|
+
let lastTouchX = 0;
|
|
360
|
+
let lastTouchTime = 0;
|
|
361
|
+
let velocityX = 0;
|
|
362
|
+
|
|
363
|
+
function handleTouchStart(e: TouchEvent) {
|
|
364
|
+
touchStartX = e.touches[0].clientX;
|
|
365
|
+
touchStartY = e.touches[0].clientY;
|
|
366
|
+
lastTouchX = touchStartX;
|
|
367
|
+
lastTouchTime = Date.now();
|
|
368
|
+
velocityX = 0;
|
|
369
|
+
isSwiping = false;
|
|
370
|
+
swipeOffset = 0;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function handleTouchMove(e: TouchEvent) {
|
|
374
|
+
if (!touchStartX) return;
|
|
375
|
+
|
|
376
|
+
const touchCurrentX = e.touches[0].clientX;
|
|
377
|
+
const touchCurrentY = e.touches[0].clientY;
|
|
378
|
+
const diffX = touchCurrentX - touchStartX; // Positive = swiping right
|
|
379
|
+
const diffY = touchCurrentY - touchStartY;
|
|
380
|
+
const now = Date.now();
|
|
381
|
+
|
|
382
|
+
// Determine if this is a horizontal swipe
|
|
383
|
+
if (!isSwiping && Math.abs(diffX) > 10) {
|
|
384
|
+
if (Math.abs(diffX) > Math.abs(diffY)) {
|
|
385
|
+
isSwiping = true;
|
|
386
|
+
} else {
|
|
387
|
+
// Vertical scroll - don't handle
|
|
388
|
+
touchStartX = 0;
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (isSwiping) {
|
|
394
|
+
// Note: preventDefault is handled by the non-passive listener in onMount
|
|
395
|
+
|
|
396
|
+
// Calculate velocity (pixels per ms)
|
|
397
|
+
const dt = now - lastTouchTime;
|
|
398
|
+
if (dt > 0) {
|
|
399
|
+
velocityX = (touchCurrentX - lastTouchX) / dt;
|
|
400
|
+
}
|
|
401
|
+
lastTouchX = touchCurrentX;
|
|
402
|
+
lastTouchTime = now;
|
|
403
|
+
|
|
404
|
+
// Only allow swiping right (positive direction) - to dismiss
|
|
405
|
+
// Add slight resistance to left swipe, full movement to right
|
|
406
|
+
if (diffX > 0) {
|
|
407
|
+
swipeOffset = diffX * DRAG_RESISTANCE;
|
|
408
|
+
} else {
|
|
409
|
+
swipeOffset = diffX * 0.2; // Heavy resistance when swiping left
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function handleTouchEnd() {
|
|
415
|
+
if (!touchStartX || !isSwiping) {
|
|
416
|
+
touchStartX = 0;
|
|
417
|
+
touchStartY = 0;
|
|
418
|
+
isSwiping = false;
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const toastWidth = toastElement?.offsetWidth || 320;
|
|
423
|
+
const swipePercent = swipeOffset / toastWidth;
|
|
424
|
+
|
|
425
|
+
// Check if we should dismiss (swiped right past threshold or quick flick right)
|
|
426
|
+
const shouldDismiss =
|
|
427
|
+
swipePercent > SWIPE_THRESHOLD || velocityX > VELOCITY_THRESHOLD;
|
|
428
|
+
|
|
429
|
+
if (shouldDismiss && swipeOffset > 0) {
|
|
430
|
+
// Animate off screen to the right
|
|
431
|
+
triggerHaptic('medium');
|
|
432
|
+
swipeOffset = toastWidth + 20; // Slide off right edge
|
|
433
|
+
|
|
434
|
+
setTimeout(() => {
|
|
435
|
+
toast.dismiss(data.id);
|
|
436
|
+
}, 200);
|
|
437
|
+
} else {
|
|
438
|
+
// Snap back
|
|
439
|
+
swipeOffset = 0;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Reset state
|
|
443
|
+
touchStartX = 0;
|
|
444
|
+
touchStartY = 0;
|
|
445
|
+
isSwiping = false;
|
|
446
|
+
velocityX = 0;
|
|
447
|
+
}
|
|
448
|
+
</script>
|
|
449
|
+
|
|
450
|
+
<!-- Simple toast content snippet -->
|
|
451
|
+
{#snippet toastContent()}
|
|
452
|
+
<div class="flex flex-1 items-center gap-3 p-3">
|
|
453
|
+
<!-- Icon - use notification icons for consistency -->
|
|
454
|
+
{#if data.type === 'loading'}
|
|
455
|
+
<div class="size-5 shrink-0 animate-spin {styles.icon}">
|
|
456
|
+
<ToastLoadingIcon class="size-5" />
|
|
457
|
+
</div>
|
|
458
|
+
{:else if typeIconComponents[data.type]}
|
|
459
|
+
{@const IconComp = typeIconComponents[data.type]}
|
|
460
|
+
<div class="size-5 shrink-0 {styles.icon}">
|
|
461
|
+
<IconComp class="size-5" />
|
|
462
|
+
</div>
|
|
463
|
+
{/if}
|
|
464
|
+
|
|
465
|
+
<!-- Text -->
|
|
466
|
+
<div class="flex-1 min-w-0">
|
|
467
|
+
<p class="text-sm font-medium {styles.text}">
|
|
468
|
+
{data.title}
|
|
469
|
+
</p>
|
|
470
|
+
{#if data.description}
|
|
471
|
+
<p class="mt-1 text-sm text-body">
|
|
472
|
+
{data.description}
|
|
473
|
+
</p>
|
|
474
|
+
{/if}
|
|
475
|
+
{#if data.action}
|
|
476
|
+
<button
|
|
477
|
+
type="button"
|
|
478
|
+
class="mt-1 text-xs font-medium text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline cursor-pointer"
|
|
479
|
+
onclick={e => {
|
|
480
|
+
e.stopPropagation();
|
|
481
|
+
data.action?.onClick();
|
|
482
|
+
toast.dismiss(data.id);
|
|
483
|
+
}}
|
|
484
|
+
>
|
|
485
|
+
{data.action.label}
|
|
486
|
+
</button>
|
|
487
|
+
{/if}
|
|
488
|
+
</div>
|
|
489
|
+
|
|
490
|
+
<!-- Close button -->
|
|
491
|
+
{#if data.dismissible}
|
|
492
|
+
<Button
|
|
493
|
+
variant="ghost"
|
|
494
|
+
size="icon-sm"
|
|
495
|
+
onclick={handleDismiss}
|
|
496
|
+
aria-label="Dismiss"
|
|
497
|
+
className="shrink-0 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
|
|
498
|
+
>
|
|
499
|
+
<CloseOutline class="size-4" />
|
|
500
|
+
</Button>
|
|
501
|
+
{/if}
|
|
502
|
+
</div>
|
|
503
|
+
{/snippet}
|
|
504
|
+
|
|
505
|
+
<!-- Notification-style content snippet -->
|
|
506
|
+
{#snippet notificationContent()}
|
|
507
|
+
<div class="flex items-start gap-3 p-3">
|
|
508
|
+
<!-- Avatar -->
|
|
509
|
+
<img
|
|
510
|
+
src={data.notification?.avatar}
|
|
511
|
+
alt=""
|
|
512
|
+
class="size-11 rounded object-cover bg-accent-subtle shrink-0"
|
|
513
|
+
/>
|
|
514
|
+
|
|
515
|
+
<!-- Text content - pattern-based layout -->
|
|
516
|
+
<div class="flex-1 min-w-0 flex flex-col gap-0.5">
|
|
517
|
+
{#if notificationConfig?.pattern === 'performer'}
|
|
518
|
+
<!-- Pattern A: Performer-centric (always 3 lines) -->
|
|
519
|
+
<!-- Line 1: Performer name -->
|
|
520
|
+
<span
|
|
521
|
+
class="text-sm font-medium text-gray-900 dark:text-white truncate"
|
|
522
|
+
>
|
|
523
|
+
{displayName}
|
|
524
|
+
</span>
|
|
525
|
+
<!-- Line 2: Icon + Status -->
|
|
526
|
+
<div class="flex items-center gap-1.5">
|
|
527
|
+
{#if notificationConfig?.IconComponent}
|
|
528
|
+
{@const StatusIcon = notificationConfig.IconComponent}
|
|
529
|
+
<div class="size-3.5 shrink-0 {notificationConfig?.statusColor}">
|
|
530
|
+
<StatusIcon class="size-3.5" />
|
|
531
|
+
</div>
|
|
532
|
+
{/if}
|
|
533
|
+
<span
|
|
534
|
+
class="text-sm font-medium whitespace-nowrap {notificationConfig?.statusColor}"
|
|
535
|
+
>
|
|
536
|
+
{notificationConfig?.statusText}
|
|
537
|
+
</span>
|
|
538
|
+
</div>
|
|
539
|
+
<!-- Line 3: Event -->
|
|
540
|
+
{#if data.notification?.event}
|
|
541
|
+
<span class="text-sm text-body truncate">
|
|
542
|
+
{data.notification.event}
|
|
543
|
+
</span>
|
|
544
|
+
{/if}
|
|
545
|
+
<!-- Line 4: Message (if any) -->
|
|
546
|
+
{#if data.notification?.performerMessage}
|
|
547
|
+
<div class="bg-accent-subtle rounded px-2 py-1 mt-0.5">
|
|
548
|
+
<p class="text-xs text-body line-clamp-2">
|
|
549
|
+
{data.notification.performerMessage}
|
|
550
|
+
</p>
|
|
551
|
+
</div>
|
|
552
|
+
{/if}
|
|
553
|
+
{:else}
|
|
554
|
+
<!-- Pattern B/C: Event-centric (sales, event status, admin bulk) -->
|
|
555
|
+
<!-- Line 1: Event name -->
|
|
556
|
+
<span
|
|
557
|
+
class="text-sm font-medium text-gray-900 dark:text-white truncate"
|
|
558
|
+
>
|
|
559
|
+
{data.notification?.event}
|
|
560
|
+
</span>
|
|
561
|
+
<!-- Line 2: Icon + Status -->
|
|
562
|
+
<div class="flex items-center gap-1.5">
|
|
563
|
+
{#if notificationConfig?.IconComponent}
|
|
564
|
+
{@const StatusIcon = notificationConfig.IconComponent}
|
|
565
|
+
<div class="size-3.5 shrink-0 {notificationConfig?.statusColor}">
|
|
566
|
+
<StatusIcon class="size-3.5" />
|
|
567
|
+
</div>
|
|
568
|
+
{/if}
|
|
569
|
+
<span
|
|
570
|
+
class="text-sm font-medium whitespace-nowrap {notificationConfig?.statusColor}"
|
|
571
|
+
>
|
|
572
|
+
{notificationConfig?.statusText}
|
|
573
|
+
</span>
|
|
574
|
+
</div>
|
|
575
|
+
<!-- Line 3: Details (message) -->
|
|
576
|
+
{#if data.notification?.message}
|
|
577
|
+
<span class="text-xs text-gray-500 dark:text-gray-400">
|
|
578
|
+
{data.notification.message}
|
|
579
|
+
</span>
|
|
580
|
+
{/if}
|
|
581
|
+
{/if}
|
|
582
|
+
</div>
|
|
583
|
+
|
|
584
|
+
<!-- Close button -->
|
|
585
|
+
{#if data.dismissible}
|
|
586
|
+
<Button
|
|
587
|
+
variant="ghost"
|
|
588
|
+
size="icon-sm"
|
|
589
|
+
onclick={handleDismiss}
|
|
590
|
+
aria-label="Dismiss"
|
|
591
|
+
className="shrink-0 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
|
592
|
+
>
|
|
593
|
+
<CloseOutline class="size-4" />
|
|
594
|
+
</Button>
|
|
595
|
+
{/if}
|
|
596
|
+
</div>
|
|
597
|
+
{/snippet}
|
|
598
|
+
|
|
599
|
+
<!-- Unified toast wrapper — one outer div handles swipe + role, inner Button for clickable -->
|
|
600
|
+
<div
|
|
601
|
+
bind:this={toastElement}
|
|
602
|
+
class="toast-item relative flex w-80 max-w-sm overflow-hidden rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700"
|
|
603
|
+
class:swiping={isSwiping}
|
|
604
|
+
style:transform="translateX({swipeOffset}px)"
|
|
605
|
+
style:opacity={swipeOffset > 0 ? Math.max(0.3, 1 - swipeOffset / 320) : 1}
|
|
606
|
+
role={isClickable ? undefined : 'alert'}
|
|
607
|
+
tabindex={undefined}
|
|
608
|
+
onmouseenter={handleMouseEnter}
|
|
609
|
+
onmouseleave={handleMouseLeave}
|
|
610
|
+
onkeydown={undefined}
|
|
611
|
+
ontouchstart={handleTouchStart}
|
|
612
|
+
ontouchmove={handleTouchMove}
|
|
613
|
+
ontouchend={handleTouchEnd}
|
|
614
|
+
>
|
|
615
|
+
{#if isClickable}
|
|
616
|
+
<Button
|
|
617
|
+
variant="ghost"
|
|
618
|
+
className="w-full h-full p-0 text-left cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg"
|
|
619
|
+
onclick={handleClick}
|
|
620
|
+
>
|
|
621
|
+
{#if isNotification}
|
|
622
|
+
{@render notificationContent()}
|
|
623
|
+
{:else}
|
|
624
|
+
{@render toastContent()}
|
|
625
|
+
{/if}
|
|
626
|
+
</Button>
|
|
627
|
+
{:else if isNotification}
|
|
628
|
+
{@render notificationContent()}
|
|
629
|
+
{:else}
|
|
630
|
+
{@render toastContent()}
|
|
631
|
+
{/if}
|
|
632
|
+
|
|
633
|
+
{#if data.showProgress && data.duration !== Infinity}
|
|
634
|
+
<div
|
|
635
|
+
bind:this={progressEl}
|
|
636
|
+
class="absolute bottom-0 left-0 h-0.5 bg-current origin-left"
|
|
637
|
+
style:transform="scaleX(1)"
|
|
638
|
+
aria-hidden="true"
|
|
639
|
+
></div>
|
|
640
|
+
{/if}
|
|
641
|
+
</div>
|
|
642
|
+
|
|
643
|
+
<style>
|
|
644
|
+
/* macOS-style toast shadow with hover elevation */
|
|
645
|
+
.toast-item {
|
|
646
|
+
box-shadow:
|
|
647
|
+
0px 4px 12px color-mix(in srgb, var(--color-black) 8%, transparent),
|
|
648
|
+
0px 1px 3px color-mix(in srgb, var(--color-black) 6%, transparent);
|
|
649
|
+
transition:
|
|
650
|
+
box-shadow 0.2s ease-out,
|
|
651
|
+
opacity 0.2s ease-out;
|
|
652
|
+
/* Transform handled by inline style for swipe */
|
|
653
|
+
will-change: transform, opacity;
|
|
654
|
+
touch-action: pan-y; /* Allow vertical scroll, capture horizontal for swipe */
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/* Disable hover effects during swipe */
|
|
658
|
+
.toast-item:not(.swiping):hover {
|
|
659
|
+
box-shadow:
|
|
660
|
+
0px 8px 24px color-mix(in srgb, var(--color-black) 12%, transparent),
|
|
661
|
+
0px 2px 6px color-mix(in srgb, var(--color-black) 8%, transparent);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.toast-item:not(.swiping):active {
|
|
665
|
+
box-shadow:
|
|
666
|
+
0px 4px 12px color-mix(in srgb, var(--color-black) 10%, transparent),
|
|
667
|
+
0px 1px 3px color-mix(in srgb, var(--color-black) 6%, transparent);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/* Smooth snap-back animation when not swiping */
|
|
671
|
+
.toast-item:not(.swiping) {
|
|
672
|
+
transition:
|
|
673
|
+
transform 0.2s ease-out,
|
|
674
|
+
box-shadow 0.2s ease-out,
|
|
675
|
+
opacity 0.2s ease-out;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/* No transition during active swipe (follows finger) */
|
|
679
|
+
.toast-item.swiping {
|
|
680
|
+
transition: none;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
@keyframes spin {
|
|
684
|
+
from {
|
|
685
|
+
transform: rotate(0deg);
|
|
686
|
+
}
|
|
687
|
+
to {
|
|
688
|
+
transform: rotate(360deg);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
:global(.animate-spin) {
|
|
693
|
+
animation: spin 1s linear infinite;
|
|
694
|
+
}
|
|
695
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Toast } from './toast.svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
data: Toast;
|
|
4
|
+
}
|
|
5
|
+
declare const ToastItem: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type ToastItem = ReturnType<typeof ToastItem>;
|
|
7
|
+
export default ToastItem;
|
|
8
|
+
//# sourceMappingURL=ToastItem.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ToastItem.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/Toast/ToastItem.svelte.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAG1C,UAAU,KAAK;IACb,IAAI,EAAE,KAAK,CAAC;CACb;AAilBH,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|