@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.js CHANGED
@@ -25,7 +25,13 @@ async function request(base, path, init = {}) {
25
25
  const data = isJson ? await res.json().catch(() => null) : null;
26
26
  if (!res.ok) {
27
27
  const err = data;
28
- throw new ApiError(res.status, err?.error ?? `request_failed_${res.status}`, err?.code, err?.conflicts, err?.reason);
28
+ throw new ApiError(
29
+ res.status,
30
+ err?.error ?? `request_failed_${res.status}`,
31
+ err?.code ?? err?.error,
32
+ err?.conflicts,
33
+ err?.reason
34
+ );
29
35
  }
30
36
  return data;
31
37
  }
@@ -98,6 +104,7 @@ var SeatingChart = class {
98
104
  this.mount = null;
99
105
  this.hostEl = null;
100
106
  this.rendered = false;
107
+ this.mode_ = null;
101
108
  this.tipEl = null;
102
109
  this.tipPos = { x: 0, y: 0 };
103
110
  this.onTipMove = null;
@@ -151,6 +158,7 @@ var SeatingChart = class {
151
158
  this.rendered = false;
152
159
  return this;
153
160
  }
161
+ this.mode_ = info.mode === "test" ? "test" : "live";
154
162
  if (this.opts.seatTooltip !== false) {
155
163
  const tip = document.createElement("div");
156
164
  tip.setAttribute("role", "tooltip");
@@ -224,6 +232,19 @@ var SeatingChart = class {
224
232
  this.tipEl.style.display = "block";
225
233
  this.placeTooltip();
226
234
  }
235
+ /**
236
+ * Whether the SERVED event is a live or a test event (`sk_test_` keys create
237
+ * test events, which never book real inventory). `null` before render()
238
+ * resolves — the mode comes from the server with the chart, not from options.
239
+ *
240
+ * The widget already surfaces this visually with the test-mode ribbon; this
241
+ * getter is for hosts that draw their own chrome — notably a native WebView
242
+ * wrapper, which must be able to tell an integrator that the build they are
243
+ * about to ship is pointed at a test event.
244
+ */
245
+ getMode() {
246
+ return this.mode_;
247
+ }
227
248
  /** Current selection with prices resolved from the chart categories. */
228
249
  getSelection() {
229
250
  return this.controller.getSelection();
@@ -231,17 +252,49 @@ var SeatingChart = class {
231
252
  /** Hold the current selection. Resolves the hold, or null on a 409 conflict. */
232
253
  async hold(options = {}) {
233
254
  try {
234
- const h = await this.controller.hold(void 0, options.ttlMs);
235
- return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
255
+ return await this.holdOrThrow(options);
236
256
  } catch (err) {
237
257
  this.opts.onError?.(err);
238
258
  return null;
239
259
  }
240
260
  }
261
+ /**
262
+ * @internal Like {@link hold} but RE-THROWS the structured API error (409
263
+ * `reason`/`code` + `conflicts`) instead of swallowing it into `onError` +
264
+ * `null`. The native WebView host adapter needs the throw so it can answer the
265
+ * originating command with a correlated error carrying the SPECIFIC reason
266
+ * (`sold_out` vs `not_enough_together`); the public method above keeps the
267
+ * catch-and-onError contract that direct web consumers rely on. Not a stable
268
+ * part of the embed API.
269
+ */
270
+ async holdOrThrow(options = {}) {
271
+ const h = await this.controller.hold(void 0, options.ttlMs);
272
+ return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
273
+ }
241
274
  /** Restore an active hold by its opaque id without extending its expiry. */
242
275
  async resumeHold(holdId) {
243
276
  try {
244
- const h = await this.controller.resumeHold(holdId);
277
+ return await this.resumeHoldOrThrow(holdId);
278
+ } catch (err) {
279
+ this.opts.onError?.(err);
280
+ return null;
281
+ }
282
+ }
283
+ /** @internal Throwing variant of {@link resumeHold} for the native host adapter. See {@link holdOrThrow}. */
284
+ async resumeHoldOrThrow(holdId) {
285
+ const h = await this.controller.resumeHold(holdId);
286
+ return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
287
+ }
288
+ /**
289
+ * Push the OPEN hold's expiry out ("need more time?"). Resolves the refreshed
290
+ * hold, or `null` when there is nothing held or the server refused (the hold
291
+ * is gone, already expired, or at its renewal cap) — refusal is a normal
292
+ * outcome, not an error, so the host decides the copy. The client-side expiry
293
+ * timer is re-armed to match, so `onHoldExpired` won't fire early.
294
+ */
295
+ async extendHold(ttlMs) {
296
+ try {
297
+ const h = await this.controller.extendHold(ttlMs);
245
298
  return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
246
299
  } catch (err) {
247
300
  this.opts.onError?.(err);
@@ -258,23 +311,31 @@ var SeatingChart = class {
258
311
  }
259
312
  async holdGA(areaId, qty, options = {}) {
260
313
  try {
261
- const h = await this.controller.holdGA(areaId, qty, options);
262
- return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
314
+ return await this.holdGAOrThrow(areaId, qty, options);
263
315
  } catch (err) {
264
316
  this.opts.onError?.(err);
265
317
  return null;
266
318
  }
267
319
  }
320
+ /** @internal Throwing variant of {@link holdGA} for the native host adapter. See {@link holdOrThrow}. */
321
+ async holdGAOrThrow(areaId, qty, options = {}) {
322
+ const h = await this.controller.holdGA(areaId, qty, options);
323
+ return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
324
+ }
268
325
  /** Ask the server for the `qty` best free seats and hold them atomically. */
269
326
  async bestAvailable(qty, categoryKey) {
270
327
  try {
271
- const h = await this.controller.bestAvailable(qty, categoryKey);
272
- return h ? { holdId: h.holdId, expiresAt: h.expiresAt, labels: h.labels, seats: h.seats, items: h.items } : null;
328
+ return await this.bestAvailableOrThrow(qty, categoryKey);
273
329
  } catch (err) {
274
330
  this.opts.onError?.(err);
275
331
  return null;
276
332
  }
277
333
  }
334
+ /** @internal Throwing variant of {@link bestAvailable} for the native host adapter. See {@link holdOrThrow}. */
335
+ async bestAvailableOrThrow(qty, categoryKey) {
336
+ const h = await this.controller.bestAvailable(qty, categoryKey);
337
+ return h ? { holdId: h.holdId, expiresAt: h.expiresAt, labels: h.labels, seats: h.seats, items: h.items } : null;
338
+ }
278
339
  /**
279
340
  * Choose a ticket tier for a selected seat (e.g. Adult → Child). The seat's
280
341
  * available `tiers` are on each `SelectedSeat` from `getSelection()` /
@@ -335,6 +396,7 @@ var SeatingChart = class {
335
396
  this.hostEl = null;
336
397
  this.mount = null;
337
398
  this.rendered = false;
399
+ this.mode_ = null;
338
400
  }
339
401
  };
340
402
 
@@ -1438,6 +1500,17 @@ var CSS = `
1438
1500
  .sl-ba-title .spark{color:var(--sl-accent);font-size:16px}
1439
1501
  .sl-ba-copy{grid-column:1/-1;margin:-4px 0 2px 23px;color:var(--sl-muted);font-size:10.5px;line-height:1.35}
1440
1502
  .sl-ba-copy .narrow{display:none}
1503
+ /* "\u2605 Best seats" premium quick-pick \u2014 gold accent echoing the \u2605 Premium pill on
1504
+ the confirm popover; deliberately distinct from the accent-toned qty/go. */
1505
+ .sl-ba-premium{position:relative;z-index:1;grid-column:1/-1;justify-self:start;display:inline-flex;align-items:center;gap:6px;
1506
+ padding:6px 12px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.02em;cursor:pointer;
1507
+ color:#c9a24b;background:color-mix(in srgb,#e8c15a 10%,var(--sl-surface));
1508
+ border:1px solid color-mix(in srgb,#e8c15a 34%,var(--sl-line));transition:filter .15s,background .15s,color .15s}
1509
+ .sl-ba-premium .star{font-size:12px;line-height:1;color:#e8c15a}
1510
+ .sl-ba-premium:hover{filter:brightness(1.05)}
1511
+ .sl-ba-premium.on{color:#1c1608;background:linear-gradient(135deg,#f0cf6b,#e0b23f);border-color:transparent;
1512
+ box-shadow:0 6px 16px color-mix(in srgb,#e8c15a 26%,transparent)}
1513
+ .sl-ba-premium.on .star{color:#5a4410}
1441
1514
  .sl-ba select{background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:8px;
1442
1515
  font:inherit;font-size:11px;padding:7px 8px;min-width:0;width:100%;max-width:none}
1443
1516
  .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)}
@@ -1535,6 +1608,25 @@ var CSS = `
1535
1608
  .sl-confirm-view:hover{border-color:var(--sl-muted)}
1536
1609
  .sl-confirm-view svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
1537
1610
 
1611
+ /* commercial seat flags \u2014 limited-view caution + premium tag. Amber tone,
1612
+ deliberately distinct from the red taken/held state; shown on the confirm
1613
+ card, echoed as a small \u25D0 marker on cart chips and the hover tip. */
1614
+ .sl-cx{display:flex;flex-direction:column;gap:6px;margin-bottom:10px}
1615
+ .sl-cx-warn{display:flex;align-items:flex-start;gap:7px;padding:8px 10px;border-radius:9px;
1616
+ background:color-mix(in srgb,#f4b740 13%,var(--sl-surface));border:1px solid color-mix(in srgb,#f4b740 40%,var(--sl-line));
1617
+ animation:slNoticeIn .28s ease both}
1618
+ .sl-cx-glyph{flex:none;font-size:14px;line-height:1.2;color:#f4b740}
1619
+ .sl-cx-txt{min-width:0;display:flex;flex-direction:column;gap:2px}
1620
+ .sl-cx-txt b{font-size:12px;font-weight:800;color:var(--sl-text)}
1621
+ .sl-cx-note{font-size:11px;line-height:1.4;color:var(--sl-muted)}
1622
+ .sl-cx-premium{display:inline-flex;align-items:center;gap:6px;align-self:flex-start;padding:4px 10px;border-radius:999px;
1623
+ font-size:11px;font-weight:800;letter-spacing:.02em;color:#c9a24b;
1624
+ background:color-mix(in srgb,#e8c15a 13%,var(--sl-surface));border:1px solid color-mix(in srgb,#e8c15a 38%,var(--sl-line))}
1625
+ .sl-cx-star{font-size:12px;line-height:1;color:#e8c15a}
1626
+ .sl-cx-mark{flex:none;font-size:12px;line-height:1;color:#f4b740;cursor:help}
1627
+ .sl-tip-cx{display:flex;align-items:center;gap:6px;padding:5px 10px 7px;font-size:10.5px;font-weight:700;color:#e8b24a}
1628
+ .sl-tip-cx .g{font-size:12px}
1629
+
1538
1630
  /* 360\xB0 seat-view modal (fills the widget; drag-to-look-around equirectangular) */
1539
1631
  .sl-view{position:absolute;inset:0;z-index:12;display:flex;flex-direction:column;background:var(--sl-bg)}
1540
1632
  .sl-view-head{display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid var(--sl-line);flex:none}
@@ -1663,6 +1755,8 @@ var SeatPicker = class _SeatPicker {
1663
1755
  this.srEl = null;
1664
1756
  this.baQty = 2;
1665
1757
  this.baCat = "";
1758
+ /** "★ Best seats" premium quick-pick toggle — biases best-available to premium seats. */
1759
+ this.baPremium = false;
1666
1760
  this.bestAvailableConfirm = false;
1667
1761
  this.releasingHold = false;
1668
1762
  /** Event sales window is closed (read-only load state / live close). */
@@ -1819,6 +1913,50 @@ var SeatPicker = class _SeatPicker {
1819
1913
  const sight = distance != null ? t2("picker.sightline", { m: distance }) : this.tf("picker.sightlineClear", "Clear sightline");
1820
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>`;
1821
1915
  }
1916
+ /** Minimal HTML/attribute escaper for buyer-authored commercial text (notes). */
1917
+ escCx(value) {
1918
+ return String(value ?? "").replace(/[&<>"]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch]);
1919
+ }
1920
+ /** Localized "Restricted view" / "Obstructed view" label for a seat's flags,
1921
+ * or '' when neither is set. Restricted takes precedence when both are on. */
1922
+ limitedViewLabel(c) {
1923
+ if (c?.restrictedView) return this.tf("picker.restrictedView", "Restricted view");
1924
+ if (c?.obstructedView) return this.tf("picker.obstructedView", "Obstructed view");
1925
+ return "";
1926
+ }
1927
+ /**
1928
+ * Commercial flags block for the confirm/detail surface: a subtle ★ Premium
1929
+ * tag plus an amber ◐ limited-view caution (with the organizer's note when
1930
+ * present). '' when the seat carries no surfaced commercial flag.
1931
+ */
1932
+ commercialConfirmHtml(c) {
1933
+ if (!c) return "";
1934
+ const rows = [];
1935
+ if (c.premium) {
1936
+ rows.push(
1937
+ `<div class="sl-cx-premium"><span class="sl-cx-star" aria-hidden="true">\u2605</span>${this.tf("picker.premiumSeat", "Premium seat")}</div>`
1938
+ );
1939
+ }
1940
+ const limited = this.limitedViewLabel(c);
1941
+ if (limited) {
1942
+ rows.push(
1943
+ `<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>`
1944
+ );
1945
+ } else if (c.note) {
1946
+ rows.push(
1947
+ `<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>`
1948
+ );
1949
+ }
1950
+ return rows.length ? `<div class="sl-cx">${rows.join("")}</div>` : "";
1951
+ }
1952
+ /** Small ◐ limited-view marker for a cart chip; title/aria uses the seat's
1953
+ * note when present, else the generic view label. '' for a clear-view seat. */
1954
+ commercialChipMarker(c) {
1955
+ const limited = this.limitedViewLabel(c);
1956
+ if (!limited) return "";
1957
+ const title = this.escCx(c?.note ? c.note : limited);
1958
+ return `<span class="sl-cx-mark" role="img" aria-label="${title}" title="${title}">\u25D0</span>`;
1959
+ }
1822
1960
  /** True when the picker is rendered inside an iframe (snippet embed at /e/:key). */
1823
1961
  isFramed() {
1824
1962
  return typeof window !== "undefined" && window.parent !== window;
@@ -2191,45 +2329,71 @@ var SeatPicker = class _SeatPicker {
2191
2329
  this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
2192
2330
  this.buildBadge(chartTheme);
2193
2331
  const present = /* @__PURE__ */ new Set();
2332
+ let hasLimitedView = false;
2194
2333
  if (this.controller.doc) {
2195
2334
  for (const seat of expandChart(this.controller.doc)) {
2196
2335
  for (const type of seat.accessibility ?? []) present.add(type);
2197
2336
  if (seat.accessible && !seat.accessibility?.length) present.add("wheelchair");
2337
+ if (seat.commercial?.restrictedView || seat.commercial?.obstructedView) hasLimitedView = true;
2198
2338
  }
2199
2339
  }
2200
- if (present.size) {
2340
+ const focusSeatsForFilter = () => {
2341
+ if (this.rungsEl && this.controller.getRung() !== "seats") {
2342
+ this.controller.setRung("seats");
2343
+ this.collapseSectionCard();
2344
+ this.syncRung();
2345
+ }
2346
+ };
2347
+ if (present.size || hasLimitedView) {
2201
2348
  const chips = document.createElement("div");
2202
2349
  chips.className = "sl-chips";
2203
- const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
2204
- const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-f="${key}">${label}</button>`;
2205
- chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
2206
2350
  this.regions["top-left"].appendChild(chips);
2207
2351
  this.a11yChipsEl = chips;
2208
- const active = /* @__PURE__ */ new Set();
2209
- const syncChips = () => {
2210
- chips.querySelectorAll("button").forEach((b) => {
2211
- const f = b.dataset.f;
2212
- const on = f === "all" ? active.size === 0 : active.has(f);
2213
- b.classList.toggle("on", on);
2214
- b.setAttribute("aria-pressed", String(on));
2352
+ if (present.size) {
2353
+ const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
2354
+ const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-a11y="1" data-f="${key}">${label}</button>`;
2355
+ chips.insertAdjacentHTML(
2356
+ "beforeend",
2357
+ mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("")
2358
+ );
2359
+ const active = /* @__PURE__ */ new Set();
2360
+ const syncChips = () => {
2361
+ chips.querySelectorAll("button[data-a11y]").forEach((b) => {
2362
+ const f = b.dataset.f;
2363
+ const on = f === "all" ? active.size === 0 : active.has(f);
2364
+ b.classList.toggle("on", on);
2365
+ b.setAttribute("aria-pressed", String(on));
2366
+ });
2367
+ const filter = active.size ? [...active] : null;
2368
+ this.controller.setAccessibilityFilter(filter);
2369
+ if (filter) focusSeatsForFilter();
2370
+ };
2371
+ chips.querySelectorAll("button[data-a11y]").forEach((btn) => {
2372
+ btn.addEventListener("click", () => {
2373
+ const f = btn.dataset.f;
2374
+ if (f === "all") active.clear();
2375
+ else if (active.has(f)) active.delete(f);
2376
+ else active.add(f);
2377
+ syncChips();
2378
+ });
2215
2379
  });
2216
- const filter = active.size ? [...active] : null;
2217
- this.controller.setAccessibilityFilter(filter);
2218
- if (filter && this.rungsEl && this.controller.getRung() !== "seats") {
2219
- this.controller.setRung("seats");
2220
- this.collapseSectionCard();
2221
- this.syncRung();
2222
- }
2223
- };
2224
- chips.querySelectorAll("button").forEach((btn) => {
2225
- btn.addEventListener("click", () => {
2226
- const f = btn.dataset.f;
2227
- if (f === "all") active.clear();
2228
- else if (active.has(f)) active.delete(f);
2229
- else active.add(f);
2230
- syncChips();
2380
+ }
2381
+ if (hasLimitedView) {
2382
+ const limited = document.createElement("button");
2383
+ limited.type = "button";
2384
+ limited.className = "sl-chip-f";
2385
+ limited.setAttribute("aria-pressed", "false");
2386
+ limited.innerHTML = `\u25D0 ${this.tf("picker.hideLimitedView", "Hide limited-view seats")}`;
2387
+ chips.appendChild(limited);
2388
+ let limitedOn = false;
2389
+ limited.addEventListener("click", () => {
2390
+ limitedOn = !limitedOn;
2391
+ limited.classList.toggle("on", limitedOn);
2392
+ limited.setAttribute("aria-pressed", String(limitedOn));
2393
+ this.controller.setCommercialLimitedFilter(limitedOn);
2394
+ if (limitedOn) focusSeatsForFilter();
2231
2395
  });
2232
- });
2396
+ }
2233
2397
  }
2234
2398
  const cb = document.createElement("button");
2235
2399
  cb.type = "button";
@@ -2952,7 +3116,7 @@ var SeatPicker = class _SeatPicker {
2952
3116
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
2953
3117
  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>`;
2954
3118
  }).join("");
2955
- 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 ${t2("picker.overview")}</button><span class="sl-seccard-hint">${t2("picker.tapSeatHint")}</span></div>`;
3119
+ 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">${t2("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 ${t2("picker.overview")}</button><span class="sl-seccard-hint">${t2("picker.tapSeatHint")}</span></div>`;
2956
3120
  card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
2957
3121
  card.querySelector(".sl-seccard-overview").addEventListener("click", () => this.controller.overview());
2958
3122
  (this.regions["top-center"] ?? this.els.map).appendChild(card);
@@ -3046,7 +3210,7 @@ var SeatPicker = class _SeatPicker {
3046
3210
  el.setAttribute("aria-modal", "true");
3047
3211
  el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
3048
3212
  el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
3049
- 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>`;
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>`;
3050
3214
  this.els.map.appendChild(el);
3051
3215
  this.confirmEl = el;
3052
3216
  this.reanchorConfirm();
@@ -3341,14 +3505,16 @@ var SeatPicker = class _SeatPicker {
3341
3505
  const noPicks = !seats.length && !heldItems.length && !this.pendingGACount();
3342
3506
  if (!this.hold && (noPicks || this.bestAvailableBusy || this.bestAvailableConfirm)) {
3343
3507
  const cats = this.controller.doc?.categories ?? [];
3344
- 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>`);
3508
+ 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
+ // 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>`);
3345
3511
  }
3346
3512
  const idGrid = (seatId, label) => {
3347
3513
  const d = seatId ? this.controller.seatDetails(seatId) : null;
3348
3514
  if (!d?.sectionLabel && !d?.rowLabel && !d?.seatNumber) {
3349
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>`;
3350
3516
  }
3351
- 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>`;
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>`;
3352
3518
  };
3353
3519
  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>`;
3354
3520
  for (const item of heldItems) {
@@ -3359,7 +3525,7 @@ var SeatPicker = class _SeatPicker {
3359
3525
  const heldSeat = item.objectType !== "ga" ? this.controller.seatByLabel(item.label) : null;
3360
3526
  const canView2 = this.seatViewEnabled() && !!heldSeat;
3361
3527
  parts.push(
3362
- `<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>`
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>`
3363
3529
  );
3364
3530
  }
3365
3531
  const heldLabels = new Set(heldItems.map((item) => item.label));
@@ -3370,7 +3536,7 @@ var SeatPicker = class _SeatPicker {
3370
3536
  const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
3371
3537
  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>` : "";
3372
3538
  parts.push(
3373
- `<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>`
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>`
3374
3540
  );
3375
3541
  }
3376
3542
  for (const area of gaAreas) {
@@ -3390,6 +3556,10 @@ var SeatPicker = class _SeatPicker {
3390
3556
  this.els.tray.querySelector("[data-ba-cat]")?.addEventListener("change", (e) => {
3391
3557
  this.baCat = e.target.value;
3392
3558
  });
3559
+ this.els.tray.querySelector("[data-ba-premium]")?.addEventListener("click", () => {
3560
+ this.baPremium = !this.baPremium;
3561
+ this.syncTray();
3562
+ });
3393
3563
  this.els.tray.querySelector(".sl-ba-go")?.addEventListener("click", () => {
3394
3564
  if (this.pendingSelectionCount() > 0) {
3395
3565
  this.bestAvailableConfirm = true;
@@ -3397,7 +3567,7 @@ var SeatPicker = class _SeatPicker {
3397
3567
  this.els.tray.querySelector("[data-ba-replace]")?.focus();
3398
3568
  return;
3399
3569
  }
3400
- void this.bestAvailable(this.baQty, this.baCat || void 0);
3570
+ void this.bestAvailable(this.baQty, this.baCat || void 0, { preferPremium: this.baPremium });
3401
3571
  });
3402
3572
  this.els.tray.querySelector("[data-ba-cancel]")?.addEventListener("click", () => {
3403
3573
  this.bestAvailableConfirm = false;
@@ -3406,7 +3576,7 @@ var SeatPicker = class _SeatPicker {
3406
3576
  });
3407
3577
  this.els.tray.querySelector("[data-ba-replace]")?.addEventListener("click", () => {
3408
3578
  this.bestAvailableConfirm = false;
3409
- void this.bestAvailable(this.baQty, this.baCat || void 0);
3579
+ void this.bestAvailable(this.baQty, this.baCat || void 0, { preferPremium: this.baPremium });
3410
3580
  });
3411
3581
  this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
3412
3582
  btn.addEventListener("click", () => {
@@ -3788,6 +3958,12 @@ var SeatPicker = class _SeatPicker {
3788
3958
  * the prefix is exact (won't touch "1040-A" under section "104"); otherwise
3789
3959
  * the label is shown verbatim.
3790
3960
  */
3961
+ /** Buyer-facing type word for the row/table key label — the designer's
3962
+ * per-object "Displayed type" override, or the default "Row". */
3963
+ rowTypeWord(details) {
3964
+ const t3 = details?.rowType?.trim();
3965
+ return t3 || "Row";
3966
+ }
3791
3967
  rowShort(details) {
3792
3968
  const row = details?.rowLabel;
3793
3969
  const sec = details?.sectionLabel;
@@ -3807,10 +3983,12 @@ var SeatPicker = class _SeatPicker {
3807
3983
  const esc2 = (v) => String(v ?? "\u2014").replace(/[&<>"]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch]);
3808
3984
  const price = this.money(this.paidPrice(details.categoryKey, details.tierId ?? null, details.price));
3809
3985
  const hasLoc = details.sectionLabel || details.rowLabel || details.seatNumber;
3810
- 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>`;
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>`;
3811
3987
  const statusLine = details.status === "free" ? "" : `<div class="sl-tip-status">${details.status === "held" ? t2("map.statusHeld") : t2("map.statusTaken")}</div>`;
3988
+ const limited = this.limitedViewLabel(details.commercial);
3989
+ const cxLine = limited ? `<div class="sl-tip-cx"><span class="g" aria-hidden="true">\u25D0</span>${esc2(limited)}</div>` : "";
3812
3990
  this.tipEl.style.setProperty("--sl-cat", details.categoryColor);
3813
- 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;
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;
3814
3992
  this.tipEl.style.display = "block";
3815
3993
  this.placeTooltip();
3816
3994
  }
@@ -3830,7 +4008,7 @@ var SeatPicker = class _SeatPicker {
3830
4008
  async removeHeldTicket(label) {
3831
4009
  return this.removeHeldLabel(label);
3832
4010
  }
3833
- async bestAvailable(qty, categoryKey) {
4011
+ async bestAvailable(qty, categoryKey, opts = {}) {
3834
4012
  if (this.salesClosed || this.bestAvailableBusy) return null;
3835
4013
  qty = Math.max(1, Math.min(this.maxTickets, Math.floor(qty)));
3836
4014
  if (this.confirmSeat) this.cancelConfirm();
@@ -3842,7 +4020,7 @@ var SeatPicker = class _SeatPicker {
3842
4020
  button.innerHTML = '<span class="sl-ba-spin" aria-hidden="true"></span>Finding\u2026';
3843
4021
  }
3844
4022
  try {
3845
- const h = await this.controller.bestAvailable(qty, categoryKey);
4023
+ const h = await this.controller.bestAvailable(qty, categoryKey, opts);
3846
4024
  if (h) {
3847
4025
  this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
3848
4026
  this.handedOff = false;
@@ -3852,6 +4030,9 @@ var SeatPicker = class _SeatPicker {
3852
4030
  this.flashHeldSeats(this.hold);
3853
4031
  this.syncTray();
3854
4032
  this.emitHoldChange();
4033
+ if (opts.preferPremium && h.seats.length && !h.seats.every((s) => s.commercial?.premium)) {
4034
+ this.toast(t2("picker.premiumFallbackNote", { count: qty }), "neutral");
4035
+ }
3855
4036
  return this.hold;
3856
4037
  }
3857
4038
  return null;