@seatlayer/js 0.6.0 → 0.7.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 CHANGED
@@ -454,6 +454,8 @@ var CSS = `
454
454
  .sl-picker[data-layout="narrow"] .sl-body{flex-direction:column}
455
455
  .sl-picker[data-layout="narrow"] .sl-map{min-height:0;flex:1}
456
456
  .sl-picker[data-layout="narrow"] .sl-side{width:100%;max-height:46%;border-left:0;border-top:1px solid var(--sl-line)}
457
+ .sl-picker[data-layout="narrow"] .sl-tray{flex:none}
458
+ .sl-picker[data-layout="narrow"] .sl-foot{position:sticky;bottom:0;background:var(--sl-bg)}
457
459
 
458
460
  /* price panel */
459
461
  .sl-sec{padding:14px 16px 4px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--sl-muted);font-weight:700}
@@ -518,6 +520,39 @@ var CSS = `
518
520
  .sl-boot-retry{margin-top:4px;padding:9px 20px;border-radius:var(--sl-r-sm);background:var(--sl-accent);
519
521
  color:var(--sl-accent-ink);font-weight:700;font-size:13px}
520
522
 
523
+ /* a11y filter chips (over the map, top-left) */
524
+ .sl-chips{position:absolute;top:12px;left:12px;z-index:5;display:flex;gap:6px;flex-wrap:wrap;max-width:70%}
525
+ .sl-chip-f{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:999px;font-size:12px;font-weight:700;
526
+ background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-muted);transition:color .15s,border-color .15s}
527
+ .sl-chip-f:hover{color:var(--sl-text)}
528
+ .sl-chip-f.on{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
529
+
530
+ /* confirm popover */
531
+ .sl-confirm{position:absolute;z-index:9;min-width:190px;background:var(--sl-surface);border:1px solid var(--sl-line);
532
+ border-radius:12px;padding:12px;box-shadow:0 18px 50px -18px rgba(0,0,0,.7);transform:translate(-50%,calc(-100% - 14px))}
533
+ .sl-confirm-label{font-weight:800;font-size:14px}
534
+ .sl-confirm-meta{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--sl-muted);margin-top:4px}
535
+ .sl-confirm-meta b{color:var(--sl-text);margin-left:auto}
536
+ .sl-confirm-row{display:flex;gap:8px;margin-top:10px}
537
+ .sl-confirm-row button{flex:1;padding:8px;border-radius:8px;font-weight:700;font-size:12.5px}
538
+ .sl-confirm-add{background:var(--sl-accent);color:var(--sl-accent-ink)}
539
+ .sl-confirm-cancel{border:1px solid var(--sl-line);color:var(--sl-muted)}
540
+ .sl-confirm-cancel:hover{color:var(--sl-text)}
541
+
542
+ /* best-available row */
543
+ .sl-ba{display:flex;align-items:center;gap:8px;padding:9px 11px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm)}
544
+ .sl-ba select{background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:7px;
545
+ font:inherit;font-size:12px;padding:5px 6px;max-width:110px}
546
+ .sl-ba-qty{display:flex;align-items:center;gap:7px}
547
+ .sl-ba-qty button{width:24px;height:24px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
548
+ font-size:14px;font-weight:700;display:flex;align-items:center;justify-content:center}
549
+ .sl-ba-qty span{min-width:14px;text-align:center;font-weight:800}
550
+ .sl-ba-go{margin-left:auto;padding:7px 12px;border-radius:999px;border:1px solid var(--sl-line);font-weight:700;font-size:12px;transition:border-color .15s}
551
+ .sl-ba-go:hover{border-color:var(--sl-muted)}
552
+
553
+ /* screen-reader live region */
554
+ .sl-sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
555
+
521
556
  /* modal host */
522
557
  .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}
523
558
  .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)}
@@ -562,6 +597,12 @@ var SeatPicker = class _SeatPicker {
562
597
  this.gaQty = /* @__PURE__ */ new Map();
563
598
  this.tipEl = null;
564
599
  this.tipPos = { x: 0, y: 0 };
600
+ this.confirmEl = null;
601
+ this.confirmSeat = null;
602
+ this.srEl = null;
603
+ this.a11yFilter = "all";
604
+ this.baQty = 2;
605
+ this.baCat = "";
565
606
  // modal plumbing (set by open())
566
607
  this.modalScrim = null;
567
608
  this.prevFocus = null;
@@ -591,6 +632,12 @@ var SeatPicker = class _SeatPicker {
591
632
  this.syncTray();
592
633
  this.opts.onHoldExpired?.();
593
634
  },
635
+ confirmSelection: options.confirmSelection,
636
+ onSelect: (seat) => {
637
+ if (this.opts.confirmSelection) this.showConfirm(seat);
638
+ },
639
+ onViewChange: () => this.reanchorConfirm(),
640
+ onFocusSeat: (seat) => this.announceSeat(seat),
594
641
  onSeatHover: (d) => this.updateTooltip(d),
595
642
  onHint: (m) => {
596
643
  if (m) this.toast(m);
@@ -724,10 +771,92 @@ var SeatPicker = class _SeatPicker {
724
771
  this.els.name.textContent = info.eventName ?? "";
725
772
  const when = info.startsAt ? new Date(info.startsAt).toLocaleString(this.opts.locale, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "";
726
773
  this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
774
+ const present = /* @__PURE__ */ new Set();
775
+ if (this.controller.doc) {
776
+ for (const seat of (0, import_core2.expandChart)(this.controller.doc)) {
777
+ for (const type of seat.accessibility ?? []) present.add(type);
778
+ if (seat.accessible && !seat.accessibility?.length) present.add("wheelchair");
779
+ }
780
+ }
781
+ if (present.size) {
782
+ const chips = document.createElement("div");
783
+ chips.className = "sl-chips";
784
+ const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
785
+ const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-f="${key}">${label}</button>`;
786
+ chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
787
+ this.els.map.appendChild(chips);
788
+ chips.querySelectorAll("button").forEach((btn) => {
789
+ btn.addEventListener("click", () => {
790
+ const f = btn.dataset.f;
791
+ this.a11yFilter = f;
792
+ chips.querySelectorAll("button").forEach((b) => b.classList.toggle("on", b === btn));
793
+ this.controller.setAccessibilityFilter(f === "all" ? null : [f]);
794
+ });
795
+ });
796
+ }
797
+ const cb = document.createElement("button");
798
+ cb.type = "button";
799
+ cb.setAttribute("aria-label", "Toggle colorblind-friendly colors");
800
+ cb.setAttribute("aria-pressed", String(!!this.opts.colorblindSafe));
801
+ 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>';
802
+ this.els.zfit.parentElement.appendChild(cb);
803
+ let cbOn = !!this.opts.colorblindSafe;
804
+ cb.addEventListener("click", () => {
805
+ cbOn = !cbOn;
806
+ cb.setAttribute("aria-pressed", String(cbOn));
807
+ this.controller.setColorblindSafe(cbOn);
808
+ });
809
+ this.srEl = document.createElement("div");
810
+ this.srEl.className = "sl-sr";
811
+ this.srEl.setAttribute("aria-live", "polite");
812
+ root.appendChild(this.srEl);
727
813
  this.syncPrices();
728
814
  this.syncTray();
729
815
  return this;
730
816
  }
817
+ /** aria-live readout when keyboard focus lands on a seat. */
818
+ announceSeat(seat) {
819
+ if (!this.srEl) return;
820
+ if (!seat) {
821
+ this.srEl.textContent = "";
822
+ return;
823
+ }
824
+ const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
825
+ const status = this.controller.getStatus(seat.id) ?? "free";
826
+ const statusText = status === "free" ? "available" : status === "held" ? "on hold" : "taken";
827
+ const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
828
+ this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
829
+ }
830
+ // ---- confirm popover (opt-in confirmSelection mode) ------------------------
831
+ showConfirm(seat) {
832
+ this.closeConfirm();
833
+ if (this.tipEl) this.tipEl.style.display = "none";
834
+ const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
835
+ const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
836
+ const el = document.createElement("div");
837
+ el.className = "sl-confirm";
838
+ 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>`;
839
+ this.els.map.appendChild(el);
840
+ this.confirmEl = el;
841
+ this.confirmSeat = seat;
842
+ this.reanchorConfirm();
843
+ el.querySelector(".sl-confirm-add").addEventListener("click", () => this.closeConfirm());
844
+ el.querySelector(".sl-confirm-cancel").addEventListener("click", () => {
845
+ this.controller.deselect([seat.id]);
846
+ this.closeConfirm();
847
+ });
848
+ }
849
+ reanchorConfirm() {
850
+ if (!this.confirmEl || !this.confirmSeat) return;
851
+ const p = this.controller.worldToScreen({ x: this.confirmSeat.x, y: this.confirmSeat.y });
852
+ this.confirmEl.style.left = `${p.x}px`;
853
+ this.confirmEl.style.top = `${p.y}px`;
854
+ }
855
+ closeConfirm() {
856
+ this.confirmEl?.remove();
857
+ this.confirmEl = null;
858
+ this.confirmSeat = null;
859
+ }
731
860
  // ---- chrome sync ----------------------------------------------------------
732
861
  money(n) {
733
862
  try {
@@ -742,8 +871,12 @@ var SeatPicker = class _SeatPicker {
742
871
  const left = this.controller.categoryAvailability();
743
872
  this.els.prices.innerHTML = doc.categories.map((c) => {
744
873
  const price = c.tiers?.length ? c.tiers[0].price : c.price;
745
- return `<div class="sl-price-row"><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>`;
874
+ return `<div class="sl-price-row" 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>`;
746
875
  }).join("");
876
+ this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
877
+ row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
878
+ row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
879
+ });
747
880
  }
748
881
  /** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
749
882
  evictTakenSelections() {
@@ -757,13 +890,21 @@ var SeatPicker = class _SeatPicker {
757
890
  if (!this.els.tray) return;
758
891
  const seats = this.controller.getSelection();
759
892
  const gaAreas = this.controller.getGAAreas();
893
+ const heldItems = this.hold?.items ?? [];
760
894
  const parts = [];
761
- if (!seats.length && !gaAreas.length) {
895
+ if (!seats.length && !heldItems.length && !gaAreas.length) {
762
896
  parts.push(`<div class="sl-tray-hint">Tap a seat on the map, or let us pick the best available for you.</div>`);
763
- } else if (!seats.length) {
897
+ } else if (!seats.length && !heldItems.length) {
764
898
  parts.push(`<div class="sl-tray-hint">Tap a seat on the map \u2014 or grab standing tickets below.</div>`);
765
899
  }
766
- for (const s of seats) {
900
+ for (const item of heldItems) {
901
+ const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
902
+ parts.push(
903
+ `<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>`
904
+ );
905
+ }
906
+ const heldLabels = new Set(heldItems.map((item) => item.label));
907
+ for (const s of seats.filter((seat) => !heldLabels.has(seat.label))) {
767
908
  const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
768
909
  parts.push(
769
910
  `<div class="sl-chip" data-seat="${s.id}"><b>${s.label}</b><span class="cat">${cat?.label ?? s.categoryKey}</span><span class="amt">${this.money(s.price)}</span><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>`
@@ -775,7 +916,25 @@ var SeatPicker = class _SeatPicker {
775
916
  `<div class="sl-ga" data-ga="${area.id}"><div class="sl-ga-info"><div class="sl-ga-name">${area.label}</div><div class="sl-ga-sub">${this.money(area.price)} \xB7 ${area.available} left</div></div><div class="sl-ga-qty"><button type="button" data-d="-1" aria-label="Fewer">\u2212</button><span>${qty}</span><button type="button" data-d="1" aria-label="More">+</button></div></div>`
776
917
  );
777
918
  }
919
+ if (!this.hold) {
920
+ const cats = this.controller.doc?.categories ?? [];
921
+ parts.push(
922
+ `<div class="sl-ba">` + (cats.length > 1 ? `<select aria-label="Category" data-ba-cat><option value="">Any tier</option>` + cats.map((c) => `<option value="${c.key}"${this.baCat === c.key ? " selected" : ""}>${c.label}</option>`).join("") + `</select>` : "") + `<div class="sl-ba-qty"><button type="button" data-ba="-1" aria-label="Fewer seats">\u2212</button><span>${this.baQty}</span><button type="button" data-ba="1" aria-label="More seats">+</button></div><button type="button" class="sl-ba-go">Best available</button></div>`
923
+ );
924
+ }
778
925
  this.els.tray.innerHTML = parts.join("");
926
+ this.els.tray.querySelectorAll("[data-ba]").forEach((btn) => {
927
+ btn.addEventListener("click", () => {
928
+ this.baQty = Math.max(1, Math.min(8, this.baQty + Number(btn.dataset.ba)));
929
+ this.syncTray();
930
+ });
931
+ });
932
+ this.els.tray.querySelector("[data-ba-cat]")?.addEventListener("change", (e) => {
933
+ this.baCat = e.target.value;
934
+ });
935
+ this.els.tray.querySelector(".sl-ba-go")?.addEventListener("click", () => {
936
+ void this.bestAvailable(this.baQty, this.baCat || void 0);
937
+ });
779
938
  this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
780
939
  btn.addEventListener("click", () => {
781
940
  const id = btn.closest(".sl-chip").dataset.seat;
@@ -794,17 +953,24 @@ var SeatPicker = class _SeatPicker {
794
953
  });
795
954
  const gaTotal = gaAreas.reduce((sum, a) => sum + a.price * (this.gaQty.get(a.id) ?? 0), 0);
796
955
  const gaCount = [...this.gaQty.values()].reduce((a, b) => a + b, 0);
797
- const total = seats.reduce((sum, s) => sum + s.price, 0) + gaTotal;
798
- const count = seats.length + gaCount;
956
+ const heldTotal = heldItems.reduce((sum, item) => sum + item.unitPrice * (item.quantity ?? 1), 0);
957
+ const heldCount = heldItems.reduce((sum, item) => sum + (item.quantity ?? 1), 0);
958
+ const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
959
+ const total = freshSeats.reduce((sum, s) => sum + s.price, 0) + gaTotal + heldTotal;
960
+ const count = freshSeats.length + gaCount + heldCount;
799
961
  this.els.count.textContent = count ? `${count} ${count === 1 ? "ticket" : "tickets"}` : "No seats selected";
800
962
  this.els.total.textContent = count ? this.money(total) : "";
801
963
  const cta = this.els.cta;
802
964
  cta.disabled = count === 0;
803
- cta.textContent = count ? "Hold seats & checkout" : "Select seats";
965
+ cta.textContent = this.hold ? "Continue to checkout" : count ? "Hold seats & checkout" : "Select seats";
804
966
  this.opts.onSelectionChange?.(seats);
805
967
  }
806
968
  async handleCta() {
807
969
  const cta = this.els.cta;
970
+ if (this.hold && !this.controller.getSelection().some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
971
+ this.opts.onCheckout?.(this.hold, this.controller.getSelection());
972
+ return;
973
+ }
808
974
  cta.disabled = true;
809
975
  cta.textContent = "Holding\u2026";
810
976
  try {
@@ -912,6 +1078,7 @@ var SeatPicker = class _SeatPicker {
912
1078
  }
913
1079
  destroy() {
914
1080
  this.destroyed = true;
1081
+ this.closeConfirm();
915
1082
  this.stopHoldTimer();
916
1083
  if (this.toastTimer) clearTimeout(this.toastTimer);
917
1084
  this.ro?.disconnect();