@aravindc26/velu 0.10.0 → 0.11.0
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 +1 -1
- package/schema/velu.schema.json +714 -16
- package/src/build.ts +207 -43
- package/src/cli.ts +65 -2
- package/src/engine/_server.mjs +127 -18
- package/src/engine/app/(docs)/[[...slug]]/layout.tsx +87 -0
- package/src/engine/app/(docs)/[[...slug]]/page.tsx +83 -6
- package/src/engine/app/(docs)/layout.tsx +1 -13
- package/src/engine/app/global.css +327 -0
- package/src/engine/app/layout.tsx +3 -7
- package/src/engine/app/search.css +20 -0
- package/src/engine/components/lang-switcher.tsx +95 -0
- package/src/engine/components/product-switcher.tsx +78 -0
- package/src/engine/components/providers.tsx +26 -0
- package/src/engine/components/search.tsx +66 -3
- package/src/engine/components/sidebar-links.tsx +51 -0
- package/src/engine/components/theme-toggle.tsx +39 -0
- package/src/engine/components/version-switcher.tsx +89 -0
- package/src/engine/lib/layout.shared.ts +28 -6
- package/src/engine/lib/navigation-normalize.mjs +456 -0
- package/src/engine/lib/navigation-normalize.ts +488 -0
- package/src/engine/lib/source.ts +14 -0
- package/src/engine/lib/velu.ts +267 -3
- package/src/engine/next.config.mjs +2 -2
- package/src/engine/src/lib/velu.ts +86 -13
- package/src/navigation-normalize.ts +488 -0
- package/src/validate.ts +116 -18
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
5
|
+
import type { VeluProductOption } from '@/lib/velu';
|
|
6
|
+
|
|
7
|
+
function ChevronDownIcon() {
|
|
8
|
+
return (
|
|
9
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
10
|
+
<path d="m6 9 6 6 6-6" />
|
|
11
|
+
</svg>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ProductSwitcher({ products }: { products: VeluProductOption[] }) {
|
|
16
|
+
const pathname = usePathname();
|
|
17
|
+
const [open, setOpen] = useState(false);
|
|
18
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
19
|
+
|
|
20
|
+
const current = useMemo(() => {
|
|
21
|
+
const firstSeg = pathname.split('/').filter(Boolean)[0] ?? '';
|
|
22
|
+
return products.find((p) => p.slug === firstSeg) ?? products[0];
|
|
23
|
+
}, [pathname, products]);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
function handleClick(e: MouseEvent) {
|
|
27
|
+
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
|
28
|
+
}
|
|
29
|
+
document.addEventListener('mousedown', handleClick);
|
|
30
|
+
return () => document.removeEventListener('mousedown', handleClick);
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
if (!current || products.length <= 1) return null;
|
|
34
|
+
|
|
35
|
+
function switchTo(target: VeluProductOption) {
|
|
36
|
+
setOpen(false);
|
|
37
|
+
|
|
38
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
39
|
+
const firstSeg = segments[0] ?? '';
|
|
40
|
+
|
|
41
|
+
if (current && current.slug === firstSeg) {
|
|
42
|
+
// Replace the product segment, keep tab/group/page segments
|
|
43
|
+
const rest = segments.slice(1);
|
|
44
|
+
if (rest.length > 0) {
|
|
45
|
+
window.location.href = '/' + [target.slug, ...rest].join('/');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
window.location.href = target.defaultPath;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="velu-product-switcher-wrap" ref={ref}>
|
|
55
|
+
<button type="button" className="velu-product-switcher" onClick={() => setOpen((v) => !v)}>
|
|
56
|
+
<span className="velu-product-switcher-label">{current.product}</span>
|
|
57
|
+
<ChevronDownIcon />
|
|
58
|
+
</button>
|
|
59
|
+
{open && (
|
|
60
|
+
<div className="velu-product-menu">
|
|
61
|
+
{products.map((product) => (
|
|
62
|
+
<button
|
|
63
|
+
key={product.slug}
|
|
64
|
+
type="button"
|
|
65
|
+
className={`velu-product-option ${product.slug === current.slug ? 'active' : ''}`}
|
|
66
|
+
onClick={() => switchTo(product)}
|
|
67
|
+
>
|
|
68
|
+
<span className="velu-product-option-name">{product.product}</span>
|
|
69
|
+
{product.description && (
|
|
70
|
+
<span className="velu-product-option-desc">{product.description}</span>
|
|
71
|
+
)}
|
|
72
|
+
</button>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import dynamic from 'next/dynamic';
|
|
5
|
+
import { RootProvider } from 'fumadocs-ui/provider/next';
|
|
6
|
+
|
|
7
|
+
const PagefindSearch = dynamic(
|
|
8
|
+
() => import('@/components/search').then((m) => m.PagefindSearch),
|
|
9
|
+
{ ssr: false }
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
interface ProvidersProps {
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
theme?: {
|
|
15
|
+
defaultTheme: string;
|
|
16
|
+
enableSystem: boolean;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function Providers({ children, theme }: ProvidersProps) {
|
|
21
|
+
return (
|
|
22
|
+
<RootProvider theme={theme} search={{ SearchDialog: PagefindSearch }}>
|
|
23
|
+
{children}
|
|
24
|
+
</RootProvider>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useEffect, useRef, useState, useCallback, type ReactNode } from 'react';
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
4
5
|
|
|
5
6
|
interface PagefindResult {
|
|
6
7
|
url: string;
|
|
@@ -19,10 +20,54 @@ interface PagefindResponse {
|
|
|
19
20
|
|
|
20
21
|
interface PagefindInstance {
|
|
21
22
|
init: () => Promise<void>;
|
|
22
|
-
search: (query: string) => Promise<PagefindResponse>;
|
|
23
|
+
search: (query: string, options?: { filters?: Record<string, string | string[]> }) => Promise<PagefindResponse>;
|
|
23
24
|
destroy: () => void;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
interface SearchFilters {
|
|
28
|
+
language?: string;
|
|
29
|
+
version?: string;
|
|
30
|
+
product?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Extract language and version/product from URL path
|
|
34
|
+
// Format: /[lang]/[version|product]/... or /[version|product]/...
|
|
35
|
+
function extractFiltersFromPath(pathname: string): SearchFilters {
|
|
36
|
+
const filters: SearchFilters = {};
|
|
37
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
38
|
+
|
|
39
|
+
// Common language codes (expand as needed)
|
|
40
|
+
const langCodes = new Set(['en', 'ja', 'es', 'fr', 'de', 'zh', 'ko', 'pt', 'ru', 'ar']);
|
|
41
|
+
|
|
42
|
+
if (segments.length === 0) return filters;
|
|
43
|
+
|
|
44
|
+
// Check if first segment is a language code
|
|
45
|
+
const firstSeg = segments[0];
|
|
46
|
+
if (langCodes.has(firstSeg)) {
|
|
47
|
+
filters.language = firstSeg;
|
|
48
|
+
// Look for version/product in second segment
|
|
49
|
+
if (segments.length > 1) {
|
|
50
|
+
const secondSeg = segments[1];
|
|
51
|
+
// Version patterns: v1, v2, v1.0, 1.0, etc.
|
|
52
|
+
if (/^v?\d/.test(secondSeg)) {
|
|
53
|
+
filters.version = secondSeg;
|
|
54
|
+
} else {
|
|
55
|
+
// Could be a product slug
|
|
56
|
+
filters.product = secondSeg;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
// First segment might be version or product
|
|
61
|
+
if (/^v?\d/.test(firstSeg)) {
|
|
62
|
+
filters.version = firstSeg;
|
|
63
|
+
} else {
|
|
64
|
+
filters.product = firstSeg;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return filters;
|
|
69
|
+
}
|
|
70
|
+
|
|
26
71
|
export function PagefindSearch({
|
|
27
72
|
open,
|
|
28
73
|
onOpenChange,
|
|
@@ -33,10 +78,20 @@ export function PagefindSearch({
|
|
|
33
78
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
34
79
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
35
80
|
const pagefindRef = useRef<PagefindInstance | null>(null);
|
|
81
|
+
const pathname = usePathname();
|
|
36
82
|
const [query, setQuery] = useState('');
|
|
37
83
|
const [results, setResults] = useState<PagefindResult[]>([]);
|
|
38
84
|
const [loading, setLoading] = useState(false);
|
|
39
85
|
const [available, setAvailable] = useState(true);
|
|
86
|
+
const [activeFilters, setActiveFilters] = useState<SearchFilters>({});
|
|
87
|
+
|
|
88
|
+
// Extract filters from current URL when search opens
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (open && pathname) {
|
|
91
|
+
const filters = extractFiltersFromPath(pathname);
|
|
92
|
+
setActiveFilters(filters);
|
|
93
|
+
}
|
|
94
|
+
}, [open, pathname]);
|
|
40
95
|
|
|
41
96
|
useEffect(() => {
|
|
42
97
|
async function loadPagefind() {
|
|
@@ -72,7 +127,15 @@ export function PagefindSearch({
|
|
|
72
127
|
}
|
|
73
128
|
setLoading(true);
|
|
74
129
|
try {
|
|
75
|
-
|
|
130
|
+
// Build filters for pagefind
|
|
131
|
+
const filters: Record<string, string> = {};
|
|
132
|
+
if (activeFilters.language) filters.language = activeFilters.language;
|
|
133
|
+
if (activeFilters.version) filters.version = activeFilters.version;
|
|
134
|
+
if (activeFilters.product) filters.product = activeFilters.product;
|
|
135
|
+
|
|
136
|
+
const response = await pagefindRef.current.search(q,
|
|
137
|
+
Object.keys(filters).length > 0 ? { filters } : undefined
|
|
138
|
+
);
|
|
76
139
|
const items = await Promise.all(
|
|
77
140
|
response.results.slice(0, 8).map((r) => r.data())
|
|
78
141
|
);
|
|
@@ -82,7 +145,7 @@ export function PagefindSearch({
|
|
|
82
145
|
}
|
|
83
146
|
setLoading(false);
|
|
84
147
|
},
|
|
85
|
-
[]
|
|
148
|
+
[activeFilters]
|
|
86
149
|
);
|
|
87
150
|
|
|
88
151
|
return (
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getGlobalAnchors, getLanguages } from '@/lib/velu';
|
|
2
|
+
import { ThemeToggle } from '@/components/theme-toggle';
|
|
3
|
+
import { LanguageSwitcher } from '@/components/lang-switcher';
|
|
4
|
+
|
|
5
|
+
function ExternalLinkIcon() {
|
|
6
|
+
return (
|
|
7
|
+
<svg
|
|
8
|
+
className="velu-sidebar-link-icon"
|
|
9
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
10
|
+
viewBox="0 0 24 24"
|
|
11
|
+
fill="none"
|
|
12
|
+
stroke="currentColor"
|
|
13
|
+
strokeWidth={2}
|
|
14
|
+
strokeLinecap="round"
|
|
15
|
+
strokeLinejoin="round"
|
|
16
|
+
>
|
|
17
|
+
<path d="M7 17 17 7" />
|
|
18
|
+
<path d="M7 7h10v10" />
|
|
19
|
+
</svg>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function SidebarLinks() {
|
|
24
|
+
const anchors = getGlobalAnchors();
|
|
25
|
+
const languages = getLanguages();
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="velu-sidebar-footer">
|
|
29
|
+
{anchors.length > 0 && (
|
|
30
|
+
<div className="velu-sidebar-links">
|
|
31
|
+
{anchors.map((anchor) => (
|
|
32
|
+
<a
|
|
33
|
+
key={anchor.href}
|
|
34
|
+
href={anchor.href}
|
|
35
|
+
target="_blank"
|
|
36
|
+
rel="noopener noreferrer"
|
|
37
|
+
className="velu-sidebar-link"
|
|
38
|
+
>
|
|
39
|
+
<span className="velu-sidebar-link-text">{anchor.anchor}</span>
|
|
40
|
+
<ExternalLinkIcon />
|
|
41
|
+
</a>
|
|
42
|
+
))}
|
|
43
|
+
</div>
|
|
44
|
+
)}
|
|
45
|
+
<div className="velu-sidebar-footer-row">
|
|
46
|
+
{languages.length > 1 && <LanguageSwitcher languages={languages} defaultLang={languages[0]} />}
|
|
47
|
+
<ThemeToggle />
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useTheme } from 'next-themes';
|
|
4
|
+
import { useEffect, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
export function ThemeToggle() {
|
|
7
|
+
const { setTheme, resolvedTheme } = useTheme();
|
|
8
|
+
const [mounted, setMounted] = useState(false);
|
|
9
|
+
|
|
10
|
+
useEffect(() => setMounted(true), []);
|
|
11
|
+
|
|
12
|
+
const isDark = mounted ? resolvedTheme === 'dark' : false;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="velu-theme-toggle" style={mounted ? undefined : { opacity: 0 }}>
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
className={`velu-theme-btn ${!isDark ? 'active' : ''}`}
|
|
19
|
+
onClick={() => setTheme('light')}
|
|
20
|
+
aria-label="Light mode"
|
|
21
|
+
>
|
|
22
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" width="16" height="16">
|
|
23
|
+
<circle cx="12" cy="12" r="4" />
|
|
24
|
+
<path d="M12 2v2" /><path d="M12 20v2" /><path d="m4.93 4.93 1.41 1.41" /><path d="m17.66 17.66 1.41 1.41" /><path d="M2 12h2" /><path d="M20 12h2" /><path d="m6.34 17.66-1.41 1.41" /><path d="m19.07 4.93-1.41 1.41" />
|
|
25
|
+
</svg>
|
|
26
|
+
</button>
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
className={`velu-theme-btn ${isDark ? 'active' : ''}`}
|
|
30
|
+
onClick={() => setTheme('dark')}
|
|
31
|
+
aria-label="Dark mode"
|
|
32
|
+
>
|
|
33
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" width="16" height="16">
|
|
34
|
+
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
|
35
|
+
</svg>
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
5
|
+
import type { VeluVersionOption } from '@/lib/velu';
|
|
6
|
+
|
|
7
|
+
function VersionIcon() {
|
|
8
|
+
return (
|
|
9
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
10
|
+
<path d="M4 6h16" />
|
|
11
|
+
<path d="M4 12h16" />
|
|
12
|
+
<path d="M4 18h16" />
|
|
13
|
+
</svg>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ChevronDownIcon() {
|
|
18
|
+
return (
|
|
19
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
20
|
+
<path d="m6 9 6 6 6-6" />
|
|
21
|
+
</svg>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function VersionSwitcher({ versions }: { versions: VeluVersionOption[] }) {
|
|
26
|
+
const pathname = usePathname();
|
|
27
|
+
const [open, setOpen] = useState(false);
|
|
28
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
29
|
+
|
|
30
|
+
const fallback = useMemo(() => versions.find((v) => v.isDefault) ?? versions[0], [versions]);
|
|
31
|
+
|
|
32
|
+
const current = useMemo(() => {
|
|
33
|
+
const firstSeg = pathname.split('/').filter(Boolean)[0] ?? '';
|
|
34
|
+
return versions.find((version) => version.tabSlugs.includes(firstSeg)) ?? fallback;
|
|
35
|
+
}, [pathname, versions, fallback]);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
function handleClick(e: MouseEvent) {
|
|
39
|
+
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
|
40
|
+
}
|
|
41
|
+
document.addEventListener('mousedown', handleClick);
|
|
42
|
+
return () => document.removeEventListener('mousedown', handleClick);
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
if (!fallback || versions.length <= 1) return null;
|
|
46
|
+
|
|
47
|
+
function switchTo(target: VeluVersionOption) {
|
|
48
|
+
setOpen(false);
|
|
49
|
+
|
|
50
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
51
|
+
const firstSeg = segments[0] ?? '';
|
|
52
|
+
|
|
53
|
+
if (current && current.tabSlugs.includes(firstSeg)) {
|
|
54
|
+
const index = current.tabSlugs.indexOf(firstSeg);
|
|
55
|
+
const targetTab = target.tabSlugs[index] ?? target.tabSlugs[0];
|
|
56
|
+
if (targetTab) {
|
|
57
|
+
const rest = segments.slice(1);
|
|
58
|
+
window.location.href = '/' + [targetTab, ...rest].join('/');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
window.location.href = target.defaultPath;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="velu-version-switcher-wrap" ref={ref}>
|
|
68
|
+
<button type="button" className="velu-version-switcher" onClick={() => setOpen((v) => !v)}>
|
|
69
|
+
<VersionIcon />
|
|
70
|
+
<span>{current.version}</span>
|
|
71
|
+
<ChevronDownIcon />
|
|
72
|
+
</button>
|
|
73
|
+
{open && (
|
|
74
|
+
<div className="velu-version-menu">
|
|
75
|
+
{versions.map((version) => (
|
|
76
|
+
<button
|
|
77
|
+
key={version.slug}
|
|
78
|
+
type="button"
|
|
79
|
+
className={`velu-version-option ${version.slug === current.slug ? 'active' : ''}`}
|
|
80
|
+
onClick={() => switchTo(version)}
|
|
81
|
+
>
|
|
82
|
+
{version.version}
|
|
83
|
+
</button>
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -1,17 +1,39 @@
|
|
|
1
1
|
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
|
2
|
-
import {
|
|
2
|
+
import { createElement } from 'react';
|
|
3
|
+
import { VersionSwitcher } from '@/components/version-switcher';
|
|
4
|
+
import { getExternalTabs, getNavbarAnchors, getVersionOptions } from '@/lib/velu';
|
|
3
5
|
|
|
4
6
|
export function baseOptions(): BaseLayoutProps {
|
|
5
7
|
const externalTabs = getExternalTabs();
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const navAnchors = getNavbarAnchors();
|
|
9
|
+
const versions = getVersionOptions();
|
|
10
|
+
|
|
11
|
+
const links = [
|
|
12
|
+
...externalTabs.map((tab: { label: string; href: string }) => ({
|
|
13
|
+
text: tab.label,
|
|
14
|
+
url: tab.href,
|
|
15
|
+
secondary: false,
|
|
16
|
+
})),
|
|
17
|
+
...navAnchors
|
|
18
|
+
.filter((a): a is { anchor: string; href: string } => typeof a.href === 'string' && a.href.length > 0)
|
|
19
|
+
.map((a) => ({
|
|
20
|
+
text: a.anchor,
|
|
21
|
+
url: a.href,
|
|
22
|
+
secondary: true,
|
|
23
|
+
})),
|
|
24
|
+
];
|
|
11
25
|
|
|
12
26
|
return {
|
|
13
27
|
nav: {
|
|
14
28
|
title: 'Velu Docs',
|
|
29
|
+
children:
|
|
30
|
+
versions.length > 1
|
|
31
|
+
? createElement(
|
|
32
|
+
'div',
|
|
33
|
+
{ className: 'velu-header-version-switcher' },
|
|
34
|
+
createElement(VersionSwitcher, { versions })
|
|
35
|
+
)
|
|
36
|
+
: undefined,
|
|
15
37
|
},
|
|
16
38
|
links,
|
|
17
39
|
};
|