@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.
- package/README.md +77 -1
- package/dist/components/explore-cta.d.mts +10 -1
- package/dist/components/explore-cta.d.ts +10 -1
- package/dist/components/explore-cta.js +82 -74
- package/dist/components/explore-cta.js.map +1 -1
- package/dist/components/explore-cta.mjs +83 -75
- package/dist/components/explore-cta.mjs.map +1 -1
- package/dist/components/explore.d.mts +2 -2
- package/dist/components/explore.d.ts +2 -2
- package/dist/components/explore.js +876 -510
- package/dist/components/explore.js.map +1 -1
- package/dist/components/explore.mjs +877 -511
- package/dist/components/explore.mjs.map +1 -1
- package/dist/{explore-POqE7J8r.d.mts → explore-BEgq1_0O.d.mts} +65 -3
- package/dist/{explore-D6kon7uz.d.ts → explore-CHiP9u-4.d.ts} +65 -3
- package/dist/{explore-types-BB8HNFzX.d.mts → explore-types-_9UJu6EA.d.mts} +1 -1
- package/dist/{explore-types-BB8HNFzX.d.ts → explore-types-_9UJu6EA.d.ts} +1 -1
- package/dist/hooks/index.d.mts +2 -2
- package/dist/hooks/index.d.ts +2 -2
- package/dist/hooks/index.js +15 -9
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/index.mjs +15 -9
- package/dist/hooks/index.mjs.map +1 -1
- package/dist/index.d.mts +177 -6
- package/dist/index.d.ts +177 -6
- package/dist/index.js +1236 -152
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1236 -155
- package/dist/index.mjs.map +1 -1
- package/dist/{use-progressive-reveal-Kr6DwGXG.d.mts → use-progressive-reveal-BFPWbEXy.d.mts} +25 -2
- package/dist/{use-progressive-reveal-BVJn9NIr.d.ts → use-progressive-reveal-C0hkdu_i.d.ts} +25 -2
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
"
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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 {
|
|
3
|
-
import '../explore-types-
|
|
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 {
|
|
3
|
-
import '../explore-types-
|
|
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';
|