@seatlayer/js 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -3
- package/dist/index.cjs +362 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +139 -11
- package/dist/index.d.ts +139 -11
- package/dist/index.js +362 -59
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -205,8 +205,28 @@ var SeatingChart = class {
|
|
|
205
205
|
ribbon.style.cssText = "position:absolute;top:18px;right:-34px;z-index:6;transform:rotate(45deg);width:140px;text-align:center;padding:4px 0;background:#f4b740;color:#1a1200;font:800 10.5px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;letter-spacing:.12em;box-shadow:0 2px 8px rgba(0,0,0,.25);pointer-events:none;";
|
|
206
206
|
host.appendChild(ribbon);
|
|
207
207
|
}
|
|
208
|
+
this.buildBadge(host);
|
|
208
209
|
return this;
|
|
209
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Attribution badge pinned to the embed's bottom-right, linking to
|
|
213
|
+
* seatlayer.io. Rendered as an absolutely-positioned overlay with
|
|
214
|
+
* self-contained inline styles — the SDK embed ships no widget CSS, and an
|
|
215
|
+
* overlay keeps it out of the layout flow so it never disturbs the SDK v0.22
|
|
216
|
+
* fill-height resize contract. Mirrors the full widget's mark + wordmark and
|
|
217
|
+
* reuses the `picker.poweredBy` i18n string.
|
|
218
|
+
*/
|
|
219
|
+
buildBadge(host) {
|
|
220
|
+
if (this.controller.doc?.theme?.hideBadge) return;
|
|
221
|
+
const badge = document.createElement("a");
|
|
222
|
+
badge.href = "https://seatlayer.io";
|
|
223
|
+
badge.target = "_blank";
|
|
224
|
+
badge.rel = "noopener noreferrer";
|
|
225
|
+
badge.setAttribute("aria-label", (0, import_core.t)("picker.poweredBy"));
|
|
226
|
+
badge.style.cssText = 'position:absolute;bottom:10px;right:12px;z-index:5;display:inline-flex;align-items:center;gap:6px;padding:5px 9px;border-radius:999px;background:rgba(255,255,255,.92);color:#4a5163;text-decoration:none;font:600 11px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;letter-spacing:.02em;box-shadow:0 2px 8px rgba(0,0,0,.12);';
|
|
227
|
+
badge.innerHTML = `<span aria-hidden="true" style="width:16px;height:16px;border-radius:4px;flex:none;display:flex;align-items:center;justify-content:center;background:#f4b740;color:#1a1200"><svg viewBox="0 0 24 24" style="width:11px;height:11px;fill:currentColor"><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>${(0, import_core.t)("picker.poweredBy")}</span>`;
|
|
228
|
+
host.appendChild(badge);
|
|
229
|
+
}
|
|
210
230
|
placeTooltip() {
|
|
211
231
|
if (!this.tipEl || !this.hostEl) return;
|
|
212
232
|
const hw = this.hostEl.clientWidth;
|
|
@@ -361,6 +381,13 @@ var TYPES = /* @__PURE__ */ new Set([
|
|
|
361
381
|
]);
|
|
362
382
|
var DEFAULT_LOADING_TIMEOUT_MS = 2e4;
|
|
363
383
|
var DEFAULT_MIN_FILL_HEIGHT = 480;
|
|
384
|
+
var RENEW_LEAD_MS = 3 * 60 * 1e3;
|
|
385
|
+
var RENEW_SHORT_TTL_MS = 15 * 60 * 1e3;
|
|
386
|
+
var RENEW_SHORT_TTL_FRACTION = 0.8;
|
|
387
|
+
var RENEW_MIN_DELAY_MS = 30 * 1e3;
|
|
388
|
+
var FILL_PROBE_HEIGHT_PX = 1e5;
|
|
389
|
+
var FILL_PROBE_TRACK_EPSILON_PX = 4;
|
|
390
|
+
var FILL_MIN_DEFINITE_HEIGHT_PX = 50;
|
|
364
391
|
function resolveContainer2(container) {
|
|
365
392
|
if (typeof container !== "string") return container;
|
|
366
393
|
const element = document.querySelector(container);
|
|
@@ -398,6 +425,14 @@ var EmbeddedDesigner = class {
|
|
|
398
425
|
this.designerOrigin = "";
|
|
399
426
|
this.overlay = null;
|
|
400
427
|
this.timeoutTimer = null;
|
|
428
|
+
/** Proactive session-renewal timer; armed from each `ready`, cleared on re-mount. */
|
|
429
|
+
this.renewTimer = null;
|
|
430
|
+
/**
|
|
431
|
+
* One automatic recovery relaunch is allowed per expiry. Reset ONLY when a fresh
|
|
432
|
+
* `ready` arrives — deliberately not on re-mount — so a session that keeps failing
|
|
433
|
+
* to load can't loop the host through endless silent relaunches.
|
|
434
|
+
*/
|
|
435
|
+
this.autoRecoverUsed = false;
|
|
401
436
|
this.phase = "loading";
|
|
402
437
|
this.restoreContainerPosition = null;
|
|
403
438
|
// Host-side fullscreen pin: saved state we restore on `off`/Escape/destroy.
|
|
@@ -408,10 +443,17 @@ var EmbeddedDesigner = class {
|
|
|
408
443
|
this.fsKeyHandler = null;
|
|
409
444
|
/** Latest height (px string) the Designer reported; re-applied after unpin. */
|
|
410
445
|
this.lastAutoHeight = "";
|
|
411
|
-
//
|
|
446
|
+
// Fill sizing: pending rAF handles + whether window listeners are attached.
|
|
412
447
|
this.fillRaf = null;
|
|
448
|
+
this.reprobeRaf = null;
|
|
413
449
|
this.fillListening = false;
|
|
414
|
-
/**
|
|
450
|
+
/** Resolved container element (fill measurement + ResizeObserver target). */
|
|
451
|
+
this.containerEl = null;
|
|
452
|
+
/** Cached fill verdict: 'container' = bounded block, 'viewport' = full page. */
|
|
453
|
+
this.fillMode = null;
|
|
454
|
+
/** Live block-size tracking in container-fill mode; disconnected on destroy. */
|
|
455
|
+
this.resizeObs = null;
|
|
456
|
+
/** rAF-throttled fill recompute, so a burst of scroll/RO ticks coalesces. */
|
|
415
457
|
this.scheduleFill = () => {
|
|
416
458
|
if (this.fillRaf !== null) return;
|
|
417
459
|
this.fillRaf = requestAnimationFrame(() => {
|
|
@@ -419,6 +461,21 @@ var EmbeddedDesigner = class {
|
|
|
419
461
|
this.applyFill();
|
|
420
462
|
});
|
|
421
463
|
};
|
|
464
|
+
/**
|
|
465
|
+
* rAF-throttled re-probe: a host layout change (responsive breakpoint, a block
|
|
466
|
+
* gaining/losing a definite height) can flip the verdict, so `resize` /
|
|
467
|
+
* `orientationchange` re-detect and swap the container observer accordingly.
|
|
468
|
+
*/
|
|
469
|
+
this.scheduleReprobe = () => {
|
|
470
|
+
if (this.reprobeRaf !== null) return;
|
|
471
|
+
this.reprobeRaf = requestAnimationFrame(() => {
|
|
472
|
+
this.reprobeRaf = null;
|
|
473
|
+
if (this.pinned) return;
|
|
474
|
+
this.fillMode = this.detectFillMode();
|
|
475
|
+
this.syncContainerObserver();
|
|
476
|
+
this.applyFill();
|
|
477
|
+
});
|
|
478
|
+
};
|
|
422
479
|
this.handleMessage = (event) => {
|
|
423
480
|
if (!this.frame || event.origin !== this.designerOrigin || event.source !== this.frame.contentWindow) return;
|
|
424
481
|
if (!event.data || typeof event.data !== "object") return;
|
|
@@ -426,7 +483,7 @@ var EmbeddedDesigner = class {
|
|
|
426
483
|
if (data.type === "seatlayer.designer.resize") {
|
|
427
484
|
if (!this.fillEnabled() && this.autoResizeEnabled() && typeof data.px === "number" && Number.isFinite(data.px) && data.px > 0) {
|
|
428
485
|
this.lastAutoHeight = `${Math.round(data.px)}px`;
|
|
429
|
-
if (!this.pinned) this.
|
|
486
|
+
if (!this.pinned) this.setFrameHeight(this.lastAutoHeight);
|
|
430
487
|
}
|
|
431
488
|
return;
|
|
432
489
|
}
|
|
@@ -454,6 +511,8 @@ var EmbeddedDesigner = class {
|
|
|
454
511
|
this.phase = "ready";
|
|
455
512
|
this.clearTimeoutTimer();
|
|
456
513
|
this.removeOverlay();
|
|
514
|
+
this.autoRecoverUsed = false;
|
|
515
|
+
this.scheduleRenewal(message.expiresAt);
|
|
457
516
|
this.options.onReady?.(message);
|
|
458
517
|
break;
|
|
459
518
|
case "seatlayer.designer.saved":
|
|
@@ -465,10 +524,18 @@ var EmbeddedDesigner = class {
|
|
|
465
524
|
case "seatlayer.designer.close":
|
|
466
525
|
this.options.onClose?.(message);
|
|
467
526
|
break;
|
|
468
|
-
case "seatlayer.designer.error":
|
|
469
|
-
|
|
527
|
+
case "seatlayer.designer.error": {
|
|
528
|
+
const cause = causeFromCode(message.code);
|
|
529
|
+
if (cause === "expired" && this.autoRenewEnabled() && !this.autoRecoverUsed) {
|
|
530
|
+
this.autoRecoverUsed = true;
|
|
531
|
+
this.clearRenewTimer();
|
|
532
|
+
this.options.onRequestRelaunch();
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
this.showError(cause);
|
|
470
536
|
this.options.onError?.(message);
|
|
471
537
|
break;
|
|
538
|
+
}
|
|
472
539
|
}
|
|
473
540
|
};
|
|
474
541
|
this.options = options;
|
|
@@ -485,12 +552,17 @@ var EmbeddedDesigner = class {
|
|
|
485
552
|
frame.allow = this.options.allow ?? "fullscreen; clipboard-write";
|
|
486
553
|
frame.referrerPolicy = this.options.referrerPolicy ?? "origin";
|
|
487
554
|
frame.src = url.toString();
|
|
488
|
-
frame.style.width
|
|
489
|
-
frame.style.
|
|
555
|
+
frame.style.setProperty("width", "100%", "important");
|
|
556
|
+
frame.style.setProperty(
|
|
557
|
+
"height",
|
|
558
|
+
typeof this.options.height === "number" ? `${this.options.height}px` : "100%",
|
|
559
|
+
"important"
|
|
560
|
+
);
|
|
490
561
|
frame.style.border = "0";
|
|
491
562
|
Object.assign(frame.style, this.options.style);
|
|
492
563
|
if (this.options.className) frame.className = this.options.className;
|
|
493
564
|
const container = resolveContainer2(this.options.container);
|
|
565
|
+
this.containerEl = container;
|
|
494
566
|
window.addEventListener("message", this.handleMessage);
|
|
495
567
|
container.append(frame);
|
|
496
568
|
this.frame = frame;
|
|
@@ -521,10 +593,13 @@ var EmbeddedDesigner = class {
|
|
|
521
593
|
this.stopFill();
|
|
522
594
|
this.unpinFullscreen();
|
|
523
595
|
this.clearTimeoutTimer();
|
|
596
|
+
this.clearRenewTimer();
|
|
524
597
|
this.removeOverlay();
|
|
525
598
|
this.restoreContainerStyle();
|
|
526
599
|
this.frame?.remove();
|
|
527
600
|
this.frame = null;
|
|
601
|
+
this.containerEl = null;
|
|
602
|
+
this.fillMode = null;
|
|
528
603
|
this.designerOrigin = "";
|
|
529
604
|
this.phase = "loading";
|
|
530
605
|
this.lastAutoHeight = "";
|
|
@@ -539,24 +614,77 @@ var EmbeddedDesigner = class {
|
|
|
539
614
|
fillEnabled() {
|
|
540
615
|
return typeof this.options.height !== "number";
|
|
541
616
|
}
|
|
617
|
+
/** Write an SDK-managed height with `!important` so a host theme can't win. */
|
|
618
|
+
setFrameHeight(value) {
|
|
619
|
+
this.frame?.style.setProperty("height", value, "important");
|
|
620
|
+
}
|
|
542
621
|
/**
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
622
|
+
* Decide whether the host gave the container a DEFINITE (bounded) height — a
|
|
623
|
+
* fixed block the embed should fill 100% of — versus a content-sized container
|
|
624
|
+
* that collapses to whatever the iframe measures (full-page usage).
|
|
625
|
+
*
|
|
626
|
+
* We drive the iframe to two extreme heights within a single synchronous task
|
|
627
|
+
* and watch whether the container follows: a bounded box barely moves, a
|
|
628
|
+
* content-sized one grows with the iframe. Because we restore the height before
|
|
629
|
+
* yielding, the browser only lays out — it never paints the extremes, so there
|
|
630
|
+
* is no visible flash. Works for px, resolved `%`, and flex (`flex:1;min-h:0`)
|
|
631
|
+
* heights, and leaves a mere `min-height` floor classified as content-sized so
|
|
632
|
+
* full-page hosts keep the old viewport-fill behavior.
|
|
633
|
+
*/
|
|
634
|
+
detectFillMode() {
|
|
635
|
+
const container = this.containerEl;
|
|
636
|
+
const frame = this.frame;
|
|
637
|
+
if (this.pinned || !container || !frame) return this.fillMode ?? "viewport";
|
|
638
|
+
const measure = () => container.getBoundingClientRect().height;
|
|
639
|
+
const savedValue = frame.style.getPropertyValue("height");
|
|
640
|
+
const savedPriority = frame.style.getPropertyPriority("height");
|
|
641
|
+
frame.style.setProperty("height", "0px", "important");
|
|
642
|
+
const collapsed = measure();
|
|
643
|
+
frame.style.setProperty("height", `${FILL_PROBE_HEIGHT_PX}px`, "important");
|
|
644
|
+
const expanded = measure();
|
|
645
|
+
if (savedValue) frame.style.setProperty("height", savedValue, savedPriority);
|
|
646
|
+
else frame.style.removeProperty("height");
|
|
647
|
+
const tracksIframe = expanded - collapsed > FILL_PROBE_TRACK_EPSILON_PX;
|
|
648
|
+
const bounded = !tracksIframe && collapsed >= FILL_MIN_DEFINITE_HEIGHT_PX;
|
|
649
|
+
return bounded ? "container" : "viewport";
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Size the iframe for the current fill verdict, clamped to `minHeight`. In
|
|
653
|
+
* container mode it fills 100% of the bounded block; in viewport mode its
|
|
654
|
+
* bottom edge meets the bottom of the viewport (`window.innerHeight - top`).
|
|
655
|
+
* No-op while pinned fullscreen (the pin fills the viewport itself).
|
|
546
656
|
*/
|
|
547
657
|
applyFill() {
|
|
548
658
|
if (!this.frame || this.pinned) return;
|
|
549
659
|
const min = this.options.minHeight ?? DEFAULT_MIN_FILL_HEIGHT;
|
|
660
|
+
if (this.fillMode === "container" && this.containerEl) {
|
|
661
|
+
const target2 = Math.max(min, Math.round(this.containerEl.getBoundingClientRect().height));
|
|
662
|
+
this.setFrameHeight(`${target2}px`);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
550
665
|
const top = this.frame.getBoundingClientRect().top;
|
|
551
666
|
const target = Math.max(min, Math.round(window.innerHeight - top));
|
|
552
|
-
this.
|
|
667
|
+
this.setFrameHeight(`${target}px`);
|
|
668
|
+
}
|
|
669
|
+
/** Attach/detach the container ResizeObserver to match the current verdict. */
|
|
670
|
+
syncContainerObserver() {
|
|
671
|
+
const want = this.fillMode === "container" && !!this.containerEl && typeof ResizeObserver !== "undefined";
|
|
672
|
+
if (want && !this.resizeObs) {
|
|
673
|
+
this.resizeObs = new ResizeObserver(() => this.scheduleFill());
|
|
674
|
+
this.resizeObs.observe(this.containerEl);
|
|
675
|
+
} else if (!want && this.resizeObs) {
|
|
676
|
+
this.resizeObs.disconnect();
|
|
677
|
+
this.resizeObs = null;
|
|
678
|
+
}
|
|
553
679
|
}
|
|
554
680
|
startFill() {
|
|
681
|
+
this.fillMode = this.detectFillMode();
|
|
682
|
+
this.syncContainerObserver();
|
|
555
683
|
this.applyFill();
|
|
556
684
|
if (this.fillListening) return;
|
|
557
685
|
this.fillListening = true;
|
|
558
|
-
window.addEventListener("resize", this.
|
|
559
|
-
window.addEventListener("orientationchange", this.
|
|
686
|
+
window.addEventListener("resize", this.scheduleReprobe);
|
|
687
|
+
window.addEventListener("orientationchange", this.scheduleReprobe);
|
|
560
688
|
window.addEventListener("scroll", this.scheduleFill, { passive: true });
|
|
561
689
|
}
|
|
562
690
|
stopFill() {
|
|
@@ -564,10 +692,18 @@ var EmbeddedDesigner = class {
|
|
|
564
692
|
cancelAnimationFrame(this.fillRaf);
|
|
565
693
|
this.fillRaf = null;
|
|
566
694
|
}
|
|
695
|
+
if (this.reprobeRaf !== null) {
|
|
696
|
+
cancelAnimationFrame(this.reprobeRaf);
|
|
697
|
+
this.reprobeRaf = null;
|
|
698
|
+
}
|
|
699
|
+
if (this.resizeObs) {
|
|
700
|
+
this.resizeObs.disconnect();
|
|
701
|
+
this.resizeObs = null;
|
|
702
|
+
}
|
|
567
703
|
if (!this.fillListening) return;
|
|
568
704
|
this.fillListening = false;
|
|
569
|
-
window.removeEventListener("resize", this.
|
|
570
|
-
window.removeEventListener("orientationchange", this.
|
|
705
|
+
window.removeEventListener("resize", this.scheduleReprobe);
|
|
706
|
+
window.removeEventListener("orientationchange", this.scheduleReprobe);
|
|
571
707
|
window.removeEventListener("scroll", this.scheduleFill);
|
|
572
708
|
}
|
|
573
709
|
/**
|
|
@@ -579,16 +715,22 @@ var EmbeddedDesigner = class {
|
|
|
579
715
|
if (this.pinned || !this.frame) return;
|
|
580
716
|
this.pinned = true;
|
|
581
717
|
this.frameStyleBeforeFs = this.frame.getAttribute("style");
|
|
582
|
-
|
|
718
|
+
const pin = {
|
|
583
719
|
position: "fixed",
|
|
584
|
-
|
|
720
|
+
top: "0",
|
|
721
|
+
right: "0",
|
|
722
|
+
bottom: "0",
|
|
723
|
+
left: "0",
|
|
585
724
|
width: "100vw",
|
|
586
725
|
height: "100vh",
|
|
587
726
|
margin: "0",
|
|
588
727
|
border: "0",
|
|
589
|
-
|
|
728
|
+
"z-index": "2147483000",
|
|
590
729
|
background: "#101625"
|
|
591
|
-
}
|
|
730
|
+
};
|
|
731
|
+
for (const [property, value] of Object.entries(pin)) {
|
|
732
|
+
this.frame.style.setProperty(property, value, "important");
|
|
733
|
+
}
|
|
592
734
|
const docEl = document.documentElement;
|
|
593
735
|
this.docOverflowBeforeFs = docEl.style.overflow;
|
|
594
736
|
docEl.style.overflow = "hidden";
|
|
@@ -609,7 +751,7 @@ var EmbeddedDesigner = class {
|
|
|
609
751
|
if (this.frameStyleBeforeFs === null) this.frame.removeAttribute("style");
|
|
610
752
|
else this.frame.setAttribute("style", this.frameStyleBeforeFs);
|
|
611
753
|
if (this.fillEnabled()) this.applyFill();
|
|
612
|
-
else if (this.autoResizeEnabled() && this.lastAutoHeight) this.
|
|
754
|
+
else if (this.autoResizeEnabled() && this.lastAutoHeight) this.setFrameHeight(this.lastAutoHeight);
|
|
613
755
|
}
|
|
614
756
|
this.frameStyleBeforeFs = null;
|
|
615
757
|
if (this.docOverflowBeforeFs !== null) {
|
|
@@ -631,6 +773,48 @@ var EmbeddedDesigner = class {
|
|
|
631
773
|
this.timeoutTimer = null;
|
|
632
774
|
}
|
|
633
775
|
}
|
|
776
|
+
/**
|
|
777
|
+
* Auto-renewal (proactive + one expiry recovery) is on when the host wired a
|
|
778
|
+
* relaunch hook and did not opt out. Without the hook there is nothing to call,
|
|
779
|
+
* so it is a no-op.
|
|
780
|
+
*/
|
|
781
|
+
autoRenewEnabled() {
|
|
782
|
+
return !!this.options.onRequestRelaunch && this.options.autoRenewSession !== false;
|
|
783
|
+
}
|
|
784
|
+
clearRenewTimer() {
|
|
785
|
+
if (this.renewTimer !== null) {
|
|
786
|
+
clearTimeout(this.renewTimer);
|
|
787
|
+
this.renewTimer = null;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Arm the proactive renewal timer from a `ready` message's `expiresAt` (epoch
|
|
792
|
+
* ms). We relaunch a comfortable lead before expiry so the host can mint a fresh
|
|
793
|
+
* session and swap `designerUrl` without the user ever seeing the expiry card:
|
|
794
|
+
*
|
|
795
|
+
* - normal TTL (≥ 15 min): renew {@link RENEW_LEAD_MS} (~3 min) before expiry;
|
|
796
|
+
* - short TTL (< 15 min): renew after {@link RENEW_SHORT_TTL_FRACTION} (80%) of
|
|
797
|
+
* the remaining life, so the lead can't overshoot the whole session;
|
|
798
|
+
* - either way, never sooner than {@link RENEW_MIN_DELAY_MS} (30s) after `ready`
|
|
799
|
+
* so a burst of `ready` messages can't spin the host.
|
|
800
|
+
*
|
|
801
|
+
* Re-armed on every `ready`; cleared on destroy / setDesignerUrl (via re-mount).
|
|
802
|
+
* A no-op when auto-renewal is off or `expiresAt` is missing/already past — the
|
|
803
|
+
* expiry-error path recovers a session that has already lapsed.
|
|
804
|
+
*/
|
|
805
|
+
scheduleRenewal(expiresAt) {
|
|
806
|
+
this.clearRenewTimer();
|
|
807
|
+
if (!this.autoRenewEnabled()) return;
|
|
808
|
+
if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) return;
|
|
809
|
+
const remaining = expiresAt - Date.now();
|
|
810
|
+
if (remaining <= 0) return;
|
|
811
|
+
const lead = remaining < RENEW_SHORT_TTL_MS ? remaining * RENEW_SHORT_TTL_FRACTION : remaining - RENEW_LEAD_MS;
|
|
812
|
+
const delay = Math.max(RENEW_MIN_DELAY_MS, lead);
|
|
813
|
+
this.renewTimer = setTimeout(() => {
|
|
814
|
+
this.renewTimer = null;
|
|
815
|
+
if (this.autoRenewEnabled()) this.options.onRequestRelaunch();
|
|
816
|
+
}, delay);
|
|
817
|
+
}
|
|
634
818
|
ensureContainerPositioned(container) {
|
|
635
819
|
const position = getComputedStyle(container).position;
|
|
636
820
|
if (position === "static") {
|
|
@@ -1278,6 +1462,17 @@ var CSS = `
|
|
|
1278
1462
|
.sl-ba-title .spark{color:var(--sl-accent);font-size:16px}
|
|
1279
1463
|
.sl-ba-copy{grid-column:1/-1;margin:-4px 0 2px 23px;color:var(--sl-muted);font-size:10.5px;line-height:1.35}
|
|
1280
1464
|
.sl-ba-copy .narrow{display:none}
|
|
1465
|
+
/* "\u2605 Best seats" premium quick-pick \u2014 gold accent echoing the \u2605 Premium pill on
|
|
1466
|
+
the confirm popover; deliberately distinct from the accent-toned qty/go. */
|
|
1467
|
+
.sl-ba-premium{position:relative;z-index:1;grid-column:1/-1;justify-self:start;display:inline-flex;align-items:center;gap:6px;
|
|
1468
|
+
padding:6px 12px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.02em;cursor:pointer;
|
|
1469
|
+
color:#c9a24b;background:color-mix(in srgb,#e8c15a 10%,var(--sl-surface));
|
|
1470
|
+
border:1px solid color-mix(in srgb,#e8c15a 34%,var(--sl-line));transition:filter .15s,background .15s,color .15s}
|
|
1471
|
+
.sl-ba-premium .star{font-size:12px;line-height:1;color:#e8c15a}
|
|
1472
|
+
.sl-ba-premium:hover{filter:brightness(1.05)}
|
|
1473
|
+
.sl-ba-premium.on{color:#1c1608;background:linear-gradient(135deg,#f0cf6b,#e0b23f);border-color:transparent;
|
|
1474
|
+
box-shadow:0 6px 16px color-mix(in srgb,#e8c15a 26%,transparent)}
|
|
1475
|
+
.sl-ba-premium.on .star{color:#5a4410}
|
|
1281
1476
|
.sl-ba select{background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:8px;
|
|
1282
1477
|
font:inherit;font-size:11px;padding:7px 8px;min-width:0;width:100%;max-width:none}
|
|
1283
1478
|
.sl-ba-qty{display:flex;align-items:center;gap:7px;padding:3px;border:1px solid var(--sl-line);border-radius:9px;background:var(--sl-surface)}
|
|
@@ -1375,6 +1570,25 @@ var CSS = `
|
|
|
1375
1570
|
.sl-confirm-view:hover{border-color:var(--sl-muted)}
|
|
1376
1571
|
.sl-confirm-view svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
1377
1572
|
|
|
1573
|
+
/* commercial seat flags \u2014 limited-view caution + premium tag. Amber tone,
|
|
1574
|
+
deliberately distinct from the red taken/held state; shown on the confirm
|
|
1575
|
+
card, echoed as a small \u25D0 marker on cart chips and the hover tip. */
|
|
1576
|
+
.sl-cx{display:flex;flex-direction:column;gap:6px;margin-bottom:10px}
|
|
1577
|
+
.sl-cx-warn{display:flex;align-items:flex-start;gap:7px;padding:8px 10px;border-radius:9px;
|
|
1578
|
+
background:color-mix(in srgb,#f4b740 13%,var(--sl-surface));border:1px solid color-mix(in srgb,#f4b740 40%,var(--sl-line));
|
|
1579
|
+
animation:slNoticeIn .28s ease both}
|
|
1580
|
+
.sl-cx-glyph{flex:none;font-size:14px;line-height:1.2;color:#f4b740}
|
|
1581
|
+
.sl-cx-txt{min-width:0;display:flex;flex-direction:column;gap:2px}
|
|
1582
|
+
.sl-cx-txt b{font-size:12px;font-weight:800;color:var(--sl-text)}
|
|
1583
|
+
.sl-cx-note{font-size:11px;line-height:1.4;color:var(--sl-muted)}
|
|
1584
|
+
.sl-cx-premium{display:inline-flex;align-items:center;gap:6px;align-self:flex-start;padding:4px 10px;border-radius:999px;
|
|
1585
|
+
font-size:11px;font-weight:800;letter-spacing:.02em;color:#c9a24b;
|
|
1586
|
+
background:color-mix(in srgb,#e8c15a 13%,var(--sl-surface));border:1px solid color-mix(in srgb,#e8c15a 38%,var(--sl-line))}
|
|
1587
|
+
.sl-cx-star{font-size:12px;line-height:1;color:#e8c15a}
|
|
1588
|
+
.sl-cx-mark{flex:none;font-size:12px;line-height:1;color:#f4b740;cursor:help}
|
|
1589
|
+
.sl-tip-cx{display:flex;align-items:center;gap:6px;padding:5px 10px 7px;font-size:10.5px;font-weight:700;color:#e8b24a}
|
|
1590
|
+
.sl-tip-cx .g{font-size:12px}
|
|
1591
|
+
|
|
1378
1592
|
/* 360\xB0 seat-view modal (fills the widget; drag-to-look-around equirectangular) */
|
|
1379
1593
|
.sl-view{position:absolute;inset:0;z-index:12;display:flex;flex-direction:column;background:var(--sl-bg)}
|
|
1380
1594
|
.sl-view-head{display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid var(--sl-line);flex:none}
|
|
@@ -1503,6 +1717,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
1503
1717
|
this.srEl = null;
|
|
1504
1718
|
this.baQty = 2;
|
|
1505
1719
|
this.baCat = "";
|
|
1720
|
+
/** "★ Best seats" premium quick-pick toggle — biases best-available to premium seats. */
|
|
1721
|
+
this.baPremium = false;
|
|
1506
1722
|
this.bestAvailableConfirm = false;
|
|
1507
1723
|
this.releasingHold = false;
|
|
1508
1724
|
/** Event sales window is closed (read-only load state / live close). */
|
|
@@ -1659,6 +1875,50 @@ var SeatPicker = class _SeatPicker {
|
|
|
1659
1875
|
const sight = distance != null ? (0, import_core2.t)("picker.sightline", { m: distance }) : this.tf("picker.sightlineClear", "Clear sightline");
|
|
1660
1876
|
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>`;
|
|
1661
1877
|
}
|
|
1878
|
+
/** Minimal HTML/attribute escaper for buyer-authored commercial text (notes). */
|
|
1879
|
+
escCx(value) {
|
|
1880
|
+
return String(value ?? "").replace(/[&<>"]/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[ch]);
|
|
1881
|
+
}
|
|
1882
|
+
/** Localized "Restricted view" / "Obstructed view" label for a seat's flags,
|
|
1883
|
+
* or '' when neither is set. Restricted takes precedence when both are on. */
|
|
1884
|
+
limitedViewLabel(c) {
|
|
1885
|
+
if (c?.restrictedView) return this.tf("picker.restrictedView", "Restricted view");
|
|
1886
|
+
if (c?.obstructedView) return this.tf("picker.obstructedView", "Obstructed view");
|
|
1887
|
+
return "";
|
|
1888
|
+
}
|
|
1889
|
+
/**
|
|
1890
|
+
* Commercial flags block for the confirm/detail surface: a subtle ★ Premium
|
|
1891
|
+
* tag plus an amber ◐ limited-view caution (with the organizer's note when
|
|
1892
|
+
* present). '' when the seat carries no surfaced commercial flag.
|
|
1893
|
+
*/
|
|
1894
|
+
commercialConfirmHtml(c) {
|
|
1895
|
+
if (!c) return "";
|
|
1896
|
+
const rows = [];
|
|
1897
|
+
if (c.premium) {
|
|
1898
|
+
rows.push(
|
|
1899
|
+
`<div class="sl-cx-premium"><span class="sl-cx-star" aria-hidden="true">\u2605</span>${this.tf("picker.premiumSeat", "Premium seat")}</div>`
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
const limited = this.limitedViewLabel(c);
|
|
1903
|
+
if (limited) {
|
|
1904
|
+
rows.push(
|
|
1905
|
+
`<div class="sl-cx-warn"><span class="sl-cx-glyph" aria-hidden="true">\u25D0</span><span class="sl-cx-txt"><b>${limited}</b>${c.note ? `<span class="sl-cx-note">${this.escCx(c.note)}</span>` : ""}</span></div>`
|
|
1906
|
+
);
|
|
1907
|
+
} else if (c.note) {
|
|
1908
|
+
rows.push(
|
|
1909
|
+
`<div class="sl-cx-warn"><span class="sl-cx-glyph" aria-hidden="true">\u2139</span><span class="sl-cx-txt"><span class="sl-cx-note">${this.escCx(c.note)}</span></span></div>`
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
return rows.length ? `<div class="sl-cx">${rows.join("")}</div>` : "";
|
|
1913
|
+
}
|
|
1914
|
+
/** Small ◐ limited-view marker for a cart chip; title/aria uses the seat's
|
|
1915
|
+
* note when present, else the generic view label. '' for a clear-view seat. */
|
|
1916
|
+
commercialChipMarker(c) {
|
|
1917
|
+
const limited = this.limitedViewLabel(c);
|
|
1918
|
+
if (!limited) return "";
|
|
1919
|
+
const title = this.escCx(c?.note ? c.note : limited);
|
|
1920
|
+
return `<span class="sl-cx-mark" role="img" aria-label="${title}" title="${title}">\u25D0</span>`;
|
|
1921
|
+
}
|
|
1662
1922
|
/** True when the picker is rendered inside an iframe (snippet embed at /e/:key). */
|
|
1663
1923
|
isFramed() {
|
|
1664
1924
|
return typeof window !== "undefined" && window.parent !== window;
|
|
@@ -2031,45 +2291,71 @@ var SeatPicker = class _SeatPicker {
|
|
|
2031
2291
|
this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
|
|
2032
2292
|
this.buildBadge(chartTheme);
|
|
2033
2293
|
const present = /* @__PURE__ */ new Set();
|
|
2294
|
+
let hasLimitedView = false;
|
|
2034
2295
|
if (this.controller.doc) {
|
|
2035
2296
|
for (const seat of (0, import_core2.expandChart)(this.controller.doc)) {
|
|
2036
2297
|
for (const type of seat.accessibility ?? []) present.add(type);
|
|
2037
2298
|
if (seat.accessible && !seat.accessibility?.length) present.add("wheelchair");
|
|
2299
|
+
if (seat.commercial?.restrictedView || seat.commercial?.obstructedView) hasLimitedView = true;
|
|
2038
2300
|
}
|
|
2039
2301
|
}
|
|
2040
|
-
|
|
2302
|
+
const focusSeatsForFilter = () => {
|
|
2303
|
+
if (this.rungsEl && this.controller.getRung() !== "seats") {
|
|
2304
|
+
this.controller.setRung("seats");
|
|
2305
|
+
this.collapseSectionCard();
|
|
2306
|
+
this.syncRung();
|
|
2307
|
+
}
|
|
2308
|
+
};
|
|
2309
|
+
if (present.size || hasLimitedView) {
|
|
2041
2310
|
const chips = document.createElement("div");
|
|
2042
2311
|
chips.className = "sl-chips";
|
|
2043
|
-
const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
|
|
2044
|
-
const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-f="${key}">${label}</button>`;
|
|
2045
|
-
chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
|
|
2046
2312
|
this.regions["top-left"].appendChild(chips);
|
|
2047
2313
|
this.a11yChipsEl = chips;
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2314
|
+
if (present.size) {
|
|
2315
|
+
const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
|
|
2316
|
+
const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-a11y="1" data-f="${key}">${label}</button>`;
|
|
2317
|
+
chips.insertAdjacentHTML(
|
|
2318
|
+
"beforeend",
|
|
2319
|
+
mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("")
|
|
2320
|
+
);
|
|
2321
|
+
const active = /* @__PURE__ */ new Set();
|
|
2322
|
+
const syncChips = () => {
|
|
2323
|
+
chips.querySelectorAll("button[data-a11y]").forEach((b) => {
|
|
2324
|
+
const f = b.dataset.f;
|
|
2325
|
+
const on = f === "all" ? active.size === 0 : active.has(f);
|
|
2326
|
+
b.classList.toggle("on", on);
|
|
2327
|
+
b.setAttribute("aria-pressed", String(on));
|
|
2328
|
+
});
|
|
2329
|
+
const filter = active.size ? [...active] : null;
|
|
2330
|
+
this.controller.setAccessibilityFilter(filter);
|
|
2331
|
+
if (filter) focusSeatsForFilter();
|
|
2332
|
+
};
|
|
2333
|
+
chips.querySelectorAll("button[data-a11y]").forEach((btn) => {
|
|
2334
|
+
btn.addEventListener("click", () => {
|
|
2335
|
+
const f = btn.dataset.f;
|
|
2336
|
+
if (f === "all") active.clear();
|
|
2337
|
+
else if (active.has(f)) active.delete(f);
|
|
2338
|
+
else active.add(f);
|
|
2339
|
+
syncChips();
|
|
2340
|
+
});
|
|
2055
2341
|
});
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2342
|
+
}
|
|
2343
|
+
if (hasLimitedView) {
|
|
2344
|
+
const limited = document.createElement("button");
|
|
2345
|
+
limited.type = "button";
|
|
2346
|
+
limited.className = "sl-chip-f";
|
|
2347
|
+
limited.setAttribute("aria-pressed", "false");
|
|
2348
|
+
limited.innerHTML = `\u25D0 ${this.tf("picker.hideLimitedView", "Hide limited-view seats")}`;
|
|
2349
|
+
chips.appendChild(limited);
|
|
2350
|
+
let limitedOn = false;
|
|
2351
|
+
limited.addEventListener("click", () => {
|
|
2352
|
+
limitedOn = !limitedOn;
|
|
2353
|
+
limited.classList.toggle("on", limitedOn);
|
|
2354
|
+
limited.setAttribute("aria-pressed", String(limitedOn));
|
|
2355
|
+
this.controller.setCommercialLimitedFilter(limitedOn);
|
|
2356
|
+
if (limitedOn) focusSeatsForFilter();
|
|
2071
2357
|
});
|
|
2072
|
-
}
|
|
2358
|
+
}
|
|
2073
2359
|
}
|
|
2074
2360
|
const cb = document.createElement("button");
|
|
2075
2361
|
cb.type = "button";
|
|
@@ -2792,7 +3078,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2792
3078
|
const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
|
|
2793
3079
|
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>`;
|
|
2794
3080
|
}).join("");
|
|
2795
|
-
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>`;
|
|
3081
|
+
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>` + (summary.entrance ? `<div class="sl-seccard-entrance">${(0, import_core2.t)("picker.entrance")} ${String(summary.entrance).replace(/[&<>"]/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[ch])}</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>`;
|
|
2796
3082
|
card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
|
|
2797
3083
|
card.querySelector(".sl-seccard-overview").addEventListener("click", () => this.controller.overview());
|
|
2798
3084
|
(this.regions["top-center"] ?? this.els.map).appendChild(card);
|
|
@@ -2886,7 +3172,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2886
3172
|
el.setAttribute("aria-modal", "true");
|
|
2887
3173
|
el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
|
|
2888
3174
|
el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
|
|
2889
|
-
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"
|
|
3175
|
+
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">${safe(this.rowTypeWord(details))}</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.displayLabel ?? 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.commercialConfirmHtml(seat.commercial) + (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>`;
|
|
2890
3176
|
this.els.map.appendChild(el);
|
|
2891
3177
|
this.confirmEl = el;
|
|
2892
3178
|
this.reanchorConfirm();
|
|
@@ -3181,14 +3467,16 @@ var SeatPicker = class _SeatPicker {
|
|
|
3181
3467
|
const noPicks = !seats.length && !heldItems.length && !this.pendingGACount();
|
|
3182
3468
|
if (!this.hold && (noPicks || this.bestAvailableBusy || this.bestAvailableConfirm)) {
|
|
3183
3469
|
const cats = this.controller.doc?.categories ?? [];
|
|
3184
|
-
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>` +
|
|
3470
|
+
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>` + // Premium quick-pick — present only when the chart actually has premium
|
|
3471
|
+
// seats (same present-only philosophy as the a11y filter chips).
|
|
3472
|
+
(this.controller.hasPremiumSeats() ? `<button type="button" class="sl-ba-premium${this.baPremium ? " on" : ""}" data-ba-premium aria-pressed="${this.baPremium ? "true" : "false"}"><span class="star" aria-hidden="true">\u2605</span>${this.tf("picker.bestSeatsPremium", "Best seats")}</button>` : "") + (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>`);
|
|
3185
3473
|
}
|
|
3186
3474
|
const idGrid = (seatId, label) => {
|
|
3187
3475
|
const d = seatId ? this.controller.seatDetails(seatId) : null;
|
|
3188
3476
|
if (!d?.sectionLabel && !d?.rowLabel && !d?.seatNumber) {
|
|
3189
3477
|
return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">Seat</span><span class="val">${label}</span></span></div>`;
|
|
3190
3478
|
}
|
|
3191
|
-
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"
|
|
3479
|
+
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">${this.rowTypeWord(d)}</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>`;
|
|
3192
3480
|
};
|
|
3193
3481
|
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>`;
|
|
3194
3482
|
for (const item of heldItems) {
|
|
@@ -3199,7 +3487,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
3199
3487
|
const heldSeat = item.objectType !== "ga" ? this.controller.seatByLabel(item.label) : null;
|
|
3200
3488
|
const canView2 = this.seatViewEnabled() && !!heldSeat;
|
|
3201
3489
|
parts.push(
|
|
3202
|
-
`<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
|
|
3490
|
+
`<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>` + this.commercialChipMarker(heldSeat?.commercial) + `<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>`
|
|
3203
3491
|
);
|
|
3204
3492
|
}
|
|
3205
3493
|
const heldLabels = new Set(heldItems.map((item) => item.label));
|
|
@@ -3210,7 +3498,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
3210
3498
|
const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
|
|
3211
3499
|
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>` : "";
|
|
3212
3500
|
parts.push(
|
|
3213
|
-
`<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>`
|
|
3501
|
+
`<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.displayLabel ?? 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>${this.commercialChipMarker(s.commercial)}${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>`
|
|
3214
3502
|
);
|
|
3215
3503
|
}
|
|
3216
3504
|
for (const area of gaAreas) {
|
|
@@ -3230,6 +3518,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
3230
3518
|
this.els.tray.querySelector("[data-ba-cat]")?.addEventListener("change", (e) => {
|
|
3231
3519
|
this.baCat = e.target.value;
|
|
3232
3520
|
});
|
|
3521
|
+
this.els.tray.querySelector("[data-ba-premium]")?.addEventListener("click", () => {
|
|
3522
|
+
this.baPremium = !this.baPremium;
|
|
3523
|
+
this.syncTray();
|
|
3524
|
+
});
|
|
3233
3525
|
this.els.tray.querySelector(".sl-ba-go")?.addEventListener("click", () => {
|
|
3234
3526
|
if (this.pendingSelectionCount() > 0) {
|
|
3235
3527
|
this.bestAvailableConfirm = true;
|
|
@@ -3237,7 +3529,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
3237
3529
|
this.els.tray.querySelector("[data-ba-replace]")?.focus();
|
|
3238
3530
|
return;
|
|
3239
3531
|
}
|
|
3240
|
-
void this.bestAvailable(this.baQty, this.baCat || void 0);
|
|
3532
|
+
void this.bestAvailable(this.baQty, this.baCat || void 0, { preferPremium: this.baPremium });
|
|
3241
3533
|
});
|
|
3242
3534
|
this.els.tray.querySelector("[data-ba-cancel]")?.addEventListener("click", () => {
|
|
3243
3535
|
this.bestAvailableConfirm = false;
|
|
@@ -3246,7 +3538,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
3246
3538
|
});
|
|
3247
3539
|
this.els.tray.querySelector("[data-ba-replace]")?.addEventListener("click", () => {
|
|
3248
3540
|
this.bestAvailableConfirm = false;
|
|
3249
|
-
void this.bestAvailable(this.baQty, this.baCat || void 0);
|
|
3541
|
+
void this.bestAvailable(this.baQty, this.baCat || void 0, { preferPremium: this.baPremium });
|
|
3250
3542
|
});
|
|
3251
3543
|
this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
|
|
3252
3544
|
btn.addEventListener("click", () => {
|
|
@@ -3628,6 +3920,12 @@ var SeatPicker = class _SeatPicker {
|
|
|
3628
3920
|
* the prefix is exact (won't touch "1040-A" under section "104"); otherwise
|
|
3629
3921
|
* the label is shown verbatim.
|
|
3630
3922
|
*/
|
|
3923
|
+
/** Buyer-facing type word for the row/table key label — the designer's
|
|
3924
|
+
* per-object "Displayed type" override, or the default "Row". */
|
|
3925
|
+
rowTypeWord(details) {
|
|
3926
|
+
const t3 = details?.rowType?.trim();
|
|
3927
|
+
return t3 || "Row";
|
|
3928
|
+
}
|
|
3631
3929
|
rowShort(details) {
|
|
3632
3930
|
const row = details?.rowLabel;
|
|
3633
3931
|
const sec = details?.sectionLabel;
|
|
@@ -3647,10 +3945,12 @@ var SeatPicker = class _SeatPicker {
|
|
|
3647
3945
|
const esc2 = (v) => String(v ?? "\u2014").replace(/[&<>"]/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[ch]);
|
|
3648
3946
|
const price = this.money(this.paidPrice(details.categoryKey, details.tierId ?? null, details.price));
|
|
3649
3947
|
const hasLoc = details.sectionLabel || details.rowLabel || details.seatNumber;
|
|
3650
|
-
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"
|
|
3948
|
+
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">${esc2(this.rowTypeWord(details))}</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.displayLabel ?? 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.displayLabel ?? details.label)}</span></div></div>`;
|
|
3651
3949
|
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>`;
|
|
3950
|
+
const limited = this.limitedViewLabel(details.commercial);
|
|
3951
|
+
const cxLine = limited ? `<div class="sl-tip-cx"><span class="g" aria-hidden="true">\u25D0</span>${esc2(limited)}</div>` : "";
|
|
3652
3952
|
this.tipEl.style.setProperty("--sl-cat", details.categoryColor);
|
|
3653
|
-
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;
|
|
3953
|
+
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>` + cxLine + statusLine;
|
|
3654
3954
|
this.tipEl.style.display = "block";
|
|
3655
3955
|
this.placeTooltip();
|
|
3656
3956
|
}
|
|
@@ -3670,7 +3970,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
3670
3970
|
async removeHeldTicket(label) {
|
|
3671
3971
|
return this.removeHeldLabel(label);
|
|
3672
3972
|
}
|
|
3673
|
-
async bestAvailable(qty, categoryKey) {
|
|
3973
|
+
async bestAvailable(qty, categoryKey, opts = {}) {
|
|
3674
3974
|
if (this.salesClosed || this.bestAvailableBusy) return null;
|
|
3675
3975
|
qty = Math.max(1, Math.min(this.maxTickets, Math.floor(qty)));
|
|
3676
3976
|
if (this.confirmSeat) this.cancelConfirm();
|
|
@@ -3682,7 +3982,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
3682
3982
|
button.innerHTML = '<span class="sl-ba-spin" aria-hidden="true"></span>Finding\u2026';
|
|
3683
3983
|
}
|
|
3684
3984
|
try {
|
|
3685
|
-
const h = await this.controller.bestAvailable(qty, categoryKey);
|
|
3985
|
+
const h = await this.controller.bestAvailable(qty, categoryKey, opts);
|
|
3686
3986
|
if (h) {
|
|
3687
3987
|
this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
|
|
3688
3988
|
this.handedOff = false;
|
|
@@ -3692,6 +3992,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
3692
3992
|
this.flashHeldSeats(this.hold);
|
|
3693
3993
|
this.syncTray();
|
|
3694
3994
|
this.emitHoldChange();
|
|
3995
|
+
if (opts.preferPremium && h.seats.length && !h.seats.every((s) => s.commercial?.premium)) {
|
|
3996
|
+
this.toast((0, import_core2.t)("picker.premiumFallbackNote", { count: qty }), "neutral");
|
|
3997
|
+
}
|
|
3695
3998
|
return this.hold;
|
|
3696
3999
|
}
|
|
3697
4000
|
return null;
|