@seatlayer/js 0.47.2 → 0.48.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  SeatManager
3
- } from "./chunk-CF5MS7UR.js";
3
+ } from "./chunk-Q5QT3YLA.js";
4
4
  import {
5
5
  ACCESS_LINK_DEFAULTS,
6
6
  ChannelsMode,
@@ -28,11 +28,11 @@ import {
28
28
  selectionSources,
29
29
  stateBadge,
30
30
  suggestMarker
31
- } from "./chunk-DMEFXZIL.js";
31
+ } from "./chunk-KNMZQZXR.js";
32
32
  import {
33
33
  ManageApi,
34
34
  ManageApiError
35
- } from "./chunk-X2R4AQSZ.js";
35
+ } from "./chunk-H4OJF6LE.js";
36
36
  import {
37
37
  __privateAdd,
38
38
  __privateGet,
@@ -524,9 +524,54 @@ var PubApi = class {
524
524
  }
525
525
  return data;
526
526
  }
527
+ /**
528
+ * Binary counterpart to `request`. Buyer media needs the same in-memory
529
+ * bearer/refresh rules as JSON, but returns bytes that the picker turns into
530
+ * a blob URL. The bearer stays in the Authorization header and is never
531
+ * appended to `path`.
532
+ */
533
+ async requestBlob(path, retried = {}) {
534
+ const headers = {};
535
+ const authorization = await this.access?.authorization(retried.auth ? "unauthorized" : "initial");
536
+ if (authorization) headers.Authorization = authorization;
537
+ const res = await fetch(`${this.base}${path}`, { method: "GET", headers, credentials: "omit" });
538
+ if (res.ok) return res.blob();
539
+ const isJson = (res.headers.get("content-type") ?? "").includes("application/json");
540
+ const data = isJson ? await res.json().catch(() => null) : null;
541
+ const code = data?.code ?? data?.error;
542
+ if (this.access?.configured && (res.status === 401 || res.status === 403 || res.status === 422)) {
543
+ const refreshed = await this.access.handleFailure(res.status, code);
544
+ if (refreshed && !retried.auth) return this.requestBlob(path, { ...retried, auth: true });
545
+ }
546
+ let retryAfterS;
547
+ if (res.status === 429) {
548
+ retryAfterS = parseRetryAfter(res.headers.get("Retry-After"), data?.retryAfterSeconds) ?? DEFAULT_RATE_LIMIT_WAIT_S;
549
+ if (!retried.rateLimit && retryAfterS <= MAX_RATE_LIMIT_WAIT_S) {
550
+ await new Promise((resolve) => setTimeout(resolve, retryAfterS * 1e3));
551
+ return this.requestBlob(path, { ...retried, rateLimit: true });
552
+ }
553
+ }
554
+ throw new ApiError(
555
+ res.status,
556
+ data?.error ?? `request_failed_${res.status}`,
557
+ code,
558
+ void 0,
559
+ void 0,
560
+ retryAfterS
561
+ );
562
+ }
527
563
  chart(key) {
528
564
  return this.request(`/pub/events/${encodeURIComponent(key)}/chart`);
529
565
  }
566
+ /** Authenticated bytes for an Event-scoped authored view image. */
567
+ asset(key, asset) {
568
+ if (!/^[a-zA-Z0-9._-]+$/.test(asset)) {
569
+ return Promise.reject(new ApiError(404, "not_found", "not_found"));
570
+ }
571
+ return this.requestBlob(
572
+ `/pub/events/${encodeURIComponent(key)}/assets/${encodeURIComponent(asset)}`
573
+ );
574
+ }
530
575
  objects(key) {
531
576
  return this.request(`/pub/events/${encodeURIComponent(key)}/objects?compact=1`);
532
577
  }
@@ -629,12 +674,11 @@ var PubApi = class {
629
674
  /**
630
675
  * What PickerController opens its own socket with.
631
676
  *
632
- * Empty for an access-scoped client: a private scope authenticates with a
677
+ * Empty for an access-scoped client: a scoped audience authenticates with a
633
678
  * subprotocol ticket, which a URL-only constructor cannot carry, so the SDK's
634
679
  * BuyerRealtimeClient owns that socket instead and the controller skips its
635
680
  * own (an empty URL is its documented "no live feed" contract). A tokenless
636
- * public client returns exactly the URL it always has, so nothing about the
637
- * public picker's realtime path changes.
681
+ * Managed public client returns exactly the URL it always has.
638
682
  */
639
683
  socketUrl(key) {
640
684
  return this.accessScoped ? "" : this.subscribeUrl(key);
@@ -1994,6 +2038,81 @@ import {
1994
2038
  schedulePanoramaUpgrade
1995
2039
  } from "@seatlayer/core/view/panoramaDelivery";
1996
2040
 
2041
+ // src/buyerAssets.ts
2042
+ var SAFE_ASSET = /^[a-zA-Z0-9._-]+$/;
2043
+ function buyerEventAssetReference(value) {
2044
+ let url;
2045
+ try {
2046
+ url = new URL(value, "https://seatlayer.invalid");
2047
+ } catch {
2048
+ return null;
2049
+ }
2050
+ if (url.search || url.hash) return null;
2051
+ const match = /^\/pub\/events\/([^/]+)\/assets\/([^/]+)$/.exec(url.pathname);
2052
+ if (!match) return null;
2053
+ try {
2054
+ const eventKey = decodeURIComponent(match[1]);
2055
+ const asset = decodeURIComponent(match[2]);
2056
+ if (!eventKey || !SAFE_ASSET.test(asset)) return null;
2057
+ return { eventKey, asset };
2058
+ } catch {
2059
+ return null;
2060
+ }
2061
+ }
2062
+ function looksLikeBuyerAsset(value) {
2063
+ try {
2064
+ return /^\/pub\/events\/[^/]+\/assets(?:\/|$)/.test(
2065
+ new URL(value, "https://seatlayer.invalid").pathname
2066
+ );
2067
+ } catch {
2068
+ return false;
2069
+ }
2070
+ }
2071
+ var BuyerAssetObjectUrls = class {
2072
+ constructor(eventKey, load) {
2073
+ this.eventKey = eventKey;
2074
+ this.load = load;
2075
+ this.pending = /* @__PURE__ */ new Map();
2076
+ this.created = /* @__PURE__ */ new Set();
2077
+ this.disposed = false;
2078
+ }
2079
+ /**
2080
+ * External organizer/CDN URLs pass through unchanged. SeatLayer event assets
2081
+ * never do: they require the transport, and a reference for another Event is
2082
+ * refused instead of being loaded anonymously.
2083
+ */
2084
+ resolve(reference) {
2085
+ const parsed = buyerEventAssetReference(reference);
2086
+ if (!parsed) {
2087
+ return Promise.resolve(looksLikeBuyerAsset(reference) ? null : reference);
2088
+ }
2089
+ if (parsed.eventKey !== this.eventKey || !this.load || this.disposed) return Promise.resolve(null);
2090
+ const existing = this.pending.get(reference);
2091
+ if (existing) return existing;
2092
+ const task = this.load(this.eventKey, parsed.asset).then((blob) => {
2093
+ const objectUrl = URL.createObjectURL(blob);
2094
+ if (this.disposed) {
2095
+ URL.revokeObjectURL(objectUrl);
2096
+ return null;
2097
+ }
2098
+ this.created.add(objectUrl);
2099
+ return objectUrl;
2100
+ }).catch((error) => {
2101
+ this.pending.delete(reference);
2102
+ throw error;
2103
+ });
2104
+ this.pending.set(reference, task);
2105
+ return task;
2106
+ }
2107
+ dispose() {
2108
+ if (this.disposed) return;
2109
+ this.disposed = true;
2110
+ for (const url of this.created) URL.revokeObjectURL(url);
2111
+ this.created.clear();
2112
+ this.pending.clear();
2113
+ }
2114
+ };
2115
+
1997
2116
  // src/offerAvailability.ts
1998
2117
  var SALE_STATES = [
1999
2118
  "on-sale",
@@ -2209,7 +2328,7 @@ async function loadHostedCheckout() {
2209
2328
  cdnChunkUrl("seatlayer-checkout.mjs")
2210
2329
  );
2211
2330
  }
2212
- return import("./hostedCheckout-SHQCWN5Y.js");
2331
+ return import("./hostedCheckout-F6C6VCCG.js");
2213
2332
  }
2214
2333
  function paymentsOffReason(reason) {
2215
2334
  return reason === "unavailable_for_event" || reason === "payments_off_for_event" ? reason : "not_configured";
@@ -2251,6 +2370,16 @@ var CSS = (
2251
2370
  .sl-close.on{display:inline-flex}
2252
2371
  .sl-close svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round}
2253
2372
 
2373
+ /* A page or popup may already own the event heading. In that case only the
2374
+ duplicate identity disappears; operational header controls stay available.
2375
+ Zero block padding lets an otherwise-empty header collapse completely. */
2376
+ .sl-picker[data-event-details-hidden="true"] .sl-head{padding-block:0;border-bottom-width:0}
2377
+ .sl-picker[data-event-details-hidden="true"] .sl-logo,
2378
+ .sl-picker[data-event-details-hidden="true"] .sl-head-info{display:none!important}
2379
+ .sl-picker[data-event-details-hidden="true"] .sl-hold-pill.on,
2380
+ .sl-picker[data-event-details-hidden="true"] .sl-closed-pill.on,
2381
+ .sl-picker[data-event-details-hidden="true"] .sl-close.on{margin-block:10px}
2382
+
2254
2383
  /* body */
2255
2384
  .sl-body{display:flex;flex:1;min-height:0}
2256
2385
  .sl-map{position:relative;flex:1;min-width:0}
@@ -2268,7 +2397,7 @@ var CSS = (
2268
2397
  .sl-picker[data-layout="narrow"] .sl-side{width:100%;border-left:0;border-top:1px solid var(--sl-line);
2269
2398
  flex:none;height:min(72%,480px);overflow:hidden;transition:height .3s cubic-bezier(.2,.8,.2,1);overscroll-behavior:contain}
2270
2399
  .sl-picker[data-layout="narrow"][data-sheet="open"][data-has-selection="false"] .sl-side{height:min(252px,52%)}
2271
- .sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side{height:86px;overflow:hidden}
2400
+ .sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side{height:76px;overflow:hidden}
2272
2401
  .sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side > :not(.sl-sheet-head){display:none}
2273
2402
  .sl-picker[data-layout="narrow"] .sl-tray{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain}
2274
2403
  .sl-picker[data-layout="narrow"] .sl-foot{position:static;background:var(--sl-bg)}
@@ -2303,9 +2432,10 @@ var CSS = (
2303
2432
  head is the tap/swipe toggle target (min 44px), so it reads as one control. */
2304
2433
  .sl-sheet-head{display:none;flex-direction:column;justify-content:center;padding:6px 12px 8px;min-height:56px;
2305
2434
  cursor:pointer;touch-action:none;user-select:none;-webkit-user-select:none;flex:none}
2306
- .sl-picker[data-layout="narrow"] .sl-sheet-head{display:flex}
2307
- .sl-sheet-grab{width:36px;height:4px;border-radius:999px;background:var(--sl-muted);opacity:.55;margin:2px auto 7px}
2308
- .sl-sheet-bar{display:flex;align-items:center;gap:10px;min-height:26px}
2435
+ .sl-picker[data-layout="narrow"] .sl-sheet-head{display:flex;min-height:64px;padding:4px 10px 6px}
2436
+ .sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-sheet-head{height:100%}
2437
+ .sl-sheet-grab{width:36px;height:4px;border-radius:999px;background:var(--sl-muted);opacity:.55;margin:1px auto 5px}
2438
+ .sl-sheet-bar{display:flex;align-items:center;gap:8px;min-height:44px}
2309
2439
  .sl-sheet-peek{display:flex;align-items:center;gap:7px;flex:1;min-width:0;font-size:13px;font-weight:700;color:var(--sl-text);
2310
2440
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
2311
2441
  .sl-sheet-peek .sub{color:var(--sl-muted);font-weight:600}
@@ -2333,7 +2463,7 @@ var CSS = (
2333
2463
  .sl-picker[data-layout="narrow"][data-has-selection="true"] .sl-filtersec.has,
2334
2464
  .sl-picker[data-layout="narrow"][data-has-selection="true"] .sl-filters.has{display:none}
2335
2465
  /* Accessibility and colour-safety controls must remain reachable on phones.
2336
- Keep them out of the 86px peek, then reveal their consolidated row whenever
2466
+ Keep them out of the collapsed peek, then reveal their consolidated row whenever
2337
2467
  the buyer explicitly opens the ticket panel. */
2338
2468
  .sl-picker[data-layout="narrow"][data-sheet="open"] .sl-filtersec.has{display:block!important}
2339
2469
  .sl-picker[data-layout="narrow"][data-sheet="open"] .sl-filters.has{display:flex!important}
@@ -2476,15 +2606,10 @@ var CSS = (
2476
2606
  .sl-picker .sl-cta:disabled{background:var(--sl-surface);color:var(--sl-muted);opacity:1;
2477
2607
  cursor:not-allowed;filter:none;transform:none}
2478
2608
 
2479
- /* Chrome anchor regions (Feature 6) \u2014 every persistent map overlay is APPENDED
2609
+ /* Chrome anchor regions (Feature 6) \u2014 every interactive map overlay is APPENDED
2480
2610
  INTO one of these positioned flex containers and flows/stacks within it, so no
2481
- two pieces of chrome free-float on top of each other. Regions never overlap:
2482
- the top strip splits into left/center/right; rails + corners own their edge. */
2483
- /* align-items:center, not the flex default of stretch. Without it a short pill
2484
- beside a taller control (TEST MODE next to the Map/3D toggle) is stretched to
2485
- the row's height, so two pills that should read as a matched pair end up
2486
- different shapes on different baselines. Column regions set their own
2487
- cross-axis alignment below and are unaffected. */
2611
+ two controls free-float on top of each other. Regions never overlap: the top
2612
+ strip splits into left/center/right; rails + corners own their edge. */
2488
2613
  .sl-anchor{position:absolute;z-index:5;display:flex;align-items:center;gap:8px;pointer-events:none}
2489
2614
  .sl-anchor > *{pointer-events:auto}
2490
2615
  .sl-anchor[data-region="top-left"]{top:12px;left:12px;flex-wrap:wrap;max-width:38%}
@@ -2500,18 +2625,17 @@ var CSS = (
2500
2625
  .sl-picker[data-layout="narrow"] .sl-anchor[data-region="top-left"]{max-width:30%}
2501
2626
  .sl-picker[data-layout="narrow"] .sl-anchor[data-region="top-center"]{max-width:44%}
2502
2627
 
2503
- /* TEST MODE badge \u2014 a small pill in the top-right region (shrinks on narrow) */
2504
- /* align-self:stretch, not a hardcoded height: the badge sits beside the Map/3D
2505
- toggle, whose height comes from its own border + padding + button metrics.
2506
- Stretching matches that row exactly and keeps matching if the toggle ever
2507
- changes, while the badge's own inline-flex keeps the label centred inside
2508
- whatever height it gets. Alone in the region it simply takes its natural
2509
- size. */
2510
- .sl-testbadge{display:inline-flex;align-items:center;align-self:stretch;padding:0 12px;border-radius:999px;
2511
- font-size:10px;font-weight:800;letter-spacing:.1em;line-height:1;
2512
- text-transform:uppercase;white-space:nowrap;background:var(--sl-accent);color:var(--sl-accent-ink);
2513
- box-shadow:0 2px 8px rgba(0,0,0,.25)}
2514
- .sl-picker[data-layout="narrow"] .sl-testbadge{padding:0 8px;font-size:8.5px;letter-spacing:.06em}
2628
+ /* TEST MODE is environment context, not an action. A clipped corner ribbon
2629
+ keeps it persistent without impersonating a button or competing with Map/3D.
2630
+ The top-left interactive region moves below it only on test events. */
2631
+ .sl-testbadge{position:absolute;top:17px;left:-38px;z-index:6;width:142px;padding:5px 0;
2632
+ transform:rotate(-45deg);text-align:center;pointer-events:none;
2633
+ font-size:9.5px;font-weight:850;letter-spacing:.13em;line-height:1.2;text-transform:uppercase;
2634
+ white-space:nowrap;background:var(--sl-accent);color:var(--sl-accent-ink);
2635
+ box-shadow:0 2px 8px rgba(0,0,0,.28)}
2636
+ .sl-picker[data-event-mode="test"] .sl-anchor[data-region="top-left"]{top:96px}
2637
+ .sl-picker[data-layout="narrow"] .sl-testbadge{top:14px;left:-35px;width:128px;font-size:8.5px}
2638
+ .sl-picker[data-layout="narrow"][data-event-mode="test"] .sl-anchor[data-region="top-left"]{top:88px}
2515
2639
 
2516
2640
  /* zoom column (flows within the bottom-right region) */
2517
2641
  .sl-zoom{display:flex;flex-direction:column;gap:6px}
@@ -2746,10 +2870,15 @@ var CSS = (
2746
2870
  .sl-ba-actions{grid-column:1/-1;display:grid;grid-template-columns:1fr 1fr;gap:7px}
2747
2871
  .sl-ba-actions button{min-height:36px;border-radius:9px;border:1px solid var(--sl-line);font-size:11.5px;font-weight:800}
2748
2872
  .sl-ba-actions .replace{border-color:var(--sl-accent);background:var(--sl-accent);color:var(--sl-accent-ink)}
2749
- .sl-picker[data-layout="narrow"] .sl-ba{padding:11px}
2873
+ .sl-picker[data-layout="narrow"] .sl-ba{padding:9px;gap:6px}
2874
+ .sl-picker[data-layout="narrow"] .sl-ba::after{display:none}
2875
+ .sl-picker[data-layout="narrow"] .sl-ba-title{font-size:12.5px}
2876
+ .sl-picker[data-layout="narrow"] .sl-ba-copy{display:none}
2750
2877
  .sl-picker[data-layout="narrow"] .sl-ba-copy .wide{display:none}
2751
2878
  .sl-picker[data-layout="narrow"] .sl-ba-copy .narrow{display:inline}
2752
- .sl-picker[data-layout="narrow"] .sl-ba select{min-height:44px}
2879
+ .sl-picker[data-layout="narrow"] .sl-ba select{min-height:40px}
2880
+ .sl-picker[data-layout="narrow"] .sl-ba-qty button{width:30px;height:30px}
2881
+ .sl-picker[data-layout="narrow"] .sl-ba-go{min-height:40px}
2753
2882
 
2754
2883
  /* screen-reader live region */
2755
2884
  .sl-sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
@@ -3157,6 +3286,8 @@ var SeatPicker = class _SeatPicker {
3157
3286
  this.secCardEl = null;
3158
3287
  this.viewEl = null;
3159
3288
  this.viewCleanup = null;
3289
+ /** Supersedes an older authored-view byte request when another seat is opened. */
3290
+ this.seatViewGen = 0;
3160
3291
  this.allSeatsCache = null;
3161
3292
  // F3 minimap
3162
3293
  this.miniCanvas = null;
@@ -3190,6 +3321,8 @@ var SeatPicker = class _SeatPicker {
3190
3321
  this.fsFallback = false;
3191
3322
  this.fsChangeHandler = null;
3192
3323
  this.fsEscHandler = null;
3324
+ /** Host-level event chrome owns the duplicate identity outside full screen. */
3325
+ this.eventDetailsHidden = false;
3193
3326
  /** True once we've asked the host page to pin us fullscreen (framed, no native). */
3194
3327
  this.framedFs = false;
3195
3328
  /** Last height (px) posted to a host frame; dedupes redundant reports. */
@@ -3212,6 +3345,7 @@ var SeatPicker = class _SeatPicker {
3212
3345
  if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
3213
3346
  if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
3214
3347
  this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
3348
+ this.eventDetailsHidden = !!options.hideEventDetails;
3215
3349
  this.hostPricing = options.pricing;
3216
3350
  this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
3217
3351
  this.access = options.transport ? null : createBuyerAccessContext(options, {
@@ -3229,6 +3363,10 @@ var SeatPicker = class _SeatPicker {
3229
3363
  onObjectUnavailable: (event) => this.opts.onSelectedObjectUnavailable?.(event)
3230
3364
  });
3231
3365
  this.api = options.transport ?? this.pubApi;
3366
+ this.buyerAssetUrls = new BuyerAssetObjectUrls(
3367
+ options.event,
3368
+ this.api.asset ? (key, asset) => this.api.asset(key, asset) : void 0
3369
+ );
3232
3370
  if (options.checkout === "hosted" && !this.pubApi) {
3233
3371
  console.warn(
3234
3372
  'seatlayer: checkout: "hosted" needs the widget\'s own transport \u2014 a custom `transport` owns its backend, so the picker is staying on onCheckout for this mount.'
@@ -3357,7 +3495,7 @@ var SeatPicker = class _SeatPicker {
3357
3495
  }
3358
3496
  }
3359
3497
  const sightHtml = hasStage && distance != null ? `<div class="sl-confirm-sight">${t2("picker.sightline", { m: distance })}</div>` : "";
3360
- const viewBtn = realPhoto ? `<button type="button" class="sl-confirm-view sl-confirm-thumbwrap" aria-label="${t2("picker.viewFromSeat", { label: seat.label })}"><img class="sl-confirm-thumb" src="${realPhoto}" alt="" /><span class="sl-confirm-thumb-badge">\u{1F52D} ${this.tf("picker.viewFromHere", "View from here")}</span></button>` : `<button type="button" class="sl-confirm-view sl-confirm-viewbtn" aria-label="${t2("picker.viewFromSeat", { label: seat.label })}"><span aria-hidden="true">\u{1F52D}</span><span>${this.tf("picker.viewFromHere", "View from here")}</span></button>`;
3498
+ const viewBtn = realPhoto ? `<button type="button" class="sl-confirm-view sl-confirm-thumbwrap" aria-label="${t2("picker.viewFromSeat", { label: seat.label })}"><img class="sl-confirm-thumb" alt="" /><span class="sl-confirm-thumb-badge">\u{1F52D} ${this.tf("picker.viewFromHere", "View from here")}</span></button>` : `<button type="button" class="sl-confirm-view sl-confirm-viewbtn" aria-label="${t2("picker.viewFromSeat", { label: seat.label })}"><span aria-hidden="true">\u{1F52D}</span><span>${this.tf("picker.viewFromHere", "View from here")}</span></button>`;
3361
3499
  return viewBtn + sightHtml;
3362
3500
  }
3363
3501
  /** "See it in 3D" (2D) / "View from this seat" (already in 3D) action for the
@@ -3492,6 +3630,10 @@ var SeatPicker = class _SeatPicker {
3492
3630
  }
3493
3631
  syncFullscreenButtons() {
3494
3632
  const active = !!document.fullscreenElement || this.fsFallback || this.framedFs;
3633
+ const hideEventDetails = this.eventDetailsHidden && !active;
3634
+ this.root?.setAttribute("data-event-details-hidden", String(hideEventDetails));
3635
+ this.els.logo?.toggleAttribute("hidden", hideEventDetails);
3636
+ this.els.headInfo?.toggleAttribute("hidden", hideEventDetails);
3495
3637
  this.els.zfs?.setAttribute("aria-pressed", String(active));
3496
3638
  this.view3dEl?.querySelector(".sl-view3d-fs")?.setAttribute("aria-pressed", String(active));
3497
3639
  }
@@ -3635,7 +3777,7 @@ var SeatPicker = class _SeatPicker {
3635
3777
  root.innerHTML = `
3636
3778
  <div class="sl-head">
3637
3779
  <div class="sl-logo" data-ref="logo"></div>
3638
- <div class="sl-head-info">
3780
+ <div class="sl-head-info" data-ref="headInfo">
3639
3781
  <div class="sl-head-name" data-ref="name"></div>
3640
3782
  <div class="sl-head-meta" data-ref="meta"></div>
3641
3783
  </div>
@@ -3697,6 +3839,7 @@ var SeatPicker = class _SeatPicker {
3697
3839
  this.els[el.dataset.ref] = el;
3698
3840
  });
3699
3841
  this.mapHost = this.els.map;
3842
+ this.syncFullscreenButtons();
3700
3843
  void this.refreshOfferAvailability(false);
3701
3844
  if (this.api.availability) {
3702
3845
  this.offerVisibilityHandler = () => {
@@ -3808,7 +3951,8 @@ var SeatPicker = class _SeatPicker {
3808
3951
  }
3809
3952
  this.els.boot.remove();
3810
3953
  this.startRealtime();
3811
- this.salesClosed = !!info.salesClosed;
3954
+ this.salesClosed = !!info.salesClosed || !!this.opts.readOnly;
3955
+ root.dataset.eventMode = info.mode === "test" ? "test" : "live";
3812
3956
  this.controller.setViewMode(this.normalizeInitialView(this.opts.initialView));
3813
3957
  this.buildRegions();
3814
3958
  this.regions["bottom-right"].appendChild(this.els.zoom);
@@ -3818,7 +3962,7 @@ var SeatPicker = class _SeatPicker {
3818
3962
  badge.className = "sl-testbadge";
3819
3963
  badge.textContent = t2("picker.testMode");
3820
3964
  badge.setAttribute("aria-label", t2("picker.testMode"));
3821
- this.regions["top-right"].appendChild(badge);
3965
+ this.els.map.appendChild(badge);
3822
3966
  }
3823
3967
  const chartTheme = this.controller.doc?.theme;
3824
3968
  Object.entries(resolveTokens(chartTheme, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
@@ -4055,8 +4199,9 @@ var SeatPicker = class _SeatPicker {
4055
4199
  * idempotent DOM apply used at load and on transition.
4056
4200
  */
4057
4201
  setSalesClosed(closed) {
4058
- if (this.salesClosed === closed) return;
4059
- this.salesClosed = closed;
4202
+ const next = closed || !!this.opts.readOnly;
4203
+ if (this.salesClosed === next) return;
4204
+ this.salesClosed = next;
4060
4205
  this.applySalesClosed();
4061
4206
  }
4062
4207
  applySalesClosed() {
@@ -4940,6 +5085,13 @@ var SeatPicker = class _SeatPicker {
4940
5085
  el.innerHTML = `<div class="sl-confirm-grid">` + identityFields + `</div><div class="sl-confirm-cat"><span class="sl-dot" style="background:${cat?.color ?? "#6e7bff"}"></span><span class="sl-confirm-cat-name">${safe(details?.categoryLabel ?? cat?.label ?? seat.categoryKey)}</span>` + (price != null ? `<span class="sl-confirm-price">${this.money(price)}</span>` : "") + `</div><div class="sl-confirm-body">` + this.wheelchairConfirmHtml(details?.wheelchairSpaceType) + this.commercialConfirmHtml(seat.commercial) + (this.seatViewEnabled() ? this.confirmThumbHtml(seat) : "") + this.see3dConfirmHtml() + `<div class="sl-confirm-row"><button type="button" class="sl-confirm-cancel">Cancel</button><button type="button" class="sl-confirm-add"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4 4L19 7"/></svg>Select</button></div></div>`;
4941
5086
  this.els.map.appendChild(el);
4942
5087
  this.confirmEl = el;
5088
+ const thumb = el.querySelector(".sl-confirm-thumb");
5089
+ if (thumb && seat.viewUrl) {
5090
+ const thumbReference = seat.viewMeta?.previewUrl ?? seat.viewUrl;
5091
+ void this.buyerAssetUrls.resolve(thumbReference).then((url) => {
5092
+ if (url && el.isConnected && this.confirmEl === el) thumb.src = url;
5093
+ }).catch((error) => this.opts.onError?.(error));
5094
+ }
4943
5095
  this.reanchorConfirm();
4944
5096
  el.querySelector(".sl-confirm-view")?.addEventListener("click", () => void this.openSeatView(seat));
4945
5097
  el.querySelector(".sl-confirm-3d")?.addEventListener("click", () => {
@@ -5020,6 +5172,7 @@ var SeatPicker = class _SeatPicker {
5020
5172
  */
5021
5173
  async openSeatView(seat) {
5022
5174
  if (!this.root || !this.seatViewEnabled()) return;
5175
+ const generation = ++this.seatViewGen;
5023
5176
  const doc = this.controller.doc;
5024
5177
  const activeId = this.controller.getActiveFloorId();
5025
5178
  const focal = seat.focalPoint ?? doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
@@ -5027,9 +5180,24 @@ var SeatPicker = class _SeatPicker {
5027
5180
  let caption;
5028
5181
  let real = false;
5029
5182
  if (seat.viewUrl) {
5183
+ let resolvedUrl;
5184
+ let resolvedPreviewUrl = null;
5185
+ try {
5186
+ const previewReference = seat.viewMeta?.previewUrl;
5187
+ if (previewReference && previewReference !== seat.viewUrl) {
5188
+ resolvedPreviewUrl = await this.buyerAssetUrls.resolve(previewReference);
5189
+ resolvedUrl = seat.viewUrl;
5190
+ } else {
5191
+ resolvedUrl = await this.buyerAssetUrls.resolve(seat.viewUrl);
5192
+ }
5193
+ } catch (error) {
5194
+ if (generation === this.seatViewGen) this.opts.onError?.(error);
5195
+ return;
5196
+ }
5197
+ if (generation !== this.seatViewGen || !resolvedUrl || seat.viewMeta?.previewUrl && seat.viewMeta.previewUrl !== seat.viewUrl && !resolvedPreviewUrl || !this.root || !this.seatViewEnabled()) return;
5030
5198
  const view = {
5031
- url: seat.viewUrl,
5032
- ...seat.viewMeta?.previewUrl ? { previewUrl: seat.viewMeta.previewUrl } : {},
5199
+ url: resolvedUrl,
5200
+ ...resolvedPreviewUrl ? { previewUrl: resolvedPreviewUrl } : {},
5033
5201
  ...seat.viewMeta?.sourceWidth !== void 0 ? { sourceWidth: seat.viewMeta.sourceWidth } : {},
5034
5202
  ...seat.viewMeta?.sourceHeight !== void 0 ? { sourceHeight: seat.viewMeta.sourceHeight } : {},
5035
5203
  ...seat.viewMeta?.previewWidth !== void 0 ? { previewWidth: seat.viewMeta.previewWidth } : {},
@@ -5047,14 +5215,14 @@ var SeatPicker = class _SeatPicker {
5047
5215
  const { generateSeatPanorama } = await loadPanorama();
5048
5216
  pano2 = generateSeatPanorama(seat, focal, this.allSeats());
5049
5217
  } catch (err) {
5050
- this.opts.onError?.(err);
5218
+ if (generation === this.seatViewGen) this.opts.onError?.(err);
5051
5219
  return;
5052
5220
  }
5053
- if (!this.root || !this.seatViewEnabled()) return;
5221
+ if (generation !== this.seatViewGen || !this.root || !this.seatViewEnabled()) return;
5054
5222
  panoSource = { url: pano2.url, generated: true };
5055
5223
  caption = t2("picker.illustrationCaption", { m: pano2.distanceM });
5056
5224
  }
5057
- this.closeSeatView();
5225
+ this.closeSeatView(false);
5058
5226
  const el = document.createElement("div");
5059
5227
  el.className = "sl-view";
5060
5228
  el.setAttribute("role", "dialog");
@@ -5070,9 +5238,12 @@ var SeatPicker = class _SeatPicker {
5070
5238
  };
5071
5239
  if (delivery.upgradeUrl) {
5072
5240
  cancelUpgrade = schedulePanoramaUpgrade(() => {
5073
- void loadPanoramaImage(delivery.upgradeUrl, loadAbort.signal).then(() => {
5074
- if (!el.isConnected || loadAbort.signal.aborted) return;
5075
- pano.style.backgroundImage = `url("${delivery.upgradeUrl}")`;
5241
+ void this.buyerAssetUrls.resolve(delivery.upgradeUrl).then((url) => {
5242
+ if (!url || loadAbort.signal.aborted) return null;
5243
+ return loadPanoramaImage(url, loadAbort.signal).then(() => url);
5244
+ }).then((url) => {
5245
+ if (!url || !el.isConnected || loadAbort.signal.aborted) return;
5246
+ pano.style.backgroundImage = `url("${url}")`;
5076
5247
  }).catch(() => {
5077
5248
  });
5078
5249
  });
@@ -5148,7 +5319,8 @@ var SeatPicker = class _SeatPicker {
5148
5319
  el.removeEventListener("keydown", onKey);
5149
5320
  };
5150
5321
  }
5151
- closeSeatView() {
5322
+ closeSeatView(cancelPending = true) {
5323
+ if (cancelPending) this.seatViewGen += 1;
5152
5324
  this.viewCleanup?.();
5153
5325
  this.viewCleanup = null;
5154
5326
  this.viewEl?.remove();
@@ -6080,6 +6252,15 @@ var SeatPicker = class _SeatPicker {
6080
6252
  this.opts.theme = { ...this.opts.theme ?? {}, map: map ?? void 0 };
6081
6253
  this.controller.setMapTheme(map);
6082
6254
  }
6255
+ /**
6256
+ * Let a host suppress duplicate event identity after mount without remounting
6257
+ * the live picker (and therefore without disturbing a selection or hold).
6258
+ * Full-screen mode still restores the identity until the buyer exits it.
6259
+ */
6260
+ setEventDetailsHidden(hidden) {
6261
+ this.eventDetailsHidden = hidden;
6262
+ this.syncFullscreenButtons();
6263
+ }
6083
6264
  /**
6084
6265
  * Replace the host pricing override AFTER mount, and repaint everything that
6085
6266
  * shows a price.
@@ -6231,19 +6412,33 @@ var SeatPicker = class _SeatPicker {
6231
6412
  async seatViewFor3d(seatId) {
6232
6413
  const seat = this.allSeats().find((s) => s.id === seatId);
6233
6414
  if (!seat) return null;
6234
- if (seat.viewUrl) return {
6235
- url: seat.viewUrl,
6236
- ...seat.viewMeta?.previewUrl ? { previewUrl: seat.viewMeta.previewUrl } : {},
6237
- ...seat.viewMeta?.sourceWidth !== void 0 ? { sourceWidth: seat.viewMeta.sourceWidth } : {},
6238
- ...seat.viewMeta?.sourceHeight !== void 0 ? { sourceHeight: seat.viewMeta.sourceHeight } : {},
6239
- ...seat.viewMeta?.previewWidth !== void 0 ? { previewWidth: seat.viewMeta.previewWidth } : {},
6240
- ...seat.viewMeta?.previewHeight !== void 0 ? { previewHeight: seat.viewMeta.previewHeight } : {},
6241
- ...seat.viewMeta?.initialBearingDeg !== void 0 ? { initialBearingDeg: seat.viewMeta.initialBearingDeg } : {},
6242
- ...seat.viewMeta?.initialPitchDeg !== void 0 ? { initialPitchDeg: seat.viewMeta.initialPitchDeg } : {},
6243
- ...seat.viewMeta?.coverage ? { coverage: seat.viewMeta.coverage } : {},
6244
- ...seat.viewMeta?.capturedAt ? { capturedAt: seat.viewMeta.capturedAt } : {},
6245
- ...seat.viewMeta?.sourceLabel ? { sourceLabel: seat.viewMeta.sourceLabel } : {}
6246
- };
6415
+ if (seat.viewUrl) {
6416
+ try {
6417
+ const previewReference = seat.viewMeta?.previewUrl;
6418
+ const progressive = !!previewReference && previewReference !== seat.viewUrl;
6419
+ const previewUrl = progressive ? await this.buyerAssetUrls.resolve(previewReference) : null;
6420
+ const url = progressive ? seat.viewUrl : await this.buyerAssetUrls.resolve(seat.viewUrl);
6421
+ if (!url) return null;
6422
+ if (progressive && !previewUrl) return null;
6423
+ return {
6424
+ url,
6425
+ ...previewUrl ? { previewUrl } : {},
6426
+ ...progressive ? { resolveUrl: (reference) => this.buyerAssetUrls.resolve(reference) } : {},
6427
+ ...seat.viewMeta?.sourceWidth !== void 0 ? { sourceWidth: seat.viewMeta.sourceWidth } : {},
6428
+ ...seat.viewMeta?.sourceHeight !== void 0 ? { sourceHeight: seat.viewMeta.sourceHeight } : {},
6429
+ ...seat.viewMeta?.previewWidth !== void 0 ? { previewWidth: seat.viewMeta.previewWidth } : {},
6430
+ ...seat.viewMeta?.previewHeight !== void 0 ? { previewHeight: seat.viewMeta.previewHeight } : {},
6431
+ ...seat.viewMeta?.initialBearingDeg !== void 0 ? { initialBearingDeg: seat.viewMeta.initialBearingDeg } : {},
6432
+ ...seat.viewMeta?.initialPitchDeg !== void 0 ? { initialPitchDeg: seat.viewMeta.initialPitchDeg } : {},
6433
+ ...seat.viewMeta?.coverage ? { coverage: seat.viewMeta.coverage } : {},
6434
+ ...seat.viewMeta?.capturedAt ? { capturedAt: seat.viewMeta.capturedAt } : {},
6435
+ ...seat.viewMeta?.sourceLabel ? { sourceLabel: seat.viewMeta.sourceLabel } : {}
6436
+ };
6437
+ } catch (error) {
6438
+ this.opts.onError?.(error);
6439
+ return null;
6440
+ }
6441
+ }
6247
6442
  const doc = this.controller.doc;
6248
6443
  if (!doc) return null;
6249
6444
  const activeId = this.controller.getActiveFloorId();
@@ -6826,6 +7021,7 @@ var SeatPicker = class _SeatPicker {
6826
7021
  this.closeSeatView();
6827
7022
  this.closeCheckoutPanel();
6828
7023
  this.exit3d();
7024
+ this.buyerAssetUrls.dispose();
6829
7025
  this.stopHoldTimer();
6830
7026
  if (this.toastTimer) clearTimeout(this.toastTimer);
6831
7027
  if (this.liveTimer) clearTimeout(this.liveTimer);