@seatlayer/js 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +626 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +78 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.js +626 -58
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -416,6 +416,14 @@ var import_core2 = require("@seatlayer/core");
|
|
|
416
416
|
var DEFAULT_API_BASE2 = "https://api.seatlayer.io";
|
|
417
417
|
var DEFAULT_MAX_SELECTION2 = 10;
|
|
418
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
|
+
}
|
|
419
427
|
function resolveContainer3(container) {
|
|
420
428
|
if (typeof container === "string") {
|
|
421
429
|
const el = document.querySelector(container);
|
|
@@ -459,12 +467,57 @@ var CSS = `
|
|
|
459
467
|
.sl-map-host{position:absolute;inset:0}
|
|
460
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}
|
|
461
469
|
|
|
462
|
-
/* 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. */
|
|
463
476
|
.sl-picker[data-layout="narrow"] .sl-body{flex-direction:column}
|
|
464
477
|
.sl-picker[data-layout="narrow"] .sl-map{min-height:0;flex:1}
|
|
465
|
-
.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}
|
|
466
482
|
.sl-picker[data-layout="narrow"] .sl-tray{flex:none}
|
|
467
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). The WHOLE
|
|
489
|
+
head is the tap/swipe toggle target (min 44px), so it reads as one control. */
|
|
490
|
+
.sl-sheet-head{display:none;flex-direction:column;justify-content:center;padding:6px 16px 8px;min-height:44px;
|
|
491
|
+
cursor:pointer;touch-action:none;user-select:none;-webkit-user-select:none;flex:none}
|
|
492
|
+
.sl-picker[data-layout="narrow"] .sl-sheet-head{display:flex}
|
|
493
|
+
.sl-sheet-grab{width:36px;height:4px;border-radius:999px;background:var(--sl-muted);opacity:.55;margin:2px auto 7px}
|
|
494
|
+
.sl-sheet-bar{display:flex;align-items:center;gap:10px;min-height:26px}
|
|
495
|
+
.sl-sheet-peek{display:flex;align-items:center;gap:7px;flex:1;min-width:0;font-size:13px;font-weight:700;color:var(--sl-text);
|
|
496
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
497
|
+
.sl-sheet-peek .sub{color:var(--sl-muted);font-weight:600}
|
|
498
|
+
/* collapsed-peek "Continue" affordance: a real accent pill, not plain text */
|
|
499
|
+
.sl-sheet-peek .go{margin-left:auto;flex:none;display:inline-flex;align-items:center;min-height:30px;
|
|
500
|
+
padding:6px 13px;border-radius:999px;background:var(--sl-accent);color:var(--sl-accent-ink);
|
|
501
|
+
font-weight:800;font-size:12.5px}
|
|
502
|
+
/* state chevron: points UP while peeking, rotates to point DOWN when open.
|
|
503
|
+
Base keeps an explicit rotate(0) \u2014 transitioning to/from a bare 'none' leaves
|
|
504
|
+
the value stuck in some engines, so both endpoints must be real transforms. */
|
|
505
|
+
.sl-sheet-chevron{flex:none;display:flex;align-items:center;justify-content:center;color:var(--sl-muted);
|
|
506
|
+
transform:rotate(0deg);transition:transform .22s ease}
|
|
507
|
+
.sl-sheet-chevron svg{width:18px;height:18px;stroke:currentColor;stroke-width:2.4;fill:none;
|
|
508
|
+
stroke-linecap:round;stroke-linejoin:round}
|
|
509
|
+
.sl-picker[data-sheet="open"] .sl-sheet-chevron{transform:rotate(180deg)}
|
|
510
|
+
|
|
511
|
+
/* consolidated Filters row inside the sheet (a11y chips + colorblind toggle
|
|
512
|
+
dock here on narrow; they live on the map / zoom column on wide) */
|
|
513
|
+
.sl-filtersec{display:none}
|
|
514
|
+
.sl-filters{display:none;gap:6px;flex-wrap:wrap;align-items:center;padding:2px 16px 10px}
|
|
515
|
+
.sl-picker[data-layout="narrow"] .sl-filtersec.has{display:block}
|
|
516
|
+
.sl-picker[data-layout="narrow"] .sl-filters.has{display:flex}
|
|
517
|
+
.sl-cbbtn{width:32px;height:32px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
518
|
+
color:var(--sl-text);display:flex;align-items:center;justify-content:center;transition:border-color .15s}
|
|
519
|
+
.sl-cbbtn:hover{border-color:var(--sl-muted)}
|
|
520
|
+
.sl-cbbtn svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
468
521
|
|
|
469
522
|
/* price panel */
|
|
470
523
|
.sl-sec{padding:14px 16px 4px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--sl-muted);font-weight:700}
|
|
@@ -502,24 +555,57 @@ var CSS = `
|
|
|
502
555
|
.sl-foot{padding:12px 16px 14px;border-top:1px solid var(--sl-line);flex:none}
|
|
503
556
|
.sl-total{display:flex;justify-content:space-between;align-items:center;font-size:13px;margin-bottom:10px}
|
|
504
557
|
.sl-total b{font-size:17px;font-variant-numeric:tabular-nums}
|
|
505
|
-
.sl-
|
|
506
|
-
|
|
507
|
-
.
|
|
508
|
-
.sl-cta:
|
|
558
|
+
/* Primary checkout CTA. Scoped under .sl-picker so it OUTWEIGHS the
|
|
559
|
+
'.sl-picker button' reset (0,1,1) \u2014 an unscoped '.sl-cta' (0,1,0) loses to it
|
|
560
|
+
and the button renders as plain text with no accent fill. */
|
|
561
|
+
.sl-picker .sl-cta{display:flex;align-items:center;justify-content:center;width:100%;min-height:44px;
|
|
562
|
+
padding:12px 16px;border-radius:var(--sl-r-sm);font-weight:800;font-size:14px;line-height:1.1;
|
|
563
|
+
background:var(--sl-accent);color:var(--sl-accent-ink);
|
|
564
|
+
transition:filter .15s,background .15s,color .15s,transform .06s}
|
|
565
|
+
.sl-picker .sl-cta:hover{filter:brightness(1.08)}
|
|
566
|
+
.sl-picker .sl-cta:active{transform:translateY(1px);filter:brightness(.94)}
|
|
567
|
+
/* Disabled ("Select seats"): quieter, but still a full-width button shape. */
|
|
568
|
+
.sl-picker .sl-cta:disabled{background:var(--sl-surface);color:var(--sl-muted);opacity:1;
|
|
569
|
+
cursor:not-allowed;filter:none;transform:none}
|
|
570
|
+
|
|
571
|
+
/* Chrome anchor regions (Feature 6) \u2014 every persistent map overlay is APPENDED
|
|
572
|
+
INTO one of these positioned flex containers and flows/stacks within it, so no
|
|
573
|
+
two pieces of chrome free-float on top of each other. Regions never overlap:
|
|
574
|
+
the top strip splits into left/center/right; rails + corners own their edge. */
|
|
575
|
+
.sl-anchor{position:absolute;z-index:5;display:flex;gap:8px;pointer-events:none}
|
|
576
|
+
.sl-anchor > *{pointer-events:auto}
|
|
577
|
+
.sl-anchor[data-region="top-left"]{top:12px;left:12px;flex-wrap:wrap;max-width:38%}
|
|
578
|
+
.sl-anchor[data-region="top-center"]{top:12px;left:50%;transform:translateX(-50%);flex-direction:column;
|
|
579
|
+
align-items:center;max-width:44%}
|
|
580
|
+
.sl-anchor[data-region="top-right"]{top:12px;right:12px;justify-content:flex-end;flex-wrap:wrap;max-width:38%}
|
|
581
|
+
.sl-anchor[data-region="left-rail"]{top:50%;left:12px;transform:translateY(-50%);flex-direction:column;max-width:42%;gap:6px}
|
|
582
|
+
.sl-anchor[data-region="bottom-left"]{left:12px;bottom:12px;flex-direction:column;align-items:flex-start}
|
|
583
|
+
.sl-anchor[data-region="bottom-center"]{left:50%;bottom:14px;transform:translateX(-50%);z-index:9;
|
|
584
|
+
flex-direction:column;align-items:center;gap:8px;max-width:92%}
|
|
585
|
+
.sl-anchor[data-region="bottom-right"]{right:12px;bottom:12px;flex-direction:column;align-items:flex-end;gap:6px}
|
|
586
|
+
/* narrow: tighten the top strip so left/center can't crowd each other */
|
|
587
|
+
.sl-picker[data-layout="narrow"] .sl-anchor[data-region="top-left"]{max-width:30%}
|
|
588
|
+
.sl-picker[data-layout="narrow"] .sl-anchor[data-region="top-center"]{max-width:44%}
|
|
509
589
|
|
|
510
|
-
/*
|
|
511
|
-
.sl-
|
|
590
|
+
/* TEST MODE badge \u2014 a small pill in the top-right region (shrinks on narrow) */
|
|
591
|
+
.sl-testbadge{padding:5px 11px;border-radius:999px;font-size:10px;font-weight:800;letter-spacing:.1em;
|
|
592
|
+
text-transform:uppercase;white-space:nowrap;background:var(--sl-accent);color:var(--sl-accent-ink);
|
|
593
|
+
box-shadow:0 2px 8px rgba(0,0,0,.25)}
|
|
594
|
+
.sl-picker[data-layout="narrow"] .sl-testbadge{padding:3px 8px;font-size:8.5px;letter-spacing:.06em}
|
|
595
|
+
|
|
596
|
+
/* zoom column (flows within the bottom-right region) */
|
|
597
|
+
.sl-zoom{display:flex;flex-direction:column;gap:6px}
|
|
512
598
|
.sl-zoom button{width:36px;height:36px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
513
599
|
color:var(--sl-text);font-size:17px;font-weight:700;display:flex;align-items:center;justify-content:center;transition:border-color .15s}
|
|
514
600
|
.sl-zoom button:hover{border-color:var(--sl-muted)}
|
|
515
601
|
.sl-zoom svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
516
602
|
|
|
517
|
-
/* toast + boot states */
|
|
518
|
-
.sl-toast{
|
|
603
|
+
/* toast + boot states (toast flows in the bottom-center region) */
|
|
604
|
+
.sl-toast{transform:translateY(6px);max-width:100%;
|
|
519
605
|
background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-text);border-radius:999px;padding:9px 16px;
|
|
520
606
|
font-size:12.5px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .2s,transform .2s;white-space:nowrap;
|
|
521
607
|
overflow:hidden;text-overflow:ellipsis}
|
|
522
|
-
.sl-toast.on{opacity:1;transform:
|
|
608
|
+
.sl-toast.on{opacity:1;transform:translateY(0)}
|
|
523
609
|
.sl-boot{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|
524
610
|
gap:10px;background:var(--sl-bg);font-size:13px;font-weight:600;color:var(--sl-muted)}
|
|
525
611
|
.sl-boot-spin{width:24px;height:24px;border-radius:50%;border:3px solid var(--sl-line);border-top-color:var(--sl-accent);
|
|
@@ -529,12 +615,12 @@ var CSS = `
|
|
|
529
615
|
.sl-boot-retry{margin-top:4px;padding:9px 20px;border-radius:var(--sl-r-sm);background:var(--sl-accent);
|
|
530
616
|
color:var(--sl-accent-ink);font-weight:700;font-size:13px}
|
|
531
617
|
|
|
532
|
-
/* "Need more time?" extend prompt (bottom-center
|
|
533
|
-
.sl-extend{
|
|
534
|
-
display:none;align-items:center;gap:12px;max-width:
|
|
618
|
+
/* "Need more time?" extend prompt (flows in the bottom-center region, above the toast) */
|
|
619
|
+
.sl-extend{transform:translateY(6px);
|
|
620
|
+
display:none;align-items:center;gap:12px;max-width:100%;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
535
621
|
color:var(--sl-text);border-radius:14px;padding:10px 12px 10px 16px;box-shadow:0 18px 50px -18px rgba(0,0,0,.6);
|
|
536
622
|
opacity:0;transition:opacity .2s,transform .2s}
|
|
537
|
-
.sl-extend.on{display:flex;opacity:1;transform:
|
|
623
|
+
.sl-extend.on{display:flex;opacity:1;transform:translateY(0)}
|
|
538
624
|
.sl-extend-txt{font-size:12.5px;font-weight:600;line-height:1.35}
|
|
539
625
|
.sl-extend-txt b{font-variant-numeric:tabular-nums}
|
|
540
626
|
.sl-extend-btn{flex:none;padding:8px 14px;border-radius:999px;font-weight:800;font-size:12.5px;
|
|
@@ -553,8 +639,8 @@ var CSS = `
|
|
|
553
639
|
.sl-booked-sub{font-size:13px;color:var(--sl-muted);line-height:1.5;max-width:320px}
|
|
554
640
|
.sl-booked-seats{font-weight:700;color:var(--sl-text)}
|
|
555
641
|
|
|
556
|
-
/* a11y filter chips (
|
|
557
|
-
.sl-chips{
|
|
642
|
+
/* a11y filter chips (flow within the top-left region) */
|
|
643
|
+
.sl-chips{display:flex;gap:6px;flex-wrap:wrap}
|
|
558
644
|
.sl-chip-f{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:999px;font-size:12px;font-weight:700;
|
|
559
645
|
background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-muted);transition:color .15s,border-color .15s}
|
|
560
646
|
.sl-chip-f:hover{color:var(--sl-text)}
|
|
@@ -594,18 +680,18 @@ var CSS = `
|
|
|
594
680
|
.sl-chip .view:hover{color:var(--sl-text)}
|
|
595
681
|
.sl-chip .view svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
596
682
|
|
|
597
|
-
/* arena: LOD rung pills (top-center
|
|
598
|
-
.sl-rungs{
|
|
599
|
-
background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:999px;padding:3px}
|
|
683
|
+
/* arena: LOD rung pills (flow within the top-center region) */
|
|
684
|
+
.sl-rungs{display:none;background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:999px;padding:3px}
|
|
600
685
|
.sl-rungs.on{display:inline-flex;gap:2px}
|
|
601
686
|
.sl-rungs button{padding:6px 13px;border-radius:999px;font-size:10.5px;font-weight:800;letter-spacing:.07em;
|
|
602
687
|
color:var(--sl-muted);white-space:nowrap;transition:color .15s}
|
|
603
688
|
.sl-rungs button:hover{color:var(--sl-text)}
|
|
604
689
|
.sl-rungs button.on{background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
690
|
+
/* narrow: shrink the rung pills so the centered row can't reach the corner regions */
|
|
691
|
+
.sl-picker[data-layout="narrow"] .sl-rungs button{padding:5px 9px;font-size:9px;letter-spacing:.03em}
|
|
605
692
|
|
|
606
|
-
/* multi-floor switcher (
|
|
607
|
-
.sl-floors{
|
|
608
|
-
flex-direction:column;gap:6px;max-width:42%}
|
|
693
|
+
/* multi-floor switcher (flows within the left-rail region) */
|
|
694
|
+
.sl-floors{display:none;flex-direction:column;gap:6px;max-width:100%}
|
|
609
695
|
.sl-floors.on{display:flex}
|
|
610
696
|
.sl-floors button{padding:7px 13px;border-radius:999px;font-size:12px;font-weight:700;background:var(--sl-surface);
|
|
611
697
|
border:1px solid var(--sl-line);color:var(--sl-muted);white-space:nowrap;max-width:100%;overflow:hidden;
|
|
@@ -613,11 +699,25 @@ var CSS = `
|
|
|
613
699
|
.sl-floors button:hover{color:var(--sl-text)}
|
|
614
700
|
.sl-floors button.on{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
|
|
615
701
|
|
|
616
|
-
/* tapped-section summary card
|
|
617
|
-
|
|
618
|
-
|
|
702
|
+
/* tapped-section summary card \u2014 docks INSIDE the top-center anchor region on
|
|
703
|
+
wide (flows below the rung pills, never over them, never floating over the
|
|
704
|
+
seats at the tap point). Auto-collapses to a slim pill once seat-picking
|
|
705
|
+
begins (first seat select, or a pan/zoom after the focus glide); tapping the
|
|
706
|
+
pill re-expands; \u2715 closes in both states. On narrow it renders as a compact
|
|
707
|
+
strip inside the bottom sheet's peek head \u2014 never over the canvas. */
|
|
708
|
+
.sl-seccard{width:250px;max-width:100%;background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:12px;
|
|
619
709
|
padding:12px 14px;box-shadow:0 18px 50px -18px rgba(0,0,0,.6);display:none}
|
|
620
710
|
.sl-seccard.on{display:block}
|
|
711
|
+
/* collapsed pill (wide) */
|
|
712
|
+
.sl-seccard.mini{width:auto;padding:5px 7px 5px 12px;border-radius:999px;cursor:pointer}
|
|
713
|
+
.sl-seccard.mini.on{display:inline-flex;align-items:center;gap:7px}
|
|
714
|
+
.sl-seccard.mini .sl-seccard-name{font-size:12px;flex:none;max-width:120px}
|
|
715
|
+
.sl-seccard.mini .sl-seccard-left{font-size:11px}
|
|
716
|
+
/* narrow: compact strip inside the sheet head (peek area) */
|
|
717
|
+
.sl-seccard.strip{width:100%;padding:7px 0 0;border:0;border-radius:0;box-shadow:none;background:none;cursor:default}
|
|
718
|
+
.sl-seccard.strip.on{display:flex;align-items:center;gap:7px;font-size:12.5px}
|
|
719
|
+
.sl-seccard.strip .sl-seccard-name{font-size:12.5px}
|
|
720
|
+
.sl-seccard.strip .sl-seccard-price{margin-left:auto}
|
|
621
721
|
.sl-seccard-head{display:flex;align-items:center;gap:8px}
|
|
622
722
|
.sl-seccard-dot{width:10px;height:10px;border-radius:50%;flex:none}
|
|
623
723
|
.sl-seccard-name{font-weight:800;font-size:14px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
@@ -659,6 +759,20 @@ var CSS = `
|
|
|
659
759
|
font-size:11.5px;font-weight:600;background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-muted);
|
|
660
760
|
white-space:nowrap;pointer-events:none;max-width:90%;overflow:hidden;text-overflow:ellipsis}
|
|
661
761
|
|
|
762
|
+
/* F3 minimap \u2014 venue overview + live viewport rect (flows in the bottom-left region) */
|
|
763
|
+
.sl-minimap{border:1px solid var(--sl-line);border-radius:9px;
|
|
764
|
+
overflow:hidden;background:var(--sl-surface);box-shadow:0 12px 34px -14px rgba(0,0,0,.55);line-height:0;cursor:pointer}
|
|
765
|
+
.sl-minimap canvas{display:block}
|
|
766
|
+
.sl-picker[data-layout="narrow"] .sl-minimap{display:none}
|
|
767
|
+
|
|
768
|
+
/* F4 price-band filter \u2014 chip row in the side panel, above the price legend it
|
|
769
|
+
* dims (keeps clear of the map's top-center rungs / top-left a11y chips). */
|
|
770
|
+
.sl-pricef{display:flex;gap:6px;flex-wrap:wrap;padding:2px 16px 10px}
|
|
771
|
+
.sl-pricef .sl-chip-f{padding:6px 11px;font-size:11.5px}
|
|
772
|
+
/* F4 legend reflection: rows + counts for out-of-band categories read muted */
|
|
773
|
+
.sl-price-row.sl-dim{opacity:.4}
|
|
774
|
+
.sl-seccard-mix-item.sl-dim{opacity:.4}
|
|
775
|
+
|
|
662
776
|
/* modal host */
|
|
663
777
|
.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}
|
|
664
778
|
.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)}
|
|
@@ -694,6 +808,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
694
808
|
this.destroyed = false;
|
|
695
809
|
// chrome refs
|
|
696
810
|
this.els = {};
|
|
811
|
+
/** Feature 6 anchor regions — positioned flex containers over the map. */
|
|
812
|
+
this.regions = {};
|
|
697
813
|
this.ro = null;
|
|
698
814
|
this.holdTimer = null;
|
|
699
815
|
this.toastTimer = null;
|
|
@@ -724,6 +840,25 @@ var SeatPicker = class _SeatPicker {
|
|
|
724
840
|
this.viewEl = null;
|
|
725
841
|
this.viewCleanup = null;
|
|
726
842
|
this.allSeatsCache = null;
|
|
843
|
+
// F3 minimap
|
|
844
|
+
this.miniEl = null;
|
|
845
|
+
this.miniCanvas = null;
|
|
846
|
+
this.miniBase = null;
|
|
847
|
+
this.miniTf = null;
|
|
848
|
+
// F4 price-band filter — active band's category keys (null = all prices)
|
|
849
|
+
this.priceBandKeys = null;
|
|
850
|
+
this.priceFilterEl = null;
|
|
851
|
+
/** Last surfaced section summary (re-rendered when the price band changes). */
|
|
852
|
+
this.lastSection = null;
|
|
853
|
+
/** Section card collapsed to its slim pill (seat-picking has begun). */
|
|
854
|
+
this.secCardCollapsed = false;
|
|
855
|
+
/** When the card was (re)shown — the focus glide's own view change must not collapse it. */
|
|
856
|
+
this.secCardShownAt = 0;
|
|
857
|
+
/** Previous tray ticket count — first 0→n transition auto-expands the mobile sheet. */
|
|
858
|
+
this.lastTrayCount = 0;
|
|
859
|
+
// narrow-layout chrome that docks into the sheet's Filters row on mobile
|
|
860
|
+
this.a11yChipsEl = null;
|
|
861
|
+
this.cbEl = null;
|
|
727
862
|
// modal plumbing (set by open())
|
|
728
863
|
this.modalScrim = null;
|
|
729
864
|
this.prevFocus = null;
|
|
@@ -742,11 +877,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
742
877
|
currency: options.currency,
|
|
743
878
|
flashOnLiveChange: true,
|
|
744
879
|
colorblindSafe: options.colorblindSafe,
|
|
745
|
-
onSelectionChange: () =>
|
|
880
|
+
onSelectionChange: () => {
|
|
881
|
+
this.syncTray();
|
|
882
|
+
if (this.controller.getSelection().length) this.collapseSectionCard();
|
|
883
|
+
},
|
|
746
884
|
onStatusChange: () => {
|
|
747
885
|
this.syncPrices();
|
|
748
886
|
this.evictTakenSelections();
|
|
749
887
|
this.detectBooked();
|
|
888
|
+
this.refreshMinimap();
|
|
750
889
|
},
|
|
751
890
|
onHoldExpired: () => {
|
|
752
891
|
this.hold = null;
|
|
@@ -765,6 +904,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
765
904
|
onViewChange: () => {
|
|
766
905
|
this.reanchorConfirm();
|
|
767
906
|
this.syncRung();
|
|
907
|
+
this.drawMinimapRect();
|
|
908
|
+
this.sectionCardOnView();
|
|
768
909
|
},
|
|
769
910
|
// Tapped-section glide-in → surface (or clear) the section-summary card.
|
|
770
911
|
onSectionFocus: (summary) => this.showSectionCard(summary),
|
|
@@ -844,7 +985,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
844
985
|
<div class="sl-body">
|
|
845
986
|
<div class="sl-map">
|
|
846
987
|
<div class="sl-map-host" data-ref="map"></div>
|
|
847
|
-
<div class="sl-zoom">
|
|
988
|
+
<div class="sl-zoom" data-ref="zoom">
|
|
848
989
|
<button type="button" aria-label="Zoom in" data-ref="zin">+</button>
|
|
849
990
|
<button type="button" aria-label="Zoom out" data-ref="zout">\u2212</button>
|
|
850
991
|
<button type="button" aria-label="Fit to screen" data-ref="zfit">
|
|
@@ -854,7 +995,18 @@ var SeatPicker = class _SeatPicker {
|
|
|
854
995
|
<div class="sl-boot" data-ref="boot"><span class="sl-boot-spin"></span>Loading seat map\u2026</div>
|
|
855
996
|
<div class="sl-toast" data-ref="toast" role="status" aria-live="polite"></div>
|
|
856
997
|
</div>
|
|
857
|
-
<div class="sl-side">
|
|
998
|
+
<div class="sl-side" data-ref="side">
|
|
999
|
+
<div class="sl-sheet-head" data-ref="sheetHead">
|
|
1000
|
+
<div class="sl-sheet-grab"></div>
|
|
1001
|
+
<div class="sl-sheet-bar">
|
|
1002
|
+
<div class="sl-sheet-peek" data-ref="peek"></div>
|
|
1003
|
+
<span class="sl-sheet-chevron" aria-hidden="true">
|
|
1004
|
+
<svg viewBox="0 0 24 24"><path d="M6 15l6-6 6 6"/></svg>
|
|
1005
|
+
</span>
|
|
1006
|
+
</div>
|
|
1007
|
+
</div>
|
|
1008
|
+
<div class="sl-sec sl-filtersec" data-ref="filtersSec">Filters</div>
|
|
1009
|
+
<div class="sl-filters" data-ref="filters"></div>
|
|
858
1010
|
<div class="sl-sec" data-ref="pricesSec">Prices</div>
|
|
859
1011
|
<div class="sl-prices" data-ref="prices"></div>
|
|
860
1012
|
<div class="sl-sec">Your seats</div>
|
|
@@ -869,14 +1021,55 @@ var SeatPicker = class _SeatPicker {
|
|
|
869
1021
|
this.els[el.dataset.ref] = el;
|
|
870
1022
|
});
|
|
871
1023
|
this.mapHost = this.els.map;
|
|
872
|
-
|
|
1024
|
+
const applyLayout = () => {
|
|
873
1025
|
const w = root.clientWidth;
|
|
874
|
-
|
|
875
|
-
|
|
1026
|
+
if (w <= 0) return;
|
|
1027
|
+
const next = w < 640 ? "narrow" : "wide";
|
|
1028
|
+
if (root.dataset.layout === next) return;
|
|
1029
|
+
root.dataset.layout = next;
|
|
1030
|
+
if (next === "narrow" && !root.dataset.sheet) root.dataset.sheet = "peek";
|
|
1031
|
+
this.dockLayoutChrome();
|
|
1032
|
+
};
|
|
1033
|
+
this.ro = new ResizeObserver(applyLayout);
|
|
876
1034
|
this.ro.observe(root);
|
|
1035
|
+
applyLayout();
|
|
1036
|
+
requestAnimationFrame(applyLayout);
|
|
877
1037
|
this.els.zin.addEventListener("click", () => this.controller.zoomIn());
|
|
878
1038
|
this.els.zout.addEventListener("click", () => this.controller.zoomOut());
|
|
879
1039
|
this.els.zfit.addEventListener("click", () => this.controller.zoomToFit());
|
|
1040
|
+
const head = this.els.sheetHead;
|
|
1041
|
+
if (head) {
|
|
1042
|
+
const setSheet = (open) => {
|
|
1043
|
+
root.dataset.sheet = open ? "open" : "peek";
|
|
1044
|
+
};
|
|
1045
|
+
let startY = 0;
|
|
1046
|
+
let swiped = false;
|
|
1047
|
+
let tracking = false;
|
|
1048
|
+
head.addEventListener("pointerdown", (e) => {
|
|
1049
|
+
tracking = true;
|
|
1050
|
+
swiped = false;
|
|
1051
|
+
startY = e.clientY;
|
|
1052
|
+
head.setPointerCapture?.(e.pointerId);
|
|
1053
|
+
});
|
|
1054
|
+
head.addEventListener("pointermove", (e) => {
|
|
1055
|
+
if (!tracking || swiped) return;
|
|
1056
|
+
const dy = e.clientY - startY;
|
|
1057
|
+
if (dy < -18) {
|
|
1058
|
+
setSheet(true);
|
|
1059
|
+
swiped = true;
|
|
1060
|
+
} else if (dy > 18) {
|
|
1061
|
+
setSheet(false);
|
|
1062
|
+
swiped = true;
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
head.addEventListener("pointerup", (e) => {
|
|
1066
|
+
if (tracking && !swiped && Math.abs(e.clientY - startY) < 6) {
|
|
1067
|
+
if (!e.target.closest(".sl-seccard")) setSheet(root.dataset.sheet !== "open");
|
|
1068
|
+
}
|
|
1069
|
+
tracking = false;
|
|
1070
|
+
head.releasePointerCapture?.(e.pointerId);
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
880
1073
|
this.tipEl = document.createElement("div");
|
|
881
1074
|
this.tipEl.setAttribute("role", "tooltip");
|
|
882
1075
|
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;";
|
|
@@ -903,13 +1096,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
903
1096
|
return this;
|
|
904
1097
|
}
|
|
905
1098
|
this.els.boot.remove();
|
|
1099
|
+
this.buildRegions();
|
|
1100
|
+
this.regions["bottom-right"].appendChild(this.els.zoom);
|
|
1101
|
+
this.regions["bottom-center"].appendChild(this.els.toast);
|
|
906
1102
|
if (info.mode === "test") {
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
this.
|
|
912
|
-
this.els.map.appendChild(ribbon);
|
|
1103
|
+
const badge = document.createElement("div");
|
|
1104
|
+
badge.className = "sl-testbadge";
|
|
1105
|
+
badge.textContent = (0, import_core2.t)("picker.testMode");
|
|
1106
|
+
badge.setAttribute("aria-label", (0, import_core2.t)("picker.testMode"));
|
|
1107
|
+
this.regions["top-right"].appendChild(badge);
|
|
913
1108
|
}
|
|
914
1109
|
const chartTheme = this.controller.doc?.theme;
|
|
915
1110
|
Object.entries(resolveTokens(chartTheme, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
|
|
@@ -933,7 +1128,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
933
1128
|
const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
|
|
934
1129
|
const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-f="${key}">${label}</button>`;
|
|
935
1130
|
chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
|
|
936
|
-
this.
|
|
1131
|
+
this.regions["top-left"].appendChild(chips);
|
|
1132
|
+
this.a11yChipsEl = chips;
|
|
937
1133
|
chips.querySelectorAll("button").forEach((btn) => {
|
|
938
1134
|
btn.addEventListener("click", () => {
|
|
939
1135
|
const f = btn.dataset.f;
|
|
@@ -945,6 +1141,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
945
1141
|
}
|
|
946
1142
|
const cb = document.createElement("button");
|
|
947
1143
|
cb.type = "button";
|
|
1144
|
+
cb.className = "sl-cbbtn";
|
|
1145
|
+
this.cbEl = cb;
|
|
948
1146
|
cb.setAttribute("aria-label", "Toggle colorblind-friendly colors");
|
|
949
1147
|
cb.setAttribute("aria-pressed", String(!!this.opts.colorblindSafe));
|
|
950
1148
|
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>';
|
|
@@ -960,19 +1158,45 @@ var SeatPicker = class _SeatPicker {
|
|
|
960
1158
|
this.srEl.setAttribute("aria-live", "polite");
|
|
961
1159
|
root.appendChild(this.srEl);
|
|
962
1160
|
this.buildArenaChrome();
|
|
1161
|
+
this.buildMinimap();
|
|
1162
|
+
this.buildPriceFilter();
|
|
963
1163
|
this.buildExtendPrompt();
|
|
964
1164
|
this.buildBookedOverlay();
|
|
1165
|
+
this.dockLayoutChrome();
|
|
965
1166
|
this.syncPrices();
|
|
966
1167
|
this.syncTray();
|
|
967
1168
|
return this;
|
|
968
1169
|
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Move layout-dependent chrome between its wide dock (map regions / zoom
|
|
1172
|
+
* column) and its narrow dock (the sheet's consolidated Filters row), and
|
|
1173
|
+
* re-render the section card in the form the layout wants (docked card/pill
|
|
1174
|
+
* on wide, sheet strip on narrow). Runs on every layout flip + once post-render.
|
|
1175
|
+
*/
|
|
1176
|
+
dockLayoutChrome() {
|
|
1177
|
+
const narrow = this.root?.dataset.layout === "narrow";
|
|
1178
|
+
const filters = this.els.filters;
|
|
1179
|
+
if (filters) {
|
|
1180
|
+
if (narrow) {
|
|
1181
|
+
if (this.a11yChipsEl) filters.appendChild(this.a11yChipsEl);
|
|
1182
|
+
if (this.cbEl) filters.appendChild(this.cbEl);
|
|
1183
|
+
} else {
|
|
1184
|
+
if (this.a11yChipsEl) this.regions["top-left"]?.appendChild(this.a11yChipsEl);
|
|
1185
|
+
if (this.cbEl) this.els.zoom?.appendChild(this.cbEl);
|
|
1186
|
+
}
|
|
1187
|
+
const has = narrow && filters.children.length > 0;
|
|
1188
|
+
filters.classList.toggle("has", has);
|
|
1189
|
+
this.els.filtersSec?.classList.toggle("has", has);
|
|
1190
|
+
}
|
|
1191
|
+
if (this.lastSection) this.renderSectionCard(this.lastSection);
|
|
1192
|
+
}
|
|
969
1193
|
/** The "Need more time?" prompt shown in the hold's final EXTEND_PROMPT_MS. */
|
|
970
1194
|
buildExtendPrompt() {
|
|
971
1195
|
const el = document.createElement("div");
|
|
972
1196
|
el.className = "sl-extend";
|
|
973
1197
|
el.setAttribute("role", "status");
|
|
974
1198
|
el.innerHTML = `<span class="sl-extend-txt" data-ref="extendTxt"></span><button type="button" class="sl-extend-btn" data-ref="extendBtn"></button>`;
|
|
975
|
-
this.els.map.appendChild(el);
|
|
1199
|
+
(this.regions["bottom-center"] ?? this.els.map).appendChild(el);
|
|
976
1200
|
this.extendEl = el;
|
|
977
1201
|
this.els.extendTxt = el.querySelector('[data-ref="extendTxt"]');
|
|
978
1202
|
this.els.extendBtn = el.querySelector('[data-ref="extendBtn"]');
|
|
@@ -990,6 +1214,258 @@ var SeatPicker = class _SeatPicker {
|
|
|
990
1214
|
this.bookedEl = el;
|
|
991
1215
|
this.els.bookedSub = el.querySelector('[data-ref="bookedSub"]');
|
|
992
1216
|
}
|
|
1217
|
+
// ---- Feature 6: chrome anchor regions -------------------------------------
|
|
1218
|
+
/**
|
|
1219
|
+
* Create the positioned flex containers that own every persistent map overlay.
|
|
1220
|
+
* Appended once after controller.render(); each chrome piece is then appended
|
|
1221
|
+
* INTO its region and flows within it, so nothing free-floats over anything
|
|
1222
|
+
* else. Regions carve the map into non-overlapping zones (top strip split into
|
|
1223
|
+
* left/center/right, left rail, and the three used corners).
|
|
1224
|
+
*/
|
|
1225
|
+
buildRegions() {
|
|
1226
|
+
if (!this.els.map) return;
|
|
1227
|
+
const REGIONS = ["top-left", "top-center", "top-right", "left-rail", "bottom-left", "bottom-center", "bottom-right"];
|
|
1228
|
+
for (const region of REGIONS) {
|
|
1229
|
+
const el = document.createElement("div");
|
|
1230
|
+
el.className = "sl-anchor";
|
|
1231
|
+
el.dataset.region = region;
|
|
1232
|
+
this.els.map.appendChild(el);
|
|
1233
|
+
this.regions[region] = el;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
// ---- F3 minimap -----------------------------------------------------------
|
|
1237
|
+
/** Read a resolved --sl-* token value (canvas needs a real color, not var()). */
|
|
1238
|
+
cssVar(name) {
|
|
1239
|
+
return this.root ? getComputedStyle(this.root).getPropertyValue(name).trim() : "";
|
|
1240
|
+
}
|
|
1241
|
+
/** Section-bearing objects on the active floor (single-floor → doc.objects). */
|
|
1242
|
+
activeFloorObjects() {
|
|
1243
|
+
const doc = this.controller.doc;
|
|
1244
|
+
if (!doc) return [];
|
|
1245
|
+
const floors = doc.floors;
|
|
1246
|
+
if (floors?.length) {
|
|
1247
|
+
const id = this.controller.getActiveFloorId();
|
|
1248
|
+
return (floors.find((f) => f.id === id) ?? floors[0]).objects ?? [];
|
|
1249
|
+
}
|
|
1250
|
+
return doc.objects ?? [];
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Build the overview minimap: a static venue thumbnail (section outlines, or
|
|
1254
|
+
* seat dots when the chart has no sections) with the live viewport rectangle
|
|
1255
|
+
* drawn on top. The rect tracks pan/zoom via the constructor's onViewChange.
|
|
1256
|
+
*/
|
|
1257
|
+
buildMinimap() {
|
|
1258
|
+
const vp = this.controller.getViewport();
|
|
1259
|
+
if (!vp || !this.els.map) return;
|
|
1260
|
+
const b = vp.bounds;
|
|
1261
|
+
if (!(b.width > 0 && b.height > 0)) return;
|
|
1262
|
+
const MAXW = 158;
|
|
1263
|
+
const MAXH = 118;
|
|
1264
|
+
const PAD = 6;
|
|
1265
|
+
const aspect = b.width / Math.max(1, b.height);
|
|
1266
|
+
let w = MAXW;
|
|
1267
|
+
let h = Math.round(MAXW / aspect);
|
|
1268
|
+
if (h > MAXH) {
|
|
1269
|
+
h = MAXH;
|
|
1270
|
+
w = Math.round(MAXH * aspect);
|
|
1271
|
+
}
|
|
1272
|
+
w = Math.max(64, w);
|
|
1273
|
+
h = Math.max(48, h);
|
|
1274
|
+
const dpr = Math.min(2, window.devicePixelRatio || 1);
|
|
1275
|
+
const wrap = document.createElement("div");
|
|
1276
|
+
wrap.className = "sl-minimap";
|
|
1277
|
+
wrap.setAttribute("aria-hidden", "true");
|
|
1278
|
+
const canvas = document.createElement("canvas");
|
|
1279
|
+
canvas.width = Math.round(w * dpr);
|
|
1280
|
+
canvas.height = Math.round(h * dpr);
|
|
1281
|
+
canvas.style.width = `${w}px`;
|
|
1282
|
+
canvas.style.height = `${h}px`;
|
|
1283
|
+
wrap.appendChild(canvas);
|
|
1284
|
+
(this.regions["bottom-left"] ?? this.els.map).appendChild(wrap);
|
|
1285
|
+
this.miniEl = wrap;
|
|
1286
|
+
this.miniCanvas = canvas;
|
|
1287
|
+
const scale = Math.min((w - PAD * 2) / Math.max(1, b.width), (h - PAD * 2) / Math.max(1, b.height)) * dpr;
|
|
1288
|
+
const offX = (w * dpr - b.width * scale) / 2 - b.x * scale;
|
|
1289
|
+
const offY = (h * dpr - b.height * scale) / 2 - b.y * scale;
|
|
1290
|
+
this.miniTf = { scale, offX, offY, dpr };
|
|
1291
|
+
const base = document.createElement("canvas");
|
|
1292
|
+
base.width = canvas.width;
|
|
1293
|
+
base.height = canvas.height;
|
|
1294
|
+
this.miniBase = base;
|
|
1295
|
+
wrap.addEventListener("click", (e) => this.minimapJump(e));
|
|
1296
|
+
this.drawMinimapStatic();
|
|
1297
|
+
this.drawMinimapRect();
|
|
1298
|
+
}
|
|
1299
|
+
/** Repaint the static overview + rect (floor switch, live open/close). */
|
|
1300
|
+
refreshMinimap() {
|
|
1301
|
+
if (!this.miniBase) return;
|
|
1302
|
+
this.drawMinimapStatic();
|
|
1303
|
+
this.drawMinimapRect();
|
|
1304
|
+
}
|
|
1305
|
+
/** Paint the venue overview into the offscreen base canvas. */
|
|
1306
|
+
drawMinimapStatic() {
|
|
1307
|
+
const base = this.miniBase;
|
|
1308
|
+
const tf = this.miniTf;
|
|
1309
|
+
const doc = this.controller.doc;
|
|
1310
|
+
if (!base || !tf || !doc) return;
|
|
1311
|
+
const ctx = base.getContext("2d");
|
|
1312
|
+
if (!ctx) return;
|
|
1313
|
+
ctx.clearRect(0, 0, base.width, base.height);
|
|
1314
|
+
const fx = (x) => x * tf.scale + tf.offX;
|
|
1315
|
+
const fy = (y) => y * tf.scale + tf.offY;
|
|
1316
|
+
const line = this.cssVar("--sl-line") || "rgba(139,147,167,.5)";
|
|
1317
|
+
const muted = this.cssVar("--sl-muted") || "#8b93a7";
|
|
1318
|
+
const accent = this.cssVar("--sl-accent") || "#6e7bff";
|
|
1319
|
+
const zoneColor = new Map((doc.zones ?? []).map((z) => [z.id, z.color]));
|
|
1320
|
+
let drewSection = false;
|
|
1321
|
+
for (const o of this.activeFloorObjects()) {
|
|
1322
|
+
if (o.type !== "section" || !o.outline || o.outline.length < 3) continue;
|
|
1323
|
+
drewSection = true;
|
|
1324
|
+
const closed = this.controller.isSectionClosed(o.id);
|
|
1325
|
+
const fill = closed ? muted : o.color ?? (o.zone && zoneColor.get(o.zone)) ?? accent;
|
|
1326
|
+
ctx.beginPath();
|
|
1327
|
+
o.outline.forEach((p, i) => i === 0 ? ctx.moveTo(fx(p.x), fy(p.y)) : ctx.lineTo(fx(p.x), fy(p.y)));
|
|
1328
|
+
ctx.closePath();
|
|
1329
|
+
ctx.globalAlpha = closed ? 0.26 : 0.42;
|
|
1330
|
+
ctx.fillStyle = fill;
|
|
1331
|
+
ctx.fill();
|
|
1332
|
+
ctx.globalAlpha = 0.85;
|
|
1333
|
+
ctx.lineWidth = Math.max(1, tf.dpr);
|
|
1334
|
+
ctx.strokeStyle = line;
|
|
1335
|
+
ctx.stroke();
|
|
1336
|
+
}
|
|
1337
|
+
ctx.globalAlpha = 1;
|
|
1338
|
+
if (!drewSection) {
|
|
1339
|
+
const r = Math.max(1, tf.dpr);
|
|
1340
|
+
for (const seat of (0, import_core2.expandChart)(doc)) {
|
|
1341
|
+
const cat = doc.categories.find((c) => c.key === seat.categoryKey);
|
|
1342
|
+
ctx.fillStyle = cat?.color ?? accent;
|
|
1343
|
+
ctx.beginPath();
|
|
1344
|
+
ctx.arc(fx(seat.x), fy(seat.y), r, 0, Math.PI * 2);
|
|
1345
|
+
ctx.fill();
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
/** Blit the base overview, then stroke the current viewport rectangle on top. */
|
|
1350
|
+
drawMinimapRect() {
|
|
1351
|
+
const canvas = this.miniCanvas;
|
|
1352
|
+
const base = this.miniBase;
|
|
1353
|
+
const tf = this.miniTf;
|
|
1354
|
+
if (!canvas || !base || !tf) return;
|
|
1355
|
+
const ctx = canvas.getContext("2d");
|
|
1356
|
+
if (!ctx) return;
|
|
1357
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
1358
|
+
ctx.drawImage(base, 0, 0);
|
|
1359
|
+
const vp = this.controller.getViewport();
|
|
1360
|
+
if (!vp) return;
|
|
1361
|
+
const v = vp.visible;
|
|
1362
|
+
const x = v.x * tf.scale + tf.offX;
|
|
1363
|
+
const y = v.y * tf.scale + tf.offY;
|
|
1364
|
+
const w = v.width * tf.scale;
|
|
1365
|
+
const h = v.height * tf.scale;
|
|
1366
|
+
const accent = this.cssVar("--sl-accent") || "#f4b740";
|
|
1367
|
+
ctx.save();
|
|
1368
|
+
ctx.globalAlpha = 0.14;
|
|
1369
|
+
ctx.fillStyle = accent;
|
|
1370
|
+
ctx.fillRect(x, y, w, h);
|
|
1371
|
+
ctx.globalAlpha = 1;
|
|
1372
|
+
ctx.lineWidth = Math.max(1.5, tf.dpr * 1.5);
|
|
1373
|
+
ctx.strokeStyle = accent;
|
|
1374
|
+
ctx.strokeRect(x, y, w, h);
|
|
1375
|
+
ctx.restore();
|
|
1376
|
+
}
|
|
1377
|
+
/** Minimap click → focus the section under the point (or overview on a miss). */
|
|
1378
|
+
minimapJump(e) {
|
|
1379
|
+
const canvas = this.miniCanvas;
|
|
1380
|
+
const tf = this.miniTf;
|
|
1381
|
+
if (!canvas || !tf) return;
|
|
1382
|
+
const r = canvas.getBoundingClientRect();
|
|
1383
|
+
const px = (e.clientX - r.left) * (canvas.width / r.width);
|
|
1384
|
+
const py = (e.clientY - r.top) * (canvas.height / r.height);
|
|
1385
|
+
const wx = (px - tf.offX) / tf.scale;
|
|
1386
|
+
const wy = (py - tf.offY) / tf.scale;
|
|
1387
|
+
for (const o of this.activeFloorObjects()) {
|
|
1388
|
+
if (o.type !== "section" || !o.outline || o.outline.length < 3) continue;
|
|
1389
|
+
if (this.controller.isSectionClosed(o.id)) continue;
|
|
1390
|
+
if (pointInPolygon(wx, wy, o.outline)) {
|
|
1391
|
+
this.controller.focusSection(o.id);
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
this.controller.overview();
|
|
1396
|
+
}
|
|
1397
|
+
// ---- F4 price-band filter -------------------------------------------------
|
|
1398
|
+
/** Effective price of a category (first tier when tiered, else base price). */
|
|
1399
|
+
catPrice(c) {
|
|
1400
|
+
return c.tiers?.length ? c.tiers[0].price : c.price;
|
|
1401
|
+
}
|
|
1402
|
+
/** Derive price bands: one chip per distinct price (≤5), else quantile ranges. */
|
|
1403
|
+
priceBands() {
|
|
1404
|
+
const doc = this.controller.doc;
|
|
1405
|
+
if (!doc) return [];
|
|
1406
|
+
const priced = doc.categories.map((c) => ({ key: c.key, price: this.catPrice(c) })).filter((x) => x.price != null);
|
|
1407
|
+
if (!priced.length) return [];
|
|
1408
|
+
const distinct = [...new Set(priced.map((p) => p.price))].sort((a, b) => a - b);
|
|
1409
|
+
if (distinct.length <= 5) {
|
|
1410
|
+
return distinct.map((price) => ({
|
|
1411
|
+
id: `p${price}`,
|
|
1412
|
+
label: this.money(price),
|
|
1413
|
+
keys: priced.filter((p) => p.price === price).map((p) => p.key),
|
|
1414
|
+
min: price,
|
|
1415
|
+
max: price
|
|
1416
|
+
}));
|
|
1417
|
+
}
|
|
1418
|
+
const chunk = Math.ceil(distinct.length / 4);
|
|
1419
|
+
const bands = [];
|
|
1420
|
+
for (let i = 0; i < distinct.length; i += chunk) {
|
|
1421
|
+
const slice = distinct.slice(i, i + chunk);
|
|
1422
|
+
const lo = slice[0];
|
|
1423
|
+
const hi = slice[slice.length - 1];
|
|
1424
|
+
bands.push({
|
|
1425
|
+
id: `b${i}`,
|
|
1426
|
+
label: lo === hi ? this.money(lo) : `${this.money(lo)}\u2013${this.money(hi)}`,
|
|
1427
|
+
keys: priced.filter((p) => p.price >= lo && p.price <= hi).map((p) => p.key),
|
|
1428
|
+
min: lo,
|
|
1429
|
+
max: hi
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
return bands;
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Build the price-band chip row in the side panel, directly under the "Prices"
|
|
1436
|
+
* header and above the legend it dims. Living in the panel (not a map overlay)
|
|
1437
|
+
* keeps it clear of the top-center rung pills and top-left a11y chips, and it
|
|
1438
|
+
* rides the bottom sheet on narrow layouts. Skipped when there's <2 bands.
|
|
1439
|
+
*/
|
|
1440
|
+
buildPriceFilter() {
|
|
1441
|
+
if (!this.els.prices || !this.els.pricesSec) return;
|
|
1442
|
+
const bands = this.priceBands();
|
|
1443
|
+
if (bands.length < 2) return;
|
|
1444
|
+
const wrap = document.createElement("div");
|
|
1445
|
+
wrap.className = "sl-pricef";
|
|
1446
|
+
wrap.setAttribute("role", "group");
|
|
1447
|
+
wrap.setAttribute("aria-label", "Filter seats by price");
|
|
1448
|
+
const mk = (band, label, on) => `<button type="button" class="sl-chip-f${on ? " on" : ""}" data-band="${band}">${label}</button>`;
|
|
1449
|
+
wrap.innerHTML = mk("all", "All prices", true) + bands.map((bd) => mk(bd.id, bd.label, false)).join("");
|
|
1450
|
+
this.els.pricesSec.after(wrap);
|
|
1451
|
+
this.priceFilterEl = wrap;
|
|
1452
|
+
wrap.querySelectorAll("button").forEach((btn) => {
|
|
1453
|
+
btn.addEventListener("click", () => {
|
|
1454
|
+
wrap.querySelectorAll("button").forEach((b) => b.classList.toggle("on", b === btn));
|
|
1455
|
+
const id = btn.dataset.band;
|
|
1456
|
+
if (id === "all") {
|
|
1457
|
+
this.priceBandKeys = null;
|
|
1458
|
+
this.controller.setCategoryFilter(null);
|
|
1459
|
+
} else {
|
|
1460
|
+
const bd = bands.find((x) => x.id === id);
|
|
1461
|
+
this.priceBandKeys = bd ? new Set(bd.keys) : null;
|
|
1462
|
+
this.controller.setCategoryFilter(bd ? bd.keys : null);
|
|
1463
|
+
}
|
|
1464
|
+
this.syncPrices();
|
|
1465
|
+
if (this.lastSection) this.showSectionCard(this.lastSection);
|
|
1466
|
+
});
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
993
1469
|
// ---- arena / multi-floor chrome -------------------------------------------
|
|
994
1470
|
/** Build the rung pills (charts with sections) and floor switcher (>1 floor). */
|
|
995
1471
|
buildArenaChrome() {
|
|
@@ -1018,7 +1494,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1018
1494
|
pills.querySelectorAll("button").forEach((btn) => {
|
|
1019
1495
|
btn.addEventListener("click", () => this.controller.setRung(btn.dataset.rung));
|
|
1020
1496
|
});
|
|
1021
|
-
this.
|
|
1497
|
+
this.regions["top-center"].appendChild(pills);
|
|
1022
1498
|
this.rungsEl = pills;
|
|
1023
1499
|
this.syncRung();
|
|
1024
1500
|
}
|
|
@@ -1035,9 +1511,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
1035
1511
|
this.showSectionCard(null);
|
|
1036
1512
|
this.syncFloors();
|
|
1037
1513
|
this.syncRung();
|
|
1514
|
+
this.refreshMinimap();
|
|
1038
1515
|
});
|
|
1039
1516
|
});
|
|
1040
|
-
this.
|
|
1517
|
+
this.regions["left-rail"].appendChild(rail);
|
|
1041
1518
|
this.floorsEl = rail;
|
|
1042
1519
|
this.syncFloors();
|
|
1043
1520
|
}
|
|
@@ -1062,27 +1539,105 @@ var SeatPicker = class _SeatPicker {
|
|
|
1062
1539
|
}
|
|
1063
1540
|
/** Show (or clear, on null) the tapped-section summary card. */
|
|
1064
1541
|
showSectionCard(summary) {
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1542
|
+
this.lastSection = summary;
|
|
1543
|
+
this.secCardEl?.remove();
|
|
1544
|
+
this.secCardEl = null;
|
|
1545
|
+
if (!summary) return;
|
|
1546
|
+
this.secCardCollapsed = false;
|
|
1547
|
+
this.secCardShownAt = Date.now();
|
|
1548
|
+
this.renderSectionCard(summary);
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Render the section card in the form the layout + state want: expanded card
|
|
1552
|
+
* or slim pill in the top-center anchor region (wide), or a compact strip in
|
|
1553
|
+
* the sheet head (narrow). Never floats over the seats at the tap point.
|
|
1554
|
+
*/
|
|
1555
|
+
renderSectionCard(summary) {
|
|
1070
1556
|
if (!this.els.map) return;
|
|
1071
1557
|
this.secCardEl?.remove();
|
|
1072
1558
|
const priceLabel = summary.priceMin === summary.priceMax ? this.money(summary.priceMin) : `${this.money(summary.priceMin)}\u2013${this.money(summary.priceMax)}`;
|
|
1559
|
+
const leftLabel = (0, import_core2.tCount)("picker.seatsLeftInSection", summary.seatsLeft);
|
|
1560
|
+
const xBtn = `<button type="button" class="sl-seccard-x" aria-label="${(0, import_core2.t)("picker.closeSectionSummary")}">\u2715</button>`;
|
|
1073
1561
|
const card = document.createElement("div");
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1562
|
+
const narrow = this.root?.dataset.layout === "narrow";
|
|
1563
|
+
if (narrow) {
|
|
1564
|
+
card.className = "sl-seccard strip on";
|
|
1565
|
+
card.setAttribute("role", "status");
|
|
1566
|
+
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;
|
|
1567
|
+
card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
|
|
1568
|
+
(this.els.sheetHead ?? this.els.side ?? this.els.map).appendChild(card);
|
|
1569
|
+
} else if (this.secCardCollapsed) {
|
|
1570
|
+
card.className = "sl-seccard mini on";
|
|
1571
|
+
card.setAttribute("role", "button");
|
|
1572
|
+
card.setAttribute("aria-label", (0, import_core2.t)("picker.sectionSummaryAria", { label: summary.label }));
|
|
1573
|
+
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;
|
|
1574
|
+
card.addEventListener("click", (e) => {
|
|
1575
|
+
if (e.target.closest(".sl-seccard-x")) return;
|
|
1576
|
+
this.secCardCollapsed = false;
|
|
1577
|
+
this.secCardShownAt = Date.now();
|
|
1578
|
+
this.renderSectionCard(summary);
|
|
1579
|
+
});
|
|
1580
|
+
card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
|
|
1581
|
+
(this.regions["top-center"] ?? this.els.map).appendChild(card);
|
|
1582
|
+
} else {
|
|
1583
|
+
card.className = "sl-seccard on";
|
|
1584
|
+
card.setAttribute("role", "dialog");
|
|
1585
|
+
card.setAttribute("aria-label", (0, import_core2.t)("picker.sectionSummaryAria", { label: summary.label }));
|
|
1586
|
+
const mix = summary.categories.map((c) => {
|
|
1587
|
+
const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
|
|
1588
|
+
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>`;
|
|
1589
|
+
}).join("");
|
|
1590
|
+
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>`;
|
|
1591
|
+
card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
|
|
1592
|
+
card.querySelector(".sl-seccard-overview").addEventListener("click", () => this.controller.overview());
|
|
1593
|
+
(this.regions["top-center"] ?? this.els.map).appendChild(card);
|
|
1594
|
+
}
|
|
1084
1595
|
this.secCardEl = card;
|
|
1085
1596
|
}
|
|
1597
|
+
/** Collapse the expanded card to its slim pill (seat-picking started). */
|
|
1598
|
+
collapseSectionCard() {
|
|
1599
|
+
if (!this.secCardEl || this.secCardCollapsed || !this.lastSection) return;
|
|
1600
|
+
if (this.root?.dataset.layout === "narrow") return;
|
|
1601
|
+
this.secCardCollapsed = true;
|
|
1602
|
+
this.renderSectionCard(this.lastSection);
|
|
1603
|
+
}
|
|
1604
|
+
/**
|
|
1605
|
+
* onViewChange hook for the card. The focus glide's own settle (within the
|
|
1606
|
+
* grace window) enforces the ~25% coverage rule with the FINAL viewport; any
|
|
1607
|
+
* later pan/zoom means seat-picking has begun → collapse to the pill.
|
|
1608
|
+
*/
|
|
1609
|
+
sectionCardOnView() {
|
|
1610
|
+
if (!this.secCardEl || this.secCardCollapsed || !this.lastSection) return;
|
|
1611
|
+
if (this.root?.dataset.layout === "narrow") return;
|
|
1612
|
+
if (Date.now() - this.secCardShownAt < 1400) {
|
|
1613
|
+
if (this.sectionCardCoverage() > 0.25) this.collapseSectionCard();
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
this.collapseSectionCard();
|
|
1617
|
+
}
|
|
1618
|
+
/** Fraction of the focused section's on-screen bbox covered by the card. */
|
|
1619
|
+
sectionCardCoverage() {
|
|
1620
|
+
const card = this.secCardEl;
|
|
1621
|
+
const sec = this.lastSection;
|
|
1622
|
+
if (!card || !sec || !this.els.map) return 0;
|
|
1623
|
+
const outline = this.activeFloorObjects().find((o) => o.type === "section" && o.id === sec.id)?.outline;
|
|
1624
|
+
if (!outline || outline.length < 3) return 0;
|
|
1625
|
+
const pts = outline.map((p) => this.controller.worldToScreen(p));
|
|
1626
|
+
const xs = pts.map((p) => p.x);
|
|
1627
|
+
const ys = pts.map((p) => p.y);
|
|
1628
|
+
const bx = Math.min(...xs);
|
|
1629
|
+
const by = Math.min(...ys);
|
|
1630
|
+
const bw = Math.max(...xs) - bx;
|
|
1631
|
+
const bh = Math.max(...ys) - by;
|
|
1632
|
+
if (bw <= 0 || bh <= 0) return 0;
|
|
1633
|
+
const mapR = this.els.map.getBoundingClientRect();
|
|
1634
|
+
const cr = card.getBoundingClientRect();
|
|
1635
|
+
const cx = cr.left - mapR.left;
|
|
1636
|
+
const cy = cr.top - mapR.top;
|
|
1637
|
+
const ox = Math.max(0, Math.min(cx + cr.width, bx + bw) - Math.max(cx, bx));
|
|
1638
|
+
const oy = Math.max(0, Math.min(cy + cr.height, by + bh) - Math.max(cy, by));
|
|
1639
|
+
return ox * oy / (bw * bh);
|
|
1640
|
+
}
|
|
1086
1641
|
/** aria-live readout when keyboard focus lands on a seat. */
|
|
1087
1642
|
announceSeat(seat) {
|
|
1088
1643
|
if (!this.srEl) return;
|
|
@@ -1257,7 +1812,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
1257
1812
|
const left = this.controller.categoryAvailability();
|
|
1258
1813
|
this.els.prices.innerHTML = doc.categories.map((c) => {
|
|
1259
1814
|
const price = c.tiers?.length ? c.tiers[0].price : c.price;
|
|
1260
|
-
|
|
1815
|
+
const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
|
|
1816
|
+
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>`;
|
|
1261
1817
|
}).join("");
|
|
1262
1818
|
this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
|
|
1263
1819
|
row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
|
|
@@ -1363,6 +1919,18 @@ var SeatPicker = class _SeatPicker {
|
|
|
1363
1919
|
const cta = this.els.cta;
|
|
1364
1920
|
cta.disabled = count === 0;
|
|
1365
1921
|
cta.textContent = this.hold ? "Continue to checkout" : count ? "Hold seats & checkout" : "Select seats";
|
|
1922
|
+
if (this.els.peek) {
|
|
1923
|
+
if (count) {
|
|
1924
|
+
this.els.peek.innerHTML = `<span>${count} ${count === 1 ? "ticket" : "tickets"} \xB7 ${this.money(total)}</span><span class="go">${this.hold ? "Continue" : "Review"}</span>`;
|
|
1925
|
+
} else {
|
|
1926
|
+
const prices = (this.controller.doc?.categories ?? []).map((c) => this.catPrice(c)).filter((p) => p != null);
|
|
1927
|
+
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>`;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
if (this.root?.dataset.layout === "narrow" && count > 0 && this.lastTrayCount === 0) {
|
|
1931
|
+
this.root.dataset.sheet = "open";
|
|
1932
|
+
}
|
|
1933
|
+
this.lastTrayCount = count;
|
|
1366
1934
|
this.opts.onSelectionChange?.(seats);
|
|
1367
1935
|
}
|
|
1368
1936
|
async handleCta() {
|