@burdenoff/website-sdk 2026.925.2 → 2026.925.3

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 (35) hide show
  1. package/dist/components/explore.d.mts +1 -1
  2. package/dist/components/explore.d.ts +1 -1
  3. package/dist/components/explore.js +1046 -500
  4. package/dist/components/explore.js.map +1 -1
  5. package/dist/components/explore.mjs +1048 -503
  6. package/dist/components/explore.mjs.map +1 -1
  7. package/dist/components/partners.js +3 -2
  8. package/dist/components/partners.js.map +1 -1
  9. package/dist/components/partners.mjs +3 -2
  10. package/dist/components/partners.mjs.map +1 -1
  11. package/dist/content/index.d.mts +1 -1
  12. package/dist/content/index.d.ts +1 -1
  13. package/dist/content/index.js +3 -2
  14. package/dist/content/index.js.map +1 -1
  15. package/dist/content/index.mjs +3 -2
  16. package/dist/content/index.mjs.map +1 -1
  17. package/dist/{explore-C0RFTKAi.d.ts → explore-Cam_ElSJ.d.ts} +70 -12
  18. package/dist/{explore-DzOf7n4m.d.mts → explore-hTQm-_SF.d.mts} +70 -12
  19. package/dist/hooks/index.js +21 -12
  20. package/dist/hooks/index.js.map +1 -1
  21. package/dist/hooks/index.mjs +21 -12
  22. package/dist/hooks/index.mjs.map +1 -1
  23. package/dist/{index-BMWQNZDX.d.mts → index-gm3pBCJT.d.mts} +8 -2
  24. package/dist/{index-BqHF-G-S.d.ts → index-ySbOLSD5.d.ts} +8 -2
  25. package/dist/index.d.mts +30 -9
  26. package/dist/index.d.ts +30 -9
  27. package/dist/index.js +827 -261
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +827 -263
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/store/index.js +3 -2
  32. package/dist/store/index.js.map +1 -1
  33. package/dist/store/index.mjs +3 -2
  34. package/dist/store/index.mjs.map +1 -1
  35. 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
+ );
1125
1353
  }
1126
- return "en";
1354
+ return /* @__PURE__ */ jsx(InsideLinkContext.Provider, { value: true, children: /* @__PURE__ */ jsx(MarkdownLink, { ...props }) });
1127
1355
  }
1128
- function exploreStrings(locale) {
1129
- return STRINGS[locale] ?? EN;
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;
1362
+ }
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
+ ) });
1130
1405
  }
1131
- function fill(template, values) {
1132
- return template.replace(/\{(\w+)\}/g, (match, key) => values[key] ?? match);
1406
+ function markdownText(node) {
1407
+ if (!node) return "";
1408
+ if (typeof node.value === "string") return node.value;
1409
+ return (node.children ?? []).map(markdownText).join("");
1410
+ }
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
@@ -3038,7 +3567,7 @@ function isHighSurrogate(code) {
3038
3567
  }
3039
3568
  function safeInlineEnd(line) {
3040
3569
  let codeSpan = null;
3041
- let link = null;
3570
+ const links = [];
3042
3571
  const emphasis = [];
3043
3572
  let at = 0;
3044
3573
  while (at < line.length) {
@@ -3047,6 +3576,13 @@ function safeInlineEnd(line) {
3047
3576
  at += 2;
3048
3577
  continue;
3049
3578
  }
3579
+ const innermost = links[links.length - 1];
3580
+ if (innermost !== void 0 && innermost.kind !== "label") {
3581
+ if (char === "(") links.push({ kind: "paren", start: innermost.start });
3582
+ else if (char === ")") links.pop();
3583
+ at += 1;
3584
+ continue;
3585
+ }
3050
3586
  if (char === "`") {
3051
3587
  let run = 1;
3052
3588
  while (line[at + run] === "`") run += 1;
@@ -3059,17 +3595,17 @@ function safeInlineEnd(line) {
3059
3595
  continue;
3060
3596
  }
3061
3597
  if (char === "[") {
3062
- if (link === null) link = line[at - 1] === "!" ? at - 1 : at;
3598
+ links.push({ kind: "label", start: line[at - 1] === "!" ? at - 1 : at });
3063
3599
  at += 1;
3064
3600
  continue;
3065
3601
  }
3066
- if (char === "]") {
3067
- if (link !== null && line[at + 1] !== "(") link = null;
3068
- at += 1;
3069
- continue;
3070
- }
3071
- if (char === ")") {
3072
- link = null;
3602
+ if (char === "]" && innermost !== void 0) {
3603
+ links.pop();
3604
+ if (line[at + 1] === "(") {
3605
+ links.push({ kind: "destination", start: innermost.start });
3606
+ at += 2;
3607
+ continue;
3608
+ }
3073
3609
  at += 1;
3074
3610
  continue;
3075
3611
  }
@@ -3096,9 +3632,11 @@ function safeInlineEnd(line) {
3096
3632
  }
3097
3633
  at += 1;
3098
3634
  }
3099
- const open = [codeSpan, link, emphasis[0]?.start ?? null].filter(
3100
- (value) => value !== null
3101
- );
3635
+ const open = [
3636
+ codeSpan,
3637
+ links[0]?.start ?? null,
3638
+ emphasis[0]?.start ?? null
3639
+ ].filter((value) => value !== null);
3102
3640
  return open.length === 0 ? line.length : Math.min(...open);
3103
3641
  }
3104
3642
  function revealBoundary(text, limit) {
@@ -5085,6 +5623,7 @@ function AssistantBubble({
5085
5623
  message,
5086
5624
  activity,
5087
5625
  imageOrigins,
5626
+ productSlugs,
5088
5627
  productNames,
5089
5628
  speechLang,
5090
5629
  onNavigate,
@@ -5199,6 +5738,7 @@ function AssistantBubble({
5199
5738
  {
5200
5739
  strings: T,
5201
5740
  imageOrigins,
5741
+ productSlugs,
5202
5742
  enabled: completed,
5203
5743
  className: "boff-prose-sm text-card-foreground",
5204
5744
  children: visible
@@ -5956,6 +6496,10 @@ function ExplorePage({
5956
6496
  () => productNamesByOrigin(catalog.products),
5957
6497
  [catalog.products]
5958
6498
  );
6499
+ const productSlugs = useMemo(
6500
+ () => productSlugsByOrigin(catalog.products),
6501
+ [catalog.products]
6502
+ );
5959
6503
  useEffect(() => {
5960
6504
  const el = textareaRef.current;
5961
6505
  if (!el) return;
@@ -6518,6 +7062,7 @@ function ExplorePage({
6518
7062
  message,
6519
7063
  activity,
6520
7064
  imageOrigins,
7065
+ productSlugs,
6521
7066
  productNames,
6522
7067
  speechLang,
6523
7068
  onNavigate,
@@ -6841,6 +7386,6 @@ function ExplorePage({
6841
7386
  );
6842
7387
  }
6843
7388
 
6844
- export { EXPLORE_PREFILL_MAX_LENGTH, ExplorePage, answerPlainText, answerSpeechText, exploreQuestionHref, inlineImageOrigins, isInlineImageAllowed, logoUrlOf, productNamesByOrigin, referenceKindOf, summariseReferences };
7389
+ export { EXPLORE_PREFILL_MAX_LENGTH, ExplorePage, answerPlainText, answerSpeechText, exploreQuestionHref, inlineImageOrigins, isInlineImageAllowed, isInlineVideoAllowed, logoUrlOf, productNamesByOrigin, referenceKindOf, summariseReferences };
6845
7390
  //# sourceMappingURL=explore.mjs.map
6846
7391
  //# sourceMappingURL=explore.mjs.map