@burdenoff/website-sdk 2026.1024.1 → 2026.1027.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 (32) hide show
  1. package/README.md +77 -1
  2. package/dist/components/explore-cta.d.mts +10 -1
  3. package/dist/components/explore-cta.d.ts +10 -1
  4. package/dist/components/explore-cta.js +82 -74
  5. package/dist/components/explore-cta.js.map +1 -1
  6. package/dist/components/explore-cta.mjs +83 -75
  7. package/dist/components/explore-cta.mjs.map +1 -1
  8. package/dist/components/explore.d.mts +2 -2
  9. package/dist/components/explore.d.ts +2 -2
  10. package/dist/components/explore.js +876 -510
  11. package/dist/components/explore.js.map +1 -1
  12. package/dist/components/explore.mjs +877 -511
  13. package/dist/components/explore.mjs.map +1 -1
  14. package/dist/{explore-POqE7J8r.d.mts → explore-BEgq1_0O.d.mts} +65 -3
  15. package/dist/{explore-D6kon7uz.d.ts → explore-CHiP9u-4.d.ts} +65 -3
  16. package/dist/{explore-types-BB8HNFzX.d.mts → explore-types-_9UJu6EA.d.mts} +1 -1
  17. package/dist/{explore-types-BB8HNFzX.d.ts → explore-types-_9UJu6EA.d.ts} +1 -1
  18. package/dist/hooks/index.d.mts +2 -2
  19. package/dist/hooks/index.d.ts +2 -2
  20. package/dist/hooks/index.js +15 -9
  21. package/dist/hooks/index.js.map +1 -1
  22. package/dist/hooks/index.mjs +15 -9
  23. package/dist/hooks/index.mjs.map +1 -1
  24. package/dist/index.d.mts +177 -6
  25. package/dist/index.d.ts +177 -6
  26. package/dist/index.js +1236 -152
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.mjs +1236 -155
  29. package/dist/index.mjs.map +1 -1
  30. package/dist/{use-progressive-reveal-Kr6DwGXG.d.mts → use-progressive-reveal-BFPWbEXy.d.mts} +25 -2
  31. package/dist/{use-progressive-reveal-BVJn9NIr.d.ts → use-progressive-reveal-C0hkdu_i.d.ts} +25 -2
  32. package/package.json +1 -1
package/README.md CHANGED
@@ -159,7 +159,9 @@ function Header() {
159
159
  |---|---|---|
160
160
  | `seo` | – | Renders `PageHead` with these overrides. Omit to manage `<head>` yourself. |
161
161
  | `productSlug` | – | The site this page is on. A soft hint to the assistant, not a hard filter. |
162
- | `examplePrompts` | – | Prompt chips used only when the backend catalog supplies none. |
162
+ | `examplePrompts` | – | Prompt chips used only when the backend catalog supplies none. With `scope="product"` they are the only prompts shown, whatever the catalog has. |
163
+ | `scope` | `'portfolio'` | `'product'` (with `productSlug`) makes the assistant one product's own. See [One product's own assistant](#one-products-own-assistant). |
164
+ | `productName` | catalog name | The product's display name in the `product` scope. Default: the catalog's name for `productSlug`. |
163
165
  | `className` | – | Extra classes on the page root. |
164
166
  | `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
167
  | `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. |
@@ -167,6 +169,80 @@ function Header() {
167
169
  | `onSend` | – | Fired with the message text each time a send is accepted (analytics). |
168
170
  | `welcomeTitle` / `welcomeBody` | catalog values | Override the welcome copy. `welcomeTitle` **is** the page `<h1>`. |
169
171
 
172
+ #### One product's own assistant
173
+
174
+ By default the assistant speaks for the whole portfolio: a "Focus on a product"
175
+ picker lists every product, concept cards come from every product, and the
176
+ catalog's copy invites comparing products. A product site that wants its
177
+ assistant to be about that product alone passes `scope="product"` with its
178
+ `productSlug`, on `ExplorePage`, on the site-wide `ExplorePanel`, or on both.
179
+ `ExplorePanel` takes the same `scope`, `productName` and `examplePrompts` props
180
+ and passes them to its chat.
181
+
182
+ ```tsx
183
+ <ExplorePanel
184
+ open={open}
185
+ onOpenChange={setOpen}
186
+ productSlug="healthybowl"
187
+ scope="product"
188
+ productName="Healthy Bowl" // the panel is titled "Healthy Bowl assistant"
189
+ examplePrompts={[
190
+ "Our herd's health records are spread across three notebooks.",
191
+ 'We only find out a field needs water once the crop is stressed.',
192
+ ]}
193
+ />
194
+ ```
195
+
196
+ In that scope:
197
+
198
+ - The panel's title is "{product} assistant", in the visitor's language and its
199
+ word order, named from `productName`, else from the catalog. Where a phone
200
+ leaves it too little room beside the tabs, it wraps to two lines, and the
201
+ header keeps its height. In English and most locales two lines hold the whole
202
+ title at 320px and wider; a narrower title box, such as Tamil's at 320px (its
203
+ tabs are wider), can still end the second line with an ellipsis.
204
+ - There is no product picker. Every question is asked about this product, also
205
+ in a conversation that was stored, reopened or shared with another focus; the
206
+ request carries `productSlug` as it always has.
207
+ - Only this product's concept cards show, and none at all when it has none.
208
+ - The example prompts are your `examplePrompts` and nothing else (each once,
209
+ blank ones dropped), whatever the catalog has. Only when you pass none does it
210
+ fall back to the catalog's, without any that name another product and without
211
+ the built-in "Which of your products fits a problem like mine?". Pass your
212
+ own: the catalog's are written for every site at once, and even the ones that
213
+ name no product describe other sites' problems. Pass them to the
214
+ product-scoped view itself (`ExplorePanel` or `ExplorePage`): an
215
+ `ExploreChatProvider`'s own `examplePrompts` only stand in until the catalog
216
+ loads.
217
+ - Unless you pass `welcomeBody`, the welcome invites the visitor to describe a
218
+ problem or ask what the product does, instead of the catalog's invitation to
219
+ compare products. A catalog title that names another product is replaced by
220
+ the built-in one. The welcome's "Compare across the ecosystem" tile is left
221
+ out.
222
+ - The composer is the published one, unchanged: the same layout, the same size
223
+ and the same fit to its draft. Only its placeholder's text is shorter, "Ask
224
+ about {product}…" instead of "Describe a problem, or ask about {product}…",
225
+ because on a phone the field stays one line tall and cuts off a placeholder
226
+ that wraps. The English one fits that line at 320px and wider. Some longer
227
+ locales, such as Spanish and Tamil, may still wrap at 320px and narrower; that
228
+ is left for a follow-up, and HealthyBowl, the site this was made for, is
229
+ English-only.
230
+ - The page's meta description (and its `og:` and `twitter:` copies) is the
231
+ product's own, "Answers about {product}, drawn from {product}'s
232
+ documentation…", never "and every other product we build". The title and
233
+ `og:site_name` name the product already.
234
+ - A question that needs more searches than one answer can make (`TOOL_BUDGET`)
235
+ is asked to be narrowed to one part of it, in the visitor's language, instead
236
+ of to "two or three specific products".
237
+
238
+ Answers still come from the backend unchanged: when a visitor asks about
239
+ another product, the answer and its sources can mention it. Each view applies
240
+ its own scope, so under an `ExploreChatProvider` a portfolio `/explore` page and
241
+ a product-scoped panel still share one conversation. Without a `productSlug`,
242
+ `scope="product"` has no product to pin to and behaves like the default (a
243
+ development build logs a warning). The default, `scope="portfolio"`, renders
244
+ exactly what it always has.
245
+
170
246
  **`ExploreCta` props**: `variant` (`'header' \| 'mobile' \| 'floating'`, required),
171
247
  `href` (default `/explore`), `label` (default `Ask AI`), `className`,
172
248
  `onNavigate`, `onClick`, `currentPath`. The `floating` variant hides itself on
@@ -20,7 +20,16 @@ interface ExploreCtaProps {
20
20
  * which cannot observe SPA `pushState` navigations on its own.
21
21
  */
22
22
  currentPath?: string;
23
+ /**
24
+ * BOFF-9697. For a host whose floating button opens the site-wide assistant panel (`ExplorePanel`) instead of
25
+ * navigating: whether that panel is open. When set, the link also says it opens a dialog (`aria-haspopup`) and
26
+ * which one (`controls`). Unset (every other site): the link renders exactly as before.
27
+ */
28
+ expanded?: boolean;
29
+ /** BOFF-9697. The id of the panel the button opens; used only together with `expanded`. */
30
+ controls?: string;
23
31
  }
24
- declare function ExploreCta({ variant, href, label, className, onNavigate, onClick, currentPath, }: ExploreCtaProps): react.JSX.Element | null;
32
+ /** The rendered `<a>` is forwarded, so a panel can return focus to the button that opened it (BOFF-9697). */
33
+ declare const ExploreCta: react.ForwardRefExoticComponent<ExploreCtaProps & react.RefAttributes<HTMLAnchorElement>>;
25
34
 
26
35
  export { ExploreCta, type ExploreCtaProps };
@@ -20,7 +20,16 @@ interface ExploreCtaProps {
20
20
  * which cannot observe SPA `pushState` navigations on its own.
21
21
  */
22
22
  currentPath?: string;
23
+ /**
24
+ * BOFF-9697. For a host whose floating button opens the site-wide assistant panel (`ExplorePanel`) instead of
25
+ * navigating: whether that panel is open. When set, the link also says it opens a dialog (`aria-haspopup`) and
26
+ * which one (`controls`). Unset (every other site): the link renders exactly as before.
27
+ */
28
+ expanded?: boolean;
29
+ /** BOFF-9697. The id of the panel the button opens; used only together with `expanded`. */
30
+ controls?: string;
23
31
  }
24
- declare function ExploreCta({ variant, href, label, className, onNavigate, onClick, currentPath, }: ExploreCtaProps): react.JSX.Element | null;
32
+ /** The rendered `<a>` is forwarded, so a panel can return focus to the button that opened it (BOFF-9697). */
33
+ declare const ExploreCta: react.ForwardRefExoticComponent<ExploreCtaProps & react.RefAttributes<HTMLAnchorElement>>;
25
34
 
26
35
  export { ExploreCta, type ExploreCtaProps };
@@ -29,81 +29,89 @@ function samePath(a, b) {
29
29
  const norm = (v) => v.length > 1 ? v.replace(/\/+$/, "") : v;
30
30
  return norm(a) === norm(b);
31
31
  }
32
- function ExploreCta({
33
- variant,
34
- href = "/explore",
35
- label = "Ask AI",
36
- className,
37
- onNavigate,
38
- onClick,
39
- currentPath
40
- }) {
41
- const path = useCurrentPath(currentPath);
42
- const isActive = samePath(path, href);
43
- if (variant === "floating" && isActive) return null;
44
- const handleClick = (event) => {
45
- onClick?.();
46
- if (!onNavigate) return;
47
- if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) {
48
- return;
49
- }
50
- event.preventDefault();
51
- onNavigate(href);
52
- };
53
- const classes = {
54
- // A quiet icon control, deliberately NOT a second gradient pill: a header should carry one
55
- // primary CTA ("Get started"), and a competing coloured button next to it reads as clutter.
56
- // This matches the theme toggle's visual weight, so the row scans as [utilities] [CTA].
57
- // The prominent, labelled entry point to /explore is the floating bubble, which is visible
58
- // on every page without scrolling. `shrink-0` keeps it from squeezing the nav.
59
- header: "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",
60
- mobile: "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",
61
- floating: "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"
62
- };
63
- const hooks = {
64
- header: "cta-header",
65
- mobile: "cta-mobile",
66
- floating: "cta-floating"
67
- };
68
- return /* @__PURE__ */ jsxRuntime.jsxs(
69
- "a",
70
- {
71
- "data-boff-explore": hooks[variant],
72
- href,
73
- onClick: handleClick,
74
- "aria-label": label,
75
- "aria-current": isActive ? "page" : void 0,
76
- className: cn(
77
- classes[variant],
78
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
79
- className
80
- ),
81
- children: [
82
- /* @__PURE__ */ jsxRuntime.jsx(
83
- lucideReact.Sparkles,
84
- {
85
- className: cn(
86
- "shrink-0",
87
- variant === "floating" ? "h-4 w-4" : variant === "header" ? "h-5 w-5" : "h-3.5 w-3.5"
88
- ),
89
- "aria-hidden": "true"
90
- }
32
+ var ExploreCta = react.forwardRef(
33
+ function ExploreCta2({
34
+ variant,
35
+ href = "/explore",
36
+ label = "Ask AI",
37
+ className,
38
+ onNavigate,
39
+ onClick,
40
+ currentPath,
41
+ expanded,
42
+ controls
43
+ }, ref) {
44
+ const path = useCurrentPath(currentPath);
45
+ const isActive = samePath(path, href);
46
+ if (variant === "floating" && isActive) return null;
47
+ const handleClick = (event) => {
48
+ onClick?.();
49
+ if (!onNavigate) return;
50
+ if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) {
51
+ return;
52
+ }
53
+ event.preventDefault();
54
+ onNavigate(href);
55
+ };
56
+ const classes = {
57
+ // A quiet icon control, deliberately NOT a second gradient pill: a header should carry one
58
+ // primary CTA ("Get started"), and a competing coloured button next to it reads as clutter.
59
+ // This matches the theme toggle's visual weight, so the row scans as [utilities] [CTA].
60
+ // The prominent, labelled entry point to /explore is the floating bubble, which is visible
61
+ // on every page without scrolling. `shrink-0` keeps it from squeezing the nav.
62
+ header: "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",
63
+ mobile: "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",
64
+ floating: "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"
65
+ };
66
+ const hooks = {
67
+ header: "cta-header",
68
+ mobile: "cta-mobile",
69
+ floating: "cta-floating"
70
+ };
71
+ return /* @__PURE__ */ jsxRuntime.jsxs(
72
+ "a",
73
+ {
74
+ ref,
75
+ "data-boff-explore": hooks[variant],
76
+ href,
77
+ onClick: handleClick,
78
+ "aria-label": label,
79
+ "aria-current": isActive ? "page" : void 0,
80
+ "aria-expanded": expanded,
81
+ "aria-haspopup": expanded === void 0 ? void 0 : "dialog",
82
+ "aria-controls": expanded === void 0 ? void 0 : controls,
83
+ className: cn(
84
+ classes[variant],
85
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
86
+ className
91
87
  ),
92
- /* @__PURE__ */ jsxRuntime.jsx(
93
- "span",
94
- {
95
- className: cn(
96
- variant === "floating" && "hidden sm:inline",
97
- // Never labelled in the header — see the class comment above.
98
- variant === "header" && "hidden"
99
- ),
100
- children: label
101
- }
102
- )
103
- ]
104
- }
105
- );
106
- }
88
+ children: [
89
+ /* @__PURE__ */ jsxRuntime.jsx(
90
+ lucideReact.Sparkles,
91
+ {
92
+ className: cn(
93
+ "shrink-0",
94
+ variant === "floating" ? "h-4 w-4" : variant === "header" ? "h-5 w-5" : "h-3.5 w-3.5"
95
+ ),
96
+ "aria-hidden": "true"
97
+ }
98
+ ),
99
+ /* @__PURE__ */ jsxRuntime.jsx(
100
+ "span",
101
+ {
102
+ className: cn(
103
+ variant === "floating" && "hidden sm:inline",
104
+ // Never labelled in the header — see the class comment above.
105
+ variant === "header" && "hidden"
106
+ ),
107
+ children: label
108
+ }
109
+ )
110
+ ]
111
+ }
112
+ );
113
+ }
114
+ );
107
115
 
108
116
  exports.ExploreCta = ExploreCta;
109
117
  //# sourceMappingURL=explore-cta.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/cn.ts","../../src/components/explore-cta.tsx"],"names":["twMerge","clsx","useState","useEffect","jsxs","jsx","Sparkles"],"mappings":"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","file":"explore-cta.js","sourcesContent":["/**\n * Utility function to merge Tailwind CSS classes\n * Combines clsx and tailwind-merge for optimal class handling\n */\n\nimport { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge multiple class names intelligently\n * - Handles conditional classes via clsx\n * - Deduplicates and resolves Tailwind conflicts via tailwind-merge\n *\n * @example\n * ```ts\n * cn('px-4 py-2', 'bg-blue-500', { 'text-white': isActive })\n * // => 'px-4 py-2 bg-blue-500 text-white' (if isActive is true)\n * ```\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\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 { useEffect, useState } from \"react\";\n\nimport { Sparkles } from \"lucide-react\";\n\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\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 */\nfunction 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/** `/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\nexport function ExploreCta({\n variant,\n href = \"/explore\",\n label = \"Ask AI\",\n className,\n onNavigate,\n onClick,\n currentPath,\n}: ExploreCtaProps) {\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 data-boff-explore={hooks[variant]}\n href={href}\n onClick={handleClick}\n aria-label={label}\n aria-current={isActive ? \"page\" : undefined}\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"]}
1
+ {"version":3,"sources":["../../src/utils/cn.ts","../../src/components/explore-cta.tsx"],"names":["twMerge","clsx","useState","useEffect","forwardRef","ExploreCta","jsxs","jsx","Sparkles"],"mappings":"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","file":"explore-cta.js","sourcesContent":["/**\n * Utility function to merge Tailwind CSS classes\n * Combines clsx and tailwind-merge for optimal class handling\n */\n\nimport { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge multiple class names intelligently\n * - Handles conditional classes via clsx\n * - Deduplicates and resolves Tailwind conflicts via tailwind-merge\n *\n * @example\n * ```ts\n * cn('px-4 py-2', 'bg-blue-500', { 'text-white': isActive })\n * // => 'px-4 py-2 bg-blue-500 text-white' (if isActive is true)\n * ```\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\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, useEffect, useState } from \"react\";\n\nimport { Sparkles } from \"lucide-react\";\n\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\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 */\nfunction 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/** `/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 },\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 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"]}
@@ -1,4 +1,4 @@
1
- import { useState, useEffect } from 'react';
1
+ import { forwardRef, useState, useEffect } from 'react';
2
2
  import { Sparkles } from 'lucide-react';
3
3
  import { clsx } from 'clsx';
4
4
  import { twMerge } from 'tailwind-merge';
@@ -27,81 +27,89 @@ function samePath(a, b) {
27
27
  const norm = (v) => v.length > 1 ? v.replace(/\/+$/, "") : v;
28
28
  return norm(a) === norm(b);
29
29
  }
30
- function ExploreCta({
31
- variant,
32
- href = "/explore",
33
- label = "Ask AI",
34
- className,
35
- onNavigate,
36
- onClick,
37
- currentPath
38
- }) {
39
- const path = useCurrentPath(currentPath);
40
- const isActive = samePath(path, href);
41
- if (variant === "floating" && isActive) return null;
42
- const handleClick = (event) => {
43
- onClick?.();
44
- if (!onNavigate) return;
45
- if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) {
46
- return;
47
- }
48
- event.preventDefault();
49
- onNavigate(href);
50
- };
51
- const classes = {
52
- // A quiet icon control, deliberately NOT a second gradient pill: a header should carry one
53
- // primary CTA ("Get started"), and a competing coloured button next to it reads as clutter.
54
- // This matches the theme toggle's visual weight, so the row scans as [utilities] [CTA].
55
- // The prominent, labelled entry point to /explore is the floating bubble, which is visible
56
- // on every page without scrolling. `shrink-0` keeps it from squeezing the nav.
57
- header: "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",
58
- mobile: "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",
59
- floating: "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"
60
- };
61
- const hooks = {
62
- header: "cta-header",
63
- mobile: "cta-mobile",
64
- floating: "cta-floating"
65
- };
66
- return /* @__PURE__ */ jsxs(
67
- "a",
68
- {
69
- "data-boff-explore": hooks[variant],
70
- href,
71
- onClick: handleClick,
72
- "aria-label": label,
73
- "aria-current": isActive ? "page" : void 0,
74
- className: cn(
75
- classes[variant],
76
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
77
- className
78
- ),
79
- children: [
80
- /* @__PURE__ */ jsx(
81
- Sparkles,
82
- {
83
- className: cn(
84
- "shrink-0",
85
- variant === "floating" ? "h-4 w-4" : variant === "header" ? "h-5 w-5" : "h-3.5 w-3.5"
86
- ),
87
- "aria-hidden": "true"
88
- }
30
+ var ExploreCta = forwardRef(
31
+ function ExploreCta2({
32
+ variant,
33
+ href = "/explore",
34
+ label = "Ask AI",
35
+ className,
36
+ onNavigate,
37
+ onClick,
38
+ currentPath,
39
+ expanded,
40
+ controls
41
+ }, ref) {
42
+ const path = useCurrentPath(currentPath);
43
+ const isActive = samePath(path, href);
44
+ if (variant === "floating" && isActive) return null;
45
+ const handleClick = (event) => {
46
+ onClick?.();
47
+ if (!onNavigate) return;
48
+ if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) {
49
+ return;
50
+ }
51
+ event.preventDefault();
52
+ onNavigate(href);
53
+ };
54
+ const classes = {
55
+ // A quiet icon control, deliberately NOT a second gradient pill: a header should carry one
56
+ // primary CTA ("Get started"), and a competing coloured button next to it reads as clutter.
57
+ // This matches the theme toggle's visual weight, so the row scans as [utilities] [CTA].
58
+ // The prominent, labelled entry point to /explore is the floating bubble, which is visible
59
+ // on every page without scrolling. `shrink-0` keeps it from squeezing the nav.
60
+ header: "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",
61
+ mobile: "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",
62
+ floating: "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"
63
+ };
64
+ const hooks = {
65
+ header: "cta-header",
66
+ mobile: "cta-mobile",
67
+ floating: "cta-floating"
68
+ };
69
+ return /* @__PURE__ */ jsxs(
70
+ "a",
71
+ {
72
+ ref,
73
+ "data-boff-explore": hooks[variant],
74
+ href,
75
+ onClick: handleClick,
76
+ "aria-label": label,
77
+ "aria-current": isActive ? "page" : void 0,
78
+ "aria-expanded": expanded,
79
+ "aria-haspopup": expanded === void 0 ? void 0 : "dialog",
80
+ "aria-controls": expanded === void 0 ? void 0 : controls,
81
+ className: cn(
82
+ classes[variant],
83
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
84
+ className
89
85
  ),
90
- /* @__PURE__ */ jsx(
91
- "span",
92
- {
93
- className: cn(
94
- variant === "floating" && "hidden sm:inline",
95
- // Never labelled in the header — see the class comment above.
96
- variant === "header" && "hidden"
97
- ),
98
- children: label
99
- }
100
- )
101
- ]
102
- }
103
- );
104
- }
86
+ children: [
87
+ /* @__PURE__ */ jsx(
88
+ Sparkles,
89
+ {
90
+ className: cn(
91
+ "shrink-0",
92
+ variant === "floating" ? "h-4 w-4" : variant === "header" ? "h-5 w-5" : "h-3.5 w-3.5"
93
+ ),
94
+ "aria-hidden": "true"
95
+ }
96
+ ),
97
+ /* @__PURE__ */ jsx(
98
+ "span",
99
+ {
100
+ className: cn(
101
+ variant === "floating" && "hidden sm:inline",
102
+ // Never labelled in the header — see the class comment above.
103
+ variant === "header" && "hidden"
104
+ ),
105
+ children: label
106
+ }
107
+ )
108
+ ]
109
+ }
110
+ );
111
+ }
112
+ );
105
113
 
106
114
  export { ExploreCta };
107
115
  //# sourceMappingURL=explore-cta.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/cn.ts","../../src/components/explore-cta.tsx"],"names":[],"mappings":"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","file":"explore-cta.mjs","sourcesContent":["/**\n * Utility function to merge Tailwind CSS classes\n * Combines clsx and tailwind-merge for optimal class handling\n */\n\nimport { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge multiple class names intelligently\n * - Handles conditional classes via clsx\n * - Deduplicates and resolves Tailwind conflicts via tailwind-merge\n *\n * @example\n * ```ts\n * cn('px-4 py-2', 'bg-blue-500', { 'text-white': isActive })\n * // => 'px-4 py-2 bg-blue-500 text-white' (if isActive is true)\n * ```\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\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 { useEffect, useState } from \"react\";\n\nimport { Sparkles } from \"lucide-react\";\n\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\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 */\nfunction 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/** `/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\nexport function ExploreCta({\n variant,\n href = \"/explore\",\n label = \"Ask AI\",\n className,\n onNavigate,\n onClick,\n currentPath,\n}: ExploreCtaProps) {\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 data-boff-explore={hooks[variant]}\n href={href}\n onClick={handleClick}\n aria-label={label}\n aria-current={isActive ? \"page\" : undefined}\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"]}
1
+ {"version":3,"sources":["../../src/utils/cn.ts","../../src/components/explore-cta.tsx"],"names":["ExploreCta"],"mappings":"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","file":"explore-cta.mjs","sourcesContent":["/**\n * Utility function to merge Tailwind CSS classes\n * Combines clsx and tailwind-merge for optimal class handling\n */\n\nimport { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge multiple class names intelligently\n * - Handles conditional classes via clsx\n * - Deduplicates and resolves Tailwind conflicts via tailwind-merge\n *\n * @example\n * ```ts\n * cn('px-4 py-2', 'bg-blue-500', { 'text-white': isActive })\n * // => 'px-4 py-2 bg-blue-500 text-white' (if isActive is true)\n * ```\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\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, useEffect, useState } from \"react\";\n\nimport { Sparkles } from \"lucide-react\";\n\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\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 */\nfunction 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/** `/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 },\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 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"]}
@@ -1,4 +1,4 @@
1
1
  import 'react';
2
- export { E as EXPLORE_PREFILL_MAX_LENGTH, d as ExplorePage, e as ExplorePageProps, l as ExploreProvenance, m as ExploreReferenceKind, f as answerPlainText, g as answerSpeechText, h as exploreQuestionHref, i as inlineImageOrigins, j as isInlineImageAllowed, k as isInlineVideoAllowed, n as logoUrlOf, o as productNamesByOrigin, r as referenceKindOf, s as summariseReferences } from '../explore-POqE7J8r.mjs';
3
- import '../explore-types-BB8HNFzX.mjs';
2
+ export { a as EXPLORE_PREFILL_MAX_LENGTH, e as ExplorePage, f as ExplorePageProps, m as ExploreProvenance, n as ExploreReferenceKind, g as answerPlainText, h as answerSpeechText, i as exploreQuestionHref, j as inlineImageOrigins, k as isInlineImageAllowed, l as isInlineVideoAllowed, o as logoUrlOf, q as productNamesByOrigin, r as referenceKindOf, s as summariseReferences } from '../explore-BEgq1_0O.mjs';
3
+ import '../explore-types-_9UJu6EA.mjs';
4
4
  import 'react-markdown';
@@ -1,4 +1,4 @@
1
1
  import 'react';
2
- export { E as EXPLORE_PREFILL_MAX_LENGTH, d as ExplorePage, e as ExplorePageProps, l as ExploreProvenance, m as ExploreReferenceKind, f as answerPlainText, g as answerSpeechText, h as exploreQuestionHref, i as inlineImageOrigins, j as isInlineImageAllowed, k as isInlineVideoAllowed, n as logoUrlOf, o as productNamesByOrigin, r as referenceKindOf, s as summariseReferences } from '../explore-D6kon7uz.js';
3
- import '../explore-types-BB8HNFzX.js';
2
+ export { a as EXPLORE_PREFILL_MAX_LENGTH, e as ExplorePage, f as ExplorePageProps, m as ExploreProvenance, n as ExploreReferenceKind, g as answerPlainText, h as answerSpeechText, i as exploreQuestionHref, j as inlineImageOrigins, k as isInlineImageAllowed, l as isInlineVideoAllowed, o as logoUrlOf, q as productNamesByOrigin, r as referenceKindOf, s as summariseReferences } from '../explore-CHiP9u-4.js';
3
+ import '../explore-types-_9UJu6EA.js';
4
4
  import 'react-markdown';