@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.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -85,10 +95,13 @@ var PubApi = class {
|
|
|
85
95
|
body: { selections, ...ttlMs ? { ttlMs } : {}, ...replaceHoldId ? { replaceHoldId } : {} }
|
|
86
96
|
});
|
|
87
97
|
}
|
|
88
|
-
|
|
98
|
+
// `zoneId` scopes the pick to one zone and `ttlMs` carries the host's checkout
|
|
99
|
+
// window — both are part of the route contract, and dropping either here made
|
|
100
|
+
// the SDK quietly pick venue-wide and hold for the server default instead.
|
|
101
|
+
bestAvailable(key, qty, categoryKey, zoneId, ttlMs) {
|
|
89
102
|
return request(this.base, `/pub/events/${encodeURIComponent(key)}/best-available`, {
|
|
90
103
|
method: "POST",
|
|
91
|
-
body: { qty, ...categoryKey ? { categoryKey } : {} }
|
|
104
|
+
body: { qty, ...categoryKey ? { categoryKey } : {}, ...zoneId ? { zoneId } : {}, ...ttlMs ? { ttlMs } : {} }
|
|
92
105
|
});
|
|
93
106
|
}
|
|
94
107
|
resume(key, holdId) {
|
|
@@ -191,6 +204,7 @@ var SeatingChart = class {
|
|
|
191
204
|
this.rendered = false;
|
|
192
205
|
return this;
|
|
193
206
|
}
|
|
207
|
+
this.controller.setViewMode(this.opts.initialView ?? "flat");
|
|
194
208
|
this.mode_ = info.mode === "test" ? "test" : "live";
|
|
195
209
|
if (this.opts.seatTooltip !== false) {
|
|
196
210
|
const tip = document.createElement("div");
|
|
@@ -355,19 +369,30 @@ var SeatingChart = class {
|
|
|
355
369
|
const h = await this.controller.holdGA(areaId, qty, options);
|
|
356
370
|
return h ? { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items } : null;
|
|
357
371
|
}
|
|
358
|
-
/**
|
|
359
|
-
|
|
372
|
+
/**
|
|
373
|
+
* Ask the server for the `qty` best free seats and hold them atomically.
|
|
374
|
+
* `options.ttlMs` sets the checkout window exactly like {@link hold}; omit it
|
|
375
|
+
* and the server falls back to the event setting, then its own default.
|
|
376
|
+
*/
|
|
377
|
+
async bestAvailable(qty, categoryKey, options = {}) {
|
|
360
378
|
try {
|
|
361
|
-
return await this.bestAvailableOrThrow(qty, categoryKey);
|
|
379
|
+
return await this.bestAvailableOrThrow(qty, categoryKey, options);
|
|
362
380
|
} catch (err) {
|
|
363
381
|
this.opts.onError?.(err);
|
|
364
382
|
return null;
|
|
365
383
|
}
|
|
366
384
|
}
|
|
367
385
|
/** @internal Throwing variant of {@link bestAvailable} for the native host adapter. See {@link holdOrThrow}. */
|
|
368
|
-
async bestAvailableOrThrow(qty, categoryKey) {
|
|
369
|
-
const h = await this.controller.bestAvailable(qty, categoryKey);
|
|
370
|
-
return h ? {
|
|
386
|
+
async bestAvailableOrThrow(qty, categoryKey, options = {}) {
|
|
387
|
+
const h = await this.controller.bestAvailable(qty, categoryKey, options);
|
|
388
|
+
return h ? {
|
|
389
|
+
holdId: h.holdId,
|
|
390
|
+
expiresAt: h.expiresAt,
|
|
391
|
+
labels: h.labels,
|
|
392
|
+
seats: h.seats,
|
|
393
|
+
items: h.items,
|
|
394
|
+
...options.zoneId ? { zoneId: options.zoneId } : {}
|
|
395
|
+
} : null;
|
|
371
396
|
}
|
|
372
397
|
/**
|
|
373
398
|
* Choose a ticket tier for a selected seat (e.g. Adult → Child). The seat's
|
|
@@ -399,6 +424,14 @@ var SeatingChart = class {
|
|
|
399
424
|
setColorblindSafe(on) {
|
|
400
425
|
this.controller.setColorblindSafe(on);
|
|
401
426
|
}
|
|
427
|
+
/** Switch between flat, isometric and exact-seat Perspective 2.5D views. */
|
|
428
|
+
setViewMode(mode) {
|
|
429
|
+
this.controller.setViewMode(mode);
|
|
430
|
+
}
|
|
431
|
+
/** Current canvas projection. */
|
|
432
|
+
getViewMode() {
|
|
433
|
+
return this.controller.getViewMode();
|
|
434
|
+
}
|
|
402
435
|
/** Zoom in one step (same increment as the wheel/pinch gesture). */
|
|
403
436
|
zoomIn() {
|
|
404
437
|
this.controller.zoomIn();
|
|
@@ -1067,6 +1100,7 @@ var EmbeddedDesigner = class {
|
|
|
1067
1100
|
|
|
1068
1101
|
// src/SeatPicker.ts
|
|
1069
1102
|
var import_core2 = require("@seatlayer/core");
|
|
1103
|
+
var import_meta = {};
|
|
1070
1104
|
var DEFAULT_API_BASE2 = "https://api.seatlayer.io";
|
|
1071
1105
|
var DEFAULT_MAX_SELECTION2 = 10;
|
|
1072
1106
|
var EXTEND_PROMPT_MS = 6e4;
|
|
@@ -1098,6 +1132,41 @@ function escapeOption(value) {
|
|
|
1098
1132
|
"'": "'"
|
|
1099
1133
|
})[character]);
|
|
1100
1134
|
}
|
|
1135
|
+
var SEATLAYER_MODULE_URL = (() => {
|
|
1136
|
+
if (typeof document !== "undefined" && document.currentScript instanceof HTMLScriptElement && document.currentScript.src) {
|
|
1137
|
+
return document.currentScript.src;
|
|
1138
|
+
}
|
|
1139
|
+
try {
|
|
1140
|
+
const u = import_meta.url;
|
|
1141
|
+
if (typeof u === "string" && u) return u;
|
|
1142
|
+
} catch {
|
|
1143
|
+
}
|
|
1144
|
+
return void 0;
|
|
1145
|
+
})();
|
|
1146
|
+
var _webgl2Cache = null;
|
|
1147
|
+
function hasWebGL2() {
|
|
1148
|
+
if (_webgl2Cache !== null) return _webgl2Cache;
|
|
1149
|
+
try {
|
|
1150
|
+
if (typeof document === "undefined") return _webgl2Cache = false;
|
|
1151
|
+
const canvas = document.createElement("canvas");
|
|
1152
|
+
_webgl2Cache = !!canvas.getContext("webgl2");
|
|
1153
|
+
} catch {
|
|
1154
|
+
_webgl2Cache = false;
|
|
1155
|
+
}
|
|
1156
|
+
return _webgl2Cache;
|
|
1157
|
+
}
|
|
1158
|
+
async function loadVenue3d() {
|
|
1159
|
+
if (typeof __SEATLAYER_CDN__ !== "undefined" && __SEATLAYER_CDN__) {
|
|
1160
|
+
const base = SEATLAYER_MODULE_URL ?? (typeof location !== "undefined" ? location.href : void 0);
|
|
1161
|
+
if (!base) throw new Error("seatlayer: cannot resolve the 3D view chunk URL");
|
|
1162
|
+
const url = new URL("./seatlayer-view3d.mjs", base).href;
|
|
1163
|
+
return import(
|
|
1164
|
+
/* @vite-ignore */
|
|
1165
|
+
url
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
return import("@seatlayer/core/view3d");
|
|
1169
|
+
}
|
|
1101
1170
|
var STYLE_ID = "seatlayer-picker-style";
|
|
1102
1171
|
var CSS = `
|
|
1103
1172
|
.sl-picker{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:420px;overflow:hidden;
|
|
@@ -1629,6 +1698,45 @@ var CSS = `
|
|
|
1629
1698
|
.sl-projection button.on{background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
1630
1699
|
.sl-picker[data-layout="narrow"] .sl-projection button{min-width:38px;min-height:32px;padding:5px 8px;font-size:9.5px}
|
|
1631
1700
|
|
|
1701
|
+
/* 3D venue overlay \u2014 mounts over the (paused) Konva stage inside the map host.
|
|
1702
|
+
Carries its own gradient so it paints instantly before the scene builds, and
|
|
1703
|
+
cross-fades on enter/exit via a compositor-only opacity transition. Sits below
|
|
1704
|
+
the anchored chrome (z-index:5) and the confirm card (z-index:10) so the
|
|
1705
|
+
Map|3D toggle and the seat confirm both stay usable over it. */
|
|
1706
|
+
.sl-view3d{position:absolute;inset:0;z-index:4;opacity:0;touch-action:none;
|
|
1707
|
+
transition:opacity .3s ease;background:radial-gradient(120% 120% at 50% 0%,#191f28 0%,#0d1014 70%)}
|
|
1708
|
+
.sl-view3d canvas{display:block;width:100%;height:100%}
|
|
1709
|
+
[data-view3d=on] .sl-chips,[data-view3d=on] .sl-rungs{display:none}
|
|
1710
|
+
.sl-view3d-back{position:absolute;top:12px;left:12px;z-index:2;display:inline-flex;align-items:center;gap:6px;
|
|
1711
|
+
padding:7px 13px 7px 9px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.03em;
|
|
1712
|
+
color:#e6edf3;background:rgba(10,14,20,.62);border:1px solid rgba(255,255,255,.22);backdrop-filter:blur(6px)}
|
|
1713
|
+
.sl-view3d-back:hover,.sl-view3d-back:focus-visible{background:rgba(16,22,30,.82);border-color:rgba(255,255,255,.4)}
|
|
1714
|
+
.sl-view3d-back svg{width:15px;height:15px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
1715
|
+
/* While immersed, the 2D-only chrome is meaningless \u2014 hide it, keep Map|3D. */
|
|
1716
|
+
.sl-picker[data-view3d="on"] .sl-rungs,
|
|
1717
|
+
.sl-picker[data-view3d="on"] .sl-floors,
|
|
1718
|
+
.sl-picker[data-view3d="on"] .sl-zoom,
|
|
1719
|
+
.sl-picker[data-view3d="on"] .sl-seccard,
|
|
1720
|
+
.sl-picker[data-view3d="on"] .sl-minimap{display:none!important}
|
|
1721
|
+
/* The confirm card bottom-sheets over 3D (no 2D screen anchor to track). */
|
|
1722
|
+
.sl-picker[data-view3d="on"] .sl-confirm{left:50%!important;top:auto!important;bottom:16px;
|
|
1723
|
+
transform:translateX(-50%);width:min(342px,calc(100% - 24px))}
|
|
1724
|
+
.sl-picker[data-view3d="on"] .sl-confirm[data-placement]{transform:translateX(-50%)}
|
|
1725
|
+
|
|
1726
|
+
/* Plain "View from here" action shown when no real photo exists (the synthetic
|
|
1727
|
+
thumb is suppressed at card size \u2014 full-screen is where it earns its keep). */
|
|
1728
|
+
.sl-confirm-viewbtn{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
|
|
1729
|
+
display:flex;align-items:center;justify-content:center;gap:7px;font-size:12px;font-weight:800;
|
|
1730
|
+
color:var(--sl-text);background:transparent;transition:border-color .15s,background .15s}
|
|
1731
|
+
.sl-confirm-viewbtn:hover,.sl-confirm-viewbtn:focus-visible{border-color:var(--sl-accent);background:color-mix(in srgb,var(--sl-accent) 10%,transparent)}
|
|
1732
|
+
/* confirm-card "See it in 3D" / "View from this seat" action \u2014 the purchase-
|
|
1733
|
+
moment bridge into the cinematic. Styled like the view-from-seat button. */
|
|
1734
|
+
.sl-confirm-3d{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
|
|
1735
|
+
display:flex;align-items:center;justify-content:center;gap:7px;font-size:12px;font-weight:800;
|
|
1736
|
+
color:var(--sl-text);background:color-mix(in srgb,var(--sl-accent) 12%,transparent);transition:border-color .15s,background .15s}
|
|
1737
|
+
.sl-confirm-3d:hover,.sl-confirm-3d:focus-visible{border-color:var(--sl-accent);background:color-mix(in srgb,var(--sl-accent) 20%,transparent)}
|
|
1738
|
+
.sl-confirm-3d svg{width:15px;height:15px;stroke:currentColor;stroke-width:1.9;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
1739
|
+
|
|
1632
1740
|
/* multi-floor switcher (flows within the left-rail region) */
|
|
1633
1741
|
.sl-floors{display:none;flex-direction:column;gap:6px;max-width:100%}
|
|
1634
1742
|
.sl-floors.on{display:flex}
|
|
@@ -1856,6 +1964,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
1856
1964
|
// arena / multi-floor / seat-view chrome
|
|
1857
1965
|
this.rungsEl = null;
|
|
1858
1966
|
this.projectionEl = null;
|
|
1967
|
+
// --- 3D venue view (Map | 3D) ---
|
|
1968
|
+
this.buyerView = "map";
|
|
1969
|
+
this.view3dEl = null;
|
|
1970
|
+
this.view3dHandle = null;
|
|
1971
|
+
/** Monotonic token so a stale async mount (buyer left before OGL finished
|
|
1972
|
+
* loading) never installs its handle over a newer state. */
|
|
1973
|
+
this.view3dGen = 0;
|
|
1974
|
+
/** Seat whose 2D confirm card launched "See it in 3D"; re-shown on return. */
|
|
1975
|
+
this.view3dReturnSeat = null;
|
|
1859
1976
|
this.floorsEl = null;
|
|
1860
1977
|
this.secCardEl = null;
|
|
1861
1978
|
this.viewEl = null;
|
|
@@ -1906,6 +2023,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
1906
2023
|
this.lastAvailFloorId = "";
|
|
1907
2024
|
this.availQuietUntil = 0;
|
|
1908
2025
|
this.liveTimer = null;
|
|
2026
|
+
/** `perspective` (2.5D) is retired from the buyer surface — accept it for
|
|
2027
|
+
* source compatibility but coerce to `flat` with a one-time deprecation warn. */
|
|
2028
|
+
this.perspectiveWarned = false;
|
|
1909
2029
|
if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
|
|
1910
2030
|
if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
|
|
1911
2031
|
if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
|
|
@@ -1924,12 +2044,14 @@ var SeatPicker = class _SeatPicker {
|
|
|
1924
2044
|
onSelectionChange: () => {
|
|
1925
2045
|
this.syncTray();
|
|
1926
2046
|
if (this.committedSelection().length) this.collapseSectionCard();
|
|
2047
|
+
this.syncSelectionTo3d();
|
|
1927
2048
|
},
|
|
1928
2049
|
onStatusChange: () => {
|
|
1929
2050
|
this.syncPrices();
|
|
1930
2051
|
this.evictTakenSelections();
|
|
1931
2052
|
this.detectBooked();
|
|
1932
2053
|
this.refreshMinimap();
|
|
2054
|
+
this.pushAvailabilityTo3d();
|
|
1933
2055
|
},
|
|
1934
2056
|
onHoldExpired: () => {
|
|
1935
2057
|
this.hold = null;
|
|
@@ -2018,19 +2140,26 @@ var SeatPicker = class _SeatPicker {
|
|
|
2018
2140
|
const realPhoto = seat.viewUrl ?? "";
|
|
2019
2141
|
const hasStage = this.chartHasStage();
|
|
2020
2142
|
if (!realPhoto && !hasStage) return "";
|
|
2021
|
-
let url = realPhoto;
|
|
2022
2143
|
let distance = null;
|
|
2023
|
-
if (!
|
|
2144
|
+
if (!realPhoto) {
|
|
2024
2145
|
try {
|
|
2025
2146
|
const thumb = (0, import_core2.generateSeatThumb)(seat, seat.focalPoint ?? doc.focalPoint);
|
|
2026
|
-
url = thumb.url;
|
|
2027
2147
|
distance = thumb.distanceM ?? null;
|
|
2028
2148
|
} catch {
|
|
2029
2149
|
return "";
|
|
2030
2150
|
}
|
|
2031
2151
|
}
|
|
2032
2152
|
const sightHtml = hasStage ? `<div class="sl-confirm-sight"><span aria-hidden="true">\u2713</span>${distance != null ? (0, import_core2.t)("picker.sightline", { m: distance }) : this.tf("picker.sightlineClear", "Clear sightline")}</div>` : "";
|
|
2033
|
-
|
|
2153
|
+
const viewBtn = realPhoto ? `<button type="button" class="sl-confirm-view sl-confirm-thumbwrap" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: seat.label })}"><img class="sl-confirm-thumb" src="${realPhoto}" alt="" /><span class="sl-confirm-thumb-badge">\u{1F52D} ${this.tf("picker.viewFromHere", "View from here")}</span></button>` : `<button type="button" class="sl-confirm-view sl-confirm-viewbtn" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: seat.label })}"><span aria-hidden="true">\u{1F52D}</span><span>${this.tf("picker.viewFromHere", "View from here")}</span></button>`;
|
|
2154
|
+
return viewBtn + sightHtml;
|
|
2155
|
+
}
|
|
2156
|
+
/** "See it in 3D" (2D) / "View from this seat" (already in 3D) action for the
|
|
2157
|
+
* confirm card. Only when 3D is available — the purchase-moment bridge into
|
|
2158
|
+
* the cinematic that reaches buyers who never press the Map | 3D toggle. */
|
|
2159
|
+
see3dConfirmHtml() {
|
|
2160
|
+
if (!this.canOffer3d()) return "";
|
|
2161
|
+
const label = this.buyerView === "venue3d" ? this.tf("picker.viewFromThisSeat", "View from this seat") : this.tf("picker.seeItIn3d", "See it in 3D");
|
|
2162
|
+
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>`;
|
|
2034
2163
|
}
|
|
2035
2164
|
/** Minimal HTML/attribute escaper for buyer-authored commercial text (notes). */
|
|
2036
2165
|
escCx(value) {
|
|
@@ -2447,7 +2576,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2447
2576
|
}
|
|
2448
2577
|
this.els.boot.remove();
|
|
2449
2578
|
this.salesClosed = !!info.salesClosed;
|
|
2450
|
-
this.controller.setViewMode(this.opts.initialView
|
|
2579
|
+
this.controller.setViewMode(this.normalizeInitialView(this.opts.initialView));
|
|
2451
2580
|
this.buildRegions();
|
|
2452
2581
|
this.regions["bottom-right"].appendChild(this.els.zoom);
|
|
2453
2582
|
this.regions["bottom-center"].appendChild(this.els.toast);
|
|
@@ -3131,15 +3260,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
3131
3260
|
const doc = this.controller.doc;
|
|
3132
3261
|
if (!doc || !this.els.map) return;
|
|
3133
3262
|
const hasSections = doc.objects.some((o) => o.type === "section") || (doc.floors ?? []).some((f) => f.objects.some((o) => o.type === "section"));
|
|
3134
|
-
if (
|
|
3263
|
+
if (this.canOffer3d()) {
|
|
3135
3264
|
const projection = document.createElement("div");
|
|
3136
3265
|
projection.className = "sl-projection";
|
|
3137
3266
|
projection.setAttribute("role", "group");
|
|
3138
|
-
projection.setAttribute("aria-label", "
|
|
3139
|
-
projection.innerHTML = '<button type="button" data-
|
|
3267
|
+
projection.setAttribute("aria-label", "Venue view");
|
|
3268
|
+
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>';
|
|
3140
3269
|
projection.querySelectorAll("button").forEach((button) => {
|
|
3141
3270
|
button.addEventListener("click", () => {
|
|
3142
|
-
this.
|
|
3271
|
+
this.setBuyerView(button.dataset.view);
|
|
3143
3272
|
});
|
|
3144
3273
|
});
|
|
3145
3274
|
this.regions["top-right"].appendChild(projection);
|
|
@@ -3209,13 +3338,17 @@ var SeatPicker = class _SeatPicker {
|
|
|
3209
3338
|
}
|
|
3210
3339
|
syncProjection() {
|
|
3211
3340
|
if (!this.projectionEl) return;
|
|
3212
|
-
const active = this.controller.getViewMode();
|
|
3213
3341
|
this.projectionEl.querySelectorAll("button").forEach((button) => {
|
|
3214
|
-
const on = button.dataset.
|
|
3342
|
+
const on = button.dataset.view === this.buyerView;
|
|
3215
3343
|
button.classList.toggle("on", on);
|
|
3216
3344
|
button.setAttribute("aria-pressed", String(on));
|
|
3217
3345
|
});
|
|
3218
3346
|
}
|
|
3347
|
+
/** Can this picker offer the 3D venue view? Requires the option (default on)
|
|
3348
|
+
* and WebGL2, and a chart to render. */
|
|
3349
|
+
canOffer3d() {
|
|
3350
|
+
return this.opts.enable3D !== false && hasWebGL2() && !!this.controller.doc;
|
|
3351
|
+
}
|
|
3219
3352
|
/** Reflect the active floor onto the switcher rail. */
|
|
3220
3353
|
syncFloors() {
|
|
3221
3354
|
if (!this.floorsEl) return;
|
|
@@ -3432,7 +3565,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
3432
3565
|
}
|
|
3433
3566
|
if (held) {
|
|
3434
3567
|
try {
|
|
3435
|
-
const updated = await this.controller.replaceTableQuantity(table.label, table.quantity);
|
|
3568
|
+
const updated = await this.controller.replaceTableQuantity(table.label, table.quantity, this.opts.holdTtlMs);
|
|
3436
3569
|
if (!updated) {
|
|
3437
3570
|
this.toast("That guest count could not be secured. Your current table hold is unchanged.", "warning");
|
|
3438
3571
|
this.renderTableDialogState();
|
|
@@ -3520,17 +3653,27 @@ var SeatPicker = class _SeatPicker {
|
|
|
3520
3653
|
el.setAttribute("aria-modal", "true");
|
|
3521
3654
|
el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
|
|
3522
3655
|
el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
|
|
3523
|
-
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>`;
|
|
3656
|
+
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>`;
|
|
3524
3657
|
this.els.map.appendChild(el);
|
|
3525
3658
|
this.confirmEl = el;
|
|
3526
3659
|
this.reanchorConfirm();
|
|
3527
3660
|
el.querySelector(".sl-confirm-view")?.addEventListener("click", () => this.openSeatView(seat));
|
|
3661
|
+
el.querySelector(".sl-confirm-3d")?.addEventListener("click", () => {
|
|
3662
|
+
if (this.buyerView === "venue3d") {
|
|
3663
|
+
void this.view3dHandle?.flyToSeat(seat.id);
|
|
3664
|
+
} else {
|
|
3665
|
+
this.view3dReturnSeat = seat;
|
|
3666
|
+
this.dismissConfirm();
|
|
3667
|
+
void this.enter3d(seat.id);
|
|
3668
|
+
}
|
|
3669
|
+
});
|
|
3528
3670
|
el.querySelector(".sl-confirm-add").addEventListener("click", () => this.commitConfirm());
|
|
3529
3671
|
el.querySelector(".sl-confirm-cancel").addEventListener("click", () => this.cancelConfirm());
|
|
3530
3672
|
requestAnimationFrame(() => el.querySelector(".sl-confirm-add")?.focus());
|
|
3531
3673
|
}
|
|
3532
3674
|
reanchorConfirm() {
|
|
3533
3675
|
if (!this.confirmEl || !this.confirmSeat) return;
|
|
3676
|
+
if (this.root?.dataset.view3d === "on") return;
|
|
3534
3677
|
const p = this.controller.worldToScreen({ x: this.confirmSeat.x, y: this.confirmSeat.y });
|
|
3535
3678
|
if (this.root?.dataset.layout === "narrow") return;
|
|
3536
3679
|
const mapWidth = this.els.map.clientWidth;
|
|
@@ -3613,16 +3756,21 @@ var SeatPicker = class _SeatPicker {
|
|
|
3613
3756
|
this.viewEl = el;
|
|
3614
3757
|
const pano = el.querySelector(".sl-view-pano");
|
|
3615
3758
|
pano.style.backgroundImage = `url("${panoUrl}")`;
|
|
3616
|
-
|
|
3617
|
-
|
|
3759
|
+
const VFOV_DEG = 70;
|
|
3760
|
+
const MAX_PITCH_DEG = 35;
|
|
3761
|
+
let zoom = 1;
|
|
3762
|
+
const vh0 = pano.clientHeight || 1;
|
|
3763
|
+
const vw0 = pano.clientWidth || 1;
|
|
3764
|
+
let posX = -(vh0 * (180 / VFOV_DEG) * 2 / 2 - vw0 / 2);
|
|
3618
3765
|
let posY = 0;
|
|
3619
3766
|
const apply = () => {
|
|
3620
3767
|
const h = pano.clientHeight || 1;
|
|
3621
|
-
const bgH = h * zoom;
|
|
3768
|
+
const bgH = h * (180 / VFOV_DEG) * zoom;
|
|
3622
3769
|
const overV = Math.max(0, bgH - h);
|
|
3623
|
-
|
|
3770
|
+
const pitchLimit = Math.min(overV / 2, MAX_PITCH_DEG / 180 * bgH);
|
|
3771
|
+
posY = Math.min(pitchLimit, Math.max(-pitchLimit, posY));
|
|
3624
3772
|
pano.style.backgroundSize = `auto ${bgH}px`;
|
|
3625
|
-
pano.style.backgroundPosition = `${posX}px ${posY
|
|
3773
|
+
pano.style.backgroundPosition = `${posX}px ${posY - overV / 2}px`;
|
|
3626
3774
|
};
|
|
3627
3775
|
apply();
|
|
3628
3776
|
let dragging = false;
|
|
@@ -4377,9 +4525,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
4377
4525
|
this.controller.setColorblindSafe(on);
|
|
4378
4526
|
writeStoredColorblind(on);
|
|
4379
4527
|
}
|
|
4380
|
-
/** Switch the
|
|
4528
|
+
/** Switch the underlying 2D renderer projection (flat / isometric). The buyer
|
|
4529
|
+
* UI no longer exposes `perspective`; it is coerced to `flat`. */
|
|
4381
4530
|
setViewMode(mode) {
|
|
4382
|
-
this.controller.setViewMode(mode);
|
|
4531
|
+
this.controller.setViewMode(this.normalizeInitialView(mode));
|
|
4383
4532
|
this.syncProjection();
|
|
4384
4533
|
this.dismissConfirm();
|
|
4385
4534
|
}
|
|
@@ -4387,6 +4536,178 @@ var SeatPicker = class _SeatPicker {
|
|
|
4387
4536
|
getViewMode() {
|
|
4388
4537
|
return this.controller.getViewMode();
|
|
4389
4538
|
}
|
|
4539
|
+
normalizeInitialView(mode) {
|
|
4540
|
+
if (mode === "perspective") {
|
|
4541
|
+
if (!this.perspectiveWarned) {
|
|
4542
|
+
this.perspectiveWarned = true;
|
|
4543
|
+
console.warn(
|
|
4544
|
+
"[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."
|
|
4545
|
+
);
|
|
4546
|
+
}
|
|
4547
|
+
return "flat";
|
|
4548
|
+
}
|
|
4549
|
+
return mode ?? "flat";
|
|
4550
|
+
}
|
|
4551
|
+
// ---- 3D venue view ---------------------------------------------------------
|
|
4552
|
+
/** Current buyer view: the flat map, or the interactive 3D venue. */
|
|
4553
|
+
getBuyerView() {
|
|
4554
|
+
return this.buyerView;
|
|
4555
|
+
}
|
|
4556
|
+
/** Toggle between the flat Map and the 3D venue view. No-op when unchanged or
|
|
4557
|
+
* when 3D is unavailable. */
|
|
4558
|
+
setBuyerView(view) {
|
|
4559
|
+
if (view === this.buyerView) return;
|
|
4560
|
+
if (view === "venue3d") void this.enter3d();
|
|
4561
|
+
else this.exit3d();
|
|
4562
|
+
}
|
|
4563
|
+
/** SeatStatus → the view3d palette state. Selection is layered separately. */
|
|
4564
|
+
seatState3dFor(id) {
|
|
4565
|
+
switch (this.controller.getStatus(id)) {
|
|
4566
|
+
case "held":
|
|
4567
|
+
return "held";
|
|
4568
|
+
case "booked":
|
|
4569
|
+
return "sold";
|
|
4570
|
+
case "not_for_sale":
|
|
4571
|
+
return "dimmed";
|
|
4572
|
+
default:
|
|
4573
|
+
return "available";
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
/** Push the full live availability snapshot into the 3D handle (selection is
|
|
4577
|
+
* preserved inside the module). Cheap enough per status delta. */
|
|
4578
|
+
pushAvailabilityTo3d() {
|
|
4579
|
+
if (!this.view3dHandle) return;
|
|
4580
|
+
const updates = this.allSeats().map((s) => ({ seatId: s.id, state: this.seatState3dFor(s.id) }));
|
|
4581
|
+
this.view3dHandle.setAvailability(updates);
|
|
4582
|
+
}
|
|
4583
|
+
/** Mirror the authoritative widget selection into the 3D handle. */
|
|
4584
|
+
syncSelectionTo3d() {
|
|
4585
|
+
if (!this.view3dHandle) return;
|
|
4586
|
+
this.view3dHandle.setSelection(this.controller.getSelection().map((s) => s.id));
|
|
4587
|
+
}
|
|
4588
|
+
/** Build the view-from-seat panorama the cinematic dissolves into — reuses the
|
|
4589
|
+
* exact input path as the 2D `openSeatView` (organizer photo, else generated). */
|
|
4590
|
+
seatViewFor3d(seatId) {
|
|
4591
|
+
const seat = this.allSeats().find((s) => s.id === seatId);
|
|
4592
|
+
if (!seat) return null;
|
|
4593
|
+
if (seat.viewUrl) return { url: seat.viewUrl };
|
|
4594
|
+
const doc = this.controller.doc;
|
|
4595
|
+
if (!doc) return null;
|
|
4596
|
+
const activeId = this.controller.getActiveFloorId();
|
|
4597
|
+
const focal = seat.focalPoint ?? doc.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc.focalPoint ?? { x: 0, y: 0 };
|
|
4598
|
+
try {
|
|
4599
|
+
return { url: (0, import_core2.generateSeatPanorama)(seat, focal, this.allSeats()).url };
|
|
4600
|
+
} catch {
|
|
4601
|
+
return null;
|
|
4602
|
+
}
|
|
4603
|
+
}
|
|
4604
|
+
/** Route the module's decoupled analytics into the host callback, tagged buyer. */
|
|
4605
|
+
emit3dAnalytics(event, props) {
|
|
4606
|
+
try {
|
|
4607
|
+
this.opts.onAnalytics?.(event, { ...props, surface: "buyer" });
|
|
4608
|
+
} catch {
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4611
|
+
/** A 3D seat tap runs the SAME selection path as a 2D tap: toggle through the
|
|
4612
|
+
* controller, then raise the shared confirm card (bottom-sheeted in 3D). */
|
|
4613
|
+
onView3dSeatPick(seatId) {
|
|
4614
|
+
if (this.salesClosed) {
|
|
4615
|
+
this.toast(this.tf("picker.salesClosedToast", "Sales are closed for this event."), "warning");
|
|
4616
|
+
this.syncSelectionTo3d();
|
|
4617
|
+
return;
|
|
4618
|
+
}
|
|
4619
|
+
const seat = this.allSeats().find((s) => s.id === seatId);
|
|
4620
|
+
if (!seat) return;
|
|
4621
|
+
const already = this.committedSelection().some((s) => s.id === seatId);
|
|
4622
|
+
if (already) {
|
|
4623
|
+
this.controller.deselect([seatId]);
|
|
4624
|
+
this.dismissConfirm();
|
|
4625
|
+
return;
|
|
4626
|
+
}
|
|
4627
|
+
this.flashPickedSeat(seatId);
|
|
4628
|
+
this.controller.select([seatId]);
|
|
4629
|
+
if (this.opts.confirmSelection !== false) this.showConfirm(seat);
|
|
4630
|
+
else this.syncTray();
|
|
4631
|
+
}
|
|
4632
|
+
async enter3d(flySeatId) {
|
|
4633
|
+
if (this.view3dEl || !this.canOffer3d() || !this.els.map) return;
|
|
4634
|
+
const doc = this.controller.doc;
|
|
4635
|
+
if (!doc) return;
|
|
4636
|
+
this.buyerView = "venue3d";
|
|
4637
|
+
this.root?.setAttribute("data-view3d", "on");
|
|
4638
|
+
this.dismissConfirm();
|
|
4639
|
+
this.syncProjection();
|
|
4640
|
+
const overlay = document.createElement("div");
|
|
4641
|
+
overlay.className = "sl-view3d";
|
|
4642
|
+
overlay.setAttribute("role", "group");
|
|
4643
|
+
overlay.setAttribute("aria-label", "Interactive 3D venue view");
|
|
4644
|
+
const back = document.createElement("button");
|
|
4645
|
+
back.type = "button";
|
|
4646
|
+
back.className = "sl-view3d-back";
|
|
4647
|
+
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>`;
|
|
4648
|
+
back.addEventListener("click", () => this.exit3d());
|
|
4649
|
+
overlay.appendChild(back);
|
|
4650
|
+
this.els.map.appendChild(overlay);
|
|
4651
|
+
this.view3dEl = overlay;
|
|
4652
|
+
requestAnimationFrame(() => {
|
|
4653
|
+
overlay.style.opacity = "1";
|
|
4654
|
+
});
|
|
4655
|
+
const gen = ++this.view3dGen;
|
|
4656
|
+
try {
|
|
4657
|
+
const seats = (0, import_core2.expandChart)(doc);
|
|
4658
|
+
const mod = await loadVenue3d();
|
|
4659
|
+
if (gen !== this.view3dGen || this.buyerView !== "venue3d" || this.view3dEl !== overlay) return;
|
|
4660
|
+
const handle = mod.mountVenue3D(overlay, { doc, seats }, {
|
|
4661
|
+
onSeatPick: (id) => this.onView3dSeatPick(id),
|
|
4662
|
+
// Deferred off the tap gesture: generateSeatPanorama walks every seat
|
|
4663
|
+
// (O(n) on a 13k chart), and the module prefetches at pick — by the
|
|
4664
|
+
// time the flight lands (~2.5s) the idle render has long finished. A
|
|
4665
|
+
// null view REJECTS so the module's no-panorama path keeps the buyer
|
|
4666
|
+
// in orbit instead of dissolving into an empty overlay.
|
|
4667
|
+
getSeatView: (id) => new Promise((resolve, reject) => {
|
|
4668
|
+
const run = () => {
|
|
4669
|
+
const view = this.seatViewFor3d(id);
|
|
4670
|
+
if (view) resolve(view);
|
|
4671
|
+
else reject(new Error("seat_view_unavailable"));
|
|
4672
|
+
};
|
|
4673
|
+
const ric = globalThis.requestIdleCallback;
|
|
4674
|
+
if (typeof ric === "function") ric(run, { timeout: 1500 });
|
|
4675
|
+
else setTimeout(run, 50);
|
|
4676
|
+
}),
|
|
4677
|
+
onAnalytics: (event, props) => this.emit3dAnalytics(event, props)
|
|
4678
|
+
});
|
|
4679
|
+
this.view3dHandle = handle;
|
|
4680
|
+
this.pushAvailabilityTo3d();
|
|
4681
|
+
this.syncSelectionTo3d();
|
|
4682
|
+
if (flySeatId) void handle.flyToSeat(flySeatId);
|
|
4683
|
+
} catch (err) {
|
|
4684
|
+
this.opts.onError?.(err);
|
|
4685
|
+
this.exit3d();
|
|
4686
|
+
}
|
|
4687
|
+
}
|
|
4688
|
+
exit3d() {
|
|
4689
|
+
if (this.buyerView !== "venue3d" && !this.view3dEl) return;
|
|
4690
|
+
this.view3dGen++;
|
|
4691
|
+
this.buyerView = "map";
|
|
4692
|
+
this.root?.removeAttribute("data-view3d");
|
|
4693
|
+
try {
|
|
4694
|
+
this.view3dHandle?.dispose();
|
|
4695
|
+
} catch {
|
|
4696
|
+
}
|
|
4697
|
+
this.view3dHandle = null;
|
|
4698
|
+
const overlay = this.view3dEl;
|
|
4699
|
+
this.view3dEl = null;
|
|
4700
|
+
if (overlay) {
|
|
4701
|
+
overlay.style.opacity = "0";
|
|
4702
|
+
setTimeout(() => overlay.remove(), 320);
|
|
4703
|
+
}
|
|
4704
|
+
this.syncProjection();
|
|
4705
|
+
const seat = this.view3dReturnSeat;
|
|
4706
|
+
this.view3dReturnSeat = null;
|
|
4707
|
+
if (seat && this.committedSelection().some((s) => s.id === seat.id) && this.opts.confirmSelection !== false) {
|
|
4708
|
+
this.showConfirm(seat);
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4390
4711
|
/** Current active/restored hold reflected in the tray. */
|
|
4391
4712
|
getCurrentHold() {
|
|
4392
4713
|
return this.hold;
|
|
@@ -4411,7 +4732,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
4411
4732
|
button.innerHTML = '<span class="sl-ba-spin" aria-hidden="true"></span>Finding\u2026';
|
|
4412
4733
|
}
|
|
4413
4734
|
try {
|
|
4414
|
-
const h = await this.controller.bestAvailable(qty, categoryKey, opts);
|
|
4735
|
+
const h = await this.controller.bestAvailable(qty, categoryKey, { ...opts, ttlMs: this.opts.holdTtlMs });
|
|
4415
4736
|
if (h) {
|
|
4416
4737
|
this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
|
|
4417
4738
|
this.handedOff = false;
|
|
@@ -4478,6 +4799,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
4478
4799
|
this.closeConfirm();
|
|
4479
4800
|
this.dismissTableDialog(false);
|
|
4480
4801
|
this.closeSeatView();
|
|
4802
|
+
this.exit3d();
|
|
4481
4803
|
this.stopHoldTimer();
|
|
4482
4804
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
4483
4805
|
if (this.liveTimer) clearTimeout(this.liveTimer);
|
|
@@ -6129,7 +6451,7 @@ var SeatManager = class {
|
|
|
6129
6451
|
const category = this.doc?.categories.find((item) => item.key === seat.categoryKey);
|
|
6130
6452
|
const sectionMetric = this.controlRoomSnapshot?.revenue.bySection.find((row) => row.sectionId === sectionId);
|
|
6131
6453
|
const object = this.doc?.objects.find((item) => item.id === seat.rowId);
|
|
6132
|
-
const
|
|
6454
|
+
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;
|
|
6133
6455
|
const itemKind = seat.kind === "booth" ? "Booth" : "Seat";
|
|
6134
6456
|
this.els.rail.innerHTML = `
|
|
6135
6457
|
<p class="slm-eyebrow">${itemKind} details</p>
|
|
@@ -6139,7 +6461,7 @@ var SeatManager = class {
|
|
|
6139
6461
|
<div class="slm-inspect-grid">
|
|
6140
6462
|
<div><span>Status</span><b>${statusLabel[status]}</b></div>
|
|
6141
6463
|
<div><span>Section</span><b>${esc(sectionLabel)}</b></div>
|
|
6142
|
-
${
|
|
6464
|
+
${location2 ? `<div><span>${location2.label}</span><b>${esc(location2.value)}</b></div>` : ""}
|
|
6143
6465
|
<div><span>Category</span><b>${esc(category?.label ?? seat.categoryKey)}</b></div>
|
|
6144
6466
|
<div><span>Sold in section</span><b>${sectionMetric ? `${sectionMetric.booked} of ${sectionMetric.total}` : "\u2014"}</b></div>
|
|
6145
6467
|
<div><span>Section revenue</span><b>${sectionMetric && this.controlRoomSnapshot ? fmtMoney(sectionMetric.bookedRevenue, this.controlRoomSnapshot.currency) : "\u2014"}</b></div>
|