@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 +1008 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +262 -7
- package/dist/index.d.ts +262 -7
- package/dist/index.js +1007 -62
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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:
|
|
854
|
-
min-height:53px;
|
|
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:
|
|
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:
|
|
865
|
-
.sl-chip-main{min-width:0}
|
|
866
|
-
.sl-chip
|
|
867
|
-
.sl-chip-
|
|
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
|
|
871
|
-
.sl-chip .rm
|
|
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:
|
|
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.
|
|
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
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
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(
|
|
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
|
-
|
|
1649
|
-
cb.setAttribute("aria-pressed", String(
|
|
1650
|
-
this.controller.setColorblindSafe(
|
|
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
|
|
2525
|
+
/** Effective display price of a category: host pricing override → first tier → base. */
|
|
2083
2526
|
catPrice(c) {
|
|
2084
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
"&": "&",
|
|
2348
2796
|
"<": "<",
|
|
@@ -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
|
|
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 =
|
|
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
|
|
2577
|
-
const
|
|
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><
|
|
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><
|
|
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 =
|
|
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
|
|
2992
|
-
const
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
this.tipEl.innerHTML = `<div
|
|
3513
|
+
const esc2 = (v) => String(v ?? "\u2014").replace(/[&<>"]/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[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
|