@seatlayer/js 0.19.0 → 0.21.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.d.cts CHANGED
@@ -565,6 +565,8 @@ declare class SeatPicker {
565
565
  private miniBase;
566
566
  private miniTf;
567
567
  private priceBandKeys;
568
+ private focusedCatKey;
569
+ private pricesExpanded;
568
570
  /** Last surfaced section summary (re-rendered when the price band changes). */
569
571
  private lastSection;
570
572
  /** Section card collapsed to its slim pill (seat-picking has begun). */
@@ -808,6 +810,10 @@ declare class SeatPicker {
808
810
  */
809
811
  private paidPrice;
810
812
  private syncPrices;
813
+ /** Tap a price row → filter + frame that category on the map; tap again to
814
+ * clear. Shares `priceBandKeys` with the band selector so the row-dim state
815
+ * has one source of truth (and each control resets the other). */
816
+ private focusCategory;
811
817
  /**
812
818
  * Live-activity strip: turn WS availability deltas into one quiet line of
813
819
  * social proof ("2 seats just taken in VIP · 118 left"). Diffs per-category
@@ -816,6 +822,9 @@ declare class SeatPicker {
816
822
  */
817
823
  private narrateAvailability;
818
824
  private lastCatAvail;
825
+ private lastAvailFloorId;
826
+ private availQuietUntil;
827
+ private liveTimer;
819
828
  /** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
820
829
  private evictTakenSelections;
821
830
  private syncTray;
package/dist/index.d.ts CHANGED
@@ -565,6 +565,8 @@ declare class SeatPicker {
565
565
  private miniBase;
566
566
  private miniTf;
567
567
  private priceBandKeys;
568
+ private focusedCatKey;
569
+ private pricesExpanded;
568
570
  /** Last surfaced section summary (re-rendered when the price band changes). */
569
571
  private lastSection;
570
572
  /** Section card collapsed to its slim pill (seat-picking has begun). */
@@ -808,6 +810,10 @@ declare class SeatPicker {
808
810
  */
809
811
  private paidPrice;
810
812
  private syncPrices;
813
+ /** Tap a price row → filter + frame that category on the map; tap again to
814
+ * clear. Shares `priceBandKeys` with the band selector so the row-dim state
815
+ * has one source of truth (and each control resets the other). */
816
+ private focusCategory;
811
817
  /**
812
818
  * Live-activity strip: turn WS availability deltas into one quiet line of
813
819
  * social proof ("2 seats just taken in VIP · 118 left"). Diffs per-category
@@ -816,6 +822,9 @@ declare class SeatPicker {
816
822
  */
817
823
  private narrateAvailability;
818
824
  private lastCatAvail;
825
+ private lastAvailFloorId;
826
+ private availQuietUntil;
827
+ private liveTimer;
819
828
  /** A live delta took one of OUR selected (not yet held) seats — evict + tell the buyer. */
820
829
  private evictTakenSelections;
821
830
  private syncTray;
package/dist/index.js CHANGED
@@ -885,20 +885,30 @@ var CSS = `
885
885
  .sl-prices-sec{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:13px}
886
886
  .sl-price-select{min-height:32px;max-width:130px;padding:5px 28px 5px 9px;border:1px solid var(--sl-line);border-radius:9px;
887
887
  background:var(--sl-surface);color:var(--sl-text);font:inherit;font-size:11px;font-weight:750;letter-spacing:0;text-transform:none}
888
- .sl-prices{padding:5px 14px 10px;border-bottom:1px solid var(--sl-line)}
888
+ .sl-prices{display:flex;flex-direction:column;padding:4px 14px 8px;border-bottom:1px solid var(--sl-line)}
889
889
  .sl-prices-sec,.sl-prices,.sl-seats-sec{flex:none}
890
- .sl-price-row{display:flex;align-items:center;gap:7px;min-height:28px;font-size:12px}
890
+ .sl-price-row{display:flex;align-items:center;gap:7px;min-height:28px;font-size:12px;
891
+ padding:0 6px;margin:0 -6px;border-radius:8px;cursor:pointer;transition:background .15s}
892
+ .sl-price-row:hover,.sl-price-row:focus-visible{background:color-mix(in srgb,var(--sl-line) 40%,transparent)}
893
+ .sl-price-row.sl-active{background:color-mix(in srgb,var(--sl-accent) 9%,transparent)}
894
+ .sl-price-row.sl-active .sl-price-label{color:var(--sl-accent)}
891
895
  .sl-dot{width:9px;height:9px;border-radius:50%;flex:none}
892
896
  .sl-price-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
893
897
  .sl-price-left{font-size:11px;color:var(--sl-muted);font-variant-numeric:tabular-nums}
894
898
  .sl-price-amt{font-weight:800;font-variant-numeric:tabular-nums}
895
- .sl-status-key{display:flex;gap:13px;flex-wrap:wrap;padding:8px 0 2px;margin-top:5px;border-top:1px solid var(--sl-line);color:var(--sl-muted);font-size:10.5px}
896
- .sl-status-item{display:inline-flex;align-items:center;gap:6px}
897
- .sl-status-icon{width:17px;height:17px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;
899
+ /* long category lists: capped by default, scroll once expanded */
900
+ .sl-prices.sl-expanded{max-height:196px;overflow-y:auto;overscroll-behavior:contain;scrollbar-gutter:stable}
901
+ .sl-price-more{display:flex;align-items:center;min-height:26px;padding:0;
902
+ color:var(--sl-muted);font-size:11px;font-weight:750;transition:color .15s}
903
+ .sl-price-more:hover,.sl-price-more:focus-visible{color:var(--sl-text)}
904
+ /* held/sold key \u2014 one quiet caption line; the map itself teaches these states */
905
+ .sl-status-key{display:flex;gap:11px;flex-wrap:wrap;padding:5px 0 0;margin-top:4px;border-top:1px solid var(--sl-line);color:var(--sl-muted);font-size:10px}
906
+ .sl-status-item{display:inline-flex;align-items:center;gap:5px}
907
+ .sl-status-icon{width:13px;height:13px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;
898
908
  color:#fff;background:#6b7280;line-height:1}
899
- .sl-status-icon svg{width:10px;height:10px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
909
+ .sl-status-icon svg{width:8px;height:8px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
900
910
  .sl-status-icon.sold{background:#8b93a0}
901
- .sl-status-icon.sold svg{width:11px;height:11px;stroke-width:2.4}
911
+ .sl-status-icon.sold svg{width:9px;height:9px;stroke-width:2.4}
902
912
 
903
913
  /* tray */
904
914
  .sl-seats-sec{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:13px}
@@ -937,12 +947,15 @@ var CSS = `
937
947
  .sl-chip .view:hover,.sl-chip .view:focus-visible{color:var(--sl-text);background:color-mix(in srgb,var(--sl-accent) 10%,transparent)}
938
948
  .sl-chip .rm svg{width:11px;height:11px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round}
939
949
  .sl-chip .view svg{width:13px;height:13px;stroke:currentColor;stroke-width:1.8;fill:none}
940
- /* live-activity strip \u2014 narrates WS availability deltas (social proof + urgency) */
941
- .sl-live{display:flex;align-items:center;gap:7px;margin:10px 14px 0;padding:7px 9px;flex:none;
950
+ /* live-activity strip \u2014 narrates WS availability deltas (social proof + urgency).
951
+ Hidden until a delta actually happens: a static "seats update in real time"
952
+ banner is dead vertical space, a "2 seats just taken" flash is a signal. */
953
+ .sl-live{display:none;align-items:center;gap:7px;margin:10px 14px 0;padding:7px 9px;flex:none;
942
954
  border:1px solid var(--sl-line);border-radius:8px;background:color-mix(in srgb,var(--sl-accent) 4%,var(--sl-surface));
943
955
  font-size:11px;color:var(--sl-muted)}
944
956
  .sl-live .dot{width:6px;height:6px;border-radius:999px;background:#22a06b;box-shadow:0 0 6px rgba(34,160,107,.75);flex:none}
945
957
  .sl-live span:last-child{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
958
+ .sl-live.on{display:flex;animation:slNoticeIn .38s cubic-bezier(.2,.8,.2,1) both}
946
959
 
947
960
  /* GA rows */
948
961
  .sl-ga{display:flex;align-items:center;gap:10px;padding:9px 11px;border:1px dashed var(--sl-line);border-radius:var(--sl-r-sm)}
@@ -1128,13 +1141,18 @@ var CSS = `
1128
1141
  border-radius:15px;box-shadow:0 24px 64px -18px rgba(0,0,0,.72);transform:translate(-50%,calc(-100% - 16px));
1129
1142
  animation:slConfirmIn .24s cubic-bezier(.2,.8,.2,1) both}
1130
1143
  .sl-confirm[data-placement="below"]{transform:translate(-50%,16px);animation:slConfirmBelowIn .24s cubic-bezier(.2,.8,.2,1) both}
1131
- .sl-confirm-grid{display:grid;grid-template-columns:1.2fr .8fr .8fr;border-bottom:1px solid var(--sl-line)}
1144
+ .sl-confirm-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(52px,auto) minmax(52px,auto);border-bottom:1px solid var(--sl-line)}
1132
1145
  .sl-confirm-field{min-width:0;padding:12px 11px 10px;border-right:1px solid var(--sl-line)}
1133
1146
  .sl-confirm-field:last-child{border-right:0;text-align:center}
1134
1147
  .sl-confirm-field:nth-child(2){text-align:center}
1135
1148
  .sl-confirm-key{display:block;font-size:8.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
1136
1149
  .sl-confirm-value{display:block;margin-top:4px;color:var(--sl-text);font-size:17px;line-height:1.1;font-weight:850;
1137
1150
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1151
+ /* Long venue section names must read in full: smaller type + up to two lines
1152
+ beats an ellipsis at identity-confirmation time. Row/seat stay big \u2014 they're
1153
+ short and they're what the buyer double-checks against the map. */
1154
+ .sl-confirm-field:first-child .sl-confirm-value{font-size:13.5px;line-height:1.25;white-space:normal;
1155
+ display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
1138
1156
  .sl-confirm-cat{display:flex;align-items:center;gap:8px;padding:10px 12px;background:color-mix(in srgb,var(--sl-cat) 76%,var(--sl-surface))}
1139
1157
  .sl-confirm-cat .sl-dot{border:2px solid rgba(255,255,255,.78);width:11px;height:11px}
1140
1158
  .sl-confirm-cat-name{font-size:13.5px;font-weight:800;color:#fff;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@@ -1428,6 +1446,8 @@ var SeatPicker = class _SeatPicker {
1428
1446
  this.miniTf = null;
1429
1447
  // F4 price-band filter — active band's category keys (null = all prices)
1430
1448
  this.priceBandKeys = null;
1449
+ this.focusedCatKey = null;
1450
+ this.pricesExpanded = false;
1431
1451
  /** Last surfaced section summary (re-rendered when the price band changes). */
1432
1452
  this.lastSection = null;
1433
1453
  /** Section card collapsed to its slim pill (seat-picking has begun). */
@@ -1462,6 +1482,9 @@ var SeatPicker = class _SeatPicker {
1462
1482
  /** Set by open(): closes the modal (scroll restore + destroy + onClose). */
1463
1483
  this.closeModal = null;
1464
1484
  this.lastCatAvail = null;
1485
+ this.lastAvailFloorId = "";
1486
+ this.availQuietUntil = 0;
1487
+ this.liveTimer = null;
1465
1488
  if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
1466
1489
  if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
1467
1490
  if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
@@ -1554,7 +1577,7 @@ var SeatPicker = class _SeatPicker {
1554
1577
  return "";
1555
1578
  }
1556
1579
  }
1557
- const sight = distance != null ? `${distance}${this.tf("picker.sightline", "m to stage \xB7 clear sightline")}` : this.tf("picker.sightlineClear", "Clear sightline");
1580
+ const sight = distance != null ? t2("picker.sightline", { m: distance }) : this.tf("picker.sightlineClear", "Clear sightline");
1558
1581
  return `<button type="button" class="sl-confirm-view sl-confirm-thumbwrap" aria-label="${t2("picker.viewFromSeat", { label: seat.label })}"><img class="sl-confirm-thumb" src="${url}" alt="" /><span class="sl-confirm-thumb-badge">\u{1F52D} ${this.tf("picker.viewFromHere", "View from here")}</span></button><div class="sl-confirm-sight"><span aria-hidden="true">\u2713</span>${sight}</div>`;
1559
1582
  }
1560
1583
  /** True when the picker is rendered inside an iframe (snippet embed at /e/:key). */
@@ -2550,9 +2573,13 @@ var SeatPicker = class _SeatPicker {
2550
2573
  select.addEventListener("change", () => {
2551
2574
  const band = bands.find((candidate) => candidate.id === select.value);
2552
2575
  const keys = band?.keys ?? null;
2576
+ this.focusedCatKey = null;
2553
2577
  this.priceBandKeys = keys ? new Set(keys) : null;
2554
2578
  this.controller.setCategoryFilter(keys);
2555
2579
  this.controller.focusCategoryFilter(keys);
2580
+ this.syncFloors();
2581
+ this.syncRung();
2582
+ this.refreshMinimap();
2556
2583
  this.syncPrices();
2557
2584
  if (this.lastSection) this.showSectionCard(this.lastSection);
2558
2585
  });
@@ -2971,15 +2998,51 @@ var SeatPicker = class _SeatPicker {
2971
2998
  const left = this.controller.categoryAvailability();
2972
2999
  this.narrateAvailability(doc.categories, left);
2973
3000
  this.syncSoldout(doc.categories, left);
2974
- this.els.prices.innerHTML = doc.categories.map((c) => {
3001
+ const PRICE_LIMIT = 5;
3002
+ const overflow = doc.categories.length - PRICE_LIMIT;
3003
+ const collapsed = overflow > 1 && !this.pricesExpanded;
3004
+ const shown = collapsed ? doc.categories.slice(0, PRICE_LIMIT) : doc.categories;
3005
+ this.els.prices.classList.toggle("sl-expanded", overflow > 1 && this.pricesExpanded);
3006
+ this.els.prices.innerHTML = shown.map((c) => {
2975
3007
  const price = this.catPrice(c);
3008
+ const active = this.focusedCatKey === c.key;
2976
3009
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
2977
- return `<div class="sl-price-row${dim ? " sl-dim" : ""}" 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>`;
2978
- }).join("") + `<div class="sl-status-key" aria-label="Seat status legend"><span class="sl-status-item"><i class="sl-status-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg></i>Temporarily held</span><span class="sl-status-item"><i class="sl-status-icon sold" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M7 17L17 7"/></svg></i>Sold</span></div>`;
3010
+ return `<div class="sl-price-row${dim ? " sl-dim" : ""}${active ? " sl-active" : ""}" data-cat="${c.key}" role="button" tabindex="0" aria-pressed="${active}" title="${active ? "Show all seats" : `Show ${c.label} seats on the map`}"><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>`;
3011
+ }).join("") + (overflow > 1 ? `<button type="button" class="sl-price-more" aria-expanded="${!collapsed}">` + (collapsed ? `Show all ${doc.categories.length} ticket types` : "Show fewer") + `</button>` : "") + `<div class="sl-status-key" aria-label="Seat status legend"><span class="sl-status-item"><i class="sl-status-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="10" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg></i>Temporarily held</span><span class="sl-status-item"><i class="sl-status-icon sold" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M7 17L17 7"/></svg></i>Sold</span></div>`;
2979
3012
  this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
2980
3013
  row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
2981
3014
  row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
3015
+ const toggle = () => this.focusCategory(row.dataset.cat ?? "");
3016
+ row.addEventListener("click", toggle);
3017
+ row.addEventListener("keydown", (e) => {
3018
+ if (e.key === "Enter" || e.key === " ") {
3019
+ e.preventDefault();
3020
+ toggle();
3021
+ }
3022
+ });
2982
3023
  });
3024
+ this.els.prices.querySelector(".sl-price-more")?.addEventListener("click", () => {
3025
+ this.pricesExpanded = !this.pricesExpanded;
3026
+ this.syncPrices();
3027
+ });
3028
+ }
3029
+ /** Tap a price row → filter + frame that category on the map; tap again to
3030
+ * clear. Shares `priceBandKeys` with the band selector so the row-dim state
3031
+ * has one source of truth (and each control resets the other). */
3032
+ focusCategory(key) {
3033
+ if (!key) return;
3034
+ const next = this.focusedCatKey === key ? null : key;
3035
+ this.focusedCatKey = next;
3036
+ this.priceBandKeys = next ? /* @__PURE__ */ new Set([next]) : null;
3037
+ const select = this.els.pricesSec?.querySelector(".sl-price-select");
3038
+ if (select) select.value = "all";
3039
+ this.controller.setCategoryFilter(next ? [next] : null);
3040
+ this.controller.focusCategoryFilter(next ? [next] : null);
3041
+ this.syncFloors();
3042
+ this.syncRung();
3043
+ this.refreshMinimap();
3044
+ this.syncPrices();
3045
+ if (this.lastSection) this.showSectionCard(this.lastSection);
2983
3046
  }
2984
3047
  /**
2985
3048
  * Live-activity strip: turn WS availability deltas into one quiet line of
@@ -2991,13 +3054,23 @@ var SeatPicker = class _SeatPicker {
2991
3054
  const textEl = this.els.liveText;
2992
3055
  const prev = this.lastCatAvail;
2993
3056
  this.lastCatAvail = { ...left };
2994
- if (!textEl || !prev) return;
3057
+ const floorId = this.controller.getActiveFloorId();
3058
+ if (floorId !== this.lastAvailFloorId) {
3059
+ this.lastAvailFloorId = floorId;
3060
+ this.availQuietUntil = performance.now() + 2e3;
3061
+ }
3062
+ if (!textEl || !prev || performance.now() < this.availQuietUntil) return;
2995
3063
  for (const cat of categories) {
2996
3064
  const before = prev[cat.key];
2997
3065
  const now = left[cat.key] ?? 0;
2998
3066
  if (before === void 0 || now >= before) continue;
2999
3067
  const taken = before - now;
3000
3068
  textEl.textContent = `${taken} seat${taken === 1 ? "" : "s"} just taken in ${cat.label} \xB7 ${now} left`;
3069
+ this.els.live?.classList.remove("on");
3070
+ void this.els.live?.offsetWidth;
3071
+ this.els.live?.classList.add("on");
3072
+ if (this.liveTimer) clearTimeout(this.liveTimer);
3073
+ this.liveTimer = setTimeout(() => this.els.live?.classList.remove("on"), 8e3);
3001
3074
  return;
3002
3075
  }
3003
3076
  }
@@ -3025,7 +3098,8 @@ var SeatPicker = class _SeatPicker {
3025
3098
  } else if (!seats.length && !heldItems.length) {
3026
3099
  parts.push(`<div class="sl-tray-hint">Tap a seat on the map \u2014 or grab standing tickets below.</div>`);
3027
3100
  }
3028
- if (!this.hold) {
3101
+ const noPicks = !seats.length && !heldItems.length;
3102
+ if (!this.hold && (noPicks || this.bestAvailableBusy || this.bestAvailableConfirm)) {
3029
3103
  const cats = this.controller.doc?.categories ?? [];
3030
3104
  parts.push(this.bestAvailableConfirm ? `<div class="sl-ba" role="alert"><div class="sl-ba-title"><span class="spark" aria-hidden="true">\u2726</span>Replace your current choices?</div><div class="sl-ba-replace"><b>We\u2019ll find ${this.baQty} seats together.</b><span>Your manually selected tickets will be removed only after a new group is secured.</span></div><div class="sl-ba-actions"><button type="button" data-ba-cancel>Keep mine</button><button type="button" class="replace" data-ba-replace>Find new seats</button></div></div>` : `<div class="sl-ba"><div class="sl-ba-title"><span class="spark" aria-hidden="true">\u2726</span>Find the best seats together</div><div class="sl-ba-copy"><span class="wide">We\u2019ll choose the closest available group for you.</span><span class="narrow">Closest available group, chosen instantly.</span></div>` + (cats.length > 1 ? `<select aria-label="Preferred ticket type" data-ba-cat><option value="">Any ticket type</option>` + cats.map((c) => `<option value="${c.key}"${this.baCat === c.key ? " selected" : ""}>${c.label}</option>`).join("") + `</select>` : `<span aria-hidden="true"></span>`) + `<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"${this.bestAvailableBusy ? " disabled" : ""}>` + (this.bestAvailableBusy ? `<span class="sl-ba-spin" aria-hidden="true"></span>Finding the best seats\u2026` : `Find ${this.baQty} best ${this.baQty === 1 ? "seat" : "seats"}`) + `</button></div>`);
3031
3105
  }
@@ -3589,6 +3663,7 @@ var SeatPicker = class _SeatPicker {
3589
3663
  this.closeSeatView();
3590
3664
  this.stopHoldTimer();
3591
3665
  if (this.toastTimer) clearTimeout(this.toastTimer);
3666
+ if (this.liveTimer) clearTimeout(this.liveTimer);
3592
3667
  for (const timer of this.motionTimers) clearTimeout(timer);
3593
3668
  this.motionTimers.clear();
3594
3669
  this.ro?.disconnect();