@seatlayer/js 0.16.0 → 0.16.1

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
@@ -500,7 +500,7 @@ var CSS = `
500
500
  .sl-body{display:flex;flex:1;min-height:0}
501
501
  .sl-map{position:relative;flex:1;min-width:0}
502
502
  .sl-map-host{position:absolute;inset:0}
503
- .sl-side{width:300px;flex:none;border-left:1px solid var(--sl-line);display:flex;flex-direction:column;min-height:0;overflow-y:auto}
503
+ .sl-side{width:300px;flex:none;border-left:1px solid var(--sl-line);display:flex;flex-direction:column;min-height:0;overflow:hidden}
504
504
 
505
505
  /* narrow (container < 640px): map-first \u2014 the map claims ~80-85% of the
506
506
  container and the side panel becomes a PEEKING bottom sheet (AXS/Ticketmaster
@@ -583,6 +583,7 @@ var CSS = `
583
583
  .sl-price-select{min-height:32px;max-width:130px;padding:5px 28px 5px 9px;border:1px solid var(--sl-line);border-radius:9px;
584
584
  background:var(--sl-surface);color:var(--sl-text);font:inherit;font-size:11px;font-weight:750;letter-spacing:0;text-transform:none}
585
585
  .sl-prices{padding:5px 14px 10px;border-bottom:1px solid var(--sl-line)}
586
+ .sl-prices-sec,.sl-prices,.sl-seats-sec{flex:none}
586
587
  .sl-price-row{display:flex;align-items:center;gap:7px;min-height:28px;font-size:12px}
587
588
  .sl-dot{width:9px;height:9px;border-radius:50%;flex:none}
588
589
  .sl-price-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
@@ -599,7 +600,8 @@ var CSS = `
599
600
  /* tray */
600
601
  .sl-seats-sec{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:13px}
601
602
  .sl-seat-summary{font-size:10px;letter-spacing:0;text-transform:none;white-space:nowrap}
602
- .sl-tray{flex:1;padding:10px 14px;display:flex;flex-direction:column;gap:7px;min-height:0}
603
+ .sl-tray{flex:1;padding:10px 14px 14px;display:flex;flex-direction:column;gap:7px;min-height:0;overflow-y:auto;
604
+ overscroll-behavior:contain;scrollbar-gutter:stable}
603
605
  .sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
604
606
  .sl-chip{position:relative;display:grid;grid-template-columns:24px minmax(0,1fr) auto 30px;align-items:center;gap:8px;
605
607
  min-height:53px;padding:7px 7px 7px 9px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm);
@@ -634,16 +636,22 @@ var CSS = `
634
636
  .sl-ga-qty span{min-width:16px;text-align:center;font-weight:800;font-variant-numeric:tabular-nums}
635
637
 
636
638
  /* footer */
637
- .sl-foot{padding:12px 16px 14px;border-top:1px solid var(--sl-line);flex:none}
638
- .sl-hold-note{display:none;align-items:center;gap:9px;margin-bottom:10px;padding:9px 10px;border-radius:var(--sl-r-sm);
639
+ .sl-foot{position:relative;z-index:2;padding:12px 16px 14px;border-top:1px solid var(--sl-line);flex:none;
640
+ background:var(--sl-bg);box-shadow:0 -10px 24px -22px rgba(0,0,0,.72)}
641
+ .sl-hold-note{display:none;align-items:center;gap:7px;margin-bottom:8px;padding:7px 8px;border-radius:var(--sl-r-sm);
639
642
  border:1px solid var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
640
643
  box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent);
641
644
  font-size:11.5px;line-height:1.35;color:var(--sl-muted)}
642
645
  .sl-hold-note.on{display:flex;animation:slNoticeIn .38s cubic-bezier(.2,.8,.2,1) both}
643
646
  .sl-hold-note svg{width:16px;height:16px;flex:none;stroke:var(--sl-accent);stroke-width:2.4;fill:none;
644
647
  stroke-linecap:round;stroke-linejoin:round}
645
- .sl-hold-note b{display:block;color:var(--sl-text);font-size:12px}
646
- .sl-hold-copy{display:block}
648
+ .sl-hold-note b{display:block;color:var(--sl-text);font-size:11.5px;white-space:nowrap}
649
+ .sl-hold-copy{display:block;white-space:nowrap;font-size:10.5px}
650
+ .sl-hold-note>span{flex:1;min-width:0}
651
+ .sl-hold-change{flex:none;min-height:30px;padding:5px 8px;border-radius:8px;border:1px solid var(--sl-line);
652
+ color:var(--sl-text);font-size:10.5px;font-weight:750;white-space:nowrap}
653
+ .sl-hold-change:hover,.sl-hold-change:focus-visible{border-color:var(--sl-accent);color:var(--sl-accent)}
654
+ .sl-hold-change:disabled{opacity:.58;cursor:wait}
647
655
  .sl-total{display:flex;justify-content:space-between;align-items:center;font-size:13px;margin-bottom:10px}
648
656
  .sl-total b{font-size:17px;font-variant-numeric:tabular-nums}
649
657
  .sl-value-pop{animation:slValuePop .32s cubic-bezier(.2,.8,.2,1)}
@@ -701,6 +709,9 @@ var CSS = `
701
709
  font-size:12.5px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .22s,transform .22s;white-space:nowrap;
702
710
  overflow:hidden;text-overflow:ellipsis}
703
711
  .sl-toast.on{opacity:1;transform:translateY(0) scale(1)}
712
+ .sl-toast.has-action{pointer-events:auto;display:flex;align-items:center;gap:12px;padding-right:8px}
713
+ .sl-toast-action{min-height:30px;padding:5px 10px;border-radius:999px;background:var(--sl-accent);color:var(--sl-accent-ink);
714
+ font:inherit;font-weight:800}
704
715
  .sl-toast[data-tone="error"]{border-color:#ef4444}
705
716
  .sl-toast[data-tone="warning"]{border-color:var(--sl-accent)}
706
717
  .sl-toast[data-tone="success"]{border-color:#22c55e}
@@ -804,6 +815,12 @@ var CSS = `
804
815
  box-shadow:0 8px 18px color-mix(in srgb,var(--sl-accent) 18%,transparent)}
805
816
  .sl-picker .sl-ba-go:hover{filter:brightness(1.06)}
806
817
  .sl-picker .sl-ba-go:disabled{opacity:.62;cursor:wait}
818
+ .sl-ba-replace{grid-column:1/-1;padding:3px 0 1px}
819
+ .sl-ba-replace b{display:block;font-size:12.5px}
820
+ .sl-ba-replace span{display:block;margin-top:3px;color:var(--sl-muted);font-size:10.5px;line-height:1.35}
821
+ .sl-ba-actions{grid-column:1/-1;display:grid;grid-template-columns:1fr 1fr;gap:7px}
822
+ .sl-ba-actions button{min-height:36px;border-radius:9px;border:1px solid var(--sl-line);font-size:11.5px;font-weight:800}
823
+ .sl-ba-actions .replace{border-color:var(--sl-accent);background:var(--sl-accent);color:var(--sl-accent-ink)}
807
824
  .sl-picker[data-layout="narrow"] .sl-ba{padding:11px}
808
825
  .sl-picker[data-layout="narrow"] .sl-ba-copy .wide{display:none}
809
826
  .sl-picker[data-layout="narrow"] .sl-ba-copy .narrow{display:inline}
@@ -995,6 +1012,8 @@ var SeatPicker = class _SeatPicker {
995
1012
  this.a11yFilter = "all";
996
1013
  this.baQty = 2;
997
1014
  this.baCat = "";
1015
+ this.bestAvailableConfirm = false;
1016
+ this.releasingHold = false;
998
1017
  // arena / multi-floor / seat-view chrome
999
1018
  this.rungsEl = null;
1000
1019
  this.floorsEl = null;
@@ -1042,10 +1061,11 @@ var SeatPicker = class _SeatPicker {
1042
1061
  this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
1043
1062
  this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
1044
1063
  this.api = new PubApi(this.apiBase);
1064
+ this.maxTickets = Math.max(1, Math.floor(options.maxSelection ?? DEFAULT_MAX_SELECTION2));
1045
1065
  this.controller = new import_core2.PickerController({
1046
1066
  transport: this.api,
1047
1067
  eventKey: options.event,
1048
- maxSelection: options.maxSelection ?? DEFAULT_MAX_SELECTION2,
1068
+ maxSelection: this.maxTickets,
1049
1069
  currency: options.currency,
1050
1070
  flashOnLiveChange: true,
1051
1071
  colorblindSafe: options.colorblindSafe,
@@ -1080,6 +1100,9 @@ var SeatPicker = class _SeatPicker {
1080
1100
  onDeselect: (seat) => {
1081
1101
  if (this.confirmSeat?.id === seat.id) this.dismissConfirm();
1082
1102
  },
1103
+ onSelectionLimit: () => {
1104
+ this.toast(`You can select up to ${this.maxTickets} tickets for this order.`, "warning");
1105
+ },
1083
1106
  onViewChange: () => {
1084
1107
  this.reanchorConfirm();
1085
1108
  this.syncRung();
@@ -1142,6 +1165,10 @@ var SeatPicker = class _SeatPicker {
1142
1165
  if (picker.confirmSeat) {
1143
1166
  e.preventDefault();
1144
1167
  picker.cancelConfirm();
1168
+ } else if (picker.bestAvailableConfirm) {
1169
+ e.preventDefault();
1170
+ picker.bestAvailableConfirm = false;
1171
+ picker.syncTray();
1145
1172
  } else {
1146
1173
  close();
1147
1174
  }
@@ -1165,10 +1192,17 @@ var SeatPicker = class _SeatPicker {
1165
1192
  this.root = root;
1166
1193
  mount.appendChild(root);
1167
1194
  root.addEventListener("keydown", (e) => {
1168
- if (e.key !== "Escape" || !this.confirmSeat) return;
1169
- e.preventDefault();
1170
- e.stopPropagation();
1171
- this.cancelConfirm();
1195
+ if (e.key !== "Escape") return;
1196
+ if (this.confirmSeat) {
1197
+ e.preventDefault();
1198
+ e.stopPropagation();
1199
+ this.cancelConfirm();
1200
+ } else if (this.bestAvailableConfirm) {
1201
+ e.preventDefault();
1202
+ e.stopPropagation();
1203
+ this.bestAvailableConfirm = false;
1204
+ this.syncTray();
1205
+ }
1172
1206
  });
1173
1207
  Object.entries(resolveTokens(void 0, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
1174
1208
  root.innerHTML = `
@@ -1216,6 +1250,7 @@ var SeatPicker = class _SeatPicker {
1216
1250
  <div class="sl-hold-note" data-ref="holdNote" role="status" aria-live="polite">
1217
1251
  <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
1218
1252
  <span><b data-ref="holdTitle">Seats secured</b><span class="sl-hold-copy" data-ref="holdCopy">Checkout timer is running.</span></span>
1253
+ <button type="button" class="sl-hold-change" data-ref="holdChange" aria-label="Release held tickets and choose different seats">Change</button>
1219
1254
  </div>
1220
1255
  <div class="sl-total"><span data-ref="count"></span><b data-ref="total"></b></div>
1221
1256
  <button type="button" class="sl-cta" data-ref="cta" disabled></button>
@@ -1293,6 +1328,7 @@ var SeatPicker = class _SeatPicker {
1293
1328
  if (this.tipEl && this.tipEl.style.display !== "none") this.placeTooltip();
1294
1329
  });
1295
1330
  this.els.cta.addEventListener("click", () => void this.handleCta());
1331
+ this.els.holdChange?.addEventListener("click", () => void this.handleChangeSeats());
1296
1332
  const canvasHost = document.createElement("div");
1297
1333
  canvasHost.style.cssText = "position:absolute;inset:0";
1298
1334
  this.mapHost.appendChild(canvasHost);
@@ -1499,15 +1535,51 @@ var SeatPicker = class _SeatPicker {
1499
1535
  const heldItems = this.hold?.items ?? [];
1500
1536
  const heldLabels = new Set(heldItems.map((item) => item.label));
1501
1537
  const pendingSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).length;
1538
+ return pendingSeats + this.pendingGACount();
1539
+ }
1540
+ heldGACounts() {
1502
1541
  const heldGA = /* @__PURE__ */ new Map();
1503
- for (const item of heldItems.filter((candidate) => candidate.objectType === "ga")) {
1542
+ for (const item of (this.hold?.items ?? []).filter((candidate) => candidate.objectType === "ga")) {
1504
1543
  heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
1505
1544
  }
1506
- const pendingGA = [...this.gaQty.entries()].reduce(
1545
+ return heldGA;
1546
+ }
1547
+ pendingGACount() {
1548
+ const heldGA = this.heldGACounts();
1549
+ return [...this.gaQty.entries()].reduce(
1507
1550
  (sum, [areaId, qty]) => sum + Math.max(0, qty - (heldGA.get(areaId) ?? 0)),
1508
1551
  0
1509
1552
  );
1510
- return pendingSeats + pendingGA;
1553
+ }
1554
+ heldTicketCount() {
1555
+ return (this.hold?.items ?? []).reduce((sum, item) => sum + (item.quantity ?? 1), 0);
1556
+ }
1557
+ totalTicketCount() {
1558
+ const heldLabels = new Set((this.hold?.items ?? []).map((item) => item.label));
1559
+ const freshSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).length;
1560
+ return this.heldTicketCount() + freshSeats + this.pendingGACount();
1561
+ }
1562
+ /** Held tickets and standing quantities consume the same order-wide cap. */
1563
+ updateSelectionCapacity() {
1564
+ const heldLabels = new Set((this.hold?.items ?? []).map((item) => item.label));
1565
+ const selectedHeld = this.committedSelection().filter((seat) => heldLabels.has(seat.label)).length;
1566
+ const remaining = Math.max(0, this.maxTickets - this.heldTicketCount() - this.pendingGACount());
1567
+ this.controller.setMaxSelection(selectedHeld + remaining);
1568
+ }
1569
+ canAddTicket() {
1570
+ if (this.totalTicketCount() < this.maxTickets) return true;
1571
+ this.toast(`You can select up to ${this.maxTickets} tickets for this order.`, "warning");
1572
+ return false;
1573
+ }
1574
+ pendingGATotal(gaAreas) {
1575
+ const heldGA = /* @__PURE__ */ new Map();
1576
+ for (const item of (this.hold?.items ?? []).filter((candidate) => candidate.objectType === "ga")) {
1577
+ heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
1578
+ }
1579
+ return gaAreas.reduce(
1580
+ (sum, area) => sum + area.price * Math.max(0, (this.gaQty.get(area.id) ?? 0) - (heldGA.get(area.id) ?? 0)),
1581
+ 0
1582
+ );
1511
1583
  }
1512
1584
  syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
1513
1585
  const cta = this.els.cta;
@@ -2234,6 +2306,7 @@ var SeatPicker = class _SeatPicker {
2234
2306
  }
2235
2307
  syncTray() {
2236
2308
  if (!this.els.tray) return;
2309
+ this.updateSelectionCapacity();
2237
2310
  const seats = this.committedSelection();
2238
2311
  const gaAreas = this.controller.getGAAreas();
2239
2312
  const heldItems = this.hold?.items ?? [];
@@ -2246,9 +2319,7 @@ var SeatPicker = class _SeatPicker {
2246
2319
  }
2247
2320
  if (!this.hold) {
2248
2321
  const cats = this.controller.doc?.categories ?? [];
2249
- parts.push(
2250
- `<div class="sl-ba"><div class="sl-ba-title"><span class="spark" aria-hidden="true">\u2726</span>Find the best seats together</div><div class="sl-ba-copy"><span class="wide">We\u2019ll choose the closest available group for you.</span><span class="narrow">Closest available group, chosen instantly.</span></div>` + (cats.length > 1 ? `<select aria-label="Preferred ticket type" data-ba-cat><option value="">Any ticket type</option>` + cats.map((c) => `<option value="${c.key}"${this.baCat === c.key ? " selected" : ""}>${c.label}</option>`).join("") + `</select>` : `<span aria-hidden="true"></span>`) + `<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 the best seats\u2026` : `Find ${this.baQty} best ${this.baQty === 1 ? "seat" : "seats"}`) + `</button></div>`
2251
- );
2322
+ parts.push(this.bestAvailableConfirm ? `<div class="sl-ba" role="alert"><div class="sl-ba-title"><span class="spark" aria-hidden="true">\u2726</span>Replace your current choices?</div><div class="sl-ba-replace"><b>We\u2019ll find ${this.baQty} seats together.</b><span>Your manually selected tickets will be removed only after a new group is secured.</span></div><div class="sl-ba-actions"><button type="button" data-ba-cancel>Keep mine</button><button type="button" class="replace" data-ba-replace>Find new seats</button></div></div>` : `<div class="sl-ba"><div class="sl-ba-title"><span class="spark" aria-hidden="true">\u2726</span>Find the best seats together</div><div class="sl-ba-copy"><span class="wide">We\u2019ll choose the closest available group for you.</span><span class="narrow">Closest available group, chosen instantly.</span></div>` + (cats.length > 1 ? `<select aria-label="Preferred ticket type" data-ba-cat><option value="">Any ticket type</option>` + cats.map((c) => `<option value="${c.key}"${this.baCat === c.key ? " selected" : ""}>${c.label}</option>`).join("") + `</select>` : `<span aria-hidden="true"></span>`) + `<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 the best seats\u2026` : `Find ${this.baQty} best ${this.baQty === 1 ? "seat" : "seats"}`) + `</button></div>`);
2252
2323
  }
2253
2324
  for (const item of heldItems) {
2254
2325
  const itemKey = `held:${item.label}`;
@@ -2283,7 +2354,7 @@ var SeatPicker = class _SeatPicker {
2283
2354
  this.lastTrayKeys = nextTrayKeys;
2284
2355
  this.els.tray.querySelectorAll("[data-ba]").forEach((btn) => {
2285
2356
  btn.addEventListener("click", () => {
2286
- this.baQty = Math.max(1, Math.min(8, this.baQty + Number(btn.dataset.ba)));
2357
+ this.baQty = Math.max(1, Math.min(this.maxTickets, this.baQty + Number(btn.dataset.ba)));
2287
2358
  this.syncTray();
2288
2359
  });
2289
2360
  });
@@ -2291,6 +2362,21 @@ var SeatPicker = class _SeatPicker {
2291
2362
  this.baCat = e.target.value;
2292
2363
  });
2293
2364
  this.els.tray.querySelector(".sl-ba-go")?.addEventListener("click", () => {
2365
+ if (this.pendingSelectionCount() > 0) {
2366
+ this.bestAvailableConfirm = true;
2367
+ this.syncTray();
2368
+ this.els.tray.querySelector("[data-ba-replace]")?.focus();
2369
+ return;
2370
+ }
2371
+ void this.bestAvailable(this.baQty, this.baCat || void 0);
2372
+ });
2373
+ this.els.tray.querySelector("[data-ba-cancel]")?.addEventListener("click", () => {
2374
+ this.bestAvailableConfirm = false;
2375
+ this.syncTray();
2376
+ this.els.tray.querySelector(".sl-ba-go")?.focus();
2377
+ });
2378
+ this.els.tray.querySelector("[data-ba-replace]")?.addEventListener("click", () => {
2379
+ this.bestAvailableConfirm = false;
2294
2380
  void this.bestAvailable(this.baQty, this.baCat || void 0);
2295
2381
  });
2296
2382
  this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
@@ -2301,12 +2387,26 @@ var SeatPicker = class _SeatPicker {
2301
2387
  return;
2302
2388
  }
2303
2389
  const id = chip.dataset.seat;
2304
- if (this.reducedMotion()) {
2390
+ const label = chip.querySelector("b")?.textContent ?? "Seat";
2391
+ const remove = () => {
2305
2392
  this.controller.deselect([id]);
2393
+ this.toast(`${label} removed.`, "neutral", {
2394
+ label: "Undo",
2395
+ onClick: () => {
2396
+ const restored = this.controller.select([id]);
2397
+ this.toast(
2398
+ restored.length ? `${label} restored.` : `${label} is no longer available.`,
2399
+ restored.length ? "success" : "warning"
2400
+ );
2401
+ }
2402
+ });
2403
+ };
2404
+ if (this.reducedMotion()) {
2405
+ remove();
2306
2406
  return;
2307
2407
  }
2308
2408
  chip.classList.add("sl-leave");
2309
- this.scheduleMotion(() => this.controller.deselect([id]), 150);
2409
+ this.scheduleMotion(remove, 150);
2310
2410
  });
2311
2411
  });
2312
2412
  this.els.tray.querySelectorAll(".sl-chip .tier").forEach((sel) => {
@@ -2323,13 +2423,15 @@ var SeatPicker = class _SeatPicker {
2323
2423
  const areaEl = btn.closest(".sl-ga");
2324
2424
  const id = areaEl.dataset.ga;
2325
2425
  const area = gaAreas.find((a) => a.id === id);
2326
- const next = Math.max(0, Math.min(area?.available ?? 0, (this.gaQty.get(id) ?? 0) + Number(btn.dataset.d)));
2426
+ const delta = Number(btn.dataset.d);
2427
+ if (delta > 0 && !this.canAddTicket()) return;
2428
+ const next = Math.max(0, Math.min(area?.available ?? 0, (this.gaQty.get(id) ?? 0) + delta));
2327
2429
  this.gaQty.set(id, next);
2328
2430
  this.syncTray();
2329
2431
  });
2330
2432
  });
2331
- const gaTotal = gaAreas.reduce((sum, a) => sum + a.price * (this.gaQty.get(a.id) ?? 0), 0);
2332
- const gaCount = [...this.gaQty.values()].reduce((a, b) => a + b, 0);
2433
+ const gaTotal = this.pendingGATotal(gaAreas);
2434
+ const gaCount = this.pendingGACount();
2333
2435
  const heldTotal = heldItems.reduce((sum, item) => sum + item.unitPrice * (item.quantity ?? 1), 0);
2334
2436
  const heldCount = heldItems.reduce((sum, item) => sum + (item.quantity ?? 1), 0);
2335
2437
  const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
@@ -2349,10 +2451,15 @@ var SeatPicker = class _SeatPicker {
2349
2451
  if (this.hold) {
2350
2452
  const securedCount = heldCount || this.hold.seats?.length || 0;
2351
2453
  if (this.els.holdTitle) {
2352
- this.els.holdTitle.textContent = `${securedCount} ${securedCount === 1 ? "seat" : "seats"} secured`;
2454
+ this.els.holdTitle.textContent = `${securedCount} secured`;
2353
2455
  }
2354
2456
  if (this.els.holdCopy) {
2355
- this.els.holdCopy.textContent = pendingCount ? `${pendingCount} more selected \u2014 secure before checkout.` : "Checkout timer is running.";
2457
+ this.els.holdCopy.textContent = pendingCount ? `${pendingCount} more selected` : "Checkout timer running";
2458
+ }
2459
+ const change = this.els.holdChange;
2460
+ if (change) {
2461
+ change.disabled = this.releasingHold;
2462
+ change.textContent = this.releasingHold ? "Releasing\u2026" : "Change";
2356
2463
  }
2357
2464
  }
2358
2465
  if (count !== previousCount) this.animateOnce(this.els.count, "sl-value-pop", 380);
@@ -2404,7 +2511,30 @@ var SeatPicker = class _SeatPicker {
2404
2511
  if (button?.isConnected) button.disabled = false;
2405
2512
  }
2406
2513
  }
2514
+ async handleChangeSeats() {
2515
+ if (!this.hold || this.releasingHold) return;
2516
+ this.releasingHold = true;
2517
+ const button = this.els.holdChange;
2518
+ if (button) {
2519
+ button.disabled = true;
2520
+ button.textContent = "Releasing\u2026";
2521
+ }
2522
+ try {
2523
+ await this.release();
2524
+ if (!this.hold) this.toast("Held tickets released. Choose your new seats.", "success");
2525
+ } finally {
2526
+ this.releasingHold = false;
2527
+ if (button?.isConnected) {
2528
+ button.disabled = false;
2529
+ button.textContent = "Change";
2530
+ }
2531
+ }
2532
+ }
2407
2533
  async handleCta() {
2534
+ if (this.totalTicketCount() > this.maxTickets) {
2535
+ this.toast(`Remove tickets until your order has ${this.maxTickets} or fewer.`, "warning");
2536
+ return;
2537
+ }
2408
2538
  const committed = this.committedSelection();
2409
2539
  if (this.hold && !committed.some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
2410
2540
  const seats = this.hold.seats ?? committed;
@@ -2442,7 +2572,10 @@ var SeatPicker = class _SeatPicker {
2442
2572
  this.opts.onCheckout?.(hold, hold.seats ?? chosenSeats, this.buildHandoff(hold));
2443
2573
  } catch (err) {
2444
2574
  this.opts.onError?.(err);
2445
- this.toast("One or more seats were just taken. Please pick again.", "error");
2575
+ const problem = err;
2576
+ const labels = (problem.conflicts ?? []).map((conflict) => conflict.label).filter(Boolean).slice(0, 3);
2577
+ const message = problem.reason === "event_closed" ? "Seat sales have closed for this event." : labels.length ? `${labels.join(", ")} ${labels.length === 1 ? "is" : "are"} no longer available. Choose another ${labels.length === 1 ? "seat" : "group"}.` : "One or more seats were just taken. Please pick again.";
2578
+ this.toast(message, "error");
2446
2579
  this.setCtaPhase("idle");
2447
2580
  } finally {
2448
2581
  this.holdingLabels.clear();
@@ -2565,15 +2698,28 @@ var SeatPicker = class _SeatPicker {
2565
2698
  hold ? this.buildHandoff(hold) : null
2566
2699
  );
2567
2700
  }
2568
- toast(msg, tone = "neutral") {
2701
+ toast(msg, tone = "neutral", action) {
2569
2702
  const el = this.els.toast;
2570
2703
  if (!el) return;
2571
- el.textContent = msg;
2704
+ el.replaceChildren();
2705
+ const copy = document.createElement("span");
2706
+ copy.textContent = msg;
2707
+ el.appendChild(copy);
2708
+ el.classList.toggle("has-action", !!action);
2709
+ if (action) {
2710
+ const button = document.createElement("button");
2711
+ button.type = "button";
2712
+ button.className = "sl-toast-action";
2713
+ button.textContent = action.label;
2714
+ button.addEventListener("click", action.onClick, { once: true });
2715
+ el.appendChild(button);
2716
+ }
2572
2717
  el.dataset.tone = tone;
2573
2718
  el.classList.add("on");
2574
2719
  if (this.toastTimer) clearTimeout(this.toastTimer);
2575
2720
  this.toastTimer = setTimeout(() => {
2576
2721
  el.classList.remove("on");
2722
+ el.classList.remove("has-action");
2577
2723
  el.dataset.tone = "neutral";
2578
2724
  }, 4200);
2579
2725
  }
@@ -2623,7 +2769,9 @@ var SeatPicker = class _SeatPicker {
2623
2769
  }
2624
2770
  async bestAvailable(qty, categoryKey) {
2625
2771
  if (this.bestAvailableBusy) return null;
2772
+ qty = Math.max(1, Math.min(this.maxTickets, Math.floor(qty)));
2626
2773
  if (this.confirmSeat) this.cancelConfirm();
2774
+ this.bestAvailableConfirm = false;
2627
2775
  this.bestAvailableBusy = true;
2628
2776
  const button = this.els.tray?.querySelector(".sl-ba-go");
2629
2777
  if (button) {
@@ -2636,6 +2784,7 @@ var SeatPicker = class _SeatPicker {
2636
2784
  this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
2637
2785
  this.handedOff = false;
2638
2786
  this.bookedShown = false;
2787
+ this.gaQty.clear();
2639
2788
  this.startHoldTimer(h.expiresAt);
2640
2789
  this.flashHeldSeats(this.hold);
2641
2790
  this.syncTray();
@@ -2645,14 +2794,13 @@ var SeatPicker = class _SeatPicker {
2645
2794
  return null;
2646
2795
  } catch (err) {
2647
2796
  this.opts.onError?.(err);
2648
- this.toast("Those seats are no longer available. Try another quantity or category.", "error");
2797
+ const reason = err?.reason;
2798
+ const message = reason === "not_enough_together" ? `We couldn't find ${qty} seats together. Try fewer seats or another ticket type.` : reason === "sold_out" ? "That ticket type is sold out. Try another ticket type." : reason === "event_closed" ? "Seat sales have closed for this event." : "Those seats are no longer available. Try another quantity or ticket type.";
2799
+ this.toast(message, "error");
2649
2800
  return null;
2650
2801
  } finally {
2651
2802
  this.bestAvailableBusy = false;
2652
- if (!this.hold && button?.isConnected) {
2653
- button.disabled = false;
2654
- button.textContent = "Best available";
2655
- }
2803
+ this.syncTray();
2656
2804
  }
2657
2805
  }
2658
2806
  async release() {