@farming-labs/astro-theme 0.0.29 → 0.0.31

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.29",
3
+ "version": "0.0.31",
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.29",
83
- "@farming-labs/astro": "0.0.29"
82
+ "@farming-labs/docs": "0.0.31",
83
+ "@farming-labs/astro": "0.0.31"
84
84
  },
85
85
  "peerDependencies": {
86
86
  "astro": ">=4.0.0"
@@ -107,7 +107,8 @@ const htmlWithoutFirstH1 = (data.html || "").replace(/<h1[^>]*>[\s\S]*?<\/h1>\s*
107
107
  </head>
108
108
 
109
109
  <DocsPage
110
- entry={config?.entry ?? "docs"}
110
+ entry={data.entry ?? config?.entry ?? "docs"}
111
+ locale={data.locale}
111
112
  tocEnabled={tocEnabled}
112
113
  tocStyle={tocStyle}
113
114
  breadcrumbEnabled={breadcrumbEnabled}
@@ -7,8 +7,25 @@ const { tree, config = null, title, titleUrl, flatPages = null } = Astro.props;
7
7
  const sidebarFlat = !!(config?.sidebar && typeof config.sidebar === "object" && (config.sidebar as { flat?: boolean }).flat);
8
8
  const useFlatSidebar = sidebarFlat && Array.isArray(flatPages) && flatPages.length > 0;
9
9
 
10
+ const i18n = config?.i18n as { locales?: string[]; defaultLocale?: string } | undefined;
11
+ const locales = Array.isArray(i18n?.locales) ? i18n.locales.filter(Boolean) : [];
12
+ const defaultLocale = (i18n?.defaultLocale && locales.includes(i18n.defaultLocale))
13
+ ? i18n.defaultLocale
14
+ : locales[0];
15
+ const activeLocale =
16
+ Astro.url.searchParams.get("lang") ??
17
+ Astro.url.searchParams.get("locale") ??
18
+ defaultLocale;
19
+ const withLang = (url?: string | null) => {
20
+ if (!url || url.startsWith("#")) return url ?? "";
21
+ const parsed = new URL(url, Astro.url);
22
+ if (activeLocale) parsed.searchParams.set("lang", activeLocale);
23
+ else parsed.searchParams.delete("lang");
24
+ return `${parsed.pathname}${parsed.search}${parsed.hash}`;
25
+ };
26
+
10
27
  const resolvedTitle = title ?? config?.nav?.title ?? "Docs";
11
- const resolvedTitleUrl = titleUrl ?? config?.nav?.url ?? "/docs";
28
+ const resolvedTitleUrl = withLang(titleUrl ?? config?.nav?.url ?? "/docs");
12
29
 
13
30
  const showThemeToggle = (() => {
14
31
  const toggle = config?.themeToggle;
@@ -207,7 +224,7 @@ const showSearch = !staticExport;
207
224
  const icon = getIcon(page.icon);
208
225
  return (
209
226
  <a
210
- href={page.url}
227
+ href={withLang(page.url)}
211
228
  class={`fd-sidebar-link fd-sidebar-top-link ${isActive(page.url) ? 'fd-sidebar-link-active' : ''} ${i === 0 ? 'fd-sidebar-first-item' : ''}`}
212
229
  data-active={isActive(page.url) || undefined}
213
230
  >
@@ -224,7 +241,7 @@ const showSearch = !staticExport;
224
241
  const icon = getIcon(node.icon);
225
242
  return (
226
243
  <a
227
- href={node.url}
244
+ href={withLang(node.url)}
228
245
  class={`fd-sidebar-link fd-sidebar-top-link ${isActive(node.url) ? 'fd-sidebar-link-active' : ''} ${i === 0 ? 'fd-sidebar-first-item' : ''}`}
229
246
  data-active={isActive(node.url) || undefined}
230
247
  >
@@ -248,7 +265,7 @@ const showSearch = !staticExport;
248
265
  <div class="fd-sidebar-folder-content">
249
266
  {node.index && (
250
267
  <a
251
- href={node.index.url}
268
+ href={withLang(node.index.url)}
252
269
  class={`fd-sidebar-link fd-sidebar-child-link ${isActive(node.index.url) ? 'fd-sidebar-link-active' : ''}`}
253
270
  data-active={isActive(node.index.url) || undefined}
254
271
  >
@@ -259,7 +276,7 @@ const showSearch = !staticExport;
259
276
  if (child.type === "page") {
260
277
  return (
261
278
  <a
262
- href={child.url}
279
+ href={withLang(child.url)}
263
280
  class={`fd-sidebar-link fd-sidebar-child-link ${isActive(child.url) ? 'fd-sidebar-link-active' : ''}`}
264
281
  data-active={isActive(child.url) || undefined}
265
282
  >
@@ -278,7 +295,7 @@ const showSearch = !staticExport;
278
295
  <div class="fd-sidebar-folder-content">
279
296
  {child.index && (
280
297
  <a
281
- href={child.index.url}
298
+ href={withLang(child.index.url)}
282
299
  class={`fd-sidebar-link fd-sidebar-child-link ${isActive(child.index.url) ? 'fd-sidebar-link-active' : ''}`}
283
300
  data-active={isActive(child.index.url) || undefined}
284
301
  >
@@ -289,7 +306,7 @@ const showSearch = !staticExport;
289
306
  if (grandchild.type === "page") {
290
307
  return (
291
308
  <a
292
- href={grandchild.url}
309
+ href={withLang(grandchild.url)}
293
310
  class={`fd-sidebar-link fd-sidebar-child-link ${isActive(grandchild.url) ? 'fd-sidebar-link-active' : ''}`}
294
311
  data-active={isActive(grandchild.url) || undefined}
295
312
  >
@@ -317,9 +334,33 @@ const showSearch = !staticExport;
317
334
  </div>
318
335
  )}
319
336
 
320
- {showThemeToggle && (
337
+ {(locales.length > 0 || showThemeToggle) && (
321
338
  <div class="fd-sidebar-footer">
322
- <ThemeToggle />
339
+ <div style="display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%">
340
+ {locales.length > 0 && (
341
+ <div style="position:relative;display:inline-flex;align-items:center;flex-shrink:0">
342
+ <select
343
+ id="fd-locale-select"
344
+ aria-label="Select language"
345
+ data-default-locale={defaultLocale}
346
+ style="appearance:none;-webkit-appearance:none;-moz-appearance:none;min-width:84px;height:36px;border-radius:9999px;border:1px solid var(--color-fd-border);background:var(--color-fd-card, var(--color-fd-background));color:var(--color-fd-foreground);padding:0 36px 0 14px;font-size:12px;font-weight:600;letter-spacing:.04em;line-height:1;cursor:pointer;box-shadow:0 1px 2px rgba(15,23,42,.08)"
347
+ >
348
+ {locales.map((item) => (
349
+ <option value={item} selected={item === activeLocale}>{item.toUpperCase()}</option>
350
+ ))}
351
+ </select>
352
+ <span
353
+ aria-hidden="true"
354
+ style="position:absolute;right:12px;display:inline-flex;align-items:center;justify-content:center;color:var(--color-fd-muted-foreground);pointer-events:none"
355
+ >
356
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
357
+ <polyline points="6 9 12 15 18 9" />
358
+ </svg>
359
+ </span>
360
+ </div>
361
+ )}
362
+ {showThemeToggle && <ThemeToggle />}
363
+ </div>
323
364
  </div>
324
365
  )}
325
366
  </aside>
@@ -331,7 +372,7 @@ const showSearch = !staticExport;
331
372
 
332
373
  {showFloatingAI && (
333
374
  <FloatingAIChat
334
- api="/api/docs"
375
+ api={withLang("/api/docs")}
335
376
  suggestedQuestions={aiConfig?.suggestedQuestions ?? []}
336
377
  aiLabel={aiConfig?.aiLabel ?? "AI"}
337
378
  position={aiConfig?.position ?? "bottom-right"}
@@ -393,4 +434,12 @@ const showSearch = !staticExport;
393
434
  overlay!.style.display = 'none';
394
435
  });
395
436
  });
437
+
438
+ document.getElementById('fd-locale-select')?.addEventListener('change', (event) => {
439
+ const target = event.target as HTMLSelectElement;
440
+ const url = new URL(window.location.href);
441
+ if (target.value) url.searchParams.set('lang', target.value);
442
+ else url.searchParams.delete('lang');
443
+ window.location.assign(`${url.pathname}${url.search}${url.hash}`);
444
+ });
396
445
  </script>
@@ -4,6 +4,7 @@ const {
4
4
  tocStyle = "default",
5
5
  breadcrumbEnabled = true,
6
6
  entry = "docs",
7
+ locale = null,
7
8
  previousPage = null,
8
9
  nextPage = null,
9
10
  editOnGithub = null,
@@ -13,19 +14,34 @@ const {
13
14
 
14
15
  const pathname = Astro.url.pathname;
15
16
  const segments = pathname.split("/").filter(Boolean);
16
- const parentLabel = segments.length >= 2 ? segments[segments.length - 2].replace(/-/g, " ").replace(/\b\w/g, c => c.toUpperCase()) : "";
17
- const currentLabel = segments.length >= 2 ? segments[segments.length - 1].replace(/-/g, " ").replace(/\b\w/g, c => c.toUpperCase()) : "";
18
- const parentUrl = segments.length >= 2
19
- ? "/" + segments.slice(0, segments.length - 1).join("/")
17
+ const entryParts = entry.split("/").filter(Boolean);
18
+ const contentSegments = segments.slice(entryParts.length);
19
+ const parentLabel = contentSegments.length >= 2
20
+ ? contentSegments[contentSegments.length - 2].replace(/-/g, " ").replace(/\b\w/g, c => c.toUpperCase())
20
21
  : "";
22
+ const currentLabel = contentSegments.length >= 2
23
+ ? contentSegments[contentSegments.length - 1].replace(/-/g, " ").replace(/\b\w/g, c => c.toUpperCase())
24
+ : "";
25
+ const parentUrl = contentSegments.length >= 2
26
+ ? "/" + [...segments.slice(0, entryParts.length), ...contentSegments.slice(0, contentSegments.length - 1)].join("/")
27
+ : "";
28
+ const llmsLangParam = locale ? `&lang=${encodeURIComponent(locale)}` : "";
29
+ const withLang = (url: string) => {
30
+ if (!url || url.startsWith("#")) return url;
31
+ const parsed = new URL(url, Astro.url);
32
+ if (locale) parsed.searchParams.set("lang", locale);
33
+ else parsed.searchParams.delete("lang");
34
+ return `${parsed.pathname}${parsed.search}${parsed.hash}`;
35
+ };
36
+ const localizedParentUrl = withLang(parentUrl);
21
37
  ---
22
38
 
23
39
  <div class="fd-page">
24
40
  <article class="fd-page-article" id="nd-page">
25
- {breadcrumbEnabled && segments.length >= 2 && (
41
+ {breadcrumbEnabled && contentSegments.length >= 2 && (
26
42
  <nav class="fd-breadcrumb" aria-label="Breadcrumb">
27
43
  <span class="fd-breadcrumb-item">
28
- <a href={parentUrl} class="fd-breadcrumb-parent fd-breadcrumb-link">{parentLabel}</a>
44
+ <a href={localizedParentUrl} class="fd-breadcrumb-parent fd-breadcrumb-link">{parentLabel}</a>
29
45
  </span>
30
46
  <span class="fd-breadcrumb-item">
31
47
  <span class="fd-breadcrumb-sep">/</span>
@@ -54,8 +70,8 @@ const parentUrl = segments.length >= 2
54
70
  )}
55
71
  {llmsTxtEnabled && (
56
72
  <span class="fd-llms-txt-links">
57
- <a href="/api/docs?format=llms" target="_blank" rel="noopener noreferrer" class="fd-llms-txt-link">llms.txt</a>
58
- <a href="/api/docs?format=llms-full" target="_blank" rel="noopener noreferrer" class="fd-llms-txt-link">llms-full.txt</a>
73
+ <a href={`/api/docs?format=llms${llmsLangParam}`} target="_blank" rel="noopener noreferrer" class="fd-llms-txt-link">llms.txt</a>
74
+ <a href={`/api/docs?format=llms-full${llmsLangParam}`} target="_blank" rel="noopener noreferrer" class="fd-llms-txt-link">llms-full.txt</a>
59
75
  </span>
60
76
  )}
61
77
  {lastModified && (
@@ -67,7 +83,7 @@ const parentUrl = segments.length >= 2
67
83
  {(previousPage || nextPage) && (
68
84
  <nav class="fd-page-nav" aria-label="Page navigation">
69
85
  {previousPage ? (
70
- <a href={previousPage.url} class="fd-page-nav-card fd-page-nav-prev">
86
+ <a href={withLang(previousPage.url)} class="fd-page-nav-card fd-page-nav-prev">
71
87
  <span class="fd-page-nav-label">
72
88
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
73
89
  <polyline points="15 18 9 12 15 6" />
@@ -78,7 +94,7 @@ const parentUrl = segments.length >= 2
78
94
  </a>
79
95
  ) : <div />}
80
96
  {nextPage ? (
81
- <a href={nextPage.url} class="fd-page-nav-card fd-page-nav-next">
97
+ <a href={withLang(nextPage.url)} class="fd-page-nav-card fd-page-nav-next">
82
98
  <span class="fd-page-nav-label">
83
99
  Next
84
100
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -127,6 +143,20 @@ const parentUrl = segments.length >= 2
127
143
  const tocAside = document.getElementById('fd-toc');
128
144
  if (container && tocList) {
129
145
  const headings = container.querySelectorAll('h2[id], h3[id], h4[id]');
146
+ const activeLocale =
147
+ new URL(window.location.href).searchParams.get('lang') ||
148
+ new URL(window.location.href).searchParams.get('locale');
149
+ container.querySelectorAll('a[href]').forEach((link) => {
150
+ const href = link.getAttribute('href');
151
+ if (!href || href.startsWith('#') || /^(mailto:|tel:|javascript:)/i.test(href)) return;
152
+ try {
153
+ const url = new URL(href, window.location.origin);
154
+ if (url.origin !== window.location.origin) return;
155
+ if (activeLocale) url.searchParams.set('lang', activeLocale);
156
+ else url.searchParams.delete('lang');
157
+ link.setAttribute('href', `${url.pathname}${url.search}${url.hash}`);
158
+ } catch {}
159
+ });
130
160
  const isClerk = tocList.closest('[data-toc-style]')?.getAttribute('data-toc-style') === 'directional';
131
161
 
132
162
  const tocItems = [];
@@ -125,6 +125,29 @@ const { config = null } = Astro.props;
125
125
  } catch (e) {}
126
126
  }
127
127
 
128
+ function getActiveLocale() {
129
+ try {
130
+ var current = new URL(window.location.href);
131
+ return current.searchParams.get("lang") || current.searchParams.get("locale") || "";
132
+ } catch (e) {
133
+ return "";
134
+ }
135
+ }
136
+
137
+ function withLang(url) {
138
+ if (!url || url.charAt(0) === "#") return url;
139
+ try {
140
+ var parsed = new URL(url, window.location.origin);
141
+ if (parsed.origin !== window.location.origin) return url;
142
+ var locale = getActiveLocale();
143
+ if (locale) parsed.searchParams.set("lang", locale);
144
+ else parsed.searchParams.delete("lang");
145
+ return parsed.pathname + parsed.search + parsed.hash;
146
+ } catch (e) {
147
+ return url;
148
+ }
149
+ }
150
+
128
151
  function openDialog() {
129
152
  var dialog = getDialog();
130
153
  if (dialog) {
@@ -241,14 +264,15 @@ const { config = null } = Astro.props;
241
264
 
242
265
  function navigateToUrl(url, newTab) {
243
266
  if (!url) return;
267
+ var localizedUrl = withLang(url);
244
268
  if (newTab || url.startsWith("http")) {
245
269
  try {
246
- window.open(url, "_blank", "noopener,noreferrer");
270
+ window.open(localizedUrl, "_blank", "noopener,noreferrer");
247
271
  } catch (err) {
248
- window.location.assign(url);
272
+ window.location.assign(localizedUrl);
249
273
  }
250
274
  } else {
251
- window.location.assign(url);
275
+ window.location.assign(localizedUrl);
252
276
  }
253
277
  }
254
278
 
@@ -352,9 +376,8 @@ const { config = null } = Astro.props;
352
376
  if (items[activeIndex]) {
353
377
  var it = items[activeIndex];
354
378
  saveRecent({ id: it.url, label: it.content || it.label, url: it.url });
355
- if (it.url.startsWith("http")) window.open(it.url, "_blank", "noopener");
356
- else window.location.href = it.url;
357
379
  closeDialog();
380
+ navigateToUrl(it.url, it.url.startsWith("http"));
358
381
  }
359
382
  }
360
383
 
@@ -375,7 +398,7 @@ const { config = null } = Astro.props;
375
398
  if (!q) return;
376
399
  debounceTimer = setTimeout(function () {
377
400
  if (getLoading()) getLoading().style.display = "flex";
378
- fetch("/api/docs?query=" + encodeURIComponent(q))
401
+ fetch(withLang("/api/docs?query=" + encodeURIComponent(q)))
379
402
  .then(function (res) { return res.ok ? res.json() : []; })
380
403
  .then(function (data) {
381
404
  currentResults = Array.isArray(data) ? data : [];