@burdenoff/website-sdk 2026.924.9 → 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.
@@ -2059,6 +2059,45 @@ function buildFallbackCatalog(examplePrompts, welcome) {
2059
2059
  conceptHighlights: []
2060
2060
  };
2061
2061
  }
2062
+ var BAKED_CATALOG_ELEMENT_ID = "boff-explore-catalog";
2063
+ function bakedCatalogLocale() {
2064
+ if (typeof navigator === "undefined") return "";
2065
+ return (navigator.language || "").toLowerCase().split("-")[0] ?? "";
2066
+ }
2067
+ function snapshotBakedElement() {
2068
+ if (typeof document === "undefined") return null;
2069
+ const el = document.getElementById(BAKED_CATALOG_ELEMENT_ID);
2070
+ if (!el || el.tagName !== "SCRIPT" || el.getAttribute("type") !== "application/json")
2071
+ return null;
2072
+ return {
2073
+ product: el.getAttribute("data-product") ?? "",
2074
+ locale: el.getAttribute("data-locale") ?? "",
2075
+ text: el.textContent ?? ""
2076
+ };
2077
+ }
2078
+ var BAKED_AT_LOAD = snapshotBakedElement();
2079
+ function isCatalogShape(value) {
2080
+ if (!value || typeof value !== "object") return false;
2081
+ const c = value;
2082
+ const listOrAbsent = (v) => v === void 0 || v === null || Array.isArray(v);
2083
+ const textOrAbsent = (v) => v === void 0 || v === null || typeof v === "string";
2084
+ return (c["enabled"] === void 0 || typeof c["enabled"] === "boolean") && listOrAbsent(c["products"]) && listOrAbsent(c["examplePrompts"]) && listOrAbsent(c["conceptHighlights"]) && textOrAbsent(c["welcomeTitle"]) && textOrAbsent(c["welcomeBody"]);
2085
+ }
2086
+ function readBakedCatalog(productSlug) {
2087
+ const baked = snapshotBakedElement() ?? BAKED_AT_LOAD;
2088
+ if (!baked) return null;
2089
+ if (baked.product !== (productSlug ?? "")) return null;
2090
+ if (baked.locale && baked.locale !== bakedCatalogLocale()) return null;
2091
+ try {
2092
+ const parsed = JSON.parse(baked.text);
2093
+ return isCatalogShape(parsed) ? parsed : null;
2094
+ } catch {
2095
+ return null;
2096
+ }
2097
+ }
2098
+ function serializeBakedCatalog(raw) {
2099
+ return JSON.stringify(raw).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
2100
+ }
2062
2101
  function normalizeCatalog(raw, fallback) {
2063
2102
  return {
2064
2103
  enabled: raw.enabled !== false,
@@ -2308,9 +2347,17 @@ function useExploreChat(options = {}) {
2308
2347
  ),
2309
2348
  [promptKey, welcomeKey]
2310
2349
  );
2311
- const [catalog, setCatalog] = React.useState(fallbackCatalog);
2350
+ const [bakedRaw] = React.useState(
2351
+ () => readBakedCatalog(productSlug)
2352
+ );
2353
+ const [catalog, setCatalog] = React.useState(
2354
+ () => bakedRaw ? normalizeCatalog(bakedRaw, fallbackCatalog) : fallbackCatalog
2355
+ );
2356
+ const [serverCatalogRaw, setServerCatalogRaw] = React.useState(bakedRaw);
2312
2357
  const [messages, setMessages] = React.useState([]);
2313
- const [phase, setPhase] = React.useState("loading");
2358
+ const [phase, setPhase] = React.useState(
2359
+ () => bakedRaw ? bakedRaw.enabled === false ? "disabled" : "ready" : "loading"
2360
+ );
2314
2361
  const [error, setError] = React.useState(null);
2315
2362
  const [remainingToday, setRemainingToday] = React.useState(null);
2316
2363
  const [focusProduct, setFocusProductState] = React.useState(null);
@@ -2328,7 +2375,7 @@ function useExploreChat(options = {}) {
2328
2375
  shareTokenRef.current = readShareToken();
2329
2376
  }
2330
2377
  const isSharedView = shareTokenRef.current !== null;
2331
- const catalogRef = React.useRef(fallbackCatalog);
2378
+ const catalogRef = React.useRef(catalog);
2332
2379
  const turnCountRef = React.useRef(0);
2333
2380
  const focusProductRef = React.useRef(null);
2334
2381
  const lastSentTextRef = React.useRef(null);
@@ -2474,25 +2521,29 @@ function useExploreChat(options = {}) {
2474
2521
  const raw = res.data?.exploreCatalog;
2475
2522
  if (res.errors?.length || !raw) {
2476
2523
  console.warn(
2477
- "[WebSDK] exploreCatalog unavailable \u2014 using the built-in fallback catalog",
2524
+ "[WebSDK] exploreCatalog unavailable \u2014 keeping the catalog the page started with",
2478
2525
  res.errors?.[0]?.message
2479
2526
  );
2480
2527
  setPhase((prev) => prev === "loading" ? "ready" : prev);
2481
2528
  return;
2482
2529
  }
2483
- const next = normalizeCatalog(raw, fallbackCatalog);
2530
+ const next = normalizeCatalog(
2531
+ raw,
2532
+ bakedRaw ? normalizeCatalog(bakedRaw, fallbackCatalog) : fallbackCatalog
2533
+ );
2484
2534
  setCatalog(next);
2535
+ setServerCatalogRaw(raw);
2485
2536
  catalogRef.current = next;
2486
2537
  setPhase((prev) => {
2487
2538
  if (!next.enabled) return "disabled";
2488
- return prev === "loading" ? "ready" : prev;
2539
+ return prev === "loading" || prev === "disabled" ? "ready" : prev;
2489
2540
  });
2490
2541
  };
2491
2542
  void load();
2492
2543
  return () => {
2493
2544
  cancelled = true;
2494
2545
  };
2495
- }, [client, productSlug, fallbackCatalog]);
2546
+ }, [client, productSlug, fallbackCatalog, bakedRaw]);
2496
2547
  const startPolling = React.useCallback(
2497
2548
  (conversationToken, messageId) => {
2498
2549
  pollGenerationRef.current += 1;
@@ -2960,6 +3011,7 @@ function useExploreChat(options = {}) {
2960
3011
  return {
2961
3012
  phase,
2962
3013
  catalog,
3014
+ serverCatalogRaw,
2963
3015
  messages,
2964
3016
  activity,
2965
3017
  remainingToday,
@@ -3918,6 +3970,30 @@ var EXPLORE_CSS = `
3918
3970
  }
3919
3971
  .boff-explore-strip::-webkit-scrollbar { display: none; }
3920
3972
 
3973
+ /* The product chips run past the edge of their row (there are 38). A hard edge sliced the
3974
+ last visible chip mid-word ("Bi", "BigC") right beside "All 38"; a short fade says "there
3975
+ is more this way" instead. A mask carries only alpha, so the gradient's colour is a token
3976
+ that never shows. Mirrored for right-to-left locales, whose row runs the other way. */
3977
+ .boff-explore-chips-fade {
3978
+ /* Focus scrolling honours scroll-padding, so a tabbed-to chip is brought clear of the fade
3979
+ rather than left half-hidden under it. */
3980
+ scroll-padding-inline-end: 2rem;
3981
+ -webkit-mask-image: linear-gradient(to right, hsl(var(--foreground)) calc(100% - 2rem), hsl(var(--foreground) / 0));
3982
+ mask-image: linear-gradient(to right, hsl(var(--foreground)) calc(100% - 2rem), hsl(var(--foreground) / 0));
3983
+ }
3984
+ /* A fade over a fixed edge would leave the LAST chip under it once the row is scrolled to
3985
+ the end \u2014 the visitor could never read it. A trailing spacer the width of the fade lets
3986
+ the final chip scroll clear of it. A pseudo-element rather than padding: trailing padding
3987
+ on a flex scroll container is not reliably part of its scrollable overflow. */
3988
+ .boff-explore-chips-fade::after {
3989
+ content: "";
3990
+ flex: 0 0 2rem;
3991
+ }
3992
+ [dir="rtl"] .boff-explore-chips-fade {
3993
+ -webkit-mask-image: linear-gradient(to left, hsl(var(--foreground)) calc(100% - 2rem), hsl(var(--foreground) / 0));
3994
+ mask-image: linear-gradient(to left, hsl(var(--foreground)) calc(100% - 2rem), hsl(var(--foreground) / 0));
3995
+ }
3996
+
3921
3997
  @keyframes boff-explore-fade-in {
3922
3998
  from { opacity: 0; transform: translateY(2px); }
3923
3999
  to { opacity: 1; transform: none; }
@@ -3994,10 +4070,10 @@ var EXPLORE_CSS = `
3994
4070
  Colour comes from the product's --primary token, so it is whatever that site's palette
3995
4071
  already is, never a colour chosen here. */
3996
4072
  .boff-explore-invite {
3997
- border-color: hsl(var(--primary) / 0.45);
4073
+ border-color: hsl(var(--primary) / 0.6);
3998
4074
  box-shadow:
3999
- 0 0 0 4px hsl(var(--primary) / 0.08),
4000
- 0 8px 24px -12px hsl(var(--primary) / 0.45);
4075
+ 0 0 0 4px hsl(var(--primary) / 0.12),
4076
+ 0 8px 24px -12px hsl(var(--primary) / 0.55);
4001
4077
  animation: boff-explore-breathe 3.2s ease-in-out infinite;
4002
4078
  }
4003
4079
  /* Focus and typing both mean "I have found it", and the hint has done its job. */
@@ -4007,8 +4083,8 @@ var EXPLORE_CSS = `
4007
4083
  box-shadow: 0 0 0 4px hsl(var(--primary) / 0.10);
4008
4084
  }
4009
4085
  @keyframes boff-explore-breathe {
4010
- 0%, 100% { box-shadow: 0 0 0 4px hsl(var(--primary) / 0.07), 0 8px 24px -12px hsl(var(--primary) / 0.40); }
4011
- 50% { box-shadow: 0 0 0 7px hsl(var(--primary) / 0.13), 0 8px 28px -12px hsl(var(--primary) / 0.55); }
4086
+ 0%, 100% { box-shadow: 0 0 0 4px hsl(var(--primary) / 0.10), 0 8px 24px -12px hsl(var(--primary) / 0.50); }
4087
+ 50% { box-shadow: 0 0 0 7px hsl(var(--primary) / 0.18), 0 8px 28px -12px hsl(var(--primary) / 0.65); }
4012
4088
  }
4013
4089
 
4014
4090
  /* Every one of the above is decoration. Reduced motion removes the movement and keeps the
@@ -5317,14 +5393,20 @@ function PromptCarousel({
5317
5393
  "aria-label": `Ask: ${current}`,
5318
5394
  "aria-live": paused ? "polite" : "off",
5319
5395
  onClick: () => onPrompt(current),
5320
- 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",
5396
+ 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",
5321
5397
  children: [
5322
5398
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "h-3.5 w-3.5 shrink-0 text-primary" }),
5323
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: current }),
5399
+ /* @__PURE__ */ jsxRuntime.jsx(
5400
+ "span",
5401
+ {
5402
+ className: "boff-explore-fade min-w-0 max-sm:line-clamp-2 sm:truncate",
5403
+ children: current
5404
+ },
5405
+ current
5406
+ ),
5324
5407
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowUp, { className: "ml-auto h-3.5 w-3.5 shrink-0 rotate-45 text-muted-foreground transition-colors group-hover:text-primary" })
5325
5408
  ]
5326
- },
5327
- current
5409
+ }
5328
5410
  ),
5329
5411
  count > 1 ? /* @__PURE__ */ jsxRuntime.jsx(
5330
5412
  "button",
@@ -5363,7 +5445,7 @@ function ProductStrip({
5363
5445
  {
5364
5446
  className: cn(
5365
5447
  "boff-explore-strip flex min-w-0 flex-1 gap-2",
5366
- expanded ? "flex-wrap" : "flex-nowrap overflow-x-auto"
5448
+ expanded ? "flex-wrap" : "boff-explore-chips-fade flex-nowrap overflow-x-auto"
5367
5449
  ),
5368
5450
  children: [
5369
5451
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -5501,11 +5583,11 @@ function ConceptStrip({
5501
5583
  onScroll: measureRail,
5502
5584
  "data-boff-explore": "concept-strip",
5503
5585
  "aria-label": T.conceptStripLabel,
5504
- className: "boff-explore-strip flex min-w-0 snap-x gap-3 overflow-x-auto pb-1",
5586
+ className: "boff-explore-strip flex min-w-0 snap-x gap-3 overflow-x-auto pb-1 sm:mx-10",
5505
5587
  children: visible.map((highlight) => /* @__PURE__ */ jsxRuntime.jsx(
5506
5588
  "li",
5507
5589
  {
5508
- className: "shrink-0 snap-start",
5590
+ className: "flex shrink-0 snap-start",
5509
5591
  children: /* @__PURE__ */ jsxRuntime.jsxs(
5510
5592
  "button",
5511
5593
  {
@@ -5546,9 +5628,9 @@ function ConceptStrip({
5546
5628
  }
5547
5629
  )
5548
5630
  ] }),
5549
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-col gap-0.5 p-2.5", children: [
5631
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5 p-2.5", children: [
5550
5632
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "line-clamp-2 text-xs font-semibold leading-snug text-card-foreground", children: highlight.title }),
5551
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: highlight.productName })
5633
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-auto truncate text-[11px] text-muted-foreground", children: highlight.productName })
5552
5634
  ] })
5553
5635
  ]
5554
5636
  }
@@ -5765,6 +5847,7 @@ function ExplorePage({
5765
5847
  const {
5766
5848
  phase,
5767
5849
  catalog,
5850
+ serverCatalogRaw,
5768
5851
  messages,
5769
5852
  activity,
5770
5853
  remainingToday,
@@ -6196,6 +6279,10 @@ function ExplorePage({
6196
6279
  const captchaOn = Boolean(
6197
6280
  catalog.captchaRequired && catalog.recaptchaSiteKey
6198
6281
  );
6282
+ const bakedCatalogJson = React.useMemo(
6283
+ () => serverCatalogRaw && serverCatalogRaw.enabled !== false ? serializeBakedCatalog(serverCatalogRaw) : null,
6284
+ [serverCatalogRaw]
6285
+ );
6199
6286
  const rootStyle = {};
6200
6287
  if (heightMode === "viewport") {
6201
6288
  rootStyle["--boff-explore-offset"] = paneOffset === null ? normalizeOffset(viewportOffset) : `${paneOffset}px`;
@@ -6235,6 +6322,16 @@ function ExplorePage({
6235
6322
  ),
6236
6323
  /* @__PURE__ */ jsxRuntime.jsx(ProseStyles, {}),
6237
6324
  /* @__PURE__ */ jsxRuntime.jsx(ExploreStyles, {}),
6325
+ bakedCatalogJson ? /* @__PURE__ */ jsxRuntime.jsx(
6326
+ "script",
6327
+ {
6328
+ id: BAKED_CATALOG_ELEMENT_ID,
6329
+ type: "application/json",
6330
+ "data-product": productSlug ?? "",
6331
+ "data-locale": bakedCatalogLocale(),
6332
+ dangerouslySetInnerHTML: { __html: bakedCatalogJson }
6333
+ }
6334
+ ) : null,
6238
6335
  /* @__PURE__ */ jsxRuntime.jsx(ExploreAnswerStyles, {}),
6239
6336
  !showWelcome ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2.5 border-b border-border px-4 py-2.5", children: [
6240
6337
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -6709,7 +6806,7 @@ function ExplorePage({
6709
6806
  )
6710
6807
  ] }) : null,
6711
6808
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [
6712
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 truncate", children: T.disclaimerShort }),
6809
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 sm:truncate", children: T.disclaimerShort }),
6713
6810
  /* @__PURE__ */ jsxRuntime.jsx(
6714
6811
  "button",
6715
6812
  {