@burdenoff/website-sdk 2026.830.1 → 2026.922.1

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.
Files changed (60) hide show
  1. package/README.md +114 -3
  2. package/dist/components/contact.js +4 -1
  3. package/dist/components/contact.js.map +1 -1
  4. package/dist/components/contact.mjs +4 -1
  5. package/dist/components/contact.mjs.map +1 -1
  6. package/dist/components/explore.d.mts +24 -3
  7. package/dist/components/explore.d.ts +24 -3
  8. package/dist/components/explore.js +690 -236
  9. package/dist/components/explore.js.map +1 -1
  10. package/dist/components/explore.mjs +690 -238
  11. package/dist/components/explore.mjs.map +1 -1
  12. package/dist/components/newsletter.js +4 -1
  13. package/dist/components/newsletter.js.map +1 -1
  14. package/dist/components/newsletter.mjs +4 -1
  15. package/dist/components/newsletter.mjs.map +1 -1
  16. package/dist/components/partners.js +23 -13
  17. package/dist/components/partners.js.map +1 -1
  18. package/dist/components/partners.mjs +23 -13
  19. package/dist/components/partners.mjs.map +1 -1
  20. package/dist/components/pricing.js +4 -1
  21. package/dist/components/pricing.js.map +1 -1
  22. package/dist/components/pricing.mjs +4 -1
  23. package/dist/components/pricing.mjs.map +1 -1
  24. package/dist/components/unsubscribe.js +4 -1
  25. package/dist/components/unsubscribe.js.map +1 -1
  26. package/dist/components/unsubscribe.mjs +4 -1
  27. package/dist/components/unsubscribe.mjs.map +1 -1
  28. package/dist/components/waitlist.js +4 -1
  29. package/dist/components/waitlist.js.map +1 -1
  30. package/dist/components/waitlist.mjs +4 -1
  31. package/dist/components/waitlist.mjs.map +1 -1
  32. package/dist/content/index.js +23 -13
  33. package/dist/content/index.js.map +1 -1
  34. package/dist/content/index.mjs +23 -13
  35. package/dist/content/index.mjs.map +1 -1
  36. package/dist/{use-explore-chat-B8xKGid5.d.mts → explore-types-CLw19R76.d.mts} +1 -143
  37. package/dist/{use-explore-chat-B8xKGid5.d.ts → explore-types-CLw19R76.d.ts} +1 -143
  38. package/dist/hooks/index.d.mts +2 -1
  39. package/dist/hooks/index.d.ts +2 -1
  40. package/dist/hooks/index.js +4 -1
  41. package/dist/hooks/index.js.map +1 -1
  42. package/dist/hooks/index.mjs +4 -1
  43. package/dist/hooks/index.mjs.map +1 -1
  44. package/dist/index.d.mts +141 -2
  45. package/dist/index.d.ts +141 -2
  46. package/dist/index.js +2025 -400
  47. package/dist/index.js.map +1 -1
  48. package/dist/index.mjs +2021 -403
  49. package/dist/index.mjs.map +1 -1
  50. package/dist/seo/index.js +4 -1
  51. package/dist/seo/index.js.map +1 -1
  52. package/dist/seo/index.mjs +4 -1
  53. package/dist/seo/index.mjs.map +1 -1
  54. package/dist/store/index.js +23 -13
  55. package/dist/store/index.js.map +1 -1
  56. package/dist/store/index.mjs +23 -13
  57. package/dist/store/index.mjs.map +1 -1
  58. package/dist/use-explore-chat-CvW-kyIm.d.mts +145 -0
  59. package/dist/use-explore-chat-DdGBimiz.d.ts +145 -0
  60. package/package.json +1 -1
package/README.md CHANGED
@@ -161,8 +161,8 @@ function Header() {
161
161
  | `productSlug` | – | The site this page is on. A soft hint to the assistant, not a hard filter. |
162
162
  | `examplePrompts` | – | Prompt chips used only when the backend catalog supplies none. |
163
163
  | `className` | – | Extra classes on the page root. |
164
- | `heightMode` | `'viewport'` | `viewport` pins the chat to the screen with an internally scrolling transcript; `auto` lets the page grow (for embedding). |
165
- | `viewportOffset` | `0` | Chrome to subtract in `viewport` mode. A number is pixels; a string is any CSS length (`'4rem'`). |
164
+ | `heightMode` | `'viewport'` | `viewport` pins the chat to the screen with an internally scrolling transcript, and is the only mode in which the component manages the host page's scroll position; `auto` lets the page grow and never touches its host's scrolling (for embedding). |
165
+ | `viewportOffset` | `0` | Chrome to subtract in `viewport` mode, **before the first measurement only**. A number is pixels; a string is any CSS length (`'4rem'`). Once mounted the pane measures its own top and this value stops being used — so an approximate header height is fine, and the on-screen keyboard is handled for you. |
166
166
  | `onNavigate` | – | SPA navigation for internal CTA paths. Without it, CTAs render as plain links. |
167
167
  | `onSend` | – | Fired with the message text each time a send is accepted (analytics). |
168
168
  | `welcomeTitle` / `welcomeBody` | catalog values | Override the welcome copy. `welcomeTitle` **is** the page `<h1>`. |
@@ -191,10 +191,121 @@ with it. This is separate from the v2 checkbox used by `ContactPage`.
191
191
  setFocusProduct, send, stop, retry, reset, canSend }` — if you want to build a
192
192
  different shell around it.
193
193
 
194
+ **Inline concept illustrations.** An answer may embed images from the product
195
+ sites' concept pages (`![alt](url)`, followed by the backend's own caption
196
+ line). They render only once the answer is `COMPLETED` — never from a streaming
197
+ preview — and only when the URL is `https` on a catalog product's website
198
+ (apex or `www.`); anything else renders nothing. Each image is lazy-loaded,
199
+ links to the full file in a new tab (unless the markdown already wraps it in a
200
+ link, which then stays the only link), carries a "Concept illustration" badge,
201
+ and disappears entirely if it fails to load. Nothing to wire on the website.
202
+
194
203
  **Testing hooks.** Stable `data-boff-explore` attributes are attached for the
195
204
  end-to-end suite: `page`, `messages`, `composer`, `chip`, `thinking`, `user`,
196
205
  `assistant` (plus `data-status`), `reference`, `cta` (plus `data-kind`),
197
- `cta-header`, `cta-mobile`, `cta-floating`, `send`, `stop`, `new-chat`, `error`.
206
+ `cta-header`, `cta-mobile`, `cta-floating`, `send`, `stop`, `new-chat`, `error`,
207
+ `inline-image`.
208
+
209
+ ### Concepts (`/concepts` + `/concept/:slug`)
210
+
211
+ `ConceptsPage` and `ConceptDetailPage` render a product's concepts: each one a
212
+ feature or flow, shown through the mockups drawn for it and explained in
213
+ reviewed copy. Every image is labelled an **illustration of the concept, not a
214
+ screenshot of the actual product** (`CONCEPT_ILLUSTRATION_NOTICE`).
215
+
216
+ The data is a generated `src/data/concepts.ts` in each site — never hand-edit
217
+ it; regenerate with
218
+ `python3 ~/products/dev/scripts/concepts/build_site_concepts.py site --product <p>`.
219
+ Both pages take the data as props and make no requests, so they prerender
220
+ completely. Import them from the package **root**, like every other page.
221
+
222
+ ```tsx
223
+ import { Link, Navigate, Route, useParams } from 'react-router'
224
+ import {
225
+ ConceptDetailPage,
226
+ ConceptsPage,
227
+ CONCEPTS_PATH,
228
+ getConceptBySlug,
229
+ type ConceptLinkProps,
230
+ } from '@burdenoff/website-sdk'
231
+ import { CONCEPT_CATEGORIES, CONCEPTS } from '@/data/concepts'
232
+
233
+ const SITE_URL = 'https://vibecontrols.com'
234
+
235
+ // Router adapter, so concept → concept navigation stays client-side.
236
+ // Without `linkComponent` the pages render plain <a href> links.
237
+ function ConceptLink({ href, className, children, 'aria-label': ariaLabel }: ConceptLinkProps) {
238
+ return (
239
+ <Link to={href} className={className} aria-label={ariaLabel}>
240
+ {children}
241
+ </Link>
242
+ )
243
+ }
244
+
245
+ // The site resolves the slug; an unknown one redirects to the landing page.
246
+ function ConceptRoute() {
247
+ const { slug } = useParams()
248
+ const concept = getConceptBySlug(CONCEPTS, slug)
249
+ if (!concept) return <Navigate to={CONCEPTS_PATH} replace />
250
+ return (
251
+ <ConceptDetailPage
252
+ productName="VibeControls"
253
+ siteUrl={SITE_URL}
254
+ concept={concept}
255
+ concepts={CONCEPTS}
256
+ categories={CONCEPT_CATEGORIES}
257
+ linkComponent={ConceptLink}
258
+ />
259
+ )
260
+ }
261
+
262
+ // Routes
263
+ <Route
264
+ path={CONCEPTS_PATH}
265
+ element={
266
+ <ConceptsPage
267
+ productName="VibeControls"
268
+ siteUrl={SITE_URL}
269
+ concepts={CONCEPTS}
270
+ categories={CONCEPT_CATEGORIES}
271
+ linkComponent={ConceptLink}
272
+ />
273
+ }
274
+ />
275
+ <Route path="/concept/:slug" element={<ConceptRoute />} />
276
+ ```
277
+
278
+ **`ConceptsPage` props**: `productName`, `siteUrl` (public origin, for
279
+ canonical/OG/JSON-LD URLs), `concepts` (required); `categories` (section order
280
+ and descriptions — a concept in an unlisted category still renders, last),
281
+ `cta` (rendered in the hero), `linkComponent`, `title` (the `<h1>`, default
282
+ `"<Product> concepts"`), `description` (meta description and hero lead; the
283
+ default is built from the counts and stays within 160 characters), `className`.
284
+ With no concepts at all the page says none are published yet, without the
285
+ filter bar or zero counts.
286
+
287
+ **`ConceptDetailPage` props**: `productName`, `siteUrl`, `concept`, `concepts`
288
+ (all of them — for related concepts and previous / next by `order`);
289
+ `categories`, `cta` (rendered as a band at the end), `linkComponent`,
290
+ `className`.
291
+
292
+ **Helpers**: `CONCEPTS_PATH` (`/concepts`), `CONCEPT_PATH_PREFIX` (`/concept`),
293
+ `conceptPath(slug)`, `getConceptBySlug(concepts, slug)`; types `Concept`,
294
+ `ConceptImage`, `ConceptCategory`, `ConceptLinkComponent`, `ConceptLinkProps`.
295
+
296
+ **SEO and llms.** One `<h1>` per page, a self-canonical URL, absolute OG images
297
+ and JSON-LD (`CollectionPage` + `ItemList` + `BreadcrumbList` on the landing
298
+ page; `ImageGallery` + `BreadcrumbList` on a concept). Add `/concepts` and every
299
+ `/concept/<slug>` to the site's prerender list and sitemap. Only the gallery
300
+ images carry `data-llms-image` (one per image — the hero does not), and each is
301
+ followed directly by its `<figcaption>`, which `scripts/llms.mjs` reads as the
302
+ caption. The lightbox is a native `<dialog>` mounted only while open, so it is
303
+ never in the prerendered HTML.
304
+
305
+ **Testing hooks.** `data-concepts="landing" | "card" (plus data-slug) | "filter"
306
+ | "notice" | "empty" (filters match nothing) | "none" (no concepts at all) |
307
+ "detail" | "category" | "hero" | "illustration" | "related" | "pager" | "cta" |
308
+ "lightbox"`, and `data-concepts-notice` on the notice.
198
309
 
199
310
  ### SEO Component
200
311
 
@@ -290,6 +290,9 @@ function resolveImageUrl(image, base) {
290
290
  return image;
291
291
  }
292
292
  }
293
+ function serializeJsonLd(data) {
294
+ return JSON.stringify(data).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
295
+ }
293
296
  function PageHead({
294
297
  title,
295
298
  description,
@@ -331,7 +334,7 @@ function PageHead({
331
334
  fullImageUrl && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:image", content: fullImageUrl }),
332
335
  twitterHandle && /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "twitter:creator", content: twitterHandle }),
333
336
  /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "theme-color", content: themeColor }),
334
- structuredDataItems.map((data, index) => /* @__PURE__ */ jsxRuntime.jsx("script", { type: "application/ld+json", children: JSON.stringify(data) }, `ld-${index}`)),
337
+ structuredDataItems.map((data, index) => /* @__PURE__ */ jsxRuntime.jsx("script", { type: "application/ld+json", children: serializeJsonLd(data) }, `ld-${index}`)),
335
338
  additionalMeta?.map((meta, index) => /* @__PURE__ */ jsxRuntime.jsx(
336
339
  "meta",
337
340
  {