@farming-labs/theme 0.1.112 → 0.1.114

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.
@@ -3,6 +3,7 @@ import { withLangInUrl } from "./i18n.mjs";
3
3
  import { DocsPageClient } from "./docs-page-client.mjs";
4
4
  import { DocsAIFeatures } from "./docs-ai-features.mjs";
5
5
  import { DocsCommandSearch } from "./docs-command-search.mjs";
6
+ import { resolveOpenDocsProviders } from "./open-docs-providers.mjs";
6
7
  import { resolvePageReadingTime, resolveReadingTimeOptions } from "./reading-time.mjs";
7
8
  import { SidebarSearchWithAI } from "./sidebar-search-ai.mjs";
8
9
  import { LocaleThemeControl } from "./locale-theme-control.mjs";
@@ -660,11 +661,12 @@ function createDocsLayout(config, options) {
660
661
  const readingTimeWordsPerMinute = readingTimeOptions.wordsPerMinute ?? 220;
661
662
  const llmsTxtEnabled = resolveEnabledByDefault(config.llmsTxt);
662
663
  const feedbackConfig = resolveFeedbackConfig(config.feedback);
663
- const openDocsProviders = (pageActions?.openDocs && typeof pageActions.openDocs === "object" && pageActions.openDocs.providers ? pageActions.openDocs.providers : void 0)?.map((p) => ({
664
- name: p.name,
665
- urlTemplate: p.urlTemplate,
666
- iconHtml: p.icon ? serializeIcon(p.icon) : void 0
667
- }));
664
+ const openDocsConfig = pageActions?.openDocs && typeof pageActions.openDocs === "object" ? pageActions.openDocs : void 0;
665
+ const openDocsProviders = resolveOpenDocsProviders(openDocsConfig?.providers, {
666
+ target: openDocsConfig?.target,
667
+ prompt: openDocsConfig?.prompt,
668
+ serializeIcon
669
+ });
668
670
  const githubRaw = config.github;
669
671
  const githubUrl = typeof githubRaw === "string" ? githubRaw.replace(/\/$/, "") : githubRaw?.url.replace(/\/$/, "");
670
672
  const githubBranch = typeof githubRaw === "object" ? githubRaw.branch ?? "main" : "main";
@@ -784,6 +786,8 @@ function createDocsLayout(config, options) {
784
786
  copyMarkdown: copyMarkdownEnabled,
785
787
  openDocs: openDocsEnabled,
786
788
  openDocsProviders,
789
+ openDocsTarget: openDocsConfig?.target,
790
+ openDocsPrompt: openDocsConfig?.prompt,
787
791
  pageActionsPosition,
788
792
  pageActionsAlignment,
789
793
  githubUrl,
@@ -8,6 +8,8 @@ interface SerializedProvider {
8
8
  name: string;
9
9
  iconHtml?: string;
10
10
  urlTemplate: string;
11
+ target?: "markdown" | "page" | "source" | "github";
12
+ prompt?: string;
11
13
  }
12
14
  interface DocsPageClientProps {
13
15
  tocEnabled: boolean;
@@ -23,6 +25,8 @@ interface DocsPageClientProps {
23
25
  copyMarkdown?: boolean;
24
26
  openDocs?: boolean;
25
27
  openDocsProviders?: SerializedProvider[];
28
+ openDocsTarget?: "markdown" | "page" | "source" | "github";
29
+ openDocsPrompt?: string;
26
30
  /** Where to render page actions relative to the title */
27
31
  pageActionsPosition?: "above-title" | "below-title";
28
32
  /** Horizontal alignment of page action buttons */
@@ -86,6 +90,8 @@ declare function DocsPageClient({
86
90
  copyMarkdown,
87
91
  openDocs,
88
92
  openDocsProviders,
93
+ openDocsTarget,
94
+ openDocsPrompt,
89
95
  pageActionsPosition,
90
96
  pageActionsAlignment,
91
97
  githubUrl,
@@ -263,7 +263,7 @@ function TitleDecorations({ description, belowTitle }) {
263
263
  if (!description && !belowTitle) return null;
264
264
  return /* @__PURE__ */ jsx(Fragment$1, { children: Children.toArray([description, belowTitle].filter(Boolean)) });
265
265
  }
266
- function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled = true, changelogBasePath, entry = "docs", publicPath, locale, copyMarkdown = false, openDocs = false, openDocsProviders, pageActionsPosition = "below-title", pageActionsAlignment = "left", githubUrl, contentDir, githubBranch = "main", githubDirectory, editOnGithubUrl, lastModifiedMap, lastModified: lastModifiedProp, readingTimeMap, readingTime: readingTimeProp, structuredDataMap, structuredData: structuredDataProp, readingTimeEnabled = false, lastUpdatedEnabled = true, lastUpdatedPosition = "footer", llmsTxtEnabled = false, descriptionMap, description, feedbackEnabled = false, feedbackQuestion, feedbackPlaceholder, feedbackPositiveLabel, feedbackNegativeLabel, feedbackSubmitLabel, feedbackOnFeedback, analytics = false, children }) {
266
+ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled = true, changelogBasePath, entry = "docs", publicPath, locale, copyMarkdown = false, openDocs = false, openDocsProviders, openDocsTarget, openDocsPrompt, pageActionsPosition = "below-title", pageActionsAlignment = "left", githubUrl, contentDir, githubBranch = "main", githubDirectory, editOnGithubUrl, lastModifiedMap, lastModified: lastModifiedProp, readingTimeMap, readingTime: readingTimeProp, structuredDataMap, structuredData: structuredDataProp, readingTimeEnabled = false, lastUpdatedEnabled = true, lastUpdatedPosition = "footer", llmsTxtEnabled = false, descriptionMap, description, feedbackEnabled = false, feedbackQuestion, feedbackPlaceholder, feedbackPositiveLabel, feedbackNegativeLabel, feedbackSubmitLabel, feedbackOnFeedback, analytics = false, children }) {
267
267
  const fdTocStyle = tocStyle === "directional" ? "clerk" : void 0;
268
268
  const [toc, setToc] = useState([]);
269
269
  const [titlePortalHost, setTitlePortalHost] = useState(null);
@@ -414,6 +414,8 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
414
414
  copyMarkdown,
415
415
  openDocs,
416
416
  providers: openDocsProviders,
417
+ openDocsTarget,
418
+ openDocsPrompt,
417
419
  alignment: pageActionsAlignment,
418
420
  githubFileUrl,
419
421
  analytics
@@ -492,6 +494,8 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
492
494
  copyMarkdown,
493
495
  openDocs,
494
496
  providers: openDocsProviders,
497
+ openDocsTarget,
498
+ openDocsPrompt,
495
499
  alignment: pageActionsAlignment,
496
500
  githubFileUrl,
497
501
  analytics
@@ -0,0 +1,79 @@
1
+ //#region src/open-docs-providers.ts
2
+ const PROMPT_PROVIDER_TEMPLATES = {
3
+ chatgpt: "https://chatgpt.com/?q={prompt}",
4
+ claude: "https://claude.ai/new?q={prompt}",
5
+ cursor: "https://cursor.com/link/prompt?text={prompt}",
6
+ gemini: "https://gemini.google.com/app?q={prompt}",
7
+ copilot: "https://github.com/copilot?prompt={prompt}"
8
+ };
9
+ const OPEN_DOCS_PROVIDER_PRESETS = {
10
+ chatgpt: {
11
+ name: "ChatGPT",
12
+ urlTemplate: "https://chatgpt.com/?q={prompt}",
13
+ promptUrlTemplate: PROMPT_PROVIDER_TEMPLATES.chatgpt
14
+ },
15
+ claude: {
16
+ name: "Claude",
17
+ urlTemplate: "https://claude.ai/new?q={prompt}",
18
+ promptUrlTemplate: PROMPT_PROVIDER_TEMPLATES.claude
19
+ },
20
+ cursor: {
21
+ name: "Cursor",
22
+ urlTemplate: "https://cursor.com/link/prompt?text={prompt}",
23
+ promptUrlTemplate: PROMPT_PROVIDER_TEMPLATES.cursor
24
+ },
25
+ gemini: {
26
+ name: "Gemini",
27
+ urlTemplate: "https://gemini.google.com/app?q={prompt}",
28
+ promptUrlTemplate: PROMPT_PROVIDER_TEMPLATES.gemini
29
+ },
30
+ copilot: {
31
+ name: "Copilot",
32
+ urlTemplate: "https://github.com/copilot?prompt={prompt}",
33
+ promptUrlTemplate: PROMPT_PROVIDER_TEMPLATES.copilot
34
+ },
35
+ github: {
36
+ name: "GitHub",
37
+ urlTemplate: "{githubUrl}",
38
+ promptUrlTemplate: "{githubUrl}",
39
+ target: "github"
40
+ }
41
+ };
42
+ function normalizeProviderName(name) {
43
+ return name.trim().toLowerCase();
44
+ }
45
+ function resolveOpenDocsProviders(providers, options = {}) {
46
+ if (!providers || providers.length === 0) return void 0;
47
+ const serialized = providers.map((provider) => resolveOpenDocsProvider(provider, options)).filter((provider) => provider !== void 0);
48
+ return serialized.length > 0 ? serialized : void 0;
49
+ }
50
+ function resolveOpenDocsProvider(provider, options = {}) {
51
+ const normalizedId = typeof provider === "string" ? normalizeProviderName(provider) : typeof provider.id === "string" ? normalizeProviderName(provider.id) : typeof provider.name === "string" ? normalizeProviderName(provider.name) : typeof provider.label === "string" ? normalizeProviderName(provider.label) : void 0;
52
+ const preset = normalizedId ? OPEN_DOCS_PROVIDER_PRESETS[normalizedId] : void 0;
53
+ if (typeof provider === "string") {
54
+ if (!preset) return void 0;
55
+ return {
56
+ name: preset.name,
57
+ urlTemplate: preset.urlTemplate,
58
+ promptUrlTemplate: preset.promptUrlTemplate,
59
+ target: preset.target ?? options.target,
60
+ prompt: options.prompt
61
+ };
62
+ }
63
+ const cursorAppTemplate = normalizedId === "cursor" && provider.mode === "app" ? "cursor://anysphere.cursor-deeplink/prompt?text={prompt}" : void 0;
64
+ const name = provider.name ?? provider.label ?? preset?.name;
65
+ const urlTemplate = provider.urlTemplate ?? cursorAppTemplate ?? preset?.urlTemplate;
66
+ const hasCustomUrlTemplate = typeof provider.urlTemplate === "string";
67
+ if (!name || !urlTemplate) return void 0;
68
+ return {
69
+ name,
70
+ urlTemplate,
71
+ promptUrlTemplate: provider.promptUrlTemplate ?? cursorAppTemplate ?? preset?.promptUrlTemplate,
72
+ iconHtml: options.serializeIcon?.(provider.icon) ?? (typeof provider.icon === "string" ? provider.icon : void 0),
73
+ target: provider.target ?? preset?.target ?? options.target ?? (hasCustomUrlTemplate ? "page" : void 0),
74
+ prompt: provider.prompt ?? options.prompt
75
+ };
76
+ }
77
+
78
+ //#endregion
79
+ export { resolveOpenDocsProviders };
@@ -6,11 +6,15 @@ interface SerializedProvider {
6
6
  name: string;
7
7
  iconHtml?: string;
8
8
  urlTemplate: string;
9
+ target?: "markdown" | "page" | "source" | "github";
10
+ prompt?: string;
9
11
  }
10
12
  interface PageActionsProps {
11
13
  copyMarkdown?: boolean;
12
14
  openDocs?: boolean;
13
15
  providers?: SerializedProvider[];
16
+ openDocsTarget?: "markdown" | "page" | "source" | "github";
17
+ openDocsPrompt?: string;
14
18
  alignment?: "left" | "right";
15
19
  /** GitHub file URL (edit view) for the current page. Used when urlTemplate contains {githubUrl}. */
16
20
  githubFileUrl?: string | null;
@@ -20,6 +24,8 @@ declare function PageActions({
20
24
  copyMarkdown,
21
25
  openDocs,
22
26
  providers,
27
+ openDocsTarget,
28
+ openDocsPrompt,
23
29
  alignment,
24
30
  githubFileUrl,
25
31
  analytics
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { emitClientAnalyticsEvent } from "./client-analytics.mjs";
4
+ import { sanitizeIconHtml } from "./safe-icon-html.mjs";
4
5
  import { useCallback, useEffect, useRef, useState } from "react";
5
6
  import { usePathname } from "fumadocs-core/framework";
6
7
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -68,12 +69,30 @@ const ExternalLinkIcon = () => /* @__PURE__ */ jsxs("svg", {
68
69
  });
69
70
  const DEFAULT_PROVIDERS = [{
70
71
  name: "ChatGPT",
71
- urlTemplate: "https://chatgpt.com/?hints=search&q=Read+{mdxUrl},+I+want+to+ask+questions+about+it."
72
+ urlTemplate: "https://chatgpt.com/?q={prompt}"
72
73
  }, {
73
74
  name: "Claude",
74
- urlTemplate: "https://claude.ai/new?q=Read+{mdxUrl},+I+want+to+ask+questions+about+it."
75
+ urlTemplate: "https://claude.ai/new?q={prompt}"
75
76
  }];
76
- function PageActions({ copyMarkdown, openDocs, providers, alignment = "left", githubFileUrl, analytics = false }) {
77
+ const DEFAULT_OPEN_DOCS_TARGET = "markdown";
78
+ const DEFAULT_OPEN_DOCS_PROMPT = "Read this documentation: {url}";
79
+ function pageUrlToMarkdownUrl(pageUrl) {
80
+ try {
81
+ const url = new URL(pageUrl);
82
+ const pathname = url.pathname.replace(/\/+$/, "") || url.pathname;
83
+ url.pathname = pathname.endsWith(".md") ? pathname : `${pathname}.md`;
84
+ url.search = "";
85
+ url.hash = "";
86
+ return url.toString();
87
+ } catch {
88
+ const clean = pageUrl.replace(/[?#].*$/, "").replace(/\/+$/, "") || pageUrl;
89
+ return clean.endsWith(".md") ? clean : `${clean}.md`;
90
+ }
91
+ }
92
+ function fillPromptTemplate(template, values) {
93
+ return template.replace(/\{pageUrl\}/g, values.pageUrl).replace(/\{markdownUrl\}/g, values.markdownUrl).replace(/\{sourceUrl\}/g, values.sourceUrl).replace(/\{mdxUrl\}/g, values.sourceUrl).replace(/\{githubUrl\}/g, values.githubUrl).replace(/\{url\}/g, values.url);
94
+ }
95
+ function PageActions({ copyMarkdown, openDocs, providers, openDocsTarget = DEFAULT_OPEN_DOCS_TARGET, openDocsPrompt = DEFAULT_OPEN_DOCS_PROMPT, alignment = "left", githubFileUrl, analytics = false }) {
77
96
  const [copied, setCopied] = useState(false);
78
97
  const [dropdownOpen, setDropdownOpen] = useState(false);
79
98
  const dropdownRef = useRef(null);
@@ -99,13 +118,28 @@ function PageActions({ copyMarkdown, openDocs, providers, alignment = "left", gi
99
118
  }, [analytics, pathname]);
100
119
  const handleOpen = useCallback((provider) => {
101
120
  const template = provider.urlTemplate;
102
- if (/\{githubUrl\}/.test(template) && !githubFileUrl) {
121
+ const githubUrl = githubFileUrl ?? "";
122
+ if (/\{githubUrl\}/.test(template) && !githubUrl) {
103
123
  setDropdownOpen(false);
104
124
  return;
105
125
  }
106
126
  const pageUrl = window.location.href;
107
- const mdxUrl = `${window.location.origin}${pathname}.mdx`;
108
- let url = template.replace(/\{url\}/g, encodeURIComponent(pageUrl)).replace(/\{mdxUrl\}/g, encodeURIComponent(mdxUrl)).replace(/\{githubUrl\}/g, githubFileUrl ?? "");
127
+ const sourceUrl = `${window.location.origin}${pathname}.mdx`;
128
+ const markdownUrl = pageUrlToMarkdownUrl(pageUrl);
129
+ const target = provider.target ?? openDocsTarget;
130
+ if (target === "github" && !githubUrl) {
131
+ setDropdownOpen(false);
132
+ return;
133
+ }
134
+ const targetUrl = target === "markdown" ? markdownUrl : target === "source" ? sourceUrl : target === "github" ? githubUrl : pageUrl;
135
+ const prompt = fillPromptTemplate(provider.prompt ?? openDocsPrompt, {
136
+ url: targetUrl,
137
+ pageUrl,
138
+ markdownUrl,
139
+ sourceUrl,
140
+ githubUrl
141
+ });
142
+ let url = template.replace(/\{prompt\}/g, encodeURIComponent(prompt)).replace(/\{url\}/g, encodeURIComponent(targetUrl)).replace(/\{pageUrl\}/g, encodeURIComponent(pageUrl)).replace(/\{markdownUrl\}/g, encodeURIComponent(markdownUrl)).replace(/\{sourceUrl\}/g, encodeURIComponent(sourceUrl)).replace(/\{mdxUrl\}/g, encodeURIComponent(sourceUrl)).replace(/\{githubUrl\}/g, githubUrl);
109
143
  if (analytics) emitClientAnalyticsEvent({
110
144
  type: "page_action_open_docs",
111
145
  properties: {
@@ -119,7 +153,9 @@ function PageActions({ copyMarkdown, openDocs, providers, alignment = "left", gi
119
153
  }, [
120
154
  analytics,
121
155
  pathname,
122
- githubFileUrl
156
+ githubFileUrl,
157
+ openDocsPrompt,
158
+ openDocsTarget
123
159
  ]);
124
160
  useEffect(() => {
125
161
  if (!dropdownOpen) return;
@@ -166,23 +202,26 @@ function PageActions({ copyMarkdown, openDocs, providers, alignment = "left", gi
166
202
  }), dropdownOpen && /* @__PURE__ */ jsx("div", {
167
203
  className: "fd-page-action-menu",
168
204
  role: "menu",
169
- children: resolvedProviders.map((provider) => /* @__PURE__ */ jsxs("button", {
170
- type: "button",
171
- role: "menuitem",
172
- className: "fd-page-action-menu-item",
173
- onClick: () => handleOpen(provider),
174
- children: [
175
- provider.iconHtml && /* @__PURE__ */ jsx("span", {
176
- className: "fd-page-action-menu-icon",
177
- dangerouslySetInnerHTML: { __html: provider.iconHtml }
178
- }),
179
- /* @__PURE__ */ jsxs("span", {
180
- className: "fd-page-action-menu-label",
181
- children: ["Open in ", provider.name]
182
- }),
183
- /* @__PURE__ */ jsx(ExternalLinkIcon, {})
184
- ]
185
- }, provider.name))
205
+ children: resolvedProviders.map((provider) => {
206
+ const iconHtml = sanitizeIconHtml(provider.iconHtml);
207
+ return /* @__PURE__ */ jsxs("button", {
208
+ type: "button",
209
+ role: "menuitem",
210
+ className: "fd-page-action-menu-item",
211
+ onClick: () => handleOpen(provider),
212
+ children: [
213
+ iconHtml && /* @__PURE__ */ jsx("span", {
214
+ className: "fd-page-action-menu-icon",
215
+ dangerouslySetInnerHTML: { __html: iconHtml }
216
+ }),
217
+ /* @__PURE__ */ jsxs("span", {
218
+ className: "fd-page-action-menu-label",
219
+ children: ["Open in ", provider.name]
220
+ }),
221
+ /* @__PURE__ */ jsx(ExternalLinkIcon, {})
222
+ ]
223
+ }, provider.name);
224
+ })
186
225
  })]
187
226
  })]
188
227
  });
package/dist/prompt.d.mts CHANGED
@@ -4,12 +4,16 @@ import * as react_jsx_runtime0 from "react/jsx-runtime";
4
4
  //#region src/prompt.d.ts
5
5
  type PromptAction = "copy" | "open";
6
6
  type PromptIconValue = React.ReactNode | string;
7
- interface PromptOpenDocsProvider {
8
- name: string;
7
+ type PromptOpenDocsProvider = string | {
8
+ id?: string;
9
+ name?: string;
10
+ label?: string;
9
11
  icon?: PromptIconValue;
10
- urlTemplate: string;
12
+ iconHtml?: string;
13
+ urlTemplate?: string;
11
14
  promptUrlTemplate?: string;
12
- }
15
+ mode?: "web" | "app";
16
+ };
13
17
  interface PromptProps {
14
18
  title?: string;
15
19
  description?: string;
package/dist/prompt.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { sanitizeIconHtml } from "./safe-icon-html.mjs";
3
4
  import { extractPromptText } from "./prompt-text.mjs";
4
5
  import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
6
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -74,6 +75,13 @@ const defaultPromptProviderTemplates = {
74
75
  gemini: "https://gemini.google.com/app?q={prompt}",
75
76
  copilot: "https://github.com/copilot?prompt={prompt}"
76
77
  };
78
+ const defaultPromptProviderLabels = {
79
+ chatgpt: "ChatGPT",
80
+ claude: "Claude",
81
+ cursor: "Cursor",
82
+ gemini: "Gemini",
83
+ copilot: "Copilot"
84
+ };
77
85
  function normalizeProviderName(name) {
78
86
  return name.trim().toLowerCase();
79
87
  }
@@ -93,8 +101,32 @@ function parseStringArray(value) {
93
101
  return normalized.length > 0 ? normalized : void 0;
94
102
  }
95
103
  function resolveProviderChoices(availableProviders, preferredNames) {
96
- const configuredByName = new Map((availableProviders ?? []).map((provider) => [normalizeProviderName(provider.name), provider]));
97
- const names = preferredNames && preferredNames.length > 0 ? preferredNames : (availableProviders ?? []).map((provider) => provider.name);
104
+ const configuredProviders = (availableProviders ?? []).map((provider) => {
105
+ if (typeof provider === "string") {
106
+ const normalized = normalizeProviderName(provider);
107
+ return {
108
+ normalized,
109
+ name: defaultPromptProviderLabels[normalized] ?? provider,
110
+ provider: {
111
+ id: provider,
112
+ name: defaultPromptProviderLabels[normalized] ?? provider
113
+ }
114
+ };
115
+ }
116
+ const rawName = provider.name ?? provider.label ?? provider.id;
117
+ if (!rawName) return null;
118
+ const normalized = normalizeProviderName(rawName);
119
+ return {
120
+ normalized,
121
+ name: provider.name ?? provider.label ?? defaultPromptProviderLabels[normalized] ?? rawName,
122
+ provider: {
123
+ ...provider,
124
+ name: provider.name ?? provider.label ?? defaultPromptProviderLabels[normalized] ?? rawName
125
+ }
126
+ };
127
+ }).filter((entry) => entry !== null);
128
+ const configuredByName = new Map(configuredProviders.map((entry) => [entry.normalized, entry.provider]));
129
+ const names = preferredNames && preferredNames.length > 0 ? preferredNames : configuredProviders.map((entry) => entry.name);
98
130
  const seen = /* @__PURE__ */ new Set();
99
131
  const resolved = [];
100
132
  for (const rawName of names) {
@@ -104,11 +136,13 @@ function resolveProviderChoices(availableProviders, preferredNames) {
104
136
  if (seen.has(normalized)) continue;
105
137
  seen.add(normalized);
106
138
  const configured = configuredByName.get(normalized);
107
- const template = configured?.promptUrlTemplate ?? configured?.urlTemplate ?? defaultPromptProviderTemplates[normalized];
139
+ const cursorAppTemplate = normalized === "cursor" && configured?.mode === "app" ? "cursor://anysphere.cursor-deeplink/prompt?text={prompt}" : void 0;
140
+ const template = configured?.promptUrlTemplate ?? cursorAppTemplate ?? configured?.urlTemplate ?? defaultPromptProviderTemplates[normalized];
108
141
  if (!template) continue;
109
142
  resolved.push({
110
- name: configured?.name ?? name,
143
+ name: configured?.name ?? defaultPromptProviderLabels[normalized] ?? name,
111
144
  icon: configured?.icon,
145
+ iconHtml: configured?.iconHtml,
112
146
  urlTemplate: template
113
147
  });
114
148
  }
@@ -252,23 +286,29 @@ function Prompt({ title, description, prompt, icon, showTitle = true, showDescri
252
286
  }), menuOpen && /* @__PURE__ */ jsx("div", {
253
287
  className: "fd-prompt-menu",
254
288
  role: "menu",
255
- children: resolvedProviders.map((provider) => /* @__PURE__ */ jsxs("button", {
256
- type: "button",
257
- role: "menuitem",
258
- className: "fd-prompt-menu-item",
259
- onClick: () => handleOpen(provider),
260
- children: [provider.icon && typeof provider.icon !== "string" ? /* @__PURE__ */ jsx("span", {
261
- className: "fd-prompt-menu-icon",
262
- children: provider.icon
263
- }) : null, /* @__PURE__ */ jsxs("span", {
264
- className: "fd-prompt-menu-label",
265
- children: [
266
- openLabel,
267
- " ",
268
- provider.name
269
- ]
270
- })]
271
- }, provider.name))
289
+ children: resolvedProviders.map((provider) => {
290
+ const iconHtml = sanitizeIconHtml(provider.iconHtml);
291
+ return /* @__PURE__ */ jsxs("button", {
292
+ type: "button",
293
+ role: "menuitem",
294
+ className: "fd-prompt-menu-item",
295
+ onClick: () => handleOpen(provider),
296
+ children: [provider.icon && typeof provider.icon !== "string" ? /* @__PURE__ */ jsx("span", {
297
+ className: "fd-prompt-menu-icon",
298
+ children: provider.icon
299
+ }) : iconHtml ? /* @__PURE__ */ jsx("span", {
300
+ className: "fd-prompt-menu-icon",
301
+ dangerouslySetInnerHTML: { __html: iconHtml }
302
+ }) : null, /* @__PURE__ */ jsxs("span", {
303
+ className: "fd-prompt-menu-label",
304
+ children: [
305
+ openLabel,
306
+ " ",
307
+ provider.name
308
+ ]
309
+ })]
310
+ }, provider.name);
311
+ })
272
312
  })]
273
313
  }) : null
274
314
  ]
@@ -0,0 +1,45 @@
1
+ //#region src/safe-icon-html.ts
2
+ const SAFE_ICON_TAGS = new Set([
3
+ "circle",
4
+ "clippath",
5
+ "defs",
6
+ "desc",
7
+ "ellipse",
8
+ "g",
9
+ "line",
10
+ "lineargradient",
11
+ "mask",
12
+ "path",
13
+ "polygon",
14
+ "polyline",
15
+ "radialgradient",
16
+ "rect",
17
+ "span",
18
+ "stop",
19
+ "svg",
20
+ "symbol",
21
+ "title",
22
+ "use"
23
+ ]);
24
+ const TAG_PATTERN = /<\/?\s*([a-zA-Z][\w:-]*)\b/g;
25
+ const BLOCKED_MARKUP_PATTERN = /<\s*!/;
26
+ const BLOCKED_ATTRIBUTE_PATTERN = /(?:^|[\s/])(?:on[a-z][\w:-]*|srcdoc|style)\s*=/i;
27
+ const URL_ATTRIBUTE_PATTERN = /(?:^|[\s/])(?:href|xlink:href|src)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
28
+ function sanitizeIconHtml(html) {
29
+ const trimmed = html?.trim();
30
+ if (!trimmed || trimmed.length > 1e4) return void 0;
31
+ if (BLOCKED_MARKUP_PATTERN.test(trimmed) || BLOCKED_ATTRIBUTE_PATTERN.test(trimmed)) return;
32
+ let tagMatch;
33
+ TAG_PATTERN.lastIndex = 0;
34
+ while ((tagMatch = TAG_PATTERN.exec(trimmed)) !== null) if (!SAFE_ICON_TAGS.has(tagMatch[1].toLowerCase())) return void 0;
35
+ let urlMatch;
36
+ URL_ATTRIBUTE_PATTERN.lastIndex = 0;
37
+ while ((urlMatch = URL_ATTRIBUTE_PATTERN.exec(trimmed)) !== null) {
38
+ const value = (urlMatch[1] ?? urlMatch[2] ?? urlMatch[3] ?? "").trim();
39
+ if (value && !value.startsWith("#")) return void 0;
40
+ }
41
+ return trimmed;
42
+ }
43
+
44
+ //#endregion
45
+ export { sanitizeIconHtml };
@@ -3,6 +3,7 @@ import { escapeJsonLdForScript } from "./json-ld.mjs";
3
3
  import { DocsPageClient } from "./docs-page-client.mjs";
4
4
  import { DocsAIFeatures } from "./docs-ai-features.mjs";
5
5
  import { DocsCommandSearch } from "./docs-command-search.mjs";
6
+ import { resolveOpenDocsProviders } from "./open-docs-providers.mjs";
6
7
  import { resolveReadingTimeOptions } from "./reading-time.mjs";
7
8
  import { SidebarSearchWithAI } from "./sidebar-search-ai.mjs";
8
9
  import { LocaleThemeControl } from "./locale-theme-control.mjs";
@@ -245,10 +246,11 @@ function TanstackDocsLayout({ config, tree, locale, description, readingTime, la
245
246
  const llmsTxtEnabled = resolveEnabledByDefault(config.llmsTxt);
246
247
  const feedbackConfig = resolveFeedbackConfig(config.feedback);
247
248
  const staticExport = !!config.staticExport;
248
- const openDocsProviders = (pageActions?.openDocs && typeof pageActions.openDocs === "object" && pageActions.openDocs.providers ? pageActions.openDocs.providers : void 0)?.map((provider) => ({
249
- name: provider.name,
250
- urlTemplate: provider.urlTemplate
251
- }));
249
+ const openDocsConfig = pageActions?.openDocs && typeof pageActions.openDocs === "object" ? pageActions.openDocs : void 0;
250
+ const openDocsProviders = resolveOpenDocsProviders(openDocsConfig?.providers, {
251
+ target: openDocsConfig?.target,
252
+ prompt: openDocsConfig?.prompt
253
+ });
252
254
  const aiConfig = config.ai;
253
255
  const aiEnabled = !staticExport && !!aiConfig?.enabled;
254
256
  const aiMode = aiConfig?.mode ?? "search";
@@ -348,6 +350,8 @@ function TanstackDocsLayout({ config, tree, locale, description, readingTime, la
348
350
  copyMarkdown: copyMarkdownEnabled,
349
351
  openDocs: openDocsEnabled,
350
352
  openDocsProviders,
353
+ openDocsTarget: openDocsConfig?.target,
354
+ openDocsPrompt: openDocsConfig?.prompt,
351
355
  pageActionsPosition,
352
356
  pageActionsAlignment,
353
357
  editOnGithubUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/theme",
3
- "version": "0.1.112",
3
+ "version": "0.1.114",
4
4
  "description": "Theme package for @farming-labs/docs — layout, provider, MDX components, and styles",
5
5
  "keywords": [
6
6
  "docs",
@@ -139,7 +139,7 @@
139
139
  "tsdown": "^0.20.3",
140
140
  "typescript": "^5.9.3",
141
141
  "vitest": "^3.2.4",
142
- "@farming-labs/docs": "0.1.112"
142
+ "@farming-labs/docs": "0.1.114"
143
143
  },
144
144
  "peerDependencies": {
145
145
  "@farming-labs/docs": ">=0.0.1",