@burdenoff/website-sdk 2026.924.10 → 2026.924.11

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.
@@ -2033,6 +2033,45 @@ function buildFallbackCatalog(examplePrompts, welcome) {
2033
2033
  conceptHighlights: []
2034
2034
  };
2035
2035
  }
2036
+ var BAKED_CATALOG_ELEMENT_ID = "boff-explore-catalog";
2037
+ function bakedCatalogLocale() {
2038
+ if (typeof navigator === "undefined") return "";
2039
+ return (navigator.language || "").toLowerCase().split("-")[0] ?? "";
2040
+ }
2041
+ function snapshotBakedElement() {
2042
+ if (typeof document === "undefined") return null;
2043
+ const el = document.getElementById(BAKED_CATALOG_ELEMENT_ID);
2044
+ if (!el || el.tagName !== "SCRIPT" || el.getAttribute("type") !== "application/json")
2045
+ return null;
2046
+ return {
2047
+ product: el.getAttribute("data-product") ?? "",
2048
+ locale: el.getAttribute("data-locale") ?? "",
2049
+ text: el.textContent ?? ""
2050
+ };
2051
+ }
2052
+ var BAKED_AT_LOAD = snapshotBakedElement();
2053
+ function isCatalogShape(value) {
2054
+ if (!value || typeof value !== "object") return false;
2055
+ const c = value;
2056
+ const listOrAbsent = (v) => v === void 0 || v === null || Array.isArray(v);
2057
+ const textOrAbsent = (v) => v === void 0 || v === null || typeof v === "string";
2058
+ return (c["enabled"] === void 0 || typeof c["enabled"] === "boolean") && listOrAbsent(c["products"]) && listOrAbsent(c["examplePrompts"]) && listOrAbsent(c["conceptHighlights"]) && textOrAbsent(c["welcomeTitle"]) && textOrAbsent(c["welcomeBody"]);
2059
+ }
2060
+ function readBakedCatalog(productSlug) {
2061
+ const baked = snapshotBakedElement() ?? BAKED_AT_LOAD;
2062
+ if (!baked) return null;
2063
+ if (baked.product !== (productSlug ?? "")) return null;
2064
+ if (baked.locale && baked.locale !== bakedCatalogLocale()) return null;
2065
+ try {
2066
+ const parsed = JSON.parse(baked.text);
2067
+ return isCatalogShape(parsed) ? parsed : null;
2068
+ } catch {
2069
+ return null;
2070
+ }
2071
+ }
2072
+ function serializeBakedCatalog(raw) {
2073
+ return JSON.stringify(raw).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
2074
+ }
2036
2075
  function normalizeCatalog(raw, fallback) {
2037
2076
  return {
2038
2077
  enabled: raw.enabled !== false,
@@ -2282,9 +2321,17 @@ function useExploreChat(options = {}) {
2282
2321
  ),
2283
2322
  [promptKey, welcomeKey]
2284
2323
  );
2285
- const [catalog, setCatalog] = useState(fallbackCatalog);
2324
+ const [bakedRaw] = useState(
2325
+ () => readBakedCatalog(productSlug)
2326
+ );
2327
+ const [catalog, setCatalog] = useState(
2328
+ () => bakedRaw ? normalizeCatalog(bakedRaw, fallbackCatalog) : fallbackCatalog
2329
+ );
2330
+ const [serverCatalogRaw, setServerCatalogRaw] = useState(bakedRaw);
2286
2331
  const [messages, setMessages] = useState([]);
2287
- const [phase, setPhase] = useState("loading");
2332
+ const [phase, setPhase] = useState(
2333
+ () => bakedRaw ? bakedRaw.enabled === false ? "disabled" : "ready" : "loading"
2334
+ );
2288
2335
  const [error, setError] = useState(null);
2289
2336
  const [remainingToday, setRemainingToday] = useState(null);
2290
2337
  const [focusProduct, setFocusProductState] = useState(null);
@@ -2302,7 +2349,7 @@ function useExploreChat(options = {}) {
2302
2349
  shareTokenRef.current = readShareToken();
2303
2350
  }
2304
2351
  const isSharedView = shareTokenRef.current !== null;
2305
- const catalogRef = useRef(fallbackCatalog);
2352
+ const catalogRef = useRef(catalog);
2306
2353
  const turnCountRef = useRef(0);
2307
2354
  const focusProductRef = useRef(null);
2308
2355
  const lastSentTextRef = useRef(null);
@@ -2448,25 +2495,29 @@ function useExploreChat(options = {}) {
2448
2495
  const raw = res.data?.exploreCatalog;
2449
2496
  if (res.errors?.length || !raw) {
2450
2497
  console.warn(
2451
- "[WebSDK] exploreCatalog unavailable \u2014 using the built-in fallback catalog",
2498
+ "[WebSDK] exploreCatalog unavailable \u2014 keeping the catalog the page started with",
2452
2499
  res.errors?.[0]?.message
2453
2500
  );
2454
2501
  setPhase((prev) => prev === "loading" ? "ready" : prev);
2455
2502
  return;
2456
2503
  }
2457
- const next = normalizeCatalog(raw, fallbackCatalog);
2504
+ const next = normalizeCatalog(
2505
+ raw,
2506
+ bakedRaw ? normalizeCatalog(bakedRaw, fallbackCatalog) : fallbackCatalog
2507
+ );
2458
2508
  setCatalog(next);
2509
+ setServerCatalogRaw(raw);
2459
2510
  catalogRef.current = next;
2460
2511
  setPhase((prev) => {
2461
2512
  if (!next.enabled) return "disabled";
2462
- return prev === "loading" ? "ready" : prev;
2513
+ return prev === "loading" || prev === "disabled" ? "ready" : prev;
2463
2514
  });
2464
2515
  };
2465
2516
  void load();
2466
2517
  return () => {
2467
2518
  cancelled = true;
2468
2519
  };
2469
- }, [client, productSlug, fallbackCatalog]);
2520
+ }, [client, productSlug, fallbackCatalog, bakedRaw]);
2470
2521
  const startPolling = useCallback(
2471
2522
  (conversationToken, messageId) => {
2472
2523
  pollGenerationRef.current += 1;
@@ -2934,6 +2985,7 @@ function useExploreChat(options = {}) {
2934
2985
  return {
2935
2986
  phase,
2936
2987
  catalog,
2988
+ serverCatalogRaw,
2937
2989
  messages,
2938
2990
  activity,
2939
2991
  remainingToday,
@@ -3892,6 +3944,30 @@ var EXPLORE_CSS = `
3892
3944
  }
3893
3945
  .boff-explore-strip::-webkit-scrollbar { display: none; }
3894
3946
 
3947
+ /* The product chips run past the edge of their row (there are 38). A hard edge sliced the
3948
+ last visible chip mid-word ("Bi", "BigC") right beside "All 38"; a short fade says "there
3949
+ is more this way" instead. A mask carries only alpha, so the gradient's colour is a token
3950
+ that never shows. Mirrored for right-to-left locales, whose row runs the other way. */
3951
+ .boff-explore-chips-fade {
3952
+ /* Focus scrolling honours scroll-padding, so a tabbed-to chip is brought clear of the fade
3953
+ rather than left half-hidden under it. */
3954
+ scroll-padding-inline-end: 2rem;
3955
+ -webkit-mask-image: linear-gradient(to right, hsl(var(--foreground)) calc(100% - 2rem), hsl(var(--foreground) / 0));
3956
+ mask-image: linear-gradient(to right, hsl(var(--foreground)) calc(100% - 2rem), hsl(var(--foreground) / 0));
3957
+ }
3958
+ /* A fade over a fixed edge would leave the LAST chip under it once the row is scrolled to
3959
+ the end \u2014 the visitor could never read it. A trailing spacer the width of the fade lets
3960
+ the final chip scroll clear of it. A pseudo-element rather than padding: trailing padding
3961
+ on a flex scroll container is not reliably part of its scrollable overflow. */
3962
+ .boff-explore-chips-fade::after {
3963
+ content: "";
3964
+ flex: 0 0 2rem;
3965
+ }
3966
+ [dir="rtl"] .boff-explore-chips-fade {
3967
+ -webkit-mask-image: linear-gradient(to left, hsl(var(--foreground)) calc(100% - 2rem), hsl(var(--foreground) / 0));
3968
+ mask-image: linear-gradient(to left, hsl(var(--foreground)) calc(100% - 2rem), hsl(var(--foreground) / 0));
3969
+ }
3970
+
3895
3971
  @keyframes boff-explore-fade-in {
3896
3972
  from { opacity: 0; transform: translateY(2px); }
3897
3973
  to { opacity: 1; transform: none; }
@@ -3968,10 +4044,10 @@ var EXPLORE_CSS = `
3968
4044
  Colour comes from the product's --primary token, so it is whatever that site's palette
3969
4045
  already is, never a colour chosen here. */
3970
4046
  .boff-explore-invite {
3971
- border-color: hsl(var(--primary) / 0.45);
4047
+ border-color: hsl(var(--primary) / 0.6);
3972
4048
  box-shadow:
3973
- 0 0 0 4px hsl(var(--primary) / 0.08),
3974
- 0 8px 24px -12px hsl(var(--primary) / 0.45);
4049
+ 0 0 0 4px hsl(var(--primary) / 0.12),
4050
+ 0 8px 24px -12px hsl(var(--primary) / 0.55);
3975
4051
  animation: boff-explore-breathe 3.2s ease-in-out infinite;
3976
4052
  }
3977
4053
  /* Focus and typing both mean "I have found it", and the hint has done its job. */
@@ -3981,8 +4057,8 @@ var EXPLORE_CSS = `
3981
4057
  box-shadow: 0 0 0 4px hsl(var(--primary) / 0.10);
3982
4058
  }
3983
4059
  @keyframes boff-explore-breathe {
3984
- 0%, 100% { box-shadow: 0 0 0 4px hsl(var(--primary) / 0.07), 0 8px 24px -12px hsl(var(--primary) / 0.40); }
3985
- 50% { box-shadow: 0 0 0 7px hsl(var(--primary) / 0.13), 0 8px 28px -12px hsl(var(--primary) / 0.55); }
4060
+ 0%, 100% { box-shadow: 0 0 0 4px hsl(var(--primary) / 0.10), 0 8px 24px -12px hsl(var(--primary) / 0.50); }
4061
+ 50% { box-shadow: 0 0 0 7px hsl(var(--primary) / 0.18), 0 8px 28px -12px hsl(var(--primary) / 0.65); }
3986
4062
  }
3987
4063
 
3988
4064
  /* Every one of the above is decoration. Reduced motion removes the movement and keeps the
@@ -5291,14 +5367,20 @@ function PromptCarousel({
5291
5367
  "aria-label": `Ask: ${current}`,
5292
5368
  "aria-live": paused ? "polite" : "off",
5293
5369
  onClick: () => onPrompt(current),
5294
- className: "boff-explore-fade group flex min-w-0 flex-1 items-center gap-2 rounded-full border border-border bg-card px-4 py-2 text-left text-sm text-card-foreground shadow-sm transition-colors hover:border-primary/50 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
5370
+ className: "group flex min-w-0 flex-1 items-center gap-2 rounded-full border border-border bg-card px-4 py-2 max-sm:min-h-[3.625rem] max-sm:rounded-2xl text-left text-sm text-card-foreground shadow-sm transition-colors hover:border-primary/50 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
5295
5371
  children: [
5296
5372
  /* @__PURE__ */ jsx(Sparkles, { className: "h-3.5 w-3.5 shrink-0 text-primary" }),
5297
- /* @__PURE__ */ jsx("span", { className: "truncate", children: current }),
5373
+ /* @__PURE__ */ jsx(
5374
+ "span",
5375
+ {
5376
+ className: "boff-explore-fade min-w-0 max-sm:line-clamp-2 sm:truncate",
5377
+ children: current
5378
+ },
5379
+ current
5380
+ ),
5298
5381
  /* @__PURE__ */ jsx(ArrowUp, { className: "ml-auto h-3.5 w-3.5 shrink-0 rotate-45 text-muted-foreground transition-colors group-hover:text-primary" })
5299
5382
  ]
5300
- },
5301
- current
5383
+ }
5302
5384
  ),
5303
5385
  count > 1 ? /* @__PURE__ */ jsx(
5304
5386
  "button",
@@ -5337,7 +5419,7 @@ function ProductStrip({
5337
5419
  {
5338
5420
  className: cn(
5339
5421
  "boff-explore-strip flex min-w-0 flex-1 gap-2",
5340
- expanded ? "flex-wrap" : "flex-nowrap overflow-x-auto"
5422
+ expanded ? "flex-wrap" : "boff-explore-chips-fade flex-nowrap overflow-x-auto"
5341
5423
  ),
5342
5424
  children: [
5343
5425
  /* @__PURE__ */ jsx(
@@ -5475,11 +5557,11 @@ function ConceptStrip({
5475
5557
  onScroll: measureRail,
5476
5558
  "data-boff-explore": "concept-strip",
5477
5559
  "aria-label": T.conceptStripLabel,
5478
- className: "boff-explore-strip flex min-w-0 snap-x gap-3 overflow-x-auto pb-1",
5560
+ className: "boff-explore-strip flex min-w-0 snap-x gap-3 overflow-x-auto pb-1 sm:mx-10",
5479
5561
  children: visible.map((highlight) => /* @__PURE__ */ jsx(
5480
5562
  "li",
5481
5563
  {
5482
- className: "shrink-0 snap-start",
5564
+ className: "flex shrink-0 snap-start",
5483
5565
  children: /* @__PURE__ */ jsxs(
5484
5566
  "button",
5485
5567
  {
@@ -5520,9 +5602,9 @@ function ConceptStrip({
5520
5602
  }
5521
5603
  )
5522
5604
  ] }),
5523
- /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col gap-0.5 p-2.5", children: [
5605
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5 p-2.5", children: [
5524
5606
  /* @__PURE__ */ jsx("span", { className: "line-clamp-2 text-xs font-semibold leading-snug text-card-foreground", children: highlight.title }),
5525
- /* @__PURE__ */ jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: highlight.productName })
5607
+ /* @__PURE__ */ jsx("span", { className: "mt-auto truncate text-[11px] text-muted-foreground", children: highlight.productName })
5526
5608
  ] })
5527
5609
  ]
5528
5610
  }
@@ -5739,6 +5821,7 @@ function ExplorePage({
5739
5821
  const {
5740
5822
  phase,
5741
5823
  catalog,
5824
+ serverCatalogRaw,
5742
5825
  messages,
5743
5826
  activity,
5744
5827
  remainingToday,
@@ -6170,6 +6253,10 @@ function ExplorePage({
6170
6253
  const captchaOn = Boolean(
6171
6254
  catalog.captchaRequired && catalog.recaptchaSiteKey
6172
6255
  );
6256
+ const bakedCatalogJson = useMemo(
6257
+ () => serverCatalogRaw && serverCatalogRaw.enabled !== false ? serializeBakedCatalog(serverCatalogRaw) : null,
6258
+ [serverCatalogRaw]
6259
+ );
6173
6260
  const rootStyle = {};
6174
6261
  if (heightMode === "viewport") {
6175
6262
  rootStyle["--boff-explore-offset"] = paneOffset === null ? normalizeOffset(viewportOffset) : `${paneOffset}px`;
@@ -6209,6 +6296,16 @@ function ExplorePage({
6209
6296
  ),
6210
6297
  /* @__PURE__ */ jsx(ProseStyles, {}),
6211
6298
  /* @__PURE__ */ jsx(ExploreStyles, {}),
6299
+ bakedCatalogJson ? /* @__PURE__ */ jsx(
6300
+ "script",
6301
+ {
6302
+ id: BAKED_CATALOG_ELEMENT_ID,
6303
+ type: "application/json",
6304
+ "data-product": productSlug ?? "",
6305
+ "data-locale": bakedCatalogLocale(),
6306
+ dangerouslySetInnerHTML: { __html: bakedCatalogJson }
6307
+ }
6308
+ ) : null,
6212
6309
  /* @__PURE__ */ jsx(ExploreAnswerStyles, {}),
6213
6310
  !showWelcome ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 border-b border-border px-4 py-2.5", children: [
6214
6311
  /* @__PURE__ */ jsx(
@@ -6683,7 +6780,7 @@ function ExplorePage({
6683
6780
  )
6684
6781
  ] }) : null,
6685
6782
  /* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [
6686
- /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: T.disclaimerShort }),
6783
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 sm:truncate", children: T.disclaimerShort }),
6687
6784
  /* @__PURE__ */ jsx(
6688
6785
  "button",
6689
6786
  {