@burdenoff/website-sdk 2026.925.2 → 2026.926.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 (67) hide show
  1. package/dist/components/contact.js +8 -0
  2. package/dist/components/contact.js.map +1 -1
  3. package/dist/components/contact.mjs +8 -0
  4. package/dist/components/contact.mjs.map +1 -1
  5. package/dist/components/explore.d.mts +1 -1
  6. package/dist/components/explore.d.ts +1 -1
  7. package/dist/components/explore.js +1101 -518
  8. package/dist/components/explore.js.map +1 -1
  9. package/dist/components/explore.mjs +1103 -521
  10. package/dist/components/explore.mjs.map +1 -1
  11. package/dist/components/newsletter.js +8 -0
  12. package/dist/components/newsletter.js.map +1 -1
  13. package/dist/components/newsletter.mjs +8 -0
  14. package/dist/components/newsletter.mjs.map +1 -1
  15. package/dist/components/partners.js +11 -2
  16. package/dist/components/partners.js.map +1 -1
  17. package/dist/components/partners.mjs +11 -2
  18. package/dist/components/partners.mjs.map +1 -1
  19. package/dist/components/resource-links.js +8 -0
  20. package/dist/components/resource-links.js.map +1 -1
  21. package/dist/components/resource-links.mjs +8 -0
  22. package/dist/components/resource-links.mjs.map +1 -1
  23. package/dist/components/social-links.js +8 -0
  24. package/dist/components/social-links.js.map +1 -1
  25. package/dist/components/social-links.mjs +8 -0
  26. package/dist/components/social-links.mjs.map +1 -1
  27. package/dist/components/waitlist.js +8 -0
  28. package/dist/components/waitlist.js.map +1 -1
  29. package/dist/components/waitlist.mjs +8 -0
  30. package/dist/components/waitlist.mjs.map +1 -1
  31. package/dist/content/index.d.mts +1 -1
  32. package/dist/content/index.d.ts +1 -1
  33. package/dist/content/index.js +11 -2
  34. package/dist/content/index.js.map +1 -1
  35. package/dist/content/index.mjs +11 -2
  36. package/dist/content/index.mjs.map +1 -1
  37. package/dist/{explore-C0RFTKAi.d.ts → explore-Cam_ElSJ.d.ts} +70 -12
  38. package/dist/{explore-DzOf7n4m.d.mts → explore-hTQm-_SF.d.mts} +70 -12
  39. package/dist/hooks/index.js +51 -13
  40. package/dist/hooks/index.js.map +1 -1
  41. package/dist/hooks/index.mjs +51 -13
  42. package/dist/hooks/index.mjs.map +1 -1
  43. package/dist/{index-BMWQNZDX.d.mts → index-gm3pBCJT.d.mts} +8 -2
  44. package/dist/{index-BqHF-G-S.d.ts → index-ySbOLSD5.d.ts} +8 -2
  45. package/dist/index.d.mts +32 -11
  46. package/dist/index.d.ts +32 -11
  47. package/dist/index.js +901 -267
  48. package/dist/index.js.map +1 -1
  49. package/dist/index.mjs +892 -269
  50. package/dist/index.mjs.map +1 -1
  51. package/dist/product/index.d.mts +16 -1
  52. package/dist/product/index.d.ts +16 -1
  53. package/dist/product/index.js +61 -17
  54. package/dist/product/index.js.map +1 -1
  55. package/dist/product/index.mjs +61 -18
  56. package/dist/product/index.mjs.map +1 -1
  57. package/dist/store/index.js +11 -2
  58. package/dist/store/index.js.map +1 -1
  59. package/dist/store/index.mjs +11 -2
  60. package/dist/store/index.mjs.map +1 -1
  61. package/dist/utils/index.d.mts +41 -1
  62. package/dist/utils/index.d.ts +41 -1
  63. package/dist/utils/index.js +42 -0
  64. package/dist/utils/index.js.map +1 -1
  65. package/dist/utils/index.mjs +35 -1
  66. package/dist/utils/index.mjs.map +1 -1
  67. package/package.json +1 -1
@@ -1,10 +1,11 @@
1
1
  import * as React from 'react';
2
- import { createContext, useMemo, useState, useRef, useEffect, useCallback, useContext, Children, isValidElement, cloneElement } from 'react';
3
- import { Sparkles, RotateCcw, Link2, History, Trash2, Paperclip, X, Mic, Square, ArrowUp, BookOpenText, Layers, TriangleAlert, ChevronLeft, ChevronRight, ExternalLink, Check, Copy, ThumbsUp, ThumbsDown, Volume2 } from 'lucide-react';
2
+ import { createContext, useMemo, useState, useRef, useEffect, useCallback, useContext, Children, isValidElement, cloneElement, useId } from 'react';
3
+ import { Sparkles, RotateCcw, Link2, History, Trash2, Paperclip, X, Mic, Square, ArrowUp, BookOpenText, Layers, TriangleAlert, ChevronLeft, ChevronRight, ExternalLink, Check, Copy, ThumbsUp, ThumbsDown, Volume2, Film, Play, Minimize, Maximize, VideoOff } from 'lucide-react';
4
+ import { createPortal } from 'react-dom';
5
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
6
  import ReactMarkdown from 'react-markdown';
5
7
  import rehypeSanitize from 'rehype-sanitize';
6
8
  import remarkGfm from 'remark-gfm';
7
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
8
9
  import { clsx } from 'clsx';
9
10
  import { twMerge } from 'tailwind-merge';
10
11
  import { Helmet } from 'react-helmet-async';
@@ -14,478 +15,12 @@ import { cva } from 'class-variance-authority';
14
15
  var __defProp = Object.defineProperty;
15
16
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
16
17
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
17
- function MarkdownLink({
18
- href,
19
- children,
20
- // react-markdown passes the hast node to every override; it is not a DOM attribute.
21
- node: _node,
22
- ...props
23
- }) {
24
- const isExternal = typeof href === "string" && (href.startsWith("http://") || href.startsWith("https://"));
25
- return /* @__PURE__ */ jsx(
26
- "a",
27
- {
28
- href,
29
- ...props,
30
- ...isExternal ? { target: "_blank", rel: "noopener noreferrer" } : { rel: "noopener noreferrer" },
31
- children
32
- }
33
- );
34
- }
35
- var defaultComponents = {
36
- a: MarkdownLink
37
- };
38
- function Markdown({
39
- children,
40
- className,
41
- inline,
42
- components
43
- }) {
44
- const content = /* @__PURE__ */ jsx(
45
- ReactMarkdown,
46
- {
47
- remarkPlugins: [remarkGfm],
48
- rehypePlugins: [rehypeSanitize],
49
- components: { ...defaultComponents, ...components },
50
- ...inline ? { disallowedElements: ["p"], unwrapDisallowed: true } : {},
51
- children
52
- }
53
- );
54
- if (inline) {
55
- return /* @__PURE__ */ jsx("span", { className, children: content });
56
- }
57
- return /* @__PURE__ */ jsx("div", { className, children: content });
58
- }
59
- function cn(...inputs) {
60
- return twMerge(clsx(inputs));
61
- }
62
- var EXPLORE_ANSWER_CSS = `
63
- /* Nothing inside an answer may widen the page. Model output carries long URLs, code and
64
- tables \u2014 each of those overflows a phone by default, and one overflowing child makes the
65
- WHOLE document horizontally scrollable, not just the bubble. */
66
- .boff-explore-body,
67
- .boff-explore-body p,
68
- .boff-explore-body li,
69
- .boff-explore-body a,
70
- .boff-explore-body h1,
71
- .boff-explore-body h2,
72
- .boff-explore-body h3,
73
- /* h4-h6 as well: a published answer page pushes the model's own headings down a level or
74
- two so they nest under the page's, and a long unbroken word is no narrower for it. */
75
- .boff-explore-body h4,
76
- .boff-explore-body h5,
77
- .boff-explore-body h6 {
78
- overflow-wrap: anywhere;
79
- word-break: break-word;
80
- }
81
- .boff-explore-body pre {
82
- overflow-x: auto;
83
- max-width: 100%;
84
- }
85
- .boff-explore-body table {
86
- display: block;
87
- overflow-x: auto;
88
- max-width: 100%;
89
- }
90
- .boff-explore-body img {
91
- max-width: 100%;
92
- height: auto;
93
- }
94
-
95
- /* ---- Comparison and pricing tables ----------------------------------------------------
96
- The harness emits a comparison, and a set of plans, as a GFM table whose FIRST column is
97
- the subject \u2014 the product, or the plan (CONTRACT.md \xA77). Two things have to be true of it:
98
- the page must not scroll sideways because of it, and it has to be readable on a 320px
99
- phone, which a four-column grid of 1px borders is not.
100
-
101
- Wide: a contained scroller. \`max-width: 100%\` keeps the box inside the bubble and
102
- \`overflow-x: auto\` keeps the columns inside the box, so the widest possible table still
103
- cannot widen the document. \`overscroll-behavior-x: contain\` stops a swipe that runs out
104
- of table from chaining to the page \u2014 the same rule the transcript needed. */
105
- .boff-explore-body .boff-explore-table {
106
- max-width: 100%;
107
- overflow-x: auto;
108
- overscroll-behavior-x: contain;
109
- margin: 0.75rem 0;
110
- border: 1px solid hsl(var(--border));
111
- border-radius: 0.75rem;
112
- background-color: hsl(var(--card));
113
- }
114
- .boff-explore-body .boff-explore-table:focus-visible {
115
- outline: 2px solid hsl(var(--ring));
116
- outline-offset: 2px;
117
- }
118
- .boff-explore-body .boff-explore-table table {
119
- /* Beats the blanket \`display: block\` above, which is the unwrapped fallback. */
120
- display: table;
121
- width: 100%;
122
- max-width: none;
123
- margin: 0;
124
- border-collapse: collapse;
125
- font-size: 0.8125rem;
126
- }
127
- .boff-explore-body .boff-explore-table th,
128
- .boff-explore-body .boff-explore-table td {
129
- border: 0;
130
- border-top: 1px solid hsl(var(--border));
131
- padding: 0.5rem 0.75rem;
132
- /* \`start\`, never \`left\`: the page root carries dir="rtl" in Arabic, and a left-aligned
133
- table there reads against the grain of every other line on the page. */
134
- text-align: start;
135
- vertical-align: top;
136
- overflow-wrap: anywhere;
137
- /* Prices in a column line up under each other instead of drifting by digit width. */
138
- font-variant-numeric: tabular-nums;
139
- min-width: 6rem;
140
- }
141
- .boff-explore-body .boff-explore-table thead th {
142
- border-top: 0;
143
- background-color: hsl(var(--muted));
144
- color: hsl(var(--foreground));
145
- font-weight: 600;
146
- white-space: nowrap;
147
- }
148
- /* The first column is what the row is ABOUT \u2014 the product, or the plan name. Reading it as
149
- the row's heading is what makes a price list scannable rather than a grid of strings. */
150
- .boff-explore-body .boff-explore-table tbody td:first-child {
151
- font-weight: 600;
152
- color: hsl(var(--card-foreground));
153
- }
154
18
 
155
- /* Narrow: one card per row. Each cell prints its own column heading, so a value is never
156
- stranded without the question it answers, and the subject cell becomes the card's title.
157
- A media query rather than a measured width \u2014 these pages are serialised post-hydration,
158
- so anything measured here would ship to every phone as static HTML. */
159
- @media (max-width: 30rem) {
160
- .boff-explore-body .boff-explore-table {
161
- overflow-x: visible;
162
- }
163
- .boff-explore-body .boff-explore-table table,
164
- .boff-explore-body .boff-explore-table tbody,
165
- .boff-explore-body .boff-explore-table tr,
166
- .boff-explore-body .boff-explore-table td {
167
- display: block;
168
- width: 100%;
169
- min-width: 0;
170
- }
171
- .boff-explore-body .boff-explore-table thead {
172
- display: none;
173
- }
174
- .boff-explore-body .boff-explore-table tr {
175
- border-top: 1px solid hsl(var(--border));
176
- padding: 0.5rem 0;
177
- }
178
- .boff-explore-body .boff-explore-table tr:first-child {
179
- border-top: 0;
180
- }
181
- .boff-explore-body .boff-explore-table td {
182
- border: 0;
183
- padding: 0.125rem 0.75rem;
184
- }
185
- .boff-explore-body .boff-explore-table td[data-label]::before {
186
- content: attr(data-label);
187
- display: block;
188
- font-size: 0.6875rem;
189
- font-weight: 600;
190
- text-transform: uppercase;
191
- letter-spacing: 0.04em;
192
- color: hsl(var(--muted-foreground));
193
- }
194
- .boff-explore-body .boff-explore-table tbody td:first-child {
195
- font-size: 0.9375rem;
196
- padding-bottom: 0.25rem;
197
- }
198
- .boff-explore-body .boff-explore-table tbody td:first-child::before {
199
- /* The plan's own name is the heading here; repeating "Plan" over it says nothing. */
200
- display: none;
201
- }
202
- }
203
- `;
204
- function ExploreAnswerStyles() {
205
- return /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: EXPLORE_ANSWER_CSS } });
206
- }
207
- function inlineImageOrigins(products) {
208
- const origins = /* @__PURE__ */ new Set();
209
- for (const product of products) {
210
- let url;
211
- try {
212
- url = new URL(product.website);
213
- } catch {
214
- continue;
215
- }
216
- if (url.protocol !== "https:") continue;
217
- const apex = url.hostname.replace(/^www\./, "");
218
- const port = url.port ? `:${url.port}` : "";
219
- origins.add(`https://${apex}${port}`);
220
- origins.add(`https://www.${apex}${port}`);
221
- }
222
- return origins;
223
- }
224
- function isInlineImageAllowed(src, origins) {
225
- if (typeof src !== "string" || src === "") return false;
226
- let url;
227
- try {
228
- url = new URL(src);
229
- } catch {
230
- return false;
231
- }
232
- if (url.protocol !== "https:" || url.username || url.password) return false;
233
- return origins.has(url.origin);
234
- }
235
- var CODE_FENCE = /^\s*(?:```|~~~)/;
236
- var TABLE_ROW_LINE = /^\s{0,3}\|/;
237
- var TABLE_DELIMITER_ROW = /^\s{0,3}\|[\s:|-]*-[\s:|-]*$/;
238
- function readableLine(raw, mode) {
239
- let line = raw;
240
- line = line.replace(/!\[[^\]]*\]\([^)\s]*(?:\s+"[^"]*")?\)/g, "");
241
- line = mode === "speech" ? (
242
- // A voice reading "h-t-t-p-s colon slash slash…" mid-sentence is noise, and the
243
- // prompt asks for two to five links in every answer. The label IS the words.
244
- line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1")
245
- ) : line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1 ($2)");
246
- line = mode === "speech" ? line.replace(/<(https?:\/\/[^>\s]+)>/g, "") : line.replace(/<(https?:\/\/[^>\s]+)>/g, "$1");
247
- line = line.replace(/^\s{0,3}#{1,6}\s+/, "");
248
- line = line.replace(/^\s{0,3}>\s?/, "");
249
- if (/^\s{0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/.test(line)) return "";
250
- line = line.replace(/`([^`]+)`/g, "$1");
251
- line = line.replace(/\*\*([^*]+)\*\*/g, "$1");
252
- line = line.replace(/__([^_]+)__/g, "$1");
253
- line = line.replace(/(^|[^*\w])\*([^*\n]+)\*(?![*\w])/g, "$1$2");
254
- line = line.replace(/(^|[^_\w])_([^_\n]+)_(?![_\w])/g, "$1$2");
255
- return line.replace(/[ \t]+$/, "");
256
- }
257
- function tableCells(row) {
258
- const trimmed = row.trim();
259
- const cells = [];
260
- let cell = "";
261
- for (let at = 0; at < trimmed.length; at += 1) {
262
- const char = trimmed[at] ?? "";
263
- if (char === "\\") {
264
- cell += char + (trimmed[at + 1] ?? "");
265
- at += 1;
266
- continue;
267
- }
268
- if (char === "|") {
269
- cells.push(cell.trim());
270
- cell = "";
271
- continue;
272
- }
273
- cell += char;
274
- }
275
- cells.push(cell.trim());
276
- if (trimmed.startsWith("|") && cells[0] === "") cells.shift();
277
- if (trimmed.endsWith("|") && cells[cells.length - 1] === "") cells.pop();
278
- return cells;
279
- }
280
- function asSentence(text) {
281
- return /[.!?]$/.test(text) ? text : `${text}.`;
282
- }
283
- function spokenTable(rows) {
284
- if (rows.length < 2 || !TABLE_DELIMITER_ROW.test(rows[1] ?? "")) return rows;
285
- const labels = tableCells(rows[0] ?? "");
286
- const spoken = [];
287
- for (const row of rows.slice(2)) {
288
- const parts = [];
289
- tableCells(row).forEach((value, column) => {
290
- if (value === "") return;
291
- const label = column === 0 ? "" : labels[column] ?? "";
292
- parts.push(label === "" ? value : `${label}: ${value}`);
293
- });
294
- if (parts.length > 0) spoken.push(asSentence(parts.join(". ")));
295
- }
296
- return spoken;
297
- }
298
- function readableText(markdown, mode) {
299
- const lines = [];
300
- let inFence = false;
301
- let table = [];
302
- const flushTable = () => {
303
- if (table.length === 0) return;
304
- for (const line of spokenTable(table)) lines.push(readableLine(line, mode));
305
- table = [];
306
- };
307
- for (const raw of markdown.split("\n")) {
308
- if (CODE_FENCE.test(raw)) {
309
- flushTable();
310
- inFence = !inFence;
311
- continue;
312
- }
313
- if (inFence) {
314
- lines.push(raw);
315
- continue;
316
- }
317
- if (mode === "speech" && TABLE_ROW_LINE.test(raw)) {
318
- table.push(raw);
319
- continue;
320
- }
321
- flushTable();
322
- lines.push(readableLine(raw, mode));
323
- }
324
- flushTable();
325
- return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
326
- }
327
- function answerPlainText(markdown) {
328
- return readableText(markdown, "clipboard");
329
- }
330
- function answerSpeechText(markdown) {
331
- return readableText(markdown, "speech");
332
- }
333
- var AnswerScopeContext = createContext(null);
334
- var InsideLinkContext = createContext(false);
335
- function AnswerLink(props) {
336
- return /* @__PURE__ */ jsx(InsideLinkContext.Provider, { value: true, children: /* @__PURE__ */ jsx(MarkdownLink, { ...props }) });
337
- }
338
- var INLINE_IMAGE_FRAME = "relative inline-block max-w-full overflow-hidden rounded-xl border border-border bg-muted align-top shadow-sm";
339
- function InlineConceptImage({ src, alt }) {
340
- const scope = useContext(AnswerScopeContext);
341
- const insideLink = useContext(InsideLinkContext);
342
- if (!scope || !scope.enabled || !isInlineImageAllowed(src, scope.origins) || scope.failed.has(src)) {
343
- return null;
344
- }
345
- const { strings: T, onFailed } = scope;
346
- const picture = /* @__PURE__ */ jsxs(Fragment, { children: [
347
- /* @__PURE__ */ jsx(
348
- "img",
349
- {
350
- src,
351
- alt: alt ?? "",
352
- loading: "lazy",
353
- decoding: "async",
354
- onError: () => onFailed(src),
355
- className: "block h-auto max-h-80 w-auto max-w-full object-contain"
356
- }
357
- ),
358
- /* @__PURE__ */ jsx(
359
- "span",
360
- {
361
- "aria-hidden": "true",
362
- className: "pointer-events-none absolute left-2 top-2 rounded-full border border-border bg-background/90 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-foreground shadow-sm",
363
- children: T.conceptIllustration
364
- }
365
- )
366
- ] });
367
- return /* @__PURE__ */ jsx("span", { "data-boff-explore": "inline-image", className: "my-3 block", children: insideLink ? /* @__PURE__ */ jsx("span", { className: INLINE_IMAGE_FRAME, children: picture }) : /* @__PURE__ */ jsxs(
368
- "a",
369
- {
370
- href: src,
371
- target: "_blank",
372
- rel: "noopener noreferrer",
373
- className: cn(
374
- INLINE_IMAGE_FRAME,
375
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
376
- ),
377
- children: [
378
- picture,
379
- /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
380
- "(",
381
- T.opensInNewTab,
382
- ")"
383
- ] })
384
- ]
385
- }
386
- ) });
387
- }
388
- function markdownText(node) {
389
- if (!node) return "";
390
- if (typeof node.value === "string") return node.value;
391
- return (node.children ?? []).map(markdownText).join("");
392
- }
393
- function hasHeaderCells(node) {
394
- return (node?.children ?? []).some((cell) => cell.tagName === "th");
395
- }
396
- function findHeaderRow(node) {
397
- for (const child of node?.children ?? []) {
398
- if (child.tagName === "tr" && hasHeaderCells(child)) return child;
399
- const nested = findHeaderRow(child);
400
- if (nested !== null) return nested;
401
- }
402
- return null;
403
- }
404
- function tableColumnLabels(node) {
405
- const row = findHeaderRow(node);
406
- if (row === null) return [];
407
- return (row.children ?? []).filter((cell) => cell.tagName === "th").map((cell) => markdownText(cell).trim());
408
- }
409
- var TableColumnsContext = createContext([]);
410
- function AnswerTable({ node, children }) {
411
- const scope = useContext(AnswerScopeContext);
412
- const table = /* @__PURE__ */ jsx(TableColumnsContext.Provider, { value: tableColumnLabels(node), children: /* @__PURE__ */ jsx("table", { children }) });
413
- if (!scope) return table;
414
- return /* @__PURE__ */ jsx(
415
- "div",
416
- {
417
- "data-boff-explore": "table",
418
- role: "region",
419
- "aria-label": scope.strings.answerTable,
420
- tabIndex: 0,
421
- className: "boff-explore-table",
422
- children: table
423
- }
424
- );
425
- }
426
- function AnswerTableRow({
427
- node,
428
- children,
429
- ...props
430
- }) {
431
- const labels = useContext(TableColumnsContext);
432
- if (labels.length === 0 || hasHeaderCells(node)) {
433
- return /* @__PURE__ */ jsx("tr", { ...props, children });
434
- }
435
- let column = 0;
436
- const cells = Children.map(children, (cell) => {
437
- if (!isValidElement(cell)) return cell;
438
- const label = labels[column] ?? "";
439
- column += 1;
440
- if (label === "") return cell;
441
- return cloneElement(cell, {
442
- "data-label": label
443
- });
444
- });
445
- return /* @__PURE__ */ jsx("tr", { ...props, children: cells });
446
- }
447
- var ANSWER_MARKDOWN_COMPONENTS = {
448
- a: AnswerLink,
449
- img: InlineConceptImage,
450
- table: AnswerTable,
451
- tr: AnswerTableRow
452
- };
453
- function ExploreAnswerBody({
454
- children,
455
- strings,
456
- imageOrigins,
457
- enabled = true,
458
- components,
459
- className
460
- }) {
461
- const [failed, setFailed] = useState(() => /* @__PURE__ */ new Set());
462
- const onFailed = useCallback((src) => {
463
- setFailed(
464
- (current) => current.has(src) ? current : new Set(current).add(src)
465
- );
466
- }, []);
467
- const scope = useMemo(
468
- () => ({
469
- strings,
470
- enabled,
471
- origins: imageOrigins,
472
- failed,
473
- onFailed
474
- }),
475
- [strings, enabled, imageOrigins, failed, onFailed]
476
- );
477
- const overrides = useMemo(
478
- () => components ? { ...ANSWER_MARKDOWN_COMPONENTS, ...components } : ANSWER_MARKDOWN_COMPONENTS,
479
- [components]
480
- );
481
- return /* @__PURE__ */ jsx("div", { className: cn("boff-prose boff-explore-body min-w-0", className), children: /* @__PURE__ */ jsx(AnswerScopeContext.Provider, { value: scope, children: /* @__PURE__ */ jsx(Markdown, { components: overrides, children }) }) });
482
- }
483
-
484
- // src/components/explore-i18n.ts
485
- var EXPLORE_LOCALES = ["en", "zh", "hi", "es", "ar", "ta"];
486
- var RTL_LOCALES = /* @__PURE__ */ new Set(["ar"]);
487
- function isRtlLocale(locale) {
488
- return RTL_LOCALES.has(locale);
19
+ // src/components/explore-i18n.ts
20
+ var EXPLORE_LOCALES = ["en", "zh", "hi", "es", "ar", "ta"];
21
+ var RTL_LOCALES = /* @__PURE__ */ new Set(["ar"]);
22
+ function isRtlLocale(locale) {
23
+ return RTL_LOCALES.has(locale);
489
24
  }
490
25
  var EN = {
491
26
  badge: "AI answers, grounded in our documentation",
@@ -568,6 +103,13 @@ var EN = {
568
103
  readAloud: "Read aloud",
569
104
  stopReading: "Stop reading",
570
105
  answerTable: "Table",
106
+ productVideo: "Product video",
107
+ productVideoTitle: "{product} product video",
108
+ playVideo: "Play {title}",
109
+ closeVideo: "Close video",
110
+ enterFullScreen: "Full screen",
111
+ exitFullScreen: "Exit full screen",
112
+ videoUnavailable: "This video can't be played right now.",
571
113
  groundedIn: "What this answer is based on",
572
114
  sourceCountOne: "1 source",
573
115
  sourceCountMany: "{count} sources",
@@ -669,6 +211,13 @@ var ZH = {
669
211
  readAloud: "\u6717\u8BFB",
670
212
  stopReading: "\u505C\u6B62\u6717\u8BFB",
671
213
  answerTable: "\u8868\u683C",
214
+ productVideo: "\u4EA7\u54C1\u89C6\u9891",
215
+ productVideoTitle: "{product} \u4EA7\u54C1\u89C6\u9891",
216
+ playVideo: "\u64AD\u653E {title}",
217
+ closeVideo: "\u5173\u95ED\u89C6\u9891",
218
+ enterFullScreen: "\u5168\u5C4F",
219
+ exitFullScreen: "\u9000\u51FA\u5168\u5C4F",
220
+ videoUnavailable: "\u6682\u65F6\u65E0\u6CD5\u64AD\u653E\u6B64\u89C6\u9891\u3002",
672
221
  groundedIn: "\u8FD9\u4E2A\u56DE\u7B54\u7684\u4F9D\u636E",
673
222
  sourceCountOne: "1 \u4E2A\u6765\u6E90",
674
223
  sourceCountMany: "{count} \u4E2A\u6765\u6E90",
@@ -766,6 +315,13 @@ var HI = {
766
315
  readAloud: "\u091C\u093C\u094B\u0930 \u0938\u0947 \u092A\u0922\u093C\u0947\u0902",
767
316
  stopReading: "\u092A\u0922\u093C\u0928\u093E \u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
768
317
  answerTable: "\u0924\u093E\u0932\u093F\u0915\u093E",
318
+ productVideo: "\u0909\u0924\u094D\u092A\u093E\u0926 \u0935\u0940\u0921\u093F\u092F\u094B",
319
+ productVideoTitle: "{product} \u0909\u0924\u094D\u092A\u093E\u0926 \u0935\u0940\u0921\u093F\u092F\u094B",
320
+ playVideo: "{title} \u091A\u0932\u093E\u090F\u0901",
321
+ closeVideo: "\u0935\u0940\u0921\u093F\u092F\u094B \u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
322
+ enterFullScreen: "\u092A\u0942\u0930\u094D\u0923 \u0938\u094D\u0915\u094D\u0930\u0940\u0928",
323
+ exitFullScreen: "\u092A\u0942\u0930\u094D\u0923 \u0938\u094D\u0915\u094D\u0930\u0940\u0928 \u0938\u0947 \u092C\u093E\u0939\u0930 \u0928\u093F\u0915\u0932\u0947\u0902",
324
+ videoUnavailable: "\u092F\u0939 \u0935\u0940\u0921\u093F\u092F\u094B \u0905\u092D\u0940 \u0928\u0939\u0940\u0902 \u091A\u0932 \u0938\u0915\u0924\u093E\u0964",
769
325
  groundedIn: "\u092F\u0939 \u0909\u0924\u094D\u0924\u0930 \u0915\u093F\u0938 \u092A\u0930 \u0906\u0927\u093E\u0930\u093F\u0924 \u0939\u0948",
770
326
  sourceCountOne: "1 \u0938\u094D\u0930\u094B\u0924",
771
327
  sourceCountMany: "{count} \u0938\u094D\u0930\u094B\u0924",
@@ -867,6 +423,13 @@ var ES = {
867
423
  readAloud: "Leer en voz alta",
868
424
  stopReading: "Dejar de leer",
869
425
  answerTable: "Tabla",
426
+ productVideo: "V\xEDdeo del producto",
427
+ productVideoTitle: "V\xEDdeo de {product}",
428
+ playVideo: "Reproducir {title}",
429
+ closeVideo: "Cerrar v\xEDdeo",
430
+ enterFullScreen: "Pantalla completa",
431
+ exitFullScreen: "Salir de pantalla completa",
432
+ videoUnavailable: "Ahora mismo no se puede reproducir este v\xEDdeo.",
870
433
  groundedIn: "En qu\xE9 se basa esta respuesta",
871
434
  sourceCountOne: "1 fuente",
872
435
  sourceCountMany: "{count} fuentes",
@@ -968,6 +531,13 @@ var AR = {
968
531
  readAloud: "\u0627\u0642\u0631\u0623 \u0628\u0635\u0648\u062A \u0639\u0627\u0644\u064D",
969
532
  stopReading: "\u0625\u064A\u0642\u0627\u0641 \u0627\u0644\u0642\u0631\u0627\u0621\u0629",
970
533
  answerTable: "\u062C\u062F\u0648\u0644",
534
+ productVideo: "\u0641\u064A\u062F\u064A\u0648 \u0627\u0644\u0645\u0646\u062A\u062C",
535
+ productVideoTitle: "\u0641\u064A\u062F\u064A\u0648 {product}",
536
+ playVideo: "\u062A\u0634\u063A\u064A\u0644 {title}",
537
+ closeVideo: "\u0625\u063A\u0644\u0627\u0642 \u0627\u0644\u0641\u064A\u062F\u064A\u0648",
538
+ enterFullScreen: "\u0645\u0644\u0621 \u0627\u0644\u0634\u0627\u0634\u0629",
539
+ exitFullScreen: "\u0627\u0644\u062E\u0631\u0648\u062C \u0645\u0646 \u0645\u0644\u0621 \u0627\u0644\u0634\u0627\u0634\u0629",
540
+ videoUnavailable: "\u062A\u0639\u0630\u0651\u0631 \u062A\u0634\u063A\u064A\u0644 \u0647\u0630\u0627 \u0627\u0644\u0641\u064A\u062F\u064A\u0648 \u0627\u0644\u0622\u0646.",
971
541
  groundedIn: "\u0639\u0644\u0649 \u0645\u0627\u0630\u0627 \u062A\u0633\u062A\u0646\u062F \u0647\u0630\u0647 \u0627\u0644\u0625\u062C\u0627\u0628\u0629",
972
542
  sourceCountOne: "\u0645\u0635\u062F\u0631 \u0648\u0627\u062D\u062F",
973
543
  sourceCountMany: "{count} \u0645\u0635\u0627\u062F\u0631",
@@ -1069,6 +639,13 @@ var TA = {
1069
639
  readAloud: "\u0B9A\u0BA4\u0BCD\u0BA4\u0BAE\u0BBE\u0B95\u0BAA\u0BCD \u0BAA\u0B9F\u0BBF",
1070
640
  stopReading: "\u0BAA\u0B9F\u0BBF\u0BAA\u0BCD\u0BAA\u0BA4\u0BC8 \u0BA8\u0BBF\u0BB1\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1",
1071
641
  answerTable: "\u0B85\u0B9F\u0BCD\u0B9F\u0BB5\u0BA3\u0BC8",
642
+ productVideo: "\u0BA4\u0BAF\u0BBE\u0BB0\u0BBF\u0BAA\u0BCD\u0BAA\u0BC1 \u0BB5\u0BC0\u0B9F\u0BBF\u0BAF\u0BCB",
643
+ productVideoTitle: "{product} \u0BA4\u0BAF\u0BBE\u0BB0\u0BBF\u0BAA\u0BCD\u0BAA\u0BC1 \u0BB5\u0BC0\u0B9F\u0BBF\u0BAF\u0BCB",
644
+ playVideo: "{title} \u0B87\u0BAF\u0B95\u0BCD\u0B95\u0BC1",
645
+ closeVideo: "\u0BB5\u0BC0\u0B9F\u0BBF\u0BAF\u0BCB\u0BB5\u0BC8 \u0BAE\u0BC2\u0B9F\u0BC1",
646
+ enterFullScreen: "\u0BAE\u0BC1\u0BB4\u0BC1\u0BA4\u0BCD \u0BA4\u0BBF\u0BB0\u0BC8",
647
+ exitFullScreen: "\u0BAE\u0BC1\u0BB4\u0BC1\u0BA4\u0BCD \u0BA4\u0BBF\u0BB0\u0BC8\u0BAF\u0BBF\u0BB2\u0BBF\u0BB0\u0BC1\u0BA8\u0BCD\u0BA4\u0BC1 \u0BB5\u0BC6\u0BB3\u0BBF\u0BAF\u0BC7\u0BB1\u0BC1",
648
+ videoUnavailable: "\u0B87\u0BA8\u0BCD\u0BA4 \u0BB5\u0BC0\u0B9F\u0BBF\u0BAF\u0BCB\u0BB5\u0BC8 \u0B87\u0BAA\u0BCD\u0BAA\u0BCB\u0BA4\u0BC1 \u0B87\u0BAF\u0B95\u0BCD\u0B95 \u0BAE\u0BC1\u0B9F\u0BBF\u0BAF\u0BB5\u0BBF\u0BB2\u0BCD\u0BB2\u0BC8.",
1072
649
  groundedIn: "\u0B87\u0BA8\u0BCD\u0BA4\u0BAA\u0BCD \u0BAA\u0BA4\u0BBF\u0BB2\u0BCD \u0B8E\u0BA4\u0BC8 \u0B85\u0B9F\u0BBF\u0BAA\u0BCD\u0BAA\u0B9F\u0BC8\u0BAF\u0BBE\u0B95\u0B95\u0BCD \u0B95\u0BCA\u0BA3\u0BCD\u0B9F\u0BA4\u0BC1",
1073
650
  sourceCountOne: "1 \u0BAE\u0BC2\u0BB2\u0BAE\u0BCD",
1074
651
  sourceCountMany: "{count} \u0BAE\u0BC2\u0BB2\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
@@ -1111,25 +688,977 @@ var SPEECH_TAGS = {
1111
688
  function speechLocaleOf(locale) {
1112
689
  return SPEECH_TAGS[locale];
1113
690
  }
1114
- function resolveExploreLocale(explicit) {
1115
- const candidates = [
1116
- explicit,
1117
- typeof document === "undefined" ? void 0 : document.documentElement.lang,
1118
- typeof navigator === "undefined" ? void 0 : navigator.language
1119
- ];
1120
- for (const candidate of candidates) {
1121
- if (!candidate) continue;
1122
- const primary = candidate.toLowerCase().split("-")[0];
1123
- const match = EXPLORE_LOCALES.find((locale) => locale === primary);
1124
- if (match) return match;
691
+ function resolveExploreLocale(explicit) {
692
+ const candidates = [
693
+ explicit,
694
+ typeof document === "undefined" ? void 0 : document.documentElement.lang,
695
+ typeof navigator === "undefined" ? void 0 : navigator.language
696
+ ];
697
+ for (const candidate of candidates) {
698
+ if (!candidate) continue;
699
+ const primary = candidate.toLowerCase().split("-")[0];
700
+ const match = EXPLORE_LOCALES.find((locale) => locale === primary);
701
+ if (match) return match;
702
+ }
703
+ return "en";
704
+ }
705
+ function exploreStrings(locale) {
706
+ return STRINGS[locale] ?? EN;
707
+ }
708
+ function fill(template, values) {
709
+ return template.replace(/\{(\w+)\}/g, (match, key) => values[key] ?? match);
710
+ }
711
+ function fullscreenSupport() {
712
+ if (typeof document === "undefined") return null;
713
+ if (document.fullscreenEnabled && typeof document.documentElement.requestFullscreen === "function") {
714
+ return "element";
715
+ }
716
+ if (typeof HTMLVideoElement !== "undefined" && "webkitEnterFullscreen" in HTMLVideoElement.prototype) {
717
+ return "video";
718
+ }
719
+ return null;
720
+ }
721
+ function ignoreRejection(result) {
722
+ result?.catch(() => void 0);
723
+ }
724
+ var VIDEO_DIALOG_CSS = `
725
+ .boff-explore-video-panel:fullscreen {
726
+ display: flex;
727
+ flex-direction: column;
728
+ gap: 0.75rem;
729
+ padding: 1rem;
730
+ background-color: hsl(var(--background));
731
+ color: hsl(var(--foreground));
732
+ }
733
+ .boff-explore-video-panel:fullscreen .boff-explore-video-frame {
734
+ display: flex;
735
+ flex: 1 1 auto;
736
+ min-height: 0;
737
+ align-items: center;
738
+ justify-content: center;
739
+ border: 0;
740
+ border-radius: 0;
741
+ box-shadow: none;
742
+ background-color: hsl(var(--background));
743
+ }
744
+ .boff-explore-video-panel:fullscreen video {
745
+ width: 100%;
746
+ height: 100%;
747
+ max-height: none;
748
+ }
749
+ `;
750
+ var DIALOG_BUTTON = "inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-border bg-card text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
751
+ function ExploreVideoDialog({
752
+ videoUrl,
753
+ posterUrl,
754
+ title,
755
+ strings: T,
756
+ dir,
757
+ onClose
758
+ }) {
759
+ const dialogRef = useRef(null);
760
+ const panelRef = useRef(null);
761
+ const videoRef = useRef(null);
762
+ const closeRef = useRef(null);
763
+ const nativeModalRef = useRef(false);
764
+ const titleId = useId();
765
+ const [support] = useState(fullscreenSupport);
766
+ const [fullscreen, setFullscreen] = useState(false);
767
+ const [failed, setFailed] = useState(false);
768
+ useEffect(() => {
769
+ const dialog = dialogRef.current;
770
+ if (!dialog) return;
771
+ if (typeof dialog.showModal === "function") {
772
+ if (!dialog.open) dialog.showModal();
773
+ nativeModalRef.current = true;
774
+ } else {
775
+ dialog.setAttribute("open", "");
776
+ nativeModalRef.current = false;
777
+ }
778
+ closeRef.current?.focus();
779
+ ignoreRejection(videoRef.current?.play());
780
+ return () => {
781
+ if (dialog.open && typeof dialog.close === "function") dialog.close();
782
+ };
783
+ }, []);
784
+ useEffect(() => {
785
+ const sync = () => {
786
+ const current = document.fullscreenElement;
787
+ setFullscreen(
788
+ current !== null && dialogRef.current?.contains(current) === true
789
+ );
790
+ };
791
+ const enter = () => setFullscreen(true);
792
+ const leave = () => setFullscreen(false);
793
+ const video = videoRef.current;
794
+ document.addEventListener("fullscreenchange", sync);
795
+ video?.addEventListener("webkitbeginfullscreen", enter);
796
+ video?.addEventListener("webkitendfullscreen", leave);
797
+ return () => {
798
+ document.removeEventListener("fullscreenchange", sync);
799
+ video?.removeEventListener("webkitbeginfullscreen", enter);
800
+ video?.removeEventListener("webkitendfullscreen", leave);
801
+ };
802
+ }, [failed]);
803
+ const leaveFullscreen = useCallback(() => {
804
+ const current = document.fullscreenElement;
805
+ if (current && dialogRef.current?.contains(current) && typeof document.exitFullscreen === "function") {
806
+ ignoreRejection(document.exitFullscreen());
807
+ }
808
+ const video = videoRef.current;
809
+ if (video?.webkitDisplayingFullscreen) video.webkitExitFullscreen?.();
810
+ }, []);
811
+ const requestClose = useCallback(() => {
812
+ videoRef.current?.pause();
813
+ leaveFullscreen();
814
+ onClose();
815
+ }, [leaveFullscreen, onClose]);
816
+ const toggleFullscreen = useCallback(() => {
817
+ if (fullscreen) {
818
+ leaveFullscreen();
819
+ return;
820
+ }
821
+ if (support === "element") {
822
+ const panel = panelRef.current;
823
+ if (panel) ignoreRejection(panel.requestFullscreen());
824
+ return;
825
+ }
826
+ const video = videoRef.current;
827
+ video?.webkitEnterFullscreen?.();
828
+ }, [fullscreen, leaveFullscreen, support]);
829
+ const handleClose = useCallback(
830
+ (event) => {
831
+ if (event.currentTarget.open) return;
832
+ requestClose();
833
+ },
834
+ [requestClose]
835
+ );
836
+ const handleKeyDown = useCallback(
837
+ (event) => {
838
+ if (event.key === "Escape" && !nativeModalRef.current) {
839
+ event.preventDefault();
840
+ requestClose();
841
+ }
842
+ },
843
+ [requestClose]
844
+ );
845
+ const handleStageClick = useCallback(
846
+ (event) => {
847
+ if (event.target === event.currentTarget) requestClose();
848
+ },
849
+ [requestClose]
850
+ );
851
+ if (typeof document === "undefined") return null;
852
+ const fullscreenLabel = fullscreen ? T.exitFullScreen : T.enterFullScreen;
853
+ return createPortal(
854
+ /* @__PURE__ */ jsxs(
855
+ "dialog",
856
+ {
857
+ ref: dialogRef,
858
+ "data-boff-explore": "video-dialog",
859
+ "aria-labelledby": titleId,
860
+ dir,
861
+ onClose: handleClose,
862
+ onKeyDown: handleKeyDown,
863
+ className: "fixed inset-0 z-50 m-0 h-full max-h-none w-full max-w-none overflow-y-auto overscroll-contain border-0 bg-background/95 p-0 text-foreground backdrop-blur-sm",
864
+ children: [
865
+ /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: VIDEO_DIALOG_CSS } }),
866
+ /* @__PURE__ */ jsx(
867
+ "div",
868
+ {
869
+ onClick: handleStageClick,
870
+ className: "flex min-h-full items-center justify-center p-4 sm:p-6",
871
+ children: /* @__PURE__ */ jsxs(
872
+ "div",
873
+ {
874
+ ref: panelRef,
875
+ className: "boff-explore-video-panel flex w-full min-w-0 max-w-[960px] flex-col gap-3",
876
+ children: [
877
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
878
+ /* @__PURE__ */ jsx(
879
+ "h2",
880
+ {
881
+ id: titleId,
882
+ className: "min-w-0 flex-1 truncate text-sm font-semibold text-foreground sm:text-base",
883
+ children: title
884
+ }
885
+ ),
886
+ support !== null && !failed ? (
887
+ // A plain button whose name says what pressing it does NOW, and no
888
+ // `aria-pressed`: a toggle button's name must not change with its state, or it
889
+ // announces "Exit full screen, pressed" — two answers to one question.
890
+ /* @__PURE__ */ jsx(
891
+ "button",
892
+ {
893
+ type: "button",
894
+ "data-boff-explore": "video-fullscreen",
895
+ "aria-label": fullscreenLabel,
896
+ title: fullscreenLabel,
897
+ onClick: toggleFullscreen,
898
+ className: DIALOG_BUTTON,
899
+ children: fullscreen ? /* @__PURE__ */ jsx(Minimize, { "aria-hidden": "true", className: "h-5 w-5" }) : /* @__PURE__ */ jsx(Maximize, { "aria-hidden": "true", className: "h-5 w-5" })
900
+ }
901
+ )
902
+ ) : null,
903
+ /* @__PURE__ */ jsx(
904
+ "button",
905
+ {
906
+ ref: closeRef,
907
+ type: "button",
908
+ "data-boff-explore": "video-close",
909
+ "aria-label": T.closeVideo,
910
+ title: T.closeVideo,
911
+ onClick: requestClose,
912
+ className: DIALOG_BUTTON,
913
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "h-5 w-5" })
914
+ }
915
+ )
916
+ ] }),
917
+ /* @__PURE__ */ jsx("div", { className: "boff-explore-video-frame overflow-hidden rounded-xl border border-border bg-muted shadow-lg", children: failed ? (
918
+ // A black box with a spinning control tells the visitor nothing; this says
919
+ // what happened, in their language.
920
+ /* @__PURE__ */ jsxs(
921
+ "div",
922
+ {
923
+ role: "alert",
924
+ className: "flex aspect-video w-full flex-col items-center justify-center gap-2 p-6 text-center text-sm text-muted-foreground",
925
+ children: [
926
+ /* @__PURE__ */ jsx(VideoOff, { "aria-hidden": "true", className: "h-6 w-6" }),
927
+ /* @__PURE__ */ jsx("p", { children: T.videoUnavailable })
928
+ ]
929
+ }
930
+ )
931
+ ) : /* @__PURE__ */ jsx(
932
+ "video",
933
+ {
934
+ ref: videoRef,
935
+ "data-boff-explore": "video-player",
936
+ src: videoUrl,
937
+ poster: posterUrl,
938
+ controls: true,
939
+ controlsList: support === "element" ? "nofullscreen" : void 0,
940
+ playsInline: true,
941
+ autoPlay: true,
942
+ preload: "metadata",
943
+ onError: () => setFailed(true),
944
+ className: "block max-h-[80vh] w-full bg-muted object-contain"
945
+ }
946
+ ) })
947
+ ]
948
+ }
949
+ )
950
+ }
951
+ )
952
+ ]
953
+ }
954
+ ),
955
+ document.body
956
+ );
957
+ }
958
+ function MarkdownLink({
959
+ href,
960
+ children,
961
+ // react-markdown passes the hast node to every override; it is not a DOM attribute.
962
+ node: _node,
963
+ ...props
964
+ }) {
965
+ const isExternal = typeof href === "string" && (href.startsWith("http://") || href.startsWith("https://"));
966
+ return /* @__PURE__ */ jsx(
967
+ "a",
968
+ {
969
+ href,
970
+ ...props,
971
+ ...isExternal ? { target: "_blank", rel: "noopener noreferrer" } : { rel: "noopener noreferrer" },
972
+ children
973
+ }
974
+ );
975
+ }
976
+ var defaultComponents = {
977
+ a: MarkdownLink
978
+ };
979
+ function Markdown({
980
+ children,
981
+ className,
982
+ inline,
983
+ components,
984
+ rehypePlugins
985
+ }) {
986
+ const content = /* @__PURE__ */ jsx(
987
+ ReactMarkdown,
988
+ {
989
+ remarkPlugins: [remarkGfm],
990
+ rehypePlugins: [rehypeSanitize, ...rehypePlugins ?? []],
991
+ components: { ...defaultComponents, ...components },
992
+ ...inline ? { disallowedElements: ["p"], unwrapDisallowed: true } : {},
993
+ children
994
+ }
995
+ );
996
+ if (inline) {
997
+ return /* @__PURE__ */ jsx("span", { className, children: content });
998
+ }
999
+ return /* @__PURE__ */ jsx("div", { className, children: content });
1000
+ }
1001
+ function cn(...inputs) {
1002
+ return twMerge(clsx(inputs));
1003
+ }
1004
+ var EXPLORE_ANSWER_CSS = `
1005
+ /* Nothing inside an answer may widen the page. Model output carries long URLs, code and
1006
+ tables \u2014 each of those overflows a phone by default, and one overflowing child makes the
1007
+ WHOLE document horizontally scrollable, not just the bubble. */
1008
+ .boff-explore-body,
1009
+ .boff-explore-body p,
1010
+ .boff-explore-body li,
1011
+ .boff-explore-body a,
1012
+ .boff-explore-body h1,
1013
+ .boff-explore-body h2,
1014
+ .boff-explore-body h3,
1015
+ /* h4-h6 as well: a published answer page pushes the model's own headings down a level or
1016
+ two so they nest under the page's, and a long unbroken word is no narrower for it. */
1017
+ .boff-explore-body h4,
1018
+ .boff-explore-body h5,
1019
+ .boff-explore-body h6 {
1020
+ overflow-wrap: anywhere;
1021
+ word-break: break-word;
1022
+ }
1023
+ .boff-explore-body pre {
1024
+ overflow-x: auto;
1025
+ max-width: 100%;
1026
+ }
1027
+ .boff-explore-body table {
1028
+ display: block;
1029
+ overflow-x: auto;
1030
+ max-width: 100%;
1031
+ }
1032
+ .boff-explore-body img {
1033
+ max-width: 100%;
1034
+ height: auto;
1035
+ }
1036
+ /* A product-video poster fills the card's reserved 16:9 frame and is cropped to it. The
1037
+ blanket \`img { height: auto }\` above \u2014 and ProseStyles' own \u2014 outrank a height utility,
1038
+ and would size it by its own aspect ratio instead, leaving a band of the frame showing
1039
+ whenever a poster is not exactly 16:9. */
1040
+ .boff-explore-body .boff-explore-video-poster {
1041
+ height: 100%;
1042
+ max-width: none;
1043
+ }
1044
+
1045
+ /* ---- Comparison and pricing tables ----------------------------------------------------
1046
+ The harness emits a comparison, and a set of plans, as a GFM table whose FIRST column is
1047
+ the subject \u2014 the product, or the plan (CONTRACT.md \xA77). Two things have to be true of it:
1048
+ the page must not scroll sideways because of it, and it has to be readable on a 320px
1049
+ phone, which a four-column grid of 1px borders is not.
1050
+
1051
+ Wide: a contained scroller. \`max-width: 100%\` keeps the box inside the bubble and
1052
+ \`overflow-x: auto\` keeps the columns inside the box, so the widest possible table still
1053
+ cannot widen the document. \`overscroll-behavior-x: contain\` stops a swipe that runs out
1054
+ of table from chaining to the page \u2014 the same rule the transcript needed. */
1055
+ .boff-explore-body .boff-explore-table {
1056
+ max-width: 100%;
1057
+ overflow-x: auto;
1058
+ overscroll-behavior-x: contain;
1059
+ margin: 0.75rem 0;
1060
+ border: 1px solid hsl(var(--border));
1061
+ border-radius: 0.75rem;
1062
+ background-color: hsl(var(--card));
1063
+ }
1064
+ .boff-explore-body .boff-explore-table:focus-visible {
1065
+ outline: 2px solid hsl(var(--ring));
1066
+ outline-offset: 2px;
1067
+ }
1068
+ .boff-explore-body .boff-explore-table table {
1069
+ /* Beats the blanket \`display: block\` above, which is the unwrapped fallback. */
1070
+ display: table;
1071
+ width: 100%;
1072
+ max-width: none;
1073
+ margin: 0;
1074
+ border-collapse: collapse;
1075
+ font-size: 0.8125rem;
1076
+ }
1077
+ .boff-explore-body .boff-explore-table th,
1078
+ .boff-explore-body .boff-explore-table td {
1079
+ border: 0;
1080
+ border-top: 1px solid hsl(var(--border));
1081
+ padding: 0.5rem 0.75rem;
1082
+ /* \`start\`, never \`left\`: the page root carries dir="rtl" in Arabic, and a left-aligned
1083
+ table there reads against the grain of every other line on the page. */
1084
+ text-align: start;
1085
+ vertical-align: top;
1086
+ overflow-wrap: anywhere;
1087
+ /* Prices in a column line up under each other instead of drifting by digit width. */
1088
+ font-variant-numeric: tabular-nums;
1089
+ min-width: 6rem;
1090
+ }
1091
+ .boff-explore-body .boff-explore-table thead th {
1092
+ border-top: 0;
1093
+ background-color: hsl(var(--muted));
1094
+ color: hsl(var(--foreground));
1095
+ font-weight: 600;
1096
+ white-space: nowrap;
1097
+ }
1098
+ /* The first column is what the row is ABOUT \u2014 the product, or the plan name. Reading it as
1099
+ the row's heading is what makes a price list scannable rather than a grid of strings. */
1100
+ .boff-explore-body .boff-explore-table tbody td:first-child {
1101
+ font-weight: 600;
1102
+ color: hsl(var(--card-foreground));
1103
+ }
1104
+
1105
+ /* Narrow: one card per row. Each cell prints its own column heading, so a value is never
1106
+ stranded without the question it answers, and the subject cell becomes the card's title.
1107
+ A media query rather than a measured width \u2014 these pages are serialised post-hydration,
1108
+ so anything measured here would ship to every phone as static HTML. */
1109
+ @media (max-width: 30rem) {
1110
+ .boff-explore-body .boff-explore-table {
1111
+ overflow-x: visible;
1112
+ }
1113
+ .boff-explore-body .boff-explore-table table,
1114
+ .boff-explore-body .boff-explore-table tbody,
1115
+ .boff-explore-body .boff-explore-table tr,
1116
+ .boff-explore-body .boff-explore-table td {
1117
+ display: block;
1118
+ width: 100%;
1119
+ min-width: 0;
1120
+ }
1121
+ .boff-explore-body .boff-explore-table thead {
1122
+ display: none;
1123
+ }
1124
+ .boff-explore-body .boff-explore-table tr {
1125
+ border-top: 1px solid hsl(var(--border));
1126
+ padding: 0.5rem 0;
1127
+ }
1128
+ .boff-explore-body .boff-explore-table tr:first-child {
1129
+ border-top: 0;
1130
+ }
1131
+ .boff-explore-body .boff-explore-table td {
1132
+ border: 0;
1133
+ padding: 0.125rem 0.75rem;
1134
+ }
1135
+ .boff-explore-body .boff-explore-table td[data-label]::before {
1136
+ content: attr(data-label);
1137
+ display: block;
1138
+ font-size: 0.6875rem;
1139
+ font-weight: 600;
1140
+ text-transform: uppercase;
1141
+ letter-spacing: 0.04em;
1142
+ color: hsl(var(--muted-foreground));
1143
+ }
1144
+ .boff-explore-body .boff-explore-table tbody td:first-child {
1145
+ font-size: 0.9375rem;
1146
+ padding-bottom: 0.25rem;
1147
+ }
1148
+ .boff-explore-body .boff-explore-table tbody td:first-child::before {
1149
+ /* The plan's own name is the heading here; repeating "Plan" over it says nothing. */
1150
+ display: none;
1151
+ }
1152
+ }
1153
+ `;
1154
+ function ExploreAnswerStyles() {
1155
+ return /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: EXPLORE_ANSWER_CSS } });
1156
+ }
1157
+ function inlineImageOrigins(products) {
1158
+ const origins = /* @__PURE__ */ new Set();
1159
+ for (const product of products) {
1160
+ let url;
1161
+ try {
1162
+ url = new URL(product.website);
1163
+ } catch {
1164
+ continue;
1165
+ }
1166
+ if (url.protocol !== "https:") continue;
1167
+ const apex = url.hostname.replace(/^www\./, "");
1168
+ const port = url.port ? `:${url.port}` : "";
1169
+ origins.add(`https://${apex}${port}`);
1170
+ origins.add(`https://www.${apex}${port}`);
1171
+ }
1172
+ return origins;
1173
+ }
1174
+ var VIDEO_FILE_PATH = /\.(?:mp4|webm|m4v|mov|vtt)$/i;
1175
+ function catalogUrlOf(value, origins) {
1176
+ if (typeof value !== "string" || value === "") return null;
1177
+ let url;
1178
+ try {
1179
+ url = new URL(value);
1180
+ } catch {
1181
+ return null;
1182
+ }
1183
+ if (url.protocol !== "https:" || url.username || url.password) return null;
1184
+ return origins.has(url.origin) ? url : null;
1185
+ }
1186
+ function isInlineImageAllowed(src, origins) {
1187
+ const url = catalogUrlOf(src, origins);
1188
+ return url !== null && !VIDEO_FILE_PATH.test(url.pathname);
1189
+ }
1190
+ var INLINE_VIDEO_PATH = /\.(?:mp4|webm)$/i;
1191
+ function isInlineVideoAllowed(href, origins) {
1192
+ const url = catalogUrlOf(href, origins);
1193
+ return url !== null && INLINE_VIDEO_PATH.test(url.pathname);
1194
+ }
1195
+ function apexOriginOf(url) {
1196
+ const apex = url.hostname.replace(/^www\./, "");
1197
+ const port = url.port ? `:${url.port}` : "";
1198
+ return `https://${apex}${port}`;
1199
+ }
1200
+ function productSlugsByOrigin(products) {
1201
+ const slugs = /* @__PURE__ */ new Map();
1202
+ for (const product of products) {
1203
+ let url;
1204
+ try {
1205
+ url = new URL(product.website);
1206
+ } catch {
1207
+ continue;
1208
+ }
1209
+ if (url.protocol !== "https:") continue;
1210
+ const key = apexOriginOf(url);
1211
+ if (!slugs.has(key)) slugs.set(key, product.slug);
1212
+ }
1213
+ return slugs;
1214
+ }
1215
+ function videoProductOf(href, slugs) {
1216
+ const url = new URL(href);
1217
+ return slugs.get(apexOriginOf(url)) ?? url.hostname.replace(/^www\./, "");
1218
+ }
1219
+ function isVideoFileTarget(href) {
1220
+ if (typeof href !== "string") return false;
1221
+ try {
1222
+ return VIDEO_FILE_PATH.test(
1223
+ new URL(href, "https://answer.invalid").pathname
1224
+ );
1225
+ } catch {
1226
+ return false;
1227
+ }
1228
+ }
1229
+ var CODE_FENCE = /^\s*(?:```|~~~)/;
1230
+ var TABLE_ROW_LINE = /^\s{0,3}\|/;
1231
+ var TABLE_DELIMITER_ROW = /^\s{0,3}\|[\s:|-]*-[\s:|-]*$/;
1232
+ var LINKED_IMAGE = /\[!\[([^[\]]*)\]\([^)\s]*(?:\s+"[^"]*")?\)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
1233
+ var IMAGE = /!\[[^[\]]*\]\([^)\s]*(?:\s+"[^"]*")?\)/g;
1234
+ var LINK = /\[([^[\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
1235
+ function readableLine(raw, mode) {
1236
+ let line = raw;
1237
+ if (line.includes("](")) {
1238
+ line = line.replace(LINKED_IMAGE, (_match, alt, href) => {
1239
+ if (mode === "speech") return "";
1240
+ return alt.trim() === "" ? href : `${alt.trim()} (${href})`;
1241
+ });
1242
+ line = line.replace(IMAGE, "");
1243
+ line = mode === "speech" ? (
1244
+ // A voice reading "h-t-t-p-s colon slash slash…" mid-sentence is noise, and the
1245
+ // prompt asks for two to five links in every answer. The label IS the words.
1246
+ line.replace(LINK, "$1")
1247
+ ) : line.replace(LINK, "$1 ($2)");
1248
+ }
1249
+ line = mode === "speech" ? line.replace(/<(https?:\/\/[^>\s]+)>/g, "") : line.replace(/<(https?:\/\/[^>\s]+)>/g, "$1");
1250
+ line = line.replace(/^\s{0,3}#{1,6}\s+/, "");
1251
+ line = line.replace(/^\s{0,3}>\s?/, "");
1252
+ if (/^\s{0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/.test(line)) return "";
1253
+ line = line.replace(/`([^`]+)`/g, "$1");
1254
+ line = line.replace(/\*\*([^*]+)\*\*/g, "$1");
1255
+ line = line.replace(/__([^_]+)__/g, "$1");
1256
+ line = line.replace(/(^|[^*\w])\*([^*\n]+)\*(?![*\w])/g, "$1$2");
1257
+ line = line.replace(/(^|[^_\w])_([^_\n]+)_(?![_\w])/g, "$1$2");
1258
+ return line.replace(/[ \t]+$/, "");
1259
+ }
1260
+ function tableCells(row) {
1261
+ const trimmed = row.trim();
1262
+ const cells = [];
1263
+ let cell = "";
1264
+ for (let at = 0; at < trimmed.length; at += 1) {
1265
+ const char = trimmed[at] ?? "";
1266
+ if (char === "\\") {
1267
+ cell += char + (trimmed[at + 1] ?? "");
1268
+ at += 1;
1269
+ continue;
1270
+ }
1271
+ if (char === "|") {
1272
+ cells.push(cell.trim());
1273
+ cell = "";
1274
+ continue;
1275
+ }
1276
+ cell += char;
1277
+ }
1278
+ cells.push(cell.trim());
1279
+ if (trimmed.startsWith("|") && cells[0] === "") cells.shift();
1280
+ if (trimmed.endsWith("|") && cells[cells.length - 1] === "") cells.pop();
1281
+ return cells;
1282
+ }
1283
+ function asSentence(text) {
1284
+ return /[.!?]$/.test(text) ? text : `${text}.`;
1285
+ }
1286
+ function spokenTable(rows) {
1287
+ if (rows.length < 2 || !TABLE_DELIMITER_ROW.test(rows[1] ?? "")) return rows;
1288
+ const labels = tableCells(rows[0] ?? "");
1289
+ const spoken = [];
1290
+ for (const row of rows.slice(2)) {
1291
+ const parts = [];
1292
+ tableCells(row).forEach((value, column) => {
1293
+ if (value === "") return;
1294
+ const label = column === 0 ? "" : labels[column] ?? "";
1295
+ parts.push(label === "" ? value : `${label}: ${value}`);
1296
+ });
1297
+ if (parts.length > 0) spoken.push(asSentence(parts.join(". ")));
1298
+ }
1299
+ return spoken;
1300
+ }
1301
+ function readableText(markdown, mode) {
1302
+ const lines = [];
1303
+ let inFence = false;
1304
+ let table = [];
1305
+ const flushTable = () => {
1306
+ if (table.length === 0) return;
1307
+ for (const line of spokenTable(table)) lines.push(readableLine(line, mode));
1308
+ table = [];
1309
+ };
1310
+ for (const raw of markdown.split("\n")) {
1311
+ if (CODE_FENCE.test(raw)) {
1312
+ flushTable();
1313
+ inFence = !inFence;
1314
+ continue;
1315
+ }
1316
+ if (inFence) {
1317
+ lines.push(raw);
1318
+ continue;
1319
+ }
1320
+ if (mode === "speech" && TABLE_ROW_LINE.test(raw)) {
1321
+ table.push(raw);
1322
+ continue;
1323
+ }
1324
+ flushTable();
1325
+ lines.push(readableLine(raw, mode));
1326
+ }
1327
+ flushTable();
1328
+ return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
1329
+ }
1330
+ function answerPlainText(markdown) {
1331
+ return readableText(markdown, "clipboard");
1332
+ }
1333
+ function answerSpeechText(markdown) {
1334
+ return readableText(markdown, "speech");
1335
+ }
1336
+ var AnswerScopeContext = createContext(null);
1337
+ var InsideLinkContext = createContext(false);
1338
+ function AnswerLink(props) {
1339
+ const scope = useContext(AnswerScopeContext);
1340
+ const role = videoRoleOf(props.node);
1341
+ if (role !== void 0 && scope !== null) {
1342
+ const video = role === "card" && scope.enabled ? productVideoOf(props.node) : null;
1343
+ if (video === null) return null;
1344
+ return /* @__PURE__ */ jsx(
1345
+ InlineVideoCard,
1346
+ {
1347
+ href: video.href,
1348
+ poster: video.poster,
1349
+ alt: video.alt,
1350
+ scope
1351
+ }
1352
+ );
1353
+ }
1354
+ return /* @__PURE__ */ jsx(InsideLinkContext.Provider, { value: true, children: /* @__PURE__ */ jsx(MarkdownLink, { ...props }) });
1355
+ }
1356
+ var INLINE_IMAGE_FRAME = "relative inline-block max-w-full overflow-hidden rounded-xl border border-border bg-muted align-top shadow-sm";
1357
+ function InlineConceptImage({ src, alt }) {
1358
+ const scope = useContext(AnswerScopeContext);
1359
+ const insideLink = useContext(InsideLinkContext);
1360
+ if (!scope || !scope.enabled || !isInlineImageAllowed(src, scope.origins) || scope.failed.has(src)) {
1361
+ return null;
1125
1362
  }
1126
- return "en";
1363
+ const { strings: T, onFailed } = scope;
1364
+ const picture = /* @__PURE__ */ jsxs(Fragment, { children: [
1365
+ /* @__PURE__ */ jsx(
1366
+ "img",
1367
+ {
1368
+ src,
1369
+ alt: alt ?? "",
1370
+ loading: "lazy",
1371
+ decoding: "async",
1372
+ onError: () => onFailed(src),
1373
+ className: "block h-auto max-h-80 w-auto max-w-full object-contain"
1374
+ }
1375
+ ),
1376
+ /* @__PURE__ */ jsx(
1377
+ "span",
1378
+ {
1379
+ "aria-hidden": "true",
1380
+ className: "pointer-events-none absolute left-2 top-2 rounded-full border border-border bg-background/90 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-foreground shadow-sm",
1381
+ children: T.conceptIllustration
1382
+ }
1383
+ )
1384
+ ] });
1385
+ return /* @__PURE__ */ jsx("span", { "data-boff-explore": "inline-image", className: "my-3 block", children: insideLink ? /* @__PURE__ */ jsx("span", { className: INLINE_IMAGE_FRAME, children: picture }) : /* @__PURE__ */ jsxs(
1386
+ "a",
1387
+ {
1388
+ href: src,
1389
+ target: "_blank",
1390
+ rel: "noopener noreferrer",
1391
+ className: cn(
1392
+ INLINE_IMAGE_FRAME,
1393
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1394
+ ),
1395
+ children: [
1396
+ picture,
1397
+ /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
1398
+ "(",
1399
+ T.opensInNewTab,
1400
+ ")"
1401
+ ] })
1402
+ ]
1403
+ }
1404
+ ) });
1127
1405
  }
1128
- function exploreStrings(locale) {
1129
- return STRINGS[locale] ?? EN;
1406
+ function markdownText(node) {
1407
+ if (!node) return "";
1408
+ if (typeof node.value === "string") return node.value;
1409
+ return (node.children ?? []).map(markdownText).join("");
1130
1410
  }
1131
- function fill(template, values) {
1132
- return template.replace(/\{(\w+)\}/g, (match, key) => values[key] ?? match);
1411
+ function hasHeaderCells(node) {
1412
+ return (node?.children ?? []).some((cell) => cell.tagName === "th");
1413
+ }
1414
+ function findHeaderRow(node) {
1415
+ for (const child of node?.children ?? []) {
1416
+ if (child.tagName === "tr" && hasHeaderCells(child)) return child;
1417
+ const nested = findHeaderRow(child);
1418
+ if (nested !== null) return nested;
1419
+ }
1420
+ return null;
1421
+ }
1422
+ function tableColumnLabels(node) {
1423
+ const row = findHeaderRow(node);
1424
+ if (row === null) return [];
1425
+ return (row.children ?? []).filter((cell) => cell.tagName === "th").map((cell) => markdownText(cell).trim());
1426
+ }
1427
+ var TableColumnsContext = createContext([]);
1428
+ function AnswerTable({ node, children }) {
1429
+ const scope = useContext(AnswerScopeContext);
1430
+ const table = /* @__PURE__ */ jsx(TableColumnsContext.Provider, { value: tableColumnLabels(node), children: /* @__PURE__ */ jsx("table", { children }) });
1431
+ if (!scope) return table;
1432
+ return /* @__PURE__ */ jsx(
1433
+ "div",
1434
+ {
1435
+ "data-boff-explore": "table",
1436
+ role: "region",
1437
+ "aria-label": scope.strings.answerTable,
1438
+ tabIndex: 0,
1439
+ className: "boff-explore-table",
1440
+ children: table
1441
+ }
1442
+ );
1443
+ }
1444
+ function AnswerTableRow({
1445
+ node,
1446
+ children,
1447
+ ...props
1448
+ }) {
1449
+ const labels = useContext(TableColumnsContext);
1450
+ if (labels.length === 0 || hasHeaderCells(node)) {
1451
+ return /* @__PURE__ */ jsx("tr", { ...props, children });
1452
+ }
1453
+ let column = 0;
1454
+ const cells = Children.map(children, (cell) => {
1455
+ if (!isValidElement(cell)) return cell;
1456
+ const label = labels[column] ?? "";
1457
+ column += 1;
1458
+ if (label === "") return cell;
1459
+ return cloneElement(cell, {
1460
+ "data-label": label
1461
+ });
1462
+ });
1463
+ return /* @__PURE__ */ jsx("tr", { ...props, children: cells });
1464
+ }
1465
+ function linkedImageOf(node) {
1466
+ if (node?.tagName !== "a") return null;
1467
+ const content = (node.children ?? []).filter(
1468
+ (child) => child.tagName !== void 0 || (child.value ?? "").trim() !== ""
1469
+ );
1470
+ const image = content.length === 1 ? content[0] : void 0;
1471
+ if (image?.tagName !== "img") return null;
1472
+ const alt = image.properties?.alt;
1473
+ return {
1474
+ href: node.properties?.href,
1475
+ src: image.properties?.src,
1476
+ alt: typeof alt === "string" ? alt.trim() : ""
1477
+ };
1478
+ }
1479
+ function isCardableVideo(embed, origins) {
1480
+ if (!isInlineVideoAllowed(embed.href, origins) || !isInlineImageAllowed(embed.src, origins)) {
1481
+ return false;
1482
+ }
1483
+ return apexOriginOf(new URL(embed.href)) === apexOriginOf(new URL(embed.src));
1484
+ }
1485
+ var NO_VIDEO_CONTAINERS = /* @__PURE__ */ new Set(["li", "table"]);
1486
+ function rehypeProductVideo(options) {
1487
+ return (tree) => {
1488
+ let claimed = false;
1489
+ const visit = (node, parent, contained) => {
1490
+ const embed = linkedImageOf(node);
1491
+ if (embed !== null && isVideoFileTarget(embed.href)) {
1492
+ const card = !claimed && !contained && parent?.tagName === "p" && isCardableVideo(embed, options.origins);
1493
+ if (card) claimed = true;
1494
+ const data = typeof node.data === "object" ? node.data : null;
1495
+ node.data = { ...data, boffVideo: card ? "card" : "drop" };
1496
+ return;
1497
+ }
1498
+ const inside = contained || node.tagName !== void 0 && NO_VIDEO_CONTAINERS.has(node.tagName);
1499
+ for (const child of node.children ?? []) visit(child, node, inside);
1500
+ };
1501
+ visit(tree, null, false);
1502
+ };
1503
+ }
1504
+ function videoRoleOf(node) {
1505
+ const data = node?.data;
1506
+ if (typeof data !== "object" || data === null || !("boffVideo" in data)) {
1507
+ return void 0;
1508
+ }
1509
+ return data.boffVideo === "card" || data.boffVideo === "drop" ? data.boffVideo : void 0;
1510
+ }
1511
+ function productVideoOf(node) {
1512
+ const embed = linkedImageOf(node);
1513
+ if (embed === null || typeof embed.href !== "string" || typeof embed.src !== "string") {
1514
+ return null;
1515
+ }
1516
+ return { href: embed.href, poster: embed.src, alt: embed.alt };
1517
+ }
1518
+ var CANONICAL_VIDEO_ALT = /^(.+?)\s+product video$/i;
1519
+ function videoTitleOf(alt, T) {
1520
+ const product = CANONICAL_VIDEO_ALT.exec(alt)?.[1]?.trim();
1521
+ if (product) return fill(T.productVideoTitle, { product });
1522
+ return alt === "" ? T.productVideo : alt;
1523
+ }
1524
+ var INLINE_VIDEO_FRAME = "group relative block aspect-video w-full max-w-md overflow-hidden rounded-xl border border-border bg-muted shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
1525
+ function InlineVideoCard({
1526
+ href,
1527
+ poster,
1528
+ alt,
1529
+ scope
1530
+ }) {
1531
+ const { strings: T, failed, onFailed, productSlugs } = scope;
1532
+ const [dialogDir, setDialogDir] = useState(null);
1533
+ const buttonRef = useRef(null);
1534
+ const returnFocusRef = useRef(false);
1535
+ const open = dialogDir !== null;
1536
+ useEffect(() => {
1537
+ if (open || !returnFocusRef.current) return;
1538
+ returnFocusRef.current = false;
1539
+ const button = buttonRef.current;
1540
+ if (button?.isConnected) button.focus();
1541
+ }, [open]);
1542
+ const openPlayer = useCallback((event) => {
1543
+ const dir = event.currentTarget.closest("[dir]")?.getAttribute("dir");
1544
+ returnFocusRef.current = true;
1545
+ setDialogDir(dir === "rtl" ? "rtl" : "ltr");
1546
+ }, []);
1547
+ const closePlayer = useCallback(() => setDialogDir(null), []);
1548
+ const name = videoTitleOf(alt, T);
1549
+ const posterFailed = failed.has(poster);
1550
+ return /* @__PURE__ */ jsxs(
1551
+ "span",
1552
+ {
1553
+ "data-boff-explore": "inline-video",
1554
+ "data-product": videoProductOf(href, productSlugs),
1555
+ className: "my-3 block",
1556
+ children: [
1557
+ /* @__PURE__ */ jsxs(
1558
+ "button",
1559
+ {
1560
+ ref: buttonRef,
1561
+ type: "button",
1562
+ "aria-haspopup": "dialog",
1563
+ "aria-label": fill(T.playVideo, { title: name }),
1564
+ onClick: openPlayer,
1565
+ className: INLINE_VIDEO_FRAME,
1566
+ children: [
1567
+ posterFailed ? /* @__PURE__ */ jsx(
1568
+ "span",
1569
+ {
1570
+ "aria-hidden": "true",
1571
+ className: "absolute inset-0 flex items-center justify-center bg-muted text-muted-foreground",
1572
+ children: /* @__PURE__ */ jsx(Film, { className: "h-10 w-10" })
1573
+ }
1574
+ ) : /* @__PURE__ */ jsx(
1575
+ "img",
1576
+ {
1577
+ src: poster,
1578
+ alt: "",
1579
+ loading: "lazy",
1580
+ decoding: "async",
1581
+ onError: () => onFailed(poster),
1582
+ className: "boff-explore-video-poster absolute inset-0 h-full w-full object-cover"
1583
+ }
1584
+ ),
1585
+ /* @__PURE__ */ jsx(
1586
+ "span",
1587
+ {
1588
+ "aria-hidden": "true",
1589
+ className: "pointer-events-none absolute inset-0 flex items-center justify-center",
1590
+ children: /* @__PURE__ */ jsx("span", { className: "flex h-14 w-14 items-center justify-center rounded-full border border-border bg-background/90 text-foreground shadow-md transition-transform group-hover:scale-105", children: /* @__PURE__ */ jsx(Play, { className: "h-6 w-6 fill-current" }) })
1591
+ }
1592
+ ),
1593
+ /* @__PURE__ */ jsx(
1594
+ "span",
1595
+ {
1596
+ "aria-hidden": "true",
1597
+ className: "pointer-events-none absolute start-2 top-2 rounded-full border border-border bg-background/90 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-foreground shadow-sm",
1598
+ children: T.productVideo
1599
+ }
1600
+ )
1601
+ ]
1602
+ }
1603
+ ),
1604
+ open ? /* @__PURE__ */ jsx(
1605
+ ExploreVideoDialog,
1606
+ {
1607
+ videoUrl: href,
1608
+ posterUrl: poster,
1609
+ title: name,
1610
+ strings: T,
1611
+ dir: dialogDir,
1612
+ onClose: closePlayer
1613
+ }
1614
+ ) : null
1615
+ ]
1616
+ }
1617
+ );
1618
+ }
1619
+ var ANSWER_MARKDOWN_COMPONENTS = {
1620
+ a: AnswerLink,
1621
+ img: InlineConceptImage,
1622
+ table: AnswerTable,
1623
+ tr: AnswerTableRow
1624
+ };
1625
+ var NO_PRODUCT_SLUGS = /* @__PURE__ */ new Map();
1626
+ function ExploreAnswerBody({
1627
+ children,
1628
+ strings,
1629
+ imageOrigins,
1630
+ videoOrigins = imageOrigins,
1631
+ productSlugs = NO_PRODUCT_SLUGS,
1632
+ enabled = true,
1633
+ components,
1634
+ className
1635
+ }) {
1636
+ const [failed, setFailed] = useState(() => /* @__PURE__ */ new Set());
1637
+ const onFailed = useCallback((src) => {
1638
+ setFailed(
1639
+ (current) => current.has(src) ? current : new Set(current).add(src)
1640
+ );
1641
+ }, []);
1642
+ const scope = useMemo(
1643
+ () => ({
1644
+ strings,
1645
+ enabled,
1646
+ origins: imageOrigins,
1647
+ productSlugs,
1648
+ failed,
1649
+ onFailed
1650
+ }),
1651
+ [strings, enabled, imageOrigins, productSlugs, failed, onFailed]
1652
+ );
1653
+ const rehypePlugins = useMemo(
1654
+ () => [[rehypeProductVideo, { origins: videoOrigins }]],
1655
+ [videoOrigins]
1656
+ );
1657
+ const overrides = useMemo(
1658
+ () => components ? { ...ANSWER_MARKDOWN_COMPONENTS, ...components } : ANSWER_MARKDOWN_COMPONENTS,
1659
+ [components]
1660
+ );
1661
+ return /* @__PURE__ */ jsx("div", { className: cn("boff-prose boff-explore-body min-w-0", className), children: /* @__PURE__ */ jsx(AnswerScopeContext.Provider, { value: scope, children: /* @__PURE__ */ jsx(Markdown, { components: overrides, rehypePlugins, children }) }) });
1133
1662
  }
1134
1663
 
1135
1664
  // src/components/explore-types.ts
@@ -1742,6 +2271,35 @@ function useWebSDKConfig() {
1742
2271
  return client.getConfig();
1743
2272
  }
1744
2273
 
2274
+ // src/utils/product-slug.ts
2275
+ var PRODUCT_SLUG_RENAMES = Object.freeze(
2276
+ [Object.freeze({ legacy: "botlit", current: "botmanor" })]
2277
+ );
2278
+ Object.freeze(
2279
+ Object.fromEntries(PRODUCT_SLUG_RENAMES.map((r) => [r.legacy, r.current]))
2280
+ );
2281
+ var PRODUCT_SLUG_EMIT = Object.freeze({ productSlug: "legacy" });
2282
+ function findRename(slug) {
2283
+ const wanted = slug.toLowerCase();
2284
+ return PRODUCT_SLUG_RENAMES.find(
2285
+ (r) => r.legacy === wanted || r.current === wanted
2286
+ );
2287
+ }
2288
+ function normalizeProductSlug(slug) {
2289
+ if (slug == null) return slug;
2290
+ return findRename(slug)?.current ?? slug;
2291
+ }
2292
+ function legacyProductSlug(slug) {
2293
+ return findRename(slug)?.legacy ?? slug;
2294
+ }
2295
+ function isSameProductSlug(a, b) {
2296
+ if (a == null || b == null) return a === b;
2297
+ return a === b || normalizeProductSlug(a) === normalizeProductSlug(b);
2298
+ }
2299
+ function emitProductSlug(slug) {
2300
+ return PRODUCT_SLUG_EMIT.productSlug === "current" ? normalizeProductSlug(slug) : legacyProductSlug(slug);
2301
+ }
2302
+
1745
2303
  // src/data/products.ts
1746
2304
  var ECOSYSTEM_PRODUCTS = [
1747
2305
  {
@@ -1764,7 +2322,12 @@ var ECOSYSTEM_PRODUCTS = [
1764
2322
  },
1765
2323
  // BotManor ships on www.botmanor.com (the apex redirects there; the old botlit.ai domain
1766
2324
  // still serves the same site but is no longer the name we link to).
1767
- { slug: "botlit", name: "BotManor", websiteUrl: "https://www.botmanor.com" },
2325
+ // The slug follows the botlit → botmanor rename switch (utils/product-slug.ts).
2326
+ {
2327
+ slug: emitProductSlug("botmanor"),
2328
+ name: "BotManor",
2329
+ websiteUrl: "https://www.botmanor.com"
2330
+ },
1768
2331
  {
1769
2332
  slug: "bigconsole",
1770
2333
  name: "BigConsole",
@@ -3038,7 +3601,7 @@ function isHighSurrogate(code) {
3038
3601
  }
3039
3602
  function safeInlineEnd(line) {
3040
3603
  let codeSpan = null;
3041
- let link = null;
3604
+ const links = [];
3042
3605
  const emphasis = [];
3043
3606
  let at = 0;
3044
3607
  while (at < line.length) {
@@ -3047,6 +3610,13 @@ function safeInlineEnd(line) {
3047
3610
  at += 2;
3048
3611
  continue;
3049
3612
  }
3613
+ const innermost = links[links.length - 1];
3614
+ if (innermost !== void 0 && innermost.kind !== "label") {
3615
+ if (char === "(") links.push({ kind: "paren", start: innermost.start });
3616
+ else if (char === ")") links.pop();
3617
+ at += 1;
3618
+ continue;
3619
+ }
3050
3620
  if (char === "`") {
3051
3621
  let run = 1;
3052
3622
  while (line[at + run] === "`") run += 1;
@@ -3059,17 +3629,17 @@ function safeInlineEnd(line) {
3059
3629
  continue;
3060
3630
  }
3061
3631
  if (char === "[") {
3062
- if (link === null) link = line[at - 1] === "!" ? at - 1 : at;
3063
- at += 1;
3064
- continue;
3065
- }
3066
- if (char === "]") {
3067
- if (link !== null && line[at + 1] !== "(") link = null;
3632
+ links.push({ kind: "label", start: line[at - 1] === "!" ? at - 1 : at });
3068
3633
  at += 1;
3069
3634
  continue;
3070
3635
  }
3071
- if (char === ")") {
3072
- link = null;
3636
+ if (char === "]" && innermost !== void 0) {
3637
+ links.pop();
3638
+ if (line[at + 1] === "(") {
3639
+ links.push({ kind: "destination", start: innermost.start });
3640
+ at += 2;
3641
+ continue;
3642
+ }
3073
3643
  at += 1;
3074
3644
  continue;
3075
3645
  }
@@ -3096,9 +3666,11 @@ function safeInlineEnd(line) {
3096
3666
  }
3097
3667
  at += 1;
3098
3668
  }
3099
- const open = [codeSpan, link, emphasis[0]?.start ?? null].filter(
3100
- (value) => value !== null
3101
- );
3669
+ const open = [
3670
+ codeSpan,
3671
+ links[0]?.start ?? null,
3672
+ emphasis[0]?.start ?? null
3673
+ ].filter((value) => value !== null);
3102
3674
  return open.length === 0 ? line.length : Math.min(...open);
3103
3675
  }
3104
3676
  function revealBoundary(text, limit) {
@@ -5085,6 +5657,7 @@ function AssistantBubble({
5085
5657
  message,
5086
5658
  activity,
5087
5659
  imageOrigins,
5660
+ productSlugs,
5088
5661
  productNames,
5089
5662
  speechLang,
5090
5663
  onNavigate,
@@ -5199,6 +5772,7 @@ function AssistantBubble({
5199
5772
  {
5200
5773
  strings: T,
5201
5774
  imageOrigins,
5775
+ productSlugs,
5202
5776
  enabled: completed,
5203
5777
  className: "boff-prose-sm text-card-foreground",
5204
5778
  children: visible
@@ -5469,21 +6043,24 @@ function ProductStrip({
5469
6043
  children: T.allProducts
5470
6044
  }
5471
6045
  ),
5472
- products.map((product) => /* @__PURE__ */ jsx(
5473
- "button",
5474
- {
5475
- "data-boff-explore": "chip",
5476
- "data-chip-kind": "product",
5477
- "data-product": product.slug,
5478
- type: "button",
5479
- "aria-pressed": focusProduct === product.slug,
5480
- title: product.tagline ?? product.name,
5481
- onClick: () => onFocus(focusProduct === product.slug ? null : product.slug),
5482
- className: chipClass(focusProduct === product.slug),
5483
- children: product.name
5484
- },
5485
- product.slug
5486
- ))
6046
+ products.map((product) => {
6047
+ const focused = isSameProductSlug(focusProduct, product.slug);
6048
+ return /* @__PURE__ */ jsx(
6049
+ "button",
6050
+ {
6051
+ "data-boff-explore": "chip",
6052
+ "data-chip-kind": "product",
6053
+ "data-product": product.slug,
6054
+ type: "button",
6055
+ "aria-pressed": focused,
6056
+ title: product.tagline ?? product.name,
6057
+ onClick: () => onFocus(focused ? null : product.slug),
6058
+ className: chipClass(focused),
6059
+ children: product.name
6060
+ },
6061
+ product.slug
6062
+ );
6063
+ })
5487
6064
  ]
5488
6065
  }
5489
6066
  ),
@@ -5941,11 +6518,11 @@ function ExplorePage({
5941
6518
  const resolvedBody = welcomeBody ?? catalog.welcomeBody;
5942
6519
  const prompts = catalog.examplePrompts;
5943
6520
  const focusedProductName = useMemo(
5944
- () => catalog.products.find((p) => p.slug === focusProduct)?.name ?? null,
6521
+ () => catalog.products.find((p) => isSameProductSlug(p.slug, focusProduct))?.name ?? null,
5945
6522
  [catalog.products, focusProduct]
5946
6523
  );
5947
6524
  const headProductName = useMemo(
5948
- () => productSlug ? catalog.products.find((p) => p.slug === productSlug)?.name ?? null : null,
6525
+ () => productSlug ? catalog.products.find((p) => isSameProductSlug(p.slug, productSlug))?.name ?? null : null,
5949
6526
  [catalog.products, productSlug]
5950
6527
  );
5951
6528
  const imageOrigins = useMemo(
@@ -5956,6 +6533,10 @@ function ExplorePage({
5956
6533
  () => productNamesByOrigin(catalog.products),
5957
6534
  [catalog.products]
5958
6535
  );
6536
+ const productSlugs = useMemo(
6537
+ () => productSlugsByOrigin(catalog.products),
6538
+ [catalog.products]
6539
+ );
5959
6540
  useEffect(() => {
5960
6541
  const el = textareaRef.current;
5961
6542
  if (!el) return;
@@ -6518,6 +7099,7 @@ function ExplorePage({
6518
7099
  message,
6519
7100
  activity,
6520
7101
  imageOrigins,
7102
+ productSlugs,
6521
7103
  productNames,
6522
7104
  speechLang,
6523
7105
  onNavigate,
@@ -6841,6 +7423,6 @@ function ExplorePage({
6841
7423
  );
6842
7424
  }
6843
7425
 
6844
- export { EXPLORE_PREFILL_MAX_LENGTH, ExplorePage, answerPlainText, answerSpeechText, exploreQuestionHref, inlineImageOrigins, isInlineImageAllowed, logoUrlOf, productNamesByOrigin, referenceKindOf, summariseReferences };
7426
+ export { EXPLORE_PREFILL_MAX_LENGTH, ExplorePage, answerPlainText, answerSpeechText, exploreQuestionHref, inlineImageOrigins, isInlineImageAllowed, isInlineVideoAllowed, logoUrlOf, productNamesByOrigin, referenceKindOf, summariseReferences };
6845
7427
  //# sourceMappingURL=explore.mjs.map
6846
7428
  //# sourceMappingURL=explore.mjs.map