@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.cjs
CHANGED
|
@@ -8,6 +8,14 @@ var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
|
8
8
|
var __typeError = (msg) => {
|
|
9
9
|
throw TypeError(msg);
|
|
10
10
|
};
|
|
11
|
+
var __esm = (fn, res, err) => function __init() {
|
|
12
|
+
if (err) throw err[0];
|
|
13
|
+
try {
|
|
14
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
15
|
+
} catch (e) {
|
|
16
|
+
throw err = [e], e;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
11
19
|
var __export = (target, all) => {
|
|
12
20
|
for (var name in all)
|
|
13
21
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -35,6 +43,354 @@ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot
|
|
|
35
43
|
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
36
44
|
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
37
45
|
|
|
46
|
+
// src/hostedCheckout.ts
|
|
47
|
+
var hostedCheckout_exports = {};
|
|
48
|
+
__export(hostedCheckout_exports, {
|
|
49
|
+
errorCopy: () => errorCopy,
|
|
50
|
+
formatMoney: () => formatMoney,
|
|
51
|
+
mountCheckout: () => mountCheckout,
|
|
52
|
+
unavailableCopy: () => unavailableCopy
|
|
53
|
+
});
|
|
54
|
+
function ensureStyle() {
|
|
55
|
+
if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return;
|
|
56
|
+
const el2 = document.createElement("style");
|
|
57
|
+
el2.id = STYLE_ID;
|
|
58
|
+
el2.textContent = CSS;
|
|
59
|
+
document.head.appendChild(el2);
|
|
60
|
+
}
|
|
61
|
+
function formatMoney(amount, currency) {
|
|
62
|
+
try {
|
|
63
|
+
return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(amount);
|
|
64
|
+
} catch {
|
|
65
|
+
return `${amount.toFixed(2)} ${currency}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function unavailableCopy(reason, seatCount) {
|
|
69
|
+
const held = `${seatCount} ${seatCount === 1 ? "seat is" : "seats are"} held for a limited time.`;
|
|
70
|
+
if (reason === "payments_off_for_event") {
|
|
71
|
+
return {
|
|
72
|
+
title: "This event isn\u2019t sold online",
|
|
73
|
+
body: `${held} The organiser isn\u2019t taking payment for this event here.`,
|
|
74
|
+
detail: "Nothing has been charged. Check where you found this event for how to get tickets."
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (reason === "unavailable_for_event") {
|
|
78
|
+
return {
|
|
79
|
+
title: "This event isn\u2019t taking payment yet",
|
|
80
|
+
body: `${held} Online payment is not switched on for this event.`,
|
|
81
|
+
detail: "Nothing has been charged. If you were sent here to pay, let the organiser know \u2014 only they can turn payment on for this event."
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
title: "Finish in the ticketing checkout",
|
|
86
|
+
body: `${held} Payment for this event is taken elsewhere.`,
|
|
87
|
+
detail: "Nothing has been charged. Continue in the checkout on this page to pay for your seats."
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function errorCopy(code) {
|
|
91
|
+
switch (code) {
|
|
92
|
+
case "gateway_not_connected":
|
|
93
|
+
case "payments_not_enabled_for_event":
|
|
94
|
+
return "This event is not taking online payments. Contact the organiser to buy these seats.";
|
|
95
|
+
case "provider_mismatch":
|
|
96
|
+
return "This event\u2019s payment setup changed while you were choosing. Nothing was charged \u2014 please try again.";
|
|
97
|
+
case "hold_not_active":
|
|
98
|
+
case "hold_not_found":
|
|
99
|
+
return "Your seats were released before checkout started. Nothing was charged \u2014 please pick again.";
|
|
100
|
+
case "checkout_already_started":
|
|
101
|
+
return "A payment for these seats is already in progress. Finish that one, or wait for it to time out before starting again.";
|
|
102
|
+
case "event_closed":
|
|
103
|
+
return "Sales for this event have closed.";
|
|
104
|
+
case "gateway_currency_mismatch":
|
|
105
|
+
case "unsupported_currency":
|
|
106
|
+
case "mixed_currency_hold":
|
|
107
|
+
case "price_unusable":
|
|
108
|
+
return "These seats cannot be checked out right now because of a pricing configuration problem. Nothing was charged \u2014 please contact the organiser.";
|
|
109
|
+
case "rate_limited":
|
|
110
|
+
return "Too many attempts. Wait a moment and try again.";
|
|
111
|
+
default:
|
|
112
|
+
return "We could not start the payment. Nothing was charged \u2014 please try again.";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function loadRazorpay() {
|
|
116
|
+
const existing = window.Razorpay;
|
|
117
|
+
if (existing) return Promise.resolve(existing);
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
const previous = document.querySelector(`script[src="${RAZORPAY_SCRIPT}"]`);
|
|
120
|
+
const tag = previous ?? document.createElement("script");
|
|
121
|
+
const done = () => {
|
|
122
|
+
const ctor = window.Razorpay;
|
|
123
|
+
if (ctor) resolve(ctor);
|
|
124
|
+
else reject(new Error("razorpay_unavailable"));
|
|
125
|
+
};
|
|
126
|
+
tag.addEventListener("load", done, { once: true });
|
|
127
|
+
tag.addEventListener("error", () => reject(new Error("razorpay_script_failed")), { once: true });
|
|
128
|
+
if (previous) return;
|
|
129
|
+
tag.src = RAZORPAY_SCRIPT;
|
|
130
|
+
tag.async = true;
|
|
131
|
+
document.head.appendChild(tag);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function el(tag, className, text) {
|
|
135
|
+
const node = document.createElement(tag);
|
|
136
|
+
if (className) node.className = className;
|
|
137
|
+
if (text !== void 0) node.textContent = text;
|
|
138
|
+
return node;
|
|
139
|
+
}
|
|
140
|
+
function mountCheckout(mount) {
|
|
141
|
+
ensureStyle();
|
|
142
|
+
let live = true;
|
|
143
|
+
let confirmed = false;
|
|
144
|
+
const scrim = el("div", "sl-hco");
|
|
145
|
+
scrim.setAttribute("role", "dialog");
|
|
146
|
+
scrim.setAttribute("aria-modal", "true");
|
|
147
|
+
const card = el("div", "sl-hco-card");
|
|
148
|
+
scrim.appendChild(card);
|
|
149
|
+
const destroy = () => {
|
|
150
|
+
live = false;
|
|
151
|
+
scrim.remove();
|
|
152
|
+
document.removeEventListener("keydown", onKey, true);
|
|
153
|
+
};
|
|
154
|
+
const cancel = () => {
|
|
155
|
+
destroy();
|
|
156
|
+
mount.onCancel();
|
|
157
|
+
};
|
|
158
|
+
function onKey(event) {
|
|
159
|
+
if (event.key !== "Escape" || !scrim.isConnected) return;
|
|
160
|
+
event.stopPropagation();
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
cancel();
|
|
163
|
+
}
|
|
164
|
+
document.addEventListener("keydown", onKey, true);
|
|
165
|
+
const title = el("h2", "sl-hco-title", "Checkout");
|
|
166
|
+
const titleId = `sl-hco-t-${Math.random().toString(36).slice(2, 8)}`;
|
|
167
|
+
title.id = titleId;
|
|
168
|
+
scrim.setAttribute("aria-labelledby", titleId);
|
|
169
|
+
const show = (...nodes) => {
|
|
170
|
+
card.replaceChildren(title, ...nodes);
|
|
171
|
+
};
|
|
172
|
+
const fail = (message) => {
|
|
173
|
+
if (!live) return;
|
|
174
|
+
const status = el("p", "sl-hco-status sl-hco-error", message);
|
|
175
|
+
status.setAttribute("role", "alert");
|
|
176
|
+
const back2 = el("button", "sl-hco-back", "Back to seats");
|
|
177
|
+
back2.type = "button";
|
|
178
|
+
back2.addEventListener("click", cancel);
|
|
179
|
+
show(status, back2);
|
|
180
|
+
};
|
|
181
|
+
const waiting = (message) => {
|
|
182
|
+
if (!live) return;
|
|
183
|
+
const status = el("p", "sl-hco-status", message);
|
|
184
|
+
status.setAttribute("role", "status");
|
|
185
|
+
show(status);
|
|
186
|
+
};
|
|
187
|
+
const awaitConfirmation = async (orderId) => {
|
|
188
|
+
waiting("Payment received \u2014 confirming your seats\u2026");
|
|
189
|
+
const deadline = Date.now() + CONFIRM_TIMEOUT_MS;
|
|
190
|
+
while (live && Date.now() < deadline) {
|
|
191
|
+
try {
|
|
192
|
+
const body = await mount.orderStatus(orderId);
|
|
193
|
+
if (!live) return;
|
|
194
|
+
if (body.status === "confirmed") {
|
|
195
|
+
confirmed = true;
|
|
196
|
+
title.textContent = "Your tickets are confirmed";
|
|
197
|
+
const status = el(
|
|
198
|
+
"p",
|
|
199
|
+
"sl-hco-status",
|
|
200
|
+
`${body.seatCount} ${body.seatCount === 1 ? "seat" : "seats"} confirmed. We have emailed your tickets.`
|
|
201
|
+
);
|
|
202
|
+
const receipt = el("dl", "sl-hco-receipt");
|
|
203
|
+
receipt.append(
|
|
204
|
+
el("dt", void 0, "Paid"),
|
|
205
|
+
el("dd", void 0, `${body.amountFormatted} ${body.currency}`),
|
|
206
|
+
el("dt", void 0, "Order"),
|
|
207
|
+
el("dd", "sl-hco-ref", body.orderId)
|
|
208
|
+
);
|
|
209
|
+
const close = el("button", "sl-hco-back", "Close");
|
|
210
|
+
close.type = "button";
|
|
211
|
+
close.addEventListener("click", cancel);
|
|
212
|
+
show(status, receipt, close);
|
|
213
|
+
mount.onConfirmed(body);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (body.status === "failed" || body.status === "expired") {
|
|
217
|
+
fail(body.status === "expired" ? "Your seats were released before payment completed. Nothing was charged \u2014 please pick again." : "The payment did not complete. If you were charged, it has been refunded automatically.");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
}
|
|
222
|
+
await new Promise((resolve) => setTimeout(resolve, CONFIRM_POLL_MS));
|
|
223
|
+
}
|
|
224
|
+
if (!live || confirmed) return;
|
|
225
|
+
fail("Still confirming with the payment provider. If your payment went through, your tickets will arrive by email shortly \u2014 you do not need to pay again.");
|
|
226
|
+
};
|
|
227
|
+
if (mount.state.kind === "unavailable") {
|
|
228
|
+
const copy = unavailableCopy(mount.state.reason, mount.state.seatCount);
|
|
229
|
+
title.textContent = copy.title;
|
|
230
|
+
const kicker = el("p", "sl-hco-label", "Seats held");
|
|
231
|
+
const back2 = el("button", "sl-hco-back", "Back to seat map");
|
|
232
|
+
back2.type = "button";
|
|
233
|
+
back2.addEventListener("click", cancel);
|
|
234
|
+
card.replaceChildren(
|
|
235
|
+
kicker,
|
|
236
|
+
title,
|
|
237
|
+
el("p", "sl-hco-status", copy.body),
|
|
238
|
+
el("p", "sl-hco-note", copy.detail),
|
|
239
|
+
back2
|
|
240
|
+
);
|
|
241
|
+
mount.root.appendChild(scrim);
|
|
242
|
+
back2.focus();
|
|
243
|
+
return { destroy };
|
|
244
|
+
}
|
|
245
|
+
if (mount.state.kind === "resume") {
|
|
246
|
+
mount.root.appendChild(scrim);
|
|
247
|
+
void awaitConfirmation(mount.state.orderId);
|
|
248
|
+
return { destroy };
|
|
249
|
+
}
|
|
250
|
+
const { order, provider } = mount.state;
|
|
251
|
+
const summary = el("div", "sl-hco-summary");
|
|
252
|
+
const seats = el("div", "sl-hco-seats");
|
|
253
|
+
for (const label of order.labels) seats.appendChild(el("span", "sl-hco-seat", label));
|
|
254
|
+
const totalText = formatMoney(order.total, order.currency);
|
|
255
|
+
const totalRow = el("div", "sl-hco-total");
|
|
256
|
+
totalRow.append(el("span", void 0, "Total"), el("strong", void 0, totalText));
|
|
257
|
+
summary.append(seats, totalRow);
|
|
258
|
+
const form = el("form", "sl-hco-form");
|
|
259
|
+
const emailLabel = el("label", "sl-hco-label", "Email \u2014 your tickets go here");
|
|
260
|
+
const email = el("input", "sl-hco-input");
|
|
261
|
+
email.type = "email";
|
|
262
|
+
email.required = true;
|
|
263
|
+
email.autocomplete = "email";
|
|
264
|
+
email.placeholder = "you@example.com";
|
|
265
|
+
email.id = `${titleId}-email`;
|
|
266
|
+
emailLabel.htmlFor = email.id;
|
|
267
|
+
const nameLabel = el("label", "sl-hco-label", "Name (optional)");
|
|
268
|
+
const name = el("input", "sl-hco-input");
|
|
269
|
+
name.type = "text";
|
|
270
|
+
name.autocomplete = "name";
|
|
271
|
+
name.placeholder = "Your name";
|
|
272
|
+
name.id = `${titleId}-name`;
|
|
273
|
+
nameLabel.htmlFor = name.id;
|
|
274
|
+
const pay = el("button", "sl-hco-pay", `Pay ${totalText}`);
|
|
275
|
+
pay.type = "submit";
|
|
276
|
+
pay.disabled = true;
|
|
277
|
+
const back = el("button", "sl-hco-back", "Back to seats");
|
|
278
|
+
back.type = "button";
|
|
279
|
+
back.addEventListener("click", cancel);
|
|
280
|
+
const until = new Date(order.expiresAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
281
|
+
const note = el(
|
|
282
|
+
"p",
|
|
283
|
+
"sl-hco-note",
|
|
284
|
+
`Your seats are held until ${until}. Payment is handled by ${provider === "razorpay" ? "Razorpay" : "Stripe"} \u2014 we never see your card details.`
|
|
285
|
+
);
|
|
286
|
+
const emailValid = () => /.+@.+\..+/.test(email.value.trim());
|
|
287
|
+
email.addEventListener("input", () => {
|
|
288
|
+
pay.disabled = !emailValid();
|
|
289
|
+
});
|
|
290
|
+
const start = async () => {
|
|
291
|
+
waiting("Opening secure payment\u2026");
|
|
292
|
+
try {
|
|
293
|
+
const body = await mount.startSession({
|
|
294
|
+
holdId: order.holdId,
|
|
295
|
+
buyerEmail: email.value.trim(),
|
|
296
|
+
// Deliberately no `provider`: since W2 the EVENT row decides which
|
|
297
|
+
// gateway charges, and naming one here can only ever 409 on a mismatch
|
|
298
|
+
// the buyer cannot do anything about.
|
|
299
|
+
...name.value.trim() ? { buyerName: name.value.trim() } : {}
|
|
300
|
+
});
|
|
301
|
+
if (!live) return;
|
|
302
|
+
if (body.redirectUrl) {
|
|
303
|
+
waiting("Taking you to secure payment\u2026");
|
|
304
|
+
window.location.assign(body.redirectUrl);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (body.clientPayload) {
|
|
308
|
+
const payload = body.clientPayload;
|
|
309
|
+
const Razorpay = await loadRazorpay();
|
|
310
|
+
if (!live) return;
|
|
311
|
+
waiting("Waiting for payment\u2026");
|
|
312
|
+
new Razorpay({
|
|
313
|
+
key: payload.key,
|
|
314
|
+
order_id: payload.orderId,
|
|
315
|
+
amount: payload.amount,
|
|
316
|
+
currency: payload.currency,
|
|
317
|
+
name: payload.name,
|
|
318
|
+
prefill: payload.prefill,
|
|
319
|
+
// The handler fires on the browser's word alone, so it only starts the
|
|
320
|
+
// wait — the webhook is what actually confirms the order.
|
|
321
|
+
handler: () => {
|
|
322
|
+
void awaitConfirmation(body.orderId);
|
|
323
|
+
},
|
|
324
|
+
modal: { ondismiss: () => {
|
|
325
|
+
if (live) details();
|
|
326
|
+
} }
|
|
327
|
+
}).open();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
fail(errorCopy("gateway_unavailable"));
|
|
331
|
+
} catch (err) {
|
|
332
|
+
mount.onError?.(err);
|
|
333
|
+
fail(errorCopy(err?.code));
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
form.addEventListener("submit", (event) => {
|
|
337
|
+
event.preventDefault();
|
|
338
|
+
if (emailValid()) void start();
|
|
339
|
+
});
|
|
340
|
+
form.append(emailLabel, email, nameLabel, name, pay, back, note);
|
|
341
|
+
const details = () => {
|
|
342
|
+
title.textContent = "Checkout";
|
|
343
|
+
show(summary, form);
|
|
344
|
+
pay.disabled = !emailValid();
|
|
345
|
+
};
|
|
346
|
+
details();
|
|
347
|
+
mount.root.appendChild(scrim);
|
|
348
|
+
email.focus();
|
|
349
|
+
return { destroy };
|
|
350
|
+
}
|
|
351
|
+
var CONFIRM_TIMEOUT_MS, CONFIRM_POLL_MS, RAZORPAY_SCRIPT, STYLE_ID, CSS;
|
|
352
|
+
var init_hostedCheckout = __esm({
|
|
353
|
+
"src/hostedCheckout.ts"() {
|
|
354
|
+
"use strict";
|
|
355
|
+
CONFIRM_TIMEOUT_MS = 9e4;
|
|
356
|
+
CONFIRM_POLL_MS = 2e3;
|
|
357
|
+
RAZORPAY_SCRIPT = "https://checkout.razorpay.com/v1/checkout.js";
|
|
358
|
+
STYLE_ID = "seatlayer-checkout-style";
|
|
359
|
+
CSS = /* @sl-css */
|
|
360
|
+
`
|
|
361
|
+
.sl-hco{position:absolute;inset:0;z-index:60;display:flex;align-items:center;justify-content:center;
|
|
362
|
+
padding:16px;background:color-mix(in srgb, var(--sl-bg) 82%, transparent);backdrop-filter:blur(3px)}
|
|
363
|
+
.sl-hco-card{width:100%;max-width:380px;max-height:100%;overflow:auto;padding:22px;
|
|
364
|
+
background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);
|
|
365
|
+
border-radius:var(--sl-radius);box-shadow:0 18px 48px rgba(0,0,0,.28)}
|
|
366
|
+
.sl-hco-title{margin:0 0 14px;font-size:19px;font-weight:650;letter-spacing:-.01em}
|
|
367
|
+
.sl-hco-summary{margin-bottom:16px;padding-bottom:14px;border-bottom:1px solid var(--sl-line)}
|
|
368
|
+
.sl-hco-seats{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}
|
|
369
|
+
.sl-hco-seat{padding:3px 8px;font-size:12px;font-weight:600;border-radius:calc(var(--sl-radius) * .5);
|
|
370
|
+
background:var(--sl-bg);border:1px solid var(--sl-line)}
|
|
371
|
+
.sl-hco-total{display:flex;justify-content:space-between;align-items:baseline;font-size:14px}
|
|
372
|
+
.sl-hco-total strong{font-size:17px;font-weight:700}
|
|
373
|
+
.sl-hco-label{display:block;margin:12px 0 5px;font-size:12px;font-weight:600;color:var(--sl-muted)}
|
|
374
|
+
.sl-hco-input{width:100%;padding:10px 11px;font:inherit;font-size:15px;color:var(--sl-text);
|
|
375
|
+
background:var(--sl-bg);border:1px solid var(--sl-line);border-radius:calc(var(--sl-radius) * .55)}
|
|
376
|
+
.sl-hco-input:focus-visible{outline:2px solid var(--sl-accent);outline-offset:1px}
|
|
377
|
+
.sl-hco-pay{width:100%;margin-top:16px;padding:12px;font:inherit;font-size:15px;font-weight:650;
|
|
378
|
+
color:var(--sl-accent-ink);background:var(--sl-accent);border:0;
|
|
379
|
+
border-radius:calc(var(--sl-radius) * .55);cursor:pointer}
|
|
380
|
+
.sl-hco-pay[disabled]{opacity:.5;cursor:default}
|
|
381
|
+
.sl-hco-back{width:100%;margin-top:8px;padding:10px;font:inherit;font-size:14px;color:var(--sl-muted);
|
|
382
|
+
background:none;border:0;cursor:pointer;text-decoration:underline}
|
|
383
|
+
.sl-hco-note{margin:12px 0 0;font-size:12px;line-height:1.5;color:var(--sl-muted)}
|
|
384
|
+
.sl-hco-status{margin:8px 0 0;font-size:14px;line-height:1.55}
|
|
385
|
+
.sl-hco-error{color:var(--sl-danger, #c0392b)}
|
|
386
|
+
.sl-hco-receipt{display:grid;grid-template-columns:auto 1fr;gap:4px 14px;margin:14px 0 0;font-size:13px}
|
|
387
|
+
.sl-hco-receipt dt{color:var(--sl-muted)}
|
|
388
|
+
.sl-hco-receipt dd{margin:0;text-align:right}
|
|
389
|
+
.sl-hco-ref{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;word-break:break-all}
|
|
390
|
+
`;
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
38
394
|
// src/index.ts
|
|
39
395
|
var index_exports = {};
|
|
40
396
|
__export(index_exports, {
|
|
@@ -185,6 +541,41 @@ var PubApi = class {
|
|
|
185
541
|
body: { holdId, ...ttlMs ? { ttlMs } : {} }
|
|
186
542
|
});
|
|
187
543
|
}
|
|
544
|
+
/**
|
|
545
|
+
* Which gateways this event can actually take money through — the question
|
|
546
|
+
* `checkout: 'hosted'` has to answer BEFORE it shows a buyer a Pay button, so
|
|
547
|
+
* the answer is never discovered by failing a payment.
|
|
548
|
+
*
|
|
549
|
+
* Anonymous, and it discloses no account, key, mode or currency for a gateway
|
|
550
|
+
* that did not match.
|
|
551
|
+
*/
|
|
552
|
+
paymentOptions(key) {
|
|
553
|
+
return this.request(`/pub/events/${encodeURIComponent(key)}/payment-options`);
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Turn a live hold into an order and start a payment.
|
|
557
|
+
*
|
|
558
|
+
* The amount is NOT sent: the server recomputes it from the hold's own items,
|
|
559
|
+
* which is the only reason a browser cannot alter what it pays. Nor is the
|
|
560
|
+
* PROVIDER — the event row decides which gateway charges, and a `provider` in
|
|
561
|
+
* the body is checked rather than obeyed (409 `provider_mismatch`). Omitting
|
|
562
|
+
* it is the shape that cannot disagree.
|
|
563
|
+
*/
|
|
564
|
+
startCheckout(key, input) {
|
|
565
|
+
return this.request(`/pub/events/${encodeURIComponent(key)}/checkout`, {
|
|
566
|
+
method: "POST",
|
|
567
|
+
body: input
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Poll an order while its gateway webhook lands. The order id is an
|
|
572
|
+
* unguessable token the buyer already holds, so it acts as the capability —
|
|
573
|
+
* which is also why a buyer returning from a gateway page can be told what
|
|
574
|
+
* happened with nothing but the id in the return URL.
|
|
575
|
+
*/
|
|
576
|
+
orderStatus(orderId) {
|
|
577
|
+
return this.request(`/pub/orders/${encodeURIComponent(orderId)}/status`);
|
|
578
|
+
}
|
|
188
579
|
/**
|
|
189
580
|
* Mint a one-use subscribe ticket for the next socket attempt (protocol doc
|
|
190
581
|
* §3). The bearer travels here, over ordinary HTTPS where CORS and Origin
|
|
@@ -845,9 +1236,9 @@ var DEFAULT_API_BASE = "https://api.seatlayer.io";
|
|
|
845
1236
|
var DEFAULT_MAX_SELECTION = 10;
|
|
846
1237
|
function resolveContainer(container) {
|
|
847
1238
|
if (typeof container === "string") {
|
|
848
|
-
const
|
|
849
|
-
if (!
|
|
850
|
-
return
|
|
1239
|
+
const el2 = document.querySelector(container);
|
|
1240
|
+
if (!el2) throw new Error(`seatmap: container "${container}" not found`);
|
|
1241
|
+
return el2;
|
|
851
1242
|
}
|
|
852
1243
|
if (!(container instanceof HTMLElement)) {
|
|
853
1244
|
throw new Error("seatmap: container must be a CSS selector or an HTMLElement");
|
|
@@ -1738,7 +2129,8 @@ var EmbeddedDesigner = class {
|
|
|
1738
2129
|
}
|
|
1739
2130
|
buildSkeleton(overlay) {
|
|
1740
2131
|
const style = document.createElement("style");
|
|
1741
|
-
style.textContent =
|
|
2132
|
+
style.textContent = /* @sl-css */
|
|
2133
|
+
`
|
|
1742
2134
|
@media (prefers-reduced-motion: no-preference) {
|
|
1743
2135
|
@keyframes seatlayer-designer-shimmer {
|
|
1744
2136
|
0% { background-position: -320px 0; }
|
|
@@ -1877,9 +2269,9 @@ function pointInPolygon(x, y, poly) {
|
|
|
1877
2269
|
}
|
|
1878
2270
|
function resolveContainer3(container) {
|
|
1879
2271
|
if (typeof container === "string") {
|
|
1880
|
-
const
|
|
1881
|
-
if (!
|
|
1882
|
-
return
|
|
2272
|
+
const el2 = document.querySelector(container);
|
|
2273
|
+
if (!el2) throw new Error(`seatmap: container "${container}" not found`);
|
|
2274
|
+
return el2;
|
|
1883
2275
|
}
|
|
1884
2276
|
if (!(container instanceof HTMLElement)) {
|
|
1885
2277
|
throw new Error("seatmap: container must be a CSS selector or an HTMLElement");
|
|
@@ -1918,20 +2310,45 @@ function hasWebGL2() {
|
|
|
1918
2310
|
}
|
|
1919
2311
|
return _webgl2Cache;
|
|
1920
2312
|
}
|
|
2313
|
+
function cdnChunkUrl(fileName) {
|
|
2314
|
+
const base = SEATLAYER_MODULE_URL ?? (typeof location !== "undefined" ? location.href : void 0);
|
|
2315
|
+
if (!base) throw new Error(`seatlayer: cannot resolve the ${fileName} chunk URL`);
|
|
2316
|
+
return new URL(`./${fileName}`, base).href;
|
|
2317
|
+
}
|
|
1921
2318
|
async function loadVenue3d() {
|
|
1922
2319
|
if (typeof __SEATLAYER_CDN__ !== "undefined" && __SEATLAYER_CDN__) {
|
|
1923
|
-
const base = SEATLAYER_MODULE_URL ?? (typeof location !== "undefined" ? location.href : void 0);
|
|
1924
|
-
if (!base) throw new Error("seatlayer: cannot resolve the 3D view chunk URL");
|
|
1925
|
-
const url = new URL("./seatlayer-view3d.mjs", base).href;
|
|
1926
2320
|
return import(
|
|
1927
2321
|
/* @vite-ignore */
|
|
1928
|
-
|
|
2322
|
+
cdnChunkUrl("seatlayer-view3d.mjs")
|
|
1929
2323
|
);
|
|
1930
2324
|
}
|
|
1931
2325
|
return import("@seatlayer/core/view3d");
|
|
1932
2326
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
2327
|
+
async function loadPanorama() {
|
|
2328
|
+
if (typeof __SEATLAYER_CDN__ !== "undefined" && __SEATLAYER_CDN__) {
|
|
2329
|
+
return import(
|
|
2330
|
+
/* @vite-ignore */
|
|
2331
|
+
cdnChunkUrl("seatlayer-panorama.mjs")
|
|
2332
|
+
);
|
|
2333
|
+
}
|
|
2334
|
+
return import("@seatlayer/core");
|
|
2335
|
+
}
|
|
2336
|
+
async function loadHostedCheckout() {
|
|
2337
|
+
if (typeof __SEATLAYER_CDN__ !== "undefined" && __SEATLAYER_CDN__) {
|
|
2338
|
+
return import(
|
|
2339
|
+
/* @vite-ignore */
|
|
2340
|
+
cdnChunkUrl("seatlayer-checkout.mjs")
|
|
2341
|
+
);
|
|
2342
|
+
}
|
|
2343
|
+
return Promise.resolve().then(() => (init_hostedCheckout(), hostedCheckout_exports));
|
|
2344
|
+
}
|
|
2345
|
+
function paymentsOffReason(reason) {
|
|
2346
|
+
return reason === "unavailable_for_event" || reason === "payments_off_for_event" ? reason : "not_configured";
|
|
2347
|
+
}
|
|
2348
|
+
var STYLE_ID2 = "seatlayer-picker-style";
|
|
2349
|
+
var CSS2 = (
|
|
2350
|
+
/* @sl-css */
|
|
2351
|
+
`
|
|
1935
2352
|
.sl-picker{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:420px;overflow:hidden;
|
|
1936
2353
|
background:var(--sl-bg);color:var(--sl-text);font-family:var(--sl-font);border-radius:var(--sl-radius);
|
|
1937
2354
|
--sl-r-sm:calc(var(--sl-radius) * .55);
|
|
@@ -2668,13 +3085,14 @@ var CSS = `
|
|
|
2668
3085
|
.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}
|
|
2669
3086
|
.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)}
|
|
2670
3087
|
@media(max-width:640px){.sl-modal-scrim{padding:0}.sl-modal-frame{width:100%;height:100%;border-radius:0}}
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
3088
|
+
`
|
|
3089
|
+
);
|
|
3090
|
+
function ensureStyle2() {
|
|
3091
|
+
if (document.getElementById(STYLE_ID2)) return;
|
|
3092
|
+
const el2 = document.createElement("style");
|
|
3093
|
+
el2.id = STYLE_ID2;
|
|
3094
|
+
el2.textContent = CSS2;
|
|
3095
|
+
document.head.appendChild(el2);
|
|
2678
3096
|
}
|
|
2679
3097
|
function resolveTokens(chart, host) {
|
|
2680
3098
|
const accent = host?.accent ?? chart?.accent ?? "#f4b740";
|
|
@@ -2733,6 +3151,14 @@ var SeatPicker = class _SeatPicker {
|
|
|
2733
3151
|
this.handedOff = false;
|
|
2734
3152
|
/** Guards single onBooked + single success overlay per hold. */
|
|
2735
3153
|
this.bookedShown = false;
|
|
3154
|
+
/**
|
|
3155
|
+
* In-flight or settled `payment-options` for this event, started at render in
|
|
3156
|
+
* hosted mode. One request, kicked off while the buyer is still choosing, so
|
|
3157
|
+
* pressing Pay does not wait on a lookup whose answer never changes mid-session.
|
|
3158
|
+
*/
|
|
3159
|
+
this.paymentOptions = null;
|
|
3160
|
+
/** The mounted payment card, while one is up. */
|
|
3161
|
+
this.checkoutPanel = null;
|
|
2736
3162
|
this.extendEl = null;
|
|
2737
3163
|
this.bookedEl = null;
|
|
2738
3164
|
this.gaQty = /* @__PURE__ */ new Map();
|
|
@@ -2847,6 +3273,12 @@ var SeatPicker = class _SeatPicker {
|
|
|
2847
3273
|
onObjectUnavailable: (event) => this.opts.onSelectedObjectUnavailable?.(event)
|
|
2848
3274
|
});
|
|
2849
3275
|
this.api = options.transport ?? this.pubApi;
|
|
3276
|
+
if (options.checkout === "hosted" && !this.pubApi) {
|
|
3277
|
+
console.warn(
|
|
3278
|
+
'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.'
|
|
3279
|
+
);
|
|
3280
|
+
}
|
|
3281
|
+
this.checkoutMode = options.checkout === "hosted" && this.pubApi ? "hosted" : "handoff";
|
|
2850
3282
|
this.maxTickets = Math.max(1, Math.floor(options.maxSelection ?? DEFAULT_MAX_SELECTION2));
|
|
2851
3283
|
this.cbSafe = readStoredColorblind() ?? !!options.colorblindSafe;
|
|
2852
3284
|
this.controller = new import_core2.PickerController({
|
|
@@ -3152,7 +3584,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
3152
3584
|
}
|
|
3153
3585
|
/** Mount the full picker as a document-level modal. Resolves after render. */
|
|
3154
3586
|
static async open(options) {
|
|
3155
|
-
|
|
3587
|
+
ensureStyle2();
|
|
3156
3588
|
const scrim = document.createElement("div");
|
|
3157
3589
|
scrim.className = "sl-modal-scrim";
|
|
3158
3590
|
const frame = document.createElement("div");
|
|
@@ -3207,9 +3639,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
3207
3639
|
async render() {
|
|
3208
3640
|
if (this.rendered) return this;
|
|
3209
3641
|
this.rendered = true;
|
|
3210
|
-
|
|
3642
|
+
ensureStyle2();
|
|
3211
3643
|
await (0, import_core2.loadLocale)(this.opts.locale);
|
|
3212
3644
|
if (this.opts.messages) (0, import_core2.setStringOverrides)(this.opts.messages);
|
|
3645
|
+
if (this.checkoutMode === "hosted") this.paymentOptions = this.pubApi.paymentOptions(this.opts.event);
|
|
3213
3646
|
const mount = resolveContainer3(this.opts.container);
|
|
3214
3647
|
const root = document.createElement("div");
|
|
3215
3648
|
root.className = "sl-picker";
|
|
@@ -3294,8 +3727,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
3294
3727
|
</div>
|
|
3295
3728
|
</div>
|
|
3296
3729
|
</div>`;
|
|
3297
|
-
root.querySelectorAll("[data-ref]").forEach((
|
|
3298
|
-
this.els[
|
|
3730
|
+
root.querySelectorAll("[data-ref]").forEach((el2) => {
|
|
3731
|
+
this.els[el2.dataset.ref] = el2;
|
|
3299
3732
|
});
|
|
3300
3733
|
this.mapHost = this.els.map;
|
|
3301
3734
|
const applyLayout = () => {
|
|
@@ -3507,8 +3940,40 @@ var SeatPicker = class _SeatPicker {
|
|
|
3507
3940
|
if (this.salesClosed) this.applySalesClosed();
|
|
3508
3941
|
this.syncPrices();
|
|
3509
3942
|
this.syncTray();
|
|
3943
|
+
this.resumeHostedOrder();
|
|
3510
3944
|
return this;
|
|
3511
3945
|
}
|
|
3946
|
+
/**
|
|
3947
|
+
* A hosted gateway returned this buyer to a page that runs the widget, with
|
|
3948
|
+
* `?order=…&status=…` in the URL. Pick the order up and finish the story.
|
|
3949
|
+
*
|
|
3950
|
+
* Only `success` resumes. `cancelled` means the buyer backed out at the
|
|
3951
|
+
* gateway and their seats are still held — the map they are looking at IS the
|
|
3952
|
+
* right screen, and opening a card to say "you cancelled" would be noise.
|
|
3953
|
+
*
|
|
3954
|
+
* The two parameters are then stripped with `replaceState`, because they are a
|
|
3955
|
+
* one-shot instruction: leaving them in place would re-open the confirmation
|
|
3956
|
+
* on every later navigation, and would carry an order id into browser history
|
|
3957
|
+
* and any Referer this page later sends. `status` is only ever removed
|
|
3958
|
+
* alongside an `order` we actually consumed, so a host page that uses a
|
|
3959
|
+
* `status` parameter of its own keeps it.
|
|
3960
|
+
*/
|
|
3961
|
+
resumeHostedOrder() {
|
|
3962
|
+
if (this.checkoutMode !== "hosted" || typeof location === "undefined") return;
|
|
3963
|
+
const params = new URLSearchParams(location.search);
|
|
3964
|
+
const orderId = params.get("order");
|
|
3965
|
+
if (!orderId) return;
|
|
3966
|
+
const status = params.get("status");
|
|
3967
|
+
params.delete("order");
|
|
3968
|
+
params.delete("status");
|
|
3969
|
+
try {
|
|
3970
|
+
const query = params.toString();
|
|
3971
|
+
history.replaceState(history.state, "", `${location.pathname}${query ? `?${query}` : ""}${location.hash}`);
|
|
3972
|
+
} catch {
|
|
3973
|
+
}
|
|
3974
|
+
if (status !== "success") return;
|
|
3975
|
+
void this.openCheckoutPanel({ kind: "resume", orderId });
|
|
3976
|
+
}
|
|
3512
3977
|
/**
|
|
3513
3978
|
* Move layout-dependent chrome between its wide dock (map regions / zoom
|
|
3514
3979
|
* column) and its narrow dock (the sheet's consolidated Filters row), and
|
|
@@ -3534,27 +3999,27 @@ var SeatPicker = class _SeatPicker {
|
|
|
3534
3999
|
}
|
|
3535
4000
|
/** The "Need more time?" prompt shown in the hold's final EXTEND_PROMPT_MS. */
|
|
3536
4001
|
buildExtendPrompt() {
|
|
3537
|
-
const
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
(this.regions["bottom-center"] ?? this.els.map).appendChild(
|
|
3542
|
-
this.extendEl =
|
|
3543
|
-
this.els.extendTxt =
|
|
3544
|
-
this.els.extendBtn =
|
|
4002
|
+
const el2 = document.createElement("div");
|
|
4003
|
+
el2.className = "sl-extend";
|
|
4004
|
+
el2.setAttribute("role", "status");
|
|
4005
|
+
el2.innerHTML = `<span class="sl-extend-txt" data-ref="extendTxt"></span><button type="button" class="sl-extend-btn" data-ref="extendBtn"></button>`;
|
|
4006
|
+
(this.regions["bottom-center"] ?? this.els.map).appendChild(el2);
|
|
4007
|
+
this.extendEl = el2;
|
|
4008
|
+
this.els.extendTxt = el2.querySelector('[data-ref="extendTxt"]');
|
|
4009
|
+
this.els.extendBtn = el2.querySelector('[data-ref="extendBtn"]');
|
|
3545
4010
|
this.els.extendBtn.textContent = "Add time";
|
|
3546
4011
|
this.els.extendBtn.addEventListener("click", () => void this.handleExtend());
|
|
3547
4012
|
}
|
|
3548
4013
|
/** Success overlay + onBooked fire when the held seats settle to booked. */
|
|
3549
4014
|
buildBookedOverlay() {
|
|
3550
|
-
const
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
this.root.appendChild(
|
|
3556
|
-
this.bookedEl =
|
|
3557
|
-
this.els.bookedSub =
|
|
4015
|
+
const el2 = document.createElement("div");
|
|
4016
|
+
el2.className = "sl-booked";
|
|
4017
|
+
el2.setAttribute("role", "status");
|
|
4018
|
+
el2.setAttribute("aria-live", "polite");
|
|
4019
|
+
el2.innerHTML = `<div class="sl-booked-badge"><svg viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg></div><div class="sl-booked-title">You're all set</div><div class="sl-booked-sub" data-ref="bookedSub"></div>`;
|
|
4020
|
+
this.root.appendChild(el2);
|
|
4021
|
+
this.bookedEl = el2;
|
|
4022
|
+
this.els.bookedSub = el2.querySelector('[data-ref="bookedSub"]');
|
|
3558
4023
|
}
|
|
3559
4024
|
/**
|
|
3560
4025
|
* Localized string with a literal fallback. `t()` returns the key itself for
|
|
@@ -3568,13 +4033,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
3568
4033
|
/** Sold-out overlay — centered over the map, disabled waitlist stub (Gap 2). */
|
|
3569
4034
|
buildSoldoutOverlay() {
|
|
3570
4035
|
if (!this.els.map) return;
|
|
3571
|
-
const
|
|
3572
|
-
|
|
3573
|
-
|
|
4036
|
+
const el2 = document.createElement("div");
|
|
4037
|
+
el2.className = "sl-soldout";
|
|
4038
|
+
el2.setAttribute("role", "status");
|
|
3574
4039
|
const name = (this.controller.doc?.theme?.brandName ?? this.opts.theme?.brandName ?? this.els.name?.textContent ?? this.tf("picker.soldOutEyebrow", "This event")).toUpperCase();
|
|
3575
|
-
|
|
3576
|
-
this.els.map.appendChild(
|
|
3577
|
-
this.soldoutEl =
|
|
4040
|
+
el2.innerHTML = `<div class="sl-soldout-eyebrow">${name}</div><div class="sl-soldout-title">${this.tf("picker.soldOutTitle", "Sold out")}</div><p class="sl-soldout-copy">${this.tf("picker.soldOutCopy", "Every seat is gone. Join the waitlist and we\u2019ll email you if seats are released.")}</p><button type="button" class="sl-soldout-btn" disabled>${this.tf("picker.waitlist", "Join waitlist")}</button>`;
|
|
4041
|
+
this.els.map.appendChild(el2);
|
|
4042
|
+
this.soldoutEl = el2;
|
|
3578
4043
|
}
|
|
3579
4044
|
/**
|
|
3580
4045
|
* Recompute the sold-out state on every price/availability sync. Sold-out ⇔
|
|
@@ -3631,10 +4096,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
3631
4096
|
if (this.badgeHidden(chartTheme)) return;
|
|
3632
4097
|
const foot = this.els.foot;
|
|
3633
4098
|
if (!foot) return;
|
|
3634
|
-
const
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
foot.appendChild(
|
|
4099
|
+
const el2 = document.createElement("div");
|
|
4100
|
+
el2.className = "sl-powered";
|
|
4101
|
+
el2.innerHTML = `<span class="sl-powered-mark" aria-hidden="true">` + SEATLAYER_ATTRIBUTION_MARK_SVG + `</span><span>${this.tf("picker.poweredBy", "Powered by SeatLayer")}</span>`;
|
|
4102
|
+
foot.appendChild(el2);
|
|
3638
4103
|
}
|
|
3639
4104
|
// ---- Feature 6: chrome anchor regions -------------------------------------
|
|
3640
4105
|
/**
|
|
@@ -3648,11 +4113,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
3648
4113
|
if (!this.els.map) return;
|
|
3649
4114
|
const REGIONS = ["top-left", "top-center", "top-right", "left-rail", "bottom-left", "bottom-center", "bottom-right"];
|
|
3650
4115
|
for (const region of REGIONS) {
|
|
3651
|
-
const
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
this.els.map.appendChild(
|
|
3655
|
-
this.regions[region] =
|
|
4116
|
+
const el2 = document.createElement("div");
|
|
4117
|
+
el2.className = "sl-anchor";
|
|
4118
|
+
el2.dataset.region = region;
|
|
4119
|
+
this.els.map.appendChild(el2);
|
|
4120
|
+
this.regions[region] = el2;
|
|
3656
4121
|
}
|
|
3657
4122
|
}
|
|
3658
4123
|
// ---- F3 minimap -----------------------------------------------------------
|
|
@@ -3672,12 +4137,12 @@ var SeatPicker = class _SeatPicker {
|
|
|
3672
4137
|
this.motionTimers.add(timer);
|
|
3673
4138
|
}
|
|
3674
4139
|
/** Restart one finite CSS animation without leaving a permanent state class. */
|
|
3675
|
-
animateOnce(
|
|
3676
|
-
if (!
|
|
3677
|
-
|
|
3678
|
-
void
|
|
3679
|
-
|
|
3680
|
-
this.scheduleMotion(() =>
|
|
4140
|
+
animateOnce(el2, className, duration = 600) {
|
|
4141
|
+
if (!el2 || this.reducedMotion()) return;
|
|
4142
|
+
el2.classList.remove(className);
|
|
4143
|
+
void el2.offsetWidth;
|
|
4144
|
+
el2.classList.add(className);
|
|
4145
|
+
this.scheduleMotion(() => el2.classList.remove(className), duration);
|
|
3681
4146
|
}
|
|
3682
4147
|
/** Selection feedback belongs on the selected seat, not across the whole map. */
|
|
3683
4148
|
flashPickedSeat(id) {
|
|
@@ -4331,13 +4796,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
4331
4796
|
const cat = this.controller.doc?.categories.find((candidate) => candidate.key === table.categoryKey);
|
|
4332
4797
|
const variable = table.bookingMode === "variable";
|
|
4333
4798
|
const typeWord = this.rowTypeWord(table);
|
|
4334
|
-
const
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
this.root.appendChild(
|
|
4338
|
-
this.tableDialogEl =
|
|
4799
|
+
const el2 = document.createElement("div");
|
|
4800
|
+
el2.className = "sl-table-scrim";
|
|
4801
|
+
el2.innerHTML = `<section class="sl-table-dialog" role="dialog" aria-modal="true" aria-labelledby="sl-table-title" aria-describedby="sl-table-copy"><div class="sl-table-head"><div class="sl-table-eyebrow">${esc3(variable ? `Flexible party \xB7 ${typeWord}` : `Whole ${typeWord}`)}</div><h2 class="sl-table-title" id="sl-table-title">${esc3(table.displayLabel ?? table.label)}</h2><p class="sl-table-copy" id="sl-table-copy">${variable ? `Choose how many guests will sit together. This table is held exclusively for your party.` : `All ${table.capacity} places are booked together as one exclusive table.`}</p></div><div class="sl-table-body"><div class="sl-table-summary"><span>${esc3(cat?.label ?? table.categoryKey)}</span><b data-table-unit>${this.money(this.paidPrice(table.categoryKey, table.tierId ?? null, table.price))} per guest</b><span class="muted">Table capacity</span><span>${table.capacity} guests</span><span class="muted">Total</span><b data-table-total></b></div>` + (variable ? `<label class="sl-table-qtylabel" id="sl-table-qty-label">Number of guests</label><div class="sl-table-stepper" role="group" aria-labelledby="sl-table-qty-label"><button type="button" data-table-step="-1" aria-label="Fewer guests">\u2212</button><output aria-live="polite" data-table-qty>${table.quantity}</output><button type="button" data-table-step="1" aria-label="More guests">+</button></div><div class="sl-table-range">Choose ${table.minOccupancy}\u2013${table.maxOccupancy} guests</div>` : `<input type="hidden" data-table-qty value="${table.capacity}">`) + `<div class="sl-table-actions"><button type="button" class="sl-table-cancel">Cancel</button><button type="button" class="sl-table-confirm">${held ? "Update table" : variable ? "Select table" : "Select whole table"}</button></div></div></section>`;
|
|
4802
|
+
this.root.appendChild(el2);
|
|
4803
|
+
this.tableDialogEl = el2;
|
|
4339
4804
|
this.renderTableDialogState();
|
|
4340
|
-
|
|
4805
|
+
el2.querySelectorAll("[data-table-step]").forEach((button) => {
|
|
4341
4806
|
button.addEventListener("click", () => {
|
|
4342
4807
|
if (!this.tableDialog) return;
|
|
4343
4808
|
const next = Math.max(
|
|
@@ -4348,14 +4813,14 @@ var SeatPicker = class _SeatPicker {
|
|
|
4348
4813
|
this.renderTableDialogState();
|
|
4349
4814
|
});
|
|
4350
4815
|
});
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
if (event.target ===
|
|
4816
|
+
el2.querySelector(".sl-table-cancel").addEventListener("click", () => this.cancelTableDialog());
|
|
4817
|
+
el2.querySelector(".sl-table-confirm").addEventListener("click", () => void this.confirmTableDialog());
|
|
4818
|
+
el2.addEventListener("mousedown", (event) => {
|
|
4819
|
+
if (event.target === el2) this.cancelTableDialog();
|
|
4355
4820
|
});
|
|
4356
|
-
|
|
4821
|
+
el2.addEventListener("keydown", (event) => {
|
|
4357
4822
|
if (event.key !== "Tab") return;
|
|
4358
|
-
const focusable = [...
|
|
4823
|
+
const focusable = [...el2.querySelectorAll('button:not(:disabled),[tabindex]:not([tabindex="-1"])')];
|
|
4359
4824
|
if (!focusable.length) return;
|
|
4360
4825
|
const first = focusable[0];
|
|
4361
4826
|
const last = focusable[focusable.length - 1];
|
|
@@ -4367,22 +4832,22 @@ var SeatPicker = class _SeatPicker {
|
|
|
4367
4832
|
first.focus();
|
|
4368
4833
|
}
|
|
4369
4834
|
});
|
|
4370
|
-
requestAnimationFrame(() =>
|
|
4835
|
+
requestAnimationFrame(() => el2.querySelector(variable ? '[data-table-step="-1"]' : ".sl-table-confirm")?.focus());
|
|
4371
4836
|
}
|
|
4372
4837
|
renderTableDialogState() {
|
|
4373
4838
|
const table = this.tableDialog;
|
|
4374
|
-
const
|
|
4375
|
-
if (!table || !
|
|
4376
|
-
const output =
|
|
4839
|
+
const el2 = this.tableDialogEl;
|
|
4840
|
+
if (!table || !el2) return;
|
|
4841
|
+
const output = el2.querySelector("output[data-table-qty]");
|
|
4377
4842
|
if (output) output.value = String(table.quantity);
|
|
4378
|
-
const hidden =
|
|
4843
|
+
const hidden = el2.querySelector("input[data-table-qty]");
|
|
4379
4844
|
if (hidden) hidden.value = String(table.quantity);
|
|
4380
|
-
|
|
4845
|
+
el2.querySelectorAll("[data-table-step]").forEach((button) => {
|
|
4381
4846
|
const delta = Number(button.dataset.tableStep);
|
|
4382
4847
|
button.disabled = delta < 0 ? table.quantity <= table.minOccupancy : table.quantity >= table.maxOccupancy;
|
|
4383
4848
|
});
|
|
4384
4849
|
const unit = this.paidPrice(table.categoryKey, table.tierId ?? null, table.price);
|
|
4385
|
-
const total =
|
|
4850
|
+
const total = el2.querySelector("[data-table-total]");
|
|
4386
4851
|
if (total) total.textContent = this.money(unit * table.quantity);
|
|
4387
4852
|
}
|
|
4388
4853
|
async confirmTableDialog() {
|
|
@@ -4478,18 +4943,18 @@ var SeatPicker = class _SeatPicker {
|
|
|
4478
4943
|
details?.rowLabel || details?.objectType === "booth" ? `<div class="sl-confirm-field"><span class="sl-confirm-key">${safe(this.rowTypeWord(details))}</span><span class="sl-confirm-value">${safe(this.rowShort(details) ?? details?.displayLabel ?? seat.displayLabel ?? seat.label)}</span></div>` : "",
|
|
4479
4944
|
details?.objectType !== "booth" ? `<div class="sl-confirm-field"><span class="sl-confirm-key">Seat</span><span class="sl-confirm-value">${safe(details?.seatNumber ?? details?.displayLabel ?? seat.displayLabel ?? seat.label)}</span></div>` : ""
|
|
4480
4945
|
].filter(Boolean).join("");
|
|
4481
|
-
const
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
this.els.map.appendChild(
|
|
4489
|
-
this.confirmEl =
|
|
4946
|
+
const el2 = document.createElement("div");
|
|
4947
|
+
el2.className = "sl-confirm";
|
|
4948
|
+
el2.setAttribute("role", "dialog");
|
|
4949
|
+
el2.setAttribute("aria-modal", "true");
|
|
4950
|
+
el2.setAttribute("aria-label", `Confirm seat ${seat.label}`);
|
|
4951
|
+
el2.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
|
|
4952
|
+
el2.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>`;
|
|
4953
|
+
this.els.map.appendChild(el2);
|
|
4954
|
+
this.confirmEl = el2;
|
|
4490
4955
|
this.reanchorConfirm();
|
|
4491
|
-
|
|
4492
|
-
|
|
4956
|
+
el2.querySelector(".sl-confirm-view")?.addEventListener("click", () => void this.openSeatView(seat));
|
|
4957
|
+
el2.querySelector(".sl-confirm-3d")?.addEventListener("click", () => {
|
|
4493
4958
|
if (this.buyerView === "venue3d") {
|
|
4494
4959
|
void this.view3dHandle?.flyToSeat(seat.id);
|
|
4495
4960
|
} else {
|
|
@@ -4498,9 +4963,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
4498
4963
|
void this.enter3d(seat.id);
|
|
4499
4964
|
}
|
|
4500
4965
|
});
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
requestAnimationFrame(() =>
|
|
4966
|
+
el2.querySelector(".sl-confirm-add").addEventListener("click", () => this.commitConfirm());
|
|
4967
|
+
el2.querySelector(".sl-confirm-cancel").addEventListener("click", () => this.cancelConfirm());
|
|
4968
|
+
requestAnimationFrame(() => el2.querySelector(".sl-confirm-add")?.focus());
|
|
4504
4969
|
}
|
|
4505
4970
|
reanchorConfirm() {
|
|
4506
4971
|
if (!this.confirmEl || !this.confirmSeat) return;
|
|
@@ -4559,10 +5024,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
4559
5024
|
* uploaded photo (seat.viewUrl) when present, else a panorama generated from
|
|
4560
5025
|
* the chart geometry — the stage placed at this seat's true bearing + size.
|
|
4561
5026
|
* Zero extra dependencies: an equirectangular image panned with `repeat-x`.
|
|
5027
|
+
*
|
|
5028
|
+
* Async only because the generator is a lazy chunk (see `loadPanorama`); an
|
|
5029
|
+
* organizer photo needs no generator and never waits on it. The two callers
|
|
5030
|
+
* are click handlers, so nothing observes the promise.
|
|
4562
5031
|
*/
|
|
4563
|
-
openSeatView(seat) {
|
|
5032
|
+
async openSeatView(seat) {
|
|
4564
5033
|
if (!this.root || !this.seatViewEnabled()) return;
|
|
4565
|
-
this.closeSeatView();
|
|
4566
5034
|
const doc = this.controller.doc;
|
|
4567
5035
|
const activeId = this.controller.getActiveFloorId();
|
|
4568
5036
|
const focal = seat.focalPoint ?? doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
|
|
@@ -4574,18 +5042,27 @@ var SeatPicker = class _SeatPicker {
|
|
|
4574
5042
|
caption = (0, import_core2.t)("picker.panorama360");
|
|
4575
5043
|
real = true;
|
|
4576
5044
|
} else {
|
|
4577
|
-
|
|
5045
|
+
let pano2;
|
|
5046
|
+
try {
|
|
5047
|
+
const { generateSeatPanorama } = await loadPanorama();
|
|
5048
|
+
pano2 = generateSeatPanorama(seat, focal, this.allSeats());
|
|
5049
|
+
} catch (err) {
|
|
5050
|
+
this.opts.onError?.(err);
|
|
5051
|
+
return;
|
|
5052
|
+
}
|
|
5053
|
+
if (!this.root || !this.seatViewEnabled()) return;
|
|
4578
5054
|
panoUrl = pano2.url;
|
|
4579
5055
|
caption = (0, import_core2.t)("picker.illustrationCaption", { m: pano2.distanceM });
|
|
4580
5056
|
}
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
this.
|
|
4588
|
-
|
|
5057
|
+
this.closeSeatView();
|
|
5058
|
+
const el2 = document.createElement("div");
|
|
5059
|
+
el2.className = "sl-view";
|
|
5060
|
+
el2.setAttribute("role", "dialog");
|
|
5061
|
+
el2.setAttribute("aria-label", (0, import_core2.t)("picker.viewFromSeat", { label: seat.label }));
|
|
5062
|
+
el2.innerHTML = `<div class="sl-view-head"><span class="sl-view-title">${(0, import_core2.t)("picker.viewFromSeat", { label: seat.label })}</span><span class="sl-view-cap">${caption}</span><button type="button" class="sl-view-x" aria-label="Close"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div><div class="sl-view-pano"><span class="sl-view-badge">${real ? (0, import_core2.t)("picker.real360") : (0, import_core2.t)("picker.preview")}</span><span class="sl-view-hint">Drag to look around \xB7 scroll to zoom</span></div>`;
|
|
5063
|
+
this.root.appendChild(el2);
|
|
5064
|
+
this.viewEl = el2;
|
|
5065
|
+
const pano = el2.querySelector(".sl-view-pano");
|
|
4589
5066
|
pano.style.backgroundImage = `url("${panoUrl}")`;
|
|
4590
5067
|
const VFOV_DEG = 70;
|
|
4591
5068
|
const MAX_PITCH_DEG = 35;
|
|
@@ -4637,7 +5114,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
4637
5114
|
pano.addEventListener("pointerup", onUp);
|
|
4638
5115
|
pano.addEventListener("pointercancel", onUp);
|
|
4639
5116
|
pano.addEventListener("wheel", onWheel, { passive: false });
|
|
4640
|
-
const closeBtn =
|
|
5117
|
+
const closeBtn = el2.querySelector(".sl-view-x");
|
|
4641
5118
|
closeBtn.addEventListener("click", () => this.closeSeatView());
|
|
4642
5119
|
const onKey = (e) => {
|
|
4643
5120
|
if (e.key === "Escape") {
|
|
@@ -4645,7 +5122,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
4645
5122
|
this.closeSeatView();
|
|
4646
5123
|
}
|
|
4647
5124
|
};
|
|
4648
|
-
|
|
5125
|
+
el2.addEventListener("keydown", onKey);
|
|
4649
5126
|
closeBtn.focus();
|
|
4650
5127
|
this.viewCleanup = () => {
|
|
4651
5128
|
pano.removeEventListener("pointerdown", onDown);
|
|
@@ -4653,7 +5130,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
4653
5130
|
pano.removeEventListener("pointerup", onUp);
|
|
4654
5131
|
pano.removeEventListener("pointercancel", onUp);
|
|
4655
5132
|
pano.removeEventListener("wheel", onWheel);
|
|
4656
|
-
|
|
5133
|
+
el2.removeEventListener("keydown", onKey);
|
|
4657
5134
|
};
|
|
4658
5135
|
}
|
|
4659
5136
|
closeSeatView() {
|
|
@@ -4943,7 +5420,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
4943
5420
|
this.els.tray.querySelectorAll(".sl-chip .view[data-view-label]").forEach((btn) => {
|
|
4944
5421
|
btn.addEventListener("click", () => {
|
|
4945
5422
|
const seat = this.controller.seatByLabel(btn.dataset.viewLabel);
|
|
4946
|
-
if (seat) this.openSeatView(seat);
|
|
5423
|
+
if (seat) void this.openSeatView(seat);
|
|
4947
5424
|
});
|
|
4948
5425
|
});
|
|
4949
5426
|
this.els.tray.querySelectorAll(".sl-chip[data-locate]").forEach((chip) => {
|
|
@@ -4964,8 +5441,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
4964
5441
|
});
|
|
4965
5442
|
});
|
|
4966
5443
|
if (this.salesClosed) {
|
|
4967
|
-
this.els.tray.querySelectorAll(".sl-ba-go,[data-ba],[data-ba-cat],[data-ba-zone],[data-ba-replace],.sl-ga button").forEach((
|
|
4968
|
-
|
|
5444
|
+
this.els.tray.querySelectorAll(".sl-ba-go,[data-ba],[data-ba-cat],[data-ba-zone],[data-ba-replace],.sl-ga button").forEach((el2) => {
|
|
5445
|
+
el2.disabled = true;
|
|
4969
5446
|
});
|
|
4970
5447
|
}
|
|
4971
5448
|
const gaTotal = this.pendingGATotal(gaAreas);
|
|
@@ -5086,7 +5563,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
5086
5563
|
const seats = this.hold.seats ?? committed;
|
|
5087
5564
|
this.handedOff = true;
|
|
5088
5565
|
this.setCtaPhase("checkout");
|
|
5089
|
-
this.
|
|
5566
|
+
this.checkoutHandoff(this.hold, seats);
|
|
5090
5567
|
return;
|
|
5091
5568
|
}
|
|
5092
5569
|
this.holdingLabels = new Set(committed.map((seat) => seat.label));
|
|
@@ -5115,7 +5592,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
5115
5592
|
this.flashHeldSeats(hold);
|
|
5116
5593
|
this.setCtaPhase("checkout");
|
|
5117
5594
|
this.emitHoldChange();
|
|
5118
|
-
this.
|
|
5595
|
+
this.checkoutHandoff(hold, hold.seats ?? chosenSeats);
|
|
5119
5596
|
} catch (err) {
|
|
5120
5597
|
this.opts.onError?.(err);
|
|
5121
5598
|
const problem = err;
|
|
@@ -5220,6 +5697,109 @@ var SeatPicker = class _SeatPicker {
|
|
|
5220
5697
|
this.bookedEl?.classList.add("on");
|
|
5221
5698
|
this.opts.onBooked?.(handoff);
|
|
5222
5699
|
}
|
|
5700
|
+
/**
|
|
5701
|
+
* The seats are held. Send the buyer wherever this picker's `checkout` option
|
|
5702
|
+
* says they go.
|
|
5703
|
+
*
|
|
5704
|
+
* The default branch is the literal call that stood here before hosted
|
|
5705
|
+
* checkout existed, unchanged, so nothing about an existing integration moves.
|
|
5706
|
+
*/
|
|
5707
|
+
checkoutHandoff(hold, seats) {
|
|
5708
|
+
if (this.checkoutMode === "hosted") {
|
|
5709
|
+
void this.startHostedCheckout(hold, seats);
|
|
5710
|
+
return;
|
|
5711
|
+
}
|
|
5712
|
+
this.opts.onCheckout?.(hold, seats, this.buildHandoff(hold));
|
|
5713
|
+
}
|
|
5714
|
+
/**
|
|
5715
|
+
* Take the money ourselves, through the organizer's own gateway.
|
|
5716
|
+
*
|
|
5717
|
+
* Order of operations matters: ASK FIRST, load second. `payment-options` is
|
|
5718
|
+
* already in flight from render, and its answer decides whether any payment
|
|
5719
|
+
* code is fetched at all — an event that cannot charge never downloads the
|
|
5720
|
+
* card that would have charged it.
|
|
5721
|
+
*
|
|
5722
|
+
* An empty list is not a failure and never dead-ends the buyer. It routes them
|
|
5723
|
+
* to whatever the host has: `onCheckoutUnavailable` (with the server's reason,
|
|
5724
|
+
* so the host can say the right one of three very different sentences), then
|
|
5725
|
+
* `onCheckout` with the ordinary handoff. A host that supplied neither gets
|
|
5726
|
+
* the widget's own honest card instead of a press that did nothing.
|
|
5727
|
+
*/
|
|
5728
|
+
async startHostedCheckout(hold, seats) {
|
|
5729
|
+
const handoff = this.buildHandoff(hold);
|
|
5730
|
+
let options = null;
|
|
5731
|
+
try {
|
|
5732
|
+
options = await (this.paymentOptions ??= this.pubApi.paymentOptions(this.opts.event));
|
|
5733
|
+
} catch (err) {
|
|
5734
|
+
this.opts.onError?.(err);
|
|
5735
|
+
}
|
|
5736
|
+
if (this.destroyed) return;
|
|
5737
|
+
const provider = options?.providers?.[0];
|
|
5738
|
+
if (!provider) {
|
|
5739
|
+
const reason = paymentsOffReason(options?.reason);
|
|
5740
|
+
const handled = !!this.opts.onCheckoutUnavailable || !!this.opts.onCheckout;
|
|
5741
|
+
this.opts.onCheckoutUnavailable?.({ reason, handoff });
|
|
5742
|
+
this.opts.onCheckout?.(hold, seats, handoff);
|
|
5743
|
+
if (!handled) {
|
|
5744
|
+
void this.openCheckoutPanel({
|
|
5745
|
+
kind: "unavailable",
|
|
5746
|
+
reason,
|
|
5747
|
+
seatCount: handoff.lineItems.reduce((sum, item) => sum + item.quantity, 0)
|
|
5748
|
+
});
|
|
5749
|
+
}
|
|
5750
|
+
return;
|
|
5751
|
+
}
|
|
5752
|
+
await this.openCheckoutPanel({
|
|
5753
|
+
kind: "pay",
|
|
5754
|
+
provider,
|
|
5755
|
+
order: {
|
|
5756
|
+
holdId: handoff.holdId,
|
|
5757
|
+
expiresAt: handoff.expiresAt,
|
|
5758
|
+
currency: handoff.currency,
|
|
5759
|
+
total: handoff.total,
|
|
5760
|
+
labels: handoff.lineItems.map((item) => item.displayLabel ?? item.label)
|
|
5761
|
+
}
|
|
5762
|
+
});
|
|
5763
|
+
}
|
|
5764
|
+
/**
|
|
5765
|
+
* Fetch the checkout chunk and put its card over the map.
|
|
5766
|
+
*
|
|
5767
|
+
* Every failure here lands the buyer back on a map with their seats still
|
|
5768
|
+
* held, which is a place they can act from — a blocked chunk request must not
|
|
5769
|
+
* leave them staring at a CTA that no longer does anything.
|
|
5770
|
+
*/
|
|
5771
|
+
async openCheckoutPanel(state) {
|
|
5772
|
+
let mountCheckout2;
|
|
5773
|
+
try {
|
|
5774
|
+
({ mountCheckout: mountCheckout2 } = await loadHostedCheckout());
|
|
5775
|
+
} catch (err) {
|
|
5776
|
+
this.opts.onError?.(err);
|
|
5777
|
+
this.toast("Checkout could not be opened. Your seats are still held \u2014 please try again.", "error");
|
|
5778
|
+
this.setCtaPhase("idle");
|
|
5779
|
+
return;
|
|
5780
|
+
}
|
|
5781
|
+
if (this.destroyed || !this.root) return;
|
|
5782
|
+
this.closeCheckoutPanel();
|
|
5783
|
+
this.checkoutPanel = mountCheckout2({
|
|
5784
|
+
root: this.root,
|
|
5785
|
+
state,
|
|
5786
|
+
startSession: (input) => this.pubApi.startCheckout(this.opts.event, input),
|
|
5787
|
+
orderStatus: (orderId) => this.pubApi.orderStatus(orderId),
|
|
5788
|
+
onCancel: () => {
|
|
5789
|
+
this.checkoutPanel = null;
|
|
5790
|
+
if (!this.hold) this.setCtaPhase("idle");
|
|
5791
|
+
},
|
|
5792
|
+
onConfirmed: (order) => {
|
|
5793
|
+
this.opts.onOrderConfirmed?.(order);
|
|
5794
|
+
void this.controller.refresh();
|
|
5795
|
+
},
|
|
5796
|
+
onError: (err) => this.opts.onError?.(err)
|
|
5797
|
+
});
|
|
5798
|
+
}
|
|
5799
|
+
closeCheckoutPanel() {
|
|
5800
|
+
this.checkoutPanel?.destroy();
|
|
5801
|
+
this.checkoutPanel = null;
|
|
5802
|
+
}
|
|
5223
5803
|
/** Assemble the stable {@link CheckoutHandoff} from a hold's server line items. */
|
|
5224
5804
|
buildHandoff(hold) {
|
|
5225
5805
|
const items = hold.items ?? [];
|
|
@@ -5251,28 +5831,28 @@ var SeatPicker = class _SeatPicker {
|
|
|
5251
5831
|
);
|
|
5252
5832
|
}
|
|
5253
5833
|
toast(msg, tone = "neutral", action) {
|
|
5254
|
-
const
|
|
5255
|
-
if (!
|
|
5256
|
-
|
|
5834
|
+
const el2 = this.els.toast;
|
|
5835
|
+
if (!el2) return;
|
|
5836
|
+
el2.replaceChildren();
|
|
5257
5837
|
const copy = document.createElement("span");
|
|
5258
5838
|
copy.textContent = msg;
|
|
5259
|
-
|
|
5260
|
-
|
|
5839
|
+
el2.appendChild(copy);
|
|
5840
|
+
el2.classList.toggle("has-action", !!action);
|
|
5261
5841
|
if (action) {
|
|
5262
5842
|
const button = document.createElement("button");
|
|
5263
5843
|
button.type = "button";
|
|
5264
5844
|
button.className = "sl-toast-action";
|
|
5265
5845
|
button.textContent = action.label;
|
|
5266
5846
|
button.addEventListener("click", action.onClick, { once: true });
|
|
5267
|
-
|
|
5847
|
+
el2.appendChild(button);
|
|
5268
5848
|
}
|
|
5269
|
-
|
|
5270
|
-
|
|
5849
|
+
el2.dataset.tone = tone;
|
|
5850
|
+
el2.classList.add("on");
|
|
5271
5851
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
5272
5852
|
this.toastTimer = setTimeout(() => {
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5853
|
+
el2.classList.remove("on");
|
|
5854
|
+
el2.classList.remove("has-action");
|
|
5855
|
+
el2.dataset.tone = "neutral";
|
|
5276
5856
|
}, 4200);
|
|
5277
5857
|
}
|
|
5278
5858
|
placeTooltip() {
|
|
@@ -5454,7 +6034,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
5454
6034
|
}
|
|
5455
6035
|
/** Build the view-from-seat panorama the cinematic dissolves into — reuses the
|
|
5456
6036
|
* exact input path as the 2D `openSeatView` (organizer photo, else generated). */
|
|
5457
|
-
seatViewFor3d(seatId) {
|
|
6037
|
+
async seatViewFor3d(seatId) {
|
|
5458
6038
|
const seat = this.allSeats().find((s) => s.id === seatId);
|
|
5459
6039
|
if (!seat) return null;
|
|
5460
6040
|
if (seat.viewUrl) return { url: seat.viewUrl };
|
|
@@ -5463,7 +6043,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
5463
6043
|
const activeId = this.controller.getActiveFloorId();
|
|
5464
6044
|
const focal = seat.focalPoint ?? doc.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc.focalPoint ?? { x: 0, y: 0 };
|
|
5465
6045
|
try {
|
|
5466
|
-
|
|
6046
|
+
const { generateSeatPanorama } = await loadPanorama();
|
|
6047
|
+
return { url: generateSeatPanorama(seat, focal, this.allSeats()).url };
|
|
5467
6048
|
} catch {
|
|
5468
6049
|
return null;
|
|
5469
6050
|
}
|
|
@@ -5602,9 +6183,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
5602
6183
|
// in orbit instead of dissolving into an empty overlay.
|
|
5603
6184
|
getSeatView: (id) => new Promise((resolve, reject) => {
|
|
5604
6185
|
const run = () => {
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
6186
|
+
void this.seatViewFor3d(id).then((view) => {
|
|
6187
|
+
if (view) resolve(view);
|
|
6188
|
+
else reject(new Error("seat_view_unavailable"));
|
|
6189
|
+
});
|
|
5608
6190
|
};
|
|
5609
6191
|
const ric = globalThis.requestIdleCallback;
|
|
5610
6192
|
if (typeof ric === "function") ric(run, { timeout: 1500 });
|
|
@@ -5882,6 +6464,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
5882
6464
|
this.closeConfirm();
|
|
5883
6465
|
this.dismissTableDialog(false);
|
|
5884
6466
|
this.closeSeatView();
|
|
6467
|
+
this.closeCheckoutPanel();
|
|
5885
6468
|
this.exit3d();
|
|
5886
6469
|
this.stopHoldTimer();
|
|
5887
6470
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
@@ -6598,7 +7181,14 @@ var ManageApi = class {
|
|
|
6598
7181
|
var POLL_MS = 1e4;
|
|
6599
7182
|
var MAX_FLAGS = 8;
|
|
6600
7183
|
var SEAT_LIST_PAGE = 300;
|
|
6601
|
-
var
|
|
7184
|
+
var PREVIEW_ELIGIBLE_FILL = "#6e7bff";
|
|
7185
|
+
var PREVIEW_ELIGIBLE_STROKE = "#b9c0ff";
|
|
7186
|
+
var PREVIEW_UNAVAILABLE_FILL = "#303846";
|
|
7187
|
+
var PREVIEW_UNAVAILABLE_STROKE = "#4b5669";
|
|
7188
|
+
var ALLOCATION_STROKE = "#101723";
|
|
7189
|
+
var CHANNELS_CSS = (
|
|
7190
|
+
/* @sl-css */
|
|
7191
|
+
`
|
|
6602
7192
|
.slm{--slm-mo-instant:80ms;--slm-mo-quick:140ms;--slm-mo-base:200ms;--slm-mo-slow:320ms;--slm-mo-ambient:2000ms;
|
|
6603
7193
|
--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);
|
|
6604
7194
|
--slm-mo-spring:cubic-bezier(.34,1.3,.64,1)}
|
|
@@ -6611,6 +7201,8 @@ var CHANNELS_CSS = `
|
|
|
6611
7201
|
background:rgba(14,16,23,.88);border:1px solid var(--slm-line);font-size:10px;font-weight:800;letter-spacing:.04em;
|
|
6612
7202
|
transform:translate(-50%,-50%);white-space:nowrap}
|
|
6613
7203
|
.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}
|
|
7204
|
+
.slm-ch-section-target{position:absolute;pointer-events:auto;padding:0;border:0;border-radius:8px;background:transparent;cursor:zoom-in}
|
|
7205
|
+
.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)}
|
|
6614
7206
|
|
|
6615
7207
|
/* preview banner \u2014 raised with the organizer chrome dim, as one transition */
|
|
6616
7208
|
.slm-ch-banner{position:absolute;left:50%;top:14px;z-index:6;display:flex;align-items:center;gap:9px;padding:8px 14px;
|
|
@@ -6630,7 +7222,7 @@ var CHANNELS_CSS = `
|
|
|
6630
7222
|
transition:transform var(--slm-mo-slow) var(--slm-mo-out),opacity var(--slm-mo-base) var(--slm-mo-out)}
|
|
6631
7223
|
.slm-ch-staged.on{transform:none;opacity:1}
|
|
6632
7224
|
.slm-ch-staged.done{background:rgba(31,122,77,.96);border-color:#1f7a4d}
|
|
6633
|
-
.slm-ch-staged.shake{animation:slm-ch-shake
|
|
7225
|
+
.slm-ch-staged.shake{animation:slm-ch-shake var(--slm-mo-slow) var(--slm-mo-in-out) 2}
|
|
6634
7226
|
.slm-ch-staged b{font-variant-numeric:tabular-nums}
|
|
6635
7227
|
.slm-ch-staged .grow{flex:1}
|
|
6636
7228
|
.slm-ch-staged .go{padding:9px 16px;min-height:44px;display:inline-flex;align-items:center;border-radius:9px;
|
|
@@ -6647,6 +7239,11 @@ var CHANNELS_CSS = `
|
|
|
6647
7239
|
.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)}
|
|
6648
7240
|
.slm-ch-viewseg button.on{background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
6649
7241
|
.slm-ch-viewseg button:disabled{opacity:.5;cursor:not-allowed}
|
|
7242
|
+
.slm-ch-mapnav{margin:-2px 0 12px;padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
|
|
7243
|
+
.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)}
|
|
7244
|
+
.slm-ch-mapnav-head button{color:var(--slm-accent);font-size:11px;font-weight:800;letter-spacing:0;text-transform:none;min-height:30px}
|
|
7245
|
+
.slm-ch-mapnav .slm-ch-viewseg{margin:8px 0 5px}
|
|
7246
|
+
.slm-ch-mapnav p{margin:0;font-size:11px;line-height:1.45;color:var(--slm-muted)}
|
|
6650
7247
|
.slm-ch-list{display:flex;flex-direction:column;gap:8px;margin-bottom:12px}
|
|
6651
7248
|
.slm-ch-row{padding:10px 11px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
6652
7249
|
text-align:left;width:100%;display:block;transition:border-color var(--slm-mo-quick) var(--slm-mo-out)}
|
|
@@ -6667,7 +7264,7 @@ var CHANNELS_CSS = `
|
|
|
6667
7264
|
font-variant-numeric:tabular-nums}
|
|
6668
7265
|
.slm-ch-counts b{color:var(--slm-text);font-weight:800}
|
|
6669
7266
|
.slm-ch-counts .free b{color:#5bd39b}
|
|
6670
|
-
.slm-ch-counts b.bump{animation:slm-ch-bump
|
|
7267
|
+
.slm-ch-counts b.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
6671
7268
|
@keyframes slm-ch-bump{0%,100%{transform:none}35%{transform:translateY(-2px) scale(1.08)}}
|
|
6672
7269
|
.slm-ch-access{margin-top:6px;font-size:10.5px;color:var(--slm-muted)}
|
|
6673
7270
|
.slm-ch-more{flex:none;color:var(--slm-muted);font-weight:800;padding:0 4px;min-height:28px}
|
|
@@ -6677,12 +7274,13 @@ var CHANNELS_CSS = `
|
|
|
6677
7274
|
font-weight:800;color:#0e1017}
|
|
6678
7275
|
.slm-ch-selsrc-row b{min-width:30px;text-align:right;font-weight:800}
|
|
6679
7276
|
.slm-ch-selsrc-row span{color:var(--slm-muted)}
|
|
6680
|
-
.slm-ch-selnum.bump{animation:slm-ch-bump
|
|
7277
|
+
.slm-ch-selnum.bump{animation:slm-ch-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
6681
7278
|
.slm-ch-row2{display:flex;gap:8px;margin-top:8px}
|
|
6682
7279
|
.slm-ch-row2 .slm-btn{flex:1;min-width:0}
|
|
6683
7280
|
.slm-ch-alert{display:flex;align-items:flex-start;gap:9px;padding:11px 13px;border-radius:10px;font-size:12.5px;
|
|
6684
7281
|
line-height:1.5;margin-bottom:12px}
|
|
6685
7282
|
.slm-ch-alert.warn{background:rgba(244,183,64,.1);border:1px solid rgba(244,183,64,.4);color:#f4d58a}
|
|
7283
|
+
.slm-ch-alert.info{background:rgba(110,123,255,.12);border:1px solid rgba(110,123,255,.44);color:#c5cbff}
|
|
6686
7284
|
.slm-ch-alert.err{background:rgba(229,72,77,.1);border:1px solid rgba(229,72,77,.45);color:#f1a4a6}
|
|
6687
7285
|
.slm-ch-alert b{color:#fff}
|
|
6688
7286
|
.slm-ch-alert button{display:block;margin-top:6px;color:#fff;font-weight:800;min-height:36px}
|
|
@@ -6779,7 +7377,8 @@ var CHANNELS_CSS = `
|
|
|
6779
7377
|
.slm-ch-staged.shake{outline:2px solid #e5484d;outline-offset:2px}
|
|
6780
7378
|
.slm-ch-staged.done{outline:2px solid #5bd39b;outline-offset:2px}
|
|
6781
7379
|
}
|
|
6782
|
-
|
|
7380
|
+
`
|
|
7381
|
+
);
|
|
6783
7382
|
function bucketRowsHtml(rows) {
|
|
6784
7383
|
return rows.map((row, index) => `
|
|
6785
7384
|
<div class="slm-ch-bucket" style="animation-delay:${Math.min(index, 4) * 30}ms">
|
|
@@ -6829,6 +7428,10 @@ var ChannelsMode = class {
|
|
|
6829
7428
|
this.loadError = null;
|
|
6830
7429
|
this.loading = true;
|
|
6831
7430
|
this.view = "inspect";
|
|
7431
|
+
/** Pan is intentionally the initial desktop interaction. Assignment's
|
|
7432
|
+
* marquee is powerful, but must never make an organizer lose map navigation. */
|
|
7433
|
+
this.mapIntent = "pan";
|
|
7434
|
+
this.focusedSectionId = null;
|
|
6832
7435
|
this.showArchived = false;
|
|
6833
7436
|
this.detailChannelId = null;
|
|
6834
7437
|
this.targetChannelId = "";
|
|
@@ -6845,6 +7448,15 @@ var ChannelsMode = class {
|
|
|
6845
7448
|
this.links = [];
|
|
6846
7449
|
this.linksChannelId = null;
|
|
6847
7450
|
this.linksState = "idle";
|
|
7451
|
+
/**
|
|
7452
|
+
* Monotonic read generations — one for the channel list + allocation, one for
|
|
7453
|
+
* the open channel's links. Reads are concurrent (a 10s poll versus a
|
|
7454
|
+
* mutation's own reload), and the network does not promise to answer them in
|
|
7455
|
+
* order. Only the NEWEST read of each kind may write to state; an older
|
|
7456
|
+
* answer that arrives late is dropped, never painted.
|
|
7457
|
+
*/
|
|
7458
|
+
this.listSeq = 0;
|
|
7459
|
+
this.linksSeq = 0;
|
|
6848
7460
|
this.previewAudience = [];
|
|
6849
7461
|
this.previewIncludePublic = false;
|
|
6850
7462
|
this.previewProjection = null;
|
|
@@ -6871,10 +7483,14 @@ var ChannelsMode = class {
|
|
|
6871
7483
|
enter() {
|
|
6872
7484
|
if (this.active) return;
|
|
6873
7485
|
this.active = true;
|
|
7486
|
+
this.mapIntent = "pan";
|
|
7487
|
+
this.focusedSectionId = null;
|
|
6874
7488
|
this.ensureLayer();
|
|
6875
7489
|
this.host.root.classList.add("ch-mode");
|
|
6876
7490
|
this.applySheetClasses();
|
|
7491
|
+
if (this.host.sections().length > 1) this.host.showSectionOverview();
|
|
6877
7492
|
this.paintRail();
|
|
7493
|
+
this.onInteractionChange?.();
|
|
6878
7494
|
void this.refresh();
|
|
6879
7495
|
this.pollTimer = setInterval(() => {
|
|
6880
7496
|
void this.refresh({ quiet: true });
|
|
@@ -6926,6 +7542,17 @@ var ChannelsMode = class {
|
|
|
6926
7542
|
canSelect() {
|
|
6927
7543
|
return this.caps.manage && this.view === "inspect";
|
|
6928
7544
|
}
|
|
7545
|
+
/** Bulk seat assignment is explicit. In Pan map, clicks can still inspect a
|
|
7546
|
+
* single seat, while a primary-button drag always moves the camera. */
|
|
7547
|
+
usesMarqueeSelection() {
|
|
7548
|
+
return this.canSelect() && this.mapIntent === "assign";
|
|
7549
|
+
}
|
|
7550
|
+
/** The renderer calls this when the organizer opens a section from overview. */
|
|
7551
|
+
handleSectionFocus(sectionId) {
|
|
7552
|
+
if (!this.active) return;
|
|
7553
|
+
this.focusedSectionId = sectionId;
|
|
7554
|
+
this.paintRail();
|
|
7555
|
+
}
|
|
6929
7556
|
/**
|
|
6930
7557
|
* Organizer realtime integration point. M5 ships a per-scope socket for
|
|
6931
7558
|
* buyers; the organizer channel-count stream is a later milestone. When it
|
|
@@ -6953,22 +7580,28 @@ var ChannelsMode = class {
|
|
|
6953
7580
|
// ---- data -----------------------------------------------------------------
|
|
6954
7581
|
async refresh(opts = {}) {
|
|
6955
7582
|
if (!this.caps.view) return;
|
|
7583
|
+
const seq = ++this.listSeq;
|
|
7584
|
+
const superseded = () => seq !== this.listSeq;
|
|
6956
7585
|
try {
|
|
6957
7586
|
const list = await this.host.api.channels(this.host.eventKey, { includeArchived: this.showArchived });
|
|
7587
|
+
if (superseded()) return;
|
|
6958
7588
|
this.list = list;
|
|
6959
7589
|
this.assignmentVersion = list.assignmentVersion;
|
|
6960
7590
|
this.loadError = null;
|
|
6961
7591
|
if (!this.targetChannelId) {
|
|
6962
7592
|
this.targetChannelId = list.channels.find((c) => c.state === "active")?.id ?? PUBLIC_CHANNEL_ID;
|
|
6963
7593
|
}
|
|
6964
|
-
await this.loadAllocation();
|
|
7594
|
+
await this.loadAllocation(seq);
|
|
7595
|
+
if (superseded()) return;
|
|
6965
7596
|
if (this.detailChannelId) await this.loadLinks(this.detailChannelId);
|
|
7597
|
+
if (superseded()) return;
|
|
6966
7598
|
this.loading = false;
|
|
6967
7599
|
if (this.active) {
|
|
6968
7600
|
this.paintRail();
|
|
6969
7601
|
this.paintOverlay();
|
|
6970
7602
|
}
|
|
6971
7603
|
} catch (err) {
|
|
7604
|
+
if (superseded()) return;
|
|
6972
7605
|
this.loading = false;
|
|
6973
7606
|
if (err instanceof ManageApiError && err.status === 403) {
|
|
6974
7607
|
this.caps = { view: false, manage: false };
|
|
@@ -6980,10 +7613,11 @@ var ChannelsMode = class {
|
|
|
6980
7613
|
}
|
|
6981
7614
|
/** Walk every allocation page. Bounded by the event's seat count, and the
|
|
6982
7615
|
* server caps each page, so an arena is a handful of round trips. */
|
|
6983
|
-
async loadAllocation() {
|
|
7616
|
+
async loadAllocation(seq) {
|
|
6984
7617
|
const next = /* @__PURE__ */ new Map();
|
|
6985
7618
|
let afterLabel;
|
|
6986
7619
|
for (let page = 0; page < 200; page += 1) {
|
|
7620
|
+
if (seq !== void 0 && seq !== this.listSeq) return;
|
|
6987
7621
|
const res = await this.host.api.channelAllocation(this.host.eventKey, {
|
|
6988
7622
|
afterLabel,
|
|
6989
7623
|
limit: 1e3
|
|
@@ -6995,6 +7629,7 @@ var ChannelsMode = class {
|
|
|
6995
7629
|
if (!res.nextAfterLabel) break;
|
|
6996
7630
|
afterLabel = res.nextAfterLabel;
|
|
6997
7631
|
}
|
|
7632
|
+
if (seq !== void 0 && seq !== this.listSeq) return;
|
|
6998
7633
|
this.allocation = next;
|
|
6999
7634
|
}
|
|
7000
7635
|
// ---- lookups --------------------------------------------------------------
|
|
@@ -7061,9 +7696,10 @@ var ChannelsMode = class {
|
|
|
7061
7696
|
* Repaint the allocation (or preview) overlay in ONE canvas pass.
|
|
7062
7697
|
*
|
|
7063
7698
|
* Channel identity on the map is a fill in the administrative color PLUS the
|
|
7064
|
-
* letter flags below — never color alone.
|
|
7065
|
-
*
|
|
7066
|
-
*
|
|
7699
|
+
* letter flags below — never color alone. In buyer preview the map instead
|
|
7700
|
+
* uses two explicit, channel-neutral access states. Physical status keeps its
|
|
7701
|
+
* own cue: only FREE units are repainted, so sold/held/blocked seats still
|
|
7702
|
+
* read exactly as they do in every other tool.
|
|
7067
7703
|
*/
|
|
7068
7704
|
paintOverlay() {
|
|
7069
7705
|
const canvas = this.canvas;
|
|
@@ -7088,11 +7724,14 @@ var ChannelsMode = class {
|
|
|
7088
7724
|
if (!ctx) return;
|
|
7089
7725
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
7090
7726
|
ctx.clearRect(0, 0, width, height);
|
|
7727
|
+
const seatDetail = this.host.isSeatDetail();
|
|
7091
7728
|
const size = Math.max(3, this.host.seatPixelSize());
|
|
7092
7729
|
const half = size / 2;
|
|
7093
7730
|
const projection = this.view === "preview" ? this.previewProjection : null;
|
|
7094
7731
|
const eligible = projection ? new Set(projection.available === false ? [] : projection.eligible ?? []) : null;
|
|
7095
7732
|
const clusters = /* @__PURE__ */ new Map();
|
|
7733
|
+
const sectionTargets = !seatDetail && this.host.sections().length > 1 ? /* @__PURE__ */ new Map() : null;
|
|
7734
|
+
const previewSections = this.view === "preview" && seatDetail && size <= 15 ? /* @__PURE__ */ new Map() : null;
|
|
7096
7735
|
for (const seat of this.host.seats()) {
|
|
7097
7736
|
const status = this.host.statusOf(seat.label) ?? "free";
|
|
7098
7737
|
const channelId = this.allocation.get(seat.label) ?? PUBLIC_CHANNEL_ID;
|
|
@@ -7103,29 +7742,155 @@ var ChannelsMode = class {
|
|
|
7103
7742
|
cluster.n += 1;
|
|
7104
7743
|
clusters.set(channelId, cluster);
|
|
7105
7744
|
}
|
|
7745
|
+
if (!seatDetail) {
|
|
7746
|
+
const section = this.host.sectionOfLabel(seat.label);
|
|
7747
|
+
const point2 = section ? this.host.worldToScreen({ x: seat.x, y: seat.y }) : null;
|
|
7748
|
+
if (sectionTargets && section && point2) {
|
|
7749
|
+
const bounds = sectionTargets.get(section.id) ?? {
|
|
7750
|
+
label: section.label,
|
|
7751
|
+
minX: point2.x,
|
|
7752
|
+
minY: point2.y,
|
|
7753
|
+
maxX: point2.x,
|
|
7754
|
+
maxY: point2.y
|
|
7755
|
+
};
|
|
7756
|
+
bounds.minX = Math.min(bounds.minX, point2.x);
|
|
7757
|
+
bounds.minY = Math.min(bounds.minY, point2.y);
|
|
7758
|
+
bounds.maxX = Math.max(bounds.maxX, point2.x);
|
|
7759
|
+
bounds.maxY = Math.max(bounds.maxY, point2.y);
|
|
7760
|
+
sectionTargets.set(section.id, bounds);
|
|
7761
|
+
}
|
|
7762
|
+
continue;
|
|
7763
|
+
}
|
|
7106
7764
|
if (status !== "free") continue;
|
|
7107
7765
|
let fill = null;
|
|
7766
|
+
let stroke = null;
|
|
7108
7767
|
if (this.view === "preview") {
|
|
7109
|
-
|
|
7768
|
+
if (eligible?.has(seat.label)) {
|
|
7769
|
+
fill = PREVIEW_ELIGIBLE_FILL;
|
|
7770
|
+
stroke = PREVIEW_ELIGIBLE_STROKE;
|
|
7771
|
+
} else {
|
|
7772
|
+
fill = PREVIEW_UNAVAILABLE_FILL;
|
|
7773
|
+
stroke = PREVIEW_UNAVAILABLE_STROKE;
|
|
7774
|
+
}
|
|
7110
7775
|
} else if (channelId !== PUBLIC_CHANNEL_ID) {
|
|
7111
7776
|
fill = this.markerFor(channelId).color;
|
|
7777
|
+
stroke = ALLOCATION_STROKE;
|
|
7112
7778
|
}
|
|
7113
7779
|
if (!fill) continue;
|
|
7114
7780
|
const point = this.host.worldToScreen({ x: seat.x, y: seat.y });
|
|
7115
7781
|
if (!point) continue;
|
|
7116
7782
|
if (point.x < -size || point.y < -size || point.x > width + size || point.y > height + size) continue;
|
|
7783
|
+
if (previewSections) {
|
|
7784
|
+
const section = this.host.sectionOfLabel(seat.label);
|
|
7785
|
+
if (section) {
|
|
7786
|
+
const bounds = previewSections.get(section.id) ?? {
|
|
7787
|
+
label: section.label,
|
|
7788
|
+
minX: point.x,
|
|
7789
|
+
minY: point.y,
|
|
7790
|
+
maxX: point.x,
|
|
7791
|
+
maxY: point.y
|
|
7792
|
+
};
|
|
7793
|
+
bounds.minX = Math.min(bounds.minX, point.x);
|
|
7794
|
+
bounds.minY = Math.min(bounds.minY, point.y);
|
|
7795
|
+
bounds.maxX = Math.max(bounds.maxX, point.x);
|
|
7796
|
+
bounds.maxY = Math.max(bounds.maxY, point.y);
|
|
7797
|
+
previewSections.set(section.id, bounds);
|
|
7798
|
+
}
|
|
7799
|
+
}
|
|
7117
7800
|
ctx.fillStyle = fill;
|
|
7118
|
-
|
|
7119
|
-
|
|
7801
|
+
if (this.view === "preview" || channelId !== PUBLIC_CHANNEL_ID) {
|
|
7802
|
+
const radius = Math.max(2, half + Math.min(1.5, half * 0.06));
|
|
7803
|
+
ctx.globalAlpha = 1;
|
|
7804
|
+
ctx.beginPath();
|
|
7805
|
+
ctx.arc(point.x, point.y, radius, 0, Math.PI * 2);
|
|
7806
|
+
ctx.fill();
|
|
7807
|
+
ctx.strokeStyle = stroke ?? fill;
|
|
7808
|
+
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));
|
|
7809
|
+
ctx.stroke();
|
|
7810
|
+
if (this.view === "preview" && eligible?.has(seat.label) && size >= 22) {
|
|
7811
|
+
this.paintPreviewSeatLabel(ctx, seat.label, point.x, point.y, radius);
|
|
7812
|
+
}
|
|
7813
|
+
} else {
|
|
7814
|
+
ctx.globalAlpha = 0.85;
|
|
7815
|
+
ctx.fillRect(point.x - half, point.y - half, size, size);
|
|
7816
|
+
}
|
|
7120
7817
|
}
|
|
7121
7818
|
ctx.globalAlpha = 1;
|
|
7819
|
+
if (previewSections) this.paintPreviewSectionLabels(ctx, previewSections);
|
|
7820
|
+
this.paintSectionTargets(sectionTargets);
|
|
7122
7821
|
this.paintFlags(clusters);
|
|
7123
7822
|
}
|
|
7823
|
+
/** Draw an eligible seat's actual chart label without inventing a new buyer
|
|
7824
|
+
* identifier. Long labels scale down and are omitted rather than overflowing
|
|
7825
|
+
* into an adjacent seat. */
|
|
7826
|
+
paintPreviewSeatLabel(ctx, label, x, y, radius) {
|
|
7827
|
+
const maxWidth = radius * 1.55;
|
|
7828
|
+
let fontSize = Math.min(13, Math.max(7, radius * 0.55));
|
|
7829
|
+
const minFontSize = 6;
|
|
7830
|
+
while (fontSize >= minFontSize) {
|
|
7831
|
+
ctx.font = `800 ${fontSize}px var(--slm-font, system-ui, sans-serif)`;
|
|
7832
|
+
if (ctx.measureText(label).width <= maxWidth) break;
|
|
7833
|
+
fontSize -= 0.5;
|
|
7834
|
+
}
|
|
7835
|
+
if (fontSize < minFontSize) return;
|
|
7836
|
+
ctx.fillStyle = "#ffffff";
|
|
7837
|
+
ctx.textAlign = "center";
|
|
7838
|
+
ctx.textBaseline = "middle";
|
|
7839
|
+
ctx.fillText(label, x, y);
|
|
7840
|
+
}
|
|
7841
|
+
/** A section overview is a navigation map. These transparent, keyboardable
|
|
7842
|
+
* hit areas sit over the renderer's section shells so both mouse and keyboard
|
|
7843
|
+
* always take the organizer into the real focused-section camera state. */
|
|
7844
|
+
paintSectionTargets(sections) {
|
|
7845
|
+
const layer = this.layer;
|
|
7846
|
+
if (!layer) return;
|
|
7847
|
+
layer.querySelectorAll(".slm-ch-section-target").forEach((el2) => el2.remove());
|
|
7848
|
+
if (!sections) return;
|
|
7849
|
+
for (const [id, section] of sections) {
|
|
7850
|
+
const width = section.maxX - section.minX;
|
|
7851
|
+
const height = section.maxY - section.minY;
|
|
7852
|
+
if (width < 20 || height < 20) continue;
|
|
7853
|
+
const target = document.createElement("button");
|
|
7854
|
+
target.type = "button";
|
|
7855
|
+
target.className = "slm-ch-section-target";
|
|
7856
|
+
target.style.left = `${section.minX - 8}px`;
|
|
7857
|
+
target.style.top = `${section.minY - 8}px`;
|
|
7858
|
+
target.style.width = `${width + 16}px`;
|
|
7859
|
+
target.style.height = `${height + 16}px`;
|
|
7860
|
+
target.setAttribute("aria-label", `Open ${section.label} seats`);
|
|
7861
|
+
target.addEventListener("click", () => {
|
|
7862
|
+
this.focusedSectionId = id;
|
|
7863
|
+
this.host.focusSection(id);
|
|
7864
|
+
this.paintRail();
|
|
7865
|
+
});
|
|
7866
|
+
layer.appendChild(target);
|
|
7867
|
+
}
|
|
7868
|
+
}
|
|
7869
|
+
/** Keep renderer section names legible over a dense, zoomed-out preview. */
|
|
7870
|
+
paintPreviewSectionLabels(ctx, sections) {
|
|
7871
|
+
for (const section of sections.values()) {
|
|
7872
|
+
const width = section.maxX - section.minX;
|
|
7873
|
+
const height = section.maxY - section.minY;
|
|
7874
|
+
if (width < 52 || height < 26) continue;
|
|
7875
|
+
const centerX = (section.minX + section.maxX) / 2;
|
|
7876
|
+
const centerY = (section.minY + section.maxY) / 2;
|
|
7877
|
+
const fontSize = Math.max(11, Math.min(15, height * 0.16));
|
|
7878
|
+
ctx.font = `800 ${fontSize}px var(--slm-font, system-ui, sans-serif)`;
|
|
7879
|
+
const labelWidth = Math.min(width - 8, ctx.measureText(section.label).width + 18);
|
|
7880
|
+
const labelHeight = fontSize + 10;
|
|
7881
|
+
ctx.fillStyle = "rgba(11, 16, 28, .88)";
|
|
7882
|
+
ctx.fillRect(centerX - labelWidth / 2, centerY - labelHeight / 2, labelWidth, labelHeight);
|
|
7883
|
+
ctx.fillStyle = "#f8fafc";
|
|
7884
|
+
ctx.textAlign = "center";
|
|
7885
|
+
ctx.textBaseline = "middle";
|
|
7886
|
+
ctx.fillText(section.label, centerX, centerY);
|
|
7887
|
+
}
|
|
7888
|
+
}
|
|
7124
7889
|
/** Letter flags at each channel's centroid — the non-color identity cue. */
|
|
7125
7890
|
paintFlags(clusters) {
|
|
7126
7891
|
const layer = this.layer;
|
|
7127
7892
|
if (!layer) return;
|
|
7128
|
-
layer.querySelectorAll(".slm-ch-flag").forEach((
|
|
7893
|
+
layer.querySelectorAll(".slm-ch-flag").forEach((el2) => el2.remove());
|
|
7129
7894
|
if (this.view === "preview") return;
|
|
7130
7895
|
const ranked = [...clusters.entries()].sort((a, b) => b[1].n - a[1].n).slice(0, MAX_FLAGS);
|
|
7131
7896
|
for (const [channelId, cluster] of ranked) {
|
|
@@ -7186,7 +7951,7 @@ var ChannelsMode = class {
|
|
|
7186
7951
|
});
|
|
7187
7952
|
});
|
|
7188
7953
|
}
|
|
7189
|
-
setBanner(on, name = "") {
|
|
7954
|
+
setBanner(on, name = "", eligibleSeats) {
|
|
7190
7955
|
const banner = this.bannerEl;
|
|
7191
7956
|
if (!banner) return;
|
|
7192
7957
|
this.host.root.classList.toggle("ch-preview", on);
|
|
@@ -7196,8 +7961,9 @@ var ChannelsMode = class {
|
|
|
7196
7961
|
return;
|
|
7197
7962
|
}
|
|
7198
7963
|
const marker = this.previewAudience.length === 1 ? this.markerFor(this.previewAudience[0]) : { color: "var(--slm-accent)", letter: "" };
|
|
7964
|
+
const availability = eligibleSeats == null ? "" : ` \xB7 ${eligibleSeats.toLocaleString()} ${eligibleSeats === 1 ? "seat" : "seats"} available now`;
|
|
7199
7965
|
banner.innerHTML = `<span class="dot" style="background:${esc(marker.color)}"></span>
|
|
7200
|
-
Previewing buyer access \xB7 ${esc(name)} \xB7 read-only
|
|
7966
|
+
Previewing buyer access \xB7 ${esc(name)}${availability} \xB7 read-only
|
|
7201
7967
|
<button type="button" data-ch-act="exit-preview">Exit preview</button>`;
|
|
7202
7968
|
banner.classList.add("on");
|
|
7203
7969
|
banner.querySelector('[data-ch-act="exit-preview"]')?.addEventListener("click", () => this.setView("inspect"));
|
|
@@ -7232,6 +7998,8 @@ var ChannelsMode = class {
|
|
|
7232
7998
|
includePublic: this.previewIncludePublic || audience.some(isPublicChannelId)
|
|
7233
7999
|
});
|
|
7234
8000
|
this.previewSupported = true;
|
|
8001
|
+
const eligibleSeats = this.previewProjection.available === false ? void 0 : this.previewProjection.counts?.eligible ?? this.previewProjection.eligible?.length;
|
|
8002
|
+
this.setBanner(true, names, eligibleSeats);
|
|
7235
8003
|
} catch (err) {
|
|
7236
8004
|
const status = err instanceof ManageApiError ? err.status : 0;
|
|
7237
8005
|
this.previewSupported = !(status === 404 || status === 405 || status === 501);
|
|
@@ -7282,6 +8050,22 @@ var ChannelsMode = class {
|
|
|
7282
8050
|
aria-pressed="${this.view === "inspect"}">Inspect allocation</button>
|
|
7283
8051
|
<button type="button" class="${previewOn.trim()}" data-ch-view="preview"
|
|
7284
8052
|
aria-pressed="${this.view === "preview"}">Preview buyer access</button>
|
|
8053
|
+
</div>${this.mapNavigationHtml()}`;
|
|
8054
|
+
}
|
|
8055
|
+
mapNavigationHtml() {
|
|
8056
|
+
if (this.host.sections().length < 2) return "";
|
|
8057
|
+
const focused = this.focusedSectionId ? this.host.sections().find((section) => section.id === this.focusedSectionId)?.label ?? "section" : null;
|
|
8058
|
+
const panOn = this.mapIntent === "pan" ? " on" : "";
|
|
8059
|
+
const assignOn = this.mapIntent === "assign" ? " on" : "";
|
|
8060
|
+
const intent = this.view === "inspect" && this.caps.manage ? `<div class="slm-ch-viewseg" role="group" aria-label="Map interaction">
|
|
8061
|
+
<button type="button" class="${panOn.trim()}" data-ch-map="pan" aria-pressed="${this.mapIntent === "pan"}">Pan map</button>
|
|
8062
|
+
<button type="button" class="${assignOn.trim()}" data-ch-map="assign" aria-pressed="${this.mapIntent === "assign"}">Assign seats</button>
|
|
8063
|
+
</div>
|
|
8064
|
+
<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>";
|
|
8065
|
+
return `<div class="slm-ch-mapnav">
|
|
8066
|
+
<div class="slm-ch-mapnav-head"><span>${focused ? `Viewing ${esc(focused)}` : "Section overview"}</span>
|
|
8067
|
+
<button type="button" data-ch-act="sections">All sections</button></div>
|
|
8068
|
+
${intent}
|
|
7285
8069
|
</div>`;
|
|
7286
8070
|
}
|
|
7287
8071
|
countsHtml(counts, key) {
|
|
@@ -7420,6 +8204,8 @@ var ChannelsMode = class {
|
|
|
7420
8204
|
*/
|
|
7421
8205
|
async loadLinks(channelId) {
|
|
7422
8206
|
if (!this.caps.view) return;
|
|
8207
|
+
const seq = ++this.linksSeq;
|
|
8208
|
+
const superseded = () => seq !== this.linksSeq || this.linksChannelId !== channelId;
|
|
7423
8209
|
if (this.linksChannelId !== channelId) {
|
|
7424
8210
|
this.links = [];
|
|
7425
8211
|
this.linksChannelId = channelId;
|
|
@@ -7427,11 +8213,11 @@ var ChannelsMode = class {
|
|
|
7427
8213
|
}
|
|
7428
8214
|
try {
|
|
7429
8215
|
const res = await this.host.api.accessLinks(this.host.eventKey, channelId);
|
|
7430
|
-
if (
|
|
8216
|
+
if (superseded()) return;
|
|
7431
8217
|
this.links = res.links ?? [];
|
|
7432
8218
|
this.linksState = "ready";
|
|
7433
8219
|
} catch (err) {
|
|
7434
|
-
if (
|
|
8220
|
+
if (superseded()) return;
|
|
7435
8221
|
const status = err instanceof ManageApiError ? err.status : 0;
|
|
7436
8222
|
this.links = [];
|
|
7437
8223
|
this.linksState = status === 404 || status === 405 || status === 501 ? "unsupported" : "error";
|
|
@@ -7508,9 +8294,9 @@ var ChannelsMode = class {
|
|
|
7508
8294
|
const unavailable = this.previewProjection?.available === false ? `<div class="slm-ch-alert warn" role="status"><span>\u23F8</span>
|
|
7509
8295
|
<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")}.
|
|
7510
8296
|
A buyer arriving with this access sees this message, not these seats.</span></div>` : "";
|
|
7511
|
-
const
|
|
7512
|
-
const summary =
|
|
7513
|
-
|
|
8297
|
+
const eligibleSeats = this.previewProjection?.counts?.eligible ?? this.previewProjection?.eligible?.length;
|
|
8298
|
+
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>
|
|
8299
|
+
This is the exact buyer-visible allocation.${this.previewProjection?.includePublic === false ? " Public sale seats are <b>not</b> included in this access." : ""}</span></div>` : "";
|
|
7514
8300
|
const includePublic = isPublicChannelId(current) ? "" : `
|
|
7515
8301
|
<label class="slm-note" style="display:flex;gap:8px;align-items:center;margin:10px 0">
|
|
7516
8302
|
<input type="checkbox" data-ch-includepublic ${this.previewIncludePublic ? "checked" : ""} />
|
|
@@ -7541,6 +8327,13 @@ var ChannelsMode = class {
|
|
|
7541
8327
|
rail.querySelectorAll("[data-ch-view]").forEach((button) => {
|
|
7542
8328
|
button.addEventListener("click", () => this.setView(button.dataset.chView));
|
|
7543
8329
|
});
|
|
8330
|
+
rail.querySelectorAll("[data-ch-map]").forEach((button) => {
|
|
8331
|
+
button.addEventListener("click", () => {
|
|
8332
|
+
this.mapIntent = button.dataset.chMap === "assign" ? "assign" : "pan";
|
|
8333
|
+
this.paintRail();
|
|
8334
|
+
this.onInteractionChange?.();
|
|
8335
|
+
});
|
|
8336
|
+
});
|
|
7544
8337
|
rail.querySelectorAll("[data-ch-detail]").forEach((button) => {
|
|
7545
8338
|
button.addEventListener("click", () => {
|
|
7546
8339
|
const channelId = button.dataset.chDetail;
|
|
@@ -7591,6 +8384,11 @@ var ChannelsMode = class {
|
|
|
7591
8384
|
}
|
|
7592
8385
|
railAction(action) {
|
|
7593
8386
|
switch (action) {
|
|
8387
|
+
case "sections":
|
|
8388
|
+
this.focusedSectionId = null;
|
|
8389
|
+
this.host.showSectionOverview();
|
|
8390
|
+
this.paintRail();
|
|
8391
|
+
break;
|
|
7594
8392
|
case "create":
|
|
7595
8393
|
this.openDialog({ kind: "create" });
|
|
7596
8394
|
break;
|
|
@@ -7765,7 +8563,7 @@ var ChannelsMode = class {
|
|
|
7765
8563
|
<label>Marker</label>
|
|
7766
8564
|
<div style="display:flex;gap:8px;align-items:center">
|
|
7767
8565
|
<span class="slm-ch-mk" data-ch-marker style="background:${esc(suggestion.color)};width:28px;height:28px;font-size:13px">${esc(suggestion.letter)}</span>
|
|
7768
|
-
<span class="slm-note" style="margin:0">Letter
|
|
8566
|
+
<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>
|
|
7769
8567
|
</div>
|
|
7770
8568
|
</div>
|
|
7771
8569
|
<div class="slm-field">
|
|
@@ -8116,6 +8914,24 @@ var ChannelsMode = class {
|
|
|
8116
8914
|
await this.loadLinks(channelId);
|
|
8117
8915
|
this.paintRail();
|
|
8118
8916
|
}
|
|
8917
|
+
/**
|
|
8918
|
+
* The reload EVERY link mutation owes the panel.
|
|
8919
|
+
*
|
|
8920
|
+
* A create/rotate/revoke changes two things the detail panel renders: the
|
|
8921
|
+
* channel's access line (the server sets `access.intent` on create, and clears
|
|
8922
|
+
* it when the last live link goes) and the link status list. Both are re-read
|
|
8923
|
+
* here and the rail repainted, so the panel the organizer is already looking
|
|
8924
|
+
* at is current the moment the mutation lands — no reload, and no dependence
|
|
8925
|
+
* on HOW the one-time reveal was dismissed (the button, Escape, or never).
|
|
8926
|
+
*/
|
|
8927
|
+
async reloadAfterLinkChange(channelId) {
|
|
8928
|
+
if (this.detailChannelId === channelId) {
|
|
8929
|
+
await this.refresh({ quiet: true });
|
|
8930
|
+
return;
|
|
8931
|
+
}
|
|
8932
|
+
await this.loadLinks(channelId);
|
|
8933
|
+
if (this.active) this.paintRail();
|
|
8934
|
+
}
|
|
8119
8935
|
linkById(linkId) {
|
|
8120
8936
|
return this.links.find((link) => link.id === linkId) ?? null;
|
|
8121
8937
|
}
|
|
@@ -8209,7 +9025,7 @@ var ChannelsMode = class {
|
|
|
8209
9025
|
try {
|
|
8210
9026
|
const reveal = await this.host.api.createAccessLink(this.host.eventKey, channelId, input);
|
|
8211
9027
|
this.revealLink(reveal, { channelId });
|
|
8212
|
-
await this.
|
|
9028
|
+
await this.reloadAfterLinkChange(channelId);
|
|
8213
9029
|
} catch (err) {
|
|
8214
9030
|
this.showDialogError(accessLinkErrorCopy(err instanceof ManageApiError ? err : void 0));
|
|
8215
9031
|
if (!(err instanceof ManageApiError)) this.host.onError(err);
|
|
@@ -8267,9 +9083,6 @@ var ChannelsMode = class {
|
|
|
8267
9083
|
}
|
|
8268
9084
|
selectSecret(dialog);
|
|
8269
9085
|
});
|
|
8270
|
-
dialog.querySelector("[data-ch-lk-done]")?.addEventListener("click", () => {
|
|
8271
|
-
void this.loadLinks(opts.channelId).then(() => this.paintRail());
|
|
8272
|
-
});
|
|
8273
9086
|
});
|
|
8274
9087
|
this.announce("Your hosted access link is ready and is shown once.");
|
|
8275
9088
|
}
|
|
@@ -8335,6 +9148,7 @@ var ChannelsMode = class {
|
|
|
8335
9148
|
endActiveSessions
|
|
8336
9149
|
);
|
|
8337
9150
|
this.revealLink(reveal, { channelId, rotated: true });
|
|
9151
|
+
await this.reloadAfterLinkChange(channelId);
|
|
8338
9152
|
} catch (err) {
|
|
8339
9153
|
this.showDialogError(accessLinkErrorCopy(err instanceof ManageApiError ? err : void 0));
|
|
8340
9154
|
if (!(err instanceof ManageApiError)) this.host.onError(err);
|
|
@@ -8377,9 +9191,7 @@ var ChannelsMode = class {
|
|
|
8377
9191
|
endActiveSessions
|
|
8378
9192
|
);
|
|
8379
9193
|
this.closeDialog();
|
|
8380
|
-
await this.
|
|
8381
|
-
await this.refresh({ quiet: true });
|
|
8382
|
-
this.paintRail();
|
|
9194
|
+
await this.reloadAfterLinkChange(channelId);
|
|
8383
9195
|
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");
|
|
8384
9196
|
} catch (err) {
|
|
8385
9197
|
this.showDialogError(accessLinkErrorCopy(err instanceof ManageApiError ? err : void 0));
|
|
@@ -8441,9 +9253,9 @@ function toLocalInput(ms) {
|
|
|
8441
9253
|
}
|
|
8442
9254
|
function resolveContainer4(container) {
|
|
8443
9255
|
if (typeof container === "string") {
|
|
8444
|
-
const
|
|
8445
|
-
if (!
|
|
8446
|
-
return
|
|
9256
|
+
const el2 = document.querySelector(container);
|
|
9257
|
+
if (!el2) throw new Error(`seatmanager: container "${container}" not found`);
|
|
9258
|
+
return el2;
|
|
8447
9259
|
}
|
|
8448
9260
|
if (!(container instanceof HTMLElement)) {
|
|
8449
9261
|
throw new Error("seatmanager: container must be a CSS selector or an HTMLElement");
|
|
@@ -8454,7 +9266,7 @@ function toRenderStatus(s) {
|
|
|
8454
9266
|
return s === "blocked" ? "not_for_sale" : s;
|
|
8455
9267
|
}
|
|
8456
9268
|
var DEFAULT_API_BASE3 = "https://api.seatlayer.io";
|
|
8457
|
-
var
|
|
9269
|
+
var STYLE_ID3 = "seatlayer-manager-style";
|
|
8458
9270
|
var FEED_CAP = 80;
|
|
8459
9271
|
var MAX_LIVE_SEAT_PULSES = 16;
|
|
8460
9272
|
var MAX_LIVE_SECTION_PULSES = 4;
|
|
@@ -8464,9 +9276,19 @@ var LEGEND = [
|
|
|
8464
9276
|
{ key: "booked", label: "Booked", color: "#22a06b" },
|
|
8465
9277
|
{ key: "blocked", label: "Blocked", color: "#8b94ac" }
|
|
8466
9278
|
];
|
|
8467
|
-
var
|
|
9279
|
+
var MANAGER_CSS = (
|
|
9280
|
+
/* @sl-css */
|
|
9281
|
+
`
|
|
8468
9282
|
.slm{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:480px;overflow:hidden;
|
|
8469
|
-
background:var(--slm-bg);color:var(--slm-text);font-family:var(--slm-font);border-radius:var(--slm-radius)
|
|
9283
|
+
background:var(--slm-bg);color:var(--slm-text);font-family:var(--slm-font);border-radius:var(--slm-radius);
|
|
9284
|
+
/* Motion tokens (motion-system \xA72), declared by the cockpit ROOT rather than
|
|
9285
|
+
borrowed from CHANNELS_CSS. The base cockpit animates whether or not
|
|
9286
|
+
Channels mode is in use, so owning its own tokens is what stops a token
|
|
9287
|
+
edit from silently changing only half the surface. Channels mode declares
|
|
9288
|
+
the identical values so an embed of it stays self-contained. */
|
|
9289
|
+
--slm-mo-instant:80ms;--slm-mo-quick:140ms;--slm-mo-base:200ms;--slm-mo-slow:320ms;--slm-mo-ambient:2000ms;
|
|
9290
|
+
--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);
|
|
9291
|
+
--slm-mo-spring:cubic-bezier(.34,1.3,.64,1)}
|
|
8470
9292
|
.slm *{box-sizing:border-box;margin:0;padding:0}
|
|
8471
9293
|
.slm button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
|
|
8472
9294
|
.slm input{font:inherit}
|
|
@@ -8479,7 +9301,8 @@ var CSS2 = `
|
|
|
8479
9301
|
.slm-mode.on{background:var(--slm-accent);color:var(--slm-accent-ink)}
|
|
8480
9302
|
.slm-live{display:inline-flex;align-items:center;gap:6px;font-size:11px;letter-spacing:.12em;font-weight:800;color:var(--slm-muted)}
|
|
8481
9303
|
.slm-live-dot{width:8px;height:8px;border-radius:50%;background:#8b94ac}
|
|
8482
|
-
.slm.live .slm-live-dot{background:#22a06b;box-shadow:0 0 0 0 rgba(34,160,107,.55);
|
|
9304
|
+
.slm.live .slm-live-dot{background:#22a06b;box-shadow:0 0 0 0 rgba(34,160,107,.55);
|
|
9305
|
+
animation:slm-pulse var(--slm-mo-ambient) infinite}
|
|
8483
9306
|
@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)}}
|
|
8484
9307
|
.slm-kpis{grid-column:1/-1;display:grid;grid-template-columns:repeat(8,minmax(0,1fr));width:100%;padding-top:10px;
|
|
8485
9308
|
border-top:1px solid var(--slm-line)}
|
|
@@ -8488,7 +9311,11 @@ var CSS2 = `
|
|
|
8488
9311
|
font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
8489
9312
|
.slm-kpi span{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--slm-muted);font-weight:700}
|
|
8490
9313
|
.slm-kpi .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:baseline}
|
|
8491
|
-
|
|
9314
|
+
/* The one playful moment this surface is allowed (\xA72 --mo-spring). It ran at
|
|
9315
|
+
.58s against a 200ms catalog, which read as a different design language from
|
|
9316
|
+
the dashboard tile it mirrors; Channels mode's own count bump is the same
|
|
9317
|
+
pattern and must stay in step with it. */
|
|
9318
|
+
.slm-kpi.changed b{animation:slm-kpi-bump var(--slm-mo-base) var(--slm-mo-spring)}
|
|
8492
9319
|
.slm-kpidelta{position:absolute;right:4px;top:-12px;padding:2px 5px;border-radius:999px;background:rgba(34,160,107,.17);
|
|
8493
9320
|
color:#5bd39b!important;font-size:9px!important;letter-spacing:0!important;text-transform:none!important;white-space:nowrap;
|
|
8494
9321
|
animation:slm-kpi-delta 1.45s ease-out both;pointer-events:none}
|
|
@@ -8507,12 +9334,14 @@ var CSS2 = `
|
|
|
8507
9334
|
.slm-hud-chip{padding:6px 11px;border-radius:999px;font-size:12px;font-weight:700;background:var(--slm-surface);
|
|
8508
9335
|
border:1px solid var(--slm-line);color:var(--slm-text)}
|
|
8509
9336
|
.slm-zoomhint{position:absolute;left:50%;top:14px;transform:translateX(-50%);padding:6px 13px;border-radius:999px;
|
|
8510
|
-
background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:700;pointer-events:none;opacity:0;
|
|
9337
|
+
background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:700;pointer-events:none;opacity:0;
|
|
9338
|
+
transition:opacity var(--slm-mo-base) var(--slm-mo-out)}
|
|
8511
9339
|
.slm-zoomhint.on{opacity:1}
|
|
8512
9340
|
.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));
|
|
8513
9341
|
padding:8px 12px;border:1px solid var(--slm-line);border-radius:999px;background:color-mix(in srgb,var(--slm-surface) 92%,transparent);
|
|
8514
9342
|
box-shadow:0 10px 34px rgba(0,0,0,.32);opacity:0;transform:translate(-50%,-8px);pointer-events:none;
|
|
8515
|
-
transition:opacity
|
|
9343
|
+
transition:opacity var(--slm-mo-quick) var(--slm-mo-out),transform var(--slm-mo-base) var(--slm-mo-out);
|
|
9344
|
+
backdrop-filter:blur(10px)}
|
|
8516
9345
|
.slm-liveevent.on{opacity:1;transform:translate(-50%,0)}
|
|
8517
9346
|
.slm.block-mode .slm-liveevent{top:52px}
|
|
8518
9347
|
.slm-liveeventdot{width:8px;height:8px;border-radius:50%;flex:none}.slm-liveeventcopy{min-width:0;overflow:hidden;text-overflow:ellipsis;
|
|
@@ -8532,7 +9361,8 @@ var CSS2 = `
|
|
|
8532
9361
|
/* activity feed */
|
|
8533
9362
|
.slm-feed{display:flex;flex-direction:column;gap:0}
|
|
8534
9363
|
.slm-feedrow{display:flex!important;width:100%;align-items:center;gap:9px;padding:8px 2px!important;border-bottom:1px solid var(--slm-line)!important;
|
|
8535
|
-
border-radius:6px;font-size:12.5px;text-align:left!important;
|
|
9364
|
+
border-radius:6px;font-size:12.5px;text-align:left!important;
|
|
9365
|
+
animation:slm-in var(--slm-mo-quick) var(--slm-mo-out)}
|
|
8536
9366
|
.slm-feedrow:hover{background:rgba(255,255,255,.035)!important}
|
|
8537
9367
|
@keyframes slm-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
|
|
8538
9368
|
.slm-feeddot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
@@ -8599,7 +9429,8 @@ var CSS2 = `
|
|
|
8599
9429
|
|
|
8600
9430
|
/* toast */
|
|
8601
9431
|
.slm-toast{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);padding:10px 16px;border-radius:10px;
|
|
8602
|
-
font-size:13px;font-weight:700;box-shadow:0 8px 24px rgba(0,0,0,.28);opacity:0;pointer-events:none;
|
|
9432
|
+
font-size:13px;font-weight:700;box-shadow:0 8px 24px rgba(0,0,0,.28);opacity:0;pointer-events:none;
|
|
9433
|
+
transition:opacity var(--slm-mo-base) var(--slm-mo-out);
|
|
8603
9434
|
background:var(--slm-surface);color:var(--slm-text);border:1px solid var(--slm-line);z-index:5}
|
|
8604
9435
|
.slm-toast.on{opacity:1}
|
|
8605
9436
|
.slm-toast.err{background:#c0392b;color:#fff;border-color:#c0392b}
|
|
@@ -8610,7 +9441,9 @@ var CSS2 = `
|
|
|
8610
9441
|
.slm-barbtn.on{background:rgba(244,183,64,.13);border-color:#f4b740;color:#f7ca6b}
|
|
8611
9442
|
.slm-sectionlist{display:flex;flex-direction:column;gap:8px;margin-top:4px}
|
|
8612
9443
|
.slm-sectionlist + .slm-eyebrow{margin-top:18px}
|
|
8613
|
-
.slm-sectionrow{width:100%;padding:10px!important;border:1px solid var(--slm-line)!important;border-radius:10px;
|
|
9444
|
+
.slm-sectionrow{width:100%;padding:10px!important;border:1px solid var(--slm-line)!important;border-radius:10px;
|
|
9445
|
+
background:var(--slm-surface)!important;text-align:left!important;
|
|
9446
|
+
transition:border-color var(--slm-mo-quick) var(--slm-mo-out),transform var(--slm-mo-quick) var(--slm-mo-out)}
|
|
8614
9447
|
.slm-sectionrow:hover{border-color:var(--slm-muted)!important;transform:translateY(-1px)}
|
|
8615
9448
|
.slm-sectiontop,.slm-sectionmeta{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
8616
9449
|
.slm-sectiontop{font-size:12.5px;font-weight:800}.slm-sectionmeta{margin-top:5px;color:var(--slm-muted);font-size:11px}
|
|
@@ -8629,7 +9462,8 @@ var CSS2 = `
|
|
|
8629
9462
|
.slm-momentumcopy{margin-top:7px;color:var(--slm-muted);font-size:11px;line-height:1.45}
|
|
8630
9463
|
/* sections: availability windows */
|
|
8631
9464
|
.slm-availlist{display:flex;flex-direction:column;gap:8px;margin:2px 0 12px}
|
|
8632
|
-
.slm-availrow{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
9465
|
+
.slm-availrow{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);
|
|
9466
|
+
transition:border-color var(--slm-mo-quick) var(--slm-mo-out),opacity var(--slm-mo-quick) var(--slm-mo-out)}
|
|
8633
9467
|
.slm-availrow.zone{background:color-mix(in srgb,var(--slm-surface) 82%,#000)}
|
|
8634
9468
|
.slm-availrow.hidden{opacity:.62}.slm-availrow.closed{opacity:.82}
|
|
8635
9469
|
.slm-availhead{display:flex;align-items:center;gap:8px}
|
|
@@ -8669,17 +9503,31 @@ var CSS2 = `
|
|
|
8669
9503
|
.slm.compact .slm-barbtn{flex:1;padding:6px 9px}.slm.compact .slm-kpis{grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
|
|
8670
9504
|
.slm.compact .slm-kpi[data-kpi="buyers"],.slm.compact .slm-kpi[data-kpi="active-holds"],
|
|
8671
9505
|
.slm.compact .slm-kpi[data-kpi="sold-pct"],.slm.compact .slm-kpi[data-kpi="gross-sales"]{display:none}
|
|
9506
|
+
/* Reduced motion, as a BLANKET over the cockpit subtree rather than a list of
|
|
9507
|
+
selectors. The list this replaces named four animations and two transitions,
|
|
9508
|
+
and had silently fallen behind the stylesheet: the zoom hint, the toast and
|
|
9509
|
+
the availability rows all still animated for a user who had asked the OS for
|
|
9510
|
+
none. An enumerated list has to be edited every time a rule is added, and
|
|
9511
|
+
nothing fails when it isn't \u2014 so it drifts. This cannot.
|
|
9512
|
+
|
|
9513
|
+
Motion is removed, never the information it carried: Channels mode's own
|
|
9514
|
+
block substitutes static outlines for its shake and success states, and it
|
|
9515
|
+
stays authoritative for those. No JS here waits on animationend or
|
|
9516
|
+
transitionend, so cutting them outright strands no state. */
|
|
8672
9517
|
@media (prefers-reduced-motion:reduce){
|
|
8673
|
-
.slm
|
|
8674
|
-
|
|
9518
|
+
.slm,.slm *,.slm *::before,.slm *::after{
|
|
9519
|
+
animation:none!important;
|
|
9520
|
+
transition:none!important;
|
|
9521
|
+
scroll-behavior:auto!important}
|
|
8675
9522
|
}
|
|
8676
|
-
${CHANNELS_CSS}
|
|
9523
|
+
${CHANNELS_CSS}`
|
|
9524
|
+
);
|
|
8677
9525
|
function injectStyle() {
|
|
8678
|
-
if (typeof document === "undefined" || document.getElementById(
|
|
8679
|
-
const
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
document.head.appendChild(
|
|
9526
|
+
if (typeof document === "undefined" || document.getElementById(STYLE_ID3)) return;
|
|
9527
|
+
const el2 = document.createElement("style");
|
|
9528
|
+
el2.id = STYLE_ID3;
|
|
9529
|
+
el2.textContent = MANAGER_CSS;
|
|
9530
|
+
document.head.appendChild(el2);
|
|
8683
9531
|
}
|
|
8684
9532
|
function themeVars(theme) {
|
|
8685
9533
|
const t3 = theme ?? {};
|
|
@@ -8738,6 +9586,11 @@ var SeatManager = class {
|
|
|
8738
9586
|
this.reconnectTimer = null;
|
|
8739
9587
|
this.attempt = 0;
|
|
8740
9588
|
this.closed = false;
|
|
9589
|
+
/** Mirrors the `live` root class, so the getter never has to read the DOM. */
|
|
9590
|
+
this.connectionStatus = "reconnecting";
|
|
9591
|
+
/** When the server last told us something. Stamped on accepted traffic only —
|
|
9592
|
+
* a socket that opens and says nothing has not refreshed anything. */
|
|
9593
|
+
this.lastMessageAt = null;
|
|
8741
9594
|
this.ready = false;
|
|
8742
9595
|
this.feed = [];
|
|
8743
9596
|
this.feedTimer = null;
|
|
@@ -8947,6 +9800,12 @@ var SeatManager = class {
|
|
|
8947
9800
|
},
|
|
8948
9801
|
worldToScreen: (point) => this.renderer?.worldToScreen(point) ?? null,
|
|
8949
9802
|
seatPixelSize: () => this.seatPixelSize(),
|
|
9803
|
+
isSeatDetail: () => this.renderer?.getRung?.() === "seats",
|
|
9804
|
+
showSectionOverview: () => {
|
|
9805
|
+
this.renderer?.clearSectionFocus();
|
|
9806
|
+
this.renderer?.setRung?.("sections");
|
|
9807
|
+
},
|
|
9808
|
+
focusSection: (sectionId) => this.renderer?.focusSection(sectionId),
|
|
8950
9809
|
isCompact: () => !!this.root?.classList.contains("compact"),
|
|
8951
9810
|
setMapInert: (inert) => {
|
|
8952
9811
|
this.mapHost.toggleAttribute("inert", inert);
|
|
@@ -8956,13 +9815,15 @@ var SeatManager = class {
|
|
|
8956
9815
|
onError: (err) => this.opts.onError?.(err)
|
|
8957
9816
|
};
|
|
8958
9817
|
}
|
|
8959
|
-
/**
|
|
8960
|
-
*
|
|
9818
|
+
/** Actual on-screen seat diameter, for the channel overlay's marks. The
|
|
9819
|
+
* renderer's base seat radius is 9 chart units; retaining the camera scale
|
|
9820
|
+
* (rather than capping it) keeps every preview paint aligned with the real
|
|
9821
|
+
* chart geometry at deep zoom. */
|
|
8961
9822
|
seatPixelSize() {
|
|
8962
9823
|
const rect = this.renderer?.getVisibleWorldRect?.();
|
|
8963
9824
|
const width = this.mapHost?.clientWidth ?? 0;
|
|
8964
9825
|
if (!rect?.width || !width) return 6;
|
|
8965
|
-
return Math.max(3,
|
|
9826
|
+
return Math.max(3, width / rect.width * 18);
|
|
8966
9827
|
}
|
|
8967
9828
|
/** Toggle the normalized sales-velocity outline overlay without changing seat colors. */
|
|
8968
9829
|
setHeatOverlay(enabled) {
|
|
@@ -9138,6 +9999,16 @@ var SeatManager = class {
|
|
|
9138
9999
|
getControlRoomSnapshot(windowMinutes = this.trendWindowMinutes) {
|
|
9139
10000
|
return this.setTrendWindow(windowMinutes);
|
|
9140
10001
|
}
|
|
10002
|
+
/**
|
|
10003
|
+
* The realtime link's current state and the "as of" behind it.
|
|
10004
|
+
*
|
|
10005
|
+
* Pair with `onConnectionChange` for the edges: a host that mounts after a
|
|
10006
|
+
* drop, or re-reads on tab focus, needs to be able to ASK rather than wait
|
|
10007
|
+
* for the next transition that may never come.
|
|
10008
|
+
*/
|
|
10009
|
+
getConnection() {
|
|
10010
|
+
return { status: this.connectionStatus, lastMessageAt: this.lastMessageAt };
|
|
10011
|
+
}
|
|
9141
10012
|
getLog(opts = {}) {
|
|
9142
10013
|
return this.api.log(this.key, opts);
|
|
9143
10014
|
}
|
|
@@ -9202,6 +10073,10 @@ var SeatManager = class {
|
|
|
9202
10073
|
onSelect: (seat) => this.handleSeatSelect(seat),
|
|
9203
10074
|
onDeselect: () => this.syncSelection(),
|
|
9204
10075
|
onMarquee: () => this.syncSelection(),
|
|
10076
|
+
onSectionTap: (sectionId) => {
|
|
10077
|
+
this.renderer?.focusSection(sectionId);
|
|
10078
|
+
this.channels?.handleSectionFocus(sectionId);
|
|
10079
|
+
},
|
|
9205
10080
|
onViewChange: () => {
|
|
9206
10081
|
this.updateZoomHint();
|
|
9207
10082
|
this.channels?.handleViewChange();
|
|
@@ -9212,10 +10087,11 @@ var SeatManager = class {
|
|
|
9212
10087
|
this.applyHeatOverlay();
|
|
9213
10088
|
this.updateZoomHint();
|
|
9214
10089
|
}
|
|
9215
|
-
/** Block
|
|
9216
|
-
*
|
|
10090
|
+
/** Block always uses a marquee. Channels only enables its marquee after the
|
|
10091
|
+
* organizer deliberately chooses Assign seats; Pan map keeps desktop drag
|
|
10092
|
+
* available for large charts. */
|
|
9217
10093
|
isBulkSelectMode() {
|
|
9218
|
-
return this.mode === "block" || this.mode === "channels" && this.channels?.
|
|
10094
|
+
return this.mode === "block" || this.mode === "channels" && this.channels?.usesMarqueeSelection() === true;
|
|
9219
10095
|
}
|
|
9220
10096
|
/**
|
|
9221
10097
|
* Block never touches held or booked inventory, so it cannot select it.
|
|
@@ -9225,7 +10101,7 @@ var SeatManager = class {
|
|
|
9225
10101
|
*/
|
|
9226
10102
|
selectableStatuses() {
|
|
9227
10103
|
if (this.mode === "block") return ["free", "not_for_sale"];
|
|
9228
|
-
if (this.mode === "inspect" || this.isBulkSelectMode()) {
|
|
10104
|
+
if (this.mode === "inspect" || this.isBulkSelectMode() || this.mode === "channels" && this.channels?.canSelect() === true) {
|
|
9229
10105
|
return ["free", "held", "booked", "not_for_sale"];
|
|
9230
10106
|
}
|
|
9231
10107
|
return [];
|
|
@@ -9328,6 +10204,7 @@ var SeatManager = class {
|
|
|
9328
10204
|
return;
|
|
9329
10205
|
}
|
|
9330
10206
|
if (!msg || typeof msg !== "object") return;
|
|
10207
|
+
this.lastMessageAt = Date.now();
|
|
9331
10208
|
const m = msg;
|
|
9332
10209
|
if (Array.isArray(m.hidden) || Array.isArray(m.closed)) {
|
|
9333
10210
|
this.updateEffectiveAvailability(m.hidden, m.closed);
|
|
@@ -9384,6 +10261,7 @@ var SeatManager = class {
|
|
|
9384
10261
|
const objs = await this.api.objects(this.key);
|
|
9385
10262
|
this.applySnapshot(objs.seats);
|
|
9386
10263
|
this.updateEffectiveAvailability(objs.hidden, objs.closed);
|
|
10264
|
+
this.lastMessageAt = Date.now();
|
|
9387
10265
|
} catch {
|
|
9388
10266
|
}
|
|
9389
10267
|
}
|
|
@@ -9800,16 +10678,16 @@ var SeatManager = class {
|
|
|
9800
10678
|
paintModeTabs() {
|
|
9801
10679
|
const available = [];
|
|
9802
10680
|
this.els.modes?.querySelectorAll("[data-mode]").forEach((b) => {
|
|
9803
|
-
const
|
|
9804
|
-
const mode =
|
|
10681
|
+
const el2 = b;
|
|
10682
|
+
const mode = el2.dataset.mode;
|
|
9805
10683
|
const permitted = mode !== "channels" || !!this.channels;
|
|
9806
|
-
|
|
10684
|
+
el2.hidden = !permitted;
|
|
9807
10685
|
if (!permitted) return;
|
|
9808
|
-
available.push({ mode, label:
|
|
10686
|
+
available.push({ mode, label: el2.textContent ?? mode });
|
|
9809
10687
|
const active = mode === this.mode;
|
|
9810
|
-
|
|
9811
|
-
|
|
9812
|
-
|
|
10688
|
+
el2.classList.toggle("on", active);
|
|
10689
|
+
el2.setAttribute("aria-selected", String(active));
|
|
10690
|
+
el2.tabIndex = active ? 0 : -1;
|
|
9813
10691
|
});
|
|
9814
10692
|
const tools = this.els.tools;
|
|
9815
10693
|
if (tools) {
|
|
@@ -9858,6 +10736,14 @@ var SeatManager = class {
|
|
|
9858
10736
|
this.root?.classList.toggle("live", on);
|
|
9859
10737
|
if (this.els.livetext) this.els.livetext.textContent = on ? "LIVE" : "RECONNECTING";
|
|
9860
10738
|
this.paintMonitorInsights();
|
|
10739
|
+
const next = on ? "live" : "reconnecting";
|
|
10740
|
+
if (next === this.connectionStatus) return;
|
|
10741
|
+
this.connectionStatus = next;
|
|
10742
|
+
try {
|
|
10743
|
+
this.opts.onConnectionChange?.(this.getConnection());
|
|
10744
|
+
} catch (err) {
|
|
10745
|
+
this.opts.onError?.(err);
|
|
10746
|
+
}
|
|
9861
10747
|
}
|
|
9862
10748
|
updateZoomHint() {
|
|
9863
10749
|
const hint = this.els.zoomhint;
|
|
@@ -10587,13 +11473,13 @@ var SeatManager = class {
|
|
|
10587
11473
|
this.toast(msg, "err");
|
|
10588
11474
|
}
|
|
10589
11475
|
toast(msg, kind) {
|
|
10590
|
-
const
|
|
10591
|
-
if (!
|
|
10592
|
-
|
|
10593
|
-
|
|
11476
|
+
const el2 = this.els.toast;
|
|
11477
|
+
if (!el2) return;
|
|
11478
|
+
el2.textContent = msg;
|
|
11479
|
+
el2.className = `slm-toast on ${kind}`;
|
|
10594
11480
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
10595
11481
|
this.toastTimer = setTimeout(() => {
|
|
10596
|
-
|
|
11482
|
+
el2.className = "slm-toast";
|
|
10597
11483
|
}, 3200);
|
|
10598
11484
|
}
|
|
10599
11485
|
fail(err) {
|