@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.cjs CHANGED
@@ -84,6 +84,12 @@ var PubApi = class {
84
84
  body: { qty, ...categoryKey ? { categoryKey } : {} }
85
85
  });
86
86
  }
87
+ resume(key, holdId) {
88
+ return request(this.base, `/pub/events/${encodeURIComponent(key)}/hold/resume`, {
89
+ method: "POST",
90
+ body: { holdId }
91
+ });
92
+ }
87
93
  release(key, labels, holdId) {
88
94
  return request(this.base, `/pub/events/${encodeURIComponent(key)}/release`, {
89
95
  method: "POST",
@@ -140,6 +146,7 @@ var SeatingChart = class {
140
146
  currency: options.currency,
141
147
  onSelectionChange: (seats) => this.opts.onSelectionChange?.(seats),
142
148
  onHold: (h) => this.opts.onHold?.({ holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items }),
149
+ onHoldRestored: (h) => this.opts.onHoldRestored?.({ holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items }),
143
150
  onHoldExpired: () => this.opts.onHoldExpired?.(),
144
151
  onGAClick: (areaId) => {
145
152
  const area = this.controller.getGAAreas().find((candidate) => candidate.id === areaId);
@@ -243,6 +250,21 @@ var SeatingChart = class {
243
250
  return null;
244
251
  }
245
252
  }
253
+ /** Restore an active hold by its opaque id without extending its expiry. */
254
+ async resumeHold(holdId) {
255
+ try {
256
+ const h = await this.controller.resumeHold(holdId);
257
+ return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
258
+ } catch (err) {
259
+ this.opts.onError?.(err);
260
+ return null;
261
+ }
262
+ }
263
+ /** Current active hold known to this chart, if any. */
264
+ getCurrentHold() {
265
+ const h = this.controller.currentHold();
266
+ return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
267
+ }
246
268
  getGAAreas() {
247
269
  return this.controller.getGAAreas();
248
270
  }
@@ -311,6 +333,10 @@ var SeatingChart = class {
311
333
  async release() {
312
334
  await this.controller.release();
313
335
  }
336
+ /** Release selected labels from the current hold while keeping the remainder. */
337
+ async releaseLabels(labels) {
338
+ return this.controller.releaseLabels(labels);
339
+ }
314
340
  /** Tear everything down: close the socket, stop timers, drop the canvas. */
315
341
  destroy() {
316
342
  if (this.hostEl && this.onTipMove) this.hostEl.removeEventListener("mousemove", this.onTipMove);
@@ -490,6 +516,15 @@ var CSS = `
490
516
  .sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side > :not(.sl-sheet-head){display:none}
491
517
  .sl-picker[data-layout="narrow"] .sl-tray{flex:none}
492
518
  .sl-picker[data-layout="narrow"] .sl-foot{position:sticky;bottom:0;background:var(--sl-bg)}
519
+ .sl-picker[data-layout="narrow"] .sl-sheet-head{order:0}
520
+ .sl-picker[data-layout="narrow"] .sl-seats-sec{order:1}
521
+ .sl-picker[data-layout="narrow"] .sl-tray{order:2}
522
+ .sl-picker[data-layout="narrow"] .sl-filtersec{order:3}
523
+ .sl-picker[data-layout="narrow"] .sl-filters{order:4}
524
+ .sl-picker[data-layout="narrow"] .sl-prices-sec{order:5}
525
+ .sl-picker[data-layout="narrow"] .sl-pricef{order:6}
526
+ .sl-picker[data-layout="narrow"] .sl-prices{order:7}
527
+ .sl-picker[data-layout="narrow"] .sl-foot{order:8}
493
528
  /* touch chrome: pinch-zoom exists \u2014 hide +/\u2212 on the sheet layout (keep fit) */
494
529
  .sl-picker[data-layout="narrow"] .sl-zoom [data-ref="zin"],
495
530
  .sl-picker[data-layout="narrow"] .sl-zoom [data-ref="zout"]{display:none}
@@ -536,6 +571,11 @@ var CSS = `
536
571
  .sl-price-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
537
572
  .sl-price-left{font-size:11px;color:var(--sl-muted);font-variant-numeric:tabular-nums}
538
573
  .sl-price-amt{font-weight:800;font-variant-numeric:tabular-nums}
574
+ .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}
575
+ .sl-status-item{display:inline-flex;align-items:center;gap:6px}
576
+ .sl-status-icon{width:17px;height:17px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;
577
+ color:#fff;background:#6b7280;font-size:9px;font-weight:900;line-height:1}
578
+ .sl-status-icon.sold{background:#374151;font-size:14px}
539
579
 
540
580
  /* tray */
541
581
  .sl-tray{flex:1;padding:10px 16px;display:flex;flex-direction:column;gap:8px;min-height:0}
@@ -686,29 +726,47 @@ var CSS = `
686
726
  .sl-chip-f:hover{color:var(--sl-text)}
687
727
  .sl-chip-f.on{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
688
728
 
689
- /* confirm popover */
690
- .sl-confirm{position:absolute;z-index:9;min-width:190px;background:var(--sl-surface);border:1px solid var(--sl-line);
691
- border-radius:12px;padding:12px;box-shadow:0 18px 50px -18px rgba(0,0,0,.7);transform:translate(-50%,calc(-100% - 14px));
729
+ /* confirm card: a candidate is not in the tray until Select. Map gestures and
730
+ floating chrome pause while the card owns focus, keeping the camera stable. */
731
+ .sl-picker[data-confirming="true"] .sl-map-host>:not(.sl-confirm){pointer-events:none}
732
+ .sl-picker[data-confirming="true"] .sl-anchor{pointer-events:none;opacity:.28;transition:opacity .16s}
733
+ .sl-picker[data-confirming="true"] .sl-side{pointer-events:none;opacity:.58;transition:opacity .16s}
734
+ .sl-confirm{position:absolute;z-index:10;width:276px;max-width:calc(100% - 24px);overflow:hidden;pointer-events:auto;
735
+ background:var(--sl-surface);border:1px solid color-mix(in srgb,var(--sl-line) 70%,var(--sl-text));
736
+ border-radius:15px;box-shadow:0 24px 64px -18px rgba(0,0,0,.72);transform:translate(-50%,calc(-100% - 16px));
692
737
  animation:slConfirmIn .24s cubic-bezier(.2,.8,.2,1) both}
693
- .sl-confirm-label{font-weight:800;font-size:14px}
694
- .sl-confirm-meta{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--sl-muted);margin-top:4px}
695
- .sl-confirm-meta b{color:var(--sl-text);margin-left:auto}
738
+ .sl-confirm[data-placement="below"]{transform:translate(-50%,16px);animation:slConfirmBelowIn .24s cubic-bezier(.2,.8,.2,1) both}
739
+ .sl-confirm-grid{display:grid;grid-template-columns:1.2fr .8fr .8fr;border-bottom:1px solid var(--sl-line)}
740
+ .sl-confirm-field{min-width:0;padding:12px 11px 10px;border-right:1px solid var(--sl-line)}
741
+ .sl-confirm-field:last-child{border-right:0;text-align:center}
742
+ .sl-confirm-field:nth-child(2){text-align:center}
743
+ .sl-confirm-key{display:block;font-size:8.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
744
+ .sl-confirm-value{display:block;margin-top:4px;color:var(--sl-text);font-size:17px;line-height:1.1;font-weight:850;
745
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
746
+ .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))}
747
+ .sl-confirm-cat .sl-dot{border:2px solid rgba(255,255,255,.78);width:11px;height:11px}
748
+ .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}
749
+ .sl-confirm-price{font-size:17px;font-weight:850;color:#fff;font-variant-numeric:tabular-nums}
750
+ .sl-confirm-body{padding:11px 12px 12px}
696
751
  .sl-confirm-row{display:flex;gap:8px;margin-top:10px}
697
- .sl-confirm-row button{flex:1;padding:8px;border-radius:8px;font-weight:700;font-size:12.5px}
698
- .sl-confirm-add{background:var(--sl-accent);color:var(--sl-accent-ink)}
699
- .sl-confirm-cancel{border:1px solid var(--sl-line);color:var(--sl-muted)}
752
+ .sl-confirm-row button{flex:1;min-height:44px;padding:9px 12px;border-radius:9px;font-weight:800;font-size:13px}
753
+ .sl-confirm-add{background:var(--sl-accent)!important;color:var(--sl-accent-ink)!important;display:flex;align-items:center;justify-content:center;gap:7px}
754
+ .sl-confirm-add svg{width:16px;height:16px;stroke:currentColor;stroke-width:2.8;fill:none;stroke-linecap:round;stroke-linejoin:round}
755
+ .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}
700
756
  .sl-confirm-cancel:hover{color:var(--sl-text)}
757
+ .sl-picker[data-layout="narrow"] .sl-confirm{left:50%!important;top:auto!important;bottom:14px;width:min(342px,calc(100% - 24px));
758
+ transform:translateX(-50%);animation:slConfirmMobileIn .24s cubic-bezier(.2,.8,.2,1) both}
701
759
 
702
760
  /* best-available row */
703
- .sl-ba{display:flex;align-items:center;gap:8px;padding:9px 11px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm)}
761
+ .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)}
704
762
  .sl-ba select{background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:7px;
705
- font:inherit;font-size:12px;padding:5px 6px;max-width:110px}
763
+ font:inherit;font-size:12px;padding:7px 8px;min-width:0;width:100%;max-width:none}
706
764
  .sl-ba-qty{display:flex;align-items:center;gap:7px}
707
765
  .sl-ba-qty button{width:24px;height:24px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
708
766
  font-size:14px;font-weight:700;display:flex;align-items:center;justify-content:center}
709
767
  .sl-ba-qty span{min-width:14px;text-align:center;font-weight:800}
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}
768
+ .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;
769
+ transition:border-color .15s,opacity .15s;display:flex;align-items:center;justify-content:center;gap:6px}
712
770
  .sl-ba-go:hover{border-color:var(--sl-muted)}
713
771
  .sl-ba-go:disabled{opacity:.62;cursor:wait}
714
772
 
@@ -831,6 +889,8 @@ var CSS = `
831
889
  @keyframes slCheckDraw{to{stroke-dashoffset:0}}
832
890
  @keyframes slCopyRise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
833
891
  @keyframes slConfirmIn{from{opacity:0;transform:translate(-50%,calc(-100% - 8px)) scale(.96)}to{opacity:1;transform:translate(-50%,calc(-100% - 14px)) scale(1)}}
892
+ @keyframes slConfirmBelowIn{from{opacity:0;transform:translate(-50%,8px) scale(.96)}to{opacity:1;transform:translate(-50%,16px) scale(1)}}
893
+ @keyframes slConfirmMobileIn{from{opacity:0;transform:translate(-50%,10px) scale(.97)}to{opacity:1;transform:translate(-50%,0) scale(1)}}
834
894
 
835
895
  @media(prefers-reduced-motion:reduce){
836
896
  .sl-picker *,.sl-modal-scrim *{animation-duration:.001ms!important;animation-iteration-count:1!important;
@@ -927,6 +987,9 @@ var SeatPicker = class _SeatPicker {
927
987
  /** Stable item keys prevent tray chips re-animating on unrelated realtime syncs. */
928
988
  this.lastTrayKeys = /* @__PURE__ */ new Set();
929
989
  this.bestAvailableBusy = false;
990
+ this.releasingLabels = /* @__PURE__ */ new Set();
991
+ /** Selected labels awaiting the hold response; their own realtime echo can arrive first. */
992
+ this.holdingLabels = /* @__PURE__ */ new Set();
930
993
  this.ctaPhase = "idle";
931
994
  // narrow-layout chrome that docks into the sheet's Filters row on mobile
932
995
  this.a11yChipsEl = null;
@@ -940,8 +1003,9 @@ var SeatPicker = class _SeatPicker {
940
1003
  if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
941
1004
  if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
942
1005
  if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
943
- this.opts = options;
944
- this.api = new PubApi((options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, ""));
1006
+ this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
1007
+ this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
1008
+ this.api = new PubApi(this.apiBase);
945
1009
  this.controller = new import_core2.PickerController({
946
1010
  transport: this.api,
947
1011
  eventKey: options.event,
@@ -951,7 +1015,7 @@ var SeatPicker = class _SeatPicker {
951
1015
  colorblindSafe: options.colorblindSafe,
952
1016
  onSelectionChange: () => {
953
1017
  this.syncTray();
954
- if (this.controller.getSelection().length) this.collapseSectionCard();
1018
+ if (this.committedSelection().length) this.collapseSectionCard();
955
1019
  },
956
1020
  onStatusChange: () => {
957
1021
  this.syncPrices();
@@ -961,6 +1025,7 @@ var SeatPicker = class _SeatPicker {
961
1025
  },
962
1026
  onHoldExpired: () => {
963
1027
  this.hold = null;
1028
+ this.forgetHold();
964
1029
  this.handedOff = false;
965
1030
  this.bookedShown = false;
966
1031
  this.ctaPhase = "idle";
@@ -968,13 +1033,17 @@ var SeatPicker = class _SeatPicker {
968
1033
  this.gaQty.clear();
969
1034
  this.toast((0, import_core2.t)("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.", "warning");
970
1035
  this.syncTray();
1036
+ this.emitHoldChange();
971
1037
  this.opts.onHoldExpired?.();
972
1038
  },
973
- confirmSelection: options.confirmSelection,
1039
+ confirmSelection: this.opts.confirmSelection,
974
1040
  onSelect: (seat) => {
975
1041
  this.flashPickedSeat(seat.id);
976
1042
  if (this.opts.confirmSelection) this.showConfirm(seat);
977
1043
  },
1044
+ onDeselect: (seat) => {
1045
+ if (this.confirmSeat?.id === seat.id) this.dismissConfirm();
1046
+ },
978
1047
  onViewChange: () => {
979
1048
  this.reanchorConfirm();
980
1049
  this.syncRung();
@@ -1033,7 +1102,13 @@ var SeatPicker = class _SeatPicker {
1033
1102
  if (e.target === scrim) close();
1034
1103
  });
1035
1104
  picker.escHandler = (e) => {
1036
- if (e.key === "Escape") close();
1105
+ if (e.key !== "Escape") return;
1106
+ if (picker.confirmSeat) {
1107
+ e.preventDefault();
1108
+ picker.cancelConfirm();
1109
+ } else {
1110
+ close();
1111
+ }
1037
1112
  };
1038
1113
  document.addEventListener("keydown", picker.escHandler);
1039
1114
  await picker.render();
@@ -1050,8 +1125,15 @@ var SeatPicker = class _SeatPicker {
1050
1125
  const mount = resolveContainer3(this.opts.container);
1051
1126
  const root = document.createElement("div");
1052
1127
  root.className = "sl-picker";
1128
+ root.tabIndex = -1;
1053
1129
  this.root = root;
1054
1130
  mount.appendChild(root);
1131
+ root.addEventListener("keydown", (e) => {
1132
+ if (e.key !== "Escape" || !this.confirmSeat) return;
1133
+ e.preventDefault();
1134
+ e.stopPropagation();
1135
+ this.cancelConfirm();
1136
+ });
1055
1137
  Object.entries(resolveTokens(void 0, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
1056
1138
  root.innerHTML = `
1057
1139
  <div class="sl-head">
@@ -1090,9 +1172,9 @@ var SeatPicker = class _SeatPicker {
1090
1172
  </div>
1091
1173
  <div class="sl-sec sl-filtersec" data-ref="filtersSec">Filters</div>
1092
1174
  <div class="sl-filters" data-ref="filters"></div>
1093
- <div class="sl-sec" data-ref="pricesSec">Prices</div>
1175
+ <div class="sl-sec sl-prices-sec" data-ref="pricesSec">Prices</div>
1094
1176
  <div class="sl-prices" data-ref="prices"></div>
1095
- <div class="sl-sec">Your seats</div>
1177
+ <div class="sl-sec sl-seats-sec">Your seats</div>
1096
1178
  <div class="sl-tray" data-ref="tray"></div>
1097
1179
  <div class="sl-foot">
1098
1180
  <div class="sl-hold-note" data-ref="holdNote" role="status" aria-live="polite">
@@ -1250,6 +1332,8 @@ var SeatPicker = class _SeatPicker {
1250
1332
  this.buildExtendPrompt();
1251
1333
  this.buildBookedOverlay();
1252
1334
  this.dockLayoutChrome();
1335
+ await this.restoreRememberedHold();
1336
+ if (this.destroyed) return this;
1253
1337
  this.syncPrices();
1254
1338
  this.syncTray();
1255
1339
  return this;
@@ -1363,10 +1447,14 @@ var SeatPicker = class _SeatPicker {
1363
1447
  });
1364
1448
  }
1365
1449
  /** Update only the action affordance; selection callbacks must not refire. */
1450
+ committedSelection() {
1451
+ const candidateId = this.confirmSeat?.id;
1452
+ return this.controller.getSelection().filter((seat) => seat.id !== candidateId);
1453
+ }
1366
1454
  pendingSelectionCount() {
1367
1455
  const heldItems = this.hold?.items ?? [];
1368
1456
  const heldLabels = new Set(heldItems.map((item) => item.label));
1369
- const pendingSeats = this.controller.getSelection().filter((seat) => !heldLabels.has(seat.label)).length;
1457
+ const pendingSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).length;
1370
1458
  const heldGA = /* @__PURE__ */ new Map();
1371
1459
  for (const item of heldItems.filter((candidate) => candidate.objectType === "ga")) {
1372
1460
  heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
@@ -1380,6 +1468,11 @@ var SeatPicker = class _SeatPicker {
1380
1468
  syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
1381
1469
  const cta = this.els.cta;
1382
1470
  if (!cta) return;
1471
+ if (this.confirmSeat) {
1472
+ cta.disabled = true;
1473
+ cta.textContent = "Confirm or cancel this seat";
1474
+ return;
1475
+ }
1383
1476
  if (this.ctaPhase === "holding") {
1384
1477
  cta.disabled = true;
1385
1478
  cta.innerHTML = '<span class="sl-cta-spin" aria-hidden="true"></span>Securing seats\u2026';
@@ -1404,6 +1497,68 @@ var SeatPicker = class _SeatPicker {
1404
1497
  }, 1100);
1405
1498
  }
1406
1499
  }
1500
+ /** Session-scoped capability key: isolated by API origin and event. */
1501
+ holdStorageKey() {
1502
+ return `@seatlayer/hold/v1/${encodeURIComponent(this.apiBase)}/${encodeURIComponent(this.opts.event)}`;
1503
+ }
1504
+ rememberedHoldId() {
1505
+ if (this.opts.initialHoldId) return this.opts.initialHoldId;
1506
+ if (this.opts.restoreHold === false || typeof window === "undefined") return null;
1507
+ try {
1508
+ return window.sessionStorage.getItem(this.holdStorageKey());
1509
+ } catch {
1510
+ return null;
1511
+ }
1512
+ }
1513
+ rememberHold(hold) {
1514
+ if (this.opts.restoreHold === false || typeof window === "undefined") return;
1515
+ try {
1516
+ window.sessionStorage.setItem(this.holdStorageKey(), hold.holdId);
1517
+ } catch {
1518
+ }
1519
+ }
1520
+ forgetHold() {
1521
+ if (typeof window === "undefined") return;
1522
+ try {
1523
+ window.sessionStorage.removeItem(this.holdStorageKey());
1524
+ } catch {
1525
+ }
1526
+ }
1527
+ async resumeHoldFromServer(holdId, automatic) {
1528
+ try {
1529
+ const h = await this.controller.resumeHold(holdId);
1530
+ if (!h) return null;
1531
+ const restored = {
1532
+ holdId: h.holdId,
1533
+ expiresAt: h.expiresAt,
1534
+ seats: h.seats,
1535
+ items: h.items
1536
+ };
1537
+ this.hold = restored;
1538
+ this.handedOff = true;
1539
+ this.bookedShown = false;
1540
+ this.ctaPhase = "idle";
1541
+ this.startHoldTimer(restored.expiresAt);
1542
+ this.rememberHold(restored);
1543
+ this.syncTray();
1544
+ this.emitHoldChange();
1545
+ this.opts.onHoldRestored?.(restored, restored.seats ?? [], this.buildHandoff(restored));
1546
+ if (automatic) this.toast("Your held tickets have been restored.", "success");
1547
+ return restored;
1548
+ } catch (error) {
1549
+ const status = error?.status;
1550
+ if (status === 404 || status === 409) {
1551
+ this.forgetHold();
1552
+ } else {
1553
+ this.opts.onError?.(error);
1554
+ }
1555
+ return null;
1556
+ }
1557
+ }
1558
+ async restoreRememberedHold() {
1559
+ const holdId = this.rememberedHoldId();
1560
+ if (holdId) await this.resumeHoldFromServer(holdId, true);
1561
+ }
1407
1562
  /** Section-bearing objects on the active floor (single-floor → doc.objects). */
1408
1563
  activeFloorObjects() {
1409
1564
  const doc = this.controller.doc;
@@ -1825,36 +1980,78 @@ var SeatPicker = class _SeatPicker {
1825
1980
  const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
1826
1981
  this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
1827
1982
  }
1828
- // ---- confirm popover (opt-in confirmSelection mode) ------------------------
1983
+ // ---- seat candidate confirmation ------------------------------------------
1829
1984
  showConfirm(seat) {
1830
- this.closeConfirm();
1985
+ const previousId = this.confirmSeat?.id;
1986
+ this.confirmEl?.remove();
1987
+ this.confirmEl = null;
1988
+ this.confirmSeat = seat;
1989
+ this.root?.setAttribute("data-confirming", "true");
1990
+ this.controller.setSelectionFocus(seat.id);
1991
+ if (previousId && previousId !== seat.id) this.controller.deselect([previousId]);
1831
1992
  if (this.tipEl) this.tipEl.style.display = "none";
1993
+ const details = this.controller.seatDetails(seat.id);
1832
1994
  const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
1833
- const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
1995
+ const price = details?.price ?? (cat?.tiers?.length ? cat.tiers[0].price : cat?.price);
1996
+ const safe = (value) => String(value ?? "\u2014").replace(/[&<>"]/g, (char) => ({
1997
+ "&": "&amp;",
1998
+ "<": "&lt;",
1999
+ ">": "&gt;",
2000
+ '"': "&quot;"
2001
+ })[char]);
1834
2002
  const el = document.createElement("div");
1835
2003
  el.className = "sl-confirm";
1836
- 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>${(0, import_core2.t)("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>`;
2004
+ el.setAttribute("role", "dialog");
2005
+ el.setAttribute("aria-modal", "true");
2006
+ el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
2007
+ el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
2008
+ 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>${(0, import_core2.t)("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>`;
1837
2009
  this.els.map.appendChild(el);
1838
2010
  this.confirmEl = el;
1839
- this.confirmSeat = seat;
1840
2011
  this.reanchorConfirm();
1841
2012
  el.querySelector(".sl-confirm-view")?.addEventListener("click", () => this.openSeatView(seat));
1842
- el.querySelector(".sl-confirm-add").addEventListener("click", () => this.closeConfirm());
1843
- el.querySelector(".sl-confirm-cancel").addEventListener("click", () => {
1844
- this.controller.deselect([seat.id]);
1845
- this.closeConfirm();
1846
- });
2013
+ el.querySelector(".sl-confirm-add").addEventListener("click", () => this.commitConfirm());
2014
+ el.querySelector(".sl-confirm-cancel").addEventListener("click", () => this.cancelConfirm());
2015
+ requestAnimationFrame(() => el.querySelector(".sl-confirm-add")?.focus());
1847
2016
  }
1848
2017
  reanchorConfirm() {
1849
2018
  if (!this.confirmEl || !this.confirmSeat) return;
1850
2019
  const p = this.controller.worldToScreen({ x: this.confirmSeat.x, y: this.confirmSeat.y });
1851
- this.confirmEl.style.left = `${p.x}px`;
1852
- this.confirmEl.style.top = `${p.y}px`;
1853
- }
1854
- closeConfirm() {
2020
+ if (this.root?.dataset.layout === "narrow") return;
2021
+ const mapWidth = this.els.map.clientWidth;
2022
+ const mapHeight = this.els.map.clientHeight;
2023
+ const cardWidth = this.confirmEl.offsetWidth || 276;
2024
+ const cardHeight = this.confirmEl.offsetHeight || 230;
2025
+ const half = cardWidth / 2 + 12;
2026
+ const x = Math.max(half, Math.min(mapWidth - half, p.x));
2027
+ const belowFits = p.y + cardHeight + 24 <= mapHeight;
2028
+ const placeBelow = p.y < cardHeight + 24 && belowFits;
2029
+ this.confirmEl.dataset.placement = placeBelow ? "below" : "above";
2030
+ this.confirmEl.style.left = `${x}px`;
2031
+ this.confirmEl.style.top = `${Math.max(8, Math.min(mapHeight - 8, p.y))}px`;
2032
+ }
2033
+ dismissConfirm() {
1855
2034
  this.confirmEl?.remove();
1856
2035
  this.confirmEl = null;
1857
2036
  this.confirmSeat = null;
2037
+ this.root?.removeAttribute("data-confirming");
2038
+ this.controller.setSelectionFocus(null);
2039
+ }
2040
+ commitConfirm() {
2041
+ if (!this.confirmSeat) return;
2042
+ this.dismissConfirm();
2043
+ this.collapseSectionCard();
2044
+ this.syncTray();
2045
+ }
2046
+ cancelConfirm() {
2047
+ const seat = this.confirmSeat;
2048
+ if (!seat) return;
2049
+ this.controller.deselect([seat.id]);
2050
+ if (this.confirmSeat) this.dismissConfirm();
2051
+ this.root?.focus({ preventScroll: true });
2052
+ }
2053
+ closeConfirm() {
2054
+ this.dismissConfirm();
1858
2055
  }
1859
2056
  // ---- 360° view-from-seat modal --------------------------------------------
1860
2057
  seatViewEnabled() {
@@ -1877,7 +2074,6 @@ var SeatPicker = class _SeatPicker {
1877
2074
  openSeatView(seat) {
1878
2075
  if (!this.root || !this.seatViewEnabled()) return;
1879
2076
  this.closeSeatView();
1880
- this.closeConfirm();
1881
2077
  const doc = this.controller.doc;
1882
2078
  const activeId = this.controller.getActiveFloorId();
1883
2079
  const focal = doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
@@ -1988,7 +2184,7 @@ var SeatPicker = class _SeatPicker {
1988
2184
  const price = c.tiers?.length ? c.tiers[0].price : c.price;
1989
2185
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
1990
2186
  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>`;
1991
- }).join("");
2187
+ }).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>`;
1992
2188
  this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
1993
2189
  row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
1994
2190
  row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
@@ -1996,7 +2192,10 @@ var SeatPicker = class _SeatPicker {
1996
2192
  }
1997
2193
  /** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
1998
2194
  evictTakenSelections() {
1999
- const ownLabels = new Set(this.controller.currentHold()?.labels ?? []);
2195
+ const ownLabels = /* @__PURE__ */ new Set([
2196
+ ...this.controller.currentHold()?.labels ?? [],
2197
+ ...this.holdingLabels
2198
+ ]);
2000
2199
  const gone = this.controller.getSelection().filter((s) => !ownLabels.has(s.label) && (this.controller.getStatus(s.id) ?? "free") !== "free");
2001
2200
  if (!gone.length) return;
2002
2201
  this.controller.deselect(gone.map((s) => s.id));
@@ -2004,7 +2203,7 @@ var SeatPicker = class _SeatPicker {
2004
2203
  }
2005
2204
  syncTray() {
2006
2205
  if (!this.els.tray) return;
2007
- const seats = this.controller.getSelection();
2206
+ const seats = this.committedSelection();
2008
2207
  const gaAreas = this.controller.getGAAreas();
2009
2208
  const heldItems = this.hold?.items ?? [];
2010
2209
  const parts = [];
@@ -2021,7 +2220,7 @@ var SeatPicker = class _SeatPicker {
2021
2220
  const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
2022
2221
  const canView2 = this.seatViewEnabled() && item.objectType !== "ga" && !!this.controller.seatByLabel(item.label);
2023
2222
  parts.push(
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>`
2223
+ `<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="${(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>` : "") + `<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>`
2025
2224
  );
2026
2225
  }
2027
2226
  const heldLabels = new Set(heldItems.map((item) => item.label));
@@ -2065,6 +2264,10 @@ var SeatPicker = class _SeatPicker {
2065
2264
  this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
2066
2265
  btn.addEventListener("click", () => {
2067
2266
  const chip = btn.closest(".sl-chip");
2267
+ if (chip.dataset.held) {
2268
+ void this.removeHeldLabel(decodeURIComponent(chip.dataset.held), chip);
2269
+ return;
2270
+ }
2068
2271
  const id = chip.dataset.seat;
2069
2272
  if (this.reducedMotion()) {
2070
2273
  this.controller.deselect([id]);
@@ -2126,26 +2329,59 @@ var SeatPicker = class _SeatPicker {
2126
2329
  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>`;
2127
2330
  }
2128
2331
  }
2129
- if (this.root?.dataset.layout === "narrow" && count > 0 && this.lastTrayCount === 0) {
2130
- this.root.dataset.sheet = "open";
2131
- }
2132
2332
  this.lastTrayCount = count;
2133
2333
  this.lastTrayTotal = total;
2134
2334
  this.opts.onSelectionChange?.(seats);
2135
2335
  }
2336
+ async removeHeldLabel(label, chip) {
2337
+ if (!label || this.releasingLabels.has(label)) return false;
2338
+ this.releasingLabels.add(label);
2339
+ chip?.setAttribute("aria-busy", "true");
2340
+ const button = chip?.querySelector(".rm");
2341
+ if (button) button.disabled = true;
2342
+ try {
2343
+ const preserveAcrossNavigation = this.handedOff;
2344
+ const released = await this.controller.releaseLabels([label]);
2345
+ if (!released) {
2346
+ this.toast(`Couldn't remove ${label}. Your hold is unchanged.`, "error");
2347
+ return false;
2348
+ }
2349
+ const remaining = this.controller.currentHold();
2350
+ this.hold = remaining ? { holdId: remaining.holdId, expiresAt: remaining.expiresAt, seats: remaining.seats, items: remaining.items } : null;
2351
+ this.handedOff = !!this.hold && preserveAcrossNavigation;
2352
+ this.bookedShown = false;
2353
+ this.ctaPhase = "idle";
2354
+ if (this.hold) {
2355
+ this.startHoldTimer(this.hold.expiresAt);
2356
+ } else {
2357
+ this.stopHoldTimer();
2358
+ this.forgetHold();
2359
+ }
2360
+ this.syncTray();
2361
+ this.emitHoldChange();
2362
+ this.toast(`${label} removed from your hold.`, "success");
2363
+ return true;
2364
+ } finally {
2365
+ this.releasingLabels.delete(label);
2366
+ chip?.removeAttribute("aria-busy");
2367
+ if (button?.isConnected) button.disabled = false;
2368
+ }
2369
+ }
2136
2370
  async handleCta() {
2137
- if (this.hold && !this.controller.getSelection().some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
2138
- const seats = this.hold.seats ?? this.controller.getSelection();
2371
+ const committed = this.committedSelection();
2372
+ if (this.hold && !committed.some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
2373
+ const seats = this.hold.seats ?? committed;
2139
2374
  this.handedOff = true;
2140
2375
  this.setCtaPhase("checkout");
2141
2376
  this.opts.onCheckout?.(this.hold, seats, this.buildHandoff(this.hold));
2142
2377
  return;
2143
2378
  }
2379
+ this.holdingLabels = new Set(committed.map((seat) => seat.label));
2144
2380
  this.setCtaPhase("holding");
2145
2381
  try {
2146
2382
  let hold = null;
2147
2383
  const gaEntries = [...this.gaQty.entries()].filter(([, q]) => q > 0);
2148
- const chosenSeats = this.controller.getSelection();
2384
+ const chosenSeats = this.committedSelection();
2149
2385
  if (chosenSeats.length) {
2150
2386
  const h = await this.controller.hold(void 0, this.opts.holdTtlMs);
2151
2387
  hold = h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
@@ -2165,12 +2401,14 @@ var SeatPicker = class _SeatPicker {
2165
2401
  this.startHoldTimer(hold.expiresAt);
2166
2402
  this.flashHeldSeats(hold);
2167
2403
  this.setCtaPhase("checkout");
2404
+ this.emitHoldChange();
2168
2405
  this.opts.onCheckout?.(hold, hold.seats ?? chosenSeats, this.buildHandoff(hold));
2169
2406
  } catch (err) {
2170
2407
  this.opts.onError?.(err);
2171
2408
  this.toast("One or more seats were just taken. Please pick again.", "error");
2172
2409
  this.setCtaPhase("idle");
2173
2410
  } finally {
2411
+ this.holdingLabels.clear();
2174
2412
  if (this.ctaPhase === "holding") this.ctaPhase = "idle";
2175
2413
  this.syncTray();
2176
2414
  }
@@ -2178,6 +2416,7 @@ var SeatPicker = class _SeatPicker {
2178
2416
  startHoldTimer(expiresAt) {
2179
2417
  this.stopHoldTimer();
2180
2418
  this.holdExpiresAt = expiresAt;
2419
+ if (this.hold) this.rememberHold(this.hold);
2181
2420
  const pill = this.els.hold;
2182
2421
  pill.innerHTML = '<span class="sl-hold-dot" aria-hidden="true"></span><span>Held</span><span class="sl-hold-time" data-ref="holdTime"></span>';
2183
2422
  const time = pill.querySelector('[data-ref="holdTime"]');
@@ -2224,6 +2463,8 @@ var SeatPicker = class _SeatPicker {
2224
2463
  this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
2225
2464
  this.holdExpiresAt = h.expiresAt;
2226
2465
  this.extendEl?.classList.remove("on");
2466
+ this.rememberHold(this.hold);
2467
+ this.emitHoldChange();
2227
2468
  this.toast("More time added \u2014 your seats are still held.", "success");
2228
2469
  } else {
2229
2470
  this.toast("Couldn't add more time \u2014 please head to checkout now.", "warning");
@@ -2254,6 +2495,7 @@ var SeatPicker = class _SeatPicker {
2254
2495
  this.bookedShown = true;
2255
2496
  const handoff = this.buildHandoff(this.hold);
2256
2497
  this.stopHoldTimer();
2498
+ this.forgetHold();
2257
2499
  const n = handoff.lineItems.reduce((sum, i) => sum + i.quantity, 0);
2258
2500
  if (this.els.bookedSub) {
2259
2501
  this.els.bookedSub.innerHTML = `<span class="sl-booked-seats">${n} ${n === 1 ? "ticket" : "tickets"}</span> confirmed. A confirmation is on its way.`;
@@ -2278,6 +2520,14 @@ var SeatPicker = class _SeatPicker {
2278
2520
  const total = lineItems.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
2279
2521
  return { holdId: hold.holdId, expiresAt: hold.expiresAt, currency, lineItems, total };
2280
2522
  }
2523
+ emitHoldChange() {
2524
+ const hold = this.hold;
2525
+ this.opts.onHoldChange?.(
2526
+ hold,
2527
+ hold?.seats ?? [],
2528
+ hold ? this.buildHandoff(hold) : null
2529
+ );
2530
+ }
2281
2531
  toast(msg, tone = "neutral") {
2282
2532
  const el = this.els.toast;
2283
2533
  if (!el) return;
@@ -2309,16 +2559,34 @@ var SeatPicker = class _SeatPicker {
2309
2559
  return;
2310
2560
  }
2311
2561
  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" ? (0, import_core2.t)("map.statusHeld") : (0, import_core2.t)("map.statusTaken")}</div>`;
2312
- 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;
2562
+ const location = [
2563
+ details.sectionLabel ? `Section ${details.sectionLabel}` : "",
2564
+ details.rowLabel ? `Row ${details.rowLabel}` : "",
2565
+ details.seatNumber ? `Seat ${details.seatNumber}` : details.label
2566
+ ].filter(Boolean).join(" \xB7 ");
2567
+ 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;
2313
2568
  this.tipEl.style.display = "block";
2314
2569
  this.placeTooltip();
2315
2570
  }
2316
2571
  // ---- public conveniences ----------------------------------------------------
2317
2572
  getSelection() {
2318
- return this.controller.getSelection();
2573
+ return this.committedSelection();
2574
+ }
2575
+ /** Current active/restored hold reflected in the tray. */
2576
+ getCurrentHold() {
2577
+ return this.hold;
2578
+ }
2579
+ /** Explicit host-driven hold restore (automatic session restore is on by default). */
2580
+ async resumeHold(holdId) {
2581
+ return this.resumeHoldFromServer(holdId, false);
2582
+ }
2583
+ /** Remove one server-held ticket while keeping the rest of the hold active. */
2584
+ async removeHeldTicket(label) {
2585
+ return this.removeHeldLabel(label);
2319
2586
  }
2320
2587
  async bestAvailable(qty, categoryKey) {
2321
2588
  if (this.bestAvailableBusy) return null;
2589
+ if (this.confirmSeat) this.cancelConfirm();
2322
2590
  this.bestAvailableBusy = true;
2323
2591
  const button = this.els.tray?.querySelector(".sl-ba-go");
2324
2592
  if (button) {
@@ -2334,6 +2602,7 @@ var SeatPicker = class _SeatPicker {
2334
2602
  this.startHoldTimer(h.expiresAt);
2335
2603
  this.flashHeldSeats(this.hold);
2336
2604
  this.syncTray();
2605
+ this.emitHoldChange();
2337
2606
  return this.hold;
2338
2607
  }
2339
2608
  return null;
@@ -2352,8 +2621,9 @@ var SeatPicker = class _SeatPicker {
2352
2621
  async release() {
2353
2622
  const tracked = this.hold;
2354
2623
  const controllerHold = this.controller.currentHold();
2624
+ let released = true;
2355
2625
  if (controllerHold) {
2356
- await this.controller.release();
2626
+ released = await this.controller.release();
2357
2627
  } else if (tracked) {
2358
2628
  const labels = [.../* @__PURE__ */ new Set([
2359
2629
  ...(tracked.items ?? []).map((item) => item.label),
@@ -2364,16 +2634,23 @@ var SeatPicker = class _SeatPicker {
2364
2634
  await this.api.release(this.opts.event, labels, tracked.holdId);
2365
2635
  } catch (error) {
2366
2636
  this.opts.onError?.(error);
2637
+ released = false;
2367
2638
  }
2368
2639
  }
2369
2640
  }
2641
+ if (!released) {
2642
+ this.toast("Couldn't release your tickets. Your hold is unchanged.", "error");
2643
+ return;
2644
+ }
2370
2645
  this.hold = null;
2646
+ this.forgetHold();
2371
2647
  this.handedOff = false;
2372
2648
  this.bookedShown = false;
2373
2649
  this.ctaPhase = "idle";
2374
2650
  this.stopHoldTimer();
2375
2651
  this.gaQty.clear();
2376
2652
  this.syncTray();
2653
+ this.emitHoldChange();
2377
2654
  }
2378
2655
  destroy() {
2379
2656
  this.destroyed = true;