@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/README.md CHANGED
@@ -47,6 +47,9 @@ realtime organizer cockpit. Mint a short-lived, event-scoped `mse_` token from
47
47
  your backend, bound to the browser's exact origin and only the capabilities it
48
48
  needs. The shared surface owns Monitor, Inspect, Block/unblock, fullscreen,
49
49
  presence, exact section revenue, sales velocity, and a clearly explained **Sales momentum** overlay.
50
+ Block mode exposes explicit multi-select category state and a searchable,
51
+ section-filtered blocked-inventory list, so an organizer can put specific seats
52
+ back on sale without resetting the entire event.
50
53
 
51
54
  ```js
52
55
  import { SeatManager } from '@seatlayer/js';
package/dist/index.cjs CHANGED
@@ -2294,7 +2294,8 @@ var CSS2 = `
2294
2294
  .slm input{font:inherit}
2295
2295
 
2296
2296
  /* top bar */
2297
- .slm-bar{display:flex;align-items:center;gap:14px;padding:10px 16px;border-bottom:1px solid var(--slm-line);flex:none;flex-wrap:wrap}
2297
+ .slm-bar{display:grid;grid-template-columns:auto auto minmax(0,1fr);align-items:center;column-gap:14px;row-gap:10px;
2298
+ padding:10px 16px;border-bottom:1px solid var(--slm-line);flex:none}
2298
2299
  .slm-modes{display:inline-flex;background:var(--slm-surface);border:1px solid var(--slm-line);border-radius:999px;padding:3px}
2299
2300
  .slm-mode{padding:6px 16px;border-radius:999px;font-weight:700;font-size:13px;color:var(--slm-muted)}
2300
2301
  .slm-mode.on{background:var(--slm-accent);color:var(--slm-accent-ink)}
@@ -2302,9 +2303,11 @@ var CSS2 = `
2302
2303
  .slm-live-dot{width:8px;height:8px;border-radius:50%;background:#8b94ac}
2303
2304
  .slm.live .slm-live-dot{background:#22a06b;box-shadow:0 0 0 0 rgba(34,160,107,.55);animation:slm-pulse 2s infinite}
2304
2305
  @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)}}
2305
- .slm-kpis{display:flex;align-items:center;gap:16px;margin-left:auto;flex-wrap:wrap}
2306
- .slm-kpi{display:flex;flex-direction:column;line-height:1.15}
2307
- .slm-kpi b{font-size:17px;font-weight:800;font-variant-numeric:tabular-nums}
2306
+ .slm-kpis{grid-column:1/-1;display:grid;grid-template-columns:repeat(8,minmax(0,1fr));width:100%;padding-top:10px;
2307
+ border-top:1px solid var(--slm-line)}
2308
+ .slm-kpi{display:flex;min-width:0;flex-direction:column;align-items:center;padding:0 5px;line-height:1.15;text-align:center}
2309
+ .slm-kpi b{display:flex;min-width:0;align-items:baseline;justify-content:center;font-size:17px;font-weight:800;
2310
+ font-variant-numeric:tabular-nums;white-space:nowrap}
2308
2311
  .slm-kpi span{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--slm-muted);font-weight:700}
2309
2312
  .slm-kpi .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:baseline}
2310
2313
  .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}
@@ -2358,11 +2361,44 @@ var CSS2 = `
2358
2361
  font-size:12px;font-weight:700;color:var(--slm-text);display:inline-flex;align-items:center;gap:6px}
2359
2362
  .slm-chip:hover{border-color:var(--slm-muted)}
2360
2363
  .slm-chip .dot{width:8px;height:8px;border-radius:50%}
2364
+ .slm-chip .slm-chipcount{min-width:18px;padding:1px 5px;border-radius:999px;background:rgba(255,255,255,.07);
2365
+ color:var(--slm-muted);font-size:10px;font-variant-numeric:tabular-nums;text-align:center}
2366
+ .slm-chip .slm-chipcheck{display:none;font-size:11px;line-height:1}
2367
+ .slm-chip.on{border-color:var(--slm-accent);background:color-mix(in srgb,var(--slm-accent) 20%,var(--slm-surface));
2368
+ box-shadow:0 0 0 1px color-mix(in srgb,var(--slm-accent) 45%,transparent)}
2369
+ .slm-chip.on .slm-chipcount{background:var(--slm-accent);color:var(--slm-accent-ink)}
2370
+ .slm-chip.on .slm-chipcheck{display:inline}
2371
+ .slm-chip.partial{border-style:dashed;border-color:var(--slm-accent)}
2372
+ .slm-chip:disabled{opacity:.42;cursor:not-allowed}
2373
+ .slm-selecthelp{margin:-1px 0 9px;color:var(--slm-muted);font-size:11px;line-height:1.4}
2361
2374
  .slm-field{margin:14px 0}
2362
2375
  .slm-field label{display:block;font-size:11px;font-weight:700;color:var(--slm-muted);margin-bottom:5px}
2363
2376
  .slm-input,.slm-select{width:100%;padding:8px 10px;border-radius:9px;border:1px solid var(--slm-line);
2364
2377
  background:var(--slm-surface);color:var(--slm-text)}
2365
2378
  .slm-note{font-size:11.5px;color:var(--slm-muted);margin-top:5px}
2379
+ .slm-blocked{margin-top:17px;padding-top:15px;border-top:1px solid var(--slm-line)}
2380
+ .slm-blockedhead{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:8px}
2381
+ .slm-blockedhead .slm-eyebrow{margin-bottom:0}.slm-blockedtotal{font-size:11px;color:var(--slm-muted)}
2382
+ .slm-blockedtotal b{color:var(--slm-text);font-variant-numeric:tabular-nums}
2383
+ .slm-blockedtools{display:grid;grid-template-columns:minmax(0,1fr);gap:7px}
2384
+ .slm-blockedsummary{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:9px 0 6px;
2385
+ color:var(--slm-muted);font-size:10.5px}
2386
+ .slm-linkbtn{font-size:11px!important;font-weight:800!important;color:var(--slm-accent)!important;text-align:right}
2387
+ .slm-linkbtn:disabled{opacity:.45;cursor:not-allowed}
2388
+ .slm-blockedlist{max-height:246px;overflow:auto;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface)}
2389
+ .slm-blockeditem{display:grid!important;grid-template-columns:18px minmax(0,1fr);width:100%;gap:8px;padding:8px 9px!important;
2390
+ border-bottom:1px solid var(--slm-line)!important;text-align:left!important}
2391
+ .slm-blockeditem:last-child{border-bottom:0!important}.slm-blockeditem:hover{background:rgba(255,255,255,.035)!important}
2392
+ .slm-blockeditem.on{background:color-mix(in srgb,var(--slm-accent) 13%,var(--slm-surface))!important}
2393
+ .slm-blockedcheck{display:flex;align-items:center;justify-content:center;width:16px;height:16px;margin-top:1px;border-radius:4px;
2394
+ border:1px solid var(--slm-muted);color:transparent;font-size:10px;font-weight:900}
2395
+ .slm-blockeditem.on .slm-blockedcheck{border-color:var(--slm-accent);background:var(--slm-accent);color:var(--slm-accent-ink)}
2396
+ .slm-blockedcopy{min-width:0}.slm-blockedlabel{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
2397
+ font-size:12px;font-weight:800}.slm-blockedmeta{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
2398
+ margin-top:2px;color:var(--slm-muted);font-size:10px}
2399
+ .slm-blockedmore{width:100%;padding:9px!important;color:var(--slm-accent)!important;font-size:11px!important;font-weight:800!important}
2400
+ .slm-blockedempty{padding:12px;color:var(--slm-muted);font-size:11.5px;line-height:1.45}
2401
+ .slm-allnote{margin-top:-4px;margin-bottom:10px}
2366
2402
 
2367
2403
  /* toast */
2368
2404
  .slm-toast{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);padding:10px 16px;border-radius:10px;
@@ -2373,7 +2409,7 @@ var CSS2 = `
2373
2409
  .slm-toast.ok{background:#1f7a4d;color:#fff;border-color:#1f7a4d}
2374
2410
 
2375
2411
  /* control-room actions + insights */
2376
- .slm-bar-actions{display:flex;align-items:center;gap:7px}
2412
+ .slm-bar-actions{display:flex;align-items:center;justify-self:end;gap:7px}
2377
2413
  .slm-barbtn.on{background:rgba(244,183,64,.13);border-color:#f4b740;color:#f7ca6b}
2378
2414
  .slm-sectionlist{display:flex;flex-direction:column;gap:8px;margin-top:4px}
2379
2415
  .slm-sectionlist + .slm-eyebrow{margin-top:18px}
@@ -2391,16 +2427,22 @@ var CSS2 = `
2391
2427
  .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}
2392
2428
  .slm-momentumgradient{height:6px;min-width:64px;flex:1;border-radius:999px;background:linear-gradient(90deg,#f4b740,#ef4444)}
2393
2429
  .slm-momentumcopy{margin-top:7px;color:var(--slm-muted);font-size:11px;line-height:1.45}
2394
- .slm-inspect-card{padding:12px;border:1px solid var(--slm-line);border-radius:12px;background:var(--slm-surface)}
2395
- .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}
2396
- .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}
2430
+ .slm-inspect-card{padding:16px;border:1px solid var(--slm-line);border-radius:12px;background:var(--slm-surface)}
2431
+ .slm-inspect-label{font-size:24px;font-weight:850;letter-spacing:-.02em;line-height:1.1}
2432
+ .slm-inspect-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px 20px;margin-top:18px}
2433
+ .slm-inspect-grid>div{min-width:0}.slm-inspect-grid span{display:block;color:var(--slm-muted);font-size:10px;
2434
+ 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}
2397
2435
  .slm:fullscreen{border-radius:0;min-height:100vh;background:var(--slm-bg)}
2398
2436
  .slm:fullscreen .slm-bar{padding:14px 22px}.slm:fullscreen .slm-kpi b{font-size:21px}.slm:fullscreen .slm-rail{width:360px}
2399
2437
 
2400
2438
  .slm.compact .slm-rail{width:100%;border-left:0;border-top:1px solid var(--slm-line);height:44%}
2401
2439
  .slm.compact .slm-body{flex-direction:column}
2402
- .slm.compact .slm-bar{gap:8px;padding:8px}.slm.compact .slm-barbtn{padding:6px 9px}
2403
- .slm.compact .slm-kpis{gap:9px}.slm.compact .slm-kpi:nth-child(n+5){display:none}
2440
+ .slm.compact .slm-bar{grid-template-columns:minmax(0,1fr) auto;gap:8px;padding:8px}
2441
+ .slm.compact .slm-modes{min-width:0}.slm.compact .slm-mode{padding-inline:11px}
2442
+ .slm.compact .slm-live{justify-self:end}.slm.compact .slm-bar-actions{grid-column:1/-1;justify-self:stretch}
2443
+ .slm.compact .slm-barbtn{flex:1;padding:6px 9px}.slm.compact .slm-kpis{grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
2444
+ .slm.compact .slm-kpi[data-kpi="buyers"],.slm.compact .slm-kpi[data-kpi="active-holds"],
2445
+ .slm.compact .slm-kpi[data-kpi="sold-pct"],.slm.compact .slm-kpi[data-kpi="gross-sales"]{display:none}
2404
2446
  `;
2405
2447
  function injectStyle() {
2406
2448
  if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
@@ -2476,6 +2518,10 @@ var SeatManager = class {
2476
2518
  this.sectionByObject = /* @__PURE__ */ new Map();
2477
2519
  this.sectionLabelById = /* @__PURE__ */ new Map();
2478
2520
  this.lastSyncedAt = null;
2521
+ this.blockedQuery = "";
2522
+ this.blockedSection = "";
2523
+ this.blockedResultLimit = 100;
2524
+ this.unblockAllConfirmTimer = null;
2479
2525
  this.onFullscreenChange = () => {
2480
2526
  this.paintFullscreenButton();
2481
2527
  this.updateContainerLayout();
@@ -2621,13 +2667,13 @@ var SeatManager = class {
2621
2667
  const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "free");
2622
2668
  if (!targets.length) return;
2623
2669
  const releaseAt = opts.releaseAt ?? this.releaseAt ?? void 0;
2624
- for (const l of targets) this.setSeatLocal(l, "blocked");
2670
+ this.setSeatsLocal(targets, "blocked");
2625
2671
  try {
2626
2672
  await this.api.block(this.key, targets, { ...opts, releaseAt });
2627
2673
  this.clearSelection();
2628
2674
  this.done("block", targets, releaseAt ? `Blocked ${targets.length} \u2014 auto-release ${new Date(releaseAt).toLocaleString()}.` : `Blocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
2629
2675
  } catch (err) {
2630
- for (const l of targets) this.setSeatLocal(l, "free");
2676
+ this.setSeatsLocal(targets, "free");
2631
2677
  this.toastErr(err instanceof ManageApiError && err.status === 409 ? "Some seats were just taken. Try again." : "Couldn't block those seats.");
2632
2678
  this.opts.onError?.(err);
2633
2679
  }
@@ -2635,13 +2681,13 @@ var SeatManager = class {
2635
2681
  async unblock(labels) {
2636
2682
  const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "blocked");
2637
2683
  if (!targets.length) return;
2638
- for (const l of targets) this.setSeatLocal(l, "free");
2684
+ this.setSeatsLocal(targets, "free");
2639
2685
  try {
2640
2686
  await this.api.unblock(this.key, targets);
2641
2687
  this.clearSelection();
2642
2688
  this.done("unblock", targets, `Unblocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
2643
2689
  } catch (err) {
2644
- for (const l of targets) this.setSeatLocal(l, "blocked");
2690
+ this.setSeatsLocal(targets, "blocked");
2645
2691
  this.toastErr("Couldn't unblock those seats.");
2646
2692
  this.opts.onError?.(err);
2647
2693
  }
@@ -2649,9 +2695,10 @@ var SeatManager = class {
2649
2695
  async unblockAll() {
2650
2696
  const blocked = [...this.status.entries()].filter(([, s]) => s === "blocked").map(([l]) => l);
2651
2697
  if (!blocked.length) return;
2652
- for (const l of blocked) this.setSeatLocal(l, "free");
2698
+ this.setSeatsLocal(blocked, "free");
2653
2699
  try {
2654
2700
  const res = await this.api.unblockAll(this.key);
2701
+ this.clearSelection();
2655
2702
  this.done("unblockAll", blocked, `Unblocked ${res.freed} seat${res.freed === 1 ? "" : "s"}.`);
2656
2703
  } catch (err) {
2657
2704
  await this.resnapshot();
@@ -2663,13 +2710,13 @@ var SeatManager = class {
2663
2710
  async cancelBooking(labels, bookingRef) {
2664
2711
  const targets = labels.filter((l) => this.status.get(l) === "booked");
2665
2712
  if (!targets.length || !bookingRef) return;
2666
- for (const l of targets) this.setSeatLocal(l, "free");
2713
+ this.setSeatsLocal(targets, "free");
2667
2714
  try {
2668
2715
  await this.api.unbook(this.key, targets, bookingRef);
2669
2716
  this.clearSelection();
2670
2717
  this.done("cancelBooking", targets, `Cancelled ${targets.length} booking${targets.length === 1 ? "" : "s"}.`);
2671
2718
  } catch (err) {
2672
- for (const l of targets) this.setSeatLocal(l, "booked");
2719
+ this.setSeatsLocal(targets, "booked");
2673
2720
  this.toastErr("Couldn't cancel that booking. Check the reference.");
2674
2721
  this.opts.onError?.(err);
2675
2722
  }
@@ -2732,6 +2779,7 @@ var SeatManager = class {
2732
2779
  if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
2733
2780
  if (this.feedTimer) clearInterval(this.feedTimer);
2734
2781
  if (this.toastTimer) clearTimeout(this.toastTimer);
2782
+ if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
2735
2783
  if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
2736
2784
  if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
2737
2785
  this.layoutObserver?.disconnect();
@@ -2910,11 +2958,16 @@ var SeatManager = class {
2910
2958
  this.afterPaint();
2911
2959
  this.recomputeTallies();
2912
2960
  }
2913
- /** Optimistic local write shared by delta stream + organizer actions. */
2914
- setSeatLocal(label, st) {
2915
- this.status.set(label, st);
2916
- const id = this.labelToId.get(label);
2917
- if (id) this.renderer?.setStatus([id], toRenderStatus(st));
2961
+ /** Optimistic local write shared by organizer actions. Paint and tally once,
2962
+ * even when an arena-sized operation changes hundreds of seats. */
2963
+ setSeatsLocal(labels, st) {
2964
+ const ids = [];
2965
+ for (const label of labels) {
2966
+ this.status.set(label, st);
2967
+ const id = this.labelToId.get(label);
2968
+ if (id) ids.push(id);
2969
+ }
2970
+ if (ids.length) this.renderer?.setStatus(ids, toRenderStatus(st));
2918
2971
  this.afterPaint();
2919
2972
  this.recomputeTallies();
2920
2973
  }
@@ -2997,6 +3050,7 @@ var SeatManager = class {
2997
3050
  this.paintLegend(t3);
2998
3051
  this.paintMonitorInsights();
2999
3052
  } else if (this.mode === "inspect") this.renderInspectRail(this.getSelection());
3053
+ else if (this.mode === "block") this.paintSelBar(this.getSelection());
3000
3054
  this.opts.onTallies?.(t3);
3001
3055
  }
3002
3056
  verbFor(prev, next) {
@@ -3218,15 +3272,15 @@ var SeatManager = class {
3218
3272
  const rev = t3.revenueStatus === "current" ? fmtMoney(t3.grossRevenue, t3.currency) : "\u2014";
3219
3273
  const presence = this.controlRoomSnapshot?.presence;
3220
3274
  this.els.kpis.innerHTML = [
3221
- { n: t3.booked.toLocaleString(), l: "Sold", dot: "#22a06b" },
3222
- { n: t3.held.toLocaleString(), l: "Held", dot: "#f4b740" },
3223
- { n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Shopping" },
3224
- { n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Live holds" },
3225
- { n: t3.free.toLocaleString(), l: "Free", dot: "#6e7bff" },
3226
- { n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac" },
3227
- { n: `${t3.capacityPct}%`, l: "Capacity" },
3228
- { n: rev, l: "Gross" }
3229
- ].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("");
3275
+ { key: "sold-seats", n: t3.booked.toLocaleString(), l: "Sold seats", dot: "#22a06b" },
3276
+ { key: "held-seats", n: t3.held.toLocaleString(), l: "Held seats", dot: "#f4b740" },
3277
+ { key: "buyers", n: presence ? presence.shoppingSessions.toLocaleString() : "\u2014", l: "Buyers" },
3278
+ { key: "active-holds", n: presence ? presence.activeHolds.toLocaleString() : "\u2014", l: "Active holds" },
3279
+ { key: "free-seats", n: t3.free.toLocaleString(), l: "Free seats", dot: "#6e7bff" },
3280
+ { key: "blocked", n: t3.blocked.toLocaleString(), l: "Blocked", dot: "#8b94ac" },
3281
+ { key: "sold-pct", n: `${t3.capacityPct}%`, l: "Sold" },
3282
+ { key: "gross-sales", n: rev, l: "Gross sales" }
3283
+ ].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("");
3230
3284
  }
3231
3285
  // ---- DOM: rails -----------------------------------------------------------
3232
3286
  paintRail() {
@@ -3325,9 +3379,9 @@ var SeatManager = class {
3325
3379
  const seat = seats[seats.length - 1];
3326
3380
  if (!seat) {
3327
3381
  this.els.rail.innerHTML = `
3328
- <p class="slm-eyebrow">Inspect</p>
3329
- <p class="slm-hint">Select any seat to see its live inventory context. Inspect is read-only and never changes availability.</p>
3330
- <div class="slm-empty">Choose a seat on the map.</div>`;
3382
+ <p class="slm-eyebrow">Inspect seats</p>
3383
+ <p class="slm-hint">Select a seat to see its availability and sales context. Nothing changes in this view.</p>
3384
+ <div class="slm-empty">Select a seat on the map.</div>`;
3331
3385
  return;
3332
3386
  }
3333
3387
  const status = this.status.get(seat.label) ?? "free";
@@ -3336,17 +3390,20 @@ var SeatManager = class {
3336
3390
  const sectionLabel = this.sectionLabelById.get(sectionId) ?? "Other seats";
3337
3391
  const category = this.doc?.categories.find((item) => item.key === seat.categoryKey);
3338
3392
  const sectionMetric = this.controlRoomSnapshot?.revenue.bySection.find((row) => row.sectionId === sectionId);
3393
+ const object = this.doc?.objects.find((item) => item.id === seat.rowId);
3394
+ 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;
3395
+ const itemKind = seat.kind === "booth" ? "Booth" : "Seat";
3339
3396
  this.els.rail.innerHTML = `
3340
- <p class="slm-eyebrow">Inspect</p>
3341
- <p class="slm-hint">Live inventory context. Booked seats remain read-only; order and payment actions belong to the host commerce system.</p>
3397
+ <p class="slm-eyebrow">${itemKind} details</p>
3398
+ <p class="slm-hint">Live availability and section performance.</p>
3342
3399
  <div class="slm-inspect-card">
3343
3400
  <div class="slm-inspect-label">${esc(seat.label)}</div>
3344
3401
  <div class="slm-inspect-grid">
3345
3402
  <div><span>Status</span><b>${statusLabel[status]}</b></div>
3346
3403
  <div><span>Section</span><b>${esc(sectionLabel)}</b></div>
3347
- <div><span>Row</span><b>${esc(seat.rowId)}</b></div>
3404
+ ${location ? `<div><span>${location.label}</span><b>${esc(location.value)}</b></div>` : ""}
3348
3405
  <div><span>Category</span><b>${esc(category?.label ?? seat.categoryKey)}</b></div>
3349
- <div><span>Section sold</span><b>${sectionMetric ? `${sectionMetric.booked}/${sectionMetric.total}` : "\u2014"}</b></div>
3406
+ <div><span>Sold in section</span><b>${sectionMetric ? `${sectionMetric.booked} of ${sectionMetric.total}` : "\u2014"}</b></div>
3350
3407
  <div><span>Section revenue</span><b>${sectionMetric && this.controlRoomSnapshot ? fmtMoney(sectionMetric.bookedRevenue, this.controlRoomSnapshot.currency) : "\u2014"}</b></div>
3351
3408
  </div>
3352
3409
  </div>`;
@@ -3373,23 +3430,30 @@ var SeatManager = class {
3373
3430
  }
3374
3431
  renderBlockRail() {
3375
3432
  const cats = this.doc?.categories ?? [];
3376
- 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("");
3433
+ const catChips = cats.map((c) => `<button class="slm-chip" type="button" data-cat="${esc(c.key)}" aria-pressed="false">
3434
+ <span class="dot" style="background:${esc(c.color ?? "#6e7bff")}"></span>
3435
+ <span>${esc(c.label ?? c.key)}</span>
3436
+ <span class="slm-chipcount" data-cat-count>0</span>
3437
+ <span class="slm-chipcheck" aria-hidden="true">\u2713</span>
3438
+ </button>`).join("");
3377
3439
  const sectionField = this.sectionOptions.length ? `<div class="slm-field"><label>Select a whole section</label>
3378
3440
  <select class="slm-select" data-ref="section"><option value="">Choose a section\u2026</option>
3379
3441
  ${this.sectionOptions.map((s) => `<option value="${esc(s.id)}">${esc(s.label)}</option>`).join("")}</select></div>` : "";
3442
+ const blockedSectionOptions = this.sectionOptions.map((s) => `<option value="${esc(s.id)}">${esc(s.label)}</option>`).join("");
3380
3443
  this.els.rail.innerHTML = `
3381
3444
  <p class="slm-eyebrow">Block &amp; unblock</p>
3382
3445
  <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>
3383
- <div class="slm-selbar"><span class="slm-selnum" data-ref="selnum">0</span><span class="slm-sellabel">selected</span></div>
3446
+ <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>
3384
3447
  <div class="slm-row">
3385
3448
  <button class="slm-btn" data-ref="doblock" disabled>Block</button>
3386
- <button class="slm-btn ghost" data-ref="dounblock" disabled>Unblock</button>
3449
+ <button class="slm-btn ghost" data-ref="dounblock" disabled>Put back on sale</button>
3387
3450
  </div>
3388
3451
  <div class="slm-row">
3389
3452
  <button class="slm-btn ghost" data-ref="selall">Select all</button>
3390
3453
  <button class="slm-btn ghost" data-ref="clearsel">Clear</button>
3391
3454
  </div>
3392
- <p class="slm-eyebrow" style="margin-top:8px">By category</p>
3455
+ <p class="slm-eyebrow" style="margin-top:8px">Select by category</p>
3456
+ <p class="slm-selecthelp">Choose one or more. A checked category is selected; click it again to remove it.</p>
3393
3457
  <div class="slm-chiprow">${catChips || '<span class="slm-empty">No categories.</span>'}</div>
3394
3458
  ${sectionField}
3395
3459
  <div class="slm-field">
@@ -3397,18 +3461,46 @@ var SeatManager = class {
3397
3461
  <input type="datetime-local" class="slm-input" data-ref="release" />
3398
3462
  <p class="slm-note" data-ref="releasenote">Leave empty to block permanently.</p>
3399
3463
  </div>
3400
- <div class="slm-row"><button class="slm-btn ghost" data-ref="markall" style="flex:1">Mark everything for sale</button></div>
3464
+ <section class="slm-blocked" aria-labelledby="slm-blocked-title">
3465
+ <div class="slm-blockedhead">
3466
+ <p class="slm-eyebrow" id="slm-blocked-title">Blocked inventory</p>
3467
+ <span class="slm-blockedtotal"><b data-ref="blockedcount">0</b> out of sale</span>
3468
+ </div>
3469
+ <p class="slm-selecthelp">Find blocked seats, select only the ones you need, then use \u201CPut back on sale\u201D.</p>
3470
+ <div class="slm-blockedtools">
3471
+ <input type="search" class="slm-input" data-ref="blockedsearch" placeholder="Find seat, row or category" aria-label="Search blocked seats" />
3472
+ <select class="slm-select" data-ref="blockedsection" aria-label="Filter blocked seats by section">
3473
+ <option value="">All sections</option>${blockedSectionOptions}
3474
+ </select>
3475
+ </div>
3476
+ <div class="slm-blockedsummary">
3477
+ <span data-ref="blockedshowing">No blocked seats</span>
3478
+ <button type="button" class="slm-linkbtn" data-ref="selblocked" disabled>Select results</button>
3479
+ </div>
3480
+ <div class="slm-blockedlist" data-ref="blockedlist"></div>
3481
+ </section>
3482
+ <div class="slm-field">
3483
+ <button class="slm-btn ghost" data-ref="markall" style="width:100%" disabled>Put all blocked seats on sale</button>
3484
+ <p class="slm-note slm-allnote" data-ref="markallnote">For a full reset only. You will be asked to confirm.</p>
3485
+ </div>
3401
3486
  `;
3402
3487
  const r = (n) => this.els.rail.querySelector(`[data-ref="${n}"]`);
3403
3488
  this.els.selnum = r("selnum");
3404
3489
  this.els.doblock = r("doblock");
3405
3490
  this.els.dounblock = r("dounblock");
3491
+ this.els.selmeta = r("selmeta");
3492
+ this.els.blockedcount = r("blockedcount");
3493
+ this.els.blockedshowing = r("blockedshowing");
3494
+ this.els.blockedlist = r("blockedlist");
3495
+ this.els.selblocked = r("selblocked");
3496
+ this.els.markall = r("markall");
3497
+ this.els.markallnote = r("markallnote");
3406
3498
  r("doblock").addEventListener("click", () => void this.block());
3407
3499
  r("dounblock").addEventListener("click", () => void this.unblock());
3408
3500
  r("selall").addEventListener("click", () => this.selectAll());
3409
3501
  r("clearsel").addEventListener("click", () => this.clearSelection());
3410
- r("markall").addEventListener("click", () => void this.unblockAll());
3411
- this.els.rail.querySelectorAll("[data-cat]").forEach((b) => b.addEventListener("click", () => this.selectCategory(b.dataset.cat)));
3502
+ r("markall").addEventListener("click", () => this.confirmUnblockAll());
3503
+ this.els.rail.querySelectorAll("[data-cat]").forEach((b) => b.addEventListener("click", () => this.toggleCategory(b.dataset.cat)));
3412
3504
  const sectionSel = this.els.rail.querySelector('[data-ref="section"]');
3413
3505
  sectionSel?.addEventListener("change", () => {
3414
3506
  if (sectionSel.value) {
@@ -3416,6 +3508,32 @@ var SeatManager = class {
3416
3508
  sectionSel.value = "";
3417
3509
  }
3418
3510
  });
3511
+ const blockedSearch = r("blockedsearch");
3512
+ const blockedSection = r("blockedsection");
3513
+ blockedSearch.value = this.blockedQuery;
3514
+ blockedSection.value = this.blockedSection;
3515
+ blockedSearch.addEventListener("input", () => {
3516
+ this.blockedQuery = blockedSearch.value;
3517
+ this.blockedResultLimit = 100;
3518
+ this.paintBlockedInventory();
3519
+ });
3520
+ blockedSection.addEventListener("change", () => {
3521
+ this.blockedSection = blockedSection.value;
3522
+ this.blockedResultLimit = 100;
3523
+ this.paintBlockedInventory();
3524
+ });
3525
+ r("selblocked").addEventListener("click", () => {
3526
+ this.toggleLabels(this.filteredBlockedSeats().map((seat) => seat.label));
3527
+ });
3528
+ r("blockedlist").addEventListener("click", (event) => {
3529
+ const target = event.target;
3530
+ const seatButton = target.closest("[data-blocked-label]");
3531
+ if (seatButton?.dataset.blockedLabel) this.toggleLabels([seatButton.dataset.blockedLabel]);
3532
+ else if (target.closest("[data-blocked-more]")) {
3533
+ this.blockedResultLimit += 100;
3534
+ this.paintBlockedInventory();
3535
+ }
3536
+ });
3419
3537
  const rel = r("release");
3420
3538
  rel.addEventListener("change", () => {
3421
3539
  const ms = rel.value ? new Date(rel.value).getTime() : NaN;
@@ -3425,18 +3543,140 @@ var SeatManager = class {
3425
3543
  });
3426
3544
  this.paintSelBar(this.getSelection());
3427
3545
  }
3428
- selectCategory(catKey) {
3546
+ toggleCategory(catKey) {
3429
3547
  const labels = [];
3430
- for (const [label, seat] of this.labelToSeat.entries()) if (seat.categoryKey === catKey) labels.push(label);
3431
- this.selectByLabels(labels);
3548
+ for (const [label, seat] of this.labelToSeat.entries()) {
3549
+ if (seat.categoryKey === catKey && this.isBlockSelectable(label)) labels.push(label);
3550
+ }
3551
+ this.toggleLabels(labels);
3552
+ }
3553
+ /** A category/filter is a real toggle: add the missing seats, or remove the
3554
+ * whole group when every eligible seat in it is already selected. */
3555
+ toggleLabels(labels) {
3556
+ if (!this.renderer) return;
3557
+ const eligible = labels.filter((label) => this.labelToSeat.has(label) && this.isBlockSelectable(label));
3558
+ if (!eligible.length) return;
3559
+ const selected = new Set(this.selectionLabels());
3560
+ const allSelected = eligible.every((label) => selected.has(label));
3561
+ if (allSelected) {
3562
+ const ids = eligible.map((label) => this.labelToId.get(label)).filter((id) => Boolean(id));
3563
+ this.renderer.deselect(ids);
3564
+ } else {
3565
+ this.renderer.selectByLabels(eligible);
3566
+ }
3567
+ this.syncSelection();
3568
+ }
3569
+ isBlockSelectable(label) {
3570
+ const status = this.status.get(label) ?? "free";
3571
+ return status === "free" || status === "blocked";
3432
3572
  }
3433
3573
  paintSelBar(seats) {
3434
3574
  if (!this.els.selnum) return;
3435
3575
  this.els.selnum.textContent = seats.length.toLocaleString();
3436
- const hasFree = seats.some((s) => this.status.get(s.label) === "free");
3437
- const hasBlocked = seats.some((s) => this.status.get(s.label) === "blocked");
3438
- this.els.doblock.disabled = !hasFree;
3439
- this.els.dounblock.disabled = !hasBlocked;
3576
+ const freeCount = seats.filter((s) => (this.status.get(s.label) ?? "free") === "free").length;
3577
+ const blockedCount = seats.filter((s) => this.status.get(s.label) === "blocked").length;
3578
+ this.els.selmeta.textContent = seats.length ? `${freeCount.toLocaleString()} available \xB7 ${blockedCount.toLocaleString()} blocked` : "selected";
3579
+ const blockButton = this.els.doblock;
3580
+ const unblockButton = this.els.dounblock;
3581
+ blockButton.disabled = freeCount === 0;
3582
+ unblockButton.disabled = blockedCount === 0;
3583
+ blockButton.textContent = freeCount ? `Block ${freeCount.toLocaleString()}` : "Block selected";
3584
+ unblockButton.textContent = blockedCount ? `Put ${blockedCount.toLocaleString()} on sale` : "Put back on sale";
3585
+ this.paintCategoryControls(seats);
3586
+ this.paintBlockedInventory();
3587
+ }
3588
+ paintCategoryControls(seats) {
3589
+ const selected = new Set(seats.map((seat) => seat.label));
3590
+ this.els.rail?.querySelectorAll("[data-cat]").forEach((button) => {
3591
+ const catKey = button.dataset.cat;
3592
+ const labels = [];
3593
+ for (const [label, seat] of this.labelToSeat.entries()) {
3594
+ if (seat.categoryKey === catKey && this.isBlockSelectable(label)) labels.push(label);
3595
+ }
3596
+ const picked = labels.filter((label) => selected.has(label)).length;
3597
+ const full = labels.length > 0 && picked === labels.length;
3598
+ const partial = picked > 0 && !full;
3599
+ button.disabled = labels.length === 0;
3600
+ button.classList.toggle("on", full);
3601
+ button.classList.toggle("partial", partial);
3602
+ button.setAttribute("aria-pressed", full ? "true" : partial ? "mixed" : "false");
3603
+ 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`);
3604
+ const count = button.querySelector("[data-cat-count]");
3605
+ if (count) count.textContent = picked ? `${picked.toLocaleString()}/${labels.length.toLocaleString()}` : labels.length.toLocaleString();
3606
+ });
3607
+ }
3608
+ filteredBlockedSeats() {
3609
+ const query = this.blockedQuery.trim().toLocaleLowerCase();
3610
+ const seats = [];
3611
+ for (const [label, seat] of this.labelToSeat.entries()) {
3612
+ if (this.status.get(label) !== "blocked") continue;
3613
+ const sectionId = this.sectionByObject.get(seat.rowId) ?? import_core3.UNGROUPED_ID;
3614
+ if (this.blockedSection && sectionId !== this.blockedSection) continue;
3615
+ if (query) {
3616
+ const category = this.doc?.categories.find((item) => item.key === seat.categoryKey)?.label ?? seat.categoryKey;
3617
+ const section = this.sectionLabelById.get(sectionId) ?? "Other seats";
3618
+ const object = this.doc?.objects.find((item) => item.id === seat.rowId);
3619
+ const objectLabel = object?.type === "row" || object?.type === "table" ? object.label : "";
3620
+ const haystack = `${label} ${category} ${section} ${objectLabel}`.toLocaleLowerCase();
3621
+ if (!haystack.includes(query)) continue;
3622
+ }
3623
+ seats.push(seat);
3624
+ }
3625
+ return seats.sort((a, b) => a.label.localeCompare(b.label, void 0, { numeric: true, sensitivity: "base" }));
3626
+ }
3627
+ paintBlockedInventory() {
3628
+ if (!this.els.blockedlist) return;
3629
+ const allBlocked = [...this.status.entries()].filter(([, status]) => status === "blocked").length;
3630
+ const filtered = this.filteredBlockedSeats();
3631
+ const visible = filtered.slice(0, this.blockedResultLimit);
3632
+ const selected = new Set(this.selectionLabels());
3633
+ const selectedResults = filtered.filter((seat) => selected.has(seat.label)).length;
3634
+ const allResultsSelected = filtered.length > 0 && selectedResults === filtered.length;
3635
+ this.els.blockedcount.textContent = allBlocked.toLocaleString();
3636
+ this.els.blockedshowing.textContent = filtered.length ? `Showing ${visible.length.toLocaleString()} of ${filtered.length.toLocaleString()}` : allBlocked ? "No matches" : "No blocked seats";
3637
+ const selectResults = this.els.selblocked;
3638
+ selectResults.disabled = filtered.length === 0;
3639
+ selectResults.textContent = allResultsSelected ? `Remove ${filtered.length.toLocaleString()} results` : `Select ${filtered.length.toLocaleString()} results`;
3640
+ this.els.blockedlist.innerHTML = visible.length ? visible.map((seat) => {
3641
+ const sectionId = this.sectionByObject.get(seat.rowId) ?? import_core3.UNGROUPED_ID;
3642
+ const section = this.sectionLabelById.get(sectionId) ?? "Other seats";
3643
+ const category = this.doc?.categories.find((item) => item.key === seat.categoryKey)?.label ?? seat.categoryKey;
3644
+ const isSelected = selected.has(seat.label);
3645
+ return `<button type="button" class="slm-blockeditem${isSelected ? " on" : ""}" data-blocked-label="${esc(seat.label)}" aria-pressed="${isSelected}">
3646
+ <span class="slm-blockedcheck" aria-hidden="true">\u2713</span>
3647
+ <span class="slm-blockedcopy"><span class="slm-blockedlabel">${esc(seat.label)}</span>
3648
+ <span class="slm-blockedmeta">${esc(section)} \xB7 ${esc(category)}</span></span>
3649
+ </button>`;
3650
+ }).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>`;
3651
+ const markAll = this.els.markall;
3652
+ const armed = markAll.dataset.confirm === "true";
3653
+ markAll.disabled = allBlocked === 0;
3654
+ markAll.textContent = armed ? `Confirm: put all ${allBlocked.toLocaleString()} on sale` : `Put all ${allBlocked.toLocaleString()} blocked seats on sale`;
3655
+ }
3656
+ confirmUnblockAll() {
3657
+ const button = this.els.markall;
3658
+ if (!button || button.disabled) return;
3659
+ if (button.dataset.confirm === "true") {
3660
+ this.resetUnblockAllConfirm();
3661
+ void this.unblockAll();
3662
+ return;
3663
+ }
3664
+ button.dataset.confirm = "true";
3665
+ button.classList.add("danger");
3666
+ this.els.markallnote.textContent = "This changes every blocked seat. Click the red button again to confirm.";
3667
+ this.paintBlockedInventory();
3668
+ if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
3669
+ this.unblockAllConfirmTimer = setTimeout(() => this.resetUnblockAllConfirm(), 6e3);
3670
+ }
3671
+ resetUnblockAllConfirm() {
3672
+ if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
3673
+ this.unblockAllConfirmTimer = null;
3674
+ const button = this.els.markall;
3675
+ if (!button) return;
3676
+ delete button.dataset.confirm;
3677
+ button.classList.remove("danger");
3678
+ if (this.els.markallnote) this.els.markallnote.textContent = "For a full reset only. You will be asked to confirm.";
3679
+ this.paintBlockedInventory();
3440
3680
  }
3441
3681
  // ---- toast / done / fail --------------------------------------------------
3442
3682
  done(action, labels, msg) {