@seatlayer/js 0.13.0 → 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/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
- .sl-hold-pill.on{display:inline-flex}
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 .22s ease}
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 .15s,color .15s,transform .06s}
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 .2s,transform .2s;white-space:nowrap;
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:none;flex-direction:column;align-items:center;
638
- justify-content:center;gap:12px;text-align:center;padding:28px;background:var(--sl-bg)}
639
- .sl-booked.on{display:flex}
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 svg{width:30px;height:30px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round}
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;transition:border-color .15s}
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
- const api = new PubApi((options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, ""));
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
- picker.destroy();
951
- options.onClose?.();
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", () => this.controller.setRung(btn.dataset.rung));
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 = false;
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</button></div>`
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 id = btn.closest(".sl-chip").dataset.seat;
1893
- this.controller.deselect([id]);
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
- const cta = this.els.cta;
1925
- cta.disabled = count === 0;
1926
- cta.textContent = this.hold ? "Continue to checkout" : count ? "Hold seats & checkout" : "Select seats";
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
- cta.disabled = true;
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.opts.onCheckout?.(hold, chosenSeats.length ? chosenSeats : hold.seats ?? [], this.buildHandoff(hold));
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
- pill.textContent = `Held ${m}:${s}`;
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(() => el.classList.remove("on"), 4200);
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
- await this.controller.release();
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);