@seatlayer/js 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -52,6 +52,12 @@ var PubApi = class {
52
52
  body: { qty, ...categoryKey ? { categoryKey } : {} }
53
53
  });
54
54
  }
55
+ resume(key, holdId) {
56
+ return request(this.base, `/pub/events/${encodeURIComponent(key)}/hold/resume`, {
57
+ method: "POST",
58
+ body: { holdId }
59
+ });
60
+ }
55
61
  release(key, labels, holdId) {
56
62
  return request(this.base, `/pub/events/${encodeURIComponent(key)}/release`, {
57
63
  method: "POST",
@@ -108,6 +114,7 @@ var SeatingChart = class {
108
114
  currency: options.currency,
109
115
  onSelectionChange: (seats) => this.opts.onSelectionChange?.(seats),
110
116
  onHold: (h) => this.opts.onHold?.({ holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items }),
117
+ onHoldRestored: (h) => this.opts.onHoldRestored?.({ holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items }),
111
118
  onHoldExpired: () => this.opts.onHoldExpired?.(),
112
119
  onGAClick: (areaId) => {
113
120
  const area = this.controller.getGAAreas().find((candidate) => candidate.id === areaId);
@@ -211,6 +218,21 @@ var SeatingChart = class {
211
218
  return null;
212
219
  }
213
220
  }
221
+ /** Restore an active hold by its opaque id without extending its expiry. */
222
+ async resumeHold(holdId) {
223
+ try {
224
+ const h = await this.controller.resumeHold(holdId);
225
+ return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
226
+ } catch (err) {
227
+ this.opts.onError?.(err);
228
+ return null;
229
+ }
230
+ }
231
+ /** Current active hold known to this chart, if any. */
232
+ getCurrentHold() {
233
+ const h = this.controller.currentHold();
234
+ return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
235
+ }
214
236
  getGAAreas() {
215
237
  return this.controller.getGAAreas();
216
238
  }
@@ -279,6 +301,10 @@ var SeatingChart = class {
279
301
  async release() {
280
302
  await this.controller.release();
281
303
  }
304
+ /** Release selected labels from the current hold while keeping the remainder. */
305
+ async releaseLabels(labels) {
306
+ return this.controller.releaseLabels(labels);
307
+ }
282
308
  /** Tear everything down: close the socket, stop timers, drop the canvas. */
283
309
  destroy() {
284
310
  if (this.hostEl && this.onTipMove) this.hostEl.removeEventListener("mousemove", this.onTipMove);
@@ -466,6 +492,15 @@ var CSS = `
466
492
  .sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side > :not(.sl-sheet-head){display:none}
467
493
  .sl-picker[data-layout="narrow"] .sl-tray{flex:none}
468
494
  .sl-picker[data-layout="narrow"] .sl-foot{position:sticky;bottom:0;background:var(--sl-bg)}
495
+ .sl-picker[data-layout="narrow"] .sl-sheet-head{order:0}
496
+ .sl-picker[data-layout="narrow"] .sl-seats-sec{order:1}
497
+ .sl-picker[data-layout="narrow"] .sl-tray{order:2}
498
+ .sl-picker[data-layout="narrow"] .sl-filtersec{order:3}
499
+ .sl-picker[data-layout="narrow"] .sl-filters{order:4}
500
+ .sl-picker[data-layout="narrow"] .sl-prices-sec{order:5}
501
+ .sl-picker[data-layout="narrow"] .sl-pricef{order:6}
502
+ .sl-picker[data-layout="narrow"] .sl-prices{order:7}
503
+ .sl-picker[data-layout="narrow"] .sl-foot{order:8}
469
504
  /* touch chrome: pinch-zoom exists \u2014 hide +/\u2212 on the sheet layout (keep fit) */
470
505
  .sl-picker[data-layout="narrow"] .sl-zoom [data-ref="zin"],
471
506
  .sl-picker[data-layout="narrow"] .sl-zoom [data-ref="zout"]{display:none}
@@ -512,6 +547,11 @@ var CSS = `
512
547
  .sl-price-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
513
548
  .sl-price-left{font-size:11px;color:var(--sl-muted);font-variant-numeric:tabular-nums}
514
549
  .sl-price-amt{font-weight:800;font-variant-numeric:tabular-nums}
550
+ .sl-status-key{display:flex;gap:12px;flex-wrap:wrap;padding:8px 16px 12px;border-bottom:1px solid var(--sl-line);color:var(--sl-muted);font-size:11px}
551
+ .sl-status-item{display:inline-flex;align-items:center;gap:6px}
552
+ .sl-status-icon{width:17px;height:17px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;
553
+ color:#fff;background:#6b7280;font-size:9px;font-weight:900;line-height:1}
554
+ .sl-status-icon.sold{background:#374151;font-size:14px}
515
555
 
516
556
  /* tray */
517
557
  .sl-tray{flex:1;padding:10px 16px;display:flex;flex-direction:column;gap:8px;min-height:0}
@@ -662,29 +702,47 @@ var CSS = `
662
702
  .sl-chip-f:hover{color:var(--sl-text)}
663
703
  .sl-chip-f.on{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
664
704
 
665
- /* confirm popover */
666
- .sl-confirm{position:absolute;z-index:9;min-width:190px;background:var(--sl-surface);border:1px solid var(--sl-line);
667
- border-radius:12px;padding:12px;box-shadow:0 18px 50px -18px rgba(0,0,0,.7);transform:translate(-50%,calc(-100% - 14px));
705
+ /* confirm card: a candidate is not in the tray until Select. Map gestures and
706
+ floating chrome pause while the card owns focus, keeping the camera stable. */
707
+ .sl-picker[data-confirming="true"] .sl-map-host>:not(.sl-confirm){pointer-events:none}
708
+ .sl-picker[data-confirming="true"] .sl-anchor{pointer-events:none;opacity:.28;transition:opacity .16s}
709
+ .sl-picker[data-confirming="true"] .sl-side{pointer-events:none;opacity:.58;transition:opacity .16s}
710
+ .sl-confirm{position:absolute;z-index:10;width:276px;max-width:calc(100% - 24px);overflow:hidden;pointer-events:auto;
711
+ background:var(--sl-surface);border:1px solid color-mix(in srgb,var(--sl-line) 70%,var(--sl-text));
712
+ border-radius:15px;box-shadow:0 24px 64px -18px rgba(0,0,0,.72);transform:translate(-50%,calc(-100% - 16px));
668
713
  animation:slConfirmIn .24s cubic-bezier(.2,.8,.2,1) both}
669
- .sl-confirm-label{font-weight:800;font-size:14px}
670
- .sl-confirm-meta{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--sl-muted);margin-top:4px}
671
- .sl-confirm-meta b{color:var(--sl-text);margin-left:auto}
714
+ .sl-confirm[data-placement="below"]{transform:translate(-50%,16px);animation:slConfirmBelowIn .24s cubic-bezier(.2,.8,.2,1) both}
715
+ .sl-confirm-grid{display:grid;grid-template-columns:1.2fr .8fr .8fr;border-bottom:1px solid var(--sl-line)}
716
+ .sl-confirm-field{min-width:0;padding:12px 11px 10px;border-right:1px solid var(--sl-line)}
717
+ .sl-confirm-field:last-child{border-right:0;text-align:center}
718
+ .sl-confirm-field:nth-child(2){text-align:center}
719
+ .sl-confirm-key{display:block;font-size:8.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
720
+ .sl-confirm-value{display:block;margin-top:4px;color:var(--sl-text);font-size:17px;line-height:1.1;font-weight:850;
721
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
722
+ .sl-confirm-cat{display:flex;align-items:center;gap:8px;padding:10px 12px;background:color-mix(in srgb,var(--sl-cat) 76%,var(--sl-surface))}
723
+ .sl-confirm-cat .sl-dot{border:2px solid rgba(255,255,255,.78);width:11px;height:11px}
724
+ .sl-confirm-cat-name{font-size:13.5px;font-weight:800;color:#fff;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
725
+ .sl-confirm-price{font-size:17px;font-weight:850;color:#fff;font-variant-numeric:tabular-nums}
726
+ .sl-confirm-body{padding:11px 12px 12px}
672
727
  .sl-confirm-row{display:flex;gap:8px;margin-top:10px}
673
- .sl-confirm-row button{flex:1;padding:8px;border-radius:8px;font-weight:700;font-size:12.5px}
674
- .sl-confirm-add{background:var(--sl-accent);color:var(--sl-accent-ink)}
675
- .sl-confirm-cancel{border:1px solid var(--sl-line);color:var(--sl-muted)}
728
+ .sl-confirm-row button{flex:1;min-height:44px;padding:9px 12px;border-radius:9px;font-weight:800;font-size:13px}
729
+ .sl-confirm-add{background:var(--sl-accent)!important;color:var(--sl-accent-ink)!important;display:flex;align-items:center;justify-content:center;gap:7px}
730
+ .sl-confirm-add svg{width:16px;height:16px;stroke:currentColor;stroke-width:2.8;fill:none;stroke-linecap:round;stroke-linejoin:round}
731
+ .sl-confirm-cancel{background:color-mix(in srgb,var(--sl-line) 44%,transparent)!important;border:1px solid var(--sl-line)!important;color:var(--sl-muted)!important}
676
732
  .sl-confirm-cancel:hover{color:var(--sl-text)}
733
+ .sl-picker[data-layout="narrow"] .sl-confirm{left:50%!important;top:auto!important;bottom:14px;width:min(342px,calc(100% - 24px));
734
+ transform:translateX(-50%);animation:slConfirmMobileIn .24s cubic-bezier(.2,.8,.2,1) both}
677
735
 
678
736
  /* best-available row */
679
- .sl-ba{display:flex;align-items:center;gap:8px;padding:9px 11px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm)}
737
+ .sl-ba{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:8px;padding:9px 11px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm)}
680
738
  .sl-ba select{background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:7px;
681
- font:inherit;font-size:12px;padding:5px 6px;max-width:110px}
739
+ font:inherit;font-size:12px;padding:7px 8px;min-width:0;width:100%;max-width:none}
682
740
  .sl-ba-qty{display:flex;align-items:center;gap:7px}
683
741
  .sl-ba-qty button{width:24px;height:24px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
684
742
  font-size:14px;font-weight:700;display:flex;align-items:center;justify-content:center}
685
743
  .sl-ba-qty span{min-width:14px;text-align:center;font-weight:800}
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}
744
+ .sl-ba-go{grid-column:1/-1;width:100%;min-height:38px;padding:7px 12px;border-radius:9px;border:1px solid var(--sl-line);font-weight:800;font-size:12px;
745
+ transition:border-color .15s,opacity .15s;display:flex;align-items:center;justify-content:center;gap:6px}
688
746
  .sl-ba-go:hover{border-color:var(--sl-muted)}
689
747
  .sl-ba-go:disabled{opacity:.62;cursor:wait}
690
748
 
@@ -807,6 +865,8 @@ var CSS = `
807
865
  @keyframes slCheckDraw{to{stroke-dashoffset:0}}
808
866
  @keyframes slCopyRise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
809
867
  @keyframes slConfirmIn{from{opacity:0;transform:translate(-50%,calc(-100% - 8px)) scale(.96)}to{opacity:1;transform:translate(-50%,calc(-100% - 14px)) scale(1)}}
868
+ @keyframes slConfirmBelowIn{from{opacity:0;transform:translate(-50%,8px) scale(.96)}to{opacity:1;transform:translate(-50%,16px) scale(1)}}
869
+ @keyframes slConfirmMobileIn{from{opacity:0;transform:translate(-50%,10px) scale(.97)}to{opacity:1;transform:translate(-50%,0) scale(1)}}
810
870
 
811
871
  @media(prefers-reduced-motion:reduce){
812
872
  .sl-picker *,.sl-modal-scrim *{animation-duration:.001ms!important;animation-iteration-count:1!important;
@@ -903,6 +963,9 @@ var SeatPicker = class _SeatPicker {
903
963
  /** Stable item keys prevent tray chips re-animating on unrelated realtime syncs. */
904
964
  this.lastTrayKeys = /* @__PURE__ */ new Set();
905
965
  this.bestAvailableBusy = false;
966
+ this.releasingLabels = /* @__PURE__ */ new Set();
967
+ /** Selected labels awaiting the hold response; their own realtime echo can arrive first. */
968
+ this.holdingLabels = /* @__PURE__ */ new Set();
906
969
  this.ctaPhase = "idle";
907
970
  // narrow-layout chrome that docks into the sheet's Filters row on mobile
908
971
  this.a11yChipsEl = null;
@@ -916,8 +979,9 @@ var SeatPicker = class _SeatPicker {
916
979
  if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
917
980
  if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
918
981
  if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
919
- this.opts = options;
920
- this.api = new PubApi((options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, ""));
982
+ this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
983
+ this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
984
+ this.api = new PubApi(this.apiBase);
921
985
  this.controller = new PickerController2({
922
986
  transport: this.api,
923
987
  eventKey: options.event,
@@ -927,7 +991,7 @@ var SeatPicker = class _SeatPicker {
927
991
  colorblindSafe: options.colorblindSafe,
928
992
  onSelectionChange: () => {
929
993
  this.syncTray();
930
- if (this.controller.getSelection().length) this.collapseSectionCard();
994
+ if (this.committedSelection().length) this.collapseSectionCard();
931
995
  },
932
996
  onStatusChange: () => {
933
997
  this.syncPrices();
@@ -937,6 +1001,7 @@ var SeatPicker = class _SeatPicker {
937
1001
  },
938
1002
  onHoldExpired: () => {
939
1003
  this.hold = null;
1004
+ this.forgetHold();
940
1005
  this.handedOff = false;
941
1006
  this.bookedShown = false;
942
1007
  this.ctaPhase = "idle";
@@ -944,13 +1009,17 @@ var SeatPicker = class _SeatPicker {
944
1009
  this.gaQty.clear();
945
1010
  this.toast(t2("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.", "warning");
946
1011
  this.syncTray();
1012
+ this.emitHoldChange();
947
1013
  this.opts.onHoldExpired?.();
948
1014
  },
949
- confirmSelection: options.confirmSelection,
1015
+ confirmSelection: this.opts.confirmSelection,
950
1016
  onSelect: (seat) => {
951
1017
  this.flashPickedSeat(seat.id);
952
1018
  if (this.opts.confirmSelection) this.showConfirm(seat);
953
1019
  },
1020
+ onDeselect: (seat) => {
1021
+ if (this.confirmSeat?.id === seat.id) this.dismissConfirm();
1022
+ },
954
1023
  onViewChange: () => {
955
1024
  this.reanchorConfirm();
956
1025
  this.syncRung();
@@ -1009,7 +1078,13 @@ var SeatPicker = class _SeatPicker {
1009
1078
  if (e.target === scrim) close();
1010
1079
  });
1011
1080
  picker.escHandler = (e) => {
1012
- if (e.key === "Escape") close();
1081
+ if (e.key !== "Escape") return;
1082
+ if (picker.confirmSeat) {
1083
+ e.preventDefault();
1084
+ picker.cancelConfirm();
1085
+ } else {
1086
+ close();
1087
+ }
1013
1088
  };
1014
1089
  document.addEventListener("keydown", picker.escHandler);
1015
1090
  await picker.render();
@@ -1026,8 +1101,15 @@ var SeatPicker = class _SeatPicker {
1026
1101
  const mount = resolveContainer3(this.opts.container);
1027
1102
  const root = document.createElement("div");
1028
1103
  root.className = "sl-picker";
1104
+ root.tabIndex = -1;
1029
1105
  this.root = root;
1030
1106
  mount.appendChild(root);
1107
+ root.addEventListener("keydown", (e) => {
1108
+ if (e.key !== "Escape" || !this.confirmSeat) return;
1109
+ e.preventDefault();
1110
+ e.stopPropagation();
1111
+ this.cancelConfirm();
1112
+ });
1031
1113
  Object.entries(resolveTokens(void 0, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
1032
1114
  root.innerHTML = `
1033
1115
  <div class="sl-head">
@@ -1066,9 +1148,9 @@ var SeatPicker = class _SeatPicker {
1066
1148
  </div>
1067
1149
  <div class="sl-sec sl-filtersec" data-ref="filtersSec">Filters</div>
1068
1150
  <div class="sl-filters" data-ref="filters"></div>
1069
- <div class="sl-sec" data-ref="pricesSec">Prices</div>
1151
+ <div class="sl-sec sl-prices-sec" data-ref="pricesSec">Prices</div>
1070
1152
  <div class="sl-prices" data-ref="prices"></div>
1071
- <div class="sl-sec">Your seats</div>
1153
+ <div class="sl-sec sl-seats-sec">Your seats</div>
1072
1154
  <div class="sl-tray" data-ref="tray"></div>
1073
1155
  <div class="sl-foot">
1074
1156
  <div class="sl-hold-note" data-ref="holdNote" role="status" aria-live="polite">
@@ -1226,6 +1308,8 @@ var SeatPicker = class _SeatPicker {
1226
1308
  this.buildExtendPrompt();
1227
1309
  this.buildBookedOverlay();
1228
1310
  this.dockLayoutChrome();
1311
+ await this.restoreRememberedHold();
1312
+ if (this.destroyed) return this;
1229
1313
  this.syncPrices();
1230
1314
  this.syncTray();
1231
1315
  return this;
@@ -1339,10 +1423,14 @@ var SeatPicker = class _SeatPicker {
1339
1423
  });
1340
1424
  }
1341
1425
  /** Update only the action affordance; selection callbacks must not refire. */
1426
+ committedSelection() {
1427
+ const candidateId = this.confirmSeat?.id;
1428
+ return this.controller.getSelection().filter((seat) => seat.id !== candidateId);
1429
+ }
1342
1430
  pendingSelectionCount() {
1343
1431
  const heldItems = this.hold?.items ?? [];
1344
1432
  const heldLabels = new Set(heldItems.map((item) => item.label));
1345
- const pendingSeats = this.controller.getSelection().filter((seat) => !heldLabels.has(seat.label)).length;
1433
+ const pendingSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).length;
1346
1434
  const heldGA = /* @__PURE__ */ new Map();
1347
1435
  for (const item of heldItems.filter((candidate) => candidate.objectType === "ga")) {
1348
1436
  heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
@@ -1356,6 +1444,11 @@ var SeatPicker = class _SeatPicker {
1356
1444
  syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
1357
1445
  const cta = this.els.cta;
1358
1446
  if (!cta) return;
1447
+ if (this.confirmSeat) {
1448
+ cta.disabled = true;
1449
+ cta.textContent = "Confirm or cancel this seat";
1450
+ return;
1451
+ }
1359
1452
  if (this.ctaPhase === "holding") {
1360
1453
  cta.disabled = true;
1361
1454
  cta.innerHTML = '<span class="sl-cta-spin" aria-hidden="true"></span>Securing seats\u2026';
@@ -1380,6 +1473,68 @@ var SeatPicker = class _SeatPicker {
1380
1473
  }, 1100);
1381
1474
  }
1382
1475
  }
1476
+ /** Session-scoped capability key: isolated by API origin and event. */
1477
+ holdStorageKey() {
1478
+ return `@seatlayer/hold/v1/${encodeURIComponent(this.apiBase)}/${encodeURIComponent(this.opts.event)}`;
1479
+ }
1480
+ rememberedHoldId() {
1481
+ if (this.opts.initialHoldId) return this.opts.initialHoldId;
1482
+ if (this.opts.restoreHold === false || typeof window === "undefined") return null;
1483
+ try {
1484
+ return window.sessionStorage.getItem(this.holdStorageKey());
1485
+ } catch {
1486
+ return null;
1487
+ }
1488
+ }
1489
+ rememberHold(hold) {
1490
+ if (this.opts.restoreHold === false || typeof window === "undefined") return;
1491
+ try {
1492
+ window.sessionStorage.setItem(this.holdStorageKey(), hold.holdId);
1493
+ } catch {
1494
+ }
1495
+ }
1496
+ forgetHold() {
1497
+ if (typeof window === "undefined") return;
1498
+ try {
1499
+ window.sessionStorage.removeItem(this.holdStorageKey());
1500
+ } catch {
1501
+ }
1502
+ }
1503
+ async resumeHoldFromServer(holdId, automatic) {
1504
+ try {
1505
+ const h = await this.controller.resumeHold(holdId);
1506
+ if (!h) return null;
1507
+ const restored = {
1508
+ holdId: h.holdId,
1509
+ expiresAt: h.expiresAt,
1510
+ seats: h.seats,
1511
+ items: h.items
1512
+ };
1513
+ this.hold = restored;
1514
+ this.handedOff = true;
1515
+ this.bookedShown = false;
1516
+ this.ctaPhase = "idle";
1517
+ this.startHoldTimer(restored.expiresAt);
1518
+ this.rememberHold(restored);
1519
+ this.syncTray();
1520
+ this.emitHoldChange();
1521
+ this.opts.onHoldRestored?.(restored, restored.seats ?? [], this.buildHandoff(restored));
1522
+ if (automatic) this.toast("Your held tickets have been restored.", "success");
1523
+ return restored;
1524
+ } catch (error) {
1525
+ const status = error?.status;
1526
+ if (status === 404 || status === 409) {
1527
+ this.forgetHold();
1528
+ } else {
1529
+ this.opts.onError?.(error);
1530
+ }
1531
+ return null;
1532
+ }
1533
+ }
1534
+ async restoreRememberedHold() {
1535
+ const holdId = this.rememberedHoldId();
1536
+ if (holdId) await this.resumeHoldFromServer(holdId, true);
1537
+ }
1383
1538
  /** Section-bearing objects on the active floor (single-floor → doc.objects). */
1384
1539
  activeFloorObjects() {
1385
1540
  const doc = this.controller.doc;
@@ -1801,36 +1956,78 @@ var SeatPicker = class _SeatPicker {
1801
1956
  const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
1802
1957
  this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
1803
1958
  }
1804
- // ---- confirm popover (opt-in confirmSelection mode) ------------------------
1959
+ // ---- seat candidate confirmation ------------------------------------------
1805
1960
  showConfirm(seat) {
1806
- this.closeConfirm();
1961
+ const previousId = this.confirmSeat?.id;
1962
+ this.confirmEl?.remove();
1963
+ this.confirmEl = null;
1964
+ this.confirmSeat = seat;
1965
+ this.root?.setAttribute("data-confirming", "true");
1966
+ this.controller.setSelectionFocus(seat.id);
1967
+ if (previousId && previousId !== seat.id) this.controller.deselect([previousId]);
1807
1968
  if (this.tipEl) this.tipEl.style.display = "none";
1969
+ const details = this.controller.seatDetails(seat.id);
1808
1970
  const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
1809
- const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
1971
+ const price = details?.price ?? (cat?.tiers?.length ? cat.tiers[0].price : cat?.price);
1972
+ const safe = (value) => String(value ?? "\u2014").replace(/[&<>"]/g, (char) => ({
1973
+ "&": "&amp;",
1974
+ "<": "&lt;",
1975
+ ">": "&gt;",
1976
+ '"': "&quot;"
1977
+ })[char]);
1810
1978
  const el = document.createElement("div");
1811
1979
  el.className = "sl-confirm";
1812
- el.innerHTML = `<div class="sl-confirm-label">${seat.label}</div><div class="sl-confirm-meta"><span class="sl-dot" style="background:${cat?.color ?? "#6e7bff"}"></span>${cat?.label ?? seat.categoryKey}${price != null ? `<b>${this.money(price)}</b>` : ""}</div>` + (this.seatViewEnabled() ? `<button type="button" class="sl-confirm-view"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>${t2("picker.open360")}</button>` : "") + `<div class="sl-confirm-row"><button type="button" class="sl-confirm-cancel">Cancel</button><button type="button" class="sl-confirm-add">Add seat</button></div>`;
1980
+ el.setAttribute("role", "dialog");
1981
+ el.setAttribute("aria-modal", "true");
1982
+ el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
1983
+ el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
1984
+ el.innerHTML = `<div class="sl-confirm-grid"><div class="sl-confirm-field"><span class="sl-confirm-key">Section</span><span class="sl-confirm-value">${safe(details?.sectionLabel)}</span></div><div class="sl-confirm-field"><span class="sl-confirm-key">Row</span><span class="sl-confirm-value">${safe(details?.rowLabel)}</span></div><div class="sl-confirm-field"><span class="sl-confirm-key">Seat</span><span class="sl-confirm-value">${safe(details?.seatNumber ?? seat.label)}</span></div></div><div class="sl-confirm-cat"><span class="sl-dot" style="background:${cat?.color ?? "#6e7bff"}"></span><span class="sl-confirm-cat-name">${safe(details?.categoryLabel ?? cat?.label ?? seat.categoryKey)}</span>` + (price != null ? `<span class="sl-confirm-price">${this.money(price)}</span>` : "") + `</div><div class="sl-confirm-body">` + (this.seatViewEnabled() ? `<button type="button" class="sl-confirm-view"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>${t2("picker.open360")}</button>` : "") + `<div class="sl-confirm-row"><button type="button" class="sl-confirm-cancel">Cancel</button><button type="button" class="sl-confirm-add"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4 4L19 7"/></svg>Select</button></div></div>`;
1813
1985
  this.els.map.appendChild(el);
1814
1986
  this.confirmEl = el;
1815
- this.confirmSeat = seat;
1816
1987
  this.reanchorConfirm();
1817
1988
  el.querySelector(".sl-confirm-view")?.addEventListener("click", () => this.openSeatView(seat));
1818
- el.querySelector(".sl-confirm-add").addEventListener("click", () => this.closeConfirm());
1819
- el.querySelector(".sl-confirm-cancel").addEventListener("click", () => {
1820
- this.controller.deselect([seat.id]);
1821
- this.closeConfirm();
1822
- });
1989
+ el.querySelector(".sl-confirm-add").addEventListener("click", () => this.commitConfirm());
1990
+ el.querySelector(".sl-confirm-cancel").addEventListener("click", () => this.cancelConfirm());
1991
+ requestAnimationFrame(() => el.querySelector(".sl-confirm-add")?.focus());
1823
1992
  }
1824
1993
  reanchorConfirm() {
1825
1994
  if (!this.confirmEl || !this.confirmSeat) return;
1826
1995
  const p = this.controller.worldToScreen({ x: this.confirmSeat.x, y: this.confirmSeat.y });
1827
- this.confirmEl.style.left = `${p.x}px`;
1828
- this.confirmEl.style.top = `${p.y}px`;
1829
- }
1830
- closeConfirm() {
1996
+ if (this.root?.dataset.layout === "narrow") return;
1997
+ const mapWidth = this.els.map.clientWidth;
1998
+ const mapHeight = this.els.map.clientHeight;
1999
+ const cardWidth = this.confirmEl.offsetWidth || 276;
2000
+ const cardHeight = this.confirmEl.offsetHeight || 230;
2001
+ const half = cardWidth / 2 + 12;
2002
+ const x = Math.max(half, Math.min(mapWidth - half, p.x));
2003
+ const belowFits = p.y + cardHeight + 24 <= mapHeight;
2004
+ const placeBelow = p.y < cardHeight + 24 && belowFits;
2005
+ this.confirmEl.dataset.placement = placeBelow ? "below" : "above";
2006
+ this.confirmEl.style.left = `${x}px`;
2007
+ this.confirmEl.style.top = `${Math.max(8, Math.min(mapHeight - 8, p.y))}px`;
2008
+ }
2009
+ dismissConfirm() {
1831
2010
  this.confirmEl?.remove();
1832
2011
  this.confirmEl = null;
1833
2012
  this.confirmSeat = null;
2013
+ this.root?.removeAttribute("data-confirming");
2014
+ this.controller.setSelectionFocus(null);
2015
+ }
2016
+ commitConfirm() {
2017
+ if (!this.confirmSeat) return;
2018
+ this.dismissConfirm();
2019
+ this.collapseSectionCard();
2020
+ this.syncTray();
2021
+ }
2022
+ cancelConfirm() {
2023
+ const seat = this.confirmSeat;
2024
+ if (!seat) return;
2025
+ this.controller.deselect([seat.id]);
2026
+ if (this.confirmSeat) this.dismissConfirm();
2027
+ this.root?.focus({ preventScroll: true });
2028
+ }
2029
+ closeConfirm() {
2030
+ this.dismissConfirm();
1834
2031
  }
1835
2032
  // ---- 360° view-from-seat modal --------------------------------------------
1836
2033
  seatViewEnabled() {
@@ -1853,7 +2050,6 @@ var SeatPicker = class _SeatPicker {
1853
2050
  openSeatView(seat) {
1854
2051
  if (!this.root || !this.seatViewEnabled()) return;
1855
2052
  this.closeSeatView();
1856
- this.closeConfirm();
1857
2053
  const doc = this.controller.doc;
1858
2054
  const activeId = this.controller.getActiveFloorId();
1859
2055
  const focal = doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
@@ -1964,7 +2160,7 @@ var SeatPicker = class _SeatPicker {
1964
2160
  const price = c.tiers?.length ? c.tiers[0].price : c.price;
1965
2161
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
1966
2162
  return `<div class="sl-price-row${dim ? " sl-dim" : ""}" data-cat="${c.key}"><span class="sl-dot" style="background:${c.color}"></span><span class="sl-price-label">${c.label}</span><span class="sl-price-left">${left[c.key] ?? 0} left</span>` + (price != null ? `<span class="sl-price-amt">${this.money(price)}</span>` : "") + `</div>`;
1967
- }).join("");
2163
+ }).join("") + `<div class="sl-status-key" aria-label="Seat status legend"><span class="sl-status-item"><i class="sl-status-icon">H</i>Held by another buyer</span><span class="sl-status-item"><i class="sl-status-icon sold">\xD7</i>Sold</span></div>`;
1968
2164
  this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
1969
2165
  row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
1970
2166
  row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
@@ -1972,7 +2168,10 @@ var SeatPicker = class _SeatPicker {
1972
2168
  }
1973
2169
  /** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
1974
2170
  evictTakenSelections() {
1975
- const ownLabels = new Set(this.controller.currentHold()?.labels ?? []);
2171
+ const ownLabels = /* @__PURE__ */ new Set([
2172
+ ...this.controller.currentHold()?.labels ?? [],
2173
+ ...this.holdingLabels
2174
+ ]);
1976
2175
  const gone = this.controller.getSelection().filter((s) => !ownLabels.has(s.label) && (this.controller.getStatus(s.id) ?? "free") !== "free");
1977
2176
  if (!gone.length) return;
1978
2177
  this.controller.deselect(gone.map((s) => s.id));
@@ -1980,7 +2179,7 @@ var SeatPicker = class _SeatPicker {
1980
2179
  }
1981
2180
  syncTray() {
1982
2181
  if (!this.els.tray) return;
1983
- const seats = this.controller.getSelection();
2182
+ const seats = this.committedSelection();
1984
2183
  const gaAreas = this.controller.getGAAreas();
1985
2184
  const heldItems = this.hold?.items ?? [];
1986
2185
  const parts = [];
@@ -1997,7 +2196,7 @@ var SeatPicker = class _SeatPicker {
1997
2196
  const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
1998
2197
  const canView2 = this.seatViewEnabled() && item.objectType !== "ga" && !!this.controller.seatByLabel(item.label);
1999
2198
  parts.push(
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>`
2199
+ `<div class="sl-chip sl-held${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-held="${encodeURIComponent(item.label)}"><b>${item.label}</b><span class="cat">${cat?.label ?? item.categoryKey}${tierName ? ` \xB7 ${tierName}` : ""}</span><span class="sl-chip-state">Held by you</span><span class="amt">${this.money(item.unitPrice * (item.quantity ?? 1))}</span>` + (canView2 ? `<button type="button" class="view" data-view-label="${item.label}" aria-label="${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>` : "") + `<button type="button" class="rm" aria-label="Remove held ticket ${item.label}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div>`
2001
2200
  );
2002
2201
  }
2003
2202
  const heldLabels = new Set(heldItems.map((item) => item.label));
@@ -2041,6 +2240,10 @@ var SeatPicker = class _SeatPicker {
2041
2240
  this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
2042
2241
  btn.addEventListener("click", () => {
2043
2242
  const chip = btn.closest(".sl-chip");
2243
+ if (chip.dataset.held) {
2244
+ void this.removeHeldLabel(decodeURIComponent(chip.dataset.held), chip);
2245
+ return;
2246
+ }
2044
2247
  const id = chip.dataset.seat;
2045
2248
  if (this.reducedMotion()) {
2046
2249
  this.controller.deselect([id]);
@@ -2102,26 +2305,59 @@ var SeatPicker = class _SeatPicker {
2102
2305
  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>`;
2103
2306
  }
2104
2307
  }
2105
- if (this.root?.dataset.layout === "narrow" && count > 0 && this.lastTrayCount === 0) {
2106
- this.root.dataset.sheet = "open";
2107
- }
2108
2308
  this.lastTrayCount = count;
2109
2309
  this.lastTrayTotal = total;
2110
2310
  this.opts.onSelectionChange?.(seats);
2111
2311
  }
2312
+ async removeHeldLabel(label, chip) {
2313
+ if (!label || this.releasingLabels.has(label)) return false;
2314
+ this.releasingLabels.add(label);
2315
+ chip?.setAttribute("aria-busy", "true");
2316
+ const button = chip?.querySelector(".rm");
2317
+ if (button) button.disabled = true;
2318
+ try {
2319
+ const preserveAcrossNavigation = this.handedOff;
2320
+ const released = await this.controller.releaseLabels([label]);
2321
+ if (!released) {
2322
+ this.toast(`Couldn't remove ${label}. Your hold is unchanged.`, "error");
2323
+ return false;
2324
+ }
2325
+ const remaining = this.controller.currentHold();
2326
+ this.hold = remaining ? { holdId: remaining.holdId, expiresAt: remaining.expiresAt, seats: remaining.seats, items: remaining.items } : null;
2327
+ this.handedOff = !!this.hold && preserveAcrossNavigation;
2328
+ this.bookedShown = false;
2329
+ this.ctaPhase = "idle";
2330
+ if (this.hold) {
2331
+ this.startHoldTimer(this.hold.expiresAt);
2332
+ } else {
2333
+ this.stopHoldTimer();
2334
+ this.forgetHold();
2335
+ }
2336
+ this.syncTray();
2337
+ this.emitHoldChange();
2338
+ this.toast(`${label} removed from your hold.`, "success");
2339
+ return true;
2340
+ } finally {
2341
+ this.releasingLabels.delete(label);
2342
+ chip?.removeAttribute("aria-busy");
2343
+ if (button?.isConnected) button.disabled = false;
2344
+ }
2345
+ }
2112
2346
  async handleCta() {
2113
- if (this.hold && !this.controller.getSelection().some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
2114
- const seats = this.hold.seats ?? this.controller.getSelection();
2347
+ const committed = this.committedSelection();
2348
+ if (this.hold && !committed.some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
2349
+ const seats = this.hold.seats ?? committed;
2115
2350
  this.handedOff = true;
2116
2351
  this.setCtaPhase("checkout");
2117
2352
  this.opts.onCheckout?.(this.hold, seats, this.buildHandoff(this.hold));
2118
2353
  return;
2119
2354
  }
2355
+ this.holdingLabels = new Set(committed.map((seat) => seat.label));
2120
2356
  this.setCtaPhase("holding");
2121
2357
  try {
2122
2358
  let hold = null;
2123
2359
  const gaEntries = [...this.gaQty.entries()].filter(([, q]) => q > 0);
2124
- const chosenSeats = this.controller.getSelection();
2360
+ const chosenSeats = this.committedSelection();
2125
2361
  if (chosenSeats.length) {
2126
2362
  const h = await this.controller.hold(void 0, this.opts.holdTtlMs);
2127
2363
  hold = h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
@@ -2141,12 +2377,14 @@ var SeatPicker = class _SeatPicker {
2141
2377
  this.startHoldTimer(hold.expiresAt);
2142
2378
  this.flashHeldSeats(hold);
2143
2379
  this.setCtaPhase("checkout");
2380
+ this.emitHoldChange();
2144
2381
  this.opts.onCheckout?.(hold, hold.seats ?? chosenSeats, this.buildHandoff(hold));
2145
2382
  } catch (err) {
2146
2383
  this.opts.onError?.(err);
2147
2384
  this.toast("One or more seats were just taken. Please pick again.", "error");
2148
2385
  this.setCtaPhase("idle");
2149
2386
  } finally {
2387
+ this.holdingLabels.clear();
2150
2388
  if (this.ctaPhase === "holding") this.ctaPhase = "idle";
2151
2389
  this.syncTray();
2152
2390
  }
@@ -2154,6 +2392,7 @@ var SeatPicker = class _SeatPicker {
2154
2392
  startHoldTimer(expiresAt) {
2155
2393
  this.stopHoldTimer();
2156
2394
  this.holdExpiresAt = expiresAt;
2395
+ if (this.hold) this.rememberHold(this.hold);
2157
2396
  const pill = this.els.hold;
2158
2397
  pill.innerHTML = '<span class="sl-hold-dot" aria-hidden="true"></span><span>Held</span><span class="sl-hold-time" data-ref="holdTime"></span>';
2159
2398
  const time = pill.querySelector('[data-ref="holdTime"]');
@@ -2200,6 +2439,8 @@ var SeatPicker = class _SeatPicker {
2200
2439
  this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
2201
2440
  this.holdExpiresAt = h.expiresAt;
2202
2441
  this.extendEl?.classList.remove("on");
2442
+ this.rememberHold(this.hold);
2443
+ this.emitHoldChange();
2203
2444
  this.toast("More time added \u2014 your seats are still held.", "success");
2204
2445
  } else {
2205
2446
  this.toast("Couldn't add more time \u2014 please head to checkout now.", "warning");
@@ -2230,6 +2471,7 @@ var SeatPicker = class _SeatPicker {
2230
2471
  this.bookedShown = true;
2231
2472
  const handoff = this.buildHandoff(this.hold);
2232
2473
  this.stopHoldTimer();
2474
+ this.forgetHold();
2233
2475
  const n = handoff.lineItems.reduce((sum, i) => sum + i.quantity, 0);
2234
2476
  if (this.els.bookedSub) {
2235
2477
  this.els.bookedSub.innerHTML = `<span class="sl-booked-seats">${n} ${n === 1 ? "ticket" : "tickets"}</span> confirmed. A confirmation is on its way.`;
@@ -2254,6 +2496,14 @@ var SeatPicker = class _SeatPicker {
2254
2496
  const total = lineItems.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
2255
2497
  return { holdId: hold.holdId, expiresAt: hold.expiresAt, currency, lineItems, total };
2256
2498
  }
2499
+ emitHoldChange() {
2500
+ const hold = this.hold;
2501
+ this.opts.onHoldChange?.(
2502
+ hold,
2503
+ hold?.seats ?? [],
2504
+ hold ? this.buildHandoff(hold) : null
2505
+ );
2506
+ }
2257
2507
  toast(msg, tone = "neutral") {
2258
2508
  const el = this.els.toast;
2259
2509
  if (!el) return;
@@ -2285,16 +2535,34 @@ var SeatPicker = class _SeatPicker {
2285
2535
  return;
2286
2536
  }
2287
2537
  const statusLine = details.status === "free" ? "" : `<div style="margin-top:5px;font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;font-weight:700">${details.status === "held" ? t2("map.statusHeld") : t2("map.statusTaken")}</div>`;
2288
- this.tipEl.innerHTML = `<div style="font-weight:800;font-size:13px">${details.label}</div><div style="display:flex;align-items:center;gap:6px;margin-top:4px"><span style="width:9px;height:9px;border-radius:50%;flex:none;background:${details.categoryColor}"></span><span style="opacity:.75">${details.categoryLabel}</span><span style="margin-left:auto;font-weight:800">${this.money(details.price)}</span></div>` + statusLine;
2538
+ const location = [
2539
+ details.sectionLabel ? `Section ${details.sectionLabel}` : "",
2540
+ details.rowLabel ? `Row ${details.rowLabel}` : "",
2541
+ details.seatNumber ? `Seat ${details.seatNumber}` : details.label
2542
+ ].filter(Boolean).join(" \xB7 ");
2543
+ this.tipEl.innerHTML = `<div style="font-weight:800;font-size:13px">${location}</div><div style="display:flex;align-items:center;gap:6px;margin-top:4px"><span style="width:9px;height:9px;border-radius:50%;flex:none;background:${details.categoryColor}"></span><span style="opacity:.75">${details.categoryLabel}</span><span style="margin-left:auto;font-weight:800">${this.money(details.price)}</span></div>` + statusLine;
2289
2544
  this.tipEl.style.display = "block";
2290
2545
  this.placeTooltip();
2291
2546
  }
2292
2547
  // ---- public conveniences ----------------------------------------------------
2293
2548
  getSelection() {
2294
- return this.controller.getSelection();
2549
+ return this.committedSelection();
2550
+ }
2551
+ /** Current active/restored hold reflected in the tray. */
2552
+ getCurrentHold() {
2553
+ return this.hold;
2554
+ }
2555
+ /** Explicit host-driven hold restore (automatic session restore is on by default). */
2556
+ async resumeHold(holdId) {
2557
+ return this.resumeHoldFromServer(holdId, false);
2558
+ }
2559
+ /** Remove one server-held ticket while keeping the rest of the hold active. */
2560
+ async removeHeldTicket(label) {
2561
+ return this.removeHeldLabel(label);
2295
2562
  }
2296
2563
  async bestAvailable(qty, categoryKey) {
2297
2564
  if (this.bestAvailableBusy) return null;
2565
+ if (this.confirmSeat) this.cancelConfirm();
2298
2566
  this.bestAvailableBusy = true;
2299
2567
  const button = this.els.tray?.querySelector(".sl-ba-go");
2300
2568
  if (button) {
@@ -2310,6 +2578,7 @@ var SeatPicker = class _SeatPicker {
2310
2578
  this.startHoldTimer(h.expiresAt);
2311
2579
  this.flashHeldSeats(this.hold);
2312
2580
  this.syncTray();
2581
+ this.emitHoldChange();
2313
2582
  return this.hold;
2314
2583
  }
2315
2584
  return null;
@@ -2328,8 +2597,9 @@ var SeatPicker = class _SeatPicker {
2328
2597
  async release() {
2329
2598
  const tracked = this.hold;
2330
2599
  const controllerHold = this.controller.currentHold();
2600
+ let released = true;
2331
2601
  if (controllerHold) {
2332
- await this.controller.release();
2602
+ released = await this.controller.release();
2333
2603
  } else if (tracked) {
2334
2604
  const labels = [.../* @__PURE__ */ new Set([
2335
2605
  ...(tracked.items ?? []).map((item) => item.label),
@@ -2340,16 +2610,23 @@ var SeatPicker = class _SeatPicker {
2340
2610
  await this.api.release(this.opts.event, labels, tracked.holdId);
2341
2611
  } catch (error) {
2342
2612
  this.opts.onError?.(error);
2613
+ released = false;
2343
2614
  }
2344
2615
  }
2345
2616
  }
2617
+ if (!released) {
2618
+ this.toast("Couldn't release your tickets. Your hold is unchanged.", "error");
2619
+ return;
2620
+ }
2346
2621
  this.hold = null;
2622
+ this.forgetHold();
2347
2623
  this.handedOff = false;
2348
2624
  this.bookedShown = false;
2349
2625
  this.ctaPhase = "idle";
2350
2626
  this.stopHoldTimer();
2351
2627
  this.gaQty.clear();
2352
2628
  this.syncTray();
2629
+ this.emitHoldChange();
2353
2630
  }
2354
2631
  destroy() {
2355
2632
  this.destroyed = true;