@seatlayer/js 0.47.0 → 0.47.2

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
@@ -3957,11 +3957,13 @@ __export(index_exports, {
3957
3957
  markerOf: () => markerOf,
3958
3958
  mutationCount: () => mutationCount,
3959
3959
  needsMoveConfirmation: () => needsMoveConfirmation,
3960
+ parseTicketOfferAvailability: () => parseTicketOfferAvailability,
3960
3961
  planAssignment: () => planAssignment,
3961
3962
  retryAfterCopy: () => retryAfterCopy,
3962
3963
  selectionSources: () => selectionSources,
3963
3964
  stateBadge: () => stateBadge,
3964
- suggestMarker: () => suggestMarker
3965
+ suggestMarker: () => suggestMarker,
3966
+ ticketOfferPrices: () => ticketOfferPrices
3965
3967
  });
3966
3968
  module.exports = __toCommonJS(index_exports);
3967
3969
 
@@ -4497,6 +4499,10 @@ var PubApi = class {
4497
4499
  paymentOptions(key) {
4498
4500
  return this.request(`/pub/events/${encodeURIComponent(key)}/payment-options`);
4499
4501
  }
4502
+ /** Server-resolved active ticket offers and category prices. */
4503
+ availability(key, live = false) {
4504
+ return this.request(`/pub/events/${encodeURIComponent(key)}/availability${live ? "?live=1" : ""}`);
4505
+ }
4500
4506
  /**
4501
4507
  * Turn a live hold into an order and start a payment.
4502
4508
  *
@@ -4865,7 +4871,6 @@ var SeatingChart = class {
4865
4871
  this.mount = null;
4866
4872
  this.hostEl = null;
4867
4873
  this.rendered = false;
4868
- this.retryBtn = null;
4869
4874
  this.mode_ = null;
4870
4875
  this.tipEl = null;
4871
4876
  this.tipPos = { x: 0, y: 0 };
@@ -4994,7 +4999,7 @@ var SeatingChart = class {
4994
4999
  this.tipEl.style.display = "none";
4995
5000
  return;
4996
5001
  }
4997
- const money = (() => {
5002
+ const money2 = (() => {
4998
5003
  try {
4999
5004
  return new Intl.NumberFormat(void 0, { style: "currency", currency: details.currency }).format(details.price);
5000
5005
  } catch {
@@ -5002,7 +5007,7 @@ var SeatingChart = class {
5002
5007
  }
5003
5008
  })();
5004
5009
  const statusLine = details.status === "free" ? "" : `<div style="margin-top:5px;font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:#fca5a5;font-weight:700">${details.status === "held" ? (0, import_core.t)("map.statusHeld") : (0, import_core.t)("map.statusTaken")}</div>`;
5005
- this.tipEl.innerHTML = `<div style="font-weight:700;font-size:13px">${details.label}</div><div style="display:flex;align-items:center;gap:6px;margin-top:4px;color:#c7cddc"><span style="width:9px;height:9px;border-radius:50%;flex:none;background:${details.categoryColor}"></span><span>${details.categoryLabel}</span><span style="margin-left:auto;font-weight:700;color:#fff">${money}</span></div>` + statusLine;
5010
+ this.tipEl.innerHTML = `<div style="font-weight:700;font-size:13px">${details.label}</div><div style="display:flex;align-items:center;gap:6px;margin-top:4px;color:#c7cddc"><span style="width:9px;height:9px;border-radius:50%;flex:none;background:${details.categoryColor}"></span><span>${details.categoryLabel}</span><span style="margin-left:auto;font-weight:700;color:#fff">${money2}</span></div>` + statusLine;
5006
5011
  this.tipEl.style.display = "block";
5007
5012
  this.placeTooltip();
5008
5013
  }
@@ -5240,14 +5245,12 @@ var SeatingChart = class {
5240
5245
  void this.render().catch((err) => this.opts.onError?.(err));
5241
5246
  });
5242
5247
  box.appendChild(btn);
5243
- this.retryBtn = btn;
5244
5248
  host.appendChild(box);
5245
5249
  }
5246
5250
  destroy() {
5247
5251
  this.realtime?.stop();
5248
5252
  this.realtime = null;
5249
5253
  this.access?.clear();
5250
- this.retryBtn = null;
5251
5254
  if (this.hostEl && this.onTipMove) this.hostEl.removeEventListener("mousemove", this.onTipMove);
5252
5255
  this.tipEl = null;
5253
5256
  this.onTipMove = null;
@@ -5898,6 +5901,136 @@ var EmbeddedDesigner = class {
5898
5901
  var import_core2 = require("@seatlayer/core");
5899
5902
  var import_panorama = require("@seatlayer/core/view3d/crossfade/panorama");
5900
5903
  var import_panoramaDelivery = require("@seatlayer/core/view/panoramaDelivery");
5904
+
5905
+ // src/offerAvailability.ts
5906
+ var SALE_STATES = [
5907
+ "on-sale",
5908
+ "low",
5909
+ "sold-out",
5910
+ "presale",
5911
+ "closed"
5912
+ ];
5913
+ function money(value) {
5914
+ if (value === null || value === void 0) return null;
5915
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
5916
+ }
5917
+ function timestamp(value) {
5918
+ if (value === null || value === void 0) return null;
5919
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
5920
+ }
5921
+ function parseSummary(value) {
5922
+ if (value == null) return null;
5923
+ if (typeof value !== "object" || Array.isArray(value)) return void 0;
5924
+ const source = value;
5925
+ const count = source.count;
5926
+ const index = source.index;
5927
+ if (typeof index !== "number" || !Number.isInteger(index) || index < 1) return void 0;
5928
+ if (typeof count !== "number" || !Number.isInteger(count) || count < index) return void 0;
5929
+ const remaining = source.remaining;
5930
+ if (remaining != null && (typeof remaining !== "number" || !Number.isInteger(remaining) || remaining < 0)) {
5931
+ return void 0;
5932
+ }
5933
+ const result = {
5934
+ index,
5935
+ count,
5936
+ remaining: remaining == null ? null : remaining
5937
+ };
5938
+ if (source.id !== void 0) {
5939
+ if (typeof source.id !== "string" || !source.id.trim()) return void 0;
5940
+ result.id = source.id.trim();
5941
+ }
5942
+ if (source.name !== void 0) {
5943
+ if (typeof source.name !== "string" || !source.name.trim()) return void 0;
5944
+ result.name = source.name.trim();
5945
+ }
5946
+ if (source.categoryKey !== void 0) {
5947
+ if (source.categoryKey !== null && (typeof source.categoryKey !== "string" || !source.categoryKey.trim())) {
5948
+ return void 0;
5949
+ }
5950
+ result.categoryKey = source.categoryKey == null ? null : source.categoryKey.trim();
5951
+ }
5952
+ for (const key of ["startsAt", "endsAt"]) {
5953
+ if (source[key] === void 0) continue;
5954
+ const parsed = timestamp(source[key]);
5955
+ if (parsed === void 0) return void 0;
5956
+ result[key] = parsed;
5957
+ }
5958
+ return result;
5959
+ }
5960
+ function parseTicketOfferAvailability(body) {
5961
+ if (!body || typeof body !== "object" || Array.isArray(body)) return null;
5962
+ const raw = body;
5963
+ const state = SALE_STATES.find((candidate) => candidate === raw.state);
5964
+ if (!state) return null;
5965
+ const fromPrice = money(raw.fromPrice);
5966
+ const previousPrice = money(raw.previousPrice);
5967
+ if (fromPrice === void 0 || previousPrice === void 0) return null;
5968
+ const currency = raw.currency == null ? null : typeof raw.currency === "string" && raw.currency.trim() ? raw.currency.trim() : void 0;
5969
+ if (currency === void 0) return null;
5970
+ const release = parseSummary(raw.release);
5971
+ if (release === void 0) return null;
5972
+ const upcoming = raw.upcoming === void 0 ? null : parseSummary(raw.upcoming);
5973
+ if (upcoming === void 0) return null;
5974
+ let prices = [];
5975
+ if (raw.prices != null) {
5976
+ if (!Array.isArray(raw.prices)) return null;
5977
+ const parsed = [];
5978
+ for (const entry of raw.prices) {
5979
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
5980
+ const row = entry;
5981
+ const categoryKey = typeof row.categoryKey === "string" ? row.categoryKey.trim() : "";
5982
+ if (!categoryKey) return null;
5983
+ const price = money(row.price);
5984
+ const previous = money(row.previousPrice);
5985
+ if (price === void 0 || price === null || previous === void 0) return null;
5986
+ const item = { categoryKey, price, previousPrice: previous };
5987
+ if (row.offerId !== void 0) {
5988
+ if (typeof row.offerId !== "string" || !row.offerId.trim()) return null;
5989
+ item.offerId = row.offerId.trim();
5990
+ }
5991
+ if (row.offerName !== void 0) {
5992
+ if (typeof row.offerName !== "string" || !row.offerName.trim()) return null;
5993
+ item.offerName = row.offerName.trim();
5994
+ }
5995
+ if (row.remaining !== void 0) {
5996
+ if (row.remaining !== null && (typeof row.remaining !== "number" || !Number.isInteger(row.remaining) || row.remaining < 0)) return null;
5997
+ item.remaining = row.remaining == null ? null : row.remaining;
5998
+ }
5999
+ for (const key of ["startsAt", "endsAt"]) {
6000
+ if (row[key] === void 0) continue;
6001
+ const at = timestamp(row[key]);
6002
+ if (at === void 0) return null;
6003
+ item[key] = at;
6004
+ }
6005
+ parsed.push(item);
6006
+ }
6007
+ prices = parsed;
6008
+ }
6009
+ return { state, fromPrice, previousPrice, currency, release, upcoming, prices };
6010
+ }
6011
+ function nextOfferTransitionAt(availability, now) {
6012
+ if (!availability) return null;
6013
+ let next = null;
6014
+ const consider = (at) => {
6015
+ if (at != null && at > now && (next === null || at < next)) next = at;
6016
+ };
6017
+ for (const summary of [availability.release, availability.upcoming]) {
6018
+ consider(summary?.startsAt);
6019
+ consider(summary?.endsAt);
6020
+ }
6021
+ for (const price of availability.prices) {
6022
+ consider(price.startsAt);
6023
+ consider(price.endsAt);
6024
+ }
6025
+ return next;
6026
+ }
6027
+ function ticketOfferPrices(availability) {
6028
+ const map = {};
6029
+ for (const entry of availability?.prices ?? []) map[entry.categoryKey] = entry.price;
6030
+ return map;
6031
+ }
6032
+
6033
+ // src/SeatPicker.ts
5901
6034
  var import_meta = {};
5902
6035
  var DEFAULT_API_BASE2 = "https://api.seatlayer.io";
5903
6036
  var DEFAULT_MAX_SELECTION2 = 10;
@@ -6119,6 +6252,15 @@ var CSS2 = (
6119
6252
  .sl-cbbtn svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
6120
6253
 
6121
6254
  /* price panel \u2014 one compact filter control replaces the wrapping price-chip row. */
6255
+ .sl-offer{display:none;margin:12px 14px 2px;padding:12px;border:1px solid color-mix(in srgb,var(--sl-accent) 34%,var(--sl-line));
6256
+ border-radius:12px;background:color-mix(in srgb,var(--sl-accent) 8%,var(--sl-surface));color:var(--sl-text)}
6257
+ .sl-offer.has{display:block}.sl-offer-main{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}
6258
+ .sl-offer-copy{min-width:0}.sl-offer-kicker{display:block;font-size:9px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:var(--sl-accent)}
6259
+ .sl-offer-name{display:block;margin-top:2px;font-size:13px;font-weight:800;line-height:1.3}.sl-offer-line{display:block;margin-top:3px;font-size:11px;line-height:1.35;color:var(--sl-muted)}
6260
+ .sl-offer-info{position:relative;flex:none}.sl-offer-info>summary{list-style:none;width:25px;height:25px;border:1px solid var(--sl-line);border-radius:999px;
6261
+ display:grid;place-items:center;cursor:pointer;font-size:12px;font-weight:850;color:var(--sl-text);background:var(--sl-surface)}
6262
+ .sl-offer-info>summary::-webkit-details-marker{display:none}.sl-offer-info[open]>summary{border-color:var(--sl-accent);color:var(--sl-accent)}
6263
+ .sl-offer-detail{margin-top:10px;padding-top:9px;border-top:1px solid var(--sl-line);font-size:10.5px;line-height:1.45;color:var(--sl-muted)}
6122
6264
  .sl-sec{padding:14px 14px 4px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--sl-muted);font-weight:700}
6123
6265
  .sl-prices-sec{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:13px}
6124
6266
  .sl-price-select{min-height:32px;max-width:130px;padding:5px 28px 5px 9px;border:1px solid var(--sl-line);border-radius:9px;
@@ -6129,6 +6271,7 @@ var CSS2 = (
6129
6271
  padding:0 6px;margin:0 -6px;border-radius:8px;cursor:pointer;transition:background .15s}
6130
6272
  .sl-price-row:hover,.sl-price-row:focus-visible{background:color-mix(in srgb,var(--sl-line) 40%,transparent)}
6131
6273
  .sl-price-row.sl-active{background:color-mix(in srgb,var(--sl-accent) 9%,transparent)}
6274
+ .sl-price-was{margin-left:auto;color:var(--sl-muted);font-size:10px;text-decoration:line-through}.sl-price-offer{display:block;color:var(--sl-accent);font-size:9px;font-weight:750}
6132
6275
  .sl-price-row.sl-active .sl-price-label{color:var(--sl-accent)}
6133
6276
  .sl-dot{width:9px;height:9px;border-radius:50%;flex:none}
6134
6277
  .sl-price-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
@@ -6570,7 +6713,21 @@ var CSS2 = (
6570
6713
  /* Venue navigation inside 3D: levels (isolate a floor) and areas (fly to a zone).
6571
6714
  Bottom-LEFT, clear of the module's own chips (Overview bottom-right, 360
6572
6715
  bottom-centre) and of the bottom-sheeted confirm card. Horizontally scrollable
6573
- so a venue with many zones never pushes the rail off a phone screen. */
6716
+ so a venue with many zones never pushes the rail off a phone screen.
6717
+
6718
+ EVERY WIDTH HERE IS RELATIVE TO THE RAIL, NEVER TO THE WINDOW. These three
6719
+ rules used to size off 100vw, which contradicts the one contract this widget
6720
+ states about itself: it adapts to a full-screen takeover, an inline div in a
6721
+ content page, or a popup, and its breakpoints key off the CONTAINER, never the
6722
+ viewport. A 390 px picker embedded in a 1400 px page asked for
6723
+ calc(100vw - 180px) = 1220 inside a 390 px rail.
6724
+
6725
+ MEASURED, IT DID NOT OVERFLOW: the scroll parent is a flex container, so the
6726
+ over-large declaration was shrunk back to the rail and both the old and new
6727
+ rules resolve to 364 px in situ. So this is a latent correctness fix, not a
6728
+ visible bug -- the numbers were wrong and were being covered for by a
6729
+ flex-shrink one level up. Left as 100% because the next person to change that
6730
+ parent's display should not inherit a rule that only works by accident. */
6574
6731
  .sl-view3d-nav{position:absolute;left:12px;bottom:16px;z-index:3;display:flex;flex-direction:column;gap:6px;
6575
6732
  max-width:calc(100% - 150px);pointer-events:none}
6576
6733
  .sl-view3d-nav > div{display:flex;gap:6px;overflow-x:auto;scrollbar-width:none;pointer-events:auto;
@@ -6582,15 +6739,15 @@ var CSS2 = (
6582
6739
  .sl-view3d-nav button:hover,.sl-view3d-nav button:focus-visible{color:#eef1f8;border-color:rgba(190,205,240,.6)}
6583
6740
  .sl-view3d-nav button[aria-pressed="true"]{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
6584
6741
  .sl-view3d-nav button:disabled{opacity:.45;cursor:not-allowed}
6585
- .sl-view3d-nav select{min-height:38px;max-width:min(280px,calc(100vw - 40px));padding:7px 34px 7px 12px;
6742
+ .sl-view3d-nav select{min-height:38px;max-width:100%;padding:7px 34px 7px 12px;
6586
6743
  border-radius:999px;font:700 11.5px/1 inherit;color:#eef1f8;background:rgba(12,18,32,.86);
6587
6744
  border:1px solid rgba(150,165,205,.45);backdrop-filter:blur(6px);cursor:pointer}
6588
6745
  .sl-view3d-nav select:focus-visible{outline:2px solid var(--sl-accent);outline-offset:2px}
6589
6746
  .sl-view3d-nav .sl-view3d-locator{display:grid;grid-template-columns:minmax(150px,1.25fr) minmax(110px,.8fr) minmax(105px,.7fr) auto;
6590
- width:min(720px,calc(100vw - 180px));overflow:visible}
6747
+ width:min(720px,100%);overflow:visible}
6591
6748
  .sl-view3d-nav.is-seat-focused .sl-view3d-locator{display:none}
6592
6749
  .sl-view3d-locator select{width:100%;min-width:0}
6593
- .sl-picker[data-layout="narrow"] .sl-view3d-nav .sl-view3d-locator{display:flex;width:calc(100vw - 24px);overflow-x:auto}
6750
+ .sl-picker[data-layout="narrow"] .sl-view3d-nav .sl-view3d-locator{display:flex;width:100%;overflow-x:auto}
6594
6751
  .sl-picker[data-layout="narrow"] .sl-view3d-locator select{flex:0 0 155px}
6595
6752
  /* On phones the library owns the bottom edge for seat/panorama/overview actions.
6596
6753
  Keep venue navigation in a separate top rail so those control families never
@@ -6840,10 +6997,17 @@ var SeatPicker = class _SeatPicker {
6840
6997
  this.ro = null;
6841
6998
  this.holdTimer = null;
6842
6999
  this.toastTimer = null;
7000
+ this.offerRefreshTimer = null;
7001
+ /** Armed only when the offer schedule has a known future transition (or as a
7002
+ * bounded retry after a failed read) — never a fixed-cadence poll. */
7003
+ this.offerBoundaryTimer = null;
7004
+ this.offerVisibilityHandler = null;
6843
7005
  /** Short-lived UI motion timers; all are cancelled on destroy. */
6844
7006
  this.motionTimers = /* @__PURE__ */ new Set();
6845
7007
  // state
6846
7008
  this.currency = "USD";
7009
+ this.eventTimezone = null;
7010
+ this.offerAvailability = null;
6847
7011
  this.hold = null;
6848
7012
  /** Latest server expiry for the open hold (moves on extend). */
6849
7013
  this.holdExpiresAt = 0;
@@ -6957,6 +7121,7 @@ var SeatPicker = class _SeatPicker {
6957
7121
  if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
6958
7122
  if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
6959
7123
  this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
7124
+ this.hostPricing = options.pricing;
6960
7125
  this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
6961
7126
  this.access = options.transport ? null : createBuyerAccessContext(options, {
6962
7127
  onExpired: (event) => {
@@ -6998,6 +7163,7 @@ var SeatPicker = class _SeatPicker {
6998
7163
  },
6999
7164
  onStatusChange: () => {
7000
7165
  this.syncPrices();
7166
+ this.scheduleOfferRefresh(true);
7001
7167
  this.evictTakenSelections();
7002
7168
  this.detectBooked();
7003
7169
  this.refreshMinimap();
@@ -7419,6 +7585,7 @@ var SeatPicker = class _SeatPicker {
7419
7585
  </div>
7420
7586
  <div class="sl-sec sl-filtersec" data-ref="filtersSec">Filters</div>
7421
7587
  <div class="sl-filters" data-ref="filters"></div>
7588
+ <div class="sl-offer" data-ref="offer" role="status" aria-live="polite"></div>
7422
7589
  <div class="sl-sec sl-prices-sec" data-ref="pricesSec"><span>Ticket prices</span></div>
7423
7590
  <div class="sl-prices" data-ref="prices"></div>
7424
7591
  <div class="sl-live" data-ref="live" role="status" aria-live="polite"><span class="dot" aria-hidden="true"></span><span data-ref="liveText">Live availability \u2014 seats update in real time</span></div>
@@ -7439,6 +7606,13 @@ var SeatPicker = class _SeatPicker {
7439
7606
  this.els[el2.dataset.ref] = el2;
7440
7607
  });
7441
7608
  this.mapHost = this.els.map;
7609
+ void this.refreshOfferAvailability(false);
7610
+ if (this.api.availability) {
7611
+ this.offerVisibilityHandler = () => {
7612
+ if (!document.hidden && !this.destroyed) void this.refreshOfferAvailability(false);
7613
+ };
7614
+ document.addEventListener("visibilitychange", this.offerVisibilityHandler);
7615
+ }
7442
7616
  const applyLayout = () => {
7443
7617
  const w = root.clientWidth;
7444
7618
  if (w <= 0) return;
@@ -7476,10 +7650,21 @@ var SeatPicker = class _SeatPicker {
7476
7650
  e.stopPropagation();
7477
7651
  setSheet(root.dataset.sheet !== "open");
7478
7652
  });
7653
+ this.els.peek?.addEventListener("click", (e) => {
7654
+ const go = e.target.closest(".sl-sheet-go");
7655
+ if (!go) return;
7656
+ e.stopPropagation();
7657
+ if (go.dataset.act === "checkout") void this.handleCta();
7658
+ else setSheet(true);
7659
+ });
7479
7660
  let startY = 0;
7480
7661
  let swiped = false;
7481
7662
  let tracking = false;
7482
7663
  head.addEventListener("pointerdown", (e) => {
7664
+ if (e.target.closest?.(".sl-seccard,.sl-sheet-toggle,.sl-sheet-go")) {
7665
+ tracking = false;
7666
+ return;
7667
+ }
7483
7668
  tracking = true;
7484
7669
  swiped = false;
7485
7670
  startY = e.clientY;
@@ -7498,7 +7683,7 @@ var SeatPicker = class _SeatPicker {
7498
7683
  });
7499
7684
  head.addEventListener("pointerup", (e) => {
7500
7685
  if (tracking && !swiped && Math.abs(e.clientY - startY) < 6) {
7501
- if (!e.target.closest(".sl-seccard,.sl-sheet-toggle")) setSheet(root.dataset.sheet !== "open");
7686
+ setSheet(root.dataset.sheet !== "open");
7502
7687
  }
7503
7688
  tracking = false;
7504
7689
  head.releasePointerCapture?.(e.pointerId);
@@ -7547,6 +7732,7 @@ var SeatPicker = class _SeatPicker {
7547
7732
  const chartTheme = this.controller.doc?.theme;
7548
7733
  Object.entries(resolveTokens(chartTheme, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
7549
7734
  this.currency = info.currency ?? this.opts.currency ?? "USD";
7735
+ this.eventTimezone = info.timezone ?? null;
7550
7736
  const logoUrl = this.opts.theme?.logoUrl ?? chartTheme?.logoUrl;
7551
7737
  if (logoUrl) this.els.logo.innerHTML = `<img src="${logoUrl}" alt="">`;
7552
7738
  else this.els.logo.textContent = (this.opts.theme?.brandName ?? chartTheme?.brandName ?? info.eventName ?? "?").slice(0, 1).toUpperCase();
@@ -7692,12 +7878,11 @@ var SeatPicker = class _SeatPicker {
7692
7878
  const narrow = this.root?.dataset.layout === "narrow";
7693
7879
  const filters = this.els.filters;
7694
7880
  if (filters) {
7881
+ if (this.cbEl) this.els.zoom?.appendChild(this.cbEl);
7695
7882
  if (narrow) {
7696
7883
  if (this.a11yChipsEl) filters.appendChild(this.a11yChipsEl);
7697
- if (this.cbEl) filters.appendChild(this.cbEl);
7698
7884
  } else {
7699
7885
  if (this.a11yChipsEl) this.regions["top-left"]?.appendChild(this.a11yChipsEl);
7700
- if (this.cbEl) this.els.zoom?.appendChild(this.cbEl);
7701
7886
  }
7702
7887
  const has = narrow && filters.children.length > 0;
7703
7888
  filters.classList.toggle("has", has);
@@ -8888,6 +9073,119 @@ var SeatPicker = class _SeatPicker {
8888
9073
  return `${n} ${this.currency}`;
8889
9074
  }
8890
9075
  }
9076
+ /**
9077
+ * Sleep until the offer schedule's next known transition, then re-read.
9078
+ *
9079
+ * A far-away boundary is capped: the wake re-reads, learns the (unchanged)
9080
+ * schedule, and re-arms — so a picker left open for days still tracks an
9081
+ * organizer's schedule edits at a cost of one request every few hours. No
9082
+ * future transition means no timer at all; an event with no releases does
9083
+ * zero background traffic. A wake in a hidden tab fetches nothing — the
9084
+ * visibilitychange handler owns catching that tab up.
9085
+ */
9086
+ scheduleOfferBoundary(availability) {
9087
+ if (this.offerBoundaryTimer) clearTimeout(this.offerBoundaryTimer);
9088
+ this.offerBoundaryTimer = null;
9089
+ if (!this.api.availability || this.destroyed) return;
9090
+ const now = Date.now();
9091
+ const boundary = nextOfferTransitionAt(availability, now);
9092
+ if (boundary == null) return;
9093
+ const MAX_SLEEP_MS = 6 * 36e5;
9094
+ const delay = Math.min(Math.max(boundary - now + 1e3, 1e3), MAX_SLEEP_MS);
9095
+ this.offerBoundaryTimer = setTimeout(() => {
9096
+ this.offerBoundaryTimer = null;
9097
+ if (document.hidden) return;
9098
+ void this.refreshOfferAvailability(false);
9099
+ }, delay);
9100
+ }
9101
+ /** Debounce the no-store offer read behind a burst of seat-status frames. */
9102
+ scheduleOfferRefresh(live) {
9103
+ if (!this.api.availability || this.destroyed) return;
9104
+ if (this.offerRefreshTimer) clearTimeout(this.offerRefreshTimer);
9105
+ this.offerRefreshTimer = setTimeout(() => {
9106
+ this.offerRefreshTimer = null;
9107
+ void this.refreshOfferAvailability(live);
9108
+ }, live ? 180 : 0);
9109
+ }
9110
+ /**
9111
+ * Pull the server's resolved answer. A failed refresh keeps the last truthful
9112
+ * answer: flashing back to a chart price while checkout still charges an
9113
+ * offer is worse than a temporarily stale remaining count.
9114
+ */
9115
+ async refreshOfferAvailability(live) {
9116
+ if (!this.api.availability || this.destroyed) return;
9117
+ try {
9118
+ const body = await this.api.availability(this.opts.event, live);
9119
+ if (this.destroyed) return;
9120
+ const availability = parseTicketOfferAvailability(body);
9121
+ if (!availability) return;
9122
+ this.offerAvailability = availability;
9123
+ this.scheduleOfferBoundary(availability);
9124
+ const server = ticketOfferPrices(availability);
9125
+ const merged = { ...this.hostPricing?.prices ?? {}, ...server };
9126
+ const pricing = Object.keys(merged).length > 0 || this.hostPricing?.formatter ? { prices: merged, ...this.hostPricing?.formatter ? { formatter: this.hostPricing.formatter } : {} } : void 0;
9127
+ this.setPricing(pricing);
9128
+ this.syncOffer();
9129
+ this.opts.onOfferAvailabilityChange?.(availability);
9130
+ } catch {
9131
+ if (!this.destroyed && !this.offerBoundaryTimer && !document.hidden) {
9132
+ this.offerBoundaryTimer = setTimeout(() => {
9133
+ this.offerBoundaryTimer = null;
9134
+ if (document.hidden) return;
9135
+ void this.refreshOfferAvailability(false);
9136
+ }, 3e4);
9137
+ }
9138
+ }
9139
+ }
9140
+ offerPrice(categoryKey) {
9141
+ if (!categoryKey) return null;
9142
+ return this.offerAvailability?.prices.find((entry) => entry.categoryKey === categoryKey) ?? null;
9143
+ }
9144
+ /** The compact current/upcoming offer card above Ticket prices. */
9145
+ syncOffer() {
9146
+ const host = this.els.offer;
9147
+ if (!host) return;
9148
+ const availability = this.offerAvailability;
9149
+ const active = availability?.release ?? null;
9150
+ const upcoming = !active ? availability?.upcoming ?? null : null;
9151
+ if (!availability || availability.state === "closed" || availability.state === "sold-out" || !active && !upcoming) {
9152
+ host.classList.remove("has");
9153
+ host.replaceChildren();
9154
+ return;
9155
+ }
9156
+ const offer = active ?? upcoming;
9157
+ const main = document.createElement("div");
9158
+ main.className = "sl-offer-main";
9159
+ const copy = document.createElement("div");
9160
+ copy.className = "sl-offer-copy";
9161
+ const kicker = document.createElement("span");
9162
+ kicker.className = "sl-offer-kicker";
9163
+ kicker.textContent = active ? "Current ticket offer" : "Upcoming ticket offer";
9164
+ const name = document.createElement("strong");
9165
+ name.className = "sl-offer-name";
9166
+ name.textContent = offer.name || (active ? "Current offer" : "Scheduled offer");
9167
+ const line = document.createElement("span");
9168
+ line.className = "sl-offer-line";
9169
+ const facts = [];
9170
+ if (active && availability.fromPrice != null) facts.push(this.money(availability.fromPrice / 100));
9171
+ if (active && offer.remaining != null) facts.push(`${offer.remaining} available`);
9172
+ if (active && offer.endsAt != null) facts.push(`until ${formatWhen(offer.endsAt, this.eventTimezone, this.opts.locale)}`);
9173
+ if (upcoming?.startsAt != null) facts.push(`starts ${formatWhen(upcoming.startsAt, this.eventTimezone, this.opts.locale)}`);
9174
+ line.textContent = facts.join(" \xB7 ");
9175
+ copy.append(kicker, name, line);
9176
+ const info = document.createElement("details");
9177
+ info.className = "sl-offer-info";
9178
+ const summary = document.createElement("summary");
9179
+ summary.setAttribute("aria-label", `How the ${name.textContent} offer works`);
9180
+ summary.textContent = "i";
9181
+ const detail = document.createElement("div");
9182
+ detail.className = "sl-offer-detail";
9183
+ detail.textContent = active ? `This price applies automatically to eligible seats. Tickets in active carts temporarily reduce the available quantity; released or expired holds return it. When the offer ends, the next matching offer or normal ticket price takes over.` : `Tickets are available at their normal price now. This scheduled offer will apply automatically to eligible seats when it starts.`;
9184
+ info.append(summary, detail);
9185
+ main.append(copy, info);
9186
+ host.replaceChildren(main);
9187
+ host.classList.add("has");
9188
+ }
8891
9189
  /**
8892
9190
  * The price the buyer will actually pay for a category (+tier): the host's
8893
9191
  * `pricing` override when present, else the chart's stored price. Every
@@ -8914,9 +9212,11 @@ var SeatPicker = class _SeatPicker {
8914
9212
  this.els.prices.classList.toggle("sl-expanded", overflow > 1 && this.pricesExpanded);
8915
9213
  this.els.prices.innerHTML = shown.map((c) => {
8916
9214
  const price = this.catPrice(c);
9215
+ const offer = this.offerPrice(c.key);
9216
+ const previous = offer?.previousPrice != null && offer.previousPrice > offer.price ? offer.previousPrice : null;
8917
9217
  const active = this.focusedCatKey === c.key;
8918
9218
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
8919
- 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>`;
9219
+ return `<div class="sl-price-row${dim ? " sl-dim" : ""}${active ? " sl-active" : ""}" data-cat="${escapeOption(c.key)}" role="button" tabindex="0" aria-pressed="${active}" title="${escapeOption(active ? "Show all seats" : `Show ${c.label} seats on the map`)}"><span class="sl-dot" style="background:${escapeOption(c.color)}"></span><span class="sl-price-label">${escapeOption(c.label)}` + (offer?.offerName ? `<small class="sl-price-offer">${escapeOption(offer.offerName)}</small>` : "") + `</span><span class="sl-price-left">${left[c.key] ?? 0} left</span>` + (previous != null ? `<span class="sl-price-was">${escapeOption(this.money(previous))}</span>` : "") + (price != null ? `<span class="sl-price-amt">${this.money(price)}</span>` : "") + `</div>`;
8920
9220
  }).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>`;
8921
9221
  this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
8922
9222
  row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
@@ -9228,10 +9528,11 @@ var SeatPicker = class _SeatPicker {
9228
9528
  if (previousCount === 0 && count > 0) this.animateOnce(this.els.cta, "sl-ready", 520);
9229
9529
  if (this.els.peek) {
9230
9530
  if (count) {
9231
- this.els.peek.innerHTML = `<span>${count} ${count === 1 ? "ticket" : "tickets"} \xB7 ${this.money(total)}</span><span class="go">${this.hold ? pendingCount ? "Secure more" : "Continue" : "Review"}</span>`;
9531
+ const holding = !!this.hold && !pendingCount;
9532
+ this.els.peek.innerHTML = `<span>${count} ${count === 1 ? "ticket" : "tickets"} \xB7 ${this.money(total)}</span><button type="button" class="go sl-sheet-go" data-act="${holding ? "checkout" : "open"}">${this.hold ? pendingCount ? "Secure more" : "Continue" : "Review"}</button>`;
9232
9533
  } else {
9233
9534
  const prices = (this.controller.doc?.categories ?? []).map((c) => this.catPrice(c)).filter((p) => p != null);
9234
- this.els.peek.innerHTML = (prices.length ? `<span>From ${this.money(Math.min(...prices))}</span>` : "<span>Pick your seats</span>") + `<span class="go">\u2726 Best seats</span>`;
9535
+ this.els.peek.innerHTML = (prices.length ? `<span>From ${this.money(Math.min(...prices))}</span>` : "<span>Pick your seats</span>") + `<button type="button" class="go sl-sheet-go" data-act="open">\u2726 Best seats</button>`;
9235
9536
  }
9236
9537
  }
9237
9538
  this.lastTrayCount = count;
@@ -9571,6 +9872,7 @@ var SeatPicker = class _SeatPicker {
9571
9872
  }
9572
9873
  emitHoldChange() {
9573
9874
  const hold = this.hold;
9875
+ this.scheduleOfferRefresh(true);
9574
9876
  this.opts.onHoldChange?.(
9575
9877
  hold,
9576
9878
  hold?.seats ?? [],
@@ -10300,6 +10602,7 @@ var SeatPicker = class _SeatPicker {
10300
10602
  flashOnLiveChange: true,
10301
10603
  onStatusChange: () => {
10302
10604
  this.syncPrices();
10605
+ this.scheduleOfferRefresh(true);
10303
10606
  this.detectBooked();
10304
10607
  this.refreshMinimap();
10305
10608
  this.pushAvailabilityTo3d();
@@ -10435,6 +10738,14 @@ var SeatPicker = class _SeatPicker {
10435
10738
  this.stopHoldTimer();
10436
10739
  if (this.toastTimer) clearTimeout(this.toastTimer);
10437
10740
  if (this.liveTimer) clearTimeout(this.liveTimer);
10741
+ if (this.offerRefreshTimer) clearTimeout(this.offerRefreshTimer);
10742
+ if (this.offerBoundaryTimer) clearTimeout(this.offerBoundaryTimer);
10743
+ this.offerRefreshTimer = null;
10744
+ this.offerBoundaryTimer = null;
10745
+ if (this.offerVisibilityHandler) {
10746
+ document.removeEventListener("visibilitychange", this.offerVisibilityHandler);
10747
+ this.offerVisibilityHandler = null;
10748
+ }
10438
10749
  for (const timer of this.motionTimers) clearTimeout(timer);
10439
10750
  this.motionTimers.clear();
10440
10751
  this.ro?.disconnect();
@@ -13109,10 +13420,12 @@ init_manageApi();
13109
13420
  markerOf,
13110
13421
  mutationCount,
13111
13422
  needsMoveConfirmation,
13423
+ parseTicketOfferAvailability,
13112
13424
  planAssignment,
13113
13425
  retryAfterCopy,
13114
13426
  selectionSources,
13115
13427
  stateBadge,
13116
- suggestMarker
13428
+ suggestMarker,
13429
+ ticketOfferPrices
13117
13430
  });
13118
13431
  //# sourceMappingURL=index.cjs.map