@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.js CHANGED
@@ -377,6 +377,7 @@ var EmbeddedDesigner = class {
377
377
  // src/SeatPicker.ts
378
378
  import {
379
379
  PickerController as PickerController2,
380
+ expandChart,
380
381
  loadLocale as loadLocale2,
381
382
  setStringOverrides as setStringOverrides2,
382
383
  t as t2
@@ -430,6 +431,8 @@ var CSS = `
430
431
  .sl-picker[data-layout="narrow"] .sl-body{flex-direction:column}
431
432
  .sl-picker[data-layout="narrow"] .sl-map{min-height:0;flex:1}
432
433
  .sl-picker[data-layout="narrow"] .sl-side{width:100%;max-height:46%;border-left:0;border-top:1px solid var(--sl-line)}
434
+ .sl-picker[data-layout="narrow"] .sl-tray{flex:none}
435
+ .sl-picker[data-layout="narrow"] .sl-foot{position:sticky;bottom:0;background:var(--sl-bg)}
433
436
 
434
437
  /* price panel */
435
438
  .sl-sec{padding:14px 16px 4px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--sl-muted);font-weight:700}
@@ -494,6 +497,39 @@ var CSS = `
494
497
  .sl-boot-retry{margin-top:4px;padding:9px 20px;border-radius:var(--sl-r-sm);background:var(--sl-accent);
495
498
  color:var(--sl-accent-ink);font-weight:700;font-size:13px}
496
499
 
500
+ /* a11y filter chips (over the map, top-left) */
501
+ .sl-chips{position:absolute;top:12px;left:12px;z-index:5;display:flex;gap:6px;flex-wrap:wrap;max-width:70%}
502
+ .sl-chip-f{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:999px;font-size:12px;font-weight:700;
503
+ background:var(--sl-surface);border:1px solid var(--sl-line);color:var(--sl-muted);transition:color .15s,border-color .15s}
504
+ .sl-chip-f:hover{color:var(--sl-text)}
505
+ .sl-chip-f.on{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
506
+
507
+ /* confirm popover */
508
+ .sl-confirm{position:absolute;z-index:9;min-width:190px;background:var(--sl-surface);border:1px solid var(--sl-line);
509
+ border-radius:12px;padding:12px;box-shadow:0 18px 50px -18px rgba(0,0,0,.7);transform:translate(-50%,calc(-100% - 14px))}
510
+ .sl-confirm-label{font-weight:800;font-size:14px}
511
+ .sl-confirm-meta{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--sl-muted);margin-top:4px}
512
+ .sl-confirm-meta b{color:var(--sl-text);margin-left:auto}
513
+ .sl-confirm-row{display:flex;gap:8px;margin-top:10px}
514
+ .sl-confirm-row button{flex:1;padding:8px;border-radius:8px;font-weight:700;font-size:12.5px}
515
+ .sl-confirm-add{background:var(--sl-accent);color:var(--sl-accent-ink)}
516
+ .sl-confirm-cancel{border:1px solid var(--sl-line);color:var(--sl-muted)}
517
+ .sl-confirm-cancel:hover{color:var(--sl-text)}
518
+
519
+ /* best-available row */
520
+ .sl-ba{display:flex;align-items:center;gap:8px;padding:9px 11px;border:1px solid var(--sl-line);border-radius:var(--sl-r-sm)}
521
+ .sl-ba select{background:var(--sl-surface);color:var(--sl-text);border:1px solid var(--sl-line);border-radius:7px;
522
+ font:inherit;font-size:12px;padding:5px 6px;max-width:110px}
523
+ .sl-ba-qty{display:flex;align-items:center;gap:7px}
524
+ .sl-ba-qty button{width:24px;height:24px;border-radius:999px;background:var(--sl-surface);border:1px solid var(--sl-line);
525
+ font-size:14px;font-weight:700;display:flex;align-items:center;justify-content:center}
526
+ .sl-ba-qty span{min-width:14px;text-align:center;font-weight:800}
527
+ .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}
528
+ .sl-ba-go:hover{border-color:var(--sl-muted)}
529
+
530
+ /* screen-reader live region */
531
+ .sl-sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
532
+
497
533
  /* modal host */
498
534
  .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}
499
535
  .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)}
@@ -538,6 +574,12 @@ var SeatPicker = class _SeatPicker {
538
574
  this.gaQty = /* @__PURE__ */ new Map();
539
575
  this.tipEl = null;
540
576
  this.tipPos = { x: 0, y: 0 };
577
+ this.confirmEl = null;
578
+ this.confirmSeat = null;
579
+ this.srEl = null;
580
+ this.a11yFilter = "all";
581
+ this.baQty = 2;
582
+ this.baCat = "";
541
583
  // modal plumbing (set by open())
542
584
  this.modalScrim = null;
543
585
  this.prevFocus = null;
@@ -567,6 +609,12 @@ var SeatPicker = class _SeatPicker {
567
609
  this.syncTray();
568
610
  this.opts.onHoldExpired?.();
569
611
  },
612
+ confirmSelection: options.confirmSelection,
613
+ onSelect: (seat) => {
614
+ if (this.opts.confirmSelection) this.showConfirm(seat);
615
+ },
616
+ onViewChange: () => this.reanchorConfirm(),
617
+ onFocusSeat: (seat) => this.announceSeat(seat),
570
618
  onSeatHover: (d) => this.updateTooltip(d),
571
619
  onHint: (m) => {
572
620
  if (m) this.toast(m);
@@ -691,6 +739,14 @@ var SeatPicker = class _SeatPicker {
691
739
  return this;
692
740
  }
693
741
  this.els.boot.remove();
742
+ if (info.mode === "test") {
743
+ const ribbon = document.createElement("div");
744
+ ribbon.textContent = t2("picker.testMode");
745
+ ribbon.setAttribute("aria-label", t2("picker.testMode"));
746
+ 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;";
747
+ this.els.map.style.overflow = "hidden";
748
+ this.els.map.appendChild(ribbon);
749
+ }
694
750
  const chartTheme = this.controller.doc?.theme;
695
751
  Object.entries(resolveTokens(chartTheme, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
696
752
  this.currency = info.currency ?? this.opts.currency ?? "USD";
@@ -700,10 +756,92 @@ var SeatPicker = class _SeatPicker {
700
756
  this.els.name.textContent = info.eventName ?? "";
701
757
  const when = info.startsAt ? new Date(info.startsAt).toLocaleString(this.opts.locale, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "";
702
758
  this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
759
+ const present = /* @__PURE__ */ new Set();
760
+ if (this.controller.doc) {
761
+ for (const seat of expandChart(this.controller.doc)) {
762
+ for (const type of seat.accessibility ?? []) present.add(type);
763
+ if (seat.accessible && !seat.accessibility?.length) present.add("wheelchair");
764
+ }
765
+ }
766
+ if (present.size) {
767
+ const chips = document.createElement("div");
768
+ chips.className = "sl-chips";
769
+ const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
770
+ const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-f="${key}">${label}</button>`;
771
+ chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
772
+ this.els.map.appendChild(chips);
773
+ chips.querySelectorAll("button").forEach((btn) => {
774
+ btn.addEventListener("click", () => {
775
+ const f = btn.dataset.f;
776
+ this.a11yFilter = f;
777
+ chips.querySelectorAll("button").forEach((b) => b.classList.toggle("on", b === btn));
778
+ this.controller.setAccessibilityFilter(f === "all" ? null : [f]);
779
+ });
780
+ });
781
+ }
782
+ const cb = document.createElement("button");
783
+ cb.type = "button";
784
+ cb.setAttribute("aria-label", "Toggle colorblind-friendly colors");
785
+ cb.setAttribute("aria-pressed", String(!!this.opts.colorblindSafe));
786
+ 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>';
787
+ this.els.zfit.parentElement.appendChild(cb);
788
+ let cbOn = !!this.opts.colorblindSafe;
789
+ cb.addEventListener("click", () => {
790
+ cbOn = !cbOn;
791
+ cb.setAttribute("aria-pressed", String(cbOn));
792
+ this.controller.setColorblindSafe(cbOn);
793
+ });
794
+ this.srEl = document.createElement("div");
795
+ this.srEl.className = "sl-sr";
796
+ this.srEl.setAttribute("aria-live", "polite");
797
+ root.appendChild(this.srEl);
703
798
  this.syncPrices();
704
799
  this.syncTray();
705
800
  return this;
706
801
  }
802
+ /** aria-live readout when keyboard focus lands on a seat. */
803
+ announceSeat(seat) {
804
+ if (!this.srEl) return;
805
+ if (!seat) {
806
+ this.srEl.textContent = "";
807
+ return;
808
+ }
809
+ const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
810
+ const status = this.controller.getStatus(seat.id) ?? "free";
811
+ const statusText = status === "free" ? "available" : status === "held" ? "on hold" : "taken";
812
+ const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
813
+ this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
814
+ }
815
+ // ---- confirm popover (opt-in confirmSelection mode) ------------------------
816
+ showConfirm(seat) {
817
+ this.closeConfirm();
818
+ if (this.tipEl) this.tipEl.style.display = "none";
819
+ const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
820
+ const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
821
+ const el = document.createElement("div");
822
+ el.className = "sl-confirm";
823
+ 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>`;
824
+ this.els.map.appendChild(el);
825
+ this.confirmEl = el;
826
+ this.confirmSeat = seat;
827
+ this.reanchorConfirm();
828
+ el.querySelector(".sl-confirm-add").addEventListener("click", () => this.closeConfirm());
829
+ el.querySelector(".sl-confirm-cancel").addEventListener("click", () => {
830
+ this.controller.deselect([seat.id]);
831
+ this.closeConfirm();
832
+ });
833
+ }
834
+ reanchorConfirm() {
835
+ if (!this.confirmEl || !this.confirmSeat) return;
836
+ const p = this.controller.worldToScreen({ x: this.confirmSeat.x, y: this.confirmSeat.y });
837
+ this.confirmEl.style.left = `${p.x}px`;
838
+ this.confirmEl.style.top = `${p.y}px`;
839
+ }
840
+ closeConfirm() {
841
+ this.confirmEl?.remove();
842
+ this.confirmEl = null;
843
+ this.confirmSeat = null;
844
+ }
707
845
  // ---- chrome sync ----------------------------------------------------------
708
846
  money(n) {
709
847
  try {
@@ -718,8 +856,12 @@ var SeatPicker = class _SeatPicker {
718
856
  const left = this.controller.categoryAvailability();
719
857
  this.els.prices.innerHTML = doc.categories.map((c) => {
720
858
  const price = c.tiers?.length ? c.tiers[0].price : c.price;
721
- 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>`;
859
+ 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>`;
722
860
  }).join("");
861
+ this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
862
+ row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
863
+ row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
864
+ });
723
865
  }
724
866
  /** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
725
867
  evictTakenSelections() {
@@ -733,13 +875,21 @@ var SeatPicker = class _SeatPicker {
733
875
  if (!this.els.tray) return;
734
876
  const seats = this.controller.getSelection();
735
877
  const gaAreas = this.controller.getGAAreas();
878
+ const heldItems = this.hold?.items ?? [];
736
879
  const parts = [];
737
- if (!seats.length && !gaAreas.length) {
880
+ if (!seats.length && !heldItems.length && !gaAreas.length) {
738
881
  parts.push(`<div class="sl-tray-hint">Tap a seat on the map, or let us pick the best available for you.</div>`);
739
- } else if (!seats.length) {
882
+ } else if (!seats.length && !heldItems.length) {
740
883
  parts.push(`<div class="sl-tray-hint">Tap a seat on the map \u2014 or grab standing tickets below.</div>`);
741
884
  }
742
- for (const s of seats) {
885
+ for (const item of heldItems) {
886
+ const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
887
+ parts.push(
888
+ `<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>`
889
+ );
890
+ }
891
+ const heldLabels = new Set(heldItems.map((item) => item.label));
892
+ for (const s of seats.filter((seat) => !heldLabels.has(seat.label))) {
743
893
  const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
744
894
  parts.push(
745
895
  `<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>`
@@ -751,7 +901,25 @@ var SeatPicker = class _SeatPicker {
751
901
  `<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>`
752
902
  );
753
903
  }
904
+ if (!this.hold) {
905
+ const cats = this.controller.doc?.categories ?? [];
906
+ parts.push(
907
+ `<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>`
908
+ );
909
+ }
754
910
  this.els.tray.innerHTML = parts.join("");
911
+ this.els.tray.querySelectorAll("[data-ba]").forEach((btn) => {
912
+ btn.addEventListener("click", () => {
913
+ this.baQty = Math.max(1, Math.min(8, this.baQty + Number(btn.dataset.ba)));
914
+ this.syncTray();
915
+ });
916
+ });
917
+ this.els.tray.querySelector("[data-ba-cat]")?.addEventListener("change", (e) => {
918
+ this.baCat = e.target.value;
919
+ });
920
+ this.els.tray.querySelector(".sl-ba-go")?.addEventListener("click", () => {
921
+ void this.bestAvailable(this.baQty, this.baCat || void 0);
922
+ });
755
923
  this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
756
924
  btn.addEventListener("click", () => {
757
925
  const id = btn.closest(".sl-chip").dataset.seat;
@@ -770,17 +938,24 @@ var SeatPicker = class _SeatPicker {
770
938
  });
771
939
  const gaTotal = gaAreas.reduce((sum, a) => sum + a.price * (this.gaQty.get(a.id) ?? 0), 0);
772
940
  const gaCount = [...this.gaQty.values()].reduce((a, b) => a + b, 0);
773
- const total = seats.reduce((sum, s) => sum + s.price, 0) + gaTotal;
774
- const count = seats.length + gaCount;
941
+ const heldTotal = heldItems.reduce((sum, item) => sum + item.unitPrice * (item.quantity ?? 1), 0);
942
+ const heldCount = heldItems.reduce((sum, item) => sum + (item.quantity ?? 1), 0);
943
+ const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
944
+ const total = freshSeats.reduce((sum, s) => sum + s.price, 0) + gaTotal + heldTotal;
945
+ const count = freshSeats.length + gaCount + heldCount;
775
946
  this.els.count.textContent = count ? `${count} ${count === 1 ? "ticket" : "tickets"}` : "No seats selected";
776
947
  this.els.total.textContent = count ? this.money(total) : "";
777
948
  const cta = this.els.cta;
778
949
  cta.disabled = count === 0;
779
- cta.textContent = count ? "Hold seats & checkout" : "Select seats";
950
+ cta.textContent = this.hold ? "Continue to checkout" : count ? "Hold seats & checkout" : "Select seats";
780
951
  this.opts.onSelectionChange?.(seats);
781
952
  }
782
953
  async handleCta() {
783
954
  const cta = this.els.cta;
955
+ if (this.hold && !this.controller.getSelection().some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
956
+ this.opts.onCheckout?.(this.hold, this.controller.getSelection());
957
+ return;
958
+ }
784
959
  cta.disabled = true;
785
960
  cta.textContent = "Holding\u2026";
786
961
  try {
@@ -888,6 +1063,7 @@ var SeatPicker = class _SeatPicker {
888
1063
  }
889
1064
  destroy() {
890
1065
  this.destroyed = true;
1066
+ this.closeConfirm();
891
1067
  this.stopHoldTimer();
892
1068
  if (this.toastTimer) clearTimeout(this.toastTimer);
893
1069
  this.ro?.disconnect();