@seatlayer/js 0.24.0 → 0.26.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
@@ -58,7 +58,13 @@ async function request(base, path, init = {}) {
58
58
  const data = isJson ? await res.json().catch(() => null) : null;
59
59
  if (!res.ok) {
60
60
  const err = data;
61
- throw new ApiError(res.status, err?.error ?? `request_failed_${res.status}`, err?.code, err?.conflicts, err?.reason);
61
+ throw new ApiError(
62
+ res.status,
63
+ err?.error ?? `request_failed_${res.status}`,
64
+ err?.code ?? err?.error,
65
+ err?.conflicts,
66
+ err?.reason
67
+ );
62
68
  }
63
69
  return data;
64
70
  }
@@ -131,6 +137,7 @@ var SeatingChart = class {
131
137
  this.mount = null;
132
138
  this.hostEl = null;
133
139
  this.rendered = false;
140
+ this.mode_ = null;
134
141
  this.tipEl = null;
135
142
  this.tipPos = { x: 0, y: 0 };
136
143
  this.onTipMove = null;
@@ -184,6 +191,7 @@ var SeatingChart = class {
184
191
  this.rendered = false;
185
192
  return this;
186
193
  }
194
+ this.mode_ = info.mode === "test" ? "test" : "live";
187
195
  if (this.opts.seatTooltip !== false) {
188
196
  const tip = document.createElement("div");
189
197
  tip.setAttribute("role", "tooltip");
@@ -257,6 +265,19 @@ var SeatingChart = class {
257
265
  this.tipEl.style.display = "block";
258
266
  this.placeTooltip();
259
267
  }
268
+ /**
269
+ * Whether the SERVED event is a live or a test event (`sk_test_` keys create
270
+ * test events, which never book real inventory). `null` before render()
271
+ * resolves — the mode comes from the server with the chart, not from options.
272
+ *
273
+ * The widget already surfaces this visually with the test-mode ribbon; this
274
+ * getter is for hosts that draw their own chrome — notably a native WebView
275
+ * wrapper, which must be able to tell an integrator that the build they are
276
+ * about to ship is pointed at a test event.
277
+ */
278
+ getMode() {
279
+ return this.mode_;
280
+ }
260
281
  /** Current selection with prices resolved from the chart categories. */
261
282
  getSelection() {
262
283
  return this.controller.getSelection();
@@ -264,17 +285,49 @@ var SeatingChart = class {
264
285
  /** Hold the current selection. Resolves the hold, or null on a 409 conflict. */
265
286
  async hold(options = {}) {
266
287
  try {
267
- const h = await this.controller.hold(void 0, options.ttlMs);
268
- return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
288
+ return await this.holdOrThrow(options);
269
289
  } catch (err) {
270
290
  this.opts.onError?.(err);
271
291
  return null;
272
292
  }
273
293
  }
294
+ /**
295
+ * @internal Like {@link hold} but RE-THROWS the structured API error (409
296
+ * `reason`/`code` + `conflicts`) instead of swallowing it into `onError` +
297
+ * `null`. The native WebView host adapter needs the throw so it can answer the
298
+ * originating command with a correlated error carrying the SPECIFIC reason
299
+ * (`sold_out` vs `not_enough_together`); the public method above keeps the
300
+ * catch-and-onError contract that direct web consumers rely on. Not a stable
301
+ * part of the embed API.
302
+ */
303
+ async holdOrThrow(options = {}) {
304
+ const h = await this.controller.hold(void 0, options.ttlMs);
305
+ return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
306
+ }
274
307
  /** Restore an active hold by its opaque id without extending its expiry. */
275
308
  async resumeHold(holdId) {
276
309
  try {
277
- const h = await this.controller.resumeHold(holdId);
310
+ return await this.resumeHoldOrThrow(holdId);
311
+ } catch (err) {
312
+ this.opts.onError?.(err);
313
+ return null;
314
+ }
315
+ }
316
+ /** @internal Throwing variant of {@link resumeHold} for the native host adapter. See {@link holdOrThrow}. */
317
+ async resumeHoldOrThrow(holdId) {
318
+ const h = await this.controller.resumeHold(holdId);
319
+ return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
320
+ }
321
+ /**
322
+ * Push the OPEN hold's expiry out ("need more time?"). Resolves the refreshed
323
+ * hold, or `null` when there is nothing held or the server refused (the hold
324
+ * is gone, already expired, or at its renewal cap) — refusal is a normal
325
+ * outcome, not an error, so the host decides the copy. The client-side expiry
326
+ * timer is re-armed to match, so `onHoldExpired` won't fire early.
327
+ */
328
+ async extendHold(ttlMs) {
329
+ try {
330
+ const h = await this.controller.extendHold(ttlMs);
278
331
  return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
279
332
  } catch (err) {
280
333
  this.opts.onError?.(err);
@@ -291,23 +344,31 @@ var SeatingChart = class {
291
344
  }
292
345
  async holdGA(areaId, qty, options = {}) {
293
346
  try {
294
- const h = await this.controller.holdGA(areaId, qty, options);
295
- return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
347
+ return await this.holdGAOrThrow(areaId, qty, options);
296
348
  } catch (err) {
297
349
  this.opts.onError?.(err);
298
350
  return null;
299
351
  }
300
352
  }
353
+ /** @internal Throwing variant of {@link holdGA} for the native host adapter. See {@link holdOrThrow}. */
354
+ async holdGAOrThrow(areaId, qty, options = {}) {
355
+ const h = await this.controller.holdGA(areaId, qty, options);
356
+ return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
357
+ }
301
358
  /** Ask the server for the `qty` best free seats and hold them atomically. */
302
359
  async bestAvailable(qty, categoryKey) {
303
360
  try {
304
- const h = await this.controller.bestAvailable(qty, categoryKey);
305
- return h ? { holdId: h.holdId, expiresAt: h.expiresAt, labels: h.labels, seats: h.seats, items: h.items } : null;
361
+ return await this.bestAvailableOrThrow(qty, categoryKey);
306
362
  } catch (err) {
307
363
  this.opts.onError?.(err);
308
364
  return null;
309
365
  }
310
366
  }
367
+ /** @internal Throwing variant of {@link bestAvailable} for the native host adapter. See {@link holdOrThrow}. */
368
+ async bestAvailableOrThrow(qty, categoryKey) {
369
+ const h = await this.controller.bestAvailable(qty, categoryKey);
370
+ return h ? { holdId: h.holdId, expiresAt: h.expiresAt, labels: h.labels, seats: h.seats, items: h.items } : null;
371
+ }
311
372
  /**
312
373
  * Choose a ticket tier for a selected seat (e.g. Adult → Child). The seat's
313
374
  * available `tiers` are on each `SelectedSeat` from `getSelection()` /
@@ -368,6 +429,7 @@ var SeatingChart = class {
368
429
  this.hostEl = null;
369
430
  this.mount = null;
370
431
  this.rendered = false;
432
+ this.mode_ = null;
371
433
  }
372
434
  };
373
435
 
@@ -1462,6 +1524,17 @@ var CSS = `
1462
1524
  .sl-ba-title .spark{color:var(--sl-accent);font-size:16px}
1463
1525
  .sl-ba-copy{grid-column:1/-1;margin:-4px 0 2px 23px;color:var(--sl-muted);font-size:10.5px;line-height:1.35}
1464
1526
  .sl-ba-copy .narrow{display:none}
1527
+ /* "\u2605 Best seats" premium quick-pick \u2014 gold accent echoing the \u2605 Premium pill on
1528
+ the confirm popover; deliberately distinct from the accent-toned qty/go. */
1529
+ .sl-ba-premium{position:relative;z-index:1;grid-column:1/-1;justify-self:start;display:inline-flex;align-items:center;gap:6px;
1530
+ padding:6px 12px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.02em;cursor:pointer;
1531
+ color:#c9a24b;background:color-mix(in srgb,#e8c15a 10%,var(--sl-surface));
1532
+ border:1px solid color-mix(in srgb,#e8c15a 34%,var(--sl-line));transition:filter .15s,background .15s,color .15s}
1533
+ .sl-ba-premium .star{font-size:12px;line-height:1;color:#e8c15a}
1534
+ .sl-ba-premium:hover{filter:brightness(1.05)}
1535
+ .sl-ba-premium.on{color:#1c1608;background:linear-gradient(135deg,#f0cf6b,#e0b23f);border-color:transparent;
1536
+ box-shadow:0 6px 16px color-mix(in srgb,#e8c15a 26%,transparent)}
1537
+ .sl-ba-premium.on .star{color:#5a4410}
1465
1538
  .sl-ba select{background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:8px;
1466
1539
  font:inherit;font-size:11px;padding:7px 8px;min-width:0;width:100%;max-width:none}
1467
1540
  .sl-ba-qty{display:flex;align-items:center;gap:7px;padding:3px;border:1px solid var(--sl-line);border-radius:9px;background:var(--sl-surface)}
@@ -1559,6 +1632,25 @@ var CSS = `
1559
1632
  .sl-confirm-view:hover{border-color:var(--sl-muted)}
1560
1633
  .sl-confirm-view svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
1561
1634
 
1635
+ /* commercial seat flags \u2014 limited-view caution + premium tag. Amber tone,
1636
+ deliberately distinct from the red taken/held state; shown on the confirm
1637
+ card, echoed as a small \u25D0 marker on cart chips and the hover tip. */
1638
+ .sl-cx{display:flex;flex-direction:column;gap:6px;margin-bottom:10px}
1639
+ .sl-cx-warn{display:flex;align-items:flex-start;gap:7px;padding:8px 10px;border-radius:9px;
1640
+ background:color-mix(in srgb,#f4b740 13%,var(--sl-surface));border:1px solid color-mix(in srgb,#f4b740 40%,var(--sl-line));
1641
+ animation:slNoticeIn .28s ease both}
1642
+ .sl-cx-glyph{flex:none;font-size:14px;line-height:1.2;color:#f4b740}
1643
+ .sl-cx-txt{min-width:0;display:flex;flex-direction:column;gap:2px}
1644
+ .sl-cx-txt b{font-size:12px;font-weight:800;color:var(--sl-text)}
1645
+ .sl-cx-note{font-size:11px;line-height:1.4;color:var(--sl-muted)}
1646
+ .sl-cx-premium{display:inline-flex;align-items:center;gap:6px;align-self:flex-start;padding:4px 10px;border-radius:999px;
1647
+ font-size:11px;font-weight:800;letter-spacing:.02em;color:#c9a24b;
1648
+ background:color-mix(in srgb,#e8c15a 13%,var(--sl-surface));border:1px solid color-mix(in srgb,#e8c15a 38%,var(--sl-line))}
1649
+ .sl-cx-star{font-size:12px;line-height:1;color:#e8c15a}
1650
+ .sl-cx-mark{flex:none;font-size:12px;line-height:1;color:#f4b740;cursor:help}
1651
+ .sl-tip-cx{display:flex;align-items:center;gap:6px;padding:5px 10px 7px;font-size:10.5px;font-weight:700;color:#e8b24a}
1652
+ .sl-tip-cx .g{font-size:12px}
1653
+
1562
1654
  /* 360\xB0 seat-view modal (fills the widget; drag-to-look-around equirectangular) */
1563
1655
  .sl-view{position:absolute;inset:0;z-index:12;display:flex;flex-direction:column;background:var(--sl-bg)}
1564
1656
  .sl-view-head{display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid var(--sl-line);flex:none}
@@ -1687,6 +1779,8 @@ var SeatPicker = class _SeatPicker {
1687
1779
  this.srEl = null;
1688
1780
  this.baQty = 2;
1689
1781
  this.baCat = "";
1782
+ /** "★ Best seats" premium quick-pick toggle — biases best-available to premium seats. */
1783
+ this.baPremium = false;
1690
1784
  this.bestAvailableConfirm = false;
1691
1785
  this.releasingHold = false;
1692
1786
  /** Event sales window is closed (read-only load state / live close). */
@@ -1843,6 +1937,50 @@ var SeatPicker = class _SeatPicker {
1843
1937
  const sight = distance != null ? (0, import_core2.t)("picker.sightline", { m: distance }) : this.tf("picker.sightlineClear", "Clear sightline");
1844
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>`;
1845
1939
  }
1940
+ /** Minimal HTML/attribute escaper for buyer-authored commercial text (notes). */
1941
+ escCx(value) {
1942
+ return String(value ?? "").replace(/[&<>"]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch]);
1943
+ }
1944
+ /** Localized "Restricted view" / "Obstructed view" label for a seat's flags,
1945
+ * or '' when neither is set. Restricted takes precedence when both are on. */
1946
+ limitedViewLabel(c) {
1947
+ if (c?.restrictedView) return this.tf("picker.restrictedView", "Restricted view");
1948
+ if (c?.obstructedView) return this.tf("picker.obstructedView", "Obstructed view");
1949
+ return "";
1950
+ }
1951
+ /**
1952
+ * Commercial flags block for the confirm/detail surface: a subtle ★ Premium
1953
+ * tag plus an amber ◐ limited-view caution (with the organizer's note when
1954
+ * present). '' when the seat carries no surfaced commercial flag.
1955
+ */
1956
+ commercialConfirmHtml(c) {
1957
+ if (!c) return "";
1958
+ const rows = [];
1959
+ if (c.premium) {
1960
+ rows.push(
1961
+ `<div class="sl-cx-premium"><span class="sl-cx-star" aria-hidden="true">\u2605</span>${this.tf("picker.premiumSeat", "Premium seat")}</div>`
1962
+ );
1963
+ }
1964
+ const limited = this.limitedViewLabel(c);
1965
+ if (limited) {
1966
+ rows.push(
1967
+ `<div class="sl-cx-warn"><span class="sl-cx-glyph" aria-hidden="true">\u25D0</span><span class="sl-cx-txt"><b>${limited}</b>${c.note ? `<span class="sl-cx-note">${this.escCx(c.note)}</span>` : ""}</span></div>`
1968
+ );
1969
+ } else if (c.note) {
1970
+ rows.push(
1971
+ `<div class="sl-cx-warn"><span class="sl-cx-glyph" aria-hidden="true">\u2139</span><span class="sl-cx-txt"><span class="sl-cx-note">${this.escCx(c.note)}</span></span></div>`
1972
+ );
1973
+ }
1974
+ return rows.length ? `<div class="sl-cx">${rows.join("")}</div>` : "";
1975
+ }
1976
+ /** Small ◐ limited-view marker for a cart chip; title/aria uses the seat's
1977
+ * note when present, else the generic view label. '' for a clear-view seat. */
1978
+ commercialChipMarker(c) {
1979
+ const limited = this.limitedViewLabel(c);
1980
+ if (!limited) return "";
1981
+ const title = this.escCx(c?.note ? c.note : limited);
1982
+ return `<span class="sl-cx-mark" role="img" aria-label="${title}" title="${title}">\u25D0</span>`;
1983
+ }
1846
1984
  /** True when the picker is rendered inside an iframe (snippet embed at /e/:key). */
1847
1985
  isFramed() {
1848
1986
  return typeof window !== "undefined" && window.parent !== window;
@@ -2215,45 +2353,71 @@ var SeatPicker = class _SeatPicker {
2215
2353
  this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
2216
2354
  this.buildBadge(chartTheme);
2217
2355
  const present = /* @__PURE__ */ new Set();
2356
+ let hasLimitedView = false;
2218
2357
  if (this.controller.doc) {
2219
2358
  for (const seat of (0, import_core2.expandChart)(this.controller.doc)) {
2220
2359
  for (const type of seat.accessibility ?? []) present.add(type);
2221
2360
  if (seat.accessible && !seat.accessibility?.length) present.add("wheelchair");
2361
+ if (seat.commercial?.restrictedView || seat.commercial?.obstructedView) hasLimitedView = true;
2222
2362
  }
2223
2363
  }
2224
- if (present.size) {
2364
+ const focusSeatsForFilter = () => {
2365
+ if (this.rungsEl && this.controller.getRung() !== "seats") {
2366
+ this.controller.setRung("seats");
2367
+ this.collapseSectionCard();
2368
+ this.syncRung();
2369
+ }
2370
+ };
2371
+ if (present.size || hasLimitedView) {
2225
2372
  const chips = document.createElement("div");
2226
2373
  chips.className = "sl-chips";
2227
- const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
2228
- const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-f="${key}">${label}</button>`;
2229
- chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
2230
2374
  this.regions["top-left"].appendChild(chips);
2231
2375
  this.a11yChipsEl = chips;
2232
- const active = /* @__PURE__ */ new Set();
2233
- const syncChips = () => {
2234
- chips.querySelectorAll("button").forEach((b) => {
2235
- const f = b.dataset.f;
2236
- const on = f === "all" ? active.size === 0 : active.has(f);
2237
- b.classList.toggle("on", on);
2238
- b.setAttribute("aria-pressed", String(on));
2376
+ if (present.size) {
2377
+ const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
2378
+ const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-a11y="1" data-f="${key}">${label}</button>`;
2379
+ chips.insertAdjacentHTML(
2380
+ "beforeend",
2381
+ mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("")
2382
+ );
2383
+ const active = /* @__PURE__ */ new Set();
2384
+ const syncChips = () => {
2385
+ chips.querySelectorAll("button[data-a11y]").forEach((b) => {
2386
+ const f = b.dataset.f;
2387
+ const on = f === "all" ? active.size === 0 : active.has(f);
2388
+ b.classList.toggle("on", on);
2389
+ b.setAttribute("aria-pressed", String(on));
2390
+ });
2391
+ const filter = active.size ? [...active] : null;
2392
+ this.controller.setAccessibilityFilter(filter);
2393
+ if (filter) focusSeatsForFilter();
2394
+ };
2395
+ chips.querySelectorAll("button[data-a11y]").forEach((btn) => {
2396
+ btn.addEventListener("click", () => {
2397
+ const f = btn.dataset.f;
2398
+ if (f === "all") active.clear();
2399
+ else if (active.has(f)) active.delete(f);
2400
+ else active.add(f);
2401
+ syncChips();
2402
+ });
2239
2403
  });
2240
- const filter = active.size ? [...active] : null;
2241
- this.controller.setAccessibilityFilter(filter);
2242
- if (filter && this.rungsEl && this.controller.getRung() !== "seats") {
2243
- this.controller.setRung("seats");
2244
- this.collapseSectionCard();
2245
- this.syncRung();
2246
- }
2247
- };
2248
- chips.querySelectorAll("button").forEach((btn) => {
2249
- btn.addEventListener("click", () => {
2250
- const f = btn.dataset.f;
2251
- if (f === "all") active.clear();
2252
- else if (active.has(f)) active.delete(f);
2253
- else active.add(f);
2254
- syncChips();
2404
+ }
2405
+ if (hasLimitedView) {
2406
+ const limited = document.createElement("button");
2407
+ limited.type = "button";
2408
+ limited.className = "sl-chip-f";
2409
+ limited.setAttribute("aria-pressed", "false");
2410
+ limited.innerHTML = `\u25D0 ${this.tf("picker.hideLimitedView", "Hide limited-view seats")}`;
2411
+ chips.appendChild(limited);
2412
+ let limitedOn = false;
2413
+ limited.addEventListener("click", () => {
2414
+ limitedOn = !limitedOn;
2415
+ limited.classList.toggle("on", limitedOn);
2416
+ limited.setAttribute("aria-pressed", String(limitedOn));
2417
+ this.controller.setCommercialLimitedFilter(limitedOn);
2418
+ if (limitedOn) focusSeatsForFilter();
2255
2419
  });
2256
- });
2420
+ }
2257
2421
  }
2258
2422
  const cb = document.createElement("button");
2259
2423
  cb.type = "button";
@@ -2976,7 +3140,7 @@ var SeatPicker = class _SeatPicker {
2976
3140
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
2977
3141
  return `<span class="sl-seccard-mix-item${dim ? " sl-dim" : ""}"><span class="sl-seccard-mix-dot" style="background:${c.color}"></span>${c.label} <span class="sl-seccard-mix-price">${this.money(this.paidPrice(c.key, null, c.price))}</span></span>`;
2978
3142
  }).join("");
2979
- card.innerHTML = `<div class="sl-seccard-head"><span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span>` + (summary.categories.length ? `<span class="sl-seccard-price">${priceLabel}</span>` : "") + xBtn + `</div><div class="sl-seccard-zone">${summary.zoneLabel ? `${summary.zoneLabel} \xB7 ` : ""}<span class="sl-seccard-left">${leftLabel}</span></div>` + (mix ? `<div class="sl-seccard-mix">${mix}</div>` : "") + `<div class="sl-seccard-foot"><button type="button" class="sl-seccard-overview">\u2190 ${(0, import_core2.t)("picker.overview")}</button><span class="sl-seccard-hint">${(0, import_core2.t)("picker.tapSeatHint")}</span></div>`;
3143
+ card.innerHTML = `<div class="sl-seccard-head"><span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span>` + (summary.categories.length ? `<span class="sl-seccard-price">${priceLabel}</span>` : "") + xBtn + `</div><div class="sl-seccard-zone">${summary.zoneLabel ? `${summary.zoneLabel} \xB7 ` : ""}<span class="sl-seccard-left">${leftLabel}</span></div>` + (summary.entrance ? `<div class="sl-seccard-entrance">${(0, import_core2.t)("picker.entrance")} ${String(summary.entrance).replace(/[&<>"]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch])}</div>` : "") + (mix ? `<div class="sl-seccard-mix">${mix}</div>` : "") + `<div class="sl-seccard-foot"><button type="button" class="sl-seccard-overview">\u2190 ${(0, import_core2.t)("picker.overview")}</button><span class="sl-seccard-hint">${(0, import_core2.t)("picker.tapSeatHint")}</span></div>`;
2980
3144
  card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
2981
3145
  card.querySelector(".sl-seccard-overview").addEventListener("click", () => this.controller.overview());
2982
3146
  (this.regions["top-center"] ?? this.els.map).appendChild(card);
@@ -3070,7 +3234,7 @@ var SeatPicker = class _SeatPicker {
3070
3234
  el.setAttribute("aria-modal", "true");
3071
3235
  el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
3072
3236
  el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
3073
- el.innerHTML = `<div class="sl-confirm-grid"><div class="sl-confirm-field"><span class="sl-confirm-key">Section</span><span class="sl-confirm-value">${safe(details?.sectionLabel)}</span></div><div class="sl-confirm-field"><span class="sl-confirm-key">Row</span><span class="sl-confirm-value">${safe(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.label)}</span></div></div><div class="sl-confirm-cat"><span class="sl-dot" style="background:${cat?.color ?? "#6e7bff"}"></span><span class="sl-confirm-cat-name">${safe(details?.categoryLabel ?? cat?.label ?? seat.categoryKey)}</span>` + (price != null ? `<span class="sl-confirm-price">${this.money(price)}</span>` : "") + `</div><div class="sl-confirm-body">` + (this.seatViewEnabled() ? 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>`;
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>`;
3074
3238
  this.els.map.appendChild(el);
3075
3239
  this.confirmEl = el;
3076
3240
  this.reanchorConfirm();
@@ -3365,14 +3529,16 @@ var SeatPicker = class _SeatPicker {
3365
3529
  const noPicks = !seats.length && !heldItems.length && !this.pendingGACount();
3366
3530
  if (!this.hold && (noPicks || this.bestAvailableBusy || this.bestAvailableConfirm)) {
3367
3531
  const cats = this.controller.doc?.categories ?? [];
3368
- parts.push(this.bestAvailableConfirm ? `<div class="sl-ba" role="alert"><div class="sl-ba-title"><span class="spark" aria-hidden="true">\u2726</span>Replace your current choices?</div><div class="sl-ba-replace"><b>We\u2019ll find ${this.baQty} seats together.</b><span>Your manually selected tickets will be removed only after a new group is secured.</span></div><div class="sl-ba-actions"><button type="button" data-ba-cancel>Keep mine</button><button type="button" class="replace" data-ba-replace>Find new seats</button></div></div>` : `<div class="sl-ba"><div class="sl-ba-title"><span class="spark" aria-hidden="true">\u2726</span>Find the best seats together</div><div class="sl-ba-copy"><span class="wide">We\u2019ll choose the closest available group for you.</span><span class="narrow">Closest available group, chosen instantly.</span></div>` + (cats.length > 1 ? `<select aria-label="Preferred ticket type" data-ba-cat><option value="">Any ticket type</option>` + cats.map((c) => `<option value="${c.key}"${this.baCat === c.key ? " selected" : ""}>${c.label}</option>`).join("") + `</select>` : `<span aria-hidden="true"></span>`) + `<div class="sl-ba-qty"><button type="button" data-ba="-1" aria-label="Fewer seats">\u2212</button><span>${this.baQty}</span><button type="button" data-ba="1" aria-label="More seats">+</button></div><button type="button" class="sl-ba-go"${this.bestAvailableBusy ? " disabled" : ""}>` + (this.bestAvailableBusy ? `<span class="sl-ba-spin" aria-hidden="true"></span>Finding the best seats\u2026` : `Find ${this.baQty} best ${this.baQty === 1 ? "seat" : "seats"}`) + `</button></div>`);
3532
+ 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
+ // 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>`);
3369
3535
  }
3370
3536
  const idGrid = (seatId, label) => {
3371
3537
  const d = seatId ? this.controller.seatDetails(seatId) : null;
3372
3538
  if (!d?.sectionLabel && !d?.rowLabel && !d?.seatNumber) {
3373
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>`;
3374
3540
  }
3375
- 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">Row</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>`;
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>`;
3376
3542
  };
3377
3543
  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>`;
3378
3544
  for (const item of heldItems) {
@@ -3383,7 +3549,7 @@ var SeatPicker = class _SeatPicker {
3383
3549
  const heldSeat = item.objectType !== "ga" ? this.controller.seatByLabel(item.label) : null;
3384
3550
  const canView2 = this.seatViewEnabled() && !!heldSeat;
3385
3551
  parts.push(
3386
- `<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><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>`
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>`
3387
3553
  );
3388
3554
  }
3389
3555
  const heldLabels = new Set(heldItems.map((item) => item.label));
@@ -3394,7 +3560,7 @@ var SeatPicker = class _SeatPicker {
3394
3560
  const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
3395
3561
  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>` : "";
3396
3562
  parts.push(
3397
- `<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) + `<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>${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>`
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>`
3398
3564
  );
3399
3565
  }
3400
3566
  for (const area of gaAreas) {
@@ -3414,6 +3580,10 @@ var SeatPicker = class _SeatPicker {
3414
3580
  this.els.tray.querySelector("[data-ba-cat]")?.addEventListener("change", (e) => {
3415
3581
  this.baCat = e.target.value;
3416
3582
  });
3583
+ this.els.tray.querySelector("[data-ba-premium]")?.addEventListener("click", () => {
3584
+ this.baPremium = !this.baPremium;
3585
+ this.syncTray();
3586
+ });
3417
3587
  this.els.tray.querySelector(".sl-ba-go")?.addEventListener("click", () => {
3418
3588
  if (this.pendingSelectionCount() > 0) {
3419
3589
  this.bestAvailableConfirm = true;
@@ -3421,7 +3591,7 @@ var SeatPicker = class _SeatPicker {
3421
3591
  this.els.tray.querySelector("[data-ba-replace]")?.focus();
3422
3592
  return;
3423
3593
  }
3424
- void this.bestAvailable(this.baQty, this.baCat || void 0);
3594
+ void this.bestAvailable(this.baQty, this.baCat || void 0, { preferPremium: this.baPremium });
3425
3595
  });
3426
3596
  this.els.tray.querySelector("[data-ba-cancel]")?.addEventListener("click", () => {
3427
3597
  this.bestAvailableConfirm = false;
@@ -3430,7 +3600,7 @@ var SeatPicker = class _SeatPicker {
3430
3600
  });
3431
3601
  this.els.tray.querySelector("[data-ba-replace]")?.addEventListener("click", () => {
3432
3602
  this.bestAvailableConfirm = false;
3433
- void this.bestAvailable(this.baQty, this.baCat || void 0);
3603
+ void this.bestAvailable(this.baQty, this.baCat || void 0, { preferPremium: this.baPremium });
3434
3604
  });
3435
3605
  this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
3436
3606
  btn.addEventListener("click", () => {
@@ -3812,6 +3982,12 @@ var SeatPicker = class _SeatPicker {
3812
3982
  * the prefix is exact (won't touch "1040-A" under section "104"); otherwise
3813
3983
  * the label is shown verbatim.
3814
3984
  */
3985
+ /** Buyer-facing type word for the row/table key label — the designer's
3986
+ * per-object "Displayed type" override, or the default "Row". */
3987
+ rowTypeWord(details) {
3988
+ const t3 = details?.rowType?.trim();
3989
+ return t3 || "Row";
3990
+ }
3815
3991
  rowShort(details) {
3816
3992
  const row = details?.rowLabel;
3817
3993
  const sec = details?.sectionLabel;
@@ -3831,10 +4007,12 @@ var SeatPicker = class _SeatPicker {
3831
4007
  const esc2 = (v) => String(v ?? "\u2014").replace(/[&<>"]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch]);
3832
4008
  const price = this.money(this.paidPrice(details.categoryKey, details.tierId ?? null, details.price));
3833
4009
  const hasLoc = details.sectionLabel || details.rowLabel || details.seatNumber;
3834
- 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">Row</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.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.label)}</span></div></div>`;
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>`;
3835
4011
  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
+ const limited = this.limitedViewLabel(details.commercial);
4013
+ const cxLine = limited ? `<div class="sl-tip-cx"><span class="g" aria-hidden="true">\u25D0</span>${esc2(limited)}</div>` : "";
3836
4014
  this.tipEl.style.setProperty("--sl-cat", details.categoryColor);
3837
- 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>` + statusLine;
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;
3838
4016
  this.tipEl.style.display = "block";
3839
4017
  this.placeTooltip();
3840
4018
  }
@@ -3854,7 +4032,7 @@ var SeatPicker = class _SeatPicker {
3854
4032
  async removeHeldTicket(label) {
3855
4033
  return this.removeHeldLabel(label);
3856
4034
  }
3857
- async bestAvailable(qty, categoryKey) {
4035
+ async bestAvailable(qty, categoryKey, opts = {}) {
3858
4036
  if (this.salesClosed || this.bestAvailableBusy) return null;
3859
4037
  qty = Math.max(1, Math.min(this.maxTickets, Math.floor(qty)));
3860
4038
  if (this.confirmSeat) this.cancelConfirm();
@@ -3866,7 +4044,7 @@ var SeatPicker = class _SeatPicker {
3866
4044
  button.innerHTML = '<span class="sl-ba-spin" aria-hidden="true"></span>Finding\u2026';
3867
4045
  }
3868
4046
  try {
3869
- const h = await this.controller.bestAvailable(qty, categoryKey);
4047
+ const h = await this.controller.bestAvailable(qty, categoryKey, opts);
3870
4048
  if (h) {
3871
4049
  this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
3872
4050
  this.handedOff = false;
@@ -3876,6 +4054,9 @@ var SeatPicker = class _SeatPicker {
3876
4054
  this.flashHeldSeats(this.hold);
3877
4055
  this.syncTray();
3878
4056
  this.emitHoldChange();
4057
+ if (opts.preferPremium && h.seats.length && !h.seats.every((s) => s.commercial?.premium)) {
4058
+ this.toast((0, import_core2.t)("picker.premiumFallbackNote", { count: qty }), "neutral");
4059
+ }
3879
4060
  return this.hold;
3880
4061
  }
3881
4062
  return null;