@farming-labs/astro-theme 0.0.3-beta.2 → 0.0.3-beta.3

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/astro-theme",
3
- "version": "0.0.3-beta.2",
3
+ "version": "0.0.3-beta.3",
4
4
  "description": "Astro UI components for @farming-labs/docs — layout, sidebar, TOC, search, and theme toggle",
5
5
  "keywords": [
6
6
  "astro",
@@ -79,8 +79,8 @@
79
79
  },
80
80
  "dependencies": {
81
81
  "sugar-high": "^0.9.5",
82
- "@farming-labs/docs": "0.0.3-beta.2",
83
- "@farming-labs/astro": "0.0.3-beta.2"
82
+ "@farming-labs/docs": "0.0.3-beta.3",
83
+ "@farming-labs/astro": "0.0.3-beta.3"
84
84
  },
85
85
  "peerDependencies": {
86
86
  "astro": ">=4.0.0"
@@ -27,6 +27,78 @@ const llmsTxtEnabled = (() => {
27
27
  if (typeof cfg === "object" && cfg !== null) return cfg.enabled !== false;
28
28
  return false;
29
29
  })();
30
+
31
+ const copyMarkdownEnabled = (() => {
32
+ const pa = config?.pageActions;
33
+ if (!pa) return false;
34
+ const cm = pa.copyMarkdown;
35
+ if (cm === true) return true;
36
+ if (typeof cm === "object" && cm !== null) return cm.enabled !== false;
37
+ return false;
38
+ })();
39
+
40
+ const openDocsEnabled = (() => {
41
+ const pa = config?.pageActions;
42
+ if (!pa) return false;
43
+ const od = pa.openDocs;
44
+ if (od === true) return true;
45
+ if (typeof od === "object" && od !== null) return od.enabled !== false;
46
+ return false;
47
+ })();
48
+
49
+ const DEFAULT_OPEN_PROVIDERS = [
50
+ { name: "ChatGPT", urlTemplate: "https://chatgpt.com/?hints=search&q=Read+{mdxUrl},+I+want+to+ask+questions+about+it." },
51
+ { name: "Claude", urlTemplate: "https://claude.ai/new?q=Read+{mdxUrl},+I+want+to+ask+questions+about+it." },
52
+ ];
53
+
54
+ const openDocsProviders = (() => {
55
+ const pa = config?.pageActions;
56
+ const od = pa && typeof pa === "object" && pa.openDocs != null ? pa.openDocs : null;
57
+ const list =
58
+ od && typeof od === "object" && "providers" in od
59
+ ? (od as { providers?: Array<{ name?: string; urlTemplate?: string }> }).providers
60
+ : undefined;
61
+ if (Array.isArray(list) && list.length > 0) {
62
+ const mapped = list
63
+ .map((p) => ({
64
+ name: typeof p?.name === "string" ? p.name : "Open",
65
+ urlTemplate: typeof (p as { urlTemplate?: string })?.urlTemplate === "string" ? (p as { urlTemplate: string }).urlTemplate : "",
66
+ }))
67
+ .filter((p) => p.urlTemplate.length > 0);
68
+ if (mapped.length > 0) return mapped;
69
+ }
70
+ return DEFAULT_OPEN_PROVIDERS;
71
+ })();
72
+
73
+ const pathname = Astro.url.pathname;
74
+ const githubFileUrl = config?.github && data.editOnGithub ? data.editOnGithub : null;
75
+
76
+ const pageActionsPosition = (() => {
77
+ const pa = config?.pageActions;
78
+ if (typeof pa === "object" && pa !== null && pa.position) return pa.position;
79
+ return "below-title";
80
+ })();
81
+
82
+ const pageActionsAlignment = (() => {
83
+ const pa = config?.pageActions;
84
+ if (typeof pa === "object" && pa !== null && pa.alignment) return pa.alignment;
85
+ return "left";
86
+ })();
87
+
88
+ const lastUpdatedConfig = (() => {
89
+ const lu = config?.lastUpdated;
90
+ if (lu === false) return { enabled: false, position: "footer" as const };
91
+ if (lu === true || lu === undefined) return { enabled: true, position: "footer" as const };
92
+ return {
93
+ enabled: (lu as { enabled?: boolean }).enabled !== false,
94
+ position: ((lu as { position?: "footer" | "below-title" }).position ?? "footer") as "footer" | "below-title",
95
+ };
96
+ })();
97
+
98
+ const showLastUpdatedInFooter = !!data.lastModified && lastUpdatedConfig.enabled && lastUpdatedConfig.position === "footer";
99
+ const showLastUpdatedBelowTitle = !!data.lastModified && lastUpdatedConfig.enabled && lastUpdatedConfig.position === "below-title";
100
+
101
+ const htmlWithoutFirstH1 = (data.html || "").replace(/<h1[^>]*>[\s\S]*?<\/h1>\s*/i, "");
30
102
  ---
31
103
 
32
104
  <head>
@@ -42,9 +114,197 @@ const llmsTxtEnabled = (() => {
42
114
  previousPage={data.previousPage}
43
115
  nextPage={data.nextPage}
44
116
  editOnGithub={showEditOnGithub ? data.editOnGithub : null}
45
- lastModified={showLastModified ? data.lastModified : null}
117
+ lastModified={showLastUpdatedInFooter ? data.lastModified : null}
46
118
  llmsTxtEnabled={llmsTxtEnabled}
47
119
  >
120
+ {pageActionsPosition === "above-title" && (copyMarkdownEnabled || openDocsEnabled) && (
121
+ <div class="fd-page-actions" data-page-actions data-actions-alignment={pageActionsAlignment}>
122
+ {copyMarkdownEnabled && (
123
+ <>
124
+ {"rawMarkdown" in data && data.rawMarkdown != null && typeof data.rawMarkdown === "string" && (
125
+ <textarea id="fd-page-raw-markdown" hidden aria-hidden="true" readonly>{data.rawMarkdown}</textarea>
126
+ )}
127
+ <button type="button" class="fd-page-action-btn" data-copy-page aria-label="Copy page content">
128
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
129
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
130
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
131
+ </svg>
132
+ <span data-copy-label>Copy page</span>
133
+ </button>
134
+ </>
135
+ )}
136
+ {openDocsEnabled && openDocsProviders.length > 0 && (
137
+ <div class="fd-page-action-dropdown" data-open-dropdown>
138
+ <button type="button" class="fd-page-action-btn" aria-expanded="false" aria-haspopup="true" data-dropdown-trigger>
139
+ <span>Open in</span>
140
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
141
+ <polyline points="6 9 12 15 18 9" />
142
+ </svg>
143
+ </button>
144
+ <div class="fd-page-action-menu" role="menu" hidden data-dropdown-menu>
145
+ {openDocsProviders.map((provider) => (
146
+ <a
147
+ role="menuitem"
148
+ class="fd-page-action-menu-item"
149
+ href="#"
150
+ data-open-provider
151
+ data-name={provider.name}
152
+ data-url-template={provider.urlTemplate}
153
+ >
154
+ <span class="fd-page-action-menu-label">Open in {provider.name}</span>
155
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
156
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
157
+ <polyline points="15 3 21 3 21 9" />
158
+ <line x1="10" y1="14" x2="21" y2="3" />
159
+ </svg>
160
+ </a>
161
+ ))}
162
+ </div>
163
+ </div>
164
+ )}
165
+ </div>
166
+ )}
167
+ <h1 class="fd-page-title">{data.title}</h1>
48
168
  {data.description && <p class="fd-page-description">{data.description}</p>}
49
- <Fragment set:html={data.html} />
169
+ {showLastUpdatedBelowTitle && data.lastModified && (
170
+ <p class="fd-last-modified fd-last-modified-below-title">Last updated: {data.lastModified}</p>
171
+ )}
172
+ {pageActionsPosition === "below-title" && (copyMarkdownEnabled || openDocsEnabled) && (
173
+ <>
174
+ <hr class="fd-page-actions-divider" aria-hidden="true" />
175
+ <div class="fd-page-actions" data-page-actions data-actions-alignment={pageActionsAlignment}>
176
+ {copyMarkdownEnabled && (
177
+ <>
178
+ {"rawMarkdown" in data && data.rawMarkdown != null && typeof data.rawMarkdown === "string" && (
179
+ <textarea id="fd-page-raw-markdown" hidden aria-hidden="true" readonly>{data.rawMarkdown}</textarea>
180
+ )}
181
+ <button type="button" class="fd-page-action-btn" data-copy-page aria-label="Copy page content">
182
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
183
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
184
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
185
+ </svg>
186
+ <span data-copy-label>Copy page</span>
187
+ </button>
188
+ </>
189
+ )}
190
+ {openDocsEnabled && openDocsProviders.length > 0 && (
191
+ <div class="fd-page-action-dropdown" data-open-dropdown>
192
+ <button type="button" class="fd-page-action-btn" aria-expanded="false" aria-haspopup="true" data-dropdown-trigger>
193
+ <span>Open in</span>
194
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
195
+ <polyline points="6 9 12 15 18 9" />
196
+ </svg>
197
+ </button>
198
+ <div class="fd-page-action-menu" role="menu" hidden data-dropdown-menu>
199
+ {openDocsProviders.map((provider) => (
200
+ <a
201
+ role="menuitem"
202
+ class="fd-page-action-menu-item"
203
+ href="#"
204
+ data-open-provider
205
+ data-name={provider.name}
206
+ data-url-template={provider.urlTemplate}
207
+ >
208
+ <span class="fd-page-action-menu-label">Open in {provider.name}</span>
209
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
210
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
211
+ <polyline points="15 3 21 3 21 9" />
212
+ <line x1="10" y1="14" x2="21" y2="3" />
213
+ </svg>
214
+ </a>
215
+ ))}
216
+ </div>
217
+ </div>
218
+ )}
219
+ </div>
220
+ </>
221
+ )}
222
+ <Fragment set:html={htmlWithoutFirstH1} />
50
223
  </DocsPage>
224
+
225
+ {(copyMarkdownEnabled || openDocsEnabled) && (
226
+ <script define:vars={{ pathname, openDocsProviders, githubFileUrl }}>
227
+ (function () {
228
+ var pathnameVal = typeof pathname === "string" ? pathname : "";
229
+ var providers = Array.isArray(openDocsProviders) ? openDocsProviders : [];
230
+ var githubFileUrlVal = githubFileUrl || "";
231
+
232
+ function init() {
233
+ document.querySelectorAll("[data-copy-page]").forEach(function (btn) {
234
+ btn.addEventListener("click", function () {
235
+ var text = "";
236
+ var rawEl = document.getElementById("fd-page-raw-markdown");
237
+ if (rawEl && rawEl.value && rawEl.value.length > 0) {
238
+ text = rawEl.value;
239
+ } else {
240
+ var article = document.querySelector("#nd-page");
241
+ if (article) text = article.innerText || "";
242
+ }
243
+ if (!text) return;
244
+ var label = btn.querySelector("[data-copy-label]");
245
+ navigator.clipboard.writeText(text).then(
246
+ function () {
247
+ if (label) label.textContent = "Copied!";
248
+ btn.setAttribute("data-copied", "true");
249
+ setTimeout(function () {
250
+ if (label) label.textContent = "Copy page";
251
+ btn.removeAttribute("data-copied");
252
+ }, 2000);
253
+ },
254
+ function () {
255
+ if (label) label.textContent = "Copy failed";
256
+ setTimeout(function () {
257
+ if (label) label.textContent = "Copy page";
258
+ }, 2000);
259
+ }
260
+ );
261
+ });
262
+ });
263
+
264
+ document.querySelectorAll("[data-dropdown-trigger]").forEach(function (trigger) {
265
+ var dropdown = trigger.closest("[data-open-dropdown]");
266
+ var menu = dropdown ? dropdown.querySelector("[data-dropdown-menu]") : null;
267
+ if (!menu) return;
268
+
269
+ trigger.addEventListener("click", function (e) {
270
+ e.preventDefault();
271
+ var open = menu.hidden;
272
+ menu.hidden = !open;
273
+ trigger.setAttribute("aria-expanded", String(!open));
274
+ });
275
+
276
+ document.addEventListener("click", function (e) {
277
+ if (!menu.hidden && dropdown && !dropdown.contains(e.target)) {
278
+ menu.hidden = true;
279
+ trigger.setAttribute("aria-expanded", "false");
280
+ }
281
+ });
282
+ });
283
+
284
+ document.querySelectorAll("[data-open-provider]").forEach(function (el) {
285
+ el.addEventListener("click", function (e) {
286
+ e.preventDefault();
287
+ var urlTemplate = el.getAttribute("data-url-template") || "";
288
+ var pageUrl = window.location.href;
289
+ var mdxUrl = window.location.origin + pathnameVal + (pathnameVal.endsWith("/") ? "page.mdx" : ".mdx");
290
+ var url = urlTemplate
291
+ .replace(/\{url\}/g, encodeURIComponent(pageUrl))
292
+ .replace(/\{mdxUrl\}/g, encodeURIComponent(mdxUrl))
293
+ .replace(/\{githubUrl\}/g, githubFileUrlVal);
294
+ window.open(url, "_blank", "noopener,noreferrer");
295
+ var menu = el.closest("[data-dropdown-menu]");
296
+ var triggerEl = document.querySelector("[data-dropdown-trigger]");
297
+ if (menu) menu.hidden = true;
298
+ if (triggerEl) triggerEl.setAttribute("aria-expanded", "false");
299
+ });
300
+ });
301
+ }
302
+
303
+ if (document.readyState === "loading") {
304
+ document.addEventListener("DOMContentLoaded", init);
305
+ } else {
306
+ init();
307
+ }
308
+ })();
309
+ </script>
310
+ )}
@@ -2,7 +2,10 @@
2
2
  import ThemeToggle from "./ThemeToggle.astro";
3
3
  import FloatingAIChat from "./FloatingAIChat.astro";
4
4
 
5
- const { tree, config = null, title, titleUrl } = Astro.props;
5
+ const { tree, config = null, title, titleUrl, flatPages = null } = Astro.props;
6
+
7
+ const sidebarFlat = !!(config?.sidebar && typeof config.sidebar === "object" && (config.sidebar as { flat?: boolean }).flat);
8
+ const useFlatSidebar = sidebarFlat && Array.isArray(flatPages) && flatPages.length > 0;
6
9
 
7
10
  const resolvedTitle = title ?? config?.nav?.title ?? "Docs";
8
11
  const resolvedTitleUrl = titleUrl ?? config?.nav?.url ?? "/docs";
@@ -192,6 +195,22 @@ const showFloatingAI = aiConfig?.mode === "floating" && aiConfig?.enabled;
192
195
  <nav class="fd-sidebar-nav">
193
196
  <slot name="sidebar" />
194
197
  </nav>
198
+ ) : useFlatSidebar ? (
199
+ <nav class="fd-sidebar-nav">
200
+ {flatPages.map((page, i) => {
201
+ const icon = getIcon(page.icon);
202
+ return (
203
+ <a
204
+ href={page.url}
205
+ class={`fd-sidebar-link fd-sidebar-top-link ${isActive(page.url) ? 'fd-sidebar-link-active' : ''} ${i === 0 ? 'fd-sidebar-first-item' : ''}`}
206
+ data-active={isActive(page.url) || undefined}
207
+ >
208
+ {icon && <span class="fd-sidebar-icon" set:html={icon} />}
209
+ {page.name}
210
+ </a>
211
+ );
212
+ })}
213
+ </nav>
195
214
  ) : (
196
215
  <nav class="fd-sidebar-nav">
197
216
  {tree?.children?.map((node, i) => {
@@ -334,29 +353,31 @@ const showFloatingAI = aiConfig?.mode === "floating" && aiConfig?.enabled;
334
353
  overlay!.style.display = 'none';
335
354
  });
336
355
 
337
- // Search Cmd+K
338
- document.addEventListener('keydown', (e) => {
339
- if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
340
- e.preventDefault();
341
- const dialog = document.getElementById('fd-search-dialog');
342
- if (dialog) dialog.style.display = dialog.style.display === 'none' ? 'flex' : 'none';
356
+ // Search Cmd+K — lock body scroll when open, always restore on close
357
+ function setSearchOpen(open: boolean) {
358
+ const dialog = document.getElementById('fd-search-dialog');
359
+ if (dialog) {
360
+ dialog.style.display = open ? 'flex' : 'none';
361
+ document.body.style.overflow = open ? 'hidden' : '';
343
362
  }
363
+ }
364
+
365
+ document.addEventListener('keydown', (e) => {
344
366
  if (e.key === 'Escape') {
345
367
  sidebar?.classList.remove('fd-sidebar-open');
346
368
  overlay!.style.display = 'none';
347
- const dialog = document.getElementById('fd-search-dialog');
348
- if (dialog) dialog.style.display = 'none';
369
+ setSearchOpen(false);
370
+ document.body.style.overflow = '';
349
371
  }
350
372
  });
351
373
 
352
374
  document.getElementById('fd-search-open')?.addEventListener('click', () => {
353
- const dialog = document.getElementById('fd-search-dialog');
354
- if (dialog) dialog.style.display = 'flex';
375
+ if (typeof (window as any).__fdOmniSearchSetOpen === 'function') (window as any).__fdOmniSearchSetOpen(true);
376
+ else setSearchOpen(true);
355
377
  });
356
-
357
378
  document.getElementById('fd-search-open-mobile')?.addEventListener('click', () => {
358
- const dialog = document.getElementById('fd-search-dialog');
359
- if (dialog) dialog.style.display = 'flex';
379
+ if (typeof (window as any).__fdOmniSearchSetOpen === 'function') (window as any).__fdOmniSearchSetOpen(true);
380
+ else setSearchOpen(true);
360
381
  });
361
382
 
362
383
  // Close sidebar on link click (mobile)
@@ -35,7 +35,9 @@ const parentUrl = segments.length >= 2
35
35
  )}
36
36
 
37
37
  <div class="fd-page-body">
38
- <slot />
38
+ <div class="fd-docs-content">
39
+ <slot />
40
+ </div>
39
41
  </div>
40
42
 
41
43
  <footer class="fd-page-footer">
@@ -92,7 +94,7 @@ const parentUrl = segments.length >= 2
92
94
  </article>
93
95
 
94
96
  {tocEnabled && (
95
- <aside class="fd-toc">
97
+ <aside class="fd-toc" id="fd-toc" data-toc-ready="false">
96
98
  <div class={`fd-toc-inner ${tocStyle === "directional" ? "fd-toc-directional" : ""}`} data-toc-style={tocStyle}>
97
99
  <h3 class="fd-toc-title">
98
100
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
@@ -122,9 +124,9 @@ const parentUrl = segments.length >= 2
122
124
  function initDocsPage() {
123
125
  const container = document.querySelector('.fd-page-body');
124
126
  const tocList = document.getElementById('fd-toc-list');
127
+ const tocAside = document.getElementById('fd-toc');
125
128
  if (container && tocList) {
126
129
  const headings = container.querySelectorAll('h2[id], h3[id], h4[id]');
127
- tocList.innerHTML = '';
128
130
  const isClerk = tocList.closest('[data-toc-style]')?.getAttribute('data-toc-style') === 'directional';
129
131
 
130
132
  const tocItems = [];
@@ -136,6 +138,7 @@ const parentUrl = segments.length >= 2
136
138
  });
137
139
  });
138
140
 
141
+ const fragment = document.createDocumentFragment();
139
142
  tocItems.forEach((item, index) => {
140
143
  const li = document.createElement('li');
141
144
  li.className = 'fd-toc-item';
@@ -189,9 +192,11 @@ const parentUrl = segments.length >= 2
189
192
  }
190
193
 
191
194
  li.appendChild(a);
192
- tocList.appendChild(li);
195
+ fragment.appendChild(li);
193
196
  });
194
197
 
198
+ tocList.replaceChildren(fragment);
199
+
195
200
  let maskEl = null;
196
201
  let thumbEl = null;
197
202
 
@@ -275,6 +280,31 @@ const parentUrl = segments.length >= 2
275
280
  }
276
281
 
277
282
  const activeSet = new Set();
283
+ let tocUpdateScheduled = false;
284
+
285
+ function scheduleTocActiveUpdate() {
286
+ if (tocUpdateScheduled) return;
287
+ tocUpdateScheduled = true;
288
+ requestAnimationFrame(() => {
289
+ tocUpdateScheduled = false;
290
+ if (isClerk) {
291
+ tocList.querySelectorAll('.fd-toc-clerk-link').forEach(link => {
292
+ const id = link.getAttribute('href')?.slice(1);
293
+ if (activeSet.has(id)) {
294
+ link.setAttribute('data-active', 'true');
295
+ } else {
296
+ link.removeAttribute('data-active');
297
+ }
298
+ });
299
+ if (typeof updateThumb === 'function') updateThumb(activeSet);
300
+ } else {
301
+ tocList.querySelectorAll('.fd-toc-link').forEach(link => {
302
+ const id = link.getAttribute('href')?.slice(1);
303
+ link.classList.toggle('fd-toc-link-active', activeSet.has(id));
304
+ });
305
+ }
306
+ });
307
+ }
278
308
 
279
309
  const observer = new IntersectionObserver((entries) => {
280
310
  for (const entry of entries) {
@@ -284,26 +314,12 @@ const parentUrl = segments.length >= 2
284
314
  activeSet.delete(entry.target.id);
285
315
  }
286
316
  }
287
-
288
- if (isClerk) {
289
- tocList.querySelectorAll('.fd-toc-clerk-link').forEach(link => {
290
- const id = link.getAttribute('href')?.slice(1);
291
- if (activeSet.has(id)) {
292
- link.setAttribute('data-active', 'true');
293
- } else {
294
- link.removeAttribute('data-active');
295
- }
296
- });
297
- if (typeof updateThumb === 'function') updateThumb(activeSet);
298
- } else {
299
- tocList.querySelectorAll('.fd-toc-link').forEach(link => {
300
- const id = link.getAttribute('href')?.slice(1);
301
- link.classList.toggle('fd-toc-link-active', activeSet.has(id));
302
- });
303
- }
317
+ scheduleTocActiveUpdate();
304
318
  }, { rootMargin: '-80px 0px -80% 0px' });
305
319
 
306
320
  headings.forEach(el => observer.observe(el));
321
+
322
+ if (tocAside) tocAside.setAttribute('data-toc-ready', 'true');
307
323
  }
308
324
 
309
325
  // Copy buttons
@@ -340,6 +356,17 @@ const parentUrl = segments.length >= 2
340
356
  });
341
357
  }
342
358
 
343
- initDocsPage();
344
- document.addEventListener('astro:after-swap', initDocsPage);
359
+ function runTocWhenIdle() {
360
+ if (typeof requestIdleCallback !== 'undefined') {
361
+ requestIdleCallback(() => initDocsPage(), { timeout: 300 });
362
+ } else {
363
+ setTimeout(initDocsPage, 0);
364
+ }
365
+ }
366
+ if (document.readyState === 'loading') {
367
+ document.addEventListener('DOMContentLoaded', runTocWhenIdle);
368
+ } else {
369
+ runTocWhenIdle();
370
+ }
371
+ document.addEventListener('astro:after-swap', () => setTimeout(initDocsPage, 0));
345
372
  </script>
@@ -37,6 +37,7 @@ function getContainerStyle(style: string, pos: string) {
37
37
 
38
38
  const btnStyle = BTN_POSITIONS[position] || BTN_POSITIONS["bottom-right"];
39
39
  const containerStyle = getContainerStyle(floatingStyle, position);
40
+ const dialogAnimation = floatingStyle === "modal" ? "fd-ai-modal-in" : "fd-ai-float-in";
40
41
  ---
41
42
 
42
43
  <div id="fd-float-config" data-api={api} data-ai-label={aiLabel} data-full-modal={isFullModal ? "true" : "false"} style="display:none"></div>
@@ -114,7 +115,7 @@ const containerStyle = getContainerStyle(floatingStyle, position);
114
115
  )}
115
116
 
116
117
  <!-- Panel/modal/popover container -->
117
- <div class="fd-ai-dialog" id="fd-float-dialog" style={`display:none;${containerStyle};animation:fd-ai-float-in 200ms ease-out`}>
118
+ <div class="fd-ai-dialog" id="fd-float-dialog" style={`display:none;${containerStyle};animation:${dialogAnimation} 200ms ease-out`}>
118
119
  <div class="fd-ai-header">
119
120
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
120
121
  <path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/>
@@ -278,6 +279,7 @@ const containerStyle = getContainerStyle(floatingStyle, position);
278
279
  let isStreaming = false;
279
280
  let isOpen = false;
280
281
  let renderScheduled = false;
282
+ let streamRenderThrottle: ReturnType<typeof setTimeout> | null = null;
281
283
 
282
284
  const overlay = document.getElementById('fd-float-overlay');
283
285
  const bar = document.getElementById('fd-float-bar');
@@ -309,18 +311,19 @@ const containerStyle = getContainerStyle(floatingStyle, position);
309
311
  if (floatDialog) floatDialog.style.display = '';
310
312
  if (floatBtn) floatBtn.style.display = 'none';
311
313
  if (modalBg) modalBg.style.display = '';
314
+ document.body.style.overflow = 'hidden';
312
315
  setTimeout(() => input?.focus(), 100);
313
316
  }
314
317
  }
315
318
 
316
319
  function closeChat() {
317
320
  isOpen = false;
321
+ document.body.style.overflow = '';
318
322
  if (isFullModal) {
319
323
  if (overlay) overlay.style.display = 'none';
320
324
  if (bar) { bar.classList.remove('fd-ai-fm-input-bar--open'); bar.classList.add('fd-ai-fm-input-bar--closed'); bar.style.cssText = bar.dataset.btnStyle || ''; }
321
325
  if (trigger) trigger.style.display = '';
322
326
  if (inputContainer) inputContainer.style.display = 'none';
323
- document.body.style.overflow = '';
324
327
  } else {
325
328
  if (floatDialog) floatDialog.style.display = 'none';
326
329
  if (floatBtn) floatBtn.style.display = '';
@@ -356,6 +359,20 @@ const containerStyle = getContainerStyle(floatingStyle, position);
356
359
  if (footerHint) footerHint.style.display = 'none';
357
360
  if (clearWrap) clearWrap.style.display = 'flex';
358
361
 
362
+ const lastMsg = messages[messages.length - 1];
363
+ const isStreamingLast = isStreaming && lastMsg.role === 'assistant' && lastMsg.content;
364
+ const wrappers = messagesEl.querySelectorAll(isFullModal ? '.fd-ai-fm-msg' : '.fd-ai-msg');
365
+ if (isStreamingLast && wrappers.length === messages.length) {
366
+ const lastWrapper = wrappers[wrappers.length - 1];
367
+ const contentDiv = lastWrapper.querySelector(isFullModal ? '.fd-ai-fm-msg-content' : '.fd-ai-bubble-ai');
368
+ if (contentDiv) {
369
+ contentDiv.innerHTML = `<div class="fd-ai-streaming">${renderMarkdown(lastMsg.content)}</div>`;
370
+ if (isFullModal) messagesEl.scrollTo({ top: messagesEl.scrollHeight, behavior: 'auto' });
371
+ else messagesEl.scrollTop = messagesEl.scrollHeight;
372
+ return;
373
+ }
374
+ }
375
+
359
376
  const thinking = thinkingHtml(aiLabel);
360
377
  messagesEl.innerHTML = messages.map((m, i) => {
361
378
  const label = m.role === 'user' ? 'You' : aiLabel;
@@ -420,7 +437,12 @@ const containerStyle = getContainerStyle(floatingStyle, position);
420
437
  if (content) {
421
438
  assistantContent += content;
422
439
  messages[messages.length - 1].content = assistantContent;
423
- scheduleRender();
440
+ if (!streamRenderThrottle) {
441
+ streamRenderThrottle = setTimeout(() => {
442
+ streamRenderThrottle = null;
443
+ doRender();
444
+ }, 80);
445
+ }
424
446
  }
425
447
  } catch {}
426
448
  }