@seatlayer/js 0.18.1 → 0.20.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 +587 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +152 -3
- package/dist/index.d.ts +152 -3
- package/dist/index.js +585 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -26,7 +26,8 @@ __export(index_exports, {
|
|
|
26
26
|
ManageApiError: () => ManageApiError,
|
|
27
27
|
SeatManager: () => SeatManager,
|
|
28
28
|
SeatPicker: () => SeatPicker,
|
|
29
|
-
SeatingChart: () => SeatingChart
|
|
29
|
+
SeatingChart: () => SeatingChart,
|
|
30
|
+
attachPickerFrame: () => attachPickerFrame
|
|
30
31
|
});
|
|
31
32
|
module.exports = __toCommonJS(index_exports);
|
|
32
33
|
|
|
@@ -398,10 +399,30 @@ var EmbeddedDesigner = class {
|
|
|
398
399
|
this.timeoutTimer = null;
|
|
399
400
|
this.phase = "loading";
|
|
400
401
|
this.restoreContainerPosition = null;
|
|
402
|
+
// Host-side fullscreen pin: saved state we restore on `off`/Escape/destroy.
|
|
403
|
+
this.pinned = false;
|
|
404
|
+
this.frameStyleBeforeFs = null;
|
|
405
|
+
this.docOverflowBeforeFs = null;
|
|
406
|
+
this.bodyOverflowBeforeFs = null;
|
|
407
|
+
this.fsKeyHandler = null;
|
|
408
|
+
/** Latest height (px string) the Designer reported; re-applied after unpin. */
|
|
409
|
+
this.lastAutoHeight = "";
|
|
401
410
|
this.handleMessage = (event) => {
|
|
402
411
|
if (!this.frame || event.origin !== this.designerOrigin || event.source !== this.frame.contentWindow) return;
|
|
403
412
|
if (!event.data || typeof event.data !== "object") return;
|
|
404
413
|
const data = event.data;
|
|
414
|
+
if (data.type === "seatlayer.designer.resize") {
|
|
415
|
+
if (this.autoResizeEnabled() && typeof data.px === "number" && Number.isFinite(data.px) && data.px > 0) {
|
|
416
|
+
this.lastAutoHeight = `${Math.round(data.px)}px`;
|
|
417
|
+
if (!this.pinned) this.frame.style.height = this.lastAutoHeight;
|
|
418
|
+
}
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (data.type === "seatlayer.designer.fullscreen") {
|
|
422
|
+
if (data.on === true) this.pinFullscreen();
|
|
423
|
+
else if (data.on === false) this.unpinFullscreen();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
405
426
|
if (typeof data.type !== "string" || !TYPES.has(data.type)) return;
|
|
406
427
|
const message = {
|
|
407
428
|
type: data.type,
|
|
@@ -484,6 +505,7 @@ var EmbeddedDesigner = class {
|
|
|
484
505
|
}
|
|
485
506
|
destroy() {
|
|
486
507
|
window.removeEventListener("message", this.handleMessage);
|
|
508
|
+
this.unpinFullscreen();
|
|
487
509
|
this.clearTimeoutTimer();
|
|
488
510
|
this.removeOverlay();
|
|
489
511
|
this.restoreContainerStyle();
|
|
@@ -491,10 +513,68 @@ var EmbeddedDesigner = class {
|
|
|
491
513
|
this.frame = null;
|
|
492
514
|
this.designerOrigin = "";
|
|
493
515
|
this.phase = "loading";
|
|
516
|
+
this.lastAutoHeight = "";
|
|
494
517
|
}
|
|
495
518
|
loadingStateEnabled() {
|
|
496
519
|
return this.options.showLoadingState !== false;
|
|
497
520
|
}
|
|
521
|
+
autoResizeEnabled() {
|
|
522
|
+
return this.options.autoResize !== false;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Pin the iframe over the host page as a viewport-filling overlay. We save the
|
|
526
|
+
* iframe's inline style and the document scroll state so `unpinFullscreen`
|
|
527
|
+
* restores everything exactly. Escape (host-side) also exits.
|
|
528
|
+
*/
|
|
529
|
+
pinFullscreen() {
|
|
530
|
+
if (this.pinned || !this.frame) return;
|
|
531
|
+
this.pinned = true;
|
|
532
|
+
this.frameStyleBeforeFs = this.frame.getAttribute("style");
|
|
533
|
+
Object.assign(this.frame.style, {
|
|
534
|
+
position: "fixed",
|
|
535
|
+
inset: "0",
|
|
536
|
+
width: "100vw",
|
|
537
|
+
height: "100vh",
|
|
538
|
+
margin: "0",
|
|
539
|
+
border: "0",
|
|
540
|
+
zIndex: "2147483000",
|
|
541
|
+
background: "#101625"
|
|
542
|
+
});
|
|
543
|
+
const docEl = document.documentElement;
|
|
544
|
+
this.docOverflowBeforeFs = docEl.style.overflow;
|
|
545
|
+
docEl.style.overflow = "hidden";
|
|
546
|
+
if (document.body) {
|
|
547
|
+
this.bodyOverflowBeforeFs = document.body.style.overflow;
|
|
548
|
+
document.body.style.overflow = "hidden";
|
|
549
|
+
}
|
|
550
|
+
this.fsKeyHandler = (event) => {
|
|
551
|
+
if (event.key === "Escape") this.unpinFullscreen();
|
|
552
|
+
};
|
|
553
|
+
window.addEventListener("keydown", this.fsKeyHandler);
|
|
554
|
+
}
|
|
555
|
+
/** Undo `pinFullscreen`: restore the iframe style + scroll lock. Idempotent. */
|
|
556
|
+
unpinFullscreen() {
|
|
557
|
+
if (!this.pinned) return;
|
|
558
|
+
this.pinned = false;
|
|
559
|
+
if (this.frame) {
|
|
560
|
+
if (this.frameStyleBeforeFs === null) this.frame.removeAttribute("style");
|
|
561
|
+
else this.frame.setAttribute("style", this.frameStyleBeforeFs);
|
|
562
|
+
if (this.autoResizeEnabled() && this.lastAutoHeight) this.frame.style.height = this.lastAutoHeight;
|
|
563
|
+
}
|
|
564
|
+
this.frameStyleBeforeFs = null;
|
|
565
|
+
if (this.docOverflowBeforeFs !== null) {
|
|
566
|
+
document.documentElement.style.overflow = this.docOverflowBeforeFs;
|
|
567
|
+
this.docOverflowBeforeFs = null;
|
|
568
|
+
}
|
|
569
|
+
if (this.bodyOverflowBeforeFs !== null && document.body) {
|
|
570
|
+
document.body.style.overflow = this.bodyOverflowBeforeFs;
|
|
571
|
+
this.bodyOverflowBeforeFs = null;
|
|
572
|
+
}
|
|
573
|
+
if (this.fsKeyHandler) {
|
|
574
|
+
window.removeEventListener("keydown", this.fsKeyHandler);
|
|
575
|
+
this.fsKeyHandler = null;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
498
578
|
clearTimeoutTimer() {
|
|
499
579
|
if (this.timeoutTimer !== null) {
|
|
500
580
|
clearTimeout(this.timeoutTimer);
|
|
@@ -1394,6 +1474,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
1394
1474
|
this.fsFallback = false;
|
|
1395
1475
|
this.fsChangeHandler = null;
|
|
1396
1476
|
this.fsEscHandler = null;
|
|
1477
|
+
/** True once we've asked the host page to pin us fullscreen (framed, no native). */
|
|
1478
|
+
this.framedFs = false;
|
|
1479
|
+
/** Last height (px) posted to a host frame; dedupes redundant reports. */
|
|
1480
|
+
this.lastPostedHeight = 0;
|
|
1397
1481
|
this.cbEl = null;
|
|
1398
1482
|
// modal plumbing (set by open())
|
|
1399
1483
|
this.modalScrim = null;
|
|
@@ -1497,24 +1581,98 @@ var SeatPicker = class _SeatPicker {
|
|
|
1497
1581
|
const sight = distance != null ? `${distance}${this.tf("picker.sightline", "m to stage \xB7 clear sightline")}` : this.tf("picker.sightlineClear", "Clear sightline");
|
|
1498
1582
|
return `<button type="button" class="sl-confirm-view sl-confirm-thumbwrap" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: seat.label })}"><img class="sl-confirm-thumb" src="${url}" alt="" /><span class="sl-confirm-thumb-badge">\u{1F52D} ${this.tf("picker.viewFromHere", "View from here")}</span></button><div class="sl-confirm-sight"><span aria-hidden="true">\u2713</span>${sight}</div>`;
|
|
1499
1583
|
}
|
|
1584
|
+
/** True when the picker is rendered inside an iframe (snippet embed at /e/:key). */
|
|
1585
|
+
isFramed() {
|
|
1586
|
+
return typeof window !== "undefined" && window.parent !== window;
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* Post a widget→host message when framed. targetOrigin is '*' because the
|
|
1590
|
+
* payload carries nothing sensitive (a height number / a fullscreen flag);
|
|
1591
|
+
* hosts verify `event.origin` on their side (see `attachPickerFrame`).
|
|
1592
|
+
*/
|
|
1593
|
+
postToHost(message) {
|
|
1594
|
+
if (!this.isFramed()) return;
|
|
1595
|
+
try {
|
|
1596
|
+
window.parent.postMessage(message, "*");
|
|
1597
|
+
} catch {
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Height (px) to advertise to a host frame.
|
|
1602
|
+
*
|
|
1603
|
+
* The picker fills whatever box it's given: `.sl-picker` is `height:100%;
|
|
1604
|
+
* overflow:hidden`, and the /e/:key shell mounts it `position:fixed; inset:0`.
|
|
1605
|
+
* So it has no intrinsic *document* height to read — `scrollHeight` just
|
|
1606
|
+
* collapses to the current viewport, which for a framed embed would echo the
|
|
1607
|
+
* host's own iframe height straight back (a circular value). We therefore
|
|
1608
|
+
* report a width-driven *desired* height: a pleasant landscape box on desktop,
|
|
1609
|
+
* taller on narrow widths where the bottom sheet needs room, clamped to the
|
|
1610
|
+
* widget's `min-height` of 420. Width is host-controlled and never moves in
|
|
1611
|
+
* response to the height we report, so this cannot feedback-loop.
|
|
1612
|
+
*/
|
|
1613
|
+
measureFramedHeight() {
|
|
1614
|
+
const root = this.root;
|
|
1615
|
+
if (!root) return 0;
|
|
1616
|
+
const width = root.clientWidth || (typeof window !== "undefined" ? window.innerWidth : 0) || 0;
|
|
1617
|
+
if (width <= 0) return 0;
|
|
1618
|
+
const ratio = width < 640 ? 1.2 : 0.62;
|
|
1619
|
+
return Math.max(420, Math.round(width * ratio));
|
|
1620
|
+
}
|
|
1621
|
+
/** Post `seatlayer:height` to the host when framed and the value changed. */
|
|
1622
|
+
reportFramedHeight() {
|
|
1623
|
+
if (!this.isFramed()) return;
|
|
1624
|
+
const px = this.measureFramedHeight();
|
|
1625
|
+
if (px <= 0 || px === this.lastPostedHeight) return;
|
|
1626
|
+
this.lastPostedHeight = px;
|
|
1627
|
+
this.postToHost({ type: "seatlayer:height", px });
|
|
1628
|
+
}
|
|
1500
1629
|
/** Full screen via the native API, falling back to a fixed-position overlay (iOS Safari). */
|
|
1501
1630
|
toggleFullscreen() {
|
|
1502
1631
|
const root = this.root;
|
|
1503
1632
|
if (!root) return;
|
|
1504
|
-
const active = !!document.fullscreenElement || this.fsFallback;
|
|
1633
|
+
const active = !!document.fullscreenElement || this.fsFallback || this.framedFs;
|
|
1505
1634
|
if (!active) {
|
|
1506
1635
|
if (root.requestFullscreen) {
|
|
1507
|
-
root.requestFullscreen().catch(() => this.
|
|
1636
|
+
root.requestFullscreen().catch(() => this.enterFsFallback());
|
|
1508
1637
|
} else {
|
|
1509
|
-
this.
|
|
1638
|
+
this.enterFsFallback();
|
|
1510
1639
|
}
|
|
1511
1640
|
} else if (document.fullscreenElement) {
|
|
1512
1641
|
void document.exitFullscreen().catch(() => {
|
|
1513
1642
|
});
|
|
1643
|
+
} else if (this.framedFs) {
|
|
1644
|
+
this.setFramedFs(false);
|
|
1514
1645
|
} else {
|
|
1515
1646
|
this.setFsFallback(false);
|
|
1516
1647
|
}
|
|
1517
1648
|
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Native element-fullscreen was unavailable or rejected. When framed, a CSS
|
|
1651
|
+
* `.sl-fs` overlay can't escape the iframe, so we ask the host page to pin us
|
|
1652
|
+
* (`seatlayer:fullscreen`). Otherwise (iOS Safari, same document) fall back to
|
|
1653
|
+
* the `.sl-fs` overlay as before.
|
|
1654
|
+
*/
|
|
1655
|
+
enterFsFallback() {
|
|
1656
|
+
if (this.isFramed()) this.setFramedFs(true);
|
|
1657
|
+
else this.setFsFallback(true);
|
|
1658
|
+
}
|
|
1659
|
+
/** Toggle host-driven (framed) fullscreen: post the flag + own the Esc key. */
|
|
1660
|
+
setFramedFs(on) {
|
|
1661
|
+
if (this.framedFs === on) return;
|
|
1662
|
+
this.framedFs = on;
|
|
1663
|
+
this.els.zfs?.setAttribute("aria-pressed", String(on || !!document.fullscreenElement));
|
|
1664
|
+
this.postToHost({ type: "seatlayer:fullscreen", on });
|
|
1665
|
+
if (on && !this.fsEscHandler) {
|
|
1666
|
+
this.fsEscHandler = (e) => {
|
|
1667
|
+
if (e.key === "Escape" && !document.fullscreenElement) this.setFramedFs(false);
|
|
1668
|
+
};
|
|
1669
|
+
window.addEventListener("keydown", this.fsEscHandler);
|
|
1670
|
+
} else if (!on && this.fsEscHandler) {
|
|
1671
|
+
window.removeEventListener("keydown", this.fsEscHandler);
|
|
1672
|
+
this.fsEscHandler = null;
|
|
1673
|
+
}
|
|
1674
|
+
requestAnimationFrame(() => this.controller.zoomToFit());
|
|
1675
|
+
}
|
|
1518
1676
|
setFsFallback(on) {
|
|
1519
1677
|
if (this.fsFallback === on) return;
|
|
1520
1678
|
this.fsFallback = on;
|
|
@@ -1684,6 +1842,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1684
1842
|
const applyLayout = () => {
|
|
1685
1843
|
const w = root.clientWidth;
|
|
1686
1844
|
if (w <= 0) return;
|
|
1845
|
+
this.reportFramedHeight();
|
|
1687
1846
|
const next = w < 640 ? "narrow" : "wide";
|
|
1688
1847
|
if (root.dataset.layout === next) return;
|
|
1689
1848
|
root.dataset.layout = next;
|
|
@@ -1700,7 +1859,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1700
1859
|
this.els.zfs.addEventListener("click", () => this.toggleFullscreen());
|
|
1701
1860
|
this.fsChangeHandler = () => {
|
|
1702
1861
|
if (!document.fullscreenElement) this.setFsFallback(false);
|
|
1703
|
-
this.els.zfs?.setAttribute("aria-pressed", String(!!document.fullscreenElement || this.fsFallback));
|
|
1862
|
+
this.els.zfs?.setAttribute("aria-pressed", String(!!document.fullscreenElement || this.fsFallback || this.framedFs));
|
|
1704
1863
|
requestAnimationFrame(() => this.controller.zoomToFit());
|
|
1705
1864
|
};
|
|
1706
1865
|
document.addEventListener("fullscreenchange", this.fsChangeHandler);
|
|
@@ -3458,6 +3617,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
3458
3617
|
this.motionTimers.clear();
|
|
3459
3618
|
this.ro?.disconnect();
|
|
3460
3619
|
this.ro = null;
|
|
3620
|
+
if (this.framedFs) this.setFramedFs(false);
|
|
3461
3621
|
if (this.escHandler) document.removeEventListener("keydown", this.escHandler);
|
|
3462
3622
|
if (this.fsChangeHandler) document.removeEventListener("fullscreenchange", this.fsChangeHandler);
|
|
3463
3623
|
if (this.fsEscHandler) window.removeEventListener("keydown", this.fsEscHandler);
|
|
@@ -3472,6 +3632,92 @@ var SeatPicker = class _SeatPicker {
|
|
|
3472
3632
|
}
|
|
3473
3633
|
};
|
|
3474
3634
|
|
|
3635
|
+
// src/attachPickerFrame.ts
|
|
3636
|
+
function attachPickerFrame(iframe, opts = {}) {
|
|
3637
|
+
let expectedOrigin = opts.origin ?? "";
|
|
3638
|
+
if (!expectedOrigin) {
|
|
3639
|
+
try {
|
|
3640
|
+
expectedOrigin = new URL(iframe.src, window.location.href).origin;
|
|
3641
|
+
} catch {
|
|
3642
|
+
expectedOrigin = "";
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
let pinned = false;
|
|
3646
|
+
let frameStyleBeforeFs = null;
|
|
3647
|
+
let docOverflowBeforeFs = null;
|
|
3648
|
+
let bodyOverflowBeforeFs = null;
|
|
3649
|
+
let lastAutoHeight = "";
|
|
3650
|
+
let keyHandler = null;
|
|
3651
|
+
const pin = () => {
|
|
3652
|
+
if (pinned) return;
|
|
3653
|
+
pinned = true;
|
|
3654
|
+
frameStyleBeforeFs = iframe.getAttribute("style");
|
|
3655
|
+
Object.assign(iframe.style, {
|
|
3656
|
+
position: "fixed",
|
|
3657
|
+
inset: "0",
|
|
3658
|
+
width: "100vw",
|
|
3659
|
+
height: "100vh",
|
|
3660
|
+
margin: "0",
|
|
3661
|
+
border: "0",
|
|
3662
|
+
zIndex: "2147483000",
|
|
3663
|
+
background: "#101625"
|
|
3664
|
+
});
|
|
3665
|
+
const docEl = document.documentElement;
|
|
3666
|
+
docOverflowBeforeFs = docEl.style.overflow;
|
|
3667
|
+
docEl.style.overflow = "hidden";
|
|
3668
|
+
if (document.body) {
|
|
3669
|
+
bodyOverflowBeforeFs = document.body.style.overflow;
|
|
3670
|
+
document.body.style.overflow = "hidden";
|
|
3671
|
+
}
|
|
3672
|
+
keyHandler = (event) => {
|
|
3673
|
+
if (event.key === "Escape") unpin();
|
|
3674
|
+
};
|
|
3675
|
+
window.addEventListener("keydown", keyHandler);
|
|
3676
|
+
};
|
|
3677
|
+
const unpin = () => {
|
|
3678
|
+
if (!pinned) return;
|
|
3679
|
+
pinned = false;
|
|
3680
|
+
if (frameStyleBeforeFs === null) iframe.removeAttribute("style");
|
|
3681
|
+
else iframe.setAttribute("style", frameStyleBeforeFs);
|
|
3682
|
+
frameStyleBeforeFs = null;
|
|
3683
|
+
if (lastAutoHeight) iframe.style.height = lastAutoHeight;
|
|
3684
|
+
if (docOverflowBeforeFs !== null) {
|
|
3685
|
+
document.documentElement.style.overflow = docOverflowBeforeFs;
|
|
3686
|
+
docOverflowBeforeFs = null;
|
|
3687
|
+
}
|
|
3688
|
+
if (bodyOverflowBeforeFs !== null && document.body) {
|
|
3689
|
+
document.body.style.overflow = bodyOverflowBeforeFs;
|
|
3690
|
+
bodyOverflowBeforeFs = null;
|
|
3691
|
+
}
|
|
3692
|
+
if (keyHandler) {
|
|
3693
|
+
window.removeEventListener("keydown", keyHandler);
|
|
3694
|
+
keyHandler = null;
|
|
3695
|
+
}
|
|
3696
|
+
};
|
|
3697
|
+
const onMessage = (event) => {
|
|
3698
|
+
if (event.source !== iframe.contentWindow) return;
|
|
3699
|
+
if (expectedOrigin && event.origin !== expectedOrigin) return;
|
|
3700
|
+
if (!event.data || typeof event.data !== "object") return;
|
|
3701
|
+
const data = event.data;
|
|
3702
|
+
if (data.type === "seatlayer:height") {
|
|
3703
|
+
if (typeof data.px === "number" && Number.isFinite(data.px) && data.px > 0) {
|
|
3704
|
+
lastAutoHeight = `${Math.round(data.px)}px`;
|
|
3705
|
+
if (!pinned) iframe.style.height = lastAutoHeight;
|
|
3706
|
+
}
|
|
3707
|
+
return;
|
|
3708
|
+
}
|
|
3709
|
+
if (data.type === "seatlayer:fullscreen") {
|
|
3710
|
+
if (data.on === true) pin();
|
|
3711
|
+
else if (data.on === false) unpin();
|
|
3712
|
+
}
|
|
3713
|
+
};
|
|
3714
|
+
window.addEventListener("message", onMessage);
|
|
3715
|
+
return () => {
|
|
3716
|
+
window.removeEventListener("message", onMessage);
|
|
3717
|
+
unpin();
|
|
3718
|
+
};
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3475
3721
|
// src/SeatManager.ts
|
|
3476
3722
|
var import_core3 = require("@seatlayer/core");
|
|
3477
3723
|
|
|
@@ -3553,6 +3799,21 @@ var ManageApi = class {
|
|
|
3553
3799
|
setHoldTtl(key, holdTtlMs) {
|
|
3554
3800
|
return this.auth(`/v1/events/${encodeURIComponent(key)}/hold-ttl`, { method: "POST", body: { holdTtlMs } });
|
|
3555
3801
|
}
|
|
3802
|
+
// ---- availability windows (token) ----
|
|
3803
|
+
/** The organizer's current per section/zone availability windows (needs
|
|
3804
|
+
* `event:view`). Ids absent from `rules` are open / on sale. */
|
|
3805
|
+
availability(key) {
|
|
3806
|
+
return this.auth(`/v1/events/${encodeURIComponent(key)}/availability`);
|
|
3807
|
+
}
|
|
3808
|
+
/** Replace the availability windows for a set of section/zone ids (needs
|
|
3809
|
+
* `event:block`). Ids absent from `rules` become open / on sale; a zone rule
|
|
3810
|
+
* cascades to its sections. The worker derives each id's seat labels, so
|
|
3811
|
+
* `labels` on the sent rules is best-effort. Resolves with the authoritative
|
|
3812
|
+
* effective `hidden` set (a due rule may fire at once) and the server-cleaned
|
|
3813
|
+
* `rules` map (fired timed/threshold windows dropped). */
|
|
3814
|
+
setAvailability(key, rules) {
|
|
3815
|
+
return this.auth(`/v1/events/${encodeURIComponent(key)}/availability`, { method: "POST", body: { rules } });
|
|
3816
|
+
}
|
|
3556
3817
|
// ---- reports (token) ----
|
|
3557
3818
|
report(key) {
|
|
3558
3819
|
return this.auth(`/v1/events/${encodeURIComponent(key)}/report`);
|
|
@@ -3580,6 +3841,27 @@ var ManageApi = class {
|
|
|
3580
3841
|
};
|
|
3581
3842
|
|
|
3582
3843
|
// src/SeatManager.ts
|
|
3844
|
+
function availabilityModeOf(rule) {
|
|
3845
|
+
return rule ? rule.mode : "open";
|
|
3846
|
+
}
|
|
3847
|
+
function availabilityRuleForMode(mode, seatLabels, prev) {
|
|
3848
|
+
switch (mode) {
|
|
3849
|
+
case "open":
|
|
3850
|
+
return null;
|
|
3851
|
+
case "hidden":
|
|
3852
|
+
return { mode: "hidden", labels: seatLabels };
|
|
3853
|
+
case "closed":
|
|
3854
|
+
return { mode: "closed", labels: seatLabels };
|
|
3855
|
+
case "timed":
|
|
3856
|
+
return { mode: "timed", revealAt: prev?.revealAt ?? Date.now() + 36e5, labels: seatLabels };
|
|
3857
|
+
case "threshold":
|
|
3858
|
+
return { mode: "threshold", thresholdPct: prev?.thresholdPct ?? 80, labels: seatLabels };
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
function toLocalInput(ms) {
|
|
3862
|
+
const d = new Date(ms - (/* @__PURE__ */ new Date()).getTimezoneOffset() * 6e4);
|
|
3863
|
+
return d.toISOString().slice(0, 16);
|
|
3864
|
+
}
|
|
3583
3865
|
function resolveContainer4(container) {
|
|
3584
3866
|
if (typeof container === "string") {
|
|
3585
3867
|
const el = document.querySelector(container);
|
|
@@ -3768,6 +4050,32 @@ var CSS2 = `
|
|
|
3768
4050
|
.slm-momentumhelp[hidden]{display:none}.slm-momentumscale{display:flex;align-items:center;gap:7px;color:var(--slm-muted);font-size:10px;font-weight:750;text-transform:uppercase;letter-spacing:.07em}
|
|
3769
4051
|
.slm-momentumgradient{height:6px;min-width:64px;flex:1;border-radius:999px;background:linear-gradient(90deg,#f4b740,#ef4444)}
|
|
3770
4052
|
.slm-momentumcopy{margin-top:7px;color:var(--slm-muted);font-size:11px;line-height:1.45}
|
|
4053
|
+
/* sections: availability windows */
|
|
4054
|
+
.slm-availlist{display:flex;flex-direction:column;gap:8px;margin:2px 0 12px}
|
|
4055
|
+
.slm-availrow{padding:10px;border:1px solid var(--slm-line);border-radius:10px;background:var(--slm-surface);transition:border-color .15s ease,opacity .15s ease}
|
|
4056
|
+
.slm-availrow.zone{background:color-mix(in srgb,var(--slm-surface) 82%,#000)}
|
|
4057
|
+
.slm-availrow.hidden{opacity:.62}.slm-availrow.closed{opacity:.82}
|
|
4058
|
+
.slm-availhead{display:flex;align-items:center;gap:8px}
|
|
4059
|
+
.slm-availlabel{display:flex;align-items:center;gap:5px;flex:1;min-width:0;font-size:12.5px;font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
4060
|
+
.slm-availcaret{flex:none;color:var(--slm-muted);font-size:10px}
|
|
4061
|
+
.slm-availcount{flex:none;font-size:11px;font-weight:700;color:var(--slm-muted);font-variant-numeric:tabular-nums}
|
|
4062
|
+
.slm-availbadge{flex:none;font-size:9px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;padding:2px 6px;border-radius:999px}
|
|
4063
|
+
.slm-availbadge.hidden{background:rgba(139,148,172,.18);color:#c2c9d8}
|
|
4064
|
+
.slm-availbadge.closed{background:rgba(244,183,64,.16);color:#f7ca6b}
|
|
4065
|
+
.slm-availselwrap{position:relative;flex:none;display:inline-flex}
|
|
4066
|
+
.slm-availmode{width:auto;max-width:190px;padding:6px 8px;font-size:11.5px;font-weight:700;cursor:pointer}
|
|
4067
|
+
.slm-availmode.on{border-color:var(--slm-accent);color:var(--slm-text)}
|
|
4068
|
+
.slm-availmode:disabled{opacity:.55;cursor:progress}
|
|
4069
|
+
.slm-availfollows{flex:none;padding:5px 10px;border:1px solid var(--slm-line);border-radius:7px;background:var(--slm-surface);color:var(--slm-muted);font-size:11px;font-weight:600;white-space:nowrap}
|
|
4070
|
+
.slm-availdetail{display:flex;align-items:center;gap:8px;margin-top:9px}
|
|
4071
|
+
.slm-availdetail .slm-input{flex:1}
|
|
4072
|
+
.slm-availpct{max-width:74px;flex:none!important}
|
|
4073
|
+
.slm-availpctlabel{font-size:11px;color:var(--slm-muted);font-weight:600;white-space:nowrap}
|
|
4074
|
+
.slm-availsummary{display:flex;align-items:center;gap:8px;padding:10px 12px;border:1px solid var(--slm-line);border-radius:9px;color:var(--slm-muted);font-size:12.5px}
|
|
4075
|
+
.slm-availdot{width:9px;height:9px;border-radius:50%;flex:none;background:#22a06b}.slm-availdot.warn{background:#f4b740}
|
|
4076
|
+
.slm-availcallout{display:flex;align-items:flex-start;gap:8px;margin-top:10px;padding:10px 12px;border:1px solid rgba(244,183,64,.45);border-radius:9px;background:rgba(244,183,64,.1)}
|
|
4077
|
+
.slm-availstar{flex:none;margin-top:1px;color:#f4b740;font-size:13px;line-height:1}
|
|
4078
|
+
.slm-availcallout p{font-size:11.5px;line-height:1.55;color:#f4d58a}.slm-availcallout b{color:#ffe4a3;font-weight:800}
|
|
3771
4079
|
.slm-inspect-card{padding:16px;border:1px solid var(--slm-line);border-radius:12px;background:var(--slm-surface)}
|
|
3772
4080
|
.slm-inspect-label{font-size:24px;font-weight:850;letter-spacing:-.02em;line-height:1.1}
|
|
3773
4081
|
.slm-inspect-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px 20px;margin-top:18px}
|
|
@@ -3868,6 +4176,13 @@ var SeatManager = class {
|
|
|
3868
4176
|
this.tokenRefreshInFlight = false;
|
|
3869
4177
|
this.sectionByObject = /* @__PURE__ */ new Map();
|
|
3870
4178
|
this.sectionLabelById = /* @__PURE__ */ new Map();
|
|
4179
|
+
this.sectionsBase = null;
|
|
4180
|
+
// Sections mode (availability windows): organizer rules + the live effective
|
|
4181
|
+
// hidden/closed sets from the snapshot + WS (a timed/threshold rule fires DO-side).
|
|
4182
|
+
this.availabilityRules = {};
|
|
4183
|
+
this.effectiveHidden = /* @__PURE__ */ new Set();
|
|
4184
|
+
this.effectiveClosed = /* @__PURE__ */ new Set();
|
|
4185
|
+
this.availabilitySaving = false;
|
|
3871
4186
|
this.lastSyncedAt = null;
|
|
3872
4187
|
this.blockedQuery = "";
|
|
3873
4188
|
this.blockedSection = "";
|
|
@@ -3886,6 +4201,7 @@ var SeatManager = class {
|
|
|
3886
4201
|
if (key === "m") this.setMode("view");
|
|
3887
4202
|
else if (key === "i") this.setMode("inspect");
|
|
3888
4203
|
else if (key === "b") this.setMode("block");
|
|
4204
|
+
else if (key === "s") this.setMode("sections");
|
|
3889
4205
|
else if (key === "f") this.toggleFullscreen();
|
|
3890
4206
|
else return;
|
|
3891
4207
|
event.preventDefault();
|
|
@@ -3929,7 +4245,8 @@ var SeatManager = class {
|
|
|
3929
4245
|
this.buildSectionOptions();
|
|
3930
4246
|
const [, controlRoom] = await Promise.all([
|
|
3931
4247
|
this.resnapshot(),
|
|
3932
|
-
this.refreshControlRoom().catch((err) => this.opts.onError?.(err))
|
|
4248
|
+
this.refreshControlRoom().catch((err) => this.opts.onError?.(err)),
|
|
4249
|
+
this.refreshAvailability()
|
|
3933
4250
|
]);
|
|
3934
4251
|
if (controlRoom?.activity) this.seedFeed(controlRoom.activity);
|
|
3935
4252
|
else this.api.log(this.key, { limit: 24 }).then((page) => this.seedFeed(page.entries)).catch(() => {
|
|
@@ -3954,6 +4271,7 @@ var SeatManager = class {
|
|
|
3954
4271
|
if (changed) this.renderer?.clearSelection();
|
|
3955
4272
|
this.paintModeTabs();
|
|
3956
4273
|
this.paintRail();
|
|
4274
|
+
this.applySectionCanvasTreatment();
|
|
3957
4275
|
if (changed) this.opts.onModeChange?.(mode);
|
|
3958
4276
|
}
|
|
3959
4277
|
/** Toggle the normalized sales-velocity outline overlay without changing seat colors. */
|
|
@@ -4245,6 +4563,7 @@ var SeatManager = class {
|
|
|
4245
4563
|
this.attempt = 0;
|
|
4246
4564
|
this.setLive(true);
|
|
4247
4565
|
void this.resnapshot().then(() => this.scheduleRevenueRefresh(0));
|
|
4566
|
+
void this.refreshAvailability();
|
|
4248
4567
|
};
|
|
4249
4568
|
ws.onmessage = (e) => this.onMessage(e);
|
|
4250
4569
|
ws.onclose = () => {
|
|
@@ -4276,6 +4595,9 @@ var SeatManager = class {
|
|
|
4276
4595
|
}
|
|
4277
4596
|
if (!msg || typeof msg !== "object") return;
|
|
4278
4597
|
const m = msg;
|
|
4598
|
+
if (Array.isArray(m.hidden) || Array.isArray(m.closed)) {
|
|
4599
|
+
this.updateEffectiveAvailability(m.hidden, m.closed);
|
|
4600
|
+
}
|
|
4279
4601
|
if (m.type === "presence") {
|
|
4280
4602
|
if (this.controlRoomSnapshot && typeof m.shoppingSessions === "number" && typeof m.activeHolds === "number") {
|
|
4281
4603
|
this.controlRoomSnapshot = {
|
|
@@ -4327,6 +4649,7 @@ var SeatManager = class {
|
|
|
4327
4649
|
try {
|
|
4328
4650
|
const objs = await this.api.objects(this.key);
|
|
4329
4651
|
this.applySnapshot(objs.seats);
|
|
4652
|
+
this.updateEffectiveAvailability(objs.hidden, objs.closed);
|
|
4330
4653
|
} catch {
|
|
4331
4654
|
}
|
|
4332
4655
|
}
|
|
@@ -4638,6 +4961,7 @@ var SeatManager = class {
|
|
|
4638
4961
|
<button class="slm-mode" role="tab" data-mode="view" title="Monitor (M)" aria-keyshortcuts="M">Monitor</button>
|
|
4639
4962
|
<button class="slm-mode" role="tab" data-mode="inspect" title="Inspect (I)" aria-keyshortcuts="I">Inspect</button>
|
|
4640
4963
|
<button class="slm-mode" role="tab" data-mode="block" title="Block (B)" aria-keyshortcuts="B">Block</button>
|
|
4964
|
+
<button class="slm-mode" role="tab" data-mode="sections" title="Sections (S)" aria-keyshortcuts="S">Sections</button>
|
|
4641
4965
|
</div>
|
|
4642
4966
|
<span class="slm-live"><span class="slm-live-dot"></span><span data-ref="livetext">CONNECTING</span></span>
|
|
4643
4967
|
<div class="slm-bar-actions">
|
|
@@ -4704,6 +5028,7 @@ var SeatManager = class {
|
|
|
4704
5028
|
if (!this.doc) return;
|
|
4705
5029
|
try {
|
|
4706
5030
|
const secs = (0, import_core3.computeSections)(this.doc);
|
|
5031
|
+
this.sectionsBase = secs;
|
|
4707
5032
|
this.sectionOptions = [];
|
|
4708
5033
|
this.sectionByObject = new Map(secs.objectToSection);
|
|
4709
5034
|
this.sectionLabelById.clear();
|
|
@@ -4829,6 +5154,7 @@ var SeatManager = class {
|
|
|
4829
5154
|
paintRail() {
|
|
4830
5155
|
if (this.mode === "view") this.renderViewRail();
|
|
4831
5156
|
else if (this.mode === "inspect") this.renderInspectRail(this.getSelection());
|
|
5157
|
+
else if (this.mode === "sections") this.renderSectionsRail();
|
|
4832
5158
|
else this.renderBlockRail();
|
|
4833
5159
|
this.updateZoomHint();
|
|
4834
5160
|
}
|
|
@@ -4951,6 +5277,259 @@ var SeatManager = class {
|
|
|
4951
5277
|
</div>
|
|
4952
5278
|
</div>`;
|
|
4953
5279
|
}
|
|
5280
|
+
// ---- sections: availability windows --------------------------------------
|
|
5281
|
+
/** Pull the organizer's availability rules (event:view). Called on load and on
|
|
5282
|
+
* every WS (re)connect, mirroring how the other panels re-hydrate. `closed` is
|
|
5283
|
+
* deterministic from the rules; `hidden` (which folds in already-due timed /
|
|
5284
|
+
* threshold windows) comes from the snapshot + WS effective set. */
|
|
5285
|
+
async refreshAvailability() {
|
|
5286
|
+
try {
|
|
5287
|
+
const res = await this.withAuthRetry(() => this.api.availability(this.key));
|
|
5288
|
+
this.availabilityRules = res.rules ?? {};
|
|
5289
|
+
this.effectiveClosed = new Set(this.closedIdsFromRules(this.availabilityRules));
|
|
5290
|
+
if (this.mode === "sections") this.renderSectionsRail();
|
|
5291
|
+
this.applySectionCanvasTreatment();
|
|
5292
|
+
} catch (err) {
|
|
5293
|
+
this.opts.onError?.(err);
|
|
5294
|
+
}
|
|
5295
|
+
}
|
|
5296
|
+
/** Run a token-authed op; on a 401 re-mint via onTokenRefresh and retry once. */
|
|
5297
|
+
async withAuthRetry(op) {
|
|
5298
|
+
try {
|
|
5299
|
+
return await op();
|
|
5300
|
+
} catch (err) {
|
|
5301
|
+
if (err instanceof ManageApiError && err.status === 401 && this.opts.onTokenRefresh && !this.tokenRefreshInFlight) {
|
|
5302
|
+
await this.rotateToken();
|
|
5303
|
+
return op();
|
|
5304
|
+
}
|
|
5305
|
+
throw err;
|
|
5306
|
+
}
|
|
5307
|
+
}
|
|
5308
|
+
closedIdsFromRules(rules) {
|
|
5309
|
+
return Object.entries(rules).filter(([, r]) => r.mode === "closed").map(([id]) => id);
|
|
5310
|
+
}
|
|
5311
|
+
/** Adopt a new effective hidden/closed set (from a snapshot or WS broadcast) and
|
|
5312
|
+
* repaint the rail + canvas when it actually moves. */
|
|
5313
|
+
updateEffectiveAvailability(hidden, closed) {
|
|
5314
|
+
let changed = false;
|
|
5315
|
+
if (Array.isArray(hidden)) {
|
|
5316
|
+
this.effectiveHidden = new Set(hidden.filter((x) => typeof x === "string"));
|
|
5317
|
+
changed = true;
|
|
5318
|
+
}
|
|
5319
|
+
if (Array.isArray(closed)) {
|
|
5320
|
+
this.effectiveClosed = new Set(closed.filter((x) => typeof x === "string"));
|
|
5321
|
+
changed = true;
|
|
5322
|
+
}
|
|
5323
|
+
if (!changed) return;
|
|
5324
|
+
if (this.mode === "sections") this.renderSectionsRail();
|
|
5325
|
+
this.applySectionCanvasTreatment();
|
|
5326
|
+
}
|
|
5327
|
+
/** Canvas read of the availability state: dim hidden sections to a whisper,
|
|
5328
|
+
* half-light closed sections, leave open sections normal. Only in Sections mode;
|
|
5329
|
+
* cleared in every other tool. */
|
|
5330
|
+
applySectionCanvasTreatment() {
|
|
5331
|
+
if (!this.renderer) return;
|
|
5332
|
+
if (this.mode === "sections") {
|
|
5333
|
+
this.renderer.setDimmedSections([...this.effectiveHidden]);
|
|
5334
|
+
this.renderer.setClosedSections([...this.effectiveClosed]);
|
|
5335
|
+
} else {
|
|
5336
|
+
this.renderer.setDimmedSections(null);
|
|
5337
|
+
this.renderer.setClosedSections(null);
|
|
5338
|
+
}
|
|
5339
|
+
}
|
|
5340
|
+
/** Zone-grouped render tree: each zone header then its sections (which follow the
|
|
5341
|
+
* zone window), then loose sections + the ungrouped bucket. Effective hidden /
|
|
5342
|
+
* closed come from the live sets, rules from the organizer map. */
|
|
5343
|
+
buildSectionRows() {
|
|
5344
|
+
const base = this.sectionsBase;
|
|
5345
|
+
if (!base) return { rows: [], hiddenSections: 0, closedSections: 0 };
|
|
5346
|
+
const zones = this.doc?.zones ?? [];
|
|
5347
|
+
const byZone = /* @__PURE__ */ new Map();
|
|
5348
|
+
const loose = [];
|
|
5349
|
+
for (const s of base.sections) {
|
|
5350
|
+
if (s.zone && zones.some((z) => z.id === s.zone)) {
|
|
5351
|
+
const list = byZone.get(s.zone) ?? [];
|
|
5352
|
+
list.push(s);
|
|
5353
|
+
byZone.set(s.zone, list);
|
|
5354
|
+
} else {
|
|
5355
|
+
loose.push(s);
|
|
5356
|
+
}
|
|
5357
|
+
}
|
|
5358
|
+
const rows = [];
|
|
5359
|
+
let hiddenSections = 0;
|
|
5360
|
+
let closedSections = 0;
|
|
5361
|
+
const push = (kind, node, zoneRuled, parentClosed = false) => {
|
|
5362
|
+
const rule = this.availabilityRules[node.id] ?? null;
|
|
5363
|
+
const effClosed = this.effectiveClosed.has(node.id) || parentClosed;
|
|
5364
|
+
const effHidden = this.effectiveHidden.has(node.id) || zoneRuled && !effClosed;
|
|
5365
|
+
if (kind === "section" && effHidden) hiddenSections += 1;
|
|
5366
|
+
if (kind === "section" && effClosed) closedSections += 1;
|
|
5367
|
+
rows.push({
|
|
5368
|
+
kind,
|
|
5369
|
+
id: node.id,
|
|
5370
|
+
label: node.label,
|
|
5371
|
+
seatCount: node.seatCount,
|
|
5372
|
+
seatLabels: node.seatLabels,
|
|
5373
|
+
rule,
|
|
5374
|
+
hidden: effHidden,
|
|
5375
|
+
closed: effClosed,
|
|
5376
|
+
followsZone: kind === "section" && zoneRuled
|
|
5377
|
+
});
|
|
5378
|
+
};
|
|
5379
|
+
for (const z of zones) {
|
|
5380
|
+
const secs = byZone.get(z.id);
|
|
5381
|
+
if (!secs || !secs.length) continue;
|
|
5382
|
+
const zoneNode = {
|
|
5383
|
+
id: z.id,
|
|
5384
|
+
label: z.label || "Zone",
|
|
5385
|
+
seatCount: secs.reduce((sum, s) => sum + s.seatCount, 0),
|
|
5386
|
+
seatLabels: secs.flatMap((s) => s.seatLabels)
|
|
5387
|
+
};
|
|
5388
|
+
const zoneRuled = !!this.availabilityRules[z.id];
|
|
5389
|
+
const zoneClosed = this.availabilityRules[z.id]?.mode === "closed";
|
|
5390
|
+
push("zone", zoneNode, false);
|
|
5391
|
+
for (const s of secs) push("section", s, zoneRuled, zoneClosed);
|
|
5392
|
+
}
|
|
5393
|
+
for (const s of loose) push("section", s, false);
|
|
5394
|
+
if (base.ungrouped) {
|
|
5395
|
+
const u = base.ungrouped;
|
|
5396
|
+
push("section", { id: import_core3.UNGROUPED_ID, label: u.label, seatCount: u.seatCount, seatLabels: u.seatLabels }, false);
|
|
5397
|
+
}
|
|
5398
|
+
return { rows, hiddenSections, closedSections };
|
|
5399
|
+
}
|
|
5400
|
+
renderSectionsRail() {
|
|
5401
|
+
const { rows, hiddenSections, closedSections } = this.buildSectionRows();
|
|
5402
|
+
if (!rows.length) {
|
|
5403
|
+
this.els.rail.innerHTML = `
|
|
5404
|
+
<p class="slm-eyebrow">Availability windows</p>
|
|
5405
|
+
<p class="slm-hint">Draw sections or zones in the designer to schedule availability per area. This chart has none yet.</p>
|
|
5406
|
+
<div class="slm-empty">No sections on this chart.</div>`;
|
|
5407
|
+
return;
|
|
5408
|
+
}
|
|
5409
|
+
const parts = [];
|
|
5410
|
+
if (hiddenSections) parts.push(`${hiddenSections} hidden`);
|
|
5411
|
+
if (closedSections) parts.push(`${closedSections} closed`);
|
|
5412
|
+
const summary = parts.length ? parts.join(" \xB7 ") : "All sections open and on sale";
|
|
5413
|
+
const warn = hiddenSections > 0 || closedSections > 0;
|
|
5414
|
+
this.els.rail.innerHTML = `
|
|
5415
|
+
<p class="slm-eyebrow">Availability windows</p>
|
|
5416
|
+
<p class="slm-hint">Control when each zone or section goes on sale. Keep it hidden, reveal it at a set time, or <b>auto-reveal once the rest sells past a threshold</b>. Hidden seats vanish for buyers; closed seats stay on the map (flat grey) but can't be bought.</p>
|
|
5417
|
+
<div class="slm-availlist" data-ref="availlist">${rows.map((row) => this.sectionRowHtml(row)).join("")}</div>
|
|
5418
|
+
<div class="slm-availsummary">
|
|
5419
|
+
<span class="slm-availdot${warn ? " warn" : ""}"></span>
|
|
5420
|
+
<span>${esc(summary)}</span>
|
|
5421
|
+
</div>
|
|
5422
|
+
<div class="slm-availcallout">
|
|
5423
|
+
<span class="slm-availstar" aria-hidden="true">\u2726</span>
|
|
5424
|
+
<p><b>Auto-reveal at % sold</b> is our differentiator \u2014 demand-triggered release: the balcony opens itself the moment the stalls hit the threshold. Neither seats.io nor Ticketmaster ships this.</p>
|
|
5425
|
+
</div>`;
|
|
5426
|
+
this.wireSectionRail();
|
|
5427
|
+
this.applySectionCanvasTreatment();
|
|
5428
|
+
}
|
|
5429
|
+
sectionRowHtml(row) {
|
|
5430
|
+
const mode = availabilityModeOf(row.rule);
|
|
5431
|
+
const cls = `slm-availrow${row.kind === "zone" ? " zone" : ""}${row.hidden ? " hidden" : ""}${row.closed ? " closed" : ""}`;
|
|
5432
|
+
const disabled = this.availabilitySaving ? " disabled" : "";
|
|
5433
|
+
const option = (value, text) => `<option value="${value}"${mode === value ? " selected" : ""}>${text}</option>`;
|
|
5434
|
+
const control = row.followsZone ? '<span class="slm-availfollows">Follows zone</span>' : `<span class="slm-availselwrap">
|
|
5435
|
+
<select class="slm-select slm-availmode${mode !== "open" ? " on" : ""}" data-avail-id="${esc(row.id)}"${disabled} aria-label="Availability for ${esc(row.label)}">
|
|
5436
|
+
${option("open", "Open \u2014 on sale")}
|
|
5437
|
+
${option("closed", "Closed \u2014 visible, not on sale")}
|
|
5438
|
+
${option("hidden", "Hidden \u2014 off the buyer map")}
|
|
5439
|
+
${option("timed", "Reveal at a time")}
|
|
5440
|
+
${option("threshold", "Auto-reveal at % sold")}
|
|
5441
|
+
</select>
|
|
5442
|
+
</span>`;
|
|
5443
|
+
let detail = "";
|
|
5444
|
+
if (!row.followsZone && mode === "timed") {
|
|
5445
|
+
const value = row.rule?.revealAt ? esc(toLocalInput(row.rule.revealAt)) : "";
|
|
5446
|
+
detail = `<div class="slm-availdetail">
|
|
5447
|
+
<input type="datetime-local" class="slm-input" data-avail-reveal="${esc(row.id)}" value="${value}"${disabled} aria-label="Reveal time for ${esc(row.label)}" />
|
|
5448
|
+
</div>`;
|
|
5449
|
+
} else if (!row.followsZone && mode === "threshold") {
|
|
5450
|
+
const pct = row.rule?.thresholdPct ?? 80;
|
|
5451
|
+
detail = `<div class="slm-availdetail">
|
|
5452
|
+
<span class="slm-availpctlabel">Reveal at</span>
|
|
5453
|
+
<input type="number" min="1" max="100" class="slm-input slm-availpct" data-avail-pct="${esc(row.id)}" value="${esc(pct)}"${disabled} aria-label="Percent sold to reveal ${esc(row.label)}" />
|
|
5454
|
+
<span class="slm-availpctlabel">% sold</span>
|
|
5455
|
+
</div>`;
|
|
5456
|
+
}
|
|
5457
|
+
const badge = row.closed ? '<span class="slm-availbadge closed">Closed</span>' : row.hidden ? '<span class="slm-availbadge hidden">Hidden</span>' : "";
|
|
5458
|
+
const caret = row.kind === "zone" ? `<span class="slm-availcaret" aria-hidden="true">${row.hidden ? "\u25B8" : "\u25BE"}</span>` : "";
|
|
5459
|
+
return `<div class="${cls}">
|
|
5460
|
+
<div class="slm-availhead">
|
|
5461
|
+
<span class="slm-availlabel">${caret}${esc(row.label)}</span>
|
|
5462
|
+
${badge}
|
|
5463
|
+
<span class="slm-availcount">${row.seatCount.toLocaleString()}</span>
|
|
5464
|
+
${control}
|
|
5465
|
+
</div>
|
|
5466
|
+
${detail}
|
|
5467
|
+
</div>`;
|
|
5468
|
+
}
|
|
5469
|
+
wireSectionRail() {
|
|
5470
|
+
const rail = this.els.rail;
|
|
5471
|
+
if (!rail) return;
|
|
5472
|
+
rail.querySelectorAll("[data-avail-id]").forEach((select) => {
|
|
5473
|
+
select.addEventListener("change", () => this.setSectionMode(select.dataset.availId, select.value));
|
|
5474
|
+
});
|
|
5475
|
+
rail.querySelectorAll("[data-avail-reveal]").forEach((input) => {
|
|
5476
|
+
input.addEventListener("change", () => {
|
|
5477
|
+
const ms = new Date(input.value).getTime();
|
|
5478
|
+
if (Number.isFinite(ms)) this.setSectionRulePatch(input.dataset.availReveal, { revealAt: ms });
|
|
5479
|
+
});
|
|
5480
|
+
});
|
|
5481
|
+
rail.querySelectorAll("[data-avail-pct]").forEach((input) => {
|
|
5482
|
+
input.addEventListener("change", () => {
|
|
5483
|
+
const pct = Math.max(1, Math.min(100, Number(input.value) || 0));
|
|
5484
|
+
this.setSectionRulePatch(input.dataset.availPct, { thresholdPct: pct });
|
|
5485
|
+
});
|
|
5486
|
+
});
|
|
5487
|
+
}
|
|
5488
|
+
/** Change one row's availability mode. A zone rule subsumes its child section
|
|
5489
|
+
* rules, so those are dropped from the map (the zone window is the truth). */
|
|
5490
|
+
setSectionMode(id, mode) {
|
|
5491
|
+
const row = this.buildSectionRows().rows.find((r) => r.id === id);
|
|
5492
|
+
const seatLabels = row?.seatLabels ?? this.availabilityRules[id]?.labels ?? [];
|
|
5493
|
+
const next = { ...this.availabilityRules };
|
|
5494
|
+
const rule = availabilityRuleForMode(mode, seatLabels, this.availabilityRules[id]);
|
|
5495
|
+
if (rule) next[id] = rule;
|
|
5496
|
+
else delete next[id];
|
|
5497
|
+
if (row?.kind === "zone" && this.sectionsBase) {
|
|
5498
|
+
for (const s of this.sectionsBase.sections) if (s.zone === id) delete next[s.id];
|
|
5499
|
+
}
|
|
5500
|
+
void this.persistAvailability(next);
|
|
5501
|
+
}
|
|
5502
|
+
/** Edit a timed reveal time / threshold percent on an existing row rule. */
|
|
5503
|
+
setSectionRulePatch(id, patch) {
|
|
5504
|
+
const cur = this.availabilityRules[id];
|
|
5505
|
+
if (!cur) return;
|
|
5506
|
+
const row = this.buildSectionRows().rows.find((r) => r.id === id);
|
|
5507
|
+
const labels = row?.seatLabels ?? cur.labels ?? [];
|
|
5508
|
+
void this.persistAvailability({ ...this.availabilityRules, [id]: { ...cur, ...patch, labels } });
|
|
5509
|
+
}
|
|
5510
|
+
/** Optimistically adopt the new rules, then reconcile with the server-cleaned
|
|
5511
|
+
* map + effective hidden/closed sets. Rolls back the rules on failure. */
|
|
5512
|
+
async persistAvailability(next) {
|
|
5513
|
+
const prev = this.availabilityRules;
|
|
5514
|
+
this.availabilityRules = next;
|
|
5515
|
+
this.availabilitySaving = true;
|
|
5516
|
+
if (this.mode === "sections") this.renderSectionsRail();
|
|
5517
|
+
try {
|
|
5518
|
+
const res = await this.withAuthRetry(() => this.api.setAvailability(this.key, next));
|
|
5519
|
+
this.availabilityRules = res.rules;
|
|
5520
|
+
this.effectiveHidden = new Set(res.hidden);
|
|
5521
|
+
this.effectiveClosed = new Set(this.closedIdsFromRules(res.rules));
|
|
5522
|
+
this.availabilitySaving = false;
|
|
5523
|
+
if (this.mode === "sections") this.renderSectionsRail();
|
|
5524
|
+
this.applySectionCanvasTreatment();
|
|
5525
|
+
} catch (err) {
|
|
5526
|
+
this.availabilityRules = prev;
|
|
5527
|
+
this.availabilitySaving = false;
|
|
5528
|
+
if (this.mode === "sections") this.renderSectionsRail();
|
|
5529
|
+
this.toastErr("Couldn't update availability. Try again.");
|
|
5530
|
+
this.opts.onError?.(err);
|
|
5531
|
+
}
|
|
5532
|
+
}
|
|
4954
5533
|
paintLegend(t3) {
|
|
4955
5534
|
if (!this.els.legend) return;
|
|
4956
5535
|
this.els.legend.innerHTML = LEGEND.map((l) => `<div class="slm-legrow"><span class="slm-legdot" style="background:${l.color}"></span>
|
|
@@ -5264,6 +5843,7 @@ var SeatManager = class {
|
|
|
5264
5843
|
ManageApiError,
|
|
5265
5844
|
SeatManager,
|
|
5266
5845
|
SeatPicker,
|
|
5267
|
-
SeatingChart
|
|
5846
|
+
SeatingChart,
|
|
5847
|
+
attachPickerFrame
|
|
5268
5848
|
});
|
|
5269
5849
|
//# sourceMappingURL=index.cjs.map
|