@seatlayer/js 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -26,7 +26,8 @@ __export(index_exports, {
26
26
  ManageApiError: () => ManageApiError,
27
27
  SeatManager: () => SeatManager,
28
28
  SeatPicker: () => SeatPicker,
29
- SeatingChart: () => SeatingChart
29
+ SeatingChart: () => SeatingChart,
30
+ attachPickerFrame: () => attachPickerFrame
30
31
  });
31
32
  module.exports = __toCommonJS(index_exports);
32
33
 
@@ -398,10 +399,30 @@ var EmbeddedDesigner = class {
398
399
  this.timeoutTimer = null;
399
400
  this.phase = "loading";
400
401
  this.restoreContainerPosition = null;
402
+ // Host-side fullscreen pin: saved state we restore on `off`/Escape/destroy.
403
+ this.pinned = false;
404
+ this.frameStyleBeforeFs = null;
405
+ this.docOverflowBeforeFs = null;
406
+ this.bodyOverflowBeforeFs = null;
407
+ this.fsKeyHandler = null;
408
+ /** Latest height (px string) the Designer reported; re-applied after unpin. */
409
+ this.lastAutoHeight = "";
401
410
  this.handleMessage = (event) => {
402
411
  if (!this.frame || event.origin !== this.designerOrigin || event.source !== this.frame.contentWindow) return;
403
412
  if (!event.data || typeof event.data !== "object") return;
404
413
  const data = event.data;
414
+ if (data.type === "seatlayer.designer.resize") {
415
+ if (this.autoResizeEnabled() && typeof data.px === "number" && Number.isFinite(data.px) && data.px > 0) {
416
+ this.lastAutoHeight = `${Math.round(data.px)}px`;
417
+ if (!this.pinned) this.frame.style.height = this.lastAutoHeight;
418
+ }
419
+ return;
420
+ }
421
+ if (data.type === "seatlayer.designer.fullscreen") {
422
+ if (data.on === true) this.pinFullscreen();
423
+ else if (data.on === false) this.unpinFullscreen();
424
+ return;
425
+ }
405
426
  if (typeof data.type !== "string" || !TYPES.has(data.type)) return;
406
427
  const message = {
407
428
  type: data.type,
@@ -484,6 +505,7 @@ var EmbeddedDesigner = class {
484
505
  }
485
506
  destroy() {
486
507
  window.removeEventListener("message", this.handleMessage);
508
+ this.unpinFullscreen();
487
509
  this.clearTimeoutTimer();
488
510
  this.removeOverlay();
489
511
  this.restoreContainerStyle();
@@ -491,10 +513,68 @@ var EmbeddedDesigner = class {
491
513
  this.frame = null;
492
514
  this.designerOrigin = "";
493
515
  this.phase = "loading";
516
+ this.lastAutoHeight = "";
494
517
  }
495
518
  loadingStateEnabled() {
496
519
  return this.options.showLoadingState !== false;
497
520
  }
521
+ autoResizeEnabled() {
522
+ return this.options.autoResize !== false;
523
+ }
524
+ /**
525
+ * Pin the iframe over the host page as a viewport-filling overlay. We save the
526
+ * iframe's inline style and the document scroll state so `unpinFullscreen`
527
+ * restores everything exactly. Escape (host-side) also exits.
528
+ */
529
+ pinFullscreen() {
530
+ if (this.pinned || !this.frame) return;
531
+ this.pinned = true;
532
+ this.frameStyleBeforeFs = this.frame.getAttribute("style");
533
+ Object.assign(this.frame.style, {
534
+ position: "fixed",
535
+ inset: "0",
536
+ width: "100vw",
537
+ height: "100vh",
538
+ margin: "0",
539
+ border: "0",
540
+ zIndex: "2147483000",
541
+ background: "#101625"
542
+ });
543
+ const docEl = document.documentElement;
544
+ this.docOverflowBeforeFs = docEl.style.overflow;
545
+ docEl.style.overflow = "hidden";
546
+ if (document.body) {
547
+ this.bodyOverflowBeforeFs = document.body.style.overflow;
548
+ document.body.style.overflow = "hidden";
549
+ }
550
+ this.fsKeyHandler = (event) => {
551
+ if (event.key === "Escape") this.unpinFullscreen();
552
+ };
553
+ window.addEventListener("keydown", this.fsKeyHandler);
554
+ }
555
+ /** Undo `pinFullscreen`: restore the iframe style + scroll lock. Idempotent. */
556
+ unpinFullscreen() {
557
+ if (!this.pinned) return;
558
+ this.pinned = false;
559
+ if (this.frame) {
560
+ if (this.frameStyleBeforeFs === null) this.frame.removeAttribute("style");
561
+ else this.frame.setAttribute("style", this.frameStyleBeforeFs);
562
+ if (this.autoResizeEnabled() && this.lastAutoHeight) this.frame.style.height = this.lastAutoHeight;
563
+ }
564
+ this.frameStyleBeforeFs = null;
565
+ if (this.docOverflowBeforeFs !== null) {
566
+ document.documentElement.style.overflow = this.docOverflowBeforeFs;
567
+ this.docOverflowBeforeFs = null;
568
+ }
569
+ if (this.bodyOverflowBeforeFs !== null && document.body) {
570
+ document.body.style.overflow = this.bodyOverflowBeforeFs;
571
+ this.bodyOverflowBeforeFs = null;
572
+ }
573
+ if (this.fsKeyHandler) {
574
+ window.removeEventListener("keydown", this.fsKeyHandler);
575
+ this.fsKeyHandler = null;
576
+ }
577
+ }
498
578
  clearTimeoutTimer() {
499
579
  if (this.timeoutTimer !== null) {
500
580
  clearTimeout(this.timeoutTimer);
@@ -850,26 +930,43 @@ var CSS = `
850
930
  .sl-tray{flex:1;padding:10px 14px 14px;display:flex;flex-direction:column;gap:7px;min-height:0;overflow-y:auto;
851
931
  overscroll-behavior:contain;scrollbar-gutter:stable}
852
932
  .sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
853
- .sl-chip{position:relative;display:grid;grid-template-columns:24px minmax(0,1fr) auto 30px;align-items:center;gap:8px;
854
- min-height:53px;padding:7px 7px 7px 9px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm);
933
+ .sl-chip{position:relative;display:grid;grid-template-columns:minmax(0,1fr) 34px;align-items:stretch;
934
+ flex:none;min-height:53px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm);overflow:hidden;
855
935
  background:var(--sl-surface);font-size:13px;transform-origin:center;transition:border-color .15s,background .15s}
856
936
  .sl-chip:hover{border-color:color-mix(in srgb,var(--sl-accent) 38%,var(--sl-line))}
857
937
  .sl-chip.sl-enter{animation:slChipIn .38s cubic-bezier(.2,.8,.2,1) both}
858
938
  .sl-chip.sl-leave{pointer-events:none;animation:slChipOut .16s ease-in both}
859
939
  .sl-chip.sl-held{border-color:var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
860
940
  box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent)}
861
- .sl-ticket-state{width:23px;height:23px;border-radius:999px;display:flex;align-items:center;justify-content:center;
941
+ .sl-ticket-state{width:17px;height:17px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
862
942
  background:var(--sl-accent);color:var(--sl-accent-ink)}
863
943
  .sl-ticket-state.held{background:color-mix(in srgb,var(--sl-accent) 18%,var(--sl-surface));color:var(--sl-accent)}
864
- .sl-ticket-state svg{width:13px;height:13px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round;stroke-linejoin:round}
865
- .sl-chip-main{min-width:0}
866
- .sl-chip b{display:block;font-weight:800;min-width:max-content;white-space:nowrap}
867
- .sl-chip-sub{display:flex;align-items:center;gap:5px;min-width:0;margin-top:2px}
944
+ .sl-ticket-state svg{width:10px;height:10px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round}
945
+ .sl-chip-main{min-width:0;padding:8px 10px 8px 11px;display:flex;flex-direction:column;justify-content:center;gap:5px}
946
+ .sl-chip-id{display:flex;gap:12px;min-width:0}
947
+ .sl-chip-id .fld{min-width:0}
948
+ .sl-chip-id .fld.sec{flex:1}
949
+ .sl-chip-id .fld.mid{flex:none;text-align:center}
950
+ .sl-chip-eb{display:block;font-size:8px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);margin-bottom:1px}
951
+ .sl-chip-id .val{display:block;font-weight:600;font-size:13px;line-height:1.25;white-space:nowrap}
952
+ .sl-chip-id .fld.sec .val{overflow:hidden;text-overflow:ellipsis}
953
+ .sl-chip-sub{display:flex;align-items:center;gap:6px;min-width:0}
868
954
  .sl-chip .cat{color:var(--sl-muted);font-size:10.5px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
869
955
  .sl-chip .amt{font-weight:700;font-variant-numeric:tabular-nums;flex:none;white-space:nowrap}
870
- .sl-chip .rm{width:29px;height:29px;border-radius:8px;flex:none;display:flex;align-items:center;justify-content:center;color:var(--sl-muted)}
871
- .sl-chip .rm:hover,.sl-chip .rm:focus-visible{color:var(--sl-accent);background:color-mix(in srgb,var(--sl-accent) 10%,transparent)}
956
+ .sl-chip-rail{display:flex;flex-direction:column;border-left:1px solid var(--sl-line)}
957
+ .sl-chip .rm,.sl-chip .view{flex:1;min-height:26px;border-radius:0;display:flex;align-items:center;justify-content:center;
958
+ color:var(--sl-muted);transition:color .15s,background .15s}
959
+ .sl-chip .view{border-top:1px solid var(--sl-line)}
960
+ .sl-chip .rm:hover,.sl-chip .rm:focus-visible{color:#e5484d;background:color-mix(in srgb,#e5484d 9%,transparent)}
961
+ .sl-chip .view:hover,.sl-chip .view:focus-visible{color:var(--sl-text);background:color-mix(in srgb,var(--sl-accent) 10%,transparent)}
872
962
  .sl-chip .rm svg{width:11px;height:11px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round}
963
+ .sl-chip .view svg{width:13px;height:13px;stroke:currentColor;stroke-width:1.8;fill:none}
964
+ /* live-activity strip \u2014 narrates WS availability deltas (social proof + urgency) */
965
+ .sl-live{display:flex;align-items:center;gap:7px;margin:10px 14px 0;padding:7px 9px;flex:none;
966
+ border:1px solid var(--sl-line);border-radius:8px;background:color-mix(in srgb,var(--sl-accent) 4%,var(--sl-surface));
967
+ font-size:11px;color:var(--sl-muted)}
968
+ .sl-live .dot{width:6px;height:6px;border-radius:999px;background:#22a06b;box-shadow:0 0 6px rgba(34,160,107,.75);flex:none}
969
+ .sl-live span:last-child{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
873
970
 
874
971
  /* GA rows */
875
972
  .sl-ga{display:flex;align-items:center;gap:10px;padding:9px 11px;border:1px dashed var(--sl-line);border-radius:var(--sl-r-sm)}
@@ -945,6 +1042,8 @@ var CSS = `
945
1042
 
946
1043
  /* zoom column (flows within the bottom-right region) */
947
1044
  .sl-zoom{display:flex;flex-direction:column;gap:6px}
1045
+ /* CSS-fallback full screen (iOS Safari has no element fullscreen API) */
1046
+ .sl-picker.sl-fs{position:fixed;inset:0;z-index:2147483000;width:auto;height:auto;max-height:none;border-radius:0}
948
1047
  .sl-zoom button{width:36px;height:36px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
949
1048
  color:var(--sl-text);font-size:17px;font-weight:700;display:flex;align-items:center;justify-content:center;transition:border-color .15s}
950
1049
  .sl-zoom button:hover{border-color:var(--sl-muted)}
@@ -1003,6 +1102,39 @@ var CSS = `
1003
1102
  .sl-booked.on .sl-booked-title{animation-delay:.22s}
1004
1103
  .sl-booked.on .sl-booked-sub{animation-delay:.3s}
1005
1104
 
1105
+ /* sold-out overlay \u2014 every SEATED category's live availability is 0. Centered
1106
+ over the map; a stub (disabled) "Join waitlist" button, exactly like the page.
1107
+ Suppressed when GA areas exist (GA capacity isn't seat-counted). Clears live
1108
+ the moment WS frees a seat up. */
1109
+ .sl-soldout{position:absolute;inset:0;z-index:10;display:none;flex-direction:column;align-items:center;
1110
+ justify-content:center;text-align:center;gap:8px;padding:24px;
1111
+ background:color-mix(in srgb,var(--sl-bg) 82%,transparent);backdrop-filter:blur(4px)}
1112
+ .sl-soldout.on{display:flex}
1113
+ .sl-soldout-eyebrow{font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--sl-accent);font-weight:800}
1114
+ .sl-soldout-title{font-size:32px;font-weight:800;color:var(--sl-text);line-height:1.05}
1115
+ .sl-soldout-copy{max-width:360px;font-size:13px;color:var(--sl-muted);line-height:1.5}
1116
+ .sl-picker .sl-soldout-btn{margin-top:10px;min-height:40px;padding:10px 18px;border-radius:var(--sl-r-sm);
1117
+ background:var(--sl-surface);color:var(--sl-muted);border:1px solid var(--sl-line);font-weight:800;font-size:13px;
1118
+ cursor:not-allowed;opacity:.85}
1119
+
1120
+ /* sales-closed pill (header) \u2014 persistent read-only state when the event's sales
1121
+ window is closed at load or closes live mid-session. Neutral (not accent) so it
1122
+ reads as "unavailable", distinct from the accent hold pill next to it. */
1123
+ .sl-closed-pill{display:none;align-items:center;gap:6px;padding:6px 12px;border-radius:999px;flex:none;
1124
+ background:color-mix(in srgb,var(--sl-text) 12%,var(--sl-surface));color:var(--sl-text);
1125
+ font-weight:700;font-size:12px;white-space:nowrap}
1126
+ .sl-closed-pill.on{display:inline-flex}
1127
+ .sl-closed-pill svg{width:13px;height:13px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
1128
+
1129
+ /* "Powered by SeatLayer" attribution badge (side-panel foot) \u2014 the small gold
1130
+ rounded logo mark + wordmark. Hidden when the host opts out or the org's paid
1131
+ theme sets hideBadge. */
1132
+ .sl-powered{display:flex;align-items:center;justify-content:center;gap:6px;margin-top:10px;
1133
+ font-size:11px;letter-spacing:.03em;color:var(--sl-muted)}
1134
+ .sl-powered-mark{width:16px;height:16px;border-radius:4px;flex:none;display:flex;align-items:center;justify-content:center;
1135
+ background:var(--sl-accent);color:var(--sl-accent-ink)}
1136
+ .sl-powered-mark svg{width:11px;height:11px;fill:currentColor}
1137
+
1006
1138
  /* a11y filter chips (flow within the top-left region) */
1007
1139
  .sl-chips{display:flex;gap:6px;flex-wrap:wrap}
1008
1140
  .sl-chip-f{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:999px;font-size:12px;font-weight:700;
@@ -1041,8 +1173,30 @@ var CSS = `
1041
1173
  .sl-picker[data-layout="narrow"] .sl-confirm{left:50%!important;top:auto!important;bottom:14px;width:min(342px,calc(100% - 24px));
1042
1174
  transform:translateX(-50%);animation:slConfirmMobileIn .24s cubic-bezier(.2,.8,.2,1) both}
1043
1175
 
1176
+ /* hover preview \u2014 a COMPACT echo of the confirm card (deliberately smaller: it's
1177
+ a passing preview on hover, not the click/select action surface). Reuses the
1178
+ Section\xB7Row\xB7Seat identity grid so hover, confirm and the cart chip all share
1179
+ one visual language, just at three sizes. */
1180
+ .sl-tip{position:absolute;z-index:7;pointer-events:none;display:none;width:190px;overflow:hidden;
1181
+ background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:11px;
1182
+ box-shadow:0 12px 30px -14px rgba(0,0,0,.6)}
1183
+ .sl-tip-grid{display:grid;grid-template-columns:1.3fr .85fr .85fr;border-bottom:1px solid var(--sl-line)}
1184
+ .sl-tip-grid.one{grid-template-columns:1fr}
1185
+ .sl-tip-field{min-width:0;padding:6px 9px;border-right:1px solid var(--sl-line)}
1186
+ .sl-tip-field:last-child{border-right:0;text-align:center}
1187
+ .sl-tip-grid:not(.one) .sl-tip-field:nth-child(2){text-align:center}
1188
+ .sl-tip-key{display:block;font-size:7.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
1189
+ .sl-tip-val{display:block;margin-top:2px;color:var(--sl-text);font-size:13px;line-height:1.1;font-weight:750;
1190
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1191
+ .sl-tip-cat{display:flex;align-items:center;gap:7px;padding:6px 10px;font-size:11px;
1192
+ background:color-mix(in srgb,var(--sl-cat) 12%,var(--sl-surface))}
1193
+ .sl-tip-dot{width:8px;height:8px;border-radius:50%;flex:none}
1194
+ .sl-tip-name{color:var(--sl-muted);flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1195
+ .sl-tip-amt{margin-left:auto;font-weight:800;color:var(--sl-text);font-variant-numeric:tabular-nums;font-size:12px}
1196
+ .sl-tip-status{padding:5px 10px 7px;font-size:8.5px;letter-spacing:.09em;text-transform:uppercase;font-weight:700;color:var(--sl-muted)}
1197
+
1044
1198
  /* Best available is a first-class shortcut, not an anonymous utility row. */
1045
- .sl-ba{position:relative;overflow:hidden;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:7px;
1199
+ .sl-ba{position:relative;flex:none;overflow:hidden;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:7px;
1046
1200
  padding:13px;border:1px solid color-mix(in srgb,var(--sl-accent) 34%,var(--sl-line));border-radius:13px;
1047
1201
  background:linear-gradient(135deg,color-mix(in srgb,var(--sl-accent) 5%,var(--sl-surface)),color-mix(in srgb,var(--sl-accent) 11%,var(--sl-surface)))}
1048
1202
  .sl-ba::after{content:'\u2726';position:absolute;right:10px;top:3px;color:color-mix(in srgb,var(--sl-accent) 20%,transparent);font-size:42px;line-height:1}
@@ -1078,12 +1232,6 @@ var CSS = `
1078
1232
  /* per-seat ticket-tier select + view-from-seat button in tray chips */
1079
1233
  .sl-chip .tier{background:var(--sl-bg);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:6px;
1080
1234
  font:inherit;font-size:10px;padding:2px 4px;min-width:0;max-width:100%;cursor:pointer}
1081
- .sl-chip .view{width:20px;height:20px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
1082
- color:var(--sl-muted);opacity:.36;transition:color .15s,opacity .15s}
1083
- .sl-chip .view:hover{color:var(--sl-text)}
1084
- .sl-chip:hover .view,.sl-chip .view:focus-visible{opacity:1;color:var(--sl-text)}
1085
- .sl-chip .view svg{width:12px;height:12px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
1086
- @media(pointer:coarse){.sl-chip .view{opacity:.58}}
1087
1235
 
1088
1236
  /* arena: LOD rung pills (flow within the top-center region) */
1089
1237
  .sl-rungs{display:none;background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:999px;padding:3px}
@@ -1141,6 +1289,14 @@ var CSS = `
1141
1289
  .sl-seccard-hint{font-size:10.5px;color:var(--sl-muted)}
1142
1290
 
1143
1291
  /* view-from-seat button on the confirm popover */
1292
+ /* Eager sightline preview inside the confirm card */
1293
+ .sl-confirm-thumbwrap{position:relative;display:block;width:100%;height:74px;margin:0 0 8px;padding:0!important;
1294
+ border-radius:9px;overflow:hidden;border:1px solid var(--sl-line);cursor:pointer}
1295
+ .sl-confirm-thumb{display:block;width:100%;height:100%;object-fit:cover}
1296
+ .sl-confirm-thumb-badge{position:absolute;right:7px;top:7px;display:inline-flex;align-items:center;gap:5px;
1297
+ font-size:10px;font-weight:700;color:#fff;background:rgba(10,14,22,0.72);border-radius:12px;padding:4px 9px;backdrop-filter:blur(3px)}
1298
+ .sl-confirm-sight{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--sl-muted);margin-bottom:2px}
1299
+ .sl-confirm-sight span{color:#22a06b;font-weight:800}
1144
1300
  .sl-confirm-view{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
1145
1301
  color:var(--sl-text);font-weight:700;font-size:12px;display:flex;align-items:center;justify-content:center;gap:7px}
1146
1302
  .sl-confirm-view:hover{border-color:var(--sl-muted)}
@@ -1224,6 +1380,22 @@ function resolveTokens(chart, host) {
1224
1380
  "--sl-radius": `${host?.radius ?? 14}px`
1225
1381
  };
1226
1382
  }
1383
+ var CB_STORAGE_KEY = "seatmap.a11y.cb";
1384
+ function readStoredColorblind() {
1385
+ try {
1386
+ if (typeof window === "undefined") return null;
1387
+ const raw = window.localStorage.getItem(CB_STORAGE_KEY);
1388
+ return raw == null ? null : raw === "1";
1389
+ } catch {
1390
+ return null;
1391
+ }
1392
+ }
1393
+ function writeStoredColorblind(on) {
1394
+ try {
1395
+ window.localStorage.setItem(CB_STORAGE_KEY, on ? "1" : "0");
1396
+ } catch {
1397
+ }
1398
+ }
1227
1399
  var SeatPicker = class _SeatPicker {
1228
1400
  constructor(options) {
1229
1401
  this.root = null;
@@ -1256,11 +1428,17 @@ var SeatPicker = class _SeatPicker {
1256
1428
  this.confirmEl = null;
1257
1429
  this.confirmSeat = null;
1258
1430
  this.srEl = null;
1259
- this.a11yFilter = "all";
1260
1431
  this.baQty = 2;
1261
1432
  this.baCat = "";
1262
1433
  this.bestAvailableConfirm = false;
1263
1434
  this.releasingHold = false;
1435
+ /** Event sales window is closed (read-only load state / live close). */
1436
+ this.salesClosed = false;
1437
+ /** Every seated category's live availability is 0 (sold-out overlay is up). */
1438
+ this.soldOut = false;
1439
+ this.soldoutEl = null;
1440
+ /** Resolved colorblind-safe state — stored preference wins over the option. */
1441
+ this.cbSafe = false;
1264
1442
  // arena / multi-floor / seat-view chrome
1265
1443
  this.rungsEl = null;
1266
1444
  this.floorsEl = null;
@@ -1269,13 +1447,11 @@ var SeatPicker = class _SeatPicker {
1269
1447
  this.viewCleanup = null;
1270
1448
  this.allSeatsCache = null;
1271
1449
  // F3 minimap
1272
- this.miniEl = null;
1273
1450
  this.miniCanvas = null;
1274
1451
  this.miniBase = null;
1275
1452
  this.miniTf = null;
1276
1453
  // F4 price-band filter — active band's category keys (null = all prices)
1277
1454
  this.priceBandKeys = null;
1278
- this.priceFilterEl = null;
1279
1455
  /** Last surfaced section summary (re-rendered when the price band changes). */
1280
1456
  this.lastSection = null;
1281
1457
  /** Section card collapsed to its slim pill (seat-picking has begun). */
@@ -1295,6 +1471,13 @@ var SeatPicker = class _SeatPicker {
1295
1471
  this.ctaPhase = "idle";
1296
1472
  // narrow-layout chrome that docks into the sheet's Filters row on mobile
1297
1473
  this.a11yChipsEl = null;
1474
+ this.fsFallback = false;
1475
+ this.fsChangeHandler = null;
1476
+ this.fsEscHandler = null;
1477
+ /** True once we've asked the host page to pin us fullscreen (framed, no native). */
1478
+ this.framedFs = false;
1479
+ /** Last height (px) posted to a host frame; dedupes redundant reports. */
1480
+ this.lastPostedHeight = 0;
1298
1481
  this.cbEl = null;
1299
1482
  // modal plumbing (set by open())
1300
1483
  this.modalScrim = null;
@@ -1302,20 +1485,22 @@ var SeatPicker = class _SeatPicker {
1302
1485
  this.escHandler = null;
1303
1486
  /** Set by open(): closes the modal (scroll restore + destroy + onClose). */
1304
1487
  this.closeModal = null;
1488
+ this.lastCatAvail = null;
1305
1489
  if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
1306
1490
  if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
1307
1491
  if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
1308
1492
  this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
1309
1493
  this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
1310
- this.api = new PubApi(this.apiBase);
1494
+ this.api = options.transport ?? new PubApi(this.apiBase);
1311
1495
  this.maxTickets = Math.max(1, Math.floor(options.maxSelection ?? DEFAULT_MAX_SELECTION2));
1496
+ this.cbSafe = readStoredColorblind() ?? !!options.colorblindSafe;
1312
1497
  this.controller = new import_core2.PickerController({
1313
1498
  transport: this.api,
1314
1499
  eventKey: options.event,
1315
1500
  maxSelection: this.maxTickets,
1316
1501
  currency: options.currency,
1317
1502
  flashOnLiveChange: true,
1318
- colorblindSafe: options.colorblindSafe,
1503
+ colorblindSafe: this.cbSafe,
1319
1504
  onSelectionChange: () => {
1320
1505
  this.syncTray();
1321
1506
  if (this.committedSelection().length) this.collapseSectionCard();
@@ -1341,6 +1526,11 @@ var SeatPicker = class _SeatPicker {
1341
1526
  },
1342
1527
  confirmSelection: this.opts.confirmSelection,
1343
1528
  onSelect: (seat) => {
1529
+ if (this.salesClosed) {
1530
+ this.controller.deselect([seat.id]);
1531
+ this.toast(this.tf("picker.salesClosedToast", "Sales are closed for this event."), "warning");
1532
+ return;
1533
+ }
1344
1534
  this.flashPickedSeat(seat.id);
1345
1535
  if (this.opts.confirmSelection) this.showConfirm(seat);
1346
1536
  },
@@ -1363,9 +1553,142 @@ var SeatPicker = class _SeatPicker {
1363
1553
  onHint: (m) => {
1364
1554
  if (m) this.toast(m);
1365
1555
  },
1556
+ // Server declared the event closed mid-session (409 event_closed) — keep
1557
+ // the toast (raised by handleCta), and add the persistent read-only state.
1558
+ onSalesClosed: () => this.setSalesClosed(true),
1366
1559
  onError: (err) => this.opts.onError?.(err)
1367
1560
  });
1368
1561
  }
1562
+ /**
1563
+ * Eager sightline preview for the confirm card: a cheap generated forward
1564
+ * view (or the organizer's real photo) plus a "Nm to stage · clear
1565
+ * sightline" line — the premium at-a-glance moment; click opens the 360.
1566
+ */
1567
+ confirmThumbHtml(seat) {
1568
+ const doc = this.controller.doc;
1569
+ if (!doc) return "";
1570
+ let url = seat.viewUrl ?? "";
1571
+ let distance = null;
1572
+ if (!url) {
1573
+ try {
1574
+ const thumb = (0, import_core2.generateSeatThumb)(seat, doc.focalPoint);
1575
+ url = thumb.url;
1576
+ distance = thumb.distanceM ?? null;
1577
+ } catch {
1578
+ return "";
1579
+ }
1580
+ }
1581
+ const sight = distance != null ? `${distance}${this.tf("picker.sightline", "m to stage \xB7 clear sightline")}` : this.tf("picker.sightlineClear", "Clear sightline");
1582
+ return `<button type="button" class="sl-confirm-view sl-confirm-thumbwrap" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: seat.label })}"><img class="sl-confirm-thumb" src="${url}" alt="" /><span class="sl-confirm-thumb-badge">\u{1F52D} ${this.tf("picker.viewFromHere", "View from here")}</span></button><div class="sl-confirm-sight"><span aria-hidden="true">\u2713</span>${sight}</div>`;
1583
+ }
1584
+ /** True when the picker is rendered inside an iframe (snippet embed at /e/:key). */
1585
+ isFramed() {
1586
+ return typeof window !== "undefined" && window.parent !== window;
1587
+ }
1588
+ /**
1589
+ * Post a widget→host message when framed. targetOrigin is '*' because the
1590
+ * payload carries nothing sensitive (a height number / a fullscreen flag);
1591
+ * hosts verify `event.origin` on their side (see `attachPickerFrame`).
1592
+ */
1593
+ postToHost(message) {
1594
+ if (!this.isFramed()) return;
1595
+ try {
1596
+ window.parent.postMessage(message, "*");
1597
+ } catch {
1598
+ }
1599
+ }
1600
+ /**
1601
+ * Height (px) to advertise to a host frame.
1602
+ *
1603
+ * The picker fills whatever box it's given: `.sl-picker` is `height:100%;
1604
+ * overflow:hidden`, and the /e/:key shell mounts it `position:fixed; inset:0`.
1605
+ * So it has no intrinsic *document* height to read — `scrollHeight` just
1606
+ * collapses to the current viewport, which for a framed embed would echo the
1607
+ * host's own iframe height straight back (a circular value). We therefore
1608
+ * report a width-driven *desired* height: a pleasant landscape box on desktop,
1609
+ * taller on narrow widths where the bottom sheet needs room, clamped to the
1610
+ * widget's `min-height` of 420. Width is host-controlled and never moves in
1611
+ * response to the height we report, so this cannot feedback-loop.
1612
+ */
1613
+ measureFramedHeight() {
1614
+ const root = this.root;
1615
+ if (!root) return 0;
1616
+ const width = root.clientWidth || (typeof window !== "undefined" ? window.innerWidth : 0) || 0;
1617
+ if (width <= 0) return 0;
1618
+ const ratio = width < 640 ? 1.2 : 0.62;
1619
+ return Math.max(420, Math.round(width * ratio));
1620
+ }
1621
+ /** Post `seatlayer:height` to the host when framed and the value changed. */
1622
+ reportFramedHeight() {
1623
+ if (!this.isFramed()) return;
1624
+ const px = this.measureFramedHeight();
1625
+ if (px <= 0 || px === this.lastPostedHeight) return;
1626
+ this.lastPostedHeight = px;
1627
+ this.postToHost({ type: "seatlayer:height", px });
1628
+ }
1629
+ /** Full screen via the native API, falling back to a fixed-position overlay (iOS Safari). */
1630
+ toggleFullscreen() {
1631
+ const root = this.root;
1632
+ if (!root) return;
1633
+ const active = !!document.fullscreenElement || this.fsFallback || this.framedFs;
1634
+ if (!active) {
1635
+ if (root.requestFullscreen) {
1636
+ root.requestFullscreen().catch(() => this.enterFsFallback());
1637
+ } else {
1638
+ this.enterFsFallback();
1639
+ }
1640
+ } else if (document.fullscreenElement) {
1641
+ void document.exitFullscreen().catch(() => {
1642
+ });
1643
+ } else if (this.framedFs) {
1644
+ this.setFramedFs(false);
1645
+ } else {
1646
+ this.setFsFallback(false);
1647
+ }
1648
+ }
1649
+ /**
1650
+ * Native element-fullscreen was unavailable or rejected. When framed, a CSS
1651
+ * `.sl-fs` overlay can't escape the iframe, so we ask the host page to pin us
1652
+ * (`seatlayer:fullscreen`). Otherwise (iOS Safari, same document) fall back to
1653
+ * the `.sl-fs` overlay as before.
1654
+ */
1655
+ enterFsFallback() {
1656
+ if (this.isFramed()) this.setFramedFs(true);
1657
+ else this.setFsFallback(true);
1658
+ }
1659
+ /** Toggle host-driven (framed) fullscreen: post the flag + own the Esc key. */
1660
+ setFramedFs(on) {
1661
+ if (this.framedFs === on) return;
1662
+ this.framedFs = on;
1663
+ this.els.zfs?.setAttribute("aria-pressed", String(on || !!document.fullscreenElement));
1664
+ this.postToHost({ type: "seatlayer:fullscreen", on });
1665
+ if (on && !this.fsEscHandler) {
1666
+ this.fsEscHandler = (e) => {
1667
+ if (e.key === "Escape" && !document.fullscreenElement) this.setFramedFs(false);
1668
+ };
1669
+ window.addEventListener("keydown", this.fsEscHandler);
1670
+ } else if (!on && this.fsEscHandler) {
1671
+ window.removeEventListener("keydown", this.fsEscHandler);
1672
+ this.fsEscHandler = null;
1673
+ }
1674
+ requestAnimationFrame(() => this.controller.zoomToFit());
1675
+ }
1676
+ setFsFallback(on) {
1677
+ if (this.fsFallback === on) return;
1678
+ this.fsFallback = on;
1679
+ this.root?.classList.toggle("sl-fs", on);
1680
+ this.els.zfs?.setAttribute("aria-pressed", String(on || !!document.fullscreenElement));
1681
+ if (on && !this.fsEscHandler) {
1682
+ this.fsEscHandler = (e) => {
1683
+ if (e.key === "Escape" && !document.fullscreenElement) this.setFsFallback(false);
1684
+ };
1685
+ window.addEventListener("keydown", this.fsEscHandler);
1686
+ } else if (!on && this.fsEscHandler) {
1687
+ window.removeEventListener("keydown", this.fsEscHandler);
1688
+ this.fsEscHandler = null;
1689
+ }
1690
+ requestAnimationFrame(() => this.controller.zoomToFit());
1691
+ }
1369
1692
  /**
1370
1693
  * Close the picker. In modal mode (SeatPicker.open()) this dismisses the
1371
1694
  * modal exactly like ESC/scrim/✕ — restores page scroll and fires onClose.
@@ -1460,6 +1783,10 @@ var SeatPicker = class _SeatPicker {
1460
1783
  <div class="sl-head-meta" data-ref="meta"></div>
1461
1784
  </div>
1462
1785
  <span class="sl-hold-pill" data-ref="hold"></span>
1786
+ <span class="sl-closed-pill" data-ref="closedPill" role="status">
1787
+ <svg viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
1788
+ <span data-ref="closedPillText"></span>
1789
+ </span>
1463
1790
  <button type="button" class="sl-close" data-ref="close" aria-label="Close">
1464
1791
  <svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
1465
1792
  </button>
@@ -1473,6 +1800,9 @@ var SeatPicker = class _SeatPicker {
1473
1800
  <button type="button" aria-label="Fit to screen" data-ref="zfit">
1474
1801
  <svg viewBox="0 0 24 24"><path d="M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M8 21H5a2 2 0 0 1-2-2v-3M16 21h3a2 2 0 0 0 2-2v-3"/></svg>
1475
1802
  </button>
1803
+ <button type="button" aria-label="Full screen" aria-pressed="false" data-ref="zfs">
1804
+ <svg viewBox="0 0 24 24"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
1805
+ </button>
1476
1806
  </div>
1477
1807
  <div class="sl-boot" data-ref="boot"><span class="sl-boot-spin"></span>Loading seat map\u2026</div>
1478
1808
  <div class="sl-toast" data-ref="toast" role="status" aria-live="polite"></div>
@@ -1491,6 +1821,7 @@ var SeatPicker = class _SeatPicker {
1491
1821
  <div class="sl-filters" data-ref="filters"></div>
1492
1822
  <div class="sl-sec sl-prices-sec" data-ref="pricesSec"><span>Ticket prices</span></div>
1493
1823
  <div class="sl-prices" data-ref="prices"></div>
1824
+ <div class="sl-live" data-ref="live" role="status" aria-live="polite"><span class="dot" aria-hidden="true"></span><span data-ref="liveText">Live availability \u2014 seats update in real time</span></div>
1494
1825
  <div class="sl-sec sl-seats-sec"><span>Your seats</span><span class="sl-seat-summary" data-ref="seatSummary"></span></div>
1495
1826
  <div class="sl-tray" data-ref="tray"></div>
1496
1827
  <div class="sl-foot" data-ref="foot">
@@ -1511,6 +1842,7 @@ var SeatPicker = class _SeatPicker {
1511
1842
  const applyLayout = () => {
1512
1843
  const w = root.clientWidth;
1513
1844
  if (w <= 0) return;
1845
+ this.reportFramedHeight();
1514
1846
  const next = w < 640 ? "narrow" : "wide";
1515
1847
  if (root.dataset.layout === next) return;
1516
1848
  root.dataset.layout = next;
@@ -1524,6 +1856,13 @@ var SeatPicker = class _SeatPicker {
1524
1856
  this.els.zin.addEventListener("click", () => this.controller.zoomIn());
1525
1857
  this.els.zout.addEventListener("click", () => this.controller.zoomOut());
1526
1858
  this.els.zfit.addEventListener("click", () => this.controller.zoomToFit());
1859
+ this.els.zfs.addEventListener("click", () => this.toggleFullscreen());
1860
+ this.fsChangeHandler = () => {
1861
+ if (!document.fullscreenElement) this.setFsFallback(false);
1862
+ this.els.zfs?.setAttribute("aria-pressed", String(!!document.fullscreenElement || this.fsFallback || this.framedFs));
1863
+ requestAnimationFrame(() => this.controller.zoomToFit());
1864
+ };
1865
+ document.addEventListener("fullscreenchange", this.fsChangeHandler);
1527
1866
  const head = this.els.sheetHead;
1528
1867
  if (head) {
1529
1868
  const toggle = this.els.sheetToggle;
@@ -1567,7 +1906,7 @@ var SeatPicker = class _SeatPicker {
1567
1906
  }
1568
1907
  this.tipEl = document.createElement("div");
1569
1908
  this.tipEl.setAttribute("role", "tooltip");
1570
- this.tipEl.style.cssText = "position:absolute;z-index:7;pointer-events:none;display:none;max-width:240px;background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:10px;padding:9px 12px;font-size:12px;line-height:1.45;";
1909
+ this.tipEl.className = "sl-tip";
1571
1910
  this.els.map.appendChild(this.tipEl);
1572
1911
  this.els.map.addEventListener("mousemove", (e) => {
1573
1912
  const r = this.els.map.getBoundingClientRect();
@@ -1592,6 +1931,7 @@ var SeatPicker = class _SeatPicker {
1592
1931
  return this;
1593
1932
  }
1594
1933
  this.els.boot.remove();
1934
+ this.salesClosed = !!info.salesClosed;
1595
1935
  this.buildRegions();
1596
1936
  this.regions["bottom-right"].appendChild(this.els.zoom);
1597
1937
  this.regions["bottom-center"].appendChild(this.els.toast);
@@ -1611,6 +1951,7 @@ var SeatPicker = class _SeatPicker {
1611
1951
  this.els.name.textContent = info.eventName ?? "";
1612
1952
  const when = info.startsAt ? new Date(info.startsAt).toLocaleString(this.opts.locale, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "";
1613
1953
  this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
1954
+ this.buildBadge(chartTheme);
1614
1955
  const present = /* @__PURE__ */ new Set();
1615
1956
  if (this.controller.doc) {
1616
1957
  for (const seat of (0, import_core2.expandChart)(this.controller.doc)) {
@@ -1626,12 +1967,29 @@ var SeatPicker = class _SeatPicker {
1626
1967
  chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
1627
1968
  this.regions["top-left"].appendChild(chips);
1628
1969
  this.a11yChipsEl = chips;
1970
+ const active = /* @__PURE__ */ new Set();
1971
+ const syncChips = () => {
1972
+ chips.querySelectorAll("button").forEach((b) => {
1973
+ const f = b.dataset.f;
1974
+ const on = f === "all" ? active.size === 0 : active.has(f);
1975
+ b.classList.toggle("on", on);
1976
+ b.setAttribute("aria-pressed", String(on));
1977
+ });
1978
+ const filter = active.size ? [...active] : null;
1979
+ this.controller.setAccessibilityFilter(filter);
1980
+ if (filter && this.rungsEl && this.controller.getRung() !== "seats") {
1981
+ this.controller.setRung("seats");
1982
+ this.collapseSectionCard();
1983
+ this.syncRung();
1984
+ }
1985
+ };
1629
1986
  chips.querySelectorAll("button").forEach((btn) => {
1630
1987
  btn.addEventListener("click", () => {
1631
1988
  const f = btn.dataset.f;
1632
- this.a11yFilter = f;
1633
- chips.querySelectorAll("button").forEach((b) => b.classList.toggle("on", b === btn));
1634
- this.controller.setAccessibilityFilter(f === "all" ? null : [f]);
1989
+ if (f === "all") active.clear();
1990
+ else if (active.has(f)) active.delete(f);
1991
+ else active.add(f);
1992
+ syncChips();
1635
1993
  });
1636
1994
  });
1637
1995
  }
@@ -1640,14 +1998,14 @@ var SeatPicker = class _SeatPicker {
1640
1998
  cb.className = "sl-cbbtn";
1641
1999
  this.cbEl = cb;
1642
2000
  cb.setAttribute("aria-label", "Toggle colorblind-friendly colors");
1643
- cb.setAttribute("aria-pressed", String(!!this.opts.colorblindSafe));
2001
+ cb.setAttribute("aria-pressed", String(this.cbSafe));
1644
2002
  cb.innerHTML = '<svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>';
1645
2003
  this.els.zfit.parentElement.appendChild(cb);
1646
- let cbOn = !!this.opts.colorblindSafe;
1647
2004
  cb.addEventListener("click", () => {
1648
- cbOn = !cbOn;
1649
- cb.setAttribute("aria-pressed", String(cbOn));
1650
- this.controller.setColorblindSafe(cbOn);
2005
+ this.cbSafe = !this.cbSafe;
2006
+ cb.setAttribute("aria-pressed", String(this.cbSafe));
2007
+ this.controller.setColorblindSafe(this.cbSafe);
2008
+ writeStoredColorblind(this.cbSafe);
1651
2009
  });
1652
2010
  this.srEl = document.createElement("div");
1653
2011
  this.srEl.className = "sl-sr";
@@ -1658,9 +2016,11 @@ var SeatPicker = class _SeatPicker {
1658
2016
  this.buildPriceFilter();
1659
2017
  this.buildExtendPrompt();
1660
2018
  this.buildBookedOverlay();
2019
+ this.buildSoldoutOverlay();
1661
2020
  this.dockLayoutChrome();
1662
2021
  await this.restoreRememberedHold();
1663
2022
  if (this.destroyed) return this;
2023
+ if (this.salesClosed) this.applySalesClosed();
1664
2024
  this.syncPrices();
1665
2025
  this.syncTray();
1666
2026
  return this;
@@ -1712,6 +2072,85 @@ var SeatPicker = class _SeatPicker {
1712
2072
  this.bookedEl = el;
1713
2073
  this.els.bookedSub = el.querySelector('[data-ref="bookedSub"]');
1714
2074
  }
2075
+ /**
2076
+ * Localized string with a literal fallback. `t()` returns the key itself for
2077
+ * unknown keys, so this collapses that to `fallback` — while still honoring a
2078
+ * host `messages` override (which makes `t()` return the override, not the key).
2079
+ */
2080
+ tf(key, fallback) {
2081
+ const v = (0, import_core2.t)(key);
2082
+ return v === key ? fallback : v;
2083
+ }
2084
+ /** Sold-out overlay — centered over the map, disabled waitlist stub (Gap 2). */
2085
+ buildSoldoutOverlay() {
2086
+ if (!this.els.map) return;
2087
+ const el = document.createElement("div");
2088
+ el.className = "sl-soldout";
2089
+ el.setAttribute("role", "status");
2090
+ const name = (this.controller.doc?.theme?.brandName ?? this.opts.theme?.brandName ?? this.els.name?.textContent ?? this.tf("picker.soldOutEyebrow", "This event")).toUpperCase();
2091
+ el.innerHTML = `<div class="sl-soldout-eyebrow">${name}</div><div class="sl-soldout-title">${this.tf("picker.soldOutTitle", "Sold out")}</div><p class="sl-soldout-copy">${this.tf("picker.soldOutCopy", "Every seat is gone. Join the waitlist and we\u2019ll email you if seats are released.")}</p><button type="button" class="sl-soldout-btn" disabled>${this.tf("picker.waitlist", "Join waitlist")}</button>`;
2092
+ this.els.map.appendChild(el);
2093
+ this.soldoutEl = el;
2094
+ }
2095
+ /**
2096
+ * Recompute the sold-out state on every price/availability sync. Sold-out ⇔
2097
+ * every SEATED category's live free count is 0. Suppressed when the chart has
2098
+ * GA areas (GA capacity isn't per-seat, so seated counts would read 0 and
2099
+ * falsely block standing room) — mirrors the public page. Clears live when WS
2100
+ * frees a seat up.
2101
+ */
2102
+ syncSoldout(categories, left) {
2103
+ const hasGA = this.controller.getGAAreas().length > 0;
2104
+ const soldOut = this.isSoldOut(categories, left, hasGA);
2105
+ if (soldOut === this.soldOut) return;
2106
+ this.soldOut = soldOut;
2107
+ this.soldoutEl?.classList.toggle("on", soldOut);
2108
+ }
2109
+ /**
2110
+ * Pure sold-out predicate: every SEATED category's free count is 0, there is at
2111
+ * least one seated category, and there are no GA areas (GA capacity isn't
2112
+ * per-seat, so seated counts read 0 and would falsely block standing room).
2113
+ * `left` is seeded implicitly — a missing key means a fully-booked tier (0 free).
2114
+ */
2115
+ isSoldOut(categories, left, hasGA) {
2116
+ return !hasGA && categories.length > 0 && categories.every((c) => (left[c.key] ?? 0) === 0);
2117
+ }
2118
+ /**
2119
+ * Sales-closed read-only state (Gap 3): persistent header pill, disabled CTA
2120
+ * with a closed label, and frozen best-available / GA controls. `setSalesClosed`
2121
+ * is the reactive entry (live 409 event_closed); `applySalesClosed` is the
2122
+ * idempotent DOM apply used at load and on transition.
2123
+ */
2124
+ setSalesClosed(closed) {
2125
+ if (this.salesClosed === closed) return;
2126
+ this.salesClosed = closed;
2127
+ this.applySalesClosed();
2128
+ }
2129
+ applySalesClosed() {
2130
+ const pill = this.els.closedPill;
2131
+ if (pill) {
2132
+ pill.classList.toggle("on", this.salesClosed);
2133
+ const text = this.els.closedPillText ?? pill;
2134
+ text.textContent = this.tf("picker.salesClosedPill", "Sales are closed");
2135
+ }
2136
+ this.root?.setAttribute("data-sales-closed", String(this.salesClosed));
2137
+ this.syncCta();
2138
+ this.syncTray();
2139
+ }
2140
+ /** The badge is hidden when the host opts out OR the org's theme sets hideBadge. */
2141
+ badgeHidden(chartTheme) {
2142
+ return !!(this.opts.hideBadge || chartTheme?.hideBadge);
2143
+ }
2144
+ /** Attribution badge in the side-panel foot (Gap 7). Hidden per host/theme. */
2145
+ buildBadge(chartTheme) {
2146
+ if (this.badgeHidden(chartTheme)) return;
2147
+ const foot = this.els.foot;
2148
+ if (!foot) return;
2149
+ const el = document.createElement("div");
2150
+ el.className = "sl-powered";
2151
+ el.innerHTML = `<span class="sl-powered-mark" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 15c0-1.1.9-2 2-2h12a2 2 0 0 1 2 2v3h-3v-2H7v2H4v-3Z"/><rect x="7" y="7" width="10" height="5" rx="1.6"/></svg></span><span>${this.tf("picker.poweredBy", "Powered by SeatLayer")}</span>`;
2152
+ foot.appendChild(el);
2153
+ }
1715
2154
  // ---- Feature 6: chrome anchor regions -------------------------------------
1716
2155
  /**
1717
2156
  * Create the positioned flex containers that own every persistent map overlay.
@@ -1824,13 +2263,18 @@ var SeatPicker = class _SeatPicker {
1824
2263
  heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
1825
2264
  }
1826
2265
  return gaAreas.reduce(
1827
- (sum, area) => sum + area.price * Math.max(0, (this.gaQty.get(area.id) ?? 0) - (heldGA.get(area.id) ?? 0)),
2266
+ (sum, area) => sum + this.paidPrice(area.categoryKey, null, area.price) * Math.max(0, (this.gaQty.get(area.id) ?? 0) - (heldGA.get(area.id) ?? 0)),
1828
2267
  0
1829
2268
  );
1830
2269
  }
1831
2270
  syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
1832
2271
  const cta = this.els.cta;
1833
2272
  if (!cta) return;
2273
+ if (this.salesClosed) {
2274
+ cta.disabled = true;
2275
+ cta.textContent = this.tf("picker.salesClosedCta", "Sales closed");
2276
+ return;
2277
+ }
1834
2278
  if (this.confirmSeat) {
1835
2279
  cta.disabled = true;
1836
2280
  cta.textContent = "Confirm or cancel this seat";
@@ -1966,7 +2410,6 @@ var SeatPicker = class _SeatPicker {
1966
2410
  canvas.style.height = `${h}px`;
1967
2411
  wrap.appendChild(canvas);
1968
2412
  (this.regions["bottom-left"] ?? this.els.map).appendChild(wrap);
1969
- this.miniEl = wrap;
1970
2413
  this.miniCanvas = canvas;
1971
2414
  const scale = Math.min((w - PAD * 2) / Math.max(1, b.width), (h - PAD * 2) / Math.max(1, b.height)) * dpr;
1972
2415
  const offX = (w * dpr - b.width * scale) / 2 - b.x * scale;
@@ -2079,9 +2522,11 @@ var SeatPicker = class _SeatPicker {
2079
2522
  this.controller.overview();
2080
2523
  }
2081
2524
  // ---- F4 price-band filter -------------------------------------------------
2082
- /** Effective price of a category (first tier when tiered, else base price). */
2525
+ /** Effective display price of a category: host pricing override → first tier base. */
2083
2526
  catPrice(c) {
2084
- return c.tiers?.length ? c.tiers[0].price : c.price;
2527
+ const chart = c.tiers?.length ? c.tiers[0].price : c.price;
2528
+ if (chart === void 0 || !c.key) return chart;
2529
+ return this.paidPrice(c.key, c.tiers?.[0]?.id ?? null, chart);
2085
2530
  }
2086
2531
  /** Derive price bands: one chip per distinct price (≤5), else quantile ranges. */
2087
2532
  priceBands() {
@@ -2126,7 +2571,6 @@ var SeatPicker = class _SeatPicker {
2126
2571
  select.setAttribute("aria-label", "Filter and focus seats by price");
2127
2572
  select.innerHTML = `<option value="all">All prices</option>` + bands.map((band) => `<option value="${band.id}">${band.label}</option>`).join("");
2128
2573
  this.els.pricesSec.appendChild(select);
2129
- this.priceFilterEl = select;
2130
2574
  select.addEventListener("change", () => {
2131
2575
  const band = bands.find((candidate) => candidate.id === select.value);
2132
2576
  const keys = band?.keys ?? null;
@@ -2230,7 +2674,10 @@ var SeatPicker = class _SeatPicker {
2230
2674
  renderSectionCard(summary) {
2231
2675
  if (!this.els.map) return;
2232
2676
  this.secCardEl?.remove();
2233
- const priceLabel = summary.priceMin === summary.priceMax ? this.money(summary.priceMin) : `${this.money(summary.priceMin)}\u2013${this.money(summary.priceMax)}`;
2677
+ const paid = summary.categories.length ? summary.categories.map((c) => this.paidPrice(c.key, null, c.price)) : [summary.priceMin, summary.priceMax];
2678
+ const paidMin = Math.min(...paid);
2679
+ const paidMax = Math.max(...paid);
2680
+ const priceLabel = paidMin === paidMax ? this.money(paidMin) : `${this.money(paidMin)}\u2013${this.money(paidMax)}`;
2234
2681
  const leftLabel = (0, import_core2.tCount)("picker.seatsLeftInSection", summary.seatsLeft);
2235
2682
  const xBtn = `<button type="button" class="sl-seccard-x" aria-label="${(0, import_core2.t)("picker.closeSectionSummary")}">\u2715</button>`;
2236
2683
  const card = document.createElement("div");
@@ -2260,7 +2707,7 @@ var SeatPicker = class _SeatPicker {
2260
2707
  card.setAttribute("aria-label", (0, import_core2.t)("picker.sectionSummaryAria", { label: summary.label }));
2261
2708
  const mix = summary.categories.map((c) => {
2262
2709
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
2263
- return `<span class="sl-seccard-mix-item${dim ? " sl-dim" : ""}"><span class="sl-seccard-mix-dot" style="background:${c.color}"></span>${c.label} <span class="sl-seccard-mix-price">${this.money(c.price)}</span></span>`;
2710
+ return `<span class="sl-seccard-mix-item${dim ? " sl-dim" : ""}"><span class="sl-seccard-mix-dot" style="background:${c.color}"></span>${c.label} <span class="sl-seccard-mix-price">${this.money(this.paidPrice(c.key, null, c.price))}</span></span>`;
2264
2711
  }).join("");
2265
2712
  card.innerHTML = `<div class="sl-seccard-head"><span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span>` + (summary.categories.length ? `<span class="sl-seccard-price">${priceLabel}</span>` : "") + xBtn + `</div><div class="sl-seccard-zone">${summary.zoneLabel ? `${summary.zoneLabel} \xB7 ` : ""}<span class="sl-seccard-left">${leftLabel}</span></div>` + (mix ? `<div class="sl-seccard-mix">${mix}</div>` : "") + `<div class="sl-seccard-foot"><button type="button" class="sl-seccard-overview">\u2190 ${(0, import_core2.t)("picker.overview")}</button><span class="sl-seccard-hint">${(0, import_core2.t)("picker.tapSeatHint")}</span></div>`;
2266
2713
  card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
@@ -2327,7 +2774,7 @@ var SeatPicker = class _SeatPicker {
2327
2774
  const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
2328
2775
  const status = this.controller.getStatus(seat.id) ?? "free";
2329
2776
  const statusText = status === "free" ? "available" : status === "held" ? "on hold" : "taken";
2330
- const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
2777
+ const price = cat ? this.catPrice(cat) : void 0;
2331
2778
  this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
2332
2779
  }
2333
2780
  // ---- seat candidate confirmation ------------------------------------------
@@ -2342,7 +2789,8 @@ var SeatPicker = class _SeatPicker {
2342
2789
  if (this.tipEl) this.tipEl.style.display = "none";
2343
2790
  const details = this.controller.seatDetails(seat.id);
2344
2791
  const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
2345
- const price = details?.price ?? (cat?.tiers?.length ? cat.tiers[0].price : cat?.price);
2792
+ const chartPrice = details?.price ?? (cat?.tiers?.length ? cat.tiers[0].price : cat?.price);
2793
+ const price = chartPrice != null ? this.paidPrice(seat.categoryKey, details?.tierId ?? cat?.tiers?.[0]?.id ?? null, chartPrice) : void 0;
2346
2794
  const safe = (value) => String(value ?? "\u2014").replace(/[&<>"]/g, (char) => ({
2347
2795
  "&": "&amp;",
2348
2796
  "<": "&lt;",
@@ -2355,7 +2803,7 @@ var SeatPicker = class _SeatPicker {
2355
2803
  el.setAttribute("aria-modal", "true");
2356
2804
  el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
2357
2805
  el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
2358
- el.innerHTML = `<div class="sl-confirm-grid"><div class="sl-confirm-field"><span class="sl-confirm-key">Section</span><span class="sl-confirm-value">${safe(details?.sectionLabel)}</span></div><div class="sl-confirm-field"><span class="sl-confirm-key">Row</span><span class="sl-confirm-value">${safe(details?.rowLabel)}</span></div><div class="sl-confirm-field"><span class="sl-confirm-key">Seat</span><span class="sl-confirm-value">${safe(details?.seatNumber ?? seat.label)}</span></div></div><div class="sl-confirm-cat"><span class="sl-dot" style="background:${cat?.color ?? "#6e7bff"}"></span><span class="sl-confirm-cat-name">${safe(details?.categoryLabel ?? cat?.label ?? seat.categoryKey)}</span>` + (price != null ? `<span class="sl-confirm-price">${this.money(price)}</span>` : "") + `</div><div class="sl-confirm-body">` + (this.seatViewEnabled() ? `<button type="button" class="sl-confirm-view"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>${(0, import_core2.t)("picker.open360")}</button>` : "") + `<div class="sl-confirm-row"><button type="button" class="sl-confirm-cancel">Cancel</button><button type="button" class="sl-confirm-add"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4 4L19 7"/></svg>Select</button></div></div>`;
2806
+ el.innerHTML = `<div class="sl-confirm-grid"><div class="sl-confirm-field"><span class="sl-confirm-key">Section</span><span class="sl-confirm-value">${safe(details?.sectionLabel)}</span></div><div class="sl-confirm-field"><span class="sl-confirm-key">Row</span><span class="sl-confirm-value">${safe(this.rowShort(details))}</span></div><div class="sl-confirm-field"><span class="sl-confirm-key">Seat</span><span class="sl-confirm-value">${safe(details?.seatNumber ?? seat.label)}</span></div></div><div class="sl-confirm-cat"><span class="sl-dot" style="background:${cat?.color ?? "#6e7bff"}"></span><span class="sl-confirm-cat-name">${safe(details?.categoryLabel ?? cat?.label ?? seat.categoryKey)}</span>` + (price != null ? `<span class="sl-confirm-price">${this.money(price)}</span>` : "") + `</div><div class="sl-confirm-body">` + (this.seatViewEnabled() ? this.confirmThumbHtml(seat) : "") + `<div class="sl-confirm-row"><button type="button" class="sl-confirm-cancel">Cancel</button><button type="button" class="sl-confirm-add"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4 4L19 7"/></svg>Select</button></div></div>`;
2359
2807
  this.els.map.appendChild(el);
2360
2808
  this.confirmEl = el;
2361
2809
  this.reanchorConfirm();
@@ -2520,18 +2968,35 @@ var SeatPicker = class _SeatPicker {
2520
2968
  }
2521
2969
  // ---- chrome sync ----------------------------------------------------------
2522
2970
  money(n) {
2971
+ const formatter = this.opts.pricing?.formatter;
2972
+ if (formatter) return formatter(n, this.currency);
2523
2973
  try {
2524
2974
  return new Intl.NumberFormat(this.opts.locale, { style: "currency", currency: this.currency }).format(n);
2525
2975
  } catch {
2526
2976
  return `${n} ${this.currency}`;
2527
2977
  }
2528
2978
  }
2979
+ /**
2980
+ * The price the buyer will actually pay for a category (+tier): the host's
2981
+ * `pricing` override when present, else the chart's stored price. Every
2982
+ * price the widget DISPLAYS or hands off must flow through here — a map
2983
+ * that shows one price while checkout charges another destroys trust.
2984
+ */
2985
+ paidPrice(categoryKey, tierId, fallback) {
2986
+ const entry = categoryKey ? this.opts.pricing?.prices?.[categoryKey] : void 0;
2987
+ if (entry === void 0) return fallback;
2988
+ if (typeof entry === "number") return entry;
2989
+ if (tierId && entry.tiers?.[tierId] !== void 0) return entry.tiers[tierId];
2990
+ return entry.base ?? fallback;
2991
+ }
2529
2992
  syncPrices() {
2530
2993
  const doc = this.controller.doc;
2531
2994
  if (!doc || !this.els.prices) return;
2532
2995
  const left = this.controller.categoryAvailability();
2996
+ this.narrateAvailability(doc.categories, left);
2997
+ this.syncSoldout(doc.categories, left);
2533
2998
  this.els.prices.innerHTML = doc.categories.map((c) => {
2534
- const price = c.tiers?.length ? c.tiers[0].price : c.price;
2999
+ const price = this.catPrice(c);
2535
3000
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
2536
3001
  return `<div class="sl-price-row${dim ? " sl-dim" : ""}" data-cat="${c.key}"><span class="sl-dot" style="background:${c.color}"></span><span class="sl-price-label">${c.label}</span><span class="sl-price-left">${left[c.key] ?? 0} left</span>` + (price != null ? `<span class="sl-price-amt">${this.money(price)}</span>` : "") + `</div>`;
2537
3002
  }).join("") + `<div class="sl-status-key" aria-label="Seat status legend"><span class="sl-status-item"><i class="sl-status-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg></i>Temporarily held</span><span class="sl-status-item"><i class="sl-status-icon sold" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M7 17L17 7"/></svg></i>Sold</span></div>`;
@@ -2540,6 +3005,26 @@ var SeatPicker = class _SeatPicker {
2540
3005
  row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
2541
3006
  });
2542
3007
  }
3008
+ /**
3009
+ * Live-activity strip: turn WS availability deltas into one quiet line of
3010
+ * social proof ("2 seats just taken in VIP · 118 left"). Diffs per-category
3011
+ * counts on every status change — no per-seat payload needed. Skips the very
3012
+ * first computation (initial load is not "activity").
3013
+ */
3014
+ narrateAvailability(categories, left) {
3015
+ const textEl = this.els.liveText;
3016
+ const prev = this.lastCatAvail;
3017
+ this.lastCatAvail = { ...left };
3018
+ if (!textEl || !prev) return;
3019
+ for (const cat of categories) {
3020
+ const before = prev[cat.key];
3021
+ const now = left[cat.key] ?? 0;
3022
+ if (before === void 0 || now >= before) continue;
3023
+ const taken = before - now;
3024
+ textEl.textContent = `${taken} seat${taken === 1 ? "" : "s"} just taken in ${cat.label} \xB7 ${now} left`;
3025
+ return;
3026
+ }
3027
+ }
2543
3028
  /** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
2544
3029
  evictTakenSelections() {
2545
3030
  const ownLabels = /* @__PURE__ */ new Set([
@@ -2568,15 +3053,23 @@ var SeatPicker = class _SeatPicker {
2568
3053
  const cats = this.controller.doc?.categories ?? [];
2569
3054
  parts.push(this.bestAvailableConfirm ? `<div class="sl-ba" role="alert"><div class="sl-ba-title"><span class="spark" aria-hidden="true">\u2726</span>Replace your current choices?</div><div class="sl-ba-replace"><b>We\u2019ll find ${this.baQty} seats together.</b><span>Your manually selected tickets will be removed only after a new group is secured.</span></div><div class="sl-ba-actions"><button type="button" data-ba-cancel>Keep mine</button><button type="button" class="replace" data-ba-replace>Find new seats</button></div></div>` : `<div class="sl-ba"><div class="sl-ba-title"><span class="spark" aria-hidden="true">\u2726</span>Find the best seats together</div><div class="sl-ba-copy"><span class="wide">We\u2019ll choose the closest available group for you.</span><span class="narrow">Closest available group, chosen instantly.</span></div>` + (cats.length > 1 ? `<select aria-label="Preferred ticket type" data-ba-cat><option value="">Any ticket type</option>` + cats.map((c) => `<option value="${c.key}"${this.baCat === c.key ? " selected" : ""}>${c.label}</option>`).join("") + `</select>` : `<span aria-hidden="true"></span>`) + `<div class="sl-ba-qty"><button type="button" data-ba="-1" aria-label="Fewer seats">\u2212</button><span>${this.baQty}</span><button type="button" data-ba="1" aria-label="More seats">+</button></div><button type="button" class="sl-ba-go"${this.bestAvailableBusy ? " disabled" : ""}>` + (this.bestAvailableBusy ? `<span class="sl-ba-spin" aria-hidden="true"></span>Finding the best seats\u2026` : `Find ${this.baQty} best ${this.baQty === 1 ? "seat" : "seats"}`) + `</button></div>`);
2570
3055
  }
3056
+ const idGrid = (seatId, label) => {
3057
+ const d = seatId ? this.controller.seatDetails(seatId) : null;
3058
+ if (!d?.sectionLabel && !d?.rowLabel && !d?.seatNumber) {
3059
+ return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">Seat</span><span class="val">${label}</span></span></div>`;
3060
+ }
3061
+ return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">Section</span><span class="val">${d.sectionLabel ?? "\u2014"}</span></span>` + (d.rowLabel ? `<span class="fld mid"><span class="sl-chip-eb">Row</span><span class="val">${this.rowShort(d)}</span></span>` : "") + (d.seatNumber ? `<span class="fld mid"><span class="sl-chip-eb">Seat</span><span class="val">${d.seatNumber}</span></span>` : "") + `</div>`;
3062
+ };
3063
+ const iconRail = (rmAria, viewLabel) => `<div class="sl-chip-rail"><button type="button" class="rm" aria-label="${rmAria}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>` + (viewLabel ? `<button type="button" class="view" data-view-label="${viewLabel}" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: viewLabel })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "") + `</div>`;
2571
3064
  for (const item of heldItems) {
2572
3065
  const itemKey = `held:${item.label}`;
2573
3066
  nextTrayKeys.add(itemKey);
2574
3067
  const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
2575
3068
  const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
2576
- const canView2 = this.seatViewEnabled() && item.objectType !== "ga" && !!this.controller.seatByLabel(item.label);
2577
- const viewBtn = canView2 ? `<button type="button" class="view" data-view-label="${item.label}" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: item.label })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "";
3069
+ const heldSeat = item.objectType !== "ga" ? this.controller.seatByLabel(item.label) : null;
3070
+ const canView2 = this.seatViewEnabled() && !!heldSeat;
2578
3071
  parts.push(
2579
- `<div class="sl-chip sl-held${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-held="${encodeURIComponent(item.label)}"><span class="sl-ticket-state held" aria-label="Held for you" title="Held for you"><svg viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg></span><div class="sl-chip-main"><b>${item.label}</b><div class="sl-chip-sub"><span class="cat">${cat?.label ?? item.categoryKey}${tierName ? ` \xB7 ${tierName}` : ""}</span>${viewBtn}</div></div><span class="amt">${this.money(item.unitPrice * (item.quantity ?? 1))}</span><button type="button" class="rm" aria-label="Remove held ticket ${item.label}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div>`
3072
+ `<div class="sl-chip sl-held${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-held="${encodeURIComponent(item.label)}"${heldSeat ? ` data-locate="${heldSeat.id}"` : ""}><div class="sl-chip-main">` + idGrid(heldSeat?.id ?? null, item.label) + `<div class="sl-chip-sub"><span class="sl-ticket-state held" aria-label="Held for you" title="Held for you"><svg viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg></span><span class="cat">${cat?.label ?? item.categoryKey}${tierName ? ` \xB7 ${tierName}` : ""}</span><span class="amt">${this.money(this.paidPrice(item.categoryKey, item.tierId, item.unitPrice) * (item.quantity ?? 1))}</span></div></div>` + iconRail(`Remove held ticket ${item.label}`, canView2 ? item.label : null) + `</div>`
2580
3073
  );
2581
3074
  }
2582
3075
  const heldLabels = new Set(heldItems.map((item) => item.label));
@@ -2585,16 +3078,15 @@ var SeatPicker = class _SeatPicker {
2585
3078
  const itemKey = `seat:${s.id}`;
2586
3079
  nextTrayKeys.add(itemKey);
2587
3080
  const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
2588
- const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${(0, import_core2.t)("picker.ticketTierFor", { label: s.label })}">` + s.tiers.map((ti) => `<option value="${ti.id}"${ti.id === s.tierId ? " selected" : ""}>${ti.name} \xB7 ${this.money(ti.price)}</option>`).join("") + `</select>` : "";
2589
- const viewBtn = canView ? `<button type="button" class="view" data-view-label="${s.label}" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: s.label })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "";
3081
+ const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${(0, import_core2.t)("picker.ticketTierFor", { label: s.label })}">` + s.tiers.map((ti) => `<option value="${ti.id}"${ti.id === s.tierId ? " selected" : ""}>${ti.name} \xB7 ${this.money(this.paidPrice(s.categoryKey, ti.id, ti.price))}</option>`).join("") + `</select>` : "";
2590
3082
  parts.push(
2591
- `<div class="sl-chip${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-seat="${s.id}"><span class="sl-ticket-state" aria-label="Selected" title="Selected"><svg viewBox="0 0 24 24"><path d="M5 12l4 4L19 6"/></svg></span><div class="sl-chip-main"><b>${s.label}</b><div class="sl-chip-sub"><span class="cat">${cat?.label ?? s.categoryKey}</span>${tierSelect}${viewBtn}</div></div><span class="amt">${this.money(s.price)}</span><button type="button" class="rm" aria-label="Remove ${s.label}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div>`
3083
+ `<div class="sl-chip${this.lastTrayKeys.has(itemKey) ? "" : " sl-enter"}" data-key="${itemKey}" data-seat="${s.id}" data-locate="${s.id}"><div class="sl-chip-main">` + idGrid(s.id, s.label) + `<div class="sl-chip-sub"><span class="sl-ticket-state" aria-label="Selected" title="Selected"><svg viewBox="0 0 24 24"><path d="M5 12l4 4L19 6"/></svg></span><span class="cat">${cat?.label ?? s.categoryKey}</span>${tierSelect}<span class="amt">${this.money(this.paidPrice(s.categoryKey, s.tierId ?? null, s.price))}</span></div></div>` + iconRail(`Remove ${s.label}`, canView ? s.label : null) + `</div>`
2592
3084
  );
2593
3085
  }
2594
3086
  for (const area of gaAreas) {
2595
3087
  const qty = this.gaQty.get(area.id) ?? 0;
2596
3088
  parts.push(
2597
- `<div class="sl-ga" data-ga="${area.id}"><div class="sl-ga-info"><div class="sl-ga-name">${area.label}</div><div class="sl-ga-sub">${this.money(area.price)} \xB7 ${area.available} left</div></div><div class="sl-ga-qty"><button type="button" data-d="-1" aria-label="Fewer">\u2212</button><span>${qty}</span><button type="button" data-d="1" aria-label="More">+</button></div></div>`
3089
+ `<div class="sl-ga" data-ga="${area.id}"><div class="sl-ga-info"><div class="sl-ga-name">${area.label}</div><div class="sl-ga-sub">${this.money(this.paidPrice(area.categoryKey, null, area.price))} \xB7 ${area.available} left</div></div><div class="sl-ga-qty"><button type="button" data-d="-1" aria-label="Fewer">\u2212</button><span>${qty}</span><button type="button" data-d="1" aria-label="More">+</button></div></div>`
2598
3090
  );
2599
3091
  }
2600
3092
  this.els.tray.innerHTML = parts.join("");
@@ -2634,7 +3126,7 @@ var SeatPicker = class _SeatPicker {
2634
3126
  return;
2635
3127
  }
2636
3128
  const id = chip.dataset.seat;
2637
- const label = chip.querySelector("b")?.textContent ?? "Seat";
3129
+ const label = this.controller.getSelection().find((sel) => sel.id === id)?.label ?? "Seat";
2638
3130
  const remove = () => {
2639
3131
  this.controller.deselect([id]);
2640
3132
  this.toast(`${label} removed.`, "neutral", {
@@ -2665,6 +3157,11 @@ var SeatPicker = class _SeatPicker {
2665
3157
  if (seat) this.openSeatView(seat);
2666
3158
  });
2667
3159
  });
3160
+ this.els.tray.querySelectorAll(".sl-chip[data-locate]").forEach((chip) => {
3161
+ const locate = () => this.controller.flashSeat(chip.dataset.locate, this.cssVar("--sl-accent") || "#f4b740");
3162
+ chip.addEventListener("mouseenter", locate);
3163
+ chip.addEventListener("focusin", locate);
3164
+ });
2668
3165
  this.els.tray.querySelectorAll(".sl-ga button").forEach((btn) => {
2669
3166
  btn.addEventListener("click", () => {
2670
3167
  const areaEl = btn.closest(".sl-ga");
@@ -2677,12 +3174,17 @@ var SeatPicker = class _SeatPicker {
2677
3174
  this.syncTray();
2678
3175
  });
2679
3176
  });
3177
+ if (this.salesClosed) {
3178
+ this.els.tray.querySelectorAll(".sl-ba-go,[data-ba],[data-ba-cat],[data-ba-replace],.sl-ga button").forEach((el) => {
3179
+ el.disabled = true;
3180
+ });
3181
+ }
2680
3182
  const gaTotal = this.pendingGATotal(gaAreas);
2681
3183
  const gaCount = this.pendingGACount();
2682
- const heldTotal = heldItems.reduce((sum, item) => sum + item.unitPrice * (item.quantity ?? 1), 0);
3184
+ const heldTotal = heldItems.reduce((sum, item) => sum + this.paidPrice(item.categoryKey, item.tierId, item.unitPrice) * (item.quantity ?? 1), 0);
2683
3185
  const heldCount = heldItems.reduce((sum, item) => sum + (item.quantity ?? 1), 0);
2684
3186
  const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
2685
- const total = freshSeats.reduce((sum, s) => sum + s.price, 0) + gaTotal + heldTotal;
3187
+ const total = freshSeats.reduce((sum, s) => sum + this.paidPrice(s.categoryKey, s.tierId ?? null, s.price), 0) + gaTotal + heldTotal;
2686
3188
  const count = freshSeats.length + gaCount + heldCount;
2687
3189
  const pendingCount = this.pendingSelectionCount();
2688
3190
  const previousCount = this.lastTrayCount;
@@ -2778,6 +3280,7 @@ var SeatPicker = class _SeatPicker {
2778
3280
  }
2779
3281
  }
2780
3282
  async handleCta() {
3283
+ if (this.salesClosed) return;
2781
3284
  if (this.totalTicketCount() > this.maxTickets) {
2782
3285
  this.toast(`Remove tickets until your order has ${this.maxTickets} or fewer.`, "warning");
2783
3286
  return;
@@ -2821,6 +3324,7 @@ var SeatPicker = class _SeatPicker {
2821
3324
  this.opts.onError?.(err);
2822
3325
  const problem = err;
2823
3326
  const labels = (problem.conflicts ?? []).map((conflict) => conflict.label).filter(Boolean).slice(0, 3);
3327
+ if (problem.reason === "event_closed") this.setSalesClosed(true);
2824
3328
  const message = problem.reason === "event_closed" ? "Seat sales have closed for this event." : labels.length ? `${labels.join(", ")} ${labels.length === 1 ? "is" : "are"} no longer available. Choose another ${labels.length === 1 ? "seat" : "group"}.` : "One or more seats were just taken. Please pick again.";
2825
3329
  this.toast(message, "error");
2826
3330
  this.setCtaPhase("idle");
@@ -2929,7 +3433,7 @@ var SeatPicker = class _SeatPicker {
2929
3433
  objectType: it.objectType,
2930
3434
  categoryKey: it.categoryKey,
2931
3435
  tierId: it.tierId,
2932
- unitPrice: it.unitPrice,
3436
+ unitPrice: this.paidPrice(it.categoryKey, it.tierId, it.unitPrice),
2933
3437
  currency: it.currency ?? this.currency,
2934
3438
  quantity: it.quantity ?? 1
2935
3439
  }));
@@ -2982,19 +3486,37 @@ var SeatPicker = class _SeatPicker {
2982
3486
  this.tipEl.style.left = `${Math.max(8, x)}px`;
2983
3487
  this.tipEl.style.top = `${Math.max(8, y)}px`;
2984
3488
  }
3489
+ /**
3490
+ * Row label without the redundant section prefix. Charts commonly name row
3491
+ * objects "104-A" while the Section column already shows "104" — so the Row
3492
+ * cell repeats the section and, in the compact hover card, truncates to
3493
+ * "10…". Strip a leading "<section><sep>" so Row reads a clean "A". Only when
3494
+ * the prefix is exact (won't touch "1040-A" under section "104"); otherwise
3495
+ * the label is shown verbatim.
3496
+ */
3497
+ rowShort(details) {
3498
+ const row = details?.rowLabel;
3499
+ const sec = details?.sectionLabel;
3500
+ if (!row || !sec) return row;
3501
+ for (const sep of ["-", " ", "\xB7", "/", "_"]) {
3502
+ const prefix = `${sec}${sep}`;
3503
+ if (row.startsWith(prefix) && row.length > prefix.length) return row.slice(prefix.length);
3504
+ }
3505
+ return row;
3506
+ }
2985
3507
  updateTooltip(details) {
2986
3508
  if (!this.tipEl) return;
2987
3509
  if (!details) {
2988
3510
  this.tipEl.style.display = "none";
2989
3511
  return;
2990
3512
  }
2991
- const statusLine = details.status === "free" ? "" : `<div style="margin-top:5px;font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;font-weight:700">${details.status === "held" ? (0, import_core2.t)("map.statusHeld") : (0, import_core2.t)("map.statusTaken")}</div>`;
2992
- const location = [
2993
- details.sectionLabel ? `Section ${details.sectionLabel}` : "",
2994
- details.rowLabel ? `Row ${details.rowLabel}` : "",
2995
- details.seatNumber ? `Seat ${details.seatNumber}` : details.label
2996
- ].filter(Boolean).join(" \xB7 ");
2997
- this.tipEl.innerHTML = `<div style="font-weight:800;font-size:13px">${location}</div><div style="display:flex;align-items:center;gap:6px;margin-top:4px"><span style="width:9px;height:9px;border-radius:50%;flex:none;background:${details.categoryColor}"></span><span style="opacity:.75">${details.categoryLabel}</span><span style="margin-left:auto;font-weight:800">${this.money(details.price)}</span></div>` + statusLine;
3513
+ const esc2 = (v) => String(v ?? "\u2014").replace(/[&<>"]/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[ch]);
3514
+ const price = this.money(this.paidPrice(details.categoryKey, details.tierId ?? null, details.price));
3515
+ const hasLoc = details.sectionLabel || details.rowLabel || details.seatNumber;
3516
+ const grid = hasLoc ? `<div class="sl-tip-grid"><div class="sl-tip-field"><span class="sl-tip-key">Section</span><span class="sl-tip-val">${esc2(details.sectionLabel)}</span></div><div class="sl-tip-field"><span class="sl-tip-key">Row</span><span class="sl-tip-val">${esc2(this.rowShort(details))}</span></div><div class="sl-tip-field"><span class="sl-tip-key">Seat</span><span class="sl-tip-val">${esc2(details.seatNumber ?? details.label)}</span></div></div>` : `<div class="sl-tip-grid one"><div class="sl-tip-field"><span class="sl-tip-key">Seat</span><span class="sl-tip-val">${esc2(details.label)}</span></div></div>`;
3517
+ const statusLine = details.status === "free" ? "" : `<div class="sl-tip-status">${details.status === "held" ? (0, import_core2.t)("map.statusHeld") : (0, import_core2.t)("map.statusTaken")}</div>`;
3518
+ this.tipEl.style.setProperty("--sl-cat", details.categoryColor);
3519
+ this.tipEl.innerHTML = grid + `<div class="sl-tip-cat"><span class="sl-tip-dot" style="background:${details.categoryColor}"></span><span class="sl-tip-name">${esc2(details.categoryLabel)}</span><span class="sl-tip-amt">${price}</span></div>` + statusLine;
2998
3520
  this.tipEl.style.display = "block";
2999
3521
  this.placeTooltip();
3000
3522
  }
@@ -3015,7 +3537,7 @@ var SeatPicker = class _SeatPicker {
3015
3537
  return this.removeHeldLabel(label);
3016
3538
  }
3017
3539
  async bestAvailable(qty, categoryKey) {
3018
- if (this.bestAvailableBusy) return null;
3540
+ if (this.salesClosed || this.bestAvailableBusy) return null;
3019
3541
  qty = Math.max(1, Math.min(this.maxTickets, Math.floor(qty)));
3020
3542
  if (this.confirmSeat) this.cancelConfirm();
3021
3543
  this.bestAvailableConfirm = false;
@@ -3095,7 +3617,10 @@ var SeatPicker = class _SeatPicker {
3095
3617
  this.motionTimers.clear();
3096
3618
  this.ro?.disconnect();
3097
3619
  this.ro = null;
3620
+ if (this.framedFs) this.setFramedFs(false);
3098
3621
  if (this.escHandler) document.removeEventListener("keydown", this.escHandler);
3622
+ if (this.fsChangeHandler) document.removeEventListener("fullscreenchange", this.fsChangeHandler);
3623
+ if (this.fsEscHandler) window.removeEventListener("keydown", this.fsEscHandler);
3099
3624
  this.controller.destroy();
3100
3625
  this.root?.remove();
3101
3626
  this.root = null;
@@ -3107,6 +3632,92 @@ var SeatPicker = class _SeatPicker {
3107
3632
  }
3108
3633
  };
3109
3634
 
3635
+ // src/attachPickerFrame.ts
3636
+ function attachPickerFrame(iframe, opts = {}) {
3637
+ let expectedOrigin = opts.origin ?? "";
3638
+ if (!expectedOrigin) {
3639
+ try {
3640
+ expectedOrigin = new URL(iframe.src, window.location.href).origin;
3641
+ } catch {
3642
+ expectedOrigin = "";
3643
+ }
3644
+ }
3645
+ let pinned = false;
3646
+ let frameStyleBeforeFs = null;
3647
+ let docOverflowBeforeFs = null;
3648
+ let bodyOverflowBeforeFs = null;
3649
+ let lastAutoHeight = "";
3650
+ let keyHandler = null;
3651
+ const pin = () => {
3652
+ if (pinned) return;
3653
+ pinned = true;
3654
+ frameStyleBeforeFs = iframe.getAttribute("style");
3655
+ Object.assign(iframe.style, {
3656
+ position: "fixed",
3657
+ inset: "0",
3658
+ width: "100vw",
3659
+ height: "100vh",
3660
+ margin: "0",
3661
+ border: "0",
3662
+ zIndex: "2147483000",
3663
+ background: "#101625"
3664
+ });
3665
+ const docEl = document.documentElement;
3666
+ docOverflowBeforeFs = docEl.style.overflow;
3667
+ docEl.style.overflow = "hidden";
3668
+ if (document.body) {
3669
+ bodyOverflowBeforeFs = document.body.style.overflow;
3670
+ document.body.style.overflow = "hidden";
3671
+ }
3672
+ keyHandler = (event) => {
3673
+ if (event.key === "Escape") unpin();
3674
+ };
3675
+ window.addEventListener("keydown", keyHandler);
3676
+ };
3677
+ const unpin = () => {
3678
+ if (!pinned) return;
3679
+ pinned = false;
3680
+ if (frameStyleBeforeFs === null) iframe.removeAttribute("style");
3681
+ else iframe.setAttribute("style", frameStyleBeforeFs);
3682
+ frameStyleBeforeFs = null;
3683
+ if (lastAutoHeight) iframe.style.height = lastAutoHeight;
3684
+ if (docOverflowBeforeFs !== null) {
3685
+ document.documentElement.style.overflow = docOverflowBeforeFs;
3686
+ docOverflowBeforeFs = null;
3687
+ }
3688
+ if (bodyOverflowBeforeFs !== null && document.body) {
3689
+ document.body.style.overflow = bodyOverflowBeforeFs;
3690
+ bodyOverflowBeforeFs = null;
3691
+ }
3692
+ if (keyHandler) {
3693
+ window.removeEventListener("keydown", keyHandler);
3694
+ keyHandler = null;
3695
+ }
3696
+ };
3697
+ const onMessage = (event) => {
3698
+ if (event.source !== iframe.contentWindow) return;
3699
+ if (expectedOrigin && event.origin !== expectedOrigin) return;
3700
+ if (!event.data || typeof event.data !== "object") return;
3701
+ const data = event.data;
3702
+ if (data.type === "seatlayer:height") {
3703
+ if (typeof data.px === "number" && Number.isFinite(data.px) && data.px > 0) {
3704
+ lastAutoHeight = `${Math.round(data.px)}px`;
3705
+ if (!pinned) iframe.style.height = lastAutoHeight;
3706
+ }
3707
+ return;
3708
+ }
3709
+ if (data.type === "seatlayer:fullscreen") {
3710
+ if (data.on === true) pin();
3711
+ else if (data.on === false) unpin();
3712
+ }
3713
+ };
3714
+ window.addEventListener("message", onMessage);
3715
+ return () => {
3716
+ window.removeEventListener("message", onMessage);
3717
+ unpin();
3718
+ };
3719
+ }
3720
+
3110
3721
  // src/SeatManager.ts
3111
3722
  var import_core3 = require("@seatlayer/core");
3112
3723
 
@@ -3188,6 +3799,21 @@ var ManageApi = class {
3188
3799
  setHoldTtl(key, holdTtlMs) {
3189
3800
  return this.auth(`/v1/events/${encodeURIComponent(key)}/hold-ttl`, { method: "POST", body: { holdTtlMs } });
3190
3801
  }
3802
+ // ---- availability windows (token) ----
3803
+ /** The organizer's current per section/zone availability windows (needs
3804
+ * `event:view`). Ids absent from `rules` are open / on sale. */
3805
+ availability(key) {
3806
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/availability`);
3807
+ }
3808
+ /** Replace the availability windows for a set of section/zone ids (needs
3809
+ * `event:block`). Ids absent from `rules` become open / on sale; a zone rule
3810
+ * cascades to its sections. The worker derives each id's seat labels, so
3811
+ * `labels` on the sent rules is best-effort. Resolves with the authoritative
3812
+ * effective `hidden` set (a due rule may fire at once) and the server-cleaned
3813
+ * `rules` map (fired timed/threshold windows dropped). */
3814
+ setAvailability(key, rules) {
3815
+ return this.auth(`/v1/events/${encodeURIComponent(key)}/availability`, { method: "POST", body: { rules } });
3816
+ }
3191
3817
  // ---- reports (token) ----
3192
3818
  report(key) {
3193
3819
  return this.auth(`/v1/events/${encodeURIComponent(key)}/report`);
@@ -3215,6 +3841,27 @@ var ManageApi = class {
3215
3841
  };
3216
3842
 
3217
3843
  // src/SeatManager.ts
3844
+ function availabilityModeOf(rule) {
3845
+ return rule ? rule.mode : "open";
3846
+ }
3847
+ function availabilityRuleForMode(mode, seatLabels, prev) {
3848
+ switch (mode) {
3849
+ case "open":
3850
+ return null;
3851
+ case "hidden":
3852
+ return { mode: "hidden", labels: seatLabels };
3853
+ case "closed":
3854
+ return { mode: "closed", labels: seatLabels };
3855
+ case "timed":
3856
+ return { mode: "timed", revealAt: prev?.revealAt ?? Date.now() + 36e5, labels: seatLabels };
3857
+ case "threshold":
3858
+ return { mode: "threshold", thresholdPct: prev?.thresholdPct ?? 80, labels: seatLabels };
3859
+ }
3860
+ }
3861
+ function toLocalInput(ms) {
3862
+ const d = new Date(ms - (/* @__PURE__ */ new Date()).getTimezoneOffset() * 6e4);
3863
+ return d.toISOString().slice(0, 16);
3864
+ }
3218
3865
  function resolveContainer4(container) {
3219
3866
  if (typeof container === "string") {
3220
3867
  const el = document.querySelector(container);
@@ -3403,6 +4050,32 @@ var CSS2 = `
3403
4050
  .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}
3404
4051
  .slm-momentumgradient{height:6px;min-width:64px;flex:1;border-radius:999px;background:linear-gradient(90deg,#f4b740,#ef4444)}
3405
4052
  .slm-momentumcopy{margin-top:7px;color:var(--slm-muted);font-size:11px;line-height:1.45}
4053
+ /* sections: availability windows */
4054
+ .slm-availlist{display:flex;flex-direction:column;gap:8px;margin:2px 0 12px}
4055
+ .slm-availrow{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);transition:border-color .15s ease,opacity .15s ease}
4056
+ .slm-availrow.zone{background:color-mix(in srgb,var(--slm-surface) 82%,#000)}
4057
+ .slm-availrow.hidden{opacity:.62}.slm-availrow.closed{opacity:.82}
4058
+ .slm-availhead{display:flex;align-items:center;gap:8px}
4059
+ .slm-availlabel{display:flex;align-items:center;gap:5px;flex:1;min-width:0;font-size:12.5px;font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
4060
+ .slm-availcaret{flex:none;color:var(--slm-muted);font-size:10px}
4061
+ .slm-availcount{flex:none;font-size:11px;font-weight:700;color:var(--slm-muted);font-variant-numeric:tabular-nums}
4062
+ .slm-availbadge{flex:none;font-size:9px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;padding:2px 6px;border-radius:999px}
4063
+ .slm-availbadge.hidden{background:rgba(139,148,172,.18);color:#c2c9d8}
4064
+ .slm-availbadge.closed{background:rgba(244,183,64,.16);color:#f7ca6b}
4065
+ .slm-availselwrap{position:relative;flex:none;display:inline-flex}
4066
+ .slm-availmode{width:auto;max-width:190px;padding:6px 8px;font-size:11.5px;font-weight:700;cursor:pointer}
4067
+ .slm-availmode.on{border-color:var(--slm-accent);color:var(--slm-text)}
4068
+ .slm-availmode:disabled{opacity:.55;cursor:progress}
4069
+ .slm-availfollows{flex:none;padding:5px 10px;border:1px solid var(--slm-line);border-radius:7px;background:var(--slm-surface);color:var(--slm-muted);font-size:11px;font-weight:600;white-space:nowrap}
4070
+ .slm-availdetail{display:flex;align-items:center;gap:8px;margin-top:9px}
4071
+ .slm-availdetail .slm-input{flex:1}
4072
+ .slm-availpct{max-width:74px;flex:none!important}
4073
+ .slm-availpctlabel{font-size:11px;color:var(--slm-muted);font-weight:600;white-space:nowrap}
4074
+ .slm-availsummary{display:flex;align-items:center;gap:8px;padding:10px 12px;border:1px solid var(--slm-line);border-radius:9px;color:var(--slm-muted);font-size:12.5px}
4075
+ .slm-availdot{width:9px;height:9px;border-radius:50%;flex:none;background:#22a06b}.slm-availdot.warn{background:#f4b740}
4076
+ .slm-availcallout{display:flex;align-items:flex-start;gap:8px;margin-top:10px;padding:10px 12px;border:1px solid rgba(244,183,64,.45);border-radius:9px;background:rgba(244,183,64,.1)}
4077
+ .slm-availstar{flex:none;margin-top:1px;color:#f4b740;font-size:13px;line-height:1}
4078
+ .slm-availcallout p{font-size:11.5px;line-height:1.55;color:#f4d58a}.slm-availcallout b{color:#ffe4a3;font-weight:800}
3406
4079
  .slm-inspect-card{padding:16px;border:1px solid var(--slm-line);border-radius:12px;background:var(--slm-surface)}
3407
4080
  .slm-inspect-label{font-size:24px;font-weight:850;letter-spacing:-.02em;line-height:1.1}
3408
4081
  .slm-inspect-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px 20px;margin-top:18px}
@@ -3503,6 +4176,13 @@ var SeatManager = class {
3503
4176
  this.tokenRefreshInFlight = false;
3504
4177
  this.sectionByObject = /* @__PURE__ */ new Map();
3505
4178
  this.sectionLabelById = /* @__PURE__ */ new Map();
4179
+ this.sectionsBase = null;
4180
+ // Sections mode (availability windows): organizer rules + the live effective
4181
+ // hidden/closed sets from the snapshot + WS (a timed/threshold rule fires DO-side).
4182
+ this.availabilityRules = {};
4183
+ this.effectiveHidden = /* @__PURE__ */ new Set();
4184
+ this.effectiveClosed = /* @__PURE__ */ new Set();
4185
+ this.availabilitySaving = false;
3506
4186
  this.lastSyncedAt = null;
3507
4187
  this.blockedQuery = "";
3508
4188
  this.blockedSection = "";
@@ -3521,6 +4201,7 @@ var SeatManager = class {
3521
4201
  if (key === "m") this.setMode("view");
3522
4202
  else if (key === "i") this.setMode("inspect");
3523
4203
  else if (key === "b") this.setMode("block");
4204
+ else if (key === "s") this.setMode("sections");
3524
4205
  else if (key === "f") this.toggleFullscreen();
3525
4206
  else return;
3526
4207
  event.preventDefault();
@@ -3564,7 +4245,8 @@ var SeatManager = class {
3564
4245
  this.buildSectionOptions();
3565
4246
  const [, controlRoom] = await Promise.all([
3566
4247
  this.resnapshot(),
3567
- this.refreshControlRoom().catch((err) => this.opts.onError?.(err))
4248
+ this.refreshControlRoom().catch((err) => this.opts.onError?.(err)),
4249
+ this.refreshAvailability()
3568
4250
  ]);
3569
4251
  if (controlRoom?.activity) this.seedFeed(controlRoom.activity);
3570
4252
  else this.api.log(this.key, { limit: 24 }).then((page) => this.seedFeed(page.entries)).catch(() => {
@@ -3589,6 +4271,7 @@ var SeatManager = class {
3589
4271
  if (changed) this.renderer?.clearSelection();
3590
4272
  this.paintModeTabs();
3591
4273
  this.paintRail();
4274
+ this.applySectionCanvasTreatment();
3592
4275
  if (changed) this.opts.onModeChange?.(mode);
3593
4276
  }
3594
4277
  /** Toggle the normalized sales-velocity outline overlay without changing seat colors. */
@@ -3880,6 +4563,7 @@ var SeatManager = class {
3880
4563
  this.attempt = 0;
3881
4564
  this.setLive(true);
3882
4565
  void this.resnapshot().then(() => this.scheduleRevenueRefresh(0));
4566
+ void this.refreshAvailability();
3883
4567
  };
3884
4568
  ws.onmessage = (e) => this.onMessage(e);
3885
4569
  ws.onclose = () => {
@@ -3911,6 +4595,9 @@ var SeatManager = class {
3911
4595
  }
3912
4596
  if (!msg || typeof msg !== "object") return;
3913
4597
  const m = msg;
4598
+ if (Array.isArray(m.hidden) || Array.isArray(m.closed)) {
4599
+ this.updateEffectiveAvailability(m.hidden, m.closed);
4600
+ }
3914
4601
  if (m.type === "presence") {
3915
4602
  if (this.controlRoomSnapshot && typeof m.shoppingSessions === "number" && typeof m.activeHolds === "number") {
3916
4603
  this.controlRoomSnapshot = {
@@ -3962,6 +4649,7 @@ var SeatManager = class {
3962
4649
  try {
3963
4650
  const objs = await this.api.objects(this.key);
3964
4651
  this.applySnapshot(objs.seats);
4652
+ this.updateEffectiveAvailability(objs.hidden, objs.closed);
3965
4653
  } catch {
3966
4654
  }
3967
4655
  }
@@ -4273,6 +4961,7 @@ var SeatManager = class {
4273
4961
  <button class="slm-mode" role="tab" data-mode="view" title="Monitor (M)" aria-keyshortcuts="M">Monitor</button>
4274
4962
  <button class="slm-mode" role="tab" data-mode="inspect" title="Inspect (I)" aria-keyshortcuts="I">Inspect</button>
4275
4963
  <button class="slm-mode" role="tab" data-mode="block" title="Block (B)" aria-keyshortcuts="B">Block</button>
4964
+ <button class="slm-mode" role="tab" data-mode="sections" title="Sections (S)" aria-keyshortcuts="S">Sections</button>
4276
4965
  </div>
4277
4966
  <span class="slm-live"><span class="slm-live-dot"></span><span data-ref="livetext">CONNECTING</span></span>
4278
4967
  <div class="slm-bar-actions">
@@ -4339,6 +5028,7 @@ var SeatManager = class {
4339
5028
  if (!this.doc) return;
4340
5029
  try {
4341
5030
  const secs = (0, import_core3.computeSections)(this.doc);
5031
+ this.sectionsBase = secs;
4342
5032
  this.sectionOptions = [];
4343
5033
  this.sectionByObject = new Map(secs.objectToSection);
4344
5034
  this.sectionLabelById.clear();
@@ -4464,6 +5154,7 @@ var SeatManager = class {
4464
5154
  paintRail() {
4465
5155
  if (this.mode === "view") this.renderViewRail();
4466
5156
  else if (this.mode === "inspect") this.renderInspectRail(this.getSelection());
5157
+ else if (this.mode === "sections") this.renderSectionsRail();
4467
5158
  else this.renderBlockRail();
4468
5159
  this.updateZoomHint();
4469
5160
  }
@@ -4586,6 +5277,259 @@ var SeatManager = class {
4586
5277
  </div>
4587
5278
  </div>`;
4588
5279
  }
5280
+ // ---- sections: availability windows --------------------------------------
5281
+ /** Pull the organizer's availability rules (event:view). Called on load and on
5282
+ * every WS (re)connect, mirroring how the other panels re-hydrate. `closed` is
5283
+ * deterministic from the rules; `hidden` (which folds in already-due timed /
5284
+ * threshold windows) comes from the snapshot + WS effective set. */
5285
+ async refreshAvailability() {
5286
+ try {
5287
+ const res = await this.withAuthRetry(() => this.api.availability(this.key));
5288
+ this.availabilityRules = res.rules ?? {};
5289
+ this.effectiveClosed = new Set(this.closedIdsFromRules(this.availabilityRules));
5290
+ if (this.mode === "sections") this.renderSectionsRail();
5291
+ this.applySectionCanvasTreatment();
5292
+ } catch (err) {
5293
+ this.opts.onError?.(err);
5294
+ }
5295
+ }
5296
+ /** Run a token-authed op; on a 401 re-mint via onTokenRefresh and retry once. */
5297
+ async withAuthRetry(op) {
5298
+ try {
5299
+ return await op();
5300
+ } catch (err) {
5301
+ if (err instanceof ManageApiError && err.status === 401 && this.opts.onTokenRefresh && !this.tokenRefreshInFlight) {
5302
+ await this.rotateToken();
5303
+ return op();
5304
+ }
5305
+ throw err;
5306
+ }
5307
+ }
5308
+ closedIdsFromRules(rules) {
5309
+ return Object.entries(rules).filter(([, r]) => r.mode === "closed").map(([id]) => id);
5310
+ }
5311
+ /** Adopt a new effective hidden/closed set (from a snapshot or WS broadcast) and
5312
+ * repaint the rail + canvas when it actually moves. */
5313
+ updateEffectiveAvailability(hidden, closed) {
5314
+ let changed = false;
5315
+ if (Array.isArray(hidden)) {
5316
+ this.effectiveHidden = new Set(hidden.filter((x) => typeof x === "string"));
5317
+ changed = true;
5318
+ }
5319
+ if (Array.isArray(closed)) {
5320
+ this.effectiveClosed = new Set(closed.filter((x) => typeof x === "string"));
5321
+ changed = true;
5322
+ }
5323
+ if (!changed) return;
5324
+ if (this.mode === "sections") this.renderSectionsRail();
5325
+ this.applySectionCanvasTreatment();
5326
+ }
5327
+ /** Canvas read of the availability state: dim hidden sections to a whisper,
5328
+ * half-light closed sections, leave open sections normal. Only in Sections mode;
5329
+ * cleared in every other tool. */
5330
+ applySectionCanvasTreatment() {
5331
+ if (!this.renderer) return;
5332
+ if (this.mode === "sections") {
5333
+ this.renderer.setDimmedSections([...this.effectiveHidden]);
5334
+ this.renderer.setClosedSections([...this.effectiveClosed]);
5335
+ } else {
5336
+ this.renderer.setDimmedSections(null);
5337
+ this.renderer.setClosedSections(null);
5338
+ }
5339
+ }
5340
+ /** Zone-grouped render tree: each zone header then its sections (which follow the
5341
+ * zone window), then loose sections + the ungrouped bucket. Effective hidden /
5342
+ * closed come from the live sets, rules from the organizer map. */
5343
+ buildSectionRows() {
5344
+ const base = this.sectionsBase;
5345
+ if (!base) return { rows: [], hiddenSections: 0, closedSections: 0 };
5346
+ const zones = this.doc?.zones ?? [];
5347
+ const byZone = /* @__PURE__ */ new Map();
5348
+ const loose = [];
5349
+ for (const s of base.sections) {
5350
+ if (s.zone && zones.some((z) => z.id === s.zone)) {
5351
+ const list = byZone.get(s.zone) ?? [];
5352
+ list.push(s);
5353
+ byZone.set(s.zone, list);
5354
+ } else {
5355
+ loose.push(s);
5356
+ }
5357
+ }
5358
+ const rows = [];
5359
+ let hiddenSections = 0;
5360
+ let closedSections = 0;
5361
+ const push = (kind, node, zoneRuled, parentClosed = false) => {
5362
+ const rule = this.availabilityRules[node.id] ?? null;
5363
+ const effClosed = this.effectiveClosed.has(node.id) || parentClosed;
5364
+ const effHidden = this.effectiveHidden.has(node.id) || zoneRuled && !effClosed;
5365
+ if (kind === "section" && effHidden) hiddenSections += 1;
5366
+ if (kind === "section" && effClosed) closedSections += 1;
5367
+ rows.push({
5368
+ kind,
5369
+ id: node.id,
5370
+ label: node.label,
5371
+ seatCount: node.seatCount,
5372
+ seatLabels: node.seatLabels,
5373
+ rule,
5374
+ hidden: effHidden,
5375
+ closed: effClosed,
5376
+ followsZone: kind === "section" && zoneRuled
5377
+ });
5378
+ };
5379
+ for (const z of zones) {
5380
+ const secs = byZone.get(z.id);
5381
+ if (!secs || !secs.length) continue;
5382
+ const zoneNode = {
5383
+ id: z.id,
5384
+ label: z.label || "Zone",
5385
+ seatCount: secs.reduce((sum, s) => sum + s.seatCount, 0),
5386
+ seatLabels: secs.flatMap((s) => s.seatLabels)
5387
+ };
5388
+ const zoneRuled = !!this.availabilityRules[z.id];
5389
+ const zoneClosed = this.availabilityRules[z.id]?.mode === "closed";
5390
+ push("zone", zoneNode, false);
5391
+ for (const s of secs) push("section", s, zoneRuled, zoneClosed);
5392
+ }
5393
+ for (const s of loose) push("section", s, false);
5394
+ if (base.ungrouped) {
5395
+ const u = base.ungrouped;
5396
+ push("section", { id: import_core3.UNGROUPED_ID, label: u.label, seatCount: u.seatCount, seatLabels: u.seatLabels }, false);
5397
+ }
5398
+ return { rows, hiddenSections, closedSections };
5399
+ }
5400
+ renderSectionsRail() {
5401
+ const { rows, hiddenSections, closedSections } = this.buildSectionRows();
5402
+ if (!rows.length) {
5403
+ this.els.rail.innerHTML = `
5404
+ <p class="slm-eyebrow">Availability windows</p>
5405
+ <p class="slm-hint">Draw sections or zones in the designer to schedule availability per area. This chart has none yet.</p>
5406
+ <div class="slm-empty">No sections on this chart.</div>`;
5407
+ return;
5408
+ }
5409
+ const parts = [];
5410
+ if (hiddenSections) parts.push(`${hiddenSections} hidden`);
5411
+ if (closedSections) parts.push(`${closedSections} closed`);
5412
+ const summary = parts.length ? parts.join(" \xB7 ") : "All sections open and on sale";
5413
+ const warn = hiddenSections > 0 || closedSections > 0;
5414
+ this.els.rail.innerHTML = `
5415
+ <p class="slm-eyebrow">Availability windows</p>
5416
+ <p class="slm-hint">Control when each zone or section goes on sale. Keep it hidden, reveal it at a set time, or <b>auto-reveal once the rest sells past a threshold</b>. Hidden seats vanish for buyers; closed seats stay on the map (flat grey) but can't be bought.</p>
5417
+ <div class="slm-availlist" data-ref="availlist">${rows.map((row) => this.sectionRowHtml(row)).join("")}</div>
5418
+ <div class="slm-availsummary">
5419
+ <span class="slm-availdot${warn ? " warn" : ""}"></span>
5420
+ <span>${esc(summary)}</span>
5421
+ </div>
5422
+ <div class="slm-availcallout">
5423
+ <span class="slm-availstar" aria-hidden="true">\u2726</span>
5424
+ <p><b>Auto-reveal at % sold</b> is our differentiator \u2014 demand-triggered release: the balcony opens itself the moment the stalls hit the threshold. Neither seats.io nor Ticketmaster ships this.</p>
5425
+ </div>`;
5426
+ this.wireSectionRail();
5427
+ this.applySectionCanvasTreatment();
5428
+ }
5429
+ sectionRowHtml(row) {
5430
+ const mode = availabilityModeOf(row.rule);
5431
+ const cls = `slm-availrow${row.kind === "zone" ? " zone" : ""}${row.hidden ? " hidden" : ""}${row.closed ? " closed" : ""}`;
5432
+ const disabled = this.availabilitySaving ? " disabled" : "";
5433
+ const option = (value, text) => `<option value="${value}"${mode === value ? " selected" : ""}>${text}</option>`;
5434
+ const control = row.followsZone ? '<span class="slm-availfollows">Follows zone</span>' : `<span class="slm-availselwrap">
5435
+ <select class="slm-select slm-availmode${mode !== "open" ? " on" : ""}" data-avail-id="${esc(row.id)}"${disabled} aria-label="Availability for ${esc(row.label)}">
5436
+ ${option("open", "Open \u2014 on sale")}
5437
+ ${option("closed", "Closed \u2014 visible, not on sale")}
5438
+ ${option("hidden", "Hidden \u2014 off the buyer map")}
5439
+ ${option("timed", "Reveal at a time")}
5440
+ ${option("threshold", "Auto-reveal at % sold")}
5441
+ </select>
5442
+ </span>`;
5443
+ let detail = "";
5444
+ if (!row.followsZone && mode === "timed") {
5445
+ const value = row.rule?.revealAt ? esc(toLocalInput(row.rule.revealAt)) : "";
5446
+ detail = `<div class="slm-availdetail">
5447
+ <input type="datetime-local" class="slm-input" data-avail-reveal="${esc(row.id)}" value="${value}"${disabled} aria-label="Reveal time for ${esc(row.label)}" />
5448
+ </div>`;
5449
+ } else if (!row.followsZone && mode === "threshold") {
5450
+ const pct = row.rule?.thresholdPct ?? 80;
5451
+ detail = `<div class="slm-availdetail">
5452
+ <span class="slm-availpctlabel">Reveal at</span>
5453
+ <input type="number" min="1" max="100" class="slm-input slm-availpct" data-avail-pct="${esc(row.id)}" value="${esc(pct)}"${disabled} aria-label="Percent sold to reveal ${esc(row.label)}" />
5454
+ <span class="slm-availpctlabel">% sold</span>
5455
+ </div>`;
5456
+ }
5457
+ const badge = row.closed ? '<span class="slm-availbadge closed">Closed</span>' : row.hidden ? '<span class="slm-availbadge hidden">Hidden</span>' : "";
5458
+ const caret = row.kind === "zone" ? `<span class="slm-availcaret" aria-hidden="true">${row.hidden ? "\u25B8" : "\u25BE"}</span>` : "";
5459
+ return `<div class="${cls}">
5460
+ <div class="slm-availhead">
5461
+ <span class="slm-availlabel">${caret}${esc(row.label)}</span>
5462
+ ${badge}
5463
+ <span class="slm-availcount">${row.seatCount.toLocaleString()}</span>
5464
+ ${control}
5465
+ </div>
5466
+ ${detail}
5467
+ </div>`;
5468
+ }
5469
+ wireSectionRail() {
5470
+ const rail = this.els.rail;
5471
+ if (!rail) return;
5472
+ rail.querySelectorAll("[data-avail-id]").forEach((select) => {
5473
+ select.addEventListener("change", () => this.setSectionMode(select.dataset.availId, select.value));
5474
+ });
5475
+ rail.querySelectorAll("[data-avail-reveal]").forEach((input) => {
5476
+ input.addEventListener("change", () => {
5477
+ const ms = new Date(input.value).getTime();
5478
+ if (Number.isFinite(ms)) this.setSectionRulePatch(input.dataset.availReveal, { revealAt: ms });
5479
+ });
5480
+ });
5481
+ rail.querySelectorAll("[data-avail-pct]").forEach((input) => {
5482
+ input.addEventListener("change", () => {
5483
+ const pct = Math.max(1, Math.min(100, Number(input.value) || 0));
5484
+ this.setSectionRulePatch(input.dataset.availPct, { thresholdPct: pct });
5485
+ });
5486
+ });
5487
+ }
5488
+ /** Change one row's availability mode. A zone rule subsumes its child section
5489
+ * rules, so those are dropped from the map (the zone window is the truth). */
5490
+ setSectionMode(id, mode) {
5491
+ const row = this.buildSectionRows().rows.find((r) => r.id === id);
5492
+ const seatLabels = row?.seatLabels ?? this.availabilityRules[id]?.labels ?? [];
5493
+ const next = { ...this.availabilityRules };
5494
+ const rule = availabilityRuleForMode(mode, seatLabels, this.availabilityRules[id]);
5495
+ if (rule) next[id] = rule;
5496
+ else delete next[id];
5497
+ if (row?.kind === "zone" && this.sectionsBase) {
5498
+ for (const s of this.sectionsBase.sections) if (s.zone === id) delete next[s.id];
5499
+ }
5500
+ void this.persistAvailability(next);
5501
+ }
5502
+ /** Edit a timed reveal time / threshold percent on an existing row rule. */
5503
+ setSectionRulePatch(id, patch) {
5504
+ const cur = this.availabilityRules[id];
5505
+ if (!cur) return;
5506
+ const row = this.buildSectionRows().rows.find((r) => r.id === id);
5507
+ const labels = row?.seatLabels ?? cur.labels ?? [];
5508
+ void this.persistAvailability({ ...this.availabilityRules, [id]: { ...cur, ...patch, labels } });
5509
+ }
5510
+ /** Optimistically adopt the new rules, then reconcile with the server-cleaned
5511
+ * map + effective hidden/closed sets. Rolls back the rules on failure. */
5512
+ async persistAvailability(next) {
5513
+ const prev = this.availabilityRules;
5514
+ this.availabilityRules = next;
5515
+ this.availabilitySaving = true;
5516
+ if (this.mode === "sections") this.renderSectionsRail();
5517
+ try {
5518
+ const res = await this.withAuthRetry(() => this.api.setAvailability(this.key, next));
5519
+ this.availabilityRules = res.rules;
5520
+ this.effectiveHidden = new Set(res.hidden);
5521
+ this.effectiveClosed = new Set(this.closedIdsFromRules(res.rules));
5522
+ this.availabilitySaving = false;
5523
+ if (this.mode === "sections") this.renderSectionsRail();
5524
+ this.applySectionCanvasTreatment();
5525
+ } catch (err) {
5526
+ this.availabilityRules = prev;
5527
+ this.availabilitySaving = false;
5528
+ if (this.mode === "sections") this.renderSectionsRail();
5529
+ this.toastErr("Couldn't update availability. Try again.");
5530
+ this.opts.onError?.(err);
5531
+ }
5532
+ }
4589
5533
  paintLegend(t3) {
4590
5534
  if (!this.els.legend) return;
4591
5535
  this.els.legend.innerHTML = LEGEND.map((l) => `<div class="slm-legrow"><span class="slm-legdot" style="background:${l.color}"></span>
@@ -4899,6 +5843,7 @@ var SeatManager = class {
4899
5843
  ManageApiError,
4900
5844
  SeatManager,
4901
5845
  SeatPicker,
4902
- SeatingChart
5846
+ SeatingChart,
5847
+ attachPickerFrame
4903
5848
  });
4904
5849
  //# sourceMappingURL=index.cjs.map