@seatlayer/js 0.46.0 → 0.47.1

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
@@ -270,8 +270,12 @@ var BuyerRealtimeClient = class {
270
270
  const next = projectionFromSnapshot(frame);
271
271
  const changes = diffProjections(this.projection, next);
272
272
  this.projection = next;
273
- if (changes === null) void this.opts.sink.resync();
274
- else if (changes.length) this.opts.sink.applyStatuses(changes);
273
+ if (changes === null) {
274
+ if (this.opts.sink.applyProjection) this.opts.sink.applyProjection(next);
275
+ else void this.opts.sink.resync();
276
+ } else if (changes.length) {
277
+ this.opts.sink.applyStatuses(changes);
278
+ }
275
279
  return;
276
280
  }
277
281
  if (type === "delta" && Array.isArray(frame.changes)) {
@@ -524,7 +528,7 @@ var PubApi = class {
524
528
  return this.request(`/pub/events/${encodeURIComponent(key)}/chart`);
525
529
  }
526
530
  objects(key) {
527
- return this.request(`/pub/events/${encodeURIComponent(key)}/objects`);
531
+ return this.request(`/pub/events/${encodeURIComponent(key)}/objects?compact=1`);
528
532
  }
529
533
  hold(key, selections, ttlMs, replaceHoldId) {
530
534
  return this.request(`/pub/events/${encodeURIComponent(key)}/hold`, {
@@ -941,7 +945,6 @@ var SeatingChart = class {
941
945
  this.mount = null;
942
946
  this.hostEl = null;
943
947
  this.rendered = false;
944
- this.retryBtn = null;
945
948
  this.mode_ = null;
946
949
  this.tipEl = null;
947
950
  this.tipPos = { x: 0, y: 0 };
@@ -1316,14 +1319,12 @@ var SeatingChart = class {
1316
1319
  void this.render().catch((err) => this.opts.onError?.(err));
1317
1320
  });
1318
1321
  box.appendChild(btn);
1319
- this.retryBtn = btn;
1320
1322
  host.appendChild(box);
1321
1323
  }
1322
1324
  destroy() {
1323
1325
  this.realtime?.stop();
1324
1326
  this.realtime = null;
1325
1327
  this.access?.clear();
1326
- this.retryBtn = null;
1327
1328
  if (this.hostEl && this.onTipMove) this.hostEl.removeEventListener("mousemove", this.onTipMove);
1328
1329
  this.tipEl = null;
1329
1330
  this.onTipMove = null;
@@ -1981,10 +1982,17 @@ import {
1981
1982
  t as t2,
1982
1983
  tCount
1983
1984
  } from "@seatlayer/core";
1985
+ import { isAuthoredSeatView, seatViewDisclosure } from "@seatlayer/core/view3d/crossfade/panorama";
1986
+ import {
1987
+ browserPanoramaConstraints,
1988
+ loadPanoramaImage,
1989
+ planPanoramaDelivery,
1990
+ schedulePanoramaUpgrade
1991
+ } from "@seatlayer/core/view/panoramaDelivery";
1984
1992
  var DEFAULT_API_BASE2 = "https://api.seatlayer.io";
1985
1993
  var DEFAULT_MAX_SELECTION2 = 10;
1986
1994
  var EXTEND_PROMPT_MS = 6e4;
1987
- var MAX_3D_SEATS_DEFAULT = 15e3;
1995
+ var MAX_3D_SEATS_DEFAULT = 6e4;
1988
1996
  var MAX_3D_SECTION_PILLS = 12;
1989
1997
  function pointInPolygon(x, y, poly) {
1990
1998
  let inside = false;
@@ -2067,7 +2075,7 @@ async function loadHostedCheckout() {
2067
2075
  cdnChunkUrl("seatlayer-checkout.mjs")
2068
2076
  );
2069
2077
  }
2070
- return import("./hostedCheckout-PLHKPT6W.js");
2078
+ return import("./hostedCheckout-SHQCWN5Y.js");
2071
2079
  }
2072
2080
  function paymentsOffReason(reason) {
2073
2081
  return reason === "unavailable_for_event" || reason === "payments_off_for_event" ? reason : "not_configured";
@@ -2328,7 +2336,12 @@ var CSS = (
2328
2336
  INTO one of these positioned flex containers and flows/stacks within it, so no
2329
2337
  two pieces of chrome free-float on top of each other. Regions never overlap:
2330
2338
  the top strip splits into left/center/right; rails + corners own their edge. */
2331
- .sl-anchor{position:absolute;z-index:5;display:flex;gap:8px;pointer-events:none}
2339
+ /* align-items:center, not the flex default of stretch. Without it a short pill
2340
+ beside a taller control (TEST MODE next to the Map/3D toggle) is stretched to
2341
+ the row's height, so two pills that should read as a matched pair end up
2342
+ different shapes on different baselines. Column regions set their own
2343
+ cross-axis alignment below and are unaffected. */
2344
+ .sl-anchor{position:absolute;z-index:5;display:flex;align-items:center;gap:8px;pointer-events:none}
2332
2345
  .sl-anchor > *{pointer-events:auto}
2333
2346
  .sl-anchor[data-region="top-left"]{top:12px;left:12px;flex-wrap:wrap;max-width:38%}
2334
2347
  .sl-anchor[data-region="top-center"]{top:12px;left:50%;transform:translateX(-50%);flex-direction:column;
@@ -2344,10 +2357,17 @@ var CSS = (
2344
2357
  .sl-picker[data-layout="narrow"] .sl-anchor[data-region="top-center"]{max-width:44%}
2345
2358
 
2346
2359
  /* TEST MODE badge \u2014 a small pill in the top-right region (shrinks on narrow) */
2347
- .sl-testbadge{padding:5px 11px;border-radius:999px;font-size:10px;font-weight:800;letter-spacing:.1em;
2360
+ /* align-self:stretch, not a hardcoded height: the badge sits beside the Map/3D
2361
+ toggle, whose height comes from its own border + padding + button metrics.
2362
+ Stretching matches that row exactly and keeps matching if the toggle ever
2363
+ changes, while the badge's own inline-flex keeps the label centred inside
2364
+ whatever height it gets. Alone in the region it simply takes its natural
2365
+ size. */
2366
+ .sl-testbadge{display:inline-flex;align-items:center;align-self:stretch;padding:0 12px;border-radius:999px;
2367
+ font-size:10px;font-weight:800;letter-spacing:.1em;line-height:1;
2348
2368
  text-transform:uppercase;white-space:nowrap;background:var(--sl-accent);color:var(--sl-accent-ink);
2349
2369
  box-shadow:0 2px 8px rgba(0,0,0,.25)}
2350
- .sl-picker[data-layout="narrow"] .sl-testbadge{padding:3px 8px;font-size:8.5px;letter-spacing:.06em}
2370
+ .sl-picker[data-layout="narrow"] .sl-testbadge{padding:0 8px;font-size:8.5px;letter-spacing:.06em}
2351
2371
 
2352
2372
  /* zoom column (flows within the bottom-right region) */
2353
2373
  .sl-zoom{display:flex;flex-direction:column;gap:6px}
@@ -2439,7 +2459,11 @@ var CSS = (
2439
2459
  Layered Rows mark + wordmark. Hidden when the host opts out or the org's paid
2440
2460
  theme sets hideBadge. */
2441
2461
  .sl-powered{display:flex;align-items:center;justify-content:center;gap:6px;margin-top:10px;
2442
- font-size:11px;letter-spacing:.03em;color:var(--sl-muted)}
2462
+ font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--sl-text);opacity:.72;
2463
+ text-decoration:none;padding:6px 10px;border-radius:999px;width:fit-content;margin-inline:auto;
2464
+ transition:opacity .15s ease,background-color .15s ease}
2465
+ .sl-powered:hover{opacity:1;background:color-mix(in srgb,var(--sl-text) 8%,transparent)}
2466
+ .sl-powered:focus-visible{opacity:1;outline:2px solid var(--sl-accent);outline-offset:2px}
2443
2467
  .sl-powered-mark{width:16px;height:16px;border-radius:4px;flex:none;display:flex;align-items:center;justify-content:center;
2444
2468
  background:#0c1220;color:#fcf7ee}
2445
2469
  .sl-powered-mark svg{width:12px;height:11px}
@@ -2618,18 +2642,41 @@ var CSS = (
2618
2642
  .sl-view3d{position:absolute;inset:0;z-index:4;opacity:0;touch-action:none;
2619
2643
  transition:opacity .3s ease;background:radial-gradient(120% 120% at 50% 0%,#191f28 0%,#0d1014 70%)}
2620
2644
  .sl-view3d canvas{display:block;width:100%;height:100%}
2645
+ .sl-view3d canvas:focus-visible{outline:2px solid var(--sl-accent);outline-offset:-3px}
2646
+ .sl-view3d-loading{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:10px;
2647
+ z-index:1;color:#d9e2f2;font-size:13px;font-weight:700;letter-spacing:.02em;pointer-events:none}
2648
+ .sl-view3d-loading::before{content:"";width:18px;height:18px;border-radius:50%;
2649
+ border:2px solid rgba(217,226,242,.28);border-top-color:#d9e2f2;animation:slSpin .8s linear infinite}
2621
2650
  [data-view3d=on] .sl-chips,[data-view3d=on] .sl-rungs{display:none}
2622
2651
  .sl-view3d-back{position:absolute;top:12px;left:12px;z-index:2;display:inline-flex;align-items:center;gap:6px;
2623
2652
  padding:7px 13px 7px 9px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.03em;
2624
2653
  color:#e6edf3;background:rgba(10,14,20,.62);border:1px solid rgba(255,255,255,.22);backdrop-filter:blur(6px)}
2625
2654
  .sl-view3d-back:hover,.sl-view3d-back:focus-visible{background:rgba(16,22,30,.82);border-color:rgba(255,255,255,.4)}
2626
2655
  .sl-view3d-back svg{width:15px;height:15px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round;stroke-linejoin:round}
2656
+ .sl-view3d-fs{position:absolute;top:12px;right:12px;z-index:2;min-width:44px;min-height:44px;padding:8px 12px;
2657
+ border-radius:999px;font-size:16px;color:#e6edf3;background:rgba(10,14,20,.62);
2658
+ border:1px solid rgba(255,255,255,.22);backdrop-filter:blur(6px)}
2659
+ .sl-view3d-fs:hover,.sl-view3d-fs:focus-visible{background:rgba(16,22,30,.82);border-color:rgba(255,255,255,.4)}
2627
2660
  /* Venue navigation inside 3D: levels (isolate a floor) and areas (fly to a zone).
2628
2661
  Bottom-LEFT, clear of the module's own chips (Overview bottom-right, 360
2629
2662
  bottom-centre) and of the bottom-sheeted confirm card. Horizontally scrollable
2630
- so a venue with many zones never pushes the rail off a phone screen. */
2663
+ so a venue with many zones never pushes the rail off a phone screen.
2664
+
2665
+ EVERY WIDTH HERE IS RELATIVE TO THE RAIL, NEVER TO THE WINDOW. These three
2666
+ rules used to size off 100vw, which contradicts the one contract this widget
2667
+ states about itself: it adapts to a full-screen takeover, an inline div in a
2668
+ content page, or a popup, and its breakpoints key off the CONTAINER, never the
2669
+ viewport. A 390 px picker embedded in a 1400 px page asked for
2670
+ calc(100vw - 180px) = 1220 inside a 390 px rail.
2671
+
2672
+ MEASURED, IT DID NOT OVERFLOW: the scroll parent is a flex container, so the
2673
+ over-large declaration was shrunk back to the rail and both the old and new
2674
+ rules resolve to 364 px in situ. So this is a latent correctness fix, not a
2675
+ visible bug -- the numbers were wrong and were being covered for by a
2676
+ flex-shrink one level up. Left as 100% because the next person to change that
2677
+ parent's display should not inherit a rule that only works by accident. */
2631
2678
  .sl-view3d-nav{position:absolute;left:12px;bottom:16px;z-index:3;display:flex;flex-direction:column;gap:6px;
2632
- max-width:calc(100% - 24px);pointer-events:none}
2679
+ max-width:calc(100% - 150px);pointer-events:none}
2633
2680
  .sl-view3d-nav > div{display:flex;gap:6px;overflow-x:auto;scrollbar-width:none;pointer-events:auto;
2634
2681
  padding:1px;-webkit-overflow-scrolling:touch}
2635
2682
  .sl-view3d-nav > div::-webkit-scrollbar{display:none}
@@ -2638,6 +2685,21 @@ var CSS = (
2638
2685
  border:1px solid rgba(150,165,205,.35);backdrop-filter:blur(6px);cursor:pointer}
2639
2686
  .sl-view3d-nav button:hover,.sl-view3d-nav button:focus-visible{color:#eef1f8;border-color:rgba(190,205,240,.6)}
2640
2687
  .sl-view3d-nav button[aria-pressed="true"]{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
2688
+ .sl-view3d-nav button:disabled{opacity:.45;cursor:not-allowed}
2689
+ .sl-view3d-nav select{min-height:38px;max-width:100%;padding:7px 34px 7px 12px;
2690
+ border-radius:999px;font:700 11.5px/1 inherit;color:#eef1f8;background:rgba(12,18,32,.86);
2691
+ border:1px solid rgba(150,165,205,.45);backdrop-filter:blur(6px);cursor:pointer}
2692
+ .sl-view3d-nav select:focus-visible{outline:2px solid var(--sl-accent);outline-offset:2px}
2693
+ .sl-view3d-nav .sl-view3d-locator{display:grid;grid-template-columns:minmax(150px,1.25fr) minmax(110px,.8fr) minmax(105px,.7fr) auto;
2694
+ width:min(720px,100%);overflow:visible}
2695
+ .sl-view3d-nav.is-seat-focused .sl-view3d-locator{display:none}
2696
+ .sl-view3d-locator select{width:100%;min-width:0}
2697
+ .sl-picker[data-layout="narrow"] .sl-view3d-nav .sl-view3d-locator{display:flex;width:100%;overflow-x:auto}
2698
+ .sl-picker[data-layout="narrow"] .sl-view3d-locator select{flex:0 0 155px}
2699
+ /* On phones the library owns the bottom edge for seat/panorama/overview actions.
2700
+ Keep venue navigation in a separate top rail so those control families never
2701
+ stack over one another. */
2702
+ .sl-picker[data-layout="narrow"] .sl-view3d-nav{top:68px;bottom:auto;max-width:calc(100% - 24px)}
2641
2703
  /* While immersed, the 2D-only chrome is meaningless \u2014 hide it, keep Map|3D. */
2642
2704
  .sl-picker[data-view3d="on"] .sl-rungs,
2643
2705
  .sl-picker[data-view3d="on"] .sl-floors,
@@ -2716,7 +2778,6 @@ var CSS = (
2716
2778
  .sl-confirm-thumb-badge{position:absolute;right:7px;top:7px;display:inline-flex;align-items:center;gap:5px;
2717
2779
  font-size:10px;font-weight:700;color:#fff;background:rgba(10,14,22,0.72);border-radius:12px;padding:4px 9px;backdrop-filter:blur(3px)}
2718
2780
  .sl-confirm-sight{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--sl-muted);margin-bottom:2px}
2719
- .sl-confirm-sight span{color:#22a06b;font-weight:800}
2720
2781
  .sl-confirm-view{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
2721
2782
  color:var(--sl-text);font-weight:700;font-size:12px;display:flex;align-items:center;justify-content:center;gap:7px}
2722
2783
  .sl-confirm-view:hover{border-color:var(--sl-muted)}
@@ -2811,6 +2872,11 @@ var CSS = (
2811
2872
  /* modal host */
2812
2873
  .sl-modal-scrim{position:fixed;inset:0;z-index:2147483000;background:rgba(5,7,12,.66);display:flex;align-items:center;justify-content:center;padding:18px}
2813
2874
  .sl-modal-frame{width:min(1200px,100%);height:min(820px,100%);border-radius:16px;overflow:hidden;box-shadow:0 40px 120px -30px rgba(0,0,0,.8)}
2875
+ /* The frame already clips to 16px. A picker rounding itself to --sl-radius
2876
+ (14px) inside it leaves a sliver of scrim showing at each corner, which
2877
+ reads as a rendering fault rather than as a rounded card. One owner of the
2878
+ corners, and it is the frame. */
2879
+ .sl-modal-frame > .sl-picker{border-radius:0}
2814
2880
  @media(max-width:640px){.sl-modal-scrim{padding:0}.sl-modal-frame{width:100%;height:100%;border-radius:0}}
2815
2881
  `
2816
2882
  );
@@ -2821,6 +2887,17 @@ function ensureStyle() {
2821
2887
  el.textContent = CSS;
2822
2888
  document.head.appendChild(el);
2823
2889
  }
2890
+ function formatWhen(startsAt, timezone, locale) {
2891
+ const options = { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" };
2892
+ const at = new Date(startsAt);
2893
+ if (timezone) {
2894
+ try {
2895
+ return at.toLocaleString(locale, { ...options, timeZone: timezone });
2896
+ } catch {
2897
+ }
2898
+ }
2899
+ return at.toLocaleString(locale, options);
2900
+ }
2824
2901
  function resolveTokens(chart, host) {
2825
2902
  const accent = host?.accent ?? chart?.accent ?? "#f4b740";
2826
2903
  const accentInk = host?.accentInk ?? chart?.accentInk ?? "#1a1200";
@@ -3015,6 +3092,9 @@ var SeatPicker = class _SeatPicker {
3015
3092
  currency: options.currency,
3016
3093
  flashOnLiveChange: true,
3017
3094
  colorblindSafe: this.cbSafe,
3095
+ // The drawn map's own overrides, when the host supplied them up front.
3096
+ // Everything else on `theme` is CSS and lands on the root instead.
3097
+ mapTheme: options.theme?.map ?? null,
3018
3098
  onSelectionChange: () => {
3019
3099
  this.syncTray();
3020
3100
  if (this.committedSelection().length) this.collapseSectionCard();
@@ -3103,8 +3183,8 @@ var SeatPicker = class _SeatPicker {
3103
3183
  /**
3104
3184
  * Eager sightline preview for the confirm card: the organizer's real view
3105
3185
  * photo, or — only when the chart has an actual stage to look at — a generated
3106
- * forward view plus an "≈ Nm to stage · clear sightline" line. On a stageless
3107
- * chart both the distance/sightline claim and the generic stage silhouette are
3186
+ * forward view plus an approximate distance-to-stage line. On a stageless
3187
+ * chart both the distance claim and the generic stage silhouette are
3108
3188
  * invented promises, so we suppress them; a real attached photo always shows.
3109
3189
  * (OV-52)
3110
3190
  */
@@ -3123,7 +3203,7 @@ var SeatPicker = class _SeatPicker {
3123
3203
  return "";
3124
3204
  }
3125
3205
  }
3126
- const sightHtml = hasStage ? `<div class="sl-confirm-sight"><span aria-hidden="true">\u2713</span>${distance != null ? t2("picker.sightline", { m: distance }) : this.tf("picker.sightlineClear", "Clear sightline")}</div>` : "";
3206
+ const sightHtml = hasStage && distance != null ? `<div class="sl-confirm-sight">${t2("picker.sightline", { m: distance })}</div>` : "";
3127
3207
  const viewBtn = realPhoto ? `<button type="button" class="sl-confirm-view sl-confirm-thumbwrap" aria-label="${t2("picker.viewFromSeat", { label: seat.label })}"><img class="sl-confirm-thumb" src="${realPhoto}" alt="" /><span class="sl-confirm-thumb-badge">\u{1F52D} ${this.tf("picker.viewFromHere", "View from here")}</span></button>` : `<button type="button" class="sl-confirm-view sl-confirm-viewbtn" aria-label="${t2("picker.viewFromSeat", { label: seat.label })}"><span aria-hidden="true">\u{1F52D}</span><span>${this.tf("picker.viewFromHere", "View from here")}</span></button>`;
3128
3208
  return viewBtn + sightHtml;
3129
3209
  }
@@ -3257,6 +3337,11 @@ var SeatPicker = class _SeatPicker {
3257
3337
  this.setFsFallback(false);
3258
3338
  }
3259
3339
  }
3340
+ syncFullscreenButtons() {
3341
+ const active = !!document.fullscreenElement || this.fsFallback || this.framedFs;
3342
+ this.els.zfs?.setAttribute("aria-pressed", String(active));
3343
+ this.view3dEl?.querySelector(".sl-view3d-fs")?.setAttribute("aria-pressed", String(active));
3344
+ }
3260
3345
  /**
3261
3346
  * Native element-fullscreen was unavailable or rejected. When framed, a CSS
3262
3347
  * `.sl-fs` overlay can't escape the iframe, so we ask the host page to pin us
@@ -3271,7 +3356,7 @@ var SeatPicker = class _SeatPicker {
3271
3356
  setFramedFs(on) {
3272
3357
  if (this.framedFs === on) return;
3273
3358
  this.framedFs = on;
3274
- this.els.zfs?.setAttribute("aria-pressed", String(on || !!document.fullscreenElement));
3359
+ this.syncFullscreenButtons();
3275
3360
  this.postToHost({ type: "seatlayer:fullscreen", on });
3276
3361
  if (on && !this.fsEscHandler) {
3277
3362
  this.fsEscHandler = (e) => {
@@ -3288,7 +3373,7 @@ var SeatPicker = class _SeatPicker {
3288
3373
  if (this.fsFallback === on) return;
3289
3374
  this.fsFallback = on;
3290
3375
  this.root?.classList.toggle("sl-fs", on);
3291
- this.els.zfs?.setAttribute("aria-pressed", String(on || !!document.fullscreenElement));
3376
+ this.syncFullscreenButtons();
3292
3377
  if (on && !this.fsEscHandler) {
3293
3378
  this.fsEscHandler = (e) => {
3294
3379
  if (e.key === "Escape" && !document.fullscreenElement) this.setFsFallback(false);
@@ -3478,7 +3563,7 @@ var SeatPicker = class _SeatPicker {
3478
3563
  this.els.zfs.addEventListener("click", () => this.toggleFullscreen());
3479
3564
  this.fsChangeHandler = () => {
3480
3565
  if (!document.fullscreenElement) this.setFsFallback(false);
3481
- this.els.zfs?.setAttribute("aria-pressed", String(!!document.fullscreenElement || this.fsFallback || this.framedFs));
3566
+ this.syncFullscreenButtons();
3482
3567
  requestAnimationFrame(() => this.controller.zoomToFit());
3483
3568
  };
3484
3569
  document.addEventListener("fullscreenchange", this.fsChangeHandler);
@@ -3495,10 +3580,21 @@ var SeatPicker = class _SeatPicker {
3495
3580
  e.stopPropagation();
3496
3581
  setSheet(root.dataset.sheet !== "open");
3497
3582
  });
3583
+ this.els.peek?.addEventListener("click", (e) => {
3584
+ const go = e.target.closest(".sl-sheet-go");
3585
+ if (!go) return;
3586
+ e.stopPropagation();
3587
+ if (go.dataset.act === "checkout") void this.handleCta();
3588
+ else setSheet(true);
3589
+ });
3498
3590
  let startY = 0;
3499
3591
  let swiped = false;
3500
3592
  let tracking = false;
3501
3593
  head.addEventListener("pointerdown", (e) => {
3594
+ if (e.target.closest?.(".sl-seccard,.sl-sheet-toggle,.sl-sheet-go")) {
3595
+ tracking = false;
3596
+ return;
3597
+ }
3502
3598
  tracking = true;
3503
3599
  swiped = false;
3504
3600
  startY = e.clientY;
@@ -3517,7 +3613,7 @@ var SeatPicker = class _SeatPicker {
3517
3613
  });
3518
3614
  head.addEventListener("pointerup", (e) => {
3519
3615
  if (tracking && !swiped && Math.abs(e.clientY - startY) < 6) {
3520
- if (!e.target.closest(".sl-seccard,.sl-sheet-toggle")) setSheet(root.dataset.sheet !== "open");
3616
+ setSheet(root.dataset.sheet !== "open");
3521
3617
  }
3522
3618
  tracking = false;
3523
3619
  head.releasePointerCapture?.(e.pointerId);
@@ -3570,7 +3666,7 @@ var SeatPicker = class _SeatPicker {
3570
3666
  if (logoUrl) this.els.logo.innerHTML = `<img src="${logoUrl}" alt="">`;
3571
3667
  else this.els.logo.textContent = (this.opts.theme?.brandName ?? chartTheme?.brandName ?? info.eventName ?? "?").slice(0, 1).toUpperCase();
3572
3668
  this.els.name.textContent = info.eventName ?? "";
3573
- const when = info.startsAt ? new Date(info.startsAt).toLocaleString(this.opts.locale, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "";
3669
+ const when = info.startsAt ? formatWhen(info.startsAt, info.timezone ?? null, this.opts.locale) : "";
3574
3670
  this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
3575
3671
  this.buildBadge(chartTheme);
3576
3672
  const present = /* @__PURE__ */ new Set();
@@ -3711,12 +3807,11 @@ var SeatPicker = class _SeatPicker {
3711
3807
  const narrow = this.root?.dataset.layout === "narrow";
3712
3808
  const filters = this.els.filters;
3713
3809
  if (filters) {
3810
+ if (this.cbEl) this.els.zoom?.appendChild(this.cbEl);
3714
3811
  if (narrow) {
3715
3812
  if (this.a11yChipsEl) filters.appendChild(this.a11yChipsEl);
3716
- if (this.cbEl) filters.appendChild(this.cbEl);
3717
3813
  } else {
3718
3814
  if (this.a11yChipsEl) this.regions["top-left"]?.appendChild(this.a11yChipsEl);
3719
- if (this.cbEl) this.els.zoom?.appendChild(this.cbEl);
3720
3815
  }
3721
3816
  const has = narrow && filters.children.length > 0;
3722
3817
  filters.classList.toggle("has", has);
@@ -3823,8 +3918,12 @@ var SeatPicker = class _SeatPicker {
3823
3918
  if (this.badgeHidden(chartTheme)) return;
3824
3919
  const foot = this.els.foot;
3825
3920
  if (!foot) return;
3826
- const el = document.createElement("div");
3921
+ const el = document.createElement("a");
3827
3922
  el.className = "sl-powered";
3923
+ el.href = "https://seatlayer.io/?ref=picker";
3924
+ el.target = "_blank";
3925
+ el.rel = "noopener noreferrer";
3926
+ el.setAttribute("aria-label", this.tf("picker.poweredBy", "Powered by SeatLayer"));
3828
3927
  el.innerHTML = `<span class="sl-powered-mark" aria-hidden="true">` + SEATLAYER_ATTRIBUTION_MARK_SVG + `</span><span>${this.tf("picker.poweredBy", "Powered by SeatLayer")}</span>`;
3829
3928
  foot.appendChild(el);
3830
3929
  }
@@ -4683,6 +4782,7 @@ var SeatPicker = class _SeatPicker {
4683
4782
  el.querySelector(".sl-confirm-view")?.addEventListener("click", () => void this.openSeatView(seat));
4684
4783
  el.querySelector(".sl-confirm-3d")?.addEventListener("click", () => {
4685
4784
  if (this.buyerView === "venue3d") {
4785
+ this.dismissConfirm();
4686
4786
  void this.view3dHandle?.flyToSeat(seat.id);
4687
4787
  } else {
4688
4788
  this.view3dReturnSeat = seat;
@@ -4761,13 +4861,24 @@ var SeatPicker = class _SeatPicker {
4761
4861
  const doc = this.controller.doc;
4762
4862
  const activeId = this.controller.getActiveFloorId();
4763
4863
  const focal = seat.focalPoint ?? doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
4764
- let panoUrl;
4864
+ let panoSource;
4765
4865
  let caption;
4766
4866
  let real = false;
4767
4867
  if (seat.viewUrl) {
4768
- panoUrl = seat.viewUrl;
4769
- caption = t2("picker.panorama360");
4770
- real = true;
4868
+ const view = {
4869
+ url: seat.viewUrl,
4870
+ ...seat.viewMeta?.previewUrl ? { previewUrl: seat.viewMeta.previewUrl } : {},
4871
+ ...seat.viewMeta?.sourceWidth !== void 0 ? { sourceWidth: seat.viewMeta.sourceWidth } : {},
4872
+ ...seat.viewMeta?.sourceHeight !== void 0 ? { sourceHeight: seat.viewMeta.sourceHeight } : {},
4873
+ ...seat.viewMeta?.previewWidth !== void 0 ? { previewWidth: seat.viewMeta.previewWidth } : {},
4874
+ ...seat.viewMeta?.previewHeight !== void 0 ? { previewHeight: seat.viewMeta.previewHeight } : {},
4875
+ ...seat.viewMeta?.coverage ? { coverage: seat.viewMeta.coverage } : {},
4876
+ ...seat.viewMeta?.capturedAt ? { capturedAt: seat.viewMeta.capturedAt } : {},
4877
+ ...seat.viewMeta?.sourceLabel ? { sourceLabel: seat.viewMeta.sourceLabel } : {}
4878
+ };
4879
+ panoSource = view;
4880
+ caption = seatViewDisclosure(view);
4881
+ real = isAuthoredSeatView(view);
4771
4882
  } else {
4772
4883
  let pano2;
4773
4884
  try {
@@ -4778,7 +4889,7 @@ var SeatPicker = class _SeatPicker {
4778
4889
  return;
4779
4890
  }
4780
4891
  if (!this.root || !this.seatViewEnabled()) return;
4781
- panoUrl = pano2.url;
4892
+ panoSource = { url: pano2.url, generated: true };
4782
4893
  caption = t2("picker.illustrationCaption", { m: pano2.distanceM });
4783
4894
  }
4784
4895
  this.closeSeatView();
@@ -4790,7 +4901,20 @@ var SeatPicker = class _SeatPicker {
4790
4901
  this.root.appendChild(el);
4791
4902
  this.viewEl = el;
4792
4903
  const pano = el.querySelector(".sl-view-pano");
4793
- pano.style.backgroundImage = `url("${panoUrl}")`;
4904
+ const delivery = planPanoramaDelivery(panoSource, browserPanoramaConstraints());
4905
+ const loadAbort = new AbortController();
4906
+ pano.style.backgroundImage = `url("${delivery.initialUrl}")`;
4907
+ let cancelUpgrade = () => {
4908
+ };
4909
+ if (delivery.upgradeUrl) {
4910
+ cancelUpgrade = schedulePanoramaUpgrade(() => {
4911
+ void loadPanoramaImage(delivery.upgradeUrl, loadAbort.signal).then(() => {
4912
+ if (!el.isConnected || loadAbort.signal.aborted) return;
4913
+ pano.style.backgroundImage = `url("${delivery.upgradeUrl}")`;
4914
+ }).catch(() => {
4915
+ });
4916
+ });
4917
+ }
4794
4918
  const VFOV_DEG = 70;
4795
4919
  const MAX_PITCH_DEG = 35;
4796
4920
  let zoom = 1;
@@ -4852,6 +4976,8 @@ var SeatPicker = class _SeatPicker {
4852
4976
  el.addEventListener("keydown", onKey);
4853
4977
  closeBtn.focus();
4854
4978
  this.viewCleanup = () => {
4979
+ cancelUpgrade();
4980
+ loadAbort.abort();
4855
4981
  pano.removeEventListener("pointerdown", onDown);
4856
4982
  pano.removeEventListener("pointermove", onMove);
4857
4983
  pano.removeEventListener("pointerup", onUp);
@@ -5216,10 +5342,11 @@ var SeatPicker = class _SeatPicker {
5216
5342
  if (previousCount === 0 && count > 0) this.animateOnce(this.els.cta, "sl-ready", 520);
5217
5343
  if (this.els.peek) {
5218
5344
  if (count) {
5219
- this.els.peek.innerHTML = `<span>${count} ${count === 1 ? "ticket" : "tickets"} \xB7 ${this.money(total)}</span><span class="go">${this.hold ? pendingCount ? "Secure more" : "Continue" : "Review"}</span>`;
5345
+ const holding = !!this.hold && !pendingCount;
5346
+ this.els.peek.innerHTML = `<span>${count} ${count === 1 ? "ticket" : "tickets"} \xB7 ${this.money(total)}</span><button type="button" class="go sl-sheet-go" data-act="${holding ? "checkout" : "open"}">${this.hold ? pendingCount ? "Secure more" : "Continue" : "Review"}</button>`;
5220
5347
  } else {
5221
5348
  const prices = (this.controller.doc?.categories ?? []).map((c) => this.catPrice(c)).filter((p) => p != null);
5222
- this.els.peek.innerHTML = (prices.length ? `<span>From ${this.money(Math.min(...prices))}</span>` : "<span>Pick your seats</span>") + `<span class="go">\u2726 Best seats</span>`;
5349
+ this.els.peek.innerHTML = (prices.length ? `<span>From ${this.money(Math.min(...prices))}</span>` : "<span>Pick your seats</span>") + `<button type="button" class="go sl-sheet-go" data-act="open">\u2726 Best seats</button>`;
5223
5350
  }
5224
5351
  }
5225
5352
  this.lastTrayCount = count;
@@ -5655,6 +5782,58 @@ var SeatPicker = class _SeatPicker {
5655
5782
  getSelection() {
5656
5783
  return this.committedSelection();
5657
5784
  }
5785
+ /**
5786
+ * Re-ink the DRAWN MAP after mount, so the canvas can follow a page palette
5787
+ * the host did not know at construction time.
5788
+ *
5789
+ * Deliberately narrower than the `theme` option: it takes only the map half.
5790
+ * The chrome half is CSS custom properties, which a host restyles from its
5791
+ * own stylesheet without asking the widget for anything — and a host that
5792
+ * used both mechanisms at once would have two things writing one token. This
5793
+ * method exists for the half CSS genuinely cannot reach: pixels Konva draws.
5794
+ *
5795
+ * The rebuild is the controller's (`setMapTheme`), which repaints statuses
5796
+ * from the map already in memory and restores the selection, so a buyer
5797
+ * mid-pick keeps their seats and no round trip is spent. A no-op when the
5798
+ * colours have not changed; safe to call on every render.
5799
+ */
5800
+ setMapTheme(map) {
5801
+ if (this.destroyed) return;
5802
+ this.opts.theme = { ...this.opts.theme ?? {}, map: map ?? void 0 };
5803
+ this.controller.setMapTheme(map);
5804
+ }
5805
+ /**
5806
+ * Replace the host pricing override AFTER mount, and repaint everything that
5807
+ * shows a price.
5808
+ *
5809
+ * WHY IT CANNOT JUST BE A MOUNT OPTION. The prices a host knows are often not
5810
+ * the prices it knows AT MOUNT: SeatLayer's own event page learns them from
5811
+ * `GET /pub/events/:key/availability`, a separate cached read that lands a
5812
+ * round trip after the map does — deliberately, because the map is not
5813
+ * allowed to wait on it. Remounting the widget to hand it new options would
5814
+ * tear down a live hold, so the override is settable in place.
5815
+ *
5816
+ * The prices themselves are still the SERVER'S. This method takes an answer;
5817
+ * it never derives one. Nothing here re-reads a release, a window or a quota
5818
+ * — see `paidPrice`, the one funnel every displayed price flows through, and
5819
+ * note that the checkout handoff's line items do not come from here at all:
5820
+ * they are priced by the worker from its own hold. So the worst a wrong call
5821
+ * to this method can do is misprint a price, never mischarge one.
5822
+ *
5823
+ * A no-op when the map is unchanged, so a host may call it on every poll.
5824
+ */
5825
+ setPricing(pricing) {
5826
+ const before = JSON.stringify(this.opts.pricing ?? null);
5827
+ const after = JSON.stringify(pricing ?? null);
5828
+ if (before === after) return;
5829
+ this.opts.pricing = pricing;
5830
+ if (this.destroyed || !this.els.prices) return;
5831
+ this.els.pricesSec?.querySelector(".sl-price-select")?.remove();
5832
+ this.buildPriceFilter();
5833
+ this.syncPrices();
5834
+ this.syncTray();
5835
+ if (this.lastSection) this.showSectionCard(this.lastSection);
5836
+ }
5658
5837
  /** Current colorblind-safe render state, resolved from the stored buyer
5659
5838
  * preference at mount. Host chrome (e.g. the Designer preview) reads this to
5660
5839
  * surface the state rather than rendering colorblind colors silently. */
@@ -5706,10 +5885,7 @@ var SeatPicker = class _SeatPicker {
5706
5885
  *
5707
5886
  * Entering `'venue3d'` with `opts.flyToSeatId` runs the cinematic tour: the
5708
5887
  * camera flies to the seat and holds in the live scene (a chip offers the
5709
- * 360° view-from-seat)
5710
- * the widget enters 3D and auto-flies the camera to that seat, dissolving into
5711
- * its view-from-seat panorama (the same chain as tapping "See the view from
5712
- * here" on a seat's confirm card). When the widget is **already** in the 3D
5888
+ * 360° view-from-seat). When the widget is **already** in the 3D
5713
5889
  * view, the camera simply flies to the requested seat — the GL scene is not
5714
5890
  * torn down or rebuilt, so there is no flash or re-entry.
5715
5891
  *
@@ -5718,7 +5894,7 @@ var SeatPicker = class _SeatPicker {
5718
5894
  *
5719
5895
  * @param view `'map'` for the flat picker, `'venue3d'` for the 3D venue.
5720
5896
  * @param opts.flyToSeatId When entering (or already in) `'venue3d'`, the seat
5721
- * id to fly the camera to — cinematic → panorama.
5897
+ * id to fly the camera to — cinematic → live seat view.
5722
5898
  * Ignored when `view` is `'map'`.
5723
5899
  *
5724
5900
  * @example
@@ -5732,7 +5908,12 @@ var SeatPicker = class _SeatPicker {
5732
5908
  }
5733
5909
  const flyToSeatId = opts?.flyToSeatId;
5734
5910
  if (this.buyerView === "venue3d") {
5735
- if (flyToSeatId) void this.view3dHandle?.flyToSeat(flyToSeatId);
5911
+ if (flyToSeatId) {
5912
+ this.opts.onBuyerViewChange?.({ view: "venue3d", seatId: flyToSeatId });
5913
+ void this.view3dHandle?.flyToSeat(flyToSeatId);
5914
+ } else if (opts?.resetView) {
5915
+ this.view3dHandle?.focusOverview();
5916
+ }
5736
5917
  return;
5737
5918
  }
5738
5919
  void this.enter3d(flyToSeatId);
@@ -5772,14 +5953,26 @@ var SeatPicker = class _SeatPicker {
5772
5953
  async seatViewFor3d(seatId) {
5773
5954
  const seat = this.allSeats().find((s) => s.id === seatId);
5774
5955
  if (!seat) return null;
5775
- if (seat.viewUrl) return { url: seat.viewUrl };
5956
+ if (seat.viewUrl) return {
5957
+ url: seat.viewUrl,
5958
+ ...seat.viewMeta?.previewUrl ? { previewUrl: seat.viewMeta.previewUrl } : {},
5959
+ ...seat.viewMeta?.sourceWidth !== void 0 ? { sourceWidth: seat.viewMeta.sourceWidth } : {},
5960
+ ...seat.viewMeta?.sourceHeight !== void 0 ? { sourceHeight: seat.viewMeta.sourceHeight } : {},
5961
+ ...seat.viewMeta?.previewWidth !== void 0 ? { previewWidth: seat.viewMeta.previewWidth } : {},
5962
+ ...seat.viewMeta?.previewHeight !== void 0 ? { previewHeight: seat.viewMeta.previewHeight } : {},
5963
+ ...seat.viewMeta?.initialBearingDeg !== void 0 ? { initialBearingDeg: seat.viewMeta.initialBearingDeg } : {},
5964
+ ...seat.viewMeta?.initialPitchDeg !== void 0 ? { initialPitchDeg: seat.viewMeta.initialPitchDeg } : {},
5965
+ ...seat.viewMeta?.coverage ? { coverage: seat.viewMeta.coverage } : {},
5966
+ ...seat.viewMeta?.capturedAt ? { capturedAt: seat.viewMeta.capturedAt } : {},
5967
+ ...seat.viewMeta?.sourceLabel ? { sourceLabel: seat.viewMeta.sourceLabel } : {}
5968
+ };
5776
5969
  const doc = this.controller.doc;
5777
5970
  if (!doc) return null;
5778
5971
  const activeId = this.controller.getActiveFloorId();
5779
5972
  const focal = seat.focalPoint ?? doc.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc.focalPoint ?? { x: 0, y: 0 };
5780
5973
  try {
5781
5974
  const { generateSeatPanorama } = await loadPanorama();
5782
- return { url: generateSeatPanorama(seat, focal, this.allSeats()).url };
5975
+ return { url: generateSeatPanorama(seat, focal, this.allSeats()).url, generated: true };
5783
5976
  } catch {
5784
5977
  return null;
5785
5978
  }
@@ -5801,14 +5994,26 @@ var SeatPicker = class _SeatPicker {
5801
5994
  }
5802
5995
  const seat = this.allSeats().find((s) => s.id === seatId);
5803
5996
  if (!seat) return;
5804
- const already = this.committedSelection().some((s) => s.id === seatId);
5997
+ const table = this.controller.tableSelection(seatId);
5998
+ const already = this.committedSelection().some((s) => table ? s.label === table.label : s.id === seatId);
5805
5999
  if (already) {
5806
6000
  this.controller.deselect([seatId]);
5807
6001
  this.dismissConfirm();
5808
6002
  return;
5809
6003
  }
6004
+ const added = this.controller.select([seatId]);
6005
+ if (!added.length) {
6006
+ this.syncSelectionTo3d();
6007
+ this.dismissConfirm();
6008
+ return;
6009
+ }
6010
+ this.opts.onBuyerViewChange?.({ view: "venue3d", seatId });
5810
6011
  this.flashPickedSeat(seatId);
5811
- this.controller.select([seatId]);
6012
+ if (table) {
6013
+ this.showTableDialog(table, false);
6014
+ this.syncSelectionTo3d();
6015
+ return;
6016
+ }
5812
6017
  if (this.opts.confirmSelection !== false) this.showConfirm(seat);
5813
6018
  else this.syncTray();
5814
6019
  }
@@ -5828,12 +6033,15 @@ var SeatPicker = class _SeatPicker {
5828
6033
  */
5829
6034
  buildView3dNav(overlay, handle) {
5830
6035
  const floors = handle.floors();
5831
- const zones = handle.zones().filter((z) => z.seatCount > 0);
5832
- const sections = zones.length > 1 ? [] : handle.sections().filter((s) => s.seatCount > 0).slice(0, MAX_3D_SECTION_PILLS);
6036
+ const floorLabels = new Set(floors.map((floor) => floor.label.trim().toLocaleLowerCase()));
6037
+ const zones = handle.zones().filter((zone) => zone.seatCount > 0 && !floorLabels.has(zone.label.trim().toLocaleLowerCase()));
6038
+ const allSections = handle.sections().filter((s) => s.seatCount > 0);
6039
+ const sections = zones.length > 1 ? [] : allSections;
5833
6040
  const wantFloors = floors.length > 1;
5834
6041
  const wantZones = zones.length > 1;
5835
6042
  const wantSections = sections.length > 1;
5836
- if (!wantFloors && !wantZones && !wantSections) return;
6043
+ const wantLocator = allSections.length > 0 && handle.rows().length > 0;
6044
+ if (!wantFloors && !wantZones && !wantSections && !wantLocator) return;
5837
6045
  const nav = document.createElement("div");
5838
6046
  nav.className = "sl-view3d-nav";
5839
6047
  if (wantFloors) {
@@ -5870,15 +6078,112 @@ var SeatPicker = class _SeatPicker {
5870
6078
  } })) : sections.map((s) => ({ id: s.id, label: s.label || s.id, go: () => {
5871
6079
  handle.focusSection(s.id);
5872
6080
  } }));
5873
- for (const e of entries) {
5874
- const b = document.createElement("button");
5875
- b.type = "button";
5876
- b.textContent = e.label;
5877
- b.addEventListener("click", e.go);
5878
- row.appendChild(b);
6081
+ if (!wantZones && entries.length > MAX_3D_SECTION_PILLS) {
6082
+ const select = document.createElement("select");
6083
+ select.setAttribute("aria-label", this.tf("picker.jumpToSection", "Jump to section"));
6084
+ const placeholder = document.createElement("option");
6085
+ placeholder.value = "";
6086
+ placeholder.textContent = this.tf("picker.jumpToSection", "Jump to section");
6087
+ select.appendChild(placeholder);
6088
+ for (const entry of entries) {
6089
+ const option = document.createElement("option");
6090
+ option.value = entry.id;
6091
+ option.textContent = entry.label;
6092
+ select.appendChild(option);
6093
+ }
6094
+ select.addEventListener("change", () => {
6095
+ entries.find((entry) => entry.id === select.value)?.go();
6096
+ });
6097
+ row.appendChild(select);
6098
+ } else {
6099
+ for (const e of entries) {
6100
+ const b = document.createElement("button");
6101
+ b.type = "button";
6102
+ b.textContent = e.label;
6103
+ b.addEventListener("click", e.go);
6104
+ row.appendChild(b);
6105
+ }
5879
6106
  }
5880
6107
  nav.appendChild(row);
5881
6108
  }
6109
+ if (wantLocator) {
6110
+ const locator = document.createElement("div");
6111
+ locator.className = "sl-view3d-locator";
6112
+ locator.setAttribute("role", "group");
6113
+ locator.setAttribute("aria-label", "Find an exact seat in 3D");
6114
+ const sectionSelect = document.createElement("select");
6115
+ sectionSelect.setAttribute("aria-label", "Choose section in 3D");
6116
+ const rowSelect = document.createElement("select");
6117
+ rowSelect.setAttribute("aria-label", "Choose row in 3D");
6118
+ const seatSelect = document.createElement("select");
6119
+ seatSelect.setAttribute("aria-label", "Choose seat in 3D");
6120
+ const view = document.createElement("button");
6121
+ view.type = "button";
6122
+ view.textContent = "View seat";
6123
+ view.disabled = true;
6124
+ const fill = (select, placeholder, entries) => {
6125
+ select.replaceChildren();
6126
+ const first = document.createElement("option");
6127
+ first.value = "";
6128
+ first.textContent = placeholder;
6129
+ select.appendChild(first);
6130
+ for (const entry of entries) {
6131
+ const option = document.createElement("option");
6132
+ option.value = entry.id;
6133
+ option.textContent = entry.label;
6134
+ select.appendChild(option);
6135
+ }
6136
+ select.value = "";
6137
+ };
6138
+ fill(sectionSelect, "1. Section", allSections.map((section) => ({
6139
+ id: section.id,
6140
+ label: `${section.label} \xB7 ${section.seatCount.toLocaleString()} seats`
6141
+ })));
6142
+ fill(rowSelect, "2. Row", []);
6143
+ fill(seatSelect, "3. Seat", []);
6144
+ rowSelect.disabled = true;
6145
+ seatSelect.disabled = true;
6146
+ sectionSelect.addEventListener("change", () => {
6147
+ const sectionId = sectionSelect.value;
6148
+ view.disabled = true;
6149
+ fill(seatSelect, "3. Seat", []);
6150
+ seatSelect.disabled = true;
6151
+ if (!sectionId || !handle.focusSection(sectionId)) {
6152
+ fill(rowSelect, "2. Row", []);
6153
+ rowSelect.disabled = true;
6154
+ return;
6155
+ }
6156
+ const rows = handle.rows(sectionId);
6157
+ fill(rowSelect, "2. Row", rows.map((row) => ({
6158
+ id: row.id,
6159
+ label: `${row.label} \xB7 ${row.seatCount} seats`
6160
+ })));
6161
+ rowSelect.disabled = rows.length === 0;
6162
+ });
6163
+ rowSelect.addEventListener("change", () => {
6164
+ const rowId = rowSelect.value;
6165
+ view.disabled = true;
6166
+ if (!rowId || !handle.focusRow(rowId)) {
6167
+ fill(seatSelect, "3. Seat", []);
6168
+ seatSelect.disabled = true;
6169
+ return;
6170
+ }
6171
+ const seats = handle.seatsInRow(rowId);
6172
+ fill(seatSelect, "3. Seat", seats);
6173
+ seatSelect.disabled = seats.length === 0;
6174
+ });
6175
+ seatSelect.addEventListener("change", () => {
6176
+ const seatId = seatSelect.value;
6177
+ view.disabled = !seatId;
6178
+ handle.setSelection(seatId ? [seatId] : []);
6179
+ });
6180
+ view.addEventListener("click", () => {
6181
+ const seatId = seatSelect.value;
6182
+ if (seatId) void handle.flyToSeat(seatId);
6183
+ });
6184
+ locator.append(sectionSelect, rowSelect, seatSelect, view);
6185
+ nav.appendChild(locator);
6186
+ }
5882
6187
  overlay.appendChild(nav);
5883
6188
  }
5884
6189
  async enter3d(flySeatId) {
@@ -5886,6 +6191,7 @@ var SeatPicker = class _SeatPicker {
5886
6191
  const doc = this.controller.doc;
5887
6192
  if (!doc) return;
5888
6193
  this.buyerView = "venue3d";
6194
+ this.opts.onBuyerViewChange?.({ view: "venue3d", ...flySeatId ? { seatId: flySeatId } : {} });
5889
6195
  this.root?.setAttribute("data-view3d", "on");
5890
6196
  this.dismissConfirm();
5891
6197
  this.syncProjection();
@@ -5899,6 +6205,20 @@ var SeatPicker = class _SeatPicker {
5899
6205
  back.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg><span>${this.tf("picker.backToMap", "Back to map")}</span>`;
5900
6206
  back.addEventListener("click", () => this.exit3d());
5901
6207
  overlay.appendChild(back);
6208
+ const fullscreen = document.createElement("button");
6209
+ fullscreen.type = "button";
6210
+ fullscreen.className = "sl-view3d-fs";
6211
+ fullscreen.textContent = "\u26F6";
6212
+ fullscreen.setAttribute("aria-label", "Full screen");
6213
+ fullscreen.setAttribute("aria-pressed", String(!!document.fullscreenElement || this.fsFallback || this.framedFs));
6214
+ fullscreen.addEventListener("click", () => this.toggleFullscreen());
6215
+ overlay.appendChild(fullscreen);
6216
+ const loading = document.createElement("div");
6217
+ loading.className = "sl-view3d-loading";
6218
+ loading.setAttribute("role", "status");
6219
+ loading.setAttribute("aria-live", "polite");
6220
+ loading.textContent = this.tf("picker.loading3d", "Building the 3D venue\u2026");
6221
+ overlay.appendChild(loading);
5902
6222
  this.els.map.appendChild(overlay);
5903
6223
  this.view3dEl = overlay;
5904
6224
  requestAnimationFrame(() => {
@@ -5909,8 +6229,30 @@ var SeatPicker = class _SeatPicker {
5909
6229
  const seats = expandChart(doc);
5910
6230
  const mod = await loadVenue3d();
5911
6231
  if (gen !== this.view3dGen || this.buyerView !== "venue3d" || this.view3dEl !== overlay) return;
5912
- const handle = mod.mountVenue3D(overlay, { doc, seats }, {
6232
+ const prepared = await mod.prepareVenue3D({ doc, seats });
6233
+ if (gen !== this.view3dGen || this.buyerView !== "venue3d" || this.view3dEl !== overlay) return;
6234
+ const handle = mod.mountVenue3D(overlay, { doc, seats, prepared }, {
6235
+ // A strict contain fit turns a wide bowl into a postage stamp inside a
6236
+ // phone. The bounded portrait fit was validated against every UX-lab
6237
+ // fixture; keep editor previews strict while making buyer seats legible.
6238
+ portraitOverviewCrop: true,
5913
6239
  onSeatPick: (id) => this.onView3dSeatPick(id),
6240
+ onSectionFocusChange: (sectionId) => {
6241
+ const select = overlay.querySelector(
6242
+ 'select[aria-label="Choose section in 3D"]'
6243
+ );
6244
+ const next = sectionId ?? "";
6245
+ if (!select || select.value === next) return;
6246
+ select.value = next;
6247
+ select.dispatchEvent(new Event("change"));
6248
+ },
6249
+ onViewTargetChange: (seatId) => {
6250
+ overlay.querySelector(".sl-view3d-nav")?.classList.toggle("is-seat-focused", !!seatId);
6251
+ this.opts.onBuyerViewChange?.({
6252
+ view: "venue3d",
6253
+ ...seatId ? { seatId } : {}
6254
+ });
6255
+ },
5914
6256
  // Deferred off the tap gesture: generateSeatPanorama walks every seat
5915
6257
  // (O(n) on a 13k chart), and the module prefetches at pick — by the
5916
6258
  // time the flight lands (~2.5s) the idle render has long finished. A
@@ -5930,12 +6272,15 @@ var SeatPicker = class _SeatPicker {
5930
6272
  onAnalytics: (event, props) => this.emit3dAnalytics(event, props)
5931
6273
  });
5932
6274
  this.view3dHandle = handle;
6275
+ loading.remove();
5933
6276
  this.pushAvailabilityTo3d();
5934
6277
  this.syncSelectionTo3d();
5935
6278
  this.buildView3dNav(overlay, handle);
5936
6279
  if (flySeatId) void handle.flyToSeat(flySeatId);
5937
6280
  } catch (err) {
6281
+ if (gen !== this.view3dGen || this.buyerView !== "venue3d" || this.view3dEl !== overlay) return;
5938
6282
  this.opts.onError?.(err);
6283
+ this.toast(this.tf("picker.unavailable3d", "3D could not start. The seat map is still available."), "warning");
5939
6284
  this.exit3d();
5940
6285
  }
5941
6286
  }
@@ -5943,6 +6288,7 @@ var SeatPicker = class _SeatPicker {
5943
6288
  if (this.buyerView !== "venue3d" && !this.view3dEl) return;
5944
6289
  this.view3dGen++;
5945
6290
  this.buyerView = "map";
6291
+ this.opts.onBuyerViewChange?.({ view: "map" });
5946
6292
  this.root?.removeAttribute("data-view3d");
5947
6293
  try {
5948
6294
  this.view3dHandle?.dispose();