@farming-labs/svelte-theme 0.2.35 → 0.2.37

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/svelte-theme",
3
- "version": "0.2.35",
3
+ "version": "0.2.37",
4
4
  "description": "Svelte UI components for @farming-labs/docs — layout, sidebar, TOC, search, and theme toggle",
5
5
  "keywords": [
6
6
  "docs",
@@ -122,8 +122,8 @@
122
122
  "dependencies": {
123
123
  "gray-matter": "^4.0.3",
124
124
  "sugar-high": "^0.9.5",
125
- "@farming-labs/docs": "0.2.35",
126
- "@farming-labs/svelte": "0.2.35"
125
+ "@farming-labs/svelte": "0.2.37",
126
+ "@farming-labs/docs": "0.2.37"
127
127
  },
128
128
  "peerDependencies": {
129
129
  "svelte": ">=5.0.0"
@@ -1,16 +1,39 @@
1
1
  /**
2
- * Markdown renderer for AI chat — exact port of the Next.js fumadocs version.
2
+ * Markdown renderer for AI chat.
3
3
  *
4
4
  * Uses sugar-high for syntax highlighting in code blocks.
5
- * Supports: fenced code blocks, tables, inline code, bold, italic,
6
- * links, headings, bullet lists, numbered lists.
5
+ * Supports fenced and streaming code blocks, loose language labels emitted by
6
+ * models, tables, inline code, emphasis, links, images, headings, blockquotes,
7
+ * horizontal rules, and lists.
7
8
  */
8
9
  import { highlight } from "sugar-high";
9
10
 
11
+ const codeBlockTokenBoundary = String.fromCharCode(0);
12
+
13
+ const looseCodeLanguageAliases = new Map([
14
+ ["bash", "bash"],
15
+ ["shell", "bash"],
16
+ ["sh", "bash"],
17
+ ["zsh", "bash"],
18
+ ["curl", "bash"],
19
+ ["javascript", "js"],
20
+ ["js", "js"],
21
+ ["typescript", "ts"],
22
+ ["ts", "ts"],
23
+ ["json", "json"],
24
+ ["python", "python"],
25
+ ["py", "python"],
26
+ ["http", "http"],
27
+ ]);
28
+
10
29
  function escapeHtml(s) {
11
30
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
12
31
  }
13
32
 
33
+ function escapeEscapedAttribute(s) {
34
+ return s.replace(/"/g, "&quot;");
35
+ }
36
+
14
37
  function buildCodeBlock(lang, code) {
15
38
  const trimmed = code.replace(/\n$/, "");
16
39
  const highlighted = highlight(trimmed).replace(/<\/span>\n<span/g, "</span><span");
@@ -24,102 +47,410 @@ function buildCodeBlock(lang, code) {
24
47
  );
25
48
  }
26
49
 
27
- function isTableRow(line) {
50
+ function getCodeFenceOpening(line) {
51
+ const match = /^(?: {0,3})(`{3,}|~{3,})(.*)$/.exec(line);
52
+ if (!match) return null;
53
+
54
+ const marker = match[1];
55
+ const rawInfo = match[2] ?? "";
56
+ if (!marker) return null;
57
+ if (marker.startsWith("`") && rawInfo.includes("`")) return null;
58
+
59
+ return {
60
+ markerChar: marker[0],
61
+ markerLength: marker.length,
62
+ lang: extractCodeFenceLanguage(rawInfo),
63
+ };
64
+ }
65
+
66
+ function isCodeFenceClosing(line, fence) {
28
67
  const trimmed = line.trim();
29
- return trimmed.startsWith("|") && trimmed.endsWith("|") && trimmed.includes("|");
68
+ if (trimmed.length < fence.markerLength) return false;
69
+
70
+ for (const char of trimmed) {
71
+ if (char !== fence.markerChar) return false;
72
+ }
73
+
74
+ return true;
30
75
  }
31
76
 
32
- function isTableSeparator(line) {
33
- return /^\|[\s:]*-+[\s:]*(\|[\s:]*-+[\s:]*)*\|$/.test(line.trim());
77
+ function extractCodeFenceLanguage(rawInfo) {
78
+ const firstToken = rawInfo.trim().split(/\s+/, 1)[0] ?? "";
79
+ return firstToken
80
+ .replace(/^\{\.?/, "")
81
+ .replace(/^\./, "")
82
+ .replace(/[},].*$/, "");
34
83
  }
35
84
 
36
- function renderTable(rows) {
37
- const parseRow = (row) =>
38
- row
39
- .trim()
40
- .replace(/^\|/, "")
41
- .replace(/\|$/, "")
42
- .split("|")
43
- .map((c) => c.trim());
85
+ function normalizeLooseCodeBlocks(text) {
86
+ const lines = text.split("\n");
87
+ const output = [];
88
+ let i = 0;
44
89
 
45
- const headerCells = parseRow(rows[0]);
46
- const thead = `<thead><tr>${headerCells.map((c) => `<th>${c}</th>`).join("")}</tr></thead>`;
90
+ while (i < lines.length) {
91
+ const lang = getLooseCodeLanguage(lines[i]);
92
+ if (!lang) {
93
+ output.push(lines[i]);
94
+ i += 1;
95
+ continue;
96
+ }
47
97
 
48
- const bodyRows = rows
49
- .slice(1)
50
- .map((row) => {
51
- const cells = parseRow(row);
52
- return `<tr>${cells.map((c) => `<td>${c}</td>`).join("")}</tr>`;
53
- })
54
- .join("");
98
+ const start = nextNonEmptyLineIndex(lines, i + 1);
99
+ if (start < 0 || !looksLikeLooseCodeLine(lines[start], lang)) {
100
+ output.push(lines[i]);
101
+ i += 1;
102
+ continue;
103
+ }
55
104
 
56
- return `<div class="fd-table-wrapper relative overflow-auto prose-no-margin my-6"><table>${thead}<tbody>${bodyRows}</tbody></table></div>`;
105
+ output.push(`\`\`\`${lang}`);
106
+ let j = start;
107
+ while (j < lines.length) {
108
+ if (j > start && isLooseCodeBoundary(lines, j, lang)) break;
109
+ output.push(lines[j]);
110
+ j += 1;
111
+ }
112
+ output.push("```");
113
+ i = j;
114
+ }
115
+
116
+ return output.join("\n");
117
+ }
118
+
119
+ function getLooseCodeLanguage(line) {
120
+ const raw = line.trim().toLowerCase();
121
+ if (!/^[a-z][\w#+.-]*$/.test(raw)) return null;
122
+ return looseCodeLanguageAliases.get(raw) ?? null;
123
+ }
124
+
125
+ function nextNonEmptyLineIndex(lines, startIndex) {
126
+ for (let i = startIndex; i < lines.length; i += 1) {
127
+ if (lines[i].trim()) return i;
128
+ }
129
+ return -1;
130
+ }
131
+
132
+ function isLooseCodeBoundary(lines, index, lang) {
133
+ const line = lines[index];
134
+ const trimmed = line.trim();
135
+ if (getHeading(trimmed) || isHorizontalRule(trimmed)) return true;
136
+
137
+ if (!trimmed) {
138
+ const next = nextNonEmptyLineIndex(lines, index + 1);
139
+ return next < 0 || !looksLikeLooseCodeLine(lines[next], lang);
140
+ }
141
+
142
+ return false;
143
+ }
144
+
145
+ function looksLikeLooseCodeLine(line, lang) {
146
+ const trimmed = line.trim();
147
+ if (!trimmed) return false;
148
+
149
+ if (lang === "json") return /^[{[]/.test(trimmed);
150
+ if (lang === "js" || lang === "ts") {
151
+ return /^(?:import|export|const|let|var|function|async|await|class|type|interface|return|console\.|fetch\(|npm|pnpm|yarn|bun)\b/.test(
152
+ trimmed,
153
+ );
154
+ }
155
+ if (lang === "python") {
156
+ return /^(?:from|import|def|class|if|for|while|print\(|python|pip)\b/.test(trimmed);
157
+ }
158
+ if (lang === "http") return /^(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+\S+/.test(trimmed);
159
+
160
+ return /^(?:[$>#]\s*)?(?:claude|opencode|curl|npx|pnpm|npm|yarn|bun|git|node|deno|python|pip|export|cd|cat|echo|mkdir|touch|rm|cp|mv|vercel|docs)\b|^(?:--|\||&&|\\)|^[A-Z_][A-Z0-9_]*=/.test(
161
+ trimmed,
162
+ );
163
+ }
164
+
165
+ function replaceFencedCodeBlocks(text, codeBlocks) {
166
+ const lines = text.split("\n");
167
+ const output = [];
168
+ let i = 0;
169
+
170
+ while (i < lines.length) {
171
+ const fence = getCodeFenceOpening(lines[i]);
172
+ if (!fence) {
173
+ output.push(lines[i]);
174
+ i += 1;
175
+ continue;
176
+ }
177
+
178
+ const codeLines = [];
179
+ i += 1;
180
+
181
+ while (i < lines.length && !isCodeFenceClosing(lines[i], fence)) {
182
+ codeLines.push(lines[i]);
183
+ i += 1;
184
+ }
185
+
186
+ if (i < lines.length) i += 1;
187
+
188
+ codeBlocks.push(buildCodeBlock(fence.lang, codeLines.join("\n")));
189
+ output.push(`${codeBlockTokenBoundary}CB${codeBlocks.length - 1}${codeBlockTokenBoundary}`);
190
+ }
191
+
192
+ return output.join("\n");
57
193
  }
58
194
 
59
195
  export function renderMarkdown(text) {
60
196
  if (!text) return "";
61
197
 
62
- const codeBlockTokenBoundary = String.fromCharCode(0);
63
- const codeBlockTokenPattern = new RegExp(
64
- `${codeBlockTokenBoundary}CB(\\d+)${codeBlockTokenBoundary}`,
65
- "g",
66
- );
67
198
  const codeBlocks = [];
199
+ const processed = replaceFencedCodeBlocks(normalizeLooseCodeBlocks(text), codeBlocks);
68
200
 
69
- // Complete fences: ```lang\n...\n```
70
- let processed = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
71
- codeBlocks.push(buildCodeBlock(lang, code));
72
- return `\x00CB${codeBlocks.length - 1}\x00`;
73
- });
74
-
75
- // Incomplete fence (streaming): opening ``` with no closing one
76
- processed = processed.replace(/```(\w*)\n([\s\S]*)$/, (_match, lang, code) => {
77
- codeBlocks.push(buildCodeBlock(lang, code));
78
- return `\x00CB${codeBlocks.length - 1}\x00`;
79
- });
201
+ return renderMarkdownBlocks(processed, codeBlocks);
202
+ }
80
203
 
81
- const lines = processed.split("\n");
204
+ function renderMarkdownBlocks(text, codeBlocks) {
205
+ const lines = text.split("\n");
82
206
  const output = [];
83
207
  let i = 0;
84
208
 
85
209
  while (i < lines.length) {
86
- if (isTableRow(lines[i]) && i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
87
- const tableLines = [lines[i]];
88
- i++; // separator
89
- i++; // move past separator
210
+ const line = lines[i];
211
+ const trimmed = line.trim();
212
+
213
+ if (!trimmed) {
214
+ i += 1;
215
+ continue;
216
+ }
217
+
218
+ const codeBlockIndex = getCodeBlockTokenIndex(trimmed);
219
+ if (codeBlockIndex !== null) {
220
+ output.push(codeBlocks[codeBlockIndex] ?? "");
221
+ i += 1;
222
+ continue;
223
+ }
224
+
225
+ if (isHorizontalRule(trimmed)) {
226
+ output.push('<hr class="fd-ai-hr" />');
227
+ i += 1;
228
+ continue;
229
+ }
230
+
231
+ const setextHeading = getSetextHeading(lines, i);
232
+ if (setextHeading) {
233
+ output.push(
234
+ `<h${setextHeading.level}>${renderInlineMarkdown(setextHeading.text)}</h${setextHeading.level}>`,
235
+ );
236
+ i += 2;
237
+ continue;
238
+ }
239
+
240
+ const heading = getHeading(trimmed);
241
+ if (heading) {
242
+ output.push(`<h${heading.level}>${renderInlineMarkdown(heading.text)}</h${heading.level}>`);
243
+ i += 1;
244
+ continue;
245
+ }
246
+
247
+ const blockquote = collectBlockquote(lines, i);
248
+ if (blockquote) {
249
+ output.push(
250
+ `<blockquote>${renderMarkdownBlocks(blockquote.lines.join("\n"), codeBlocks)}</blockquote>`,
251
+ );
252
+ i = blockquote.nextIndex;
253
+ continue;
254
+ }
255
+
256
+ if (isTableRow(line) && i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
257
+ const tableLines = [line];
258
+ i += 2;
90
259
  while (i < lines.length && isTableRow(lines[i])) {
91
260
  tableLines.push(lines[i]);
92
- i++;
261
+ i += 1;
93
262
  }
94
263
  output.push(renderTable(tableLines));
95
264
  continue;
96
265
  }
97
- output.push(lines[i]);
98
- i++;
266
+
267
+ const unorderedItems = collectListItems(lines, i, "unordered");
268
+ if (unorderedItems) {
269
+ output.push(`<ul>${unorderedItems.items.map((item) => renderListItem(item)).join("")}</ul>`);
270
+ i = unorderedItems.nextIndex;
271
+ continue;
272
+ }
273
+
274
+ const orderedItems = collectListItems(lines, i, "ordered");
275
+ if (orderedItems) {
276
+ output.push(`<ol>${orderedItems.items.map((item) => renderListItem(item)).join("")}</ol>`);
277
+ i = orderedItems.nextIndex;
278
+ continue;
279
+ }
280
+
281
+ const paragraphLines = [];
282
+ while (i < lines.length && lines[i].trim() && !isMarkdownBlockStart(lines, i)) {
283
+ paragraphLines.push(lines[i].trim());
284
+ i += 1;
285
+ }
286
+
287
+ output.push(
288
+ `<p>${paragraphLines.map((paragraphLine) => renderInlineMarkdown(paragraphLine)).join("<br>")}</p>`,
289
+ );
290
+ }
291
+
292
+ return output.join("");
293
+ }
294
+
295
+ function getCodeBlockTokenIndex(line) {
296
+ const prefix = `${codeBlockTokenBoundary}CB`;
297
+ if (!line.startsWith(prefix) || !line.endsWith(codeBlockTokenBoundary)) return null;
298
+
299
+ const rawIndex = line.slice(prefix.length, -codeBlockTokenBoundary.length);
300
+ if (!/^\d+$/.test(rawIndex)) return null;
301
+
302
+ return Number(rawIndex);
303
+ }
304
+
305
+ function getHeading(line) {
306
+ const match = /^(#{1,6})[ \t]+(.+?)(?:[ \t]+#+[ \t]*)?$/.exec(line);
307
+ if (!match) return null;
308
+
309
+ return {
310
+ level: match[1].length,
311
+ text: match[2],
312
+ };
313
+ }
314
+
315
+ function getSetextHeading(lines, index) {
316
+ if (index + 1 >= lines.length) return null;
317
+
318
+ const text = lines[index].trim();
319
+ const underline = lines[index + 1].trim();
320
+ if (!text || !/^(?:=+|-+)$/.test(underline)) return null;
321
+ if (getHeading(text) || isHorizontalRule(text) || isBlockquoteStart(text)) return null;
322
+ if (isTableRow(lines[index])) return null;
323
+ if (collectListItems(lines, index, "unordered") || collectListItems(lines, index, "ordered"))
324
+ return null;
325
+
326
+ return {
327
+ level: underline.startsWith("=") ? 1 : 2,
328
+ text,
329
+ };
330
+ }
331
+
332
+ function collectBlockquote(lines, startIndex) {
333
+ if (!isBlockquoteStart(lines[startIndex])) return null;
334
+
335
+ const quoteLines = [];
336
+ let i = startIndex;
337
+
338
+ while (i < lines.length) {
339
+ const match = /^(?: {0,3})>\s?(.*)$/.exec(lines[i]);
340
+ if (!match) break;
341
+
342
+ quoteLines.push(match[1]);
343
+ i += 1;
99
344
  }
100
345
 
101
- let result = output.join("\n");
346
+ return quoteLines.length > 0 ? { lines: quoteLines, nextIndex: i } : null;
347
+ }
348
+
349
+ function isBlockquoteStart(line) {
350
+ return /^(?: {0,3})>/.test(line);
351
+ }
352
+
353
+ function collectListItems(lines, startIndex, type) {
354
+ const pattern = type === "ordered" ? /^(?: {0,3})\d+\.\s+(.+)$/ : /^(?: {0,3})[-*+]\s+(.+)$/;
355
+ const items = [];
356
+ let i = startIndex;
357
+
358
+ while (i < lines.length) {
359
+ const match = pattern.exec(lines[i]);
360
+ if (!match) break;
361
+
362
+ items.push(match[1]);
363
+ i += 1;
364
+ }
365
+
366
+ return items.length > 0 ? { items, nextIndex: i } : null;
367
+ }
368
+
369
+ function renderListItem(item) {
370
+ const task = /^\[([ xX])\]\s+(.+)$/.exec(item);
371
+ if (!task) return `<li>${renderInlineMarkdown(item)}</li>`;
372
+
373
+ const checked = task[1].toLowerCase() === "x" ? " checked" : "";
374
+ return `<li class="fd-ai-task-list-item"><input class="fd-ai-task-checkbox" type="checkbox" disabled${checked}>${renderInlineMarkdown(task[2])}</li>`;
375
+ }
376
+
377
+ function isMarkdownBlockStart(lines, index) {
378
+ const line = lines[index];
379
+ const trimmed = line.trim();
380
+
381
+ return (
382
+ getCodeBlockTokenIndex(trimmed) !== null ||
383
+ isHorizontalRule(trimmed) ||
384
+ Boolean(getSetextHeading(lines, index)) ||
385
+ Boolean(getHeading(trimmed)) ||
386
+ isBlockquoteStart(line) ||
387
+ (isTableRow(line) && index + 1 < lines.length && isTableSeparator(lines[index + 1])) ||
388
+ Boolean(collectListItems(lines, index, "unordered")) ||
389
+ Boolean(collectListItems(lines, index, "ordered"))
390
+ );
391
+ }
392
+
393
+ function renderInlineMarkdown(text) {
394
+ const inlineCodeTokens = [];
395
+ let result = escapeHtml(text).replace(/`([^`]+)`/g, (_match, code) => {
396
+ inlineCodeTokens.push(`<code>${code}</code>`);
397
+ return `${codeBlockTokenBoundary}IC${inlineCodeTokens.length - 1}${codeBlockTokenBoundary}`;
398
+ });
102
399
 
103
400
  result = result
104
- .replace(/`([^`]+)`/g, "<code>$1</code>")
105
401
  .replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
402
+ .replace(/__(.*?)__/g, "<strong>$1</strong>")
403
+ .replace(/~~(.*?)~~/g, "<del>$1</del>")
106
404
  .replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "<em>$1</em>")
107
- .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
108
- .replace(/^### (.*$)/gm, "<h4>$1</h4>")
109
- .replace(/^## (.*$)/gm, "<h3>$1</h3>")
110
- .replace(/^# (.*$)/gm, "<h2>$1</h2>")
111
- .replace(
112
- /^[-*] (.*$)/gm,
113
- '<div style="display:flex;gap:8px;padding:2px 0"><span style="opacity:0.5">\u2022</span><span>$1</span></div>',
114
- )
115
- .replace(
116
- /^(\d+)\. (.*$)/gm,
117
- '<div style="display:flex;gap:8px;padding:2px 0"><span style="opacity:0.5">$1.</span><span>$2</span></div>',
118
- )
119
- .replace(/\n\n/g, '<div style="height:8px"></div>')
120
- .replace(/\n/g, "<br>");
121
-
122
- result = result.replace(codeBlockTokenPattern, (_m, idx) => codeBlocks[Number(idx)]);
123
-
124
- return result;
405
+ .replace(/(?<!_)_([^_]+)_(?!_)/g, "<em>$1</em>")
406
+ .replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+&quot;[^)]*&quot;)?\)/g, (_match, alt, src) => {
407
+ return `<img src="${escapeEscapedAttribute(src)}" alt="${escapeEscapedAttribute(alt)}" loading="lazy">`;
408
+ })
409
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, label, href) => {
410
+ return `<a href="${escapeEscapedAttribute(href)}">${label}</a>`;
411
+ })
412
+ .replace(/&lt;(https?:\/\/[^<>\s]+)&gt;/g, (_match, href) => {
413
+ return `<a href="${escapeEscapedAttribute(href)}">${href}</a>`;
414
+ });
415
+
416
+ return result.replace(
417
+ new RegExp(`${codeBlockTokenBoundary}IC(\\d+)${codeBlockTokenBoundary}`, "g"),
418
+ (_match, idx) => inlineCodeTokens[Number(idx)] ?? "",
419
+ );
420
+ }
421
+
422
+ function isHorizontalRule(line) {
423
+ return /^(?:-{3,}|\*{3,}|_{3,})$/.test(line);
424
+ }
425
+
426
+ function isTableRow(line) {
427
+ const trimmed = line.trim();
428
+ return trimmed.startsWith("|") && trimmed.endsWith("|") && trimmed.includes("|");
429
+ }
430
+
431
+ function isTableSeparator(line) {
432
+ return /^\|[\s:]*-+[\s:]*(\|[\s:]*-+[\s:]*)*\|$/.test(line.trim());
433
+ }
434
+
435
+ function renderTable(rows) {
436
+ const parseRow = (row) =>
437
+ row
438
+ .trim()
439
+ .replace(/^\|/, "")
440
+ .replace(/\|$/, "")
441
+ .split("|")
442
+ .map((c) => c.trim());
443
+
444
+ const headerCells = parseRow(rows[0]);
445
+ const thead = `<thead><tr>${headerCells.map((c) => `<th>${renderInlineMarkdown(c)}</th>`).join("")}</tr></thead>`;
446
+
447
+ const bodyRows = rows
448
+ .slice(1)
449
+ .map((row) => {
450
+ const cells = parseRow(row);
451
+ return `<tr>${cells.map((c) => `<td>${renderInlineMarkdown(c)}</td>`).join("")}</tr>`;
452
+ })
453
+ .join("");
454
+
455
+ return `<div class="fd-table-wrapper relative overflow-auto prose-no-margin my-6"><table>${thead}<tbody>${bodyRows}</tbody></table></div>`;
125
456
  }
package/styles/docs.css CHANGED
@@ -2634,20 +2634,38 @@ html.dark #nd-docs-layout pre.shiki {
2634
2634
  font-size: inherit;
2635
2635
  }
2636
2636
 
2637
+ #nd-docs-layout .fd-ai-bubble-ai .fd-table-wrapper {
2638
+ margin: 8px 0;
2639
+ overflow: auto;
2640
+ border: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.1));
2641
+ border-radius: var(--radius, 6px);
2642
+ }
2643
+
2637
2644
  #nd-docs-layout .fd-ai-bubble-ai table {
2638
2645
  width: 100%;
2639
- border-collapse: collapse;
2640
- margin: 8px 0;
2646
+ border-collapse: separate;
2647
+ border-spacing: 0;
2648
+ margin: 0;
2641
2649
  font-size: 13px;
2642
2650
  }
2643
2651
 
2644
2652
  #nd-docs-layout .fd-ai-bubble-ai th,
2645
2653
  #nd-docs-layout .fd-ai-bubble-ai td {
2646
- border: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.1));
2654
+ border: 0;
2655
+ border-right: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.1));
2656
+ border-bottom: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.1));
2647
2657
  padding: 6px 12px;
2648
2658
  text-align: left;
2649
2659
  }
2650
2660
 
2661
+ #nd-docs-layout .fd-ai-bubble-ai tr > :last-child {
2662
+ border-right: 0;
2663
+ }
2664
+
2665
+ #nd-docs-layout .fd-ai-bubble-ai tbody tr:last-child td {
2666
+ border-bottom: 0;
2667
+ }
2668
+
2651
2669
  #nd-docs-layout .fd-ai-bubble-ai th {
2652
2670
  background: var(--color-fd-muted, #1a1a2e);
2653
2671
  font-weight: 600;
@@ -2656,9 +2674,24 @@ html.dark #nd-docs-layout pre.shiki {
2656
2674
 
2657
2675
  /* ─── Markdown prose inside AI bubbles (panel/modal/popover) ─────── */
2658
2676
 
2677
+ #nd-docs-layout .fd-ai-bubble-ai h1,
2678
+ #nd-docs-layout .fd-ai-bubble-ai h2,
2679
+ #nd-docs-layout .fd-ai-bubble-ai h3,
2680
+ #nd-docs-layout .fd-ai-bubble-ai h4,
2681
+ #nd-docs-layout .fd-ai-bubble-ai h5,
2682
+ #nd-docs-layout .fd-ai-bubble-ai h6 {
2683
+ font-weight: 600;
2684
+ color: var(--color-fd-foreground, #e4e4e7);
2685
+ }
2686
+
2687
+ #nd-docs-layout .fd-ai-bubble-ai h1 {
2688
+ font-size: 1.25rem;
2689
+ margin: 14px 0 7px;
2690
+ line-height: 1.25;
2691
+ }
2692
+
2659
2693
  #nd-docs-layout .fd-ai-bubble-ai h2 {
2660
2694
  font-size: 1.125rem;
2661
- font-weight: 600;
2662
2695
  margin: 12px 0 6px;
2663
2696
  line-height: 1.3;
2664
2697
  }
@@ -2672,11 +2705,17 @@ html.dark #nd-docs-layout pre.shiki {
2672
2705
 
2673
2706
  #nd-docs-layout .fd-ai-bubble-ai h4 {
2674
2707
  font-size: 0.9375rem;
2675
- font-weight: 600;
2676
2708
  margin: 8px 0 4px;
2677
2709
  line-height: 1.4;
2678
2710
  }
2679
2711
 
2712
+ #nd-docs-layout .fd-ai-bubble-ai h5,
2713
+ #nd-docs-layout .fd-ai-bubble-ai h6 {
2714
+ font-size: 0.8125rem;
2715
+ margin: 8px 0 4px;
2716
+ line-height: 1.45;
2717
+ }
2718
+
2680
2719
  #nd-docs-layout .fd-ai-bubble-ai strong {
2681
2720
  font-weight: 600;
2682
2721
  color: var(--color-fd-foreground, #e4e4e7);
@@ -2696,6 +2735,58 @@ html.dark #nd-docs-layout pre.shiki {
2696
2735
  margin-top: 2px;
2697
2736
  }
2698
2737
 
2738
+ #nd-docs-layout .fd-ai-bubble-ai ul,
2739
+ #nd-docs-layout .fd-ai-bubble-ai ol {
2740
+ margin: 6px 0 8px;
2741
+ padding-left: 1.25rem;
2742
+ }
2743
+
2744
+ #nd-docs-layout .fd-ai-bubble-ai li {
2745
+ margin: 3px 0;
2746
+ }
2747
+
2748
+ #nd-docs-layout .fd-ai-bubble-ai blockquote {
2749
+ margin: 8px 0;
2750
+ padding: 6px 0 6px 12px;
2751
+ border-left: 2px solid var(--color-fd-border, rgba(255, 255, 255, 0.16));
2752
+ color: var(--color-fd-muted-foreground, #a1a1aa);
2753
+ }
2754
+
2755
+ #nd-docs-layout .fd-ai-bubble-ai blockquote p:last-child {
2756
+ margin-bottom: 0;
2757
+ }
2758
+
2759
+ #nd-docs-layout .fd-ai-bubble-ai .fd-ai-hr {
2760
+ margin: 12px 0;
2761
+ border: none;
2762
+ border-top: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.12));
2763
+ }
2764
+
2765
+ #nd-docs-layout .fd-ai-bubble-ai del {
2766
+ color: var(--color-fd-muted-foreground, #a1a1aa);
2767
+ }
2768
+
2769
+ #nd-docs-layout .fd-ai-bubble-ai .fd-ai-task-list-item {
2770
+ list-style: none;
2771
+ }
2772
+
2773
+ #nd-docs-layout .fd-ai-bubble-ai .fd-ai-task-checkbox {
2774
+ width: 0.875rem;
2775
+ height: 0.875rem;
2776
+ margin: 0 0.45rem 0 -1.25rem;
2777
+ vertical-align: -0.12em;
2778
+ accent-color: var(--color-fd-primary, #6366f1);
2779
+ }
2780
+
2781
+ #nd-docs-layout .fd-ai-bubble-ai img {
2782
+ display: block;
2783
+ max-width: 100%;
2784
+ height: auto;
2785
+ margin: 8px 0;
2786
+ border: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.12));
2787
+ border-radius: var(--radius, 4px);
2788
+ }
2789
+
2699
2790
  #nd-docs-layout .fd-ai-bubble-ai pre {
2700
2791
  margin: 8px 0;
2701
2792
  border-radius: 0;
@@ -3100,20 +3191,38 @@ html.dark #nd-docs-layout pre.shiki {
3100
3191
  text-decoration: underline;
3101
3192
  }
3102
3193
 
3194
+ #nd-docs-layout .fd-ai-fm-msg-content .fd-table-wrapper {
3195
+ margin: 8px 0;
3196
+ overflow: auto;
3197
+ border: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.1));
3198
+ border-radius: var(--radius, 6px);
3199
+ }
3200
+
3103
3201
  #nd-docs-layout .fd-ai-fm-msg-content table {
3104
3202
  width: 100%;
3105
- border-collapse: collapse;
3106
- margin: 8px 0;
3203
+ border-collapse: separate;
3204
+ border-spacing: 0;
3205
+ margin: 0;
3107
3206
  font-size: 13px;
3108
3207
  }
3109
3208
 
3110
3209
  #nd-docs-layout .fd-ai-fm-msg-content th,
3111
3210
  #nd-docs-layout .fd-ai-fm-msg-content td {
3112
- border: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.1));
3211
+ border: 0;
3212
+ border-right: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.1));
3213
+ border-bottom: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.1));
3113
3214
  padding: 6px 12px;
3114
3215
  text-align: left;
3115
3216
  }
3116
3217
 
3218
+ #nd-docs-layout .fd-ai-fm-msg-content tr > :last-child {
3219
+ border-right: 0;
3220
+ }
3221
+
3222
+ #nd-docs-layout .fd-ai-fm-msg-content tbody tr:last-child td {
3223
+ border-bottom: 0;
3224
+ }
3225
+
3117
3226
  #nd-docs-layout .fd-ai-fm-msg-content th {
3118
3227
  background: var(--color-fd-muted, #1a1a2e);
3119
3228
  font-weight: 600;
@@ -3122,9 +3231,24 @@ html.dark #nd-docs-layout pre.shiki {
3122
3231
 
3123
3232
  /* ─── Markdown prose inside full-modal messages ──────────────────── */
3124
3233
 
3234
+ #nd-docs-layout .fd-ai-fm-msg-content h1,
3235
+ #nd-docs-layout .fd-ai-fm-msg-content h2,
3236
+ #nd-docs-layout .fd-ai-fm-msg-content h3,
3237
+ #nd-docs-layout .fd-ai-fm-msg-content h4,
3238
+ #nd-docs-layout .fd-ai-fm-msg-content h5,
3239
+ #nd-docs-layout .fd-ai-fm-msg-content h6 {
3240
+ font-weight: 600;
3241
+ color: var(--color-fd-foreground, #e4e4e7);
3242
+ }
3243
+
3244
+ #nd-docs-layout .fd-ai-fm-msg-content h1 {
3245
+ font-size: 1.375rem;
3246
+ margin: 18px 0 9px;
3247
+ line-height: 1.25;
3248
+ }
3249
+
3125
3250
  #nd-docs-layout .fd-ai-fm-msg-content h2 {
3126
3251
  font-size: 1.25rem;
3127
- font-weight: 600;
3128
3252
  margin: 16px 0 8px;
3129
3253
  line-height: 1.3;
3130
3254
  }
@@ -3138,11 +3262,17 @@ html.dark #nd-docs-layout pre.shiki {
3138
3262
 
3139
3263
  #nd-docs-layout .fd-ai-fm-msg-content h4 {
3140
3264
  font-size: 1rem;
3141
- font-weight: 600;
3142
3265
  margin: 10px 0 4px;
3143
3266
  line-height: 1.4;
3144
3267
  }
3145
3268
 
3269
+ #nd-docs-layout .fd-ai-fm-msg-content h5,
3270
+ #nd-docs-layout .fd-ai-fm-msg-content h6 {
3271
+ font-size: 0.875rem;
3272
+ margin: 10px 0 4px;
3273
+ line-height: 1.45;
3274
+ }
3275
+
3146
3276
  #nd-docs-layout .fd-ai-fm-msg-content strong {
3147
3277
  font-weight: 600;
3148
3278
  color: var(--color-fd-foreground, #e4e4e7);
@@ -3162,6 +3292,58 @@ html.dark #nd-docs-layout pre.shiki {
3162
3292
  margin-top: 2px;
3163
3293
  }
3164
3294
 
3295
+ #nd-docs-layout .fd-ai-fm-msg-content ul,
3296
+ #nd-docs-layout .fd-ai-fm-msg-content ol {
3297
+ margin: 8px 0 10px;
3298
+ padding-left: 1.35rem;
3299
+ }
3300
+
3301
+ #nd-docs-layout .fd-ai-fm-msg-content li {
3302
+ margin: 4px 0;
3303
+ }
3304
+
3305
+ #nd-docs-layout .fd-ai-fm-msg-content blockquote {
3306
+ margin: 10px 0;
3307
+ padding: 7px 0 7px 14px;
3308
+ border-left: 2px solid var(--color-fd-border, rgba(255, 255, 255, 0.16));
3309
+ color: var(--color-fd-muted-foreground, #a1a1aa);
3310
+ }
3311
+
3312
+ #nd-docs-layout .fd-ai-fm-msg-content blockquote p:last-child {
3313
+ margin-bottom: 0;
3314
+ }
3315
+
3316
+ #nd-docs-layout .fd-ai-fm-msg-content .fd-ai-hr {
3317
+ margin: 14px 0;
3318
+ border: none;
3319
+ border-top: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.12));
3320
+ }
3321
+
3322
+ #nd-docs-layout .fd-ai-fm-msg-content del {
3323
+ color: var(--color-fd-muted-foreground, #a1a1aa);
3324
+ }
3325
+
3326
+ #nd-docs-layout .fd-ai-fm-msg-content .fd-ai-task-list-item {
3327
+ list-style: none;
3328
+ }
3329
+
3330
+ #nd-docs-layout .fd-ai-fm-msg-content .fd-ai-task-checkbox {
3331
+ width: 0.9rem;
3332
+ height: 0.9rem;
3333
+ margin: 0 0.5rem 0 -1.35rem;
3334
+ vertical-align: -0.12em;
3335
+ accent-color: var(--color-fd-primary, #6366f1);
3336
+ }
3337
+
3338
+ #nd-docs-layout .fd-ai-fm-msg-content img {
3339
+ display: block;
3340
+ max-width: 100%;
3341
+ height: auto;
3342
+ margin: 10px 0;
3343
+ border: 1px solid var(--color-fd-border, rgba(255, 255, 255, 0.12));
3344
+ border-radius: var(--radius, 4px);
3345
+ }
3346
+
3165
3347
  #nd-docs-layout .fd-ai-fm-msg-content pre {
3166
3348
  margin: 8px 0;
3167
3349
  border-radius: 0;