@seatlayer/js 0.18.1 → 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 +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.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);
|
|
@@ -1371,6 +1450,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
1371
1450
|
this.fsFallback = false;
|
|
1372
1451
|
this.fsChangeHandler = null;
|
|
1373
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;
|
|
1374
1457
|
this.cbEl = null;
|
|
1375
1458
|
// modal plumbing (set by open())
|
|
1376
1459
|
this.modalScrim = null;
|
|
@@ -1474,24 +1557,98 @@ var SeatPicker = class _SeatPicker {
|
|
|
1474
1557
|
const sight = distance != null ? `${distance}${this.tf("picker.sightline", "m to stage \xB7 clear sightline")}` : this.tf("picker.sightlineClear", "Clear sightline");
|
|
1475
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>`;
|
|
1476
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
|
+
}
|
|
1477
1605
|
/** Full screen via the native API, falling back to a fixed-position overlay (iOS Safari). */
|
|
1478
1606
|
toggleFullscreen() {
|
|
1479
1607
|
const root = this.root;
|
|
1480
1608
|
if (!root) return;
|
|
1481
|
-
const active = !!document.fullscreenElement || this.fsFallback;
|
|
1609
|
+
const active = !!document.fullscreenElement || this.fsFallback || this.framedFs;
|
|
1482
1610
|
if (!active) {
|
|
1483
1611
|
if (root.requestFullscreen) {
|
|
1484
|
-
root.requestFullscreen().catch(() => this.
|
|
1612
|
+
root.requestFullscreen().catch(() => this.enterFsFallback());
|
|
1485
1613
|
} else {
|
|
1486
|
-
this.
|
|
1614
|
+
this.enterFsFallback();
|
|
1487
1615
|
}
|
|
1488
1616
|
} else if (document.fullscreenElement) {
|
|
1489
1617
|
void document.exitFullscreen().catch(() => {
|
|
1490
1618
|
});
|
|
1619
|
+
} else if (this.framedFs) {
|
|
1620
|
+
this.setFramedFs(false);
|
|
1491
1621
|
} else {
|
|
1492
1622
|
this.setFsFallback(false);
|
|
1493
1623
|
}
|
|
1494
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
|
+
}
|
|
1495
1652
|
setFsFallback(on) {
|
|
1496
1653
|
if (this.fsFallback === on) return;
|
|
1497
1654
|
this.fsFallback = on;
|
|
@@ -1661,6 +1818,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1661
1818
|
const applyLayout = () => {
|
|
1662
1819
|
const w = root.clientWidth;
|
|
1663
1820
|
if (w <= 0) return;
|
|
1821
|
+
this.reportFramedHeight();
|
|
1664
1822
|
const next = w < 640 ? "narrow" : "wide";
|
|
1665
1823
|
if (root.dataset.layout === next) return;
|
|
1666
1824
|
root.dataset.layout = next;
|
|
@@ -1677,7 +1835,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1677
1835
|
this.els.zfs.addEventListener("click", () => this.toggleFullscreen());
|
|
1678
1836
|
this.fsChangeHandler = () => {
|
|
1679
1837
|
if (!document.fullscreenElement) this.setFsFallback(false);
|
|
1680
|
-
this.els.zfs?.setAttribute("aria-pressed", String(!!document.fullscreenElement || this.fsFallback));
|
|
1838
|
+
this.els.zfs?.setAttribute("aria-pressed", String(!!document.fullscreenElement || this.fsFallback || this.framedFs));
|
|
1681
1839
|
requestAnimationFrame(() => this.controller.zoomToFit());
|
|
1682
1840
|
};
|
|
1683
1841
|
document.addEventListener("fullscreenchange", this.fsChangeHandler);
|
|
@@ -3435,6 +3593,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
3435
3593
|
this.motionTimers.clear();
|
|
3436
3594
|
this.ro?.disconnect();
|
|
3437
3595
|
this.ro = null;
|
|
3596
|
+
if (this.framedFs) this.setFramedFs(false);
|
|
3438
3597
|
if (this.escHandler) document.removeEventListener("keydown", this.escHandler);
|
|
3439
3598
|
if (this.fsChangeHandler) document.removeEventListener("fullscreenchange", this.fsChangeHandler);
|
|
3440
3599
|
if (this.fsEscHandler) window.removeEventListener("keydown", this.fsEscHandler);
|
|
@@ -3449,6 +3608,92 @@ var SeatPicker = class _SeatPicker {
|
|
|
3449
3608
|
}
|
|
3450
3609
|
};
|
|
3451
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
|
+
|
|
3452
3697
|
// src/SeatManager.ts
|
|
3453
3698
|
import {
|
|
3454
3699
|
SeatmapRenderer,
|
|
@@ -3535,6 +3780,21 @@ var ManageApi = class {
|
|
|
3535
3780
|
setHoldTtl(key, holdTtlMs) {
|
|
3536
3781
|
return this.auth(`/v1/events/${encodeURIComponent(key)}/hold-ttl`, { method: "POST", body: { holdTtlMs } });
|
|
3537
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
|
+
}
|
|
3538
3798
|
// ---- reports (token) ----
|
|
3539
3799
|
report(key) {
|
|
3540
3800
|
return this.auth(`/v1/events/${encodeURIComponent(key)}/report`);
|
|
@@ -3562,6 +3822,27 @@ var ManageApi = class {
|
|
|
3562
3822
|
};
|
|
3563
3823
|
|
|
3564
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
|
+
}
|
|
3565
3846
|
function resolveContainer4(container) {
|
|
3566
3847
|
if (typeof container === "string") {
|
|
3567
3848
|
const el = document.querySelector(container);
|
|
@@ -3750,6 +4031,32 @@ var CSS2 = `
|
|
|
3750
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}
|
|
3751
4032
|
.slm-momentumgradient{height:6px;min-width:64px;flex:1;border-radius:999px;background:linear-gradient(90deg,#f4b740,#ef4444)}
|
|
3752
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}
|
|
3753
4060
|
.slm-inspect-card{padding:16px;border:1px solid var(--slm-line);border-radius:12px;background:var(--slm-surface)}
|
|
3754
4061
|
.slm-inspect-label{font-size:24px;font-weight:850;letter-spacing:-.02em;line-height:1.1}
|
|
3755
4062
|
.slm-inspect-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px 20px;margin-top:18px}
|
|
@@ -3850,6 +4157,13 @@ var SeatManager = class {
|
|
|
3850
4157
|
this.tokenRefreshInFlight = false;
|
|
3851
4158
|
this.sectionByObject = /* @__PURE__ */ new Map();
|
|
3852
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;
|
|
3853
4167
|
this.lastSyncedAt = null;
|
|
3854
4168
|
this.blockedQuery = "";
|
|
3855
4169
|
this.blockedSection = "";
|
|
@@ -3868,6 +4182,7 @@ var SeatManager = class {
|
|
|
3868
4182
|
if (key === "m") this.setMode("view");
|
|
3869
4183
|
else if (key === "i") this.setMode("inspect");
|
|
3870
4184
|
else if (key === "b") this.setMode("block");
|
|
4185
|
+
else if (key === "s") this.setMode("sections");
|
|
3871
4186
|
else if (key === "f") this.toggleFullscreen();
|
|
3872
4187
|
else return;
|
|
3873
4188
|
event.preventDefault();
|
|
@@ -3911,7 +4226,8 @@ var SeatManager = class {
|
|
|
3911
4226
|
this.buildSectionOptions();
|
|
3912
4227
|
const [, controlRoom] = await Promise.all([
|
|
3913
4228
|
this.resnapshot(),
|
|
3914
|
-
this.refreshControlRoom().catch((err) => this.opts.onError?.(err))
|
|
4229
|
+
this.refreshControlRoom().catch((err) => this.opts.onError?.(err)),
|
|
4230
|
+
this.refreshAvailability()
|
|
3915
4231
|
]);
|
|
3916
4232
|
if (controlRoom?.activity) this.seedFeed(controlRoom.activity);
|
|
3917
4233
|
else this.api.log(this.key, { limit: 24 }).then((page) => this.seedFeed(page.entries)).catch(() => {
|
|
@@ -3936,6 +4252,7 @@ var SeatManager = class {
|
|
|
3936
4252
|
if (changed) this.renderer?.clearSelection();
|
|
3937
4253
|
this.paintModeTabs();
|
|
3938
4254
|
this.paintRail();
|
|
4255
|
+
this.applySectionCanvasTreatment();
|
|
3939
4256
|
if (changed) this.opts.onModeChange?.(mode);
|
|
3940
4257
|
}
|
|
3941
4258
|
/** Toggle the normalized sales-velocity outline overlay without changing seat colors. */
|
|
@@ -4227,6 +4544,7 @@ var SeatManager = class {
|
|
|
4227
4544
|
this.attempt = 0;
|
|
4228
4545
|
this.setLive(true);
|
|
4229
4546
|
void this.resnapshot().then(() => this.scheduleRevenueRefresh(0));
|
|
4547
|
+
void this.refreshAvailability();
|
|
4230
4548
|
};
|
|
4231
4549
|
ws.onmessage = (e) => this.onMessage(e);
|
|
4232
4550
|
ws.onclose = () => {
|
|
@@ -4258,6 +4576,9 @@ var SeatManager = class {
|
|
|
4258
4576
|
}
|
|
4259
4577
|
if (!msg || typeof msg !== "object") return;
|
|
4260
4578
|
const m = msg;
|
|
4579
|
+
if (Array.isArray(m.hidden) || Array.isArray(m.closed)) {
|
|
4580
|
+
this.updateEffectiveAvailability(m.hidden, m.closed);
|
|
4581
|
+
}
|
|
4261
4582
|
if (m.type === "presence") {
|
|
4262
4583
|
if (this.controlRoomSnapshot && typeof m.shoppingSessions === "number" && typeof m.activeHolds === "number") {
|
|
4263
4584
|
this.controlRoomSnapshot = {
|
|
@@ -4309,6 +4630,7 @@ var SeatManager = class {
|
|
|
4309
4630
|
try {
|
|
4310
4631
|
const objs = await this.api.objects(this.key);
|
|
4311
4632
|
this.applySnapshot(objs.seats);
|
|
4633
|
+
this.updateEffectiveAvailability(objs.hidden, objs.closed);
|
|
4312
4634
|
} catch {
|
|
4313
4635
|
}
|
|
4314
4636
|
}
|
|
@@ -4620,6 +4942,7 @@ var SeatManager = class {
|
|
|
4620
4942
|
<button class="slm-mode" role="tab" data-mode="view" title="Monitor (M)" aria-keyshortcuts="M">Monitor</button>
|
|
4621
4943
|
<button class="slm-mode" role="tab" data-mode="inspect" title="Inspect (I)" aria-keyshortcuts="I">Inspect</button>
|
|
4622
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>
|
|
4623
4946
|
</div>
|
|
4624
4947
|
<span class="slm-live"><span class="slm-live-dot"></span><span data-ref="livetext">CONNECTING</span></span>
|
|
4625
4948
|
<div class="slm-bar-actions">
|
|
@@ -4686,6 +5009,7 @@ var SeatManager = class {
|
|
|
4686
5009
|
if (!this.doc) return;
|
|
4687
5010
|
try {
|
|
4688
5011
|
const secs = computeSections(this.doc);
|
|
5012
|
+
this.sectionsBase = secs;
|
|
4689
5013
|
this.sectionOptions = [];
|
|
4690
5014
|
this.sectionByObject = new Map(secs.objectToSection);
|
|
4691
5015
|
this.sectionLabelById.clear();
|
|
@@ -4811,6 +5135,7 @@ var SeatManager = class {
|
|
|
4811
5135
|
paintRail() {
|
|
4812
5136
|
if (this.mode === "view") this.renderViewRail();
|
|
4813
5137
|
else if (this.mode === "inspect") this.renderInspectRail(this.getSelection());
|
|
5138
|
+
else if (this.mode === "sections") this.renderSectionsRail();
|
|
4814
5139
|
else this.renderBlockRail();
|
|
4815
5140
|
this.updateZoomHint();
|
|
4816
5141
|
}
|
|
@@ -4933,6 +5258,259 @@ var SeatManager = class {
|
|
|
4933
5258
|
</div>
|
|
4934
5259
|
</div>`;
|
|
4935
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
|
+
}
|
|
4936
5514
|
paintLegend(t3) {
|
|
4937
5515
|
if (!this.els.legend) return;
|
|
4938
5516
|
this.els.legend.innerHTML = LEGEND.map((l) => `<div class="slm-legrow"><span class="slm-legdot" style="background:${l.color}"></span>
|
|
@@ -5245,6 +5823,7 @@ export {
|
|
|
5245
5823
|
ManageApiError,
|
|
5246
5824
|
SeatManager,
|
|
5247
5825
|
SeatPicker,
|
|
5248
|
-
SeatingChart
|
|
5826
|
+
SeatingChart,
|
|
5827
|
+
attachPickerFrame
|
|
5249
5828
|
};
|
|
5250
5829
|
//# sourceMappingURL=index.js.map
|