@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.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
  });
@@ -2995,15 +3022,51 @@ var SeatPicker = class _SeatPicker {
2995
3022
  const left = this.controller.categoryAvailability();
2996
3023
  this.narrateAvailability(doc.categories, left);
2997
3024
  this.syncSoldout(doc.categories, left);
2998
- this.els.prices.innerHTML = doc.categories.map((c) => {
3025
+ const PRICE_LIMIT = 5;
3026
+ const overflow = doc.categories.length - PRICE_LIMIT;
3027
+ const collapsed = overflow > 1 && !this.pricesExpanded;
3028
+ const shown = collapsed ? doc.categories.slice(0, PRICE_LIMIT) : doc.categories;
3029
+ this.els.prices.classList.toggle("sl-expanded", overflow > 1 && this.pricesExpanded);
3030
+ this.els.prices.innerHTML = shown.map((c) => {
2999
3031
  const price = this.catPrice(c);
3032
+ const active = this.focusedCatKey === c.key;
3000
3033
  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>`;
3034
+ 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>`;
3035
+ }).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
3036
  this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
3004
3037
  row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
3005
3038
  row.addEventListener("mouseleave", () => this.controller.getRenderer()?.setCategoryHighlight?.(null));
3039
+ const toggle = () => this.focusCategory(row.dataset.cat ?? "");
3040
+ row.addEventListener("click", toggle);
3041
+ row.addEventListener("keydown", (e) => {
3042
+ if (e.key === "Enter" || e.key === " ") {
3043
+ e.preventDefault();
3044
+ toggle();
3045
+ }
3046
+ });
3006
3047
  });
3048
+ this.els.prices.querySelector(".sl-price-more")?.addEventListener("click", () => {
3049
+ this.pricesExpanded = !this.pricesExpanded;
3050
+ this.syncPrices();
3051
+ });
3052
+ }
3053
+ /** Tap a price row → filter + frame that category on the map; tap again to
3054
+ * clear. Shares `priceBandKeys` with the band selector so the row-dim state
3055
+ * has one source of truth (and each control resets the other). */
3056
+ focusCategory(key) {
3057
+ if (!key) return;
3058
+ const next = this.focusedCatKey === key ? null : key;
3059
+ this.focusedCatKey = next;
3060
+ this.priceBandKeys = next ? /* @__PURE__ */ new Set([next]) : null;
3061
+ const select = this.els.pricesSec?.querySelector(".sl-price-select");
3062
+ if (select) select.value = "all";
3063
+ this.controller.setCategoryFilter(next ? [next] : null);
3064
+ this.controller.focusCategoryFilter(next ? [next] : null);
3065
+ this.syncFloors();
3066
+ this.syncRung();
3067
+ this.refreshMinimap();
3068
+ this.syncPrices();
3069
+ if (this.lastSection) this.showSectionCard(this.lastSection);
3007
3070
  }
3008
3071
  /**
3009
3072
  * Live-activity strip: turn WS availability deltas into one quiet line of
@@ -3015,13 +3078,23 @@ var SeatPicker = class _SeatPicker {
3015
3078
  const textEl = this.els.liveText;
3016
3079
  const prev = this.lastCatAvail;
3017
3080
  this.lastCatAvail = { ...left };
3018
- if (!textEl || !prev) return;
3081
+ const floorId = this.controller.getActiveFloorId();
3082
+ if (floorId !== this.lastAvailFloorId) {
3083
+ this.lastAvailFloorId = floorId;
3084
+ this.availQuietUntil = performance.now() + 2e3;
3085
+ }
3086
+ if (!textEl || !prev || performance.now() < this.availQuietUntil) return;
3019
3087
  for (const cat of categories) {
3020
3088
  const before = prev[cat.key];
3021
3089
  const now = left[cat.key] ?? 0;
3022
3090
  if (before === void 0 || now >= before) continue;
3023
3091
  const taken = before - now;
3024
3092
  textEl.textContent = `${taken} seat${taken === 1 ? "" : "s"} just taken in ${cat.label} \xB7 ${now} left`;
3093
+ this.els.live?.classList.remove("on");
3094
+ void this.els.live?.offsetWidth;
3095
+ this.els.live?.classList.add("on");
3096
+ if (this.liveTimer) clearTimeout(this.liveTimer);
3097
+ this.liveTimer = setTimeout(() => this.els.live?.classList.remove("on"), 8e3);
3025
3098
  return;
3026
3099
  }
3027
3100
  }
@@ -3049,7 +3122,8 @@ var SeatPicker = class _SeatPicker {
3049
3122
  } else if (!seats.length && !heldItems.length) {
3050
3123
  parts.push(`<div class="sl-tray-hint">Tap a seat on the map \u2014 or grab standing tickets below.</div>`);
3051
3124
  }
3052
- if (!this.hold) {
3125
+ const noPicks = !seats.length && !heldItems.length;
3126
+ if (!this.hold && (noPicks || this.bestAvailableBusy || this.bestAvailableConfirm)) {
3053
3127
  const cats = this.controller.doc?.categories ?? [];
3054
3128
  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
3129
  }
@@ -3613,6 +3687,7 @@ var SeatPicker = class _SeatPicker {
3613
3687
  this.closeSeatView();
3614
3688
  this.stopHoldTimer();
3615
3689
  if (this.toastTimer) clearTimeout(this.toastTimer);
3690
+ if (this.liveTimer) clearTimeout(this.liveTimer);
3616
3691
  for (const timer of this.motionTimers) clearTimeout(timer);
3617
3692
  this.motionTimers.clear();
3618
3693
  this.ro?.disconnect();