@seatlayer/js 0.47.2 → 0.48.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.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { PickerSeat, RendererViewMode, SeatHoverDetails, PickerTransport, PickerMapTheme } from '@seatlayer/core';
2
2
  export { ExpandedSeat, SeatHoverDetails } from '@seatlayer/core';
3
- import { C as ChannelListResult, a as ChannelAllocationPage, b as ChannelRecord, A as AssignmentResult, c as ChannelPreviewProjection, d as ChannelAccessIntent, e as AccessLinkReveal, f as AccessLinkStatusRecord, g as ChannelSeatStatus, B as BucketRow } from './manager-CAIPkUYl.cjs';
4
- export { h as ACCESS_LINK_DEFAULTS, i as AccessIntentForbidsDetails, j as AccessLinkRecord, k as AccessLinkState, l as AccessLinkStatus, m as ArchiveBlockedDetails, n as AssignmentBuckets, o as AssignmentDropDetails, p as ChannelAccessSummary, q as ChannelAuditEntry, r as ChannelAuditPage, s as ChannelCounts, t as ChannelState, u as ControlRoomActivityEntry, v as ControlRoomSectionMetric, w as ControlRoomSnapshot, I as IntentSwitchBlockedDetails, L as LogEntry, x as LogPage, M as ManageApi, y as ManageApiError, P as PUBLIC_CHANNEL_ID, z as PUBLIC_CHANNEL_NAME, R as ReportByStatus, D as ReportCategoryMeta, E as ReportCategoryRow, F as ReportResult, S as SeatManager, G as SeatManagerActionResult, H as SeatManagerActivity, J as SeatManagerCapability, K as SeatManagerConnection, N as SeatManagerMode, O as SeatManagerOptions, Q as SeatManagerTallies, T as SelectionSourceRow, U as accessIntentDescription, V as accessIntentLabel, W as accessLine, X as accessLinkBadge, Y as accessLinkErrorCopy, Z as accessLinkIsLive, _ as accessLinkPolicyLines, $ as bucketRows, a0 as dropReviewRows, a1 as intentForbidsCopy, a2 as intentSwitchBlockedCopy, a3 as isPublicChannelId, a4 as markerLetter, a5 as markerOf, a6 as mutationCount, a7 as needsMoveConfirmation, a8 as planAssignment, a9 as retryAfterCopy, aa as selectionSources, ab as stateBadge, ac as suggestMarker } from './manager-CAIPkUYl.cjs';
3
+ import { C as ChannelListResult, a as ChannelAllocationPage, b as ChannelRecord, A as AssignmentResult, c as ChannelPreviewProjection, d as ChannelAccessIntent, e as AccessLinkReveal, f as AccessLinkStatusRecord, g as ChannelSeatStatus, B as BucketRow } from './manager-DEjbKiqJ.cjs';
4
+ export { h as ACCESS_LINK_DEFAULTS, i as AccessIntentForbidsDetails, j as AccessLinkRecord, k as AccessLinkState, l as AccessLinkStatus, m as ArchiveBlockedDetails, n as AssignmentBuckets, o as AssignmentDropDetails, p as ChannelAccessSummary, q as ChannelAttribution, r as ChannelAuditEntry, s as ChannelAuditPage, t as ChannelCounts, u as ChannelReport, v as ChannelReportLinkRecord, w as ChannelReportLinkReveal, x as ChannelReportResult, y as ChannelReportRow, z as ChannelState, D as ControlRoomActivityEntry, E as ControlRoomSectionMetric, F as ControlRoomSnapshot, I as IntentSwitchBlockedDetails, G as InventoryBooking, H as InventoryBookingActivity, J as InventoryBookingDetail, K as InventoryBookingObject, L as InventoryBookingState, M as InventoryBookingsPage, N as InventoryBookingsQuery, O as LogEntry, P as LogPage, Q as ManageApi, R as ManageApiError, S as PUBLIC_CHANNEL_ID, T as PUBLIC_CHANNEL_NAME, U as ReportByStatus, V as ReportCategoryMeta, W as ReportCategoryRow, X as ReportResult, Y as SeatManager, Z as SeatManagerActionResult, _ as SeatManagerActivity, $ as SeatManagerCapability, a0 as SeatManagerConnection, a1 as SeatManagerMode, a2 as SeatManagerOptions, a3 as SeatManagerTallies, a4 as SelectionSourceRow, a5 as accessIntentDescription, a6 as accessIntentLabel, a7 as accessLine, a8 as accessLinkBadge, a9 as accessLinkErrorCopy, aa as accessLinkIsLive, ab as accessLinkPolicyLines, ac as bucketRows, ad as dropReviewRows, ae as intentForbidsCopy, af as intentSwitchBlockedCopy, ag as isPublicChannelId, ah as markerLetter, ai as markerOf, aj as mutationCount, ak as needsMoveConfirmation, al as planAssignment, am as retryAfterCopy, an as selectionSources, ao as stateBadge, ap as suggestMarker } from './manager-DEjbKiqJ.cjs';
5
5
 
6
6
  /**
7
7
  * Buyer access context — the browser half of the Sales Channels contract
@@ -369,8 +369,10 @@ interface ControllerSinkOptions {
369
369
  declare function createControllerSink(controller: PickerControllerLike, options?: ControllerSinkOptions): RealtimeSink;
370
370
 
371
371
  /**
372
- * Minimal client for the public embed surface of workers/api (the `/pub/*`
373
- * routes). Deliberately self-contained it does NOT reuse src/lib/api.ts,
372
+ * Minimal client for the browser embed surface of workers/api (the `/pub/*`
373
+ * routes). Platform events bind this client to a buyer access context; Managed
374
+ * public/unlisted events may still use it anonymously. Deliberately
375
+ * self-contained — it does NOT reuse src/lib/api.ts,
374
376
  * which bakes in a build-time API base and dashboard session credentials. The
375
377
  * SDK runs cross-origin on a third-party ticketing page, so:
376
378
  * - apiBase is per-instance (constructor option), not a build constant;
@@ -1371,6 +1373,13 @@ interface SeatPickerOptions {
1371
1373
  * owns hold persistence and supplies initialHoldId itself.
1372
1374
  */
1373
1375
  restoreHold?: boolean;
1376
+ /**
1377
+ * Render the real chart and live seat statuses without allowing selection,
1378
+ * holds or checkout. This is for venue previews and pre-sale Website pages;
1379
+ * it is enforced by the widget even if the event later opens while mounted.
1380
+ * Default false.
1381
+ */
1382
+ readOnly?: boolean;
1374
1383
  /**
1375
1384
  * Confirm mode: tapping a seat shows a confirmation card with section, row,
1376
1385
  * seat, category, price and Select/Cancel before it enters the tray. Default
@@ -1522,6 +1531,8 @@ interface SeatPickerOptions {
1522
1531
  declare class SeatPicker {
1523
1532
  private readonly opts;
1524
1533
  private readonly api;
1534
+ /** Authenticated view media, cached only for this picker lifetime. */
1535
+ private readonly buyerAssetUrls;
1525
1536
  /** Our own public client, or null when the host injected a transport. */
1526
1537
  private readonly pubApi;
1527
1538
  /** Null for the ordinary public picker — the tokenless path is untouched. */
@@ -1615,6 +1626,8 @@ declare class SeatPicker {
1615
1626
  private secCardEl;
1616
1627
  private viewEl;
1617
1628
  private viewCleanup;
1629
+ /** Supersedes an older authored-view byte request when another seat is opened. */
1630
+ private seatViewGen;
1618
1631
  private allSeatsCache;
1619
1632
  private miniCanvas;
1620
1633
  private miniBase;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { PickerSeat, RendererViewMode, SeatHoverDetails, PickerTransport, PickerMapTheme } from '@seatlayer/core';
2
2
  export { ExpandedSeat, SeatHoverDetails } from '@seatlayer/core';
3
- import { C as ChannelListResult, a as ChannelAllocationPage, b as ChannelRecord, A as AssignmentResult, c as ChannelPreviewProjection, d as ChannelAccessIntent, e as AccessLinkReveal, f as AccessLinkStatusRecord, g as ChannelSeatStatus, B as BucketRow } from './manager-CAIPkUYl.js';
4
- export { h as ACCESS_LINK_DEFAULTS, i as AccessIntentForbidsDetails, j as AccessLinkRecord, k as AccessLinkState, l as AccessLinkStatus, m as ArchiveBlockedDetails, n as AssignmentBuckets, o as AssignmentDropDetails, p as ChannelAccessSummary, q as ChannelAuditEntry, r as ChannelAuditPage, s as ChannelCounts, t as ChannelState, u as ControlRoomActivityEntry, v as ControlRoomSectionMetric, w as ControlRoomSnapshot, I as IntentSwitchBlockedDetails, L as LogEntry, x as LogPage, M as ManageApi, y as ManageApiError, P as PUBLIC_CHANNEL_ID, z as PUBLIC_CHANNEL_NAME, R as ReportByStatus, D as ReportCategoryMeta, E as ReportCategoryRow, F as ReportResult, S as SeatManager, G as SeatManagerActionResult, H as SeatManagerActivity, J as SeatManagerCapability, K as SeatManagerConnection, N as SeatManagerMode, O as SeatManagerOptions, Q as SeatManagerTallies, T as SelectionSourceRow, U as accessIntentDescription, V as accessIntentLabel, W as accessLine, X as accessLinkBadge, Y as accessLinkErrorCopy, Z as accessLinkIsLive, _ as accessLinkPolicyLines, $ as bucketRows, a0 as dropReviewRows, a1 as intentForbidsCopy, a2 as intentSwitchBlockedCopy, a3 as isPublicChannelId, a4 as markerLetter, a5 as markerOf, a6 as mutationCount, a7 as needsMoveConfirmation, a8 as planAssignment, a9 as retryAfterCopy, aa as selectionSources, ab as stateBadge, ac as suggestMarker } from './manager-CAIPkUYl.js';
3
+ import { C as ChannelListResult, a as ChannelAllocationPage, b as ChannelRecord, A as AssignmentResult, c as ChannelPreviewProjection, d as ChannelAccessIntent, e as AccessLinkReveal, f as AccessLinkStatusRecord, g as ChannelSeatStatus, B as BucketRow } from './manager-DEjbKiqJ.js';
4
+ export { h as ACCESS_LINK_DEFAULTS, i as AccessIntentForbidsDetails, j as AccessLinkRecord, k as AccessLinkState, l as AccessLinkStatus, m as ArchiveBlockedDetails, n as AssignmentBuckets, o as AssignmentDropDetails, p as ChannelAccessSummary, q as ChannelAttribution, r as ChannelAuditEntry, s as ChannelAuditPage, t as ChannelCounts, u as ChannelReport, v as ChannelReportLinkRecord, w as ChannelReportLinkReveal, x as ChannelReportResult, y as ChannelReportRow, z as ChannelState, D as ControlRoomActivityEntry, E as ControlRoomSectionMetric, F as ControlRoomSnapshot, I as IntentSwitchBlockedDetails, G as InventoryBooking, H as InventoryBookingActivity, J as InventoryBookingDetail, K as InventoryBookingObject, L as InventoryBookingState, M as InventoryBookingsPage, N as InventoryBookingsQuery, O as LogEntry, P as LogPage, Q as ManageApi, R as ManageApiError, S as PUBLIC_CHANNEL_ID, T as PUBLIC_CHANNEL_NAME, U as ReportByStatus, V as ReportCategoryMeta, W as ReportCategoryRow, X as ReportResult, Y as SeatManager, Z as SeatManagerActionResult, _ as SeatManagerActivity, $ as SeatManagerCapability, a0 as SeatManagerConnection, a1 as SeatManagerMode, a2 as SeatManagerOptions, a3 as SeatManagerTallies, a4 as SelectionSourceRow, a5 as accessIntentDescription, a6 as accessIntentLabel, a7 as accessLine, a8 as accessLinkBadge, a9 as accessLinkErrorCopy, aa as accessLinkIsLive, ab as accessLinkPolicyLines, ac as bucketRows, ad as dropReviewRows, ae as intentForbidsCopy, af as intentSwitchBlockedCopy, ag as isPublicChannelId, ah as markerLetter, ai as markerOf, aj as mutationCount, ak as needsMoveConfirmation, al as planAssignment, am as retryAfterCopy, an as selectionSources, ao as stateBadge, ap as suggestMarker } from './manager-DEjbKiqJ.js';
5
5
 
6
6
  /**
7
7
  * Buyer access context — the browser half of the Sales Channels contract
@@ -369,8 +369,10 @@ interface ControllerSinkOptions {
369
369
  declare function createControllerSink(controller: PickerControllerLike, options?: ControllerSinkOptions): RealtimeSink;
370
370
 
371
371
  /**
372
- * Minimal client for the public embed surface of workers/api (the `/pub/*`
373
- * routes). Deliberately self-contained it does NOT reuse src/lib/api.ts,
372
+ * Minimal client for the browser embed surface of workers/api (the `/pub/*`
373
+ * routes). Platform events bind this client to a buyer access context; Managed
374
+ * public/unlisted events may still use it anonymously. Deliberately
375
+ * self-contained — it does NOT reuse src/lib/api.ts,
374
376
  * which bakes in a build-time API base and dashboard session credentials. The
375
377
  * SDK runs cross-origin on a third-party ticketing page, so:
376
378
  * - apiBase is per-instance (constructor option), not a build constant;
@@ -1371,6 +1373,13 @@ interface SeatPickerOptions {
1371
1373
  * owns hold persistence and supplies initialHoldId itself.
1372
1374
  */
1373
1375
  restoreHold?: boolean;
1376
+ /**
1377
+ * Render the real chart and live seat statuses without allowing selection,
1378
+ * holds or checkout. This is for venue previews and pre-sale Website pages;
1379
+ * it is enforced by the widget even if the event later opens while mounted.
1380
+ * Default false.
1381
+ */
1382
+ readOnly?: boolean;
1374
1383
  /**
1375
1384
  * Confirm mode: tapping a seat shows a confirmation card with section, row,
1376
1385
  * seat, category, price and Select/Cancel before it enters the tray. Default
@@ -1522,6 +1531,8 @@ interface SeatPickerOptions {
1522
1531
  declare class SeatPicker {
1523
1532
  private readonly opts;
1524
1533
  private readonly api;
1534
+ /** Authenticated view media, cached only for this picker lifetime. */
1535
+ private readonly buyerAssetUrls;
1525
1536
  /** Our own public client, or null when the host injected a transport. */
1526
1537
  private readonly pubApi;
1527
1538
  /** Null for the ordinary public picker — the tokenless path is untouched. */
@@ -1615,6 +1626,8 @@ declare class SeatPicker {
1615
1626
  private secCardEl;
1616
1627
  private viewEl;
1617
1628
  private viewCleanup;
1629
+ /** Supersedes an older authored-view byte request when another seat is opened. */
1630
+ private seatViewGen;
1618
1631
  private allSeatsCache;
1619
1632
  private miniCanvas;
1620
1633
  private miniBase;
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",
@@ -3157,6 +3276,8 @@ var SeatPicker = class _SeatPicker {
3157
3276
  this.secCardEl = null;
3158
3277
  this.viewEl = null;
3159
3278
  this.viewCleanup = null;
3279
+ /** Supersedes an older authored-view byte request when another seat is opened. */
3280
+ this.seatViewGen = 0;
3160
3281
  this.allSeatsCache = null;
3161
3282
  // F3 minimap
3162
3283
  this.miniCanvas = null;
@@ -3229,6 +3350,10 @@ var SeatPicker = class _SeatPicker {
3229
3350
  onObjectUnavailable: (event) => this.opts.onSelectedObjectUnavailable?.(event)
3230
3351
  });
3231
3352
  this.api = options.transport ?? this.pubApi;
3353
+ this.buyerAssetUrls = new BuyerAssetObjectUrls(
3354
+ options.event,
3355
+ this.api.asset ? (key, asset) => this.api.asset(key, asset) : void 0
3356
+ );
3232
3357
  if (options.checkout === "hosted" && !this.pubApi) {
3233
3358
  console.warn(
3234
3359
  '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 +3482,7 @@ var SeatPicker = class _SeatPicker {
3357
3482
  }
3358
3483
  }
3359
3484
  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>`;
3485
+ 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
3486
  return viewBtn + sightHtml;
3362
3487
  }
3363
3488
  /** "See it in 3D" (2D) / "View from this seat" (already in 3D) action for the
@@ -3808,7 +3933,7 @@ var SeatPicker = class _SeatPicker {
3808
3933
  }
3809
3934
  this.els.boot.remove();
3810
3935
  this.startRealtime();
3811
- this.salesClosed = !!info.salesClosed;
3936
+ this.salesClosed = !!info.salesClosed || !!this.opts.readOnly;
3812
3937
  this.controller.setViewMode(this.normalizeInitialView(this.opts.initialView));
3813
3938
  this.buildRegions();
3814
3939
  this.regions["bottom-right"].appendChild(this.els.zoom);
@@ -4055,8 +4180,9 @@ var SeatPicker = class _SeatPicker {
4055
4180
  * idempotent DOM apply used at load and on transition.
4056
4181
  */
4057
4182
  setSalesClosed(closed) {
4058
- if (this.salesClosed === closed) return;
4059
- this.salesClosed = closed;
4183
+ const next = closed || !!this.opts.readOnly;
4184
+ if (this.salesClosed === next) return;
4185
+ this.salesClosed = next;
4060
4186
  this.applySalesClosed();
4061
4187
  }
4062
4188
  applySalesClosed() {
@@ -4940,6 +5066,13 @@ var SeatPicker = class _SeatPicker {
4940
5066
  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
5067
  this.els.map.appendChild(el);
4942
5068
  this.confirmEl = el;
5069
+ const thumb = el.querySelector(".sl-confirm-thumb");
5070
+ if (thumb && seat.viewUrl) {
5071
+ const thumbReference = seat.viewMeta?.previewUrl ?? seat.viewUrl;
5072
+ void this.buyerAssetUrls.resolve(thumbReference).then((url) => {
5073
+ if (url && el.isConnected && this.confirmEl === el) thumb.src = url;
5074
+ }).catch((error) => this.opts.onError?.(error));
5075
+ }
4943
5076
  this.reanchorConfirm();
4944
5077
  el.querySelector(".sl-confirm-view")?.addEventListener("click", () => void this.openSeatView(seat));
4945
5078
  el.querySelector(".sl-confirm-3d")?.addEventListener("click", () => {
@@ -5020,6 +5153,7 @@ var SeatPicker = class _SeatPicker {
5020
5153
  */
5021
5154
  async openSeatView(seat) {
5022
5155
  if (!this.root || !this.seatViewEnabled()) return;
5156
+ const generation = ++this.seatViewGen;
5023
5157
  const doc = this.controller.doc;
5024
5158
  const activeId = this.controller.getActiveFloorId();
5025
5159
  const focal = seat.focalPoint ?? doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
@@ -5027,9 +5161,24 @@ var SeatPicker = class _SeatPicker {
5027
5161
  let caption;
5028
5162
  let real = false;
5029
5163
  if (seat.viewUrl) {
5164
+ let resolvedUrl;
5165
+ let resolvedPreviewUrl = null;
5166
+ try {
5167
+ const previewReference = seat.viewMeta?.previewUrl;
5168
+ if (previewReference && previewReference !== seat.viewUrl) {
5169
+ resolvedPreviewUrl = await this.buyerAssetUrls.resolve(previewReference);
5170
+ resolvedUrl = seat.viewUrl;
5171
+ } else {
5172
+ resolvedUrl = await this.buyerAssetUrls.resolve(seat.viewUrl);
5173
+ }
5174
+ } catch (error) {
5175
+ if (generation === this.seatViewGen) this.opts.onError?.(error);
5176
+ return;
5177
+ }
5178
+ if (generation !== this.seatViewGen || !resolvedUrl || seat.viewMeta?.previewUrl && seat.viewMeta.previewUrl !== seat.viewUrl && !resolvedPreviewUrl || !this.root || !this.seatViewEnabled()) return;
5030
5179
  const view = {
5031
- url: seat.viewUrl,
5032
- ...seat.viewMeta?.previewUrl ? { previewUrl: seat.viewMeta.previewUrl } : {},
5180
+ url: resolvedUrl,
5181
+ ...resolvedPreviewUrl ? { previewUrl: resolvedPreviewUrl } : {},
5033
5182
  ...seat.viewMeta?.sourceWidth !== void 0 ? { sourceWidth: seat.viewMeta.sourceWidth } : {},
5034
5183
  ...seat.viewMeta?.sourceHeight !== void 0 ? { sourceHeight: seat.viewMeta.sourceHeight } : {},
5035
5184
  ...seat.viewMeta?.previewWidth !== void 0 ? { previewWidth: seat.viewMeta.previewWidth } : {},
@@ -5047,14 +5196,14 @@ var SeatPicker = class _SeatPicker {
5047
5196
  const { generateSeatPanorama } = await loadPanorama();
5048
5197
  pano2 = generateSeatPanorama(seat, focal, this.allSeats());
5049
5198
  } catch (err) {
5050
- this.opts.onError?.(err);
5199
+ if (generation === this.seatViewGen) this.opts.onError?.(err);
5051
5200
  return;
5052
5201
  }
5053
- if (!this.root || !this.seatViewEnabled()) return;
5202
+ if (generation !== this.seatViewGen || !this.root || !this.seatViewEnabled()) return;
5054
5203
  panoSource = { url: pano2.url, generated: true };
5055
5204
  caption = t2("picker.illustrationCaption", { m: pano2.distanceM });
5056
5205
  }
5057
- this.closeSeatView();
5206
+ this.closeSeatView(false);
5058
5207
  const el = document.createElement("div");
5059
5208
  el.className = "sl-view";
5060
5209
  el.setAttribute("role", "dialog");
@@ -5070,9 +5219,12 @@ var SeatPicker = class _SeatPicker {
5070
5219
  };
5071
5220
  if (delivery.upgradeUrl) {
5072
5221
  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}")`;
5222
+ void this.buyerAssetUrls.resolve(delivery.upgradeUrl).then((url) => {
5223
+ if (!url || loadAbort.signal.aborted) return null;
5224
+ return loadPanoramaImage(url, loadAbort.signal).then(() => url);
5225
+ }).then((url) => {
5226
+ if (!url || !el.isConnected || loadAbort.signal.aborted) return;
5227
+ pano.style.backgroundImage = `url("${url}")`;
5076
5228
  }).catch(() => {
5077
5229
  });
5078
5230
  });
@@ -5148,7 +5300,8 @@ var SeatPicker = class _SeatPicker {
5148
5300
  el.removeEventListener("keydown", onKey);
5149
5301
  };
5150
5302
  }
5151
- closeSeatView() {
5303
+ closeSeatView(cancelPending = true) {
5304
+ if (cancelPending) this.seatViewGen += 1;
5152
5305
  this.viewCleanup?.();
5153
5306
  this.viewCleanup = null;
5154
5307
  this.viewEl?.remove();
@@ -6231,19 +6384,33 @@ var SeatPicker = class _SeatPicker {
6231
6384
  async seatViewFor3d(seatId) {
6232
6385
  const seat = this.allSeats().find((s) => s.id === seatId);
6233
6386
  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
- };
6387
+ if (seat.viewUrl) {
6388
+ try {
6389
+ const previewReference = seat.viewMeta?.previewUrl;
6390
+ const progressive = !!previewReference && previewReference !== seat.viewUrl;
6391
+ const previewUrl = progressive ? await this.buyerAssetUrls.resolve(previewReference) : null;
6392
+ const url = progressive ? seat.viewUrl : await this.buyerAssetUrls.resolve(seat.viewUrl);
6393
+ if (!url) return null;
6394
+ if (progressive && !previewUrl) return null;
6395
+ return {
6396
+ url,
6397
+ ...previewUrl ? { previewUrl } : {},
6398
+ ...progressive ? { resolveUrl: (reference) => this.buyerAssetUrls.resolve(reference) } : {},
6399
+ ...seat.viewMeta?.sourceWidth !== void 0 ? { sourceWidth: seat.viewMeta.sourceWidth } : {},
6400
+ ...seat.viewMeta?.sourceHeight !== void 0 ? { sourceHeight: seat.viewMeta.sourceHeight } : {},
6401
+ ...seat.viewMeta?.previewWidth !== void 0 ? { previewWidth: seat.viewMeta.previewWidth } : {},
6402
+ ...seat.viewMeta?.previewHeight !== void 0 ? { previewHeight: seat.viewMeta.previewHeight } : {},
6403
+ ...seat.viewMeta?.initialBearingDeg !== void 0 ? { initialBearingDeg: seat.viewMeta.initialBearingDeg } : {},
6404
+ ...seat.viewMeta?.initialPitchDeg !== void 0 ? { initialPitchDeg: seat.viewMeta.initialPitchDeg } : {},
6405
+ ...seat.viewMeta?.coverage ? { coverage: seat.viewMeta.coverage } : {},
6406
+ ...seat.viewMeta?.capturedAt ? { capturedAt: seat.viewMeta.capturedAt } : {},
6407
+ ...seat.viewMeta?.sourceLabel ? { sourceLabel: seat.viewMeta.sourceLabel } : {}
6408
+ };
6409
+ } catch (error) {
6410
+ this.opts.onError?.(error);
6411
+ return null;
6412
+ }
6413
+ }
6247
6414
  const doc = this.controller.doc;
6248
6415
  if (!doc) return null;
6249
6416
  const activeId = this.controller.getActiveFloorId();
@@ -6826,6 +6993,7 @@ var SeatPicker = class _SeatPicker {
6826
6993
  this.closeSeatView();
6827
6994
  this.closeCheckoutPanel();
6828
6995
  this.exit3d();
6996
+ this.buyerAssetUrls.dispose();
6829
6997
  this.stopHoldTimer();
6830
6998
  if (this.toastTimer) clearTimeout(this.toastTimer);
6831
6999
  if (this.liveTimer) clearTimeout(this.liveTimer);