@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.cjs
CHANGED
|
@@ -358,16 +358,46 @@ var TYPES = /* @__PURE__ */ new Set([
|
|
|
358
358
|
"seatlayer.designer.close",
|
|
359
359
|
"seatlayer.designer.error"
|
|
360
360
|
]);
|
|
361
|
+
var DEFAULT_LOADING_TIMEOUT_MS = 2e4;
|
|
361
362
|
function resolveContainer2(container) {
|
|
362
363
|
if (typeof container !== "string") return container;
|
|
363
364
|
const element = document.querySelector(container);
|
|
364
365
|
if (!element) throw new Error(`EmbeddedDesigner container not found: ${container}`);
|
|
365
366
|
return element;
|
|
366
367
|
}
|
|
368
|
+
function causeFromCode(code) {
|
|
369
|
+
const value = (code ?? "").toLowerCase();
|
|
370
|
+
if (value.includes("expire") || value.includes("revoke") || value === "401") return "expired";
|
|
371
|
+
if (value.includes("mismatch")) return "mismatch";
|
|
372
|
+
if (value.includes("timeout")) return "timeout";
|
|
373
|
+
return "load";
|
|
374
|
+
}
|
|
375
|
+
var ERROR_COPY = {
|
|
376
|
+
expired: {
|
|
377
|
+
title: "This design session expired",
|
|
378
|
+
body: "For your security, editing sessions are short-lived. Start a fresh one to keep designing."
|
|
379
|
+
},
|
|
380
|
+
mismatch: {
|
|
381
|
+
title: "This editor doesn't match this chart",
|
|
382
|
+
body: "The session that loaded belongs to a different chart or workspace. Reopen the designer to continue."
|
|
383
|
+
},
|
|
384
|
+
timeout: {
|
|
385
|
+
title: "The designer is taking too long",
|
|
386
|
+
body: "It did not finish loading in time. This is usually a slow connection \u2014 try again."
|
|
387
|
+
},
|
|
388
|
+
load: {
|
|
389
|
+
title: "We couldn't load the designer",
|
|
390
|
+
body: "Something went wrong while opening the editor. Please try again."
|
|
391
|
+
}
|
|
392
|
+
};
|
|
367
393
|
var EmbeddedDesigner = class {
|
|
368
394
|
constructor(options) {
|
|
369
395
|
this.frame = null;
|
|
370
396
|
this.designerOrigin = "";
|
|
397
|
+
this.overlay = null;
|
|
398
|
+
this.timeoutTimer = null;
|
|
399
|
+
this.phase = "loading";
|
|
400
|
+
this.restoreContainerPosition = null;
|
|
371
401
|
this.handleMessage = (event) => {
|
|
372
402
|
if (!this.frame || event.origin !== this.designerOrigin || event.source !== this.frame.contentWindow) return;
|
|
373
403
|
if (!event.data || typeof event.data !== "object") return;
|
|
@@ -382,10 +412,15 @@ var EmbeddedDesigner = class {
|
|
|
382
412
|
message: typeof data.message === "string" ? data.message : void 0,
|
|
383
413
|
meta: data.meta
|
|
384
414
|
};
|
|
385
|
-
if (this.options.expectedChartId && message.chartId && message.chartId !== this.options.expectedChartId)
|
|
386
|
-
|
|
415
|
+
if (this.options.expectedChartId && message.chartId && message.chartId !== this.options.expectedChartId || this.options.expectedWorkspaceId && message.workspaceId && message.workspaceId !== this.options.expectedWorkspaceId) {
|
|
416
|
+
this.showError("mismatch");
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
387
419
|
switch (message.type) {
|
|
388
420
|
case "seatlayer.designer.ready":
|
|
421
|
+
this.phase = "ready";
|
|
422
|
+
this.clearTimeoutTimer();
|
|
423
|
+
this.removeOverlay();
|
|
389
424
|
this.options.onReady?.(message);
|
|
390
425
|
break;
|
|
391
426
|
case "seatlayer.designer.saved":
|
|
@@ -398,6 +433,7 @@ var EmbeddedDesigner = class {
|
|
|
398
433
|
this.options.onClose?.(message);
|
|
399
434
|
break;
|
|
400
435
|
case "seatlayer.designer.error":
|
|
436
|
+
this.showError(causeFromCode(message.code));
|
|
401
437
|
this.options.onError?.(message);
|
|
402
438
|
break;
|
|
403
439
|
}
|
|
@@ -421,9 +457,21 @@ var EmbeddedDesigner = class {
|
|
|
421
457
|
frame.style.border = "0";
|
|
422
458
|
Object.assign(frame.style, this.options.style);
|
|
423
459
|
if (this.options.className) frame.className = this.options.className;
|
|
460
|
+
const container = resolveContainer2(this.options.container);
|
|
424
461
|
window.addEventListener("message", this.handleMessage);
|
|
425
|
-
|
|
462
|
+
container.append(frame);
|
|
426
463
|
this.frame = frame;
|
|
464
|
+
this.phase = "loading";
|
|
465
|
+
if (this.loadingStateEnabled()) {
|
|
466
|
+
this.ensureContainerPositioned(container);
|
|
467
|
+
this.renderOverlay(container, "loading");
|
|
468
|
+
const timeout = this.options.loadingTimeoutMs ?? DEFAULT_LOADING_TIMEOUT_MS;
|
|
469
|
+
if (timeout > 0 && Number.isFinite(timeout)) {
|
|
470
|
+
this.timeoutTimer = setTimeout(() => {
|
|
471
|
+
if (this.phase === "loading") this.showError("timeout");
|
|
472
|
+
}, timeout);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
427
475
|
return frame;
|
|
428
476
|
}
|
|
429
477
|
/** Replace the iframe instead of assigning a new fragment to an existing one. */
|
|
@@ -436,9 +484,208 @@ var EmbeddedDesigner = class {
|
|
|
436
484
|
}
|
|
437
485
|
destroy() {
|
|
438
486
|
window.removeEventListener("message", this.handleMessage);
|
|
487
|
+
this.clearTimeoutTimer();
|
|
488
|
+
this.removeOverlay();
|
|
489
|
+
this.restoreContainerStyle();
|
|
439
490
|
this.frame?.remove();
|
|
440
491
|
this.frame = null;
|
|
441
492
|
this.designerOrigin = "";
|
|
493
|
+
this.phase = "loading";
|
|
494
|
+
}
|
|
495
|
+
loadingStateEnabled() {
|
|
496
|
+
return this.options.showLoadingState !== false;
|
|
497
|
+
}
|
|
498
|
+
clearTimeoutTimer() {
|
|
499
|
+
if (this.timeoutTimer !== null) {
|
|
500
|
+
clearTimeout(this.timeoutTimer);
|
|
501
|
+
this.timeoutTimer = null;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
ensureContainerPositioned(container) {
|
|
505
|
+
const position = getComputedStyle(container).position;
|
|
506
|
+
if (position === "static") {
|
|
507
|
+
this.restoreContainerPosition = container.style.position;
|
|
508
|
+
container.style.position = "relative";
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
restoreContainerStyle() {
|
|
512
|
+
if (this.restoreContainerPosition === null) return;
|
|
513
|
+
try {
|
|
514
|
+
resolveContainer2(this.options.container).style.position = this.restoreContainerPosition;
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
this.restoreContainerPosition = null;
|
|
518
|
+
}
|
|
519
|
+
removeOverlay() {
|
|
520
|
+
this.overlay?.remove();
|
|
521
|
+
this.overlay = null;
|
|
522
|
+
}
|
|
523
|
+
showError(cause) {
|
|
524
|
+
this.phase = "error";
|
|
525
|
+
this.clearTimeoutTimer();
|
|
526
|
+
if (!this.loadingStateEnabled()) return;
|
|
527
|
+
let container;
|
|
528
|
+
try {
|
|
529
|
+
container = resolveContainer2(this.options.container);
|
|
530
|
+
} catch {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
this.renderOverlay(container, "error", cause);
|
|
534
|
+
}
|
|
535
|
+
handleTryAgain() {
|
|
536
|
+
if (this.options.onRequestRelaunch) {
|
|
537
|
+
this.options.onRequestRelaunch();
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
this.mount();
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Build (or rebuild) the overlay for the given phase. A single overlay element
|
|
544
|
+
* is reused so we never stack stale skeletons or cards.
|
|
545
|
+
*/
|
|
546
|
+
renderOverlay(container, phase, cause) {
|
|
547
|
+
this.removeOverlay();
|
|
548
|
+
const overlay = document.createElement("div");
|
|
549
|
+
overlay.setAttribute("data-seatlayer-designer-overlay", phase);
|
|
550
|
+
overlay.setAttribute("role", phase === "error" ? "alert" : "status");
|
|
551
|
+
overlay.setAttribute("aria-live", "polite");
|
|
552
|
+
Object.assign(overlay.style, {
|
|
553
|
+
position: "absolute",
|
|
554
|
+
inset: "0",
|
|
555
|
+
display: "flex",
|
|
556
|
+
alignItems: "center",
|
|
557
|
+
justifyContent: "center",
|
|
558
|
+
background: "#101625",
|
|
559
|
+
color: "#e6ebf5",
|
|
560
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
|
561
|
+
zIndex: "2",
|
|
562
|
+
overflow: "hidden"
|
|
563
|
+
});
|
|
564
|
+
if (phase === "loading") this.buildSkeleton(overlay);
|
|
565
|
+
else this.buildErrorCard(overlay, cause ?? "load");
|
|
566
|
+
container.append(overlay);
|
|
567
|
+
this.overlay = overlay;
|
|
568
|
+
}
|
|
569
|
+
buildSkeleton(overlay) {
|
|
570
|
+
const style = document.createElement("style");
|
|
571
|
+
style.textContent = `
|
|
572
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
573
|
+
@keyframes seatlayer-designer-shimmer {
|
|
574
|
+
0% { background-position: -320px 0; }
|
|
575
|
+
100% { background-position: 320px 0; }
|
|
576
|
+
}
|
|
577
|
+
[data-seatlayer-designer-overlay="loading"] .sl-shimmer {
|
|
578
|
+
animation: seatlayer-designer-shimmer 1.25s ease-in-out infinite;
|
|
579
|
+
background-size: 640px 100%;
|
|
580
|
+
}
|
|
581
|
+
}`;
|
|
582
|
+
overlay.append(style);
|
|
583
|
+
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%)";
|
|
584
|
+
const scaffold = document.createElement("div");
|
|
585
|
+
Object.assign(scaffold.style, {
|
|
586
|
+
position: "absolute",
|
|
587
|
+
inset: "0",
|
|
588
|
+
display: "flex",
|
|
589
|
+
flexDirection: "column",
|
|
590
|
+
padding: "16px",
|
|
591
|
+
gap: "14px",
|
|
592
|
+
opacity: "0.9"
|
|
593
|
+
});
|
|
594
|
+
const bar = (styles) => {
|
|
595
|
+
const node = document.createElement("div");
|
|
596
|
+
node.className = "sl-shimmer";
|
|
597
|
+
Object.assign(node.style, {
|
|
598
|
+
background: shimmer,
|
|
599
|
+
borderRadius: "8px"
|
|
600
|
+
});
|
|
601
|
+
Object.assign(node.style, styles);
|
|
602
|
+
return node;
|
|
603
|
+
};
|
|
604
|
+
scaffold.append(bar({ height: "40px", width: "100%", flex: "0 0 auto" }));
|
|
605
|
+
const body = document.createElement("div");
|
|
606
|
+
Object.assign(body.style, {
|
|
607
|
+
display: "flex",
|
|
608
|
+
gap: "14px",
|
|
609
|
+
flex: "1 1 auto",
|
|
610
|
+
minHeight: "0"
|
|
611
|
+
});
|
|
612
|
+
body.append(bar({ width: "220px", height: "100%", flex: "0 0 auto" }));
|
|
613
|
+
body.append(bar({ flex: "1 1 auto", height: "100%" }));
|
|
614
|
+
scaffold.append(body);
|
|
615
|
+
overlay.append(scaffold);
|
|
616
|
+
const caption = document.createElement("div");
|
|
617
|
+
Object.assign(caption.style, {
|
|
618
|
+
position: "relative",
|
|
619
|
+
zIndex: "1",
|
|
620
|
+
display: "flex",
|
|
621
|
+
alignItems: "center",
|
|
622
|
+
gap: "10px",
|
|
623
|
+
padding: "10px 16px",
|
|
624
|
+
borderRadius: "999px",
|
|
625
|
+
background: "rgba(16, 22, 37, 0.72)",
|
|
626
|
+
fontSize: "13px",
|
|
627
|
+
fontWeight: "500",
|
|
628
|
+
letterSpacing: "0.01em"
|
|
629
|
+
});
|
|
630
|
+
const dot = document.createElement("span");
|
|
631
|
+
dot.className = "sl-shimmer";
|
|
632
|
+
Object.assign(dot.style, {
|
|
633
|
+
width: "9px",
|
|
634
|
+
height: "9px",
|
|
635
|
+
borderRadius: "50%",
|
|
636
|
+
background: shimmer,
|
|
637
|
+
flex: "0 0 auto"
|
|
638
|
+
});
|
|
639
|
+
caption.append(dot);
|
|
640
|
+
caption.append(document.createTextNode("Loading designer\u2026"));
|
|
641
|
+
overlay.append(caption);
|
|
642
|
+
}
|
|
643
|
+
buildErrorCard(overlay, cause) {
|
|
644
|
+
const copy = ERROR_COPY[cause];
|
|
645
|
+
const card = document.createElement("div");
|
|
646
|
+
Object.assign(card.style, {
|
|
647
|
+
maxWidth: "420px",
|
|
648
|
+
margin: "0 24px",
|
|
649
|
+
padding: "28px",
|
|
650
|
+
textAlign: "center",
|
|
651
|
+
background: "rgba(255, 255, 255, 0.03)",
|
|
652
|
+
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
653
|
+
borderRadius: "16px",
|
|
654
|
+
boxShadow: "0 12px 40px rgba(0, 0, 0, 0.35)"
|
|
655
|
+
});
|
|
656
|
+
const heading = document.createElement("h2");
|
|
657
|
+
heading.textContent = copy.title;
|
|
658
|
+
Object.assign(heading.style, {
|
|
659
|
+
margin: "0 0 8px",
|
|
660
|
+
fontSize: "17px",
|
|
661
|
+
fontWeight: "600",
|
|
662
|
+
color: "#f4f7ff"
|
|
663
|
+
});
|
|
664
|
+
const body = document.createElement("p");
|
|
665
|
+
body.textContent = copy.body;
|
|
666
|
+
Object.assign(body.style, {
|
|
667
|
+
margin: "0 0 20px",
|
|
668
|
+
fontSize: "13.5px",
|
|
669
|
+
lineHeight: "1.5",
|
|
670
|
+
color: "#aab4c8"
|
|
671
|
+
});
|
|
672
|
+
const button = document.createElement("button");
|
|
673
|
+
button.type = "button";
|
|
674
|
+
button.textContent = "Try again";
|
|
675
|
+
Object.assign(button.style, {
|
|
676
|
+
appearance: "none",
|
|
677
|
+
cursor: "pointer",
|
|
678
|
+
border: "0",
|
|
679
|
+
borderRadius: "10px",
|
|
680
|
+
padding: "10px 22px",
|
|
681
|
+
fontSize: "14px",
|
|
682
|
+
fontWeight: "600",
|
|
683
|
+
color: "#101625",
|
|
684
|
+
background: "#7aa2ff"
|
|
685
|
+
});
|
|
686
|
+
button.addEventListener("click", () => this.handleTryAgain());
|
|
687
|
+
card.append(heading, body, button);
|
|
688
|
+
overlay.append(card);
|
|
442
689
|
}
|
|
443
690
|
};
|
|
444
691
|
|
|
@@ -2985,6 +3232,8 @@ function toRenderStatus(s) {
|
|
|
2985
3232
|
var DEFAULT_API_BASE3 = "https://api.seatlayer.io";
|
|
2986
3233
|
var STYLE_ID2 = "seatlayer-manager-style";
|
|
2987
3234
|
var FEED_CAP = 80;
|
|
3235
|
+
var MAX_LIVE_SEAT_PULSES = 16;
|
|
3236
|
+
var MAX_LIVE_SECTION_PULSES = 4;
|
|
2988
3237
|
var LEGEND = [
|
|
2989
3238
|
{ key: "free", label: "Free", color: "#6e7bff" },
|
|
2990
3239
|
{ key: "held", label: "Held", color: "#f4b740" },
|
|
@@ -3010,13 +3259,21 @@ var CSS2 = `
|
|
|
3010
3259
|
@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)}}
|
|
3011
3260
|
.slm-kpis{grid-column:1/-1;display:grid;grid-template-columns:repeat(8,minmax(0,1fr));width:100%;padding-top:10px;
|
|
3012
3261
|
border-top:1px solid var(--slm-line)}
|
|
3013
|
-
.slm-kpi{display:flex;min-width:0;flex-direction:column;align-items:center;padding:0 5px;line-height:1.15;text-align:center}
|
|
3262
|
+
.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}
|
|
3014
3263
|
.slm-kpi b{display:flex;min-width:0;align-items:baseline;justify-content:center;font-size:17px;font-weight:800;
|
|
3015
3264
|
font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
3016
3265
|
.slm-kpi span{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--slm-muted);font-weight:700}
|
|
3017
3266
|
.slm-kpi .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:baseline}
|
|
3267
|
+
.slm-kpi.changed b{animation:slm-kpi-bump .58s cubic-bezier(.2,.8,.2,1)}
|
|
3268
|
+
.slm-kpidelta{position:absolute;right:4px;top:-12px;padding:2px 5px;border-radius:999px;background:rgba(34,160,107,.17);
|
|
3269
|
+
color:#5bd39b!important;font-size:9px!important;letter-spacing:0!important;text-transform:none!important;white-space:nowrap;
|
|
3270
|
+
animation:slm-kpi-delta 1.45s ease-out both;pointer-events:none}
|
|
3271
|
+
.slm-kpidelta.down{background:rgba(244,183,64,.14);color:#f7ca6b!important}
|
|
3272
|
+
@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)}}
|
|
3273
|
+
@keyframes slm-kpi-delta{0%{opacity:0;transform:translateY(5px)}18%,72%{opacity:1;transform:none}100%{opacity:0;transform:translateY(-5px)}}
|
|
3018
3274
|
.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}
|
|
3019
3275
|
.slm-barbtn:hover{border-color:var(--slm-muted)}
|
|
3276
|
+
.slm-barbtn.follow.on{background:rgba(34,160,107,.13);border-color:#22a06b;color:#5bd39b}
|
|
3020
3277
|
|
|
3021
3278
|
/* body */
|
|
3022
3279
|
.slm-body{display:flex;flex:1;min-height:0}
|
|
@@ -3028,6 +3285,14 @@ var CSS2 = `
|
|
|
3028
3285
|
.slm-zoomhint{position:absolute;left:50%;top:14px;transform:translateX(-50%);padding:6px 13px;border-radius:999px;
|
|
3029
3286
|
background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:700;pointer-events:none;opacity:0;transition:opacity .2s}
|
|
3030
3287
|
.slm-zoomhint.on{opacity:1}
|
|
3288
|
+
.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));
|
|
3289
|
+
padding:8px 12px;border:1px solid var(--slm-line);border-radius:999px;background:color-mix(in srgb,var(--slm-surface) 92%,transparent);
|
|
3290
|
+
box-shadow:0 10px 34px rgba(0,0,0,.32);opacity:0;transform:translate(-50%,-8px);pointer-events:none;
|
|
3291
|
+
transition:opacity .18s ease,transform .24s ease;backdrop-filter:blur(10px)}
|
|
3292
|
+
.slm-liveevent.on{opacity:1;transform:translate(-50%,0)}
|
|
3293
|
+
.slm.block-mode .slm-liveevent{top:52px}
|
|
3294
|
+
.slm-liveeventdot{width:8px;height:8px;border-radius:50%;flex:none}.slm-liveeventcopy{min-width:0;overflow:hidden;text-overflow:ellipsis;
|
|
3295
|
+
white-space:nowrap;font-size:12px;font-weight:800}.slm-liveeventhint{color:var(--slm-muted);font-size:10px;white-space:nowrap}
|
|
3031
3296
|
.slm-rail{width:320px;flex:none;border-left:1px solid var(--slm-line);display:flex;flex-direction:column;min-height:0}
|
|
3032
3297
|
.slm-railscroll{flex:1;overflow-y:auto;padding:16px}
|
|
3033
3298
|
.slm-eyebrow{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--slm-muted);font-weight:800;margin-bottom:6px}
|
|
@@ -3042,13 +3307,16 @@ var CSS2 = `
|
|
|
3042
3307
|
|
|
3043
3308
|
/* activity feed */
|
|
3044
3309
|
.slm-feed{display:flex;flex-direction:column;gap:0}
|
|
3045
|
-
.slm-feedrow{display:flex;align-items:center;gap:9px;padding:8px 2px;border-bottom:1px solid var(--slm-line);
|
|
3046
|
-
font-size:12.5px;animation:slm-in .35s ease}
|
|
3310
|
+
.slm-feedrow{display:flex!important;width:100%;align-items:center;gap:9px;padding:8px 2px!important;border-bottom:1px solid var(--slm-line)!important;
|
|
3311
|
+
border-radius:6px;font-size:12.5px;text-align:left!important;animation:slm-in .35s ease}
|
|
3312
|
+
.slm-feedrow:hover{background:rgba(255,255,255,.035)!important}
|
|
3047
3313
|
@keyframes slm-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
|
|
3048
3314
|
.slm-feeddot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
3049
3315
|
.slm-feedtext{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
3050
3316
|
.slm-feedtext b{font-weight:800}
|
|
3051
|
-
.slm-
|
|
3317
|
+
.slm-feedsection{display:block;overflow:hidden;text-overflow:ellipsis;color:var(--slm-muted);font-size:10px;font-weight:750}
|
|
3318
|
+
.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}
|
|
3319
|
+
.slm-feedlocate{font-size:9.5px;color:var(--slm-accent);font-weight:800}
|
|
3052
3320
|
.slm-empty{font-size:12.5px;color:var(--slm-muted);padding:12px 0}
|
|
3053
3321
|
|
|
3054
3322
|
/* block toolbar */
|
|
@@ -3118,10 +3386,13 @@ var CSS2 = `
|
|
|
3118
3386
|
.slm-barbtn.on{background:rgba(244,183,64,.13);border-color:#f4b740;color:#f7ca6b}
|
|
3119
3387
|
.slm-sectionlist{display:flex;flex-direction:column;gap:8px;margin-top:4px}
|
|
3120
3388
|
.slm-sectionlist + .slm-eyebrow{margin-top:18px}
|
|
3121
|
-
.slm-sectionrow{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
|
|
3389
|
+
.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}
|
|
3390
|
+
.slm-sectionrow:hover{border-color:var(--slm-muted)!important;transform:translateY(-1px)}
|
|
3122
3391
|
.slm-sectiontop,.slm-sectionmeta{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
3123
3392
|
.slm-sectiontop{font-size:12.5px;font-weight:800}.slm-sectionmeta{margin-top:5px;color:var(--slm-muted);font-size:11px}
|
|
3393
|
+
.slm-sectionmeta>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3124
3394
|
.slm-trend{font-size:10px;text-transform:uppercase;letter-spacing:.08em}.slm-trend.rising{color:#22a06b}.slm-trend.cooling{color:#f4b740}
|
|
3395
|
+
.slm-sectionlocate{color:var(--slm-accent);font-size:9.5px;font-weight:800}
|
|
3125
3396
|
.slm-health{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px}
|
|
3126
3397
|
.slm-healthitem{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
|
|
3127
3398
|
.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}
|
|
@@ -3148,6 +3419,10 @@ var CSS2 = `
|
|
|
3148
3419
|
.slm.compact .slm-barbtn{flex:1;padding:6px 9px}.slm.compact .slm-kpis{grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
|
|
3149
3420
|
.slm.compact .slm-kpi[data-kpi="buyers"],.slm.compact .slm-kpi[data-kpi="active-holds"],
|
|
3150
3421
|
.slm.compact .slm-kpi[data-kpi="sold-pct"],.slm.compact .slm-kpi[data-kpi="gross-sales"]{display:none}
|
|
3422
|
+
@media (prefers-reduced-motion:reduce){
|
|
3423
|
+
.slm.live .slm-live-dot,.slm-feedrow,.slm-kpi.changed b,.slm-kpidelta{animation:none!important}
|
|
3424
|
+
.slm-liveevent,.slm-sectionrow{transition:none!important}
|
|
3425
|
+
}
|
|
3151
3426
|
`;
|
|
3152
3427
|
function injectStyle() {
|
|
3153
3428
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
|
|
@@ -3206,6 +3481,8 @@ var SeatManager = class {
|
|
|
3206
3481
|
this.controlRoomSnapshot = null;
|
|
3207
3482
|
this.trendWindowMinutes = 15;
|
|
3208
3483
|
this.heatEnabled = false;
|
|
3484
|
+
this.lastKpiValues = /* @__PURE__ */ new Map();
|
|
3485
|
+
this.activeKpiDeltas = /* @__PURE__ */ new Map();
|
|
3209
3486
|
// realtime socket
|
|
3210
3487
|
this.ws = null;
|
|
3211
3488
|
this.reconnectTimer = null;
|
|
@@ -3215,6 +3492,10 @@ var SeatManager = class {
|
|
|
3215
3492
|
this.feed = [];
|
|
3216
3493
|
this.feedTimer = null;
|
|
3217
3494
|
this.toastTimer = null;
|
|
3495
|
+
this.liveEventTimer = null;
|
|
3496
|
+
this.kpiCleanupTimer = null;
|
|
3497
|
+
this.followLiveTimer = null;
|
|
3498
|
+
this.followSeatTimer = null;
|
|
3218
3499
|
this.releaseAt = null;
|
|
3219
3500
|
this.layoutObserver = null;
|
|
3220
3501
|
this.tokenExpiresAt = null;
|
|
@@ -3244,11 +3525,22 @@ var SeatManager = class {
|
|
|
3244
3525
|
else return;
|
|
3245
3526
|
event.preventDefault();
|
|
3246
3527
|
};
|
|
3528
|
+
this.onRailClick = (event) => {
|
|
3529
|
+
const target = event.target;
|
|
3530
|
+
const sectionButton = target?.closest("[data-section-focus]");
|
|
3531
|
+
if (sectionButton?.dataset.sectionFocus) {
|
|
3532
|
+
this.locateSection(sectionButton.dataset.sectionFocus);
|
|
3533
|
+
return;
|
|
3534
|
+
}
|
|
3535
|
+
const feedButton = target?.closest("[data-feed-id]");
|
|
3536
|
+
if (feedButton?.dataset.feedId) this.locateActivity(feedButton.dataset.feedId);
|
|
3537
|
+
};
|
|
3247
3538
|
this.sectionOptions = [];
|
|
3248
3539
|
this.opts = options;
|
|
3249
3540
|
this.key = options.eventKey;
|
|
3250
3541
|
this.mode = options.mode ?? "view";
|
|
3251
3542
|
this.keepLive = options.keepLiveWhileHidden ?? true;
|
|
3543
|
+
this.followLive = options.followLive ?? false;
|
|
3252
3544
|
this.currency = options.currency ?? "USD";
|
|
3253
3545
|
this.tokenExpiresAt = options.tokenExpiresAt ?? null;
|
|
3254
3546
|
this.api = new ManageApi(options.apiBase ?? DEFAULT_API_BASE3, options.token);
|
|
@@ -3305,6 +3597,19 @@ var SeatManager = class {
|
|
|
3305
3597
|
this.applyHeatOverlay();
|
|
3306
3598
|
this.paintHeatButton();
|
|
3307
3599
|
}
|
|
3600
|
+
/** Toggle opt-in camera following for new buyer hold/book events. */
|
|
3601
|
+
setFollowLive(enabled) {
|
|
3602
|
+
const changed = this.followLive !== enabled;
|
|
3603
|
+
this.followLive = enabled;
|
|
3604
|
+
if (!enabled) {
|
|
3605
|
+
if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
|
|
3606
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
3607
|
+
this.followLiveTimer = null;
|
|
3608
|
+
this.followSeatTimer = null;
|
|
3609
|
+
}
|
|
3610
|
+
this.paintFollowLiveButton();
|
|
3611
|
+
if (changed) this.opts.onFollowLiveChange?.(enabled);
|
|
3612
|
+
}
|
|
3308
3613
|
/** Change the current-vs-previous sales window and refresh the private projection. */
|
|
3309
3614
|
setTrendWindow(windowMinutes) {
|
|
3310
3615
|
const normalized = Number.isFinite(windowMinutes) ? Math.floor(windowMinutes) : 15;
|
|
@@ -3477,6 +3782,7 @@ var SeatManager = class {
|
|
|
3477
3782
|
return Promise.resolve();
|
|
3478
3783
|
}
|
|
3479
3784
|
zoomToFit() {
|
|
3785
|
+
this.renderer?.clearSectionFocus();
|
|
3480
3786
|
this.renderer?.zoomToFit();
|
|
3481
3787
|
}
|
|
3482
3788
|
destroy() {
|
|
@@ -3484,12 +3790,17 @@ var SeatManager = class {
|
|
|
3484
3790
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
3485
3791
|
if (this.feedTimer) clearInterval(this.feedTimer);
|
|
3486
3792
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
3793
|
+
if (this.liveEventTimer) clearTimeout(this.liveEventTimer);
|
|
3794
|
+
if (this.kpiCleanupTimer) clearTimeout(this.kpiCleanupTimer);
|
|
3795
|
+
if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
|
|
3796
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
3487
3797
|
if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
|
|
3488
3798
|
if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
|
|
3489
3799
|
if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
|
|
3490
3800
|
this.layoutObserver?.disconnect();
|
|
3491
3801
|
this.layoutObserver = null;
|
|
3492
3802
|
this.root?.removeEventListener("keydown", this.onKeyDown);
|
|
3803
|
+
this.els.rail?.removeEventListener("click", this.onRailClick);
|
|
3493
3804
|
if (typeof document !== "undefined") document.removeEventListener("fullscreenchange", this.onFullscreenChange);
|
|
3494
3805
|
if (this.ws) {
|
|
3495
3806
|
try {
|
|
@@ -3627,7 +3938,6 @@ var SeatManager = class {
|
|
|
3627
3938
|
const id = this.labelToId.get(ch.label);
|
|
3628
3939
|
if (id) {
|
|
3629
3940
|
this.renderer?.setStatus([id], toRenderStatus(st));
|
|
3630
|
-
this.flash(id, st);
|
|
3631
3941
|
ids.push(id);
|
|
3632
3942
|
}
|
|
3633
3943
|
const verb = this.verbFor(prev, st);
|
|
@@ -3636,7 +3946,10 @@ var SeatManager = class {
|
|
|
3636
3946
|
group.labels.push(ch.label);
|
|
3637
3947
|
groups.set(groupKey, group);
|
|
3638
3948
|
}
|
|
3639
|
-
for (const group of groups.values())
|
|
3949
|
+
for (const group of groups.values()) {
|
|
3950
|
+
const activity = this.pushActivity(group.labels, group.verb, group.status);
|
|
3951
|
+
if (activity) this.paintSpatialActivity(activity);
|
|
3952
|
+
}
|
|
3640
3953
|
if (ids.length) {
|
|
3641
3954
|
this.lastSyncedAt = Date.now();
|
|
3642
3955
|
this.afterPaint();
|
|
@@ -3682,9 +3995,108 @@ var SeatManager = class {
|
|
|
3682
3995
|
this.renderer?.forceDraw();
|
|
3683
3996
|
}
|
|
3684
3997
|
}
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3998
|
+
activityColor(status) {
|
|
3999
|
+
return status === "held" ? "#f4b740" : status === "booked" ? "#22a06b" : status === "blocked" ? "#8b94ac" : "#6e7bff";
|
|
4000
|
+
}
|
|
4001
|
+
sectionsForLabels(labels) {
|
|
4002
|
+
const ids = /* @__PURE__ */ new Set();
|
|
4003
|
+
for (const label of labels) {
|
|
4004
|
+
const seat = this.labelToSeat.get(label);
|
|
4005
|
+
if (!seat) continue;
|
|
4006
|
+
const sectionId = this.sectionByObject.get(seat.rowId);
|
|
4007
|
+
if (sectionId && sectionId !== import_core3.UNGROUPED_ID) ids.add(sectionId);
|
|
4008
|
+
}
|
|
4009
|
+
const sectionIds = [...ids];
|
|
4010
|
+
return {
|
|
4011
|
+
ids: sectionIds,
|
|
4012
|
+
labels: sectionIds.map((id) => this.sectionLabelById.get(id) ?? id)
|
|
4013
|
+
};
|
|
4014
|
+
}
|
|
4015
|
+
pulseSeatLabels(labels, status) {
|
|
4016
|
+
const color = this.activityColor(status);
|
|
4017
|
+
for (const label of labels.slice(0, MAX_LIVE_SEAT_PULSES)) {
|
|
4018
|
+
const id = this.labelToId.get(label);
|
|
4019
|
+
if (id) this.renderer?.flashSeat(id, color);
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
/** Render one grouped realtime operation at the right semantic zoom level. */
|
|
4023
|
+
paintSpatialActivity(activity) {
|
|
4024
|
+
const sectionIds = activity.sectionIds ?? this.sectionsForLabels(activity.labels).ids;
|
|
4025
|
+
const focused = this.renderer?.getFocusedSection() ?? null;
|
|
4026
|
+
const followable = this.followLive && sectionIds.length === 1 && (activity.status === "held" || activity.status === "booked");
|
|
4027
|
+
if (followable && focused === sectionIds[0]) {
|
|
4028
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
4029
|
+
return;
|
|
4030
|
+
}
|
|
4031
|
+
if (followable) {
|
|
4032
|
+
if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
|
|
4033
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
4034
|
+
this.followLiveTimer = setTimeout(() => {
|
|
4035
|
+
this.followLiveTimer = null;
|
|
4036
|
+
this.renderer?.focusSection(sectionIds[0]);
|
|
4037
|
+
this.followSeatTimer = setTimeout(() => {
|
|
4038
|
+
this.followSeatTimer = null;
|
|
4039
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
4040
|
+
}, 520);
|
|
4041
|
+
}, 220);
|
|
4042
|
+
return;
|
|
4043
|
+
}
|
|
4044
|
+
if (!focused && sectionIds.length) {
|
|
4045
|
+
const color = this.activityColor(activity.status);
|
|
4046
|
+
for (const sectionId of sectionIds.slice(0, MAX_LIVE_SECTION_PULSES)) {
|
|
4047
|
+
this.renderer?.flashSection(sectionId, color);
|
|
4048
|
+
}
|
|
4049
|
+
return;
|
|
4050
|
+
}
|
|
4051
|
+
if (!sectionIds.length || focused && sectionIds.includes(focused)) {
|
|
4052
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
locateSection(sectionId) {
|
|
4056
|
+
this.renderer?.focusSection(sectionId);
|
|
4057
|
+
}
|
|
4058
|
+
locateActivity(activityId) {
|
|
4059
|
+
const activity = this.feed.find((item) => item.id === activityId);
|
|
4060
|
+
if (!activity) return;
|
|
4061
|
+
const sectionIds = activity.sectionIds ?? this.sectionsForLabels(activity.labels).ids;
|
|
4062
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
4063
|
+
if (sectionIds.length === 1) {
|
|
4064
|
+
this.locateSection(sectionIds[0]);
|
|
4065
|
+
this.followSeatTimer = setTimeout(() => {
|
|
4066
|
+
this.followSeatTimer = null;
|
|
4067
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
4068
|
+
}, 520);
|
|
4069
|
+
return;
|
|
4070
|
+
}
|
|
4071
|
+
this.zoomToFit();
|
|
4072
|
+
this.followSeatTimer = setTimeout(() => {
|
|
4073
|
+
this.followSeatTimer = null;
|
|
4074
|
+
if (sectionIds.length) {
|
|
4075
|
+
const color = this.activityColor(activity.status);
|
|
4076
|
+
for (const sectionId of sectionIds.slice(0, MAX_LIVE_SECTION_PULSES)) {
|
|
4077
|
+
this.renderer?.flashSection(sectionId, color);
|
|
4078
|
+
}
|
|
4079
|
+
} else {
|
|
4080
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
4081
|
+
}
|
|
4082
|
+
}, 280);
|
|
4083
|
+
}
|
|
4084
|
+
showLiveEvent(activity) {
|
|
4085
|
+
const element = this.els.liveevent;
|
|
4086
|
+
if (!element) return;
|
|
4087
|
+
const sections = activity.sectionLabels ?? [];
|
|
4088
|
+
const place = sections.length === 1 ? sections[0] : sections.length > 1 ? `${sections.length} sections` : activity.label;
|
|
4089
|
+
const noun = activity.count === 1 ? "seat" : "seats";
|
|
4090
|
+
element.innerHTML = `<span class="slm-liveeventdot" style="background:${this.activityColor(activity.status)}"></span>
|
|
4091
|
+
<span class="slm-liveeventcopy">${esc(place)} \xB7 ${activity.count.toLocaleString()} ${noun} ${esc(activity.verb)}</span>
|
|
4092
|
+
<span class="slm-liveeventhint">Live</span>`;
|
|
4093
|
+
element.classList.add("on");
|
|
4094
|
+
if (this.liveEventTimer) clearTimeout(this.liveEventTimer);
|
|
4095
|
+
this.liveEventTimer = setTimeout(() => {
|
|
4096
|
+
this.liveEventTimer = null;
|
|
4097
|
+
element.classList.remove("on");
|
|
4098
|
+
element.innerHTML = "";
|
|
4099
|
+
}, 2800);
|
|
3688
4100
|
}
|
|
3689
4101
|
// ---- tallies + feed -------------------------------------------------------
|
|
3690
4102
|
applyReportRevenue(report) {
|
|
@@ -3767,7 +4179,8 @@ var SeatManager = class {
|
|
|
3767
4179
|
}
|
|
3768
4180
|
pushActivity(labels, verb, status, at = Date.now()) {
|
|
3769
4181
|
const label = labels[0];
|
|
3770
|
-
if (!label) return;
|
|
4182
|
+
if (!label) return null;
|
|
4183
|
+
const sections = this.sectionsForLabels(labels);
|
|
3771
4184
|
const item = {
|
|
3772
4185
|
id: `${label}:${at}:${Math.random().toString(36).slice(2, 6)}`,
|
|
3773
4186
|
at,
|
|
@@ -3775,12 +4188,16 @@ var SeatManager = class {
|
|
|
3775
4188
|
labels: [...labels],
|
|
3776
4189
|
count: labels.length,
|
|
3777
4190
|
verb,
|
|
3778
|
-
status
|
|
4191
|
+
status,
|
|
4192
|
+
sectionIds: sections.ids,
|
|
4193
|
+
sectionLabels: sections.labels
|
|
3779
4194
|
};
|
|
3780
4195
|
this.feed.unshift(item);
|
|
3781
4196
|
if (this.feed.length > FEED_CAP) this.feed.length = FEED_CAP;
|
|
3782
4197
|
if (this.mode === "view") this.paintFeed();
|
|
4198
|
+
this.showLiveEvent(item);
|
|
3783
4199
|
this.opts.onActivity?.(item);
|
|
4200
|
+
return item;
|
|
3784
4201
|
}
|
|
3785
4202
|
seedFeed(entries) {
|
|
3786
4203
|
const verbByAction = {
|
|
@@ -3804,6 +4221,7 @@ var SeatManager = class {
|
|
|
3804
4221
|
for (const e of entries) {
|
|
3805
4222
|
const label = e.labels[0];
|
|
3806
4223
|
if (!label) continue;
|
|
4224
|
+
const sections = this.sectionsForLabels(e.labels);
|
|
3807
4225
|
const item = {
|
|
3808
4226
|
id: `log:${e.id}`,
|
|
3809
4227
|
at: e.at,
|
|
@@ -3811,7 +4229,9 @@ var SeatManager = class {
|
|
|
3811
4229
|
labels: [...e.labels],
|
|
3812
4230
|
count: e.labels.length,
|
|
3813
4231
|
verb: verbByAction[e.action] ?? e.action,
|
|
3814
|
-
status: stByAction[e.action] ?? "free"
|
|
4232
|
+
status: stByAction[e.action] ?? "free",
|
|
4233
|
+
sectionIds: sections.ids,
|
|
4234
|
+
sectionLabels: sections.labels
|
|
3815
4235
|
};
|
|
3816
4236
|
this.feed.push(item);
|
|
3817
4237
|
this.opts.onActivity?.(item);
|
|
@@ -3849,13 +4269,15 @@ var SeatManager = class {
|
|
|
3849
4269
|
for (const [k, v] of Object.entries(vars)) root.style.setProperty(k, v);
|
|
3850
4270
|
root.innerHTML = `
|
|
3851
4271
|
<div class="slm-bar">
|
|
3852
|
-
<div class="slm-modes" data-ref="modes">
|
|
3853
|
-
<button class="slm-mode" data-mode="view" title="Monitor (M)" aria-keyshortcuts="M">Monitor</button>
|
|
3854
|
-
<button class="slm-mode" data-mode="inspect" title="Inspect (I)" aria-keyshortcuts="I">Inspect</button>
|
|
3855
|
-
<button class="slm-mode" data-mode="block" title="Block (B)" aria-keyshortcuts="B">Block</button>
|
|
4272
|
+
<div class="slm-modes" data-ref="modes" role="tablist" aria-label="Manager tools">
|
|
4273
|
+
<button class="slm-mode" role="tab" data-mode="view" title="Monitor (M)" aria-keyshortcuts="M">Monitor</button>
|
|
4274
|
+
<button class="slm-mode" role="tab" data-mode="inspect" title="Inspect (I)" aria-keyshortcuts="I">Inspect</button>
|
|
4275
|
+
<button class="slm-mode" role="tab" data-mode="block" title="Block (B)" aria-keyshortcuts="B">Block</button>
|
|
3856
4276
|
</div>
|
|
3857
4277
|
<span class="slm-live"><span class="slm-live-dot"></span><span data-ref="livetext">CONNECTING</span></span>
|
|
3858
4278
|
<div class="slm-bar-actions">
|
|
4279
|
+
<button class="slm-barbtn follow" data-ref="follow" aria-pressed="false"
|
|
4280
|
+
title="Stay on the current map view unless enabled">Follow live</button>
|
|
3859
4281
|
<button class="slm-barbtn" data-ref="heat" aria-pressed="false"
|
|
3860
4282
|
aria-label="Sales momentum overlay off"
|
|
3861
4283
|
title="Highlight sections selling fastest in the selected time window">Sales momentum</button>
|
|
@@ -3867,6 +4289,7 @@ var SeatManager = class {
|
|
|
3867
4289
|
<div class="slm-map">
|
|
3868
4290
|
<div class="slm-map-host" data-ref="maphost"></div>
|
|
3869
4291
|
<div class="slm-zoomhint" data-ref="zoomhint">Zoom in to marquee-select</div>
|
|
4292
|
+
<div class="slm-liveevent" data-ref="liveevent" role="status" aria-live="polite"></div>
|
|
3870
4293
|
<div class="slm-hud"><button class="slm-hud-chip" data-ref="zfit">Zoom to fit</button></div>
|
|
3871
4294
|
</div>
|
|
3872
4295
|
<aside class="slm-rail"><div class="slm-railscroll" data-ref="rail"></div></aside>
|
|
@@ -3886,20 +4309,25 @@ var SeatManager = class {
|
|
|
3886
4309
|
modes: ref("modes"),
|
|
3887
4310
|
livetext: ref("livetext"),
|
|
3888
4311
|
kpis: ref("kpis"),
|
|
4312
|
+
follow: ref("follow"),
|
|
3889
4313
|
heat: ref("heat"),
|
|
3890
4314
|
fullscreen: ref("fullscreen"),
|
|
3891
4315
|
zoomhint: ref("zoomhint"),
|
|
4316
|
+
liveevent: ref("liveevent"),
|
|
3892
4317
|
rail: ref("rail"),
|
|
3893
4318
|
toast: ref("toast"),
|
|
3894
4319
|
zfit: ref("zfit")
|
|
3895
4320
|
};
|
|
3896
4321
|
this.els.modes.querySelectorAll("[data-mode]").forEach((b) => b.addEventListener("click", () => this.setMode(b.dataset.mode)));
|
|
3897
4322
|
this.els.zfit.addEventListener("click", () => this.zoomToFit());
|
|
4323
|
+
this.els.follow.addEventListener("click", () => this.setFollowLive(!this.followLive));
|
|
3898
4324
|
this.els.heat.addEventListener("click", () => this.setHeatOverlay(!this.heatEnabled));
|
|
3899
4325
|
this.els.fullscreen.addEventListener("click", () => this.toggleFullscreen());
|
|
3900
4326
|
root.addEventListener("keydown", this.onKeyDown);
|
|
4327
|
+
this.els.rail.addEventListener("click", this.onRailClick);
|
|
3901
4328
|
document.addEventListener("fullscreenchange", this.onFullscreenChange);
|
|
3902
4329
|
this.paintModeTabs();
|
|
4330
|
+
this.paintFollowLiveButton();
|
|
3903
4331
|
this.paintHeatButton();
|
|
3904
4332
|
this.paintFullscreenButton();
|
|
3905
4333
|
}
|
|
@@ -3928,10 +4356,20 @@ var SeatManager = class {
|
|
|
3928
4356
|
paintModeTabs() {
|
|
3929
4357
|
this.els.modes?.querySelectorAll("[data-mode]").forEach((b) => {
|
|
3930
4358
|
const el = b;
|
|
3931
|
-
|
|
4359
|
+
const active = el.dataset.mode === this.mode;
|
|
4360
|
+
el.classList.toggle("on", active);
|
|
4361
|
+
el.setAttribute("aria-selected", String(active));
|
|
4362
|
+
el.tabIndex = active ? 0 : -1;
|
|
3932
4363
|
});
|
|
3933
4364
|
this.root?.classList.toggle("block-mode", this.mode === "block");
|
|
3934
4365
|
}
|
|
4366
|
+
paintFollowLiveButton() {
|
|
4367
|
+
const button = this.els.follow;
|
|
4368
|
+
if (!button) return;
|
|
4369
|
+
button.classList.toggle("on", this.followLive);
|
|
4370
|
+
button.setAttribute("aria-pressed", String(this.followLive));
|
|
4371
|
+
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.");
|
|
4372
|
+
}
|
|
3935
4373
|
paintHeatButton() {
|
|
3936
4374
|
const button = this.els.heat;
|
|
3937
4375
|
if (!button) return;
|
|
@@ -3972,20 +4410,55 @@ var SeatManager = class {
|
|
|
3972
4410
|
const show = this.mode === "block" && this.renderer?.getRung?.() !== "seats";
|
|
3973
4411
|
hint.classList.toggle("on", !!show);
|
|
3974
4412
|
}
|
|
4413
|
+
formatKpiDelta(key, delta, currency) {
|
|
4414
|
+
const sign = delta > 0 ? "+" : "\u2212";
|
|
4415
|
+
const absolute = Math.abs(delta);
|
|
4416
|
+
if (key === "gross-sales") return `${sign}${fmtMoney(absolute, currency)}`;
|
|
4417
|
+
if (key === "sold-pct") return `${sign}${absolute.toLocaleString()}pt`;
|
|
4418
|
+
return `${sign}${absolute.toLocaleString()}`;
|
|
4419
|
+
}
|
|
3975
4420
|
paintKpis(t3) {
|
|
3976
4421
|
if (!this.els.kpis) return;
|
|
3977
4422
|
const rev = t3.revenueStatus === "current" ? fmtMoney(t3.grossRevenue, t3.currency) : "\u2014";
|
|
3978
4423
|
const presence = this.controlRoomSnapshot?.presence;
|
|
3979
|
-
|
|
3980
|
-
{ key: "sold-seats", n: t3.booked.toLocaleString(), l: "Sold seats", dot: "#22a06b" },
|
|
3981
|
-
{ key: "held-seats", n: t3.held.toLocaleString(), l: "Held seats", dot: "#f4b740" },
|
|
3982
|
-
{ key: "buyers", n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Buyers" },
|
|
3983
|
-
{ key: "active-holds", n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Active holds" },
|
|
3984
|
-
{ key: "free-seats", n: t3.free.toLocaleString(), l: "Free seats", dot: "#6e7bff" },
|
|
3985
|
-
{ key: "blocked", n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac" },
|
|
3986
|
-
{ key: "sold-pct", n: `${t3.capacityPct}%`, l: "Sold" },
|
|
3987
|
-
{ key: "gross-sales", n: rev, l: "Gross sales" }
|
|
3988
|
-
]
|
|
4424
|
+
const items = [
|
|
4425
|
+
{ key: "sold-seats", raw: t3.booked, n: t3.booked.toLocaleString(), l: "Sold seats", dot: "#22a06b" },
|
|
4426
|
+
{ key: "held-seats", raw: t3.held, n: t3.held.toLocaleString(), l: "Held seats", dot: "#f4b740" },
|
|
4427
|
+
{ key: "buyers", raw: presence?.shoppingSessions ?? null, n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Buyers" },
|
|
4428
|
+
{ key: "active-holds", raw: presence?.activeHolds ?? null, n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Active holds" },
|
|
4429
|
+
{ key: "free-seats", raw: t3.free, n: t3.free.toLocaleString(), l: "Free seats", dot: "#6e7bff" },
|
|
4430
|
+
{ key: "blocked", raw: t3.blocked, n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac" },
|
|
4431
|
+
{ key: "sold-pct", raw: t3.capacityPct, n: `${t3.capacityPct}%`, l: "Sold" },
|
|
4432
|
+
{ key: "gross-sales", raw: t3.revenueStatus === "current" ? t3.grossRevenue : null, n: rev, l: "Gross sales" }
|
|
4433
|
+
];
|
|
4434
|
+
let hasChanges = false;
|
|
4435
|
+
this.els.kpis.innerHTML = items.map((item) => {
|
|
4436
|
+
const previous = this.lastKpiValues.get(item.key);
|
|
4437
|
+
const changed = item.raw != null && previous != null && item.raw !== previous;
|
|
4438
|
+
const delta = changed ? item.raw - previous : 0;
|
|
4439
|
+
if (changed) {
|
|
4440
|
+
hasChanges = true;
|
|
4441
|
+
this.activeKpiDeltas.set(item.key, {
|
|
4442
|
+
text: this.formatKpiDelta(item.key, delta, t3.currency),
|
|
4443
|
+
down: delta < 0
|
|
4444
|
+
});
|
|
4445
|
+
}
|
|
4446
|
+
if (item.raw != null) this.lastKpiValues.set(item.key, item.raw);
|
|
4447
|
+
const activeDelta = this.activeKpiDeltas.get(item.key);
|
|
4448
|
+
return `<div class="slm-kpi${activeDelta ? " changed" : ""}" data-kpi="${item.key}">
|
|
4449
|
+
<b>${item.dot ? `<span class="dot" style="background:${item.dot}"></span>` : ""}${item.n}</b><span>${item.l}</span>
|
|
4450
|
+
${activeDelta ? `<span class="slm-kpidelta${activeDelta.down ? " down" : ""}">${activeDelta.text}</span>` : ""}
|
|
4451
|
+
</div>`;
|
|
4452
|
+
}).join("");
|
|
4453
|
+
if (hasChanges) {
|
|
4454
|
+
if (this.kpiCleanupTimer) clearTimeout(this.kpiCleanupTimer);
|
|
4455
|
+
this.kpiCleanupTimer = setTimeout(() => {
|
|
4456
|
+
this.kpiCleanupTimer = null;
|
|
4457
|
+
this.activeKpiDeltas.clear();
|
|
4458
|
+
this.els.kpis?.querySelectorAll(".slm-kpidelta").forEach((element) => element.remove());
|
|
4459
|
+
this.els.kpis?.querySelectorAll(".slm-kpi.changed").forEach((element) => element.classList.remove("changed"));
|
|
4460
|
+
}, 1500);
|
|
4461
|
+
}
|
|
3989
4462
|
}
|
|
3990
4463
|
// ---- DOM: rails -----------------------------------------------------------
|
|
3991
4464
|
paintRail() {
|
|
@@ -4056,10 +4529,10 @@ var SeatManager = class {
|
|
|
4056
4529
|
const net = speed?.netBooked ?? 0;
|
|
4057
4530
|
const netLabel = `${net > 0 ? "+" : ""}${net}`;
|
|
4058
4531
|
const trend = speed?.trend === "rising" || speed?.trend === "cooling" ? speed.trend : "steady";
|
|
4059
|
-
return `<
|
|
4060
|
-
<
|
|
4061
|
-
<
|
|
4062
|
-
</
|
|
4532
|
+
return `<button type="button" class="slm-sectionrow" data-section-focus="${esc(row.sectionId)}" title="Focus ${esc(row.sectionLabel)} on the map">
|
|
4533
|
+
<span class="slm-sectiontop"><span>${esc(row.sectionLabel)}</span><span>${fmtMoney(row.bookedRevenue, snapshot.currency)}</span></span>
|
|
4534
|
+
<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>
|
|
4535
|
+
</button>`;
|
|
4063
4536
|
}).join("") : '<div class="slm-empty">No section metrics are available for this chart.</div>';
|
|
4064
4537
|
this.paintTrendWindow();
|
|
4065
4538
|
this.paintMomentumHelp();
|
|
@@ -4128,9 +4601,13 @@ var SeatManager = class {
|
|
|
4128
4601
|
const color = { free: "#6e7bff", held: "#f4b740", booked: "#22a06b", blocked: "#8b94ac" };
|
|
4129
4602
|
this.els.feed.innerHTML = this.feed.map((a) => {
|
|
4130
4603
|
const extra = a.count > 1 ? ` +${a.count - 1}` : "";
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4604
|
+
const sections = a.sectionLabels ?? [];
|
|
4605
|
+
const sectionCopy = sections.length === 1 ? sections[0] : sections.length > 1 ? `${sections.length} sections` : "";
|
|
4606
|
+
return `<button type="button" class="slm-feedrow" data-feed-id="${esc(a.id)}" title="Locate this activity on the map">
|
|
4607
|
+
<span class="slm-feeddot" style="background:${color[a.status]}"></span>
|
|
4608
|
+
<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>
|
|
4609
|
+
<span class="slm-feedmeta"><span class="slm-feedtime">${relTime(a.at, now)}</span><span class="slm-feedlocate">Locate</span></span>
|
|
4610
|
+
</button>`;
|
|
4134
4611
|
}).join("");
|
|
4135
4612
|
}
|
|
4136
4613
|
renderBlockRail() {
|
|
@@ -4386,6 +4863,10 @@ var SeatManager = class {
|
|
|
4386
4863
|
// ---- toast / done / fail --------------------------------------------------
|
|
4387
4864
|
done(action, labels, msg) {
|
|
4388
4865
|
this.toastOk(msg);
|
|
4866
|
+
if (labels.length) {
|
|
4867
|
+
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;
|
|
4868
|
+
if (activity) this.paintSpatialActivity(activity);
|
|
4869
|
+
}
|
|
4389
4870
|
if (action !== "setHoldTtl") this.scheduleRevenueRefresh(0);
|
|
4390
4871
|
this.opts.onActionComplete?.({ action, labels, count: labels.length });
|
|
4391
4872
|
}
|