@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.cjs CHANGED
@@ -205,8 +205,28 @@ var SeatingChart = class {
205
205
  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;";
206
206
  host.appendChild(ribbon);
207
207
  }
208
+ this.buildBadge(host);
208
209
  return this;
209
210
  }
211
+ /**
212
+ * Attribution badge pinned to the embed's bottom-right, linking to
213
+ * seatlayer.io. Rendered as an absolutely-positioned overlay with
214
+ * self-contained inline styles — the SDK embed ships no widget CSS, and an
215
+ * overlay keeps it out of the layout flow so it never disturbs the SDK v0.22
216
+ * fill-height resize contract. Mirrors the full widget's mark + wordmark and
217
+ * reuses the `picker.poweredBy` i18n string.
218
+ */
219
+ buildBadge(host) {
220
+ if (this.controller.doc?.theme?.hideBadge) return;
221
+ const badge = document.createElement("a");
222
+ badge.href = "https://seatlayer.io";
223
+ badge.target = "_blank";
224
+ badge.rel = "noopener noreferrer";
225
+ badge.setAttribute("aria-label", (0, import_core.t)("picker.poweredBy"));
226
+ 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);';
227
+ 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>${(0, import_core.t)("picker.poweredBy")}</span>`;
228
+ host.appendChild(badge);
229
+ }
210
230
  placeTooltip() {
211
231
  if (!this.tipEl || !this.hostEl) return;
212
232
  const hw = this.hostEl.clientWidth;
@@ -361,6 +381,13 @@ var TYPES = /* @__PURE__ */ new Set([
361
381
  ]);
362
382
  var DEFAULT_LOADING_TIMEOUT_MS = 2e4;
363
383
  var DEFAULT_MIN_FILL_HEIGHT = 480;
384
+ var RENEW_LEAD_MS = 3 * 60 * 1e3;
385
+ var RENEW_SHORT_TTL_MS = 15 * 60 * 1e3;
386
+ var RENEW_SHORT_TTL_FRACTION = 0.8;
387
+ var RENEW_MIN_DELAY_MS = 30 * 1e3;
388
+ var FILL_PROBE_HEIGHT_PX = 1e5;
389
+ var FILL_PROBE_TRACK_EPSILON_PX = 4;
390
+ var FILL_MIN_DEFINITE_HEIGHT_PX = 50;
364
391
  function resolveContainer2(container) {
365
392
  if (typeof container !== "string") return container;
366
393
  const element = document.querySelector(container);
@@ -398,6 +425,14 @@ var EmbeddedDesigner = class {
398
425
  this.designerOrigin = "";
399
426
  this.overlay = null;
400
427
  this.timeoutTimer = null;
428
+ /** Proactive session-renewal timer; armed from each `ready`, cleared on re-mount. */
429
+ this.renewTimer = null;
430
+ /**
431
+ * One automatic recovery relaunch is allowed per expiry. Reset ONLY when a fresh
432
+ * `ready` arrives — deliberately not on re-mount — so a session that keeps failing
433
+ * to load can't loop the host through endless silent relaunches.
434
+ */
435
+ this.autoRecoverUsed = false;
401
436
  this.phase = "loading";
402
437
  this.restoreContainerPosition = null;
403
438
  // Host-side fullscreen pin: saved state we restore on `off`/Escape/destroy.
@@ -408,10 +443,17 @@ var EmbeddedDesigner = class {
408
443
  this.fsKeyHandler = null;
409
444
  /** Latest height (px string) the Designer reported; re-applied after unpin. */
410
445
  this.lastAutoHeight = "";
411
- // Viewport-fill sizing: pending rAF handle + whether listeners are attached.
446
+ // Fill sizing: pending rAF handles + whether window listeners are attached.
412
447
  this.fillRaf = null;
448
+ this.reprobeRaf = null;
413
449
  this.fillListening = false;
414
- /** rAF-throttled fill recompute, so a burst of scroll/resize ticks coalesces. */
450
+ /** Resolved container element (fill measurement + ResizeObserver target). */
451
+ this.containerEl = null;
452
+ /** Cached fill verdict: 'container' = bounded block, 'viewport' = full page. */
453
+ this.fillMode = null;
454
+ /** Live block-size tracking in container-fill mode; disconnected on destroy. */
455
+ this.resizeObs = null;
456
+ /** rAF-throttled fill recompute, so a burst of scroll/RO ticks coalesces. */
415
457
  this.scheduleFill = () => {
416
458
  if (this.fillRaf !== null) return;
417
459
  this.fillRaf = requestAnimationFrame(() => {
@@ -419,6 +461,21 @@ var EmbeddedDesigner = class {
419
461
  this.applyFill();
420
462
  });
421
463
  };
464
+ /**
465
+ * rAF-throttled re-probe: a host layout change (responsive breakpoint, a block
466
+ * gaining/losing a definite height) can flip the verdict, so `resize` /
467
+ * `orientationchange` re-detect and swap the container observer accordingly.
468
+ */
469
+ this.scheduleReprobe = () => {
470
+ if (this.reprobeRaf !== null) return;
471
+ this.reprobeRaf = requestAnimationFrame(() => {
472
+ this.reprobeRaf = null;
473
+ if (this.pinned) return;
474
+ this.fillMode = this.detectFillMode();
475
+ this.syncContainerObserver();
476
+ this.applyFill();
477
+ });
478
+ };
422
479
  this.handleMessage = (event) => {
423
480
  if (!this.frame || event.origin !== this.designerOrigin || event.source !== this.frame.contentWindow) return;
424
481
  if (!event.data || typeof event.data !== "object") return;
@@ -426,7 +483,7 @@ var EmbeddedDesigner = class {
426
483
  if (data.type === "seatlayer.designer.resize") {
427
484
  if (!this.fillEnabled() && this.autoResizeEnabled() && typeof data.px === "number" && Number.isFinite(data.px) && data.px > 0) {
428
485
  this.lastAutoHeight = `${Math.round(data.px)}px`;
429
- if (!this.pinned) this.frame.style.height = this.lastAutoHeight;
486
+ if (!this.pinned) this.setFrameHeight(this.lastAutoHeight);
430
487
  }
431
488
  return;
432
489
  }
@@ -454,6 +511,8 @@ var EmbeddedDesigner = class {
454
511
  this.phase = "ready";
455
512
  this.clearTimeoutTimer();
456
513
  this.removeOverlay();
514
+ this.autoRecoverUsed = false;
515
+ this.scheduleRenewal(message.expiresAt);
457
516
  this.options.onReady?.(message);
458
517
  break;
459
518
  case "seatlayer.designer.saved":
@@ -465,10 +524,18 @@ var EmbeddedDesigner = class {
465
524
  case "seatlayer.designer.close":
466
525
  this.options.onClose?.(message);
467
526
  break;
468
- case "seatlayer.designer.error":
469
- this.showError(causeFromCode(message.code));
527
+ case "seatlayer.designer.error": {
528
+ const cause = causeFromCode(message.code);
529
+ if (cause === "expired" && this.autoRenewEnabled() && !this.autoRecoverUsed) {
530
+ this.autoRecoverUsed = true;
531
+ this.clearRenewTimer();
532
+ this.options.onRequestRelaunch();
533
+ return;
534
+ }
535
+ this.showError(cause);
470
536
  this.options.onError?.(message);
471
537
  break;
538
+ }
472
539
  }
473
540
  };
474
541
  this.options = options;
@@ -485,12 +552,17 @@ var EmbeddedDesigner = class {
485
552
  frame.allow = this.options.allow ?? "fullscreen; clipboard-write";
486
553
  frame.referrerPolicy = this.options.referrerPolicy ?? "origin";
487
554
  frame.src = url.toString();
488
- frame.style.width = "100%";
489
- frame.style.height = typeof this.options.height === "number" ? `${this.options.height}px` : "100%";
555
+ frame.style.setProperty("width", "100%", "important");
556
+ frame.style.setProperty(
557
+ "height",
558
+ typeof this.options.height === "number" ? `${this.options.height}px` : "100%",
559
+ "important"
560
+ );
490
561
  frame.style.border = "0";
491
562
  Object.assign(frame.style, this.options.style);
492
563
  if (this.options.className) frame.className = this.options.className;
493
564
  const container = resolveContainer2(this.options.container);
565
+ this.containerEl = container;
494
566
  window.addEventListener("message", this.handleMessage);
495
567
  container.append(frame);
496
568
  this.frame = frame;
@@ -521,10 +593,13 @@ var EmbeddedDesigner = class {
521
593
  this.stopFill();
522
594
  this.unpinFullscreen();
523
595
  this.clearTimeoutTimer();
596
+ this.clearRenewTimer();
524
597
  this.removeOverlay();
525
598
  this.restoreContainerStyle();
526
599
  this.frame?.remove();
527
600
  this.frame = null;
601
+ this.containerEl = null;
602
+ this.fillMode = null;
528
603
  this.designerOrigin = "";
529
604
  this.phase = "loading";
530
605
  this.lastAutoHeight = "";
@@ -539,24 +614,77 @@ var EmbeddedDesigner = class {
539
614
  fillEnabled() {
540
615
  return typeof this.options.height !== "number";
541
616
  }
617
+ /** Write an SDK-managed height with `!important` so a host theme can't win. */
618
+ setFrameHeight(value) {
619
+ this.frame?.style.setProperty("height", value, "important");
620
+ }
542
621
  /**
543
- * Size the iframe so its bottom edge meets the bottom of the viewport
544
- * (`window.innerHeight - top`), clamped to `minHeight`. No-op while pinned
545
- * fullscreen (the pin fills the viewport itself).
622
+ * Decide whether the host gave the container a DEFINITE (bounded) height a
623
+ * fixed block the embed should fill 100% of — versus a content-sized container
624
+ * that collapses to whatever the iframe measures (full-page usage).
625
+ *
626
+ * We drive the iframe to two extreme heights within a single synchronous task
627
+ * and watch whether the container follows: a bounded box barely moves, a
628
+ * content-sized one grows with the iframe. Because we restore the height before
629
+ * yielding, the browser only lays out — it never paints the extremes, so there
630
+ * is no visible flash. Works for px, resolved `%`, and flex (`flex:1;min-h:0`)
631
+ * heights, and leaves a mere `min-height` floor classified as content-sized so
632
+ * full-page hosts keep the old viewport-fill behavior.
633
+ */
634
+ detectFillMode() {
635
+ const container = this.containerEl;
636
+ const frame = this.frame;
637
+ if (this.pinned || !container || !frame) return this.fillMode ?? "viewport";
638
+ const measure = () => container.getBoundingClientRect().height;
639
+ const savedValue = frame.style.getPropertyValue("height");
640
+ const savedPriority = frame.style.getPropertyPriority("height");
641
+ frame.style.setProperty("height", "0px", "important");
642
+ const collapsed = measure();
643
+ frame.style.setProperty("height", `${FILL_PROBE_HEIGHT_PX}px`, "important");
644
+ const expanded = measure();
645
+ if (savedValue) frame.style.setProperty("height", savedValue, savedPriority);
646
+ else frame.style.removeProperty("height");
647
+ const tracksIframe = expanded - collapsed > FILL_PROBE_TRACK_EPSILON_PX;
648
+ const bounded = !tracksIframe && collapsed >= FILL_MIN_DEFINITE_HEIGHT_PX;
649
+ return bounded ? "container" : "viewport";
650
+ }
651
+ /**
652
+ * Size the iframe for the current fill verdict, clamped to `minHeight`. In
653
+ * container mode it fills 100% of the bounded block; in viewport mode its
654
+ * bottom edge meets the bottom of the viewport (`window.innerHeight - top`).
655
+ * No-op while pinned fullscreen (the pin fills the viewport itself).
546
656
  */
547
657
  applyFill() {
548
658
  if (!this.frame || this.pinned) return;
549
659
  const min = this.options.minHeight ?? DEFAULT_MIN_FILL_HEIGHT;
660
+ if (this.fillMode === "container" && this.containerEl) {
661
+ const target2 = Math.max(min, Math.round(this.containerEl.getBoundingClientRect().height));
662
+ this.setFrameHeight(`${target2}px`);
663
+ return;
664
+ }
550
665
  const top = this.frame.getBoundingClientRect().top;
551
666
  const target = Math.max(min, Math.round(window.innerHeight - top));
552
- this.frame.style.height = `${target}px`;
667
+ this.setFrameHeight(`${target}px`);
668
+ }
669
+ /** Attach/detach the container ResizeObserver to match the current verdict. */
670
+ syncContainerObserver() {
671
+ const want = this.fillMode === "container" && !!this.containerEl && typeof ResizeObserver !== "undefined";
672
+ if (want && !this.resizeObs) {
673
+ this.resizeObs = new ResizeObserver(() => this.scheduleFill());
674
+ this.resizeObs.observe(this.containerEl);
675
+ } else if (!want && this.resizeObs) {
676
+ this.resizeObs.disconnect();
677
+ this.resizeObs = null;
678
+ }
553
679
  }
554
680
  startFill() {
681
+ this.fillMode = this.detectFillMode();
682
+ this.syncContainerObserver();
555
683
  this.applyFill();
556
684
  if (this.fillListening) return;
557
685
  this.fillListening = true;
558
- window.addEventListener("resize", this.scheduleFill);
559
- window.addEventListener("orientationchange", this.scheduleFill);
686
+ window.addEventListener("resize", this.scheduleReprobe);
687
+ window.addEventListener("orientationchange", this.scheduleReprobe);
560
688
  window.addEventListener("scroll", this.scheduleFill, { passive: true });
561
689
  }
562
690
  stopFill() {
@@ -564,10 +692,18 @@ var EmbeddedDesigner = class {
564
692
  cancelAnimationFrame(this.fillRaf);
565
693
  this.fillRaf = null;
566
694
  }
695
+ if (this.reprobeRaf !== null) {
696
+ cancelAnimationFrame(this.reprobeRaf);
697
+ this.reprobeRaf = null;
698
+ }
699
+ if (this.resizeObs) {
700
+ this.resizeObs.disconnect();
701
+ this.resizeObs = null;
702
+ }
567
703
  if (!this.fillListening) return;
568
704
  this.fillListening = false;
569
- window.removeEventListener("resize", this.scheduleFill);
570
- window.removeEventListener("orientationchange", this.scheduleFill);
705
+ window.removeEventListener("resize", this.scheduleReprobe);
706
+ window.removeEventListener("orientationchange", this.scheduleReprobe);
571
707
  window.removeEventListener("scroll", this.scheduleFill);
572
708
  }
573
709
  /**
@@ -579,16 +715,22 @@ var EmbeddedDesigner = class {
579
715
  if (this.pinned || !this.frame) return;
580
716
  this.pinned = true;
581
717
  this.frameStyleBeforeFs = this.frame.getAttribute("style");
582
- Object.assign(this.frame.style, {
718
+ const pin = {
583
719
  position: "fixed",
584
- inset: "0",
720
+ top: "0",
721
+ right: "0",
722
+ bottom: "0",
723
+ left: "0",
585
724
  width: "100vw",
586
725
  height: "100vh",
587
726
  margin: "0",
588
727
  border: "0",
589
- zIndex: "2147483000",
728
+ "z-index": "2147483000",
590
729
  background: "#101625"
591
- });
730
+ };
731
+ for (const [property, value] of Object.entries(pin)) {
732
+ this.frame.style.setProperty(property, value, "important");
733
+ }
592
734
  const docEl = document.documentElement;
593
735
  this.docOverflowBeforeFs = docEl.style.overflow;
594
736
  docEl.style.overflow = "hidden";
@@ -609,7 +751,7 @@ var EmbeddedDesigner = class {
609
751
  if (this.frameStyleBeforeFs === null) this.frame.removeAttribute("style");
610
752
  else this.frame.setAttribute("style", this.frameStyleBeforeFs);
611
753
  if (this.fillEnabled()) this.applyFill();
612
- else if (this.autoResizeEnabled() && this.lastAutoHeight) this.frame.style.height = this.lastAutoHeight;
754
+ else if (this.autoResizeEnabled() && this.lastAutoHeight) this.setFrameHeight(this.lastAutoHeight);
613
755
  }
614
756
  this.frameStyleBeforeFs = null;
615
757
  if (this.docOverflowBeforeFs !== null) {
@@ -631,6 +773,48 @@ var EmbeddedDesigner = class {
631
773
  this.timeoutTimer = null;
632
774
  }
633
775
  }
776
+ /**
777
+ * Auto-renewal (proactive + one expiry recovery) is on when the host wired a
778
+ * relaunch hook and did not opt out. Without the hook there is nothing to call,
779
+ * so it is a no-op.
780
+ */
781
+ autoRenewEnabled() {
782
+ return !!this.options.onRequestRelaunch && this.options.autoRenewSession !== false;
783
+ }
784
+ clearRenewTimer() {
785
+ if (this.renewTimer !== null) {
786
+ clearTimeout(this.renewTimer);
787
+ this.renewTimer = null;
788
+ }
789
+ }
790
+ /**
791
+ * Arm the proactive renewal timer from a `ready` message's `expiresAt` (epoch
792
+ * ms). We relaunch a comfortable lead before expiry so the host can mint a fresh
793
+ * session and swap `designerUrl` without the user ever seeing the expiry card:
794
+ *
795
+ * - normal TTL (≥ 15 min): renew {@link RENEW_LEAD_MS} (~3 min) before expiry;
796
+ * - short TTL (< 15 min): renew after {@link RENEW_SHORT_TTL_FRACTION} (80%) of
797
+ * the remaining life, so the lead can't overshoot the whole session;
798
+ * - either way, never sooner than {@link RENEW_MIN_DELAY_MS} (30s) after `ready`
799
+ * so a burst of `ready` messages can't spin the host.
800
+ *
801
+ * Re-armed on every `ready`; cleared on destroy / setDesignerUrl (via re-mount).
802
+ * A no-op when auto-renewal is off or `expiresAt` is missing/already past — the
803
+ * expiry-error path recovers a session that has already lapsed.
804
+ */
805
+ scheduleRenewal(expiresAt) {
806
+ this.clearRenewTimer();
807
+ if (!this.autoRenewEnabled()) return;
808
+ if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) return;
809
+ const remaining = expiresAt - Date.now();
810
+ if (remaining <= 0) return;
811
+ const lead = remaining < RENEW_SHORT_TTL_MS ? remaining * RENEW_SHORT_TTL_FRACTION : remaining - RENEW_LEAD_MS;
812
+ const delay = Math.max(RENEW_MIN_DELAY_MS, lead);
813
+ this.renewTimer = setTimeout(() => {
814
+ this.renewTimer = null;
815
+ if (this.autoRenewEnabled()) this.options.onRequestRelaunch();
816
+ }, delay);
817
+ }
634
818
  ensureContainerPositioned(container) {
635
819
  const position = getComputedStyle(container).position;
636
820
  if (position === "static") {
@@ -1278,6 +1462,17 @@ var CSS = `
1278
1462
  .sl-ba-title .spark{color:var(--sl-accent);font-size:16px}
1279
1463
  .sl-ba-copy{grid-column:1/-1;margin:-4px 0 2px 23px;color:var(--sl-muted);font-size:10.5px;line-height:1.35}
1280
1464
  .sl-ba-copy .narrow{display:none}
1465
+ /* "\u2605 Best seats" premium quick-pick \u2014 gold accent echoing the \u2605 Premium pill on
1466
+ the confirm popover; deliberately distinct from the accent-toned qty/go. */
1467
+ .sl-ba-premium{position:relative;z-index:1;grid-column:1/-1;justify-self:start;display:inline-flex;align-items:center;gap:6px;
1468
+ padding:6px 12px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.02em;cursor:pointer;
1469
+ color:#c9a24b;background:color-mix(in srgb,#e8c15a 10%,var(--sl-surface));
1470
+ border:1px solid color-mix(in srgb,#e8c15a 34%,var(--sl-line));transition:filter .15s,background .15s,color .15s}
1471
+ .sl-ba-premium .star{font-size:12px;line-height:1;color:#e8c15a}
1472
+ .sl-ba-premium:hover{filter:brightness(1.05)}
1473
+ .sl-ba-premium.on{color:#1c1608;background:linear-gradient(135deg,#f0cf6b,#e0b23f);border-color:transparent;
1474
+ box-shadow:0 6px 16px color-mix(in srgb,#e8c15a 26%,transparent)}
1475
+ .sl-ba-premium.on .star{color:#5a4410}
1281
1476
  .sl-ba select{background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:8px;
1282
1477
  font:inherit;font-size:11px;padding:7px 8px;min-width:0;width:100%;max-width:none}
1283
1478
  .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)}
@@ -1375,6 +1570,25 @@ var CSS = `
1375
1570
  .sl-confirm-view:hover{border-color:var(--sl-muted)}
1376
1571
  .sl-confirm-view svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
1377
1572
 
1573
+ /* commercial seat flags \u2014 limited-view caution + premium tag. Amber tone,
1574
+ deliberately distinct from the red taken/held state; shown on the confirm
1575
+ card, echoed as a small \u25D0 marker on cart chips and the hover tip. */
1576
+ .sl-cx{display:flex;flex-direction:column;gap:6px;margin-bottom:10px}
1577
+ .sl-cx-warn{display:flex;align-items:flex-start;gap:7px;padding:8px 10px;border-radius:9px;
1578
+ background:color-mix(in srgb,#f4b740 13%,var(--sl-surface));border:1px solid color-mix(in srgb,#f4b740 40%,var(--sl-line));
1579
+ animation:slNoticeIn .28s ease both}
1580
+ .sl-cx-glyph{flex:none;font-size:14px;line-height:1.2;color:#f4b740}
1581
+ .sl-cx-txt{min-width:0;display:flex;flex-direction:column;gap:2px}
1582
+ .sl-cx-txt b{font-size:12px;font-weight:800;color:var(--sl-text)}
1583
+ .sl-cx-note{font-size:11px;line-height:1.4;color:var(--sl-muted)}
1584
+ .sl-cx-premium{display:inline-flex;align-items:center;gap:6px;align-self:flex-start;padding:4px 10px;border-radius:999px;
1585
+ font-size:11px;font-weight:800;letter-spacing:.02em;color:#c9a24b;
1586
+ background:color-mix(in srgb,#e8c15a 13%,var(--sl-surface));border:1px solid color-mix(in srgb,#e8c15a 38%,var(--sl-line))}
1587
+ .sl-cx-star{font-size:12px;line-height:1;color:#e8c15a}
1588
+ .sl-cx-mark{flex:none;font-size:12px;line-height:1;color:#f4b740;cursor:help}
1589
+ .sl-tip-cx{display:flex;align-items:center;gap:6px;padding:5px 10px 7px;font-size:10.5px;font-weight:700;color:#e8b24a}
1590
+ .sl-tip-cx .g{font-size:12px}
1591
+
1378
1592
  /* 360\xB0 seat-view modal (fills the widget; drag-to-look-around equirectangular) */
1379
1593
  .sl-view{position:absolute;inset:0;z-index:12;display:flex;flex-direction:column;background:var(--sl-bg)}
1380
1594
  .sl-view-head{display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid var(--sl-line);flex:none}
@@ -1503,6 +1717,8 @@ var SeatPicker = class _SeatPicker {
1503
1717
  this.srEl = null;
1504
1718
  this.baQty = 2;
1505
1719
  this.baCat = "";
1720
+ /** "★ Best seats" premium quick-pick toggle — biases best-available to premium seats. */
1721
+ this.baPremium = false;
1506
1722
  this.bestAvailableConfirm = false;
1507
1723
  this.releasingHold = false;
1508
1724
  /** Event sales window is closed (read-only load state / live close). */
@@ -1659,6 +1875,50 @@ var SeatPicker = class _SeatPicker {
1659
1875
  const sight = distance != null ? (0, import_core2.t)("picker.sightline", { m: distance }) : this.tf("picker.sightlineClear", "Clear sightline");
1660
1876
  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>`;
1661
1877
  }
1878
+ /** Minimal HTML/attribute escaper for buyer-authored commercial text (notes). */
1879
+ escCx(value) {
1880
+ return String(value ?? "").replace(/[&<>"]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch]);
1881
+ }
1882
+ /** Localized "Restricted view" / "Obstructed view" label for a seat's flags,
1883
+ * or '' when neither is set. Restricted takes precedence when both are on. */
1884
+ limitedViewLabel(c) {
1885
+ if (c?.restrictedView) return this.tf("picker.restrictedView", "Restricted view");
1886
+ if (c?.obstructedView) return this.tf("picker.obstructedView", "Obstructed view");
1887
+ return "";
1888
+ }
1889
+ /**
1890
+ * Commercial flags block for the confirm/detail surface: a subtle ★ Premium
1891
+ * tag plus an amber ◐ limited-view caution (with the organizer's note when
1892
+ * present). '' when the seat carries no surfaced commercial flag.
1893
+ */
1894
+ commercialConfirmHtml(c) {
1895
+ if (!c) return "";
1896
+ const rows = [];
1897
+ if (c.premium) {
1898
+ rows.push(
1899
+ `<div class="sl-cx-premium"><span class="sl-cx-star" aria-hidden="true">\u2605</span>${this.tf("picker.premiumSeat", "Premium seat")}</div>`
1900
+ );
1901
+ }
1902
+ const limited = this.limitedViewLabel(c);
1903
+ if (limited) {
1904
+ rows.push(
1905
+ `<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>`
1906
+ );
1907
+ } else if (c.note) {
1908
+ rows.push(
1909
+ `<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>`
1910
+ );
1911
+ }
1912
+ return rows.length ? `<div class="sl-cx">${rows.join("")}</div>` : "";
1913
+ }
1914
+ /** Small ◐ limited-view marker for a cart chip; title/aria uses the seat's
1915
+ * note when present, else the generic view label. '' for a clear-view seat. */
1916
+ commercialChipMarker(c) {
1917
+ const limited = this.limitedViewLabel(c);
1918
+ if (!limited) return "";
1919
+ const title = this.escCx(c?.note ? c.note : limited);
1920
+ return `<span class="sl-cx-mark" role="img" aria-label="${title}" title="${title}">\u25D0</span>`;
1921
+ }
1662
1922
  /** True when the picker is rendered inside an iframe (snippet embed at /e/:key). */
1663
1923
  isFramed() {
1664
1924
  return typeof window !== "undefined" && window.parent !== window;
@@ -2031,45 +2291,71 @@ var SeatPicker = class _SeatPicker {
2031
2291
  this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
2032
2292
  this.buildBadge(chartTheme);
2033
2293
  const present = /* @__PURE__ */ new Set();
2294
+ let hasLimitedView = false;
2034
2295
  if (this.controller.doc) {
2035
2296
  for (const seat of (0, import_core2.expandChart)(this.controller.doc)) {
2036
2297
  for (const type of seat.accessibility ?? []) present.add(type);
2037
2298
  if (seat.accessible && !seat.accessibility?.length) present.add("wheelchair");
2299
+ if (seat.commercial?.restrictedView || seat.commercial?.obstructedView) hasLimitedView = true;
2038
2300
  }
2039
2301
  }
2040
- if (present.size) {
2302
+ const focusSeatsForFilter = () => {
2303
+ if (this.rungsEl && this.controller.getRung() !== "seats") {
2304
+ this.controller.setRung("seats");
2305
+ this.collapseSectionCard();
2306
+ this.syncRung();
2307
+ }
2308
+ };
2309
+ if (present.size || hasLimitedView) {
2041
2310
  const chips = document.createElement("div");
2042
2311
  chips.className = "sl-chips";
2043
- const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
2044
- const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-f="${key}">${label}</button>`;
2045
- chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
2046
2312
  this.regions["top-left"].appendChild(chips);
2047
2313
  this.a11yChipsEl = chips;
2048
- const active = /* @__PURE__ */ new Set();
2049
- const syncChips = () => {
2050
- chips.querySelectorAll("button").forEach((b) => {
2051
- const f = b.dataset.f;
2052
- const on = f === "all" ? active.size === 0 : active.has(f);
2053
- b.classList.toggle("on", on);
2054
- b.setAttribute("aria-pressed", String(on));
2314
+ if (present.size) {
2315
+ const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
2316
+ const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-a11y="1" data-f="${key}">${label}</button>`;
2317
+ chips.insertAdjacentHTML(
2318
+ "beforeend",
2319
+ mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("")
2320
+ );
2321
+ const active = /* @__PURE__ */ new Set();
2322
+ const syncChips = () => {
2323
+ chips.querySelectorAll("button[data-a11y]").forEach((b) => {
2324
+ const f = b.dataset.f;
2325
+ const on = f === "all" ? active.size === 0 : active.has(f);
2326
+ b.classList.toggle("on", on);
2327
+ b.setAttribute("aria-pressed", String(on));
2328
+ });
2329
+ const filter = active.size ? [...active] : null;
2330
+ this.controller.setAccessibilityFilter(filter);
2331
+ if (filter) focusSeatsForFilter();
2332
+ };
2333
+ chips.querySelectorAll("button[data-a11y]").forEach((btn) => {
2334
+ btn.addEventListener("click", () => {
2335
+ const f = btn.dataset.f;
2336
+ if (f === "all") active.clear();
2337
+ else if (active.has(f)) active.delete(f);
2338
+ else active.add(f);
2339
+ syncChips();
2340
+ });
2055
2341
  });
2056
- const filter = active.size ? [...active] : null;
2057
- this.controller.setAccessibilityFilter(filter);
2058
- if (filter && this.rungsEl && this.controller.getRung() !== "seats") {
2059
- this.controller.setRung("seats");
2060
- this.collapseSectionCard();
2061
- this.syncRung();
2062
- }
2063
- };
2064
- chips.querySelectorAll("button").forEach((btn) => {
2065
- btn.addEventListener("click", () => {
2066
- const f = btn.dataset.f;
2067
- if (f === "all") active.clear();
2068
- else if (active.has(f)) active.delete(f);
2069
- else active.add(f);
2070
- syncChips();
2342
+ }
2343
+ if (hasLimitedView) {
2344
+ const limited = document.createElement("button");
2345
+ limited.type = "button";
2346
+ limited.className = "sl-chip-f";
2347
+ limited.setAttribute("aria-pressed", "false");
2348
+ limited.innerHTML = `\u25D0 ${this.tf("picker.hideLimitedView", "Hide limited-view seats")}`;
2349
+ chips.appendChild(limited);
2350
+ let limitedOn = false;
2351
+ limited.addEventListener("click", () => {
2352
+ limitedOn = !limitedOn;
2353
+ limited.classList.toggle("on", limitedOn);
2354
+ limited.setAttribute("aria-pressed", String(limitedOn));
2355
+ this.controller.setCommercialLimitedFilter(limitedOn);
2356
+ if (limitedOn) focusSeatsForFilter();
2071
2357
  });
2072
- });
2358
+ }
2073
2359
  }
2074
2360
  const cb = document.createElement("button");
2075
2361
  cb.type = "button";
@@ -2792,7 +3078,7 @@ var SeatPicker = class _SeatPicker {
2792
3078
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
2793
3079
  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>`;
2794
3080
  }).join("");
2795
- 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>`;
3081
+ 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>`;
2796
3082
  card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
2797
3083
  card.querySelector(".sl-seccard-overview").addEventListener("click", () => this.controller.overview());
2798
3084
  (this.regions["top-center"] ?? this.els.map).appendChild(card);
@@ -2886,7 +3172,7 @@ var SeatPicker = class _SeatPicker {
2886
3172
  el.setAttribute("aria-modal", "true");
2887
3173
  el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
2888
3174
  el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
2889
- 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>`;
3175
+ 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>`;
2890
3176
  this.els.map.appendChild(el);
2891
3177
  this.confirmEl = el;
2892
3178
  this.reanchorConfirm();
@@ -3181,14 +3467,16 @@ var SeatPicker = class _SeatPicker {
3181
3467
  const noPicks = !seats.length && !heldItems.length && !this.pendingGACount();
3182
3468
  if (!this.hold && (noPicks || this.bestAvailableBusy || this.bestAvailableConfirm)) {
3183
3469
  const cats = this.controller.doc?.categories ?? [];
3184
- 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>`);
3470
+ 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
3471
+ // seats (same present-only philosophy as the a11y filter chips).
3472
+ (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>`);
3185
3473
  }
3186
3474
  const idGrid = (seatId, label) => {
3187
3475
  const d = seatId ? this.controller.seatDetails(seatId) : null;
3188
3476
  if (!d?.sectionLabel && !d?.rowLabel && !d?.seatNumber) {
3189
3477
  return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">Seat</span><span class="val">${label}</span></span></div>`;
3190
3478
  }
3191
- 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>`;
3479
+ 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>`;
3192
3480
  };
3193
3481
  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>`;
3194
3482
  for (const item of heldItems) {
@@ -3199,7 +3487,7 @@ var SeatPicker = class _SeatPicker {
3199
3487
  const heldSeat = item.objectType !== "ga" ? this.controller.seatByLabel(item.label) : null;
3200
3488
  const canView2 = this.seatViewEnabled() && !!heldSeat;
3201
3489
  parts.push(
3202
- `<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>`
3490
+ `<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>`
3203
3491
  );
3204
3492
  }
3205
3493
  const heldLabels = new Set(heldItems.map((item) => item.label));
@@ -3210,7 +3498,7 @@ var SeatPicker = class _SeatPicker {
3210
3498
  const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
3211
3499
  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>` : "";
3212
3500
  parts.push(
3213
- `<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>`
3501
+ `<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>`
3214
3502
  );
3215
3503
  }
3216
3504
  for (const area of gaAreas) {
@@ -3230,6 +3518,10 @@ var SeatPicker = class _SeatPicker {
3230
3518
  this.els.tray.querySelector("[data-ba-cat]")?.addEventListener("change", (e) => {
3231
3519
  this.baCat = e.target.value;
3232
3520
  });
3521
+ this.els.tray.querySelector("[data-ba-premium]")?.addEventListener("click", () => {
3522
+ this.baPremium = !this.baPremium;
3523
+ this.syncTray();
3524
+ });
3233
3525
  this.els.tray.querySelector(".sl-ba-go")?.addEventListener("click", () => {
3234
3526
  if (this.pendingSelectionCount() > 0) {
3235
3527
  this.bestAvailableConfirm = true;
@@ -3237,7 +3529,7 @@ var SeatPicker = class _SeatPicker {
3237
3529
  this.els.tray.querySelector("[data-ba-replace]")?.focus();
3238
3530
  return;
3239
3531
  }
3240
- void this.bestAvailable(this.baQty, this.baCat || void 0);
3532
+ void this.bestAvailable(this.baQty, this.baCat || void 0, { preferPremium: this.baPremium });
3241
3533
  });
3242
3534
  this.els.tray.querySelector("[data-ba-cancel]")?.addEventListener("click", () => {
3243
3535
  this.bestAvailableConfirm = false;
@@ -3246,7 +3538,7 @@ var SeatPicker = class _SeatPicker {
3246
3538
  });
3247
3539
  this.els.tray.querySelector("[data-ba-replace]")?.addEventListener("click", () => {
3248
3540
  this.bestAvailableConfirm = false;
3249
- void this.bestAvailable(this.baQty, this.baCat || void 0);
3541
+ void this.bestAvailable(this.baQty, this.baCat || void 0, { preferPremium: this.baPremium });
3250
3542
  });
3251
3543
  this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
3252
3544
  btn.addEventListener("click", () => {
@@ -3628,6 +3920,12 @@ var SeatPicker = class _SeatPicker {
3628
3920
  * the prefix is exact (won't touch "1040-A" under section "104"); otherwise
3629
3921
  * the label is shown verbatim.
3630
3922
  */
3923
+ /** Buyer-facing type word for the row/table key label — the designer's
3924
+ * per-object "Displayed type" override, or the default "Row". */
3925
+ rowTypeWord(details) {
3926
+ const t3 = details?.rowType?.trim();
3927
+ return t3 || "Row";
3928
+ }
3631
3929
  rowShort(details) {
3632
3930
  const row = details?.rowLabel;
3633
3931
  const sec = details?.sectionLabel;
@@ -3647,10 +3945,12 @@ var SeatPicker = class _SeatPicker {
3647
3945
  const esc2 = (v) => String(v ?? "\u2014").replace(/[&<>"]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch]);
3648
3946
  const price = this.money(this.paidPrice(details.categoryKey, details.tierId ?? null, details.price));
3649
3947
  const hasLoc = details.sectionLabel || details.rowLabel || details.seatNumber;
3650
- 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>`;
3948
+ 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>`;
3651
3949
  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>`;
3950
+ const limited = this.limitedViewLabel(details.commercial);
3951
+ const cxLine = limited ? `<div class="sl-tip-cx"><span class="g" aria-hidden="true">\u25D0</span>${esc2(limited)}</div>` : "";
3652
3952
  this.tipEl.style.setProperty("--sl-cat", details.categoryColor);
3653
- 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;
3953
+ 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;
3654
3954
  this.tipEl.style.display = "block";
3655
3955
  this.placeTooltip();
3656
3956
  }
@@ -3670,7 +3970,7 @@ var SeatPicker = class _SeatPicker {
3670
3970
  async removeHeldTicket(label) {
3671
3971
  return this.removeHeldLabel(label);
3672
3972
  }
3673
- async bestAvailable(qty, categoryKey) {
3973
+ async bestAvailable(qty, categoryKey, opts = {}) {
3674
3974
  if (this.salesClosed || this.bestAvailableBusy) return null;
3675
3975
  qty = Math.max(1, Math.min(this.maxTickets, Math.floor(qty)));
3676
3976
  if (this.confirmSeat) this.cancelConfirm();
@@ -3682,7 +3982,7 @@ var SeatPicker = class _SeatPicker {
3682
3982
  button.innerHTML = '<span class="sl-ba-spin" aria-hidden="true"></span>Finding\u2026';
3683
3983
  }
3684
3984
  try {
3685
- const h = await this.controller.bestAvailable(qty, categoryKey);
3985
+ const h = await this.controller.bestAvailable(qty, categoryKey, opts);
3686
3986
  if (h) {
3687
3987
  this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
3688
3988
  this.handedOff = false;
@@ -3692,6 +3992,9 @@ var SeatPicker = class _SeatPicker {
3692
3992
  this.flashHeldSeats(this.hold);
3693
3993
  this.syncTray();
3694
3994
  this.emitHoldChange();
3995
+ if (opts.preferPremium && h.seats.length && !h.seats.every((s) => s.commercial?.premium)) {
3996
+ this.toast((0, import_core2.t)("picker.premiumFallbackNote", { count: qty }), "neutral");
3997
+ }
3695
3998
  return this.hold;
3696
3999
  }
3697
4000
  return null;