@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.cjs
CHANGED
|
@@ -84,6 +84,12 @@ var PubApi = class {
|
|
|
84
84
|
body: { qty, ...categoryKey ? { categoryKey } : {} }
|
|
85
85
|
});
|
|
86
86
|
}
|
|
87
|
+
resume(key, holdId) {
|
|
88
|
+
return request(this.base, `/pub/events/${encodeURIComponent(key)}/hold/resume`, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
body: { holdId }
|
|
91
|
+
});
|
|
92
|
+
}
|
|
87
93
|
release(key, labels, holdId) {
|
|
88
94
|
return request(this.base, `/pub/events/${encodeURIComponent(key)}/release`, {
|
|
89
95
|
method: "POST",
|
|
@@ -140,6 +146,7 @@ var SeatingChart = class {
|
|
|
140
146
|
currency: options.currency,
|
|
141
147
|
onSelectionChange: (seats) => this.opts.onSelectionChange?.(seats),
|
|
142
148
|
onHold: (h) => this.opts.onHold?.({ holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items }),
|
|
149
|
+
onHoldRestored: (h) => this.opts.onHoldRestored?.({ holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items }),
|
|
143
150
|
onHoldExpired: () => this.opts.onHoldExpired?.(),
|
|
144
151
|
onGAClick: (areaId) => {
|
|
145
152
|
const area = this.controller.getGAAreas().find((candidate) => candidate.id === areaId);
|
|
@@ -243,6 +250,21 @@ var SeatingChart = class {
|
|
|
243
250
|
return null;
|
|
244
251
|
}
|
|
245
252
|
}
|
|
253
|
+
/** Restore an active hold by its opaque id without extending its expiry. */
|
|
254
|
+
async resumeHold(holdId) {
|
|
255
|
+
try {
|
|
256
|
+
const h = await this.controller.resumeHold(holdId);
|
|
257
|
+
return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
|
|
258
|
+
} catch (err) {
|
|
259
|
+
this.opts.onError?.(err);
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/** Current active hold known to this chart, if any. */
|
|
264
|
+
getCurrentHold() {
|
|
265
|
+
const h = this.controller.currentHold();
|
|
266
|
+
return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
|
|
267
|
+
}
|
|
246
268
|
getGAAreas() {
|
|
247
269
|
return this.controller.getGAAreas();
|
|
248
270
|
}
|
|
@@ -311,6 +333,10 @@ var SeatingChart = class {
|
|
|
311
333
|
async release() {
|
|
312
334
|
await this.controller.release();
|
|
313
335
|
}
|
|
336
|
+
/** Release selected labels from the current hold while keeping the remainder. */
|
|
337
|
+
async releaseLabels(labels) {
|
|
338
|
+
return this.controller.releaseLabels(labels);
|
|
339
|
+
}
|
|
314
340
|
/** Tear everything down: close the socket, stop timers, drop the canvas. */
|
|
315
341
|
destroy() {
|
|
316
342
|
if (this.hostEl && this.onTipMove) this.hostEl.removeEventListener("mousemove", this.onTipMove);
|
|
@@ -458,8 +484,12 @@ var CSS = `
|
|
|
458
484
|
.sl-head-meta{font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--sl-muted);margin-top:3px;
|
|
459
485
|
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
|
|
460
486
|
.sl-hold-pill{display:none;align-items:center;gap:6px;padding:6px 12px;border-radius:999px;flex:none;
|
|
461
|
-
background:var(--sl-accent);color:var(--sl-accent-ink);font-weight:700;font-size:12px;font-variant-numeric:tabular-nums
|
|
462
|
-
|
|
487
|
+
background:var(--sl-accent);color:var(--sl-accent-ink);font-weight:700;font-size:12px;font-variant-numeric:tabular-nums;
|
|
488
|
+
transform-origin:right center}
|
|
489
|
+
.sl-hold-pill.on{display:inline-flex;animation:slPillIn .34s cubic-bezier(.2,.8,.2,1) both}
|
|
490
|
+
.sl-hold-dot{width:7px;height:7px;border-radius:50%;background:currentColor;opacity:.78;box-shadow:0 0 0 0 currentColor}
|
|
491
|
+
.sl-hold-pill.is-expiring .sl-hold-dot{animation:slHoldPulse 1.4s ease-out infinite}
|
|
492
|
+
.sl-hold-time{min-width:3.35em;text-align:left}
|
|
463
493
|
.sl-close{width:32px;height:32px;border-radius:999px;flex:none;display:none;align-items:center;justify-content:center;
|
|
464
494
|
border:1px solid var(--sl-line);color:var(--sl-muted);transition:color .15s,border-color .15s}
|
|
465
495
|
.sl-close:hover{color:var(--sl-text);border-color:var(--sl-muted)}
|
|
@@ -481,11 +511,20 @@ var CSS = `
|
|
|
481
511
|
.sl-picker[data-layout="narrow"] .sl-body{flex-direction:column}
|
|
482
512
|
.sl-picker[data-layout="narrow"] .sl-map{min-height:0;flex:1}
|
|
483
513
|
.sl-picker[data-layout="narrow"] .sl-side{width:100%;border-left:0;border-top:1px solid var(--sl-line);
|
|
484
|
-
flex:none;height:50%;transition:height .
|
|
514
|
+
flex:none;height:50%;transition:height .3s cubic-bezier(.2,.8,.2,1)}
|
|
485
515
|
.sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side{height:86px;overflow:hidden}
|
|
486
516
|
.sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side > :not(.sl-sheet-head){display:none}
|
|
487
517
|
.sl-picker[data-layout="narrow"] .sl-tray{flex:none}
|
|
488
518
|
.sl-picker[data-layout="narrow"] .sl-foot{position:sticky;bottom:0;background:var(--sl-bg)}
|
|
519
|
+
.sl-picker[data-layout="narrow"] .sl-sheet-head{order:0}
|
|
520
|
+
.sl-picker[data-layout="narrow"] .sl-seats-sec{order:1}
|
|
521
|
+
.sl-picker[data-layout="narrow"] .sl-tray{order:2}
|
|
522
|
+
.sl-picker[data-layout="narrow"] .sl-filtersec{order:3}
|
|
523
|
+
.sl-picker[data-layout="narrow"] .sl-filters{order:4}
|
|
524
|
+
.sl-picker[data-layout="narrow"] .sl-prices-sec{order:5}
|
|
525
|
+
.sl-picker[data-layout="narrow"] .sl-pricef{order:6}
|
|
526
|
+
.sl-picker[data-layout="narrow"] .sl-prices{order:7}
|
|
527
|
+
.sl-picker[data-layout="narrow"] .sl-foot{order:8}
|
|
489
528
|
/* touch chrome: pinch-zoom exists \u2014 hide +/\u2212 on the sheet layout (keep fit) */
|
|
490
529
|
.sl-picker[data-layout="narrow"] .sl-zoom [data-ref="zin"],
|
|
491
530
|
.sl-picker[data-layout="narrow"] .sl-zoom [data-ref="zout"]{display:none}
|
|
@@ -532,15 +571,27 @@ var CSS = `
|
|
|
532
571
|
.sl-price-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
|
|
533
572
|
.sl-price-left{font-size:11px;color:var(--sl-muted);font-variant-numeric:tabular-nums}
|
|
534
573
|
.sl-price-amt{font-weight:800;font-variant-numeric:tabular-nums}
|
|
574
|
+
.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}
|
|
575
|
+
.sl-status-item{display:inline-flex;align-items:center;gap:6px}
|
|
576
|
+
.sl-status-icon{width:17px;height:17px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;
|
|
577
|
+
color:#fff;background:#6b7280;font-size:9px;font-weight:900;line-height:1}
|
|
578
|
+
.sl-status-icon.sold{background:#374151;font-size:14px}
|
|
535
579
|
|
|
536
580
|
/* tray */
|
|
537
581
|
.sl-tray{flex:1;padding:10px 16px;display:flex;flex-direction:column;gap:8px;min-height:0}
|
|
538
582
|
.sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
|
|
539
583
|
.sl-chip{display:flex;align-items:center;gap:9px;padding:9px 11px;border:1px solid var(--sl-line);
|
|
540
|
-
border-radius:var(--sl-r-sm);background:var(--sl-surface);font-size:13px}
|
|
584
|
+
border-radius:var(--sl-r-sm);background:var(--sl-surface);font-size:13px;transform-origin:center}
|
|
585
|
+
.sl-chip.sl-enter{animation:slChipIn .38s cubic-bezier(.2,.8,.2,1) both}
|
|
586
|
+
.sl-chip.sl-leave{pointer-events:none;animation:slChipOut .16s ease-in both}
|
|
587
|
+
.sl-chip.sl-held{border-color:var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
|
|
588
|
+
box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent)}
|
|
541
589
|
.sl-chip b{font-weight:800}
|
|
542
590
|
.sl-chip .cat{color:var(--sl-muted);font-size:11.5px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
543
591
|
.sl-chip .amt{font-weight:700;font-variant-numeric:tabular-nums}
|
|
592
|
+
.sl-chip-state{flex:none;padding:3px 6px;border-radius:999px;background:var(--sl-accent);color:var(--sl-accent-ink);
|
|
593
|
+
font-size:8.5px;line-height:1;font-weight:900;letter-spacing:.08em;text-transform:uppercase}
|
|
594
|
+
.sl-chip-state.sl-pending{background:transparent;color:var(--sl-muted);border:1px solid var(--sl-line)}
|
|
544
595
|
.sl-chip .rm{width:22px;height:22px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;color:var(--sl-muted)}
|
|
545
596
|
.sl-chip .rm:hover{color:var(--sl-text)}
|
|
546
597
|
.sl-chip .rm svg{width:11px;height:11px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round}
|
|
@@ -558,17 +609,30 @@ var CSS = `
|
|
|
558
609
|
|
|
559
610
|
/* footer */
|
|
560
611
|
.sl-foot{padding:12px 16px 14px;border-top:1px solid var(--sl-line);flex:none}
|
|
612
|
+
.sl-hold-note{display:none;align-items:center;gap:9px;margin-bottom:10px;padding:9px 10px;border-radius:var(--sl-r-sm);
|
|
613
|
+
border:1px solid var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
|
|
614
|
+
box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent);
|
|
615
|
+
font-size:11.5px;line-height:1.35;color:var(--sl-muted)}
|
|
616
|
+
.sl-hold-note.on{display:flex;animation:slNoticeIn .38s cubic-bezier(.2,.8,.2,1) both}
|
|
617
|
+
.sl-hold-note svg{width:16px;height:16px;flex:none;stroke:var(--sl-accent);stroke-width:2.4;fill:none;
|
|
618
|
+
stroke-linecap:round;stroke-linejoin:round}
|
|
619
|
+
.sl-hold-note b{display:block;color:var(--sl-text);font-size:12px}
|
|
620
|
+
.sl-hold-copy{display:block}
|
|
561
621
|
.sl-total{display:flex;justify-content:space-between;align-items:center;font-size:13px;margin-bottom:10px}
|
|
562
622
|
.sl-total b{font-size:17px;font-variant-numeric:tabular-nums}
|
|
623
|
+
.sl-value-pop{animation:slValuePop .32s cubic-bezier(.2,.8,.2,1)}
|
|
563
624
|
/* Primary checkout CTA. Scoped under .sl-picker so it OUTWEIGHS the
|
|
564
625
|
'.sl-picker button' reset (0,1,1) \u2014 an unscoped '.sl-cta' (0,1,0) loses to it
|
|
565
626
|
and the button renders as plain text with no accent fill. */
|
|
566
627
|
.sl-picker .sl-cta{display:flex;align-items:center;justify-content:center;width:100%;min-height:44px;
|
|
567
628
|
padding:12px 16px;border-radius:var(--sl-r-sm);font-weight:800;font-size:14px;line-height:1.1;
|
|
568
629
|
background:var(--sl-accent);color:var(--sl-accent-ink);
|
|
569
|
-
transition:filter .15s,background .
|
|
630
|
+
transition:filter .15s,background .22s,color .22s,transform .12s,box-shadow .22s;gap:8px}
|
|
570
631
|
.sl-picker .sl-cta:hover{filter:brightness(1.08)}
|
|
571
632
|
.sl-picker .sl-cta:active{transform:translateY(1px);filter:brightness(.94)}
|
|
633
|
+
.sl-picker .sl-cta.sl-ready{animation:slCtaReady .42s cubic-bezier(.2,.8,.2,1)}
|
|
634
|
+
.sl-cta-spin,.sl-ba-spin{width:14px;height:14px;border-radius:50%;border:2px solid currentColor;border-right-color:transparent;
|
|
635
|
+
animation:slspin .7s linear infinite;flex:none}
|
|
572
636
|
/* Disabled ("Select seats"): quieter, but still a full-width button shape. */
|
|
573
637
|
.sl-picker .sl-cta:disabled{background:var(--sl-surface);color:var(--sl-muted);opacity:1;
|
|
574
638
|
cursor:not-allowed;filter:none;transform:none}
|
|
@@ -606,11 +670,15 @@ var CSS = `
|
|
|
606
670
|
.sl-zoom svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
607
671
|
|
|
608
672
|
/* toast + boot states (toast flows in the bottom-center region) */
|
|
609
|
-
.sl-toast{transform:translateY(6px);max-width:100%;
|
|
673
|
+
.sl-toast{transform:translateY(6px) scale(.98);max-width:100%;
|
|
610
674
|
background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-text);border-radius:999px;padding:9px 16px;
|
|
611
|
-
font-size:12.5px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .
|
|
675
|
+
font-size:12.5px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .22s,transform .22s;white-space:nowrap;
|
|
612
676
|
overflow:hidden;text-overflow:ellipsis}
|
|
613
|
-
.sl-toast.on{opacity:1;transform:translateY(0)}
|
|
677
|
+
.sl-toast.on{opacity:1;transform:translateY(0) scale(1)}
|
|
678
|
+
.sl-toast[data-tone="error"]{border-color:#ef4444}
|
|
679
|
+
.sl-toast[data-tone="warning"]{border-color:var(--sl-accent)}
|
|
680
|
+
.sl-toast[data-tone="success"]{border-color:#22c55e}
|
|
681
|
+
.sl-toast.on[data-tone="error"]{animation:slToastNudge .32s ease-out}
|
|
614
682
|
.sl-boot{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|
615
683
|
gap:10px;background:var(--sl-bg);font-size:13px;font-weight:600;color:var(--sl-muted)}
|
|
616
684
|
.sl-boot-spin{width:24px;height:24px;border-radius:50%;border:3px solid var(--sl-line);border-top-color:var(--sl-accent);
|
|
@@ -634,15 +702,22 @@ var CSS = `
|
|
|
634
702
|
.sl-extend-btn:disabled{opacity:.5;cursor:not-allowed}
|
|
635
703
|
|
|
636
704
|
/* booked confirmation overlay (covers the widget once the held seats are sold) */
|
|
637
|
-
.sl-booked{position:absolute;inset:0;z-index:11;display:
|
|
638
|
-
justify-content:center;gap:12px;text-align:center;padding:28px;background:var(--sl-bg)
|
|
639
|
-
|
|
705
|
+
.sl-booked{position:absolute;inset:0;z-index:11;display:flex;flex-direction:column;align-items:center;
|
|
706
|
+
justify-content:center;gap:12px;text-align:center;padding:28px;background:var(--sl-bg);opacity:0;visibility:hidden;
|
|
707
|
+
pointer-events:none;transition:opacity .34s ease,visibility 0s linear .34s}
|
|
708
|
+
.sl-booked.on{opacity:1;visibility:visible;pointer-events:auto;transition:opacity .34s ease,visibility 0s}
|
|
640
709
|
.sl-booked-badge{width:60px;height:60px;border-radius:999px;display:flex;align-items:center;justify-content:center;
|
|
641
|
-
background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
642
|
-
.sl-booked-badge
|
|
710
|
+
background:var(--sl-accent);color:var(--sl-accent-ink);transform:scale(.72)}
|
|
711
|
+
.sl-booked.on .sl-booked-badge{animation:slSuccessPop .58s cubic-bezier(.2,1.25,.3,1) .08s both}
|
|
712
|
+
.sl-booked-badge svg{width:30px;height:30px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round;
|
|
713
|
+
stroke-dasharray:30;stroke-dashoffset:30}
|
|
714
|
+
.sl-booked.on .sl-booked-badge svg{animation:slCheckDraw .42s ease-out .32s forwards}
|
|
643
715
|
.sl-booked-title{font-weight:800;font-size:19px;color:var(--sl-text)}
|
|
644
716
|
.sl-booked-sub{font-size:13px;color:var(--sl-muted);line-height:1.5;max-width:320px}
|
|
645
717
|
.sl-booked-seats{font-weight:700;color:var(--sl-text)}
|
|
718
|
+
.sl-booked.on .sl-booked-title,.sl-booked.on .sl-booked-sub{animation:slCopyRise .42s ease-out both}
|
|
719
|
+
.sl-booked.on .sl-booked-title{animation-delay:.22s}
|
|
720
|
+
.sl-booked.on .sl-booked-sub{animation-delay:.3s}
|
|
646
721
|
|
|
647
722
|
/* a11y filter chips (flow within the top-left region) */
|
|
648
723
|
.sl-chips{display:flex;gap:6px;flex-wrap:wrap}
|
|
@@ -651,28 +726,49 @@ var CSS = `
|
|
|
651
726
|
.sl-chip-f:hover{color:var(--sl-text)}
|
|
652
727
|
.sl-chip-f.on{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
|
|
653
728
|
|
|
654
|
-
/* confirm
|
|
655
|
-
.
|
|
656
|
-
|
|
657
|
-
.sl-
|
|
658
|
-
.sl-
|
|
659
|
-
.sl-confirm
|
|
729
|
+
/* confirm card: a candidate is not in the tray until Select. Map gestures and
|
|
730
|
+
floating chrome pause while the card owns focus, keeping the camera stable. */
|
|
731
|
+
.sl-picker[data-confirming="true"] .sl-map-host>:not(.sl-confirm){pointer-events:none}
|
|
732
|
+
.sl-picker[data-confirming="true"] .sl-anchor{pointer-events:none;opacity:.28;transition:opacity .16s}
|
|
733
|
+
.sl-picker[data-confirming="true"] .sl-side{pointer-events:none;opacity:.58;transition:opacity .16s}
|
|
734
|
+
.sl-confirm{position:absolute;z-index:10;width:276px;max-width:calc(100% - 24px);overflow:hidden;pointer-events:auto;
|
|
735
|
+
background:var(--sl-surface);border:1px solid color-mix(in srgb,var(--sl-line) 70%,var(--sl-text));
|
|
736
|
+
border-radius:15px;box-shadow:0 24px 64px -18px rgba(0,0,0,.72);transform:translate(-50%,calc(-100% - 16px));
|
|
737
|
+
animation:slConfirmIn .24s cubic-bezier(.2,.8,.2,1) both}
|
|
738
|
+
.sl-confirm[data-placement="below"]{transform:translate(-50%,16px);animation:slConfirmBelowIn .24s cubic-bezier(.2,.8,.2,1) both}
|
|
739
|
+
.sl-confirm-grid{display:grid;grid-template-columns:1.2fr .8fr .8fr;border-bottom:1px solid var(--sl-line)}
|
|
740
|
+
.sl-confirm-field{min-width:0;padding:12px 11px 10px;border-right:1px solid var(--sl-line)}
|
|
741
|
+
.sl-confirm-field:last-child{border-right:0;text-align:center}
|
|
742
|
+
.sl-confirm-field:nth-child(2){text-align:center}
|
|
743
|
+
.sl-confirm-key{display:block;font-size:8.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
|
|
744
|
+
.sl-confirm-value{display:block;margin-top:4px;color:var(--sl-text);font-size:17px;line-height:1.1;font-weight:850;
|
|
745
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
746
|
+
.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))}
|
|
747
|
+
.sl-confirm-cat .sl-dot{border:2px solid rgba(255,255,255,.78);width:11px;height:11px}
|
|
748
|
+
.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}
|
|
749
|
+
.sl-confirm-price{font-size:17px;font-weight:850;color:#fff;font-variant-numeric:tabular-nums}
|
|
750
|
+
.sl-confirm-body{padding:11px 12px 12px}
|
|
660
751
|
.sl-confirm-row{display:flex;gap:8px;margin-top:10px}
|
|
661
|
-
.sl-confirm-row button{flex:1;padding:
|
|
662
|
-
.sl-confirm-add{background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
663
|
-
.sl-confirm-
|
|
752
|
+
.sl-confirm-row button{flex:1;min-height:44px;padding:9px 12px;border-radius:9px;font-weight:800;font-size:13px}
|
|
753
|
+
.sl-confirm-add{background:var(--sl-accent)!important;color:var(--sl-accent-ink)!important;display:flex;align-items:center;justify-content:center;gap:7px}
|
|
754
|
+
.sl-confirm-add svg{width:16px;height:16px;stroke:currentColor;stroke-width:2.8;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
755
|
+
.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}
|
|
664
756
|
.sl-confirm-cancel:hover{color:var(--sl-text)}
|
|
757
|
+
.sl-picker[data-layout="narrow"] .sl-confirm{left:50%!important;top:auto!important;bottom:14px;width:min(342px,calc(100% - 24px));
|
|
758
|
+
transform:translateX(-50%);animation:slConfirmMobileIn .24s cubic-bezier(.2,.8,.2,1) both}
|
|
665
759
|
|
|
666
760
|
/* best-available row */
|
|
667
|
-
.sl-ba{display:
|
|
761
|
+
.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)}
|
|
668
762
|
.sl-ba select{background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:7px;
|
|
669
|
-
font:inherit;font-size:12px;padding:
|
|
763
|
+
font:inherit;font-size:12px;padding:7px 8px;min-width:0;width:100%;max-width:none}
|
|
670
764
|
.sl-ba-qty{display:flex;align-items:center;gap:7px}
|
|
671
765
|
.sl-ba-qty button{width:24px;height:24px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
672
766
|
font-size:14px;font-weight:700;display:flex;align-items:center;justify-content:center}
|
|
673
767
|
.sl-ba-qty span{min-width:14px;text-align:center;font-weight:800}
|
|
674
|
-
.sl-ba-go{
|
|
768
|
+
.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;
|
|
769
|
+
transition:border-color .15s,opacity .15s;display:flex;align-items:center;justify-content:center;gap:6px}
|
|
675
770
|
.sl-ba-go:hover{border-color:var(--sl-muted)}
|
|
771
|
+
.sl-ba-go:disabled{opacity:.62;cursor:wait}
|
|
676
772
|
|
|
677
773
|
/* screen-reader live region */
|
|
678
774
|
.sl-sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
|
@@ -778,6 +874,29 @@ var CSS = `
|
|
|
778
874
|
.sl-price-row.sl-dim{opacity:.4}
|
|
779
875
|
.sl-seccard-mix-item.sl-dim{opacity:.4}
|
|
780
876
|
|
|
877
|
+
/* Buyer-journey motion: every animation explains a state transition (selected,
|
|
878
|
+
held, checkout handoff, conflict or booked). No decorative infinite motion
|
|
879
|
+
except the expiring-hold pulse and active progress spinners. */
|
|
880
|
+
@keyframes slPillIn{from{opacity:0;transform:translateX(7px) scale(.9)}to{opacity:1;transform:translateX(0) scale(1)}}
|
|
881
|
+
@keyframes slHoldPulse{0%{box-shadow:0 0 0 0 currentColor;opacity:.9}75%,100%{box-shadow:0 0 0 7px transparent;opacity:.55}}
|
|
882
|
+
@keyframes slChipIn{from{opacity:0;transform:translateY(8px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}
|
|
883
|
+
@keyframes slChipOut{to{opacity:0;transform:translateX(10px) scale(.98)}}
|
|
884
|
+
@keyframes slNoticeIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
|
|
885
|
+
@keyframes slValuePop{0%{opacity:.6;transform:translateY(3px)}55%{transform:translateY(-1px) scale(1.05)}100%{opacity:1;transform:none}}
|
|
886
|
+
@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}}
|
|
887
|
+
@keyframes slToastNudge{0%,100%{margin-left:0}30%{margin-left:-4px}60%{margin-left:3px}}
|
|
888
|
+
@keyframes slSuccessPop{0%{opacity:0;transform:scale(.72)}65%{opacity:1;transform:scale(1.08)}100%{opacity:1;transform:scale(1)}}
|
|
889
|
+
@keyframes slCheckDraw{to{stroke-dashoffset:0}}
|
|
890
|
+
@keyframes slCopyRise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
|
891
|
+
@keyframes slConfirmIn{from{opacity:0;transform:translate(-50%,calc(-100% - 8px)) scale(.96)}to{opacity:1;transform:translate(-50%,calc(-100% - 14px)) scale(1)}}
|
|
892
|
+
@keyframes slConfirmBelowIn{from{opacity:0;transform:translate(-50%,8px) scale(.96)}to{opacity:1;transform:translate(-50%,16px) scale(1)}}
|
|
893
|
+
@keyframes slConfirmMobileIn{from{opacity:0;transform:translate(-50%,10px) scale(.97)}to{opacity:1;transform:translate(-50%,0) scale(1)}}
|
|
894
|
+
|
|
895
|
+
@media(prefers-reduced-motion:reduce){
|
|
896
|
+
.sl-picker *,.sl-modal-scrim *{animation-duration:.001ms!important;animation-iteration-count:1!important;
|
|
897
|
+
transition-duration:.001ms!important;scroll-behavior:auto!important}
|
|
898
|
+
}
|
|
899
|
+
|
|
781
900
|
/* modal host */
|
|
782
901
|
.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}
|
|
783
902
|
.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)}
|
|
@@ -818,6 +937,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
818
937
|
this.ro = null;
|
|
819
938
|
this.holdTimer = null;
|
|
820
939
|
this.toastTimer = null;
|
|
940
|
+
/** Short-lived UI motion timers; all are cancelled on destroy. */
|
|
941
|
+
this.motionTimers = /* @__PURE__ */ new Set();
|
|
821
942
|
// state
|
|
822
943
|
this.currency = "USD";
|
|
823
944
|
this.hold = null;
|
|
@@ -861,6 +982,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
861
982
|
this.secCardShownAt = 0;
|
|
862
983
|
/** Previous tray ticket count — first 0→n transition auto-expands the mobile sheet. */
|
|
863
984
|
this.lastTrayCount = 0;
|
|
985
|
+
/** Previous computed total — drives a single explanatory value bump. */
|
|
986
|
+
this.lastTrayTotal = 0;
|
|
987
|
+
/** Stable item keys prevent tray chips re-animating on unrelated realtime syncs. */
|
|
988
|
+
this.lastTrayKeys = /* @__PURE__ */ new Set();
|
|
989
|
+
this.bestAvailableBusy = false;
|
|
990
|
+
this.releasingLabels = /* @__PURE__ */ new Set();
|
|
991
|
+
/** Selected labels awaiting the hold response; their own realtime echo can arrive first. */
|
|
992
|
+
this.holdingLabels = /* @__PURE__ */ new Set();
|
|
993
|
+
this.ctaPhase = "idle";
|
|
864
994
|
// narrow-layout chrome that docks into the sheet's Filters row on mobile
|
|
865
995
|
this.a11yChipsEl = null;
|
|
866
996
|
this.cbEl = null;
|
|
@@ -873,10 +1003,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
873
1003
|
if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
|
|
874
1004
|
if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
|
|
875
1005
|
if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
|
|
876
|
-
this.opts = options;
|
|
877
|
-
|
|
1006
|
+
this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
|
|
1007
|
+
this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
|
|
1008
|
+
this.api = new PubApi(this.apiBase);
|
|
878
1009
|
this.controller = new import_core2.PickerController({
|
|
879
|
-
transport: api,
|
|
1010
|
+
transport: this.api,
|
|
880
1011
|
eventKey: options.event,
|
|
881
1012
|
maxSelection: options.maxSelection ?? DEFAULT_MAX_SELECTION2,
|
|
882
1013
|
currency: options.currency,
|
|
@@ -884,7 +1015,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
884
1015
|
colorblindSafe: options.colorblindSafe,
|
|
885
1016
|
onSelectionChange: () => {
|
|
886
1017
|
this.syncTray();
|
|
887
|
-
if (this.
|
|
1018
|
+
if (this.committedSelection().length) this.collapseSectionCard();
|
|
888
1019
|
},
|
|
889
1020
|
onStatusChange: () => {
|
|
890
1021
|
this.syncPrices();
|
|
@@ -894,18 +1025,25 @@ var SeatPicker = class _SeatPicker {
|
|
|
894
1025
|
},
|
|
895
1026
|
onHoldExpired: () => {
|
|
896
1027
|
this.hold = null;
|
|
1028
|
+
this.forgetHold();
|
|
897
1029
|
this.handedOff = false;
|
|
898
1030
|
this.bookedShown = false;
|
|
1031
|
+
this.ctaPhase = "idle";
|
|
899
1032
|
this.stopHoldTimer();
|
|
900
1033
|
this.gaQty.clear();
|
|
901
|
-
this.toast((0, import_core2.t)("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.");
|
|
1034
|
+
this.toast((0, import_core2.t)("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.", "warning");
|
|
902
1035
|
this.syncTray();
|
|
1036
|
+
this.emitHoldChange();
|
|
903
1037
|
this.opts.onHoldExpired?.();
|
|
904
1038
|
},
|
|
905
|
-
confirmSelection:
|
|
1039
|
+
confirmSelection: this.opts.confirmSelection,
|
|
906
1040
|
onSelect: (seat) => {
|
|
1041
|
+
this.flashPickedSeat(seat.id);
|
|
907
1042
|
if (this.opts.confirmSelection) this.showConfirm(seat);
|
|
908
1043
|
},
|
|
1044
|
+
onDeselect: (seat) => {
|
|
1045
|
+
if (this.confirmSeat?.id === seat.id) this.dismissConfirm();
|
|
1046
|
+
},
|
|
909
1047
|
onViewChange: () => {
|
|
910
1048
|
this.reanchorConfirm();
|
|
911
1049
|
this.syncRung();
|
|
@@ -945,17 +1083,32 @@ var SeatPicker = class _SeatPicker {
|
|
|
945
1083
|
const picker = new _SeatPicker({ ...options, container: frame });
|
|
946
1084
|
picker.modalScrim = scrim;
|
|
947
1085
|
picker.prevFocus = document.activeElement;
|
|
1086
|
+
let closing = false;
|
|
948
1087
|
const close = () => {
|
|
1088
|
+
if (closing) return;
|
|
1089
|
+
closing = true;
|
|
949
1090
|
document.body.style.overflow = prevOverflow;
|
|
950
|
-
|
|
951
|
-
|
|
1091
|
+
scrim.style.opacity = "0";
|
|
1092
|
+
scrim.style.pointerEvents = "none";
|
|
1093
|
+
const finish = () => {
|
|
1094
|
+
picker.destroy();
|
|
1095
|
+
options.onClose?.();
|
|
1096
|
+
};
|
|
1097
|
+
if (picker.hold && !picker.handedOff) void picker.release().finally(finish);
|
|
1098
|
+
else finish();
|
|
952
1099
|
};
|
|
953
1100
|
picker.closeModal = close;
|
|
954
1101
|
scrim.addEventListener("mousedown", (e) => {
|
|
955
1102
|
if (e.target === scrim) close();
|
|
956
1103
|
});
|
|
957
1104
|
picker.escHandler = (e) => {
|
|
958
|
-
if (e.key
|
|
1105
|
+
if (e.key !== "Escape") return;
|
|
1106
|
+
if (picker.confirmSeat) {
|
|
1107
|
+
e.preventDefault();
|
|
1108
|
+
picker.cancelConfirm();
|
|
1109
|
+
} else {
|
|
1110
|
+
close();
|
|
1111
|
+
}
|
|
959
1112
|
};
|
|
960
1113
|
document.addEventListener("keydown", picker.escHandler);
|
|
961
1114
|
await picker.render();
|
|
@@ -972,8 +1125,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
972
1125
|
const mount = resolveContainer3(this.opts.container);
|
|
973
1126
|
const root = document.createElement("div");
|
|
974
1127
|
root.className = "sl-picker";
|
|
1128
|
+
root.tabIndex = -1;
|
|
975
1129
|
this.root = root;
|
|
976
1130
|
mount.appendChild(root);
|
|
1131
|
+
root.addEventListener("keydown", (e) => {
|
|
1132
|
+
if (e.key !== "Escape" || !this.confirmSeat) return;
|
|
1133
|
+
e.preventDefault();
|
|
1134
|
+
e.stopPropagation();
|
|
1135
|
+
this.cancelConfirm();
|
|
1136
|
+
});
|
|
977
1137
|
Object.entries(resolveTokens(void 0, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
|
|
978
1138
|
root.innerHTML = `
|
|
979
1139
|
<div class="sl-head">
|
|
@@ -1012,11 +1172,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
1012
1172
|
</div>
|
|
1013
1173
|
<div class="sl-sec sl-filtersec" data-ref="filtersSec">Filters</div>
|
|
1014
1174
|
<div class="sl-filters" data-ref="filters"></div>
|
|
1015
|
-
<div class="sl-sec" data-ref="pricesSec">Prices</div>
|
|
1175
|
+
<div class="sl-sec sl-prices-sec" data-ref="pricesSec">Prices</div>
|
|
1016
1176
|
<div class="sl-prices" data-ref="prices"></div>
|
|
1017
|
-
<div class="sl-sec">Your seats</div>
|
|
1177
|
+
<div class="sl-sec sl-seats-sec">Your seats</div>
|
|
1018
1178
|
<div class="sl-tray" data-ref="tray"></div>
|
|
1019
1179
|
<div class="sl-foot">
|
|
1180
|
+
<div class="sl-hold-note" data-ref="holdNote" role="status" aria-live="polite">
|
|
1181
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
|
|
1182
|
+
<span><b data-ref="holdTitle">Seats secured</b><span class="sl-hold-copy" data-ref="holdCopy">Checkout timer is running.</span></span>
|
|
1183
|
+
</div>
|
|
1020
1184
|
<div class="sl-total"><span data-ref="count"></span><b data-ref="total"></b></div>
|
|
1021
1185
|
<button type="button" class="sl-cta" data-ref="cta" disabled></button>
|
|
1022
1186
|
</div>
|
|
@@ -1168,6 +1332,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
1168
1332
|
this.buildExtendPrompt();
|
|
1169
1333
|
this.buildBookedOverlay();
|
|
1170
1334
|
this.dockLayoutChrome();
|
|
1335
|
+
await this.restoreRememberedHold();
|
|
1336
|
+
if (this.destroyed) return this;
|
|
1171
1337
|
this.syncPrices();
|
|
1172
1338
|
this.syncTray();
|
|
1173
1339
|
return this;
|
|
@@ -1243,6 +1409,156 @@ var SeatPicker = class _SeatPicker {
|
|
|
1243
1409
|
cssVar(name) {
|
|
1244
1410
|
return this.root ? getComputedStyle(this.root).getPropertyValue(name).trim() : "";
|
|
1245
1411
|
}
|
|
1412
|
+
/** Motion is progressive enhancement; all state remains legible when reduced. */
|
|
1413
|
+
reducedMotion() {
|
|
1414
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1415
|
+
}
|
|
1416
|
+
scheduleMotion(fn, delay) {
|
|
1417
|
+
const timer = setTimeout(() => {
|
|
1418
|
+
this.motionTimers.delete(timer);
|
|
1419
|
+
if (!this.destroyed) fn();
|
|
1420
|
+
}, delay);
|
|
1421
|
+
this.motionTimers.add(timer);
|
|
1422
|
+
}
|
|
1423
|
+
/** Restart one finite CSS animation without leaving a permanent state class. */
|
|
1424
|
+
animateOnce(el, className, duration = 600) {
|
|
1425
|
+
if (!el || this.reducedMotion()) return;
|
|
1426
|
+
el.classList.remove(className);
|
|
1427
|
+
void el.offsetWidth;
|
|
1428
|
+
el.classList.add(className);
|
|
1429
|
+
this.scheduleMotion(() => el.classList.remove(className), duration);
|
|
1430
|
+
}
|
|
1431
|
+
/** Selection feedback belongs on the selected seat, not across the whole map. */
|
|
1432
|
+
flashPickedSeat(id) {
|
|
1433
|
+
if (this.reducedMotion()) return;
|
|
1434
|
+
this.controller.flashSeat(id, this.cssVar("--sl-accent") || "#f4b740");
|
|
1435
|
+
}
|
|
1436
|
+
/** A completed hold gets one short map ripple per concrete seat. */
|
|
1437
|
+
flashHeldSeats(hold) {
|
|
1438
|
+
if (this.reducedMotion()) return;
|
|
1439
|
+
const labels = (hold.items ?? []).filter((item) => item.objectType !== "ga").map((item) => item.label);
|
|
1440
|
+
labels.slice(0, 10).forEach((label, index) => {
|
|
1441
|
+
const seat = this.controller.seatByLabel(label);
|
|
1442
|
+
if (!seat) return;
|
|
1443
|
+
this.scheduleMotion(
|
|
1444
|
+
() => this.controller.flashSeat(seat.id, this.cssVar("--sl-accent") || "#f4b740"),
|
|
1445
|
+
index * 55
|
|
1446
|
+
);
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
/** Update only the action affordance; selection callbacks must not refire. */
|
|
1450
|
+
committedSelection() {
|
|
1451
|
+
const candidateId = this.confirmSeat?.id;
|
|
1452
|
+
return this.controller.getSelection().filter((seat) => seat.id !== candidateId);
|
|
1453
|
+
}
|
|
1454
|
+
pendingSelectionCount() {
|
|
1455
|
+
const heldItems = this.hold?.items ?? [];
|
|
1456
|
+
const heldLabels = new Set(heldItems.map((item) => item.label));
|
|
1457
|
+
const pendingSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).length;
|
|
1458
|
+
const heldGA = /* @__PURE__ */ new Map();
|
|
1459
|
+
for (const item of heldItems.filter((candidate) => candidate.objectType === "ga")) {
|
|
1460
|
+
heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
|
|
1461
|
+
}
|
|
1462
|
+
const pendingGA = [...this.gaQty.entries()].reduce(
|
|
1463
|
+
(sum, [areaId, qty]) => sum + Math.max(0, qty - (heldGA.get(areaId) ?? 0)),
|
|
1464
|
+
0
|
|
1465
|
+
);
|
|
1466
|
+
return pendingSeats + pendingGA;
|
|
1467
|
+
}
|
|
1468
|
+
syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
|
|
1469
|
+
const cta = this.els.cta;
|
|
1470
|
+
if (!cta) return;
|
|
1471
|
+
if (this.confirmSeat) {
|
|
1472
|
+
cta.disabled = true;
|
|
1473
|
+
cta.textContent = "Confirm or cancel this seat";
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
if (this.ctaPhase === "holding") {
|
|
1477
|
+
cta.disabled = true;
|
|
1478
|
+
cta.innerHTML = '<span class="sl-cta-spin" aria-hidden="true"></span>Securing seats\u2026';
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
if (this.ctaPhase === "checkout") {
|
|
1482
|
+
cta.disabled = true;
|
|
1483
|
+
cta.innerHTML = '<span class="sl-cta-spin" aria-hidden="true"></span>Opening checkout\u2026';
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
cta.disabled = count === 0;
|
|
1487
|
+
cta.textContent = this.hold ? pending ? `Secure ${pending} more & checkout` : "Continue to checkout" : count ? "Hold seats & checkout" : "Select seats";
|
|
1488
|
+
}
|
|
1489
|
+
setCtaPhase(phase) {
|
|
1490
|
+
this.ctaPhase = phase;
|
|
1491
|
+
this.syncCta();
|
|
1492
|
+
if (phase === "checkout") {
|
|
1493
|
+
this.scheduleMotion(() => {
|
|
1494
|
+
if (this.ctaPhase !== "checkout") return;
|
|
1495
|
+
this.ctaPhase = "idle";
|
|
1496
|
+
this.syncCta();
|
|
1497
|
+
}, 1100);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
/** Session-scoped capability key: isolated by API origin and event. */
|
|
1501
|
+
holdStorageKey() {
|
|
1502
|
+
return `@seatlayer/hold/v1/${encodeURIComponent(this.apiBase)}/${encodeURIComponent(this.opts.event)}`;
|
|
1503
|
+
}
|
|
1504
|
+
rememberedHoldId() {
|
|
1505
|
+
if (this.opts.initialHoldId) return this.opts.initialHoldId;
|
|
1506
|
+
if (this.opts.restoreHold === false || typeof window === "undefined") return null;
|
|
1507
|
+
try {
|
|
1508
|
+
return window.sessionStorage.getItem(this.holdStorageKey());
|
|
1509
|
+
} catch {
|
|
1510
|
+
return null;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
rememberHold(hold) {
|
|
1514
|
+
if (this.opts.restoreHold === false || typeof window === "undefined") return;
|
|
1515
|
+
try {
|
|
1516
|
+
window.sessionStorage.setItem(this.holdStorageKey(), hold.holdId);
|
|
1517
|
+
} catch {
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
forgetHold() {
|
|
1521
|
+
if (typeof window === "undefined") return;
|
|
1522
|
+
try {
|
|
1523
|
+
window.sessionStorage.removeItem(this.holdStorageKey());
|
|
1524
|
+
} catch {
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
async resumeHoldFromServer(holdId, automatic) {
|
|
1528
|
+
try {
|
|
1529
|
+
const h = await this.controller.resumeHold(holdId);
|
|
1530
|
+
if (!h) return null;
|
|
1531
|
+
const restored = {
|
|
1532
|
+
holdId: h.holdId,
|
|
1533
|
+
expiresAt: h.expiresAt,
|
|
1534
|
+
seats: h.seats,
|
|
1535
|
+
items: h.items
|
|
1536
|
+
};
|
|
1537
|
+
this.hold = restored;
|
|
1538
|
+
this.handedOff = true;
|
|
1539
|
+
this.bookedShown = false;
|
|
1540
|
+
this.ctaPhase = "idle";
|
|
1541
|
+
this.startHoldTimer(restored.expiresAt);
|
|
1542
|
+
this.rememberHold(restored);
|
|
1543
|
+
this.syncTray();
|
|
1544
|
+
this.emitHoldChange();
|
|
1545
|
+
this.opts.onHoldRestored?.(restored, restored.seats ?? [], this.buildHandoff(restored));
|
|
1546
|
+
if (automatic) this.toast("Your held tickets have been restored.", "success");
|
|
1547
|
+
return restored;
|
|
1548
|
+
} catch (error) {
|
|
1549
|
+
const status = error?.status;
|
|
1550
|
+
if (status === 404 || status === 409) {
|
|
1551
|
+
this.forgetHold();
|
|
1552
|
+
} else {
|
|
1553
|
+
this.opts.onError?.(error);
|
|
1554
|
+
}
|
|
1555
|
+
return null;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
async restoreRememberedHold() {
|
|
1559
|
+
const holdId = this.rememberedHoldId();
|
|
1560
|
+
if (holdId) await this.resumeHoldFromServer(holdId, true);
|
|
1561
|
+
}
|
|
1246
1562
|
/** Section-bearing objects on the active floor (single-floor → doc.objects). */
|
|
1247
1563
|
activeFloorObjects() {
|
|
1248
1564
|
const doc = this.controller.doc;
|
|
@@ -1497,7 +1813,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1497
1813
|
(r) => `<button type="button" data-rung="${r}" title="${TIP[r]}" aria-pressed="false">${LABEL[r]}</button>`
|
|
1498
1814
|
).join("");
|
|
1499
1815
|
pills.querySelectorAll("button").forEach((btn) => {
|
|
1500
|
-
btn.addEventListener("click", () =>
|
|
1816
|
+
btn.addEventListener("click", () => {
|
|
1817
|
+
const rung = btn.dataset.rung;
|
|
1818
|
+
this.controller.setRung(rung);
|
|
1819
|
+
if (rung === "seats") this.collapseSectionCard();
|
|
1820
|
+
});
|
|
1501
1821
|
});
|
|
1502
1822
|
this.regions["top-center"].appendChild(pills);
|
|
1503
1823
|
this.rungsEl = pills;
|
|
@@ -1548,7 +1868,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1548
1868
|
this.secCardEl?.remove();
|
|
1549
1869
|
this.secCardEl = null;
|
|
1550
1870
|
if (!summary) return;
|
|
1551
|
-
this.secCardCollapsed =
|
|
1871
|
+
this.secCardCollapsed = this.controller.getRung() === "seats";
|
|
1552
1872
|
this.secCardShownAt = Date.now();
|
|
1553
1873
|
this.renderSectionCard(summary);
|
|
1554
1874
|
}
|
|
@@ -1614,6 +1934,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
1614
1934
|
sectionCardOnView() {
|
|
1615
1935
|
if (!this.secCardEl || this.secCardCollapsed || !this.lastSection) return;
|
|
1616
1936
|
if (this.root?.dataset.layout === "narrow") return;
|
|
1937
|
+
if (this.controller.getRung() === "seats") {
|
|
1938
|
+
this.collapseSectionCard();
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1617
1941
|
if (Date.now() - this.secCardShownAt < 1400) {
|
|
1618
1942
|
if (this.sectionCardCoverage() > 0.25) this.collapseSectionCard();
|
|
1619
1943
|
return;
|
|
@@ -1656,36 +1980,78 @@ var SeatPicker = class _SeatPicker {
|
|
|
1656
1980
|
const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
|
|
1657
1981
|
this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
|
|
1658
1982
|
}
|
|
1659
|
-
// ----
|
|
1983
|
+
// ---- seat candidate confirmation ------------------------------------------
|
|
1660
1984
|
showConfirm(seat) {
|
|
1661
|
-
this.
|
|
1985
|
+
const previousId = this.confirmSeat?.id;
|
|
1986
|
+
this.confirmEl?.remove();
|
|
1987
|
+
this.confirmEl = null;
|
|
1988
|
+
this.confirmSeat = seat;
|
|
1989
|
+
this.root?.setAttribute("data-confirming", "true");
|
|
1990
|
+
this.controller.setSelectionFocus(seat.id);
|
|
1991
|
+
if (previousId && previousId !== seat.id) this.controller.deselect([previousId]);
|
|
1662
1992
|
if (this.tipEl) this.tipEl.style.display = "none";
|
|
1993
|
+
const details = this.controller.seatDetails(seat.id);
|
|
1663
1994
|
const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
|
|
1664
|
-
const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
|
|
1995
|
+
const price = details?.price ?? (cat?.tiers?.length ? cat.tiers[0].price : cat?.price);
|
|
1996
|
+
const safe = (value) => String(value ?? "\u2014").replace(/[&<>"]/g, (char) => ({
|
|
1997
|
+
"&": "&",
|
|
1998
|
+
"<": "<",
|
|
1999
|
+
">": ">",
|
|
2000
|
+
'"': """
|
|
2001
|
+
})[char]);
|
|
1665
2002
|
const el = document.createElement("div");
|
|
1666
2003
|
el.className = "sl-confirm";
|
|
1667
|
-
el.
|
|
2004
|
+
el.setAttribute("role", "dialog");
|
|
2005
|
+
el.setAttribute("aria-modal", "true");
|
|
2006
|
+
el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
|
|
2007
|
+
el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
|
|
2008
|
+
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>${(0, import_core2.t)("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>`;
|
|
1668
2009
|
this.els.map.appendChild(el);
|
|
1669
2010
|
this.confirmEl = el;
|
|
1670
|
-
this.confirmSeat = seat;
|
|
1671
2011
|
this.reanchorConfirm();
|
|
1672
2012
|
el.querySelector(".sl-confirm-view")?.addEventListener("click", () => this.openSeatView(seat));
|
|
1673
|
-
el.querySelector(".sl-confirm-add").addEventListener("click", () => this.
|
|
1674
|
-
el.querySelector(".sl-confirm-cancel").addEventListener("click", () =>
|
|
1675
|
-
|
|
1676
|
-
this.closeConfirm();
|
|
1677
|
-
});
|
|
2013
|
+
el.querySelector(".sl-confirm-add").addEventListener("click", () => this.commitConfirm());
|
|
2014
|
+
el.querySelector(".sl-confirm-cancel").addEventListener("click", () => this.cancelConfirm());
|
|
2015
|
+
requestAnimationFrame(() => el.querySelector(".sl-confirm-add")?.focus());
|
|
1678
2016
|
}
|
|
1679
2017
|
reanchorConfirm() {
|
|
1680
2018
|
if (!this.confirmEl || !this.confirmSeat) return;
|
|
1681
2019
|
const p = this.controller.worldToScreen({ x: this.confirmSeat.x, y: this.confirmSeat.y });
|
|
1682
|
-
this.
|
|
1683
|
-
this.
|
|
1684
|
-
|
|
1685
|
-
|
|
2020
|
+
if (this.root?.dataset.layout === "narrow") return;
|
|
2021
|
+
const mapWidth = this.els.map.clientWidth;
|
|
2022
|
+
const mapHeight = this.els.map.clientHeight;
|
|
2023
|
+
const cardWidth = this.confirmEl.offsetWidth || 276;
|
|
2024
|
+
const cardHeight = this.confirmEl.offsetHeight || 230;
|
|
2025
|
+
const half = cardWidth / 2 + 12;
|
|
2026
|
+
const x = Math.max(half, Math.min(mapWidth - half, p.x));
|
|
2027
|
+
const belowFits = p.y + cardHeight + 24 <= mapHeight;
|
|
2028
|
+
const placeBelow = p.y < cardHeight + 24 && belowFits;
|
|
2029
|
+
this.confirmEl.dataset.placement = placeBelow ? "below" : "above";
|
|
2030
|
+
this.confirmEl.style.left = `${x}px`;
|
|
2031
|
+
this.confirmEl.style.top = `${Math.max(8, Math.min(mapHeight - 8, p.y))}px`;
|
|
2032
|
+
}
|
|
2033
|
+
dismissConfirm() {
|
|
1686
2034
|
this.confirmEl?.remove();
|
|
1687
2035
|
this.confirmEl = null;
|
|
1688
2036
|
this.confirmSeat = null;
|
|
2037
|
+
this.root?.removeAttribute("data-confirming");
|
|
2038
|
+
this.controller.setSelectionFocus(null);
|
|
2039
|
+
}
|
|
2040
|
+
commitConfirm() {
|
|
2041
|
+
if (!this.confirmSeat) return;
|
|
2042
|
+
this.dismissConfirm();
|
|
2043
|
+
this.collapseSectionCard();
|
|
2044
|
+
this.syncTray();
|
|
2045
|
+
}
|
|
2046
|
+
cancelConfirm() {
|
|
2047
|
+
const seat = this.confirmSeat;
|
|
2048
|
+
if (!seat) return;
|
|
2049
|
+
this.controller.deselect([seat.id]);
|
|
2050
|
+
if (this.confirmSeat) this.dismissConfirm();
|
|
2051
|
+
this.root?.focus({ preventScroll: true });
|
|
2052
|
+
}
|
|
2053
|
+
closeConfirm() {
|
|
2054
|
+
this.dismissConfirm();
|
|
1689
2055
|
}
|
|
1690
2056
|
// ---- 360° view-from-seat modal --------------------------------------------
|
|
1691
2057
|
seatViewEnabled() {
|
|
@@ -1708,7 +2074,6 @@ var SeatPicker = class _SeatPicker {
|
|
|
1708
2074
|
openSeatView(seat) {
|
|
1709
2075
|
if (!this.root || !this.seatViewEnabled()) return;
|
|
1710
2076
|
this.closeSeatView();
|
|
1711
|
-
this.closeConfirm();
|
|
1712
2077
|
const doc = this.controller.doc;
|
|
1713
2078
|
const activeId = this.controller.getActiveFloorId();
|
|
1714
2079
|
const focal = doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
|
|
@@ -1819,7 +2184,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1819
2184
|
const price = c.tiers?.length ? c.tiers[0].price : c.price;
|
|
1820
2185
|
const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
|
|
1821
2186
|
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>`;
|
|
1822
|
-
}).join("")
|
|
2187
|
+
}).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>`;
|
|
1823
2188
|
this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
|
|
1824
2189
|
row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
|
|
1825
2190
|
row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
|
|
@@ -1827,39 +2192,47 @@ var SeatPicker = class _SeatPicker {
|
|
|
1827
2192
|
}
|
|
1828
2193
|
/** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
|
|
1829
2194
|
evictTakenSelections() {
|
|
1830
|
-
const ownLabels = new Set(
|
|
2195
|
+
const ownLabels = /* @__PURE__ */ new Set([
|
|
2196
|
+
...this.controller.currentHold()?.labels ?? [],
|
|
2197
|
+
...this.holdingLabels
|
|
2198
|
+
]);
|
|
1831
2199
|
const gone = this.controller.getSelection().filter((s) => !ownLabels.has(s.label) && (this.controller.getStatus(s.id) ?? "free") !== "free");
|
|
1832
2200
|
if (!gone.length) return;
|
|
1833
2201
|
this.controller.deselect(gone.map((s) => s.id));
|
|
1834
|
-
this.toast(`Seat ${gone[0].label} was just taken by another buyer
|
|
2202
|
+
this.toast(`Seat ${gone[0].label} was just taken by another buyer.`, "error");
|
|
1835
2203
|
}
|
|
1836
2204
|
syncTray() {
|
|
1837
2205
|
if (!this.els.tray) return;
|
|
1838
|
-
const seats = this.
|
|
2206
|
+
const seats = this.committedSelection();
|
|
1839
2207
|
const gaAreas = this.controller.getGAAreas();
|
|
1840
2208
|
const heldItems = this.hold?.items ?? [];
|
|
1841
2209
|
const parts = [];
|
|
2210
|
+
const nextTrayKeys = /* @__PURE__ */ new Set();
|
|
1842
2211
|
if (!seats.length && !heldItems.length && !gaAreas.length) {
|
|
1843
2212
|
parts.push(`<div class="sl-tray-hint">Tap a seat on the map, or let us pick the best available for you.</div>`);
|
|
1844
2213
|
} else if (!seats.length && !heldItems.length) {
|
|
1845
2214
|
parts.push(`<div class="sl-tray-hint">Tap a seat on the map \u2014 or grab standing tickets below.</div>`);
|
|
1846
2215
|
}
|
|
1847
2216
|
for (const item of heldItems) {
|
|
2217
|
+
const itemKey = `held:${item.label}`;
|
|
2218
|
+
nextTrayKeys.add(itemKey);
|
|
1848
2219
|
const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
|
|
1849
2220
|
const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
|
|
1850
2221
|
const canView2 = this.seatViewEnabled() && item.objectType !== "ga" && !!this.controller.seatByLabel(item.label);
|
|
1851
2222
|
parts.push(
|
|
1852
|
-
`<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="${(0, import_core2.t)("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>` : "") +
|
|
2223
|
+
`<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="${(0, import_core2.t)("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>`
|
|
1853
2224
|
);
|
|
1854
2225
|
}
|
|
1855
2226
|
const heldLabels = new Set(heldItems.map((item) => item.label));
|
|
1856
2227
|
const canView = this.seatViewEnabled();
|
|
1857
2228
|
for (const s of seats.filter((seat) => !heldLabels.has(seat.label))) {
|
|
2229
|
+
const itemKey = `seat:${s.id}`;
|
|
2230
|
+
nextTrayKeys.add(itemKey);
|
|
1858
2231
|
const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
|
|
1859
2232
|
const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${(0, import_core2.t)("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>` : "";
|
|
1860
2233
|
const viewBtn = canView ? `<button type="button" class="view" data-view-label="${s.label}" aria-label="${(0, import_core2.t)("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>` : "";
|
|
1861
2234
|
parts.push(
|
|
1862
|
-
`<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>`
|
|
2235
|
+
`<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>`
|
|
1863
2236
|
);
|
|
1864
2237
|
}
|
|
1865
2238
|
for (const area of gaAreas) {
|
|
@@ -1871,10 +2244,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1871
2244
|
if (!this.hold) {
|
|
1872
2245
|
const cats = this.controller.doc?.categories ?? [];
|
|
1873
2246
|
parts.push(
|
|
1874
|
-
`<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
|
|
2247
|
+
`<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>`
|
|
1875
2248
|
);
|
|
1876
2249
|
}
|
|
1877
2250
|
this.els.tray.innerHTML = parts.join("");
|
|
2251
|
+
this.lastTrayKeys = nextTrayKeys;
|
|
1878
2252
|
this.els.tray.querySelectorAll("[data-ba]").forEach((btn) => {
|
|
1879
2253
|
btn.addEventListener("click", () => {
|
|
1880
2254
|
this.baQty = Math.max(1, Math.min(8, this.baQty + Number(btn.dataset.ba)));
|
|
@@ -1889,8 +2263,18 @@ var SeatPicker = class _SeatPicker {
|
|
|
1889
2263
|
});
|
|
1890
2264
|
this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
|
|
1891
2265
|
btn.addEventListener("click", () => {
|
|
1892
|
-
const
|
|
1893
|
-
|
|
2266
|
+
const chip = btn.closest(".sl-chip");
|
|
2267
|
+
if (chip.dataset.held) {
|
|
2268
|
+
void this.removeHeldLabel(decodeURIComponent(chip.dataset.held), chip);
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2271
|
+
const id = chip.dataset.seat;
|
|
2272
|
+
if (this.reducedMotion()) {
|
|
2273
|
+
this.controller.deselect([id]);
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
chip.classList.add("sl-leave");
|
|
2277
|
+
this.scheduleMotion(() => this.controller.deselect([id]), 150);
|
|
1894
2278
|
});
|
|
1895
2279
|
});
|
|
1896
2280
|
this.els.tray.querySelectorAll(".sl-chip .tier").forEach((sel) => {
|
|
@@ -1919,39 +2303,85 @@ var SeatPicker = class _SeatPicker {
|
|
|
1919
2303
|
const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
|
|
1920
2304
|
const total = freshSeats.reduce((sum, s) => sum + s.price, 0) + gaTotal + heldTotal;
|
|
1921
2305
|
const count = freshSeats.length + gaCount + heldCount;
|
|
2306
|
+
const pendingCount = this.pendingSelectionCount();
|
|
2307
|
+
const previousCount = this.lastTrayCount;
|
|
2308
|
+
const previousTotal = this.lastTrayTotal;
|
|
1922
2309
|
this.els.count.textContent = count ? `${count} ${count === 1 ? "ticket" : "tickets"}` : "No seats selected";
|
|
1923
2310
|
this.els.total.textContent = count ? this.money(total) : "";
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
2311
|
+
this.syncCta(count, pendingCount);
|
|
2312
|
+
if (this.hold) {
|
|
2313
|
+
const securedCount = heldCount || this.hold.seats?.length || 0;
|
|
2314
|
+
if (this.els.holdTitle) {
|
|
2315
|
+
this.els.holdTitle.textContent = `${securedCount} ${securedCount === 1 ? "seat" : "seats"} secured`;
|
|
2316
|
+
}
|
|
2317
|
+
if (this.els.holdCopy) {
|
|
2318
|
+
this.els.holdCopy.textContent = pendingCount ? `${pendingCount} more selected \u2014 secure before checkout.` : "Checkout timer is running.";
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
if (count !== previousCount) this.animateOnce(this.els.count, "sl-value-pop", 380);
|
|
2322
|
+
if (total !== previousTotal) this.animateOnce(this.els.total, "sl-value-pop", 380);
|
|
2323
|
+
if (previousCount === 0 && count > 0) this.animateOnce(this.els.cta, "sl-ready", 520);
|
|
1927
2324
|
if (this.els.peek) {
|
|
1928
2325
|
if (count) {
|
|
1929
|
-
this.els.peek.innerHTML = `<span>${count} ${count === 1 ? "ticket" : "tickets"} \xB7 ${this.money(total)}</span><span class="go">${this.hold ? "Continue" : "Review"}</span>`;
|
|
2326
|
+
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>`;
|
|
1930
2327
|
} else {
|
|
1931
2328
|
const prices = (this.controller.doc?.categories ?? []).map((c) => this.catPrice(c)).filter((p) => p != null);
|
|
1932
2329
|
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>`;
|
|
1933
2330
|
}
|
|
1934
2331
|
}
|
|
1935
|
-
if (this.root?.dataset.layout === "narrow" && count > 0 && this.lastTrayCount === 0) {
|
|
1936
|
-
this.root.dataset.sheet = "open";
|
|
1937
|
-
}
|
|
1938
2332
|
this.lastTrayCount = count;
|
|
2333
|
+
this.lastTrayTotal = total;
|
|
1939
2334
|
this.opts.onSelectionChange?.(seats);
|
|
1940
2335
|
}
|
|
2336
|
+
async removeHeldLabel(label, chip) {
|
|
2337
|
+
if (!label || this.releasingLabels.has(label)) return false;
|
|
2338
|
+
this.releasingLabels.add(label);
|
|
2339
|
+
chip?.setAttribute("aria-busy", "true");
|
|
2340
|
+
const button = chip?.querySelector(".rm");
|
|
2341
|
+
if (button) button.disabled = true;
|
|
2342
|
+
try {
|
|
2343
|
+
const preserveAcrossNavigation = this.handedOff;
|
|
2344
|
+
const released = await this.controller.releaseLabels([label]);
|
|
2345
|
+
if (!released) {
|
|
2346
|
+
this.toast(`Couldn't remove ${label}. Your hold is unchanged.`, "error");
|
|
2347
|
+
return false;
|
|
2348
|
+
}
|
|
2349
|
+
const remaining = this.controller.currentHold();
|
|
2350
|
+
this.hold = remaining ? { holdId: remaining.holdId, expiresAt: remaining.expiresAt, seats: remaining.seats, items: remaining.items } : null;
|
|
2351
|
+
this.handedOff = !!this.hold && preserveAcrossNavigation;
|
|
2352
|
+
this.bookedShown = false;
|
|
2353
|
+
this.ctaPhase = "idle";
|
|
2354
|
+
if (this.hold) {
|
|
2355
|
+
this.startHoldTimer(this.hold.expiresAt);
|
|
2356
|
+
} else {
|
|
2357
|
+
this.stopHoldTimer();
|
|
2358
|
+
this.forgetHold();
|
|
2359
|
+
}
|
|
2360
|
+
this.syncTray();
|
|
2361
|
+
this.emitHoldChange();
|
|
2362
|
+
this.toast(`${label} removed from your hold.`, "success");
|
|
2363
|
+
return true;
|
|
2364
|
+
} finally {
|
|
2365
|
+
this.releasingLabels.delete(label);
|
|
2366
|
+
chip?.removeAttribute("aria-busy");
|
|
2367
|
+
if (button?.isConnected) button.disabled = false;
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
1941
2370
|
async handleCta() {
|
|
1942
|
-
const
|
|
1943
|
-
if (this.hold && !
|
|
1944
|
-
const seats = this.hold.seats ??
|
|
2371
|
+
const committed = this.committedSelection();
|
|
2372
|
+
if (this.hold && !committed.some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
|
|
2373
|
+
const seats = this.hold.seats ?? committed;
|
|
1945
2374
|
this.handedOff = true;
|
|
2375
|
+
this.setCtaPhase("checkout");
|
|
1946
2376
|
this.opts.onCheckout?.(this.hold, seats, this.buildHandoff(this.hold));
|
|
1947
2377
|
return;
|
|
1948
2378
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
2379
|
+
this.holdingLabels = new Set(committed.map((seat) => seat.label));
|
|
2380
|
+
this.setCtaPhase("holding");
|
|
1951
2381
|
try {
|
|
1952
2382
|
let hold = null;
|
|
1953
2383
|
const gaEntries = [...this.gaQty.entries()].filter(([, q]) => q > 0);
|
|
1954
|
-
const chosenSeats = this.
|
|
2384
|
+
const chosenSeats = this.committedSelection();
|
|
1955
2385
|
if (chosenSeats.length) {
|
|
1956
2386
|
const h = await this.controller.hold(void 0, this.opts.holdTtlMs);
|
|
1957
2387
|
hold = h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
|
|
@@ -1961,31 +2391,43 @@ var SeatPicker = class _SeatPicker {
|
|
|
1961
2391
|
hold = h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : hold;
|
|
1962
2392
|
}
|
|
1963
2393
|
if (!hold) {
|
|
1964
|
-
this.toast("One or more seats were just taken. Please pick again.");
|
|
2394
|
+
this.toast("One or more seats were just taken. Please pick again.", "error");
|
|
2395
|
+
this.setCtaPhase("idle");
|
|
1965
2396
|
this.syncTray();
|
|
1966
2397
|
return;
|
|
1967
2398
|
}
|
|
1968
2399
|
this.hold = hold;
|
|
1969
2400
|
this.handedOff = true;
|
|
1970
2401
|
this.startHoldTimer(hold.expiresAt);
|
|
1971
|
-
this.
|
|
2402
|
+
this.flashHeldSeats(hold);
|
|
2403
|
+
this.setCtaPhase("checkout");
|
|
2404
|
+
this.emitHoldChange();
|
|
2405
|
+
this.opts.onCheckout?.(hold, hold.seats ?? chosenSeats, this.buildHandoff(hold));
|
|
1972
2406
|
} catch (err) {
|
|
1973
2407
|
this.opts.onError?.(err);
|
|
1974
|
-
this.toast("One or more seats were just taken. Please pick again.");
|
|
2408
|
+
this.toast("One or more seats were just taken. Please pick again.", "error");
|
|
2409
|
+
this.setCtaPhase("idle");
|
|
1975
2410
|
} finally {
|
|
2411
|
+
this.holdingLabels.clear();
|
|
2412
|
+
if (this.ctaPhase === "holding") this.ctaPhase = "idle";
|
|
1976
2413
|
this.syncTray();
|
|
1977
2414
|
}
|
|
1978
2415
|
}
|
|
1979
2416
|
startHoldTimer(expiresAt) {
|
|
1980
2417
|
this.stopHoldTimer();
|
|
1981
2418
|
this.holdExpiresAt = expiresAt;
|
|
2419
|
+
if (this.hold) this.rememberHold(this.hold);
|
|
1982
2420
|
const pill = this.els.hold;
|
|
2421
|
+
pill.innerHTML = '<span class="sl-hold-dot" aria-hidden="true"></span><span>Held</span><span class="sl-hold-time" data-ref="holdTime"></span>';
|
|
2422
|
+
const time = pill.querySelector('[data-ref="holdTime"]');
|
|
2423
|
+
this.els.holdNote?.classList.add("on");
|
|
1983
2424
|
const tick = () => {
|
|
1984
2425
|
const ms = Math.max(0, this.holdExpiresAt - Date.now());
|
|
1985
2426
|
const m = Math.floor(ms / 6e4);
|
|
1986
2427
|
const s = String(Math.floor(ms % 6e4 / 1e3)).padStart(2, "0");
|
|
1987
|
-
|
|
2428
|
+
if (time) time.textContent = `${m}:${s}`;
|
|
1988
2429
|
pill.classList.add("on");
|
|
2430
|
+
pill.classList.toggle("is-expiring", ms > 0 && ms <= EXTEND_PROMPT_MS);
|
|
1989
2431
|
this.setExtendPrompt(ms > 0 && ms <= EXTEND_PROMPT_MS, ms);
|
|
1990
2432
|
if (ms <= 0) this.stopHoldTimer();
|
|
1991
2433
|
};
|
|
@@ -1995,7 +2437,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
1995
2437
|
stopHoldTimer() {
|
|
1996
2438
|
if (this.holdTimer) clearInterval(this.holdTimer);
|
|
1997
2439
|
this.holdTimer = null;
|
|
1998
|
-
this.els.hold?.classList.remove("on");
|
|
2440
|
+
this.els.hold?.classList.remove("on", "is-expiring");
|
|
2441
|
+
this.els.holdNote?.classList.remove("on");
|
|
1999
2442
|
this.setExtendPrompt(false, 0);
|
|
2000
2443
|
}
|
|
2001
2444
|
/** Show/refresh (or hide) the "Need more time?" prompt with the live seconds left. */
|
|
@@ -2020,13 +2463,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
2020
2463
|
this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
|
|
2021
2464
|
this.holdExpiresAt = h.expiresAt;
|
|
2022
2465
|
this.extendEl?.classList.remove("on");
|
|
2023
|
-
this.
|
|
2466
|
+
this.rememberHold(this.hold);
|
|
2467
|
+
this.emitHoldChange();
|
|
2468
|
+
this.toast("More time added \u2014 your seats are still held.", "success");
|
|
2024
2469
|
} else {
|
|
2025
|
-
this.toast("Couldn't add more time \u2014 please head to checkout now.");
|
|
2470
|
+
this.toast("Couldn't add more time \u2014 please head to checkout now.", "warning");
|
|
2026
2471
|
}
|
|
2027
2472
|
} catch (err) {
|
|
2028
2473
|
this.opts.onError?.(err);
|
|
2029
|
-
this.toast("Couldn't add more time \u2014 please head to checkout now.");
|
|
2474
|
+
this.toast("Couldn't add more time \u2014 please head to checkout now.", "warning");
|
|
2030
2475
|
} finally {
|
|
2031
2476
|
btn.disabled = false;
|
|
2032
2477
|
btn.textContent = prev;
|
|
@@ -2050,6 +2495,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2050
2495
|
this.bookedShown = true;
|
|
2051
2496
|
const handoff = this.buildHandoff(this.hold);
|
|
2052
2497
|
this.stopHoldTimer();
|
|
2498
|
+
this.forgetHold();
|
|
2053
2499
|
const n = handoff.lineItems.reduce((sum, i) => sum + i.quantity, 0);
|
|
2054
2500
|
if (this.els.bookedSub) {
|
|
2055
2501
|
this.els.bookedSub.innerHTML = `<span class="sl-booked-seats">${n} ${n === 1 ? "ticket" : "tickets"}</span> confirmed. A confirmation is on its way.`;
|
|
@@ -2074,13 +2520,25 @@ var SeatPicker = class _SeatPicker {
|
|
|
2074
2520
|
const total = lineItems.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
|
|
2075
2521
|
return { holdId: hold.holdId, expiresAt: hold.expiresAt, currency, lineItems, total };
|
|
2076
2522
|
}
|
|
2077
|
-
|
|
2523
|
+
emitHoldChange() {
|
|
2524
|
+
const hold = this.hold;
|
|
2525
|
+
this.opts.onHoldChange?.(
|
|
2526
|
+
hold,
|
|
2527
|
+
hold?.seats ?? [],
|
|
2528
|
+
hold ? this.buildHandoff(hold) : null
|
|
2529
|
+
);
|
|
2530
|
+
}
|
|
2531
|
+
toast(msg, tone = "neutral") {
|
|
2078
2532
|
const el = this.els.toast;
|
|
2079
2533
|
if (!el) return;
|
|
2080
2534
|
el.textContent = msg;
|
|
2535
|
+
el.dataset.tone = tone;
|
|
2081
2536
|
el.classList.add("on");
|
|
2082
2537
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
2083
|
-
this.toastTimer = setTimeout(() =>
|
|
2538
|
+
this.toastTimer = setTimeout(() => {
|
|
2539
|
+
el.classList.remove("on");
|
|
2540
|
+
el.dataset.tone = "neutral";
|
|
2541
|
+
}, 4200);
|
|
2084
2542
|
}
|
|
2085
2543
|
placeTooltip() {
|
|
2086
2544
|
if (!this.tipEl) return;
|
|
@@ -2101,15 +2559,40 @@ var SeatPicker = class _SeatPicker {
|
|
|
2101
2559
|
return;
|
|
2102
2560
|
}
|
|
2103
2561
|
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" ? (0, import_core2.t)("map.statusHeld") : (0, import_core2.t)("map.statusTaken")}</div>`;
|
|
2104
|
-
|
|
2562
|
+
const location = [
|
|
2563
|
+
details.sectionLabel ? `Section ${details.sectionLabel}` : "",
|
|
2564
|
+
details.rowLabel ? `Row ${details.rowLabel}` : "",
|
|
2565
|
+
details.seatNumber ? `Seat ${details.seatNumber}` : details.label
|
|
2566
|
+
].filter(Boolean).join(" \xB7 ");
|
|
2567
|
+
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;
|
|
2105
2568
|
this.tipEl.style.display = "block";
|
|
2106
2569
|
this.placeTooltip();
|
|
2107
2570
|
}
|
|
2108
2571
|
// ---- public conveniences ----------------------------------------------------
|
|
2109
2572
|
getSelection() {
|
|
2110
|
-
return this.
|
|
2573
|
+
return this.committedSelection();
|
|
2574
|
+
}
|
|
2575
|
+
/** Current active/restored hold reflected in the tray. */
|
|
2576
|
+
getCurrentHold() {
|
|
2577
|
+
return this.hold;
|
|
2578
|
+
}
|
|
2579
|
+
/** Explicit host-driven hold restore (automatic session restore is on by default). */
|
|
2580
|
+
async resumeHold(holdId) {
|
|
2581
|
+
return this.resumeHoldFromServer(holdId, false);
|
|
2582
|
+
}
|
|
2583
|
+
/** Remove one server-held ticket while keeping the rest of the hold active. */
|
|
2584
|
+
async removeHeldTicket(label) {
|
|
2585
|
+
return this.removeHeldLabel(label);
|
|
2111
2586
|
}
|
|
2112
2587
|
async bestAvailable(qty, categoryKey) {
|
|
2588
|
+
if (this.bestAvailableBusy) return null;
|
|
2589
|
+
if (this.confirmSeat) this.cancelConfirm();
|
|
2590
|
+
this.bestAvailableBusy = true;
|
|
2591
|
+
const button = this.els.tray?.querySelector(".sl-ba-go");
|
|
2592
|
+
if (button) {
|
|
2593
|
+
button.disabled = true;
|
|
2594
|
+
button.innerHTML = '<span class="sl-ba-spin" aria-hidden="true"></span>Finding\u2026';
|
|
2595
|
+
}
|
|
2113
2596
|
try {
|
|
2114
2597
|
const h = await this.controller.bestAvailable(qty, categoryKey);
|
|
2115
2598
|
if (h) {
|
|
@@ -2117,30 +2600,67 @@ var SeatPicker = class _SeatPicker {
|
|
|
2117
2600
|
this.handedOff = false;
|
|
2118
2601
|
this.bookedShown = false;
|
|
2119
2602
|
this.startHoldTimer(h.expiresAt);
|
|
2603
|
+
this.flashHeldSeats(this.hold);
|
|
2120
2604
|
this.syncTray();
|
|
2605
|
+
this.emitHoldChange();
|
|
2121
2606
|
return this.hold;
|
|
2122
2607
|
}
|
|
2123
2608
|
return null;
|
|
2124
2609
|
} catch (err) {
|
|
2125
2610
|
this.opts.onError?.(err);
|
|
2611
|
+
this.toast("Those seats are no longer available. Try another quantity or category.", "error");
|
|
2126
2612
|
return null;
|
|
2613
|
+
} finally {
|
|
2614
|
+
this.bestAvailableBusy = false;
|
|
2615
|
+
if (!this.hold && button?.isConnected) {
|
|
2616
|
+
button.disabled = false;
|
|
2617
|
+
button.textContent = "Best available";
|
|
2618
|
+
}
|
|
2127
2619
|
}
|
|
2128
2620
|
}
|
|
2129
2621
|
async release() {
|
|
2130
|
-
|
|
2622
|
+
const tracked = this.hold;
|
|
2623
|
+
const controllerHold = this.controller.currentHold();
|
|
2624
|
+
let released = true;
|
|
2625
|
+
if (controllerHold) {
|
|
2626
|
+
released = await this.controller.release();
|
|
2627
|
+
} else if (tracked) {
|
|
2628
|
+
const labels = [.../* @__PURE__ */ new Set([
|
|
2629
|
+
...(tracked.items ?? []).map((item) => item.label),
|
|
2630
|
+
...(tracked.seats ?? []).map((seat) => seat.label)
|
|
2631
|
+
])];
|
|
2632
|
+
if (labels.length) {
|
|
2633
|
+
try {
|
|
2634
|
+
await this.api.release(this.opts.event, labels, tracked.holdId);
|
|
2635
|
+
} catch (error) {
|
|
2636
|
+
this.opts.onError?.(error);
|
|
2637
|
+
released = false;
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
if (!released) {
|
|
2642
|
+
this.toast("Couldn't release your tickets. Your hold is unchanged.", "error");
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2131
2645
|
this.hold = null;
|
|
2646
|
+
this.forgetHold();
|
|
2132
2647
|
this.handedOff = false;
|
|
2133
2648
|
this.bookedShown = false;
|
|
2649
|
+
this.ctaPhase = "idle";
|
|
2134
2650
|
this.stopHoldTimer();
|
|
2135
2651
|
this.gaQty.clear();
|
|
2136
2652
|
this.syncTray();
|
|
2653
|
+
this.emitHoldChange();
|
|
2137
2654
|
}
|
|
2138
2655
|
destroy() {
|
|
2139
2656
|
this.destroyed = true;
|
|
2657
|
+
if (this.hold && !this.handedOff) void this.controller.release();
|
|
2140
2658
|
this.closeConfirm();
|
|
2141
2659
|
this.closeSeatView();
|
|
2142
2660
|
this.stopHoldTimer();
|
|
2143
2661
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
2662
|
+
for (const timer of this.motionTimers) clearTimeout(timer);
|
|
2663
|
+
this.motionTimers.clear();
|
|
2144
2664
|
this.ro?.disconnect();
|
|
2145
2665
|
this.ro = null;
|
|
2146
2666
|
if (this.escHandler) document.removeEventListener("keydown", this.escHandler);
|