@seatlayer/js 0.23.0 → 0.25.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
@@ -172,8 +172,28 @@ var SeatingChart = class {
172
172
  ribbon.style.cssText = "position:absolute;top:18px;right:-34px;z-index:6;transform:rotate(45deg);width:140px;text-align:center;padding:4px 0;background:#f4b740;color:#1a1200;font:800 10.5px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;letter-spacing:.12em;box-shadow:0 2px 8px rgba(0,0,0,.25);pointer-events:none;";
173
173
  host.appendChild(ribbon);
174
174
  }
175
+ this.buildBadge(host);
175
176
  return this;
176
177
  }
178
+ /**
179
+ * Attribution badge pinned to the embed's bottom-right, linking to
180
+ * seatlayer.io. Rendered as an absolutely-positioned overlay with
181
+ * self-contained inline styles — the SDK embed ships no widget CSS, and an
182
+ * overlay keeps it out of the layout flow so it never disturbs the SDK v0.22
183
+ * fill-height resize contract. Mirrors the full widget's mark + wordmark and
184
+ * reuses the `picker.poweredBy` i18n string.
185
+ */
186
+ buildBadge(host) {
187
+ if (this.controller.doc?.theme?.hideBadge) return;
188
+ const badge = document.createElement("a");
189
+ badge.href = "https://seatlayer.io";
190
+ badge.target = "_blank";
191
+ badge.rel = "noopener noreferrer";
192
+ badge.setAttribute("aria-label", t("picker.poweredBy"));
193
+ badge.style.cssText = 'position:absolute;bottom:10px;right:12px;z-index:5;display:inline-flex;align-items:center;gap:6px;padding:5px 9px;border-radius:999px;background:rgba(255,255,255,.92);color:#4a5163;text-decoration:none;font:600 11px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;letter-spacing:.02em;box-shadow:0 2px 8px rgba(0,0,0,.12);';
194
+ badge.innerHTML = `<span aria-hidden="true" style="width:16px;height:16px;border-radius:4px;flex:none;display:flex;align-items:center;justify-content:center;background:#f4b740;color:#1a1200"><svg viewBox="0 0 24 24" style="width:11px;height:11px;fill:currentColor"><path d="M4 15c0-1.1.9-2 2-2h12a2 2 0 0 1 2 2v3h-3v-2H7v2H4v-3Z"/><rect x="7" y="7" width="10" height="5" rx="1.6"/></svg></span><span>${t("picker.poweredBy")}</span>`;
195
+ host.appendChild(badge);
196
+ }
177
197
  placeTooltip() {
178
198
  if (!this.tipEl || !this.hostEl) return;
179
199
  const hw = this.hostEl.clientWidth;
@@ -328,6 +348,13 @@ var TYPES = /* @__PURE__ */ new Set([
328
348
  ]);
329
349
  var DEFAULT_LOADING_TIMEOUT_MS = 2e4;
330
350
  var DEFAULT_MIN_FILL_HEIGHT = 480;
351
+ var RENEW_LEAD_MS = 3 * 60 * 1e3;
352
+ var RENEW_SHORT_TTL_MS = 15 * 60 * 1e3;
353
+ var RENEW_SHORT_TTL_FRACTION = 0.8;
354
+ var RENEW_MIN_DELAY_MS = 30 * 1e3;
355
+ var FILL_PROBE_HEIGHT_PX = 1e5;
356
+ var FILL_PROBE_TRACK_EPSILON_PX = 4;
357
+ var FILL_MIN_DEFINITE_HEIGHT_PX = 50;
331
358
  function resolveContainer2(container) {
332
359
  if (typeof container !== "string") return container;
333
360
  const element = document.querySelector(container);
@@ -365,6 +392,14 @@ var EmbeddedDesigner = class {
365
392
  this.designerOrigin = "";
366
393
  this.overlay = null;
367
394
  this.timeoutTimer = null;
395
+ /** Proactive session-renewal timer; armed from each `ready`, cleared on re-mount. */
396
+ this.renewTimer = null;
397
+ /**
398
+ * One automatic recovery relaunch is allowed per expiry. Reset ONLY when a fresh
399
+ * `ready` arrives — deliberately not on re-mount — so a session that keeps failing
400
+ * to load can't loop the host through endless silent relaunches.
401
+ */
402
+ this.autoRecoverUsed = false;
368
403
  this.phase = "loading";
369
404
  this.restoreContainerPosition = null;
370
405
  // Host-side fullscreen pin: saved state we restore on `off`/Escape/destroy.
@@ -375,10 +410,17 @@ var EmbeddedDesigner = class {
375
410
  this.fsKeyHandler = null;
376
411
  /** Latest height (px string) the Designer reported; re-applied after unpin. */
377
412
  this.lastAutoHeight = "";
378
- // Viewport-fill sizing: pending rAF handle + whether listeners are attached.
413
+ // Fill sizing: pending rAF handles + whether window listeners are attached.
379
414
  this.fillRaf = null;
415
+ this.reprobeRaf = null;
380
416
  this.fillListening = false;
381
- /** rAF-throttled fill recompute, so a burst of scroll/resize ticks coalesces. */
417
+ /** Resolved container element (fill measurement + ResizeObserver target). */
418
+ this.containerEl = null;
419
+ /** Cached fill verdict: 'container' = bounded block, 'viewport' = full page. */
420
+ this.fillMode = null;
421
+ /** Live block-size tracking in container-fill mode; disconnected on destroy. */
422
+ this.resizeObs = null;
423
+ /** rAF-throttled fill recompute, so a burst of scroll/RO ticks coalesces. */
382
424
  this.scheduleFill = () => {
383
425
  if (this.fillRaf !== null) return;
384
426
  this.fillRaf = requestAnimationFrame(() => {
@@ -386,6 +428,21 @@ var EmbeddedDesigner = class {
386
428
  this.applyFill();
387
429
  });
388
430
  };
431
+ /**
432
+ * rAF-throttled re-probe: a host layout change (responsive breakpoint, a block
433
+ * gaining/losing a definite height) can flip the verdict, so `resize` /
434
+ * `orientationchange` re-detect and swap the container observer accordingly.
435
+ */
436
+ this.scheduleReprobe = () => {
437
+ if (this.reprobeRaf !== null) return;
438
+ this.reprobeRaf = requestAnimationFrame(() => {
439
+ this.reprobeRaf = null;
440
+ if (this.pinned) return;
441
+ this.fillMode = this.detectFillMode();
442
+ this.syncContainerObserver();
443
+ this.applyFill();
444
+ });
445
+ };
389
446
  this.handleMessage = (event) => {
390
447
  if (!this.frame || event.origin !== this.designerOrigin || event.source !== this.frame.contentWindow) return;
391
448
  if (!event.data || typeof event.data !== "object") return;
@@ -393,7 +450,7 @@ var EmbeddedDesigner = class {
393
450
  if (data.type === "seatlayer.designer.resize") {
394
451
  if (!this.fillEnabled() && this.autoResizeEnabled() && typeof data.px === "number" && Number.isFinite(data.px) && data.px > 0) {
395
452
  this.lastAutoHeight = `${Math.round(data.px)}px`;
396
- if (!this.pinned) this.frame.style.height = this.lastAutoHeight;
453
+ if (!this.pinned) this.setFrameHeight(this.lastAutoHeight);
397
454
  }
398
455
  return;
399
456
  }
@@ -421,6 +478,8 @@ var EmbeddedDesigner = class {
421
478
  this.phase = "ready";
422
479
  this.clearTimeoutTimer();
423
480
  this.removeOverlay();
481
+ this.autoRecoverUsed = false;
482
+ this.scheduleRenewal(message.expiresAt);
424
483
  this.options.onReady?.(message);
425
484
  break;
426
485
  case "seatlayer.designer.saved":
@@ -432,10 +491,18 @@ var EmbeddedDesigner = class {
432
491
  case "seatlayer.designer.close":
433
492
  this.options.onClose?.(message);
434
493
  break;
435
- case "seatlayer.designer.error":
436
- this.showError(causeFromCode(message.code));
494
+ case "seatlayer.designer.error": {
495
+ const cause = causeFromCode(message.code);
496
+ if (cause === "expired" && this.autoRenewEnabled() && !this.autoRecoverUsed) {
497
+ this.autoRecoverUsed = true;
498
+ this.clearRenewTimer();
499
+ this.options.onRequestRelaunch();
500
+ return;
501
+ }
502
+ this.showError(cause);
437
503
  this.options.onError?.(message);
438
504
  break;
505
+ }
439
506
  }
440
507
  };
441
508
  this.options = options;
@@ -452,12 +519,17 @@ var EmbeddedDesigner = class {
452
519
  frame.allow = this.options.allow ?? "fullscreen; clipboard-write";
453
520
  frame.referrerPolicy = this.options.referrerPolicy ?? "origin";
454
521
  frame.src = url.toString();
455
- frame.style.width = "100%";
456
- frame.style.height = typeof this.options.height === "number" ? `${this.options.height}px` : "100%";
522
+ frame.style.setProperty("width", "100%", "important");
523
+ frame.style.setProperty(
524
+ "height",
525
+ typeof this.options.height === "number" ? `${this.options.height}px` : "100%",
526
+ "important"
527
+ );
457
528
  frame.style.border = "0";
458
529
  Object.assign(frame.style, this.options.style);
459
530
  if (this.options.className) frame.className = this.options.className;
460
531
  const container = resolveContainer2(this.options.container);
532
+ this.containerEl = container;
461
533
  window.addEventListener("message", this.handleMessage);
462
534
  container.append(frame);
463
535
  this.frame = frame;
@@ -488,10 +560,13 @@ var EmbeddedDesigner = class {
488
560
  this.stopFill();
489
561
  this.unpinFullscreen();
490
562
  this.clearTimeoutTimer();
563
+ this.clearRenewTimer();
491
564
  this.removeOverlay();
492
565
  this.restoreContainerStyle();
493
566
  this.frame?.remove();
494
567
  this.frame = null;
568
+ this.containerEl = null;
569
+ this.fillMode = null;
495
570
  this.designerOrigin = "";
496
571
  this.phase = "loading";
497
572
  this.lastAutoHeight = "";
@@ -506,24 +581,77 @@ var EmbeddedDesigner = class {
506
581
  fillEnabled() {
507
582
  return typeof this.options.height !== "number";
508
583
  }
584
+ /** Write an SDK-managed height with `!important` so a host theme can't win. */
585
+ setFrameHeight(value) {
586
+ this.frame?.style.setProperty("height", value, "important");
587
+ }
509
588
  /**
510
- * Size the iframe so its bottom edge meets the bottom of the viewport
511
- * (`window.innerHeight - top`), clamped to `minHeight`. No-op while pinned
512
- * fullscreen (the pin fills the viewport itself).
589
+ * Decide whether the host gave the container a DEFINITE (bounded) height a
590
+ * fixed block the embed should fill 100% of — versus a content-sized container
591
+ * that collapses to whatever the iframe measures (full-page usage).
592
+ *
593
+ * We drive the iframe to two extreme heights within a single synchronous task
594
+ * and watch whether the container follows: a bounded box barely moves, a
595
+ * content-sized one grows with the iframe. Because we restore the height before
596
+ * yielding, the browser only lays out — it never paints the extremes, so there
597
+ * is no visible flash. Works for px, resolved `%`, and flex (`flex:1;min-h:0`)
598
+ * heights, and leaves a mere `min-height` floor classified as content-sized so
599
+ * full-page hosts keep the old viewport-fill behavior.
600
+ */
601
+ detectFillMode() {
602
+ const container = this.containerEl;
603
+ const frame = this.frame;
604
+ if (this.pinned || !container || !frame) return this.fillMode ?? "viewport";
605
+ const measure = () => container.getBoundingClientRect().height;
606
+ const savedValue = frame.style.getPropertyValue("height");
607
+ const savedPriority = frame.style.getPropertyPriority("height");
608
+ frame.style.setProperty("height", "0px", "important");
609
+ const collapsed = measure();
610
+ frame.style.setProperty("height", `${FILL_PROBE_HEIGHT_PX}px`, "important");
611
+ const expanded = measure();
612
+ if (savedValue) frame.style.setProperty("height", savedValue, savedPriority);
613
+ else frame.style.removeProperty("height");
614
+ const tracksIframe = expanded - collapsed > FILL_PROBE_TRACK_EPSILON_PX;
615
+ const bounded = !tracksIframe && collapsed >= FILL_MIN_DEFINITE_HEIGHT_PX;
616
+ return bounded ? "container" : "viewport";
617
+ }
618
+ /**
619
+ * Size the iframe for the current fill verdict, clamped to `minHeight`. In
620
+ * container mode it fills 100% of the bounded block; in viewport mode its
621
+ * bottom edge meets the bottom of the viewport (`window.innerHeight - top`).
622
+ * No-op while pinned fullscreen (the pin fills the viewport itself).
513
623
  */
514
624
  applyFill() {
515
625
  if (!this.frame || this.pinned) return;
516
626
  const min = this.options.minHeight ?? DEFAULT_MIN_FILL_HEIGHT;
627
+ if (this.fillMode === "container" && this.containerEl) {
628
+ const target2 = Math.max(min, Math.round(this.containerEl.getBoundingClientRect().height));
629
+ this.setFrameHeight(`${target2}px`);
630
+ return;
631
+ }
517
632
  const top = this.frame.getBoundingClientRect().top;
518
633
  const target = Math.max(min, Math.round(window.innerHeight - top));
519
- this.frame.style.height = `${target}px`;
634
+ this.setFrameHeight(`${target}px`);
635
+ }
636
+ /** Attach/detach the container ResizeObserver to match the current verdict. */
637
+ syncContainerObserver() {
638
+ const want = this.fillMode === "container" && !!this.containerEl && typeof ResizeObserver !== "undefined";
639
+ if (want && !this.resizeObs) {
640
+ this.resizeObs = new ResizeObserver(() => this.scheduleFill());
641
+ this.resizeObs.observe(this.containerEl);
642
+ } else if (!want && this.resizeObs) {
643
+ this.resizeObs.disconnect();
644
+ this.resizeObs = null;
645
+ }
520
646
  }
521
647
  startFill() {
648
+ this.fillMode = this.detectFillMode();
649
+ this.syncContainerObserver();
522
650
  this.applyFill();
523
651
  if (this.fillListening) return;
524
652
  this.fillListening = true;
525
- window.addEventListener("resize", this.scheduleFill);
526
- window.addEventListener("orientationchange", this.scheduleFill);
653
+ window.addEventListener("resize", this.scheduleReprobe);
654
+ window.addEventListener("orientationchange", this.scheduleReprobe);
527
655
  window.addEventListener("scroll", this.scheduleFill, { passive: true });
528
656
  }
529
657
  stopFill() {
@@ -531,10 +659,18 @@ var EmbeddedDesigner = class {
531
659
  cancelAnimationFrame(this.fillRaf);
532
660
  this.fillRaf = null;
533
661
  }
662
+ if (this.reprobeRaf !== null) {
663
+ cancelAnimationFrame(this.reprobeRaf);
664
+ this.reprobeRaf = null;
665
+ }
666
+ if (this.resizeObs) {
667
+ this.resizeObs.disconnect();
668
+ this.resizeObs = null;
669
+ }
534
670
  if (!this.fillListening) return;
535
671
  this.fillListening = false;
536
- window.removeEventListener("resize", this.scheduleFill);
537
- window.removeEventListener("orientationchange", this.scheduleFill);
672
+ window.removeEventListener("resize", this.scheduleReprobe);
673
+ window.removeEventListener("orientationchange", this.scheduleReprobe);
538
674
  window.removeEventListener("scroll", this.scheduleFill);
539
675
  }
540
676
  /**
@@ -546,16 +682,22 @@ var EmbeddedDesigner = class {
546
682
  if (this.pinned || !this.frame) return;
547
683
  this.pinned = true;
548
684
  this.frameStyleBeforeFs = this.frame.getAttribute("style");
549
- Object.assign(this.frame.style, {
685
+ const pin = {
550
686
  position: "fixed",
551
- inset: "0",
687
+ top: "0",
688
+ right: "0",
689
+ bottom: "0",
690
+ left: "0",
552
691
  width: "100vw",
553
692
  height: "100vh",
554
693
  margin: "0",
555
694
  border: "0",
556
- zIndex: "2147483000",
695
+ "z-index": "2147483000",
557
696
  background: "#101625"
558
- });
697
+ };
698
+ for (const [property, value] of Object.entries(pin)) {
699
+ this.frame.style.setProperty(property, value, "important");
700
+ }
559
701
  const docEl = document.documentElement;
560
702
  this.docOverflowBeforeFs = docEl.style.overflow;
561
703
  docEl.style.overflow = "hidden";
@@ -576,7 +718,7 @@ var EmbeddedDesigner = class {
576
718
  if (this.frameStyleBeforeFs === null) this.frame.removeAttribute("style");
577
719
  else this.frame.setAttribute("style", this.frameStyleBeforeFs);
578
720
  if (this.fillEnabled()) this.applyFill();
579
- else if (this.autoResizeEnabled() && this.lastAutoHeight) this.frame.style.height = this.lastAutoHeight;
721
+ else if (this.autoResizeEnabled() && this.lastAutoHeight) this.setFrameHeight(this.lastAutoHeight);
580
722
  }
581
723
  this.frameStyleBeforeFs = null;
582
724
  if (this.docOverflowBeforeFs !== null) {
@@ -598,6 +740,48 @@ var EmbeddedDesigner = class {
598
740
  this.timeoutTimer = null;
599
741
  }
600
742
  }
743
+ /**
744
+ * Auto-renewal (proactive + one expiry recovery) is on when the host wired a
745
+ * relaunch hook and did not opt out. Without the hook there is nothing to call,
746
+ * so it is a no-op.
747
+ */
748
+ autoRenewEnabled() {
749
+ return !!this.options.onRequestRelaunch && this.options.autoRenewSession !== false;
750
+ }
751
+ clearRenewTimer() {
752
+ if (this.renewTimer !== null) {
753
+ clearTimeout(this.renewTimer);
754
+ this.renewTimer = null;
755
+ }
756
+ }
757
+ /**
758
+ * Arm the proactive renewal timer from a `ready` message's `expiresAt` (epoch
759
+ * ms). We relaunch a comfortable lead before expiry so the host can mint a fresh
760
+ * session and swap `designerUrl` without the user ever seeing the expiry card:
761
+ *
762
+ * - normal TTL (≥ 15 min): renew {@link RENEW_LEAD_MS} (~3 min) before expiry;
763
+ * - short TTL (< 15 min): renew after {@link RENEW_SHORT_TTL_FRACTION} (80%) of
764
+ * the remaining life, so the lead can't overshoot the whole session;
765
+ * - either way, never sooner than {@link RENEW_MIN_DELAY_MS} (30s) after `ready`
766
+ * so a burst of `ready` messages can't spin the host.
767
+ *
768
+ * Re-armed on every `ready`; cleared on destroy / setDesignerUrl (via re-mount).
769
+ * A no-op when auto-renewal is off or `expiresAt` is missing/already past — the
770
+ * expiry-error path recovers a session that has already lapsed.
771
+ */
772
+ scheduleRenewal(expiresAt) {
773
+ this.clearRenewTimer();
774
+ if (!this.autoRenewEnabled()) return;
775
+ if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) return;
776
+ const remaining = expiresAt - Date.now();
777
+ if (remaining <= 0) return;
778
+ const lead = remaining < RENEW_SHORT_TTL_MS ? remaining * RENEW_SHORT_TTL_FRACTION : remaining - RENEW_LEAD_MS;
779
+ const delay = Math.max(RENEW_MIN_DELAY_MS, lead);
780
+ this.renewTimer = setTimeout(() => {
781
+ this.renewTimer = null;
782
+ if (this.autoRenewEnabled()) this.options.onRequestRelaunch();
783
+ }, delay);
784
+ }
601
785
  ensureContainerPositioned(container) {
602
786
  const position = getComputedStyle(container).position;
603
787
  if (position === "static") {
@@ -1254,6 +1438,17 @@ var CSS = `
1254
1438
  .sl-ba-title .spark{color:var(--sl-accent);font-size:16px}
1255
1439
  .sl-ba-copy{grid-column:1/-1;margin:-4px 0 2px 23px;color:var(--sl-muted);font-size:10.5px;line-height:1.35}
1256
1440
  .sl-ba-copy .narrow{display:none}
1441
+ /* "\u2605 Best seats" premium quick-pick \u2014 gold accent echoing the \u2605 Premium pill on
1442
+ the confirm popover; deliberately distinct from the accent-toned qty/go. */
1443
+ .sl-ba-premium{position:relative;z-index:1;grid-column:1/-1;justify-self:start;display:inline-flex;align-items:center;gap:6px;
1444
+ padding:6px 12px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.02em;cursor:pointer;
1445
+ color:#c9a24b;background:color-mix(in srgb,#e8c15a 10%,var(--sl-surface));
1446
+ border:1px solid color-mix(in srgb,#e8c15a 34%,var(--sl-line));transition:filter .15s,background .15s,color .15s}
1447
+ .sl-ba-premium .star{font-size:12px;line-height:1;color:#e8c15a}
1448
+ .sl-ba-premium:hover{filter:brightness(1.05)}
1449
+ .sl-ba-premium.on{color:#1c1608;background:linear-gradient(135deg,#f0cf6b,#e0b23f);border-color:transparent;
1450
+ box-shadow:0 6px 16px color-mix(in srgb,#e8c15a 26%,transparent)}
1451
+ .sl-ba-premium.on .star{color:#5a4410}
1257
1452
  .sl-ba select{background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:8px;
1258
1453
  font:inherit;font-size:11px;padding:7px 8px;min-width:0;width:100%;max-width:none}
1259
1454
  .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)}
@@ -1351,6 +1546,25 @@ var CSS = `
1351
1546
  .sl-confirm-view:hover{border-color:var(--sl-muted)}
1352
1547
  .sl-confirm-view svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
1353
1548
 
1549
+ /* commercial seat flags \u2014 limited-view caution + premium tag. Amber tone,
1550
+ deliberately distinct from the red taken/held state; shown on the confirm
1551
+ card, echoed as a small \u25D0 marker on cart chips and the hover tip. */
1552
+ .sl-cx{display:flex;flex-direction:column;gap:6px;margin-bottom:10px}
1553
+ .sl-cx-warn{display:flex;align-items:flex-start;gap:7px;padding:8px 10px;border-radius:9px;
1554
+ background:color-mix(in srgb,#f4b740 13%,var(--sl-surface));border:1px solid color-mix(in srgb,#f4b740 40%,var(--sl-line));
1555
+ animation:slNoticeIn .28s ease both}
1556
+ .sl-cx-glyph{flex:none;font-size:14px;line-height:1.2;color:#f4b740}
1557
+ .sl-cx-txt{min-width:0;display:flex;flex-direction:column;gap:2px}
1558
+ .sl-cx-txt b{font-size:12px;font-weight:800;color:var(--sl-text)}
1559
+ .sl-cx-note{font-size:11px;line-height:1.4;color:var(--sl-muted)}
1560
+ .sl-cx-premium{display:inline-flex;align-items:center;gap:6px;align-self:flex-start;padding:4px 10px;border-radius:999px;
1561
+ font-size:11px;font-weight:800;letter-spacing:.02em;color:#c9a24b;
1562
+ background:color-mix(in srgb,#e8c15a 13%,var(--sl-surface));border:1px solid color-mix(in srgb,#e8c15a 38%,var(--sl-line))}
1563
+ .sl-cx-star{font-size:12px;line-height:1;color:#e8c15a}
1564
+ .sl-cx-mark{flex:none;font-size:12px;line-height:1;color:#f4b740;cursor:help}
1565
+ .sl-tip-cx{display:flex;align-items:center;gap:6px;padding:5px 10px 7px;font-size:10.5px;font-weight:700;color:#e8b24a}
1566
+ .sl-tip-cx .g{font-size:12px}
1567
+
1354
1568
  /* 360\xB0 seat-view modal (fills the widget; drag-to-look-around equirectangular) */
1355
1569
  .sl-view{position:absolute;inset:0;z-index:12;display:flex;flex-direction:column;background:var(--sl-bg)}
1356
1570
  .sl-view-head{display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid var(--sl-line);flex:none}
@@ -1479,6 +1693,8 @@ var SeatPicker = class _SeatPicker {
1479
1693
  this.srEl = null;
1480
1694
  this.baQty = 2;
1481
1695
  this.baCat = "";
1696
+ /** "★ Best seats" premium quick-pick toggle — biases best-available to premium seats. */
1697
+ this.baPremium = false;
1482
1698
  this.bestAvailableConfirm = false;
1483
1699
  this.releasingHold = false;
1484
1700
  /** Event sales window is closed (read-only load state / live close). */
@@ -1635,6 +1851,50 @@ var SeatPicker = class _SeatPicker {
1635
1851
  const sight = distance != null ? t2("picker.sightline", { m: distance }) : this.tf("picker.sightlineClear", "Clear sightline");
1636
1852
  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>`;
1637
1853
  }
1854
+ /** Minimal HTML/attribute escaper for buyer-authored commercial text (notes). */
1855
+ escCx(value) {
1856
+ return String(value ?? "").replace(/[&<>"]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch]);
1857
+ }
1858
+ /** Localized "Restricted view" / "Obstructed view" label for a seat's flags,
1859
+ * or '' when neither is set. Restricted takes precedence when both are on. */
1860
+ limitedViewLabel(c) {
1861
+ if (c?.restrictedView) return this.tf("picker.restrictedView", "Restricted view");
1862
+ if (c?.obstructedView) return this.tf("picker.obstructedView", "Obstructed view");
1863
+ return "";
1864
+ }
1865
+ /**
1866
+ * Commercial flags block for the confirm/detail surface: a subtle ★ Premium
1867
+ * tag plus an amber ◐ limited-view caution (with the organizer's note when
1868
+ * present). '' when the seat carries no surfaced commercial flag.
1869
+ */
1870
+ commercialConfirmHtml(c) {
1871
+ if (!c) return "";
1872
+ const rows = [];
1873
+ if (c.premium) {
1874
+ rows.push(
1875
+ `<div class="sl-cx-premium"><span class="sl-cx-star" aria-hidden="true">\u2605</span>${this.tf("picker.premiumSeat", "Premium seat")}</div>`
1876
+ );
1877
+ }
1878
+ const limited = this.limitedViewLabel(c);
1879
+ if (limited) {
1880
+ rows.push(
1881
+ `<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>`
1882
+ );
1883
+ } else if (c.note) {
1884
+ rows.push(
1885
+ `<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>`
1886
+ );
1887
+ }
1888
+ return rows.length ? `<div class="sl-cx">${rows.join("")}</div>` : "";
1889
+ }
1890
+ /** Small ◐ limited-view marker for a cart chip; title/aria uses the seat's
1891
+ * note when present, else the generic view label. '' for a clear-view seat. */
1892
+ commercialChipMarker(c) {
1893
+ const limited = this.limitedViewLabel(c);
1894
+ if (!limited) return "";
1895
+ const title = this.escCx(c?.note ? c.note : limited);
1896
+ return `<span class="sl-cx-mark" role="img" aria-label="${title}" title="${title}">\u25D0</span>`;
1897
+ }
1638
1898
  /** True when the picker is rendered inside an iframe (snippet embed at /e/:key). */
1639
1899
  isFramed() {
1640
1900
  return typeof window !== "undefined" && window.parent !== window;
@@ -2007,45 +2267,71 @@ var SeatPicker = class _SeatPicker {
2007
2267
  this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
2008
2268
  this.buildBadge(chartTheme);
2009
2269
  const present = /* @__PURE__ */ new Set();
2270
+ let hasLimitedView = false;
2010
2271
  if (this.controller.doc) {
2011
2272
  for (const seat of expandChart(this.controller.doc)) {
2012
2273
  for (const type of seat.accessibility ?? []) present.add(type);
2013
2274
  if (seat.accessible && !seat.accessibility?.length) present.add("wheelchair");
2275
+ if (seat.commercial?.restrictedView || seat.commercial?.obstructedView) hasLimitedView = true;
2014
2276
  }
2015
2277
  }
2016
- if (present.size) {
2278
+ const focusSeatsForFilter = () => {
2279
+ if (this.rungsEl && this.controller.getRung() !== "seats") {
2280
+ this.controller.setRung("seats");
2281
+ this.collapseSectionCard();
2282
+ this.syncRung();
2283
+ }
2284
+ };
2285
+ if (present.size || hasLimitedView) {
2017
2286
  const chips = document.createElement("div");
2018
2287
  chips.className = "sl-chips";
2019
- const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
2020
- const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-f="${key}">${label}</button>`;
2021
- chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
2022
2288
  this.regions["top-left"].appendChild(chips);
2023
2289
  this.a11yChipsEl = chips;
2024
- const active = /* @__PURE__ */ new Set();
2025
- const syncChips = () => {
2026
- chips.querySelectorAll("button").forEach((b) => {
2027
- const f = b.dataset.f;
2028
- const on = f === "all" ? active.size === 0 : active.has(f);
2029
- b.classList.toggle("on", on);
2030
- b.setAttribute("aria-pressed", String(on));
2290
+ if (present.size) {
2291
+ const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
2292
+ const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-a11y="1" data-f="${key}">${label}</button>`;
2293
+ chips.insertAdjacentHTML(
2294
+ "beforeend",
2295
+ mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("")
2296
+ );
2297
+ const active = /* @__PURE__ */ new Set();
2298
+ const syncChips = () => {
2299
+ chips.querySelectorAll("button[data-a11y]").forEach((b) => {
2300
+ const f = b.dataset.f;
2301
+ const on = f === "all" ? active.size === 0 : active.has(f);
2302
+ b.classList.toggle("on", on);
2303
+ b.setAttribute("aria-pressed", String(on));
2304
+ });
2305
+ const filter = active.size ? [...active] : null;
2306
+ this.controller.setAccessibilityFilter(filter);
2307
+ if (filter) focusSeatsForFilter();
2308
+ };
2309
+ chips.querySelectorAll("button[data-a11y]").forEach((btn) => {
2310
+ btn.addEventListener("click", () => {
2311
+ const f = btn.dataset.f;
2312
+ if (f === "all") active.clear();
2313
+ else if (active.has(f)) active.delete(f);
2314
+ else active.add(f);
2315
+ syncChips();
2316
+ });
2031
2317
  });
2032
- const filter = active.size ? [...active] : null;
2033
- this.controller.setAccessibilityFilter(filter);
2034
- if (filter && this.rungsEl && this.controller.getRung() !== "seats") {
2035
- this.controller.setRung("seats");
2036
- this.collapseSectionCard();
2037
- this.syncRung();
2038
- }
2039
- };
2040
- chips.querySelectorAll("button").forEach((btn) => {
2041
- btn.addEventListener("click", () => {
2042
- const f = btn.dataset.f;
2043
- if (f === "all") active.clear();
2044
- else if (active.has(f)) active.delete(f);
2045
- else active.add(f);
2046
- syncChips();
2318
+ }
2319
+ if (hasLimitedView) {
2320
+ const limited = document.createElement("button");
2321
+ limited.type = "button";
2322
+ limited.className = "sl-chip-f";
2323
+ limited.setAttribute("aria-pressed", "false");
2324
+ limited.innerHTML = `\u25D0 ${this.tf("picker.hideLimitedView", "Hide limited-view seats")}`;
2325
+ chips.appendChild(limited);
2326
+ let limitedOn = false;
2327
+ limited.addEventListener("click", () => {
2328
+ limitedOn = !limitedOn;
2329
+ limited.classList.toggle("on", limitedOn);
2330
+ limited.setAttribute("aria-pressed", String(limitedOn));
2331
+ this.controller.setCommercialLimitedFilter(limitedOn);
2332
+ if (limitedOn) focusSeatsForFilter();
2047
2333
  });
2048
- });
2334
+ }
2049
2335
  }
2050
2336
  const cb = document.createElement("button");
2051
2337
  cb.type = "button";
@@ -2768,7 +3054,7 @@ var SeatPicker = class _SeatPicker {
2768
3054
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
2769
3055
  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>`;
2770
3056
  }).join("");
2771
- 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>`;
3057
+ 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>`;
2772
3058
  card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
2773
3059
  card.querySelector(".sl-seccard-overview").addEventListener("click", () => this.controller.overview());
2774
3060
  (this.regions["top-center"] ?? this.els.map).appendChild(card);
@@ -2862,7 +3148,7 @@ var SeatPicker = class _SeatPicker {
2862
3148
  el.setAttribute("aria-modal", "true");
2863
3149
  el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
2864
3150
  el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
2865
- 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>`;
3151
+ 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>`;
2866
3152
  this.els.map.appendChild(el);
2867
3153
  this.confirmEl = el;
2868
3154
  this.reanchorConfirm();
@@ -3157,14 +3443,16 @@ var SeatPicker = class _SeatPicker {
3157
3443
  const noPicks = !seats.length && !heldItems.length && !this.pendingGACount();
3158
3444
  if (!this.hold && (noPicks || this.bestAvailableBusy || this.bestAvailableConfirm)) {
3159
3445
  const cats = this.controller.doc?.categories ?? [];
3160
- 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>`);
3446
+ 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
3447
+ // seats (same present-only philosophy as the a11y filter chips).
3448
+ (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>`);
3161
3449
  }
3162
3450
  const idGrid = (seatId, label) => {
3163
3451
  const d = seatId ? this.controller.seatDetails(seatId) : null;
3164
3452
  if (!d?.sectionLabel && !d?.rowLabel && !d?.seatNumber) {
3165
3453
  return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">Seat</span><span class="val">${label}</span></span></div>`;
3166
3454
  }
3167
- 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>`;
3455
+ 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>`;
3168
3456
  };
3169
3457
  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>`;
3170
3458
  for (const item of heldItems) {
@@ -3175,7 +3463,7 @@ var SeatPicker = class _SeatPicker {
3175
3463
  const heldSeat = item.objectType !== "ga" ? this.controller.seatByLabel(item.label) : null;
3176
3464
  const canView2 = this.seatViewEnabled() && !!heldSeat;
3177
3465
  parts.push(
3178
- `<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>`
3466
+ `<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>`
3179
3467
  );
3180
3468
  }
3181
3469
  const heldLabels = new Set(heldItems.map((item) => item.label));
@@ -3186,7 +3474,7 @@ var SeatPicker = class _SeatPicker {
3186
3474
  const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
3187
3475
  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>` : "";
3188
3476
  parts.push(
3189
- `<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>`
3477
+ `<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>`
3190
3478
  );
3191
3479
  }
3192
3480
  for (const area of gaAreas) {
@@ -3206,6 +3494,10 @@ var SeatPicker = class _SeatPicker {
3206
3494
  this.els.tray.querySelector("[data-ba-cat]")?.addEventListener("change", (e) => {
3207
3495
  this.baCat = e.target.value;
3208
3496
  });
3497
+ this.els.tray.querySelector("[data-ba-premium]")?.addEventListener("click", () => {
3498
+ this.baPremium = !this.baPremium;
3499
+ this.syncTray();
3500
+ });
3209
3501
  this.els.tray.querySelector(".sl-ba-go")?.addEventListener("click", () => {
3210
3502
  if (this.pendingSelectionCount() > 0) {
3211
3503
  this.bestAvailableConfirm = true;
@@ -3213,7 +3505,7 @@ var SeatPicker = class _SeatPicker {
3213
3505
  this.els.tray.querySelector("[data-ba-replace]")?.focus();
3214
3506
  return;
3215
3507
  }
3216
- void this.bestAvailable(this.baQty, this.baCat || void 0);
3508
+ void this.bestAvailable(this.baQty, this.baCat || void 0, { preferPremium: this.baPremium });
3217
3509
  });
3218
3510
  this.els.tray.querySelector("[data-ba-cancel]")?.addEventListener("click", () => {
3219
3511
  this.bestAvailableConfirm = false;
@@ -3222,7 +3514,7 @@ var SeatPicker = class _SeatPicker {
3222
3514
  });
3223
3515
  this.els.tray.querySelector("[data-ba-replace]")?.addEventListener("click", () => {
3224
3516
  this.bestAvailableConfirm = false;
3225
- void this.bestAvailable(this.baQty, this.baCat || void 0);
3517
+ void this.bestAvailable(this.baQty, this.baCat || void 0, { preferPremium: this.baPremium });
3226
3518
  });
3227
3519
  this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
3228
3520
  btn.addEventListener("click", () => {
@@ -3604,6 +3896,12 @@ var SeatPicker = class _SeatPicker {
3604
3896
  * the prefix is exact (won't touch "1040-A" under section "104"); otherwise
3605
3897
  * the label is shown verbatim.
3606
3898
  */
3899
+ /** Buyer-facing type word for the row/table key label — the designer's
3900
+ * per-object "Displayed type" override, or the default "Row". */
3901
+ rowTypeWord(details) {
3902
+ const t3 = details?.rowType?.trim();
3903
+ return t3 || "Row";
3904
+ }
3607
3905
  rowShort(details) {
3608
3906
  const row = details?.rowLabel;
3609
3907
  const sec = details?.sectionLabel;
@@ -3623,10 +3921,12 @@ var SeatPicker = class _SeatPicker {
3623
3921
  const esc2 = (v) => String(v ?? "\u2014").replace(/[&<>"]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch]);
3624
3922
  const price = this.money(this.paidPrice(details.categoryKey, details.tierId ?? null, details.price));
3625
3923
  const hasLoc = details.sectionLabel || details.rowLabel || details.seatNumber;
3626
- 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>`;
3924
+ 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>`;
3627
3925
  const statusLine = details.status === "free" ? "" : `<div class="sl-tip-status">${details.status === "held" ? t2("map.statusHeld") : t2("map.statusTaken")}</div>`;
3926
+ const limited = this.limitedViewLabel(details.commercial);
3927
+ const cxLine = limited ? `<div class="sl-tip-cx"><span class="g" aria-hidden="true">\u25D0</span>${esc2(limited)}</div>` : "";
3628
3928
  this.tipEl.style.setProperty("--sl-cat", details.categoryColor);
3629
- 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;
3929
+ 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;
3630
3930
  this.tipEl.style.display = "block";
3631
3931
  this.placeTooltip();
3632
3932
  }
@@ -3646,7 +3946,7 @@ var SeatPicker = class _SeatPicker {
3646
3946
  async removeHeldTicket(label) {
3647
3947
  return this.removeHeldLabel(label);
3648
3948
  }
3649
- async bestAvailable(qty, categoryKey) {
3949
+ async bestAvailable(qty, categoryKey, opts = {}) {
3650
3950
  if (this.salesClosed || this.bestAvailableBusy) return null;
3651
3951
  qty = Math.max(1, Math.min(this.maxTickets, Math.floor(qty)));
3652
3952
  if (this.confirmSeat) this.cancelConfirm();
@@ -3658,7 +3958,7 @@ var SeatPicker = class _SeatPicker {
3658
3958
  button.innerHTML = '<span class="sl-ba-spin" aria-hidden="true"></span>Finding\u2026';
3659
3959
  }
3660
3960
  try {
3661
- const h = await this.controller.bestAvailable(qty, categoryKey);
3961
+ const h = await this.controller.bestAvailable(qty, categoryKey, opts);
3662
3962
  if (h) {
3663
3963
  this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
3664
3964
  this.handedOff = false;
@@ -3668,6 +3968,9 @@ var SeatPicker = class _SeatPicker {
3668
3968
  this.flashHeldSeats(this.hold);
3669
3969
  this.syncTray();
3670
3970
  this.emitHoldChange();
3971
+ if (opts.preferPremium && h.seats.length && !h.seats.every((s) => s.commercial?.premium)) {
3972
+ this.toast(t2("picker.premiumFallbackNote", { count: qty }), "neutral");
3973
+ }
3671
3974
  return this.hold;
3672
3975
  }
3673
3976
  return null;