@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.
package/dist/index.mjs CHANGED
@@ -9004,6 +9004,45 @@ function buildFallbackCatalog(examplePrompts, welcome) {
9004
9004
  conceptHighlights: []
9005
9005
  };
9006
9006
  }
9007
+ var BAKED_CATALOG_ELEMENT_ID = "boff-explore-catalog";
9008
+ function bakedCatalogLocale() {
9009
+ if (typeof navigator === "undefined") return "";
9010
+ return (navigator.language || "").toLowerCase().split("-")[0] ?? "";
9011
+ }
9012
+ function snapshotBakedElement() {
9013
+ if (typeof document === "undefined") return null;
9014
+ const el = document.getElementById(BAKED_CATALOG_ELEMENT_ID);
9015
+ if (!el || el.tagName !== "SCRIPT" || el.getAttribute("type") !== "application/json")
9016
+ return null;
9017
+ return {
9018
+ product: el.getAttribute("data-product") ?? "",
9019
+ locale: el.getAttribute("data-locale") ?? "",
9020
+ text: el.textContent ?? ""
9021
+ };
9022
+ }
9023
+ var BAKED_AT_LOAD = snapshotBakedElement();
9024
+ function isCatalogShape(value) {
9025
+ if (!value || typeof value !== "object") return false;
9026
+ const c = value;
9027
+ const listOrAbsent = (v) => v === void 0 || v === null || Array.isArray(v);
9028
+ const textOrAbsent = (v) => v === void 0 || v === null || typeof v === "string";
9029
+ return (c["enabled"] === void 0 || typeof c["enabled"] === "boolean") && listOrAbsent(c["products"]) && listOrAbsent(c["examplePrompts"]) && listOrAbsent(c["conceptHighlights"]) && textOrAbsent(c["welcomeTitle"]) && textOrAbsent(c["welcomeBody"]);
9030
+ }
9031
+ function readBakedCatalog(productSlug) {
9032
+ const baked = snapshotBakedElement() ?? BAKED_AT_LOAD;
9033
+ if (!baked) return null;
9034
+ if (baked.product !== (productSlug ?? "")) return null;
9035
+ if (baked.locale && baked.locale !== bakedCatalogLocale()) return null;
9036
+ try {
9037
+ const parsed = JSON.parse(baked.text);
9038
+ return isCatalogShape(parsed) ? parsed : null;
9039
+ } catch {
9040
+ return null;
9041
+ }
9042
+ }
9043
+ function serializeBakedCatalog(raw) {
9044
+ return JSON.stringify(raw).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
9045
+ }
9007
9046
  function normalizeCatalog(raw, fallback) {
9008
9047
  return {
9009
9048
  enabled: raw.enabled !== false,
@@ -9253,9 +9292,17 @@ function useExploreChat(options = {}) {
9253
9292
  ),
9254
9293
  [promptKey, welcomeKey]
9255
9294
  );
9256
- const [catalog, setCatalog] = useState(fallbackCatalog);
9295
+ const [bakedRaw] = useState(
9296
+ () => readBakedCatalog(productSlug)
9297
+ );
9298
+ const [catalog, setCatalog] = useState(
9299
+ () => bakedRaw ? normalizeCatalog(bakedRaw, fallbackCatalog) : fallbackCatalog
9300
+ );
9301
+ const [serverCatalogRaw, setServerCatalogRaw] = useState(bakedRaw);
9257
9302
  const [messages, setMessages] = useState([]);
9258
- const [phase, setPhase] = useState("loading");
9303
+ const [phase, setPhase] = useState(
9304
+ () => bakedRaw ? bakedRaw.enabled === false ? "disabled" : "ready" : "loading"
9305
+ );
9259
9306
  const [error, setError] = useState(null);
9260
9307
  const [remainingToday, setRemainingToday] = useState(null);
9261
9308
  const [focusProduct, setFocusProductState] = useState(null);
@@ -9273,7 +9320,7 @@ function useExploreChat(options = {}) {
9273
9320
  shareTokenRef.current = readShareToken();
9274
9321
  }
9275
9322
  const isSharedView = shareTokenRef.current !== null;
9276
- const catalogRef = useRef(fallbackCatalog);
9323
+ const catalogRef = useRef(catalog);
9277
9324
  const turnCountRef = useRef(0);
9278
9325
  const focusProductRef = useRef(null);
9279
9326
  const lastSentTextRef = useRef(null);
@@ -9419,25 +9466,29 @@ function useExploreChat(options = {}) {
9419
9466
  const raw = res.data?.exploreCatalog;
9420
9467
  if (res.errors?.length || !raw) {
9421
9468
  console.warn(
9422
- "[WebSDK] exploreCatalog unavailable \u2014 using the built-in fallback catalog",
9469
+ "[WebSDK] exploreCatalog unavailable \u2014 keeping the catalog the page started with",
9423
9470
  res.errors?.[0]?.message
9424
9471
  );
9425
9472
  setPhase((prev) => prev === "loading" ? "ready" : prev);
9426
9473
  return;
9427
9474
  }
9428
- const next = normalizeCatalog(raw, fallbackCatalog);
9475
+ const next = normalizeCatalog(
9476
+ raw,
9477
+ bakedRaw ? normalizeCatalog(bakedRaw, fallbackCatalog) : fallbackCatalog
9478
+ );
9429
9479
  setCatalog(next);
9480
+ setServerCatalogRaw(raw);
9430
9481
  catalogRef.current = next;
9431
9482
  setPhase((prev) => {
9432
9483
  if (!next.enabled) return "disabled";
9433
- return prev === "loading" ? "ready" : prev;
9484
+ return prev === "loading" || prev === "disabled" ? "ready" : prev;
9434
9485
  });
9435
9486
  };
9436
9487
  void load();
9437
9488
  return () => {
9438
9489
  cancelled = true;
9439
9490
  };
9440
- }, [client, productSlug, fallbackCatalog]);
9491
+ }, [client, productSlug, fallbackCatalog, bakedRaw]);
9441
9492
  const startPolling = useCallback(
9442
9493
  (conversationToken, messageId) => {
9443
9494
  pollGenerationRef.current += 1;
@@ -9905,6 +9956,7 @@ function useExploreChat(options = {}) {
9905
9956
  return {
9906
9957
  phase,
9907
9958
  catalog,
9959
+ serverCatalogRaw,
9908
9960
  messages,
9909
9961
  activity,
9910
9962
  remainingToday,
@@ -10738,6 +10790,30 @@ var EXPLORE_CSS = `
10738
10790
  }
10739
10791
  .boff-explore-strip::-webkit-scrollbar { display: none; }
10740
10792
 
10793
+ /* The product chips run past the edge of their row (there are 38). A hard edge sliced the
10794
+ last visible chip mid-word ("Bi", "BigC") right beside "All 38"; a short fade says "there
10795
+ is more this way" instead. A mask carries only alpha, so the gradient's colour is a token
10796
+ that never shows. Mirrored for right-to-left locales, whose row runs the other way. */
10797
+ .boff-explore-chips-fade {
10798
+ /* Focus scrolling honours scroll-padding, so a tabbed-to chip is brought clear of the fade
10799
+ rather than left half-hidden under it. */
10800
+ scroll-padding-inline-end: 2rem;
10801
+ -webkit-mask-image: linear-gradient(to right, hsl(var(--foreground)) calc(100% - 2rem), hsl(var(--foreground) / 0));
10802
+ mask-image: linear-gradient(to right, hsl(var(--foreground)) calc(100% - 2rem), hsl(var(--foreground) / 0));
10803
+ }
10804
+ /* A fade over a fixed edge would leave the LAST chip under it once the row is scrolled to
10805
+ the end \u2014 the visitor could never read it. A trailing spacer the width of the fade lets
10806
+ the final chip scroll clear of it. A pseudo-element rather than padding: trailing padding
10807
+ on a flex scroll container is not reliably part of its scrollable overflow. */
10808
+ .boff-explore-chips-fade::after {
10809
+ content: "";
10810
+ flex: 0 0 2rem;
10811
+ }
10812
+ [dir="rtl"] .boff-explore-chips-fade {
10813
+ -webkit-mask-image: linear-gradient(to left, hsl(var(--foreground)) calc(100% - 2rem), hsl(var(--foreground) / 0));
10814
+ mask-image: linear-gradient(to left, hsl(var(--foreground)) calc(100% - 2rem), hsl(var(--foreground) / 0));
10815
+ }
10816
+
10741
10817
  @keyframes boff-explore-fade-in {
10742
10818
  from { opacity: 0; transform: translateY(2px); }
10743
10819
  to { opacity: 1; transform: none; }
@@ -10814,10 +10890,10 @@ var EXPLORE_CSS = `
10814
10890
  Colour comes from the product's --primary token, so it is whatever that site's palette
10815
10891
  already is, never a colour chosen here. */
10816
10892
  .boff-explore-invite {
10817
- border-color: hsl(var(--primary) / 0.45);
10893
+ border-color: hsl(var(--primary) / 0.6);
10818
10894
  box-shadow:
10819
- 0 0 0 4px hsl(var(--primary) / 0.08),
10820
- 0 8px 24px -12px hsl(var(--primary) / 0.45);
10895
+ 0 0 0 4px hsl(var(--primary) / 0.12),
10896
+ 0 8px 24px -12px hsl(var(--primary) / 0.55);
10821
10897
  animation: boff-explore-breathe 3.2s ease-in-out infinite;
10822
10898
  }
10823
10899
  /* Focus and typing both mean "I have found it", and the hint has done its job. */
@@ -10827,8 +10903,8 @@ var EXPLORE_CSS = `
10827
10903
  box-shadow: 0 0 0 4px hsl(var(--primary) / 0.10);
10828
10904
  }
10829
10905
  @keyframes boff-explore-breathe {
10830
- 0%, 100% { box-shadow: 0 0 0 4px hsl(var(--primary) / 0.07), 0 8px 24px -12px hsl(var(--primary) / 0.40); }
10831
- 50% { box-shadow: 0 0 0 7px hsl(var(--primary) / 0.13), 0 8px 28px -12px hsl(var(--primary) / 0.55); }
10906
+ 0%, 100% { box-shadow: 0 0 0 4px hsl(var(--primary) / 0.10), 0 8px 24px -12px hsl(var(--primary) / 0.50); }
10907
+ 50% { box-shadow: 0 0 0 7px hsl(var(--primary) / 0.18), 0 8px 28px -12px hsl(var(--primary) / 0.65); }
10832
10908
  }
10833
10909
 
10834
10910
  /* Every one of the above is decoration. Reduced motion removes the movement and keeps the
@@ -12137,14 +12213,20 @@ function PromptCarousel({
12137
12213
  "aria-label": `Ask: ${current}`,
12138
12214
  "aria-live": paused ? "polite" : "off",
12139
12215
  onClick: () => onPrompt(current),
12140
- 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",
12216
+ 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",
12141
12217
  children: [
12142
12218
  /* @__PURE__ */ jsx(Sparkles, { className: "h-3.5 w-3.5 shrink-0 text-primary" }),
12143
- /* @__PURE__ */ jsx("span", { className: "truncate", children: current }),
12219
+ /* @__PURE__ */ jsx(
12220
+ "span",
12221
+ {
12222
+ className: "boff-explore-fade min-w-0 max-sm:line-clamp-2 sm:truncate",
12223
+ children: current
12224
+ },
12225
+ current
12226
+ ),
12144
12227
  /* @__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" })
12145
12228
  ]
12146
- },
12147
- current
12229
+ }
12148
12230
  ),
12149
12231
  count > 1 ? /* @__PURE__ */ jsx(
12150
12232
  "button",
@@ -12183,7 +12265,7 @@ function ProductStrip({
12183
12265
  {
12184
12266
  className: cn(
12185
12267
  "boff-explore-strip flex min-w-0 flex-1 gap-2",
12186
- expanded ? "flex-wrap" : "flex-nowrap overflow-x-auto"
12268
+ expanded ? "flex-wrap" : "boff-explore-chips-fade flex-nowrap overflow-x-auto"
12187
12269
  ),
12188
12270
  children: [
12189
12271
  /* @__PURE__ */ jsx(
@@ -12321,11 +12403,11 @@ function ConceptStrip({
12321
12403
  onScroll: measureRail,
12322
12404
  "data-boff-explore": "concept-strip",
12323
12405
  "aria-label": T.conceptStripLabel,
12324
- className: "boff-explore-strip flex min-w-0 snap-x gap-3 overflow-x-auto pb-1",
12406
+ className: "boff-explore-strip flex min-w-0 snap-x gap-3 overflow-x-auto pb-1 sm:mx-10",
12325
12407
  children: visible.map((highlight) => /* @__PURE__ */ jsx(
12326
12408
  "li",
12327
12409
  {
12328
- className: "shrink-0 snap-start",
12410
+ className: "flex shrink-0 snap-start",
12329
12411
  children: /* @__PURE__ */ jsxs(
12330
12412
  "button",
12331
12413
  {
@@ -12366,9 +12448,9 @@ function ConceptStrip({
12366
12448
  }
12367
12449
  )
12368
12450
  ] }),
12369
- /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col gap-0.5 p-2.5", children: [
12451
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5 p-2.5", children: [
12370
12452
  /* @__PURE__ */ jsx("span", { className: "line-clamp-2 text-xs font-semibold leading-snug text-card-foreground", children: highlight.title }),
12371
- /* @__PURE__ */ jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: highlight.productName })
12453
+ /* @__PURE__ */ jsx("span", { className: "mt-auto truncate text-[11px] text-muted-foreground", children: highlight.productName })
12372
12454
  ] })
12373
12455
  ]
12374
12456
  }
@@ -12585,6 +12667,7 @@ function ExplorePage({
12585
12667
  const {
12586
12668
  phase,
12587
12669
  catalog,
12670
+ serverCatalogRaw,
12588
12671
  messages,
12589
12672
  activity,
12590
12673
  remainingToday,
@@ -13016,6 +13099,10 @@ function ExplorePage({
13016
13099
  const captchaOn = Boolean(
13017
13100
  catalog.captchaRequired && catalog.recaptchaSiteKey
13018
13101
  );
13102
+ const bakedCatalogJson = useMemo(
13103
+ () => serverCatalogRaw && serverCatalogRaw.enabled !== false ? serializeBakedCatalog(serverCatalogRaw) : null,
13104
+ [serverCatalogRaw]
13105
+ );
13019
13106
  const rootStyle = {};
13020
13107
  if (heightMode === "viewport") {
13021
13108
  rootStyle["--boff-explore-offset"] = paneOffset === null ? normalizeOffset(viewportOffset) : `${paneOffset}px`;
@@ -13055,6 +13142,16 @@ function ExplorePage({
13055
13142
  ),
13056
13143
  /* @__PURE__ */ jsx(ProseStyles, {}),
13057
13144
  /* @__PURE__ */ jsx(ExploreStyles, {}),
13145
+ bakedCatalogJson ? /* @__PURE__ */ jsx(
13146
+ "script",
13147
+ {
13148
+ id: BAKED_CATALOG_ELEMENT_ID,
13149
+ type: "application/json",
13150
+ "data-product": productSlug ?? "",
13151
+ "data-locale": bakedCatalogLocale(),
13152
+ dangerouslySetInnerHTML: { __html: bakedCatalogJson }
13153
+ }
13154
+ ) : null,
13058
13155
  /* @__PURE__ */ jsx(ExploreAnswerStyles, {}),
13059
13156
  !showWelcome ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 border-b border-border px-4 py-2.5", children: [
13060
13157
  /* @__PURE__ */ jsx(
@@ -13529,7 +13626,7 @@ function ExplorePage({
13529
13626
  )
13530
13627
  ] }) : null,
13531
13628
  /* @__PURE__ */ jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [
13532
- /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: T.disclaimerShort }),
13629
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 sm:truncate", children: T.disclaimerShort }),
13533
13630
  /* @__PURE__ */ jsx(
13534
13631
  "button",
13535
13632
  {