@seatlayer/js 0.26.0 → 0.28.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
@@ -1089,6 +1089,15 @@ function resolveContainer3(container) {
1089
1089
  }
1090
1090
  return container;
1091
1091
  }
1092
+ function escapeOption(value) {
1093
+ return String(value ?? "").replace(/[&<>"']/g, (character) => ({
1094
+ "&": "&amp;",
1095
+ "<": "&lt;",
1096
+ ">": "&gt;",
1097
+ '"': "&quot;",
1098
+ "'": "&#39;"
1099
+ })[character]);
1100
+ }
1092
1101
  var STYLE_ID = "seatlayer-picker-style";
1093
1102
  var CSS = `
1094
1103
  .sl-picker{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:420px;overflow:hidden;
@@ -1200,6 +1209,11 @@ var CSS = `
1200
1209
  .sl-picker[data-layout="narrow"] .sl-filters.has{display:none}
1201
1210
  .sl-picker[data-layout="narrow"][data-has-selection="true"] .sl-filtersec.has,
1202
1211
  .sl-picker[data-layout="narrow"][data-has-selection="true"] .sl-filters.has{display:none}
1212
+ /* Accessibility and colour-safety controls must remain reachable on phones.
1213
+ Keep them out of the 86px peek, then reveal their consolidated row whenever
1214
+ the buyer explicitly opens the ticket panel. */
1215
+ .sl-picker[data-layout="narrow"][data-sheet="open"] .sl-filtersec.has{display:block!important}
1216
+ .sl-picker[data-layout="narrow"][data-sheet="open"] .sl-filters.has{display:flex!important}
1203
1217
  .sl-cbbtn{width:32px;height:32px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
1204
1218
  color:var(--sl-text);display:flex;align-items:center;justify-content:center;transition:border-color .15s}
1205
1219
  .sl-cbbtn:hover{border-color:var(--sl-muted)}
@@ -1492,6 +1506,37 @@ var CSS = `
1492
1506
  .sl-picker[data-layout="narrow"] .sl-confirm{left:50%!important;top:auto!important;bottom:14px;width:min(342px,calc(100% - 24px));
1493
1507
  transform:translateX(-50%);animation:slConfirmMobileIn .24s cubic-bezier(.2,.8,.2,1) both}
1494
1508
 
1509
+ /* Atomic whole/variable-table chooser. Lives inside the widget so the same
1510
+ accessible dialog works in inline, modal, desktop and 390px mobile hosts. */
1511
+ .sl-table-scrim{position:absolute;inset:0;z-index:45;display:flex;align-items:center;justify-content:center;padding:18px;
1512
+ background:color-mix(in srgb,var(--sl-bg) 66%,transparent);backdrop-filter:blur(3px)}
1513
+ .sl-table-dialog{width:min(408px,100%);max-height:calc(100% - 24px);overflow:auto;border:1px solid var(--sl-line);
1514
+ border-radius:calc(var(--sl-radius) * 1.15);background:var(--sl-surface);box-shadow:0 28px 70px rgba(0,0,0,.42)}
1515
+ .sl-table-head{padding:18px 18px 14px;border-bottom:1px solid var(--sl-line)}
1516
+ .sl-table-eyebrow{font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
1517
+ .sl-table-title{margin-top:5px;font-size:22px;line-height:1.15;font-weight:850}
1518
+ .sl-table-copy{margin-top:7px;color:var(--sl-muted);font-size:13px;line-height:1.45}
1519
+ .sl-table-body{padding:16px 18px 18px}
1520
+ .sl-table-summary{display:grid;grid-template-columns:1fr auto;gap:8px 16px;padding:12px;border:1px solid var(--sl-line);
1521
+ border-radius:var(--sl-r-sm);background:color-mix(in srgb,var(--sl-line) 22%,transparent);font-size:13px}
1522
+ .sl-table-summary b{font-size:15px}.sl-table-summary .muted{color:var(--sl-muted)}
1523
+ .sl-table-qtylabel{display:block;margin:16px 0 8px;font-size:12px;font-weight:800}
1524
+ .sl-table-stepper{display:grid;grid-template-columns:48px 1fr 48px;align-items:center;border:1px solid var(--sl-line);
1525
+ border-radius:12px;overflow:hidden;background:var(--sl-bg)}
1526
+ .sl-table-stepper button{height:48px;font-size:24px;font-weight:700;background:color-mix(in srgb,var(--sl-line) 34%,transparent)!important}
1527
+ .sl-table-stepper button:disabled{opacity:.38;cursor:not-allowed}
1528
+ .sl-table-stepper output{text-align:center;font-size:19px;font-weight:850;font-variant-numeric:tabular-nums}
1529
+ .sl-table-range{margin-top:7px;color:var(--sl-muted);font-size:11px;text-align:center}
1530
+ .sl-table-actions{display:flex;gap:9px;margin-top:17px}.sl-table-actions button{flex:1;min-height:46px;border-radius:10px;font-weight:800}
1531
+ .sl-table-cancel{border:1px solid var(--sl-line)!important;color:var(--sl-muted)!important}
1532
+ .sl-table-confirm{background:var(--sl-accent)!important;color:var(--sl-accent-ink)!important}
1533
+ .sl-table-confirm:disabled{opacity:.62;cursor:wait}
1534
+ .sl-table-edit{margin-left:4px;padding:2px 7px!important;border:1px solid var(--sl-line)!important;border-radius:999px!important;
1535
+ color:var(--sl-muted)!important;font-size:10px!important;font-weight:800!important}
1536
+ .sl-picker[data-layout="narrow"] .sl-table-scrim{align-items:flex-end;padding:0;background:rgba(5,7,12,.58)}
1537
+ .sl-picker[data-layout="narrow"] .sl-table-dialog{width:100%;max-height:min(78%,620px);border-radius:18px 18px 0 0;border-width:1px 0 0}
1538
+ .sl-picker[data-layout="narrow"] .sl-table-head{padding-top:22px}.sl-picker[data-layout="narrow"] .sl-table-body{padding-bottom:max(20px,env(safe-area-inset-bottom))}
1539
+
1495
1540
  /* hover preview \u2014 a COMPACT echo of the confirm card (deliberately smaller: it's
1496
1541
  a passing preview on hover, not the click/select action surface). Reuses the
1497
1542
  Section\xB7Row\xB7Seat identity grid so hover, confirm and the cart chip all share
@@ -1555,6 +1600,7 @@ var CSS = `
1555
1600
  .sl-picker[data-layout="narrow"] .sl-ba{padding:11px}
1556
1601
  .sl-picker[data-layout="narrow"] .sl-ba-copy .wide{display:none}
1557
1602
  .sl-picker[data-layout="narrow"] .sl-ba-copy .narrow{display:inline}
1603
+ .sl-picker[data-layout="narrow"] .sl-ba select{min-height:44px}
1558
1604
 
1559
1605
  /* screen-reader live region */
1560
1606
  .sl-sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
@@ -1573,6 +1619,16 @@ var CSS = `
1573
1619
  /* narrow: shrink the rung pills so the centered row can't reach the corner regions */
1574
1620
  .sl-picker[data-layout="narrow"] .sl-rungs button{padding:5px 9px;font-size:9px;letter-spacing:.03em}
1575
1621
 
1622
+ /* Projection is deliberately a separate control from zoom/LOD. Perspective
1623
+ changes geometry; the two choices stay explicit and keyboard-native. */
1624
+ .sl-projection{display:inline-flex;align-items:center;gap:2px;padding:3px;border-radius:999px;
1625
+ background:var(--sl-surface);border:1px solid var(--sl-line);box-shadow:0 8px 24px -16px rgba(0,0,0,.65)}
1626
+ .sl-projection button{min-width:42px;min-height:30px;padding:5px 10px;border-radius:999px;color:var(--sl-muted);
1627
+ font-size:10px;font-weight:800;letter-spacing:.04em;white-space:nowrap}
1628
+ .sl-projection button:hover,.sl-projection button:focus-visible{color:var(--sl-text)}
1629
+ .sl-projection button.on{background:var(--sl-accent);color:var(--sl-accent-ink)}
1630
+ .sl-picker[data-layout="narrow"] .sl-projection button{min-width:38px;min-height:32px;padding:5px 8px;font-size:9.5px}
1631
+
1576
1632
  /* multi-floor switcher (flows within the left-rail region) */
1577
1633
  .sl-floors{display:none;flex-direction:column;gap:6px;max-width:100%}
1578
1634
  .sl-floors.on{display:flex}
@@ -1701,6 +1757,7 @@ var CSS = `
1701
1757
  .sl-picker *,.sl-modal-scrim *{animation-duration:.001ms!important;animation-iteration-count:1!important;
1702
1758
  transition-duration:.001ms!important;scroll-behavior:auto!important}
1703
1759
  }
1760
+ .sl-ba [data-ba-zone]{grid-column:1/-1;width:100%}
1704
1761
 
1705
1762
  /* modal host */
1706
1763
  .sl-modal-scrim{position:fixed;inset:0;z-index:2147483000;background:rgba(5,7,12,.66);display:flex;align-items:center;justify-content:center;padding:18px}
@@ -1776,9 +1833,15 @@ var SeatPicker = class _SeatPicker {
1776
1833
  this.tipPos = { x: 0, y: 0 };
1777
1834
  this.confirmEl = null;
1778
1835
  this.confirmSeat = null;
1836
+ this.tableDialogEl = null;
1837
+ this.tableDialog = null;
1838
+ this.tableDialogHeld = false;
1839
+ this.tableDialogReturnFocus = null;
1779
1840
  this.srEl = null;
1780
1841
  this.baQty = 2;
1781
1842
  this.baCat = "";
1843
+ /** Optional navigation-zone scope for buyer best-available. */
1844
+ this.baZone = "";
1782
1845
  /** "★ Best seats" premium quick-pick toggle — biases best-available to premium seats. */
1783
1846
  this.baPremium = false;
1784
1847
  this.bestAvailableConfirm = false;
@@ -1792,6 +1855,7 @@ var SeatPicker = class _SeatPicker {
1792
1855
  this.cbSafe = false;
1793
1856
  // arena / multi-floor / seat-view chrome
1794
1857
  this.rungsEl = null;
1858
+ this.projectionEl = null;
1795
1859
  this.floorsEl = null;
1796
1860
  this.secCardEl = null;
1797
1861
  this.viewEl = null;
@@ -1887,11 +1951,21 @@ var SeatPicker = class _SeatPicker {
1887
1951
  this.toast(this.tf("picker.salesClosedToast", "Sales are closed for this event."), "warning");
1888
1952
  return;
1889
1953
  }
1954
+ if (this.controller.tableSelection(seat.id)) return;
1890
1955
  this.flashPickedSeat(seat.id);
1891
1956
  if (this.opts.confirmSelection) this.showConfirm(seat);
1892
1957
  },
1958
+ onTableSelectionRequest: (table) => {
1959
+ if (this.salesClosed) {
1960
+ this.controller.deselect(table.physicalSeatIds);
1961
+ this.toast(this.tf("picker.salesClosedToast", "Sales are closed for this event."), "warning");
1962
+ return;
1963
+ }
1964
+ this.showTableDialog(table, false);
1965
+ },
1893
1966
  onDeselect: (seat) => {
1894
1967
  if (this.confirmSeat?.id === seat.id) this.dismissConfirm();
1968
+ if (this.tableDialog?.physicalSeatIds.includes(seat.id)) this.dismissTableDialog();
1895
1969
  },
1896
1970
  onSelectionLimit: () => {
1897
1971
  this.toast(`You can select up to ${this.maxTickets} tickets for this order.`, "warning");
@@ -1899,6 +1973,7 @@ var SeatPicker = class _SeatPicker {
1899
1973
  onViewChange: () => {
1900
1974
  this.reanchorConfirm();
1901
1975
  this.syncRung();
1976
+ this.syncProjection();
1902
1977
  this.drawMinimapRect();
1903
1978
  this.sectionCardOnView();
1904
1979
  },
@@ -1915,27 +1990,47 @@ var SeatPicker = class _SeatPicker {
1915
1990
  onError: (err) => this.opts.onError?.(err)
1916
1991
  });
1917
1992
  }
1993
+ /** True when the chart carries a real performance anchor — a stage-kind shape.
1994
+ * Every chart has a `focalPoint` (a bare look-at coordinate), so its presence
1995
+ * alone never justifies a "to stage" claim; only an actual stage does. */
1996
+ chartHasStage() {
1997
+ const doc = this.controller.doc;
1998
+ if (!doc) return false;
1999
+ const objectSets = doc.floors?.length ? doc.floors.map((f) => f.objects ?? []) : [doc.objects ?? []];
2000
+ for (const objects of objectSets) {
2001
+ for (const obj of objects) {
2002
+ if (obj.type === "shape" && (obj.role === "stage" || obj.stageKind)) return true;
2003
+ }
2004
+ }
2005
+ return false;
2006
+ }
1918
2007
  /**
1919
- * Eager sightline preview for the confirm card: a cheap generated forward
1920
- * view (or the organizer's real photo) plus a "Nm to stage · clear
1921
- * sightline" line the premium at-a-glance moment; click opens the 360.
2008
+ * Eager sightline preview for the confirm card: the organizer's real view
2009
+ * photo, or only when the chart has an actual stage to look at — a generated
2010
+ * forward view plus an " Nm to stage · clear sightline" line. On a stageless
2011
+ * chart both the distance/sightline claim and the generic stage silhouette are
2012
+ * invented promises, so we suppress them; a real attached photo always shows.
2013
+ * (OV-52)
1922
2014
  */
1923
2015
  confirmThumbHtml(seat) {
1924
2016
  const doc = this.controller.doc;
1925
2017
  if (!doc) return "";
1926
- let url = seat.viewUrl ?? "";
2018
+ const realPhoto = seat.viewUrl ?? "";
2019
+ const hasStage = this.chartHasStage();
2020
+ if (!realPhoto && !hasStage) return "";
2021
+ let url = realPhoto;
1927
2022
  let distance = null;
1928
2023
  if (!url) {
1929
2024
  try {
1930
- const thumb = (0, import_core2.generateSeatThumb)(seat, doc.focalPoint);
2025
+ const thumb = (0, import_core2.generateSeatThumb)(seat, seat.focalPoint ?? doc.focalPoint);
1931
2026
  url = thumb.url;
1932
2027
  distance = thumb.distanceM ?? null;
1933
2028
  } catch {
1934
2029
  return "";
1935
2030
  }
1936
2031
  }
1937
- const sight = distance != null ? (0, import_core2.t)("picker.sightline", { m: distance }) : this.tf("picker.sightlineClear", "Clear sightline");
1938
- return `<button type="button" class="sl-confirm-view sl-confirm-thumbwrap" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: seat.label })}"><img class="sl-confirm-thumb" src="${url}" alt="" /><span class="sl-confirm-thumb-badge">\u{1F52D} ${this.tf("picker.viewFromHere", "View from here")}</span></button><div class="sl-confirm-sight"><span aria-hidden="true">\u2713</span>${sight}</div>`;
2032
+ const sightHtml = hasStage ? `<div class="sl-confirm-sight"><span aria-hidden="true">\u2713</span>${distance != null ? (0, import_core2.t)("picker.sightline", { m: distance }) : this.tf("picker.sightlineClear", "Clear sightline")}</div>` : "";
2033
+ return `<button type="button" class="sl-confirm-view sl-confirm-thumbwrap" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: seat.label })}"><img class="sl-confirm-thumb" src="${url}" alt="" /><span class="sl-confirm-thumb-badge">\u{1F52D} ${this.tf("picker.viewFromHere", "View from here")}</span></button>` + sightHtml;
1939
2034
  }
1940
2035
  /** Minimal HTML/attribute escaper for buyer-authored commercial text (notes). */
1941
2036
  escCx(value) {
@@ -1981,6 +2076,19 @@ var SeatPicker = class _SeatPicker {
1981
2076
  const title = this.escCx(c?.note ? c.note : limited);
1982
2077
  return `<span class="sl-cx-mark" role="img" aria-label="${title}" title="${title}">\u25D0</span>`;
1983
2078
  }
2079
+ wheelchairProvisionLabel(type) {
2080
+ if (type === "no-seat") return "Empty wheelchair space";
2081
+ if (type === "seat-present") return "Accessible physical seat";
2082
+ return "";
2083
+ }
2084
+ wheelchairConfirmHtml(type) {
2085
+ const label = this.wheelchairProvisionLabel(type);
2086
+ return label ? `<div class="sl-cx"><div class="sl-cx-warn"><span class="sl-cx-glyph" aria-hidden="true">\u267F</span><span class="sl-cx-txt"><b>${label}</b></span></div></div>` : "";
2087
+ }
2088
+ wheelchairChipMarker(type) {
2089
+ const label = this.wheelchairProvisionLabel(type);
2090
+ return label ? `<span class="sl-cx-mark" role="img" aria-label="${label}" title="${label}">\u267F</span>` : "";
2091
+ }
1984
2092
  /** True when the picker is rendered inside an iframe (snippet embed at /e/:key). */
1985
2093
  isFramed() {
1986
2094
  return typeof window !== "undefined" && window.parent !== window;
@@ -2132,7 +2240,10 @@ var SeatPicker = class _SeatPicker {
2132
2240
  });
2133
2241
  picker.escHandler = (e) => {
2134
2242
  if (e.key !== "Escape") return;
2135
- if (picker.confirmSeat) {
2243
+ if (picker.tableDialog) {
2244
+ e.preventDefault();
2245
+ picker.cancelTableDialog();
2246
+ } else if (picker.confirmSeat) {
2136
2247
  e.preventDefault();
2137
2248
  picker.cancelConfirm();
2138
2249
  } else if (picker.bestAvailableConfirm) {
@@ -2163,7 +2274,11 @@ var SeatPicker = class _SeatPicker {
2163
2274
  mount.appendChild(root);
2164
2275
  root.addEventListener("keydown", (e) => {
2165
2276
  if (e.key !== "Escape") return;
2166
- if (this.confirmSeat) {
2277
+ if (this.tableDialog) {
2278
+ e.preventDefault();
2279
+ e.stopPropagation();
2280
+ this.cancelTableDialog();
2281
+ } else if (this.confirmSeat) {
2167
2282
  e.preventDefault();
2168
2283
  e.stopPropagation();
2169
2284
  this.cancelConfirm();
@@ -2332,6 +2447,7 @@ var SeatPicker = class _SeatPicker {
2332
2447
  }
2333
2448
  this.els.boot.remove();
2334
2449
  this.salesClosed = !!info.salesClosed;
2450
+ this.controller.setViewMode(this.opts.initialView ?? "flat");
2335
2451
  this.buildRegions();
2336
2452
  this.regions["bottom-right"].appendChild(this.els.zoom);
2337
2453
  this.regions["bottom-center"].appendChild(this.els.toast);
@@ -2374,11 +2490,10 @@ var SeatPicker = class _SeatPicker {
2374
2490
  this.regions["top-left"].appendChild(chips);
2375
2491
  this.a11yChipsEl = chips;
2376
2492
  if (present.size) {
2377
- const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
2378
2493
  const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-a11y="1" data-f="${key}">${label}</button>`;
2379
2494
  chips.insertAdjacentHTML(
2380
2495
  "beforeend",
2381
- mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("")
2496
+ mk("all", "All seats") + import_core2.ACCESSIBILITY_TYPES.filter(({ key }) => present.has(key)).map(({ key, short, icon }) => mk(key, `${icon} ${short}`)).join("")
2382
2497
  );
2383
2498
  const active = /* @__PURE__ */ new Set();
2384
2499
  const syncChips = () => {
@@ -2428,10 +2543,7 @@ var SeatPicker = class _SeatPicker {
2428
2543
  cb.innerHTML = '<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>';
2429
2544
  this.els.zfit.parentElement.appendChild(cb);
2430
2545
  cb.addEventListener("click", () => {
2431
- this.cbSafe = !this.cbSafe;
2432
- cb.setAttribute("aria-pressed", String(this.cbSafe));
2433
- this.controller.setColorblindSafe(this.cbSafe);
2434
- writeStoredColorblind(this.cbSafe);
2546
+ this.setColorblindSafe(!this.cbSafe);
2435
2547
  });
2436
2548
  this.srEl = document.createElement("div");
2437
2549
  this.srEl.className = "sl-sr";
@@ -2553,6 +2665,7 @@ var SeatPicker = class _SeatPicker {
2553
2665
  this.applySalesClosed();
2554
2666
  }
2555
2667
  applySalesClosed() {
2668
+ if (this.salesClosed && this.tableDialog && !this.tableDialogHeld) this.cancelTableDialog();
2556
2669
  const pill = this.els.closedPill;
2557
2670
  if (pill) {
2558
2671
  pill.classList.toggle("on", this.salesClosed);
@@ -2641,12 +2754,13 @@ var SeatPicker = class _SeatPicker {
2641
2754
  /** Update only the action affordance; selection callbacks must not refire. */
2642
2755
  committedSelection() {
2643
2756
  const candidateId = this.confirmSeat?.id;
2644
- return this.controller.getSelection().filter((seat) => seat.id !== candidateId);
2757
+ const pendingTableId = this.tableDialog && !this.tableDialogHeld ? this.tableDialog.id : null;
2758
+ return this.controller.getSelection().filter((seat) => seat.id !== candidateId && seat.id !== pendingTableId);
2645
2759
  }
2646
2760
  pendingSelectionCount() {
2647
2761
  const heldItems = this.hold?.items ?? [];
2648
2762
  const heldLabels = new Set(heldItems.map((item) => item.label));
2649
- const pendingSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).length;
2763
+ const pendingSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).reduce((sum, seat) => sum + (seat.quantity ?? 1), 0);
2650
2764
  return pendingSeats + this.pendingGACount();
2651
2765
  }
2652
2766
  heldGACounts() {
@@ -2668,13 +2782,13 @@ var SeatPicker = class _SeatPicker {
2668
2782
  }
2669
2783
  totalTicketCount() {
2670
2784
  const heldLabels = new Set((this.hold?.items ?? []).map((item) => item.label));
2671
- const freshSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).length;
2785
+ const freshSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).reduce((sum, seat) => sum + (seat.quantity ?? 1), 0);
2672
2786
  return this.heldTicketCount() + freshSeats + this.pendingGACount();
2673
2787
  }
2674
2788
  /** Held tickets and standing quantities consume the same order-wide cap. */
2675
2789
  updateSelectionCapacity() {
2676
2790
  const heldLabels = new Set((this.hold?.items ?? []).map((item) => item.label));
2677
- const selectedHeld = this.committedSelection().filter((seat) => heldLabels.has(seat.label)).length;
2791
+ const selectedHeld = this.committedSelection().filter((seat) => heldLabels.has(seat.label)).reduce((sum, seat) => sum + (seat.quantity ?? 1), 0);
2678
2792
  const remaining = Math.max(0, this.maxTickets - this.heldTicketCount() - this.pendingGACount());
2679
2793
  this.controller.setMaxSelection(selectedHeld + remaining);
2680
2794
  }
@@ -2701,9 +2815,9 @@ var SeatPicker = class _SeatPicker {
2701
2815
  cta.textContent = this.tf("picker.salesClosedCta", "Sales closed");
2702
2816
  return;
2703
2817
  }
2704
- if (this.confirmSeat) {
2818
+ if (this.confirmSeat || this.tableDialog && !this.tableDialogHeld) {
2705
2819
  cta.disabled = true;
2706
- cta.textContent = "Confirm or cancel this seat";
2820
+ cta.textContent = this.tableDialog ? "Confirm your table" : "Confirm or cancel this seat";
2707
2821
  return;
2708
2822
  }
2709
2823
  if (this.ctaPhase === "holding") {
@@ -3012,11 +3126,26 @@ var SeatPicker = class _SeatPicker {
3012
3126
  });
3013
3127
  }
3014
3128
  // ---- arena / multi-floor chrome -------------------------------------------
3015
- /** Build the rung pills (charts with sections) and floor switcher (>1 floor). */
3129
+ /** Build projection, rung and floor controls for arena-scale charts. */
3016
3130
  buildArenaChrome() {
3017
3131
  const doc = this.controller.doc;
3018
3132
  if (!doc || !this.els.map) return;
3019
3133
  const hasSections = doc.objects.some((o) => o.type === "section") || (doc.floors ?? []).some((f) => f.objects.some((o) => o.type === "section"));
3134
+ if (hasSections) {
3135
+ const projection = document.createElement("div");
3136
+ projection.className = "sl-projection";
3137
+ projection.setAttribute("role", "group");
3138
+ projection.setAttribute("aria-label", "Map projection");
3139
+ projection.innerHTML = '<button type="button" data-projection="flat" aria-pressed="false" title="Flat 2D map">2D</button><button type="button" data-projection="perspective" aria-pressed="false" title="Perspective 2.5D map">2.5D</button>';
3140
+ projection.querySelectorAll("button").forEach((button) => {
3141
+ button.addEventListener("click", () => {
3142
+ this.setViewMode(button.dataset.projection);
3143
+ });
3144
+ });
3145
+ this.regions["top-right"].appendChild(projection);
3146
+ this.projectionEl = projection;
3147
+ this.syncProjection();
3148
+ }
3020
3149
  if (hasSections) {
3021
3150
  const RUNGS = ["zones", "sections", "seats"];
3022
3151
  const pills = document.createElement("div");
@@ -3078,6 +3207,15 @@ var SeatPicker = class _SeatPicker {
3078
3207
  btn.setAttribute("aria-pressed", String(on));
3079
3208
  });
3080
3209
  }
3210
+ syncProjection() {
3211
+ if (!this.projectionEl) return;
3212
+ const active = this.controller.getViewMode();
3213
+ this.projectionEl.querySelectorAll("button").forEach((button) => {
3214
+ const on = button.dataset.projection === active;
3215
+ button.classList.toggle("on", on);
3216
+ button.setAttribute("aria-pressed", String(on));
3217
+ });
3218
+ }
3081
3219
  /** Reflect the active floor onto the switcher rail. */
3082
3220
  syncFloors() {
3083
3221
  if (!this.floorsEl) return;
@@ -3206,7 +3344,150 @@ var SeatPicker = class _SeatPicker {
3206
3344
  const status = this.controller.getStatus(seat.id) ?? "free";
3207
3345
  const statusText = status === "free" ? "available" : status === "held" ? "on hold" : "taken";
3208
3346
  const price = cat ? this.catPrice(cat) : void 0;
3209
- this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
3347
+ const table = this.controller.tableSelection(seat.id);
3348
+ const details = table ?? this.controller.seatDetails(seat.id);
3349
+ const typeWord = this.rowTypeWord(details);
3350
+ const buyerName = details?.rowLabel ?? details?.displayLabel ?? seat.displayLabel ?? seat.label;
3351
+ const identity = table ? `${typeWord} ${buyerName}, ${table.bookingMode === "variable" ? `${table.minOccupancy} to ${table.maxOccupancy} guests` : `${table.capacity} guests`}` : details?.objectType === "booth" ? `${typeWord} ${buyerName}` : details?.objectType === "table" ? `${typeWord} ${buyerName}, seat ${details.seatNumber ?? seat.label}` : `Seat ${details?.displayLabel ?? seat.displayLabel ?? seat.label}`;
3352
+ this.srEl.textContent = `${identity}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
3353
+ }
3354
+ // ---- atomic table confirmation / guest quantity ---------------------------
3355
+ showTableDialog(table, held, returnFocus) {
3356
+ this.dismissTableDialog(false);
3357
+ this.tableDialog = { ...table };
3358
+ this.tableDialogHeld = held;
3359
+ this.tableDialogReturnFocus = returnFocus ?? document.activeElement;
3360
+ const esc2 = (value) => String(value ?? "").replace(/[&<>"']/g, (ch) => ({
3361
+ "&": "&amp;",
3362
+ "<": "&lt;",
3363
+ ">": "&gt;",
3364
+ '"': "&quot;",
3365
+ "'": "&#39;"
3366
+ })[ch]);
3367
+ const cat = this.controller.doc?.categories.find((candidate) => candidate.key === table.categoryKey);
3368
+ const variable = table.bookingMode === "variable";
3369
+ const typeWord = this.rowTypeWord(table);
3370
+ const el = document.createElement("div");
3371
+ el.className = "sl-table-scrim";
3372
+ el.innerHTML = `<section class="sl-table-dialog" role="dialog" aria-modal="true" aria-labelledby="sl-table-title" aria-describedby="sl-table-copy"><div class="sl-table-head"><div class="sl-table-eyebrow">${esc2(variable ? `Flexible party \xB7 ${typeWord}` : `Whole ${typeWord}`)}</div><h2 class="sl-table-title" id="sl-table-title">${esc2(table.displayLabel ?? table.label)}</h2><p class="sl-table-copy" id="sl-table-copy">${variable ? `Choose how many guests will sit together. This table is held exclusively for your party.` : `All ${table.capacity} places are booked together as one exclusive table.`}</p></div><div class="sl-table-body"><div class="sl-table-summary"><span>${esc2(cat?.label ?? table.categoryKey)}</span><b data-table-unit>${this.money(this.paidPrice(table.categoryKey, table.tierId ?? null, table.price))} per guest</b><span class="muted">Table capacity</span><span>${table.capacity} guests</span><span class="muted">Total</span><b data-table-total></b></div>` + (variable ? `<label class="sl-table-qtylabel" id="sl-table-qty-label">Number of guests</label><div class="sl-table-stepper" role="group" aria-labelledby="sl-table-qty-label"><button type="button" data-table-step="-1" aria-label="Fewer guests">\u2212</button><output aria-live="polite" data-table-qty>${table.quantity}</output><button type="button" data-table-step="1" aria-label="More guests">+</button></div><div class="sl-table-range">Choose ${table.minOccupancy}\u2013${table.maxOccupancy} guests</div>` : `<input type="hidden" data-table-qty value="${table.capacity}">`) + `<div class="sl-table-actions"><button type="button" class="sl-table-cancel">Cancel</button><button type="button" class="sl-table-confirm">${held ? "Update table" : variable ? "Select table" : "Select whole table"}</button></div></div></section>`;
3373
+ this.root.appendChild(el);
3374
+ this.tableDialogEl = el;
3375
+ this.renderTableDialogState();
3376
+ el.querySelectorAll("[data-table-step]").forEach((button) => {
3377
+ button.addEventListener("click", () => {
3378
+ if (!this.tableDialog) return;
3379
+ const next = Math.max(
3380
+ this.tableDialog.minOccupancy,
3381
+ Math.min(this.tableDialog.maxOccupancy, this.tableDialog.quantity + Number(button.dataset.tableStep))
3382
+ );
3383
+ this.tableDialog = { ...this.tableDialog, quantity: next };
3384
+ this.renderTableDialogState();
3385
+ });
3386
+ });
3387
+ el.querySelector(".sl-table-cancel").addEventListener("click", () => this.cancelTableDialog());
3388
+ el.querySelector(".sl-table-confirm").addEventListener("click", () => void this.confirmTableDialog());
3389
+ el.addEventListener("mousedown", (event) => {
3390
+ if (event.target === el) this.cancelTableDialog();
3391
+ });
3392
+ el.addEventListener("keydown", (event) => {
3393
+ if (event.key !== "Tab") return;
3394
+ const focusable = [...el.querySelectorAll('button:not(:disabled),[tabindex]:not([tabindex="-1"])')];
3395
+ if (!focusable.length) return;
3396
+ const first = focusable[0];
3397
+ const last = focusable[focusable.length - 1];
3398
+ if (event.shiftKey && document.activeElement === first) {
3399
+ event.preventDefault();
3400
+ last.focus();
3401
+ } else if (!event.shiftKey && document.activeElement === last) {
3402
+ event.preventDefault();
3403
+ first.focus();
3404
+ }
3405
+ });
3406
+ requestAnimationFrame(() => el.querySelector(variable ? '[data-table-step="-1"]' : ".sl-table-confirm")?.focus());
3407
+ }
3408
+ renderTableDialogState() {
3409
+ const table = this.tableDialog;
3410
+ const el = this.tableDialogEl;
3411
+ if (!table || !el) return;
3412
+ const output = el.querySelector("output[data-table-qty]");
3413
+ if (output) output.value = String(table.quantity);
3414
+ const hidden = el.querySelector("input[data-table-qty]");
3415
+ if (hidden) hidden.value = String(table.quantity);
3416
+ el.querySelectorAll("[data-table-step]").forEach((button) => {
3417
+ const delta = Number(button.dataset.tableStep);
3418
+ button.disabled = delta < 0 ? table.quantity <= table.minOccupancy : table.quantity >= table.maxOccupancy;
3419
+ });
3420
+ const unit = this.paidPrice(table.categoryKey, table.tierId ?? null, table.price);
3421
+ const total = el.querySelector("[data-table-total]");
3422
+ if (total) total.textContent = this.money(unit * table.quantity);
3423
+ }
3424
+ async confirmTableDialog() {
3425
+ const table = this.tableDialog;
3426
+ const held = this.tableDialogHeld;
3427
+ if (!table) return;
3428
+ const button = this.tableDialogEl?.querySelector(".sl-table-confirm");
3429
+ if (button) {
3430
+ button.disabled = true;
3431
+ button.textContent = held ? "Updating\u2026" : "Selecting\u2026";
3432
+ }
3433
+ if (held) {
3434
+ try {
3435
+ const updated = await this.controller.replaceTableQuantity(table.label, table.quantity);
3436
+ if (!updated) {
3437
+ this.toast("That guest count could not be secured. Your current table hold is unchanged.", "warning");
3438
+ this.renderTableDialogState();
3439
+ if (button) {
3440
+ button.disabled = false;
3441
+ button.textContent = "Update table";
3442
+ }
3443
+ return;
3444
+ }
3445
+ this.hold = {
3446
+ holdId: updated.holdId,
3447
+ expiresAt: updated.expiresAt,
3448
+ seats: updated.seats,
3449
+ items: updated.items
3450
+ };
3451
+ this.startHoldTimer(updated.expiresAt);
3452
+ this.dismissTableDialog();
3453
+ this.syncTray();
3454
+ this.emitHoldChange();
3455
+ this.toast(`${table.label} updated for ${table.quantity} guests.`, "success");
3456
+ } catch (error) {
3457
+ this.opts.onError?.(error);
3458
+ this.toast("That guest count is no longer available. Your current hold is unchanged.", "error");
3459
+ if (button) {
3460
+ button.disabled = false;
3461
+ button.textContent = "Update table";
3462
+ }
3463
+ }
3464
+ return;
3465
+ }
3466
+ if (!this.controller.setTableQuantity(table.label, table.quantity)) {
3467
+ if (button) {
3468
+ button.disabled = false;
3469
+ button.textContent = table.bookingMode === "variable" ? "Select table" : "Select whole table";
3470
+ }
3471
+ return;
3472
+ }
3473
+ this.dismissTableDialog();
3474
+ this.collapseSectionCard();
3475
+ this.syncTray();
3476
+ }
3477
+ cancelTableDialog() {
3478
+ const table = this.tableDialog;
3479
+ const held = this.tableDialogHeld;
3480
+ this.dismissTableDialog();
3481
+ if (table && !held) this.controller.deselect(table.physicalSeatIds);
3482
+ }
3483
+ dismissTableDialog(restoreFocus = true) {
3484
+ const focus = this.tableDialogReturnFocus;
3485
+ this.tableDialogEl?.remove();
3486
+ this.tableDialogEl = null;
3487
+ this.tableDialog = null;
3488
+ this.tableDialogHeld = false;
3489
+ this.tableDialogReturnFocus = null;
3490
+ if (restoreFocus) requestAnimationFrame(() => (focus?.isConnected ? focus : this.root)?.focus());
3210
3491
  }
3211
3492
  // ---- seat candidate confirmation ------------------------------------------
3212
3493
  showConfirm(seat) {
@@ -3228,13 +3509,18 @@ var SeatPicker = class _SeatPicker {
3228
3509
  ">": "&gt;",
3229
3510
  '"': "&quot;"
3230
3511
  })[char]);
3512
+ const identityFields = [
3513
+ details?.sectionLabel ? `<div class="sl-confirm-field"><span class="sl-confirm-key">Section</span><span class="sl-confirm-value">${safe(details.sectionLabel)}</span></div>` : "",
3514
+ details?.rowLabel || details?.objectType === "booth" ? `<div class="sl-confirm-field"><span class="sl-confirm-key">${safe(this.rowTypeWord(details))}</span><span class="sl-confirm-value">${safe(this.rowShort(details) ?? details?.displayLabel ?? seat.displayLabel ?? seat.label)}</span></div>` : "",
3515
+ details?.objectType !== "booth" ? `<div class="sl-confirm-field"><span class="sl-confirm-key">Seat</span><span class="sl-confirm-value">${safe(details?.seatNumber ?? details?.displayLabel ?? seat.displayLabel ?? seat.label)}</span></div>` : ""
3516
+ ].filter(Boolean).join("");
3231
3517
  const el = document.createElement("div");
3232
3518
  el.className = "sl-confirm";
3233
3519
  el.setAttribute("role", "dialog");
3234
3520
  el.setAttribute("aria-modal", "true");
3235
3521
  el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
3236
3522
  el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
3237
- 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">${safe(this.rowTypeWord(details))}</span><span class="sl-confirm-value">${safe(this.rowShort(details))}</span></div><div class="sl-confirm-field"><span class="sl-confirm-key">Seat</span><span class="sl-confirm-value">${safe(details?.seatNumber ?? seat.displayLabel ?? 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.commercialConfirmHtml(seat.commercial) + (this.seatViewEnabled() ? this.confirmThumbHtml(seat) : "") + `<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>`;
3523
+ el.innerHTML = `<div class="sl-confirm-grid">` + identityFields + `</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.wheelchairConfirmHtml(details?.wheelchairSpaceType) + this.commercialConfirmHtml(seat.commercial) + (this.seatViewEnabled() ? this.confirmThumbHtml(seat) : "") + `<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>`;
3238
3524
  this.els.map.appendChild(el);
3239
3525
  this.confirmEl = el;
3240
3526
  this.reanchorConfirm();
@@ -3305,7 +3591,7 @@ var SeatPicker = class _SeatPicker {
3305
3591
  this.closeSeatView();
3306
3592
  const doc = this.controller.doc;
3307
3593
  const activeId = this.controller.getActiveFloorId();
3308
- const focal = doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
3594
+ const focal = seat.focalPoint ?? doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
3309
3595
  let panoUrl;
3310
3596
  let caption;
3311
3597
  let real = false;
@@ -3529,16 +3815,37 @@ var SeatPicker = class _SeatPicker {
3529
3815
  const noPicks = !seats.length && !heldItems.length && !this.pendingGACount();
3530
3816
  if (!this.hold && (noPicks || this.bestAvailableBusy || this.bestAvailableConfirm)) {
3531
3817
  const cats = this.controller.doc?.categories ?? [];
3818
+ const zones = this.controller.getBestAvailableZones();
3819
+ if (this.baZone && !zones.some((zone) => zone.id === this.baZone)) this.baZone = "";
3532
3820
  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>` + // Premium quick-pick — present only when the chart actually has premium
3533
3821
  // seats (same present-only philosophy as the a11y filter chips).
3534
- (this.controller.hasPremiumSeats() ? `<button type="button" class="sl-ba-premium${this.baPremium ? " on" : ""}" data-ba-premium aria-pressed="${this.baPremium ? "true" : "false"}"><span class="star" aria-hidden="true">\u2605</span>${this.tf("picker.bestSeatsPremium", "Best seats")}</button>` : "") + (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>`);
3535
- }
3536
- const idGrid = (seatId, label) => {
3822
+ (this.controller.hasPremiumSeats() ? `<button type="button" class="sl-ba-premium${this.baPremium ? " on" : ""}" data-ba-premium aria-pressed="${this.baPremium ? "true" : "false"}"><span class="star" aria-hidden="true">\u2605</span>${this.tf("picker.bestSeatsPremium", "Best seats")}</button>` : "") + (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>`) + (zones.length ? `<select aria-label="Preferred venue zone" data-ba-zone><option value="">Any venue zone</option>` + zones.map((zone) => `<option value="${escapeOption(zone.id)}"${this.baZone === zone.id ? " selected" : ""}>${escapeOption(zone.label)}</option>`).join("") + `</select>` : "") + `<div class="sl-ba-qty"><button type="button" data-ba="-1" aria-label="Fewer seats">\u2212</button><span>${this.baQty}</span><button type="button" data-ba="1" aria-label="More seats">+</button></div><button type="button" class="sl-ba-go"${this.bestAvailableBusy ? " disabled" : ""}>` + (this.bestAvailableBusy ? `<span class="sl-ba-spin" aria-hidden="true"></span>Finding the best seats\u2026` : `Find ${this.baQty} best ${this.baQty === 1 ? "seat" : "seats"}`) + `</button></div>`);
3823
+ }
3824
+ const idGrid = (seatId, label, objectType, quantity = 1, objectId, identity) => {
3825
+ const esc2 = (value) => String(value ?? "\u2014").replace(/[&<>"]/g, (char) => ({
3826
+ "&": "&amp;",
3827
+ "<": "&lt;",
3828
+ ">": "&gt;",
3829
+ '"': "&quot;"
3830
+ })[char]);
3537
3831
  const d = seatId ? this.controller.seatDetails(seatId) : null;
3832
+ const area = objectType === "ga" ? gaAreas.find((candidate) => candidate.id === objectId) : void 0;
3833
+ const effectiveType = objectType === "ga" ? "ga" : d?.objectType ?? objectType;
3834
+ const typeWord = identity?.displayType?.trim() || d?.displayType?.trim() || area?.displayType?.trim() || (effectiveType === "table" ? "Table" : effectiveType === "booth" ? "Booth" : effectiveType === "ga" ? "General admission" : "Row");
3835
+ const buyerName = identity?.rowLabel ?? identity?.displayLabel ?? d?.rowLabel ?? d?.displayLabel ?? area?.displayLabel ?? area?.label ?? label;
3836
+ if (effectiveType === "table" && identity?.bookingMode) {
3837
+ return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">${esc2(typeWord)}</span><span class="val">${esc2(buyerName)}</span></span><span class="fld mid"><span class="sl-chip-eb">Guests</span><span class="val">${quantity}</span></span></div>`;
3838
+ }
3839
+ if (effectiveType === "ga") {
3840
+ return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">${esc2(typeWord)}</span><span class="val">${esc2(buyerName)}</span></span>` + (quantity > 1 ? `<span class="fld mid"><span class="sl-chip-eb">Tickets</span><span class="val">${quantity}</span></span>` : "") + `</div>`;
3841
+ }
3842
+ if (effectiveType === "booth") {
3843
+ return `<div class="sl-chip-id">` + (d?.sectionLabel ? `<span class="fld sec"><span class="sl-chip-eb">Section</span><span class="val">${esc2(d.sectionLabel)}</span></span>` : "") + `<span class="fld mid"><span class="sl-chip-eb">${esc2(typeWord)}</span><span class="val">${esc2(buyerName)}</span></span></div>`;
3844
+ }
3538
3845
  if (!d?.sectionLabel && !d?.rowLabel && !d?.seatNumber) {
3539
- return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">Seat</span><span class="val">${label}</span></span></div>`;
3846
+ return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">Seat</span><span class="val">${esc2(buyerName)}</span></span></div>`;
3540
3847
  }
3541
- return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">Section</span><span class="val">${d.sectionLabel ?? "\u2014"}</span></span>` + (d.rowLabel ? `<span class="fld mid"><span class="sl-chip-eb">${this.rowTypeWord(d)}</span><span class="val">${this.rowShort(d)}</span></span>` : "") + (d.seatNumber ? `<span class="fld mid"><span class="sl-chip-eb">Seat</span><span class="val">${d.seatNumber}</span></span>` : "") + `</div>`;
3848
+ return `<div class="sl-chip-id">` + (d.sectionLabel ? `<span class="fld sec"><span class="sl-chip-eb">Section</span><span class="val">${esc2(d.sectionLabel)}</span></span>` : "") + (d.rowLabel ? `<span class="fld mid"><span class="sl-chip-eb">${esc2(typeWord)}</span><span class="val">${esc2(this.rowShort(d))}</span></span>` : "") + (d.seatNumber ? `<span class="fld mid"><span class="sl-chip-eb">Seat</span><span class="val">${esc2(d.seatNumber)}</span></span>` : "") + `</div>`;
3542
3849
  };
3543
3850
  const iconRail = (rmAria, viewLabel) => `<div class="sl-chip-rail"><button type="button" class="rm" aria-label="${rmAria}"><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>` + (viewLabel ? `<button type="button" class="view" data-view-label="${viewLabel}" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: viewLabel })}"><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>`;
3544
3851
  for (const item of heldItems) {
@@ -3547,9 +3854,10 @@ var SeatPicker = class _SeatPicker {
3547
3854
  const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
3548
3855
  const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
3549
3856
  const heldSeat = item.objectType !== "ga" ? this.controller.seatByLabel(item.label) : null;
3550
- const canView2 = this.seatViewEnabled() && !!heldSeat;
3857
+ const table = item.objectType === "table" ? this.controller.tableSelection(item.label) : null;
3858
+ const canView2 = this.seatViewEnabled() && !!heldSeat && item.objectType !== "table";
3551
3859
  parts.push(
3552
- `<div class="sl-chip sl-held${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-held="${encodeURIComponent(item.label)}"${heldSeat ? ` data-locate="${heldSeat.id}"` : ""}><div class="sl-chip-main">` + idGrid(heldSeat?.id ?? null, item.label) + `<div class="sl-chip-sub"><span class="sl-ticket-state held" aria-label="Held for you" title="Held for you"><svg viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg></span><span class="cat">${cat?.label ?? item.categoryKey}${tierName ? ` \xB7 ${tierName}` : ""}</span>` + this.commercialChipMarker(heldSeat?.commercial) + `<span class="amt">${this.money(this.paidPrice(item.categoryKey, item.tierId, item.unitPrice) * (item.quantity ?? 1))}</span></div></div>` + iconRail(`Remove held ticket ${item.label}`, canView2 ? item.label : null) + `</div>`
3860
+ `<div class="sl-chip sl-held${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-held="${encodeURIComponent(item.label)}"${heldSeat ? ` data-locate="${heldSeat.id}"` : ""}><div class="sl-chip-main">` + idGrid(heldSeat?.id ?? null, item.label, item.objectType, item.quantity ?? 1, item.objectId, table ?? void 0) + `<div class="sl-chip-sub"><span class="sl-ticket-state held" aria-label="Held for you" title="Held for you"><svg viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg></span><span class="cat">${cat?.label ?? item.categoryKey}${tierName ? ` \xB7 ${tierName}` : ""}</span>` + (table?.bookingMode === "variable" ? `<button type="button" class="sl-table-edit" data-table-edit="${encodeURIComponent(item.label)}">${item.quantity ?? 1} guests \xB7 Edit</button>` : "") + this.wheelchairChipMarker(heldSeat?.wheelchairSpaceType) + this.commercialChipMarker(heldSeat?.commercial) + `<span class="amt">${this.money(this.paidPrice(item.categoryKey, item.tierId, item.unitPrice) * (item.quantity ?? 1))}</span></div></div>` + iconRail(`Remove held ticket ${item.label}`, canView2 ? item.label : null) + `</div>`
3553
3861
  );
3554
3862
  }
3555
3863
  const heldLabels = new Set(heldItems.map((item) => item.label));
@@ -3560,13 +3868,13 @@ var SeatPicker = class _SeatPicker {
3560
3868
  const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
3561
3869
  const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${(0, import_core2.t)("picker.ticketTierFor", { label: s.label })}">` + s.tiers.map((ti) => `<option value="${ti.id}"${ti.id === s.tierId ? " selected" : ""}>${ti.name} \xB7 ${this.money(this.paidPrice(s.categoryKey, ti.id, ti.price))}</option>`).join("") + `</select>` : "";
3562
3870
  parts.push(
3563
- `<div class="sl-chip${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-seat="${s.id}" data-locate="${s.id}"><div class="sl-chip-main">` + idGrid(s.id, s.displayLabel ?? s.label) + `<div class="sl-chip-sub"><span class="sl-ticket-state" aria-label="Selected" title="Selected"><svg viewBox="0 0 24 24"><path d="M5 12l4 4L19 6"/></svg></span><span class="cat">${cat?.label ?? s.categoryKey}</span>${this.commercialChipMarker(s.commercial)}${tierSelect}<span class="amt">${this.money(this.paidPrice(s.categoryKey, s.tierId ?? null, s.price))}</span></div></div>` + iconRail(`Remove ${s.label}`, canView ? s.label : null) + `</div>`
3871
+ `<div class="sl-chip${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-seat="${s.id}" data-locate="${s.id}"><div class="sl-chip-main">` + idGrid(s.id, s.label, s.objectType, s.quantity ?? 1, s.objectId, s) + `<div class="sl-chip-sub"><span class="sl-ticket-state" aria-label="Selected" title="Selected"><svg viewBox="0 0 24 24"><path d="M5 12l4 4L19 6"/></svg></span><span class="cat">${cat?.label ?? s.categoryKey}</span>` + (s.objectType === "table" && s.bookingMode === "variable" ? `<button type="button" class="sl-table-edit" data-table-edit="${encodeURIComponent(s.label)}">${s.quantity ?? 1} guests \xB7 Edit</button>` : "") + `${this.wheelchairChipMarker(s.wheelchairSpaceType)}${this.commercialChipMarker(s.commercial)}${tierSelect}<span class="amt">${this.money(this.paidPrice(s.categoryKey, s.tierId ?? null, s.price) * (s.quantity ?? 1))}</span></div></div>` + iconRail(`Remove ${s.label}`, canView && s.objectType !== "table" ? s.label : null) + `</div>`
3564
3872
  );
3565
3873
  }
3566
3874
  for (const area of gaAreas) {
3567
3875
  const qty = this.gaQty.get(area.id) ?? 0;
3568
3876
  parts.push(
3569
- `<div class="sl-ga" data-ga="${area.id}"><div class="sl-ga-info"><div class="sl-ga-name">${area.label}</div><div class="sl-ga-sub">${this.money(this.paidPrice(area.categoryKey, null, area.price))} \xB7 ${area.available} left</div></div><div class="sl-ga-qty"><button type="button" data-d="-1" aria-label="Fewer">\u2212</button><span>${qty}</span><button type="button" data-d="1" aria-label="More">+</button></div></div>`
3877
+ `<div class="sl-ga" data-ga="${area.id}"><div class="sl-ga-info"><div class="sl-ga-name">${area.displayLabel ?? area.label}</div><div class="sl-ga-sub">${area.displayType ?? "General admission"} \xB7 ${this.money(this.paidPrice(area.categoryKey, null, area.price))} \xB7 ${area.available} left</div></div><div class="sl-ga-qty"><button type="button" data-d="-1" aria-label="Fewer">\u2212</button><span>${qty}</span><button type="button" data-d="1" aria-label="More">+</button></div></div>`
3570
3878
  );
3571
3879
  }
3572
3880
  this.els.tray.innerHTML = parts.join("");
@@ -3580,6 +3888,9 @@ var SeatPicker = class _SeatPicker {
3580
3888
  this.els.tray.querySelector("[data-ba-cat]")?.addEventListener("change", (e) => {
3581
3889
  this.baCat = e.target.value;
3582
3890
  });
3891
+ this.els.tray.querySelector("[data-ba-zone]")?.addEventListener("change", (e) => {
3892
+ this.baZone = e.target.value;
3893
+ });
3583
3894
  this.els.tray.querySelector("[data-ba-premium]")?.addEventListener("click", () => {
3584
3895
  this.baPremium = !this.baPremium;
3585
3896
  this.syncTray();
@@ -3591,7 +3902,7 @@ var SeatPicker = class _SeatPicker {
3591
3902
  this.els.tray.querySelector("[data-ba-replace]")?.focus();
3592
3903
  return;
3593
3904
  }
3594
- void this.bestAvailable(this.baQty, this.baCat || void 0, { preferPremium: this.baPremium });
3905
+ void this.bestAvailable(this.baQty, this.baCat || void 0, { preferPremium: this.baPremium, zoneId: this.baZone || void 0 });
3595
3906
  });
3596
3907
  this.els.tray.querySelector("[data-ba-cancel]")?.addEventListener("click", () => {
3597
3908
  this.bestAvailableConfirm = false;
@@ -3600,7 +3911,7 @@ var SeatPicker = class _SeatPicker {
3600
3911
  });
3601
3912
  this.els.tray.querySelector("[data-ba-replace]")?.addEventListener("click", () => {
3602
3913
  this.bestAvailableConfirm = false;
3603
- void this.bestAvailable(this.baQty, this.baCat || void 0, { preferPremium: this.baPremium });
3914
+ void this.bestAvailable(this.baQty, this.baCat || void 0, { preferPremium: this.baPremium, zoneId: this.baZone || void 0 });
3604
3915
  });
3605
3916
  this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
3606
3917
  btn.addEventListener("click", () => {
@@ -3632,6 +3943,20 @@ var SeatPicker = class _SeatPicker {
3632
3943
  this.scheduleMotion(remove, 150);
3633
3944
  });
3634
3945
  });
3946
+ this.els.tray.querySelectorAll("[data-table-edit]").forEach((button) => {
3947
+ button.addEventListener("click", (event) => {
3948
+ event.stopPropagation();
3949
+ const label = decodeURIComponent(button.dataset.tableEdit ?? "");
3950
+ const details = this.controller.tableSelection(label);
3951
+ if (!details) return;
3952
+ const heldItem = heldItems.find((item) => item.label === label && item.objectType === "table");
3953
+ this.showTableDialog(
3954
+ { ...details, quantity: heldItem?.quantity ?? details.quantity },
3955
+ !!heldItem,
3956
+ button
3957
+ );
3958
+ });
3959
+ });
3635
3960
  this.els.tray.querySelectorAll(".sl-chip .tier").forEach((sel) => {
3636
3961
  sel.addEventListener("change", () => this.controller.setSeatTier(sel.dataset.tier, sel.value || null));
3637
3962
  });
@@ -3659,7 +3984,7 @@ var SeatPicker = class _SeatPicker {
3659
3984
  });
3660
3985
  });
3661
3986
  if (this.salesClosed) {
3662
- this.els.tray.querySelectorAll(".sl-ba-go,[data-ba],[data-ba-cat],[data-ba-replace],.sl-ga button").forEach((el) => {
3987
+ this.els.tray.querySelectorAll(".sl-ba-go,[data-ba],[data-ba-cat],[data-ba-zone],[data-ba-replace],.sl-ga button").forEach((el) => {
3663
3988
  el.disabled = true;
3664
3989
  });
3665
3990
  }
@@ -3668,8 +3993,11 @@ var SeatPicker = class _SeatPicker {
3668
3993
  const heldTotal = heldItems.reduce((sum, item) => sum + this.paidPrice(item.categoryKey, item.tierId, item.unitPrice) * (item.quantity ?? 1), 0);
3669
3994
  const heldCount = heldItems.reduce((sum, item) => sum + (item.quantity ?? 1), 0);
3670
3995
  const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
3671
- const total = freshSeats.reduce((sum, s) => sum + this.paidPrice(s.categoryKey, s.tierId ?? null, s.price), 0) + gaTotal + heldTotal;
3672
- const count = freshSeats.length + gaCount + heldCount;
3996
+ const total = freshSeats.reduce(
3997
+ (sum, s) => sum + this.paidPrice(s.categoryKey, s.tierId ?? null, s.price) * (s.quantity ?? 1),
3998
+ 0
3999
+ ) + gaTotal + heldTotal;
4000
+ const count = freshSeats.reduce((sum, seat) => sum + (seat.quantity ?? 1), 0) + gaCount + heldCount;
3673
4001
  const pendingCount = this.pendingSelectionCount();
3674
4002
  const previousCount = this.lastTrayCount;
3675
4003
  const previousTotal = this.lastTrayTotal;
@@ -3915,16 +4243,21 @@ var SeatPicker = class _SeatPicker {
3915
4243
  /** Assemble the stable {@link CheckoutHandoff} from a hold's server line items. */
3916
4244
  buildHandoff(hold) {
3917
4245
  const items = hold.items ?? [];
3918
- const lineItems = items.map((it) => ({
3919
- label: it.label,
3920
- objectId: it.objectId,
3921
- objectType: it.objectType,
3922
- categoryKey: it.categoryKey,
3923
- tierId: it.tierId,
3924
- unitPrice: this.paidPrice(it.categoryKey, it.tierId, it.unitPrice),
3925
- currency: it.currency ?? this.currency,
3926
- quantity: it.quantity ?? 1
3927
- }));
4246
+ const lineItems = items.map((it) => {
4247
+ const display = this.controller.lineItemDisplay(it);
4248
+ return {
4249
+ label: it.label,
4250
+ ...display.displayLabel ? { displayLabel: display.displayLabel } : {},
4251
+ ...display.displayType ? { displayType: display.displayType } : {},
4252
+ objectId: it.objectId,
4253
+ objectType: it.objectType,
4254
+ categoryKey: it.categoryKey,
4255
+ tierId: it.tierId,
4256
+ unitPrice: this.paidPrice(it.categoryKey, it.tierId, it.unitPrice),
4257
+ currency: it.currency ?? this.currency,
4258
+ quantity: it.quantity ?? 1
4259
+ };
4260
+ });
3928
4261
  const currency = lineItems[0]?.currency ?? this.currency;
3929
4262
  const total = lineItems.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
3930
4263
  return { holdId: hold.holdId, expiresAt: hold.expiresAt, currency, lineItems, total };
@@ -3985,8 +4318,11 @@ var SeatPicker = class _SeatPicker {
3985
4318
  /** Buyer-facing type word for the row/table key label — the designer's
3986
4319
  * per-object "Displayed type" override, or the default "Row". */
3987
4320
  rowTypeWord(details) {
3988
- const t3 = details?.rowType?.trim();
3989
- return t3 || "Row";
4321
+ const authored = details?.displayType?.trim() || details?.rowType?.trim();
4322
+ if (authored) return authored;
4323
+ if (details?.objectType === "table") return "Table";
4324
+ if (details?.objectType === "booth") return "Booth";
4325
+ return "Row";
3990
4326
  }
3991
4327
  rowShort(details) {
3992
4328
  const row = details?.rowLabel;
@@ -4006,13 +4342,17 @@ var SeatPicker = class _SeatPicker {
4006
4342
  }
4007
4343
  const esc2 = (v) => String(v ?? "\u2014").replace(/[&<>"]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch]);
4008
4344
  const price = this.money(this.paidPrice(details.categoryKey, details.tierId ?? null, details.price));
4345
+ const isGroupedTable = details.objectType === "table" && !!details.bookingMode;
4346
+ const isBooth = details.objectType === "booth";
4009
4347
  const hasLoc = details.sectionLabel || details.rowLabel || details.seatNumber;
4010
- const grid = hasLoc ? `<div class="sl-tip-grid"><div class="sl-tip-field"><span class="sl-tip-key">Section</span><span class="sl-tip-val">${esc2(details.sectionLabel)}</span></div><div class="sl-tip-field"><span class="sl-tip-key">${esc2(this.rowTypeWord(details))}</span><span class="sl-tip-val">${esc2(this.rowShort(details))}</span></div><div class="sl-tip-field"><span class="sl-tip-key">Seat</span><span class="sl-tip-val">${esc2(details.seatNumber ?? details.displayLabel ?? details.label)}</span></div></div>` : `<div class="sl-tip-grid one"><div class="sl-tip-field"><span class="sl-tip-key">Seat</span><span class="sl-tip-val">${esc2(details.displayLabel ?? details.label)}</span></div></div>`;
4348
+ const grid = isGroupedTable ? `<div class="sl-tip-grid"><div class="sl-tip-field"><span class="sl-tip-key">${esc2(this.rowTypeWord(details))}</span><span class="sl-tip-val">${esc2(details.rowLabel ?? details.displayLabel ?? details.label)}</span></div><div class="sl-tip-field"><span class="sl-tip-key">Guests</span><span class="sl-tip-val">${details.bookingMode === "variable" ? `${details.minOccupancy}\u2013${details.maxOccupancy}` : details.capacity}</span></div></div>` : isBooth ? `<div class="sl-tip-grid">` + (details.sectionLabel ? `<div class="sl-tip-field"><span class="sl-tip-key">Section</span><span class="sl-tip-val">${esc2(details.sectionLabel)}</span></div>` : "") + `<div class="sl-tip-field"><span class="sl-tip-key">${esc2(this.rowTypeWord(details))}</span><span class="sl-tip-val">${esc2(details.rowLabel ?? details.displayLabel ?? details.label)}</span></div></div>` : hasLoc ? `<div class="sl-tip-grid">` + (details.sectionLabel ? `<div class="sl-tip-field"><span class="sl-tip-key">Section</span><span class="sl-tip-val">${esc2(details.sectionLabel)}</span></div>` : "") + (details.rowLabel ? `<div class="sl-tip-field"><span class="sl-tip-key">${esc2(this.rowTypeWord(details))}</span><span class="sl-tip-val">${esc2(this.rowShort(details))}</span></div>` : "") + (details.seatNumber ? `<div class="sl-tip-field"><span class="sl-tip-key">Seat</span><span class="sl-tip-val">${esc2(details.seatNumber)}</span></div>` : "") + `</div>` : `<div class="sl-tip-grid one"><div class="sl-tip-field"><span class="sl-tip-key">Seat</span><span class="sl-tip-val">${esc2(details.displayLabel ?? details.label)}</span></div></div>`;
4011
4349
  const statusLine = details.status === "free" ? "" : `<div class="sl-tip-status">${details.status === "held" ? (0, import_core2.t)("map.statusHeld") : (0, import_core2.t)("map.statusTaken")}</div>`;
4012
4350
  const limited = this.limitedViewLabel(details.commercial);
4013
4351
  const cxLine = limited ? `<div class="sl-tip-cx"><span class="g" aria-hidden="true">\u25D0</span>${esc2(limited)}</div>` : "";
4352
+ const wheelchair = this.wheelchairProvisionLabel(details.wheelchairSpaceType);
4353
+ const wheelchairLine = wheelchair ? `<div class="sl-tip-cx"><span class="g" aria-hidden="true">\u267F</span>${esc2(wheelchair)}</div>` : "";
4014
4354
  this.tipEl.style.setProperty("--sl-cat", details.categoryColor);
4015
- this.tipEl.innerHTML = grid + `<div class="sl-tip-cat"><span class="sl-tip-dot" style="background:${details.categoryColor}"></span><span class="sl-tip-name">${esc2(details.categoryLabel)}</span><span class="sl-tip-amt">${price}</span></div>` + cxLine + statusLine;
4355
+ this.tipEl.innerHTML = grid + `<div class="sl-tip-cat"><span class="sl-tip-dot" style="background:${details.categoryColor}"></span><span class="sl-tip-name">${esc2(details.categoryLabel)}</span><span class="sl-tip-amt">${price}</span></div>` + wheelchairLine + cxLine + statusLine;
4016
4356
  this.tipEl.style.display = "block";
4017
4357
  this.placeTooltip();
4018
4358
  }
@@ -4020,6 +4360,33 @@ var SeatPicker = class _SeatPicker {
4020
4360
  getSelection() {
4021
4361
  return this.committedSelection();
4022
4362
  }
4363
+ /** Current colorblind-safe render state, resolved from the stored buyer
4364
+ * preference at mount. Host chrome (e.g. the Designer preview) reads this to
4365
+ * surface the state rather than rendering colorblind colors silently. */
4366
+ isColorblindSafe() {
4367
+ return this.cbSafe;
4368
+ }
4369
+ /** Single source of truth for the colorblind-safe toggle — used by both the
4370
+ * in-widget button and host chrome. Updates the renderer, the persisted
4371
+ * cross-surface preference, and the in-widget button's pressed state together
4372
+ * so the two controls never diverge. */
4373
+ setColorblindSafe(on) {
4374
+ if (on === this.cbSafe) return;
4375
+ this.cbSafe = on;
4376
+ this.cbEl?.setAttribute("aria-pressed", String(on));
4377
+ this.controller.setColorblindSafe(on);
4378
+ writeStoredColorblind(on);
4379
+ }
4380
+ /** Switch the buyer canvas between flat, isometric and Perspective 2.5D. */
4381
+ setViewMode(mode) {
4382
+ this.controller.setViewMode(mode);
4383
+ this.syncProjection();
4384
+ this.dismissConfirm();
4385
+ }
4386
+ /** Current buyer canvas projection. */
4387
+ getViewMode() {
4388
+ return this.controller.getViewMode();
4389
+ }
4023
4390
  /** Current active/restored hold reflected in the tray. */
4024
4391
  getCurrentHold() {
4025
4392
  return this.hold;
@@ -4109,6 +4476,7 @@ var SeatPicker = class _SeatPicker {
4109
4476
  this.destroyed = true;
4110
4477
  if (this.hold && !this.handedOff) void this.controller.release();
4111
4478
  this.closeConfirm();
4479
+ this.dismissTableDialog(false);
4112
4480
  this.closeSeatView();
4113
4481
  this.stopHoldTimer();
4114
4482
  if (this.toastTimer) clearTimeout(this.toastTimer);
@@ -4122,6 +4490,7 @@ var SeatPicker = class _SeatPicker {
4122
4490
  if (this.fsChangeHandler) document.removeEventListener("fullscreenchange", this.fsChangeHandler);
4123
4491
  if (this.fsEscHandler) window.removeEventListener("keydown", this.fsEscHandler);
4124
4492
  this.controller.destroy();
4493
+ this.projectionEl = null;
4125
4494
  this.root?.remove();
4126
4495
  this.root = null;
4127
4496
  if (this.modalScrim) {