@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.cjs
CHANGED
|
@@ -2985,6 +2985,8 @@ function toRenderStatus(s) {
|
|
|
2985
2985
|
var DEFAULT_API_BASE3 = "https://api.seatlayer.io";
|
|
2986
2986
|
var STYLE_ID2 = "seatlayer-manager-style";
|
|
2987
2987
|
var FEED_CAP = 80;
|
|
2988
|
+
var MAX_LIVE_SEAT_PULSES = 16;
|
|
2989
|
+
var MAX_LIVE_SECTION_PULSES = 4;
|
|
2988
2990
|
var LEGEND = [
|
|
2989
2991
|
{ key: "free", label: "Free", color: "#6e7bff" },
|
|
2990
2992
|
{ key: "held", label: "Held", color: "#f4b740" },
|
|
@@ -3010,13 +3012,21 @@ var CSS2 = `
|
|
|
3010
3012
|
@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
3013
|
.slm-kpis{grid-column:1/-1;display:grid;grid-template-columns:repeat(8,minmax(0,1fr));width:100%;padding-top:10px;
|
|
3012
3014
|
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}
|
|
3015
|
+
.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
3016
|
.slm-kpi b{display:flex;min-width:0;align-items:baseline;justify-content:center;font-size:17px;font-weight:800;
|
|
3015
3017
|
font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
3016
3018
|
.slm-kpi span{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--slm-muted);font-weight:700}
|
|
3017
3019
|
.slm-kpi .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:baseline}
|
|
3020
|
+
.slm-kpi.changed b{animation:slm-kpi-bump .58s cubic-bezier(.2,.8,.2,1)}
|
|
3021
|
+
.slm-kpidelta{position:absolute;right:4px;top:-12px;padding:2px 5px;border-radius:999px;background:rgba(34,160,107,.17);
|
|
3022
|
+
color:#5bd39b!important;font-size:9px!important;letter-spacing:0!important;text-transform:none!important;white-space:nowrap;
|
|
3023
|
+
animation:slm-kpi-delta 1.45s ease-out both;pointer-events:none}
|
|
3024
|
+
.slm-kpidelta.down{background:rgba(244,183,64,.14);color:#f7ca6b!important}
|
|
3025
|
+
@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)}}
|
|
3026
|
+
@keyframes slm-kpi-delta{0%{opacity:0;transform:translateY(5px)}18%,72%{opacity:1;transform:none}100%{opacity:0;transform:translateY(-5px)}}
|
|
3018
3027
|
.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
3028
|
.slm-barbtn:hover{border-color:var(--slm-muted)}
|
|
3029
|
+
.slm-barbtn.follow.on{background:rgba(34,160,107,.13);border-color:#22a06b;color:#5bd39b}
|
|
3020
3030
|
|
|
3021
3031
|
/* body */
|
|
3022
3032
|
.slm-body{display:flex;flex:1;min-height:0}
|
|
@@ -3028,6 +3038,14 @@ var CSS2 = `
|
|
|
3028
3038
|
.slm-zoomhint{position:absolute;left:50%;top:14px;transform:translateX(-50%);padding:6px 13px;border-radius:999px;
|
|
3029
3039
|
background:rgba(0,0,0,.55);color:#fff;font-size:12px;font-weight:700;pointer-events:none;opacity:0;transition:opacity .2s}
|
|
3030
3040
|
.slm-zoomhint.on{opacity:1}
|
|
3041
|
+
.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));
|
|
3042
|
+
padding:8px 12px;border:1px solid var(--slm-line);border-radius:999px;background:color-mix(in srgb,var(--slm-surface) 92%,transparent);
|
|
3043
|
+
box-shadow:0 10px 34px rgba(0,0,0,.32);opacity:0;transform:translate(-50%,-8px);pointer-events:none;
|
|
3044
|
+
transition:opacity .18s ease,transform .24s ease;backdrop-filter:blur(10px)}
|
|
3045
|
+
.slm-liveevent.on{opacity:1;transform:translate(-50%,0)}
|
|
3046
|
+
.slm.block-mode .slm-liveevent{top:52px}
|
|
3047
|
+
.slm-liveeventdot{width:8px;height:8px;border-radius:50%;flex:none}.slm-liveeventcopy{min-width:0;overflow:hidden;text-overflow:ellipsis;
|
|
3048
|
+
white-space:nowrap;font-size:12px;font-weight:800}.slm-liveeventhint{color:var(--slm-muted);font-size:10px;white-space:nowrap}
|
|
3031
3049
|
.slm-rail{width:320px;flex:none;border-left:1px solid var(--slm-line);display:flex;flex-direction:column;min-height:0}
|
|
3032
3050
|
.slm-railscroll{flex:1;overflow-y:auto;padding:16px}
|
|
3033
3051
|
.slm-eyebrow{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--slm-muted);font-weight:800;margin-bottom:6px}
|
|
@@ -3042,13 +3060,16 @@ var CSS2 = `
|
|
|
3042
3060
|
|
|
3043
3061
|
/* activity feed */
|
|
3044
3062
|
.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}
|
|
3063
|
+
.slm-feedrow{display:flex!important;width:100%;align-items:center;gap:9px;padding:8px 2px!important;border-bottom:1px solid var(--slm-line)!important;
|
|
3064
|
+
border-radius:6px;font-size:12.5px;text-align:left!important;animation:slm-in .35s ease}
|
|
3065
|
+
.slm-feedrow:hover{background:rgba(255,255,255,.035)!important}
|
|
3047
3066
|
@keyframes slm-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
|
|
3048
3067
|
.slm-feeddot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
3049
3068
|
.slm-feedtext{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
3050
3069
|
.slm-feedtext b{font-weight:800}
|
|
3051
|
-
.slm-
|
|
3070
|
+
.slm-feedsection{display:block;overflow:hidden;text-overflow:ellipsis;color:var(--slm-muted);font-size:10px;font-weight:750}
|
|
3071
|
+
.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}
|
|
3072
|
+
.slm-feedlocate{font-size:9.5px;color:var(--slm-accent);font-weight:800}
|
|
3052
3073
|
.slm-empty{font-size:12.5px;color:var(--slm-muted);padding:12px 0}
|
|
3053
3074
|
|
|
3054
3075
|
/* block toolbar */
|
|
@@ -3118,10 +3139,13 @@ var CSS2 = `
|
|
|
3118
3139
|
.slm-barbtn.on{background:rgba(244,183,64,.13);border-color:#f4b740;color:#f7ca6b}
|
|
3119
3140
|
.slm-sectionlist{display:flex;flex-direction:column;gap:8px;margin-top:4px}
|
|
3120
3141
|
.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)}
|
|
3142
|
+
.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}
|
|
3143
|
+
.slm-sectionrow:hover{border-color:var(--slm-muted)!important;transform:translateY(-1px)}
|
|
3122
3144
|
.slm-sectiontop,.slm-sectionmeta{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
3123
3145
|
.slm-sectiontop{font-size:12.5px;font-weight:800}.slm-sectionmeta{margin-top:5px;color:var(--slm-muted);font-size:11px}
|
|
3146
|
+
.slm-sectionmeta>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
3124
3147
|
.slm-trend{font-size:10px;text-transform:uppercase;letter-spacing:.08em}.slm-trend.rising{color:#22a06b}.slm-trend.cooling{color:#f4b740}
|
|
3148
|
+
.slm-sectionlocate{color:var(--slm-accent);font-size:9.5px;font-weight:800}
|
|
3125
3149
|
.slm-health{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px}
|
|
3126
3150
|
.slm-healthitem{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
|
|
3127
3151
|
.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 +3172,10 @@ var CSS2 = `
|
|
|
3148
3172
|
.slm.compact .slm-barbtn{flex:1;padding:6px 9px}.slm.compact .slm-kpis{grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
|
|
3149
3173
|
.slm.compact .slm-kpi[data-kpi="buyers"],.slm.compact .slm-kpi[data-kpi="active-holds"],
|
|
3150
3174
|
.slm.compact .slm-kpi[data-kpi="sold-pct"],.slm.compact .slm-kpi[data-kpi="gross-sales"]{display:none}
|
|
3175
|
+
@media (prefers-reduced-motion:reduce){
|
|
3176
|
+
.slm.live .slm-live-dot,.slm-feedrow,.slm-kpi.changed b,.slm-kpidelta{animation:none!important}
|
|
3177
|
+
.slm-liveevent,.slm-sectionrow{transition:none!important}
|
|
3178
|
+
}
|
|
3151
3179
|
`;
|
|
3152
3180
|
function injectStyle() {
|
|
3153
3181
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
|
|
@@ -3206,6 +3234,8 @@ var SeatManager = class {
|
|
|
3206
3234
|
this.controlRoomSnapshot = null;
|
|
3207
3235
|
this.trendWindowMinutes = 15;
|
|
3208
3236
|
this.heatEnabled = false;
|
|
3237
|
+
this.lastKpiValues = /* @__PURE__ */ new Map();
|
|
3238
|
+
this.activeKpiDeltas = /* @__PURE__ */ new Map();
|
|
3209
3239
|
// realtime socket
|
|
3210
3240
|
this.ws = null;
|
|
3211
3241
|
this.reconnectTimer = null;
|
|
@@ -3215,6 +3245,10 @@ var SeatManager = class {
|
|
|
3215
3245
|
this.feed = [];
|
|
3216
3246
|
this.feedTimer = null;
|
|
3217
3247
|
this.toastTimer = null;
|
|
3248
|
+
this.liveEventTimer = null;
|
|
3249
|
+
this.kpiCleanupTimer = null;
|
|
3250
|
+
this.followLiveTimer = null;
|
|
3251
|
+
this.followSeatTimer = null;
|
|
3218
3252
|
this.releaseAt = null;
|
|
3219
3253
|
this.layoutObserver = null;
|
|
3220
3254
|
this.tokenExpiresAt = null;
|
|
@@ -3244,11 +3278,22 @@ var SeatManager = class {
|
|
|
3244
3278
|
else return;
|
|
3245
3279
|
event.preventDefault();
|
|
3246
3280
|
};
|
|
3281
|
+
this.onRailClick = (event) => {
|
|
3282
|
+
const target = event.target;
|
|
3283
|
+
const sectionButton = target?.closest("[data-section-focus]");
|
|
3284
|
+
if (sectionButton?.dataset.sectionFocus) {
|
|
3285
|
+
this.locateSection(sectionButton.dataset.sectionFocus);
|
|
3286
|
+
return;
|
|
3287
|
+
}
|
|
3288
|
+
const feedButton = target?.closest("[data-feed-id]");
|
|
3289
|
+
if (feedButton?.dataset.feedId) this.locateActivity(feedButton.dataset.feedId);
|
|
3290
|
+
};
|
|
3247
3291
|
this.sectionOptions = [];
|
|
3248
3292
|
this.opts = options;
|
|
3249
3293
|
this.key = options.eventKey;
|
|
3250
3294
|
this.mode = options.mode ?? "view";
|
|
3251
3295
|
this.keepLive = options.keepLiveWhileHidden ?? true;
|
|
3296
|
+
this.followLive = options.followLive ?? false;
|
|
3252
3297
|
this.currency = options.currency ?? "USD";
|
|
3253
3298
|
this.tokenExpiresAt = options.tokenExpiresAt ?? null;
|
|
3254
3299
|
this.api = new ManageApi(options.apiBase ?? DEFAULT_API_BASE3, options.token);
|
|
@@ -3305,6 +3350,19 @@ var SeatManager = class {
|
|
|
3305
3350
|
this.applyHeatOverlay();
|
|
3306
3351
|
this.paintHeatButton();
|
|
3307
3352
|
}
|
|
3353
|
+
/** Toggle opt-in camera following for new buyer hold/book events. */
|
|
3354
|
+
setFollowLive(enabled) {
|
|
3355
|
+
const changed = this.followLive !== enabled;
|
|
3356
|
+
this.followLive = enabled;
|
|
3357
|
+
if (!enabled) {
|
|
3358
|
+
if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
|
|
3359
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
3360
|
+
this.followLiveTimer = null;
|
|
3361
|
+
this.followSeatTimer = null;
|
|
3362
|
+
}
|
|
3363
|
+
this.paintFollowLiveButton();
|
|
3364
|
+
if (changed) this.opts.onFollowLiveChange?.(enabled);
|
|
3365
|
+
}
|
|
3308
3366
|
/** Change the current-vs-previous sales window and refresh the private projection. */
|
|
3309
3367
|
setTrendWindow(windowMinutes) {
|
|
3310
3368
|
const normalized = Number.isFinite(windowMinutes) ? Math.floor(windowMinutes) : 15;
|
|
@@ -3477,6 +3535,7 @@ var SeatManager = class {
|
|
|
3477
3535
|
return Promise.resolve();
|
|
3478
3536
|
}
|
|
3479
3537
|
zoomToFit() {
|
|
3538
|
+
this.renderer?.clearSectionFocus();
|
|
3480
3539
|
this.renderer?.zoomToFit();
|
|
3481
3540
|
}
|
|
3482
3541
|
destroy() {
|
|
@@ -3484,12 +3543,17 @@ var SeatManager = class {
|
|
|
3484
3543
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
3485
3544
|
if (this.feedTimer) clearInterval(this.feedTimer);
|
|
3486
3545
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
3546
|
+
if (this.liveEventTimer) clearTimeout(this.liveEventTimer);
|
|
3547
|
+
if (this.kpiCleanupTimer) clearTimeout(this.kpiCleanupTimer);
|
|
3548
|
+
if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
|
|
3549
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
3487
3550
|
if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
|
|
3488
3551
|
if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
|
|
3489
3552
|
if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
|
|
3490
3553
|
this.layoutObserver?.disconnect();
|
|
3491
3554
|
this.layoutObserver = null;
|
|
3492
3555
|
this.root?.removeEventListener("keydown", this.onKeyDown);
|
|
3556
|
+
this.els.rail?.removeEventListener("click", this.onRailClick);
|
|
3493
3557
|
if (typeof document !== "undefined") document.removeEventListener("fullscreenchange", this.onFullscreenChange);
|
|
3494
3558
|
if (this.ws) {
|
|
3495
3559
|
try {
|
|
@@ -3627,7 +3691,6 @@ var SeatManager = class {
|
|
|
3627
3691
|
const id = this.labelToId.get(ch.label);
|
|
3628
3692
|
if (id) {
|
|
3629
3693
|
this.renderer?.setStatus([id], toRenderStatus(st));
|
|
3630
|
-
this.flash(id, st);
|
|
3631
3694
|
ids.push(id);
|
|
3632
3695
|
}
|
|
3633
3696
|
const verb = this.verbFor(prev, st);
|
|
@@ -3636,7 +3699,10 @@ var SeatManager = class {
|
|
|
3636
3699
|
group.labels.push(ch.label);
|
|
3637
3700
|
groups.set(groupKey, group);
|
|
3638
3701
|
}
|
|
3639
|
-
for (const group of groups.values())
|
|
3702
|
+
for (const group of groups.values()) {
|
|
3703
|
+
const activity = this.pushActivity(group.labels, group.verb, group.status);
|
|
3704
|
+
if (activity) this.paintSpatialActivity(activity);
|
|
3705
|
+
}
|
|
3640
3706
|
if (ids.length) {
|
|
3641
3707
|
this.lastSyncedAt = Date.now();
|
|
3642
3708
|
this.afterPaint();
|
|
@@ -3682,9 +3748,108 @@ var SeatManager = class {
|
|
|
3682
3748
|
this.renderer?.forceDraw();
|
|
3683
3749
|
}
|
|
3684
3750
|
}
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3751
|
+
activityColor(status) {
|
|
3752
|
+
return status === "held" ? "#f4b740" : status === "booked" ? "#22a06b" : status === "blocked" ? "#8b94ac" : "#6e7bff";
|
|
3753
|
+
}
|
|
3754
|
+
sectionsForLabels(labels) {
|
|
3755
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3756
|
+
for (const label of labels) {
|
|
3757
|
+
const seat = this.labelToSeat.get(label);
|
|
3758
|
+
if (!seat) continue;
|
|
3759
|
+
const sectionId = this.sectionByObject.get(seat.rowId);
|
|
3760
|
+
if (sectionId && sectionId !== import_core3.UNGROUPED_ID) ids.add(sectionId);
|
|
3761
|
+
}
|
|
3762
|
+
const sectionIds = [...ids];
|
|
3763
|
+
return {
|
|
3764
|
+
ids: sectionIds,
|
|
3765
|
+
labels: sectionIds.map((id) => this.sectionLabelById.get(id) ?? id)
|
|
3766
|
+
};
|
|
3767
|
+
}
|
|
3768
|
+
pulseSeatLabels(labels, status) {
|
|
3769
|
+
const color = this.activityColor(status);
|
|
3770
|
+
for (const label of labels.slice(0, MAX_LIVE_SEAT_PULSES)) {
|
|
3771
|
+
const id = this.labelToId.get(label);
|
|
3772
|
+
if (id) this.renderer?.flashSeat(id, color);
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
/** Render one grouped realtime operation at the right semantic zoom level. */
|
|
3776
|
+
paintSpatialActivity(activity) {
|
|
3777
|
+
const sectionIds = activity.sectionIds ?? this.sectionsForLabels(activity.labels).ids;
|
|
3778
|
+
const focused = this.renderer?.getFocusedSection() ?? null;
|
|
3779
|
+
const followable = this.followLive && sectionIds.length === 1 && (activity.status === "held" || activity.status === "booked");
|
|
3780
|
+
if (followable && focused === sectionIds[0]) {
|
|
3781
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
3782
|
+
return;
|
|
3783
|
+
}
|
|
3784
|
+
if (followable) {
|
|
3785
|
+
if (this.followLiveTimer) clearTimeout(this.followLiveTimer);
|
|
3786
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
3787
|
+
this.followLiveTimer = setTimeout(() => {
|
|
3788
|
+
this.followLiveTimer = null;
|
|
3789
|
+
this.renderer?.focusSection(sectionIds[0]);
|
|
3790
|
+
this.followSeatTimer = setTimeout(() => {
|
|
3791
|
+
this.followSeatTimer = null;
|
|
3792
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
3793
|
+
}, 520);
|
|
3794
|
+
}, 220);
|
|
3795
|
+
return;
|
|
3796
|
+
}
|
|
3797
|
+
if (!focused && sectionIds.length) {
|
|
3798
|
+
const color = this.activityColor(activity.status);
|
|
3799
|
+
for (const sectionId of sectionIds.slice(0, MAX_LIVE_SECTION_PULSES)) {
|
|
3800
|
+
this.renderer?.flashSection(sectionId, color);
|
|
3801
|
+
}
|
|
3802
|
+
return;
|
|
3803
|
+
}
|
|
3804
|
+
if (!sectionIds.length || focused && sectionIds.includes(focused)) {
|
|
3805
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
locateSection(sectionId) {
|
|
3809
|
+
this.renderer?.focusSection(sectionId);
|
|
3810
|
+
}
|
|
3811
|
+
locateActivity(activityId) {
|
|
3812
|
+
const activity = this.feed.find((item) => item.id === activityId);
|
|
3813
|
+
if (!activity) return;
|
|
3814
|
+
const sectionIds = activity.sectionIds ?? this.sectionsForLabels(activity.labels).ids;
|
|
3815
|
+
if (this.followSeatTimer) clearTimeout(this.followSeatTimer);
|
|
3816
|
+
if (sectionIds.length === 1) {
|
|
3817
|
+
this.locateSection(sectionIds[0]);
|
|
3818
|
+
this.followSeatTimer = setTimeout(() => {
|
|
3819
|
+
this.followSeatTimer = null;
|
|
3820
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
3821
|
+
}, 520);
|
|
3822
|
+
return;
|
|
3823
|
+
}
|
|
3824
|
+
this.zoomToFit();
|
|
3825
|
+
this.followSeatTimer = setTimeout(() => {
|
|
3826
|
+
this.followSeatTimer = null;
|
|
3827
|
+
if (sectionIds.length) {
|
|
3828
|
+
const color = this.activityColor(activity.status);
|
|
3829
|
+
for (const sectionId of sectionIds.slice(0, MAX_LIVE_SECTION_PULSES)) {
|
|
3830
|
+
this.renderer?.flashSection(sectionId, color);
|
|
3831
|
+
}
|
|
3832
|
+
} else {
|
|
3833
|
+
this.pulseSeatLabels(activity.labels, activity.status);
|
|
3834
|
+
}
|
|
3835
|
+
}, 280);
|
|
3836
|
+
}
|
|
3837
|
+
showLiveEvent(activity) {
|
|
3838
|
+
const element = this.els.liveevent;
|
|
3839
|
+
if (!element) return;
|
|
3840
|
+
const sections = activity.sectionLabels ?? [];
|
|
3841
|
+
const place = sections.length === 1 ? sections[0] : sections.length > 1 ? `${sections.length} sections` : activity.label;
|
|
3842
|
+
const noun = activity.count === 1 ? "seat" : "seats";
|
|
3843
|
+
element.innerHTML = `<span class="slm-liveeventdot" style="background:${this.activityColor(activity.status)}"></span>
|
|
3844
|
+
<span class="slm-liveeventcopy">${esc(place)} \xB7 ${activity.count.toLocaleString()} ${noun} ${esc(activity.verb)}</span>
|
|
3845
|
+
<span class="slm-liveeventhint">Live</span>`;
|
|
3846
|
+
element.classList.add("on");
|
|
3847
|
+
if (this.liveEventTimer) clearTimeout(this.liveEventTimer);
|
|
3848
|
+
this.liveEventTimer = setTimeout(() => {
|
|
3849
|
+
this.liveEventTimer = null;
|
|
3850
|
+
element.classList.remove("on");
|
|
3851
|
+
element.innerHTML = "";
|
|
3852
|
+
}, 2800);
|
|
3688
3853
|
}
|
|
3689
3854
|
// ---- tallies + feed -------------------------------------------------------
|
|
3690
3855
|
applyReportRevenue(report) {
|
|
@@ -3767,7 +3932,8 @@ var SeatManager = class {
|
|
|
3767
3932
|
}
|
|
3768
3933
|
pushActivity(labels, verb, status, at = Date.now()) {
|
|
3769
3934
|
const label = labels[0];
|
|
3770
|
-
if (!label) return;
|
|
3935
|
+
if (!label) return null;
|
|
3936
|
+
const sections = this.sectionsForLabels(labels);
|
|
3771
3937
|
const item = {
|
|
3772
3938
|
id: `${label}:${at}:${Math.random().toString(36).slice(2, 6)}`,
|
|
3773
3939
|
at,
|
|
@@ -3775,12 +3941,16 @@ var SeatManager = class {
|
|
|
3775
3941
|
labels: [...labels],
|
|
3776
3942
|
count: labels.length,
|
|
3777
3943
|
verb,
|
|
3778
|
-
status
|
|
3944
|
+
status,
|
|
3945
|
+
sectionIds: sections.ids,
|
|
3946
|
+
sectionLabels: sections.labels
|
|
3779
3947
|
};
|
|
3780
3948
|
this.feed.unshift(item);
|
|
3781
3949
|
if (this.feed.length > FEED_CAP) this.feed.length = FEED_CAP;
|
|
3782
3950
|
if (this.mode === "view") this.paintFeed();
|
|
3951
|
+
this.showLiveEvent(item);
|
|
3783
3952
|
this.opts.onActivity?.(item);
|
|
3953
|
+
return item;
|
|
3784
3954
|
}
|
|
3785
3955
|
seedFeed(entries) {
|
|
3786
3956
|
const verbByAction = {
|
|
@@ -3804,6 +3974,7 @@ var SeatManager = class {
|
|
|
3804
3974
|
for (const e of entries) {
|
|
3805
3975
|
const label = e.labels[0];
|
|
3806
3976
|
if (!label) continue;
|
|
3977
|
+
const sections = this.sectionsForLabels(e.labels);
|
|
3807
3978
|
const item = {
|
|
3808
3979
|
id: `log:${e.id}`,
|
|
3809
3980
|
at: e.at,
|
|
@@ -3811,7 +3982,9 @@ var SeatManager = class {
|
|
|
3811
3982
|
labels: [...e.labels],
|
|
3812
3983
|
count: e.labels.length,
|
|
3813
3984
|
verb: verbByAction[e.action] ?? e.action,
|
|
3814
|
-
status: stByAction[e.action] ?? "free"
|
|
3985
|
+
status: stByAction[e.action] ?? "free",
|
|
3986
|
+
sectionIds: sections.ids,
|
|
3987
|
+
sectionLabels: sections.labels
|
|
3815
3988
|
};
|
|
3816
3989
|
this.feed.push(item);
|
|
3817
3990
|
this.opts.onActivity?.(item);
|
|
@@ -3849,13 +4022,15 @@ var SeatManager = class {
|
|
|
3849
4022
|
for (const [k, v] of Object.entries(vars)) root.style.setProperty(k, v);
|
|
3850
4023
|
root.innerHTML = `
|
|
3851
4024
|
<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>
|
|
4025
|
+
<div class="slm-modes" data-ref="modes" role="tablist" aria-label="Manager tools">
|
|
4026
|
+
<button class="slm-mode" role="tab" data-mode="view" title="Monitor (M)" aria-keyshortcuts="M">Monitor</button>
|
|
4027
|
+
<button class="slm-mode" role="tab" data-mode="inspect" title="Inspect (I)" aria-keyshortcuts="I">Inspect</button>
|
|
4028
|
+
<button class="slm-mode" role="tab" data-mode="block" title="Block (B)" aria-keyshortcuts="B">Block</button>
|
|
3856
4029
|
</div>
|
|
3857
4030
|
<span class="slm-live"><span class="slm-live-dot"></span><span data-ref="livetext">CONNECTING</span></span>
|
|
3858
4031
|
<div class="slm-bar-actions">
|
|
4032
|
+
<button class="slm-barbtn follow" data-ref="follow" aria-pressed="false"
|
|
4033
|
+
title="Stay on the current map view unless enabled">Follow live</button>
|
|
3859
4034
|
<button class="slm-barbtn" data-ref="heat" aria-pressed="false"
|
|
3860
4035
|
aria-label="Sales momentum overlay off"
|
|
3861
4036
|
title="Highlight sections selling fastest in the selected time window">Sales momentum</button>
|
|
@@ -3867,6 +4042,7 @@ var SeatManager = class {
|
|
|
3867
4042
|
<div class="slm-map">
|
|
3868
4043
|
<div class="slm-map-host" data-ref="maphost"></div>
|
|
3869
4044
|
<div class="slm-zoomhint" data-ref="zoomhint">Zoom in to marquee-select</div>
|
|
4045
|
+
<div class="slm-liveevent" data-ref="liveevent" role="status" aria-live="polite"></div>
|
|
3870
4046
|
<div class="slm-hud"><button class="slm-hud-chip" data-ref="zfit">Zoom to fit</button></div>
|
|
3871
4047
|
</div>
|
|
3872
4048
|
<aside class="slm-rail"><div class="slm-railscroll" data-ref="rail"></div></aside>
|
|
@@ -3886,20 +4062,25 @@ var SeatManager = class {
|
|
|
3886
4062
|
modes: ref("modes"),
|
|
3887
4063
|
livetext: ref("livetext"),
|
|
3888
4064
|
kpis: ref("kpis"),
|
|
4065
|
+
follow: ref("follow"),
|
|
3889
4066
|
heat: ref("heat"),
|
|
3890
4067
|
fullscreen: ref("fullscreen"),
|
|
3891
4068
|
zoomhint: ref("zoomhint"),
|
|
4069
|
+
liveevent: ref("liveevent"),
|
|
3892
4070
|
rail: ref("rail"),
|
|
3893
4071
|
toast: ref("toast"),
|
|
3894
4072
|
zfit: ref("zfit")
|
|
3895
4073
|
};
|
|
3896
4074
|
this.els.modes.querySelectorAll("[data-mode]").forEach((b) => b.addEventListener("click", () => this.setMode(b.dataset.mode)));
|
|
3897
4075
|
this.els.zfit.addEventListener("click", () => this.zoomToFit());
|
|
4076
|
+
this.els.follow.addEventListener("click", () => this.setFollowLive(!this.followLive));
|
|
3898
4077
|
this.els.heat.addEventListener("click", () => this.setHeatOverlay(!this.heatEnabled));
|
|
3899
4078
|
this.els.fullscreen.addEventListener("click", () => this.toggleFullscreen());
|
|
3900
4079
|
root.addEventListener("keydown", this.onKeyDown);
|
|
4080
|
+
this.els.rail.addEventListener("click", this.onRailClick);
|
|
3901
4081
|
document.addEventListener("fullscreenchange", this.onFullscreenChange);
|
|
3902
4082
|
this.paintModeTabs();
|
|
4083
|
+
this.paintFollowLiveButton();
|
|
3903
4084
|
this.paintHeatButton();
|
|
3904
4085
|
this.paintFullscreenButton();
|
|
3905
4086
|
}
|
|
@@ -3928,10 +4109,20 @@ var SeatManager = class {
|
|
|
3928
4109
|
paintModeTabs() {
|
|
3929
4110
|
this.els.modes?.querySelectorAll("[data-mode]").forEach((b) => {
|
|
3930
4111
|
const el = b;
|
|
3931
|
-
|
|
4112
|
+
const active = el.dataset.mode === this.mode;
|
|
4113
|
+
el.classList.toggle("on", active);
|
|
4114
|
+
el.setAttribute("aria-selected", String(active));
|
|
4115
|
+
el.tabIndex = active ? 0 : -1;
|
|
3932
4116
|
});
|
|
3933
4117
|
this.root?.classList.toggle("block-mode", this.mode === "block");
|
|
3934
4118
|
}
|
|
4119
|
+
paintFollowLiveButton() {
|
|
4120
|
+
const button = this.els.follow;
|
|
4121
|
+
if (!button) return;
|
|
4122
|
+
button.classList.toggle("on", this.followLive);
|
|
4123
|
+
button.setAttribute("aria-pressed", String(this.followLive));
|
|
4124
|
+
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.");
|
|
4125
|
+
}
|
|
3935
4126
|
paintHeatButton() {
|
|
3936
4127
|
const button = this.els.heat;
|
|
3937
4128
|
if (!button) return;
|
|
@@ -3972,20 +4163,55 @@ var SeatManager = class {
|
|
|
3972
4163
|
const show = this.mode === "block" && this.renderer?.getRung?.() !== "seats";
|
|
3973
4164
|
hint.classList.toggle("on", !!show);
|
|
3974
4165
|
}
|
|
4166
|
+
formatKpiDelta(key, delta, currency) {
|
|
4167
|
+
const sign = delta > 0 ? "+" : "\u2212";
|
|
4168
|
+
const absolute = Math.abs(delta);
|
|
4169
|
+
if (key === "gross-sales") return `${sign}${fmtMoney(absolute, currency)}`;
|
|
4170
|
+
if (key === "sold-pct") return `${sign}${absolute.toLocaleString()}pt`;
|
|
4171
|
+
return `${sign}${absolute.toLocaleString()}`;
|
|
4172
|
+
}
|
|
3975
4173
|
paintKpis(t3) {
|
|
3976
4174
|
if (!this.els.kpis) return;
|
|
3977
4175
|
const rev = t3.revenueStatus === "current" ? fmtMoney(t3.grossRevenue, t3.currency) : "\u2014";
|
|
3978
4176
|
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
|
-
]
|
|
4177
|
+
const items = [
|
|
4178
|
+
{ key: "sold-seats", raw: t3.booked, n: t3.booked.toLocaleString(), l: "Sold seats", dot: "#22a06b" },
|
|
4179
|
+
{ key: "held-seats", raw: t3.held, n: t3.held.toLocaleString(), l: "Held seats", dot: "#f4b740" },
|
|
4180
|
+
{ key: "buyers", raw: presence?.shoppingSessions ?? null, n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Buyers" },
|
|
4181
|
+
{ key: "active-holds", raw: presence?.activeHolds ?? null, n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Active holds" },
|
|
4182
|
+
{ key: "free-seats", raw: t3.free, n: t3.free.toLocaleString(), l: "Free seats", dot: "#6e7bff" },
|
|
4183
|
+
{ key: "blocked", raw: t3.blocked, n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac" },
|
|
4184
|
+
{ key: "sold-pct", raw: t3.capacityPct, n: `${t3.capacityPct}%`, l: "Sold" },
|
|
4185
|
+
{ key: "gross-sales", raw: t3.revenueStatus === "current" ? t3.grossRevenue : null, n: rev, l: "Gross sales" }
|
|
4186
|
+
];
|
|
4187
|
+
let hasChanges = false;
|
|
4188
|
+
this.els.kpis.innerHTML = items.map((item) => {
|
|
4189
|
+
const previous = this.lastKpiValues.get(item.key);
|
|
4190
|
+
const changed = item.raw != null && previous != null && item.raw !== previous;
|
|
4191
|
+
const delta = changed ? item.raw - previous : 0;
|
|
4192
|
+
if (changed) {
|
|
4193
|
+
hasChanges = true;
|
|
4194
|
+
this.activeKpiDeltas.set(item.key, {
|
|
4195
|
+
text: this.formatKpiDelta(item.key, delta, t3.currency),
|
|
4196
|
+
down: delta < 0
|
|
4197
|
+
});
|
|
4198
|
+
}
|
|
4199
|
+
if (item.raw != null) this.lastKpiValues.set(item.key, item.raw);
|
|
4200
|
+
const activeDelta = this.activeKpiDeltas.get(item.key);
|
|
4201
|
+
return `<div class="slm-kpi${activeDelta ? " changed" : ""}" data-kpi="${item.key}">
|
|
4202
|
+
<b>${item.dot ? `<span class="dot" style="background:${item.dot}"></span>` : ""}${item.n}</b><span>${item.l}</span>
|
|
4203
|
+
${activeDelta ? `<span class="slm-kpidelta${activeDelta.down ? " down" : ""}">${activeDelta.text}</span>` : ""}
|
|
4204
|
+
</div>`;
|
|
4205
|
+
}).join("");
|
|
4206
|
+
if (hasChanges) {
|
|
4207
|
+
if (this.kpiCleanupTimer) clearTimeout(this.kpiCleanupTimer);
|
|
4208
|
+
this.kpiCleanupTimer = setTimeout(() => {
|
|
4209
|
+
this.kpiCleanupTimer = null;
|
|
4210
|
+
this.activeKpiDeltas.clear();
|
|
4211
|
+
this.els.kpis?.querySelectorAll(".slm-kpidelta").forEach((element) => element.remove());
|
|
4212
|
+
this.els.kpis?.querySelectorAll(".slm-kpi.changed").forEach((element) => element.classList.remove("changed"));
|
|
4213
|
+
}, 1500);
|
|
4214
|
+
}
|
|
3989
4215
|
}
|
|
3990
4216
|
// ---- DOM: rails -----------------------------------------------------------
|
|
3991
4217
|
paintRail() {
|
|
@@ -4056,10 +4282,10 @@ var SeatManager = class {
|
|
|
4056
4282
|
const net = speed?.netBooked ?? 0;
|
|
4057
4283
|
const netLabel = `${net > 0 ? "+" : ""}${net}`;
|
|
4058
4284
|
const trend = speed?.trend === "rising" || speed?.trend === "cooling" ? speed.trend : "steady";
|
|
4059
|
-
return `<
|
|
4060
|
-
<
|
|
4061
|
-
<
|
|
4062
|
-
</
|
|
4285
|
+
return `<button type="button" class="slm-sectionrow" data-section-focus="${esc(row.sectionId)}" title="Focus ${esc(row.sectionLabel)} on the map">
|
|
4286
|
+
<span class="slm-sectiontop"><span>${esc(row.sectionLabel)}</span><span>${fmtMoney(row.bookedRevenue, snapshot.currency)}</span></span>
|
|
4287
|
+
<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>
|
|
4288
|
+
</button>`;
|
|
4063
4289
|
}).join("") : '<div class="slm-empty">No section metrics are available for this chart.</div>';
|
|
4064
4290
|
this.paintTrendWindow();
|
|
4065
4291
|
this.paintMomentumHelp();
|
|
@@ -4128,9 +4354,13 @@ var SeatManager = class {
|
|
|
4128
4354
|
const color = { free: "#6e7bff", held: "#f4b740", booked: "#22a06b", blocked: "#8b94ac" };
|
|
4129
4355
|
this.els.feed.innerHTML = this.feed.map((a) => {
|
|
4130
4356
|
const extra = a.count > 1 ? ` +${a.count - 1}` : "";
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4357
|
+
const sections = a.sectionLabels ?? [];
|
|
4358
|
+
const sectionCopy = sections.length === 1 ? sections[0] : sections.length > 1 ? `${sections.length} sections` : "";
|
|
4359
|
+
return `<button type="button" class="slm-feedrow" data-feed-id="${esc(a.id)}" title="Locate this activity on the map">
|
|
4360
|
+
<span class="slm-feeddot" style="background:${color[a.status]}"></span>
|
|
4361
|
+
<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>
|
|
4362
|
+
<span class="slm-feedmeta"><span class="slm-feedtime">${relTime(a.at, now)}</span><span class="slm-feedlocate">Locate</span></span>
|
|
4363
|
+
</button>`;
|
|
4134
4364
|
}).join("");
|
|
4135
4365
|
}
|
|
4136
4366
|
renderBlockRail() {
|
|
@@ -4386,6 +4616,10 @@ var SeatManager = class {
|
|
|
4386
4616
|
// ---- toast / done / fail --------------------------------------------------
|
|
4387
4617
|
done(action, labels, msg) {
|
|
4388
4618
|
this.toastOk(msg);
|
|
4619
|
+
if (labels.length) {
|
|
4620
|
+
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;
|
|
4621
|
+
if (activity) this.paintSpatialActivity(activity);
|
|
4622
|
+
}
|
|
4389
4623
|
if (action !== "setHoldTtl") this.scheduleRevenueRefresh(0);
|
|
4390
4624
|
this.opts.onActionComplete?.({ action, labels, count: labels.length });
|
|
4391
4625
|
}
|