@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.js
CHANGED
|
@@ -326,16 +326,46 @@ var TYPES = /* @__PURE__ */ new Set([
|
|
|
326
326
|
"seatlayer.designer.close",
|
|
327
327
|
"seatlayer.designer.error"
|
|
328
328
|
]);
|
|
329
|
+
var DEFAULT_LOADING_TIMEOUT_MS = 2e4;
|
|
329
330
|
function resolveContainer2(container) {
|
|
330
331
|
if (typeof container !== "string") return container;
|
|
331
332
|
const element = document.querySelector(container);
|
|
332
333
|
if (!element) throw new Error(`EmbeddedDesigner container not found: ${container}`);
|
|
333
334
|
return element;
|
|
334
335
|
}
|
|
336
|
+
function causeFromCode(code) {
|
|
337
|
+
const value = (code ?? "").toLowerCase();
|
|
338
|
+
if (value.includes("expire") || value.includes("revoke") || value === "401") return "expired";
|
|
339
|
+
if (value.includes("mismatch")) return "mismatch";
|
|
340
|
+
if (value.includes("timeout")) return "timeout";
|
|
341
|
+
return "load";
|
|
342
|
+
}
|
|
343
|
+
var ERROR_COPY = {
|
|
344
|
+
expired: {
|
|
345
|
+
title: "This design session expired",
|
|
346
|
+
body: "For your security, editing sessions are short-lived. Start a fresh one to keep designing."
|
|
347
|
+
},
|
|
348
|
+
mismatch: {
|
|
349
|
+
title: "This editor doesn't match this chart",
|
|
350
|
+
body: "The session that loaded belongs to a different chart or workspace. Reopen the designer to continue."
|
|
351
|
+
},
|
|
352
|
+
timeout: {
|
|
353
|
+
title: "The designer is taking too long",
|
|
354
|
+
body: "It did not finish loading in time. This is usually a slow connection \u2014 try again."
|
|
355
|
+
},
|
|
356
|
+
load: {
|
|
357
|
+
title: "We couldn't load the designer",
|
|
358
|
+
body: "Something went wrong while opening the editor. Please try again."
|
|
359
|
+
}
|
|
360
|
+
};
|
|
335
361
|
var EmbeddedDesigner = class {
|
|
336
362
|
constructor(options) {
|
|
337
363
|
this.frame = null;
|
|
338
364
|
this.designerOrigin = "";
|
|
365
|
+
this.overlay = null;
|
|
366
|
+
this.timeoutTimer = null;
|
|
367
|
+
this.phase = "loading";
|
|
368
|
+
this.restoreContainerPosition = null;
|
|
339
369
|
this.handleMessage = (event) => {
|
|
340
370
|
if (!this.frame || event.origin !== this.designerOrigin || event.source !== this.frame.contentWindow) return;
|
|
341
371
|
if (!event.data || typeof event.data !== "object") return;
|
|
@@ -350,10 +380,15 @@ var EmbeddedDesigner = class {
|
|
|
350
380
|
message: typeof data.message === "string" ? data.message : void 0,
|
|
351
381
|
meta: data.meta
|
|
352
382
|
};
|
|
353
|
-
if (this.options.expectedChartId && message.chartId && message.chartId !== this.options.expectedChartId)
|
|
354
|
-
|
|
383
|
+
if (this.options.expectedChartId && message.chartId && message.chartId !== this.options.expectedChartId || this.options.expectedWorkspaceId && message.workspaceId && message.workspaceId !== this.options.expectedWorkspaceId) {
|
|
384
|
+
this.showError("mismatch");
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
355
387
|
switch (message.type) {
|
|
356
388
|
case "seatlayer.designer.ready":
|
|
389
|
+
this.phase = "ready";
|
|
390
|
+
this.clearTimeoutTimer();
|
|
391
|
+
this.removeOverlay();
|
|
357
392
|
this.options.onReady?.(message);
|
|
358
393
|
break;
|
|
359
394
|
case "seatlayer.designer.saved":
|
|
@@ -366,6 +401,7 @@ var EmbeddedDesigner = class {
|
|
|
366
401
|
this.options.onClose?.(message);
|
|
367
402
|
break;
|
|
368
403
|
case "seatlayer.designer.error":
|
|
404
|
+
this.showError(causeFromCode(message.code));
|
|
369
405
|
this.options.onError?.(message);
|
|
370
406
|
break;
|
|
371
407
|
}
|
|
@@ -389,9 +425,21 @@ var EmbeddedDesigner = class {
|
|
|
389
425
|
frame.style.border = "0";
|
|
390
426
|
Object.assign(frame.style, this.options.style);
|
|
391
427
|
if (this.options.className) frame.className = this.options.className;
|
|
428
|
+
const container = resolveContainer2(this.options.container);
|
|
392
429
|
window.addEventListener("message", this.handleMessage);
|
|
393
|
-
|
|
430
|
+
container.append(frame);
|
|
394
431
|
this.frame = frame;
|
|
432
|
+
this.phase = "loading";
|
|
433
|
+
if (this.loadingStateEnabled()) {
|
|
434
|
+
this.ensureContainerPositioned(container);
|
|
435
|
+
this.renderOverlay(container, "loading");
|
|
436
|
+
const timeout = this.options.loadingTimeoutMs ?? DEFAULT_LOADING_TIMEOUT_MS;
|
|
437
|
+
if (timeout > 0 && Number.isFinite(timeout)) {
|
|
438
|
+
this.timeoutTimer = setTimeout(() => {
|
|
439
|
+
if (this.phase === "loading") this.showError("timeout");
|
|
440
|
+
}, timeout);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
395
443
|
return frame;
|
|
396
444
|
}
|
|
397
445
|
/** Replace the iframe instead of assigning a new fragment to an existing one. */
|
|
@@ -404,9 +452,208 @@ var EmbeddedDesigner = class {
|
|
|
404
452
|
}
|
|
405
453
|
destroy() {
|
|
406
454
|
window.removeEventListener("message", this.handleMessage);
|
|
455
|
+
this.clearTimeoutTimer();
|
|
456
|
+
this.removeOverlay();
|
|
457
|
+
this.restoreContainerStyle();
|
|
407
458
|
this.frame?.remove();
|
|
408
459
|
this.frame = null;
|
|
409
460
|
this.designerOrigin = "";
|
|
461
|
+
this.phase = "loading";
|
|
462
|
+
}
|
|
463
|
+
loadingStateEnabled() {
|
|
464
|
+
return this.options.showLoadingState !== false;
|
|
465
|
+
}
|
|
466
|
+
clearTimeoutTimer() {
|
|
467
|
+
if (this.timeoutTimer !== null) {
|
|
468
|
+
clearTimeout(this.timeoutTimer);
|
|
469
|
+
this.timeoutTimer = null;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
ensureContainerPositioned(container) {
|
|
473
|
+
const position = getComputedStyle(container).position;
|
|
474
|
+
if (position === "static") {
|
|
475
|
+
this.restoreContainerPosition = container.style.position;
|
|
476
|
+
container.style.position = "relative";
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
restoreContainerStyle() {
|
|
480
|
+
if (this.restoreContainerPosition === null) return;
|
|
481
|
+
try {
|
|
482
|
+
resolveContainer2(this.options.container).style.position = this.restoreContainerPosition;
|
|
483
|
+
} catch {
|
|
484
|
+
}
|
|
485
|
+
this.restoreContainerPosition = null;
|
|
486
|
+
}
|
|
487
|
+
removeOverlay() {
|
|
488
|
+
this.overlay?.remove();
|
|
489
|
+
this.overlay = null;
|
|
490
|
+
}
|
|
491
|
+
showError(cause) {
|
|
492
|
+
this.phase = "error";
|
|
493
|
+
this.clearTimeoutTimer();
|
|
494
|
+
if (!this.loadingStateEnabled()) return;
|
|
495
|
+
let container;
|
|
496
|
+
try {
|
|
497
|
+
container = resolveContainer2(this.options.container);
|
|
498
|
+
} catch {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
this.renderOverlay(container, "error", cause);
|
|
502
|
+
}
|
|
503
|
+
handleTryAgain() {
|
|
504
|
+
if (this.options.onRequestRelaunch) {
|
|
505
|
+
this.options.onRequestRelaunch();
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
this.mount();
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Build (or rebuild) the overlay for the given phase. A single overlay element
|
|
512
|
+
* is reused so we never stack stale skeletons or cards.
|
|
513
|
+
*/
|
|
514
|
+
renderOverlay(container, phase, cause) {
|
|
515
|
+
this.removeOverlay();
|
|
516
|
+
const overlay = document.createElement("div");
|
|
517
|
+
overlay.setAttribute("data-seatlayer-designer-overlay", phase);
|
|
518
|
+
overlay.setAttribute("role", phase === "error" ? "alert" : "status");
|
|
519
|
+
overlay.setAttribute("aria-live", "polite");
|
|
520
|
+
Object.assign(overlay.style, {
|
|
521
|
+
position: "absolute",
|
|
522
|
+
inset: "0",
|
|
523
|
+
display: "flex",
|
|
524
|
+
alignItems: "center",
|
|
525
|
+
justifyContent: "center",
|
|
526
|
+
background: "#101625",
|
|
527
|
+
color: "#e6ebf5",
|
|
528
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
|
529
|
+
zIndex: "2",
|
|
530
|
+
overflow: "hidden"
|
|
531
|
+
});
|
|
532
|
+
if (phase === "loading") this.buildSkeleton(overlay);
|
|
533
|
+
else this.buildErrorCard(overlay, cause ?? "load");
|
|
534
|
+
container.append(overlay);
|
|
535
|
+
this.overlay = overlay;
|
|
536
|
+
}
|
|
537
|
+
buildSkeleton(overlay) {
|
|
538
|
+
const style = document.createElement("style");
|
|
539
|
+
style.textContent = `
|
|
540
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
541
|
+
@keyframes seatlayer-designer-shimmer {
|
|
542
|
+
0% { background-position: -320px 0; }
|
|
543
|
+
100% { background-position: 320px 0; }
|
|
544
|
+
}
|
|
545
|
+
[data-seatlayer-designer-overlay="loading"] .sl-shimmer {
|
|
546
|
+
animation: seatlayer-designer-shimmer 1.25s ease-in-out infinite;
|
|
547
|
+
background-size: 640px 100%;
|
|
548
|
+
}
|
|
549
|
+
}`;
|
|
550
|
+
overlay.append(style);
|
|
551
|
+
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%)";
|
|
552
|
+
const scaffold = document.createElement("div");
|
|
553
|
+
Object.assign(scaffold.style, {
|
|
554
|
+
position: "absolute",
|
|
555
|
+
inset: "0",
|
|
556
|
+
display: "flex",
|
|
557
|
+
flexDirection: "column",
|
|
558
|
+
padding: "16px",
|
|
559
|
+
gap: "14px",
|
|
560
|
+
opacity: "0.9"
|
|
561
|
+
});
|
|
562
|
+
const bar = (styles) => {
|
|
563
|
+
const node = document.createElement("div");
|
|
564
|
+
node.className = "sl-shimmer";
|
|
565
|
+
Object.assign(node.style, {
|
|
566
|
+
background: shimmer,
|
|
567
|
+
borderRadius: "8px"
|
|
568
|
+
});
|
|
569
|
+
Object.assign(node.style, styles);
|
|
570
|
+
return node;
|
|
571
|
+
};
|
|
572
|
+
scaffold.append(bar({ height: "40px", width: "100%", flex: "0 0 auto" }));
|
|
573
|
+
const body = document.createElement("div");
|
|
574
|
+
Object.assign(body.style, {
|
|
575
|
+
display: "flex",
|
|
576
|
+
gap: "14px",
|
|
577
|
+
flex: "1 1 auto",
|
|
578
|
+
minHeight: "0"
|
|
579
|
+
});
|
|
580
|
+
body.append(bar({ width: "220px", height: "100%", flex: "0 0 auto" }));
|
|
581
|
+
body.append(bar({ flex: "1 1 auto", height: "100%" }));
|
|
582
|
+
scaffold.append(body);
|
|
583
|
+
overlay.append(scaffold);
|
|
584
|
+
const caption = document.createElement("div");
|
|
585
|
+
Object.assign(caption.style, {
|
|
586
|
+
position: "relative",
|
|
587
|
+
zIndex: "1",
|
|
588
|
+
display: "flex",
|
|
589
|
+
alignItems: "center",
|
|
590
|
+
gap: "10px",
|
|
591
|
+
padding: "10px 16px",
|
|
592
|
+
borderRadius: "999px",
|
|
593
|
+
background: "rgba(16, 22, 37, 0.72)",
|
|
594
|
+
fontSize: "13px",
|
|
595
|
+
fontWeight: "500",
|
|
596
|
+
letterSpacing: "0.01em"
|
|
597
|
+
});
|
|
598
|
+
const dot = document.createElement("span");
|
|
599
|
+
dot.className = "sl-shimmer";
|
|
600
|
+
Object.assign(dot.style, {
|
|
601
|
+
width: "9px",
|
|
602
|
+
height: "9px",
|
|
603
|
+
borderRadius: "50%",
|
|
604
|
+
background: shimmer,
|
|
605
|
+
flex: "0 0 auto"
|
|
606
|
+
});
|
|
607
|
+
caption.append(dot);
|
|
608
|
+
caption.append(document.createTextNode("Loading designer\u2026"));
|
|
609
|
+
overlay.append(caption);
|
|
610
|
+
}
|
|
611
|
+
buildErrorCard(overlay, cause) {
|
|
612
|
+
const copy = ERROR_COPY[cause];
|
|
613
|
+
const card = document.createElement("div");
|
|
614
|
+
Object.assign(card.style, {
|
|
615
|
+
maxWidth: "420px",
|
|
616
|
+
margin: "0 24px",
|
|
617
|
+
padding: "28px",
|
|
618
|
+
textAlign: "center",
|
|
619
|
+
background: "rgba(255, 255, 255, 0.03)",
|
|
620
|
+
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
621
|
+
borderRadius: "16px",
|
|
622
|
+
boxShadow: "0 12px 40px rgba(0, 0, 0, 0.35)"
|
|
623
|
+
});
|
|
624
|
+
const heading = document.createElement("h2");
|
|
625
|
+
heading.textContent = copy.title;
|
|
626
|
+
Object.assign(heading.style, {
|
|
627
|
+
margin: "0 0 8px",
|
|
628
|
+
fontSize: "17px",
|
|
629
|
+
fontWeight: "600",
|
|
630
|
+
color: "#f4f7ff"
|
|
631
|
+
});
|
|
632
|
+
const body = document.createElement("p");
|
|
633
|
+
body.textContent = copy.body;
|
|
634
|
+
Object.assign(body.style, {
|
|
635
|
+
margin: "0 0 20px",
|
|
636
|
+
fontSize: "13.5px",
|
|
637
|
+
lineHeight: "1.5",
|
|
638
|
+
color: "#aab4c8"
|
|
639
|
+
});
|
|
640
|
+
const button = document.createElement("button");
|
|
641
|
+
button.type = "button";
|
|
642
|
+
button.textContent = "Try again";
|
|
643
|
+
Object.assign(button.style, {
|
|
644
|
+
appearance: "none",
|
|
645
|
+
cursor: "pointer",
|
|
646
|
+
border: "0",
|
|
647
|
+
borderRadius: "10px",
|
|
648
|
+
padding: "10px 22px",
|
|
649
|
+
fontSize: "14px",
|
|
650
|
+
fontWeight: "600",
|
|
651
|
+
color: "#101625",
|
|
652
|
+
background: "#7aa2ff"
|
|
653
|
+
});
|
|
654
|
+
button.addEventListener("click", () => this.handleTryAgain());
|
|
655
|
+
card.append(heading, body, button);
|
|
656
|
+
overlay.append(card);
|
|
410
657
|
}
|
|
411
658
|
};
|
|
412
659
|
|
|
@@ -415,6 +662,7 @@ import {
|
|
|
415
662
|
PickerController as PickerController2,
|
|
416
663
|
expandChart,
|
|
417
664
|
generateSeatPanorama,
|
|
665
|
+
generateSeatThumb,
|
|
418
666
|
loadLocale as loadLocale2,
|
|
419
667
|
setStringOverrides as setStringOverrides2,
|
|
420
668
|
t as t2,
|
|
@@ -579,26 +827,43 @@ var CSS = `
|
|
|
579
827
|
.sl-tray{flex:1;padding:10px 14px 14px;display:flex;flex-direction:column;gap:7px;min-height:0;overflow-y:auto;
|
|
580
828
|
overscroll-behavior:contain;scrollbar-gutter:stable}
|
|
581
829
|
.sl-tray-hint{font-size:12.5px;color:var(--sl-muted);line-height:1.5}
|
|
582
|
-
.sl-chip{position:relative;display:grid;grid-template-columns:
|
|
583
|
-
min-height:53px;
|
|
830
|
+
.sl-chip{position:relative;display:grid;grid-template-columns:minmax(0,1fr) 34px;align-items:stretch;
|
|
831
|
+
flex:none;min-height:53px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm);overflow:hidden;
|
|
584
832
|
background:var(--sl-surface);font-size:13px;transform-origin:center;transition:border-color .15s,background .15s}
|
|
585
833
|
.sl-chip:hover{border-color:color-mix(in srgb,var(--sl-accent) 38%,var(--sl-line))}
|
|
586
834
|
.sl-chip.sl-enter{animation:slChipIn .38s cubic-bezier(.2,.8,.2,1) both}
|
|
587
835
|
.sl-chip.sl-leave{pointer-events:none;animation:slChipOut .16s ease-in both}
|
|
588
836
|
.sl-chip.sl-held{border-color:var(--sl-line);background:color-mix(in srgb,var(--sl-accent) 7%,var(--sl-surface));
|
|
589
837
|
box-shadow:inset 3px 0 0 color-mix(in srgb,var(--sl-accent) 72%,transparent)}
|
|
590
|
-
.sl-ticket-state{width:
|
|
838
|
+
.sl-ticket-state{width:17px;height:17px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
591
839
|
background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
592
840
|
.sl-ticket-state.held{background:color-mix(in srgb,var(--sl-accent) 18%,var(--sl-surface));color:var(--sl-accent)}
|
|
593
|
-
.sl-ticket-state svg{width:
|
|
594
|
-
.sl-chip-main{min-width:0}
|
|
595
|
-
.sl-chip
|
|
596
|
-
.sl-chip-
|
|
841
|
+
.sl-ticket-state svg{width:10px;height:10px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
842
|
+
.sl-chip-main{min-width:0;padding:8px 10px 8px 11px;display:flex;flex-direction:column;justify-content:center;gap:5px}
|
|
843
|
+
.sl-chip-id{display:flex;gap:12px;min-width:0}
|
|
844
|
+
.sl-chip-id .fld{min-width:0}
|
|
845
|
+
.sl-chip-id .fld.sec{flex:1}
|
|
846
|
+
.sl-chip-id .fld.mid{flex:none;text-align:center}
|
|
847
|
+
.sl-chip-eb{display:block;font-size:8px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);margin-bottom:1px}
|
|
848
|
+
.sl-chip-id .val{display:block;font-weight:600;font-size:13px;line-height:1.25;white-space:nowrap}
|
|
849
|
+
.sl-chip-id .fld.sec .val{overflow:hidden;text-overflow:ellipsis}
|
|
850
|
+
.sl-chip-sub{display:flex;align-items:center;gap:6px;min-width:0}
|
|
597
851
|
.sl-chip .cat{color:var(--sl-muted);font-size:10.5px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
598
852
|
.sl-chip .amt{font-weight:700;font-variant-numeric:tabular-nums;flex:none;white-space:nowrap}
|
|
599
|
-
.sl-chip
|
|
600
|
-
.sl-chip .rm
|
|
853
|
+
.sl-chip-rail{display:flex;flex-direction:column;border-left:1px solid var(--sl-line)}
|
|
854
|
+
.sl-chip .rm,.sl-chip .view{flex:1;min-height:26px;border-radius:0;display:flex;align-items:center;justify-content:center;
|
|
855
|
+
color:var(--sl-muted);transition:color .15s,background .15s}
|
|
856
|
+
.sl-chip .view{border-top:1px solid var(--sl-line)}
|
|
857
|
+
.sl-chip .rm:hover,.sl-chip .rm:focus-visible{color:#e5484d;background:color-mix(in srgb,#e5484d 9%,transparent)}
|
|
858
|
+
.sl-chip .view:hover,.sl-chip .view:focus-visible{color:var(--sl-text);background:color-mix(in srgb,var(--sl-accent) 10%,transparent)}
|
|
601
859
|
.sl-chip .rm svg{width:11px;height:11px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round}
|
|
860
|
+
.sl-chip .view svg{width:13px;height:13px;stroke:currentColor;stroke-width:1.8;fill:none}
|
|
861
|
+
/* live-activity strip \u2014 narrates WS availability deltas (social proof + urgency) */
|
|
862
|
+
.sl-live{display:flex;align-items:center;gap:7px;margin:10px 14px 0;padding:7px 9px;flex:none;
|
|
863
|
+
border:1px solid var(--sl-line);border-radius:8px;background:color-mix(in srgb,var(--sl-accent) 4%,var(--sl-surface));
|
|
864
|
+
font-size:11px;color:var(--sl-muted)}
|
|
865
|
+
.sl-live .dot{width:6px;height:6px;border-radius:999px;background:#22a06b;box-shadow:0 0 6px rgba(34,160,107,.75);flex:none}
|
|
866
|
+
.sl-live span:last-child{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
602
867
|
|
|
603
868
|
/* GA rows */
|
|
604
869
|
.sl-ga{display:flex;align-items:center;gap:10px;padding:9px 11px;border:1px dashed var(--sl-line);border-radius:var(--sl-r-sm)}
|
|
@@ -674,6 +939,8 @@ var CSS = `
|
|
|
674
939
|
|
|
675
940
|
/* zoom column (flows within the bottom-right region) */
|
|
676
941
|
.sl-zoom{display:flex;flex-direction:column;gap:6px}
|
|
942
|
+
/* CSS-fallback full screen (iOS Safari has no element fullscreen API) */
|
|
943
|
+
.sl-picker.sl-fs{position:fixed;inset:0;z-index:2147483000;width:auto;height:auto;max-height:none;border-radius:0}
|
|
677
944
|
.sl-zoom button{width:36px;height:36px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
678
945
|
color:var(--sl-text);font-size:17px;font-weight:700;display:flex;align-items:center;justify-content:center;transition:border-color .15s}
|
|
679
946
|
.sl-zoom button:hover{border-color:var(--sl-muted)}
|
|
@@ -732,6 +999,39 @@ var CSS = `
|
|
|
732
999
|
.sl-booked.on .sl-booked-title{animation-delay:.22s}
|
|
733
1000
|
.sl-booked.on .sl-booked-sub{animation-delay:.3s}
|
|
734
1001
|
|
|
1002
|
+
/* sold-out overlay \u2014 every SEATED category's live availability is 0. Centered
|
|
1003
|
+
over the map; a stub (disabled) "Join waitlist" button, exactly like the page.
|
|
1004
|
+
Suppressed when GA areas exist (GA capacity isn't seat-counted). Clears live
|
|
1005
|
+
the moment WS frees a seat up. */
|
|
1006
|
+
.sl-soldout{position:absolute;inset:0;z-index:10;display:none;flex-direction:column;align-items:center;
|
|
1007
|
+
justify-content:center;text-align:center;gap:8px;padding:24px;
|
|
1008
|
+
background:color-mix(in srgb,var(--sl-bg) 82%,transparent);backdrop-filter:blur(4px)}
|
|
1009
|
+
.sl-soldout.on{display:flex}
|
|
1010
|
+
.sl-soldout-eyebrow{font-size:10px;letter-spacing:.2em;text-transform:uppercase;color:var(--sl-accent);font-weight:800}
|
|
1011
|
+
.sl-soldout-title{font-size:32px;font-weight:800;color:var(--sl-text);line-height:1.05}
|
|
1012
|
+
.sl-soldout-copy{max-width:360px;font-size:13px;color:var(--sl-muted);line-height:1.5}
|
|
1013
|
+
.sl-picker .sl-soldout-btn{margin-top:10px;min-height:40px;padding:10px 18px;border-radius:var(--sl-r-sm);
|
|
1014
|
+
background:var(--sl-surface);color:var(--sl-muted);border:1px solid var(--sl-line);font-weight:800;font-size:13px;
|
|
1015
|
+
cursor:not-allowed;opacity:.85}
|
|
1016
|
+
|
|
1017
|
+
/* sales-closed pill (header) \u2014 persistent read-only state when the event's sales
|
|
1018
|
+
window is closed at load or closes live mid-session. Neutral (not accent) so it
|
|
1019
|
+
reads as "unavailable", distinct from the accent hold pill next to it. */
|
|
1020
|
+
.sl-closed-pill{display:none;align-items:center;gap:6px;padding:6px 12px;border-radius:999px;flex:none;
|
|
1021
|
+
background:color-mix(in srgb,var(--sl-text) 12%,var(--sl-surface));color:var(--sl-text);
|
|
1022
|
+
font-weight:700;font-size:12px;white-space:nowrap}
|
|
1023
|
+
.sl-closed-pill.on{display:inline-flex}
|
|
1024
|
+
.sl-closed-pill svg{width:13px;height:13px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
1025
|
+
|
|
1026
|
+
/* "Powered by SeatLayer" attribution badge (side-panel foot) \u2014 the small gold
|
|
1027
|
+
rounded logo mark + wordmark. Hidden when the host opts out or the org's paid
|
|
1028
|
+
theme sets hideBadge. */
|
|
1029
|
+
.sl-powered{display:flex;align-items:center;justify-content:center;gap:6px;margin-top:10px;
|
|
1030
|
+
font-size:11px;letter-spacing:.03em;color:var(--sl-muted)}
|
|
1031
|
+
.sl-powered-mark{width:16px;height:16px;border-radius:4px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
1032
|
+
background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
1033
|
+
.sl-powered-mark svg{width:11px;height:11px;fill:currentColor}
|
|
1034
|
+
|
|
735
1035
|
/* a11y filter chips (flow within the top-left region) */
|
|
736
1036
|
.sl-chips{display:flex;gap:6px;flex-wrap:wrap}
|
|
737
1037
|
.sl-chip-f{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:999px;font-size:12px;font-weight:700;
|
|
@@ -770,8 +1070,30 @@ var CSS = `
|
|
|
770
1070
|
.sl-picker[data-layout="narrow"] .sl-confirm{left:50%!important;top:auto!important;bottom:14px;width:min(342px,calc(100% - 24px));
|
|
771
1071
|
transform:translateX(-50%);animation:slConfirmMobileIn .24s cubic-bezier(.2,.8,.2,1) both}
|
|
772
1072
|
|
|
1073
|
+
/* hover preview \u2014 a COMPACT echo of the confirm card (deliberately smaller: it's
|
|
1074
|
+
a passing preview on hover, not the click/select action surface). Reuses the
|
|
1075
|
+
Section\xB7Row\xB7Seat identity grid so hover, confirm and the cart chip all share
|
|
1076
|
+
one visual language, just at three sizes. */
|
|
1077
|
+
.sl-tip{position:absolute;z-index:7;pointer-events:none;display:none;width:190px;overflow:hidden;
|
|
1078
|
+
background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:11px;
|
|
1079
|
+
box-shadow:0 12px 30px -14px rgba(0,0,0,.6)}
|
|
1080
|
+
.sl-tip-grid{display:grid;grid-template-columns:1.3fr .85fr .85fr;border-bottom:1px solid var(--sl-line)}
|
|
1081
|
+
.sl-tip-grid.one{grid-template-columns:1fr}
|
|
1082
|
+
.sl-tip-field{min-width:0;padding:6px 9px;border-right:1px solid var(--sl-line)}
|
|
1083
|
+
.sl-tip-field:last-child{border-right:0;text-align:center}
|
|
1084
|
+
.sl-tip-grid:not(.one) .sl-tip-field:nth-child(2){text-align:center}
|
|
1085
|
+
.sl-tip-key{display:block;font-size:7.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
|
|
1086
|
+
.sl-tip-val{display:block;margin-top:2px;color:var(--sl-text);font-size:13px;line-height:1.1;font-weight:750;
|
|
1087
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1088
|
+
.sl-tip-cat{display:flex;align-items:center;gap:7px;padding:6px 10px;font-size:11px;
|
|
1089
|
+
background:color-mix(in srgb,var(--sl-cat) 12%,var(--sl-surface))}
|
|
1090
|
+
.sl-tip-dot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
1091
|
+
.sl-tip-name{color:var(--sl-muted);flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
1092
|
+
.sl-tip-amt{margin-left:auto;font-weight:800;color:var(--sl-text);font-variant-numeric:tabular-nums;font-size:12px}
|
|
1093
|
+
.sl-tip-status{padding:5px 10px 7px;font-size:8.5px;letter-spacing:.09em;text-transform:uppercase;font-weight:700;color:var(--sl-muted)}
|
|
1094
|
+
|
|
773
1095
|
/* Best available is a first-class shortcut, not an anonymous utility row. */
|
|
774
|
-
.sl-ba{position:relative;overflow:hidden;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:7px;
|
|
1096
|
+
.sl-ba{position:relative;flex:none;overflow:hidden;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:7px;
|
|
775
1097
|
padding:13px;border:1px solid color-mix(in srgb,var(--sl-accent) 34%,var(--sl-line));border-radius:13px;
|
|
776
1098
|
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)))}
|
|
777
1099
|
.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}
|
|
@@ -807,12 +1129,6 @@ var CSS = `
|
|
|
807
1129
|
/* per-seat ticket-tier select + view-from-seat button in tray chips */
|
|
808
1130
|
.sl-chip .tier{background:var(--sl-bg);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:6px;
|
|
809
1131
|
font:inherit;font-size:10px;padding:2px 4px;min-width:0;max-width:100%;cursor:pointer}
|
|
810
|
-
.sl-chip .view{width:20px;height:20px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
811
|
-
color:var(--sl-muted);opacity:.36;transition:color .15s,opacity .15s}
|
|
812
|
-
.sl-chip .view:hover{color:var(--sl-text)}
|
|
813
|
-
.sl-chip:hover .view,.sl-chip .view:focus-visible{opacity:1;color:var(--sl-text)}
|
|
814
|
-
.sl-chip .view svg{width:12px;height:12px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
815
|
-
@media(pointer:coarse){.sl-chip .view{opacity:.58}}
|
|
816
1132
|
|
|
817
1133
|
/* arena: LOD rung pills (flow within the top-center region) */
|
|
818
1134
|
.sl-rungs{display:none;background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:999px;padding:3px}
|
|
@@ -870,6 +1186,14 @@ var CSS = `
|
|
|
870
1186
|
.sl-seccard-hint{font-size:10.5px;color:var(--sl-muted)}
|
|
871
1187
|
|
|
872
1188
|
/* view-from-seat button on the confirm popover */
|
|
1189
|
+
/* Eager sightline preview inside the confirm card */
|
|
1190
|
+
.sl-confirm-thumbwrap{position:relative;display:block;width:100%;height:74px;margin:0 0 8px;padding:0!important;
|
|
1191
|
+
border-radius:9px;overflow:hidden;border:1px solid var(--sl-line);cursor:pointer}
|
|
1192
|
+
.sl-confirm-thumb{display:block;width:100%;height:100%;object-fit:cover}
|
|
1193
|
+
.sl-confirm-thumb-badge{position:absolute;right:7px;top:7px;display:inline-flex;align-items:center;gap:5px;
|
|
1194
|
+
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)}
|
|
1195
|
+
.sl-confirm-sight{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--sl-muted);margin-bottom:2px}
|
|
1196
|
+
.sl-confirm-sight span{color:#22a06b;font-weight:800}
|
|
873
1197
|
.sl-confirm-view{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
|
|
874
1198
|
color:var(--sl-text);font-weight:700;font-size:12px;display:flex;align-items:center;justify-content:center;gap:7px}
|
|
875
1199
|
.sl-confirm-view:hover{border-color:var(--sl-muted)}
|
|
@@ -953,6 +1277,22 @@ function resolveTokens(chart, host) {
|
|
|
953
1277
|
"--sl-radius": `${host?.radius ?? 14}px`
|
|
954
1278
|
};
|
|
955
1279
|
}
|
|
1280
|
+
var CB_STORAGE_KEY = "seatmap.a11y.cb";
|
|
1281
|
+
function readStoredColorblind() {
|
|
1282
|
+
try {
|
|
1283
|
+
if (typeof window === "undefined") return null;
|
|
1284
|
+
const raw = window.localStorage.getItem(CB_STORAGE_KEY);
|
|
1285
|
+
return raw == null ? null : raw === "1";
|
|
1286
|
+
} catch {
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function writeStoredColorblind(on) {
|
|
1291
|
+
try {
|
|
1292
|
+
window.localStorage.setItem(CB_STORAGE_KEY, on ? "1" : "0");
|
|
1293
|
+
} catch {
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
956
1296
|
var SeatPicker = class _SeatPicker {
|
|
957
1297
|
constructor(options) {
|
|
958
1298
|
this.root = null;
|
|
@@ -985,11 +1325,17 @@ var SeatPicker = class _SeatPicker {
|
|
|
985
1325
|
this.confirmEl = null;
|
|
986
1326
|
this.confirmSeat = null;
|
|
987
1327
|
this.srEl = null;
|
|
988
|
-
this.a11yFilter = "all";
|
|
989
1328
|
this.baQty = 2;
|
|
990
1329
|
this.baCat = "";
|
|
991
1330
|
this.bestAvailableConfirm = false;
|
|
992
1331
|
this.releasingHold = false;
|
|
1332
|
+
/** Event sales window is closed (read-only load state / live close). */
|
|
1333
|
+
this.salesClosed = false;
|
|
1334
|
+
/** Every seated category's live availability is 0 (sold-out overlay is up). */
|
|
1335
|
+
this.soldOut = false;
|
|
1336
|
+
this.soldoutEl = null;
|
|
1337
|
+
/** Resolved colorblind-safe state — stored preference wins over the option. */
|
|
1338
|
+
this.cbSafe = false;
|
|
993
1339
|
// arena / multi-floor / seat-view chrome
|
|
994
1340
|
this.rungsEl = null;
|
|
995
1341
|
this.floorsEl = null;
|
|
@@ -998,13 +1344,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
998
1344
|
this.viewCleanup = null;
|
|
999
1345
|
this.allSeatsCache = null;
|
|
1000
1346
|
// F3 minimap
|
|
1001
|
-
this.miniEl = null;
|
|
1002
1347
|
this.miniCanvas = null;
|
|
1003
1348
|
this.miniBase = null;
|
|
1004
1349
|
this.miniTf = null;
|
|
1005
1350
|
// F4 price-band filter — active band's category keys (null = all prices)
|
|
1006
1351
|
this.priceBandKeys = null;
|
|
1007
|
-
this.priceFilterEl = null;
|
|
1008
1352
|
/** Last surfaced section summary (re-rendered when the price band changes). */
|
|
1009
1353
|
this.lastSection = null;
|
|
1010
1354
|
/** Section card collapsed to its slim pill (seat-picking has begun). */
|
|
@@ -1024,6 +1368,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
1024
1368
|
this.ctaPhase = "idle";
|
|
1025
1369
|
// narrow-layout chrome that docks into the sheet's Filters row on mobile
|
|
1026
1370
|
this.a11yChipsEl = null;
|
|
1371
|
+
this.fsFallback = false;
|
|
1372
|
+
this.fsChangeHandler = null;
|
|
1373
|
+
this.fsEscHandler = null;
|
|
1027
1374
|
this.cbEl = null;
|
|
1028
1375
|
// modal plumbing (set by open())
|
|
1029
1376
|
this.modalScrim = null;
|
|
@@ -1031,20 +1378,22 @@ var SeatPicker = class _SeatPicker {
|
|
|
1031
1378
|
this.escHandler = null;
|
|
1032
1379
|
/** Set by open(): closes the modal (scroll restore + destroy + onClose). */
|
|
1033
1380
|
this.closeModal = null;
|
|
1381
|
+
this.lastCatAvail = null;
|
|
1034
1382
|
if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
|
|
1035
1383
|
if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
|
|
1036
1384
|
if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
|
|
1037
1385
|
this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
|
|
1038
1386
|
this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
|
|
1039
|
-
this.api = new PubApi(this.apiBase);
|
|
1387
|
+
this.api = options.transport ?? new PubApi(this.apiBase);
|
|
1040
1388
|
this.maxTickets = Math.max(1, Math.floor(options.maxSelection ?? DEFAULT_MAX_SELECTION2));
|
|
1389
|
+
this.cbSafe = readStoredColorblind() ?? !!options.colorblindSafe;
|
|
1041
1390
|
this.controller = new PickerController2({
|
|
1042
1391
|
transport: this.api,
|
|
1043
1392
|
eventKey: options.event,
|
|
1044
1393
|
maxSelection: this.maxTickets,
|
|
1045
1394
|
currency: options.currency,
|
|
1046
1395
|
flashOnLiveChange: true,
|
|
1047
|
-
colorblindSafe:
|
|
1396
|
+
colorblindSafe: this.cbSafe,
|
|
1048
1397
|
onSelectionChange: () => {
|
|
1049
1398
|
this.syncTray();
|
|
1050
1399
|
if (this.committedSelection().length) this.collapseSectionCard();
|
|
@@ -1070,6 +1419,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1070
1419
|
},
|
|
1071
1420
|
confirmSelection: this.opts.confirmSelection,
|
|
1072
1421
|
onSelect: (seat) => {
|
|
1422
|
+
if (this.salesClosed) {
|
|
1423
|
+
this.controller.deselect([seat.id]);
|
|
1424
|
+
this.toast(this.tf("picker.salesClosedToast", "Sales are closed for this event."), "warning");
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1073
1427
|
this.flashPickedSeat(seat.id);
|
|
1074
1428
|
if (this.opts.confirmSelection) this.showConfirm(seat);
|
|
1075
1429
|
},
|
|
@@ -1092,9 +1446,68 @@ var SeatPicker = class _SeatPicker {
|
|
|
1092
1446
|
onHint: (m) => {
|
|
1093
1447
|
if (m) this.toast(m);
|
|
1094
1448
|
},
|
|
1449
|
+
// Server declared the event closed mid-session (409 event_closed) — keep
|
|
1450
|
+
// the toast (raised by handleCta), and add the persistent read-only state.
|
|
1451
|
+
onSalesClosed: () => this.setSalesClosed(true),
|
|
1095
1452
|
onError: (err) => this.opts.onError?.(err)
|
|
1096
1453
|
});
|
|
1097
1454
|
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Eager sightline preview for the confirm card: a cheap generated forward
|
|
1457
|
+
* view (or the organizer's real photo) plus a "Nm to stage · clear
|
|
1458
|
+
* sightline" line — the premium at-a-glance moment; click opens the 360.
|
|
1459
|
+
*/
|
|
1460
|
+
confirmThumbHtml(seat) {
|
|
1461
|
+
const doc = this.controller.doc;
|
|
1462
|
+
if (!doc) return "";
|
|
1463
|
+
let url = seat.viewUrl ?? "";
|
|
1464
|
+
let distance = null;
|
|
1465
|
+
if (!url) {
|
|
1466
|
+
try {
|
|
1467
|
+
const thumb = generateSeatThumb(seat, doc.focalPoint);
|
|
1468
|
+
url = thumb.url;
|
|
1469
|
+
distance = thumb.distanceM ?? null;
|
|
1470
|
+
} catch {
|
|
1471
|
+
return "";
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
const sight = distance != null ? `${distance}${this.tf("picker.sightline", "m to stage \xB7 clear sightline")}` : this.tf("picker.sightlineClear", "Clear sightline");
|
|
1475
|
+
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
|
+
}
|
|
1477
|
+
/** Full screen via the native API, falling back to a fixed-position overlay (iOS Safari). */
|
|
1478
|
+
toggleFullscreen() {
|
|
1479
|
+
const root = this.root;
|
|
1480
|
+
if (!root) return;
|
|
1481
|
+
const active = !!document.fullscreenElement || this.fsFallback;
|
|
1482
|
+
if (!active) {
|
|
1483
|
+
if (root.requestFullscreen) {
|
|
1484
|
+
root.requestFullscreen().catch(() => this.setFsFallback(true));
|
|
1485
|
+
} else {
|
|
1486
|
+
this.setFsFallback(true);
|
|
1487
|
+
}
|
|
1488
|
+
} else if (document.fullscreenElement) {
|
|
1489
|
+
void document.exitFullscreen().catch(() => {
|
|
1490
|
+
});
|
|
1491
|
+
} else {
|
|
1492
|
+
this.setFsFallback(false);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
setFsFallback(on) {
|
|
1496
|
+
if (this.fsFallback === on) return;
|
|
1497
|
+
this.fsFallback = on;
|
|
1498
|
+
this.root?.classList.toggle("sl-fs", on);
|
|
1499
|
+
this.els.zfs?.setAttribute("aria-pressed", String(on || !!document.fullscreenElement));
|
|
1500
|
+
if (on && !this.fsEscHandler) {
|
|
1501
|
+
this.fsEscHandler = (e) => {
|
|
1502
|
+
if (e.key === "Escape" && !document.fullscreenElement) this.setFsFallback(false);
|
|
1503
|
+
};
|
|
1504
|
+
window.addEventListener("keydown", this.fsEscHandler);
|
|
1505
|
+
} else if (!on && this.fsEscHandler) {
|
|
1506
|
+
window.removeEventListener("keydown", this.fsEscHandler);
|
|
1507
|
+
this.fsEscHandler = null;
|
|
1508
|
+
}
|
|
1509
|
+
requestAnimationFrame(() => this.controller.zoomToFit());
|
|
1510
|
+
}
|
|
1098
1511
|
/**
|
|
1099
1512
|
* Close the picker. In modal mode (SeatPicker.open()) this dismisses the
|
|
1100
1513
|
* modal exactly like ESC/scrim/✕ — restores page scroll and fires onClose.
|
|
@@ -1189,6 +1602,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
1189
1602
|
<div class="sl-head-meta" data-ref="meta"></div>
|
|
1190
1603
|
</div>
|
|
1191
1604
|
<span class="sl-hold-pill" data-ref="hold"></span>
|
|
1605
|
+
<span class="sl-closed-pill" data-ref="closedPill" role="status">
|
|
1606
|
+
<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>
|
|
1607
|
+
<span data-ref="closedPillText"></span>
|
|
1608
|
+
</span>
|
|
1192
1609
|
<button type="button" class="sl-close" data-ref="close" aria-label="Close">
|
|
1193
1610
|
<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>
|
|
1194
1611
|
</button>
|
|
@@ -1202,6 +1619,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
1202
1619
|
<button type="button" aria-label="Fit to screen" data-ref="zfit">
|
|
1203
1620
|
<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>
|
|
1204
1621
|
</button>
|
|
1622
|
+
<button type="button" aria-label="Full screen" aria-pressed="false" data-ref="zfs">
|
|
1623
|
+
<svg viewBox="0 0 24 24"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
|
|
1624
|
+
</button>
|
|
1205
1625
|
</div>
|
|
1206
1626
|
<div class="sl-boot" data-ref="boot"><span class="sl-boot-spin"></span>Loading seat map\u2026</div>
|
|
1207
1627
|
<div class="sl-toast" data-ref="toast" role="status" aria-live="polite"></div>
|
|
@@ -1220,6 +1640,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1220
1640
|
<div class="sl-filters" data-ref="filters"></div>
|
|
1221
1641
|
<div class="sl-sec sl-prices-sec" data-ref="pricesSec"><span>Ticket prices</span></div>
|
|
1222
1642
|
<div class="sl-prices" data-ref="prices"></div>
|
|
1643
|
+
<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>
|
|
1223
1644
|
<div class="sl-sec sl-seats-sec"><span>Your seats</span><span class="sl-seat-summary" data-ref="seatSummary"></span></div>
|
|
1224
1645
|
<div class="sl-tray" data-ref="tray"></div>
|
|
1225
1646
|
<div class="sl-foot" data-ref="foot">
|
|
@@ -1253,6 +1674,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
1253
1674
|
this.els.zin.addEventListener("click", () => this.controller.zoomIn());
|
|
1254
1675
|
this.els.zout.addEventListener("click", () => this.controller.zoomOut());
|
|
1255
1676
|
this.els.zfit.addEventListener("click", () => this.controller.zoomToFit());
|
|
1677
|
+
this.els.zfs.addEventListener("click", () => this.toggleFullscreen());
|
|
1678
|
+
this.fsChangeHandler = () => {
|
|
1679
|
+
if (!document.fullscreenElement) this.setFsFallback(false);
|
|
1680
|
+
this.els.zfs?.setAttribute("aria-pressed", String(!!document.fullscreenElement || this.fsFallback));
|
|
1681
|
+
requestAnimationFrame(() => this.controller.zoomToFit());
|
|
1682
|
+
};
|
|
1683
|
+
document.addEventListener("fullscreenchange", this.fsChangeHandler);
|
|
1256
1684
|
const head = this.els.sheetHead;
|
|
1257
1685
|
if (head) {
|
|
1258
1686
|
const toggle = this.els.sheetToggle;
|
|
@@ -1296,7 +1724,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1296
1724
|
}
|
|
1297
1725
|
this.tipEl = document.createElement("div");
|
|
1298
1726
|
this.tipEl.setAttribute("role", "tooltip");
|
|
1299
|
-
this.tipEl.
|
|
1727
|
+
this.tipEl.className = "sl-tip";
|
|
1300
1728
|
this.els.map.appendChild(this.tipEl);
|
|
1301
1729
|
this.els.map.addEventListener("mousemove", (e) => {
|
|
1302
1730
|
const r = this.els.map.getBoundingClientRect();
|
|
@@ -1321,6 +1749,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1321
1749
|
return this;
|
|
1322
1750
|
}
|
|
1323
1751
|
this.els.boot.remove();
|
|
1752
|
+
this.salesClosed = !!info.salesClosed;
|
|
1324
1753
|
this.buildRegions();
|
|
1325
1754
|
this.regions["bottom-right"].appendChild(this.els.zoom);
|
|
1326
1755
|
this.regions["bottom-center"].appendChild(this.els.toast);
|
|
@@ -1340,6 +1769,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1340
1769
|
this.els.name.textContent = info.eventName ?? "";
|
|
1341
1770
|
const when = info.startsAt ? new Date(info.startsAt).toLocaleString(this.opts.locale, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "";
|
|
1342
1771
|
this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
|
|
1772
|
+
this.buildBadge(chartTheme);
|
|
1343
1773
|
const present = /* @__PURE__ */ new Set();
|
|
1344
1774
|
if (this.controller.doc) {
|
|
1345
1775
|
for (const seat of expandChart(this.controller.doc)) {
|
|
@@ -1355,12 +1785,29 @@ var SeatPicker = class _SeatPicker {
|
|
|
1355
1785
|
chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
|
|
1356
1786
|
this.regions["top-left"].appendChild(chips);
|
|
1357
1787
|
this.a11yChipsEl = chips;
|
|
1788
|
+
const active = /* @__PURE__ */ new Set();
|
|
1789
|
+
const syncChips = () => {
|
|
1790
|
+
chips.querySelectorAll("button").forEach((b) => {
|
|
1791
|
+
const f = b.dataset.f;
|
|
1792
|
+
const on = f === "all" ? active.size === 0 : active.has(f);
|
|
1793
|
+
b.classList.toggle("on", on);
|
|
1794
|
+
b.setAttribute("aria-pressed", String(on));
|
|
1795
|
+
});
|
|
1796
|
+
const filter = active.size ? [...active] : null;
|
|
1797
|
+
this.controller.setAccessibilityFilter(filter);
|
|
1798
|
+
if (filter && this.rungsEl && this.controller.getRung() !== "seats") {
|
|
1799
|
+
this.controller.setRung("seats");
|
|
1800
|
+
this.collapseSectionCard();
|
|
1801
|
+
this.syncRung();
|
|
1802
|
+
}
|
|
1803
|
+
};
|
|
1358
1804
|
chips.querySelectorAll("button").forEach((btn) => {
|
|
1359
1805
|
btn.addEventListener("click", () => {
|
|
1360
1806
|
const f = btn.dataset.f;
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1807
|
+
if (f === "all") active.clear();
|
|
1808
|
+
else if (active.has(f)) active.delete(f);
|
|
1809
|
+
else active.add(f);
|
|
1810
|
+
syncChips();
|
|
1364
1811
|
});
|
|
1365
1812
|
});
|
|
1366
1813
|
}
|
|
@@ -1369,14 +1816,14 @@ var SeatPicker = class _SeatPicker {
|
|
|
1369
1816
|
cb.className = "sl-cbbtn";
|
|
1370
1817
|
this.cbEl = cb;
|
|
1371
1818
|
cb.setAttribute("aria-label", "Toggle colorblind-friendly colors");
|
|
1372
|
-
cb.setAttribute("aria-pressed", String(
|
|
1819
|
+
cb.setAttribute("aria-pressed", String(this.cbSafe));
|
|
1373
1820
|
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>';
|
|
1374
1821
|
this.els.zfit.parentElement.appendChild(cb);
|
|
1375
|
-
let cbOn = !!this.opts.colorblindSafe;
|
|
1376
1822
|
cb.addEventListener("click", () => {
|
|
1377
|
-
|
|
1378
|
-
cb.setAttribute("aria-pressed", String(
|
|
1379
|
-
this.controller.setColorblindSafe(
|
|
1823
|
+
this.cbSafe = !this.cbSafe;
|
|
1824
|
+
cb.setAttribute("aria-pressed", String(this.cbSafe));
|
|
1825
|
+
this.controller.setColorblindSafe(this.cbSafe);
|
|
1826
|
+
writeStoredColorblind(this.cbSafe);
|
|
1380
1827
|
});
|
|
1381
1828
|
this.srEl = document.createElement("div");
|
|
1382
1829
|
this.srEl.className = "sl-sr";
|
|
@@ -1387,9 +1834,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1387
1834
|
this.buildPriceFilter();
|
|
1388
1835
|
this.buildExtendPrompt();
|
|
1389
1836
|
this.buildBookedOverlay();
|
|
1837
|
+
this.buildSoldoutOverlay();
|
|
1390
1838
|
this.dockLayoutChrome();
|
|
1391
1839
|
await this.restoreRememberedHold();
|
|
1392
1840
|
if (this.destroyed) return this;
|
|
1841
|
+
if (this.salesClosed) this.applySalesClosed();
|
|
1393
1842
|
this.syncPrices();
|
|
1394
1843
|
this.syncTray();
|
|
1395
1844
|
return this;
|
|
@@ -1441,6 +1890,85 @@ var SeatPicker = class _SeatPicker {
|
|
|
1441
1890
|
this.bookedEl = el;
|
|
1442
1891
|
this.els.bookedSub = el.querySelector('[data-ref="bookedSub"]');
|
|
1443
1892
|
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Localized string with a literal fallback. `t()` returns the key itself for
|
|
1895
|
+
* unknown keys, so this collapses that to `fallback` — while still honoring a
|
|
1896
|
+
* host `messages` override (which makes `t()` return the override, not the key).
|
|
1897
|
+
*/
|
|
1898
|
+
tf(key, fallback) {
|
|
1899
|
+
const v = t2(key);
|
|
1900
|
+
return v === key ? fallback : v;
|
|
1901
|
+
}
|
|
1902
|
+
/** Sold-out overlay — centered over the map, disabled waitlist stub (Gap 2). */
|
|
1903
|
+
buildSoldoutOverlay() {
|
|
1904
|
+
if (!this.els.map) return;
|
|
1905
|
+
const el = document.createElement("div");
|
|
1906
|
+
el.className = "sl-soldout";
|
|
1907
|
+
el.setAttribute("role", "status");
|
|
1908
|
+
const name = (this.controller.doc?.theme?.brandName ?? this.opts.theme?.brandName ?? this.els.name?.textContent ?? this.tf("picker.soldOutEyebrow", "This event")).toUpperCase();
|
|
1909
|
+
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>`;
|
|
1910
|
+
this.els.map.appendChild(el);
|
|
1911
|
+
this.soldoutEl = el;
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* Recompute the sold-out state on every price/availability sync. Sold-out ⇔
|
|
1915
|
+
* every SEATED category's live free count is 0. Suppressed when the chart has
|
|
1916
|
+
* GA areas (GA capacity isn't per-seat, so seated counts would read 0 and
|
|
1917
|
+
* falsely block standing room) — mirrors the public page. Clears live when WS
|
|
1918
|
+
* frees a seat up.
|
|
1919
|
+
*/
|
|
1920
|
+
syncSoldout(categories, left) {
|
|
1921
|
+
const hasGA = this.controller.getGAAreas().length > 0;
|
|
1922
|
+
const soldOut = this.isSoldOut(categories, left, hasGA);
|
|
1923
|
+
if (soldOut === this.soldOut) return;
|
|
1924
|
+
this.soldOut = soldOut;
|
|
1925
|
+
this.soldoutEl?.classList.toggle("on", soldOut);
|
|
1926
|
+
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Pure sold-out predicate: every SEATED category's free count is 0, there is at
|
|
1929
|
+
* least one seated category, and there are no GA areas (GA capacity isn't
|
|
1930
|
+
* per-seat, so seated counts read 0 and would falsely block standing room).
|
|
1931
|
+
* `left` is seeded implicitly — a missing key means a fully-booked tier (0 free).
|
|
1932
|
+
*/
|
|
1933
|
+
isSoldOut(categories, left, hasGA) {
|
|
1934
|
+
return !hasGA && categories.length > 0 && categories.every((c) => (left[c.key] ?? 0) === 0);
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Sales-closed read-only state (Gap 3): persistent header pill, disabled CTA
|
|
1938
|
+
* with a closed label, and frozen best-available / GA controls. `setSalesClosed`
|
|
1939
|
+
* is the reactive entry (live 409 event_closed); `applySalesClosed` is the
|
|
1940
|
+
* idempotent DOM apply used at load and on transition.
|
|
1941
|
+
*/
|
|
1942
|
+
setSalesClosed(closed) {
|
|
1943
|
+
if (this.salesClosed === closed) return;
|
|
1944
|
+
this.salesClosed = closed;
|
|
1945
|
+
this.applySalesClosed();
|
|
1946
|
+
}
|
|
1947
|
+
applySalesClosed() {
|
|
1948
|
+
const pill = this.els.closedPill;
|
|
1949
|
+
if (pill) {
|
|
1950
|
+
pill.classList.toggle("on", this.salesClosed);
|
|
1951
|
+
const text = this.els.closedPillText ?? pill;
|
|
1952
|
+
text.textContent = this.tf("picker.salesClosedPill", "Sales are closed");
|
|
1953
|
+
}
|
|
1954
|
+
this.root?.setAttribute("data-sales-closed", String(this.salesClosed));
|
|
1955
|
+
this.syncCta();
|
|
1956
|
+
this.syncTray();
|
|
1957
|
+
}
|
|
1958
|
+
/** The badge is hidden when the host opts out OR the org's theme sets hideBadge. */
|
|
1959
|
+
badgeHidden(chartTheme) {
|
|
1960
|
+
return !!(this.opts.hideBadge || chartTheme?.hideBadge);
|
|
1961
|
+
}
|
|
1962
|
+
/** Attribution badge in the side-panel foot (Gap 7). Hidden per host/theme. */
|
|
1963
|
+
buildBadge(chartTheme) {
|
|
1964
|
+
if (this.badgeHidden(chartTheme)) return;
|
|
1965
|
+
const foot = this.els.foot;
|
|
1966
|
+
if (!foot) return;
|
|
1967
|
+
const el = document.createElement("div");
|
|
1968
|
+
el.className = "sl-powered";
|
|
1969
|
+
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>`;
|
|
1970
|
+
foot.appendChild(el);
|
|
1971
|
+
}
|
|
1444
1972
|
// ---- Feature 6: chrome anchor regions -------------------------------------
|
|
1445
1973
|
/**
|
|
1446
1974
|
* Create the positioned flex containers that own every persistent map overlay.
|
|
@@ -1553,13 +2081,18 @@ var SeatPicker = class _SeatPicker {
|
|
|
1553
2081
|
heldGA.set(item.objectId, (heldGA.get(item.objectId) ?? 0) + (item.quantity ?? 1));
|
|
1554
2082
|
}
|
|
1555
2083
|
return gaAreas.reduce(
|
|
1556
|
-
(sum, area) => sum + area.price * Math.max(0, (this.gaQty.get(area.id) ?? 0) - (heldGA.get(area.id) ?? 0)),
|
|
2084
|
+
(sum, area) => sum + this.paidPrice(area.categoryKey, null, area.price) * Math.max(0, (this.gaQty.get(area.id) ?? 0) - (heldGA.get(area.id) ?? 0)),
|
|
1557
2085
|
0
|
|
1558
2086
|
);
|
|
1559
2087
|
}
|
|
1560
2088
|
syncCta(count = this.lastTrayCount, pending = this.pendingSelectionCount()) {
|
|
1561
2089
|
const cta = this.els.cta;
|
|
1562
2090
|
if (!cta) return;
|
|
2091
|
+
if (this.salesClosed) {
|
|
2092
|
+
cta.disabled = true;
|
|
2093
|
+
cta.textContent = this.tf("picker.salesClosedCta", "Sales closed");
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
1563
2096
|
if (this.confirmSeat) {
|
|
1564
2097
|
cta.disabled = true;
|
|
1565
2098
|
cta.textContent = "Confirm or cancel this seat";
|
|
@@ -1695,7 +2228,6 @@ var SeatPicker = class _SeatPicker {
|
|
|
1695
2228
|
canvas.style.height = `${h}px`;
|
|
1696
2229
|
wrap.appendChild(canvas);
|
|
1697
2230
|
(this.regions["bottom-left"] ?? this.els.map).appendChild(wrap);
|
|
1698
|
-
this.miniEl = wrap;
|
|
1699
2231
|
this.miniCanvas = canvas;
|
|
1700
2232
|
const scale = Math.min((w - PAD * 2) / Math.max(1, b.width), (h - PAD * 2) / Math.max(1, b.height)) * dpr;
|
|
1701
2233
|
const offX = (w * dpr - b.width * scale) / 2 - b.x * scale;
|
|
@@ -1808,9 +2340,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
1808
2340
|
this.controller.overview();
|
|
1809
2341
|
}
|
|
1810
2342
|
// ---- F4 price-band filter -------------------------------------------------
|
|
1811
|
-
/** Effective price of a category
|
|
2343
|
+
/** Effective display price of a category: host pricing override → first tier → base. */
|
|
1812
2344
|
catPrice(c) {
|
|
1813
|
-
|
|
2345
|
+
const chart = c.tiers?.length ? c.tiers[0].price : c.price;
|
|
2346
|
+
if (chart === void 0 || !c.key) return chart;
|
|
2347
|
+
return this.paidPrice(c.key, c.tiers?.[0]?.id ?? null, chart);
|
|
1814
2348
|
}
|
|
1815
2349
|
/** Derive price bands: one chip per distinct price (≤5), else quantile ranges. */
|
|
1816
2350
|
priceBands() {
|
|
@@ -1855,7 +2389,6 @@ var SeatPicker = class _SeatPicker {
|
|
|
1855
2389
|
select.setAttribute("aria-label", "Filter and focus seats by price");
|
|
1856
2390
|
select.innerHTML = `<option value="all">All prices</option>` + bands.map((band) => `<option value="${band.id}">${band.label}</option>`).join("");
|
|
1857
2391
|
this.els.pricesSec.appendChild(select);
|
|
1858
|
-
this.priceFilterEl = select;
|
|
1859
2392
|
select.addEventListener("change", () => {
|
|
1860
2393
|
const band = bands.find((candidate) => candidate.id === select.value);
|
|
1861
2394
|
const keys = band?.keys ?? null;
|
|
@@ -1959,7 +2492,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
1959
2492
|
renderSectionCard(summary) {
|
|
1960
2493
|
if (!this.els.map) return;
|
|
1961
2494
|
this.secCardEl?.remove();
|
|
1962
|
-
const
|
|
2495
|
+
const paid = summary.categories.length ? summary.categories.map((c) => this.paidPrice(c.key, null, c.price)) : [summary.priceMin, summary.priceMax];
|
|
2496
|
+
const paidMin = Math.min(...paid);
|
|
2497
|
+
const paidMax = Math.max(...paid);
|
|
2498
|
+
const priceLabel = paidMin === paidMax ? this.money(paidMin) : `${this.money(paidMin)}\u2013${this.money(paidMax)}`;
|
|
1963
2499
|
const leftLabel = tCount("picker.seatsLeftInSection", summary.seatsLeft);
|
|
1964
2500
|
const xBtn = `<button type="button" class="sl-seccard-x" aria-label="${t2("picker.closeSectionSummary")}">\u2715</button>`;
|
|
1965
2501
|
const card = document.createElement("div");
|
|
@@ -1989,7 +2525,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1989
2525
|
card.setAttribute("aria-label", t2("picker.sectionSummaryAria", { label: summary.label }));
|
|
1990
2526
|
const mix = summary.categories.map((c) => {
|
|
1991
2527
|
const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
|
|
1992
|
-
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>`;
|
|
2528
|
+
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>`;
|
|
1993
2529
|
}).join("");
|
|
1994
2530
|
card.innerHTML = `<div class="sl-seccard-head"><span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span>` + (summary.categories.length ? `<span class="sl-seccard-price">${priceLabel}</span>` : "") + xBtn + `</div><div class="sl-seccard-zone">${summary.zoneLabel ? `${summary.zoneLabel} \xB7 ` : ""}<span class="sl-seccard-left">${leftLabel}</span></div>` + (mix ? `<div class="sl-seccard-mix">${mix}</div>` : "") + `<div class="sl-seccard-foot"><button type="button" class="sl-seccard-overview">\u2190 ${t2("picker.overview")}</button><span class="sl-seccard-hint">${t2("picker.tapSeatHint")}</span></div>`;
|
|
1995
2531
|
card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
|
|
@@ -2056,7 +2592,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2056
2592
|
const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
|
|
2057
2593
|
const status = this.controller.getStatus(seat.id) ?? "free";
|
|
2058
2594
|
const statusText = status === "free" ? "available" : status === "held" ? "on hold" : "taken";
|
|
2059
|
-
const price = cat
|
|
2595
|
+
const price = cat ? this.catPrice(cat) : void 0;
|
|
2060
2596
|
this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
|
|
2061
2597
|
}
|
|
2062
2598
|
// ---- seat candidate confirmation ------------------------------------------
|
|
@@ -2071,7 +2607,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
2071
2607
|
if (this.tipEl) this.tipEl.style.display = "none";
|
|
2072
2608
|
const details = this.controller.seatDetails(seat.id);
|
|
2073
2609
|
const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
|
|
2074
|
-
const
|
|
2610
|
+
const chartPrice = details?.price ?? (cat?.tiers?.length ? cat.tiers[0].price : cat?.price);
|
|
2611
|
+
const price = chartPrice != null ? this.paidPrice(seat.categoryKey, details?.tierId ?? cat?.tiers?.[0]?.id ?? null, chartPrice) : void 0;
|
|
2075
2612
|
const safe = (value) => String(value ?? "\u2014").replace(/[&<>"]/g, (char) => ({
|
|
2076
2613
|
"&": "&",
|
|
2077
2614
|
"<": "<",
|
|
@@ -2084,7 +2621,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2084
2621
|
el.setAttribute("aria-modal", "true");
|
|
2085
2622
|
el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
|
|
2086
2623
|
el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
|
|
2087
|
-
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
|
|
2624
|
+
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>`;
|
|
2088
2625
|
this.els.map.appendChild(el);
|
|
2089
2626
|
this.confirmEl = el;
|
|
2090
2627
|
this.reanchorConfirm();
|
|
@@ -2249,18 +2786,35 @@ var SeatPicker = class _SeatPicker {
|
|
|
2249
2786
|
}
|
|
2250
2787
|
// ---- chrome sync ----------------------------------------------------------
|
|
2251
2788
|
money(n) {
|
|
2789
|
+
const formatter = this.opts.pricing?.formatter;
|
|
2790
|
+
if (formatter) return formatter(n, this.currency);
|
|
2252
2791
|
try {
|
|
2253
2792
|
return new Intl.NumberFormat(this.opts.locale, { style: "currency", currency: this.currency }).format(n);
|
|
2254
2793
|
} catch {
|
|
2255
2794
|
return `${n} ${this.currency}`;
|
|
2256
2795
|
}
|
|
2257
2796
|
}
|
|
2797
|
+
/**
|
|
2798
|
+
* The price the buyer will actually pay for a category (+tier): the host's
|
|
2799
|
+
* `pricing` override when present, else the chart's stored price. Every
|
|
2800
|
+
* price the widget DISPLAYS or hands off must flow through here — a map
|
|
2801
|
+
* that shows one price while checkout charges another destroys trust.
|
|
2802
|
+
*/
|
|
2803
|
+
paidPrice(categoryKey, tierId, fallback) {
|
|
2804
|
+
const entry = categoryKey ? this.opts.pricing?.prices?.[categoryKey] : void 0;
|
|
2805
|
+
if (entry === void 0) return fallback;
|
|
2806
|
+
if (typeof entry === "number") return entry;
|
|
2807
|
+
if (tierId && entry.tiers?.[tierId] !== void 0) return entry.tiers[tierId];
|
|
2808
|
+
return entry.base ?? fallback;
|
|
2809
|
+
}
|
|
2258
2810
|
syncPrices() {
|
|
2259
2811
|
const doc = this.controller.doc;
|
|
2260
2812
|
if (!doc || !this.els.prices) return;
|
|
2261
2813
|
const left = this.controller.categoryAvailability();
|
|
2814
|
+
this.narrateAvailability(doc.categories, left);
|
|
2815
|
+
this.syncSoldout(doc.categories, left);
|
|
2262
2816
|
this.els.prices.innerHTML = doc.categories.map((c) => {
|
|
2263
|
-
const price =
|
|
2817
|
+
const price = this.catPrice(c);
|
|
2264
2818
|
const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
|
|
2265
2819
|
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>`;
|
|
2266
2820
|
}).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>`;
|
|
@@ -2269,6 +2823,26 @@ var SeatPicker = class _SeatPicker {
|
|
|
2269
2823
|
row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
|
|
2270
2824
|
});
|
|
2271
2825
|
}
|
|
2826
|
+
/**
|
|
2827
|
+
* Live-activity strip: turn WS availability deltas into one quiet line of
|
|
2828
|
+
* social proof ("2 seats just taken in VIP · 118 left"). Diffs per-category
|
|
2829
|
+
* counts on every status change — no per-seat payload needed. Skips the very
|
|
2830
|
+
* first computation (initial load is not "activity").
|
|
2831
|
+
*/
|
|
2832
|
+
narrateAvailability(categories, left) {
|
|
2833
|
+
const textEl = this.els.liveText;
|
|
2834
|
+
const prev = this.lastCatAvail;
|
|
2835
|
+
this.lastCatAvail = { ...left };
|
|
2836
|
+
if (!textEl || !prev) return;
|
|
2837
|
+
for (const cat of categories) {
|
|
2838
|
+
const before = prev[cat.key];
|
|
2839
|
+
const now = left[cat.key] ?? 0;
|
|
2840
|
+
if (before === void 0 || now >= before) continue;
|
|
2841
|
+
const taken = before - now;
|
|
2842
|
+
textEl.textContent = `${taken} seat${taken === 1 ? "" : "s"} just taken in ${cat.label} \xB7 ${now} left`;
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2272
2846
|
/** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
|
|
2273
2847
|
evictTakenSelections() {
|
|
2274
2848
|
const ownLabels = /* @__PURE__ */ new Set([
|
|
@@ -2297,15 +2871,23 @@ var SeatPicker = class _SeatPicker {
|
|
|
2297
2871
|
const cats = this.controller.doc?.categories ?? [];
|
|
2298
2872
|
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>`);
|
|
2299
2873
|
}
|
|
2874
|
+
const idGrid = (seatId, label) => {
|
|
2875
|
+
const d = seatId ? this.controller.seatDetails(seatId) : null;
|
|
2876
|
+
if (!d?.sectionLabel && !d?.rowLabel && !d?.seatNumber) {
|
|
2877
|
+
return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">Seat</span><span class="val">${label}</span></span></div>`;
|
|
2878
|
+
}
|
|
2879
|
+
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>`;
|
|
2880
|
+
};
|
|
2881
|
+
const iconRail = (rmAria, viewLabel) => `<div class="sl-chip-rail"><button type="button" class="rm" aria-label="${rmAria}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>` + (viewLabel ? `<button type="button" class="view" data-view-label="${viewLabel}" aria-label="${t2("picker.viewFromSeat", { label: viewLabel })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "") + `</div>`;
|
|
2300
2882
|
for (const item of heldItems) {
|
|
2301
2883
|
const itemKey = `held:${item.label}`;
|
|
2302
2884
|
nextTrayKeys.add(itemKey);
|
|
2303
2885
|
const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
|
|
2304
2886
|
const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
|
|
2305
|
-
const
|
|
2306
|
-
const
|
|
2887
|
+
const heldSeat = item.objectType !== "ga" ? this.controller.seatByLabel(item.label) : null;
|
|
2888
|
+
const canView2 = this.seatViewEnabled() && !!heldSeat;
|
|
2307
2889
|
parts.push(
|
|
2308
|
-
`<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><
|
|
2890
|
+
`<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>`
|
|
2309
2891
|
);
|
|
2310
2892
|
}
|
|
2311
2893
|
const heldLabels = new Set(heldItems.map((item) => item.label));
|
|
@@ -2314,16 +2896,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
2314
2896
|
const itemKey = `seat:${s.id}`;
|
|
2315
2897
|
nextTrayKeys.add(itemKey);
|
|
2316
2898
|
const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
|
|
2317
|
-
const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${t2("picker.ticketTierFor", { label: s.label })}">` + s.tiers.map((ti) => `<option value="${ti.id}"${ti.id === s.tierId ? " selected" : ""}>${ti.name} \xB7 ${this.money(ti.price)}</option>`).join("") + `</select>` : "";
|
|
2318
|
-
const viewBtn = canView ? `<button type="button" class="view" data-view-label="${s.label}" aria-label="${t2("picker.viewFromSeat", { label: s.label })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "";
|
|
2899
|
+
const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${t2("picker.ticketTierFor", { label: s.label })}">` + s.tiers.map((ti) => `<option value="${ti.id}"${ti.id === s.tierId ? " selected" : ""}>${ti.name} \xB7 ${this.money(this.paidPrice(s.categoryKey, ti.id, ti.price))}</option>`).join("") + `</select>` : "";
|
|
2319
2900
|
parts.push(
|
|
2320
|
-
`<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><
|
|
2901
|
+
`<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>`
|
|
2321
2902
|
);
|
|
2322
2903
|
}
|
|
2323
2904
|
for (const area of gaAreas) {
|
|
2324
2905
|
const qty = this.gaQty.get(area.id) ?? 0;
|
|
2325
2906
|
parts.push(
|
|
2326
|
-
`<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>`
|
|
2907
|
+
`<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>`
|
|
2327
2908
|
);
|
|
2328
2909
|
}
|
|
2329
2910
|
this.els.tray.innerHTML = parts.join("");
|
|
@@ -2363,7 +2944,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2363
2944
|
return;
|
|
2364
2945
|
}
|
|
2365
2946
|
const id = chip.dataset.seat;
|
|
2366
|
-
const label =
|
|
2947
|
+
const label = this.controller.getSelection().find((sel) => sel.id === id)?.label ?? "Seat";
|
|
2367
2948
|
const remove = () => {
|
|
2368
2949
|
this.controller.deselect([id]);
|
|
2369
2950
|
this.toast(`${label} removed.`, "neutral", {
|
|
@@ -2394,6 +2975,11 @@ var SeatPicker = class _SeatPicker {
|
|
|
2394
2975
|
if (seat) this.openSeatView(seat);
|
|
2395
2976
|
});
|
|
2396
2977
|
});
|
|
2978
|
+
this.els.tray.querySelectorAll(".sl-chip[data-locate]").forEach((chip) => {
|
|
2979
|
+
const locate = () => this.controller.flashSeat(chip.dataset.locate, this.cssVar("--sl-accent") || "#f4b740");
|
|
2980
|
+
chip.addEventListener("mouseenter", locate);
|
|
2981
|
+
chip.addEventListener("focusin", locate);
|
|
2982
|
+
});
|
|
2397
2983
|
this.els.tray.querySelectorAll(".sl-ga button").forEach((btn) => {
|
|
2398
2984
|
btn.addEventListener("click", () => {
|
|
2399
2985
|
const areaEl = btn.closest(".sl-ga");
|
|
@@ -2406,12 +2992,17 @@ var SeatPicker = class _SeatPicker {
|
|
|
2406
2992
|
this.syncTray();
|
|
2407
2993
|
});
|
|
2408
2994
|
});
|
|
2995
|
+
if (this.salesClosed) {
|
|
2996
|
+
this.els.tray.querySelectorAll(".sl-ba-go,[data-ba],[data-ba-cat],[data-ba-replace],.sl-ga button").forEach((el) => {
|
|
2997
|
+
el.disabled = true;
|
|
2998
|
+
});
|
|
2999
|
+
}
|
|
2409
3000
|
const gaTotal = this.pendingGATotal(gaAreas);
|
|
2410
3001
|
const gaCount = this.pendingGACount();
|
|
2411
|
-
const heldTotal = heldItems.reduce((sum, item) => sum + item.unitPrice * (item.quantity ?? 1), 0);
|
|
3002
|
+
const heldTotal = heldItems.reduce((sum, item) => sum + this.paidPrice(item.categoryKey, item.tierId, item.unitPrice) * (item.quantity ?? 1), 0);
|
|
2412
3003
|
const heldCount = heldItems.reduce((sum, item) => sum + (item.quantity ?? 1), 0);
|
|
2413
3004
|
const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
|
|
2414
|
-
const total = freshSeats.reduce((sum, s) => sum + s.price, 0) + gaTotal + heldTotal;
|
|
3005
|
+
const total = freshSeats.reduce((sum, s) => sum + this.paidPrice(s.categoryKey, s.tierId ?? null, s.price), 0) + gaTotal + heldTotal;
|
|
2415
3006
|
const count = freshSeats.length + gaCount + heldCount;
|
|
2416
3007
|
const pendingCount = this.pendingSelectionCount();
|
|
2417
3008
|
const previousCount = this.lastTrayCount;
|
|
@@ -2507,6 +3098,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2507
3098
|
}
|
|
2508
3099
|
}
|
|
2509
3100
|
async handleCta() {
|
|
3101
|
+
if (this.salesClosed) return;
|
|
2510
3102
|
if (this.totalTicketCount() > this.maxTickets) {
|
|
2511
3103
|
this.toast(`Remove tickets until your order has ${this.maxTickets} or fewer.`, "warning");
|
|
2512
3104
|
return;
|
|
@@ -2550,6 +3142,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2550
3142
|
this.opts.onError?.(err);
|
|
2551
3143
|
const problem = err;
|
|
2552
3144
|
const labels = (problem.conflicts ?? []).map((conflict) => conflict.label).filter(Boolean).slice(0, 3);
|
|
3145
|
+
if (problem.reason === "event_closed") this.setSalesClosed(true);
|
|
2553
3146
|
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.";
|
|
2554
3147
|
this.toast(message, "error");
|
|
2555
3148
|
this.setCtaPhase("idle");
|
|
@@ -2658,7 +3251,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2658
3251
|
objectType: it.objectType,
|
|
2659
3252
|
categoryKey: it.categoryKey,
|
|
2660
3253
|
tierId: it.tierId,
|
|
2661
|
-
unitPrice: it.unitPrice,
|
|
3254
|
+
unitPrice: this.paidPrice(it.categoryKey, it.tierId, it.unitPrice),
|
|
2662
3255
|
currency: it.currency ?? this.currency,
|
|
2663
3256
|
quantity: it.quantity ?? 1
|
|
2664
3257
|
}));
|
|
@@ -2711,19 +3304,37 @@ var SeatPicker = class _SeatPicker {
|
|
|
2711
3304
|
this.tipEl.style.left = `${Math.max(8, x)}px`;
|
|
2712
3305
|
this.tipEl.style.top = `${Math.max(8, y)}px`;
|
|
2713
3306
|
}
|
|
3307
|
+
/**
|
|
3308
|
+
* Row label without the redundant section prefix. Charts commonly name row
|
|
3309
|
+
* objects "104-A" while the Section column already shows "104" — so the Row
|
|
3310
|
+
* cell repeats the section and, in the compact hover card, truncates to
|
|
3311
|
+
* "10…". Strip a leading "<section><sep>" so Row reads a clean "A". Only when
|
|
3312
|
+
* the prefix is exact (won't touch "1040-A" under section "104"); otherwise
|
|
3313
|
+
* the label is shown verbatim.
|
|
3314
|
+
*/
|
|
3315
|
+
rowShort(details) {
|
|
3316
|
+
const row = details?.rowLabel;
|
|
3317
|
+
const sec = details?.sectionLabel;
|
|
3318
|
+
if (!row || !sec) return row;
|
|
3319
|
+
for (const sep of ["-", " ", "\xB7", "/", "_"]) {
|
|
3320
|
+
const prefix = `${sec}${sep}`;
|
|
3321
|
+
if (row.startsWith(prefix) && row.length > prefix.length) return row.slice(prefix.length);
|
|
3322
|
+
}
|
|
3323
|
+
return row;
|
|
3324
|
+
}
|
|
2714
3325
|
updateTooltip(details) {
|
|
2715
3326
|
if (!this.tipEl) return;
|
|
2716
3327
|
if (!details) {
|
|
2717
3328
|
this.tipEl.style.display = "none";
|
|
2718
3329
|
return;
|
|
2719
3330
|
}
|
|
2720
|
-
const
|
|
2721
|
-
const
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
this.tipEl.innerHTML = `<div
|
|
3331
|
+
const esc2 = (v) => String(v ?? "\u2014").replace(/[&<>"]/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[ch]);
|
|
3332
|
+
const price = this.money(this.paidPrice(details.categoryKey, details.tierId ?? null, details.price));
|
|
3333
|
+
const hasLoc = details.sectionLabel || details.rowLabel || details.seatNumber;
|
|
3334
|
+
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>`;
|
|
3335
|
+
const statusLine = details.status === "free" ? "" : `<div class="sl-tip-status">${details.status === "held" ? t2("map.statusHeld") : t2("map.statusTaken")}</div>`;
|
|
3336
|
+
this.tipEl.style.setProperty("--sl-cat", details.categoryColor);
|
|
3337
|
+
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;
|
|
2727
3338
|
this.tipEl.style.display = "block";
|
|
2728
3339
|
this.placeTooltip();
|
|
2729
3340
|
}
|
|
@@ -2744,7 +3355,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2744
3355
|
return this.removeHeldLabel(label);
|
|
2745
3356
|
}
|
|
2746
3357
|
async bestAvailable(qty, categoryKey) {
|
|
2747
|
-
if (this.bestAvailableBusy) return null;
|
|
3358
|
+
if (this.salesClosed || this.bestAvailableBusy) return null;
|
|
2748
3359
|
qty = Math.max(1, Math.min(this.maxTickets, Math.floor(qty)));
|
|
2749
3360
|
if (this.confirmSeat) this.cancelConfirm();
|
|
2750
3361
|
this.bestAvailableConfirm = false;
|
|
@@ -2825,6 +3436,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
2825
3436
|
this.ro?.disconnect();
|
|
2826
3437
|
this.ro = null;
|
|
2827
3438
|
if (this.escHandler) document.removeEventListener("keydown", this.escHandler);
|
|
3439
|
+
if (this.fsChangeHandler) document.removeEventListener("fullscreenchange", this.fsChangeHandler);
|
|
3440
|
+
if (this.fsEscHandler) window.removeEventListener("keydown", this.fsEscHandler);
|
|
2828
3441
|
this.controller.destroy();
|
|
2829
3442
|
this.root?.remove();
|
|
2830
3443
|
this.root = null;
|