@farming-labs/nuxt-theme 0.0.3-beta.2 → 0.0.3-beta.4

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/nuxt-theme",
3
- "version": "0.0.3-beta.2",
3
+ "version": "0.0.3-beta.4",
4
4
  "description": "Nuxt/Vue UI components for @farming-labs/docs — layout, sidebar, TOC, search, and theme toggle",
5
5
  "keywords": [
6
6
  "docs",
@@ -60,7 +60,7 @@
60
60
  },
61
61
  "dependencies": {
62
62
  "sugar-high": "^0.9.5",
63
- "@farming-labs/docs": "0.0.3-beta.2"
63
+ "@farming-labs/docs": "0.0.3-beta.4"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "nuxt": ">=3.0.0",
@@ -1,12 +1,18 @@
1
1
  <script setup lang="ts">
2
- import { computed } from "vue";
2
+ import { computed, ref, onMounted, onUnmounted } from "vue";
3
3
  import DocsPage from "./DocsPage.vue";
4
4
 
5
+ const DEFAULT_OPEN_PROVIDERS = [
6
+ { name: "ChatGPT", urlTemplate: "https://chatgpt.com/?hints=search&q=Read+{mdxUrl},+I+want+to+ask+questions+about+it." },
7
+ { name: "Claude", urlTemplate: "https://claude.ai/new?q=Read+{mdxUrl},+I+want+to+ask+questions+about+it." },
8
+ ];
9
+
5
10
  const props = defineProps<{
6
11
  data: {
7
12
  title: string;
8
13
  description?: string;
9
14
  html: string;
15
+ rawMarkdown?: string;
10
16
  previousPage?: { name: string; url: string } | null;
11
17
  nextPage?: { name: string; url: string } | null;
12
18
  editOnGithub?: string;
@@ -15,15 +21,25 @@ const props = defineProps<{
15
21
  config?: Record<string, unknown> | null;
16
22
  }>();
17
23
 
24
+ const route = useRoute();
25
+ const openDropdownMenu = ref(false);
26
+ const copyLabel = ref("Copy page");
27
+ const copied = ref(false);
28
+
18
29
  const titleSuffix = computed(() =>
19
30
  props.config?.metadata?.titleTemplate
20
- ? String(props.config.metadata.titleTemplate).replace("%s", "")
31
+ ? String((props.config.metadata as Record<string, string>).titleTemplate).replace("%s", "")
21
32
  : " – Docs",
22
33
  );
23
34
 
24
- const tocEnabled = computed(() => (props.config?.theme as any)?.ui?.layout?.toc?.enabled ?? true);
25
-
26
- const tocStyle = computed(() => (props.config?.theme as any)?.ui?.layout?.toc?.style ?? "default");
35
+ const themeUi = computed(() => (props.config?.theme as Record<string, unknown>)?.ui as Record<string, unknown> | undefined);
36
+ const layout = computed(() => themeUi.value?.layout as Record<string, unknown> | undefined);
37
+ const tocConfig = computed(() => layout.value?.toc as Record<string, unknown> | undefined);
38
+ const tocEnabledVal = computed(() => tocConfig.value?.enabled ?? true);
39
+ const tocStyleVal = computed(() => {
40
+ const style = tocConfig.value?.style as string | undefined;
41
+ return style === "directional" ? "directional" : "default";
42
+ });
27
43
 
28
44
  const breadcrumbEnabled = computed(() => {
29
45
  const bc = props.config?.breadcrumb;
@@ -45,30 +61,293 @@ const llmsTxtEnabled = computed(() => {
45
61
 
46
62
  const entry = computed(() => (props.config?.entry as string) ?? "docs");
47
63
 
64
+ const copyMarkdownEnabled = computed(() => {
65
+ const pa = props.config?.pageActions as Record<string, unknown> | undefined;
66
+ if (!pa) return false;
67
+ const cm = pa.copyMarkdown;
68
+ if (cm === true) return true;
69
+ if (typeof cm === "object" && cm !== null) return (cm as { enabled?: boolean }).enabled !== false;
70
+ return false;
71
+ });
72
+
73
+ const openDocsEnabled = computed(() => {
74
+ const pa = props.config?.pageActions as Record<string, unknown> | undefined;
75
+ if (!pa) return false;
76
+ const od = pa.openDocs;
77
+ if (od === true) return true;
78
+ if (typeof od === "object" && od !== null) return (od as { enabled?: boolean }).enabled !== false;
79
+ return false;
80
+ });
81
+
82
+ const openDocsProviders = computed(() => {
83
+ const pa = props.config?.pageActions as Record<string, unknown> | undefined;
84
+ const od = pa && typeof pa === "object" && pa.openDocs != null ? pa.openDocs : null;
85
+ const list = od && typeof od === "object" && "providers" in od
86
+ ? (od as { providers?: Array<{ name?: string; urlTemplate?: string }> }).providers
87
+ : undefined;
88
+ if (Array.isArray(list) && list.length > 0) {
89
+ const mapped = list
90
+ .map((p) => ({
91
+ name: typeof p?.name === "string" ? p.name : "Open",
92
+ urlTemplate: typeof p?.urlTemplate === "string" ? p.urlTemplate : "",
93
+ }))
94
+ .filter((p) => p.urlTemplate.length > 0);
95
+ if (mapped.length > 0) return mapped;
96
+ }
97
+ return DEFAULT_OPEN_PROVIDERS;
98
+ });
99
+
100
+ const pageActionsPosition = computed(() => {
101
+ const pa = props.config?.pageActions as Record<string, unknown> | undefined;
102
+ if (typeof pa === "object" && pa !== null && pa.position) return pa.position as string;
103
+ return "below-title";
104
+ });
105
+
106
+ const pageActionsAlignment = computed(() => {
107
+ const pa = props.config?.pageActions as Record<string, unknown> | undefined;
108
+ if (typeof pa === "object" && pa !== null && pa.alignment) return pa.alignment as string;
109
+ return "left";
110
+ });
111
+
112
+ const lastUpdatedConfig = computed(() => {
113
+ const lu = props.config?.lastUpdated;
114
+ if (lu === false) return { enabled: false, position: "footer" as const };
115
+ if (lu === true || lu === undefined) return { enabled: true, position: "footer" as const };
116
+ const o = lu as { enabled?: boolean; position?: "footer" | "below-title" };
117
+ return {
118
+ enabled: o.enabled !== false,
119
+ position: (o.position ?? "footer") as "footer" | "below-title",
120
+ };
121
+ });
122
+
123
+ const showLastUpdatedInFooter = computed(
124
+ () => !!props.data.lastModified && lastUpdatedConfig.value.enabled && lastUpdatedConfig.value.position === "footer",
125
+ );
126
+ const showLastUpdatedBelowTitle = computed(
127
+ () => !!props.data.lastModified && lastUpdatedConfig.value.enabled && lastUpdatedConfig.value.position === "below-title",
128
+ );
129
+
130
+ const htmlWithoutFirstH1 = computed(() => {
131
+ const html = props.data.html || "";
132
+ return html.replace(/<h1[^>]*>[\s\S]*?<\/h1>\s*/i, "");
133
+ });
134
+
48
135
  const metaDescription = computed(
49
- () => props.data.description ?? (props.config?.metadata as any)?.description ?? undefined,
136
+ () => props.data.description ?? (props.config?.metadata as Record<string, string>)?.description ?? undefined,
50
137
  );
51
138
 
139
+ const showPageActions = computed(
140
+ () => (copyMarkdownEnabled.value || openDocsEnabled.value) && openDocsProviders.value.length >= 0,
141
+ );
142
+ const showActionsAbove = computed(() => pageActionsPosition.value === "above-title" && showPageActions.value);
143
+ const showActionsBelow = computed(() => pageActionsPosition.value === "below-title" && showPageActions.value);
144
+
52
145
  useHead({
53
146
  title: () => `${props.data.title}${titleSuffix.value}`,
54
147
  meta: () =>
55
148
  metaDescription.value ? [{ name: "description", content: metaDescription.value }] : [],
56
149
  });
150
+
151
+ function handleCopyPage() {
152
+ let text = "";
153
+ const raw = props.data.rawMarkdown;
154
+ if (raw && typeof raw === "string" && raw.length > 0) {
155
+ text = raw;
156
+ } else {
157
+ const article = document.querySelector("#nd-page");
158
+ if (article) text = (article as HTMLElement).innerText || "";
159
+ }
160
+ if (!text) return;
161
+ navigator.clipboard.writeText(text).then(
162
+ () => {
163
+ copyLabel.value = "Copied!";
164
+ copied.value = true;
165
+ setTimeout(() => {
166
+ copyLabel.value = "Copy page";
167
+ copied.value = false;
168
+ }, 2000);
169
+ },
170
+ () => {
171
+ copyLabel.value = "Copy failed";
172
+ setTimeout(() => { copyLabel.value = "Copy page"; }, 2000);
173
+ },
174
+ );
175
+ }
176
+
177
+ function toggleDropdown() {
178
+ openDropdownMenu.value = !openDropdownMenu.value;
179
+ }
180
+
181
+ function closeDropdown() {
182
+ openDropdownMenu.value = false;
183
+ }
184
+
185
+ function openInProvider(provider: { name: string; urlTemplate: string }) {
186
+ const pathname = route.path;
187
+ const pageUrl = typeof window !== "undefined" ? window.location.href : "";
188
+ const mdxUrl = typeof window !== "undefined"
189
+ ? window.location.origin + pathname + (pathname.endsWith("/") ? "page.mdx" : ".mdx")
190
+ : "";
191
+ const githubUrl = props.data.editOnGithub || "";
192
+ const url = provider.urlTemplate
193
+ .replace(/\{url\}/g, encodeURIComponent(pageUrl))
194
+ .replace(/\{mdxUrl\}/g, encodeURIComponent(mdxUrl))
195
+ .replace(/\{githubUrl\}/g, githubUrl);
196
+ if (typeof window !== "undefined") window.open(url, "_blank", "noopener,noreferrer");
197
+ closeDropdown();
198
+ }
199
+
200
+ function handleClickOutside(e: MouseEvent) {
201
+ const target = e.target as Node;
202
+ if (openDropdownMenu.value && !(target as Element).closest?.(".fd-page-action-dropdown")) {
203
+ closeDropdown();
204
+ }
205
+ }
206
+
207
+ onMounted(() => {
208
+ document.addEventListener("click", handleClickOutside);
209
+ });
210
+ onUnmounted(() => {
211
+ document.removeEventListener("click", handleClickOutside);
212
+ });
57
213
  </script>
58
214
 
59
215
  <template>
60
216
  <DocsPage
61
217
  :entry="entry"
62
- :toc-enabled="tocEnabled"
63
- :toc-style="tocStyle"
218
+ :toc-enabled="tocEnabledVal"
219
+ :toc-style="tocStyleVal"
64
220
  :breadcrumb-enabled="breadcrumbEnabled"
65
221
  :previous-page="data.previousPage ?? null"
66
222
  :next-page="data.nextPage ?? null"
67
223
  :edit-on-github="showEditOnGithub ? data.editOnGithub : null"
68
- :last-modified="showLastModified ? data.lastModified : null"
224
+ :last-modified="showLastUpdatedInFooter ? data.lastModified : null"
69
225
  :llms-txt-enabled="llmsTxtEnabled"
70
226
  >
227
+ <!-- Above-title actions -->
228
+ <div
229
+ v-if="showActionsAbove"
230
+ class="fd-page-actions"
231
+ data-page-actions
232
+ :data-actions-alignment="pageActionsAlignment"
233
+ >
234
+ <button
235
+ v-if="copyMarkdownEnabled"
236
+ type="button"
237
+ class="fd-page-action-btn"
238
+ aria-label="Copy page content"
239
+ :data-copied="copied"
240
+ @click="handleCopyPage"
241
+ >
242
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
243
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
244
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
245
+ </svg>
246
+ <span>{{ copyLabel }}</span>
247
+ </button>
248
+ <div v-if="openDocsEnabled && openDocsProviders.length > 0" class="fd-page-action-dropdown">
249
+ <button
250
+ type="button"
251
+ class="fd-page-action-btn"
252
+ :aria-expanded="openDropdownMenu"
253
+ aria-haspopup="true"
254
+ @click="toggleDropdown"
255
+ >
256
+ <span>Open in</span>
257
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
258
+ <polyline points="6 9 12 15 18 9" />
259
+ </svg>
260
+ </button>
261
+ <div
262
+ class="fd-page-action-menu"
263
+ role="menu"
264
+ :hidden="!openDropdownMenu"
265
+ >
266
+ <a
267
+ v-for="provider in openDocsProviders"
268
+ :key="provider.name"
269
+ role="menuitem"
270
+ href="#"
271
+ class="fd-page-action-menu-item"
272
+ @click.prevent="openInProvider(provider)"
273
+ >
274
+ <span class="fd-page-action-menu-label">Open in {{ provider.name }}</span>
275
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
276
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
277
+ <polyline points="15 3 21 3 21 9" />
278
+ <line x1="10" y1="14" x2="21" y2="3" />
279
+ </svg>
280
+ </a>
281
+ </div>
282
+ </div>
283
+ </div>
284
+
285
+ <h1 class="fd-page-title">{{ data.title }}</h1>
71
286
  <p v-if="data.description" class="fd-page-description">{{ data.description }}</p>
72
- <div class="fd-docs-content" v-html="data.html" />
287
+ <p v-if="showLastUpdatedBelowTitle && data.lastModified" class="fd-last-modified fd-last-modified-below-title">
288
+ Last updated: {{ data.lastModified }}
289
+ </p>
290
+
291
+ <!-- Below-title actions -->
292
+ <template v-if="showActionsBelow">
293
+ <hr class="fd-page-actions-divider" aria-hidden="true" />
294
+ <div
295
+ class="fd-page-actions"
296
+ data-page-actions
297
+ :data-actions-alignment="pageActionsAlignment"
298
+ >
299
+ <button
300
+ v-if="copyMarkdownEnabled"
301
+ type="button"
302
+ class="fd-page-action-btn"
303
+ aria-label="Copy page content"
304
+ :data-copied="copied"
305
+ @click="handleCopyPage"
306
+ >
307
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
308
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
309
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
310
+ </svg>
311
+ <span>{{ copyLabel }}</span>
312
+ </button>
313
+ <div v-if="openDocsEnabled && openDocsProviders.length > 0" class="fd-page-action-dropdown">
314
+ <button
315
+ type="button"
316
+ class="fd-page-action-btn"
317
+ :aria-expanded="openDropdownMenu"
318
+ aria-haspopup="true"
319
+ @click="toggleDropdown"
320
+ >
321
+ <span>Open in</span>
322
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
323
+ <polyline points="6 9 12 15 18 9" />
324
+ </svg>
325
+ </button>
326
+ <div
327
+ class="fd-page-action-menu"
328
+ role="menu"
329
+ :hidden="!openDropdownMenu"
330
+ >
331
+ <a
332
+ v-for="provider in openDocsProviders"
333
+ :key="provider.name"
334
+ role="menuitem"
335
+ href="#"
336
+ class="fd-page-action-menu-item"
337
+ @click.prevent="openInProvider(provider)"
338
+ >
339
+ <span class="fd-page-action-menu-label">Open in {{ provider.name }}</span>
340
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
341
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
342
+ <polyline points="15 3 21 3 21 9" />
343
+ <line x1="10" y1="14" x2="21" y2="3" />
344
+ </svg>
345
+ </a>
346
+ </div>
347
+ </div>
348
+ </div>
349
+ </template>
350
+
351
+ <div v-html="htmlWithoutFirstH1" />
73
352
  </DocsPage>
74
353
  </template>
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, computed } from "vue";
3
+ import { useRoute } from "vue-router";
3
4
  import SearchDialog from "./SearchDialog.vue";
4
5
  import FloatingAIChat from "./FloatingAIChat.vue";
5
6
  import ThemeToggle from "./ThemeToggle.vue";
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, onMounted, watch } from "vue";
3
+ import { useRoute } from "vue-router";
3
4
  import Breadcrumb from "./Breadcrumb.vue";
4
5
  import TableOfContents from "./TableOfContents.vue";
5
6
 
@@ -104,7 +105,9 @@ watch(
104
105
  <Breadcrumb v-if="breadcrumbEnabled" :pathname="route.path" :entry="entry" />
105
106
 
106
107
  <div class="fd-page-body">
107
- <slot />
108
+ <div class="fd-docs-content">
109
+ <slot />
110
+ </div>
108
111
  </div>
109
112
 
110
113
  <footer class="fd-page-footer">
@@ -243,7 +243,7 @@ function handleFmKeyDown(e: KeyboardEvent) {
243
243
  :style="btnStyle"
244
244
  @click="isOpen = true"
245
245
  >
246
- <component :is="triggerComponent" />
246
+ <component :is="triggerComponent" :ai-label="label" />
247
247
  </div>
248
248
  <button
249
249
  v-else-if="!isOpen"
@@ -499,7 +499,7 @@ function handleFmKeyDown(e: KeyboardEvent) {
499
499
  :style="btnStyle"
500
500
  @click="isOpen = true"
501
501
  >
502
- <component :is="triggerComponent" />
502
+ <component :is="triggerComponent" :ai-label="label" />
503
503
  </div>
504
504
  <button
505
505
  v-else