@seatlayer/js 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +750 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +151 -3
- package/dist/index.d.ts +151 -3
- package/dist/index.js +750 -52
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -86,6 +86,14 @@ var PubApi = class {
|
|
|
86
86
|
body: { labels, holdId }
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
|
+
/** P4 "need more time?": push an active hold's expiry out. Throws ApiError 409
|
|
90
|
+
* (reason: expired | extend_limit | not_found | not_active) if it can't. */
|
|
91
|
+
extend(key, holdId, ttlMs) {
|
|
92
|
+
return request(this.base, `/pub/events/${encodeURIComponent(key)}/extend`, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
body: { holdId, ...ttlMs ? { ttlMs } : {} }
|
|
95
|
+
});
|
|
96
|
+
}
|
|
89
97
|
socketUrl(key) {
|
|
90
98
|
const wsBase = this.base.replace(/^http/, "ws");
|
|
91
99
|
return `${wsBase}/pub/events/${encodeURIComponent(key)}/subscribe`;
|
|
@@ -407,6 +415,15 @@ var EmbeddedDesigner = class {
|
|
|
407
415
|
var import_core2 = require("@seatlayer/core");
|
|
408
416
|
var DEFAULT_API_BASE2 = "https://api.seatlayer.io";
|
|
409
417
|
var DEFAULT_MAX_SELECTION2 = 10;
|
|
418
|
+
var EXTEND_PROMPT_MS = 6e4;
|
|
419
|
+
function pointInPolygon(x, y, poly) {
|
|
420
|
+
let inside = false;
|
|
421
|
+
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
|
422
|
+
const xi = poly[i].x, yi = poly[i].y, xj = poly[j].x, yj = poly[j].y;
|
|
423
|
+
if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside;
|
|
424
|
+
}
|
|
425
|
+
return inside;
|
|
426
|
+
}
|
|
410
427
|
function resolveContainer3(container) {
|
|
411
428
|
if (typeof container === "string") {
|
|
412
429
|
const el = document.querySelector(container);
|
|
@@ -450,12 +467,44 @@ var CSS = `
|
|
|
450
467
|
.sl-map-host{position:absolute;inset:0}
|
|
451
468
|
.sl-side{width:300px;flex:none;border-left:1px solid var(--sl-line);display:flex;flex-direction:column;min-height:0;overflow-y:auto}
|
|
452
469
|
|
|
453
|
-
/* narrow (container < 640px):
|
|
470
|
+
/* narrow (container < 640px): map-first \u2014 the map claims ~80-85% of the
|
|
471
|
+
container and the side panel becomes a PEEKING bottom sheet (AXS/Ticketmaster
|
|
472
|
+
mobile pattern). data-sheet on the root: "peek" (default: grab handle + one
|
|
473
|
+
summary line) / "open" (\u226450%, swipe up or auto-expand on first selection).
|
|
474
|
+
Swipe handling lives on the sheet head ONLY \u2014 never the map host, so the
|
|
475
|
+
map's raw-pointer gesture pipeline is untouched. */
|
|
454
476
|
.sl-picker[data-layout="narrow"] .sl-body{flex-direction:column}
|
|
455
477
|
.sl-picker[data-layout="narrow"] .sl-map{min-height:0;flex:1}
|
|
456
|
-
.sl-picker[data-layout="narrow"] .sl-side{width:100%;
|
|
478
|
+
.sl-picker[data-layout="narrow"] .sl-side{width:100%;border-left:0;border-top:1px solid var(--sl-line);
|
|
479
|
+
flex:none;height:50%;transition:height .22s ease}
|
|
480
|
+
.sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side{height:86px;overflow:hidden}
|
|
481
|
+
.sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side > :not(.sl-sheet-head){display:none}
|
|
457
482
|
.sl-picker[data-layout="narrow"] .sl-tray{flex:none}
|
|
458
483
|
.sl-picker[data-layout="narrow"] .sl-foot{position:sticky;bottom:0;background:var(--sl-bg)}
|
|
484
|
+
/* touch chrome: pinch-zoom exists \u2014 hide +/\u2212 on the sheet layout (keep fit) */
|
|
485
|
+
.sl-picker[data-layout="narrow"] .sl-zoom [data-ref="zin"],
|
|
486
|
+
.sl-picker[data-layout="narrow"] .sl-zoom [data-ref="zout"]{display:none}
|
|
487
|
+
|
|
488
|
+
/* bottom-sheet head: grab handle + one-line summary (narrow only) */
|
|
489
|
+
.sl-sheet-head{display:none;flex-direction:column;padding:6px 16px 8px;cursor:grab;touch-action:none;
|
|
490
|
+
user-select:none;-webkit-user-select:none;flex:none}
|
|
491
|
+
.sl-picker[data-layout="narrow"] .sl-sheet-head{display:flex}
|
|
492
|
+
.sl-sheet-grab{width:36px;height:4px;border-radius:999px;background:var(--sl-muted);opacity:.55;margin:2px auto 7px}
|
|
493
|
+
.sl-sheet-peek{display:flex;align-items:center;gap:7px;font-size:13px;font-weight:700;color:var(--sl-text);
|
|
494
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-height:20px}
|
|
495
|
+
.sl-sheet-peek .sub{color:var(--sl-muted);font-weight:600}
|
|
496
|
+
.sl-sheet-peek .go{margin-left:auto;color:var(--sl-accent);font-weight:800;flex:none}
|
|
497
|
+
|
|
498
|
+
/* consolidated Filters row inside the sheet (a11y chips + colorblind toggle
|
|
499
|
+
dock here on narrow; they live on the map / zoom column on wide) */
|
|
500
|
+
.sl-filtersec{display:none}
|
|
501
|
+
.sl-filters{display:none;gap:6px;flex-wrap:wrap;align-items:center;padding:2px 16px 10px}
|
|
502
|
+
.sl-picker[data-layout="narrow"] .sl-filtersec.has{display:block}
|
|
503
|
+
.sl-picker[data-layout="narrow"] .sl-filters.has{display:flex}
|
|
504
|
+
.sl-cbbtn{width:32px;height:32px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
505
|
+
color:var(--sl-text);display:flex;align-items:center;justify-content:center;transition:border-color .15s}
|
|
506
|
+
.sl-cbbtn:hover{border-color:var(--sl-muted)}
|
|
507
|
+
.sl-cbbtn svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
459
508
|
|
|
460
509
|
/* price panel */
|
|
461
510
|
.sl-sec{padding:14px 16px 4px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--sl-muted);font-weight:700}
|
|
@@ -498,19 +547,44 @@ var CSS = `
|
|
|
498
547
|
.sl-cta:hover{filter:brightness(1.08)}
|
|
499
548
|
.sl-cta:disabled{opacity:.45;cursor:not-allowed}
|
|
500
549
|
|
|
501
|
-
/*
|
|
502
|
-
|
|
550
|
+
/* Chrome anchor regions (Feature 6) \u2014 every persistent map overlay is APPENDED
|
|
551
|
+
INTO one of these positioned flex containers and flows/stacks within it, so no
|
|
552
|
+
two pieces of chrome free-float on top of each other. Regions never overlap:
|
|
553
|
+
the top strip splits into left/center/right; rails + corners own their edge. */
|
|
554
|
+
.sl-anchor{position:absolute;z-index:5;display:flex;gap:8px;pointer-events:none}
|
|
555
|
+
.sl-anchor > *{pointer-events:auto}
|
|
556
|
+
.sl-anchor[data-region="top-left"]{top:12px;left:12px;flex-wrap:wrap;max-width:38%}
|
|
557
|
+
.sl-anchor[data-region="top-center"]{top:12px;left:50%;transform:translateX(-50%);flex-direction:column;
|
|
558
|
+
align-items:center;max-width:44%}
|
|
559
|
+
.sl-anchor[data-region="top-right"]{top:12px;right:12px;justify-content:flex-end;flex-wrap:wrap;max-width:38%}
|
|
560
|
+
.sl-anchor[data-region="left-rail"]{top:50%;left:12px;transform:translateY(-50%);flex-direction:column;max-width:42%;gap:6px}
|
|
561
|
+
.sl-anchor[data-region="bottom-left"]{left:12px;bottom:12px;flex-direction:column;align-items:flex-start}
|
|
562
|
+
.sl-anchor[data-region="bottom-center"]{left:50%;bottom:14px;transform:translateX(-50%);z-index:9;
|
|
563
|
+
flex-direction:column;align-items:center;gap:8px;max-width:92%}
|
|
564
|
+
.sl-anchor[data-region="bottom-right"]{right:12px;bottom:12px;flex-direction:column;align-items:flex-end;gap:6px}
|
|
565
|
+
/* narrow: tighten the top strip so left/center can't crowd each other */
|
|
566
|
+
.sl-picker[data-layout="narrow"] .sl-anchor[data-region="top-left"]{max-width:30%}
|
|
567
|
+
.sl-picker[data-layout="narrow"] .sl-anchor[data-region="top-center"]{max-width:44%}
|
|
568
|
+
|
|
569
|
+
/* TEST MODE badge \u2014 a small pill in the top-right region (shrinks on narrow) */
|
|
570
|
+
.sl-testbadge{padding:5px 11px;border-radius:999px;font-size:10px;font-weight:800;letter-spacing:.1em;
|
|
571
|
+
text-transform:uppercase;white-space:nowrap;background:var(--sl-accent);color:var(--sl-accent-ink);
|
|
572
|
+
box-shadow:0 2px 8px rgba(0,0,0,.25)}
|
|
573
|
+
.sl-picker[data-layout="narrow"] .sl-testbadge{padding:3px 8px;font-size:8.5px;letter-spacing:.06em}
|
|
574
|
+
|
|
575
|
+
/* zoom column (flows within the bottom-right region) */
|
|
576
|
+
.sl-zoom{display:flex;flex-direction:column;gap:6px}
|
|
503
577
|
.sl-zoom button{width:36px;height:36px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
504
578
|
color:var(--sl-text);font-size:17px;font-weight:700;display:flex;align-items:center;justify-content:center;transition:border-color .15s}
|
|
505
579
|
.sl-zoom button:hover{border-color:var(--sl-muted)}
|
|
506
580
|
.sl-zoom svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
507
581
|
|
|
508
|
-
/* toast + boot states */
|
|
509
|
-
.sl-toast{
|
|
582
|
+
/* toast + boot states (toast flows in the bottom-center region) */
|
|
583
|
+
.sl-toast{transform:translateY(6px);max-width:100%;
|
|
510
584
|
background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-text);border-radius:999px;padding:9px 16px;
|
|
511
585
|
font-size:12.5px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .2s,transform .2s;white-space:nowrap;
|
|
512
586
|
overflow:hidden;text-overflow:ellipsis}
|
|
513
|
-
.sl-toast.on{opacity:1;transform:
|
|
587
|
+
.sl-toast.on{opacity:1;transform:translateY(0)}
|
|
514
588
|
.sl-boot{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|
515
589
|
gap:10px;background:var(--sl-bg);font-size:13px;font-weight:600;color:var(--sl-muted)}
|
|
516
590
|
.sl-boot-spin{width:24px;height:24px;border-radius:50%;border:3px solid var(--sl-line);border-top-color:var(--sl-accent);
|
|
@@ -520,8 +594,32 @@ var CSS = `
|
|
|
520
594
|
.sl-boot-retry{margin-top:4px;padding:9px 20px;border-radius:var(--sl-r-sm);background:var(--sl-accent);
|
|
521
595
|
color:var(--sl-accent-ink);font-weight:700;font-size:13px}
|
|
522
596
|
|
|
523
|
-
/*
|
|
524
|
-
.sl-
|
|
597
|
+
/* "Need more time?" extend prompt (flows in the bottom-center region, above the toast) */
|
|
598
|
+
.sl-extend{transform:translateY(6px);
|
|
599
|
+
display:none;align-items:center;gap:12px;max-width:100%;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
600
|
+
color:var(--sl-text);border-radius:14px;padding:10px 12px 10px 16px;box-shadow:0 18px 50px -18px rgba(0,0,0,.6);
|
|
601
|
+
opacity:0;transition:opacity .2s,transform .2s}
|
|
602
|
+
.sl-extend.on{display:flex;opacity:1;transform:translateY(0)}
|
|
603
|
+
.sl-extend-txt{font-size:12.5px;font-weight:600;line-height:1.35}
|
|
604
|
+
.sl-extend-txt b{font-variant-numeric:tabular-nums}
|
|
605
|
+
.sl-extend-btn{flex:none;padding:8px 14px;border-radius:999px;font-weight:800;font-size:12.5px;
|
|
606
|
+
background:var(--sl-accent);color:var(--sl-accent-ink);transition:filter .15s,opacity .15s}
|
|
607
|
+
.sl-extend-btn:hover{filter:brightness(1.08)}
|
|
608
|
+
.sl-extend-btn:disabled{opacity:.5;cursor:not-allowed}
|
|
609
|
+
|
|
610
|
+
/* booked confirmation overlay (covers the widget once the held seats are sold) */
|
|
611
|
+
.sl-booked{position:absolute;inset:0;z-index:11;display:none;flex-direction:column;align-items:center;
|
|
612
|
+
justify-content:center;gap:12px;text-align:center;padding:28px;background:var(--sl-bg)}
|
|
613
|
+
.sl-booked.on{display:flex}
|
|
614
|
+
.sl-booked-badge{width:60px;height:60px;border-radius:999px;display:flex;align-items:center;justify-content:center;
|
|
615
|
+
background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
616
|
+
.sl-booked-badge svg{width:30px;height:30px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
617
|
+
.sl-booked-title{font-weight:800;font-size:19px;color:var(--sl-text)}
|
|
618
|
+
.sl-booked-sub{font-size:13px;color:var(--sl-muted);line-height:1.5;max-width:320px}
|
|
619
|
+
.sl-booked-seats{font-weight:700;color:var(--sl-text)}
|
|
620
|
+
|
|
621
|
+
/* a11y filter chips (flow within the top-left region) */
|
|
622
|
+
.sl-chips{display:flex;gap:6px;flex-wrap:wrap}
|
|
525
623
|
.sl-chip-f{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:999px;font-size:12px;font-weight:700;
|
|
526
624
|
background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-muted);transition:color .15s,border-color .15s}
|
|
527
625
|
.sl-chip-f:hover{color:var(--sl-text)}
|
|
@@ -561,18 +659,18 @@ var CSS = `
|
|
|
561
659
|
.sl-chip .view:hover{color:var(--sl-text)}
|
|
562
660
|
.sl-chip .view svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
563
661
|
|
|
564
|
-
/* arena: LOD rung pills (top-center
|
|
565
|
-
.sl-rungs{
|
|
566
|
-
background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:999px;padding:3px}
|
|
662
|
+
/* arena: LOD rung pills (flow within the top-center region) */
|
|
663
|
+
.sl-rungs{display:none;background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:999px;padding:3px}
|
|
567
664
|
.sl-rungs.on{display:inline-flex;gap:2px}
|
|
568
665
|
.sl-rungs button{padding:6px 13px;border-radius:999px;font-size:10.5px;font-weight:800;letter-spacing:.07em;
|
|
569
666
|
color:var(--sl-muted);white-space:nowrap;transition:color .15s}
|
|
570
667
|
.sl-rungs button:hover{color:var(--sl-text)}
|
|
571
668
|
.sl-rungs button.on{background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
669
|
+
/* narrow: shrink the rung pills so the centered row can't reach the corner regions */
|
|
670
|
+
.sl-picker[data-layout="narrow"] .sl-rungs button{padding:5px 9px;font-size:9px;letter-spacing:.03em}
|
|
572
671
|
|
|
573
|
-
/* multi-floor switcher (
|
|
574
|
-
.sl-floors{
|
|
575
|
-
flex-direction:column;gap:6px;max-width:42%}
|
|
672
|
+
/* multi-floor switcher (flows within the left-rail region) */
|
|
673
|
+
.sl-floors{display:none;flex-direction:column;gap:6px;max-width:100%}
|
|
576
674
|
.sl-floors.on{display:flex}
|
|
577
675
|
.sl-floors button{padding:7px 13px;border-radius:999px;font-size:12px;font-weight:700;background:var(--sl-surface);
|
|
578
676
|
border:1px solid var(--sl-line);color:var(--sl-muted);white-space:nowrap;max-width:100%;overflow:hidden;
|
|
@@ -580,11 +678,25 @@ var CSS = `
|
|
|
580
678
|
.sl-floors button:hover{color:var(--sl-text)}
|
|
581
679
|
.sl-floors button.on{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
|
|
582
680
|
|
|
583
|
-
/* tapped-section summary card
|
|
584
|
-
|
|
585
|
-
|
|
681
|
+
/* tapped-section summary card \u2014 docks INSIDE the top-center anchor region on
|
|
682
|
+
wide (flows below the rung pills, never over them, never floating over the
|
|
683
|
+
seats at the tap point). Auto-collapses to a slim pill once seat-picking
|
|
684
|
+
begins (first seat select, or a pan/zoom after the focus glide); tapping the
|
|
685
|
+
pill re-expands; \u2715 closes in both states. On narrow it renders as a compact
|
|
686
|
+
strip inside the bottom sheet's peek head \u2014 never over the canvas. */
|
|
687
|
+
.sl-seccard{width:250px;max-width:100%;background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:12px;
|
|
586
688
|
padding:12px 14px;box-shadow:0 18px 50px -18px rgba(0,0,0,.6);display:none}
|
|
587
689
|
.sl-seccard.on{display:block}
|
|
690
|
+
/* collapsed pill (wide) */
|
|
691
|
+
.sl-seccard.mini{width:auto;padding:5px 7px 5px 12px;border-radius:999px;cursor:pointer}
|
|
692
|
+
.sl-seccard.mini.on{display:inline-flex;align-items:center;gap:7px}
|
|
693
|
+
.sl-seccard.mini .sl-seccard-name{font-size:12px;flex:none;max-width:120px}
|
|
694
|
+
.sl-seccard.mini .sl-seccard-left{font-size:11px}
|
|
695
|
+
/* narrow: compact strip inside the sheet head (peek area) */
|
|
696
|
+
.sl-seccard.strip{width:100%;padding:7px 0 0;border:0;border-radius:0;box-shadow:none;background:none;cursor:default}
|
|
697
|
+
.sl-seccard.strip.on{display:flex;align-items:center;gap:7px;font-size:12.5px}
|
|
698
|
+
.sl-seccard.strip .sl-seccard-name{font-size:12.5px}
|
|
699
|
+
.sl-seccard.strip .sl-seccard-price{margin-left:auto}
|
|
588
700
|
.sl-seccard-head{display:flex;align-items:center;gap:8px}
|
|
589
701
|
.sl-seccard-dot{width:10px;height:10px;border-radius:50%;flex:none}
|
|
590
702
|
.sl-seccard-name{font-weight:800;font-size:14px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
@@ -626,6 +738,20 @@ var CSS = `
|
|
|
626
738
|
font-size:11.5px;font-weight:600;background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-muted);
|
|
627
739
|
white-space:nowrap;pointer-events:none;max-width:90%;overflow:hidden;text-overflow:ellipsis}
|
|
628
740
|
|
|
741
|
+
/* F3 minimap \u2014 venue overview + live viewport rect (flows in the bottom-left region) */
|
|
742
|
+
.sl-minimap{border:1px solid var(--sl-line);border-radius:9px;
|
|
743
|
+
overflow:hidden;background:var(--sl-surface);box-shadow:0 12px 34px -14px rgba(0,0,0,.55);line-height:0;cursor:pointer}
|
|
744
|
+
.sl-minimap canvas{display:block}
|
|
745
|
+
.sl-picker[data-layout="narrow"] .sl-minimap{display:none}
|
|
746
|
+
|
|
747
|
+
/* F4 price-band filter \u2014 chip row in the side panel, above the price legend it
|
|
748
|
+
* dims (keeps clear of the map's top-center rungs / top-left a11y chips). */
|
|
749
|
+
.sl-pricef{display:flex;gap:6px;flex-wrap:wrap;padding:2px 16px 10px}
|
|
750
|
+
.sl-pricef .sl-chip-f{padding:6px 11px;font-size:11.5px}
|
|
751
|
+
/* F4 legend reflection: rows + counts for out-of-band categories read muted */
|
|
752
|
+
.sl-price-row.sl-dim{opacity:.4}
|
|
753
|
+
.sl-seccard-mix-item.sl-dim{opacity:.4}
|
|
754
|
+
|
|
629
755
|
/* modal host */
|
|
630
756
|
.sl-modal-scrim{position:fixed;inset:0;z-index:2147483000;background:rgba(5,7,12,.66);display:flex;align-items:center;justify-content:center;padding:18px}
|
|
631
757
|
.sl-modal-frame{width:min(1200px,100%);height:min(820px,100%);border-radius:16px;overflow:hidden;box-shadow:0 40px 120px -30px rgba(0,0,0,.8)}
|
|
@@ -661,12 +787,22 @@ var SeatPicker = class _SeatPicker {
|
|
|
661
787
|
this.destroyed = false;
|
|
662
788
|
// chrome refs
|
|
663
789
|
this.els = {};
|
|
790
|
+
/** Feature 6 anchor regions — positioned flex containers over the map. */
|
|
791
|
+
this.regions = {};
|
|
664
792
|
this.ro = null;
|
|
665
793
|
this.holdTimer = null;
|
|
666
794
|
this.toastTimer = null;
|
|
667
795
|
// state
|
|
668
796
|
this.currency = "USD";
|
|
669
797
|
this.hold = null;
|
|
798
|
+
/** Latest server expiry for the open hold (moves on extend). */
|
|
799
|
+
this.holdExpiresAt = 0;
|
|
800
|
+
/** True once we handed off to checkout — arms booked-confirmation detection. */
|
|
801
|
+
this.handedOff = false;
|
|
802
|
+
/** Guards single onBooked + single success overlay per hold. */
|
|
803
|
+
this.bookedShown = false;
|
|
804
|
+
this.extendEl = null;
|
|
805
|
+
this.bookedEl = null;
|
|
670
806
|
this.gaQty = /* @__PURE__ */ new Map();
|
|
671
807
|
this.tipEl = null;
|
|
672
808
|
this.tipPos = { x: 0, y: 0 };
|
|
@@ -683,6 +819,25 @@ var SeatPicker = class _SeatPicker {
|
|
|
683
819
|
this.viewEl = null;
|
|
684
820
|
this.viewCleanup = null;
|
|
685
821
|
this.allSeatsCache = null;
|
|
822
|
+
// F3 minimap
|
|
823
|
+
this.miniEl = null;
|
|
824
|
+
this.miniCanvas = null;
|
|
825
|
+
this.miniBase = null;
|
|
826
|
+
this.miniTf = null;
|
|
827
|
+
// F4 price-band filter — active band's category keys (null = all prices)
|
|
828
|
+
this.priceBandKeys = null;
|
|
829
|
+
this.priceFilterEl = null;
|
|
830
|
+
/** Last surfaced section summary (re-rendered when the price band changes). */
|
|
831
|
+
this.lastSection = null;
|
|
832
|
+
/** Section card collapsed to its slim pill (seat-picking has begun). */
|
|
833
|
+
this.secCardCollapsed = false;
|
|
834
|
+
/** When the card was (re)shown — the focus glide's own view change must not collapse it. */
|
|
835
|
+
this.secCardShownAt = 0;
|
|
836
|
+
/** Previous tray ticket count — first 0→n transition auto-expands the mobile sheet. */
|
|
837
|
+
this.lastTrayCount = 0;
|
|
838
|
+
// narrow-layout chrome that docks into the sheet's Filters row on mobile
|
|
839
|
+
this.a11yChipsEl = null;
|
|
840
|
+
this.cbEl = null;
|
|
686
841
|
// modal plumbing (set by open())
|
|
687
842
|
this.modalScrim = null;
|
|
688
843
|
this.prevFocus = null;
|
|
@@ -701,13 +856,20 @@ var SeatPicker = class _SeatPicker {
|
|
|
701
856
|
currency: options.currency,
|
|
702
857
|
flashOnLiveChange: true,
|
|
703
858
|
colorblindSafe: options.colorblindSafe,
|
|
704
|
-
onSelectionChange: () =>
|
|
859
|
+
onSelectionChange: () => {
|
|
860
|
+
this.syncTray();
|
|
861
|
+
if (this.controller.getSelection().length) this.collapseSectionCard();
|
|
862
|
+
},
|
|
705
863
|
onStatusChange: () => {
|
|
706
864
|
this.syncPrices();
|
|
707
865
|
this.evictTakenSelections();
|
|
866
|
+
this.detectBooked();
|
|
867
|
+
this.refreshMinimap();
|
|
708
868
|
},
|
|
709
869
|
onHoldExpired: () => {
|
|
710
870
|
this.hold = null;
|
|
871
|
+
this.handedOff = false;
|
|
872
|
+
this.bookedShown = false;
|
|
711
873
|
this.stopHoldTimer();
|
|
712
874
|
this.gaQty.clear();
|
|
713
875
|
this.toast((0, import_core2.t)("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.");
|
|
@@ -721,6 +883,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
721
883
|
onViewChange: () => {
|
|
722
884
|
this.reanchorConfirm();
|
|
723
885
|
this.syncRung();
|
|
886
|
+
this.drawMinimapRect();
|
|
887
|
+
this.sectionCardOnView();
|
|
724
888
|
},
|
|
725
889
|
// Tapped-section glide-in → surface (or clear) the section-summary card.
|
|
726
890
|
onSectionFocus: (summary) => this.showSectionCard(summary),
|
|
@@ -800,7 +964,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
800
964
|
<div class="sl-body">
|
|
801
965
|
<div class="sl-map">
|
|
802
966
|
<div class="sl-map-host" data-ref="map"></div>
|
|
803
|
-
<div class="sl-zoom">
|
|
967
|
+
<div class="sl-zoom" data-ref="zoom">
|
|
804
968
|
<button type="button" aria-label="Zoom in" data-ref="zin">+</button>
|
|
805
969
|
<button type="button" aria-label="Zoom out" data-ref="zout">\u2212</button>
|
|
806
970
|
<button type="button" aria-label="Fit to screen" data-ref="zfit">
|
|
@@ -810,7 +974,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
810
974
|
<div class="sl-boot" data-ref="boot"><span class="sl-boot-spin"></span>Loading seat map\u2026</div>
|
|
811
975
|
<div class="sl-toast" data-ref="toast" role="status" aria-live="polite"></div>
|
|
812
976
|
</div>
|
|
813
|
-
<div class="sl-side">
|
|
977
|
+
<div class="sl-side" data-ref="side">
|
|
978
|
+
<div class="sl-sheet-head" data-ref="sheetHead">
|
|
979
|
+
<div class="sl-sheet-grab"></div>
|
|
980
|
+
<div class="sl-sheet-peek" data-ref="peek"></div>
|
|
981
|
+
</div>
|
|
982
|
+
<div class="sl-sec sl-filtersec" data-ref="filtersSec">Filters</div>
|
|
983
|
+
<div class="sl-filters" data-ref="filters"></div>
|
|
814
984
|
<div class="sl-sec" data-ref="pricesSec">Prices</div>
|
|
815
985
|
<div class="sl-prices" data-ref="prices"></div>
|
|
816
986
|
<div class="sl-sec">Your seats</div>
|
|
@@ -825,14 +995,55 @@ var SeatPicker = class _SeatPicker {
|
|
|
825
995
|
this.els[el.dataset.ref] = el;
|
|
826
996
|
});
|
|
827
997
|
this.mapHost = this.els.map;
|
|
828
|
-
|
|
998
|
+
const applyLayout = () => {
|
|
829
999
|
const w = root.clientWidth;
|
|
830
|
-
|
|
831
|
-
|
|
1000
|
+
if (w <= 0) return;
|
|
1001
|
+
const next = w < 640 ? "narrow" : "wide";
|
|
1002
|
+
if (root.dataset.layout === next) return;
|
|
1003
|
+
root.dataset.layout = next;
|
|
1004
|
+
if (next === "narrow" && !root.dataset.sheet) root.dataset.sheet = "peek";
|
|
1005
|
+
this.dockLayoutChrome();
|
|
1006
|
+
};
|
|
1007
|
+
this.ro = new ResizeObserver(applyLayout);
|
|
832
1008
|
this.ro.observe(root);
|
|
1009
|
+
applyLayout();
|
|
1010
|
+
requestAnimationFrame(applyLayout);
|
|
833
1011
|
this.els.zin.addEventListener("click", () => this.controller.zoomIn());
|
|
834
1012
|
this.els.zout.addEventListener("click", () => this.controller.zoomOut());
|
|
835
1013
|
this.els.zfit.addEventListener("click", () => this.controller.zoomToFit());
|
|
1014
|
+
const head = this.els.sheetHead;
|
|
1015
|
+
if (head) {
|
|
1016
|
+
const setSheet = (open) => {
|
|
1017
|
+
root.dataset.sheet = open ? "open" : "peek";
|
|
1018
|
+
};
|
|
1019
|
+
let startY = 0;
|
|
1020
|
+
let swiped = false;
|
|
1021
|
+
let tracking = false;
|
|
1022
|
+
head.addEventListener("pointerdown", (e) => {
|
|
1023
|
+
tracking = true;
|
|
1024
|
+
swiped = false;
|
|
1025
|
+
startY = e.clientY;
|
|
1026
|
+
head.setPointerCapture?.(e.pointerId);
|
|
1027
|
+
});
|
|
1028
|
+
head.addEventListener("pointermove", (e) => {
|
|
1029
|
+
if (!tracking || swiped) return;
|
|
1030
|
+
const dy = e.clientY - startY;
|
|
1031
|
+
if (dy < -18) {
|
|
1032
|
+
setSheet(true);
|
|
1033
|
+
swiped = true;
|
|
1034
|
+
} else if (dy > 18) {
|
|
1035
|
+
setSheet(false);
|
|
1036
|
+
swiped = true;
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
head.addEventListener("pointerup", (e) => {
|
|
1040
|
+
if (tracking && !swiped && Math.abs(e.clientY - startY) < 6) {
|
|
1041
|
+
if (!e.target.closest(".sl-seccard")) setSheet(root.dataset.sheet !== "open");
|
|
1042
|
+
}
|
|
1043
|
+
tracking = false;
|
|
1044
|
+
head.releasePointerCapture?.(e.pointerId);
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
836
1047
|
this.tipEl = document.createElement("div");
|
|
837
1048
|
this.tipEl.setAttribute("role", "tooltip");
|
|
838
1049
|
this.tipEl.style.cssText = "position:absolute;z-index:7;pointer-events:none;display:none;max-width:240px;background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:10px;padding:9px 12px;font-size:12px;line-height:1.45;";
|
|
@@ -859,13 +1070,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
859
1070
|
return this;
|
|
860
1071
|
}
|
|
861
1072
|
this.els.boot.remove();
|
|
1073
|
+
this.buildRegions();
|
|
1074
|
+
this.regions["bottom-right"].appendChild(this.els.zoom);
|
|
1075
|
+
this.regions["bottom-center"].appendChild(this.els.toast);
|
|
862
1076
|
if (info.mode === "test") {
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
this.
|
|
868
|
-
this.els.map.appendChild(ribbon);
|
|
1077
|
+
const badge = document.createElement("div");
|
|
1078
|
+
badge.className = "sl-testbadge";
|
|
1079
|
+
badge.textContent = (0, import_core2.t)("picker.testMode");
|
|
1080
|
+
badge.setAttribute("aria-label", (0, import_core2.t)("picker.testMode"));
|
|
1081
|
+
this.regions["top-right"].appendChild(badge);
|
|
869
1082
|
}
|
|
870
1083
|
const chartTheme = this.controller.doc?.theme;
|
|
871
1084
|
Object.entries(resolveTokens(chartTheme, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
|
|
@@ -889,7 +1102,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
889
1102
|
const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
|
|
890
1103
|
const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-f="${key}">${label}</button>`;
|
|
891
1104
|
chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
|
|
892
|
-
this.
|
|
1105
|
+
this.regions["top-left"].appendChild(chips);
|
|
1106
|
+
this.a11yChipsEl = chips;
|
|
893
1107
|
chips.querySelectorAll("button").forEach((btn) => {
|
|
894
1108
|
btn.addEventListener("click", () => {
|
|
895
1109
|
const f = btn.dataset.f;
|
|
@@ -901,6 +1115,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
901
1115
|
}
|
|
902
1116
|
const cb = document.createElement("button");
|
|
903
1117
|
cb.type = "button";
|
|
1118
|
+
cb.className = "sl-cbbtn";
|
|
1119
|
+
this.cbEl = cb;
|
|
904
1120
|
cb.setAttribute("aria-label", "Toggle colorblind-friendly colors");
|
|
905
1121
|
cb.setAttribute("aria-pressed", String(!!this.opts.colorblindSafe));
|
|
906
1122
|
cb.innerHTML = '<svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>';
|
|
@@ -916,10 +1132,314 @@ var SeatPicker = class _SeatPicker {
|
|
|
916
1132
|
this.srEl.setAttribute("aria-live", "polite");
|
|
917
1133
|
root.appendChild(this.srEl);
|
|
918
1134
|
this.buildArenaChrome();
|
|
1135
|
+
this.buildMinimap();
|
|
1136
|
+
this.buildPriceFilter();
|
|
1137
|
+
this.buildExtendPrompt();
|
|
1138
|
+
this.buildBookedOverlay();
|
|
1139
|
+
this.dockLayoutChrome();
|
|
919
1140
|
this.syncPrices();
|
|
920
1141
|
this.syncTray();
|
|
921
1142
|
return this;
|
|
922
1143
|
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Move layout-dependent chrome between its wide dock (map regions / zoom
|
|
1146
|
+
* column) and its narrow dock (the sheet's consolidated Filters row), and
|
|
1147
|
+
* re-render the section card in the form the layout wants (docked card/pill
|
|
1148
|
+
* on wide, sheet strip on narrow). Runs on every layout flip + once post-render.
|
|
1149
|
+
*/
|
|
1150
|
+
dockLayoutChrome() {
|
|
1151
|
+
const narrow = this.root?.dataset.layout === "narrow";
|
|
1152
|
+
const filters = this.els.filters;
|
|
1153
|
+
if (filters) {
|
|
1154
|
+
if (narrow) {
|
|
1155
|
+
if (this.a11yChipsEl) filters.appendChild(this.a11yChipsEl);
|
|
1156
|
+
if (this.cbEl) filters.appendChild(this.cbEl);
|
|
1157
|
+
} else {
|
|
1158
|
+
if (this.a11yChipsEl) this.regions["top-left"]?.appendChild(this.a11yChipsEl);
|
|
1159
|
+
if (this.cbEl) this.els.zoom?.appendChild(this.cbEl);
|
|
1160
|
+
}
|
|
1161
|
+
const has = narrow && filters.children.length > 0;
|
|
1162
|
+
filters.classList.toggle("has", has);
|
|
1163
|
+
this.els.filtersSec?.classList.toggle("has", has);
|
|
1164
|
+
}
|
|
1165
|
+
if (this.lastSection) this.renderSectionCard(this.lastSection);
|
|
1166
|
+
}
|
|
1167
|
+
/** The "Need more time?" prompt shown in the hold's final EXTEND_PROMPT_MS. */
|
|
1168
|
+
buildExtendPrompt() {
|
|
1169
|
+
const el = document.createElement("div");
|
|
1170
|
+
el.className = "sl-extend";
|
|
1171
|
+
el.setAttribute("role", "status");
|
|
1172
|
+
el.innerHTML = `<span class="sl-extend-txt" data-ref="extendTxt"></span><button type="button" class="sl-extend-btn" data-ref="extendBtn"></button>`;
|
|
1173
|
+
(this.regions["bottom-center"] ?? this.els.map).appendChild(el);
|
|
1174
|
+
this.extendEl = el;
|
|
1175
|
+
this.els.extendTxt = el.querySelector('[data-ref="extendTxt"]');
|
|
1176
|
+
this.els.extendBtn = el.querySelector('[data-ref="extendBtn"]');
|
|
1177
|
+
this.els.extendBtn.textContent = "Add time";
|
|
1178
|
+
this.els.extendBtn.addEventListener("click", () => void this.handleExtend());
|
|
1179
|
+
}
|
|
1180
|
+
/** Success overlay + onBooked fire when the held seats settle to booked. */
|
|
1181
|
+
buildBookedOverlay() {
|
|
1182
|
+
const el = document.createElement("div");
|
|
1183
|
+
el.className = "sl-booked";
|
|
1184
|
+
el.setAttribute("role", "status");
|
|
1185
|
+
el.setAttribute("aria-live", "polite");
|
|
1186
|
+
el.innerHTML = `<div class="sl-booked-badge"><svg viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg></div><div class="sl-booked-title">You're all set</div><div class="sl-booked-sub" data-ref="bookedSub"></div>`;
|
|
1187
|
+
this.root.appendChild(el);
|
|
1188
|
+
this.bookedEl = el;
|
|
1189
|
+
this.els.bookedSub = el.querySelector('[data-ref="bookedSub"]');
|
|
1190
|
+
}
|
|
1191
|
+
// ---- Feature 6: chrome anchor regions -------------------------------------
|
|
1192
|
+
/**
|
|
1193
|
+
* Create the positioned flex containers that own every persistent map overlay.
|
|
1194
|
+
* Appended once after controller.render(); each chrome piece is then appended
|
|
1195
|
+
* INTO its region and flows within it, so nothing free-floats over anything
|
|
1196
|
+
* else. Regions carve the map into non-overlapping zones (top strip split into
|
|
1197
|
+
* left/center/right, left rail, and the three used corners).
|
|
1198
|
+
*/
|
|
1199
|
+
buildRegions() {
|
|
1200
|
+
if (!this.els.map) return;
|
|
1201
|
+
const REGIONS = ["top-left", "top-center", "top-right", "left-rail", "bottom-left", "bottom-center", "bottom-right"];
|
|
1202
|
+
for (const region of REGIONS) {
|
|
1203
|
+
const el = document.createElement("div");
|
|
1204
|
+
el.className = "sl-anchor";
|
|
1205
|
+
el.dataset.region = region;
|
|
1206
|
+
this.els.map.appendChild(el);
|
|
1207
|
+
this.regions[region] = el;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
// ---- F3 minimap -----------------------------------------------------------
|
|
1211
|
+
/** Read a resolved --sl-* token value (canvas needs a real color, not var()). */
|
|
1212
|
+
cssVar(name) {
|
|
1213
|
+
return this.root ? getComputedStyle(this.root).getPropertyValue(name).trim() : "";
|
|
1214
|
+
}
|
|
1215
|
+
/** Section-bearing objects on the active floor (single-floor → doc.objects). */
|
|
1216
|
+
activeFloorObjects() {
|
|
1217
|
+
const doc = this.controller.doc;
|
|
1218
|
+
if (!doc) return [];
|
|
1219
|
+
const floors = doc.floors;
|
|
1220
|
+
if (floors?.length) {
|
|
1221
|
+
const id = this.controller.getActiveFloorId();
|
|
1222
|
+
return (floors.find((f) => f.id === id) ?? floors[0]).objects ?? [];
|
|
1223
|
+
}
|
|
1224
|
+
return doc.objects ?? [];
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Build the overview minimap: a static venue thumbnail (section outlines, or
|
|
1228
|
+
* seat dots when the chart has no sections) with the live viewport rectangle
|
|
1229
|
+
* drawn on top. The rect tracks pan/zoom via the constructor's onViewChange.
|
|
1230
|
+
*/
|
|
1231
|
+
buildMinimap() {
|
|
1232
|
+
const vp = this.controller.getViewport();
|
|
1233
|
+
if (!vp || !this.els.map) return;
|
|
1234
|
+
const b = vp.bounds;
|
|
1235
|
+
if (!(b.width > 0 && b.height > 0)) return;
|
|
1236
|
+
const MAXW = 158;
|
|
1237
|
+
const MAXH = 118;
|
|
1238
|
+
const PAD = 6;
|
|
1239
|
+
const aspect = b.width / Math.max(1, b.height);
|
|
1240
|
+
let w = MAXW;
|
|
1241
|
+
let h = Math.round(MAXW / aspect);
|
|
1242
|
+
if (h > MAXH) {
|
|
1243
|
+
h = MAXH;
|
|
1244
|
+
w = Math.round(MAXH * aspect);
|
|
1245
|
+
}
|
|
1246
|
+
w = Math.max(64, w);
|
|
1247
|
+
h = Math.max(48, h);
|
|
1248
|
+
const dpr = Math.min(2, window.devicePixelRatio || 1);
|
|
1249
|
+
const wrap = document.createElement("div");
|
|
1250
|
+
wrap.className = "sl-minimap";
|
|
1251
|
+
wrap.setAttribute("aria-hidden", "true");
|
|
1252
|
+
const canvas = document.createElement("canvas");
|
|
1253
|
+
canvas.width = Math.round(w * dpr);
|
|
1254
|
+
canvas.height = Math.round(h * dpr);
|
|
1255
|
+
canvas.style.width = `${w}px`;
|
|
1256
|
+
canvas.style.height = `${h}px`;
|
|
1257
|
+
wrap.appendChild(canvas);
|
|
1258
|
+
(this.regions["bottom-left"] ?? this.els.map).appendChild(wrap);
|
|
1259
|
+
this.miniEl = wrap;
|
|
1260
|
+
this.miniCanvas = canvas;
|
|
1261
|
+
const scale = Math.min((w - PAD * 2) / Math.max(1, b.width), (h - PAD * 2) / Math.max(1, b.height)) * dpr;
|
|
1262
|
+
const offX = (w * dpr - b.width * scale) / 2 - b.x * scale;
|
|
1263
|
+
const offY = (h * dpr - b.height * scale) / 2 - b.y * scale;
|
|
1264
|
+
this.miniTf = { scale, offX, offY, dpr };
|
|
1265
|
+
const base = document.createElement("canvas");
|
|
1266
|
+
base.width = canvas.width;
|
|
1267
|
+
base.height = canvas.height;
|
|
1268
|
+
this.miniBase = base;
|
|
1269
|
+
wrap.addEventListener("click", (e) => this.minimapJump(e));
|
|
1270
|
+
this.drawMinimapStatic();
|
|
1271
|
+
this.drawMinimapRect();
|
|
1272
|
+
}
|
|
1273
|
+
/** Repaint the static overview + rect (floor switch, live open/close). */
|
|
1274
|
+
refreshMinimap() {
|
|
1275
|
+
if (!this.miniBase) return;
|
|
1276
|
+
this.drawMinimapStatic();
|
|
1277
|
+
this.drawMinimapRect();
|
|
1278
|
+
}
|
|
1279
|
+
/** Paint the venue overview into the offscreen base canvas. */
|
|
1280
|
+
drawMinimapStatic() {
|
|
1281
|
+
const base = this.miniBase;
|
|
1282
|
+
const tf = this.miniTf;
|
|
1283
|
+
const doc = this.controller.doc;
|
|
1284
|
+
if (!base || !tf || !doc) return;
|
|
1285
|
+
const ctx = base.getContext("2d");
|
|
1286
|
+
if (!ctx) return;
|
|
1287
|
+
ctx.clearRect(0, 0, base.width, base.height);
|
|
1288
|
+
const fx = (x) => x * tf.scale + tf.offX;
|
|
1289
|
+
const fy = (y) => y * tf.scale + tf.offY;
|
|
1290
|
+
const line = this.cssVar("--sl-line") || "rgba(139,147,167,.5)";
|
|
1291
|
+
const muted = this.cssVar("--sl-muted") || "#8b93a7";
|
|
1292
|
+
const accent = this.cssVar("--sl-accent") || "#6e7bff";
|
|
1293
|
+
const zoneColor = new Map((doc.zones ?? []).map((z) => [z.id, z.color]));
|
|
1294
|
+
let drewSection = false;
|
|
1295
|
+
for (const o of this.activeFloorObjects()) {
|
|
1296
|
+
if (o.type !== "section" || !o.outline || o.outline.length < 3) continue;
|
|
1297
|
+
drewSection = true;
|
|
1298
|
+
const closed = this.controller.isSectionClosed(o.id);
|
|
1299
|
+
const fill = closed ? muted : o.color ?? (o.zone && zoneColor.get(o.zone)) ?? accent;
|
|
1300
|
+
ctx.beginPath();
|
|
1301
|
+
o.outline.forEach((p, i) => i === 0 ? ctx.moveTo(fx(p.x), fy(p.y)) : ctx.lineTo(fx(p.x), fy(p.y)));
|
|
1302
|
+
ctx.closePath();
|
|
1303
|
+
ctx.globalAlpha = closed ? 0.26 : 0.42;
|
|
1304
|
+
ctx.fillStyle = fill;
|
|
1305
|
+
ctx.fill();
|
|
1306
|
+
ctx.globalAlpha = 0.85;
|
|
1307
|
+
ctx.lineWidth = Math.max(1, tf.dpr);
|
|
1308
|
+
ctx.strokeStyle = line;
|
|
1309
|
+
ctx.stroke();
|
|
1310
|
+
}
|
|
1311
|
+
ctx.globalAlpha = 1;
|
|
1312
|
+
if (!drewSection) {
|
|
1313
|
+
const r = Math.max(1, tf.dpr);
|
|
1314
|
+
for (const seat of (0, import_core2.expandChart)(doc)) {
|
|
1315
|
+
const cat = doc.categories.find((c) => c.key === seat.categoryKey);
|
|
1316
|
+
ctx.fillStyle = cat?.color ?? accent;
|
|
1317
|
+
ctx.beginPath();
|
|
1318
|
+
ctx.arc(fx(seat.x), fy(seat.y), r, 0, Math.PI * 2);
|
|
1319
|
+
ctx.fill();
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
/** Blit the base overview, then stroke the current viewport rectangle on top. */
|
|
1324
|
+
drawMinimapRect() {
|
|
1325
|
+
const canvas = this.miniCanvas;
|
|
1326
|
+
const base = this.miniBase;
|
|
1327
|
+
const tf = this.miniTf;
|
|
1328
|
+
if (!canvas || !base || !tf) return;
|
|
1329
|
+
const ctx = canvas.getContext("2d");
|
|
1330
|
+
if (!ctx) return;
|
|
1331
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
1332
|
+
ctx.drawImage(base, 0, 0);
|
|
1333
|
+
const vp = this.controller.getViewport();
|
|
1334
|
+
if (!vp) return;
|
|
1335
|
+
const v = vp.visible;
|
|
1336
|
+
const x = v.x * tf.scale + tf.offX;
|
|
1337
|
+
const y = v.y * tf.scale + tf.offY;
|
|
1338
|
+
const w = v.width * tf.scale;
|
|
1339
|
+
const h = v.height * tf.scale;
|
|
1340
|
+
const accent = this.cssVar("--sl-accent") || "#f4b740";
|
|
1341
|
+
ctx.save();
|
|
1342
|
+
ctx.globalAlpha = 0.14;
|
|
1343
|
+
ctx.fillStyle = accent;
|
|
1344
|
+
ctx.fillRect(x, y, w, h);
|
|
1345
|
+
ctx.globalAlpha = 1;
|
|
1346
|
+
ctx.lineWidth = Math.max(1.5, tf.dpr * 1.5);
|
|
1347
|
+
ctx.strokeStyle = accent;
|
|
1348
|
+
ctx.strokeRect(x, y, w, h);
|
|
1349
|
+
ctx.restore();
|
|
1350
|
+
}
|
|
1351
|
+
/** Minimap click → focus the section under the point (or overview on a miss). */
|
|
1352
|
+
minimapJump(e) {
|
|
1353
|
+
const canvas = this.miniCanvas;
|
|
1354
|
+
const tf = this.miniTf;
|
|
1355
|
+
if (!canvas || !tf) return;
|
|
1356
|
+
const r = canvas.getBoundingClientRect();
|
|
1357
|
+
const px = (e.clientX - r.left) * (canvas.width / r.width);
|
|
1358
|
+
const py = (e.clientY - r.top) * (canvas.height / r.height);
|
|
1359
|
+
const wx = (px - tf.offX) / tf.scale;
|
|
1360
|
+
const wy = (py - tf.offY) / tf.scale;
|
|
1361
|
+
for (const o of this.activeFloorObjects()) {
|
|
1362
|
+
if (o.type !== "section" || !o.outline || o.outline.length < 3) continue;
|
|
1363
|
+
if (this.controller.isSectionClosed(o.id)) continue;
|
|
1364
|
+
if (pointInPolygon(wx, wy, o.outline)) {
|
|
1365
|
+
this.controller.focusSection(o.id);
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
this.controller.overview();
|
|
1370
|
+
}
|
|
1371
|
+
// ---- F4 price-band filter -------------------------------------------------
|
|
1372
|
+
/** Effective price of a category (first tier when tiered, else base price). */
|
|
1373
|
+
catPrice(c) {
|
|
1374
|
+
return c.tiers?.length ? c.tiers[0].price : c.price;
|
|
1375
|
+
}
|
|
1376
|
+
/** Derive price bands: one chip per distinct price (≤5), else quantile ranges. */
|
|
1377
|
+
priceBands() {
|
|
1378
|
+
const doc = this.controller.doc;
|
|
1379
|
+
if (!doc) return [];
|
|
1380
|
+
const priced = doc.categories.map((c) => ({ key: c.key, price: this.catPrice(c) })).filter((x) => x.price != null);
|
|
1381
|
+
if (!priced.length) return [];
|
|
1382
|
+
const distinct = [...new Set(priced.map((p) => p.price))].sort((a, b) => a - b);
|
|
1383
|
+
if (distinct.length <= 5) {
|
|
1384
|
+
return distinct.map((price) => ({
|
|
1385
|
+
id: `p${price}`,
|
|
1386
|
+
label: this.money(price),
|
|
1387
|
+
keys: priced.filter((p) => p.price === price).map((p) => p.key),
|
|
1388
|
+
min: price,
|
|
1389
|
+
max: price
|
|
1390
|
+
}));
|
|
1391
|
+
}
|
|
1392
|
+
const chunk = Math.ceil(distinct.length / 4);
|
|
1393
|
+
const bands = [];
|
|
1394
|
+
for (let i = 0; i < distinct.length; i += chunk) {
|
|
1395
|
+
const slice = distinct.slice(i, i + chunk);
|
|
1396
|
+
const lo = slice[0];
|
|
1397
|
+
const hi = slice[slice.length - 1];
|
|
1398
|
+
bands.push({
|
|
1399
|
+
id: `b${i}`,
|
|
1400
|
+
label: lo === hi ? this.money(lo) : `${this.money(lo)}\u2013${this.money(hi)}`,
|
|
1401
|
+
keys: priced.filter((p) => p.price >= lo && p.price <= hi).map((p) => p.key),
|
|
1402
|
+
min: lo,
|
|
1403
|
+
max: hi
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
return bands;
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Build the price-band chip row in the side panel, directly under the "Prices"
|
|
1410
|
+
* header and above the legend it dims. Living in the panel (not a map overlay)
|
|
1411
|
+
* keeps it clear of the top-center rung pills and top-left a11y chips, and it
|
|
1412
|
+
* rides the bottom sheet on narrow layouts. Skipped when there's <2 bands.
|
|
1413
|
+
*/
|
|
1414
|
+
buildPriceFilter() {
|
|
1415
|
+
if (!this.els.prices || !this.els.pricesSec) return;
|
|
1416
|
+
const bands = this.priceBands();
|
|
1417
|
+
if (bands.length < 2) return;
|
|
1418
|
+
const wrap = document.createElement("div");
|
|
1419
|
+
wrap.className = "sl-pricef";
|
|
1420
|
+
wrap.setAttribute("role", "group");
|
|
1421
|
+
wrap.setAttribute("aria-label", "Filter seats by price");
|
|
1422
|
+
const mk = (band, label, on) => `<button type="button" class="sl-chip-f${on ? " on" : ""}" data-band="${band}">${label}</button>`;
|
|
1423
|
+
wrap.innerHTML = mk("all", "All prices", true) + bands.map((bd) => mk(bd.id, bd.label, false)).join("");
|
|
1424
|
+
this.els.pricesSec.after(wrap);
|
|
1425
|
+
this.priceFilterEl = wrap;
|
|
1426
|
+
wrap.querySelectorAll("button").forEach((btn) => {
|
|
1427
|
+
btn.addEventListener("click", () => {
|
|
1428
|
+
wrap.querySelectorAll("button").forEach((b) => b.classList.toggle("on", b === btn));
|
|
1429
|
+
const id = btn.dataset.band;
|
|
1430
|
+
if (id === "all") {
|
|
1431
|
+
this.priceBandKeys = null;
|
|
1432
|
+
this.controller.setCategoryFilter(null);
|
|
1433
|
+
} else {
|
|
1434
|
+
const bd = bands.find((x) => x.id === id);
|
|
1435
|
+
this.priceBandKeys = bd ? new Set(bd.keys) : null;
|
|
1436
|
+
this.controller.setCategoryFilter(bd ? bd.keys : null);
|
|
1437
|
+
}
|
|
1438
|
+
this.syncPrices();
|
|
1439
|
+
if (this.lastSection) this.showSectionCard(this.lastSection);
|
|
1440
|
+
});
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
923
1443
|
// ---- arena / multi-floor chrome -------------------------------------------
|
|
924
1444
|
/** Build the rung pills (charts with sections) and floor switcher (>1 floor). */
|
|
925
1445
|
buildArenaChrome() {
|
|
@@ -948,7 +1468,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
948
1468
|
pills.querySelectorAll("button").forEach((btn) => {
|
|
949
1469
|
btn.addEventListener("click", () => this.controller.setRung(btn.dataset.rung));
|
|
950
1470
|
});
|
|
951
|
-
this.
|
|
1471
|
+
this.regions["top-center"].appendChild(pills);
|
|
952
1472
|
this.rungsEl = pills;
|
|
953
1473
|
this.syncRung();
|
|
954
1474
|
}
|
|
@@ -965,9 +1485,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
965
1485
|
this.showSectionCard(null);
|
|
966
1486
|
this.syncFloors();
|
|
967
1487
|
this.syncRung();
|
|
1488
|
+
this.refreshMinimap();
|
|
968
1489
|
});
|
|
969
1490
|
});
|
|
970
|
-
this.
|
|
1491
|
+
this.regions["left-rail"].appendChild(rail);
|
|
971
1492
|
this.floorsEl = rail;
|
|
972
1493
|
this.syncFloors();
|
|
973
1494
|
}
|
|
@@ -992,27 +1513,105 @@ var SeatPicker = class _SeatPicker {
|
|
|
992
1513
|
}
|
|
993
1514
|
/** Show (or clear, on null) the tapped-section summary card. */
|
|
994
1515
|
showSectionCard(summary) {
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1516
|
+
this.lastSection = summary;
|
|
1517
|
+
this.secCardEl?.remove();
|
|
1518
|
+
this.secCardEl = null;
|
|
1519
|
+
if (!summary) return;
|
|
1520
|
+
this.secCardCollapsed = false;
|
|
1521
|
+
this.secCardShownAt = Date.now();
|
|
1522
|
+
this.renderSectionCard(summary);
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Render the section card in the form the layout + state want: expanded card
|
|
1526
|
+
* or slim pill in the top-center anchor region (wide), or a compact strip in
|
|
1527
|
+
* the sheet head (narrow). Never floats over the seats at the tap point.
|
|
1528
|
+
*/
|
|
1529
|
+
renderSectionCard(summary) {
|
|
1000
1530
|
if (!this.els.map) return;
|
|
1001
1531
|
this.secCardEl?.remove();
|
|
1002
1532
|
const priceLabel = summary.priceMin === summary.priceMax ? this.money(summary.priceMin) : `${this.money(summary.priceMin)}\u2013${this.money(summary.priceMax)}`;
|
|
1533
|
+
const leftLabel = (0, import_core2.tCount)("picker.seatsLeftInSection", summary.seatsLeft);
|
|
1534
|
+
const xBtn = `<button type="button" class="sl-seccard-x" aria-label="${(0, import_core2.t)("picker.closeSectionSummary")}">\u2715</button>`;
|
|
1003
1535
|
const card = document.createElement("div");
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1536
|
+
const narrow = this.root?.dataset.layout === "narrow";
|
|
1537
|
+
if (narrow) {
|
|
1538
|
+
card.className = "sl-seccard strip on";
|
|
1539
|
+
card.setAttribute("role", "status");
|
|
1540
|
+
card.innerHTML = `<span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span><span class="sl-seccard-left">${leftLabel}</span>` + (summary.categories.length ? `<span class="sl-seccard-price">${priceLabel}</span>` : "") + xBtn;
|
|
1541
|
+
card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
|
|
1542
|
+
(this.els.sheetHead ?? this.els.side ?? this.els.map).appendChild(card);
|
|
1543
|
+
} else if (this.secCardCollapsed) {
|
|
1544
|
+
card.className = "sl-seccard mini on";
|
|
1545
|
+
card.setAttribute("role", "button");
|
|
1546
|
+
card.setAttribute("aria-label", (0, import_core2.t)("picker.sectionSummaryAria", { label: summary.label }));
|
|
1547
|
+
card.innerHTML = `<span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span><span class="sl-seccard-left">${leftLabel}</span>` + xBtn;
|
|
1548
|
+
card.addEventListener("click", (e) => {
|
|
1549
|
+
if (e.target.closest(".sl-seccard-x")) return;
|
|
1550
|
+
this.secCardCollapsed = false;
|
|
1551
|
+
this.secCardShownAt = Date.now();
|
|
1552
|
+
this.renderSectionCard(summary);
|
|
1553
|
+
});
|
|
1554
|
+
card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
|
|
1555
|
+
(this.regions["top-center"] ?? this.els.map).appendChild(card);
|
|
1556
|
+
} else {
|
|
1557
|
+
card.className = "sl-seccard on";
|
|
1558
|
+
card.setAttribute("role", "dialog");
|
|
1559
|
+
card.setAttribute("aria-label", (0, import_core2.t)("picker.sectionSummaryAria", { label: summary.label }));
|
|
1560
|
+
const mix = summary.categories.map((c) => {
|
|
1561
|
+
const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
|
|
1562
|
+
return `<span class="sl-seccard-mix-item${dim ? " sl-dim" : ""}"><span class="sl-seccard-mix-dot" style="background:${c.color}"></span>${c.label} <span class="sl-seccard-mix-price">${this.money(c.price)}</span></span>`;
|
|
1563
|
+
}).join("");
|
|
1564
|
+
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>`;
|
|
1565
|
+
card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
|
|
1566
|
+
card.querySelector(".sl-seccard-overview").addEventListener("click", () => this.controller.overview());
|
|
1567
|
+
(this.regions["top-center"] ?? this.els.map).appendChild(card);
|
|
1568
|
+
}
|
|
1014
1569
|
this.secCardEl = card;
|
|
1015
1570
|
}
|
|
1571
|
+
/** Collapse the expanded card to its slim pill (seat-picking started). */
|
|
1572
|
+
collapseSectionCard() {
|
|
1573
|
+
if (!this.secCardEl || this.secCardCollapsed || !this.lastSection) return;
|
|
1574
|
+
if (this.root?.dataset.layout === "narrow") return;
|
|
1575
|
+
this.secCardCollapsed = true;
|
|
1576
|
+
this.renderSectionCard(this.lastSection);
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* onViewChange hook for the card. The focus glide's own settle (within the
|
|
1580
|
+
* grace window) enforces the ~25% coverage rule with the FINAL viewport; any
|
|
1581
|
+
* later pan/zoom means seat-picking has begun → collapse to the pill.
|
|
1582
|
+
*/
|
|
1583
|
+
sectionCardOnView() {
|
|
1584
|
+
if (!this.secCardEl || this.secCardCollapsed || !this.lastSection) return;
|
|
1585
|
+
if (this.root?.dataset.layout === "narrow") return;
|
|
1586
|
+
if (Date.now() - this.secCardShownAt < 1400) {
|
|
1587
|
+
if (this.sectionCardCoverage() > 0.25) this.collapseSectionCard();
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
this.collapseSectionCard();
|
|
1591
|
+
}
|
|
1592
|
+
/** Fraction of the focused section's on-screen bbox covered by the card. */
|
|
1593
|
+
sectionCardCoverage() {
|
|
1594
|
+
const card = this.secCardEl;
|
|
1595
|
+
const sec = this.lastSection;
|
|
1596
|
+
if (!card || !sec || !this.els.map) return 0;
|
|
1597
|
+
const outline = this.activeFloorObjects().find((o) => o.type === "section" && o.id === sec.id)?.outline;
|
|
1598
|
+
if (!outline || outline.length < 3) return 0;
|
|
1599
|
+
const pts = outline.map((p) => this.controller.worldToScreen(p));
|
|
1600
|
+
const xs = pts.map((p) => p.x);
|
|
1601
|
+
const ys = pts.map((p) => p.y);
|
|
1602
|
+
const bx = Math.min(...xs);
|
|
1603
|
+
const by = Math.min(...ys);
|
|
1604
|
+
const bw = Math.max(...xs) - bx;
|
|
1605
|
+
const bh = Math.max(...ys) - by;
|
|
1606
|
+
if (bw <= 0 || bh <= 0) return 0;
|
|
1607
|
+
const mapR = this.els.map.getBoundingClientRect();
|
|
1608
|
+
const cr = card.getBoundingClientRect();
|
|
1609
|
+
const cx = cr.left - mapR.left;
|
|
1610
|
+
const cy = cr.top - mapR.top;
|
|
1611
|
+
const ox = Math.max(0, Math.min(cx + cr.width, bx + bw) - Math.max(cx, bx));
|
|
1612
|
+
const oy = Math.max(0, Math.min(cy + cr.height, by + bh) - Math.max(cy, by));
|
|
1613
|
+
return ox * oy / (bw * bh);
|
|
1614
|
+
}
|
|
1016
1615
|
/** aria-live readout when keyboard focus lands on a seat. */
|
|
1017
1616
|
announceSeat(seat) {
|
|
1018
1617
|
if (!this.srEl) return;
|
|
@@ -1187,7 +1786,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
1187
1786
|
const left = this.controller.categoryAvailability();
|
|
1188
1787
|
this.els.prices.innerHTML = doc.categories.map((c) => {
|
|
1189
1788
|
const price = c.tiers?.length ? c.tiers[0].price : c.price;
|
|
1190
|
-
|
|
1789
|
+
const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
|
|
1790
|
+
return `<div class="sl-price-row${dim ? " sl-dim" : ""}" data-cat="${c.key}"><span class="sl-dot" style="background:${c.color}"></span><span class="sl-price-label">${c.label}</span><span class="sl-price-left">${left[c.key] ?? 0} left</span>` + (price != null ? `<span class="sl-price-amt">${this.money(price)}</span>` : "") + `</div>`;
|
|
1191
1791
|
}).join("");
|
|
1192
1792
|
this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
|
|
1193
1793
|
row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
|
|
@@ -1293,12 +1893,26 @@ var SeatPicker = class _SeatPicker {
|
|
|
1293
1893
|
const cta = this.els.cta;
|
|
1294
1894
|
cta.disabled = count === 0;
|
|
1295
1895
|
cta.textContent = this.hold ? "Continue to checkout" : count ? "Hold seats & checkout" : "Select seats";
|
|
1896
|
+
if (this.els.peek) {
|
|
1897
|
+
if (count) {
|
|
1898
|
+
this.els.peek.innerHTML = `<span>${count} ${count === 1 ? "ticket" : "tickets"} \xB7 ${this.money(total)}</span><span class="go">${this.hold ? "Continue \u2192" : "Review \u2192"}</span>`;
|
|
1899
|
+
} else {
|
|
1900
|
+
const prices = (this.controller.doc?.categories ?? []).map((c) => this.catPrice(c)).filter((p) => p != null);
|
|
1901
|
+
this.els.peek.innerHTML = (prices.length ? `<span>From ${this.money(Math.min(...prices))}</span>` : "<span>Pick your seats</span>") + `<span class="sub">\xB7 Best available</span><span class="go">\u2191</span>`;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
if (this.root?.dataset.layout === "narrow" && count > 0 && this.lastTrayCount === 0) {
|
|
1905
|
+
this.root.dataset.sheet = "open";
|
|
1906
|
+
}
|
|
1907
|
+
this.lastTrayCount = count;
|
|
1296
1908
|
this.opts.onSelectionChange?.(seats);
|
|
1297
1909
|
}
|
|
1298
1910
|
async handleCta() {
|
|
1299
1911
|
const cta = this.els.cta;
|
|
1300
1912
|
if (this.hold && !this.controller.getSelection().some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
|
|
1301
|
-
|
|
1913
|
+
const seats = this.hold.seats ?? this.controller.getSelection();
|
|
1914
|
+
this.handedOff = true;
|
|
1915
|
+
this.opts.onCheckout?.(this.hold, seats, this.buildHandoff(this.hold));
|
|
1302
1916
|
return;
|
|
1303
1917
|
}
|
|
1304
1918
|
cta.disabled = true;
|
|
@@ -1321,8 +1935,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
1321
1935
|
return;
|
|
1322
1936
|
}
|
|
1323
1937
|
this.hold = hold;
|
|
1938
|
+
this.handedOff = true;
|
|
1324
1939
|
this.startHoldTimer(hold.expiresAt);
|
|
1325
|
-
this.opts.onCheckout?.(hold, chosenSeats.length ? chosenSeats : hold.seats ?? []);
|
|
1940
|
+
this.opts.onCheckout?.(hold, chosenSeats.length ? chosenSeats : hold.seats ?? [], this.buildHandoff(hold));
|
|
1326
1941
|
} catch (err) {
|
|
1327
1942
|
this.opts.onError?.(err);
|
|
1328
1943
|
this.toast("One or more seats were just taken. Please pick again.");
|
|
@@ -1332,13 +1947,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
1332
1947
|
}
|
|
1333
1948
|
startHoldTimer(expiresAt) {
|
|
1334
1949
|
this.stopHoldTimer();
|
|
1950
|
+
this.holdExpiresAt = expiresAt;
|
|
1335
1951
|
const pill = this.els.hold;
|
|
1336
1952
|
const tick = () => {
|
|
1337
|
-
const ms = Math.max(0,
|
|
1953
|
+
const ms = Math.max(0, this.holdExpiresAt - Date.now());
|
|
1338
1954
|
const m = Math.floor(ms / 6e4);
|
|
1339
1955
|
const s = String(Math.floor(ms % 6e4 / 1e3)).padStart(2, "0");
|
|
1340
1956
|
pill.textContent = `Held ${m}:${s}`;
|
|
1341
1957
|
pill.classList.add("on");
|
|
1958
|
+
this.setExtendPrompt(ms > 0 && ms <= EXTEND_PROMPT_MS, ms);
|
|
1342
1959
|
if (ms <= 0) this.stopHoldTimer();
|
|
1343
1960
|
};
|
|
1344
1961
|
tick();
|
|
@@ -1348,6 +1965,83 @@ var SeatPicker = class _SeatPicker {
|
|
|
1348
1965
|
if (this.holdTimer) clearInterval(this.holdTimer);
|
|
1349
1966
|
this.holdTimer = null;
|
|
1350
1967
|
this.els.hold?.classList.remove("on");
|
|
1968
|
+
this.setExtendPrompt(false, 0);
|
|
1969
|
+
}
|
|
1970
|
+
/** Show/refresh (or hide) the "Need more time?" prompt with the live seconds left. */
|
|
1971
|
+
setExtendPrompt(show, ms) {
|
|
1972
|
+
if (!this.extendEl) return;
|
|
1973
|
+
if (show && this.controller.currentHold() && !this.bookedShown) {
|
|
1974
|
+
const secs = Math.ceil(ms / 1e3);
|
|
1975
|
+
this.els.extendTxt.innerHTML = `Your seats are held for <b>0:${String(secs).padStart(2, "0")}</b>. Need more time?`;
|
|
1976
|
+
this.extendEl.classList.add("on");
|
|
1977
|
+
} else {
|
|
1978
|
+
this.extendEl.classList.remove("on");
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
async handleExtend() {
|
|
1982
|
+
const btn = this.els.extendBtn;
|
|
1983
|
+
btn.disabled = true;
|
|
1984
|
+
const prev = btn.textContent;
|
|
1985
|
+
btn.textContent = "Adding\u2026";
|
|
1986
|
+
try {
|
|
1987
|
+
const h = await this.controller.extendHold(this.opts.holdTtlMs);
|
|
1988
|
+
if (h) {
|
|
1989
|
+
this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
|
|
1990
|
+
this.holdExpiresAt = h.expiresAt;
|
|
1991
|
+
this.extendEl?.classList.remove("on");
|
|
1992
|
+
this.toast("More time added \u2014 your seats are still held.");
|
|
1993
|
+
} else {
|
|
1994
|
+
this.toast("Couldn't add more time \u2014 please head to checkout now.");
|
|
1995
|
+
}
|
|
1996
|
+
} catch (err) {
|
|
1997
|
+
this.opts.onError?.(err);
|
|
1998
|
+
this.toast("Couldn't add more time \u2014 please head to checkout now.");
|
|
1999
|
+
} finally {
|
|
2000
|
+
btn.disabled = false;
|
|
2001
|
+
btn.textContent = prev;
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Fire the booked-confirmation state once the buyer's held seats settle to
|
|
2006
|
+
* booked. The controller clears its own hold the moment every held label reads
|
|
2007
|
+
* 'booked' over the realtime channel (clearBookedHoldIfSettled), and this runs
|
|
2008
|
+
* on the same onStatusChange — so `currentHold() === null` while we still hold
|
|
2009
|
+
* a checkout handoff means "sold", not expired (expiry clears via onHoldExpired
|
|
2010
|
+
* on a different path, which nulls this.hold first).
|
|
2011
|
+
*/
|
|
2012
|
+
detectBooked() {
|
|
2013
|
+
if (this.bookedShown || !this.handedOff || !this.hold) return;
|
|
2014
|
+
if (this.controller.currentHold() !== null) return;
|
|
2015
|
+
this.showBooked();
|
|
2016
|
+
}
|
|
2017
|
+
showBooked() {
|
|
2018
|
+
if (this.bookedShown || !this.hold) return;
|
|
2019
|
+
this.bookedShown = true;
|
|
2020
|
+
const handoff = this.buildHandoff(this.hold);
|
|
2021
|
+
this.stopHoldTimer();
|
|
2022
|
+
const n = handoff.lineItems.reduce((sum, i) => sum + i.quantity, 0);
|
|
2023
|
+
if (this.els.bookedSub) {
|
|
2024
|
+
this.els.bookedSub.innerHTML = `<span class="sl-booked-seats">${n} ${n === 1 ? "ticket" : "tickets"}</span> confirmed. A confirmation is on its way.`;
|
|
2025
|
+
}
|
|
2026
|
+
this.bookedEl?.classList.add("on");
|
|
2027
|
+
this.opts.onBooked?.(handoff);
|
|
2028
|
+
}
|
|
2029
|
+
/** Assemble the stable {@link CheckoutHandoff} from a hold's server line items. */
|
|
2030
|
+
buildHandoff(hold) {
|
|
2031
|
+
const items = hold.items ?? [];
|
|
2032
|
+
const lineItems = items.map((it) => ({
|
|
2033
|
+
label: it.label,
|
|
2034
|
+
objectId: it.objectId,
|
|
2035
|
+
objectType: it.objectType,
|
|
2036
|
+
categoryKey: it.categoryKey,
|
|
2037
|
+
tierId: it.tierId,
|
|
2038
|
+
unitPrice: it.unitPrice,
|
|
2039
|
+
currency: it.currency ?? this.currency,
|
|
2040
|
+
quantity: it.quantity ?? 1
|
|
2041
|
+
}));
|
|
2042
|
+
const currency = lineItems[0]?.currency ?? this.currency;
|
|
2043
|
+
const total = lineItems.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
|
|
2044
|
+
return { holdId: hold.holdId, expiresAt: hold.expiresAt, currency, lineItems, total };
|
|
1351
2045
|
}
|
|
1352
2046
|
toast(msg) {
|
|
1353
2047
|
const el = this.els.toast;
|
|
@@ -1389,6 +2083,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
1389
2083
|
const h = await this.controller.bestAvailable(qty, categoryKey);
|
|
1390
2084
|
if (h) {
|
|
1391
2085
|
this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
|
|
2086
|
+
this.handedOff = false;
|
|
2087
|
+
this.bookedShown = false;
|
|
1392
2088
|
this.startHoldTimer(h.expiresAt);
|
|
1393
2089
|
this.syncTray();
|
|
1394
2090
|
return this.hold;
|
|
@@ -1402,6 +2098,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
1402
2098
|
async release() {
|
|
1403
2099
|
await this.controller.release();
|
|
1404
2100
|
this.hold = null;
|
|
2101
|
+
this.handedOff = false;
|
|
2102
|
+
this.bookedShown = false;
|
|
1405
2103
|
this.stopHoldTimer();
|
|
1406
2104
|
this.gaQty.clear();
|
|
1407
2105
|
this.syncTray();
|