@farming-labs/theme 0.0.2-beta.22 → 0.0.2-beta.24

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,10 +1,12 @@
1
1
  //#region src/ai-search-dialog.d.ts
2
+ type LoaderVariant = "shimmer-dots" | "circular" | "dots" | "typing" | "wave" | "bars" | "pulse" | "pulse-dot" | "terminal" | "text-blink" | "text-shimmer" | "loading-dots";
2
3
  declare function DocsSearchDialog({
3
4
  open,
4
5
  onOpenChange,
5
6
  api,
6
7
  suggestedQuestions,
7
8
  aiLabel,
9
+ loaderVariant,
8
10
  loadingComponentHtml
9
11
  }: {
10
12
  open: boolean;
@@ -12,6 +14,7 @@ declare function DocsSearchDialog({
12
14
  api?: string;
13
15
  suggestedQuestions?: string[];
14
16
  aiLabel?: string;
17
+ loaderVariant?: LoaderVariant;
15
18
  loadingComponentHtml?: string;
16
19
  }): any;
17
20
  type FloatingPosition = "bottom-right" | "bottom-left" | "bottom-center";
@@ -23,6 +26,7 @@ declare function FloatingAIChat({
23
26
  triggerComponentHtml,
24
27
  suggestedQuestions,
25
28
  aiLabel,
29
+ loaderVariant,
26
30
  loadingComponentHtml
27
31
  }: {
28
32
  api?: string;
@@ -31,6 +35,7 @@ declare function FloatingAIChat({
31
35
  triggerComponentHtml?: string;
32
36
  suggestedQuestions?: string[];
33
37
  aiLabel?: string;
38
+ loaderVariant?: LoaderVariant;
34
39
  loadingComponentHtml?: string;
35
40
  }): any;
36
41
  declare function AIModalDialog({
@@ -39,6 +44,7 @@ declare function AIModalDialog({
39
44
  api,
40
45
  suggestedQuestions,
41
46
  aiLabel,
47
+ loaderVariant,
42
48
  loadingComponentHtml
43
49
  }: {
44
50
  open: boolean;
@@ -46,6 +52,7 @@ declare function AIModalDialog({
46
52
  api?: string;
47
53
  suggestedQuestions?: string[];
48
54
  aiLabel?: string;
55
+ loaderVariant?: LoaderVariant;
49
56
  loadingComponentHtml?: string;
50
57
  }): any;
51
58
  //#endregion
@@ -143,33 +143,145 @@ function XIcon() {
143
143
  children: [/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }), /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })]
144
144
  });
145
145
  }
146
- function DefaultLoadingIndicator({ label }) {
147
- return /* @__PURE__ */ jsxs("span", {
148
- className: "fd-ai-loading",
149
- children: [/* @__PURE__ */ jsxs("span", {
150
- className: "fd-ai-loading-text",
151
- children: [label, " is thinking"]
152
- }), /* @__PURE__ */ jsxs("span", {
153
- className: "fd-ai-loading-dots",
154
- children: [
155
- /* @__PURE__ */ jsx("span", { className: "fd-ai-loading-dot" }),
156
- /* @__PURE__ */ jsx("span", { className: "fd-ai-loading-dot" }),
157
- /* @__PURE__ */ jsx("span", { className: "fd-ai-loading-dot" })
158
- ]
159
- })]
160
- });
146
+ function LoaderIndicator({ variant = "shimmer-dots" }) {
147
+ const text = "Thinking";
148
+ switch (variant) {
149
+ case "circular": return /* @__PURE__ */ jsxs("div", {
150
+ className: "fd-ai-loader",
151
+ children: [/* @__PURE__ */ jsx("div", { className: "fd-ai-loader-circular" }), /* @__PURE__ */ jsx("span", {
152
+ className: "sr-only",
153
+ children: "Loading"
154
+ })]
155
+ });
156
+ case "dots": return /* @__PURE__ */ jsx("div", {
157
+ className: "fd-ai-loader",
158
+ children: /* @__PURE__ */ jsxs("span", {
159
+ className: "fd-ai-loader-dots",
160
+ children: [
161
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loader-bounce-dot" }),
162
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loader-bounce-dot" }),
163
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loader-bounce-dot" })
164
+ ]
165
+ })
166
+ });
167
+ case "typing": return /* @__PURE__ */ jsx("div", {
168
+ className: "fd-ai-loader",
169
+ children: /* @__PURE__ */ jsxs("span", {
170
+ className: "fd-ai-loader-typing-dots",
171
+ children: [
172
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loader-typing-dot" }),
173
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loader-typing-dot" }),
174
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loader-typing-dot" })
175
+ ]
176
+ })
177
+ });
178
+ case "wave": return /* @__PURE__ */ jsx("div", {
179
+ className: "fd-ai-loader",
180
+ children: /* @__PURE__ */ jsxs("span", {
181
+ className: "fd-ai-loader-wave",
182
+ children: [
183
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loader-wave-bar" }),
184
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loader-wave-bar" }),
185
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loader-wave-bar" }),
186
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loader-wave-bar" }),
187
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loader-wave-bar" })
188
+ ]
189
+ })
190
+ });
191
+ case "bars": return /* @__PURE__ */ jsx("div", {
192
+ className: "fd-ai-loader",
193
+ children: /* @__PURE__ */ jsxs("span", {
194
+ className: "fd-ai-loader-bars",
195
+ children: [
196
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loader-bar" }),
197
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loader-bar" }),
198
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loader-bar" })
199
+ ]
200
+ })
201
+ });
202
+ case "pulse": return /* @__PURE__ */ jsx("div", {
203
+ className: "fd-ai-loader",
204
+ children: /* @__PURE__ */ jsx("div", { className: "fd-ai-loader-pulse" })
205
+ });
206
+ case "pulse-dot": return /* @__PURE__ */ jsx("div", {
207
+ className: "fd-ai-loader",
208
+ children: /* @__PURE__ */ jsx("div", { className: "fd-ai-loader-pulse-dot" })
209
+ });
210
+ case "terminal": return /* @__PURE__ */ jsx("div", {
211
+ className: "fd-ai-loader",
212
+ children: /* @__PURE__ */ jsxs("span", {
213
+ className: "fd-ai-loader-terminal",
214
+ children: [/* @__PURE__ */ jsx("span", {
215
+ className: "fd-ai-loader-terminal-prompt",
216
+ children: ">"
217
+ }), /* @__PURE__ */ jsx("span", { className: "fd-ai-loader-terminal-cursor" })]
218
+ })
219
+ });
220
+ case "text-blink": return /* @__PURE__ */ jsx("div", {
221
+ className: "fd-ai-loader",
222
+ children: /* @__PURE__ */ jsx("span", {
223
+ className: "fd-ai-loader-text-blink",
224
+ children: text
225
+ })
226
+ });
227
+ case "text-shimmer": return /* @__PURE__ */ jsx("div", {
228
+ className: "fd-ai-loader",
229
+ children: /* @__PURE__ */ jsx("span", {
230
+ className: "fd-ai-loader-shimmer-text",
231
+ children: text
232
+ })
233
+ });
234
+ case "loading-dots": return /* @__PURE__ */ jsxs("div", {
235
+ className: "fd-ai-loader",
236
+ children: [/* @__PURE__ */ jsx("span", {
237
+ className: "fd-ai-loader-text",
238
+ children: text
239
+ }), /* @__PURE__ */ jsxs("span", {
240
+ className: "fd-ai-loader-text-dots",
241
+ children: [
242
+ /* @__PURE__ */ jsx("span", {
243
+ className: "fd-ai-loader-text-dot",
244
+ children: "."
245
+ }),
246
+ /* @__PURE__ */ jsx("span", {
247
+ className: "fd-ai-loader-text-dot",
248
+ children: "."
249
+ }),
250
+ /* @__PURE__ */ jsx("span", {
251
+ className: "fd-ai-loader-text-dot",
252
+ children: "."
253
+ })
254
+ ]
255
+ })]
256
+ });
257
+ default: return /* @__PURE__ */ jsxs("div", {
258
+ className: "fd-ai-loader",
259
+ children: [/* @__PURE__ */ jsx("span", {
260
+ className: "fd-ai-loader-shimmer-text",
261
+ children: text
262
+ }), /* @__PURE__ */ jsxs("span", {
263
+ className: "fd-ai-loader-typing-dots",
264
+ children: [
265
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loader-typing-dot" }),
266
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loader-typing-dot" }),
267
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loader-typing-dot" })
268
+ ]
269
+ })]
270
+ });
271
+ }
161
272
  }
162
- function LoadingDots() {
273
+ function InlineLoaderDots() {
163
274
  return /* @__PURE__ */ jsxs("span", {
164
- className: "fd-ai-loading-dots",
275
+ className: "fd-ai-loader-typing-dots",
276
+ style: { marginLeft: 0 },
165
277
  children: [
166
- /* @__PURE__ */ jsx("span", { className: "fd-ai-loading-dot" }),
167
- /* @__PURE__ */ jsx("span", { className: "fd-ai-loading-dot" }),
168
- /* @__PURE__ */ jsx("span", { className: "fd-ai-loading-dot" })
278
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loader-typing-dot" }),
279
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loader-typing-dot" }),
280
+ /* @__PURE__ */ jsx("span", { className: "fd-ai-loader-typing-dot" })
169
281
  ]
170
282
  });
171
283
  }
172
- function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming, setIsStreaming, suggestedQuestions, aiLabel, loadingComponentHtml }) {
284
+ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming, setIsStreaming, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml }) {
173
285
  const label = aiLabel || "AI";
174
286
  const aiInputRef = useRef(null);
175
287
  const messagesEndRef = useRef(null);
@@ -311,7 +423,13 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
311
423
  children: msg.content
312
424
  }) : /* @__PURE__ */ jsx("div", {
313
425
  className: "fd-ai-bubble-ai",
314
- children: msg.content ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderMarkdown(msg.content) } }) : loadingComponentHtml ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: loadingComponentHtml } }) : /* @__PURE__ */ jsx(DefaultLoadingIndicator, { label })
426
+ children: msg.content ? /* @__PURE__ */ jsx("div", {
427
+ className: isStreaming && i === messages.length - 1 ? "fd-ai-streaming" : void 0,
428
+ dangerouslySetInnerHTML: { __html: renderMarkdown(msg.content) }
429
+ }) : loadingComponentHtml ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: loadingComponentHtml } }) : /* @__PURE__ */ jsx(LoaderIndicator, {
430
+ variant: loaderVariant,
431
+ label
432
+ })
315
433
  })]
316
434
  }, i)), /* @__PURE__ */ jsx("div", { ref: messagesEndRef })]
317
435
  }), /* @__PURE__ */ jsxs("div", {
@@ -353,7 +471,7 @@ function AIChat({ api, messages, setMessages, aiInput, setAiInput, isStreaming,
353
471
  })]
354
472
  });
355
473
  }
356
- function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestions, aiLabel, loadingComponentHtml }) {
474
+ function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml }) {
357
475
  const [tab, setTab] = useState("search");
358
476
  const [searchQuery, setSearchQuery] = useState("");
359
477
  const [searchResults, setSearchResults] = useState([]);
@@ -496,7 +614,7 @@ function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQues
496
614
  onKeyDown: handleSearchKeyDown,
497
615
  className: "fd-ai-input"
498
616
  }),
499
- isSearching && /* @__PURE__ */ jsx(LoadingDots, {})
617
+ isSearching && /* @__PURE__ */ jsx(InlineLoaderDots, {})
500
618
  ]
501
619
  }), /* @__PURE__ */ jsx("div", {
502
620
  className: "fd-ai-results",
@@ -528,6 +646,7 @@ function DocsSearchDialog({ open, onOpenChange, api = "/api/docs", suggestedQues
528
646
  setIsStreaming,
529
647
  suggestedQuestions,
530
648
  aiLabel,
649
+ loaderVariant,
531
650
  loadingComponentHtml
532
651
  })
533
652
  ]
@@ -602,7 +721,7 @@ function getContainerStyles(style, position) {
602
721
  function getAnimation(style) {
603
722
  return style === "modal" ? "fd-ai-float-center-in 200ms ease-out" : "fd-ai-float-in 200ms ease-out";
604
723
  }
605
- function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floatingStyle = "panel", triggerComponentHtml, suggestedQuestions, aiLabel, loadingComponentHtml }) {
724
+ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floatingStyle = "panel", triggerComponentHtml, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml }) {
606
725
  const [mounted, setMounted] = useState(false);
607
726
  const [isOpen, setIsOpen] = useState(false);
608
727
  const [messages, setMessages] = useState([]);
@@ -640,6 +759,7 @@ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floating
640
759
  setIsStreaming,
641
760
  suggestedQuestions,
642
761
  aiLabel,
762
+ loaderVariant,
643
763
  loadingComponentHtml,
644
764
  triggerComponentHtml,
645
765
  position
@@ -688,6 +808,7 @@ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floating
688
808
  setIsStreaming,
689
809
  suggestedQuestions,
690
810
  aiLabel,
811
+ loaderVariant,
691
812
  loadingComponentHtml
692
813
  })]
693
814
  }),
@@ -705,7 +826,7 @@ function FloatingAIChat({ api = "/api/docs", position = "bottom-right", floating
705
826
  }))
706
827
  ] }), document.body);
707
828
  }
708
- function FullModalAIChat({ api, isOpen, setIsOpen, messages, setMessages, aiInput, setAiInput, isStreaming, setIsStreaming, suggestedQuestions, aiLabel, loadingComponentHtml, triggerComponentHtml, position }) {
829
+ function FullModalAIChat({ api, isOpen, setIsOpen, messages, setMessages, aiInput, setAiInput, isStreaming, setIsStreaming, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml, triggerComponentHtml, position }) {
709
830
  const label = aiLabel || "AI";
710
831
  const inputRef = useRef(null);
711
832
  const listRef = useRef(null);
@@ -831,13 +952,12 @@ function FullModalAIChat({ api, isOpen, setIsOpen, messages, setMessages, aiInpu
831
952
  children: msg.role === "user" ? "you" : label
832
953
  }), /* @__PURE__ */ jsx("div", {
833
954
  className: "fd-ai-fm-msg-content",
834
- children: msg.content ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderMarkdown(msg.content) } }) : loadingComponentHtml ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: loadingComponentHtml } }) : /* @__PURE__ */ jsxs("div", {
835
- className: "fd-ai-fm-thinking",
836
- children: [
837
- /* @__PURE__ */ jsx("span", { className: "fd-ai-fm-thinking-dot" }),
838
- /* @__PURE__ */ jsx("span", { className: "fd-ai-fm-thinking-dot" }),
839
- /* @__PURE__ */ jsx("span", { className: "fd-ai-fm-thinking-dot" })
840
- ]
955
+ children: msg.content ? /* @__PURE__ */ jsx("div", {
956
+ className: isStreaming && i === messages.length - 1 ? "fd-ai-streaming" : void 0,
957
+ dangerouslySetInnerHTML: { __html: renderMarkdown(msg.content) }
958
+ }) : loadingComponentHtml ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: loadingComponentHtml } }) : /* @__PURE__ */ jsx(LoaderIndicator, {
959
+ variant: loaderVariant,
960
+ label
841
961
  })
842
962
  })]
843
963
  }, i))
@@ -871,7 +991,7 @@ function FullModalAIChat({ api, isOpen, setIsOpen, messages, setMessages, aiInpu
871
991
  }), isStreaming ? /* @__PURE__ */ jsx("button", {
872
992
  className: "fd-ai-fm-send-btn",
873
993
  onClick: () => setIsStreaming(false),
874
- children: /* @__PURE__ */ jsx(LoadingDots, {})
994
+ children: /* @__PURE__ */ jsx(InlineLoaderDots, {})
875
995
  }) : /* @__PURE__ */ jsx("button", {
876
996
  className: "fd-ai-fm-send-btn",
877
997
  "data-active": canSend,
@@ -932,7 +1052,7 @@ function TrashIcon() {
932
1052
  ]
933
1053
  });
934
1054
  }
935
- function AIModalDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestions, aiLabel, loadingComponentHtml }) {
1055
+ function AIModalDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml }) {
936
1056
  const [messages, setMessages] = useState([]);
937
1057
  const [aiInput, setAiInput] = useState("");
938
1058
  const [isStreaming, setIsStreaming] = useState(false);
@@ -999,6 +1119,7 @@ function AIModalDialog({ open, onOpenChange, api = "/api/docs", suggestedQuestio
999
1119
  setIsStreaming,
1000
1120
  suggestedQuestions,
1001
1121
  aiLabel,
1122
+ loaderVariant,
1002
1123
  loadingComponentHtml
1003
1124
  }),
1004
1125
  /* @__PURE__ */ jsx("div", {
@@ -8,6 +8,7 @@ interface DocsAIFeaturesProps {
8
8
  triggerComponentHtml?: string;
9
9
  suggestedQuestions?: string[];
10
10
  aiLabel?: string;
11
+ loaderVariant?: string;
11
12
  loadingComponentHtml?: string;
12
13
  }
13
14
  declare function DocsAIFeatures({
@@ -17,6 +18,7 @@ declare function DocsAIFeatures({
17
18
  triggerComponentHtml,
18
19
  suggestedQuestions,
19
20
  aiLabel,
21
+ loaderVariant,
20
22
  loadingComponentHtml
21
23
  }: DocsAIFeaturesProps): react_jsx_runtime0.JSX.Element;
22
24
  //#endregion
@@ -19,15 +19,17 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
19
19
  * This component is rendered inside the docs layout so the user's root layout
20
20
  * never needs to be modified — AI features work purely from `docs.config.tsx`.
21
21
  */
22
- function DocsAIFeatures({ mode, position = "bottom-right", floatingStyle = "panel", triggerComponentHtml, suggestedQuestions, aiLabel, loadingComponentHtml }) {
22
+ function DocsAIFeatures({ mode, position = "bottom-right", floatingStyle = "panel", triggerComponentHtml, suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml }) {
23
23
  if (mode === "search") return /* @__PURE__ */ jsx(SearchModeAI, {
24
24
  suggestedQuestions,
25
25
  aiLabel,
26
+ loaderVariant,
26
27
  loadingComponentHtml
27
28
  });
28
29
  if (mode === "sidebar-icon") return /* @__PURE__ */ jsx(SidebarIconModeAI, {
29
30
  suggestedQuestions,
30
31
  aiLabel,
32
+ loaderVariant,
31
33
  loadingComponentHtml
32
34
  });
33
35
  return /* @__PURE__ */ jsx(FloatingAIChat, {
@@ -37,15 +39,11 @@ function DocsAIFeatures({ mode, position = "bottom-right", floatingStyle = "pane
37
39
  triggerComponentHtml,
38
40
  suggestedQuestions,
39
41
  aiLabel,
42
+ loaderVariant,
40
43
  loadingComponentHtml
41
44
  });
42
45
  }
43
- /**
44
- * Search mode: intercepts Cmd+K / Ctrl+K globally and opens the
45
- * custom search dialog (with Search + Ask AI tabs) instead of
46
- * fumadocs' built-in search dialog.
47
- */
48
- function SearchModeAI({ suggestedQuestions, aiLabel, loadingComponentHtml }) {
46
+ function SearchModeAI({ suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml }) {
49
47
  const [open, setOpen] = useState(false);
50
48
  useEffect(() => {
51
49
  function handler(e) {
@@ -80,15 +78,11 @@ function SearchModeAI({ suggestedQuestions, aiLabel, loadingComponentHtml }) {
80
78
  api: "/api/docs",
81
79
  suggestedQuestions,
82
80
  aiLabel,
81
+ loaderVariant,
83
82
  loadingComponentHtml
84
83
  });
85
84
  }
86
- /**
87
- * Sidebar-icon mode: injects a sparkle icon button next to the search bar
88
- * in the sidebar header. The search button opens the Cmd+K search dialog,
89
- * and the AI sparkle button opens a pure AI modal (no search tabs).
90
- */
91
- function SidebarIconModeAI({ suggestedQuestions, aiLabel, loadingComponentHtml }) {
85
+ function SidebarIconModeAI({ suggestedQuestions, aiLabel, loaderVariant, loadingComponentHtml }) {
92
86
  const [searchOpen, setSearchOpen] = useState(false);
93
87
  const [aiOpen, setAiOpen] = useState(false);
94
88
  useEffect(() => {
@@ -123,6 +117,7 @@ function SidebarIconModeAI({ suggestedQuestions, aiLabel, loadingComponentHtml }
123
117
  api: "/api/docs",
124
118
  suggestedQuestions,
125
119
  aiLabel,
120
+ loaderVariant,
126
121
  loadingComponentHtml
127
122
  }), /* @__PURE__ */ jsx(AIModalDialog, {
128
123
  open: aiOpen,
@@ -130,6 +125,7 @@ function SidebarIconModeAI({ suggestedQuestions, aiLabel, loadingComponentHtml }
130
125
  api: "/api/docs",
131
126
  suggestedQuestions,
132
127
  aiLabel,
128
+ loaderVariant,
133
129
  loadingComponentHtml
134
130
  })] });
135
131
  }
@@ -38,12 +38,14 @@ interface DocsAPIOptions {
38
38
  * Create a unified docs API route handler.
39
39
  *
40
40
  * Returns `{ GET, POST }` for use in a Next.js route handler:
41
- * - **GET** → full-text search (same as the old `createDocsSearchAPI`)
42
- * - **POST** AI-powered chat with RAG (when AI is enabled in config)
41
+ * - **GET ?query=…** → full-text search
42
+ * - **GET ?format=llms** llms.txt (concise page listing)
43
+ * - **GET ?format=llms-full** → llms-full.txt (full page content)
44
+ * - **POST** → AI-powered chat with RAG
43
45
  *
44
46
  * @example
45
47
  * ```ts
46
- * // app/api/docs/route.ts
48
+ * // app/api/docs/route.ts (auto-generated by withDocs)
47
49
  * import { createDocsAPI } from "@farming-labs/theme/api";
48
50
  * export const { GET, POST } = createDocsAPI();
49
51
  * export const revalidate = false;
@@ -53,10 +55,9 @@ interface DocsAPIOptions {
53
55
  */
54
56
  declare function createDocsAPI(options?: DocsAPIOptions): {
55
57
  /**
56
- * GET handler — full-text search.
57
- * Query: `?query=search+term`
58
+ * GET handler — search, llms.txt, or llms-full.txt depending on query params.
58
59
  */
59
- GET: (request: Request) => Promise<Response>;
60
+ GET(request: Request): Response | Promise<Response>;
60
61
  /**
61
62
  * POST handler — AI chat with RAG.
62
63
  * Body: `{ messages: [{ role: "user", content: "How do I …?" }] }`
package/dist/docs-api.mjs CHANGED
@@ -163,16 +163,64 @@ async function handleAskAI(request, indexes, searchServer, aiConfig) {
163
163
  Connection: "keep-alive"
164
164
  } });
165
165
  }
166
+ function readLlmsTxtConfig(root) {
167
+ for (const ext of FILE_EXTS) {
168
+ const configPath = path.join(root, `docs.config.${ext}`);
169
+ if (fs.existsSync(configPath)) try {
170
+ const content = fs.readFileSync(configPath, "utf-8");
171
+ if (!content.includes("llmsTxt")) return { enabled: false };
172
+ if (/llmsTxt\s*:\s*true/.test(content)) return { enabled: true };
173
+ const enabledMatch = content.match(/llmsTxt\s*:\s*\{[^}]*enabled\s*:\s*(true|false)/s);
174
+ if (enabledMatch && enabledMatch[1] === "false") return { enabled: false };
175
+ const baseUrlMatch = content.match(/llmsTxt\s*:\s*\{[^}]*baseUrl\s*:\s*["']([^"']+)["']/s);
176
+ const siteTitleMatch = content.match(/llmsTxt\s*:\s*\{[^}]*siteTitle\s*:\s*["']([^"']+)["']/s);
177
+ const siteDescMatch = content.match(/llmsTxt\s*:\s*\{[^}]*siteDescription\s*:\s*["']([^"']+)["']/s);
178
+ const navTitleMatch = content.match(/nav\s*:\s*\{[^}]*title\s*:\s*["']([^"']+)["']/s);
179
+ return {
180
+ enabled: true,
181
+ baseUrl: baseUrlMatch?.[1],
182
+ siteTitle: siteTitleMatch?.[1] ?? navTitleMatch?.[1],
183
+ siteDescription: siteDescMatch?.[1]
184
+ };
185
+ } catch {}
186
+ }
187
+ return { enabled: false };
188
+ }
189
+ function generateLlmsTxt(indexes, options) {
190
+ const { siteTitle = "Documentation", siteDescription, baseUrl = "" } = options;
191
+ let llmsTxt = `# ${siteTitle}\n\n`;
192
+ if (siteDescription) llmsTxt += `> ${siteDescription}\n\n`;
193
+ llmsTxt += `## Pages\n\n`;
194
+ for (const page of indexes) {
195
+ llmsTxt += `- [${page.title}](${baseUrl}${page.url})`;
196
+ if (page.description) llmsTxt += `: ${page.description}`;
197
+ llmsTxt += `\n`;
198
+ }
199
+ let llmsFullTxt = `# ${siteTitle}\n\n`;
200
+ if (siteDescription) llmsFullTxt += `> ${siteDescription}\n\n`;
201
+ for (const page of indexes) {
202
+ llmsFullTxt += `## ${page.title}\n\n`;
203
+ llmsFullTxt += `URL: ${baseUrl}${page.url}\n\n`;
204
+ if (page.description) llmsFullTxt += `${page.description}\n\n`;
205
+ llmsFullTxt += `${page.content}\n\n---\n\n`;
206
+ }
207
+ return {
208
+ llmsTxt,
209
+ llmsFullTxt
210
+ };
211
+ }
166
212
  /**
167
213
  * Create a unified docs API route handler.
168
214
  *
169
215
  * Returns `{ GET, POST }` for use in a Next.js route handler:
170
- * - **GET** → full-text search (same as the old `createDocsSearchAPI`)
171
- * - **POST** AI-powered chat with RAG (when AI is enabled in config)
216
+ * - **GET ?query=…** → full-text search
217
+ * - **GET ?format=llms** llms.txt (concise page listing)
218
+ * - **GET ?format=llms-full** → llms-full.txt (full page content)
219
+ * - **POST** → AI-powered chat with RAG
172
220
  *
173
221
  * @example
174
222
  * ```ts
175
- * // app/api/docs/route.ts
223
+ * // app/api/docs/route.ts (auto-generated by withDocs)
176
224
  * import { createDocsAPI } from "@farming-labs/theme/api";
177
225
  * export const { GET, POST } = createDocsAPI();
178
226
  * export const revalidate = false;
@@ -186,13 +234,34 @@ function createDocsAPI(options) {
186
234
  const docsDir = path.join(root, "app", entry);
187
235
  const language = options?.language ?? "english";
188
236
  const aiConfig = options?.ai ?? readAIConfig(root);
237
+ const llmsConfig = readLlmsTxtConfig(root);
189
238
  const indexes = scanDocsDir(docsDir, entry);
239
+ let _llmsCache = null;
240
+ function getLlmsContent() {
241
+ if (!_llmsCache) _llmsCache = generateLlmsTxt(indexes, {
242
+ siteTitle: llmsConfig.siteTitle ?? "Documentation",
243
+ siteDescription: llmsConfig.siteDescription,
244
+ baseUrl: llmsConfig.baseUrl ?? ""
245
+ });
246
+ return _llmsCache;
247
+ }
190
248
  const searchAPI = createSearchAPI("simple", {
191
249
  language,
192
250
  indexes
193
251
  });
194
252
  return {
195
- GET: searchAPI.GET,
253
+ GET(request) {
254
+ const format = new URL(request.url).searchParams.get("format");
255
+ if (format === "llms") return new Response(getLlmsContent().llmsTxt, { headers: {
256
+ "Content-Type": "text/plain; charset=utf-8",
257
+ "Cache-Control": "public, max-age=3600"
258
+ } });
259
+ if (format === "llms-full") return new Response(getLlmsContent().llmsFullTxt, { headers: {
260
+ "Content-Type": "text/plain; charset=utf-8",
261
+ "Cache-Control": "public, max-age=3600"
262
+ } });
263
+ return searchAPI.GET(request);
264
+ },
196
265
  async POST(request) {
197
266
  if (!aiConfig.enabled) return Response.json({ error: "AI is not enabled. Set `ai: { enabled: true }` in your docs.config to enable it." }, { status: 404 });
198
267
  return handleAskAI(request, indexes, searchAPI, aiConfig);
@@ -357,6 +357,7 @@ function createDocsLayout(config) {
357
357
  const lastUpdatedRaw = config.lastUpdated;
358
358
  const lastUpdatedEnabled = lastUpdatedRaw !== false && (typeof lastUpdatedRaw !== "object" || lastUpdatedRaw.enabled !== false);
359
359
  const lastUpdatedPosition = typeof lastUpdatedRaw === "object" ? lastUpdatedRaw.position ?? "footer" : "footer";
360
+ const llmsTxtEnabled = resolveBool(config.llmsTxt);
360
361
  const openDocsProviders = (typeof pageActions?.openDocs === "object" && pageActions.openDocs.providers ? pageActions.openDocs.providers : void 0)?.map((p) => ({
361
362
  name: p.name,
362
363
  urlTemplate: p.urlTemplate,
@@ -374,6 +375,7 @@ function createDocsLayout(config) {
374
375
  const aiTriggerComponentHtml = aiConfig?.triggerComponent ? serializeIcon(aiConfig.triggerComponent) : void 0;
375
376
  const aiSuggestedQuestions = aiConfig?.suggestedQuestions;
376
377
  const aiLabel = aiConfig?.aiLabel;
378
+ const aiLoaderVariant = aiConfig?.loader;
377
379
  const aiLoadingComponentHtml = typeof aiConfig?.loadingComponent === "function" ? serializeIcon(aiConfig.loadingComponent({ name: aiLabel || "AI" })) : void 0;
378
380
  const lastModifiedMap = buildLastModifiedMap(config.entry);
379
381
  const descriptionMap = buildDescriptionMap(config.entry);
@@ -400,6 +402,7 @@ function createDocsLayout(config) {
400
402
  triggerComponentHtml: aiTriggerComponentHtml,
401
403
  suggestedQuestions: aiSuggestedQuestions,
402
404
  aiLabel,
405
+ loaderVariant: aiLoaderVariant,
403
406
  loadingComponentHtml: aiLoadingComponentHtml
404
407
  }),
405
408
  /* @__PURE__ */ jsx(DocsPageClient, {
@@ -418,6 +421,7 @@ function createDocsLayout(config) {
418
421
  lastModifiedMap,
419
422
  lastUpdatedEnabled,
420
423
  lastUpdatedPosition,
424
+ llmsTxtEnabled,
421
425
  descriptionMap,
422
426
  children
423
427
  })
@@ -33,6 +33,8 @@ interface DocsPageClientProps {
33
33
  lastUpdatedEnabled?: boolean;
34
34
  /** Where to show the "Last updated" date: "footer" (next to Edit on GitHub) or "below-title" */
35
35
  lastUpdatedPosition?: "footer" | "below-title";
36
+ /** Whether llms.txt is enabled — shows links in footer */
37
+ llmsTxtEnabled?: boolean;
36
38
  /** Map of pathname → frontmatter description */
37
39
  descriptionMap?: Record<string, string>;
38
40
  /** Frontmatter description to display below the page title (overrides descriptionMap) */
@@ -55,6 +57,7 @@ declare function DocsPageClient({
55
57
  lastModifiedMap,
56
58
  lastUpdatedEnabled,
57
59
  lastUpdatedPosition,
60
+ llmsTxtEnabled,
58
61
  descriptionMap,
59
62
  description,
60
63
  children
@@ -63,7 +63,7 @@ function buildGithubFileUrl(githubUrl, branch, pathname, directory) {
63
63
  const segments = pathname.replace(/^\//, "").replace(/\/$/, "");
64
64
  return `${githubUrl}/tree/${branch}/${directory ? `${directory}/` : ""}app/${segments}/page.mdx`;
65
65
  }
66
- function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled = true, entry = "docs", copyMarkdown = false, openDocs = false, openDocsProviders, pageActionsPosition = "below-title", pageActionsAlignment = "left", githubUrl, githubBranch = "main", githubDirectory, lastModifiedMap, lastUpdatedEnabled = true, lastUpdatedPosition = "footer", descriptionMap, description, children }) {
66
+ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled = true, entry = "docs", copyMarkdown = false, openDocs = false, openDocsProviders, pageActionsPosition = "below-title", pageActionsAlignment = "left", githubUrl, githubBranch = "main", githubDirectory, lastModifiedMap, lastUpdatedEnabled = true, lastUpdatedPosition = "footer", llmsTxtEnabled = false, descriptionMap, description, children }) {
67
67
  const fdTocStyle = tocStyle === "directional" ? "clerk" : void 0;
68
68
  const [toc, setToc] = useState([]);
69
69
  const pathname = usePathname();
@@ -109,7 +109,7 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
109
109
  const lastModified = lastUpdatedEnabled ? lastModifiedMap?.[normalizedPath] : void 0;
110
110
  const showLastUpdatedBelowTitle = !!lastModified && lastUpdatedPosition === "below-title";
111
111
  const showLastUpdatedInFooter = !!lastModified && lastUpdatedPosition === "footer";
112
- const showFooter = !!githubFileUrl || showLastUpdatedInFooter;
112
+ const showFooter = !!githubFileUrl || showLastUpdatedInFooter || llmsTxtEnabled;
113
113
  const needsBelowTitleBlock = showLastUpdatedBelowTitle || showActions;
114
114
  useEffect(() => {
115
115
  if (!needsBelowTitleBlock) return;
@@ -188,10 +188,29 @@ function DocsPageClient({ tocEnabled, tocStyle = "default", breadcrumbEnabled =
188
188
  children
189
189
  }), showFooter && /* @__PURE__ */ jsxs("div", {
190
190
  className: "not-prose fd-page-footer",
191
- children: [githubFileUrl && /* @__PURE__ */ jsx(EditOnGitHub, { href: githubFileUrl }), showLastUpdatedInFooter && lastModified && /* @__PURE__ */ jsxs("span", {
192
- className: "fd-last-updated-footer",
193
- children: ["Last updated ", lastModified]
194
- })]
191
+ children: [
192
+ githubFileUrl && /* @__PURE__ */ jsx(EditOnGitHub, { href: githubFileUrl }),
193
+ llmsTxtEnabled && /* @__PURE__ */ jsxs("span", {
194
+ className: "fd-llms-txt-links",
195
+ children: [/* @__PURE__ */ jsx("a", {
196
+ href: "/api/docs?format=llms",
197
+ target: "_blank",
198
+ rel: "noopener noreferrer",
199
+ className: "fd-llms-txt-link",
200
+ children: "llms.txt"
201
+ }), /* @__PURE__ */ jsx("a", {
202
+ href: "/api/docs?format=llms-full",
203
+ target: "_blank",
204
+ rel: "noopener noreferrer",
205
+ className: "fd-llms-txt-link",
206
+ children: "llms-full.txt"
207
+ })]
208
+ }),
209
+ showLastUpdatedInFooter && lastModified && /* @__PURE__ */ jsxs("span", {
210
+ className: "fd-last-updated-footer",
211
+ children: ["Last updated ", lastModified]
212
+ })
213
+ ]
195
214
  })]
196
215
  })
197
216
  ]
package/dist/search.d.mts CHANGED
@@ -27,7 +27,7 @@ declare function createDocsSearchAPI(options?: {
27
27
  entry?: string;
28
28
  language?: string;
29
29
  }): {
30
- GET: (request: Request) => Promise<Response>;
30
+ GET(request: Request): Response | Promise<Response>;
31
31
  POST(request: Request): Promise<Response>;
32
32
  };
33
33
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/theme",
3
- "version": "0.0.2-beta.22",
3
+ "version": "0.0.2-beta.24",
4
4
  "description": "Theme package for @farming-labs/docs — layout, provider, MDX components, and styles",
5
5
  "keywords": [
6
6
  "docs",
@@ -98,7 +98,7 @@
98
98
  "next": ">=14.0.0",
99
99
  "tsdown": "^0.20.3",
100
100
  "typescript": "^5.9.3",
101
- "@farming-labs/docs": "0.0.2-beta.22"
101
+ "@farming-labs/docs": "0.0.2-beta.24"
102
102
  },
103
103
  "peerDependencies": {
104
104
  "@farming-labs/docs": ">=0.0.1",
package/styles/ai.css CHANGED
@@ -7,19 +7,6 @@
7
7
 
8
8
  /* ─── Animations ─────────────────────────────────────────────────── */
9
9
 
10
- @keyframes fd-ai-dot {
11
- 0%,
12
- 80%,
13
- 100% {
14
- transform: scale(0);
15
- opacity: 0.5;
16
- }
17
- 40% {
18
- transform: scale(1);
19
- opacity: 1;
20
- }
21
- }
22
-
23
10
  @keyframes fd-ai-fade-in {
24
11
  from {
25
12
  opacity: 0;
@@ -343,6 +330,12 @@
343
330
  line-height: 1.6;
344
331
  max-width: 95%;
345
332
  word-break: break-word;
333
+ animation: fd-ai-msg-in 300ms ease-out;
334
+ }
335
+
336
+ @keyframes fd-ai-msg-in {
337
+ from { opacity: 0; transform: translateY(6px); }
338
+ to { opacity: 1; transform: translateY(0); }
346
339
  }
347
340
 
348
341
  /* ─── Chat input ─────────────────────────────────────────────────── */
@@ -397,38 +390,274 @@
397
390
  color: var(--color-fd-primary-foreground, #fff);
398
391
  }
399
392
 
400
- /* ─── Loading indicator ──────────────────────────────────────────── */
393
+ /* ═══════════════════════════════════════════════════════════════════
394
+ * AI Loader variants — fd-ai-loader-*
395
+ * Default: "shimmer-dots" (shimmer text + typing dots in a row)
396
+ * ═══════════════════════════════════════════════════════════════════ */
401
397
 
402
- .fd-ai-loading {
398
+ .fd-ai-loader {
403
399
  display: inline-flex;
400
+ align-items: center;
404
401
  gap: 6px;
402
+ animation: fd-ai-loader-in 300ms ease-out;
403
+ }
404
+
405
+ @keyframes fd-ai-loader-in {
406
+ from { opacity: 0; transform: translateY(4px); }
407
+ to { opacity: 1; transform: translateY(0); }
408
+ }
409
+
410
+ /* ── shimmer-dots (default): shimmer text + typing dots ──────────── */
411
+
412
+ .fd-ai-loader-shimmer-text {
413
+ font-size: 13px;
414
+ font-weight: 500;
415
+ background: linear-gradient(
416
+ to right,
417
+ var(--color-fd-muted-foreground, #888) 40%,
418
+ var(--color-fd-foreground, #fff) 60%,
419
+ var(--color-fd-muted-foreground, #888) 80%
420
+ );
421
+ background-size: 200% auto;
422
+ background-clip: text;
423
+ -webkit-background-clip: text;
424
+ color: transparent;
425
+ animation: fd-ai-shimmer-text 3s linear infinite;
426
+ }
427
+
428
+ @keyframes fd-ai-shimmer-text {
429
+ 0% { background-position: 150% center; }
430
+ 100% { background-position: -150% center; }
431
+ }
432
+
433
+ .fd-ai-loader-typing-dots {
434
+ display: inline-flex;
405
435
  align-items: center;
436
+ gap: 2px;
406
437
  }
407
438
 
408
- .fd-ai-loading-text {
409
- font-size: 12px;
410
- color: var(--color-fd-muted-foreground, #888);
439
+ .fd-ai-loader-typing-dot {
440
+ width: 4px;
441
+ height: 4px;
442
+ border-radius: 50%;
443
+ background: var(--color-fd-primary, #6366f1);
444
+ animation: fd-ai-typing 1s infinite;
411
445
  }
412
446
 
413
- .fd-ai-loading-dots {
447
+ .fd-ai-loader-typing-dot:nth-child(2) { animation-delay: 250ms; }
448
+ .fd-ai-loader-typing-dot:nth-child(3) { animation-delay: 500ms; }
449
+
450
+ @keyframes fd-ai-typing {
451
+ 0%, 100% { transform: translateY(0); opacity: 0.5; }
452
+ 50% { transform: translateY(-2px); opacity: 1; }
453
+ }
454
+
455
+ /* ── circular: spinning ring ─────────────────────────────────────── */
456
+
457
+ .fd-ai-loader-circular {
458
+ width: 16px;
459
+ height: 16px;
460
+ border: 2px solid var(--color-fd-primary, #6366f1);
461
+ border-top-color: transparent;
462
+ border-radius: 50%;
463
+ animation: fd-ai-spin 0.8s linear infinite;
464
+ }
465
+
466
+ @keyframes fd-ai-spin {
467
+ to { transform: rotate(360deg); }
468
+ }
469
+
470
+ /* ── dots: bouncing dots ─────────────────────────────────────────── */
471
+
472
+ .fd-ai-loader-dots {
414
473
  display: inline-flex;
474
+ align-items: center;
415
475
  gap: 3px;
476
+ }
477
+
478
+ .fd-ai-loader-bounce-dot {
479
+ width: 6px;
480
+ height: 6px;
481
+ border-radius: 50%;
482
+ background: var(--color-fd-primary, #6366f1);
483
+ animation: fd-ai-bounce-dots 1.4s ease-in-out infinite;
484
+ }
485
+
486
+ .fd-ai-loader-bounce-dot:nth-child(2) { animation-delay: 160ms; }
487
+ .fd-ai-loader-bounce-dot:nth-child(3) { animation-delay: 320ms; }
488
+
489
+ @keyframes fd-ai-bounce-dots {
490
+ 0%, 100% { transform: scale(0.8); opacity: 0.5; }
491
+ 50% { transform: scale(1.2); opacity: 1; }
492
+ }
493
+
494
+ /* ── wave: wave bars ─────────────────────────────────────────────── */
495
+
496
+ .fd-ai-loader-wave {
497
+ display: inline-flex;
416
498
  align-items: center;
499
+ gap: 2px;
500
+ height: 16px;
417
501
  }
418
502
 
419
- .fd-ai-loading-dot {
420
- width: 5px;
421
- height: 5px;
503
+ .fd-ai-loader-wave-bar {
504
+ width: 2px;
505
+ border-radius: 2px;
506
+ background: var(--color-fd-primary, #6366f1);
507
+ animation: fd-ai-wave 1s ease-in-out infinite;
508
+ }
509
+
510
+ .fd-ai-loader-wave-bar:nth-child(1) { height: 6px; animation-delay: 0ms; }
511
+ .fd-ai-loader-wave-bar:nth-child(2) { height: 10px; animation-delay: 100ms; }
512
+ .fd-ai-loader-wave-bar:nth-child(3) { height: 14px; animation-delay: 200ms; }
513
+ .fd-ai-loader-wave-bar:nth-child(4) { height: 10px; animation-delay: 300ms; }
514
+ .fd-ai-loader-wave-bar:nth-child(5) { height: 6px; animation-delay: 400ms; }
515
+
516
+ @keyframes fd-ai-wave {
517
+ 0%, 100% { transform: scaleY(1); }
518
+ 50% { transform: scaleY(0.6); }
519
+ }
520
+
521
+ /* ── pulse: pulsing ring ─────────────────────────────────────────── */
522
+
523
+ .fd-ai-loader-pulse {
524
+ width: 16px;
525
+ height: 16px;
526
+ border: 2px solid var(--color-fd-primary, #6366f1);
422
527
  border-radius: 50%;
423
- background: var(--color-fd-muted-foreground, #888);
424
- animation: fd-ai-dot 1.4s infinite ease-in-out both;
528
+ animation: fd-ai-pulse 1.5s ease-in-out infinite;
529
+ }
530
+
531
+ @keyframes fd-ai-pulse {
532
+ 0%, 100% { transform: scale(0.95); opacity: 0.8; }
533
+ 50% { transform: scale(1.05); opacity: 0.4; }
534
+ }
535
+
536
+ /* ── pulse-dot: pulsing dot ──────────────────────────────────────── */
537
+
538
+ .fd-ai-loader-pulse-dot {
539
+ width: 8px;
540
+ height: 8px;
541
+ border-radius: 50%;
542
+ background: var(--color-fd-primary, #6366f1);
543
+ animation: fd-ai-pulse-dot 1.2s ease-in-out infinite;
544
+ }
545
+
546
+ @keyframes fd-ai-pulse-dot {
547
+ 0%, 100% { transform: scale(1); opacity: 0.8; }
548
+ 50% { transform: scale(1.5); opacity: 1; }
549
+ }
550
+
551
+ /* ── terminal: blinking cursor ───────────────────────────────────── */
552
+
553
+ .fd-ai-loader-terminal {
554
+ display: inline-flex;
555
+ align-items: center;
556
+ gap: 3px;
557
+ }
558
+
559
+ .fd-ai-loader-terminal-prompt {
560
+ font-family: var(--fd-font-mono, ui-monospace, monospace);
561
+ font-size: 13px;
562
+ color: var(--color-fd-primary, #6366f1);
563
+ }
564
+
565
+ .fd-ai-loader-terminal-cursor {
566
+ width: 7px;
567
+ height: 14px;
568
+ background: var(--color-fd-primary, #6366f1);
569
+ animation: fd-ai-blink 1s step-end infinite;
570
+ }
571
+
572
+ @keyframes fd-ai-blink {
573
+ 0%, 100% { opacity: 1; }
574
+ 50% { opacity: 0; }
575
+ }
576
+
577
+ /* ── text-shimmer: shimmer text only ─────────────────────────────── */
578
+ /* (reuses .fd-ai-loader-shimmer-text above) */
579
+
580
+ /* ── text-blink: blinking text ───────────────────────────────────── */
581
+
582
+ .fd-ai-loader-text-blink {
583
+ font-size: 13px;
584
+ font-weight: 500;
585
+ animation: fd-ai-text-blink 2s ease-in-out infinite;
586
+ }
587
+
588
+ @keyframes fd-ai-text-blink {
589
+ 0%, 100% { color: var(--color-fd-primary, #6366f1); }
590
+ 50% { color: var(--color-fd-muted-foreground, #888); }
591
+ }
592
+
593
+ /* ── loading-dots: "Thinking..." with animated dots ──────────────── */
594
+
595
+ .fd-ai-loader-text {
596
+ font-size: 13px;
597
+ font-weight: 500;
598
+ color: var(--color-fd-primary, #6366f1);
425
599
  }
426
600
 
427
- .fd-ai-loading-dot:nth-child(2) {
428
- animation-delay: 0.16s;
601
+ .fd-ai-loader-text-dots {
602
+ display: inline-flex;
429
603
  }
430
- .fd-ai-loading-dot:nth-child(3) {
431
- animation-delay: 0.32s;
604
+
605
+ .fd-ai-loader-text-dot {
606
+ font-size: 13px;
607
+ font-weight: 500;
608
+ color: var(--color-fd-primary, #6366f1);
609
+ animation: fd-ai-loading-dots 1.4s infinite;
610
+ }
611
+
612
+ .fd-ai-loader-text-dot:nth-child(1) { animation-delay: 200ms; }
613
+ .fd-ai-loader-text-dot:nth-child(2) { animation-delay: 400ms; }
614
+ .fd-ai-loader-text-dot:nth-child(3) { animation-delay: 600ms; }
615
+
616
+ @keyframes fd-ai-loading-dots {
617
+ 0%, 100% { opacity: 0; }
618
+ 50% { opacity: 1; }
619
+ }
620
+
621
+ /* ── bars: thick wave bars ───────────────────────────────────────── */
622
+
623
+ .fd-ai-loader-bars {
624
+ display: inline-flex;
625
+ align-items: stretch;
626
+ gap: 3px;
627
+ height: 16px;
628
+ }
629
+
630
+ .fd-ai-loader-bar {
631
+ width: 4px;
632
+ background: var(--color-fd-primary, #6366f1);
633
+ animation: fd-ai-wave-bars 1.2s ease-in-out infinite;
634
+ }
635
+
636
+ .fd-ai-loader-bar:nth-child(1) { animation-delay: 0s; }
637
+ .fd-ai-loader-bar:nth-child(2) { animation-delay: 0.2s; }
638
+ .fd-ai-loader-bar:nth-child(3) { animation-delay: 0.4s; }
639
+
640
+ @keyframes fd-ai-wave-bars {
641
+ 0%, 100% { transform: scaleY(1); opacity: 0.5; }
642
+ 50% { transform: scaleY(0.6); opacity: 1; }
643
+ }
644
+
645
+ /* ─── Streaming cursor ───────────────────────────────────────────── */
646
+
647
+ .fd-ai-streaming::after {
648
+ content: "";
649
+ display: inline-block;
650
+ width: 2px;
651
+ height: 1em;
652
+ background: var(--color-fd-primary, #6366f1);
653
+ margin-left: 2px;
654
+ vertical-align: text-bottom;
655
+ animation: fd-ai-cursor-blink 0.8s step-end infinite;
656
+ }
657
+
658
+ @keyframes fd-ai-cursor-blink {
659
+ 0%, 100% { opacity: 1; }
660
+ 50% { opacity: 0; }
432
661
  }
433
662
 
434
663
  /* ─── Floating trigger button ────────────────────────────────────── */
@@ -747,39 +976,7 @@
747
976
 
748
977
  /* ─── Thinking dots ──────────────────────────────────────────────── */
749
978
 
750
- .fd-ai-fm-thinking {
751
- display: flex;
752
- gap: 4px;
753
- align-items: center;
754
- }
755
-
756
- .fd-ai-fm-thinking-dot {
757
- width: 6px;
758
- height: 6px;
759
- border-radius: 9999px;
760
- background: var(--color-fd-primary, #6366f1);
761
- animation: fd-ai-fm-bounce 1s infinite ease-in-out;
762
- }
763
-
764
- .fd-ai-fm-thinking-dot:nth-child(2) {
765
- animation-delay: 150ms;
766
- }
767
- .fd-ai-fm-thinking-dot:nth-child(3) {
768
- animation-delay: 300ms;
769
- }
770
-
771
- @keyframes fd-ai-fm-bounce {
772
- 0%,
773
- 80%,
774
- 100% {
775
- transform: scale(0.6);
776
- opacity: 0.4;
777
- }
778
- 40% {
779
- transform: scale(1);
780
- opacity: 1;
781
- }
782
- }
979
+ /* Full-modal now uses the shared .fd-ai-loader indicator */
783
980
 
784
981
  /* ─── Bottom input bar ───────────────────────────────────────────── */
785
982
 
package/styles/base.css CHANGED
@@ -409,6 +409,29 @@ figure.shiki:has(figcaption) figcaption {
409
409
  margin-left: auto;
410
410
  }
411
411
 
412
+ .fd-llms-txt-links {
413
+ display: inline-flex;
414
+ align-items: center;
415
+ gap: 0.5rem;
416
+ }
417
+
418
+ .fd-llms-txt-link {
419
+ color: var(--color-fd-muted-foreground, hsl(0 0% 45%));
420
+ font-size: 0.75rem;
421
+ font-family: var(--fd-font-mono, ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace);
422
+ text-decoration: none;
423
+ padding: 0.125rem 0.375rem;
424
+ border-radius: 0.25rem;
425
+ border: 1px solid var(--color-fd-border, hsl(0 0% 80% / 50%));
426
+ transition: color 150ms, border-color 150ms;
427
+ }
428
+
429
+ .fd-llms-txt-link:hover {
430
+ /* color: var(--color-fd-foreground, hsl(0 0% 10%));
431
+ border-color: var(--color-fd-foreground, hsl(0 0% 10%)); */
432
+ text-decoration: none;
433
+ }
434
+
412
435
  /* ─── Code block copy button: show on hover ────────────────────────── */
413
436
 
414
437
  figure.shiki > button,
@@ -408,7 +408,7 @@ article a[class*="text-fd-muted-foreground"] {
408
408
  border-radius: 2px;
409
409
  }
410
410
 
411
- .fd-ai-fm-thinking-dot {
411
+ .fd-ai-loader-typing-dot {
412
412
  border-radius: 2px;
413
413
  }
414
414
 
@@ -425,6 +425,29 @@ figure.shiki > div:first-child {
425
425
  font-family: var(--fd-font-mono, var(--font-geist-mono, ui-monospace, monospace));
426
426
  text-transform: uppercase;
427
427
  }
428
+ /* llms.txt links */
429
+ .fd-llms-txt-links {
430
+ display: inline-flex;
431
+ align-items: center;
432
+ gap: 0.5rem;
433
+ }
434
+
435
+ .fd-llms-txt-link {
436
+ color: var(--color-fd-muted-foreground, hsl(0 0% 45%));
437
+ font-size: 0.65rem !important;
438
+ font-family: var(--fd-font-mono, ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace);
439
+ text-decoration: none;
440
+ padding: 0.015rem 0.4rem !important;
441
+ border-radius: 0px !important;
442
+ border: 0.5px solid var(--color-fd-border, hsl(0 0% 80% / 50%));
443
+ transition: color 150ms, border-color 150ms;
444
+ }
445
+
446
+ .fd-llms-txt-link:hover {
447
+ color: var(--color-fd-foreground, hsl(0 0% 10%)) !important;
448
+ border: 0.5px solid var(--color-fd-muted-foreground, hsl(0 0% 10% / 10%)) !important;
449
+ text-decoration: none;
450
+ }
428
451
 
429
452
  /* ─── Page Actions (pixel-border overrides) ───────────────────────── */
430
453
 
@@ -533,13 +556,13 @@ figure.shiki > div:first-child {
533
556
  letter-spacing: 0.06em;
534
557
  }
535
558
 
536
- .fd-ai-loading-text {
559
+ .fd-ai-loader-shimmer-text {
537
560
  text-transform: uppercase;
538
561
  letter-spacing: 0.04em;
539
562
  font-size: 11px;
540
563
  }
541
564
 
542
- .fd-ai-loading-dot {
565
+ .fd-ai-loader-typing-dot {
543
566
  border-radius: 0;
544
567
  width: 4px;
545
568
  height: 4px;
@@ -617,11 +640,7 @@ figure.shiki > div:first-child {
617
640
  font-size: 11px;
618
641
  }
619
642
 
620
- .fd-ai-fm-thinking-dot {
621
- border-radius: 0;
622
- width: 5px;
623
- height: 5px;
624
- }
643
+ /* Full-modal now uses .fd-ai-loader-typing-dot (see above) */
625
644
 
626
645
  /* ─── Code blocks (pixel-border) ─────────────────────────────────── */
627
646