@seatlayer/js 0.12.2 → 0.13.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.d.cts CHANGED
@@ -933,6 +933,10 @@ declare class SeatManager {
933
933
  private sectionByObject;
934
934
  private sectionLabelById;
935
935
  private lastSyncedAt;
936
+ private blockedQuery;
937
+ private blockedSection;
938
+ private blockedResultLimit;
939
+ private unblockAllConfirmTimer;
936
940
  private readonly onFullscreenChange;
937
941
  private readonly onKeyDown;
938
942
  constructor(options: SeatManagerOptions);
@@ -988,8 +992,9 @@ declare class SeatManager {
988
992
  private onMessage;
989
993
  private resnapshot;
990
994
  private applySnapshot;
991
- /** Optimistic local write shared by delta stream + organizer actions. */
992
- private setSeatLocal;
995
+ /** Optimistic local write shared by organizer actions. Paint and tally once,
996
+ * even when an arena-sized operation changes hundreds of seats. */
997
+ private setSeatsLocal;
993
998
  /** Keep the canvas painting on hidden/occluded tabs (war-room second monitor). */
994
999
  private afterPaint;
995
1000
  private flash;
@@ -1023,8 +1028,17 @@ declare class SeatManager {
1023
1028
  private paintLegend;
1024
1029
  private paintFeed;
1025
1030
  private renderBlockRail;
1026
- private selectCategory;
1031
+ private toggleCategory;
1032
+ /** A category/filter is a real toggle: add the missing seats, or remove the
1033
+ * whole group when every eligible seat in it is already selected. */
1034
+ private toggleLabels;
1035
+ private isBlockSelectable;
1027
1036
  private paintSelBar;
1037
+ private paintCategoryControls;
1038
+ private filteredBlockedSeats;
1039
+ private paintBlockedInventory;
1040
+ private confirmUnblockAll;
1041
+ private resetUnblockAllConfirm;
1028
1042
  private done;
1029
1043
  private toastOk;
1030
1044
  private toastErr;
package/dist/index.d.ts CHANGED
@@ -933,6 +933,10 @@ declare class SeatManager {
933
933
  private sectionByObject;
934
934
  private sectionLabelById;
935
935
  private lastSyncedAt;
936
+ private blockedQuery;
937
+ private blockedSection;
938
+ private blockedResultLimit;
939
+ private unblockAllConfirmTimer;
936
940
  private readonly onFullscreenChange;
937
941
  private readonly onKeyDown;
938
942
  constructor(options: SeatManagerOptions);
@@ -988,8 +992,9 @@ declare class SeatManager {
988
992
  private onMessage;
989
993
  private resnapshot;
990
994
  private applySnapshot;
991
- /** Optimistic local write shared by delta stream + organizer actions. */
992
- private setSeatLocal;
995
+ /** Optimistic local write shared by organizer actions. Paint and tally once,
996
+ * even when an arena-sized operation changes hundreds of seats. */
997
+ private setSeatsLocal;
993
998
  /** Keep the canvas painting on hidden/occluded tabs (war-room second monitor). */
994
999
  private afterPaint;
995
1000
  private flash;
@@ -1023,8 +1028,17 @@ declare class SeatManager {
1023
1028
  private paintLegend;
1024
1029
  private paintFeed;
1025
1030
  private renderBlockRail;
1026
- private selectCategory;
1031
+ private toggleCategory;
1032
+ /** A category/filter is a real toggle: add the missing seats, or remove the
1033
+ * whole group when every eligible seat in it is already selected. */
1034
+ private toggleLabels;
1035
+ private isBlockSelectable;
1027
1036
  private paintSelBar;
1037
+ private paintCategoryControls;
1038
+ private filteredBlockedSeats;
1039
+ private paintBlockedInventory;
1040
+ private confirmUnblockAll;
1041
+ private resetUnblockAllConfirm;
1028
1042
  private done;
1029
1043
  private toastOk;
1030
1044
  private toastErr;
package/dist/index.js CHANGED
@@ -2275,7 +2275,8 @@ var CSS2 = `
2275
2275
  .slm input{font:inherit}
2276
2276
 
2277
2277
  /* top bar */
2278
- .slm-bar{display:flex;align-items:center;gap:14px;padding:10px 16px;border-bottom:1px solid var(--slm-line);flex:none;flex-wrap:wrap}
2278
+ .slm-bar{display:grid;grid-template-columns:auto auto minmax(0,1fr);align-items:center;column-gap:14px;row-gap:10px;
2279
+ padding:10px 16px;border-bottom:1px solid var(--slm-line);flex:none}
2279
2280
  .slm-modes{display:inline-flex;background:var(--slm-surface);border:1px solid var(--slm-line);border-radius:999px;padding:3px}
2280
2281
  .slm-mode{padding:6px 16px;border-radius:999px;font-weight:700;font-size:13px;color:var(--slm-muted)}
2281
2282
  .slm-mode.on{background:var(--slm-accent);color:var(--slm-accent-ink)}
@@ -2283,9 +2284,11 @@ var CSS2 = `
2283
2284
  .slm-live-dot{width:8px;height:8px;border-radius:50%;background:#8b94ac}
2284
2285
  .slm.live .slm-live-dot{background:#22a06b;box-shadow:0 0 0 0 rgba(34,160,107,.55);animation:slm-pulse 2s infinite}
2285
2286
  @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)}}
2286
- .slm-kpis{display:flex;align-items:center;gap:16px;margin-left:auto;flex-wrap:wrap}
2287
- .slm-kpi{display:flex;flex-direction:column;line-height:1.15}
2288
- .slm-kpi b{font-size:17px;font-weight:800;font-variant-numeric:tabular-nums}
2287
+ .slm-kpis{grid-column:1/-1;display:grid;grid-template-columns:repeat(8,minmax(0,1fr));width:100%;padding-top:10px;
2288
+ border-top:1px solid var(--slm-line)}
2289
+ .slm-kpi{display:flex;min-width:0;flex-direction:column;align-items:center;padding:0 5px;line-height:1.15;text-align:center}
2290
+ .slm-kpi b{display:flex;min-width:0;align-items:baseline;justify-content:center;font-size:17px;font-weight:800;
2291
+ font-variant-numeric:tabular-nums;white-space:nowrap}
2289
2292
  .slm-kpi span{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--slm-muted);font-weight:700}
2290
2293
  .slm-kpi .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:baseline}
2291
2294
  .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}
@@ -2339,11 +2342,44 @@ var CSS2 = `
2339
2342
  font-size:12px;font-weight:700;color:var(--slm-text);display:inline-flex;align-items:center;gap:6px}
2340
2343
  .slm-chip:hover{border-color:var(--slm-muted)}
2341
2344
  .slm-chip .dot{width:8px;height:8px;border-radius:50%}
2345
+ .slm-chip .slm-chipcount{min-width:18px;padding:1px 5px;border-radius:999px;background:rgba(255,255,255,.07);
2346
+ color:var(--slm-muted);font-size:10px;font-variant-numeric:tabular-nums;text-align:center}
2347
+ .slm-chip .slm-chipcheck{display:none;font-size:11px;line-height:1}
2348
+ .slm-chip.on{border-color:var(--slm-accent);background:color-mix(in srgb,var(--slm-accent) 20%,var(--slm-surface));
2349
+ box-shadow:0 0 0 1px color-mix(in srgb,var(--slm-accent) 45%,transparent)}
2350
+ .slm-chip.on .slm-chipcount{background:var(--slm-accent);color:var(--slm-accent-ink)}
2351
+ .slm-chip.on .slm-chipcheck{display:inline}
2352
+ .slm-chip.partial{border-style:dashed;border-color:var(--slm-accent)}
2353
+ .slm-chip:disabled{opacity:.42;cursor:not-allowed}
2354
+ .slm-selecthelp{margin:-1px 0 9px;color:var(--slm-muted);font-size:11px;line-height:1.4}
2342
2355
  .slm-field{margin:14px 0}
2343
2356
  .slm-field label{display:block;font-size:11px;font-weight:700;color:var(--slm-muted);margin-bottom:5px}
2344
2357
  .slm-input,.slm-select{width:100%;padding:8px 10px;border-radius:9px;border:1px solid var(--slm-line);
2345
2358
  background:var(--slm-surface);color:var(--slm-text)}
2346
2359
  .slm-note{font-size:11.5px;color:var(--slm-muted);margin-top:5px}
2360
+ .slm-blocked{margin-top:17px;padding-top:15px;border-top:1px solid var(--slm-line)}
2361
+ .slm-blockedhead{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:8px}
2362
+ .slm-blockedhead .slm-eyebrow{margin-bottom:0}.slm-blockedtotal{font-size:11px;color:var(--slm-muted)}
2363
+ .slm-blockedtotal b{color:var(--slm-text);font-variant-numeric:tabular-nums}
2364
+ .slm-blockedtools{display:grid;grid-template-columns:minmax(0,1fr);gap:7px}
2365
+ .slm-blockedsummary{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:9px 0 6px;
2366
+ color:var(--slm-muted);font-size:10.5px}
2367
+ .slm-linkbtn{font-size:11px!important;font-weight:800!important;color:var(--slm-accent)!important;text-align:right}
2368
+ .slm-linkbtn:disabled{opacity:.45;cursor:not-allowed}
2369
+ .slm-blockedlist{max-height:246px;overflow:auto;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
2370
+ .slm-blockeditem{display:grid!important;grid-template-columns:18px minmax(0,1fr);width:100%;gap:8px;padding:8px 9px!important;
2371
+ border-bottom:1px solid var(--slm-line)!important;text-align:left!important}
2372
+ .slm-blockeditem:last-child{border-bottom:0!important}.slm-blockeditem:hover{background:rgba(255,255,255,.035)!important}
2373
+ .slm-blockeditem.on{background:color-mix(in srgb,var(--slm-accent) 13%,var(--slm-surface))!important}
2374
+ .slm-blockedcheck{display:flex;align-items:center;justify-content:center;width:16px;height:16px;margin-top:1px;border-radius:4px;
2375
+ border:1px solid var(--slm-muted);color:transparent;font-size:10px;font-weight:900}
2376
+ .slm-blockeditem.on .slm-blockedcheck{border-color:var(--slm-accent);background:var(--slm-accent);color:var(--slm-accent-ink)}
2377
+ .slm-blockedcopy{min-width:0}.slm-blockedlabel{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
2378
+ font-size:12px;font-weight:800}.slm-blockedmeta{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
2379
+ margin-top:2px;color:var(--slm-muted);font-size:10px}
2380
+ .slm-blockedmore{width:100%;padding:9px!important;color:var(--slm-accent)!important;font-size:11px!important;font-weight:800!important}
2381
+ .slm-blockedempty{padding:12px;color:var(--slm-muted);font-size:11.5px;line-height:1.45}
2382
+ .slm-allnote{margin-top:-4px;margin-bottom:10px}
2347
2383
 
2348
2384
  /* toast */
2349
2385
  .slm-toast{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);padding:10px 16px;border-radius:10px;
@@ -2354,7 +2390,7 @@ var CSS2 = `
2354
2390
  .slm-toast.ok{background:#1f7a4d;color:#fff;border-color:#1f7a4d}
2355
2391
 
2356
2392
  /* control-room actions + insights */
2357
- .slm-bar-actions{display:flex;align-items:center;gap:7px}
2393
+ .slm-bar-actions{display:flex;align-items:center;justify-self:end;gap:7px}
2358
2394
  .slm-barbtn.on{background:rgba(244,183,64,.13);border-color:#f4b740;color:#f7ca6b}
2359
2395
  .slm-sectionlist{display:flex;flex-direction:column;gap:8px;margin-top:4px}
2360
2396
  .slm-sectionlist + .slm-eyebrow{margin-top:18px}
@@ -2372,16 +2408,22 @@ var CSS2 = `
2372
2408
  .slm-momentumhelp[hidden]{display:none}.slm-momentumscale{display:flex;align-items:center;gap:7px;color:var(--slm-muted);font-size:10px;font-weight:750;text-transform:uppercase;letter-spacing:.07em}
2373
2409
  .slm-momentumgradient{height:6px;min-width:64px;flex:1;border-radius:999px;background:linear-gradient(90deg,#f4b740,#ef4444)}
2374
2410
  .slm-momentumcopy{margin-top:7px;color:var(--slm-muted);font-size:11px;line-height:1.45}
2375
- .slm-inspect-card{padding:12px;border:1px solid var(--slm-line);border-radius:12px;background:var(--slm-surface)}
2376
- .slm-inspect-label{font-size:24px;font-weight:850;letter-spacing:-.02em}.slm-inspect-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:14px}
2377
- .slm-inspect-grid span{display:block;color:var(--slm-muted);font-size:10px;text-transform:uppercase;letter-spacing:.08em}.slm-inspect-grid b{display:block;margin-top:3px;font-size:12.5px}
2411
+ .slm-inspect-card{padding:16px;border:1px solid var(--slm-line);border-radius:12px;background:var(--slm-surface)}
2412
+ .slm-inspect-label{font-size:24px;font-weight:850;letter-spacing:-.02em;line-height:1.1}
2413
+ .slm-inspect-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px 20px;margin-top:18px}
2414
+ .slm-inspect-grid>div{min-width:0}.slm-inspect-grid span{display:block;color:var(--slm-muted);font-size:10px;
2415
+ text-transform:uppercase;letter-spacing:.08em}.slm-inspect-grid b{display:block;margin-top:4px;font-size:13px;line-height:1.35;overflow-wrap:anywhere}
2378
2416
  .slm:fullscreen{border-radius:0;min-height:100vh;background:var(--slm-bg)}
2379
2417
  .slm:fullscreen .slm-bar{padding:14px 22px}.slm:fullscreen .slm-kpi b{font-size:21px}.slm:fullscreen .slm-rail{width:360px}
2380
2418
 
2381
2419
  .slm.compact .slm-rail{width:100%;border-left:0;border-top:1px solid var(--slm-line);height:44%}
2382
2420
  .slm.compact .slm-body{flex-direction:column}
2383
- .slm.compact .slm-bar{gap:8px;padding:8px}.slm.compact .slm-barbtn{padding:6px 9px}
2384
- .slm.compact .slm-kpis{gap:9px}.slm.compact .slm-kpi:nth-child(n+5){display:none}
2421
+ .slm.compact .slm-bar{grid-template-columns:minmax(0,1fr) auto;gap:8px;padding:8px}
2422
+ .slm.compact .slm-modes{min-width:0}.slm.compact .slm-mode{padding-inline:11px}
2423
+ .slm.compact .slm-live{justify-self:end}.slm.compact .slm-bar-actions{grid-column:1/-1;justify-self:stretch}
2424
+ .slm.compact .slm-barbtn{flex:1;padding:6px 9px}.slm.compact .slm-kpis{grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
2425
+ .slm.compact .slm-kpi[data-kpi="buyers"],.slm.compact .slm-kpi[data-kpi="active-holds"],
2426
+ .slm.compact .slm-kpi[data-kpi="sold-pct"],.slm.compact .slm-kpi[data-kpi="gross-sales"]{display:none}
2385
2427
  `;
2386
2428
  function injectStyle() {
2387
2429
  if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
@@ -2457,6 +2499,10 @@ var SeatManager = class {
2457
2499
  this.sectionByObject = /* @__PURE__ */ new Map();
2458
2500
  this.sectionLabelById = /* @__PURE__ */ new Map();
2459
2501
  this.lastSyncedAt = null;
2502
+ this.blockedQuery = "";
2503
+ this.blockedSection = "";
2504
+ this.blockedResultLimit = 100;
2505
+ this.unblockAllConfirmTimer = null;
2460
2506
  this.onFullscreenChange = () => {
2461
2507
  this.paintFullscreenButton();
2462
2508
  this.updateContainerLayout();
@@ -2602,13 +2648,13 @@ var SeatManager = class {
2602
2648
  const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "free");
2603
2649
  if (!targets.length) return;
2604
2650
  const releaseAt = opts.releaseAt ?? this.releaseAt ?? void 0;
2605
- for (const l of targets) this.setSeatLocal(l, "blocked");
2651
+ this.setSeatsLocal(targets, "blocked");
2606
2652
  try {
2607
2653
  await this.api.block(this.key, targets, { ...opts, releaseAt });
2608
2654
  this.clearSelection();
2609
2655
  this.done("block", targets, releaseAt ? `Blocked ${targets.length} \u2014 auto-release ${new Date(releaseAt).toLocaleString()}.` : `Blocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
2610
2656
  } catch (err) {
2611
- for (const l of targets) this.setSeatLocal(l, "free");
2657
+ this.setSeatsLocal(targets, "free");
2612
2658
  this.toastErr(err instanceof ManageApiError && err.status === 409 ? "Some seats were just taken. Try again." : "Couldn't block those seats.");
2613
2659
  this.opts.onError?.(err);
2614
2660
  }
@@ -2616,13 +2662,13 @@ var SeatManager = class {
2616
2662
  async unblock(labels) {
2617
2663
  const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "blocked");
2618
2664
  if (!targets.length) return;
2619
- for (const l of targets) this.setSeatLocal(l, "free");
2665
+ this.setSeatsLocal(targets, "free");
2620
2666
  try {
2621
2667
  await this.api.unblock(this.key, targets);
2622
2668
  this.clearSelection();
2623
2669
  this.done("unblock", targets, `Unblocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
2624
2670
  } catch (err) {
2625
- for (const l of targets) this.setSeatLocal(l, "blocked");
2671
+ this.setSeatsLocal(targets, "blocked");
2626
2672
  this.toastErr("Couldn't unblock those seats.");
2627
2673
  this.opts.onError?.(err);
2628
2674
  }
@@ -2630,9 +2676,10 @@ var SeatManager = class {
2630
2676
  async unblockAll() {
2631
2677
  const blocked = [...this.status.entries()].filter(([, s]) => s === "blocked").map(([l]) => l);
2632
2678
  if (!blocked.length) return;
2633
- for (const l of blocked) this.setSeatLocal(l, "free");
2679
+ this.setSeatsLocal(blocked, "free");
2634
2680
  try {
2635
2681
  const res = await this.api.unblockAll(this.key);
2682
+ this.clearSelection();
2636
2683
  this.done("unblockAll", blocked, `Unblocked ${res.freed} seat${res.freed === 1 ? "" : "s"}.`);
2637
2684
  } catch (err) {
2638
2685
  await this.resnapshot();
@@ -2644,13 +2691,13 @@ var SeatManager = class {
2644
2691
  async cancelBooking(labels, bookingRef) {
2645
2692
  const targets = labels.filter((l) => this.status.get(l) === "booked");
2646
2693
  if (!targets.length || !bookingRef) return;
2647
- for (const l of targets) this.setSeatLocal(l, "free");
2694
+ this.setSeatsLocal(targets, "free");
2648
2695
  try {
2649
2696
  await this.api.unbook(this.key, targets, bookingRef);
2650
2697
  this.clearSelection();
2651
2698
  this.done("cancelBooking", targets, `Cancelled ${targets.length} booking${targets.length === 1 ? "" : "s"}.`);
2652
2699
  } catch (err) {
2653
- for (const l of targets) this.setSeatLocal(l, "booked");
2700
+ this.setSeatsLocal(targets, "booked");
2654
2701
  this.toastErr("Couldn't cancel that booking. Check the reference.");
2655
2702
  this.opts.onError?.(err);
2656
2703
  }
@@ -2713,6 +2760,7 @@ var SeatManager = class {
2713
2760
  if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
2714
2761
  if (this.feedTimer) clearInterval(this.feedTimer);
2715
2762
  if (this.toastTimer) clearTimeout(this.toastTimer);
2763
+ if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
2716
2764
  if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
2717
2765
  if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
2718
2766
  this.layoutObserver?.disconnect();
@@ -2891,11 +2939,16 @@ var SeatManager = class {
2891
2939
  this.afterPaint();
2892
2940
  this.recomputeTallies();
2893
2941
  }
2894
- /** Optimistic local write shared by delta stream + organizer actions. */
2895
- setSeatLocal(label, st) {
2896
- this.status.set(label, st);
2897
- const id = this.labelToId.get(label);
2898
- if (id) this.renderer?.setStatus([id], toRenderStatus(st));
2942
+ /** Optimistic local write shared by organizer actions. Paint and tally once,
2943
+ * even when an arena-sized operation changes hundreds of seats. */
2944
+ setSeatsLocal(labels, st) {
2945
+ const ids = [];
2946
+ for (const label of labels) {
2947
+ this.status.set(label, st);
2948
+ const id = this.labelToId.get(label);
2949
+ if (id) ids.push(id);
2950
+ }
2951
+ if (ids.length) this.renderer?.setStatus(ids, toRenderStatus(st));
2899
2952
  this.afterPaint();
2900
2953
  this.recomputeTallies();
2901
2954
  }
@@ -2978,6 +3031,7 @@ var SeatManager = class {
2978
3031
  this.paintLegend(t3);
2979
3032
  this.paintMonitorInsights();
2980
3033
  } else if (this.mode === "inspect") this.renderInspectRail(this.getSelection());
3034
+ else if (this.mode === "block") this.paintSelBar(this.getSelection());
2981
3035
  this.opts.onTallies?.(t3);
2982
3036
  }
2983
3037
  verbFor(prev, next) {
@@ -3199,15 +3253,15 @@ var SeatManager = class {
3199
3253
  const rev = t3.revenueStatus === "current" ? fmtMoney(t3.grossRevenue, t3.currency) : "\u2014";
3200
3254
  const presence = this.controlRoomSnapshot?.presence;
3201
3255
  this.els.kpis.innerHTML = [
3202
- { n: t3.booked.toLocaleString(), l: "Sold", dot: "#22a06b" },
3203
- { n: t3.held.toLocaleString(), l: "Held", dot: "#f4b740" },
3204
- { n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Shopping" },
3205
- { n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Live holds" },
3206
- { n: t3.free.toLocaleString(), l: "Free", dot: "#6e7bff" },
3207
- { n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac" },
3208
- { n: `${t3.capacityPct}%`, l: "Capacity" },
3209
- { n: rev, l: "Gross" }
3210
- ].map((k) => `<div class="slm-kpi"><b>${k.dot ? `<span class="dot" style="background:${k.dot}"></span>` : ""}${k.n}</b><span>${k.l}</span></div>`).join("");
3256
+ { key: "sold-seats", n: t3.booked.toLocaleString(), l: "Sold seats", dot: "#22a06b" },
3257
+ { key: "held-seats", n: t3.held.toLocaleString(), l: "Held seats", dot: "#f4b740" },
3258
+ { key: "buyers", n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Buyers" },
3259
+ { key: "active-holds", n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Active holds" },
3260
+ { key: "free-seats", n: t3.free.toLocaleString(), l: "Free seats", dot: "#6e7bff" },
3261
+ { key: "blocked", n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac" },
3262
+ { key: "sold-pct", n: `${t3.capacityPct}%`, l: "Sold" },
3263
+ { key: "gross-sales", n: rev, l: "Gross sales" }
3264
+ ].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("");
3211
3265
  }
3212
3266
  // ---- DOM: rails -----------------------------------------------------------
3213
3267
  paintRail() {
@@ -3306,9 +3360,9 @@ var SeatManager = class {
3306
3360
  const seat = seats[seats.length - 1];
3307
3361
  if (!seat) {
3308
3362
  this.els.rail.innerHTML = `
3309
- <p class="slm-eyebrow">Inspect</p>
3310
- <p class="slm-hint">Select any seat to see its live inventory context. Inspect is read-only and never changes availability.</p>
3311
- <div class="slm-empty">Choose a seat on the map.</div>`;
3363
+ <p class="slm-eyebrow">Inspect seats</p>
3364
+ <p class="slm-hint">Select a seat to see its availability and sales context. Nothing changes in this view.</p>
3365
+ <div class="slm-empty">Select a seat on the map.</div>`;
3312
3366
  return;
3313
3367
  }
3314
3368
  const status = this.status.get(seat.label) ?? "free";
@@ -3317,17 +3371,20 @@ var SeatManager = class {
3317
3371
  const sectionLabel = this.sectionLabelById.get(sectionId) ?? "Other seats";
3318
3372
  const category = this.doc?.categories.find((item) => item.key === seat.categoryKey);
3319
3373
  const sectionMetric = this.controlRoomSnapshot?.revenue.bySection.find((row) => row.sectionId === sectionId);
3374
+ const object = this.doc?.objects.find((item) => item.id === seat.rowId);
3375
+ const location = object?.type === "row" ? { label: "Row", value: object.label } : object?.type === "table" ? { label: "Table", value: object.label } : seat.kind === "booth" ? { label: "Type", value: "Booth" } : null;
3376
+ const itemKind = seat.kind === "booth" ? "Booth" : "Seat";
3320
3377
  this.els.rail.innerHTML = `
3321
- <p class="slm-eyebrow">Inspect</p>
3322
- <p class="slm-hint">Live inventory context. Booked seats remain read-only; order and payment actions belong to the host commerce system.</p>
3378
+ <p class="slm-eyebrow">${itemKind} details</p>
3379
+ <p class="slm-hint">Live availability and section performance.</p>
3323
3380
  <div class="slm-inspect-card">
3324
3381
  <div class="slm-inspect-label">${esc(seat.label)}</div>
3325
3382
  <div class="slm-inspect-grid">
3326
3383
  <div><span>Status</span><b>${statusLabel[status]}</b></div>
3327
3384
  <div><span>Section</span><b>${esc(sectionLabel)}</b></div>
3328
- <div><span>Row</span><b>${esc(seat.rowId)}</b></div>
3385
+ ${location ? `<div><span>${location.label}</span><b>${esc(location.value)}</b></div>` : ""}
3329
3386
  <div><span>Category</span><b>${esc(category?.label ?? seat.categoryKey)}</b></div>
3330
- <div><span>Section sold</span><b>${sectionMetric ? `${sectionMetric.booked}/${sectionMetric.total}` : "\u2014"}</b></div>
3387
+ <div><span>Sold in section</span><b>${sectionMetric ? `${sectionMetric.booked} of ${sectionMetric.total}` : "\u2014"}</b></div>
3331
3388
  <div><span>Section revenue</span><b>${sectionMetric && this.controlRoomSnapshot ? fmtMoney(sectionMetric.bookedRevenue, this.controlRoomSnapshot.currency) : "\u2014"}</b></div>
3332
3389
  </div>
3333
3390
  </div>`;
@@ -3354,23 +3411,30 @@ var SeatManager = class {
3354
3411
  }
3355
3412
  renderBlockRail() {
3356
3413
  const cats = this.doc?.categories ?? [];
3357
- const catChips = cats.map((c) => `<button class="slm-chip" data-cat="${esc(c.key)}"><span class="dot" style="background:${esc(c.color ?? "#6e7bff")}"></span>${esc(c.label ?? c.key)}</button>`).join("");
3414
+ const catChips = cats.map((c) => `<button class="slm-chip" type="button" data-cat="${esc(c.key)}" aria-pressed="false">
3415
+ <span class="dot" style="background:${esc(c.color ?? "#6e7bff")}"></span>
3416
+ <span>${esc(c.label ?? c.key)}</span>
3417
+ <span class="slm-chipcount" data-cat-count>0</span>
3418
+ <span class="slm-chipcheck" aria-hidden="true">\u2713</span>
3419
+ </button>`).join("");
3358
3420
  const sectionField = this.sectionOptions.length ? `<div class="slm-field"><label>Select a whole section</label>
3359
3421
  <select class="slm-select" data-ref="section"><option value="">Choose a section\u2026</option>
3360
3422
  ${this.sectionOptions.map((s) => `<option value="${esc(s.id)}">${esc(s.label)}</option>`).join("")}</select></div>` : "";
3423
+ const blockedSectionOptions = this.sectionOptions.map((s) => `<option value="${esc(s.id)}">${esc(s.label)}</option>`).join("");
3361
3424
  this.els.rail.innerHTML = `
3362
3425
  <p class="slm-eyebrow">Block &amp; unblock</p>
3363
3426
  <p class="slm-hint">Drag a box on the map to marquee-select, \u2318A for all, or pick a category/section. Booked and held inventory is never actionable here.</p>
3364
- <div class="slm-selbar"><span class="slm-selnum" data-ref="selnum">0</span><span class="slm-sellabel">selected</span></div>
3427
+ <div class="slm-selbar" aria-live="polite"><span class="slm-selnum" data-ref="selnum">0</span><span class="slm-sellabel" data-ref="selmeta">selected</span></div>
3365
3428
  <div class="slm-row">
3366
3429
  <button class="slm-btn" data-ref="doblock" disabled>Block</button>
3367
- <button class="slm-btn ghost" data-ref="dounblock" disabled>Unblock</button>
3430
+ <button class="slm-btn ghost" data-ref="dounblock" disabled>Put back on sale</button>
3368
3431
  </div>
3369
3432
  <div class="slm-row">
3370
3433
  <button class="slm-btn ghost" data-ref="selall">Select all</button>
3371
3434
  <button class="slm-btn ghost" data-ref="clearsel">Clear</button>
3372
3435
  </div>
3373
- <p class="slm-eyebrow" style="margin-top:8px">By category</p>
3436
+ <p class="slm-eyebrow" style="margin-top:8px">Select by category</p>
3437
+ <p class="slm-selecthelp">Choose one or more. A checked category is selected; click it again to remove it.</p>
3374
3438
  <div class="slm-chiprow">${catChips || '<span class="slm-empty">No categories.</span>'}</div>
3375
3439
  ${sectionField}
3376
3440
  <div class="slm-field">
@@ -3378,18 +3442,46 @@ var SeatManager = class {
3378
3442
  <input type="datetime-local" class="slm-input" data-ref="release" />
3379
3443
  <p class="slm-note" data-ref="releasenote">Leave empty to block permanently.</p>
3380
3444
  </div>
3381
- <div class="slm-row"><button class="slm-btn ghost" data-ref="markall" style="flex:1">Mark everything for sale</button></div>
3445
+ <section class="slm-blocked" aria-labelledby="slm-blocked-title">
3446
+ <div class="slm-blockedhead">
3447
+ <p class="slm-eyebrow" id="slm-blocked-title">Blocked inventory</p>
3448
+ <span class="slm-blockedtotal"><b data-ref="blockedcount">0</b> out of sale</span>
3449
+ </div>
3450
+ <p class="slm-selecthelp">Find blocked seats, select only the ones you need, then use \u201CPut back on sale\u201D.</p>
3451
+ <div class="slm-blockedtools">
3452
+ <input type="search" class="slm-input" data-ref="blockedsearch" placeholder="Find seat, row or category" aria-label="Search blocked seats" />
3453
+ <select class="slm-select" data-ref="blockedsection" aria-label="Filter blocked seats by section">
3454
+ <option value="">All sections</option>${blockedSectionOptions}
3455
+ </select>
3456
+ </div>
3457
+ <div class="slm-blockedsummary">
3458
+ <span data-ref="blockedshowing">No blocked seats</span>
3459
+ <button type="button" class="slm-linkbtn" data-ref="selblocked" disabled>Select results</button>
3460
+ </div>
3461
+ <div class="slm-blockedlist" data-ref="blockedlist"></div>
3462
+ </section>
3463
+ <div class="slm-field">
3464
+ <button class="slm-btn ghost" data-ref="markall" style="width:100%" disabled>Put all blocked seats on sale</button>
3465
+ <p class="slm-note slm-allnote" data-ref="markallnote">For a full reset only. You will be asked to confirm.</p>
3466
+ </div>
3382
3467
  `;
3383
3468
  const r = (n) => this.els.rail.querySelector(`[data-ref="${n}"]`);
3384
3469
  this.els.selnum = r("selnum");
3385
3470
  this.els.doblock = r("doblock");
3386
3471
  this.els.dounblock = r("dounblock");
3472
+ this.els.selmeta = r("selmeta");
3473
+ this.els.blockedcount = r("blockedcount");
3474
+ this.els.blockedshowing = r("blockedshowing");
3475
+ this.els.blockedlist = r("blockedlist");
3476
+ this.els.selblocked = r("selblocked");
3477
+ this.els.markall = r("markall");
3478
+ this.els.markallnote = r("markallnote");
3387
3479
  r("doblock").addEventListener("click", () => void this.block());
3388
3480
  r("dounblock").addEventListener("click", () => void this.unblock());
3389
3481
  r("selall").addEventListener("click", () => this.selectAll());
3390
3482
  r("clearsel").addEventListener("click", () => this.clearSelection());
3391
- r("markall").addEventListener("click", () => void this.unblockAll());
3392
- this.els.rail.querySelectorAll("[data-cat]").forEach((b) => b.addEventListener("click", () => this.selectCategory(b.dataset.cat)));
3483
+ r("markall").addEventListener("click", () => this.confirmUnblockAll());
3484
+ this.els.rail.querySelectorAll("[data-cat]").forEach((b) => b.addEventListener("click", () => this.toggleCategory(b.dataset.cat)));
3393
3485
  const sectionSel = this.els.rail.querySelector('[data-ref="section"]');
3394
3486
  sectionSel?.addEventListener("change", () => {
3395
3487
  if (sectionSel.value) {
@@ -3397,6 +3489,32 @@ var SeatManager = class {
3397
3489
  sectionSel.value = "";
3398
3490
  }
3399
3491
  });
3492
+ const blockedSearch = r("blockedsearch");
3493
+ const blockedSection = r("blockedsection");
3494
+ blockedSearch.value = this.blockedQuery;
3495
+ blockedSection.value = this.blockedSection;
3496
+ blockedSearch.addEventListener("input", () => {
3497
+ this.blockedQuery = blockedSearch.value;
3498
+ this.blockedResultLimit = 100;
3499
+ this.paintBlockedInventory();
3500
+ });
3501
+ blockedSection.addEventListener("change", () => {
3502
+ this.blockedSection = blockedSection.value;
3503
+ this.blockedResultLimit = 100;
3504
+ this.paintBlockedInventory();
3505
+ });
3506
+ r("selblocked").addEventListener("click", () => {
3507
+ this.toggleLabels(this.filteredBlockedSeats().map((seat) => seat.label));
3508
+ });
3509
+ r("blockedlist").addEventListener("click", (event) => {
3510
+ const target = event.target;
3511
+ const seatButton = target.closest("[data-blocked-label]");
3512
+ if (seatButton?.dataset.blockedLabel) this.toggleLabels([seatButton.dataset.blockedLabel]);
3513
+ else if (target.closest("[data-blocked-more]")) {
3514
+ this.blockedResultLimit += 100;
3515
+ this.paintBlockedInventory();
3516
+ }
3517
+ });
3400
3518
  const rel = r("release");
3401
3519
  rel.addEventListener("change", () => {
3402
3520
  const ms = rel.value ? new Date(rel.value).getTime() : NaN;
@@ -3406,18 +3524,140 @@ var SeatManager = class {
3406
3524
  });
3407
3525
  this.paintSelBar(this.getSelection());
3408
3526
  }
3409
- selectCategory(catKey) {
3527
+ toggleCategory(catKey) {
3410
3528
  const labels = [];
3411
- for (const [label, seat] of this.labelToSeat.entries()) if (seat.categoryKey === catKey) labels.push(label);
3412
- this.selectByLabels(labels);
3529
+ for (const [label, seat] of this.labelToSeat.entries()) {
3530
+ if (seat.categoryKey === catKey && this.isBlockSelectable(label)) labels.push(label);
3531
+ }
3532
+ this.toggleLabels(labels);
3533
+ }
3534
+ /** A category/filter is a real toggle: add the missing seats, or remove the
3535
+ * whole group when every eligible seat in it is already selected. */
3536
+ toggleLabels(labels) {
3537
+ if (!this.renderer) return;
3538
+ const eligible = labels.filter((label) => this.labelToSeat.has(label) && this.isBlockSelectable(label));
3539
+ if (!eligible.length) return;
3540
+ const selected = new Set(this.selectionLabels());
3541
+ const allSelected = eligible.every((label) => selected.has(label));
3542
+ if (allSelected) {
3543
+ const ids = eligible.map((label) => this.labelToId.get(label)).filter((id) => Boolean(id));
3544
+ this.renderer.deselect(ids);
3545
+ } else {
3546
+ this.renderer.selectByLabels(eligible);
3547
+ }
3548
+ this.syncSelection();
3549
+ }
3550
+ isBlockSelectable(label) {
3551
+ const status = this.status.get(label) ?? "free";
3552
+ return status === "free" || status === "blocked";
3413
3553
  }
3414
3554
  paintSelBar(seats) {
3415
3555
  if (!this.els.selnum) return;
3416
3556
  this.els.selnum.textContent = seats.length.toLocaleString();
3417
- const hasFree = seats.some((s) => this.status.get(s.label) === "free");
3418
- const hasBlocked = seats.some((s) => this.status.get(s.label) === "blocked");
3419
- this.els.doblock.disabled = !hasFree;
3420
- this.els.dounblock.disabled = !hasBlocked;
3557
+ const freeCount = seats.filter((s) => (this.status.get(s.label) ?? "free") === "free").length;
3558
+ const blockedCount = seats.filter((s) => this.status.get(s.label) === "blocked").length;
3559
+ this.els.selmeta.textContent = seats.length ? `${freeCount.toLocaleString()} available \xB7 ${blockedCount.toLocaleString()} blocked` : "selected";
3560
+ const blockButton = this.els.doblock;
3561
+ const unblockButton = this.els.dounblock;
3562
+ blockButton.disabled = freeCount === 0;
3563
+ unblockButton.disabled = blockedCount === 0;
3564
+ blockButton.textContent = freeCount ? `Block ${freeCount.toLocaleString()}` : "Block selected";
3565
+ unblockButton.textContent = blockedCount ? `Put ${blockedCount.toLocaleString()} on sale` : "Put back on sale";
3566
+ this.paintCategoryControls(seats);
3567
+ this.paintBlockedInventory();
3568
+ }
3569
+ paintCategoryControls(seats) {
3570
+ const selected = new Set(seats.map((seat) => seat.label));
3571
+ this.els.rail?.querySelectorAll("[data-cat]").forEach((button) => {
3572
+ const catKey = button.dataset.cat;
3573
+ const labels = [];
3574
+ for (const [label, seat] of this.labelToSeat.entries()) {
3575
+ if (seat.categoryKey === catKey && this.isBlockSelectable(label)) labels.push(label);
3576
+ }
3577
+ const picked = labels.filter((label) => selected.has(label)).length;
3578
+ const full = labels.length > 0 && picked === labels.length;
3579
+ const partial = picked > 0 && !full;
3580
+ button.disabled = labels.length === 0;
3581
+ button.classList.toggle("on", full);
3582
+ button.classList.toggle("partial", partial);
3583
+ button.setAttribute("aria-pressed", full ? "true" : partial ? "mixed" : "false");
3584
+ button.setAttribute("title", full ? `Remove all ${labels.length.toLocaleString()} seats in this category from the selection` : partial ? `Select the remaining ${(labels.length - picked).toLocaleString()} seats in this category` : `Select all ${labels.length.toLocaleString()} seats in this category`);
3585
+ const count = button.querySelector("[data-cat-count]");
3586
+ if (count) count.textContent = picked ? `${picked.toLocaleString()}/${labels.length.toLocaleString()}` : labels.length.toLocaleString();
3587
+ });
3588
+ }
3589
+ filteredBlockedSeats() {
3590
+ const query = this.blockedQuery.trim().toLocaleLowerCase();
3591
+ const seats = [];
3592
+ for (const [label, seat] of this.labelToSeat.entries()) {
3593
+ if (this.status.get(label) !== "blocked") continue;
3594
+ const sectionId = this.sectionByObject.get(seat.rowId) ?? UNGROUPED_ID;
3595
+ if (this.blockedSection && sectionId !== this.blockedSection) continue;
3596
+ if (query) {
3597
+ const category = this.doc?.categories.find((item) => item.key === seat.categoryKey)?.label ?? seat.categoryKey;
3598
+ const section = this.sectionLabelById.get(sectionId) ?? "Other seats";
3599
+ const object = this.doc?.objects.find((item) => item.id === seat.rowId);
3600
+ const objectLabel = object?.type === "row" || object?.type === "table" ? object.label : "";
3601
+ const haystack = `${label} ${category} ${section} ${objectLabel}`.toLocaleLowerCase();
3602
+ if (!haystack.includes(query)) continue;
3603
+ }
3604
+ seats.push(seat);
3605
+ }
3606
+ return seats.sort((a, b) => a.label.localeCompare(b.label, void 0, { numeric: true, sensitivity: "base" }));
3607
+ }
3608
+ paintBlockedInventory() {
3609
+ if (!this.els.blockedlist) return;
3610
+ const allBlocked = [...this.status.entries()].filter(([, status]) => status === "blocked").length;
3611
+ const filtered = this.filteredBlockedSeats();
3612
+ const visible = filtered.slice(0, this.blockedResultLimit);
3613
+ const selected = new Set(this.selectionLabels());
3614
+ const selectedResults = filtered.filter((seat) => selected.has(seat.label)).length;
3615
+ const allResultsSelected = filtered.length > 0 && selectedResults === filtered.length;
3616
+ this.els.blockedcount.textContent = allBlocked.toLocaleString();
3617
+ this.els.blockedshowing.textContent = filtered.length ? `Showing ${visible.length.toLocaleString()} of ${filtered.length.toLocaleString()}` : allBlocked ? "No matches" : "No blocked seats";
3618
+ const selectResults = this.els.selblocked;
3619
+ selectResults.disabled = filtered.length === 0;
3620
+ selectResults.textContent = allResultsSelected ? `Remove ${filtered.length.toLocaleString()} results` : `Select ${filtered.length.toLocaleString()} results`;
3621
+ this.els.blockedlist.innerHTML = visible.length ? visible.map((seat) => {
3622
+ const sectionId = this.sectionByObject.get(seat.rowId) ?? UNGROUPED_ID;
3623
+ const section = this.sectionLabelById.get(sectionId) ?? "Other seats";
3624
+ const category = this.doc?.categories.find((item) => item.key === seat.categoryKey)?.label ?? seat.categoryKey;
3625
+ const isSelected = selected.has(seat.label);
3626
+ return `<button type="button" class="slm-blockeditem${isSelected ? " on" : ""}" data-blocked-label="${esc(seat.label)}" aria-pressed="${isSelected}">
3627
+ <span class="slm-blockedcheck" aria-hidden="true">\u2713</span>
3628
+ <span class="slm-blockedcopy"><span class="slm-blockedlabel">${esc(seat.label)}</span>
3629
+ <span class="slm-blockedmeta">${esc(section)} \xB7 ${esc(category)}</span></span>
3630
+ </button>`;
3631
+ }).join("") + (filtered.length > visible.length ? `<button type="button" class="slm-blockedmore" data-blocked-more>Show 100 more</button>` : "") : `<div class="slm-blockedempty">${allBlocked ? "No blocked seats match this search or section." : "No seats are blocked. Newly blocked seats will appear here."}</div>`;
3632
+ const markAll = this.els.markall;
3633
+ const armed = markAll.dataset.confirm === "true";
3634
+ markAll.disabled = allBlocked === 0;
3635
+ markAll.textContent = armed ? `Confirm: put all ${allBlocked.toLocaleString()} on sale` : `Put all ${allBlocked.toLocaleString()} blocked seats on sale`;
3636
+ }
3637
+ confirmUnblockAll() {
3638
+ const button = this.els.markall;
3639
+ if (!button || button.disabled) return;
3640
+ if (button.dataset.confirm === "true") {
3641
+ this.resetUnblockAllConfirm();
3642
+ void this.unblockAll();
3643
+ return;
3644
+ }
3645
+ button.dataset.confirm = "true";
3646
+ button.classList.add("danger");
3647
+ this.els.markallnote.textContent = "This changes every blocked seat. Click the red button again to confirm.";
3648
+ this.paintBlockedInventory();
3649
+ if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
3650
+ this.unblockAllConfirmTimer = setTimeout(() => this.resetUnblockAllConfirm(), 6e3);
3651
+ }
3652
+ resetUnblockAllConfirm() {
3653
+ if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
3654
+ this.unblockAllConfirmTimer = null;
3655
+ const button = this.els.markall;
3656
+ if (!button) return;
3657
+ delete button.dataset.confirm;
3658
+ button.classList.remove("danger");
3659
+ if (this.els.markallnote) this.els.markallnote.textContent = "For a full reset only. You will be asked to confirm.";
3660
+ this.paintBlockedInventory();
3421
3661
  }
3422
3662
  // ---- toast / done / fail --------------------------------------------------
3423
3663
  done(action, labels, msg) {