@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.cjs
CHANGED
|
@@ -458,8 +458,12 @@ var CSS = `
|
|
|
458
458
|
.sl-head-meta{font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--sl-muted);margin-top:3px;
|
|
459
459
|
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
|
|
460
460
|
.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
|
-
|
|
461
|
+
background:var(--sl-accent);color:var(--sl-accent-ink);font-weight:700;font-size:12px;font-variant-numeric:tabular-nums;
|
|
462
|
+
transform-origin:right center}
|
|
463
|
+
.sl-hold-pill.on{display:inline-flex;animation:slPillIn .34s cubic-bezier(.2,.8,.2,1) both}
|
|
464
|
+
.sl-hold-dot{width:7px;height:7px;border-radius:50%;background:currentColor;opacity:.78;box-shadow:0 0 0 0 currentColor}
|
|
465
|
+
.sl-hold-pill.is-expiring .sl-hold-dot{animation:slHoldPulse 1.4s ease-out infinite}
|
|
466
|
+
.sl-hold-time{min-width:3.35em;text-align:left}
|
|
463
467
|
.sl-close{width:32px;height:32px;border-radius:999px;flex:none;display:none;align-items:center;justify-content:center;
|
|
464
468
|
border:1px solid var(--sl-line);color:var(--sl-muted);transition:color .15s,border-color .15s}
|
|
465
469
|
.sl-close:hover{color:var(--sl-text);border-color:var(--sl-muted)}
|
|
@@ -481,7 +485,7 @@ var CSS = `
|
|
|
481
485
|
.sl-picker[data-layout="narrow"] .sl-body{flex-direction:column}
|
|
482
486
|
.sl-picker[data-layout="narrow"] .sl-map{min-height:0;flex:1}
|
|
483
487
|
.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 .
|
|
488
|
+
flex:none;height:50%;transition:height .3s cubic-bezier(.2,.8,.2,1)}
|
|
485
489
|
.sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side{height:86px;overflow:hidden}
|
|
486
490
|
.sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side > :not(.sl-sheet-head){display:none}
|
|
487
491
|
.sl-picker[data-layout="narrow"] .sl-tray{flex:none}
|
|
@@ -537,10 +541,17 @@ var CSS = `
|
|
|
537
541
|
.sl-tray{flex:1;padding:10px 16px;display:flex;flex-direction:column;gap:8px;min-height:0}
|
|
538
542
|
.sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
|
|
539
543
|
.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}
|
|
544
|
+
border-radius:var(--sl-r-sm);background:var(--sl-surface);font-size:13px;transform-origin:center}
|
|
545
|
+
.sl-chip.sl-enter{animation:slChipIn .38s cubic-bezier(.2,.8,.2,1) both}
|
|
546
|
+
.sl-chip.sl-leave{pointer-events:none;animation:slChipOut .16s ease-in both}
|
|
547
|
+
.sl-chip.sl-held{border-color:var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
|
|
548
|
+
box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent)}
|
|
541
549
|
.sl-chip b{font-weight:800}
|
|
542
550
|
.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
551
|
.sl-chip .amt{font-weight:700;font-variant-numeric:tabular-nums}
|
|
552
|
+
.sl-chip-state{flex:none;padding:3px 6px;border-radius:999px;background:var(--sl-accent);color:var(--sl-accent-ink);
|
|
553
|
+
font-size:8.5px;line-height:1;font-weight:900;letter-spacing:.08em;text-transform:uppercase}
|
|
554
|
+
.sl-chip-state.sl-pending{background:transparent;color:var(--sl-muted);border:1px solid var(--sl-line)}
|
|
544
555
|
.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
556
|
.sl-chip .rm:hover{color:var(--sl-text)}
|
|
546
557
|
.sl-chip .rm svg{width:11px;height:11px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round}
|
|
@@ -558,17 +569,30 @@ var CSS = `
|
|
|
558
569
|
|
|
559
570
|
/* footer */
|
|
560
571
|
.sl-foot{padding:12px 16px 14px;border-top:1px solid var(--sl-line);flex:none}
|
|
572
|
+
.sl-hold-note{display:none;align-items:center;gap:9px;margin-bottom:10px;padding:9px 10px;border-radius:var(--sl-r-sm);
|
|
573
|
+
border:1px solid var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
|
|
574
|
+
box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent);
|
|
575
|
+
font-size:11.5px;line-height:1.35;color:var(--sl-muted)}
|
|
576
|
+
.sl-hold-note.on{display:flex;animation:slNoticeIn .38s cubic-bezier(.2,.8,.2,1) both}
|
|
577
|
+
.sl-hold-note svg{width:16px;height:16px;flex:none;stroke:var(--sl-accent);stroke-width:2.4;fill:none;
|
|
578
|
+
stroke-linecap:round;stroke-linejoin:round}
|
|
579
|
+
.sl-hold-note b{display:block;color:var(--sl-text);font-size:12px}
|
|
580
|
+
.sl-hold-copy{display:block}
|
|
561
581
|
.sl-total{display:flex;justify-content:space-between;align-items:center;font-size:13px;margin-bottom:10px}
|
|
562
582
|
.sl-total b{font-size:17px;font-variant-numeric:tabular-nums}
|
|
583
|
+
.sl-value-pop{animation:slValuePop .32s cubic-bezier(.2,.8,.2,1)}
|
|
563
584
|
/* Primary checkout CTA. Scoped under .sl-picker so it OUTWEIGHS the
|
|
564
585
|
'.sl-picker button' reset (0,1,1) \u2014 an unscoped '.sl-cta' (0,1,0) loses to it
|
|
565
586
|
and the button renders as plain text with no accent fill. */
|
|
566
587
|
.sl-picker .sl-cta{display:flex;align-items:center;justify-content:center;width:100%;min-height:44px;
|
|
567
588
|
padding:12px 16px;border-radius:var(--sl-r-sm);font-weight:800;font-size:14px;line-height:1.1;
|
|
568
589
|
background:var(--sl-accent);color:var(--sl-accent-ink);
|
|
569
|
-
transition:filter .15s,background .
|
|
590
|
+
transition:filter .15s,background .22s,color .22s,transform .12s,box-shadow .22s;gap:8px}
|
|
570
591
|
.sl-picker .sl-cta:hover{filter:brightness(1.08)}
|
|
571
592
|
.sl-picker .sl-cta:active{transform:translateY(1px);filter:brightness(.94)}
|
|
593
|
+
.sl-picker .sl-cta.sl-ready{animation:slCtaReady .42s cubic-bezier(.2,.8,.2,1)}
|
|
594
|
+
.sl-cta-spin,.sl-ba-spin{width:14px;height:14px;border-radius:50%;border:2px solid currentColor;border-right-color:transparent;
|
|
595
|
+
animation:slspin .7s linear infinite;flex:none}
|
|
572
596
|
/* Disabled ("Select seats"): quieter, but still a full-width button shape. */
|
|
573
597
|
.sl-picker .sl-cta:disabled{background:var(--sl-surface);color:var(--sl-muted);opacity:1;
|
|
574
598
|
cursor:not-allowed;filter:none;transform:none}
|
|
@@ -606,11 +630,15 @@ var CSS = `
|
|
|
606
630
|
.sl-zoom svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
607
631
|
|
|
608
632
|
/* toast + boot states (toast flows in the bottom-center region) */
|
|
609
|
-
.sl-toast{transform:translateY(6px);max-width:100%;
|
|
633
|
+
.sl-toast{transform:translateY(6px) scale(.98);max-width:100%;
|
|
610
634
|
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 .
|
|
635
|
+
font-size:12.5px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .22s,transform .22s;white-space:nowrap;
|
|
612
636
|
overflow:hidden;text-overflow:ellipsis}
|
|
613
|
-
.sl-toast.on{opacity:1;transform:translateY(0)}
|
|
637
|
+
.sl-toast.on{opacity:1;transform:translateY(0) scale(1)}
|
|
638
|
+
.sl-toast[data-tone="error"]{border-color:#ef4444}
|
|
639
|
+
.sl-toast[data-tone="warning"]{border-color:var(--sl-accent)}
|
|
640
|
+
.sl-toast[data-tone="success"]{border-color:#22c55e}
|
|
641
|
+
.sl-toast.on[data-tone="error"]{animation:slToastNudge .32s ease-out}
|
|
614
642
|
.sl-boot{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|
615
643
|
gap:10px;background:var(--sl-bg);font-size:13px;font-weight:600;color:var(--sl-muted)}
|
|
616
644
|
.sl-boot-spin{width:24px;height:24px;border-radius:50%;border:3px solid var(--sl-line);border-top-color:var(--sl-accent);
|
|
@@ -634,15 +662,22 @@ var CSS = `
|
|
|
634
662
|
.sl-extend-btn:disabled{opacity:.5;cursor:not-allowed}
|
|
635
663
|
|
|
636
664
|
/* 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
|
-
|
|
665
|
+
.sl-booked{position:absolute;inset:0;z-index:11;display:flex;flex-direction:column;align-items:center;
|
|
666
|
+
justify-content:center;gap:12px;text-align:center;padding:28px;background:var(--sl-bg);opacity:0;visibility:hidden;
|
|
667
|
+
pointer-events:none;transition:opacity .34s ease,visibility 0s linear .34s}
|
|
668
|
+
.sl-booked.on{opacity:1;visibility:visible;pointer-events:auto;transition:opacity .34s ease,visibility 0s}
|
|
640
669
|
.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
|
|
670
|
+
background:var(--sl-accent);color:var(--sl-accent-ink);transform:scale(.72)}
|
|
671
|
+
.sl-booked.on .sl-booked-badge{animation:slSuccessPop .58s cubic-bezier(.2,1.25,.3,1) .08s both}
|
|
672
|
+
.sl-booked-badge svg{width:30px;height:30px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round;
|
|
673
|
+
stroke-dasharray:30;stroke-dashoffset:30}
|
|
674
|
+
.sl-booked.on .sl-booked-badge svg{animation:slCheckDraw .42s ease-out .32s forwards}
|
|
643
675
|
.sl-booked-title{font-weight:800;font-size:19px;color:var(--sl-text)}
|
|
644
676
|
.sl-booked-sub{font-size:13px;color:var(--sl-muted);line-height:1.5;max-width:320px}
|
|
645
677
|
.sl-booked-seats{font-weight:700;color:var(--sl-text)}
|
|
678
|
+
.sl-booked.on .sl-booked-title,.sl-booked.on .sl-booked-sub{animation:slCopyRise .42s ease-out both}
|
|
679
|
+
.sl-booked.on .sl-booked-title{animation-delay:.22s}
|
|
680
|
+
.sl-booked.on .sl-booked-sub{animation-delay:.3s}
|
|
646
681
|
|
|
647
682
|
/* a11y filter chips (flow within the top-left region) */
|
|
648
683
|
.sl-chips{display:flex;gap:6px;flex-wrap:wrap}
|
|
@@ -653,7 +688,8 @@ var CSS = `
|
|
|
653
688
|
|
|
654
689
|
/* confirm popover */
|
|
655
690
|
.sl-confirm{position:absolute;z-index:9;min-width:190px;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
656
|
-
border-radius:12px;padding:12px;box-shadow:0 18px 50px -18px rgba(0,0,0,.7);transform:translate(-50%,calc(-100% - 14px))
|
|
691
|
+
border-radius:12px;padding:12px;box-shadow:0 18px 50px -18px rgba(0,0,0,.7);transform:translate(-50%,calc(-100% - 14px));
|
|
692
|
+
animation:slConfirmIn .24s cubic-bezier(.2,.8,.2,1) both}
|
|
657
693
|
.sl-confirm-label{font-weight:800;font-size:14px}
|
|
658
694
|
.sl-confirm-meta{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--sl-muted);margin-top:4px}
|
|
659
695
|
.sl-confirm-meta b{color:var(--sl-text);margin-left:auto}
|
|
@@ -671,8 +707,10 @@ var CSS = `
|
|
|
671
707
|
.sl-ba-qty button{width:24px;height:24px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
672
708
|
font-size:14px;font-weight:700;display:flex;align-items:center;justify-content:center}
|
|
673
709
|
.sl-ba-qty span{min-width:14px;text-align:center;font-weight:800}
|
|
674
|
-
.sl-ba-go{margin-left:auto;padding:7px 12px;border-radius:999px;border:1px solid var(--sl-line);font-weight:700;font-size:12px;
|
|
710
|
+
.sl-ba-go{margin-left:auto;padding:7px 12px;border-radius:999px;border:1px solid var(--sl-line);font-weight:700;font-size:12px;
|
|
711
|
+
transition:border-color .15s,opacity .15s;display:flex;align-items:center;gap:6px}
|
|
675
712
|
.sl-ba-go:hover{border-color:var(--sl-muted)}
|
|
713
|
+
.sl-ba-go:disabled{opacity:.62;cursor:wait}
|
|
676
714
|
|
|
677
715
|
/* screen-reader live region */
|
|
678
716
|
.sl-sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
|
@@ -778,6 +816,27 @@ var CSS = `
|
|
|
778
816
|
.sl-price-row.sl-dim{opacity:.4}
|
|
779
817
|
.sl-seccard-mix-item.sl-dim{opacity:.4}
|
|
780
818
|
|
|
819
|
+
/* Buyer-journey motion: every animation explains a state transition (selected,
|
|
820
|
+
held, checkout handoff, conflict or booked). No decorative infinite motion
|
|
821
|
+
except the expiring-hold pulse and active progress spinners. */
|
|
822
|
+
@keyframes slPillIn{from{opacity:0;transform:translateX(7px) scale(.9)}to{opacity:1;transform:translateX(0) scale(1)}}
|
|
823
|
+
@keyframes slHoldPulse{0%{box-shadow:0 0 0 0 currentColor;opacity:.9}75%,100%{box-shadow:0 0 0 7px transparent;opacity:.55}}
|
|
824
|
+
@keyframes slChipIn{from{opacity:0;transform:translateY(8px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}
|
|
825
|
+
@keyframes slChipOut{to{opacity:0;transform:translateX(10px) scale(.98)}}
|
|
826
|
+
@keyframes slNoticeIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
|
|
827
|
+
@keyframes slValuePop{0%{opacity:.6;transform:translateY(3px)}55%{transform:translateY(-1px) scale(1.05)}100%{opacity:1;transform:none}}
|
|
828
|
+
@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}}
|
|
829
|
+
@keyframes slToastNudge{0%,100%{margin-left:0}30%{margin-left:-4px}60%{margin-left:3px}}
|
|
830
|
+
@keyframes slSuccessPop{0%{opacity:0;transform:scale(.72)}65%{opacity:1;transform:scale(1.08)}100%{opacity:1;transform:scale(1)}}
|
|
831
|
+
@keyframes slCheckDraw{to{stroke-dashoffset:0}}
|
|
832
|
+
@keyframes slCopyRise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
|
833
|
+
@keyframes slConfirmIn{from{opacity:0;transform:translate(-50%,calc(-100% - 8px)) scale(.96)}to{opacity:1;transform:translate(-50%,calc(-100% - 14px)) scale(1)}}
|
|
834
|
+
|
|
835
|
+
@media(prefers-reduced-motion:reduce){
|
|
836
|
+
.sl-picker *,.sl-modal-scrim *{animation-duration:.001ms!important;animation-iteration-count:1!important;
|
|
837
|
+
transition-duration:.001ms!important;scroll-behavior:auto!important}
|
|
838
|
+
}
|
|
839
|
+
|
|
781
840
|
/* modal host */
|
|
782
841
|
.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
842
|
.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 +877,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
818
877
|
this.ro = null;
|
|
819
878
|
this.holdTimer = null;
|
|
820
879
|
this.toastTimer = null;
|
|
880
|
+
/** Short-lived UI motion timers; all are cancelled on destroy. */
|
|
881
|
+
this.motionTimers = /* @__PURE__ */ new Set();
|
|
821
882
|
// state
|
|
822
883
|
this.currency = "USD";
|
|
823
884
|
this.hold = null;
|
|
@@ -861,6 +922,12 @@ var SeatPicker = class _SeatPicker {
|
|
|
861
922
|
this.secCardShownAt = 0;
|
|
862
923
|
/** Previous tray ticket count — first 0→n transition auto-expands the mobile sheet. */
|
|
863
924
|
this.lastTrayCount = 0;
|
|
925
|
+
/** Previous computed total — drives a single explanatory value bump. */
|
|
926
|
+
this.lastTrayTotal = 0;
|
|
927
|
+
/** Stable item keys prevent tray chips re-animating on unrelated realtime syncs. */
|
|
928
|
+
this.lastTrayKeys = /* @__PURE__ */ new Set();
|
|
929
|
+
this.bestAvailableBusy = false;
|
|
930
|
+
this.ctaPhase = "idle";
|
|
864
931
|
// narrow-layout chrome that docks into the sheet's Filters row on mobile
|
|
865
932
|
this.a11yChipsEl = null;
|
|
866
933
|
this.cbEl = null;
|
|
@@ -874,9 +941,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
874
941
|
if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
|
|
875
942
|
if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
|
|
876
943
|
this.opts = options;
|
|
877
|
-
|
|
944
|
+
this.api = new PubApi((options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, ""));
|
|
878
945
|
this.controller = new import_core2.PickerController({
|
|
879
|
-
transport: api,
|
|
946
|
+
transport: this.api,
|
|
880
947
|
eventKey: options.event,
|
|
881
948
|
maxSelection: options.maxSelection ?? DEFAULT_MAX_SELECTION2,
|
|
882
949
|
currency: options.currency,
|
|
@@ -896,14 +963,16 @@ var SeatPicker = class _SeatPicker {
|
|
|
896
963
|
this.hold = null;
|
|
897
964
|
this.handedOff = false;
|
|
898
965
|
this.bookedShown = false;
|
|
966
|
+
this.ctaPhase = "idle";
|
|
899
967
|
this.stopHoldTimer();
|
|
900
968
|
this.gaQty.clear();
|
|
901
|
-
this.toast((0, import_core2.t)("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.");
|
|
969
|
+
this.toast((0, import_core2.t)("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.", "warning");
|
|
902
970
|
this.syncTray();
|
|
903
971
|
this.opts.onHoldExpired?.();
|
|
904
972
|
},
|
|
905
973
|
confirmSelection: options.confirmSelection,
|
|
906
974
|
onSelect: (seat) => {
|
|
975
|
+
this.flashPickedSeat(seat.id);
|
|
907
976
|
if (this.opts.confirmSelection) this.showConfirm(seat);
|
|
908
977
|
},
|
|
909
978
|
onViewChange: () => {
|
|
@@ -945,10 +1014,19 @@ var SeatPicker = class _SeatPicker {
|
|
|
945
1014
|
const picker = new _SeatPicker({ ...options, container: frame });
|
|
946
1015
|
picker.modalScrim = scrim;
|
|
947
1016
|
picker.prevFocus = document.activeElement;
|
|
1017
|
+
let closing = false;
|
|
948
1018
|
const close = () => {
|
|
1019
|
+
if (closing) return;
|
|
1020
|
+
closing = true;
|
|
949
1021
|
document.body.style.overflow = prevOverflow;
|
|
950
|
-
|
|
951
|
-
|
|
1022
|
+
scrim.style.opacity = "0";
|
|
1023
|
+
scrim.style.pointerEvents = "none";
|
|
1024
|
+
const finish = () => {
|
|
1025
|
+
picker.destroy();
|
|
1026
|
+
options.onClose?.();
|
|
1027
|
+
};
|
|
1028
|
+
if (picker.hold && !picker.handedOff) void picker.release().finally(finish);
|
|
1029
|
+
else finish();
|
|
952
1030
|
};
|
|
953
1031
|
picker.closeModal = close;
|
|
954
1032
|
scrim.addEventListener("mousedown", (e) => {
|
|
@@ -1017,6 +1095,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
1017
1095
|
<div class="sl-sec">Your seats</div>
|
|
1018
1096
|
<div class="sl-tray" data-ref="tray"></div>
|
|
1019
1097
|
<div class="sl-foot">
|
|
1098
|
+
<div class="sl-hold-note" data-ref="holdNote" role="status" aria-live="polite">
|
|
1099
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
|
|
1100
|
+
<span><b data-ref="holdTitle">Seats secured</b><span class="sl-hold-copy" data-ref="holdCopy">Checkout timer is running.</span></span>
|
|
1101
|
+
</div>
|
|
1020
1102
|
<div class="sl-total"><span data-ref="count"></span><b data-ref="total"></b></div>
|
|
1021
1103
|
<button type="button" class="sl-cta" data-ref="cta" disabled></button>
|
|
1022
1104
|
</div>
|
|
@@ -1243,6 +1325,85 @@ var SeatPicker = class _SeatPicker {
|
|
|
1243
1325
|
cssVar(name) {
|
|
1244
1326
|
return this.root ? getComputedStyle(this.root).getPropertyValue(name).trim() : "";
|
|
1245
1327
|
}
|
|
1328
|
+
/** Motion is progressive enhancement; all state remains legible when reduced. */
|
|
1329
|
+
reducedMotion() {
|
|
1330
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1331
|
+
}
|
|
1332
|
+
scheduleMotion(fn, delay) {
|
|
1333
|
+
const timer = setTimeout(() => {
|
|
1334
|
+
this.motionTimers.delete(timer);
|
|
1335
|
+
if (!this.destroyed) fn();
|
|
1336
|
+
}, delay);
|
|
1337
|
+
this.motionTimers.add(timer);
|
|
1338
|
+
}
|
|
1339
|
+
/** Restart one finite CSS animation without leaving a permanent state class. */
|
|
1340
|
+
animateOnce(el, className, duration = 600) {
|
|
1341
|
+
if (!el || this.reducedMotion()) return;
|
|
1342
|
+
el.classList.remove(className);
|
|
1343
|
+
void el.offsetWidth;
|
|
1344
|
+
el.classList.add(className);
|
|
1345
|
+
this.scheduleMotion(() => el.classList.remove(className), duration);
|
|
1346
|
+
}
|
|
1347
|
+
/** Selection feedback belongs on the selected seat, not across the whole map. */
|
|
1348
|
+
flashPickedSeat(id) {
|
|
1349
|
+
if (this.reducedMotion()) return;
|
|
1350
|
+
this.controller.flashSeat(id, this.cssVar("--sl-accent") || "#f4b740");
|
|
1351
|
+
}
|
|
1352
|
+
/** A completed hold gets one short map ripple per concrete seat. */
|
|
1353
|
+
flashHeldSeats(hold) {
|
|
1354
|
+
if (this.reducedMotion()) return;
|
|
1355
|
+
const labels = (hold.items ?? []).filter((item) => item.objectType !== "ga").map((item) => item.label);
|
|
1356
|
+
labels.slice(0, 10).forEach((label, index) => {
|
|
1357
|
+
const seat = this.controller.seatByLabel(label);
|
|
1358
|
+
if (!seat) return;
|
|
1359
|
+
this.scheduleMotion(
|
|
1360
|
+
() => this.controller.flashSeat(seat.id, this.cssVar("--sl-accent") || "#f4b740"),
|
|
1361
|
+
index * 55
|
|
1362
|
+
);
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
/** Update only the action affordance; selection callbacks must not refire. */
|
|
1366
|
+
pendingSelectionCount() {
|
|
1367
|
+
const heldItems = this.hold?.items ?? [];
|
|
1368
|
+
const heldLabels = new Set(heldItems.map((item) => item.label));
|
|
1369
|
+
const pendingSeats = this.controller.getSelection().filter((seat) => !heldLabels.has(seat.label)).length;
|
|
1370
|
+
const heldGA = /* @__PURE__ */ new Map();
|
|
1371
|
+
for (const item of heldItems.filter((candidate) => candidate.objectType === "ga")) {
|
|
1372
|
+
heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
|
|
1373
|
+
}
|
|
1374
|
+
const pendingGA = [...this.gaQty.entries()].reduce(
|
|
1375
|
+
(sum, [areaId, qty]) => sum + Math.max(0, qty - (heldGA.get(areaId) ?? 0)),
|
|
1376
|
+
0
|
|
1377
|
+
);
|
|
1378
|
+
return pendingSeats + pendingGA;
|
|
1379
|
+
}
|
|
1380
|
+
syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
|
|
1381
|
+
const cta = this.els.cta;
|
|
1382
|
+
if (!cta) return;
|
|
1383
|
+
if (this.ctaPhase === "holding") {
|
|
1384
|
+
cta.disabled = true;
|
|
1385
|
+
cta.innerHTML = '<span class="sl-cta-spin" aria-hidden="true"></span>Securing seats\u2026';
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
if (this.ctaPhase === "checkout") {
|
|
1389
|
+
cta.disabled = true;
|
|
1390
|
+
cta.innerHTML = '<span class="sl-cta-spin" aria-hidden="true"></span>Opening checkout\u2026';
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
cta.disabled = count === 0;
|
|
1394
|
+
cta.textContent = this.hold ? pending ? `Secure ${pending} more & checkout` : "Continue to checkout" : count ? "Hold seats & checkout" : "Select seats";
|
|
1395
|
+
}
|
|
1396
|
+
setCtaPhase(phase) {
|
|
1397
|
+
this.ctaPhase = phase;
|
|
1398
|
+
this.syncCta();
|
|
1399
|
+
if (phase === "checkout") {
|
|
1400
|
+
this.scheduleMotion(() => {
|
|
1401
|
+
if (this.ctaPhase !== "checkout") return;
|
|
1402
|
+
this.ctaPhase = "idle";
|
|
1403
|
+
this.syncCta();
|
|
1404
|
+
}, 1100);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1246
1407
|
/** Section-bearing objects on the active floor (single-floor → doc.objects). */
|
|
1247
1408
|
activeFloorObjects() {
|
|
1248
1409
|
const doc = this.controller.doc;
|
|
@@ -1497,7 +1658,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1497
1658
|
(r) => `<button type="button" data-rung="${r}" title="${TIP[r]}" aria-pressed="false">${LABEL[r]}</button>`
|
|
1498
1659
|
).join("");
|
|
1499
1660
|
pills.querySelectorAll("button").forEach((btn) => {
|
|
1500
|
-
btn.addEventListener("click", () =>
|
|
1661
|
+
btn.addEventListener("click", () => {
|
|
1662
|
+
const rung = btn.dataset.rung;
|
|
1663
|
+
this.controller.setRung(rung);
|
|
1664
|
+
if (rung === "seats") this.collapseSectionCard();
|
|
1665
|
+
});
|
|
1501
1666
|
});
|
|
1502
1667
|
this.regions["top-center"].appendChild(pills);
|
|
1503
1668
|
this.rungsEl = pills;
|
|
@@ -1548,7 +1713,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1548
1713
|
this.secCardEl?.remove();
|
|
1549
1714
|
this.secCardEl = null;
|
|
1550
1715
|
if (!summary) return;
|
|
1551
|
-
this.secCardCollapsed =
|
|
1716
|
+
this.secCardCollapsed = this.controller.getRung() === "seats";
|
|
1552
1717
|
this.secCardShownAt = Date.now();
|
|
1553
1718
|
this.renderSectionCard(summary);
|
|
1554
1719
|
}
|
|
@@ -1614,6 +1779,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
1614
1779
|
sectionCardOnView() {
|
|
1615
1780
|
if (!this.secCardEl || this.secCardCollapsed || !this.lastSection) return;
|
|
1616
1781
|
if (this.root?.dataset.layout === "narrow") return;
|
|
1782
|
+
if (this.controller.getRung() === "seats") {
|
|
1783
|
+
this.collapseSectionCard();
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1617
1786
|
if (Date.now() - this.secCardShownAt < 1400) {
|
|
1618
1787
|
if (this.sectionCardCoverage() > 0.25) this.collapseSectionCard();
|
|
1619
1788
|
return;
|
|
@@ -1831,7 +2000,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1831
2000
|
const gone = this.controller.getSelection().filter((s) => !ownLabels.has(s.label) && (this.controller.getStatus(s.id) ?? "free") !== "free");
|
|
1832
2001
|
if (!gone.length) return;
|
|
1833
2002
|
this.controller.deselect(gone.map((s) => s.id));
|
|
1834
|
-
this.toast(`Seat ${gone[0].label} was just taken by another buyer
|
|
2003
|
+
this.toast(`Seat ${gone[0].label} was just taken by another buyer.`, "error");
|
|
1835
2004
|
}
|
|
1836
2005
|
syncTray() {
|
|
1837
2006
|
if (!this.els.tray) return;
|
|
@@ -1839,27 +2008,32 @@ var SeatPicker = class _SeatPicker {
|
|
|
1839
2008
|
const gaAreas = this.controller.getGAAreas();
|
|
1840
2009
|
const heldItems = this.hold?.items ?? [];
|
|
1841
2010
|
const parts = [];
|
|
2011
|
+
const nextTrayKeys = /* @__PURE__ */ new Set();
|
|
1842
2012
|
if (!seats.length && !heldItems.length && !gaAreas.length) {
|
|
1843
2013
|
parts.push(`<div class="sl-tray-hint">Tap a seat on the map, or let us pick the best available for you.</div>`);
|
|
1844
2014
|
} else if (!seats.length && !heldItems.length) {
|
|
1845
2015
|
parts.push(`<div class="sl-tray-hint">Tap a seat on the map \u2014 or grab standing tickets below.</div>`);
|
|
1846
2016
|
}
|
|
1847
2017
|
for (const item of heldItems) {
|
|
2018
|
+
const itemKey = `held:${item.label}`;
|
|
2019
|
+
nextTrayKeys.add(itemKey);
|
|
1848
2020
|
const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
|
|
1849
2021
|
const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
|
|
1850
2022
|
const canView2 = this.seatViewEnabled() && item.objectType !== "ga" && !!this.controller.seatByLabel(item.label);
|
|
1851
2023
|
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>` : "") + `</div>`
|
|
2024
|
+
`<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="${(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>` : "") + `</div>`
|
|
1853
2025
|
);
|
|
1854
2026
|
}
|
|
1855
2027
|
const heldLabels = new Set(heldItems.map((item) => item.label));
|
|
1856
2028
|
const canView = this.seatViewEnabled();
|
|
1857
2029
|
for (const s of seats.filter((seat) => !heldLabels.has(seat.label))) {
|
|
2030
|
+
const itemKey = `seat:${s.id}`;
|
|
2031
|
+
nextTrayKeys.add(itemKey);
|
|
1858
2032
|
const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
|
|
1859
2033
|
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
2034
|
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
2035
|
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>`
|
|
2036
|
+
`<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
2037
|
);
|
|
1864
2038
|
}
|
|
1865
2039
|
for (const area of gaAreas) {
|
|
@@ -1871,10 +2045,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1871
2045
|
if (!this.hold) {
|
|
1872
2046
|
const cats = this.controller.doc?.categories ?? [];
|
|
1873
2047
|
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
|
|
2048
|
+
`<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
2049
|
);
|
|
1876
2050
|
}
|
|
1877
2051
|
this.els.tray.innerHTML = parts.join("");
|
|
2052
|
+
this.lastTrayKeys = nextTrayKeys;
|
|
1878
2053
|
this.els.tray.querySelectorAll("[data-ba]").forEach((btn) => {
|
|
1879
2054
|
btn.addEventListener("click", () => {
|
|
1880
2055
|
this.baQty = Math.max(1, Math.min(8, this.baQty + Number(btn.dataset.ba)));
|
|
@@ -1889,8 +2064,14 @@ var SeatPicker = class _SeatPicker {
|
|
|
1889
2064
|
});
|
|
1890
2065
|
this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
|
|
1891
2066
|
btn.addEventListener("click", () => {
|
|
1892
|
-
const
|
|
1893
|
-
|
|
2067
|
+
const chip = btn.closest(".sl-chip");
|
|
2068
|
+
const id = chip.dataset.seat;
|
|
2069
|
+
if (this.reducedMotion()) {
|
|
2070
|
+
this.controller.deselect([id]);
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
chip.classList.add("sl-leave");
|
|
2074
|
+
this.scheduleMotion(() => this.controller.deselect([id]), 150);
|
|
1894
2075
|
});
|
|
1895
2076
|
});
|
|
1896
2077
|
this.els.tray.querySelectorAll(".sl-chip .tier").forEach((sel) => {
|
|
@@ -1919,14 +2100,27 @@ var SeatPicker = class _SeatPicker {
|
|
|
1919
2100
|
const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
|
|
1920
2101
|
const total = freshSeats.reduce((sum, s) => sum + s.price, 0) + gaTotal + heldTotal;
|
|
1921
2102
|
const count = freshSeats.length + gaCount + heldCount;
|
|
2103
|
+
const pendingCount = this.pendingSelectionCount();
|
|
2104
|
+
const previousCount = this.lastTrayCount;
|
|
2105
|
+
const previousTotal = this.lastTrayTotal;
|
|
1922
2106
|
this.els.count.textContent = count ? `${count} ${count === 1 ? "ticket" : "tickets"}` : "No seats selected";
|
|
1923
2107
|
this.els.total.textContent = count ? this.money(total) : "";
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
2108
|
+
this.syncCta(count, pendingCount);
|
|
2109
|
+
if (this.hold) {
|
|
2110
|
+
const securedCount = heldCount || this.hold.seats?.length || 0;
|
|
2111
|
+
if (this.els.holdTitle) {
|
|
2112
|
+
this.els.holdTitle.textContent = `${securedCount} ${securedCount === 1 ? "seat" : "seats"} secured`;
|
|
2113
|
+
}
|
|
2114
|
+
if (this.els.holdCopy) {
|
|
2115
|
+
this.els.holdCopy.textContent = pendingCount ? `${pendingCount} more selected \u2014 secure before checkout.` : "Checkout timer is running.";
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
if (count !== previousCount) this.animateOnce(this.els.count, "sl-value-pop", 380);
|
|
2119
|
+
if (total !== previousTotal) this.animateOnce(this.els.total, "sl-value-pop", 380);
|
|
2120
|
+
if (previousCount === 0 && count > 0) this.animateOnce(this.els.cta, "sl-ready", 520);
|
|
1927
2121
|
if (this.els.peek) {
|
|
1928
2122
|
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>`;
|
|
2123
|
+
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
2124
|
} else {
|
|
1931
2125
|
const prices = (this.controller.doc?.categories ?? []).map((c) => this.catPrice(c)).filter((p) => p != null);
|
|
1932
2126
|
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>`;
|
|
@@ -1936,18 +2130,18 @@ var SeatPicker = class _SeatPicker {
|
|
|
1936
2130
|
this.root.dataset.sheet = "open";
|
|
1937
2131
|
}
|
|
1938
2132
|
this.lastTrayCount = count;
|
|
2133
|
+
this.lastTrayTotal = total;
|
|
1939
2134
|
this.opts.onSelectionChange?.(seats);
|
|
1940
2135
|
}
|
|
1941
2136
|
async handleCta() {
|
|
1942
|
-
const cta = this.els.cta;
|
|
1943
2137
|
if (this.hold && !this.controller.getSelection().some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
|
|
1944
2138
|
const seats = this.hold.seats ?? this.controller.getSelection();
|
|
1945
2139
|
this.handedOff = true;
|
|
2140
|
+
this.setCtaPhase("checkout");
|
|
1946
2141
|
this.opts.onCheckout?.(this.hold, seats, this.buildHandoff(this.hold));
|
|
1947
2142
|
return;
|
|
1948
2143
|
}
|
|
1949
|
-
|
|
1950
|
-
cta.textContent = "Holding\u2026";
|
|
2144
|
+
this.setCtaPhase("holding");
|
|
1951
2145
|
try {
|
|
1952
2146
|
let hold = null;
|
|
1953
2147
|
const gaEntries = [...this.gaQty.entries()].filter(([, q]) => q > 0);
|
|
@@ -1961,18 +2155,23 @@ var SeatPicker = class _SeatPicker {
|
|
|
1961
2155
|
hold = h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : hold;
|
|
1962
2156
|
}
|
|
1963
2157
|
if (!hold) {
|
|
1964
|
-
this.toast("One or more seats were just taken. Please pick again.");
|
|
2158
|
+
this.toast("One or more seats were just taken. Please pick again.", "error");
|
|
2159
|
+
this.setCtaPhase("idle");
|
|
1965
2160
|
this.syncTray();
|
|
1966
2161
|
return;
|
|
1967
2162
|
}
|
|
1968
2163
|
this.hold = hold;
|
|
1969
2164
|
this.handedOff = true;
|
|
1970
2165
|
this.startHoldTimer(hold.expiresAt);
|
|
1971
|
-
this.
|
|
2166
|
+
this.flashHeldSeats(hold);
|
|
2167
|
+
this.setCtaPhase("checkout");
|
|
2168
|
+
this.opts.onCheckout?.(hold, hold.seats ?? chosenSeats, this.buildHandoff(hold));
|
|
1972
2169
|
} catch (err) {
|
|
1973
2170
|
this.opts.onError?.(err);
|
|
1974
|
-
this.toast("One or more seats were just taken. Please pick again.");
|
|
2171
|
+
this.toast("One or more seats were just taken. Please pick again.", "error");
|
|
2172
|
+
this.setCtaPhase("idle");
|
|
1975
2173
|
} finally {
|
|
2174
|
+
if (this.ctaPhase === "holding") this.ctaPhase = "idle";
|
|
1976
2175
|
this.syncTray();
|
|
1977
2176
|
}
|
|
1978
2177
|
}
|
|
@@ -1980,12 +2179,16 @@ var SeatPicker = class _SeatPicker {
|
|
|
1980
2179
|
this.stopHoldTimer();
|
|
1981
2180
|
this.holdExpiresAt = expiresAt;
|
|
1982
2181
|
const pill = this.els.hold;
|
|
2182
|
+
pill.innerHTML = '<span class="sl-hold-dot" aria-hidden="true"></span><span>Held</span><span class="sl-hold-time" data-ref="holdTime"></span>';
|
|
2183
|
+
const time = pill.querySelector('[data-ref="holdTime"]');
|
|
2184
|
+
this.els.holdNote?.classList.add("on");
|
|
1983
2185
|
const tick = () => {
|
|
1984
2186
|
const ms = Math.max(0, this.holdExpiresAt - Date.now());
|
|
1985
2187
|
const m = Math.floor(ms / 6e4);
|
|
1986
2188
|
const s = String(Math.floor(ms % 6e4 / 1e3)).padStart(2, "0");
|
|
1987
|
-
|
|
2189
|
+
if (time) time.textContent = `${m}:${s}`;
|
|
1988
2190
|
pill.classList.add("on");
|
|
2191
|
+
pill.classList.toggle("is-expiring", ms > 0 && ms <= EXTEND_PROMPT_MS);
|
|
1989
2192
|
this.setExtendPrompt(ms > 0 && ms <= EXTEND_PROMPT_MS, ms);
|
|
1990
2193
|
if (ms <= 0) this.stopHoldTimer();
|
|
1991
2194
|
};
|
|
@@ -1995,7 +2198,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
1995
2198
|
stopHoldTimer() {
|
|
1996
2199
|
if (this.holdTimer) clearInterval(this.holdTimer);
|
|
1997
2200
|
this.holdTimer = null;
|
|
1998
|
-
this.els.hold?.classList.remove("on");
|
|
2201
|
+
this.els.hold?.classList.remove("on", "is-expiring");
|
|
2202
|
+
this.els.holdNote?.classList.remove("on");
|
|
1999
2203
|
this.setExtendPrompt(false, 0);
|
|
2000
2204
|
}
|
|
2001
2205
|
/** Show/refresh (or hide) the "Need more time?" prompt with the live seconds left. */
|
|
@@ -2020,13 +2224,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
2020
2224
|
this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
|
|
2021
2225
|
this.holdExpiresAt = h.expiresAt;
|
|
2022
2226
|
this.extendEl?.classList.remove("on");
|
|
2023
|
-
this.toast("More time added \u2014 your seats are still held.");
|
|
2227
|
+
this.toast("More time added \u2014 your seats are still held.", "success");
|
|
2024
2228
|
} else {
|
|
2025
|
-
this.toast("Couldn't add more time \u2014 please head to checkout now.");
|
|
2229
|
+
this.toast("Couldn't add more time \u2014 please head to checkout now.", "warning");
|
|
2026
2230
|
}
|
|
2027
2231
|
} catch (err) {
|
|
2028
2232
|
this.opts.onError?.(err);
|
|
2029
|
-
this.toast("Couldn't add more time \u2014 please head to checkout now.");
|
|
2233
|
+
this.toast("Couldn't add more time \u2014 please head to checkout now.", "warning");
|
|
2030
2234
|
} finally {
|
|
2031
2235
|
btn.disabled = false;
|
|
2032
2236
|
btn.textContent = prev;
|
|
@@ -2074,13 +2278,17 @@ var SeatPicker = class _SeatPicker {
|
|
|
2074
2278
|
const total = lineItems.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
|
|
2075
2279
|
return { holdId: hold.holdId, expiresAt: hold.expiresAt, currency, lineItems, total };
|
|
2076
2280
|
}
|
|
2077
|
-
toast(msg) {
|
|
2281
|
+
toast(msg, tone = "neutral") {
|
|
2078
2282
|
const el = this.els.toast;
|
|
2079
2283
|
if (!el) return;
|
|
2080
2284
|
el.textContent = msg;
|
|
2285
|
+
el.dataset.tone = tone;
|
|
2081
2286
|
el.classList.add("on");
|
|
2082
2287
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
2083
|
-
this.toastTimer = setTimeout(() =>
|
|
2288
|
+
this.toastTimer = setTimeout(() => {
|
|
2289
|
+
el.classList.remove("on");
|
|
2290
|
+
el.dataset.tone = "neutral";
|
|
2291
|
+
}, 4200);
|
|
2084
2292
|
}
|
|
2085
2293
|
placeTooltip() {
|
|
2086
2294
|
if (!this.tipEl) return;
|
|
@@ -2110,6 +2318,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
2110
2318
|
return this.controller.getSelection();
|
|
2111
2319
|
}
|
|
2112
2320
|
async bestAvailable(qty, categoryKey) {
|
|
2321
|
+
if (this.bestAvailableBusy) return null;
|
|
2322
|
+
this.bestAvailableBusy = true;
|
|
2323
|
+
const button = this.els.tray?.querySelector(".sl-ba-go");
|
|
2324
|
+
if (button) {
|
|
2325
|
+
button.disabled = true;
|
|
2326
|
+
button.innerHTML = '<span class="sl-ba-spin" aria-hidden="true"></span>Finding\u2026';
|
|
2327
|
+
}
|
|
2113
2328
|
try {
|
|
2114
2329
|
const h = await this.controller.bestAvailable(qty, categoryKey);
|
|
2115
2330
|
if (h) {
|
|
@@ -2117,30 +2332,58 @@ var SeatPicker = class _SeatPicker {
|
|
|
2117
2332
|
this.handedOff = false;
|
|
2118
2333
|
this.bookedShown = false;
|
|
2119
2334
|
this.startHoldTimer(h.expiresAt);
|
|
2335
|
+
this.flashHeldSeats(this.hold);
|
|
2120
2336
|
this.syncTray();
|
|
2121
2337
|
return this.hold;
|
|
2122
2338
|
}
|
|
2123
2339
|
return null;
|
|
2124
2340
|
} catch (err) {
|
|
2125
2341
|
this.opts.onError?.(err);
|
|
2342
|
+
this.toast("Those seats are no longer available. Try another quantity or category.", "error");
|
|
2126
2343
|
return null;
|
|
2344
|
+
} finally {
|
|
2345
|
+
this.bestAvailableBusy = false;
|
|
2346
|
+
if (!this.hold && button?.isConnected) {
|
|
2347
|
+
button.disabled = false;
|
|
2348
|
+
button.textContent = "Best available";
|
|
2349
|
+
}
|
|
2127
2350
|
}
|
|
2128
2351
|
}
|
|
2129
2352
|
async release() {
|
|
2130
|
-
|
|
2353
|
+
const tracked = this.hold;
|
|
2354
|
+
const controllerHold = this.controller.currentHold();
|
|
2355
|
+
if (controllerHold) {
|
|
2356
|
+
await this.controller.release();
|
|
2357
|
+
} else if (tracked) {
|
|
2358
|
+
const labels = [.../* @__PURE__ */ new Set([
|
|
2359
|
+
...(tracked.items ?? []).map((item) => item.label),
|
|
2360
|
+
...(tracked.seats ?? []).map((seat) => seat.label)
|
|
2361
|
+
])];
|
|
2362
|
+
if (labels.length) {
|
|
2363
|
+
try {
|
|
2364
|
+
await this.api.release(this.opts.event, labels, tracked.holdId);
|
|
2365
|
+
} catch (error) {
|
|
2366
|
+
this.opts.onError?.(error);
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2131
2370
|
this.hold = null;
|
|
2132
2371
|
this.handedOff = false;
|
|
2133
2372
|
this.bookedShown = false;
|
|
2373
|
+
this.ctaPhase = "idle";
|
|
2134
2374
|
this.stopHoldTimer();
|
|
2135
2375
|
this.gaQty.clear();
|
|
2136
2376
|
this.syncTray();
|
|
2137
2377
|
}
|
|
2138
2378
|
destroy() {
|
|
2139
2379
|
this.destroyed = true;
|
|
2380
|
+
if (this.hold && !this.handedOff) void this.controller.release();
|
|
2140
2381
|
this.closeConfirm();
|
|
2141
2382
|
this.closeSeatView();
|
|
2142
2383
|
this.stopHoldTimer();
|
|
2143
2384
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
2385
|
+
for (const timer of this.motionTimers) clearTimeout(timer);
|
|
2386
|
+
this.motionTimers.clear();
|
|
2144
2387
|
this.ro?.disconnect();
|
|
2145
2388
|
this.ro = null;
|
|
2146
2389
|
if (this.escHandler) document.removeEventListener("keydown", this.escHandler);
|
|
@@ -2361,11 +2604,44 @@ var CSS2 = `
|
|
|
2361
2604
|
font-size:12px;font-weight:700;color:var(--slm-text);display:inline-flex;align-items:center;gap:6px}
|
|
2362
2605
|
.slm-chip:hover{border-color:var(--slm-muted)}
|
|
2363
2606
|
.slm-chip .dot{width:8px;height:8px;border-radius:50%}
|
|
2607
|
+
.slm-chip .slm-chipcount{min-width:18px;padding:1px 5px;border-radius:999px;background:rgba(255,255,255,.07);
|
|
2608
|
+
color:var(--slm-muted);font-size:10px;font-variant-numeric:tabular-nums;text-align:center}
|
|
2609
|
+
.slm-chip .slm-chipcheck{display:none;font-size:11px;line-height:1}
|
|
2610
|
+
.slm-chip.on{border-color:var(--slm-accent);background:color-mix(in srgb,var(--slm-accent) 20%,var(--slm-surface));
|
|
2611
|
+
box-shadow:0 0 0 1px color-mix(in srgb,var(--slm-accent) 45%,transparent)}
|
|
2612
|
+
.slm-chip.on .slm-chipcount{background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
2613
|
+
.slm-chip.on .slm-chipcheck{display:inline}
|
|
2614
|
+
.slm-chip.partial{border-style:dashed;border-color:var(--slm-accent)}
|
|
2615
|
+
.slm-chip:disabled{opacity:.42;cursor:not-allowed}
|
|
2616
|
+
.slm-selecthelp{margin:-1px 0 9px;color:var(--slm-muted);font-size:11px;line-height:1.4}
|
|
2364
2617
|
.slm-field{margin:14px 0}
|
|
2365
2618
|
.slm-field label{display:block;font-size:11px;font-weight:700;color:var(--slm-muted);margin-bottom:5px}
|
|
2366
2619
|
.slm-input,.slm-select{width:100%;padding:8px 10px;border-radius:9px;border:1px solid var(--slm-line);
|
|
2367
2620
|
background:var(--slm-surface);color:var(--slm-text)}
|
|
2368
2621
|
.slm-note{font-size:11.5px;color:var(--slm-muted);margin-top:5px}
|
|
2622
|
+
.slm-blocked{margin-top:17px;padding-top:15px;border-top:1px solid var(--slm-line)}
|
|
2623
|
+
.slm-blockedhead{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:8px}
|
|
2624
|
+
.slm-blockedhead .slm-eyebrow{margin-bottom:0}.slm-blockedtotal{font-size:11px;color:var(--slm-muted)}
|
|
2625
|
+
.slm-blockedtotal b{color:var(--slm-text);font-variant-numeric:tabular-nums}
|
|
2626
|
+
.slm-blockedtools{display:grid;grid-template-columns:minmax(0,1fr);gap:7px}
|
|
2627
|
+
.slm-blockedsummary{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:9px 0 6px;
|
|
2628
|
+
color:var(--slm-muted);font-size:10.5px}
|
|
2629
|
+
.slm-linkbtn{font-size:11px!important;font-weight:800!important;color:var(--slm-accent)!important;text-align:right}
|
|
2630
|
+
.slm-linkbtn:disabled{opacity:.45;cursor:not-allowed}
|
|
2631
|
+
.slm-blockedlist{max-height:246px;overflow:auto;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
|
|
2632
|
+
.slm-blockeditem{display:grid!important;grid-template-columns:18px minmax(0,1fr);width:100%;gap:8px;padding:8px 9px!important;
|
|
2633
|
+
border-bottom:1px solid var(--slm-line)!important;text-align:left!important}
|
|
2634
|
+
.slm-blockeditem:last-child{border-bottom:0!important}.slm-blockeditem:hover{background:rgba(255,255,255,.035)!important}
|
|
2635
|
+
.slm-blockeditem.on{background:color-mix(in srgb,var(--slm-accent) 13%,var(--slm-surface))!important}
|
|
2636
|
+
.slm-blockedcheck{display:flex;align-items:center;justify-content:center;width:16px;height:16px;margin-top:1px;border-radius:4px;
|
|
2637
|
+
border:1px solid var(--slm-muted);color:transparent;font-size:10px;font-weight:900}
|
|
2638
|
+
.slm-blockeditem.on .slm-blockedcheck{border-color:var(--slm-accent);background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
2639
|
+
.slm-blockedcopy{min-width:0}.slm-blockedlabel{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
|
2640
|
+
font-size:12px;font-weight:800}.slm-blockedmeta{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
|
2641
|
+
margin-top:2px;color:var(--slm-muted);font-size:10px}
|
|
2642
|
+
.slm-blockedmore{width:100%;padding:9px!important;color:var(--slm-accent)!important;font-size:11px!important;font-weight:800!important}
|
|
2643
|
+
.slm-blockedempty{padding:12px;color:var(--slm-muted);font-size:11.5px;line-height:1.45}
|
|
2644
|
+
.slm-allnote{margin-top:-4px;margin-bottom:10px}
|
|
2369
2645
|
|
|
2370
2646
|
/* toast */
|
|
2371
2647
|
.slm-toast{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);padding:10px 16px;border-radius:10px;
|
|
@@ -2485,6 +2761,10 @@ var SeatManager = class {
|
|
|
2485
2761
|
this.sectionByObject = /* @__PURE__ */ new Map();
|
|
2486
2762
|
this.sectionLabelById = /* @__PURE__ */ new Map();
|
|
2487
2763
|
this.lastSyncedAt = null;
|
|
2764
|
+
this.blockedQuery = "";
|
|
2765
|
+
this.blockedSection = "";
|
|
2766
|
+
this.blockedResultLimit = 100;
|
|
2767
|
+
this.unblockAllConfirmTimer = null;
|
|
2488
2768
|
this.onFullscreenChange = () => {
|
|
2489
2769
|
this.paintFullscreenButton();
|
|
2490
2770
|
this.updateContainerLayout();
|
|
@@ -2630,13 +2910,13 @@ var SeatManager = class {
|
|
|
2630
2910
|
const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "free");
|
|
2631
2911
|
if (!targets.length) return;
|
|
2632
2912
|
const releaseAt = opts.releaseAt ?? this.releaseAt ?? void 0;
|
|
2633
|
-
|
|
2913
|
+
this.setSeatsLocal(targets, "blocked");
|
|
2634
2914
|
try {
|
|
2635
2915
|
await this.api.block(this.key, targets, { ...opts, releaseAt });
|
|
2636
2916
|
this.clearSelection();
|
|
2637
2917
|
this.done("block", targets, releaseAt ? `Blocked ${targets.length} \u2014 auto-release ${new Date(releaseAt).toLocaleString()}.` : `Blocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
|
|
2638
2918
|
} catch (err) {
|
|
2639
|
-
|
|
2919
|
+
this.setSeatsLocal(targets, "free");
|
|
2640
2920
|
this.toastErr(err instanceof ManageApiError && err.status === 409 ? "Some seats were just taken. Try again." : "Couldn't block those seats.");
|
|
2641
2921
|
this.opts.onError?.(err);
|
|
2642
2922
|
}
|
|
@@ -2644,13 +2924,13 @@ var SeatManager = class {
|
|
|
2644
2924
|
async unblock(labels) {
|
|
2645
2925
|
const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "blocked");
|
|
2646
2926
|
if (!targets.length) return;
|
|
2647
|
-
|
|
2927
|
+
this.setSeatsLocal(targets, "free");
|
|
2648
2928
|
try {
|
|
2649
2929
|
await this.api.unblock(this.key, targets);
|
|
2650
2930
|
this.clearSelection();
|
|
2651
2931
|
this.done("unblock", targets, `Unblocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
|
|
2652
2932
|
} catch (err) {
|
|
2653
|
-
|
|
2933
|
+
this.setSeatsLocal(targets, "blocked");
|
|
2654
2934
|
this.toastErr("Couldn't unblock those seats.");
|
|
2655
2935
|
this.opts.onError?.(err);
|
|
2656
2936
|
}
|
|
@@ -2658,9 +2938,10 @@ var SeatManager = class {
|
|
|
2658
2938
|
async unblockAll() {
|
|
2659
2939
|
const blocked = [...this.status.entries()].filter(([, s]) => s === "blocked").map(([l]) => l);
|
|
2660
2940
|
if (!blocked.length) return;
|
|
2661
|
-
|
|
2941
|
+
this.setSeatsLocal(blocked, "free");
|
|
2662
2942
|
try {
|
|
2663
2943
|
const res = await this.api.unblockAll(this.key);
|
|
2944
|
+
this.clearSelection();
|
|
2664
2945
|
this.done("unblockAll", blocked, `Unblocked ${res.freed} seat${res.freed === 1 ? "" : "s"}.`);
|
|
2665
2946
|
} catch (err) {
|
|
2666
2947
|
await this.resnapshot();
|
|
@@ -2672,13 +2953,13 @@ var SeatManager = class {
|
|
|
2672
2953
|
async cancelBooking(labels, bookingRef) {
|
|
2673
2954
|
const targets = labels.filter((l) => this.status.get(l) === "booked");
|
|
2674
2955
|
if (!targets.length || !bookingRef) return;
|
|
2675
|
-
|
|
2956
|
+
this.setSeatsLocal(targets, "free");
|
|
2676
2957
|
try {
|
|
2677
2958
|
await this.api.unbook(this.key, targets, bookingRef);
|
|
2678
2959
|
this.clearSelection();
|
|
2679
2960
|
this.done("cancelBooking", targets, `Cancelled ${targets.length} booking${targets.length === 1 ? "" : "s"}.`);
|
|
2680
2961
|
} catch (err) {
|
|
2681
|
-
|
|
2962
|
+
this.setSeatsLocal(targets, "booked");
|
|
2682
2963
|
this.toastErr("Couldn't cancel that booking. Check the reference.");
|
|
2683
2964
|
this.opts.onError?.(err);
|
|
2684
2965
|
}
|
|
@@ -2741,6 +3022,7 @@ var SeatManager = class {
|
|
|
2741
3022
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
2742
3023
|
if (this.feedTimer) clearInterval(this.feedTimer);
|
|
2743
3024
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
3025
|
+
if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
|
|
2744
3026
|
if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
|
|
2745
3027
|
if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
|
|
2746
3028
|
this.layoutObserver?.disconnect();
|
|
@@ -2919,11 +3201,16 @@ var SeatManager = class {
|
|
|
2919
3201
|
this.afterPaint();
|
|
2920
3202
|
this.recomputeTallies();
|
|
2921
3203
|
}
|
|
2922
|
-
/** Optimistic local write shared by
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
const
|
|
2926
|
-
|
|
3204
|
+
/** Optimistic local write shared by organizer actions. Paint and tally once,
|
|
3205
|
+
* even when an arena-sized operation changes hundreds of seats. */
|
|
3206
|
+
setSeatsLocal(labels, st) {
|
|
3207
|
+
const ids = [];
|
|
3208
|
+
for (const label of labels) {
|
|
3209
|
+
this.status.set(label, st);
|
|
3210
|
+
const id = this.labelToId.get(label);
|
|
3211
|
+
if (id) ids.push(id);
|
|
3212
|
+
}
|
|
3213
|
+
if (ids.length) this.renderer?.setStatus(ids, toRenderStatus(st));
|
|
2927
3214
|
this.afterPaint();
|
|
2928
3215
|
this.recomputeTallies();
|
|
2929
3216
|
}
|
|
@@ -3006,6 +3293,7 @@ var SeatManager = class {
|
|
|
3006
3293
|
this.paintLegend(t3);
|
|
3007
3294
|
this.paintMonitorInsights();
|
|
3008
3295
|
} else if (this.mode === "inspect") this.renderInspectRail(this.getSelection());
|
|
3296
|
+
else if (this.mode === "block") this.paintSelBar(this.getSelection());
|
|
3009
3297
|
this.opts.onTallies?.(t3);
|
|
3010
3298
|
}
|
|
3011
3299
|
verbFor(prev, next) {
|
|
@@ -3385,23 +3673,30 @@ var SeatManager = class {
|
|
|
3385
3673
|
}
|
|
3386
3674
|
renderBlockRail() {
|
|
3387
3675
|
const cats = this.doc?.categories ?? [];
|
|
3388
|
-
const catChips = cats.map((c) => `<button class="slm-chip" data-cat="${esc(c.key)}"
|
|
3676
|
+
const catChips = cats.map((c) => `<button class="slm-chip" type="button" data-cat="${esc(c.key)}" aria-pressed="false">
|
|
3677
|
+
<span class="dot" style="background:${esc(c.color ?? "#6e7bff")}"></span>
|
|
3678
|
+
<span>${esc(c.label ?? c.key)}</span>
|
|
3679
|
+
<span class="slm-chipcount" data-cat-count>0</span>
|
|
3680
|
+
<span class="slm-chipcheck" aria-hidden="true">\u2713</span>
|
|
3681
|
+
</button>`).join("");
|
|
3389
3682
|
const sectionField = this.sectionOptions.length ? `<div class="slm-field"><label>Select a whole section</label>
|
|
3390
3683
|
<select class="slm-select" data-ref="section"><option value="">Choose a section\u2026</option>
|
|
3391
3684
|
${this.sectionOptions.map((s) => `<option value="${esc(s.id)}">${esc(s.label)}</option>`).join("")}</select></div>` : "";
|
|
3685
|
+
const blockedSectionOptions = this.sectionOptions.map((s) => `<option value="${esc(s.id)}">${esc(s.label)}</option>`).join("");
|
|
3392
3686
|
this.els.rail.innerHTML = `
|
|
3393
3687
|
<p class="slm-eyebrow">Block & unblock</p>
|
|
3394
3688
|
<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>
|
|
3395
|
-
<div class="slm-selbar"><span class="slm-selnum" data-ref="selnum">0</span><span class="slm-sellabel">selected</span></div>
|
|
3689
|
+
<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>
|
|
3396
3690
|
<div class="slm-row">
|
|
3397
3691
|
<button class="slm-btn" data-ref="doblock" disabled>Block</button>
|
|
3398
|
-
<button class="slm-btn ghost" data-ref="dounblock" disabled>
|
|
3692
|
+
<button class="slm-btn ghost" data-ref="dounblock" disabled>Put back on sale</button>
|
|
3399
3693
|
</div>
|
|
3400
3694
|
<div class="slm-row">
|
|
3401
3695
|
<button class="slm-btn ghost" data-ref="selall">Select all</button>
|
|
3402
3696
|
<button class="slm-btn ghost" data-ref="clearsel">Clear</button>
|
|
3403
3697
|
</div>
|
|
3404
|
-
<p class="slm-eyebrow" style="margin-top:8px">
|
|
3698
|
+
<p class="slm-eyebrow" style="margin-top:8px">Select by category</p>
|
|
3699
|
+
<p class="slm-selecthelp">Choose one or more. A checked category is selected; click it again to remove it.</p>
|
|
3405
3700
|
<div class="slm-chiprow">${catChips || '<span class="slm-empty">No categories.</span>'}</div>
|
|
3406
3701
|
${sectionField}
|
|
3407
3702
|
<div class="slm-field">
|
|
@@ -3409,18 +3704,46 @@ var SeatManager = class {
|
|
|
3409
3704
|
<input type="datetime-local" class="slm-input" data-ref="release" />
|
|
3410
3705
|
<p class="slm-note" data-ref="releasenote">Leave empty to block permanently.</p>
|
|
3411
3706
|
</div>
|
|
3412
|
-
<
|
|
3707
|
+
<section class="slm-blocked" aria-labelledby="slm-blocked-title">
|
|
3708
|
+
<div class="slm-blockedhead">
|
|
3709
|
+
<p class="slm-eyebrow" id="slm-blocked-title">Blocked inventory</p>
|
|
3710
|
+
<span class="slm-blockedtotal"><b data-ref="blockedcount">0</b> out of sale</span>
|
|
3711
|
+
</div>
|
|
3712
|
+
<p class="slm-selecthelp">Find blocked seats, select only the ones you need, then use \u201CPut back on sale\u201D.</p>
|
|
3713
|
+
<div class="slm-blockedtools">
|
|
3714
|
+
<input type="search" class="slm-input" data-ref="blockedsearch" placeholder="Find seat, row or category" aria-label="Search blocked seats" />
|
|
3715
|
+
<select class="slm-select" data-ref="blockedsection" aria-label="Filter blocked seats by section">
|
|
3716
|
+
<option value="">All sections</option>${blockedSectionOptions}
|
|
3717
|
+
</select>
|
|
3718
|
+
</div>
|
|
3719
|
+
<div class="slm-blockedsummary">
|
|
3720
|
+
<span data-ref="blockedshowing">No blocked seats</span>
|
|
3721
|
+
<button type="button" class="slm-linkbtn" data-ref="selblocked" disabled>Select results</button>
|
|
3722
|
+
</div>
|
|
3723
|
+
<div class="slm-blockedlist" data-ref="blockedlist"></div>
|
|
3724
|
+
</section>
|
|
3725
|
+
<div class="slm-field">
|
|
3726
|
+
<button class="slm-btn ghost" data-ref="markall" style="width:100%" disabled>Put all blocked seats on sale</button>
|
|
3727
|
+
<p class="slm-note slm-allnote" data-ref="markallnote">For a full reset only. You will be asked to confirm.</p>
|
|
3728
|
+
</div>
|
|
3413
3729
|
`;
|
|
3414
3730
|
const r = (n) => this.els.rail.querySelector(`[data-ref="${n}"]`);
|
|
3415
3731
|
this.els.selnum = r("selnum");
|
|
3416
3732
|
this.els.doblock = r("doblock");
|
|
3417
3733
|
this.els.dounblock = r("dounblock");
|
|
3734
|
+
this.els.selmeta = r("selmeta");
|
|
3735
|
+
this.els.blockedcount = r("blockedcount");
|
|
3736
|
+
this.els.blockedshowing = r("blockedshowing");
|
|
3737
|
+
this.els.blockedlist = r("blockedlist");
|
|
3738
|
+
this.els.selblocked = r("selblocked");
|
|
3739
|
+
this.els.markall = r("markall");
|
|
3740
|
+
this.els.markallnote = r("markallnote");
|
|
3418
3741
|
r("doblock").addEventListener("click", () => void this.block());
|
|
3419
3742
|
r("dounblock").addEventListener("click", () => void this.unblock());
|
|
3420
3743
|
r("selall").addEventListener("click", () => this.selectAll());
|
|
3421
3744
|
r("clearsel").addEventListener("click", () => this.clearSelection());
|
|
3422
|
-
r("markall").addEventListener("click", () =>
|
|
3423
|
-
this.els.rail.querySelectorAll("[data-cat]").forEach((b) => b.addEventListener("click", () => this.
|
|
3745
|
+
r("markall").addEventListener("click", () => this.confirmUnblockAll());
|
|
3746
|
+
this.els.rail.querySelectorAll("[data-cat]").forEach((b) => b.addEventListener("click", () => this.toggleCategory(b.dataset.cat)));
|
|
3424
3747
|
const sectionSel = this.els.rail.querySelector('[data-ref="section"]');
|
|
3425
3748
|
sectionSel?.addEventListener("change", () => {
|
|
3426
3749
|
if (sectionSel.value) {
|
|
@@ -3428,6 +3751,32 @@ var SeatManager = class {
|
|
|
3428
3751
|
sectionSel.value = "";
|
|
3429
3752
|
}
|
|
3430
3753
|
});
|
|
3754
|
+
const blockedSearch = r("blockedsearch");
|
|
3755
|
+
const blockedSection = r("blockedsection");
|
|
3756
|
+
blockedSearch.value = this.blockedQuery;
|
|
3757
|
+
blockedSection.value = this.blockedSection;
|
|
3758
|
+
blockedSearch.addEventListener("input", () => {
|
|
3759
|
+
this.blockedQuery = blockedSearch.value;
|
|
3760
|
+
this.blockedResultLimit = 100;
|
|
3761
|
+
this.paintBlockedInventory();
|
|
3762
|
+
});
|
|
3763
|
+
blockedSection.addEventListener("change", () => {
|
|
3764
|
+
this.blockedSection = blockedSection.value;
|
|
3765
|
+
this.blockedResultLimit = 100;
|
|
3766
|
+
this.paintBlockedInventory();
|
|
3767
|
+
});
|
|
3768
|
+
r("selblocked").addEventListener("click", () => {
|
|
3769
|
+
this.toggleLabels(this.filteredBlockedSeats().map((seat) => seat.label));
|
|
3770
|
+
});
|
|
3771
|
+
r("blockedlist").addEventListener("click", (event) => {
|
|
3772
|
+
const target = event.target;
|
|
3773
|
+
const seatButton = target.closest("[data-blocked-label]");
|
|
3774
|
+
if (seatButton?.dataset.blockedLabel) this.toggleLabels([seatButton.dataset.blockedLabel]);
|
|
3775
|
+
else if (target.closest("[data-blocked-more]")) {
|
|
3776
|
+
this.blockedResultLimit += 100;
|
|
3777
|
+
this.paintBlockedInventory();
|
|
3778
|
+
}
|
|
3779
|
+
});
|
|
3431
3780
|
const rel = r("release");
|
|
3432
3781
|
rel.addEventListener("change", () => {
|
|
3433
3782
|
const ms = rel.value ? new Date(rel.value).getTime() : NaN;
|
|
@@ -3437,18 +3786,140 @@ var SeatManager = class {
|
|
|
3437
3786
|
});
|
|
3438
3787
|
this.paintSelBar(this.getSelection());
|
|
3439
3788
|
}
|
|
3440
|
-
|
|
3789
|
+
toggleCategory(catKey) {
|
|
3441
3790
|
const labels = [];
|
|
3442
|
-
for (const [label, seat] of this.labelToSeat.entries())
|
|
3443
|
-
|
|
3791
|
+
for (const [label, seat] of this.labelToSeat.entries()) {
|
|
3792
|
+
if (seat.categoryKey === catKey && this.isBlockSelectable(label)) labels.push(label);
|
|
3793
|
+
}
|
|
3794
|
+
this.toggleLabels(labels);
|
|
3795
|
+
}
|
|
3796
|
+
/** A category/filter is a real toggle: add the missing seats, or remove the
|
|
3797
|
+
* whole group when every eligible seat in it is already selected. */
|
|
3798
|
+
toggleLabels(labels) {
|
|
3799
|
+
if (!this.renderer) return;
|
|
3800
|
+
const eligible = labels.filter((label) => this.labelToSeat.has(label) && this.isBlockSelectable(label));
|
|
3801
|
+
if (!eligible.length) return;
|
|
3802
|
+
const selected = new Set(this.selectionLabels());
|
|
3803
|
+
const allSelected = eligible.every((label) => selected.has(label));
|
|
3804
|
+
if (allSelected) {
|
|
3805
|
+
const ids = eligible.map((label) => this.labelToId.get(label)).filter((id) => Boolean(id));
|
|
3806
|
+
this.renderer.deselect(ids);
|
|
3807
|
+
} else {
|
|
3808
|
+
this.renderer.selectByLabels(eligible);
|
|
3809
|
+
}
|
|
3810
|
+
this.syncSelection();
|
|
3811
|
+
}
|
|
3812
|
+
isBlockSelectable(label) {
|
|
3813
|
+
const status = this.status.get(label) ?? "free";
|
|
3814
|
+
return status === "free" || status === "blocked";
|
|
3444
3815
|
}
|
|
3445
3816
|
paintSelBar(seats) {
|
|
3446
3817
|
if (!this.els.selnum) return;
|
|
3447
3818
|
this.els.selnum.textContent = seats.length.toLocaleString();
|
|
3448
|
-
const
|
|
3449
|
-
const
|
|
3450
|
-
this.els.
|
|
3451
|
-
this.els.
|
|
3819
|
+
const freeCount = seats.filter((s) => (this.status.get(s.label) ?? "free") === "free").length;
|
|
3820
|
+
const blockedCount = seats.filter((s) => this.status.get(s.label) === "blocked").length;
|
|
3821
|
+
this.els.selmeta.textContent = seats.length ? `${freeCount.toLocaleString()} available \xB7 ${blockedCount.toLocaleString()} blocked` : "selected";
|
|
3822
|
+
const blockButton = this.els.doblock;
|
|
3823
|
+
const unblockButton = this.els.dounblock;
|
|
3824
|
+
blockButton.disabled = freeCount === 0;
|
|
3825
|
+
unblockButton.disabled = blockedCount === 0;
|
|
3826
|
+
blockButton.textContent = freeCount ? `Block ${freeCount.toLocaleString()}` : "Block selected";
|
|
3827
|
+
unblockButton.textContent = blockedCount ? `Put ${blockedCount.toLocaleString()} on sale` : "Put back on sale";
|
|
3828
|
+
this.paintCategoryControls(seats);
|
|
3829
|
+
this.paintBlockedInventory();
|
|
3830
|
+
}
|
|
3831
|
+
paintCategoryControls(seats) {
|
|
3832
|
+
const selected = new Set(seats.map((seat) => seat.label));
|
|
3833
|
+
this.els.rail?.querySelectorAll("[data-cat]").forEach((button) => {
|
|
3834
|
+
const catKey = button.dataset.cat;
|
|
3835
|
+
const labels = [];
|
|
3836
|
+
for (const [label, seat] of this.labelToSeat.entries()) {
|
|
3837
|
+
if (seat.categoryKey === catKey && this.isBlockSelectable(label)) labels.push(label);
|
|
3838
|
+
}
|
|
3839
|
+
const picked = labels.filter((label) => selected.has(label)).length;
|
|
3840
|
+
const full = labels.length > 0 && picked === labels.length;
|
|
3841
|
+
const partial = picked > 0 && !full;
|
|
3842
|
+
button.disabled = labels.length === 0;
|
|
3843
|
+
button.classList.toggle("on", full);
|
|
3844
|
+
button.classList.toggle("partial", partial);
|
|
3845
|
+
button.setAttribute("aria-pressed", full ? "true" : partial ? "mixed" : "false");
|
|
3846
|
+
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`);
|
|
3847
|
+
const count = button.querySelector("[data-cat-count]");
|
|
3848
|
+
if (count) count.textContent = picked ? `${picked.toLocaleString()}/${labels.length.toLocaleString()}` : labels.length.toLocaleString();
|
|
3849
|
+
});
|
|
3850
|
+
}
|
|
3851
|
+
filteredBlockedSeats() {
|
|
3852
|
+
const query = this.blockedQuery.trim().toLocaleLowerCase();
|
|
3853
|
+
const seats = [];
|
|
3854
|
+
for (const [label, seat] of this.labelToSeat.entries()) {
|
|
3855
|
+
if (this.status.get(label) !== "blocked") continue;
|
|
3856
|
+
const sectionId = this.sectionByObject.get(seat.rowId) ?? import_core3.UNGROUPED_ID;
|
|
3857
|
+
if (this.blockedSection && sectionId !== this.blockedSection) continue;
|
|
3858
|
+
if (query) {
|
|
3859
|
+
const category = this.doc?.categories.find((item) => item.key === seat.categoryKey)?.label ?? seat.categoryKey;
|
|
3860
|
+
const section = this.sectionLabelById.get(sectionId) ?? "Other seats";
|
|
3861
|
+
const object = this.doc?.objects.find((item) => item.id === seat.rowId);
|
|
3862
|
+
const objectLabel = object?.type === "row" || object?.type === "table" ? object.label : "";
|
|
3863
|
+
const haystack = `${label} ${category} ${section} ${objectLabel}`.toLocaleLowerCase();
|
|
3864
|
+
if (!haystack.includes(query)) continue;
|
|
3865
|
+
}
|
|
3866
|
+
seats.push(seat);
|
|
3867
|
+
}
|
|
3868
|
+
return seats.sort((a, b) => a.label.localeCompare(b.label, void 0, { numeric: true, sensitivity: "base" }));
|
|
3869
|
+
}
|
|
3870
|
+
paintBlockedInventory() {
|
|
3871
|
+
if (!this.els.blockedlist) return;
|
|
3872
|
+
const allBlocked = [...this.status.entries()].filter(([, status]) => status === "blocked").length;
|
|
3873
|
+
const filtered = this.filteredBlockedSeats();
|
|
3874
|
+
const visible = filtered.slice(0, this.blockedResultLimit);
|
|
3875
|
+
const selected = new Set(this.selectionLabels());
|
|
3876
|
+
const selectedResults = filtered.filter((seat) => selected.has(seat.label)).length;
|
|
3877
|
+
const allResultsSelected = filtered.length > 0 && selectedResults === filtered.length;
|
|
3878
|
+
this.els.blockedcount.textContent = allBlocked.toLocaleString();
|
|
3879
|
+
this.els.blockedshowing.textContent = filtered.length ? `Showing ${visible.length.toLocaleString()} of ${filtered.length.toLocaleString()}` : allBlocked ? "No matches" : "No blocked seats";
|
|
3880
|
+
const selectResults = this.els.selblocked;
|
|
3881
|
+
selectResults.disabled = filtered.length === 0;
|
|
3882
|
+
selectResults.textContent = allResultsSelected ? `Remove ${filtered.length.toLocaleString()} results` : `Select ${filtered.length.toLocaleString()} results`;
|
|
3883
|
+
this.els.blockedlist.innerHTML = visible.length ? visible.map((seat) => {
|
|
3884
|
+
const sectionId = this.sectionByObject.get(seat.rowId) ?? import_core3.UNGROUPED_ID;
|
|
3885
|
+
const section = this.sectionLabelById.get(sectionId) ?? "Other seats";
|
|
3886
|
+
const category = this.doc?.categories.find((item) => item.key === seat.categoryKey)?.label ?? seat.categoryKey;
|
|
3887
|
+
const isSelected = selected.has(seat.label);
|
|
3888
|
+
return `<button type="button" class="slm-blockeditem${isSelected ? " on" : ""}" data-blocked-label="${esc(seat.label)}" aria-pressed="${isSelected}">
|
|
3889
|
+
<span class="slm-blockedcheck" aria-hidden="true">\u2713</span>
|
|
3890
|
+
<span class="slm-blockedcopy"><span class="slm-blockedlabel">${esc(seat.label)}</span>
|
|
3891
|
+
<span class="slm-blockedmeta">${esc(section)} \xB7 ${esc(category)}</span></span>
|
|
3892
|
+
</button>`;
|
|
3893
|
+
}).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>`;
|
|
3894
|
+
const markAll = this.els.markall;
|
|
3895
|
+
const armed = markAll.dataset.confirm === "true";
|
|
3896
|
+
markAll.disabled = allBlocked === 0;
|
|
3897
|
+
markAll.textContent = armed ? `Confirm: put all ${allBlocked.toLocaleString()} on sale` : `Put all ${allBlocked.toLocaleString()} blocked seats on sale`;
|
|
3898
|
+
}
|
|
3899
|
+
confirmUnblockAll() {
|
|
3900
|
+
const button = this.els.markall;
|
|
3901
|
+
if (!button || button.disabled) return;
|
|
3902
|
+
if (button.dataset.confirm === "true") {
|
|
3903
|
+
this.resetUnblockAllConfirm();
|
|
3904
|
+
void this.unblockAll();
|
|
3905
|
+
return;
|
|
3906
|
+
}
|
|
3907
|
+
button.dataset.confirm = "true";
|
|
3908
|
+
button.classList.add("danger");
|
|
3909
|
+
this.els.markallnote.textContent = "This changes every blocked seat. Click the red button again to confirm.";
|
|
3910
|
+
this.paintBlockedInventory();
|
|
3911
|
+
if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
|
|
3912
|
+
this.unblockAllConfirmTimer = setTimeout(() => this.resetUnblockAllConfirm(), 6e3);
|
|
3913
|
+
}
|
|
3914
|
+
resetUnblockAllConfirm() {
|
|
3915
|
+
if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
|
|
3916
|
+
this.unblockAllConfirmTimer = null;
|
|
3917
|
+
const button = this.els.markall;
|
|
3918
|
+
if (!button) return;
|
|
3919
|
+
delete button.dataset.confirm;
|
|
3920
|
+
button.classList.remove("danger");
|
|
3921
|
+
if (this.els.markallnote) this.els.markallnote.textContent = "For a full reset only. You will be asked to confirm.";
|
|
3922
|
+
this.paintBlockedInventory();
|
|
3452
3923
|
}
|
|
3453
3924
|
// ---- toast / done / fail --------------------------------------------------
|
|
3454
3925
|
done(action, labels, msg) {
|