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