@seatlayer/js 0.28.3 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +353 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +89 -7
- package/dist/index.d.ts +89 -7
- package/dist/index.js +348 -32
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// src/SeatingChart.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
PickerController,
|
|
4
|
+
loadLocale,
|
|
5
|
+
setStringOverrides,
|
|
6
|
+
t
|
|
7
|
+
} from "@seatlayer/core";
|
|
3
8
|
|
|
4
9
|
// src/api.ts
|
|
5
10
|
var ApiError = class extends Error {
|
|
@@ -52,10 +57,13 @@ var PubApi = class {
|
|
|
52
57
|
body: { selections, ...ttlMs ? { ttlMs } : {}, ...replaceHoldId ? { replaceHoldId } : {} }
|
|
53
58
|
});
|
|
54
59
|
}
|
|
55
|
-
|
|
60
|
+
// `zoneId` scopes the pick to one zone and `ttlMs` carries the host's checkout
|
|
61
|
+
// window — both are part of the route contract, and dropping either here made
|
|
62
|
+
// the SDK quietly pick venue-wide and hold for the server default instead.
|
|
63
|
+
bestAvailable(key, qty, categoryKey, zoneId, ttlMs) {
|
|
56
64
|
return request(this.base, `/pub/events/${encodeURIComponent(key)}/best-available`, {
|
|
57
65
|
method: "POST",
|
|
58
|
-
body: { qty, ...categoryKey ? { categoryKey } : {} }
|
|
66
|
+
body: { qty, ...categoryKey ? { categoryKey } : {}, ...zoneId ? { zoneId } : {}, ...ttlMs ? { ttlMs } : {} }
|
|
59
67
|
});
|
|
60
68
|
}
|
|
61
69
|
resume(key, holdId) {
|
|
@@ -158,6 +166,7 @@ var SeatingChart = class {
|
|
|
158
166
|
this.rendered = false;
|
|
159
167
|
return this;
|
|
160
168
|
}
|
|
169
|
+
this.controller.setViewMode(this.opts.initialView ?? "flat");
|
|
161
170
|
this.mode_ = info.mode === "test" ? "test" : "live";
|
|
162
171
|
if (this.opts.seatTooltip !== false) {
|
|
163
172
|
const tip = document.createElement("div");
|
|
@@ -322,19 +331,30 @@ var SeatingChart = class {
|
|
|
322
331
|
const h = await this.controller.holdGA(areaId, qty, options);
|
|
323
332
|
return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
|
|
324
333
|
}
|
|
325
|
-
/**
|
|
326
|
-
|
|
334
|
+
/**
|
|
335
|
+
* Ask the server for the `qty` best free seats and hold them atomically.
|
|
336
|
+
* `options.ttlMs` sets the checkout window exactly like {@link hold}; omit it
|
|
337
|
+
* and the server falls back to the event setting, then its own default.
|
|
338
|
+
*/
|
|
339
|
+
async bestAvailable(qty, categoryKey, options = {}) {
|
|
327
340
|
try {
|
|
328
|
-
return await this.bestAvailableOrThrow(qty, categoryKey);
|
|
341
|
+
return await this.bestAvailableOrThrow(qty, categoryKey, options);
|
|
329
342
|
} catch (err) {
|
|
330
343
|
this.opts.onError?.(err);
|
|
331
344
|
return null;
|
|
332
345
|
}
|
|
333
346
|
}
|
|
334
347
|
/** @internal Throwing variant of {@link bestAvailable} for the native host adapter. See {@link holdOrThrow}. */
|
|
335
|
-
async bestAvailableOrThrow(qty, categoryKey) {
|
|
336
|
-
const h = await this.controller.bestAvailable(qty, categoryKey);
|
|
337
|
-
return h ? {
|
|
348
|
+
async bestAvailableOrThrow(qty, categoryKey, options = {}) {
|
|
349
|
+
const h = await this.controller.bestAvailable(qty, categoryKey, options);
|
|
350
|
+
return h ? {
|
|
351
|
+
holdId: h.holdId,
|
|
352
|
+
expiresAt: h.expiresAt,
|
|
353
|
+
labels: h.labels,
|
|
354
|
+
seats: h.seats,
|
|
355
|
+
items: h.items,
|
|
356
|
+
...options.zoneId ? { zoneId: options.zoneId } : {}
|
|
357
|
+
} : null;
|
|
338
358
|
}
|
|
339
359
|
/**
|
|
340
360
|
* Choose a ticket tier for a selected seat (e.g. Adult → Child). The seat's
|
|
@@ -366,6 +386,14 @@ var SeatingChart = class {
|
|
|
366
386
|
setColorblindSafe(on) {
|
|
367
387
|
this.controller.setColorblindSafe(on);
|
|
368
388
|
}
|
|
389
|
+
/** Switch between flat, isometric and exact-seat Perspective 2.5D views. */
|
|
390
|
+
setViewMode(mode) {
|
|
391
|
+
this.controller.setViewMode(mode);
|
|
392
|
+
}
|
|
393
|
+
/** Current canvas projection. */
|
|
394
|
+
getViewMode() {
|
|
395
|
+
return this.controller.getViewMode();
|
|
396
|
+
}
|
|
369
397
|
/** Zoom in one step (same increment as the wheel/pinch gesture). */
|
|
370
398
|
zoomIn() {
|
|
371
399
|
this.controller.zoomIn();
|
|
@@ -1075,6 +1103,41 @@ function escapeOption(value) {
|
|
|
1075
1103
|
"'": "'"
|
|
1076
1104
|
})[character]);
|
|
1077
1105
|
}
|
|
1106
|
+
var SEATLAYER_MODULE_URL = (() => {
|
|
1107
|
+
if (typeof document !== "undefined" && document.currentScript instanceof HTMLScriptElement && document.currentScript.src) {
|
|
1108
|
+
return document.currentScript.src;
|
|
1109
|
+
}
|
|
1110
|
+
try {
|
|
1111
|
+
const u = import.meta.url;
|
|
1112
|
+
if (typeof u === "string" && u) return u;
|
|
1113
|
+
} catch {
|
|
1114
|
+
}
|
|
1115
|
+
return void 0;
|
|
1116
|
+
})();
|
|
1117
|
+
var _webgl2Cache = null;
|
|
1118
|
+
function hasWebGL2() {
|
|
1119
|
+
if (_webgl2Cache !== null) return _webgl2Cache;
|
|
1120
|
+
try {
|
|
1121
|
+
if (typeof document === "undefined") return _webgl2Cache = false;
|
|
1122
|
+
const canvas = document.createElement("canvas");
|
|
1123
|
+
_webgl2Cache = !!canvas.getContext("webgl2");
|
|
1124
|
+
} catch {
|
|
1125
|
+
_webgl2Cache = false;
|
|
1126
|
+
}
|
|
1127
|
+
return _webgl2Cache;
|
|
1128
|
+
}
|
|
1129
|
+
async function loadVenue3d() {
|
|
1130
|
+
if (typeof __SEATLAYER_CDN__ !== "undefined" && __SEATLAYER_CDN__) {
|
|
1131
|
+
const base = SEATLAYER_MODULE_URL ?? (typeof location !== "undefined" ? location.href : void 0);
|
|
1132
|
+
if (!base) throw new Error("seatlayer: cannot resolve the 3D view chunk URL");
|
|
1133
|
+
const url = new URL("./seatlayer-view3d.mjs", base).href;
|
|
1134
|
+
return import(
|
|
1135
|
+
/* @vite-ignore */
|
|
1136
|
+
url
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
return import("@seatlayer/core/view3d");
|
|
1140
|
+
}
|
|
1078
1141
|
var STYLE_ID = "seatlayer-picker-style";
|
|
1079
1142
|
var CSS = `
|
|
1080
1143
|
.sl-picker{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:420px;overflow:hidden;
|
|
@@ -1606,6 +1669,45 @@ var CSS = `
|
|
|
1606
1669
|
.sl-projection button.on{background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
1607
1670
|
.sl-picker[data-layout="narrow"] .sl-projection button{min-width:38px;min-height:32px;padding:5px 8px;font-size:9.5px}
|
|
1608
1671
|
|
|
1672
|
+
/* 3D venue overlay \u2014 mounts over the (paused) Konva stage inside the map host.
|
|
1673
|
+
Carries its own gradient so it paints instantly before the scene builds, and
|
|
1674
|
+
cross-fades on enter/exit via a compositor-only opacity transition. Sits below
|
|
1675
|
+
the anchored chrome (z-index:5) and the confirm card (z-index:10) so the
|
|
1676
|
+
Map|3D toggle and the seat confirm both stay usable over it. */
|
|
1677
|
+
.sl-view3d{position:absolute;inset:0;z-index:4;opacity:0;touch-action:none;
|
|
1678
|
+
transition:opacity .3s ease;background:radial-gradient(120% 120% at 50% 0%,#191f28 0%,#0d1014 70%)}
|
|
1679
|
+
.sl-view3d canvas{display:block;width:100%;height:100%}
|
|
1680
|
+
[data-view3d=on] .sl-chips,[data-view3d=on] .sl-rungs{display:none}
|
|
1681
|
+
.sl-view3d-back{position:absolute;top:12px;left:12px;z-index:2;display:inline-flex;align-items:center;gap:6px;
|
|
1682
|
+
padding:7px 13px 7px 9px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.03em;
|
|
1683
|
+
color:#e6edf3;background:rgba(10,14,20,.62);border:1px solid rgba(255,255,255,.22);backdrop-filter:blur(6px)}
|
|
1684
|
+
.sl-view3d-back:hover,.sl-view3d-back:focus-visible{background:rgba(16,22,30,.82);border-color:rgba(255,255,255,.4)}
|
|
1685
|
+
.sl-view3d-back svg{width:15px;height:15px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
1686
|
+
/* While immersed, the 2D-only chrome is meaningless \u2014 hide it, keep Map|3D. */
|
|
1687
|
+
.sl-picker[data-view3d="on"] .sl-rungs,
|
|
1688
|
+
.sl-picker[data-view3d="on"] .sl-floors,
|
|
1689
|
+
.sl-picker[data-view3d="on"] .sl-zoom,
|
|
1690
|
+
.sl-picker[data-view3d="on"] .sl-seccard,
|
|
1691
|
+
.sl-picker[data-view3d="on"] .sl-minimap{display:none!important}
|
|
1692
|
+
/* The confirm card bottom-sheets over 3D (no 2D screen anchor to track). */
|
|
1693
|
+
.sl-picker[data-view3d="on"] .sl-confirm{left:50%!important;top:auto!important;bottom:16px;
|
|
1694
|
+
transform:translateX(-50%);width:min(342px,calc(100% - 24px))}
|
|
1695
|
+
.sl-picker[data-view3d="on"] .sl-confirm[data-placement]{transform:translateX(-50%)}
|
|
1696
|
+
|
|
1697
|
+
/* Plain "View from here" action shown when no real photo exists (the synthetic
|
|
1698
|
+
thumb is suppressed at card size \u2014 full-screen is where it earns its keep). */
|
|
1699
|
+
.sl-confirm-viewbtn{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
|
|
1700
|
+
display:flex;align-items:center;justify-content:center;gap:7px;font-size:12px;font-weight:800;
|
|
1701
|
+
color:var(--sl-text);background:transparent;transition:border-color .15s,background .15s}
|
|
1702
|
+
.sl-confirm-viewbtn:hover,.sl-confirm-viewbtn:focus-visible{border-color:var(--sl-accent);background:color-mix(in srgb,var(--sl-accent) 10%,transparent)}
|
|
1703
|
+
/* confirm-card "See it in 3D" / "View from this seat" action \u2014 the purchase-
|
|
1704
|
+
moment bridge into the cinematic. Styled like the view-from-seat button. */
|
|
1705
|
+
.sl-confirm-3d{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
|
|
1706
|
+
display:flex;align-items:center;justify-content:center;gap:7px;font-size:12px;font-weight:800;
|
|
1707
|
+
color:var(--sl-text);background:color-mix(in srgb,var(--sl-accent) 12%,transparent);transition:border-color .15s,background .15s}
|
|
1708
|
+
.sl-confirm-3d:hover,.sl-confirm-3d:focus-visible{border-color:var(--sl-accent);background:color-mix(in srgb,var(--sl-accent) 20%,transparent)}
|
|
1709
|
+
.sl-confirm-3d svg{width:15px;height:15px;stroke:currentColor;stroke-width:1.9;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
1710
|
+
|
|
1609
1711
|
/* multi-floor switcher (flows within the left-rail region) */
|
|
1610
1712
|
.sl-floors{display:none;flex-direction:column;gap:6px;max-width:100%}
|
|
1611
1713
|
.sl-floors.on{display:flex}
|
|
@@ -1833,6 +1935,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
1833
1935
|
// arena / multi-floor / seat-view chrome
|
|
1834
1936
|
this.rungsEl = null;
|
|
1835
1937
|
this.projectionEl = null;
|
|
1938
|
+
// --- 3D venue view (Map | 3D) ---
|
|
1939
|
+
this.buyerView = "map";
|
|
1940
|
+
this.view3dEl = null;
|
|
1941
|
+
this.view3dHandle = null;
|
|
1942
|
+
/** Monotonic token so a stale async mount (buyer left before OGL finished
|
|
1943
|
+
* loading) never installs its handle over a newer state. */
|
|
1944
|
+
this.view3dGen = 0;
|
|
1945
|
+
/** Seat whose 2D confirm card launched "See it in 3D"; re-shown on return. */
|
|
1946
|
+
this.view3dReturnSeat = null;
|
|
1836
1947
|
this.floorsEl = null;
|
|
1837
1948
|
this.secCardEl = null;
|
|
1838
1949
|
this.viewEl = null;
|
|
@@ -1883,6 +1994,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
1883
1994
|
this.lastAvailFloorId = "";
|
|
1884
1995
|
this.availQuietUntil = 0;
|
|
1885
1996
|
this.liveTimer = null;
|
|
1997
|
+
/** `perspective` (2.5D) is retired from the buyer surface — accept it for
|
|
1998
|
+
* source compatibility but coerce to `flat` with a one-time deprecation warn. */
|
|
1999
|
+
this.perspectiveWarned = false;
|
|
1886
2000
|
if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
|
|
1887
2001
|
if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
|
|
1888
2002
|
if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
|
|
@@ -1901,12 +2015,14 @@ var SeatPicker = class _SeatPicker {
|
|
|
1901
2015
|
onSelectionChange: () => {
|
|
1902
2016
|
this.syncTray();
|
|
1903
2017
|
if (this.committedSelection().length) this.collapseSectionCard();
|
|
2018
|
+
this.syncSelectionTo3d();
|
|
1904
2019
|
},
|
|
1905
2020
|
onStatusChange: () => {
|
|
1906
2021
|
this.syncPrices();
|
|
1907
2022
|
this.evictTakenSelections();
|
|
1908
2023
|
this.detectBooked();
|
|
1909
2024
|
this.refreshMinimap();
|
|
2025
|
+
this.pushAvailabilityTo3d();
|
|
1910
2026
|
},
|
|
1911
2027
|
onHoldExpired: () => {
|
|
1912
2028
|
this.hold = null;
|
|
@@ -1995,19 +2111,26 @@ var SeatPicker = class _SeatPicker {
|
|
|
1995
2111
|
const realPhoto = seat.viewUrl ?? "";
|
|
1996
2112
|
const hasStage = this.chartHasStage();
|
|
1997
2113
|
if (!realPhoto && !hasStage) return "";
|
|
1998
|
-
let url = realPhoto;
|
|
1999
2114
|
let distance = null;
|
|
2000
|
-
if (!
|
|
2115
|
+
if (!realPhoto) {
|
|
2001
2116
|
try {
|
|
2002
2117
|
const thumb = generateSeatThumb(seat, seat.focalPoint ?? doc.focalPoint);
|
|
2003
|
-
url = thumb.url;
|
|
2004
2118
|
distance = thumb.distanceM ?? null;
|
|
2005
2119
|
} catch {
|
|
2006
2120
|
return "";
|
|
2007
2121
|
}
|
|
2008
2122
|
}
|
|
2009
2123
|
const sightHtml = hasStage ? `<div class="sl-confirm-sight"><span aria-hidden="true">\u2713</span>${distance != null ? t2("picker.sightline", { m: distance }) : this.tf("picker.sightlineClear", "Clear sightline")}</div>` : "";
|
|
2010
|
-
|
|
2124
|
+
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>`;
|
|
2125
|
+
return viewBtn + sightHtml;
|
|
2126
|
+
}
|
|
2127
|
+
/** "See it in 3D" (2D) / "View from this seat" (already in 3D) action for the
|
|
2128
|
+
* confirm card. Only when 3D is available — the purchase-moment bridge into
|
|
2129
|
+
* the cinematic that reaches buyers who never press the Map | 3D toggle. */
|
|
2130
|
+
see3dConfirmHtml() {
|
|
2131
|
+
if (!this.canOffer3d()) return "";
|
|
2132
|
+
const label = this.buyerView === "venue3d" ? this.tf("picker.viewFromThisSeat", "View from this seat") : this.tf("picker.seeItIn3d", "See it in 3D");
|
|
2133
|
+
return `<button type="button" class="sl-confirm-3d" aria-label="${label}"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2l9 5v10l-9 5-9-5V7z"/><path d="M12 12l9-5M12 12v10M12 12L3 7"/></svg><span>${label}</span></button>`;
|
|
2011
2134
|
}
|
|
2012
2135
|
/** Minimal HTML/attribute escaper for buyer-authored commercial text (notes). */
|
|
2013
2136
|
escCx(value) {
|
|
@@ -2424,7 +2547,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2424
2547
|
}
|
|
2425
2548
|
this.els.boot.remove();
|
|
2426
2549
|
this.salesClosed = !!info.salesClosed;
|
|
2427
|
-
this.controller.setViewMode(this.opts.initialView
|
|
2550
|
+
this.controller.setViewMode(this.normalizeInitialView(this.opts.initialView));
|
|
2428
2551
|
this.buildRegions();
|
|
2429
2552
|
this.regions["bottom-right"].appendChild(this.els.zoom);
|
|
2430
2553
|
this.regions["bottom-center"].appendChild(this.els.toast);
|
|
@@ -3108,15 +3231,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
3108
3231
|
const doc = this.controller.doc;
|
|
3109
3232
|
if (!doc || !this.els.map) return;
|
|
3110
3233
|
const hasSections = doc.objects.some((o) => o.type === "section") || (doc.floors ?? []).some((f) => f.objects.some((o) => o.type === "section"));
|
|
3111
|
-
if (
|
|
3234
|
+
if (this.canOffer3d()) {
|
|
3112
3235
|
const projection = document.createElement("div");
|
|
3113
3236
|
projection.className = "sl-projection";
|
|
3114
3237
|
projection.setAttribute("role", "group");
|
|
3115
|
-
projection.setAttribute("aria-label", "
|
|
3116
|
-
projection.innerHTML = '<button type="button" data-
|
|
3238
|
+
projection.setAttribute("aria-label", "Venue view");
|
|
3239
|
+
projection.innerHTML = '<button type="button" data-view="map" aria-pressed="true" title="Flat 2D map">Map</button><button type="button" data-view="venue3d" aria-pressed="false" title="Interactive 3D venue view">3D</button>';
|
|
3117
3240
|
projection.querySelectorAll("button").forEach((button) => {
|
|
3118
3241
|
button.addEventListener("click", () => {
|
|
3119
|
-
this.
|
|
3242
|
+
this.setBuyerView(button.dataset.view);
|
|
3120
3243
|
});
|
|
3121
3244
|
});
|
|
3122
3245
|
this.regions["top-right"].appendChild(projection);
|
|
@@ -3186,13 +3309,17 @@ var SeatPicker = class _SeatPicker {
|
|
|
3186
3309
|
}
|
|
3187
3310
|
syncProjection() {
|
|
3188
3311
|
if (!this.projectionEl) return;
|
|
3189
|
-
const active = this.controller.getViewMode();
|
|
3190
3312
|
this.projectionEl.querySelectorAll("button").forEach((button) => {
|
|
3191
|
-
const on = button.dataset.
|
|
3313
|
+
const on = button.dataset.view === this.buyerView;
|
|
3192
3314
|
button.classList.toggle("on", on);
|
|
3193
3315
|
button.setAttribute("aria-pressed", String(on));
|
|
3194
3316
|
});
|
|
3195
3317
|
}
|
|
3318
|
+
/** Can this picker offer the 3D venue view? Requires the option (default on)
|
|
3319
|
+
* and WebGL2, and a chart to render. */
|
|
3320
|
+
canOffer3d() {
|
|
3321
|
+
return this.opts.enable3D !== false && hasWebGL2() && !!this.controller.doc;
|
|
3322
|
+
}
|
|
3196
3323
|
/** Reflect the active floor onto the switcher rail. */
|
|
3197
3324
|
syncFloors() {
|
|
3198
3325
|
if (!this.floorsEl) return;
|
|
@@ -3409,7 +3536,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
3409
3536
|
}
|
|
3410
3537
|
if (held) {
|
|
3411
3538
|
try {
|
|
3412
|
-
const updated = await this.controller.replaceTableQuantity(table.label, table.quantity);
|
|
3539
|
+
const updated = await this.controller.replaceTableQuantity(table.label, table.quantity, this.opts.holdTtlMs);
|
|
3413
3540
|
if (!updated) {
|
|
3414
3541
|
this.toast("That guest count could not be secured. Your current table hold is unchanged.", "warning");
|
|
3415
3542
|
this.renderTableDialogState();
|
|
@@ -3497,17 +3624,27 @@ var SeatPicker = class _SeatPicker {
|
|
|
3497
3624
|
el.setAttribute("aria-modal", "true");
|
|
3498
3625
|
el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
|
|
3499
3626
|
el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
|
|
3500
|
-
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) : "") + `<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>`;
|
|
3627
|
+
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>`;
|
|
3501
3628
|
this.els.map.appendChild(el);
|
|
3502
3629
|
this.confirmEl = el;
|
|
3503
3630
|
this.reanchorConfirm();
|
|
3504
3631
|
el.querySelector(".sl-confirm-view")?.addEventListener("click", () => this.openSeatView(seat));
|
|
3632
|
+
el.querySelector(".sl-confirm-3d")?.addEventListener("click", () => {
|
|
3633
|
+
if (this.buyerView === "venue3d") {
|
|
3634
|
+
void this.view3dHandle?.flyToSeat(seat.id);
|
|
3635
|
+
} else {
|
|
3636
|
+
this.view3dReturnSeat = seat;
|
|
3637
|
+
this.dismissConfirm();
|
|
3638
|
+
void this.enter3d(seat.id);
|
|
3639
|
+
}
|
|
3640
|
+
});
|
|
3505
3641
|
el.querySelector(".sl-confirm-add").addEventListener("click", () => this.commitConfirm());
|
|
3506
3642
|
el.querySelector(".sl-confirm-cancel").addEventListener("click", () => this.cancelConfirm());
|
|
3507
3643
|
requestAnimationFrame(() => el.querySelector(".sl-confirm-add")?.focus());
|
|
3508
3644
|
}
|
|
3509
3645
|
reanchorConfirm() {
|
|
3510
3646
|
if (!this.confirmEl || !this.confirmSeat) return;
|
|
3647
|
+
if (this.root?.dataset.view3d === "on") return;
|
|
3511
3648
|
const p = this.controller.worldToScreen({ x: this.confirmSeat.x, y: this.confirmSeat.y });
|
|
3512
3649
|
if (this.root?.dataset.layout === "narrow") return;
|
|
3513
3650
|
const mapWidth = this.els.map.clientWidth;
|
|
@@ -3590,16 +3727,21 @@ var SeatPicker = class _SeatPicker {
|
|
|
3590
3727
|
this.viewEl = el;
|
|
3591
3728
|
const pano = el.querySelector(".sl-view-pano");
|
|
3592
3729
|
pano.style.backgroundImage = `url("${panoUrl}")`;
|
|
3593
|
-
|
|
3594
|
-
|
|
3730
|
+
const VFOV_DEG = 70;
|
|
3731
|
+
const MAX_PITCH_DEG = 35;
|
|
3732
|
+
let zoom = 1;
|
|
3733
|
+
const vh0 = pano.clientHeight || 1;
|
|
3734
|
+
const vw0 = pano.clientWidth || 1;
|
|
3735
|
+
let posX = -(vh0 * (180 / VFOV_DEG) * 2 / 2 - vw0 / 2);
|
|
3595
3736
|
let posY = 0;
|
|
3596
3737
|
const apply = () => {
|
|
3597
3738
|
const h = pano.clientHeight || 1;
|
|
3598
|
-
const bgH = h * zoom;
|
|
3739
|
+
const bgH = h * (180 / VFOV_DEG) * zoom;
|
|
3599
3740
|
const overV = Math.max(0, bgH - h);
|
|
3600
|
-
|
|
3741
|
+
const pitchLimit = Math.min(overV / 2, MAX_PITCH_DEG / 180 * bgH);
|
|
3742
|
+
posY = Math.min(pitchLimit, Math.max(-pitchLimit, posY));
|
|
3601
3743
|
pano.style.backgroundSize = `auto ${bgH}px`;
|
|
3602
|
-
pano.style.backgroundPosition = `${posX}px ${posY
|
|
3744
|
+
pano.style.backgroundPosition = `${posX}px ${posY - overV / 2}px`;
|
|
3603
3745
|
};
|
|
3604
3746
|
apply();
|
|
3605
3747
|
let dragging = false;
|
|
@@ -4354,9 +4496,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
4354
4496
|
this.controller.setColorblindSafe(on);
|
|
4355
4497
|
writeStoredColorblind(on);
|
|
4356
4498
|
}
|
|
4357
|
-
/** Switch the
|
|
4499
|
+
/** Switch the underlying 2D renderer projection (flat / isometric). The buyer
|
|
4500
|
+
* UI no longer exposes `perspective`; it is coerced to `flat`. */
|
|
4358
4501
|
setViewMode(mode) {
|
|
4359
|
-
this.controller.setViewMode(mode);
|
|
4502
|
+
this.controller.setViewMode(this.normalizeInitialView(mode));
|
|
4360
4503
|
this.syncProjection();
|
|
4361
4504
|
this.dismissConfirm();
|
|
4362
4505
|
}
|
|
@@ -4364,6 +4507,178 @@ var SeatPicker = class _SeatPicker {
|
|
|
4364
4507
|
getViewMode() {
|
|
4365
4508
|
return this.controller.getViewMode();
|
|
4366
4509
|
}
|
|
4510
|
+
normalizeInitialView(mode) {
|
|
4511
|
+
if (mode === "perspective") {
|
|
4512
|
+
if (!this.perspectiveWarned) {
|
|
4513
|
+
this.perspectiveWarned = true;
|
|
4514
|
+
console.warn(
|
|
4515
|
+
"[seatlayer] initialView:'perspective' (2.5D) is deprecated for the buyer picker and was coerced to 'flat'. Use the Map | 3D control for the immersive view."
|
|
4516
|
+
);
|
|
4517
|
+
}
|
|
4518
|
+
return "flat";
|
|
4519
|
+
}
|
|
4520
|
+
return mode ?? "flat";
|
|
4521
|
+
}
|
|
4522
|
+
// ---- 3D venue view ---------------------------------------------------------
|
|
4523
|
+
/** Current buyer view: the flat map, or the interactive 3D venue. */
|
|
4524
|
+
getBuyerView() {
|
|
4525
|
+
return this.buyerView;
|
|
4526
|
+
}
|
|
4527
|
+
/** Toggle between the flat Map and the 3D venue view. No-op when unchanged or
|
|
4528
|
+
* when 3D is unavailable. */
|
|
4529
|
+
setBuyerView(view) {
|
|
4530
|
+
if (view === this.buyerView) return;
|
|
4531
|
+
if (view === "venue3d") void this.enter3d();
|
|
4532
|
+
else this.exit3d();
|
|
4533
|
+
}
|
|
4534
|
+
/** SeatStatus → the view3d palette state. Selection is layered separately. */
|
|
4535
|
+
seatState3dFor(id) {
|
|
4536
|
+
switch (this.controller.getStatus(id)) {
|
|
4537
|
+
case "held":
|
|
4538
|
+
return "held";
|
|
4539
|
+
case "booked":
|
|
4540
|
+
return "sold";
|
|
4541
|
+
case "not_for_sale":
|
|
4542
|
+
return "dimmed";
|
|
4543
|
+
default:
|
|
4544
|
+
return "available";
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
4547
|
+
/** Push the full live availability snapshot into the 3D handle (selection is
|
|
4548
|
+
* preserved inside the module). Cheap enough per status delta. */
|
|
4549
|
+
pushAvailabilityTo3d() {
|
|
4550
|
+
if (!this.view3dHandle) return;
|
|
4551
|
+
const updates = this.allSeats().map((s) => ({ seatId: s.id, state: this.seatState3dFor(s.id) }));
|
|
4552
|
+
this.view3dHandle.setAvailability(updates);
|
|
4553
|
+
}
|
|
4554
|
+
/** Mirror the authoritative widget selection into the 3D handle. */
|
|
4555
|
+
syncSelectionTo3d() {
|
|
4556
|
+
if (!this.view3dHandle) return;
|
|
4557
|
+
this.view3dHandle.setSelection(this.controller.getSelection().map((s) => s.id));
|
|
4558
|
+
}
|
|
4559
|
+
/** Build the view-from-seat panorama the cinematic dissolves into — reuses the
|
|
4560
|
+
* exact input path as the 2D `openSeatView` (organizer photo, else generated). */
|
|
4561
|
+
seatViewFor3d(seatId) {
|
|
4562
|
+
const seat = this.allSeats().find((s) => s.id === seatId);
|
|
4563
|
+
if (!seat) return null;
|
|
4564
|
+
if (seat.viewUrl) return { url: seat.viewUrl };
|
|
4565
|
+
const doc = this.controller.doc;
|
|
4566
|
+
if (!doc) return null;
|
|
4567
|
+
const activeId = this.controller.getActiveFloorId();
|
|
4568
|
+
const focal = seat.focalPoint ?? doc.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc.focalPoint ?? { x: 0, y: 0 };
|
|
4569
|
+
try {
|
|
4570
|
+
return { url: generateSeatPanorama(seat, focal, this.allSeats()).url };
|
|
4571
|
+
} catch {
|
|
4572
|
+
return null;
|
|
4573
|
+
}
|
|
4574
|
+
}
|
|
4575
|
+
/** Route the module's decoupled analytics into the host callback, tagged buyer. */
|
|
4576
|
+
emit3dAnalytics(event, props) {
|
|
4577
|
+
try {
|
|
4578
|
+
this.opts.onAnalytics?.(event, { ...props, surface: "buyer" });
|
|
4579
|
+
} catch {
|
|
4580
|
+
}
|
|
4581
|
+
}
|
|
4582
|
+
/** A 3D seat tap runs the SAME selection path as a 2D tap: toggle through the
|
|
4583
|
+
* controller, then raise the shared confirm card (bottom-sheeted in 3D). */
|
|
4584
|
+
onView3dSeatPick(seatId) {
|
|
4585
|
+
if (this.salesClosed) {
|
|
4586
|
+
this.toast(this.tf("picker.salesClosedToast", "Sales are closed for this event."), "warning");
|
|
4587
|
+
this.syncSelectionTo3d();
|
|
4588
|
+
return;
|
|
4589
|
+
}
|
|
4590
|
+
const seat = this.allSeats().find((s) => s.id === seatId);
|
|
4591
|
+
if (!seat) return;
|
|
4592
|
+
const already = this.committedSelection().some((s) => s.id === seatId);
|
|
4593
|
+
if (already) {
|
|
4594
|
+
this.controller.deselect([seatId]);
|
|
4595
|
+
this.dismissConfirm();
|
|
4596
|
+
return;
|
|
4597
|
+
}
|
|
4598
|
+
this.flashPickedSeat(seatId);
|
|
4599
|
+
this.controller.select([seatId]);
|
|
4600
|
+
if (this.opts.confirmSelection !== false) this.showConfirm(seat);
|
|
4601
|
+
else this.syncTray();
|
|
4602
|
+
}
|
|
4603
|
+
async enter3d(flySeatId) {
|
|
4604
|
+
if (this.view3dEl || !this.canOffer3d() || !this.els.map) return;
|
|
4605
|
+
const doc = this.controller.doc;
|
|
4606
|
+
if (!doc) return;
|
|
4607
|
+
this.buyerView = "venue3d";
|
|
4608
|
+
this.root?.setAttribute("data-view3d", "on");
|
|
4609
|
+
this.dismissConfirm();
|
|
4610
|
+
this.syncProjection();
|
|
4611
|
+
const overlay = document.createElement("div");
|
|
4612
|
+
overlay.className = "sl-view3d";
|
|
4613
|
+
overlay.setAttribute("role", "group");
|
|
4614
|
+
overlay.setAttribute("aria-label", "Interactive 3D venue view");
|
|
4615
|
+
const back = document.createElement("button");
|
|
4616
|
+
back.type = "button";
|
|
4617
|
+
back.className = "sl-view3d-back";
|
|
4618
|
+
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>`;
|
|
4619
|
+
back.addEventListener("click", () => this.exit3d());
|
|
4620
|
+
overlay.appendChild(back);
|
|
4621
|
+
this.els.map.appendChild(overlay);
|
|
4622
|
+
this.view3dEl = overlay;
|
|
4623
|
+
requestAnimationFrame(() => {
|
|
4624
|
+
overlay.style.opacity = "1";
|
|
4625
|
+
});
|
|
4626
|
+
const gen = ++this.view3dGen;
|
|
4627
|
+
try {
|
|
4628
|
+
const seats = expandChart(doc);
|
|
4629
|
+
const mod = await loadVenue3d();
|
|
4630
|
+
if (gen !== this.view3dGen || this.buyerView !== "venue3d" || this.view3dEl !== overlay) return;
|
|
4631
|
+
const handle = mod.mountVenue3D(overlay, { doc, seats }, {
|
|
4632
|
+
onSeatPick: (id) => this.onView3dSeatPick(id),
|
|
4633
|
+
// Deferred off the tap gesture: generateSeatPanorama walks every seat
|
|
4634
|
+
// (O(n) on a 13k chart), and the module prefetches at pick — by the
|
|
4635
|
+
// time the flight lands (~2.5s) the idle render has long finished. A
|
|
4636
|
+
// null view REJECTS so the module's no-panorama path keeps the buyer
|
|
4637
|
+
// in orbit instead of dissolving into an empty overlay.
|
|
4638
|
+
getSeatView: (id) => new Promise((resolve, reject) => {
|
|
4639
|
+
const run = () => {
|
|
4640
|
+
const view = this.seatViewFor3d(id);
|
|
4641
|
+
if (view) resolve(view);
|
|
4642
|
+
else reject(new Error("seat_view_unavailable"));
|
|
4643
|
+
};
|
|
4644
|
+
const ric = globalThis.requestIdleCallback;
|
|
4645
|
+
if (typeof ric === "function") ric(run, { timeout: 1500 });
|
|
4646
|
+
else setTimeout(run, 50);
|
|
4647
|
+
}),
|
|
4648
|
+
onAnalytics: (event, props) => this.emit3dAnalytics(event, props)
|
|
4649
|
+
});
|
|
4650
|
+
this.view3dHandle = handle;
|
|
4651
|
+
this.pushAvailabilityTo3d();
|
|
4652
|
+
this.syncSelectionTo3d();
|
|
4653
|
+
if (flySeatId) void handle.flyToSeat(flySeatId);
|
|
4654
|
+
} catch (err) {
|
|
4655
|
+
this.opts.onError?.(err);
|
|
4656
|
+
this.exit3d();
|
|
4657
|
+
}
|
|
4658
|
+
}
|
|
4659
|
+
exit3d() {
|
|
4660
|
+
if (this.buyerView !== "venue3d" && !this.view3dEl) return;
|
|
4661
|
+
this.view3dGen++;
|
|
4662
|
+
this.buyerView = "map";
|
|
4663
|
+
this.root?.removeAttribute("data-view3d");
|
|
4664
|
+
try {
|
|
4665
|
+
this.view3dHandle?.dispose();
|
|
4666
|
+
} catch {
|
|
4667
|
+
}
|
|
4668
|
+
this.view3dHandle = null;
|
|
4669
|
+
const overlay = this.view3dEl;
|
|
4670
|
+
this.view3dEl = null;
|
|
4671
|
+
if (overlay) {
|
|
4672
|
+
overlay.style.opacity = "0";
|
|
4673
|
+
setTimeout(() => overlay.remove(), 320);
|
|
4674
|
+
}
|
|
4675
|
+
this.syncProjection();
|
|
4676
|
+
const seat = this.view3dReturnSeat;
|
|
4677
|
+
this.view3dReturnSeat = null;
|
|
4678
|
+
if (seat && this.committedSelection().some((s) => s.id === seat.id) && this.opts.confirmSelection !== false) {
|
|
4679
|
+
this.showConfirm(seat);
|
|
4680
|
+
}
|
|
4681
|
+
}
|
|
4367
4682
|
/** Current active/restored hold reflected in the tray. */
|
|
4368
4683
|
getCurrentHold() {
|
|
4369
4684
|
return this.hold;
|
|
@@ -4388,7 +4703,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
4388
4703
|
button.innerHTML = '<span class="sl-ba-spin" aria-hidden="true"></span>Finding\u2026';
|
|
4389
4704
|
}
|
|
4390
4705
|
try {
|
|
4391
|
-
const h = await this.controller.bestAvailable(qty, categoryKey, opts);
|
|
4706
|
+
const h = await this.controller.bestAvailable(qty, categoryKey, { ...opts, ttlMs: this.opts.holdTtlMs });
|
|
4392
4707
|
if (h) {
|
|
4393
4708
|
this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
|
|
4394
4709
|
this.handedOff = false;
|
|
@@ -4455,6 +4770,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
4455
4770
|
this.closeConfirm();
|
|
4456
4771
|
this.dismissTableDialog(false);
|
|
4457
4772
|
this.closeSeatView();
|
|
4773
|
+
this.exit3d();
|
|
4458
4774
|
this.stopHoldTimer();
|
|
4459
4775
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
4460
4776
|
if (this.liveTimer) clearTimeout(this.liveTimer);
|
|
@@ -6111,7 +6427,7 @@ var SeatManager = class {
|
|
|
6111
6427
|
const category = this.doc?.categories.find((item) => item.key === seat.categoryKey);
|
|
6112
6428
|
const sectionMetric = this.controlRoomSnapshot?.revenue.bySection.find((row) => row.sectionId === sectionId);
|
|
6113
6429
|
const object = this.doc?.objects.find((item) => item.id === seat.rowId);
|
|
6114
|
-
const
|
|
6430
|
+
const location2 = object?.type === "row" ? { label: "Row", value: object.label } : object?.type === "table" ? { label: "Table", value: object.label } : seat.kind === "booth" ? { label: "Type", value: "Booth" } : null;
|
|
6115
6431
|
const itemKind = seat.kind === "booth" ? "Booth" : "Seat";
|
|
6116
6432
|
this.els.rail.innerHTML = `
|
|
6117
6433
|
<p class="slm-eyebrow">${itemKind} details</p>
|
|
@@ -6121,7 +6437,7 @@ var SeatManager = class {
|
|
|
6121
6437
|
<div class="slm-inspect-grid">
|
|
6122
6438
|
<div><span>Status</span><b>${statusLabel[status]}</b></div>
|
|
6123
6439
|
<div><span>Section</span><b>${esc(sectionLabel)}</b></div>
|
|
6124
|
-
${
|
|
6440
|
+
${location2 ? `<div><span>${location2.label}</span><b>${esc(location2.value)}</b></div>` : ""}
|
|
6125
6441
|
<div><span>Category</span><b>${esc(category?.label ?? seat.categoryKey)}</b></div>
|
|
6126
6442
|
<div><span>Sold in section</span><b>${sectionMetric ? `${sectionMetric.booked} of ${sectionMetric.total}` : "\u2014"}</b></div>
|
|
6127
6443
|
<div><span>Section revenue</span><b>${sectionMetric && this.controlRoomSnapshot ? fmtMoney(sectionMetric.bookedRevenue, this.controlRoomSnapshot.currency) : "\u2014"}</b></div>
|