@seatlayer/js 0.36.3 → 0.38.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 +33 -0
- package/dist/chunk-HMLY7DHA.js +16 -0
- package/dist/chunk-HMLY7DHA.js.map +1 -0
- package/dist/hostedCheckout-PLHKPT6W.js +345 -0
- package/dist/hostedCheckout-PLHKPT6W.js.map +1 -0
- package/dist/index.cjs +1085 -199
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +275 -8
- package/dist/index.d.ts +275 -8
- package/dist/index.js +603 -76
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
8
|
-
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
1
|
+
import {
|
|
2
|
+
__privateAdd,
|
|
3
|
+
__privateGet,
|
|
4
|
+
__privateMethod,
|
|
5
|
+
__privateSet
|
|
6
|
+
} from "./chunk-HMLY7DHA.js";
|
|
9
7
|
|
|
10
8
|
// src/SeatingChart.ts
|
|
11
9
|
import {
|
|
@@ -120,6 +118,41 @@ var PubApi = class {
|
|
|
120
118
|
body: { holdId, ...ttlMs ? { ttlMs } : {} }
|
|
121
119
|
});
|
|
122
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Which gateways this event can actually take money through — the question
|
|
123
|
+
* `checkout: 'hosted'` has to answer BEFORE it shows a buyer a Pay button, so
|
|
124
|
+
* the answer is never discovered by failing a payment.
|
|
125
|
+
*
|
|
126
|
+
* Anonymous, and it discloses no account, key, mode or currency for a gateway
|
|
127
|
+
* that did not match.
|
|
128
|
+
*/
|
|
129
|
+
paymentOptions(key) {
|
|
130
|
+
return this.request(`/pub/events/${encodeURIComponent(key)}/payment-options`);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Turn a live hold into an order and start a payment.
|
|
134
|
+
*
|
|
135
|
+
* The amount is NOT sent: the server recomputes it from the hold's own items,
|
|
136
|
+
* which is the only reason a browser cannot alter what it pays. Nor is the
|
|
137
|
+
* PROVIDER — the event row decides which gateway charges, and a `provider` in
|
|
138
|
+
* the body is checked rather than obeyed (409 `provider_mismatch`). Omitting
|
|
139
|
+
* it is the shape that cannot disagree.
|
|
140
|
+
*/
|
|
141
|
+
startCheckout(key, input) {
|
|
142
|
+
return this.request(`/pub/events/${encodeURIComponent(key)}/checkout`, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
body: input
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Poll an order while its gateway webhook lands. The order id is an
|
|
149
|
+
* unguessable token the buyer already holds, so it acts as the capability —
|
|
150
|
+
* which is also why a buyer returning from a gateway page can be told what
|
|
151
|
+
* happened with nothing but the id in the return URL.
|
|
152
|
+
*/
|
|
153
|
+
orderStatus(orderId) {
|
|
154
|
+
return this.request(`/pub/orders/${encodeURIComponent(orderId)}/status`);
|
|
155
|
+
}
|
|
123
156
|
/**
|
|
124
157
|
* Mint a one-use subscribe ticket for the next socket attempt (protocol doc
|
|
125
158
|
* §3). The bearer travels here, over ordinary HTTPS where CORS and Origin
|
|
@@ -1673,7 +1706,8 @@ var EmbeddedDesigner = class {
|
|
|
1673
1706
|
}
|
|
1674
1707
|
buildSkeleton(overlay) {
|
|
1675
1708
|
const style = document.createElement("style");
|
|
1676
|
-
style.textContent =
|
|
1709
|
+
style.textContent = /* @sl-css */
|
|
1710
|
+
`
|
|
1677
1711
|
@media (prefers-reduced-motion: no-preference) {
|
|
1678
1712
|
@keyframes seatlayer-designer-shimmer {
|
|
1679
1713
|
0% { background-position: -320px 0; }
|
|
@@ -1799,7 +1833,6 @@ import {
|
|
|
1799
1833
|
PickerController as PickerController2,
|
|
1800
1834
|
ACCESSIBILITY_TYPES,
|
|
1801
1835
|
expandChart,
|
|
1802
|
-
generateSeatPanorama,
|
|
1803
1836
|
generateSeatThumb,
|
|
1804
1837
|
loadLocale as loadLocale2,
|
|
1805
1838
|
setStringOverrides as setStringOverrides2,
|
|
@@ -1862,20 +1895,45 @@ function hasWebGL2() {
|
|
|
1862
1895
|
}
|
|
1863
1896
|
return _webgl2Cache;
|
|
1864
1897
|
}
|
|
1898
|
+
function cdnChunkUrl(fileName) {
|
|
1899
|
+
const base = SEATLAYER_MODULE_URL ?? (typeof location !== "undefined" ? location.href : void 0);
|
|
1900
|
+
if (!base) throw new Error(`seatlayer: cannot resolve the ${fileName} chunk URL`);
|
|
1901
|
+
return new URL(`./${fileName}`, base).href;
|
|
1902
|
+
}
|
|
1865
1903
|
async function loadVenue3d() {
|
|
1866
1904
|
if (typeof __SEATLAYER_CDN__ !== "undefined" && __SEATLAYER_CDN__) {
|
|
1867
|
-
const base = SEATLAYER_MODULE_URL ?? (typeof location !== "undefined" ? location.href : void 0);
|
|
1868
|
-
if (!base) throw new Error("seatlayer: cannot resolve the 3D view chunk URL");
|
|
1869
|
-
const url = new URL("./seatlayer-view3d.mjs", base).href;
|
|
1870
1905
|
return import(
|
|
1871
1906
|
/* @vite-ignore */
|
|
1872
|
-
|
|
1907
|
+
cdnChunkUrl("seatlayer-view3d.mjs")
|
|
1873
1908
|
);
|
|
1874
1909
|
}
|
|
1875
1910
|
return import("@seatlayer/core/view3d");
|
|
1876
1911
|
}
|
|
1912
|
+
async function loadPanorama() {
|
|
1913
|
+
if (typeof __SEATLAYER_CDN__ !== "undefined" && __SEATLAYER_CDN__) {
|
|
1914
|
+
return import(
|
|
1915
|
+
/* @vite-ignore */
|
|
1916
|
+
cdnChunkUrl("seatlayer-panorama.mjs")
|
|
1917
|
+
);
|
|
1918
|
+
}
|
|
1919
|
+
return import("@seatlayer/core");
|
|
1920
|
+
}
|
|
1921
|
+
async function loadHostedCheckout() {
|
|
1922
|
+
if (typeof __SEATLAYER_CDN__ !== "undefined" && __SEATLAYER_CDN__) {
|
|
1923
|
+
return import(
|
|
1924
|
+
/* @vite-ignore */
|
|
1925
|
+
cdnChunkUrl("seatlayer-checkout.mjs")
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
return import("./hostedCheckout-PLHKPT6W.js");
|
|
1929
|
+
}
|
|
1930
|
+
function paymentsOffReason(reason) {
|
|
1931
|
+
return reason === "unavailable_for_event" || reason === "payments_off_for_event" ? reason : "not_configured";
|
|
1932
|
+
}
|
|
1877
1933
|
var STYLE_ID = "seatlayer-picker-style";
|
|
1878
|
-
var CSS =
|
|
1934
|
+
var CSS = (
|
|
1935
|
+
/* @sl-css */
|
|
1936
|
+
`
|
|
1879
1937
|
.sl-picker{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:420px;overflow:hidden;
|
|
1880
1938
|
background:var(--sl-bg);color:var(--sl-text);font-family:var(--sl-font);border-radius:var(--sl-radius);
|
|
1881
1939
|
--sl-r-sm:calc(var(--sl-radius) * .55);
|
|
@@ -2612,7 +2670,8 @@ var CSS = `
|
|
|
2612
2670
|
.sl-modal-scrim{position:fixed;inset:0;z-index:2147483000;background:rgba(5,7,12,.66);display:flex;align-items:center;justify-content:center;padding:18px}
|
|
2613
2671
|
.sl-modal-frame{width:min(1200px,100%);height:min(820px,100%);border-radius:16px;overflow:hidden;box-shadow:0 40px 120px -30px rgba(0,0,0,.8)}
|
|
2614
2672
|
@media(max-width:640px){.sl-modal-scrim{padding:0}.sl-modal-frame{width:100%;height:100%;border-radius:0}}
|
|
2615
|
-
|
|
2673
|
+
`
|
|
2674
|
+
);
|
|
2616
2675
|
function ensureStyle() {
|
|
2617
2676
|
if (document.getElementById(STYLE_ID)) return;
|
|
2618
2677
|
const el = document.createElement("style");
|
|
@@ -2677,6 +2736,14 @@ var SeatPicker = class _SeatPicker {
|
|
|
2677
2736
|
this.handedOff = false;
|
|
2678
2737
|
/** Guards single onBooked + single success overlay per hold. */
|
|
2679
2738
|
this.bookedShown = false;
|
|
2739
|
+
/**
|
|
2740
|
+
* In-flight or settled `payment-options` for this event, started at render in
|
|
2741
|
+
* hosted mode. One request, kicked off while the buyer is still choosing, so
|
|
2742
|
+
* pressing Pay does not wait on a lookup whose answer never changes mid-session.
|
|
2743
|
+
*/
|
|
2744
|
+
this.paymentOptions = null;
|
|
2745
|
+
/** The mounted payment card, while one is up. */
|
|
2746
|
+
this.checkoutPanel = null;
|
|
2680
2747
|
this.extendEl = null;
|
|
2681
2748
|
this.bookedEl = null;
|
|
2682
2749
|
this.gaQty = /* @__PURE__ */ new Map();
|
|
@@ -2791,6 +2858,12 @@ var SeatPicker = class _SeatPicker {
|
|
|
2791
2858
|
onObjectUnavailable: (event) => this.opts.onSelectedObjectUnavailable?.(event)
|
|
2792
2859
|
});
|
|
2793
2860
|
this.api = options.transport ?? this.pubApi;
|
|
2861
|
+
if (options.checkout === "hosted" && !this.pubApi) {
|
|
2862
|
+
console.warn(
|
|
2863
|
+
'seatlayer: checkout: "hosted" needs the widget\'s own transport \u2014 a custom `transport` owns its backend, so the picker is staying on onCheckout for this mount.'
|
|
2864
|
+
);
|
|
2865
|
+
}
|
|
2866
|
+
this.checkoutMode = options.checkout === "hosted" && this.pubApi ? "hosted" : "handoff";
|
|
2794
2867
|
this.maxTickets = Math.max(1, Math.floor(options.maxSelection ?? DEFAULT_MAX_SELECTION2));
|
|
2795
2868
|
this.cbSafe = readStoredColorblind() ?? !!options.colorblindSafe;
|
|
2796
2869
|
this.controller = new PickerController2({
|
|
@@ -3154,6 +3227,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
3154
3227
|
ensureStyle();
|
|
3155
3228
|
await loadLocale2(this.opts.locale);
|
|
3156
3229
|
if (this.opts.messages) setStringOverrides2(this.opts.messages);
|
|
3230
|
+
if (this.checkoutMode === "hosted") this.paymentOptions = this.pubApi.paymentOptions(this.opts.event);
|
|
3157
3231
|
const mount = resolveContainer3(this.opts.container);
|
|
3158
3232
|
const root = document.createElement("div");
|
|
3159
3233
|
root.className = "sl-picker";
|
|
@@ -3451,8 +3525,40 @@ var SeatPicker = class _SeatPicker {
|
|
|
3451
3525
|
if (this.salesClosed) this.applySalesClosed();
|
|
3452
3526
|
this.syncPrices();
|
|
3453
3527
|
this.syncTray();
|
|
3528
|
+
this.resumeHostedOrder();
|
|
3454
3529
|
return this;
|
|
3455
3530
|
}
|
|
3531
|
+
/**
|
|
3532
|
+
* A hosted gateway returned this buyer to a page that runs the widget, with
|
|
3533
|
+
* `?order=…&status=…` in the URL. Pick the order up and finish the story.
|
|
3534
|
+
*
|
|
3535
|
+
* Only `success` resumes. `cancelled` means the buyer backed out at the
|
|
3536
|
+
* gateway and their seats are still held — the map they are looking at IS the
|
|
3537
|
+
* right screen, and opening a card to say "you cancelled" would be noise.
|
|
3538
|
+
*
|
|
3539
|
+
* The two parameters are then stripped with `replaceState`, because they are a
|
|
3540
|
+
* one-shot instruction: leaving them in place would re-open the confirmation
|
|
3541
|
+
* on every later navigation, and would carry an order id into browser history
|
|
3542
|
+
* and any Referer this page later sends. `status` is only ever removed
|
|
3543
|
+
* alongside an `order` we actually consumed, so a host page that uses a
|
|
3544
|
+
* `status` parameter of its own keeps it.
|
|
3545
|
+
*/
|
|
3546
|
+
resumeHostedOrder() {
|
|
3547
|
+
if (this.checkoutMode !== "hosted" || typeof location === "undefined") return;
|
|
3548
|
+
const params = new URLSearchParams(location.search);
|
|
3549
|
+
const orderId = params.get("order");
|
|
3550
|
+
if (!orderId) return;
|
|
3551
|
+
const status = params.get("status");
|
|
3552
|
+
params.delete("order");
|
|
3553
|
+
params.delete("status");
|
|
3554
|
+
try {
|
|
3555
|
+
const query = params.toString();
|
|
3556
|
+
history.replaceState(history.state, "", `${location.pathname}${query ? `?${query}` : ""}${location.hash}`);
|
|
3557
|
+
} catch {
|
|
3558
|
+
}
|
|
3559
|
+
if (status !== "success") return;
|
|
3560
|
+
void this.openCheckoutPanel({ kind: "resume", orderId });
|
|
3561
|
+
}
|
|
3456
3562
|
/**
|
|
3457
3563
|
* Move layout-dependent chrome between its wide dock (map regions / zoom
|
|
3458
3564
|
* column) and its narrow dock (the sheet's consolidated Filters row), and
|
|
@@ -4432,7 +4538,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
4432
4538
|
this.els.map.appendChild(el);
|
|
4433
4539
|
this.confirmEl = el;
|
|
4434
4540
|
this.reanchorConfirm();
|
|
4435
|
-
el.querySelector(".sl-confirm-view")?.addEventListener("click", () => this.openSeatView(seat));
|
|
4541
|
+
el.querySelector(".sl-confirm-view")?.addEventListener("click", () => void this.openSeatView(seat));
|
|
4436
4542
|
el.querySelector(".sl-confirm-3d")?.addEventListener("click", () => {
|
|
4437
4543
|
if (this.buyerView === "venue3d") {
|
|
4438
4544
|
void this.view3dHandle?.flyToSeat(seat.id);
|
|
@@ -4503,10 +4609,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
4503
4609
|
* uploaded photo (seat.viewUrl) when present, else a panorama generated from
|
|
4504
4610
|
* the chart geometry — the stage placed at this seat's true bearing + size.
|
|
4505
4611
|
* Zero extra dependencies: an equirectangular image panned with `repeat-x`.
|
|
4612
|
+
*
|
|
4613
|
+
* Async only because the generator is a lazy chunk (see `loadPanorama`); an
|
|
4614
|
+
* organizer photo needs no generator and never waits on it. The two callers
|
|
4615
|
+
* are click handlers, so nothing observes the promise.
|
|
4506
4616
|
*/
|
|
4507
|
-
openSeatView(seat) {
|
|
4617
|
+
async openSeatView(seat) {
|
|
4508
4618
|
if (!this.root || !this.seatViewEnabled()) return;
|
|
4509
|
-
this.closeSeatView();
|
|
4510
4619
|
const doc = this.controller.doc;
|
|
4511
4620
|
const activeId = this.controller.getActiveFloorId();
|
|
4512
4621
|
const focal = seat.focalPoint ?? doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
|
|
@@ -4518,10 +4627,19 @@ var SeatPicker = class _SeatPicker {
|
|
|
4518
4627
|
caption = t2("picker.panorama360");
|
|
4519
4628
|
real = true;
|
|
4520
4629
|
} else {
|
|
4521
|
-
|
|
4630
|
+
let pano2;
|
|
4631
|
+
try {
|
|
4632
|
+
const { generateSeatPanorama } = await loadPanorama();
|
|
4633
|
+
pano2 = generateSeatPanorama(seat, focal, this.allSeats());
|
|
4634
|
+
} catch (err) {
|
|
4635
|
+
this.opts.onError?.(err);
|
|
4636
|
+
return;
|
|
4637
|
+
}
|
|
4638
|
+
if (!this.root || !this.seatViewEnabled()) return;
|
|
4522
4639
|
panoUrl = pano2.url;
|
|
4523
4640
|
caption = t2("picker.illustrationCaption", { m: pano2.distanceM });
|
|
4524
4641
|
}
|
|
4642
|
+
this.closeSeatView();
|
|
4525
4643
|
const el = document.createElement("div");
|
|
4526
4644
|
el.className = "sl-view";
|
|
4527
4645
|
el.setAttribute("role", "dialog");
|
|
@@ -4887,7 +5005,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
4887
5005
|
this.els.tray.querySelectorAll(".sl-chip .view[data-view-label]").forEach((btn) => {
|
|
4888
5006
|
btn.addEventListener("click", () => {
|
|
4889
5007
|
const seat = this.controller.seatByLabel(btn.dataset.viewLabel);
|
|
4890
|
-
if (seat) this.openSeatView(seat);
|
|
5008
|
+
if (seat) void this.openSeatView(seat);
|
|
4891
5009
|
});
|
|
4892
5010
|
});
|
|
4893
5011
|
this.els.tray.querySelectorAll(".sl-chip[data-locate]").forEach((chip) => {
|
|
@@ -5030,7 +5148,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
5030
5148
|
const seats = this.hold.seats ?? committed;
|
|
5031
5149
|
this.handedOff = true;
|
|
5032
5150
|
this.setCtaPhase("checkout");
|
|
5033
|
-
this.
|
|
5151
|
+
this.checkoutHandoff(this.hold, seats);
|
|
5034
5152
|
return;
|
|
5035
5153
|
}
|
|
5036
5154
|
this.holdingLabels = new Set(committed.map((seat) => seat.label));
|
|
@@ -5059,7 +5177,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
5059
5177
|
this.flashHeldSeats(hold);
|
|
5060
5178
|
this.setCtaPhase("checkout");
|
|
5061
5179
|
this.emitHoldChange();
|
|
5062
|
-
this.
|
|
5180
|
+
this.checkoutHandoff(hold, hold.seats ?? chosenSeats);
|
|
5063
5181
|
} catch (err) {
|
|
5064
5182
|
this.opts.onError?.(err);
|
|
5065
5183
|
const problem = err;
|
|
@@ -5164,6 +5282,109 @@ var SeatPicker = class _SeatPicker {
|
|
|
5164
5282
|
this.bookedEl?.classList.add("on");
|
|
5165
5283
|
this.opts.onBooked?.(handoff);
|
|
5166
5284
|
}
|
|
5285
|
+
/**
|
|
5286
|
+
* The seats are held. Send the buyer wherever this picker's `checkout` option
|
|
5287
|
+
* says they go.
|
|
5288
|
+
*
|
|
5289
|
+
* The default branch is the literal call that stood here before hosted
|
|
5290
|
+
* checkout existed, unchanged, so nothing about an existing integration moves.
|
|
5291
|
+
*/
|
|
5292
|
+
checkoutHandoff(hold, seats) {
|
|
5293
|
+
if (this.checkoutMode === "hosted") {
|
|
5294
|
+
void this.startHostedCheckout(hold, seats);
|
|
5295
|
+
return;
|
|
5296
|
+
}
|
|
5297
|
+
this.opts.onCheckout?.(hold, seats, this.buildHandoff(hold));
|
|
5298
|
+
}
|
|
5299
|
+
/**
|
|
5300
|
+
* Take the money ourselves, through the organizer's own gateway.
|
|
5301
|
+
*
|
|
5302
|
+
* Order of operations matters: ASK FIRST, load second. `payment-options` is
|
|
5303
|
+
* already in flight from render, and its answer decides whether any payment
|
|
5304
|
+
* code is fetched at all — an event that cannot charge never downloads the
|
|
5305
|
+
* card that would have charged it.
|
|
5306
|
+
*
|
|
5307
|
+
* An empty list is not a failure and never dead-ends the buyer. It routes them
|
|
5308
|
+
* to whatever the host has: `onCheckoutUnavailable` (with the server's reason,
|
|
5309
|
+
* so the host can say the right one of three very different sentences), then
|
|
5310
|
+
* `onCheckout` with the ordinary handoff. A host that supplied neither gets
|
|
5311
|
+
* the widget's own honest card instead of a press that did nothing.
|
|
5312
|
+
*/
|
|
5313
|
+
async startHostedCheckout(hold, seats) {
|
|
5314
|
+
const handoff = this.buildHandoff(hold);
|
|
5315
|
+
let options = null;
|
|
5316
|
+
try {
|
|
5317
|
+
options = await (this.paymentOptions ??= this.pubApi.paymentOptions(this.opts.event));
|
|
5318
|
+
} catch (err) {
|
|
5319
|
+
this.opts.onError?.(err);
|
|
5320
|
+
}
|
|
5321
|
+
if (this.destroyed) return;
|
|
5322
|
+
const provider = options?.providers?.[0];
|
|
5323
|
+
if (!provider) {
|
|
5324
|
+
const reason = paymentsOffReason(options?.reason);
|
|
5325
|
+
const handled = !!this.opts.onCheckoutUnavailable || !!this.opts.onCheckout;
|
|
5326
|
+
this.opts.onCheckoutUnavailable?.({ reason, handoff });
|
|
5327
|
+
this.opts.onCheckout?.(hold, seats, handoff);
|
|
5328
|
+
if (!handled) {
|
|
5329
|
+
void this.openCheckoutPanel({
|
|
5330
|
+
kind: "unavailable",
|
|
5331
|
+
reason,
|
|
5332
|
+
seatCount: handoff.lineItems.reduce((sum, item) => sum + item.quantity, 0)
|
|
5333
|
+
});
|
|
5334
|
+
}
|
|
5335
|
+
return;
|
|
5336
|
+
}
|
|
5337
|
+
await this.openCheckoutPanel({
|
|
5338
|
+
kind: "pay",
|
|
5339
|
+
provider,
|
|
5340
|
+
order: {
|
|
5341
|
+
holdId: handoff.holdId,
|
|
5342
|
+
expiresAt: handoff.expiresAt,
|
|
5343
|
+
currency: handoff.currency,
|
|
5344
|
+
total: handoff.total,
|
|
5345
|
+
labels: handoff.lineItems.map((item) => item.displayLabel ?? item.label)
|
|
5346
|
+
}
|
|
5347
|
+
});
|
|
5348
|
+
}
|
|
5349
|
+
/**
|
|
5350
|
+
* Fetch the checkout chunk and put its card over the map.
|
|
5351
|
+
*
|
|
5352
|
+
* Every failure here lands the buyer back on a map with their seats still
|
|
5353
|
+
* held, which is a place they can act from — a blocked chunk request must not
|
|
5354
|
+
* leave them staring at a CTA that no longer does anything.
|
|
5355
|
+
*/
|
|
5356
|
+
async openCheckoutPanel(state) {
|
|
5357
|
+
let mountCheckout;
|
|
5358
|
+
try {
|
|
5359
|
+
({ mountCheckout } = await loadHostedCheckout());
|
|
5360
|
+
} catch (err) {
|
|
5361
|
+
this.opts.onError?.(err);
|
|
5362
|
+
this.toast("Checkout could not be opened. Your seats are still held \u2014 please try again.", "error");
|
|
5363
|
+
this.setCtaPhase("idle");
|
|
5364
|
+
return;
|
|
5365
|
+
}
|
|
5366
|
+
if (this.destroyed || !this.root) return;
|
|
5367
|
+
this.closeCheckoutPanel();
|
|
5368
|
+
this.checkoutPanel = mountCheckout({
|
|
5369
|
+
root: this.root,
|
|
5370
|
+
state,
|
|
5371
|
+
startSession: (input) => this.pubApi.startCheckout(this.opts.event, input),
|
|
5372
|
+
orderStatus: (orderId) => this.pubApi.orderStatus(orderId),
|
|
5373
|
+
onCancel: () => {
|
|
5374
|
+
this.checkoutPanel = null;
|
|
5375
|
+
if (!this.hold) this.setCtaPhase("idle");
|
|
5376
|
+
},
|
|
5377
|
+
onConfirmed: (order) => {
|
|
5378
|
+
this.opts.onOrderConfirmed?.(order);
|
|
5379
|
+
void this.controller.refresh();
|
|
5380
|
+
},
|
|
5381
|
+
onError: (err) => this.opts.onError?.(err)
|
|
5382
|
+
});
|
|
5383
|
+
}
|
|
5384
|
+
closeCheckoutPanel() {
|
|
5385
|
+
this.checkoutPanel?.destroy();
|
|
5386
|
+
this.checkoutPanel = null;
|
|
5387
|
+
}
|
|
5167
5388
|
/** Assemble the stable {@link CheckoutHandoff} from a hold's server line items. */
|
|
5168
5389
|
buildHandoff(hold) {
|
|
5169
5390
|
const items = hold.items ?? [];
|
|
@@ -5398,7 +5619,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
5398
5619
|
}
|
|
5399
5620
|
/** Build the view-from-seat panorama the cinematic dissolves into — reuses the
|
|
5400
5621
|
* exact input path as the 2D `openSeatView` (organizer photo, else generated). */
|
|
5401
|
-
seatViewFor3d(seatId) {
|
|
5622
|
+
async seatViewFor3d(seatId) {
|
|
5402
5623
|
const seat = this.allSeats().find((s) => s.id === seatId);
|
|
5403
5624
|
if (!seat) return null;
|
|
5404
5625
|
if (seat.viewUrl) return { url: seat.viewUrl };
|
|
@@ -5407,6 +5628,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
5407
5628
|
const activeId = this.controller.getActiveFloorId();
|
|
5408
5629
|
const focal = seat.focalPoint ?? doc.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc.focalPoint ?? { x: 0, y: 0 };
|
|
5409
5630
|
try {
|
|
5631
|
+
const { generateSeatPanorama } = await loadPanorama();
|
|
5410
5632
|
return { url: generateSeatPanorama(seat, focal, this.allSeats()).url };
|
|
5411
5633
|
} catch {
|
|
5412
5634
|
return null;
|
|
@@ -5546,9 +5768,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
5546
5768
|
// in orbit instead of dissolving into an empty overlay.
|
|
5547
5769
|
getSeatView: (id) => new Promise((resolve, reject) => {
|
|
5548
5770
|
const run = () => {
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5771
|
+
void this.seatViewFor3d(id).then((view) => {
|
|
5772
|
+
if (view) resolve(view);
|
|
5773
|
+
else reject(new Error("seat_view_unavailable"));
|
|
5774
|
+
});
|
|
5552
5775
|
};
|
|
5553
5776
|
const ric = globalThis.requestIdleCallback;
|
|
5554
5777
|
if (typeof ric === "function") ric(run, { timeout: 1500 });
|
|
@@ -5826,6 +6049,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
5826
6049
|
this.closeConfirm();
|
|
5827
6050
|
this.dismissTableDialog(false);
|
|
5828
6051
|
this.closeSeatView();
|
|
6052
|
+
this.closeCheckoutPanel();
|
|
5829
6053
|
this.exit3d();
|
|
5830
6054
|
this.stopHoldTimer();
|
|
5831
6055
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
@@ -6547,7 +6771,14 @@ var ManageApi = class {
|
|
|
6547
6771
|
var POLL_MS = 1e4;
|
|
6548
6772
|
var MAX_FLAGS = 8;
|
|
6549
6773
|
var SEAT_LIST_PAGE = 300;
|
|
6550
|
-
var
|
|
6774
|
+
var PREVIEW_ELIGIBLE_FILL = "#6e7bff";
|
|
6775
|
+
var PREVIEW_ELIGIBLE_STROKE = "#b9c0ff";
|
|
6776
|
+
var PREVIEW_UNAVAILABLE_FILL = "#303846";
|
|
6777
|
+
var PREVIEW_UNAVAILABLE_STROKE = "#4b5669";
|
|
6778
|
+
var ALLOCATION_STROKE = "#101723";
|
|
6779
|
+
var CHANNELS_CSS = (
|
|
6780
|
+
/* @sl-css */
|
|
6781
|
+
`
|
|
6551
6782
|
.slm{--slm-mo-instant:80ms;--slm-mo-quick:140ms;--slm-mo-base:200ms;--slm-mo-slow:320ms;--slm-mo-ambient:2000ms;
|
|
6552
6783
|
--slm-mo-out:cubic-bezier(.2,.8,.2,1);--slm-mo-in-out:cubic-bezier(.4,0,.2,1);--slm-mo-exit:cubic-bezier(.4,0,1,1);
|
|
6553
6784
|
--slm-mo-spring:cubic-bezier(.34,1.3,.64,1)}
|
|
@@ -6560,6 +6791,8 @@ var CHANNELS_CSS = `
|
|
|
6560
6791
|
background:rgba(14,16,23,.88);border:1px solid var(--slm-line);font-size:10px;font-weight:800;letter-spacing:.04em;
|
|
6561
6792
|
transform:translate(-50%,-50%);white-space:nowrap}
|
|
6562
6793
|
.slm-ch-flag .mk{width:14px;height:14px;border-radius:4px;display:grid;place-items:center;font-size:8.5px;font-weight:800;color:#0e1017}
|
|
6794
|
+
.slm-ch-section-target{position:absolute;pointer-events:auto;padding:0;border:0;border-radius:8px;background:transparent;cursor:zoom-in}
|
|
6795
|
+
.slm-ch-section-target:focus-visible{outline:2px solid var(--slm-accent);outline-offset:-3px;background:color-mix(in srgb,var(--slm-accent) 12%,transparent)}
|
|
6563
6796
|
|
|
6564
6797
|
/* preview banner \u2014 raised with the organizer chrome dim, as one transition */
|
|
6565
6798
|
.slm-ch-banner{position:absolute;left:50%;top:14px;z-index:6;display:flex;align-items:center;gap:9px;padding:8px 14px;
|
|
@@ -6579,7 +6812,7 @@ var CHANNELS_CSS = `
|
|
|
6579
6812
|
transition:transform var(--slm-mo-slow) var(--slm-mo-out),opacity var(--slm-mo-base) var(--slm-mo-out)}
|
|
6580
6813
|
.slm-ch-staged.on{transform:none;opacity:1}
|
|
6581
6814
|
.slm-ch-staged.done{background:rgba(31,122,77,.96);border-color:#1f7a4d}
|
|
6582
|
-
.slm-ch-staged.shake{animation:slm-ch-shake
|
|
6815
|
+
.slm-ch-staged.shake{animation:slm-ch-shake var(--slm-mo-slow) var(--slm-mo-in-out) 2}
|
|
6583
6816
|
.slm-ch-staged b{font-variant-numeric:tabular-nums}
|
|
6584
6817
|
.slm-ch-staged .grow{flex:1}
|
|
6585
6818
|
.slm-ch-staged .go{padding:9px 16px;min-height:44px;display:inline-flex;align-items:center;border-radius:9px;
|
|
@@ -6596,6 +6829,11 @@ var CHANNELS_CSS = `
|
|
|
6596
6829
|
.slm-ch-viewseg button{flex:1;padding:6px 8px;min-height:34px;border-radius:7px;font-size:11px;font-weight:800;color:var(--slm-muted)}
|
|
6597
6830
|
.slm-ch-viewseg button.on{background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
6598
6831
|
.slm-ch-viewseg button:disabled{opacity:.5;cursor:not-allowed}
|
|
6832
|
+
.slm-ch-mapnav{margin:-2px 0 12px;padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
|
|
6833
|
+
.slm-ch-mapnav-head{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--slm-muted)}
|
|
6834
|
+
.slm-ch-mapnav-head button{color:var(--slm-accent);font-size:11px;font-weight:800;letter-spacing:0;text-transform:none;min-height:30px}
|
|
6835
|
+
.slm-ch-mapnav .slm-ch-viewseg{margin:8px 0 5px}
|
|
6836
|
+
.slm-ch-mapnav p{margin:0;font-size:11px;line-height:1.45;color:var(--slm-muted)}
|
|
6599
6837
|
.slm-ch-list{display:flex;flex-direction:column;gap:8px;margin-bottom:12px}
|
|
6600
6838
|
.slm-ch-row{padding:10px 11px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
6601
6839
|
text-align:left;width:100%;display:block;transition:border-color var(--slm-mo-quick) var(--slm-mo-out)}
|
|
@@ -6616,7 +6854,7 @@ var CHANNELS_CSS = `
|
|
|
6616
6854
|
font-variant-numeric:tabular-nums}
|
|
6617
6855
|
.slm-ch-counts b{color:var(--slm-text);font-weight:800}
|
|
6618
6856
|
.slm-ch-counts .free b{color:#5bd39b}
|
|
6619
|
-
.slm-ch-counts b.bump{animation:slm-ch-bump
|
|
6857
|
+
.slm-ch-counts b.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
6620
6858
|
@keyframes slm-ch-bump{0%,100%{transform:none}35%{transform:translateY(-2px) scale(1.08)}}
|
|
6621
6859
|
.slm-ch-access{margin-top:6px;font-size:10.5px;color:var(--slm-muted)}
|
|
6622
6860
|
.slm-ch-more{flex:none;color:var(--slm-muted);font-weight:800;padding:0 4px;min-height:28px}
|
|
@@ -6626,12 +6864,13 @@ var CHANNELS_CSS = `
|
|
|
6626
6864
|
font-weight:800;color:#0e1017}
|
|
6627
6865
|
.slm-ch-selsrc-row b{min-width:30px;text-align:right;font-weight:800}
|
|
6628
6866
|
.slm-ch-selsrc-row span{color:var(--slm-muted)}
|
|
6629
|
-
.slm-ch-selnum.bump{animation:slm-ch-bump
|
|
6867
|
+
.slm-ch-selnum.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
6630
6868
|
.slm-ch-row2{display:flex;gap:8px;margin-top:8px}
|
|
6631
6869
|
.slm-ch-row2 .slm-btn{flex:1;min-width:0}
|
|
6632
6870
|
.slm-ch-alert{display:flex;align-items:flex-start;gap:9px;padding:11px 13px;border-radius:10px;font-size:12.5px;
|
|
6633
6871
|
line-height:1.5;margin-bottom:12px}
|
|
6634
6872
|
.slm-ch-alert.warn{background:rgba(244,183,64,.1);border:1px solid rgba(244,183,64,.4);color:#f4d58a}
|
|
6873
|
+
.slm-ch-alert.info{background:rgba(110,123,255,.12);border:1px solid rgba(110,123,255,.44);color:#c5cbff}
|
|
6635
6874
|
.slm-ch-alert.err{background:rgba(229,72,77,.1);border:1px solid rgba(229,72,77,.45);color:#f1a4a6}
|
|
6636
6875
|
.slm-ch-alert b{color:#fff}
|
|
6637
6876
|
.slm-ch-alert button{display:block;margin-top:6px;color:#fff;font-weight:800;min-height:36px}
|
|
@@ -6728,7 +6967,8 @@ var CHANNELS_CSS = `
|
|
|
6728
6967
|
.slm-ch-staged.shake{outline:2px solid #e5484d;outline-offset:2px}
|
|
6729
6968
|
.slm-ch-staged.done{outline:2px solid #5bd39b;outline-offset:2px}
|
|
6730
6969
|
}
|
|
6731
|
-
|
|
6970
|
+
`
|
|
6971
|
+
);
|
|
6732
6972
|
function bucketRowsHtml(rows) {
|
|
6733
6973
|
return rows.map((row, index) => `
|
|
6734
6974
|
<div class="slm-ch-bucket" style="animation-delay:${Math.min(index, 4) * 30}ms">
|
|
@@ -6778,6 +7018,10 @@ var ChannelsMode = class {
|
|
|
6778
7018
|
this.loadError = null;
|
|
6779
7019
|
this.loading = true;
|
|
6780
7020
|
this.view = "inspect";
|
|
7021
|
+
/** Pan is intentionally the initial desktop interaction. Assignment's
|
|
7022
|
+
* marquee is powerful, but must never make an organizer lose map navigation. */
|
|
7023
|
+
this.mapIntent = "pan";
|
|
7024
|
+
this.focusedSectionId = null;
|
|
6781
7025
|
this.showArchived = false;
|
|
6782
7026
|
this.detailChannelId = null;
|
|
6783
7027
|
this.targetChannelId = "";
|
|
@@ -6794,6 +7038,15 @@ var ChannelsMode = class {
|
|
|
6794
7038
|
this.links = [];
|
|
6795
7039
|
this.linksChannelId = null;
|
|
6796
7040
|
this.linksState = "idle";
|
|
7041
|
+
/**
|
|
7042
|
+
* Monotonic read generations — one for the channel list + allocation, one for
|
|
7043
|
+
* the open channel's links. Reads are concurrent (a 10s poll versus a
|
|
7044
|
+
* mutation's own reload), and the network does not promise to answer them in
|
|
7045
|
+
* order. Only the NEWEST read of each kind may write to state; an older
|
|
7046
|
+
* answer that arrives late is dropped, never painted.
|
|
7047
|
+
*/
|
|
7048
|
+
this.listSeq = 0;
|
|
7049
|
+
this.linksSeq = 0;
|
|
6797
7050
|
this.previewAudience = [];
|
|
6798
7051
|
this.previewIncludePublic = false;
|
|
6799
7052
|
this.previewProjection = null;
|
|
@@ -6820,10 +7073,14 @@ var ChannelsMode = class {
|
|
|
6820
7073
|
enter() {
|
|
6821
7074
|
if (this.active) return;
|
|
6822
7075
|
this.active = true;
|
|
7076
|
+
this.mapIntent = "pan";
|
|
7077
|
+
this.focusedSectionId = null;
|
|
6823
7078
|
this.ensureLayer();
|
|
6824
7079
|
this.host.root.classList.add("ch-mode");
|
|
6825
7080
|
this.applySheetClasses();
|
|
7081
|
+
if (this.host.sections().length > 1) this.host.showSectionOverview();
|
|
6826
7082
|
this.paintRail();
|
|
7083
|
+
this.onInteractionChange?.();
|
|
6827
7084
|
void this.refresh();
|
|
6828
7085
|
this.pollTimer = setInterval(() => {
|
|
6829
7086
|
void this.refresh({ quiet: true });
|
|
@@ -6875,6 +7132,17 @@ var ChannelsMode = class {
|
|
|
6875
7132
|
canSelect() {
|
|
6876
7133
|
return this.caps.manage && this.view === "inspect";
|
|
6877
7134
|
}
|
|
7135
|
+
/** Bulk seat assignment is explicit. In Pan map, clicks can still inspect a
|
|
7136
|
+
* single seat, while a primary-button drag always moves the camera. */
|
|
7137
|
+
usesMarqueeSelection() {
|
|
7138
|
+
return this.canSelect() && this.mapIntent === "assign";
|
|
7139
|
+
}
|
|
7140
|
+
/** The renderer calls this when the organizer opens a section from overview. */
|
|
7141
|
+
handleSectionFocus(sectionId) {
|
|
7142
|
+
if (!this.active) return;
|
|
7143
|
+
this.focusedSectionId = sectionId;
|
|
7144
|
+
this.paintRail();
|
|
7145
|
+
}
|
|
6878
7146
|
/**
|
|
6879
7147
|
* Organizer realtime integration point. M5 ships a per-scope socket for
|
|
6880
7148
|
* buyers; the organizer channel-count stream is a later milestone. When it
|
|
@@ -6902,22 +7170,28 @@ var ChannelsMode = class {
|
|
|
6902
7170
|
// ---- data -----------------------------------------------------------------
|
|
6903
7171
|
async refresh(opts = {}) {
|
|
6904
7172
|
if (!this.caps.view) return;
|
|
7173
|
+
const seq = ++this.listSeq;
|
|
7174
|
+
const superseded = () => seq !== this.listSeq;
|
|
6905
7175
|
try {
|
|
6906
7176
|
const list = await this.host.api.channels(this.host.eventKey, { includeArchived: this.showArchived });
|
|
7177
|
+
if (superseded()) return;
|
|
6907
7178
|
this.list = list;
|
|
6908
7179
|
this.assignmentVersion = list.assignmentVersion;
|
|
6909
7180
|
this.loadError = null;
|
|
6910
7181
|
if (!this.targetChannelId) {
|
|
6911
7182
|
this.targetChannelId = list.channels.find((c) => c.state === "active")?.id ?? PUBLIC_CHANNEL_ID;
|
|
6912
7183
|
}
|
|
6913
|
-
await this.loadAllocation();
|
|
7184
|
+
await this.loadAllocation(seq);
|
|
7185
|
+
if (superseded()) return;
|
|
6914
7186
|
if (this.detailChannelId) await this.loadLinks(this.detailChannelId);
|
|
7187
|
+
if (superseded()) return;
|
|
6915
7188
|
this.loading = false;
|
|
6916
7189
|
if (this.active) {
|
|
6917
7190
|
this.paintRail();
|
|
6918
7191
|
this.paintOverlay();
|
|
6919
7192
|
}
|
|
6920
7193
|
} catch (err) {
|
|
7194
|
+
if (superseded()) return;
|
|
6921
7195
|
this.loading = false;
|
|
6922
7196
|
if (err instanceof ManageApiError && err.status === 403) {
|
|
6923
7197
|
this.caps = { view: false, manage: false };
|
|
@@ -6929,10 +7203,11 @@ var ChannelsMode = class {
|
|
|
6929
7203
|
}
|
|
6930
7204
|
/** Walk every allocation page. Bounded by the event's seat count, and the
|
|
6931
7205
|
* server caps each page, so an arena is a handful of round trips. */
|
|
6932
|
-
async loadAllocation() {
|
|
7206
|
+
async loadAllocation(seq) {
|
|
6933
7207
|
const next = /* @__PURE__ */ new Map();
|
|
6934
7208
|
let afterLabel;
|
|
6935
7209
|
for (let page = 0; page < 200; page += 1) {
|
|
7210
|
+
if (seq !== void 0 && seq !== this.listSeq) return;
|
|
6936
7211
|
const res = await this.host.api.channelAllocation(this.host.eventKey, {
|
|
6937
7212
|
afterLabel,
|
|
6938
7213
|
limit: 1e3
|
|
@@ -6944,6 +7219,7 @@ var ChannelsMode = class {
|
|
|
6944
7219
|
if (!res.nextAfterLabel) break;
|
|
6945
7220
|
afterLabel = res.nextAfterLabel;
|
|
6946
7221
|
}
|
|
7222
|
+
if (seq !== void 0 && seq !== this.listSeq) return;
|
|
6947
7223
|
this.allocation = next;
|
|
6948
7224
|
}
|
|
6949
7225
|
// ---- lookups --------------------------------------------------------------
|
|
@@ -7010,9 +7286,10 @@ var ChannelsMode = class {
|
|
|
7010
7286
|
* Repaint the allocation (or preview) overlay in ONE canvas pass.
|
|
7011
7287
|
*
|
|
7012
7288
|
* Channel identity on the map is a fill in the administrative color PLUS the
|
|
7013
|
-
* letter flags below — never color alone.
|
|
7014
|
-
*
|
|
7015
|
-
*
|
|
7289
|
+
* letter flags below — never color alone. In buyer preview the map instead
|
|
7290
|
+
* uses two explicit, channel-neutral access states. Physical status keeps its
|
|
7291
|
+
* own cue: only FREE units are repainted, so sold/held/blocked seats still
|
|
7292
|
+
* read exactly as they do in every other tool.
|
|
7016
7293
|
*/
|
|
7017
7294
|
paintOverlay() {
|
|
7018
7295
|
const canvas = this.canvas;
|
|
@@ -7037,11 +7314,14 @@ var ChannelsMode = class {
|
|
|
7037
7314
|
if (!ctx) return;
|
|
7038
7315
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
7039
7316
|
ctx.clearRect(0, 0, width, height);
|
|
7317
|
+
const seatDetail = this.host.isSeatDetail();
|
|
7040
7318
|
const size = Math.max(3, this.host.seatPixelSize());
|
|
7041
7319
|
const half = size / 2;
|
|
7042
7320
|
const projection = this.view === "preview" ? this.previewProjection : null;
|
|
7043
7321
|
const eligible = projection ? new Set(projection.available === false ? [] : projection.eligible ?? []) : null;
|
|
7044
7322
|
const clusters = /* @__PURE__ */ new Map();
|
|
7323
|
+
const sectionTargets = !seatDetail && this.host.sections().length > 1 ? /* @__PURE__ */ new Map() : null;
|
|
7324
|
+
const previewSections = this.view === "preview" && seatDetail && size <= 15 ? /* @__PURE__ */ new Map() : null;
|
|
7045
7325
|
for (const seat of this.host.seats()) {
|
|
7046
7326
|
const status = this.host.statusOf(seat.label) ?? "free";
|
|
7047
7327
|
const channelId = this.allocation.get(seat.label) ?? PUBLIC_CHANNEL_ID;
|
|
@@ -7052,24 +7332,150 @@ var ChannelsMode = class {
|
|
|
7052
7332
|
cluster.n += 1;
|
|
7053
7333
|
clusters.set(channelId, cluster);
|
|
7054
7334
|
}
|
|
7335
|
+
if (!seatDetail) {
|
|
7336
|
+
const section = this.host.sectionOfLabel(seat.label);
|
|
7337
|
+
const point2 = section ? this.host.worldToScreen({ x: seat.x, y: seat.y }) : null;
|
|
7338
|
+
if (sectionTargets && section && point2) {
|
|
7339
|
+
const bounds = sectionTargets.get(section.id) ?? {
|
|
7340
|
+
label: section.label,
|
|
7341
|
+
minX: point2.x,
|
|
7342
|
+
minY: point2.y,
|
|
7343
|
+
maxX: point2.x,
|
|
7344
|
+
maxY: point2.y
|
|
7345
|
+
};
|
|
7346
|
+
bounds.minX = Math.min(bounds.minX, point2.x);
|
|
7347
|
+
bounds.minY = Math.min(bounds.minY, point2.y);
|
|
7348
|
+
bounds.maxX = Math.max(bounds.maxX, point2.x);
|
|
7349
|
+
bounds.maxY = Math.max(bounds.maxY, point2.y);
|
|
7350
|
+
sectionTargets.set(section.id, bounds);
|
|
7351
|
+
}
|
|
7352
|
+
continue;
|
|
7353
|
+
}
|
|
7055
7354
|
if (status !== "free") continue;
|
|
7056
7355
|
let fill = null;
|
|
7356
|
+
let stroke = null;
|
|
7057
7357
|
if (this.view === "preview") {
|
|
7058
|
-
|
|
7358
|
+
if (eligible?.has(seat.label)) {
|
|
7359
|
+
fill = PREVIEW_ELIGIBLE_FILL;
|
|
7360
|
+
stroke = PREVIEW_ELIGIBLE_STROKE;
|
|
7361
|
+
} else {
|
|
7362
|
+
fill = PREVIEW_UNAVAILABLE_FILL;
|
|
7363
|
+
stroke = PREVIEW_UNAVAILABLE_STROKE;
|
|
7364
|
+
}
|
|
7059
7365
|
} else if (channelId !== PUBLIC_CHANNEL_ID) {
|
|
7060
7366
|
fill = this.markerFor(channelId).color;
|
|
7367
|
+
stroke = ALLOCATION_STROKE;
|
|
7061
7368
|
}
|
|
7062
7369
|
if (!fill) continue;
|
|
7063
7370
|
const point = this.host.worldToScreen({ x: seat.x, y: seat.y });
|
|
7064
7371
|
if (!point) continue;
|
|
7065
7372
|
if (point.x < -size || point.y < -size || point.x > width + size || point.y > height + size) continue;
|
|
7373
|
+
if (previewSections) {
|
|
7374
|
+
const section = this.host.sectionOfLabel(seat.label);
|
|
7375
|
+
if (section) {
|
|
7376
|
+
const bounds = previewSections.get(section.id) ?? {
|
|
7377
|
+
label: section.label,
|
|
7378
|
+
minX: point.x,
|
|
7379
|
+
minY: point.y,
|
|
7380
|
+
maxX: point.x,
|
|
7381
|
+
maxY: point.y
|
|
7382
|
+
};
|
|
7383
|
+
bounds.minX = Math.min(bounds.minX, point.x);
|
|
7384
|
+
bounds.minY = Math.min(bounds.minY, point.y);
|
|
7385
|
+
bounds.maxX = Math.max(bounds.maxX, point.x);
|
|
7386
|
+
bounds.maxY = Math.max(bounds.maxY, point.y);
|
|
7387
|
+
previewSections.set(section.id, bounds);
|
|
7388
|
+
}
|
|
7389
|
+
}
|
|
7066
7390
|
ctx.fillStyle = fill;
|
|
7067
|
-
|
|
7068
|
-
|
|
7391
|
+
if (this.view === "preview" || channelId !== PUBLIC_CHANNEL_ID) {
|
|
7392
|
+
const radius = Math.max(2, half + Math.min(1.5, half * 0.06));
|
|
7393
|
+
ctx.globalAlpha = 1;
|
|
7394
|
+
ctx.beginPath();
|
|
7395
|
+
ctx.arc(point.x, point.y, radius, 0, Math.PI * 2);
|
|
7396
|
+
ctx.fill();
|
|
7397
|
+
ctx.strokeStyle = stroke ?? fill;
|
|
7398
|
+
ctx.lineWidth = this.view === "preview" ? Math.max(1, Math.min(1.75, size * 0.13)) : Math.max(1, Math.min(1.5, size * 0.1));
|
|
7399
|
+
ctx.stroke();
|
|
7400
|
+
if (this.view === "preview" && eligible?.has(seat.label) && size >= 22) {
|
|
7401
|
+
this.paintPreviewSeatLabel(ctx, seat.label, point.x, point.y, radius);
|
|
7402
|
+
}
|
|
7403
|
+
} else {
|
|
7404
|
+
ctx.globalAlpha = 0.85;
|
|
7405
|
+
ctx.fillRect(point.x - half, point.y - half, size, size);
|
|
7406
|
+
}
|
|
7069
7407
|
}
|
|
7070
7408
|
ctx.globalAlpha = 1;
|
|
7409
|
+
if (previewSections) this.paintPreviewSectionLabels(ctx, previewSections);
|
|
7410
|
+
this.paintSectionTargets(sectionTargets);
|
|
7071
7411
|
this.paintFlags(clusters);
|
|
7072
7412
|
}
|
|
7413
|
+
/** Draw an eligible seat's actual chart label without inventing a new buyer
|
|
7414
|
+
* identifier. Long labels scale down and are omitted rather than overflowing
|
|
7415
|
+
* into an adjacent seat. */
|
|
7416
|
+
paintPreviewSeatLabel(ctx, label, x, y, radius) {
|
|
7417
|
+
const maxWidth = radius * 1.55;
|
|
7418
|
+
let fontSize = Math.min(13, Math.max(7, radius * 0.55));
|
|
7419
|
+
const minFontSize = 6;
|
|
7420
|
+
while (fontSize >= minFontSize) {
|
|
7421
|
+
ctx.font = `800 ${fontSize}px var(--slm-font, system-ui, sans-serif)`;
|
|
7422
|
+
if (ctx.measureText(label).width <= maxWidth) break;
|
|
7423
|
+
fontSize -= 0.5;
|
|
7424
|
+
}
|
|
7425
|
+
if (fontSize < minFontSize) return;
|
|
7426
|
+
ctx.fillStyle = "#ffffff";
|
|
7427
|
+
ctx.textAlign = "center";
|
|
7428
|
+
ctx.textBaseline = "middle";
|
|
7429
|
+
ctx.fillText(label, x, y);
|
|
7430
|
+
}
|
|
7431
|
+
/** A section overview is a navigation map. These transparent, keyboardable
|
|
7432
|
+
* hit areas sit over the renderer's section shells so both mouse and keyboard
|
|
7433
|
+
* always take the organizer into the real focused-section camera state. */
|
|
7434
|
+
paintSectionTargets(sections) {
|
|
7435
|
+
const layer = this.layer;
|
|
7436
|
+
if (!layer) return;
|
|
7437
|
+
layer.querySelectorAll(".slm-ch-section-target").forEach((el) => el.remove());
|
|
7438
|
+
if (!sections) return;
|
|
7439
|
+
for (const [id, section] of sections) {
|
|
7440
|
+
const width = section.maxX - section.minX;
|
|
7441
|
+
const height = section.maxY - section.minY;
|
|
7442
|
+
if (width < 20 || height < 20) continue;
|
|
7443
|
+
const target = document.createElement("button");
|
|
7444
|
+
target.type = "button";
|
|
7445
|
+
target.className = "slm-ch-section-target";
|
|
7446
|
+
target.style.left = `${section.minX - 8}px`;
|
|
7447
|
+
target.style.top = `${section.minY - 8}px`;
|
|
7448
|
+
target.style.width = `${width + 16}px`;
|
|
7449
|
+
target.style.height = `${height + 16}px`;
|
|
7450
|
+
target.setAttribute("aria-label", `Open ${section.label} seats`);
|
|
7451
|
+
target.addEventListener("click", () => {
|
|
7452
|
+
this.focusedSectionId = id;
|
|
7453
|
+
this.host.focusSection(id);
|
|
7454
|
+
this.paintRail();
|
|
7455
|
+
});
|
|
7456
|
+
layer.appendChild(target);
|
|
7457
|
+
}
|
|
7458
|
+
}
|
|
7459
|
+
/** Keep renderer section names legible over a dense, zoomed-out preview. */
|
|
7460
|
+
paintPreviewSectionLabels(ctx, sections) {
|
|
7461
|
+
for (const section of sections.values()) {
|
|
7462
|
+
const width = section.maxX - section.minX;
|
|
7463
|
+
const height = section.maxY - section.minY;
|
|
7464
|
+
if (width < 52 || height < 26) continue;
|
|
7465
|
+
const centerX = (section.minX + section.maxX) / 2;
|
|
7466
|
+
const centerY = (section.minY + section.maxY) / 2;
|
|
7467
|
+
const fontSize = Math.max(11, Math.min(15, height * 0.16));
|
|
7468
|
+
ctx.font = `800 ${fontSize}px var(--slm-font, system-ui, sans-serif)`;
|
|
7469
|
+
const labelWidth = Math.min(width - 8, ctx.measureText(section.label).width + 18);
|
|
7470
|
+
const labelHeight = fontSize + 10;
|
|
7471
|
+
ctx.fillStyle = "rgba(11, 16, 28, .88)";
|
|
7472
|
+
ctx.fillRect(centerX - labelWidth / 2, centerY - labelHeight / 2, labelWidth, labelHeight);
|
|
7473
|
+
ctx.fillStyle = "#f8fafc";
|
|
7474
|
+
ctx.textAlign = "center";
|
|
7475
|
+
ctx.textBaseline = "middle";
|
|
7476
|
+
ctx.fillText(section.label, centerX, centerY);
|
|
7477
|
+
}
|
|
7478
|
+
}
|
|
7073
7479
|
/** Letter flags at each channel's centroid — the non-color identity cue. */
|
|
7074
7480
|
paintFlags(clusters) {
|
|
7075
7481
|
const layer = this.layer;
|
|
@@ -7135,7 +7541,7 @@ var ChannelsMode = class {
|
|
|
7135
7541
|
});
|
|
7136
7542
|
});
|
|
7137
7543
|
}
|
|
7138
|
-
setBanner(on, name = "") {
|
|
7544
|
+
setBanner(on, name = "", eligibleSeats) {
|
|
7139
7545
|
const banner = this.bannerEl;
|
|
7140
7546
|
if (!banner) return;
|
|
7141
7547
|
this.host.root.classList.toggle("ch-preview", on);
|
|
@@ -7145,8 +7551,9 @@ var ChannelsMode = class {
|
|
|
7145
7551
|
return;
|
|
7146
7552
|
}
|
|
7147
7553
|
const marker = this.previewAudience.length === 1 ? this.markerFor(this.previewAudience[0]) : { color: "var(--slm-accent)", letter: "" };
|
|
7554
|
+
const availability = eligibleSeats == null ? "" : ` \xB7 ${eligibleSeats.toLocaleString()} ${eligibleSeats === 1 ? "seat" : "seats"} available now`;
|
|
7148
7555
|
banner.innerHTML = `<span class="dot" style="background:${esc(marker.color)}"></span>
|
|
7149
|
-
Previewing buyer access \xB7 ${esc(name)} \xB7 read-only
|
|
7556
|
+
Previewing buyer access \xB7 ${esc(name)}${availability} \xB7 read-only
|
|
7150
7557
|
<button type="button" data-ch-act="exit-preview">Exit preview</button>`;
|
|
7151
7558
|
banner.classList.add("on");
|
|
7152
7559
|
banner.querySelector('[data-ch-act="exit-preview"]')?.addEventListener("click", () => this.setView("inspect"));
|
|
@@ -7181,6 +7588,8 @@ var ChannelsMode = class {
|
|
|
7181
7588
|
includePublic: this.previewIncludePublic || audience.some(isPublicChannelId)
|
|
7182
7589
|
});
|
|
7183
7590
|
this.previewSupported = true;
|
|
7591
|
+
const eligibleSeats = this.previewProjection.available === false ? void 0 : this.previewProjection.counts?.eligible ?? this.previewProjection.eligible?.length;
|
|
7592
|
+
this.setBanner(true, names, eligibleSeats);
|
|
7184
7593
|
} catch (err) {
|
|
7185
7594
|
const status = err instanceof ManageApiError ? err.status : 0;
|
|
7186
7595
|
this.previewSupported = !(status === 404 || status === 405 || status === 501);
|
|
@@ -7231,6 +7640,22 @@ var ChannelsMode = class {
|
|
|
7231
7640
|
aria-pressed="${this.view === "inspect"}">Inspect allocation</button>
|
|
7232
7641
|
<button type="button" class="${previewOn.trim()}" data-ch-view="preview"
|
|
7233
7642
|
aria-pressed="${this.view === "preview"}">Preview buyer access</button>
|
|
7643
|
+
</div>${this.mapNavigationHtml()}`;
|
|
7644
|
+
}
|
|
7645
|
+
mapNavigationHtml() {
|
|
7646
|
+
if (this.host.sections().length < 2) return "";
|
|
7647
|
+
const focused = this.focusedSectionId ? this.host.sections().find((section) => section.id === this.focusedSectionId)?.label ?? "section" : null;
|
|
7648
|
+
const panOn = this.mapIntent === "pan" ? " on" : "";
|
|
7649
|
+
const assignOn = this.mapIntent === "assign" ? " on" : "";
|
|
7650
|
+
const intent = this.view === "inspect" && this.caps.manage ? `<div class="slm-ch-viewseg" role="group" aria-label="Map interaction">
|
|
7651
|
+
<button type="button" class="${panOn.trim()}" data-ch-map="pan" aria-pressed="${this.mapIntent === "pan"}">Pan map</button>
|
|
7652
|
+
<button type="button" class="${assignOn.trim()}" data-ch-map="assign" aria-pressed="${this.mapIntent === "assign"}">Assign seats</button>
|
|
7653
|
+
</div>
|
|
7654
|
+
<p>${this.mapIntent === "pan" ? "Drag to explore. Click a section to open its seats." : "Drag across seats to select them for allocation."}</p>` : "<p>Drag to explore. Click a section to open its seats.</p>";
|
|
7655
|
+
return `<div class="slm-ch-mapnav">
|
|
7656
|
+
<div class="slm-ch-mapnav-head"><span>${focused ? `Viewing ${esc(focused)}` : "Section overview"}</span>
|
|
7657
|
+
<button type="button" data-ch-act="sections">All sections</button></div>
|
|
7658
|
+
${intent}
|
|
7234
7659
|
</div>`;
|
|
7235
7660
|
}
|
|
7236
7661
|
countsHtml(counts, key) {
|
|
@@ -7369,6 +7794,8 @@ var ChannelsMode = class {
|
|
|
7369
7794
|
*/
|
|
7370
7795
|
async loadLinks(channelId) {
|
|
7371
7796
|
if (!this.caps.view) return;
|
|
7797
|
+
const seq = ++this.linksSeq;
|
|
7798
|
+
const superseded = () => seq !== this.linksSeq || this.linksChannelId !== channelId;
|
|
7372
7799
|
if (this.linksChannelId !== channelId) {
|
|
7373
7800
|
this.links = [];
|
|
7374
7801
|
this.linksChannelId = channelId;
|
|
@@ -7376,11 +7803,11 @@ var ChannelsMode = class {
|
|
|
7376
7803
|
}
|
|
7377
7804
|
try {
|
|
7378
7805
|
const res = await this.host.api.accessLinks(this.host.eventKey, channelId);
|
|
7379
|
-
if (
|
|
7806
|
+
if (superseded()) return;
|
|
7380
7807
|
this.links = res.links ?? [];
|
|
7381
7808
|
this.linksState = "ready";
|
|
7382
7809
|
} catch (err) {
|
|
7383
|
-
if (
|
|
7810
|
+
if (superseded()) return;
|
|
7384
7811
|
const status = err instanceof ManageApiError ? err.status : 0;
|
|
7385
7812
|
this.links = [];
|
|
7386
7813
|
this.linksState = status === 404 || status === 405 || status === 501 ? "unsupported" : "error";
|
|
@@ -7457,9 +7884,9 @@ var ChannelsMode = class {
|
|
|
7457
7884
|
const unavailable = this.previewProjection?.available === false ? `<div class="slm-ch-alert warn" role="status"><span>\u23F8</span>
|
|
7458
7885
|
<span><b>This private sale is not available.</b> ${esc((this.previewProjection.unavailable ?? []).map((entry) => `${this.nameOf(entry.channelId) ?? "This channel"} is ${entry.state}`).join("; ") || "The audience cannot buy right now")}.
|
|
7459
7886
|
A buyer arriving with this access sees this message, not these seats.</span></div>` : "";
|
|
7460
|
-
const
|
|
7461
|
-
const summary =
|
|
7462
|
-
|
|
7887
|
+
const eligibleSeats = this.previewProjection?.counts?.eligible ?? this.previewProjection?.eligible?.length;
|
|
7888
|
+
const summary = eligibleSeats != null && this.previewProjection?.available !== false ? `<div class="slm-ch-alert info"><span>\u2713</span><span><b>${eligibleSeats.toLocaleString()} ${eligibleSeats === 1 ? "seat is" : "seats are"} available now.</b>
|
|
7889
|
+
This is the exact buyer-visible allocation.${this.previewProjection?.includePublic === false ? " Public sale seats are <b>not</b> included in this access." : ""}</span></div>` : "";
|
|
7463
7890
|
const includePublic = isPublicChannelId(current) ? "" : `
|
|
7464
7891
|
<label class="slm-note" style="display:flex;gap:8px;align-items:center;margin:10px 0">
|
|
7465
7892
|
<input type="checkbox" data-ch-includepublic ${this.previewIncludePublic ? "checked" : ""} />
|
|
@@ -7490,6 +7917,13 @@ var ChannelsMode = class {
|
|
|
7490
7917
|
rail.querySelectorAll("[data-ch-view]").forEach((button) => {
|
|
7491
7918
|
button.addEventListener("click", () => this.setView(button.dataset.chView));
|
|
7492
7919
|
});
|
|
7920
|
+
rail.querySelectorAll("[data-ch-map]").forEach((button) => {
|
|
7921
|
+
button.addEventListener("click", () => {
|
|
7922
|
+
this.mapIntent = button.dataset.chMap === "assign" ? "assign" : "pan";
|
|
7923
|
+
this.paintRail();
|
|
7924
|
+
this.onInteractionChange?.();
|
|
7925
|
+
});
|
|
7926
|
+
});
|
|
7493
7927
|
rail.querySelectorAll("[data-ch-detail]").forEach((button) => {
|
|
7494
7928
|
button.addEventListener("click", () => {
|
|
7495
7929
|
const channelId = button.dataset.chDetail;
|
|
@@ -7540,6 +7974,11 @@ var ChannelsMode = class {
|
|
|
7540
7974
|
}
|
|
7541
7975
|
railAction(action) {
|
|
7542
7976
|
switch (action) {
|
|
7977
|
+
case "sections":
|
|
7978
|
+
this.focusedSectionId = null;
|
|
7979
|
+
this.host.showSectionOverview();
|
|
7980
|
+
this.paintRail();
|
|
7981
|
+
break;
|
|
7543
7982
|
case "create":
|
|
7544
7983
|
this.openDialog({ kind: "create" });
|
|
7545
7984
|
break;
|
|
@@ -7714,7 +8153,7 @@ var ChannelsMode = class {
|
|
|
7714
8153
|
<label>Marker</label>
|
|
7715
8154
|
<div style="display:flex;gap:8px;align-items:center">
|
|
7716
8155
|
<span class="slm-ch-mk" data-ch-marker style="background:${esc(suggestion.color)};width:28px;height:28px;font-size:13px">${esc(suggestion.letter)}</span>
|
|
7717
|
-
<span class="slm-note" style="margin:0">Letter
|
|
8156
|
+
<span class="slm-note" style="margin:0">Letter comes from the name; colour is chosen automatically from the next available palette. Buyers never see either.</span>
|
|
7718
8157
|
</div>
|
|
7719
8158
|
</div>
|
|
7720
8159
|
<div class="slm-field">
|
|
@@ -8065,6 +8504,24 @@ var ChannelsMode = class {
|
|
|
8065
8504
|
await this.loadLinks(channelId);
|
|
8066
8505
|
this.paintRail();
|
|
8067
8506
|
}
|
|
8507
|
+
/**
|
|
8508
|
+
* The reload EVERY link mutation owes the panel.
|
|
8509
|
+
*
|
|
8510
|
+
* A create/rotate/revoke changes two things the detail panel renders: the
|
|
8511
|
+
* channel's access line (the server sets `access.intent` on create, and clears
|
|
8512
|
+
* it when the last live link goes) and the link status list. Both are re-read
|
|
8513
|
+
* here and the rail repainted, so the panel the organizer is already looking
|
|
8514
|
+
* at is current the moment the mutation lands — no reload, and no dependence
|
|
8515
|
+
* on HOW the one-time reveal was dismissed (the button, Escape, or never).
|
|
8516
|
+
*/
|
|
8517
|
+
async reloadAfterLinkChange(channelId) {
|
|
8518
|
+
if (this.detailChannelId === channelId) {
|
|
8519
|
+
await this.refresh({ quiet: true });
|
|
8520
|
+
return;
|
|
8521
|
+
}
|
|
8522
|
+
await this.loadLinks(channelId);
|
|
8523
|
+
if (this.active) this.paintRail();
|
|
8524
|
+
}
|
|
8068
8525
|
linkById(linkId) {
|
|
8069
8526
|
return this.links.find((link) => link.id === linkId) ?? null;
|
|
8070
8527
|
}
|
|
@@ -8158,7 +8615,7 @@ var ChannelsMode = class {
|
|
|
8158
8615
|
try {
|
|
8159
8616
|
const reveal = await this.host.api.createAccessLink(this.host.eventKey, channelId, input);
|
|
8160
8617
|
this.revealLink(reveal, { channelId });
|
|
8161
|
-
await this.
|
|
8618
|
+
await this.reloadAfterLinkChange(channelId);
|
|
8162
8619
|
} catch (err) {
|
|
8163
8620
|
this.showDialogError(accessLinkErrorCopy(err instanceof ManageApiError ? err : void 0));
|
|
8164
8621
|
if (!(err instanceof ManageApiError)) this.host.onError(err);
|
|
@@ -8216,9 +8673,6 @@ var ChannelsMode = class {
|
|
|
8216
8673
|
}
|
|
8217
8674
|
selectSecret(dialog);
|
|
8218
8675
|
});
|
|
8219
|
-
dialog.querySelector("[data-ch-lk-done]")?.addEventListener("click", () => {
|
|
8220
|
-
void this.loadLinks(opts.channelId).then(() => this.paintRail());
|
|
8221
|
-
});
|
|
8222
8676
|
});
|
|
8223
8677
|
this.announce("Your hosted access link is ready and is shown once.");
|
|
8224
8678
|
}
|
|
@@ -8284,6 +8738,7 @@ var ChannelsMode = class {
|
|
|
8284
8738
|
endActiveSessions
|
|
8285
8739
|
);
|
|
8286
8740
|
this.revealLink(reveal, { channelId, rotated: true });
|
|
8741
|
+
await this.reloadAfterLinkChange(channelId);
|
|
8287
8742
|
} catch (err) {
|
|
8288
8743
|
this.showDialogError(accessLinkErrorCopy(err instanceof ManageApiError ? err : void 0));
|
|
8289
8744
|
if (!(err instanceof ManageApiError)) this.host.onError(err);
|
|
@@ -8326,9 +8781,7 @@ var ChannelsMode = class {
|
|
|
8326
8781
|
endActiveSessions
|
|
8327
8782
|
);
|
|
8328
8783
|
this.closeDialog();
|
|
8329
|
-
await this.
|
|
8330
|
-
await this.refresh({ quiet: true });
|
|
8331
|
-
this.paintRail();
|
|
8784
|
+
await this.reloadAfterLinkChange(channelId);
|
|
8332
8785
|
this.host.toast(res.endedSessions ? `Link revoked. ${res.endedSessions.toLocaleString()} buyer${res.endedSessions === 1 ? "" : "s"} lost access.` : "Link revoked. It no longer opens for anyone.", "ok");
|
|
8333
8786
|
} catch (err) {
|
|
8334
8787
|
this.showDialogError(accessLinkErrorCopy(err instanceof ManageApiError ? err : void 0));
|
|
@@ -8413,9 +8866,19 @@ var LEGEND = [
|
|
|
8413
8866
|
{ key: "booked", label: "Booked", color: "#22a06b" },
|
|
8414
8867
|
{ key: "blocked", label: "Blocked", color: "#8b94ac" }
|
|
8415
8868
|
];
|
|
8416
|
-
var
|
|
8869
|
+
var MANAGER_CSS = (
|
|
8870
|
+
/* @sl-css */
|
|
8871
|
+
`
|
|
8417
8872
|
.slm{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:480px;overflow:hidden;
|
|
8418
|
-
background:var(--slm-bg);color:var(--slm-text);font-family:var(--slm-font);border-radius:var(--slm-radius)
|
|
8873
|
+
background:var(--slm-bg);color:var(--slm-text);font-family:var(--slm-font);border-radius:var(--slm-radius);
|
|
8874
|
+
/* Motion tokens (motion-system \xA72), declared by the cockpit ROOT rather than
|
|
8875
|
+
borrowed from CHANNELS_CSS. The base cockpit animates whether or not
|
|
8876
|
+
Channels mode is in use, so owning its own tokens is what stops a token
|
|
8877
|
+
edit from silently changing only half the surface. Channels mode declares
|
|
8878
|
+
the identical values so an embed of it stays self-contained. */
|
|
8879
|
+
--slm-mo-instant:80ms;--slm-mo-quick:140ms;--slm-mo-base:200ms;--slm-mo-slow:320ms;--slm-mo-ambient:2000ms;
|
|
8880
|
+
--slm-mo-out:cubic-bezier(.2,.8,.2,1);--slm-mo-in-out:cubic-bezier(.4,0,.2,1);--slm-mo-exit:cubic-bezier(.4,0,1,1);
|
|
8881
|
+
--slm-mo-spring:cubic-bezier(.34,1.3,.64,1)}
|
|
8419
8882
|
.slm *{box-sizing:border-box;margin:0;padding:0}
|
|
8420
8883
|
.slm button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
|
|
8421
8884
|
.slm input{font:inherit}
|
|
@@ -8428,7 +8891,8 @@ var CSS2 = `
|
|
|
8428
8891
|
.slm-mode.on{background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
8429
8892
|
.slm-live{display:inline-flex;align-items:center;gap:6px;font-size:11px;letter-spacing:.12em;font-weight:800;color:var(--slm-muted)}
|
|
8430
8893
|
.slm-live-dot{width:8px;height:8px;border-radius:50%;background:#8b94ac}
|
|
8431
|
-
.slm.live .slm-live-dot{background:#22a06b;box-shadow:0 0 0 0 rgba(34,160,107,.55);
|
|
8894
|
+
.slm.live .slm-live-dot{background:#22a06b;box-shadow:0 0 0 0 rgba(34,160,107,.55);
|
|
8895
|
+
animation:slm-pulse var(--slm-mo-ambient) infinite}
|
|
8432
8896
|
@keyframes slm-pulse{0%{box-shadow:0 0 0 0 rgba(34,160,107,.5)}70%{box-shadow:0 0 0 7px rgba(34,160,107,0)}100%{box-shadow:0 0 0 0 rgba(34,160,107,0)}}
|
|
8433
8897
|
.slm-kpis{grid-column:1/-1;display:grid;grid-template-columns:repeat(8,minmax(0,1fr));width:100%;padding-top:10px;
|
|
8434
8898
|
border-top:1px solid var(--slm-line)}
|
|
@@ -8437,7 +8901,11 @@ var CSS2 = `
|
|
|
8437
8901
|
font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
8438
8902
|
.slm-kpi span{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--slm-muted);font-weight:700}
|
|
8439
8903
|
.slm-kpi .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:baseline}
|
|
8440
|
-
|
|
8904
|
+
/* The one playful moment this surface is allowed (\xA72 --mo-spring). It ran at
|
|
8905
|
+
.58s against a 200ms catalog, which read as a different design language from
|
|
8906
|
+
the dashboard tile it mirrors; Channels mode's own count bump is the same
|
|
8907
|
+
pattern and must stay in step with it. */
|
|
8908
|
+
.slm-kpi.changed b{animation:slm-kpi-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
8441
8909
|
.slm-kpidelta{position:absolute;right:4px;top:-12px;padding:2px 5px;border-radius:999px;background:rgba(34,160,107,.17);
|
|
8442
8910
|
color:#5bd39b!important;font-size:9px!important;letter-spacing:0!important;text-transform:none!important;white-space:nowrap;
|
|
8443
8911
|
animation:slm-kpi-delta 1.45s ease-out both;pointer-events:none}
|
|
@@ -8456,12 +8924,14 @@ var CSS2 = `
|
|
|
8456
8924
|
.slm-hud-chip{padding:6px 11px;border-radius:999px;font-size:12px;font-weight:700;background:var(--slm-surface);
|
|
8457
8925
|
border:1px solid var(--slm-line);color:var(--slm-text)}
|
|
8458
8926
|
.slm-zoomhint{position:absolute;left:50%;top:14px;transform:translateX(-50%);padding:6px 13px;border-radius:999px;
|
|
8459
|
-
background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:700;pointer-events:none;opacity:0;
|
|
8927
|
+
background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:700;pointer-events:none;opacity:0;
|
|
8928
|
+
transition:opacity var(--slm-mo-base) var(--slm-mo-out)}
|
|
8460
8929
|
.slm-zoomhint.on{opacity:1}
|
|
8461
8930
|
.slm-liveevent{position:absolute;left:50%;top:14px;z-index:4;display:flex;align-items:center;gap:8px;max-width:min(560px,calc(100% - 32px));
|
|
8462
8931
|
padding:8px 12px;border:1px solid var(--slm-line);border-radius:999px;background:color-mix(in srgb,var(--slm-surface) 92%,transparent);
|
|
8463
8932
|
box-shadow:0 10px 34px rgba(0,0,0,.32);opacity:0;transform:translate(-50%,-8px);pointer-events:none;
|
|
8464
|
-
transition:opacity
|
|
8933
|
+
transition:opacity var(--slm-mo-quick) var(--slm-mo-out),transform var(--slm-mo-base) var(--slm-mo-out);
|
|
8934
|
+
backdrop-filter:blur(10px)}
|
|
8465
8935
|
.slm-liveevent.on{opacity:1;transform:translate(-50%,0)}
|
|
8466
8936
|
.slm.block-mode .slm-liveevent{top:52px}
|
|
8467
8937
|
.slm-liveeventdot{width:8px;height:8px;border-radius:50%;flex:none}.slm-liveeventcopy{min-width:0;overflow:hidden;text-overflow:ellipsis;
|
|
@@ -8481,7 +8951,8 @@ var CSS2 = `
|
|
|
8481
8951
|
/* activity feed */
|
|
8482
8952
|
.slm-feed{display:flex;flex-direction:column;gap:0}
|
|
8483
8953
|
.slm-feedrow{display:flex!important;width:100%;align-items:center;gap:9px;padding:8px 2px!important;border-bottom:1px solid var(--slm-line)!important;
|
|
8484
|
-
border-radius:6px;font-size:12.5px;text-align:left!important;
|
|
8954
|
+
border-radius:6px;font-size:12.5px;text-align:left!important;
|
|
8955
|
+
animation:slm-in var(--slm-mo-quick) var(--slm-mo-out)}
|
|
8485
8956
|
.slm-feedrow:hover{background:rgba(255,255,255,.035)!important}
|
|
8486
8957
|
@keyframes slm-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
|
|
8487
8958
|
.slm-feeddot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
@@ -8548,7 +9019,8 @@ var CSS2 = `
|
|
|
8548
9019
|
|
|
8549
9020
|
/* toast */
|
|
8550
9021
|
.slm-toast{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);padding:10px 16px;border-radius:10px;
|
|
8551
|
-
font-size:13px;font-weight:700;box-shadow:0 8px 24px rgba(0,0,0,.28);opacity:0;pointer-events:none;
|
|
9022
|
+
font-size:13px;font-weight:700;box-shadow:0 8px 24px rgba(0,0,0,.28);opacity:0;pointer-events:none;
|
|
9023
|
+
transition:opacity var(--slm-mo-base) var(--slm-mo-out);
|
|
8552
9024
|
background:var(--slm-surface);color:var(--slm-text);border:1px solid var(--slm-line);z-index:5}
|
|
8553
9025
|
.slm-toast.on{opacity:1}
|
|
8554
9026
|
.slm-toast.err{background:#c0392b;color:#fff;border-color:#c0392b}
|
|
@@ -8559,7 +9031,9 @@ var CSS2 = `
|
|
|
8559
9031
|
.slm-barbtn.on{background:rgba(244,183,64,.13);border-color:#f4b740;color:#f7ca6b}
|
|
8560
9032
|
.slm-sectionlist{display:flex;flex-direction:column;gap:8px;margin-top:4px}
|
|
8561
9033
|
.slm-sectionlist + .slm-eyebrow{margin-top:18px}
|
|
8562
|
-
.slm-sectionrow{width:100%;padding:10px!important;border:1px solid var(--slm-line)!important;border-radius:10px;
|
|
9034
|
+
.slm-sectionrow{width:100%;padding:10px!important;border:1px solid var(--slm-line)!important;border-radius:10px;
|
|
9035
|
+
background:var(--slm-surface)!important;text-align:left!important;
|
|
9036
|
+
transition:border-color var(--slm-mo-quick) var(--slm-mo-out),transform var(--slm-mo-quick) var(--slm-mo-out)}
|
|
8563
9037
|
.slm-sectionrow:hover{border-color:var(--slm-muted)!important;transform:translateY(-1px)}
|
|
8564
9038
|
.slm-sectiontop,.slm-sectionmeta{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
8565
9039
|
.slm-sectiontop{font-size:12.5px;font-weight:800}.slm-sectionmeta{margin-top:5px;color:var(--slm-muted);font-size:11px}
|
|
@@ -8578,7 +9052,8 @@ var CSS2 = `
|
|
|
8578
9052
|
.slm-momentumcopy{margin-top:7px;color:var(--slm-muted);font-size:11px;line-height:1.45}
|
|
8579
9053
|
/* sections: availability windows */
|
|
8580
9054
|
.slm-availlist{display:flex;flex-direction:column;gap:8px;margin:2px 0 12px}
|
|
8581
|
-
.slm-availrow{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
9055
|
+
.slm-availrow{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
9056
|
+
transition:border-color var(--slm-mo-quick) var(--slm-mo-out),opacity var(--slm-mo-quick) var(--slm-mo-out)}
|
|
8582
9057
|
.slm-availrow.zone{background:color-mix(in srgb,var(--slm-surface) 82%,#000)}
|
|
8583
9058
|
.slm-availrow.hidden{opacity:.62}.slm-availrow.closed{opacity:.82}
|
|
8584
9059
|
.slm-availhead{display:flex;align-items:center;gap:8px}
|
|
@@ -8618,16 +9093,30 @@ var CSS2 = `
|
|
|
8618
9093
|
.slm.compact .slm-barbtn{flex:1;padding:6px 9px}.slm.compact .slm-kpis{grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
|
|
8619
9094
|
.slm.compact .slm-kpi[data-kpi="buyers"],.slm.compact .slm-kpi[data-kpi="active-holds"],
|
|
8620
9095
|
.slm.compact .slm-kpi[data-kpi="sold-pct"],.slm.compact .slm-kpi[data-kpi="gross-sales"]{display:none}
|
|
9096
|
+
/* Reduced motion, as a BLANKET over the cockpit subtree rather than a list of
|
|
9097
|
+
selectors. The list this replaces named four animations and two transitions,
|
|
9098
|
+
and had silently fallen behind the stylesheet: the zoom hint, the toast and
|
|
9099
|
+
the availability rows all still animated for a user who had asked the OS for
|
|
9100
|
+
none. An enumerated list has to be edited every time a rule is added, and
|
|
9101
|
+
nothing fails when it isn't \u2014 so it drifts. This cannot.
|
|
9102
|
+
|
|
9103
|
+
Motion is removed, never the information it carried: Channels mode's own
|
|
9104
|
+
block substitutes static outlines for its shake and success states, and it
|
|
9105
|
+
stays authoritative for those. No JS here waits on animationend or
|
|
9106
|
+
transitionend, so cutting them outright strands no state. */
|
|
8621
9107
|
@media (prefers-reduced-motion:reduce){
|
|
8622
|
-
.slm
|
|
8623
|
-
|
|
9108
|
+
.slm,.slm *,.slm *::before,.slm *::after{
|
|
9109
|
+
animation:none!important;
|
|
9110
|
+
transition:none!important;
|
|
9111
|
+
scroll-behavior:auto!important}
|
|
8624
9112
|
}
|
|
8625
|
-
${CHANNELS_CSS}
|
|
9113
|
+
${CHANNELS_CSS}`
|
|
9114
|
+
);
|
|
8626
9115
|
function injectStyle() {
|
|
8627
9116
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
|
|
8628
9117
|
const el = document.createElement("style");
|
|
8629
9118
|
el.id = STYLE_ID2;
|
|
8630
|
-
el.textContent =
|
|
9119
|
+
el.textContent = MANAGER_CSS;
|
|
8631
9120
|
document.head.appendChild(el);
|
|
8632
9121
|
}
|
|
8633
9122
|
function themeVars(theme) {
|
|
@@ -8687,6 +9176,11 @@ var SeatManager = class {
|
|
|
8687
9176
|
this.reconnectTimer = null;
|
|
8688
9177
|
this.attempt = 0;
|
|
8689
9178
|
this.closed = false;
|
|
9179
|
+
/** Mirrors the `live` root class, so the getter never has to read the DOM. */
|
|
9180
|
+
this.connectionStatus = "reconnecting";
|
|
9181
|
+
/** When the server last told us something. Stamped on accepted traffic only —
|
|
9182
|
+
* a socket that opens and says nothing has not refreshed anything. */
|
|
9183
|
+
this.lastMessageAt = null;
|
|
8690
9184
|
this.ready = false;
|
|
8691
9185
|
this.feed = [];
|
|
8692
9186
|
this.feedTimer = null;
|
|
@@ -8896,6 +9390,12 @@ var SeatManager = class {
|
|
|
8896
9390
|
},
|
|
8897
9391
|
worldToScreen: (point) => this.renderer?.worldToScreen(point) ?? null,
|
|
8898
9392
|
seatPixelSize: () => this.seatPixelSize(),
|
|
9393
|
+
isSeatDetail: () => this.renderer?.getRung?.() === "seats",
|
|
9394
|
+
showSectionOverview: () => {
|
|
9395
|
+
this.renderer?.clearSectionFocus();
|
|
9396
|
+
this.renderer?.setRung?.("sections");
|
|
9397
|
+
},
|
|
9398
|
+
focusSection: (sectionId) => this.renderer?.focusSection(sectionId),
|
|
8899
9399
|
isCompact: () => !!this.root?.classList.contains("compact"),
|
|
8900
9400
|
setMapInert: (inert) => {
|
|
8901
9401
|
this.mapHost.toggleAttribute("inert", inert);
|
|
@@ -8905,13 +9405,15 @@ var SeatManager = class {
|
|
|
8905
9405
|
onError: (err) => this.opts.onError?.(err)
|
|
8906
9406
|
};
|
|
8907
9407
|
}
|
|
8908
|
-
/**
|
|
8909
|
-
*
|
|
9408
|
+
/** Actual on-screen seat diameter, for the channel overlay's marks. The
|
|
9409
|
+
* renderer's base seat radius is 9 chart units; retaining the camera scale
|
|
9410
|
+
* (rather than capping it) keeps every preview paint aligned with the real
|
|
9411
|
+
* chart geometry at deep zoom. */
|
|
8910
9412
|
seatPixelSize() {
|
|
8911
9413
|
const rect = this.renderer?.getVisibleWorldRect?.();
|
|
8912
9414
|
const width = this.mapHost?.clientWidth ?? 0;
|
|
8913
9415
|
if (!rect?.width || !width) return 6;
|
|
8914
|
-
return Math.max(3,
|
|
9416
|
+
return Math.max(3, width / rect.width * 18);
|
|
8915
9417
|
}
|
|
8916
9418
|
/** Toggle the normalized sales-velocity outline overlay without changing seat colors. */
|
|
8917
9419
|
setHeatOverlay(enabled) {
|
|
@@ -9087,6 +9589,16 @@ var SeatManager = class {
|
|
|
9087
9589
|
getControlRoomSnapshot(windowMinutes = this.trendWindowMinutes) {
|
|
9088
9590
|
return this.setTrendWindow(windowMinutes);
|
|
9089
9591
|
}
|
|
9592
|
+
/**
|
|
9593
|
+
* The realtime link's current state and the "as of" behind it.
|
|
9594
|
+
*
|
|
9595
|
+
* Pair with `onConnectionChange` for the edges: a host that mounts after a
|
|
9596
|
+
* drop, or re-reads on tab focus, needs to be able to ASK rather than wait
|
|
9597
|
+
* for the next transition that may never come.
|
|
9598
|
+
*/
|
|
9599
|
+
getConnection() {
|
|
9600
|
+
return { status: this.connectionStatus, lastMessageAt: this.lastMessageAt };
|
|
9601
|
+
}
|
|
9090
9602
|
getLog(opts = {}) {
|
|
9091
9603
|
return this.api.log(this.key, opts);
|
|
9092
9604
|
}
|
|
@@ -9151,6 +9663,10 @@ var SeatManager = class {
|
|
|
9151
9663
|
onSelect: (seat) => this.handleSeatSelect(seat),
|
|
9152
9664
|
onDeselect: () => this.syncSelection(),
|
|
9153
9665
|
onMarquee: () => this.syncSelection(),
|
|
9666
|
+
onSectionTap: (sectionId) => {
|
|
9667
|
+
this.renderer?.focusSection(sectionId);
|
|
9668
|
+
this.channels?.handleSectionFocus(sectionId);
|
|
9669
|
+
},
|
|
9154
9670
|
onViewChange: () => {
|
|
9155
9671
|
this.updateZoomHint();
|
|
9156
9672
|
this.channels?.handleViewChange();
|
|
@@ -9161,10 +9677,11 @@ var SeatManager = class {
|
|
|
9161
9677
|
this.applyHeatOverlay();
|
|
9162
9678
|
this.updateZoomHint();
|
|
9163
9679
|
}
|
|
9164
|
-
/** Block
|
|
9165
|
-
*
|
|
9680
|
+
/** Block always uses a marquee. Channels only enables its marquee after the
|
|
9681
|
+
* organizer deliberately chooses Assign seats; Pan map keeps desktop drag
|
|
9682
|
+
* available for large charts. */
|
|
9166
9683
|
isBulkSelectMode() {
|
|
9167
|
-
return this.mode === "block" || this.mode === "channels" && this.channels?.
|
|
9684
|
+
return this.mode === "block" || this.mode === "channels" && this.channels?.usesMarqueeSelection() === true;
|
|
9168
9685
|
}
|
|
9169
9686
|
/**
|
|
9170
9687
|
* Block never touches held or booked inventory, so it cannot select it.
|
|
@@ -9174,7 +9691,7 @@ var SeatManager = class {
|
|
|
9174
9691
|
*/
|
|
9175
9692
|
selectableStatuses() {
|
|
9176
9693
|
if (this.mode === "block") return ["free", "not_for_sale"];
|
|
9177
|
-
if (this.mode === "inspect" || this.isBulkSelectMode()) {
|
|
9694
|
+
if (this.mode === "inspect" || this.isBulkSelectMode() || this.mode === "channels" && this.channels?.canSelect() === true) {
|
|
9178
9695
|
return ["free", "held", "booked", "not_for_sale"];
|
|
9179
9696
|
}
|
|
9180
9697
|
return [];
|
|
@@ -9277,6 +9794,7 @@ var SeatManager = class {
|
|
|
9277
9794
|
return;
|
|
9278
9795
|
}
|
|
9279
9796
|
if (!msg || typeof msg !== "object") return;
|
|
9797
|
+
this.lastMessageAt = Date.now();
|
|
9280
9798
|
const m = msg;
|
|
9281
9799
|
if (Array.isArray(m.hidden) || Array.isArray(m.closed)) {
|
|
9282
9800
|
this.updateEffectiveAvailability(m.hidden, m.closed);
|
|
@@ -9333,6 +9851,7 @@ var SeatManager = class {
|
|
|
9333
9851
|
const objs = await this.api.objects(this.key);
|
|
9334
9852
|
this.applySnapshot(objs.seats);
|
|
9335
9853
|
this.updateEffectiveAvailability(objs.hidden, objs.closed);
|
|
9854
|
+
this.lastMessageAt = Date.now();
|
|
9336
9855
|
} catch {
|
|
9337
9856
|
}
|
|
9338
9857
|
}
|
|
@@ -9807,6 +10326,14 @@ var SeatManager = class {
|
|
|
9807
10326
|
this.root?.classList.toggle("live", on);
|
|
9808
10327
|
if (this.els.livetext) this.els.livetext.textContent = on ? "LIVE" : "RECONNECTING";
|
|
9809
10328
|
this.paintMonitorInsights();
|
|
10329
|
+
const next = on ? "live" : "reconnecting";
|
|
10330
|
+
if (next === this.connectionStatus) return;
|
|
10331
|
+
this.connectionStatus = next;
|
|
10332
|
+
try {
|
|
10333
|
+
this.opts.onConnectionChange?.(this.getConnection());
|
|
10334
|
+
} catch (err) {
|
|
10335
|
+
this.opts.onError?.(err);
|
|
10336
|
+
}
|
|
9810
10337
|
}
|
|
9811
10338
|
updateZoomHint() {
|
|
9812
10339
|
const hint = this.els.zoomhint;
|