@seatlayer/js 0.46.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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`, {
@@ -1981,10 +1985,17 @@ import {
1981
1985
  t as t2,
1982
1986
  tCount
1983
1987
  } from "@seatlayer/core";
1988
+ import { isAuthoredSeatView, seatViewDisclosure } from "@seatlayer/core/view3d/crossfade/panorama";
1989
+ import {
1990
+ browserPanoramaConstraints,
1991
+ loadPanoramaImage,
1992
+ planPanoramaDelivery,
1993
+ schedulePanoramaUpgrade
1994
+ } from "@seatlayer/core/view/panoramaDelivery";
1984
1995
  var DEFAULT_API_BASE2 = "https://api.seatlayer.io";
1985
1996
  var DEFAULT_MAX_SELECTION2 = 10;
1986
1997
  var EXTEND_PROMPT_MS = 6e4;
1987
- var MAX_3D_SEATS_DEFAULT = 15e3;
1998
+ var MAX_3D_SEATS_DEFAULT = 6e4;
1988
1999
  var MAX_3D_SECTION_PILLS = 12;
1989
2000
  function pointInPolygon(x, y, poly) {
1990
2001
  let inside = false;
@@ -2067,7 +2078,7 @@ async function loadHostedCheckout() {
2067
2078
  cdnChunkUrl("seatlayer-checkout.mjs")
2068
2079
  );
2069
2080
  }
2070
- return import("./hostedCheckout-PLHKPT6W.js");
2081
+ return import("./hostedCheckout-SHQCWN5Y.js");
2071
2082
  }
2072
2083
  function paymentsOffReason(reason) {
2073
2084
  return reason === "unavailable_for_event" || reason === "payments_off_for_event" ? reason : "not_configured";
@@ -2328,7 +2339,12 @@ var CSS = (
2328
2339
  INTO one of these positioned flex containers and flows/stacks within it, so no
2329
2340
  two pieces of chrome free-float on top of each other. Regions never overlap:
2330
2341
  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}
2342
+ /* align-items:center, not the flex default of stretch. Without it a short pill
2343
+ beside a taller control (TEST MODE next to the Map/3D toggle) is stretched to
2344
+ the row's height, so two pills that should read as a matched pair end up
2345
+ different shapes on different baselines. Column regions set their own
2346
+ cross-axis alignment below and are unaffected. */
2347
+ .sl-anchor{position:absolute;z-index:5;display:flex;align-items:center;gap:8px;pointer-events:none}
2332
2348
  .sl-anchor > *{pointer-events:auto}
2333
2349
  .sl-anchor[data-region="top-left"]{top:12px;left:12px;flex-wrap:wrap;max-width:38%}
2334
2350
  .sl-anchor[data-region="top-center"]{top:12px;left:50%;transform:translateX(-50%);flex-direction:column;
@@ -2344,10 +2360,17 @@ var CSS = (
2344
2360
  .sl-picker[data-layout="narrow"] .sl-anchor[data-region="top-center"]{max-width:44%}
2345
2361
 
2346
2362
  /* 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;
2363
+ /* align-self:stretch, not a hardcoded height: the badge sits beside the Map/3D
2364
+ toggle, whose height comes from its own border + padding + button metrics.
2365
+ Stretching matches that row exactly and keeps matching if the toggle ever
2366
+ changes, while the badge's own inline-flex keeps the label centred inside
2367
+ whatever height it gets. Alone in the region it simply takes its natural
2368
+ size. */
2369
+ .sl-testbadge{display:inline-flex;align-items:center;align-self:stretch;padding:0 12px;border-radius:999px;
2370
+ font-size:10px;font-weight:800;letter-spacing:.1em;line-height:1;
2348
2371
  text-transform:uppercase;white-space:nowrap;background:var(--sl-accent);color:var(--sl-accent-ink);
2349
2372
  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}
2373
+ .sl-picker[data-layout="narrow"] .sl-testbadge{padding:0 8px;font-size:8.5px;letter-spacing:.06em}
2351
2374
 
2352
2375
  /* zoom column (flows within the bottom-right region) */
2353
2376
  .sl-zoom{display:flex;flex-direction:column;gap:6px}
@@ -2439,7 +2462,11 @@ var CSS = (
2439
2462
  Layered Rows mark + wordmark. Hidden when the host opts out or the org's paid
2440
2463
  theme sets hideBadge. */
2441
2464
  .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)}
2465
+ font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--sl-text);opacity:.72;
2466
+ text-decoration:none;padding:6px 10px;border-radius:999px;width:fit-content;margin-inline:auto;
2467
+ transition:opacity .15s ease,background-color .15s ease}
2468
+ .sl-powered:hover{opacity:1;background:color-mix(in srgb,var(--sl-text) 8%,transparent)}
2469
+ .sl-powered:focus-visible{opacity:1;outline:2px solid var(--sl-accent);outline-offset:2px}
2443
2470
  .sl-powered-mark{width:16px;height:16px;border-radius:4px;flex:none;display:flex;align-items:center;justify-content:center;
2444
2471
  background:#0c1220;color:#fcf7ee}
2445
2472
  .sl-powered-mark svg{width:12px;height:11px}
@@ -2618,18 +2645,27 @@ var CSS = (
2618
2645
  .sl-view3d{position:absolute;inset:0;z-index:4;opacity:0;touch-action:none;
2619
2646
  transition:opacity .3s ease;background:radial-gradient(120% 120% at 50% 0%,#191f28 0%,#0d1014 70%)}
2620
2647
  .sl-view3d canvas{display:block;width:100%;height:100%}
2648
+ .sl-view3d canvas:focus-visible{outline:2px solid var(--sl-accent);outline-offset:-3px}
2649
+ .sl-view3d-loading{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:10px;
2650
+ z-index:1;color:#d9e2f2;font-size:13px;font-weight:700;letter-spacing:.02em;pointer-events:none}
2651
+ .sl-view3d-loading::before{content:"";width:18px;height:18px;border-radius:50%;
2652
+ border:2px solid rgba(217,226,242,.28);border-top-color:#d9e2f2;animation:slSpin .8s linear infinite}
2621
2653
  [data-view3d=on] .sl-chips,[data-view3d=on] .sl-rungs{display:none}
2622
2654
  .sl-view3d-back{position:absolute;top:12px;left:12px;z-index:2;display:inline-flex;align-items:center;gap:6px;
2623
2655
  padding:7px 13px 7px 9px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.03em;
2624
2656
  color:#e6edf3;background:rgba(10,14,20,.62);border:1px solid rgba(255,255,255,.22);backdrop-filter:blur(6px)}
2625
2657
  .sl-view3d-back:hover,.sl-view3d-back:focus-visible{background:rgba(16,22,30,.82);border-color:rgba(255,255,255,.4)}
2626
2658
  .sl-view3d-back svg{width:15px;height:15px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round;stroke-linejoin:round}
2659
+ .sl-view3d-fs{position:absolute;top:12px;right:12px;z-index:2;min-width:44px;min-height:44px;padding:8px 12px;
2660
+ border-radius:999px;font-size:16px;color:#e6edf3;background:rgba(10,14,20,.62);
2661
+ border:1px solid rgba(255,255,255,.22);backdrop-filter:blur(6px)}
2662
+ .sl-view3d-fs:hover,.sl-view3d-fs:focus-visible{background:rgba(16,22,30,.82);border-color:rgba(255,255,255,.4)}
2627
2663
  /* Venue navigation inside 3D: levels (isolate a floor) and areas (fly to a zone).
2628
2664
  Bottom-LEFT, clear of the module's own chips (Overview bottom-right, 360
2629
2665
  bottom-centre) and of the bottom-sheeted confirm card. Horizontally scrollable
2630
2666
  so a venue with many zones never pushes the rail off a phone screen. */
2631
2667
  .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}
2668
+ max-width:calc(100% - 150px);pointer-events:none}
2633
2669
  .sl-view3d-nav > div{display:flex;gap:6px;overflow-x:auto;scrollbar-width:none;pointer-events:auto;
2634
2670
  padding:1px;-webkit-overflow-scrolling:touch}
2635
2671
  .sl-view3d-nav > div::-webkit-scrollbar{display:none}
@@ -2638,6 +2674,21 @@ var CSS = (
2638
2674
  border:1px solid rgba(150,165,205,.35);backdrop-filter:blur(6px);cursor:pointer}
2639
2675
  .sl-view3d-nav button:hover,.sl-view3d-nav button:focus-visible{color:#eef1f8;border-color:rgba(190,205,240,.6)}
2640
2676
  .sl-view3d-nav button[aria-pressed="true"]{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
2677
+ .sl-view3d-nav button:disabled{opacity:.45;cursor:not-allowed}
2678
+ .sl-view3d-nav select{min-height:38px;max-width:min(280px,calc(100vw - 40px));padding:7px 34px 7px 12px;
2679
+ border-radius:999px;font:700 11.5px/1 inherit;color:#eef1f8;background:rgba(12,18,32,.86);
2680
+ border:1px solid rgba(150,165,205,.45);backdrop-filter:blur(6px);cursor:pointer}
2681
+ .sl-view3d-nav select:focus-visible{outline:2px solid var(--sl-accent);outline-offset:2px}
2682
+ .sl-view3d-nav .sl-view3d-locator{display:grid;grid-template-columns:minmax(150px,1.25fr) minmax(110px,.8fr) minmax(105px,.7fr) auto;
2683
+ width:min(720px,calc(100vw - 180px));overflow:visible}
2684
+ .sl-view3d-nav.is-seat-focused .sl-view3d-locator{display:none}
2685
+ .sl-view3d-locator select{width:100%;min-width:0}
2686
+ .sl-picker[data-layout="narrow"] .sl-view3d-nav .sl-view3d-locator{display:flex;width:calc(100vw - 24px);overflow-x:auto}
2687
+ .sl-picker[data-layout="narrow"] .sl-view3d-locator select{flex:0 0 155px}
2688
+ /* On phones the library owns the bottom edge for seat/panorama/overview actions.
2689
+ Keep venue navigation in a separate top rail so those control families never
2690
+ stack over one another. */
2691
+ .sl-picker[data-layout="narrow"] .sl-view3d-nav{top:68px;bottom:auto;max-width:calc(100% - 24px)}
2641
2692
  /* While immersed, the 2D-only chrome is meaningless \u2014 hide it, keep Map|3D. */
2642
2693
  .sl-picker[data-view3d="on"] .sl-rungs,
2643
2694
  .sl-picker[data-view3d="on"] .sl-floors,
@@ -2716,7 +2767,6 @@ var CSS = (
2716
2767
  .sl-confirm-thumb-badge{position:absolute;right:7px;top:7px;display:inline-flex;align-items:center;gap:5px;
2717
2768
  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
2769
  .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
2770
  .sl-confirm-view{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
2721
2771
  color:var(--sl-text);font-weight:700;font-size:12px;display:flex;align-items:center;justify-content:center;gap:7px}
2722
2772
  .sl-confirm-view:hover{border-color:var(--sl-muted)}
@@ -2811,6 +2861,11 @@ var CSS = (
2811
2861
  /* modal host */
2812
2862
  .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
2863
  .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)}
2864
+ /* The frame already clips to 16px. A picker rounding itself to --sl-radius
2865
+ (14px) inside it leaves a sliver of scrim showing at each corner, which
2866
+ reads as a rendering fault rather than as a rounded card. One owner of the
2867
+ corners, and it is the frame. */
2868
+ .sl-modal-frame > .sl-picker{border-radius:0}
2814
2869
  @media(max-width:640px){.sl-modal-scrim{padding:0}.sl-modal-frame{width:100%;height:100%;border-radius:0}}
2815
2870
  `
2816
2871
  );
@@ -2821,6 +2876,17 @@ function ensureStyle() {
2821
2876
  el.textContent = CSS;
2822
2877
  document.head.appendChild(el);
2823
2878
  }
2879
+ function formatWhen(startsAt, timezone, locale) {
2880
+ const options = { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" };
2881
+ const at = new Date(startsAt);
2882
+ if (timezone) {
2883
+ try {
2884
+ return at.toLocaleString(locale, { ...options, timeZone: timezone });
2885
+ } catch {
2886
+ }
2887
+ }
2888
+ return at.toLocaleString(locale, options);
2889
+ }
2824
2890
  function resolveTokens(chart, host) {
2825
2891
  const accent = host?.accent ?? chart?.accent ?? "#f4b740";
2826
2892
  const accentInk = host?.accentInk ?? chart?.accentInk ?? "#1a1200";
@@ -3015,6 +3081,9 @@ var SeatPicker = class _SeatPicker {
3015
3081
  currency: options.currency,
3016
3082
  flashOnLiveChange: true,
3017
3083
  colorblindSafe: this.cbSafe,
3084
+ // The drawn map's own overrides, when the host supplied them up front.
3085
+ // Everything else on `theme` is CSS and lands on the root instead.
3086
+ mapTheme: options.theme?.map ?? null,
3018
3087
  onSelectionChange: () => {
3019
3088
  this.syncTray();
3020
3089
  if (this.committedSelection().length) this.collapseSectionCard();
@@ -3103,8 +3172,8 @@ var SeatPicker = class _SeatPicker {
3103
3172
  /**
3104
3173
  * Eager sightline preview for the confirm card: the organizer's real view
3105
3174
  * 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
3175
+ * forward view plus an approximate distance-to-stage line. On a stageless
3176
+ * chart both the distance claim and the generic stage silhouette are
3108
3177
  * invented promises, so we suppress them; a real attached photo always shows.
3109
3178
  * (OV-52)
3110
3179
  */
@@ -3123,7 +3192,7 @@ var SeatPicker = class _SeatPicker {
3123
3192
  return "";
3124
3193
  }
3125
3194
  }
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>` : "";
3195
+ const sightHtml = hasStage && distance != null ? `<div class="sl-confirm-sight">${t2("picker.sightline", { m: distance })}</div>` : "";
3127
3196
  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
3197
  return viewBtn + sightHtml;
3129
3198
  }
@@ -3257,6 +3326,11 @@ var SeatPicker = class _SeatPicker {
3257
3326
  this.setFsFallback(false);
3258
3327
  }
3259
3328
  }
3329
+ syncFullscreenButtons() {
3330
+ const active = !!document.fullscreenElement || this.fsFallback || this.framedFs;
3331
+ this.els.zfs?.setAttribute("aria-pressed", String(active));
3332
+ this.view3dEl?.querySelector(".sl-view3d-fs")?.setAttribute("aria-pressed", String(active));
3333
+ }
3260
3334
  /**
3261
3335
  * Native element-fullscreen was unavailable or rejected. When framed, a CSS
3262
3336
  * `.sl-fs` overlay can't escape the iframe, so we ask the host page to pin us
@@ -3271,7 +3345,7 @@ var SeatPicker = class _SeatPicker {
3271
3345
  setFramedFs(on) {
3272
3346
  if (this.framedFs === on) return;
3273
3347
  this.framedFs = on;
3274
- this.els.zfs?.setAttribute("aria-pressed", String(on || !!document.fullscreenElement));
3348
+ this.syncFullscreenButtons();
3275
3349
  this.postToHost({ type: "seatlayer:fullscreen", on });
3276
3350
  if (on && !this.fsEscHandler) {
3277
3351
  this.fsEscHandler = (e) => {
@@ -3288,7 +3362,7 @@ var SeatPicker = class _SeatPicker {
3288
3362
  if (this.fsFallback === on) return;
3289
3363
  this.fsFallback = on;
3290
3364
  this.root?.classList.toggle("sl-fs", on);
3291
- this.els.zfs?.setAttribute("aria-pressed", String(on || !!document.fullscreenElement));
3365
+ this.syncFullscreenButtons();
3292
3366
  if (on && !this.fsEscHandler) {
3293
3367
  this.fsEscHandler = (e) => {
3294
3368
  if (e.key === "Escape" && !document.fullscreenElement) this.setFsFallback(false);
@@ -3478,7 +3552,7 @@ var SeatPicker = class _SeatPicker {
3478
3552
  this.els.zfs.addEventListener("click", () => this.toggleFullscreen());
3479
3553
  this.fsChangeHandler = () => {
3480
3554
  if (!document.fullscreenElement) this.setFsFallback(false);
3481
- this.els.zfs?.setAttribute("aria-pressed", String(!!document.fullscreenElement || this.fsFallback || this.framedFs));
3555
+ this.syncFullscreenButtons();
3482
3556
  requestAnimationFrame(() => this.controller.zoomToFit());
3483
3557
  };
3484
3558
  document.addEventListener("fullscreenchange", this.fsChangeHandler);
@@ -3570,7 +3644,7 @@ var SeatPicker = class _SeatPicker {
3570
3644
  if (logoUrl) this.els.logo.innerHTML = `<img src="${logoUrl}" alt="">`;
3571
3645
  else this.els.logo.textContent = (this.opts.theme?.brandName ?? chartTheme?.brandName ?? info.eventName ?? "?").slice(0, 1).toUpperCase();
3572
3646
  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" }) : "";
3647
+ const when = info.startsAt ? formatWhen(info.startsAt, info.timezone ?? null, this.opts.locale) : "";
3574
3648
  this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
3575
3649
  this.buildBadge(chartTheme);
3576
3650
  const present = /* @__PURE__ */ new Set();
@@ -3823,8 +3897,12 @@ var SeatPicker = class _SeatPicker {
3823
3897
  if (this.badgeHidden(chartTheme)) return;
3824
3898
  const foot = this.els.foot;
3825
3899
  if (!foot) return;
3826
- const el = document.createElement("div");
3900
+ const el = document.createElement("a");
3827
3901
  el.className = "sl-powered";
3902
+ el.href = "https://seatlayer.io/?ref=picker";
3903
+ el.target = "_blank";
3904
+ el.rel = "noopener noreferrer";
3905
+ el.setAttribute("aria-label", this.tf("picker.poweredBy", "Powered by SeatLayer"));
3828
3906
  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
3907
  foot.appendChild(el);
3830
3908
  }
@@ -4683,6 +4761,7 @@ var SeatPicker = class _SeatPicker {
4683
4761
  el.querySelector(".sl-confirm-view")?.addEventListener("click", () => void this.openSeatView(seat));
4684
4762
  el.querySelector(".sl-confirm-3d")?.addEventListener("click", () => {
4685
4763
  if (this.buyerView === "venue3d") {
4764
+ this.dismissConfirm();
4686
4765
  void this.view3dHandle?.flyToSeat(seat.id);
4687
4766
  } else {
4688
4767
  this.view3dReturnSeat = seat;
@@ -4761,13 +4840,24 @@ var SeatPicker = class _SeatPicker {
4761
4840
  const doc = this.controller.doc;
4762
4841
  const activeId = this.controller.getActiveFloorId();
4763
4842
  const focal = seat.focalPoint ?? doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
4764
- let panoUrl;
4843
+ let panoSource;
4765
4844
  let caption;
4766
4845
  let real = false;
4767
4846
  if (seat.viewUrl) {
4768
- panoUrl = seat.viewUrl;
4769
- caption = t2("picker.panorama360");
4770
- real = true;
4847
+ const view = {
4848
+ url: seat.viewUrl,
4849
+ ...seat.viewMeta?.previewUrl ? { previewUrl: seat.viewMeta.previewUrl } : {},
4850
+ ...seat.viewMeta?.sourceWidth !== void 0 ? { sourceWidth: seat.viewMeta.sourceWidth } : {},
4851
+ ...seat.viewMeta?.sourceHeight !== void 0 ? { sourceHeight: seat.viewMeta.sourceHeight } : {},
4852
+ ...seat.viewMeta?.previewWidth !== void 0 ? { previewWidth: seat.viewMeta.previewWidth } : {},
4853
+ ...seat.viewMeta?.previewHeight !== void 0 ? { previewHeight: seat.viewMeta.previewHeight } : {},
4854
+ ...seat.viewMeta?.coverage ? { coverage: seat.viewMeta.coverage } : {},
4855
+ ...seat.viewMeta?.capturedAt ? { capturedAt: seat.viewMeta.capturedAt } : {},
4856
+ ...seat.viewMeta?.sourceLabel ? { sourceLabel: seat.viewMeta.sourceLabel } : {}
4857
+ };
4858
+ panoSource = view;
4859
+ caption = seatViewDisclosure(view);
4860
+ real = isAuthoredSeatView(view);
4771
4861
  } else {
4772
4862
  let pano2;
4773
4863
  try {
@@ -4778,7 +4868,7 @@ var SeatPicker = class _SeatPicker {
4778
4868
  return;
4779
4869
  }
4780
4870
  if (!this.root || !this.seatViewEnabled()) return;
4781
- panoUrl = pano2.url;
4871
+ panoSource = { url: pano2.url, generated: true };
4782
4872
  caption = t2("picker.illustrationCaption", { m: pano2.distanceM });
4783
4873
  }
4784
4874
  this.closeSeatView();
@@ -4790,7 +4880,20 @@ var SeatPicker = class _SeatPicker {
4790
4880
  this.root.appendChild(el);
4791
4881
  this.viewEl = el;
4792
4882
  const pano = el.querySelector(".sl-view-pano");
4793
- pano.style.backgroundImage = `url("${panoUrl}")`;
4883
+ const delivery = planPanoramaDelivery(panoSource, browserPanoramaConstraints());
4884
+ const loadAbort = new AbortController();
4885
+ pano.style.backgroundImage = `url("${delivery.initialUrl}")`;
4886
+ let cancelUpgrade = () => {
4887
+ };
4888
+ if (delivery.upgradeUrl) {
4889
+ cancelUpgrade = schedulePanoramaUpgrade(() => {
4890
+ void loadPanoramaImage(delivery.upgradeUrl, loadAbort.signal).then(() => {
4891
+ if (!el.isConnected || loadAbort.signal.aborted) return;
4892
+ pano.style.backgroundImage = `url("${delivery.upgradeUrl}")`;
4893
+ }).catch(() => {
4894
+ });
4895
+ });
4896
+ }
4794
4897
  const VFOV_DEG = 70;
4795
4898
  const MAX_PITCH_DEG = 35;
4796
4899
  let zoom = 1;
@@ -4852,6 +4955,8 @@ var SeatPicker = class _SeatPicker {
4852
4955
  el.addEventListener("keydown", onKey);
4853
4956
  closeBtn.focus();
4854
4957
  this.viewCleanup = () => {
4958
+ cancelUpgrade();
4959
+ loadAbort.abort();
4855
4960
  pano.removeEventListener("pointerdown", onDown);
4856
4961
  pano.removeEventListener("pointermove", onMove);
4857
4962
  pano.removeEventListener("pointerup", onUp);
@@ -5655,6 +5760,58 @@ var SeatPicker = class _SeatPicker {
5655
5760
  getSelection() {
5656
5761
  return this.committedSelection();
5657
5762
  }
5763
+ /**
5764
+ * Re-ink the DRAWN MAP after mount, so the canvas can follow a page palette
5765
+ * the host did not know at construction time.
5766
+ *
5767
+ * Deliberately narrower than the `theme` option: it takes only the map half.
5768
+ * The chrome half is CSS custom properties, which a host restyles from its
5769
+ * own stylesheet without asking the widget for anything — and a host that
5770
+ * used both mechanisms at once would have two things writing one token. This
5771
+ * method exists for the half CSS genuinely cannot reach: pixels Konva draws.
5772
+ *
5773
+ * The rebuild is the controller's (`setMapTheme`), which repaints statuses
5774
+ * from the map already in memory and restores the selection, so a buyer
5775
+ * mid-pick keeps their seats and no round trip is spent. A no-op when the
5776
+ * colours have not changed; safe to call on every render.
5777
+ */
5778
+ setMapTheme(map) {
5779
+ if (this.destroyed) return;
5780
+ this.opts.theme = { ...this.opts.theme ?? {}, map: map ?? void 0 };
5781
+ this.controller.setMapTheme(map);
5782
+ }
5783
+ /**
5784
+ * Replace the host pricing override AFTER mount, and repaint everything that
5785
+ * shows a price.
5786
+ *
5787
+ * WHY IT CANNOT JUST BE A MOUNT OPTION. The prices a host knows are often not
5788
+ * the prices it knows AT MOUNT: SeatLayer's own event page learns them from
5789
+ * `GET /pub/events/:key/availability`, a separate cached read that lands a
5790
+ * round trip after the map does — deliberately, because the map is not
5791
+ * allowed to wait on it. Remounting the widget to hand it new options would
5792
+ * tear down a live hold, so the override is settable in place.
5793
+ *
5794
+ * The prices themselves are still the SERVER'S. This method takes an answer;
5795
+ * it never derives one. Nothing here re-reads a release, a window or a quota
5796
+ * — see `paidPrice`, the one funnel every displayed price flows through, and
5797
+ * note that the checkout handoff's line items do not come from here at all:
5798
+ * they are priced by the worker from its own hold. So the worst a wrong call
5799
+ * to this method can do is misprint a price, never mischarge one.
5800
+ *
5801
+ * A no-op when the map is unchanged, so a host may call it on every poll.
5802
+ */
5803
+ setPricing(pricing) {
5804
+ const before = JSON.stringify(this.opts.pricing ?? null);
5805
+ const after = JSON.stringify(pricing ?? null);
5806
+ if (before === after) return;
5807
+ this.opts.pricing = pricing;
5808
+ if (this.destroyed || !this.els.prices) return;
5809
+ this.els.pricesSec?.querySelector(".sl-price-select")?.remove();
5810
+ this.buildPriceFilter();
5811
+ this.syncPrices();
5812
+ this.syncTray();
5813
+ if (this.lastSection) this.showSectionCard(this.lastSection);
5814
+ }
5658
5815
  /** Current colorblind-safe render state, resolved from the stored buyer
5659
5816
  * preference at mount. Host chrome (e.g. the Designer preview) reads this to
5660
5817
  * surface the state rather than rendering colorblind colors silently. */
@@ -5706,10 +5863,7 @@ var SeatPicker = class _SeatPicker {
5706
5863
  *
5707
5864
  * Entering `'venue3d'` with `opts.flyToSeatId` runs the cinematic tour: the
5708
5865
  * 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
5866
+ * 360° view-from-seat). When the widget is **already** in the 3D
5713
5867
  * view, the camera simply flies to the requested seat — the GL scene is not
5714
5868
  * torn down or rebuilt, so there is no flash or re-entry.
5715
5869
  *
@@ -5718,7 +5872,7 @@ var SeatPicker = class _SeatPicker {
5718
5872
  *
5719
5873
  * @param view `'map'` for the flat picker, `'venue3d'` for the 3D venue.
5720
5874
  * @param opts.flyToSeatId When entering (or already in) `'venue3d'`, the seat
5721
- * id to fly the camera to — cinematic → panorama.
5875
+ * id to fly the camera to — cinematic → live seat view.
5722
5876
  * Ignored when `view` is `'map'`.
5723
5877
  *
5724
5878
  * @example
@@ -5732,7 +5886,12 @@ var SeatPicker = class _SeatPicker {
5732
5886
  }
5733
5887
  const flyToSeatId = opts?.flyToSeatId;
5734
5888
  if (this.buyerView === "venue3d") {
5735
- if (flyToSeatId) void this.view3dHandle?.flyToSeat(flyToSeatId);
5889
+ if (flyToSeatId) {
5890
+ this.opts.onBuyerViewChange?.({ view: "venue3d", seatId: flyToSeatId });
5891
+ void this.view3dHandle?.flyToSeat(flyToSeatId);
5892
+ } else if (opts?.resetView) {
5893
+ this.view3dHandle?.focusOverview();
5894
+ }
5736
5895
  return;
5737
5896
  }
5738
5897
  void this.enter3d(flyToSeatId);
@@ -5772,14 +5931,26 @@ var SeatPicker = class _SeatPicker {
5772
5931
  async seatViewFor3d(seatId) {
5773
5932
  const seat = this.allSeats().find((s) => s.id === seatId);
5774
5933
  if (!seat) return null;
5775
- if (seat.viewUrl) return { url: seat.viewUrl };
5934
+ if (seat.viewUrl) return {
5935
+ url: seat.viewUrl,
5936
+ ...seat.viewMeta?.previewUrl ? { previewUrl: seat.viewMeta.previewUrl } : {},
5937
+ ...seat.viewMeta?.sourceWidth !== void 0 ? { sourceWidth: seat.viewMeta.sourceWidth } : {},
5938
+ ...seat.viewMeta?.sourceHeight !== void 0 ? { sourceHeight: seat.viewMeta.sourceHeight } : {},
5939
+ ...seat.viewMeta?.previewWidth !== void 0 ? { previewWidth: seat.viewMeta.previewWidth } : {},
5940
+ ...seat.viewMeta?.previewHeight !== void 0 ? { previewHeight: seat.viewMeta.previewHeight } : {},
5941
+ ...seat.viewMeta?.initialBearingDeg !== void 0 ? { initialBearingDeg: seat.viewMeta.initialBearingDeg } : {},
5942
+ ...seat.viewMeta?.initialPitchDeg !== void 0 ? { initialPitchDeg: seat.viewMeta.initialPitchDeg } : {},
5943
+ ...seat.viewMeta?.coverage ? { coverage: seat.viewMeta.coverage } : {},
5944
+ ...seat.viewMeta?.capturedAt ? { capturedAt: seat.viewMeta.capturedAt } : {},
5945
+ ...seat.viewMeta?.sourceLabel ? { sourceLabel: seat.viewMeta.sourceLabel } : {}
5946
+ };
5776
5947
  const doc = this.controller.doc;
5777
5948
  if (!doc) return null;
5778
5949
  const activeId = this.controller.getActiveFloorId();
5779
5950
  const focal = seat.focalPoint ?? doc.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc.focalPoint ?? { x: 0, y: 0 };
5780
5951
  try {
5781
5952
  const { generateSeatPanorama } = await loadPanorama();
5782
- return { url: generateSeatPanorama(seat, focal, this.allSeats()).url };
5953
+ return { url: generateSeatPanorama(seat, focal, this.allSeats()).url, generated: true };
5783
5954
  } catch {
5784
5955
  return null;
5785
5956
  }
@@ -5801,14 +5972,26 @@ var SeatPicker = class _SeatPicker {
5801
5972
  }
5802
5973
  const seat = this.allSeats().find((s) => s.id === seatId);
5803
5974
  if (!seat) return;
5804
- const already = this.committedSelection().some((s) => s.id === seatId);
5975
+ const table = this.controller.tableSelection(seatId);
5976
+ const already = this.committedSelection().some((s) => table ? s.label === table.label : s.id === seatId);
5805
5977
  if (already) {
5806
5978
  this.controller.deselect([seatId]);
5807
5979
  this.dismissConfirm();
5808
5980
  return;
5809
5981
  }
5982
+ const added = this.controller.select([seatId]);
5983
+ if (!added.length) {
5984
+ this.syncSelectionTo3d();
5985
+ this.dismissConfirm();
5986
+ return;
5987
+ }
5988
+ this.opts.onBuyerViewChange?.({ view: "venue3d", seatId });
5810
5989
  this.flashPickedSeat(seatId);
5811
- this.controller.select([seatId]);
5990
+ if (table) {
5991
+ this.showTableDialog(table, false);
5992
+ this.syncSelectionTo3d();
5993
+ return;
5994
+ }
5812
5995
  if (this.opts.confirmSelection !== false) this.showConfirm(seat);
5813
5996
  else this.syncTray();
5814
5997
  }
@@ -5828,12 +6011,15 @@ var SeatPicker = class _SeatPicker {
5828
6011
  */
5829
6012
  buildView3dNav(overlay, handle) {
5830
6013
  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);
6014
+ const floorLabels = new Set(floors.map((floor) => floor.label.trim().toLocaleLowerCase()));
6015
+ const zones = handle.zones().filter((zone) => zone.seatCount > 0 && !floorLabels.has(zone.label.trim().toLocaleLowerCase()));
6016
+ const allSections = handle.sections().filter((s) => s.seatCount > 0);
6017
+ const sections = zones.length > 1 ? [] : allSections;
5833
6018
  const wantFloors = floors.length > 1;
5834
6019
  const wantZones = zones.length > 1;
5835
6020
  const wantSections = sections.length > 1;
5836
- if (!wantFloors && !wantZones && !wantSections) return;
6021
+ const wantLocator = allSections.length > 0 && handle.rows().length > 0;
6022
+ if (!wantFloors && !wantZones && !wantSections && !wantLocator) return;
5837
6023
  const nav = document.createElement("div");
5838
6024
  nav.className = "sl-view3d-nav";
5839
6025
  if (wantFloors) {
@@ -5870,15 +6056,112 @@ var SeatPicker = class _SeatPicker {
5870
6056
  } })) : sections.map((s) => ({ id: s.id, label: s.label || s.id, go: () => {
5871
6057
  handle.focusSection(s.id);
5872
6058
  } }));
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);
6059
+ if (!wantZones && entries.length > MAX_3D_SECTION_PILLS) {
6060
+ const select = document.createElement("select");
6061
+ select.setAttribute("aria-label", this.tf("picker.jumpToSection", "Jump to section"));
6062
+ const placeholder = document.createElement("option");
6063
+ placeholder.value = "";
6064
+ placeholder.textContent = this.tf("picker.jumpToSection", "Jump to section");
6065
+ select.appendChild(placeholder);
6066
+ for (const entry of entries) {
6067
+ const option = document.createElement("option");
6068
+ option.value = entry.id;
6069
+ option.textContent = entry.label;
6070
+ select.appendChild(option);
6071
+ }
6072
+ select.addEventListener("change", () => {
6073
+ entries.find((entry) => entry.id === select.value)?.go();
6074
+ });
6075
+ row.appendChild(select);
6076
+ } else {
6077
+ for (const e of entries) {
6078
+ const b = document.createElement("button");
6079
+ b.type = "button";
6080
+ b.textContent = e.label;
6081
+ b.addEventListener("click", e.go);
6082
+ row.appendChild(b);
6083
+ }
5879
6084
  }
5880
6085
  nav.appendChild(row);
5881
6086
  }
6087
+ if (wantLocator) {
6088
+ const locator = document.createElement("div");
6089
+ locator.className = "sl-view3d-locator";
6090
+ locator.setAttribute("role", "group");
6091
+ locator.setAttribute("aria-label", "Find an exact seat in 3D");
6092
+ const sectionSelect = document.createElement("select");
6093
+ sectionSelect.setAttribute("aria-label", "Choose section in 3D");
6094
+ const rowSelect = document.createElement("select");
6095
+ rowSelect.setAttribute("aria-label", "Choose row in 3D");
6096
+ const seatSelect = document.createElement("select");
6097
+ seatSelect.setAttribute("aria-label", "Choose seat in 3D");
6098
+ const view = document.createElement("button");
6099
+ view.type = "button";
6100
+ view.textContent = "View seat";
6101
+ view.disabled = true;
6102
+ const fill = (select, placeholder, entries) => {
6103
+ select.replaceChildren();
6104
+ const first = document.createElement("option");
6105
+ first.value = "";
6106
+ first.textContent = placeholder;
6107
+ select.appendChild(first);
6108
+ for (const entry of entries) {
6109
+ const option = document.createElement("option");
6110
+ option.value = entry.id;
6111
+ option.textContent = entry.label;
6112
+ select.appendChild(option);
6113
+ }
6114
+ select.value = "";
6115
+ };
6116
+ fill(sectionSelect, "1. Section", allSections.map((section) => ({
6117
+ id: section.id,
6118
+ label: `${section.label} \xB7 ${section.seatCount.toLocaleString()} seats`
6119
+ })));
6120
+ fill(rowSelect, "2. Row", []);
6121
+ fill(seatSelect, "3. Seat", []);
6122
+ rowSelect.disabled = true;
6123
+ seatSelect.disabled = true;
6124
+ sectionSelect.addEventListener("change", () => {
6125
+ const sectionId = sectionSelect.value;
6126
+ view.disabled = true;
6127
+ fill(seatSelect, "3. Seat", []);
6128
+ seatSelect.disabled = true;
6129
+ if (!sectionId || !handle.focusSection(sectionId)) {
6130
+ fill(rowSelect, "2. Row", []);
6131
+ rowSelect.disabled = true;
6132
+ return;
6133
+ }
6134
+ const rows = handle.rows(sectionId);
6135
+ fill(rowSelect, "2. Row", rows.map((row) => ({
6136
+ id: row.id,
6137
+ label: `${row.label} \xB7 ${row.seatCount} seats`
6138
+ })));
6139
+ rowSelect.disabled = rows.length === 0;
6140
+ });
6141
+ rowSelect.addEventListener("change", () => {
6142
+ const rowId = rowSelect.value;
6143
+ view.disabled = true;
6144
+ if (!rowId || !handle.focusRow(rowId)) {
6145
+ fill(seatSelect, "3. Seat", []);
6146
+ seatSelect.disabled = true;
6147
+ return;
6148
+ }
6149
+ const seats = handle.seatsInRow(rowId);
6150
+ fill(seatSelect, "3. Seat", seats);
6151
+ seatSelect.disabled = seats.length === 0;
6152
+ });
6153
+ seatSelect.addEventListener("change", () => {
6154
+ const seatId = seatSelect.value;
6155
+ view.disabled = !seatId;
6156
+ handle.setSelection(seatId ? [seatId] : []);
6157
+ });
6158
+ view.addEventListener("click", () => {
6159
+ const seatId = seatSelect.value;
6160
+ if (seatId) void handle.flyToSeat(seatId);
6161
+ });
6162
+ locator.append(sectionSelect, rowSelect, seatSelect, view);
6163
+ nav.appendChild(locator);
6164
+ }
5882
6165
  overlay.appendChild(nav);
5883
6166
  }
5884
6167
  async enter3d(flySeatId) {
@@ -5886,6 +6169,7 @@ var SeatPicker = class _SeatPicker {
5886
6169
  const doc = this.controller.doc;
5887
6170
  if (!doc) return;
5888
6171
  this.buyerView = "venue3d";
6172
+ this.opts.onBuyerViewChange?.({ view: "venue3d", ...flySeatId ? { seatId: flySeatId } : {} });
5889
6173
  this.root?.setAttribute("data-view3d", "on");
5890
6174
  this.dismissConfirm();
5891
6175
  this.syncProjection();
@@ -5899,6 +6183,20 @@ var SeatPicker = class _SeatPicker {
5899
6183
  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
6184
  back.addEventListener("click", () => this.exit3d());
5901
6185
  overlay.appendChild(back);
6186
+ const fullscreen = document.createElement("button");
6187
+ fullscreen.type = "button";
6188
+ fullscreen.className = "sl-view3d-fs";
6189
+ fullscreen.textContent = "\u26F6";
6190
+ fullscreen.setAttribute("aria-label", "Full screen");
6191
+ fullscreen.setAttribute("aria-pressed", String(!!document.fullscreenElement || this.fsFallback || this.framedFs));
6192
+ fullscreen.addEventListener("click", () => this.toggleFullscreen());
6193
+ overlay.appendChild(fullscreen);
6194
+ const loading = document.createElement("div");
6195
+ loading.className = "sl-view3d-loading";
6196
+ loading.setAttribute("role", "status");
6197
+ loading.setAttribute("aria-live", "polite");
6198
+ loading.textContent = this.tf("picker.loading3d", "Building the 3D venue\u2026");
6199
+ overlay.appendChild(loading);
5902
6200
  this.els.map.appendChild(overlay);
5903
6201
  this.view3dEl = overlay;
5904
6202
  requestAnimationFrame(() => {
@@ -5909,8 +6207,30 @@ var SeatPicker = class _SeatPicker {
5909
6207
  const seats = expandChart(doc);
5910
6208
  const mod = await loadVenue3d();
5911
6209
  if (gen !== this.view3dGen || this.buyerView !== "venue3d" || this.view3dEl !== overlay) return;
5912
- const handle = mod.mountVenue3D(overlay, { doc, seats }, {
6210
+ const prepared = await mod.prepareVenue3D({ doc, seats });
6211
+ if (gen !== this.view3dGen || this.buyerView !== "venue3d" || this.view3dEl !== overlay) return;
6212
+ const handle = mod.mountVenue3D(overlay, { doc, seats, prepared }, {
6213
+ // A strict contain fit turns a wide bowl into a postage stamp inside a
6214
+ // phone. The bounded portrait fit was validated against every UX-lab
6215
+ // fixture; keep editor previews strict while making buyer seats legible.
6216
+ portraitOverviewCrop: true,
5913
6217
  onSeatPick: (id) => this.onView3dSeatPick(id),
6218
+ onSectionFocusChange: (sectionId) => {
6219
+ const select = overlay.querySelector(
6220
+ 'select[aria-label="Choose section in 3D"]'
6221
+ );
6222
+ const next = sectionId ?? "";
6223
+ if (!select || select.value === next) return;
6224
+ select.value = next;
6225
+ select.dispatchEvent(new Event("change"));
6226
+ },
6227
+ onViewTargetChange: (seatId) => {
6228
+ overlay.querySelector(".sl-view3d-nav")?.classList.toggle("is-seat-focused", !!seatId);
6229
+ this.opts.onBuyerViewChange?.({
6230
+ view: "venue3d",
6231
+ ...seatId ? { seatId } : {}
6232
+ });
6233
+ },
5914
6234
  // Deferred off the tap gesture: generateSeatPanorama walks every seat
5915
6235
  // (O(n) on a 13k chart), and the module prefetches at pick — by the
5916
6236
  // time the flight lands (~2.5s) the idle render has long finished. A
@@ -5930,12 +6250,15 @@ var SeatPicker = class _SeatPicker {
5930
6250
  onAnalytics: (event, props) => this.emit3dAnalytics(event, props)
5931
6251
  });
5932
6252
  this.view3dHandle = handle;
6253
+ loading.remove();
5933
6254
  this.pushAvailabilityTo3d();
5934
6255
  this.syncSelectionTo3d();
5935
6256
  this.buildView3dNav(overlay, handle);
5936
6257
  if (flySeatId) void handle.flyToSeat(flySeatId);
5937
6258
  } catch (err) {
6259
+ if (gen !== this.view3dGen || this.buyerView !== "venue3d" || this.view3dEl !== overlay) return;
5938
6260
  this.opts.onError?.(err);
6261
+ this.toast(this.tf("picker.unavailable3d", "3D could not start. The seat map is still available."), "warning");
5939
6262
  this.exit3d();
5940
6263
  }
5941
6264
  }
@@ -5943,6 +6266,7 @@ var SeatPicker = class _SeatPicker {
5943
6266
  if (this.buyerView !== "venue3d" && !this.view3dEl) return;
5944
6267
  this.view3dGen++;
5945
6268
  this.buyerView = "map";
6269
+ this.opts.onBuyerViewChange?.({ view: "map" });
5946
6270
  this.root?.removeAttribute("data-view3d");
5947
6271
  try {
5948
6272
  this.view3dHandle?.dispose();