@seatlayer/js 0.28.4 → 0.30.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/README.md +32 -5
- package/dist/index.cjs +370 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -4
- package/dist/index.d.ts +104 -4
- package/dist/index.js +365 -23
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
# @seatlayer/js
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@seatlayer/js)
|
|
4
|
+
[](https://www.npmjs.com/package/@seatlayer/js)
|
|
5
|
+
[](../../LICENSE)
|
|
6
|
+
|
|
7
|
+
The framework-agnostic SeatLayer embed SDK. Render the complete buyer picker or
|
|
8
|
+
a headless interactive chart, let buyers select and **hold** inventory in the
|
|
9
|
+
browser, then **book** it from your trusted server.
|
|
10
|
+
|
|
11
|
+
[Package on npm](https://www.npmjs.com/package/@seatlayer/js) ·
|
|
12
|
+
[Install guide](https://docs.seatlayer.io/buyer-sdk/install/) ·
|
|
13
|
+
[Live demo](https://app.seatlayer.io/demo/play) ·
|
|
14
|
+
[Website](https://seatlayer.io/developers/) ·
|
|
15
|
+
[AI Toolkit](https://github.com/seatlayer/seatlayer-ai-toolkit)
|
|
6
16
|
|
|
7
17
|
Works in plain HTML and any framework (React, Vue, Svelte, Angular…). For React,
|
|
8
18
|
prefer the [`@seatlayer/react`](https://www.npmjs.com/package/@seatlayer/react) wrapper.
|
|
9
19
|
|
|
20
|
+
## Install
|
|
21
|
+
|
|
10
22
|
```bash
|
|
11
23
|
npm install @seatlayer/js
|
|
12
24
|
```
|
|
@@ -186,5 +198,20 @@ The full `SeatPicker` automatically restores an active hold after same-tab
|
|
|
186
198
|
checkout navigation and lets the buyer remove individual held tickets. Set
|
|
187
199
|
`restoreHold: false` and pass `initialHoldId` when your host owns persistence.
|
|
188
200
|
|
|
189
|
-
The browser **holds**; your **server books** with a secret key. See
|
|
190
|
-
[integration
|
|
201
|
+
The browser **holds**; your **server books** with a secret key. See
|
|
202
|
+
[how the integration works](https://docs.seatlayer.io/start/how-it-works/).
|
|
203
|
+
|
|
204
|
+
## Related resources
|
|
205
|
+
|
|
206
|
+
- [Buyer SDK documentation](https://docs.seatlayer.io/buyer-sdk/install/)
|
|
207
|
+
- [SeatingChart reference](https://docs.seatlayer.io/buyer-sdk/seating-chart/)
|
|
208
|
+
- [Holds and checkout](https://docs.seatlayer.io/buyer-sdk/holds-and-checkout/)
|
|
209
|
+
- [Complete checkout example](https://docs.seatlayer.io/examples/complete-checkout/)
|
|
210
|
+
- [`@seatlayer/react`](https://www.npmjs.com/package/@seatlayer/react)
|
|
211
|
+
- [iOS SDK](https://github.com/seatlayer/seatlayer-ios)
|
|
212
|
+
- [Flutter package](https://pub.dev/packages/seatlayer)
|
|
213
|
+
- [Agent-readable documentation](https://docs.seatlayer.io/llms.txt)
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
MIT © SeatLayer
|
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
|
|
@@ -194,6 +204,7 @@ var SeatingChart = class {
|
|
|
194
204
|
this.rendered = false;
|
|
195
205
|
return this;
|
|
196
206
|
}
|
|
207
|
+
this.controller.setViewMode(this.opts.initialView ?? "flat");
|
|
197
208
|
this.mode_ = info.mode === "test" ? "test" : "live";
|
|
198
209
|
if (this.opts.seatTooltip !== false) {
|
|
199
210
|
const tip = document.createElement("div");
|
|
@@ -374,7 +385,14 @@ var SeatingChart = class {
|
|
|
374
385
|
/** @internal Throwing variant of {@link bestAvailable} for the native host adapter. See {@link holdOrThrow}. */
|
|
375
386
|
async bestAvailableOrThrow(qty, categoryKey, options = {}) {
|
|
376
387
|
const h = await this.controller.bestAvailable(qty, categoryKey, options);
|
|
377
|
-
return h ? {
|
|
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;
|
|
378
396
|
}
|
|
379
397
|
/**
|
|
380
398
|
* Choose a ticket tier for a selected seat (e.g. Adult → Child). The seat's
|
|
@@ -406,6 +424,17 @@ var SeatingChart = class {
|
|
|
406
424
|
setColorblindSafe(on) {
|
|
407
425
|
this.controller.setColorblindSafe(on);
|
|
408
426
|
}
|
|
427
|
+
/** Switch the 2D canvas projection.
|
|
428
|
+
* @deprecated `'isometric'` and `'perspective'` are retired in favour of the
|
|
429
|
+
* real 3D venue view (`setBuyerView('venue3d')`); accepted for source
|
|
430
|
+
* compatibility until the next major. */
|
|
431
|
+
setViewMode(mode) {
|
|
432
|
+
this.controller.setViewMode(mode);
|
|
433
|
+
}
|
|
434
|
+
/** Current canvas projection. */
|
|
435
|
+
getViewMode() {
|
|
436
|
+
return this.controller.getViewMode();
|
|
437
|
+
}
|
|
409
438
|
/** Zoom in one step (same increment as the wheel/pinch gesture). */
|
|
410
439
|
zoomIn() {
|
|
411
440
|
this.controller.zoomIn();
|
|
@@ -1074,6 +1103,7 @@ var EmbeddedDesigner = class {
|
|
|
1074
1103
|
|
|
1075
1104
|
// src/SeatPicker.ts
|
|
1076
1105
|
var import_core2 = require("@seatlayer/core");
|
|
1106
|
+
var import_meta = {};
|
|
1077
1107
|
var DEFAULT_API_BASE2 = "https://api.seatlayer.io";
|
|
1078
1108
|
var DEFAULT_MAX_SELECTION2 = 10;
|
|
1079
1109
|
var EXTEND_PROMPT_MS = 6e4;
|
|
@@ -1105,6 +1135,41 @@ function escapeOption(value) {
|
|
|
1105
1135
|
"'": "'"
|
|
1106
1136
|
})[character]);
|
|
1107
1137
|
}
|
|
1138
|
+
var SEATLAYER_MODULE_URL = (() => {
|
|
1139
|
+
if (typeof document !== "undefined" && document.currentScript instanceof HTMLScriptElement && document.currentScript.src) {
|
|
1140
|
+
return document.currentScript.src;
|
|
1141
|
+
}
|
|
1142
|
+
try {
|
|
1143
|
+
const u = import_meta.url;
|
|
1144
|
+
if (typeof u === "string" && u) return u;
|
|
1145
|
+
} catch {
|
|
1146
|
+
}
|
|
1147
|
+
return void 0;
|
|
1148
|
+
})();
|
|
1149
|
+
var _webgl2Cache = null;
|
|
1150
|
+
function hasWebGL2() {
|
|
1151
|
+
if (_webgl2Cache !== null) return _webgl2Cache;
|
|
1152
|
+
try {
|
|
1153
|
+
if (typeof document === "undefined") return _webgl2Cache = false;
|
|
1154
|
+
const canvas = document.createElement("canvas");
|
|
1155
|
+
_webgl2Cache = !!canvas.getContext("webgl2");
|
|
1156
|
+
} catch {
|
|
1157
|
+
_webgl2Cache = false;
|
|
1158
|
+
}
|
|
1159
|
+
return _webgl2Cache;
|
|
1160
|
+
}
|
|
1161
|
+
async function loadVenue3d() {
|
|
1162
|
+
if (typeof __SEATLAYER_CDN__ !== "undefined" && __SEATLAYER_CDN__) {
|
|
1163
|
+
const base = SEATLAYER_MODULE_URL ?? (typeof location !== "undefined" ? location.href : void 0);
|
|
1164
|
+
if (!base) throw new Error("seatlayer: cannot resolve the 3D view chunk URL");
|
|
1165
|
+
const url = new URL("./seatlayer-view3d.mjs", base).href;
|
|
1166
|
+
return import(
|
|
1167
|
+
/* @vite-ignore */
|
|
1168
|
+
url
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
return import("@seatlayer/core/view3d");
|
|
1172
|
+
}
|
|
1108
1173
|
var STYLE_ID = "seatlayer-picker-style";
|
|
1109
1174
|
var CSS = `
|
|
1110
1175
|
.sl-picker{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:420px;overflow:hidden;
|
|
@@ -1636,6 +1701,45 @@ var CSS = `
|
|
|
1636
1701
|
.sl-projection button.on{background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
1637
1702
|
.sl-picker[data-layout="narrow"] .sl-projection button{min-width:38px;min-height:32px;padding:5px 8px;font-size:9.5px}
|
|
1638
1703
|
|
|
1704
|
+
/* 3D venue overlay \u2014 mounts over the (paused) Konva stage inside the map host.
|
|
1705
|
+
Carries its own gradient so it paints instantly before the scene builds, and
|
|
1706
|
+
cross-fades on enter/exit via a compositor-only opacity transition. Sits below
|
|
1707
|
+
the anchored chrome (z-index:5) and the confirm card (z-index:10) so the
|
|
1708
|
+
Map|3D toggle and the seat confirm both stay usable over it. */
|
|
1709
|
+
.sl-view3d{position:absolute;inset:0;z-index:4;opacity:0;touch-action:none;
|
|
1710
|
+
transition:opacity .3s ease;background:radial-gradient(120% 120% at 50% 0%,#191f28 0%,#0d1014 70%)}
|
|
1711
|
+
.sl-view3d canvas{display:block;width:100%;height:100%}
|
|
1712
|
+
[data-view3d=on] .sl-chips,[data-view3d=on] .sl-rungs{display:none}
|
|
1713
|
+
.sl-view3d-back{position:absolute;top:12px;left:12px;z-index:2;display:inline-flex;align-items:center;gap:6px;
|
|
1714
|
+
padding:7px 13px 7px 9px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.03em;
|
|
1715
|
+
color:#e6edf3;background:rgba(10,14,20,.62);border:1px solid rgba(255,255,255,.22);backdrop-filter:blur(6px)}
|
|
1716
|
+
.sl-view3d-back:hover,.sl-view3d-back:focus-visible{background:rgba(16,22,30,.82);border-color:rgba(255,255,255,.4)}
|
|
1717
|
+
.sl-view3d-back svg{width:15px;height:15px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
1718
|
+
/* While immersed, the 2D-only chrome is meaningless \u2014 hide it, keep Map|3D. */
|
|
1719
|
+
.sl-picker[data-view3d="on"] .sl-rungs,
|
|
1720
|
+
.sl-picker[data-view3d="on"] .sl-floors,
|
|
1721
|
+
.sl-picker[data-view3d="on"] .sl-zoom,
|
|
1722
|
+
.sl-picker[data-view3d="on"] .sl-seccard,
|
|
1723
|
+
.sl-picker[data-view3d="on"] .sl-minimap{display:none!important}
|
|
1724
|
+
/* The confirm card bottom-sheets over 3D (no 2D screen anchor to track). */
|
|
1725
|
+
.sl-picker[data-view3d="on"] .sl-confirm{left:50%!important;top:auto!important;bottom:16px;
|
|
1726
|
+
transform:translateX(-50%);width:min(342px,calc(100% - 24px))}
|
|
1727
|
+
.sl-picker[data-view3d="on"] .sl-confirm[data-placement]{transform:translateX(-50%)}
|
|
1728
|
+
|
|
1729
|
+
/* Plain "View from here" action shown when no real photo exists (the synthetic
|
|
1730
|
+
thumb is suppressed at card size \u2014 full-screen is where it earns its keep). */
|
|
1731
|
+
.sl-confirm-viewbtn{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
|
|
1732
|
+
display:flex;align-items:center;justify-content:center;gap:7px;font-size:12px;font-weight:800;
|
|
1733
|
+
color:var(--sl-text);background:transparent;transition:border-color .15s,background .15s}
|
|
1734
|
+
.sl-confirm-viewbtn:hover,.sl-confirm-viewbtn:focus-visible{border-color:var(--sl-accent);background:color-mix(in srgb,var(--sl-accent) 10%,transparent)}
|
|
1735
|
+
/* confirm-card "See it in 3D" / "View from this seat" action \u2014 the purchase-
|
|
1736
|
+
moment bridge into the cinematic. Styled like the view-from-seat button. */
|
|
1737
|
+
.sl-confirm-3d{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
|
|
1738
|
+
display:flex;align-items:center;justify-content:center;gap:7px;font-size:12px;font-weight:800;
|
|
1739
|
+
color:var(--sl-text);background:color-mix(in srgb,var(--sl-accent) 12%,transparent);transition:border-color .15s,background .15s}
|
|
1740
|
+
.sl-confirm-3d:hover,.sl-confirm-3d:focus-visible{border-color:var(--sl-accent);background:color-mix(in srgb,var(--sl-accent) 20%,transparent)}
|
|
1741
|
+
.sl-confirm-3d svg{width:15px;height:15px;stroke:currentColor;stroke-width:1.9;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
1742
|
+
|
|
1639
1743
|
/* multi-floor switcher (flows within the left-rail region) */
|
|
1640
1744
|
.sl-floors{display:none;flex-direction:column;gap:6px;max-width:100%}
|
|
1641
1745
|
.sl-floors.on{display:flex}
|
|
@@ -1863,6 +1967,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
1863
1967
|
// arena / multi-floor / seat-view chrome
|
|
1864
1968
|
this.rungsEl = null;
|
|
1865
1969
|
this.projectionEl = null;
|
|
1970
|
+
// --- 3D venue view (Map | 3D) ---
|
|
1971
|
+
this.buyerView = "map";
|
|
1972
|
+
this.view3dEl = null;
|
|
1973
|
+
this.view3dHandle = null;
|
|
1974
|
+
/** Monotonic token so a stale async mount (buyer left before OGL finished
|
|
1975
|
+
* loading) never installs its handle over a newer state. */
|
|
1976
|
+
this.view3dGen = 0;
|
|
1977
|
+
/** Seat whose 2D confirm card launched "See it in 3D"; re-shown on return. */
|
|
1978
|
+
this.view3dReturnSeat = null;
|
|
1866
1979
|
this.floorsEl = null;
|
|
1867
1980
|
this.secCardEl = null;
|
|
1868
1981
|
this.viewEl = null;
|
|
@@ -1913,6 +2026,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
1913
2026
|
this.lastAvailFloorId = "";
|
|
1914
2027
|
this.availQuietUntil = 0;
|
|
1915
2028
|
this.liveTimer = null;
|
|
2029
|
+
/** `perspective` (2.5D) is retired from the buyer surface — accept it for
|
|
2030
|
+
* source compatibility but coerce to `flat` with a one-time deprecation warn. */
|
|
2031
|
+
this.perspectiveWarned = false;
|
|
1916
2032
|
if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
|
|
1917
2033
|
if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
|
|
1918
2034
|
if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
|
|
@@ -1931,12 +2047,14 @@ var SeatPicker = class _SeatPicker {
|
|
|
1931
2047
|
onSelectionChange: () => {
|
|
1932
2048
|
this.syncTray();
|
|
1933
2049
|
if (this.committedSelection().length) this.collapseSectionCard();
|
|
2050
|
+
this.syncSelectionTo3d();
|
|
1934
2051
|
},
|
|
1935
2052
|
onStatusChange: () => {
|
|
1936
2053
|
this.syncPrices();
|
|
1937
2054
|
this.evictTakenSelections();
|
|
1938
2055
|
this.detectBooked();
|
|
1939
2056
|
this.refreshMinimap();
|
|
2057
|
+
this.pushAvailabilityTo3d();
|
|
1940
2058
|
},
|
|
1941
2059
|
onHoldExpired: () => {
|
|
1942
2060
|
this.hold = null;
|
|
@@ -2025,19 +2143,26 @@ var SeatPicker = class _SeatPicker {
|
|
|
2025
2143
|
const realPhoto = seat.viewUrl ?? "";
|
|
2026
2144
|
const hasStage = this.chartHasStage();
|
|
2027
2145
|
if (!realPhoto && !hasStage) return "";
|
|
2028
|
-
let url = realPhoto;
|
|
2029
2146
|
let distance = null;
|
|
2030
|
-
if (!
|
|
2147
|
+
if (!realPhoto) {
|
|
2031
2148
|
try {
|
|
2032
2149
|
const thumb = (0, import_core2.generateSeatThumb)(seat, seat.focalPoint ?? doc.focalPoint);
|
|
2033
|
-
url = thumb.url;
|
|
2034
2150
|
distance = thumb.distanceM ?? null;
|
|
2035
2151
|
} catch {
|
|
2036
2152
|
return "";
|
|
2037
2153
|
}
|
|
2038
2154
|
}
|
|
2039
2155
|
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>` : "";
|
|
2040
|
-
|
|
2156
|
+
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>`;
|
|
2157
|
+
return viewBtn + sightHtml;
|
|
2158
|
+
}
|
|
2159
|
+
/** "See it in 3D" (2D) / "View from this seat" (already in 3D) action for the
|
|
2160
|
+
* confirm card. Only when 3D is available — the purchase-moment bridge into
|
|
2161
|
+
* the cinematic that reaches buyers who never press the Map | 3D toggle. */
|
|
2162
|
+
see3dConfirmHtml() {
|
|
2163
|
+
if (!this.canOffer3d()) return "";
|
|
2164
|
+
const label = this.buyerView === "venue3d" ? this.tf("picker.viewFromThisSeat", "View from this seat") : this.tf("picker.seeItIn3d", "See it in 3D");
|
|
2165
|
+
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>`;
|
|
2041
2166
|
}
|
|
2042
2167
|
/** Minimal HTML/attribute escaper for buyer-authored commercial text (notes). */
|
|
2043
2168
|
escCx(value) {
|
|
@@ -2454,7 +2579,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2454
2579
|
}
|
|
2455
2580
|
this.els.boot.remove();
|
|
2456
2581
|
this.salesClosed = !!info.salesClosed;
|
|
2457
|
-
this.controller.setViewMode(this.opts.initialView
|
|
2582
|
+
this.controller.setViewMode(this.normalizeInitialView(this.opts.initialView));
|
|
2458
2583
|
this.buildRegions();
|
|
2459
2584
|
this.regions["bottom-right"].appendChild(this.els.zoom);
|
|
2460
2585
|
this.regions["bottom-center"].appendChild(this.els.toast);
|
|
@@ -3138,15 +3263,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
3138
3263
|
const doc = this.controller.doc;
|
|
3139
3264
|
if (!doc || !this.els.map) return;
|
|
3140
3265
|
const hasSections = doc.objects.some((o) => o.type === "section") || (doc.floors ?? []).some((f) => f.objects.some((o) => o.type === "section"));
|
|
3141
|
-
if (
|
|
3266
|
+
if (this.canOffer3d()) {
|
|
3142
3267
|
const projection = document.createElement("div");
|
|
3143
3268
|
projection.className = "sl-projection";
|
|
3144
3269
|
projection.setAttribute("role", "group");
|
|
3145
|
-
projection.setAttribute("aria-label", "
|
|
3146
|
-
projection.innerHTML = '<button type="button" data-
|
|
3270
|
+
projection.setAttribute("aria-label", "Venue view");
|
|
3271
|
+
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>';
|
|
3147
3272
|
projection.querySelectorAll("button").forEach((button) => {
|
|
3148
3273
|
button.addEventListener("click", () => {
|
|
3149
|
-
this.
|
|
3274
|
+
this.setBuyerView(button.dataset.view);
|
|
3150
3275
|
});
|
|
3151
3276
|
});
|
|
3152
3277
|
this.regions["top-right"].appendChild(projection);
|
|
@@ -3216,13 +3341,17 @@ var SeatPicker = class _SeatPicker {
|
|
|
3216
3341
|
}
|
|
3217
3342
|
syncProjection() {
|
|
3218
3343
|
if (!this.projectionEl) return;
|
|
3219
|
-
const active = this.controller.getViewMode();
|
|
3220
3344
|
this.projectionEl.querySelectorAll("button").forEach((button) => {
|
|
3221
|
-
const on = button.dataset.
|
|
3345
|
+
const on = button.dataset.view === this.buyerView;
|
|
3222
3346
|
button.classList.toggle("on", on);
|
|
3223
3347
|
button.setAttribute("aria-pressed", String(on));
|
|
3224
3348
|
});
|
|
3225
3349
|
}
|
|
3350
|
+
/** Can this picker offer the 3D venue view? Requires the option (default on)
|
|
3351
|
+
* and WebGL2, and a chart to render. */
|
|
3352
|
+
canOffer3d() {
|
|
3353
|
+
return this.opts.enable3D !== false && hasWebGL2() && !!this.controller.doc;
|
|
3354
|
+
}
|
|
3226
3355
|
/** Reflect the active floor onto the switcher rail. */
|
|
3227
3356
|
syncFloors() {
|
|
3228
3357
|
if (!this.floorsEl) return;
|
|
@@ -3527,17 +3656,27 @@ var SeatPicker = class _SeatPicker {
|
|
|
3527
3656
|
el.setAttribute("aria-modal", "true");
|
|
3528
3657
|
el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
|
|
3529
3658
|
el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
|
|
3530
|
-
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>`;
|
|
3659
|
+
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>`;
|
|
3531
3660
|
this.els.map.appendChild(el);
|
|
3532
3661
|
this.confirmEl = el;
|
|
3533
3662
|
this.reanchorConfirm();
|
|
3534
3663
|
el.querySelector(".sl-confirm-view")?.addEventListener("click", () => this.openSeatView(seat));
|
|
3664
|
+
el.querySelector(".sl-confirm-3d")?.addEventListener("click", () => {
|
|
3665
|
+
if (this.buyerView === "venue3d") {
|
|
3666
|
+
void this.view3dHandle?.flyToSeat(seat.id);
|
|
3667
|
+
} else {
|
|
3668
|
+
this.view3dReturnSeat = seat;
|
|
3669
|
+
this.dismissConfirm();
|
|
3670
|
+
void this.enter3d(seat.id);
|
|
3671
|
+
}
|
|
3672
|
+
});
|
|
3535
3673
|
el.querySelector(".sl-confirm-add").addEventListener("click", () => this.commitConfirm());
|
|
3536
3674
|
el.querySelector(".sl-confirm-cancel").addEventListener("click", () => this.cancelConfirm());
|
|
3537
3675
|
requestAnimationFrame(() => el.querySelector(".sl-confirm-add")?.focus());
|
|
3538
3676
|
}
|
|
3539
3677
|
reanchorConfirm() {
|
|
3540
3678
|
if (!this.confirmEl || !this.confirmSeat) return;
|
|
3679
|
+
if (this.root?.dataset.view3d === "on") return;
|
|
3541
3680
|
const p = this.controller.worldToScreen({ x: this.confirmSeat.x, y: this.confirmSeat.y });
|
|
3542
3681
|
if (this.root?.dataset.layout === "narrow") return;
|
|
3543
3682
|
const mapWidth = this.els.map.clientWidth;
|
|
@@ -3620,16 +3759,21 @@ var SeatPicker = class _SeatPicker {
|
|
|
3620
3759
|
this.viewEl = el;
|
|
3621
3760
|
const pano = el.querySelector(".sl-view-pano");
|
|
3622
3761
|
pano.style.backgroundImage = `url("${panoUrl}")`;
|
|
3623
|
-
|
|
3624
|
-
|
|
3762
|
+
const VFOV_DEG = 70;
|
|
3763
|
+
const MAX_PITCH_DEG = 35;
|
|
3764
|
+
let zoom = 1;
|
|
3765
|
+
const vh0 = pano.clientHeight || 1;
|
|
3766
|
+
const vw0 = pano.clientWidth || 1;
|
|
3767
|
+
let posX = -(vh0 * (180 / VFOV_DEG) * 2 / 2 - vw0 / 2);
|
|
3625
3768
|
let posY = 0;
|
|
3626
3769
|
const apply = () => {
|
|
3627
3770
|
const h = pano.clientHeight || 1;
|
|
3628
|
-
const bgH = h * zoom;
|
|
3771
|
+
const bgH = h * (180 / VFOV_DEG) * zoom;
|
|
3629
3772
|
const overV = Math.max(0, bgH - h);
|
|
3630
|
-
|
|
3773
|
+
const pitchLimit = Math.min(overV / 2, MAX_PITCH_DEG / 180 * bgH);
|
|
3774
|
+
posY = Math.min(pitchLimit, Math.max(-pitchLimit, posY));
|
|
3631
3775
|
pano.style.backgroundSize = `auto ${bgH}px`;
|
|
3632
|
-
pano.style.backgroundPosition = `${posX}px ${posY
|
|
3776
|
+
pano.style.backgroundPosition = `${posX}px ${posY - overV / 2}px`;
|
|
3633
3777
|
};
|
|
3634
3778
|
apply();
|
|
3635
3779
|
let dragging = false;
|
|
@@ -4384,9 +4528,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
4384
4528
|
this.controller.setColorblindSafe(on);
|
|
4385
4529
|
writeStoredColorblind(on);
|
|
4386
4530
|
}
|
|
4387
|
-
/** Switch the
|
|
4531
|
+
/** Switch the underlying 2D renderer projection (flat / isometric). The buyer
|
|
4532
|
+
* UI no longer exposes `perspective`; it is coerced to `flat`. */
|
|
4388
4533
|
setViewMode(mode) {
|
|
4389
|
-
this.controller.setViewMode(mode);
|
|
4534
|
+
this.controller.setViewMode(this.normalizeInitialView(mode));
|
|
4390
4535
|
this.syncProjection();
|
|
4391
4536
|
this.dismissConfirm();
|
|
4392
4537
|
}
|
|
@@ -4394,6 +4539,208 @@ var SeatPicker = class _SeatPicker {
|
|
|
4394
4539
|
getViewMode() {
|
|
4395
4540
|
return this.controller.getViewMode();
|
|
4396
4541
|
}
|
|
4542
|
+
normalizeInitialView(mode) {
|
|
4543
|
+
if (mode === "perspective") {
|
|
4544
|
+
if (!this.perspectiveWarned) {
|
|
4545
|
+
this.perspectiveWarned = true;
|
|
4546
|
+
console.warn(
|
|
4547
|
+
"[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."
|
|
4548
|
+
);
|
|
4549
|
+
}
|
|
4550
|
+
return "flat";
|
|
4551
|
+
}
|
|
4552
|
+
return mode ?? "flat";
|
|
4553
|
+
}
|
|
4554
|
+
// ---- 3D venue view ---------------------------------------------------------
|
|
4555
|
+
/** Current buyer view: the flat map, or the interactive 3D venue. */
|
|
4556
|
+
getBuyerView() {
|
|
4557
|
+
return this.buyerView;
|
|
4558
|
+
}
|
|
4559
|
+
/**
|
|
4560
|
+
* Switch between the flat seat **Map** and the interactive **3D venue** view —
|
|
4561
|
+
* the same control the buyer's on-widget `Map | 3D` toggle drives.
|
|
4562
|
+
*
|
|
4563
|
+
* Entering `'venue3d'` with `opts.flyToSeatId` runs the cinematic tour: the
|
|
4564
|
+
* camera flies to the seat and holds in the live scene (a chip offers the
|
|
4565
|
+
* 360° view-from-seat) —
|
|
4566
|
+
* the widget enters 3D and auto-flies the camera to that seat, dissolving into
|
|
4567
|
+
* its view-from-seat panorama (the same chain as tapping "See the view from
|
|
4568
|
+
* here" on a seat's confirm card). When the widget is **already** in the 3D
|
|
4569
|
+
* view, the camera simply flies to the requested seat — the GL scene is not
|
|
4570
|
+
* torn down or rebuilt, so there is no flash or re-entry.
|
|
4571
|
+
*
|
|
4572
|
+
* No-op when the view is unchanged and no `flyToSeatId` is given, or when 3D is
|
|
4573
|
+
* unavailable for this chart.
|
|
4574
|
+
*
|
|
4575
|
+
* @param view `'map'` for the flat picker, `'venue3d'` for the 3D venue.
|
|
4576
|
+
* @param opts.flyToSeatId When entering (or already in) `'venue3d'`, the seat
|
|
4577
|
+
* id to fly the camera to — cinematic → panorama.
|
|
4578
|
+
* Ignored when `view` is `'map'`.
|
|
4579
|
+
*
|
|
4580
|
+
* @example
|
|
4581
|
+
* // Public 3D tour entry — enter 3D and fly straight to the buyer's seat:
|
|
4582
|
+
* picker.setBuyerView('venue3d', { flyToSeatId: 'A-12' });
|
|
4583
|
+
*/
|
|
4584
|
+
setBuyerView(view, opts) {
|
|
4585
|
+
if (view === "map") {
|
|
4586
|
+
this.exit3d();
|
|
4587
|
+
return;
|
|
4588
|
+
}
|
|
4589
|
+
const flyToSeatId = opts?.flyToSeatId;
|
|
4590
|
+
if (this.buyerView === "venue3d") {
|
|
4591
|
+
if (flyToSeatId) void this.view3dHandle?.flyToSeat(flyToSeatId);
|
|
4592
|
+
return;
|
|
4593
|
+
}
|
|
4594
|
+
void this.enter3d(flyToSeatId);
|
|
4595
|
+
}
|
|
4596
|
+
/** SeatStatus → the view3d palette state. Selection is layered separately. */
|
|
4597
|
+
seatState3dFor(id) {
|
|
4598
|
+
switch (this.controller.getStatus(id)) {
|
|
4599
|
+
case "held":
|
|
4600
|
+
return "held";
|
|
4601
|
+
case "booked":
|
|
4602
|
+
return "sold";
|
|
4603
|
+
case "not_for_sale":
|
|
4604
|
+
return "dimmed";
|
|
4605
|
+
default:
|
|
4606
|
+
return "available";
|
|
4607
|
+
}
|
|
4608
|
+
}
|
|
4609
|
+
/** Push the full live availability snapshot into the 3D handle (selection is
|
|
4610
|
+
* preserved inside the module). Cheap enough per status delta. */
|
|
4611
|
+
pushAvailabilityTo3d() {
|
|
4612
|
+
if (!this.view3dHandle) return;
|
|
4613
|
+
const updates = this.allSeats().map((s) => ({ seatId: s.id, state: this.seatState3dFor(s.id) }));
|
|
4614
|
+
this.view3dHandle.setAvailability(updates);
|
|
4615
|
+
}
|
|
4616
|
+
/** Mirror the authoritative widget selection into the 3D handle. */
|
|
4617
|
+
syncSelectionTo3d() {
|
|
4618
|
+
if (!this.view3dHandle) return;
|
|
4619
|
+
this.view3dHandle.setSelection(this.controller.getSelection().map((s) => s.id));
|
|
4620
|
+
}
|
|
4621
|
+
/** Build the view-from-seat panorama the cinematic dissolves into — reuses the
|
|
4622
|
+
* exact input path as the 2D `openSeatView` (organizer photo, else generated). */
|
|
4623
|
+
seatViewFor3d(seatId) {
|
|
4624
|
+
const seat = this.allSeats().find((s) => s.id === seatId);
|
|
4625
|
+
if (!seat) return null;
|
|
4626
|
+
if (seat.viewUrl) return { url: seat.viewUrl };
|
|
4627
|
+
const doc = this.controller.doc;
|
|
4628
|
+
if (!doc) return null;
|
|
4629
|
+
const activeId = this.controller.getActiveFloorId();
|
|
4630
|
+
const focal = seat.focalPoint ?? doc.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc.focalPoint ?? { x: 0, y: 0 };
|
|
4631
|
+
try {
|
|
4632
|
+
return { url: (0, import_core2.generateSeatPanorama)(seat, focal, this.allSeats()).url };
|
|
4633
|
+
} catch {
|
|
4634
|
+
return null;
|
|
4635
|
+
}
|
|
4636
|
+
}
|
|
4637
|
+
/** Route the module's decoupled analytics into the host callback, tagged buyer. */
|
|
4638
|
+
emit3dAnalytics(event, props) {
|
|
4639
|
+
try {
|
|
4640
|
+
this.opts.onAnalytics?.(event, { ...props, surface: "buyer" });
|
|
4641
|
+
} catch {
|
|
4642
|
+
}
|
|
4643
|
+
}
|
|
4644
|
+
/** A 3D seat tap runs the SAME selection path as a 2D tap: toggle through the
|
|
4645
|
+
* controller, then raise the shared confirm card (bottom-sheeted in 3D). */
|
|
4646
|
+
onView3dSeatPick(seatId) {
|
|
4647
|
+
if (this.salesClosed) {
|
|
4648
|
+
this.toast(this.tf("picker.salesClosedToast", "Sales are closed for this event."), "warning");
|
|
4649
|
+
this.syncSelectionTo3d();
|
|
4650
|
+
return;
|
|
4651
|
+
}
|
|
4652
|
+
const seat = this.allSeats().find((s) => s.id === seatId);
|
|
4653
|
+
if (!seat) return;
|
|
4654
|
+
const already = this.committedSelection().some((s) => s.id === seatId);
|
|
4655
|
+
if (already) {
|
|
4656
|
+
this.controller.deselect([seatId]);
|
|
4657
|
+
this.dismissConfirm();
|
|
4658
|
+
return;
|
|
4659
|
+
}
|
|
4660
|
+
this.flashPickedSeat(seatId);
|
|
4661
|
+
this.controller.select([seatId]);
|
|
4662
|
+
if (this.opts.confirmSelection !== false) this.showConfirm(seat);
|
|
4663
|
+
else this.syncTray();
|
|
4664
|
+
}
|
|
4665
|
+
async enter3d(flySeatId) {
|
|
4666
|
+
if (this.view3dEl || !this.canOffer3d() || !this.els.map) return;
|
|
4667
|
+
const doc = this.controller.doc;
|
|
4668
|
+
if (!doc) return;
|
|
4669
|
+
this.buyerView = "venue3d";
|
|
4670
|
+
this.root?.setAttribute("data-view3d", "on");
|
|
4671
|
+
this.dismissConfirm();
|
|
4672
|
+
this.syncProjection();
|
|
4673
|
+
const overlay = document.createElement("div");
|
|
4674
|
+
overlay.className = "sl-view3d";
|
|
4675
|
+
overlay.setAttribute("role", "group");
|
|
4676
|
+
overlay.setAttribute("aria-label", "Interactive 3D venue view");
|
|
4677
|
+
const back = document.createElement("button");
|
|
4678
|
+
back.type = "button";
|
|
4679
|
+
back.className = "sl-view3d-back";
|
|
4680
|
+
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>`;
|
|
4681
|
+
back.addEventListener("click", () => this.exit3d());
|
|
4682
|
+
overlay.appendChild(back);
|
|
4683
|
+
this.els.map.appendChild(overlay);
|
|
4684
|
+
this.view3dEl = overlay;
|
|
4685
|
+
requestAnimationFrame(() => {
|
|
4686
|
+
overlay.style.opacity = "1";
|
|
4687
|
+
});
|
|
4688
|
+
const gen = ++this.view3dGen;
|
|
4689
|
+
try {
|
|
4690
|
+
const seats = (0, import_core2.expandChart)(doc);
|
|
4691
|
+
const mod = await loadVenue3d();
|
|
4692
|
+
if (gen !== this.view3dGen || this.buyerView !== "venue3d" || this.view3dEl !== overlay) return;
|
|
4693
|
+
const handle = mod.mountVenue3D(overlay, { doc, seats }, {
|
|
4694
|
+
onSeatPick: (id) => this.onView3dSeatPick(id),
|
|
4695
|
+
// Deferred off the tap gesture: generateSeatPanorama walks every seat
|
|
4696
|
+
// (O(n) on a 13k chart), and the module prefetches at pick — by the
|
|
4697
|
+
// time the flight lands (~2.5s) the idle render has long finished. A
|
|
4698
|
+
// null view REJECTS so the module's no-panorama path keeps the buyer
|
|
4699
|
+
// in orbit instead of dissolving into an empty overlay.
|
|
4700
|
+
getSeatView: (id) => new Promise((resolve, reject) => {
|
|
4701
|
+
const run = () => {
|
|
4702
|
+
const view = this.seatViewFor3d(id);
|
|
4703
|
+
if (view) resolve(view);
|
|
4704
|
+
else reject(new Error("seat_view_unavailable"));
|
|
4705
|
+
};
|
|
4706
|
+
const ric = globalThis.requestIdleCallback;
|
|
4707
|
+
if (typeof ric === "function") ric(run, { timeout: 1500 });
|
|
4708
|
+
else setTimeout(run, 50);
|
|
4709
|
+
}),
|
|
4710
|
+
onAnalytics: (event, props) => this.emit3dAnalytics(event, props)
|
|
4711
|
+
});
|
|
4712
|
+
this.view3dHandle = handle;
|
|
4713
|
+
this.pushAvailabilityTo3d();
|
|
4714
|
+
this.syncSelectionTo3d();
|
|
4715
|
+
if (flySeatId) void handle.flyToSeat(flySeatId);
|
|
4716
|
+
} catch (err) {
|
|
4717
|
+
this.opts.onError?.(err);
|
|
4718
|
+
this.exit3d();
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
exit3d() {
|
|
4722
|
+
if (this.buyerView !== "venue3d" && !this.view3dEl) return;
|
|
4723
|
+
this.view3dGen++;
|
|
4724
|
+
this.buyerView = "map";
|
|
4725
|
+
this.root?.removeAttribute("data-view3d");
|
|
4726
|
+
try {
|
|
4727
|
+
this.view3dHandle?.dispose();
|
|
4728
|
+
} catch {
|
|
4729
|
+
}
|
|
4730
|
+
this.view3dHandle = null;
|
|
4731
|
+
const overlay = this.view3dEl;
|
|
4732
|
+
this.view3dEl = null;
|
|
4733
|
+
if (overlay) {
|
|
4734
|
+
overlay.style.opacity = "0";
|
|
4735
|
+
setTimeout(() => overlay.remove(), 320);
|
|
4736
|
+
}
|
|
4737
|
+
this.syncProjection();
|
|
4738
|
+
const seat = this.view3dReturnSeat;
|
|
4739
|
+
this.view3dReturnSeat = null;
|
|
4740
|
+
if (seat && this.committedSelection().some((s) => s.id === seat.id) && this.opts.confirmSelection !== false) {
|
|
4741
|
+
this.showConfirm(seat);
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4397
4744
|
/** Current active/restored hold reflected in the tray. */
|
|
4398
4745
|
getCurrentHold() {
|
|
4399
4746
|
return this.hold;
|
|
@@ -4485,6 +4832,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
4485
4832
|
this.closeConfirm();
|
|
4486
4833
|
this.dismissTableDialog(false);
|
|
4487
4834
|
this.closeSeatView();
|
|
4835
|
+
this.exit3d();
|
|
4488
4836
|
this.stopHoldTimer();
|
|
4489
4837
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
4490
4838
|
if (this.liveTimer) clearTimeout(this.liveTimer);
|
|
@@ -6136,7 +6484,7 @@ var SeatManager = class {
|
|
|
6136
6484
|
const category = this.doc?.categories.find((item) => item.key === seat.categoryKey);
|
|
6137
6485
|
const sectionMetric = this.controlRoomSnapshot?.revenue.bySection.find((row) => row.sectionId === sectionId);
|
|
6138
6486
|
const object = this.doc?.objects.find((item) => item.id === seat.rowId);
|
|
6139
|
-
const
|
|
6487
|
+
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;
|
|
6140
6488
|
const itemKind = seat.kind === "booth" ? "Booth" : "Seat";
|
|
6141
6489
|
this.els.rail.innerHTML = `
|
|
6142
6490
|
<p class="slm-eyebrow">${itemKind} details</p>
|
|
@@ -6146,7 +6494,7 @@ var SeatManager = class {
|
|
|
6146
6494
|
<div class="slm-inspect-grid">
|
|
6147
6495
|
<div><span>Status</span><b>${statusLabel[status]}</b></div>
|
|
6148
6496
|
<div><span>Section</span><b>${esc(sectionLabel)}</b></div>
|
|
6149
|
-
${
|
|
6497
|
+
${location2 ? `<div><span>${location2.label}</span><b>${esc(location2.value)}</b></div>` : ""}
|
|
6150
6498
|
<div><span>Category</span><b>${esc(category?.label ?? seat.categoryKey)}</b></div>
|
|
6151
6499
|
<div><span>Sold in section</span><b>${sectionMetric ? `${sectionMetric.booked} of ${sectionMetric.total}` : "\u2014"}</b></div>
|
|
6152
6500
|
<div><span>Section revenue</span><b>${sectionMetric && this.controlRoomSnapshot ? fmtMoney(sectionMetric.bookedRevenue, this.controlRoomSnapshot.currency) : "\u2014"}</b></div>
|