@seatlayer/js 0.12.3 → 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
@@ -2361,11 +2361,44 @@ var CSS2 = `
2361
2361
  font-size:12px;font-weight:700;color:var(--slm-text);display:inline-flex;align-items:center;gap:6px}
2362
2362
  .slm-chip:hover{border-color:var(--slm-muted)}
2363
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}
2364
2374
  .slm-field{margin:14px 0}
2365
2375
  .slm-field label{display:block;font-size:11px;font-weight:700;color:var(--slm-muted);margin-bottom:5px}
2366
2376
  .slm-input,.slm-select{width:100%;padding:8px 10px;border-radius:9px;border:1px solid var(--slm-line);
2367
2377
  background:var(--slm-surface);color:var(--slm-text)}
2368
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}
2369
2402
 
2370
2403
  /* toast */
2371
2404
  .slm-toast{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);padding:10px 16px;border-radius:10px;
@@ -2485,6 +2518,10 @@ var SeatManager = class {
2485
2518
  this.sectionByObject = /* @__PURE__ */ new Map();
2486
2519
  this.sectionLabelById = /* @__PURE__ */ new Map();
2487
2520
  this.lastSyncedAt = null;
2521
+ this.blockedQuery = "";
2522
+ this.blockedSection = "";
2523
+ this.blockedResultLimit = 100;
2524
+ this.unblockAllConfirmTimer = null;
2488
2525
  this.onFullscreenChange = () => {
2489
2526
  this.paintFullscreenButton();
2490
2527
  this.updateContainerLayout();
@@ -2630,13 +2667,13 @@ var SeatManager = class {
2630
2667
  const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "free");
2631
2668
  if (!targets.length) return;
2632
2669
  const releaseAt = opts.releaseAt ?? this.releaseAt ?? void 0;
2633
- for (const l of targets) this.setSeatLocal(l, "blocked");
2670
+ this.setSeatsLocal(targets, "blocked");
2634
2671
  try {
2635
2672
  await this.api.block(this.key, targets, { ...opts, releaseAt });
2636
2673
  this.clearSelection();
2637
2674
  this.done("block", targets, releaseAt ? `Blocked ${targets.length} \u2014 auto-release ${new Date(releaseAt).toLocaleString()}.` : `Blocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
2638
2675
  } catch (err) {
2639
- for (const l of targets) this.setSeatLocal(l, "free");
2676
+ this.setSeatsLocal(targets, "free");
2640
2677
  this.toastErr(err instanceof ManageApiError && err.status === 409 ? "Some seats were just taken. Try again." : "Couldn't block those seats.");
2641
2678
  this.opts.onError?.(err);
2642
2679
  }
@@ -2644,13 +2681,13 @@ var SeatManager = class {
2644
2681
  async unblock(labels) {
2645
2682
  const targets = (labels ?? this.selectionLabels()).filter((l) => this.status.get(l) === "blocked");
2646
2683
  if (!targets.length) return;
2647
- for (const l of targets) this.setSeatLocal(l, "free");
2684
+ this.setSeatsLocal(targets, "free");
2648
2685
  try {
2649
2686
  await this.api.unblock(this.key, targets);
2650
2687
  this.clearSelection();
2651
2688
  this.done("unblock", targets, `Unblocked ${targets.length} seat${targets.length === 1 ? "" : "s"}.`);
2652
2689
  } catch (err) {
2653
- for (const l of targets) this.setSeatLocal(l, "blocked");
2690
+ this.setSeatsLocal(targets, "blocked");
2654
2691
  this.toastErr("Couldn't unblock those seats.");
2655
2692
  this.opts.onError?.(err);
2656
2693
  }
@@ -2658,9 +2695,10 @@ var SeatManager = class {
2658
2695
  async unblockAll() {
2659
2696
  const blocked = [...this.status.entries()].filter(([, s]) => s === "blocked").map(([l]) => l);
2660
2697
  if (!blocked.length) return;
2661
- for (const l of blocked) this.setSeatLocal(l, "free");
2698
+ this.setSeatsLocal(blocked, "free");
2662
2699
  try {
2663
2700
  const res = await this.api.unblockAll(this.key);
2701
+ this.clearSelection();
2664
2702
  this.done("unblockAll", blocked, `Unblocked ${res.freed} seat${res.freed === 1 ? "" : "s"}.`);
2665
2703
  } catch (err) {
2666
2704
  await this.resnapshot();
@@ -2672,13 +2710,13 @@ var SeatManager = class {
2672
2710
  async cancelBooking(labels, bookingRef) {
2673
2711
  const targets = labels.filter((l) => this.status.get(l) === "booked");
2674
2712
  if (!targets.length || !bookingRef) return;
2675
- for (const l of targets) this.setSeatLocal(l, "free");
2713
+ this.setSeatsLocal(targets, "free");
2676
2714
  try {
2677
2715
  await this.api.unbook(this.key, targets, bookingRef);
2678
2716
  this.clearSelection();
2679
2717
  this.done("cancelBooking", targets, `Cancelled ${targets.length} booking${targets.length === 1 ? "" : "s"}.`);
2680
2718
  } catch (err) {
2681
- for (const l of targets) this.setSeatLocal(l, "booked");
2719
+ this.setSeatsLocal(targets, "booked");
2682
2720
  this.toastErr("Couldn't cancel that booking. Check the reference.");
2683
2721
  this.opts.onError?.(err);
2684
2722
  }
@@ -2741,6 +2779,7 @@ var SeatManager = class {
2741
2779
  if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
2742
2780
  if (this.feedTimer) clearInterval(this.feedTimer);
2743
2781
  if (this.toastTimer) clearTimeout(this.toastTimer);
2782
+ if (this.unblockAllConfirmTimer) clearTimeout(this.unblockAllConfirmTimer);
2744
2783
  if (this.revenueRefreshTimer) clearTimeout(this.revenueRefreshTimer);
2745
2784
  if (this.tokenRefreshTimer) clearTimeout(this.tokenRefreshTimer);
2746
2785
  this.layoutObserver?.disconnect();
@@ -2919,11 +2958,16 @@ var SeatManager = class {
2919
2958
  this.afterPaint();
2920
2959
  this.recomputeTallies();
2921
2960
  }
2922
- /** Optimistic local write shared by delta stream + organizer actions. */
2923
- setSeatLocal(label, st) {
2924
- this.status.set(label, st);
2925
- const id = this.labelToId.get(label);
2926
- 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));
2927
2971
  this.afterPaint();
2928
2972
  this.recomputeTallies();
2929
2973
  }
@@ -3006,6 +3050,7 @@ var SeatManager = class {
3006
3050
  this.paintLegend(t3);
3007
3051
  this.paintMonitorInsights();
3008
3052
  } else if (this.mode === "inspect") this.renderInspectRail(this.getSelection());
3053
+ else if (this.mode === "block") this.paintSelBar(this.getSelection());
3009
3054
  this.opts.onTallies?.(t3);
3010
3055
  }
3011
3056
  verbFor(prev, next) {
@@ -3385,23 +3430,30 @@ var SeatManager = class {
3385
3430
  }
3386
3431
  renderBlockRail() {
3387
3432
  const cats = this.doc?.categories ?? [];
3388
- 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("");
3389
3439
  const sectionField = this.sectionOptions.length ? `<div class="slm-field"><label>Select a whole section</label>
3390
3440
  <select class="slm-select" data-ref="section"><option value="">Choose a section\u2026</option>
3391
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("");
3392
3443
  this.els.rail.innerHTML = `
3393
3444
  <p class="slm-eyebrow">Block &amp; unblock</p>
3394
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>
3395
- <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>
3396
3447
  <div class="slm-row">
3397
3448
  <button class="slm-btn" data-ref="doblock" disabled>Block</button>
3398
- <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>
3399
3450
  </div>
3400
3451
  <div class="slm-row">
3401
3452
  <button class="slm-btn ghost" data-ref="selall">Select all</button>
3402
3453
  <button class="slm-btn ghost" data-ref="clearsel">Clear</button>
3403
3454
  </div>
3404
- <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>
3405
3457
  <div class="slm-chiprow">${catChips || '<span class="slm-empty">No categories.</span>'}</div>
3406
3458
  ${sectionField}
3407
3459
  <div class="slm-field">
@@ -3409,18 +3461,46 @@ var SeatManager = class {
3409
3461
  <input type="datetime-local" class="slm-input" data-ref="release" />
3410
3462
  <p class="slm-note" data-ref="releasenote">Leave empty to block permanently.</p>
3411
3463
  </div>
3412
- <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>
3413
3486
  `;
3414
3487
  const r = (n) => this.els.rail.querySelector(`[data-ref="${n}"]`);
3415
3488
  this.els.selnum = r("selnum");
3416
3489
  this.els.doblock = r("doblock");
3417
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");
3418
3498
  r("doblock").addEventListener("click", () => void this.block());
3419
3499
  r("dounblock").addEventListener("click", () => void this.unblock());
3420
3500
  r("selall").addEventListener("click", () => this.selectAll());
3421
3501
  r("clearsel").addEventListener("click", () => this.clearSelection());
3422
- r("markall").addEventListener("click", () => void this.unblockAll());
3423
- 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)));
3424
3504
  const sectionSel = this.els.rail.querySelector('[data-ref="section"]');
3425
3505
  sectionSel?.addEventListener("change", () => {
3426
3506
  if (sectionSel.value) {
@@ -3428,6 +3508,32 @@ var SeatManager = class {
3428
3508
  sectionSel.value = "";
3429
3509
  }
3430
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
+ });
3431
3537
  const rel = r("release");
3432
3538
  rel.addEventListener("change", () => {
3433
3539
  const ms = rel.value ? new Date(rel.value).getTime() : NaN;
@@ -3437,18 +3543,140 @@ var SeatManager = class {
3437
3543
  });
3438
3544
  this.paintSelBar(this.getSelection());
3439
3545
  }
3440
- selectCategory(catKey) {
3546
+ toggleCategory(catKey) {
3441
3547
  const labels = [];
3442
- for (const [label, seat] of this.labelToSeat.entries()) if (seat.categoryKey === catKey) labels.push(label);
3443
- 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";
3444
3572
  }
3445
3573
  paintSelBar(seats) {
3446
3574
  if (!this.els.selnum) return;
3447
3575
  this.els.selnum.textContent = seats.length.toLocaleString();
3448
- const hasFree = seats.some((s) => this.status.get(s.label) === "free");
3449
- const hasBlocked = seats.some((s) => this.status.get(s.label) === "blocked");
3450
- this.els.doblock.disabled = !hasFree;
3451
- 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();
3452
3680
  }
3453
3681
  // ---- toast / done / fail --------------------------------------------------
3454
3682
  done(action, labels, msg) {