@farming-labs/theme 0.1.60 → 0.1.62

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.
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { emitClientAnalyticsEvent } from "./client-analytics.mjs";
3
4
  import { useWindowSearchParams } from "./client-location.mjs";
4
5
  import { resolveClientLocale, withLangInUrl } from "./i18n.mjs";
5
6
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -302,7 +303,7 @@ function labelForType(type) {
302
303
  * fuzzy-search experience. Styled entirely via omni-* CSS classes
303
304
  * so each theme provides its own visual variant.
304
305
  */
305
- function DocsCommandSearch({ api = "/api/docs", locale }) {
306
+ function DocsCommandSearch({ api = "/api/docs", locale, analytics = false }) {
306
307
  const [open, setOpen] = useState(false);
307
308
  const [query, setQuery] = useState("");
308
309
  const [debouncedQuery, setDebouncedQuery] = useState("");
@@ -315,6 +316,18 @@ function DocsCommandSearch({ api = "/api/docs", locale }) {
315
316
  const searchApi = useMemo(() => withLangInUrl(api, activeLocale), [activeLocale, api]);
316
317
  const inputRef = useRef(null);
317
318
  const listRef = useRef(null);
319
+ const setOpenWithAnalytics = useCallback((nextOpen, trigger) => {
320
+ setOpen(nextOpen);
321
+ if (!analytics) return;
322
+ emitClientAnalyticsEvent({
323
+ type: nextOpen ? "search_open" : "search_close",
324
+ locale: activeLocale,
325
+ properties: {
326
+ trigger,
327
+ mode: "command"
328
+ }
329
+ });
330
+ }, [activeLocale, analytics]);
318
331
  useEffect(() => {
319
332
  setMounted(true);
320
333
  try {
@@ -332,12 +345,12 @@ function DocsCommandSearch({ api = "/api/docs", locale }) {
332
345
  e.preventDefault();
333
346
  e.stopPropagation();
334
347
  e.stopImmediatePropagation();
335
- setOpen(true);
348
+ setOpenWithAnalytics(true, "keyboard");
336
349
  }
337
350
  }
338
351
  document.addEventListener("keydown", handler, true);
339
352
  return () => document.removeEventListener("keydown", handler, true);
340
- }, []);
353
+ }, [setOpenWithAnalytics]);
341
354
  useEffect(() => {
342
355
  function handler(e) {
343
356
  const button = e.target.closest("button");
@@ -348,12 +361,12 @@ function DocsCommandSearch({ api = "/api/docs", locale }) {
348
361
  e.preventDefault();
349
362
  e.stopPropagation();
350
363
  e.stopImmediatePropagation();
351
- setOpen(true);
364
+ setOpenWithAnalytics(true, "button");
352
365
  }
353
366
  }
354
367
  document.addEventListener("click", handler, true);
355
368
  return () => document.removeEventListener("click", handler, true);
356
- }, []);
369
+ }, [setOpenWithAnalytics]);
357
370
  useEffect(() => {
358
371
  if (!debouncedQuery.trim()) {
359
372
  setResults([]);
@@ -363,6 +376,7 @@ function DocsCommandSearch({ api = "/api/docs", locale }) {
363
376
  let cancelled = false;
364
377
  setLoading(true);
365
378
  (async () => {
379
+ const startedAt = Date.now();
366
380
  try {
367
381
  const requestUrl = new URL(searchApi, window.location.origin);
368
382
  requestUrl.searchParams.set("query", debouncedQuery);
@@ -385,8 +399,28 @@ function DocsCommandSearch({ api = "/api/docs", locale }) {
385
399
  if (!cancelled) {
386
400
  setResults(items);
387
401
  setActiveIndex(0);
402
+ if (analytics) emitClientAnalyticsEvent({
403
+ type: "search_query",
404
+ locale: activeLocale,
405
+ properties: {
406
+ mode: "command",
407
+ queryLength: debouncedQuery.length,
408
+ resultCount: items.length,
409
+ durationMs: Math.max(0, Date.now() - startedAt)
410
+ }
411
+ });
388
412
  }
389
- } catch {}
413
+ } catch {
414
+ if (!cancelled && analytics) emitClientAnalyticsEvent({
415
+ type: "search_error",
416
+ locale: activeLocale,
417
+ properties: {
418
+ mode: "command",
419
+ queryLength: debouncedQuery.length,
420
+ durationMs: Math.max(0, Date.now() - startedAt)
421
+ }
422
+ });
423
+ }
390
424
  if (!cancelled) setLoading(false);
391
425
  })();
392
426
  return () => {
@@ -394,6 +428,7 @@ function DocsCommandSearch({ api = "/api/docs", locale }) {
394
428
  };
395
429
  }, [
396
430
  activeLocale,
431
+ analytics,
397
432
  debouncedQuery,
398
433
  searchApi
399
434
  ]);
@@ -424,11 +459,11 @@ function DocsCommandSearch({ api = "/api/docs", locale }) {
424
459
  useEffect(() => {
425
460
  if (!open) return;
426
461
  function handler(e) {
427
- if (e.key === "Escape") setOpen(false);
462
+ if (e.key === "Escape") setOpenWithAnalytics(false, "escape");
428
463
  }
429
464
  document.addEventListener("keydown", handler);
430
465
  return () => document.removeEventListener("keydown", handler);
431
- }, [open]);
466
+ }, [open, setOpenWithAnalytics]);
432
467
  const saveRecent = useCallback((item) => {
433
468
  try {
434
469
  const entry = {
@@ -442,11 +477,28 @@ function DocsCommandSearch({ api = "/api/docs", locale }) {
442
477
  } catch {}
443
478
  }, [recents]);
444
479
  const execute = useCallback((item) => {
480
+ if (analytics) emitClientAnalyticsEvent({
481
+ type: "search_result_click",
482
+ locale: activeLocale,
483
+ path: item.url,
484
+ properties: {
485
+ mode: "command",
486
+ resultId: item.id,
487
+ resultUrl: item.url,
488
+ labelLength: item.label.length,
489
+ queryLength: query.length
490
+ }
491
+ });
445
492
  saveRecent(item);
446
493
  setOpen(false);
447
494
  if (item.url.startsWith("http")) window.open(item.url, "_blank", "noopener");
448
495
  else window.location.href = item.url;
449
- }, [saveRecent]);
496
+ }, [
497
+ activeLocale,
498
+ analytics,
499
+ query.length,
500
+ saveRecent
501
+ ]);
450
502
  const displayItems = useMemo(() => {
451
503
  if (results.length > 0) return results;
452
504
  return [];
@@ -494,7 +546,7 @@ function DocsCommandSearch({ api = "/api/docs", locale }) {
494
546
  const allItems = [...recentItems, ...displayItems];
495
547
  return createPortal(/* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
496
548
  className: "omni-overlay",
497
- onClick: () => setOpen(false)
549
+ onClick: () => setOpenWithAnalytics(false, "overlay")
498
550
  }), /* @__PURE__ */ jsxs("div", {
499
551
  className: "omni-content",
500
552
  role: "dialog",
@@ -528,7 +580,7 @@ function DocsCommandSearch({ api = "/api/docs", locale }) {
528
580
  /* @__PURE__ */ jsx("button", {
529
581
  "aria-label": "Close",
530
582
  className: "omni-close-btn",
531
- onClick: () => setOpen(false),
583
+ onClick: () => setOpenWithAnalytics(false, "button"),
532
584
  children: /* @__PURE__ */ jsx(CloseIcon, {})
533
585
  })
534
586
  ]
@@ -12,6 +12,7 @@ interface DocsFeedbackProps {
12
12
  negativeLabel?: string;
13
13
  submitLabel?: string;
14
14
  onFeedback?: (data: DocsFeedbackData) => void | Promise<void>;
15
+ analytics?: boolean;
15
16
  }
16
17
  declare function DocsFeedback({
17
18
  pathname,
@@ -22,7 +23,8 @@ declare function DocsFeedback({
22
23
  positiveLabel,
23
24
  negativeLabel,
24
25
  submitLabel,
25
- onFeedback
26
+ onFeedback,
27
+ analytics
26
28
  }: DocsFeedbackProps): react_jsx_runtime0.JSX.Element;
27
29
  //#endregion
28
30
  export { DocsFeedback, DocsFeedbackProps };
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { emitClientAnalyticsEvent } from "./client-analytics.mjs";
3
4
  import { useEffect, useMemo, useState } from "react";
4
5
  import { jsx, jsxs } from "react/jsx-runtime";
5
6
 
@@ -99,7 +100,7 @@ function CheckIcon() {
99
100
  })
100
101
  });
101
102
  }
102
- function DocsFeedback({ pathname, entry, locale, question = "How is this guide?", placeholder = "Leave your feedback...", positiveLabel = "Good", negativeLabel = "Bad", submitLabel = "Submit", onFeedback }) {
103
+ function DocsFeedback({ pathname, entry, locale, question = "How is this guide?", placeholder = "Leave your feedback...", positiveLabel = "Good", negativeLabel = "Bad", submitLabel = "Submit", onFeedback, analytics = false }) {
103
104
  const [selected, setSelected] = useState(null);
104
105
  const [comment, setComment] = useState("");
105
106
  const [status, setStatus] = useState("idle");
@@ -114,14 +115,46 @@ function DocsFeedback({ pathname, entry, locale, question = "How is this guide?"
114
115
  function handleSelect(value) {
115
116
  setSelected(value);
116
117
  if (status !== "idle") setStatus("idle");
118
+ if (analytics) emitClientAnalyticsEvent({
119
+ type: "feedback_select",
120
+ locale,
121
+ path: normalizedPathname,
122
+ properties: {
123
+ value,
124
+ slug: resolveSlug(entry, normalizedPathname)
125
+ }
126
+ });
117
127
  }
118
128
  async function handleSubmit() {
119
129
  if (!selected || status === "submitting") return;
120
130
  setStatus("submitting");
121
131
  try {
122
- await emitFeedback(buildFeedbackPayload(selected, normalizedPathname, entry, comment, locale), onFeedback);
132
+ const payload = buildFeedbackPayload(selected, normalizedPathname, entry, comment, locale);
133
+ await emitFeedback(payload, onFeedback);
134
+ if (analytics) emitClientAnalyticsEvent({
135
+ type: "feedback_submit",
136
+ locale,
137
+ path: normalizedPathname,
138
+ properties: {
139
+ value: payload.value,
140
+ slug: payload.slug,
141
+ hasComment: Boolean(payload.comment),
142
+ commentLength: payload.comment?.length ?? 0
143
+ }
144
+ });
123
145
  setStatus("submitted");
124
146
  } catch {
147
+ if (analytics) emitClientAnalyticsEvent({
148
+ type: "feedback_error",
149
+ locale,
150
+ path: normalizedPathname,
151
+ properties: {
152
+ value: selected,
153
+ slug: resolveSlug(entry, normalizedPathname),
154
+ hasComment: Boolean(comment.trim()),
155
+ commentLength: comment.trim().length
156
+ }
157
+ });
125
158
  setStatus("error");
126
159
  }
127
160
  }
@@ -11,7 +11,7 @@ import path from "node:path";
11
11
  import matter from "gray-matter";
12
12
  import { DocsLayout } from "fumadocs-ui/layouts/docs";
13
13
  import { Suspense } from "react";
14
- import { applySidebarFolderIndexBehavior, buildPageOpenGraph, buildPageTwitter, resolveChangelogConfig, resolveDocsAgentMdxContent, resolvePageSidebarFolderIndexBehavior } from "@farming-labs/docs";
14
+ import { applySidebarFolderIndexBehavior, buildPageOpenGraph, buildPageTwitter, resolveChangelogConfig, resolveDocsAgentMdxContent, resolveDocsAnalyticsConfig, resolvePageSidebarFolderIndexBehavior } from "@farming-labs/docs";
15
15
  import { jsx, jsxs } from "react/jsx-runtime";
16
16
 
17
17
  //#region src/docs-layout.tsx
@@ -554,6 +554,7 @@ function createDocsLayout(config, options) {
554
554
  const tocConfig = config.theme?.ui?.layout?.toc;
555
555
  const tocEnabled = tocConfig?.enabled !== false;
556
556
  const tocStyle = tocConfig?.style;
557
+ const analyticsEnabled = resolveDocsAnalyticsConfig(config.analytics).enabled;
557
558
  const localeContext = resolveDocsLocaleContext(config, options?.locale);
558
559
  const i18n = resolveDocsI18nConfig(getDocsI18n(config));
559
560
  const activeLocale = localeContext.locale ?? i18n?.defaultLocale;
@@ -673,7 +674,8 @@ function createDocsLayout(config, options) {
673
674
  fallback: null,
674
675
  children: /* @__PURE__ */ jsx(DocsCommandSearch, {
675
676
  api: docsApiUrl,
676
- locale: activeLocale
677
+ locale: activeLocale,
678
+ analytics: analyticsEnabled
677
679
  })
678
680
  }),
679
681
  aiEnabled && /* @__PURE__ */ jsx(Suspense, {
@@ -690,7 +692,8 @@ function createDocsLayout(config, options) {
690
692
  loaderVariant: aiLoaderVariant,
691
693
  loadingComponentHtml: aiLoadingComponentHtml,
692
694
  models: aiModels,
693
- defaultModelId: aiDefaultModelId
695
+ defaultModelId: aiDefaultModelId,
696
+ analytics: analyticsEnabled
694
697
  })
695
698
  }),
696
699
  /* @__PURE__ */ jsx(Suspense, {
@@ -724,6 +727,7 @@ function createDocsLayout(config, options) {
724
727
  feedbackPositiveLabel: feedbackConfig.positiveLabel,
725
728
  feedbackNegativeLabel: feedbackConfig.negativeLabel,
726
729
  feedbackSubmitLabel: feedbackConfig.submitLabel,
730
+ analytics: analyticsEnabled,
727
731
  children
728
732
  })
729
733
  })
@@ -66,6 +66,7 @@ interface DocsPageClientProps {
66
66
  feedbackNegativeLabel?: string;
67
67
  feedbackSubmitLabel?: string;
68
68
  feedbackOnFeedback?: (data: DocsFeedbackData) => void | Promise<void>;
69
+ analytics?: boolean;
69
70
  children: ReactNode;
70
71
  }
71
72
  declare function DocsPageClient({
@@ -102,6 +103,7 @@ declare function DocsPageClient({
102
103
  feedbackNegativeLabel,
103
104
  feedbackSubmitLabel,
104
105
  feedbackOnFeedback,
106
+ analytics,
105
107
  children
106
108
  }: DocsPageClientProps): react_jsx_runtime0.JSX.Element;
107
109
  //#endregion
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { emitClientAnalyticsEvent } from "./client-analytics.mjs";
3
4
  import { PageActions } from "./page-actions.mjs";
4
5
  import { useWindowSearchParams } from "./client-location.mjs";
5
6
  import { DocsFeedback } from "./docs-feedback.mjs";
@@ -162,7 +163,7 @@ function TitleDecorations({ description, belowTitle }) {
162
163
  if (!description && !belowTitle) return null;
163
164
  return /* @__PURE__ */ jsx(Fragment$1, { children: Children.toArray([description, belowTitle].filter(Boolean)) });
164
165
  }
165
- function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled = true, changelogBasePath, entry = "docs", locale, copyMarkdown = false, openDocs = false, openDocsProviders, pageActionsPosition = "below-title", pageActionsAlignment = "left", githubUrl, contentDir, githubBranch = "main", githubDirectory, editOnGithubUrl, lastModifiedMap, lastModified: lastModifiedProp, readingTimeMap, readingTime: readingTimeProp, readingTimeEnabled = false, lastUpdatedEnabled = true, lastUpdatedPosition = "footer", llmsTxtEnabled = false, descriptionMap, description, feedbackEnabled = false, feedbackQuestion, feedbackPlaceholder, feedbackPositiveLabel, feedbackNegativeLabel, feedbackSubmitLabel, feedbackOnFeedback, children }) {
166
+ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled = true, changelogBasePath, entry = "docs", locale, copyMarkdown = false, openDocs = false, openDocsProviders, pageActionsPosition = "below-title", pageActionsAlignment = "left", githubUrl, contentDir, githubBranch = "main", githubDirectory, editOnGithubUrl, lastModifiedMap, lastModified: lastModifiedProp, readingTimeMap, readingTime: readingTimeProp, readingTimeEnabled = false, lastUpdatedEnabled = true, lastUpdatedPosition = "footer", llmsTxtEnabled = false, descriptionMap, description, feedbackEnabled = false, feedbackQuestion, feedbackPlaceholder, feedbackPositiveLabel, feedbackNegativeLabel, feedbackSubmitLabel, feedbackOnFeedback, analytics = false, children }) {
166
167
  const fdTocStyle = tocStyle === "directional" ? "clerk" : void 0;
167
168
  const [toc, setToc] = useState([]);
168
169
  const [titlePortalHost, setTitlePortalHost] = useState(null);
@@ -174,6 +175,25 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
174
175
  const normalizedPath = (browserPath ?? pathname).replace(/\/$/, "") || "/";
175
176
  const isChangelogRoute = !!(changelogBasePath && (normalizedPath === changelogBasePath || normalizedPath.startsWith(`${changelogBasePath}/`)));
176
177
  const matchedReadingTime = readingTimeMap?.[normalizedPath];
178
+ useEffect(() => {
179
+ if (!analytics) return;
180
+ emitClientAnalyticsEvent({
181
+ type: "page_view",
182
+ locale: activeLocale,
183
+ path: normalizedPath,
184
+ properties: {
185
+ entry,
186
+ pathname: normalizedPath,
187
+ isChangelogRoute
188
+ }
189
+ });
190
+ }, [
191
+ analytics,
192
+ activeLocale,
193
+ entry,
194
+ isChangelogRoute,
195
+ normalizedPath
196
+ ]);
177
197
  const resolvedReadingTime = !isChangelogRoute ? readingTimeProp !== void 0 ? readingTimeProp : readingTimeEnabled ? matchedReadingTime : void 0 : void 0;
178
198
  const effectiveTocEnabled = isChangelogRoute ? false : tocEnabled;
179
199
  const effectiveBreadcrumbEnabled = isChangelogRoute ? false : breadcrumbEnabled;
@@ -285,7 +305,8 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
285
305
  openDocs,
286
306
  providers: openDocsProviders,
287
307
  alignment: pageActionsAlignment,
288
- githubFileUrl
308
+ githubFileUrl,
309
+ analytics
289
310
  })
290
311
  }),
291
312
  showReadingTimeBelowTitle && readingTimeBlock
@@ -349,7 +370,8 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
349
370
  openDocs,
350
371
  providers: openDocsProviders,
351
372
  alignment: pageActionsAlignment,
352
- githubFileUrl
373
+ githubFileUrl,
374
+ analytics
353
375
  })
354
376
  }), readingTimeBlock]
355
377
  }),
@@ -374,7 +396,8 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
374
396
  positiveLabel: feedbackPositiveLabel,
375
397
  negativeLabel: feedbackNegativeLabel,
376
398
  submitLabel: feedbackSubmitLabel,
377
- onFeedback: feedbackOnFeedback
399
+ onFeedback: feedbackOnFeedback,
400
+ analytics
378
401
  }),
379
402
  showFooter && /* @__PURE__ */ jsxs("div", {
380
403
  className: "not-prose fd-page-footer",
@@ -14,13 +14,15 @@ interface PageActionsProps {
14
14
  alignment?: "left" | "right";
15
15
  /** GitHub file URL (edit view) for the current page. Used when urlTemplate contains {githubUrl}. */
16
16
  githubFileUrl?: string | null;
17
+ analytics?: boolean;
17
18
  }
18
19
  declare function PageActions({
19
20
  copyMarkdown,
20
21
  openDocs,
21
22
  providers,
22
23
  alignment,
23
- githubFileUrl
24
+ githubFileUrl,
25
+ analytics
24
26
  }: PageActionsProps): react_jsx_runtime0.JSX.Element | null;
25
27
  //#endregion
26
28
  export { PageActions };
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { emitClientAnalyticsEvent } from "./client-analytics.mjs";
3
4
  import { useCallback, useEffect, useRef, useState } from "react";
4
5
  import { usePathname } from "fumadocs-core/framework";
5
6
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -72,7 +73,7 @@ const DEFAULT_PROVIDERS = [{
72
73
  name: "Claude",
73
74
  urlTemplate: "https://claude.ai/new?q=Read+{mdxUrl},+I+want+to+ask+questions+about+it."
74
75
  }];
75
- function PageActions({ copyMarkdown, openDocs, providers, alignment = "left", githubFileUrl }) {
76
+ function PageActions({ copyMarkdown, openDocs, providers, alignment = "left", githubFileUrl, analytics = false }) {
76
77
  const [copied, setCopied] = useState(false);
77
78
  const [dropdownOpen, setDropdownOpen] = useState(false);
78
79
  const dropdownRef = useRef(null);
@@ -82,13 +83,22 @@ function PageActions({ copyMarkdown, openDocs, providers, alignment = "left", gi
82
83
  try {
83
84
  const article = document.querySelector("article");
84
85
  if (article) {
85
- await navigator.clipboard.writeText(article.innerText || "");
86
+ const content = article.innerText || "";
87
+ await navigator.clipboard.writeText(content);
88
+ if (analytics) emitClientAnalyticsEvent({
89
+ type: "page_action_copy_markdown",
90
+ properties: {
91
+ contentLength: content.length,
92
+ pathname
93
+ }
94
+ });
86
95
  setCopied(true);
87
96
  setTimeout(() => setCopied(false), 2e3);
88
97
  }
89
98
  } catch {}
90
- }, []);
91
- const handleOpen = useCallback((template) => {
99
+ }, [analytics, pathname]);
100
+ const handleOpen = useCallback((provider) => {
101
+ const template = provider.urlTemplate;
92
102
  if (/\{githubUrl\}/.test(template) && !githubFileUrl) {
93
103
  setDropdownOpen(false);
94
104
  return;
@@ -96,9 +106,21 @@ function PageActions({ copyMarkdown, openDocs, providers, alignment = "left", gi
96
106
  const pageUrl = window.location.href;
97
107
  const mdxUrl = `${window.location.origin}${pathname}.mdx`;
98
108
  let url = template.replace(/\{url\}/g, encodeURIComponent(pageUrl)).replace(/\{mdxUrl\}/g, encodeURIComponent(mdxUrl)).replace(/\{githubUrl\}/g, githubFileUrl ?? "");
109
+ if (analytics) emitClientAnalyticsEvent({
110
+ type: "page_action_open_docs",
111
+ properties: {
112
+ provider: provider.name,
113
+ pathname,
114
+ usesGithubUrl: template.includes("{githubUrl}")
115
+ }
116
+ });
99
117
  window.open(url, "_blank", "noopener,noreferrer");
100
118
  setDropdownOpen(false);
101
- }, [pathname, githubFileUrl]);
119
+ }, [
120
+ analytics,
121
+ pathname,
122
+ githubFileUrl
123
+ ]);
102
124
  useEffect(() => {
103
125
  if (!dropdownOpen) return;
104
126
  function handleClick(e) {
@@ -127,7 +149,17 @@ function PageActions({ copyMarkdown, openDocs, providers, alignment = "left", gi
127
149
  className: "fd-page-action-dropdown",
128
150
  children: [/* @__PURE__ */ jsxs("button", {
129
151
  type: "button",
130
- onClick: () => setDropdownOpen(!dropdownOpen),
152
+ onClick: () => {
153
+ const next = !dropdownOpen;
154
+ setDropdownOpen(next);
155
+ if (analytics && next) emitClientAnalyticsEvent({
156
+ type: "page_action_open_docs_menu",
157
+ properties: {
158
+ providerCount: resolvedProviders.length,
159
+ pathname
160
+ }
161
+ });
162
+ },
131
163
  className: "fd-page-action-btn",
132
164
  "aria-expanded": dropdownOpen,
133
165
  children: [/* @__PURE__ */ jsx("span", { children: "Open in" }), /* @__PURE__ */ jsx(ChevronDownIcon, {})]
@@ -138,7 +170,7 @@ function PageActions({ copyMarkdown, openDocs, providers, alignment = "left", gi
138
170
  type: "button",
139
171
  role: "menuitem",
140
172
  className: "fd-page-action-menu-item",
141
- onClick: () => handleOpen(provider.urlTemplate),
173
+ onClick: () => handleOpen(provider),
142
174
  children: [
143
175
  provider.iconHtml && /* @__PURE__ */ jsx("span", {
144
176
  className: "fd-page-action-menu-icon",
@@ -7,7 +7,7 @@ import { SidebarSearchWithAI } from "./sidebar-search-ai.mjs";
7
7
  import { LocaleThemeControl } from "./locale-theme-control.mjs";
8
8
  import { DocsLayout } from "fumadocs-ui/layouts/docs";
9
9
  import { Suspense } from "react";
10
- import { applySidebarFolderIndexBehavior } from "@farming-labs/docs";
10
+ import { applySidebarFolderIndexBehavior, resolveDocsAnalyticsConfig } from "@farming-labs/docs";
11
11
  import { jsx, jsxs } from "react/jsx-runtime";
12
12
 
13
13
  //#region src/tanstack-layout.tsx
@@ -211,6 +211,7 @@ function TanstackDocsLayout({ config, tree, locale, description, readingTime, la
211
211
  const tocConfig = config.theme?.ui?.layout?.toc;
212
212
  const tocEnabled = tocConfig?.enabled !== false;
213
213
  const tocStyle = tocConfig?.style;
214
+ const analyticsEnabled = resolveDocsAnalyticsConfig(config.analytics).enabled;
214
215
  const docsApiUrl = withLangInUrl("/api/docs", locale);
215
216
  const navTitle = config.nav?.title ?? "Docs";
216
217
  const navUrl = withLangInUrl(config.nav?.url ?? `/${config.entry ?? "docs"}`, locale);
@@ -310,7 +311,8 @@ function TanstackDocsLayout({ config, tree, locale, description, readingTime, la
310
311
  fallback: null,
311
312
  children: /* @__PURE__ */ jsx(DocsCommandSearch, {
312
313
  api: docsApiUrl,
313
- locale
314
+ locale,
315
+ analytics: analyticsEnabled
314
316
  })
315
317
  }),
316
318
  aiEnabled && /* @__PURE__ */ jsx(Suspense, {
@@ -325,7 +327,8 @@ function TanstackDocsLayout({ config, tree, locale, description, readingTime, la
325
327
  aiLabel,
326
328
  loaderVariant: aiLoaderVariant,
327
329
  models: aiModels,
328
- defaultModelId: aiDefaultModelId
330
+ defaultModelId: aiDefaultModelId,
331
+ analytics: analyticsEnabled
329
332
  })
330
333
  }),
331
334
  /* @__PURE__ */ jsx(Suspense, {
@@ -355,6 +358,7 @@ function TanstackDocsLayout({ config, tree, locale, description, readingTime, la
355
358
  feedbackPositiveLabel: feedbackConfig.positiveLabel,
356
359
  feedbackNegativeLabel: feedbackConfig.negativeLabel,
357
360
  feedbackSubmitLabel: feedbackConfig.submitLabel,
361
+ analytics: analyticsEnabled,
358
362
  children
359
363
  })
360
364
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/theme",
3
- "version": "0.1.60",
3
+ "version": "0.1.62",
4
4
  "description": "Theme package for @farming-labs/docs — layout, provider, MDX components, and styles",
5
5
  "keywords": [
6
6
  "docs",
@@ -133,7 +133,7 @@
133
133
  "tsdown": "^0.20.3",
134
134
  "typescript": "^5.9.3",
135
135
  "vitest": "^3.2.4",
136
- "@farming-labs/docs": "0.1.60"
136
+ "@farming-labs/docs": "0.1.62"
137
137
  },
138
138
  "peerDependencies": {
139
139
  "@farming-labs/docs": ">=0.0.1",
package/styles/base.css CHANGED
@@ -1399,8 +1399,10 @@ html[data-fd-route-kind="changelog"] .fd-ai-fm-input-bar {
1399
1399
  .fd-page-nav-card {
1400
1400
  display: flex;
1401
1401
  min-width: 0;
1402
+ min-height: 8.75rem;
1402
1403
  flex-direction: column;
1403
- gap: 0.25rem;
1404
+ justify-content: flex-start;
1405
+ gap: 0.375rem;
1404
1406
  padding: 1rem;
1405
1407
  border: 1px solid var(--color-fd-border, hsl(0 0% 80% / 50%));
1406
1408
  border-radius: 0.5rem;
@@ -1434,10 +1436,32 @@ html[data-fd-route-kind="changelog"] .fd-ai-fm-input-bar {
1434
1436
  }
1435
1437
 
1436
1438
  .fd-page-nav-title {
1439
+ display: -webkit-box;
1440
+ overflow: hidden;
1441
+ min-height: calc(1.4em * 2);
1437
1442
  font-size: 0.875rem;
1438
1443
  font-weight: 600;
1439
1444
  line-height: 1.4;
1440
1445
  color: var(--color-fd-foreground);
1446
+ overflow-wrap: anywhere;
1447
+ -webkit-box-orient: vertical;
1448
+ -webkit-line-clamp: 2;
1449
+ }
1450
+
1451
+ .fd-page-nav-description {
1452
+ display: -webkit-box;
1453
+ overflow: hidden;
1454
+ min-height: calc(1.5em * 2);
1455
+ -webkit-box-orient: vertical;
1456
+ -webkit-line-clamp: 2;
1457
+ font-size: 0.875rem;
1458
+ line-height: 1.5;
1459
+ color: var(--color-fd-muted-foreground, hsl(0 0% 45%));
1460
+ overflow-wrap: anywhere;
1461
+ }
1462
+
1463
+ .fd-page-nav-description-empty {
1464
+ visibility: hidden;
1441
1465
  }
1442
1466
 
1443
1467
  @media (max-width: 640px) {