@seatlayer/js 0.6.0 → 0.7.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 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);
@@ -715,6 +762,14 @@ var SeatPicker = class _SeatPicker {
715
762
  return this;
716
763
  }
717
764
  this.els.boot.remove();
765
+ if (info.mode === "test") {
766
+ const ribbon = document.createElement("div");
767
+ ribbon.textContent = (0, import_core2.t)("picker.testMode");
768
+ ribbon.setAttribute("aria-label", (0, import_core2.t)("picker.testMode"));
769
+ ribbon.style.cssText = "position:absolute;top:18px;right:-34px;z-index:6;transform:rotate(45deg);width:140px;text-align:center;padding:4px 0;background:#f4b740;color:#1a1200;font:800 10.5px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;letter-spacing:.12em;box-shadow:0 2px 8px rgba(0,0,0,.25);pointer-events:none;";
770
+ this.els.map.style.overflow = "hidden";
771
+ this.els.map.appendChild(ribbon);
772
+ }
718
773
  const chartTheme = this.controller.doc?.theme;
719
774
  Object.entries(resolveTokens(chartTheme, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
720
775
  this.currency = info.currency ?? this.opts.currency ?? "USD";
@@ -724,10 +779,92 @@ var SeatPicker = class _SeatPicker {
724
779
  this.els.name.textContent = info.eventName ?? "";
725
780
  const when = info.startsAt ? new Date(info.startsAt).toLocaleString(this.opts.locale, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "";
726
781
  this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
782
+ const present = /* @__PURE__ */ new Set();
783
+ if (this.controller.doc) {
784
+ for (const seat of (0, import_core2.expandChart)(this.controller.doc)) {
785
+ for (const type of seat.accessibility ?? []) present.add(type);
786
+ if (seat.accessible && !seat.accessibility?.length) present.add("wheelchair");
787
+ }
788
+ }
789
+ if (present.size) {
790
+ const chips = document.createElement("div");
791
+ chips.className = "sl-chips";
792
+ const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
793
+ const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-f="${key}">${label}</button>`;
794
+ chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
795
+ this.els.map.appendChild(chips);
796
+ chips.querySelectorAll("button").forEach((btn) => {
797
+ btn.addEventListener("click", () => {
798
+ const f = btn.dataset.f;
799
+ this.a11yFilter = f;
800
+ chips.querySelectorAll("button").forEach((b) => b.classList.toggle("on", b === btn));
801
+ this.controller.setAccessibilityFilter(f === "all" ? null : [f]);
802
+ });
803
+ });
804
+ }
805
+ const cb = document.createElement("button");
806
+ cb.type = "button";
807
+ cb.setAttribute("aria-label", "Toggle colorblind-friendly colors");
808
+ cb.setAttribute("aria-pressed", String(!!this.opts.colorblindSafe));
809
+ 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>';
810
+ this.els.zfit.parentElement.appendChild(cb);
811
+ let cbOn = !!this.opts.colorblindSafe;
812
+ cb.addEventListener("click", () => {
813
+ cbOn = !cbOn;
814
+ cb.setAttribute("aria-pressed", String(cbOn));
815
+ this.controller.setColorblindSafe(cbOn);
816
+ });
817
+ this.srEl = document.createElement("div");
818
+ this.srEl.className = "sl-sr";
819
+ this.srEl.setAttribute("aria-live", "polite");
820
+ root.appendChild(this.srEl);
727
821
  this.syncPrices();
728
822
  this.syncTray();
729
823
  return this;
730
824
  }
825
+ /** aria-live readout when keyboard focus lands on a seat. */
826
+ announceSeat(seat) {
827
+ if (!this.srEl) return;
828
+ if (!seat) {
829
+ this.srEl.textContent = "";
830
+ return;
831
+ }
832
+ const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
833
+ const status = this.controller.getStatus(seat.id) ?? "free";
834
+ const statusText = status === "free" ? "available" : status === "held" ? "on hold" : "taken";
835
+ const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
836
+ this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
837
+ }
838
+ // ---- confirm popover (opt-in confirmSelection mode) ------------------------
839
+ showConfirm(seat) {
840
+ this.closeConfirm();
841
+ if (this.tipEl) this.tipEl.style.display = "none";
842
+ const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
843
+ const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
844
+ const el = document.createElement("div");
845
+ el.className = "sl-confirm";
846
+ 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>`;
847
+ this.els.map.appendChild(el);
848
+ this.confirmEl = el;
849
+ this.confirmSeat = seat;
850
+ this.reanchorConfirm();
851
+ el.querySelector(".sl-confirm-add").addEventListener("click", () => this.closeConfirm());
852
+ el.querySelector(".sl-confirm-cancel").addEventListener("click", () => {
853
+ this.controller.deselect([seat.id]);
854
+ this.closeConfirm();
855
+ });
856
+ }
857
+ reanchorConfirm() {
858
+ if (!this.confirmEl || !this.confirmSeat) return;
859
+ const p = this.controller.worldToScreen({ x: this.confirmSeat.x, y: this.confirmSeat.y });
860
+ this.confirmEl.style.left = `${p.x}px`;
861
+ this.confirmEl.style.top = `${p.y}px`;
862
+ }
863
+ closeConfirm() {
864
+ this.confirmEl?.remove();
865
+ this.confirmEl = null;
866
+ this.confirmSeat = null;
867
+ }
731
868
  // ---- chrome sync ----------------------------------------------------------
732
869
  money(n) {
733
870
  try {
@@ -742,8 +879,12 @@ var SeatPicker = class _SeatPicker {
742
879
  const left = this.controller.categoryAvailability();
743
880
  this.els.prices.innerHTML = doc.categories.map((c) => {
744
881
  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>`;
882
+ 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
883
  }).join("");
884
+ this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
885
+ row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
886
+ row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
887
+ });
747
888
  }
748
889
  /** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
749
890
  evictTakenSelections() {
@@ -757,13 +898,21 @@ var SeatPicker = class _SeatPicker {
757
898
  if (!this.els.tray) return;
758
899
  const seats = this.controller.getSelection();
759
900
  const gaAreas = this.controller.getGAAreas();
901
+ const heldItems = this.hold?.items ?? [];
760
902
  const parts = [];
761
- if (!seats.length && !gaAreas.length) {
903
+ if (!seats.length && !heldItems.length && !gaAreas.length) {
762
904
  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) {
905
+ } else if (!seats.length && !heldItems.length) {
764
906
  parts.push(`<div class="sl-tray-hint">Tap a seat on the map \u2014 or grab standing tickets below.</div>`);
765
907
  }
766
- for (const s of seats) {
908
+ for (const item of heldItems) {
909
+ const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
910
+ parts.push(
911
+ `<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>`
912
+ );
913
+ }
914
+ const heldLabels = new Set(heldItems.map((item) => item.label));
915
+ for (const s of seats.filter((seat) => !heldLabels.has(seat.label))) {
767
916
  const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
768
917
  parts.push(
769
918
  `<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 +924,25 @@ var SeatPicker = class _SeatPicker {
775
924
  `<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
925
  );
777
926
  }
927
+ if (!this.hold) {
928
+ const cats = this.controller.doc?.categories ?? [];
929
+ parts.push(
930
+ `<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>`
931
+ );
932
+ }
778
933
  this.els.tray.innerHTML = parts.join("");
934
+ this.els.tray.querySelectorAll("[data-ba]").forEach((btn) => {
935
+ btn.addEventListener("click", () => {
936
+ this.baQty = Math.max(1, Math.min(8, this.baQty + Number(btn.dataset.ba)));
937
+ this.syncTray();
938
+ });
939
+ });
940
+ this.els.tray.querySelector("[data-ba-cat]")?.addEventListener("change", (e) => {
941
+ this.baCat = e.target.value;
942
+ });
943
+ this.els.tray.querySelector(".sl-ba-go")?.addEventListener("click", () => {
944
+ void this.bestAvailable(this.baQty, this.baCat || void 0);
945
+ });
779
946
  this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
780
947
  btn.addEventListener("click", () => {
781
948
  const id = btn.closest(".sl-chip").dataset.seat;
@@ -794,17 +961,24 @@ var SeatPicker = class _SeatPicker {
794
961
  });
795
962
  const gaTotal = gaAreas.reduce((sum, a) => sum + a.price * (this.gaQty.get(a.id) ?? 0), 0);
796
963
  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;
964
+ const heldTotal = heldItems.reduce((sum, item) => sum + item.unitPrice * (item.quantity ?? 1), 0);
965
+ const heldCount = heldItems.reduce((sum, item) => sum + (item.quantity ?? 1), 0);
966
+ const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
967
+ const total = freshSeats.reduce((sum, s) => sum + s.price, 0) + gaTotal + heldTotal;
968
+ const count = freshSeats.length + gaCount + heldCount;
799
969
  this.els.count.textContent = count ? `${count} ${count === 1 ? "ticket" : "tickets"}` : "No seats selected";
800
970
  this.els.total.textContent = count ? this.money(total) : "";
801
971
  const cta = this.els.cta;
802
972
  cta.disabled = count === 0;
803
- cta.textContent = count ? "Hold seats & checkout" : "Select seats";
973
+ cta.textContent = this.hold ? "Continue to checkout" : count ? "Hold seats & checkout" : "Select seats";
804
974
  this.opts.onSelectionChange?.(seats);
805
975
  }
806
976
  async handleCta() {
807
977
  const cta = this.els.cta;
978
+ if (this.hold && !this.controller.getSelection().some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
979
+ this.opts.onCheckout?.(this.hold, this.controller.getSelection());
980
+ return;
981
+ }
808
982
  cta.disabled = true;
809
983
  cta.textContent = "Holding\u2026";
810
984
  try {
@@ -912,6 +1086,7 @@ var SeatPicker = class _SeatPicker {
912
1086
  }
913
1087
  destroy() {
914
1088
  this.destroyed = true;
1089
+ this.closeConfirm();
915
1090
  this.stopHoldTimer();
916
1091
  if (this.toastTimer) clearTimeout(this.toastTimer);
917
1092
  this.ro?.disconnect();