@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.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);
@@ -700,10 +748,92 @@ var SeatPicker = class _SeatPicker {
700
748
  this.els.name.textContent = info.eventName ?? "";
701
749
  const when = info.startsAt ? new Date(info.startsAt).toLocaleString(this.opts.locale, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : "";
702
750
  this.els.meta.textContent = [info.venue, when].filter(Boolean).join(" \xB7 ");
751
+ const present = /* @__PURE__ */ new Set();
752
+ if (this.controller.doc) {
753
+ for (const seat of expandChart(this.controller.doc)) {
754
+ for (const type of seat.accessibility ?? []) present.add(type);
755
+ if (seat.accessible && !seat.accessibility?.length) present.add("wheelchair");
756
+ }
757
+ }
758
+ if (present.size) {
759
+ const chips = document.createElement("div");
760
+ chips.className = "sl-chips";
761
+ const GLYPH = { wheelchair: "\u267F", companion: "\u{1F9D1}\u200D\u{1F91D}\u200D\u{1F9D1}" };
762
+ const mk = (key, label) => `<button type="button" class="sl-chip-f${key === "all" ? " on" : ""}" data-f="${key}">${label}</button>`;
763
+ chips.innerHTML = mk("all", "All seats") + [...present].map((type) => mk(type, `${GLYPH[type] ? GLYPH[type] + " " : ""}${type[0].toUpperCase()}${type.slice(1).replace(/-/g, " ")}`)).join("");
764
+ this.els.map.appendChild(chips);
765
+ chips.querySelectorAll("button").forEach((btn) => {
766
+ btn.addEventListener("click", () => {
767
+ const f = btn.dataset.f;
768
+ this.a11yFilter = f;
769
+ chips.querySelectorAll("button").forEach((b) => b.classList.toggle("on", b === btn));
770
+ this.controller.setAccessibilityFilter(f === "all" ? null : [f]);
771
+ });
772
+ });
773
+ }
774
+ const cb = document.createElement("button");
775
+ cb.type = "button";
776
+ cb.setAttribute("aria-label", "Toggle colorblind-friendly colors");
777
+ cb.setAttribute("aria-pressed", String(!!this.opts.colorblindSafe));
778
+ 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>';
779
+ this.els.zfit.parentElement.appendChild(cb);
780
+ let cbOn = !!this.opts.colorblindSafe;
781
+ cb.addEventListener("click", () => {
782
+ cbOn = !cbOn;
783
+ cb.setAttribute("aria-pressed", String(cbOn));
784
+ this.controller.setColorblindSafe(cbOn);
785
+ });
786
+ this.srEl = document.createElement("div");
787
+ this.srEl.className = "sl-sr";
788
+ this.srEl.setAttribute("aria-live", "polite");
789
+ root.appendChild(this.srEl);
703
790
  this.syncPrices();
704
791
  this.syncTray();
705
792
  return this;
706
793
  }
794
+ /** aria-live readout when keyboard focus lands on a seat. */
795
+ announceSeat(seat) {
796
+ if (!this.srEl) return;
797
+ if (!seat) {
798
+ this.srEl.textContent = "";
799
+ return;
800
+ }
801
+ const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
802
+ const status = this.controller.getStatus(seat.id) ?? "free";
803
+ const statusText = status === "free" ? "available" : status === "held" ? "on hold" : "taken";
804
+ const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
805
+ this.srEl.textContent = `Seat ${seat.label}, ${cat?.label ?? seat.categoryKey}${price != null ? `, ${this.money(price)}` : ""}, ${statusText}`;
806
+ }
807
+ // ---- confirm popover (opt-in confirmSelection mode) ------------------------
808
+ showConfirm(seat) {
809
+ this.closeConfirm();
810
+ if (this.tipEl) this.tipEl.style.display = "none";
811
+ const cat = this.controller.doc?.categories.find((c) => c.key === seat.categoryKey);
812
+ const price = cat?.tiers?.length ? cat.tiers[0].price : cat?.price;
813
+ const el = document.createElement("div");
814
+ el.className = "sl-confirm";
815
+ 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>`;
816
+ this.els.map.appendChild(el);
817
+ this.confirmEl = el;
818
+ this.confirmSeat = seat;
819
+ this.reanchorConfirm();
820
+ el.querySelector(".sl-confirm-add").addEventListener("click", () => this.closeConfirm());
821
+ el.querySelector(".sl-confirm-cancel").addEventListener("click", () => {
822
+ this.controller.deselect([seat.id]);
823
+ this.closeConfirm();
824
+ });
825
+ }
826
+ reanchorConfirm() {
827
+ if (!this.confirmEl || !this.confirmSeat) return;
828
+ const p = this.controller.worldToScreen({ x: this.confirmSeat.x, y: this.confirmSeat.y });
829
+ this.confirmEl.style.left = `${p.x}px`;
830
+ this.confirmEl.style.top = `${p.y}px`;
831
+ }
832
+ closeConfirm() {
833
+ this.confirmEl?.remove();
834
+ this.confirmEl = null;
835
+ this.confirmSeat = null;
836
+ }
707
837
  // ---- chrome sync ----------------------------------------------------------
708
838
  money(n) {
709
839
  try {
@@ -718,8 +848,12 @@ var SeatPicker = class _SeatPicker {
718
848
  const left = this.controller.categoryAvailability();
719
849
  this.els.prices.innerHTML = doc.categories.map((c) => {
720
850
  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>`;
851
+ 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
852
  }).join("");
853
+ this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
854
+ row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
855
+ row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
856
+ });
723
857
  }
724
858
  /** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
725
859
  evictTakenSelections() {
@@ -733,13 +867,21 @@ var SeatPicker = class _SeatPicker {
733
867
  if (!this.els.tray) return;
734
868
  const seats = this.controller.getSelection();
735
869
  const gaAreas = this.controller.getGAAreas();
870
+ const heldItems = this.hold?.items ?? [];
736
871
  const parts = [];
737
- if (!seats.length && !gaAreas.length) {
872
+ if (!seats.length && !heldItems.length && !gaAreas.length) {
738
873
  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) {
874
+ } else if (!seats.length && !heldItems.length) {
740
875
  parts.push(`<div class="sl-tray-hint">Tap a seat on the map \u2014 or grab standing tickets below.</div>`);
741
876
  }
742
- for (const s of seats) {
877
+ for (const item of heldItems) {
878
+ const cat = this.controller.doc?.categories.find((c) => c.key === item.categoryKey);
879
+ parts.push(
880
+ `<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>`
881
+ );
882
+ }
883
+ const heldLabels = new Set(heldItems.map((item) => item.label));
884
+ for (const s of seats.filter((seat) => !heldLabels.has(seat.label))) {
743
885
  const cat = this.controller.doc?.categories.find((c) => c.key === s.categoryKey);
744
886
  parts.push(
745
887
  `<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 +893,25 @@ var SeatPicker = class _SeatPicker {
751
893
  `<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
894
  );
753
895
  }
896
+ if (!this.hold) {
897
+ const cats = this.controller.doc?.categories ?? [];
898
+ parts.push(
899
+ `<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>`
900
+ );
901
+ }
754
902
  this.els.tray.innerHTML = parts.join("");
903
+ this.els.tray.querySelectorAll("[data-ba]").forEach((btn) => {
904
+ btn.addEventListener("click", () => {
905
+ this.baQty = Math.max(1, Math.min(8, this.baQty + Number(btn.dataset.ba)));
906
+ this.syncTray();
907
+ });
908
+ });
909
+ this.els.tray.querySelector("[data-ba-cat]")?.addEventListener("change", (e) => {
910
+ this.baCat = e.target.value;
911
+ });
912
+ this.els.tray.querySelector(".sl-ba-go")?.addEventListener("click", () => {
913
+ void this.bestAvailable(this.baQty, this.baCat || void 0);
914
+ });
755
915
  this.els.tray.querySelectorAll(".sl-chip .rm").forEach((btn) => {
756
916
  btn.addEventListener("click", () => {
757
917
  const id = btn.closest(".sl-chip").dataset.seat;
@@ -770,17 +930,24 @@ var SeatPicker = class _SeatPicker {
770
930
  });
771
931
  const gaTotal = gaAreas.reduce((sum, a) => sum + a.price * (this.gaQty.get(a.id) ?? 0), 0);
772
932
  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;
933
+ const heldTotal = heldItems.reduce((sum, item) => sum + item.unitPrice * (item.quantity ?? 1), 0);
934
+ const heldCount = heldItems.reduce((sum, item) => sum + (item.quantity ?? 1), 0);
935
+ const freshSeats = seats.filter((seat) => !heldLabels.has(seat.label));
936
+ const total = freshSeats.reduce((sum, s) => sum + s.price, 0) + gaTotal + heldTotal;
937
+ const count = freshSeats.length + gaCount + heldCount;
775
938
  this.els.count.textContent = count ? `${count} ${count === 1 ? "ticket" : "tickets"}` : "No seats selected";
776
939
  this.els.total.textContent = count ? this.money(total) : "";
777
940
  const cta = this.els.cta;
778
941
  cta.disabled = count === 0;
779
- cta.textContent = count ? "Hold seats & checkout" : "Select seats";
942
+ cta.textContent = this.hold ? "Continue to checkout" : count ? "Hold seats & checkout" : "Select seats";
780
943
  this.opts.onSelectionChange?.(seats);
781
944
  }
782
945
  async handleCta() {
783
946
  const cta = this.els.cta;
947
+ if (this.hold && !this.controller.getSelection().some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
948
+ this.opts.onCheckout?.(this.hold, this.controller.getSelection());
949
+ return;
950
+ }
784
951
  cta.disabled = true;
785
952
  cta.textContent = "Holding\u2026";
786
953
  try {
@@ -888,6 +1055,7 @@ var SeatPicker = class _SeatPicker {
888
1055
  }
889
1056
  destroy() {
890
1057
  this.destroyed = true;
1058
+ this.closeConfirm();
891
1059
  this.stopHoldTimer();
892
1060
  if (this.toastTimer) clearTimeout(this.toastTimer);
893
1061
  this.ro?.disconnect();