@seatlayer/js 0.12.3 → 0.14.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 +3 -0
- package/dist/index.cjs +543 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -3
- package/dist/index.d.ts +39 -3
- package/dist/index.js +543 -72
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -434,8 +434,12 @@ var CSS = `
|
|
|
434
434
|
.sl-head-meta{font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--sl-muted);margin-top:3px;
|
|
435
435
|
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
|
|
436
436
|
.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
|
-
|
|
437
|
+
background:var(--sl-accent);color:var(--sl-accent-ink);font-weight:700;font-size:12px;font-variant-numeric:tabular-nums;
|
|
438
|
+
transform-origin:right center}
|
|
439
|
+
.sl-hold-pill.on{display:inline-flex;animation:slPillIn .34s cubic-bezier(.2,.8,.2,1) both}
|
|
440
|
+
.sl-hold-dot{width:7px;height:7px;border-radius:50%;background:currentColor;opacity:.78;box-shadow:0 0 0 0 currentColor}
|
|
441
|
+
.sl-hold-pill.is-expiring .sl-hold-dot{animation:slHoldPulse 1.4s ease-out infinite}
|
|
442
|
+
.sl-hold-time{min-width:3.35em;text-align:left}
|
|
439
443
|
.sl-close{width:32px;height:32px;border-radius:999px;flex:none;display:none;align-items:center;justify-content:center;
|
|
440
444
|
border:1px solid var(--sl-line);color:var(--sl-muted);transition:color .15s,border-color .15s}
|
|
441
445
|
.sl-close:hover{color:var(--sl-text);border-color:var(--sl-muted)}
|
|
@@ -457,7 +461,7 @@ var CSS = `
|
|
|
457
461
|
.sl-picker[data-layout="narrow"] .sl-body{flex-direction:column}
|
|
458
462
|
.sl-picker[data-layout="narrow"] .sl-map{min-height:0;flex:1}
|
|
459
463
|
.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 .
|
|
464
|
+
flex:none;height:50%;transition:height .3s cubic-bezier(.2,.8,.2,1)}
|
|
461
465
|
.sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side{height:86px;overflow:hidden}
|
|
462
466
|
.sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side > :not(.sl-sheet-head){display:none}
|
|
463
467
|
.sl-picker[data-layout="narrow"] .sl-tray{flex:none}
|
|
@@ -513,10 +517,17 @@ var CSS = `
|
|
|
513
517
|
.sl-tray{flex:1;padding:10px 16px;display:flex;flex-direction:column;gap:8px;min-height:0}
|
|
514
518
|
.sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
|
|
515
519
|
.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}
|
|
520
|
+
border-radius:var(--sl-r-sm);background:var(--sl-surface);font-size:13px;transform-origin:center}
|
|
521
|
+
.sl-chip.sl-enter{animation:slChipIn .38s cubic-bezier(.2,.8,.2,1) both}
|
|
522
|
+
.sl-chip.sl-leave{pointer-events:none;animation:slChipOut .16s ease-in both}
|
|
523
|
+
.sl-chip.sl-held{border-color:var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
|
|
524
|
+
box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent)}
|
|
517
525
|
.sl-chip b{font-weight:800}
|
|
518
526
|
.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
527
|
.sl-chip .amt{font-weight:700;font-variant-numeric:tabular-nums}
|
|
528
|
+
.sl-chip-state{flex:none;padding:3px 6px;border-radius:999px;background:var(--sl-accent);color:var(--sl-accent-ink);
|
|
529
|
+
font-size:8.5px;line-height:1;font-weight:900;letter-spacing:.08em;text-transform:uppercase}
|
|
530
|
+
.sl-chip-state.sl-pending{background:transparent;color:var(--sl-muted);border:1px solid var(--sl-line)}
|
|
520
531
|
.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
532
|
.sl-chip .rm:hover{color:var(--sl-text)}
|
|
522
533
|
.sl-chip .rm svg{width:11px;height:11px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round}
|
|
@@ -534,17 +545,30 @@ var CSS = `
|
|
|
534
545
|
|
|
535
546
|
/* footer */
|
|
536
547
|
.sl-foot{padding:12px 16px 14px;border-top:1px solid var(--sl-line);flex:none}
|
|
548
|
+
.sl-hold-note{display:none;align-items:center;gap:9px;margin-bottom:10px;padding:9px 10px;border-radius:var(--sl-r-sm);
|
|
549
|
+
border:1px solid var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
|
|
550
|
+
box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent);
|
|
551
|
+
font-size:11.5px;line-height:1.35;color:var(--sl-muted)}
|
|
552
|
+
.sl-hold-note.on{display:flex;animation:slNoticeIn .38s cubic-bezier(.2,.8,.2,1) both}
|
|
553
|
+
.sl-hold-note svg{width:16px;height:16px;flex:none;stroke:var(--sl-accent);stroke-width:2.4;fill:none;
|
|
554
|
+
stroke-linecap:round;stroke-linejoin:round}
|
|
555
|
+
.sl-hold-note b{display:block;color:var(--sl-text);font-size:12px}
|
|
556
|
+
.sl-hold-copy{display:block}
|
|
537
557
|
.sl-total{display:flex;justify-content:space-between;align-items:center;font-size:13px;margin-bottom:10px}
|
|
538
558
|
.sl-total b{font-size:17px;font-variant-numeric:tabular-nums}
|
|
559
|
+
.sl-value-pop{animation:slValuePop .32s cubic-bezier(.2,.8,.2,1)}
|
|
539
560
|
/* Primary checkout CTA. Scoped under .sl-picker so it OUTWEIGHS the
|
|
540
561
|
'.sl-picker button' reset (0,1,1) \u2014 an unscoped '.sl-cta' (0,1,0) loses to it
|
|
541
562
|
and the button renders as plain text with no accent fill. */
|
|
542
563
|
.sl-picker .sl-cta{display:flex;align-items:center;justify-content:center;width:100%;min-height:44px;
|
|
543
564
|
padding:12px 16px;border-radius:var(--sl-r-sm);font-weight:800;font-size:14px;line-height:1.1;
|
|
544
565
|
background:var(--sl-accent);color:var(--sl-accent-ink);
|
|
545
|
-
transition:filter .15s,background .
|
|
566
|
+
transition:filter .15s,background .22s,color .22s,transform .12s,box-shadow .22s;gap:8px}
|
|
546
567
|
.sl-picker .sl-cta:hover{filter:brightness(1.08)}
|
|
547
568
|
.sl-picker .sl-cta:active{transform:translateY(1px);filter:brightness(.94)}
|
|
569
|
+
.sl-picker .sl-cta.sl-ready{animation:slCtaReady .42s cubic-bezier(.2,.8,.2,1)}
|
|
570
|
+
.sl-cta-spin,.sl-ba-spin{width:14px;height:14px;border-radius:50%;border:2px solid currentColor;border-right-color:transparent;
|
|
571
|
+
animation:slspin .7s linear infinite;flex:none}
|
|
548
572
|
/* Disabled ("Select seats"): quieter, but still a full-width button shape. */
|
|
549
573
|
.sl-picker .sl-cta:disabled{background:var(--sl-surface);color:var(--sl-muted);opacity:1;
|
|
550
574
|
cursor:not-allowed;filter:none;transform:none}
|
|
@@ -582,11 +606,15 @@ var CSS = `
|
|
|
582
606
|
.sl-zoom svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
583
607
|
|
|
584
608
|
/* toast + boot states (toast flows in the bottom-center region) */
|
|
585
|
-
.sl-toast{transform:translateY(6px);max-width:100%;
|
|
609
|
+
.sl-toast{transform:translateY(6px) scale(.98);max-width:100%;
|
|
586
610
|
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 .
|
|
611
|
+
font-size:12.5px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .22s,transform .22s;white-space:nowrap;
|
|
588
612
|
overflow:hidden;text-overflow:ellipsis}
|
|
589
|
-
.sl-toast.on{opacity:1;transform:translateY(0)}
|
|
613
|
+
.sl-toast.on{opacity:1;transform:translateY(0) scale(1)}
|
|
614
|
+
.sl-toast[data-tone="error"]{border-color:#ef4444}
|
|
615
|
+
.sl-toast[data-tone="warning"]{border-color:var(--sl-accent)}
|
|
616
|
+
.sl-toast[data-tone="success"]{border-color:#22c55e}
|
|
617
|
+
.sl-toast.on[data-tone="error"]{animation:slToastNudge .32s ease-out}
|
|
590
618
|
.sl-boot{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|
591
619
|
gap:10px;background:var(--sl-bg);font-size:13px;font-weight:600;color:var(--sl-muted)}
|
|
592
620
|
.sl-boot-spin{width:24px;height:24px;border-radius:50%;border:3px solid var(--sl-line);border-top-color:var(--sl-accent);
|
|
@@ -610,15 +638,22 @@ var CSS = `
|
|
|
610
638
|
.sl-extend-btn:disabled{opacity:.5;cursor:not-allowed}
|
|
611
639
|
|
|
612
640
|
/* 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
|
-
|
|
641
|
+
.sl-booked{position:absolute;inset:0;z-index:11;display:flex;flex-direction:column;align-items:center;
|
|
642
|
+
justify-content:center;gap:12px;text-align:center;padding:28px;background:var(--sl-bg);opacity:0;visibility:hidden;
|
|
643
|
+
pointer-events:none;transition:opacity .34s ease,visibility 0s linear .34s}
|
|
644
|
+
.sl-booked.on{opacity:1;visibility:visible;pointer-events:auto;transition:opacity .34s ease,visibility 0s}
|
|
616
645
|
.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
|
|
646
|
+
background:var(--sl-accent);color:var(--sl-accent-ink);transform:scale(.72)}
|
|
647
|
+
.sl-booked.on .sl-booked-badge{animation:slSuccessPop .58s cubic-bezier(.2,1.25,.3,1) .08s both}
|
|
648
|
+
.sl-booked-badge svg{width:30px;height:30px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round;
|
|
649
|
+
stroke-dasharray:30;stroke-dashoffset:30}
|
|
650
|
+
.sl-booked.on .sl-booked-badge svg{animation:slCheckDraw .42s ease-out .32s forwards}
|
|
619
651
|
.sl-booked-title{font-weight:800;font-size:19px;color:var(--sl-text)}
|
|
620
652
|
.sl-booked-sub{font-size:13px;color:var(--sl-muted);line-height:1.5;max-width:320px}
|
|
621
653
|
.sl-booked-seats{font-weight:700;color:var(--sl-text)}
|
|
654
|
+
.sl-booked.on .sl-booked-title,.sl-booked.on .sl-booked-sub{animation:slCopyRise .42s ease-out both}
|
|
655
|
+
.sl-booked.on .sl-booked-title{animation-delay:.22s}
|
|
656
|
+
.sl-booked.on .sl-booked-sub{animation-delay:.3s}
|
|
622
657
|
|
|
623
658
|
/* a11y filter chips (flow within the top-left region) */
|
|
624
659
|
.sl-chips{display:flex;gap:6px;flex-wrap:wrap}
|
|
@@ -629,7 +664,8 @@ var CSS = `
|
|
|
629
664
|
|
|
630
665
|
/* confirm popover */
|
|
631
666
|
.sl-confirm{position:absolute;z-index:9;min-width:190px;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
632
|
-
border-radius:12px;padding:12px;box-shadow:0 18px 50px -18px rgba(0,0,0,.7);transform:translate(-50%,calc(-100% - 14px))
|
|
667
|
+
border-radius:12px;padding:12px;box-shadow:0 18px 50px -18px rgba(0,0,0,.7);transform:translate(-50%,calc(-100% - 14px));
|
|
668
|
+
animation:slConfirmIn .24s cubic-bezier(.2,.8,.2,1) both}
|
|
633
669
|
.sl-confirm-label{font-weight:800;font-size:14px}
|
|
634
670
|
.sl-confirm-meta{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--sl-muted);margin-top:4px}
|
|
635
671
|
.sl-confirm-meta b{color:var(--sl-text);margin-left:auto}
|
|
@@ -647,8 +683,10 @@ var CSS = `
|
|
|
647
683
|
.sl-ba-qty button{width:24px;height:24px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
648
684
|
font-size:14px;font-weight:700;display:flex;align-items:center;justify-content:center}
|
|
649
685
|
.sl-ba-qty span{min-width:14px;text-align:center;font-weight:800}
|
|
650
|
-
.sl-ba-go{margin-left:auto;padding:7px 12px;border-radius:999px;border:1px solid var(--sl-line);font-weight:700;font-size:12px;
|
|
686
|
+
.sl-ba-go{margin-left:auto;padding:7px 12px;border-radius:999px;border:1px solid var(--sl-line);font-weight:700;font-size:12px;
|
|
687
|
+
transition:border-color .15s,opacity .15s;display:flex;align-items:center;gap:6px}
|
|
651
688
|
.sl-ba-go:hover{border-color:var(--sl-muted)}
|
|
689
|
+
.sl-ba-go:disabled{opacity:.62;cursor:wait}
|
|
652
690
|
|
|
653
691
|
/* screen-reader live region */
|
|
654
692
|
.sl-sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
|
@@ -754,6 +792,27 @@ var CSS = `
|
|
|
754
792
|
.sl-price-row.sl-dim{opacity:.4}
|
|
755
793
|
.sl-seccard-mix-item.sl-dim{opacity:.4}
|
|
756
794
|
|
|
795
|
+
/* Buyer-journey motion: every animation explains a state transition (selected,
|
|
796
|
+
held, checkout handoff, conflict or booked). No decorative infinite motion
|
|
797
|
+
except the expiring-hold pulse and active progress spinners. */
|
|
798
|
+
@keyframes slPillIn{from{opacity:0;transform:translateX(7px) scale(.9)}to{opacity:1;transform:translateX(0) scale(1)}}
|
|
799
|
+
@keyframes slHoldPulse{0%{box-shadow:0 0 0 0 currentColor;opacity:.9}75%,100%{box-shadow:0 0 0 7px transparent;opacity:.55}}
|
|
800
|
+
@keyframes slChipIn{from{opacity:0;transform:translateY(8px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}
|
|
801
|
+
@keyframes slChipOut{to{opacity:0;transform:translateX(10px) scale(.98)}}
|
|
802
|
+
@keyframes slNoticeIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
|
|
803
|
+
@keyframes slValuePop{0%{opacity:.6;transform:translateY(3px)}55%{transform:translateY(-1px) scale(1.05)}100%{opacity:1;transform:none}}
|
|
804
|
+
@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}}
|
|
805
|
+
@keyframes slToastNudge{0%,100%{margin-left:0}30%{margin-left:-4px}60%{margin-left:3px}}
|
|
806
|
+
@keyframes slSuccessPop{0%{opacity:0;transform:scale(.72)}65%{opacity:1;transform:scale(1.08)}100%{opacity:1;transform:scale(1)}}
|
|
807
|
+
@keyframes slCheckDraw{to{stroke-dashoffset:0}}
|
|
808
|
+
@keyframes slCopyRise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
|
809
|
+
@keyframes slConfirmIn{from{opacity:0;transform:translate(-50%,calc(-100% - 8px)) scale(.96)}to{opacity:1;transform:translate(-50%,calc(-100% - 14px)) scale(1)}}
|
|
810
|
+
|
|
811
|
+
@media(prefers-reduced-motion:reduce){
|
|
812
|
+
.sl-picker *,.sl-modal-scrim *{animation-duration:.001ms!important;animation-iteration-count:1!important;
|
|
813
|
+
transition-duration:.001ms!important;scroll-behavior:auto!important}
|
|
814
|
+
}
|
|
815
|
+
|
|
757
816
|
/* modal host */
|
|
758
817
|
.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
818
|
.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 +853,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
794
853
|
this.ro = null;
|
|
795
854
|
this.holdTimer = null;
|
|
796
855
|
this.toastTimer = null;
|
|
856
|
+
/** Short-lived UI motion timers; all are cancelled on destroy. */
|
|
857
|
+
this.motionTimers = /* @__PURE__ */ new Set();
|
|
797
858
|
// state
|
|
798
859
|
this.currency = "USD";
|
|
799
860
|
this.hold = null;
|
|
@@ -837,6 +898,12 @@ var SeatPicker = class _SeatPicker {
|
|
|
837
898
|
this.secCardShownAt = 0;
|
|
838
899
|
/** Previous tray ticket count — first 0→n transition auto-expands the mobile sheet. */
|
|
839
900
|
this.lastTrayCount = 0;
|
|
901
|
+
/** Previous computed total — drives a single explanatory value bump. */
|
|
902
|
+
this.lastTrayTotal = 0;
|
|
903
|
+
/** Stable item keys prevent tray chips re-animating on unrelated realtime syncs. */
|
|
904
|
+
this.lastTrayKeys = /* @__PURE__ */ new Set();
|
|
905
|
+
this.bestAvailableBusy = false;
|
|
906
|
+
this.ctaPhase = "idle";
|
|
840
907
|
// narrow-layout chrome that docks into the sheet's Filters row on mobile
|
|
841
908
|
this.a11yChipsEl = null;
|
|
842
909
|
this.cbEl = null;
|
|
@@ -850,9 +917,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
850
917
|
if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
|
|
851
918
|
if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
|
|
852
919
|
this.opts = options;
|
|
853
|
-
|
|
920
|
+
this.api = new PubApi((options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, ""));
|
|
854
921
|
this.controller = new PickerController2({
|
|
855
|
-
transport: api,
|
|
922
|
+
transport: this.api,
|
|
856
923
|
eventKey: options.event,
|
|
857
924
|
maxSelection: options.maxSelection ?? DEFAULT_MAX_SELECTION2,
|
|
858
925
|
currency: options.currency,
|
|
@@ -872,14 +939,16 @@ var SeatPicker = class _SeatPicker {
|
|
|
872
939
|
this.hold = null;
|
|
873
940
|
this.handedOff = false;
|
|
874
941
|
this.bookedShown = false;
|
|
942
|
+
this.ctaPhase = "idle";
|
|
875
943
|
this.stopHoldTimer();
|
|
876
944
|
this.gaQty.clear();
|
|
877
|
-
this.toast(t2("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.");
|
|
945
|
+
this.toast(t2("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.", "warning");
|
|
878
946
|
this.syncTray();
|
|
879
947
|
this.opts.onHoldExpired?.();
|
|
880
948
|
},
|
|
881
949
|
confirmSelection: options.confirmSelection,
|
|
882
950
|
onSelect: (seat) => {
|
|
951
|
+
this.flashPickedSeat(seat.id);
|
|
883
952
|
if (this.opts.confirmSelection) this.showConfirm(seat);
|
|
884
953
|
},
|
|
885
954
|
onViewChange: () => {
|
|
@@ -921,10 +990,19 @@ var SeatPicker = class _SeatPicker {
|
|
|
921
990
|
const picker = new _SeatPicker({ ...options, container: frame });
|
|
922
991
|
picker.modalScrim = scrim;
|
|
923
992
|
picker.prevFocus = document.activeElement;
|
|
993
|
+
let closing = false;
|
|
924
994
|
const close = () => {
|
|
995
|
+
if (closing) return;
|
|
996
|
+
closing = true;
|
|
925
997
|
document.body.style.overflow = prevOverflow;
|
|
926
|
-
|
|
927
|
-
|
|
998
|
+
scrim.style.opacity = "0";
|
|
999
|
+
scrim.style.pointerEvents = "none";
|
|
1000
|
+
const finish = () => {
|
|
1001
|
+
picker.destroy();
|
|
1002
|
+
options.onClose?.();
|
|
1003
|
+
};
|
|
1004
|
+
if (picker.hold && !picker.handedOff) void picker.release().finally(finish);
|
|
1005
|
+
else finish();
|
|
928
1006
|
};
|
|
929
1007
|
picker.closeModal = close;
|
|
930
1008
|
scrim.addEventListener("mousedown", (e) => {
|
|
@@ -993,6 +1071,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
993
1071
|
<div class="sl-sec">Your seats</div>
|
|
994
1072
|
<div class="sl-tray" data-ref="tray"></div>
|
|
995
1073
|
<div class="sl-foot">
|
|
1074
|
+
<div class="sl-hold-note" data-ref="holdNote" role="status" aria-live="polite">
|
|
1075
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
|
|
1076
|
+
<span><b data-ref="holdTitle">Seats secured</b><span class="sl-hold-copy" data-ref="holdCopy">Checkout timer is running.</span></span>
|
|
1077
|
+
</div>
|
|
996
1078
|
<div class="sl-total"><span data-ref="count"></span><b data-ref="total"></b></div>
|
|
997
1079
|
<button type="button" class="sl-cta" data-ref="cta" disabled></button>
|
|
998
1080
|
</div>
|
|
@@ -1219,6 +1301,85 @@ var SeatPicker = class _SeatPicker {
|
|
|
1219
1301
|
cssVar(name) {
|
|
1220
1302
|
return this.root ? getComputedStyle(this.root).getPropertyValue(name).trim() : "";
|
|
1221
1303
|
}
|
|
1304
|
+
/** Motion is progressive enhancement; all state remains legible when reduced. */
|
|
1305
|
+
reducedMotion() {
|
|
1306
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1307
|
+
}
|
|
1308
|
+
scheduleMotion(fn, delay) {
|
|
1309
|
+
const timer = setTimeout(() => {
|
|
1310
|
+
this.motionTimers.delete(timer);
|
|
1311
|
+
if (!this.destroyed) fn();
|
|
1312
|
+
}, delay);
|
|
1313
|
+
this.motionTimers.add(timer);
|
|
1314
|
+
}
|
|
1315
|
+
/** Restart one finite CSS animation without leaving a permanent state class. */
|
|
1316
|
+
animateOnce(el, className, duration = 600) {
|
|
1317
|
+
if (!el || this.reducedMotion()) return;
|
|
1318
|
+
el.classList.remove(className);
|
|
1319
|
+
void el.offsetWidth;
|
|
1320
|
+
el.classList.add(className);
|
|
1321
|
+
this.scheduleMotion(() => el.classList.remove(className), duration);
|
|
1322
|
+
}
|
|
1323
|
+
/** Selection feedback belongs on the selected seat, not across the whole map. */
|
|
1324
|
+
flashPickedSeat(id) {
|
|
1325
|
+
if (this.reducedMotion()) return;
|
|
1326
|
+
this.controller.flashSeat(id, this.cssVar("--sl-accent") || "#f4b740");
|
|
1327
|
+
}
|
|
1328
|
+
/** A completed hold gets one short map ripple per concrete seat. */
|
|
1329
|
+
flashHeldSeats(hold) {
|
|
1330
|
+
if (this.reducedMotion()) return;
|
|
1331
|
+
const labels = (hold.items ?? []).filter((item) => item.objectType !== "ga").map((item) => item.label);
|
|
1332
|
+
labels.slice(0, 10).forEach((label, index) => {
|
|
1333
|
+
const seat = this.controller.seatByLabel(label);
|
|
1334
|
+
if (!seat) return;
|
|
1335
|
+
this.scheduleMotion(
|
|
1336
|
+
() => this.controller.flashSeat(seat.id, this.cssVar("--sl-accent") || "#f4b740"),
|
|
1337
|
+
index * 55
|
|
1338
|
+
);
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
/** Update only the action affordance; selection callbacks must not refire. */
|
|
1342
|
+
pendingSelectionCount() {
|
|
1343
|
+
const heldItems = this.hold?.items ?? [];
|
|
1344
|
+
const heldLabels = new Set(heldItems.map((item) => item.label));
|
|
1345
|
+
const pendingSeats = this.controller.getSelection().filter((seat) => !heldLabels.has(seat.label)).length;
|
|
1346
|
+
const heldGA = /* @__PURE__ */ new Map();
|
|
1347
|
+
for (const item of heldItems.filter((candidate) => candidate.objectType === "ga")) {
|
|
1348
|
+
heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
|
|
1349
|
+
}
|
|
1350
|
+
const pendingGA = [...this.gaQty.entries()].reduce(
|
|
1351
|
+
(sum, [areaId, qty]) => sum + Math.max(0, qty - (heldGA.get(areaId) ?? 0)),
|
|
1352
|
+
0
|
|
1353
|
+
);
|
|
1354
|
+
return pendingSeats + pendingGA;
|
|
1355
|
+
}
|
|
1356
|
+
syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
|
|
1357
|
+
const cta = this.els.cta;
|
|
1358
|
+
if (!cta) return;
|
|
1359
|
+
if (this.ctaPhase === "holding") {
|
|
1360
|
+
cta.disabled = true;
|
|
1361
|
+
cta.innerHTML = '<span class="sl-cta-spin" aria-hidden="true"></span>Securing seats\u2026';
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
if (this.ctaPhase === "checkout") {
|
|
1365
|
+
cta.disabled = true;
|
|
1366
|
+
cta.innerHTML = '<span class="sl-cta-spin" aria-hidden="true"></span>Opening checkout\u2026';
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
cta.disabled = count === 0;
|
|
1370
|
+
cta.textContent = this.hold ? pending ? `Secure ${pending} more & checkout` : "Continue to checkout" : count ? "Hold seats & checkout" : "Select seats";
|
|
1371
|
+
}
|
|
1372
|
+
setCtaPhase(phase) {
|
|
1373
|
+
this.ctaPhase = phase;
|
|
1374
|
+
this.syncCta();
|
|
1375
|
+
if (phase === "checkout") {
|
|
1376
|
+
this.scheduleMotion(() => {
|
|
1377
|
+
if (this.ctaPhase !== "checkout") return;
|
|
1378
|
+
this.ctaPhase = "idle";
|
|
1379
|
+
this.syncCta();
|
|
1380
|
+
}, 1100);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1222
1383
|
/** Section-bearing objects on the active floor (single-floor → doc.objects). */
|
|
1223
1384
|
activeFloorObjects() {
|
|
1224
1385
|
const doc = this.controller.doc;
|
|
@@ -1473,7 +1634,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1473
1634
|
(r) => `<button type="button" data-rung="${r}" title="${TIP[r]}" aria-pressed="false">${LABEL[r]}</button>`
|
|
1474
1635
|
).join("");
|
|
1475
1636
|
pills.querySelectorAll("button").forEach((btn) => {
|
|
1476
|
-
btn.addEventListener("click", () =>
|
|
1637
|
+
btn.addEventListener("click", () => {
|
|
1638
|
+
const rung = btn.dataset.rung;
|
|
1639
|
+
this.controller.setRung(rung);
|
|
1640
|
+
if (rung === "seats") this.collapseSectionCard();
|
|
1641
|
+
});
|
|
1477
1642
|
});
|
|
1478
1643
|
this.regions["top-center"].appendChild(pills);
|
|
1479
1644
|
this.rungsEl = pills;
|
|
@@ -1524,7 +1689,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1524
1689
|
this.secCardEl?.remove();
|
|
1525
1690
|
this.secCardEl = null;
|
|
1526
1691
|
if (!summary) return;
|
|
1527
|
-
this.secCardCollapsed =
|
|
1692
|
+
this.secCardCollapsed = this.controller.getRung() === "seats";
|
|
1528
1693
|
this.secCardShownAt = Date.now();
|
|
1529
1694
|
this.renderSectionCard(summary);
|
|
1530
1695
|
}
|
|
@@ -1590,6 +1755,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
1590
1755
|
sectionCardOnView() {
|
|
1591
1756
|
if (!this.secCardEl || this.secCardCollapsed || !this.lastSection) return;
|
|
1592
1757
|
if (this.root?.dataset.layout === "narrow") return;
|
|
1758
|
+
if (this.controller.getRung() === "seats") {
|
|
1759
|
+
this.collapseSectionCard();
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1593
1762
|
if (Date.now() - this.secCardShownAt < 1400) {
|
|
1594
1763
|
if (this.sectionCardCoverage() > 0.25) this.collapseSectionCard();
|
|
1595
1764
|
return;
|
|
@@ -1807,7 +1976,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1807
1976
|
const gone = this.controller.getSelection().filter((s) => !ownLabels.has(s.label) && (this.controller.getStatus(s.id) ?? "free") !== "free");
|
|
1808
1977
|
if (!gone.length) return;
|
|
1809
1978
|
this.controller.deselect(gone.map((s) => s.id));
|
|
1810
|
-
this.toast(`Seat ${gone[0].label} was just taken by another buyer
|
|
1979
|
+
this.toast(`Seat ${gone[0].label} was just taken by another buyer.`, "error");
|
|
1811
1980
|
}
|
|
1812
1981
|
syncTray() {
|
|
1813
1982
|
if (!this.els.tray) return;
|
|
@@ -1815,27 +1984,32 @@ var SeatPicker = class _SeatPicker {
|
|
|
1815
1984
|
const gaAreas = this.controller.getGAAreas();
|
|
1816
1985
|
const heldItems = this.hold?.items ?? [];
|
|
1817
1986
|
const parts = [];
|
|
1987
|
+
const nextTrayKeys = /* @__PURE__ */ new Set();
|
|
1818
1988
|
if (!seats.length && !heldItems.length && !gaAreas.length) {
|
|
1819
1989
|
parts.push(`<div class="sl-tray-hint">Tap a seat on the map, or let us pick the best available for you.</div>`);
|
|
1820
1990
|
} else if (!seats.length && !heldItems.length) {
|
|
1821
1991
|
parts.push(`<div class="sl-tray-hint">Tap a seat on the map \u2014 or grab standing tickets below.</div>`);
|
|
1822
1992
|
}
|
|
1823
1993
|
for (const item of heldItems) {
|
|
1994
|
+
const itemKey = `held:${item.label}`;
|
|
1995
|
+
nextTrayKeys.add(itemKey);
|
|
1824
1996
|
const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
|
|
1825
1997
|
const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
|
|
1826
1998
|
const canView2 = this.seatViewEnabled() && item.objectType !== "ga" && !!this.controller.seatByLabel(item.label);
|
|
1827
1999
|
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>` : "") + `</div>`
|
|
2000
|
+
`<div class="sl-chip sl-held${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}"><b>${item.label}</b><span class="cat">${cat?.label ?? item.categoryKey}${tierName ? ` \xB7 ${tierName}` : ""}</span><span class="sl-chip-state">Held</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>` : "") + `</div>`
|
|
1829
2001
|
);
|
|
1830
2002
|
}
|
|
1831
2003
|
const heldLabels = new Set(heldItems.map((item) => item.label));
|
|
1832
2004
|
const canView = this.seatViewEnabled();
|
|
1833
2005
|
for (const s of seats.filter((seat) => !heldLabels.has(seat.label))) {
|
|
2006
|
+
const itemKey = `seat:${s.id}`;
|
|
2007
|
+
nextTrayKeys.add(itemKey);
|
|
1834
2008
|
const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
|
|
1835
2009
|
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
2010
|
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
2011
|
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>`
|
|
2012
|
+
`<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
2013
|
);
|
|
1840
2014
|
}
|
|
1841
2015
|
for (const area of gaAreas) {
|
|
@@ -1847,10 +2021,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1847
2021
|
if (!this.hold) {
|
|
1848
2022
|
const cats = this.controller.doc?.categories ?? [];
|
|
1849
2023
|
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
|
|
2024
|
+
`<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
2025
|
);
|
|
1852
2026
|
}
|
|
1853
2027
|
this.els.tray.innerHTML = parts.join("");
|
|
2028
|
+
this.lastTrayKeys = nextTrayKeys;
|
|
1854
2029
|
this.els.tray.querySelectorAll("[data-ba]").forEach((btn) => {
|
|
1855
2030
|
btn.addEventListener("click", () => {
|
|
1856
2031
|
this.baQty = Math.max(1, Math.min(8, this.baQty + Number(btn.dataset.ba)));
|
|
@@ -1865,8 +2040,14 @@ var SeatPicker = class _SeatPicker {
|
|
|
1865
2040
|
});
|
|
1866
2041
|
this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
|
|
1867
2042
|
btn.addEventListener("click", () => {
|
|
1868
|
-
const
|
|
1869
|
-
|
|
2043
|
+
const chip = btn.closest(".sl-chip");
|
|
2044
|
+
const id = chip.dataset.seat;
|
|
2045
|
+
if (this.reducedMotion()) {
|
|
2046
|
+
this.controller.deselect([id]);
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
chip.classList.add("sl-leave");
|
|
2050
|
+
this.scheduleMotion(() => this.controller.deselect([id]), 150);
|
|
1870
2051
|
});
|
|
1871
2052
|
});
|
|
1872
2053
|
this.els.tray.querySelectorAll(".sl-chip .tier").forEach((sel) => {
|
|
@@ -1895,14 +2076,27 @@ var SeatPicker = class _SeatPicker {
|
|
|
1895
2076
|
const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
|
|
1896
2077
|
const total = freshSeats.reduce((sum, s) => sum + s.price, 0) + gaTotal + heldTotal;
|
|
1897
2078
|
const count = freshSeats.length + gaCount + heldCount;
|
|
2079
|
+
const pendingCount = this.pendingSelectionCount();
|
|
2080
|
+
const previousCount = this.lastTrayCount;
|
|
2081
|
+
const previousTotal = this.lastTrayTotal;
|
|
1898
2082
|
this.els.count.textContent = count ? `${count} ${count === 1 ? "ticket" : "tickets"}` : "No seats selected";
|
|
1899
2083
|
this.els.total.textContent = count ? this.money(total) : "";
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
2084
|
+
this.syncCta(count, pendingCount);
|
|
2085
|
+
if (this.hold) {
|
|
2086
|
+
const securedCount = heldCount || this.hold.seats?.length || 0;
|
|
2087
|
+
if (this.els.holdTitle) {
|
|
2088
|
+
this.els.holdTitle.textContent = `${securedCount} ${securedCount === 1 ? "seat" : "seats"} secured`;
|
|
2089
|
+
}
|
|
2090
|
+
if (this.els.holdCopy) {
|
|
2091
|
+
this.els.holdCopy.textContent = pendingCount ? `${pendingCount} more selected \u2014 secure before checkout.` : "Checkout timer is running.";
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
if (count !== previousCount) this.animateOnce(this.els.count, "sl-value-pop", 380);
|
|
2095
|
+
if (total !== previousTotal) this.animateOnce(this.els.total, "sl-value-pop", 380);
|
|
2096
|
+
if (previousCount === 0 && count > 0) this.animateOnce(this.els.cta, "sl-ready", 520);
|
|
1903
2097
|
if (this.els.peek) {
|
|
1904
2098
|
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>`;
|
|
2099
|
+
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
2100
|
} else {
|
|
1907
2101
|
const prices = (this.controller.doc?.categories ?? []).map((c) => this.catPrice(c)).filter((p) => p != null);
|
|
1908
2102
|
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>`;
|
|
@@ -1912,18 +2106,18 @@ var SeatPicker = class _SeatPicker {
|
|
|
1912
2106
|
this.root.dataset.sheet = "open";
|
|
1913
2107
|
}
|
|
1914
2108
|
this.lastTrayCount = count;
|
|
2109
|
+
this.lastTrayTotal = total;
|
|
1915
2110
|
this.opts.onSelectionChange?.(seats);
|
|
1916
2111
|
}
|
|
1917
2112
|
async handleCta() {
|
|
1918
|
-
const cta = this.els.cta;
|
|
1919
2113
|
if (this.hold && !this.controller.getSelection().some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
|
|
1920
2114
|
const seats = this.hold.seats ?? this.controller.getSelection();
|
|
1921
2115
|
this.handedOff = true;
|
|
2116
|
+
this.setCtaPhase("checkout");
|
|
1922
2117
|
this.opts.onCheckout?.(this.hold, seats, this.buildHandoff(this.hold));
|
|
1923
2118
|
return;
|
|
1924
2119
|
}
|
|
1925
|
-
|
|
1926
|
-
cta.textContent = "Holding\u2026";
|
|
2120
|
+
this.setCtaPhase("holding");
|
|
1927
2121
|
try {
|
|
1928
2122
|
let hold = null;
|
|
1929
2123
|
const gaEntries = [...this.gaQty.entries()].filter(([, q]) => q > 0);
|
|
@@ -1937,18 +2131,23 @@ var SeatPicker = class _SeatPicker {
|
|
|
1937
2131
|
hold = h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : hold;
|
|
1938
2132
|
}
|
|
1939
2133
|
if (!hold) {
|
|
1940
|
-
this.toast("One or more seats were just taken. Please pick again.");
|
|
2134
|
+
this.toast("One or more seats were just taken. Please pick again.", "error");
|
|
2135
|
+
this.setCtaPhase("idle");
|
|
1941
2136
|
this.syncTray();
|
|
1942
2137
|
return;
|
|
1943
2138
|
}
|
|
1944
2139
|
this.hold = hold;
|
|
1945
2140
|
this.handedOff = true;
|
|
1946
2141
|
this.startHoldTimer(hold.expiresAt);
|
|
1947
|
-
this.
|
|
2142
|
+
this.flashHeldSeats(hold);
|
|
2143
|
+
this.setCtaPhase("checkout");
|
|
2144
|
+
this.opts.onCheckout?.(hold, hold.seats ?? chosenSeats, this.buildHandoff(hold));
|
|
1948
2145
|
} catch (err) {
|
|
1949
2146
|
this.opts.onError?.(err);
|
|
1950
|
-
this.toast("One or more seats were just taken. Please pick again.");
|
|
2147
|
+
this.toast("One or more seats were just taken. Please pick again.", "error");
|
|
2148
|
+
this.setCtaPhase("idle");
|
|
1951
2149
|
} finally {
|
|
2150
|
+
if (this.ctaPhase === "holding") this.ctaPhase = "idle";
|
|
1952
2151
|
this.syncTray();
|
|
1953
2152
|
}
|
|
1954
2153
|
}
|
|
@@ -1956,12 +2155,16 @@ var SeatPicker = class _SeatPicker {
|
|
|
1956
2155
|
this.stopHoldTimer();
|
|
1957
2156
|
this.holdExpiresAt = expiresAt;
|
|
1958
2157
|
const pill = this.els.hold;
|
|
2158
|
+
pill.innerHTML = '<span class="sl-hold-dot" aria-hidden="true"></span><span>Held</span><span class="sl-hold-time" data-ref="holdTime"></span>';
|
|
2159
|
+
const time = pill.querySelector('[data-ref="holdTime"]');
|
|
2160
|
+
this.els.holdNote?.classList.add("on");
|
|
1959
2161
|
const tick = () => {
|
|
1960
2162
|
const ms = Math.max(0, this.holdExpiresAt - Date.now());
|
|
1961
2163
|
const m = Math.floor(ms / 6e4);
|
|
1962
2164
|
const s = String(Math.floor(ms % 6e4 / 1e3)).padStart(2, "0");
|
|
1963
|
-
|
|
2165
|
+
if (time) time.textContent = `${m}:${s}`;
|
|
1964
2166
|
pill.classList.add("on");
|
|
2167
|
+
pill.classList.toggle("is-expiring", ms > 0 && ms <= EXTEND_PROMPT_MS);
|
|
1965
2168
|
this.setExtendPrompt(ms > 0 && ms <= EXTEND_PROMPT_MS, ms);
|
|
1966
2169
|
if (ms <= 0) this.stopHoldTimer();
|
|
1967
2170
|
};
|
|
@@ -1971,7 +2174,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
1971
2174
|
stopHoldTimer() {
|
|
1972
2175
|
if (this.holdTimer) clearInterval(this.holdTimer);
|
|
1973
2176
|
this.holdTimer = null;
|
|
1974
|
-
this.els.hold?.classList.remove("on");
|
|
2177
|
+
this.els.hold?.classList.remove("on", "is-expiring");
|
|
2178
|
+
this.els.holdNote?.classList.remove("on");
|
|
1975
2179
|
this.setExtendPrompt(false, 0);
|
|
1976
2180
|
}
|
|
1977
2181
|
/** Show/refresh (or hide) the "Need more time?" prompt with the live seconds left. */
|
|
@@ -1996,13 +2200,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
1996
2200
|
this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
|
|
1997
2201
|
this.holdExpiresAt = h.expiresAt;
|
|
1998
2202
|
this.extendEl?.classList.remove("on");
|
|
1999
|
-
this.toast("More time added \u2014 your seats are still held.");
|
|
2203
|
+
this.toast("More time added \u2014 your seats are still held.", "success");
|
|
2000
2204
|
} else {
|
|
2001
|
-
this.toast("Couldn't add more time \u2014 please head to checkout now.");
|
|
2205
|
+
this.toast("Couldn't add more time \u2014 please head to checkout now.", "warning");
|
|
2002
2206
|
}
|
|
2003
2207
|
} catch (err) {
|
|
2004
2208
|
this.opts.onError?.(err);
|
|
2005
|
-
this.toast("Couldn't add more time \u2014 please head to checkout now.");
|
|
2209
|
+
this.toast("Couldn't add more time \u2014 please head to checkout now.", "warning");
|
|
2006
2210
|
} finally {
|
|
2007
2211
|
btn.disabled = false;
|
|
2008
2212
|
btn.textContent = prev;
|
|
@@ -2050,13 +2254,17 @@ var SeatPicker = class _SeatPicker {
|
|
|
2050
2254
|
const total = lineItems.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
|
|
2051
2255
|
return { holdId: hold.holdId, expiresAt: hold.expiresAt, currency, lineItems, total };
|
|
2052
2256
|
}
|
|
2053
|
-
toast(msg) {
|
|
2257
|
+
toast(msg, tone = "neutral") {
|
|
2054
2258
|
const el = this.els.toast;
|
|
2055
2259
|
if (!el) return;
|
|
2056
2260
|
el.textContent = msg;
|
|
2261
|
+
el.dataset.tone = tone;
|
|
2057
2262
|
el.classList.add("on");
|
|
2058
2263
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
2059
|
-
this.toastTimer = setTimeout(() =>
|
|
2264
|
+
this.toastTimer = setTimeout(() => {
|
|
2265
|
+
el.classList.remove("on");
|
|
2266
|
+
el.dataset.tone = "neutral";
|
|
2267
|
+
}, 4200);
|
|
2060
2268
|
}
|
|
2061
2269
|
placeTooltip() {
|
|
2062
2270
|
if (!this.tipEl) return;
|
|
@@ -2086,6 +2294,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
2086
2294
|
return this.controller.getSelection();
|
|
2087
2295
|
}
|
|
2088
2296
|
async bestAvailable(qty, categoryKey) {
|
|
2297
|
+
if (this.bestAvailableBusy) return null;
|
|
2298
|
+
this.bestAvailableBusy = true;
|
|
2299
|
+
const button = this.els.tray?.querySelector(".sl-ba-go");
|
|
2300
|
+
if (button) {
|
|
2301
|
+
button.disabled = true;
|
|
2302
|
+
button.innerHTML = '<span class="sl-ba-spin" aria-hidden="true"></span>Finding\u2026';
|
|
2303
|
+
}
|
|
2089
2304
|
try {
|
|
2090
2305
|
const h = await this.controller.bestAvailable(qty, categoryKey);
|
|
2091
2306
|
if (h) {
|
|
@@ -2093,30 +2308,58 @@ var SeatPicker = class _SeatPicker {
|
|
|
2093
2308
|
this.handedOff = false;
|
|
2094
2309
|
this.bookedShown = false;
|
|
2095
2310
|
this.startHoldTimer(h.expiresAt);
|
|
2311
|
+
this.flashHeldSeats(this.hold);
|
|
2096
2312
|
this.syncTray();
|
|
2097
2313
|
return this.hold;
|
|
2098
2314
|
}
|
|
2099
2315
|
return null;
|
|
2100
2316
|
} catch (err) {
|
|
2101
2317
|
this.opts.onError?.(err);
|
|
2318
|
+
this.toast("Those seats are no longer available. Try another quantity or category.", "error");
|
|
2102
2319
|
return null;
|
|
2320
|
+
} finally {
|
|
2321
|
+
this.bestAvailableBusy = false;
|
|
2322
|
+
if (!this.hold && button?.isConnected) {
|
|
2323
|
+
button.disabled = false;
|
|
2324
|
+
button.textContent = "Best available";
|
|
2325
|
+
}
|
|
2103
2326
|
}
|
|
2104
2327
|
}
|
|
2105
2328
|
async release() {
|
|
2106
|
-
|
|
2329
|
+
const tracked = this.hold;
|
|
2330
|
+
const controllerHold = this.controller.currentHold();
|
|
2331
|
+
if (controllerHold) {
|
|
2332
|
+
await this.controller.release();
|
|
2333
|
+
} else if (tracked) {
|
|
2334
|
+
const labels = [.../* @__PURE__ */ new Set([
|
|
2335
|
+
...(tracked.items ?? []).map((item) => item.label),
|
|
2336
|
+
...(tracked.seats ?? []).map((seat) => seat.label)
|
|
2337
|
+
])];
|
|
2338
|
+
if (labels.length) {
|
|
2339
|
+
try {
|
|
2340
|
+
await this.api.release(this.opts.event, labels, tracked.holdId);
|
|
2341
|
+
} catch (error) {
|
|
2342
|
+
this.opts.onError?.(error);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2107
2346
|
this.hold = null;
|
|
2108
2347
|
this.handedOff = false;
|
|
2109
2348
|
this.bookedShown = false;
|
|
2349
|
+
this.ctaPhase = "idle";
|
|
2110
2350
|
this.stopHoldTimer();
|
|
2111
2351
|
this.gaQty.clear();
|
|
2112
2352
|
this.syncTray();
|
|
2113
2353
|
}
|
|
2114
2354
|
destroy() {
|
|
2115
2355
|
this.destroyed = true;
|
|
2356
|
+
if (this.hold && !this.handedOff) void this.controller.release();
|
|
2116
2357
|
this.closeConfirm();
|
|
2117
2358
|
this.closeSeatView();
|
|
2118
2359
|
this.stopHoldTimer();
|
|
2119
2360
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
2361
|
+
for (const timer of this.motionTimers) clearTimeout(timer);
|
|
2362
|
+
this.motionTimers.clear();
|
|
2120
2363
|
this.ro?.disconnect();
|
|
2121
2364
|
this.ro = null;
|
|
2122
2365
|
if (this.escHandler) document.removeEventListener("keydown", this.escHandler);
|
|
@@ -2342,11 +2585,44 @@ var CSS2 = `
|
|
|
2342
2585
|
font-size:12px;font-weight:700;color:var(--slm-text);display:inline-flex;align-items:center;gap:6px}
|
|
2343
2586
|
.slm-chip:hover{border-color:var(--slm-muted)}
|
|
2344
2587
|
.slm-chip .dot{width:8px;height:8px;border-radius:50%}
|
|
2588
|
+
.slm-chip .slm-chipcount{min-width:18px;padding:1px 5px;border-radius:999px;background:rgba(255,255,255,.07);
|
|
2589
|
+
color:var(--slm-muted);font-size:10px;font-variant-numeric:tabular-nums;text-align:center}
|
|
2590
|
+
.slm-chip .slm-chipcheck{display:none;font-size:11px;line-height:1}
|
|
2591
|
+
.slm-chip.on{border-color:var(--slm-accent);background:color-mix(in srgb,var(--slm-accent) 20%,var(--slm-surface));
|
|
2592
|
+
box-shadow:0 0 0 1px color-mix(in srgb,var(--slm-accent) 45%,transparent)}
|
|
2593
|
+
.slm-chip.on .slm-chipcount{background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
2594
|
+
.slm-chip.on .slm-chipcheck{display:inline}
|
|
2595
|
+
.slm-chip.partial{border-style:dashed;border-color:var(--slm-accent)}
|
|
2596
|
+
.slm-chip:disabled{opacity:.42;cursor:not-allowed}
|
|
2597
|
+
.slm-selecthelp{margin:-1px 0 9px;color:var(--slm-muted);font-size:11px;line-height:1.4}
|
|
2345
2598
|
.slm-field{margin:14px 0}
|
|
2346
2599
|
.slm-field label{display:block;font-size:11px;font-weight:700;color:var(--slm-muted);margin-bottom:5px}
|
|
2347
2600
|
.slm-input,.slm-select{width:100%;padding:8px 10px;border-radius:9px;border:1px solid var(--slm-line);
|
|
2348
2601
|
background:var(--slm-surface);color:var(--slm-text)}
|
|
2349
2602
|
.slm-note{font-size:11.5px;color:var(--slm-muted);margin-top:5px}
|
|
2603
|
+
.slm-blocked{margin-top:17px;padding-top:15px;border-top:1px solid var(--slm-line)}
|
|
2604
|
+
.slm-blockedhead{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:8px}
|
|
2605
|
+
.slm-blockedhead .slm-eyebrow{margin-bottom:0}.slm-blockedtotal{font-size:11px;color:var(--slm-muted)}
|
|
2606
|
+
.slm-blockedtotal b{color:var(--slm-text);font-variant-numeric:tabular-nums}
|
|
2607
|
+
.slm-blockedtools{display:grid;grid-template-columns:minmax(0,1fr);gap:7px}
|
|
2608
|
+
.slm-blockedsummary{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:9px 0 6px;
|
|
2609
|
+
color:var(--slm-muted);font-size:10.5px}
|
|
2610
|
+
.slm-linkbtn{font-size:11px!important;font-weight:800!important;color:var(--slm-accent)!important;text-align:right}
|
|
2611
|
+
.slm-linkbtn:disabled{opacity:.45;cursor:not-allowed}
|
|
2612
|
+
.slm-blockedlist{max-height:246px;overflow:auto;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
|
|
2613
|
+
.slm-blockeditem{display:grid!important;grid-template-columns:18px minmax(0,1fr);width:100%;gap:8px;padding:8px 9px!important;
|
|
2614
|
+
border-bottom:1px solid var(--slm-line)!important;text-align:left!important}
|
|
2615
|
+
.slm-blockeditem:last-child{border-bottom:0!important}.slm-blockeditem:hover{background:rgba(255,255,255,.035)!important}
|
|
2616
|
+
.slm-blockeditem.on{background:color-mix(in srgb,var(--slm-accent) 13%,var(--slm-surface))!important}
|
|
2617
|
+
.slm-blockedcheck{display:flex;align-items:center;justify-content:center;width:16px;height:16px;margin-top:1px;border-radius:4px;
|
|
2618
|
+
border:1px solid var(--slm-muted);color:transparent;font-size:10px;font-weight:900}
|
|
2619
|
+
.slm-blockeditem.on .slm-blockedcheck{border-color:var(--slm-accent);background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
2620
|
+
.slm-blockedcopy{min-width:0}.slm-blockedlabel{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
|
2621
|
+
font-size:12px;font-weight:800}.slm-blockedmeta{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
|
2622
|
+
margin-top:2px;color:var(--slm-muted);font-size:10px}
|
|
2623
|
+
.slm-blockedmore{width:100%;padding:9px!important;color:var(--slm-accent)!important;font-size:11px!important;font-weight:800!important}
|
|
2624
|
+
.slm-blockedempty{padding:12px;color:var(--slm-muted);font-size:11.5px;line-height:1.45}
|
|
2625
|
+
.slm-allnote{margin-top:-4px;margin-bottom:10px}
|
|
2350
2626
|
|
|
2351
2627
|
/* toast */
|
|
2352
2628
|
.slm-toast{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);padding:10px 16px;border-radius:10px;
|
|
@@ -2466,6 +2742,10 @@ var SeatManager = class {
|
|
|
2466
2742
|
this.sectionByObject = /* @__PURE__ */ new Map();
|
|
2467
2743
|
this.sectionLabelById = /* @__PURE__ */ new Map();
|
|
2468
2744
|
this.lastSyncedAt = null;
|
|
2745
|
+
this.blockedQuery = "";
|
|
2746
|
+
this.blockedSection = "";
|
|
2747
|
+
this.blockedResultLimit = 100;
|
|
2748
|
+
this.unblockAllConfirmTimer = null;
|
|
2469
2749
|
this.onFullscreenChange = () => {
|
|
2470
2750
|
this.paintFullscreenButton();
|
|
2471
2751
|
this.updateContainerLayout();
|
|
@@ -2611,13 +2891,13 @@ var SeatManager = class {
|
|
|
2611
2891
|
const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "free");
|
|
2612
2892
|
if (!targets.length) return;
|
|
2613
2893
|
const releaseAt = opts.releaseAt ?? this.releaseAt ?? void 0;
|
|
2614
|
-
|
|
2894
|
+
this.setSeatsLocal(targets, "blocked");
|
|
2615
2895
|
try {
|
|
2616
2896
|
await this.api.block(this.key, targets, { ...opts, releaseAt });
|
|
2617
2897
|
this.clearSelection();
|
|
2618
2898
|
this.done("block", targets, releaseAt ? `Blocked ${targets.length} \u2014 auto-release ${new Date(releaseAt).toLocaleString()}.` : `Blocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
|
|
2619
2899
|
} catch (err) {
|
|
2620
|
-
|
|
2900
|
+
this.setSeatsLocal(targets, "free");
|
|
2621
2901
|
this.toastErr(err instanceof ManageApiError && err.status === 409 ? "Some seats were just taken. Try again." : "Couldn't block those seats.");
|
|
2622
2902
|
this.opts.onError?.(err);
|
|
2623
2903
|
}
|
|
@@ -2625,13 +2905,13 @@ var SeatManager = class {
|
|
|
2625
2905
|
async unblock(labels) {
|
|
2626
2906
|
const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "blocked");
|
|
2627
2907
|
if (!targets.length) return;
|
|
2628
|
-
|
|
2908
|
+
this.setSeatsLocal(targets, "free");
|
|
2629
2909
|
try {
|
|
2630
2910
|
await this.api.unblock(this.key, targets);
|
|
2631
2911
|
this.clearSelection();
|
|
2632
2912
|
this.done("unblock", targets, `Unblocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
|
|
2633
2913
|
} catch (err) {
|
|
2634
|
-
|
|
2914
|
+
this.setSeatsLocal(targets, "blocked");
|
|
2635
2915
|
this.toastErr("Couldn't unblock those seats.");
|
|
2636
2916
|
this.opts.onError?.(err);
|
|
2637
2917
|
}
|
|
@@ -2639,9 +2919,10 @@ var SeatManager = class {
|
|
|
2639
2919
|
async unblockAll() {
|
|
2640
2920
|
const blocked = [...this.status.entries()].filter(([, s]) => s === "blocked").map(([l]) => l);
|
|
2641
2921
|
if (!blocked.length) return;
|
|
2642
|
-
|
|
2922
|
+
this.setSeatsLocal(blocked, "free");
|
|
2643
2923
|
try {
|
|
2644
2924
|
const res = await this.api.unblockAll(this.key);
|
|
2925
|
+
this.clearSelection();
|
|
2645
2926
|
this.done("unblockAll", blocked, `Unblocked ${res.freed} seat${res.freed === 1 ? "" : "s"}.`);
|
|
2646
2927
|
} catch (err) {
|
|
2647
2928
|
await this.resnapshot();
|
|
@@ -2653,13 +2934,13 @@ var SeatManager = class {
|
|
|
2653
2934
|
async cancelBooking(labels, bookingRef) {
|
|
2654
2935
|
const targets = labels.filter((l) => this.status.get(l) === "booked");
|
|
2655
2936
|
if (!targets.length || !bookingRef) return;
|
|
2656
|
-
|
|
2937
|
+
this.setSeatsLocal(targets, "free");
|
|
2657
2938
|
try {
|
|
2658
2939
|
await this.api.unbook(this.key, targets, bookingRef);
|
|
2659
2940
|
this.clearSelection();
|
|
2660
2941
|
this.done("cancelBooking", targets, `Cancelled ${targets.length} booking${targets.length === 1 ? "" : "s"}.`);
|
|
2661
2942
|
} catch (err) {
|
|
2662
|
-
|
|
2943
|
+
this.setSeatsLocal(targets, "booked");
|
|
2663
2944
|
this.toastErr("Couldn't cancel that booking. Check the reference.");
|
|
2664
2945
|
this.opts.onError?.(err);
|
|
2665
2946
|
}
|
|
@@ -2722,6 +3003,7 @@ var SeatManager = class {
|
|
|
2722
3003
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
2723
3004
|
if (this.feedTimer) clearInterval(this.feedTimer);
|
|
2724
3005
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
3006
|
+
if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
|
|
2725
3007
|
if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
|
|
2726
3008
|
if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
|
|
2727
3009
|
this.layoutObserver?.disconnect();
|
|
@@ -2900,11 +3182,16 @@ var SeatManager = class {
|
|
|
2900
3182
|
this.afterPaint();
|
|
2901
3183
|
this.recomputeTallies();
|
|
2902
3184
|
}
|
|
2903
|
-
/** Optimistic local write shared by
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
const
|
|
2907
|
-
|
|
3185
|
+
/** Optimistic local write shared by organizer actions. Paint and tally once,
|
|
3186
|
+
* even when an arena-sized operation changes hundreds of seats. */
|
|
3187
|
+
setSeatsLocal(labels, st) {
|
|
3188
|
+
const ids = [];
|
|
3189
|
+
for (const label of labels) {
|
|
3190
|
+
this.status.set(label, st);
|
|
3191
|
+
const id = this.labelToId.get(label);
|
|
3192
|
+
if (id) ids.push(id);
|
|
3193
|
+
}
|
|
3194
|
+
if (ids.length) this.renderer?.setStatus(ids, toRenderStatus(st));
|
|
2908
3195
|
this.afterPaint();
|
|
2909
3196
|
this.recomputeTallies();
|
|
2910
3197
|
}
|
|
@@ -2987,6 +3274,7 @@ var SeatManager = class {
|
|
|
2987
3274
|
this.paintLegend(t3);
|
|
2988
3275
|
this.paintMonitorInsights();
|
|
2989
3276
|
} else if (this.mode === "inspect") this.renderInspectRail(this.getSelection());
|
|
3277
|
+
else if (this.mode === "block") this.paintSelBar(this.getSelection());
|
|
2990
3278
|
this.opts.onTallies?.(t3);
|
|
2991
3279
|
}
|
|
2992
3280
|
verbFor(prev, next) {
|
|
@@ -3366,23 +3654,30 @@ var SeatManager = class {
|
|
|
3366
3654
|
}
|
|
3367
3655
|
renderBlockRail() {
|
|
3368
3656
|
const cats = this.doc?.categories ?? [];
|
|
3369
|
-
const catChips = cats.map((c) => `<button class="slm-chip" data-cat="${esc(c.key)}"
|
|
3657
|
+
const catChips = cats.map((c) => `<button class="slm-chip" type="button" data-cat="${esc(c.key)}" aria-pressed="false">
|
|
3658
|
+
<span class="dot" style="background:${esc(c.color ?? "#6e7bff")}"></span>
|
|
3659
|
+
<span>${esc(c.label ?? c.key)}</span>
|
|
3660
|
+
<span class="slm-chipcount" data-cat-count>0</span>
|
|
3661
|
+
<span class="slm-chipcheck" aria-hidden="true">\u2713</span>
|
|
3662
|
+
</button>`).join("");
|
|
3370
3663
|
const sectionField = this.sectionOptions.length ? `<div class="slm-field"><label>Select a whole section</label>
|
|
3371
3664
|
<select class="slm-select" data-ref="section"><option value="">Choose a section\u2026</option>
|
|
3372
3665
|
${this.sectionOptions.map((s) => `<option value="${esc(s.id)}">${esc(s.label)}</option>`).join("")}</select></div>` : "";
|
|
3666
|
+
const blockedSectionOptions = this.sectionOptions.map((s) => `<option value="${esc(s.id)}">${esc(s.label)}</option>`).join("");
|
|
3373
3667
|
this.els.rail.innerHTML = `
|
|
3374
3668
|
<p class="slm-eyebrow">Block & unblock</p>
|
|
3375
3669
|
<p class="slm-hint">Drag a box on the map to marquee-select, \u2318A for all, or pick a category/section. Booked and held inventory is never actionable here.</p>
|
|
3376
|
-
<div class="slm-selbar"><span class="slm-selnum" data-ref="selnum">0</span><span class="slm-sellabel">selected</span></div>
|
|
3670
|
+
<div class="slm-selbar" aria-live="polite"><span class="slm-selnum" data-ref="selnum">0</span><span class="slm-sellabel" data-ref="selmeta">selected</span></div>
|
|
3377
3671
|
<div class="slm-row">
|
|
3378
3672
|
<button class="slm-btn" data-ref="doblock" disabled>Block</button>
|
|
3379
|
-
<button class="slm-btn ghost" data-ref="dounblock" disabled>
|
|
3673
|
+
<button class="slm-btn ghost" data-ref="dounblock" disabled>Put back on sale</button>
|
|
3380
3674
|
</div>
|
|
3381
3675
|
<div class="slm-row">
|
|
3382
3676
|
<button class="slm-btn ghost" data-ref="selall">Select all</button>
|
|
3383
3677
|
<button class="slm-btn ghost" data-ref="clearsel">Clear</button>
|
|
3384
3678
|
</div>
|
|
3385
|
-
<p class="slm-eyebrow" style="margin-top:8px">
|
|
3679
|
+
<p class="slm-eyebrow" style="margin-top:8px">Select by category</p>
|
|
3680
|
+
<p class="slm-selecthelp">Choose one or more. A checked category is selected; click it again to remove it.</p>
|
|
3386
3681
|
<div class="slm-chiprow">${catChips || '<span class="slm-empty">No categories.</span>'}</div>
|
|
3387
3682
|
${sectionField}
|
|
3388
3683
|
<div class="slm-field">
|
|
@@ -3390,18 +3685,46 @@ var SeatManager = class {
|
|
|
3390
3685
|
<input type="datetime-local" class="slm-input" data-ref="release" />
|
|
3391
3686
|
<p class="slm-note" data-ref="releasenote">Leave empty to block permanently.</p>
|
|
3392
3687
|
</div>
|
|
3393
|
-
<
|
|
3688
|
+
<section class="slm-blocked" aria-labelledby="slm-blocked-title">
|
|
3689
|
+
<div class="slm-blockedhead">
|
|
3690
|
+
<p class="slm-eyebrow" id="slm-blocked-title">Blocked inventory</p>
|
|
3691
|
+
<span class="slm-blockedtotal"><b data-ref="blockedcount">0</b> out of sale</span>
|
|
3692
|
+
</div>
|
|
3693
|
+
<p class="slm-selecthelp">Find blocked seats, select only the ones you need, then use \u201CPut back on sale\u201D.</p>
|
|
3694
|
+
<div class="slm-blockedtools">
|
|
3695
|
+
<input type="search" class="slm-input" data-ref="blockedsearch" placeholder="Find seat, row or category" aria-label="Search blocked seats" />
|
|
3696
|
+
<select class="slm-select" data-ref="blockedsection" aria-label="Filter blocked seats by section">
|
|
3697
|
+
<option value="">All sections</option>${blockedSectionOptions}
|
|
3698
|
+
</select>
|
|
3699
|
+
</div>
|
|
3700
|
+
<div class="slm-blockedsummary">
|
|
3701
|
+
<span data-ref="blockedshowing">No blocked seats</span>
|
|
3702
|
+
<button type="button" class="slm-linkbtn" data-ref="selblocked" disabled>Select results</button>
|
|
3703
|
+
</div>
|
|
3704
|
+
<div class="slm-blockedlist" data-ref="blockedlist"></div>
|
|
3705
|
+
</section>
|
|
3706
|
+
<div class="slm-field">
|
|
3707
|
+
<button class="slm-btn ghost" data-ref="markall" style="width:100%" disabled>Put all blocked seats on sale</button>
|
|
3708
|
+
<p class="slm-note slm-allnote" data-ref="markallnote">For a full reset only. You will be asked to confirm.</p>
|
|
3709
|
+
</div>
|
|
3394
3710
|
`;
|
|
3395
3711
|
const r = (n) => this.els.rail.querySelector(`[data-ref="${n}"]`);
|
|
3396
3712
|
this.els.selnum = r("selnum");
|
|
3397
3713
|
this.els.doblock = r("doblock");
|
|
3398
3714
|
this.els.dounblock = r("dounblock");
|
|
3715
|
+
this.els.selmeta = r("selmeta");
|
|
3716
|
+
this.els.blockedcount = r("blockedcount");
|
|
3717
|
+
this.els.blockedshowing = r("blockedshowing");
|
|
3718
|
+
this.els.blockedlist = r("blockedlist");
|
|
3719
|
+
this.els.selblocked = r("selblocked");
|
|
3720
|
+
this.els.markall = r("markall");
|
|
3721
|
+
this.els.markallnote = r("markallnote");
|
|
3399
3722
|
r("doblock").addEventListener("click", () => void this.block());
|
|
3400
3723
|
r("dounblock").addEventListener("click", () => void this.unblock());
|
|
3401
3724
|
r("selall").addEventListener("click", () => this.selectAll());
|
|
3402
3725
|
r("clearsel").addEventListener("click", () => this.clearSelection());
|
|
3403
|
-
r("markall").addEventListener("click", () =>
|
|
3404
|
-
this.els.rail.querySelectorAll("[data-cat]").forEach((b) => b.addEventListener("click", () => this.
|
|
3726
|
+
r("markall").addEventListener("click", () => this.confirmUnblockAll());
|
|
3727
|
+
this.els.rail.querySelectorAll("[data-cat]").forEach((b) => b.addEventListener("click", () => this.toggleCategory(b.dataset.cat)));
|
|
3405
3728
|
const sectionSel = this.els.rail.querySelector('[data-ref="section"]');
|
|
3406
3729
|
sectionSel?.addEventListener("change", () => {
|
|
3407
3730
|
if (sectionSel.value) {
|
|
@@ -3409,6 +3732,32 @@ var SeatManager = class {
|
|
|
3409
3732
|
sectionSel.value = "";
|
|
3410
3733
|
}
|
|
3411
3734
|
});
|
|
3735
|
+
const blockedSearch = r("blockedsearch");
|
|
3736
|
+
const blockedSection = r("blockedsection");
|
|
3737
|
+
blockedSearch.value = this.blockedQuery;
|
|
3738
|
+
blockedSection.value = this.blockedSection;
|
|
3739
|
+
blockedSearch.addEventListener("input", () => {
|
|
3740
|
+
this.blockedQuery = blockedSearch.value;
|
|
3741
|
+
this.blockedResultLimit = 100;
|
|
3742
|
+
this.paintBlockedInventory();
|
|
3743
|
+
});
|
|
3744
|
+
blockedSection.addEventListener("change", () => {
|
|
3745
|
+
this.blockedSection = blockedSection.value;
|
|
3746
|
+
this.blockedResultLimit = 100;
|
|
3747
|
+
this.paintBlockedInventory();
|
|
3748
|
+
});
|
|
3749
|
+
r("selblocked").addEventListener("click", () => {
|
|
3750
|
+
this.toggleLabels(this.filteredBlockedSeats().map((seat) => seat.label));
|
|
3751
|
+
});
|
|
3752
|
+
r("blockedlist").addEventListener("click", (event) => {
|
|
3753
|
+
const target = event.target;
|
|
3754
|
+
const seatButton = target.closest("[data-blocked-label]");
|
|
3755
|
+
if (seatButton?.dataset.blockedLabel) this.toggleLabels([seatButton.dataset.blockedLabel]);
|
|
3756
|
+
else if (target.closest("[data-blocked-more]")) {
|
|
3757
|
+
this.blockedResultLimit += 100;
|
|
3758
|
+
this.paintBlockedInventory();
|
|
3759
|
+
}
|
|
3760
|
+
});
|
|
3412
3761
|
const rel = r("release");
|
|
3413
3762
|
rel.addEventListener("change", () => {
|
|
3414
3763
|
const ms = rel.value ? new Date(rel.value).getTime() : NaN;
|
|
@@ -3418,18 +3767,140 @@ var SeatManager = class {
|
|
|
3418
3767
|
});
|
|
3419
3768
|
this.paintSelBar(this.getSelection());
|
|
3420
3769
|
}
|
|
3421
|
-
|
|
3770
|
+
toggleCategory(catKey) {
|
|
3422
3771
|
const labels = [];
|
|
3423
|
-
for (const [label, seat] of this.labelToSeat.entries())
|
|
3424
|
-
|
|
3772
|
+
for (const [label, seat] of this.labelToSeat.entries()) {
|
|
3773
|
+
if (seat.categoryKey === catKey && this.isBlockSelectable(label)) labels.push(label);
|
|
3774
|
+
}
|
|
3775
|
+
this.toggleLabels(labels);
|
|
3776
|
+
}
|
|
3777
|
+
/** A category/filter is a real toggle: add the missing seats, or remove the
|
|
3778
|
+
* whole group when every eligible seat in it is already selected. */
|
|
3779
|
+
toggleLabels(labels) {
|
|
3780
|
+
if (!this.renderer) return;
|
|
3781
|
+
const eligible = labels.filter((label) => this.labelToSeat.has(label) && this.isBlockSelectable(label));
|
|
3782
|
+
if (!eligible.length) return;
|
|
3783
|
+
const selected = new Set(this.selectionLabels());
|
|
3784
|
+
const allSelected = eligible.every((label) => selected.has(label));
|
|
3785
|
+
if (allSelected) {
|
|
3786
|
+
const ids = eligible.map((label) => this.labelToId.get(label)).filter((id) => Boolean(id));
|
|
3787
|
+
this.renderer.deselect(ids);
|
|
3788
|
+
} else {
|
|
3789
|
+
this.renderer.selectByLabels(eligible);
|
|
3790
|
+
}
|
|
3791
|
+
this.syncSelection();
|
|
3792
|
+
}
|
|
3793
|
+
isBlockSelectable(label) {
|
|
3794
|
+
const status = this.status.get(label) ?? "free";
|
|
3795
|
+
return status === "free" || status === "blocked";
|
|
3425
3796
|
}
|
|
3426
3797
|
paintSelBar(seats) {
|
|
3427
3798
|
if (!this.els.selnum) return;
|
|
3428
3799
|
this.els.selnum.textContent = seats.length.toLocaleString();
|
|
3429
|
-
const
|
|
3430
|
-
const
|
|
3431
|
-
this.els.
|
|
3432
|
-
this.els.
|
|
3800
|
+
const freeCount = seats.filter((s) => (this.status.get(s.label) ?? "free") === "free").length;
|
|
3801
|
+
const blockedCount = seats.filter((s) => this.status.get(s.label) === "blocked").length;
|
|
3802
|
+
this.els.selmeta.textContent = seats.length ? `${freeCount.toLocaleString()} available \xB7 ${blockedCount.toLocaleString()} blocked` : "selected";
|
|
3803
|
+
const blockButton = this.els.doblock;
|
|
3804
|
+
const unblockButton = this.els.dounblock;
|
|
3805
|
+
blockButton.disabled = freeCount === 0;
|
|
3806
|
+
unblockButton.disabled = blockedCount === 0;
|
|
3807
|
+
blockButton.textContent = freeCount ? `Block ${freeCount.toLocaleString()}` : "Block selected";
|
|
3808
|
+
unblockButton.textContent = blockedCount ? `Put ${blockedCount.toLocaleString()} on sale` : "Put back on sale";
|
|
3809
|
+
this.paintCategoryControls(seats);
|
|
3810
|
+
this.paintBlockedInventory();
|
|
3811
|
+
}
|
|
3812
|
+
paintCategoryControls(seats) {
|
|
3813
|
+
const selected = new Set(seats.map((seat) => seat.label));
|
|
3814
|
+
this.els.rail?.querySelectorAll("[data-cat]").forEach((button) => {
|
|
3815
|
+
const catKey = button.dataset.cat;
|
|
3816
|
+
const labels = [];
|
|
3817
|
+
for (const [label, seat] of this.labelToSeat.entries()) {
|
|
3818
|
+
if (seat.categoryKey === catKey && this.isBlockSelectable(label)) labels.push(label);
|
|
3819
|
+
}
|
|
3820
|
+
const picked = labels.filter((label) => selected.has(label)).length;
|
|
3821
|
+
const full = labels.length > 0 && picked === labels.length;
|
|
3822
|
+
const partial = picked > 0 && !full;
|
|
3823
|
+
button.disabled = labels.length === 0;
|
|
3824
|
+
button.classList.toggle("on", full);
|
|
3825
|
+
button.classList.toggle("partial", partial);
|
|
3826
|
+
button.setAttribute("aria-pressed", full ? "true" : partial ? "mixed" : "false");
|
|
3827
|
+
button.setAttribute("title", full ? `Remove all ${labels.length.toLocaleString()} seats in this category from the selection` : partial ? `Select the remaining ${(labels.length - picked).toLocaleString()} seats in this category` : `Select all ${labels.length.toLocaleString()} seats in this category`);
|
|
3828
|
+
const count = button.querySelector("[data-cat-count]");
|
|
3829
|
+
if (count) count.textContent = picked ? `${picked.toLocaleString()}/${labels.length.toLocaleString()}` : labels.length.toLocaleString();
|
|
3830
|
+
});
|
|
3831
|
+
}
|
|
3832
|
+
filteredBlockedSeats() {
|
|
3833
|
+
const query = this.blockedQuery.trim().toLocaleLowerCase();
|
|
3834
|
+
const seats = [];
|
|
3835
|
+
for (const [label, seat] of this.labelToSeat.entries()) {
|
|
3836
|
+
if (this.status.get(label) !== "blocked") continue;
|
|
3837
|
+
const sectionId = this.sectionByObject.get(seat.rowId) ?? UNGROUPED_ID;
|
|
3838
|
+
if (this.blockedSection && sectionId !== this.blockedSection) continue;
|
|
3839
|
+
if (query) {
|
|
3840
|
+
const category = this.doc?.categories.find((item) => item.key === seat.categoryKey)?.label ?? seat.categoryKey;
|
|
3841
|
+
const section = this.sectionLabelById.get(sectionId) ?? "Other seats";
|
|
3842
|
+
const object = this.doc?.objects.find((item) => item.id === seat.rowId);
|
|
3843
|
+
const objectLabel = object?.type === "row" || object?.type === "table" ? object.label : "";
|
|
3844
|
+
const haystack = `${label} ${category} ${section} ${objectLabel}`.toLocaleLowerCase();
|
|
3845
|
+
if (!haystack.includes(query)) continue;
|
|
3846
|
+
}
|
|
3847
|
+
seats.push(seat);
|
|
3848
|
+
}
|
|
3849
|
+
return seats.sort((a, b) => a.label.localeCompare(b.label, void 0, { numeric: true, sensitivity: "base" }));
|
|
3850
|
+
}
|
|
3851
|
+
paintBlockedInventory() {
|
|
3852
|
+
if (!this.els.blockedlist) return;
|
|
3853
|
+
const allBlocked = [...this.status.entries()].filter(([, status]) => status === "blocked").length;
|
|
3854
|
+
const filtered = this.filteredBlockedSeats();
|
|
3855
|
+
const visible = filtered.slice(0, this.blockedResultLimit);
|
|
3856
|
+
const selected = new Set(this.selectionLabels());
|
|
3857
|
+
const selectedResults = filtered.filter((seat) => selected.has(seat.label)).length;
|
|
3858
|
+
const allResultsSelected = filtered.length > 0 && selectedResults === filtered.length;
|
|
3859
|
+
this.els.blockedcount.textContent = allBlocked.toLocaleString();
|
|
3860
|
+
this.els.blockedshowing.textContent = filtered.length ? `Showing ${visible.length.toLocaleString()} of ${filtered.length.toLocaleString()}` : allBlocked ? "No matches" : "No blocked seats";
|
|
3861
|
+
const selectResults = this.els.selblocked;
|
|
3862
|
+
selectResults.disabled = filtered.length === 0;
|
|
3863
|
+
selectResults.textContent = allResultsSelected ? `Remove ${filtered.length.toLocaleString()} results` : `Select ${filtered.length.toLocaleString()} results`;
|
|
3864
|
+
this.els.blockedlist.innerHTML = visible.length ? visible.map((seat) => {
|
|
3865
|
+
const sectionId = this.sectionByObject.get(seat.rowId) ?? UNGROUPED_ID;
|
|
3866
|
+
const section = this.sectionLabelById.get(sectionId) ?? "Other seats";
|
|
3867
|
+
const category = this.doc?.categories.find((item) => item.key === seat.categoryKey)?.label ?? seat.categoryKey;
|
|
3868
|
+
const isSelected = selected.has(seat.label);
|
|
3869
|
+
return `<button type="button" class="slm-blockeditem${isSelected ? " on" : ""}" data-blocked-label="${esc(seat.label)}" aria-pressed="${isSelected}">
|
|
3870
|
+
<span class="slm-blockedcheck" aria-hidden="true">\u2713</span>
|
|
3871
|
+
<span class="slm-blockedcopy"><span class="slm-blockedlabel">${esc(seat.label)}</span>
|
|
3872
|
+
<span class="slm-blockedmeta">${esc(section)} \xB7 ${esc(category)}</span></span>
|
|
3873
|
+
</button>`;
|
|
3874
|
+
}).join("") + (filtered.length > visible.length ? `<button type="button" class="slm-blockedmore" data-blocked-more>Show 100 more</button>` : "") : `<div class="slm-blockedempty">${allBlocked ? "No blocked seats match this search or section." : "No seats are blocked. Newly blocked seats will appear here."}</div>`;
|
|
3875
|
+
const markAll = this.els.markall;
|
|
3876
|
+
const armed = markAll.dataset.confirm === "true";
|
|
3877
|
+
markAll.disabled = allBlocked === 0;
|
|
3878
|
+
markAll.textContent = armed ? `Confirm: put all ${allBlocked.toLocaleString()} on sale` : `Put all ${allBlocked.toLocaleString()} blocked seats on sale`;
|
|
3879
|
+
}
|
|
3880
|
+
confirmUnblockAll() {
|
|
3881
|
+
const button = this.els.markall;
|
|
3882
|
+
if (!button || button.disabled) return;
|
|
3883
|
+
if (button.dataset.confirm === "true") {
|
|
3884
|
+
this.resetUnblockAllConfirm();
|
|
3885
|
+
void this.unblockAll();
|
|
3886
|
+
return;
|
|
3887
|
+
}
|
|
3888
|
+
button.dataset.confirm = "true";
|
|
3889
|
+
button.classList.add("danger");
|
|
3890
|
+
this.els.markallnote.textContent = "This changes every blocked seat. Click the red button again to confirm.";
|
|
3891
|
+
this.paintBlockedInventory();
|
|
3892
|
+
if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
|
|
3893
|
+
this.unblockAllConfirmTimer = setTimeout(() => this.resetUnblockAllConfirm(), 6e3);
|
|
3894
|
+
}
|
|
3895
|
+
resetUnblockAllConfirm() {
|
|
3896
|
+
if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
|
|
3897
|
+
this.unblockAllConfirmTimer = null;
|
|
3898
|
+
const button = this.els.markall;
|
|
3899
|
+
if (!button) return;
|
|
3900
|
+
delete button.dataset.confirm;
|
|
3901
|
+
button.classList.remove("danger");
|
|
3902
|
+
if (this.els.markallnote) this.els.markallnote.textContent = "For a full reset only. You will be asked to confirm.";
|
|
3903
|
+
this.paintBlockedInventory();
|
|
3433
3904
|
}
|
|
3434
3905
|
// ---- toast / done / fail --------------------------------------------------
|
|
3435
3906
|
done(action, labels, msg) {
|