@seatlayer/js 0.20.0 → 0.21.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
@@ -909,20 +909,30 @@ var CSS = `
909
909
  .sl-prices-sec{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:13px}
910
910
  .sl-price-select{min-height:32px;max-width:130px;padding:5px 28px 5px 9px;border:1px solid var(--sl-line);border-radius:9px;
911
911
  background:var(--sl-surface);color:var(--sl-text);font:inherit;font-size:11px;font-weight:750;letter-spacing:0;text-transform:none}
912
- .sl-prices{padding:5px 14px 10px;border-bottom:1px solid var(--sl-line)}
912
+ .sl-prices{display:flex;flex-direction:column;padding:4px 14px 8px;border-bottom:1px solid var(--sl-line)}
913
913
  .sl-prices-sec,.sl-prices,.sl-seats-sec{flex:none}
914
- .sl-price-row{display:flex;align-items:center;gap:7px;min-height:28px;font-size:12px}
914
+ .sl-price-row{display:flex;align-items:center;gap:7px;min-height:28px;font-size:12px;
915
+ padding:0 6px;margin:0 -6px;border-radius:8px;cursor:pointer;transition:background .15s}
916
+ .sl-price-row:hover,.sl-price-row:focus-visible{background:color-mix(in srgb,var(--sl-line) 40%,transparent)}
917
+ .sl-price-row.sl-active{background:color-mix(in srgb,var(--sl-accent) 9%,transparent)}
918
+ .sl-price-row.sl-active .sl-price-label{color:var(--sl-accent)}
915
919
  .sl-dot{width:9px;height:9px;border-radius:50%;flex:none}
916
920
  .sl-price-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
917
921
  .sl-price-left{font-size:11px;color:var(--sl-muted);font-variant-numeric:tabular-nums}
918
922
  .sl-price-amt{font-weight:800;font-variant-numeric:tabular-nums}
919
- .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}
920
- .sl-status-item{display:inline-flex;align-items:center;gap:6px}
921
- .sl-status-icon{width:17px;height:17px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;
923
+ /* long category lists: capped by default, scroll once expanded */
924
+ .sl-prices.sl-expanded{max-height:196px;overflow-y:auto;overscroll-behavior:contain;scrollbar-gutter:stable}
925
+ .sl-price-more{display:flex;align-items:center;min-height:26px;padding:0;
926
+ color:var(--sl-muted);font-size:11px;font-weight:750;transition:color .15s}
927
+ .sl-price-more:hover,.sl-price-more:focus-visible{color:var(--sl-text)}
928
+ /* held/sold key \u2014 one quiet caption line; the map itself teaches these states */
929
+ .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}
930
+ .sl-status-item{display:inline-flex;align-items:center;gap:5px}
931
+ .sl-status-icon{width:13px;height:13px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;
922
932
  color:#fff;background:#6b7280;line-height:1}
923
- .sl-status-icon svg{width:10px;height:10px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
933
+ .sl-status-icon svg{width:8px;height:8px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
924
934
  .sl-status-icon.sold{background:#8b93a0}
925
- .sl-status-icon.sold svg{width:11px;height:11px;stroke-width:2.4}
935
+ .sl-status-icon.sold svg{width:9px;height:9px;stroke-width:2.4}
926
936
 
927
937
  /* tray */
928
938
  .sl-seats-sec{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:13px}
@@ -961,12 +971,15 @@ var CSS = `
961
971
  .sl-chip .view:hover,.sl-chip .view:focus-visible{color:var(--sl-text);background:color-mix(in srgb,var(--sl-accent) 10%,transparent)}
962
972
  .sl-chip .rm svg{width:11px;height:11px;stroke:currentColor;stroke-width:2.4;fill:none;stroke-linecap:round}
963
973
  .sl-chip .view svg{width:13px;height:13px;stroke:currentColor;stroke-width:1.8;fill:none}
964
- /* live-activity strip \u2014 narrates WS availability deltas (social proof + urgency) */
965
- .sl-live{display:flex;align-items:center;gap:7px;margin:10px 14px 0;padding:7px 9px;flex:none;
974
+ /* live-activity strip \u2014 narrates WS availability deltas (social proof + urgency).
975
+ Hidden until a delta actually happens: a static "seats update in real time"
976
+ banner is dead vertical space, a "2 seats just taken" flash is a signal. */
977
+ .sl-live{display:none;align-items:center;gap:7px;margin:10px 14px 0;padding:7px 9px;flex:none;
966
978
  border:1px solid var(--sl-line);border-radius:8px;background:color-mix(in srgb,var(--sl-accent) 4%,var(--sl-surface));
967
979
  font-size:11px;color:var(--sl-muted)}
968
980
  .sl-live .dot{width:6px;height:6px;border-radius:999px;background:#22a06b;box-shadow:0 0 6px rgba(34,160,107,.75);flex:none}
969
981
  .sl-live span:last-child{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
982
+ .sl-live.on{display:flex;animation:slNoticeIn .38s cubic-bezier(.2,.8,.2,1) both}
970
983
 
971
984
  /* GA rows */
972
985
  .sl-ga{display:flex;align-items:center;gap:10px;padding:9px 11px;border:1px dashed var(--sl-line);border-radius:var(--sl-r-sm)}
@@ -1152,13 +1165,18 @@ var CSS = `
1152
1165
  border-radius:15px;box-shadow:0 24px 64px -18px rgba(0,0,0,.72);transform:translate(-50%,calc(-100% - 16px));
1153
1166
  animation:slConfirmIn .24s cubic-bezier(.2,.8,.2,1) both}
1154
1167
  .sl-confirm[data-placement="below"]{transform:translate(-50%,16px);animation:slConfirmBelowIn .24s cubic-bezier(.2,.8,.2,1) both}
1155
- .sl-confirm-grid{display:grid;grid-template-columns:1.2fr .8fr .8fr;border-bottom:1px solid var(--sl-line)}
1168
+ .sl-confirm-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(52px,auto) minmax(52px,auto);border-bottom:1px solid var(--sl-line)}
1156
1169
  .sl-confirm-field{min-width:0;padding:12px 11px 10px;border-right:1px solid var(--sl-line)}
1157
1170
  .sl-confirm-field:last-child{border-right:0;text-align:center}
1158
1171
  .sl-confirm-field:nth-child(2){text-align:center}
1159
1172
  .sl-confirm-key{display:block;font-size:8.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-muted);font-weight:800}
1160
1173
  .sl-confirm-value{display:block;margin-top:4px;color:var(--sl-text);font-size:17px;line-height:1.1;font-weight:850;
1161
1174
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1175
+ /* Long venue section names must read in full: smaller type + up to two lines
1176
+ beats an ellipsis at identity-confirmation time. Row/seat stay big \u2014 they're
1177
+ short and they're what the buyer double-checks against the map. */
1178
+ .sl-confirm-field:first-child .sl-confirm-value{font-size:13.5px;line-height:1.25;white-space:normal;
1179
+ display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
1162
1180
  .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))}
1163
1181
  .sl-confirm-cat .sl-dot{border:2px solid rgba(255,255,255,.78);width:11px;height:11px}
1164
1182
  .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}
@@ -1452,6 +1470,8 @@ var SeatPicker = class _SeatPicker {
1452
1470
  this.miniTf = null;
1453
1471
  // F4 price-band filter — active band's category keys (null = all prices)
1454
1472
  this.priceBandKeys = null;
1473
+ this.focusedCatKey = null;
1474
+ this.pricesExpanded = false;
1455
1475
  /** Last surfaced section summary (re-rendered when the price band changes). */
1456
1476
  this.lastSection = null;
1457
1477
  /** Section card collapsed to its slim pill (seat-picking has begun). */
@@ -1486,6 +1506,9 @@ var SeatPicker = class _SeatPicker {
1486
1506
  /** Set by open(): closes the modal (scroll restore + destroy + onClose). */
1487
1507
  this.closeModal = null;
1488
1508
  this.lastCatAvail = null;
1509
+ this.lastAvailFloorId = "";
1510
+ this.availQuietUntil = 0;
1511
+ this.liveTimer = null;
1489
1512
  if (!options || typeof options !== "object") throw new Error("seatmap: options object is required");
1490
1513
  if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
1491
1514
  if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
@@ -1578,7 +1601,7 @@ var SeatPicker = class _SeatPicker {
1578
1601
  return "";
1579
1602
  }
1580
1603
  }
1581
- const sight = distance != null ? `${distance}${this.tf("picker.sightline", "m to stage \xB7 clear sightline")}` : this.tf("picker.sightlineClear", "Clear sightline");
1604
+ const sight = distance != null ? (0, import_core2.t)("picker.sightline", { m: distance }) : this.tf("picker.sightlineClear", "Clear sightline");
1582
1605
  return `<button type="button" class="sl-confirm-view sl-confirm-thumbwrap" aria-label="${(0, import_core2.t)("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>`;
1583
1606
  }
1584
1607
  /** True when the picker is rendered inside an iframe (snippet embed at /e/:key). */
@@ -2574,9 +2597,13 @@ var SeatPicker = class _SeatPicker {
2574
2597
  select.addEventListener("change", () => {
2575
2598
  const band = bands.find((candidate) => candidate.id === select.value);
2576
2599
  const keys = band?.keys ?? null;
2600
+ this.focusedCatKey = null;
2577
2601
  this.priceBandKeys = keys ? new Set(keys) : null;
2578
2602
  this.controller.setCategoryFilter(keys);
2579
2603
  this.controller.focusCategoryFilter(keys);
2604
+ this.syncFloors();
2605
+ this.syncRung();
2606
+ this.refreshMinimap();
2580
2607
  this.syncPrices();
2581
2608
  if (this.lastSection) this.showSectionCard(this.lastSection);
2582
2609
  });
@@ -2685,6 +2712,7 @@ var SeatPicker = class _SeatPicker {
2685
2712
  if (narrow) {
2686
2713
  card.className = "sl-seccard strip on";
2687
2714
  card.setAttribute("role", "status");
2715
+ card.setAttribute("aria-label", (0, import_core2.t)("picker.sectionSummaryAria", { label: summary.label }));
2688
2716
  card.innerHTML = `<span class="sl-seccard-dot" style="background:${summary.color}"></span><span class="sl-seccard-name">${summary.label}</span><span class="sl-seccard-left">${leftLabel}</span>` + (summary.categories.length ? `<span class="sl-seccard-price">${priceLabel}</span>` : "") + xBtn;
2689
2717
  card.querySelector(".sl-seccard-x").addEventListener("click", () => this.controller.overview());
2690
2718
  (this.els.sheetHead ?? this.els.side ?? this.els.map).appendChild(card);
@@ -2995,16 +3023,52 @@ var SeatPicker = class _SeatPicker {
2995
3023
  const left = this.controller.categoryAvailability();
2996
3024
  this.narrateAvailability(doc.categories, left);
2997
3025
  this.syncSoldout(doc.categories, left);
2998
- this.els.prices.innerHTML = doc.categories.map((c) => {
3026
+ const PRICE_LIMIT = 5;
3027
+ const overflow = doc.categories.length - PRICE_LIMIT;
3028
+ const collapsed = overflow > 1 && !this.pricesExpanded;
3029
+ const shown = collapsed ? doc.categories.slice(0, PRICE_LIMIT) : doc.categories;
3030
+ this.els.prices.classList.toggle("sl-expanded", overflow > 1 && this.pricesExpanded);
3031
+ this.els.prices.innerHTML = shown.map((c) => {
2999
3032
  const price = this.catPrice(c);
3033
+ const active = this.focusedCatKey === c.key;
3000
3034
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
3001
- 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>`;
3002
- }).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>`;
3035
+ 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>`;
3036
+ }).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>`;
3003
3037
  this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
3004
3038
  row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
3005
3039
  row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
3040
+ const toggle = () => this.focusCategory(row.dataset.cat ?? "");
3041
+ row.addEventListener("click", toggle);
3042
+ row.addEventListener("keydown", (e) => {
3043
+ if (e.key === "Enter" || e.key === " ") {
3044
+ e.preventDefault();
3045
+ toggle();
3046
+ }
3047
+ });
3048
+ });
3049
+ this.els.prices.querySelector(".sl-price-more")?.addEventListener("click", () => {
3050
+ this.pricesExpanded = !this.pricesExpanded;
3051
+ this.syncPrices();
3006
3052
  });
3007
3053
  }
3054
+ /** Tap a price row → filter + frame that category on the map; tap again to
3055
+ * clear. Shares `priceBandKeys` with the band selector so the row-dim state
3056
+ * has one source of truth (and each control resets the other). */
3057
+ focusCategory(key) {
3058
+ if (!key) return;
3059
+ const next = this.focusedCatKey === key ? null : key;
3060
+ this.focusedCatKey = next;
3061
+ this.priceBandKeys = next ? /* @__PURE__ */ new Set([next]) : null;
3062
+ const select = this.els.pricesSec?.querySelector(".sl-price-select");
3063
+ if (select) select.value = "all";
3064
+ this.controller.setCategoryFilter(next ? [next] : null);
3065
+ this.controller.focusCategoryFilter(next ? [next] : null);
3066
+ this.syncFloors();
3067
+ this.syncRung();
3068
+ this.refreshMinimap();
3069
+ this.syncPrices();
3070
+ if (this.lastSection) this.showSectionCard(this.lastSection);
3071
+ }
3008
3072
  /**
3009
3073
  * Live-activity strip: turn WS availability deltas into one quiet line of
3010
3074
  * social proof ("2 seats just taken in VIP · 118 left"). Diffs per-category
@@ -3015,13 +3079,23 @@ var SeatPicker = class _SeatPicker {
3015
3079
  const textEl = this.els.liveText;
3016
3080
  const prev = this.lastCatAvail;
3017
3081
  this.lastCatAvail = { ...left };
3018
- if (!textEl || !prev) return;
3082
+ const floorId = this.controller.getActiveFloorId();
3083
+ if (floorId !== this.lastAvailFloorId) {
3084
+ this.lastAvailFloorId = floorId;
3085
+ this.availQuietUntil = performance.now() + 2e3;
3086
+ }
3087
+ if (!textEl || !prev || performance.now() < this.availQuietUntil) return;
3019
3088
  for (const cat of categories) {
3020
3089
  const before = prev[cat.key];
3021
3090
  const now = left[cat.key] ?? 0;
3022
3091
  if (before === void 0 || now >= before) continue;
3023
3092
  const taken = before - now;
3024
3093
  textEl.textContent = `${taken} seat${taken === 1 ? "" : "s"} just taken in ${cat.label} \xB7 ${now} left`;
3094
+ this.els.live?.classList.remove("on");
3095
+ void this.els.live?.offsetWidth;
3096
+ this.els.live?.classList.add("on");
3097
+ if (this.liveTimer) clearTimeout(this.liveTimer);
3098
+ this.liveTimer = setTimeout(() => this.els.live?.classList.remove("on"), 8e3);
3025
3099
  return;
3026
3100
  }
3027
3101
  }
@@ -3049,7 +3123,8 @@ var SeatPicker = class _SeatPicker {
3049
3123
  } else if (!seats.length && !heldItems.length) {
3050
3124
  parts.push(`<div class="sl-tray-hint">Tap a seat on the map \u2014 or grab standing tickets below.</div>`);
3051
3125
  }
3052
- if (!this.hold) {
3126
+ const noPicks = !seats.length && !heldItems.length;
3127
+ if (!this.hold && (noPicks || this.bestAvailableBusy || this.bestAvailableConfirm)) {
3053
3128
  const cats = this.controller.doc?.categories ?? [];
3054
3129
  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>`);
3055
3130
  }
@@ -3613,6 +3688,7 @@ var SeatPicker = class _SeatPicker {
3613
3688
  this.closeSeatView();
3614
3689
  this.stopHoldTimer();
3615
3690
  if (this.toastTimer) clearTimeout(this.toastTimer);
3691
+ if (this.liveTimer) clearTimeout(this.liveTimer);
3616
3692
  for (const timer of this.motionTimers) clearTimeout(timer);
3617
3693
  this.motionTimers.clear();
3618
3694
  this.ro?.disconnect();