@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.d.cts CHANGED
@@ -425,6 +425,7 @@ declare class SeatPicker {
425
425
  private readonly api;
426
426
  private readonly apiBase;
427
427
  private readonly controller;
428
+ private readonly maxTickets;
428
429
  private root;
429
430
  private mapHost;
430
431
  private rendered;
@@ -456,6 +457,8 @@ declare class SeatPicker {
456
457
  private a11yFilter;
457
458
  private baQty;
458
459
  private baCat;
460
+ private bestAvailableConfirm;
461
+ private releasingHold;
459
462
  private rungsEl;
460
463
  private floorsEl;
461
464
  private secCardEl;
@@ -535,6 +538,14 @@ declare class SeatPicker {
535
538
  /** Update only the action affordance; selection callbacks must not refire. */
536
539
  private committedSelection;
537
540
  private pendingSelectionCount;
541
+ private heldGACounts;
542
+ private pendingGACount;
543
+ private heldTicketCount;
544
+ private totalTicketCount;
545
+ /** Held tickets and standing quantities consume the same order-wide cap. */
546
+ private updateSelectionCapacity;
547
+ private canAddTicket;
548
+ private pendingGATotal;
538
549
  private syncCta;
539
550
  private setCtaPhase;
540
551
  /** Session-scoped capability key: isolated by API origin and event. */
@@ -616,6 +627,7 @@ declare class SeatPicker {
616
627
  private evictTakenSelections;
617
628
  private syncTray;
618
629
  private removeHeldLabel;
630
+ private handleChangeSeats;
619
631
  private handleCta;
620
632
  private startHoldTimer;
621
633
  private stopHoldTimer;
package/dist/index.d.ts CHANGED
@@ -425,6 +425,7 @@ declare class SeatPicker {
425
425
  private readonly api;
426
426
  private readonly apiBase;
427
427
  private readonly controller;
428
+ private readonly maxTickets;
428
429
  private root;
429
430
  private mapHost;
430
431
  private rendered;
@@ -456,6 +457,8 @@ declare class SeatPicker {
456
457
  private a11yFilter;
457
458
  private baQty;
458
459
  private baCat;
460
+ private bestAvailableConfirm;
461
+ private releasingHold;
459
462
  private rungsEl;
460
463
  private floorsEl;
461
464
  private secCardEl;
@@ -535,6 +538,14 @@ declare class SeatPicker {
535
538
  /** Update only the action affordance; selection callbacks must not refire. */
536
539
  private committedSelection;
537
540
  private pendingSelectionCount;
541
+ private heldGACounts;
542
+ private pendingGACount;
543
+ private heldTicketCount;
544
+ private totalTicketCount;
545
+ /** Held tickets and standing quantities consume the same order-wide cap. */
546
+ private updateSelectionCapacity;
547
+ private canAddTicket;
548
+ private pendingGATotal;
538
549
  private syncCta;
539
550
  private setCtaPhase;
540
551
  /** Session-scoped capability key: isolated by API origin and event. */
@@ -616,6 +627,7 @@ declare class SeatPicker {
616
627
  private evictTakenSelections;
617
628
  private syncTray;
618
629
  private removeHeldLabel;
630
+ private handleChangeSeats;
619
631
  private handleCta;
620
632
  private startHoldTimer;
621
633
  private stopHoldTimer;
package/dist/index.js CHANGED
@@ -476,7 +476,7 @@ var CSS = `
476
476
  .sl-body{display:flex;flex:1;min-height:0}
477
477
  .sl-map{position:relative;flex:1;min-width:0}
478
478
  .sl-map-host{position:absolute;inset:0}
479
- .sl-side{width:300px;flex:none;border-left:1px solid var(--sl-line);display:flex;flex-direction:column;min-height:0;overflow-y:auto}
479
+ .sl-side{width:300px;flex:none;border-left:1px solid var(--sl-line);display:flex;flex-direction:column;min-height:0;overflow:hidden}
480
480
 
481
481
  /* narrow (container < 640px): map-first \u2014 the map claims ~80-85% of the
482
482
  container and the side panel becomes a PEEKING bottom sheet (AXS/Ticketmaster
@@ -559,6 +559,7 @@ var CSS = `
559
559
  .sl-price-select{min-height:32px;max-width:130px;padding:5px 28px 5px 9px;border:1px solid var(--sl-line);border-radius:9px;
560
560
  background:var(--sl-surface);color:var(--sl-text);font:inherit;font-size:11px;font-weight:750;letter-spacing:0;text-transform:none}
561
561
  .sl-prices{padding:5px 14px 10px;border-bottom:1px solid var(--sl-line)}
562
+ .sl-prices-sec,.sl-prices,.sl-seats-sec{flex:none}
562
563
  .sl-price-row{display:flex;align-items:center;gap:7px;min-height:28px;font-size:12px}
563
564
  .sl-dot{width:9px;height:9px;border-radius:50%;flex:none}
564
565
  .sl-price-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
@@ -575,7 +576,8 @@ var CSS = `
575
576
  /* tray */
576
577
  .sl-seats-sec{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:13px}
577
578
  .sl-seat-summary{font-size:10px;letter-spacing:0;text-transform:none;white-space:nowrap}
578
- .sl-tray{flex:1;padding:10px 14px;display:flex;flex-direction:column;gap:7px;min-height:0}
579
+ .sl-tray{flex:1;padding:10px 14px 14px;display:flex;flex-direction:column;gap:7px;min-height:0;overflow-y:auto;
580
+ overscroll-behavior:contain;scrollbar-gutter:stable}
579
581
  .sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
580
582
  .sl-chip{position:relative;display:grid;grid-template-columns:24px minmax(0,1fr) auto 30px;align-items:center;gap:8px;
581
583
  min-height:53px;padding:7px 7px 7px 9px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm);
@@ -610,16 +612,22 @@ var CSS = `
610
612
  .sl-ga-qty span{min-width:16px;text-align:center;font-weight:800;font-variant-numeric:tabular-nums}
611
613
 
612
614
  /* footer */
613
- .sl-foot{padding:12px 16px 14px;border-top:1px solid var(--sl-line);flex:none}
614
- .sl-hold-note{display:none;align-items:center;gap:9px;margin-bottom:10px;padding:9px 10px;border-radius:var(--sl-r-sm);
615
+ .sl-foot{position:relative;z-index:2;padding:12px 16px 14px;border-top:1px solid var(--sl-line);flex:none;
616
+ background:var(--sl-bg);box-shadow:0 -10px 24px -22px rgba(0,0,0,.72)}
617
+ .sl-hold-note{display:none;align-items:center;gap:7px;margin-bottom:8px;padding:7px 8px;border-radius:var(--sl-r-sm);
615
618
  border:1px solid var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
616
619
  box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent);
617
620
  font-size:11.5px;line-height:1.35;color:var(--sl-muted)}
618
621
  .sl-hold-note.on{display:flex;animation:slNoticeIn .38s cubic-bezier(.2,.8,.2,1) both}
619
622
  .sl-hold-note svg{width:16px;height:16px;flex:none;stroke:var(--sl-accent);stroke-width:2.4;fill:none;
620
623
  stroke-linecap:round;stroke-linejoin:round}
621
- .sl-hold-note b{display:block;color:var(--sl-text);font-size:12px}
622
- .sl-hold-copy{display:block}
624
+ .sl-hold-note b{display:block;color:var(--sl-text);font-size:11.5px;white-space:nowrap}
625
+ .sl-hold-copy{display:block;white-space:nowrap;font-size:10.5px}
626
+ .sl-hold-note>span{flex:1;min-width:0}
627
+ .sl-hold-change{flex:none;min-height:30px;padding:5px 8px;border-radius:8px;border:1px solid var(--sl-line);
628
+ color:var(--sl-text);font-size:10.5px;font-weight:750;white-space:nowrap}
629
+ .sl-hold-change:hover,.sl-hold-change:focus-visible{border-color:var(--sl-accent);color:var(--sl-accent)}
630
+ .sl-hold-change:disabled{opacity:.58;cursor:wait}
623
631
  .sl-total{display:flex;justify-content:space-between;align-items:center;font-size:13px;margin-bottom:10px}
624
632
  .sl-total b{font-size:17px;font-variant-numeric:tabular-nums}
625
633
  .sl-value-pop{animation:slValuePop .32s cubic-bezier(.2,.8,.2,1)}
@@ -677,6 +685,9 @@ var CSS = `
677
685
  font-size:12.5px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .22s,transform .22s;white-space:nowrap;
678
686
  overflow:hidden;text-overflow:ellipsis}
679
687
  .sl-toast.on{opacity:1;transform:translateY(0) scale(1)}
688
+ .sl-toast.has-action{pointer-events:auto;display:flex;align-items:center;gap:12px;padding-right:8px}
689
+ .sl-toast-action{min-height:30px;padding:5px 10px;border-radius:999px;background:var(--sl-accent);color:var(--sl-accent-ink);
690
+ font:inherit;font-weight:800}
680
691
  .sl-toast[data-tone="error"]{border-color:#ef4444}
681
692
  .sl-toast[data-tone="warning"]{border-color:var(--sl-accent)}
682
693
  .sl-toast[data-tone="success"]{border-color:#22c55e}
@@ -780,6 +791,12 @@ var CSS = `
780
791
  box-shadow:0 8px 18px color-mix(in srgb,var(--sl-accent) 18%,transparent)}
781
792
  .sl-picker .sl-ba-go:hover{filter:brightness(1.06)}
782
793
  .sl-picker .sl-ba-go:disabled{opacity:.62;cursor:wait}
794
+ .sl-ba-replace{grid-column:1/-1;padding:3px 0 1px}
795
+ .sl-ba-replace b{display:block;font-size:12.5px}
796
+ .sl-ba-replace span{display:block;margin-top:3px;color:var(--sl-muted);font-size:10.5px;line-height:1.35}
797
+ .sl-ba-actions{grid-column:1/-1;display:grid;grid-template-columns:1fr 1fr;gap:7px}
798
+ .sl-ba-actions button{min-height:36px;border-radius:9px;border:1px solid var(--sl-line);font-size:11.5px;font-weight:800}
799
+ .sl-ba-actions .replace{border-color:var(--sl-accent);background:var(--sl-accent);color:var(--sl-accent-ink)}
783
800
  .sl-picker[data-layout="narrow"] .sl-ba{padding:11px}
784
801
  .sl-picker[data-layout="narrow"] .sl-ba-copy .wide{display:none}
785
802
  .sl-picker[data-layout="narrow"] .sl-ba-copy .narrow{display:inline}
@@ -971,6 +988,8 @@ var SeatPicker = class _SeatPicker {
971
988
  this.a11yFilter = "all";
972
989
  this.baQty = 2;
973
990
  this.baCat = "";
991
+ this.bestAvailableConfirm = false;
992
+ this.releasingHold = false;
974
993
  // arena / multi-floor / seat-view chrome
975
994
  this.rungsEl = null;
976
995
  this.floorsEl = null;
@@ -1018,10 +1037,11 @@ var SeatPicker = class _SeatPicker {
1018
1037
  this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
1019
1038
  this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
1020
1039
  this.api = new PubApi(this.apiBase);
1040
+ this.maxTickets = Math.max(1, Math.floor(options.maxSelection ?? DEFAULT_MAX_SELECTION2));
1021
1041
  this.controller = new PickerController2({
1022
1042
  transport: this.api,
1023
1043
  eventKey: options.event,
1024
- maxSelection: options.maxSelection ?? DEFAULT_MAX_SELECTION2,
1044
+ maxSelection: this.maxTickets,
1025
1045
  currency: options.currency,
1026
1046
  flashOnLiveChange: true,
1027
1047
  colorblindSafe: options.colorblindSafe,
@@ -1056,6 +1076,9 @@ var SeatPicker = class _SeatPicker {
1056
1076
  onDeselect: (seat) => {
1057
1077
  if (this.confirmSeat?.id === seat.id) this.dismissConfirm();
1058
1078
  },
1079
+ onSelectionLimit: () => {
1080
+ this.toast(`You can select up to ${this.maxTickets} tickets for this order.`, "warning");
1081
+ },
1059
1082
  onViewChange: () => {
1060
1083
  this.reanchorConfirm();
1061
1084
  this.syncRung();
@@ -1118,6 +1141,10 @@ var SeatPicker = class _SeatPicker {
1118
1141
  if (picker.confirmSeat) {
1119
1142
  e.preventDefault();
1120
1143
  picker.cancelConfirm();
1144
+ } else if (picker.bestAvailableConfirm) {
1145
+ e.preventDefault();
1146
+ picker.bestAvailableConfirm = false;
1147
+ picker.syncTray();
1121
1148
  } else {
1122
1149
  close();
1123
1150
  }
@@ -1141,10 +1168,17 @@ var SeatPicker = class _SeatPicker {
1141
1168
  this.root = root;
1142
1169
  mount.appendChild(root);
1143
1170
  root.addEventListener("keydown", (e) => {
1144
- if (e.key !== "Escape" || !this.confirmSeat) return;
1145
- e.preventDefault();
1146
- e.stopPropagation();
1147
- this.cancelConfirm();
1171
+ if (e.key !== "Escape") return;
1172
+ if (this.confirmSeat) {
1173
+ e.preventDefault();
1174
+ e.stopPropagation();
1175
+ this.cancelConfirm();
1176
+ } else if (this.bestAvailableConfirm) {
1177
+ e.preventDefault();
1178
+ e.stopPropagation();
1179
+ this.bestAvailableConfirm = false;
1180
+ this.syncTray();
1181
+ }
1148
1182
  });
1149
1183
  Object.entries(resolveTokens(void 0, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
1150
1184
  root.innerHTML = `
@@ -1192,6 +1226,7 @@ var SeatPicker = class _SeatPicker {
1192
1226
  <div class="sl-hold-note" data-ref="holdNote" role="status" aria-live="polite">
1193
1227
  <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg>
1194
1228
  <span><b data-ref="holdTitle">Seats secured</b><span class="sl-hold-copy" data-ref="holdCopy">Checkout timer is running.</span></span>
1229
+ <button type="button" class="sl-hold-change" data-ref="holdChange" aria-label="Release held tickets and choose different seats">Change</button>
1195
1230
  </div>
1196
1231
  <div class="sl-total"><span data-ref="count"></span><b data-ref="total"></b></div>
1197
1232
  <button type="button" class="sl-cta" data-ref="cta" disabled></button>
@@ -1269,6 +1304,7 @@ var SeatPicker = class _SeatPicker {
1269
1304
  if (this.tipEl && this.tipEl.style.display !== "none") this.placeTooltip();
1270
1305
  });
1271
1306
  this.els.cta.addEventListener("click", () => void this.handleCta());
1307
+ this.els.holdChange?.addEventListener("click", () => void this.handleChangeSeats());
1272
1308
  const canvasHost = document.createElement("div");
1273
1309
  canvasHost.style.cssText = "position:absolute;inset:0";
1274
1310
  this.mapHost.appendChild(canvasHost);
@@ -1475,15 +1511,51 @@ var SeatPicker = class _SeatPicker {
1475
1511
  const heldItems = this.hold?.items ?? [];
1476
1512
  const heldLabels = new Set(heldItems.map((item) => item.label));
1477
1513
  const pendingSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).length;
1514
+ return pendingSeats + this.pendingGACount();
1515
+ }
1516
+ heldGACounts() {
1478
1517
  const heldGA = /* @__PURE__ */ new Map();
1479
- for (const item of heldItems.filter((candidate) => candidate.objectType === "ga")) {
1518
+ for (const item of (this.hold?.items ?? []).filter((candidate) => candidate.objectType === "ga")) {
1480
1519
  heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
1481
1520
  }
1482
- const pendingGA = [...this.gaQty.entries()].reduce(
1521
+ return heldGA;
1522
+ }
1523
+ pendingGACount() {
1524
+ const heldGA = this.heldGACounts();
1525
+ return [...this.gaQty.entries()].reduce(
1483
1526
  (sum, [areaId, qty]) => sum + Math.max(0, qty - (heldGA.get(areaId) ?? 0)),
1484
1527
  0
1485
1528
  );
1486
- return pendingSeats + pendingGA;
1529
+ }
1530
+ heldTicketCount() {
1531
+ return (this.hold?.items ?? []).reduce((sum, item) => sum + (item.quantity ?? 1), 0);
1532
+ }
1533
+ totalTicketCount() {
1534
+ const heldLabels = new Set((this.hold?.items ?? []).map((item) => item.label));
1535
+ const freshSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).length;
1536
+ return this.heldTicketCount() + freshSeats + this.pendingGACount();
1537
+ }
1538
+ /** Held tickets and standing quantities consume the same order-wide cap. */
1539
+ updateSelectionCapacity() {
1540
+ const heldLabels = new Set((this.hold?.items ?? []).map((item) => item.label));
1541
+ const selectedHeld = this.committedSelection().filter((seat) => heldLabels.has(seat.label)).length;
1542
+ const remaining = Math.max(0, this.maxTickets - this.heldTicketCount() - this.pendingGACount());
1543
+ this.controller.setMaxSelection(selectedHeld + remaining);
1544
+ }
1545
+ canAddTicket() {
1546
+ if (this.totalTicketCount() < this.maxTickets) return true;
1547
+ this.toast(`You can select up to ${this.maxTickets} tickets for this order.`, "warning");
1548
+ return false;
1549
+ }
1550
+ pendingGATotal(gaAreas) {
1551
+ const heldGA = /* @__PURE__ */ new Map();
1552
+ for (const item of (this.hold?.items ?? []).filter((candidate) => candidate.objectType === "ga")) {
1553
+ heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
1554
+ }
1555
+ return gaAreas.reduce(
1556
+ (sum, area) => sum + area.price * Math.max(0, (this.gaQty.get(area.id) ?? 0) - (heldGA.get(area.id) ?? 0)),
1557
+ 0
1558
+ );
1487
1559
  }
1488
1560
  syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
1489
1561
  const cta = this.els.cta;
@@ -2210,6 +2282,7 @@ var SeatPicker = class _SeatPicker {
2210
2282
  }
2211
2283
  syncTray() {
2212
2284
  if (!this.els.tray) return;
2285
+ this.updateSelectionCapacity();
2213
2286
  const seats = this.committedSelection();
2214
2287
  const gaAreas = this.controller.getGAAreas();
2215
2288
  const heldItems = this.hold?.items ?? [];
@@ -2222,9 +2295,7 @@ var SeatPicker = class _SeatPicker {
2222
2295
  }
2223
2296
  if (!this.hold) {
2224
2297
  const cats = this.controller.doc?.categories ?? [];
2225
- parts.push(
2226
- `<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>`
2227
- );
2298
+ 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>`);
2228
2299
  }
2229
2300
  for (const item of heldItems) {
2230
2301
  const itemKey = `held:${item.label}`;
@@ -2259,7 +2330,7 @@ var SeatPicker = class _SeatPicker {
2259
2330
  this.lastTrayKeys = nextTrayKeys;
2260
2331
  this.els.tray.querySelectorAll("[data-ba]").forEach((btn) => {
2261
2332
  btn.addEventListener("click", () => {
2262
- this.baQty = Math.max(1, Math.min(8, this.baQty + Number(btn.dataset.ba)));
2333
+ this.baQty = Math.max(1, Math.min(this.maxTickets, this.baQty + Number(btn.dataset.ba)));
2263
2334
  this.syncTray();
2264
2335
  });
2265
2336
  });
@@ -2267,6 +2338,21 @@ var SeatPicker = class _SeatPicker {
2267
2338
  this.baCat = e.target.value;
2268
2339
  });
2269
2340
  this.els.tray.querySelector(".sl-ba-go")?.addEventListener("click", () => {
2341
+ if (this.pendingSelectionCount() > 0) {
2342
+ this.bestAvailableConfirm = true;
2343
+ this.syncTray();
2344
+ this.els.tray.querySelector("[data-ba-replace]")?.focus();
2345
+ return;
2346
+ }
2347
+ void this.bestAvailable(this.baQty, this.baCat || void 0);
2348
+ });
2349
+ this.els.tray.querySelector("[data-ba-cancel]")?.addEventListener("click", () => {
2350
+ this.bestAvailableConfirm = false;
2351
+ this.syncTray();
2352
+ this.els.tray.querySelector(".sl-ba-go")?.focus();
2353
+ });
2354
+ this.els.tray.querySelector("[data-ba-replace]")?.addEventListener("click", () => {
2355
+ this.bestAvailableConfirm = false;
2270
2356
  void this.bestAvailable(this.baQty, this.baCat || void 0);
2271
2357
  });
2272
2358
  this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
@@ -2277,12 +2363,26 @@ var SeatPicker = class _SeatPicker {
2277
2363
  return;
2278
2364
  }
2279
2365
  const id = chip.dataset.seat;
2280
- if (this.reducedMotion()) {
2366
+ const label = chip.querySelector("b")?.textContent ?? "Seat";
2367
+ const remove = () => {
2281
2368
  this.controller.deselect([id]);
2369
+ this.toast(`${label} removed.`, "neutral", {
2370
+ label: "Undo",
2371
+ onClick: () => {
2372
+ const restored = this.controller.select([id]);
2373
+ this.toast(
2374
+ restored.length ? `${label} restored.` : `${label} is no longer available.`,
2375
+ restored.length ? "success" : "warning"
2376
+ );
2377
+ }
2378
+ });
2379
+ };
2380
+ if (this.reducedMotion()) {
2381
+ remove();
2282
2382
  return;
2283
2383
  }
2284
2384
  chip.classList.add("sl-leave");
2285
- this.scheduleMotion(() => this.controller.deselect([id]), 150);
2385
+ this.scheduleMotion(remove, 150);
2286
2386
  });
2287
2387
  });
2288
2388
  this.els.tray.querySelectorAll(".sl-chip .tier").forEach((sel) => {
@@ -2299,13 +2399,15 @@ var SeatPicker = class _SeatPicker {
2299
2399
  const areaEl = btn.closest(".sl-ga");
2300
2400
  const id = areaEl.dataset.ga;
2301
2401
  const area = gaAreas.find((a) => a.id === id);
2302
- const next = Math.max(0, Math.min(area?.available ?? 0, (this.gaQty.get(id) ?? 0) + Number(btn.dataset.d)));
2402
+ const delta = Number(btn.dataset.d);
2403
+ if (delta > 0 && !this.canAddTicket()) return;
2404
+ const next = Math.max(0, Math.min(area?.available ?? 0, (this.gaQty.get(id) ?? 0) + delta));
2303
2405
  this.gaQty.set(id, next);
2304
2406
  this.syncTray();
2305
2407
  });
2306
2408
  });
2307
- const gaTotal = gaAreas.reduce((sum, a) => sum + a.price * (this.gaQty.get(a.id) ?? 0), 0);
2308
- const gaCount = [...this.gaQty.values()].reduce((a, b) => a + b, 0);
2409
+ const gaTotal = this.pendingGATotal(gaAreas);
2410
+ const gaCount = this.pendingGACount();
2309
2411
  const heldTotal = heldItems.reduce((sum, item) => sum + item.unitPrice * (item.quantity ?? 1), 0);
2310
2412
  const heldCount = heldItems.reduce((sum, item) => sum + (item.quantity ?? 1), 0);
2311
2413
  const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
@@ -2325,10 +2427,15 @@ var SeatPicker = class _SeatPicker {
2325
2427
  if (this.hold) {
2326
2428
  const securedCount = heldCount || this.hold.seats?.length || 0;
2327
2429
  if (this.els.holdTitle) {
2328
- this.els.holdTitle.textContent = `${securedCount} ${securedCount === 1 ? "seat" : "seats"} secured`;
2430
+ this.els.holdTitle.textContent = `${securedCount} secured`;
2329
2431
  }
2330
2432
  if (this.els.holdCopy) {
2331
- this.els.holdCopy.textContent = pendingCount ? `${pendingCount} more selected \u2014 secure before checkout.` : "Checkout timer is running.";
2433
+ this.els.holdCopy.textContent = pendingCount ? `${pendingCount} more selected` : "Checkout timer running";
2434
+ }
2435
+ const change = this.els.holdChange;
2436
+ if (change) {
2437
+ change.disabled = this.releasingHold;
2438
+ change.textContent = this.releasingHold ? "Releasing\u2026" : "Change";
2332
2439
  }
2333
2440
  }
2334
2441
  if (count !== previousCount) this.animateOnce(this.els.count, "sl-value-pop", 380);
@@ -2380,7 +2487,30 @@ var SeatPicker = class _SeatPicker {
2380
2487
  if (button?.isConnected) button.disabled = false;
2381
2488
  }
2382
2489
  }
2490
+ async handleChangeSeats() {
2491
+ if (!this.hold || this.releasingHold) return;
2492
+ this.releasingHold = true;
2493
+ const button = this.els.holdChange;
2494
+ if (button) {
2495
+ button.disabled = true;
2496
+ button.textContent = "Releasing\u2026";
2497
+ }
2498
+ try {
2499
+ await this.release();
2500
+ if (!this.hold) this.toast("Held tickets released. Choose your new seats.", "success");
2501
+ } finally {
2502
+ this.releasingHold = false;
2503
+ if (button?.isConnected) {
2504
+ button.disabled = false;
2505
+ button.textContent = "Change";
2506
+ }
2507
+ }
2508
+ }
2383
2509
  async handleCta() {
2510
+ if (this.totalTicketCount() > this.maxTickets) {
2511
+ this.toast(`Remove tickets until your order has ${this.maxTickets} or fewer.`, "warning");
2512
+ return;
2513
+ }
2384
2514
  const committed = this.committedSelection();
2385
2515
  if (this.hold && !committed.some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
2386
2516
  const seats = this.hold.seats ?? committed;
@@ -2418,7 +2548,10 @@ var SeatPicker = class _SeatPicker {
2418
2548
  this.opts.onCheckout?.(hold, hold.seats ?? chosenSeats, this.buildHandoff(hold));
2419
2549
  } catch (err) {
2420
2550
  this.opts.onError?.(err);
2421
- this.toast("One or more seats were just taken. Please pick again.", "error");
2551
+ const problem = err;
2552
+ const labels = (problem.conflicts ?? []).map((conflict) => conflict.label).filter(Boolean).slice(0, 3);
2553
+ 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.";
2554
+ this.toast(message, "error");
2422
2555
  this.setCtaPhase("idle");
2423
2556
  } finally {
2424
2557
  this.holdingLabels.clear();
@@ -2541,15 +2674,28 @@ var SeatPicker = class _SeatPicker {
2541
2674
  hold ? this.buildHandoff(hold) : null
2542
2675
  );
2543
2676
  }
2544
- toast(msg, tone = "neutral") {
2677
+ toast(msg, tone = "neutral", action) {
2545
2678
  const el = this.els.toast;
2546
2679
  if (!el) return;
2547
- el.textContent = msg;
2680
+ el.replaceChildren();
2681
+ const copy = document.createElement("span");
2682
+ copy.textContent = msg;
2683
+ el.appendChild(copy);
2684
+ el.classList.toggle("has-action", !!action);
2685
+ if (action) {
2686
+ const button = document.createElement("button");
2687
+ button.type = "button";
2688
+ button.className = "sl-toast-action";
2689
+ button.textContent = action.label;
2690
+ button.addEventListener("click", action.onClick, { once: true });
2691
+ el.appendChild(button);
2692
+ }
2548
2693
  el.dataset.tone = tone;
2549
2694
  el.classList.add("on");
2550
2695
  if (this.toastTimer) clearTimeout(this.toastTimer);
2551
2696
  this.toastTimer = setTimeout(() => {
2552
2697
  el.classList.remove("on");
2698
+ el.classList.remove("has-action");
2553
2699
  el.dataset.tone = "neutral";
2554
2700
  }, 4200);
2555
2701
  }
@@ -2599,7 +2745,9 @@ var SeatPicker = class _SeatPicker {
2599
2745
  }
2600
2746
  async bestAvailable(qty, categoryKey) {
2601
2747
  if (this.bestAvailableBusy) return null;
2748
+ qty = Math.max(1, Math.min(this.maxTickets, Math.floor(qty)));
2602
2749
  if (this.confirmSeat) this.cancelConfirm();
2750
+ this.bestAvailableConfirm = false;
2603
2751
  this.bestAvailableBusy = true;
2604
2752
  const button = this.els.tray?.querySelector(".sl-ba-go");
2605
2753
  if (button) {
@@ -2612,6 +2760,7 @@ var SeatPicker = class _SeatPicker {
2612
2760
  this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
2613
2761
  this.handedOff = false;
2614
2762
  this.bookedShown = false;
2763
+ this.gaQty.clear();
2615
2764
  this.startHoldTimer(h.expiresAt);
2616
2765
  this.flashHeldSeats(this.hold);
2617
2766
  this.syncTray();
@@ -2621,14 +2770,13 @@ var SeatPicker = class _SeatPicker {
2621
2770
  return null;
2622
2771
  } catch (err) {
2623
2772
  this.opts.onError?.(err);
2624
- this.toast("Those seats are no longer available. Try another quantity or category.", "error");
2773
+ const reason = err?.reason;
2774
+ 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.";
2775
+ this.toast(message, "error");
2625
2776
  return null;
2626
2777
  } finally {
2627
2778
  this.bestAvailableBusy = false;
2628
- if (!this.hold && button?.isConnected) {
2629
- button.disabled = false;
2630
- button.textContent = "Best available";
2631
- }
2779
+ this.syncTray();
2632
2780
  }
2633
2781
  }
2634
2782
  async release() {