@getmicdrop/svelte-components 5.21.3 → 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.
Files changed (79) hide show
  1. package/dist/components/Toast/ToastItem.svelte +695 -0
  2. package/dist/components/Toast/ToastItem.svelte.d.ts +8 -0
  3. package/dist/components/Toast/ToastItem.svelte.d.ts.map +1 -0
  4. package/dist/components/Toast/Toaster.svelte +184 -0
  5. package/dist/components/Toast/Toaster.svelte.d.ts +11 -0
  6. package/dist/components/Toast/Toaster.svelte.d.ts.map +1 -0
  7. package/dist/components/Toast/index.d.ts +9 -0
  8. package/dist/components/Toast/index.d.ts.map +1 -0
  9. package/dist/components/Toast/index.js +13 -0
  10. package/dist/components/Toast/toast.svelte.d.ts +93 -0
  11. package/dist/components/Toast/toast.svelte.d.ts.map +1 -0
  12. package/dist/components/Toast/toast.svelte.js +386 -0
  13. package/dist/components/index.d.ts +1 -0
  14. package/dist/components/index.js +3 -0
  15. package/dist/primitives/Icons/CancelledIcon.svelte +8 -0
  16. package/dist/primitives/Icons/CancelledIcon.svelte.d.ts +16 -0
  17. package/dist/primitives/Icons/CancelledIcon.svelte.d.ts.map +1 -0
  18. package/dist/primitives/Icons/CartIcon.svelte +12 -0
  19. package/dist/primitives/Icons/CartIcon.svelte.d.ts +16 -0
  20. package/dist/primitives/Icons/CartIcon.svelte.d.ts.map +1 -0
  21. package/dist/primitives/Icons/ConfirmedIcon.svelte +8 -0
  22. package/dist/primitives/Icons/ConfirmedIcon.svelte.d.ts +16 -0
  23. package/dist/primitives/Icons/ConfirmedIcon.svelte.d.ts.map +1 -0
  24. package/dist/primitives/Icons/InvitedIcon.svelte +7 -0
  25. package/dist/primitives/Icons/InvitedIcon.svelte.d.ts +16 -0
  26. package/dist/primitives/Icons/InvitedIcon.svelte.d.ts.map +1 -0
  27. package/dist/primitives/Icons/TicketIcon.svelte +12 -0
  28. package/dist/primitives/Icons/TicketIcon.svelte.d.ts +16 -0
  29. package/dist/primitives/Icons/TicketIcon.svelte.d.ts.map +1 -0
  30. package/dist/primitives/Icons/ToastErrorIcon.svelte +9 -0
  31. package/dist/primitives/Icons/ToastErrorIcon.svelte.d.ts +16 -0
  32. package/dist/primitives/Icons/ToastErrorIcon.svelte.d.ts.map +1 -0
  33. package/dist/primitives/Icons/ToastInfoIcon.svelte +9 -0
  34. package/dist/primitives/Icons/ToastInfoIcon.svelte.d.ts +16 -0
  35. package/dist/primitives/Icons/ToastInfoIcon.svelte.d.ts.map +1 -0
  36. package/dist/primitives/Icons/ToastLoadingIcon.svelte +14 -0
  37. package/dist/primitives/Icons/ToastLoadingIcon.svelte.d.ts +16 -0
  38. package/dist/primitives/Icons/ToastLoadingIcon.svelte.d.ts.map +1 -0
  39. package/dist/primitives/Icons/ToastSuccessIcon.svelte +8 -0
  40. package/dist/primitives/Icons/ToastSuccessIcon.svelte.d.ts +16 -0
  41. package/dist/primitives/Icons/ToastSuccessIcon.svelte.d.ts.map +1 -0
  42. package/dist/primitives/Icons/ToastWarningIcon.svelte +9 -0
  43. package/dist/primitives/Icons/ToastWarningIcon.svelte.d.ts +16 -0
  44. package/dist/primitives/Icons/ToastWarningIcon.svelte.d.ts.map +1 -0
  45. package/dist/primitives/Icons/index.d.ts +10 -0
  46. package/dist/primitives/Icons/index.d.ts.map +1 -1
  47. package/dist/primitives/Icons/index.js +12 -0
  48. package/dist/recipes/Toaster/Toaster.stories.svelte +9 -28
  49. package/dist/recipes/Toaster/Toaster.stories.svelte.d.ts +1 -1
  50. package/dist/recipes/Toaster/Toaster.stories.svelte.d.ts.map +1 -1
  51. package/dist/schemas/auth.d.ts +17 -107
  52. package/dist/schemas/auth.d.ts.map +1 -1
  53. package/dist/schemas/common.d.ts +13 -41
  54. package/dist/schemas/common.d.ts.map +1 -1
  55. package/dist/schemas/event.d.ts +41 -147
  56. package/dist/schemas/event.d.ts.map +1 -1
  57. package/dist/schemas/order.d.ts +51 -208
  58. package/dist/schemas/order.d.ts.map +1 -1
  59. package/dist/schemas/performer.d.ts +44 -199
  60. package/dist/schemas/performer.d.ts.map +1 -1
  61. package/dist/schemas/promo.d.ts +55 -221
  62. package/dist/schemas/promo.d.ts.map +1 -1
  63. package/dist/schemas/ticket.d.ts +61 -187
  64. package/dist/schemas/ticket.d.ts.map +1 -1
  65. package/dist/schemas/user.d.ts +54 -114
  66. package/dist/schemas/user.d.ts.map +1 -1
  67. package/dist/schemas/venue.d.ts +20 -238
  68. package/dist/schemas/venue.d.ts.map +1 -1
  69. package/dist/stores/formSave.svelte.js +4 -4
  70. package/dist/stores/formSave.svelte.spec.js +10 -6
  71. package/dist/stores/index.d.ts +0 -1
  72. package/dist/stores/index.js +0 -1
  73. package/package.json +5 -4
  74. package/dist/stores/toaster.d.ts +0 -4
  75. package/dist/stores/toaster.d.ts.map +0 -1
  76. package/dist/stores/toaster.js +0 -13
  77. package/dist/stores/toaster.spec.d.ts +0 -2
  78. package/dist/stores/toaster.spec.d.ts.map +0 -1
  79. 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"}