@seatlayer/js 0.16.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +269 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -1
- package/dist/index.d.ts +30 -1
- package/dist/index.js +269 -35
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2966,6 +2966,8 @@ function toRenderStatus(s) {
|
|
|
2966
2966
|
var DEFAULT_API_BASE3 = "https://api.seatlayer.io";
|
|
2967
2967
|
var STYLE_ID2 = "seatlayer-manager-style";
|
|
2968
2968
|
var FEED_CAP = 80;
|
|
2969
|
+
var MAX_LIVE_SEAT_PULSES = 16;
|
|
2970
|
+
var MAX_LIVE_SECTION_PULSES = 4;
|
|
2969
2971
|
var LEGEND = [
|
|
2970
2972
|
{ key: "free", label: "Free", color: "#6e7bff" },
|
|
2971
2973
|
{ key: "held", label: "Held", color: "#f4b740" },
|
|
@@ -2991,13 +2993,21 @@ var CSS2 = `
|
|
|
2991
2993
|
@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
2994
|
.slm-kpis{grid-column:1/-1;display:grid;grid-template-columns:repeat(8,minmax(0,1fr));width:100%;padding-top:10px;
|
|
2993
2995
|
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}
|
|
2996
|
+
.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
2997
|
.slm-kpi b{display:flex;min-width:0;align-items:baseline;justify-content:center;font-size:17px;font-weight:800;
|
|
2996
2998
|
font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
2997
2999
|
.slm-kpi span{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--slm-muted);font-weight:700}
|
|
2998
3000
|
.slm-kpi .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:baseline}
|
|
3001
|
+
.slm-kpi.changed b{animation:slm-kpi-bump .58s cubic-bezier(.2,.8,.2,1)}
|
|
3002
|
+
.slm-kpidelta{position:absolute;right:4px;top:-12px;padding:2px 5px;border-radius:999px;background:rgba(34,160,107,.17);
|
|
3003
|
+
color:#5bd39b!important;font-size:9px!important;letter-spacing:0!important;text-transform:none!important;white-space:nowrap;
|
|
3004
|
+
animation:slm-kpi-delta 1.45s ease-out both;pointer-events:none}
|
|
3005
|
+
.slm-kpidelta.down{background:rgba(244,183,64,.14);color:#f7ca6b!important}
|
|
3006
|
+
@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)}}
|
|
3007
|
+
@keyframes slm-kpi-delta{0%{opacity:0;transform:translateY(5px)}18%,72%{opacity:1;transform:none}100%{opacity:0;transform:translateY(-5px)}}
|
|
2999
3008
|
.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
3009
|
.slm-barbtn:hover{border-color:var(--slm-muted)}
|
|
3010
|
+
.slm-barbtn.follow.on{background:rgba(34,160,107,.13);border-color:#22a06b;color:#5bd39b}
|
|
3001
3011
|
|
|
3002
3012
|
/* body */
|
|
3003
3013
|
.slm-body{display:flex;flex:1;min-height:0}
|
|
@@ -3009,6 +3019,14 @@ var CSS2 = `
|
|
|
3009
3019
|
.slm-zoomhint{position:absolute;left:50%;top:14px;transform:translateX(-50%);padding:6px 13px;border-radius:999px;
|
|
3010
3020
|
background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:700;pointer-events:none;opacity:0;transition:opacity .2s}
|
|
3011
3021
|
.slm-zoomhint.on{opacity:1}
|
|
3022
|
+
.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));
|
|
3023
|
+
padding:8px 12px;border:1px solid var(--slm-line);border-radius:999px;background:color-mix(in srgb,var(--slm-surface) 92%,transparent);
|
|
3024
|
+
box-shadow:0 10px 34px rgba(0,0,0,.32);opacity:0;transform:translate(-50%,-8px);pointer-events:none;
|
|
3025
|
+
transition:opacity .18s ease,transform .24s ease;backdrop-filter:blur(10px)}
|
|
3026
|
+
.slm-liveevent.on{opacity:1;transform:translate(-50%,0)}
|
|
3027
|
+
.slm.block-mode .slm-liveevent{top:52px}
|
|
3028
|
+
.slm-liveeventdot{width:8px;height:8px;border-radius:50%;flex:none}.slm-liveeventcopy{min-width:0;overflow:hidden;text-overflow:ellipsis;
|
|
3029
|
+
white-space:nowrap;font-size:12px;font-weight:800}.slm-liveeventhint{color:var(--slm-muted);font-size:10px;white-space:nowrap}
|
|
3012
3030
|
.slm-rail{width:320px;flex:none;border-left:1px solid var(--slm-line);display:flex;flex-direction:column;min-height:0}
|
|
3013
3031
|
.slm-railscroll{flex:1;overflow-y:auto;padding:16px}
|
|
3014
3032
|
.slm-eyebrow{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--slm-muted);font-weight:800;margin-bottom:6px}
|
|
@@ -3023,13 +3041,16 @@ var CSS2 = `
|
|
|
3023
3041
|
|
|
3024
3042
|
/* activity feed */
|
|
3025
3043
|
.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}
|
|
3044
|
+
.slm-feedrow{display:flex!important;width:100%;align-items:center;gap:9px;padding:8px 2px!important;border-bottom:1px solid var(--slm-line)!important;
|
|
3045
|
+
border-radius:6px;font-size:12.5px;text-align:left!important;animation:slm-in .35s ease}
|
|
3046
|
+
.slm-feedrow:hover{background:rgba(255,255,255,.035)!important}
|
|
3028
3047
|
@keyframes slm-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
|
|
3029
3048
|
.slm-feeddot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
3030
3049
|
.slm-feedtext{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
3031
3050
|
.slm-feedtext b{font-weight:800}
|
|
3032
|
-
.slm-
|
|
3051
|
+
.slm-feedsection{display:block;overflow:hidden;text-overflow:ellipsis;color:var(--slm-muted);font-size:10px;font-weight:750}
|
|
3052
|
+
.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}
|
|
3053
|
+
.slm-feedlocate{font-size:9.5px;color:var(--slm-accent);font-weight:800}
|
|
3033
3054
|
.slm-empty{font-size:12.5px;color:var(--slm-muted);padding:12px 0}
|
|
3034
3055
|
|
|
3035
3056
|
/* block toolbar */
|
|
@@ -3099,10 +3120,13 @@ var CSS2 = `
|
|
|
3099
3120
|
.slm-barbtn.on{background:rgba(244,183,64,.13);border-color:#f4b740;color:#f7ca6b}
|
|
3100
3121
|
.slm-sectionlist{display:flex;flex-direction:column;gap:8px;margin-top:4px}
|
|
3101
3122
|
.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)}
|
|
3123
|
+
.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}
|
|
3124
|
+
.slm-sectionrow:hover{border-color:var(--slm-muted)!important;transform:translateY(-1px)}
|
|
3103
3125
|
.slm-sectiontop,.slm-sectionmeta{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
3104
3126
|
.slm-sectiontop{font-size:12.5px;font-weight:800}.slm-sectionmeta{margin-top:5px;color:var(--slm-muted);font-size:11px}
|
|
3127
|
+
.slm-sectionmeta>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3105
3128
|
.slm-trend{font-size:10px;text-transform:uppercase;letter-spacing:.08em}.slm-trend.rising{color:#22a06b}.slm-trend.cooling{color:#f4b740}
|
|
3129
|
+
.slm-sectionlocate{color:var(--slm-accent);font-size:9.5px;font-weight:800}
|
|
3106
3130
|
.slm-health{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px}
|
|
3107
3131
|
.slm-healthitem{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
|
|
3108
3132
|
.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 +3153,10 @@ var CSS2 = `
|
|
|
3129
3153
|
.slm.compact .slm-barbtn{flex:1;padding:6px 9px}.slm.compact .slm-kpis{grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
|
|
3130
3154
|
.slm.compact .slm-kpi[data-kpi="buyers"],.slm.compact .slm-kpi[data-kpi="active-holds"],
|
|
3131
3155
|
.slm.compact .slm-kpi[data-kpi="sold-pct"],.slm.compact .slm-kpi[data-kpi="gross-sales"]{display:none}
|
|
3156
|
+
@media (prefers-reduced-motion:reduce){
|
|
3157
|
+
.slm.live .slm-live-dot,.slm-feedrow,.slm-kpi.changed b,.slm-kpidelta{animation:none!important}
|
|
3158
|
+
.slm-liveevent,.slm-sectionrow{transition:none!important}
|
|
3159
|
+
}
|
|
3132
3160
|
`;
|
|
3133
3161
|
function injectStyle() {
|
|
3134
3162
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
|
|
@@ -3187,6 +3215,8 @@ var SeatManager = class {
|
|
|
3187
3215
|
this.controlRoomSnapshot = null;
|
|
3188
3216
|
this.trendWindowMinutes = 15;
|
|
3189
3217
|
this.heatEnabled = false;
|
|
3218
|
+
this.lastKpiValues = /* @__PURE__ */ new Map();
|
|
3219
|
+
this.activeKpiDeltas = /* @__PURE__ */ new Map();
|
|
3190
3220
|
// realtime socket
|
|
3191
3221
|
this.ws = null;
|
|
3192
3222
|
this.reconnectTimer = null;
|
|
@@ -3196,6 +3226,10 @@ var SeatManager = class {
|
|
|
3196
3226
|
this.feed = [];
|
|
3197
3227
|
this.feedTimer = null;
|
|
3198
3228
|
this.toastTimer = null;
|
|
3229
|
+
this.liveEventTimer = null;
|
|
3230
|
+
this.kpiCleanupTimer = null;
|
|
3231
|
+
this.followLiveTimer = null;
|
|
3232
|
+
this.followSeatTimer = null;
|
|
3199
3233
|
this.releaseAt = null;
|
|
3200
3234
|
this.layoutObserver = null;
|
|
3201
3235
|
this.tokenExpiresAt = null;
|
|
@@ -3225,11 +3259,22 @@ var SeatManager = class {
|
|
|
3225
3259
|
else return;
|
|
3226
3260
|
event.preventDefault();
|
|
3227
3261
|
};
|
|
3262
|
+
this.onRailClick = (event) => {
|
|
3263
|
+
const target = event.target;
|
|
3264
|
+
const sectionButton = target?.closest("[data-section-focus]");
|
|
3265
|
+
if (sectionButton?.dataset.sectionFocus) {
|
|
3266
|
+
this.locateSection(sectionButton.dataset.sectionFocus);
|
|
3267
|
+
return;
|
|
3268
|
+
}
|
|
3269
|
+
const feedButton = target?.closest("[data-feed-id]");
|
|
3270
|
+
if (feedButton?.dataset.feedId) this.locateActivity(feedButton.dataset.feedId);
|
|
3271
|
+
};
|
|
3228
3272
|
this.sectionOptions = [];
|
|
3229
3273
|
this.opts = options;
|
|
3230
3274
|
this.key = options.eventKey;
|
|
3231
3275
|
this.mode = options.mode ?? "view";
|
|
3232
3276
|
this.keepLive = options.keepLiveWhileHidden ?? true;
|
|
3277
|
+
this.followLive = options.followLive ?? false;
|
|
3233
3278
|
this.currency = options.currency ?? "USD";
|
|
3234
3279
|
this.tokenExpiresAt = options.tokenExpiresAt ?? null;
|
|
3235
3280
|
this.api = new ManageApi(options.apiBase ?? DEFAULT_API_BASE3, options.token);
|
|
@@ -3286,6 +3331,19 @@ var SeatManager = class {
|
|
|
3286
3331
|
this.applyHeatOverlay();
|
|
3287
3332
|
this.paintHeatButton();
|
|
3288
3333
|
}
|
|
3334
|
+
/** Toggle opt-in camera following for new buyer hold/book events. */
|
|
3335
|
+
setFollowLive(enabled) {
|
|
3336
|
+
const changed = this.followLive !== enabled;
|
|
3337
|
+
this.followLive = enabled;
|
|
3338
|
+
if (!enabled) {
|
|
3339
|
+
if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
|
|
3340
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
3341
|
+
this.followLiveTimer = null;
|
|
3342
|
+
this.followSeatTimer = null;
|
|
3343
|
+
}
|
|
3344
|
+
this.paintFollowLiveButton();
|
|
3345
|
+
if (changed) this.opts.onFollowLiveChange?.(enabled);
|
|
3346
|
+
}
|
|
3289
3347
|
/** Change the current-vs-previous sales window and refresh the private projection. */
|
|
3290
3348
|
setTrendWindow(windowMinutes) {
|
|
3291
3349
|
const normalized = Number.isFinite(windowMinutes) ? Math.floor(windowMinutes) : 15;
|
|
@@ -3458,6 +3516,7 @@ var SeatManager = class {
|
|
|
3458
3516
|
return Promise.resolve();
|
|
3459
3517
|
}
|
|
3460
3518
|
zoomToFit() {
|
|
3519
|
+
this.renderer?.clearSectionFocus();
|
|
3461
3520
|
this.renderer?.zoomToFit();
|
|
3462
3521
|
}
|
|
3463
3522
|
destroy() {
|
|
@@ -3465,12 +3524,17 @@ var SeatManager = class {
|
|
|
3465
3524
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
3466
3525
|
if (this.feedTimer) clearInterval(this.feedTimer);
|
|
3467
3526
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
3527
|
+
if (this.liveEventTimer) clearTimeout(this.liveEventTimer);
|
|
3528
|
+
if (this.kpiCleanupTimer) clearTimeout(this.kpiCleanupTimer);
|
|
3529
|
+
if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
|
|
3530
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
3468
3531
|
if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
|
|
3469
3532
|
if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
|
|
3470
3533
|
if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
|
|
3471
3534
|
this.layoutObserver?.disconnect();
|
|
3472
3535
|
this.layoutObserver = null;
|
|
3473
3536
|
this.root?.removeEventListener("keydown", this.onKeyDown);
|
|
3537
|
+
this.els.rail?.removeEventListener("click", this.onRailClick);
|
|
3474
3538
|
if (typeof document !== "undefined") document.removeEventListener("fullscreenchange", this.onFullscreenChange);
|
|
3475
3539
|
if (this.ws) {
|
|
3476
3540
|
try {
|
|
@@ -3608,7 +3672,6 @@ var SeatManager = class {
|
|
|
3608
3672
|
const id = this.labelToId.get(ch.label);
|
|
3609
3673
|
if (id) {
|
|
3610
3674
|
this.renderer?.setStatus([id], toRenderStatus(st));
|
|
3611
|
-
this.flash(id, st);
|
|
3612
3675
|
ids.push(id);
|
|
3613
3676
|
}
|
|
3614
3677
|
const verb = this.verbFor(prev, st);
|
|
@@ -3617,7 +3680,10 @@ var SeatManager = class {
|
|
|
3617
3680
|
group.labels.push(ch.label);
|
|
3618
3681
|
groups.set(groupKey, group);
|
|
3619
3682
|
}
|
|
3620
|
-
for (const group of groups.values())
|
|
3683
|
+
for (const group of groups.values()) {
|
|
3684
|
+
const activity = this.pushActivity(group.labels, group.verb, group.status);
|
|
3685
|
+
if (activity) this.paintSpatialActivity(activity);
|
|
3686
|
+
}
|
|
3621
3687
|
if (ids.length) {
|
|
3622
3688
|
this.lastSyncedAt = Date.now();
|
|
3623
3689
|
this.afterPaint();
|
|
@@ -3663,9 +3729,108 @@ var SeatManager = class {
|
|
|
3663
3729
|
this.renderer?.forceDraw();
|
|
3664
3730
|
}
|
|
3665
3731
|
}
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3732
|
+
activityColor(status) {
|
|
3733
|
+
return status === "held" ? "#f4b740" : status === "booked" ? "#22a06b" : status === "blocked" ? "#8b94ac" : "#6e7bff";
|
|
3734
|
+
}
|
|
3735
|
+
sectionsForLabels(labels) {
|
|
3736
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3737
|
+
for (const label of labels) {
|
|
3738
|
+
const seat = this.labelToSeat.get(label);
|
|
3739
|
+
if (!seat) continue;
|
|
3740
|
+
const sectionId = this.sectionByObject.get(seat.rowId);
|
|
3741
|
+
if (sectionId && sectionId !== UNGROUPED_ID) ids.add(sectionId);
|
|
3742
|
+
}
|
|
3743
|
+
const sectionIds = [...ids];
|
|
3744
|
+
return {
|
|
3745
|
+
ids: sectionIds,
|
|
3746
|
+
labels: sectionIds.map((id) => this.sectionLabelById.get(id) ?? id)
|
|
3747
|
+
};
|
|
3748
|
+
}
|
|
3749
|
+
pulseSeatLabels(labels, status) {
|
|
3750
|
+
const color = this.activityColor(status);
|
|
3751
|
+
for (const label of labels.slice(0, MAX_LIVE_SEAT_PULSES)) {
|
|
3752
|
+
const id = this.labelToId.get(label);
|
|
3753
|
+
if (id) this.renderer?.flashSeat(id, color);
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
/** Render one grouped realtime operation at the right semantic zoom level. */
|
|
3757
|
+
paintSpatialActivity(activity) {
|
|
3758
|
+
const sectionIds = activity.sectionIds ?? this.sectionsForLabels(activity.labels).ids;
|
|
3759
|
+
const focused = this.renderer?.getFocusedSection() ?? null;
|
|
3760
|
+
const followable = this.followLive && sectionIds.length === 1 && (activity.status === "held" || activity.status === "booked");
|
|
3761
|
+
if (followable && focused === sectionIds[0]) {
|
|
3762
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
3763
|
+
return;
|
|
3764
|
+
}
|
|
3765
|
+
if (followable) {
|
|
3766
|
+
if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
|
|
3767
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
3768
|
+
this.followLiveTimer = setTimeout(() => {
|
|
3769
|
+
this.followLiveTimer = null;
|
|
3770
|
+
this.renderer?.focusSection(sectionIds[0]);
|
|
3771
|
+
this.followSeatTimer = setTimeout(() => {
|
|
3772
|
+
this.followSeatTimer = null;
|
|
3773
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
3774
|
+
}, 520);
|
|
3775
|
+
}, 220);
|
|
3776
|
+
return;
|
|
3777
|
+
}
|
|
3778
|
+
if (!focused && sectionIds.length) {
|
|
3779
|
+
const color = this.activityColor(activity.status);
|
|
3780
|
+
for (const sectionId of sectionIds.slice(0, MAX_LIVE_SECTION_PULSES)) {
|
|
3781
|
+
this.renderer?.flashSection(sectionId, color);
|
|
3782
|
+
}
|
|
3783
|
+
return;
|
|
3784
|
+
}
|
|
3785
|
+
if (!sectionIds.length || focused && sectionIds.includes(focused)) {
|
|
3786
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
locateSection(sectionId) {
|
|
3790
|
+
this.renderer?.focusSection(sectionId);
|
|
3791
|
+
}
|
|
3792
|
+
locateActivity(activityId) {
|
|
3793
|
+
const activity = this.feed.find((item) => item.id === activityId);
|
|
3794
|
+
if (!activity) return;
|
|
3795
|
+
const sectionIds = activity.sectionIds ?? this.sectionsForLabels(activity.labels).ids;
|
|
3796
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
3797
|
+
if (sectionIds.length === 1) {
|
|
3798
|
+
this.locateSection(sectionIds[0]);
|
|
3799
|
+
this.followSeatTimer = setTimeout(() => {
|
|
3800
|
+
this.followSeatTimer = null;
|
|
3801
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
3802
|
+
}, 520);
|
|
3803
|
+
return;
|
|
3804
|
+
}
|
|
3805
|
+
this.zoomToFit();
|
|
3806
|
+
this.followSeatTimer = setTimeout(() => {
|
|
3807
|
+
this.followSeatTimer = null;
|
|
3808
|
+
if (sectionIds.length) {
|
|
3809
|
+
const color = this.activityColor(activity.status);
|
|
3810
|
+
for (const sectionId of sectionIds.slice(0, MAX_LIVE_SECTION_PULSES)) {
|
|
3811
|
+
this.renderer?.flashSection(sectionId, color);
|
|
3812
|
+
}
|
|
3813
|
+
} else {
|
|
3814
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
3815
|
+
}
|
|
3816
|
+
}, 280);
|
|
3817
|
+
}
|
|
3818
|
+
showLiveEvent(activity) {
|
|
3819
|
+
const element = this.els.liveevent;
|
|
3820
|
+
if (!element) return;
|
|
3821
|
+
const sections = activity.sectionLabels ?? [];
|
|
3822
|
+
const place = sections.length === 1 ? sections[0] : sections.length > 1 ? `${sections.length} sections` : activity.label;
|
|
3823
|
+
const noun = activity.count === 1 ? "seat" : "seats";
|
|
3824
|
+
element.innerHTML = `<span class="slm-liveeventdot" style="background:${this.activityColor(activity.status)}"></span>
|
|
3825
|
+
<span class="slm-liveeventcopy">${esc(place)} \xB7 ${activity.count.toLocaleString()} ${noun} ${esc(activity.verb)}</span>
|
|
3826
|
+
<span class="slm-liveeventhint">Live</span>`;
|
|
3827
|
+
element.classList.add("on");
|
|
3828
|
+
if (this.liveEventTimer) clearTimeout(this.liveEventTimer);
|
|
3829
|
+
this.liveEventTimer = setTimeout(() => {
|
|
3830
|
+
this.liveEventTimer = null;
|
|
3831
|
+
element.classList.remove("on");
|
|
3832
|
+
element.innerHTML = "";
|
|
3833
|
+
}, 2800);
|
|
3669
3834
|
}
|
|
3670
3835
|
// ---- tallies + feed -------------------------------------------------------
|
|
3671
3836
|
applyReportRevenue(report) {
|
|
@@ -3748,7 +3913,8 @@ var SeatManager = class {
|
|
|
3748
3913
|
}
|
|
3749
3914
|
pushActivity(labels, verb, status, at = Date.now()) {
|
|
3750
3915
|
const label = labels[0];
|
|
3751
|
-
if (!label) return;
|
|
3916
|
+
if (!label) return null;
|
|
3917
|
+
const sections = this.sectionsForLabels(labels);
|
|
3752
3918
|
const item = {
|
|
3753
3919
|
id: `${label}:${at}:${Math.random().toString(36).slice(2, 6)}`,
|
|
3754
3920
|
at,
|
|
@@ -3756,12 +3922,16 @@ var SeatManager = class {
|
|
|
3756
3922
|
labels: [...labels],
|
|
3757
3923
|
count: labels.length,
|
|
3758
3924
|
verb,
|
|
3759
|
-
status
|
|
3925
|
+
status,
|
|
3926
|
+
sectionIds: sections.ids,
|
|
3927
|
+
sectionLabels: sections.labels
|
|
3760
3928
|
};
|
|
3761
3929
|
this.feed.unshift(item);
|
|
3762
3930
|
if (this.feed.length > FEED_CAP) this.feed.length = FEED_CAP;
|
|
3763
3931
|
if (this.mode === "view") this.paintFeed();
|
|
3932
|
+
this.showLiveEvent(item);
|
|
3764
3933
|
this.opts.onActivity?.(item);
|
|
3934
|
+
return item;
|
|
3765
3935
|
}
|
|
3766
3936
|
seedFeed(entries) {
|
|
3767
3937
|
const verbByAction = {
|
|
@@ -3785,6 +3955,7 @@ var SeatManager = class {
|
|
|
3785
3955
|
for (const e of entries) {
|
|
3786
3956
|
const label = e.labels[0];
|
|
3787
3957
|
if (!label) continue;
|
|
3958
|
+
const sections = this.sectionsForLabels(e.labels);
|
|
3788
3959
|
const item = {
|
|
3789
3960
|
id: `log:${e.id}`,
|
|
3790
3961
|
at: e.at,
|
|
@@ -3792,7 +3963,9 @@ var SeatManager = class {
|
|
|
3792
3963
|
labels: [...e.labels],
|
|
3793
3964
|
count: e.labels.length,
|
|
3794
3965
|
verb: verbByAction[e.action] ?? e.action,
|
|
3795
|
-
status: stByAction[e.action] ?? "free"
|
|
3966
|
+
status: stByAction[e.action] ?? "free",
|
|
3967
|
+
sectionIds: sections.ids,
|
|
3968
|
+
sectionLabels: sections.labels
|
|
3796
3969
|
};
|
|
3797
3970
|
this.feed.push(item);
|
|
3798
3971
|
this.opts.onActivity?.(item);
|
|
@@ -3830,13 +4003,15 @@ var SeatManager = class {
|
|
|
3830
4003
|
for (const [k, v] of Object.entries(vars)) root.style.setProperty(k, v);
|
|
3831
4004
|
root.innerHTML = `
|
|
3832
4005
|
<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>
|
|
4006
|
+
<div class="slm-modes" data-ref="modes" role="tablist" aria-label="Manager tools">
|
|
4007
|
+
<button class="slm-mode" role="tab" data-mode="view" title="Monitor (M)" aria-keyshortcuts="M">Monitor</button>
|
|
4008
|
+
<button class="slm-mode" role="tab" data-mode="inspect" title="Inspect (I)" aria-keyshortcuts="I">Inspect</button>
|
|
4009
|
+
<button class="slm-mode" role="tab" data-mode="block" title="Block (B)" aria-keyshortcuts="B">Block</button>
|
|
3837
4010
|
</div>
|
|
3838
4011
|
<span class="slm-live"><span class="slm-live-dot"></span><span data-ref="livetext">CONNECTING</span></span>
|
|
3839
4012
|
<div class="slm-bar-actions">
|
|
4013
|
+
<button class="slm-barbtn follow" data-ref="follow" aria-pressed="false"
|
|
4014
|
+
title="Stay on the current map view unless enabled">Follow live</button>
|
|
3840
4015
|
<button class="slm-barbtn" data-ref="heat" aria-pressed="false"
|
|
3841
4016
|
aria-label="Sales momentum overlay off"
|
|
3842
4017
|
title="Highlight sections selling fastest in the selected time window">Sales momentum</button>
|
|
@@ -3848,6 +4023,7 @@ var SeatManager = class {
|
|
|
3848
4023
|
<div class="slm-map">
|
|
3849
4024
|
<div class="slm-map-host" data-ref="maphost"></div>
|
|
3850
4025
|
<div class="slm-zoomhint" data-ref="zoomhint">Zoom in to marquee-select</div>
|
|
4026
|
+
<div class="slm-liveevent" data-ref="liveevent" role="status" aria-live="polite"></div>
|
|
3851
4027
|
<div class="slm-hud"><button class="slm-hud-chip" data-ref="zfit">Zoom to fit</button></div>
|
|
3852
4028
|
</div>
|
|
3853
4029
|
<aside class="slm-rail"><div class="slm-railscroll" data-ref="rail"></div></aside>
|
|
@@ -3867,20 +4043,25 @@ var SeatManager = class {
|
|
|
3867
4043
|
modes: ref("modes"),
|
|
3868
4044
|
livetext: ref("livetext"),
|
|
3869
4045
|
kpis: ref("kpis"),
|
|
4046
|
+
follow: ref("follow"),
|
|
3870
4047
|
heat: ref("heat"),
|
|
3871
4048
|
fullscreen: ref("fullscreen"),
|
|
3872
4049
|
zoomhint: ref("zoomhint"),
|
|
4050
|
+
liveevent: ref("liveevent"),
|
|
3873
4051
|
rail: ref("rail"),
|
|
3874
4052
|
toast: ref("toast"),
|
|
3875
4053
|
zfit: ref("zfit")
|
|
3876
4054
|
};
|
|
3877
4055
|
this.els.modes.querySelectorAll("[data-mode]").forEach((b) => b.addEventListener("click", () => this.setMode(b.dataset.mode)));
|
|
3878
4056
|
this.els.zfit.addEventListener("click", () => this.zoomToFit());
|
|
4057
|
+
this.els.follow.addEventListener("click", () => this.setFollowLive(!this.followLive));
|
|
3879
4058
|
this.els.heat.addEventListener("click", () => this.setHeatOverlay(!this.heatEnabled));
|
|
3880
4059
|
this.els.fullscreen.addEventListener("click", () => this.toggleFullscreen());
|
|
3881
4060
|
root.addEventListener("keydown", this.onKeyDown);
|
|
4061
|
+
this.els.rail.addEventListener("click", this.onRailClick);
|
|
3882
4062
|
document.addEventListener("fullscreenchange", this.onFullscreenChange);
|
|
3883
4063
|
this.paintModeTabs();
|
|
4064
|
+
this.paintFollowLiveButton();
|
|
3884
4065
|
this.paintHeatButton();
|
|
3885
4066
|
this.paintFullscreenButton();
|
|
3886
4067
|
}
|
|
@@ -3909,10 +4090,20 @@ var SeatManager = class {
|
|
|
3909
4090
|
paintModeTabs() {
|
|
3910
4091
|
this.els.modes?.querySelectorAll("[data-mode]").forEach((b) => {
|
|
3911
4092
|
const el = b;
|
|
3912
|
-
|
|
4093
|
+
const active = el.dataset.mode === this.mode;
|
|
4094
|
+
el.classList.toggle("on", active);
|
|
4095
|
+
el.setAttribute("aria-selected", String(active));
|
|
4096
|
+
el.tabIndex = active ? 0 : -1;
|
|
3913
4097
|
});
|
|
3914
4098
|
this.root?.classList.toggle("block-mode", this.mode === "block");
|
|
3915
4099
|
}
|
|
4100
|
+
paintFollowLiveButton() {
|
|
4101
|
+
const button = this.els.follow;
|
|
4102
|
+
if (!button) return;
|
|
4103
|
+
button.classList.toggle("on", this.followLive);
|
|
4104
|
+
button.setAttribute("aria-pressed", String(this.followLive));
|
|
4105
|
+
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.");
|
|
4106
|
+
}
|
|
3916
4107
|
paintHeatButton() {
|
|
3917
4108
|
const button = this.els.heat;
|
|
3918
4109
|
if (!button) return;
|
|
@@ -3953,20 +4144,55 @@ var SeatManager = class {
|
|
|
3953
4144
|
const show = this.mode === "block" && this.renderer?.getRung?.() !== "seats";
|
|
3954
4145
|
hint.classList.toggle("on", !!show);
|
|
3955
4146
|
}
|
|
4147
|
+
formatKpiDelta(key, delta, currency) {
|
|
4148
|
+
const sign = delta > 0 ? "+" : "\u2212";
|
|
4149
|
+
const absolute = Math.abs(delta);
|
|
4150
|
+
if (key === "gross-sales") return `${sign}${fmtMoney(absolute, currency)}`;
|
|
4151
|
+
if (key === "sold-pct") return `${sign}${absolute.toLocaleString()}pt`;
|
|
4152
|
+
return `${sign}${absolute.toLocaleString()}`;
|
|
4153
|
+
}
|
|
3956
4154
|
paintKpis(t3) {
|
|
3957
4155
|
if (!this.els.kpis) return;
|
|
3958
4156
|
const rev = t3.revenueStatus === "current" ? fmtMoney(t3.grossRevenue, t3.currency) : "\u2014";
|
|
3959
4157
|
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
|
-
]
|
|
4158
|
+
const items = [
|
|
4159
|
+
{ key: "sold-seats", raw: t3.booked, n: t3.booked.toLocaleString(), l: "Sold seats", dot: "#22a06b" },
|
|
4160
|
+
{ key: "held-seats", raw: t3.held, n: t3.held.toLocaleString(), l: "Held seats", dot: "#f4b740" },
|
|
4161
|
+
{ key: "buyers", raw: presence?.shoppingSessions ?? null, n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Buyers" },
|
|
4162
|
+
{ key: "active-holds", raw: presence?.activeHolds ?? null, n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Active holds" },
|
|
4163
|
+
{ key: "free-seats", raw: t3.free, n: t3.free.toLocaleString(), l: "Free seats", dot: "#6e7bff" },
|
|
4164
|
+
{ key: "blocked", raw: t3.blocked, n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac" },
|
|
4165
|
+
{ key: "sold-pct", raw: t3.capacityPct, n: `${t3.capacityPct}%`, l: "Sold" },
|
|
4166
|
+
{ key: "gross-sales", raw: t3.revenueStatus === "current" ? t3.grossRevenue : null, n: rev, l: "Gross sales" }
|
|
4167
|
+
];
|
|
4168
|
+
let hasChanges = false;
|
|
4169
|
+
this.els.kpis.innerHTML = items.map((item) => {
|
|
4170
|
+
const previous = this.lastKpiValues.get(item.key);
|
|
4171
|
+
const changed = item.raw != null && previous != null && item.raw !== previous;
|
|
4172
|
+
const delta = changed ? item.raw - previous : 0;
|
|
4173
|
+
if (changed) {
|
|
4174
|
+
hasChanges = true;
|
|
4175
|
+
this.activeKpiDeltas.set(item.key, {
|
|
4176
|
+
text: this.formatKpiDelta(item.key, delta, t3.currency),
|
|
4177
|
+
down: delta < 0
|
|
4178
|
+
});
|
|
4179
|
+
}
|
|
4180
|
+
if (item.raw != null) this.lastKpiValues.set(item.key, item.raw);
|
|
4181
|
+
const activeDelta = this.activeKpiDeltas.get(item.key);
|
|
4182
|
+
return `<div class="slm-kpi${activeDelta ? " changed" : ""}" data-kpi="${item.key}">
|
|
4183
|
+
<b>${item.dot ? `<span class="dot" style="background:${item.dot}"></span>` : ""}${item.n}</b><span>${item.l}</span>
|
|
4184
|
+
${activeDelta ? `<span class="slm-kpidelta${activeDelta.down ? " down" : ""}">${activeDelta.text}</span>` : ""}
|
|
4185
|
+
</div>`;
|
|
4186
|
+
}).join("");
|
|
4187
|
+
if (hasChanges) {
|
|
4188
|
+
if (this.kpiCleanupTimer) clearTimeout(this.kpiCleanupTimer);
|
|
4189
|
+
this.kpiCleanupTimer = setTimeout(() => {
|
|
4190
|
+
this.kpiCleanupTimer = null;
|
|
4191
|
+
this.activeKpiDeltas.clear();
|
|
4192
|
+
this.els.kpis?.querySelectorAll(".slm-kpidelta").forEach((element) => element.remove());
|
|
4193
|
+
this.els.kpis?.querySelectorAll(".slm-kpi.changed").forEach((element) => element.classList.remove("changed"));
|
|
4194
|
+
}, 1500);
|
|
4195
|
+
}
|
|
3970
4196
|
}
|
|
3971
4197
|
// ---- DOM: rails -----------------------------------------------------------
|
|
3972
4198
|
paintRail() {
|
|
@@ -4037,10 +4263,10 @@ var SeatManager = class {
|
|
|
4037
4263
|
const net = speed?.netBooked ?? 0;
|
|
4038
4264
|
const netLabel = `${net > 0 ? "+" : ""}${net}`;
|
|
4039
4265
|
const trend = speed?.trend === "rising" || speed?.trend === "cooling" ? speed.trend : "steady";
|
|
4040
|
-
return `<
|
|
4041
|
-
<
|
|
4042
|
-
<
|
|
4043
|
-
</
|
|
4266
|
+
return `<button type="button" class="slm-sectionrow" data-section-focus="${esc(row.sectionId)}" title="Focus ${esc(row.sectionLabel)} on the map">
|
|
4267
|
+
<span class="slm-sectiontop"><span>${esc(row.sectionLabel)}</span><span>${fmtMoney(row.bookedRevenue, snapshot.currency)}</span></span>
|
|
4268
|
+
<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>
|
|
4269
|
+
</button>`;
|
|
4044
4270
|
}).join("") : '<div class="slm-empty">No section metrics are available for this chart.</div>';
|
|
4045
4271
|
this.paintTrendWindow();
|
|
4046
4272
|
this.paintMomentumHelp();
|
|
@@ -4109,9 +4335,13 @@ var SeatManager = class {
|
|
|
4109
4335
|
const color = { free: "#6e7bff", held: "#f4b740", booked: "#22a06b", blocked: "#8b94ac" };
|
|
4110
4336
|
this.els.feed.innerHTML = this.feed.map((a) => {
|
|
4111
4337
|
const extra = a.count > 1 ? ` +${a.count - 1}` : "";
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4338
|
+
const sections = a.sectionLabels ?? [];
|
|
4339
|
+
const sectionCopy = sections.length === 1 ? sections[0] : sections.length > 1 ? `${sections.length} sections` : "";
|
|
4340
|
+
return `<button type="button" class="slm-feedrow" data-feed-id="${esc(a.id)}" title="Locate this activity on the map">
|
|
4341
|
+
<span class="slm-feeddot" style="background:${color[a.status]}"></span>
|
|
4342
|
+
<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>
|
|
4343
|
+
<span class="slm-feedmeta"><span class="slm-feedtime">${relTime(a.at, now)}</span><span class="slm-feedlocate">Locate</span></span>
|
|
4344
|
+
</button>`;
|
|
4115
4345
|
}).join("");
|
|
4116
4346
|
}
|
|
4117
4347
|
renderBlockRail() {
|
|
@@ -4367,6 +4597,10 @@ var SeatManager = class {
|
|
|
4367
4597
|
// ---- toast / done / fail --------------------------------------------------
|
|
4368
4598
|
done(action, labels, msg) {
|
|
4369
4599
|
this.toastOk(msg);
|
|
4600
|
+
if (labels.length) {
|
|
4601
|
+
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;
|
|
4602
|
+
if (activity) this.paintSpatialActivity(activity);
|
|
4603
|
+
}
|
|
4370
4604
|
if (action !== "setHoldTtl") this.scheduleRevenueRefresh(0);
|
|
4371
4605
|
this.opts.onActionComplete?.({ action, labels, count: labels.length });
|
|
4372
4606
|
}
|