@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.js CHANGED
@@ -434,8 +434,12 @@ var CSS = `
434
434
  .sl-head-meta{font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--sl-muted);margin-top:3px;
435
435
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
436
436
  .sl-hold-pill{display:none;align-items:center;gap:6px;padding:6px 12px;border-radius:999px;flex:none;
437
- background:var(--sl-accent);color:var(--sl-accent-ink);font-weight:700;font-size:12px;font-variant-numeric:tabular-nums}
438
- .sl-hold-pill.on{display:inline-flex}
437
+ background:var(--sl-accent);color:var(--sl-accent-ink);font-weight:700;font-size:12px;font-variant-numeric:tabular-nums;
438
+ transform-origin:right center}
439
+ .sl-hold-pill.on{display:inline-flex;animation:slPillIn .34s cubic-bezier(.2,.8,.2,1) both}
440
+ .sl-hold-dot{width:7px;height:7px;border-radius:50%;background:currentColor;opacity:.78;box-shadow:0 0 0 0 currentColor}
441
+ .sl-hold-pill.is-expiring .sl-hold-dot{animation:slHoldPulse 1.4s ease-out infinite}
442
+ .sl-hold-time{min-width:3.35em;text-align:left}
439
443
  .sl-close{width:32px;height:32px;border-radius:999px;flex:none;display:none;align-items:center;justify-content:center;
440
444
  border:1px solid var(--sl-line);color:var(--sl-muted);transition:color .15s,border-color .15s}
441
445
  .sl-close:hover{color:var(--sl-text);border-color:var(--sl-muted)}
@@ -457,7 +461,7 @@ var CSS = `
457
461
  .sl-picker[data-layout="narrow"] .sl-body{flex-direction:column}
458
462
  .sl-picker[data-layout="narrow"] .sl-map{min-height:0;flex:1}
459
463
  .sl-picker[data-layout="narrow"] .sl-side{width:100%;border-left:0;border-top:1px solid var(--sl-line);
460
- flex:none;height:50%;transition:height .22s ease}
464
+ flex:none;height:50%;transition:height .3s cubic-bezier(.2,.8,.2,1)}
461
465
  .sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side{height:86px;overflow:hidden}
462
466
  .sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side > :not(.sl-sheet-head){display:none}
463
467
  .sl-picker[data-layout="narrow"] .sl-tray{flex:none}
@@ -513,10 +517,17 @@ var CSS = `
513
517
  .sl-tray{flex:1;padding:10px 16px;display:flex;flex-direction:column;gap:8px;min-height:0}
514
518
  .sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
515
519
  .sl-chip{display:flex;align-items:center;gap:9px;padding:9px 11px;border:1px solid var(--sl-line);
516
- border-radius:var(--sl-r-sm);background:var(--sl-surface);font-size:13px}
520
+ border-radius:var(--sl-r-sm);background:var(--sl-surface);font-size:13px;transform-origin:center}
521
+ .sl-chip.sl-enter{animation:slChipIn .38s cubic-bezier(.2,.8,.2,1) both}
522
+ .sl-chip.sl-leave{pointer-events:none;animation:slChipOut .16s ease-in both}
523
+ .sl-chip.sl-held{border-color:var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
524
+ box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent)}
517
525
  .sl-chip b{font-weight:800}
518
526
  .sl-chip .cat{color:var(--sl-muted);font-size:11.5px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
519
527
  .sl-chip .amt{font-weight:700;font-variant-numeric:tabular-nums}
528
+ .sl-chip-state{flex:none;padding:3px 6px;border-radius:999px;background:var(--sl-accent);color:var(--sl-accent-ink);
529
+ font-size:8.5px;line-height:1;font-weight:900;letter-spacing:.08em;text-transform:uppercase}
530
+ .sl-chip-state.sl-pending{background:transparent;color:var(--sl-muted);border:1px solid var(--sl-line)}
520
531
  .sl-chip .rm{width:22px;height:22px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;color:var(--sl-muted)}
521
532
  .sl-chip .rm:hover{color:var(--sl-text)}
522
533
  .sl-chip .rm svg{width:11px;height:11px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round}
@@ -534,17 +545,30 @@ var CSS = `
534
545
 
535
546
  /* footer */
536
547
  .sl-foot{padding:12px 16px 14px;border-top:1px solid var(--sl-line);flex:none}
548
+ .sl-hold-note{display:none;align-items:center;gap:9px;margin-bottom:10px;padding:9px 10px;border-radius:var(--sl-r-sm);
549
+ border:1px solid var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
550
+ box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent);
551
+ font-size:11.5px;line-height:1.35;color:var(--sl-muted)}
552
+ .sl-hold-note.on{display:flex;animation:slNoticeIn .38s cubic-bezier(.2,.8,.2,1) both}
553
+ .sl-hold-note svg{width:16px;height:16px;flex:none;stroke:var(--sl-accent);stroke-width:2.4;fill:none;
554
+ stroke-linecap:round;stroke-linejoin:round}
555
+ .sl-hold-note b{display:block;color:var(--sl-text);font-size:12px}
556
+ .sl-hold-copy{display:block}
537
557
  .sl-total{display:flex;justify-content:space-between;align-items:center;font-size:13px;margin-bottom:10px}
538
558
  .sl-total b{font-size:17px;font-variant-numeric:tabular-nums}
559
+ .sl-value-pop{animation:slValuePop .32s cubic-bezier(.2,.8,.2,1)}
539
560
  /* Primary checkout CTA. Scoped under .sl-picker so it OUTWEIGHS the
540
561
  '.sl-picker button' reset (0,1,1) \u2014 an unscoped '.sl-cta' (0,1,0) loses to it
541
562
  and the button renders as plain text with no accent fill. */
542
563
  .sl-picker .sl-cta{display:flex;align-items:center;justify-content:center;width:100%;min-height:44px;
543
564
  padding:12px 16px;border-radius:var(--sl-r-sm);font-weight:800;font-size:14px;line-height:1.1;
544
565
  background:var(--sl-accent);color:var(--sl-accent-ink);
545
- transition:filter .15s,background .15s,color .15s,transform .06s}
566
+ transition:filter .15s,background .22s,color .22s,transform .12s,box-shadow .22s;gap:8px}
546
567
  .sl-picker .sl-cta:hover{filter:brightness(1.08)}
547
568
  .sl-picker .sl-cta:active{transform:translateY(1px);filter:brightness(.94)}
569
+ .sl-picker .sl-cta.sl-ready{animation:slCtaReady .42s cubic-bezier(.2,.8,.2,1)}
570
+ .sl-cta-spin,.sl-ba-spin{width:14px;height:14px;border-radius:50%;border:2px solid currentColor;border-right-color:transparent;
571
+ animation:slspin .7s linear infinite;flex:none}
548
572
  /* Disabled ("Select seats"): quieter, but still a full-width button shape. */
549
573
  .sl-picker .sl-cta:disabled{background:var(--sl-surface);color:var(--sl-muted);opacity:1;
550
574
  cursor:not-allowed;filter:none;transform:none}
@@ -582,11 +606,15 @@ var CSS = `
582
606
  .sl-zoom svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
583
607
 
584
608
  /* toast + boot states (toast flows in the bottom-center region) */
585
- .sl-toast{transform:translateY(6px);max-width:100%;
609
+ .sl-toast{transform:translateY(6px) scale(.98);max-width:100%;
586
610
  background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-text);border-radius:999px;padding:9px 16px;
587
- font-size:12.5px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .2s,transform .2s;white-space:nowrap;
611
+ font-size:12.5px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .22s,transform .22s;white-space:nowrap;
588
612
  overflow:hidden;text-overflow:ellipsis}
589
- .sl-toast.on{opacity:1;transform:translateY(0)}
613
+ .sl-toast.on{opacity:1;transform:translateY(0) scale(1)}
614
+ .sl-toast[data-tone="error"]{border-color:#ef4444}
615
+ .sl-toast[data-tone="warning"]{border-color:var(--sl-accent)}
616
+ .sl-toast[data-tone="success"]{border-color:#22c55e}
617
+ .sl-toast.on[data-tone="error"]{animation:slToastNudge .32s ease-out}
590
618
  .sl-boot{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;align-items:center;justify-content:center;
591
619
  gap:10px;background:var(--sl-bg);font-size:13px;font-weight:600;color:var(--sl-muted)}
592
620
  .sl-boot-spin{width:24px;height:24px;border-radius:50%;border:3px solid var(--sl-line);border-top-color:var(--sl-accent);
@@ -610,15 +638,22 @@ var CSS = `
610
638
  .sl-extend-btn:disabled{opacity:.5;cursor:not-allowed}
611
639
 
612
640
  /* booked confirmation overlay (covers the widget once the held seats are sold) */
613
- .sl-booked{position:absolute;inset:0;z-index:11;display:none;flex-direction:column;align-items:center;
614
- justify-content:center;gap:12px;text-align:center;padding:28px;background:var(--sl-bg)}
615
- .sl-booked.on{display:flex}
641
+ .sl-booked{position:absolute;inset:0;z-index:11;display:flex;flex-direction:column;align-items:center;
642
+ justify-content:center;gap:12px;text-align:center;padding:28px;background:var(--sl-bg);opacity:0;visibility:hidden;
643
+ pointer-events:none;transition:opacity .34s ease,visibility 0s linear .34s}
644
+ .sl-booked.on{opacity:1;visibility:visible;pointer-events:auto;transition:opacity .34s ease,visibility 0s}
616
645
  .sl-booked-badge{width:60px;height:60px;border-radius:999px;display:flex;align-items:center;justify-content:center;
617
- background:var(--sl-accent);color:var(--sl-accent-ink)}
618
- .sl-booked-badge svg{width:30px;height:30px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round}
646
+ background:var(--sl-accent);color:var(--sl-accent-ink);transform:scale(.72)}
647
+ .sl-booked.on .sl-booked-badge{animation:slSuccessPop .58s cubic-bezier(.2,1.25,.3,1) .08s both}
648
+ .sl-booked-badge svg{width:30px;height:30px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round;
649
+ stroke-dasharray:30;stroke-dashoffset:30}
650
+ .sl-booked.on .sl-booked-badge svg{animation:slCheckDraw .42s ease-out .32s forwards}
619
651
  .sl-booked-title{font-weight:800;font-size:19px;color:var(--sl-text)}
620
652
  .sl-booked-sub{font-size:13px;color:var(--sl-muted);line-height:1.5;max-width:320px}
621
653
  .sl-booked-seats{font-weight:700;color:var(--sl-text)}
654
+ .sl-booked.on .sl-booked-title,.sl-booked.on .sl-booked-sub{animation:slCopyRise .42s ease-out both}
655
+ .sl-booked.on .sl-booked-title{animation-delay:.22s}
656
+ .sl-booked.on .sl-booked-sub{animation-delay:.3s}
622
657
 
623
658
  /* a11y filter chips (flow within the top-left region) */
624
659
  .sl-chips{display:flex;gap:6px;flex-wrap:wrap}
@@ -629,7 +664,8 @@ var CSS = `
629
664
 
630
665
  /* confirm popover */
631
666
  .sl-confirm{position:absolute;z-index:9;min-width:190px;background:var(--sl-surface);border:1px solid var(--sl-line);
632
- border-radius:12px;padding:12px;box-shadow:0 18px 50px -18px rgba(0,0,0,.7);transform:translate(-50%,calc(-100% - 14px))}
667
+ border-radius:12px;padding:12px;box-shadow:0 18px 50px -18px rgba(0,0,0,.7);transform:translate(-50%,calc(-100% - 14px));
668
+ animation:slConfirmIn .24s cubic-bezier(.2,.8,.2,1) both}
633
669
  .sl-confirm-label{font-weight:800;font-size:14px}
634
670
  .sl-confirm-meta{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--sl-muted);margin-top:4px}
635
671
  .sl-confirm-meta b{color:var(--sl-text);margin-left:auto}
@@ -647,8 +683,10 @@ var CSS = `
647
683
  .sl-ba-qty button{width:24px;height:24px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
648
684
  font-size:14px;font-weight:700;display:flex;align-items:center;justify-content:center}
649
685
  .sl-ba-qty span{min-width:14px;text-align:center;font-weight:800}
650
- .sl-ba-go{margin-left:auto;padding:7px 12px;border-radius:999px;border:1px solid var(--sl-line);font-weight:700;font-size:12px;transition:border-color .15s}
686
+ .sl-ba-go{margin-left:auto;padding:7px 12px;border-radius:999px;border:1px solid var(--sl-line);font-weight:700;font-size:12px;
687
+ transition:border-color .15s,opacity .15s;display:flex;align-items:center;gap:6px}
651
688
  .sl-ba-go:hover{border-color:var(--sl-muted)}
689
+ .sl-ba-go:disabled{opacity:.62;cursor:wait}
652
690
 
653
691
  /* screen-reader live region */
654
692
  .sl-sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
@@ -754,6 +792,27 @@ var CSS = `
754
792
  .sl-price-row.sl-dim{opacity:.4}
755
793
  .sl-seccard-mix-item.sl-dim{opacity:.4}
756
794
 
795
+ /* Buyer-journey motion: every animation explains a state transition (selected,
796
+ held, checkout handoff, conflict or booked). No decorative infinite motion
797
+ except the expiring-hold pulse and active progress spinners. */
798
+ @keyframes slPillIn{from{opacity:0;transform:translateX(7px) scale(.9)}to{opacity:1;transform:translateX(0) scale(1)}}
799
+ @keyframes slHoldPulse{0%{box-shadow:0 0 0 0 currentColor;opacity:.9}75%,100%{box-shadow:0 0 0 7px transparent;opacity:.55}}
800
+ @keyframes slChipIn{from{opacity:0;transform:translateY(8px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}
801
+ @keyframes slChipOut{to{opacity:0;transform:translateX(10px) scale(.98)}}
802
+ @keyframes slNoticeIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
803
+ @keyframes slValuePop{0%{opacity:.6;transform:translateY(3px)}55%{transform:translateY(-1px) scale(1.05)}100%{opacity:1;transform:none}}
804
+ @keyframes slCtaReady{0%{transform:scale(.98);box-shadow:0 0 0 0 transparent}55%{transform:scale(1.01);box-shadow:0 0 0 5px color-mix(in srgb,var(--sl-accent) 18%,transparent)}100%{transform:none;box-shadow:none}}
805
+ @keyframes slToastNudge{0%,100%{margin-left:0}30%{margin-left:-4px}60%{margin-left:3px}}
806
+ @keyframes slSuccessPop{0%{opacity:0;transform:scale(.72)}65%{opacity:1;transform:scale(1.08)}100%{opacity:1;transform:scale(1)}}
807
+ @keyframes slCheckDraw{to{stroke-dashoffset:0}}
808
+ @keyframes slCopyRise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
809
+ @keyframes slConfirmIn{from{opacity:0;transform:translate(-50%,calc(-100% - 8px)) scale(.96)}to{opacity:1;transform:translate(-50%,calc(-100% - 14px)) scale(1)}}
810
+
811
+ @media(prefers-reduced-motion:reduce){
812
+ .sl-picker *,.sl-modal-scrim *{animation-duration:.001ms!important;animation-iteration-count:1!important;
813
+ transition-duration:.001ms!important;scroll-behavior:auto!important}
814
+ }
815
+
757
816
  /* modal host */
758
817
  .sl-modal-scrim{position:fixed;inset:0;z-index:2147483000;background:rgba(5,7,12,.66);display:flex;align-items:center;justify-content:center;padding:18px}
759
818
  .sl-modal-frame{width:min(1200px,100%);height:min(820px,100%);border-radius:16px;overflow:hidden;box-shadow:0 40px 120px -30px rgba(0,0,0,.8)}
@@ -794,6 +853,8 @@ var SeatPicker = class _SeatPicker {
794
853
  this.ro = null;
795
854
  this.holdTimer = null;
796
855
  this.toastTimer = null;
856
+ /** Short-lived UI motion timers; all are cancelled on destroy. */
857
+ this.motionTimers = /* @__PURE__ */ new Set();
797
858
  // state
798
859
  this.currency = "USD";
799
860
  this.hold = null;
@@ -837,6 +898,12 @@ var SeatPicker = class _SeatPicker {
837
898
  this.secCardShownAt = 0;
838
899
  /** Previous tray ticket count — first 0→n transition auto-expands the mobile sheet. */
839
900
  this.lastTrayCount = 0;
901
+ /** Previous computed total — drives a single explanatory value bump. */
902
+ this.lastTrayTotal = 0;
903
+ /** Stable item keys prevent tray chips re-animating on unrelated realtime syncs. */
904
+ this.lastTrayKeys = /* @__PURE__ */ new Set();
905
+ this.bestAvailableBusy = false;
906
+ this.ctaPhase = "idle";
840
907
  // narrow-layout chrome that docks into the sheet's Filters row on mobile
841
908
  this.a11yChipsEl = null;
842
909
  this.cbEl = null;
@@ -850,9 +917,9 @@ var SeatPicker = class _SeatPicker {
850
917
  if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
851
918
  if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
852
919
  this.opts = options;
853
- const api = new PubApi((options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, ""));
920
+ this.api = new PubApi((options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, ""));
854
921
  this.controller = new PickerController2({
855
- transport: api,
922
+ transport: this.api,
856
923
  eventKey: options.event,
857
924
  maxSelection: options.maxSelection ?? DEFAULT_MAX_SELECTION2,
858
925
  currency: options.currency,
@@ -872,14 +939,16 @@ var SeatPicker = class _SeatPicker {
872
939
  this.hold = null;
873
940
  this.handedOff = false;
874
941
  this.bookedShown = false;
942
+ this.ctaPhase = "idle";
875
943
  this.stopHoldTimer();
876
944
  this.gaQty.clear();
877
- this.toast(t2("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.");
945
+ this.toast(t2("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.", "warning");
878
946
  this.syncTray();
879
947
  this.opts.onHoldExpired?.();
880
948
  },
881
949
  confirmSelection: options.confirmSelection,
882
950
  onSelect: (seat) => {
951
+ this.flashPickedSeat(seat.id);
883
952
  if (this.opts.confirmSelection) this.showConfirm(seat);
884
953
  },
885
954
  onViewChange: () => {
@@ -921,10 +990,19 @@ var SeatPicker = class _SeatPicker {
921
990
  const picker = new _SeatPicker({ ...options, container: frame });
922
991
  picker.modalScrim = scrim;
923
992
  picker.prevFocus = document.activeElement;
993
+ let closing = false;
924
994
  const close = () => {
995
+ if (closing) return;
996
+ closing = true;
925
997
  document.body.style.overflow = prevOverflow;
926
- picker.destroy();
927
- options.onClose?.();
998
+ scrim.style.opacity = "0";
999
+ scrim.style.pointerEvents = "none";
1000
+ const finish = () => {
1001
+ picker.destroy();
1002
+ options.onClose?.();
1003
+ };
1004
+ if (picker.hold && !picker.handedOff) void picker.release().finally(finish);
1005
+ else finish();
928
1006
  };
929
1007
  picker.closeModal = close;
930
1008
  scrim.addEventListener("mousedown", (e) => {
@@ -993,6 +1071,10 @@ var SeatPicker = class _SeatPicker {
993
1071
  <div class="sl-sec">Your seats</div>
994
1072
  <div class="sl-tray" data-ref="tray"></div>
995
1073
  <div class="sl-foot">
1074
+ <div class="sl-hold-note" data-ref="holdNote" role="status" aria-live="polite">
1075
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
1076
+ <span><b data-ref="holdTitle">Seats secured</b><span class="sl-hold-copy" data-ref="holdCopy">Checkout timer is running.</span></span>
1077
+ </div>
996
1078
  <div class="sl-total"><span data-ref="count"></span><b data-ref="total"></b></div>
997
1079
  <button type="button" class="sl-cta" data-ref="cta" disabled></button>
998
1080
  </div>
@@ -1219,6 +1301,85 @@ var SeatPicker = class _SeatPicker {
1219
1301
  cssVar(name) {
1220
1302
  return this.root ? getComputedStyle(this.root).getPropertyValue(name).trim() : "";
1221
1303
  }
1304
+ /** Motion is progressive enhancement; all state remains legible when reduced. */
1305
+ reducedMotion() {
1306
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1307
+ }
1308
+ scheduleMotion(fn, delay) {
1309
+ const timer = setTimeout(() => {
1310
+ this.motionTimers.delete(timer);
1311
+ if (!this.destroyed) fn();
1312
+ }, delay);
1313
+ this.motionTimers.add(timer);
1314
+ }
1315
+ /** Restart one finite CSS animation without leaving a permanent state class. */
1316
+ animateOnce(el, className, duration = 600) {
1317
+ if (!el || this.reducedMotion()) return;
1318
+ el.classList.remove(className);
1319
+ void el.offsetWidth;
1320
+ el.classList.add(className);
1321
+ this.scheduleMotion(() => el.classList.remove(className), duration);
1322
+ }
1323
+ /** Selection feedback belongs on the selected seat, not across the whole map. */
1324
+ flashPickedSeat(id) {
1325
+ if (this.reducedMotion()) return;
1326
+ this.controller.flashSeat(id, this.cssVar("--sl-accent") || "#f4b740");
1327
+ }
1328
+ /** A completed hold gets one short map ripple per concrete seat. */
1329
+ flashHeldSeats(hold) {
1330
+ if (this.reducedMotion()) return;
1331
+ const labels = (hold.items ?? []).filter((item) => item.objectType !== "ga").map((item) => item.label);
1332
+ labels.slice(0, 10).forEach((label, index) => {
1333
+ const seat = this.controller.seatByLabel(label);
1334
+ if (!seat) return;
1335
+ this.scheduleMotion(
1336
+ () => this.controller.flashSeat(seat.id, this.cssVar("--sl-accent") || "#f4b740"),
1337
+ index * 55
1338
+ );
1339
+ });
1340
+ }
1341
+ /** Update only the action affordance; selection callbacks must not refire. */
1342
+ pendingSelectionCount() {
1343
+ const heldItems = this.hold?.items ?? [];
1344
+ const heldLabels = new Set(heldItems.map((item) => item.label));
1345
+ const pendingSeats = this.controller.getSelection().filter((seat) => !heldLabels.has(seat.label)).length;
1346
+ const heldGA = /* @__PURE__ */ new Map();
1347
+ for (const item of heldItems.filter((candidate) => candidate.objectType === "ga")) {
1348
+ heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
1349
+ }
1350
+ const pendingGA = [...this.gaQty.entries()].reduce(
1351
+ (sum, [areaId, qty]) => sum + Math.max(0, qty - (heldGA.get(areaId) ?? 0)),
1352
+ 0
1353
+ );
1354
+ return pendingSeats + pendingGA;
1355
+ }
1356
+ syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
1357
+ const cta = this.els.cta;
1358
+ if (!cta) return;
1359
+ if (this.ctaPhase === "holding") {
1360
+ cta.disabled = true;
1361
+ cta.innerHTML = '<span class="sl-cta-spin" aria-hidden="true"></span>Securing seats\u2026';
1362
+ return;
1363
+ }
1364
+ if (this.ctaPhase === "checkout") {
1365
+ cta.disabled = true;
1366
+ cta.innerHTML = '<span class="sl-cta-spin" aria-hidden="true"></span>Opening checkout\u2026';
1367
+ return;
1368
+ }
1369
+ cta.disabled = count === 0;
1370
+ cta.textContent = this.hold ? pending ? `Secure ${pending} more & checkout` : "Continue to checkout" : count ? "Hold seats & checkout" : "Select seats";
1371
+ }
1372
+ setCtaPhase(phase) {
1373
+ this.ctaPhase = phase;
1374
+ this.syncCta();
1375
+ if (phase === "checkout") {
1376
+ this.scheduleMotion(() => {
1377
+ if (this.ctaPhase !== "checkout") return;
1378
+ this.ctaPhase = "idle";
1379
+ this.syncCta();
1380
+ }, 1100);
1381
+ }
1382
+ }
1222
1383
  /** Section-bearing objects on the active floor (single-floor → doc.objects). */
1223
1384
  activeFloorObjects() {
1224
1385
  const doc = this.controller.doc;
@@ -1473,7 +1634,11 @@ var SeatPicker = class _SeatPicker {
1473
1634
  (r) => `<button type="button" data-rung="${r}" title="${TIP[r]}" aria-pressed="false">${LABEL[r]}</button>`
1474
1635
  ).join("");
1475
1636
  pills.querySelectorAll("button").forEach((btn) => {
1476
- btn.addEventListener("click", () => this.controller.setRung(btn.dataset.rung));
1637
+ btn.addEventListener("click", () => {
1638
+ const rung = btn.dataset.rung;
1639
+ this.controller.setRung(rung);
1640
+ if (rung === "seats") this.collapseSectionCard();
1641
+ });
1477
1642
  });
1478
1643
  this.regions["top-center"].appendChild(pills);
1479
1644
  this.rungsEl = pills;
@@ -1524,7 +1689,7 @@ var SeatPicker = class _SeatPicker {
1524
1689
  this.secCardEl?.remove();
1525
1690
  this.secCardEl = null;
1526
1691
  if (!summary) return;
1527
- this.secCardCollapsed = false;
1692
+ this.secCardCollapsed = this.controller.getRung() === "seats";
1528
1693
  this.secCardShownAt = Date.now();
1529
1694
  this.renderSectionCard(summary);
1530
1695
  }
@@ -1590,6 +1755,10 @@ var SeatPicker = class _SeatPicker {
1590
1755
  sectionCardOnView() {
1591
1756
  if (!this.secCardEl || this.secCardCollapsed || !this.lastSection) return;
1592
1757
  if (this.root?.dataset.layout === "narrow") return;
1758
+ if (this.controller.getRung() === "seats") {
1759
+ this.collapseSectionCard();
1760
+ return;
1761
+ }
1593
1762
  if (Date.now() - this.secCardShownAt < 1400) {
1594
1763
  if (this.sectionCardCoverage() > 0.25) this.collapseSectionCard();
1595
1764
  return;
@@ -1807,7 +1976,7 @@ var SeatPicker = class _SeatPicker {
1807
1976
  const gone = this.controller.getSelection().filter((s) => !ownLabels.has(s.label) && (this.controller.getStatus(s.id) ?? "free") !== "free");
1808
1977
  if (!gone.length) return;
1809
1978
  this.controller.deselect(gone.map((s) => s.id));
1810
- this.toast(`Seat ${gone[0].label} was just taken by another buyer.`);
1979
+ this.toast(`Seat ${gone[0].label} was just taken by another buyer.`, "error");
1811
1980
  }
1812
1981
  syncTray() {
1813
1982
  if (!this.els.tray) return;
@@ -1815,27 +1984,32 @@ var SeatPicker = class _SeatPicker {
1815
1984
  const gaAreas = this.controller.getGAAreas();
1816
1985
  const heldItems = this.hold?.items ?? [];
1817
1986
  const parts = [];
1987
+ const nextTrayKeys = /* @__PURE__ */ new Set();
1818
1988
  if (!seats.length && !heldItems.length && !gaAreas.length) {
1819
1989
  parts.push(`<div class="sl-tray-hint">Tap a seat on the map, or let us pick the best available for you.</div>`);
1820
1990
  } else if (!seats.length && !heldItems.length) {
1821
1991
  parts.push(`<div class="sl-tray-hint">Tap a seat on the map \u2014 or grab standing tickets below.</div>`);
1822
1992
  }
1823
1993
  for (const item of heldItems) {
1994
+ const itemKey = `held:${item.label}`;
1995
+ nextTrayKeys.add(itemKey);
1824
1996
  const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
1825
1997
  const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
1826
1998
  const canView2 = this.seatViewEnabled() && item.objectType !== "ga" && !!this.controller.seatByLabel(item.label);
1827
1999
  parts.push(
1828
- `<div class="sl-chip"><b>${item.label}</b><span class="cat">${cat?.label ?? item.categoryKey}${tierName ? ` \xB7 ${tierName}` : ""}</span><span class="amt">${this.money(item.unitPrice * (item.quantity ?? 1))}</span>` + (canView2 ? `<button type="button" class="view" data-view-label="${item.label}" aria-label="${t2("picker.viewFromSeat", { label: item.label })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "") + `</div>`
2000
+ `<div class="sl-chip sl-held${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}"><b>${item.label}</b><span class="cat">${cat?.label ?? item.categoryKey}${tierName ? ` \xB7 ${tierName}` : ""}</span><span class="sl-chip-state">Held</span><span class="amt">${this.money(item.unitPrice * (item.quantity ?? 1))}</span>` + (canView2 ? `<button type="button" class="view" data-view-label="${item.label}" aria-label="${t2("picker.viewFromSeat", { label: item.label })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "") + `</div>`
1829
2001
  );
1830
2002
  }
1831
2003
  const heldLabels = new Set(heldItems.map((item) => item.label));
1832
2004
  const canView = this.seatViewEnabled();
1833
2005
  for (const s of seats.filter((seat) => !heldLabels.has(seat.label))) {
2006
+ const itemKey = `seat:${s.id}`;
2007
+ nextTrayKeys.add(itemKey);
1834
2008
  const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
1835
2009
  const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${t2("picker.ticketTierFor", { label: s.label })}">` + s.tiers.map((ti) => `<option value="${ti.id}"${ti.id === s.tierId ? " selected" : ""}>${ti.name} \xB7 ${this.money(ti.price)}</option>`).join("") + `</select>` : "";
1836
2010
  const viewBtn = canView ? `<button type="button" class="view" data-view-label="${s.label}" aria-label="${t2("picker.viewFromSeat", { label: s.label })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "";
1837
2011
  parts.push(
1838
- `<div class="sl-chip" data-seat="${s.id}"><b>${s.label}</b><span class="cat">${cat?.label ?? s.categoryKey}</span>` + tierSelect + `<span class="amt">${this.money(s.price)}</span>` + viewBtn + `<button type="button" class="rm" aria-label="Remove ${s.label}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div>`
2012
+ `<div class="sl-chip${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-seat="${s.id}"><b>${s.label}</b><span class="cat">${cat?.label ?? s.categoryKey}</span>` + tierSelect + `<span class="sl-chip-state sl-pending">Selected</span><span class="amt">${this.money(s.price)}</span>` + viewBtn + `<button type="button" class="rm" aria-label="Remove ${s.label}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div>`
1839
2013
  );
1840
2014
  }
1841
2015
  for (const area of gaAreas) {
@@ -1847,10 +2021,11 @@ var SeatPicker = class _SeatPicker {
1847
2021
  if (!this.hold) {
1848
2022
  const cats = this.controller.doc?.categories ?? [];
1849
2023
  parts.push(
1850
- `<div class="sl-ba">` + (cats.length > 1 ? `<select aria-label="Category" data-ba-cat><option value="">Any tier</option>` + cats.map((c) => `<option value="${c.key}"${this.baCat === c.key ? " selected" : ""}>${c.label}</option>`).join("") + `</select>` : "") + `<div class="sl-ba-qty"><button type="button" data-ba="-1" aria-label="Fewer seats">\u2212</button><span>${this.baQty}</span><button type="button" data-ba="1" aria-label="More seats">+</button></div><button type="button" class="sl-ba-go">Best available</button></div>`
2024
+ `<div class="sl-ba">` + (cats.length > 1 ? `<select aria-label="Category" data-ba-cat><option value="">Any tier</option>` + cats.map((c) => `<option value="${c.key}"${this.baCat === c.key ? " selected" : ""}>${c.label}</option>`).join("") + `</select>` : "") + `<div class="sl-ba-qty"><button type="button" data-ba="-1" aria-label="Fewer seats">\u2212</button><span>${this.baQty}</span><button type="button" data-ba="1" aria-label="More seats">+</button></div><button type="button" class="sl-ba-go"${this.bestAvailableBusy ? " disabled" : ""}>` + (this.bestAvailableBusy ? `<span class="sl-ba-spin" aria-hidden="true"></span>Finding\u2026` : "Best available") + `</button></div>`
1851
2025
  );
1852
2026
  }
1853
2027
  this.els.tray.innerHTML = parts.join("");
2028
+ this.lastTrayKeys = nextTrayKeys;
1854
2029
  this.els.tray.querySelectorAll("[data-ba]").forEach((btn) => {
1855
2030
  btn.addEventListener("click", () => {
1856
2031
  this.baQty = Math.max(1, Math.min(8, this.baQty + Number(btn.dataset.ba)));
@@ -1865,8 +2040,14 @@ var SeatPicker = class _SeatPicker {
1865
2040
  });
1866
2041
  this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
1867
2042
  btn.addEventListener("click", () => {
1868
- const id = btn.closest(".sl-chip").dataset.seat;
1869
- this.controller.deselect([id]);
2043
+ const chip = btn.closest(".sl-chip");
2044
+ const id = chip.dataset.seat;
2045
+ if (this.reducedMotion()) {
2046
+ this.controller.deselect([id]);
2047
+ return;
2048
+ }
2049
+ chip.classList.add("sl-leave");
2050
+ this.scheduleMotion(() => this.controller.deselect([id]), 150);
1870
2051
  });
1871
2052
  });
1872
2053
  this.els.tray.querySelectorAll(".sl-chip .tier").forEach((sel) => {
@@ -1895,14 +2076,27 @@ var SeatPicker = class _SeatPicker {
1895
2076
  const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
1896
2077
  const total = freshSeats.reduce((sum, s) => sum + s.price, 0) + gaTotal + heldTotal;
1897
2078
  const count = freshSeats.length + gaCount + heldCount;
2079
+ const pendingCount = this.pendingSelectionCount();
2080
+ const previousCount = this.lastTrayCount;
2081
+ const previousTotal = this.lastTrayTotal;
1898
2082
  this.els.count.textContent = count ? `${count} ${count === 1 ? "ticket" : "tickets"}` : "No seats selected";
1899
2083
  this.els.total.textContent = count ? this.money(total) : "";
1900
- const cta = this.els.cta;
1901
- cta.disabled = count === 0;
1902
- cta.textContent = this.hold ? "Continue to checkout" : count ? "Hold seats & checkout" : "Select seats";
2084
+ this.syncCta(count, pendingCount);
2085
+ if (this.hold) {
2086
+ const securedCount = heldCount || this.hold.seats?.length || 0;
2087
+ if (this.els.holdTitle) {
2088
+ this.els.holdTitle.textContent = `${securedCount} ${securedCount === 1 ? "seat" : "seats"} secured`;
2089
+ }
2090
+ if (this.els.holdCopy) {
2091
+ this.els.holdCopy.textContent = pendingCount ? `${pendingCount} more selected \u2014 secure before checkout.` : "Checkout timer is running.";
2092
+ }
2093
+ }
2094
+ if (count !== previousCount) this.animateOnce(this.els.count, "sl-value-pop", 380);
2095
+ if (total !== previousTotal) this.animateOnce(this.els.total, "sl-value-pop", 380);
2096
+ if (previousCount === 0 && count > 0) this.animateOnce(this.els.cta, "sl-ready", 520);
1903
2097
  if (this.els.peek) {
1904
2098
  if (count) {
1905
- this.els.peek.innerHTML = `<span>${count} ${count === 1 ? "ticket" : "tickets"} \xB7 ${this.money(total)}</span><span class="go">${this.hold ? "Continue" : "Review"}</span>`;
2099
+ this.els.peek.innerHTML = `<span>${count} ${count === 1 ? "ticket" : "tickets"} \xB7 ${this.money(total)}</span><span class="go">${this.hold ? pendingCount ? "Secure more" : "Continue" : "Review"}</span>`;
1906
2100
  } else {
1907
2101
  const prices = (this.controller.doc?.categories ?? []).map((c) => this.catPrice(c)).filter((p) => p != null);
1908
2102
  this.els.peek.innerHTML = (prices.length ? `<span>From ${this.money(Math.min(...prices))}</span>` : "<span>Pick your seats</span>") + `<span class="sub">\xB7 Best available</span>`;
@@ -1912,18 +2106,18 @@ var SeatPicker = class _SeatPicker {
1912
2106
  this.root.dataset.sheet = "open";
1913
2107
  }
1914
2108
  this.lastTrayCount = count;
2109
+ this.lastTrayTotal = total;
1915
2110
  this.opts.onSelectionChange?.(seats);
1916
2111
  }
1917
2112
  async handleCta() {
1918
- const cta = this.els.cta;
1919
2113
  if (this.hold && !this.controller.getSelection().some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
1920
2114
  const seats = this.hold.seats ?? this.controller.getSelection();
1921
2115
  this.handedOff = true;
2116
+ this.setCtaPhase("checkout");
1922
2117
  this.opts.onCheckout?.(this.hold, seats, this.buildHandoff(this.hold));
1923
2118
  return;
1924
2119
  }
1925
- cta.disabled = true;
1926
- cta.textContent = "Holding\u2026";
2120
+ this.setCtaPhase("holding");
1927
2121
  try {
1928
2122
  let hold = null;
1929
2123
  const gaEntries = [...this.gaQty.entries()].filter(([, q]) => q > 0);
@@ -1937,18 +2131,23 @@ var SeatPicker = class _SeatPicker {
1937
2131
  hold = h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : hold;
1938
2132
  }
1939
2133
  if (!hold) {
1940
- this.toast("One or more seats were just taken. Please pick again.");
2134
+ this.toast("One or more seats were just taken. Please pick again.", "error");
2135
+ this.setCtaPhase("idle");
1941
2136
  this.syncTray();
1942
2137
  return;
1943
2138
  }
1944
2139
  this.hold = hold;
1945
2140
  this.handedOff = true;
1946
2141
  this.startHoldTimer(hold.expiresAt);
1947
- this.opts.onCheckout?.(hold, chosenSeats.length ? chosenSeats : hold.seats ?? [], this.buildHandoff(hold));
2142
+ this.flashHeldSeats(hold);
2143
+ this.setCtaPhase("checkout");
2144
+ this.opts.onCheckout?.(hold, hold.seats ?? chosenSeats, this.buildHandoff(hold));
1948
2145
  } catch (err) {
1949
2146
  this.opts.onError?.(err);
1950
- this.toast("One or more seats were just taken. Please pick again.");
2147
+ this.toast("One or more seats were just taken. Please pick again.", "error");
2148
+ this.setCtaPhase("idle");
1951
2149
  } finally {
2150
+ if (this.ctaPhase === "holding") this.ctaPhase = "idle";
1952
2151
  this.syncTray();
1953
2152
  }
1954
2153
  }
@@ -1956,12 +2155,16 @@ var SeatPicker = class _SeatPicker {
1956
2155
  this.stopHoldTimer();
1957
2156
  this.holdExpiresAt = expiresAt;
1958
2157
  const pill = this.els.hold;
2158
+ pill.innerHTML = '<span class="sl-hold-dot" aria-hidden="true"></span><span>Held</span><span class="sl-hold-time" data-ref="holdTime"></span>';
2159
+ const time = pill.querySelector('[data-ref="holdTime"]');
2160
+ this.els.holdNote?.classList.add("on");
1959
2161
  const tick = () => {
1960
2162
  const ms = Math.max(0, this.holdExpiresAt - Date.now());
1961
2163
  const m = Math.floor(ms / 6e4);
1962
2164
  const s = String(Math.floor(ms % 6e4 / 1e3)).padStart(2, "0");
1963
- pill.textContent = `Held ${m}:${s}`;
2165
+ if (time) time.textContent = `${m}:${s}`;
1964
2166
  pill.classList.add("on");
2167
+ pill.classList.toggle("is-expiring", ms > 0 && ms <= EXTEND_PROMPT_MS);
1965
2168
  this.setExtendPrompt(ms > 0 && ms <= EXTEND_PROMPT_MS, ms);
1966
2169
  if (ms <= 0) this.stopHoldTimer();
1967
2170
  };
@@ -1971,7 +2174,8 @@ var SeatPicker = class _SeatPicker {
1971
2174
  stopHoldTimer() {
1972
2175
  if (this.holdTimer) clearInterval(this.holdTimer);
1973
2176
  this.holdTimer = null;
1974
- this.els.hold?.classList.remove("on");
2177
+ this.els.hold?.classList.remove("on", "is-expiring");
2178
+ this.els.holdNote?.classList.remove("on");
1975
2179
  this.setExtendPrompt(false, 0);
1976
2180
  }
1977
2181
  /** Show/refresh (or hide) the "Need more time?" prompt with the live seconds left. */
@@ -1996,13 +2200,13 @@ var SeatPicker = class _SeatPicker {
1996
2200
  this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
1997
2201
  this.holdExpiresAt = h.expiresAt;
1998
2202
  this.extendEl?.classList.remove("on");
1999
- this.toast("More time added \u2014 your seats are still held.");
2203
+ this.toast("More time added \u2014 your seats are still held.", "success");
2000
2204
  } else {
2001
- this.toast("Couldn't add more time \u2014 please head to checkout now.");
2205
+ this.toast("Couldn't add more time \u2014 please head to checkout now.", "warning");
2002
2206
  }
2003
2207
  } catch (err) {
2004
2208
  this.opts.onError?.(err);
2005
- this.toast("Couldn't add more time \u2014 please head to checkout now.");
2209
+ this.toast("Couldn't add more time \u2014 please head to checkout now.", "warning");
2006
2210
  } finally {
2007
2211
  btn.disabled = false;
2008
2212
  btn.textContent = prev;
@@ -2050,13 +2254,17 @@ var SeatPicker = class _SeatPicker {
2050
2254
  const total = lineItems.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
2051
2255
  return { holdId: hold.holdId, expiresAt: hold.expiresAt, currency, lineItems, total };
2052
2256
  }
2053
- toast(msg) {
2257
+ toast(msg, tone = "neutral") {
2054
2258
  const el = this.els.toast;
2055
2259
  if (!el) return;
2056
2260
  el.textContent = msg;
2261
+ el.dataset.tone = tone;
2057
2262
  el.classList.add("on");
2058
2263
  if (this.toastTimer) clearTimeout(this.toastTimer);
2059
- this.toastTimer = setTimeout(() => el.classList.remove("on"), 4200);
2264
+ this.toastTimer = setTimeout(() => {
2265
+ el.classList.remove("on");
2266
+ el.dataset.tone = "neutral";
2267
+ }, 4200);
2060
2268
  }
2061
2269
  placeTooltip() {
2062
2270
  if (!this.tipEl) return;
@@ -2086,6 +2294,13 @@ var SeatPicker = class _SeatPicker {
2086
2294
  return this.controller.getSelection();
2087
2295
  }
2088
2296
  async bestAvailable(qty, categoryKey) {
2297
+ if (this.bestAvailableBusy) return null;
2298
+ this.bestAvailableBusy = true;
2299
+ const button = this.els.tray?.querySelector(".sl-ba-go");
2300
+ if (button) {
2301
+ button.disabled = true;
2302
+ button.innerHTML = '<span class="sl-ba-spin" aria-hidden="true"></span>Finding\u2026';
2303
+ }
2089
2304
  try {
2090
2305
  const h = await this.controller.bestAvailable(qty, categoryKey);
2091
2306
  if (h) {
@@ -2093,30 +2308,58 @@ var SeatPicker = class _SeatPicker {
2093
2308
  this.handedOff = false;
2094
2309
  this.bookedShown = false;
2095
2310
  this.startHoldTimer(h.expiresAt);
2311
+ this.flashHeldSeats(this.hold);
2096
2312
  this.syncTray();
2097
2313
  return this.hold;
2098
2314
  }
2099
2315
  return null;
2100
2316
  } catch (err) {
2101
2317
  this.opts.onError?.(err);
2318
+ this.toast("Those seats are no longer available. Try another quantity or category.", "error");
2102
2319
  return null;
2320
+ } finally {
2321
+ this.bestAvailableBusy = false;
2322
+ if (!this.hold && button?.isConnected) {
2323
+ button.disabled = false;
2324
+ button.textContent = "Best available";
2325
+ }
2103
2326
  }
2104
2327
  }
2105
2328
  async release() {
2106
- await this.controller.release();
2329
+ const tracked = this.hold;
2330
+ const controllerHold = this.controller.currentHold();
2331
+ if (controllerHold) {
2332
+ await this.controller.release();
2333
+ } else if (tracked) {
2334
+ const labels = [.../* @__PURE__ */ new Set([
2335
+ ...(tracked.items ?? []).map((item) => item.label),
2336
+ ...(tracked.seats ?? []).map((seat) => seat.label)
2337
+ ])];
2338
+ if (labels.length) {
2339
+ try {
2340
+ await this.api.release(this.opts.event, labels, tracked.holdId);
2341
+ } catch (error) {
2342
+ this.opts.onError?.(error);
2343
+ }
2344
+ }
2345
+ }
2107
2346
  this.hold = null;
2108
2347
  this.handedOff = false;
2109
2348
  this.bookedShown = false;
2349
+ this.ctaPhase = "idle";
2110
2350
  this.stopHoldTimer();
2111
2351
  this.gaQty.clear();
2112
2352
  this.syncTray();
2113
2353
  }
2114
2354
  destroy() {
2115
2355
  this.destroyed = true;
2356
+ if (this.hold && !this.handedOff) void this.controller.release();
2116
2357
  this.closeConfirm();
2117
2358
  this.closeSeatView();
2118
2359
  this.stopHoldTimer();
2119
2360
  if (this.toastTimer) clearTimeout(this.toastTimer);
2361
+ for (const timer of this.motionTimers) clearTimeout(timer);
2362
+ this.motionTimers.clear();
2120
2363
  this.ro?.disconnect();
2121
2364
  this.ro = null;
2122
2365
  if (this.escHandler) document.removeEventListener("keydown", this.escHandler);