@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.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`, {
@@ -4838,7 +4865,6 @@ var SeatingChart = class {
4838
4865
  this.mount = null;
4839
4866
  this.hostEl = null;
4840
4867
  this.rendered = false;
4841
- this.retryBtn = null;
4842
4868
  this.mode_ = null;
4843
4869
  this.tipEl = null;
4844
4870
  this.tipPos = { x: 0, y: 0 };
@@ -5213,14 +5239,12 @@ var SeatingChart = class {
5213
5239
  void this.render().catch((err) => this.opts.onError?.(err));
5214
5240
  });
5215
5241
  box.appendChild(btn);
5216
- this.retryBtn = btn;
5217
5242
  host.appendChild(box);
5218
5243
  }
5219
5244
  destroy() {
5220
5245
  this.realtime?.stop();
5221
5246
  this.realtime = null;
5222
5247
  this.access?.clear();
5223
- this.retryBtn = null;
5224
5248
  if (this.hostEl && this.onTipMove) this.hostEl.removeEventListener("mousemove", this.onTipMove);
5225
5249
  this.tipEl = null;
5226
5250
  this.onTipMove = null;
@@ -5869,11 +5893,13 @@ var EmbeddedDesigner = class {
5869
5893
 
5870
5894
  // src/SeatPicker.ts
5871
5895
  var import_core2 = require("@seatlayer/core");
5896
+ var import_panorama = require("@seatlayer/core/view3d/crossfade/panorama");
5897
+ var import_panoramaDelivery = require("@seatlayer/core/view/panoramaDelivery");
5872
5898
  var import_meta = {};
5873
5899
  var DEFAULT_API_BASE2 = "https://api.seatlayer.io";
5874
5900
  var DEFAULT_MAX_SELECTION2 = 10;
5875
5901
  var EXTEND_PROMPT_MS = 6e4;
5876
- var MAX_3D_SEATS_DEFAULT = 15e3;
5902
+ var MAX_3D_SEATS_DEFAULT = 6e4;
5877
5903
  var MAX_3D_SECTION_PILLS = 12;
5878
5904
  function pointInPolygon(x, y, poly) {
5879
5905
  let inside = false;
@@ -6217,7 +6243,12 @@ var CSS2 = (
6217
6243
  INTO one of these positioned flex containers and flows/stacks within it, so no
6218
6244
  two pieces of chrome free-float on top of each other. Regions never overlap:
6219
6245
  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}
6246
+ /* align-items:center, not the flex default of stretch. Without it a short pill
6247
+ beside a taller control (TEST MODE next to the Map/3D toggle) is stretched to
6248
+ the row's height, so two pills that should read as a matched pair end up
6249
+ different shapes on different baselines. Column regions set their own
6250
+ cross-axis alignment below and are unaffected. */
6251
+ .sl-anchor{position:absolute;z-index:5;display:flex;align-items:center;gap:8px;pointer-events:none}
6221
6252
  .sl-anchor > *{pointer-events:auto}
6222
6253
  .sl-anchor[data-region="top-left"]{top:12px;left:12px;flex-wrap:wrap;max-width:38%}
6223
6254
  .sl-anchor[data-region="top-center"]{top:12px;left:50%;transform:translateX(-50%);flex-direction:column;
@@ -6233,10 +6264,17 @@ var CSS2 = (
6233
6264
  .sl-picker[data-layout="narrow"] .sl-anchor[data-region="top-center"]{max-width:44%}
6234
6265
 
6235
6266
  /* 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;
6267
+ /* align-self:stretch, not a hardcoded height: the badge sits beside the Map/3D
6268
+ toggle, whose height comes from its own border + padding + button metrics.
6269
+ Stretching matches that row exactly and keeps matching if the toggle ever
6270
+ changes, while the badge's own inline-flex keeps the label centred inside
6271
+ whatever height it gets. Alone in the region it simply takes its natural
6272
+ size. */
6273
+ .sl-testbadge{display:inline-flex;align-items:center;align-self:stretch;padding:0 12px;border-radius:999px;
6274
+ font-size:10px;font-weight:800;letter-spacing:.1em;line-height:1;
6237
6275
  text-transform:uppercase;white-space:nowrap;background:var(--sl-accent);color:var(--sl-accent-ink);
6238
6276
  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}
6277
+ .sl-picker[data-layout="narrow"] .sl-testbadge{padding:0 8px;font-size:8.5px;letter-spacing:.06em}
6240
6278
 
6241
6279
  /* zoom column (flows within the bottom-right region) */
6242
6280
  .sl-zoom{display:flex;flex-direction:column;gap:6px}
@@ -6328,7 +6366,11 @@ var CSS2 = (
6328
6366
  Layered Rows mark + wordmark. Hidden when the host opts out or the org's paid
6329
6367
  theme sets hideBadge. */
6330
6368
  .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)}
6369
+ font-size:12px;font-weight:600;letter-spacing:.02em;color:var(--sl-text);opacity:.72;
6370
+ text-decoration:none;padding:6px 10px;border-radius:999px;width:fit-content;margin-inline:auto;
6371
+ transition:opacity .15s ease,background-color .15s ease}
6372
+ .sl-powered:hover{opacity:1;background:color-mix(in srgb,var(--sl-text) 8%,transparent)}
6373
+ .sl-powered:focus-visible{opacity:1;outline:2px solid var(--sl-accent);outline-offset:2px}
6332
6374
  .sl-powered-mark{width:16px;height:16px;border-radius:4px;flex:none;display:flex;align-items:center;justify-content:center;
6333
6375
  background:#0c1220;color:#fcf7ee}
6334
6376
  .sl-powered-mark svg{width:12px;height:11px}
@@ -6507,18 +6549,41 @@ var CSS2 = (
6507
6549
  .sl-view3d{position:absolute;inset:0;z-index:4;opacity:0;touch-action:none;
6508
6550
  transition:opacity .3s ease;background:radial-gradient(120% 120% at 50% 0%,#191f28 0%,#0d1014 70%)}
6509
6551
  .sl-view3d canvas{display:block;width:100%;height:100%}
6552
+ .sl-view3d canvas:focus-visible{outline:2px solid var(--sl-accent);outline-offset:-3px}
6553
+ .sl-view3d-loading{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:10px;
6554
+ z-index:1;color:#d9e2f2;font-size:13px;font-weight:700;letter-spacing:.02em;pointer-events:none}
6555
+ .sl-view3d-loading::before{content:"";width:18px;height:18px;border-radius:50%;
6556
+ border:2px solid rgba(217,226,242,.28);border-top-color:#d9e2f2;animation:slSpin .8s linear infinite}
6510
6557
  [data-view3d=on] .sl-chips,[data-view3d=on] .sl-rungs{display:none}
6511
6558
  .sl-view3d-back{position:absolute;top:12px;left:12px;z-index:2;display:inline-flex;align-items:center;gap:6px;
6512
6559
  padding:7px 13px 7px 9px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.03em;
6513
6560
  color:#e6edf3;background:rgba(10,14,20,.62);border:1px solid rgba(255,255,255,.22);backdrop-filter:blur(6px)}
6514
6561
  .sl-view3d-back:hover,.sl-view3d-back:focus-visible{background:rgba(16,22,30,.82);border-color:rgba(255,255,255,.4)}
6515
6562
  .sl-view3d-back svg{width:15px;height:15px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round;stroke-linejoin:round}
6563
+ .sl-view3d-fs{position:absolute;top:12px;right:12px;z-index:2;min-width:44px;min-height:44px;padding:8px 12px;
6564
+ border-radius:999px;font-size:16px;color:#e6edf3;background:rgba(10,14,20,.62);
6565
+ border:1px solid rgba(255,255,255,.22);backdrop-filter:blur(6px)}
6566
+ .sl-view3d-fs:hover,.sl-view3d-fs:focus-visible{background:rgba(16,22,30,.82);border-color:rgba(255,255,255,.4)}
6516
6567
  /* Venue navigation inside 3D: levels (isolate a floor) and areas (fly to a zone).
6517
6568
  Bottom-LEFT, clear of the module's own chips (Overview bottom-right, 360
6518
6569
  bottom-centre) and of the bottom-sheeted confirm card. Horizontally scrollable
6519
- so a venue with many zones never pushes the rail off a phone screen. */
6570
+ so a venue with many zones never pushes the rail off a phone screen.
6571
+
6572
+ EVERY WIDTH HERE IS RELATIVE TO THE RAIL, NEVER TO THE WINDOW. These three
6573
+ rules used to size off 100vw, which contradicts the one contract this widget
6574
+ states about itself: it adapts to a full-screen takeover, an inline div in a
6575
+ content page, or a popup, and its breakpoints key off the CONTAINER, never the
6576
+ viewport. A 390 px picker embedded in a 1400 px page asked for
6577
+ calc(100vw - 180px) = 1220 inside a 390 px rail.
6578
+
6579
+ MEASURED, IT DID NOT OVERFLOW: the scroll parent is a flex container, so the
6580
+ over-large declaration was shrunk back to the rail and both the old and new
6581
+ rules resolve to 364 px in situ. So this is a latent correctness fix, not a
6582
+ visible bug -- the numbers were wrong and were being covered for by a
6583
+ flex-shrink one level up. Left as 100% because the next person to change that
6584
+ parent's display should not inherit a rule that only works by accident. */
6520
6585
  .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}
6586
+ max-width:calc(100% - 150px);pointer-events:none}
6522
6587
  .sl-view3d-nav > div{display:flex;gap:6px;overflow-x:auto;scrollbar-width:none;pointer-events:auto;
6523
6588
  padding:1px;-webkit-overflow-scrolling:touch}
6524
6589
  .sl-view3d-nav > div::-webkit-scrollbar{display:none}
@@ -6527,6 +6592,21 @@ var CSS2 = (
6527
6592
  border:1px solid rgba(150,165,205,.35);backdrop-filter:blur(6px);cursor:pointer}
6528
6593
  .sl-view3d-nav button:hover,.sl-view3d-nav button:focus-visible{color:#eef1f8;border-color:rgba(190,205,240,.6)}
6529
6594
  .sl-view3d-nav button[aria-pressed="true"]{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
6595
+ .sl-view3d-nav button:disabled{opacity:.45;cursor:not-allowed}
6596
+ .sl-view3d-nav select{min-height:38px;max-width:100%;padding:7px 34px 7px 12px;
6597
+ border-radius:999px;font:700 11.5px/1 inherit;color:#eef1f8;background:rgba(12,18,32,.86);
6598
+ border:1px solid rgba(150,165,205,.45);backdrop-filter:blur(6px);cursor:pointer}
6599
+ .sl-view3d-nav select:focus-visible{outline:2px solid var(--sl-accent);outline-offset:2px}
6600
+ .sl-view3d-nav .sl-view3d-locator{display:grid;grid-template-columns:minmax(150px,1.25fr) minmax(110px,.8fr) minmax(105px,.7fr) auto;
6601
+ width:min(720px,100%);overflow:visible}
6602
+ .sl-view3d-nav.is-seat-focused .sl-view3d-locator{display:none}
6603
+ .sl-view3d-locator select{width:100%;min-width:0}
6604
+ .sl-picker[data-layout="narrow"] .sl-view3d-nav .sl-view3d-locator{display:flex;width:100%;overflow-x:auto}
6605
+ .sl-picker[data-layout="narrow"] .sl-view3d-locator select{flex:0 0 155px}
6606
+ /* On phones the library owns the bottom edge for seat/panorama/overview actions.
6607
+ Keep venue navigation in a separate top rail so those control families never
6608
+ stack over one another. */
6609
+ .sl-picker[data-layout="narrow"] .sl-view3d-nav{top:68px;bottom:auto;max-width:calc(100% - 24px)}
6530
6610
  /* While immersed, the 2D-only chrome is meaningless \u2014 hide it, keep Map|3D. */
6531
6611
  .sl-picker[data-view3d="on"] .sl-rungs,
6532
6612
  .sl-picker[data-view3d="on"] .sl-floors,
@@ -6605,7 +6685,6 @@ var CSS2 = (
6605
6685
  .sl-confirm-thumb-badge{position:absolute;right:7px;top:7px;display:inline-flex;align-items:center;gap:5px;
6606
6686
  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
6687
  .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
6688
  .sl-confirm-view{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
6610
6689
  color:var(--sl-text);font-weight:700;font-size:12px;display:flex;align-items:center;justify-content:center;gap:7px}
6611
6690
  .sl-confirm-view:hover{border-color:var(--sl-muted)}
@@ -6700,6 +6779,11 @@ var CSS2 = (
6700
6779
  /* modal host */
6701
6780
  .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
6781
  .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)}
6782
+ /* The frame already clips to 16px. A picker rounding itself to --sl-radius
6783
+ (14px) inside it leaves a sliver of scrim showing at each corner, which
6784
+ reads as a rendering fault rather than as a rounded card. One owner of the
6785
+ corners, and it is the frame. */
6786
+ .sl-modal-frame > .sl-picker{border-radius:0}
6703
6787
  @media(max-width:640px){.sl-modal-scrim{padding:0}.sl-modal-frame{width:100%;height:100%;border-radius:0}}
6704
6788
  `
6705
6789
  );
@@ -6710,6 +6794,17 @@ function ensureStyle2() {
6710
6794
  el2.textContent = CSS2;
6711
6795
  document.head.appendChild(el2);
6712
6796
  }
6797
+ function formatWhen(startsAt, timezone, locale) {
6798
+ const options = { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" };
6799
+ const at = new Date(startsAt);
6800
+ if (timezone) {
6801
+ try {
6802
+ return at.toLocaleString(locale, { ...options, timeZone: timezone });
6803
+ } catch {
6804
+ }
6805
+ }
6806
+ return at.toLocaleString(locale, options);
6807
+ }
6713
6808
  function resolveTokens(chart, host) {
6714
6809
  const accent = host?.accent ?? chart?.accent ?? "#f4b740";
6715
6810
  const accentInk = host?.accentInk ?? chart?.accentInk ?? "#1a1200";
@@ -6904,6 +6999,9 @@ var SeatPicker = class _SeatPicker {
6904
6999
  currency: options.currency,
6905
7000
  flashOnLiveChange: true,
6906
7001
  colorblindSafe: this.cbSafe,
7002
+ // The drawn map's own overrides, when the host supplied them up front.
7003
+ // Everything else on `theme` is CSS and lands on the root instead.
7004
+ mapTheme: options.theme?.map ?? null,
6907
7005
  onSelectionChange: () => {
6908
7006
  this.syncTray();
6909
7007
  if (this.committedSelection().length) this.collapseSectionCard();
@@ -6992,8 +7090,8 @@ var SeatPicker = class _SeatPicker {
6992
7090
  /**
6993
7091
  * Eager sightline preview for the confirm card: the organizer's real view
6994
7092
  * 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
7093
+ * forward view plus an approximate distance-to-stage line. On a stageless
7094
+ * chart both the distance claim and the generic stage silhouette are
6997
7095
  * invented promises, so we suppress them; a real attached photo always shows.
6998
7096
  * (OV-52)
6999
7097
  */
@@ -7012,7 +7110,7 @@ var SeatPicker = class _SeatPicker {
7012
7110
  return "";
7013
7111
  }
7014
7112
  }
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>` : "";
7113
+ const sightHtml = hasStage && distance != null ? `<div class="sl-confirm-sight">${(0, import_core2.t)("picker.sightline", { m: distance })}</div>` : "";
7016
7114
  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
7115
  return viewBtn + sightHtml;
7018
7116
  }
@@ -7146,6 +7244,11 @@ var SeatPicker = class _SeatPicker {
7146
7244
  this.setFsFallback(false);
7147
7245
  }
7148
7246
  }
7247
+ syncFullscreenButtons() {
7248
+ const active = !!document.fullscreenElement || this.fsFallback || this.framedFs;
7249
+ this.els.zfs?.setAttribute("aria-pressed", String(active));
7250
+ this.view3dEl?.querySelector(".sl-view3d-fs")?.setAttribute("aria-pressed", String(active));
7251
+ }
7149
7252
  /**
7150
7253
  * Native element-fullscreen was unavailable or rejected. When framed, a CSS
7151
7254
  * `.sl-fs` overlay can't escape the iframe, so we ask the host page to pin us
@@ -7160,7 +7263,7 @@ var SeatPicker = class _SeatPicker {
7160
7263
  setFramedFs(on) {
7161
7264
  if (this.framedFs === on) return;
7162
7265
  this.framedFs = on;
7163
- this.els.zfs?.setAttribute("aria-pressed", String(on || !!document.fullscreenElement));
7266
+ this.syncFullscreenButtons();
7164
7267
  this.postToHost({ type: "seatlayer:fullscreen", on });
7165
7268
  if (on && !this.fsEscHandler) {
7166
7269
  this.fsEscHandler = (e) => {
@@ -7177,7 +7280,7 @@ var SeatPicker = class _SeatPicker {
7177
7280
  if (this.fsFallback === on) return;
7178
7281
  this.fsFallback = on;
7179
7282
  this.root?.classList.toggle("sl-fs", on);
7180
- this.els.zfs?.setAttribute("aria-pressed", String(on || !!document.fullscreenElement));
7283
+ this.syncFullscreenButtons();
7181
7284
  if (on && !this.fsEscHandler) {
7182
7285
  this.fsEscHandler = (e) => {
7183
7286
  if (e.key === "Escape" && !document.fullscreenElement) this.setFsFallback(false);
@@ -7367,7 +7470,7 @@ var SeatPicker = class _SeatPicker {
7367
7470
  this.els.zfs.addEventListener("click", () => this.toggleFullscreen());
7368
7471
  this.fsChangeHandler = () => {
7369
7472
  if (!document.fullscreenElement) this.setFsFallback(false);
7370
- this.els.zfs?.setAttribute("aria-pressed", String(!!document.fullscreenElement || this.fsFallback || this.framedFs));
7473
+ this.syncFullscreenButtons();
7371
7474
  requestAnimationFrame(() => this.controller.zoomToFit());
7372
7475
  };
7373
7476
  document.addEventListener("fullscreenchange", this.fsChangeHandler);
@@ -7384,10 +7487,21 @@ var SeatPicker = class _SeatPicker {
7384
7487
  e.stopPropagation();
7385
7488
  setSheet(root.dataset.sheet !== "open");
7386
7489
  });
7490
+ this.els.peek?.addEventListener("click", (e) => {
7491
+ const go = e.target.closest(".sl-sheet-go");
7492
+ if (!go) return;
7493
+ e.stopPropagation();
7494
+ if (go.dataset.act === "checkout") void this.handleCta();
7495
+ else setSheet(true);
7496
+ });
7387
7497
  let startY = 0;
7388
7498
  let swiped = false;
7389
7499
  let tracking = false;
7390
7500
  head.addEventListener("pointerdown", (e) => {
7501
+ if (e.target.closest?.(".sl-seccard,.sl-sheet-toggle,.sl-sheet-go")) {
7502
+ tracking = false;
7503
+ return;
7504
+ }
7391
7505
  tracking = true;
7392
7506
  swiped = false;
7393
7507
  startY = e.clientY;
@@ -7406,7 +7520,7 @@ var SeatPicker = class _SeatPicker {
7406
7520
  });
7407
7521
  head.addEventListener("pointerup", (e) => {
7408
7522
  if (tracking && !swiped && Math.abs(e.clientY - startY) < 6) {
7409
- if (!e.target.closest(".sl-seccard,.sl-sheet-toggle")) setSheet(root.dataset.sheet !== "open");
7523
+ setSheet(root.dataset.sheet !== "open");
7410
7524
  }
7411
7525
  tracking = false;
7412
7526
  head.releasePointerCapture?.(e.pointerId);
@@ -7459,7 +7573,7 @@ var SeatPicker = class _SeatPicker {
7459
7573
  if (logoUrl) this.els.logo.innerHTML = `<img src="${logoUrl}" alt="">`;
7460
7574
  else this.els.logo.textContent = (this.opts.theme?.brandName ?? chartTheme?.brandName ?? info.eventName ?? "?").slice(0, 1).toUpperCase();
7461
7575
  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" }) : "";
7576
+ const when = info.startsAt ? formatWhen(info.startsAt, info.timezone ?? null, this.opts.locale) : "";
7463
7577
  this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
7464
7578
  this.buildBadge(chartTheme);
7465
7579
  const present = /* @__PURE__ */ new Set();
@@ -7600,12 +7714,11 @@ var SeatPicker = class _SeatPicker {
7600
7714
  const narrow = this.root?.dataset.layout === "narrow";
7601
7715
  const filters = this.els.filters;
7602
7716
  if (filters) {
7717
+ if (this.cbEl) this.els.zoom?.appendChild(this.cbEl);
7603
7718
  if (narrow) {
7604
7719
  if (this.a11yChipsEl) filters.appendChild(this.a11yChipsEl);
7605
- if (this.cbEl) filters.appendChild(this.cbEl);
7606
7720
  } else {
7607
7721
  if (this.a11yChipsEl) this.regions["top-left"]?.appendChild(this.a11yChipsEl);
7608
- if (this.cbEl) this.els.zoom?.appendChild(this.cbEl);
7609
7722
  }
7610
7723
  const has = narrow && filters.children.length > 0;
7611
7724
  filters.classList.toggle("has", has);
@@ -7712,8 +7825,12 @@ var SeatPicker = class _SeatPicker {
7712
7825
  if (this.badgeHidden(chartTheme)) return;
7713
7826
  const foot = this.els.foot;
7714
7827
  if (!foot) return;
7715
- const el2 = document.createElement("div");
7828
+ const el2 = document.createElement("a");
7716
7829
  el2.className = "sl-powered";
7830
+ el2.href = "https://seatlayer.io/?ref=picker";
7831
+ el2.target = "_blank";
7832
+ el2.rel = "noopener noreferrer";
7833
+ el2.setAttribute("aria-label", this.tf("picker.poweredBy", "Powered by SeatLayer"));
7717
7834
  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
7835
  foot.appendChild(el2);
7719
7836
  }
@@ -8572,6 +8689,7 @@ var SeatPicker = class _SeatPicker {
8572
8689
  el2.querySelector(".sl-confirm-view")?.addEventListener("click", () => void this.openSeatView(seat));
8573
8690
  el2.querySelector(".sl-confirm-3d")?.addEventListener("click", () => {
8574
8691
  if (this.buyerView === "venue3d") {
8692
+ this.dismissConfirm();
8575
8693
  void this.view3dHandle?.flyToSeat(seat.id);
8576
8694
  } else {
8577
8695
  this.view3dReturnSeat = seat;
@@ -8650,13 +8768,24 @@ var SeatPicker = class _SeatPicker {
8650
8768
  const doc = this.controller.doc;
8651
8769
  const activeId = this.controller.getActiveFloorId();
8652
8770
  const focal = seat.focalPoint ?? doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
8653
- let panoUrl;
8771
+ let panoSource;
8654
8772
  let caption;
8655
8773
  let real = false;
8656
8774
  if (seat.viewUrl) {
8657
- panoUrl = seat.viewUrl;
8658
- caption = (0, import_core2.t)("picker.panorama360");
8659
- real = true;
8775
+ const view = {
8776
+ url: seat.viewUrl,
8777
+ ...seat.viewMeta?.previewUrl ? { previewUrl: seat.viewMeta.previewUrl } : {},
8778
+ ...seat.viewMeta?.sourceWidth !== void 0 ? { sourceWidth: seat.viewMeta.sourceWidth } : {},
8779
+ ...seat.viewMeta?.sourceHeight !== void 0 ? { sourceHeight: seat.viewMeta.sourceHeight } : {},
8780
+ ...seat.viewMeta?.previewWidth !== void 0 ? { previewWidth: seat.viewMeta.previewWidth } : {},
8781
+ ...seat.viewMeta?.previewHeight !== void 0 ? { previewHeight: seat.viewMeta.previewHeight } : {},
8782
+ ...seat.viewMeta?.coverage ? { coverage: seat.viewMeta.coverage } : {},
8783
+ ...seat.viewMeta?.capturedAt ? { capturedAt: seat.viewMeta.capturedAt } : {},
8784
+ ...seat.viewMeta?.sourceLabel ? { sourceLabel: seat.viewMeta.sourceLabel } : {}
8785
+ };
8786
+ panoSource = view;
8787
+ caption = (0, import_panorama.seatViewDisclosure)(view);
8788
+ real = (0, import_panorama.isAuthoredSeatView)(view);
8660
8789
  } else {
8661
8790
  let pano2;
8662
8791
  try {
@@ -8667,7 +8796,7 @@ var SeatPicker = class _SeatPicker {
8667
8796
  return;
8668
8797
  }
8669
8798
  if (!this.root || !this.seatViewEnabled()) return;
8670
- panoUrl = pano2.url;
8799
+ panoSource = { url: pano2.url, generated: true };
8671
8800
  caption = (0, import_core2.t)("picker.illustrationCaption", { m: pano2.distanceM });
8672
8801
  }
8673
8802
  this.closeSeatView();
@@ -8679,7 +8808,20 @@ var SeatPicker = class _SeatPicker {
8679
8808
  this.root.appendChild(el2);
8680
8809
  this.viewEl = el2;
8681
8810
  const pano = el2.querySelector(".sl-view-pano");
8682
- pano.style.backgroundImage = `url("${panoUrl}")`;
8811
+ const delivery = (0, import_panoramaDelivery.planPanoramaDelivery)(panoSource, (0, import_panoramaDelivery.browserPanoramaConstraints)());
8812
+ const loadAbort = new AbortController();
8813
+ pano.style.backgroundImage = `url("${delivery.initialUrl}")`;
8814
+ let cancelUpgrade = () => {
8815
+ };
8816
+ if (delivery.upgradeUrl) {
8817
+ cancelUpgrade = (0, import_panoramaDelivery.schedulePanoramaUpgrade)(() => {
8818
+ void (0, import_panoramaDelivery.loadPanoramaImage)(delivery.upgradeUrl, loadAbort.signal).then(() => {
8819
+ if (!el2.isConnected || loadAbort.signal.aborted) return;
8820
+ pano.style.backgroundImage = `url("${delivery.upgradeUrl}")`;
8821
+ }).catch(() => {
8822
+ });
8823
+ });
8824
+ }
8683
8825
  const VFOV_DEG = 70;
8684
8826
  const MAX_PITCH_DEG = 35;
8685
8827
  let zoom = 1;
@@ -8741,6 +8883,8 @@ var SeatPicker = class _SeatPicker {
8741
8883
  el2.addEventListener("keydown", onKey);
8742
8884
  closeBtn.focus();
8743
8885
  this.viewCleanup = () => {
8886
+ cancelUpgrade();
8887
+ loadAbort.abort();
8744
8888
  pano.removeEventListener("pointerdown", onDown);
8745
8889
  pano.removeEventListener("pointermove", onMove);
8746
8890
  pano.removeEventListener("pointerup", onUp);
@@ -9105,10 +9249,11 @@ var SeatPicker = class _SeatPicker {
9105
9249
  if (previousCount === 0 && count > 0) this.animateOnce(this.els.cta, "sl-ready", 520);
9106
9250
  if (this.els.peek) {
9107
9251
  if (count) {
9108
- 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>`;
9252
+ const holding = !!this.hold && !pendingCount;
9253
+ 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>`;
9109
9254
  } else {
9110
9255
  const prices = (this.controller.doc?.categories ?? []).map((c) => this.catPrice(c)).filter((p) => p != null);
9111
- 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>`;
9256
+ 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>`;
9112
9257
  }
9113
9258
  }
9114
9259
  this.lastTrayCount = count;
@@ -9544,6 +9689,58 @@ var SeatPicker = class _SeatPicker {
9544
9689
  getSelection() {
9545
9690
  return this.committedSelection();
9546
9691
  }
9692
+ /**
9693
+ * Re-ink the DRAWN MAP after mount, so the canvas can follow a page palette
9694
+ * the host did not know at construction time.
9695
+ *
9696
+ * Deliberately narrower than the `theme` option: it takes only the map half.
9697
+ * The chrome half is CSS custom properties, which a host restyles from its
9698
+ * own stylesheet without asking the widget for anything — and a host that
9699
+ * used both mechanisms at once would have two things writing one token. This
9700
+ * method exists for the half CSS genuinely cannot reach: pixels Konva draws.
9701
+ *
9702
+ * The rebuild is the controller's (`setMapTheme`), which repaints statuses
9703
+ * from the map already in memory and restores the selection, so a buyer
9704
+ * mid-pick keeps their seats and no round trip is spent. A no-op when the
9705
+ * colours have not changed; safe to call on every render.
9706
+ */
9707
+ setMapTheme(map) {
9708
+ if (this.destroyed) return;
9709
+ this.opts.theme = { ...this.opts.theme ?? {}, map: map ?? void 0 };
9710
+ this.controller.setMapTheme(map);
9711
+ }
9712
+ /**
9713
+ * Replace the host pricing override AFTER mount, and repaint everything that
9714
+ * shows a price.
9715
+ *
9716
+ * WHY IT CANNOT JUST BE A MOUNT OPTION. The prices a host knows are often not
9717
+ * the prices it knows AT MOUNT: SeatLayer's own event page learns them from
9718
+ * `GET /pub/events/:key/availability`, a separate cached read that lands a
9719
+ * round trip after the map does — deliberately, because the map is not
9720
+ * allowed to wait on it. Remounting the widget to hand it new options would
9721
+ * tear down a live hold, so the override is settable in place.
9722
+ *
9723
+ * The prices themselves are still the SERVER'S. This method takes an answer;
9724
+ * it never derives one. Nothing here re-reads a release, a window or a quota
9725
+ * — see `paidPrice`, the one funnel every displayed price flows through, and
9726
+ * note that the checkout handoff's line items do not come from here at all:
9727
+ * they are priced by the worker from its own hold. So the worst a wrong call
9728
+ * to this method can do is misprint a price, never mischarge one.
9729
+ *
9730
+ * A no-op when the map is unchanged, so a host may call it on every poll.
9731
+ */
9732
+ setPricing(pricing) {
9733
+ const before = JSON.stringify(this.opts.pricing ?? null);
9734
+ const after = JSON.stringify(pricing ?? null);
9735
+ if (before === after) return;
9736
+ this.opts.pricing = pricing;
9737
+ if (this.destroyed || !this.els.prices) return;
9738
+ this.els.pricesSec?.querySelector(".sl-price-select")?.remove();
9739
+ this.buildPriceFilter();
9740
+ this.syncPrices();
9741
+ this.syncTray();
9742
+ if (this.lastSection) this.showSectionCard(this.lastSection);
9743
+ }
9547
9744
  /** Current colorblind-safe render state, resolved from the stored buyer
9548
9745
  * preference at mount. Host chrome (e.g. the Designer preview) reads this to
9549
9746
  * surface the state rather than rendering colorblind colors silently. */
@@ -9595,10 +9792,7 @@ var SeatPicker = class _SeatPicker {
9595
9792
  *
9596
9793
  * Entering `'venue3d'` with `opts.flyToSeatId` runs the cinematic tour: the
9597
9794
  * 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
9795
+ * 360° view-from-seat). When the widget is **already** in the 3D
9602
9796
  * view, the camera simply flies to the requested seat — the GL scene is not
9603
9797
  * torn down or rebuilt, so there is no flash or re-entry.
9604
9798
  *
@@ -9607,7 +9801,7 @@ var SeatPicker = class _SeatPicker {
9607
9801
  *
9608
9802
  * @param view `'map'` for the flat picker, `'venue3d'` for the 3D venue.
9609
9803
  * @param opts.flyToSeatId When entering (or already in) `'venue3d'`, the seat
9610
- * id to fly the camera to — cinematic → panorama.
9804
+ * id to fly the camera to — cinematic → live seat view.
9611
9805
  * Ignored when `view` is `'map'`.
9612
9806
  *
9613
9807
  * @example
@@ -9621,7 +9815,12 @@ var SeatPicker = class _SeatPicker {
9621
9815
  }
9622
9816
  const flyToSeatId = opts?.flyToSeatId;
9623
9817
  if (this.buyerView === "venue3d") {
9624
- if (flyToSeatId) void this.view3dHandle?.flyToSeat(flyToSeatId);
9818
+ if (flyToSeatId) {
9819
+ this.opts.onBuyerViewChange?.({ view: "venue3d", seatId: flyToSeatId });
9820
+ void this.view3dHandle?.flyToSeat(flyToSeatId);
9821
+ } else if (opts?.resetView) {
9822
+ this.view3dHandle?.focusOverview();
9823
+ }
9625
9824
  return;
9626
9825
  }
9627
9826
  void this.enter3d(flyToSeatId);
@@ -9661,14 +9860,26 @@ var SeatPicker = class _SeatPicker {
9661
9860
  async seatViewFor3d(seatId) {
9662
9861
  const seat = this.allSeats().find((s) => s.id === seatId);
9663
9862
  if (!seat) return null;
9664
- if (seat.viewUrl) return { url: seat.viewUrl };
9863
+ if (seat.viewUrl) return {
9864
+ url: seat.viewUrl,
9865
+ ...seat.viewMeta?.previewUrl ? { previewUrl: seat.viewMeta.previewUrl } : {},
9866
+ ...seat.viewMeta?.sourceWidth !== void 0 ? { sourceWidth: seat.viewMeta.sourceWidth } : {},
9867
+ ...seat.viewMeta?.sourceHeight !== void 0 ? { sourceHeight: seat.viewMeta.sourceHeight } : {},
9868
+ ...seat.viewMeta?.previewWidth !== void 0 ? { previewWidth: seat.viewMeta.previewWidth } : {},
9869
+ ...seat.viewMeta?.previewHeight !== void 0 ? { previewHeight: seat.viewMeta.previewHeight } : {},
9870
+ ...seat.viewMeta?.initialBearingDeg !== void 0 ? { initialBearingDeg: seat.viewMeta.initialBearingDeg } : {},
9871
+ ...seat.viewMeta?.initialPitchDeg !== void 0 ? { initialPitchDeg: seat.viewMeta.initialPitchDeg } : {},
9872
+ ...seat.viewMeta?.coverage ? { coverage: seat.viewMeta.coverage } : {},
9873
+ ...seat.viewMeta?.capturedAt ? { capturedAt: seat.viewMeta.capturedAt } : {},
9874
+ ...seat.viewMeta?.sourceLabel ? { sourceLabel: seat.viewMeta.sourceLabel } : {}
9875
+ };
9665
9876
  const doc = this.controller.doc;
9666
9877
  if (!doc) return null;
9667
9878
  const activeId = this.controller.getActiveFloorId();
9668
9879
  const focal = seat.focalPoint ?? doc.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc.focalPoint ?? { x: 0, y: 0 };
9669
9880
  try {
9670
9881
  const { generateSeatPanorama } = await loadPanorama();
9671
- return { url: generateSeatPanorama(seat, focal, this.allSeats()).url };
9882
+ return { url: generateSeatPanorama(seat, focal, this.allSeats()).url, generated: true };
9672
9883
  } catch {
9673
9884
  return null;
9674
9885
  }
@@ -9690,14 +9901,26 @@ var SeatPicker = class _SeatPicker {
9690
9901
  }
9691
9902
  const seat = this.allSeats().find((s) => s.id === seatId);
9692
9903
  if (!seat) return;
9693
- const already = this.committedSelection().some((s) => s.id === seatId);
9904
+ const table = this.controller.tableSelection(seatId);
9905
+ const already = this.committedSelection().some((s) => table ? s.label === table.label : s.id === seatId);
9694
9906
  if (already) {
9695
9907
  this.controller.deselect([seatId]);
9696
9908
  this.dismissConfirm();
9697
9909
  return;
9698
9910
  }
9911
+ const added = this.controller.select([seatId]);
9912
+ if (!added.length) {
9913
+ this.syncSelectionTo3d();
9914
+ this.dismissConfirm();
9915
+ return;
9916
+ }
9917
+ this.opts.onBuyerViewChange?.({ view: "venue3d", seatId });
9699
9918
  this.flashPickedSeat(seatId);
9700
- this.controller.select([seatId]);
9919
+ if (table) {
9920
+ this.showTableDialog(table, false);
9921
+ this.syncSelectionTo3d();
9922
+ return;
9923
+ }
9701
9924
  if (this.opts.confirmSelection !== false) this.showConfirm(seat);
9702
9925
  else this.syncTray();
9703
9926
  }
@@ -9717,12 +9940,15 @@ var SeatPicker = class _SeatPicker {
9717
9940
  */
9718
9941
  buildView3dNav(overlay, handle) {
9719
9942
  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);
9943
+ const floorLabels = new Set(floors.map((floor) => floor.label.trim().toLocaleLowerCase()));
9944
+ const zones = handle.zones().filter((zone) => zone.seatCount > 0 && !floorLabels.has(zone.label.trim().toLocaleLowerCase()));
9945
+ const allSections = handle.sections().filter((s) => s.seatCount > 0);
9946
+ const sections = zones.length > 1 ? [] : allSections;
9722
9947
  const wantFloors = floors.length > 1;
9723
9948
  const wantZones = zones.length > 1;
9724
9949
  const wantSections = sections.length > 1;
9725
- if (!wantFloors && !wantZones && !wantSections) return;
9950
+ const wantLocator = allSections.length > 0 && handle.rows().length > 0;
9951
+ if (!wantFloors && !wantZones && !wantSections && !wantLocator) return;
9726
9952
  const nav = document.createElement("div");
9727
9953
  nav.className = "sl-view3d-nav";
9728
9954
  if (wantFloors) {
@@ -9759,15 +9985,112 @@ var SeatPicker = class _SeatPicker {
9759
9985
  } })) : sections.map((s) => ({ id: s.id, label: s.label || s.id, go: () => {
9760
9986
  handle.focusSection(s.id);
9761
9987
  } }));
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);
9988
+ if (!wantZones && entries.length > MAX_3D_SECTION_PILLS) {
9989
+ const select = document.createElement("select");
9990
+ select.setAttribute("aria-label", this.tf("picker.jumpToSection", "Jump to section"));
9991
+ const placeholder = document.createElement("option");
9992
+ placeholder.value = "";
9993
+ placeholder.textContent = this.tf("picker.jumpToSection", "Jump to section");
9994
+ select.appendChild(placeholder);
9995
+ for (const entry of entries) {
9996
+ const option = document.createElement("option");
9997
+ option.value = entry.id;
9998
+ option.textContent = entry.label;
9999
+ select.appendChild(option);
10000
+ }
10001
+ select.addEventListener("change", () => {
10002
+ entries.find((entry) => entry.id === select.value)?.go();
10003
+ });
10004
+ row.appendChild(select);
10005
+ } else {
10006
+ for (const e of entries) {
10007
+ const b = document.createElement("button");
10008
+ b.type = "button";
10009
+ b.textContent = e.label;
10010
+ b.addEventListener("click", e.go);
10011
+ row.appendChild(b);
10012
+ }
9768
10013
  }
9769
10014
  nav.appendChild(row);
9770
10015
  }
10016
+ if (wantLocator) {
10017
+ const locator = document.createElement("div");
10018
+ locator.className = "sl-view3d-locator";
10019
+ locator.setAttribute("role", "group");
10020
+ locator.setAttribute("aria-label", "Find an exact seat in 3D");
10021
+ const sectionSelect = document.createElement("select");
10022
+ sectionSelect.setAttribute("aria-label", "Choose section in 3D");
10023
+ const rowSelect = document.createElement("select");
10024
+ rowSelect.setAttribute("aria-label", "Choose row in 3D");
10025
+ const seatSelect = document.createElement("select");
10026
+ seatSelect.setAttribute("aria-label", "Choose seat in 3D");
10027
+ const view = document.createElement("button");
10028
+ view.type = "button";
10029
+ view.textContent = "View seat";
10030
+ view.disabled = true;
10031
+ const fill = (select, placeholder, entries) => {
10032
+ select.replaceChildren();
10033
+ const first = document.createElement("option");
10034
+ first.value = "";
10035
+ first.textContent = placeholder;
10036
+ select.appendChild(first);
10037
+ for (const entry of entries) {
10038
+ const option = document.createElement("option");
10039
+ option.value = entry.id;
10040
+ option.textContent = entry.label;
10041
+ select.appendChild(option);
10042
+ }
10043
+ select.value = "";
10044
+ };
10045
+ fill(sectionSelect, "1. Section", allSections.map((section) => ({
10046
+ id: section.id,
10047
+ label: `${section.label} \xB7 ${section.seatCount.toLocaleString()} seats`
10048
+ })));
10049
+ fill(rowSelect, "2. Row", []);
10050
+ fill(seatSelect, "3. Seat", []);
10051
+ rowSelect.disabled = true;
10052
+ seatSelect.disabled = true;
10053
+ sectionSelect.addEventListener("change", () => {
10054
+ const sectionId = sectionSelect.value;
10055
+ view.disabled = true;
10056
+ fill(seatSelect, "3. Seat", []);
10057
+ seatSelect.disabled = true;
10058
+ if (!sectionId || !handle.focusSection(sectionId)) {
10059
+ fill(rowSelect, "2. Row", []);
10060
+ rowSelect.disabled = true;
10061
+ return;
10062
+ }
10063
+ const rows = handle.rows(sectionId);
10064
+ fill(rowSelect, "2. Row", rows.map((row) => ({
10065
+ id: row.id,
10066
+ label: `${row.label} \xB7 ${row.seatCount} seats`
10067
+ })));
10068
+ rowSelect.disabled = rows.length === 0;
10069
+ });
10070
+ rowSelect.addEventListener("change", () => {
10071
+ const rowId = rowSelect.value;
10072
+ view.disabled = true;
10073
+ if (!rowId || !handle.focusRow(rowId)) {
10074
+ fill(seatSelect, "3. Seat", []);
10075
+ seatSelect.disabled = true;
10076
+ return;
10077
+ }
10078
+ const seats = handle.seatsInRow(rowId);
10079
+ fill(seatSelect, "3. Seat", seats);
10080
+ seatSelect.disabled = seats.length === 0;
10081
+ });
10082
+ seatSelect.addEventListener("change", () => {
10083
+ const seatId = seatSelect.value;
10084
+ view.disabled = !seatId;
10085
+ handle.setSelection(seatId ? [seatId] : []);
10086
+ });
10087
+ view.addEventListener("click", () => {
10088
+ const seatId = seatSelect.value;
10089
+ if (seatId) void handle.flyToSeat(seatId);
10090
+ });
10091
+ locator.append(sectionSelect, rowSelect, seatSelect, view);
10092
+ nav.appendChild(locator);
10093
+ }
9771
10094
  overlay.appendChild(nav);
9772
10095
  }
9773
10096
  async enter3d(flySeatId) {
@@ -9775,6 +10098,7 @@ var SeatPicker = class _SeatPicker {
9775
10098
  const doc = this.controller.doc;
9776
10099
  if (!doc) return;
9777
10100
  this.buyerView = "venue3d";
10101
+ this.opts.onBuyerViewChange?.({ view: "venue3d", ...flySeatId ? { seatId: flySeatId } : {} });
9778
10102
  this.root?.setAttribute("data-view3d", "on");
9779
10103
  this.dismissConfirm();
9780
10104
  this.syncProjection();
@@ -9788,6 +10112,20 @@ var SeatPicker = class _SeatPicker {
9788
10112
  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
10113
  back.addEventListener("click", () => this.exit3d());
9790
10114
  overlay.appendChild(back);
10115
+ const fullscreen = document.createElement("button");
10116
+ fullscreen.type = "button";
10117
+ fullscreen.className = "sl-view3d-fs";
10118
+ fullscreen.textContent = "\u26F6";
10119
+ fullscreen.setAttribute("aria-label", "Full screen");
10120
+ fullscreen.setAttribute("aria-pressed", String(!!document.fullscreenElement || this.fsFallback || this.framedFs));
10121
+ fullscreen.addEventListener("click", () => this.toggleFullscreen());
10122
+ overlay.appendChild(fullscreen);
10123
+ const loading = document.createElement("div");
10124
+ loading.className = "sl-view3d-loading";
10125
+ loading.setAttribute("role", "status");
10126
+ loading.setAttribute("aria-live", "polite");
10127
+ loading.textContent = this.tf("picker.loading3d", "Building the 3D venue\u2026");
10128
+ overlay.appendChild(loading);
9791
10129
  this.els.map.appendChild(overlay);
9792
10130
  this.view3dEl = overlay;
9793
10131
  requestAnimationFrame(() => {
@@ -9798,8 +10136,30 @@ var SeatPicker = class _SeatPicker {
9798
10136
  const seats = (0, import_core2.expandChart)(doc);
9799
10137
  const mod = await loadVenue3d();
9800
10138
  if (gen !== this.view3dGen || this.buyerView !== "venue3d" || this.view3dEl !== overlay) return;
9801
- const handle = mod.mountVenue3D(overlay, { doc, seats }, {
10139
+ const prepared = await mod.prepareVenue3D({ doc, seats });
10140
+ if (gen !== this.view3dGen || this.buyerView !== "venue3d" || this.view3dEl !== overlay) return;
10141
+ const handle = mod.mountVenue3D(overlay, { doc, seats, prepared }, {
10142
+ // A strict contain fit turns a wide bowl into a postage stamp inside a
10143
+ // phone. The bounded portrait fit was validated against every UX-lab
10144
+ // fixture; keep editor previews strict while making buyer seats legible.
10145
+ portraitOverviewCrop: true,
9802
10146
  onSeatPick: (id) => this.onView3dSeatPick(id),
10147
+ onSectionFocusChange: (sectionId) => {
10148
+ const select = overlay.querySelector(
10149
+ 'select[aria-label="Choose section in 3D"]'
10150
+ );
10151
+ const next = sectionId ?? "";
10152
+ if (!select || select.value === next) return;
10153
+ select.value = next;
10154
+ select.dispatchEvent(new Event("change"));
10155
+ },
10156
+ onViewTargetChange: (seatId) => {
10157
+ overlay.querySelector(".sl-view3d-nav")?.classList.toggle("is-seat-focused", !!seatId);
10158
+ this.opts.onBuyerViewChange?.({
10159
+ view: "venue3d",
10160
+ ...seatId ? { seatId } : {}
10161
+ });
10162
+ },
9803
10163
  // Deferred off the tap gesture: generateSeatPanorama walks every seat
9804
10164
  // (O(n) on a 13k chart), and the module prefetches at pick — by the
9805
10165
  // time the flight lands (~2.5s) the idle render has long finished. A
@@ -9819,12 +10179,15 @@ var SeatPicker = class _SeatPicker {
9819
10179
  onAnalytics: (event, props) => this.emit3dAnalytics(event, props)
9820
10180
  });
9821
10181
  this.view3dHandle = handle;
10182
+ loading.remove();
9822
10183
  this.pushAvailabilityTo3d();
9823
10184
  this.syncSelectionTo3d();
9824
10185
  this.buildView3dNav(overlay, handle);
9825
10186
  if (flySeatId) void handle.flyToSeat(flySeatId);
9826
10187
  } catch (err) {
10188
+ if (gen !== this.view3dGen || this.buyerView !== "venue3d" || this.view3dEl !== overlay) return;
9827
10189
  this.opts.onError?.(err);
10190
+ this.toast(this.tf("picker.unavailable3d", "3D could not start. The seat map is still available."), "warning");
9828
10191
  this.exit3d();
9829
10192
  }
9830
10193
  }
@@ -9832,6 +10195,7 @@ var SeatPicker = class _SeatPicker {
9832
10195
  if (this.buyerView !== "venue3d" && !this.view3dEl) return;
9833
10196
  this.view3dGen++;
9834
10197
  this.buyerView = "map";
10198
+ this.opts.onBuyerViewChange?.({ view: "map" });
9835
10199
  this.root?.removeAttribute("data-view3d");
9836
10200
  try {
9837
10201
  this.view3dHandle?.dispose();