@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.js
CHANGED
|
@@ -395,6 +395,14 @@ import {
|
|
|
395
395
|
var DEFAULT_API_BASE2 = "https://api.seatlayer.io";
|
|
396
396
|
var DEFAULT_MAX_SELECTION2 = 10;
|
|
397
397
|
var EXTEND_PROMPT_MS = 6e4;
|
|
398
|
+
function pointInPolygon(x, y, poly) {
|
|
399
|
+
let inside = false;
|
|
400
|
+
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
|
401
|
+
const xi = poly[i].x, yi = poly[i].y, xj = poly[j].x, yj = poly[j].y;
|
|
402
|
+
if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside;
|
|
403
|
+
}
|
|
404
|
+
return inside;
|
|
405
|
+
}
|
|
398
406
|
function resolveContainer3(container) {
|
|
399
407
|
if (typeof container === "string") {
|
|
400
408
|
const el = document.querySelector(container);
|
|
@@ -438,12 +446,57 @@ var CSS = `
|
|
|
438
446
|
.sl-map-host{position:absolute;inset:0}
|
|
439
447
|
.sl-side{width:300px;flex:none;border-left:1px solid var(--sl-line);display:flex;flex-direction:column;min-height:0;overflow-y:auto}
|
|
440
448
|
|
|
441
|
-
/* narrow (container < 640px):
|
|
449
|
+
/* narrow (container < 640px): map-first \u2014 the map claims ~80-85% of the
|
|
450
|
+
container and the side panel becomes a PEEKING bottom sheet (AXS/Ticketmaster
|
|
451
|
+
mobile pattern). data-sheet on the root: "peek" (default: grab handle + one
|
|
452
|
+
summary line) / "open" (\u226450%, swipe up or auto-expand on first selection).
|
|
453
|
+
Swipe handling lives on the sheet head ONLY \u2014 never the map host, so the
|
|
454
|
+
map's raw-pointer gesture pipeline is untouched. */
|
|
442
455
|
.sl-picker[data-layout="narrow"] .sl-body{flex-direction:column}
|
|
443
456
|
.sl-picker[data-layout="narrow"] .sl-map{min-height:0;flex:1}
|
|
444
|
-
.sl-picker[data-layout="narrow"] .sl-side{width:100%;
|
|
457
|
+
.sl-picker[data-layout="narrow"] .sl-side{width:100%;border-left:0;border-top:1px solid var(--sl-line);
|
|
458
|
+
flex:none;height:50%;transition:height .22s ease}
|
|
459
|
+
.sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side{height:86px;overflow:hidden}
|
|
460
|
+
.sl-picker[data-layout="narrow"][data-sheet="peek"] .sl-side > :not(.sl-sheet-head){display:none}
|
|
445
461
|
.sl-picker[data-layout="narrow"] .sl-tray{flex:none}
|
|
446
462
|
.sl-picker[data-layout="narrow"] .sl-foot{position:sticky;bottom:0;background:var(--sl-bg)}
|
|
463
|
+
/* touch chrome: pinch-zoom exists \u2014 hide +/\u2212 on the sheet layout (keep fit) */
|
|
464
|
+
.sl-picker[data-layout="narrow"] .sl-zoom [data-ref="zin"],
|
|
465
|
+
.sl-picker[data-layout="narrow"] .sl-zoom [data-ref="zout"]{display:none}
|
|
466
|
+
|
|
467
|
+
/* bottom-sheet head: grab handle + one-line summary (narrow only). The WHOLE
|
|
468
|
+
head is the tap/swipe toggle target (min 44px), so it reads as one control. */
|
|
469
|
+
.sl-sheet-head{display:none;flex-direction:column;justify-content:center;padding:6px 16px 8px;min-height:44px;
|
|
470
|
+
cursor:pointer;touch-action:none;user-select:none;-webkit-user-select:none;flex:none}
|
|
471
|
+
.sl-picker[data-layout="narrow"] .sl-sheet-head{display:flex}
|
|
472
|
+
.sl-sheet-grab{width:36px;height:4px;border-radius:999px;background:var(--sl-muted);opacity:.55;margin:2px auto 7px}
|
|
473
|
+
.sl-sheet-bar{display:flex;align-items:center;gap:10px;min-height:26px}
|
|
474
|
+
.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);
|
|
475
|
+
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
476
|
+
.sl-sheet-peek .sub{color:var(--sl-muted);font-weight:600}
|
|
477
|
+
/* collapsed-peek "Continue" affordance: a real accent pill, not plain text */
|
|
478
|
+
.sl-sheet-peek .go{margin-left:auto;flex:none;display:inline-flex;align-items:center;min-height:30px;
|
|
479
|
+
padding:6px 13px;border-radius:999px;background:var(--sl-accent);color:var(--sl-accent-ink);
|
|
480
|
+
font-weight:800;font-size:12.5px}
|
|
481
|
+
/* state chevron: points UP while peeking, rotates to point DOWN when open.
|
|
482
|
+
Base keeps an explicit rotate(0) \u2014 transitioning to/from a bare 'none' leaves
|
|
483
|
+
the value stuck in some engines, so both endpoints must be real transforms. */
|
|
484
|
+
.sl-sheet-chevron{flex:none;display:flex;align-items:center;justify-content:center;color:var(--sl-muted);
|
|
485
|
+
transform:rotate(0deg);transition:transform .22s ease}
|
|
486
|
+
.sl-sheet-chevron svg{width:18px;height:18px;stroke:currentColor;stroke-width:2.4;fill:none;
|
|
487
|
+
stroke-linecap:round;stroke-linejoin:round}
|
|
488
|
+
.sl-picker[data-sheet="open"] .sl-sheet-chevron{transform:rotate(180deg)}
|
|
489
|
+
|
|
490
|
+
/* consolidated Filters row inside the sheet (a11y chips + colorblind toggle
|
|
491
|
+
dock here on narrow; they live on the map / zoom column on wide) */
|
|
492
|
+
.sl-filtersec{display:none}
|
|
493
|
+
.sl-filters{display:none;gap:6px;flex-wrap:wrap;align-items:center;padding:2px 16px 10px}
|
|
494
|
+
.sl-picker[data-layout="narrow"] .sl-filtersec.has{display:block}
|
|
495
|
+
.sl-picker[data-layout="narrow"] .sl-filters.has{display:flex}
|
|
496
|
+
.sl-cbbtn{width:32px;height:32px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
497
|
+
color:var(--sl-text);display:flex;align-items:center;justify-content:center;transition:border-color .15s}
|
|
498
|
+
.sl-cbbtn:hover{border-color:var(--sl-muted)}
|
|
499
|
+
.sl-cbbtn svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
447
500
|
|
|
448
501
|
/* price panel */
|
|
449
502
|
.sl-sec{padding:14px 16px 4px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--sl-muted);font-weight:700}
|
|
@@ -481,24 +534,57 @@ var CSS = `
|
|
|
481
534
|
.sl-foot{padding:12px 16px 14px;border-top:1px solid var(--sl-line);flex:none}
|
|
482
535
|
.sl-total{display:flex;justify-content:space-between;align-items:center;font-size:13px;margin-bottom:10px}
|
|
483
536
|
.sl-total b{font-size:17px;font-variant-numeric:tabular-nums}
|
|
484
|
-
.sl-
|
|
485
|
-
|
|
486
|
-
.
|
|
487
|
-
.sl-cta:
|
|
537
|
+
/* Primary checkout CTA. Scoped under .sl-picker so it OUTWEIGHS the
|
|
538
|
+
'.sl-picker button' reset (0,1,1) \u2014 an unscoped '.sl-cta' (0,1,0) loses to it
|
|
539
|
+
and the button renders as plain text with no accent fill. */
|
|
540
|
+
.sl-picker .sl-cta{display:flex;align-items:center;justify-content:center;width:100%;min-height:44px;
|
|
541
|
+
padding:12px 16px;border-radius:var(--sl-r-sm);font-weight:800;font-size:14px;line-height:1.1;
|
|
542
|
+
background:var(--sl-accent);color:var(--sl-accent-ink);
|
|
543
|
+
transition:filter .15s,background .15s,color .15s,transform .06s}
|
|
544
|
+
.sl-picker .sl-cta:hover{filter:brightness(1.08)}
|
|
545
|
+
.sl-picker .sl-cta:active{transform:translateY(1px);filter:brightness(.94)}
|
|
546
|
+
/* Disabled ("Select seats"): quieter, but still a full-width button shape. */
|
|
547
|
+
.sl-picker .sl-cta:disabled{background:var(--sl-surface);color:var(--sl-muted);opacity:1;
|
|
548
|
+
cursor:not-allowed;filter:none;transform:none}
|
|
549
|
+
|
|
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%}
|
|
488
568
|
|
|
489
|
-
/*
|
|
490
|
-
.sl-
|
|
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}
|
|
491
577
|
.sl-zoom button{width:36px;height:36px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
492
578
|
color:var(--sl-text);font-size:17px;font-weight:700;display:flex;align-items:center;justify-content:center;transition:border-color .15s}
|
|
493
579
|
.sl-zoom button:hover{border-color:var(--sl-muted)}
|
|
494
580
|
.sl-zoom svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
495
581
|
|
|
496
|
-
/* toast + boot states */
|
|
497
|
-
.sl-toast{
|
|
582
|
+
/* toast + boot states (toast flows in the bottom-center region) */
|
|
583
|
+
.sl-toast{transform:translateY(6px);max-width:100%;
|
|
498
584
|
background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-text);border-radius:999px;padding:9px 16px;
|
|
499
585
|
font-size:12.5px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .2s,transform .2s;white-space:nowrap;
|
|
500
586
|
overflow:hidden;text-overflow:ellipsis}
|
|
501
|
-
.sl-toast.on{opacity:1;transform:
|
|
587
|
+
.sl-toast.on{opacity:1;transform:translateY(0)}
|
|
502
588
|
.sl-boot{position:absolute;inset:0;z-index:6;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
|
503
589
|
gap:10px;background:var(--sl-bg);font-size:13px;font-weight:600;color:var(--sl-muted)}
|
|
504
590
|
.sl-boot-spin{width:24px;height:24px;border-radius:50%;border:3px solid var(--sl-line);border-top-color:var(--sl-accent);
|
|
@@ -508,12 +594,12 @@ var CSS = `
|
|
|
508
594
|
.sl-boot-retry{margin-top:4px;padding:9px 20px;border-radius:var(--sl-r-sm);background:var(--sl-accent);
|
|
509
595
|
color:var(--sl-accent-ink);font-weight:700;font-size:13px}
|
|
510
596
|
|
|
511
|
-
/* "Need more time?" extend prompt (bottom-center
|
|
512
|
-
.sl-extend{
|
|
513
|
-
display:none;align-items:center;gap:12px;max-width:
|
|
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);
|
|
514
600
|
color:var(--sl-text);border-radius:14px;padding:10px 12px 10px 16px;box-shadow:0 18px 50px -18px rgba(0,0,0,.6);
|
|
515
601
|
opacity:0;transition:opacity .2s,transform .2s}
|
|
516
|
-
.sl-extend.on{display:flex;opacity:1;transform:
|
|
602
|
+
.sl-extend.on{display:flex;opacity:1;transform:translateY(0)}
|
|
517
603
|
.sl-extend-txt{font-size:12.5px;font-weight:600;line-height:1.35}
|
|
518
604
|
.sl-extend-txt b{font-variant-numeric:tabular-nums}
|
|
519
605
|
.sl-extend-btn{flex:none;padding:8px 14px;border-radius:999px;font-weight:800;font-size:12.5px;
|
|
@@ -532,8 +618,8 @@ var CSS = `
|
|
|
532
618
|
.sl-booked-sub{font-size:13px;color:var(--sl-muted);line-height:1.5;max-width:320px}
|
|
533
619
|
.sl-booked-seats{font-weight:700;color:var(--sl-text)}
|
|
534
620
|
|
|
535
|
-
/* a11y filter chips (
|
|
536
|
-
.sl-chips{
|
|
621
|
+
/* a11y filter chips (flow within the top-left region) */
|
|
622
|
+
.sl-chips{display:flex;gap:6px;flex-wrap:wrap}
|
|
537
623
|
.sl-chip-f{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:999px;font-size:12px;font-weight:700;
|
|
538
624
|
background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-muted);transition:color .15s,border-color .15s}
|
|
539
625
|
.sl-chip-f:hover{color:var(--sl-text)}
|
|
@@ -573,18 +659,18 @@ var CSS = `
|
|
|
573
659
|
.sl-chip .view:hover{color:var(--sl-text)}
|
|
574
660
|
.sl-chip .view svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
575
661
|
|
|
576
|
-
/* arena: LOD rung pills (top-center
|
|
577
|
-
.sl-rungs{
|
|
578
|
-
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}
|
|
579
664
|
.sl-rungs.on{display:inline-flex;gap:2px}
|
|
580
665
|
.sl-rungs button{padding:6px 13px;border-radius:999px;font-size:10.5px;font-weight:800;letter-spacing:.07em;
|
|
581
666
|
color:var(--sl-muted);white-space:nowrap;transition:color .15s}
|
|
582
667
|
.sl-rungs button:hover{color:var(--sl-text)}
|
|
583
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}
|
|
584
671
|
|
|
585
|
-
/* multi-floor switcher (
|
|
586
|
-
.sl-floors{
|
|
587
|
-
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%}
|
|
588
674
|
.sl-floors.on{display:flex}
|
|
589
675
|
.sl-floors button{padding:7px 13px;border-radius:999px;font-size:12px;font-weight:700;background:var(--sl-surface);
|
|
590
676
|
border:1px solid var(--sl-line);color:var(--sl-muted);white-space:nowrap;max-width:100%;overflow:hidden;
|
|
@@ -592,11 +678,25 @@ var CSS = `
|
|
|
592
678
|
.sl-floors button:hover{color:var(--sl-text)}
|
|
593
679
|
.sl-floors button.on{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
|
|
594
680
|
|
|
595
|
-
/* tapped-section summary card
|
|
596
|
-
|
|
597
|
-
|
|
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;
|
|
598
688
|
padding:12px 14px;box-shadow:0 18px 50px -18px rgba(0,0,0,.6);display:none}
|
|
599
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}
|
|
600
700
|
.sl-seccard-head{display:flex;align-items:center;gap:8px}
|
|
601
701
|
.sl-seccard-dot{width:10px;height:10px;border-radius:50%;flex:none}
|
|
602
702
|
.sl-seccard-name{font-weight:800;font-size:14px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
@@ -638,6 +738,20 @@ var CSS = `
|
|
|
638
738
|
font-size:11.5px;font-weight:600;background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-muted);
|
|
639
739
|
white-space:nowrap;pointer-events:none;max-width:90%;overflow:hidden;text-overflow:ellipsis}
|
|
640
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
|
+
|
|
641
755
|
/* modal host */
|
|
642
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}
|
|
643
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)}
|
|
@@ -673,6 +787,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
673
787
|
this.destroyed = false;
|
|
674
788
|
// chrome refs
|
|
675
789
|
this.els = {};
|
|
790
|
+
/** Feature 6 anchor regions — positioned flex containers over the map. */
|
|
791
|
+
this.regions = {};
|
|
676
792
|
this.ro = null;
|
|
677
793
|
this.holdTimer = null;
|
|
678
794
|
this.toastTimer = null;
|
|
@@ -703,6 +819,25 @@ var SeatPicker = class _SeatPicker {
|
|
|
703
819
|
this.viewEl = null;
|
|
704
820
|
this.viewCleanup = null;
|
|
705
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;
|
|
706
841
|
// modal plumbing (set by open())
|
|
707
842
|
this.modalScrim = null;
|
|
708
843
|
this.prevFocus = null;
|
|
@@ -721,11 +856,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
721
856
|
currency: options.currency,
|
|
722
857
|
flashOnLiveChange: true,
|
|
723
858
|
colorblindSafe: options.colorblindSafe,
|
|
724
|
-
onSelectionChange: () =>
|
|
859
|
+
onSelectionChange: () => {
|
|
860
|
+
this.syncTray();
|
|
861
|
+
if (this.controller.getSelection().length) this.collapseSectionCard();
|
|
862
|
+
},
|
|
725
863
|
onStatusChange: () => {
|
|
726
864
|
this.syncPrices();
|
|
727
865
|
this.evictTakenSelections();
|
|
728
866
|
this.detectBooked();
|
|
867
|
+
this.refreshMinimap();
|
|
729
868
|
},
|
|
730
869
|
onHoldExpired: () => {
|
|
731
870
|
this.hold = null;
|
|
@@ -744,6 +883,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
744
883
|
onViewChange: () => {
|
|
745
884
|
this.reanchorConfirm();
|
|
746
885
|
this.syncRung();
|
|
886
|
+
this.drawMinimapRect();
|
|
887
|
+
this.sectionCardOnView();
|
|
747
888
|
},
|
|
748
889
|
// Tapped-section glide-in → surface (or clear) the section-summary card.
|
|
749
890
|
onSectionFocus: (summary) => this.showSectionCard(summary),
|
|
@@ -823,7 +964,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
823
964
|
<div class="sl-body">
|
|
824
965
|
<div class="sl-map">
|
|
825
966
|
<div class="sl-map-host" data-ref="map"></div>
|
|
826
|
-
<div class="sl-zoom">
|
|
967
|
+
<div class="sl-zoom" data-ref="zoom">
|
|
827
968
|
<button type="button" aria-label="Zoom in" data-ref="zin">+</button>
|
|
828
969
|
<button type="button" aria-label="Zoom out" data-ref="zout">\u2212</button>
|
|
829
970
|
<button type="button" aria-label="Fit to screen" data-ref="zfit">
|
|
@@ -833,7 +974,18 @@ var SeatPicker = class _SeatPicker {
|
|
|
833
974
|
<div class="sl-boot" data-ref="boot"><span class="sl-boot-spin"></span>Loading seat map\u2026</div>
|
|
834
975
|
<div class="sl-toast" data-ref="toast" role="status" aria-live="polite"></div>
|
|
835
976
|
</div>
|
|
836
|
-
<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-bar">
|
|
981
|
+
<div class="sl-sheet-peek" data-ref="peek"></div>
|
|
982
|
+
<span class="sl-sheet-chevron" aria-hidden="true">
|
|
983
|
+
<svg viewBox="0 0 24 24"><path d="M6 15l6-6 6 6"/></svg>
|
|
984
|
+
</span>
|
|
985
|
+
</div>
|
|
986
|
+
</div>
|
|
987
|
+
<div class="sl-sec sl-filtersec" data-ref="filtersSec">Filters</div>
|
|
988
|
+
<div class="sl-filters" data-ref="filters"></div>
|
|
837
989
|
<div class="sl-sec" data-ref="pricesSec">Prices</div>
|
|
838
990
|
<div class="sl-prices" data-ref="prices"></div>
|
|
839
991
|
<div class="sl-sec">Your seats</div>
|
|
@@ -848,14 +1000,55 @@ var SeatPicker = class _SeatPicker {
|
|
|
848
1000
|
this.els[el.dataset.ref] = el;
|
|
849
1001
|
});
|
|
850
1002
|
this.mapHost = this.els.map;
|
|
851
|
-
|
|
1003
|
+
const applyLayout = () => {
|
|
852
1004
|
const w = root.clientWidth;
|
|
853
|
-
|
|
854
|
-
|
|
1005
|
+
if (w <= 0) return;
|
|
1006
|
+
const next = w < 640 ? "narrow" : "wide";
|
|
1007
|
+
if (root.dataset.layout === next) return;
|
|
1008
|
+
root.dataset.layout = next;
|
|
1009
|
+
if (next === "narrow" && !root.dataset.sheet) root.dataset.sheet = "peek";
|
|
1010
|
+
this.dockLayoutChrome();
|
|
1011
|
+
};
|
|
1012
|
+
this.ro = new ResizeObserver(applyLayout);
|
|
855
1013
|
this.ro.observe(root);
|
|
1014
|
+
applyLayout();
|
|
1015
|
+
requestAnimationFrame(applyLayout);
|
|
856
1016
|
this.els.zin.addEventListener("click", () => this.controller.zoomIn());
|
|
857
1017
|
this.els.zout.addEventListener("click", () => this.controller.zoomOut());
|
|
858
1018
|
this.els.zfit.addEventListener("click", () => this.controller.zoomToFit());
|
|
1019
|
+
const head = this.els.sheetHead;
|
|
1020
|
+
if (head) {
|
|
1021
|
+
const setSheet = (open) => {
|
|
1022
|
+
root.dataset.sheet = open ? "open" : "peek";
|
|
1023
|
+
};
|
|
1024
|
+
let startY = 0;
|
|
1025
|
+
let swiped = false;
|
|
1026
|
+
let tracking = false;
|
|
1027
|
+
head.addEventListener("pointerdown", (e) => {
|
|
1028
|
+
tracking = true;
|
|
1029
|
+
swiped = false;
|
|
1030
|
+
startY = e.clientY;
|
|
1031
|
+
head.setPointerCapture?.(e.pointerId);
|
|
1032
|
+
});
|
|
1033
|
+
head.addEventListener("pointermove", (e) => {
|
|
1034
|
+
if (!tracking || swiped) return;
|
|
1035
|
+
const dy = e.clientY - startY;
|
|
1036
|
+
if (dy < -18) {
|
|
1037
|
+
setSheet(true);
|
|
1038
|
+
swiped = true;
|
|
1039
|
+
} else if (dy > 18) {
|
|
1040
|
+
setSheet(false);
|
|
1041
|
+
swiped = true;
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
head.addEventListener("pointerup", (e) => {
|
|
1045
|
+
if (tracking && !swiped && Math.abs(e.clientY - startY) < 6) {
|
|
1046
|
+
if (!e.target.closest(".sl-seccard")) setSheet(root.dataset.sheet !== "open");
|
|
1047
|
+
}
|
|
1048
|
+
tracking = false;
|
|
1049
|
+
head.releasePointerCapture?.(e.pointerId);
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
859
1052
|
this.tipEl = document.createElement("div");
|
|
860
1053
|
this.tipEl.setAttribute("role", "tooltip");
|
|
861
1054
|
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;";
|
|
@@ -882,13 +1075,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
882
1075
|
return this;
|
|
883
1076
|
}
|
|
884
1077
|
this.els.boot.remove();
|
|
1078
|
+
this.buildRegions();
|
|
1079
|
+
this.regions["bottom-right"].appendChild(this.els.zoom);
|
|
1080
|
+
this.regions["bottom-center"].appendChild(this.els.toast);
|
|
885
1081
|
if (info.mode === "test") {
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
this.
|
|
891
|
-
this.els.map.appendChild(ribbon);
|
|
1082
|
+
const badge = document.createElement("div");
|
|
1083
|
+
badge.className = "sl-testbadge";
|
|
1084
|
+
badge.textContent = t2("picker.testMode");
|
|
1085
|
+
badge.setAttribute("aria-label", t2("picker.testMode"));
|
|
1086
|
+
this.regions["top-right"].appendChild(badge);
|
|
892
1087
|
}
|
|
893
1088
|
const chartTheme = this.controller.doc?.theme;
|
|
894
1089
|
Object.entries(resolveTokens(chartTheme, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
|
|
@@ -912,7 +1107,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
912
1107
|
const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
|
|
913
1108
|
const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-f="${key}">${label}</button>`;
|
|
914
1109
|
chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
|
|
915
|
-
this.
|
|
1110
|
+
this.regions["top-left"].appendChild(chips);
|
|
1111
|
+
this.a11yChipsEl = chips;
|
|
916
1112
|
chips.querySelectorAll("button").forEach((btn) => {
|
|
917
1113
|
btn.addEventListener("click", () => {
|
|
918
1114
|
const f = btn.dataset.f;
|
|
@@ -924,6 +1120,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
924
1120
|
}
|
|
925
1121
|
const cb = document.createElement("button");
|
|
926
1122
|
cb.type = "button";
|
|
1123
|
+
cb.className = "sl-cbbtn";
|
|
1124
|
+
this.cbEl = cb;
|
|
927
1125
|
cb.setAttribute("aria-label", "Toggle colorblind-friendly colors");
|
|
928
1126
|
cb.setAttribute("aria-pressed", String(!!this.opts.colorblindSafe));
|
|
929
1127
|
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>';
|
|
@@ -939,19 +1137,45 @@ var SeatPicker = class _SeatPicker {
|
|
|
939
1137
|
this.srEl.setAttribute("aria-live", "polite");
|
|
940
1138
|
root.appendChild(this.srEl);
|
|
941
1139
|
this.buildArenaChrome();
|
|
1140
|
+
this.buildMinimap();
|
|
1141
|
+
this.buildPriceFilter();
|
|
942
1142
|
this.buildExtendPrompt();
|
|
943
1143
|
this.buildBookedOverlay();
|
|
1144
|
+
this.dockLayoutChrome();
|
|
944
1145
|
this.syncPrices();
|
|
945
1146
|
this.syncTray();
|
|
946
1147
|
return this;
|
|
947
1148
|
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Move layout-dependent chrome between its wide dock (map regions / zoom
|
|
1151
|
+
* column) and its narrow dock (the sheet's consolidated Filters row), and
|
|
1152
|
+
* re-render the section card in the form the layout wants (docked card/pill
|
|
1153
|
+
* on wide, sheet strip on narrow). Runs on every layout flip + once post-render.
|
|
1154
|
+
*/
|
|
1155
|
+
dockLayoutChrome() {
|
|
1156
|
+
const narrow = this.root?.dataset.layout === "narrow";
|
|
1157
|
+
const filters = this.els.filters;
|
|
1158
|
+
if (filters) {
|
|
1159
|
+
if (narrow) {
|
|
1160
|
+
if (this.a11yChipsEl) filters.appendChild(this.a11yChipsEl);
|
|
1161
|
+
if (this.cbEl) filters.appendChild(this.cbEl);
|
|
1162
|
+
} else {
|
|
1163
|
+
if (this.a11yChipsEl) this.regions["top-left"]?.appendChild(this.a11yChipsEl);
|
|
1164
|
+
if (this.cbEl) this.els.zoom?.appendChild(this.cbEl);
|
|
1165
|
+
}
|
|
1166
|
+
const has = narrow && filters.children.length > 0;
|
|
1167
|
+
filters.classList.toggle("has", has);
|
|
1168
|
+
this.els.filtersSec?.classList.toggle("has", has);
|
|
1169
|
+
}
|
|
1170
|
+
if (this.lastSection) this.renderSectionCard(this.lastSection);
|
|
1171
|
+
}
|
|
948
1172
|
/** The "Need more time?" prompt shown in the hold's final EXTEND_PROMPT_MS. */
|
|
949
1173
|
buildExtendPrompt() {
|
|
950
1174
|
const el = document.createElement("div");
|
|
951
1175
|
el.className = "sl-extend";
|
|
952
1176
|
el.setAttribute("role", "status");
|
|
953
1177
|
el.innerHTML = `<span class="sl-extend-txt" data-ref="extendTxt"></span><button type="button" class="sl-extend-btn" data-ref="extendBtn"></button>`;
|
|
954
|
-
this.els.map.appendChild(el);
|
|
1178
|
+
(this.regions["bottom-center"] ?? this.els.map).appendChild(el);
|
|
955
1179
|
this.extendEl = el;
|
|
956
1180
|
this.els.extendTxt = el.querySelector('[data-ref="extendTxt"]');
|
|
957
1181
|
this.els.extendBtn = el.querySelector('[data-ref="extendBtn"]');
|
|
@@ -969,6 +1193,258 @@ var SeatPicker = class _SeatPicker {
|
|
|
969
1193
|
this.bookedEl = el;
|
|
970
1194
|
this.els.bookedSub = el.querySelector('[data-ref="bookedSub"]');
|
|
971
1195
|
}
|
|
1196
|
+
// ---- Feature 6: chrome anchor regions -------------------------------------
|
|
1197
|
+
/**
|
|
1198
|
+
* Create the positioned flex containers that own every persistent map overlay.
|
|
1199
|
+
* Appended once after controller.render(); each chrome piece is then appended
|
|
1200
|
+
* INTO its region and flows within it, so nothing free-floats over anything
|
|
1201
|
+
* else. Regions carve the map into non-overlapping zones (top strip split into
|
|
1202
|
+
* left/center/right, left rail, and the three used corners).
|
|
1203
|
+
*/
|
|
1204
|
+
buildRegions() {
|
|
1205
|
+
if (!this.els.map) return;
|
|
1206
|
+
const REGIONS = ["top-left", "top-center", "top-right", "left-rail", "bottom-left", "bottom-center", "bottom-right"];
|
|
1207
|
+
for (const region of REGIONS) {
|
|
1208
|
+
const el = document.createElement("div");
|
|
1209
|
+
el.className = "sl-anchor";
|
|
1210
|
+
el.dataset.region = region;
|
|
1211
|
+
this.els.map.appendChild(el);
|
|
1212
|
+
this.regions[region] = el;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
// ---- F3 minimap -----------------------------------------------------------
|
|
1216
|
+
/** Read a resolved --sl-* token value (canvas needs a real color, not var()). */
|
|
1217
|
+
cssVar(name) {
|
|
1218
|
+
return this.root ? getComputedStyle(this.root).getPropertyValue(name).trim() : "";
|
|
1219
|
+
}
|
|
1220
|
+
/** Section-bearing objects on the active floor (single-floor → doc.objects). */
|
|
1221
|
+
activeFloorObjects() {
|
|
1222
|
+
const doc = this.controller.doc;
|
|
1223
|
+
if (!doc) return [];
|
|
1224
|
+
const floors = doc.floors;
|
|
1225
|
+
if (floors?.length) {
|
|
1226
|
+
const id = this.controller.getActiveFloorId();
|
|
1227
|
+
return (floors.find((f) => f.id === id) ?? floors[0]).objects ?? [];
|
|
1228
|
+
}
|
|
1229
|
+
return doc.objects ?? [];
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Build the overview minimap: a static venue thumbnail (section outlines, or
|
|
1233
|
+
* seat dots when the chart has no sections) with the live viewport rectangle
|
|
1234
|
+
* drawn on top. The rect tracks pan/zoom via the constructor's onViewChange.
|
|
1235
|
+
*/
|
|
1236
|
+
buildMinimap() {
|
|
1237
|
+
const vp = this.controller.getViewport();
|
|
1238
|
+
if (!vp || !this.els.map) return;
|
|
1239
|
+
const b = vp.bounds;
|
|
1240
|
+
if (!(b.width > 0 && b.height > 0)) return;
|
|
1241
|
+
const MAXW = 158;
|
|
1242
|
+
const MAXH = 118;
|
|
1243
|
+
const PAD = 6;
|
|
1244
|
+
const aspect = b.width / Math.max(1, b.height);
|
|
1245
|
+
let w = MAXW;
|
|
1246
|
+
let h = Math.round(MAXW / aspect);
|
|
1247
|
+
if (h > MAXH) {
|
|
1248
|
+
h = MAXH;
|
|
1249
|
+
w = Math.round(MAXH * aspect);
|
|
1250
|
+
}
|
|
1251
|
+
w = Math.max(64, w);
|
|
1252
|
+
h = Math.max(48, h);
|
|
1253
|
+
const dpr = Math.min(2, window.devicePixelRatio || 1);
|
|
1254
|
+
const wrap = document.createElement("div");
|
|
1255
|
+
wrap.className = "sl-minimap";
|
|
1256
|
+
wrap.setAttribute("aria-hidden", "true");
|
|
1257
|
+
const canvas = document.createElement("canvas");
|
|
1258
|
+
canvas.width = Math.round(w * dpr);
|
|
1259
|
+
canvas.height = Math.round(h * dpr);
|
|
1260
|
+
canvas.style.width = `${w}px`;
|
|
1261
|
+
canvas.style.height = `${h}px`;
|
|
1262
|
+
wrap.appendChild(canvas);
|
|
1263
|
+
(this.regions["bottom-left"] ?? this.els.map).appendChild(wrap);
|
|
1264
|
+
this.miniEl = wrap;
|
|
1265
|
+
this.miniCanvas = canvas;
|
|
1266
|
+
const scale = Math.min((w - PAD * 2) / Math.max(1, b.width), (h - PAD * 2) / Math.max(1, b.height)) * dpr;
|
|
1267
|
+
const offX = (w * dpr - b.width * scale) / 2 - b.x * scale;
|
|
1268
|
+
const offY = (h * dpr - b.height * scale) / 2 - b.y * scale;
|
|
1269
|
+
this.miniTf = { scale, offX, offY, dpr };
|
|
1270
|
+
const base = document.createElement("canvas");
|
|
1271
|
+
base.width = canvas.width;
|
|
1272
|
+
base.height = canvas.height;
|
|
1273
|
+
this.miniBase = base;
|
|
1274
|
+
wrap.addEventListener("click", (e) => this.minimapJump(e));
|
|
1275
|
+
this.drawMinimapStatic();
|
|
1276
|
+
this.drawMinimapRect();
|
|
1277
|
+
}
|
|
1278
|
+
/** Repaint the static overview + rect (floor switch, live open/close). */
|
|
1279
|
+
refreshMinimap() {
|
|
1280
|
+
if (!this.miniBase) return;
|
|
1281
|
+
this.drawMinimapStatic();
|
|
1282
|
+
this.drawMinimapRect();
|
|
1283
|
+
}
|
|
1284
|
+
/** Paint the venue overview into the offscreen base canvas. */
|
|
1285
|
+
drawMinimapStatic() {
|
|
1286
|
+
const base = this.miniBase;
|
|
1287
|
+
const tf = this.miniTf;
|
|
1288
|
+
const doc = this.controller.doc;
|
|
1289
|
+
if (!base || !tf || !doc) return;
|
|
1290
|
+
const ctx = base.getContext("2d");
|
|
1291
|
+
if (!ctx) return;
|
|
1292
|
+
ctx.clearRect(0, 0, base.width, base.height);
|
|
1293
|
+
const fx = (x) => x * tf.scale + tf.offX;
|
|
1294
|
+
const fy = (y) => y * tf.scale + tf.offY;
|
|
1295
|
+
const line = this.cssVar("--sl-line") || "rgba(139,147,167,.5)";
|
|
1296
|
+
const muted = this.cssVar("--sl-muted") || "#8b93a7";
|
|
1297
|
+
const accent = this.cssVar("--sl-accent") || "#6e7bff";
|
|
1298
|
+
const zoneColor = new Map((doc.zones ?? []).map((z) => [z.id, z.color]));
|
|
1299
|
+
let drewSection = false;
|
|
1300
|
+
for (const o of this.activeFloorObjects()) {
|
|
1301
|
+
if (o.type !== "section" || !o.outline || o.outline.length < 3) continue;
|
|
1302
|
+
drewSection = true;
|
|
1303
|
+
const closed = this.controller.isSectionClosed(o.id);
|
|
1304
|
+
const fill = closed ? muted : o.color ?? (o.zone && zoneColor.get(o.zone)) ?? accent;
|
|
1305
|
+
ctx.beginPath();
|
|
1306
|
+
o.outline.forEach((p, i) => i === 0 ? ctx.moveTo(fx(p.x), fy(p.y)) : ctx.lineTo(fx(p.x), fy(p.y)));
|
|
1307
|
+
ctx.closePath();
|
|
1308
|
+
ctx.globalAlpha = closed ? 0.26 : 0.42;
|
|
1309
|
+
ctx.fillStyle = fill;
|
|
1310
|
+
ctx.fill();
|
|
1311
|
+
ctx.globalAlpha = 0.85;
|
|
1312
|
+
ctx.lineWidth = Math.max(1, tf.dpr);
|
|
1313
|
+
ctx.strokeStyle = line;
|
|
1314
|
+
ctx.stroke();
|
|
1315
|
+
}
|
|
1316
|
+
ctx.globalAlpha = 1;
|
|
1317
|
+
if (!drewSection) {
|
|
1318
|
+
const r = Math.max(1, tf.dpr);
|
|
1319
|
+
for (const seat of expandChart(doc)) {
|
|
1320
|
+
const cat = doc.categories.find((c) => c.key === seat.categoryKey);
|
|
1321
|
+
ctx.fillStyle = cat?.color ?? accent;
|
|
1322
|
+
ctx.beginPath();
|
|
1323
|
+
ctx.arc(fx(seat.x), fy(seat.y), r, 0, Math.PI * 2);
|
|
1324
|
+
ctx.fill();
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
/** Blit the base overview, then stroke the current viewport rectangle on top. */
|
|
1329
|
+
drawMinimapRect() {
|
|
1330
|
+
const canvas = this.miniCanvas;
|
|
1331
|
+
const base = this.miniBase;
|
|
1332
|
+
const tf = this.miniTf;
|
|
1333
|
+
if (!canvas || !base || !tf) return;
|
|
1334
|
+
const ctx = canvas.getContext("2d");
|
|
1335
|
+
if (!ctx) return;
|
|
1336
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
1337
|
+
ctx.drawImage(base, 0, 0);
|
|
1338
|
+
const vp = this.controller.getViewport();
|
|
1339
|
+
if (!vp) return;
|
|
1340
|
+
const v = vp.visible;
|
|
1341
|
+
const x = v.x * tf.scale + tf.offX;
|
|
1342
|
+
const y = v.y * tf.scale + tf.offY;
|
|
1343
|
+
const w = v.width * tf.scale;
|
|
1344
|
+
const h = v.height * tf.scale;
|
|
1345
|
+
const accent = this.cssVar("--sl-accent") || "#f4b740";
|
|
1346
|
+
ctx.save();
|
|
1347
|
+
ctx.globalAlpha = 0.14;
|
|
1348
|
+
ctx.fillStyle = accent;
|
|
1349
|
+
ctx.fillRect(x, y, w, h);
|
|
1350
|
+
ctx.globalAlpha = 1;
|
|
1351
|
+
ctx.lineWidth = Math.max(1.5, tf.dpr * 1.5);
|
|
1352
|
+
ctx.strokeStyle = accent;
|
|
1353
|
+
ctx.strokeRect(x, y, w, h);
|
|
1354
|
+
ctx.restore();
|
|
1355
|
+
}
|
|
1356
|
+
/** Minimap click → focus the section under the point (or overview on a miss). */
|
|
1357
|
+
minimapJump(e) {
|
|
1358
|
+
const canvas = this.miniCanvas;
|
|
1359
|
+
const tf = this.miniTf;
|
|
1360
|
+
if (!canvas || !tf) return;
|
|
1361
|
+
const r = canvas.getBoundingClientRect();
|
|
1362
|
+
const px = (e.clientX - r.left) * (canvas.width / r.width);
|
|
1363
|
+
const py = (e.clientY - r.top) * (canvas.height / r.height);
|
|
1364
|
+
const wx = (px - tf.offX) / tf.scale;
|
|
1365
|
+
const wy = (py - tf.offY) / tf.scale;
|
|
1366
|
+
for (const o of this.activeFloorObjects()) {
|
|
1367
|
+
if (o.type !== "section" || !o.outline || o.outline.length < 3) continue;
|
|
1368
|
+
if (this.controller.isSectionClosed(o.id)) continue;
|
|
1369
|
+
if (pointInPolygon(wx, wy, o.outline)) {
|
|
1370
|
+
this.controller.focusSection(o.id);
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
this.controller.overview();
|
|
1375
|
+
}
|
|
1376
|
+
// ---- F4 price-band filter -------------------------------------------------
|
|
1377
|
+
/** Effective price of a category (first tier when tiered, else base price). */
|
|
1378
|
+
catPrice(c) {
|
|
1379
|
+
return c.tiers?.length ? c.tiers[0].price : c.price;
|
|
1380
|
+
}
|
|
1381
|
+
/** Derive price bands: one chip per distinct price (≤5), else quantile ranges. */
|
|
1382
|
+
priceBands() {
|
|
1383
|
+
const doc = this.controller.doc;
|
|
1384
|
+
if (!doc) return [];
|
|
1385
|
+
const priced = doc.categories.map((c) => ({ key: c.key, price: this.catPrice(c) })).filter((x) => x.price != null);
|
|
1386
|
+
if (!priced.length) return [];
|
|
1387
|
+
const distinct = [...new Set(priced.map((p) => p.price))].sort((a, b) => a - b);
|
|
1388
|
+
if (distinct.length <= 5) {
|
|
1389
|
+
return distinct.map((price) => ({
|
|
1390
|
+
id: `p${price}`,
|
|
1391
|
+
label: this.money(price),
|
|
1392
|
+
keys: priced.filter((p) => p.price === price).map((p) => p.key),
|
|
1393
|
+
min: price,
|
|
1394
|
+
max: price
|
|
1395
|
+
}));
|
|
1396
|
+
}
|
|
1397
|
+
const chunk = Math.ceil(distinct.length / 4);
|
|
1398
|
+
const bands = [];
|
|
1399
|
+
for (let i = 0; i < distinct.length; i += chunk) {
|
|
1400
|
+
const slice = distinct.slice(i, i + chunk);
|
|
1401
|
+
const lo = slice[0];
|
|
1402
|
+
const hi = slice[slice.length - 1];
|
|
1403
|
+
bands.push({
|
|
1404
|
+
id: `b${i}`,
|
|
1405
|
+
label: lo === hi ? this.money(lo) : `${this.money(lo)}\u2013${this.money(hi)}`,
|
|
1406
|
+
keys: priced.filter((p) => p.price >= lo && p.price <= hi).map((p) => p.key),
|
|
1407
|
+
min: lo,
|
|
1408
|
+
max: hi
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
return bands;
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Build the price-band chip row in the side panel, directly under the "Prices"
|
|
1415
|
+
* header and above the legend it dims. Living in the panel (not a map overlay)
|
|
1416
|
+
* keeps it clear of the top-center rung pills and top-left a11y chips, and it
|
|
1417
|
+
* rides the bottom sheet on narrow layouts. Skipped when there's <2 bands.
|
|
1418
|
+
*/
|
|
1419
|
+
buildPriceFilter() {
|
|
1420
|
+
if (!this.els.prices || !this.els.pricesSec) return;
|
|
1421
|
+
const bands = this.priceBands();
|
|
1422
|
+
if (bands.length < 2) return;
|
|
1423
|
+
const wrap = document.createElement("div");
|
|
1424
|
+
wrap.className = "sl-pricef";
|
|
1425
|
+
wrap.setAttribute("role", "group");
|
|
1426
|
+
wrap.setAttribute("aria-label", "Filter seats by price");
|
|
1427
|
+
const mk = (band, label, on) => `<button type="button" class="sl-chip-f${on ? " on" : ""}" data-band="${band}">${label}</button>`;
|
|
1428
|
+
wrap.innerHTML = mk("all", "All prices", true) + bands.map((bd) => mk(bd.id, bd.label, false)).join("");
|
|
1429
|
+
this.els.pricesSec.after(wrap);
|
|
1430
|
+
this.priceFilterEl = wrap;
|
|
1431
|
+
wrap.querySelectorAll("button").forEach((btn) => {
|
|
1432
|
+
btn.addEventListener("click", () => {
|
|
1433
|
+
wrap.querySelectorAll("button").forEach((b) => b.classList.toggle("on", b === btn));
|
|
1434
|
+
const id = btn.dataset.band;
|
|
1435
|
+
if (id === "all") {
|
|
1436
|
+
this.priceBandKeys = null;
|
|
1437
|
+
this.controller.setCategoryFilter(null);
|
|
1438
|
+
} else {
|
|
1439
|
+
const bd = bands.find((x) => x.id === id);
|
|
1440
|
+
this.priceBandKeys = bd ? new Set(bd.keys) : null;
|
|
1441
|
+
this.controller.setCategoryFilter(bd ? bd.keys : null);
|
|
1442
|
+
}
|
|
1443
|
+
this.syncPrices();
|
|
1444
|
+
if (this.lastSection) this.showSectionCard(this.lastSection);
|
|
1445
|
+
});
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
972
1448
|
// ---- arena / multi-floor chrome -------------------------------------------
|
|
973
1449
|
/** Build the rung pills (charts with sections) and floor switcher (>1 floor). */
|
|
974
1450
|
buildArenaChrome() {
|
|
@@ -997,7 +1473,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
997
1473
|
pills.querySelectorAll("button").forEach((btn) => {
|
|
998
1474
|
btn.addEventListener("click", () => this.controller.setRung(btn.dataset.rung));
|
|
999
1475
|
});
|
|
1000
|
-
this.
|
|
1476
|
+
this.regions["top-center"].appendChild(pills);
|
|
1001
1477
|
this.rungsEl = pills;
|
|
1002
1478
|
this.syncRung();
|
|
1003
1479
|
}
|
|
@@ -1014,9 +1490,10 @@ var SeatPicker = class _SeatPicker {
|
|
|
1014
1490
|
this.showSectionCard(null);
|
|
1015
1491
|
this.syncFloors();
|
|
1016
1492
|
this.syncRung();
|
|
1493
|
+
this.refreshMinimap();
|
|
1017
1494
|
});
|
|
1018
1495
|
});
|
|
1019
|
-
this.
|
|
1496
|
+
this.regions["left-rail"].appendChild(rail);
|
|
1020
1497
|
this.floorsEl = rail;
|
|
1021
1498
|
this.syncFloors();
|
|
1022
1499
|
}
|
|
@@ -1041,27 +1518,105 @@ var SeatPicker = class _SeatPicker {
|
|
|
1041
1518
|
}
|
|
1042
1519
|
/** Show (or clear, on null) the tapped-section summary card. */
|
|
1043
1520
|
showSectionCard(summary) {
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1521
|
+
this.lastSection = summary;
|
|
1522
|
+
this.secCardEl?.remove();
|
|
1523
|
+
this.secCardEl = null;
|
|
1524
|
+
if (!summary) return;
|
|
1525
|
+
this.secCardCollapsed = false;
|
|
1526
|
+
this.secCardShownAt = Date.now();
|
|
1527
|
+
this.renderSectionCard(summary);
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Render the section card in the form the layout + state want: expanded card
|
|
1531
|
+
* or slim pill in the top-center anchor region (wide), or a compact strip in
|
|
1532
|
+
* the sheet head (narrow). Never floats over the seats at the tap point.
|
|
1533
|
+
*/
|
|
1534
|
+
renderSectionCard(summary) {
|
|
1049
1535
|
if (!this.els.map) return;
|
|
1050
1536
|
this.secCardEl?.remove();
|
|
1051
1537
|
const priceLabel = summary.priceMin === summary.priceMax ? this.money(summary.priceMin) : `${this.money(summary.priceMin)}\u2013${this.money(summary.priceMax)}`;
|
|
1538
|
+
const leftLabel = tCount("picker.seatsLeftInSection", summary.seatsLeft);
|
|
1539
|
+
const xBtn = `<button type="button" class="sl-seccard-x" aria-label="${t2("picker.closeSectionSummary")}">\u2715</button>`;
|
|
1052
1540
|
const card = document.createElement("div");
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1541
|
+
const narrow = this.root?.dataset.layout === "narrow";
|
|
1542
|
+
if (narrow) {
|
|
1543
|
+
card.className = "sl-seccard strip on";
|
|
1544
|
+
card.setAttribute("role", "status");
|
|
1545
|
+
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;
|
|
1546
|
+
card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
|
|
1547
|
+
(this.els.sheetHead ?? this.els.side ?? this.els.map).appendChild(card);
|
|
1548
|
+
} else if (this.secCardCollapsed) {
|
|
1549
|
+
card.className = "sl-seccard mini on";
|
|
1550
|
+
card.setAttribute("role", "button");
|
|
1551
|
+
card.setAttribute("aria-label", t2("picker.sectionSummaryAria", { label: summary.label }));
|
|
1552
|
+
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;
|
|
1553
|
+
card.addEventListener("click", (e) => {
|
|
1554
|
+
if (e.target.closest(".sl-seccard-x")) return;
|
|
1555
|
+
this.secCardCollapsed = false;
|
|
1556
|
+
this.secCardShownAt = Date.now();
|
|
1557
|
+
this.renderSectionCard(summary);
|
|
1558
|
+
});
|
|
1559
|
+
card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
|
|
1560
|
+
(this.regions["top-center"] ?? this.els.map).appendChild(card);
|
|
1561
|
+
} else {
|
|
1562
|
+
card.className = "sl-seccard on";
|
|
1563
|
+
card.setAttribute("role", "dialog");
|
|
1564
|
+
card.setAttribute("aria-label", t2("picker.sectionSummaryAria", { label: summary.label }));
|
|
1565
|
+
const mix = summary.categories.map((c) => {
|
|
1566
|
+
const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
|
|
1567
|
+
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>`;
|
|
1568
|
+
}).join("");
|
|
1569
|
+
card.innerHTML = `<div class="sl-seccard-head"><span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span>` + (summary.categories.length ? `<span class="sl-seccard-price">${priceLabel}</span>` : "") + xBtn + `</div><div class="sl-seccard-zone">${summary.zoneLabel ? `${summary.zoneLabel} \xB7 ` : ""}<span class="sl-seccard-left">${leftLabel}</span></div>` + (mix ? `<div class="sl-seccard-mix">${mix}</div>` : "") + `<div class="sl-seccard-foot"><button type="button" class="sl-seccard-overview">\u2190 ${t2("picker.overview")}</button><span class="sl-seccard-hint">${t2("picker.tapSeatHint")}</span></div>`;
|
|
1570
|
+
card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
|
|
1571
|
+
card.querySelector(".sl-seccard-overview").addEventListener("click", () => this.controller.overview());
|
|
1572
|
+
(this.regions["top-center"] ?? this.els.map).appendChild(card);
|
|
1573
|
+
}
|
|
1063
1574
|
this.secCardEl = card;
|
|
1064
1575
|
}
|
|
1576
|
+
/** Collapse the expanded card to its slim pill (seat-picking started). */
|
|
1577
|
+
collapseSectionCard() {
|
|
1578
|
+
if (!this.secCardEl || this.secCardCollapsed || !this.lastSection) return;
|
|
1579
|
+
if (this.root?.dataset.layout === "narrow") return;
|
|
1580
|
+
this.secCardCollapsed = true;
|
|
1581
|
+
this.renderSectionCard(this.lastSection);
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* onViewChange hook for the card. The focus glide's own settle (within the
|
|
1585
|
+
* grace window) enforces the ~25% coverage rule with the FINAL viewport; any
|
|
1586
|
+
* later pan/zoom means seat-picking has begun → collapse to the pill.
|
|
1587
|
+
*/
|
|
1588
|
+
sectionCardOnView() {
|
|
1589
|
+
if (!this.secCardEl || this.secCardCollapsed || !this.lastSection) return;
|
|
1590
|
+
if (this.root?.dataset.layout === "narrow") return;
|
|
1591
|
+
if (Date.now() - this.secCardShownAt < 1400) {
|
|
1592
|
+
if (this.sectionCardCoverage() > 0.25) this.collapseSectionCard();
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
this.collapseSectionCard();
|
|
1596
|
+
}
|
|
1597
|
+
/** Fraction of the focused section's on-screen bbox covered by the card. */
|
|
1598
|
+
sectionCardCoverage() {
|
|
1599
|
+
const card = this.secCardEl;
|
|
1600
|
+
const sec = this.lastSection;
|
|
1601
|
+
if (!card || !sec || !this.els.map) return 0;
|
|
1602
|
+
const outline = this.activeFloorObjects().find((o) => o.type === "section" && o.id === sec.id)?.outline;
|
|
1603
|
+
if (!outline || outline.length < 3) return 0;
|
|
1604
|
+
const pts = outline.map((p) => this.controller.worldToScreen(p));
|
|
1605
|
+
const xs = pts.map((p) => p.x);
|
|
1606
|
+
const ys = pts.map((p) => p.y);
|
|
1607
|
+
const bx = Math.min(...xs);
|
|
1608
|
+
const by = Math.min(...ys);
|
|
1609
|
+
const bw = Math.max(...xs) - bx;
|
|
1610
|
+
const bh = Math.max(...ys) - by;
|
|
1611
|
+
if (bw <= 0 || bh <= 0) return 0;
|
|
1612
|
+
const mapR = this.els.map.getBoundingClientRect();
|
|
1613
|
+
const cr = card.getBoundingClientRect();
|
|
1614
|
+
const cx = cr.left - mapR.left;
|
|
1615
|
+
const cy = cr.top - mapR.top;
|
|
1616
|
+
const ox = Math.max(0, Math.min(cx + cr.width, bx + bw) - Math.max(cx, bx));
|
|
1617
|
+
const oy = Math.max(0, Math.min(cy + cr.height, by + bh) - Math.max(cy, by));
|
|
1618
|
+
return ox * oy / (bw * bh);
|
|
1619
|
+
}
|
|
1065
1620
|
/** aria-live readout when keyboard focus lands on a seat. */
|
|
1066
1621
|
announceSeat(seat) {
|
|
1067
1622
|
if (!this.srEl) return;
|
|
@@ -1236,7 +1791,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
1236
1791
|
const left = this.controller.categoryAvailability();
|
|
1237
1792
|
this.els.prices.innerHTML = doc.categories.map((c) => {
|
|
1238
1793
|
const price = c.tiers?.length ? c.tiers[0].price : c.price;
|
|
1239
|
-
|
|
1794
|
+
const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
|
|
1795
|
+
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>`;
|
|
1240
1796
|
}).join("");
|
|
1241
1797
|
this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
|
|
1242
1798
|
row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
|
|
@@ -1342,6 +1898,18 @@ var SeatPicker = class _SeatPicker {
|
|
|
1342
1898
|
const cta = this.els.cta;
|
|
1343
1899
|
cta.disabled = count === 0;
|
|
1344
1900
|
cta.textContent = this.hold ? "Continue to checkout" : count ? "Hold seats & checkout" : "Select seats";
|
|
1901
|
+
if (this.els.peek) {
|
|
1902
|
+
if (count) {
|
|
1903
|
+
this.els.peek.innerHTML = `<span>${count} ${count === 1 ? "ticket" : "tickets"} \xB7 ${this.money(total)}</span><span class="go">${this.hold ? "Continue" : "Review"}</span>`;
|
|
1904
|
+
} else {
|
|
1905
|
+
const prices = (this.controller.doc?.categories ?? []).map((c) => this.catPrice(c)).filter((p) => p != null);
|
|
1906
|
+
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>`;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
if (this.root?.dataset.layout === "narrow" && count > 0 && this.lastTrayCount === 0) {
|
|
1910
|
+
this.root.dataset.sheet = "open";
|
|
1911
|
+
}
|
|
1912
|
+
this.lastTrayCount = count;
|
|
1345
1913
|
this.opts.onSelectionChange?.(seats);
|
|
1346
1914
|
}
|
|
1347
1915
|
async handleCta() {
|