@seatlayer/js 0.17.0 → 0.18.1
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/README.md +23 -0
- package/dist/index.cjs +675 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +148 -5
- package/dist/index.d.ts +148 -5
- package/dist/index.js +676 -63
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -358,16 +358,46 @@ var TYPES = /* @__PURE__ */ new Set([
|
|
|
358
358
|
"seatlayer.designer.close",
|
|
359
359
|
"seatlayer.designer.error"
|
|
360
360
|
]);
|
|
361
|
+
var DEFAULT_LOADING_TIMEOUT_MS = 2e4;
|
|
361
362
|
function resolveContainer2(container) {
|
|
362
363
|
if (typeof container !== "string") return container;
|
|
363
364
|
const element = document.querySelector(container);
|
|
364
365
|
if (!element) throw new Error(`EmbeddedDesigner container not found: ${container}`);
|
|
365
366
|
return element;
|
|
366
367
|
}
|
|
368
|
+
function causeFromCode(code) {
|
|
369
|
+
const value = (code ?? "").toLowerCase();
|
|
370
|
+
if (value.includes("expire") || value.includes("revoke") || value === "401") return "expired";
|
|
371
|
+
if (value.includes("mismatch")) return "mismatch";
|
|
372
|
+
if (value.includes("timeout")) return "timeout";
|
|
373
|
+
return "load";
|
|
374
|
+
}
|
|
375
|
+
var ERROR_COPY = {
|
|
376
|
+
expired: {
|
|
377
|
+
title: "This design session expired",
|
|
378
|
+
body: "For your security, editing sessions are short-lived. Start a fresh one to keep designing."
|
|
379
|
+
},
|
|
380
|
+
mismatch: {
|
|
381
|
+
title: "This editor doesn't match this chart",
|
|
382
|
+
body: "The session that loaded belongs to a different chart or workspace. Reopen the designer to continue."
|
|
383
|
+
},
|
|
384
|
+
timeout: {
|
|
385
|
+
title: "The designer is taking too long",
|
|
386
|
+
body: "It did not finish loading in time. This is usually a slow connection \u2014 try again."
|
|
387
|
+
},
|
|
388
|
+
load: {
|
|
389
|
+
title: "We couldn't load the designer",
|
|
390
|
+
body: "Something went wrong while opening the editor. Please try again."
|
|
391
|
+
}
|
|
392
|
+
};
|
|
367
393
|
var EmbeddedDesigner = class {
|
|
368
394
|
constructor(options) {
|
|
369
395
|
this.frame = null;
|
|
370
396
|
this.designerOrigin = "";
|
|
397
|
+
this.overlay = null;
|
|
398
|
+
this.timeoutTimer = null;
|
|
399
|
+
this.phase = "loading";
|
|
400
|
+
this.restoreContainerPosition = null;
|
|
371
401
|
this.handleMessage = (event) => {
|
|
372
402
|
if (!this.frame || event.origin !== this.designerOrigin || event.source !== this.frame.contentWindow) return;
|
|
373
403
|
if (!event.data || typeof event.data !== "object") return;
|
|
@@ -382,10 +412,15 @@ var EmbeddedDesigner = class {
|
|
|
382
412
|
message: typeof data.message === "string" ? data.message : void 0,
|
|
383
413
|
meta: data.meta
|
|
384
414
|
};
|
|
385
|
-
if (this.options.expectedChartId && message.chartId && message.chartId !== this.options.expectedChartId)
|
|
386
|
-
|
|
415
|
+
if (this.options.expectedChartId && message.chartId && message.chartId !== this.options.expectedChartId || this.options.expectedWorkspaceId && message.workspaceId && message.workspaceId !== this.options.expectedWorkspaceId) {
|
|
416
|
+
this.showError("mismatch");
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
387
419
|
switch (message.type) {
|
|
388
420
|
case "seatlayer.designer.ready":
|
|
421
|
+
this.phase = "ready";
|
|
422
|
+
this.clearTimeoutTimer();
|
|
423
|
+
this.removeOverlay();
|
|
389
424
|
this.options.onReady?.(message);
|
|
390
425
|
break;
|
|
391
426
|
case "seatlayer.designer.saved":
|
|
@@ -398,6 +433,7 @@ var EmbeddedDesigner = class {
|
|
|
398
433
|
this.options.onClose?.(message);
|
|
399
434
|
break;
|
|
400
435
|
case "seatlayer.designer.error":
|
|
436
|
+
this.showError(causeFromCode(message.code));
|
|
401
437
|
this.options.onError?.(message);
|
|
402
438
|
break;
|
|
403
439
|
}
|
|
@@ -421,9 +457,21 @@ var EmbeddedDesigner = class {
|
|
|
421
457
|
frame.style.border = "0";
|
|
422
458
|
Object.assign(frame.style, this.options.style);
|
|
423
459
|
if (this.options.className) frame.className = this.options.className;
|
|
460
|
+
const container = resolveContainer2(this.options.container);
|
|
424
461
|
window.addEventListener("message", this.handleMessage);
|
|
425
|
-
|
|
462
|
+
container.append(frame);
|
|
426
463
|
this.frame = frame;
|
|
464
|
+
this.phase = "loading";
|
|
465
|
+
if (this.loadingStateEnabled()) {
|
|
466
|
+
this.ensureContainerPositioned(container);
|
|
467
|
+
this.renderOverlay(container, "loading");
|
|
468
|
+
const timeout = this.options.loadingTimeoutMs ?? DEFAULT_LOADING_TIMEOUT_MS;
|
|
469
|
+
if (timeout > 0 && Number.isFinite(timeout)) {
|
|
470
|
+
this.timeoutTimer = setTimeout(() => {
|
|
471
|
+
if (this.phase === "loading") this.showError("timeout");
|
|
472
|
+
}, timeout);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
427
475
|
return frame;
|
|
428
476
|
}
|
|
429
477
|
/** Replace the iframe instead of assigning a new fragment to an existing one. */
|
|
@@ -436,9 +484,208 @@ var EmbeddedDesigner = class {
|
|
|
436
484
|
}
|
|
437
485
|
destroy() {
|
|
438
486
|
window.removeEventListener("message", this.handleMessage);
|
|
487
|
+
this.clearTimeoutTimer();
|
|
488
|
+
this.removeOverlay();
|
|
489
|
+
this.restoreContainerStyle();
|
|
439
490
|
this.frame?.remove();
|
|
440
491
|
this.frame = null;
|
|
441
492
|
this.designerOrigin = "";
|
|
493
|
+
this.phase = "loading";
|
|
494
|
+
}
|
|
495
|
+
loadingStateEnabled() {
|
|
496
|
+
return this.options.showLoadingState !== false;
|
|
497
|
+
}
|
|
498
|
+
clearTimeoutTimer() {
|
|
499
|
+
if (this.timeoutTimer !== null) {
|
|
500
|
+
clearTimeout(this.timeoutTimer);
|
|
501
|
+
this.timeoutTimer = null;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
ensureContainerPositioned(container) {
|
|
505
|
+
const position = getComputedStyle(container).position;
|
|
506
|
+
if (position === "static") {
|
|
507
|
+
this.restoreContainerPosition = container.style.position;
|
|
508
|
+
container.style.position = "relative";
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
restoreContainerStyle() {
|
|
512
|
+
if (this.restoreContainerPosition === null) return;
|
|
513
|
+
try {
|
|
514
|
+
resolveContainer2(this.options.container).style.position = this.restoreContainerPosition;
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
this.restoreContainerPosition = null;
|
|
518
|
+
}
|
|
519
|
+
removeOverlay() {
|
|
520
|
+
this.overlay?.remove();
|
|
521
|
+
this.overlay = null;
|
|
522
|
+
}
|
|
523
|
+
showError(cause) {
|
|
524
|
+
this.phase = "error";
|
|
525
|
+
this.clearTimeoutTimer();
|
|
526
|
+
if (!this.loadingStateEnabled()) return;
|
|
527
|
+
let container;
|
|
528
|
+
try {
|
|
529
|
+
container = resolveContainer2(this.options.container);
|
|
530
|
+
} catch {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
this.renderOverlay(container, "error", cause);
|
|
534
|
+
}
|
|
535
|
+
handleTryAgain() {
|
|
536
|
+
if (this.options.onRequestRelaunch) {
|
|
537
|
+
this.options.onRequestRelaunch();
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
this.mount();
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Build (or rebuild) the overlay for the given phase. A single overlay element
|
|
544
|
+
* is reused so we never stack stale skeletons or cards.
|
|
545
|
+
*/
|
|
546
|
+
renderOverlay(container, phase, cause) {
|
|
547
|
+
this.removeOverlay();
|
|
548
|
+
const overlay = document.createElement("div");
|
|
549
|
+
overlay.setAttribute("data-seatlayer-designer-overlay", phase);
|
|
550
|
+
overlay.setAttribute("role", phase === "error" ? "alert" : "status");
|
|
551
|
+
overlay.setAttribute("aria-live", "polite");
|
|
552
|
+
Object.assign(overlay.style, {
|
|
553
|
+
position: "absolute",
|
|
554
|
+
inset: "0",
|
|
555
|
+
display: "flex",
|
|
556
|
+
alignItems: "center",
|
|
557
|
+
justifyContent: "center",
|
|
558
|
+
background: "#101625",
|
|
559
|
+
color: "#e6ebf5",
|
|
560
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
|
561
|
+
zIndex: "2",
|
|
562
|
+
overflow: "hidden"
|
|
563
|
+
});
|
|
564
|
+
if (phase === "loading") this.buildSkeleton(overlay);
|
|
565
|
+
else this.buildErrorCard(overlay, cause ?? "load");
|
|
566
|
+
container.append(overlay);
|
|
567
|
+
this.overlay = overlay;
|
|
568
|
+
}
|
|
569
|
+
buildSkeleton(overlay) {
|
|
570
|
+
const style = document.createElement("style");
|
|
571
|
+
style.textContent = `
|
|
572
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
573
|
+
@keyframes seatlayer-designer-shimmer {
|
|
574
|
+
0% { background-position: -320px 0; }
|
|
575
|
+
100% { background-position: 320px 0; }
|
|
576
|
+
}
|
|
577
|
+
[data-seatlayer-designer-overlay="loading"] .sl-shimmer {
|
|
578
|
+
animation: seatlayer-designer-shimmer 1.25s ease-in-out infinite;
|
|
579
|
+
background-size: 640px 100%;
|
|
580
|
+
}
|
|
581
|
+
}`;
|
|
582
|
+
overlay.append(style);
|
|
583
|
+
const shimmer = "linear-gradient(90deg, rgba(255,255,255,0.04) 25%, rgba(255,255,255,0.10) 37%, rgba(255,255,255,0.04) 63%)";
|
|
584
|
+
const scaffold = document.createElement("div");
|
|
585
|
+
Object.assign(scaffold.style, {
|
|
586
|
+
position: "absolute",
|
|
587
|
+
inset: "0",
|
|
588
|
+
display: "flex",
|
|
589
|
+
flexDirection: "column",
|
|
590
|
+
padding: "16px",
|
|
591
|
+
gap: "14px",
|
|
592
|
+
opacity: "0.9"
|
|
593
|
+
});
|
|
594
|
+
const bar = (styles) => {
|
|
595
|
+
const node = document.createElement("div");
|
|
596
|
+
node.className = "sl-shimmer";
|
|
597
|
+
Object.assign(node.style, {
|
|
598
|
+
background: shimmer,
|
|
599
|
+
borderRadius: "8px"
|
|
600
|
+
});
|
|
601
|
+
Object.assign(node.style, styles);
|
|
602
|
+
return node;
|
|
603
|
+
};
|
|
604
|
+
scaffold.append(bar({ height: "40px", width: "100%", flex: "0 0 auto" }));
|
|
605
|
+
const body = document.createElement("div");
|
|
606
|
+
Object.assign(body.style, {
|
|
607
|
+
display: "flex",
|
|
608
|
+
gap: "14px",
|
|
609
|
+
flex: "1 1 auto",
|
|
610
|
+
minHeight: "0"
|
|
611
|
+
});
|
|
612
|
+
body.append(bar({ width: "220px", height: "100%", flex: "0 0 auto" }));
|
|
613
|
+
body.append(bar({ flex: "1 1 auto", height: "100%" }));
|
|
614
|
+
scaffold.append(body);
|
|
615
|
+
overlay.append(scaffold);
|
|
616
|
+
const caption = document.createElement("div");
|
|
617
|
+
Object.assign(caption.style, {
|
|
618
|
+
position: "relative",
|
|
619
|
+
zIndex: "1",
|
|
620
|
+
display: "flex",
|
|
621
|
+
alignItems: "center",
|
|
622
|
+
gap: "10px",
|
|
623
|
+
padding: "10px 16px",
|
|
624
|
+
borderRadius: "999px",
|
|
625
|
+
background: "rgba(16, 22, 37, 0.72)",
|
|
626
|
+
fontSize: "13px",
|
|
627
|
+
fontWeight: "500",
|
|
628
|
+
letterSpacing: "0.01em"
|
|
629
|
+
});
|
|
630
|
+
const dot = document.createElement("span");
|
|
631
|
+
dot.className = "sl-shimmer";
|
|
632
|
+
Object.assign(dot.style, {
|
|
633
|
+
width: "9px",
|
|
634
|
+
height: "9px",
|
|
635
|
+
borderRadius: "50%",
|
|
636
|
+
background: shimmer,
|
|
637
|
+
flex: "0 0 auto"
|
|
638
|
+
});
|
|
639
|
+
caption.append(dot);
|
|
640
|
+
caption.append(document.createTextNode("Loading designer\u2026"));
|
|
641
|
+
overlay.append(caption);
|
|
642
|
+
}
|
|
643
|
+
buildErrorCard(overlay, cause) {
|
|
644
|
+
const copy = ERROR_COPY[cause];
|
|
645
|
+
const card = document.createElement("div");
|
|
646
|
+
Object.assign(card.style, {
|
|
647
|
+
maxWidth: "420px",
|
|
648
|
+
margin: "0 24px",
|
|
649
|
+
padding: "28px",
|
|
650
|
+
textAlign: "center",
|
|
651
|
+
background: "rgba(255, 255, 255, 0.03)",
|
|
652
|
+
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
653
|
+
borderRadius: "16px",
|
|
654
|
+
boxShadow: "0 12px 40px rgba(0, 0, 0, 0.35)"
|
|
655
|
+
});
|
|
656
|
+
const heading = document.createElement("h2");
|
|
657
|
+
heading.textContent = copy.title;
|
|
658
|
+
Object.assign(heading.style, {
|
|
659
|
+
margin: "0 0 8px",
|
|
660
|
+
fontSize: "17px",
|
|
661
|
+
fontWeight: "600",
|
|
662
|
+
color: "#f4f7ff"
|
|
663
|
+
});
|
|
664
|
+
const body = document.createElement("p");
|
|
665
|
+
body.textContent = copy.body;
|
|
666
|
+
Object.assign(body.style, {
|
|
667
|
+
margin: "0 0 20px",
|
|
668
|
+
fontSize: "13.5px",
|
|
669
|
+
lineHeight: "1.5",
|
|
670
|
+
color: "#aab4c8"
|
|
671
|
+
});
|
|
672
|
+
const button = document.createElement("button");
|
|
673
|
+
button.type = "button";
|
|
674
|
+
button.textContent = "Try again";
|
|
675
|
+
Object.assign(button.style, {
|
|
676
|
+
appearance: "none",
|
|
677
|
+
cursor: "pointer",
|
|
678
|
+
border: "0",
|
|
679
|
+
borderRadius: "10px",
|
|
680
|
+
padding: "10px 22px",
|
|
681
|
+
fontSize: "14px",
|
|
682
|
+
fontWeight: "600",
|
|
683
|
+
color: "#101625",
|
|
684
|
+
background: "#7aa2ff"
|
|
685
|
+
});
|
|
686
|
+
button.addEventListener("click", () => this.handleTryAgain());
|
|
687
|
+
card.append(heading, body, button);
|
|
688
|
+
overlay.append(card);
|
|
442
689
|
}
|
|
443
690
|
};
|
|
444
691
|
|
|
@@ -603,26 +850,43 @@ var CSS = `
|
|
|
603
850
|
.sl-tray{flex:1;padding:10px 14px 14px;display:flex;flex-direction:column;gap:7px;min-height:0;overflow-y:auto;
|
|
604
851
|
overscroll-behavior:contain;scrollbar-gutter:stable}
|
|
605
852
|
.sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
|
|
606
|
-
.sl-chip{position:relative;display:grid;grid-template-columns:
|
|
607
|
-
min-height:53px;
|
|
853
|
+
.sl-chip{position:relative;display:grid;grid-template-columns:minmax(0,1fr) 34px;align-items:stretch;
|
|
854
|
+
flex:none;min-height:53px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm);overflow:hidden;
|
|
608
855
|
background:var(--sl-surface);font-size:13px;transform-origin:center;transition:border-color .15s,background .15s}
|
|
609
856
|
.sl-chip:hover{border-color:color-mix(in srgb,var(--sl-accent) 38%,var(--sl-line))}
|
|
610
857
|
.sl-chip.sl-enter{animation:slChipIn .38s cubic-bezier(.2,.8,.2,1) both}
|
|
611
858
|
.sl-chip.sl-leave{pointer-events:none;animation:slChipOut .16s ease-in both}
|
|
612
859
|
.sl-chip.sl-held{border-color:var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
|
|
613
860
|
box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent)}
|
|
614
|
-
.sl-ticket-state{width:
|
|
861
|
+
.sl-ticket-state{width:17px;height:17px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
615
862
|
background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
616
863
|
.sl-ticket-state.held{background:color-mix(in srgb,var(--sl-accent) 18%,var(--sl-surface));color:var(--sl-accent)}
|
|
617
|
-
.sl-ticket-state svg{width:
|
|
618
|
-
.sl-chip-main{min-width:0}
|
|
619
|
-
.sl-chip
|
|
620
|
-
.sl-chip-
|
|
864
|
+
.sl-ticket-state svg{width:10px;height:10px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
865
|
+
.sl-chip-main{min-width:0;padding:8px 10px 8px 11px;display:flex;flex-direction:column;justify-content:center;gap:5px}
|
|
866
|
+
.sl-chip-id{display:flex;gap:12px;min-width:0}
|
|
867
|
+
.sl-chip-id .fld{min-width:0}
|
|
868
|
+
.sl-chip-id .fld.sec{flex:1}
|
|
869
|
+
.sl-chip-id .fld.mid{flex:none;text-align:center}
|
|
870
|
+
.sl-chip-eb{display:block;font-size:8px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);margin-bottom:1px}
|
|
871
|
+
.sl-chip-id .val{display:block;font-weight:600;font-size:13px;line-height:1.25;white-space:nowrap}
|
|
872
|
+
.sl-chip-id .fld.sec .val{overflow:hidden;text-overflow:ellipsis}
|
|
873
|
+
.sl-chip-sub{display:flex;align-items:center;gap:6px;min-width:0}
|
|
621
874
|
.sl-chip .cat{color:var(--sl-muted);font-size:10.5px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
622
875
|
.sl-chip .amt{font-weight:700;font-variant-numeric:tabular-nums;flex:none;white-space:nowrap}
|
|
623
|
-
.sl-chip
|
|
624
|
-
.sl-chip .rm
|
|
876
|
+
.sl-chip-rail{display:flex;flex-direction:column;border-left:1px solid var(--sl-line)}
|
|
877
|
+
.sl-chip .rm,.sl-chip .view{flex:1;min-height:26px;border-radius:0;display:flex;align-items:center;justify-content:center;
|
|
878
|
+
color:var(--sl-muted);transition:color .15s,background .15s}
|
|
879
|
+
.sl-chip .view{border-top:1px solid var(--sl-line)}
|
|
880
|
+
.sl-chip .rm:hover,.sl-chip .rm:focus-visible{color:#e5484d;background:color-mix(in srgb,#e5484d 9%,transparent)}
|
|
881
|
+
.sl-chip .view:hover,.sl-chip .view:focus-visible{color:var(--sl-text);background:color-mix(in srgb,var(--sl-accent) 10%,transparent)}
|
|
625
882
|
.sl-chip .rm svg{width:11px;height:11px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round}
|
|
883
|
+
.sl-chip .view svg{width:13px;height:13px;stroke:currentColor;stroke-width:1.8;fill:none}
|
|
884
|
+
/* live-activity strip \u2014 narrates WS availability deltas (social proof + urgency) */
|
|
885
|
+
.sl-live{display:flex;align-items:center;gap:7px;margin:10px 14px 0;padding:7px 9px;flex:none;
|
|
886
|
+
border:1px solid var(--sl-line);border-radius:8px;background:color-mix(in srgb,var(--sl-accent) 4%,var(--sl-surface));
|
|
887
|
+
font-size:11px;color:var(--sl-muted)}
|
|
888
|
+
.sl-live .dot{width:6px;height:6px;border-radius:999px;background:#22a06b;box-shadow:0 0 6px rgba(34,160,107,.75);flex:none}
|
|
889
|
+
.sl-live span:last-child{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
626
890
|
|
|
627
891
|
/* GA rows */
|
|
628
892
|
.sl-ga{display:flex;align-items:center;gap:10px;padding:9px 11px;border:1px dashed var(--sl-line);border-radius:var(--sl-r-sm)}
|
|
@@ -698,6 +962,8 @@ var CSS = `
|
|
|
698
962
|
|
|
699
963
|
/* zoom column (flows within the bottom-right region) */
|
|
700
964
|
.sl-zoom{display:flex;flex-direction:column;gap:6px}
|
|
965
|
+
/* CSS-fallback full screen (iOS Safari has no element fullscreen API) */
|
|
966
|
+
.sl-picker.sl-fs{position:fixed;inset:0;z-index:2147483000;width:auto;height:auto;max-height:none;border-radius:0}
|
|
701
967
|
.sl-zoom button{width:36px;height:36px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
702
968
|
color:var(--sl-text);font-size:17px;font-weight:700;display:flex;align-items:center;justify-content:center;transition:border-color .15s}
|
|
703
969
|
.sl-zoom button:hover{border-color:var(--sl-muted)}
|
|
@@ -756,6 +1022,39 @@ var CSS = `
|
|
|
756
1022
|
.sl-booked.on .sl-booked-title{animation-delay:.22s}
|
|
757
1023
|
.sl-booked.on .sl-booked-sub{animation-delay:.3s}
|
|
758
1024
|
|
|
1025
|
+
/* sold-out overlay \u2014 every SEATED category's live availability is 0. Centered
|
|
1026
|
+
over the map; a stub (disabled) "Join waitlist" button, exactly like the page.
|
|
1027
|
+
Suppressed when GA areas exist (GA capacity isn't seat-counted). Clears live
|
|
1028
|
+
the moment WS frees a seat up. */
|
|
1029
|
+
.sl-soldout{position:absolute;inset:0;z-index:10;display:none;flex-direction:column;align-items:center;
|
|
1030
|
+
justify-content:center;text-align:center;gap:8px;padding:24px;
|
|
1031
|
+
background:color-mix(in srgb,var(--sl-bg) 82%,transparent);backdrop-filter:blur(4px)}
|
|
1032
|
+
.sl-soldout.on{display:flex}
|
|
1033
|
+
.sl-soldout-eyebrow{font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--sl-accent);font-weight:800}
|
|
1034
|
+
.sl-soldout-title{font-size:32px;font-weight:800;color:var(--sl-text);line-height:1.05}
|
|
1035
|
+
.sl-soldout-copy{max-width:360px;font-size:13px;color:var(--sl-muted);line-height:1.5}
|
|
1036
|
+
.sl-picker .sl-soldout-btn{margin-top:10px;min-height:40px;padding:10px 18px;border-radius:var(--sl-r-sm);
|
|
1037
|
+
background:var(--sl-surface);color:var(--sl-muted);border:1px solid var(--sl-line);font-weight:800;font-size:13px;
|
|
1038
|
+
cursor:not-allowed;opacity:.85}
|
|
1039
|
+
|
|
1040
|
+
/* sales-closed pill (header) \u2014 persistent read-only state when the event's sales
|
|
1041
|
+
window is closed at load or closes live mid-session. Neutral (not accent) so it
|
|
1042
|
+
reads as "unavailable", distinct from the accent hold pill next to it. */
|
|
1043
|
+
.sl-closed-pill{display:none;align-items:center;gap:6px;padding:6px 12px;border-radius:999px;flex:none;
|
|
1044
|
+
background:color-mix(in srgb,var(--sl-text) 12%,var(--sl-surface));color:var(--sl-text);
|
|
1045
|
+
font-weight:700;font-size:12px;white-space:nowrap}
|
|
1046
|
+
.sl-closed-pill.on{display:inline-flex}
|
|
1047
|
+
.sl-closed-pill svg{width:13px;height:13px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
1048
|
+
|
|
1049
|
+
/* "Powered by SeatLayer" attribution badge (side-panel foot) \u2014 the small gold
|
|
1050
|
+
rounded logo mark + wordmark. Hidden when the host opts out or the org's paid
|
|
1051
|
+
theme sets hideBadge. */
|
|
1052
|
+
.sl-powered{display:flex;align-items:center;justify-content:center;gap:6px;margin-top:10px;
|
|
1053
|
+
font-size:11px;letter-spacing:.03em;color:var(--sl-muted)}
|
|
1054
|
+
.sl-powered-mark{width:16px;height:16px;border-radius:4px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
1055
|
+
background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
1056
|
+
.sl-powered-mark svg{width:11px;height:11px;fill:currentColor}
|
|
1057
|
+
|
|
759
1058
|
/* a11y filter chips (flow within the top-left region) */
|
|
760
1059
|
.sl-chips{display:flex;gap:6px;flex-wrap:wrap}
|
|
761
1060
|
.sl-chip-f{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:999px;font-size:12px;font-weight:700;
|
|
@@ -794,8 +1093,30 @@ var CSS = `
|
|
|
794
1093
|
.sl-picker[data-layout="narrow"] .sl-confirm{left:50%!important;top:auto!important;bottom:14px;width:min(342px,calc(100% - 24px));
|
|
795
1094
|
transform:translateX(-50%);animation:slConfirmMobileIn .24s cubic-bezier(.2,.8,.2,1) both}
|
|
796
1095
|
|
|
1096
|
+
/* hover preview \u2014 a COMPACT echo of the confirm card (deliberately smaller: it's
|
|
1097
|
+
a passing preview on hover, not the click/select action surface). Reuses the
|
|
1098
|
+
Section\xB7Row\xB7Seat identity grid so hover, confirm and the cart chip all share
|
|
1099
|
+
one visual language, just at three sizes. */
|
|
1100
|
+
.sl-tip{position:absolute;z-index:7;pointer-events:none;display:none;width:190px;overflow:hidden;
|
|
1101
|
+
background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:11px;
|
|
1102
|
+
box-shadow:0 12px 30px -14px rgba(0,0,0,.6)}
|
|
1103
|
+
.sl-tip-grid{display:grid;grid-template-columns:1.3fr .85fr .85fr;border-bottom:1px solid var(--sl-line)}
|
|
1104
|
+
.sl-tip-grid.one{grid-template-columns:1fr}
|
|
1105
|
+
.sl-tip-field{min-width:0;padding:6px 9px;border-right:1px solid var(--sl-line)}
|
|
1106
|
+
.sl-tip-field:last-child{border-right:0;text-align:center}
|
|
1107
|
+
.sl-tip-grid:not(.one) .sl-tip-field:nth-child(2){text-align:center}
|
|
1108
|
+
.sl-tip-key{display:block;font-size:7.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
|
|
1109
|
+
.sl-tip-val{display:block;margin-top:2px;color:var(--sl-text);font-size:13px;line-height:1.1;font-weight:750;
|
|
1110
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1111
|
+
.sl-tip-cat{display:flex;align-items:center;gap:7px;padding:6px 10px;font-size:11px;
|
|
1112
|
+
background:color-mix(in srgb,var(--sl-cat) 12%,var(--sl-surface))}
|
|
1113
|
+
.sl-tip-dot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
1114
|
+
.sl-tip-name{color:var(--sl-muted);flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1115
|
+
.sl-tip-amt{margin-left:auto;font-weight:800;color:var(--sl-text);font-variant-numeric:tabular-nums;font-size:12px}
|
|
1116
|
+
.sl-tip-status{padding:5px 10px 7px;font-size:8.5px;letter-spacing:.09em;text-transform:uppercase;font-weight:700;color:var(--sl-muted)}
|
|
1117
|
+
|
|
797
1118
|
/* Best available is a first-class shortcut, not an anonymous utility row. */
|
|
798
|
-
.sl-ba{position:relative;overflow:hidden;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:7px;
|
|
1119
|
+
.sl-ba{position:relative;flex:none;overflow:hidden;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:7px;
|
|
799
1120
|
padding:13px;border:1px solid color-mix(in srgb,var(--sl-accent) 34%,var(--sl-line));border-radius:13px;
|
|
800
1121
|
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)))}
|
|
801
1122
|
.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}
|
|
@@ -831,12 +1152,6 @@ var CSS = `
|
|
|
831
1152
|
/* per-seat ticket-tier select + view-from-seat button in tray chips */
|
|
832
1153
|
.sl-chip .tier{background:var(--sl-bg);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:6px;
|
|
833
1154
|
font:inherit;font-size:10px;padding:2px 4px;min-width:0;max-width:100%;cursor:pointer}
|
|
834
|
-
.sl-chip .view{width:20px;height:20px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
835
|
-
color:var(--sl-muted);opacity:.36;transition:color .15s,opacity .15s}
|
|
836
|
-
.sl-chip .view:hover{color:var(--sl-text)}
|
|
837
|
-
.sl-chip:hover .view,.sl-chip .view:focus-visible{opacity:1;color:var(--sl-text)}
|
|
838
|
-
.sl-chip .view svg{width:12px;height:12px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
839
|
-
@media(pointer:coarse){.sl-chip .view{opacity:.58}}
|
|
840
1155
|
|
|
841
1156
|
/* arena: LOD rung pills (flow within the top-center region) */
|
|
842
1157
|
.sl-rungs{display:none;background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:999px;padding:3px}
|
|
@@ -894,6 +1209,14 @@ var CSS = `
|
|
|
894
1209
|
.sl-seccard-hint{font-size:10.5px;color:var(--sl-muted)}
|
|
895
1210
|
|
|
896
1211
|
/* view-from-seat button on the confirm popover */
|
|
1212
|
+
/* Eager sightline preview inside the confirm card */
|
|
1213
|
+
.sl-confirm-thumbwrap{position:relative;display:block;width:100%;height:74px;margin:0 0 8px;padding:0!important;
|
|
1214
|
+
border-radius:9px;overflow:hidden;border:1px solid var(--sl-line);cursor:pointer}
|
|
1215
|
+
.sl-confirm-thumb{display:block;width:100%;height:100%;object-fit:cover}
|
|
1216
|
+
.sl-confirm-thumb-badge{position:absolute;right:7px;top:7px;display:inline-flex;align-items:center;gap:5px;
|
|
1217
|
+
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)}
|
|
1218
|
+
.sl-confirm-sight{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--sl-muted);margin-bottom:2px}
|
|
1219
|
+
.sl-confirm-sight span{color:#22a06b;font-weight:800}
|
|
897
1220
|
.sl-confirm-view{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
|
|
898
1221
|
color:var(--sl-text);font-weight:700;font-size:12px;display:flex;align-items:center;justify-content:center;gap:7px}
|
|
899
1222
|
.sl-confirm-view:hover{border-color:var(--sl-muted)}
|
|
@@ -977,6 +1300,22 @@ function resolveTokens(chart, host) {
|
|
|
977
1300
|
"--sl-radius": `${host?.radius ?? 14}px`
|
|
978
1301
|
};
|
|
979
1302
|
}
|
|
1303
|
+
var CB_STORAGE_KEY = "seatmap.a11y.cb";
|
|
1304
|
+
function readStoredColorblind() {
|
|
1305
|
+
try {
|
|
1306
|
+
if (typeof window === "undefined") return null;
|
|
1307
|
+
const raw = window.localStorage.getItem(CB_STORAGE_KEY);
|
|
1308
|
+
return raw == null ? null : raw === "1";
|
|
1309
|
+
} catch {
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
function writeStoredColorblind(on) {
|
|
1314
|
+
try {
|
|
1315
|
+
window.localStorage.setItem(CB_STORAGE_KEY, on ? "1" : "0");
|
|
1316
|
+
} catch {
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
980
1319
|
var SeatPicker = class _SeatPicker {
|
|
981
1320
|
constructor(options) {
|
|
982
1321
|
this.root = null;
|
|
@@ -1009,11 +1348,17 @@ var SeatPicker = class _SeatPicker {
|
|
|
1009
1348
|
this.confirmEl = null;
|
|
1010
1349
|
this.confirmSeat = null;
|
|
1011
1350
|
this.srEl = null;
|
|
1012
|
-
this.a11yFilter = "all";
|
|
1013
1351
|
this.baQty = 2;
|
|
1014
1352
|
this.baCat = "";
|
|
1015
1353
|
this.bestAvailableConfirm = false;
|
|
1016
1354
|
this.releasingHold = false;
|
|
1355
|
+
/** Event sales window is closed (read-only load state / live close). */
|
|
1356
|
+
this.salesClosed = false;
|
|
1357
|
+
/** Every seated category's live availability is 0 (sold-out overlay is up). */
|
|
1358
|
+
this.soldOut = false;
|
|
1359
|
+
this.soldoutEl = null;
|
|
1360
|
+
/** Resolved colorblind-safe state — stored preference wins over the option. */
|
|
1361
|
+
this.cbSafe = false;
|
|
1017
1362
|
// arena / multi-floor / seat-view chrome
|
|
1018
1363
|
this.rungsEl = null;
|
|
1019
1364
|
this.floorsEl = null;
|
|
@@ -1022,13 +1367,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1022
1367
|
this.viewCleanup = null;
|
|
1023
1368
|
this.allSeatsCache = null;
|
|
1024
1369
|
// F3 minimap
|
|
1025
|
-
this.miniEl = null;
|
|
1026
1370
|
this.miniCanvas = null;
|
|
1027
1371
|
this.miniBase = null;
|
|
1028
1372
|
this.miniTf = null;
|
|
1029
1373
|
// F4 price-band filter — active band's category keys (null = all prices)
|
|
1030
1374
|
this.priceBandKeys = null;
|
|
1031
|
-
this.priceFilterEl = null;
|
|
1032
1375
|
/** Last surfaced section summary (re-rendered when the price band changes). */
|
|
1033
1376
|
this.lastSection = null;
|
|
1034
1377
|
/** Section card collapsed to its slim pill (seat-picking has begun). */
|
|
@@ -1048,6 +1391,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
1048
1391
|
this.ctaPhase = "idle";
|
|
1049
1392
|
// narrow-layout chrome that docks into the sheet's Filters row on mobile
|
|
1050
1393
|
this.a11yChipsEl = null;
|
|
1394
|
+
this.fsFallback = false;
|
|
1395
|
+
this.fsChangeHandler = null;
|
|
1396
|
+
this.fsEscHandler = null;
|
|
1051
1397
|
this.cbEl = null;
|
|
1052
1398
|
// modal plumbing (set by open())
|
|
1053
1399
|
this.modalScrim = null;
|
|
@@ -1055,20 +1401,22 @@ var SeatPicker = class _SeatPicker {
|
|
|
1055
1401
|
this.escHandler = null;
|
|
1056
1402
|
/** Set by open(): closes the modal (scroll restore + destroy + onClose). */
|
|
1057
1403
|
this.closeModal = null;
|
|
1404
|
+
this.lastCatAvail = null;
|
|
1058
1405
|
if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
|
|
1059
1406
|
if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
|
|
1060
1407
|
if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
|
|
1061
1408
|
this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
|
|
1062
1409
|
this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
|
|
1063
|
-
this.api = new PubApi(this.apiBase);
|
|
1410
|
+
this.api = options.transport ?? new PubApi(this.apiBase);
|
|
1064
1411
|
this.maxTickets = Math.max(1, Math.floor(options.maxSelection ?? DEFAULT_MAX_SELECTION2));
|
|
1412
|
+
this.cbSafe = readStoredColorblind() ?? !!options.colorblindSafe;
|
|
1065
1413
|
this.controller = new import_core2.PickerController({
|
|
1066
1414
|
transport: this.api,
|
|
1067
1415
|
eventKey: options.event,
|
|
1068
1416
|
maxSelection: this.maxTickets,
|
|
1069
1417
|
currency: options.currency,
|
|
1070
1418
|
flashOnLiveChange: true,
|
|
1071
|
-
colorblindSafe:
|
|
1419
|
+
colorblindSafe: this.cbSafe,
|
|
1072
1420
|
onSelectionChange: () => {
|
|
1073
1421
|
this.syncTray();
|
|
1074
1422
|
if (this.committedSelection().length) this.collapseSectionCard();
|
|
@@ -1094,6 +1442,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1094
1442
|
},
|
|
1095
1443
|
confirmSelection: this.opts.confirmSelection,
|
|
1096
1444
|
onSelect: (seat) => {
|
|
1445
|
+
if (this.salesClosed) {
|
|
1446
|
+
this.controller.deselect([seat.id]);
|
|
1447
|
+
this.toast(this.tf("picker.salesClosedToast", "Sales are closed for this event."), "warning");
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1097
1450
|
this.flashPickedSeat(seat.id);
|
|
1098
1451
|
if (this.opts.confirmSelection) this.showConfirm(seat);
|
|
1099
1452
|
},
|
|
@@ -1116,9 +1469,68 @@ var SeatPicker = class _SeatPicker {
|
|
|
1116
1469
|
onHint: (m) => {
|
|
1117
1470
|
if (m) this.toast(m);
|
|
1118
1471
|
},
|
|
1472
|
+
// Server declared the event closed mid-session (409 event_closed) — keep
|
|
1473
|
+
// the toast (raised by handleCta), and add the persistent read-only state.
|
|
1474
|
+
onSalesClosed: () => this.setSalesClosed(true),
|
|
1119
1475
|
onError: (err) => this.opts.onError?.(err)
|
|
1120
1476
|
});
|
|
1121
1477
|
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Eager sightline preview for the confirm card: a cheap generated forward
|
|
1480
|
+
* view (or the organizer's real photo) plus a "Nm to stage · clear
|
|
1481
|
+
* sightline" line — the premium at-a-glance moment; click opens the 360.
|
|
1482
|
+
*/
|
|
1483
|
+
confirmThumbHtml(seat) {
|
|
1484
|
+
const doc = this.controller.doc;
|
|
1485
|
+
if (!doc) return "";
|
|
1486
|
+
let url = seat.viewUrl ?? "";
|
|
1487
|
+
let distance = null;
|
|
1488
|
+
if (!url) {
|
|
1489
|
+
try {
|
|
1490
|
+
const thumb = (0, import_core2.generateSeatThumb)(seat, doc.focalPoint);
|
|
1491
|
+
url = thumb.url;
|
|
1492
|
+
distance = thumb.distanceM ?? null;
|
|
1493
|
+
} catch {
|
|
1494
|
+
return "";
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
const sight = distance != null ? `${distance}${this.tf("picker.sightline", "m to stage \xB7 clear sightline")}` : this.tf("picker.sightlineClear", "Clear sightline");
|
|
1498
|
+
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
|
+
}
|
|
1500
|
+
/** Full screen via the native API, falling back to a fixed-position overlay (iOS Safari). */
|
|
1501
|
+
toggleFullscreen() {
|
|
1502
|
+
const root = this.root;
|
|
1503
|
+
if (!root) return;
|
|
1504
|
+
const active = !!document.fullscreenElement || this.fsFallback;
|
|
1505
|
+
if (!active) {
|
|
1506
|
+
if (root.requestFullscreen) {
|
|
1507
|
+
root.requestFullscreen().catch(() => this.setFsFallback(true));
|
|
1508
|
+
} else {
|
|
1509
|
+
this.setFsFallback(true);
|
|
1510
|
+
}
|
|
1511
|
+
} else if (document.fullscreenElement) {
|
|
1512
|
+
void document.exitFullscreen().catch(() => {
|
|
1513
|
+
});
|
|
1514
|
+
} else {
|
|
1515
|
+
this.setFsFallback(false);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
setFsFallback(on) {
|
|
1519
|
+
if (this.fsFallback === on) return;
|
|
1520
|
+
this.fsFallback = on;
|
|
1521
|
+
this.root?.classList.toggle("sl-fs", on);
|
|
1522
|
+
this.els.zfs?.setAttribute("aria-pressed", String(on || !!document.fullscreenElement));
|
|
1523
|
+
if (on && !this.fsEscHandler) {
|
|
1524
|
+
this.fsEscHandler = (e) => {
|
|
1525
|
+
if (e.key === "Escape" && !document.fullscreenElement) this.setFsFallback(false);
|
|
1526
|
+
};
|
|
1527
|
+
window.addEventListener("keydown", this.fsEscHandler);
|
|
1528
|
+
} else if (!on && this.fsEscHandler) {
|
|
1529
|
+
window.removeEventListener("keydown", this.fsEscHandler);
|
|
1530
|
+
this.fsEscHandler = null;
|
|
1531
|
+
}
|
|
1532
|
+
requestAnimationFrame(() => this.controller.zoomToFit());
|
|
1533
|
+
}
|
|
1122
1534
|
/**
|
|
1123
1535
|
* Close the picker. In modal mode (SeatPicker.open()) this dismisses the
|
|
1124
1536
|
* modal exactly like ESC/scrim/✕ — restores page scroll and fires onClose.
|
|
@@ -1213,6 +1625,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
1213
1625
|
<div class="sl-head-meta" data-ref="meta"></div>
|
|
1214
1626
|
</div>
|
|
1215
1627
|
<span class="sl-hold-pill" data-ref="hold"></span>
|
|
1628
|
+
<span class="sl-closed-pill" data-ref="closedPill" role="status">
|
|
1629
|
+
<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>
|
|
1630
|
+
<span data-ref="closedPillText"></span>
|
|
1631
|
+
</span>
|
|
1216
1632
|
<button type="button" class="sl-close" data-ref="close" aria-label="Close">
|
|
1217
1633
|
<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>
|
|
1218
1634
|
</button>
|
|
@@ -1226,6 +1642,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
1226
1642
|
<button type="button" aria-label="Fit to screen" data-ref="zfit">
|
|
1227
1643
|
<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>
|
|
1228
1644
|
</button>
|
|
1645
|
+
<button type="button" aria-label="Full screen" aria-pressed="false" data-ref="zfs">
|
|
1646
|
+
<svg viewBox="0 0 24 24"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
|
|
1647
|
+
</button>
|
|
1229
1648
|
</div>
|
|
1230
1649
|
<div class="sl-boot" data-ref="boot"><span class="sl-boot-spin"></span>Loading seat map\u2026</div>
|
|
1231
1650
|
<div class="sl-toast" data-ref="toast" role="status" aria-live="polite"></div>
|
|
@@ -1244,6 +1663,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1244
1663
|
<div class="sl-filters" data-ref="filters"></div>
|
|
1245
1664
|
<div class="sl-sec sl-prices-sec" data-ref="pricesSec"><span>Ticket prices</span></div>
|
|
1246
1665
|
<div class="sl-prices" data-ref="prices"></div>
|
|
1666
|
+
<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>
|
|
1247
1667
|
<div class="sl-sec sl-seats-sec"><span>Your seats</span><span class="sl-seat-summary" data-ref="seatSummary"></span></div>
|
|
1248
1668
|
<div class="sl-tray" data-ref="tray"></div>
|
|
1249
1669
|
<div class="sl-foot" data-ref="foot">
|
|
@@ -1277,6 +1697,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
1277
1697
|
this.els.zin.addEventListener("click", () => this.controller.zoomIn());
|
|
1278
1698
|
this.els.zout.addEventListener("click", () => this.controller.zoomOut());
|
|
1279
1699
|
this.els.zfit.addEventListener("click", () => this.controller.zoomToFit());
|
|
1700
|
+
this.els.zfs.addEventListener("click", () => this.toggleFullscreen());
|
|
1701
|
+
this.fsChangeHandler = () => {
|
|
1702
|
+
if (!document.fullscreenElement) this.setFsFallback(false);
|
|
1703
|
+
this.els.zfs?.setAttribute("aria-pressed", String(!!document.fullscreenElement || this.fsFallback));
|
|
1704
|
+
requestAnimationFrame(() => this.controller.zoomToFit());
|
|
1705
|
+
};
|
|
1706
|
+
document.addEventListener("fullscreenchange", this.fsChangeHandler);
|
|
1280
1707
|
const head = this.els.sheetHead;
|
|
1281
1708
|
if (head) {
|
|
1282
1709
|
const toggle = this.els.sheetToggle;
|
|
@@ -1320,7 +1747,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1320
1747
|
}
|
|
1321
1748
|
this.tipEl = document.createElement("div");
|
|
1322
1749
|
this.tipEl.setAttribute("role", "tooltip");
|
|
1323
|
-
this.tipEl.
|
|
1750
|
+
this.tipEl.className = "sl-tip";
|
|
1324
1751
|
this.els.map.appendChild(this.tipEl);
|
|
1325
1752
|
this.els.map.addEventListener("mousemove", (e) => {
|
|
1326
1753
|
const r = this.els.map.getBoundingClientRect();
|
|
@@ -1345,6 +1772,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1345
1772
|
return this;
|
|
1346
1773
|
}
|
|
1347
1774
|
this.els.boot.remove();
|
|
1775
|
+
this.salesClosed = !!info.salesClosed;
|
|
1348
1776
|
this.buildRegions();
|
|
1349
1777
|
this.regions["bottom-right"].appendChild(this.els.zoom);
|
|
1350
1778
|
this.regions["bottom-center"].appendChild(this.els.toast);
|
|
@@ -1364,6 +1792,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1364
1792
|
this.els.name.textContent = info.eventName ?? "";
|
|
1365
1793
|
const when = info.startsAt ? new Date(info.startsAt).toLocaleString(this.opts.locale, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "";
|
|
1366
1794
|
this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
|
|
1795
|
+
this.buildBadge(chartTheme);
|
|
1367
1796
|
const present = /* @__PURE__ */ new Set();
|
|
1368
1797
|
if (this.controller.doc) {
|
|
1369
1798
|
for (const seat of (0, import_core2.expandChart)(this.controller.doc)) {
|
|
@@ -1379,12 +1808,29 @@ var SeatPicker = class _SeatPicker {
|
|
|
1379
1808
|
chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
|
|
1380
1809
|
this.regions["top-left"].appendChild(chips);
|
|
1381
1810
|
this.a11yChipsEl = chips;
|
|
1811
|
+
const active = /* @__PURE__ */ new Set();
|
|
1812
|
+
const syncChips = () => {
|
|
1813
|
+
chips.querySelectorAll("button").forEach((b) => {
|
|
1814
|
+
const f = b.dataset.f;
|
|
1815
|
+
const on = f === "all" ? active.size === 0 : active.has(f);
|
|
1816
|
+
b.classList.toggle("on", on);
|
|
1817
|
+
b.setAttribute("aria-pressed", String(on));
|
|
1818
|
+
});
|
|
1819
|
+
const filter = active.size ? [...active] : null;
|
|
1820
|
+
this.controller.setAccessibilityFilter(filter);
|
|
1821
|
+
if (filter && this.rungsEl && this.controller.getRung() !== "seats") {
|
|
1822
|
+
this.controller.setRung("seats");
|
|
1823
|
+
this.collapseSectionCard();
|
|
1824
|
+
this.syncRung();
|
|
1825
|
+
}
|
|
1826
|
+
};
|
|
1382
1827
|
chips.querySelectorAll("button").forEach((btn) => {
|
|
1383
1828
|
btn.addEventListener("click", () => {
|
|
1384
1829
|
const f = btn.dataset.f;
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1830
|
+
if (f === "all") active.clear();
|
|
1831
|
+
else if (active.has(f)) active.delete(f);
|
|
1832
|
+
else active.add(f);
|
|
1833
|
+
syncChips();
|
|
1388
1834
|
});
|
|
1389
1835
|
});
|
|
1390
1836
|
}
|
|
@@ -1393,14 +1839,14 @@ var SeatPicker = class _SeatPicker {
|
|
|
1393
1839
|
cb.className = "sl-cbbtn";
|
|
1394
1840
|
this.cbEl = cb;
|
|
1395
1841
|
cb.setAttribute("aria-label", "Toggle colorblind-friendly colors");
|
|
1396
|
-
cb.setAttribute("aria-pressed", String(
|
|
1842
|
+
cb.setAttribute("aria-pressed", String(this.cbSafe));
|
|
1397
1843
|
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>';
|
|
1398
1844
|
this.els.zfit.parentElement.appendChild(cb);
|
|
1399
|
-
let cbOn = !!this.opts.colorblindSafe;
|
|
1400
1845
|
cb.addEventListener("click", () => {
|
|
1401
|
-
|
|
1402
|
-
cb.setAttribute("aria-pressed", String(
|
|
1403
|
-
this.controller.setColorblindSafe(
|
|
1846
|
+
this.cbSafe = !this.cbSafe;
|
|
1847
|
+
cb.setAttribute("aria-pressed", String(this.cbSafe));
|
|
1848
|
+
this.controller.setColorblindSafe(this.cbSafe);
|
|
1849
|
+
writeStoredColorblind(this.cbSafe);
|
|
1404
1850
|
});
|
|
1405
1851
|
this.srEl = document.createElement("div");
|
|
1406
1852
|
this.srEl.className = "sl-sr";
|
|
@@ -1411,9 +1857,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1411
1857
|
this.buildPriceFilter();
|
|
1412
1858
|
this.buildExtendPrompt();
|
|
1413
1859
|
this.buildBookedOverlay();
|
|
1860
|
+
this.buildSoldoutOverlay();
|
|
1414
1861
|
this.dockLayoutChrome();
|
|
1415
1862
|
await this.restoreRememberedHold();
|
|
1416
1863
|
if (this.destroyed) return this;
|
|
1864
|
+
if (this.salesClosed) this.applySalesClosed();
|
|
1417
1865
|
this.syncPrices();
|
|
1418
1866
|
this.syncTray();
|
|
1419
1867
|
return this;
|
|
@@ -1465,6 +1913,85 @@ var SeatPicker = class _SeatPicker {
|
|
|
1465
1913
|
this.bookedEl = el;
|
|
1466
1914
|
this.els.bookedSub = el.querySelector('[data-ref="bookedSub"]');
|
|
1467
1915
|
}
|
|
1916
|
+
/**
|
|
1917
|
+
* Localized string with a literal fallback. `t()` returns the key itself for
|
|
1918
|
+
* unknown keys, so this collapses that to `fallback` — while still honoring a
|
|
1919
|
+
* host `messages` override (which makes `t()` return the override, not the key).
|
|
1920
|
+
*/
|
|
1921
|
+
tf(key, fallback) {
|
|
1922
|
+
const v = (0, import_core2.t)(key);
|
|
1923
|
+
return v === key ? fallback : v;
|
|
1924
|
+
}
|
|
1925
|
+
/** Sold-out overlay — centered over the map, disabled waitlist stub (Gap 2). */
|
|
1926
|
+
buildSoldoutOverlay() {
|
|
1927
|
+
if (!this.els.map) return;
|
|
1928
|
+
const el = document.createElement("div");
|
|
1929
|
+
el.className = "sl-soldout";
|
|
1930
|
+
el.setAttribute("role", "status");
|
|
1931
|
+
const name = (this.controller.doc?.theme?.brandName ?? this.opts.theme?.brandName ?? this.els.name?.textContent ?? this.tf("picker.soldOutEyebrow", "This event")).toUpperCase();
|
|
1932
|
+
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>`;
|
|
1933
|
+
this.els.map.appendChild(el);
|
|
1934
|
+
this.soldoutEl = el;
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Recompute the sold-out state on every price/availability sync. Sold-out ⇔
|
|
1938
|
+
* every SEATED category's live free count is 0. Suppressed when the chart has
|
|
1939
|
+
* GA areas (GA capacity isn't per-seat, so seated counts would read 0 and
|
|
1940
|
+
* falsely block standing room) — mirrors the public page. Clears live when WS
|
|
1941
|
+
* frees a seat up.
|
|
1942
|
+
*/
|
|
1943
|
+
syncSoldout(categories, left) {
|
|
1944
|
+
const hasGA = this.controller.getGAAreas().length > 0;
|
|
1945
|
+
const soldOut = this.isSoldOut(categories, left, hasGA);
|
|
1946
|
+
if (soldOut === this.soldOut) return;
|
|
1947
|
+
this.soldOut = soldOut;
|
|
1948
|
+
this.soldoutEl?.classList.toggle("on", soldOut);
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Pure sold-out predicate: every SEATED category's free count is 0, there is at
|
|
1952
|
+
* least one seated category, and there are no GA areas (GA capacity isn't
|
|
1953
|
+
* per-seat, so seated counts read 0 and would falsely block standing room).
|
|
1954
|
+
* `left` is seeded implicitly — a missing key means a fully-booked tier (0 free).
|
|
1955
|
+
*/
|
|
1956
|
+
isSoldOut(categories, left, hasGA) {
|
|
1957
|
+
return !hasGA && categories.length > 0 && categories.every((c) => (left[c.key] ?? 0) === 0);
|
|
1958
|
+
}
|
|
1959
|
+
/**
|
|
1960
|
+
* Sales-closed read-only state (Gap 3): persistent header pill, disabled CTA
|
|
1961
|
+
* with a closed label, and frozen best-available / GA controls. `setSalesClosed`
|
|
1962
|
+
* is the reactive entry (live 409 event_closed); `applySalesClosed` is the
|
|
1963
|
+
* idempotent DOM apply used at load and on transition.
|
|
1964
|
+
*/
|
|
1965
|
+
setSalesClosed(closed) {
|
|
1966
|
+
if (this.salesClosed === closed) return;
|
|
1967
|
+
this.salesClosed = closed;
|
|
1968
|
+
this.applySalesClosed();
|
|
1969
|
+
}
|
|
1970
|
+
applySalesClosed() {
|
|
1971
|
+
const pill = this.els.closedPill;
|
|
1972
|
+
if (pill) {
|
|
1973
|
+
pill.classList.toggle("on", this.salesClosed);
|
|
1974
|
+
const text = this.els.closedPillText ?? pill;
|
|
1975
|
+
text.textContent = this.tf("picker.salesClosedPill", "Sales are closed");
|
|
1976
|
+
}
|
|
1977
|
+
this.root?.setAttribute("data-sales-closed", String(this.salesClosed));
|
|
1978
|
+
this.syncCta();
|
|
1979
|
+
this.syncTray();
|
|
1980
|
+
}
|
|
1981
|
+
/** The badge is hidden when the host opts out OR the org's theme sets hideBadge. */
|
|
1982
|
+
badgeHidden(chartTheme) {
|
|
1983
|
+
return !!(this.opts.hideBadge || chartTheme?.hideBadge);
|
|
1984
|
+
}
|
|
1985
|
+
/** Attribution badge in the side-panel foot (Gap 7). Hidden per host/theme. */
|
|
1986
|
+
buildBadge(chartTheme) {
|
|
1987
|
+
if (this.badgeHidden(chartTheme)) return;
|
|
1988
|
+
const foot = this.els.foot;
|
|
1989
|
+
if (!foot) return;
|
|
1990
|
+
const el = document.createElement("div");
|
|
1991
|
+
el.className = "sl-powered";
|
|
1992
|
+
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>`;
|
|
1993
|
+
foot.appendChild(el);
|
|
1994
|
+
}
|
|
1468
1995
|
// ---- Feature 6: chrome anchor regions -------------------------------------
|
|
1469
1996
|
/**
|
|
1470
1997
|
* Create the positioned flex containers that own every persistent map overlay.
|
|
@@ -1577,13 +2104,18 @@ var SeatPicker = class _SeatPicker {
|
|
|
1577
2104
|
heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
|
|
1578
2105
|
}
|
|
1579
2106
|
return gaAreas.reduce(
|
|
1580
|
-
(sum, area) => sum + area.price * Math.max(0, (this.gaQty.get(area.id) ?? 0) - (heldGA.get(area.id) ?? 0)),
|
|
2107
|
+
(sum, area) => sum + this.paidPrice(area.categoryKey, null, area.price) * Math.max(0, (this.gaQty.get(area.id) ?? 0) - (heldGA.get(area.id) ?? 0)),
|
|
1581
2108
|
0
|
|
1582
2109
|
);
|
|
1583
2110
|
}
|
|
1584
2111
|
syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
|
|
1585
2112
|
const cta = this.els.cta;
|
|
1586
2113
|
if (!cta) return;
|
|
2114
|
+
if (this.salesClosed) {
|
|
2115
|
+
cta.disabled = true;
|
|
2116
|
+
cta.textContent = this.tf("picker.salesClosedCta", "Sales closed");
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
1587
2119
|
if (this.confirmSeat) {
|
|
1588
2120
|
cta.disabled = true;
|
|
1589
2121
|
cta.textContent = "Confirm or cancel this seat";
|
|
@@ -1719,7 +2251,6 @@ var SeatPicker = class _SeatPicker {
|
|
|
1719
2251
|
canvas.style.height = `${h}px`;
|
|
1720
2252
|
wrap.appendChild(canvas);
|
|
1721
2253
|
(this.regions["bottom-left"] ?? this.els.map).appendChild(wrap);
|
|
1722
|
-
this.miniEl = wrap;
|
|
1723
2254
|
this.miniCanvas = canvas;
|
|
1724
2255
|
const scale = Math.min((w - PAD * 2) / Math.max(1, b.width), (h - PAD * 2) / Math.max(1, b.height)) * dpr;
|
|
1725
2256
|
const offX = (w * dpr - b.width * scale) / 2 - b.x * scale;
|
|
@@ -1832,9 +2363,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1832
2363
|
this.controller.overview();
|
|
1833
2364
|
}
|
|
1834
2365
|
// ---- F4 price-band filter -------------------------------------------------
|
|
1835
|
-
/** Effective price of a category
|
|
2366
|
+
/** Effective display price of a category: host pricing override → first tier → base. */
|
|
1836
2367
|
catPrice(c) {
|
|
1837
|
-
|
|
2368
|
+
const chart = c.tiers?.length ? c.tiers[0].price : c.price;
|
|
2369
|
+
if (chart === void 0 || !c.key) return chart;
|
|
2370
|
+
return this.paidPrice(c.key, c.tiers?.[0]?.id ?? null, chart);
|
|
1838
2371
|
}
|
|
1839
2372
|
/** Derive price bands: one chip per distinct price (≤5), else quantile ranges. */
|
|
1840
2373
|
priceBands() {
|
|
@@ -1879,7 +2412,6 @@ var SeatPicker = class _SeatPicker {
|
|
|
1879
2412
|
select.setAttribute("aria-label", "Filter and focus seats by price");
|
|
1880
2413
|
select.innerHTML = `<option value="all">All prices</option>` + bands.map((band) => `<option value="${band.id}">${band.label}</option>`).join("");
|
|
1881
2414
|
this.els.pricesSec.appendChild(select);
|
|
1882
|
-
this.priceFilterEl = select;
|
|
1883
2415
|
select.addEventListener("change", () => {
|
|
1884
2416
|
const band = bands.find((candidate) => candidate.id === select.value);
|
|
1885
2417
|
const keys = band?.keys ?? null;
|
|
@@ -1983,7 +2515,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
1983
2515
|
renderSectionCard(summary) {
|
|
1984
2516
|
if (!this.els.map) return;
|
|
1985
2517
|
this.secCardEl?.remove();
|
|
1986
|
-
const
|
|
2518
|
+
const paid = summary.categories.length ? summary.categories.map((c) => this.paidPrice(c.key, null, c.price)) : [summary.priceMin, summary.priceMax];
|
|
2519
|
+
const paidMin = Math.min(...paid);
|
|
2520
|
+
const paidMax = Math.max(...paid);
|
|
2521
|
+
const priceLabel = paidMin === paidMax ? this.money(paidMin) : `${this.money(paidMin)}\u2013${this.money(paidMax)}`;
|
|
1987
2522
|
const leftLabel = (0, import_core2.tCount)("picker.seatsLeftInSection", summary.seatsLeft);
|
|
1988
2523
|
const xBtn = `<button type="button" class="sl-seccard-x" aria-label="${(0, import_core2.t)("picker.closeSectionSummary")}">\u2715</button>`;
|
|
1989
2524
|
const card = document.createElement("div");
|
|
@@ -2013,7 +2548,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2013
2548
|
card.setAttribute("aria-label", (0, import_core2.t)("picker.sectionSummaryAria", { label: summary.label }));
|
|
2014
2549
|
const mix = summary.categories.map((c) => {
|
|
2015
2550
|
const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
|
|
2016
|
-
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>`;
|
|
2551
|
+
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>`;
|
|
2017
2552
|
}).join("");
|
|
2018
2553
|
card.innerHTML = `<div class="sl-seccard-head"><span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span>` + (summary.categories.length ? `<span class="sl-seccard-price">${priceLabel}</span>` : "") + xBtn + `</div><div class="sl-seccard-zone">${summary.zoneLabel ? `${summary.zoneLabel} \xB7 ` : ""}<span class="sl-seccard-left">${leftLabel}</span></div>` + (mix ? `<div class="sl-seccard-mix">${mix}</div>` : "") + `<div class="sl-seccard-foot"><button type="button" class="sl-seccard-overview">\u2190 ${(0, import_core2.t)("picker.overview")}</button><span class="sl-seccard-hint">${(0, import_core2.t)("picker.tapSeatHint")}</span></div>`;
|
|
2019
2554
|
card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
|
|
@@ -2080,7 +2615,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2080
2615
|
const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
|
|
2081
2616
|
const status = this.controller.getStatus(seat.id) ?? "free";
|
|
2082
2617
|
const statusText = status === "free" ? "available" : status === "held" ? "on hold" : "taken";
|
|
2083
|
-
const price = cat
|
|
2618
|
+
const price = cat ? this.catPrice(cat) : void 0;
|
|
2084
2619
|
this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
|
|
2085
2620
|
}
|
|
2086
2621
|
// ---- seat candidate confirmation ------------------------------------------
|
|
@@ -2095,7 +2630,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
2095
2630
|
if (this.tipEl) this.tipEl.style.display = "none";
|
|
2096
2631
|
const details = this.controller.seatDetails(seat.id);
|
|
2097
2632
|
const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
|
|
2098
|
-
const
|
|
2633
|
+
const chartPrice = details?.price ?? (cat?.tiers?.length ? cat.tiers[0].price : cat?.price);
|
|
2634
|
+
const price = chartPrice != null ? this.paidPrice(seat.categoryKey, details?.tierId ?? cat?.tiers?.[0]?.id ?? null, chartPrice) : void 0;
|
|
2099
2635
|
const safe = (value) => String(value ?? "\u2014").replace(/[&<>"]/g, (char) => ({
|
|
2100
2636
|
"&": "&",
|
|
2101
2637
|
"<": "<",
|
|
@@ -2108,7 +2644,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2108
2644
|
el.setAttribute("aria-modal", "true");
|
|
2109
2645
|
el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
|
|
2110
2646
|
el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
|
|
2111
|
-
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
|
|
2647
|
+
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>`;
|
|
2112
2648
|
this.els.map.appendChild(el);
|
|
2113
2649
|
this.confirmEl = el;
|
|
2114
2650
|
this.reanchorConfirm();
|
|
@@ -2273,18 +2809,35 @@ var SeatPicker = class _SeatPicker {
|
|
|
2273
2809
|
}
|
|
2274
2810
|
// ---- chrome sync ----------------------------------------------------------
|
|
2275
2811
|
money(n) {
|
|
2812
|
+
const formatter = this.opts.pricing?.formatter;
|
|
2813
|
+
if (formatter) return formatter(n, this.currency);
|
|
2276
2814
|
try {
|
|
2277
2815
|
return new Intl.NumberFormat(this.opts.locale, { style: "currency", currency: this.currency }).format(n);
|
|
2278
2816
|
} catch {
|
|
2279
2817
|
return `${n} ${this.currency}`;
|
|
2280
2818
|
}
|
|
2281
2819
|
}
|
|
2820
|
+
/**
|
|
2821
|
+
* The price the buyer will actually pay for a category (+tier): the host's
|
|
2822
|
+
* `pricing` override when present, else the chart's stored price. Every
|
|
2823
|
+
* price the widget DISPLAYS or hands off must flow through here — a map
|
|
2824
|
+
* that shows one price while checkout charges another destroys trust.
|
|
2825
|
+
*/
|
|
2826
|
+
paidPrice(categoryKey, tierId, fallback) {
|
|
2827
|
+
const entry = categoryKey ? this.opts.pricing?.prices?.[categoryKey] : void 0;
|
|
2828
|
+
if (entry === void 0) return fallback;
|
|
2829
|
+
if (typeof entry === "number") return entry;
|
|
2830
|
+
if (tierId && entry.tiers?.[tierId] !== void 0) return entry.tiers[tierId];
|
|
2831
|
+
return entry.base ?? fallback;
|
|
2832
|
+
}
|
|
2282
2833
|
syncPrices() {
|
|
2283
2834
|
const doc = this.controller.doc;
|
|
2284
2835
|
if (!doc || !this.els.prices) return;
|
|
2285
2836
|
const left = this.controller.categoryAvailability();
|
|
2837
|
+
this.narrateAvailability(doc.categories, left);
|
|
2838
|
+
this.syncSoldout(doc.categories, left);
|
|
2286
2839
|
this.els.prices.innerHTML = doc.categories.map((c) => {
|
|
2287
|
-
const price =
|
|
2840
|
+
const price = this.catPrice(c);
|
|
2288
2841
|
const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
|
|
2289
2842
|
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>`;
|
|
2290
2843
|
}).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>`;
|
|
@@ -2293,6 +2846,26 @@ var SeatPicker = class _SeatPicker {
|
|
|
2293
2846
|
row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
|
|
2294
2847
|
});
|
|
2295
2848
|
}
|
|
2849
|
+
/**
|
|
2850
|
+
* Live-activity strip: turn WS availability deltas into one quiet line of
|
|
2851
|
+
* social proof ("2 seats just taken in VIP · 118 left"). Diffs per-category
|
|
2852
|
+
* counts on every status change — no per-seat payload needed. Skips the very
|
|
2853
|
+
* first computation (initial load is not "activity").
|
|
2854
|
+
*/
|
|
2855
|
+
narrateAvailability(categories, left) {
|
|
2856
|
+
const textEl = this.els.liveText;
|
|
2857
|
+
const prev = this.lastCatAvail;
|
|
2858
|
+
this.lastCatAvail = { ...left };
|
|
2859
|
+
if (!textEl || !prev) return;
|
|
2860
|
+
for (const cat of categories) {
|
|
2861
|
+
const before = prev[cat.key];
|
|
2862
|
+
const now = left[cat.key] ?? 0;
|
|
2863
|
+
if (before === void 0 || now >= before) continue;
|
|
2864
|
+
const taken = before - now;
|
|
2865
|
+
textEl.textContent = `${taken} seat${taken === 1 ? "" : "s"} just taken in ${cat.label} \xB7 ${now} left`;
|
|
2866
|
+
return;
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2296
2869
|
/** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
|
|
2297
2870
|
evictTakenSelections() {
|
|
2298
2871
|
const ownLabels = /* @__PURE__ */ new Set([
|
|
@@ -2321,15 +2894,23 @@ var SeatPicker = class _SeatPicker {
|
|
|
2321
2894
|
const cats = this.controller.doc?.categories ?? [];
|
|
2322
2895
|
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>`);
|
|
2323
2896
|
}
|
|
2897
|
+
const idGrid = (seatId, label) => {
|
|
2898
|
+
const d = seatId ? this.controller.seatDetails(seatId) : null;
|
|
2899
|
+
if (!d?.sectionLabel && !d?.rowLabel && !d?.seatNumber) {
|
|
2900
|
+
return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">Seat</span><span class="val">${label}</span></span></div>`;
|
|
2901
|
+
}
|
|
2902
|
+
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>`;
|
|
2903
|
+
};
|
|
2904
|
+
const iconRail = (rmAria, viewLabel) => `<div class="sl-chip-rail"><button type="button" class="rm" aria-label="${rmAria}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>` + (viewLabel ? `<button type="button" class="view" data-view-label="${viewLabel}" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: viewLabel })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "") + `</div>`;
|
|
2324
2905
|
for (const item of heldItems) {
|
|
2325
2906
|
const itemKey = `held:${item.label}`;
|
|
2326
2907
|
nextTrayKeys.add(itemKey);
|
|
2327
2908
|
const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
|
|
2328
2909
|
const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
|
|
2329
|
-
const
|
|
2330
|
-
const
|
|
2910
|
+
const heldSeat = item.objectType !== "ga" ? this.controller.seatByLabel(item.label) : null;
|
|
2911
|
+
const canView2 = this.seatViewEnabled() && !!heldSeat;
|
|
2331
2912
|
parts.push(
|
|
2332
|
-
`<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><
|
|
2913
|
+
`<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>`
|
|
2333
2914
|
);
|
|
2334
2915
|
}
|
|
2335
2916
|
const heldLabels = new Set(heldItems.map((item) => item.label));
|
|
@@ -2338,16 +2919,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
2338
2919
|
const itemKey = `seat:${s.id}`;
|
|
2339
2920
|
nextTrayKeys.add(itemKey);
|
|
2340
2921
|
const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
|
|
2341
|
-
const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${(0, import_core2.t)("picker.ticketTierFor", { label: s.label })}">` + s.tiers.map((ti) => `<option value="${ti.id}"${ti.id === s.tierId ? " selected" : ""}>${ti.name} \xB7 ${this.money(ti.price)}</option>`).join("") + `</select>` : "";
|
|
2342
|
-
const viewBtn = canView ? `<button type="button" class="view" data-view-label="${s.label}" aria-label="${(0, import_core2.t)("picker.viewFromSeat", { label: s.label })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "";
|
|
2922
|
+
const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${(0, import_core2.t)("picker.ticketTierFor", { label: s.label })}">` + s.tiers.map((ti) => `<option value="${ti.id}"${ti.id === s.tierId ? " selected" : ""}>${ti.name} \xB7 ${this.money(this.paidPrice(s.categoryKey, ti.id, ti.price))}</option>`).join("") + `</select>` : "";
|
|
2343
2923
|
parts.push(
|
|
2344
|
-
`<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><
|
|
2924
|
+
`<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>`
|
|
2345
2925
|
);
|
|
2346
2926
|
}
|
|
2347
2927
|
for (const area of gaAreas) {
|
|
2348
2928
|
const qty = this.gaQty.get(area.id) ?? 0;
|
|
2349
2929
|
parts.push(
|
|
2350
|
-
`<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>`
|
|
2930
|
+
`<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>`
|
|
2351
2931
|
);
|
|
2352
2932
|
}
|
|
2353
2933
|
this.els.tray.innerHTML = parts.join("");
|
|
@@ -2387,7 +2967,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2387
2967
|
return;
|
|
2388
2968
|
}
|
|
2389
2969
|
const id = chip.dataset.seat;
|
|
2390
|
-
const label =
|
|
2970
|
+
const label = this.controller.getSelection().find((sel) => sel.id === id)?.label ?? "Seat";
|
|
2391
2971
|
const remove = () => {
|
|
2392
2972
|
this.controller.deselect([id]);
|
|
2393
2973
|
this.toast(`${label} removed.`, "neutral", {
|
|
@@ -2418,6 +2998,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
2418
2998
|
if (seat) this.openSeatView(seat);
|
|
2419
2999
|
});
|
|
2420
3000
|
});
|
|
3001
|
+
this.els.tray.querySelectorAll(".sl-chip[data-locate]").forEach((chip) => {
|
|
3002
|
+
const locate = () => this.controller.flashSeat(chip.dataset.locate, this.cssVar("--sl-accent") || "#f4b740");
|
|
3003
|
+
chip.addEventListener("mouseenter", locate);
|
|
3004
|
+
chip.addEventListener("focusin", locate);
|
|
3005
|
+
});
|
|
2421
3006
|
this.els.tray.querySelectorAll(".sl-ga button").forEach((btn) => {
|
|
2422
3007
|
btn.addEventListener("click", () => {
|
|
2423
3008
|
const areaEl = btn.closest(".sl-ga");
|
|
@@ -2430,12 +3015,17 @@ var SeatPicker = class _SeatPicker {
|
|
|
2430
3015
|
this.syncTray();
|
|
2431
3016
|
});
|
|
2432
3017
|
});
|
|
3018
|
+
if (this.salesClosed) {
|
|
3019
|
+
this.els.tray.querySelectorAll(".sl-ba-go,[data-ba],[data-ba-cat],[data-ba-replace],.sl-ga button").forEach((el) => {
|
|
3020
|
+
el.disabled = true;
|
|
3021
|
+
});
|
|
3022
|
+
}
|
|
2433
3023
|
const gaTotal = this.pendingGATotal(gaAreas);
|
|
2434
3024
|
const gaCount = this.pendingGACount();
|
|
2435
|
-
const heldTotal = heldItems.reduce((sum, item) => sum + item.unitPrice * (item.quantity ?? 1), 0);
|
|
3025
|
+
const heldTotal = heldItems.reduce((sum, item) => sum + this.paidPrice(item.categoryKey, item.tierId, item.unitPrice) * (item.quantity ?? 1), 0);
|
|
2436
3026
|
const heldCount = heldItems.reduce((sum, item) => sum + (item.quantity ?? 1), 0);
|
|
2437
3027
|
const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
|
|
2438
|
-
const total = freshSeats.reduce((sum, s) => sum + s.price, 0) + gaTotal + heldTotal;
|
|
3028
|
+
const total = freshSeats.reduce((sum, s) => sum + this.paidPrice(s.categoryKey, s.tierId ?? null, s.price), 0) + gaTotal + heldTotal;
|
|
2439
3029
|
const count = freshSeats.length + gaCount + heldCount;
|
|
2440
3030
|
const pendingCount = this.pendingSelectionCount();
|
|
2441
3031
|
const previousCount = this.lastTrayCount;
|
|
@@ -2531,6 +3121,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2531
3121
|
}
|
|
2532
3122
|
}
|
|
2533
3123
|
async handleCta() {
|
|
3124
|
+
if (this.salesClosed) return;
|
|
2534
3125
|
if (this.totalTicketCount() > this.maxTickets) {
|
|
2535
3126
|
this.toast(`Remove tickets until your order has ${this.maxTickets} or fewer.`, "warning");
|
|
2536
3127
|
return;
|
|
@@ -2574,6 +3165,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2574
3165
|
this.opts.onError?.(err);
|
|
2575
3166
|
const problem = err;
|
|
2576
3167
|
const labels = (problem.conflicts ?? []).map((conflict) => conflict.label).filter(Boolean).slice(0, 3);
|
|
3168
|
+
if (problem.reason === "event_closed") this.setSalesClosed(true);
|
|
2577
3169
|
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.";
|
|
2578
3170
|
this.toast(message, "error");
|
|
2579
3171
|
this.setCtaPhase("idle");
|
|
@@ -2682,7 +3274,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2682
3274
|
objectType: it.objectType,
|
|
2683
3275
|
categoryKey: it.categoryKey,
|
|
2684
3276
|
tierId: it.tierId,
|
|
2685
|
-
unitPrice: it.unitPrice,
|
|
3277
|
+
unitPrice: this.paidPrice(it.categoryKey, it.tierId, it.unitPrice),
|
|
2686
3278
|
currency: it.currency ?? this.currency,
|
|
2687
3279
|
quantity: it.quantity ?? 1
|
|
2688
3280
|
}));
|
|
@@ -2735,19 +3327,37 @@ var SeatPicker = class _SeatPicker {
|
|
|
2735
3327
|
this.tipEl.style.left = `${Math.max(8, x)}px`;
|
|
2736
3328
|
this.tipEl.style.top = `${Math.max(8, y)}px`;
|
|
2737
3329
|
}
|
|
3330
|
+
/**
|
|
3331
|
+
* Row label without the redundant section prefix. Charts commonly name row
|
|
3332
|
+
* objects "104-A" while the Section column already shows "104" — so the Row
|
|
3333
|
+
* cell repeats the section and, in the compact hover card, truncates to
|
|
3334
|
+
* "10…". Strip a leading "<section><sep>" so Row reads a clean "A". Only when
|
|
3335
|
+
* the prefix is exact (won't touch "1040-A" under section "104"); otherwise
|
|
3336
|
+
* the label is shown verbatim.
|
|
3337
|
+
*/
|
|
3338
|
+
rowShort(details) {
|
|
3339
|
+
const row = details?.rowLabel;
|
|
3340
|
+
const sec = details?.sectionLabel;
|
|
3341
|
+
if (!row || !sec) return row;
|
|
3342
|
+
for (const sep of ["-", " ", "\xB7", "/", "_"]) {
|
|
3343
|
+
const prefix = `${sec}${sep}`;
|
|
3344
|
+
if (row.startsWith(prefix) && row.length > prefix.length) return row.slice(prefix.length);
|
|
3345
|
+
}
|
|
3346
|
+
return row;
|
|
3347
|
+
}
|
|
2738
3348
|
updateTooltip(details) {
|
|
2739
3349
|
if (!this.tipEl) return;
|
|
2740
3350
|
if (!details) {
|
|
2741
3351
|
this.tipEl.style.display = "none";
|
|
2742
3352
|
return;
|
|
2743
3353
|
}
|
|
2744
|
-
const
|
|
2745
|
-
const
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
this.tipEl.innerHTML = `<div
|
|
3354
|
+
const esc2 = (v) => String(v ?? "\u2014").replace(/[&<>"]/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[ch]);
|
|
3355
|
+
const price = this.money(this.paidPrice(details.categoryKey, details.tierId ?? null, details.price));
|
|
3356
|
+
const hasLoc = details.sectionLabel || details.rowLabel || details.seatNumber;
|
|
3357
|
+
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>`;
|
|
3358
|
+
const statusLine = details.status === "free" ? "" : `<div class="sl-tip-status">${details.status === "held" ? (0, import_core2.t)("map.statusHeld") : (0, import_core2.t)("map.statusTaken")}</div>`;
|
|
3359
|
+
this.tipEl.style.setProperty("--sl-cat", details.categoryColor);
|
|
3360
|
+
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;
|
|
2751
3361
|
this.tipEl.style.display = "block";
|
|
2752
3362
|
this.placeTooltip();
|
|
2753
3363
|
}
|
|
@@ -2768,7 +3378,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2768
3378
|
return this.removeHeldLabel(label);
|
|
2769
3379
|
}
|
|
2770
3380
|
async bestAvailable(qty, categoryKey) {
|
|
2771
|
-
if (this.bestAvailableBusy) return null;
|
|
3381
|
+
if (this.salesClosed || this.bestAvailableBusy) return null;
|
|
2772
3382
|
qty = Math.max(1, Math.min(this.maxTickets, Math.floor(qty)));
|
|
2773
3383
|
if (this.confirmSeat) this.cancelConfirm();
|
|
2774
3384
|
this.bestAvailableConfirm = false;
|
|
@@ -2849,6 +3459,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
2849
3459
|
this.ro?.disconnect();
|
|
2850
3460
|
this.ro = null;
|
|
2851
3461
|
if (this.escHandler) document.removeEventListener("keydown", this.escHandler);
|
|
3462
|
+
if (this.fsChangeHandler) document.removeEventListener("fullscreenchange", this.fsChangeHandler);
|
|
3463
|
+
if (this.fsEscHandler) window.removeEventListener("keydown", this.fsEscHandler);
|
|
2852
3464
|
this.controller.destroy();
|
|
2853
3465
|
this.root?.remove();
|
|
2854
3466
|
this.root = null;
|