@seatlayer/js 0.13.0 → 0.15.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/README.md +6 -2
- package/dist/index.cjs +608 -88
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +82 -3
- package/dist/index.d.ts +82 -3
- package/dist/index.js +608 -88
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -52,6 +52,12 @@ var PubApi = class {
|
|
|
52
52
|
body: { qty, ...categoryKey ? { categoryKey } : {} }
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
|
+
resume(key, holdId) {
|
|
56
|
+
return request(this.base, `/pub/events/${encodeURIComponent(key)}/hold/resume`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
body: { holdId }
|
|
59
|
+
});
|
|
60
|
+
}
|
|
55
61
|
release(key, labels, holdId) {
|
|
56
62
|
return request(this.base, `/pub/events/${encodeURIComponent(key)}/release`, {
|
|
57
63
|
method: "POST",
|
|
@@ -108,6 +114,7 @@ var SeatingChart = class {
|
|
|
108
114
|
currency: options.currency,
|
|
109
115
|
onSelectionChange: (seats) => this.opts.onSelectionChange?.(seats),
|
|
110
116
|
onHold: (h) => this.opts.onHold?.({ holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items }),
|
|
117
|
+
onHoldRestored: (h) => this.opts.onHoldRestored?.({ holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items }),
|
|
111
118
|
onHoldExpired: () => this.opts.onHoldExpired?.(),
|
|
112
119
|
onGAClick: (areaId) => {
|
|
113
120
|
const area = this.controller.getGAAreas().find((candidate) => candidate.id === areaId);
|
|
@@ -211,6 +218,21 @@ var SeatingChart = class {
|
|
|
211
218
|
return null;
|
|
212
219
|
}
|
|
213
220
|
}
|
|
221
|
+
/** Restore an active hold by its opaque id without extending its expiry. */
|
|
222
|
+
async resumeHold(holdId) {
|
|
223
|
+
try {
|
|
224
|
+
const h = await this.controller.resumeHold(holdId);
|
|
225
|
+
return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
|
|
226
|
+
} catch (err) {
|
|
227
|
+
this.opts.onError?.(err);
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/** Current active hold known to this chart, if any. */
|
|
232
|
+
getCurrentHold() {
|
|
233
|
+
const h = this.controller.currentHold();
|
|
234
|
+
return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
|
|
235
|
+
}
|
|
214
236
|
getGAAreas() {
|
|
215
237
|
return this.controller.getGAAreas();
|
|
216
238
|
}
|
|
@@ -279,6 +301,10 @@ var SeatingChart = class {
|
|
|
279
301
|
async release() {
|
|
280
302
|
await this.controller.release();
|
|
281
303
|
}
|
|
304
|
+
/** Release selected labels from the current hold while keeping the remainder. */
|
|
305
|
+
async releaseLabels(labels) {
|
|
306
|
+
return this.controller.releaseLabels(labels);
|
|
307
|
+
}
|
|
282
308
|
/** Tear everything down: close the socket, stop timers, drop the canvas. */
|
|
283
309
|
destroy() {
|
|
284
310
|
if (this.hostEl && this.onTipMove) this.hostEl.removeEventListener("mousemove", this.onTipMove);
|
|
@@ -434,8 +460,12 @@ var CSS = `
|
|
|
434
460
|
.sl-head-meta{font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--sl-muted);margin-top:3px;
|
|
435
461
|
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
|
|
436
462
|
.sl-hold-pill{display:none;align-items:center;gap:6px;padding:6px 12px;border-radius:999px;flex:none;
|
|
437
|
-
background:var(--sl-accent);color:var(--sl-accent-ink);font-weight:700;font-size:12px;font-variant-numeric:tabular-nums
|
|
438
|
-
|
|
463
|
+
background:var(--sl-accent);color:var(--sl-accent-ink);font-weight:700;font-size:12px;font-variant-numeric:tabular-nums;
|
|
464
|
+
transform-origin:right center}
|
|
465
|
+
.sl-hold-pill.on{display:inline-flex;animation:slPillIn .34s cubic-bezier(.2,.8,.2,1) both}
|
|
466
|
+
.sl-hold-dot{width:7px;height:7px;border-radius:50%;background:currentColor;opacity:.78;box-shadow:0 0 0 0 currentColor}
|
|
467
|
+
.sl-hold-pill.is-expiring .sl-hold-dot{animation:slHoldPulse 1.4s ease-out infinite}
|
|
468
|
+
.sl-hold-time{min-width:3.35em;text-align:left}
|
|
439
469
|
.sl-close{width:32px;height:32px;border-radius:999px;flex:none;display:none;align-items:center;justify-content:center;
|
|
440
470
|
border:1px solid var(--sl-line);color:var(--sl-muted);transition:color .15s,border-color .15s}
|
|
441
471
|
.sl-close:hover{color:var(--sl-text);border-color:var(--sl-muted)}
|
|
@@ -457,11 +487,20 @@ var CSS = `
|
|
|
457
487
|
.sl-picker[data-layout="narrow"] .sl-body{flex-direction:column}
|
|
458
488
|
.sl-picker[data-layout="narrow"] .sl-map{min-height:0;flex:1}
|
|
459
489
|
.sl-picker[data-layout="narrow"] .sl-side{width:100%;border-left:0;border-top:1px solid var(--sl-line);
|
|
460
|
-
flex:none;height:50%;transition:height .
|
|
490
|
+
flex:none;height:50%;transition:height .3s cubic-bezier(.2,.8,.2,1)}
|
|
461
491
|
.sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side{height:86px;overflow:hidden}
|
|
462
492
|
.sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side > :not(.sl-sheet-head){display:none}
|
|
463
493
|
.sl-picker[data-layout="narrow"] .sl-tray{flex:none}
|
|
464
494
|
.sl-picker[data-layout="narrow"] .sl-foot{position:sticky;bottom:0;background:var(--sl-bg)}
|
|
495
|
+
.sl-picker[data-layout="narrow"] .sl-sheet-head{order:0}
|
|
496
|
+
.sl-picker[data-layout="narrow"] .sl-seats-sec{order:1}
|
|
497
|
+
.sl-picker[data-layout="narrow"] .sl-tray{order:2}
|
|
498
|
+
.sl-picker[data-layout="narrow"] .sl-filtersec{order:3}
|
|
499
|
+
.sl-picker[data-layout="narrow"] .sl-filters{order:4}
|
|
500
|
+
.sl-picker[data-layout="narrow"] .sl-prices-sec{order:5}
|
|
501
|
+
.sl-picker[data-layout="narrow"] .sl-pricef{order:6}
|
|
502
|
+
.sl-picker[data-layout="narrow"] .sl-prices{order:7}
|
|
503
|
+
.sl-picker[data-layout="narrow"] .sl-foot{order:8}
|
|
465
504
|
/* touch chrome: pinch-zoom exists \u2014 hide +/\u2212 on the sheet layout (keep fit) */
|
|
466
505
|
.sl-picker[data-layout="narrow"] .sl-zoom [data-ref="zin"],
|
|
467
506
|
.sl-picker[data-layout="narrow"] .sl-zoom [data-ref="zout"]{display:none}
|
|
@@ -508,15 +547,27 @@ var CSS = `
|
|
|
508
547
|
.sl-price-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
|
|
509
548
|
.sl-price-left{font-size:11px;color:var(--sl-muted);font-variant-numeric:tabular-nums}
|
|
510
549
|
.sl-price-amt{font-weight:800;font-variant-numeric:tabular-nums}
|
|
550
|
+
.sl-status-key{display:flex;gap:12px;flex-wrap:wrap;padding:8px 16px 12px;border-bottom:1px solid var(--sl-line);color:var(--sl-muted);font-size:11px}
|
|
551
|
+
.sl-status-item{display:inline-flex;align-items:center;gap:6px}
|
|
552
|
+
.sl-status-icon{width:17px;height:17px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;
|
|
553
|
+
color:#fff;background:#6b7280;font-size:9px;font-weight:900;line-height:1}
|
|
554
|
+
.sl-status-icon.sold{background:#374151;font-size:14px}
|
|
511
555
|
|
|
512
556
|
/* tray */
|
|
513
557
|
.sl-tray{flex:1;padding:10px 16px;display:flex;flex-direction:column;gap:8px;min-height:0}
|
|
514
558
|
.sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
|
|
515
559
|
.sl-chip{display:flex;align-items:center;gap:9px;padding:9px 11px;border:1px solid var(--sl-line);
|
|
516
|
-
border-radius:var(--sl-r-sm);background:var(--sl-surface);font-size:13px}
|
|
560
|
+
border-radius:var(--sl-r-sm);background:var(--sl-surface);font-size:13px;transform-origin:center}
|
|
561
|
+
.sl-chip.sl-enter{animation:slChipIn .38s cubic-bezier(.2,.8,.2,1) both}
|
|
562
|
+
.sl-chip.sl-leave{pointer-events:none;animation:slChipOut .16s ease-in both}
|
|
563
|
+
.sl-chip.sl-held{border-color:var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
|
|
564
|
+
box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent)}
|
|
517
565
|
.sl-chip b{font-weight:800}
|
|
518
566
|
.sl-chip .cat{color:var(--sl-muted);font-size:11.5px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
519
567
|
.sl-chip .amt{font-weight:700;font-variant-numeric:tabular-nums}
|
|
568
|
+
.sl-chip-state{flex:none;padding:3px 6px;border-radius:999px;background:var(--sl-accent);color:var(--sl-accent-ink);
|
|
569
|
+
font-size:8.5px;line-height:1;font-weight:900;letter-spacing:.08em;text-transform:uppercase}
|
|
570
|
+
.sl-chip-state.sl-pending{background:transparent;color:var(--sl-muted);border:1px solid var(--sl-line)}
|
|
520
571
|
.sl-chip .rm{width:22px;height:22px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;color:var(--sl-muted)}
|
|
521
572
|
.sl-chip .rm:hover{color:var(--sl-text)}
|
|
522
573
|
.sl-chip .rm svg{width:11px;height:11px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round}
|
|
@@ -534,17 +585,30 @@ var CSS = `
|
|
|
534
585
|
|
|
535
586
|
/* footer */
|
|
536
587
|
.sl-foot{padding:12px 16px 14px;border-top:1px solid var(--sl-line);flex:none}
|
|
588
|
+
.sl-hold-note{display:none;align-items:center;gap:9px;margin-bottom:10px;padding:9px 10px;border-radius:var(--sl-r-sm);
|
|
589
|
+
border:1px solid var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
|
|
590
|
+
box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent);
|
|
591
|
+
font-size:11.5px;line-height:1.35;color:var(--sl-muted)}
|
|
592
|
+
.sl-hold-note.on{display:flex;animation:slNoticeIn .38s cubic-bezier(.2,.8,.2,1) both}
|
|
593
|
+
.sl-hold-note svg{width:16px;height:16px;flex:none;stroke:var(--sl-accent);stroke-width:2.4;fill:none;
|
|
594
|
+
stroke-linecap:round;stroke-linejoin:round}
|
|
595
|
+
.sl-hold-note b{display:block;color:var(--sl-text);font-size:12px}
|
|
596
|
+
.sl-hold-copy{display:block}
|
|
537
597
|
.sl-total{display:flex;justify-content:space-between;align-items:center;font-size:13px;margin-bottom:10px}
|
|
538
598
|
.sl-total b{font-size:17px;font-variant-numeric:tabular-nums}
|
|
599
|
+
.sl-value-pop{animation:slValuePop .32s cubic-bezier(.2,.8,.2,1)}
|
|
539
600
|
/* Primary checkout CTA. Scoped under .sl-picker so it OUTWEIGHS the
|
|
540
601
|
'.sl-picker button' reset (0,1,1) \u2014 an unscoped '.sl-cta' (0,1,0) loses to it
|
|
541
602
|
and the button renders as plain text with no accent fill. */
|
|
542
603
|
.sl-picker .sl-cta{display:flex;align-items:center;justify-content:center;width:100%;min-height:44px;
|
|
543
604
|
padding:12px 16px;border-radius:var(--sl-r-sm);font-weight:800;font-size:14px;line-height:1.1;
|
|
544
605
|
background:var(--sl-accent);color:var(--sl-accent-ink);
|
|
545
|
-
transition:filter .15s,background .
|
|
606
|
+
transition:filter .15s,background .22s,color .22s,transform .12s,box-shadow .22s;gap:8px}
|
|
546
607
|
.sl-picker .sl-cta:hover{filter:brightness(1.08)}
|
|
547
608
|
.sl-picker .sl-cta:active{transform:translateY(1px);filter:brightness(.94)}
|
|
609
|
+
.sl-picker .sl-cta.sl-ready{animation:slCtaReady .42s cubic-bezier(.2,.8,.2,1)}
|
|
610
|
+
.sl-cta-spin,.sl-ba-spin{width:14px;height:14px;border-radius:50%;border:2px solid currentColor;border-right-color:transparent;
|
|
611
|
+
animation:slspin .7s linear infinite;flex:none}
|
|
548
612
|
/* Disabled ("Select seats"): quieter, but still a full-width button shape. */
|
|
549
613
|
.sl-picker .sl-cta:disabled{background:var(--sl-surface);color:var(--sl-muted);opacity:1;
|
|
550
614
|
cursor:not-allowed;filter:none;transform:none}
|
|
@@ -582,11 +646,15 @@ var CSS = `
|
|
|
582
646
|
.sl-zoom svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
583
647
|
|
|
584
648
|
/* toast + boot states (toast flows in the bottom-center region) */
|
|
585
|
-
.sl-toast{transform:translateY(6px);max-width:100%;
|
|
649
|
+
.sl-toast{transform:translateY(6px) scale(.98);max-width:100%;
|
|
586
650
|
background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-text);border-radius:999px;padding:9px 16px;
|
|
587
|
-
font-size:12.5px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .
|
|
651
|
+
font-size:12.5px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .22s,transform .22s;white-space:nowrap;
|
|
588
652
|
overflow:hidden;text-overflow:ellipsis}
|
|
589
|
-
.sl-toast.on{opacity:1;transform:translateY(0)}
|
|
653
|
+
.sl-toast.on{opacity:1;transform:translateY(0) scale(1)}
|
|
654
|
+
.sl-toast[data-tone="error"]{border-color:#ef4444}
|
|
655
|
+
.sl-toast[data-tone="warning"]{border-color:var(--sl-accent)}
|
|
656
|
+
.sl-toast[data-tone="success"]{border-color:#22c55e}
|
|
657
|
+
.sl-toast.on[data-tone="error"]{animation:slToastNudge .32s ease-out}
|
|
590
658
|
.sl-boot{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|
591
659
|
gap:10px;background:var(--sl-bg);font-size:13px;font-weight:600;color:var(--sl-muted)}
|
|
592
660
|
.sl-boot-spin{width:24px;height:24px;border-radius:50%;border:3px solid var(--sl-line);border-top-color:var(--sl-accent);
|
|
@@ -610,15 +678,22 @@ var CSS = `
|
|
|
610
678
|
.sl-extend-btn:disabled{opacity:.5;cursor:not-allowed}
|
|
611
679
|
|
|
612
680
|
/* booked confirmation overlay (covers the widget once the held seats are sold) */
|
|
613
|
-
.sl-booked{position:absolute;inset:0;z-index:11;display:
|
|
614
|
-
justify-content:center;gap:12px;text-align:center;padding:28px;background:var(--sl-bg)
|
|
615
|
-
|
|
681
|
+
.sl-booked{position:absolute;inset:0;z-index:11;display:flex;flex-direction:column;align-items:center;
|
|
682
|
+
justify-content:center;gap:12px;text-align:center;padding:28px;background:var(--sl-bg);opacity:0;visibility:hidden;
|
|
683
|
+
pointer-events:none;transition:opacity .34s ease,visibility 0s linear .34s}
|
|
684
|
+
.sl-booked.on{opacity:1;visibility:visible;pointer-events:auto;transition:opacity .34s ease,visibility 0s}
|
|
616
685
|
.sl-booked-badge{width:60px;height:60px;border-radius:999px;display:flex;align-items:center;justify-content:center;
|
|
617
|
-
background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
618
|
-
.sl-booked-badge
|
|
686
|
+
background:var(--sl-accent);color:var(--sl-accent-ink);transform:scale(.72)}
|
|
687
|
+
.sl-booked.on .sl-booked-badge{animation:slSuccessPop .58s cubic-bezier(.2,1.25,.3,1) .08s both}
|
|
688
|
+
.sl-booked-badge svg{width:30px;height:30px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round;
|
|
689
|
+
stroke-dasharray:30;stroke-dashoffset:30}
|
|
690
|
+
.sl-booked.on .sl-booked-badge svg{animation:slCheckDraw .42s ease-out .32s forwards}
|
|
619
691
|
.sl-booked-title{font-weight:800;font-size:19px;color:var(--sl-text)}
|
|
620
692
|
.sl-booked-sub{font-size:13px;color:var(--sl-muted);line-height:1.5;max-width:320px}
|
|
621
693
|
.sl-booked-seats{font-weight:700;color:var(--sl-text)}
|
|
694
|
+
.sl-booked.on .sl-booked-title,.sl-booked.on .sl-booked-sub{animation:slCopyRise .42s ease-out both}
|
|
695
|
+
.sl-booked.on .sl-booked-title{animation-delay:.22s}
|
|
696
|
+
.sl-booked.on .sl-booked-sub{animation-delay:.3s}
|
|
622
697
|
|
|
623
698
|
/* a11y filter chips (flow within the top-left region) */
|
|
624
699
|
.sl-chips{display:flex;gap:6px;flex-wrap:wrap}
|
|
@@ -627,28 +702,49 @@ var CSS = `
|
|
|
627
702
|
.sl-chip-f:hover{color:var(--sl-text)}
|
|
628
703
|
.sl-chip-f.on{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
|
|
629
704
|
|
|
630
|
-
/* confirm
|
|
631
|
-
.
|
|
632
|
-
|
|
633
|
-
.sl-
|
|
634
|
-
.sl-
|
|
635
|
-
.sl-confirm
|
|
705
|
+
/* confirm card: a candidate is not in the tray until Select. Map gestures and
|
|
706
|
+
floating chrome pause while the card owns focus, keeping the camera stable. */
|
|
707
|
+
.sl-picker[data-confirming="true"] .sl-map-host>:not(.sl-confirm){pointer-events:none}
|
|
708
|
+
.sl-picker[data-confirming="true"] .sl-anchor{pointer-events:none;opacity:.28;transition:opacity .16s}
|
|
709
|
+
.sl-picker[data-confirming="true"] .sl-side{pointer-events:none;opacity:.58;transition:opacity .16s}
|
|
710
|
+
.sl-confirm{position:absolute;z-index:10;width:276px;max-width:calc(100% - 24px);overflow:hidden;pointer-events:auto;
|
|
711
|
+
background:var(--sl-surface);border:1px solid color-mix(in srgb,var(--sl-line) 70%,var(--sl-text));
|
|
712
|
+
border-radius:15px;box-shadow:0 24px 64px -18px rgba(0,0,0,.72);transform:translate(-50%,calc(-100% - 16px));
|
|
713
|
+
animation:slConfirmIn .24s cubic-bezier(.2,.8,.2,1) both}
|
|
714
|
+
.sl-confirm[data-placement="below"]{transform:translate(-50%,16px);animation:slConfirmBelowIn .24s cubic-bezier(.2,.8,.2,1) both}
|
|
715
|
+
.sl-confirm-grid{display:grid;grid-template-columns:1.2fr .8fr .8fr;border-bottom:1px solid var(--sl-line)}
|
|
716
|
+
.sl-confirm-field{min-width:0;padding:12px 11px 10px;border-right:1px solid var(--sl-line)}
|
|
717
|
+
.sl-confirm-field:last-child{border-right:0;text-align:center}
|
|
718
|
+
.sl-confirm-field:nth-child(2){text-align:center}
|
|
719
|
+
.sl-confirm-key{display:block;font-size:8.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
|
|
720
|
+
.sl-confirm-value{display:block;margin-top:4px;color:var(--sl-text);font-size:17px;line-height:1.1;font-weight:850;
|
|
721
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
722
|
+
.sl-confirm-cat{display:flex;align-items:center;gap:8px;padding:10px 12px;background:color-mix(in srgb,var(--sl-cat) 76%,var(--sl-surface))}
|
|
723
|
+
.sl-confirm-cat .sl-dot{border:2px solid rgba(255,255,255,.78);width:11px;height:11px}
|
|
724
|
+
.sl-confirm-cat-name{font-size:13.5px;font-weight:800;color:#fff;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
725
|
+
.sl-confirm-price{font-size:17px;font-weight:850;color:#fff;font-variant-numeric:tabular-nums}
|
|
726
|
+
.sl-confirm-body{padding:11px 12px 12px}
|
|
636
727
|
.sl-confirm-row{display:flex;gap:8px;margin-top:10px}
|
|
637
|
-
.sl-confirm-row button{flex:1;padding:
|
|
638
|
-
.sl-confirm-add{background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
639
|
-
.sl-confirm-
|
|
728
|
+
.sl-confirm-row button{flex:1;min-height:44px;padding:9px 12px;border-radius:9px;font-weight:800;font-size:13px}
|
|
729
|
+
.sl-confirm-add{background:var(--sl-accent)!important;color:var(--sl-accent-ink)!important;display:flex;align-items:center;justify-content:center;gap:7px}
|
|
730
|
+
.sl-confirm-add svg{width:16px;height:16px;stroke:currentColor;stroke-width:2.8;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
731
|
+
.sl-confirm-cancel{background:color-mix(in srgb,var(--sl-line) 44%,transparent)!important;border:1px solid var(--sl-line)!important;color:var(--sl-muted)!important}
|
|
640
732
|
.sl-confirm-cancel:hover{color:var(--sl-text)}
|
|
733
|
+
.sl-picker[data-layout="narrow"] .sl-confirm{left:50%!important;top:auto!important;bottom:14px;width:min(342px,calc(100% - 24px));
|
|
734
|
+
transform:translateX(-50%);animation:slConfirmMobileIn .24s cubic-bezier(.2,.8,.2,1) both}
|
|
641
735
|
|
|
642
736
|
/* best-available row */
|
|
643
|
-
.sl-ba{display:
|
|
737
|
+
.sl-ba{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:8px;padding:9px 11px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm)}
|
|
644
738
|
.sl-ba select{background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:7px;
|
|
645
|
-
font:inherit;font-size:12px;padding:
|
|
739
|
+
font:inherit;font-size:12px;padding:7px 8px;min-width:0;width:100%;max-width:none}
|
|
646
740
|
.sl-ba-qty{display:flex;align-items:center;gap:7px}
|
|
647
741
|
.sl-ba-qty button{width:24px;height:24px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
648
742
|
font-size:14px;font-weight:700;display:flex;align-items:center;justify-content:center}
|
|
649
743
|
.sl-ba-qty span{min-width:14px;text-align:center;font-weight:800}
|
|
650
|
-
.sl-ba-go{
|
|
744
|
+
.sl-ba-go{grid-column:1/-1;width:100%;min-height:38px;padding:7px 12px;border-radius:9px;border:1px solid var(--sl-line);font-weight:800;font-size:12px;
|
|
745
|
+
transition:border-color .15s,opacity .15s;display:flex;align-items:center;justify-content:center;gap:6px}
|
|
651
746
|
.sl-ba-go:hover{border-color:var(--sl-muted)}
|
|
747
|
+
.sl-ba-go:disabled{opacity:.62;cursor:wait}
|
|
652
748
|
|
|
653
749
|
/* screen-reader live region */
|
|
654
750
|
.sl-sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
|
@@ -754,6 +850,29 @@ var CSS = `
|
|
|
754
850
|
.sl-price-row.sl-dim{opacity:.4}
|
|
755
851
|
.sl-seccard-mix-item.sl-dim{opacity:.4}
|
|
756
852
|
|
|
853
|
+
/* Buyer-journey motion: every animation explains a state transition (selected,
|
|
854
|
+
held, checkout handoff, conflict or booked). No decorative infinite motion
|
|
855
|
+
except the expiring-hold pulse and active progress spinners. */
|
|
856
|
+
@keyframes slPillIn{from{opacity:0;transform:translateX(7px) scale(.9)}to{opacity:1;transform:translateX(0) scale(1)}}
|
|
857
|
+
@keyframes slHoldPulse{0%{box-shadow:0 0 0 0 currentColor;opacity:.9}75%,100%{box-shadow:0 0 0 7px transparent;opacity:.55}}
|
|
858
|
+
@keyframes slChipIn{from{opacity:0;transform:translateY(8px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}
|
|
859
|
+
@keyframes slChipOut{to{opacity:0;transform:translateX(10px) scale(.98)}}
|
|
860
|
+
@keyframes slNoticeIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
|
|
861
|
+
@keyframes slValuePop{0%{opacity:.6;transform:translateY(3px)}55%{transform:translateY(-1px) scale(1.05)}100%{opacity:1;transform:none}}
|
|
862
|
+
@keyframes slCtaReady{0%{transform:scale(.98);box-shadow:0 0 0 0 transparent}55%{transform:scale(1.01);box-shadow:0 0 0 5px color-mix(in srgb,var(--sl-accent) 18%,transparent)}100%{transform:none;box-shadow:none}}
|
|
863
|
+
@keyframes slToastNudge{0%,100%{margin-left:0}30%{margin-left:-4px}60%{margin-left:3px}}
|
|
864
|
+
@keyframes slSuccessPop{0%{opacity:0;transform:scale(.72)}65%{opacity:1;transform:scale(1.08)}100%{opacity:1;transform:scale(1)}}
|
|
865
|
+
@keyframes slCheckDraw{to{stroke-dashoffset:0}}
|
|
866
|
+
@keyframes slCopyRise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
|
867
|
+
@keyframes slConfirmIn{from{opacity:0;transform:translate(-50%,calc(-100% - 8px)) scale(.96)}to{opacity:1;transform:translate(-50%,calc(-100% - 14px)) scale(1)}}
|
|
868
|
+
@keyframes slConfirmBelowIn{from{opacity:0;transform:translate(-50%,8px) scale(.96)}to{opacity:1;transform:translate(-50%,16px) scale(1)}}
|
|
869
|
+
@keyframes slConfirmMobileIn{from{opacity:0;transform:translate(-50%,10px) scale(.97)}to{opacity:1;transform:translate(-50%,0) scale(1)}}
|
|
870
|
+
|
|
871
|
+
@media(prefers-reduced-motion:reduce){
|
|
872
|
+
.sl-picker *,.sl-modal-scrim *{animation-duration:.001ms!important;animation-iteration-count:1!important;
|
|
873
|
+
transition-duration:.001ms!important;scroll-behavior:auto!important}
|
|
874
|
+
}
|
|
875
|
+
|
|
757
876
|
/* modal host */
|
|
758
877
|
.sl-modal-scrim{position:fixed;inset:0;z-index:2147483000;background:rgba(5,7,12,.66);display:flex;align-items:center;justify-content:center;padding:18px}
|
|
759
878
|
.sl-modal-frame{width:min(1200px,100%);height:min(820px,100%);border-radius:16px;overflow:hidden;box-shadow:0 40px 120px -30px rgba(0,0,0,.8)}
|
|
@@ -794,6 +913,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
794
913
|
this.ro = null;
|
|
795
914
|
this.holdTimer = null;
|
|
796
915
|
this.toastTimer = null;
|
|
916
|
+
/** Short-lived UI motion timers; all are cancelled on destroy. */
|
|
917
|
+
this.motionTimers = /* @__PURE__ */ new Set();
|
|
797
918
|
// state
|
|
798
919
|
this.currency = "USD";
|
|
799
920
|
this.hold = null;
|
|
@@ -837,6 +958,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
837
958
|
this.secCardShownAt = 0;
|
|
838
959
|
/** Previous tray ticket count — first 0→n transition auto-expands the mobile sheet. */
|
|
839
960
|
this.lastTrayCount = 0;
|
|
961
|
+
/** Previous computed total — drives a single explanatory value bump. */
|
|
962
|
+
this.lastTrayTotal = 0;
|
|
963
|
+
/** Stable item keys prevent tray chips re-animating on unrelated realtime syncs. */
|
|
964
|
+
this.lastTrayKeys = /* @__PURE__ */ new Set();
|
|
965
|
+
this.bestAvailableBusy = false;
|
|
966
|
+
this.releasingLabels = /* @__PURE__ */ new Set();
|
|
967
|
+
/** Selected labels awaiting the hold response; their own realtime echo can arrive first. */
|
|
968
|
+
this.holdingLabels = /* @__PURE__ */ new Set();
|
|
969
|
+
this.ctaPhase = "idle";
|
|
840
970
|
// narrow-layout chrome that docks into the sheet's Filters row on mobile
|
|
841
971
|
this.a11yChipsEl = null;
|
|
842
972
|
this.cbEl = null;
|
|
@@ -849,10 +979,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
849
979
|
if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
|
|
850
980
|
if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
|
|
851
981
|
if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
|
|
852
|
-
this.opts = options;
|
|
853
|
-
|
|
982
|
+
this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
|
|
983
|
+
this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
|
|
984
|
+
this.api = new PubApi(this.apiBase);
|
|
854
985
|
this.controller = new PickerController2({
|
|
855
|
-
transport: api,
|
|
986
|
+
transport: this.api,
|
|
856
987
|
eventKey: options.event,
|
|
857
988
|
maxSelection: options.maxSelection ?? DEFAULT_MAX_SELECTION2,
|
|
858
989
|
currency: options.currency,
|
|
@@ -860,7 +991,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
860
991
|
colorblindSafe: options.colorblindSafe,
|
|
861
992
|
onSelectionChange: () => {
|
|
862
993
|
this.syncTray();
|
|
863
|
-
if (this.
|
|
994
|
+
if (this.committedSelection().length) this.collapseSectionCard();
|
|
864
995
|
},
|
|
865
996
|
onStatusChange: () => {
|
|
866
997
|
this.syncPrices();
|
|
@@ -870,18 +1001,25 @@ var SeatPicker = class _SeatPicker {
|
|
|
870
1001
|
},
|
|
871
1002
|
onHoldExpired: () => {
|
|
872
1003
|
this.hold = null;
|
|
1004
|
+
this.forgetHold();
|
|
873
1005
|
this.handedOff = false;
|
|
874
1006
|
this.bookedShown = false;
|
|
1007
|
+
this.ctaPhase = "idle";
|
|
875
1008
|
this.stopHoldTimer();
|
|
876
1009
|
this.gaQty.clear();
|
|
877
|
-
this.toast(t2("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.");
|
|
1010
|
+
this.toast(t2("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.", "warning");
|
|
878
1011
|
this.syncTray();
|
|
1012
|
+
this.emitHoldChange();
|
|
879
1013
|
this.opts.onHoldExpired?.();
|
|
880
1014
|
},
|
|
881
|
-
confirmSelection:
|
|
1015
|
+
confirmSelection: this.opts.confirmSelection,
|
|
882
1016
|
onSelect: (seat) => {
|
|
1017
|
+
this.flashPickedSeat(seat.id);
|
|
883
1018
|
if (this.opts.confirmSelection) this.showConfirm(seat);
|
|
884
1019
|
},
|
|
1020
|
+
onDeselect: (seat) => {
|
|
1021
|
+
if (this.confirmSeat?.id === seat.id) this.dismissConfirm();
|
|
1022
|
+
},
|
|
885
1023
|
onViewChange: () => {
|
|
886
1024
|
this.reanchorConfirm();
|
|
887
1025
|
this.syncRung();
|
|
@@ -921,17 +1059,32 @@ var SeatPicker = class _SeatPicker {
|
|
|
921
1059
|
const picker = new _SeatPicker({ ...options, container: frame });
|
|
922
1060
|
picker.modalScrim = scrim;
|
|
923
1061
|
picker.prevFocus = document.activeElement;
|
|
1062
|
+
let closing = false;
|
|
924
1063
|
const close = () => {
|
|
1064
|
+
if (closing) return;
|
|
1065
|
+
closing = true;
|
|
925
1066
|
document.body.style.overflow = prevOverflow;
|
|
926
|
-
|
|
927
|
-
|
|
1067
|
+
scrim.style.opacity = "0";
|
|
1068
|
+
scrim.style.pointerEvents = "none";
|
|
1069
|
+
const finish = () => {
|
|
1070
|
+
picker.destroy();
|
|
1071
|
+
options.onClose?.();
|
|
1072
|
+
};
|
|
1073
|
+
if (picker.hold && !picker.handedOff) void picker.release().finally(finish);
|
|
1074
|
+
else finish();
|
|
928
1075
|
};
|
|
929
1076
|
picker.closeModal = close;
|
|
930
1077
|
scrim.addEventListener("mousedown", (e) => {
|
|
931
1078
|
if (e.target === scrim) close();
|
|
932
1079
|
});
|
|
933
1080
|
picker.escHandler = (e) => {
|
|
934
|
-
if (e.key
|
|
1081
|
+
if (e.key !== "Escape") return;
|
|
1082
|
+
if (picker.confirmSeat) {
|
|
1083
|
+
e.preventDefault();
|
|
1084
|
+
picker.cancelConfirm();
|
|
1085
|
+
} else {
|
|
1086
|
+
close();
|
|
1087
|
+
}
|
|
935
1088
|
};
|
|
936
1089
|
document.addEventListener("keydown", picker.escHandler);
|
|
937
1090
|
await picker.render();
|
|
@@ -948,8 +1101,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
948
1101
|
const mount = resolveContainer3(this.opts.container);
|
|
949
1102
|
const root = document.createElement("div");
|
|
950
1103
|
root.className = "sl-picker";
|
|
1104
|
+
root.tabIndex = -1;
|
|
951
1105
|
this.root = root;
|
|
952
1106
|
mount.appendChild(root);
|
|
1107
|
+
root.addEventListener("keydown", (e) => {
|
|
1108
|
+
if (e.key !== "Escape" || !this.confirmSeat) return;
|
|
1109
|
+
e.preventDefault();
|
|
1110
|
+
e.stopPropagation();
|
|
1111
|
+
this.cancelConfirm();
|
|
1112
|
+
});
|
|
953
1113
|
Object.entries(resolveTokens(void 0, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
|
|
954
1114
|
root.innerHTML = `
|
|
955
1115
|
<div class="sl-head">
|
|
@@ -988,11 +1148,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
988
1148
|
</div>
|
|
989
1149
|
<div class="sl-sec sl-filtersec" data-ref="filtersSec">Filters</div>
|
|
990
1150
|
<div class="sl-filters" data-ref="filters"></div>
|
|
991
|
-
<div class="sl-sec" data-ref="pricesSec">Prices</div>
|
|
1151
|
+
<div class="sl-sec sl-prices-sec" data-ref="pricesSec">Prices</div>
|
|
992
1152
|
<div class="sl-prices" data-ref="prices"></div>
|
|
993
|
-
<div class="sl-sec">Your seats</div>
|
|
1153
|
+
<div class="sl-sec sl-seats-sec">Your seats</div>
|
|
994
1154
|
<div class="sl-tray" data-ref="tray"></div>
|
|
995
1155
|
<div class="sl-foot">
|
|
1156
|
+
<div class="sl-hold-note" data-ref="holdNote" role="status" aria-live="polite">
|
|
1157
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
|
|
1158
|
+
<span><b data-ref="holdTitle">Seats secured</b><span class="sl-hold-copy" data-ref="holdCopy">Checkout timer is running.</span></span>
|
|
1159
|
+
</div>
|
|
996
1160
|
<div class="sl-total"><span data-ref="count"></span><b data-ref="total"></b></div>
|
|
997
1161
|
<button type="button" class="sl-cta" data-ref="cta" disabled></button>
|
|
998
1162
|
</div>
|
|
@@ -1144,6 +1308,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
1144
1308
|
this.buildExtendPrompt();
|
|
1145
1309
|
this.buildBookedOverlay();
|
|
1146
1310
|
this.dockLayoutChrome();
|
|
1311
|
+
await this.restoreRememberedHold();
|
|
1312
|
+
if (this.destroyed) return this;
|
|
1147
1313
|
this.syncPrices();
|
|
1148
1314
|
this.syncTray();
|
|
1149
1315
|
return this;
|
|
@@ -1219,6 +1385,156 @@ var SeatPicker = class _SeatPicker {
|
|
|
1219
1385
|
cssVar(name) {
|
|
1220
1386
|
return this.root ? getComputedStyle(this.root).getPropertyValue(name).trim() : "";
|
|
1221
1387
|
}
|
|
1388
|
+
/** Motion is progressive enhancement; all state remains legible when reduced. */
|
|
1389
|
+
reducedMotion() {
|
|
1390
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1391
|
+
}
|
|
1392
|
+
scheduleMotion(fn, delay) {
|
|
1393
|
+
const timer = setTimeout(() => {
|
|
1394
|
+
this.motionTimers.delete(timer);
|
|
1395
|
+
if (!this.destroyed) fn();
|
|
1396
|
+
}, delay);
|
|
1397
|
+
this.motionTimers.add(timer);
|
|
1398
|
+
}
|
|
1399
|
+
/** Restart one finite CSS animation without leaving a permanent state class. */
|
|
1400
|
+
animateOnce(el, className, duration = 600) {
|
|
1401
|
+
if (!el || this.reducedMotion()) return;
|
|
1402
|
+
el.classList.remove(className);
|
|
1403
|
+
void el.offsetWidth;
|
|
1404
|
+
el.classList.add(className);
|
|
1405
|
+
this.scheduleMotion(() => el.classList.remove(className), duration);
|
|
1406
|
+
}
|
|
1407
|
+
/** Selection feedback belongs on the selected seat, not across the whole map. */
|
|
1408
|
+
flashPickedSeat(id) {
|
|
1409
|
+
if (this.reducedMotion()) return;
|
|
1410
|
+
this.controller.flashSeat(id, this.cssVar("--sl-accent") || "#f4b740");
|
|
1411
|
+
}
|
|
1412
|
+
/** A completed hold gets one short map ripple per concrete seat. */
|
|
1413
|
+
flashHeldSeats(hold) {
|
|
1414
|
+
if (this.reducedMotion()) return;
|
|
1415
|
+
const labels = (hold.items ?? []).filter((item) => item.objectType !== "ga").map((item) => item.label);
|
|
1416
|
+
labels.slice(0, 10).forEach((label, index) => {
|
|
1417
|
+
const seat = this.controller.seatByLabel(label);
|
|
1418
|
+
if (!seat) return;
|
|
1419
|
+
this.scheduleMotion(
|
|
1420
|
+
() => this.controller.flashSeat(seat.id, this.cssVar("--sl-accent") || "#f4b740"),
|
|
1421
|
+
index * 55
|
|
1422
|
+
);
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
/** Update only the action affordance; selection callbacks must not refire. */
|
|
1426
|
+
committedSelection() {
|
|
1427
|
+
const candidateId = this.confirmSeat?.id;
|
|
1428
|
+
return this.controller.getSelection().filter((seat) => seat.id !== candidateId);
|
|
1429
|
+
}
|
|
1430
|
+
pendingSelectionCount() {
|
|
1431
|
+
const heldItems = this.hold?.items ?? [];
|
|
1432
|
+
const heldLabels = new Set(heldItems.map((item) => item.label));
|
|
1433
|
+
const pendingSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).length;
|
|
1434
|
+
const heldGA = /* @__PURE__ */ new Map();
|
|
1435
|
+
for (const item of heldItems.filter((candidate) => candidate.objectType === "ga")) {
|
|
1436
|
+
heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
|
|
1437
|
+
}
|
|
1438
|
+
const pendingGA = [...this.gaQty.entries()].reduce(
|
|
1439
|
+
(sum, [areaId, qty]) => sum + Math.max(0, qty - (heldGA.get(areaId) ?? 0)),
|
|
1440
|
+
0
|
|
1441
|
+
);
|
|
1442
|
+
return pendingSeats + pendingGA;
|
|
1443
|
+
}
|
|
1444
|
+
syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
|
|
1445
|
+
const cta = this.els.cta;
|
|
1446
|
+
if (!cta) return;
|
|
1447
|
+
if (this.confirmSeat) {
|
|
1448
|
+
cta.disabled = true;
|
|
1449
|
+
cta.textContent = "Confirm or cancel this seat";
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
if (this.ctaPhase === "holding") {
|
|
1453
|
+
cta.disabled = true;
|
|
1454
|
+
cta.innerHTML = '<span class="sl-cta-spin" aria-hidden="true"></span>Securing seats\u2026';
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
if (this.ctaPhase === "checkout") {
|
|
1458
|
+
cta.disabled = true;
|
|
1459
|
+
cta.innerHTML = '<span class="sl-cta-spin" aria-hidden="true"></span>Opening checkout\u2026';
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
cta.disabled = count === 0;
|
|
1463
|
+
cta.textContent = this.hold ? pending ? `Secure ${pending} more & checkout` : "Continue to checkout" : count ? "Hold seats & checkout" : "Select seats";
|
|
1464
|
+
}
|
|
1465
|
+
setCtaPhase(phase) {
|
|
1466
|
+
this.ctaPhase = phase;
|
|
1467
|
+
this.syncCta();
|
|
1468
|
+
if (phase === "checkout") {
|
|
1469
|
+
this.scheduleMotion(() => {
|
|
1470
|
+
if (this.ctaPhase !== "checkout") return;
|
|
1471
|
+
this.ctaPhase = "idle";
|
|
1472
|
+
this.syncCta();
|
|
1473
|
+
}, 1100);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
/** Session-scoped capability key: isolated by API origin and event. */
|
|
1477
|
+
holdStorageKey() {
|
|
1478
|
+
return `@seatlayer/hold/v1/${encodeURIComponent(this.apiBase)}/${encodeURIComponent(this.opts.event)}`;
|
|
1479
|
+
}
|
|
1480
|
+
rememberedHoldId() {
|
|
1481
|
+
if (this.opts.initialHoldId) return this.opts.initialHoldId;
|
|
1482
|
+
if (this.opts.restoreHold === false || typeof window === "undefined") return null;
|
|
1483
|
+
try {
|
|
1484
|
+
return window.sessionStorage.getItem(this.holdStorageKey());
|
|
1485
|
+
} catch {
|
|
1486
|
+
return null;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
rememberHold(hold) {
|
|
1490
|
+
if (this.opts.restoreHold === false || typeof window === "undefined") return;
|
|
1491
|
+
try {
|
|
1492
|
+
window.sessionStorage.setItem(this.holdStorageKey(), hold.holdId);
|
|
1493
|
+
} catch {
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
forgetHold() {
|
|
1497
|
+
if (typeof window === "undefined") return;
|
|
1498
|
+
try {
|
|
1499
|
+
window.sessionStorage.removeItem(this.holdStorageKey());
|
|
1500
|
+
} catch {
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
async resumeHoldFromServer(holdId, automatic) {
|
|
1504
|
+
try {
|
|
1505
|
+
const h = await this.controller.resumeHold(holdId);
|
|
1506
|
+
if (!h) return null;
|
|
1507
|
+
const restored = {
|
|
1508
|
+
holdId: h.holdId,
|
|
1509
|
+
expiresAt: h.expiresAt,
|
|
1510
|
+
seats: h.seats,
|
|
1511
|
+
items: h.items
|
|
1512
|
+
};
|
|
1513
|
+
this.hold = restored;
|
|
1514
|
+
this.handedOff = true;
|
|
1515
|
+
this.bookedShown = false;
|
|
1516
|
+
this.ctaPhase = "idle";
|
|
1517
|
+
this.startHoldTimer(restored.expiresAt);
|
|
1518
|
+
this.rememberHold(restored);
|
|
1519
|
+
this.syncTray();
|
|
1520
|
+
this.emitHoldChange();
|
|
1521
|
+
this.opts.onHoldRestored?.(restored, restored.seats ?? [], this.buildHandoff(restored));
|
|
1522
|
+
if (automatic) this.toast("Your held tickets have been restored.", "success");
|
|
1523
|
+
return restored;
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
const status = error?.status;
|
|
1526
|
+
if (status === 404 || status === 409) {
|
|
1527
|
+
this.forgetHold();
|
|
1528
|
+
} else {
|
|
1529
|
+
this.opts.onError?.(error);
|
|
1530
|
+
}
|
|
1531
|
+
return null;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
async restoreRememberedHold() {
|
|
1535
|
+
const holdId = this.rememberedHoldId();
|
|
1536
|
+
if (holdId) await this.resumeHoldFromServer(holdId, true);
|
|
1537
|
+
}
|
|
1222
1538
|
/** Section-bearing objects on the active floor (single-floor → doc.objects). */
|
|
1223
1539
|
activeFloorObjects() {
|
|
1224
1540
|
const doc = this.controller.doc;
|
|
@@ -1473,7 +1789,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1473
1789
|
(r) => `<button type="button" data-rung="${r}" title="${TIP[r]}" aria-pressed="false">${LABEL[r]}</button>`
|
|
1474
1790
|
).join("");
|
|
1475
1791
|
pills.querySelectorAll("button").forEach((btn) => {
|
|
1476
|
-
btn.addEventListener("click", () =>
|
|
1792
|
+
btn.addEventListener("click", () => {
|
|
1793
|
+
const rung = btn.dataset.rung;
|
|
1794
|
+
this.controller.setRung(rung);
|
|
1795
|
+
if (rung === "seats") this.collapseSectionCard();
|
|
1796
|
+
});
|
|
1477
1797
|
});
|
|
1478
1798
|
this.regions["top-center"].appendChild(pills);
|
|
1479
1799
|
this.rungsEl = pills;
|
|
@@ -1524,7 +1844,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1524
1844
|
this.secCardEl?.remove();
|
|
1525
1845
|
this.secCardEl = null;
|
|
1526
1846
|
if (!summary) return;
|
|
1527
|
-
this.secCardCollapsed =
|
|
1847
|
+
this.secCardCollapsed = this.controller.getRung() === "seats";
|
|
1528
1848
|
this.secCardShownAt = Date.now();
|
|
1529
1849
|
this.renderSectionCard(summary);
|
|
1530
1850
|
}
|
|
@@ -1590,6 +1910,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
1590
1910
|
sectionCardOnView() {
|
|
1591
1911
|
if (!this.secCardEl || this.secCardCollapsed || !this.lastSection) return;
|
|
1592
1912
|
if (this.root?.dataset.layout === "narrow") return;
|
|
1913
|
+
if (this.controller.getRung() === "seats") {
|
|
1914
|
+
this.collapseSectionCard();
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1593
1917
|
if (Date.now() - this.secCardShownAt < 1400) {
|
|
1594
1918
|
if (this.sectionCardCoverage() > 0.25) this.collapseSectionCard();
|
|
1595
1919
|
return;
|
|
@@ -1632,36 +1956,78 @@ var SeatPicker = class _SeatPicker {
|
|
|
1632
1956
|
const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
|
|
1633
1957
|
this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
|
|
1634
1958
|
}
|
|
1635
|
-
// ----
|
|
1959
|
+
// ---- seat candidate confirmation ------------------------------------------
|
|
1636
1960
|
showConfirm(seat) {
|
|
1637
|
-
this.
|
|
1961
|
+
const previousId = this.confirmSeat?.id;
|
|
1962
|
+
this.confirmEl?.remove();
|
|
1963
|
+
this.confirmEl = null;
|
|
1964
|
+
this.confirmSeat = seat;
|
|
1965
|
+
this.root?.setAttribute("data-confirming", "true");
|
|
1966
|
+
this.controller.setSelectionFocus(seat.id);
|
|
1967
|
+
if (previousId && previousId !== seat.id) this.controller.deselect([previousId]);
|
|
1638
1968
|
if (this.tipEl) this.tipEl.style.display = "none";
|
|
1969
|
+
const details = this.controller.seatDetails(seat.id);
|
|
1639
1970
|
const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
|
|
1640
|
-
const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
|
|
1971
|
+
const price = details?.price ?? (cat?.tiers?.length ? cat.tiers[0].price : cat?.price);
|
|
1972
|
+
const safe = (value) => String(value ?? "\u2014").replace(/[&<>"]/g, (char) => ({
|
|
1973
|
+
"&": "&",
|
|
1974
|
+
"<": "<",
|
|
1975
|
+
">": ">",
|
|
1976
|
+
'"': """
|
|
1977
|
+
})[char]);
|
|
1641
1978
|
const el = document.createElement("div");
|
|
1642
1979
|
el.className = "sl-confirm";
|
|
1643
|
-
el.
|
|
1980
|
+
el.setAttribute("role", "dialog");
|
|
1981
|
+
el.setAttribute("aria-modal", "true");
|
|
1982
|
+
el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
|
|
1983
|
+
el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
|
|
1984
|
+
el.innerHTML = `<div class="sl-confirm-grid"><div class="sl-confirm-field"><span class="sl-confirm-key">Section</span><span class="sl-confirm-value">${safe(details?.sectionLabel)}</span></div><div class="sl-confirm-field"><span class="sl-confirm-key">Row</span><span class="sl-confirm-value">${safe(details?.rowLabel)}</span></div><div class="sl-confirm-field"><span class="sl-confirm-key">Seat</span><span class="sl-confirm-value">${safe(details?.seatNumber ?? seat.label)}</span></div></div><div class="sl-confirm-cat"><span class="sl-dot" style="background:${cat?.color ?? "#6e7bff"}"></span><span class="sl-confirm-cat-name">${safe(details?.categoryLabel ?? cat?.label ?? seat.categoryKey)}</span>` + (price != null ? `<span class="sl-confirm-price">${this.money(price)}</span>` : "") + `</div><div class="sl-confirm-body">` + (this.seatViewEnabled() ? `<button type="button" class="sl-confirm-view"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>${t2("picker.open360")}</button>` : "") + `<div class="sl-confirm-row"><button type="button" class="sl-confirm-cancel">Cancel</button><button type="button" class="sl-confirm-add"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4 4L19 7"/></svg>Select</button></div></div>`;
|
|
1644
1985
|
this.els.map.appendChild(el);
|
|
1645
1986
|
this.confirmEl = el;
|
|
1646
|
-
this.confirmSeat = seat;
|
|
1647
1987
|
this.reanchorConfirm();
|
|
1648
1988
|
el.querySelector(".sl-confirm-view")?.addEventListener("click", () => this.openSeatView(seat));
|
|
1649
|
-
el.querySelector(".sl-confirm-add").addEventListener("click", () => this.
|
|
1650
|
-
el.querySelector(".sl-confirm-cancel").addEventListener("click", () =>
|
|
1651
|
-
|
|
1652
|
-
this.closeConfirm();
|
|
1653
|
-
});
|
|
1989
|
+
el.querySelector(".sl-confirm-add").addEventListener("click", () => this.commitConfirm());
|
|
1990
|
+
el.querySelector(".sl-confirm-cancel").addEventListener("click", () => this.cancelConfirm());
|
|
1991
|
+
requestAnimationFrame(() => el.querySelector(".sl-confirm-add")?.focus());
|
|
1654
1992
|
}
|
|
1655
1993
|
reanchorConfirm() {
|
|
1656
1994
|
if (!this.confirmEl || !this.confirmSeat) return;
|
|
1657
1995
|
const p = this.controller.worldToScreen({ x: this.confirmSeat.x, y: this.confirmSeat.y });
|
|
1658
|
-
this.
|
|
1659
|
-
this.
|
|
1660
|
-
|
|
1661
|
-
|
|
1996
|
+
if (this.root?.dataset.layout === "narrow") return;
|
|
1997
|
+
const mapWidth = this.els.map.clientWidth;
|
|
1998
|
+
const mapHeight = this.els.map.clientHeight;
|
|
1999
|
+
const cardWidth = this.confirmEl.offsetWidth || 276;
|
|
2000
|
+
const cardHeight = this.confirmEl.offsetHeight || 230;
|
|
2001
|
+
const half = cardWidth / 2 + 12;
|
|
2002
|
+
const x = Math.max(half, Math.min(mapWidth - half, p.x));
|
|
2003
|
+
const belowFits = p.y + cardHeight + 24 <= mapHeight;
|
|
2004
|
+
const placeBelow = p.y < cardHeight + 24 && belowFits;
|
|
2005
|
+
this.confirmEl.dataset.placement = placeBelow ? "below" : "above";
|
|
2006
|
+
this.confirmEl.style.left = `${x}px`;
|
|
2007
|
+
this.confirmEl.style.top = `${Math.max(8, Math.min(mapHeight - 8, p.y))}px`;
|
|
2008
|
+
}
|
|
2009
|
+
dismissConfirm() {
|
|
1662
2010
|
this.confirmEl?.remove();
|
|
1663
2011
|
this.confirmEl = null;
|
|
1664
2012
|
this.confirmSeat = null;
|
|
2013
|
+
this.root?.removeAttribute("data-confirming");
|
|
2014
|
+
this.controller.setSelectionFocus(null);
|
|
2015
|
+
}
|
|
2016
|
+
commitConfirm() {
|
|
2017
|
+
if (!this.confirmSeat) return;
|
|
2018
|
+
this.dismissConfirm();
|
|
2019
|
+
this.collapseSectionCard();
|
|
2020
|
+
this.syncTray();
|
|
2021
|
+
}
|
|
2022
|
+
cancelConfirm() {
|
|
2023
|
+
const seat = this.confirmSeat;
|
|
2024
|
+
if (!seat) return;
|
|
2025
|
+
this.controller.deselect([seat.id]);
|
|
2026
|
+
if (this.confirmSeat) this.dismissConfirm();
|
|
2027
|
+
this.root?.focus({ preventScroll: true });
|
|
2028
|
+
}
|
|
2029
|
+
closeConfirm() {
|
|
2030
|
+
this.dismissConfirm();
|
|
1665
2031
|
}
|
|
1666
2032
|
// ---- 360° view-from-seat modal --------------------------------------------
|
|
1667
2033
|
seatViewEnabled() {
|
|
@@ -1684,7 +2050,6 @@ var SeatPicker = class _SeatPicker {
|
|
|
1684
2050
|
openSeatView(seat) {
|
|
1685
2051
|
if (!this.root || !this.seatViewEnabled()) return;
|
|
1686
2052
|
this.closeSeatView();
|
|
1687
|
-
this.closeConfirm();
|
|
1688
2053
|
const doc = this.controller.doc;
|
|
1689
2054
|
const activeId = this.controller.getActiveFloorId();
|
|
1690
2055
|
const focal = doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
|
|
@@ -1795,7 +2160,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1795
2160
|
const price = c.tiers?.length ? c.tiers[0].price : c.price;
|
|
1796
2161
|
const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
|
|
1797
2162
|
return `<div class="sl-price-row${dim ? " sl-dim" : ""}" data-cat="${c.key}"><span class="sl-dot" style="background:${c.color}"></span><span class="sl-price-label">${c.label}</span><span class="sl-price-left">${left[c.key] ?? 0} left</span>` + (price != null ? `<span class="sl-price-amt">${this.money(price)}</span>` : "") + `</div>`;
|
|
1798
|
-
}).join("")
|
|
2163
|
+
}).join("") + `<div class="sl-status-key" aria-label="Seat status legend"><span class="sl-status-item"><i class="sl-status-icon">H</i>Held by another buyer</span><span class="sl-status-item"><i class="sl-status-icon sold">\xD7</i>Sold</span></div>`;
|
|
1799
2164
|
this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
|
|
1800
2165
|
row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
|
|
1801
2166
|
row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
|
|
@@ -1803,39 +2168,47 @@ var SeatPicker = class _SeatPicker {
|
|
|
1803
2168
|
}
|
|
1804
2169
|
/** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
|
|
1805
2170
|
evictTakenSelections() {
|
|
1806
|
-
const ownLabels = new Set(
|
|
2171
|
+
const ownLabels = /* @__PURE__ */ new Set([
|
|
2172
|
+
...this.controller.currentHold()?.labels ?? [],
|
|
2173
|
+
...this.holdingLabels
|
|
2174
|
+
]);
|
|
1807
2175
|
const gone = this.controller.getSelection().filter((s) => !ownLabels.has(s.label) && (this.controller.getStatus(s.id) ?? "free") !== "free");
|
|
1808
2176
|
if (!gone.length) return;
|
|
1809
2177
|
this.controller.deselect(gone.map((s) => s.id));
|
|
1810
|
-
this.toast(`Seat ${gone[0].label} was just taken by another buyer
|
|
2178
|
+
this.toast(`Seat ${gone[0].label} was just taken by another buyer.`, "error");
|
|
1811
2179
|
}
|
|
1812
2180
|
syncTray() {
|
|
1813
2181
|
if (!this.els.tray) return;
|
|
1814
|
-
const seats = this.
|
|
2182
|
+
const seats = this.committedSelection();
|
|
1815
2183
|
const gaAreas = this.controller.getGAAreas();
|
|
1816
2184
|
const heldItems = this.hold?.items ?? [];
|
|
1817
2185
|
const parts = [];
|
|
2186
|
+
const nextTrayKeys = /* @__PURE__ */ new Set();
|
|
1818
2187
|
if (!seats.length && !heldItems.length && !gaAreas.length) {
|
|
1819
2188
|
parts.push(`<div class="sl-tray-hint">Tap a seat on the map, or let us pick the best available for you.</div>`);
|
|
1820
2189
|
} else if (!seats.length && !heldItems.length) {
|
|
1821
2190
|
parts.push(`<div class="sl-tray-hint">Tap a seat on the map \u2014 or grab standing tickets below.</div>`);
|
|
1822
2191
|
}
|
|
1823
2192
|
for (const item of heldItems) {
|
|
2193
|
+
const itemKey = `held:${item.label}`;
|
|
2194
|
+
nextTrayKeys.add(itemKey);
|
|
1824
2195
|
const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
|
|
1825
2196
|
const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
|
|
1826
2197
|
const canView2 = this.seatViewEnabled() && item.objectType !== "ga" && !!this.controller.seatByLabel(item.label);
|
|
1827
2198
|
parts.push(
|
|
1828
|
-
`<div class="sl-chip"><b>${item.label}</b><span class="cat">${cat?.label ?? item.categoryKey}${tierName ? ` \xB7 ${tierName}` : ""}</span><span class="amt">${this.money(item.unitPrice * (item.quantity ?? 1))}</span>` + (canView2 ? `<button type="button" class="view" data-view-label="${item.label}" aria-label="${t2("picker.viewFromSeat", { label: item.label })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "") +
|
|
2199
|
+
`<div class="sl-chip sl-held${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-held="${encodeURIComponent(item.label)}"><b>${item.label}</b><span class="cat">${cat?.label ?? item.categoryKey}${tierName ? ` \xB7 ${tierName}` : ""}</span><span class="sl-chip-state">Held by you</span><span class="amt">${this.money(item.unitPrice * (item.quantity ?? 1))}</span>` + (canView2 ? `<button type="button" class="view" data-view-label="${item.label}" aria-label="${t2("picker.viewFromSeat", { label: item.label })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "") + `<button type="button" class="rm" aria-label="Remove held ticket ${item.label}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div>`
|
|
1829
2200
|
);
|
|
1830
2201
|
}
|
|
1831
2202
|
const heldLabels = new Set(heldItems.map((item) => item.label));
|
|
1832
2203
|
const canView = this.seatViewEnabled();
|
|
1833
2204
|
for (const s of seats.filter((seat) => !heldLabels.has(seat.label))) {
|
|
2205
|
+
const itemKey = `seat:${s.id}`;
|
|
2206
|
+
nextTrayKeys.add(itemKey);
|
|
1834
2207
|
const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
|
|
1835
2208
|
const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${t2("picker.ticketTierFor", { label: s.label })}">` + s.tiers.map((ti) => `<option value="${ti.id}"${ti.id === s.tierId ? " selected" : ""}>${ti.name} \xB7 ${this.money(ti.price)}</option>`).join("") + `</select>` : "";
|
|
1836
2209
|
const viewBtn = canView ? `<button type="button" class="view" data-view-label="${s.label}" aria-label="${t2("picker.viewFromSeat", { label: s.label })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "";
|
|
1837
2210
|
parts.push(
|
|
1838
|
-
`<div class="sl-chip" data-seat="${s.id}"><b>${s.label}</b><span class="cat">${cat?.label ?? s.categoryKey}</span>` + tierSelect + `<span class="amt">${this.money(s.price)}</span>` + viewBtn + `<button type="button" class="rm" aria-label="Remove ${s.label}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div>`
|
|
2211
|
+
`<div class="sl-chip${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-seat="${s.id}"><b>${s.label}</b><span class="cat">${cat?.label ?? s.categoryKey}</span>` + tierSelect + `<span class="sl-chip-state sl-pending">Selected</span><span class="amt">${this.money(s.price)}</span>` + viewBtn + `<button type="button" class="rm" aria-label="Remove ${s.label}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div>`
|
|
1839
2212
|
);
|
|
1840
2213
|
}
|
|
1841
2214
|
for (const area of gaAreas) {
|
|
@@ -1847,10 +2220,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1847
2220
|
if (!this.hold) {
|
|
1848
2221
|
const cats = this.controller.doc?.categories ?? [];
|
|
1849
2222
|
parts.push(
|
|
1850
|
-
`<div class="sl-ba">` + (cats.length > 1 ? `<select aria-label="Category" data-ba-cat><option value="">Any tier</option>` + cats.map((c) => `<option value="${c.key}"${this.baCat === c.key ? " selected" : ""}>${c.label}</option>`).join("") + `</select>` : "") + `<div class="sl-ba-qty"><button type="button" data-ba="-1" aria-label="Fewer seats">\u2212</button><span>${this.baQty}</span><button type="button" data-ba="1" aria-label="More seats">+</button></div><button type="button" class="sl-ba-go">Best available
|
|
2223
|
+
`<div class="sl-ba">` + (cats.length > 1 ? `<select aria-label="Category" data-ba-cat><option value="">Any tier</option>` + cats.map((c) => `<option value="${c.key}"${this.baCat === c.key ? " selected" : ""}>${c.label}</option>`).join("") + `</select>` : "") + `<div class="sl-ba-qty"><button type="button" data-ba="-1" aria-label="Fewer seats">\u2212</button><span>${this.baQty}</span><button type="button" data-ba="1" aria-label="More seats">+</button></div><button type="button" class="sl-ba-go"${this.bestAvailableBusy ? " disabled" : ""}>` + (this.bestAvailableBusy ? `<span class="sl-ba-spin" aria-hidden="true"></span>Finding\u2026` : "Best available") + `</button></div>`
|
|
1851
2224
|
);
|
|
1852
2225
|
}
|
|
1853
2226
|
this.els.tray.innerHTML = parts.join("");
|
|
2227
|
+
this.lastTrayKeys = nextTrayKeys;
|
|
1854
2228
|
this.els.tray.querySelectorAll("[data-ba]").forEach((btn) => {
|
|
1855
2229
|
btn.addEventListener("click", () => {
|
|
1856
2230
|
this.baQty = Math.max(1, Math.min(8, this.baQty + Number(btn.dataset.ba)));
|
|
@@ -1865,8 +2239,18 @@ var SeatPicker = class _SeatPicker {
|
|
|
1865
2239
|
});
|
|
1866
2240
|
this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
|
|
1867
2241
|
btn.addEventListener("click", () => {
|
|
1868
|
-
const
|
|
1869
|
-
|
|
2242
|
+
const chip = btn.closest(".sl-chip");
|
|
2243
|
+
if (chip.dataset.held) {
|
|
2244
|
+
void this.removeHeldLabel(decodeURIComponent(chip.dataset.held), chip);
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
const id = chip.dataset.seat;
|
|
2248
|
+
if (this.reducedMotion()) {
|
|
2249
|
+
this.controller.deselect([id]);
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
chip.classList.add("sl-leave");
|
|
2253
|
+
this.scheduleMotion(() => this.controller.deselect([id]), 150);
|
|
1870
2254
|
});
|
|
1871
2255
|
});
|
|
1872
2256
|
this.els.tray.querySelectorAll(".sl-chip .tier").forEach((sel) => {
|
|
@@ -1895,39 +2279,85 @@ var SeatPicker = class _SeatPicker {
|
|
|
1895
2279
|
const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
|
|
1896
2280
|
const total = freshSeats.reduce((sum, s) => sum + s.price, 0) + gaTotal + heldTotal;
|
|
1897
2281
|
const count = freshSeats.length + gaCount + heldCount;
|
|
2282
|
+
const pendingCount = this.pendingSelectionCount();
|
|
2283
|
+
const previousCount = this.lastTrayCount;
|
|
2284
|
+
const previousTotal = this.lastTrayTotal;
|
|
1898
2285
|
this.els.count.textContent = count ? `${count} ${count === 1 ? "ticket" : "tickets"}` : "No seats selected";
|
|
1899
2286
|
this.els.total.textContent = count ? this.money(total) : "";
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
2287
|
+
this.syncCta(count, pendingCount);
|
|
2288
|
+
if (this.hold) {
|
|
2289
|
+
const securedCount = heldCount || this.hold.seats?.length || 0;
|
|
2290
|
+
if (this.els.holdTitle) {
|
|
2291
|
+
this.els.holdTitle.textContent = `${securedCount} ${securedCount === 1 ? "seat" : "seats"} secured`;
|
|
2292
|
+
}
|
|
2293
|
+
if (this.els.holdCopy) {
|
|
2294
|
+
this.els.holdCopy.textContent = pendingCount ? `${pendingCount} more selected \u2014 secure before checkout.` : "Checkout timer is running.";
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
if (count !== previousCount) this.animateOnce(this.els.count, "sl-value-pop", 380);
|
|
2298
|
+
if (total !== previousTotal) this.animateOnce(this.els.total, "sl-value-pop", 380);
|
|
2299
|
+
if (previousCount === 0 && count > 0) this.animateOnce(this.els.cta, "sl-ready", 520);
|
|
1903
2300
|
if (this.els.peek) {
|
|
1904
2301
|
if (count) {
|
|
1905
|
-
this.els.peek.innerHTML = `<span>${count} ${count === 1 ? "ticket" : "tickets"} \xB7 ${this.money(total)}</span><span class="go">${this.hold ? "Continue" : "Review"}</span>`;
|
|
2302
|
+
this.els.peek.innerHTML = `<span>${count} ${count === 1 ? "ticket" : "tickets"} \xB7 ${this.money(total)}</span><span class="go">${this.hold ? pendingCount ? "Secure more" : "Continue" : "Review"}</span>`;
|
|
1906
2303
|
} else {
|
|
1907
2304
|
const prices = (this.controller.doc?.categories ?? []).map((c) => this.catPrice(c)).filter((p) => p != null);
|
|
1908
2305
|
this.els.peek.innerHTML = (prices.length ? `<span>From ${this.money(Math.min(...prices))}</span>` : "<span>Pick your seats</span>") + `<span class="sub">\xB7 Best available</span>`;
|
|
1909
2306
|
}
|
|
1910
2307
|
}
|
|
1911
|
-
if (this.root?.dataset.layout === "narrow" && count > 0 && this.lastTrayCount === 0) {
|
|
1912
|
-
this.root.dataset.sheet = "open";
|
|
1913
|
-
}
|
|
1914
2308
|
this.lastTrayCount = count;
|
|
2309
|
+
this.lastTrayTotal = total;
|
|
1915
2310
|
this.opts.onSelectionChange?.(seats);
|
|
1916
2311
|
}
|
|
2312
|
+
async removeHeldLabel(label, chip) {
|
|
2313
|
+
if (!label || this.releasingLabels.has(label)) return false;
|
|
2314
|
+
this.releasingLabels.add(label);
|
|
2315
|
+
chip?.setAttribute("aria-busy", "true");
|
|
2316
|
+
const button = chip?.querySelector(".rm");
|
|
2317
|
+
if (button) button.disabled = true;
|
|
2318
|
+
try {
|
|
2319
|
+
const preserveAcrossNavigation = this.handedOff;
|
|
2320
|
+
const released = await this.controller.releaseLabels([label]);
|
|
2321
|
+
if (!released) {
|
|
2322
|
+
this.toast(`Couldn't remove ${label}. Your hold is unchanged.`, "error");
|
|
2323
|
+
return false;
|
|
2324
|
+
}
|
|
2325
|
+
const remaining = this.controller.currentHold();
|
|
2326
|
+
this.hold = remaining ? { holdId: remaining.holdId, expiresAt: remaining.expiresAt, seats: remaining.seats, items: remaining.items } : null;
|
|
2327
|
+
this.handedOff = !!this.hold && preserveAcrossNavigation;
|
|
2328
|
+
this.bookedShown = false;
|
|
2329
|
+
this.ctaPhase = "idle";
|
|
2330
|
+
if (this.hold) {
|
|
2331
|
+
this.startHoldTimer(this.hold.expiresAt);
|
|
2332
|
+
} else {
|
|
2333
|
+
this.stopHoldTimer();
|
|
2334
|
+
this.forgetHold();
|
|
2335
|
+
}
|
|
2336
|
+
this.syncTray();
|
|
2337
|
+
this.emitHoldChange();
|
|
2338
|
+
this.toast(`${label} removed from your hold.`, "success");
|
|
2339
|
+
return true;
|
|
2340
|
+
} finally {
|
|
2341
|
+
this.releasingLabels.delete(label);
|
|
2342
|
+
chip?.removeAttribute("aria-busy");
|
|
2343
|
+
if (button?.isConnected) button.disabled = false;
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
1917
2346
|
async handleCta() {
|
|
1918
|
-
const
|
|
1919
|
-
if (this.hold && !
|
|
1920
|
-
const seats = this.hold.seats ??
|
|
2347
|
+
const committed = this.committedSelection();
|
|
2348
|
+
if (this.hold && !committed.some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
|
|
2349
|
+
const seats = this.hold.seats ?? committed;
|
|
1921
2350
|
this.handedOff = true;
|
|
2351
|
+
this.setCtaPhase("checkout");
|
|
1922
2352
|
this.opts.onCheckout?.(this.hold, seats, this.buildHandoff(this.hold));
|
|
1923
2353
|
return;
|
|
1924
2354
|
}
|
|
1925
|
-
|
|
1926
|
-
|
|
2355
|
+
this.holdingLabels = new Set(committed.map((seat) => seat.label));
|
|
2356
|
+
this.setCtaPhase("holding");
|
|
1927
2357
|
try {
|
|
1928
2358
|
let hold = null;
|
|
1929
2359
|
const gaEntries = [...this.gaQty.entries()].filter(([, q]) => q > 0);
|
|
1930
|
-
const chosenSeats = this.
|
|
2360
|
+
const chosenSeats = this.committedSelection();
|
|
1931
2361
|
if (chosenSeats.length) {
|
|
1932
2362
|
const h = await this.controller.hold(void 0, this.opts.holdTtlMs);
|
|
1933
2363
|
hold = h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
|
|
@@ -1937,31 +2367,43 @@ var SeatPicker = class _SeatPicker {
|
|
|
1937
2367
|
hold = h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : hold;
|
|
1938
2368
|
}
|
|
1939
2369
|
if (!hold) {
|
|
1940
|
-
this.toast("One or more seats were just taken. Please pick again.");
|
|
2370
|
+
this.toast("One or more seats were just taken. Please pick again.", "error");
|
|
2371
|
+
this.setCtaPhase("idle");
|
|
1941
2372
|
this.syncTray();
|
|
1942
2373
|
return;
|
|
1943
2374
|
}
|
|
1944
2375
|
this.hold = hold;
|
|
1945
2376
|
this.handedOff = true;
|
|
1946
2377
|
this.startHoldTimer(hold.expiresAt);
|
|
1947
|
-
this.
|
|
2378
|
+
this.flashHeldSeats(hold);
|
|
2379
|
+
this.setCtaPhase("checkout");
|
|
2380
|
+
this.emitHoldChange();
|
|
2381
|
+
this.opts.onCheckout?.(hold, hold.seats ?? chosenSeats, this.buildHandoff(hold));
|
|
1948
2382
|
} catch (err) {
|
|
1949
2383
|
this.opts.onError?.(err);
|
|
1950
|
-
this.toast("One or more seats were just taken. Please pick again.");
|
|
2384
|
+
this.toast("One or more seats were just taken. Please pick again.", "error");
|
|
2385
|
+
this.setCtaPhase("idle");
|
|
1951
2386
|
} finally {
|
|
2387
|
+
this.holdingLabels.clear();
|
|
2388
|
+
if (this.ctaPhase === "holding") this.ctaPhase = "idle";
|
|
1952
2389
|
this.syncTray();
|
|
1953
2390
|
}
|
|
1954
2391
|
}
|
|
1955
2392
|
startHoldTimer(expiresAt) {
|
|
1956
2393
|
this.stopHoldTimer();
|
|
1957
2394
|
this.holdExpiresAt = expiresAt;
|
|
2395
|
+
if (this.hold) this.rememberHold(this.hold);
|
|
1958
2396
|
const pill = this.els.hold;
|
|
2397
|
+
pill.innerHTML = '<span class="sl-hold-dot" aria-hidden="true"></span><span>Held</span><span class="sl-hold-time" data-ref="holdTime"></span>';
|
|
2398
|
+
const time = pill.querySelector('[data-ref="holdTime"]');
|
|
2399
|
+
this.els.holdNote?.classList.add("on");
|
|
1959
2400
|
const tick = () => {
|
|
1960
2401
|
const ms = Math.max(0, this.holdExpiresAt - Date.now());
|
|
1961
2402
|
const m = Math.floor(ms / 6e4);
|
|
1962
2403
|
const s = String(Math.floor(ms % 6e4 / 1e3)).padStart(2, "0");
|
|
1963
|
-
|
|
2404
|
+
if (time) time.textContent = `${m}:${s}`;
|
|
1964
2405
|
pill.classList.add("on");
|
|
2406
|
+
pill.classList.toggle("is-expiring", ms > 0 && ms <= EXTEND_PROMPT_MS);
|
|
1965
2407
|
this.setExtendPrompt(ms > 0 && ms <= EXTEND_PROMPT_MS, ms);
|
|
1966
2408
|
if (ms <= 0) this.stopHoldTimer();
|
|
1967
2409
|
};
|
|
@@ -1971,7 +2413,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
1971
2413
|
stopHoldTimer() {
|
|
1972
2414
|
if (this.holdTimer) clearInterval(this.holdTimer);
|
|
1973
2415
|
this.holdTimer = null;
|
|
1974
|
-
this.els.hold?.classList.remove("on");
|
|
2416
|
+
this.els.hold?.classList.remove("on", "is-expiring");
|
|
2417
|
+
this.els.holdNote?.classList.remove("on");
|
|
1975
2418
|
this.setExtendPrompt(false, 0);
|
|
1976
2419
|
}
|
|
1977
2420
|
/** Show/refresh (or hide) the "Need more time?" prompt with the live seconds left. */
|
|
@@ -1996,13 +2439,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
1996
2439
|
this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
|
|
1997
2440
|
this.holdExpiresAt = h.expiresAt;
|
|
1998
2441
|
this.extendEl?.classList.remove("on");
|
|
1999
|
-
this.
|
|
2442
|
+
this.rememberHold(this.hold);
|
|
2443
|
+
this.emitHoldChange();
|
|
2444
|
+
this.toast("More time added \u2014 your seats are still held.", "success");
|
|
2000
2445
|
} else {
|
|
2001
|
-
this.toast("Couldn't add more time \u2014 please head to checkout now.");
|
|
2446
|
+
this.toast("Couldn't add more time \u2014 please head to checkout now.", "warning");
|
|
2002
2447
|
}
|
|
2003
2448
|
} catch (err) {
|
|
2004
2449
|
this.opts.onError?.(err);
|
|
2005
|
-
this.toast("Couldn't add more time \u2014 please head to checkout now.");
|
|
2450
|
+
this.toast("Couldn't add more time \u2014 please head to checkout now.", "warning");
|
|
2006
2451
|
} finally {
|
|
2007
2452
|
btn.disabled = false;
|
|
2008
2453
|
btn.textContent = prev;
|
|
@@ -2026,6 +2471,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2026
2471
|
this.bookedShown = true;
|
|
2027
2472
|
const handoff = this.buildHandoff(this.hold);
|
|
2028
2473
|
this.stopHoldTimer();
|
|
2474
|
+
this.forgetHold();
|
|
2029
2475
|
const n = handoff.lineItems.reduce((sum, i) => sum + i.quantity, 0);
|
|
2030
2476
|
if (this.els.bookedSub) {
|
|
2031
2477
|
this.els.bookedSub.innerHTML = `<span class="sl-booked-seats">${n} ${n === 1 ? "ticket" : "tickets"}</span> confirmed. A confirmation is on its way.`;
|
|
@@ -2050,13 +2496,25 @@ var SeatPicker = class _SeatPicker {
|
|
|
2050
2496
|
const total = lineItems.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
|
|
2051
2497
|
return { holdId: hold.holdId, expiresAt: hold.expiresAt, currency, lineItems, total };
|
|
2052
2498
|
}
|
|
2053
|
-
|
|
2499
|
+
emitHoldChange() {
|
|
2500
|
+
const hold = this.hold;
|
|
2501
|
+
this.opts.onHoldChange?.(
|
|
2502
|
+
hold,
|
|
2503
|
+
hold?.seats ?? [],
|
|
2504
|
+
hold ? this.buildHandoff(hold) : null
|
|
2505
|
+
);
|
|
2506
|
+
}
|
|
2507
|
+
toast(msg, tone = "neutral") {
|
|
2054
2508
|
const el = this.els.toast;
|
|
2055
2509
|
if (!el) return;
|
|
2056
2510
|
el.textContent = msg;
|
|
2511
|
+
el.dataset.tone = tone;
|
|
2057
2512
|
el.classList.add("on");
|
|
2058
2513
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
2059
|
-
this.toastTimer = setTimeout(() =>
|
|
2514
|
+
this.toastTimer = setTimeout(() => {
|
|
2515
|
+
el.classList.remove("on");
|
|
2516
|
+
el.dataset.tone = "neutral";
|
|
2517
|
+
}, 4200);
|
|
2060
2518
|
}
|
|
2061
2519
|
placeTooltip() {
|
|
2062
2520
|
if (!this.tipEl) return;
|
|
@@ -2077,15 +2535,40 @@ var SeatPicker = class _SeatPicker {
|
|
|
2077
2535
|
return;
|
|
2078
2536
|
}
|
|
2079
2537
|
const statusLine = details.status === "free" ? "" : `<div style="margin-top:5px;font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;font-weight:700">${details.status === "held" ? t2("map.statusHeld") : t2("map.statusTaken")}</div>`;
|
|
2080
|
-
|
|
2538
|
+
const location = [
|
|
2539
|
+
details.sectionLabel ? `Section ${details.sectionLabel}` : "",
|
|
2540
|
+
details.rowLabel ? `Row ${details.rowLabel}` : "",
|
|
2541
|
+
details.seatNumber ? `Seat ${details.seatNumber}` : details.label
|
|
2542
|
+
].filter(Boolean).join(" \xB7 ");
|
|
2543
|
+
this.tipEl.innerHTML = `<div style="font-weight:800;font-size:13px">${location}</div><div style="display:flex;align-items:center;gap:6px;margin-top:4px"><span style="width:9px;height:9px;border-radius:50%;flex:none;background:${details.categoryColor}"></span><span style="opacity:.75">${details.categoryLabel}</span><span style="margin-left:auto;font-weight:800">${this.money(details.price)}</span></div>` + statusLine;
|
|
2081
2544
|
this.tipEl.style.display = "block";
|
|
2082
2545
|
this.placeTooltip();
|
|
2083
2546
|
}
|
|
2084
2547
|
// ---- public conveniences ----------------------------------------------------
|
|
2085
2548
|
getSelection() {
|
|
2086
|
-
return this.
|
|
2549
|
+
return this.committedSelection();
|
|
2550
|
+
}
|
|
2551
|
+
/** Current active/restored hold reflected in the tray. */
|
|
2552
|
+
getCurrentHold() {
|
|
2553
|
+
return this.hold;
|
|
2554
|
+
}
|
|
2555
|
+
/** Explicit host-driven hold restore (automatic session restore is on by default). */
|
|
2556
|
+
async resumeHold(holdId) {
|
|
2557
|
+
return this.resumeHoldFromServer(holdId, false);
|
|
2558
|
+
}
|
|
2559
|
+
/** Remove one server-held ticket while keeping the rest of the hold active. */
|
|
2560
|
+
async removeHeldTicket(label) {
|
|
2561
|
+
return this.removeHeldLabel(label);
|
|
2087
2562
|
}
|
|
2088
2563
|
async bestAvailable(qty, categoryKey) {
|
|
2564
|
+
if (this.bestAvailableBusy) return null;
|
|
2565
|
+
if (this.confirmSeat) this.cancelConfirm();
|
|
2566
|
+
this.bestAvailableBusy = true;
|
|
2567
|
+
const button = this.els.tray?.querySelector(".sl-ba-go");
|
|
2568
|
+
if (button) {
|
|
2569
|
+
button.disabled = true;
|
|
2570
|
+
button.innerHTML = '<span class="sl-ba-spin" aria-hidden="true"></span>Finding\u2026';
|
|
2571
|
+
}
|
|
2089
2572
|
try {
|
|
2090
2573
|
const h = await this.controller.bestAvailable(qty, categoryKey);
|
|
2091
2574
|
if (h) {
|
|
@@ -2093,30 +2576,67 @@ var SeatPicker = class _SeatPicker {
|
|
|
2093
2576
|
this.handedOff = false;
|
|
2094
2577
|
this.bookedShown = false;
|
|
2095
2578
|
this.startHoldTimer(h.expiresAt);
|
|
2579
|
+
this.flashHeldSeats(this.hold);
|
|
2096
2580
|
this.syncTray();
|
|
2581
|
+
this.emitHoldChange();
|
|
2097
2582
|
return this.hold;
|
|
2098
2583
|
}
|
|
2099
2584
|
return null;
|
|
2100
2585
|
} catch (err) {
|
|
2101
2586
|
this.opts.onError?.(err);
|
|
2587
|
+
this.toast("Those seats are no longer available. Try another quantity or category.", "error");
|
|
2102
2588
|
return null;
|
|
2589
|
+
} finally {
|
|
2590
|
+
this.bestAvailableBusy = false;
|
|
2591
|
+
if (!this.hold && button?.isConnected) {
|
|
2592
|
+
button.disabled = false;
|
|
2593
|
+
button.textContent = "Best available";
|
|
2594
|
+
}
|
|
2103
2595
|
}
|
|
2104
2596
|
}
|
|
2105
2597
|
async release() {
|
|
2106
|
-
|
|
2598
|
+
const tracked = this.hold;
|
|
2599
|
+
const controllerHold = this.controller.currentHold();
|
|
2600
|
+
let released = true;
|
|
2601
|
+
if (controllerHold) {
|
|
2602
|
+
released = await this.controller.release();
|
|
2603
|
+
} else if (tracked) {
|
|
2604
|
+
const labels = [.../* @__PURE__ */ new Set([
|
|
2605
|
+
...(tracked.items ?? []).map((item) => item.label),
|
|
2606
|
+
...(tracked.seats ?? []).map((seat) => seat.label)
|
|
2607
|
+
])];
|
|
2608
|
+
if (labels.length) {
|
|
2609
|
+
try {
|
|
2610
|
+
await this.api.release(this.opts.event, labels, tracked.holdId);
|
|
2611
|
+
} catch (error) {
|
|
2612
|
+
this.opts.onError?.(error);
|
|
2613
|
+
released = false;
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
if (!released) {
|
|
2618
|
+
this.toast("Couldn't release your tickets. Your hold is unchanged.", "error");
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2107
2621
|
this.hold = null;
|
|
2622
|
+
this.forgetHold();
|
|
2108
2623
|
this.handedOff = false;
|
|
2109
2624
|
this.bookedShown = false;
|
|
2625
|
+
this.ctaPhase = "idle";
|
|
2110
2626
|
this.stopHoldTimer();
|
|
2111
2627
|
this.gaQty.clear();
|
|
2112
2628
|
this.syncTray();
|
|
2629
|
+
this.emitHoldChange();
|
|
2113
2630
|
}
|
|
2114
2631
|
destroy() {
|
|
2115
2632
|
this.destroyed = true;
|
|
2633
|
+
if (this.hold && !this.handedOff) void this.controller.release();
|
|
2116
2634
|
this.closeConfirm();
|
|
2117
2635
|
this.closeSeatView();
|
|
2118
2636
|
this.stopHoldTimer();
|
|
2119
2637
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
2638
|
+
for (const timer of this.motionTimers) clearTimeout(timer);
|
|
2639
|
+
this.motionTimers.clear();
|
|
2120
2640
|
this.ro?.disconnect();
|
|
2121
2641
|
this.ro = null;
|
|
2122
2642
|
if (this.escHandler) document.removeEventListener("keydown", this.escHandler);
|