@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.js
CHANGED
|
@@ -366,10 +366,30 @@ var EmbeddedDesigner = class {
|
|
|
366
366
|
this.timeoutTimer = null;
|
|
367
367
|
this.phase = "loading";
|
|
368
368
|
this.restoreContainerPosition = null;
|
|
369
|
+
// Host-side fullscreen pin: saved state we restore on `off`/Escape/destroy.
|
|
370
|
+
this.pinned = false;
|
|
371
|
+
this.frameStyleBeforeFs = null;
|
|
372
|
+
this.docOverflowBeforeFs = null;
|
|
373
|
+
this.bodyOverflowBeforeFs = null;
|
|
374
|
+
this.fsKeyHandler = null;
|
|
375
|
+
/** Latest height (px string) the Designer reported; re-applied after unpin. */
|
|
376
|
+
this.lastAutoHeight = "";
|
|
369
377
|
this.handleMessage = (event) => {
|
|
370
378
|
if (!this.frame || event.origin !== this.designerOrigin || event.source !== this.frame.contentWindow) return;
|
|
371
379
|
if (!event.data || typeof event.data !== "object") return;
|
|
372
380
|
const data = event.data;
|
|
381
|
+
if (data.type === "seatlayer.designer.resize") {
|
|
382
|
+
if (this.autoResizeEnabled() && typeof data.px === "number" && Number.isFinite(data.px) && data.px > 0) {
|
|
383
|
+
this.lastAutoHeight = `${Math.round(data.px)}px`;
|
|
384
|
+
if (!this.pinned) this.frame.style.height = this.lastAutoHeight;
|
|
385
|
+
}
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (data.type === "seatlayer.designer.fullscreen") {
|
|
389
|
+
if (data.on === true) this.pinFullscreen();
|
|
390
|
+
else if (data.on === false) this.unpinFullscreen();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
373
393
|
if (typeof data.type !== "string" || !TYPES.has(data.type)) return;
|
|
374
394
|
const message = {
|
|
375
395
|
type: data.type,
|
|
@@ -452,6 +472,7 @@ var EmbeddedDesigner = class {
|
|
|
452
472
|
}
|
|
453
473
|
destroy() {
|
|
454
474
|
window.removeEventListener("message", this.handleMessage);
|
|
475
|
+
this.unpinFullscreen();
|
|
455
476
|
this.clearTimeoutTimer();
|
|
456
477
|
this.removeOverlay();
|
|
457
478
|
this.restoreContainerStyle();
|
|
@@ -459,10 +480,68 @@ var EmbeddedDesigner = class {
|
|
|
459
480
|
this.frame = null;
|
|
460
481
|
this.designerOrigin = "";
|
|
461
482
|
this.phase = "loading";
|
|
483
|
+
this.lastAutoHeight = "";
|
|
462
484
|
}
|
|
463
485
|
loadingStateEnabled() {
|
|
464
486
|
return this.options.showLoadingState !== false;
|
|
465
487
|
}
|
|
488
|
+
autoResizeEnabled() {
|
|
489
|
+
return this.options.autoResize !== false;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Pin the iframe over the host page as a viewport-filling overlay. We save the
|
|
493
|
+
* iframe's inline style and the document scroll state so `unpinFullscreen`
|
|
494
|
+
* restores everything exactly. Escape (host-side) also exits.
|
|
495
|
+
*/
|
|
496
|
+
pinFullscreen() {
|
|
497
|
+
if (this.pinned || !this.frame) return;
|
|
498
|
+
this.pinned = true;
|
|
499
|
+
this.frameStyleBeforeFs = this.frame.getAttribute("style");
|
|
500
|
+
Object.assign(this.frame.style, {
|
|
501
|
+
position: "fixed",
|
|
502
|
+
inset: "0",
|
|
503
|
+
width: "100vw",
|
|
504
|
+
height: "100vh",
|
|
505
|
+
margin: "0",
|
|
506
|
+
border: "0",
|
|
507
|
+
zIndex: "2147483000",
|
|
508
|
+
background: "#101625"
|
|
509
|
+
});
|
|
510
|
+
const docEl = document.documentElement;
|
|
511
|
+
this.docOverflowBeforeFs = docEl.style.overflow;
|
|
512
|
+
docEl.style.overflow = "hidden";
|
|
513
|
+
if (document.body) {
|
|
514
|
+
this.bodyOverflowBeforeFs = document.body.style.overflow;
|
|
515
|
+
document.body.style.overflow = "hidden";
|
|
516
|
+
}
|
|
517
|
+
this.fsKeyHandler = (event) => {
|
|
518
|
+
if (event.key === "Escape") this.unpinFullscreen();
|
|
519
|
+
};
|
|
520
|
+
window.addEventListener("keydown", this.fsKeyHandler);
|
|
521
|
+
}
|
|
522
|
+
/** Undo `pinFullscreen`: restore the iframe style + scroll lock. Idempotent. */
|
|
523
|
+
unpinFullscreen() {
|
|
524
|
+
if (!this.pinned) return;
|
|
525
|
+
this.pinned = false;
|
|
526
|
+
if (this.frame) {
|
|
527
|
+
if (this.frameStyleBeforeFs === null) this.frame.removeAttribute("style");
|
|
528
|
+
else this.frame.setAttribute("style", this.frameStyleBeforeFs);
|
|
529
|
+
if (this.autoResizeEnabled() && this.lastAutoHeight) this.frame.style.height = this.lastAutoHeight;
|
|
530
|
+
}
|
|
531
|
+
this.frameStyleBeforeFs = null;
|
|
532
|
+
if (this.docOverflowBeforeFs !== null) {
|
|
533
|
+
document.documentElement.style.overflow = this.docOverflowBeforeFs;
|
|
534
|
+
this.docOverflowBeforeFs = null;
|
|
535
|
+
}
|
|
536
|
+
if (this.bodyOverflowBeforeFs !== null && document.body) {
|
|
537
|
+
document.body.style.overflow = this.bodyOverflowBeforeFs;
|
|
538
|
+
this.bodyOverflowBeforeFs = null;
|
|
539
|
+
}
|
|
540
|
+
if (this.fsKeyHandler) {
|
|
541
|
+
window.removeEventListener("keydown", this.fsKeyHandler);
|
|
542
|
+
this.fsKeyHandler = null;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
466
545
|
clearTimeoutTimer() {
|
|
467
546
|
if (this.timeoutTimer !== null) {
|
|
468
547
|
clearTimeout(this.timeoutTimer);
|
|
@@ -662,6 +741,7 @@ import {
|
|
|
662
741
|
PickerController as PickerController2,
|
|
663
742
|
expandChart,
|
|
664
743
|
generateSeatPanorama,
|
|
744
|
+
generateSeatThumb,
|
|
665
745
|
loadLocale as loadLocale2,
|
|
666
746
|
setStringOverrides as setStringOverrides2,
|
|
667
747
|
t as t2,
|
|
@@ -826,26 +906,43 @@ var CSS = `
|
|
|
826
906
|
.sl-tray{flex:1;padding:10px 14px 14px;display:flex;flex-direction:column;gap:7px;min-height:0;overflow-y:auto;
|
|
827
907
|
overscroll-behavior:contain;scrollbar-gutter:stable}
|
|
828
908
|
.sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
|
|
829
|
-
.sl-chip{position:relative;display:grid;grid-template-columns:
|
|
830
|
-
min-height:53px;
|
|
909
|
+
.sl-chip{position:relative;display:grid;grid-template-columns:minmax(0,1fr) 34px;align-items:stretch;
|
|
910
|
+
flex:none;min-height:53px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm);overflow:hidden;
|
|
831
911
|
background:var(--sl-surface);font-size:13px;transform-origin:center;transition:border-color .15s,background .15s}
|
|
832
912
|
.sl-chip:hover{border-color:color-mix(in srgb,var(--sl-accent) 38%,var(--sl-line))}
|
|
833
913
|
.sl-chip.sl-enter{animation:slChipIn .38s cubic-bezier(.2,.8,.2,1) both}
|
|
834
914
|
.sl-chip.sl-leave{pointer-events:none;animation:slChipOut .16s ease-in both}
|
|
835
915
|
.sl-chip.sl-held{border-color:var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
|
|
836
916
|
box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent)}
|
|
837
|
-
.sl-ticket-state{width:
|
|
917
|
+
.sl-ticket-state{width:17px;height:17px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
838
918
|
background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
839
919
|
.sl-ticket-state.held{background:color-mix(in srgb,var(--sl-accent) 18%,var(--sl-surface));color:var(--sl-accent)}
|
|
840
|
-
.sl-ticket-state svg{width:
|
|
841
|
-
.sl-chip-main{min-width:0}
|
|
842
|
-
.sl-chip
|
|
843
|
-
.sl-chip-
|
|
920
|
+
.sl-ticket-state svg{width:10px;height:10px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
921
|
+
.sl-chip-main{min-width:0;padding:8px 10px 8px 11px;display:flex;flex-direction:column;justify-content:center;gap:5px}
|
|
922
|
+
.sl-chip-id{display:flex;gap:12px;min-width:0}
|
|
923
|
+
.sl-chip-id .fld{min-width:0}
|
|
924
|
+
.sl-chip-id .fld.sec{flex:1}
|
|
925
|
+
.sl-chip-id .fld.mid{flex:none;text-align:center}
|
|
926
|
+
.sl-chip-eb{display:block;font-size:8px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);margin-bottom:1px}
|
|
927
|
+
.sl-chip-id .val{display:block;font-weight:600;font-size:13px;line-height:1.25;white-space:nowrap}
|
|
928
|
+
.sl-chip-id .fld.sec .val{overflow:hidden;text-overflow:ellipsis}
|
|
929
|
+
.sl-chip-sub{display:flex;align-items:center;gap:6px;min-width:0}
|
|
844
930
|
.sl-chip .cat{color:var(--sl-muted);font-size:10.5px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
845
931
|
.sl-chip .amt{font-weight:700;font-variant-numeric:tabular-nums;flex:none;white-space:nowrap}
|
|
846
|
-
.sl-chip
|
|
847
|
-
.sl-chip .rm
|
|
932
|
+
.sl-chip-rail{display:flex;flex-direction:column;border-left:1px solid var(--sl-line)}
|
|
933
|
+
.sl-chip .rm,.sl-chip .view{flex:1;min-height:26px;border-radius:0;display:flex;align-items:center;justify-content:center;
|
|
934
|
+
color:var(--sl-muted);transition:color .15s,background .15s}
|
|
935
|
+
.sl-chip .view{border-top:1px solid var(--sl-line)}
|
|
936
|
+
.sl-chip .rm:hover,.sl-chip .rm:focus-visible{color:#e5484d;background:color-mix(in srgb,#e5484d 9%,transparent)}
|
|
937
|
+
.sl-chip .view:hover,.sl-chip .view:focus-visible{color:var(--sl-text);background:color-mix(in srgb,var(--sl-accent) 10%,transparent)}
|
|
848
938
|
.sl-chip .rm svg{width:11px;height:11px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round}
|
|
939
|
+
.sl-chip .view svg{width:13px;height:13px;stroke:currentColor;stroke-width:1.8;fill:none}
|
|
940
|
+
/* live-activity strip \u2014 narrates WS availability deltas (social proof + urgency) */
|
|
941
|
+
.sl-live{display:flex;align-items:center;gap:7px;margin:10px 14px 0;padding:7px 9px;flex:none;
|
|
942
|
+
border:1px solid var(--sl-line);border-radius:8px;background:color-mix(in srgb,var(--sl-accent) 4%,var(--sl-surface));
|
|
943
|
+
font-size:11px;color:var(--sl-muted)}
|
|
944
|
+
.sl-live .dot{width:6px;height:6px;border-radius:999px;background:#22a06b;box-shadow:0 0 6px rgba(34,160,107,.75);flex:none}
|
|
945
|
+
.sl-live span:last-child{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
849
946
|
|
|
850
947
|
/* GA rows */
|
|
851
948
|
.sl-ga{display:flex;align-items:center;gap:10px;padding:9px 11px;border:1px dashed var(--sl-line);border-radius:var(--sl-r-sm)}
|
|
@@ -921,6 +1018,8 @@ var CSS = `
|
|
|
921
1018
|
|
|
922
1019
|
/* zoom column (flows within the bottom-right region) */
|
|
923
1020
|
.sl-zoom{display:flex;flex-direction:column;gap:6px}
|
|
1021
|
+
/* CSS-fallback full screen (iOS Safari has no element fullscreen API) */
|
|
1022
|
+
.sl-picker.sl-fs{position:fixed;inset:0;z-index:2147483000;width:auto;height:auto;max-height:none;border-radius:0}
|
|
924
1023
|
.sl-zoom button{width:36px;height:36px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
925
1024
|
color:var(--sl-text);font-size:17px;font-weight:700;display:flex;align-items:center;justify-content:center;transition:border-color .15s}
|
|
926
1025
|
.sl-zoom button:hover{border-color:var(--sl-muted)}
|
|
@@ -979,6 +1078,39 @@ var CSS = `
|
|
|
979
1078
|
.sl-booked.on .sl-booked-title{animation-delay:.22s}
|
|
980
1079
|
.sl-booked.on .sl-booked-sub{animation-delay:.3s}
|
|
981
1080
|
|
|
1081
|
+
/* sold-out overlay \u2014 every SEATED category's live availability is 0. Centered
|
|
1082
|
+
over the map; a stub (disabled) "Join waitlist" button, exactly like the page.
|
|
1083
|
+
Suppressed when GA areas exist (GA capacity isn't seat-counted). Clears live
|
|
1084
|
+
the moment WS frees a seat up. */
|
|
1085
|
+
.sl-soldout{position:absolute;inset:0;z-index:10;display:none;flex-direction:column;align-items:center;
|
|
1086
|
+
justify-content:center;text-align:center;gap:8px;padding:24px;
|
|
1087
|
+
background:color-mix(in srgb,var(--sl-bg) 82%,transparent);backdrop-filter:blur(4px)}
|
|
1088
|
+
.sl-soldout.on{display:flex}
|
|
1089
|
+
.sl-soldout-eyebrow{font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--sl-accent);font-weight:800}
|
|
1090
|
+
.sl-soldout-title{font-size:32px;font-weight:800;color:var(--sl-text);line-height:1.05}
|
|
1091
|
+
.sl-soldout-copy{max-width:360px;font-size:13px;color:var(--sl-muted);line-height:1.5}
|
|
1092
|
+
.sl-picker .sl-soldout-btn{margin-top:10px;min-height:40px;padding:10px 18px;border-radius:var(--sl-r-sm);
|
|
1093
|
+
background:var(--sl-surface);color:var(--sl-muted);border:1px solid var(--sl-line);font-weight:800;font-size:13px;
|
|
1094
|
+
cursor:not-allowed;opacity:.85}
|
|
1095
|
+
|
|
1096
|
+
/* sales-closed pill (header) \u2014 persistent read-only state when the event's sales
|
|
1097
|
+
window is closed at load or closes live mid-session. Neutral (not accent) so it
|
|
1098
|
+
reads as "unavailable", distinct from the accent hold pill next to it. */
|
|
1099
|
+
.sl-closed-pill{display:none;align-items:center;gap:6px;padding:6px 12px;border-radius:999px;flex:none;
|
|
1100
|
+
background:color-mix(in srgb,var(--sl-text) 12%,var(--sl-surface));color:var(--sl-text);
|
|
1101
|
+
font-weight:700;font-size:12px;white-space:nowrap}
|
|
1102
|
+
.sl-closed-pill.on{display:inline-flex}
|
|
1103
|
+
.sl-closed-pill svg{width:13px;height:13px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
1104
|
+
|
|
1105
|
+
/* "Powered by SeatLayer" attribution badge (side-panel foot) \u2014 the small gold
|
|
1106
|
+
rounded logo mark + wordmark. Hidden when the host opts out or the org's paid
|
|
1107
|
+
theme sets hideBadge. */
|
|
1108
|
+
.sl-powered{display:flex;align-items:center;justify-content:center;gap:6px;margin-top:10px;
|
|
1109
|
+
font-size:11px;letter-spacing:.03em;color:var(--sl-muted)}
|
|
1110
|
+
.sl-powered-mark{width:16px;height:16px;border-radius:4px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
1111
|
+
background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
1112
|
+
.sl-powered-mark svg{width:11px;height:11px;fill:currentColor}
|
|
1113
|
+
|
|
982
1114
|
/* a11y filter chips (flow within the top-left region) */
|
|
983
1115
|
.sl-chips{display:flex;gap:6px;flex-wrap:wrap}
|
|
984
1116
|
.sl-chip-f{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:999px;font-size:12px;font-weight:700;
|
|
@@ -1017,8 +1149,30 @@ var CSS = `
|
|
|
1017
1149
|
.sl-picker[data-layout="narrow"] .sl-confirm{left:50%!important;top:auto!important;bottom:14px;width:min(342px,calc(100% - 24px));
|
|
1018
1150
|
transform:translateX(-50%);animation:slConfirmMobileIn .24s cubic-bezier(.2,.8,.2,1) both}
|
|
1019
1151
|
|
|
1152
|
+
/* hover preview \u2014 a COMPACT echo of the confirm card (deliberately smaller: it's
|
|
1153
|
+
a passing preview on hover, not the click/select action surface). Reuses the
|
|
1154
|
+
Section\xB7Row\xB7Seat identity grid so hover, confirm and the cart chip all share
|
|
1155
|
+
one visual language, just at three sizes. */
|
|
1156
|
+
.sl-tip{position:absolute;z-index:7;pointer-events:none;display:none;width:190px;overflow:hidden;
|
|
1157
|
+
background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:11px;
|
|
1158
|
+
box-shadow:0 12px 30px -14px rgba(0,0,0,.6)}
|
|
1159
|
+
.sl-tip-grid{display:grid;grid-template-columns:1.3fr .85fr .85fr;border-bottom:1px solid var(--sl-line)}
|
|
1160
|
+
.sl-tip-grid.one{grid-template-columns:1fr}
|
|
1161
|
+
.sl-tip-field{min-width:0;padding:6px 9px;border-right:1px solid var(--sl-line)}
|
|
1162
|
+
.sl-tip-field:last-child{border-right:0;text-align:center}
|
|
1163
|
+
.sl-tip-grid:not(.one) .sl-tip-field:nth-child(2){text-align:center}
|
|
1164
|
+
.sl-tip-key{display:block;font-size:7.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
|
|
1165
|
+
.sl-tip-val{display:block;margin-top:2px;color:var(--sl-text);font-size:13px;line-height:1.1;font-weight:750;
|
|
1166
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1167
|
+
.sl-tip-cat{display:flex;align-items:center;gap:7px;padding:6px 10px;font-size:11px;
|
|
1168
|
+
background:color-mix(in srgb,var(--sl-cat) 12%,var(--sl-surface))}
|
|
1169
|
+
.sl-tip-dot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
1170
|
+
.sl-tip-name{color:var(--sl-muted);flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1171
|
+
.sl-tip-amt{margin-left:auto;font-weight:800;color:var(--sl-text);font-variant-numeric:tabular-nums;font-size:12px}
|
|
1172
|
+
.sl-tip-status{padding:5px 10px 7px;font-size:8.5px;letter-spacing:.09em;text-transform:uppercase;font-weight:700;color:var(--sl-muted)}
|
|
1173
|
+
|
|
1020
1174
|
/* Best available is a first-class shortcut, not an anonymous utility row. */
|
|
1021
|
-
.sl-ba{position:relative;overflow:hidden;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:7px;
|
|
1175
|
+
.sl-ba{position:relative;flex:none;overflow:hidden;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:7px;
|
|
1022
1176
|
padding:13px;border:1px solid color-mix(in srgb,var(--sl-accent) 34%,var(--sl-line));border-radius:13px;
|
|
1023
1177
|
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)))}
|
|
1024
1178
|
.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}
|
|
@@ -1054,12 +1208,6 @@ var CSS = `
|
|
|
1054
1208
|
/* per-seat ticket-tier select + view-from-seat button in tray chips */
|
|
1055
1209
|
.sl-chip .tier{background:var(--sl-bg);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:6px;
|
|
1056
1210
|
font:inherit;font-size:10px;padding:2px 4px;min-width:0;max-width:100%;cursor:pointer}
|
|
1057
|
-
.sl-chip .view{width:20px;height:20px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
1058
|
-
color:var(--sl-muted);opacity:.36;transition:color .15s,opacity .15s}
|
|
1059
|
-
.sl-chip .view:hover{color:var(--sl-text)}
|
|
1060
|
-
.sl-chip:hover .view,.sl-chip .view:focus-visible{opacity:1;color:var(--sl-text)}
|
|
1061
|
-
.sl-chip .view svg{width:12px;height:12px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
1062
|
-
@media(pointer:coarse){.sl-chip .view{opacity:.58}}
|
|
1063
1211
|
|
|
1064
1212
|
/* arena: LOD rung pills (flow within the top-center region) */
|
|
1065
1213
|
.sl-rungs{display:none;background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:999px;padding:3px}
|
|
@@ -1117,6 +1265,14 @@ var CSS = `
|
|
|
1117
1265
|
.sl-seccard-hint{font-size:10.5px;color:var(--sl-muted)}
|
|
1118
1266
|
|
|
1119
1267
|
/* view-from-seat button on the confirm popover */
|
|
1268
|
+
/* Eager sightline preview inside the confirm card */
|
|
1269
|
+
.sl-confirm-thumbwrap{position:relative;display:block;width:100%;height:74px;margin:0 0 8px;padding:0!important;
|
|
1270
|
+
border-radius:9px;overflow:hidden;border:1px solid var(--sl-line);cursor:pointer}
|
|
1271
|
+
.sl-confirm-thumb{display:block;width:100%;height:100%;object-fit:cover}
|
|
1272
|
+
.sl-confirm-thumb-badge{position:absolute;right:7px;top:7px;display:inline-flex;align-items:center;gap:5px;
|
|
1273
|
+
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)}
|
|
1274
|
+
.sl-confirm-sight{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--sl-muted);margin-bottom:2px}
|
|
1275
|
+
.sl-confirm-sight span{color:#22a06b;font-weight:800}
|
|
1120
1276
|
.sl-confirm-view{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
|
|
1121
1277
|
color:var(--sl-text);font-weight:700;font-size:12px;display:flex;align-items:center;justify-content:center;gap:7px}
|
|
1122
1278
|
.sl-confirm-view:hover{border-color:var(--sl-muted)}
|
|
@@ -1200,6 +1356,22 @@ function resolveTokens(chart, host) {
|
|
|
1200
1356
|
"--sl-radius": `${host?.radius ?? 14}px`
|
|
1201
1357
|
};
|
|
1202
1358
|
}
|
|
1359
|
+
var CB_STORAGE_KEY = "seatmap.a11y.cb";
|
|
1360
|
+
function readStoredColorblind() {
|
|
1361
|
+
try {
|
|
1362
|
+
if (typeof window === "undefined") return null;
|
|
1363
|
+
const raw = window.localStorage.getItem(CB_STORAGE_KEY);
|
|
1364
|
+
return raw == null ? null : raw === "1";
|
|
1365
|
+
} catch {
|
|
1366
|
+
return null;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
function writeStoredColorblind(on) {
|
|
1370
|
+
try {
|
|
1371
|
+
window.localStorage.setItem(CB_STORAGE_KEY, on ? "1" : "0");
|
|
1372
|
+
} catch {
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1203
1375
|
var SeatPicker = class _SeatPicker {
|
|
1204
1376
|
constructor(options) {
|
|
1205
1377
|
this.root = null;
|
|
@@ -1232,11 +1404,17 @@ var SeatPicker = class _SeatPicker {
|
|
|
1232
1404
|
this.confirmEl = null;
|
|
1233
1405
|
this.confirmSeat = null;
|
|
1234
1406
|
this.srEl = null;
|
|
1235
|
-
this.a11yFilter = "all";
|
|
1236
1407
|
this.baQty = 2;
|
|
1237
1408
|
this.baCat = "";
|
|
1238
1409
|
this.bestAvailableConfirm = false;
|
|
1239
1410
|
this.releasingHold = false;
|
|
1411
|
+
/** Event sales window is closed (read-only load state / live close). */
|
|
1412
|
+
this.salesClosed = false;
|
|
1413
|
+
/** Every seated category's live availability is 0 (sold-out overlay is up). */
|
|
1414
|
+
this.soldOut = false;
|
|
1415
|
+
this.soldoutEl = null;
|
|
1416
|
+
/** Resolved colorblind-safe state — stored preference wins over the option. */
|
|
1417
|
+
this.cbSafe = false;
|
|
1240
1418
|
// arena / multi-floor / seat-view chrome
|
|
1241
1419
|
this.rungsEl = null;
|
|
1242
1420
|
this.floorsEl = null;
|
|
@@ -1245,13 +1423,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1245
1423
|
this.viewCleanup = null;
|
|
1246
1424
|
this.allSeatsCache = null;
|
|
1247
1425
|
// F3 minimap
|
|
1248
|
-
this.miniEl = null;
|
|
1249
1426
|
this.miniCanvas = null;
|
|
1250
1427
|
this.miniBase = null;
|
|
1251
1428
|
this.miniTf = null;
|
|
1252
1429
|
// F4 price-band filter — active band's category keys (null = all prices)
|
|
1253
1430
|
this.priceBandKeys = null;
|
|
1254
|
-
this.priceFilterEl = null;
|
|
1255
1431
|
/** Last surfaced section summary (re-rendered when the price band changes). */
|
|
1256
1432
|
this.lastSection = null;
|
|
1257
1433
|
/** Section card collapsed to its slim pill (seat-picking has begun). */
|
|
@@ -1271,6 +1447,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
1271
1447
|
this.ctaPhase = "idle";
|
|
1272
1448
|
// narrow-layout chrome that docks into the sheet's Filters row on mobile
|
|
1273
1449
|
this.a11yChipsEl = null;
|
|
1450
|
+
this.fsFallback = false;
|
|
1451
|
+
this.fsChangeHandler = null;
|
|
1452
|
+
this.fsEscHandler = null;
|
|
1453
|
+
/** True once we've asked the host page to pin us fullscreen (framed, no native). */
|
|
1454
|
+
this.framedFs = false;
|
|
1455
|
+
/** Last height (px) posted to a host frame; dedupes redundant reports. */
|
|
1456
|
+
this.lastPostedHeight = 0;
|
|
1274
1457
|
this.cbEl = null;
|
|
1275
1458
|
// modal plumbing (set by open())
|
|
1276
1459
|
this.modalScrim = null;
|
|
@@ -1278,20 +1461,22 @@ var SeatPicker = class _SeatPicker {
|
|
|
1278
1461
|
this.escHandler = null;
|
|
1279
1462
|
/** Set by open(): closes the modal (scroll restore + destroy + onClose). */
|
|
1280
1463
|
this.closeModal = null;
|
|
1464
|
+
this.lastCatAvail = null;
|
|
1281
1465
|
if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
|
|
1282
1466
|
if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
|
|
1283
1467
|
if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
|
|
1284
1468
|
this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
|
|
1285
1469
|
this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
|
|
1286
|
-
this.api = new PubApi(this.apiBase);
|
|
1470
|
+
this.api = options.transport ?? new PubApi(this.apiBase);
|
|
1287
1471
|
this.maxTickets = Math.max(1, Math.floor(options.maxSelection ?? DEFAULT_MAX_SELECTION2));
|
|
1472
|
+
this.cbSafe = readStoredColorblind() ?? !!options.colorblindSafe;
|
|
1288
1473
|
this.controller = new PickerController2({
|
|
1289
1474
|
transport: this.api,
|
|
1290
1475
|
eventKey: options.event,
|
|
1291
1476
|
maxSelection: this.maxTickets,
|
|
1292
1477
|
currency: options.currency,
|
|
1293
1478
|
flashOnLiveChange: true,
|
|
1294
|
-
colorblindSafe:
|
|
1479
|
+
colorblindSafe: this.cbSafe,
|
|
1295
1480
|
onSelectionChange: () => {
|
|
1296
1481
|
this.syncTray();
|
|
1297
1482
|
if (this.committedSelection().length) this.collapseSectionCard();
|
|
@@ -1317,6 +1502,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1317
1502
|
},
|
|
1318
1503
|
confirmSelection: this.opts.confirmSelection,
|
|
1319
1504
|
onSelect: (seat) => {
|
|
1505
|
+
if (this.salesClosed) {
|
|
1506
|
+
this.controller.deselect([seat.id]);
|
|
1507
|
+
this.toast(this.tf("picker.salesClosedToast", "Sales are closed for this event."), "warning");
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1320
1510
|
this.flashPickedSeat(seat.id);
|
|
1321
1511
|
if (this.opts.confirmSelection) this.showConfirm(seat);
|
|
1322
1512
|
},
|
|
@@ -1339,9 +1529,142 @@ var SeatPicker = class _SeatPicker {
|
|
|
1339
1529
|
onHint: (m) => {
|
|
1340
1530
|
if (m) this.toast(m);
|
|
1341
1531
|
},
|
|
1532
|
+
// Server declared the event closed mid-session (409 event_closed) — keep
|
|
1533
|
+
// the toast (raised by handleCta), and add the persistent read-only state.
|
|
1534
|
+
onSalesClosed: () => this.setSalesClosed(true),
|
|
1342
1535
|
onError: (err) => this.opts.onError?.(err)
|
|
1343
1536
|
});
|
|
1344
1537
|
}
|
|
1538
|
+
/**
|
|
1539
|
+
* Eager sightline preview for the confirm card: a cheap generated forward
|
|
1540
|
+
* view (or the organizer's real photo) plus a "Nm to stage · clear
|
|
1541
|
+
* sightline" line — the premium at-a-glance moment; click opens the 360.
|
|
1542
|
+
*/
|
|
1543
|
+
confirmThumbHtml(seat) {
|
|
1544
|
+
const doc = this.controller.doc;
|
|
1545
|
+
if (!doc) return "";
|
|
1546
|
+
let url = seat.viewUrl ?? "";
|
|
1547
|
+
let distance = null;
|
|
1548
|
+
if (!url) {
|
|
1549
|
+
try {
|
|
1550
|
+
const thumb = generateSeatThumb(seat, doc.focalPoint);
|
|
1551
|
+
url = thumb.url;
|
|
1552
|
+
distance = thumb.distanceM ?? null;
|
|
1553
|
+
} catch {
|
|
1554
|
+
return "";
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
const sight = distance != null ? `${distance}${this.tf("picker.sightline", "m to stage \xB7 clear sightline")}` : this.tf("picker.sightlineClear", "Clear sightline");
|
|
1558
|
+
return `<button type="button" class="sl-confirm-view sl-confirm-thumbwrap" aria-label="${t2("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>`;
|
|
1559
|
+
}
|
|
1560
|
+
/** True when the picker is rendered inside an iframe (snippet embed at /e/:key). */
|
|
1561
|
+
isFramed() {
|
|
1562
|
+
return typeof window !== "undefined" && window.parent !== window;
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Post a widget→host message when framed. targetOrigin is '*' because the
|
|
1566
|
+
* payload carries nothing sensitive (a height number / a fullscreen flag);
|
|
1567
|
+
* hosts verify `event.origin` on their side (see `attachPickerFrame`).
|
|
1568
|
+
*/
|
|
1569
|
+
postToHost(message) {
|
|
1570
|
+
if (!this.isFramed()) return;
|
|
1571
|
+
try {
|
|
1572
|
+
window.parent.postMessage(message, "*");
|
|
1573
|
+
} catch {
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Height (px) to advertise to a host frame.
|
|
1578
|
+
*
|
|
1579
|
+
* The picker fills whatever box it's given: `.sl-picker` is `height:100%;
|
|
1580
|
+
* overflow:hidden`, and the /e/:key shell mounts it `position:fixed; inset:0`.
|
|
1581
|
+
* So it has no intrinsic *document* height to read — `scrollHeight` just
|
|
1582
|
+
* collapses to the current viewport, which for a framed embed would echo the
|
|
1583
|
+
* host's own iframe height straight back (a circular value). We therefore
|
|
1584
|
+
* report a width-driven *desired* height: a pleasant landscape box on desktop,
|
|
1585
|
+
* taller on narrow widths where the bottom sheet needs room, clamped to the
|
|
1586
|
+
* widget's `min-height` of 420. Width is host-controlled and never moves in
|
|
1587
|
+
* response to the height we report, so this cannot feedback-loop.
|
|
1588
|
+
*/
|
|
1589
|
+
measureFramedHeight() {
|
|
1590
|
+
const root = this.root;
|
|
1591
|
+
if (!root) return 0;
|
|
1592
|
+
const width = root.clientWidth || (typeof window !== "undefined" ? window.innerWidth : 0) || 0;
|
|
1593
|
+
if (width <= 0) return 0;
|
|
1594
|
+
const ratio = width < 640 ? 1.2 : 0.62;
|
|
1595
|
+
return Math.max(420, Math.round(width * ratio));
|
|
1596
|
+
}
|
|
1597
|
+
/** Post `seatlayer:height` to the host when framed and the value changed. */
|
|
1598
|
+
reportFramedHeight() {
|
|
1599
|
+
if (!this.isFramed()) return;
|
|
1600
|
+
const px = this.measureFramedHeight();
|
|
1601
|
+
if (px <= 0 || px === this.lastPostedHeight) return;
|
|
1602
|
+
this.lastPostedHeight = px;
|
|
1603
|
+
this.postToHost({ type: "seatlayer:height", px });
|
|
1604
|
+
}
|
|
1605
|
+
/** Full screen via the native API, falling back to a fixed-position overlay (iOS Safari). */
|
|
1606
|
+
toggleFullscreen() {
|
|
1607
|
+
const root = this.root;
|
|
1608
|
+
if (!root) return;
|
|
1609
|
+
const active = !!document.fullscreenElement || this.fsFallback || this.framedFs;
|
|
1610
|
+
if (!active) {
|
|
1611
|
+
if (root.requestFullscreen) {
|
|
1612
|
+
root.requestFullscreen().catch(() => this.enterFsFallback());
|
|
1613
|
+
} else {
|
|
1614
|
+
this.enterFsFallback();
|
|
1615
|
+
}
|
|
1616
|
+
} else if (document.fullscreenElement) {
|
|
1617
|
+
void document.exitFullscreen().catch(() => {
|
|
1618
|
+
});
|
|
1619
|
+
} else if (this.framedFs) {
|
|
1620
|
+
this.setFramedFs(false);
|
|
1621
|
+
} else {
|
|
1622
|
+
this.setFsFallback(false);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Native element-fullscreen was unavailable or rejected. When framed, a CSS
|
|
1627
|
+
* `.sl-fs` overlay can't escape the iframe, so we ask the host page to pin us
|
|
1628
|
+
* (`seatlayer:fullscreen`). Otherwise (iOS Safari, same document) fall back to
|
|
1629
|
+
* the `.sl-fs` overlay as before.
|
|
1630
|
+
*/
|
|
1631
|
+
enterFsFallback() {
|
|
1632
|
+
if (this.isFramed()) this.setFramedFs(true);
|
|
1633
|
+
else this.setFsFallback(true);
|
|
1634
|
+
}
|
|
1635
|
+
/** Toggle host-driven (framed) fullscreen: post the flag + own the Esc key. */
|
|
1636
|
+
setFramedFs(on) {
|
|
1637
|
+
if (this.framedFs === on) return;
|
|
1638
|
+
this.framedFs = on;
|
|
1639
|
+
this.els.zfs?.setAttribute("aria-pressed", String(on || !!document.fullscreenElement));
|
|
1640
|
+
this.postToHost({ type: "seatlayer:fullscreen", on });
|
|
1641
|
+
if (on && !this.fsEscHandler) {
|
|
1642
|
+
this.fsEscHandler = (e) => {
|
|
1643
|
+
if (e.key === "Escape" && !document.fullscreenElement) this.setFramedFs(false);
|
|
1644
|
+
};
|
|
1645
|
+
window.addEventListener("keydown", this.fsEscHandler);
|
|
1646
|
+
} else if (!on && this.fsEscHandler) {
|
|
1647
|
+
window.removeEventListener("keydown", this.fsEscHandler);
|
|
1648
|
+
this.fsEscHandler = null;
|
|
1649
|
+
}
|
|
1650
|
+
requestAnimationFrame(() => this.controller.zoomToFit());
|
|
1651
|
+
}
|
|
1652
|
+
setFsFallback(on) {
|
|
1653
|
+
if (this.fsFallback === on) return;
|
|
1654
|
+
this.fsFallback = on;
|
|
1655
|
+
this.root?.classList.toggle("sl-fs", on);
|
|
1656
|
+
this.els.zfs?.setAttribute("aria-pressed", String(on || !!document.fullscreenElement));
|
|
1657
|
+
if (on && !this.fsEscHandler) {
|
|
1658
|
+
this.fsEscHandler = (e) => {
|
|
1659
|
+
if (e.key === "Escape" && !document.fullscreenElement) this.setFsFallback(false);
|
|
1660
|
+
};
|
|
1661
|
+
window.addEventListener("keydown", this.fsEscHandler);
|
|
1662
|
+
} else if (!on && this.fsEscHandler) {
|
|
1663
|
+
window.removeEventListener("keydown", this.fsEscHandler);
|
|
1664
|
+
this.fsEscHandler = null;
|
|
1665
|
+
}
|
|
1666
|
+
requestAnimationFrame(() => this.controller.zoomToFit());
|
|
1667
|
+
}
|
|
1345
1668
|
/**
|
|
1346
1669
|
* Close the picker. In modal mode (SeatPicker.open()) this dismisses the
|
|
1347
1670
|
* modal exactly like ESC/scrim/✕ — restores page scroll and fires onClose.
|
|
@@ -1436,6 +1759,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
1436
1759
|
<div class="sl-head-meta" data-ref="meta"></div>
|
|
1437
1760
|
</div>
|
|
1438
1761
|
<span class="sl-hold-pill" data-ref="hold"></span>
|
|
1762
|
+
<span class="sl-closed-pill" data-ref="closedPill" role="status">
|
|
1763
|
+
<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>
|
|
1764
|
+
<span data-ref="closedPillText"></span>
|
|
1765
|
+
</span>
|
|
1439
1766
|
<button type="button" class="sl-close" data-ref="close" aria-label="Close">
|
|
1440
1767
|
<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>
|
|
1441
1768
|
</button>
|
|
@@ -1449,6 +1776,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
1449
1776
|
<button type="button" aria-label="Fit to screen" data-ref="zfit">
|
|
1450
1777
|
<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>
|
|
1451
1778
|
</button>
|
|
1779
|
+
<button type="button" aria-label="Full screen" aria-pressed="false" data-ref="zfs">
|
|
1780
|
+
<svg viewBox="0 0 24 24"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
|
|
1781
|
+
</button>
|
|
1452
1782
|
</div>
|
|
1453
1783
|
<div class="sl-boot" data-ref="boot"><span class="sl-boot-spin"></span>Loading seat map\u2026</div>
|
|
1454
1784
|
<div class="sl-toast" data-ref="toast" role="status" aria-live="polite"></div>
|
|
@@ -1467,6 +1797,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1467
1797
|
<div class="sl-filters" data-ref="filters"></div>
|
|
1468
1798
|
<div class="sl-sec sl-prices-sec" data-ref="pricesSec"><span>Ticket prices</span></div>
|
|
1469
1799
|
<div class="sl-prices" data-ref="prices"></div>
|
|
1800
|
+
<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>
|
|
1470
1801
|
<div class="sl-sec sl-seats-sec"><span>Your seats</span><span class="sl-seat-summary" data-ref="seatSummary"></span></div>
|
|
1471
1802
|
<div class="sl-tray" data-ref="tray"></div>
|
|
1472
1803
|
<div class="sl-foot" data-ref="foot">
|
|
@@ -1487,6 +1818,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1487
1818
|
const applyLayout = () => {
|
|
1488
1819
|
const w = root.clientWidth;
|
|
1489
1820
|
if (w <= 0) return;
|
|
1821
|
+
this.reportFramedHeight();
|
|
1490
1822
|
const next = w < 640 ? "narrow" : "wide";
|
|
1491
1823
|
if (root.dataset.layout === next) return;
|
|
1492
1824
|
root.dataset.layout = next;
|
|
@@ -1500,6 +1832,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
1500
1832
|
this.els.zin.addEventListener("click", () => this.controller.zoomIn());
|
|
1501
1833
|
this.els.zout.addEventListener("click", () => this.controller.zoomOut());
|
|
1502
1834
|
this.els.zfit.addEventListener("click", () => this.controller.zoomToFit());
|
|
1835
|
+
this.els.zfs.addEventListener("click", () => this.toggleFullscreen());
|
|
1836
|
+
this.fsChangeHandler = () => {
|
|
1837
|
+
if (!document.fullscreenElement) this.setFsFallback(false);
|
|
1838
|
+
this.els.zfs?.setAttribute("aria-pressed", String(!!document.fullscreenElement || this.fsFallback || this.framedFs));
|
|
1839
|
+
requestAnimationFrame(() => this.controller.zoomToFit());
|
|
1840
|
+
};
|
|
1841
|
+
document.addEventListener("fullscreenchange", this.fsChangeHandler);
|
|
1503
1842
|
const head = this.els.sheetHead;
|
|
1504
1843
|
if (head) {
|
|
1505
1844
|
const toggle = this.els.sheetToggle;
|
|
@@ -1543,7 +1882,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1543
1882
|
}
|
|
1544
1883
|
this.tipEl = document.createElement("div");
|
|
1545
1884
|
this.tipEl.setAttribute("role", "tooltip");
|
|
1546
|
-
this.tipEl.
|
|
1885
|
+
this.tipEl.className = "sl-tip";
|
|
1547
1886
|
this.els.map.appendChild(this.tipEl);
|
|
1548
1887
|
this.els.map.addEventListener("mousemove", (e) => {
|
|
1549
1888
|
const r = this.els.map.getBoundingClientRect();
|
|
@@ -1568,6 +1907,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1568
1907
|
return this;
|
|
1569
1908
|
}
|
|
1570
1909
|
this.els.boot.remove();
|
|
1910
|
+
this.salesClosed = !!info.salesClosed;
|
|
1571
1911
|
this.buildRegions();
|
|
1572
1912
|
this.regions["bottom-right"].appendChild(this.els.zoom);
|
|
1573
1913
|
this.regions["bottom-center"].appendChild(this.els.toast);
|
|
@@ -1587,6 +1927,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1587
1927
|
this.els.name.textContent = info.eventName ?? "";
|
|
1588
1928
|
const when = info.startsAt ? new Date(info.startsAt).toLocaleString(this.opts.locale, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "";
|
|
1589
1929
|
this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
|
|
1930
|
+
this.buildBadge(chartTheme);
|
|
1590
1931
|
const present = /* @__PURE__ */ new Set();
|
|
1591
1932
|
if (this.controller.doc) {
|
|
1592
1933
|
for (const seat of expandChart(this.controller.doc)) {
|
|
@@ -1602,12 +1943,29 @@ var SeatPicker = class _SeatPicker {
|
|
|
1602
1943
|
chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
|
|
1603
1944
|
this.regions["top-left"].appendChild(chips);
|
|
1604
1945
|
this.a11yChipsEl = chips;
|
|
1946
|
+
const active = /* @__PURE__ */ new Set();
|
|
1947
|
+
const syncChips = () => {
|
|
1948
|
+
chips.querySelectorAll("button").forEach((b) => {
|
|
1949
|
+
const f = b.dataset.f;
|
|
1950
|
+
const on = f === "all" ? active.size === 0 : active.has(f);
|
|
1951
|
+
b.classList.toggle("on", on);
|
|
1952
|
+
b.setAttribute("aria-pressed", String(on));
|
|
1953
|
+
});
|
|
1954
|
+
const filter = active.size ? [...active] : null;
|
|
1955
|
+
this.controller.setAccessibilityFilter(filter);
|
|
1956
|
+
if (filter && this.rungsEl && this.controller.getRung() !== "seats") {
|
|
1957
|
+
this.controller.setRung("seats");
|
|
1958
|
+
this.collapseSectionCard();
|
|
1959
|
+
this.syncRung();
|
|
1960
|
+
}
|
|
1961
|
+
};
|
|
1605
1962
|
chips.querySelectorAll("button").forEach((btn) => {
|
|
1606
1963
|
btn.addEventListener("click", () => {
|
|
1607
1964
|
const f = btn.dataset.f;
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1965
|
+
if (f === "all") active.clear();
|
|
1966
|
+
else if (active.has(f)) active.delete(f);
|
|
1967
|
+
else active.add(f);
|
|
1968
|
+
syncChips();
|
|
1611
1969
|
});
|
|
1612
1970
|
});
|
|
1613
1971
|
}
|
|
@@ -1616,14 +1974,14 @@ var SeatPicker = class _SeatPicker {
|
|
|
1616
1974
|
cb.className = "sl-cbbtn";
|
|
1617
1975
|
this.cbEl = cb;
|
|
1618
1976
|
cb.setAttribute("aria-label", "Toggle colorblind-friendly colors");
|
|
1619
|
-
cb.setAttribute("aria-pressed", String(
|
|
1977
|
+
cb.setAttribute("aria-pressed", String(this.cbSafe));
|
|
1620
1978
|
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>';
|
|
1621
1979
|
this.els.zfit.parentElement.appendChild(cb);
|
|
1622
|
-
let cbOn = !!this.opts.colorblindSafe;
|
|
1623
1980
|
cb.addEventListener("click", () => {
|
|
1624
|
-
|
|
1625
|
-
cb.setAttribute("aria-pressed", String(
|
|
1626
|
-
this.controller.setColorblindSafe(
|
|
1981
|
+
this.cbSafe = !this.cbSafe;
|
|
1982
|
+
cb.setAttribute("aria-pressed", String(this.cbSafe));
|
|
1983
|
+
this.controller.setColorblindSafe(this.cbSafe);
|
|
1984
|
+
writeStoredColorblind(this.cbSafe);
|
|
1627
1985
|
});
|
|
1628
1986
|
this.srEl = document.createElement("div");
|
|
1629
1987
|
this.srEl.className = "sl-sr";
|
|
@@ -1634,9 +1992,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1634
1992
|
this.buildPriceFilter();
|
|
1635
1993
|
this.buildExtendPrompt();
|
|
1636
1994
|
this.buildBookedOverlay();
|
|
1995
|
+
this.buildSoldoutOverlay();
|
|
1637
1996
|
this.dockLayoutChrome();
|
|
1638
1997
|
await this.restoreRememberedHold();
|
|
1639
1998
|
if (this.destroyed) return this;
|
|
1999
|
+
if (this.salesClosed) this.applySalesClosed();
|
|
1640
2000
|
this.syncPrices();
|
|
1641
2001
|
this.syncTray();
|
|
1642
2002
|
return this;
|
|
@@ -1688,6 +2048,85 @@ var SeatPicker = class _SeatPicker {
|
|
|
1688
2048
|
this.bookedEl = el;
|
|
1689
2049
|
this.els.bookedSub = el.querySelector('[data-ref="bookedSub"]');
|
|
1690
2050
|
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Localized string with a literal fallback. `t()` returns the key itself for
|
|
2053
|
+
* unknown keys, so this collapses that to `fallback` — while still honoring a
|
|
2054
|
+
* host `messages` override (which makes `t()` return the override, not the key).
|
|
2055
|
+
*/
|
|
2056
|
+
tf(key, fallback) {
|
|
2057
|
+
const v = t2(key);
|
|
2058
|
+
return v === key ? fallback : v;
|
|
2059
|
+
}
|
|
2060
|
+
/** Sold-out overlay — centered over the map, disabled waitlist stub (Gap 2). */
|
|
2061
|
+
buildSoldoutOverlay() {
|
|
2062
|
+
if (!this.els.map) return;
|
|
2063
|
+
const el = document.createElement("div");
|
|
2064
|
+
el.className = "sl-soldout";
|
|
2065
|
+
el.setAttribute("role", "status");
|
|
2066
|
+
const name = (this.controller.doc?.theme?.brandName ?? this.opts.theme?.brandName ?? this.els.name?.textContent ?? this.tf("picker.soldOutEyebrow", "This event")).toUpperCase();
|
|
2067
|
+
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>`;
|
|
2068
|
+
this.els.map.appendChild(el);
|
|
2069
|
+
this.soldoutEl = el;
|
|
2070
|
+
}
|
|
2071
|
+
/**
|
|
2072
|
+
* Recompute the sold-out state on every price/availability sync. Sold-out ⇔
|
|
2073
|
+
* every SEATED category's live free count is 0. Suppressed when the chart has
|
|
2074
|
+
* GA areas (GA capacity isn't per-seat, so seated counts would read 0 and
|
|
2075
|
+
* falsely block standing room) — mirrors the public page. Clears live when WS
|
|
2076
|
+
* frees a seat up.
|
|
2077
|
+
*/
|
|
2078
|
+
syncSoldout(categories, left) {
|
|
2079
|
+
const hasGA = this.controller.getGAAreas().length > 0;
|
|
2080
|
+
const soldOut = this.isSoldOut(categories, left, hasGA);
|
|
2081
|
+
if (soldOut === this.soldOut) return;
|
|
2082
|
+
this.soldOut = soldOut;
|
|
2083
|
+
this.soldoutEl?.classList.toggle("on", soldOut);
|
|
2084
|
+
}
|
|
2085
|
+
/**
|
|
2086
|
+
* Pure sold-out predicate: every SEATED category's free count is 0, there is at
|
|
2087
|
+
* least one seated category, and there are no GA areas (GA capacity isn't
|
|
2088
|
+
* per-seat, so seated counts read 0 and would falsely block standing room).
|
|
2089
|
+
* `left` is seeded implicitly — a missing key means a fully-booked tier (0 free).
|
|
2090
|
+
*/
|
|
2091
|
+
isSoldOut(categories, left, hasGA) {
|
|
2092
|
+
return !hasGA && categories.length > 0 && categories.every((c) => (left[c.key] ?? 0) === 0);
|
|
2093
|
+
}
|
|
2094
|
+
/**
|
|
2095
|
+
* Sales-closed read-only state (Gap 3): persistent header pill, disabled CTA
|
|
2096
|
+
* with a closed label, and frozen best-available / GA controls. `setSalesClosed`
|
|
2097
|
+
* is the reactive entry (live 409 event_closed); `applySalesClosed` is the
|
|
2098
|
+
* idempotent DOM apply used at load and on transition.
|
|
2099
|
+
*/
|
|
2100
|
+
setSalesClosed(closed) {
|
|
2101
|
+
if (this.salesClosed === closed) return;
|
|
2102
|
+
this.salesClosed = closed;
|
|
2103
|
+
this.applySalesClosed();
|
|
2104
|
+
}
|
|
2105
|
+
applySalesClosed() {
|
|
2106
|
+
const pill = this.els.closedPill;
|
|
2107
|
+
if (pill) {
|
|
2108
|
+
pill.classList.toggle("on", this.salesClosed);
|
|
2109
|
+
const text = this.els.closedPillText ?? pill;
|
|
2110
|
+
text.textContent = this.tf("picker.salesClosedPill", "Sales are closed");
|
|
2111
|
+
}
|
|
2112
|
+
this.root?.setAttribute("data-sales-closed", String(this.salesClosed));
|
|
2113
|
+
this.syncCta();
|
|
2114
|
+
this.syncTray();
|
|
2115
|
+
}
|
|
2116
|
+
/** The badge is hidden when the host opts out OR the org's theme sets hideBadge. */
|
|
2117
|
+
badgeHidden(chartTheme) {
|
|
2118
|
+
return !!(this.opts.hideBadge || chartTheme?.hideBadge);
|
|
2119
|
+
}
|
|
2120
|
+
/** Attribution badge in the side-panel foot (Gap 7). Hidden per host/theme. */
|
|
2121
|
+
buildBadge(chartTheme) {
|
|
2122
|
+
if (this.badgeHidden(chartTheme)) return;
|
|
2123
|
+
const foot = this.els.foot;
|
|
2124
|
+
if (!foot) return;
|
|
2125
|
+
const el = document.createElement("div");
|
|
2126
|
+
el.className = "sl-powered";
|
|
2127
|
+
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>`;
|
|
2128
|
+
foot.appendChild(el);
|
|
2129
|
+
}
|
|
1691
2130
|
// ---- Feature 6: chrome anchor regions -------------------------------------
|
|
1692
2131
|
/**
|
|
1693
2132
|
* Create the positioned flex containers that own every persistent map overlay.
|
|
@@ -1800,13 +2239,18 @@ var SeatPicker = class _SeatPicker {
|
|
|
1800
2239
|
heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
|
|
1801
2240
|
}
|
|
1802
2241
|
return gaAreas.reduce(
|
|
1803
|
-
(sum, area) => sum + area.price * Math.max(0, (this.gaQty.get(area.id) ?? 0) - (heldGA.get(area.id) ?? 0)),
|
|
2242
|
+
(sum, area) => sum + this.paidPrice(area.categoryKey, null, area.price) * Math.max(0, (this.gaQty.get(area.id) ?? 0) - (heldGA.get(area.id) ?? 0)),
|
|
1804
2243
|
0
|
|
1805
2244
|
);
|
|
1806
2245
|
}
|
|
1807
2246
|
syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
|
|
1808
2247
|
const cta = this.els.cta;
|
|
1809
2248
|
if (!cta) return;
|
|
2249
|
+
if (this.salesClosed) {
|
|
2250
|
+
cta.disabled = true;
|
|
2251
|
+
cta.textContent = this.tf("picker.salesClosedCta", "Sales closed");
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
1810
2254
|
if (this.confirmSeat) {
|
|
1811
2255
|
cta.disabled = true;
|
|
1812
2256
|
cta.textContent = "Confirm or cancel this seat";
|
|
@@ -1942,7 +2386,6 @@ var SeatPicker = class _SeatPicker {
|
|
|
1942
2386
|
canvas.style.height = `${h}px`;
|
|
1943
2387
|
wrap.appendChild(canvas);
|
|
1944
2388
|
(this.regions["bottom-left"] ?? this.els.map).appendChild(wrap);
|
|
1945
|
-
this.miniEl = wrap;
|
|
1946
2389
|
this.miniCanvas = canvas;
|
|
1947
2390
|
const scale = Math.min((w - PAD * 2) / Math.max(1, b.width), (h - PAD * 2) / Math.max(1, b.height)) * dpr;
|
|
1948
2391
|
const offX = (w * dpr - b.width * scale) / 2 - b.x * scale;
|
|
@@ -2055,9 +2498,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
2055
2498
|
this.controller.overview();
|
|
2056
2499
|
}
|
|
2057
2500
|
// ---- F4 price-band filter -------------------------------------------------
|
|
2058
|
-
/** Effective price of a category
|
|
2501
|
+
/** Effective display price of a category: host pricing override → first tier → base. */
|
|
2059
2502
|
catPrice(c) {
|
|
2060
|
-
|
|
2503
|
+
const chart = c.tiers?.length ? c.tiers[0].price : c.price;
|
|
2504
|
+
if (chart === void 0 || !c.key) return chart;
|
|
2505
|
+
return this.paidPrice(c.key, c.tiers?.[0]?.id ?? null, chart);
|
|
2061
2506
|
}
|
|
2062
2507
|
/** Derive price bands: one chip per distinct price (≤5), else quantile ranges. */
|
|
2063
2508
|
priceBands() {
|
|
@@ -2102,7 +2547,6 @@ var SeatPicker = class _SeatPicker {
|
|
|
2102
2547
|
select.setAttribute("aria-label", "Filter and focus seats by price");
|
|
2103
2548
|
select.innerHTML = `<option value="all">All prices</option>` + bands.map((band) => `<option value="${band.id}">${band.label}</option>`).join("");
|
|
2104
2549
|
this.els.pricesSec.appendChild(select);
|
|
2105
|
-
this.priceFilterEl = select;
|
|
2106
2550
|
select.addEventListener("change", () => {
|
|
2107
2551
|
const band = bands.find((candidate) => candidate.id === select.value);
|
|
2108
2552
|
const keys = band?.keys ?? null;
|
|
@@ -2206,7 +2650,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
2206
2650
|
renderSectionCard(summary) {
|
|
2207
2651
|
if (!this.els.map) return;
|
|
2208
2652
|
this.secCardEl?.remove();
|
|
2209
|
-
const
|
|
2653
|
+
const paid = summary.categories.length ? summary.categories.map((c) => this.paidPrice(c.key, null, c.price)) : [summary.priceMin, summary.priceMax];
|
|
2654
|
+
const paidMin = Math.min(...paid);
|
|
2655
|
+
const paidMax = Math.max(...paid);
|
|
2656
|
+
const priceLabel = paidMin === paidMax ? this.money(paidMin) : `${this.money(paidMin)}\u2013${this.money(paidMax)}`;
|
|
2210
2657
|
const leftLabel = tCount("picker.seatsLeftInSection", summary.seatsLeft);
|
|
2211
2658
|
const xBtn = `<button type="button" class="sl-seccard-x" aria-label="${t2("picker.closeSectionSummary")}">\u2715</button>`;
|
|
2212
2659
|
const card = document.createElement("div");
|
|
@@ -2236,7 +2683,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2236
2683
|
card.setAttribute("aria-label", t2("picker.sectionSummaryAria", { label: summary.label }));
|
|
2237
2684
|
const mix = summary.categories.map((c) => {
|
|
2238
2685
|
const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
|
|
2239
|
-
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>`;
|
|
2686
|
+
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>`;
|
|
2240
2687
|
}).join("");
|
|
2241
2688
|
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 ${t2("picker.overview")}</button><span class="sl-seccard-hint">${t2("picker.tapSeatHint")}</span></div>`;
|
|
2242
2689
|
card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
|
|
@@ -2303,7 +2750,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2303
2750
|
const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
|
|
2304
2751
|
const status = this.controller.getStatus(seat.id) ?? "free";
|
|
2305
2752
|
const statusText = status === "free" ? "available" : status === "held" ? "on hold" : "taken";
|
|
2306
|
-
const price = cat
|
|
2753
|
+
const price = cat ? this.catPrice(cat) : void 0;
|
|
2307
2754
|
this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
|
|
2308
2755
|
}
|
|
2309
2756
|
// ---- seat candidate confirmation ------------------------------------------
|
|
@@ -2318,7 +2765,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
2318
2765
|
if (this.tipEl) this.tipEl.style.display = "none";
|
|
2319
2766
|
const details = this.controller.seatDetails(seat.id);
|
|
2320
2767
|
const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
|
|
2321
|
-
const
|
|
2768
|
+
const chartPrice = details?.price ?? (cat?.tiers?.length ? cat.tiers[0].price : cat?.price);
|
|
2769
|
+
const price = chartPrice != null ? this.paidPrice(seat.categoryKey, details?.tierId ?? cat?.tiers?.[0]?.id ?? null, chartPrice) : void 0;
|
|
2322
2770
|
const safe = (value) => String(value ?? "\u2014").replace(/[&<>"]/g, (char) => ({
|
|
2323
2771
|
"&": "&",
|
|
2324
2772
|
"<": "<",
|
|
@@ -2331,7 +2779,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2331
2779
|
el.setAttribute("aria-modal", "true");
|
|
2332
2780
|
el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
|
|
2333
2781
|
el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
|
|
2334
|
-
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
|
|
2782
|
+
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>`;
|
|
2335
2783
|
this.els.map.appendChild(el);
|
|
2336
2784
|
this.confirmEl = el;
|
|
2337
2785
|
this.reanchorConfirm();
|
|
@@ -2496,18 +2944,35 @@ var SeatPicker = class _SeatPicker {
|
|
|
2496
2944
|
}
|
|
2497
2945
|
// ---- chrome sync ----------------------------------------------------------
|
|
2498
2946
|
money(n) {
|
|
2947
|
+
const formatter = this.opts.pricing?.formatter;
|
|
2948
|
+
if (formatter) return formatter(n, this.currency);
|
|
2499
2949
|
try {
|
|
2500
2950
|
return new Intl.NumberFormat(this.opts.locale, { style: "currency", currency: this.currency }).format(n);
|
|
2501
2951
|
} catch {
|
|
2502
2952
|
return `${n} ${this.currency}`;
|
|
2503
2953
|
}
|
|
2504
2954
|
}
|
|
2955
|
+
/**
|
|
2956
|
+
* The price the buyer will actually pay for a category (+tier): the host's
|
|
2957
|
+
* `pricing` override when present, else the chart's stored price. Every
|
|
2958
|
+
* price the widget DISPLAYS or hands off must flow through here — a map
|
|
2959
|
+
* that shows one price while checkout charges another destroys trust.
|
|
2960
|
+
*/
|
|
2961
|
+
paidPrice(categoryKey, tierId, fallback) {
|
|
2962
|
+
const entry = categoryKey ? this.opts.pricing?.prices?.[categoryKey] : void 0;
|
|
2963
|
+
if (entry === void 0) return fallback;
|
|
2964
|
+
if (typeof entry === "number") return entry;
|
|
2965
|
+
if (tierId && entry.tiers?.[tierId] !== void 0) return entry.tiers[tierId];
|
|
2966
|
+
return entry.base ?? fallback;
|
|
2967
|
+
}
|
|
2505
2968
|
syncPrices() {
|
|
2506
2969
|
const doc = this.controller.doc;
|
|
2507
2970
|
if (!doc || !this.els.prices) return;
|
|
2508
2971
|
const left = this.controller.categoryAvailability();
|
|
2972
|
+
this.narrateAvailability(doc.categories, left);
|
|
2973
|
+
this.syncSoldout(doc.categories, left);
|
|
2509
2974
|
this.els.prices.innerHTML = doc.categories.map((c) => {
|
|
2510
|
-
const price =
|
|
2975
|
+
const price = this.catPrice(c);
|
|
2511
2976
|
const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
|
|
2512
2977
|
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>`;
|
|
2513
2978
|
}).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>`;
|
|
@@ -2516,6 +2981,26 @@ var SeatPicker = class _SeatPicker {
|
|
|
2516
2981
|
row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
|
|
2517
2982
|
});
|
|
2518
2983
|
}
|
|
2984
|
+
/**
|
|
2985
|
+
* Live-activity strip: turn WS availability deltas into one quiet line of
|
|
2986
|
+
* social proof ("2 seats just taken in VIP · 118 left"). Diffs per-category
|
|
2987
|
+
* counts on every status change — no per-seat payload needed. Skips the very
|
|
2988
|
+
* first computation (initial load is not "activity").
|
|
2989
|
+
*/
|
|
2990
|
+
narrateAvailability(categories, left) {
|
|
2991
|
+
const textEl = this.els.liveText;
|
|
2992
|
+
const prev = this.lastCatAvail;
|
|
2993
|
+
this.lastCatAvail = { ...left };
|
|
2994
|
+
if (!textEl || !prev) return;
|
|
2995
|
+
for (const cat of categories) {
|
|
2996
|
+
const before = prev[cat.key];
|
|
2997
|
+
const now = left[cat.key] ?? 0;
|
|
2998
|
+
if (before === void 0 || now >= before) continue;
|
|
2999
|
+
const taken = before - now;
|
|
3000
|
+
textEl.textContent = `${taken} seat${taken === 1 ? "" : "s"} just taken in ${cat.label} \xB7 ${now} left`;
|
|
3001
|
+
return;
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
2519
3004
|
/** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
|
|
2520
3005
|
evictTakenSelections() {
|
|
2521
3006
|
const ownLabels = /* @__PURE__ */ new Set([
|
|
@@ -2544,15 +3029,23 @@ var SeatPicker = class _SeatPicker {
|
|
|
2544
3029
|
const cats = this.controller.doc?.categories ?? [];
|
|
2545
3030
|
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>`);
|
|
2546
3031
|
}
|
|
3032
|
+
const idGrid = (seatId, label) => {
|
|
3033
|
+
const d = seatId ? this.controller.seatDetails(seatId) : null;
|
|
3034
|
+
if (!d?.sectionLabel && !d?.rowLabel && !d?.seatNumber) {
|
|
3035
|
+
return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">Seat</span><span class="val">${label}</span></span></div>`;
|
|
3036
|
+
}
|
|
3037
|
+
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>`;
|
|
3038
|
+
};
|
|
3039
|
+
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="${t2("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>`;
|
|
2547
3040
|
for (const item of heldItems) {
|
|
2548
3041
|
const itemKey = `held:${item.label}`;
|
|
2549
3042
|
nextTrayKeys.add(itemKey);
|
|
2550
3043
|
const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
|
|
2551
3044
|
const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
|
|
2552
|
-
const
|
|
2553
|
-
const
|
|
3045
|
+
const heldSeat = item.objectType !== "ga" ? this.controller.seatByLabel(item.label) : null;
|
|
3046
|
+
const canView2 = this.seatViewEnabled() && !!heldSeat;
|
|
2554
3047
|
parts.push(
|
|
2555
|
-
`<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><
|
|
3048
|
+
`<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>`
|
|
2556
3049
|
);
|
|
2557
3050
|
}
|
|
2558
3051
|
const heldLabels = new Set(heldItems.map((item) => item.label));
|
|
@@ -2561,16 +3054,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
2561
3054
|
const itemKey = `seat:${s.id}`;
|
|
2562
3055
|
nextTrayKeys.add(itemKey);
|
|
2563
3056
|
const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
|
|
2564
|
-
const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${t2("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>` : "";
|
|
2565
|
-
const viewBtn = canView ? `<button type="button" class="view" data-view-label="${s.label}" aria-label="${t2("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>` : "";
|
|
3057
|
+
const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${t2("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>` : "";
|
|
2566
3058
|
parts.push(
|
|
2567
|
-
`<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><
|
|
3059
|
+
`<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>`
|
|
2568
3060
|
);
|
|
2569
3061
|
}
|
|
2570
3062
|
for (const area of gaAreas) {
|
|
2571
3063
|
const qty = this.gaQty.get(area.id) ?? 0;
|
|
2572
3064
|
parts.push(
|
|
2573
|
-
`<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>`
|
|
3065
|
+
`<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>`
|
|
2574
3066
|
);
|
|
2575
3067
|
}
|
|
2576
3068
|
this.els.tray.innerHTML = parts.join("");
|
|
@@ -2610,7 +3102,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2610
3102
|
return;
|
|
2611
3103
|
}
|
|
2612
3104
|
const id = chip.dataset.seat;
|
|
2613
|
-
const label =
|
|
3105
|
+
const label = this.controller.getSelection().find((sel) => sel.id === id)?.label ?? "Seat";
|
|
2614
3106
|
const remove = () => {
|
|
2615
3107
|
this.controller.deselect([id]);
|
|
2616
3108
|
this.toast(`${label} removed.`, "neutral", {
|
|
@@ -2641,6 +3133,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
2641
3133
|
if (seat) this.openSeatView(seat);
|
|
2642
3134
|
});
|
|
2643
3135
|
});
|
|
3136
|
+
this.els.tray.querySelectorAll(".sl-chip[data-locate]").forEach((chip) => {
|
|
3137
|
+
const locate = () => this.controller.flashSeat(chip.dataset.locate, this.cssVar("--sl-accent") || "#f4b740");
|
|
3138
|
+
chip.addEventListener("mouseenter", locate);
|
|
3139
|
+
chip.addEventListener("focusin", locate);
|
|
3140
|
+
});
|
|
2644
3141
|
this.els.tray.querySelectorAll(".sl-ga button").forEach((btn) => {
|
|
2645
3142
|
btn.addEventListener("click", () => {
|
|
2646
3143
|
const areaEl = btn.closest(".sl-ga");
|
|
@@ -2653,12 +3150,17 @@ var SeatPicker = class _SeatPicker {
|
|
|
2653
3150
|
this.syncTray();
|
|
2654
3151
|
});
|
|
2655
3152
|
});
|
|
3153
|
+
if (this.salesClosed) {
|
|
3154
|
+
this.els.tray.querySelectorAll(".sl-ba-go,[data-ba],[data-ba-cat],[data-ba-replace],.sl-ga button").forEach((el) => {
|
|
3155
|
+
el.disabled = true;
|
|
3156
|
+
});
|
|
3157
|
+
}
|
|
2656
3158
|
const gaTotal = this.pendingGATotal(gaAreas);
|
|
2657
3159
|
const gaCount = this.pendingGACount();
|
|
2658
|
-
const heldTotal = heldItems.reduce((sum, item) => sum + item.unitPrice * (item.quantity ?? 1), 0);
|
|
3160
|
+
const heldTotal = heldItems.reduce((sum, item) => sum + this.paidPrice(item.categoryKey, item.tierId, item.unitPrice) * (item.quantity ?? 1), 0);
|
|
2659
3161
|
const heldCount = heldItems.reduce((sum, item) => sum + (item.quantity ?? 1), 0);
|
|
2660
3162
|
const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
|
|
2661
|
-
const total = freshSeats.reduce((sum, s) => sum + s.price, 0) + gaTotal + heldTotal;
|
|
3163
|
+
const total = freshSeats.reduce((sum, s) => sum + this.paidPrice(s.categoryKey, s.tierId ?? null, s.price), 0) + gaTotal + heldTotal;
|
|
2662
3164
|
const count = freshSeats.length + gaCount + heldCount;
|
|
2663
3165
|
const pendingCount = this.pendingSelectionCount();
|
|
2664
3166
|
const previousCount = this.lastTrayCount;
|
|
@@ -2754,6 +3256,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2754
3256
|
}
|
|
2755
3257
|
}
|
|
2756
3258
|
async handleCta() {
|
|
3259
|
+
if (this.salesClosed) return;
|
|
2757
3260
|
if (this.totalTicketCount() > this.maxTickets) {
|
|
2758
3261
|
this.toast(`Remove tickets until your order has ${this.maxTickets} or fewer.`, "warning");
|
|
2759
3262
|
return;
|
|
@@ -2797,6 +3300,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2797
3300
|
this.opts.onError?.(err);
|
|
2798
3301
|
const problem = err;
|
|
2799
3302
|
const labels = (problem.conflicts ?? []).map((conflict) => conflict.label).filter(Boolean).slice(0, 3);
|
|
3303
|
+
if (problem.reason === "event_closed") this.setSalesClosed(true);
|
|
2800
3304
|
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.";
|
|
2801
3305
|
this.toast(message, "error");
|
|
2802
3306
|
this.setCtaPhase("idle");
|
|
@@ -2905,7 +3409,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2905
3409
|
objectType: it.objectType,
|
|
2906
3410
|
categoryKey: it.categoryKey,
|
|
2907
3411
|
tierId: it.tierId,
|
|
2908
|
-
unitPrice: it.unitPrice,
|
|
3412
|
+
unitPrice: this.paidPrice(it.categoryKey, it.tierId, it.unitPrice),
|
|
2909
3413
|
currency: it.currency ?? this.currency,
|
|
2910
3414
|
quantity: it.quantity ?? 1
|
|
2911
3415
|
}));
|
|
@@ -2958,19 +3462,37 @@ var SeatPicker = class _SeatPicker {
|
|
|
2958
3462
|
this.tipEl.style.left = `${Math.max(8, x)}px`;
|
|
2959
3463
|
this.tipEl.style.top = `${Math.max(8, y)}px`;
|
|
2960
3464
|
}
|
|
3465
|
+
/**
|
|
3466
|
+
* Row label without the redundant section prefix. Charts commonly name row
|
|
3467
|
+
* objects "104-A" while the Section column already shows "104" — so the Row
|
|
3468
|
+
* cell repeats the section and, in the compact hover card, truncates to
|
|
3469
|
+
* "10…". Strip a leading "<section><sep>" so Row reads a clean "A". Only when
|
|
3470
|
+
* the prefix is exact (won't touch "1040-A" under section "104"); otherwise
|
|
3471
|
+
* the label is shown verbatim.
|
|
3472
|
+
*/
|
|
3473
|
+
rowShort(details) {
|
|
3474
|
+
const row = details?.rowLabel;
|
|
3475
|
+
const sec = details?.sectionLabel;
|
|
3476
|
+
if (!row || !sec) return row;
|
|
3477
|
+
for (const sep of ["-", " ", "\xB7", "/", "_"]) {
|
|
3478
|
+
const prefix = `${sec}${sep}`;
|
|
3479
|
+
if (row.startsWith(prefix) && row.length > prefix.length) return row.slice(prefix.length);
|
|
3480
|
+
}
|
|
3481
|
+
return row;
|
|
3482
|
+
}
|
|
2961
3483
|
updateTooltip(details) {
|
|
2962
3484
|
if (!this.tipEl) return;
|
|
2963
3485
|
if (!details) {
|
|
2964
3486
|
this.tipEl.style.display = "none";
|
|
2965
3487
|
return;
|
|
2966
3488
|
}
|
|
2967
|
-
const
|
|
2968
|
-
const
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
this.tipEl.innerHTML = `<div
|
|
3489
|
+
const esc2 = (v) => String(v ?? "\u2014").replace(/[&<>"]/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[ch]);
|
|
3490
|
+
const price = this.money(this.paidPrice(details.categoryKey, details.tierId ?? null, details.price));
|
|
3491
|
+
const hasLoc = details.sectionLabel || details.rowLabel || details.seatNumber;
|
|
3492
|
+
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>`;
|
|
3493
|
+
const statusLine = details.status === "free" ? "" : `<div class="sl-tip-status">${details.status === "held" ? t2("map.statusHeld") : t2("map.statusTaken")}</div>`;
|
|
3494
|
+
this.tipEl.style.setProperty("--sl-cat", details.categoryColor);
|
|
3495
|
+
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;
|
|
2974
3496
|
this.tipEl.style.display = "block";
|
|
2975
3497
|
this.placeTooltip();
|
|
2976
3498
|
}
|
|
@@ -2991,7 +3513,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2991
3513
|
return this.removeHeldLabel(label);
|
|
2992
3514
|
}
|
|
2993
3515
|
async bestAvailable(qty, categoryKey) {
|
|
2994
|
-
if (this.bestAvailableBusy) return null;
|
|
3516
|
+
if (this.salesClosed || this.bestAvailableBusy) return null;
|
|
2995
3517
|
qty = Math.max(1, Math.min(this.maxTickets, Math.floor(qty)));
|
|
2996
3518
|
if (this.confirmSeat) this.cancelConfirm();
|
|
2997
3519
|
this.bestAvailableConfirm = false;
|
|
@@ -3071,7 +3593,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
3071
3593
|
this.motionTimers.clear();
|
|
3072
3594
|
this.ro?.disconnect();
|
|
3073
3595
|
this.ro = null;
|
|
3596
|
+
if (this.framedFs) this.setFramedFs(false);
|
|
3074
3597
|
if (this.escHandler) document.removeEventListener("keydown", this.escHandler);
|
|
3598
|
+
if (this.fsChangeHandler) document.removeEventListener("fullscreenchange", this.fsChangeHandler);
|
|
3599
|
+
if (this.fsEscHandler) window.removeEventListener("keydown", this.fsEscHandler);
|
|
3075
3600
|
this.controller.destroy();
|
|
3076
3601
|
this.root?.remove();
|
|
3077
3602
|
this.root = null;
|
|
@@ -3083,6 +3608,92 @@ var SeatPicker = class _SeatPicker {
|
|
|
3083
3608
|
}
|
|
3084
3609
|
};
|
|
3085
3610
|
|
|
3611
|
+
// src/attachPickerFrame.ts
|
|
3612
|
+
function attachPickerFrame(iframe, opts = {}) {
|
|
3613
|
+
let expectedOrigin = opts.origin ?? "";
|
|
3614
|
+
if (!expectedOrigin) {
|
|
3615
|
+
try {
|
|
3616
|
+
expectedOrigin = new URL(iframe.src, window.location.href).origin;
|
|
3617
|
+
} catch {
|
|
3618
|
+
expectedOrigin = "";
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
let pinned = false;
|
|
3622
|
+
let frameStyleBeforeFs = null;
|
|
3623
|
+
let docOverflowBeforeFs = null;
|
|
3624
|
+
let bodyOverflowBeforeFs = null;
|
|
3625
|
+
let lastAutoHeight = "";
|
|
3626
|
+
let keyHandler = null;
|
|
3627
|
+
const pin = () => {
|
|
3628
|
+
if (pinned) return;
|
|
3629
|
+
pinned = true;
|
|
3630
|
+
frameStyleBeforeFs = iframe.getAttribute("style");
|
|
3631
|
+
Object.assign(iframe.style, {
|
|
3632
|
+
position: "fixed",
|
|
3633
|
+
inset: "0",
|
|
3634
|
+
width: "100vw",
|
|
3635
|
+
height: "100vh",
|
|
3636
|
+
margin: "0",
|
|
3637
|
+
border: "0",
|
|
3638
|
+
zIndex: "2147483000",
|
|
3639
|
+
background: "#101625"
|
|
3640
|
+
});
|
|
3641
|
+
const docEl = document.documentElement;
|
|
3642
|
+
docOverflowBeforeFs = docEl.style.overflow;
|
|
3643
|
+
docEl.style.overflow = "hidden";
|
|
3644
|
+
if (document.body) {
|
|
3645
|
+
bodyOverflowBeforeFs = document.body.style.overflow;
|
|
3646
|
+
document.body.style.overflow = "hidden";
|
|
3647
|
+
}
|
|
3648
|
+
keyHandler = (event) => {
|
|
3649
|
+
if (event.key === "Escape") unpin();
|
|
3650
|
+
};
|
|
3651
|
+
window.addEventListener("keydown", keyHandler);
|
|
3652
|
+
};
|
|
3653
|
+
const unpin = () => {
|
|
3654
|
+
if (!pinned) return;
|
|
3655
|
+
pinned = false;
|
|
3656
|
+
if (frameStyleBeforeFs === null) iframe.removeAttribute("style");
|
|
3657
|
+
else iframe.setAttribute("style", frameStyleBeforeFs);
|
|
3658
|
+
frameStyleBeforeFs = null;
|
|
3659
|
+
if (lastAutoHeight) iframe.style.height = lastAutoHeight;
|
|
3660
|
+
if (docOverflowBeforeFs !== null) {
|
|
3661
|
+
document.documentElement.style.overflow = docOverflowBeforeFs;
|
|
3662
|
+
docOverflowBeforeFs = null;
|
|
3663
|
+
}
|
|
3664
|
+
if (bodyOverflowBeforeFs !== null && document.body) {
|
|
3665
|
+
document.body.style.overflow = bodyOverflowBeforeFs;
|
|
3666
|
+
bodyOverflowBeforeFs = null;
|
|
3667
|
+
}
|
|
3668
|
+
if (keyHandler) {
|
|
3669
|
+
window.removeEventListener("keydown", keyHandler);
|
|
3670
|
+
keyHandler = null;
|
|
3671
|
+
}
|
|
3672
|
+
};
|
|
3673
|
+
const onMessage = (event) => {
|
|
3674
|
+
if (event.source !== iframe.contentWindow) return;
|
|
3675
|
+
if (expectedOrigin && event.origin !== expectedOrigin) return;
|
|
3676
|
+
if (!event.data || typeof event.data !== "object") return;
|
|
3677
|
+
const data = event.data;
|
|
3678
|
+
if (data.type === "seatlayer:height") {
|
|
3679
|
+
if (typeof data.px === "number" && Number.isFinite(data.px) && data.px > 0) {
|
|
3680
|
+
lastAutoHeight = `${Math.round(data.px)}px`;
|
|
3681
|
+
if (!pinned) iframe.style.height = lastAutoHeight;
|
|
3682
|
+
}
|
|
3683
|
+
return;
|
|
3684
|
+
}
|
|
3685
|
+
if (data.type === "seatlayer:fullscreen") {
|
|
3686
|
+
if (data.on === true) pin();
|
|
3687
|
+
else if (data.on === false) unpin();
|
|
3688
|
+
}
|
|
3689
|
+
};
|
|
3690
|
+
window.addEventListener("message", onMessage);
|
|
3691
|
+
return () => {
|
|
3692
|
+
window.removeEventListener("message", onMessage);
|
|
3693
|
+
unpin();
|
|
3694
|
+
};
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3086
3697
|
// src/SeatManager.ts
|
|
3087
3698
|
import {
|
|
3088
3699
|
SeatmapRenderer,
|
|
@@ -3169,6 +3780,21 @@ var ManageApi = class {
|
|
|
3169
3780
|
setHoldTtl(key, holdTtlMs) {
|
|
3170
3781
|
return this.auth(`/v1/events/${encodeURIComponent(key)}/hold-ttl`, { method: "POST", body: { holdTtlMs } });
|
|
3171
3782
|
}
|
|
3783
|
+
// ---- availability windows (token) ----
|
|
3784
|
+
/** The organizer's current per section/zone availability windows (needs
|
|
3785
|
+
* `event:view`). Ids absent from `rules` are open / on sale. */
|
|
3786
|
+
availability(key) {
|
|
3787
|
+
return this.auth(`/v1/events/${encodeURIComponent(key)}/availability`);
|
|
3788
|
+
}
|
|
3789
|
+
/** Replace the availability windows for a set of section/zone ids (needs
|
|
3790
|
+
* `event:block`). Ids absent from `rules` become open / on sale; a zone rule
|
|
3791
|
+
* cascades to its sections. The worker derives each id's seat labels, so
|
|
3792
|
+
* `labels` on the sent rules is best-effort. Resolves with the authoritative
|
|
3793
|
+
* effective `hidden` set (a due rule may fire at once) and the server-cleaned
|
|
3794
|
+
* `rules` map (fired timed/threshold windows dropped). */
|
|
3795
|
+
setAvailability(key, rules) {
|
|
3796
|
+
return this.auth(`/v1/events/${encodeURIComponent(key)}/availability`, { method: "POST", body: { rules } });
|
|
3797
|
+
}
|
|
3172
3798
|
// ---- reports (token) ----
|
|
3173
3799
|
report(key) {
|
|
3174
3800
|
return this.auth(`/v1/events/${encodeURIComponent(key)}/report`);
|
|
@@ -3196,6 +3822,27 @@ var ManageApi = class {
|
|
|
3196
3822
|
};
|
|
3197
3823
|
|
|
3198
3824
|
// src/SeatManager.ts
|
|
3825
|
+
function availabilityModeOf(rule) {
|
|
3826
|
+
return rule ? rule.mode : "open";
|
|
3827
|
+
}
|
|
3828
|
+
function availabilityRuleForMode(mode, seatLabels, prev) {
|
|
3829
|
+
switch (mode) {
|
|
3830
|
+
case "open":
|
|
3831
|
+
return null;
|
|
3832
|
+
case "hidden":
|
|
3833
|
+
return { mode: "hidden", labels: seatLabels };
|
|
3834
|
+
case "closed":
|
|
3835
|
+
return { mode: "closed", labels: seatLabels };
|
|
3836
|
+
case "timed":
|
|
3837
|
+
return { mode: "timed", revealAt: prev?.revealAt ?? Date.now() + 36e5, labels: seatLabels };
|
|
3838
|
+
case "threshold":
|
|
3839
|
+
return { mode: "threshold", thresholdPct: prev?.thresholdPct ?? 80, labels: seatLabels };
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
function toLocalInput(ms) {
|
|
3843
|
+
const d = new Date(ms - (/* @__PURE__ */ new Date()).getTimezoneOffset() * 6e4);
|
|
3844
|
+
return d.toISOString().slice(0, 16);
|
|
3845
|
+
}
|
|
3199
3846
|
function resolveContainer4(container) {
|
|
3200
3847
|
if (typeof container === "string") {
|
|
3201
3848
|
const el = document.querySelector(container);
|
|
@@ -3384,6 +4031,32 @@ var CSS2 = `
|
|
|
3384
4031
|
.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}
|
|
3385
4032
|
.slm-momentumgradient{height:6px;min-width:64px;flex:1;border-radius:999px;background:linear-gradient(90deg,#f4b740,#ef4444)}
|
|
3386
4033
|
.slm-momentumcopy{margin-top:7px;color:var(--slm-muted);font-size:11px;line-height:1.45}
|
|
4034
|
+
/* sections: availability windows */
|
|
4035
|
+
.slm-availlist{display:flex;flex-direction:column;gap:8px;margin:2px 0 12px}
|
|
4036
|
+
.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}
|
|
4037
|
+
.slm-availrow.zone{background:color-mix(in srgb,var(--slm-surface) 82%,#000)}
|
|
4038
|
+
.slm-availrow.hidden{opacity:.62}.slm-availrow.closed{opacity:.82}
|
|
4039
|
+
.slm-availhead{display:flex;align-items:center;gap:8px}
|
|
4040
|
+
.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}
|
|
4041
|
+
.slm-availcaret{flex:none;color:var(--slm-muted);font-size:10px}
|
|
4042
|
+
.slm-availcount{flex:none;font-size:11px;font-weight:700;color:var(--slm-muted);font-variant-numeric:tabular-nums}
|
|
4043
|
+
.slm-availbadge{flex:none;font-size:9px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;padding:2px 6px;border-radius:999px}
|
|
4044
|
+
.slm-availbadge.hidden{background:rgba(139,148,172,.18);color:#c2c9d8}
|
|
4045
|
+
.slm-availbadge.closed{background:rgba(244,183,64,.16);color:#f7ca6b}
|
|
4046
|
+
.slm-availselwrap{position:relative;flex:none;display:inline-flex}
|
|
4047
|
+
.slm-availmode{width:auto;max-width:190px;padding:6px 8px;font-size:11.5px;font-weight:700;cursor:pointer}
|
|
4048
|
+
.slm-availmode.on{border-color:var(--slm-accent);color:var(--slm-text)}
|
|
4049
|
+
.slm-availmode:disabled{opacity:.55;cursor:progress}
|
|
4050
|
+
.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}
|
|
4051
|
+
.slm-availdetail{display:flex;align-items:center;gap:8px;margin-top:9px}
|
|
4052
|
+
.slm-availdetail .slm-input{flex:1}
|
|
4053
|
+
.slm-availpct{max-width:74px;flex:none!important}
|
|
4054
|
+
.slm-availpctlabel{font-size:11px;color:var(--slm-muted);font-weight:600;white-space:nowrap}
|
|
4055
|
+
.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}
|
|
4056
|
+
.slm-availdot{width:9px;height:9px;border-radius:50%;flex:none;background:#22a06b}.slm-availdot.warn{background:#f4b740}
|
|
4057
|
+
.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)}
|
|
4058
|
+
.slm-availstar{flex:none;margin-top:1px;color:#f4b740;font-size:13px;line-height:1}
|
|
4059
|
+
.slm-availcallout p{font-size:11.5px;line-height:1.55;color:#f4d58a}.slm-availcallout b{color:#ffe4a3;font-weight:800}
|
|
3387
4060
|
.slm-inspect-card{padding:16px;border:1px solid var(--slm-line);border-radius:12px;background:var(--slm-surface)}
|
|
3388
4061
|
.slm-inspect-label{font-size:24px;font-weight:850;letter-spacing:-.02em;line-height:1.1}
|
|
3389
4062
|
.slm-inspect-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px 20px;margin-top:18px}
|
|
@@ -3484,6 +4157,13 @@ var SeatManager = class {
|
|
|
3484
4157
|
this.tokenRefreshInFlight = false;
|
|
3485
4158
|
this.sectionByObject = /* @__PURE__ */ new Map();
|
|
3486
4159
|
this.sectionLabelById = /* @__PURE__ */ new Map();
|
|
4160
|
+
this.sectionsBase = null;
|
|
4161
|
+
// Sections mode (availability windows): organizer rules + the live effective
|
|
4162
|
+
// hidden/closed sets from the snapshot + WS (a timed/threshold rule fires DO-side).
|
|
4163
|
+
this.availabilityRules = {};
|
|
4164
|
+
this.effectiveHidden = /* @__PURE__ */ new Set();
|
|
4165
|
+
this.effectiveClosed = /* @__PURE__ */ new Set();
|
|
4166
|
+
this.availabilitySaving = false;
|
|
3487
4167
|
this.lastSyncedAt = null;
|
|
3488
4168
|
this.blockedQuery = "";
|
|
3489
4169
|
this.blockedSection = "";
|
|
@@ -3502,6 +4182,7 @@ var SeatManager = class {
|
|
|
3502
4182
|
if (key === "m") this.setMode("view");
|
|
3503
4183
|
else if (key === "i") this.setMode("inspect");
|
|
3504
4184
|
else if (key === "b") this.setMode("block");
|
|
4185
|
+
else if (key === "s") this.setMode("sections");
|
|
3505
4186
|
else if (key === "f") this.toggleFullscreen();
|
|
3506
4187
|
else return;
|
|
3507
4188
|
event.preventDefault();
|
|
@@ -3545,7 +4226,8 @@ var SeatManager = class {
|
|
|
3545
4226
|
this.buildSectionOptions();
|
|
3546
4227
|
const [, controlRoom] = await Promise.all([
|
|
3547
4228
|
this.resnapshot(),
|
|
3548
|
-
this.refreshControlRoom().catch((err) => this.opts.onError?.(err))
|
|
4229
|
+
this.refreshControlRoom().catch((err) => this.opts.onError?.(err)),
|
|
4230
|
+
this.refreshAvailability()
|
|
3549
4231
|
]);
|
|
3550
4232
|
if (controlRoom?.activity) this.seedFeed(controlRoom.activity);
|
|
3551
4233
|
else this.api.log(this.key, { limit: 24 }).then((page) => this.seedFeed(page.entries)).catch(() => {
|
|
@@ -3570,6 +4252,7 @@ var SeatManager = class {
|
|
|
3570
4252
|
if (changed) this.renderer?.clearSelection();
|
|
3571
4253
|
this.paintModeTabs();
|
|
3572
4254
|
this.paintRail();
|
|
4255
|
+
this.applySectionCanvasTreatment();
|
|
3573
4256
|
if (changed) this.opts.onModeChange?.(mode);
|
|
3574
4257
|
}
|
|
3575
4258
|
/** Toggle the normalized sales-velocity outline overlay without changing seat colors. */
|
|
@@ -3861,6 +4544,7 @@ var SeatManager = class {
|
|
|
3861
4544
|
this.attempt = 0;
|
|
3862
4545
|
this.setLive(true);
|
|
3863
4546
|
void this.resnapshot().then(() => this.scheduleRevenueRefresh(0));
|
|
4547
|
+
void this.refreshAvailability();
|
|
3864
4548
|
};
|
|
3865
4549
|
ws.onmessage = (e) => this.onMessage(e);
|
|
3866
4550
|
ws.onclose = () => {
|
|
@@ -3892,6 +4576,9 @@ var SeatManager = class {
|
|
|
3892
4576
|
}
|
|
3893
4577
|
if (!msg || typeof msg !== "object") return;
|
|
3894
4578
|
const m = msg;
|
|
4579
|
+
if (Array.isArray(m.hidden) || Array.isArray(m.closed)) {
|
|
4580
|
+
this.updateEffectiveAvailability(m.hidden, m.closed);
|
|
4581
|
+
}
|
|
3895
4582
|
if (m.type === "presence") {
|
|
3896
4583
|
if (this.controlRoomSnapshot && typeof m.shoppingSessions === "number" && typeof m.activeHolds === "number") {
|
|
3897
4584
|
this.controlRoomSnapshot = {
|
|
@@ -3943,6 +4630,7 @@ var SeatManager = class {
|
|
|
3943
4630
|
try {
|
|
3944
4631
|
const objs = await this.api.objects(this.key);
|
|
3945
4632
|
this.applySnapshot(objs.seats);
|
|
4633
|
+
this.updateEffectiveAvailability(objs.hidden, objs.closed);
|
|
3946
4634
|
} catch {
|
|
3947
4635
|
}
|
|
3948
4636
|
}
|
|
@@ -4254,6 +4942,7 @@ var SeatManager = class {
|
|
|
4254
4942
|
<button class="slm-mode" role="tab" data-mode="view" title="Monitor (M)" aria-keyshortcuts="M">Monitor</button>
|
|
4255
4943
|
<button class="slm-mode" role="tab" data-mode="inspect" title="Inspect (I)" aria-keyshortcuts="I">Inspect</button>
|
|
4256
4944
|
<button class="slm-mode" role="tab" data-mode="block" title="Block (B)" aria-keyshortcuts="B">Block</button>
|
|
4945
|
+
<button class="slm-mode" role="tab" data-mode="sections" title="Sections (S)" aria-keyshortcuts="S">Sections</button>
|
|
4257
4946
|
</div>
|
|
4258
4947
|
<span class="slm-live"><span class="slm-live-dot"></span><span data-ref="livetext">CONNECTING</span></span>
|
|
4259
4948
|
<div class="slm-bar-actions">
|
|
@@ -4320,6 +5009,7 @@ var SeatManager = class {
|
|
|
4320
5009
|
if (!this.doc) return;
|
|
4321
5010
|
try {
|
|
4322
5011
|
const secs = computeSections(this.doc);
|
|
5012
|
+
this.sectionsBase = secs;
|
|
4323
5013
|
this.sectionOptions = [];
|
|
4324
5014
|
this.sectionByObject = new Map(secs.objectToSection);
|
|
4325
5015
|
this.sectionLabelById.clear();
|
|
@@ -4445,6 +5135,7 @@ var SeatManager = class {
|
|
|
4445
5135
|
paintRail() {
|
|
4446
5136
|
if (this.mode === "view") this.renderViewRail();
|
|
4447
5137
|
else if (this.mode === "inspect") this.renderInspectRail(this.getSelection());
|
|
5138
|
+
else if (this.mode === "sections") this.renderSectionsRail();
|
|
4448
5139
|
else this.renderBlockRail();
|
|
4449
5140
|
this.updateZoomHint();
|
|
4450
5141
|
}
|
|
@@ -4567,6 +5258,259 @@ var SeatManager = class {
|
|
|
4567
5258
|
</div>
|
|
4568
5259
|
</div>`;
|
|
4569
5260
|
}
|
|
5261
|
+
// ---- sections: availability windows --------------------------------------
|
|
5262
|
+
/** Pull the organizer's availability rules (event:view). Called on load and on
|
|
5263
|
+
* every WS (re)connect, mirroring how the other panels re-hydrate. `closed` is
|
|
5264
|
+
* deterministic from the rules; `hidden` (which folds in already-due timed /
|
|
5265
|
+
* threshold windows) comes from the snapshot + WS effective set. */
|
|
5266
|
+
async refreshAvailability() {
|
|
5267
|
+
try {
|
|
5268
|
+
const res = await this.withAuthRetry(() => this.api.availability(this.key));
|
|
5269
|
+
this.availabilityRules = res.rules ?? {};
|
|
5270
|
+
this.effectiveClosed = new Set(this.closedIdsFromRules(this.availabilityRules));
|
|
5271
|
+
if (this.mode === "sections") this.renderSectionsRail();
|
|
5272
|
+
this.applySectionCanvasTreatment();
|
|
5273
|
+
} catch (err) {
|
|
5274
|
+
this.opts.onError?.(err);
|
|
5275
|
+
}
|
|
5276
|
+
}
|
|
5277
|
+
/** Run a token-authed op; on a 401 re-mint via onTokenRefresh and retry once. */
|
|
5278
|
+
async withAuthRetry(op) {
|
|
5279
|
+
try {
|
|
5280
|
+
return await op();
|
|
5281
|
+
} catch (err) {
|
|
5282
|
+
if (err instanceof ManageApiError && err.status === 401 && this.opts.onTokenRefresh && !this.tokenRefreshInFlight) {
|
|
5283
|
+
await this.rotateToken();
|
|
5284
|
+
return op();
|
|
5285
|
+
}
|
|
5286
|
+
throw err;
|
|
5287
|
+
}
|
|
5288
|
+
}
|
|
5289
|
+
closedIdsFromRules(rules) {
|
|
5290
|
+
return Object.entries(rules).filter(([, r]) => r.mode === "closed").map(([id]) => id);
|
|
5291
|
+
}
|
|
5292
|
+
/** Adopt a new effective hidden/closed set (from a snapshot or WS broadcast) and
|
|
5293
|
+
* repaint the rail + canvas when it actually moves. */
|
|
5294
|
+
updateEffectiveAvailability(hidden, closed) {
|
|
5295
|
+
let changed = false;
|
|
5296
|
+
if (Array.isArray(hidden)) {
|
|
5297
|
+
this.effectiveHidden = new Set(hidden.filter((x) => typeof x === "string"));
|
|
5298
|
+
changed = true;
|
|
5299
|
+
}
|
|
5300
|
+
if (Array.isArray(closed)) {
|
|
5301
|
+
this.effectiveClosed = new Set(closed.filter((x) => typeof x === "string"));
|
|
5302
|
+
changed = true;
|
|
5303
|
+
}
|
|
5304
|
+
if (!changed) return;
|
|
5305
|
+
if (this.mode === "sections") this.renderSectionsRail();
|
|
5306
|
+
this.applySectionCanvasTreatment();
|
|
5307
|
+
}
|
|
5308
|
+
/** Canvas read of the availability state: dim hidden sections to a whisper,
|
|
5309
|
+
* half-light closed sections, leave open sections normal. Only in Sections mode;
|
|
5310
|
+
* cleared in every other tool. */
|
|
5311
|
+
applySectionCanvasTreatment() {
|
|
5312
|
+
if (!this.renderer) return;
|
|
5313
|
+
if (this.mode === "sections") {
|
|
5314
|
+
this.renderer.setDimmedSections([...this.effectiveHidden]);
|
|
5315
|
+
this.renderer.setClosedSections([...this.effectiveClosed]);
|
|
5316
|
+
} else {
|
|
5317
|
+
this.renderer.setDimmedSections(null);
|
|
5318
|
+
this.renderer.setClosedSections(null);
|
|
5319
|
+
}
|
|
5320
|
+
}
|
|
5321
|
+
/** Zone-grouped render tree: each zone header then its sections (which follow the
|
|
5322
|
+
* zone window), then loose sections + the ungrouped bucket. Effective hidden /
|
|
5323
|
+
* closed come from the live sets, rules from the organizer map. */
|
|
5324
|
+
buildSectionRows() {
|
|
5325
|
+
const base = this.sectionsBase;
|
|
5326
|
+
if (!base) return { rows: [], hiddenSections: 0, closedSections: 0 };
|
|
5327
|
+
const zones = this.doc?.zones ?? [];
|
|
5328
|
+
const byZone = /* @__PURE__ */ new Map();
|
|
5329
|
+
const loose = [];
|
|
5330
|
+
for (const s of base.sections) {
|
|
5331
|
+
if (s.zone && zones.some((z) => z.id === s.zone)) {
|
|
5332
|
+
const list = byZone.get(s.zone) ?? [];
|
|
5333
|
+
list.push(s);
|
|
5334
|
+
byZone.set(s.zone, list);
|
|
5335
|
+
} else {
|
|
5336
|
+
loose.push(s);
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
5339
|
+
const rows = [];
|
|
5340
|
+
let hiddenSections = 0;
|
|
5341
|
+
let closedSections = 0;
|
|
5342
|
+
const push = (kind, node, zoneRuled, parentClosed = false) => {
|
|
5343
|
+
const rule = this.availabilityRules[node.id] ?? null;
|
|
5344
|
+
const effClosed = this.effectiveClosed.has(node.id) || parentClosed;
|
|
5345
|
+
const effHidden = this.effectiveHidden.has(node.id) || zoneRuled && !effClosed;
|
|
5346
|
+
if (kind === "section" && effHidden) hiddenSections += 1;
|
|
5347
|
+
if (kind === "section" && effClosed) closedSections += 1;
|
|
5348
|
+
rows.push({
|
|
5349
|
+
kind,
|
|
5350
|
+
id: node.id,
|
|
5351
|
+
label: node.label,
|
|
5352
|
+
seatCount: node.seatCount,
|
|
5353
|
+
seatLabels: node.seatLabels,
|
|
5354
|
+
rule,
|
|
5355
|
+
hidden: effHidden,
|
|
5356
|
+
closed: effClosed,
|
|
5357
|
+
followsZone: kind === "section" && zoneRuled
|
|
5358
|
+
});
|
|
5359
|
+
};
|
|
5360
|
+
for (const z of zones) {
|
|
5361
|
+
const secs = byZone.get(z.id);
|
|
5362
|
+
if (!secs || !secs.length) continue;
|
|
5363
|
+
const zoneNode = {
|
|
5364
|
+
id: z.id,
|
|
5365
|
+
label: z.label || "Zone",
|
|
5366
|
+
seatCount: secs.reduce((sum, s) => sum + s.seatCount, 0),
|
|
5367
|
+
seatLabels: secs.flatMap((s) => s.seatLabels)
|
|
5368
|
+
};
|
|
5369
|
+
const zoneRuled = !!this.availabilityRules[z.id];
|
|
5370
|
+
const zoneClosed = this.availabilityRules[z.id]?.mode === "closed";
|
|
5371
|
+
push("zone", zoneNode, false);
|
|
5372
|
+
for (const s of secs) push("section", s, zoneRuled, zoneClosed);
|
|
5373
|
+
}
|
|
5374
|
+
for (const s of loose) push("section", s, false);
|
|
5375
|
+
if (base.ungrouped) {
|
|
5376
|
+
const u = base.ungrouped;
|
|
5377
|
+
push("section", { id: UNGROUPED_ID, label: u.label, seatCount: u.seatCount, seatLabels: u.seatLabels }, false);
|
|
5378
|
+
}
|
|
5379
|
+
return { rows, hiddenSections, closedSections };
|
|
5380
|
+
}
|
|
5381
|
+
renderSectionsRail() {
|
|
5382
|
+
const { rows, hiddenSections, closedSections } = this.buildSectionRows();
|
|
5383
|
+
if (!rows.length) {
|
|
5384
|
+
this.els.rail.innerHTML = `
|
|
5385
|
+
<p class="slm-eyebrow">Availability windows</p>
|
|
5386
|
+
<p class="slm-hint">Draw sections or zones in the designer to schedule availability per area. This chart has none yet.</p>
|
|
5387
|
+
<div class="slm-empty">No sections on this chart.</div>`;
|
|
5388
|
+
return;
|
|
5389
|
+
}
|
|
5390
|
+
const parts = [];
|
|
5391
|
+
if (hiddenSections) parts.push(`${hiddenSections} hidden`);
|
|
5392
|
+
if (closedSections) parts.push(`${closedSections} closed`);
|
|
5393
|
+
const summary = parts.length ? parts.join(" \xB7 ") : "All sections open and on sale";
|
|
5394
|
+
const warn = hiddenSections > 0 || closedSections > 0;
|
|
5395
|
+
this.els.rail.innerHTML = `
|
|
5396
|
+
<p class="slm-eyebrow">Availability windows</p>
|
|
5397
|
+
<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>
|
|
5398
|
+
<div class="slm-availlist" data-ref="availlist">${rows.map((row) => this.sectionRowHtml(row)).join("")}</div>
|
|
5399
|
+
<div class="slm-availsummary">
|
|
5400
|
+
<span class="slm-availdot${warn ? " warn" : ""}"></span>
|
|
5401
|
+
<span>${esc(summary)}</span>
|
|
5402
|
+
</div>
|
|
5403
|
+
<div class="slm-availcallout">
|
|
5404
|
+
<span class="slm-availstar" aria-hidden="true">\u2726</span>
|
|
5405
|
+
<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>
|
|
5406
|
+
</div>`;
|
|
5407
|
+
this.wireSectionRail();
|
|
5408
|
+
this.applySectionCanvasTreatment();
|
|
5409
|
+
}
|
|
5410
|
+
sectionRowHtml(row) {
|
|
5411
|
+
const mode = availabilityModeOf(row.rule);
|
|
5412
|
+
const cls = `slm-availrow${row.kind === "zone" ? " zone" : ""}${row.hidden ? " hidden" : ""}${row.closed ? " closed" : ""}`;
|
|
5413
|
+
const disabled = this.availabilitySaving ? " disabled" : "";
|
|
5414
|
+
const option = (value, text) => `<option value="${value}"${mode === value ? " selected" : ""}>${text}</option>`;
|
|
5415
|
+
const control = row.followsZone ? '<span class="slm-availfollows">Follows zone</span>' : `<span class="slm-availselwrap">
|
|
5416
|
+
<select class="slm-select slm-availmode${mode !== "open" ? " on" : ""}" data-avail-id="${esc(row.id)}"${disabled} aria-label="Availability for ${esc(row.label)}">
|
|
5417
|
+
${option("open", "Open \u2014 on sale")}
|
|
5418
|
+
${option("closed", "Closed \u2014 visible, not on sale")}
|
|
5419
|
+
${option("hidden", "Hidden \u2014 off the buyer map")}
|
|
5420
|
+
${option("timed", "Reveal at a time")}
|
|
5421
|
+
${option("threshold", "Auto-reveal at % sold")}
|
|
5422
|
+
</select>
|
|
5423
|
+
</span>`;
|
|
5424
|
+
let detail = "";
|
|
5425
|
+
if (!row.followsZone && mode === "timed") {
|
|
5426
|
+
const value = row.rule?.revealAt ? esc(toLocalInput(row.rule.revealAt)) : "";
|
|
5427
|
+
detail = `<div class="slm-availdetail">
|
|
5428
|
+
<input type="datetime-local" class="slm-input" data-avail-reveal="${esc(row.id)}" value="${value}"${disabled} aria-label="Reveal time for ${esc(row.label)}" />
|
|
5429
|
+
</div>`;
|
|
5430
|
+
} else if (!row.followsZone && mode === "threshold") {
|
|
5431
|
+
const pct = row.rule?.thresholdPct ?? 80;
|
|
5432
|
+
detail = `<div class="slm-availdetail">
|
|
5433
|
+
<span class="slm-availpctlabel">Reveal at</span>
|
|
5434
|
+
<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)}" />
|
|
5435
|
+
<span class="slm-availpctlabel">% sold</span>
|
|
5436
|
+
</div>`;
|
|
5437
|
+
}
|
|
5438
|
+
const badge = row.closed ? '<span class="slm-availbadge closed">Closed</span>' : row.hidden ? '<span class="slm-availbadge hidden">Hidden</span>' : "";
|
|
5439
|
+
const caret = row.kind === "zone" ? `<span class="slm-availcaret" aria-hidden="true">${row.hidden ? "\u25B8" : "\u25BE"}</span>` : "";
|
|
5440
|
+
return `<div class="${cls}">
|
|
5441
|
+
<div class="slm-availhead">
|
|
5442
|
+
<span class="slm-availlabel">${caret}${esc(row.label)}</span>
|
|
5443
|
+
${badge}
|
|
5444
|
+
<span class="slm-availcount">${row.seatCount.toLocaleString()}</span>
|
|
5445
|
+
${control}
|
|
5446
|
+
</div>
|
|
5447
|
+
${detail}
|
|
5448
|
+
</div>`;
|
|
5449
|
+
}
|
|
5450
|
+
wireSectionRail() {
|
|
5451
|
+
const rail = this.els.rail;
|
|
5452
|
+
if (!rail) return;
|
|
5453
|
+
rail.querySelectorAll("[data-avail-id]").forEach((select) => {
|
|
5454
|
+
select.addEventListener("change", () => this.setSectionMode(select.dataset.availId, select.value));
|
|
5455
|
+
});
|
|
5456
|
+
rail.querySelectorAll("[data-avail-reveal]").forEach((input) => {
|
|
5457
|
+
input.addEventListener("change", () => {
|
|
5458
|
+
const ms = new Date(input.value).getTime();
|
|
5459
|
+
if (Number.isFinite(ms)) this.setSectionRulePatch(input.dataset.availReveal, { revealAt: ms });
|
|
5460
|
+
});
|
|
5461
|
+
});
|
|
5462
|
+
rail.querySelectorAll("[data-avail-pct]").forEach((input) => {
|
|
5463
|
+
input.addEventListener("change", () => {
|
|
5464
|
+
const pct = Math.max(1, Math.min(100, Number(input.value) || 0));
|
|
5465
|
+
this.setSectionRulePatch(input.dataset.availPct, { thresholdPct: pct });
|
|
5466
|
+
});
|
|
5467
|
+
});
|
|
5468
|
+
}
|
|
5469
|
+
/** Change one row's availability mode. A zone rule subsumes its child section
|
|
5470
|
+
* rules, so those are dropped from the map (the zone window is the truth). */
|
|
5471
|
+
setSectionMode(id, mode) {
|
|
5472
|
+
const row = this.buildSectionRows().rows.find((r) => r.id === id);
|
|
5473
|
+
const seatLabels = row?.seatLabels ?? this.availabilityRules[id]?.labels ?? [];
|
|
5474
|
+
const next = { ...this.availabilityRules };
|
|
5475
|
+
const rule = availabilityRuleForMode(mode, seatLabels, this.availabilityRules[id]);
|
|
5476
|
+
if (rule) next[id] = rule;
|
|
5477
|
+
else delete next[id];
|
|
5478
|
+
if (row?.kind === "zone" && this.sectionsBase) {
|
|
5479
|
+
for (const s of this.sectionsBase.sections) if (s.zone === id) delete next[s.id];
|
|
5480
|
+
}
|
|
5481
|
+
void this.persistAvailability(next);
|
|
5482
|
+
}
|
|
5483
|
+
/** Edit a timed reveal time / threshold percent on an existing row rule. */
|
|
5484
|
+
setSectionRulePatch(id, patch) {
|
|
5485
|
+
const cur = this.availabilityRules[id];
|
|
5486
|
+
if (!cur) return;
|
|
5487
|
+
const row = this.buildSectionRows().rows.find((r) => r.id === id);
|
|
5488
|
+
const labels = row?.seatLabels ?? cur.labels ?? [];
|
|
5489
|
+
void this.persistAvailability({ ...this.availabilityRules, [id]: { ...cur, ...patch, labels } });
|
|
5490
|
+
}
|
|
5491
|
+
/** Optimistically adopt the new rules, then reconcile with the server-cleaned
|
|
5492
|
+
* map + effective hidden/closed sets. Rolls back the rules on failure. */
|
|
5493
|
+
async persistAvailability(next) {
|
|
5494
|
+
const prev = this.availabilityRules;
|
|
5495
|
+
this.availabilityRules = next;
|
|
5496
|
+
this.availabilitySaving = true;
|
|
5497
|
+
if (this.mode === "sections") this.renderSectionsRail();
|
|
5498
|
+
try {
|
|
5499
|
+
const res = await this.withAuthRetry(() => this.api.setAvailability(this.key, next));
|
|
5500
|
+
this.availabilityRules = res.rules;
|
|
5501
|
+
this.effectiveHidden = new Set(res.hidden);
|
|
5502
|
+
this.effectiveClosed = new Set(this.closedIdsFromRules(res.rules));
|
|
5503
|
+
this.availabilitySaving = false;
|
|
5504
|
+
if (this.mode === "sections") this.renderSectionsRail();
|
|
5505
|
+
this.applySectionCanvasTreatment();
|
|
5506
|
+
} catch (err) {
|
|
5507
|
+
this.availabilityRules = prev;
|
|
5508
|
+
this.availabilitySaving = false;
|
|
5509
|
+
if (this.mode === "sections") this.renderSectionsRail();
|
|
5510
|
+
this.toastErr("Couldn't update availability. Try again.");
|
|
5511
|
+
this.opts.onError?.(err);
|
|
5512
|
+
}
|
|
5513
|
+
}
|
|
4570
5514
|
paintLegend(t3) {
|
|
4571
5515
|
if (!this.els.legend) return;
|
|
4572
5516
|
this.els.legend.innerHTML = LEGEND.map((l) => `<div class="slm-legrow"><span class="slm-legdot" style="background:${l.color}"></span>
|
|
@@ -4879,6 +5823,7 @@ export {
|
|
|
4879
5823
|
ManageApiError,
|
|
4880
5824
|
SeatManager,
|
|
4881
5825
|
SeatPicker,
|
|
4882
|
-
SeatingChart
|
|
5826
|
+
SeatingChart,
|
|
5827
|
+
attachPickerFrame
|
|
4883
5828
|
};
|
|
4884
5829
|
//# sourceMappingURL=index.js.map
|