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