@seatlayer/js 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +317 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +31 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +320 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -324,6 +324,12 @@ interface SeatPickerOptions {
|
|
|
324
324
|
* price · Add/Cancel) instead of adding straight to the tray. Default false.
|
|
325
325
|
*/
|
|
326
326
|
confirmSelection?: boolean;
|
|
327
|
+
/**
|
|
328
|
+
* Offer a "View from seat" 360° preview (confirm popover + tray chips). The
|
|
329
|
+
* panorama is generated from the chart geometry, or the organizer's uploaded
|
|
330
|
+
* photo when a seat carries one. Default true; set false to hide the affordance.
|
|
331
|
+
*/
|
|
332
|
+
seatView?: boolean;
|
|
327
333
|
/**
|
|
328
334
|
* Buyer pressed the CTA and the hold succeeded — hand off to YOUR checkout.
|
|
329
335
|
* The hold carries holdId, expiresAt, seat labels and priced line items.
|
|
@@ -359,6 +365,12 @@ declare class SeatPicker {
|
|
|
359
365
|
private a11yFilter;
|
|
360
366
|
private baQty;
|
|
361
367
|
private baCat;
|
|
368
|
+
private rungsEl;
|
|
369
|
+
private floorsEl;
|
|
370
|
+
private secCardEl;
|
|
371
|
+
private viewEl;
|
|
372
|
+
private viewCleanup;
|
|
373
|
+
private allSeatsCache;
|
|
362
374
|
private modalScrim;
|
|
363
375
|
private prevFocus;
|
|
364
376
|
private escHandler;
|
|
@@ -374,11 +386,30 @@ declare class SeatPicker {
|
|
|
374
386
|
static open(options: Omit<SeatPickerOptions, 'container'>): Promise<SeatPicker>;
|
|
375
387
|
constructor(options: SeatPickerOptions);
|
|
376
388
|
render(): Promise<this>;
|
|
389
|
+
/** Build the rung pills (charts with sections) and floor switcher (>1 floor). */
|
|
390
|
+
private buildArenaChrome;
|
|
391
|
+
/** Reflect the engine's current LOD rung onto the pill group. */
|
|
392
|
+
private syncRung;
|
|
393
|
+
/** Reflect the active floor onto the switcher rail. */
|
|
394
|
+
private syncFloors;
|
|
395
|
+
/** Show (or clear, on null) the tapped-section summary card. */
|
|
396
|
+
private showSectionCard;
|
|
377
397
|
/** aria-live readout when keyboard focus lands on a seat. */
|
|
378
398
|
private announceSeat;
|
|
379
399
|
private showConfirm;
|
|
380
400
|
private reanchorConfirm;
|
|
381
401
|
private closeConfirm;
|
|
402
|
+
private seatViewEnabled;
|
|
403
|
+
/** Every bookable seat (cached) — neighbor heads for the generated panorama. */
|
|
404
|
+
private allSeats;
|
|
405
|
+
/**
|
|
406
|
+
* Open the drag-to-look-around 360° preview for a seat. Uses the organizer's
|
|
407
|
+
* uploaded photo (seat.viewUrl) when present, else a panorama generated from
|
|
408
|
+
* the chart geometry — the stage placed at this seat's true bearing + size.
|
|
409
|
+
* Zero extra dependencies: an equirectangular image panned with `repeat-x`.
|
|
410
|
+
*/
|
|
411
|
+
private openSeatView;
|
|
412
|
+
private closeSeatView;
|
|
382
413
|
private money;
|
|
383
414
|
private syncPrices;
|
|
384
415
|
/** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
|
package/dist/index.d.ts
CHANGED
|
@@ -324,6 +324,12 @@ interface SeatPickerOptions {
|
|
|
324
324
|
* price · Add/Cancel) instead of adding straight to the tray. Default false.
|
|
325
325
|
*/
|
|
326
326
|
confirmSelection?: boolean;
|
|
327
|
+
/**
|
|
328
|
+
* Offer a "View from seat" 360° preview (confirm popover + tray chips). The
|
|
329
|
+
* panorama is generated from the chart geometry, or the organizer's uploaded
|
|
330
|
+
* photo when a seat carries one. Default true; set false to hide the affordance.
|
|
331
|
+
*/
|
|
332
|
+
seatView?: boolean;
|
|
327
333
|
/**
|
|
328
334
|
* Buyer pressed the CTA and the hold succeeded — hand off to YOUR checkout.
|
|
329
335
|
* The hold carries holdId, expiresAt, seat labels and priced line items.
|
|
@@ -359,6 +365,12 @@ declare class SeatPicker {
|
|
|
359
365
|
private a11yFilter;
|
|
360
366
|
private baQty;
|
|
361
367
|
private baCat;
|
|
368
|
+
private rungsEl;
|
|
369
|
+
private floorsEl;
|
|
370
|
+
private secCardEl;
|
|
371
|
+
private viewEl;
|
|
372
|
+
private viewCleanup;
|
|
373
|
+
private allSeatsCache;
|
|
362
374
|
private modalScrim;
|
|
363
375
|
private prevFocus;
|
|
364
376
|
private escHandler;
|
|
@@ -374,11 +386,30 @@ declare class SeatPicker {
|
|
|
374
386
|
static open(options: Omit<SeatPickerOptions, 'container'>): Promise<SeatPicker>;
|
|
375
387
|
constructor(options: SeatPickerOptions);
|
|
376
388
|
render(): Promise<this>;
|
|
389
|
+
/** Build the rung pills (charts with sections) and floor switcher (>1 floor). */
|
|
390
|
+
private buildArenaChrome;
|
|
391
|
+
/** Reflect the engine's current LOD rung onto the pill group. */
|
|
392
|
+
private syncRung;
|
|
393
|
+
/** Reflect the active floor onto the switcher rail. */
|
|
394
|
+
private syncFloors;
|
|
395
|
+
/** Show (or clear, on null) the tapped-section summary card. */
|
|
396
|
+
private showSectionCard;
|
|
377
397
|
/** aria-live readout when keyboard focus lands on a seat. */
|
|
378
398
|
private announceSeat;
|
|
379
399
|
private showConfirm;
|
|
380
400
|
private reanchorConfirm;
|
|
381
401
|
private closeConfirm;
|
|
402
|
+
private seatViewEnabled;
|
|
403
|
+
/** Every bookable seat (cached) — neighbor heads for the generated panorama. */
|
|
404
|
+
private allSeats;
|
|
405
|
+
/**
|
|
406
|
+
* Open the drag-to-look-around 360° preview for a seat. Uses the organizer's
|
|
407
|
+
* uploaded photo (seat.viewUrl) when present, else a panorama generated from
|
|
408
|
+
* the chart geometry — the stage placed at this seat's true bearing + size.
|
|
409
|
+
* Zero extra dependencies: an equirectangular image panned with `repeat-x`.
|
|
410
|
+
*/
|
|
411
|
+
private openSeatView;
|
|
412
|
+
private closeSeatView;
|
|
382
413
|
private money;
|
|
383
414
|
private syncPrices;
|
|
384
415
|
/** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
|
package/dist/index.js
CHANGED
|
@@ -378,9 +378,11 @@ var EmbeddedDesigner = class {
|
|
|
378
378
|
import {
|
|
379
379
|
PickerController as PickerController2,
|
|
380
380
|
expandChart,
|
|
381
|
+
generateSeatPanorama,
|
|
381
382
|
loadLocale as loadLocale2,
|
|
382
383
|
setStringOverrides as setStringOverrides2,
|
|
383
|
-
t as t2
|
|
384
|
+
t as t2,
|
|
385
|
+
tCount
|
|
384
386
|
} from "@seatlayer/core";
|
|
385
387
|
var DEFAULT_API_BASE2 = "https://api.seatlayer.io";
|
|
386
388
|
var DEFAULT_MAX_SELECTION2 = 10;
|
|
@@ -530,6 +532,79 @@ var CSS = `
|
|
|
530
532
|
/* screen-reader live region */
|
|
531
533
|
.sl-sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
|
532
534
|
|
|
535
|
+
/* per-seat ticket-tier select + view-from-seat button in tray chips */
|
|
536
|
+
.sl-chip .tier{background:var(--sl-bg);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:7px;
|
|
537
|
+
font:inherit;font-size:11px;padding:3px 5px;max-width:130px;cursor:pointer}
|
|
538
|
+
.sl-chip .view{width:24px;height:24px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
539
|
+
color:var(--sl-muted);transition:color .15s}
|
|
540
|
+
.sl-chip .view:hover{color:var(--sl-text)}
|
|
541
|
+
.sl-chip .view svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
542
|
+
|
|
543
|
+
/* arena: LOD rung pills (top-center over the map) */
|
|
544
|
+
.sl-rungs{position:absolute;top:12px;left:50%;transform:translateX(-50%);z-index:5;display:none;
|
|
545
|
+
background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:999px;padding:3px}
|
|
546
|
+
.sl-rungs.on{display:inline-flex;gap:2px}
|
|
547
|
+
.sl-rungs button{padding:6px 13px;border-radius:999px;font-size:10.5px;font-weight:800;letter-spacing:.07em;
|
|
548
|
+
color:var(--sl-muted);white-space:nowrap;transition:color .15s}
|
|
549
|
+
.sl-rungs button:hover{color:var(--sl-text)}
|
|
550
|
+
.sl-rungs button.on{background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
551
|
+
|
|
552
|
+
/* multi-floor switcher (center-left rail over the map) */
|
|
553
|
+
.sl-floors{position:absolute;top:50%;left:12px;transform:translateY(-50%);z-index:5;display:none;
|
|
554
|
+
flex-direction:column;gap:6px;max-width:42%}
|
|
555
|
+
.sl-floors.on{display:flex}
|
|
556
|
+
.sl-floors button{padding:7px 13px;border-radius:999px;font-size:12px;font-weight:700;background:var(--sl-surface);
|
|
557
|
+
border:1px solid var(--sl-line);color:var(--sl-muted);white-space:nowrap;max-width:100%;overflow:hidden;
|
|
558
|
+
text-overflow:ellipsis;transition:color .15s,border-color .15s}
|
|
559
|
+
.sl-floors button:hover{color:var(--sl-text)}
|
|
560
|
+
.sl-floors button.on{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
|
|
561
|
+
|
|
562
|
+
/* tapped-section summary card (top-center, under the rung pills) */
|
|
563
|
+
.sl-seccard{position:absolute;top:54px;left:50%;transform:translateX(-50%);z-index:6;width:250px;
|
|
564
|
+
max-width:calc(100% - 24px);background:var(--sl-surface);border:1px solid var(--sl-line);border-radius:12px;
|
|
565
|
+
padding:12px 14px;box-shadow:0 18px 50px -18px rgba(0,0,0,.6);display:none}
|
|
566
|
+
.sl-seccard.on{display:block}
|
|
567
|
+
.sl-seccard-head{display:flex;align-items:center;gap:8px}
|
|
568
|
+
.sl-seccard-dot{width:10px;height:10px;border-radius:50%;flex:none}
|
|
569
|
+
.sl-seccard-name{font-weight:800;font-size:14px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
570
|
+
.sl-seccard-price{font-weight:800;font-size:12.5px;font-variant-numeric:tabular-nums}
|
|
571
|
+
.sl-seccard-x{width:22px;height:22px;border-radius:999px;flex:none;display:flex;align-items:center;justify-content:center;
|
|
572
|
+
color:var(--sl-muted);font-size:12px}
|
|
573
|
+
.sl-seccard-x:hover{color:var(--sl-text)}
|
|
574
|
+
.sl-seccard-zone{font-size:11.5px;color:var(--sl-muted);margin-top:6px}
|
|
575
|
+
.sl-seccard-left{color:var(--sl-text);font-weight:700}
|
|
576
|
+
.sl-seccard-mix{display:flex;flex-wrap:wrap;gap:6px 10px;margin-top:8px}
|
|
577
|
+
.sl-seccard-mix-item{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:var(--sl-muted)}
|
|
578
|
+
.sl-seccard-mix-dot{width:8px;height:8px;border-radius:50%;flex:none}
|
|
579
|
+
.sl-seccard-mix-price{font-weight:700;color:var(--sl-text)}
|
|
580
|
+
.sl-seccard-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:10px}
|
|
581
|
+
.sl-seccard-overview{font-size:12px;font-weight:800;color:var(--sl-accent)}
|
|
582
|
+
.sl-seccard-hint{font-size:10.5px;color:var(--sl-muted)}
|
|
583
|
+
|
|
584
|
+
/* view-from-seat button on the confirm popover */
|
|
585
|
+
.sl-confirm-view{width:100%;margin-top:9px;padding:8px;border-radius:8px;border:1px solid var(--sl-line);
|
|
586
|
+
color:var(--sl-text);font-weight:700;font-size:12px;display:flex;align-items:center;justify-content:center;gap:7px}
|
|
587
|
+
.sl-confirm-view:hover{border-color:var(--sl-muted)}
|
|
588
|
+
.sl-confirm-view svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
589
|
+
|
|
590
|
+
/* 360\xB0 seat-view modal (fills the widget; drag-to-look-around equirectangular) */
|
|
591
|
+
.sl-view{position:absolute;inset:0;z-index:12;display:flex;flex-direction:column;background:var(--sl-bg)}
|
|
592
|
+
.sl-view-head{display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid var(--sl-line);flex:none}
|
|
593
|
+
.sl-view-title{font-weight:800;font-size:15px}
|
|
594
|
+
.sl-view-cap{font-size:11px;color:var(--sl-muted)}
|
|
595
|
+
.sl-view-x{margin-left:auto;width:32px;height:32px;border-radius:999px;border:1px solid var(--sl-line);color:var(--sl-muted);
|
|
596
|
+
flex:none;display:flex;align-items:center;justify-content:center;transition:color .15s,border-color .15s}
|
|
597
|
+
.sl-view-x:hover{color:var(--sl-text);border-color:var(--sl-muted)}
|
|
598
|
+
.sl-view-x svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round}
|
|
599
|
+
.sl-view-pano{position:relative;flex:1;min-height:0;overflow:hidden;cursor:grab;background-color:#05070c;
|
|
600
|
+
background-repeat:repeat-x;touch-action:none;user-select:none}
|
|
601
|
+
.sl-view-pano.drag{cursor:grabbing}
|
|
602
|
+
.sl-view-badge{position:absolute;top:12px;left:12px;padding:5px 11px;border-radius:999px;font-size:10px;font-weight:800;
|
|
603
|
+
letter-spacing:.08em;background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-muted)}
|
|
604
|
+
.sl-view-hint{position:absolute;left:50%;bottom:12px;transform:translateX(-50%);padding:6px 14px;border-radius:999px;
|
|
605
|
+
font-size:11.5px;font-weight:600;background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-muted);
|
|
606
|
+
white-space:nowrap;pointer-events:none;max-width:90%;overflow:hidden;text-overflow:ellipsis}
|
|
607
|
+
|
|
533
608
|
/* modal host */
|
|
534
609
|
.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}
|
|
535
610
|
.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)}
|
|
@@ -580,6 +655,13 @@ var SeatPicker = class _SeatPicker {
|
|
|
580
655
|
this.a11yFilter = "all";
|
|
581
656
|
this.baQty = 2;
|
|
582
657
|
this.baCat = "";
|
|
658
|
+
// arena / multi-floor / seat-view chrome
|
|
659
|
+
this.rungsEl = null;
|
|
660
|
+
this.floorsEl = null;
|
|
661
|
+
this.secCardEl = null;
|
|
662
|
+
this.viewEl = null;
|
|
663
|
+
this.viewCleanup = null;
|
|
664
|
+
this.allSeatsCache = null;
|
|
583
665
|
// modal plumbing (set by open())
|
|
584
666
|
this.modalScrim = null;
|
|
585
667
|
this.prevFocus = null;
|
|
@@ -615,7 +697,12 @@ var SeatPicker = class _SeatPicker {
|
|
|
615
697
|
onSelect: (seat) => {
|
|
616
698
|
if (this.opts.confirmSelection) this.showConfirm(seat);
|
|
617
699
|
},
|
|
618
|
-
onViewChange: () =>
|
|
700
|
+
onViewChange: () => {
|
|
701
|
+
this.reanchorConfirm();
|
|
702
|
+
this.syncRung();
|
|
703
|
+
},
|
|
704
|
+
// Tapped-section glide-in → surface (or clear) the section-summary card.
|
|
705
|
+
onSectionFocus: (summary) => this.showSectionCard(summary),
|
|
619
706
|
onFocusSeat: (seat) => this.announceSeat(seat),
|
|
620
707
|
onSeatHover: (d) => this.updateTooltip(d),
|
|
621
708
|
onHint: (m) => {
|
|
@@ -807,10 +894,104 @@ var SeatPicker = class _SeatPicker {
|
|
|
807
894
|
this.srEl.className = "sl-sr";
|
|
808
895
|
this.srEl.setAttribute("aria-live", "polite");
|
|
809
896
|
root.appendChild(this.srEl);
|
|
897
|
+
this.buildArenaChrome();
|
|
810
898
|
this.syncPrices();
|
|
811
899
|
this.syncTray();
|
|
812
900
|
return this;
|
|
813
901
|
}
|
|
902
|
+
// ---- arena / multi-floor chrome -------------------------------------------
|
|
903
|
+
/** Build the rung pills (charts with sections) and floor switcher (>1 floor). */
|
|
904
|
+
buildArenaChrome() {
|
|
905
|
+
const doc = this.controller.doc;
|
|
906
|
+
if (!doc || !this.els.map) return;
|
|
907
|
+
const hasSections = doc.objects.some((o) => o.type === "section") || (doc.floors ?? []).some((f) => f.objects.some((o) => o.type === "section"));
|
|
908
|
+
if (hasSections) {
|
|
909
|
+
const RUNGS = ["zones", "sections", "seats"];
|
|
910
|
+
const pills = document.createElement("div");
|
|
911
|
+
pills.className = "sl-rungs on";
|
|
912
|
+
pills.setAttribute("role", "group");
|
|
913
|
+
pills.setAttribute("aria-label", t2("picker.zoomLevel"));
|
|
914
|
+
const LABEL = {
|
|
915
|
+
zones: t2("picker.rungLabel.zones"),
|
|
916
|
+
sections: t2("picker.rungLabel.sections"),
|
|
917
|
+
seats: t2("picker.rungLabel.seats")
|
|
918
|
+
};
|
|
919
|
+
const TIP = {
|
|
920
|
+
zones: t2("picker.rungTip.zones"),
|
|
921
|
+
sections: t2("picker.rungTip.sections"),
|
|
922
|
+
seats: t2("picker.rungTip.seats")
|
|
923
|
+
};
|
|
924
|
+
pills.innerHTML = RUNGS.map(
|
|
925
|
+
(r) => `<button type="button" data-rung="${r}" title="${TIP[r]}" aria-pressed="false">${LABEL[r]}</button>`
|
|
926
|
+
).join("");
|
|
927
|
+
pills.querySelectorAll("button").forEach((btn) => {
|
|
928
|
+
btn.addEventListener("click", () => this.controller.setRung(btn.dataset.rung));
|
|
929
|
+
});
|
|
930
|
+
this.els.map.appendChild(pills);
|
|
931
|
+
this.rungsEl = pills;
|
|
932
|
+
this.syncRung();
|
|
933
|
+
}
|
|
934
|
+
if (this.controller.isMultiFloor()) {
|
|
935
|
+
const floors = this.controller.getFloors();
|
|
936
|
+
const rail = document.createElement("div");
|
|
937
|
+
rail.className = "sl-floors on";
|
|
938
|
+
rail.setAttribute("role", "group");
|
|
939
|
+
rail.setAttribute("aria-label", t2("picker.floor"));
|
|
940
|
+
rail.innerHTML = floors.map((f) => `<button type="button" data-floor="${f.id}">${f.name}</button>`).join("");
|
|
941
|
+
rail.querySelectorAll("button").forEach((btn) => {
|
|
942
|
+
btn.addEventListener("click", () => {
|
|
943
|
+
this.controller.setFloor(btn.dataset.floor);
|
|
944
|
+
this.showSectionCard(null);
|
|
945
|
+
this.syncFloors();
|
|
946
|
+
this.syncRung();
|
|
947
|
+
});
|
|
948
|
+
});
|
|
949
|
+
this.els.map.appendChild(rail);
|
|
950
|
+
this.floorsEl = rail;
|
|
951
|
+
this.syncFloors();
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
/** Reflect the engine's current LOD rung onto the pill group. */
|
|
955
|
+
syncRung() {
|
|
956
|
+
if (!this.rungsEl) return;
|
|
957
|
+
const active = this.controller.getRung();
|
|
958
|
+
this.rungsEl.querySelectorAll("button").forEach((btn) => {
|
|
959
|
+
const on = btn.dataset.rung === active;
|
|
960
|
+
btn.classList.toggle("on", on);
|
|
961
|
+
btn.setAttribute("aria-pressed", String(on));
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
/** Reflect the active floor onto the switcher rail. */
|
|
965
|
+
syncFloors() {
|
|
966
|
+
if (!this.floorsEl) return;
|
|
967
|
+
const active = this.controller.getActiveFloorId();
|
|
968
|
+
this.floorsEl.querySelectorAll("button").forEach((btn) => {
|
|
969
|
+
btn.classList.toggle("on", btn.dataset.floor === active);
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
/** Show (or clear, on null) the tapped-section summary card. */
|
|
973
|
+
showSectionCard(summary) {
|
|
974
|
+
if (!summary) {
|
|
975
|
+
this.secCardEl?.remove();
|
|
976
|
+
this.secCardEl = null;
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
if (!this.els.map) return;
|
|
980
|
+
this.secCardEl?.remove();
|
|
981
|
+
const priceLabel = summary.priceMin === summary.priceMax ? this.money(summary.priceMin) : `${this.money(summary.priceMin)}\u2013${this.money(summary.priceMax)}`;
|
|
982
|
+
const card = document.createElement("div");
|
|
983
|
+
card.className = "sl-seccard on";
|
|
984
|
+
card.setAttribute("role", "dialog");
|
|
985
|
+
card.setAttribute("aria-label", t2("picker.sectionSummaryAria", { label: summary.label }));
|
|
986
|
+
const mix = summary.categories.map(
|
|
987
|
+
(c) => `<span class="sl-seccard-mix-item"><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>`
|
|
988
|
+
).join("");
|
|
989
|
+
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>` : "") + `<button type="button" class="sl-seccard-x" aria-label="${t2("picker.closeSectionSummary")}">\u2715</button></div><div class="sl-seccard-zone">${summary.zoneLabel ? `${summary.zoneLabel} \xB7 ` : ""}<span class="sl-seccard-left">${tCount("picker.seatsLeftInSection", summary.seatsLeft)}</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>`;
|
|
990
|
+
card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
|
|
991
|
+
card.querySelector(".sl-seccard-overview").addEventListener("click", () => this.controller.overview());
|
|
992
|
+
this.els.map.appendChild(card);
|
|
993
|
+
this.secCardEl = card;
|
|
994
|
+
}
|
|
814
995
|
/** aria-live readout when keyboard focus lands on a seat. */
|
|
815
996
|
announceSeat(seat) {
|
|
816
997
|
if (!this.srEl) return;
|
|
@@ -832,11 +1013,12 @@ var SeatPicker = class _SeatPicker {
|
|
|
832
1013
|
const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
|
|
833
1014
|
const el = document.createElement("div");
|
|
834
1015
|
el.className = "sl-confirm";
|
|
835
|
-
el.innerHTML = `<div class="sl-confirm-label">${seat.label}</div><div class="sl-confirm-meta"><span class="sl-dot" style="background:${cat?.color ?? "#6e7bff"}"></span>${cat?.label ?? seat.categoryKey}${price != null ? `<b>${this.money(price)}</b>` : ""}</div><div class="sl-confirm-row"><button type="button" class="sl-confirm-cancel">Cancel</button><button type="button" class="sl-confirm-add">Add seat</button></div>`;
|
|
1016
|
+
el.innerHTML = `<div class="sl-confirm-label">${seat.label}</div><div class="sl-confirm-meta"><span class="sl-dot" style="background:${cat?.color ?? "#6e7bff"}"></span>${cat?.label ?? seat.categoryKey}${price != null ? `<b>${this.money(price)}</b>` : ""}</div>` + (this.seatViewEnabled() ? `<button type="button" class="sl-confirm-view"><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>${t2("picker.open360")}</button>` : "") + `<div class="sl-confirm-row"><button type="button" class="sl-confirm-cancel">Cancel</button><button type="button" class="sl-confirm-add">Add seat</button></div>`;
|
|
836
1017
|
this.els.map.appendChild(el);
|
|
837
1018
|
this.confirmEl = el;
|
|
838
1019
|
this.confirmSeat = seat;
|
|
839
1020
|
this.reanchorConfirm();
|
|
1021
|
+
el.querySelector(".sl-confirm-view")?.addEventListener("click", () => this.openSeatView(seat));
|
|
840
1022
|
el.querySelector(".sl-confirm-add").addEventListener("click", () => this.closeConfirm());
|
|
841
1023
|
el.querySelector(".sl-confirm-cancel").addEventListener("click", () => {
|
|
842
1024
|
this.controller.deselect([seat.id]);
|
|
@@ -854,6 +1036,122 @@ var SeatPicker = class _SeatPicker {
|
|
|
854
1036
|
this.confirmEl = null;
|
|
855
1037
|
this.confirmSeat = null;
|
|
856
1038
|
}
|
|
1039
|
+
// ---- 360° view-from-seat modal --------------------------------------------
|
|
1040
|
+
seatViewEnabled() {
|
|
1041
|
+
return this.opts.seatView !== false;
|
|
1042
|
+
}
|
|
1043
|
+
/** Every bookable seat (cached) — neighbor heads for the generated panorama. */
|
|
1044
|
+
allSeats() {
|
|
1045
|
+
if (!this.allSeatsCache) {
|
|
1046
|
+
const doc = this.controller.doc;
|
|
1047
|
+
this.allSeatsCache = doc ? expandChart(doc) : [];
|
|
1048
|
+
}
|
|
1049
|
+
return this.allSeatsCache;
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Open the drag-to-look-around 360° preview for a seat. Uses the organizer's
|
|
1053
|
+
* uploaded photo (seat.viewUrl) when present, else a panorama generated from
|
|
1054
|
+
* the chart geometry — the stage placed at this seat's true bearing + size.
|
|
1055
|
+
* Zero extra dependencies: an equirectangular image panned with `repeat-x`.
|
|
1056
|
+
*/
|
|
1057
|
+
openSeatView(seat) {
|
|
1058
|
+
if (!this.root || !this.seatViewEnabled()) return;
|
|
1059
|
+
this.closeSeatView();
|
|
1060
|
+
this.closeConfirm();
|
|
1061
|
+
const doc = this.controller.doc;
|
|
1062
|
+
const activeId = this.controller.getActiveFloorId();
|
|
1063
|
+
const focal = doc?.floors?.find((f) => f.id === activeId)?.focalPoint ?? doc?.focalPoint ?? { x: 0, y: 0 };
|
|
1064
|
+
let panoUrl;
|
|
1065
|
+
let caption;
|
|
1066
|
+
let real = false;
|
|
1067
|
+
if (seat.viewUrl) {
|
|
1068
|
+
panoUrl = seat.viewUrl;
|
|
1069
|
+
caption = t2("picker.panorama360");
|
|
1070
|
+
real = true;
|
|
1071
|
+
} else {
|
|
1072
|
+
const pano2 = generateSeatPanorama(seat, focal, this.allSeats());
|
|
1073
|
+
panoUrl = pano2.url;
|
|
1074
|
+
caption = t2("picker.illustrationCaption", { m: pano2.distanceM });
|
|
1075
|
+
}
|
|
1076
|
+
const el = document.createElement("div");
|
|
1077
|
+
el.className = "sl-view";
|
|
1078
|
+
el.setAttribute("role", "dialog");
|
|
1079
|
+
el.setAttribute("aria-label", t2("picker.viewFromSeat", { label: seat.label }));
|
|
1080
|
+
el.innerHTML = `<div class="sl-view-head"><span class="sl-view-title">${t2("picker.viewFromSeat", { label: seat.label })}</span><span class="sl-view-cap">${caption}</span><button type="button" class="sl-view-x" aria-label="Close"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div><div class="sl-view-pano"><span class="sl-view-badge">${real ? t2("picker.real360") : t2("picker.preview")}</span><span class="sl-view-hint">Drag to look around \xB7 scroll to zoom</span></div>`;
|
|
1081
|
+
this.root.appendChild(el);
|
|
1082
|
+
this.viewEl = el;
|
|
1083
|
+
const pano = el.querySelector(".sl-view-pano");
|
|
1084
|
+
pano.style.backgroundImage = `url("${panoUrl}")`;
|
|
1085
|
+
let zoom = 1.2;
|
|
1086
|
+
let posX = 0;
|
|
1087
|
+
let posY = 0;
|
|
1088
|
+
const apply = () => {
|
|
1089
|
+
const h = pano.clientHeight || 1;
|
|
1090
|
+
const bgH = h * zoom;
|
|
1091
|
+
const overV = Math.max(0, bgH - h);
|
|
1092
|
+
posY = Math.min(overV / 2, Math.max(-overV / 2, posY));
|
|
1093
|
+
pano.style.backgroundSize = `auto ${bgH}px`;
|
|
1094
|
+
pano.style.backgroundPosition = `${posX}px ${posY + overV / 2}px`;
|
|
1095
|
+
};
|
|
1096
|
+
apply();
|
|
1097
|
+
let dragging = false;
|
|
1098
|
+
let lastX = 0;
|
|
1099
|
+
let lastY = 0;
|
|
1100
|
+
const onDown = (e) => {
|
|
1101
|
+
dragging = true;
|
|
1102
|
+
lastX = e.clientX;
|
|
1103
|
+
lastY = e.clientY;
|
|
1104
|
+
pano.classList.add("drag");
|
|
1105
|
+
pano.setPointerCapture?.(e.pointerId);
|
|
1106
|
+
};
|
|
1107
|
+
const onMove = (e) => {
|
|
1108
|
+
if (!dragging) return;
|
|
1109
|
+
posX += e.clientX - lastX;
|
|
1110
|
+
posY += e.clientY - lastY;
|
|
1111
|
+
lastX = e.clientX;
|
|
1112
|
+
lastY = e.clientY;
|
|
1113
|
+
apply();
|
|
1114
|
+
};
|
|
1115
|
+
const onUp = (e) => {
|
|
1116
|
+
dragging = false;
|
|
1117
|
+
pano.classList.remove("drag");
|
|
1118
|
+
pano.releasePointerCapture?.(e.pointerId);
|
|
1119
|
+
};
|
|
1120
|
+
const onWheel = (e) => {
|
|
1121
|
+
e.preventDefault();
|
|
1122
|
+
zoom = Math.min(2.4, Math.max(1, zoom + (e.deltaY < 0 ? 0.12 : -0.12)));
|
|
1123
|
+
apply();
|
|
1124
|
+
};
|
|
1125
|
+
pano.addEventListener("pointerdown", onDown);
|
|
1126
|
+
pano.addEventListener("pointermove", onMove);
|
|
1127
|
+
pano.addEventListener("pointerup", onUp);
|
|
1128
|
+
pano.addEventListener("pointercancel", onUp);
|
|
1129
|
+
pano.addEventListener("wheel", onWheel, { passive: false });
|
|
1130
|
+
const closeBtn = el.querySelector(".sl-view-x");
|
|
1131
|
+
closeBtn.addEventListener("click", () => this.closeSeatView());
|
|
1132
|
+
const onKey = (e) => {
|
|
1133
|
+
if (e.key === "Escape") {
|
|
1134
|
+
e.stopPropagation();
|
|
1135
|
+
this.closeSeatView();
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
el.addEventListener("keydown", onKey);
|
|
1139
|
+
closeBtn.focus();
|
|
1140
|
+
this.viewCleanup = () => {
|
|
1141
|
+
pano.removeEventListener("pointerdown", onDown);
|
|
1142
|
+
pano.removeEventListener("pointermove", onMove);
|
|
1143
|
+
pano.removeEventListener("pointerup", onUp);
|
|
1144
|
+
pano.removeEventListener("pointercancel", onUp);
|
|
1145
|
+
pano.removeEventListener("wheel", onWheel);
|
|
1146
|
+
el.removeEventListener("keydown", onKey);
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
closeSeatView() {
|
|
1150
|
+
this.viewCleanup?.();
|
|
1151
|
+
this.viewCleanup = null;
|
|
1152
|
+
this.viewEl?.remove();
|
|
1153
|
+
this.viewEl = null;
|
|
1154
|
+
}
|
|
857
1155
|
// ---- chrome sync ----------------------------------------------------------
|
|
858
1156
|
money(n) {
|
|
859
1157
|
try {
|
|
@@ -896,15 +1194,20 @@ var SeatPicker = class _SeatPicker {
|
|
|
896
1194
|
}
|
|
897
1195
|
for (const item of heldItems) {
|
|
898
1196
|
const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
|
|
1197
|
+
const tierName = item.tierId ? cat?.tiers?.find((ti) => ti.id === item.tierId)?.name : void 0;
|
|
1198
|
+
const canView2 = this.seatViewEnabled() && item.objectType !== "ga" && !!this.controller.seatByLabel(item.label);
|
|
899
1199
|
parts.push(
|
|
900
|
-
`<div class="sl-chip"><b>${item.label}</b><span class="cat">${cat?.label ?? item.categoryKey}</span><span class="amt">${this.money(item.unitPrice * (item.quantity ?? 1))}</span></div>`
|
|
1200
|
+
`<div class="sl-chip"><b>${item.label}</b><span class="cat">${cat?.label ?? item.categoryKey}${tierName ? ` \xB7 ${tierName}` : ""}</span><span class="amt">${this.money(item.unitPrice * (item.quantity ?? 1))}</span>` + (canView2 ? `<button type="button" class="view" data-view-label="${item.label}" aria-label="${t2("picker.viewFromSeat", { label: item.label })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "") + `</div>`
|
|
901
1201
|
);
|
|
902
1202
|
}
|
|
903
1203
|
const heldLabels = new Set(heldItems.map((item) => item.label));
|
|
1204
|
+
const canView = this.seatViewEnabled();
|
|
904
1205
|
for (const s of seats.filter((seat) => !heldLabels.has(seat.label))) {
|
|
905
1206
|
const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
|
|
1207
|
+
const tierSelect = s.tiers && s.tiers.length ? `<select class="tier" data-tier="${s.id}" aria-label="${t2("picker.ticketTierFor", { label: s.label })}">` + s.tiers.map((ti) => `<option value="${ti.id}"${ti.id === s.tierId ? " selected" : ""}>${ti.name} \xB7 ${this.money(ti.price)}</option>`).join("") + `</select>` : "";
|
|
1208
|
+
const viewBtn = canView ? `<button type="button" class="view" data-view-label="${s.label}" aria-label="${t2("picker.viewFromSeat", { label: s.label })}"><svg viewBox="0 0 24 24"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>` : "";
|
|
906
1209
|
parts.push(
|
|
907
|
-
`<div class="sl-chip" data-seat="${s.id}"><b>${s.label}</b><span class="cat">${cat?.label ?? s.categoryKey}</span
|
|
1210
|
+
`<div class="sl-chip" data-seat="${s.id}"><b>${s.label}</b><span class="cat">${cat?.label ?? s.categoryKey}</span>` + tierSelect + `<span class="amt">${this.money(s.price)}</span>` + viewBtn + `<button type="button" class="rm" aria-label="Remove ${s.label}"><svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div>`
|
|
908
1211
|
);
|
|
909
1212
|
}
|
|
910
1213
|
for (const area of gaAreas) {
|
|
@@ -938,6 +1241,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
938
1241
|
this.controller.deselect([id]);
|
|
939
1242
|
});
|
|
940
1243
|
});
|
|
1244
|
+
this.els.tray.querySelectorAll(".sl-chip .tier").forEach((sel) => {
|
|
1245
|
+
sel.addEventListener("change", () => this.controller.setSeatTier(sel.dataset.tier, sel.value || null));
|
|
1246
|
+
});
|
|
1247
|
+
this.els.tray.querySelectorAll(".sl-chip .view[data-view-label]").forEach((btn) => {
|
|
1248
|
+
btn.addEventListener("click", () => {
|
|
1249
|
+
const seat = this.controller.seatByLabel(btn.dataset.viewLabel);
|
|
1250
|
+
if (seat) this.openSeatView(seat);
|
|
1251
|
+
});
|
|
1252
|
+
});
|
|
941
1253
|
this.els.tray.querySelectorAll(".sl-ga button").forEach((btn) => {
|
|
942
1254
|
btn.addEventListener("click", () => {
|
|
943
1255
|
const areaEl = btn.closest(".sl-ga");
|
|
@@ -965,7 +1277,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
965
1277
|
async handleCta() {
|
|
966
1278
|
const cta = this.els.cta;
|
|
967
1279
|
if (this.hold && !this.controller.getSelection().some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
|
|
968
|
-
this.opts.onCheckout?.(this.hold, this.controller.getSelection());
|
|
1280
|
+
this.opts.onCheckout?.(this.hold, this.hold.seats ?? this.controller.getSelection());
|
|
969
1281
|
return;
|
|
970
1282
|
}
|
|
971
1283
|
cta.disabled = true;
|
|
@@ -989,7 +1301,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
989
1301
|
}
|
|
990
1302
|
this.hold = hold;
|
|
991
1303
|
this.startHoldTimer(hold.expiresAt);
|
|
992
|
-
this.opts.onCheckout?.(hold, chosenSeats);
|
|
1304
|
+
this.opts.onCheckout?.(hold, chosenSeats.length ? chosenSeats : hold.seats ?? []);
|
|
993
1305
|
} catch (err) {
|
|
994
1306
|
this.opts.onError?.(err);
|
|
995
1307
|
this.toast("One or more seats were just taken. Please pick again.");
|
|
@@ -1076,6 +1388,7 @@ var SeatPicker = class _SeatPicker {
|
|
|
1076
1388
|
destroy() {
|
|
1077
1389
|
this.destroyed = true;
|
|
1078
1390
|
this.closeConfirm();
|
|
1391
|
+
this.closeSeatView();
|
|
1079
1392
|
this.stopHoldTimer();
|
|
1080
1393
|
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
1081
1394
|
this.ro?.disconnect();
|