@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.cjs CHANGED
@@ -197,9 +197,13 @@ function mountCheckout(mount) {
197
197
  const status = el(
198
198
  "p",
199
199
  "sl-hco-status",
200
- `${body.seatCount} ${body.seatCount === 1 ? "seat" : "seats"} confirmed. We have emailed your tickets.`
200
+ `${body.seatCount} ${body.seatCount === 1 ? "seat" : "seats"} confirmed. Your tickets \u2014 with their door QR codes \u2014 are in your email.`
201
201
  );
202
202
  const receipt = el("dl", "sl-hco-receipt");
203
+ const seatLabels = (body.tickets ?? []).map((t3) => t3.label);
204
+ if (seatLabels.length) {
205
+ receipt.append(el("dt", void 0, "Seats"), el("dd", void 0, seatLabels.join(", ")));
206
+ }
203
207
  receipt.append(
204
208
  el("dt", void 0, "Paid"),
205
209
  el("dd", void 0, `${body.amountFormatted} ${body.currency}`),
@@ -209,7 +213,15 @@ function mountCheckout(mount) {
209
213
  const close = el("button", "sl-hco-back", "Close");
210
214
  close.type = "button";
211
215
  close.addEventListener("click", cancel);
212
- show(status, receipt, close);
216
+ if (body.ticketUrl) {
217
+ const view = el("a", "sl-hco-tickets", "View tickets & QR codes");
218
+ view.href = body.ticketUrl;
219
+ view.target = "_blank";
220
+ view.rel = "noreferrer";
221
+ show(status, receipt, view, close);
222
+ } else {
223
+ show(status, receipt, close);
224
+ }
213
225
  mount.onConfirmed(body);
214
226
  return;
215
227
  }
@@ -283,6 +295,13 @@ function mountCheckout(mount) {
283
295
  "sl-hco-note",
284
296
  `Your seats are held until ${until}. Payment is handled by ${provider === "razorpay" ? "Razorpay" : "Stripe"} \u2014 we never see your card details.`
285
297
  );
298
+ const privacy = el("p", "sl-hco-note");
299
+ privacy.append("The event organizer and SeatLayer use your email to issue and manage your tickets. ");
300
+ const privacyLink = el("a", void 0, "Privacy Policy");
301
+ privacyLink.href = "https://seatlayer.io/privacy/";
302
+ privacyLink.target = "_blank";
303
+ privacyLink.rel = "noreferrer";
304
+ privacy.appendChild(privacyLink);
286
305
  const emailValid = () => /.+@.+\..+/.test(email.value.trim());
287
306
  email.addEventListener("input", () => {
288
307
  pay.disabled = !emailValid();
@@ -337,7 +356,7 @@ function mountCheckout(mount) {
337
356
  event.preventDefault();
338
357
  if (emailValid()) void start();
339
358
  });
340
- form.append(emailLabel, email, nameLabel, name, pay, back, note);
359
+ form.append(emailLabel, email, nameLabel, name, privacy, pay, back, note);
341
360
  const details = () => {
342
361
  title.textContent = "Checkout";
343
362
  show(summary, form);
@@ -381,12 +400,16 @@ var init_hostedCheckout = __esm({
381
400
  .sl-hco-back{width:100%;margin-top:8px;padding:10px;font:inherit;font-size:14px;color:var(--sl-muted);
382
401
  background:none;border:0;cursor:pointer;text-decoration:underline}
383
402
  .sl-hco-note{margin:12px 0 0;font-size:12px;line-height:1.5;color:var(--sl-muted)}
403
+ .sl-hco-note a{color:inherit;text-decoration:underline;text-underline-offset:2px}
384
404
  .sl-hco-status{margin:8px 0 0;font-size:14px;line-height:1.55}
385
405
  .sl-hco-error{color:var(--sl-danger, #c0392b)}
386
406
  .sl-hco-receipt{display:grid;grid-template-columns:auto 1fr;gap:4px 14px;margin:14px 0 0;font-size:13px}
387
407
  .sl-hco-receipt dt{color:var(--sl-muted)}
388
408
  .sl-hco-receipt dd{margin:0;text-align:right}
389
409
  .sl-hco-ref{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;word-break:break-all}
410
+ .sl-hco-tickets{display:block;width:100%;margin-top:14px;padding:12px;font:inherit;font-size:15px;
411
+ font-weight:650;text-align:center;text-decoration:none;color:var(--sl-accent-ink);
412
+ background:var(--sl-accent);border-radius:calc(var(--sl-radius) * .55)}
390
413
  `;
391
414
  }
392
415
  });
@@ -4167,8 +4190,12 @@ var BuyerRealtimeClient = class {
4167
4190
  const next = projectionFromSnapshot(frame);
4168
4191
  const changes = diffProjections(this.projection, next);
4169
4192
  this.projection = next;
4170
- if (changes === null) void this.opts.sink.resync();
4171
- else if (changes.length) this.opts.sink.applyStatuses(changes);
4193
+ if (changes === null) {
4194
+ if (this.opts.sink.applyProjection) this.opts.sink.applyProjection(next);
4195
+ else void this.opts.sink.resync();
4196
+ } else if (changes.length) {
4197
+ this.opts.sink.applyStatuses(changes);
4198
+ }
4172
4199
  return;
4173
4200
  }
4174
4201
  if (type === "delta" && Array.isArray(frame.changes)) {
@@ -4421,7 +4448,7 @@ var PubApi = class {
4421
4448
  return this.request(`/pub/events/${encodeURIComponent(key)}/chart`);
4422
4449
  }
4423
4450
  objects(key) {
4424
- return this.request(`/pub/events/${encodeURIComponent(key)}/objects`);
4451
+ return this.request(`/pub/events/${encodeURIComponent(key)}/objects?compact=1`);
4425
4452
  }
4426
4453
  hold(key, selections, ttlMs, replaceHoldId) {
4427
4454
  return this.request(`/pub/events/${encodeURIComponent(key)}/hold`, {
@@ -5869,11 +5896,13 @@ var EmbeddedDesigner = class {
5869
5896
 
5870
5897
  // src/SeatPicker.ts
5871
5898
  var import_core2 = require("@seatlayer/core");
5899
+ var import_panorama = require("@seatlayer/core/view3d/crossfade/panorama");
5900
+ var import_panoramaDelivery = require("@seatlayer/core/view/panoramaDelivery");
5872
5901
  var import_meta = {};
5873
5902
  var DEFAULT_API_BASE2 = "https://api.seatlayer.io";
5874
5903
  var DEFAULT_MAX_SELECTION2 = 10;
5875
5904
  var EXTEND_PROMPT_MS = 6e4;
5876
- var MAX_3D_SEATS_DEFAULT = 15e3;
5905
+ var MAX_3D_SEATS_DEFAULT = 6e4;
5877
5906
  var MAX_3D_SECTION_PILLS = 12;
5878
5907
  function pointInPolygon(x, y, poly) {
5879
5908
  let inside = false;
@@ -6217,7 +6246,12 @@ var CSS2 = (
6217
6246
  INTO one of these positioned flex containers and flows/stacks within it, so no
6218
6247
  two pieces of chrome free-float on top of each other. Regions never overlap:
6219
6248
  the top strip splits into left/center/right; rails + corners own their edge. */
6220
- .sl-anchor{position:absolute;z-index:5;display:flex;gap:8px;pointer-events:none}
6249
+ /* align-items:center, not the flex default of stretch. Without it a short pill
6250
+ beside a taller control (TEST MODE next to the Map/3D toggle) is stretched to
6251
+ the row's height, so two pills that should read as a matched pair end up
6252
+ different shapes on different baselines. Column regions set their own
6253
+ cross-axis alignment below and are unaffected. */
6254
+ .sl-anchor{position:absolute;z-index:5;display:flex;align-items:center;gap:8px;pointer-events:none}
6221
6255
  .sl-anchor > *{pointer-events:auto}
6222
6256
  .sl-anchor[data-region="top-left"]{top:12px;left:12px;flex-wrap:wrap;max-width:38%}
6223
6257
  .sl-anchor[data-region="top-center"]{top:12px;left:50%;transform:translateX(-50%);flex-direction:column;
@@ -6233,10 +6267,17 @@ var CSS2 = (
6233
6267
  .sl-picker[data-layout="narrow"] .sl-anchor[data-region="top-center"]{max-width:44%}
6234
6268
 
6235
6269
  /* TEST MODE badge \u2014 a small pill in the top-right region (shrinks on narrow) */
6236
- .sl-testbadge{padding:5px 11px;border-radius:999px;font-size:10px;font-weight:800;letter-spacing:.1em;
6270
+ /* align-self:stretch, not a hardcoded height: the badge sits beside the Map/3D
6271
+ toggle, whose height comes from its own border + padding + button metrics.
6272
+ Stretching matches that row exactly and keeps matching if the toggle ever
6273
+ changes, while the badge's own inline-flex keeps the label centred inside
6274
+ whatever height it gets. Alone in the region it simply takes its natural
6275
+ size. */
6276
+ .sl-testbadge{display:inline-flex;align-items:center;align-self:stretch;padding:0 12px;border-radius:999px;
6277
+ font-size:10px;font-weight:800;letter-spacing:.1em;line-height:1;
6237
6278
  text-transform:uppercase;white-space:nowrap;background:var(--sl-accent);color:var(--sl-accent-ink);
6238
6279
  box-shadow:0 2px 8px rgba(0,0,0,.25)}
6239
- .sl-picker[data-layout="narrow"] .sl-testbadge{padding:3px 8px;font-size:8.5px;letter-spacing:.06em}
6280
+ .sl-picker[data-layout="narrow"] .sl-testbadge{padding:0 8px;font-size:8.5px;letter-spacing:.06em}
6240
6281
 
6241
6282
  /* zoom column (flows within the bottom-right region) */
6242
6283
  .sl-zoom{display:flex;flex-direction:column;gap:6px}
@@ -6328,7 +6369,11 @@ var CSS2 = (
6328
6369
  Layered Rows mark + wordmark. Hidden when the host opts out or the org's paid
6329
6370
  theme sets hideBadge. */
6330
6371
  .sl-powered{display:flex;align-items:center;justify-content:center;gap:6px;margin-top:10px;
6331
- font-size:11px;letter-spacing:.03em;color:var(--sl-muted)}
6372
+ font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--sl-text);opacity:.72;
6373
+ text-decoration:none;padding:6px 10px;border-radius:999px;width:fit-content;margin-inline:auto;
6374
+ transition:opacity .15s ease,background-color .15s ease}
6375
+ .sl-powered:hover{opacity:1;background:color-mix(in srgb,var(--sl-text) 8%,transparent)}
6376
+ .sl-powered:focus-visible{opacity:1;outline:2px solid var(--sl-accent);outline-offset:2px}
6332
6377
  .sl-powered-mark{width:16px;height:16px;border-radius:4px;flex:none;display:flex;align-items:center;justify-content:center;
6333
6378
  background:#0c1220;color:#fcf7ee}
6334
6379
  .sl-powered-mark svg{width:12px;height:11px}
@@ -6507,18 +6552,27 @@ var CSS2 = (
6507
6552
  .sl-view3d{position:absolute;inset:0;z-index:4;opacity:0;touch-action:none;
6508
6553
  transition:opacity .3s ease;background:radial-gradient(120% 120% at 50% 0%,#191f28 0%,#0d1014 70%)}
6509
6554
  .sl-view3d canvas{display:block;width:100%;height:100%}
6555
+ .sl-view3d canvas:focus-visible{outline:2px solid var(--sl-accent);outline-offset:-3px}
6556
+ .sl-view3d-loading{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:10px;
6557
+ z-index:1;color:#d9e2f2;font-size:13px;font-weight:700;letter-spacing:.02em;pointer-events:none}
6558
+ .sl-view3d-loading::before{content:"";width:18px;height:18px;border-radius:50%;
6559
+ border:2px solid rgba(217,226,242,.28);border-top-color:#d9e2f2;animation:slSpin .8s linear infinite}
6510
6560
  [data-view3d=on] .sl-chips,[data-view3d=on] .sl-rungs{display:none}
6511
6561
  .sl-view3d-back{position:absolute;top:12px;left:12px;z-index:2;display:inline-flex;align-items:center;gap:6px;
6512
6562
  padding:7px 13px 7px 9px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.03em;
6513
6563
  color:#e6edf3;background:rgba(10,14,20,.62);border:1px solid rgba(255,255,255,.22);backdrop-filter:blur(6px)}
6514
6564
  .sl-view3d-back:hover,.sl-view3d-back:focus-visible{background:rgba(16,22,30,.82);border-color:rgba(255,255,255,.4)}
6515
6565
  .sl-view3d-back svg{width:15px;height:15px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round;stroke-linejoin:round}
6566
+ .sl-view3d-fs{position:absolute;top:12px;right:12px;z-index:2;min-width:44px;min-height:44px;padding:8px 12px;
6567
+ border-radius:999px;font-size:16px;color:#e6edf3;background:rgba(10,14,20,.62);
6568
+ border:1px solid rgba(255,255,255,.22);backdrop-filter:blur(6px)}
6569
+ .sl-view3d-fs:hover,.sl-view3d-fs:focus-visible{background:rgba(16,22,30,.82);border-color:rgba(255,255,255,.4)}
6516
6570
  /* Venue navigation inside 3D: levels (isolate a floor) and areas (fly to a zone).
6517
6571
  Bottom-LEFT, clear of the module's own chips (Overview bottom-right, 360
6518
6572
  bottom-centre) and of the bottom-sheeted confirm card. Horizontally scrollable
6519
6573
  so a venue with many zones never pushes the rail off a phone screen. */
6520
6574
  .sl-view3d-nav{position:absolute;left:12px;bottom:16px;z-index:3;display:flex;flex-direction:column;gap:6px;
6521
- max-width:calc(100% - 24px);pointer-events:none}
6575
+ max-width:calc(100% - 150px);pointer-events:none}
6522
6576
  .sl-view3d-nav > div{display:flex;gap:6px;overflow-x:auto;scrollbar-width:none;pointer-events:auto;
6523
6577
  padding:1px;-webkit-overflow-scrolling:touch}
6524
6578
  .sl-view3d-nav > div::-webkit-scrollbar{display:none}
@@ -6527,6 +6581,21 @@ var CSS2 = (
6527
6581
  border:1px solid rgba(150,165,205,.35);backdrop-filter:blur(6px);cursor:pointer}
6528
6582
  .sl-view3d-nav button:hover,.sl-view3d-nav button:focus-visible{color:#eef1f8;border-color:rgba(190,205,240,.6)}
6529
6583
  .sl-view3d-nav button[aria-pressed="true"]{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
6584
+ .sl-view3d-nav button:disabled{opacity:.45;cursor:not-allowed}
6585
+ .sl-view3d-nav select{min-height:38px;max-width:min(280px,calc(100vw - 40px));padding:7px 34px 7px 12px;
6586
+ border-radius:999px;font:700 11.5px/1 inherit;color:#eef1f8;background:rgba(12,18,32,.86);
6587
+ border:1px solid rgba(150,165,205,.45);backdrop-filter:blur(6px);cursor:pointer}
6588
+ .sl-view3d-nav select:focus-visible{outline:2px solid var(--sl-accent);outline-offset:2px}
6589
+ .sl-view3d-nav .sl-view3d-locator{display:grid;grid-template-columns:minmax(150px,1.25fr) minmax(110px,.8fr) minmax(105px,.7fr) auto;
6590
+ width:min(720px,calc(100vw - 180px));overflow:visible}
6591
+ .sl-view3d-nav.is-seat-focused .sl-view3d-locator{display:none}
6592
+ .sl-view3d-locator select{width:100%;min-width:0}
6593
+ .sl-picker[data-layout="narrow"] .sl-view3d-nav .sl-view3d-locator{display:flex;width:calc(100vw - 24px);overflow-x:auto}
6594
+ .sl-picker[data-layout="narrow"] .sl-view3d-locator select{flex:0 0 155px}
6595
+ /* On phones the library owns the bottom edge for seat/panorama/overview actions.
6596
+ Keep venue navigation in a separate top rail so those control families never
6597
+ stack over one another. */
6598
+ .sl-picker[data-layout="narrow"] .sl-view3d-nav{top:68px;bottom:auto;max-width:calc(100% - 24px)}
6530
6599
  /* While immersed, the 2D-only chrome is meaningless \u2014 hide it, keep Map|3D. */
6531
6600
  .sl-picker[data-view3d="on"] .sl-rungs,
6532
6601
  .sl-picker[data-view3d="on"] .sl-floors,
@@ -6605,7 +6674,6 @@ var CSS2 = (
6605
6674
  .sl-confirm-thumb-badge{position:absolute;right:7px;top:7px;display:inline-flex;align-items:center;gap:5px;
6606
6675
  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)}
6607
6676
  .sl-confirm-sight{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--sl-muted);margin-bottom:2px}
6608
- .sl-confirm-sight span{color:#22a06b;font-weight:800}
6609
6677
  .sl-confirm-view{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
6610
6678
  color:var(--sl-text);font-weight:700;font-size:12px;display:flex;align-items:center;justify-content:center;gap:7px}
6611
6679
  .sl-confirm-view:hover{border-color:var(--sl-muted)}
@@ -6700,6 +6768,11 @@ var CSS2 = (
6700
6768
  /* modal host */
6701
6769
  .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}
6702
6770
  .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)}
6771
+ /* The frame already clips to 16px. A picker rounding itself to --sl-radius
6772
+ (14px) inside it leaves a sliver of scrim showing at each corner, which
6773
+ reads as a rendering fault rather than as a rounded card. One owner of the
6774
+ corners, and it is the frame. */
6775
+ .sl-modal-frame > .sl-picker{border-radius:0}
6703
6776
  @media(max-width:640px){.sl-modal-scrim{padding:0}.sl-modal-frame{width:100%;height:100%;border-radius:0}}
6704
6777
  `
6705
6778
  );
@@ -6710,6 +6783,17 @@ function ensureStyle2() {
6710
6783
  el2.textContent = CSS2;
6711
6784
  document.head.appendChild(el2);
6712
6785
  }
6786
+ function formatWhen(startsAt, timezone, locale) {
6787
+ const options = { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" };
6788
+ const at = new Date(startsAt);
6789
+ if (timezone) {
6790
+ try {
6791
+ return at.toLocaleString(locale, { ...options, timeZone: timezone });
6792
+ } catch {
6793
+ }
6794
+ }
6795
+ return at.toLocaleString(locale, options);
6796
+ }
6713
6797
  function resolveTokens(chart, host) {
6714
6798
  const accent = host?.accent ?? chart?.accent ?? "#f4b740";
6715
6799
  const accentInk = host?.accentInk ?? chart?.accentInk ?? "#1a1200";
@@ -6904,6 +6988,9 @@ var SeatPicker = class _SeatPicker {
6904
6988
  currency: options.currency,
6905
6989
  flashOnLiveChange: true,
6906
6990
  colorblindSafe: this.cbSafe,
6991
+ // The drawn map's own overrides, when the host supplied them up front.
6992
+ // Everything else on `theme` is CSS and lands on the root instead.
6993
+ mapTheme: options.theme?.map ?? null,
6907
6994
  onSelectionChange: () => {
6908
6995
  this.syncTray();
6909
6996
  if (this.committedSelection().length) this.collapseSectionCard();
@@ -6992,8 +7079,8 @@ var SeatPicker = class _SeatPicker {
6992
7079
  /**
6993
7080
  * Eager sightline preview for the confirm card: the organizer's real view
6994
7081
  * photo, or — only when the chart has an actual stage to look at — a generated
6995
- * forward view plus an "≈ Nm to stage · clear sightline" line. On a stageless
6996
- * chart both the distance/sightline claim and the generic stage silhouette are
7082
+ * forward view plus an approximate distance-to-stage line. On a stageless
7083
+ * chart both the distance claim and the generic stage silhouette are
6997
7084
  * invented promises, so we suppress them; a real attached photo always shows.
6998
7085
  * (OV-52)
6999
7086
  */
@@ -7012,7 +7099,7 @@ var SeatPicker = class _SeatPicker {
7012
7099
  return "";
7013
7100
  }
7014
7101
  }
7015
- const sightHtml = hasStage ? `<div class="sl-confirm-sight"><span aria-hidden="true">\u2713</span>${distance != null ? (0, import_core2.t)("picker.sightline", { m: distance }) : this.tf("picker.sightlineClear", "Clear sightline")}</div>` : "";
7102
+ const sightHtml = hasStage && distance != null ? `<div class="sl-confirm-sight">${(0, import_core2.t)("picker.sightline", { m: distance })}</div>` : "";
7016
7103
  const viewBtn = realPhoto ? `<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="${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="${(0, import_core2.t)("picker.viewFromSeat", { label: seat.label })}"><span aria-hidden="true">\u{1F52D}</span><span>${this.tf("picker.viewFromHere", "View from here")}</span></button>`;
7017
7104
  return viewBtn + sightHtml;
7018
7105
  }
@@ -7146,6 +7233,11 @@ var SeatPicker = class _SeatPicker {
7146
7233
  this.setFsFallback(false);
7147
7234
  }
7148
7235
  }
7236
+ syncFullscreenButtons() {
7237
+ const active = !!document.fullscreenElement || this.fsFallback || this.framedFs;
7238
+ this.els.zfs?.setAttribute("aria-pressed", String(active));
7239
+ this.view3dEl?.querySelector(".sl-view3d-fs")?.setAttribute("aria-pressed", String(active));
7240
+ }
7149
7241
  /**
7150
7242
  * Native element-fullscreen was unavailable or rejected. When framed, a CSS
7151
7243
  * `.sl-fs` overlay can't escape the iframe, so we ask the host page to pin us
@@ -7160,7 +7252,7 @@ var SeatPicker = class _SeatPicker {
7160
7252
  setFramedFs(on) {
7161
7253
  if (this.framedFs === on) return;
7162
7254
  this.framedFs = on;
7163
- this.els.zfs?.setAttribute("aria-pressed", String(on || !!document.fullscreenElement));
7255
+ this.syncFullscreenButtons();
7164
7256
  this.postToHost({ type: "seatlayer:fullscreen", on });
7165
7257
  if (on && !this.fsEscHandler) {
7166
7258
  this.fsEscHandler = (e) => {
@@ -7177,7 +7269,7 @@ var SeatPicker = class _SeatPicker {
7177
7269
  if (this.fsFallback === on) return;
7178
7270
  this.fsFallback = on;
7179
7271
  this.root?.classList.toggle("sl-fs", on);
7180
- this.els.zfs?.setAttribute("aria-pressed", String(on || !!document.fullscreenElement));
7272
+ this.syncFullscreenButtons();
7181
7273
  if (on && !this.fsEscHandler) {
7182
7274
  this.fsEscHandler = (e) => {
7183
7275
  if (e.key === "Escape" && !document.fullscreenElement) this.setFsFallback(false);
@@ -7367,7 +7459,7 @@ var SeatPicker = class _SeatPicker {
7367
7459
  this.els.zfs.addEventListener("click", () => this.toggleFullscreen());
7368
7460
  this.fsChangeHandler = () => {
7369
7461
  if (!document.fullscreenElement) this.setFsFallback(false);
7370
- this.els.zfs?.setAttribute("aria-pressed", String(!!document.fullscreenElement || this.fsFallback || this.framedFs));
7462
+ this.syncFullscreenButtons();
7371
7463
  requestAnimationFrame(() => this.controller.zoomToFit());
7372
7464
  };
7373
7465
  document.addEventListener("fullscreenchange", this.fsChangeHandler);
@@ -7459,7 +7551,7 @@ var SeatPicker = class _SeatPicker {
7459
7551
  if (logoUrl) this.els.logo.innerHTML = `<img src="${logoUrl}" alt="">`;
7460
7552
  else this.els.logo.textContent = (this.opts.theme?.brandName ?? chartTheme?.brandName ?? info.eventName ?? "?").slice(0, 1).toUpperCase();
7461
7553
  this.els.name.textContent = info.eventName ?? "";
7462
- const when = info.startsAt ? new Date(info.startsAt).toLocaleString(this.opts.locale, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "";
7554
+ const when = info.startsAt ? formatWhen(info.startsAt, info.timezone ?? null, this.opts.locale) : "";
7463
7555
  this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
7464
7556
  this.buildBadge(chartTheme);
7465
7557
  const present = /* @__PURE__ */ new Set();
@@ -7712,8 +7804,12 @@ var SeatPicker = class _SeatPicker {
7712
7804
  if (this.badgeHidden(chartTheme)) return;
7713
7805
  const foot = this.els.foot;
7714
7806
  if (!foot) return;
7715
- const el2 = document.createElement("div");
7807
+ const el2 = document.createElement("a");
7716
7808
  el2.className = "sl-powered";
7809
+ el2.href = "https://seatlayer.io/?ref=picker";
7810
+ el2.target = "_blank";
7811
+ el2.rel = "noopener noreferrer";
7812
+ el2.setAttribute("aria-label", this.tf("picker.poweredBy", "Powered by SeatLayer"));
7717
7813
  el2.innerHTML = `<span class="sl-powered-mark" aria-hidden="true">` + SEATLAYER_ATTRIBUTION_MARK_SVG + `</span><span>${this.tf("picker.poweredBy", "Powered by SeatLayer")}</span>`;
7718
7814
  foot.appendChild(el2);
7719
7815
  }
@@ -8572,6 +8668,7 @@ var SeatPicker = class _SeatPicker {
8572
8668
  el2.querySelector(".sl-confirm-view")?.addEventListener("click", () => void this.openSeatView(seat));
8573
8669
  el2.querySelector(".sl-confirm-3d")?.addEventListener("click", () => {
8574
8670
  if (this.buyerView === "venue3d") {
8671
+ this.dismissConfirm();
8575
8672
  void this.view3dHandle?.flyToSeat(seat.id);
8576
8673
  } else {
8577
8674
  this.view3dReturnSeat = seat;
@@ -8650,13 +8747,24 @@ var SeatPicker = class _SeatPicker {
8650
8747
  const doc = this.controller.doc;
8651
8748
  const activeId = this.controller.getActiveFloorId();
8652
8749
  const focal = seat.focalPoint ?? doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
8653
- let panoUrl;
8750
+ let panoSource;
8654
8751
  let caption;
8655
8752
  let real = false;
8656
8753
  if (seat.viewUrl) {
8657
- panoUrl = seat.viewUrl;
8658
- caption = (0, import_core2.t)("picker.panorama360");
8659
- real = true;
8754
+ const view = {
8755
+ url: seat.viewUrl,
8756
+ ...seat.viewMeta?.previewUrl ? { previewUrl: seat.viewMeta.previewUrl } : {},
8757
+ ...seat.viewMeta?.sourceWidth !== void 0 ? { sourceWidth: seat.viewMeta.sourceWidth } : {},
8758
+ ...seat.viewMeta?.sourceHeight !== void 0 ? { sourceHeight: seat.viewMeta.sourceHeight } : {},
8759
+ ...seat.viewMeta?.previewWidth !== void 0 ? { previewWidth: seat.viewMeta.previewWidth } : {},
8760
+ ...seat.viewMeta?.previewHeight !== void 0 ? { previewHeight: seat.viewMeta.previewHeight } : {},
8761
+ ...seat.viewMeta?.coverage ? { coverage: seat.viewMeta.coverage } : {},
8762
+ ...seat.viewMeta?.capturedAt ? { capturedAt: seat.viewMeta.capturedAt } : {},
8763
+ ...seat.viewMeta?.sourceLabel ? { sourceLabel: seat.viewMeta.sourceLabel } : {}
8764
+ };
8765
+ panoSource = view;
8766
+ caption = (0, import_panorama.seatViewDisclosure)(view);
8767
+ real = (0, import_panorama.isAuthoredSeatView)(view);
8660
8768
  } else {
8661
8769
  let pano2;
8662
8770
  try {
@@ -8667,7 +8775,7 @@ var SeatPicker = class _SeatPicker {
8667
8775
  return;
8668
8776
  }
8669
8777
  if (!this.root || !this.seatViewEnabled()) return;
8670
- panoUrl = pano2.url;
8778
+ panoSource = { url: pano2.url, generated: true };
8671
8779
  caption = (0, import_core2.t)("picker.illustrationCaption", { m: pano2.distanceM });
8672
8780
  }
8673
8781
  this.closeSeatView();
@@ -8679,7 +8787,20 @@ var SeatPicker = class _SeatPicker {
8679
8787
  this.root.appendChild(el2);
8680
8788
  this.viewEl = el2;
8681
8789
  const pano = el2.querySelector(".sl-view-pano");
8682
- pano.style.backgroundImage = `url("${panoUrl}")`;
8790
+ const delivery = (0, import_panoramaDelivery.planPanoramaDelivery)(panoSource, (0, import_panoramaDelivery.browserPanoramaConstraints)());
8791
+ const loadAbort = new AbortController();
8792
+ pano.style.backgroundImage = `url("${delivery.initialUrl}")`;
8793
+ let cancelUpgrade = () => {
8794
+ };
8795
+ if (delivery.upgradeUrl) {
8796
+ cancelUpgrade = (0, import_panoramaDelivery.schedulePanoramaUpgrade)(() => {
8797
+ void (0, import_panoramaDelivery.loadPanoramaImage)(delivery.upgradeUrl, loadAbort.signal).then(() => {
8798
+ if (!el2.isConnected || loadAbort.signal.aborted) return;
8799
+ pano.style.backgroundImage = `url("${delivery.upgradeUrl}")`;
8800
+ }).catch(() => {
8801
+ });
8802
+ });
8803
+ }
8683
8804
  const VFOV_DEG = 70;
8684
8805
  const MAX_PITCH_DEG = 35;
8685
8806
  let zoom = 1;
@@ -8741,6 +8862,8 @@ var SeatPicker = class _SeatPicker {
8741
8862
  el2.addEventListener("keydown", onKey);
8742
8863
  closeBtn.focus();
8743
8864
  this.viewCleanup = () => {
8865
+ cancelUpgrade();
8866
+ loadAbort.abort();
8744
8867
  pano.removeEventListener("pointerdown", onDown);
8745
8868
  pano.removeEventListener("pointermove", onMove);
8746
8869
  pano.removeEventListener("pointerup", onUp);
@@ -9544,6 +9667,58 @@ var SeatPicker = class _SeatPicker {
9544
9667
  getSelection() {
9545
9668
  return this.committedSelection();
9546
9669
  }
9670
+ /**
9671
+ * Re-ink the DRAWN MAP after mount, so the canvas can follow a page palette
9672
+ * the host did not know at construction time.
9673
+ *
9674
+ * Deliberately narrower than the `theme` option: it takes only the map half.
9675
+ * The chrome half is CSS custom properties, which a host restyles from its
9676
+ * own stylesheet without asking the widget for anything — and a host that
9677
+ * used both mechanisms at once would have two things writing one token. This
9678
+ * method exists for the half CSS genuinely cannot reach: pixels Konva draws.
9679
+ *
9680
+ * The rebuild is the controller's (`setMapTheme`), which repaints statuses
9681
+ * from the map already in memory and restores the selection, so a buyer
9682
+ * mid-pick keeps their seats and no round trip is spent. A no-op when the
9683
+ * colours have not changed; safe to call on every render.
9684
+ */
9685
+ setMapTheme(map) {
9686
+ if (this.destroyed) return;
9687
+ this.opts.theme = { ...this.opts.theme ?? {}, map: map ?? void 0 };
9688
+ this.controller.setMapTheme(map);
9689
+ }
9690
+ /**
9691
+ * Replace the host pricing override AFTER mount, and repaint everything that
9692
+ * shows a price.
9693
+ *
9694
+ * WHY IT CANNOT JUST BE A MOUNT OPTION. The prices a host knows are often not
9695
+ * the prices it knows AT MOUNT: SeatLayer's own event page learns them from
9696
+ * `GET /pub/events/:key/availability`, a separate cached read that lands a
9697
+ * round trip after the map does — deliberately, because the map is not
9698
+ * allowed to wait on it. Remounting the widget to hand it new options would
9699
+ * tear down a live hold, so the override is settable in place.
9700
+ *
9701
+ * The prices themselves are still the SERVER'S. This method takes an answer;
9702
+ * it never derives one. Nothing here re-reads a release, a window or a quota
9703
+ * — see `paidPrice`, the one funnel every displayed price flows through, and
9704
+ * note that the checkout handoff's line items do not come from here at all:
9705
+ * they are priced by the worker from its own hold. So the worst a wrong call
9706
+ * to this method can do is misprint a price, never mischarge one.
9707
+ *
9708
+ * A no-op when the map is unchanged, so a host may call it on every poll.
9709
+ */
9710
+ setPricing(pricing) {
9711
+ const before = JSON.stringify(this.opts.pricing ?? null);
9712
+ const after = JSON.stringify(pricing ?? null);
9713
+ if (before === after) return;
9714
+ this.opts.pricing = pricing;
9715
+ if (this.destroyed || !this.els.prices) return;
9716
+ this.els.pricesSec?.querySelector(".sl-price-select")?.remove();
9717
+ this.buildPriceFilter();
9718
+ this.syncPrices();
9719
+ this.syncTray();
9720
+ if (this.lastSection) this.showSectionCard(this.lastSection);
9721
+ }
9547
9722
  /** Current colorblind-safe render state, resolved from the stored buyer
9548
9723
  * preference at mount. Host chrome (e.g. the Designer preview) reads this to
9549
9724
  * surface the state rather than rendering colorblind colors silently. */
@@ -9595,10 +9770,7 @@ var SeatPicker = class _SeatPicker {
9595
9770
  *
9596
9771
  * Entering `'venue3d'` with `opts.flyToSeatId` runs the cinematic tour: the
9597
9772
  * camera flies to the seat and holds in the live scene (a chip offers the
9598
- * 360° view-from-seat)
9599
- * the widget enters 3D and auto-flies the camera to that seat, dissolving into
9600
- * its view-from-seat panorama (the same chain as tapping "See the view from
9601
- * here" on a seat's confirm card). When the widget is **already** in the 3D
9773
+ * 360° view-from-seat). When the widget is **already** in the 3D
9602
9774
  * view, the camera simply flies to the requested seat — the GL scene is not
9603
9775
  * torn down or rebuilt, so there is no flash or re-entry.
9604
9776
  *
@@ -9607,7 +9779,7 @@ var SeatPicker = class _SeatPicker {
9607
9779
  *
9608
9780
  * @param view `'map'` for the flat picker, `'venue3d'` for the 3D venue.
9609
9781
  * @param opts.flyToSeatId When entering (or already in) `'venue3d'`, the seat
9610
- * id to fly the camera to — cinematic → panorama.
9782
+ * id to fly the camera to — cinematic → live seat view.
9611
9783
  * Ignored when `view` is `'map'`.
9612
9784
  *
9613
9785
  * @example
@@ -9621,7 +9793,12 @@ var SeatPicker = class _SeatPicker {
9621
9793
  }
9622
9794
  const flyToSeatId = opts?.flyToSeatId;
9623
9795
  if (this.buyerView === "venue3d") {
9624
- if (flyToSeatId) void this.view3dHandle?.flyToSeat(flyToSeatId);
9796
+ if (flyToSeatId) {
9797
+ this.opts.onBuyerViewChange?.({ view: "venue3d", seatId: flyToSeatId });
9798
+ void this.view3dHandle?.flyToSeat(flyToSeatId);
9799
+ } else if (opts?.resetView) {
9800
+ this.view3dHandle?.focusOverview();
9801
+ }
9625
9802
  return;
9626
9803
  }
9627
9804
  void this.enter3d(flyToSeatId);
@@ -9661,14 +9838,26 @@ var SeatPicker = class _SeatPicker {
9661
9838
  async seatViewFor3d(seatId) {
9662
9839
  const seat = this.allSeats().find((s) => s.id === seatId);
9663
9840
  if (!seat) return null;
9664
- if (seat.viewUrl) return { url: seat.viewUrl };
9841
+ if (seat.viewUrl) return {
9842
+ url: seat.viewUrl,
9843
+ ...seat.viewMeta?.previewUrl ? { previewUrl: seat.viewMeta.previewUrl } : {},
9844
+ ...seat.viewMeta?.sourceWidth !== void 0 ? { sourceWidth: seat.viewMeta.sourceWidth } : {},
9845
+ ...seat.viewMeta?.sourceHeight !== void 0 ? { sourceHeight: seat.viewMeta.sourceHeight } : {},
9846
+ ...seat.viewMeta?.previewWidth !== void 0 ? { previewWidth: seat.viewMeta.previewWidth } : {},
9847
+ ...seat.viewMeta?.previewHeight !== void 0 ? { previewHeight: seat.viewMeta.previewHeight } : {},
9848
+ ...seat.viewMeta?.initialBearingDeg !== void 0 ? { initialBearingDeg: seat.viewMeta.initialBearingDeg } : {},
9849
+ ...seat.viewMeta?.initialPitchDeg !== void 0 ? { initialPitchDeg: seat.viewMeta.initialPitchDeg } : {},
9850
+ ...seat.viewMeta?.coverage ? { coverage: seat.viewMeta.coverage } : {},
9851
+ ...seat.viewMeta?.capturedAt ? { capturedAt: seat.viewMeta.capturedAt } : {},
9852
+ ...seat.viewMeta?.sourceLabel ? { sourceLabel: seat.viewMeta.sourceLabel } : {}
9853
+ };
9665
9854
  const doc = this.controller.doc;
9666
9855
  if (!doc) return null;
9667
9856
  const activeId = this.controller.getActiveFloorId();
9668
9857
  const focal = seat.focalPoint ?? doc.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc.focalPoint ?? { x: 0, y: 0 };
9669
9858
  try {
9670
9859
  const { generateSeatPanorama } = await loadPanorama();
9671
- return { url: generateSeatPanorama(seat, focal, this.allSeats()).url };
9860
+ return { url: generateSeatPanorama(seat, focal, this.allSeats()).url, generated: true };
9672
9861
  } catch {
9673
9862
  return null;
9674
9863
  }
@@ -9690,14 +9879,26 @@ var SeatPicker = class _SeatPicker {
9690
9879
  }
9691
9880
  const seat = this.allSeats().find((s) => s.id === seatId);
9692
9881
  if (!seat) return;
9693
- const already = this.committedSelection().some((s) => s.id === seatId);
9882
+ const table = this.controller.tableSelection(seatId);
9883
+ const already = this.committedSelection().some((s) => table ? s.label === table.label : s.id === seatId);
9694
9884
  if (already) {
9695
9885
  this.controller.deselect([seatId]);
9696
9886
  this.dismissConfirm();
9697
9887
  return;
9698
9888
  }
9889
+ const added = this.controller.select([seatId]);
9890
+ if (!added.length) {
9891
+ this.syncSelectionTo3d();
9892
+ this.dismissConfirm();
9893
+ return;
9894
+ }
9895
+ this.opts.onBuyerViewChange?.({ view: "venue3d", seatId });
9699
9896
  this.flashPickedSeat(seatId);
9700
- this.controller.select([seatId]);
9897
+ if (table) {
9898
+ this.showTableDialog(table, false);
9899
+ this.syncSelectionTo3d();
9900
+ return;
9901
+ }
9701
9902
  if (this.opts.confirmSelection !== false) this.showConfirm(seat);
9702
9903
  else this.syncTray();
9703
9904
  }
@@ -9717,12 +9918,15 @@ var SeatPicker = class _SeatPicker {
9717
9918
  */
9718
9919
  buildView3dNav(overlay, handle) {
9719
9920
  const floors = handle.floors();
9720
- const zones = handle.zones().filter((z) => z.seatCount > 0);
9721
- const sections = zones.length > 1 ? [] : handle.sections().filter((s) => s.seatCount > 0).slice(0, MAX_3D_SECTION_PILLS);
9921
+ const floorLabels = new Set(floors.map((floor) => floor.label.trim().toLocaleLowerCase()));
9922
+ const zones = handle.zones().filter((zone) => zone.seatCount > 0 && !floorLabels.has(zone.label.trim().toLocaleLowerCase()));
9923
+ const allSections = handle.sections().filter((s) => s.seatCount > 0);
9924
+ const sections = zones.length > 1 ? [] : allSections;
9722
9925
  const wantFloors = floors.length > 1;
9723
9926
  const wantZones = zones.length > 1;
9724
9927
  const wantSections = sections.length > 1;
9725
- if (!wantFloors && !wantZones && !wantSections) return;
9928
+ const wantLocator = allSections.length > 0 && handle.rows().length > 0;
9929
+ if (!wantFloors && !wantZones && !wantSections && !wantLocator) return;
9726
9930
  const nav = document.createElement("div");
9727
9931
  nav.className = "sl-view3d-nav";
9728
9932
  if (wantFloors) {
@@ -9759,15 +9963,112 @@ var SeatPicker = class _SeatPicker {
9759
9963
  } })) : sections.map((s) => ({ id: s.id, label: s.label || s.id, go: () => {
9760
9964
  handle.focusSection(s.id);
9761
9965
  } }));
9762
- for (const e of entries) {
9763
- const b = document.createElement("button");
9764
- b.type = "button";
9765
- b.textContent = e.label;
9766
- b.addEventListener("click", e.go);
9767
- row.appendChild(b);
9966
+ if (!wantZones && entries.length > MAX_3D_SECTION_PILLS) {
9967
+ const select = document.createElement("select");
9968
+ select.setAttribute("aria-label", this.tf("picker.jumpToSection", "Jump to section"));
9969
+ const placeholder = document.createElement("option");
9970
+ placeholder.value = "";
9971
+ placeholder.textContent = this.tf("picker.jumpToSection", "Jump to section");
9972
+ select.appendChild(placeholder);
9973
+ for (const entry of entries) {
9974
+ const option = document.createElement("option");
9975
+ option.value = entry.id;
9976
+ option.textContent = entry.label;
9977
+ select.appendChild(option);
9978
+ }
9979
+ select.addEventListener("change", () => {
9980
+ entries.find((entry) => entry.id === select.value)?.go();
9981
+ });
9982
+ row.appendChild(select);
9983
+ } else {
9984
+ for (const e of entries) {
9985
+ const b = document.createElement("button");
9986
+ b.type = "button";
9987
+ b.textContent = e.label;
9988
+ b.addEventListener("click", e.go);
9989
+ row.appendChild(b);
9990
+ }
9768
9991
  }
9769
9992
  nav.appendChild(row);
9770
9993
  }
9994
+ if (wantLocator) {
9995
+ const locator = document.createElement("div");
9996
+ locator.className = "sl-view3d-locator";
9997
+ locator.setAttribute("role", "group");
9998
+ locator.setAttribute("aria-label", "Find an exact seat in 3D");
9999
+ const sectionSelect = document.createElement("select");
10000
+ sectionSelect.setAttribute("aria-label", "Choose section in 3D");
10001
+ const rowSelect = document.createElement("select");
10002
+ rowSelect.setAttribute("aria-label", "Choose row in 3D");
10003
+ const seatSelect = document.createElement("select");
10004
+ seatSelect.setAttribute("aria-label", "Choose seat in 3D");
10005
+ const view = document.createElement("button");
10006
+ view.type = "button";
10007
+ view.textContent = "View seat";
10008
+ view.disabled = true;
10009
+ const fill = (select, placeholder, entries) => {
10010
+ select.replaceChildren();
10011
+ const first = document.createElement("option");
10012
+ first.value = "";
10013
+ first.textContent = placeholder;
10014
+ select.appendChild(first);
10015
+ for (const entry of entries) {
10016
+ const option = document.createElement("option");
10017
+ option.value = entry.id;
10018
+ option.textContent = entry.label;
10019
+ select.appendChild(option);
10020
+ }
10021
+ select.value = "";
10022
+ };
10023
+ fill(sectionSelect, "1. Section", allSections.map((section) => ({
10024
+ id: section.id,
10025
+ label: `${section.label} \xB7 ${section.seatCount.toLocaleString()} seats`
10026
+ })));
10027
+ fill(rowSelect, "2. Row", []);
10028
+ fill(seatSelect, "3. Seat", []);
10029
+ rowSelect.disabled = true;
10030
+ seatSelect.disabled = true;
10031
+ sectionSelect.addEventListener("change", () => {
10032
+ const sectionId = sectionSelect.value;
10033
+ view.disabled = true;
10034
+ fill(seatSelect, "3. Seat", []);
10035
+ seatSelect.disabled = true;
10036
+ if (!sectionId || !handle.focusSection(sectionId)) {
10037
+ fill(rowSelect, "2. Row", []);
10038
+ rowSelect.disabled = true;
10039
+ return;
10040
+ }
10041
+ const rows = handle.rows(sectionId);
10042
+ fill(rowSelect, "2. Row", rows.map((row) => ({
10043
+ id: row.id,
10044
+ label: `${row.label} \xB7 ${row.seatCount} seats`
10045
+ })));
10046
+ rowSelect.disabled = rows.length === 0;
10047
+ });
10048
+ rowSelect.addEventListener("change", () => {
10049
+ const rowId = rowSelect.value;
10050
+ view.disabled = true;
10051
+ if (!rowId || !handle.focusRow(rowId)) {
10052
+ fill(seatSelect, "3. Seat", []);
10053
+ seatSelect.disabled = true;
10054
+ return;
10055
+ }
10056
+ const seats = handle.seatsInRow(rowId);
10057
+ fill(seatSelect, "3. Seat", seats);
10058
+ seatSelect.disabled = seats.length === 0;
10059
+ });
10060
+ seatSelect.addEventListener("change", () => {
10061
+ const seatId = seatSelect.value;
10062
+ view.disabled = !seatId;
10063
+ handle.setSelection(seatId ? [seatId] : []);
10064
+ });
10065
+ view.addEventListener("click", () => {
10066
+ const seatId = seatSelect.value;
10067
+ if (seatId) void handle.flyToSeat(seatId);
10068
+ });
10069
+ locator.append(sectionSelect, rowSelect, seatSelect, view);
10070
+ nav.appendChild(locator);
10071
+ }
9771
10072
  overlay.appendChild(nav);
9772
10073
  }
9773
10074
  async enter3d(flySeatId) {
@@ -9775,6 +10076,7 @@ var SeatPicker = class _SeatPicker {
9775
10076
  const doc = this.controller.doc;
9776
10077
  if (!doc) return;
9777
10078
  this.buyerView = "venue3d";
10079
+ this.opts.onBuyerViewChange?.({ view: "venue3d", ...flySeatId ? { seatId: flySeatId } : {} });
9778
10080
  this.root?.setAttribute("data-view3d", "on");
9779
10081
  this.dismissConfirm();
9780
10082
  this.syncProjection();
@@ -9788,6 +10090,20 @@ var SeatPicker = class _SeatPicker {
9788
10090
  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>`;
9789
10091
  back.addEventListener("click", () => this.exit3d());
9790
10092
  overlay.appendChild(back);
10093
+ const fullscreen = document.createElement("button");
10094
+ fullscreen.type = "button";
10095
+ fullscreen.className = "sl-view3d-fs";
10096
+ fullscreen.textContent = "\u26F6";
10097
+ fullscreen.setAttribute("aria-label", "Full screen");
10098
+ fullscreen.setAttribute("aria-pressed", String(!!document.fullscreenElement || this.fsFallback || this.framedFs));
10099
+ fullscreen.addEventListener("click", () => this.toggleFullscreen());
10100
+ overlay.appendChild(fullscreen);
10101
+ const loading = document.createElement("div");
10102
+ loading.className = "sl-view3d-loading";
10103
+ loading.setAttribute("role", "status");
10104
+ loading.setAttribute("aria-live", "polite");
10105
+ loading.textContent = this.tf("picker.loading3d", "Building the 3D venue\u2026");
10106
+ overlay.appendChild(loading);
9791
10107
  this.els.map.appendChild(overlay);
9792
10108
  this.view3dEl = overlay;
9793
10109
  requestAnimationFrame(() => {
@@ -9798,8 +10114,30 @@ var SeatPicker = class _SeatPicker {
9798
10114
  const seats = (0, import_core2.expandChart)(doc);
9799
10115
  const mod = await loadVenue3d();
9800
10116
  if (gen !== this.view3dGen || this.buyerView !== "venue3d" || this.view3dEl !== overlay) return;
9801
- const handle = mod.mountVenue3D(overlay, { doc, seats }, {
10117
+ const prepared = await mod.prepareVenue3D({ doc, seats });
10118
+ if (gen !== this.view3dGen || this.buyerView !== "venue3d" || this.view3dEl !== overlay) return;
10119
+ const handle = mod.mountVenue3D(overlay, { doc, seats, prepared }, {
10120
+ // A strict contain fit turns a wide bowl into a postage stamp inside a
10121
+ // phone. The bounded portrait fit was validated against every UX-lab
10122
+ // fixture; keep editor previews strict while making buyer seats legible.
10123
+ portraitOverviewCrop: true,
9802
10124
  onSeatPick: (id) => this.onView3dSeatPick(id),
10125
+ onSectionFocusChange: (sectionId) => {
10126
+ const select = overlay.querySelector(
10127
+ 'select[aria-label="Choose section in 3D"]'
10128
+ );
10129
+ const next = sectionId ?? "";
10130
+ if (!select || select.value === next) return;
10131
+ select.value = next;
10132
+ select.dispatchEvent(new Event("change"));
10133
+ },
10134
+ onViewTargetChange: (seatId) => {
10135
+ overlay.querySelector(".sl-view3d-nav")?.classList.toggle("is-seat-focused", !!seatId);
10136
+ this.opts.onBuyerViewChange?.({
10137
+ view: "venue3d",
10138
+ ...seatId ? { seatId } : {}
10139
+ });
10140
+ },
9803
10141
  // Deferred off the tap gesture: generateSeatPanorama walks every seat
9804
10142
  // (O(n) on a 13k chart), and the module prefetches at pick — by the
9805
10143
  // time the flight lands (~2.5s) the idle render has long finished. A
@@ -9819,12 +10157,15 @@ var SeatPicker = class _SeatPicker {
9819
10157
  onAnalytics: (event, props) => this.emit3dAnalytics(event, props)
9820
10158
  });
9821
10159
  this.view3dHandle = handle;
10160
+ loading.remove();
9822
10161
  this.pushAvailabilityTo3d();
9823
10162
  this.syncSelectionTo3d();
9824
10163
  this.buildView3dNav(overlay, handle);
9825
10164
  if (flySeatId) void handle.flyToSeat(flySeatId);
9826
10165
  } catch (err) {
10166
+ if (gen !== this.view3dGen || this.buyerView !== "venue3d" || this.view3dEl !== overlay) return;
9827
10167
  this.opts.onError?.(err);
10168
+ this.toast(this.tf("picker.unavailable3d", "3D could not start. The seat map is still available."), "warning");
9828
10169
  this.exit3d();
9829
10170
  }
9830
10171
  }
@@ -9832,6 +10173,7 @@ var SeatPicker = class _SeatPicker {
9832
10173
  if (this.buyerView !== "venue3d" && !this.view3dEl) return;
9833
10174
  this.view3dGen++;
9834
10175
  this.buyerView = "map";
10176
+ this.opts.onBuyerViewChange?.({ view: "map" });
9835
10177
  this.root?.removeAttribute("data-view3d");
9836
10178
  try {
9837
10179
  this.view3dHandle?.dispose();