@seatlayer/js 0.27.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.js CHANGED
@@ -1035,6 +1035,7 @@ var EmbeddedDesigner = class {
1035
1035
  // src/SeatPicker.ts
1036
1036
  import {
1037
1037
  PickerController as PickerController2,
1038
+ ACCESSIBILITY_TYPES,
1038
1039
  expandChart,
1039
1040
  generateSeatPanorama,
1040
1041
  generateSeatThumb,
@@ -1065,6 +1066,15 @@ function resolveContainer3(container) {
1065
1066
  }
1066
1067
  return container;
1067
1068
  }
1069
+ function escapeOption(value) {
1070
+ return String(value ?? "").replace(/[&<>"']/g, (character) => ({
1071
+ "&": "&amp;",
1072
+ "<": "&lt;",
1073
+ ">": "&gt;",
1074
+ '"': "&quot;",
1075
+ "'": "&#39;"
1076
+ })[character]);
1077
+ }
1068
1078
  var STYLE_ID = "seatlayer-picker-style";
1069
1079
  var CSS = `
1070
1080
  .sl-picker{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:420px;overflow:hidden;
@@ -1176,6 +1186,11 @@ var CSS = `
1176
1186
  .sl-picker[data-layout="narrow"] .sl-filters.has{display:none}
1177
1187
  .sl-picker[data-layout="narrow"][data-has-selection="true"] .sl-filtersec.has,
1178
1188
  .sl-picker[data-layout="narrow"][data-has-selection="true"] .sl-filters.has{display:none}
1189
+ /* Accessibility and colour-safety controls must remain reachable on phones.
1190
+ Keep them out of the 86px peek, then reveal their consolidated row whenever
1191
+ the buyer explicitly opens the ticket panel. */
1192
+ .sl-picker[data-layout="narrow"][data-sheet="open"] .sl-filtersec.has{display:block!important}
1193
+ .sl-picker[data-layout="narrow"][data-sheet="open"] .sl-filters.has{display:flex!important}
1179
1194
  .sl-cbbtn{width:32px;height:32px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
1180
1195
  color:var(--sl-text);display:flex;align-items:center;justify-content:center;transition:border-color .15s}
1181
1196
  .sl-cbbtn:hover{border-color:var(--sl-muted)}
@@ -1468,6 +1483,37 @@ var CSS = `
1468
1483
  .sl-picker[data-layout="narrow"] .sl-confirm{left:50%!important;top:auto!important;bottom:14px;width:min(342px,calc(100% - 24px));
1469
1484
  transform:translateX(-50%);animation:slConfirmMobileIn .24s cubic-bezier(.2,.8,.2,1) both}
1470
1485
 
1486
+ /* Atomic whole/variable-table chooser. Lives inside the widget so the same
1487
+ accessible dialog works in inline, modal, desktop and 390px mobile hosts. */
1488
+ .sl-table-scrim{position:absolute;inset:0;z-index:45;display:flex;align-items:center;justify-content:center;padding:18px;
1489
+ background:color-mix(in srgb,var(--sl-bg) 66%,transparent);backdrop-filter:blur(3px)}
1490
+ .sl-table-dialog{width:min(408px,100%);max-height:calc(100% - 24px);overflow:auto;border:1px solid var(--sl-line);
1491
+ border-radius:calc(var(--sl-radius) * 1.15);background:var(--sl-surface);box-shadow:0 28px 70px rgba(0,0,0,.42)}
1492
+ .sl-table-head{padding:18px 18px 14px;border-bottom:1px solid var(--sl-line)}
1493
+ .sl-table-eyebrow{font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
1494
+ .sl-table-title{margin-top:5px;font-size:22px;line-height:1.15;font-weight:850}
1495
+ .sl-table-copy{margin-top:7px;color:var(--sl-muted);font-size:13px;line-height:1.45}
1496
+ .sl-table-body{padding:16px 18px 18px}
1497
+ .sl-table-summary{display:grid;grid-template-columns:1fr auto;gap:8px 16px;padding:12px;border:1px solid var(--sl-line);
1498
+ border-radius:var(--sl-r-sm);background:color-mix(in srgb,var(--sl-line) 22%,transparent);font-size:13px}
1499
+ .sl-table-summary b{font-size:15px}.sl-table-summary .muted{color:var(--sl-muted)}
1500
+ .sl-table-qtylabel{display:block;margin:16px 0 8px;font-size:12px;font-weight:800}
1501
+ .sl-table-stepper{display:grid;grid-template-columns:48px 1fr 48px;align-items:center;border:1px solid var(--sl-line);
1502
+ border-radius:12px;overflow:hidden;background:var(--sl-bg)}
1503
+ .sl-table-stepper button{height:48px;font-size:24px;font-weight:700;background:color-mix(in srgb,var(--sl-line) 34%,transparent)!important}
1504
+ .sl-table-stepper button:disabled{opacity:.38;cursor:not-allowed}
1505
+ .sl-table-stepper output{text-align:center;font-size:19px;font-weight:850;font-variant-numeric:tabular-nums}
1506
+ .sl-table-range{margin-top:7px;color:var(--sl-muted);font-size:11px;text-align:center}
1507
+ .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}
1508
+ .sl-table-cancel{border:1px solid var(--sl-line)!important;color:var(--sl-muted)!important}
1509
+ .sl-table-confirm{background:var(--sl-accent)!important;color:var(--sl-accent-ink)!important}
1510
+ .sl-table-confirm:disabled{opacity:.62;cursor:wait}
1511
+ .sl-table-edit{margin-left:4px;padding:2px 7px!important;border:1px solid var(--sl-line)!important;border-radius:999px!important;
1512
+ color:var(--sl-muted)!important;font-size:10px!important;font-weight:800!important}
1513
+ .sl-picker[data-layout="narrow"] .sl-table-scrim{align-items:flex-end;padding:0;background:rgba(5,7,12,.58)}
1514
+ .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}
1515
+ .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))}
1516
+
1471
1517
  /* hover preview \u2014 a COMPACT echo of the confirm card (deliberately smaller: it's
1472
1518
  a passing preview on hover, not the click/select action surface). Reuses the
1473
1519
  Section\xB7Row\xB7Seat identity grid so hover, confirm and the cart chip all share
@@ -1531,6 +1577,7 @@ var CSS = `
1531
1577
  .sl-picker[data-layout="narrow"] .sl-ba{padding:11px}
1532
1578
  .sl-picker[data-layout="narrow"] .sl-ba-copy .wide{display:none}
1533
1579
  .sl-picker[data-layout="narrow"] .sl-ba-copy .narrow{display:inline}
1580
+ .sl-picker[data-layout="narrow"] .sl-ba select{min-height:44px}
1534
1581
 
1535
1582
  /* screen-reader live region */
1536
1583
  .sl-sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
@@ -1549,6 +1596,16 @@ var CSS = `
1549
1596
  /* narrow: shrink the rung pills so the centered row can't reach the corner regions */
1550
1597
  .sl-picker[data-layout="narrow"] .sl-rungs button{padding:5px 9px;font-size:9px;letter-spacing:.03em}
1551
1598
 
1599
+ /* Projection is deliberately a separate control from zoom/LOD. Perspective
1600
+ changes geometry; the two choices stay explicit and keyboard-native. */
1601
+ .sl-projection{display:inline-flex;align-items:center;gap:2px;padding:3px;border-radius:999px;
1602
+ background:var(--sl-surface);border:1px solid var(--sl-line);box-shadow:0 8px 24px -16px rgba(0,0,0,.65)}
1603
+ .sl-projection button{min-width:42px;min-height:30px;padding:5px 10px;border-radius:999px;color:var(--sl-muted);
1604
+ font-size:10px;font-weight:800;letter-spacing:.04em;white-space:nowrap}
1605
+ .sl-projection button:hover,.sl-projection button:focus-visible{color:var(--sl-text)}
1606
+ .sl-projection button.on{background:var(--sl-accent);color:var(--sl-accent-ink)}
1607
+ .sl-picker[data-layout="narrow"] .sl-projection button{min-width:38px;min-height:32px;padding:5px 8px;font-size:9.5px}
1608
+
1552
1609
  /* multi-floor switcher (flows within the left-rail region) */
1553
1610
  .sl-floors{display:none;flex-direction:column;gap:6px;max-width:100%}
1554
1611
  .sl-floors.on{display:flex}
@@ -1677,6 +1734,7 @@ var CSS = `
1677
1734
  .sl-picker *,.sl-modal-scrim *{animation-duration:.001ms!important;animation-iteration-count:1!important;
1678
1735
  transition-duration:.001ms!important;scroll-behavior:auto!important}
1679
1736
  }
1737
+ .sl-ba [data-ba-zone]{grid-column:1/-1;width:100%}
1680
1738
 
1681
1739
  /* modal host */
1682
1740
  .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}
@@ -1752,9 +1810,15 @@ var SeatPicker = class _SeatPicker {
1752
1810
  this.tipPos = { x: 0, y: 0 };
1753
1811
  this.confirmEl = null;
1754
1812
  this.confirmSeat = null;
1813
+ this.tableDialogEl = null;
1814
+ this.tableDialog = null;
1815
+ this.tableDialogHeld = false;
1816
+ this.tableDialogReturnFocus = null;
1755
1817
  this.srEl = null;
1756
1818
  this.baQty = 2;
1757
1819
  this.baCat = "";
1820
+ /** Optional navigation-zone scope for buyer best-available. */
1821
+ this.baZone = "";
1758
1822
  /** "★ Best seats" premium quick-pick toggle — biases best-available to premium seats. */
1759
1823
  this.baPremium = false;
1760
1824
  this.bestAvailableConfirm = false;
@@ -1768,6 +1832,7 @@ var SeatPicker = class _SeatPicker {
1768
1832
  this.cbSafe = false;
1769
1833
  // arena / multi-floor / seat-view chrome
1770
1834
  this.rungsEl = null;
1835
+ this.projectionEl = null;
1771
1836
  this.floorsEl = null;
1772
1837
  this.secCardEl = null;
1773
1838
  this.viewEl = null;
@@ -1863,11 +1928,21 @@ var SeatPicker = class _SeatPicker {
1863
1928
  this.toast(this.tf("picker.salesClosedToast", "Sales are closed for this event."), "warning");
1864
1929
  return;
1865
1930
  }
1931
+ if (this.controller.tableSelection(seat.id)) return;
1866
1932
  this.flashPickedSeat(seat.id);
1867
1933
  if (this.opts.confirmSelection) this.showConfirm(seat);
1868
1934
  },
1935
+ onTableSelectionRequest: (table) => {
1936
+ if (this.salesClosed) {
1937
+ this.controller.deselect(table.physicalSeatIds);
1938
+ this.toast(this.tf("picker.salesClosedToast", "Sales are closed for this event."), "warning");
1939
+ return;
1940
+ }
1941
+ this.showTableDialog(table, false);
1942
+ },
1869
1943
  onDeselect: (seat) => {
1870
1944
  if (this.confirmSeat?.id === seat.id) this.dismissConfirm();
1945
+ if (this.tableDialog?.physicalSeatIds.includes(seat.id)) this.dismissTableDialog();
1871
1946
  },
1872
1947
  onSelectionLimit: () => {
1873
1948
  this.toast(`You can select up to ${this.maxTickets} tickets for this order.`, "warning");
@@ -1875,6 +1950,7 @@ var SeatPicker = class _SeatPicker {
1875
1950
  onViewChange: () => {
1876
1951
  this.reanchorConfirm();
1877
1952
  this.syncRung();
1953
+ this.syncProjection();
1878
1954
  this.drawMinimapRect();
1879
1955
  this.sectionCardOnView();
1880
1956
  },
@@ -1891,27 +1967,47 @@ var SeatPicker = class _SeatPicker {
1891
1967
  onError: (err) => this.opts.onError?.(err)
1892
1968
  });
1893
1969
  }
1970
+ /** True when the chart carries a real performance anchor — a stage-kind shape.
1971
+ * Every chart has a `focalPoint` (a bare look-at coordinate), so its presence
1972
+ * alone never justifies a "to stage" claim; only an actual stage does. */
1973
+ chartHasStage() {
1974
+ const doc = this.controller.doc;
1975
+ if (!doc) return false;
1976
+ const objectSets = doc.floors?.length ? doc.floors.map((f) => f.objects ?? []) : [doc.objects ?? []];
1977
+ for (const objects of objectSets) {
1978
+ for (const obj of objects) {
1979
+ if (obj.type === "shape" && (obj.role === "stage" || obj.stageKind)) return true;
1980
+ }
1981
+ }
1982
+ return false;
1983
+ }
1894
1984
  /**
1895
- * Eager sightline preview for the confirm card: a cheap generated forward
1896
- * view (or the organizer's real photo) plus a "Nm to stage · clear
1897
- * sightline" line the premium at-a-glance moment; click opens the 360.
1985
+ * Eager sightline preview for the confirm card: the organizer's real view
1986
+ * photo, or only when the chart has an actual stage to look at — a generated
1987
+ * forward view plus an " Nm to stage · clear sightline" line. On a stageless
1988
+ * chart both the distance/sightline claim and the generic stage silhouette are
1989
+ * invented promises, so we suppress them; a real attached photo always shows.
1990
+ * (OV-52)
1898
1991
  */
1899
1992
  confirmThumbHtml(seat) {
1900
1993
  const doc = this.controller.doc;
1901
1994
  if (!doc) return "";
1902
- let url = seat.viewUrl ?? "";
1995
+ const realPhoto = seat.viewUrl ?? "";
1996
+ const hasStage = this.chartHasStage();
1997
+ if (!realPhoto && !hasStage) return "";
1998
+ let url = realPhoto;
1903
1999
  let distance = null;
1904
2000
  if (!url) {
1905
2001
  try {
1906
- const thumb = generateSeatThumb(seat, doc.focalPoint);
2002
+ const thumb = generateSeatThumb(seat, seat.focalPoint ?? doc.focalPoint);
1907
2003
  url = thumb.url;
1908
2004
  distance = thumb.distanceM ?? null;
1909
2005
  } catch {
1910
2006
  return "";
1911
2007
  }
1912
2008
  }
1913
- const sight = distance != null ? t2("picker.sightline", { m: distance }) : this.tf("picker.sightlineClear", "Clear sightline");
1914
- return `<button type="button" class="sl-confirm-view sl-confirm-thumbwrap" aria-label="${t2("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>`;
2009
+ const sightHtml = hasStage ? `<div class="sl-confirm-sight"><span aria-hidden="true">\u2713</span>${distance != null ? t2("picker.sightline", { m: distance }) : this.tf("picker.sightlineClear", "Clear sightline")}</div>` : "";
2010
+ return `<button type="button" class="sl-confirm-view sl-confirm-thumbwrap" aria-label="${t2("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;
1915
2011
  }
1916
2012
  /** Minimal HTML/attribute escaper for buyer-authored commercial text (notes). */
1917
2013
  escCx(value) {
@@ -1957,6 +2053,19 @@ var SeatPicker = class _SeatPicker {
1957
2053
  const title = this.escCx(c?.note ? c.note : limited);
1958
2054
  return `<span class="sl-cx-mark" role="img" aria-label="${title}" title="${title}">\u25D0</span>`;
1959
2055
  }
2056
+ wheelchairProvisionLabel(type) {
2057
+ if (type === "no-seat") return "Empty wheelchair space";
2058
+ if (type === "seat-present") return "Accessible physical seat";
2059
+ return "";
2060
+ }
2061
+ wheelchairConfirmHtml(type) {
2062
+ const label = this.wheelchairProvisionLabel(type);
2063
+ 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>` : "";
2064
+ }
2065
+ wheelchairChipMarker(type) {
2066
+ const label = this.wheelchairProvisionLabel(type);
2067
+ return label ? `<span class="sl-cx-mark" role="img" aria-label="${label}" title="${label}">\u267F</span>` : "";
2068
+ }
1960
2069
  /** True when the picker is rendered inside an iframe (snippet embed at /e/:key). */
1961
2070
  isFramed() {
1962
2071
  return typeof window !== "undefined" && window.parent !== window;
@@ -2108,7 +2217,10 @@ var SeatPicker = class _SeatPicker {
2108
2217
  });
2109
2218
  picker.escHandler = (e) => {
2110
2219
  if (e.key !== "Escape") return;
2111
- if (picker.confirmSeat) {
2220
+ if (picker.tableDialog) {
2221
+ e.preventDefault();
2222
+ picker.cancelTableDialog();
2223
+ } else if (picker.confirmSeat) {
2112
2224
  e.preventDefault();
2113
2225
  picker.cancelConfirm();
2114
2226
  } else if (picker.bestAvailableConfirm) {
@@ -2139,7 +2251,11 @@ var SeatPicker = class _SeatPicker {
2139
2251
  mount.appendChild(root);
2140
2252
  root.addEventListener("keydown", (e) => {
2141
2253
  if (e.key !== "Escape") return;
2142
- if (this.confirmSeat) {
2254
+ if (this.tableDialog) {
2255
+ e.preventDefault();
2256
+ e.stopPropagation();
2257
+ this.cancelTableDialog();
2258
+ } else if (this.confirmSeat) {
2143
2259
  e.preventDefault();
2144
2260
  e.stopPropagation();
2145
2261
  this.cancelConfirm();
@@ -2308,6 +2424,7 @@ var SeatPicker = class _SeatPicker {
2308
2424
  }
2309
2425
  this.els.boot.remove();
2310
2426
  this.salesClosed = !!info.salesClosed;
2427
+ this.controller.setViewMode(this.opts.initialView ?? "flat");
2311
2428
  this.buildRegions();
2312
2429
  this.regions["bottom-right"].appendChild(this.els.zoom);
2313
2430
  this.regions["bottom-center"].appendChild(this.els.toast);
@@ -2350,11 +2467,10 @@ var SeatPicker = class _SeatPicker {
2350
2467
  this.regions["top-left"].appendChild(chips);
2351
2468
  this.a11yChipsEl = chips;
2352
2469
  if (present.size) {
2353
- const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
2354
2470
  const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-a11y="1" data-f="${key}">${label}</button>`;
2355
2471
  chips.insertAdjacentHTML(
2356
2472
  "beforeend",
2357
- mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("")
2473
+ mk("all", "All seats") + ACCESSIBILITY_TYPES.filter(({ key }) => present.has(key)).map(({ key, short, icon }) => mk(key, `${icon} ${short}`)).join("")
2358
2474
  );
2359
2475
  const active = /* @__PURE__ */ new Set();
2360
2476
  const syncChips = () => {
@@ -2404,10 +2520,7 @@ var SeatPicker = class _SeatPicker {
2404
2520
  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>';
2405
2521
  this.els.zfit.parentElement.appendChild(cb);
2406
2522
  cb.addEventListener("click", () => {
2407
- this.cbSafe = !this.cbSafe;
2408
- cb.setAttribute("aria-pressed", String(this.cbSafe));
2409
- this.controller.setColorblindSafe(this.cbSafe);
2410
- writeStoredColorblind(this.cbSafe);
2523
+ this.setColorblindSafe(!this.cbSafe);
2411
2524
  });
2412
2525
  this.srEl = document.createElement("div");
2413
2526
  this.srEl.className = "sl-sr";
@@ -2529,6 +2642,7 @@ var SeatPicker = class _SeatPicker {
2529
2642
  this.applySalesClosed();
2530
2643
  }
2531
2644
  applySalesClosed() {
2645
+ if (this.salesClosed && this.tableDialog && !this.tableDialogHeld) this.cancelTableDialog();
2532
2646
  const pill = this.els.closedPill;
2533
2647
  if (pill) {
2534
2648
  pill.classList.toggle("on", this.salesClosed);
@@ -2617,12 +2731,13 @@ var SeatPicker = class _SeatPicker {
2617
2731
  /** Update only the action affordance; selection callbacks must not refire. */
2618
2732
  committedSelection() {
2619
2733
  const candidateId = this.confirmSeat?.id;
2620
- return this.controller.getSelection().filter((seat) => seat.id !== candidateId);
2734
+ const pendingTableId = this.tableDialog && !this.tableDialogHeld ? this.tableDialog.id : null;
2735
+ return this.controller.getSelection().filter((seat) => seat.id !== candidateId && seat.id !== pendingTableId);
2621
2736
  }
2622
2737
  pendingSelectionCount() {
2623
2738
  const heldItems = this.hold?.items ?? [];
2624
2739
  const heldLabels = new Set(heldItems.map((item) => item.label));
2625
- const pendingSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).length;
2740
+ const pendingSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).reduce((sum, seat) => sum + (seat.quantity ?? 1), 0);
2626
2741
  return pendingSeats + this.pendingGACount();
2627
2742
  }
2628
2743
  heldGACounts() {
@@ -2644,13 +2759,13 @@ var SeatPicker = class _SeatPicker {
2644
2759
  }
2645
2760
  totalTicketCount() {
2646
2761
  const heldLabels = new Set((this.hold?.items ?? []).map((item) => item.label));
2647
- const freshSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).length;
2762
+ const freshSeats = this.committedSelection().filter((seat) => !heldLabels.has(seat.label)).reduce((sum, seat) => sum + (seat.quantity ?? 1), 0);
2648
2763
  return this.heldTicketCount() + freshSeats + this.pendingGACount();
2649
2764
  }
2650
2765
  /** Held tickets and standing quantities consume the same order-wide cap. */
2651
2766
  updateSelectionCapacity() {
2652
2767
  const heldLabels = new Set((this.hold?.items ?? []).map((item) => item.label));
2653
- const selectedHeld = this.committedSelection().filter((seat) => heldLabels.has(seat.label)).length;
2768
+ const selectedHeld = this.committedSelection().filter((seat) => heldLabels.has(seat.label)).reduce((sum, seat) => sum + (seat.quantity ?? 1), 0);
2654
2769
  const remaining = Math.max(0, this.maxTickets - this.heldTicketCount() - this.pendingGACount());
2655
2770
  this.controller.setMaxSelection(selectedHeld + remaining);
2656
2771
  }
@@ -2677,9 +2792,9 @@ var SeatPicker = class _SeatPicker {
2677
2792
  cta.textContent = this.tf("picker.salesClosedCta", "Sales closed");
2678
2793
  return;
2679
2794
  }
2680
- if (this.confirmSeat) {
2795
+ if (this.confirmSeat || this.tableDialog && !this.tableDialogHeld) {
2681
2796
  cta.disabled = true;
2682
- cta.textContent = "Confirm or cancel this seat";
2797
+ cta.textContent = this.tableDialog ? "Confirm your table" : "Confirm or cancel this seat";
2683
2798
  return;
2684
2799
  }
2685
2800
  if (this.ctaPhase === "holding") {
@@ -2988,11 +3103,26 @@ var SeatPicker = class _SeatPicker {
2988
3103
  });
2989
3104
  }
2990
3105
  // ---- arena / multi-floor chrome -------------------------------------------
2991
- /** Build the rung pills (charts with sections) and floor switcher (>1 floor). */
3106
+ /** Build projection, rung and floor controls for arena-scale charts. */
2992
3107
  buildArenaChrome() {
2993
3108
  const doc = this.controller.doc;
2994
3109
  if (!doc || !this.els.map) return;
2995
3110
  const hasSections = doc.objects.some((o) => o.type === "section") || (doc.floors ?? []).some((f) => f.objects.some((o) => o.type === "section"));
3111
+ if (hasSections) {
3112
+ const projection = document.createElement("div");
3113
+ projection.className = "sl-projection";
3114
+ projection.setAttribute("role", "group");
3115
+ projection.setAttribute("aria-label", "Map projection");
3116
+ 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>';
3117
+ projection.querySelectorAll("button").forEach((button) => {
3118
+ button.addEventListener("click", () => {
3119
+ this.setViewMode(button.dataset.projection);
3120
+ });
3121
+ });
3122
+ this.regions["top-right"].appendChild(projection);
3123
+ this.projectionEl = projection;
3124
+ this.syncProjection();
3125
+ }
2996
3126
  if (hasSections) {
2997
3127
  const RUNGS = ["zones", "sections", "seats"];
2998
3128
  const pills = document.createElement("div");
@@ -3054,6 +3184,15 @@ var SeatPicker = class _SeatPicker {
3054
3184
  btn.setAttribute("aria-pressed", String(on));
3055
3185
  });
3056
3186
  }
3187
+ syncProjection() {
3188
+ if (!this.projectionEl) return;
3189
+ const active = this.controller.getViewMode();
3190
+ this.projectionEl.querySelectorAll("button").forEach((button) => {
3191
+ const on = button.dataset.projection === active;
3192
+ button.classList.toggle("on", on);
3193
+ button.setAttribute("aria-pressed", String(on));
3194
+ });
3195
+ }
3057
3196
  /** Reflect the active floor onto the switcher rail. */
3058
3197
  syncFloors() {
3059
3198
  if (!this.floorsEl) return;
@@ -3182,7 +3321,150 @@ var SeatPicker = class _SeatPicker {
3182
3321
  const status = this.controller.getStatus(seat.id) ?? "free";
3183
3322
  const statusText = status === "free" ? "available" : status === "held" ? "on hold" : "taken";
3184
3323
  const price = cat ? this.catPrice(cat) : void 0;
3185
- this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
3324
+ const table = this.controller.tableSelection(seat.id);
3325
+ const details = table ?? this.controller.seatDetails(seat.id);
3326
+ const typeWord = this.rowTypeWord(details);
3327
+ const buyerName = details?.rowLabel ?? details?.displayLabel ?? seat.displayLabel ?? seat.label;
3328
+ 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}`;
3329
+ this.srEl.textContent = `${identity}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
3330
+ }
3331
+ // ---- atomic table confirmation / guest quantity ---------------------------
3332
+ showTableDialog(table, held, returnFocus) {
3333
+ this.dismissTableDialog(false);
3334
+ this.tableDialog = { ...table };
3335
+ this.tableDialogHeld = held;
3336
+ this.tableDialogReturnFocus = returnFocus ?? document.activeElement;
3337
+ const esc2 = (value) => String(value ?? "").replace(/[&<>"']/g, (ch) => ({
3338
+ "&": "&amp;",
3339
+ "<": "&lt;",
3340
+ ">": "&gt;",
3341
+ '"': "&quot;",
3342
+ "'": "&#39;"
3343
+ })[ch]);
3344
+ const cat = this.controller.doc?.categories.find((candidate) => candidate.key === table.categoryKey);
3345
+ const variable = table.bookingMode === "variable";
3346
+ const typeWord = this.rowTypeWord(table);
3347
+ const el = document.createElement("div");
3348
+ el.className = "sl-table-scrim";
3349
+ 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>`;
3350
+ this.root.appendChild(el);
3351
+ this.tableDialogEl = el;
3352
+ this.renderTableDialogState();
3353
+ el.querySelectorAll("[data-table-step]").forEach((button) => {
3354
+ button.addEventListener("click", () => {
3355
+ if (!this.tableDialog) return;
3356
+ const next = Math.max(
3357
+ this.tableDialog.minOccupancy,
3358
+ Math.min(this.tableDialog.maxOccupancy, this.tableDialog.quantity + Number(button.dataset.tableStep))
3359
+ );
3360
+ this.tableDialog = { ...this.tableDialog, quantity: next };
3361
+ this.renderTableDialogState();
3362
+ });
3363
+ });
3364
+ el.querySelector(".sl-table-cancel").addEventListener("click", () => this.cancelTableDialog());
3365
+ el.querySelector(".sl-table-confirm").addEventListener("click", () => void this.confirmTableDialog());
3366
+ el.addEventListener("mousedown", (event) => {
3367
+ if (event.target === el) this.cancelTableDialog();
3368
+ });
3369
+ el.addEventListener("keydown", (event) => {
3370
+ if (event.key !== "Tab") return;
3371
+ const focusable = [...el.querySelectorAll('button:not(:disabled),[tabindex]:not([tabindex="-1"])')];
3372
+ if (!focusable.length) return;
3373
+ const first = focusable[0];
3374
+ const last = focusable[focusable.length - 1];
3375
+ if (event.shiftKey && document.activeElement === first) {
3376
+ event.preventDefault();
3377
+ last.focus();
3378
+ } else if (!event.shiftKey && document.activeElement === last) {
3379
+ event.preventDefault();
3380
+ first.focus();
3381
+ }
3382
+ });
3383
+ requestAnimationFrame(() => el.querySelector(variable ? '[data-table-step="-1"]' : ".sl-table-confirm")?.focus());
3384
+ }
3385
+ renderTableDialogState() {
3386
+ const table = this.tableDialog;
3387
+ const el = this.tableDialogEl;
3388
+ if (!table || !el) return;
3389
+ const output = el.querySelector("output[data-table-qty]");
3390
+ if (output) output.value = String(table.quantity);
3391
+ const hidden = el.querySelector("input[data-table-qty]");
3392
+ if (hidden) hidden.value = String(table.quantity);
3393
+ el.querySelectorAll("[data-table-step]").forEach((button) => {
3394
+ const delta = Number(button.dataset.tableStep);
3395
+ button.disabled = delta < 0 ? table.quantity <= table.minOccupancy : table.quantity >= table.maxOccupancy;
3396
+ });
3397
+ const unit = this.paidPrice(table.categoryKey, table.tierId ?? null, table.price);
3398
+ const total = el.querySelector("[data-table-total]");
3399
+ if (total) total.textContent = this.money(unit * table.quantity);
3400
+ }
3401
+ async confirmTableDialog() {
3402
+ const table = this.tableDialog;
3403
+ const held = this.tableDialogHeld;
3404
+ if (!table) return;
3405
+ const button = this.tableDialogEl?.querySelector(".sl-table-confirm");
3406
+ if (button) {
3407
+ button.disabled = true;
3408
+ button.textContent = held ? "Updating\u2026" : "Selecting\u2026";
3409
+ }
3410
+ if (held) {
3411
+ try {
3412
+ const updated = await this.controller.replaceTableQuantity(table.label, table.quantity);
3413
+ if (!updated) {
3414
+ this.toast("That guest count could not be secured. Your current table hold is unchanged.", "warning");
3415
+ this.renderTableDialogState();
3416
+ if (button) {
3417
+ button.disabled = false;
3418
+ button.textContent = "Update table";
3419
+ }
3420
+ return;
3421
+ }
3422
+ this.hold = {
3423
+ holdId: updated.holdId,
3424
+ expiresAt: updated.expiresAt,
3425
+ seats: updated.seats,
3426
+ items: updated.items
3427
+ };
3428
+ this.startHoldTimer(updated.expiresAt);
3429
+ this.dismissTableDialog();
3430
+ this.syncTray();
3431
+ this.emitHoldChange();
3432
+ this.toast(`${table.label} updated for ${table.quantity} guests.`, "success");
3433
+ } catch (error) {
3434
+ this.opts.onError?.(error);
3435
+ this.toast("That guest count is no longer available. Your current hold is unchanged.", "error");
3436
+ if (button) {
3437
+ button.disabled = false;
3438
+ button.textContent = "Update table";
3439
+ }
3440
+ }
3441
+ return;
3442
+ }
3443
+ if (!this.controller.setTableQuantity(table.label, table.quantity)) {
3444
+ if (button) {
3445
+ button.disabled = false;
3446
+ button.textContent = table.bookingMode === "variable" ? "Select table" : "Select whole table";
3447
+ }
3448
+ return;
3449
+ }
3450
+ this.dismissTableDialog();
3451
+ this.collapseSectionCard();
3452
+ this.syncTray();
3453
+ }
3454
+ cancelTableDialog() {
3455
+ const table = this.tableDialog;
3456
+ const held = this.tableDialogHeld;
3457
+ this.dismissTableDialog();
3458
+ if (table && !held) this.controller.deselect(table.physicalSeatIds);
3459
+ }
3460
+ dismissTableDialog(restoreFocus = true) {
3461
+ const focus = this.tableDialogReturnFocus;
3462
+ this.tableDialogEl?.remove();
3463
+ this.tableDialogEl = null;
3464
+ this.tableDialog = null;
3465
+ this.tableDialogHeld = false;
3466
+ this.tableDialogReturnFocus = null;
3467
+ if (restoreFocus) requestAnimationFrame(() => (focus?.isConnected ? focus : this.root)?.focus());
3186
3468
  }
3187
3469
  // ---- seat candidate confirmation ------------------------------------------
3188
3470
  showConfirm(seat) {
@@ -3204,13 +3486,18 @@ var SeatPicker = class _SeatPicker {
3204
3486
  ">": "&gt;",
3205
3487
  '"': "&quot;"
3206
3488
  })[char]);
3489
+ const identityFields = [
3490
+ details?.sectionLabel ? `<div class="sl-confirm-field"><span class="sl-confirm-key">Section</span><span class="sl-confirm-value">${safe(details.sectionLabel)}</span></div>` : "",
3491
+ 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>` : "",
3492
+ 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>` : ""
3493
+ ].filter(Boolean).join("");
3207
3494
  const el = document.createElement("div");
3208
3495
  el.className = "sl-confirm";
3209
3496
  el.setAttribute("role", "dialog");
3210
3497
  el.setAttribute("aria-modal", "true");
3211
3498
  el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
3212
3499
  el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
3213
- 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>`;
3500
+ 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>`;
3214
3501
  this.els.map.appendChild(el);
3215
3502
  this.confirmEl = el;
3216
3503
  this.reanchorConfirm();
@@ -3281,7 +3568,7 @@ var SeatPicker = class _SeatPicker {
3281
3568
  this.closeSeatView();
3282
3569
  const doc = this.controller.doc;
3283
3570
  const activeId = this.controller.getActiveFloorId();
3284
- const focal = doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
3571
+ const focal = seat.focalPoint ?? doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
3285
3572
  let panoUrl;
3286
3573
  let caption;
3287
3574
  let real = false;
@@ -3505,16 +3792,37 @@ var SeatPicker = class _SeatPicker {
3505
3792
  const noPicks = !seats.length && !heldItems.length && !this.pendingGACount();
3506
3793
  if (!this.hold && (noPicks || this.bestAvailableBusy || this.bestAvailableConfirm)) {
3507
3794
  const cats = this.controller.doc?.categories ?? [];
3795
+ const zones = this.controller.getBestAvailableZones();
3796
+ if (this.baZone && !zones.some((zone) => zone.id === this.baZone)) this.baZone = "";
3508
3797
  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
3509
3798
  // seats (same present-only philosophy as the a11y filter chips).
3510
- (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>`);
3511
- }
3512
- const idGrid = (seatId, label) => {
3799
+ (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>`);
3800
+ }
3801
+ const idGrid = (seatId, label, objectType, quantity = 1, objectId, identity) => {
3802
+ const esc2 = (value) => String(value ?? "\u2014").replace(/[&<>"]/g, (char) => ({
3803
+ "&": "&amp;",
3804
+ "<": "&lt;",
3805
+ ">": "&gt;",
3806
+ '"': "&quot;"
3807
+ })[char]);
3513
3808
  const d = seatId ? this.controller.seatDetails(seatId) : null;
3809
+ const area = objectType === "ga" ? gaAreas.find((candidate) => candidate.id === objectId) : void 0;
3810
+ const effectiveType = objectType === "ga" ? "ga" : d?.objectType ?? objectType;
3811
+ const typeWord = identity?.displayType?.trim() || d?.displayType?.trim() || area?.displayType?.trim() || (effectiveType === "table" ? "Table" : effectiveType === "booth" ? "Booth" : effectiveType === "ga" ? "General admission" : "Row");
3812
+ const buyerName = identity?.rowLabel ?? identity?.displayLabel ?? d?.rowLabel ?? d?.displayLabel ?? area?.displayLabel ?? area?.label ?? label;
3813
+ if (effectiveType === "table" && identity?.bookingMode) {
3814
+ 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>`;
3815
+ }
3816
+ if (effectiveType === "ga") {
3817
+ 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>`;
3818
+ }
3819
+ if (effectiveType === "booth") {
3820
+ 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>`;
3821
+ }
3514
3822
  if (!d?.sectionLabel && !d?.rowLabel && !d?.seatNumber) {
3515
- return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">Seat</span><span class="val">${label}</span></span></div>`;
3823
+ 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>`;
3516
3824
  }
3517
- 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>`;
3825
+ 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>`;
3518
3826
  };
3519
3827
  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="${t2("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>`;
3520
3828
  for (const item of heldItems) {
@@ -3523,9 +3831,10 @@ var SeatPicker = class _SeatPicker {
3523
3831
  const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
3524
3832
  const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
3525
3833
  const heldSeat = item.objectType !== "ga" ? this.controller.seatByLabel(item.label) : null;
3526
- const canView2 = this.seatViewEnabled() && !!heldSeat;
3834
+ const table = item.objectType === "table" ? this.controller.tableSelection(item.label) : null;
3835
+ const canView2 = this.seatViewEnabled() && !!heldSeat && item.objectType !== "table";
3527
3836
  parts.push(
3528
- `<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>`
3837
+ `<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>`
3529
3838
  );
3530
3839
  }
3531
3840
  const heldLabels = new Set(heldItems.map((item) => item.label));
@@ -3536,13 +3845,13 @@ var SeatPicker = class _SeatPicker {
3536
3845
  const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
3537
3846
  const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${t2("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>` : "";
3538
3847
  parts.push(
3539
- `<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>`
3848
+ `<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>`
3540
3849
  );
3541
3850
  }
3542
3851
  for (const area of gaAreas) {
3543
3852
  const qty = this.gaQty.get(area.id) ?? 0;
3544
3853
  parts.push(
3545
- `<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>`
3854
+ `<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>`
3546
3855
  );
3547
3856
  }
3548
3857
  this.els.tray.innerHTML = parts.join("");
@@ -3556,6 +3865,9 @@ var SeatPicker = class _SeatPicker {
3556
3865
  this.els.tray.querySelector("[data-ba-cat]")?.addEventListener("change", (e) => {
3557
3866
  this.baCat = e.target.value;
3558
3867
  });
3868
+ this.els.tray.querySelector("[data-ba-zone]")?.addEventListener("change", (e) => {
3869
+ this.baZone = e.target.value;
3870
+ });
3559
3871
  this.els.tray.querySelector("[data-ba-premium]")?.addEventListener("click", () => {
3560
3872
  this.baPremium = !this.baPremium;
3561
3873
  this.syncTray();
@@ -3567,7 +3879,7 @@ var SeatPicker = class _SeatPicker {
3567
3879
  this.els.tray.querySelector("[data-ba-replace]")?.focus();
3568
3880
  return;
3569
3881
  }
3570
- void this.bestAvailable(this.baQty, this.baCat || void 0, { preferPremium: this.baPremium });
3882
+ void this.bestAvailable(this.baQty, this.baCat || void 0, { preferPremium: this.baPremium, zoneId: this.baZone || void 0 });
3571
3883
  });
3572
3884
  this.els.tray.querySelector("[data-ba-cancel]")?.addEventListener("click", () => {
3573
3885
  this.bestAvailableConfirm = false;
@@ -3576,7 +3888,7 @@ var SeatPicker = class _SeatPicker {
3576
3888
  });
3577
3889
  this.els.tray.querySelector("[data-ba-replace]")?.addEventListener("click", () => {
3578
3890
  this.bestAvailableConfirm = false;
3579
- void this.bestAvailable(this.baQty, this.baCat || void 0, { preferPremium: this.baPremium });
3891
+ void this.bestAvailable(this.baQty, this.baCat || void 0, { preferPremium: this.baPremium, zoneId: this.baZone || void 0 });
3580
3892
  });
3581
3893
  this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
3582
3894
  btn.addEventListener("click", () => {
@@ -3608,6 +3920,20 @@ var SeatPicker = class _SeatPicker {
3608
3920
  this.scheduleMotion(remove, 150);
3609
3921
  });
3610
3922
  });
3923
+ this.els.tray.querySelectorAll("[data-table-edit]").forEach((button) => {
3924
+ button.addEventListener("click", (event) => {
3925
+ event.stopPropagation();
3926
+ const label = decodeURIComponent(button.dataset.tableEdit ?? "");
3927
+ const details = this.controller.tableSelection(label);
3928
+ if (!details) return;
3929
+ const heldItem = heldItems.find((item) => item.label === label && item.objectType === "table");
3930
+ this.showTableDialog(
3931
+ { ...details, quantity: heldItem?.quantity ?? details.quantity },
3932
+ !!heldItem,
3933
+ button
3934
+ );
3935
+ });
3936
+ });
3611
3937
  this.els.tray.querySelectorAll(".sl-chip .tier").forEach((sel) => {
3612
3938
  sel.addEventListener("change", () => this.controller.setSeatTier(sel.dataset.tier, sel.value || null));
3613
3939
  });
@@ -3635,7 +3961,7 @@ var SeatPicker = class _SeatPicker {
3635
3961
  });
3636
3962
  });
3637
3963
  if (this.salesClosed) {
3638
- this.els.tray.querySelectorAll(".sl-ba-go,[data-ba],[data-ba-cat],[data-ba-replace],.sl-ga button").forEach((el) => {
3964
+ this.els.tray.querySelectorAll(".sl-ba-go,[data-ba],[data-ba-cat],[data-ba-zone],[data-ba-replace],.sl-ga button").forEach((el) => {
3639
3965
  el.disabled = true;
3640
3966
  });
3641
3967
  }
@@ -3644,8 +3970,11 @@ var SeatPicker = class _SeatPicker {
3644
3970
  const heldTotal = heldItems.reduce((sum, item) => sum + this.paidPrice(item.categoryKey, item.tierId, item.unitPrice) * (item.quantity ?? 1), 0);
3645
3971
  const heldCount = heldItems.reduce((sum, item) => sum + (item.quantity ?? 1), 0);
3646
3972
  const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
3647
- const total = freshSeats.reduce((sum, s) => sum + this.paidPrice(s.categoryKey, s.tierId ?? null, s.price), 0) + gaTotal + heldTotal;
3648
- const count = freshSeats.length + gaCount + heldCount;
3973
+ const total = freshSeats.reduce(
3974
+ (sum, s) => sum + this.paidPrice(s.categoryKey, s.tierId ?? null, s.price) * (s.quantity ?? 1),
3975
+ 0
3976
+ ) + gaTotal + heldTotal;
3977
+ const count = freshSeats.reduce((sum, seat) => sum + (seat.quantity ?? 1), 0) + gaCount + heldCount;
3649
3978
  const pendingCount = this.pendingSelectionCount();
3650
3979
  const previousCount = this.lastTrayCount;
3651
3980
  const previousTotal = this.lastTrayTotal;
@@ -3891,16 +4220,21 @@ var SeatPicker = class _SeatPicker {
3891
4220
  /** Assemble the stable {@link CheckoutHandoff} from a hold's server line items. */
3892
4221
  buildHandoff(hold) {
3893
4222
  const items = hold.items ?? [];
3894
- const lineItems = items.map((it) => ({
3895
- label: it.label,
3896
- objectId: it.objectId,
3897
- objectType: it.objectType,
3898
- categoryKey: it.categoryKey,
3899
- tierId: it.tierId,
3900
- unitPrice: this.paidPrice(it.categoryKey, it.tierId, it.unitPrice),
3901
- currency: it.currency ?? this.currency,
3902
- quantity: it.quantity ?? 1
3903
- }));
4223
+ const lineItems = items.map((it) => {
4224
+ const display = this.controller.lineItemDisplay(it);
4225
+ return {
4226
+ label: it.label,
4227
+ ...display.displayLabel ? { displayLabel: display.displayLabel } : {},
4228
+ ...display.displayType ? { displayType: display.displayType } : {},
4229
+ objectId: it.objectId,
4230
+ objectType: it.objectType,
4231
+ categoryKey: it.categoryKey,
4232
+ tierId: it.tierId,
4233
+ unitPrice: this.paidPrice(it.categoryKey, it.tierId, it.unitPrice),
4234
+ currency: it.currency ?? this.currency,
4235
+ quantity: it.quantity ?? 1
4236
+ };
4237
+ });
3904
4238
  const currency = lineItems[0]?.currency ?? this.currency;
3905
4239
  const total = lineItems.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
3906
4240
  return { holdId: hold.holdId, expiresAt: hold.expiresAt, currency, lineItems, total };
@@ -3961,8 +4295,11 @@ var SeatPicker = class _SeatPicker {
3961
4295
  /** Buyer-facing type word for the row/table key label — the designer's
3962
4296
  * per-object "Displayed type" override, or the default "Row". */
3963
4297
  rowTypeWord(details) {
3964
- const t3 = details?.rowType?.trim();
3965
- return t3 || "Row";
4298
+ const authored = details?.displayType?.trim() || details?.rowType?.trim();
4299
+ if (authored) return authored;
4300
+ if (details?.objectType === "table") return "Table";
4301
+ if (details?.objectType === "booth") return "Booth";
4302
+ return "Row";
3966
4303
  }
3967
4304
  rowShort(details) {
3968
4305
  const row = details?.rowLabel;
@@ -3982,13 +4319,17 @@ var SeatPicker = class _SeatPicker {
3982
4319
  }
3983
4320
  const esc2 = (v) => String(v ?? "\u2014").replace(/[&<>"]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch]);
3984
4321
  const price = this.money(this.paidPrice(details.categoryKey, details.tierId ?? null, details.price));
4322
+ const isGroupedTable = details.objectType === "table" && !!details.bookingMode;
4323
+ const isBooth = details.objectType === "booth";
3985
4324
  const hasLoc = details.sectionLabel || details.rowLabel || details.seatNumber;
3986
- 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>`;
4325
+ 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>`;
3987
4326
  const statusLine = details.status === "free" ? "" : `<div class="sl-tip-status">${details.status === "held" ? t2("map.statusHeld") : t2("map.statusTaken")}</div>`;
3988
4327
  const limited = this.limitedViewLabel(details.commercial);
3989
4328
  const cxLine = limited ? `<div class="sl-tip-cx"><span class="g" aria-hidden="true">\u25D0</span>${esc2(limited)}</div>` : "";
4329
+ const wheelchair = this.wheelchairProvisionLabel(details.wheelchairSpaceType);
4330
+ const wheelchairLine = wheelchair ? `<div class="sl-tip-cx"><span class="g" aria-hidden="true">\u267F</span>${esc2(wheelchair)}</div>` : "";
3990
4331
  this.tipEl.style.setProperty("--sl-cat", details.categoryColor);
3991
- 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;
4332
+ 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;
3992
4333
  this.tipEl.style.display = "block";
3993
4334
  this.placeTooltip();
3994
4335
  }
@@ -3996,6 +4337,33 @@ var SeatPicker = class _SeatPicker {
3996
4337
  getSelection() {
3997
4338
  return this.committedSelection();
3998
4339
  }
4340
+ /** Current colorblind-safe render state, resolved from the stored buyer
4341
+ * preference at mount. Host chrome (e.g. the Designer preview) reads this to
4342
+ * surface the state rather than rendering colorblind colors silently. */
4343
+ isColorblindSafe() {
4344
+ return this.cbSafe;
4345
+ }
4346
+ /** Single source of truth for the colorblind-safe toggle — used by both the
4347
+ * in-widget button and host chrome. Updates the renderer, the persisted
4348
+ * cross-surface preference, and the in-widget button's pressed state together
4349
+ * so the two controls never diverge. */
4350
+ setColorblindSafe(on) {
4351
+ if (on === this.cbSafe) return;
4352
+ this.cbSafe = on;
4353
+ this.cbEl?.setAttribute("aria-pressed", String(on));
4354
+ this.controller.setColorblindSafe(on);
4355
+ writeStoredColorblind(on);
4356
+ }
4357
+ /** Switch the buyer canvas between flat, isometric and Perspective 2.5D. */
4358
+ setViewMode(mode) {
4359
+ this.controller.setViewMode(mode);
4360
+ this.syncProjection();
4361
+ this.dismissConfirm();
4362
+ }
4363
+ /** Current buyer canvas projection. */
4364
+ getViewMode() {
4365
+ return this.controller.getViewMode();
4366
+ }
3999
4367
  /** Current active/restored hold reflected in the tray. */
4000
4368
  getCurrentHold() {
4001
4369
  return this.hold;
@@ -4085,6 +4453,7 @@ var SeatPicker = class _SeatPicker {
4085
4453
  this.destroyed = true;
4086
4454
  if (this.hold && !this.handedOff) void this.controller.release();
4087
4455
  this.closeConfirm();
4456
+ this.dismissTableDialog(false);
4088
4457
  this.closeSeatView();
4089
4458
  this.stopHoldTimer();
4090
4459
  if (this.toastTimer) clearTimeout(this.toastTimer);
@@ -4098,6 +4467,7 @@ var SeatPicker = class _SeatPicker {
4098
4467
  if (this.fsChangeHandler) document.removeEventListener("fullscreenchange", this.fsChangeHandler);
4099
4468
  if (this.fsEscHandler) window.removeEventListener("keydown", this.fsEscHandler);
4100
4469
  this.controller.destroy();
4470
+ this.projectionEl = null;
4101
4471
  this.root?.remove();
4102
4472
  this.root = null;
4103
4473
  if (this.modalScrim) {