@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.js
CHANGED
|
@@ -172,8 +172,28 @@ var SeatingChart = class {
|
|
|
172
172
|
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;";
|
|
173
173
|
host.appendChild(ribbon);
|
|
174
174
|
}
|
|
175
|
+
this.buildBadge(host);
|
|
175
176
|
return this;
|
|
176
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Attribution badge pinned to the embed's bottom-right, linking to
|
|
180
|
+
* seatlayer.io. Rendered as an absolutely-positioned overlay with
|
|
181
|
+
* self-contained inline styles — the SDK embed ships no widget CSS, and an
|
|
182
|
+
* overlay keeps it out of the layout flow so it never disturbs the SDK v0.22
|
|
183
|
+
* fill-height resize contract. Mirrors the full widget's mark + wordmark and
|
|
184
|
+
* reuses the `picker.poweredBy` i18n string.
|
|
185
|
+
*/
|
|
186
|
+
buildBadge(host) {
|
|
187
|
+
if (this.controller.doc?.theme?.hideBadge) return;
|
|
188
|
+
const badge = document.createElement("a");
|
|
189
|
+
badge.href = "https://seatlayer.io";
|
|
190
|
+
badge.target = "_blank";
|
|
191
|
+
badge.rel = "noopener noreferrer";
|
|
192
|
+
badge.setAttribute("aria-label", t("picker.poweredBy"));
|
|
193
|
+
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);';
|
|
194
|
+
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>${t("picker.poweredBy")}</span>`;
|
|
195
|
+
host.appendChild(badge);
|
|
196
|
+
}
|
|
177
197
|
placeTooltip() {
|
|
178
198
|
if (!this.tipEl || !this.hostEl) return;
|
|
179
199
|
const hw = this.hostEl.clientWidth;
|
|
@@ -328,6 +348,13 @@ var TYPES = /* @__PURE__ */ new Set([
|
|
|
328
348
|
]);
|
|
329
349
|
var DEFAULT_LOADING_TIMEOUT_MS = 2e4;
|
|
330
350
|
var DEFAULT_MIN_FILL_HEIGHT = 480;
|
|
351
|
+
var RENEW_LEAD_MS = 3 * 60 * 1e3;
|
|
352
|
+
var RENEW_SHORT_TTL_MS = 15 * 60 * 1e3;
|
|
353
|
+
var RENEW_SHORT_TTL_FRACTION = 0.8;
|
|
354
|
+
var RENEW_MIN_DELAY_MS = 30 * 1e3;
|
|
355
|
+
var FILL_PROBE_HEIGHT_PX = 1e5;
|
|
356
|
+
var FILL_PROBE_TRACK_EPSILON_PX = 4;
|
|
357
|
+
var FILL_MIN_DEFINITE_HEIGHT_PX = 50;
|
|
331
358
|
function resolveContainer2(container) {
|
|
332
359
|
if (typeof container !== "string") return container;
|
|
333
360
|
const element = document.querySelector(container);
|
|
@@ -365,6 +392,14 @@ var EmbeddedDesigner = class {
|
|
|
365
392
|
this.designerOrigin = "";
|
|
366
393
|
this.overlay = null;
|
|
367
394
|
this.timeoutTimer = null;
|
|
395
|
+
/** Proactive session-renewal timer; armed from each `ready`, cleared on re-mount. */
|
|
396
|
+
this.renewTimer = null;
|
|
397
|
+
/**
|
|
398
|
+
* One automatic recovery relaunch is allowed per expiry. Reset ONLY when a fresh
|
|
399
|
+
* `ready` arrives — deliberately not on re-mount — so a session that keeps failing
|
|
400
|
+
* to load can't loop the host through endless silent relaunches.
|
|
401
|
+
*/
|
|
402
|
+
this.autoRecoverUsed = false;
|
|
368
403
|
this.phase = "loading";
|
|
369
404
|
this.restoreContainerPosition = null;
|
|
370
405
|
// Host-side fullscreen pin: saved state we restore on `off`/Escape/destroy.
|
|
@@ -375,10 +410,17 @@ var EmbeddedDesigner = class {
|
|
|
375
410
|
this.fsKeyHandler = null;
|
|
376
411
|
/** Latest height (px string) the Designer reported; re-applied after unpin. */
|
|
377
412
|
this.lastAutoHeight = "";
|
|
378
|
-
//
|
|
413
|
+
// Fill sizing: pending rAF handles + whether window listeners are attached.
|
|
379
414
|
this.fillRaf = null;
|
|
415
|
+
this.reprobeRaf = null;
|
|
380
416
|
this.fillListening = false;
|
|
381
|
-
/**
|
|
417
|
+
/** Resolved container element (fill measurement + ResizeObserver target). */
|
|
418
|
+
this.containerEl = null;
|
|
419
|
+
/** Cached fill verdict: 'container' = bounded block, 'viewport' = full page. */
|
|
420
|
+
this.fillMode = null;
|
|
421
|
+
/** Live block-size tracking in container-fill mode; disconnected on destroy. */
|
|
422
|
+
this.resizeObs = null;
|
|
423
|
+
/** rAF-throttled fill recompute, so a burst of scroll/RO ticks coalesces. */
|
|
382
424
|
this.scheduleFill = () => {
|
|
383
425
|
if (this.fillRaf !== null) return;
|
|
384
426
|
this.fillRaf = requestAnimationFrame(() => {
|
|
@@ -386,6 +428,21 @@ var EmbeddedDesigner = class {
|
|
|
386
428
|
this.applyFill();
|
|
387
429
|
});
|
|
388
430
|
};
|
|
431
|
+
/**
|
|
432
|
+
* rAF-throttled re-probe: a host layout change (responsive breakpoint, a block
|
|
433
|
+
* gaining/losing a definite height) can flip the verdict, so `resize` /
|
|
434
|
+
* `orientationchange` re-detect and swap the container observer accordingly.
|
|
435
|
+
*/
|
|
436
|
+
this.scheduleReprobe = () => {
|
|
437
|
+
if (this.reprobeRaf !== null) return;
|
|
438
|
+
this.reprobeRaf = requestAnimationFrame(() => {
|
|
439
|
+
this.reprobeRaf = null;
|
|
440
|
+
if (this.pinned) return;
|
|
441
|
+
this.fillMode = this.detectFillMode();
|
|
442
|
+
this.syncContainerObserver();
|
|
443
|
+
this.applyFill();
|
|
444
|
+
});
|
|
445
|
+
};
|
|
389
446
|
this.handleMessage = (event) => {
|
|
390
447
|
if (!this.frame || event.origin !== this.designerOrigin || event.source !== this.frame.contentWindow) return;
|
|
391
448
|
if (!event.data || typeof event.data !== "object") return;
|
|
@@ -393,7 +450,7 @@ var EmbeddedDesigner = class {
|
|
|
393
450
|
if (data.type === "seatlayer.designer.resize") {
|
|
394
451
|
if (!this.fillEnabled() && this.autoResizeEnabled() && typeof data.px === "number" && Number.isFinite(data.px) && data.px > 0) {
|
|
395
452
|
this.lastAutoHeight = `${Math.round(data.px)}px`;
|
|
396
|
-
if (!this.pinned) this.
|
|
453
|
+
if (!this.pinned) this.setFrameHeight(this.lastAutoHeight);
|
|
397
454
|
}
|
|
398
455
|
return;
|
|
399
456
|
}
|
|
@@ -421,6 +478,8 @@ var EmbeddedDesigner = class {
|
|
|
421
478
|
this.phase = "ready";
|
|
422
479
|
this.clearTimeoutTimer();
|
|
423
480
|
this.removeOverlay();
|
|
481
|
+
this.autoRecoverUsed = false;
|
|
482
|
+
this.scheduleRenewal(message.expiresAt);
|
|
424
483
|
this.options.onReady?.(message);
|
|
425
484
|
break;
|
|
426
485
|
case "seatlayer.designer.saved":
|
|
@@ -432,10 +491,18 @@ var EmbeddedDesigner = class {
|
|
|
432
491
|
case "seatlayer.designer.close":
|
|
433
492
|
this.options.onClose?.(message);
|
|
434
493
|
break;
|
|
435
|
-
case "seatlayer.designer.error":
|
|
436
|
-
|
|
494
|
+
case "seatlayer.designer.error": {
|
|
495
|
+
const cause = causeFromCode(message.code);
|
|
496
|
+
if (cause === "expired" && this.autoRenewEnabled() && !this.autoRecoverUsed) {
|
|
497
|
+
this.autoRecoverUsed = true;
|
|
498
|
+
this.clearRenewTimer();
|
|
499
|
+
this.options.onRequestRelaunch();
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
this.showError(cause);
|
|
437
503
|
this.options.onError?.(message);
|
|
438
504
|
break;
|
|
505
|
+
}
|
|
439
506
|
}
|
|
440
507
|
};
|
|
441
508
|
this.options = options;
|
|
@@ -452,12 +519,17 @@ var EmbeddedDesigner = class {
|
|
|
452
519
|
frame.allow = this.options.allow ?? "fullscreen; clipboard-write";
|
|
453
520
|
frame.referrerPolicy = this.options.referrerPolicy ?? "origin";
|
|
454
521
|
frame.src = url.toString();
|
|
455
|
-
frame.style.width
|
|
456
|
-
frame.style.
|
|
522
|
+
frame.style.setProperty("width", "100%", "important");
|
|
523
|
+
frame.style.setProperty(
|
|
524
|
+
"height",
|
|
525
|
+
typeof this.options.height === "number" ? `${this.options.height}px` : "100%",
|
|
526
|
+
"important"
|
|
527
|
+
);
|
|
457
528
|
frame.style.border = "0";
|
|
458
529
|
Object.assign(frame.style, this.options.style);
|
|
459
530
|
if (this.options.className) frame.className = this.options.className;
|
|
460
531
|
const container = resolveContainer2(this.options.container);
|
|
532
|
+
this.containerEl = container;
|
|
461
533
|
window.addEventListener("message", this.handleMessage);
|
|
462
534
|
container.append(frame);
|
|
463
535
|
this.frame = frame;
|
|
@@ -488,10 +560,13 @@ var EmbeddedDesigner = class {
|
|
|
488
560
|
this.stopFill();
|
|
489
561
|
this.unpinFullscreen();
|
|
490
562
|
this.clearTimeoutTimer();
|
|
563
|
+
this.clearRenewTimer();
|
|
491
564
|
this.removeOverlay();
|
|
492
565
|
this.restoreContainerStyle();
|
|
493
566
|
this.frame?.remove();
|
|
494
567
|
this.frame = null;
|
|
568
|
+
this.containerEl = null;
|
|
569
|
+
this.fillMode = null;
|
|
495
570
|
this.designerOrigin = "";
|
|
496
571
|
this.phase = "loading";
|
|
497
572
|
this.lastAutoHeight = "";
|
|
@@ -506,24 +581,77 @@ var EmbeddedDesigner = class {
|
|
|
506
581
|
fillEnabled() {
|
|
507
582
|
return typeof this.options.height !== "number";
|
|
508
583
|
}
|
|
584
|
+
/** Write an SDK-managed height with `!important` so a host theme can't win. */
|
|
585
|
+
setFrameHeight(value) {
|
|
586
|
+
this.frame?.style.setProperty("height", value, "important");
|
|
587
|
+
}
|
|
509
588
|
/**
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
589
|
+
* Decide whether the host gave the container a DEFINITE (bounded) height — a
|
|
590
|
+
* fixed block the embed should fill 100% of — versus a content-sized container
|
|
591
|
+
* that collapses to whatever the iframe measures (full-page usage).
|
|
592
|
+
*
|
|
593
|
+
* We drive the iframe to two extreme heights within a single synchronous task
|
|
594
|
+
* and watch whether the container follows: a bounded box barely moves, a
|
|
595
|
+
* content-sized one grows with the iframe. Because we restore the height before
|
|
596
|
+
* yielding, the browser only lays out — it never paints the extremes, so there
|
|
597
|
+
* is no visible flash. Works for px, resolved `%`, and flex (`flex:1;min-h:0`)
|
|
598
|
+
* heights, and leaves a mere `min-height` floor classified as content-sized so
|
|
599
|
+
* full-page hosts keep the old viewport-fill behavior.
|
|
600
|
+
*/
|
|
601
|
+
detectFillMode() {
|
|
602
|
+
const container = this.containerEl;
|
|
603
|
+
const frame = this.frame;
|
|
604
|
+
if (this.pinned || !container || !frame) return this.fillMode ?? "viewport";
|
|
605
|
+
const measure = () => container.getBoundingClientRect().height;
|
|
606
|
+
const savedValue = frame.style.getPropertyValue("height");
|
|
607
|
+
const savedPriority = frame.style.getPropertyPriority("height");
|
|
608
|
+
frame.style.setProperty("height", "0px", "important");
|
|
609
|
+
const collapsed = measure();
|
|
610
|
+
frame.style.setProperty("height", `${FILL_PROBE_HEIGHT_PX}px`, "important");
|
|
611
|
+
const expanded = measure();
|
|
612
|
+
if (savedValue) frame.style.setProperty("height", savedValue, savedPriority);
|
|
613
|
+
else frame.style.removeProperty("height");
|
|
614
|
+
const tracksIframe = expanded - collapsed > FILL_PROBE_TRACK_EPSILON_PX;
|
|
615
|
+
const bounded = !tracksIframe && collapsed >= FILL_MIN_DEFINITE_HEIGHT_PX;
|
|
616
|
+
return bounded ? "container" : "viewport";
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Size the iframe for the current fill verdict, clamped to `minHeight`. In
|
|
620
|
+
* container mode it fills 100% of the bounded block; in viewport mode its
|
|
621
|
+
* bottom edge meets the bottom of the viewport (`window.innerHeight - top`).
|
|
622
|
+
* No-op while pinned fullscreen (the pin fills the viewport itself).
|
|
513
623
|
*/
|
|
514
624
|
applyFill() {
|
|
515
625
|
if (!this.frame || this.pinned) return;
|
|
516
626
|
const min = this.options.minHeight ?? DEFAULT_MIN_FILL_HEIGHT;
|
|
627
|
+
if (this.fillMode === "container" && this.containerEl) {
|
|
628
|
+
const target2 = Math.max(min, Math.round(this.containerEl.getBoundingClientRect().height));
|
|
629
|
+
this.setFrameHeight(`${target2}px`);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
517
632
|
const top = this.frame.getBoundingClientRect().top;
|
|
518
633
|
const target = Math.max(min, Math.round(window.innerHeight - top));
|
|
519
|
-
this.
|
|
634
|
+
this.setFrameHeight(`${target}px`);
|
|
635
|
+
}
|
|
636
|
+
/** Attach/detach the container ResizeObserver to match the current verdict. */
|
|
637
|
+
syncContainerObserver() {
|
|
638
|
+
const want = this.fillMode === "container" && !!this.containerEl && typeof ResizeObserver !== "undefined";
|
|
639
|
+
if (want && !this.resizeObs) {
|
|
640
|
+
this.resizeObs = new ResizeObserver(() => this.scheduleFill());
|
|
641
|
+
this.resizeObs.observe(this.containerEl);
|
|
642
|
+
} else if (!want && this.resizeObs) {
|
|
643
|
+
this.resizeObs.disconnect();
|
|
644
|
+
this.resizeObs = null;
|
|
645
|
+
}
|
|
520
646
|
}
|
|
521
647
|
startFill() {
|
|
648
|
+
this.fillMode = this.detectFillMode();
|
|
649
|
+
this.syncContainerObserver();
|
|
522
650
|
this.applyFill();
|
|
523
651
|
if (this.fillListening) return;
|
|
524
652
|
this.fillListening = true;
|
|
525
|
-
window.addEventListener("resize", this.
|
|
526
|
-
window.addEventListener("orientationchange", this.
|
|
653
|
+
window.addEventListener("resize", this.scheduleReprobe);
|
|
654
|
+
window.addEventListener("orientationchange", this.scheduleReprobe);
|
|
527
655
|
window.addEventListener("scroll", this.scheduleFill, { passive: true });
|
|
528
656
|
}
|
|
529
657
|
stopFill() {
|
|
@@ -531,10 +659,18 @@ var EmbeddedDesigner = class {
|
|
|
531
659
|
cancelAnimationFrame(this.fillRaf);
|
|
532
660
|
this.fillRaf = null;
|
|
533
661
|
}
|
|
662
|
+
if (this.reprobeRaf !== null) {
|
|
663
|
+
cancelAnimationFrame(this.reprobeRaf);
|
|
664
|
+
this.reprobeRaf = null;
|
|
665
|
+
}
|
|
666
|
+
if (this.resizeObs) {
|
|
667
|
+
this.resizeObs.disconnect();
|
|
668
|
+
this.resizeObs = null;
|
|
669
|
+
}
|
|
534
670
|
if (!this.fillListening) return;
|
|
535
671
|
this.fillListening = false;
|
|
536
|
-
window.removeEventListener("resize", this.
|
|
537
|
-
window.removeEventListener("orientationchange", this.
|
|
672
|
+
window.removeEventListener("resize", this.scheduleReprobe);
|
|
673
|
+
window.removeEventListener("orientationchange", this.scheduleReprobe);
|
|
538
674
|
window.removeEventListener("scroll", this.scheduleFill);
|
|
539
675
|
}
|
|
540
676
|
/**
|
|
@@ -546,16 +682,22 @@ var EmbeddedDesigner = class {
|
|
|
546
682
|
if (this.pinned || !this.frame) return;
|
|
547
683
|
this.pinned = true;
|
|
548
684
|
this.frameStyleBeforeFs = this.frame.getAttribute("style");
|
|
549
|
-
|
|
685
|
+
const pin = {
|
|
550
686
|
position: "fixed",
|
|
551
|
-
|
|
687
|
+
top: "0",
|
|
688
|
+
right: "0",
|
|
689
|
+
bottom: "0",
|
|
690
|
+
left: "0",
|
|
552
691
|
width: "100vw",
|
|
553
692
|
height: "100vh",
|
|
554
693
|
margin: "0",
|
|
555
694
|
border: "0",
|
|
556
|
-
|
|
695
|
+
"z-index": "2147483000",
|
|
557
696
|
background: "#101625"
|
|
558
|
-
}
|
|
697
|
+
};
|
|
698
|
+
for (const [property, value] of Object.entries(pin)) {
|
|
699
|
+
this.frame.style.setProperty(property, value, "important");
|
|
700
|
+
}
|
|
559
701
|
const docEl = document.documentElement;
|
|
560
702
|
this.docOverflowBeforeFs = docEl.style.overflow;
|
|
561
703
|
docEl.style.overflow = "hidden";
|
|
@@ -576,7 +718,7 @@ var EmbeddedDesigner = class {
|
|
|
576
718
|
if (this.frameStyleBeforeFs === null) this.frame.removeAttribute("style");
|
|
577
719
|
else this.frame.setAttribute("style", this.frameStyleBeforeFs);
|
|
578
720
|
if (this.fillEnabled()) this.applyFill();
|
|
579
|
-
else if (this.autoResizeEnabled() && this.lastAutoHeight) this.
|
|
721
|
+
else if (this.autoResizeEnabled() && this.lastAutoHeight) this.setFrameHeight(this.lastAutoHeight);
|
|
580
722
|
}
|
|
581
723
|
this.frameStyleBeforeFs = null;
|
|
582
724
|
if (this.docOverflowBeforeFs !== null) {
|
|
@@ -598,6 +740,48 @@ var EmbeddedDesigner = class {
|
|
|
598
740
|
this.timeoutTimer = null;
|
|
599
741
|
}
|
|
600
742
|
}
|
|
743
|
+
/**
|
|
744
|
+
* Auto-renewal (proactive + one expiry recovery) is on when the host wired a
|
|
745
|
+
* relaunch hook and did not opt out. Without the hook there is nothing to call,
|
|
746
|
+
* so it is a no-op.
|
|
747
|
+
*/
|
|
748
|
+
autoRenewEnabled() {
|
|
749
|
+
return !!this.options.onRequestRelaunch && this.options.autoRenewSession !== false;
|
|
750
|
+
}
|
|
751
|
+
clearRenewTimer() {
|
|
752
|
+
if (this.renewTimer !== null) {
|
|
753
|
+
clearTimeout(this.renewTimer);
|
|
754
|
+
this.renewTimer = null;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Arm the proactive renewal timer from a `ready` message's `expiresAt` (epoch
|
|
759
|
+
* ms). We relaunch a comfortable lead before expiry so the host can mint a fresh
|
|
760
|
+
* session and swap `designerUrl` without the user ever seeing the expiry card:
|
|
761
|
+
*
|
|
762
|
+
* - normal TTL (≥ 15 min): renew {@link RENEW_LEAD_MS} (~3 min) before expiry;
|
|
763
|
+
* - short TTL (< 15 min): renew after {@link RENEW_SHORT_TTL_FRACTION} (80%) of
|
|
764
|
+
* the remaining life, so the lead can't overshoot the whole session;
|
|
765
|
+
* - either way, never sooner than {@link RENEW_MIN_DELAY_MS} (30s) after `ready`
|
|
766
|
+
* so a burst of `ready` messages can't spin the host.
|
|
767
|
+
*
|
|
768
|
+
* Re-armed on every `ready`; cleared on destroy / setDesignerUrl (via re-mount).
|
|
769
|
+
* A no-op when auto-renewal is off or `expiresAt` is missing/already past — the
|
|
770
|
+
* expiry-error path recovers a session that has already lapsed.
|
|
771
|
+
*/
|
|
772
|
+
scheduleRenewal(expiresAt) {
|
|
773
|
+
this.clearRenewTimer();
|
|
774
|
+
if (!this.autoRenewEnabled()) return;
|
|
775
|
+
if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) return;
|
|
776
|
+
const remaining = expiresAt - Date.now();
|
|
777
|
+
if (remaining <= 0) return;
|
|
778
|
+
const lead = remaining < RENEW_SHORT_TTL_MS ? remaining * RENEW_SHORT_TTL_FRACTION : remaining - RENEW_LEAD_MS;
|
|
779
|
+
const delay = Math.max(RENEW_MIN_DELAY_MS, lead);
|
|
780
|
+
this.renewTimer = setTimeout(() => {
|
|
781
|
+
this.renewTimer = null;
|
|
782
|
+
if (this.autoRenewEnabled()) this.options.onRequestRelaunch();
|
|
783
|
+
}, delay);
|
|
784
|
+
}
|
|
601
785
|
ensureContainerPositioned(container) {
|
|
602
786
|
const position = getComputedStyle(container).position;
|
|
603
787
|
if (position === "static") {
|
|
@@ -1254,6 +1438,17 @@ var CSS = `
|
|
|
1254
1438
|
.sl-ba-title .spark{color:var(--sl-accent);font-size:16px}
|
|
1255
1439
|
.sl-ba-copy{grid-column:1/-1;margin:-4px 0 2px 23px;color:var(--sl-muted);font-size:10.5px;line-height:1.35}
|
|
1256
1440
|
.sl-ba-copy .narrow{display:none}
|
|
1441
|
+
/* "\u2605 Best seats" premium quick-pick \u2014 gold accent echoing the \u2605 Premium pill on
|
|
1442
|
+
the confirm popover; deliberately distinct from the accent-toned qty/go. */
|
|
1443
|
+
.sl-ba-premium{position:relative;z-index:1;grid-column:1/-1;justify-self:start;display:inline-flex;align-items:center;gap:6px;
|
|
1444
|
+
padding:6px 12px;border-radius:999px;font-size:11px;font-weight:800;letter-spacing:.02em;cursor:pointer;
|
|
1445
|
+
color:#c9a24b;background:color-mix(in srgb,#e8c15a 10%,var(--sl-surface));
|
|
1446
|
+
border:1px solid color-mix(in srgb,#e8c15a 34%,var(--sl-line));transition:filter .15s,background .15s,color .15s}
|
|
1447
|
+
.sl-ba-premium .star{font-size:12px;line-height:1;color:#e8c15a}
|
|
1448
|
+
.sl-ba-premium:hover{filter:brightness(1.05)}
|
|
1449
|
+
.sl-ba-premium.on{color:#1c1608;background:linear-gradient(135deg,#f0cf6b,#e0b23f);border-color:transparent;
|
|
1450
|
+
box-shadow:0 6px 16px color-mix(in srgb,#e8c15a 26%,transparent)}
|
|
1451
|
+
.sl-ba-premium.on .star{color:#5a4410}
|
|
1257
1452
|
.sl-ba select{background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:8px;
|
|
1258
1453
|
font:inherit;font-size:11px;padding:7px 8px;min-width:0;width:100%;max-width:none}
|
|
1259
1454
|
.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)}
|
|
@@ -1351,6 +1546,25 @@ var CSS = `
|
|
|
1351
1546
|
.sl-confirm-view:hover{border-color:var(--sl-muted)}
|
|
1352
1547
|
.sl-confirm-view svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
1353
1548
|
|
|
1549
|
+
/* commercial seat flags \u2014 limited-view caution + premium tag. Amber tone,
|
|
1550
|
+
deliberately distinct from the red taken/held state; shown on the confirm
|
|
1551
|
+
card, echoed as a small \u25D0 marker on cart chips and the hover tip. */
|
|
1552
|
+
.sl-cx{display:flex;flex-direction:column;gap:6px;margin-bottom:10px}
|
|
1553
|
+
.sl-cx-warn{display:flex;align-items:flex-start;gap:7px;padding:8px 10px;border-radius:9px;
|
|
1554
|
+
background:color-mix(in srgb,#f4b740 13%,var(--sl-surface));border:1px solid color-mix(in srgb,#f4b740 40%,var(--sl-line));
|
|
1555
|
+
animation:slNoticeIn .28s ease both}
|
|
1556
|
+
.sl-cx-glyph{flex:none;font-size:14px;line-height:1.2;color:#f4b740}
|
|
1557
|
+
.sl-cx-txt{min-width:0;display:flex;flex-direction:column;gap:2px}
|
|
1558
|
+
.sl-cx-txt b{font-size:12px;font-weight:800;color:var(--sl-text)}
|
|
1559
|
+
.sl-cx-note{font-size:11px;line-height:1.4;color:var(--sl-muted)}
|
|
1560
|
+
.sl-cx-premium{display:inline-flex;align-items:center;gap:6px;align-self:flex-start;padding:4px 10px;border-radius:999px;
|
|
1561
|
+
font-size:11px;font-weight:800;letter-spacing:.02em;color:#c9a24b;
|
|
1562
|
+
background:color-mix(in srgb,#e8c15a 13%,var(--sl-surface));border:1px solid color-mix(in srgb,#e8c15a 38%,var(--sl-line))}
|
|
1563
|
+
.sl-cx-star{font-size:12px;line-height:1;color:#e8c15a}
|
|
1564
|
+
.sl-cx-mark{flex:none;font-size:12px;line-height:1;color:#f4b740;cursor:help}
|
|
1565
|
+
.sl-tip-cx{display:flex;align-items:center;gap:6px;padding:5px 10px 7px;font-size:10.5px;font-weight:700;color:#e8b24a}
|
|
1566
|
+
.sl-tip-cx .g{font-size:12px}
|
|
1567
|
+
|
|
1354
1568
|
/* 360\xB0 seat-view modal (fills the widget; drag-to-look-around equirectangular) */
|
|
1355
1569
|
.sl-view{position:absolute;inset:0;z-index:12;display:flex;flex-direction:column;background:var(--sl-bg)}
|
|
1356
1570
|
.sl-view-head{display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid var(--sl-line);flex:none}
|
|
@@ -1479,6 +1693,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
1479
1693
|
this.srEl = null;
|
|
1480
1694
|
this.baQty = 2;
|
|
1481
1695
|
this.baCat = "";
|
|
1696
|
+
/** "★ Best seats" premium quick-pick toggle — biases best-available to premium seats. */
|
|
1697
|
+
this.baPremium = false;
|
|
1482
1698
|
this.bestAvailableConfirm = false;
|
|
1483
1699
|
this.releasingHold = false;
|
|
1484
1700
|
/** Event sales window is closed (read-only load state / live close). */
|
|
@@ -1635,6 +1851,50 @@ var SeatPicker = class _SeatPicker {
|
|
|
1635
1851
|
const sight = distance != null ? t2("picker.sightline", { m: distance }) : this.tf("picker.sightlineClear", "Clear sightline");
|
|
1636
1852
|
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>`;
|
|
1637
1853
|
}
|
|
1854
|
+
/** Minimal HTML/attribute escaper for buyer-authored commercial text (notes). */
|
|
1855
|
+
escCx(value) {
|
|
1856
|
+
return String(value ?? "").replace(/[&<>"]/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[ch]);
|
|
1857
|
+
}
|
|
1858
|
+
/** Localized "Restricted view" / "Obstructed view" label for a seat's flags,
|
|
1859
|
+
* or '' when neither is set. Restricted takes precedence when both are on. */
|
|
1860
|
+
limitedViewLabel(c) {
|
|
1861
|
+
if (c?.restrictedView) return this.tf("picker.restrictedView", "Restricted view");
|
|
1862
|
+
if (c?.obstructedView) return this.tf("picker.obstructedView", "Obstructed view");
|
|
1863
|
+
return "";
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* Commercial flags block for the confirm/detail surface: a subtle ★ Premium
|
|
1867
|
+
* tag plus an amber ◐ limited-view caution (with the organizer's note when
|
|
1868
|
+
* present). '' when the seat carries no surfaced commercial flag.
|
|
1869
|
+
*/
|
|
1870
|
+
commercialConfirmHtml(c) {
|
|
1871
|
+
if (!c) return "";
|
|
1872
|
+
const rows = [];
|
|
1873
|
+
if (c.premium) {
|
|
1874
|
+
rows.push(
|
|
1875
|
+
`<div class="sl-cx-premium"><span class="sl-cx-star" aria-hidden="true">\u2605</span>${this.tf("picker.premiumSeat", "Premium seat")}</div>`
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
const limited = this.limitedViewLabel(c);
|
|
1879
|
+
if (limited) {
|
|
1880
|
+
rows.push(
|
|
1881
|
+
`<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>`
|
|
1882
|
+
);
|
|
1883
|
+
} else if (c.note) {
|
|
1884
|
+
rows.push(
|
|
1885
|
+
`<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>`
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
return rows.length ? `<div class="sl-cx">${rows.join("")}</div>` : "";
|
|
1889
|
+
}
|
|
1890
|
+
/** Small ◐ limited-view marker for a cart chip; title/aria uses the seat's
|
|
1891
|
+
* note when present, else the generic view label. '' for a clear-view seat. */
|
|
1892
|
+
commercialChipMarker(c) {
|
|
1893
|
+
const limited = this.limitedViewLabel(c);
|
|
1894
|
+
if (!limited) return "";
|
|
1895
|
+
const title = this.escCx(c?.note ? c.note : limited);
|
|
1896
|
+
return `<span class="sl-cx-mark" role="img" aria-label="${title}" title="${title}">\u25D0</span>`;
|
|
1897
|
+
}
|
|
1638
1898
|
/** True when the picker is rendered inside an iframe (snippet embed at /e/:key). */
|
|
1639
1899
|
isFramed() {
|
|
1640
1900
|
return typeof window !== "undefined" && window.parent !== window;
|
|
@@ -2007,45 +2267,71 @@ var SeatPicker = class _SeatPicker {
|
|
|
2007
2267
|
this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
|
|
2008
2268
|
this.buildBadge(chartTheme);
|
|
2009
2269
|
const present = /* @__PURE__ */ new Set();
|
|
2270
|
+
let hasLimitedView = false;
|
|
2010
2271
|
if (this.controller.doc) {
|
|
2011
2272
|
for (const seat of expandChart(this.controller.doc)) {
|
|
2012
2273
|
for (const type of seat.accessibility ?? []) present.add(type);
|
|
2013
2274
|
if (seat.accessible && !seat.accessibility?.length) present.add("wheelchair");
|
|
2275
|
+
if (seat.commercial?.restrictedView || seat.commercial?.obstructedView) hasLimitedView = true;
|
|
2014
2276
|
}
|
|
2015
2277
|
}
|
|
2016
|
-
|
|
2278
|
+
const focusSeatsForFilter = () => {
|
|
2279
|
+
if (this.rungsEl && this.controller.getRung() !== "seats") {
|
|
2280
|
+
this.controller.setRung("seats");
|
|
2281
|
+
this.collapseSectionCard();
|
|
2282
|
+
this.syncRung();
|
|
2283
|
+
}
|
|
2284
|
+
};
|
|
2285
|
+
if (present.size || hasLimitedView) {
|
|
2017
2286
|
const chips = document.createElement("div");
|
|
2018
2287
|
chips.className = "sl-chips";
|
|
2019
|
-
const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
|
|
2020
|
-
const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-f="${key}">${label}</button>`;
|
|
2021
|
-
chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
|
|
2022
2288
|
this.regions["top-left"].appendChild(chips);
|
|
2023
2289
|
this.a11yChipsEl = chips;
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2290
|
+
if (present.size) {
|
|
2291
|
+
const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
|
|
2292
|
+
const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-a11y="1" data-f="${key}">${label}</button>`;
|
|
2293
|
+
chips.insertAdjacentHTML(
|
|
2294
|
+
"beforeend",
|
|
2295
|
+
mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("")
|
|
2296
|
+
);
|
|
2297
|
+
const active = /* @__PURE__ */ new Set();
|
|
2298
|
+
const syncChips = () => {
|
|
2299
|
+
chips.querySelectorAll("button[data-a11y]").forEach((b) => {
|
|
2300
|
+
const f = b.dataset.f;
|
|
2301
|
+
const on = f === "all" ? active.size === 0 : active.has(f);
|
|
2302
|
+
b.classList.toggle("on", on);
|
|
2303
|
+
b.setAttribute("aria-pressed", String(on));
|
|
2304
|
+
});
|
|
2305
|
+
const filter = active.size ? [...active] : null;
|
|
2306
|
+
this.controller.setAccessibilityFilter(filter);
|
|
2307
|
+
if (filter) focusSeatsForFilter();
|
|
2308
|
+
};
|
|
2309
|
+
chips.querySelectorAll("button[data-a11y]").forEach((btn) => {
|
|
2310
|
+
btn.addEventListener("click", () => {
|
|
2311
|
+
const f = btn.dataset.f;
|
|
2312
|
+
if (f === "all") active.clear();
|
|
2313
|
+
else if (active.has(f)) active.delete(f);
|
|
2314
|
+
else active.add(f);
|
|
2315
|
+
syncChips();
|
|
2316
|
+
});
|
|
2031
2317
|
});
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2318
|
+
}
|
|
2319
|
+
if (hasLimitedView) {
|
|
2320
|
+
const limited = document.createElement("button");
|
|
2321
|
+
limited.type = "button";
|
|
2322
|
+
limited.className = "sl-chip-f";
|
|
2323
|
+
limited.setAttribute("aria-pressed", "false");
|
|
2324
|
+
limited.innerHTML = `\u25D0 ${this.tf("picker.hideLimitedView", "Hide limited-view seats")}`;
|
|
2325
|
+
chips.appendChild(limited);
|
|
2326
|
+
let limitedOn = false;
|
|
2327
|
+
limited.addEventListener("click", () => {
|
|
2328
|
+
limitedOn = !limitedOn;
|
|
2329
|
+
limited.classList.toggle("on", limitedOn);
|
|
2330
|
+
limited.setAttribute("aria-pressed", String(limitedOn));
|
|
2331
|
+
this.controller.setCommercialLimitedFilter(limitedOn);
|
|
2332
|
+
if (limitedOn) focusSeatsForFilter();
|
|
2047
2333
|
});
|
|
2048
|
-
}
|
|
2334
|
+
}
|
|
2049
2335
|
}
|
|
2050
2336
|
const cb = document.createElement("button");
|
|
2051
2337
|
cb.type = "button";
|
|
@@ -2768,7 +3054,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2768
3054
|
const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
|
|
2769
3055
|
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>`;
|
|
2770
3056
|
}).join("");
|
|
2771
|
-
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>`;
|
|
3057
|
+
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">${t2("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 ${t2("picker.overview")}</button><span class="sl-seccard-hint">${t2("picker.tapSeatHint")}</span></div>`;
|
|
2772
3058
|
card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
|
|
2773
3059
|
card.querySelector(".sl-seccard-overview").addEventListener("click", () => this.controller.overview());
|
|
2774
3060
|
(this.regions["top-center"] ?? this.els.map).appendChild(card);
|
|
@@ -2862,7 +3148,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
2862
3148
|
el.setAttribute("aria-modal", "true");
|
|
2863
3149
|
el.setAttribute("aria-label", `Confirm seat ${seat.label}`);
|
|
2864
3150
|
el.style.setProperty("--sl-cat", cat?.color ?? "#6e7bff");
|
|
2865
|
-
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"
|
|
3151
|
+
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>`;
|
|
2866
3152
|
this.els.map.appendChild(el);
|
|
2867
3153
|
this.confirmEl = el;
|
|
2868
3154
|
this.reanchorConfirm();
|
|
@@ -3157,14 +3443,16 @@ var SeatPicker = class _SeatPicker {
|
|
|
3157
3443
|
const noPicks = !seats.length && !heldItems.length && !this.pendingGACount();
|
|
3158
3444
|
if (!this.hold && (noPicks || this.bestAvailableBusy || this.bestAvailableConfirm)) {
|
|
3159
3445
|
const cats = this.controller.doc?.categories ?? [];
|
|
3160
|
-
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>` +
|
|
3446
|
+
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
|
|
3447
|
+
// seats (same present-only philosophy as the a11y filter chips).
|
|
3448
|
+
(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>`);
|
|
3161
3449
|
}
|
|
3162
3450
|
const idGrid = (seatId, label) => {
|
|
3163
3451
|
const d = seatId ? this.controller.seatDetails(seatId) : null;
|
|
3164
3452
|
if (!d?.sectionLabel && !d?.rowLabel && !d?.seatNumber) {
|
|
3165
3453
|
return `<div class="sl-chip-id"><span class="fld sec"><span class="sl-chip-eb">Seat</span><span class="val">${label}</span></span></div>`;
|
|
3166
3454
|
}
|
|
3167
|
-
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"
|
|
3455
|
+
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>`;
|
|
3168
3456
|
};
|
|
3169
3457
|
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>`;
|
|
3170
3458
|
for (const item of heldItems) {
|
|
@@ -3175,7 +3463,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
3175
3463
|
const heldSeat = item.objectType !== "ga" ? this.controller.seatByLabel(item.label) : null;
|
|
3176
3464
|
const canView2 = this.seatViewEnabled() && !!heldSeat;
|
|
3177
3465
|
parts.push(
|
|
3178
|
-
`<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
|
|
3466
|
+
`<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>`
|
|
3179
3467
|
);
|
|
3180
3468
|
}
|
|
3181
3469
|
const heldLabels = new Set(heldItems.map((item) => item.label));
|
|
@@ -3186,7 +3474,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
3186
3474
|
const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
|
|
3187
3475
|
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>` : "";
|
|
3188
3476
|
parts.push(
|
|
3189
|
-
`<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>`
|
|
3477
|
+
`<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>`
|
|
3190
3478
|
);
|
|
3191
3479
|
}
|
|
3192
3480
|
for (const area of gaAreas) {
|
|
@@ -3206,6 +3494,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
3206
3494
|
this.els.tray.querySelector("[data-ba-cat]")?.addEventListener("change", (e) => {
|
|
3207
3495
|
this.baCat = e.target.value;
|
|
3208
3496
|
});
|
|
3497
|
+
this.els.tray.querySelector("[data-ba-premium]")?.addEventListener("click", () => {
|
|
3498
|
+
this.baPremium = !this.baPremium;
|
|
3499
|
+
this.syncTray();
|
|
3500
|
+
});
|
|
3209
3501
|
this.els.tray.querySelector(".sl-ba-go")?.addEventListener("click", () => {
|
|
3210
3502
|
if (this.pendingSelectionCount() > 0) {
|
|
3211
3503
|
this.bestAvailableConfirm = true;
|
|
@@ -3213,7 +3505,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
3213
3505
|
this.els.tray.querySelector("[data-ba-replace]")?.focus();
|
|
3214
3506
|
return;
|
|
3215
3507
|
}
|
|
3216
|
-
void this.bestAvailable(this.baQty, this.baCat || void 0);
|
|
3508
|
+
void this.bestAvailable(this.baQty, this.baCat || void 0, { preferPremium: this.baPremium });
|
|
3217
3509
|
});
|
|
3218
3510
|
this.els.tray.querySelector("[data-ba-cancel]")?.addEventListener("click", () => {
|
|
3219
3511
|
this.bestAvailableConfirm = false;
|
|
@@ -3222,7 +3514,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
3222
3514
|
});
|
|
3223
3515
|
this.els.tray.querySelector("[data-ba-replace]")?.addEventListener("click", () => {
|
|
3224
3516
|
this.bestAvailableConfirm = false;
|
|
3225
|
-
void this.bestAvailable(this.baQty, this.baCat || void 0);
|
|
3517
|
+
void this.bestAvailable(this.baQty, this.baCat || void 0, { preferPremium: this.baPremium });
|
|
3226
3518
|
});
|
|
3227
3519
|
this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
|
|
3228
3520
|
btn.addEventListener("click", () => {
|
|
@@ -3604,6 +3896,12 @@ var SeatPicker = class _SeatPicker {
|
|
|
3604
3896
|
* the prefix is exact (won't touch "1040-A" under section "104"); otherwise
|
|
3605
3897
|
* the label is shown verbatim.
|
|
3606
3898
|
*/
|
|
3899
|
+
/** Buyer-facing type word for the row/table key label — the designer's
|
|
3900
|
+
* per-object "Displayed type" override, or the default "Row". */
|
|
3901
|
+
rowTypeWord(details) {
|
|
3902
|
+
const t3 = details?.rowType?.trim();
|
|
3903
|
+
return t3 || "Row";
|
|
3904
|
+
}
|
|
3607
3905
|
rowShort(details) {
|
|
3608
3906
|
const row = details?.rowLabel;
|
|
3609
3907
|
const sec = details?.sectionLabel;
|
|
@@ -3623,10 +3921,12 @@ var SeatPicker = class _SeatPicker {
|
|
|
3623
3921
|
const esc2 = (v) => String(v ?? "\u2014").replace(/[&<>"]/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[ch]);
|
|
3624
3922
|
const price = this.money(this.paidPrice(details.categoryKey, details.tierId ?? null, details.price));
|
|
3625
3923
|
const hasLoc = details.sectionLabel || details.rowLabel || details.seatNumber;
|
|
3626
|
-
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"
|
|
3924
|
+
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>`;
|
|
3627
3925
|
const statusLine = details.status === "free" ? "" : `<div class="sl-tip-status">${details.status === "held" ? t2("map.statusHeld") : t2("map.statusTaken")}</div>`;
|
|
3926
|
+
const limited = this.limitedViewLabel(details.commercial);
|
|
3927
|
+
const cxLine = limited ? `<div class="sl-tip-cx"><span class="g" aria-hidden="true">\u25D0</span>${esc2(limited)}</div>` : "";
|
|
3628
3928
|
this.tipEl.style.setProperty("--sl-cat", details.categoryColor);
|
|
3629
|
-
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;
|
|
3929
|
+
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;
|
|
3630
3930
|
this.tipEl.style.display = "block";
|
|
3631
3931
|
this.placeTooltip();
|
|
3632
3932
|
}
|
|
@@ -3646,7 +3946,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
3646
3946
|
async removeHeldTicket(label) {
|
|
3647
3947
|
return this.removeHeldLabel(label);
|
|
3648
3948
|
}
|
|
3649
|
-
async bestAvailable(qty, categoryKey) {
|
|
3949
|
+
async bestAvailable(qty, categoryKey, opts = {}) {
|
|
3650
3950
|
if (this.salesClosed || this.bestAvailableBusy) return null;
|
|
3651
3951
|
qty = Math.max(1, Math.min(this.maxTickets, Math.floor(qty)));
|
|
3652
3952
|
if (this.confirmSeat) this.cancelConfirm();
|
|
@@ -3658,7 +3958,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
3658
3958
|
button.innerHTML = '<span class="sl-ba-spin" aria-hidden="true"></span>Finding\u2026';
|
|
3659
3959
|
}
|
|
3660
3960
|
try {
|
|
3661
|
-
const h = await this.controller.bestAvailable(qty, categoryKey);
|
|
3961
|
+
const h = await this.controller.bestAvailable(qty, categoryKey, opts);
|
|
3662
3962
|
if (h) {
|
|
3663
3963
|
this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
|
|
3664
3964
|
this.handedOff = false;
|
|
@@ -3668,6 +3968,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
3668
3968
|
this.flashHeldSeats(this.hold);
|
|
3669
3969
|
this.syncTray();
|
|
3670
3970
|
this.emitHoldChange();
|
|
3971
|
+
if (opts.preferPremium && h.seats.length && !h.seats.every((s) => s.commercial?.premium)) {
|
|
3972
|
+
this.toast(t2("picker.premiumFallbackNote", { count: qty }), "neutral");
|
|
3973
|
+
}
|
|
3671
3974
|
return this.hold;
|
|
3672
3975
|
}
|
|
3673
3976
|
return null;
|