@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
@@ -2,10 +2,11 @@
2
2
 
3
3
  var React = require('react');
4
4
  var lucideReact = require('lucide-react');
5
+ var reactDom = require('react-dom');
6
+ var jsxRuntime = require('react/jsx-runtime');
5
7
  var ReactMarkdown = require('react-markdown');
6
8
  var rehypeSanitize = require('rehype-sanitize');
7
9
  var remarkGfm = require('remark-gfm');
8
- var jsxRuntime = require('react/jsx-runtime');
9
10
  var clsx = require('clsx');
10
11
  var tailwindMerge = require('tailwind-merge');
11
12
  var reactHelmetAsync = require('react-helmet-async');
@@ -40,478 +41,12 @@ var remarkGfm__default = /*#__PURE__*/_interopDefault(remarkGfm);
40
41
  var __defProp = Object.defineProperty;
41
42
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
42
43
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
43
- function MarkdownLink({
44
- href,
45
- children,
46
- // react-markdown passes the hast node to every override; it is not a DOM attribute.
47
- node: _node,
48
- ...props
49
- }) {
50
- const isExternal = typeof href === "string" && (href.startsWith("http://") || href.startsWith("https://"));
51
- return /* @__PURE__ */ jsxRuntime.jsx(
52
- "a",
53
- {
54
- href,
55
- ...props,
56
- ...isExternal ? { target: "_blank", rel: "noopener noreferrer" } : { rel: "noopener noreferrer" },
57
- children
58
- }
59
- );
60
- }
61
- var defaultComponents = {
62
- a: MarkdownLink
63
- };
64
- function Markdown({
65
- children,
66
- className,
67
- inline,
68
- components
69
- }) {
70
- const content = /* @__PURE__ */ jsxRuntime.jsx(
71
- ReactMarkdown__default.default,
72
- {
73
- remarkPlugins: [remarkGfm__default.default],
74
- rehypePlugins: [rehypeSanitize__default.default],
75
- components: { ...defaultComponents, ...components },
76
- ...inline ? { disallowedElements: ["p"], unwrapDisallowed: true } : {},
77
- children
78
- }
79
- );
80
- if (inline) {
81
- return /* @__PURE__ */ jsxRuntime.jsx("span", { className, children: content });
82
- }
83
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: content });
84
- }
85
- function cn(...inputs) {
86
- return tailwindMerge.twMerge(clsx.clsx(inputs));
87
- }
88
- var EXPLORE_ANSWER_CSS = `
89
- /* Nothing inside an answer may widen the page. Model output carries long URLs, code and
90
- tables \u2014 each of those overflows a phone by default, and one overflowing child makes the
91
- WHOLE document horizontally scrollable, not just the bubble. */
92
- .boff-explore-body,
93
- .boff-explore-body p,
94
- .boff-explore-body li,
95
- .boff-explore-body a,
96
- .boff-explore-body h1,
97
- .boff-explore-body h2,
98
- .boff-explore-body h3,
99
- /* h4-h6 as well: a published answer page pushes the model's own headings down a level or
100
- two so they nest under the page's, and a long unbroken word is no narrower for it. */
101
- .boff-explore-body h4,
102
- .boff-explore-body h5,
103
- .boff-explore-body h6 {
104
- overflow-wrap: anywhere;
105
- word-break: break-word;
106
- }
107
- .boff-explore-body pre {
108
- overflow-x: auto;
109
- max-width: 100%;
110
- }
111
- .boff-explore-body table {
112
- display: block;
113
- overflow-x: auto;
114
- max-width: 100%;
115
- }
116
- .boff-explore-body img {
117
- max-width: 100%;
118
- height: auto;
119
- }
120
-
121
- /* ---- Comparison and pricing tables ----------------------------------------------------
122
- The harness emits a comparison, and a set of plans, as a GFM table whose FIRST column is
123
- the subject \u2014 the product, or the plan (CONTRACT.md \xA77). Two things have to be true of it:
124
- the page must not scroll sideways because of it, and it has to be readable on a 320px
125
- phone, which a four-column grid of 1px borders is not.
126
-
127
- Wide: a contained scroller. \`max-width: 100%\` keeps the box inside the bubble and
128
- \`overflow-x: auto\` keeps the columns inside the box, so the widest possible table still
129
- cannot widen the document. \`overscroll-behavior-x: contain\` stops a swipe that runs out
130
- of table from chaining to the page \u2014 the same rule the transcript needed. */
131
- .boff-explore-body .boff-explore-table {
132
- max-width: 100%;
133
- overflow-x: auto;
134
- overscroll-behavior-x: contain;
135
- margin: 0.75rem 0;
136
- border: 1px solid hsl(var(--border));
137
- border-radius: 0.75rem;
138
- background-color: hsl(var(--card));
139
- }
140
- .boff-explore-body .boff-explore-table:focus-visible {
141
- outline: 2px solid hsl(var(--ring));
142
- outline-offset: 2px;
143
- }
144
- .boff-explore-body .boff-explore-table table {
145
- /* Beats the blanket \`display: block\` above, which is the unwrapped fallback. */
146
- display: table;
147
- width: 100%;
148
- max-width: none;
149
- margin: 0;
150
- border-collapse: collapse;
151
- font-size: 0.8125rem;
152
- }
153
- .boff-explore-body .boff-explore-table th,
154
- .boff-explore-body .boff-explore-table td {
155
- border: 0;
156
- border-top: 1px solid hsl(var(--border));
157
- padding: 0.5rem 0.75rem;
158
- /* \`start\`, never \`left\`: the page root carries dir="rtl" in Arabic, and a left-aligned
159
- table there reads against the grain of every other line on the page. */
160
- text-align: start;
161
- vertical-align: top;
162
- overflow-wrap: anywhere;
163
- /* Prices in a column line up under each other instead of drifting by digit width. */
164
- font-variant-numeric: tabular-nums;
165
- min-width: 6rem;
166
- }
167
- .boff-explore-body .boff-explore-table thead th {
168
- border-top: 0;
169
- background-color: hsl(var(--muted));
170
- color: hsl(var(--foreground));
171
- font-weight: 600;
172
- white-space: nowrap;
173
- }
174
- /* The first column is what the row is ABOUT \u2014 the product, or the plan name. Reading it as
175
- the row's heading is what makes a price list scannable rather than a grid of strings. */
176
- .boff-explore-body .boff-explore-table tbody td:first-child {
177
- font-weight: 600;
178
- color: hsl(var(--card-foreground));
179
- }
180
44
 
181
- /* Narrow: one card per row. Each cell prints its own column heading, so a value is never
182
- stranded without the question it answers, and the subject cell becomes the card's title.
183
- A media query rather than a measured width \u2014 these pages are serialised post-hydration,
184
- so anything measured here would ship to every phone as static HTML. */
185
- @media (max-width: 30rem) {
186
- .boff-explore-body .boff-explore-table {
187
- overflow-x: visible;
188
- }
189
- .boff-explore-body .boff-explore-table table,
190
- .boff-explore-body .boff-explore-table tbody,
191
- .boff-explore-body .boff-explore-table tr,
192
- .boff-explore-body .boff-explore-table td {
193
- display: block;
194
- width: 100%;
195
- min-width: 0;
196
- }
197
- .boff-explore-body .boff-explore-table thead {
198
- display: none;
199
- }
200
- .boff-explore-body .boff-explore-table tr {
201
- border-top: 1px solid hsl(var(--border));
202
- padding: 0.5rem 0;
203
- }
204
- .boff-explore-body .boff-explore-table tr:first-child {
205
- border-top: 0;
206
- }
207
- .boff-explore-body .boff-explore-table td {
208
- border: 0;
209
- padding: 0.125rem 0.75rem;
210
- }
211
- .boff-explore-body .boff-explore-table td[data-label]::before {
212
- content: attr(data-label);
213
- display: block;
214
- font-size: 0.6875rem;
215
- font-weight: 600;
216
- text-transform: uppercase;
217
- letter-spacing: 0.04em;
218
- color: hsl(var(--muted-foreground));
219
- }
220
- .boff-explore-body .boff-explore-table tbody td:first-child {
221
- font-size: 0.9375rem;
222
- padding-bottom: 0.25rem;
223
- }
224
- .boff-explore-body .boff-explore-table tbody td:first-child::before {
225
- /* The plan's own name is the heading here; repeating "Plan" over it says nothing. */
226
- display: none;
227
- }
228
- }
229
- `;
230
- function ExploreAnswerStyles() {
231
- return /* @__PURE__ */ jsxRuntime.jsx("style", { dangerouslySetInnerHTML: { __html: EXPLORE_ANSWER_CSS } });
232
- }
233
- function inlineImageOrigins(products) {
234
- const origins = /* @__PURE__ */ new Set();
235
- for (const product of products) {
236
- let url;
237
- try {
238
- url = new URL(product.website);
239
- } catch {
240
- continue;
241
- }
242
- if (url.protocol !== "https:") continue;
243
- const apex = url.hostname.replace(/^www\./, "");
244
- const port = url.port ? `:${url.port}` : "";
245
- origins.add(`https://${apex}${port}`);
246
- origins.add(`https://www.${apex}${port}`);
247
- }
248
- return origins;
249
- }
250
- function isInlineImageAllowed(src, origins) {
251
- if (typeof src !== "string" || src === "") return false;
252
- let url;
253
- try {
254
- url = new URL(src);
255
- } catch {
256
- return false;
257
- }
258
- if (url.protocol !== "https:" || url.username || url.password) return false;
259
- return origins.has(url.origin);
260
- }
261
- var CODE_FENCE = /^\s*(?:```|~~~)/;
262
- var TABLE_ROW_LINE = /^\s{0,3}\|/;
263
- var TABLE_DELIMITER_ROW = /^\s{0,3}\|[\s:|-]*-[\s:|-]*$/;
264
- function readableLine(raw, mode) {
265
- let line = raw;
266
- line = line.replace(/!\[[^\]]*\]\([^)\s]*(?:\s+"[^"]*")?\)/g, "");
267
- line = mode === "speech" ? (
268
- // A voice reading "h-t-t-p-s colon slash slash…" mid-sentence is noise, and the
269
- // prompt asks for two to five links in every answer. The label IS the words.
270
- line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1")
271
- ) : line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1 ($2)");
272
- line = mode === "speech" ? line.replace(/<(https?:\/\/[^>\s]+)>/g, "") : line.replace(/<(https?:\/\/[^>\s]+)>/g, "$1");
273
- line = line.replace(/^\s{0,3}#{1,6}\s+/, "");
274
- line = line.replace(/^\s{0,3}>\s?/, "");
275
- if (/^\s{0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/.test(line)) return "";
276
- line = line.replace(/`([^`]+)`/g, "$1");
277
- line = line.replace(/\*\*([^*]+)\*\*/g, "$1");
278
- line = line.replace(/__([^_]+)__/g, "$1");
279
- line = line.replace(/(^|[^*\w])\*([^*\n]+)\*(?![*\w])/g, "$1$2");
280
- line = line.replace(/(^|[^_\w])_([^_\n]+)_(?![_\w])/g, "$1$2");
281
- return line.replace(/[ \t]+$/, "");
282
- }
283
- function tableCells(row) {
284
- const trimmed = row.trim();
285
- const cells = [];
286
- let cell = "";
287
- for (let at = 0; at < trimmed.length; at += 1) {
288
- const char = trimmed[at] ?? "";
289
- if (char === "\\") {
290
- cell += char + (trimmed[at + 1] ?? "");
291
- at += 1;
292
- continue;
293
- }
294
- if (char === "|") {
295
- cells.push(cell.trim());
296
- cell = "";
297
- continue;
298
- }
299
- cell += char;
300
- }
301
- cells.push(cell.trim());
302
- if (trimmed.startsWith("|") && cells[0] === "") cells.shift();
303
- if (trimmed.endsWith("|") && cells[cells.length - 1] === "") cells.pop();
304
- return cells;
305
- }
306
- function asSentence(text) {
307
- return /[.!?]$/.test(text) ? text : `${text}.`;
308
- }
309
- function spokenTable(rows) {
310
- if (rows.length < 2 || !TABLE_DELIMITER_ROW.test(rows[1] ?? "")) return rows;
311
- const labels = tableCells(rows[0] ?? "");
312
- const spoken = [];
313
- for (const row of rows.slice(2)) {
314
- const parts = [];
315
- tableCells(row).forEach((value, column) => {
316
- if (value === "") return;
317
- const label = column === 0 ? "" : labels[column] ?? "";
318
- parts.push(label === "" ? value : `${label}: ${value}`);
319
- });
320
- if (parts.length > 0) spoken.push(asSentence(parts.join(". ")));
321
- }
322
- return spoken;
323
- }
324
- function readableText(markdown, mode) {
325
- const lines = [];
326
- let inFence = false;
327
- let table = [];
328
- const flushTable = () => {
329
- if (table.length === 0) return;
330
- for (const line of spokenTable(table)) lines.push(readableLine(line, mode));
331
- table = [];
332
- };
333
- for (const raw of markdown.split("\n")) {
334
- if (CODE_FENCE.test(raw)) {
335
- flushTable();
336
- inFence = !inFence;
337
- continue;
338
- }
339
- if (inFence) {
340
- lines.push(raw);
341
- continue;
342
- }
343
- if (mode === "speech" && TABLE_ROW_LINE.test(raw)) {
344
- table.push(raw);
345
- continue;
346
- }
347
- flushTable();
348
- lines.push(readableLine(raw, mode));
349
- }
350
- flushTable();
351
- return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
352
- }
353
- function answerPlainText(markdown) {
354
- return readableText(markdown, "clipboard");
355
- }
356
- function answerSpeechText(markdown) {
357
- return readableText(markdown, "speech");
358
- }
359
- var AnswerScopeContext = React.createContext(null);
360
- var InsideLinkContext = React.createContext(false);
361
- function AnswerLink(props) {
362
- return /* @__PURE__ */ jsxRuntime.jsx(InsideLinkContext.Provider, { value: true, children: /* @__PURE__ */ jsxRuntime.jsx(MarkdownLink, { ...props }) });
363
- }
364
- var INLINE_IMAGE_FRAME = "relative inline-block max-w-full overflow-hidden rounded-xl border border-border bg-muted align-top shadow-sm";
365
- function InlineConceptImage({ src, alt }) {
366
- const scope = React.useContext(AnswerScopeContext);
367
- const insideLink = React.useContext(InsideLinkContext);
368
- if (!scope || !scope.enabled || !isInlineImageAllowed(src, scope.origins) || scope.failed.has(src)) {
369
- return null;
370
- }
371
- const { strings: T, onFailed } = scope;
372
- const picture = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
373
- /* @__PURE__ */ jsxRuntime.jsx(
374
- "img",
375
- {
376
- src,
377
- alt: alt ?? "",
378
- loading: "lazy",
379
- decoding: "async",
380
- onError: () => onFailed(src),
381
- className: "block h-auto max-h-80 w-auto max-w-full object-contain"
382
- }
383
- ),
384
- /* @__PURE__ */ jsxRuntime.jsx(
385
- "span",
386
- {
387
- "aria-hidden": "true",
388
- 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",
389
- children: T.conceptIllustration
390
- }
391
- )
392
- ] });
393
- return /* @__PURE__ */ jsxRuntime.jsx("span", { "data-boff-explore": "inline-image", className: "my-3 block", children: insideLink ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: INLINE_IMAGE_FRAME, children: picture }) : /* @__PURE__ */ jsxRuntime.jsxs(
394
- "a",
395
- {
396
- href: src,
397
- target: "_blank",
398
- rel: "noopener noreferrer",
399
- className: cn(
400
- INLINE_IMAGE_FRAME,
401
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
402
- ),
403
- children: [
404
- picture,
405
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sr-only", children: [
406
- "(",
407
- T.opensInNewTab,
408
- ")"
409
- ] })
410
- ]
411
- }
412
- ) });
413
- }
414
- function markdownText(node) {
415
- if (!node) return "";
416
- if (typeof node.value === "string") return node.value;
417
- return (node.children ?? []).map(markdownText).join("");
418
- }
419
- function hasHeaderCells(node) {
420
- return (node?.children ?? []).some((cell) => cell.tagName === "th");
421
- }
422
- function findHeaderRow(node) {
423
- for (const child of node?.children ?? []) {
424
- if (child.tagName === "tr" && hasHeaderCells(child)) return child;
425
- const nested = findHeaderRow(child);
426
- if (nested !== null) return nested;
427
- }
428
- return null;
429
- }
430
- function tableColumnLabels(node) {
431
- const row = findHeaderRow(node);
432
- if (row === null) return [];
433
- return (row.children ?? []).filter((cell) => cell.tagName === "th").map((cell) => markdownText(cell).trim());
434
- }
435
- var TableColumnsContext = React.createContext([]);
436
- function AnswerTable({ node, children }) {
437
- const scope = React.useContext(AnswerScopeContext);
438
- const table = /* @__PURE__ */ jsxRuntime.jsx(TableColumnsContext.Provider, { value: tableColumnLabels(node), children: /* @__PURE__ */ jsxRuntime.jsx("table", { children }) });
439
- if (!scope) return table;
440
- return /* @__PURE__ */ jsxRuntime.jsx(
441
- "div",
442
- {
443
- "data-boff-explore": "table",
444
- role: "region",
445
- "aria-label": scope.strings.answerTable,
446
- tabIndex: 0,
447
- className: "boff-explore-table",
448
- children: table
449
- }
450
- );
451
- }
452
- function AnswerTableRow({
453
- node,
454
- children,
455
- ...props
456
- }) {
457
- const labels = React.useContext(TableColumnsContext);
458
- if (labels.length === 0 || hasHeaderCells(node)) {
459
- return /* @__PURE__ */ jsxRuntime.jsx("tr", { ...props, children });
460
- }
461
- let column = 0;
462
- const cells = React.Children.map(children, (cell) => {
463
- if (!React.isValidElement(cell)) return cell;
464
- const label = labels[column] ?? "";
465
- column += 1;
466
- if (label === "") return cell;
467
- return React.cloneElement(cell, {
468
- "data-label": label
469
- });
470
- });
471
- return /* @__PURE__ */ jsxRuntime.jsx("tr", { ...props, children: cells });
472
- }
473
- var ANSWER_MARKDOWN_COMPONENTS = {
474
- a: AnswerLink,
475
- img: InlineConceptImage,
476
- table: AnswerTable,
477
- tr: AnswerTableRow
478
- };
479
- function ExploreAnswerBody({
480
- children,
481
- strings,
482
- imageOrigins,
483
- enabled = true,
484
- components,
485
- className
486
- }) {
487
- const [failed, setFailed] = React.useState(() => /* @__PURE__ */ new Set());
488
- const onFailed = React.useCallback((src) => {
489
- setFailed(
490
- (current) => current.has(src) ? current : new Set(current).add(src)
491
- );
492
- }, []);
493
- const scope = React.useMemo(
494
- () => ({
495
- strings,
496
- enabled,
497
- origins: imageOrigins,
498
- failed,
499
- onFailed
500
- }),
501
- [strings, enabled, imageOrigins, failed, onFailed]
502
- );
503
- const overrides = React.useMemo(
504
- () => components ? { ...ANSWER_MARKDOWN_COMPONENTS, ...components } : ANSWER_MARKDOWN_COMPONENTS,
505
- [components]
506
- );
507
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("boff-prose boff-explore-body min-w-0", className), children: /* @__PURE__ */ jsxRuntime.jsx(AnswerScopeContext.Provider, { value: scope, children: /* @__PURE__ */ jsxRuntime.jsx(Markdown, { components: overrides, children }) }) });
508
- }
509
-
510
- // src/components/explore-i18n.ts
511
- var EXPLORE_LOCALES = ["en", "zh", "hi", "es", "ar", "ta"];
512
- var RTL_LOCALES = /* @__PURE__ */ new Set(["ar"]);
513
- function isRtlLocale(locale) {
514
- return RTL_LOCALES.has(locale);
45
+ // src/components/explore-i18n.ts
46
+ var EXPLORE_LOCALES = ["en", "zh", "hi", "es", "ar", "ta"];
47
+ var RTL_LOCALES = /* @__PURE__ */ new Set(["ar"]);
48
+ function isRtlLocale(locale) {
49
+ return RTL_LOCALES.has(locale);
515
50
  }
516
51
  var EN = {
517
52
  badge: "AI answers, grounded in our documentation",
@@ -594,6 +129,13 @@ var EN = {
594
129
  readAloud: "Read aloud",
595
130
  stopReading: "Stop reading",
596
131
  answerTable: "Table",
132
+ productVideo: "Product video",
133
+ productVideoTitle: "{product} product video",
134
+ playVideo: "Play {title}",
135
+ closeVideo: "Close video",
136
+ enterFullScreen: "Full screen",
137
+ exitFullScreen: "Exit full screen",
138
+ videoUnavailable: "This video can't be played right now.",
597
139
  groundedIn: "What this answer is based on",
598
140
  sourceCountOne: "1 source",
599
141
  sourceCountMany: "{count} sources",
@@ -695,6 +237,13 @@ var ZH = {
695
237
  readAloud: "\u6717\u8BFB",
696
238
  stopReading: "\u505C\u6B62\u6717\u8BFB",
697
239
  answerTable: "\u8868\u683C",
240
+ productVideo: "\u4EA7\u54C1\u89C6\u9891",
241
+ productVideoTitle: "{product} \u4EA7\u54C1\u89C6\u9891",
242
+ playVideo: "\u64AD\u653E {title}",
243
+ closeVideo: "\u5173\u95ED\u89C6\u9891",
244
+ enterFullScreen: "\u5168\u5C4F",
245
+ exitFullScreen: "\u9000\u51FA\u5168\u5C4F",
246
+ videoUnavailable: "\u6682\u65F6\u65E0\u6CD5\u64AD\u653E\u6B64\u89C6\u9891\u3002",
698
247
  groundedIn: "\u8FD9\u4E2A\u56DE\u7B54\u7684\u4F9D\u636E",
699
248
  sourceCountOne: "1 \u4E2A\u6765\u6E90",
700
249
  sourceCountMany: "{count} \u4E2A\u6765\u6E90",
@@ -792,6 +341,13 @@ var HI = {
792
341
  readAloud: "\u091C\u093C\u094B\u0930 \u0938\u0947 \u092A\u0922\u093C\u0947\u0902",
793
342
  stopReading: "\u092A\u0922\u093C\u0928\u093E \u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
794
343
  answerTable: "\u0924\u093E\u0932\u093F\u0915\u093E",
344
+ productVideo: "\u0909\u0924\u094D\u092A\u093E\u0926 \u0935\u0940\u0921\u093F\u092F\u094B",
345
+ productVideoTitle: "{product} \u0909\u0924\u094D\u092A\u093E\u0926 \u0935\u0940\u0921\u093F\u092F\u094B",
346
+ playVideo: "{title} \u091A\u0932\u093E\u090F\u0901",
347
+ closeVideo: "\u0935\u0940\u0921\u093F\u092F\u094B \u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
348
+ enterFullScreen: "\u092A\u0942\u0930\u094D\u0923 \u0938\u094D\u0915\u094D\u0930\u0940\u0928",
349
+ 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",
350
+ videoUnavailable: "\u092F\u0939 \u0935\u0940\u0921\u093F\u092F\u094B \u0905\u092D\u0940 \u0928\u0939\u0940\u0902 \u091A\u0932 \u0938\u0915\u0924\u093E\u0964",
795
351
  groundedIn: "\u092F\u0939 \u0909\u0924\u094D\u0924\u0930 \u0915\u093F\u0938 \u092A\u0930 \u0906\u0927\u093E\u0930\u093F\u0924 \u0939\u0948",
796
352
  sourceCountOne: "1 \u0938\u094D\u0930\u094B\u0924",
797
353
  sourceCountMany: "{count} \u0938\u094D\u0930\u094B\u0924",
@@ -893,6 +449,13 @@ var ES = {
893
449
  readAloud: "Leer en voz alta",
894
450
  stopReading: "Dejar de leer",
895
451
  answerTable: "Tabla",
452
+ productVideo: "V\xEDdeo del producto",
453
+ productVideoTitle: "V\xEDdeo de {product}",
454
+ playVideo: "Reproducir {title}",
455
+ closeVideo: "Cerrar v\xEDdeo",
456
+ enterFullScreen: "Pantalla completa",
457
+ exitFullScreen: "Salir de pantalla completa",
458
+ videoUnavailable: "Ahora mismo no se puede reproducir este v\xEDdeo.",
896
459
  groundedIn: "En qu\xE9 se basa esta respuesta",
897
460
  sourceCountOne: "1 fuente",
898
461
  sourceCountMany: "{count} fuentes",
@@ -994,6 +557,13 @@ var AR = {
994
557
  readAloud: "\u0627\u0642\u0631\u0623 \u0628\u0635\u0648\u062A \u0639\u0627\u0644\u064D",
995
558
  stopReading: "\u0625\u064A\u0642\u0627\u0641 \u0627\u0644\u0642\u0631\u0627\u0621\u0629",
996
559
  answerTable: "\u062C\u062F\u0648\u0644",
560
+ productVideo: "\u0641\u064A\u062F\u064A\u0648 \u0627\u0644\u0645\u0646\u062A\u062C",
561
+ productVideoTitle: "\u0641\u064A\u062F\u064A\u0648 {product}",
562
+ playVideo: "\u062A\u0634\u063A\u064A\u0644 {title}",
563
+ closeVideo: "\u0625\u063A\u0644\u0627\u0642 \u0627\u0644\u0641\u064A\u062F\u064A\u0648",
564
+ enterFullScreen: "\u0645\u0644\u0621 \u0627\u0644\u0634\u0627\u0634\u0629",
565
+ exitFullScreen: "\u0627\u0644\u062E\u0631\u0648\u062C \u0645\u0646 \u0645\u0644\u0621 \u0627\u0644\u0634\u0627\u0634\u0629",
566
+ 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.",
997
567
  groundedIn: "\u0639\u0644\u0649 \u0645\u0627\u0630\u0627 \u062A\u0633\u062A\u0646\u062F \u0647\u0630\u0647 \u0627\u0644\u0625\u062C\u0627\u0628\u0629",
998
568
  sourceCountOne: "\u0645\u0635\u062F\u0631 \u0648\u0627\u062D\u062F",
999
569
  sourceCountMany: "{count} \u0645\u0635\u0627\u062F\u0631",
@@ -1095,6 +665,13 @@ var TA = {
1095
665
  readAloud: "\u0B9A\u0BA4\u0BCD\u0BA4\u0BAE\u0BBE\u0B95\u0BAA\u0BCD \u0BAA\u0B9F\u0BBF",
1096
666
  stopReading: "\u0BAA\u0B9F\u0BBF\u0BAA\u0BCD\u0BAA\u0BA4\u0BC8 \u0BA8\u0BBF\u0BB1\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1",
1097
667
  answerTable: "\u0B85\u0B9F\u0BCD\u0B9F\u0BB5\u0BA3\u0BC8",
668
+ productVideo: "\u0BA4\u0BAF\u0BBE\u0BB0\u0BBF\u0BAA\u0BCD\u0BAA\u0BC1 \u0BB5\u0BC0\u0B9F\u0BBF\u0BAF\u0BCB",
669
+ productVideoTitle: "{product} \u0BA4\u0BAF\u0BBE\u0BB0\u0BBF\u0BAA\u0BCD\u0BAA\u0BC1 \u0BB5\u0BC0\u0B9F\u0BBF\u0BAF\u0BCB",
670
+ playVideo: "{title} \u0B87\u0BAF\u0B95\u0BCD\u0B95\u0BC1",
671
+ closeVideo: "\u0BB5\u0BC0\u0B9F\u0BBF\u0BAF\u0BCB\u0BB5\u0BC8 \u0BAE\u0BC2\u0B9F\u0BC1",
672
+ enterFullScreen: "\u0BAE\u0BC1\u0BB4\u0BC1\u0BA4\u0BCD \u0BA4\u0BBF\u0BB0\u0BC8",
673
+ 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",
674
+ 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.",
1098
675
  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",
1099
676
  sourceCountOne: "1 \u0BAE\u0BC2\u0BB2\u0BAE\u0BCD",
1100
677
  sourceCountMany: "{count} \u0BAE\u0BC2\u0BB2\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
@@ -1137,25 +714,977 @@ var SPEECH_TAGS = {
1137
714
  function speechLocaleOf(locale) {
1138
715
  return SPEECH_TAGS[locale];
1139
716
  }
1140
- function resolveExploreLocale(explicit) {
1141
- const candidates = [
1142
- explicit,
1143
- typeof document === "undefined" ? void 0 : document.documentElement.lang,
1144
- typeof navigator === "undefined" ? void 0 : navigator.language
1145
- ];
1146
- for (const candidate of candidates) {
1147
- if (!candidate) continue;
1148
- const primary = candidate.toLowerCase().split("-")[0];
1149
- const match = EXPLORE_LOCALES.find((locale) => locale === primary);
1150
- if (match) return match;
717
+ function resolveExploreLocale(explicit) {
718
+ const candidates = [
719
+ explicit,
720
+ typeof document === "undefined" ? void 0 : document.documentElement.lang,
721
+ typeof navigator === "undefined" ? void 0 : navigator.language
722
+ ];
723
+ for (const candidate of candidates) {
724
+ if (!candidate) continue;
725
+ const primary = candidate.toLowerCase().split("-")[0];
726
+ const match = EXPLORE_LOCALES.find((locale) => locale === primary);
727
+ if (match) return match;
728
+ }
729
+ return "en";
730
+ }
731
+ function exploreStrings(locale) {
732
+ return STRINGS[locale] ?? EN;
733
+ }
734
+ function fill(template, values) {
735
+ return template.replace(/\{(\w+)\}/g, (match, key) => values[key] ?? match);
736
+ }
737
+ function fullscreenSupport() {
738
+ if (typeof document === "undefined") return null;
739
+ if (document.fullscreenEnabled && typeof document.documentElement.requestFullscreen === "function") {
740
+ return "element";
741
+ }
742
+ if (typeof HTMLVideoElement !== "undefined" && "webkitEnterFullscreen" in HTMLVideoElement.prototype) {
743
+ return "video";
744
+ }
745
+ return null;
746
+ }
747
+ function ignoreRejection(result) {
748
+ result?.catch(() => void 0);
749
+ }
750
+ var VIDEO_DIALOG_CSS = `
751
+ .boff-explore-video-panel:fullscreen {
752
+ display: flex;
753
+ flex-direction: column;
754
+ gap: 0.75rem;
755
+ padding: 1rem;
756
+ background-color: hsl(var(--background));
757
+ color: hsl(var(--foreground));
758
+ }
759
+ .boff-explore-video-panel:fullscreen .boff-explore-video-frame {
760
+ display: flex;
761
+ flex: 1 1 auto;
762
+ min-height: 0;
763
+ align-items: center;
764
+ justify-content: center;
765
+ border: 0;
766
+ border-radius: 0;
767
+ box-shadow: none;
768
+ background-color: hsl(var(--background));
769
+ }
770
+ .boff-explore-video-panel:fullscreen video {
771
+ width: 100%;
772
+ height: 100%;
773
+ max-height: none;
774
+ }
775
+ `;
776
+ 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";
777
+ function ExploreVideoDialog({
778
+ videoUrl,
779
+ posterUrl,
780
+ title,
781
+ strings: T,
782
+ dir,
783
+ onClose
784
+ }) {
785
+ const dialogRef = React.useRef(null);
786
+ const panelRef = React.useRef(null);
787
+ const videoRef = React.useRef(null);
788
+ const closeRef = React.useRef(null);
789
+ const nativeModalRef = React.useRef(false);
790
+ const titleId = React.useId();
791
+ const [support] = React.useState(fullscreenSupport);
792
+ const [fullscreen, setFullscreen] = React.useState(false);
793
+ const [failed, setFailed] = React.useState(false);
794
+ React.useEffect(() => {
795
+ const dialog = dialogRef.current;
796
+ if (!dialog) return;
797
+ if (typeof dialog.showModal === "function") {
798
+ if (!dialog.open) dialog.showModal();
799
+ nativeModalRef.current = true;
800
+ } else {
801
+ dialog.setAttribute("open", "");
802
+ nativeModalRef.current = false;
803
+ }
804
+ closeRef.current?.focus();
805
+ ignoreRejection(videoRef.current?.play());
806
+ return () => {
807
+ if (dialog.open && typeof dialog.close === "function") dialog.close();
808
+ };
809
+ }, []);
810
+ React.useEffect(() => {
811
+ const sync = () => {
812
+ const current = document.fullscreenElement;
813
+ setFullscreen(
814
+ current !== null && dialogRef.current?.contains(current) === true
815
+ );
816
+ };
817
+ const enter = () => setFullscreen(true);
818
+ const leave = () => setFullscreen(false);
819
+ const video = videoRef.current;
820
+ document.addEventListener("fullscreenchange", sync);
821
+ video?.addEventListener("webkitbeginfullscreen", enter);
822
+ video?.addEventListener("webkitendfullscreen", leave);
823
+ return () => {
824
+ document.removeEventListener("fullscreenchange", sync);
825
+ video?.removeEventListener("webkitbeginfullscreen", enter);
826
+ video?.removeEventListener("webkitendfullscreen", leave);
827
+ };
828
+ }, [failed]);
829
+ const leaveFullscreen = React.useCallback(() => {
830
+ const current = document.fullscreenElement;
831
+ if (current && dialogRef.current?.contains(current) && typeof document.exitFullscreen === "function") {
832
+ ignoreRejection(document.exitFullscreen());
833
+ }
834
+ const video = videoRef.current;
835
+ if (video?.webkitDisplayingFullscreen) video.webkitExitFullscreen?.();
836
+ }, []);
837
+ const requestClose = React.useCallback(() => {
838
+ videoRef.current?.pause();
839
+ leaveFullscreen();
840
+ onClose();
841
+ }, [leaveFullscreen, onClose]);
842
+ const toggleFullscreen = React.useCallback(() => {
843
+ if (fullscreen) {
844
+ leaveFullscreen();
845
+ return;
846
+ }
847
+ if (support === "element") {
848
+ const panel = panelRef.current;
849
+ if (panel) ignoreRejection(panel.requestFullscreen());
850
+ return;
851
+ }
852
+ const video = videoRef.current;
853
+ video?.webkitEnterFullscreen?.();
854
+ }, [fullscreen, leaveFullscreen, support]);
855
+ const handleClose = React.useCallback(
856
+ (event) => {
857
+ if (event.currentTarget.open) return;
858
+ requestClose();
859
+ },
860
+ [requestClose]
861
+ );
862
+ const handleKeyDown = React.useCallback(
863
+ (event) => {
864
+ if (event.key === "Escape" && !nativeModalRef.current) {
865
+ event.preventDefault();
866
+ requestClose();
867
+ }
868
+ },
869
+ [requestClose]
870
+ );
871
+ const handleStageClick = React.useCallback(
872
+ (event) => {
873
+ if (event.target === event.currentTarget) requestClose();
874
+ },
875
+ [requestClose]
876
+ );
877
+ if (typeof document === "undefined") return null;
878
+ const fullscreenLabel = fullscreen ? T.exitFullScreen : T.enterFullScreen;
879
+ return reactDom.createPortal(
880
+ /* @__PURE__ */ jsxRuntime.jsxs(
881
+ "dialog",
882
+ {
883
+ ref: dialogRef,
884
+ "data-boff-explore": "video-dialog",
885
+ "aria-labelledby": titleId,
886
+ dir,
887
+ onClose: handleClose,
888
+ onKeyDown: handleKeyDown,
889
+ 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",
890
+ children: [
891
+ /* @__PURE__ */ jsxRuntime.jsx("style", { dangerouslySetInnerHTML: { __html: VIDEO_DIALOG_CSS } }),
892
+ /* @__PURE__ */ jsxRuntime.jsx(
893
+ "div",
894
+ {
895
+ onClick: handleStageClick,
896
+ className: "flex min-h-full items-center justify-center p-4 sm:p-6",
897
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
898
+ "div",
899
+ {
900
+ ref: panelRef,
901
+ className: "boff-explore-video-panel flex w-full min-w-0 max-w-[960px] flex-col gap-3",
902
+ children: [
903
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
904
+ /* @__PURE__ */ jsxRuntime.jsx(
905
+ "h2",
906
+ {
907
+ id: titleId,
908
+ className: "min-w-0 flex-1 truncate text-sm font-semibold text-foreground sm:text-base",
909
+ children: title
910
+ }
911
+ ),
912
+ support !== null && !failed ? (
913
+ // A plain button whose name says what pressing it does NOW, and no
914
+ // `aria-pressed`: a toggle button's name must not change with its state, or it
915
+ // announces "Exit full screen, pressed" — two answers to one question.
916
+ /* @__PURE__ */ jsxRuntime.jsx(
917
+ "button",
918
+ {
919
+ type: "button",
920
+ "data-boff-explore": "video-fullscreen",
921
+ "aria-label": fullscreenLabel,
922
+ title: fullscreenLabel,
923
+ onClick: toggleFullscreen,
924
+ className: DIALOG_BUTTON,
925
+ children: fullscreen ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Minimize, { "aria-hidden": "true", className: "h-5 w-5" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Maximize, { "aria-hidden": "true", className: "h-5 w-5" })
926
+ }
927
+ )
928
+ ) : null,
929
+ /* @__PURE__ */ jsxRuntime.jsx(
930
+ "button",
931
+ {
932
+ ref: closeRef,
933
+ type: "button",
934
+ "data-boff-explore": "video-close",
935
+ "aria-label": T.closeVideo,
936
+ title: T.closeVideo,
937
+ onClick: requestClose,
938
+ className: DIALOG_BUTTON,
939
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { "aria-hidden": "true", className: "h-5 w-5" })
940
+ }
941
+ )
942
+ ] }),
943
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "boff-explore-video-frame overflow-hidden rounded-xl border border-border bg-muted shadow-lg", children: failed ? (
944
+ // A black box with a spinning control tells the visitor nothing; this says
945
+ // what happened, in their language.
946
+ /* @__PURE__ */ jsxRuntime.jsxs(
947
+ "div",
948
+ {
949
+ role: "alert",
950
+ className: "flex aspect-video w-full flex-col items-center justify-center gap-2 p-6 text-center text-sm text-muted-foreground",
951
+ children: [
952
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.VideoOff, { "aria-hidden": "true", className: "h-6 w-6" }),
953
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: T.videoUnavailable })
954
+ ]
955
+ }
956
+ )
957
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
958
+ "video",
959
+ {
960
+ ref: videoRef,
961
+ "data-boff-explore": "video-player",
962
+ src: videoUrl,
963
+ poster: posterUrl,
964
+ controls: true,
965
+ controlsList: support === "element" ? "nofullscreen" : void 0,
966
+ playsInline: true,
967
+ autoPlay: true,
968
+ preload: "metadata",
969
+ onError: () => setFailed(true),
970
+ className: "block max-h-[80vh] w-full bg-muted object-contain"
971
+ }
972
+ ) })
973
+ ]
974
+ }
975
+ )
976
+ }
977
+ )
978
+ ]
979
+ }
980
+ ),
981
+ document.body
982
+ );
983
+ }
984
+ function MarkdownLink({
985
+ href,
986
+ children,
987
+ // react-markdown passes the hast node to every override; it is not a DOM attribute.
988
+ node: _node,
989
+ ...props
990
+ }) {
991
+ const isExternal = typeof href === "string" && (href.startsWith("http://") || href.startsWith("https://"));
992
+ return /* @__PURE__ */ jsxRuntime.jsx(
993
+ "a",
994
+ {
995
+ href,
996
+ ...props,
997
+ ...isExternal ? { target: "_blank", rel: "noopener noreferrer" } : { rel: "noopener noreferrer" },
998
+ children
999
+ }
1000
+ );
1001
+ }
1002
+ var defaultComponents = {
1003
+ a: MarkdownLink
1004
+ };
1005
+ function Markdown({
1006
+ children,
1007
+ className,
1008
+ inline,
1009
+ components,
1010
+ rehypePlugins
1011
+ }) {
1012
+ const content = /* @__PURE__ */ jsxRuntime.jsx(
1013
+ ReactMarkdown__default.default,
1014
+ {
1015
+ remarkPlugins: [remarkGfm__default.default],
1016
+ rehypePlugins: [rehypeSanitize__default.default, ...rehypePlugins ?? []],
1017
+ components: { ...defaultComponents, ...components },
1018
+ ...inline ? { disallowedElements: ["p"], unwrapDisallowed: true } : {},
1019
+ children
1020
+ }
1021
+ );
1022
+ if (inline) {
1023
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className, children: content });
1024
+ }
1025
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className, children: content });
1026
+ }
1027
+ function cn(...inputs) {
1028
+ return tailwindMerge.twMerge(clsx.clsx(inputs));
1029
+ }
1030
+ var EXPLORE_ANSWER_CSS = `
1031
+ /* Nothing inside an answer may widen the page. Model output carries long URLs, code and
1032
+ tables \u2014 each of those overflows a phone by default, and one overflowing child makes the
1033
+ WHOLE document horizontally scrollable, not just the bubble. */
1034
+ .boff-explore-body,
1035
+ .boff-explore-body p,
1036
+ .boff-explore-body li,
1037
+ .boff-explore-body a,
1038
+ .boff-explore-body h1,
1039
+ .boff-explore-body h2,
1040
+ .boff-explore-body h3,
1041
+ /* h4-h6 as well: a published answer page pushes the model's own headings down a level or
1042
+ two so they nest under the page's, and a long unbroken word is no narrower for it. */
1043
+ .boff-explore-body h4,
1044
+ .boff-explore-body h5,
1045
+ .boff-explore-body h6 {
1046
+ overflow-wrap: anywhere;
1047
+ word-break: break-word;
1048
+ }
1049
+ .boff-explore-body pre {
1050
+ overflow-x: auto;
1051
+ max-width: 100%;
1052
+ }
1053
+ .boff-explore-body table {
1054
+ display: block;
1055
+ overflow-x: auto;
1056
+ max-width: 100%;
1057
+ }
1058
+ .boff-explore-body img {
1059
+ max-width: 100%;
1060
+ height: auto;
1061
+ }
1062
+ /* A product-video poster fills the card's reserved 16:9 frame and is cropped to it. The
1063
+ blanket \`img { height: auto }\` above \u2014 and ProseStyles' own \u2014 outrank a height utility,
1064
+ and would size it by its own aspect ratio instead, leaving a band of the frame showing
1065
+ whenever a poster is not exactly 16:9. */
1066
+ .boff-explore-body .boff-explore-video-poster {
1067
+ height: 100%;
1068
+ max-width: none;
1069
+ }
1070
+
1071
+ /* ---- Comparison and pricing tables ----------------------------------------------------
1072
+ The harness emits a comparison, and a set of plans, as a GFM table whose FIRST column is
1073
+ the subject \u2014 the product, or the plan (CONTRACT.md \xA77). Two things have to be true of it:
1074
+ the page must not scroll sideways because of it, and it has to be readable on a 320px
1075
+ phone, which a four-column grid of 1px borders is not.
1076
+
1077
+ Wide: a contained scroller. \`max-width: 100%\` keeps the box inside the bubble and
1078
+ \`overflow-x: auto\` keeps the columns inside the box, so the widest possible table still
1079
+ cannot widen the document. \`overscroll-behavior-x: contain\` stops a swipe that runs out
1080
+ of table from chaining to the page \u2014 the same rule the transcript needed. */
1081
+ .boff-explore-body .boff-explore-table {
1082
+ max-width: 100%;
1083
+ overflow-x: auto;
1084
+ overscroll-behavior-x: contain;
1085
+ margin: 0.75rem 0;
1086
+ border: 1px solid hsl(var(--border));
1087
+ border-radius: 0.75rem;
1088
+ background-color: hsl(var(--card));
1089
+ }
1090
+ .boff-explore-body .boff-explore-table:focus-visible {
1091
+ outline: 2px solid hsl(var(--ring));
1092
+ outline-offset: 2px;
1093
+ }
1094
+ .boff-explore-body .boff-explore-table table {
1095
+ /* Beats the blanket \`display: block\` above, which is the unwrapped fallback. */
1096
+ display: table;
1097
+ width: 100%;
1098
+ max-width: none;
1099
+ margin: 0;
1100
+ border-collapse: collapse;
1101
+ font-size: 0.8125rem;
1102
+ }
1103
+ .boff-explore-body .boff-explore-table th,
1104
+ .boff-explore-body .boff-explore-table td {
1105
+ border: 0;
1106
+ border-top: 1px solid hsl(var(--border));
1107
+ padding: 0.5rem 0.75rem;
1108
+ /* \`start\`, never \`left\`: the page root carries dir="rtl" in Arabic, and a left-aligned
1109
+ table there reads against the grain of every other line on the page. */
1110
+ text-align: start;
1111
+ vertical-align: top;
1112
+ overflow-wrap: anywhere;
1113
+ /* Prices in a column line up under each other instead of drifting by digit width. */
1114
+ font-variant-numeric: tabular-nums;
1115
+ min-width: 6rem;
1116
+ }
1117
+ .boff-explore-body .boff-explore-table thead th {
1118
+ border-top: 0;
1119
+ background-color: hsl(var(--muted));
1120
+ color: hsl(var(--foreground));
1121
+ font-weight: 600;
1122
+ white-space: nowrap;
1123
+ }
1124
+ /* The first column is what the row is ABOUT \u2014 the product, or the plan name. Reading it as
1125
+ the row's heading is what makes a price list scannable rather than a grid of strings. */
1126
+ .boff-explore-body .boff-explore-table tbody td:first-child {
1127
+ font-weight: 600;
1128
+ color: hsl(var(--card-foreground));
1129
+ }
1130
+
1131
+ /* Narrow: one card per row. Each cell prints its own column heading, so a value is never
1132
+ stranded without the question it answers, and the subject cell becomes the card's title.
1133
+ A media query rather than a measured width \u2014 these pages are serialised post-hydration,
1134
+ so anything measured here would ship to every phone as static HTML. */
1135
+ @media (max-width: 30rem) {
1136
+ .boff-explore-body .boff-explore-table {
1137
+ overflow-x: visible;
1138
+ }
1139
+ .boff-explore-body .boff-explore-table table,
1140
+ .boff-explore-body .boff-explore-table tbody,
1141
+ .boff-explore-body .boff-explore-table tr,
1142
+ .boff-explore-body .boff-explore-table td {
1143
+ display: block;
1144
+ width: 100%;
1145
+ min-width: 0;
1146
+ }
1147
+ .boff-explore-body .boff-explore-table thead {
1148
+ display: none;
1149
+ }
1150
+ .boff-explore-body .boff-explore-table tr {
1151
+ border-top: 1px solid hsl(var(--border));
1152
+ padding: 0.5rem 0;
1153
+ }
1154
+ .boff-explore-body .boff-explore-table tr:first-child {
1155
+ border-top: 0;
1156
+ }
1157
+ .boff-explore-body .boff-explore-table td {
1158
+ border: 0;
1159
+ padding: 0.125rem 0.75rem;
1160
+ }
1161
+ .boff-explore-body .boff-explore-table td[data-label]::before {
1162
+ content: attr(data-label);
1163
+ display: block;
1164
+ font-size: 0.6875rem;
1165
+ font-weight: 600;
1166
+ text-transform: uppercase;
1167
+ letter-spacing: 0.04em;
1168
+ color: hsl(var(--muted-foreground));
1169
+ }
1170
+ .boff-explore-body .boff-explore-table tbody td:first-child {
1171
+ font-size: 0.9375rem;
1172
+ padding-bottom: 0.25rem;
1173
+ }
1174
+ .boff-explore-body .boff-explore-table tbody td:first-child::before {
1175
+ /* The plan's own name is the heading here; repeating "Plan" over it says nothing. */
1176
+ display: none;
1177
+ }
1178
+ }
1179
+ `;
1180
+ function ExploreAnswerStyles() {
1181
+ return /* @__PURE__ */ jsxRuntime.jsx("style", { dangerouslySetInnerHTML: { __html: EXPLORE_ANSWER_CSS } });
1182
+ }
1183
+ function inlineImageOrigins(products) {
1184
+ const origins = /* @__PURE__ */ new Set();
1185
+ for (const product of products) {
1186
+ let url;
1187
+ try {
1188
+ url = new URL(product.website);
1189
+ } catch {
1190
+ continue;
1191
+ }
1192
+ if (url.protocol !== "https:") continue;
1193
+ const apex = url.hostname.replace(/^www\./, "");
1194
+ const port = url.port ? `:${url.port}` : "";
1195
+ origins.add(`https://${apex}${port}`);
1196
+ origins.add(`https://www.${apex}${port}`);
1197
+ }
1198
+ return origins;
1199
+ }
1200
+ var VIDEO_FILE_PATH = /\.(?:mp4|webm|m4v|mov|vtt)$/i;
1201
+ function catalogUrlOf(value, origins) {
1202
+ if (typeof value !== "string" || value === "") return null;
1203
+ let url;
1204
+ try {
1205
+ url = new URL(value);
1206
+ } catch {
1207
+ return null;
1208
+ }
1209
+ if (url.protocol !== "https:" || url.username || url.password) return null;
1210
+ return origins.has(url.origin) ? url : null;
1211
+ }
1212
+ function isInlineImageAllowed(src, origins) {
1213
+ const url = catalogUrlOf(src, origins);
1214
+ return url !== null && !VIDEO_FILE_PATH.test(url.pathname);
1215
+ }
1216
+ var INLINE_VIDEO_PATH = /\.(?:mp4|webm)$/i;
1217
+ function isInlineVideoAllowed(href, origins) {
1218
+ const url = catalogUrlOf(href, origins);
1219
+ return url !== null && INLINE_VIDEO_PATH.test(url.pathname);
1220
+ }
1221
+ function apexOriginOf(url) {
1222
+ const apex = url.hostname.replace(/^www\./, "");
1223
+ const port = url.port ? `:${url.port}` : "";
1224
+ return `https://${apex}${port}`;
1225
+ }
1226
+ function productSlugsByOrigin(products) {
1227
+ const slugs = /* @__PURE__ */ new Map();
1228
+ for (const product of products) {
1229
+ let url;
1230
+ try {
1231
+ url = new URL(product.website);
1232
+ } catch {
1233
+ continue;
1234
+ }
1235
+ if (url.protocol !== "https:") continue;
1236
+ const key = apexOriginOf(url);
1237
+ if (!slugs.has(key)) slugs.set(key, product.slug);
1238
+ }
1239
+ return slugs;
1240
+ }
1241
+ function videoProductOf(href, slugs) {
1242
+ const url = new URL(href);
1243
+ return slugs.get(apexOriginOf(url)) ?? url.hostname.replace(/^www\./, "");
1244
+ }
1245
+ function isVideoFileTarget(href) {
1246
+ if (typeof href !== "string") return false;
1247
+ try {
1248
+ return VIDEO_FILE_PATH.test(
1249
+ new URL(href, "https://answer.invalid").pathname
1250
+ );
1251
+ } catch {
1252
+ return false;
1253
+ }
1254
+ }
1255
+ var CODE_FENCE = /^\s*(?:```|~~~)/;
1256
+ var TABLE_ROW_LINE = /^\s{0,3}\|/;
1257
+ var TABLE_DELIMITER_ROW = /^\s{0,3}\|[\s:|-]*-[\s:|-]*$/;
1258
+ var LINKED_IMAGE = /\[!\[([^[\]]*)\]\([^)\s]*(?:\s+"[^"]*")?\)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
1259
+ var IMAGE = /!\[[^[\]]*\]\([^)\s]*(?:\s+"[^"]*")?\)/g;
1260
+ var LINK = /\[([^[\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
1261
+ function readableLine(raw, mode) {
1262
+ let line = raw;
1263
+ if (line.includes("](")) {
1264
+ line = line.replace(LINKED_IMAGE, (_match, alt, href) => {
1265
+ if (mode === "speech") return "";
1266
+ return alt.trim() === "" ? href : `${alt.trim()} (${href})`;
1267
+ });
1268
+ line = line.replace(IMAGE, "");
1269
+ line = mode === "speech" ? (
1270
+ // A voice reading "h-t-t-p-s colon slash slash…" mid-sentence is noise, and the
1271
+ // prompt asks for two to five links in every answer. The label IS the words.
1272
+ line.replace(LINK, "$1")
1273
+ ) : line.replace(LINK, "$1 ($2)");
1274
+ }
1275
+ line = mode === "speech" ? line.replace(/<(https?:\/\/[^>\s]+)>/g, "") : line.replace(/<(https?:\/\/[^>\s]+)>/g, "$1");
1276
+ line = line.replace(/^\s{0,3}#{1,6}\s+/, "");
1277
+ line = line.replace(/^\s{0,3}>\s?/, "");
1278
+ if (/^\s{0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/.test(line)) return "";
1279
+ line = line.replace(/`([^`]+)`/g, "$1");
1280
+ line = line.replace(/\*\*([^*]+)\*\*/g, "$1");
1281
+ line = line.replace(/__([^_]+)__/g, "$1");
1282
+ line = line.replace(/(^|[^*\w])\*([^*\n]+)\*(?![*\w])/g, "$1$2");
1283
+ line = line.replace(/(^|[^_\w])_([^_\n]+)_(?![_\w])/g, "$1$2");
1284
+ return line.replace(/[ \t]+$/, "");
1285
+ }
1286
+ function tableCells(row) {
1287
+ const trimmed = row.trim();
1288
+ const cells = [];
1289
+ let cell = "";
1290
+ for (let at = 0; at < trimmed.length; at += 1) {
1291
+ const char = trimmed[at] ?? "";
1292
+ if (char === "\\") {
1293
+ cell += char + (trimmed[at + 1] ?? "");
1294
+ at += 1;
1295
+ continue;
1296
+ }
1297
+ if (char === "|") {
1298
+ cells.push(cell.trim());
1299
+ cell = "";
1300
+ continue;
1301
+ }
1302
+ cell += char;
1303
+ }
1304
+ cells.push(cell.trim());
1305
+ if (trimmed.startsWith("|") && cells[0] === "") cells.shift();
1306
+ if (trimmed.endsWith("|") && cells[cells.length - 1] === "") cells.pop();
1307
+ return cells;
1308
+ }
1309
+ function asSentence(text) {
1310
+ return /[.!?]$/.test(text) ? text : `${text}.`;
1311
+ }
1312
+ function spokenTable(rows) {
1313
+ if (rows.length < 2 || !TABLE_DELIMITER_ROW.test(rows[1] ?? "")) return rows;
1314
+ const labels = tableCells(rows[0] ?? "");
1315
+ const spoken = [];
1316
+ for (const row of rows.slice(2)) {
1317
+ const parts = [];
1318
+ tableCells(row).forEach((value, column) => {
1319
+ if (value === "") return;
1320
+ const label = column === 0 ? "" : labels[column] ?? "";
1321
+ parts.push(label === "" ? value : `${label}: ${value}`);
1322
+ });
1323
+ if (parts.length > 0) spoken.push(asSentence(parts.join(". ")));
1324
+ }
1325
+ return spoken;
1326
+ }
1327
+ function readableText(markdown, mode) {
1328
+ const lines = [];
1329
+ let inFence = false;
1330
+ let table = [];
1331
+ const flushTable = () => {
1332
+ if (table.length === 0) return;
1333
+ for (const line of spokenTable(table)) lines.push(readableLine(line, mode));
1334
+ table = [];
1335
+ };
1336
+ for (const raw of markdown.split("\n")) {
1337
+ if (CODE_FENCE.test(raw)) {
1338
+ flushTable();
1339
+ inFence = !inFence;
1340
+ continue;
1341
+ }
1342
+ if (inFence) {
1343
+ lines.push(raw);
1344
+ continue;
1345
+ }
1346
+ if (mode === "speech" && TABLE_ROW_LINE.test(raw)) {
1347
+ table.push(raw);
1348
+ continue;
1349
+ }
1350
+ flushTable();
1351
+ lines.push(readableLine(raw, mode));
1352
+ }
1353
+ flushTable();
1354
+ return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
1355
+ }
1356
+ function answerPlainText(markdown) {
1357
+ return readableText(markdown, "clipboard");
1358
+ }
1359
+ function answerSpeechText(markdown) {
1360
+ return readableText(markdown, "speech");
1361
+ }
1362
+ var AnswerScopeContext = React.createContext(null);
1363
+ var InsideLinkContext = React.createContext(false);
1364
+ function AnswerLink(props) {
1365
+ const scope = React.useContext(AnswerScopeContext);
1366
+ const role = videoRoleOf(props.node);
1367
+ if (role !== void 0 && scope !== null) {
1368
+ const video = role === "card" && scope.enabled ? productVideoOf(props.node) : null;
1369
+ if (video === null) return null;
1370
+ return /* @__PURE__ */ jsxRuntime.jsx(
1371
+ InlineVideoCard,
1372
+ {
1373
+ href: video.href,
1374
+ poster: video.poster,
1375
+ alt: video.alt,
1376
+ scope
1377
+ }
1378
+ );
1151
1379
  }
1152
- return "en";
1380
+ return /* @__PURE__ */ jsxRuntime.jsx(InsideLinkContext.Provider, { value: true, children: /* @__PURE__ */ jsxRuntime.jsx(MarkdownLink, { ...props }) });
1153
1381
  }
1154
- function exploreStrings(locale) {
1155
- return STRINGS[locale] ?? EN;
1382
+ var INLINE_IMAGE_FRAME = "relative inline-block max-w-full overflow-hidden rounded-xl border border-border bg-muted align-top shadow-sm";
1383
+ function InlineConceptImage({ src, alt }) {
1384
+ const scope = React.useContext(AnswerScopeContext);
1385
+ const insideLink = React.useContext(InsideLinkContext);
1386
+ if (!scope || !scope.enabled || !isInlineImageAllowed(src, scope.origins) || scope.failed.has(src)) {
1387
+ return null;
1388
+ }
1389
+ const { strings: T, onFailed } = scope;
1390
+ const picture = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1391
+ /* @__PURE__ */ jsxRuntime.jsx(
1392
+ "img",
1393
+ {
1394
+ src,
1395
+ alt: alt ?? "",
1396
+ loading: "lazy",
1397
+ decoding: "async",
1398
+ onError: () => onFailed(src),
1399
+ className: "block h-auto max-h-80 w-auto max-w-full object-contain"
1400
+ }
1401
+ ),
1402
+ /* @__PURE__ */ jsxRuntime.jsx(
1403
+ "span",
1404
+ {
1405
+ "aria-hidden": "true",
1406
+ 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",
1407
+ children: T.conceptIllustration
1408
+ }
1409
+ )
1410
+ ] });
1411
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { "data-boff-explore": "inline-image", className: "my-3 block", children: insideLink ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: INLINE_IMAGE_FRAME, children: picture }) : /* @__PURE__ */ jsxRuntime.jsxs(
1412
+ "a",
1413
+ {
1414
+ href: src,
1415
+ target: "_blank",
1416
+ rel: "noopener noreferrer",
1417
+ className: cn(
1418
+ INLINE_IMAGE_FRAME,
1419
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1420
+ ),
1421
+ children: [
1422
+ picture,
1423
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sr-only", children: [
1424
+ "(",
1425
+ T.opensInNewTab,
1426
+ ")"
1427
+ ] })
1428
+ ]
1429
+ }
1430
+ ) });
1156
1431
  }
1157
- function fill(template, values) {
1158
- return template.replace(/\{(\w+)\}/g, (match, key) => values[key] ?? match);
1432
+ function markdownText(node) {
1433
+ if (!node) return "";
1434
+ if (typeof node.value === "string") return node.value;
1435
+ return (node.children ?? []).map(markdownText).join("");
1436
+ }
1437
+ function hasHeaderCells(node) {
1438
+ return (node?.children ?? []).some((cell) => cell.tagName === "th");
1439
+ }
1440
+ function findHeaderRow(node) {
1441
+ for (const child of node?.children ?? []) {
1442
+ if (child.tagName === "tr" && hasHeaderCells(child)) return child;
1443
+ const nested = findHeaderRow(child);
1444
+ if (nested !== null) return nested;
1445
+ }
1446
+ return null;
1447
+ }
1448
+ function tableColumnLabels(node) {
1449
+ const row = findHeaderRow(node);
1450
+ if (row === null) return [];
1451
+ return (row.children ?? []).filter((cell) => cell.tagName === "th").map((cell) => markdownText(cell).trim());
1452
+ }
1453
+ var TableColumnsContext = React.createContext([]);
1454
+ function AnswerTable({ node, children }) {
1455
+ const scope = React.useContext(AnswerScopeContext);
1456
+ const table = /* @__PURE__ */ jsxRuntime.jsx(TableColumnsContext.Provider, { value: tableColumnLabels(node), children: /* @__PURE__ */ jsxRuntime.jsx("table", { children }) });
1457
+ if (!scope) return table;
1458
+ return /* @__PURE__ */ jsxRuntime.jsx(
1459
+ "div",
1460
+ {
1461
+ "data-boff-explore": "table",
1462
+ role: "region",
1463
+ "aria-label": scope.strings.answerTable,
1464
+ tabIndex: 0,
1465
+ className: "boff-explore-table",
1466
+ children: table
1467
+ }
1468
+ );
1469
+ }
1470
+ function AnswerTableRow({
1471
+ node,
1472
+ children,
1473
+ ...props
1474
+ }) {
1475
+ const labels = React.useContext(TableColumnsContext);
1476
+ if (labels.length === 0 || hasHeaderCells(node)) {
1477
+ return /* @__PURE__ */ jsxRuntime.jsx("tr", { ...props, children });
1478
+ }
1479
+ let column = 0;
1480
+ const cells = React.Children.map(children, (cell) => {
1481
+ if (!React.isValidElement(cell)) return cell;
1482
+ const label = labels[column] ?? "";
1483
+ column += 1;
1484
+ if (label === "") return cell;
1485
+ return React.cloneElement(cell, {
1486
+ "data-label": label
1487
+ });
1488
+ });
1489
+ return /* @__PURE__ */ jsxRuntime.jsx("tr", { ...props, children: cells });
1490
+ }
1491
+ function linkedImageOf(node) {
1492
+ if (node?.tagName !== "a") return null;
1493
+ const content = (node.children ?? []).filter(
1494
+ (child) => child.tagName !== void 0 || (child.value ?? "").trim() !== ""
1495
+ );
1496
+ const image = content.length === 1 ? content[0] : void 0;
1497
+ if (image?.tagName !== "img") return null;
1498
+ const alt = image.properties?.alt;
1499
+ return {
1500
+ href: node.properties?.href,
1501
+ src: image.properties?.src,
1502
+ alt: typeof alt === "string" ? alt.trim() : ""
1503
+ };
1504
+ }
1505
+ function isCardableVideo(embed, origins) {
1506
+ if (!isInlineVideoAllowed(embed.href, origins) || !isInlineImageAllowed(embed.src, origins)) {
1507
+ return false;
1508
+ }
1509
+ return apexOriginOf(new URL(embed.href)) === apexOriginOf(new URL(embed.src));
1510
+ }
1511
+ var NO_VIDEO_CONTAINERS = /* @__PURE__ */ new Set(["li", "table"]);
1512
+ function rehypeProductVideo(options) {
1513
+ return (tree) => {
1514
+ let claimed = false;
1515
+ const visit = (node, parent, contained) => {
1516
+ const embed = linkedImageOf(node);
1517
+ if (embed !== null && isVideoFileTarget(embed.href)) {
1518
+ const card = !claimed && !contained && parent?.tagName === "p" && isCardableVideo(embed, options.origins);
1519
+ if (card) claimed = true;
1520
+ const data = typeof node.data === "object" ? node.data : null;
1521
+ node.data = { ...data, boffVideo: card ? "card" : "drop" };
1522
+ return;
1523
+ }
1524
+ const inside = contained || node.tagName !== void 0 && NO_VIDEO_CONTAINERS.has(node.tagName);
1525
+ for (const child of node.children ?? []) visit(child, node, inside);
1526
+ };
1527
+ visit(tree, null, false);
1528
+ };
1529
+ }
1530
+ function videoRoleOf(node) {
1531
+ const data = node?.data;
1532
+ if (typeof data !== "object" || data === null || !("boffVideo" in data)) {
1533
+ return void 0;
1534
+ }
1535
+ return data.boffVideo === "card" || data.boffVideo === "drop" ? data.boffVideo : void 0;
1536
+ }
1537
+ function productVideoOf(node) {
1538
+ const embed = linkedImageOf(node);
1539
+ if (embed === null || typeof embed.href !== "string" || typeof embed.src !== "string") {
1540
+ return null;
1541
+ }
1542
+ return { href: embed.href, poster: embed.src, alt: embed.alt };
1543
+ }
1544
+ var CANONICAL_VIDEO_ALT = /^(.+?)\s+product video$/i;
1545
+ function videoTitleOf(alt, T) {
1546
+ const product = CANONICAL_VIDEO_ALT.exec(alt)?.[1]?.trim();
1547
+ if (product) return fill(T.productVideoTitle, { product });
1548
+ return alt === "" ? T.productVideo : alt;
1549
+ }
1550
+ 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";
1551
+ function InlineVideoCard({
1552
+ href,
1553
+ poster,
1554
+ alt,
1555
+ scope
1556
+ }) {
1557
+ const { strings: T, failed, onFailed, productSlugs } = scope;
1558
+ const [dialogDir, setDialogDir] = React.useState(null);
1559
+ const buttonRef = React.useRef(null);
1560
+ const returnFocusRef = React.useRef(false);
1561
+ const open = dialogDir !== null;
1562
+ React.useEffect(() => {
1563
+ if (open || !returnFocusRef.current) return;
1564
+ returnFocusRef.current = false;
1565
+ const button = buttonRef.current;
1566
+ if (button?.isConnected) button.focus();
1567
+ }, [open]);
1568
+ const openPlayer = React.useCallback((event) => {
1569
+ const dir = event.currentTarget.closest("[dir]")?.getAttribute("dir");
1570
+ returnFocusRef.current = true;
1571
+ setDialogDir(dir === "rtl" ? "rtl" : "ltr");
1572
+ }, []);
1573
+ const closePlayer = React.useCallback(() => setDialogDir(null), []);
1574
+ const name = videoTitleOf(alt, T);
1575
+ const posterFailed = failed.has(poster);
1576
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1577
+ "span",
1578
+ {
1579
+ "data-boff-explore": "inline-video",
1580
+ "data-product": videoProductOf(href, productSlugs),
1581
+ className: "my-3 block",
1582
+ children: [
1583
+ /* @__PURE__ */ jsxRuntime.jsxs(
1584
+ "button",
1585
+ {
1586
+ ref: buttonRef,
1587
+ type: "button",
1588
+ "aria-haspopup": "dialog",
1589
+ "aria-label": fill(T.playVideo, { title: name }),
1590
+ onClick: openPlayer,
1591
+ className: INLINE_VIDEO_FRAME,
1592
+ children: [
1593
+ posterFailed ? /* @__PURE__ */ jsxRuntime.jsx(
1594
+ "span",
1595
+ {
1596
+ "aria-hidden": "true",
1597
+ className: "absolute inset-0 flex items-center justify-center bg-muted text-muted-foreground",
1598
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Film, { className: "h-10 w-10" })
1599
+ }
1600
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
1601
+ "img",
1602
+ {
1603
+ src: poster,
1604
+ alt: "",
1605
+ loading: "lazy",
1606
+ decoding: "async",
1607
+ onError: () => onFailed(poster),
1608
+ className: "boff-explore-video-poster absolute inset-0 h-full w-full object-cover"
1609
+ }
1610
+ ),
1611
+ /* @__PURE__ */ jsxRuntime.jsx(
1612
+ "span",
1613
+ {
1614
+ "aria-hidden": "true",
1615
+ className: "pointer-events-none absolute inset-0 flex items-center justify-center",
1616
+ children: /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(lucideReact.Play, { className: "h-6 w-6 fill-current" }) })
1617
+ }
1618
+ ),
1619
+ /* @__PURE__ */ jsxRuntime.jsx(
1620
+ "span",
1621
+ {
1622
+ "aria-hidden": "true",
1623
+ 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",
1624
+ children: T.productVideo
1625
+ }
1626
+ )
1627
+ ]
1628
+ }
1629
+ ),
1630
+ open ? /* @__PURE__ */ jsxRuntime.jsx(
1631
+ ExploreVideoDialog,
1632
+ {
1633
+ videoUrl: href,
1634
+ posterUrl: poster,
1635
+ title: name,
1636
+ strings: T,
1637
+ dir: dialogDir,
1638
+ onClose: closePlayer
1639
+ }
1640
+ ) : null
1641
+ ]
1642
+ }
1643
+ );
1644
+ }
1645
+ var ANSWER_MARKDOWN_COMPONENTS = {
1646
+ a: AnswerLink,
1647
+ img: InlineConceptImage,
1648
+ table: AnswerTable,
1649
+ tr: AnswerTableRow
1650
+ };
1651
+ var NO_PRODUCT_SLUGS = /* @__PURE__ */ new Map();
1652
+ function ExploreAnswerBody({
1653
+ children,
1654
+ strings,
1655
+ imageOrigins,
1656
+ videoOrigins = imageOrigins,
1657
+ productSlugs = NO_PRODUCT_SLUGS,
1658
+ enabled = true,
1659
+ components,
1660
+ className
1661
+ }) {
1662
+ const [failed, setFailed] = React.useState(() => /* @__PURE__ */ new Set());
1663
+ const onFailed = React.useCallback((src) => {
1664
+ setFailed(
1665
+ (current) => current.has(src) ? current : new Set(current).add(src)
1666
+ );
1667
+ }, []);
1668
+ const scope = React.useMemo(
1669
+ () => ({
1670
+ strings,
1671
+ enabled,
1672
+ origins: imageOrigins,
1673
+ productSlugs,
1674
+ failed,
1675
+ onFailed
1676
+ }),
1677
+ [strings, enabled, imageOrigins, productSlugs, failed, onFailed]
1678
+ );
1679
+ const rehypePlugins = React.useMemo(
1680
+ () => [[rehypeProductVideo, { origins: videoOrigins }]],
1681
+ [videoOrigins]
1682
+ );
1683
+ const overrides = React.useMemo(
1684
+ () => components ? { ...ANSWER_MARKDOWN_COMPONENTS, ...components } : ANSWER_MARKDOWN_COMPONENTS,
1685
+ [components]
1686
+ );
1687
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("boff-prose boff-explore-body min-w-0", className), children: /* @__PURE__ */ jsxRuntime.jsx(AnswerScopeContext.Provider, { value: scope, children: /* @__PURE__ */ jsxRuntime.jsx(Markdown, { components: overrides, rehypePlugins, children }) }) });
1159
1688
  }
1160
1689
 
1161
1690
  // src/components/explore-types.ts
@@ -3064,7 +3593,7 @@ function isHighSurrogate(code) {
3064
3593
  }
3065
3594
  function safeInlineEnd(line) {
3066
3595
  let codeSpan = null;
3067
- let link = null;
3596
+ const links = [];
3068
3597
  const emphasis = [];
3069
3598
  let at = 0;
3070
3599
  while (at < line.length) {
@@ -3073,6 +3602,13 @@ function safeInlineEnd(line) {
3073
3602
  at += 2;
3074
3603
  continue;
3075
3604
  }
3605
+ const innermost = links[links.length - 1];
3606
+ if (innermost !== void 0 && innermost.kind !== "label") {
3607
+ if (char === "(") links.push({ kind: "paren", start: innermost.start });
3608
+ else if (char === ")") links.pop();
3609
+ at += 1;
3610
+ continue;
3611
+ }
3076
3612
  if (char === "`") {
3077
3613
  let run = 1;
3078
3614
  while (line[at + run] === "`") run += 1;
@@ -3085,17 +3621,17 @@ function safeInlineEnd(line) {
3085
3621
  continue;
3086
3622
  }
3087
3623
  if (char === "[") {
3088
- if (link === null) link = line[at - 1] === "!" ? at - 1 : at;
3624
+ links.push({ kind: "label", start: line[at - 1] === "!" ? at - 1 : at });
3089
3625
  at += 1;
3090
3626
  continue;
3091
3627
  }
3092
- if (char === "]") {
3093
- if (link !== null && line[at + 1] !== "(") link = null;
3094
- at += 1;
3095
- continue;
3096
- }
3097
- if (char === ")") {
3098
- link = null;
3628
+ if (char === "]" && innermost !== void 0) {
3629
+ links.pop();
3630
+ if (line[at + 1] === "(") {
3631
+ links.push({ kind: "destination", start: innermost.start });
3632
+ at += 2;
3633
+ continue;
3634
+ }
3099
3635
  at += 1;
3100
3636
  continue;
3101
3637
  }
@@ -3122,9 +3658,11 @@ function safeInlineEnd(line) {
3122
3658
  }
3123
3659
  at += 1;
3124
3660
  }
3125
- const open = [codeSpan, link, emphasis[0]?.start ?? null].filter(
3126
- (value) => value !== null
3127
- );
3661
+ const open = [
3662
+ codeSpan,
3663
+ links[0]?.start ?? null,
3664
+ emphasis[0]?.start ?? null
3665
+ ].filter((value) => value !== null);
3128
3666
  return open.length === 0 ? line.length : Math.min(...open);
3129
3667
  }
3130
3668
  function revealBoundary(text, limit) {
@@ -5111,6 +5649,7 @@ function AssistantBubble({
5111
5649
  message,
5112
5650
  activity,
5113
5651
  imageOrigins,
5652
+ productSlugs,
5114
5653
  productNames,
5115
5654
  speechLang,
5116
5655
  onNavigate,
@@ -5225,6 +5764,7 @@ function AssistantBubble({
5225
5764
  {
5226
5765
  strings: T,
5227
5766
  imageOrigins,
5767
+ productSlugs,
5228
5768
  enabled: completed,
5229
5769
  className: "boff-prose-sm text-card-foreground",
5230
5770
  children: visible
@@ -5982,6 +6522,10 @@ function ExplorePage({
5982
6522
  () => productNamesByOrigin(catalog.products),
5983
6523
  [catalog.products]
5984
6524
  );
6525
+ const productSlugs = React.useMemo(
6526
+ () => productSlugsByOrigin(catalog.products),
6527
+ [catalog.products]
6528
+ );
5985
6529
  React.useEffect(() => {
5986
6530
  const el = textareaRef.current;
5987
6531
  if (!el) return;
@@ -6544,6 +7088,7 @@ function ExplorePage({
6544
7088
  message,
6545
7089
  activity,
6546
7090
  imageOrigins,
7091
+ productSlugs,
6547
7092
  productNames,
6548
7093
  speechLang,
6549
7094
  onNavigate,
@@ -6874,6 +7419,7 @@ exports.answerSpeechText = answerSpeechText;
6874
7419
  exports.exploreQuestionHref = exploreQuestionHref;
6875
7420
  exports.inlineImageOrigins = inlineImageOrigins;
6876
7421
  exports.isInlineImageAllowed = isInlineImageAllowed;
7422
+ exports.isInlineVideoAllowed = isInlineVideoAllowed;
6877
7423
  exports.logoUrlOf = logoUrlOf;
6878
7424
  exports.productNamesByOrigin = productNamesByOrigin;
6879
7425
  exports.referenceKindOf = referenceKindOf;