@seatlayer/js 0.16.1 → 0.18.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 +23 -0
- package/dist/index.cjs +519 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +67 -1
- package/dist/index.d.ts +67 -1
- package/dist/index.js +519 -38
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -326,16 +326,46 @@ var TYPES = /* @__PURE__ */ new Set([
|
|
|
326
326
|
"seatlayer.designer.close",
|
|
327
327
|
"seatlayer.designer.error"
|
|
328
328
|
]);
|
|
329
|
+
var DEFAULT_LOADING_TIMEOUT_MS = 2e4;
|
|
329
330
|
function resolveContainer2(container) {
|
|
330
331
|
if (typeof container !== "string") return container;
|
|
331
332
|
const element = document.querySelector(container);
|
|
332
333
|
if (!element) throw new Error(`EmbeddedDesigner container not found: ${container}`);
|
|
333
334
|
return element;
|
|
334
335
|
}
|
|
336
|
+
function causeFromCode(code) {
|
|
337
|
+
const value = (code ?? "").toLowerCase();
|
|
338
|
+
if (value.includes("expire") || value.includes("revoke") || value === "401") return "expired";
|
|
339
|
+
if (value.includes("mismatch")) return "mismatch";
|
|
340
|
+
if (value.includes("timeout")) return "timeout";
|
|
341
|
+
return "load";
|
|
342
|
+
}
|
|
343
|
+
var ERROR_COPY = {
|
|
344
|
+
expired: {
|
|
345
|
+
title: "This design session expired",
|
|
346
|
+
body: "For your security, editing sessions are short-lived. Start a fresh one to keep designing."
|
|
347
|
+
},
|
|
348
|
+
mismatch: {
|
|
349
|
+
title: "This editor doesn't match this chart",
|
|
350
|
+
body: "The session that loaded belongs to a different chart or workspace. Reopen the designer to continue."
|
|
351
|
+
},
|
|
352
|
+
timeout: {
|
|
353
|
+
title: "The designer is taking too long",
|
|
354
|
+
body: "It did not finish loading in time. This is usually a slow connection \u2014 try again."
|
|
355
|
+
},
|
|
356
|
+
load: {
|
|
357
|
+
title: "We couldn't load the designer",
|
|
358
|
+
body: "Something went wrong while opening the editor. Please try again."
|
|
359
|
+
}
|
|
360
|
+
};
|
|
335
361
|
var EmbeddedDesigner = class {
|
|
336
362
|
constructor(options) {
|
|
337
363
|
this.frame = null;
|
|
338
364
|
this.designerOrigin = "";
|
|
365
|
+
this.overlay = null;
|
|
366
|
+
this.timeoutTimer = null;
|
|
367
|
+
this.phase = "loading";
|
|
368
|
+
this.restoreContainerPosition = null;
|
|
339
369
|
this.handleMessage = (event) => {
|
|
340
370
|
if (!this.frame || event.origin !== this.designerOrigin || event.source !== this.frame.contentWindow) return;
|
|
341
371
|
if (!event.data || typeof event.data !== "object") return;
|
|
@@ -350,10 +380,15 @@ var EmbeddedDesigner = class {
|
|
|
350
380
|
message: typeof data.message === "string" ? data.message : void 0,
|
|
351
381
|
meta: data.meta
|
|
352
382
|
};
|
|
353
|
-
if (this.options.expectedChartId && message.chartId && message.chartId !== this.options.expectedChartId)
|
|
354
|
-
|
|
383
|
+
if (this.options.expectedChartId && message.chartId && message.chartId !== this.options.expectedChartId || this.options.expectedWorkspaceId && message.workspaceId && message.workspaceId !== this.options.expectedWorkspaceId) {
|
|
384
|
+
this.showError("mismatch");
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
355
387
|
switch (message.type) {
|
|
356
388
|
case "seatlayer.designer.ready":
|
|
389
|
+
this.phase = "ready";
|
|
390
|
+
this.clearTimeoutTimer();
|
|
391
|
+
this.removeOverlay();
|
|
357
392
|
this.options.onReady?.(message);
|
|
358
393
|
break;
|
|
359
394
|
case "seatlayer.designer.saved":
|
|
@@ -366,6 +401,7 @@ var EmbeddedDesigner = class {
|
|
|
366
401
|
this.options.onClose?.(message);
|
|
367
402
|
break;
|
|
368
403
|
case "seatlayer.designer.error":
|
|
404
|
+
this.showError(causeFromCode(message.code));
|
|
369
405
|
this.options.onError?.(message);
|
|
370
406
|
break;
|
|
371
407
|
}
|
|
@@ -389,9 +425,21 @@ var EmbeddedDesigner = class {
|
|
|
389
425
|
frame.style.border = "0";
|
|
390
426
|
Object.assign(frame.style, this.options.style);
|
|
391
427
|
if (this.options.className) frame.className = this.options.className;
|
|
428
|
+
const container = resolveContainer2(this.options.container);
|
|
392
429
|
window.addEventListener("message", this.handleMessage);
|
|
393
|
-
|
|
430
|
+
container.append(frame);
|
|
394
431
|
this.frame = frame;
|
|
432
|
+
this.phase = "loading";
|
|
433
|
+
if (this.loadingStateEnabled()) {
|
|
434
|
+
this.ensureContainerPositioned(container);
|
|
435
|
+
this.renderOverlay(container, "loading");
|
|
436
|
+
const timeout = this.options.loadingTimeoutMs ?? DEFAULT_LOADING_TIMEOUT_MS;
|
|
437
|
+
if (timeout > 0 && Number.isFinite(timeout)) {
|
|
438
|
+
this.timeoutTimer = setTimeout(() => {
|
|
439
|
+
if (this.phase === "loading") this.showError("timeout");
|
|
440
|
+
}, timeout);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
395
443
|
return frame;
|
|
396
444
|
}
|
|
397
445
|
/** Replace the iframe instead of assigning a new fragment to an existing one. */
|
|
@@ -404,9 +452,208 @@ var EmbeddedDesigner = class {
|
|
|
404
452
|
}
|
|
405
453
|
destroy() {
|
|
406
454
|
window.removeEventListener("message", this.handleMessage);
|
|
455
|
+
this.clearTimeoutTimer();
|
|
456
|
+
this.removeOverlay();
|
|
457
|
+
this.restoreContainerStyle();
|
|
407
458
|
this.frame?.remove();
|
|
408
459
|
this.frame = null;
|
|
409
460
|
this.designerOrigin = "";
|
|
461
|
+
this.phase = "loading";
|
|
462
|
+
}
|
|
463
|
+
loadingStateEnabled() {
|
|
464
|
+
return this.options.showLoadingState !== false;
|
|
465
|
+
}
|
|
466
|
+
clearTimeoutTimer() {
|
|
467
|
+
if (this.timeoutTimer !== null) {
|
|
468
|
+
clearTimeout(this.timeoutTimer);
|
|
469
|
+
this.timeoutTimer = null;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
ensureContainerPositioned(container) {
|
|
473
|
+
const position = getComputedStyle(container).position;
|
|
474
|
+
if (position === "static") {
|
|
475
|
+
this.restoreContainerPosition = container.style.position;
|
|
476
|
+
container.style.position = "relative";
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
restoreContainerStyle() {
|
|
480
|
+
if (this.restoreContainerPosition === null) return;
|
|
481
|
+
try {
|
|
482
|
+
resolveContainer2(this.options.container).style.position = this.restoreContainerPosition;
|
|
483
|
+
} catch {
|
|
484
|
+
}
|
|
485
|
+
this.restoreContainerPosition = null;
|
|
486
|
+
}
|
|
487
|
+
removeOverlay() {
|
|
488
|
+
this.overlay?.remove();
|
|
489
|
+
this.overlay = null;
|
|
490
|
+
}
|
|
491
|
+
showError(cause) {
|
|
492
|
+
this.phase = "error";
|
|
493
|
+
this.clearTimeoutTimer();
|
|
494
|
+
if (!this.loadingStateEnabled()) return;
|
|
495
|
+
let container;
|
|
496
|
+
try {
|
|
497
|
+
container = resolveContainer2(this.options.container);
|
|
498
|
+
} catch {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
this.renderOverlay(container, "error", cause);
|
|
502
|
+
}
|
|
503
|
+
handleTryAgain() {
|
|
504
|
+
if (this.options.onRequestRelaunch) {
|
|
505
|
+
this.options.onRequestRelaunch();
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
this.mount();
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Build (or rebuild) the overlay for the given phase. A single overlay element
|
|
512
|
+
* is reused so we never stack stale skeletons or cards.
|
|
513
|
+
*/
|
|
514
|
+
renderOverlay(container, phase, cause) {
|
|
515
|
+
this.removeOverlay();
|
|
516
|
+
const overlay = document.createElement("div");
|
|
517
|
+
overlay.setAttribute("data-seatlayer-designer-overlay", phase);
|
|
518
|
+
overlay.setAttribute("role", phase === "error" ? "alert" : "status");
|
|
519
|
+
overlay.setAttribute("aria-live", "polite");
|
|
520
|
+
Object.assign(overlay.style, {
|
|
521
|
+
position: "absolute",
|
|
522
|
+
inset: "0",
|
|
523
|
+
display: "flex",
|
|
524
|
+
alignItems: "center",
|
|
525
|
+
justifyContent: "center",
|
|
526
|
+
background: "#101625",
|
|
527
|
+
color: "#e6ebf5",
|
|
528
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
|
529
|
+
zIndex: "2",
|
|
530
|
+
overflow: "hidden"
|
|
531
|
+
});
|
|
532
|
+
if (phase === "loading") this.buildSkeleton(overlay);
|
|
533
|
+
else this.buildErrorCard(overlay, cause ?? "load");
|
|
534
|
+
container.append(overlay);
|
|
535
|
+
this.overlay = overlay;
|
|
536
|
+
}
|
|
537
|
+
buildSkeleton(overlay) {
|
|
538
|
+
const style = document.createElement("style");
|
|
539
|
+
style.textContent = `
|
|
540
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
541
|
+
@keyframes seatlayer-designer-shimmer {
|
|
542
|
+
0% { background-position: -320px 0; }
|
|
543
|
+
100% { background-position: 320px 0; }
|
|
544
|
+
}
|
|
545
|
+
[data-seatlayer-designer-overlay="loading"] .sl-shimmer {
|
|
546
|
+
animation: seatlayer-designer-shimmer 1.25s ease-in-out infinite;
|
|
547
|
+
background-size: 640px 100%;
|
|
548
|
+
}
|
|
549
|
+
}`;
|
|
550
|
+
overlay.append(style);
|
|
551
|
+
const shimmer = "linear-gradient(90deg, rgba(255,255,255,0.04) 25%, rgba(255,255,255,0.10) 37%, rgba(255,255,255,0.04) 63%)";
|
|
552
|
+
const scaffold = document.createElement("div");
|
|
553
|
+
Object.assign(scaffold.style, {
|
|
554
|
+
position: "absolute",
|
|
555
|
+
inset: "0",
|
|
556
|
+
display: "flex",
|
|
557
|
+
flexDirection: "column",
|
|
558
|
+
padding: "16px",
|
|
559
|
+
gap: "14px",
|
|
560
|
+
opacity: "0.9"
|
|
561
|
+
});
|
|
562
|
+
const bar = (styles) => {
|
|
563
|
+
const node = document.createElement("div");
|
|
564
|
+
node.className = "sl-shimmer";
|
|
565
|
+
Object.assign(node.style, {
|
|
566
|
+
background: shimmer,
|
|
567
|
+
borderRadius: "8px"
|
|
568
|
+
});
|
|
569
|
+
Object.assign(node.style, styles);
|
|
570
|
+
return node;
|
|
571
|
+
};
|
|
572
|
+
scaffold.append(bar({ height: "40px", width: "100%", flex: "0 0 auto" }));
|
|
573
|
+
const body = document.createElement("div");
|
|
574
|
+
Object.assign(body.style, {
|
|
575
|
+
display: "flex",
|
|
576
|
+
gap: "14px",
|
|
577
|
+
flex: "1 1 auto",
|
|
578
|
+
minHeight: "0"
|
|
579
|
+
});
|
|
580
|
+
body.append(bar({ width: "220px", height: "100%", flex: "0 0 auto" }));
|
|
581
|
+
body.append(bar({ flex: "1 1 auto", height: "100%" }));
|
|
582
|
+
scaffold.append(body);
|
|
583
|
+
overlay.append(scaffold);
|
|
584
|
+
const caption = document.createElement("div");
|
|
585
|
+
Object.assign(caption.style, {
|
|
586
|
+
position: "relative",
|
|
587
|
+
zIndex: "1",
|
|
588
|
+
display: "flex",
|
|
589
|
+
alignItems: "center",
|
|
590
|
+
gap: "10px",
|
|
591
|
+
padding: "10px 16px",
|
|
592
|
+
borderRadius: "999px",
|
|
593
|
+
background: "rgba(16, 22, 37, 0.72)",
|
|
594
|
+
fontSize: "13px",
|
|
595
|
+
fontWeight: "500",
|
|
596
|
+
letterSpacing: "0.01em"
|
|
597
|
+
});
|
|
598
|
+
const dot = document.createElement("span");
|
|
599
|
+
dot.className = "sl-shimmer";
|
|
600
|
+
Object.assign(dot.style, {
|
|
601
|
+
width: "9px",
|
|
602
|
+
height: "9px",
|
|
603
|
+
borderRadius: "50%",
|
|
604
|
+
background: shimmer,
|
|
605
|
+
flex: "0 0 auto"
|
|
606
|
+
});
|
|
607
|
+
caption.append(dot);
|
|
608
|
+
caption.append(document.createTextNode("Loading designer\u2026"));
|
|
609
|
+
overlay.append(caption);
|
|
610
|
+
}
|
|
611
|
+
buildErrorCard(overlay, cause) {
|
|
612
|
+
const copy = ERROR_COPY[cause];
|
|
613
|
+
const card = document.createElement("div");
|
|
614
|
+
Object.assign(card.style, {
|
|
615
|
+
maxWidth: "420px",
|
|
616
|
+
margin: "0 24px",
|
|
617
|
+
padding: "28px",
|
|
618
|
+
textAlign: "center",
|
|
619
|
+
background: "rgba(255, 255, 255, 0.03)",
|
|
620
|
+
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
621
|
+
borderRadius: "16px",
|
|
622
|
+
boxShadow: "0 12px 40px rgba(0, 0, 0, 0.35)"
|
|
623
|
+
});
|
|
624
|
+
const heading = document.createElement("h2");
|
|
625
|
+
heading.textContent = copy.title;
|
|
626
|
+
Object.assign(heading.style, {
|
|
627
|
+
margin: "0 0 8px",
|
|
628
|
+
fontSize: "17px",
|
|
629
|
+
fontWeight: "600",
|
|
630
|
+
color: "#f4f7ff"
|
|
631
|
+
});
|
|
632
|
+
const body = document.createElement("p");
|
|
633
|
+
body.textContent = copy.body;
|
|
634
|
+
Object.assign(body.style, {
|
|
635
|
+
margin: "0 0 20px",
|
|
636
|
+
fontSize: "13.5px",
|
|
637
|
+
lineHeight: "1.5",
|
|
638
|
+
color: "#aab4c8"
|
|
639
|
+
});
|
|
640
|
+
const button = document.createElement("button");
|
|
641
|
+
button.type = "button";
|
|
642
|
+
button.textContent = "Try again";
|
|
643
|
+
Object.assign(button.style, {
|
|
644
|
+
appearance: "none",
|
|
645
|
+
cursor: "pointer",
|
|
646
|
+
border: "0",
|
|
647
|
+
borderRadius: "10px",
|
|
648
|
+
padding: "10px 22px",
|
|
649
|
+
fontSize: "14px",
|
|
650
|
+
fontWeight: "600",
|
|
651
|
+
color: "#101625",
|
|
652
|
+
background: "#7aa2ff"
|
|
653
|
+
});
|
|
654
|
+
button.addEventListener("click", () => this.handleTryAgain());
|
|
655
|
+
card.append(heading, body, button);
|
|
656
|
+
overlay.append(card);
|
|
410
657
|
}
|
|
411
658
|
};
|
|
412
659
|
|
|
@@ -2966,6 +3213,8 @@ function toRenderStatus(s) {
|
|
|
2966
3213
|
var DEFAULT_API_BASE3 = "https://api.seatlayer.io";
|
|
2967
3214
|
var STYLE_ID2 = "seatlayer-manager-style";
|
|
2968
3215
|
var FEED_CAP = 80;
|
|
3216
|
+
var MAX_LIVE_SEAT_PULSES = 16;
|
|
3217
|
+
var MAX_LIVE_SECTION_PULSES = 4;
|
|
2969
3218
|
var LEGEND = [
|
|
2970
3219
|
{ key: "free", label: "Free", color: "#6e7bff" },
|
|
2971
3220
|
{ key: "held", label: "Held", color: "#f4b740" },
|
|
@@ -2991,13 +3240,21 @@ var CSS2 = `
|
|
|
2991
3240
|
@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)}}
|
|
2992
3241
|
.slm-kpis{grid-column:1/-1;display:grid;grid-template-columns:repeat(8,minmax(0,1fr));width:100%;padding-top:10px;
|
|
2993
3242
|
border-top:1px solid var(--slm-line)}
|
|
2994
|
-
.slm-kpi{display:flex;min-width:0;flex-direction:column;align-items:center;padding:0 5px;line-height:1.15;text-align:center}
|
|
3243
|
+
.slm-kpi{position:relative;display:flex;min-width:0;flex-direction:column;align-items:center;padding:0 5px;line-height:1.15;text-align:center}
|
|
2995
3244
|
.slm-kpi b{display:flex;min-width:0;align-items:baseline;justify-content:center;font-size:17px;font-weight:800;
|
|
2996
3245
|
font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
2997
3246
|
.slm-kpi span{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--slm-muted);font-weight:700}
|
|
2998
3247
|
.slm-kpi .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:baseline}
|
|
3248
|
+
.slm-kpi.changed b{animation:slm-kpi-bump .58s cubic-bezier(.2,.8,.2,1)}
|
|
3249
|
+
.slm-kpidelta{position:absolute;right:4px;top:-12px;padding:2px 5px;border-radius:999px;background:rgba(34,160,107,.17);
|
|
3250
|
+
color:#5bd39b!important;font-size:9px!important;letter-spacing:0!important;text-transform:none!important;white-space:nowrap;
|
|
3251
|
+
animation:slm-kpi-delta 1.45s ease-out both;pointer-events:none}
|
|
3252
|
+
.slm-kpidelta.down{background:rgba(244,183,64,.14);color:#f7ca6b!important}
|
|
3253
|
+
@keyframes slm-kpi-bump{0%,100%{transform:none}35%{transform:translateY(-2px) scale(1.08);text-shadow:0 0 18px rgba(255,255,255,.24)}}
|
|
3254
|
+
@keyframes slm-kpi-delta{0%{opacity:0;transform:translateY(5px)}18%,72%{opacity:1;transform:none}100%{opacity:0;transform:translateY(-5px)}}
|
|
2999
3255
|
.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}
|
|
3000
3256
|
.slm-barbtn:hover{border-color:var(--slm-muted)}
|
|
3257
|
+
.slm-barbtn.follow.on{background:rgba(34,160,107,.13);border-color:#22a06b;color:#5bd39b}
|
|
3001
3258
|
|
|
3002
3259
|
/* body */
|
|
3003
3260
|
.slm-body{display:flex;flex:1;min-height:0}
|
|
@@ -3009,6 +3266,14 @@ var CSS2 = `
|
|
|
3009
3266
|
.slm-zoomhint{position:absolute;left:50%;top:14px;transform:translateX(-50%);padding:6px 13px;border-radius:999px;
|
|
3010
3267
|
background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:700;pointer-events:none;opacity:0;transition:opacity .2s}
|
|
3011
3268
|
.slm-zoomhint.on{opacity:1}
|
|
3269
|
+
.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));
|
|
3270
|
+
padding:8px 12px;border:1px solid var(--slm-line);border-radius:999px;background:color-mix(in srgb,var(--slm-surface) 92%,transparent);
|
|
3271
|
+
box-shadow:0 10px 34px rgba(0,0,0,.32);opacity:0;transform:translate(-50%,-8px);pointer-events:none;
|
|
3272
|
+
transition:opacity .18s ease,transform .24s ease;backdrop-filter:blur(10px)}
|
|
3273
|
+
.slm-liveevent.on{opacity:1;transform:translate(-50%,0)}
|
|
3274
|
+
.slm.block-mode .slm-liveevent{top:52px}
|
|
3275
|
+
.slm-liveeventdot{width:8px;height:8px;border-radius:50%;flex:none}.slm-liveeventcopy{min-width:0;overflow:hidden;text-overflow:ellipsis;
|
|
3276
|
+
white-space:nowrap;font-size:12px;font-weight:800}.slm-liveeventhint{color:var(--slm-muted);font-size:10px;white-space:nowrap}
|
|
3012
3277
|
.slm-rail{width:320px;flex:none;border-left:1px solid var(--slm-line);display:flex;flex-direction:column;min-height:0}
|
|
3013
3278
|
.slm-railscroll{flex:1;overflow-y:auto;padding:16px}
|
|
3014
3279
|
.slm-eyebrow{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--slm-muted);font-weight:800;margin-bottom:6px}
|
|
@@ -3023,13 +3288,16 @@ var CSS2 = `
|
|
|
3023
3288
|
|
|
3024
3289
|
/* activity feed */
|
|
3025
3290
|
.slm-feed{display:flex;flex-direction:column;gap:0}
|
|
3026
|
-
.slm-feedrow{display:flex;align-items:center;gap:9px;padding:8px 2px;border-bottom:1px solid var(--slm-line);
|
|
3027
|
-
font-size:12.5px;animation:slm-in .35s ease}
|
|
3291
|
+
.slm-feedrow{display:flex!important;width:100%;align-items:center;gap:9px;padding:8px 2px!important;border-bottom:1px solid var(--slm-line)!important;
|
|
3292
|
+
border-radius:6px;font-size:12.5px;text-align:left!important;animation:slm-in .35s ease}
|
|
3293
|
+
.slm-feedrow:hover{background:rgba(255,255,255,.035)!important}
|
|
3028
3294
|
@keyframes slm-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
|
|
3029
3295
|
.slm-feeddot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
3030
3296
|
.slm-feedtext{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
3031
3297
|
.slm-feedtext b{font-weight:800}
|
|
3032
|
-
.slm-
|
|
3298
|
+
.slm-feedsection{display:block;overflow:hidden;text-overflow:ellipsis;color:var(--slm-muted);font-size:10px;font-weight:750}
|
|
3299
|
+
.slm-feedmeta{display:flex;flex:none;flex-direction:column;align-items:flex-end;gap:1px}.slm-feedtime{font-size:10px;color:var(--slm-muted);font-variant-numeric:tabular-nums}
|
|
3300
|
+
.slm-feedlocate{font-size:9.5px;color:var(--slm-accent);font-weight:800}
|
|
3033
3301
|
.slm-empty{font-size:12.5px;color:var(--slm-muted);padding:12px 0}
|
|
3034
3302
|
|
|
3035
3303
|
/* block toolbar */
|
|
@@ -3099,10 +3367,13 @@ var CSS2 = `
|
|
|
3099
3367
|
.slm-barbtn.on{background:rgba(244,183,64,.13);border-color:#f4b740;color:#f7ca6b}
|
|
3100
3368
|
.slm-sectionlist{display:flex;flex-direction:column;gap:8px;margin-top:4px}
|
|
3101
3369
|
.slm-sectionlist + .slm-eyebrow{margin-top:18px}
|
|
3102
|
-
.slm-sectionrow{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
|
|
3370
|
+
.slm-sectionrow{width:100%;padding:10px!important;border:1px solid var(--slm-line)!important;border-radius:10px;background:var(--slm-surface)!important;text-align:left!important;transition:border-color .15s ease,transform .15s ease}
|
|
3371
|
+
.slm-sectionrow:hover{border-color:var(--slm-muted)!important;transform:translateY(-1px)}
|
|
3103
3372
|
.slm-sectiontop,.slm-sectionmeta{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
3104
3373
|
.slm-sectiontop{font-size:12.5px;font-weight:800}.slm-sectionmeta{margin-top:5px;color:var(--slm-muted);font-size:11px}
|
|
3374
|
+
.slm-sectionmeta>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3105
3375
|
.slm-trend{font-size:10px;text-transform:uppercase;letter-spacing:.08em}.slm-trend.rising{color:#22a06b}.slm-trend.cooling{color:#f4b740}
|
|
3376
|
+
.slm-sectionlocate{color:var(--slm-accent);font-size:9.5px;font-weight:800}
|
|
3106
3377
|
.slm-health{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px}
|
|
3107
3378
|
.slm-healthitem{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
|
|
3108
3379
|
.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}
|
|
@@ -3129,6 +3400,10 @@ var CSS2 = `
|
|
|
3129
3400
|
.slm.compact .slm-barbtn{flex:1;padding:6px 9px}.slm.compact .slm-kpis{grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
|
|
3130
3401
|
.slm.compact .slm-kpi[data-kpi="buyers"],.slm.compact .slm-kpi[data-kpi="active-holds"],
|
|
3131
3402
|
.slm.compact .slm-kpi[data-kpi="sold-pct"],.slm.compact .slm-kpi[data-kpi="gross-sales"]{display:none}
|
|
3403
|
+
@media (prefers-reduced-motion:reduce){
|
|
3404
|
+
.slm.live .slm-live-dot,.slm-feedrow,.slm-kpi.changed b,.slm-kpidelta{animation:none!important}
|
|
3405
|
+
.slm-liveevent,.slm-sectionrow{transition:none!important}
|
|
3406
|
+
}
|
|
3132
3407
|
`;
|
|
3133
3408
|
function injectStyle() {
|
|
3134
3409
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
|
|
@@ -3187,6 +3462,8 @@ var SeatManager = class {
|
|
|
3187
3462
|
this.controlRoomSnapshot = null;
|
|
3188
3463
|
this.trendWindowMinutes = 15;
|
|
3189
3464
|
this.heatEnabled = false;
|
|
3465
|
+
this.lastKpiValues = /* @__PURE__ */ new Map();
|
|
3466
|
+
this.activeKpiDeltas = /* @__PURE__ */ new Map();
|
|
3190
3467
|
// realtime socket
|
|
3191
3468
|
this.ws = null;
|
|
3192
3469
|
this.reconnectTimer = null;
|
|
@@ -3196,6 +3473,10 @@ var SeatManager = class {
|
|
|
3196
3473
|
this.feed = [];
|
|
3197
3474
|
this.feedTimer = null;
|
|
3198
3475
|
this.toastTimer = null;
|
|
3476
|
+
this.liveEventTimer = null;
|
|
3477
|
+
this.kpiCleanupTimer = null;
|
|
3478
|
+
this.followLiveTimer = null;
|
|
3479
|
+
this.followSeatTimer = null;
|
|
3199
3480
|
this.releaseAt = null;
|
|
3200
3481
|
this.layoutObserver = null;
|
|
3201
3482
|
this.tokenExpiresAt = null;
|
|
@@ -3225,11 +3506,22 @@ var SeatManager = class {
|
|
|
3225
3506
|
else return;
|
|
3226
3507
|
event.preventDefault();
|
|
3227
3508
|
};
|
|
3509
|
+
this.onRailClick = (event) => {
|
|
3510
|
+
const target = event.target;
|
|
3511
|
+
const sectionButton = target?.closest("[data-section-focus]");
|
|
3512
|
+
if (sectionButton?.dataset.sectionFocus) {
|
|
3513
|
+
this.locateSection(sectionButton.dataset.sectionFocus);
|
|
3514
|
+
return;
|
|
3515
|
+
}
|
|
3516
|
+
const feedButton = target?.closest("[data-feed-id]");
|
|
3517
|
+
if (feedButton?.dataset.feedId) this.locateActivity(feedButton.dataset.feedId);
|
|
3518
|
+
};
|
|
3228
3519
|
this.sectionOptions = [];
|
|
3229
3520
|
this.opts = options;
|
|
3230
3521
|
this.key = options.eventKey;
|
|
3231
3522
|
this.mode = options.mode ?? "view";
|
|
3232
3523
|
this.keepLive = options.keepLiveWhileHidden ?? true;
|
|
3524
|
+
this.followLive = options.followLive ?? false;
|
|
3233
3525
|
this.currency = options.currency ?? "USD";
|
|
3234
3526
|
this.tokenExpiresAt = options.tokenExpiresAt ?? null;
|
|
3235
3527
|
this.api = new ManageApi(options.apiBase ?? DEFAULT_API_BASE3, options.token);
|
|
@@ -3286,6 +3578,19 @@ var SeatManager = class {
|
|
|
3286
3578
|
this.applyHeatOverlay();
|
|
3287
3579
|
this.paintHeatButton();
|
|
3288
3580
|
}
|
|
3581
|
+
/** Toggle opt-in camera following for new buyer hold/book events. */
|
|
3582
|
+
setFollowLive(enabled) {
|
|
3583
|
+
const changed = this.followLive !== enabled;
|
|
3584
|
+
this.followLive = enabled;
|
|
3585
|
+
if (!enabled) {
|
|
3586
|
+
if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
|
|
3587
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
3588
|
+
this.followLiveTimer = null;
|
|
3589
|
+
this.followSeatTimer = null;
|
|
3590
|
+
}
|
|
3591
|
+
this.paintFollowLiveButton();
|
|
3592
|
+
if (changed) this.opts.onFollowLiveChange?.(enabled);
|
|
3593
|
+
}
|
|
3289
3594
|
/** Change the current-vs-previous sales window and refresh the private projection. */
|
|
3290
3595
|
setTrendWindow(windowMinutes) {
|
|
3291
3596
|
const normalized = Number.isFinite(windowMinutes) ? Math.floor(windowMinutes) : 15;
|
|
@@ -3458,6 +3763,7 @@ var SeatManager = class {
|
|
|
3458
3763
|
return Promise.resolve();
|
|
3459
3764
|
}
|
|
3460
3765
|
zoomToFit() {
|
|
3766
|
+
this.renderer?.clearSectionFocus();
|
|
3461
3767
|
this.renderer?.zoomToFit();
|
|
3462
3768
|
}
|
|
3463
3769
|
destroy() {
|
|
@@ -3465,12 +3771,17 @@ var SeatManager = class {
|
|
|
3465
3771
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
3466
3772
|
if (this.feedTimer) clearInterval(this.feedTimer);
|
|
3467
3773
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
3774
|
+
if (this.liveEventTimer) clearTimeout(this.liveEventTimer);
|
|
3775
|
+
if (this.kpiCleanupTimer) clearTimeout(this.kpiCleanupTimer);
|
|
3776
|
+
if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
|
|
3777
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
3468
3778
|
if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
|
|
3469
3779
|
if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
|
|
3470
3780
|
if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
|
|
3471
3781
|
this.layoutObserver?.disconnect();
|
|
3472
3782
|
this.layoutObserver = null;
|
|
3473
3783
|
this.root?.removeEventListener("keydown", this.onKeyDown);
|
|
3784
|
+
this.els.rail?.removeEventListener("click", this.onRailClick);
|
|
3474
3785
|
if (typeof document !== "undefined") document.removeEventListener("fullscreenchange", this.onFullscreenChange);
|
|
3475
3786
|
if (this.ws) {
|
|
3476
3787
|
try {
|
|
@@ -3608,7 +3919,6 @@ var SeatManager = class {
|
|
|
3608
3919
|
const id = this.labelToId.get(ch.label);
|
|
3609
3920
|
if (id) {
|
|
3610
3921
|
this.renderer?.setStatus([id], toRenderStatus(st));
|
|
3611
|
-
this.flash(id, st);
|
|
3612
3922
|
ids.push(id);
|
|
3613
3923
|
}
|
|
3614
3924
|
const verb = this.verbFor(prev, st);
|
|
@@ -3617,7 +3927,10 @@ var SeatManager = class {
|
|
|
3617
3927
|
group.labels.push(ch.label);
|
|
3618
3928
|
groups.set(groupKey, group);
|
|
3619
3929
|
}
|
|
3620
|
-
for (const group of groups.values())
|
|
3930
|
+
for (const group of groups.values()) {
|
|
3931
|
+
const activity = this.pushActivity(group.labels, group.verb, group.status);
|
|
3932
|
+
if (activity) this.paintSpatialActivity(activity);
|
|
3933
|
+
}
|
|
3621
3934
|
if (ids.length) {
|
|
3622
3935
|
this.lastSyncedAt = Date.now();
|
|
3623
3936
|
this.afterPaint();
|
|
@@ -3663,9 +3976,108 @@ var SeatManager = class {
|
|
|
3663
3976
|
this.renderer?.forceDraw();
|
|
3664
3977
|
}
|
|
3665
3978
|
}
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3979
|
+
activityColor(status) {
|
|
3980
|
+
return status === "held" ? "#f4b740" : status === "booked" ? "#22a06b" : status === "blocked" ? "#8b94ac" : "#6e7bff";
|
|
3981
|
+
}
|
|
3982
|
+
sectionsForLabels(labels) {
|
|
3983
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3984
|
+
for (const label of labels) {
|
|
3985
|
+
const seat = this.labelToSeat.get(label);
|
|
3986
|
+
if (!seat) continue;
|
|
3987
|
+
const sectionId = this.sectionByObject.get(seat.rowId);
|
|
3988
|
+
if (sectionId && sectionId !== UNGROUPED_ID) ids.add(sectionId);
|
|
3989
|
+
}
|
|
3990
|
+
const sectionIds = [...ids];
|
|
3991
|
+
return {
|
|
3992
|
+
ids: sectionIds,
|
|
3993
|
+
labels: sectionIds.map((id) => this.sectionLabelById.get(id) ?? id)
|
|
3994
|
+
};
|
|
3995
|
+
}
|
|
3996
|
+
pulseSeatLabels(labels, status) {
|
|
3997
|
+
const color = this.activityColor(status);
|
|
3998
|
+
for (const label of labels.slice(0, MAX_LIVE_SEAT_PULSES)) {
|
|
3999
|
+
const id = this.labelToId.get(label);
|
|
4000
|
+
if (id) this.renderer?.flashSeat(id, color);
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
/** Render one grouped realtime operation at the right semantic zoom level. */
|
|
4004
|
+
paintSpatialActivity(activity) {
|
|
4005
|
+
const sectionIds = activity.sectionIds ?? this.sectionsForLabels(activity.labels).ids;
|
|
4006
|
+
const focused = this.renderer?.getFocusedSection() ?? null;
|
|
4007
|
+
const followable = this.followLive && sectionIds.length === 1 && (activity.status === "held" || activity.status === "booked");
|
|
4008
|
+
if (followable && focused === sectionIds[0]) {
|
|
4009
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
4010
|
+
return;
|
|
4011
|
+
}
|
|
4012
|
+
if (followable) {
|
|
4013
|
+
if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
|
|
4014
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
4015
|
+
this.followLiveTimer = setTimeout(() => {
|
|
4016
|
+
this.followLiveTimer = null;
|
|
4017
|
+
this.renderer?.focusSection(sectionIds[0]);
|
|
4018
|
+
this.followSeatTimer = setTimeout(() => {
|
|
4019
|
+
this.followSeatTimer = null;
|
|
4020
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
4021
|
+
}, 520);
|
|
4022
|
+
}, 220);
|
|
4023
|
+
return;
|
|
4024
|
+
}
|
|
4025
|
+
if (!focused && sectionIds.length) {
|
|
4026
|
+
const color = this.activityColor(activity.status);
|
|
4027
|
+
for (const sectionId of sectionIds.slice(0, MAX_LIVE_SECTION_PULSES)) {
|
|
4028
|
+
this.renderer?.flashSection(sectionId, color);
|
|
4029
|
+
}
|
|
4030
|
+
return;
|
|
4031
|
+
}
|
|
4032
|
+
if (!sectionIds.length || focused && sectionIds.includes(focused)) {
|
|
4033
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
locateSection(sectionId) {
|
|
4037
|
+
this.renderer?.focusSection(sectionId);
|
|
4038
|
+
}
|
|
4039
|
+
locateActivity(activityId) {
|
|
4040
|
+
const activity = this.feed.find((item) => item.id === activityId);
|
|
4041
|
+
if (!activity) return;
|
|
4042
|
+
const sectionIds = activity.sectionIds ?? this.sectionsForLabels(activity.labels).ids;
|
|
4043
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
4044
|
+
if (sectionIds.length === 1) {
|
|
4045
|
+
this.locateSection(sectionIds[0]);
|
|
4046
|
+
this.followSeatTimer = setTimeout(() => {
|
|
4047
|
+
this.followSeatTimer = null;
|
|
4048
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
4049
|
+
}, 520);
|
|
4050
|
+
return;
|
|
4051
|
+
}
|
|
4052
|
+
this.zoomToFit();
|
|
4053
|
+
this.followSeatTimer = setTimeout(() => {
|
|
4054
|
+
this.followSeatTimer = null;
|
|
4055
|
+
if (sectionIds.length) {
|
|
4056
|
+
const color = this.activityColor(activity.status);
|
|
4057
|
+
for (const sectionId of sectionIds.slice(0, MAX_LIVE_SECTION_PULSES)) {
|
|
4058
|
+
this.renderer?.flashSection(sectionId, color);
|
|
4059
|
+
}
|
|
4060
|
+
} else {
|
|
4061
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
4062
|
+
}
|
|
4063
|
+
}, 280);
|
|
4064
|
+
}
|
|
4065
|
+
showLiveEvent(activity) {
|
|
4066
|
+
const element = this.els.liveevent;
|
|
4067
|
+
if (!element) return;
|
|
4068
|
+
const sections = activity.sectionLabels ?? [];
|
|
4069
|
+
const place = sections.length === 1 ? sections[0] : sections.length > 1 ? `${sections.length} sections` : activity.label;
|
|
4070
|
+
const noun = activity.count === 1 ? "seat" : "seats";
|
|
4071
|
+
element.innerHTML = `<span class="slm-liveeventdot" style="background:${this.activityColor(activity.status)}"></span>
|
|
4072
|
+
<span class="slm-liveeventcopy">${esc(place)} \xB7 ${activity.count.toLocaleString()} ${noun} ${esc(activity.verb)}</span>
|
|
4073
|
+
<span class="slm-liveeventhint">Live</span>`;
|
|
4074
|
+
element.classList.add("on");
|
|
4075
|
+
if (this.liveEventTimer) clearTimeout(this.liveEventTimer);
|
|
4076
|
+
this.liveEventTimer = setTimeout(() => {
|
|
4077
|
+
this.liveEventTimer = null;
|
|
4078
|
+
element.classList.remove("on");
|
|
4079
|
+
element.innerHTML = "";
|
|
4080
|
+
}, 2800);
|
|
3669
4081
|
}
|
|
3670
4082
|
// ---- tallies + feed -------------------------------------------------------
|
|
3671
4083
|
applyReportRevenue(report) {
|
|
@@ -3748,7 +4160,8 @@ var SeatManager = class {
|
|
|
3748
4160
|
}
|
|
3749
4161
|
pushActivity(labels, verb, status, at = Date.now()) {
|
|
3750
4162
|
const label = labels[0];
|
|
3751
|
-
if (!label) return;
|
|
4163
|
+
if (!label) return null;
|
|
4164
|
+
const sections = this.sectionsForLabels(labels);
|
|
3752
4165
|
const item = {
|
|
3753
4166
|
id: `${label}:${at}:${Math.random().toString(36).slice(2, 6)}`,
|
|
3754
4167
|
at,
|
|
@@ -3756,12 +4169,16 @@ var SeatManager = class {
|
|
|
3756
4169
|
labels: [...labels],
|
|
3757
4170
|
count: labels.length,
|
|
3758
4171
|
verb,
|
|
3759
|
-
status
|
|
4172
|
+
status,
|
|
4173
|
+
sectionIds: sections.ids,
|
|
4174
|
+
sectionLabels: sections.labels
|
|
3760
4175
|
};
|
|
3761
4176
|
this.feed.unshift(item);
|
|
3762
4177
|
if (this.feed.length > FEED_CAP) this.feed.length = FEED_CAP;
|
|
3763
4178
|
if (this.mode === "view") this.paintFeed();
|
|
4179
|
+
this.showLiveEvent(item);
|
|
3764
4180
|
this.opts.onActivity?.(item);
|
|
4181
|
+
return item;
|
|
3765
4182
|
}
|
|
3766
4183
|
seedFeed(entries) {
|
|
3767
4184
|
const verbByAction = {
|
|
@@ -3785,6 +4202,7 @@ var SeatManager = class {
|
|
|
3785
4202
|
for (const e of entries) {
|
|
3786
4203
|
const label = e.labels[0];
|
|
3787
4204
|
if (!label) continue;
|
|
4205
|
+
const sections = this.sectionsForLabels(e.labels);
|
|
3788
4206
|
const item = {
|
|
3789
4207
|
id: `log:${e.id}`,
|
|
3790
4208
|
at: e.at,
|
|
@@ -3792,7 +4210,9 @@ var SeatManager = class {
|
|
|
3792
4210
|
labels: [...e.labels],
|
|
3793
4211
|
count: e.labels.length,
|
|
3794
4212
|
verb: verbByAction[e.action] ?? e.action,
|
|
3795
|
-
status: stByAction[e.action] ?? "free"
|
|
4213
|
+
status: stByAction[e.action] ?? "free",
|
|
4214
|
+
sectionIds: sections.ids,
|
|
4215
|
+
sectionLabels: sections.labels
|
|
3796
4216
|
};
|
|
3797
4217
|
this.feed.push(item);
|
|
3798
4218
|
this.opts.onActivity?.(item);
|
|
@@ -3830,13 +4250,15 @@ var SeatManager = class {
|
|
|
3830
4250
|
for (const [k, v] of Object.entries(vars)) root.style.setProperty(k, v);
|
|
3831
4251
|
root.innerHTML = `
|
|
3832
4252
|
<div class="slm-bar">
|
|
3833
|
-
<div class="slm-modes" data-ref="modes">
|
|
3834
|
-
<button class="slm-mode" data-mode="view" title="Monitor (M)" aria-keyshortcuts="M">Monitor</button>
|
|
3835
|
-
<button class="slm-mode" data-mode="inspect" title="Inspect (I)" aria-keyshortcuts="I">Inspect</button>
|
|
3836
|
-
<button class="slm-mode" data-mode="block" title="Block (B)" aria-keyshortcuts="B">Block</button>
|
|
4253
|
+
<div class="slm-modes" data-ref="modes" role="tablist" aria-label="Manager tools">
|
|
4254
|
+
<button class="slm-mode" role="tab" data-mode="view" title="Monitor (M)" aria-keyshortcuts="M">Monitor</button>
|
|
4255
|
+
<button class="slm-mode" role="tab" data-mode="inspect" title="Inspect (I)" aria-keyshortcuts="I">Inspect</button>
|
|
4256
|
+
<button class="slm-mode" role="tab" data-mode="block" title="Block (B)" aria-keyshortcuts="B">Block</button>
|
|
3837
4257
|
</div>
|
|
3838
4258
|
<span class="slm-live"><span class="slm-live-dot"></span><span data-ref="livetext">CONNECTING</span></span>
|
|
3839
4259
|
<div class="slm-bar-actions">
|
|
4260
|
+
<button class="slm-barbtn follow" data-ref="follow" aria-pressed="false"
|
|
4261
|
+
title="Stay on the current map view unless enabled">Follow live</button>
|
|
3840
4262
|
<button class="slm-barbtn" data-ref="heat" aria-pressed="false"
|
|
3841
4263
|
aria-label="Sales momentum overlay off"
|
|
3842
4264
|
title="Highlight sections selling fastest in the selected time window">Sales momentum</button>
|
|
@@ -3848,6 +4270,7 @@ var SeatManager = class {
|
|
|
3848
4270
|
<div class="slm-map">
|
|
3849
4271
|
<div class="slm-map-host" data-ref="maphost"></div>
|
|
3850
4272
|
<div class="slm-zoomhint" data-ref="zoomhint">Zoom in to marquee-select</div>
|
|
4273
|
+
<div class="slm-liveevent" data-ref="liveevent" role="status" aria-live="polite"></div>
|
|
3851
4274
|
<div class="slm-hud"><button class="slm-hud-chip" data-ref="zfit">Zoom to fit</button></div>
|
|
3852
4275
|
</div>
|
|
3853
4276
|
<aside class="slm-rail"><div class="slm-railscroll" data-ref="rail"></div></aside>
|
|
@@ -3867,20 +4290,25 @@ var SeatManager = class {
|
|
|
3867
4290
|
modes: ref("modes"),
|
|
3868
4291
|
livetext: ref("livetext"),
|
|
3869
4292
|
kpis: ref("kpis"),
|
|
4293
|
+
follow: ref("follow"),
|
|
3870
4294
|
heat: ref("heat"),
|
|
3871
4295
|
fullscreen: ref("fullscreen"),
|
|
3872
4296
|
zoomhint: ref("zoomhint"),
|
|
4297
|
+
liveevent: ref("liveevent"),
|
|
3873
4298
|
rail: ref("rail"),
|
|
3874
4299
|
toast: ref("toast"),
|
|
3875
4300
|
zfit: ref("zfit")
|
|
3876
4301
|
};
|
|
3877
4302
|
this.els.modes.querySelectorAll("[data-mode]").forEach((b) => b.addEventListener("click", () => this.setMode(b.dataset.mode)));
|
|
3878
4303
|
this.els.zfit.addEventListener("click", () => this.zoomToFit());
|
|
4304
|
+
this.els.follow.addEventListener("click", () => this.setFollowLive(!this.followLive));
|
|
3879
4305
|
this.els.heat.addEventListener("click", () => this.setHeatOverlay(!this.heatEnabled));
|
|
3880
4306
|
this.els.fullscreen.addEventListener("click", () => this.toggleFullscreen());
|
|
3881
4307
|
root.addEventListener("keydown", this.onKeyDown);
|
|
4308
|
+
this.els.rail.addEventListener("click", this.onRailClick);
|
|
3882
4309
|
document.addEventListener("fullscreenchange", this.onFullscreenChange);
|
|
3883
4310
|
this.paintModeTabs();
|
|
4311
|
+
this.paintFollowLiveButton();
|
|
3884
4312
|
this.paintHeatButton();
|
|
3885
4313
|
this.paintFullscreenButton();
|
|
3886
4314
|
}
|
|
@@ -3909,10 +4337,20 @@ var SeatManager = class {
|
|
|
3909
4337
|
paintModeTabs() {
|
|
3910
4338
|
this.els.modes?.querySelectorAll("[data-mode]").forEach((b) => {
|
|
3911
4339
|
const el = b;
|
|
3912
|
-
|
|
4340
|
+
const active = el.dataset.mode === this.mode;
|
|
4341
|
+
el.classList.toggle("on", active);
|
|
4342
|
+
el.setAttribute("aria-selected", String(active));
|
|
4343
|
+
el.tabIndex = active ? 0 : -1;
|
|
3913
4344
|
});
|
|
3914
4345
|
this.root?.classList.toggle("block-mode", this.mode === "block");
|
|
3915
4346
|
}
|
|
4347
|
+
paintFollowLiveButton() {
|
|
4348
|
+
const button = this.els.follow;
|
|
4349
|
+
if (!button) return;
|
|
4350
|
+
button.classList.toggle("on", this.followLive);
|
|
4351
|
+
button.setAttribute("aria-pressed", String(this.followLive));
|
|
4352
|
+
button.setAttribute("title", this.followLive ? "Following new buyer holds and bookings. Turn off to keep the current view." : "Stay on the current map view. Enable to follow new buyer holds and bookings.");
|
|
4353
|
+
}
|
|
3916
4354
|
paintHeatButton() {
|
|
3917
4355
|
const button = this.els.heat;
|
|
3918
4356
|
if (!button) return;
|
|
@@ -3953,20 +4391,55 @@ var SeatManager = class {
|
|
|
3953
4391
|
const show = this.mode === "block" && this.renderer?.getRung?.() !== "seats";
|
|
3954
4392
|
hint.classList.toggle("on", !!show);
|
|
3955
4393
|
}
|
|
4394
|
+
formatKpiDelta(key, delta, currency) {
|
|
4395
|
+
const sign = delta > 0 ? "+" : "\u2212";
|
|
4396
|
+
const absolute = Math.abs(delta);
|
|
4397
|
+
if (key === "gross-sales") return `${sign}${fmtMoney(absolute, currency)}`;
|
|
4398
|
+
if (key === "sold-pct") return `${sign}${absolute.toLocaleString()}pt`;
|
|
4399
|
+
return `${sign}${absolute.toLocaleString()}`;
|
|
4400
|
+
}
|
|
3956
4401
|
paintKpis(t3) {
|
|
3957
4402
|
if (!this.els.kpis) return;
|
|
3958
4403
|
const rev = t3.revenueStatus === "current" ? fmtMoney(t3.grossRevenue, t3.currency) : "\u2014";
|
|
3959
4404
|
const presence = this.controlRoomSnapshot?.presence;
|
|
3960
|
-
|
|
3961
|
-
{ key: "sold-seats", n: t3.booked.toLocaleString(), l: "Sold seats", dot: "#22a06b" },
|
|
3962
|
-
{ key: "held-seats", n: t3.held.toLocaleString(), l: "Held seats", dot: "#f4b740" },
|
|
3963
|
-
{ key: "buyers", n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Buyers" },
|
|
3964
|
-
{ key: "active-holds", n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Active holds" },
|
|
3965
|
-
{ key: "free-seats", n: t3.free.toLocaleString(), l: "Free seats", dot: "#6e7bff" },
|
|
3966
|
-
{ key: "blocked", n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac" },
|
|
3967
|
-
{ key: "sold-pct", n: `${t3.capacityPct}%`, l: "Sold" },
|
|
3968
|
-
{ key: "gross-sales", n: rev, l: "Gross sales" }
|
|
3969
|
-
]
|
|
4405
|
+
const items = [
|
|
4406
|
+
{ key: "sold-seats", raw: t3.booked, n: t3.booked.toLocaleString(), l: "Sold seats", dot: "#22a06b" },
|
|
4407
|
+
{ key: "held-seats", raw: t3.held, n: t3.held.toLocaleString(), l: "Held seats", dot: "#f4b740" },
|
|
4408
|
+
{ key: "buyers", raw: presence?.shoppingSessions ?? null, n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Buyers" },
|
|
4409
|
+
{ key: "active-holds", raw: presence?.activeHolds ?? null, n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Active holds" },
|
|
4410
|
+
{ key: "free-seats", raw: t3.free, n: t3.free.toLocaleString(), l: "Free seats", dot: "#6e7bff" },
|
|
4411
|
+
{ key: "blocked", raw: t3.blocked, n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac" },
|
|
4412
|
+
{ key: "sold-pct", raw: t3.capacityPct, n: `${t3.capacityPct}%`, l: "Sold" },
|
|
4413
|
+
{ key: "gross-sales", raw: t3.revenueStatus === "current" ? t3.grossRevenue : null, n: rev, l: "Gross sales" }
|
|
4414
|
+
];
|
|
4415
|
+
let hasChanges = false;
|
|
4416
|
+
this.els.kpis.innerHTML = items.map((item) => {
|
|
4417
|
+
const previous = this.lastKpiValues.get(item.key);
|
|
4418
|
+
const changed = item.raw != null && previous != null && item.raw !== previous;
|
|
4419
|
+
const delta = changed ? item.raw - previous : 0;
|
|
4420
|
+
if (changed) {
|
|
4421
|
+
hasChanges = true;
|
|
4422
|
+
this.activeKpiDeltas.set(item.key, {
|
|
4423
|
+
text: this.formatKpiDelta(item.key, delta, t3.currency),
|
|
4424
|
+
down: delta < 0
|
|
4425
|
+
});
|
|
4426
|
+
}
|
|
4427
|
+
if (item.raw != null) this.lastKpiValues.set(item.key, item.raw);
|
|
4428
|
+
const activeDelta = this.activeKpiDeltas.get(item.key);
|
|
4429
|
+
return `<div class="slm-kpi${activeDelta ? " changed" : ""}" data-kpi="${item.key}">
|
|
4430
|
+
<b>${item.dot ? `<span class="dot" style="background:${item.dot}"></span>` : ""}${item.n}</b><span>${item.l}</span>
|
|
4431
|
+
${activeDelta ? `<span class="slm-kpidelta${activeDelta.down ? " down" : ""}">${activeDelta.text}</span>` : ""}
|
|
4432
|
+
</div>`;
|
|
4433
|
+
}).join("");
|
|
4434
|
+
if (hasChanges) {
|
|
4435
|
+
if (this.kpiCleanupTimer) clearTimeout(this.kpiCleanupTimer);
|
|
4436
|
+
this.kpiCleanupTimer = setTimeout(() => {
|
|
4437
|
+
this.kpiCleanupTimer = null;
|
|
4438
|
+
this.activeKpiDeltas.clear();
|
|
4439
|
+
this.els.kpis?.querySelectorAll(".slm-kpidelta").forEach((element) => element.remove());
|
|
4440
|
+
this.els.kpis?.querySelectorAll(".slm-kpi.changed").forEach((element) => element.classList.remove("changed"));
|
|
4441
|
+
}, 1500);
|
|
4442
|
+
}
|
|
3970
4443
|
}
|
|
3971
4444
|
// ---- DOM: rails -----------------------------------------------------------
|
|
3972
4445
|
paintRail() {
|
|
@@ -4037,10 +4510,10 @@ var SeatManager = class {
|
|
|
4037
4510
|
const net = speed?.netBooked ?? 0;
|
|
4038
4511
|
const netLabel = `${net > 0 ? "+" : ""}${net}`;
|
|
4039
4512
|
const trend = speed?.trend === "rising" || speed?.trend === "cooling" ? speed.trend : "steady";
|
|
4040
|
-
return `<
|
|
4041
|
-
<
|
|
4042
|
-
<
|
|
4043
|
-
</
|
|
4513
|
+
return `<button type="button" class="slm-sectionrow" data-section-focus="${esc(row.sectionId)}" title="Focus ${esc(row.sectionLabel)} on the map">
|
|
4514
|
+
<span class="slm-sectiontop"><span>${esc(row.sectionLabel)}</span><span>${fmtMoney(row.bookedRevenue, snapshot.currency)}</span></span>
|
|
4515
|
+
<span 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><span class="slm-sectionlocate">Locate</span></span>
|
|
4516
|
+
</button>`;
|
|
4044
4517
|
}).join("") : '<div class="slm-empty">No section metrics are available for this chart.</div>';
|
|
4045
4518
|
this.paintTrendWindow();
|
|
4046
4519
|
this.paintMomentumHelp();
|
|
@@ -4109,9 +4582,13 @@ var SeatManager = class {
|
|
|
4109
4582
|
const color = { free: "#6e7bff", held: "#f4b740", booked: "#22a06b", blocked: "#8b94ac" };
|
|
4110
4583
|
this.els.feed.innerHTML = this.feed.map((a) => {
|
|
4111
4584
|
const extra = a.count > 1 ? ` +${a.count - 1}` : "";
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4585
|
+
const sections = a.sectionLabels ?? [];
|
|
4586
|
+
const sectionCopy = sections.length === 1 ? sections[0] : sections.length > 1 ? `${sections.length} sections` : "";
|
|
4587
|
+
return `<button type="button" class="slm-feedrow" data-feed-id="${esc(a.id)}" title="Locate this activity on the map">
|
|
4588
|
+
<span class="slm-feeddot" style="background:${color[a.status]}"></span>
|
|
4589
|
+
<span class="slm-feedtext">${sectionCopy ? `<span class="slm-feedsection">${esc(sectionCopy)}</span>` : ""}${a.count === 1 ? "Seat" : "Seats"} <b>${esc(a.label)}${extra}</b> ${esc(a.verb)}</span>
|
|
4590
|
+
<span class="slm-feedmeta"><span class="slm-feedtime">${relTime(a.at, now)}</span><span class="slm-feedlocate">Locate</span></span>
|
|
4591
|
+
</button>`;
|
|
4115
4592
|
}).join("");
|
|
4116
4593
|
}
|
|
4117
4594
|
renderBlockRail() {
|
|
@@ -4367,6 +4844,10 @@ var SeatManager = class {
|
|
|
4367
4844
|
// ---- toast / done / fail --------------------------------------------------
|
|
4368
4845
|
done(action, labels, msg) {
|
|
4369
4846
|
this.toastOk(msg);
|
|
4847
|
+
if (labels.length) {
|
|
4848
|
+
const activity = action === "block" ? this.pushActivity(labels, "blocked", "blocked") : action === "unblock" || action === "unblockAll" ? this.pushActivity(labels, "unblocked", "free") : action === "cancelBooking" ? this.pushActivity(labels, "cancelled", "free") : null;
|
|
4849
|
+
if (activity) this.paintSpatialActivity(activity);
|
|
4850
|
+
}
|
|
4370
4851
|
if (action !== "setHoldTtl") this.scheduleRevenueRefresh(0);
|
|
4371
4852
|
this.opts.onActionComplete?.({ action, labels, count: labels.length });
|
|
4372
4853
|
}
|