@seatlayer/js 0.10.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -22,6 +22,9 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ApiError: () => ApiError,
24
24
  EmbeddedDesigner: () => EmbeddedDesigner,
25
+ ManageApi: () => ManageApi,
26
+ ManageApiError: () => ManageApiError,
27
+ SeatManager: () => SeatManager,
25
28
  SeatPicker: () => SeatPicker,
26
29
  SeatingChart: () => SeatingChart
27
30
  });
@@ -61,6 +64,7 @@ async function request(base, path, init = {}) {
61
64
  var PubApi = class {
62
65
  constructor(base) {
63
66
  this.base = base;
67
+ this.viewerId = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `viewer_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
64
68
  }
65
69
  chart(key) {
66
70
  return request(this.base, `/pub/events/${encodeURIComponent(key)}/chart`);
@@ -96,7 +100,8 @@ var PubApi = class {
96
100
  }
97
101
  socketUrl(key) {
98
102
  const wsBase = this.base.replace(/^http/, "ws");
99
- return `${wsBase}/pub/events/${encodeURIComponent(key)}/subscribe`;
103
+ const params = new URLSearchParams({ surface: "picker", viewerId: this.viewerId });
104
+ return `${wsBase}/pub/events/${encodeURIComponent(key)}/subscribe?${params}`;
100
105
  }
101
106
  };
102
107
 
@@ -2149,10 +2154,1297 @@ var SeatPicker = class _SeatPicker {
2149
2154
  }
2150
2155
  }
2151
2156
  };
2157
+
2158
+ // src/SeatManager.ts
2159
+ var import_core3 = require("@seatlayer/core");
2160
+
2161
+ // src/manageApi.ts
2162
+ var ManageApiError = class extends Error {
2163
+ constructor(status, message, code, conflicts) {
2164
+ super(message);
2165
+ this.name = "ManageApiError";
2166
+ this.status = status;
2167
+ this.code = code;
2168
+ this.conflicts = conflicts;
2169
+ }
2170
+ };
2171
+ async function parse(res) {
2172
+ const isJson = (res.headers.get("content-type") ?? "").includes("application/json");
2173
+ const data = isJson ? await res.json().catch(() => null) : null;
2174
+ if (!res.ok) {
2175
+ const err = data;
2176
+ throw new ManageApiError(res.status, err?.error ?? `request_failed_${res.status}`, err?.code, err?.conflicts);
2177
+ }
2178
+ return data;
2179
+ }
2180
+ var ManageApi = class {
2181
+ constructor(apiBase, token) {
2182
+ this.base = apiBase.replace(/\/+$/, "");
2183
+ this.token = token;
2184
+ }
2185
+ /** Swap the Bearer token in place (SeatManager re-mints on 401). */
2186
+ setToken(token) {
2187
+ this.token = token;
2188
+ }
2189
+ auth(path, init = {}) {
2190
+ const method = init.method ?? "GET";
2191
+ const headers = { Authorization: `Bearer ${this.token}` };
2192
+ let body;
2193
+ if (init.body !== void 0) {
2194
+ headers["Content-Type"] = "application/json";
2195
+ body = JSON.stringify(init.body);
2196
+ }
2197
+ return fetch(`${this.base}${path}`, { method, headers, body, credentials: "omit" }).then((r) => parse(r));
2198
+ }
2199
+ pub(path) {
2200
+ return fetch(`${this.base}${path}`, { credentials: "omit" }).then((r) => parse(r));
2201
+ }
2202
+ // ---- realtime read (public, no token) ----
2203
+ chart(key) {
2204
+ return this.pub(`/pub/events/${encodeURIComponent(key)}/chart`);
2205
+ }
2206
+ objects(key) {
2207
+ return this.pub(`/pub/events/${encodeURIComponent(key)}/objects`);
2208
+ }
2209
+ socketUrl(key) {
2210
+ return `${this.base.replace(/^http/, "ws")}/pub/events/${encodeURIComponent(key)}/subscribe?surface=manager`;
2211
+ }
2212
+ // ---- inventory writes (token) ----
2213
+ /** Take FREE seats off sale in one batched call. Optional `releaseAt` (epoch
2214
+ * ms, future) auto-returns them to sale; `reason` tags the block (M3 uses it).
2215
+ * Throws ManageApiError 409 (conflicts) if any seat was just taken. */
2216
+ block(key, labels, opts = {}) {
2217
+ const body = { labels };
2218
+ if (typeof opts.releaseAt === "number") body.releaseAt = opts.releaseAt;
2219
+ if (opts.reason) body.reason = opts.reason;
2220
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/block`, { method: "POST", body });
2221
+ }
2222
+ /** Return specific blocked seats to sale (one batched call). */
2223
+ unblock(key, labels) {
2224
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/unblock`, { method: "POST", body: { labels } });
2225
+ }
2226
+ /** Return every blocked seat to sale; resolves with the freed count. */
2227
+ unblockAll(key) {
2228
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/unblock-all`, { method: "POST" });
2229
+ }
2230
+ /** Cancel bookings — return BOOKED seats to free (credit not refunded).
2231
+ * Guarded by the original booking reference. */
2232
+ unbook(key, labels, bookingRef) {
2233
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/unbook`, { method: "POST", body: { labels, bookingRef } });
2234
+ }
2235
+ /** Set (ms, clamped 1–60 min server-side) or clear (null) the hold TTL. */
2236
+ setHoldTtl(key, holdTtlMs) {
2237
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/hold-ttl`, { method: "POST", body: { holdTtlMs } });
2238
+ }
2239
+ // ---- reports (token) ----
2240
+ report(key) {
2241
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/report`);
2242
+ }
2243
+ controlRoom(key, windowMinutes = 15) {
2244
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/control-room?window=${windowMinutes}`);
2245
+ }
2246
+ log(key, opts = {}) {
2247
+ const params = new URLSearchParams();
2248
+ if (opts.limit != null) params.set("limit", String(opts.limit));
2249
+ if (opts.before != null) params.set("before", String(opts.before));
2250
+ const qs = params.toString();
2251
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/log${qs ? `?${qs}` : ""}`);
2252
+ }
2253
+ /** CSV report as a Blob (Bearer auth can't ride a plain <a href>). Host builds
2254
+ * an object URL for download. */
2255
+ async reportCsv(key) {
2256
+ const res = await fetch(`${this.base}/v1/events/${encodeURIComponent(key)}/report.csv`, {
2257
+ headers: { Authorization: `Bearer ${this.token}` },
2258
+ credentials: "omit"
2259
+ });
2260
+ if (!res.ok) throw new ManageApiError(res.status, `request_failed_${res.status}`);
2261
+ return res.blob();
2262
+ }
2263
+ };
2264
+
2265
+ // src/SeatManager.ts
2266
+ function resolveContainer4(container) {
2267
+ if (typeof container === "string") {
2268
+ const el = document.querySelector(container);
2269
+ if (!el) throw new Error(`seatmanager: container "${container}" not found`);
2270
+ return el;
2271
+ }
2272
+ if (!(container instanceof HTMLElement)) {
2273
+ throw new Error("seatmanager: container must be a CSS selector or an HTMLElement");
2274
+ }
2275
+ return container;
2276
+ }
2277
+ function toRenderStatus(s) {
2278
+ return s === "blocked" ? "not_for_sale" : s;
2279
+ }
2280
+ var DEFAULT_API_BASE3 = "https://api.seatlayer.io";
2281
+ var STYLE_ID2 = "seatlayer-manager-style";
2282
+ var FEED_CAP = 80;
2283
+ var LEGEND = [
2284
+ { key: "free", label: "Free", color: "#6e7bff" },
2285
+ { key: "held", label: "Held", color: "#f4b740" },
2286
+ { key: "booked", label: "Booked", color: "#22a06b" },
2287
+ { key: "blocked", label: "Blocked", color: "#8b94ac" }
2288
+ ];
2289
+ var CSS2 = `
2290
+ .slm{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:480px;overflow:hidden;
2291
+ background:var(--slm-bg);color:var(--slm-text);font-family:var(--slm-font);border-radius:var(--slm-radius)}
2292
+ .slm *{box-sizing:border-box;margin:0;padding:0}
2293
+ .slm button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
2294
+ .slm input{font:inherit}
2295
+
2296
+ /* top bar */
2297
+ .slm-bar{display:flex;align-items:center;gap:14px;padding:10px 16px;border-bottom:1px solid var(--slm-line);flex:none;flex-wrap:wrap}
2298
+ .slm-modes{display:inline-flex;background:var(--slm-surface);border:1px solid var(--slm-line);border-radius:999px;padding:3px}
2299
+ .slm-mode{padding:6px 16px;border-radius:999px;font-weight:700;font-size:13px;color:var(--slm-muted)}
2300
+ .slm-mode.on{background:var(--slm-accent);color:var(--slm-accent-ink)}
2301
+ .slm-live{display:inline-flex;align-items:center;gap:6px;font-size:11px;letter-spacing:.12em;font-weight:800;color:var(--slm-muted)}
2302
+ .slm-live-dot{width:8px;height:8px;border-radius:50%;background:#8b94ac}
2303
+ .slm.live .slm-live-dot{background:#22a06b;box-shadow:0 0 0 0 rgba(34,160,107,.55);animation:slm-pulse 2s infinite}
2304
+ @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)}}
2305
+ .slm-kpis{display:flex;align-items:center;gap:16px;margin-left:auto;flex-wrap:wrap}
2306
+ .slm-kpi{display:flex;flex-direction:column;line-height:1.15}
2307
+ .slm-kpi b{font-size:17px;font-weight:800;font-variant-numeric:tabular-nums}
2308
+ .slm-kpi span{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--slm-muted);font-weight:700}
2309
+ .slm-kpi .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:baseline}
2310
+ .slm-barbtn{padding:7px 13px;border-radius:9px;border:1px solid var(--slm-line);color:var(--slm-text);font-weight:700;font-size:12.5px}
2311
+ .slm-barbtn:hover{border-color:var(--slm-muted)}
2312
+
2313
+ /* body */
2314
+ .slm-body{display:flex;flex:1;min-height:0}
2315
+ .slm-map{position:relative;flex:1;min-width:0}
2316
+ .slm-map-host{position:absolute;inset:0}
2317
+ .slm-hud{position:absolute;left:12px;bottom:12px;display:flex;gap:8px}
2318
+ .slm-hud-chip{padding:6px 11px;border-radius:999px;font-size:12px;font-weight:700;background:var(--slm-surface);
2319
+ border:1px solid var(--slm-line);color:var(--slm-text)}
2320
+ .slm-zoomhint{position:absolute;left:50%;top:14px;transform:translateX(-50%);padding:6px 13px;border-radius:999px;
2321
+ background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:700;pointer-events:none;opacity:0;transition:opacity .2s}
2322
+ .slm-zoomhint.on{opacity:1}
2323
+ .slm-rail{width:320px;flex:none;border-left:1px solid var(--slm-line);display:flex;flex-direction:column;min-height:0}
2324
+ .slm-railscroll{flex:1;overflow-y:auto;padding:16px}
2325
+ .slm-eyebrow{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--slm-muted);font-weight:800;margin-bottom:6px}
2326
+ .slm-hint{font-size:12.5px;color:var(--slm-muted);line-height:1.5;margin-bottom:14px}
2327
+
2328
+ /* legend rows */
2329
+ .slm-legend{display:flex;flex-direction:column;gap:2px;margin-bottom:16px}
2330
+ .slm-legrow{display:flex;align-items:center;gap:9px;padding:7px 2px;border-bottom:1px solid var(--slm-line)}
2331
+ .slm-legdot{width:10px;height:10px;border-radius:50%;flex:none}
2332
+ .slm-leglabel{flex:1;font-size:13px;font-weight:600}
2333
+ .slm-legcount{font-size:13px;font-weight:800;font-variant-numeric:tabular-nums}
2334
+
2335
+ /* activity feed */
2336
+ .slm-feed{display:flex;flex-direction:column;gap:0}
2337
+ .slm-feedrow{display:flex;align-items:center;gap:9px;padding:8px 2px;border-bottom:1px solid var(--slm-line);
2338
+ font-size:12.5px;animation:slm-in .35s ease}
2339
+ @keyframes slm-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
2340
+ .slm-feeddot{width:8px;height:8px;border-radius:50%;flex:none}
2341
+ .slm-feedtext{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
2342
+ .slm-feedtext b{font-weight:800}
2343
+ .slm-feedtime{font-size:11px;color:var(--slm-muted);font-variant-numeric:tabular-nums;flex:none}
2344
+ .slm-empty{font-size:12.5px;color:var(--slm-muted);padding:12px 0}
2345
+
2346
+ /* block toolbar */
2347
+ .slm-selbar{display:flex;align-items:baseline;gap:8px;margin-bottom:10px}
2348
+ .slm-selnum{font-size:26px;font-weight:800;font-variant-numeric:tabular-nums}
2349
+ .slm-sellabel{font-size:12px;color:var(--slm-muted);font-weight:600}
2350
+ .slm-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px}
2351
+ .slm-btn{flex:1;min-width:120px;padding:10px 14px;border-radius:10px;background:var(--slm-accent);color:var(--slm-accent-ink);
2352
+ font-weight:800;font-size:13px;text-align:center}
2353
+ .slm-btn:disabled{opacity:.45;cursor:not-allowed}
2354
+ .slm-btn.ghost{background:var(--slm-surface);border:1px solid var(--slm-line);color:var(--slm-text)}
2355
+ .slm-btn.danger{background:#c0392b;color:#fff}
2356
+ .slm-chiprow{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:6px}
2357
+ .slm-chip{padding:6px 11px;border-radius:999px;border:1px solid var(--slm-line);background:var(--slm-surface);
2358
+ font-size:12px;font-weight:700;color:var(--slm-text);display:inline-flex;align-items:center;gap:6px}
2359
+ .slm-chip:hover{border-color:var(--slm-muted)}
2360
+ .slm-chip .dot{width:8px;height:8px;border-radius:50%}
2361
+ .slm-field{margin:14px 0}
2362
+ .slm-field label{display:block;font-size:11px;font-weight:700;color:var(--slm-muted);margin-bottom:5px}
2363
+ .slm-input,.slm-select{width:100%;padding:8px 10px;border-radius:9px;border:1px solid var(--slm-line);
2364
+ background:var(--slm-surface);color:var(--slm-text)}
2365
+ .slm-note{font-size:11.5px;color:var(--slm-muted);margin-top:5px}
2366
+
2367
+ /* toast */
2368
+ .slm-toast{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);padding:10px 16px;border-radius:10px;
2369
+ font-size:13px;font-weight:700;box-shadow:0 8px 24px rgba(0,0,0,.28);opacity:0;pointer-events:none;transition:opacity .2s;
2370
+ background:var(--slm-surface);color:var(--slm-text);border:1px solid var(--slm-line);z-index:5}
2371
+ .slm-toast.on{opacity:1}
2372
+ .slm-toast.err{background:#c0392b;color:#fff;border-color:#c0392b}
2373
+ .slm-toast.ok{background:#1f7a4d;color:#fff;border-color:#1f7a4d}
2374
+
2375
+ /* control-room actions + insights */
2376
+ .slm-bar-actions{display:flex;align-items:center;gap:7px}
2377
+ .slm-barbtn.on{background:rgba(244,183,64,.13);border-color:#f4b740;color:#f7ca6b}
2378
+ .slm-sectionlist{display:flex;flex-direction:column;gap:8px;margin-top:4px}
2379
+ .slm-sectionlist + .slm-eyebrow{margin-top:18px}
2380
+ .slm-sectionrow{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
2381
+ .slm-sectiontop,.slm-sectionmeta{display:flex;align-items:center;justify-content:space-between;gap:10px}
2382
+ .slm-sectiontop{font-size:12.5px;font-weight:800}.slm-sectionmeta{margin-top:5px;color:var(--slm-muted);font-size:11px}
2383
+ .slm-trend{font-size:10px;text-transform:uppercase;letter-spacing:.08em}.slm-trend.rising{color:#22a06b}.slm-trend.cooling{color:#f4b740}
2384
+ .slm-health{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px}
2385
+ .slm-healthitem{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
2386
+ .slm-healthitem b{display:block;font-size:17px;font-variant-numeric:tabular-nums}.slm-healthitem span{display:block;margin-top:2px;color:var(--slm-muted);font-size:10px;text-transform:uppercase;letter-spacing:.08em}
2387
+ .slm-sectionhead{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;margin-top:18px}
2388
+ .slm-windows{display:flex;gap:3px;padding:2px;border:1px solid var(--slm-line);border-radius:8px;background:var(--slm-surface)}
2389
+ .slm-window{padding:4px 6px;border-radius:6px;font-size:10px;font-weight:800;color:var(--slm-muted)}.slm-window.on{background:var(--slm-accent);color:var(--slm-accent-ink)}
2390
+ .slm-inspect-card{padding:12px;border:1px solid var(--slm-line);border-radius:12px;background:var(--slm-surface)}
2391
+ .slm-inspect-label{font-size:24px;font-weight:850;letter-spacing:-.02em}.slm-inspect-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:14px}
2392
+ .slm-inspect-grid span{display:block;color:var(--slm-muted);font-size:10px;text-transform:uppercase;letter-spacing:.08em}.slm-inspect-grid b{display:block;margin-top:3px;font-size:12.5px}
2393
+ .slm:fullscreen{border-radius:0;min-height:100vh;background:var(--slm-bg)}
2394
+ .slm:fullscreen .slm-bar{padding:14px 22px}.slm:fullscreen .slm-kpi b{font-size:21px}.slm:fullscreen .slm-rail{width:360px}
2395
+
2396
+ .slm.compact .slm-rail{width:100%;border-left:0;border-top:1px solid var(--slm-line);height:44%}
2397
+ .slm.compact .slm-body{flex-direction:column}
2398
+ .slm.compact .slm-bar{gap:8px;padding:8px}.slm.compact .slm-barbtn{padding:6px 9px}
2399
+ .slm.compact .slm-kpis{gap:9px}.slm.compact .slm-kpi:nth-child(n+5){display:none}
2400
+ `;
2401
+ function injectStyle() {
2402
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
2403
+ const el = document.createElement("style");
2404
+ el.id = STYLE_ID2;
2405
+ el.textContent = CSS2;
2406
+ document.head.appendChild(el);
2407
+ }
2408
+ function themeVars(theme) {
2409
+ const t3 = theme ?? {};
2410
+ return {
2411
+ "--slm-bg": t3.background ?? "#0e1017",
2412
+ "--slm-surface": "#181b24",
2413
+ "--slm-text": "#eef1f7",
2414
+ "--slm-muted": "#8b93a7",
2415
+ "--slm-line": "rgba(255,255,255,.09)",
2416
+ "--slm-accent": t3.accent ?? "#6e7bff",
2417
+ "--slm-accent-ink": t3.accentInk ?? "#ffffff",
2418
+ "--slm-font": "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif",
2419
+ "--slm-radius": "14px"
2420
+ };
2421
+ }
2422
+ function relTime(at, now) {
2423
+ const s = Math.max(0, Math.round((now - at) / 1e3));
2424
+ if (s < 5) return "just now";
2425
+ if (s < 60) return `${s}s ago`;
2426
+ const m = Math.round(s / 60);
2427
+ if (m < 60) return `${m}m ago`;
2428
+ return `${Math.round(m / 60)}h ago`;
2429
+ }
2430
+ function fmtMoney(amount, currency) {
2431
+ try {
2432
+ return new Intl.NumberFormat(void 0, { style: "currency", currency, maximumFractionDigits: 0 }).format(amount);
2433
+ } catch {
2434
+ return `${currency} ${Math.round(amount).toLocaleString()}`;
2435
+ }
2436
+ }
2437
+ function esc(value) {
2438
+ return String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
2439
+ }
2440
+ var SeatManager = class {
2441
+ constructor(options) {
2442
+ this.els = {};
2443
+ this.renderer = null;
2444
+ this.doc = null;
2445
+ // label ⇄ id + status truth (backend speaks labels, engine speaks ids).
2446
+ this.labelToId = /* @__PURE__ */ new Map();
2447
+ this.labelToSeat = /* @__PURE__ */ new Map();
2448
+ this.allIds = [];
2449
+ this.status = /* @__PURE__ */ new Map();
2450
+ this.currency = "USD";
2451
+ this.authoritativeGrossRevenue = 0;
2452
+ this.revenueStatus = "loading";
2453
+ this.revenueRequest = 0;
2454
+ this.revenueRefreshTimer = null;
2455
+ this.controlRoomSnapshot = null;
2456
+ this.trendWindowMinutes = 15;
2457
+ this.heatEnabled = false;
2458
+ // realtime socket
2459
+ this.ws = null;
2460
+ this.reconnectTimer = null;
2461
+ this.attempt = 0;
2462
+ this.closed = false;
2463
+ this.ready = false;
2464
+ this.feed = [];
2465
+ this.feedTimer = null;
2466
+ this.toastTimer = null;
2467
+ this.releaseAt = null;
2468
+ this.layoutObserver = null;
2469
+ this.tokenExpiresAt = null;
2470
+ this.tokenRefreshTimer = null;
2471
+ this.tokenRefreshInFlight = false;
2472
+ this.sectionByObject = /* @__PURE__ */ new Map();
2473
+ this.sectionLabelById = /* @__PURE__ */ new Map();
2474
+ this.lastSyncedAt = null;
2475
+ this.onFullscreenChange = () => {
2476
+ this.paintFullscreenButton();
2477
+ this.updateContainerLayout();
2478
+ this.renderer?.forceDraw();
2479
+ };
2480
+ this.onKeyDown = (event) => {
2481
+ if (event.metaKey || event.ctrlKey || event.altKey) return;
2482
+ const target = event.target;
2483
+ if (target?.matches('input,select,textarea,[contenteditable="true"]')) return;
2484
+ const key = event.key.toLowerCase();
2485
+ if (key === "m") this.setMode("view");
2486
+ else if (key === "i") this.setMode("inspect");
2487
+ else if (key === "b") this.setMode("block");
2488
+ else if (key === "f") this.toggleFullscreen();
2489
+ else return;
2490
+ event.preventDefault();
2491
+ };
2492
+ this.sectionOptions = [];
2493
+ this.opts = options;
2494
+ this.key = options.eventKey;
2495
+ this.mode = options.mode ?? "view";
2496
+ this.keepLive = options.keepLiveWhileHidden ?? true;
2497
+ this.currency = options.currency ?? "USD";
2498
+ this.tokenExpiresAt = options.tokenExpiresAt ?? null;
2499
+ this.api = new ManageApi(options.apiBase ?? DEFAULT_API_BASE3, options.token);
2500
+ this.host = resolveContainer4(options.container);
2501
+ }
2502
+ /** Build the DOM, load the chart, subscribe to realtime, mount the board. */
2503
+ async render() {
2504
+ injectStyle();
2505
+ this.buildChrome();
2506
+ try {
2507
+ const res = await this.api.chart(this.key);
2508
+ this.doc = res.doc;
2509
+ this.currency = res.event.currency ?? this.opts.currency ?? this.currency;
2510
+ const seats = (0, import_core3.expandChart)(res.doc);
2511
+ for (const s of seats) {
2512
+ this.labelToId.set(s.label, s.id);
2513
+ this.labelToSeat.set(s.label, s);
2514
+ this.allIds.push(s.id);
2515
+ }
2516
+ this.buildRenderer();
2517
+ this.buildSectionOptions();
2518
+ await Promise.all([
2519
+ this.resnapshot(),
2520
+ this.refreshControlRoom().catch((err) => this.opts.onError?.(err))
2521
+ ]);
2522
+ this.api.log(this.key, { limit: 24 }).then((page) => this.seedFeed(page.entries)).catch(() => {
2523
+ });
2524
+ this.connect();
2525
+ this.startFeedClock();
2526
+ this.ready = true;
2527
+ this.setMode(this.mode);
2528
+ this.scheduleTokenRefresh();
2529
+ this.opts.onReady?.();
2530
+ } catch (err) {
2531
+ this.fail(err);
2532
+ }
2533
+ return this;
2534
+ }
2535
+ // ---- public API -----------------------------------------------------------
2536
+ setMode(mode) {
2537
+ const changed = mode !== this.mode;
2538
+ this.mode = mode;
2539
+ if (!this.renderer && this.doc) this.buildRenderer();
2540
+ else this.updateRendererInteraction();
2541
+ if (changed) this.renderer?.clearSelection();
2542
+ this.paintModeTabs();
2543
+ this.paintRail();
2544
+ if (changed) this.opts.onModeChange?.(mode);
2545
+ }
2546
+ /** Toggle the normalized sales-velocity outline overlay without changing seat colors. */
2547
+ setHeatOverlay(enabled) {
2548
+ this.heatEnabled = enabled;
2549
+ this.applyHeatOverlay();
2550
+ this.paintHeatButton();
2551
+ }
2552
+ /** Change the current-vs-previous sales window and refresh the private projection. */
2553
+ setTrendWindow(windowMinutes) {
2554
+ const normalized = Number.isFinite(windowMinutes) ? Math.floor(windowMinutes) : 15;
2555
+ this.trendWindowMinutes = Math.max(5, Math.min(60, normalized));
2556
+ this.paintTrendWindow();
2557
+ return this.refreshControlRoom();
2558
+ }
2559
+ async enterFullscreen() {
2560
+ if (!this.root?.requestFullscreen || this.isFullscreen()) return;
2561
+ await this.root.requestFullscreen();
2562
+ this.root.focus({ preventScroll: true });
2563
+ }
2564
+ async exitFullscreen() {
2565
+ if (typeof document === "undefined" || !this.isFullscreen()) return;
2566
+ await document.exitFullscreen();
2567
+ }
2568
+ isFullscreen() {
2569
+ return typeof document !== "undefined" && document.fullscreenElement === this.root;
2570
+ }
2571
+ toggleFullscreen() {
2572
+ const request2 = this.isFullscreen() ? this.exitFullscreen() : this.enterFullscreen();
2573
+ void request2.catch((err) => this.opts.onError?.(err));
2574
+ }
2575
+ /** Rotate the delegated credential without rebuilding DOM, canvas or socket. */
2576
+ setToken(token, expiresAt) {
2577
+ this.api.setToken(token);
2578
+ this.tokenExpiresAt = expiresAt ?? null;
2579
+ this.scheduleTokenRefresh();
2580
+ }
2581
+ scheduleTokenRefresh() {
2582
+ if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
2583
+ this.tokenRefreshTimer = null;
2584
+ const refresh = this.opts.onTokenRefresh;
2585
+ const expiresAt = this.tokenExpiresAt;
2586
+ if (this.closed || !refresh || !expiresAt || !Number.isFinite(expiresAt)) return;
2587
+ const remaining = expiresAt - Date.now();
2588
+ const lead = Math.min(12e4, Math.max(3e4, remaining * 0.2));
2589
+ const delay = Math.max(0, remaining - lead);
2590
+ this.tokenRefreshTimer = setTimeout(() => {
2591
+ this.tokenRefreshTimer = null;
2592
+ void this.rotateToken();
2593
+ }, delay);
2594
+ }
2595
+ async rotateToken() {
2596
+ if (this.closed || this.tokenRefreshInFlight || !this.opts.onTokenRefresh) return;
2597
+ this.tokenRefreshInFlight = true;
2598
+ try {
2599
+ const next = await this.opts.onTokenRefresh();
2600
+ if (!next?.token || !Number.isFinite(next.expiresAt)) throw new Error("invalid_token_refresh_result");
2601
+ this.setToken(next.token, next.expiresAt);
2602
+ } catch (err) {
2603
+ this.opts.onError?.(err);
2604
+ if (!this.closed) {
2605
+ this.tokenRefreshTimer = setTimeout(() => {
2606
+ this.tokenRefreshTimer = null;
2607
+ void this.rotateToken();
2608
+ }, 3e4);
2609
+ }
2610
+ } finally {
2611
+ this.tokenRefreshInFlight = false;
2612
+ }
2613
+ }
2614
+ /** Bulk block the given labels (or the current selection when omitted). */
2615
+ async block(labels, opts = {}) {
2616
+ const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "free");
2617
+ if (!targets.length) return;
2618
+ const releaseAt = opts.releaseAt ?? this.releaseAt ?? void 0;
2619
+ for (const l of targets) this.setSeatLocal(l, "blocked");
2620
+ try {
2621
+ await this.api.block(this.key, targets, { ...opts, releaseAt });
2622
+ this.clearSelection();
2623
+ this.done("block", targets, releaseAt ? `Blocked ${targets.length} \u2014 auto-release ${new Date(releaseAt).toLocaleString()}.` : `Blocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
2624
+ } catch (err) {
2625
+ for (const l of targets) this.setSeatLocal(l, "free");
2626
+ this.toastErr(err instanceof ManageApiError && err.status === 409 ? "Some seats were just taken. Try again." : "Couldn't block those seats.");
2627
+ this.opts.onError?.(err);
2628
+ }
2629
+ }
2630
+ async unblock(labels) {
2631
+ const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "blocked");
2632
+ if (!targets.length) return;
2633
+ for (const l of targets) this.setSeatLocal(l, "free");
2634
+ try {
2635
+ await this.api.unblock(this.key, targets);
2636
+ this.clearSelection();
2637
+ this.done("unblock", targets, `Unblocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
2638
+ } catch (err) {
2639
+ for (const l of targets) this.setSeatLocal(l, "blocked");
2640
+ this.toastErr("Couldn't unblock those seats.");
2641
+ this.opts.onError?.(err);
2642
+ }
2643
+ }
2644
+ async unblockAll() {
2645
+ const blocked = [...this.status.entries()].filter(([, s]) => s === "blocked").map(([l]) => l);
2646
+ if (!blocked.length) return;
2647
+ for (const l of blocked) this.setSeatLocal(l, "free");
2648
+ try {
2649
+ const res = await this.api.unblockAll(this.key);
2650
+ this.done("unblockAll", blocked, `Unblocked ${res.freed} seat${res.freed === 1 ? "" : "s"}.`);
2651
+ } catch (err) {
2652
+ await this.resnapshot();
2653
+ this.toastErr("Couldn't mark everything for sale.");
2654
+ this.opts.onError?.(err);
2655
+ }
2656
+ }
2657
+ /** Cancel bookings (BOOKED → free), guarded by the original booking ref. */
2658
+ async cancelBooking(labels, bookingRef) {
2659
+ const targets = labels.filter((l) => this.status.get(l) === "booked");
2660
+ if (!targets.length || !bookingRef) return;
2661
+ for (const l of targets) this.setSeatLocal(l, "free");
2662
+ try {
2663
+ await this.api.unbook(this.key, targets, bookingRef);
2664
+ this.clearSelection();
2665
+ this.done("cancelBooking", targets, `Cancelled ${targets.length} booking${targets.length === 1 ? "" : "s"}.`);
2666
+ } catch (err) {
2667
+ for (const l of targets) this.setSeatLocal(l, "booked");
2668
+ this.toastErr("Couldn't cancel that booking. Check the reference.");
2669
+ this.opts.onError?.(err);
2670
+ }
2671
+ }
2672
+ selectAll() {
2673
+ const seats = this.renderer?.selectAllSelectable() ?? [];
2674
+ this.syncSelection();
2675
+ return seats;
2676
+ }
2677
+ selectSection(sectionId) {
2678
+ if (!this.renderer) return [];
2679
+ const seats = this.renderer.getSelectableInSection(sectionId);
2680
+ this.renderer.selectByLabels(seats.map((s) => s.label));
2681
+ this.syncSelection();
2682
+ return this.renderer.getSelection();
2683
+ }
2684
+ selectByLabels(labels) {
2685
+ const seats = this.renderer?.selectByLabels(labels) ?? [];
2686
+ this.syncSelection();
2687
+ return seats;
2688
+ }
2689
+ clearSelection() {
2690
+ this.renderer?.clearSelection();
2691
+ this.syncSelection();
2692
+ }
2693
+ getSelection() {
2694
+ return this.renderer?.getSelection() ?? [];
2695
+ }
2696
+ getReport() {
2697
+ return this.api.report(this.key).then((report) => {
2698
+ this.applyReportRevenue(report);
2699
+ return report;
2700
+ });
2701
+ }
2702
+ getControlRoomSnapshot(windowMinutes = this.trendWindowMinutes) {
2703
+ return this.setTrendWindow(windowMinutes);
2704
+ }
2705
+ getLog(opts = {}) {
2706
+ return this.api.log(this.key, opts);
2707
+ }
2708
+ async setHoldTtl(ms) {
2709
+ try {
2710
+ await this.api.setHoldTtl(this.key, ms);
2711
+ this.done("setHoldTtl", [], ms ? `Checkout window set to ${Math.round(ms / 6e4)} min.` : "Checkout window reset.");
2712
+ } catch (err) {
2713
+ this.toastErr("Couldn't update the checkout window.");
2714
+ this.opts.onError?.(err);
2715
+ }
2716
+ }
2717
+ /** M2 — box-office booking from free seats. Stubbed (route is session-only today). */
2718
+ boxBook(_labels, _bookingRef) {
2719
+ this.toastErr("Box office ships in a later milestone.");
2720
+ return Promise.resolve();
2721
+ }
2722
+ zoomToFit() {
2723
+ this.renderer?.zoomToFit();
2724
+ }
2725
+ destroy() {
2726
+ this.closed = true;
2727
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
2728
+ if (this.feedTimer) clearInterval(this.feedTimer);
2729
+ if (this.toastTimer) clearTimeout(this.toastTimer);
2730
+ if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
2731
+ if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
2732
+ this.layoutObserver?.disconnect();
2733
+ this.layoutObserver = null;
2734
+ this.root?.removeEventListener("keydown", this.onKeyDown);
2735
+ if (typeof document !== "undefined") document.removeEventListener("fullscreenchange", this.onFullscreenChange);
2736
+ if (this.ws) {
2737
+ try {
2738
+ this.ws.close();
2739
+ } catch {
2740
+ }
2741
+ this.ws = null;
2742
+ }
2743
+ this.renderer?.destroy();
2744
+ this.renderer = null;
2745
+ if (this.root && this.root.parentNode === this.host) this.host.removeChild(this.root);
2746
+ }
2747
+ // ---- renderer lifecycle ---------------------------------------------------
2748
+ buildRenderer() {
2749
+ if (!this.doc) return;
2750
+ const block = this.mode === "block";
2751
+ const inspect = this.mode === "inspect";
2752
+ this.renderer = new import_core3.SeatmapRenderer(this.mapHost, {
2753
+ manageMode: true,
2754
+ marqueeSelect: block,
2755
+ maxSelection: 1e6,
2756
+ selectableStatuses: block ? ["free", "not_for_sale"] : inspect ? ["free", "held", "booked", "not_for_sale"] : [],
2757
+ currency: this.currency,
2758
+ onSelect: (seat) => this.handleSeatSelect(seat),
2759
+ onDeselect: () => this.syncSelection(),
2760
+ onMarquee: () => this.syncSelection(),
2761
+ onViewChange: () => this.updateZoomHint()
2762
+ });
2763
+ this.renderer.setChart(this.doc);
2764
+ this.repaintAll();
2765
+ this.applyHeatOverlay();
2766
+ this.updateZoomHint();
2767
+ }
2768
+ updateRendererInteraction() {
2769
+ const block = this.mode === "block";
2770
+ const inspect = this.mode === "inspect";
2771
+ this.renderer?.setManageInteraction({
2772
+ manageMode: true,
2773
+ marqueeSelect: block,
2774
+ maxSelection: 1e6,
2775
+ selectableStatuses: block ? ["free", "not_for_sale"] : inspect ? ["free", "held", "booked", "not_for_sale"] : []
2776
+ });
2777
+ this.updateZoomHint();
2778
+ }
2779
+ handleSeatSelect(seat) {
2780
+ if (this.mode === "inspect") {
2781
+ const others = this.getSelection().filter((selected) => selected.id !== seat.id).map((selected) => selected.id);
2782
+ if (others.length) this.renderer?.deselect(others);
2783
+ }
2784
+ this.syncSelection();
2785
+ }
2786
+ repaintAll() {
2787
+ const r = this.renderer;
2788
+ if (!r) return;
2789
+ if (this.allIds.length) r.setStatus(this.allIds, "free");
2790
+ const byStatus = { free: [], held: [], booked: [], not_for_sale: [] };
2791
+ for (const [label, st] of this.status.entries()) {
2792
+ const id = this.labelToId.get(label);
2793
+ if (id) byStatus[toRenderStatus(st)].push(id);
2794
+ }
2795
+ ["held", "booked", "not_for_sale"].forEach((st) => {
2796
+ if (byStatus[st].length) r.setStatus(byStatus[st], st);
2797
+ });
2798
+ }
2799
+ // ---- realtime -------------------------------------------------------------
2800
+ connect() {
2801
+ if (this.closed) return;
2802
+ let ws;
2803
+ try {
2804
+ ws = new WebSocket(this.api.socketUrl(this.key));
2805
+ } catch {
2806
+ this.scheduleReconnect();
2807
+ return;
2808
+ }
2809
+ this.ws = ws;
2810
+ ws.onopen = () => {
2811
+ this.attempt = 0;
2812
+ this.setLive(true);
2813
+ void this.resnapshot().then(() => this.scheduleRevenueRefresh(0));
2814
+ };
2815
+ ws.onmessage = (e) => this.onMessage(e);
2816
+ ws.onclose = () => {
2817
+ if (this.ws === ws) this.ws = null;
2818
+ this.setLive(false);
2819
+ this.scheduleReconnect();
2820
+ };
2821
+ ws.onerror = () => {
2822
+ try {
2823
+ ws.close();
2824
+ } catch {
2825
+ }
2826
+ };
2827
+ }
2828
+ scheduleReconnect() {
2829
+ if (this.closed || this.reconnectTimer) return;
2830
+ const delay = Math.min(1e3 * 2 ** Math.min(this.attempt++, 5), 15e3);
2831
+ this.reconnectTimer = setTimeout(() => {
2832
+ this.reconnectTimer = null;
2833
+ this.connect();
2834
+ }, delay);
2835
+ }
2836
+ onMessage(e) {
2837
+ let msg;
2838
+ try {
2839
+ msg = JSON.parse(typeof e.data === "string" ? e.data : "");
2840
+ } catch {
2841
+ return;
2842
+ }
2843
+ if (!msg || typeof msg !== "object") return;
2844
+ const m = msg;
2845
+ if (m.type === "presence") {
2846
+ if (this.controlRoomSnapshot && typeof m.shoppingSessions === "number" && typeof m.activeHolds === "number") {
2847
+ this.controlRoomSnapshot = {
2848
+ ...this.controlRoomSnapshot,
2849
+ presence: { shoppingSessions: m.shoppingSessions, activeHolds: m.activeHolds }
2850
+ };
2851
+ this.lastSyncedAt = Date.now();
2852
+ this.recomputeTallies();
2853
+ this.paintMonitorInsights();
2854
+ this.opts.onControlRoom?.(this.controlRoomSnapshot);
2855
+ }
2856
+ return;
2857
+ }
2858
+ if (m.type === "hidden") return;
2859
+ if (m.seats && typeof m.seats === "object") {
2860
+ this.applySnapshot(m.seats);
2861
+ } else if (Array.isArray(m.changes)) {
2862
+ const ids = [];
2863
+ const groups = /* @__PURE__ */ new Map();
2864
+ for (const ch of m.changes) {
2865
+ const st = ["free", "held", "booked", "blocked"].includes(ch.status) ? ch.status : "free";
2866
+ const prev = this.status.get(ch.label) ?? "free";
2867
+ if (prev === st) continue;
2868
+ this.status.set(ch.label, st);
2869
+ const id = this.labelToId.get(ch.label);
2870
+ if (id) {
2871
+ this.renderer?.setStatus([id], toRenderStatus(st));
2872
+ this.flash(id, st);
2873
+ ids.push(id);
2874
+ }
2875
+ const verb = this.verbFor(prev, st);
2876
+ const groupKey = `${verb}:${st}`;
2877
+ const group = groups.get(groupKey) ?? { labels: [], verb, status: st };
2878
+ group.labels.push(ch.label);
2879
+ groups.set(groupKey, group);
2880
+ }
2881
+ for (const group of groups.values()) this.pushActivity(group.labels, group.verb, group.status);
2882
+ if (ids.length) {
2883
+ this.lastSyncedAt = Date.now();
2884
+ this.afterPaint();
2885
+ }
2886
+ this.recomputeTallies();
2887
+ if (ids.length) this.scheduleRevenueRefresh();
2888
+ }
2889
+ }
2890
+ async resnapshot() {
2891
+ try {
2892
+ const objs = await this.api.objects(this.key);
2893
+ this.applySnapshot(objs.seats);
2894
+ } catch {
2895
+ }
2896
+ }
2897
+ applySnapshot(seats) {
2898
+ const next = /* @__PURE__ */ new Map();
2899
+ for (const [label, st] of Object.entries(seats)) {
2900
+ next.set(label, ["free", "held", "booked", "blocked"].includes(st) ? st : "free");
2901
+ }
2902
+ this.status = next;
2903
+ this.lastSyncedAt = Date.now();
2904
+ this.repaintAll();
2905
+ this.afterPaint();
2906
+ this.recomputeTallies();
2907
+ }
2908
+ /** Optimistic local write shared by delta stream + organizer actions. */
2909
+ setSeatLocal(label, st) {
2910
+ this.status.set(label, st);
2911
+ const id = this.labelToId.get(label);
2912
+ if (id) this.renderer?.setStatus([id], toRenderStatus(st));
2913
+ this.afterPaint();
2914
+ this.recomputeTallies();
2915
+ }
2916
+ /** Keep the canvas painting on hidden/occluded tabs (war-room second monitor). */
2917
+ afterPaint() {
2918
+ if (this.keepLive && typeof document !== "undefined" && document.hidden) {
2919
+ this.renderer?.forceDraw();
2920
+ }
2921
+ }
2922
+ flash(id, st) {
2923
+ if (st === "held") this.renderer?.flashSeat(id, "#f4b740");
2924
+ else if (st === "booked") this.renderer?.flashSeat(id, "#22a06b");
2925
+ }
2926
+ // ---- tallies + feed -------------------------------------------------------
2927
+ applyReportRevenue(report) {
2928
+ this.authoritativeGrossRevenue = report.report.byCategory.reduce(
2929
+ (sum, row) => sum + (Number.isFinite(row.bookedRevenue) ? row.bookedRevenue : 0),
2930
+ 0
2931
+ );
2932
+ this.revenueStatus = "current";
2933
+ this.recomputeTallies();
2934
+ }
2935
+ async refreshControlRoom() {
2936
+ const request2 = ++this.revenueRequest;
2937
+ try {
2938
+ const snapshot = await this.api.controlRoom(this.key, this.trendWindowMinutes);
2939
+ if (request2 === this.revenueRequest) {
2940
+ this.controlRoomSnapshot = snapshot;
2941
+ this.lastSyncedAt = Date.now();
2942
+ this.authoritativeGrossRevenue = snapshot.revenue.gross;
2943
+ this.currency = snapshot.currency;
2944
+ this.revenueStatus = "current";
2945
+ this.recomputeTallies();
2946
+ this.applyHeatOverlay();
2947
+ this.paintMonitorInsights();
2948
+ this.opts.onControlRoom?.(snapshot);
2949
+ }
2950
+ return snapshot;
2951
+ } catch (err) {
2952
+ if (request2 === this.revenueRequest) {
2953
+ this.revenueStatus = "stale";
2954
+ this.recomputeTallies();
2955
+ }
2956
+ throw err;
2957
+ }
2958
+ }
2959
+ scheduleRevenueRefresh(delay = 140) {
2960
+ this.revenueStatus = "stale";
2961
+ this.recomputeTallies();
2962
+ if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
2963
+ this.revenueRefreshTimer = setTimeout(() => {
2964
+ this.revenueRefreshTimer = null;
2965
+ void this.refreshControlRoom().catch((err) => this.opts.onError?.(err));
2966
+ }, delay);
2967
+ }
2968
+ recomputeTallies() {
2969
+ const t3 = {
2970
+ free: 0,
2971
+ held: 0,
2972
+ booked: 0,
2973
+ blocked: 0,
2974
+ total: this.allIds.length,
2975
+ capacityPct: 0,
2976
+ sellThroughPct: 0,
2977
+ grossRevenue: this.authoritativeGrossRevenue,
2978
+ revenueStatus: this.revenueStatus,
2979
+ currency: this.currency
2980
+ };
2981
+ let nonFree = 0;
2982
+ for (const st of this.status.values()) {
2983
+ t3[st] += 1;
2984
+ if (st !== "free") nonFree += 1;
2985
+ }
2986
+ t3.free = Math.max(0, t3.total - nonFree);
2987
+ t3.capacityPct = t3.total ? Math.round(t3.booked / t3.total * 100) : 0;
2988
+ const sellable = t3.total - t3.blocked;
2989
+ t3.sellThroughPct = sellable > 0 ? Math.round(t3.booked / sellable * 100) : 0;
2990
+ this.paintKpis(t3);
2991
+ if (this.mode === "view") {
2992
+ this.paintLegend(t3);
2993
+ this.paintMonitorInsights();
2994
+ } else if (this.mode === "inspect") this.renderInspectRail(this.getSelection());
2995
+ this.opts.onTallies?.(t3);
2996
+ }
2997
+ verbFor(prev, next) {
2998
+ if (next === "held") return "held";
2999
+ if (next === "booked") return "booked";
3000
+ if (next === "blocked") return "blocked";
3001
+ if (next === "free") return prev === "blocked" ? "unblocked" : prev === "booked" ? "cancelled" : "released";
3002
+ return next;
3003
+ }
3004
+ pushActivity(labels, verb, status, at = Date.now()) {
3005
+ const label = labels[0];
3006
+ if (!label) return;
3007
+ const item = {
3008
+ id: `${label}:${at}:${Math.random().toString(36).slice(2, 6)}`,
3009
+ at,
3010
+ label,
3011
+ labels: [...labels],
3012
+ count: labels.length,
3013
+ verb,
3014
+ status
3015
+ };
3016
+ this.feed.unshift(item);
3017
+ if (this.feed.length > FEED_CAP) this.feed.length = FEED_CAP;
3018
+ if (this.mode === "view") this.paintFeed();
3019
+ this.opts.onActivity?.(item);
3020
+ }
3021
+ seedFeed(entries) {
3022
+ const verbByAction = {
3023
+ hold: "held",
3024
+ book: "booked",
3025
+ release: "released",
3026
+ expire: "expired",
3027
+ block: "blocked",
3028
+ unblock: "unblocked"
3029
+ };
3030
+ const stByAction = {
3031
+ hold: "held",
3032
+ book: "booked",
3033
+ release: "free",
3034
+ expire: "free",
3035
+ block: "blocked",
3036
+ unblock: "free"
3037
+ };
3038
+ for (const e of entries) {
3039
+ const label = e.labels[0];
3040
+ if (!label) continue;
3041
+ const item = {
3042
+ id: `log:${e.id}`,
3043
+ at: e.at,
3044
+ label,
3045
+ labels: [...e.labels],
3046
+ count: e.labels.length,
3047
+ verb: verbByAction[e.action] ?? e.action,
3048
+ status: stByAction[e.action] ?? "free"
3049
+ };
3050
+ this.feed.push(item);
3051
+ this.opts.onActivity?.(item);
3052
+ }
3053
+ this.feed.sort((a, b) => b.at - a.at);
3054
+ if (this.feed.length > FEED_CAP) this.feed.length = FEED_CAP;
3055
+ if (this.mode === "view") this.paintFeed();
3056
+ }
3057
+ startFeedClock() {
3058
+ this.feedTimer = setInterval(() => {
3059
+ if (this.mode === "view") {
3060
+ this.paintFeed();
3061
+ this.paintMonitorInsights();
3062
+ }
3063
+ }, 1e4);
3064
+ }
3065
+ // ---- selection ------------------------------------------------------------
3066
+ selectionLabels() {
3067
+ return this.getSelection().map((s) => s.label);
3068
+ }
3069
+ syncSelection() {
3070
+ const seats = this.getSelection();
3071
+ if (this.mode === "block") this.paintSelBar(seats);
3072
+ else if (this.mode === "inspect") this.renderInspectRail(seats);
3073
+ this.opts.onSelectionChange?.(seats);
3074
+ }
3075
+ // ---- DOM: chrome ----------------------------------------------------------
3076
+ buildChrome() {
3077
+ const root = document.createElement("div");
3078
+ root.className = "slm";
3079
+ root.tabIndex = 0;
3080
+ root.setAttribute("role", "region");
3081
+ root.setAttribute("aria-label", "SeatLayer live control room");
3082
+ const vars = themeVars(this.opts.theme);
3083
+ for (const [k, v] of Object.entries(vars)) root.style.setProperty(k, v);
3084
+ root.innerHTML = `
3085
+ <div class="slm-bar">
3086
+ <div class="slm-modes" data-ref="modes">
3087
+ <button class="slm-mode" data-mode="view" title="Monitor (M)" aria-keyshortcuts="M">Monitor</button>
3088
+ <button class="slm-mode" data-mode="inspect" title="Inspect (I)" aria-keyshortcuts="I">Inspect</button>
3089
+ <button class="slm-mode" data-mode="block" title="Block (B)" aria-keyshortcuts="B">Block</button>
3090
+ </div>
3091
+ <span class="slm-live"><span class="slm-live-dot"></span><span data-ref="livetext">CONNECTING</span></span>
3092
+ <div class="slm-bar-actions">
3093
+ <button class="slm-barbtn" data-ref="heat" aria-pressed="false">Heat</button>
3094
+ <button class="slm-barbtn" data-ref="fullscreen" title="Full screen (F)" aria-keyshortcuts="F">Full screen</button>
3095
+ </div>
3096
+ <div class="slm-kpis" data-ref="kpis"></div>
3097
+ </div>
3098
+ <div class="slm-body">
3099
+ <div class="slm-map">
3100
+ <div class="slm-map-host" data-ref="maphost"></div>
3101
+ <div class="slm-zoomhint" data-ref="zoomhint">Zoom in to marquee-select</div>
3102
+ <div class="slm-hud"><button class="slm-hud-chip" data-ref="zfit">Zoom to fit</button></div>
3103
+ </div>
3104
+ <aside class="slm-rail"><div class="slm-railscroll" data-ref="rail"></div></aside>
3105
+ </div>
3106
+ <div class="slm-toast" data-ref="toast"></div>
3107
+ `;
3108
+ this.host.appendChild(root);
3109
+ this.root = root;
3110
+ this.updateContainerLayout();
3111
+ if (typeof ResizeObserver !== "undefined") {
3112
+ this.layoutObserver = new ResizeObserver(() => this.updateContainerLayout());
3113
+ this.layoutObserver.observe(root);
3114
+ }
3115
+ const ref = (n) => root.querySelector(`[data-ref="${n}"]`);
3116
+ this.mapHost = ref("maphost");
3117
+ this.els = {
3118
+ modes: ref("modes"),
3119
+ livetext: ref("livetext"),
3120
+ kpis: ref("kpis"),
3121
+ heat: ref("heat"),
3122
+ fullscreen: ref("fullscreen"),
3123
+ zoomhint: ref("zoomhint"),
3124
+ rail: ref("rail"),
3125
+ toast: ref("toast"),
3126
+ zfit: ref("zfit")
3127
+ };
3128
+ this.els.modes.querySelectorAll("[data-mode]").forEach((b) => b.addEventListener("click", () => this.setMode(b.dataset.mode)));
3129
+ this.els.zfit.addEventListener("click", () => this.zoomToFit());
3130
+ this.els.heat.addEventListener("click", () => this.setHeatOverlay(!this.heatEnabled));
3131
+ this.els.fullscreen.addEventListener("click", () => this.toggleFullscreen());
3132
+ root.addEventListener("keydown", this.onKeyDown);
3133
+ document.addEventListener("fullscreenchange", this.onFullscreenChange);
3134
+ this.paintModeTabs();
3135
+ this.paintHeatButton();
3136
+ this.paintFullscreenButton();
3137
+ }
3138
+ updateContainerLayout() {
3139
+ const width = this.root?.getBoundingClientRect().width || this.host.clientWidth;
3140
+ this.root?.classList.toggle("compact", width > 0 && width < 800);
3141
+ }
3142
+ buildSectionOptions() {
3143
+ if (!this.doc) return;
3144
+ try {
3145
+ const secs = (0, import_core3.computeSections)(this.doc);
3146
+ this.sectionOptions = [];
3147
+ this.sectionByObject = new Map(secs.objectToSection);
3148
+ this.sectionLabelById.clear();
3149
+ for (const s of secs.sections) {
3150
+ this.sectionOptions.push({ id: s.id, label: s.label });
3151
+ this.sectionLabelById.set(s.id, s.label);
3152
+ }
3153
+ if (secs.ungrouped) {
3154
+ this.sectionOptions.push({ id: import_core3.UNGROUPED_ID, label: secs.ungrouped.label });
3155
+ this.sectionLabelById.set(import_core3.UNGROUPED_ID, secs.ungrouped.label);
3156
+ }
3157
+ } catch {
3158
+ }
3159
+ }
3160
+ paintModeTabs() {
3161
+ this.els.modes?.querySelectorAll("[data-mode]").forEach((b) => {
3162
+ const el = b;
3163
+ el.classList.toggle("on", el.dataset.mode === this.mode);
3164
+ });
3165
+ this.root?.classList.toggle("block-mode", this.mode === "block");
3166
+ }
3167
+ paintHeatButton() {
3168
+ const button = this.els.heat;
3169
+ if (!button) return;
3170
+ button.classList.toggle("on", this.heatEnabled);
3171
+ button.setAttribute("aria-pressed", String(this.heatEnabled));
3172
+ button.textContent = this.heatEnabled ? "Heat on" : "Heat";
3173
+ }
3174
+ paintFullscreenButton() {
3175
+ if (!this.els.fullscreen) return;
3176
+ this.els.fullscreen.textContent = this.isFullscreen() ? "Exit full screen" : "Full screen";
3177
+ }
3178
+ paintTrendWindow() {
3179
+ this.els.rail?.querySelectorAll("[data-window]").forEach((button) => {
3180
+ const value = Number(button.dataset.window);
3181
+ button.classList.toggle("on", value === this.trendWindowMinutes);
3182
+ });
3183
+ }
3184
+ setLive(on) {
3185
+ this.root?.classList.toggle("live", on);
3186
+ if (this.els.livetext) this.els.livetext.textContent = on ? "LIVE" : "RECONNECTING";
3187
+ this.paintMonitorInsights();
3188
+ }
3189
+ updateZoomHint() {
3190
+ const hint = this.els.zoomhint;
3191
+ if (!hint) return;
3192
+ const show = this.mode === "block" && this.renderer?.getRung?.() !== "seats";
3193
+ hint.classList.toggle("on", !!show);
3194
+ }
3195
+ paintKpis(t3) {
3196
+ if (!this.els.kpis) return;
3197
+ const rev = t3.revenueStatus === "current" ? fmtMoney(t3.grossRevenue, t3.currency) : "\u2014";
3198
+ const presence = this.controlRoomSnapshot?.presence;
3199
+ this.els.kpis.innerHTML = [
3200
+ { n: t3.booked.toLocaleString(), l: "Sold", dot: "#22a06b" },
3201
+ { n: t3.held.toLocaleString(), l: "Held", dot: "#f4b740" },
3202
+ { n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Shopping" },
3203
+ { n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Live holds" },
3204
+ { n: t3.free.toLocaleString(), l: "Free", dot: "#6e7bff" },
3205
+ { n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac" },
3206
+ { n: `${t3.capacityPct}%`, l: "Capacity" },
3207
+ { n: rev, l: "Gross" }
3208
+ ].map((k) => `<div class="slm-kpi"><b>${k.dot ? `<span class="dot" style="background:${k.dot}"></span>` : ""}${k.n}</b><span>${k.l}</span></div>`).join("");
3209
+ }
3210
+ // ---- DOM: rails -----------------------------------------------------------
3211
+ paintRail() {
3212
+ if (this.mode === "view") this.renderViewRail();
3213
+ else if (this.mode === "inspect") this.renderInspectRail(this.getSelection());
3214
+ else this.renderBlockRail();
3215
+ this.updateZoomHint();
3216
+ }
3217
+ renderViewRail() {
3218
+ this.els.rail.innerHTML = `
3219
+ <p class="slm-eyebrow">Monitor</p>
3220
+ <p class="slm-hint">Read-only. Inventory, buyer presence and sales movement update on the same live board.</p>
3221
+ <div class="slm-health" data-ref="presence"></div>
3222
+ <div class="slm-legend" data-ref="legend"></div>
3223
+ <div class="slm-sectionhead">
3224
+ <div><p class="slm-eyebrow">Section performance</p><p class="slm-note">Exact booked revenue \xB7 net sales velocity</p></div>
3225
+ <div class="slm-windows" aria-label="Sales velocity window">
3226
+ ${[5, 15, 30, 60].map((window2) => `<button class="slm-window" data-window="${window2}">${window2}m</button>`).join("")}
3227
+ </div>
3228
+ </div>
3229
+ <div class="slm-sectionlist" data-ref="sections"></div>
3230
+ <p class="slm-eyebrow">Activity</p>
3231
+ <div class="slm-feed" data-ref="feed"></div>
3232
+ `;
3233
+ this.els.presence = this.els.rail.querySelector('[data-ref="presence"]');
3234
+ this.els.legend = this.els.rail.querySelector('[data-ref="legend"]');
3235
+ this.els.sections = this.els.rail.querySelector('[data-ref="sections"]');
3236
+ this.els.feed = this.els.rail.querySelector('[data-ref="feed"]');
3237
+ this.els.rail.querySelectorAll("[data-window]").forEach((button) => button.addEventListener("click", () => {
3238
+ const windowMinutes = Number(button.dataset.window);
3239
+ void this.setTrendWindow(windowMinutes).catch((err) => this.opts.onError?.(err));
3240
+ }));
3241
+ this.recomputeTallies();
3242
+ this.paintMonitorInsights();
3243
+ this.paintTrendWindow();
3244
+ this.paintFeed();
3245
+ }
3246
+ paintMonitorInsights() {
3247
+ if (this.mode !== "view") return;
3248
+ const snapshot = this.controlRoomSnapshot;
3249
+ if (this.els.presence) {
3250
+ const connected = this.root?.classList.contains("live");
3251
+ const sync = this.lastSyncedAt ? relTime(this.lastSyncedAt, Date.now()) : "waiting";
3252
+ this.els.presence.innerHTML = `
3253
+ <div class="slm-healthitem"><b>${snapshot ? snapshot.presence.shoppingSessions.toLocaleString() : "\u2014"}</b><span>Buyer sessions</span></div>
3254
+ <div class="slm-healthitem"><b>${snapshot ? snapshot.presence.activeHolds.toLocaleString() : "\u2014"}</b><span>Active holds</span></div>
3255
+ <div class="slm-healthitem"><b>${connected ? "Healthy" : "Reconnecting"}</b><span>Live connection</span></div>
3256
+ <div class="slm-healthitem"><b>${sync}</b><span>Last sync</span></div>`;
3257
+ }
3258
+ if (!this.els.sections) return;
3259
+ if (!snapshot) {
3260
+ this.els.sections.innerHTML = '<div class="slm-empty">Loading authoritative section metrics\u2026</div>';
3261
+ return;
3262
+ }
3263
+ const velocity = new Map(snapshot.velocity.bySection.map((row) => [row.sectionId, row]));
3264
+ const rows = [...snapshot.revenue.bySection].sort((a, b) => {
3265
+ const av = velocity.get(a.sectionId)?.netBooked ?? 0;
3266
+ const bv = velocity.get(b.sectionId)?.netBooked ?? 0;
3267
+ return bv - av || b.bookedRevenue - a.bookedRevenue;
3268
+ });
3269
+ this.els.sections.innerHTML = rows.length ? rows.map((row) => {
3270
+ const speed = velocity.get(row.sectionId);
3271
+ const net = speed?.netBooked ?? 0;
3272
+ const netLabel = `${net > 0 ? "+" : ""}${net}`;
3273
+ const trend = speed?.trend === "rising" || speed?.trend === "cooling" ? speed.trend : "steady";
3274
+ return `<div class="slm-sectionrow">
3275
+ <div class="slm-sectiontop"><span>${esc(row.sectionLabel)}</span><span>${fmtMoney(row.bookedRevenue, snapshot.currency)}</span></div>
3276
+ <div class="slm-sectionmeta"><span>${row.booked.toLocaleString()}/${row.total.toLocaleString()} sold \xB7 ${netLabel} in ${snapshot.velocity.windowMinutes}m</span><span class="slm-trend ${trend}">${trend}</span></div>
3277
+ </div>`;
3278
+ }).join("") : '<div class="slm-empty">No section metrics are available for this chart.</div>';
3279
+ this.paintTrendWindow();
3280
+ }
3281
+ applyHeatOverlay() {
3282
+ const snapshot = this.controlRoomSnapshot;
3283
+ if (!this.heatEnabled || !snapshot) {
3284
+ this.renderer?.setSectionHeat(null);
3285
+ return;
3286
+ }
3287
+ const capacity = new Map(snapshot.revenue.bySection.map((row) => [row.sectionId, Math.max(1, row.total)]));
3288
+ const rates = snapshot.velocity.bySection.map((row) => ({
3289
+ sectionId: row.sectionId,
3290
+ rate: Math.max(0, row.netBooked) / (capacity.get(row.sectionId) ?? 1) / snapshot.velocity.windowMinutes
3291
+ }));
3292
+ const max = Math.max(0, ...rates.map((row) => row.rate));
3293
+ const scores = {};
3294
+ for (const row of rates) scores[row.sectionId] = max > 0 ? Math.sqrt(row.rate / max) : 0;
3295
+ this.renderer?.setSectionHeat(scores);
3296
+ }
3297
+ renderInspectRail(seats) {
3298
+ const seat = seats[seats.length - 1];
3299
+ if (!seat) {
3300
+ this.els.rail.innerHTML = `
3301
+ <p class="slm-eyebrow">Inspect</p>
3302
+ <p class="slm-hint">Select any seat to see its live inventory context. Inspect is read-only and never changes availability.</p>
3303
+ <div class="slm-empty">Choose a seat on the map.</div>`;
3304
+ return;
3305
+ }
3306
+ const status = this.status.get(seat.label) ?? "free";
3307
+ const statusLabel = { free: "Free", held: "Held", booked: "Booked", blocked: "Blocked" };
3308
+ const sectionId = this.sectionByObject.get(seat.rowId) ?? import_core3.UNGROUPED_ID;
3309
+ const sectionLabel = this.sectionLabelById.get(sectionId) ?? "Other seats";
3310
+ const category = this.doc?.categories.find((item) => item.key === seat.categoryKey);
3311
+ const sectionMetric = this.controlRoomSnapshot?.revenue.bySection.find((row) => row.sectionId === sectionId);
3312
+ this.els.rail.innerHTML = `
3313
+ <p class="slm-eyebrow">Inspect</p>
3314
+ <p class="slm-hint">Live inventory context. Booked seats remain read-only; order and payment actions belong to the host commerce system.</p>
3315
+ <div class="slm-inspect-card">
3316
+ <div class="slm-inspect-label">${esc(seat.label)}</div>
3317
+ <div class="slm-inspect-grid">
3318
+ <div><span>Status</span><b>${statusLabel[status]}</b></div>
3319
+ <div><span>Section</span><b>${esc(sectionLabel)}</b></div>
3320
+ <div><span>Row</span><b>${esc(seat.rowId)}</b></div>
3321
+ <div><span>Category</span><b>${esc(category?.label ?? seat.categoryKey)}</b></div>
3322
+ <div><span>Section sold</span><b>${sectionMetric ? `${sectionMetric.booked}/${sectionMetric.total}` : "\u2014"}</b></div>
3323
+ <div><span>Section revenue</span><b>${sectionMetric && this.controlRoomSnapshot ? fmtMoney(sectionMetric.bookedRevenue, this.controlRoomSnapshot.currency) : "\u2014"}</b></div>
3324
+ </div>
3325
+ </div>`;
3326
+ }
3327
+ paintLegend(t3) {
3328
+ if (!this.els.legend) return;
3329
+ this.els.legend.innerHTML = LEGEND.map((l) => `<div class="slm-legrow"><span class="slm-legdot" style="background:${l.color}"></span>
3330
+ <span class="slm-leglabel">${l.label}</span><span class="slm-legcount">${t3[l.key].toLocaleString()}</span></div>`).join("");
3331
+ }
3332
+ paintFeed() {
3333
+ if (!this.els.feed) return;
3334
+ if (!this.feed.length) {
3335
+ this.els.feed.innerHTML = `<div class="slm-empty">No activity yet \u2014 it'll stream in live.</div>`;
3336
+ return;
3337
+ }
3338
+ const now = Date.now();
3339
+ const color = { free: "#6e7bff", held: "#f4b740", booked: "#22a06b", blocked: "#8b94ac" };
3340
+ this.els.feed.innerHTML = this.feed.map((a) => {
3341
+ const extra = a.count > 1 ? ` +${a.count - 1}` : "";
3342
+ return `<div class="slm-feedrow"><span class="slm-feeddot" style="background:${color[a.status]}"></span>
3343
+ <span class="slm-feedtext">${a.count === 1 ? "Seat" : "Seats"} <b>${esc(a.label)}${extra}</b> ${esc(a.verb)}</span>
3344
+ <span class="slm-feedtime">${relTime(a.at, now)}</span></div>`;
3345
+ }).join("");
3346
+ }
3347
+ renderBlockRail() {
3348
+ const cats = this.doc?.categories ?? [];
3349
+ const catChips = cats.map((c) => `<button class="slm-chip" data-cat="${esc(c.key)}"><span class="dot" style="background:${esc(c.color ?? "#6e7bff")}"></span>${esc(c.label ?? c.key)}</button>`).join("");
3350
+ const sectionField = this.sectionOptions.length ? `<div class="slm-field"><label>Select a whole section</label>
3351
+ <select class="slm-select" data-ref="section"><option value="">Choose a section\u2026</option>
3352
+ ${this.sectionOptions.map((s) => `<option value="${esc(s.id)}">${esc(s.label)}</option>`).join("")}</select></div>` : "";
3353
+ this.els.rail.innerHTML = `
3354
+ <p class="slm-eyebrow">Block &amp; unblock</p>
3355
+ <p class="slm-hint">Drag a box on the map to marquee-select, \u2318A for all, or pick a category/section. Booked and held inventory is never actionable here.</p>
3356
+ <div class="slm-selbar"><span class="slm-selnum" data-ref="selnum">0</span><span class="slm-sellabel">selected</span></div>
3357
+ <div class="slm-row">
3358
+ <button class="slm-btn" data-ref="doblock" disabled>Block</button>
3359
+ <button class="slm-btn ghost" data-ref="dounblock" disabled>Unblock</button>
3360
+ </div>
3361
+ <div class="slm-row">
3362
+ <button class="slm-btn ghost" data-ref="selall">Select all</button>
3363
+ <button class="slm-btn ghost" data-ref="clearsel">Clear</button>
3364
+ </div>
3365
+ <p class="slm-eyebrow" style="margin-top:8px">By category</p>
3366
+ <div class="slm-chiprow">${catChips || '<span class="slm-empty">No categories.</span>'}</div>
3367
+ ${sectionField}
3368
+ <div class="slm-field">
3369
+ <label>Auto-release blocks at (optional)</label>
3370
+ <input type="datetime-local" class="slm-input" data-ref="release" />
3371
+ <p class="slm-note" data-ref="releasenote">Leave empty to block permanently.</p>
3372
+ </div>
3373
+ <div class="slm-row"><button class="slm-btn ghost" data-ref="markall" style="flex:1">Mark everything for sale</button></div>
3374
+ `;
3375
+ const r = (n) => this.els.rail.querySelector(`[data-ref="${n}"]`);
3376
+ this.els.selnum = r("selnum");
3377
+ this.els.doblock = r("doblock");
3378
+ this.els.dounblock = r("dounblock");
3379
+ r("doblock").addEventListener("click", () => void this.block());
3380
+ r("dounblock").addEventListener("click", () => void this.unblock());
3381
+ r("selall").addEventListener("click", () => this.selectAll());
3382
+ r("clearsel").addEventListener("click", () => this.clearSelection());
3383
+ r("markall").addEventListener("click", () => void this.unblockAll());
3384
+ this.els.rail.querySelectorAll("[data-cat]").forEach((b) => b.addEventListener("click", () => this.selectCategory(b.dataset.cat)));
3385
+ const sectionSel = this.els.rail.querySelector('[data-ref="section"]');
3386
+ sectionSel?.addEventListener("change", () => {
3387
+ if (sectionSel.value) {
3388
+ this.selectSection(sectionSel.value);
3389
+ sectionSel.value = "";
3390
+ }
3391
+ });
3392
+ const rel = r("release");
3393
+ rel.addEventListener("change", () => {
3394
+ const ms = rel.value ? new Date(rel.value).getTime() : NaN;
3395
+ this.releaseAt = Number.isFinite(ms) && ms > Date.now() ? ms : null;
3396
+ const note = r("releasenote");
3397
+ note.textContent = this.releaseAt ? `New blocks auto-release ${new Date(this.releaseAt).toLocaleString()}.` : rel.value ? "Pick a time in the future." : "Leave empty to block permanently.";
3398
+ });
3399
+ this.paintSelBar(this.getSelection());
3400
+ }
3401
+ selectCategory(catKey) {
3402
+ const labels = [];
3403
+ for (const [label, seat] of this.labelToSeat.entries()) if (seat.categoryKey === catKey) labels.push(label);
3404
+ this.selectByLabels(labels);
3405
+ }
3406
+ paintSelBar(seats) {
3407
+ if (!this.els.selnum) return;
3408
+ this.els.selnum.textContent = seats.length.toLocaleString();
3409
+ const hasFree = seats.some((s) => this.status.get(s.label) === "free");
3410
+ const hasBlocked = seats.some((s) => this.status.get(s.label) === "blocked");
3411
+ this.els.doblock.disabled = !hasFree;
3412
+ this.els.dounblock.disabled = !hasBlocked;
3413
+ }
3414
+ // ---- toast / done / fail --------------------------------------------------
3415
+ done(action, labels, msg) {
3416
+ this.toastOk(msg);
3417
+ if (action !== "setHoldTtl") this.scheduleRevenueRefresh(0);
3418
+ this.opts.onActionComplete?.({ action, labels, count: labels.length });
3419
+ }
3420
+ toastOk(msg) {
3421
+ this.toast(msg, "ok");
3422
+ }
3423
+ toastErr(msg) {
3424
+ this.toast(msg, "err");
3425
+ }
3426
+ toast(msg, kind) {
3427
+ const el = this.els.toast;
3428
+ if (!el) return;
3429
+ el.textContent = msg;
3430
+ el.className = `slm-toast on ${kind}`;
3431
+ if (this.toastTimer) clearTimeout(this.toastTimer);
3432
+ this.toastTimer = setTimeout(() => {
3433
+ el.className = "slm-toast";
3434
+ }, 3200);
3435
+ }
3436
+ fail(err) {
3437
+ this.opts.onError?.(err);
3438
+ if (this.els.rail) this.els.rail.innerHTML = `<div class="slm-empty">Couldn't load this event. Check the event key and token.</div>`;
3439
+ }
3440
+ };
2152
3441
  // Annotate the CommonJS export names for ESM import in node:
2153
3442
  0 && (module.exports = {
2154
3443
  ApiError,
2155
3444
  EmbeddedDesigner,
3445
+ ManageApi,
3446
+ ManageApiError,
3447
+ SeatManager,
2156
3448
  SeatPicker,
2157
3449
  SeatingChart
2158
3450
  });