@burdenoff/website-sdk 2026.1101.3 → 2026.1101.4
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/README.md +183 -2
- package/dist/{chunk-Y4V5CEWD.mjs → chunk-2LPDCXQH.mjs} +7 -4
- package/dist/chunk-2LPDCXQH.mjs.map +1 -0
- package/dist/{chunk-FFADMVIK.mjs → chunk-ETI2HQEC.mjs} +7 -5
- package/dist/chunk-ETI2HQEC.mjs.map +1 -0
- package/dist/{chunk-OYEUNZJY.js → chunk-PIXTBL63.js} +7 -3
- package/dist/chunk-PIXTBL63.js.map +1 -0
- package/dist/{chunk-O76QDGRM.js → chunk-R742FS5E.js} +8 -4
- package/dist/chunk-R742FS5E.js.map +1 -0
- package/dist/components/explore-cta.d.mts +7 -0
- package/dist/components/explore-cta.d.ts +7 -0
- package/dist/components/explore-cta.js +2 -2
- package/dist/components/explore-cta.mjs +1 -1
- package/dist/components/explore.js +13 -13
- package/dist/components/explore.mjs +1 -1
- package/dist/index.d.mts +95 -2
- package/dist/index.d.ts +95 -2
- package/dist/index.js +201 -38
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +168 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-FFADMVIK.mjs.map +0 -1
- package/dist/chunk-O76QDGRM.js.map +0 -1
- package/dist/chunk-OYEUNZJY.js.map +0 -1
- package/dist/chunk-Y4V5CEWD.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -446,10 +446,191 @@ Testing hooks (`data-boff-explore`): `avatar-live` (with `data-phase`),
|
|
|
446
446
|
`avatar-live-captions`, `avatar-live-countdown`, `avatar-live-sound`,
|
|
447
447
|
`avatar-live-continue`, `avatar-live-back`, `avatar-live-bot-check`.
|
|
448
448
|
|
|
449
|
+
#### Site assistant dock (`SiteAssistantDock`, every product site)
|
|
450
|
+
|
|
451
|
+
`SiteAssistantDock` is the site-wide assistant in one component: the floating
|
|
452
|
+
"Ask AI" button, which opens the assistant panel (`ExplorePanel`) over the
|
|
453
|
+
current page, and that panel, with the site's product scope and its avatar. It
|
|
454
|
+
replaces `<ExploreCta variant="floating" />` in a site's layout. The behaviour
|
|
455
|
+
is HealthyBowl's, as approved there (BOFF-9697, BOFF-9796):
|
|
456
|
+
|
|
457
|
+
- The button is still a real link to `/explore`: a modified click (new tab) and
|
|
458
|
+
a visitor without JavaScript reach the page; a plain click opens the compact
|
|
459
|
+
panel without leaving the page.
|
|
460
|
+
- Expand and Restore keep the same conversation, and so do closing, reopening
|
|
461
|
+
and going to `/explore` and back: the panel and `/explore` share the
|
|
462
|
+
provider's one conversation. Nothing is asked of a server until the first
|
|
463
|
+
opening; the dock starts the provider's conversation then.
|
|
464
|
+
- Escape and the phone's Back go expanded, then compact, then closed, through
|
|
465
|
+
history entries of the panel's own (the same URL, so the router stays put).
|
|
466
|
+
Closing stops the microphone and reading aloud, and focus goes back to the
|
|
467
|
+
button, which carries `aria-controls` and `aria-expanded`.
|
|
468
|
+
- On `/explore` (`explorePath`, matched as the router matches it: ignoring case
|
|
469
|
+
and a trailing slash) there is no button and the panel hides; an open panel
|
|
470
|
+
shows again on the way out.
|
|
471
|
+
- The panel keeps `bottomOffset` clear at the bottom of the window, so it never
|
|
472
|
+
covers the cookie banner's controls, and the floating button sits that much
|
|
473
|
+
higher (`calc(1.25rem + bottomOffset)`), so the banner does not cover the
|
|
474
|
+
button before the visitor answers it. Unset, the button is where it always was.
|
|
475
|
+
- One dock, one panel, one conversation: render it once, in the layout every
|
|
476
|
+
route shares. A second dock mounted while one is there renders nothing, in
|
|
477
|
+
every build (a development build also logs an error); if the first one
|
|
478
|
+
unmounts, the second takes over.
|
|
479
|
+
|
|
480
|
+
It **needs an `ExploreChatProvider` above it**. Without one a development build
|
|
481
|
+
throws `SiteAssistantDock must be rendered under an ExploreChatProvider`, and a
|
|
482
|
+
production build logs that error and renders the plain floating link to
|
|
483
|
+
`/explore` that sites had before the dock.
|
|
484
|
+
|
|
485
|
+
A site adds these lines.
|
|
486
|
+
|
|
487
|
+
1. **The provider, at the app root**, inside the router and around the routes.
|
|
488
|
+
The dock starts it on the first opening, so `enabled` is for `/explore` only:
|
|
489
|
+
|
|
490
|
+
```tsx
|
|
491
|
+
import { ExploreChatProvider, normalizeRoutePath } from '@burdenoff/website-sdk'
|
|
492
|
+
|
|
493
|
+
function SiteShell() {
|
|
494
|
+
const { pathname, search } = useLocation()
|
|
495
|
+
const onExplore = normalizeRoutePath(pathname) === '/explore'
|
|
496
|
+
// A shared conversation is /explore?c=<token> and nothing else.
|
|
497
|
+
const shareToken = onExplore ? new URLSearchParams(search).get('c') : null
|
|
498
|
+
const pageUrl =
|
|
499
|
+
typeof window === 'undefined' ? undefined : new URL(`${pathname}${search}`, window.location.origin).href
|
|
500
|
+
return (
|
|
501
|
+
<ExploreChatProvider
|
|
502
|
+
key={shareToken ?? ''}
|
|
503
|
+
enabled={onExplore}
|
|
504
|
+
productSlug={PRODUCT_SLUG}
|
|
505
|
+
locale={ASSISTANT.locale}
|
|
506
|
+
pageUrl={pageUrl}
|
|
507
|
+
shareToken={shareToken}
|
|
508
|
+
>
|
|
509
|
+
<AppRoutes />
|
|
510
|
+
</ExploreChatProvider>
|
|
511
|
+
)
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
2. **The config objects** (`src/config/assistant.ts`), built once at module
|
|
516
|
+
scope so the panel's memoised prompt list is not rebuilt on every render:
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
import { websiteAvatarLive } from '@burdenoff/website-sdk'
|
|
520
|
+
import manifest from '@/data/avatar-manifest.<site>.en.json'
|
|
521
|
+
|
|
522
|
+
export const ASSISTANT = {
|
|
523
|
+
productSlug: PRODUCT_SLUG,
|
|
524
|
+
scope: 'product',
|
|
525
|
+
productName: 'Healthy Bowl', // the brand as a reader sees it
|
|
526
|
+
examplePrompts: PROBLEMS.map(({ problem }) => problem), // the questions the manifest lists
|
|
527
|
+
locale: import.meta.env.VITE_DEPLOY_ENV === 'alpha' ? 'en' : undefined,
|
|
528
|
+
} as const
|
|
529
|
+
|
|
530
|
+
export const AVATAR = {
|
|
531
|
+
src: '/avatar-faces/av_stock_001.webp',
|
|
532
|
+
live: websiteAvatarLive({
|
|
533
|
+
deployEnv: import.meta.env.VITE_DEPLOY_ENV,
|
|
534
|
+
siteKey: import.meta.env.VITE_AVATAR_TURNSTILE_SITE_KEY,
|
|
535
|
+
site: '<site>', // as the live avatar service knows it: `healthybowl`
|
|
536
|
+
manifestVersion: manifest.version,
|
|
537
|
+
load: () => import('@burdenoff/website-sdk/avatar-live'),
|
|
538
|
+
}),
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
3. **The dock in the layout**, in place of `<ExploreCta variant="floating" />`:
|
|
543
|
+
|
|
544
|
+
```tsx
|
|
545
|
+
import { SiteAssistantDock } from '@burdenoff/website-sdk'
|
|
546
|
+
|
|
547
|
+
const { pathname } = useLocation()
|
|
548
|
+
const navigate = useNavigate()
|
|
549
|
+
|
|
550
|
+
<SiteAssistantDock
|
|
551
|
+
{...ASSISTANT}
|
|
552
|
+
avatar={AVATAR}
|
|
553
|
+
currentPath={pathname}
|
|
554
|
+
onNavigate={(path) => navigate(path)}
|
|
555
|
+
bottomOffset="var(--hb-cookie-banner-height, 0px)" // YOUR banner's variable, see below
|
|
556
|
+
/>
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
`bottomOffset` must name **the site's own cookie banner's height
|
|
560
|
+
variable**. The SDK sets no such variable and there is no shared name:
|
|
561
|
+
HealthyBowl's banner publishes `--hb-cookie-banner-height`, and another
|
|
562
|
+
site passes whatever its banner publishes. A banner that publishes none
|
|
563
|
+
leaves the panel and the button under it. We recommend every site's banner
|
|
564
|
+
publish its height as a CSS variable on `<html>` while it shows, and remove
|
|
565
|
+
it when it goes:
|
|
566
|
+
|
|
567
|
+
```tsx
|
|
568
|
+
// In the site's cookie banner component.
|
|
569
|
+
const bannerRef = useRef<HTMLDivElement>(null)
|
|
570
|
+
useLayoutEffect(() => {
|
|
571
|
+
const banner = bannerRef.current
|
|
572
|
+
if (!banner) return
|
|
573
|
+
const root = document.documentElement
|
|
574
|
+
const publish = () =>
|
|
575
|
+
root.style.setProperty('--<site>-cookie-banner-height', `${banner.offsetHeight}px`)
|
|
576
|
+
publish()
|
|
577
|
+
const observer = new ResizeObserver(publish) // wrapping text, a resized window
|
|
578
|
+
observer.observe(banner)
|
|
579
|
+
return () => {
|
|
580
|
+
observer.disconnect()
|
|
581
|
+
root.style.removeProperty('--<site>-cookie-banner-height') // answered: nothing to keep clear
|
|
582
|
+
}
|
|
583
|
+
}, [])
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
and keep the `, 0px` fallback in `bottomOffset`, so the dock sits at its
|
|
587
|
+
usual place whenever the banner is not on screen.
|
|
588
|
+
|
|
589
|
+
4. **The build env var** `VITE_AVATAR_TURNSTILE_SITE_KEY`: the site's
|
|
590
|
+
Cloudflare Turnstile site key. `websiteAvatarLive` returns `undefined` (the
|
|
591
|
+
still picture and "Live avatar coming soon", no live avatar) when it is
|
|
592
|
+
unset or empty; on alpha (`VITE_DEPLOY_ENV=alpha`) it accepts Cloudflare's
|
|
593
|
+
always-pass test key `1x00000000000000000000AA` (the alpha gateway runs
|
|
594
|
+
Turnstile in dummy mode) or a real key; everywhere else, production
|
|
595
|
+
included, only a real-looking key (`0x…`, letters, digits, `_` and `-`, 18
|
|
596
|
+
to 64 characters) that is not one of Cloudflare's test keys (`1x…AA`,
|
|
597
|
+
`2x…AB`, `1x…BB`, `2x…BB`, `3x…FF`, or anything of their shape). Install
|
|
598
|
+
`livekit-client` on the site (see [Live avatar](#live-avatar-opt-in-receive-only)),
|
|
599
|
+
and declare the variable in `vite-env.d.ts`.
|
|
600
|
+
`websiteAvatarLive` trusts `VITE_DEPLOY_ENV`, so a production build must
|
|
601
|
+
never be labelled `alpha` (that would let a test key through the check);
|
|
602
|
+
the production gateway's real Turnstile secret would still reject a test
|
|
603
|
+
token, so the live avatar would fail to start rather than run unchecked.
|
|
604
|
+
|
|
605
|
+
5. **The privacy anchor** `/privacy#live-avatar`: the live avatar's consent
|
|
606
|
+
card links there, so the site's privacy page needs an element with
|
|
607
|
+
`id="live-avatar"` describing the live avatar.
|
|
608
|
+
|
|
609
|
+
6. **The still picture** `public/avatar-faces/av_stock_001.webp`: the approved
|
|
610
|
+
avatar picture the Avatar view shows (and shows instead of the live avatar
|
|
611
|
+
when it is off), copied from HealthyBowl's website.
|
|
612
|
+
|
|
613
|
+
**`SiteAssistantDock` props**
|
|
614
|
+
|
|
615
|
+
| Prop | Default | What it does |
|
|
616
|
+
|---|---|---|
|
|
617
|
+
| `productSlug`, `scope`, `productName`, `examplePrompts`, `locale` | – | Passed to the panel's chat unchanged. See [One product's own assistant](#one-products-own-assistant). |
|
|
618
|
+
| `avatar` | – | `{ src, name?, live? }`: the Avatar view's still picture, and `live` (from `websiteAvatarLive`) to opt in to the live avatar. Absent: no Avatar view. |
|
|
619
|
+
| `onNavigate` | – | The router's navigate, for links in the conversation. |
|
|
620
|
+
| `bottomOffset` | `0px` | A CSS length kept clear at the bottom: the site's own cookie banner's height variable (HealthyBowl: `var(--hb-cookie-banner-height, 0px)`). The panel keeps it clear and the floating button sits that much higher. |
|
|
621
|
+
| `className` | – | Extra classes for the floating button. |
|
|
622
|
+
| `panelClassName` | – | Extra classes for the panel. |
|
|
623
|
+
| `currentPath` | `window.location.pathname` | The router's path. Pass it: the fallback cannot see `pushState` navigations. |
|
|
624
|
+
| `explorePath` | `/explore` | The button's link, the route the dock hides on, and the panel's share links. |
|
|
625
|
+
| `label` | `Ask AI` | The button's label and accessible name. |
|
|
626
|
+
| `id` | `boff-explore-panel` | The panel's id, for the button's `aria-controls`. |
|
|
627
|
+
| `zIndex` | `50` | The panel's `z-index`. |
|
|
628
|
+
|
|
449
629
|
**`ExploreCta` props**: `variant` (`'header' \| 'mobile' \| 'floating'`, required),
|
|
450
630
|
`href` (default `/explore`), `label` (default `Ask AI`), `className`,
|
|
451
|
-
`onNavigate`, `onClick`, `currentPath
|
|
452
|
-
|
|
631
|
+
`onNavigate`, `onClick`, `currentPath`, and, for `floating` only,
|
|
632
|
+
`bottomOffset` (a CSS length to lift the bubble by, as on the dock). The
|
|
633
|
+
`floating` variant hides itself on `/explore`. Pass `currentPath` from your router — the fallback
|
|
453
634
|
(`window.location.pathname` + `popstate`) cannot observe SPA `pushState`
|
|
454
635
|
navigations.
|
|
455
636
|
|
|
@@ -18,6 +18,7 @@ function useCurrentPath(explicit) {
|
|
|
18
18
|
}, [explicit]);
|
|
19
19
|
return path;
|
|
20
20
|
}
|
|
21
|
+
var FLOATING_BOTTOM = "1.25rem";
|
|
21
22
|
function samePath(a, b) {
|
|
22
23
|
if (!a) return false;
|
|
23
24
|
const norm = (v) => v.length > 1 ? v.replace(/\/+$/, "") : v;
|
|
@@ -33,7 +34,8 @@ var ExploreCta = forwardRef(
|
|
|
33
34
|
onClick,
|
|
34
35
|
currentPath,
|
|
35
36
|
expanded,
|
|
36
|
-
controls
|
|
37
|
+
controls,
|
|
38
|
+
bottomOffset
|
|
37
39
|
}, ref) {
|
|
38
40
|
const path = useCurrentPath(currentPath);
|
|
39
41
|
const isActive = samePath(path, href);
|
|
@@ -74,6 +76,7 @@ var ExploreCta = forwardRef(
|
|
|
74
76
|
"aria-expanded": expanded,
|
|
75
77
|
"aria-haspopup": expanded === void 0 ? void 0 : "dialog",
|
|
76
78
|
"aria-controls": expanded === void 0 ? void 0 : controls,
|
|
79
|
+
style: variant === "floating" && bottomOffset ? { bottom: `calc(${FLOATING_BOTTOM} + ${bottomOffset})` } : void 0,
|
|
77
80
|
className: cn(
|
|
78
81
|
classes[variant],
|
|
79
82
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
@@ -107,6 +110,6 @@ var ExploreCta = forwardRef(
|
|
|
107
110
|
}
|
|
108
111
|
);
|
|
109
112
|
|
|
110
|
-
export { ExploreCta };
|
|
111
|
-
//# sourceMappingURL=chunk-
|
|
112
|
-
//# sourceMappingURL=chunk-
|
|
113
|
+
export { ExploreCta, useCurrentPath };
|
|
114
|
+
//# sourceMappingURL=chunk-2LPDCXQH.mjs.map
|
|
115
|
+
//# sourceMappingURL=chunk-2LPDCXQH.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/explore-route.ts","../src/components/explore-cta.tsx"],"names":["ExploreCta"],"mappings":"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","file":"chunk-2LPDCXQH.mjs","sourcesContent":["/**\n * The route the page is on, for the SDK's own entry points to `/explore` (`ExploreCta`, `SiteAssistantDock`).\n * Internal: not exported from any entry.\n */\n\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/**\n * Resolve the active path without breaking SSR/prerender: the first render\n * always returns `null` (matching the server output), and the real value lands\n * in an effect. `popstate` covers back/forward; hosts that navigate with\n * `pushState` should pass `currentPath` from their router.\n */\nexport function useCurrentPath(explicit?: string): string | null {\n const [path, setPath] = useState<string | null>(explicit ?? null);\n\n useEffect(() => {\n if (explicit !== undefined) {\n setPath(explicit);\n return;\n }\n if (typeof window === \"undefined\") return;\n const read = () => setPath(window.location.pathname);\n read();\n window.addEventListener(\"popstate\", read);\n return () => window.removeEventListener(\"popstate\", read);\n }, [explicit]);\n\n return path;\n}\n","/**\n * ExploreCta — the entry point to `/explore` from a site's navigation.\n *\n * Three shapes of the same link, so a product site can place it once in the\n * desktop header, once in the mobile menu, and optionally as a floating bubble\n * on content pages, without any of them re-implementing the styling:\n *\n * - `header` — a gradient sparkle pill for the desktop nav.\n * - `mobile` — a compact full-width row for a mobile menu list.\n * - `floating` — a fixed bottom-right bubble; hides itself on `/explore`\n * so it never covers the page it links to.\n *\n * Import from the package ROOT (`@burdenoff/website-sdk`) for consistency with\n * `ExplorePage`, which must be root-imported because of tsup's\n * `splitting: false`. This component holds no context of its own, so a\n * sub-path import also works — but keeping both on the root import is one less\n * footgun for the 38 sites that ship them together.\n *\n * @example\n * ```tsx\n * import { ExploreCta } from '@burdenoff/website-sdk'\n *\n * <ExploreCta variant=\"header\" onNavigate={(href) => navigate(href)} />\n * <ExploreCta variant=\"floating\" currentPath={location.pathname} />\n * ```\n */\n\"use client\";\n\nimport { forwardRef } from \"react\";\n\nimport { Sparkles } from \"lucide-react\";\n\nimport { useCurrentPath } from \"./explore-route\";\nimport { cn } from \"../utils/cn\";\n\nexport interface ExploreCtaProps {\n /** Which of the three presentations to render. */\n variant: \"header\" | \"mobile\" | \"floating\";\n /** Target route. Default `/explore`. */\n href?: string;\n /** Visible label / accessible name. Default `Ask AI`. */\n label?: string;\n /** Extra classes for the rendered element. */\n className?: string;\n /** SPA navigation. When omitted the component renders a plain `<a href>`. */\n onNavigate?: (href: string) => void;\n /** Extra click handler (analytics, closing a mobile menu, …). */\n onClick?: () => void;\n /**\n * Current route, when the host knows it (`useLocation().pathname`).\n * Used to mark the link as `aria-current=\"page\"` and to hide the floating\n * bubble on the Explore page itself. Falls back to `window.location.pathname`,\n * which cannot observe SPA `pushState` navigations on its own.\n */\n currentPath?: string;\n /**\n * BOFF-9697. For a host whose floating button opens the site-wide assistant panel (`ExplorePanel`) instead of\n * navigating: whether that panel is open. When set, the link also says it opens a dialog (`aria-haspopup`) and\n * which one (`controls`). Unset (every other site): the link renders exactly as before.\n */\n expanded?: boolean;\n /** BOFF-9697. The id of the panel the button opens; used only together with `expanded`. */\n controls?: string;\n /**\n * BOFF-9797. `floating` only: a CSS length to lift the bubble by, the site's cookie banner's height (say\n * `var(--hb-cookie-banner-height, 0px)`), so a banner on screen does not cover it. The bubble then sits at\n * `calc(1.25rem + bottomOffset)` from the bottom, set inline, so it wins over a `bottom-*` class in `className`.\n * Unset: the bubble's usual place (`bottom-5`), exactly as before.\n */\n bottomOffset?: string;\n}\n\n/** The floating bubble's own distance from the bottom of the window: `bottom-5`. */\nconst FLOATING_BOTTOM = \"1.25rem\";\n\n/** `/explore` and `/explore/` are the same page. */\nfunction samePath(a: string | null, b: string): boolean {\n if (!a) return false;\n const norm = (v: string) => (v.length > 1 ? v.replace(/\\/+$/, \"\") : v);\n return norm(a) === norm(b);\n}\n\n/** The rendered `<a>` is forwarded, so a panel can return focus to the button that opened it (BOFF-9697). */\nexport const ExploreCta = forwardRef<HTMLAnchorElement, ExploreCtaProps>(\n function ExploreCta(\n {\n variant,\n href = \"/explore\",\n label = \"Ask AI\",\n className,\n onNavigate,\n onClick,\n currentPath,\n expanded,\n controls,\n bottomOffset,\n },\n ref,\n ) {\n const path = useCurrentPath(currentPath);\n const isActive = samePath(path, href);\n\n // The floating bubble exists to get visitors TO /explore. On /explore it is\n // pure obstruction, so it removes itself.\n if (variant === \"floating\" && isActive) return null;\n\n const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {\n onClick?.();\n if (!onNavigate) return;\n // Let the browser handle modified clicks (new tab, download, …).\n if (\n event.defaultPrevented ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey ||\n event.altKey ||\n event.button !== 0\n ) {\n return;\n }\n event.preventDefault();\n onNavigate(href);\n };\n\n const classes: Record<ExploreCtaProps[\"variant\"], string> = {\n // A quiet icon control, deliberately NOT a second gradient pill: a header should carry one\n // primary CTA (\"Get started\"), and a competing coloured button next to it reads as clutter.\n // This matches the theme toggle's visual weight, so the row scans as [utilities] [CTA].\n // The prominent, labelled entry point to /explore is the floating bubble, which is visible\n // on every page without scrolling. `shrink-0` keeps it from squeezing the nav.\n header:\n \"inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground\",\n mobile:\n \"inline-flex w-full items-center gap-2 rounded-lg border border-border bg-card px-3 py-2 text-sm font-medium text-card-foreground transition-colors hover:bg-accent hover:text-accent-foreground\",\n floating:\n \"fixed bottom-5 right-5 z-40 inline-flex items-center gap-2 rounded-full bg-gradient-to-r from-primary to-primary/70 px-4 py-3 text-sm font-medium text-primary-foreground shadow-lg transition-opacity hover:opacity-90\",\n };\n\n const hooks: Record<ExploreCtaProps[\"variant\"], string> = {\n header: \"cta-header\",\n mobile: \"cta-mobile\",\n floating: \"cta-floating\",\n };\n\n return (\n <a\n ref={ref}\n data-boff-explore={hooks[variant]}\n href={href}\n onClick={handleClick}\n aria-label={label}\n aria-current={isActive ? \"page\" : undefined}\n aria-expanded={expanded}\n aria-haspopup={expanded === undefined ? undefined : \"dialog\"}\n aria-controls={expanded === undefined ? undefined : controls}\n style={\n variant === \"floating\" && bottomOffset\n ? { bottom: `calc(${FLOATING_BOTTOM} + ${bottomOffset})` }\n : undefined\n }\n className={cn(\n classes[variant],\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n className,\n )}\n >\n <Sparkles\n className={cn(\n \"shrink-0\",\n variant === \"floating\"\n ? \"h-4 w-4\"\n : variant === \"header\"\n ? \"h-5 w-5\"\n : \"h-3.5 w-3.5\",\n )}\n aria-hidden=\"true\"\n />\n {/* The icon alone carries the affordance where space is tight; `aria-label` carries\n the accessible name at every width, so hiding the text costs nothing to a\n screen reader. Floating shows its label from `sm` up, header from `2xl`. */}\n <span\n className={cn(\n variant === \"floating\" && \"hidden sm:inline\",\n // Never labelled in the header — see the class comment above.\n variant === \"header\" && \"hidden\",\n )}\n >\n {label}\n </span>\n </a>\n );\n },\n);\n"]}
|
|
@@ -1707,6 +1707,7 @@ function ExploreAnswerBody({
|
|
|
1707
1707
|
}
|
|
1708
1708
|
var ExploreConversationContext = createContext(null);
|
|
1709
1709
|
var ExploreCatalogProductsContext = createContext(null);
|
|
1710
|
+
var ExploreConversationStartContext = createContext(null);
|
|
1710
1711
|
function useExploreConversation(options = {}) {
|
|
1711
1712
|
const {
|
|
1712
1713
|
productSlug,
|
|
@@ -1754,13 +1755,14 @@ function ExploreChatProvider({
|
|
|
1754
1755
|
// `enabled` as well as `started`: on the render that first sees `enabled`, the latch above has only been queued.
|
|
1755
1756
|
enabled: enabled || started
|
|
1756
1757
|
});
|
|
1757
|
-
|
|
1758
|
+
const start = useCallback(() => setStarted(true), []);
|
|
1759
|
+
return /* @__PURE__ */ jsx(ExploreConversationStartContext.Provider, { value: start, children: /* @__PURE__ */ jsx(ExploreConversationContext.Provider, { value: conversation, children: /* @__PURE__ */ jsx(
|
|
1758
1760
|
ExploreCatalogProductsContext.Provider,
|
|
1759
1761
|
{
|
|
1760
1762
|
value: conversation.catalog.products,
|
|
1761
1763
|
children
|
|
1762
1764
|
}
|
|
1763
|
-
) });
|
|
1765
|
+
) }) });
|
|
1764
1766
|
}
|
|
1765
1767
|
|
|
1766
1768
|
// src/components/explore-scope.ts
|
|
@@ -5122,6 +5124,6 @@ function ExploreView({
|
|
|
5122
5124
|
);
|
|
5123
5125
|
}
|
|
5124
5126
|
|
|
5125
|
-
export { EXPLORE_LOCALES, EXPLORE_PREFILL_MAX_LENGTH, ExploreAnswerBody, ExploreAnswerStyles, ExploreCatalogProductsContext, ExploreChatProvider, ExploreConversationContext, ExplorePage, answerPlainText, answerSpeechText, exploreQuestionHref, exploreStrings, inlineImageOrigins, isInlineImageAllowed, isInlineVideoAllowed, isRtlLocale, logoUrlOf, pinnedProductSlug, productNameFor, productNamesByOrigin, productSlugsByOrigin, referenceKindOf, resolveExploreLocale, speechLocaleOf, summariseReferences, useExploreConversation };
|
|
5126
|
-
//# sourceMappingURL=chunk-
|
|
5127
|
-
//# sourceMappingURL=chunk-
|
|
5127
|
+
export { EXPLORE_LOCALES, EXPLORE_PREFILL_MAX_LENGTH, ExploreAnswerBody, ExploreAnswerStyles, ExploreCatalogProductsContext, ExploreChatProvider, ExploreConversationContext, ExploreConversationStartContext, ExplorePage, answerPlainText, answerSpeechText, exploreQuestionHref, exploreStrings, inlineImageOrigins, isDevelopmentBuild, isInlineImageAllowed, isInlineVideoAllowed, isRtlLocale, logoUrlOf, pinnedProductSlug, productNameFor, productNamesByOrigin, productSlugsByOrigin, referenceKindOf, resolveExploreLocale, speechLocaleOf, summariseReferences, useExploreConversation };
|
|
5128
|
+
//# sourceMappingURL=chunk-ETI2HQEC.mjs.map
|
|
5129
|
+
//# sourceMappingURL=chunk-ETI2HQEC.mjs.map
|