@farming-labs/theme 0.0.51 → 0.0.53

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.
@@ -0,0 +1,14 @@
1
+ import { CodeBlockCopyData, DocsFeedbackData } from "@farming-labs/docs";
2
+
3
+ //#region src/docs-client-hooks.d.ts
4
+ type CopyHandler = (data: CodeBlockCopyData) => void;
5
+ type FeedbackHandler = (data: DocsFeedbackData) => void | Promise<void>;
6
+ declare function DocsClientHooks({
7
+ onCopyClick,
8
+ onFeedback
9
+ }: {
10
+ onCopyClick?: CopyHandler;
11
+ onFeedback?: FeedbackHandler;
12
+ }): null;
13
+ //#endregion
14
+ export { DocsClientHooks };
@@ -0,0 +1,26 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+
5
+ //#region src/docs-client-hooks.tsx
6
+ function useWindowHook(key, handler) {
7
+ useEffect(() => {
8
+ if (typeof window === "undefined") return;
9
+ if (typeof handler !== "function") return;
10
+ const target = window;
11
+ const previous = target[key];
12
+ target[key] = handler;
13
+ return () => {
14
+ if (target[key] === handler) if (typeof previous === "function") target[key] = previous;
15
+ else delete target[key];
16
+ };
17
+ }, [handler, key]);
18
+ }
19
+ function DocsClientHooks({ onCopyClick, onFeedback }) {
20
+ useWindowHook("__fdOnCopyClick__", onCopyClick);
21
+ useWindowHook("__fdOnFeedback__", onFeedback);
22
+ return null;
23
+ }
24
+
25
+ //#endregion
26
+ export { DocsClientHooks };
@@ -0,0 +1,28 @@
1
+ import { DocsFeedbackData } from "@farming-labs/docs";
2
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
3
+
4
+ //#region src/docs-feedback.d.ts
5
+ interface DocsFeedbackProps {
6
+ pathname: string;
7
+ entry: string;
8
+ locale?: string;
9
+ question?: string;
10
+ placeholder?: string;
11
+ positiveLabel?: string;
12
+ negativeLabel?: string;
13
+ submitLabel?: string;
14
+ onFeedback?: (data: DocsFeedbackData) => void | Promise<void>;
15
+ }
16
+ declare function DocsFeedback({
17
+ pathname,
18
+ entry,
19
+ locale,
20
+ question,
21
+ placeholder,
22
+ positiveLabel,
23
+ negativeLabel,
24
+ submitLabel,
25
+ onFeedback
26
+ }: DocsFeedbackProps): react_jsx_runtime0.JSX.Element;
27
+ //#endregion
28
+ export { DocsFeedback, DocsFeedbackProps };
@@ -0,0 +1,210 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+
6
+ //#region src/docs-feedback.tsx
7
+ function normalizePathname(pathname) {
8
+ return pathname.replace(/\/$/, "") || "/";
9
+ }
10
+ function resolveSlug(entry, pathname) {
11
+ const entryParts = entry.split("/").filter(Boolean);
12
+ const pathParts = pathname.split("/").filter(Boolean);
13
+ return (pathParts.slice(0, entryParts.length).join("/") === entryParts.join("/") ? pathParts.slice(entryParts.length) : pathParts).join("/");
14
+ }
15
+ function readTextContent(selector) {
16
+ if (typeof document === "undefined") return void 0;
17
+ const text = document.querySelector(selector)?.textContent?.trim();
18
+ return text && text.length > 0 ? text : void 0;
19
+ }
20
+ async function emitFeedback(data, onFeedback) {
21
+ let firstError;
22
+ try {
23
+ await onFeedback?.(data);
24
+ } catch (error) {
25
+ firstError ??= error;
26
+ }
27
+ if (typeof window === "undefined") {
28
+ if (firstError) throw firstError;
29
+ return;
30
+ }
31
+ try {
32
+ await window.__fdOnFeedback__?.(data);
33
+ } catch (error) {
34
+ firstError ??= error;
35
+ }
36
+ window.dispatchEvent(new CustomEvent("fd:feedback", { detail: data }));
37
+ if (firstError) throw firstError;
38
+ }
39
+ function buildFeedbackPayload(value, pathname, entry, comment, locale) {
40
+ const normalizedPathname = normalizePathname(pathname);
41
+ return {
42
+ value,
43
+ comment: comment?.trim() ? comment.trim() : void 0,
44
+ title: readTextContent(".fd-page-title, h1"),
45
+ description: readTextContent(".fd-page-description"),
46
+ url: typeof window !== "undefined" ? window.location.href : normalizedPathname,
47
+ pathname: normalizedPathname,
48
+ path: normalizedPathname,
49
+ entry,
50
+ slug: resolveSlug(entry, normalizedPathname),
51
+ locale
52
+ };
53
+ }
54
+ function ThumbUpIcon() {
55
+ return /* @__PURE__ */ jsx("svg", {
56
+ width: "14",
57
+ height: "14",
58
+ viewBox: "0 0 24 24",
59
+ fill: "none",
60
+ "aria-hidden": "true",
61
+ children: /* @__PURE__ */ jsx("path", {
62
+ d: "M7 21H5a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h2m0 11V10m0 11h9.28a2 2 0 0 0 1.97-1.66l1.2-7A2 2 0 0 0 17.48 10H13V6.5a2.5 2.5 0 0 0-2.5-2.5L7 10",
63
+ stroke: "currentColor",
64
+ strokeWidth: "2",
65
+ strokeLinecap: "round",
66
+ strokeLinejoin: "round"
67
+ })
68
+ });
69
+ }
70
+ function ThumbDownIcon() {
71
+ return /* @__PURE__ */ jsx("svg", {
72
+ width: "14",
73
+ height: "14",
74
+ viewBox: "0 0 24 24",
75
+ fill: "none",
76
+ "aria-hidden": "true",
77
+ children: /* @__PURE__ */ jsx("path", {
78
+ d: "M17 3h2a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-2M17 3v11m0-11H7.72a2 2 0 0 0-1.97 1.66l-1.2 7A2 2 0 0 0 6.52 14H11v3.5a2.5 2.5 0 0 0 2.5 2.5L17 14",
79
+ stroke: "currentColor",
80
+ strokeWidth: "2",
81
+ strokeLinecap: "round",
82
+ strokeLinejoin: "round"
83
+ })
84
+ });
85
+ }
86
+ function CheckIcon() {
87
+ return /* @__PURE__ */ jsx("svg", {
88
+ width: "14",
89
+ height: "14",
90
+ viewBox: "0 0 24 24",
91
+ fill: "none",
92
+ "aria-hidden": "true",
93
+ children: /* @__PURE__ */ jsx("path", {
94
+ d: "M20 6 9 17l-5-5",
95
+ stroke: "currentColor",
96
+ strokeWidth: "2",
97
+ strokeLinecap: "round",
98
+ strokeLinejoin: "round"
99
+ })
100
+ });
101
+ }
102
+ function DocsFeedback({ pathname, entry, locale, question = "How is this guide?", placeholder = "Leave your feedback...", positiveLabel = "Good", negativeLabel = "Bad", submitLabel = "Submit", onFeedback }) {
103
+ const [selected, setSelected] = useState(null);
104
+ const [comment, setComment] = useState("");
105
+ const [status, setStatus] = useState("idle");
106
+ const normalizedPathname = useMemo(() => normalizePathname(pathname), [pathname]);
107
+ const showForm = selected !== null;
108
+ useEffect(() => {
109
+ setSelected(null);
110
+ setComment("");
111
+ setStatus("idle");
112
+ }, [normalizedPathname]);
113
+ function handleSelect(value) {
114
+ setSelected(value);
115
+ if (status !== "idle") setStatus("idle");
116
+ }
117
+ async function handleSubmit() {
118
+ if (!selected || status === "submitting") return;
119
+ setStatus("submitting");
120
+ try {
121
+ await emitFeedback(buildFeedbackPayload(selected, normalizedPathname, entry, comment, locale), onFeedback);
122
+ setStatus("submitted");
123
+ } catch {
124
+ setStatus("error");
125
+ }
126
+ }
127
+ return /* @__PURE__ */ jsxs("section", {
128
+ className: "fd-feedback not-prose",
129
+ "aria-label": "Page feedback",
130
+ children: [/* @__PURE__ */ jsxs("div", {
131
+ className: "fd-feedback-content",
132
+ children: [/* @__PURE__ */ jsx("p", {
133
+ className: "fd-feedback-question",
134
+ children: question
135
+ }), /* @__PURE__ */ jsxs("div", {
136
+ className: "fd-feedback-actions",
137
+ role: "group",
138
+ "aria-label": question,
139
+ children: [/* @__PURE__ */ jsxs("button", {
140
+ type: "button",
141
+ className: "fd-page-action-btn fd-feedback-choice",
142
+ "aria-pressed": selected === "positive",
143
+ "data-selected": selected === "positive" ? "true" : void 0,
144
+ "data-feedback-value": "positive",
145
+ disabled: status === "submitting",
146
+ onClick: () => handleSelect("positive"),
147
+ children: [/* @__PURE__ */ jsx(ThumbUpIcon, {}), /* @__PURE__ */ jsx("span", { children: positiveLabel })]
148
+ }), /* @__PURE__ */ jsxs("button", {
149
+ type: "button",
150
+ className: "fd-page-action-btn fd-feedback-choice",
151
+ "aria-pressed": selected === "negative",
152
+ "data-selected": selected === "negative" ? "true" : void 0,
153
+ "data-feedback-value": "negative",
154
+ disabled: status === "submitting",
155
+ onClick: () => handleSelect("negative"),
156
+ children: [/* @__PURE__ */ jsx(ThumbDownIcon, {}), /* @__PURE__ */ jsx("span", { children: negativeLabel })]
157
+ })]
158
+ })]
159
+ }), showForm && /* @__PURE__ */ jsxs("div", {
160
+ className: "fd-feedback-form",
161
+ children: [
162
+ /* @__PURE__ */ jsx("textarea", {
163
+ id: "fd-feedback-comment",
164
+ className: "fd-feedback-input",
165
+ "aria-label": "Additional feedback",
166
+ placeholder,
167
+ value: comment,
168
+ disabled: status === "submitting",
169
+ onChange: (event) => {
170
+ setComment(event.target.value);
171
+ if (status !== "idle") setStatus("idle");
172
+ }
173
+ }),
174
+ /* @__PURE__ */ jsxs("div", {
175
+ className: "fd-feedback-submit-row",
176
+ children: [/* @__PURE__ */ jsxs("button", {
177
+ type: "button",
178
+ className: "fd-page-action-btn fd-feedback-submit",
179
+ disabled: status === "submitting" || status === "submitted",
180
+ onClick: () => void handleSubmit(),
181
+ children: [
182
+ status === "submitting" && /* @__PURE__ */ jsx("span", {
183
+ className: "fd-feedback-spinner",
184
+ "aria-hidden": "true"
185
+ }),
186
+ status === "submitted" && /* @__PURE__ */ jsx(CheckIcon, {}),
187
+ /* @__PURE__ */ jsx("span", { children: status === "submitted" ? "Submitted" : submitLabel })
188
+ ]
189
+ }), status === "submitted" && /* @__PURE__ */ jsx("p", {
190
+ className: "fd-feedback-status",
191
+ "data-status": "success",
192
+ role: "status",
193
+ "aria-live": "polite",
194
+ children: "Thanks for the feedback."
195
+ })]
196
+ }),
197
+ status === "error" && /* @__PURE__ */ jsx("p", {
198
+ className: "fd-feedback-status",
199
+ "data-status": "error",
200
+ role: "status",
201
+ "aria-live": "polite",
202
+ children: "Could not send feedback. Please try again."
203
+ })
204
+ ]
205
+ })]
206
+ });
207
+ }
208
+
209
+ //#endregion
210
+ export { DocsFeedback };
@@ -466,6 +466,7 @@ function createDocsLayout(config, options) {
466
466
  const lastUpdatedEnabled = lastUpdatedRaw !== false && (typeof lastUpdatedRaw !== "object" || lastUpdatedRaw.enabled !== false);
467
467
  const lastUpdatedPosition = typeof lastUpdatedRaw === "object" ? lastUpdatedRaw.position ?? "footer" : "footer";
468
468
  const llmsTxtEnabled = resolveBool(config.llmsTxt);
469
+ const feedbackConfig = resolveFeedbackConfig(config.feedback);
469
470
  const openDocsProviders = (typeof pageActions?.openDocs === "object" && pageActions.openDocs.providers ? pageActions.openDocs.providers : void 0)?.map((p) => ({
470
471
  name: p.name,
471
472
  urlTemplate: p.urlTemplate,
@@ -589,6 +590,12 @@ function createDocsLayout(config, options) {
589
590
  lastUpdatedPosition,
590
591
  llmsTxtEnabled,
591
592
  descriptionMap,
593
+ feedbackEnabled: feedbackConfig.enabled,
594
+ feedbackQuestion: feedbackConfig.question,
595
+ feedbackPlaceholder: feedbackConfig.placeholder,
596
+ feedbackPositiveLabel: feedbackConfig.positiveLabel,
597
+ feedbackNegativeLabel: feedbackConfig.negativeLabel,
598
+ feedbackSubmitLabel: feedbackConfig.submitLabel,
592
599
  children
593
600
  })
594
601
  })
@@ -603,6 +610,29 @@ function resolveBool(v) {
603
610
  if (typeof v === "boolean") return v;
604
611
  return v.enabled !== false;
605
612
  }
613
+ function resolveFeedbackConfig(feedback) {
614
+ const defaults = {
615
+ enabled: false,
616
+ question: "How is this guide?",
617
+ placeholder: "Leave your feedback...",
618
+ positiveLabel: "Good",
619
+ negativeLabel: "Bad",
620
+ submitLabel: "Submit"
621
+ };
622
+ if (feedback === void 0 || feedback === false) return defaults;
623
+ if (feedback === true) return {
624
+ ...defaults,
625
+ enabled: true
626
+ };
627
+ return {
628
+ enabled: feedback.enabled !== false,
629
+ question: feedback.question ?? defaults.question,
630
+ placeholder: feedback.placeholder ?? defaults.placeholder,
631
+ positiveLabel: feedback.positiveLabel ?? defaults.positiveLabel,
632
+ negativeLabel: feedback.negativeLabel ?? defaults.negativeLabel,
633
+ submitLabel: feedback.submitLabel ?? defaults.submitLabel
634
+ };
635
+ }
606
636
  /**
607
637
  * Tiny inline script to force a theme when the toggle is hidden.
608
638
  * Sets the class on <html> before React hydrates to avoid FOUC.
@@ -1,4 +1,5 @@
1
1
  import { ReactNode } from "react";
2
+ import { DocsFeedbackData } from "@farming-labs/docs";
2
3
  import * as react_jsx_runtime0 from "react/jsx-runtime";
3
4
 
4
5
  //#region src/docs-page-client.d.ts
@@ -47,6 +48,14 @@ interface DocsPageClientProps {
47
48
  descriptionMap?: Record<string, string>;
48
49
  /** Frontmatter description to display below the page title (overrides descriptionMap) */
49
50
  description?: string;
51
+ /** Built-in page feedback prompt configuration */
52
+ feedbackEnabled?: boolean;
53
+ feedbackQuestion?: string;
54
+ feedbackPlaceholder?: string;
55
+ feedbackPositiveLabel?: string;
56
+ feedbackNegativeLabel?: string;
57
+ feedbackSubmitLabel?: string;
58
+ feedbackOnFeedback?: (data: DocsFeedbackData) => void | Promise<void>;
50
59
  children: ReactNode;
51
60
  }
52
61
  declare function DocsPageClient({
@@ -72,6 +81,13 @@ declare function DocsPageClient({
72
81
  llmsTxtEnabled,
73
82
  descriptionMap,
74
83
  description,
84
+ feedbackEnabled,
85
+ feedbackQuestion,
86
+ feedbackPlaceholder,
87
+ feedbackPositiveLabel,
88
+ feedbackNegativeLabel,
89
+ feedbackSubmitLabel,
90
+ feedbackOnFeedback,
75
91
  children
76
92
  }: DocsPageClientProps): react_jsx_runtime0.JSX.Element;
77
93
  //#endregion
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { PageActions } from "./page-actions.mjs";
4
4
  import { useWindowSearchParams } from "./client-location.mjs";
5
+ import { DocsFeedback } from "./docs-feedback.mjs";
5
6
  import { resolveClientLocale, withLangInUrl } from "./i18n.mjs";
6
7
  import { Children, cloneElement, isValidElement, useEffect, useState } from "react";
7
8
  import { DocsBody, DocsPage, EditOnGitHub } from "fumadocs-ui/layouts/docs/page";
@@ -119,7 +120,7 @@ function injectTitleDecorations(node, { description, belowTitle }) {
119
120
  });
120
121
  return visit(node);
121
122
  }
122
- function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled = true, entry = "docs", locale, copyMarkdown = false, openDocs = false, openDocsProviders, pageActionsPosition = "below-title", pageActionsAlignment = "left", githubUrl, contentDir, githubBranch = "main", githubDirectory, editOnGithubUrl, lastModifiedMap, lastModified: lastModifiedProp, lastUpdatedEnabled = true, lastUpdatedPosition = "footer", llmsTxtEnabled = false, descriptionMap, description, children }) {
123
+ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled = true, entry = "docs", locale, copyMarkdown = false, openDocs = false, openDocsProviders, pageActionsPosition = "below-title", pageActionsAlignment = "left", githubUrl, contentDir, githubBranch = "main", githubDirectory, editOnGithubUrl, lastModifiedMap, lastModified: lastModifiedProp, lastUpdatedEnabled = true, lastUpdatedPosition = "footer", llmsTxtEnabled = false, descriptionMap, description, feedbackEnabled = false, feedbackQuestion, feedbackPlaceholder, feedbackPositiveLabel, feedbackNegativeLabel, feedbackSubmitLabel, feedbackOnFeedback, children }) {
123
124
  const fdTocStyle = tocStyle === "directional" ? "clerk" : void 0;
124
125
  const [toc, setToc] = useState([]);
125
126
  const pathname = usePathname();
@@ -219,35 +220,49 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
219
220
  display: "flex",
220
221
  flexDirection: "column"
221
222
  },
222
- children: [/* @__PURE__ */ jsx("div", {
223
- style: { flex: 1 },
224
- children: decoratedChildren
225
- }), showFooter && /* @__PURE__ */ jsxs("div", {
226
- className: "not-prose fd-page-footer",
227
- children: [
228
- githubFileUrl && /* @__PURE__ */ jsx(EditOnGitHub, { href: githubFileUrl }),
229
- llmsTxtEnabled && /* @__PURE__ */ jsxs("span", {
230
- className: "fd-llms-txt-links",
231
- children: [/* @__PURE__ */ jsx("a", {
232
- href: `/api/docs?format=llms${llmsLangParam}`,
233
- target: "_blank",
234
- rel: "noopener noreferrer",
235
- className: "fd-llms-txt-link",
236
- children: "llms.txt"
237
- }), /* @__PURE__ */ jsx("a", {
238
- href: `/api/docs?format=llms-full${llmsLangParam}`,
239
- target: "_blank",
240
- rel: "noopener noreferrer",
241
- className: "fd-llms-txt-link",
242
- children: "llms-full.txt"
243
- })]
244
- }),
245
- showLastUpdatedInFooter && lastModified && /* @__PURE__ */ jsxs("span", {
246
- className: "fd-last-updated-footer",
247
- children: ["Last updated ", lastModified]
248
- })
249
- ]
250
- })]
223
+ children: [
224
+ /* @__PURE__ */ jsx("div", {
225
+ style: { flex: 1 },
226
+ children: decoratedChildren
227
+ }),
228
+ feedbackEnabled && /* @__PURE__ */ jsx(DocsFeedback, {
229
+ pathname: normalizedPath,
230
+ entry,
231
+ locale: activeLocale,
232
+ question: feedbackQuestion,
233
+ placeholder: feedbackPlaceholder,
234
+ positiveLabel: feedbackPositiveLabel,
235
+ negativeLabel: feedbackNegativeLabel,
236
+ submitLabel: feedbackSubmitLabel,
237
+ onFeedback: feedbackOnFeedback
238
+ }),
239
+ showFooter && /* @__PURE__ */ jsxs("div", {
240
+ className: "not-prose fd-page-footer",
241
+ children: [
242
+ githubFileUrl && /* @__PURE__ */ jsx(EditOnGitHub, { href: githubFileUrl }),
243
+ llmsTxtEnabled && /* @__PURE__ */ jsxs("span", {
244
+ className: "fd-llms-txt-links",
245
+ children: [/* @__PURE__ */ jsx("a", {
246
+ href: `/api/docs?format=llms${llmsLangParam}`,
247
+ target: "_blank",
248
+ rel: "noopener noreferrer",
249
+ className: "fd-llms-txt-link",
250
+ children: "llms.txt"
251
+ }), /* @__PURE__ */ jsx("a", {
252
+ href: `/api/docs?format=llms-full${llmsLangParam}`,
253
+ target: "_blank",
254
+ rel: "noopener noreferrer",
255
+ className: "fd-llms-txt-link",
256
+ children: "llms-full.txt"
257
+ })]
258
+ }),
259
+ showLastUpdatedInFooter && lastModified && /* @__PURE__ */ jsxs("span", {
260
+ className: "fd-last-updated-footer",
261
+ children: ["Last updated ", lastModified]
262
+ })
263
+ ]
264
+ })
265
+ ]
251
266
  })
252
267
  ]
253
268
  });
package/dist/index.d.mts CHANGED
@@ -3,12 +3,14 @@ import { DocsCommandSearch } from "./docs-command-search.mjs";
3
3
  import { createDocsLayout, createDocsMetadata, createPageMetadata } from "./docs-layout.mjs";
4
4
  import { DocsPageClient } from "./docs-page-client.mjs";
5
5
  import { RootProvider } from "./provider.mjs";
6
+ import { DocsClientHooks } from "./docs-client-hooks.mjs";
7
+ import { DocsFeedback, DocsFeedbackProps } from "./docs-feedback.mjs";
6
8
  import { PageActions } from "./page-actions.mjs";
7
9
  import { withLangInUrl } from "./i18n.mjs";
8
10
  import { HoverLink, HoverLinkProps } from "./hover-link.mjs";
9
11
  import { DocsLayout } from "fumadocs-ui/layouts/docs";
10
- import { AIConfig, BreadcrumbConfig, CopyMarkdownConfig, DocsConfig, DocsMetadata, DocsNav, DocsTheme, FontStyle, OGConfig, OpenDocsConfig, OpenDocsProvider, PageActionsConfig, PageFrontmatter, SidebarConfig, ThemeToggleConfig, TypographyConfig, UIConfig, createTheme, deepMerge, defineDocs, extendTheme } from "@farming-labs/docs";
12
+ import { AIConfig, BreadcrumbConfig, CopyMarkdownConfig, DocsConfig, DocsFeedbackData, DocsFeedbackValue, DocsMetadata, DocsNav, DocsTheme, FeedbackConfig, FontStyle, OGConfig, OpenDocsConfig, OpenDocsProvider, PageActionsConfig, PageFrontmatter, SidebarConfig, ThemeToggleConfig, TypographyConfig, UIConfig, createTheme, deepMerge, defineDocs, extendTheme } from "@farming-labs/docs";
11
13
  import { DocsBody, DocsPage } from "fumadocs-ui/layouts/docs/page";
12
14
  import { Tab, Tabs } from "fumadocs-ui/components/tabs";
13
15
  import { CodeBlock, CodeBlockTab, CodeBlockTabs, CodeBlockTabsList, CodeBlockTabsTrigger, Pre } from "fumadocs-ui/components/codeblock";
14
- export { type AIConfig, type BreadcrumbConfig, CodeBlock, CodeBlockTab, CodeBlockTabs, CodeBlockTabsList, CodeBlockTabsTrigger, type CopyMarkdownConfig, DocsBody, DocsCommandSearch, type DocsConfig, DocsLayout, type DocsMetadata, type DocsNav, DocsPage, DocsPageClient, type DocsTheme, type FontStyle, DefaultUIDefaults as FumadocsUIDefaults, HoverLink, type HoverLinkProps, type OGConfig, type OpenDocsConfig, type OpenDocsProvider, PageActions, type PageActionsConfig, type PageFrontmatter, Pre, RootProvider, type SidebarConfig, Tab, Tabs, type ThemeToggleConfig, type TypographyConfig, type UIConfig, createDocsLayout, createDocsMetadata, createPageMetadata, createTheme, deepMerge, defineDocs, extendTheme, fumadocs, withLangInUrl };
16
+ export { type AIConfig, type BreadcrumbConfig, CodeBlock, CodeBlockTab, CodeBlockTabs, CodeBlockTabsList, CodeBlockTabsTrigger, type CopyMarkdownConfig, DocsBody, DocsClientHooks, DocsCommandSearch, type DocsConfig, DocsFeedback, type DocsFeedbackData, type DocsFeedbackProps, type DocsFeedbackValue, DocsLayout, type DocsMetadata, type DocsNav, DocsPage, DocsPageClient, type DocsTheme, type FeedbackConfig, type FontStyle, DefaultUIDefaults as FumadocsUIDefaults, HoverLink, type HoverLinkProps, type OGConfig, type OpenDocsConfig, type OpenDocsProvider, PageActions, type PageActionsConfig, type PageFrontmatter, Pre, RootProvider, type SidebarConfig, Tab, Tabs, type ThemeToggleConfig, type TypographyConfig, type UIConfig, createDocsLayout, createDocsMetadata, createPageMetadata, createTheme, deepMerge, defineDocs, extendTheme, fumadocs, withLangInUrl };
package/dist/index.mjs CHANGED
@@ -1,10 +1,12 @@
1
1
  import { PageActions } from "./page-actions.mjs";
2
+ import { DocsFeedback } from "./docs-feedback.mjs";
2
3
  import { withLangInUrl } from "./i18n.mjs";
3
4
  import { DocsPageClient } from "./docs-page-client.mjs";
4
5
  import { DocsCommandSearch } from "./docs-command-search.mjs";
5
6
  import { createDocsLayout, createDocsMetadata, createPageMetadata } from "./docs-layout.mjs";
6
7
  import { RootProvider } from "./provider.mjs";
7
8
  import { DefaultUIDefaults, fumadocs } from "./default/index.mjs";
9
+ import { DocsClientHooks } from "./docs-client-hooks.mjs";
8
10
  import { HoverLink } from "./hover-link.mjs";
9
11
  import { DocsLayout } from "fumadocs-ui/layouts/docs";
10
12
  import { createTheme, deepMerge, defineDocs, extendTheme } from "@farming-labs/docs";
@@ -12,4 +14,4 @@ import { DocsBody, DocsPage } from "fumadocs-ui/layouts/docs/page";
12
14
  import { Tab, Tabs } from "fumadocs-ui/components/tabs";
13
15
  import { CodeBlock, CodeBlockTab, CodeBlockTabs, CodeBlockTabsList, CodeBlockTabsTrigger, Pre } from "fumadocs-ui/components/codeblock";
14
16
 
15
- export { CodeBlock, CodeBlockTab, CodeBlockTabs, CodeBlockTabsList, CodeBlockTabsTrigger, DocsBody, DocsCommandSearch, DocsLayout, DocsPage, DocsPageClient, DefaultUIDefaults as FumadocsUIDefaults, HoverLink, PageActions, Pre, RootProvider, Tab, Tabs, createDocsLayout, createDocsMetadata, createPageMetadata, createTheme, deepMerge, defineDocs, extendTheme, fumadocs, withLangInUrl };
17
+ export { CodeBlock, CodeBlockTab, CodeBlockTabs, CodeBlockTabsList, CodeBlockTabsTrigger, DocsBody, DocsClientHooks, DocsCommandSearch, DocsFeedback, DocsLayout, DocsPage, DocsPageClient, DefaultUIDefaults as FumadocsUIDefaults, HoverLink, PageActions, Pre, RootProvider, Tab, Tabs, createDocsLayout, createDocsMetadata, createPageMetadata, createTheme, deepMerge, defineDocs, extendTheme, fumadocs, withLangInUrl };
@@ -178,6 +178,29 @@ function resolveBool(value) {
178
178
  if (typeof value === "boolean") return value;
179
179
  return value.enabled !== false;
180
180
  }
181
+ function resolveFeedbackConfig(feedback) {
182
+ const defaults = {
183
+ enabled: false,
184
+ question: "How is this guide?",
185
+ placeholder: "Leave your feedback...",
186
+ positiveLabel: "Good",
187
+ negativeLabel: "Bad",
188
+ submitLabel: "Submit"
189
+ };
190
+ if (feedback === void 0 || feedback === false) return defaults;
191
+ if (feedback === true) return {
192
+ ...defaults,
193
+ enabled: true
194
+ };
195
+ return {
196
+ enabled: feedback.enabled !== false,
197
+ question: feedback.question ?? defaults.question,
198
+ placeholder: feedback.placeholder ?? defaults.placeholder,
199
+ positiveLabel: feedback.positiveLabel ?? defaults.positiveLabel,
200
+ negativeLabel: feedback.negativeLabel ?? defaults.negativeLabel,
201
+ submitLabel: feedback.submitLabel ?? defaults.submitLabel
202
+ };
203
+ }
181
204
  function ForcedThemeScript({ theme }) {
182
205
  return /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: `document.documentElement.classList.remove('light','dark');document.documentElement.classList.add('${theme === "light" || theme === "dark" ? theme : "light"}');` } });
183
206
  }
@@ -209,6 +232,7 @@ function TanstackDocsLayout({ config, tree, locale, description, lastModified, e
209
232
  const lastUpdatedEnabled = lastUpdatedRaw !== false && (typeof lastUpdatedRaw !== "object" || lastUpdatedRaw.enabled !== false);
210
233
  const lastUpdatedPosition = typeof lastUpdatedRaw === "object" ? lastUpdatedRaw.position ?? "footer" : "footer";
211
234
  const llmsTxtEnabled = resolveBool(config.llmsTxt);
235
+ const feedbackConfig = resolveFeedbackConfig(config.feedback);
212
236
  const staticExport = !!config.staticExport;
213
237
  const openDocsProviders = (typeof pageActions?.openDocs === "object" && pageActions.openDocs.providers ? pageActions.openDocs.providers : void 0)?.map((provider) => ({
214
238
  name: provider.name,
@@ -316,6 +340,12 @@ function TanstackDocsLayout({ config, tree, locale, description, lastModified, e
316
340
  lastModified,
317
341
  llmsTxtEnabled,
318
342
  description,
343
+ feedbackEnabled: feedbackConfig.enabled,
344
+ feedbackQuestion: feedbackConfig.question,
345
+ feedbackPlaceholder: feedbackConfig.placeholder,
346
+ feedbackPositiveLabel: feedbackConfig.positiveLabel,
347
+ feedbackNegativeLabel: feedbackConfig.negativeLabel,
348
+ feedbackSubmitLabel: feedbackConfig.submitLabel,
319
349
  children
320
350
  })
321
351
  })
@@ -1,3 +1,4 @@
1
+ import { DocsClientHooks } from "./docs-client-hooks.mjs";
1
2
  import { RootProvider } from "./provider-tanstack.mjs";
2
3
  import { TanstackDocsLayout } from "./tanstack-layout.mjs";
3
- export { RootProvider, TanstackDocsLayout };
4
+ export { DocsClientHooks, RootProvider, TanstackDocsLayout };
package/dist/tanstack.mjs CHANGED
@@ -1,4 +1,5 @@
1
+ import { DocsClientHooks } from "./docs-client-hooks.mjs";
1
2
  import { RootProvider } from "./provider-tanstack.mjs";
2
3
  import { TanstackDocsLayout } from "./tanstack-layout.mjs";
3
4
 
4
- export { RootProvider, TanstackDocsLayout };
5
+ export { DocsClientHooks, RootProvider, TanstackDocsLayout };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/theme",
3
- "version": "0.0.51",
3
+ "version": "0.0.53",
4
4
  "description": "Theme package for @farming-labs/docs — layout, provider, MDX components, and styles",
5
5
  "keywords": [
6
6
  "docs",
@@ -28,6 +28,11 @@
28
28
  "import": "./dist/mdx.mjs",
29
29
  "default": "./dist/mdx.mjs"
30
30
  },
31
+ "./client-hooks": {
32
+ "types": "./dist/docs-client-hooks.d.mts",
33
+ "import": "./dist/docs-client-hooks.mjs",
34
+ "default": "./dist/docs-client-hooks.mjs"
35
+ },
31
36
  "./default": {
32
37
  "types": "./dist/default/index.d.mts",
33
38
  "import": "./dist/default/index.mjs",
@@ -110,7 +115,7 @@
110
115
  "tsdown": "^0.20.3",
111
116
  "typescript": "^5.9.3",
112
117
  "vitest": "^3.2.4",
113
- "@farming-labs/docs": "0.0.51"
118
+ "@farming-labs/docs": "0.0.53"
114
119
  },
115
120
  "peerDependencies": {
116
121
  "@farming-labs/docs": ">=0.0.1",
package/styles/base.css CHANGED
@@ -307,11 +307,20 @@ figure.shiki:has(figcaption) figcaption {
307
307
  color: var(--color-fd-foreground);
308
308
  }
309
309
 
310
+ .fd-page-action-btn[data-selected="true"] {
311
+ color: var(--color-fd-accent-foreground);
312
+ background: var(--color-fd-accent);
313
+ }
314
+
310
315
  .fd-page-action-btn svg {
311
316
  flex-shrink: 0;
312
317
  color: var(--color-fd-muted-foreground);
313
318
  }
314
319
 
320
+ .fd-page-action-btn[data-selected="true"] svg {
321
+ color: currentColor;
322
+ }
323
+
315
324
  /* ─── Open dropdown ─────────────────────────────────────────────────── */
316
325
 
317
326
  .fd-page-action-dropdown {
@@ -388,6 +397,125 @@ figure.shiki:has(figcaption) figcaption {
388
397
  flex: 1;
389
398
  }
390
399
 
400
+ /* ─── Docs Feedback ────────────────────────────────────────────────── */
401
+
402
+ .fd-feedback {
403
+ margin-top: 2rem;
404
+ margin-bottom: 1.25rem;
405
+ padding-top: 1.25rem;
406
+ border-top: 1px solid var(--color-fd-border, hsl(0 0% 80% / 50%));
407
+ }
408
+
409
+ .fd-feedback-content {
410
+ display: flex;
411
+ align-items: center;
412
+ justify-content: space-between;
413
+ gap: 1rem;
414
+ flex-wrap: wrap;
415
+ }
416
+
417
+ .fd-feedback-question {
418
+ margin: 0;
419
+ font-size: 0.9375rem;
420
+ font-weight: 600;
421
+ line-height: 1.5;
422
+ color: var(--color-fd-foreground);
423
+ }
424
+
425
+ .fd-feedback-actions {
426
+ display: inline-flex;
427
+ align-items: center;
428
+ gap: 0.5rem;
429
+ flex-wrap: wrap;
430
+ }
431
+
432
+ .fd-feedback-form {
433
+ display: flex;
434
+ flex-direction: column;
435
+ gap: 0.75rem;
436
+ margin-top: 0.875rem;
437
+ }
438
+
439
+ .fd-feedback-choice[data-selected="true"] {
440
+ background: var(--color-fd-accent, hsl(0 0% 96%));
441
+ color: var(--color-fd-foreground);
442
+ border-color: color-mix(in srgb, var(--color-fd-primary, currentColor) 65%, transparent);
443
+ }
444
+
445
+ .fd-feedback-input {
446
+ width: 100%;
447
+ min-height: 4.75rem;
448
+ resize: vertical;
449
+ padding: 0.875rem 1rem;
450
+ border: 1px solid var(--color-fd-border, hsl(0 0% 80% / 50%));
451
+ background: var(--color-fd-card, transparent);
452
+ color: var(--color-fd-foreground);
453
+ font: inherit;
454
+ line-height: 1.55;
455
+ outline: none;
456
+ transition:
457
+ border-color 150ms ease,
458
+ box-shadow 150ms ease,
459
+ background-color 150ms ease;
460
+ }
461
+
462
+ .fd-feedback-input::placeholder {
463
+ color: var(--color-fd-muted-foreground, hsl(0 0% 45%));
464
+ }
465
+
466
+ .fd-feedback-input:focus {
467
+ border-color: var(--color-fd-ring, var(--color-fd-primary, currentColor));
468
+ box-shadow: 0 0 0 1px var(--color-fd-ring, var(--color-fd-primary, currentColor));
469
+ }
470
+
471
+ .fd-feedback-submit-row {
472
+ display: flex;
473
+ align-items: center;
474
+ gap: 0.75rem;
475
+ flex-wrap: wrap;
476
+ }
477
+
478
+ .fd-feedback-submit {
479
+ min-width: 7rem;
480
+ justify-content: center;
481
+ }
482
+
483
+ .fd-feedback-submit:disabled {
484
+ cursor: not-allowed;
485
+ opacity: 0.65;
486
+ }
487
+
488
+ .fd-feedback-spinner {
489
+ width: 0.875rem;
490
+ height: 0.875rem;
491
+ border-radius: 9999px;
492
+ border: 2px solid currentColor;
493
+ border-right-color: transparent;
494
+ animation: fd-feedback-spin 0.8s linear infinite;
495
+ }
496
+
497
+ .fd-feedback-status {
498
+ margin: 0;
499
+ font-size: 0.8125rem;
500
+ color: var(--color-fd-muted-foreground, hsl(0 0% 45%));
501
+ }
502
+
503
+ .fd-feedback-status[data-status="success"] {
504
+ display: inline-flex;
505
+ align-items: center;
506
+ gap: 0.4rem;
507
+ }
508
+
509
+ .fd-feedback-status[data-status="error"] {
510
+ color: var(--color-fd-foreground);
511
+ }
512
+
513
+ @keyframes fd-feedback-spin {
514
+ to {
515
+ transform: rotate(360deg);
516
+ }
517
+ }
518
+
391
519
  /* ─── Page Footer (Edit on GitHub + Last Updated) ──────────────────── */
392
520
 
393
521
  .fd-page-footer {
@@ -413,6 +541,11 @@ figure.shiki:has(figcaption) figcaption {
413
541
  }
414
542
 
415
543
  @media (max-width: 640px) {
544
+ .fd-feedback-content {
545
+ flex-direction: column;
546
+ align-items: flex-start;
547
+ }
548
+
416
549
  .fd-last-updated-footer {
417
550
  margin-left: 0;
418
551
  width: 100%;
@@ -382,3 +382,18 @@
382
382
  .omni-search-input:focus {
383
383
  caret-color: var(--color-fd-primary);
384
384
  }
385
+
386
+ /* ─── Feedback (colorful theme) ──────────────────────────────────── */
387
+
388
+ .fd-feedback-input,
389
+ .fd-feedback-submit {
390
+ border-radius: 0.5rem;
391
+ }
392
+
393
+ .fd-feedback-choice[data-selected="true"] {
394
+ background: color-mix(in srgb, var(--color-fd-primary) 12%, var(--color-fd-secondary));
395
+ }
396
+
397
+ .fd-feedback-status[data-status="success"] {
398
+ color: var(--color-fd-primary);
399
+ }
@@ -573,3 +573,21 @@ details > :not(summary) {
573
573
  .fd-sidebar::-webkit-scrollbar-track {
574
574
  background: transparent;
575
575
  }
576
+
577
+ /* ─── Feedback (darkbold theme) ──────────────────────────────────── */
578
+
579
+ .fd-feedback-input,
580
+ .fd-feedback-submit {
581
+ border-radius: 6px;
582
+ }
583
+
584
+ .fd-feedback-submit,
585
+ .fd-feedback-choice {
586
+ text-transform: uppercase;
587
+ font-family: var(--fd-font-mono, var(--font-geist-mono, ui-monospace, monospace));
588
+ }
589
+
590
+ .fd-feedback-status[data-status="success"] {
591
+ color: var(--color-fd-foreground);
592
+ font-weight: 600;
593
+ }
@@ -464,3 +464,22 @@ article a[class*="text-fd-muted-foreground"] {
464
464
  .omni-highlight {
465
465
  background: color-mix(in srgb, var(--color-fd-primary) 30%, transparent);
466
466
  }
467
+
468
+ /* ─── Feedback (darksharp theme) ─────────────────────────────────── */
469
+
470
+ .fd-feedback-input,
471
+ .fd-feedback-submit {
472
+ border-radius: 0.2rem;
473
+ }
474
+
475
+ .fd-feedback-input {
476
+ background: hsl(0 0% 4%);
477
+ }
478
+
479
+ .fd-feedback-choice[data-selected="true"] {
480
+ background: hsl(0 0% 8%);
481
+ }
482
+
483
+ .fd-feedback-status[data-status="success"] {
484
+ color: hsl(0 0% 90%);
485
+ }
@@ -231,3 +231,14 @@
231
231
  .omni-highlight {
232
232
  background: color-mix(in srgb, var(--color-fd-primary, #6366f1) 25%, transparent);
233
233
  }
234
+
235
+ /* ─── Feedback (default theme) ───────────────────────────────────── */
236
+
237
+ .fd-feedback-input,
238
+ .fd-feedback-submit {
239
+ border-radius: 0.5rem;
240
+ }
241
+
242
+ .fd-feedback-status[data-status="success"] {
243
+ color: color-mix(in srgb, var(--color-fd-primary) 85%, var(--color-fd-foreground));
244
+ }
@@ -760,4 +760,19 @@ details > :not(summary) {
760
760
  padding: 6px 10px;
761
761
  border: none;
762
762
  border-radius: var(--radius, 8px) !important;
763
- }
763
+ }
764
+
765
+ /* ─── Feedback (greentree theme) ─────────────────────────────────── */
766
+
767
+ .fd-feedback-input,
768
+ .fd-feedback-submit {
769
+ border-radius: 0.75rem;
770
+ }
771
+
772
+ .fd-feedback-choice[data-selected="true"] {
773
+ background: color-mix(in srgb, var(--color-fd-primary) 12%, var(--color-fd-secondary));
774
+ }
775
+
776
+ .fd-feedback-status[data-status="success"] {
777
+ color: var(--color-fd-primary);
778
+ }
@@ -885,3 +885,20 @@ hr {
885
885
  background: color-mix(in srgb, var(--color-fd-primary, #6366f1) 25%, transparent);
886
886
  border-radius: 0 !important;
887
887
  }
888
+
889
+ /* ─── Feedback (pixel-border theme) ──────────────────────────────── */
890
+
891
+ .fd-feedback-input,
892
+ .fd-feedback-submit {
893
+ border-radius: 0 !important;
894
+ box-shadow: 3px 3px 0 0 var(--color-fd-border);
895
+ }
896
+
897
+ .fd-feedback-choice[data-selected="true"] {
898
+ background: var(--color-fd-secondary);
899
+ }
900
+
901
+ .fd-feedback-status[data-status="success"] {
902
+ font-family: var(--fd-font-mono, var(--font-geist-mono, ui-monospace, monospace));
903
+ text-transform: uppercase;
904
+ }
package/styles/shiny.css CHANGED
@@ -624,3 +624,27 @@ details > :not(summary) {
624
624
  .fd-sidebar::-webkit-scrollbar-track {
625
625
  background: transparent;
626
626
  }
627
+
628
+ /* ─── Feedback (shiny theme) ─────────────────────────────────────── */
629
+
630
+ .fd-feedback-input,
631
+ .fd-feedback-submit {
632
+ border-radius: 0.75rem;
633
+ }
634
+
635
+ .fd-feedback-input {
636
+ background: color-mix(in srgb, var(--color-fd-card) 92%, transparent);
637
+ box-shadow:
638
+ inset 0 1px 0 rgba(255, 255, 255, 0.06),
639
+ 0 1px 2px rgba(0, 0, 0, 0.04);
640
+ }
641
+
642
+ .dark .fd-feedback-input {
643
+ box-shadow:
644
+ inset 0 1px 0 rgba(255, 255, 255, 0.04),
645
+ 0 6px 18px rgba(0, 0, 0, 0.18);
646
+ }
647
+
648
+ .fd-feedback-status[data-status="success"] {
649
+ color: var(--color-fd-primary);
650
+ }