@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.js CHANGED
@@ -577,6 +577,10 @@ var PubApi = class {
577
577
  paymentOptions(key) {
578
578
  return this.request(`/pub/events/${encodeURIComponent(key)}/payment-options`);
579
579
  }
580
+ /** Server-resolved active ticket offers and category prices. */
581
+ availability(key, live = false) {
582
+ return this.request(`/pub/events/${encodeURIComponent(key)}/availability${live ? "?live=1" : ""}`);
583
+ }
580
584
  /**
581
585
  * Turn a live hold into an order and start a payment.
582
586
  *
@@ -945,7 +949,6 @@ var SeatingChart = class {
945
949
  this.mount = null;
946
950
  this.hostEl = null;
947
951
  this.rendered = false;
948
- this.retryBtn = null;
949
952
  this.mode_ = null;
950
953
  this.tipEl = null;
951
954
  this.tipPos = { x: 0, y: 0 };
@@ -1074,7 +1077,7 @@ var SeatingChart = class {
1074
1077
  this.tipEl.style.display = "none";
1075
1078
  return;
1076
1079
  }
1077
- const money = (() => {
1080
+ const money2 = (() => {
1078
1081
  try {
1079
1082
  return new Intl.NumberFormat(void 0, { style: "currency", currency: details.currency }).format(details.price);
1080
1083
  } catch {
@@ -1082,7 +1085,7 @@ var SeatingChart = class {
1082
1085
  }
1083
1086
  })();
1084
1087
  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" ? t("map.statusHeld") : t("map.statusTaken")}</div>`;
1085
- 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;
1088
+ 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;
1086
1089
  this.tipEl.style.display = "block";
1087
1090
  this.placeTooltip();
1088
1091
  }
@@ -1320,14 +1323,12 @@ var SeatingChart = class {
1320
1323
  void this.render().catch((err) => this.opts.onError?.(err));
1321
1324
  });
1322
1325
  box.appendChild(btn);
1323
- this.retryBtn = btn;
1324
1326
  host.appendChild(box);
1325
1327
  }
1326
1328
  destroy() {
1327
1329
  this.realtime?.stop();
1328
1330
  this.realtime = null;
1329
1331
  this.access?.clear();
1330
- this.retryBtn = null;
1331
1332
  if (this.hostEl && this.onTipMove) this.hostEl.removeEventListener("mousemove", this.onTipMove);
1332
1333
  this.tipEl = null;
1333
1334
  this.onTipMove = null;
@@ -1992,6 +1993,136 @@ import {
1992
1993
  planPanoramaDelivery,
1993
1994
  schedulePanoramaUpgrade
1994
1995
  } from "@seatlayer/core/view/panoramaDelivery";
1996
+
1997
+ // src/offerAvailability.ts
1998
+ var SALE_STATES = [
1999
+ "on-sale",
2000
+ "low",
2001
+ "sold-out",
2002
+ "presale",
2003
+ "closed"
2004
+ ];
2005
+ function money(value) {
2006
+ if (value === null || value === void 0) return null;
2007
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
2008
+ }
2009
+ function timestamp(value) {
2010
+ if (value === null || value === void 0) return null;
2011
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
2012
+ }
2013
+ function parseSummary(value) {
2014
+ if (value == null) return null;
2015
+ if (typeof value !== "object" || Array.isArray(value)) return void 0;
2016
+ const source = value;
2017
+ const count = source.count;
2018
+ const index = source.index;
2019
+ if (typeof index !== "number" || !Number.isInteger(index) || index < 1) return void 0;
2020
+ if (typeof count !== "number" || !Number.isInteger(count) || count < index) return void 0;
2021
+ const remaining = source.remaining;
2022
+ if (remaining != null && (typeof remaining !== "number" || !Number.isInteger(remaining) || remaining < 0)) {
2023
+ return void 0;
2024
+ }
2025
+ const result = {
2026
+ index,
2027
+ count,
2028
+ remaining: remaining == null ? null : remaining
2029
+ };
2030
+ if (source.id !== void 0) {
2031
+ if (typeof source.id !== "string" || !source.id.trim()) return void 0;
2032
+ result.id = source.id.trim();
2033
+ }
2034
+ if (source.name !== void 0) {
2035
+ if (typeof source.name !== "string" || !source.name.trim()) return void 0;
2036
+ result.name = source.name.trim();
2037
+ }
2038
+ if (source.categoryKey !== void 0) {
2039
+ if (source.categoryKey !== null && (typeof source.categoryKey !== "string" || !source.categoryKey.trim())) {
2040
+ return void 0;
2041
+ }
2042
+ result.categoryKey = source.categoryKey == null ? null : source.categoryKey.trim();
2043
+ }
2044
+ for (const key of ["startsAt", "endsAt"]) {
2045
+ if (source[key] === void 0) continue;
2046
+ const parsed = timestamp(source[key]);
2047
+ if (parsed === void 0) return void 0;
2048
+ result[key] = parsed;
2049
+ }
2050
+ return result;
2051
+ }
2052
+ function parseTicketOfferAvailability(body) {
2053
+ if (!body || typeof body !== "object" || Array.isArray(body)) return null;
2054
+ const raw = body;
2055
+ const state = SALE_STATES.find((candidate) => candidate === raw.state);
2056
+ if (!state) return null;
2057
+ const fromPrice = money(raw.fromPrice);
2058
+ const previousPrice = money(raw.previousPrice);
2059
+ if (fromPrice === void 0 || previousPrice === void 0) return null;
2060
+ const currency = raw.currency == null ? null : typeof raw.currency === "string" && raw.currency.trim() ? raw.currency.trim() : void 0;
2061
+ if (currency === void 0) return null;
2062
+ const release = parseSummary(raw.release);
2063
+ if (release === void 0) return null;
2064
+ const upcoming = raw.upcoming === void 0 ? null : parseSummary(raw.upcoming);
2065
+ if (upcoming === void 0) return null;
2066
+ let prices = [];
2067
+ if (raw.prices != null) {
2068
+ if (!Array.isArray(raw.prices)) return null;
2069
+ const parsed = [];
2070
+ for (const entry of raw.prices) {
2071
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
2072
+ const row = entry;
2073
+ const categoryKey = typeof row.categoryKey === "string" ? row.categoryKey.trim() : "";
2074
+ if (!categoryKey) return null;
2075
+ const price = money(row.price);
2076
+ const previous = money(row.previousPrice);
2077
+ if (price === void 0 || price === null || previous === void 0) return null;
2078
+ const item = { categoryKey, price, previousPrice: previous };
2079
+ if (row.offerId !== void 0) {
2080
+ if (typeof row.offerId !== "string" || !row.offerId.trim()) return null;
2081
+ item.offerId = row.offerId.trim();
2082
+ }
2083
+ if (row.offerName !== void 0) {
2084
+ if (typeof row.offerName !== "string" || !row.offerName.trim()) return null;
2085
+ item.offerName = row.offerName.trim();
2086
+ }
2087
+ if (row.remaining !== void 0) {
2088
+ if (row.remaining !== null && (typeof row.remaining !== "number" || !Number.isInteger(row.remaining) || row.remaining < 0)) return null;
2089
+ item.remaining = row.remaining == null ? null : row.remaining;
2090
+ }
2091
+ for (const key of ["startsAt", "endsAt"]) {
2092
+ if (row[key] === void 0) continue;
2093
+ const at = timestamp(row[key]);
2094
+ if (at === void 0) return null;
2095
+ item[key] = at;
2096
+ }
2097
+ parsed.push(item);
2098
+ }
2099
+ prices = parsed;
2100
+ }
2101
+ return { state, fromPrice, previousPrice, currency, release, upcoming, prices };
2102
+ }
2103
+ function nextOfferTransitionAt(availability, now) {
2104
+ if (!availability) return null;
2105
+ let next = null;
2106
+ const consider = (at) => {
2107
+ if (at != null && at > now && (next === null || at < next)) next = at;
2108
+ };
2109
+ for (const summary of [availability.release, availability.upcoming]) {
2110
+ consider(summary?.startsAt);
2111
+ consider(summary?.endsAt);
2112
+ }
2113
+ for (const price of availability.prices) {
2114
+ consider(price.startsAt);
2115
+ consider(price.endsAt);
2116
+ }
2117
+ return next;
2118
+ }
2119
+ function ticketOfferPrices(availability) {
2120
+ const map = {};
2121
+ for (const entry of availability?.prices ?? []) map[entry.categoryKey] = entry.price;
2122
+ return map;
2123
+ }
2124
+
2125
+ // src/SeatPicker.ts
1995
2126
  var DEFAULT_API_BASE2 = "https://api.seatlayer.io";
1996
2127
  var DEFAULT_MAX_SELECTION2 = 10;
1997
2128
  var EXTEND_PROMPT_MS = 6e4;
@@ -2212,6 +2343,15 @@ var CSS = (
2212
2343
  .sl-cbbtn svg{width:14px;height:14px;stroke:currentColor;stroke-width:2;fill:none;stroke-linecap:round;stroke-linejoin:round}
2213
2344
 
2214
2345
  /* price panel \u2014 one compact filter control replaces the wrapping price-chip row. */
2346
+ .sl-offer{display:none;margin:12px 14px 2px;padding:12px;border:1px solid color-mix(in srgb,var(--sl-accent) 34%,var(--sl-line));
2347
+ border-radius:12px;background:color-mix(in srgb,var(--sl-accent) 8%,var(--sl-surface));color:var(--sl-text)}
2348
+ .sl-offer.has{display:block}.sl-offer-main{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}
2349
+ .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)}
2350
+ .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)}
2351
+ .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;
2352
+ display:grid;place-items:center;cursor:pointer;font-size:12px;font-weight:850;color:var(--sl-text);background:var(--sl-surface)}
2353
+ .sl-offer-info>summary::-webkit-details-marker{display:none}.sl-offer-info[open]>summary{border-color:var(--sl-accent);color:var(--sl-accent)}
2354
+ .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)}
2215
2355
  .sl-sec{padding:14px 14px 4px;font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--sl-muted);font-weight:700}
2216
2356
  .sl-prices-sec{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-top:13px}
2217
2357
  .sl-price-select{min-height:32px;max-width:130px;padding:5px 28px 5px 9px;border:1px solid var(--sl-line);border-radius:9px;
@@ -2222,6 +2362,7 @@ var CSS = (
2222
2362
  padding:0 6px;margin:0 -6px;border-radius:8px;cursor:pointer;transition:background .15s}
2223
2363
  .sl-price-row:hover,.sl-price-row:focus-visible{background:color-mix(in srgb,var(--sl-line) 40%,transparent)}
2224
2364
  .sl-price-row.sl-active{background:color-mix(in srgb,var(--sl-accent) 9%,transparent)}
2365
+ .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}
2225
2366
  .sl-price-row.sl-active .sl-price-label{color:var(--sl-accent)}
2226
2367
  .sl-dot{width:9px;height:9px;border-radius:50%;flex:none}
2227
2368
  .sl-price-label{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}
@@ -2663,7 +2804,21 @@ var CSS = (
2663
2804
  /* Venue navigation inside 3D: levels (isolate a floor) and areas (fly to a zone).
2664
2805
  Bottom-LEFT, clear of the module's own chips (Overview bottom-right, 360
2665
2806
  bottom-centre) and of the bottom-sheeted confirm card. Horizontally scrollable
2666
- so a venue with many zones never pushes the rail off a phone screen. */
2807
+ so a venue with many zones never pushes the rail off a phone screen.
2808
+
2809
+ EVERY WIDTH HERE IS RELATIVE TO THE RAIL, NEVER TO THE WINDOW. These three
2810
+ rules used to size off 100vw, which contradicts the one contract this widget
2811
+ states about itself: it adapts to a full-screen takeover, an inline div in a
2812
+ content page, or a popup, and its breakpoints key off the CONTAINER, never the
2813
+ viewport. A 390 px picker embedded in a 1400 px page asked for
2814
+ calc(100vw - 180px) = 1220 inside a 390 px rail.
2815
+
2816
+ MEASURED, IT DID NOT OVERFLOW: the scroll parent is a flex container, so the
2817
+ over-large declaration was shrunk back to the rail and both the old and new
2818
+ rules resolve to 364 px in situ. So this is a latent correctness fix, not a
2819
+ visible bug -- the numbers were wrong and were being covered for by a
2820
+ flex-shrink one level up. Left as 100% because the next person to change that
2821
+ parent's display should not inherit a rule that only works by accident. */
2667
2822
  .sl-view3d-nav{position:absolute;left:12px;bottom:16px;z-index:3;display:flex;flex-direction:column;gap:6px;
2668
2823
  max-width:calc(100% - 150px);pointer-events:none}
2669
2824
  .sl-view3d-nav > div{display:flex;gap:6px;overflow-x:auto;scrollbar-width:none;pointer-events:auto;
@@ -2675,15 +2830,15 @@ var CSS = (
2675
2830
  .sl-view3d-nav button:hover,.sl-view3d-nav button:focus-visible{color:#eef1f8;border-color:rgba(190,205,240,.6)}
2676
2831
  .sl-view3d-nav button[aria-pressed="true"]{background:var(--sl-accent);color:var(--sl-accent-ink);border-color:transparent}
2677
2832
  .sl-view3d-nav button:disabled{opacity:.45;cursor:not-allowed}
2678
- .sl-view3d-nav select{min-height:38px;max-width:min(280px,calc(100vw - 40px));padding:7px 34px 7px 12px;
2833
+ .sl-view3d-nav select{min-height:38px;max-width:100%;padding:7px 34px 7px 12px;
2679
2834
  border-radius:999px;font:700 11.5px/1 inherit;color:#eef1f8;background:rgba(12,18,32,.86);
2680
2835
  border:1px solid rgba(150,165,205,.45);backdrop-filter:blur(6px);cursor:pointer}
2681
2836
  .sl-view3d-nav select:focus-visible{outline:2px solid var(--sl-accent);outline-offset:2px}
2682
2837
  .sl-view3d-nav .sl-view3d-locator{display:grid;grid-template-columns:minmax(150px,1.25fr) minmax(110px,.8fr) minmax(105px,.7fr) auto;
2683
- width:min(720px,calc(100vw - 180px));overflow:visible}
2838
+ width:min(720px,100%);overflow:visible}
2684
2839
  .sl-view3d-nav.is-seat-focused .sl-view3d-locator{display:none}
2685
2840
  .sl-view3d-locator select{width:100%;min-width:0}
2686
- .sl-picker[data-layout="narrow"] .sl-view3d-nav .sl-view3d-locator{display:flex;width:calc(100vw - 24px);overflow-x:auto}
2841
+ .sl-picker[data-layout="narrow"] .sl-view3d-nav .sl-view3d-locator{display:flex;width:100%;overflow-x:auto}
2687
2842
  .sl-picker[data-layout="narrow"] .sl-view3d-locator select{flex:0 0 155px}
2688
2843
  /* On phones the library owns the bottom edge for seat/panorama/overview actions.
2689
2844
  Keep venue navigation in a separate top rail so those control families never
@@ -2933,10 +3088,17 @@ var SeatPicker = class _SeatPicker {
2933
3088
  this.ro = null;
2934
3089
  this.holdTimer = null;
2935
3090
  this.toastTimer = null;
3091
+ this.offerRefreshTimer = null;
3092
+ /** Armed only when the offer schedule has a known future transition (or as a
3093
+ * bounded retry after a failed read) — never a fixed-cadence poll. */
3094
+ this.offerBoundaryTimer = null;
3095
+ this.offerVisibilityHandler = null;
2936
3096
  /** Short-lived UI motion timers; all are cancelled on destroy. */
2937
3097
  this.motionTimers = /* @__PURE__ */ new Set();
2938
3098
  // state
2939
3099
  this.currency = "USD";
3100
+ this.eventTimezone = null;
3101
+ this.offerAvailability = null;
2940
3102
  this.hold = null;
2941
3103
  /** Latest server expiry for the open hold (moves on extend). */
2942
3104
  this.holdExpiresAt = 0;
@@ -3050,6 +3212,7 @@ var SeatPicker = class _SeatPicker {
3050
3212
  if (!options.event || typeof options.event !== "string") throw new Error("seatmap: `event` key is required");
3051
3213
  if (!options.container) throw new Error("seatmap: `container` is required (or use SeatPicker.open())");
3052
3214
  this.opts = { ...options, confirmSelection: options.confirmSelection ?? true };
3215
+ this.hostPricing = options.pricing;
3053
3216
  this.apiBase = (options.apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
3054
3217
  this.access = options.transport ? null : createBuyerAccessContext(options, {
3055
3218
  onExpired: (event) => {
@@ -3091,6 +3254,7 @@ var SeatPicker = class _SeatPicker {
3091
3254
  },
3092
3255
  onStatusChange: () => {
3093
3256
  this.syncPrices();
3257
+ this.scheduleOfferRefresh(true);
3094
3258
  this.evictTakenSelections();
3095
3259
  this.detectBooked();
3096
3260
  this.refreshMinimap();
@@ -3512,6 +3676,7 @@ var SeatPicker = class _SeatPicker {
3512
3676
  </div>
3513
3677
  <div class="sl-sec sl-filtersec" data-ref="filtersSec">Filters</div>
3514
3678
  <div class="sl-filters" data-ref="filters"></div>
3679
+ <div class="sl-offer" data-ref="offer" role="status" aria-live="polite"></div>
3515
3680
  <div class="sl-sec sl-prices-sec" data-ref="pricesSec"><span>Ticket prices</span></div>
3516
3681
  <div class="sl-prices" data-ref="prices"></div>
3517
3682
  <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>
@@ -3532,6 +3697,13 @@ var SeatPicker = class _SeatPicker {
3532
3697
  this.els[el.dataset.ref] = el;
3533
3698
  });
3534
3699
  this.mapHost = this.els.map;
3700
+ void this.refreshOfferAvailability(false);
3701
+ if (this.api.availability) {
3702
+ this.offerVisibilityHandler = () => {
3703
+ if (!document.hidden && !this.destroyed) void this.refreshOfferAvailability(false);
3704
+ };
3705
+ document.addEventListener("visibilitychange", this.offerVisibilityHandler);
3706
+ }
3535
3707
  const applyLayout = () => {
3536
3708
  const w = root.clientWidth;
3537
3709
  if (w <= 0) return;
@@ -3569,10 +3741,21 @@ var SeatPicker = class _SeatPicker {
3569
3741
  e.stopPropagation();
3570
3742
  setSheet(root.dataset.sheet !== "open");
3571
3743
  });
3744
+ this.els.peek?.addEventListener("click", (e) => {
3745
+ const go = e.target.closest(".sl-sheet-go");
3746
+ if (!go) return;
3747
+ e.stopPropagation();
3748
+ if (go.dataset.act === "checkout") void this.handleCta();
3749
+ else setSheet(true);
3750
+ });
3572
3751
  let startY = 0;
3573
3752
  let swiped = false;
3574
3753
  let tracking = false;
3575
3754
  head.addEventListener("pointerdown", (e) => {
3755
+ if (e.target.closest?.(".sl-seccard,.sl-sheet-toggle,.sl-sheet-go")) {
3756
+ tracking = false;
3757
+ return;
3758
+ }
3576
3759
  tracking = true;
3577
3760
  swiped = false;
3578
3761
  startY = e.clientY;
@@ -3591,7 +3774,7 @@ var SeatPicker = class _SeatPicker {
3591
3774
  });
3592
3775
  head.addEventListener("pointerup", (e) => {
3593
3776
  if (tracking && !swiped && Math.abs(e.clientY - startY) < 6) {
3594
- if (!e.target.closest(".sl-seccard,.sl-sheet-toggle")) setSheet(root.dataset.sheet !== "open");
3777
+ setSheet(root.dataset.sheet !== "open");
3595
3778
  }
3596
3779
  tracking = false;
3597
3780
  head.releasePointerCapture?.(e.pointerId);
@@ -3640,6 +3823,7 @@ var SeatPicker = class _SeatPicker {
3640
3823
  const chartTheme = this.controller.doc?.theme;
3641
3824
  Object.entries(resolveTokens(chartTheme, this.opts.theme)).forEach(([k, v]) => root.style.setProperty(k, v));
3642
3825
  this.currency = info.currency ?? this.opts.currency ?? "USD";
3826
+ this.eventTimezone = info.timezone ?? null;
3643
3827
  const logoUrl = this.opts.theme?.logoUrl ?? chartTheme?.logoUrl;
3644
3828
  if (logoUrl) this.els.logo.innerHTML = `<img src="${logoUrl}" alt="">`;
3645
3829
  else this.els.logo.textContent = (this.opts.theme?.brandName ?? chartTheme?.brandName ?? info.eventName ?? "?").slice(0, 1).toUpperCase();
@@ -3785,12 +3969,11 @@ var SeatPicker = class _SeatPicker {
3785
3969
  const narrow = this.root?.dataset.layout === "narrow";
3786
3970
  const filters = this.els.filters;
3787
3971
  if (filters) {
3972
+ if (this.cbEl) this.els.zoom?.appendChild(this.cbEl);
3788
3973
  if (narrow) {
3789
3974
  if (this.a11yChipsEl) filters.appendChild(this.a11yChipsEl);
3790
- if (this.cbEl) filters.appendChild(this.cbEl);
3791
3975
  } else {
3792
3976
  if (this.a11yChipsEl) this.regions["top-left"]?.appendChild(this.a11yChipsEl);
3793
- if (this.cbEl) this.els.zoom?.appendChild(this.cbEl);
3794
3977
  }
3795
3978
  const has = narrow && filters.children.length > 0;
3796
3979
  filters.classList.toggle("has", has);
@@ -4981,6 +5164,119 @@ var SeatPicker = class _SeatPicker {
4981
5164
  return `${n} ${this.currency}`;
4982
5165
  }
4983
5166
  }
5167
+ /**
5168
+ * Sleep until the offer schedule's next known transition, then re-read.
5169
+ *
5170
+ * A far-away boundary is capped: the wake re-reads, learns the (unchanged)
5171
+ * schedule, and re-arms — so a picker left open for days still tracks an
5172
+ * organizer's schedule edits at a cost of one request every few hours. No
5173
+ * future transition means no timer at all; an event with no releases does
5174
+ * zero background traffic. A wake in a hidden tab fetches nothing — the
5175
+ * visibilitychange handler owns catching that tab up.
5176
+ */
5177
+ scheduleOfferBoundary(availability) {
5178
+ if (this.offerBoundaryTimer) clearTimeout(this.offerBoundaryTimer);
5179
+ this.offerBoundaryTimer = null;
5180
+ if (!this.api.availability || this.destroyed) return;
5181
+ const now = Date.now();
5182
+ const boundary = nextOfferTransitionAt(availability, now);
5183
+ if (boundary == null) return;
5184
+ const MAX_SLEEP_MS = 6 * 36e5;
5185
+ const delay = Math.min(Math.max(boundary - now + 1e3, 1e3), MAX_SLEEP_MS);
5186
+ this.offerBoundaryTimer = setTimeout(() => {
5187
+ this.offerBoundaryTimer = null;
5188
+ if (document.hidden) return;
5189
+ void this.refreshOfferAvailability(false);
5190
+ }, delay);
5191
+ }
5192
+ /** Debounce the no-store offer read behind a burst of seat-status frames. */
5193
+ scheduleOfferRefresh(live) {
5194
+ if (!this.api.availability || this.destroyed) return;
5195
+ if (this.offerRefreshTimer) clearTimeout(this.offerRefreshTimer);
5196
+ this.offerRefreshTimer = setTimeout(() => {
5197
+ this.offerRefreshTimer = null;
5198
+ void this.refreshOfferAvailability(live);
5199
+ }, live ? 180 : 0);
5200
+ }
5201
+ /**
5202
+ * Pull the server's resolved answer. A failed refresh keeps the last truthful
5203
+ * answer: flashing back to a chart price while checkout still charges an
5204
+ * offer is worse than a temporarily stale remaining count.
5205
+ */
5206
+ async refreshOfferAvailability(live) {
5207
+ if (!this.api.availability || this.destroyed) return;
5208
+ try {
5209
+ const body = await this.api.availability(this.opts.event, live);
5210
+ if (this.destroyed) return;
5211
+ const availability = parseTicketOfferAvailability(body);
5212
+ if (!availability) return;
5213
+ this.offerAvailability = availability;
5214
+ this.scheduleOfferBoundary(availability);
5215
+ const server = ticketOfferPrices(availability);
5216
+ const merged = { ...this.hostPricing?.prices ?? {}, ...server };
5217
+ const pricing = Object.keys(merged).length > 0 || this.hostPricing?.formatter ? { prices: merged, ...this.hostPricing?.formatter ? { formatter: this.hostPricing.formatter } : {} } : void 0;
5218
+ this.setPricing(pricing);
5219
+ this.syncOffer();
5220
+ this.opts.onOfferAvailabilityChange?.(availability);
5221
+ } catch {
5222
+ if (!this.destroyed && !this.offerBoundaryTimer && !document.hidden) {
5223
+ this.offerBoundaryTimer = setTimeout(() => {
5224
+ this.offerBoundaryTimer = null;
5225
+ if (document.hidden) return;
5226
+ void this.refreshOfferAvailability(false);
5227
+ }, 3e4);
5228
+ }
5229
+ }
5230
+ }
5231
+ offerPrice(categoryKey) {
5232
+ if (!categoryKey) return null;
5233
+ return this.offerAvailability?.prices.find((entry) => entry.categoryKey === categoryKey) ?? null;
5234
+ }
5235
+ /** The compact current/upcoming offer card above Ticket prices. */
5236
+ syncOffer() {
5237
+ const host = this.els.offer;
5238
+ if (!host) return;
5239
+ const availability = this.offerAvailability;
5240
+ const active = availability?.release ?? null;
5241
+ const upcoming = !active ? availability?.upcoming ?? null : null;
5242
+ if (!availability || availability.state === "closed" || availability.state === "sold-out" || !active && !upcoming) {
5243
+ host.classList.remove("has");
5244
+ host.replaceChildren();
5245
+ return;
5246
+ }
5247
+ const offer = active ?? upcoming;
5248
+ const main = document.createElement("div");
5249
+ main.className = "sl-offer-main";
5250
+ const copy = document.createElement("div");
5251
+ copy.className = "sl-offer-copy";
5252
+ const kicker = document.createElement("span");
5253
+ kicker.className = "sl-offer-kicker";
5254
+ kicker.textContent = active ? "Current ticket offer" : "Upcoming ticket offer";
5255
+ const name = document.createElement("strong");
5256
+ name.className = "sl-offer-name";
5257
+ name.textContent = offer.name || (active ? "Current offer" : "Scheduled offer");
5258
+ const line = document.createElement("span");
5259
+ line.className = "sl-offer-line";
5260
+ const facts = [];
5261
+ if (active && availability.fromPrice != null) facts.push(this.money(availability.fromPrice / 100));
5262
+ if (active && offer.remaining != null) facts.push(`${offer.remaining} available`);
5263
+ if (active && offer.endsAt != null) facts.push(`until ${formatWhen(offer.endsAt, this.eventTimezone, this.opts.locale)}`);
5264
+ if (upcoming?.startsAt != null) facts.push(`starts ${formatWhen(upcoming.startsAt, this.eventTimezone, this.opts.locale)}`);
5265
+ line.textContent = facts.join(" \xB7 ");
5266
+ copy.append(kicker, name, line);
5267
+ const info = document.createElement("details");
5268
+ info.className = "sl-offer-info";
5269
+ const summary = document.createElement("summary");
5270
+ summary.setAttribute("aria-label", `How the ${name.textContent} offer works`);
5271
+ summary.textContent = "i";
5272
+ const detail = document.createElement("div");
5273
+ detail.className = "sl-offer-detail";
5274
+ 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.`;
5275
+ info.append(summary, detail);
5276
+ main.append(copy, info);
5277
+ host.replaceChildren(main);
5278
+ host.classList.add("has");
5279
+ }
4984
5280
  /**
4985
5281
  * The price the buyer will actually pay for a category (+tier): the host's
4986
5282
  * `pricing` override when present, else the chart's stored price. Every
@@ -5007,9 +5303,11 @@ var SeatPicker = class _SeatPicker {
5007
5303
  this.els.prices.classList.toggle("sl-expanded", overflow > 1 && this.pricesExpanded);
5008
5304
  this.els.prices.innerHTML = shown.map((c) => {
5009
5305
  const price = this.catPrice(c);
5306
+ const offer = this.offerPrice(c.key);
5307
+ const previous = offer?.previousPrice != null && offer.previousPrice > offer.price ? offer.previousPrice : null;
5010
5308
  const active = this.focusedCatKey === c.key;
5011
5309
  const dim = this.priceBandKeys != null && !this.priceBandKeys.has(c.key);
5012
- 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>`;
5310
+ 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>`;
5013
5311
  }).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>`;
5014
5312
  this.els.prices.querySelectorAll(".sl-price-row").forEach((row) => {
5015
5313
  row.addEventListener("mouseenter", () => this.controller.getRenderer()?.setCategoryHighlight?.(row.dataset.cat ?? null));
@@ -5321,10 +5619,11 @@ var SeatPicker = class _SeatPicker {
5321
5619
  if (previousCount === 0 && count > 0) this.animateOnce(this.els.cta, "sl-ready", 520);
5322
5620
  if (this.els.peek) {
5323
5621
  if (count) {
5324
- 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>`;
5622
+ const holding = !!this.hold && !pendingCount;
5623
+ 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>`;
5325
5624
  } else {
5326
5625
  const prices = (this.controller.doc?.categories ?? []).map((c) => this.catPrice(c)).filter((p) => p != null);
5327
- 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>`;
5626
+ 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>`;
5328
5627
  }
5329
5628
  }
5330
5629
  this.lastTrayCount = count;
@@ -5664,6 +5963,7 @@ var SeatPicker = class _SeatPicker {
5664
5963
  }
5665
5964
  emitHoldChange() {
5666
5965
  const hold = this.hold;
5966
+ this.scheduleOfferRefresh(true);
5667
5967
  this.opts.onHoldChange?.(
5668
5968
  hold,
5669
5969
  hold?.seats ?? [],
@@ -6393,6 +6693,7 @@ var SeatPicker = class _SeatPicker {
6393
6693
  flashOnLiveChange: true,
6394
6694
  onStatusChange: () => {
6395
6695
  this.syncPrices();
6696
+ this.scheduleOfferRefresh(true);
6396
6697
  this.detectBooked();
6397
6698
  this.refreshMinimap();
6398
6699
  this.pushAvailabilityTo3d();
@@ -6528,6 +6829,14 @@ var SeatPicker = class _SeatPicker {
6528
6829
  this.stopHoldTimer();
6529
6830
  if (this.toastTimer) clearTimeout(this.toastTimer);
6530
6831
  if (this.liveTimer) clearTimeout(this.liveTimer);
6832
+ if (this.offerRefreshTimer) clearTimeout(this.offerRefreshTimer);
6833
+ if (this.offerBoundaryTimer) clearTimeout(this.offerBoundaryTimer);
6834
+ this.offerRefreshTimer = null;
6835
+ this.offerBoundaryTimer = null;
6836
+ if (this.offerVisibilityHandler) {
6837
+ document.removeEventListener("visibilitychange", this.offerVisibilityHandler);
6838
+ this.offerVisibilityHandler = null;
6839
+ }
6531
6840
  for (const timer of this.motionTimers) clearTimeout(timer);
6532
6841
  this.motionTimers.clear();
6533
6842
  this.ro?.disconnect();
@@ -6668,10 +6977,12 @@ export {
6668
6977
  markerOf,
6669
6978
  mutationCount,
6670
6979
  needsMoveConfirmation,
6980
+ parseTicketOfferAvailability,
6671
6981
  planAssignment,
6672
6982
  retryAfterCopy,
6673
6983
  selectionSources,
6674
6984
  stateBadge,
6675
- suggestMarker
6985
+ suggestMarker,
6986
+ ticketOfferPrices
6676
6987
  };
6677
6988
  //# sourceMappingURL=index.js.map