@farming-labs/theme 0.2.97 → 0.2.99

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,25 @@
1
+ import { TanstackDocsLayout, TanstackDocsLayoutProps } from "./tanstack-layout.mjs";
2
+ import { ComponentPropsWithoutRef } from "react";
3
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
4
+ import { RootProvider } from "fumadocs-ui/provider/base";
5
+
6
+ //#region src/browser.d.ts
7
+ type FumadocsProviderProps = ComponentPropsWithoutRef<typeof RootProvider>;
8
+ declare global {
9
+ interface Window {
10
+ __fdBrowserHistoryPatched?: boolean;
11
+ }
12
+ }
13
+ interface BrowserRootProviderProps extends FumadocsProviderProps {
14
+ /** Path rendered by the server, used to keep hydration deterministic. */
15
+ initialPathname?: string;
16
+ }
17
+ /** Framework-neutral provider for server-rendered React documentation adapters. */
18
+ declare function BrowserRootProvider({
19
+ children,
20
+ search,
21
+ initialPathname,
22
+ ...props
23
+ }: BrowserRootProviderProps): react_jsx_runtime0.JSX.Element;
24
+ //#endregion
25
+ export { TanstackDocsLayout as BrowserDocsLayout, type TanstackDocsLayoutProps as BrowserDocsLayoutProps, BrowserRootProvider, BrowserRootProviderProps };
@@ -0,0 +1,74 @@
1
+ "use client";
2
+
3
+ import { TanstackDocsLayout } from "./tanstack-layout.mjs";
4
+ import { useMemo, useSyncExternalStore } from "react";
5
+ import { FrameworkProvider } from "fumadocs-core/framework";
6
+ import { jsx } from "react/jsx-runtime";
7
+ import { RootProvider } from "fumadocs-ui/provider/base";
8
+
9
+ //#region src/browser.tsx
10
+ function patchHistoryEvents() {
11
+ if (typeof window === "undefined" || window.__fdBrowserHistoryPatched) return;
12
+ for (const method of ["pushState", "replaceState"]) {
13
+ const original = window.history[method];
14
+ window.history[method] = function patchedHistoryMethod(...args) {
15
+ const result = original.apply(this, args);
16
+ window.dispatchEvent(new Event("fd-location-change"));
17
+ return result;
18
+ };
19
+ }
20
+ window.__fdBrowserHistoryPatched = true;
21
+ }
22
+ function subscribeToLocation(onStoreChange) {
23
+ if (typeof window === "undefined") return () => {};
24
+ patchHistoryEvents();
25
+ window.addEventListener("popstate", onStoreChange);
26
+ window.addEventListener("fd-location-change", onStoreChange);
27
+ return () => {
28
+ window.removeEventListener("popstate", onStoreChange);
29
+ window.removeEventListener("fd-location-change", onStoreChange);
30
+ };
31
+ }
32
+ function getBrowserPathname() {
33
+ return typeof window === "undefined" ? "/" : window.location.pathname;
34
+ }
35
+ function useBrowserRouter() {
36
+ return useMemo(() => ({
37
+ push(url) {
38
+ window.location.assign(url);
39
+ },
40
+ refresh() {
41
+ window.location.reload();
42
+ }
43
+ }), []);
44
+ }
45
+ function useBrowserParams() {
46
+ return useMemo(() => ({}), []);
47
+ }
48
+ function BrowserLink({ prefetch: _prefetch, ...props }) {
49
+ return /* @__PURE__ */ jsx("a", { ...props });
50
+ }
51
+ /** Framework-neutral provider for server-rendered React documentation adapters. */
52
+ function BrowserRootProvider({ children, search, initialPathname = "/", ...props }) {
53
+ const useBrowserPathname = () => useSyncExternalStore(subscribeToLocation, getBrowserPathname, () => initialPathname);
54
+ return /* @__PURE__ */ jsx(FrameworkProvider, {
55
+ Link: BrowserLink,
56
+ usePathname: useBrowserPathname,
57
+ useParams: useBrowserParams,
58
+ useRouter: useBrowserRouter,
59
+ children: /* @__PURE__ */ jsx(RootProvider, {
60
+ search: {
61
+ ...search,
62
+ options: {
63
+ api: "/api/docs",
64
+ ...search?.options
65
+ }
66
+ },
67
+ ...props,
68
+ children
69
+ })
70
+ });
71
+ }
72
+
73
+ //#endregion
74
+ export { TanstackDocsLayout as BrowserDocsLayout, BrowserRootProvider };
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { emitClientAnalyticsEvent } from "./client-analytics.mjs";
4
4
  import { useEffect } from "react";
5
- import { emitDocsAnalyticsEvent, resolveDocsAnalyticsConfig } from "@farming-labs/docs";
5
+ import { emitDocsAnalyticsEvent, resolveDocsAnalyticsConfig } from "@farming-labs/docs/browser";
6
6
 
7
7
  //#region src/docs-client-hooks.tsx
8
8
  function useWindowHook(key, handler) {
@@ -256,6 +256,7 @@ function DocsCommandSearch({ api = "/api/docs", locale, analytics = false }) {
256
256
  const searchApi = useMemo(() => withLangInUrl(api, activeLocale), [activeLocale, api]);
257
257
  const inputRef = useRef(null);
258
258
  const listRef = useRef(null);
259
+ const restoreFocusRef = useRef(null);
259
260
  const searchCacheRef = useRef(/* @__PURE__ */ new Map());
260
261
  const setOpenWithAnalytics = useCallback((nextOpen, trigger) => {
261
262
  setOpen(nextOpen);
@@ -404,13 +405,23 @@ function DocsCommandSearch({ api = "/api/docs", locale, analytics = false }) {
404
405
  searchApi
405
406
  ]);
406
407
  useEffect(() => {
407
- if (open) setTimeout(() => inputRef.current?.focus(), 10);
408
- else {
408
+ if (open) {
409
+ const activeElement = document.activeElement;
410
+ restoreFocusRef.current = activeElement instanceof HTMLElement ? activeElement : null;
411
+ const focusTimer = window.setTimeout(() => inputRef.current?.focus(), 10);
412
+ return () => window.clearTimeout(focusTimer);
413
+ } else {
409
414
  setQuery("");
410
415
  setResults([]);
411
416
  setFilter("all");
412
417
  setFilterOpen(false);
413
418
  setActiveIndex(0);
419
+ const restoreTarget = restoreFocusRef.current;
420
+ restoreFocusRef.current = null;
421
+ if (restoreTarget?.isConnected) {
422
+ const focusFrame = window.requestAnimationFrame(() => restoreTarget.focus({ preventScroll: true }));
423
+ return () => window.cancelAnimationFrame(focusFrame);
424
+ }
414
425
  }
415
426
  }, [open]);
416
427
  useEffect(() => {
@@ -1,12 +1,14 @@
1
1
  import { serializeIcon } from "./serialize-icon.mjs";
2
+ import { resolveOpenDocsProviders } from "./open-docs-providers.mjs";
2
3
  import { withLangInUrl } from "./i18n.mjs";
3
4
  import { DocsPageClient } from "./docs-page-client.mjs";
4
5
  import { DocsAIFeatures } from "./docs-ai-features.mjs";
5
6
  import { resolveDocsCloudAIClientRequest } from "./docs-cloud-ai-client.mjs";
6
7
  import { DocsCommandSearch } from "./docs-command-search.mjs";
7
- import { resolveOpenDocsProviders } from "./open-docs-providers.mjs";
8
- import { resolvePageReadingTime, resolveReadingTimeOptions } from "./reading-time.mjs";
8
+ import { resolveReadingTimeOptions } from "./reading-time-options.mjs";
9
+ import { resolvePageReadingTime } from "./reading-time.mjs";
9
10
  import { SidebarSearchWithAI } from "./sidebar-search-ai.mjs";
11
+ import { TabletSidebarBridge } from "./tablet-sidebar-bridge.mjs";
10
12
  import { LocaleThemeControl } from "./locale-theme-control.mjs";
11
13
  import fs from "node:fs";
12
14
  import path from "node:path";
@@ -35,6 +37,30 @@ function readFrontmatter(filePath) {
35
37
  return {};
36
38
  }
37
39
  }
40
+ function hasAuthoredPageTitle(source) {
41
+ const { content } = matter(source);
42
+ const lines = content.split(/\r?\n/);
43
+ let fence;
44
+ let previousTextLine = "";
45
+ for (const line of lines) {
46
+ const fenceMatch = line.match(/^\s*(`{3,}|~{3,})/);
47
+ if (fenceMatch) {
48
+ const marker = fenceMatch[1]?.[0];
49
+ fence = fence === marker ? void 0 : fence ?? marker;
50
+ previousTextLine = "";
51
+ continue;
52
+ }
53
+ if (fence) continue;
54
+ if (/^ {0,3}#(?:\s+|$)/.test(line) || /^\s*<h1(?:\s|>)/i.test(line)) return true;
55
+ if (previousTextLine && /^ {0,3}=+\s*$/.test(line)) return true;
56
+ previousTextLine = line.trim() && !line.trimStart().startsWith("<") ? line : "";
57
+ }
58
+ return false;
59
+ }
60
+ function resolveSidebarFolderName(themeName, slug, pageTitle) {
61
+ if (themeName !== "shadcn" || pageTitle.trim().toLowerCase() !== "introduction") return pageTitle;
62
+ return slug.split(/[-_]/).filter(Boolean).map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" ");
63
+ }
38
64
  /** Check if a directory has any subdirectories that contain page.mdx. */
39
65
  function isWithinDir(candidate, target) {
40
66
  const relative = path.relative(target, candidate);
@@ -194,12 +220,13 @@ function buildTree(config, ctx, flat = false) {
194
220
  const url = publicDocsRoute(ctx, slug);
195
221
  const icon = resolveIcon(data.icon, icons);
196
222
  const displayName = data.title ?? name.replace(/-/g, " ");
223
+ const folderName = resolveSidebarFolderName(config.theme?.name, name, displayName);
197
224
  const hasChildren = hasChildPages(full, excludedDirs);
198
225
  if (data.hidden === true) {
199
226
  if (!hasChildren) return null;
200
227
  return {
201
228
  type: "folder",
202
- name: displayName,
229
+ name: folderName,
203
230
  icon,
204
231
  children: scanDir(full, slug, slugOrder),
205
232
  ...flat ? {
@@ -212,7 +239,7 @@ function buildTree(config, ctx, flat = false) {
212
239
  const folderChildren = scanDir(full, slug, slugOrder);
213
240
  return {
214
241
  type: "folder",
215
- name: displayName,
242
+ name: folderName,
216
243
  icon,
217
244
  index: {
218
245
  type: "page",
@@ -380,6 +407,31 @@ function buildDescriptionMap(config, ctx) {
380
407
  scan(docsDir, []);
381
408
  return map;
382
409
  }
410
+ /**
411
+ * Build titles only for pages whose MDX body does not already author an h1.
412
+ * This lets the page frame render the frontmatter title exactly once.
413
+ */
414
+ function buildGeneratedTitleMap(config, ctx) {
415
+ const docsDir = ctx.docsDir;
416
+ const map = {};
417
+ const excludedDirs = getExcludedDocsDirs(config, ctx);
418
+ function scan(dir, slugParts) {
419
+ if (!fs.existsSync(dir)) return;
420
+ if (isExcludedDir(dir, excludedDirs)) return;
421
+ const pagePath = findDocsPageFile(dir);
422
+ if (pagePath) {
423
+ const source = fs.readFileSync(pagePath, "utf-8");
424
+ const { data } = matter(source);
425
+ if (typeof data.title === "string" && !hasAuthoredPageTitle(source)) map[publicDocsRoute(ctx, slugParts)] = data.title;
426
+ }
427
+ for (const name of fs.readdirSync(dir)) {
428
+ const full = path.join(dir, name);
429
+ if (fs.statSync(full).isDirectory()) scan(full, [...slugParts, name]);
430
+ }
431
+ }
432
+ scan(docsDir, []);
433
+ return map;
434
+ }
383
435
  function buildReadingTimeMap(config, ctx, options) {
384
436
  const docsDir = ctx.docsDir;
385
437
  const map = {};
@@ -702,6 +754,7 @@ function createDocsLayout(config, options) {
702
754
  }
703
755
  const lastModifiedMap = buildLastModifiedMap(config, localeContext);
704
756
  const descriptionMap = buildDescriptionMap(config, localeContext);
757
+ const generatedTitleMap = config.theme?.name === "shadcn" ? buildGeneratedTitleMap(config, localeContext) : {};
705
758
  const readingTimeMap = buildReadingTimeMap(config, localeContext, {
706
759
  enabledByDefault: readingTimeEnabledByDefault,
707
760
  wordsPerMinute: readingTimeWordsPerMinute,
@@ -756,6 +809,7 @@ function createDocsLayout(config, options) {
756
809
  /* @__PURE__ */ jsx(TypographyStyle, { typography }),
757
810
  /* @__PURE__ */ jsx(LayoutStyle, { layout: layoutDimensions }),
758
811
  forcedTheme && /* @__PURE__ */ jsx(ForcedThemeScript, { theme: forcedTheme }),
812
+ config.theme?.name === "fumadocs-pixel-border" && /* @__PURE__ */ jsx(TabletSidebarBridge, {}),
759
813
  !staticExport && /* @__PURE__ */ jsx(Suspense, {
760
814
  fallback: null,
761
815
  children: /* @__PURE__ */ jsx(DocsCommandSearch, {
@@ -790,6 +844,7 @@ function createDocsLayout(config, options) {
790
844
  fallback: children,
791
845
  children: /* @__PURE__ */ jsx(DocsPageClient, {
792
846
  tocEnabled,
847
+ themeName: config.theme?.name,
793
848
  tocStyle,
794
849
  breadcrumbEnabled,
795
850
  changelogBasePath,
@@ -820,6 +875,7 @@ function createDocsLayout(config, options) {
820
875
  readingTimeMap,
821
876
  structuredDataMap,
822
877
  llmsTxtEnabled,
878
+ generatedTitleMap,
823
879
  descriptionMap,
824
880
  feedbackEnabled: feedbackConfig.enabled,
825
881
  feedbackQuestion: feedbackConfig.question,
@@ -17,6 +17,8 @@ interface SerializedProvider {
17
17
  }
18
18
  interface DocsPageClientProps {
19
19
  tocEnabled: boolean;
20
+ /** Active theme preset name, used for theme-specific structural affordances. */
21
+ themeName?: string;
20
22
  tocStyle?: "default" | "directional";
21
23
  breadcrumbEnabled?: boolean;
22
24
  changelogBasePath?: string;
@@ -78,10 +80,14 @@ interface DocsPageClientProps {
78
80
  lastUpdatedPosition?: "footer" | "below-title";
79
81
  /** Whether llms.txt is enabled — shows links in footer */
80
82
  llmsTxtEnabled?: boolean;
83
+ /** Frontmatter titles for pages whose MDX body does not author an h1 */
84
+ generatedTitleMap?: Record<string, string>;
81
85
  /** Map of pathname → frontmatter description */
82
86
  descriptionMap?: Record<string, string>;
83
87
  /** Frontmatter description to display below the page title (overrides descriptionMap) */
84
88
  description?: string;
89
+ /** The first authored paragraph already contains the frontmatter description. */
90
+ descriptionInBody?: boolean;
85
91
  /** Built-in page feedback prompt configuration */
86
92
  feedbackEnabled?: boolean;
87
93
  feedbackQuestion?: string;
@@ -98,6 +104,7 @@ interface DocsPageClientProps {
98
104
  }
99
105
  declare function DocsPageClient({
100
106
  tocEnabled,
107
+ themeName,
101
108
  tocStyle,
102
109
  breadcrumbEnabled,
103
110
  changelogBasePath,
@@ -134,8 +141,10 @@ declare function DocsPageClient({
134
141
  lastUpdatedLabel,
135
142
  lastUpdatedPosition,
136
143
  llmsTxtEnabled,
144
+ generatedTitleMap,
137
145
  descriptionMap,
138
146
  description,
147
+ descriptionInBody,
139
148
  feedbackEnabled,
140
149
  feedbackQuestion,
141
150
  feedbackPlaceholder,
@@ -255,7 +255,7 @@ function findThreadlineTocActionsContainer() {
255
255
  }
256
256
  return toc.parentElement ?? toc;
257
257
  }
258
- function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled = true, changelogBasePath, entry = "docs", publicPath, locale, copyMarkdown = false, copyMarkdownFormat, copyMarkdownIncludeTitle, copyMarkdownLabel, copyMarkdownCopiedLabel, openDocs = false, openDocsProviders, openDocsTarget, openDocsPrompt, pageActionsPosition = "below-title", pageActionsAlignment = "left", githubUrl, contentDir, githubBranch = "main", githubDirectory, editOnGithubUrl, lastModifiedMap, lastModified: lastModifiedProp, readingTimeMap, readingTime: readingTimeProp, readingTimeFormat = "long", previousPage, nextPage, structuredDataMap, structuredData: structuredDataProp, readingTimeEnabled = false, lastUpdatedEnabled = true, lastUpdatedLabel = "Last updated", lastUpdatedPosition = "footer", llmsTxtEnabled = false, descriptionMap, description, feedbackEnabled = false, feedbackQuestion, feedbackPlaceholder, feedbackRequireComment, feedbackPositiveLabel, feedbackNegativeLabel, feedbackSubmitLabel, feedbackSuccessMessage, feedbackErrorMessage, feedbackOnFeedback, analytics = false, children }) {
258
+ function DocsPageClient({ tocEnabled, themeName, tocStyle = "default", breadcrumbEnabled = true, changelogBasePath, entry = "docs", publicPath, locale, copyMarkdown = false, copyMarkdownFormat, copyMarkdownIncludeTitle, copyMarkdownLabel, copyMarkdownCopiedLabel, openDocs = false, openDocsProviders, openDocsTarget, openDocsPrompt, pageActionsPosition = "below-title", pageActionsAlignment = "left", githubUrl, contentDir, githubBranch = "main", githubDirectory, editOnGithubUrl, lastModifiedMap, lastModified: lastModifiedProp, readingTimeMap, readingTime: readingTimeProp, readingTimeFormat = "long", previousPage, nextPage, structuredDataMap, structuredData: structuredDataProp, readingTimeEnabled = false, lastUpdatedEnabled = true, lastUpdatedLabel = "Last updated", lastUpdatedPosition = "footer", llmsTxtEnabled = false, generatedTitleMap, descriptionMap, description, descriptionInBody = false, feedbackEnabled = false, feedbackQuestion, feedbackPlaceholder, feedbackRequireComment, feedbackPositiveLabel, feedbackNegativeLabel, feedbackSubmitLabel, feedbackSuccessMessage, feedbackErrorMessage, feedbackOnFeedback, analytics = false, children }) {
259
259
  const fdTocStyle = tocStyle === "directional" ? "clerk" : void 0;
260
260
  const [toc, setToc] = useState([]);
261
261
  const [titlePortalHost, setTitlePortalHost] = useState(null);
@@ -268,8 +268,10 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
268
268
  const activeLocale = resolveClientLocale(searchParams, locale);
269
269
  const resolvedPublicPath = normalizePublicDocsPath(publicPath, entry);
270
270
  const llmsLangQuery = activeLocale ? `?lang=${encodeURIComponent(activeLocale)}` : "";
271
- const pageDescription = description ?? descriptionMap?.[pathname.replace(/\/$/, "") || "/"];
271
+ const showLlmsInHeader = llmsTxtEnabled && themeName === "fumadocs-pixel-border";
272
272
  const normalizedPath = (browserPathname || pathname).replace(/\/$/, "") || "/";
273
+ const pageTitle = generatedTitleMap?.[normalizedPath];
274
+ const pageDescription = description ?? descriptionMap?.[normalizedPath];
273
275
  const isChangelogRoute = !!(changelogBasePath && (normalizedPath === changelogBasePath || normalizedPath.startsWith(`${changelogBasePath}/`)));
274
276
  const matchedReadingTime = readingTimeMap?.[normalizedPath];
275
277
  const structuredDataJson = !isChangelogRoute ? structuredDataProp ?? structuredDataMap?.[normalizedPath] : void 0;
@@ -495,7 +497,7 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
495
497
  ]
496
498
  }, "below-title") : void 0;
497
499
  const decoratedChildren = children;
498
- const needsTitleDecorationsPortal = !!titleDescription || !!belowTitleBlock;
500
+ const needsTitleDecorationsPortal = !pageTitle && (!!titleDescription || !!belowTitleBlock);
499
501
  useEffect(() => {
500
502
  if (!needsTitleDecorationsPortal) {
501
503
  setTitlePortalHost(null);
@@ -508,19 +510,54 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
508
510
  }
509
511
  const host = document.createElement("div");
510
512
  host.className = "fd-title-decorations-host";
513
+ const placeHost = () => {
514
+ let anchor = title;
515
+ if (descriptionInBody) {
516
+ let sibling = title.nextElementSibling;
517
+ while (sibling) {
518
+ if (sibling === host || sibling.matches(".not-prose, .fd-title-decorations-host")) {
519
+ sibling = sibling.nextElementSibling;
520
+ continue;
521
+ }
522
+ if (sibling.matches("p")) anchor = sibling;
523
+ break;
524
+ }
525
+ }
526
+ if (anchor.nextElementSibling !== host) anchor.insertAdjacentElement("afterend", host);
527
+ };
511
528
  title.insertAdjacentElement("afterend", host);
529
+ placeHost();
530
+ const observer = new MutationObserver(placeHost);
531
+ observer.observe(title.parentElement ?? title, { childList: true });
532
+ const animationFrame = window.requestAnimationFrame(placeHost);
512
533
  setTitlePortalHost(host);
513
534
  return () => {
535
+ window.cancelAnimationFrame(animationFrame);
536
+ observer.disconnect();
514
537
  host.remove();
515
538
  setTitlePortalHost(null);
516
539
  };
517
- }, [needsTitleDecorationsPortal, pathname]);
540
+ }, [
541
+ descriptionInBody,
542
+ needsTitleDecorationsPortal,
543
+ pathname
544
+ ]);
518
545
  const titleDecorations = needsTitleDecorationsPortal ? /* @__PURE__ */ jsx(TitleDecorations, {
519
546
  description: titleDescription,
520
547
  belowTitle: belowTitleBlock
521
548
  }) : null;
522
549
  const titleDecorationsPortal = titleDecorations && titlePortalHost ? createPortal(titleDecorations, titlePortalHost, "title-decorations") : null;
523
550
  const titleDecorationsFallback = titleDecorations && !titlePortalHost ? titleDecorations : null;
551
+ const generatedPageHeader = pageTitle ? /* @__PURE__ */ jsxs("div", {
552
+ className: "fd-generated-page-header not-prose",
553
+ children: [/* @__PURE__ */ jsx("h1", {
554
+ className: "fd-page-title",
555
+ children: pageTitle
556
+ }), /* @__PURE__ */ jsx(TitleDecorations, {
557
+ description: titleDescription,
558
+ belowTitle: belowTitleBlock
559
+ })]
560
+ }) : null;
524
561
  const titleControlsPortal = showActionsInToc && titleControlsPortalHost ? createPortal(/* @__PURE__ */ jsx(ThreadlinePageControls, {}), titleControlsPortalHost, "title-controls") : null;
525
562
  const tocActionsPortal = showActionsInToc && tocActionsPortalHost ? createPortal(/* @__PURE__ */ jsx("div", {
526
563
  className: "fd-actions-toc-portal not-prose",
@@ -549,10 +586,11 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
549
586
  llmsTxtEnabled && /* @__PURE__ */ jsx("a", {
550
587
  href: `/llms.txt${llmsLangQuery}`,
551
588
  className: "fd-agent-llms-directive",
552
- style: agentLlmsDirectiveStyle,
553
- tabIndex: -1,
554
- "aria-hidden": "true",
555
- children: "llms.txt"
589
+ "data-visible-in-header": showLlmsInHeader ? "true" : void 0,
590
+ style: showLlmsInHeader ? void 0 : agentLlmsDirectiveStyle,
591
+ tabIndex: showLlmsInHeader ? void 0 : -1,
592
+ "aria-hidden": showLlmsInHeader ? void 0 : true,
593
+ children: showLlmsInHeader ? "LLMS.TXT" : "llms.txt"
556
594
  }, "llms-txt"),
557
595
  titleControlsPortal,
558
596
  tocActionsPortal,
@@ -568,7 +606,7 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
568
606
  style: fdTocStyle
569
607
  },
570
608
  breadcrumb: { enabled: false },
571
- footer: { enabled: !isChangelogRoute },
609
+ footer: { enabled: false },
572
610
  children: [
573
611
  effectiveBreadcrumbEnabled && /* @__PURE__ */ jsx(PathBreadcrumb, {
574
612
  pathname,
@@ -604,11 +642,12 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
604
642
  flexDirection: "column"
605
643
  },
606
644
  children: [
645
+ generatedPageHeader,
607
646
  /* @__PURE__ */ jsx("div", {
608
647
  style: { flex: 1 },
609
648
  children: renderedChildren
610
649
  }, "content"),
611
- titleDecorationsFallback,
650
+ !generatedPageHeader && titleDecorationsFallback,
612
651
  titleDecorationsPortal,
613
652
  !isChangelogRoute && feedbackEnabled && /* @__PURE__ */ jsx(DocsFeedback, {
614
653
  pathname: normalizedPath,
package/dist/mdx.mjs CHANGED
@@ -5,7 +5,7 @@ import { HoverLink } from "./hover-link.mjs";
5
5
  import { extractPromptText } from "./prompt-text.mjs";
6
6
  import { Prompt } from "./prompt.mjs";
7
7
  import React from "react";
8
- import { resolveDocsAudienceExposure } from "@farming-labs/docs";
8
+ import { resolveDocsAudienceExposure } from "@farming-labs/docs/browser";
9
9
  import { Tab, Tabs } from "fumadocs-ui/components/tabs";
10
10
  import { CodeBlockTab, CodeBlockTabs, CodeBlockTabsList, CodeBlockTabsTrigger } from "fumadocs-ui/components/codeblock";
11
11
  import defaultMdxComponents from "fumadocs-ui/mdx";
@@ -0,0 +1,14 @@
1
+ //#region src/open-docs-provider-icons.ts
2
+ /** Monochrome provider marks used by the built-in Open in… presets. */
3
+ const OPEN_DOCS_PROVIDER_ICONS = {
4
+ chatgpt: "<svg viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M11.248 18.25q-.825 0-1.568-.314a4.3 4.3 0 0 1-1.32-.874 4 4 0 0 1-1.304.214 4 4 0 0 1-2.046-.544 4.27 4.27 0 0 1-1.518-1.485 4 4 0 0 1-.56-2.095q0-.48.131-1.04A4.4 4.4 0 0 1 2.04 10.71a4.07 4.07 0 0 1 .017-3.4 4.2 4.2 0 0 1 1.056-1.418 3.8 3.8 0 0 1 1.6-.842 3.9 3.9 0 0 1 .76-1.683q.593-.759 1.451-1.188a4.04 4.04 0 0 1 1.832-.429q.825 0 1.567.313.742.314 1.32.875a4 4 0 0 1 1.304-.215q1.106 0 2.046.545a4.14 4.14 0 0 1 1.501 1.485q.578.941.578 2.095 0 .48-.132 1.04.66.61 1.023 1.419.363.792.363 1.666 0 .892-.38 1.717a4.3 4.3 0 0 1-1.072 1.435 3.8 3.8 0 0 1-1.584.825 3.8 3.8 0 0 1-.775 1.683 4.06 4.06 0 0 1-1.436 1.188 4.04 4.04 0 0 1-1.832.429m-4.076-2.062q.825 0 1.435-.347l3.103-1.782a.36.36 0 0 0 .164-.313v-1.42L7.881 14.62a.67.67 0 0 1-.726 0l-3.118-1.798a.5.5 0 0 1-.017.115v.198q0 .841.396 1.551.413.693 1.139 1.089a3.2 3.2 0 0 0 1.617.412m.165-2.69a.4.4 0 0 0 .181.05q.083 0 .165-.05l1.238-.71-3.977-2.31a.7.7 0 0 1-.363-.643v-3.58q-.825.362-1.32 1.122a2.9 2.9 0 0 0-.495 1.65q0 .809.413 1.55.412.743 1.072 1.123zm3.91 3.663q.875 0 1.585-.396a2.96 2.96 0 0 0 1.534-2.64v-3.564a.32.32 0 0 0-.165-.297l-1.254-.726v4.604a.7.7 0 0 1-.363.643l-3.119 1.799a3 3 0 0 0 1.783.577m.627-6.039V8.878L10.01 7.822 8.129 8.878v2.244l1.881 1.056zM7.057 5.859a.7.7 0 0 1 .363-.644l3.119-1.798a3 3 0 0 0-1.782-.578q-.874 0-1.584.396A2.96 2.96 0 0 0 6.05 4.324a3.07 3.07 0 0 0-.396 1.551v3.547q0 .199.165.314l1.237.726zm8.383 7.887q.825-.364 1.303-1.123.495-.758.495-1.65a3.15 3.15 0 0 0-.412-1.55q-.413-.743-1.073-1.123l-3.086-1.782q-.099-.065-.181-.049a.3.3 0 0 0-.165.05l-1.238.692 3.993 2.327a.6.6 0 0 1 .264.264.64.64 0 0 1 .1.363zm-3.317-8.382a.63.63 0 0 1 .726 0l3.135 1.831v-.297q0-.792-.396-1.501a2.86 2.86 0 0 0-1.105-1.155q-.71-.43-1.65-.43-.825 0-1.436.347L8.294 5.941a.36.36 0 0 0-.165.314v1.418z\"/></svg>",
5
+ claude: "<svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z\"/></svg>",
6
+ cursor: "<svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M11.503.131 1.891 5.678a.84.84 0 0 0-.42.726v11.188c0 .3.162.575.42.724l9.609 5.55a1 1 0 0 0 .998 0l9.61-5.55a.84.84 0 0 0 .42-.724V6.404a.84.84 0 0 0-.42-.726L12.497.131a1.01 1.01 0 0 0-.996 0M2.657 6.338h18.55c.263 0 .43.287.297.515L12.23 22.918c-.062.107-.229.064-.229-.06V12.335a.59.59 0 0 0-.295-.51l-9.11-5.257c-.109-.063-.064-.23.061-.23\"/></svg>",
7
+ gemini: "<svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81\"/></svg>",
8
+ copilot: "<svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M23.922 16.997C23.061 18.492 18.063 22.02 12 22.02 5.937 22.02.939 18.492.078 16.997A.641.641 0 0 1 0 16.741v-2.869a.883.883 0 0 1 .053-.22c.372-.935 1.347-2.292 2.605-2.656.167-.429.414-1.055.644-1.517a10.098 10.098 0 0 1-.052-1.086c0-1.331.282-2.499 1.132-3.368.397-.406.89-.717 1.474-.952C7.255 2.937 9.248 1.98 11.978 1.98c2.731 0 4.767.957 6.166 2.093.584.235 1.077.546 1.474.952.85.869 1.132 2.037 1.132 3.368 0 .368-.014.733-.052 1.086.23.462.477 1.088.644 1.517 1.258.364 2.233 1.721 2.605 2.656a.841.841 0 0 1 .053.22v2.869a.641.641 0 0 1-.078.256Zm-11.75-5.992h-.344a4.359 4.359 0 0 1-.355.508c-.77.947-1.918 1.492-3.508 1.492-1.725 0-2.989-.359-3.782-1.259a2.137 2.137 0 0 1-.085-.104L4 11.746v6.585c1.435.779 4.514 2.179 8 2.179 3.486 0 6.565-1.4 8-2.179v-6.585l-.098-.104s-.033.045-.085.104c-.793.9-2.057 1.259-3.782 1.259-1.59 0-2.738-.545-3.508-1.492a4.359 4.359 0 0 1-.355-.508Zm2.328 3.25c.549 0 1 .451 1 1v2c0 .549-.451 1-1 1-.549 0-1-.451-1-1v-2c0-.549.451-1 1-1Zm-5 0c.549 0 1 .451 1 1v2c0 .549-.451 1-1 1-.549 0-1-.451-1-1v-2c0-.549.451-1 1-1Zm3.313-6.185c.136 1.057.403 1.913.878 2.497.442.544 1.134.938 2.344.938 1.573 0 2.292-.337 2.657-.751.384-.435.558-1.15.558-2.361 0-1.14-.243-1.847-.705-2.319-.477-.488-1.319-.862-2.824-1.025-1.487-.161-2.192.138-2.533.529-.269.307-.437.808-.438 1.578v.021c0 .265.021.562.063.893Zm-1.626 0c.042-.331.063-.628.063-.894v-.02c-.001-.77-.169-1.271-.438-1.578-.341-.391-1.046-.69-2.533-.529-1.505.163-2.347.537-2.824 1.025-.462.472-.705 1.179-.705 2.319 0 1.211.175 1.926.558 2.361.365.414 1.084.751 2.657.751 1.21 0 1.902-.394 2.344-.938.475-.584.742-1.44.878-2.497Z\"/></svg>",
9
+ perplexity: "<svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M22.3977 7.0896h-2.3106V.0676l-7.5094 6.3542V.1577h-1.1554v6.1966L4.4904 0v7.0896H1.6023v10.3976h2.8882V24l6.932-6.3591v6.2005h1.1554v-6.0469l6.9318 6.1807v-6.4879h2.8882V7.0896zm-3.4657-4.531v4.531h-5.355l5.355-4.531zm-13.2862.0676 4.8691 4.4634H5.6458V2.6262zM2.7576 16.332V8.245h7.8476l-6.1149 6.1147v1.9723H2.7576zm2.8882 5.0404v-3.8852h.0001v-2.6488l5.7763-5.7764v7.0111l-5.7764 5.2993zm12.7086.0248-5.7766-5.1509V9.0618l5.7766 5.7766v6.5588zm2.8882-5.0652h-1.733v-1.9723L13.3948 8.245h7.8478v8.087z\"/></svg>",
10
+ github: "<svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\"/></svg>"
11
+ };
12
+
13
+ //#endregion
14
+ export { OPEN_DOCS_PROVIDER_ICONS };
@@ -1,3 +1,5 @@
1
+ import { OPEN_DOCS_PROVIDER_ICONS } from "./open-docs-provider-icons.mjs";
2
+
1
3
  //#region src/open-docs-providers.ts
2
4
  const PROMPT_PROVIDER_TEMPLATES = {
3
5
  chatgpt: "https://chatgpt.com/?q={prompt}",
@@ -10,36 +12,43 @@ const PROMPT_PROVIDER_TEMPLATES = {
10
12
  const OPEN_DOCS_PROVIDER_PRESETS = {
11
13
  chatgpt: {
12
14
  name: "ChatGPT",
15
+ iconHtml: OPEN_DOCS_PROVIDER_ICONS.chatgpt,
13
16
  urlTemplate: "https://chatgpt.com/?q={prompt}",
14
17
  promptUrlTemplate: PROMPT_PROVIDER_TEMPLATES.chatgpt
15
18
  },
16
19
  claude: {
17
20
  name: "Claude",
21
+ iconHtml: OPEN_DOCS_PROVIDER_ICONS.claude,
18
22
  urlTemplate: "https://claude.ai/new?q={prompt}",
19
23
  promptUrlTemplate: PROMPT_PROVIDER_TEMPLATES.claude
20
24
  },
21
25
  cursor: {
22
26
  name: "Cursor",
27
+ iconHtml: OPEN_DOCS_PROVIDER_ICONS.cursor,
23
28
  urlTemplate: "https://cursor.com/link/prompt?text={prompt}",
24
29
  promptUrlTemplate: PROMPT_PROVIDER_TEMPLATES.cursor
25
30
  },
26
31
  gemini: {
27
32
  name: "Gemini",
33
+ iconHtml: OPEN_DOCS_PROVIDER_ICONS.gemini,
28
34
  urlTemplate: "https://gemini.google.com/app?q={prompt}",
29
35
  promptUrlTemplate: PROMPT_PROVIDER_TEMPLATES.gemini
30
36
  },
31
37
  copilot: {
32
38
  name: "Copilot",
39
+ iconHtml: OPEN_DOCS_PROVIDER_ICONS.copilot,
33
40
  urlTemplate: "https://github.com/copilot?prompt={prompt}",
34
41
  promptUrlTemplate: PROMPT_PROVIDER_TEMPLATES.copilot
35
42
  },
36
43
  perplexity: {
37
44
  name: "Perplexity",
45
+ iconHtml: OPEN_DOCS_PROVIDER_ICONS.perplexity,
38
46
  urlTemplate: "https://www.perplexity.ai/search/?q={prompt}",
39
47
  promptUrlTemplate: PROMPT_PROVIDER_TEMPLATES.perplexity
40
48
  },
41
49
  github: {
42
50
  name: "GitHub",
51
+ iconHtml: OPEN_DOCS_PROVIDER_ICONS.github,
43
52
  urlTemplate: "{githubUrl}",
44
53
  promptUrlTemplate: "{githubUrl}",
45
54
  target: "github"
@@ -48,6 +57,11 @@ const OPEN_DOCS_PROVIDER_PRESETS = {
48
57
  function normalizeProviderName(name) {
49
58
  return name.trim().toLowerCase();
50
59
  }
60
+ function resolveOpenDocsProviderIcon(name) {
61
+ if (!name) return void 0;
62
+ const normalizedName = normalizeProviderName(name);
63
+ return OPEN_DOCS_PROVIDER_PRESETS[normalizedName === "github copilot" ? "copilot" : normalizedName]?.iconHtml;
64
+ }
51
65
  function resolveOpenDocsProviders(providers, options = {}) {
52
66
  if (!providers || providers.length === 0) return void 0;
53
67
  const serialized = providers.map((provider) => resolveOpenDocsProvider(provider, options)).filter((provider) => provider !== void 0);
@@ -60,6 +74,7 @@ function resolveOpenDocsProvider(provider, options = {}) {
60
74
  if (!preset) return void 0;
61
75
  return {
62
76
  name: preset.name,
77
+ iconHtml: preset.iconHtml,
63
78
  urlTemplate: preset.urlTemplate,
64
79
  promptUrlTemplate: preset.promptUrlTemplate,
65
80
  target: preset.target ?? options.target,
@@ -75,11 +90,11 @@ function resolveOpenDocsProvider(provider, options = {}) {
75
90
  name,
76
91
  urlTemplate,
77
92
  promptUrlTemplate: provider.promptUrlTemplate ?? cursorAppTemplate ?? preset?.promptUrlTemplate,
78
- iconHtml: options.serializeIcon?.(provider.icon) ?? (typeof provider.icon === "string" ? provider.icon : void 0),
93
+ iconHtml: options.serializeIcon?.(provider.icon) ?? (typeof provider.icon === "string" ? provider.icon : void 0) ?? preset?.iconHtml,
79
94
  target: provider.target ?? preset?.target ?? options.target ?? (hasCustomUrlTemplate ? "page" : void 0),
80
95
  prompt: provider.prompt ?? options.prompt
81
96
  };
82
97
  }
83
98
 
84
99
  //#endregion
85
- export { resolveOpenDocsProviders };
100
+ export { resolveOpenDocsProviderIcon, resolveOpenDocsProviders };
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { emitClientAnalyticsEvent } from "./client-analytics.mjs";
4
+ import { resolveOpenDocsProviderIcon, resolveOpenDocsProviders } from "./open-docs-providers.mjs";
4
5
  import { sanitizeIconHtml } from "./safe-icon-html.mjs";
5
6
  import { useCallback, useEffect, useRef, useState } from "react";
6
7
  import { usePathname } from "fumadocs-core/framework";
@@ -107,13 +108,7 @@ const SparklesIcon = () => /* @__PURE__ */ jsxs("svg", {
107
108
  /* @__PURE__ */ jsx("path", { d: "M19 11.5 20 15l3.5 1-3.5 1-1 3.5-1-3.5-3.5-1 3.5-1Z" })
108
109
  ]
109
110
  });
110
- const DEFAULT_PROVIDERS = [{
111
- name: "ChatGPT",
112
- urlTemplate: "https://chatgpt.com/?q={prompt}"
113
- }, {
114
- name: "Claude",
115
- urlTemplate: "https://claude.ai/new?q={prompt}"
116
- }];
111
+ const DEFAULT_PROVIDERS = resolveOpenDocsProviders(["chatgpt", "claude"]) ?? [];
117
112
  const DEFAULT_OPEN_DOCS_TARGET = "markdown";
118
113
  const DEFAULT_OPEN_DOCS_PROMPT = "Read this documentation: {url}";
119
114
  function pageUrlToMarkdownUrl(pageUrl) {
@@ -328,7 +323,7 @@ function PageActions({ copyMarkdown, copyMarkdownFormat = "markdown", copyMarkdo
328
323
  className: "fd-page-action-menu",
329
324
  role: "menu",
330
325
  children: resolvedProviders.map((provider) => {
331
- const iconHtml = sanitizeIconHtml(provider.iconHtml);
326
+ const iconHtml = sanitizeIconHtml(provider.iconHtml ?? resolveOpenDocsProviderIcon(provider.name));
332
327
  return /* @__PURE__ */ jsxs("button", {
333
328
  type: "button",
334
329
  role: "menuitem",
@@ -59,6 +59,7 @@ declare const PixelBorderUIDefaults: {
59
59
  layout: {
60
60
  contentWidth: number;
61
61
  sidebarWidth: number;
62
+ tocWidth: number;
62
63
  toc: {
63
64
  enabled: boolean;
64
65
  depth: number;
@@ -29,30 +29,30 @@ const PixelBorderUIDefaults = {
29
29
  },
30
30
  h1: {
31
31
  size: "2.25rem",
32
- weight: 700,
33
- lineHeight: "1.2",
32
+ weight: 400,
33
+ lineHeight: "1.14",
34
34
  letterSpacing: "-0.02em"
35
35
  },
36
36
  h2: {
37
37
  size: "1.5rem",
38
- weight: 600,
39
- lineHeight: "1.3",
38
+ weight: 400,
39
+ lineHeight: "1.24",
40
40
  letterSpacing: "-0.01em"
41
41
  },
42
42
  h3: {
43
43
  size: "1.25rem",
44
- weight: 600,
45
- lineHeight: "1.4"
44
+ weight: 400,
45
+ lineHeight: "1.3"
46
46
  },
47
47
  h4: {
48
48
  size: "1.125rem",
49
- weight: 600,
49
+ weight: 400,
50
50
  lineHeight: "1.4"
51
51
  },
52
52
  body: {
53
- size: "1rem",
53
+ size: "0.975rem",
54
54
  weight: 400,
55
- lineHeight: "1.75"
55
+ lineHeight: "1.8"
56
56
  },
57
57
  small: {
58
58
  size: "0.875rem",
@@ -63,12 +63,13 @@ const PixelBorderUIDefaults = {
63
63
  layout: {
64
64
  contentWidth: 860,
65
65
  sidebarWidth: 286,
66
+ tocWidth: 240,
66
67
  toc: {
67
68
  enabled: true,
68
69
  depth: 3
69
70
  },
70
71
  header: {
71
- height: 56,
72
+ height: 44,
72
73
  sticky: true
73
74
  }
74
75
  },