@farming-labs/nuxt-theme 0.0.2-beta.17
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 +71 -0
- package/src/components/Breadcrumb.vue +45 -0
- package/src/components/DocsContent.vue +77 -0
- package/src/components/DocsLayout.vue +354 -0
- package/src/components/DocsPage.vue +146 -0
- package/src/components/FloatingAIChat.vue +436 -0
- package/src/components/SearchDialog.vue +94 -0
- package/src/components/TableOfContents.vue +302 -0
- package/src/components/ThemeToggle.vue +42 -0
- package/src/index.d.ts +17 -0
- package/src/index.js +18 -0
- package/src/lib/renderMarkdown.js +108 -0
- package/src/themes/colorful.d.ts +2 -0
- package/src/themes/colorful.js +42 -0
- package/src/themes/darksharp.d.ts +4 -0
- package/src/themes/darksharp.js +42 -0
- package/src/themes/default.d.ts +4 -0
- package/src/themes/default.js +42 -0
- package/src/themes/pixel-border.d.ts +4 -0
- package/src/themes/pixel-border.js +38 -0
- package/styles/colorful-bundle.css +2 -0
- package/styles/colorful.css +148 -0
- package/styles/darksharp-bundle.css +6 -0
- package/styles/darksharp.css +206 -0
- package/styles/docs.css +2218 -0
- package/styles/pixel-border-bundle.css +6 -0
- package/styles/pixel-border.css +606 -0
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@farming-labs/nuxt-theme",
|
|
3
|
+
"version": "0.0.2-beta.17",
|
|
4
|
+
"description": "Nuxt/Vue UI components for @farming-labs/docs — layout, sidebar, TOC, search, and theme toggle",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./src/index.d.ts",
|
|
9
|
+
"import": "./src/index.js",
|
|
10
|
+
"default": "./src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./fumadocs": {
|
|
13
|
+
"types": "./src/themes/default.d.ts",
|
|
14
|
+
"import": "./src/themes/default.js",
|
|
15
|
+
"default": "./src/themes/default.js"
|
|
16
|
+
},
|
|
17
|
+
"./pixel-border": {
|
|
18
|
+
"types": "./src/themes/pixel-border.d.ts",
|
|
19
|
+
"import": "./src/themes/pixel-border.js",
|
|
20
|
+
"default": "./src/themes/pixel-border.js"
|
|
21
|
+
},
|
|
22
|
+
"./darksharp": {
|
|
23
|
+
"types": "./src/themes/darksharp.d.ts",
|
|
24
|
+
"import": "./src/themes/darksharp.js",
|
|
25
|
+
"default": "./src/themes/darksharp.js"
|
|
26
|
+
},
|
|
27
|
+
"./colorful": {
|
|
28
|
+
"types": "./src/themes/colorful.d.ts",
|
|
29
|
+
"import": "./src/themes/colorful.js",
|
|
30
|
+
"default": "./src/themes/colorful.js"
|
|
31
|
+
},
|
|
32
|
+
"./css": "./styles/docs.css",
|
|
33
|
+
"./fumadocs/css": "./styles/docs.css",
|
|
34
|
+
"./styles/pixel-border.css": "./styles/pixel-border.css",
|
|
35
|
+
"./styles/darksharp.css": "./styles/darksharp.css",
|
|
36
|
+
"./pixel-border/css": "./styles/pixel-border-bundle.css",
|
|
37
|
+
"./darksharp/css": "./styles/darksharp-bundle.css",
|
|
38
|
+
"./styles/colorful.css": "./styles/colorful.css",
|
|
39
|
+
"./colorful/css": "./styles/colorful-bundle.css"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"src",
|
|
43
|
+
"styles"
|
|
44
|
+
],
|
|
45
|
+
"keywords": [
|
|
46
|
+
"docs",
|
|
47
|
+
"nuxt",
|
|
48
|
+
"vue",
|
|
49
|
+
"theme",
|
|
50
|
+
"documentation"
|
|
51
|
+
],
|
|
52
|
+
"author": "Farming Labs",
|
|
53
|
+
"license": "MIT",
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"sugar-high": "^0.9.5",
|
|
56
|
+
"@farming-labs/docs": "0.0.2-beta.17"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"vue": ">=3.0.0",
|
|
60
|
+
"nuxt": ">=3.0.0"
|
|
61
|
+
},
|
|
62
|
+
"peerDependenciesMeta": {
|
|
63
|
+
"nuxt": {
|
|
64
|
+
"optional": true
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"build": "echo 'Nuxt theme components are shipped as source'",
|
|
69
|
+
"typecheck": "echo 'ok'"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(
|
|
5
|
+
defineProps<{ pathname?: string; entry?: string }>(),
|
|
6
|
+
{ pathname: "", entry: "docs" }
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
const segments = computed(() => {
|
|
10
|
+
return props.pathname.split("/").filter(Boolean);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const parentLabel = computed(() => {
|
|
14
|
+
if (segments.value.length < 2) return "";
|
|
15
|
+
return segments.value[segments.value.length - 2]
|
|
16
|
+
.replace(/-/g, " ")
|
|
17
|
+
.replace(/\b\w/g, (c: string) => c.toUpperCase());
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const currentLabel = computed(() => {
|
|
21
|
+
if (segments.value.length < 2) return "";
|
|
22
|
+
return segments.value[segments.value.length - 1]
|
|
23
|
+
.replace(/-/g, " ")
|
|
24
|
+
.replace(/\b\w/g, (c: string) => c.toUpperCase());
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const parentUrl = computed(() => {
|
|
28
|
+
if (segments.value.length < 2) return "";
|
|
29
|
+
return "/" + segments.value.slice(0, segments.value.length - 1).join("/");
|
|
30
|
+
});
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<nav v-if="segments.length >= 2" class="fd-breadcrumb" aria-label="Breadcrumb">
|
|
35
|
+
<span class="fd-breadcrumb-item">
|
|
36
|
+
<a :href="parentUrl" class="fd-breadcrumb-parent fd-breadcrumb-link">
|
|
37
|
+
{{ parentLabel }}
|
|
38
|
+
</a>
|
|
39
|
+
</span>
|
|
40
|
+
<span class="fd-breadcrumb-item">
|
|
41
|
+
<span class="fd-breadcrumb-sep">/</span>
|
|
42
|
+
<span class="fd-breadcrumb-current">{{ currentLabel }}</span>
|
|
43
|
+
</span>
|
|
44
|
+
</nav>
|
|
45
|
+
</template>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import DocsPage from "./DocsPage.vue";
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
data: {
|
|
7
|
+
title: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
html: string;
|
|
10
|
+
previousPage?: { name: string; url: string } | null;
|
|
11
|
+
nextPage?: { name: string; url: string } | null;
|
|
12
|
+
editOnGithub?: string;
|
|
13
|
+
lastModified?: string;
|
|
14
|
+
};
|
|
15
|
+
config?: Record<string, unknown> | null;
|
|
16
|
+
}>();
|
|
17
|
+
|
|
18
|
+
const titleSuffix = computed(() =>
|
|
19
|
+
props.config?.metadata?.titleTemplate
|
|
20
|
+
? String(props.config.metadata.titleTemplate).replace("%s", "")
|
|
21
|
+
: " – Docs"
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const tocEnabled = computed(
|
|
25
|
+
() => (props.config?.theme as any)?.ui?.layout?.toc?.enabled ?? true
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const tocStyle = computed(
|
|
29
|
+
() => (props.config?.theme as any)?.ui?.layout?.toc?.style ?? "default"
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const breadcrumbEnabled = computed(() => {
|
|
33
|
+
const bc = props.config?.breadcrumb;
|
|
34
|
+
if (bc === undefined || bc === true) return true;
|
|
35
|
+
if (bc === false) return false;
|
|
36
|
+
if (typeof bc === "object") return (bc as { enabled?: boolean }).enabled !== false;
|
|
37
|
+
return true;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const showEditOnGithub = computed(
|
|
41
|
+
() => !!props.config?.github && !!props.data.editOnGithub
|
|
42
|
+
);
|
|
43
|
+
const showLastModified = computed(() => !!props.data.lastModified);
|
|
44
|
+
|
|
45
|
+
const entry = computed(() => (props.config?.entry as string) ?? "docs");
|
|
46
|
+
|
|
47
|
+
const metaDescription = computed(
|
|
48
|
+
() =>
|
|
49
|
+
props.data.description ??
|
|
50
|
+
(props.config?.metadata as any)?.description ??
|
|
51
|
+
undefined
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
useHead({
|
|
55
|
+
title: () => `${props.data.title}${titleSuffix.value}`,
|
|
56
|
+
meta: () =>
|
|
57
|
+
metaDescription.value
|
|
58
|
+
? [{ name: "description", content: metaDescription.value }]
|
|
59
|
+
: [],
|
|
60
|
+
});
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<template>
|
|
64
|
+
<DocsPage
|
|
65
|
+
:entry="entry"
|
|
66
|
+
:toc-enabled="tocEnabled"
|
|
67
|
+
:toc-style="tocStyle"
|
|
68
|
+
:breadcrumb-enabled="breadcrumbEnabled"
|
|
69
|
+
:previous-page="data.previousPage ?? null"
|
|
70
|
+
:next-page="data.nextPage ?? null"
|
|
71
|
+
:edit-on-github="showEditOnGithub ? data.editOnGithub : null"
|
|
72
|
+
:last-modified="showLastModified ? data.lastModified : null"
|
|
73
|
+
>
|
|
74
|
+
<p v-if="data.description" class="fd-page-description">{{ data.description }}</p>
|
|
75
|
+
<div class="fd-docs-content" v-html="data.html" />
|
|
76
|
+
</DocsPage>
|
|
77
|
+
</template>
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed } from "vue";
|
|
3
|
+
import SearchDialog from "./SearchDialog.vue";
|
|
4
|
+
import FloatingAIChat from "./FloatingAIChat.vue";
|
|
5
|
+
import ThemeToggle from "./ThemeToggle.vue";
|
|
6
|
+
|
|
7
|
+
interface NavNode {
|
|
8
|
+
type: "page" | "folder";
|
|
9
|
+
name: string;
|
|
10
|
+
url?: string;
|
|
11
|
+
icon?: string;
|
|
12
|
+
index?: { name: string; url: string };
|
|
13
|
+
children?: NavNode[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(
|
|
17
|
+
defineProps<{
|
|
18
|
+
tree?: { name: string; children: NavNode[] };
|
|
19
|
+
config?: Record<string, any> | null;
|
|
20
|
+
title?: string;
|
|
21
|
+
titleUrl?: string;
|
|
22
|
+
triggerComponent?: object | null;
|
|
23
|
+
}>(),
|
|
24
|
+
{ config: null, title: undefined, titleUrl: undefined, triggerComponent: null }
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const route = useRoute();
|
|
28
|
+
|
|
29
|
+
const resolvedTitle = computed(
|
|
30
|
+
() => props.title ?? props.config?.nav?.title ?? "Docs"
|
|
31
|
+
);
|
|
32
|
+
const resolvedTitleUrl = computed(
|
|
33
|
+
() => props.titleUrl ?? props.config?.nav?.url ?? "/docs"
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const showThemeToggle = computed(() => {
|
|
37
|
+
const toggle = props.config?.themeToggle;
|
|
38
|
+
if (toggle === undefined || toggle === true) return true;
|
|
39
|
+
if (toggle === false) return false;
|
|
40
|
+
if (typeof toggle === "object") return toggle.enabled !== false;
|
|
41
|
+
return true;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const forcedTheme = computed(() => {
|
|
45
|
+
const toggle = props.config?.themeToggle;
|
|
46
|
+
if (typeof toggle === "object" && toggle?.enabled === false && toggle?.default) {
|
|
47
|
+
return toggle.default as string;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const defaultTheme = computed(() => {
|
|
53
|
+
const toggle = props.config?.themeToggle;
|
|
54
|
+
if (typeof toggle === "object" && toggle?.default) return toggle.default as string;
|
|
55
|
+
return null;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Theme initialization script — runs before paint to avoid flash.
|
|
59
|
+
// Uses cookies for SSR compatibility, falls back to system preference.
|
|
60
|
+
const themeInitScript = computed(() => {
|
|
61
|
+
if (forcedTheme.value) {
|
|
62
|
+
return `document.documentElement.classList.remove('light','dark');document.documentElement.classList.add('${forcedTheme.value}')`;
|
|
63
|
+
}
|
|
64
|
+
const def = defaultTheme.value;
|
|
65
|
+
const fallback = def ? `'${def}'` : `(window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light')`;
|
|
66
|
+
return [
|
|
67
|
+
"(function(){",
|
|
68
|
+
"var m=document.cookie.match(/(?:^|;\\s*)theme=(\\w+)/);",
|
|
69
|
+
`var t=m?m[1]:${fallback};`,
|
|
70
|
+
"document.documentElement.classList.remove('light','dark');",
|
|
71
|
+
"document.documentElement.classList.add(t);",
|
|
72
|
+
"})()",
|
|
73
|
+
].join("");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ─── Color CSS variable generation ───────────────────────────
|
|
77
|
+
const COLOR_MAP: Record<string, string> = {
|
|
78
|
+
primary: "--color-fd-primary",
|
|
79
|
+
primaryForeground: "--color-fd-primary-foreground",
|
|
80
|
+
background: "--color-fd-background",
|
|
81
|
+
foreground: "--color-fd-foreground",
|
|
82
|
+
muted: "--color-fd-muted",
|
|
83
|
+
mutedForeground: "--color-fd-muted-foreground",
|
|
84
|
+
border: "--color-fd-border",
|
|
85
|
+
card: "--color-fd-card",
|
|
86
|
+
cardForeground: "--color-fd-card-foreground",
|
|
87
|
+
accent: "--color-fd-accent",
|
|
88
|
+
accentForeground: "--color-fd-accent-foreground",
|
|
89
|
+
popover: "--color-fd-popover",
|
|
90
|
+
popoverForeground: "--color-fd-popover-foreground",
|
|
91
|
+
secondary: "--color-fd-secondary",
|
|
92
|
+
secondaryForeground: "--color-fd-secondary-foreground",
|
|
93
|
+
ring: "--color-fd-ring",
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
function buildColorsCSS(colors: Record<string, string> | undefined): string {
|
|
97
|
+
if (!colors) return "";
|
|
98
|
+
const vars: string[] = [];
|
|
99
|
+
for (const [key, value] of Object.entries(colors)) {
|
|
100
|
+
if (!value || !COLOR_MAP[key]) continue;
|
|
101
|
+
vars.push(`${COLOR_MAP[key]}: ${value};`);
|
|
102
|
+
}
|
|
103
|
+
if (vars.length === 0) return "";
|
|
104
|
+
return `.dark {\n ${vars.join("\n ")}\n}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── Typography CSS variable generation ──────────────────────
|
|
108
|
+
function buildFontStyleVars(prefix: string, style: Record<string, any>): string {
|
|
109
|
+
if (!style) return "";
|
|
110
|
+
const parts: string[] = [];
|
|
111
|
+
if (style.size) parts.push(`${prefix}-size: ${style.size};`);
|
|
112
|
+
if (style.weight != null) parts.push(`${prefix}-weight: ${style.weight};`);
|
|
113
|
+
if (style.lineHeight) parts.push(`${prefix}-line-height: ${style.lineHeight};`);
|
|
114
|
+
if (style.letterSpacing) parts.push(`${prefix}-letter-spacing: ${style.letterSpacing};`);
|
|
115
|
+
return parts.join("\n ");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildTypographyCSS(typo: Record<string, any> | undefined): string {
|
|
119
|
+
if (!typo?.font) return "";
|
|
120
|
+
const vars: string[] = [];
|
|
121
|
+
const fontStyle = typo.font.style;
|
|
122
|
+
if (fontStyle?.sans) vars.push(`--fd-font-sans: ${fontStyle.sans};`);
|
|
123
|
+
if (fontStyle?.mono) vars.push(`--fd-font-mono: ${fontStyle.mono};`);
|
|
124
|
+
for (const el of ["h1", "h2", "h3", "h4", "body", "small"]) {
|
|
125
|
+
const elStyle = typo.font[el];
|
|
126
|
+
if (elStyle) {
|
|
127
|
+
const elVars = buildFontStyleVars(`--fd-${el}`, elStyle);
|
|
128
|
+
if (elVars) vars.push(elVars);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (vars.length === 0) return "";
|
|
132
|
+
return `:root {\n ${vars.join("\n ")}\n}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const overrideCSS = computed(() => {
|
|
136
|
+
const colorOverrides = (props.config?.theme as any)?._userColorOverrides
|
|
137
|
+
?? (props.config?.theme as any)?.ui?.colors;
|
|
138
|
+
const typography = (props.config?.theme as any)?.ui?.typography;
|
|
139
|
+
return [buildColorsCSS(colorOverrides), buildTypographyCSS(typography)].filter(Boolean).join("\n");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
useHead(() => ({
|
|
143
|
+
script: [
|
|
144
|
+
{ innerHTML: themeInitScript.value, tagPosition: "head" },
|
|
145
|
+
],
|
|
146
|
+
style: overrideCSS.value
|
|
147
|
+
? [{ innerHTML: overrideCSS.value }]
|
|
148
|
+
: [],
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
// ─── Sidebar / search / keyboard ─────────────────────────────
|
|
152
|
+
const sidebarOpen = ref(false);
|
|
153
|
+
const searchOpen = ref(false);
|
|
154
|
+
|
|
155
|
+
function toggleSidebar() {
|
|
156
|
+
sidebarOpen.value = !sidebarOpen.value;
|
|
157
|
+
}
|
|
158
|
+
function closeSidebar() {
|
|
159
|
+
sidebarOpen.value = false;
|
|
160
|
+
}
|
|
161
|
+
function openSearch() {
|
|
162
|
+
searchOpen.value = true;
|
|
163
|
+
}
|
|
164
|
+
function closeSearch() {
|
|
165
|
+
searchOpen.value = false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isActive(url: string) {
|
|
169
|
+
const current = route.path;
|
|
170
|
+
const normalised = (url ?? "").replace(/\/$/, "") || "/";
|
|
171
|
+
const currentNorm = current.replace(/\/$/, "") || "/";
|
|
172
|
+
return normalised === currentNorm;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const ICON_MAP: Record<string, string> = {
|
|
176
|
+
book: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
|
|
177
|
+
terminal: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>',
|
|
178
|
+
code: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
|
|
179
|
+
file: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>',
|
|
180
|
+
folder: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>',
|
|
181
|
+
rocket: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg>',
|
|
182
|
+
settings: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>',
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
function getIcon(iconKey?: string) {
|
|
186
|
+
if (!iconKey) return null;
|
|
187
|
+
return ICON_MAP[iconKey] ?? null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
191
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
|
192
|
+
e.preventDefault();
|
|
193
|
+
searchOpen.value = !searchOpen.value;
|
|
194
|
+
}
|
|
195
|
+
if (e.key === "Escape") {
|
|
196
|
+
searchOpen.value = false;
|
|
197
|
+
sidebarOpen.value = false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const showFloatingAI = computed(
|
|
202
|
+
() =>
|
|
203
|
+
props.config?.ai?.enabled &&
|
|
204
|
+
props.config?.ai?.mode === "floating"
|
|
205
|
+
);
|
|
206
|
+
</script>
|
|
207
|
+
|
|
208
|
+
<template>
|
|
209
|
+
<div class="fd-layout-root">
|
|
210
|
+
<div class="fd-layout" @keydown="handleKeydown">
|
|
211
|
+
<header class="fd-header">
|
|
212
|
+
<button class="fd-menu-btn" type="button" aria-label="Toggle sidebar" @click="toggleSidebar">
|
|
213
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
214
|
+
<line x1="3" y1="6" x2="21" y2="6" />
|
|
215
|
+
<line x1="3" y1="12" x2="21" y2="12" />
|
|
216
|
+
<line x1="3" y1="18" x2="21" y2="18" />
|
|
217
|
+
</svg>
|
|
218
|
+
</button>
|
|
219
|
+
<NuxtLink :to="resolvedTitleUrl" class="fd-header-title">{{ resolvedTitle }}</NuxtLink>
|
|
220
|
+
<button class="fd-search-trigger-mobile" type="button" aria-label="Search" @click="openSearch">
|
|
221
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
222
|
+
<circle cx="11" cy="11" r="8" />
|
|
223
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
224
|
+
</svg>
|
|
225
|
+
</button>
|
|
226
|
+
</header>
|
|
227
|
+
|
|
228
|
+
<div v-if="sidebarOpen" class="fd-sidebar-overlay" aria-hidden="true" @click="closeSidebar" />
|
|
229
|
+
|
|
230
|
+
<aside class="fd-sidebar" :class="{ 'fd-sidebar-open': sidebarOpen }">
|
|
231
|
+
<div class="fd-sidebar-header">
|
|
232
|
+
<NuxtLink :to="resolvedTitleUrl" class="fd-sidebar-title" @click="closeSidebar">
|
|
233
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
234
|
+
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
|
|
235
|
+
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
|
|
236
|
+
</svg>
|
|
237
|
+
{{ resolvedTitle }}
|
|
238
|
+
</NuxtLink>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<div class="fd-sidebar-search">
|
|
242
|
+
<button type="button" class="fd-sidebar-search-btn" @click="openSearch">
|
|
243
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
244
|
+
<circle cx="11" cy="11" r="8" />
|
|
245
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
246
|
+
</svg>
|
|
247
|
+
<span>Search</span>
|
|
248
|
+
<kbd>⌘</kbd><kbd>K</kbd>
|
|
249
|
+
</button>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<nav class="fd-sidebar-nav">
|
|
253
|
+
<template v-if="tree?.children">
|
|
254
|
+
<template v-for="(node, i) in tree.children" :key="node.name + (node.url ?? '')">
|
|
255
|
+
<NuxtLink
|
|
256
|
+
v-if="node.type === 'page'"
|
|
257
|
+
:to="node.url!"
|
|
258
|
+
class="fd-sidebar-link fd-sidebar-top-link"
|
|
259
|
+
:class="{ 'fd-sidebar-link-active': isActive(node.url ?? ''), 'fd-sidebar-first-item': i === 0 }"
|
|
260
|
+
@click="closeSidebar"
|
|
261
|
+
>
|
|
262
|
+
<span v-if="getIcon(node.icon)" class="fd-sidebar-icon" v-html="getIcon(node.icon)" />
|
|
263
|
+
{{ node.name }}
|
|
264
|
+
</NuxtLink>
|
|
265
|
+
<details v-else-if="node.type === 'folder'" class="fd-sidebar-folder" :class="{ 'fd-sidebar-first-item': i === 0 }" open>
|
|
266
|
+
<summary class="fd-sidebar-folder-trigger">
|
|
267
|
+
<span class="fd-sidebar-folder-label">
|
|
268
|
+
<span v-if="getIcon(node.icon)" class="fd-sidebar-icon" v-html="getIcon(node.icon)" />
|
|
269
|
+
{{ node.name }}
|
|
270
|
+
</span>
|
|
271
|
+
<svg class="fd-sidebar-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
272
|
+
<polyline points="6 9 12 15 18 9" />
|
|
273
|
+
</svg>
|
|
274
|
+
</summary>
|
|
275
|
+
<div class="fd-sidebar-folder-content">
|
|
276
|
+
<NuxtLink
|
|
277
|
+
v-if="node.index"
|
|
278
|
+
:to="node.index.url"
|
|
279
|
+
class="fd-sidebar-link fd-sidebar-child-link"
|
|
280
|
+
:class="{ 'fd-sidebar-link-active': isActive(node.index.url) }"
|
|
281
|
+
@click="closeSidebar"
|
|
282
|
+
>
|
|
283
|
+
{{ node.index.name }}
|
|
284
|
+
</NuxtLink>
|
|
285
|
+
<template v-for="child in node.children" :key="child.name + ((child as any).url ?? '')">
|
|
286
|
+
<NuxtLink
|
|
287
|
+
v-if="child.type === 'page'"
|
|
288
|
+
:to="(child as any).url"
|
|
289
|
+
class="fd-sidebar-link fd-sidebar-child-link"
|
|
290
|
+
:class="{ 'fd-sidebar-link-active': isActive((child as any).url) }"
|
|
291
|
+
@click="closeSidebar"
|
|
292
|
+
>
|
|
293
|
+
{{ child.name }}
|
|
294
|
+
</NuxtLink>
|
|
295
|
+
<details v-else-if="child.type === 'folder'" class="fd-sidebar-folder fd-sidebar-nested-folder" open>
|
|
296
|
+
<summary class="fd-sidebar-folder-trigger">
|
|
297
|
+
<span class="fd-sidebar-folder-label">{{ child.name }}</span>
|
|
298
|
+
<svg class="fd-sidebar-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
299
|
+
<polyline points="6 9 12 15 18 9" />
|
|
300
|
+
</svg>
|
|
301
|
+
</summary>
|
|
302
|
+
<div class="fd-sidebar-folder-content">
|
|
303
|
+
<NuxtLink
|
|
304
|
+
v-if="(child as any).index"
|
|
305
|
+
:to="(child as any).index.url"
|
|
306
|
+
class="fd-sidebar-link fd-sidebar-child-link"
|
|
307
|
+
:class="{ 'fd-sidebar-link-active': isActive((child as any).index.url) }"
|
|
308
|
+
@click="closeSidebar"
|
|
309
|
+
>
|
|
310
|
+
{{ (child as any).index.name }}
|
|
311
|
+
</NuxtLink>
|
|
312
|
+
<NuxtLink
|
|
313
|
+
v-for="grandchild in (child as any).children"
|
|
314
|
+
v-if="grandchild.type === 'page'"
|
|
315
|
+
:key="grandchild.url"
|
|
316
|
+
:to="grandchild.url"
|
|
317
|
+
class="fd-sidebar-link fd-sidebar-child-link"
|
|
318
|
+
:class="{ 'fd-sidebar-link-active': isActive(grandchild.url) }"
|
|
319
|
+
@click="closeSidebar"
|
|
320
|
+
>
|
|
321
|
+
{{ grandchild.name }}
|
|
322
|
+
</NuxtLink>
|
|
323
|
+
</div>
|
|
324
|
+
</details>
|
|
325
|
+
</template>
|
|
326
|
+
</div>
|
|
327
|
+
</details>
|
|
328
|
+
</template>
|
|
329
|
+
</template>
|
|
330
|
+
</nav>
|
|
331
|
+
|
|
332
|
+
<div v-if="showThemeToggle" class="fd-sidebar-footer">
|
|
333
|
+
<ThemeToggle />
|
|
334
|
+
</div>
|
|
335
|
+
</aside>
|
|
336
|
+
|
|
337
|
+
<main class="fd-main">
|
|
338
|
+
<slot />
|
|
339
|
+
</main>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<FloatingAIChat
|
|
343
|
+
v-if="showFloatingAI"
|
|
344
|
+
api="/api/docs"
|
|
345
|
+
:suggested-questions="config?.ai?.suggestedQuestions ?? []"
|
|
346
|
+
:ai-label="config?.ai?.aiLabel ?? 'AI'"
|
|
347
|
+
:position="config?.ai?.position ?? 'bottom-right'"
|
|
348
|
+
:floating-style="config?.ai?.floatingStyle ?? 'panel'"
|
|
349
|
+
:trigger-component="triggerComponent"
|
|
350
|
+
/>
|
|
351
|
+
|
|
352
|
+
<SearchDialog v-if="searchOpen" @close="closeSearch" />
|
|
353
|
+
</div>
|
|
354
|
+
</template>
|