@burdenoff/website-sdk 2026.925.2 → 2026.926.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/components/contact.js +8 -0
  2. package/dist/components/contact.js.map +1 -1
  3. package/dist/components/contact.mjs +8 -0
  4. package/dist/components/contact.mjs.map +1 -1
  5. package/dist/components/explore.d.mts +1 -1
  6. package/dist/components/explore.d.ts +1 -1
  7. package/dist/components/explore.js +1101 -518
  8. package/dist/components/explore.js.map +1 -1
  9. package/dist/components/explore.mjs +1103 -521
  10. package/dist/components/explore.mjs.map +1 -1
  11. package/dist/components/newsletter.js +8 -0
  12. package/dist/components/newsletter.js.map +1 -1
  13. package/dist/components/newsletter.mjs +8 -0
  14. package/dist/components/newsletter.mjs.map +1 -1
  15. package/dist/components/partners.js +11 -2
  16. package/dist/components/partners.js.map +1 -1
  17. package/dist/components/partners.mjs +11 -2
  18. package/dist/components/partners.mjs.map +1 -1
  19. package/dist/components/resource-links.js +8 -0
  20. package/dist/components/resource-links.js.map +1 -1
  21. package/dist/components/resource-links.mjs +8 -0
  22. package/dist/components/resource-links.mjs.map +1 -1
  23. package/dist/components/social-links.js +8 -0
  24. package/dist/components/social-links.js.map +1 -1
  25. package/dist/components/social-links.mjs +8 -0
  26. package/dist/components/social-links.mjs.map +1 -1
  27. package/dist/components/waitlist.js +8 -0
  28. package/dist/components/waitlist.js.map +1 -1
  29. package/dist/components/waitlist.mjs +8 -0
  30. package/dist/components/waitlist.mjs.map +1 -1
  31. package/dist/content/index.d.mts +1 -1
  32. package/dist/content/index.d.ts +1 -1
  33. package/dist/content/index.js +11 -2
  34. package/dist/content/index.js.map +1 -1
  35. package/dist/content/index.mjs +11 -2
  36. package/dist/content/index.mjs.map +1 -1
  37. package/dist/{explore-C0RFTKAi.d.ts → explore-Cam_ElSJ.d.ts} +70 -12
  38. package/dist/{explore-DzOf7n4m.d.mts → explore-hTQm-_SF.d.mts} +70 -12
  39. package/dist/hooks/index.js +51 -13
  40. package/dist/hooks/index.js.map +1 -1
  41. package/dist/hooks/index.mjs +51 -13
  42. package/dist/hooks/index.mjs.map +1 -1
  43. package/dist/{index-BMWQNZDX.d.mts → index-gm3pBCJT.d.mts} +8 -2
  44. package/dist/{index-BqHF-G-S.d.ts → index-ySbOLSD5.d.ts} +8 -2
  45. package/dist/index.d.mts +32 -11
  46. package/dist/index.d.ts +32 -11
  47. package/dist/index.js +901 -267
  48. package/dist/index.js.map +1 -1
  49. package/dist/index.mjs +892 -269
  50. package/dist/index.mjs.map +1 -1
  51. package/dist/product/index.d.mts +16 -1
  52. package/dist/product/index.d.ts +16 -1
  53. package/dist/product/index.js +61 -17
  54. package/dist/product/index.js.map +1 -1
  55. package/dist/product/index.mjs +61 -18
  56. package/dist/product/index.mjs.map +1 -1
  57. package/dist/store/index.js +11 -2
  58. package/dist/store/index.js.map +1 -1
  59. package/dist/store/index.mjs +11 -2
  60. package/dist/store/index.mjs.map +1 -1
  61. package/dist/utils/index.d.mts +41 -1
  62. package/dist/utils/index.d.ts +41 -1
  63. package/dist/utils/index.js +42 -0
  64. package/dist/utils/index.js.map +1 -1
  65. package/dist/utils/index.mjs +35 -1
  66. package/dist/utils/index.mjs.map +1 -1
  67. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ var zod$1 = require('@hookform/resolvers/zod');
19
19
  var reactHookForm = require('react-hook-form');
20
20
  var zod = require('zod');
21
21
  var DropdownMenu = require('@radix-ui/react-dropdown-menu');
22
+ var reactDom = require('react-dom');
22
23
 
23
24
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
24
25
 
@@ -374,6 +375,40 @@ function useWebSDKConfig() {
374
375
  const client = useWebSDK();
375
376
  return client.getConfig();
376
377
  }
378
+
379
+ // src/utils/product-slug.ts
380
+ var PRODUCT_SLUG_RENAMES = Object.freeze(
381
+ [Object.freeze({ legacy: "botlit", current: "botmanor" })]
382
+ );
383
+ var PRODUCT_SLUG_ALIASES = Object.freeze(
384
+ Object.fromEntries(PRODUCT_SLUG_RENAMES.map((r) => [r.legacy, r.current]))
385
+ );
386
+ var PRODUCT_SLUG_EMIT = Object.freeze({ productSlug: "legacy" });
387
+ function findRename(slug) {
388
+ const wanted = slug.toLowerCase();
389
+ return PRODUCT_SLUG_RENAMES.find(
390
+ (r) => r.legacy === wanted || r.current === wanted
391
+ );
392
+ }
393
+ function normalizeProductSlug(slug) {
394
+ if (slug == null) return slug;
395
+ return findRename(slug)?.current ?? slug;
396
+ }
397
+ function legacyProductSlug(slug) {
398
+ return findRename(slug)?.legacy ?? slug;
399
+ }
400
+ function productSlugVariants(slug) {
401
+ return [
402
+ .../* @__PURE__ */ new Set([slug, normalizeProductSlug(slug), legacyProductSlug(slug)])
403
+ ];
404
+ }
405
+ function isSameProductSlug(a, b) {
406
+ if (a == null || b == null) return a === b;
407
+ return a === b || normalizeProductSlug(a) === normalizeProductSlug(b);
408
+ }
409
+ function emitProductSlug(slug) {
410
+ return PRODUCT_SLUG_EMIT.productSlug === "current" ? normalizeProductSlug(slug) : legacyProductSlug(slug);
411
+ }
377
412
  var PRODUCT_FIELDS_BASE = `
378
413
  id
379
414
  name
@@ -421,6 +456,35 @@ var ProductContext = React.createContext({
421
456
  refetch: () => {
422
457
  }
423
458
  });
459
+ async function queryPublicProduct(client, slug) {
460
+ const result = await client.query(
461
+ PUBLIC_PRODUCT_QUERY,
462
+ { slug }
463
+ );
464
+ const schemaMissingResourceLinks = result.errors?.some(
465
+ (e) => /Cannot query field ["']?resourceLinks["']?/i.test(e?.message ?? "")
466
+ );
467
+ if (!schemaMissingResourceLinks) return result;
468
+ return client.query(
469
+ PUBLIC_PRODUCT_QUERY_FALLBACK,
470
+ { slug }
471
+ );
472
+ }
473
+ async function fetchPublicProduct(client, slug) {
474
+ for (const candidate of productSlugVariants(slug)) {
475
+ const result = await queryPublicProduct(client, candidate);
476
+ if (result.errors?.length) {
477
+ return {
478
+ product: null,
479
+ error: result.errors[0]?.message ?? "Unknown error"
480
+ };
481
+ }
482
+ if (result.data?.publicProduct) {
483
+ return { product: result.data.publicProduct, error: null };
484
+ }
485
+ }
486
+ return { product: null, error: null };
487
+ }
424
488
  function ProductProvider({ children, fallback }) {
425
489
  const client = useWebSDK();
426
490
  const config = useWebSDKConfig();
@@ -437,23 +501,11 @@ function ProductProvider({ children, fallback }) {
437
501
  setLoading(true);
438
502
  setError(null);
439
503
  try {
440
- let result = await client.query(
441
- PUBLIC_PRODUCT_QUERY,
442
- { slug: config.productSlug }
443
- );
444
- const schemaMissingResourceLinks = result.errors?.some(
445
- (e) => /Cannot query field ["']?resourceLinks["']?/i.test(e?.message ?? "")
446
- );
447
- if (schemaMissingResourceLinks) {
448
- result = await client.query(
449
- PUBLIC_PRODUCT_QUERY_FALLBACK,
450
- { slug: config.productSlug }
451
- );
452
- }
453
- if (result.errors?.length) {
454
- setError(result.errors[0]?.message ?? "Unknown error");
455
- } else if (result.data?.publicProduct) {
456
- setProduct(result.data.publicProduct);
504
+ const outcome = await fetchPublicProduct(client, config.productSlug);
505
+ if (outcome.error) {
506
+ setError(outcome.error);
507
+ } else if (outcome.product) {
508
+ setProduct(outcome.product);
457
509
  }
458
510
  } catch (err) {
459
511
  setError(err instanceof Error ? err.message : "Failed to fetch product");
@@ -1138,13 +1190,14 @@ function Markdown({
1138
1190
  children,
1139
1191
  className,
1140
1192
  inline,
1141
- components
1193
+ components,
1194
+ rehypePlugins
1142
1195
  }) {
1143
1196
  const content = /* @__PURE__ */ jsxRuntime.jsx(
1144
1197
  ReactMarkdown__default.default,
1145
1198
  {
1146
1199
  remarkPlugins: [remarkGfm__default.default],
1147
- rehypePlugins: [rehypeSanitize__default.default],
1200
+ rehypePlugins: [rehypeSanitize__default.default, ...rehypePlugins ?? []],
1148
1201
  components: { ...defaultComponents, ...components },
1149
1202
  ...inline ? { disallowedElements: ["p"], unwrapDisallowed: true } : {},
1150
1203
  children
@@ -6165,6 +6218,13 @@ var EN = {
6165
6218
  readAloud: "Read aloud",
6166
6219
  stopReading: "Stop reading",
6167
6220
  answerTable: "Table",
6221
+ productVideo: "Product video",
6222
+ productVideoTitle: "{product} product video",
6223
+ playVideo: "Play {title}",
6224
+ closeVideo: "Close video",
6225
+ enterFullScreen: "Full screen",
6226
+ exitFullScreen: "Exit full screen",
6227
+ videoUnavailable: "This video can't be played right now.",
6168
6228
  groundedIn: "What this answer is based on",
6169
6229
  sourceCountOne: "1 source",
6170
6230
  sourceCountMany: "{count} sources",
@@ -6266,6 +6326,13 @@ var ZH = {
6266
6326
  readAloud: "\u6717\u8BFB",
6267
6327
  stopReading: "\u505C\u6B62\u6717\u8BFB",
6268
6328
  answerTable: "\u8868\u683C",
6329
+ productVideo: "\u4EA7\u54C1\u89C6\u9891",
6330
+ productVideoTitle: "{product} \u4EA7\u54C1\u89C6\u9891",
6331
+ playVideo: "\u64AD\u653E {title}",
6332
+ closeVideo: "\u5173\u95ED\u89C6\u9891",
6333
+ enterFullScreen: "\u5168\u5C4F",
6334
+ exitFullScreen: "\u9000\u51FA\u5168\u5C4F",
6335
+ videoUnavailable: "\u6682\u65F6\u65E0\u6CD5\u64AD\u653E\u6B64\u89C6\u9891\u3002",
6269
6336
  groundedIn: "\u8FD9\u4E2A\u56DE\u7B54\u7684\u4F9D\u636E",
6270
6337
  sourceCountOne: "1 \u4E2A\u6765\u6E90",
6271
6338
  sourceCountMany: "{count} \u4E2A\u6765\u6E90",
@@ -6363,6 +6430,13 @@ var HI = {
6363
6430
  readAloud: "\u091C\u093C\u094B\u0930 \u0938\u0947 \u092A\u0922\u093C\u0947\u0902",
6364
6431
  stopReading: "\u092A\u0922\u093C\u0928\u093E \u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
6365
6432
  answerTable: "\u0924\u093E\u0932\u093F\u0915\u093E",
6433
+ productVideo: "\u0909\u0924\u094D\u092A\u093E\u0926 \u0935\u0940\u0921\u093F\u092F\u094B",
6434
+ productVideoTitle: "{product} \u0909\u0924\u094D\u092A\u093E\u0926 \u0935\u0940\u0921\u093F\u092F\u094B",
6435
+ playVideo: "{title} \u091A\u0932\u093E\u090F\u0901",
6436
+ closeVideo: "\u0935\u0940\u0921\u093F\u092F\u094B \u092C\u0902\u0926 \u0915\u0930\u0947\u0902",
6437
+ enterFullScreen: "\u092A\u0942\u0930\u094D\u0923 \u0938\u094D\u0915\u094D\u0930\u0940\u0928",
6438
+ 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",
6439
+ videoUnavailable: "\u092F\u0939 \u0935\u0940\u0921\u093F\u092F\u094B \u0905\u092D\u0940 \u0928\u0939\u0940\u0902 \u091A\u0932 \u0938\u0915\u0924\u093E\u0964",
6366
6440
  groundedIn: "\u092F\u0939 \u0909\u0924\u094D\u0924\u0930 \u0915\u093F\u0938 \u092A\u0930 \u0906\u0927\u093E\u0930\u093F\u0924 \u0939\u0948",
6367
6441
  sourceCountOne: "1 \u0938\u094D\u0930\u094B\u0924",
6368
6442
  sourceCountMany: "{count} \u0938\u094D\u0930\u094B\u0924",
@@ -6464,6 +6538,13 @@ var ES = {
6464
6538
  readAloud: "Leer en voz alta",
6465
6539
  stopReading: "Dejar de leer",
6466
6540
  answerTable: "Tabla",
6541
+ productVideo: "V\xEDdeo del producto",
6542
+ productVideoTitle: "V\xEDdeo de {product}",
6543
+ playVideo: "Reproducir {title}",
6544
+ closeVideo: "Cerrar v\xEDdeo",
6545
+ enterFullScreen: "Pantalla completa",
6546
+ exitFullScreen: "Salir de pantalla completa",
6547
+ videoUnavailable: "Ahora mismo no se puede reproducir este v\xEDdeo.",
6467
6548
  groundedIn: "En qu\xE9 se basa esta respuesta",
6468
6549
  sourceCountOne: "1 fuente",
6469
6550
  sourceCountMany: "{count} fuentes",
@@ -6565,6 +6646,13 @@ var AR = {
6565
6646
  readAloud: "\u0627\u0642\u0631\u0623 \u0628\u0635\u0648\u062A \u0639\u0627\u0644\u064D",
6566
6647
  stopReading: "\u0625\u064A\u0642\u0627\u0641 \u0627\u0644\u0642\u0631\u0627\u0621\u0629",
6567
6648
  answerTable: "\u062C\u062F\u0648\u0644",
6649
+ productVideo: "\u0641\u064A\u062F\u064A\u0648 \u0627\u0644\u0645\u0646\u062A\u062C",
6650
+ productVideoTitle: "\u0641\u064A\u062F\u064A\u0648 {product}",
6651
+ playVideo: "\u062A\u0634\u063A\u064A\u0644 {title}",
6652
+ closeVideo: "\u0625\u063A\u0644\u0627\u0642 \u0627\u0644\u0641\u064A\u062F\u064A\u0648",
6653
+ enterFullScreen: "\u0645\u0644\u0621 \u0627\u0644\u0634\u0627\u0634\u0629",
6654
+ exitFullScreen: "\u0627\u0644\u062E\u0631\u0648\u062C \u0645\u0646 \u0645\u0644\u0621 \u0627\u0644\u0634\u0627\u0634\u0629",
6655
+ 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.",
6568
6656
  groundedIn: "\u0639\u0644\u0649 \u0645\u0627\u0630\u0627 \u062A\u0633\u062A\u0646\u062F \u0647\u0630\u0647 \u0627\u0644\u0625\u062C\u0627\u0628\u0629",
6569
6657
  sourceCountOne: "\u0645\u0635\u062F\u0631 \u0648\u0627\u062D\u062F",
6570
6658
  sourceCountMany: "{count} \u0645\u0635\u0627\u062F\u0631",
@@ -6666,6 +6754,13 @@ var TA = {
6666
6754
  readAloud: "\u0B9A\u0BA4\u0BCD\u0BA4\u0BAE\u0BBE\u0B95\u0BAA\u0BCD \u0BAA\u0B9F\u0BBF",
6667
6755
  stopReading: "\u0BAA\u0B9F\u0BBF\u0BAA\u0BCD\u0BAA\u0BA4\u0BC8 \u0BA8\u0BBF\u0BB1\u0BC1\u0BA4\u0BCD\u0BA4\u0BC1",
6668
6756
  answerTable: "\u0B85\u0B9F\u0BCD\u0B9F\u0BB5\u0BA3\u0BC8",
6757
+ productVideo: "\u0BA4\u0BAF\u0BBE\u0BB0\u0BBF\u0BAA\u0BCD\u0BAA\u0BC1 \u0BB5\u0BC0\u0B9F\u0BBF\u0BAF\u0BCB",
6758
+ productVideoTitle: "{product} \u0BA4\u0BAF\u0BBE\u0BB0\u0BBF\u0BAA\u0BCD\u0BAA\u0BC1 \u0BB5\u0BC0\u0B9F\u0BBF\u0BAF\u0BCB",
6759
+ playVideo: "{title} \u0B87\u0BAF\u0B95\u0BCD\u0B95\u0BC1",
6760
+ closeVideo: "\u0BB5\u0BC0\u0B9F\u0BBF\u0BAF\u0BCB\u0BB5\u0BC8 \u0BAE\u0BC2\u0B9F\u0BC1",
6761
+ enterFullScreen: "\u0BAE\u0BC1\u0BB4\u0BC1\u0BA4\u0BCD \u0BA4\u0BBF\u0BB0\u0BC8",
6762
+ 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",
6763
+ 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.",
6669
6764
  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",
6670
6765
  sourceCountOne: "1 \u0BAE\u0BC2\u0BB2\u0BAE\u0BCD",
6671
6766
  sourceCountMany: "{count} \u0BAE\u0BC2\u0BB2\u0B99\u0BCD\u0B95\u0BB3\u0BCD",
@@ -6955,7 +7050,14 @@ function answerStrings(locale) {
6955
7050
  ...chrome,
6956
7051
  answerTable: explore.answerTable,
6957
7052
  conceptIllustration: explore.conceptIllustration,
6958
- opensInNewTab: explore.opensInNewTab
7053
+ opensInNewTab: explore.opensInNewTab,
7054
+ productVideo: explore.productVideo,
7055
+ productVideoTitle: explore.productVideoTitle,
7056
+ playVideo: explore.playVideo,
7057
+ closeVideo: explore.closeVideo,
7058
+ enterFullScreen: explore.enterFullScreen,
7059
+ exitFullScreen: explore.exitFullScreen,
7060
+ videoUnavailable: explore.videoUnavailable
6959
7061
  };
6960
7062
  }
6961
7063
  var DATE_TAGS = {
@@ -7023,6 +7125,253 @@ function answerSourceHost(url) {
7023
7125
  return "";
7024
7126
  }
7025
7127
  }
7128
+ function fullscreenSupport() {
7129
+ if (typeof document === "undefined") return null;
7130
+ if (document.fullscreenEnabled && typeof document.documentElement.requestFullscreen === "function") {
7131
+ return "element";
7132
+ }
7133
+ if (typeof HTMLVideoElement !== "undefined" && "webkitEnterFullscreen" in HTMLVideoElement.prototype) {
7134
+ return "video";
7135
+ }
7136
+ return null;
7137
+ }
7138
+ function ignoreRejection(result) {
7139
+ result?.catch(() => void 0);
7140
+ }
7141
+ var VIDEO_DIALOG_CSS = `
7142
+ .boff-explore-video-panel:fullscreen {
7143
+ display: flex;
7144
+ flex-direction: column;
7145
+ gap: 0.75rem;
7146
+ padding: 1rem;
7147
+ background-color: hsl(var(--background));
7148
+ color: hsl(var(--foreground));
7149
+ }
7150
+ .boff-explore-video-panel:fullscreen .boff-explore-video-frame {
7151
+ display: flex;
7152
+ flex: 1 1 auto;
7153
+ min-height: 0;
7154
+ align-items: center;
7155
+ justify-content: center;
7156
+ border: 0;
7157
+ border-radius: 0;
7158
+ box-shadow: none;
7159
+ background-color: hsl(var(--background));
7160
+ }
7161
+ .boff-explore-video-panel:fullscreen video {
7162
+ width: 100%;
7163
+ height: 100%;
7164
+ max-height: none;
7165
+ }
7166
+ `;
7167
+ 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";
7168
+ function ExploreVideoDialog({
7169
+ videoUrl,
7170
+ posterUrl,
7171
+ title,
7172
+ strings: T,
7173
+ dir,
7174
+ onClose
7175
+ }) {
7176
+ const dialogRef = React.useRef(null);
7177
+ const panelRef = React.useRef(null);
7178
+ const videoRef = React.useRef(null);
7179
+ const closeRef = React.useRef(null);
7180
+ const nativeModalRef = React.useRef(false);
7181
+ const titleId = React.useId();
7182
+ const [support] = React.useState(fullscreenSupport);
7183
+ const [fullscreen, setFullscreen] = React.useState(false);
7184
+ const [failed, setFailed] = React.useState(false);
7185
+ React.useEffect(() => {
7186
+ const dialog = dialogRef.current;
7187
+ if (!dialog) return;
7188
+ if (typeof dialog.showModal === "function") {
7189
+ if (!dialog.open) dialog.showModal();
7190
+ nativeModalRef.current = true;
7191
+ } else {
7192
+ dialog.setAttribute("open", "");
7193
+ nativeModalRef.current = false;
7194
+ }
7195
+ closeRef.current?.focus();
7196
+ ignoreRejection(videoRef.current?.play());
7197
+ return () => {
7198
+ if (dialog.open && typeof dialog.close === "function") dialog.close();
7199
+ };
7200
+ }, []);
7201
+ React.useEffect(() => {
7202
+ const sync = () => {
7203
+ const current = document.fullscreenElement;
7204
+ setFullscreen(
7205
+ current !== null && dialogRef.current?.contains(current) === true
7206
+ );
7207
+ };
7208
+ const enter = () => setFullscreen(true);
7209
+ const leave = () => setFullscreen(false);
7210
+ const video = videoRef.current;
7211
+ document.addEventListener("fullscreenchange", sync);
7212
+ video?.addEventListener("webkitbeginfullscreen", enter);
7213
+ video?.addEventListener("webkitendfullscreen", leave);
7214
+ return () => {
7215
+ document.removeEventListener("fullscreenchange", sync);
7216
+ video?.removeEventListener("webkitbeginfullscreen", enter);
7217
+ video?.removeEventListener("webkitendfullscreen", leave);
7218
+ };
7219
+ }, [failed]);
7220
+ const leaveFullscreen = React.useCallback(() => {
7221
+ const current = document.fullscreenElement;
7222
+ if (current && dialogRef.current?.contains(current) && typeof document.exitFullscreen === "function") {
7223
+ ignoreRejection(document.exitFullscreen());
7224
+ }
7225
+ const video = videoRef.current;
7226
+ if (video?.webkitDisplayingFullscreen) video.webkitExitFullscreen?.();
7227
+ }, []);
7228
+ const requestClose = React.useCallback(() => {
7229
+ videoRef.current?.pause();
7230
+ leaveFullscreen();
7231
+ onClose();
7232
+ }, [leaveFullscreen, onClose]);
7233
+ const toggleFullscreen = React.useCallback(() => {
7234
+ if (fullscreen) {
7235
+ leaveFullscreen();
7236
+ return;
7237
+ }
7238
+ if (support === "element") {
7239
+ const panel = panelRef.current;
7240
+ if (panel) ignoreRejection(panel.requestFullscreen());
7241
+ return;
7242
+ }
7243
+ const video = videoRef.current;
7244
+ video?.webkitEnterFullscreen?.();
7245
+ }, [fullscreen, leaveFullscreen, support]);
7246
+ const handleClose = React.useCallback(
7247
+ (event) => {
7248
+ if (event.currentTarget.open) return;
7249
+ requestClose();
7250
+ },
7251
+ [requestClose]
7252
+ );
7253
+ const handleKeyDown = React.useCallback(
7254
+ (event) => {
7255
+ if (event.key === "Escape" && !nativeModalRef.current) {
7256
+ event.preventDefault();
7257
+ requestClose();
7258
+ }
7259
+ },
7260
+ [requestClose]
7261
+ );
7262
+ const handleStageClick = React.useCallback(
7263
+ (event) => {
7264
+ if (event.target === event.currentTarget) requestClose();
7265
+ },
7266
+ [requestClose]
7267
+ );
7268
+ if (typeof document === "undefined") return null;
7269
+ const fullscreenLabel = fullscreen ? T.exitFullScreen : T.enterFullScreen;
7270
+ return reactDom.createPortal(
7271
+ /* @__PURE__ */ jsxRuntime.jsxs(
7272
+ "dialog",
7273
+ {
7274
+ ref: dialogRef,
7275
+ "data-boff-explore": "video-dialog",
7276
+ "aria-labelledby": titleId,
7277
+ dir,
7278
+ onClose: handleClose,
7279
+ onKeyDown: handleKeyDown,
7280
+ 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",
7281
+ children: [
7282
+ /* @__PURE__ */ jsxRuntime.jsx("style", { dangerouslySetInnerHTML: { __html: VIDEO_DIALOG_CSS } }),
7283
+ /* @__PURE__ */ jsxRuntime.jsx(
7284
+ "div",
7285
+ {
7286
+ onClick: handleStageClick,
7287
+ className: "flex min-h-full items-center justify-center p-4 sm:p-6",
7288
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
7289
+ "div",
7290
+ {
7291
+ ref: panelRef,
7292
+ className: "boff-explore-video-panel flex w-full min-w-0 max-w-[960px] flex-col gap-3",
7293
+ children: [
7294
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
7295
+ /* @__PURE__ */ jsxRuntime.jsx(
7296
+ "h2",
7297
+ {
7298
+ id: titleId,
7299
+ className: "min-w-0 flex-1 truncate text-sm font-semibold text-foreground sm:text-base",
7300
+ children: title
7301
+ }
7302
+ ),
7303
+ support !== null && !failed ? (
7304
+ // A plain button whose name says what pressing it does NOW, and no
7305
+ // `aria-pressed`: a toggle button's name must not change with its state, or it
7306
+ // announces "Exit full screen, pressed" — two answers to one question.
7307
+ /* @__PURE__ */ jsxRuntime.jsx(
7308
+ "button",
7309
+ {
7310
+ type: "button",
7311
+ "data-boff-explore": "video-fullscreen",
7312
+ "aria-label": fullscreenLabel,
7313
+ title: fullscreenLabel,
7314
+ onClick: toggleFullscreen,
7315
+ className: DIALOG_BUTTON,
7316
+ 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" })
7317
+ }
7318
+ )
7319
+ ) : null,
7320
+ /* @__PURE__ */ jsxRuntime.jsx(
7321
+ "button",
7322
+ {
7323
+ ref: closeRef,
7324
+ type: "button",
7325
+ "data-boff-explore": "video-close",
7326
+ "aria-label": T.closeVideo,
7327
+ title: T.closeVideo,
7328
+ onClick: requestClose,
7329
+ className: DIALOG_BUTTON,
7330
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { "aria-hidden": "true", className: "h-5 w-5" })
7331
+ }
7332
+ )
7333
+ ] }),
7334
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "boff-explore-video-frame overflow-hidden rounded-xl border border-border bg-muted shadow-lg", children: failed ? (
7335
+ // A black box with a spinning control tells the visitor nothing; this says
7336
+ // what happened, in their language.
7337
+ /* @__PURE__ */ jsxRuntime.jsxs(
7338
+ "div",
7339
+ {
7340
+ role: "alert",
7341
+ className: "flex aspect-video w-full flex-col items-center justify-center gap-2 p-6 text-center text-sm text-muted-foreground",
7342
+ children: [
7343
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.VideoOff, { "aria-hidden": "true", className: "h-6 w-6" }),
7344
+ /* @__PURE__ */ jsxRuntime.jsx("p", { children: T.videoUnavailable })
7345
+ ]
7346
+ }
7347
+ )
7348
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
7349
+ "video",
7350
+ {
7351
+ ref: videoRef,
7352
+ "data-boff-explore": "video-player",
7353
+ src: videoUrl,
7354
+ poster: posterUrl,
7355
+ controls: true,
7356
+ controlsList: support === "element" ? "nofullscreen" : void 0,
7357
+ playsInline: true,
7358
+ autoPlay: true,
7359
+ preload: "metadata",
7360
+ onError: () => setFailed(true),
7361
+ className: "block max-h-[80vh] w-full bg-muted object-contain"
7362
+ }
7363
+ ) })
7364
+ ]
7365
+ }
7366
+ )
7367
+ }
7368
+ )
7369
+ ]
7370
+ }
7371
+ ),
7372
+ document.body
7373
+ );
7374
+ }
7026
7375
  var EXPLORE_ANSWER_CSS = `
7027
7376
  /* Nothing inside an answer may widen the page. Model output carries long URLs, code and
7028
7377
  tables \u2014 each of those overflows a phone by default, and one overflowing child makes the
@@ -7055,6 +7404,14 @@ var EXPLORE_ANSWER_CSS = `
7055
7404
  max-width: 100%;
7056
7405
  height: auto;
7057
7406
  }
7407
+ /* A product-video poster fills the card's reserved 16:9 frame and is cropped to it. The
7408
+ blanket \`img { height: auto }\` above \u2014 and ProseStyles' own \u2014 outrank a height utility,
7409
+ and would size it by its own aspect ratio instead, leaving a band of the frame showing
7410
+ whenever a poster is not exactly 16:9. */
7411
+ .boff-explore-body .boff-explore-video-poster {
7412
+ height: 100%;
7413
+ max-width: none;
7414
+ }
7058
7415
 
7059
7416
  /* ---- Comparison and pricing tables ----------------------------------------------------
7060
7417
  The harness emits a comparison, and a set of plans, as a GFM table whose FIRST column is
@@ -7185,28 +7542,81 @@ function inlineImageOrigins(products) {
7185
7542
  }
7186
7543
  return origins;
7187
7544
  }
7188
- function isInlineImageAllowed(src, origins) {
7189
- if (typeof src !== "string" || src === "") return false;
7545
+ var VIDEO_FILE_PATH = /\.(?:mp4|webm|m4v|mov|vtt)$/i;
7546
+ function catalogUrlOf(value, origins) {
7547
+ if (typeof value !== "string" || value === "") return null;
7190
7548
  let url;
7191
7549
  try {
7192
- url = new URL(src);
7550
+ url = new URL(value);
7551
+ } catch {
7552
+ return null;
7553
+ }
7554
+ if (url.protocol !== "https:" || url.username || url.password) return null;
7555
+ return origins.has(url.origin) ? url : null;
7556
+ }
7557
+ function isInlineImageAllowed(src, origins) {
7558
+ const url = catalogUrlOf(src, origins);
7559
+ return url !== null && !VIDEO_FILE_PATH.test(url.pathname);
7560
+ }
7561
+ var INLINE_VIDEO_PATH = /\.(?:mp4|webm)$/i;
7562
+ function isInlineVideoAllowed(href, origins) {
7563
+ const url = catalogUrlOf(href, origins);
7564
+ return url !== null && INLINE_VIDEO_PATH.test(url.pathname);
7565
+ }
7566
+ function apexOriginOf(url) {
7567
+ const apex = url.hostname.replace(/^www\./, "");
7568
+ const port = url.port ? `:${url.port}` : "";
7569
+ return `https://${apex}${port}`;
7570
+ }
7571
+ function productSlugsByOrigin(products) {
7572
+ const slugs = /* @__PURE__ */ new Map();
7573
+ for (const product of products) {
7574
+ let url;
7575
+ try {
7576
+ url = new URL(product.website);
7577
+ } catch {
7578
+ continue;
7579
+ }
7580
+ if (url.protocol !== "https:") continue;
7581
+ const key = apexOriginOf(url);
7582
+ if (!slugs.has(key)) slugs.set(key, product.slug);
7583
+ }
7584
+ return slugs;
7585
+ }
7586
+ function videoProductOf(href, slugs) {
7587
+ const url = new URL(href);
7588
+ return slugs.get(apexOriginOf(url)) ?? url.hostname.replace(/^www\./, "");
7589
+ }
7590
+ function isVideoFileTarget(href) {
7591
+ if (typeof href !== "string") return false;
7592
+ try {
7593
+ return VIDEO_FILE_PATH.test(
7594
+ new URL(href, "https://answer.invalid").pathname
7595
+ );
7193
7596
  } catch {
7194
7597
  return false;
7195
7598
  }
7196
- if (url.protocol !== "https:" || url.username || url.password) return false;
7197
- return origins.has(url.origin);
7198
7599
  }
7199
7600
  var CODE_FENCE = /^\s*(?:```|~~~)/;
7200
7601
  var TABLE_ROW_LINE = /^\s{0,3}\|/;
7201
7602
  var TABLE_DELIMITER_ROW = /^\s{0,3}\|[\s:|-]*-[\s:|-]*$/;
7603
+ var LINKED_IMAGE = /\[!\[([^[\]]*)\]\([^)\s]*(?:\s+"[^"]*")?\)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
7604
+ var IMAGE = /!\[[^[\]]*\]\([^)\s]*(?:\s+"[^"]*")?\)/g;
7605
+ var LINK = /\[([^[\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
7202
7606
  function readableLine(raw, mode) {
7203
7607
  let line = raw;
7204
- line = line.replace(/!\[[^\]]*\]\([^)\s]*(?:\s+"[^"]*")?\)/g, "");
7205
- line = mode === "speech" ? (
7206
- // A voice reading "h-t-t-p-s colon slash slash…" mid-sentence is noise, and the
7207
- // prompt asks for two to five links in every answer. The label IS the words.
7208
- line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1")
7209
- ) : line.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, "$1 ($2)");
7608
+ if (line.includes("](")) {
7609
+ line = line.replace(LINKED_IMAGE, (_match, alt, href) => {
7610
+ if (mode === "speech") return "";
7611
+ return alt.trim() === "" ? href : `${alt.trim()} (${href})`;
7612
+ });
7613
+ line = line.replace(IMAGE, "");
7614
+ line = mode === "speech" ? (
7615
+ // A voice reading "h-t-t-p-s colon slash slash…" mid-sentence is noise, and the
7616
+ // prompt asks for two to five links in every answer. The label IS the words.
7617
+ line.replace(LINK, "$1")
7618
+ ) : line.replace(LINK, "$1 ($2)");
7619
+ }
7210
7620
  line = mode === "speech" ? line.replace(/<(https?:\/\/[^>\s]+)>/g, "") : line.replace(/<(https?:\/\/[^>\s]+)>/g, "$1");
7211
7621
  line = line.replace(/^\s{0,3}#{1,6}\s+/, "");
7212
7622
  line = line.replace(/^\s{0,3}>\s?/, "");
@@ -7297,6 +7707,21 @@ function answerSpeechText(markdown) {
7297
7707
  var AnswerScopeContext = React.createContext(null);
7298
7708
  var InsideLinkContext = React.createContext(false);
7299
7709
  function AnswerLink(props) {
7710
+ const scope = React.useContext(AnswerScopeContext);
7711
+ const role = videoRoleOf(props.node);
7712
+ if (role !== void 0 && scope !== null) {
7713
+ const video = role === "card" && scope.enabled ? productVideoOf(props.node) : null;
7714
+ if (video === null) return null;
7715
+ return /* @__PURE__ */ jsxRuntime.jsx(
7716
+ InlineVideoCard,
7717
+ {
7718
+ href: video.href,
7719
+ poster: video.poster,
7720
+ alt: video.alt,
7721
+ scope
7722
+ }
7723
+ );
7724
+ }
7300
7725
  return /* @__PURE__ */ jsxRuntime.jsx(InsideLinkContext.Provider, { value: true, children: /* @__PURE__ */ jsxRuntime.jsx(MarkdownLink, { ...props }) });
7301
7726
  }
7302
7727
  var INLINE_IMAGE_FRAME = "relative inline-block max-w-full overflow-hidden rounded-xl border border-border bg-muted align-top shadow-sm";
@@ -7408,16 +7833,173 @@ function AnswerTableRow({
7408
7833
  });
7409
7834
  return /* @__PURE__ */ jsxRuntime.jsx("tr", { ...props, children: cells });
7410
7835
  }
7836
+ function linkedImageOf(node) {
7837
+ if (node?.tagName !== "a") return null;
7838
+ const content = (node.children ?? []).filter(
7839
+ (child) => child.tagName !== void 0 || (child.value ?? "").trim() !== ""
7840
+ );
7841
+ const image = content.length === 1 ? content[0] : void 0;
7842
+ if (image?.tagName !== "img") return null;
7843
+ const alt = image.properties?.alt;
7844
+ return {
7845
+ href: node.properties?.href,
7846
+ src: image.properties?.src,
7847
+ alt: typeof alt === "string" ? alt.trim() : ""
7848
+ };
7849
+ }
7850
+ function isCardableVideo(embed, origins) {
7851
+ if (!isInlineVideoAllowed(embed.href, origins) || !isInlineImageAllowed(embed.src, origins)) {
7852
+ return false;
7853
+ }
7854
+ return apexOriginOf(new URL(embed.href)) === apexOriginOf(new URL(embed.src));
7855
+ }
7856
+ var NO_VIDEO_CONTAINERS = /* @__PURE__ */ new Set(["li", "table"]);
7857
+ function rehypeProductVideo(options) {
7858
+ return (tree) => {
7859
+ let claimed = false;
7860
+ const visit = (node, parent, contained) => {
7861
+ const embed = linkedImageOf(node);
7862
+ if (embed !== null && isVideoFileTarget(embed.href)) {
7863
+ const card = !claimed && !contained && parent?.tagName === "p" && isCardableVideo(embed, options.origins);
7864
+ if (card) claimed = true;
7865
+ const data = typeof node.data === "object" ? node.data : null;
7866
+ node.data = { ...data, boffVideo: card ? "card" : "drop" };
7867
+ return;
7868
+ }
7869
+ const inside = contained || node.tagName !== void 0 && NO_VIDEO_CONTAINERS.has(node.tagName);
7870
+ for (const child of node.children ?? []) visit(child, node, inside);
7871
+ };
7872
+ visit(tree, null, false);
7873
+ };
7874
+ }
7875
+ function videoRoleOf(node) {
7876
+ const data = node?.data;
7877
+ if (typeof data !== "object" || data === null || !("boffVideo" in data)) {
7878
+ return void 0;
7879
+ }
7880
+ return data.boffVideo === "card" || data.boffVideo === "drop" ? data.boffVideo : void 0;
7881
+ }
7882
+ function productVideoOf(node) {
7883
+ const embed = linkedImageOf(node);
7884
+ if (embed === null || typeof embed.href !== "string" || typeof embed.src !== "string") {
7885
+ return null;
7886
+ }
7887
+ return { href: embed.href, poster: embed.src, alt: embed.alt };
7888
+ }
7889
+ var CANONICAL_VIDEO_ALT = /^(.+?)\s+product video$/i;
7890
+ function videoTitleOf(alt, T) {
7891
+ const product = CANONICAL_VIDEO_ALT.exec(alt)?.[1]?.trim();
7892
+ if (product) return fill(T.productVideoTitle, { product });
7893
+ return alt === "" ? T.productVideo : alt;
7894
+ }
7895
+ 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";
7896
+ function InlineVideoCard({
7897
+ href,
7898
+ poster,
7899
+ alt,
7900
+ scope
7901
+ }) {
7902
+ const { strings: T, failed, onFailed, productSlugs } = scope;
7903
+ const [dialogDir, setDialogDir] = React.useState(null);
7904
+ const buttonRef = React.useRef(null);
7905
+ const returnFocusRef = React.useRef(false);
7906
+ const open = dialogDir !== null;
7907
+ React.useEffect(() => {
7908
+ if (open || !returnFocusRef.current) return;
7909
+ returnFocusRef.current = false;
7910
+ const button = buttonRef.current;
7911
+ if (button?.isConnected) button.focus();
7912
+ }, [open]);
7913
+ const openPlayer = React.useCallback((event) => {
7914
+ const dir = event.currentTarget.closest("[dir]")?.getAttribute("dir");
7915
+ returnFocusRef.current = true;
7916
+ setDialogDir(dir === "rtl" ? "rtl" : "ltr");
7917
+ }, []);
7918
+ const closePlayer = React.useCallback(() => setDialogDir(null), []);
7919
+ const name = videoTitleOf(alt, T);
7920
+ const posterFailed = failed.has(poster);
7921
+ return /* @__PURE__ */ jsxRuntime.jsxs(
7922
+ "span",
7923
+ {
7924
+ "data-boff-explore": "inline-video",
7925
+ "data-product": videoProductOf(href, productSlugs),
7926
+ className: "my-3 block",
7927
+ children: [
7928
+ /* @__PURE__ */ jsxRuntime.jsxs(
7929
+ "button",
7930
+ {
7931
+ ref: buttonRef,
7932
+ type: "button",
7933
+ "aria-haspopup": "dialog",
7934
+ "aria-label": fill(T.playVideo, { title: name }),
7935
+ onClick: openPlayer,
7936
+ className: INLINE_VIDEO_FRAME,
7937
+ children: [
7938
+ posterFailed ? /* @__PURE__ */ jsxRuntime.jsx(
7939
+ "span",
7940
+ {
7941
+ "aria-hidden": "true",
7942
+ className: "absolute inset-0 flex items-center justify-center bg-muted text-muted-foreground",
7943
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Film, { className: "h-10 w-10" })
7944
+ }
7945
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
7946
+ "img",
7947
+ {
7948
+ src: poster,
7949
+ alt: "",
7950
+ loading: "lazy",
7951
+ decoding: "async",
7952
+ onError: () => onFailed(poster),
7953
+ className: "boff-explore-video-poster absolute inset-0 h-full w-full object-cover"
7954
+ }
7955
+ ),
7956
+ /* @__PURE__ */ jsxRuntime.jsx(
7957
+ "span",
7958
+ {
7959
+ "aria-hidden": "true",
7960
+ className: "pointer-events-none absolute inset-0 flex items-center justify-center",
7961
+ 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" }) })
7962
+ }
7963
+ ),
7964
+ /* @__PURE__ */ jsxRuntime.jsx(
7965
+ "span",
7966
+ {
7967
+ "aria-hidden": "true",
7968
+ 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",
7969
+ children: T.productVideo
7970
+ }
7971
+ )
7972
+ ]
7973
+ }
7974
+ ),
7975
+ open ? /* @__PURE__ */ jsxRuntime.jsx(
7976
+ ExploreVideoDialog,
7977
+ {
7978
+ videoUrl: href,
7979
+ posterUrl: poster,
7980
+ title: name,
7981
+ strings: T,
7982
+ dir: dialogDir,
7983
+ onClose: closePlayer
7984
+ }
7985
+ ) : null
7986
+ ]
7987
+ }
7988
+ );
7989
+ }
7411
7990
  var ANSWER_MARKDOWN_COMPONENTS = {
7412
7991
  a: AnswerLink,
7413
7992
  img: InlineConceptImage,
7414
7993
  table: AnswerTable,
7415
7994
  tr: AnswerTableRow
7416
7995
  };
7996
+ var NO_PRODUCT_SLUGS = /* @__PURE__ */ new Map();
7417
7997
  function ExploreAnswerBody({
7418
7998
  children,
7419
7999
  strings,
7420
8000
  imageOrigins,
8001
+ videoOrigins = imageOrigins,
8002
+ productSlugs = NO_PRODUCT_SLUGS,
7421
8003
  enabled = true,
7422
8004
  components,
7423
8005
  className
@@ -7433,16 +8015,21 @@ function ExploreAnswerBody({
7433
8015
  strings,
7434
8016
  enabled,
7435
8017
  origins: imageOrigins,
8018
+ productSlugs,
7436
8019
  failed,
7437
8020
  onFailed
7438
8021
  }),
7439
- [strings, enabled, imageOrigins, failed, onFailed]
8022
+ [strings, enabled, imageOrigins, productSlugs, failed, onFailed]
8023
+ );
8024
+ const rehypePlugins = React.useMemo(
8025
+ () => [[rehypeProductVideo, { origins: videoOrigins }]],
8026
+ [videoOrigins]
7440
8027
  );
7441
8028
  const overrides = React.useMemo(
7442
8029
  () => components ? { ...ANSWER_MARKDOWN_COMPONENTS, ...components } : ANSWER_MARKDOWN_COMPONENTS,
7443
8030
  [components]
7444
8031
  );
7445
- 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 }) }) });
8032
+ 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 }) }) });
7446
8033
  }
7447
8034
  var PROSE_CSS = `
7448
8035
  .boff-prose {
@@ -7620,32 +8207,216 @@ var PROSE_CSS = `
7620
8207
  function ProseStyles() {
7621
8208
  return /* @__PURE__ */ jsxRuntime.jsx("style", { dangerouslySetInnerHTML: { __html: PROSE_CSS } });
7622
8209
  }
7623
- var META_DESCRIPTION_LIMIT2 = 160;
7624
- var TITLE_LIMIT = 100;
7625
- var RELATED_LIMIT2 = 4;
7626
- function normaliseSiteUrl2(siteUrl) {
7627
- return siteUrl.trim().replace(/\/+$/, "");
7628
- }
7629
- function absoluteUrl2(base, path) {
7630
- if (/^https?:\/\//i.test(path)) return path;
7631
- return `${base}${path.startsWith("/") ? path : `/${path}`}`;
7632
- }
7633
- function sortByOrder2(answers) {
7634
- return [...answers].sort(
7635
- (a, b) => a.order - b.order || a.slug.localeCompare(b.slug)
7636
- );
7637
- }
7638
- function clampText2(text, limit) {
7639
- if (text.length <= limit) return text;
7640
- const cut = text.slice(0, limit - 1);
7641
- const lastSpace = cut.lastIndexOf(" ");
7642
- return `${(lastSpace > 0 ? cut.slice(0, lastSpace) : cut).replace(/[\s,;:—–-]+$/, "")}\u2026`;
7643
- }
7644
- function questionCount(T, count) {
7645
- return count === 1 ? T.questionCountOne : fill(T.questionCountMany, { count: String(count) });
7646
- }
7647
- function sourceCount(T, count) {
7648
- return count === 1 ? T.sourceCountOne : fill(T.sourceCountMany, { count: String(count) });
8210
+
8211
+ // src/data/products.ts
8212
+ var ECOSYSTEM_PRODUCTS = [
8213
+ {
8214
+ slug: "workspaces",
8215
+ name: "Workspaces",
8216
+ websiteUrl: "https://burdenoff.com"
8217
+ },
8218
+ {
8219
+ slug: "vibecontrols",
8220
+ name: "VibeControls",
8221
+ websiteUrl: "https://vibecontrols.com"
8222
+ },
8223
+ // FluidGrids ships on www.fluidgrids.ai — fluidgrids.com only redirects there. The explore
8224
+ // catalog and the site's own sitemap use this origin, and so must the fallback catalog
8225
+ // built from this list, or /explore rejects FluidGrids concept images until it loads.
8226
+ {
8227
+ slug: "fluidgrids",
8228
+ name: "FluidGrids",
8229
+ websiteUrl: "https://www.fluidgrids.ai"
8230
+ },
8231
+ // BotManor ships on www.botmanor.com (the apex redirects there; the old botlit.ai domain
8232
+ // still serves the same site but is no longer the name we link to).
8233
+ // The slug follows the botlit → botmanor rename switch (utils/product-slug.ts).
8234
+ {
8235
+ slug: emitProductSlug("botmanor"),
8236
+ name: "BotManor",
8237
+ websiteUrl: "https://www.botmanor.com"
8238
+ },
8239
+ {
8240
+ slug: "bigconsole",
8241
+ name: "BigConsole",
8242
+ websiteUrl: "https://bigconsole.com"
8243
+ },
8244
+ {
8245
+ slug: "semanticfed",
8246
+ name: "SemanticFed",
8247
+ websiteUrl: "https://semanticfed.com"
8248
+ },
8249
+ {
8250
+ slug: "intelwatchtower",
8251
+ name: "IntelWatchtower",
8252
+ websiteUrl: "https://intelwatchtower.com"
8253
+ },
8254
+ { slug: "buildmyiq", name: "BuildMyIQ", websiteUrl: "https://buildmyiq.com" },
8255
+ {
8256
+ slug: "assethandler",
8257
+ name: "AssetHandler",
8258
+ websiteUrl: "https://assethandler.com"
8259
+ },
8260
+ {
8261
+ slug: "crewfoundry",
8262
+ name: "CrewFoundry",
8263
+ websiteUrl: "https://crewfoundry.com"
8264
+ },
8265
+ {
8266
+ slug: "planmagnet",
8267
+ name: "PlanMagnet",
8268
+ websiteUrl: "https://planmagnet.com"
8269
+ },
8270
+ {
8271
+ slug: "eventfullymanaged",
8272
+ name: "EventfullyManaged",
8273
+ websiteUrl: "https://eventfullymanaged.com"
8274
+ },
8275
+ {
8276
+ slug: "headshotmarketing",
8277
+ name: "HeadshotMarketing",
8278
+ websiteUrl: "https://headshotmarketing.com"
8279
+ },
8280
+ {
8281
+ slug: "technospam",
8282
+ name: "TechnoSpam",
8283
+ websiteUrl: "https://technospam.com"
8284
+ },
8285
+ { slug: "kadaikodi", name: "Kadaikodi", websiteUrl: "https://kadaikodi.com" },
8286
+ {
8287
+ slug: "movethewheels",
8288
+ name: "MoveTheWheels",
8289
+ websiteUrl: "https://movethewheels.com"
8290
+ },
8291
+ {
8292
+ slug: "healthybowl",
8293
+ name: "Healthy Bowl",
8294
+ websiteUrl: "https://gethealthybowl.com"
8295
+ },
8296
+ { slug: "ggnomad", name: "GGNomad", websiteUrl: "https://ggnomad.com" },
8297
+ {
8298
+ slug: "timecampus",
8299
+ name: "TimeCampus",
8300
+ websiteUrl: "https://timecampus.com"
8301
+ },
8302
+ {
8303
+ slug: "manufacturedops",
8304
+ name: "ManufacturedOps",
8305
+ websiteUrl: "https://manufacturedops.com"
8306
+ },
8307
+ {
8308
+ slug: "theglobalfuel",
8309
+ name: "TheGlobalFuel",
8310
+ websiteUrl: "https://theglobalfuel.com"
8311
+ },
8312
+ {
8313
+ slug: "housingvista",
8314
+ name: "HousingVista",
8315
+ websiteUrl: "https://housingvista.com"
8316
+ },
8317
+ {
8318
+ slug: "proserviceworld",
8319
+ name: "ProServiceWorld",
8320
+ websiteUrl: "https://proserviceworld.com"
8321
+ },
8322
+ {
8323
+ slug: "mybodyshield",
8324
+ name: "MyBodyShield",
8325
+ websiteUrl: "https://mybodyshield.com"
8326
+ },
8327
+ {
8328
+ slug: "artistrybase",
8329
+ name: "ArtistryBase",
8330
+ websiteUrl: "https://artistrybase.com"
8331
+ },
8332
+ {
8333
+ slug: "theculturepost",
8334
+ name: "TheCulturePost",
8335
+ websiteUrl: "https://theculturepost.com"
8336
+ },
8337
+ { slug: "collabkin", name: "CollabKin", websiteUrl: "https://collabkin.com" },
8338
+ {
8339
+ slug: "comradecircle",
8340
+ name: "ComradeCircle",
8341
+ websiteUrl: "https://comradecircle.com"
8342
+ },
8343
+ {
8344
+ slug: "brainyrich",
8345
+ name: "BrainyRich",
8346
+ websiteUrl: "https://brainyrich.com"
8347
+ },
8348
+ {
8349
+ slug: "cosmicintersection",
8350
+ name: "CosmicIntersection",
8351
+ websiteUrl: "https://cosmicintersection.com"
8352
+ },
8353
+ {
8354
+ slug: "govcitizenhub",
8355
+ name: "GovCitizenHub",
8356
+ websiteUrl: "https://govcitizenhub.com"
8357
+ },
8358
+ {
8359
+ slug: "adaptercloud",
8360
+ name: "AdapterCloud",
8361
+ websiteUrl: "https://adaptercloud.com"
8362
+ },
8363
+ {
8364
+ slug: "subscriberbot",
8365
+ name: "Subscriber Bot",
8366
+ websiteUrl: "https://subscriberbot.com"
8367
+ },
8368
+ {
8369
+ slug: "hookmovies",
8370
+ name: "HookMovies",
8371
+ websiteUrl: "https://hookmovies.com"
8372
+ },
8373
+ {
8374
+ slug: "coolandlovely",
8375
+ name: "CoolAndLovely",
8376
+ websiteUrl: "https://coolandlovely.com"
8377
+ },
8378
+ {
8379
+ slug: "atheletebridge",
8380
+ name: "AthleteBridge",
8381
+ websiteUrl: "https://atheletebridge.com"
8382
+ },
8383
+ {
8384
+ slug: "labsofscience",
8385
+ name: "LabsOfScience",
8386
+ websiteUrl: "https://labsofscience.com"
8387
+ },
8388
+ {
8389
+ slug: "ecoimpacthub",
8390
+ name: "EcoImpactHub",
8391
+ websiteUrl: "https://ecoimpacthub.com"
8392
+ }
8393
+ ];
8394
+ var META_DESCRIPTION_LIMIT2 = 160;
8395
+ var TITLE_LIMIT = 100;
8396
+ var RELATED_LIMIT2 = 4;
8397
+ function normaliseSiteUrl2(siteUrl) {
8398
+ return siteUrl.trim().replace(/\/+$/, "");
8399
+ }
8400
+ function absoluteUrl2(base, path) {
8401
+ if (/^https?:\/\//i.test(path)) return path;
8402
+ return `${base}${path.startsWith("/") ? path : `/${path}`}`;
8403
+ }
8404
+ function sortByOrder2(answers) {
8405
+ return [...answers].sort(
8406
+ (a, b) => a.order - b.order || a.slug.localeCompare(b.slug)
8407
+ );
8408
+ }
8409
+ function clampText2(text, limit) {
8410
+ if (text.length <= limit) return text;
8411
+ const cut = text.slice(0, limit - 1);
8412
+ const lastSpace = cut.lastIndexOf(" ");
8413
+ return `${(lastSpace > 0 ? cut.slice(0, lastSpace) : cut).replace(/[\s,;:—–-]+$/, "")}\u2026`;
8414
+ }
8415
+ function questionCount(T, count) {
8416
+ return count === 1 ? T.questionCountOne : fill(T.questionCountMany, { count: String(count) });
8417
+ }
8418
+ function sourceCount(T, count) {
8419
+ return count === 1 ? T.sourceCountOne : fill(T.sourceCountMany, { count: String(count) });
7649
8420
  }
7650
8421
  function slugifyAnchor2(value) {
7651
8422
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -7725,6 +8496,12 @@ var ANSWER_HEADING_SHIFT = {
7725
8496
  h6: demoteHeading("h6")
7726
8497
  };
7727
8498
  var ENGLISH_TEXT = { lang: "en" };
8499
+ var ECOSYSTEM_SITES = ECOSYSTEM_PRODUCTS.map((product) => ({
8500
+ slug: product.slug,
8501
+ website: product.websiteUrl
8502
+ }));
8503
+ var ECOSYSTEM_PRODUCT_SLUGS = productSlugsByOrigin(ECOSYSTEM_SITES);
8504
+ var ECOSYSTEM_ORIGINS = inlineImageOrigins(ECOSYSTEM_SITES);
7728
8505
  function DefaultAnswerLink({
7729
8506
  href,
7730
8507
  className,
@@ -8223,6 +9000,10 @@ function AnswerDetail({
8223
9000
  ]),
8224
9001
  [base, sources, imageOrigins]
8225
9002
  );
9003
+ const answerVideoOrigins = React.useMemo(
9004
+ () => /* @__PURE__ */ new Set([...answerImageOrigins, ...ECOSYSTEM_ORIGINS]),
9005
+ [answerImageOrigins]
9006
+ );
8226
9007
  const canonical = absoluteUrl2(base, answerPath(answer.slug));
8227
9008
  const answerText = React.useMemo(
8228
9009
  () => answerSpeechText(answer.answer),
@@ -8384,6 +9165,8 @@ function AnswerDetail({
8384
9165
  {
8385
9166
  strings: T,
8386
9167
  imageOrigins: answerImageOrigins,
9168
+ videoOrigins: answerVideoOrigins,
9169
+ productSlugs: ECOSYSTEM_PRODUCT_SLUGS,
8387
9170
  components: ANSWER_HEADING_SHIFT,
8388
9171
  className: "mt-4",
8389
9172
  children: answer.answer
@@ -8745,185 +9528,6 @@ var EXPLORE_SHARED_CONVERSATION_QUERY = (
8745
9528
  `
8746
9529
  );
8747
9530
 
8748
- // src/data/products.ts
8749
- var ECOSYSTEM_PRODUCTS = [
8750
- {
8751
- slug: "workspaces",
8752
- name: "Workspaces",
8753
- websiteUrl: "https://burdenoff.com"
8754
- },
8755
- {
8756
- slug: "vibecontrols",
8757
- name: "VibeControls",
8758
- websiteUrl: "https://vibecontrols.com"
8759
- },
8760
- // FluidGrids ships on www.fluidgrids.ai — fluidgrids.com only redirects there. The explore
8761
- // catalog and the site's own sitemap use this origin, and so must the fallback catalog
8762
- // built from this list, or /explore rejects FluidGrids concept images until it loads.
8763
- {
8764
- slug: "fluidgrids",
8765
- name: "FluidGrids",
8766
- websiteUrl: "https://www.fluidgrids.ai"
8767
- },
8768
- // BotManor ships on www.botmanor.com (the apex redirects there; the old botlit.ai domain
8769
- // still serves the same site but is no longer the name we link to).
8770
- { slug: "botlit", name: "BotManor", websiteUrl: "https://www.botmanor.com" },
8771
- {
8772
- slug: "bigconsole",
8773
- name: "BigConsole",
8774
- websiteUrl: "https://bigconsole.com"
8775
- },
8776
- {
8777
- slug: "semanticfed",
8778
- name: "SemanticFed",
8779
- websiteUrl: "https://semanticfed.com"
8780
- },
8781
- {
8782
- slug: "intelwatchtower",
8783
- name: "IntelWatchtower",
8784
- websiteUrl: "https://intelwatchtower.com"
8785
- },
8786
- { slug: "buildmyiq", name: "BuildMyIQ", websiteUrl: "https://buildmyiq.com" },
8787
- {
8788
- slug: "assethandler",
8789
- name: "AssetHandler",
8790
- websiteUrl: "https://assethandler.com"
8791
- },
8792
- {
8793
- slug: "crewfoundry",
8794
- name: "CrewFoundry",
8795
- websiteUrl: "https://crewfoundry.com"
8796
- },
8797
- {
8798
- slug: "planmagnet",
8799
- name: "PlanMagnet",
8800
- websiteUrl: "https://planmagnet.com"
8801
- },
8802
- {
8803
- slug: "eventfullymanaged",
8804
- name: "EventfullyManaged",
8805
- websiteUrl: "https://eventfullymanaged.com"
8806
- },
8807
- {
8808
- slug: "headshotmarketing",
8809
- name: "HeadshotMarketing",
8810
- websiteUrl: "https://headshotmarketing.com"
8811
- },
8812
- {
8813
- slug: "technospam",
8814
- name: "TechnoSpam",
8815
- websiteUrl: "https://technospam.com"
8816
- },
8817
- { slug: "kadaikodi", name: "Kadaikodi", websiteUrl: "https://kadaikodi.com" },
8818
- {
8819
- slug: "movethewheels",
8820
- name: "MoveTheWheels",
8821
- websiteUrl: "https://movethewheels.com"
8822
- },
8823
- {
8824
- slug: "healthybowl",
8825
- name: "Healthy Bowl",
8826
- websiteUrl: "https://gethealthybowl.com"
8827
- },
8828
- { slug: "ggnomad", name: "GGNomad", websiteUrl: "https://ggnomad.com" },
8829
- {
8830
- slug: "timecampus",
8831
- name: "TimeCampus",
8832
- websiteUrl: "https://timecampus.com"
8833
- },
8834
- {
8835
- slug: "manufacturedops",
8836
- name: "ManufacturedOps",
8837
- websiteUrl: "https://manufacturedops.com"
8838
- },
8839
- {
8840
- slug: "theglobalfuel",
8841
- name: "TheGlobalFuel",
8842
- websiteUrl: "https://theglobalfuel.com"
8843
- },
8844
- {
8845
- slug: "housingvista",
8846
- name: "HousingVista",
8847
- websiteUrl: "https://housingvista.com"
8848
- },
8849
- {
8850
- slug: "proserviceworld",
8851
- name: "ProServiceWorld",
8852
- websiteUrl: "https://proserviceworld.com"
8853
- },
8854
- {
8855
- slug: "mybodyshield",
8856
- name: "MyBodyShield",
8857
- websiteUrl: "https://mybodyshield.com"
8858
- },
8859
- {
8860
- slug: "artistrybase",
8861
- name: "ArtistryBase",
8862
- websiteUrl: "https://artistrybase.com"
8863
- },
8864
- {
8865
- slug: "theculturepost",
8866
- name: "TheCulturePost",
8867
- websiteUrl: "https://theculturepost.com"
8868
- },
8869
- { slug: "collabkin", name: "CollabKin", websiteUrl: "https://collabkin.com" },
8870
- {
8871
- slug: "comradecircle",
8872
- name: "ComradeCircle",
8873
- websiteUrl: "https://comradecircle.com"
8874
- },
8875
- {
8876
- slug: "brainyrich",
8877
- name: "BrainyRich",
8878
- websiteUrl: "https://brainyrich.com"
8879
- },
8880
- {
8881
- slug: "cosmicintersection",
8882
- name: "CosmicIntersection",
8883
- websiteUrl: "https://cosmicintersection.com"
8884
- },
8885
- {
8886
- slug: "govcitizenhub",
8887
- name: "GovCitizenHub",
8888
- websiteUrl: "https://govcitizenhub.com"
8889
- },
8890
- {
8891
- slug: "adaptercloud",
8892
- name: "AdapterCloud",
8893
- websiteUrl: "https://adaptercloud.com"
8894
- },
8895
- {
8896
- slug: "subscriberbot",
8897
- name: "Subscriber Bot",
8898
- websiteUrl: "https://subscriberbot.com"
8899
- },
8900
- {
8901
- slug: "hookmovies",
8902
- name: "HookMovies",
8903
- websiteUrl: "https://hookmovies.com"
8904
- },
8905
- {
8906
- slug: "coolandlovely",
8907
- name: "CoolAndLovely",
8908
- websiteUrl: "https://coolandlovely.com"
8909
- },
8910
- {
8911
- slug: "atheletebridge",
8912
- name: "AthleteBridge",
8913
- websiteUrl: "https://atheletebridge.com"
8914
- },
8915
- {
8916
- slug: "labsofscience",
8917
- name: "LabsOfScience",
8918
- websiteUrl: "https://labsofscience.com"
8919
- },
8920
- {
8921
- slug: "ecoimpacthub",
8922
- name: "EcoImpactHub",
8923
- websiteUrl: "https://ecoimpacthub.com"
8924
- }
8925
- ];
8926
-
8927
9531
  // src/utils/recaptcha-v3.ts
8928
9532
  var SCRIPT_ID = "boff-recaptcha-v3";
8929
9533
  var SCRIPT_SRC = "https://www.google.com/recaptcha/api.js?render=";
@@ -10041,7 +10645,7 @@ function isHighSurrogate(code) {
10041
10645
  }
10042
10646
  function safeInlineEnd(line) {
10043
10647
  let codeSpan = null;
10044
- let link = null;
10648
+ const links = [];
10045
10649
  const emphasis = [];
10046
10650
  let at = 0;
10047
10651
  while (at < line.length) {
@@ -10050,6 +10654,13 @@ function safeInlineEnd(line) {
10050
10654
  at += 2;
10051
10655
  continue;
10052
10656
  }
10657
+ const innermost = links[links.length - 1];
10658
+ if (innermost !== void 0 && innermost.kind !== "label") {
10659
+ if (char === "(") links.push({ kind: "paren", start: innermost.start });
10660
+ else if (char === ")") links.pop();
10661
+ at += 1;
10662
+ continue;
10663
+ }
10053
10664
  if (char === "`") {
10054
10665
  let run = 1;
10055
10666
  while (line[at + run] === "`") run += 1;
@@ -10062,17 +10673,17 @@ function safeInlineEnd(line) {
10062
10673
  continue;
10063
10674
  }
10064
10675
  if (char === "[") {
10065
- if (link === null) link = line[at - 1] === "!" ? at - 1 : at;
10676
+ links.push({ kind: "label", start: line[at - 1] === "!" ? at - 1 : at });
10066
10677
  at += 1;
10067
10678
  continue;
10068
10679
  }
10069
- if (char === "]") {
10070
- if (link !== null && line[at + 1] !== "(") link = null;
10071
- at += 1;
10072
- continue;
10073
- }
10074
- if (char === ")") {
10075
- link = null;
10680
+ if (char === "]" && innermost !== void 0) {
10681
+ links.pop();
10682
+ if (line[at + 1] === "(") {
10683
+ links.push({ kind: "destination", start: innermost.start });
10684
+ at += 2;
10685
+ continue;
10686
+ }
10076
10687
  at += 1;
10077
10688
  continue;
10078
10689
  }
@@ -10099,9 +10710,11 @@ function safeInlineEnd(line) {
10099
10710
  }
10100
10711
  at += 1;
10101
10712
  }
10102
- const open = [codeSpan, link, emphasis[0]?.start ?? null].filter(
10103
- (value) => value !== null
10104
- );
10713
+ const open = [
10714
+ codeSpan,
10715
+ links[0]?.start ?? null,
10716
+ emphasis[0]?.start ?? null
10717
+ ].filter((value) => value !== null);
10105
10718
  return open.length === 0 ? line.length : Math.min(...open);
10106
10719
  }
10107
10720
  function revealBoundary(text, limit) {
@@ -11963,6 +12576,7 @@ function AssistantBubble({
11963
12576
  message,
11964
12577
  activity,
11965
12578
  imageOrigins,
12579
+ productSlugs,
11966
12580
  productNames,
11967
12581
  speechLang,
11968
12582
  onNavigate,
@@ -12077,6 +12691,7 @@ function AssistantBubble({
12077
12691
  {
12078
12692
  strings: T,
12079
12693
  imageOrigins,
12694
+ productSlugs,
12080
12695
  enabled: completed,
12081
12696
  className: "boff-prose-sm text-card-foreground",
12082
12697
  children: visible
@@ -12347,21 +12962,24 @@ function ProductStrip({
12347
12962
  children: T.allProducts
12348
12963
  }
12349
12964
  ),
12350
- products.map((product) => /* @__PURE__ */ jsxRuntime.jsx(
12351
- "button",
12352
- {
12353
- "data-boff-explore": "chip",
12354
- "data-chip-kind": "product",
12355
- "data-product": product.slug,
12356
- type: "button",
12357
- "aria-pressed": focusProduct === product.slug,
12358
- title: product.tagline ?? product.name,
12359
- onClick: () => onFocus(focusProduct === product.slug ? null : product.slug),
12360
- className: chipClass(focusProduct === product.slug),
12361
- children: product.name
12362
- },
12363
- product.slug
12364
- ))
12965
+ products.map((product) => {
12966
+ const focused = isSameProductSlug(focusProduct, product.slug);
12967
+ return /* @__PURE__ */ jsxRuntime.jsx(
12968
+ "button",
12969
+ {
12970
+ "data-boff-explore": "chip",
12971
+ "data-chip-kind": "product",
12972
+ "data-product": product.slug,
12973
+ type: "button",
12974
+ "aria-pressed": focused,
12975
+ title: product.tagline ?? product.name,
12976
+ onClick: () => onFocus(focused ? null : product.slug),
12977
+ className: chipClass(focused),
12978
+ children: product.name
12979
+ },
12980
+ product.slug
12981
+ );
12982
+ })
12365
12983
  ]
12366
12984
  }
12367
12985
  ),
@@ -12819,11 +13437,11 @@ function ExplorePage({
12819
13437
  const resolvedBody = welcomeBody ?? catalog.welcomeBody;
12820
13438
  const prompts = catalog.examplePrompts;
12821
13439
  const focusedProductName = React.useMemo(
12822
- () => catalog.products.find((p) => p.slug === focusProduct)?.name ?? null,
13440
+ () => catalog.products.find((p) => isSameProductSlug(p.slug, focusProduct))?.name ?? null,
12823
13441
  [catalog.products, focusProduct]
12824
13442
  );
12825
13443
  const headProductName = React.useMemo(
12826
- () => productSlug ? catalog.products.find((p) => p.slug === productSlug)?.name ?? null : null,
13444
+ () => productSlug ? catalog.products.find((p) => isSameProductSlug(p.slug, productSlug))?.name ?? null : null,
12827
13445
  [catalog.products, productSlug]
12828
13446
  );
12829
13447
  const imageOrigins = React.useMemo(
@@ -12834,6 +13452,10 @@ function ExplorePage({
12834
13452
  () => productNamesByOrigin(catalog.products),
12835
13453
  [catalog.products]
12836
13454
  );
13455
+ const productSlugs = React.useMemo(
13456
+ () => productSlugsByOrigin(catalog.products),
13457
+ [catalog.products]
13458
+ );
12837
13459
  React.useEffect(() => {
12838
13460
  const el = textareaRef.current;
12839
13461
  if (!el) return;
@@ -13396,6 +14018,7 @@ function ExplorePage({
13396
14018
  message,
13397
14019
  activity,
13398
14020
  imageOrigins,
14021
+ productSlugs,
13399
14022
  productNames,
13400
14023
  speechLang,
13401
14024
  onNavigate,
@@ -18437,6 +19060,9 @@ exports.LaunchCountdown = LaunchCountdown;
18437
19060
  exports.Markdown = Markdown;
18438
19061
  exports.NewsletterForm = NewsletterForm;
18439
19062
  exports.NewsletterPage = NewsletterPage;
19063
+ exports.PRODUCT_SLUG_ALIASES = PRODUCT_SLUG_ALIASES;
19064
+ exports.PRODUCT_SLUG_EMIT = PRODUCT_SLUG_EMIT;
19065
+ exports.PRODUCT_SLUG_RENAMES = PRODUCT_SLUG_RENAMES;
18440
19066
  exports.PUBLISHED_ANSWER_FIELDS = PUBLISHED_ANSWER_FIELDS;
18441
19067
  exports.PageHead = PageHead;
18442
19068
  exports.PartnersPage = PartnersPage;
@@ -18473,10 +19099,12 @@ exports.classifyExploreError = classifyExploreError;
18473
19099
  exports.cn = cn;
18474
19100
  exports.conceptPath = conceptPath;
18475
19101
  exports.detectElectronPlatform = detectElectronPlatform;
19102
+ exports.emitProductSlug = emitProductSlug;
18476
19103
  exports.exploreQuestionHref = exploreQuestionHref;
18477
19104
  exports.fetchPublicJobPosting = fetchPublicJobPosting;
18478
19105
  exports.fetchPublicJobPostings = fetchPublicJobPostings;
18479
19106
  exports.fetchPublicJobPostingsForSubmission = fetchPublicJobPostingsForSubmission;
19107
+ exports.fetchPublicProduct = fetchPublicProduct;
18480
19108
  exports.formatAnswerDate = formatAnswerDate;
18481
19109
  exports.formatDownloads = formatDownloads;
18482
19110
  exports.formatPrice = formatPrice2;
@@ -18486,11 +19114,17 @@ exports.getRecaptchaV3Token = getRecaptchaV3Token;
18486
19114
  exports.inlineImageOrigins = inlineImageOrigins;
18487
19115
  exports.isFooterlessPath = isFooterlessPath;
18488
19116
  exports.isInlineImageAllowed = isInlineImageAllowed;
19117
+ exports.isInlineVideoAllowed = isInlineVideoAllowed;
18489
19118
  exports.isPublishedSourceUrl = isPublishedSourceUrl;
19119
+ exports.isSameProductSlug = isSameProductSlug;
18490
19120
  exports.isValidUrl = isValidUrl;
19121
+ exports.legacyProductSlug = legacyProductSlug;
18491
19122
  exports.loadRecaptchaV3 = loadRecaptchaV3;
18492
19123
  exports.looksLikeDeliverableEmail = looksLikeDeliverableEmail;
19124
+ exports.normalizeProductSlug = normalizeProductSlug;
18493
19125
  exports.normalizeRoutePath = normalizeRoutePath;
19126
+ exports.productSlugVariants = productSlugVariants;
19127
+ exports.productSlugsByOrigin = productSlugsByOrigin;
18494
19128
  exports.shouldRenderSiteFooter = shouldRenderSiteFooter;
18495
19129
  exports.speechLocaleOf = speechLocaleOf;
18496
19130
  exports.stopSpeaking = stopSpeaking;