@alliance-droid/svelte-docs-system 0.0.1
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/COMPONENTS.md +365 -0
- package/COVERAGE_REPORT.md +663 -0
- package/README.md +42 -0
- package/SEARCH_VERIFICATION.md +229 -0
- package/TEST_SUMMARY.md +344 -0
- package/bin/init.js +821 -0
- package/docs/E2E_TESTS.md +354 -0
- package/docs/TESTING.md +754 -0
- package/docs/de/index.md +41 -0
- package/docs/en/COMPONENTS.md +443 -0
- package/docs/en/api/examples.md +100 -0
- package/docs/en/api/overview.md +69 -0
- package/docs/en/components/index.md +622 -0
- package/docs/en/config/navigation.md +505 -0
- package/docs/en/config/theme-and-colors.md +395 -0
- package/docs/en/getting-started/integration.md +406 -0
- package/docs/en/guides/common-setups.md +651 -0
- package/docs/en/index.md +243 -0
- package/docs/en/markdown.md +102 -0
- package/docs/en/routing.md +64 -0
- package/docs/en/setup.md +52 -0
- package/docs/en/troubleshooting.md +704 -0
- package/docs/es/index.md +41 -0
- package/docs/fr/index.md +41 -0
- package/docs/ja/index.md +41 -0
- package/package.json +40 -0
- package/pagefind.toml +8 -0
- package/postcss.config.js +5 -0
- package/src/app.css +119 -0
- package/src/app.d.ts +13 -0
- package/src/app.html +11 -0
- package/src/lib/assets/favicon.svg +1 -0
- package/src/lib/components/APITable.svelte +120 -0
- package/src/lib/components/APITable.test.ts +153 -0
- package/src/lib/components/Breadcrumbs.svelte +85 -0
- package/src/lib/components/Breadcrumbs.test.ts +148 -0
- package/src/lib/components/Callout.svelte +60 -0
- package/src/lib/components/Callout.test.ts +100 -0
- package/src/lib/components/CodeBlock.svelte +68 -0
- package/src/lib/components/CodeBlock.test.ts +133 -0
- package/src/lib/components/DocLayout.svelte +84 -0
- package/src/lib/components/Footer.svelte +78 -0
- package/src/lib/components/Image.svelte +100 -0
- package/src/lib/components/Image.test.ts +163 -0
- package/src/lib/components/Navbar.svelte +141 -0
- package/src/lib/components/Search.svelte +248 -0
- package/src/lib/components/Sidebar.svelte +110 -0
- package/src/lib/components/Tabs.svelte +48 -0
- package/src/lib/components/Tabs.test.ts +102 -0
- package/src/lib/config.test.ts +140 -0
- package/src/lib/config.ts +179 -0
- package/src/lib/configIntegration.test.ts +272 -0
- package/src/lib/configLoader.ts +231 -0
- package/src/lib/configParser.test.ts +217 -0
- package/src/lib/configParser.ts +234 -0
- package/src/lib/index.ts +34 -0
- package/src/lib/integration.test.ts +426 -0
- package/src/lib/navigationBuilder.test.ts +338 -0
- package/src/lib/navigationBuilder.ts +268 -0
- package/src/lib/performance.test.ts +369 -0
- package/src/lib/routing.test.ts +202 -0
- package/src/lib/routing.ts +127 -0
- package/src/lib/search-functionality.test.ts +493 -0
- package/src/lib/stores/i18n.test.ts +180 -0
- package/src/lib/stores/i18n.ts +143 -0
- package/src/lib/stores/nav.ts +36 -0
- package/src/lib/stores/search.test.ts +140 -0
- package/src/lib/stores/search.ts +162 -0
- package/src/lib/stores/theme.ts +59 -0
- package/src/lib/stores/version.test.ts +139 -0
- package/src/lib/stores/version.ts +111 -0
- package/src/lib/themeCustomization.test.ts +223 -0
- package/src/lib/themeCustomization.ts +212 -0
- package/src/lib/utils/highlight.test.ts +136 -0
- package/src/lib/utils/highlight.ts +100 -0
- package/src/lib/utils/index.ts +7 -0
- package/src/lib/utils/markdown.test.ts +357 -0
- package/src/lib/utils/markdown.ts +77 -0
- package/src/routes/+layout.server.ts +1 -0
- package/src/routes/+layout.svelte +28 -0
- package/src/routes/+page.svelte +165 -0
- package/static/robots.txt +3 -0
- package/svelte.config.js +18 -0
- package/tailwind.config.ts +55 -0
- package/template-starter/.github/workflows/build.yml +40 -0
- package/template-starter/.github/workflows/deploy-github-pages.yml +47 -0
- package/template-starter/.github/workflows/deploy-netlify.yml +41 -0
- package/template-starter/.github/workflows/deploy-vercel.yml +64 -0
- package/template-starter/NPM-PACKAGE-SETUP.md +233 -0
- package/template-starter/README.md +320 -0
- package/template-starter/docs/_config.json +39 -0
- package/template-starter/docs/api/components.md +257 -0
- package/template-starter/docs/api/overview.md +169 -0
- package/template-starter/docs/guides/configuration.md +145 -0
- package/template-starter/docs/guides/github-pages-deployment.md +254 -0
- package/template-starter/docs/guides/netlify-deployment.md +159 -0
- package/template-starter/docs/guides/vercel-deployment.md +131 -0
- package/template-starter/docs/index.md +49 -0
- package/template-starter/docs/setup.md +149 -0
- package/template-starter/package.json +31 -0
- package/template-starter/pagefind.toml +3 -0
- package/template-starter/postcss.config.js +5 -0
- package/template-starter/src/app.css +34 -0
- package/template-starter/src/app.d.ts +13 -0
- package/template-starter/src/app.html +11 -0
- package/template-starter/src/lib/components/APITable.svelte +120 -0
- package/template-starter/src/lib/components/APITable.test.ts +19 -0
- package/template-starter/src/lib/components/Breadcrumbs.svelte +85 -0
- package/template-starter/src/lib/components/Breadcrumbs.test.ts +19 -0
- package/template-starter/src/lib/components/Callout.svelte +60 -0
- package/template-starter/src/lib/components/Callout.test.ts +16 -0
- package/template-starter/src/lib/components/CodeBlock.svelte +68 -0
- package/template-starter/src/lib/components/CodeBlock.test.ts +12 -0
- package/template-starter/src/lib/components/DocLayout.svelte +84 -0
- package/template-starter/src/lib/components/Footer.svelte +78 -0
- package/template-starter/src/lib/components/Image.svelte +100 -0
- package/template-starter/src/lib/components/Image.test.ts +15 -0
- package/template-starter/src/lib/components/Navbar.svelte +141 -0
- package/template-starter/src/lib/components/Search.svelte +248 -0
- package/template-starter/src/lib/components/Sidebar.svelte +110 -0
- package/template-starter/src/lib/components/Tabs.svelte +48 -0
- package/template-starter/src/lib/components/Tabs.test.ts +17 -0
- package/template-starter/src/lib/index.ts +15 -0
- package/template-starter/src/routes/+layout.svelte +28 -0
- package/template-starter/src/routes/+page.svelte +92 -0
- package/template-starter/svelte.config.js +17 -0
- package/template-starter/tailwind.config.ts +17 -0
- package/template-starter/tsconfig.json +13 -0
- package/template-starter/vite.config.ts +6 -0
- package/tests/e2e/example.spec.ts +345 -0
- package/tsconfig.json +20 -0
- package/vite.config.ts +6 -0
- package/vitest.config.ts +34 -0
- package/vitest.setup.ts +21 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { NavSection, NavItem } from '$lib/stores/nav';
|
|
3
|
+
import { sidebarOpen } from '$lib/stores/nav';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
sections = [] as NavSection[],
|
|
7
|
+
currentPath = '',
|
|
8
|
+
onNavigate = () => {},
|
|
9
|
+
}: {
|
|
10
|
+
sections?: NavSection[];
|
|
11
|
+
currentPath?: string;
|
|
12
|
+
onNavigate?: (path: string) => void;
|
|
13
|
+
} = $props();
|
|
14
|
+
|
|
15
|
+
function handleNavClick(path: string) {
|
|
16
|
+
onNavigate(path);
|
|
17
|
+
sidebarOpen.set(false);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isActive(href: string | undefined): boolean {
|
|
21
|
+
if (!href) return false;
|
|
22
|
+
return currentPath === href || currentPath.startsWith(href + '/');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function renderNavItem(item: NavItem) {
|
|
26
|
+
const active = isActive(item.href);
|
|
27
|
+
return {
|
|
28
|
+
label: item.label,
|
|
29
|
+
href: item.href,
|
|
30
|
+
children: item.children,
|
|
31
|
+
active,
|
|
32
|
+
icon: item.icon,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<!-- Sidebar Overlay for Mobile -->
|
|
38
|
+
{#if $sidebarOpen}
|
|
39
|
+
<button
|
|
40
|
+
class="fixed inset-0 z-30 bg-black/20 md:hidden"
|
|
41
|
+
onclick={() => sidebarOpen.set(false)}
|
|
42
|
+
aria-label="Close menu"
|
|
43
|
+
></button>
|
|
44
|
+
{/if}
|
|
45
|
+
|
|
46
|
+
<!-- Sidebar -->
|
|
47
|
+
<aside
|
|
48
|
+
class="fixed left-0 top-16 bottom-0 w-64 overflow-y-auto border-r border-claude-border dark:border-claude-dark-border bg-white dark:bg-claude-dark-bg-secondary transition-transform md:relative md:top-0 {$sidebarOpen
|
|
49
|
+
? 'translate-x-0'
|
|
50
|
+
: '-translate-x-full md:translate-x-0'} z-30 md:z-auto"
|
|
51
|
+
>
|
|
52
|
+
<div class="p-6 space-y-8">
|
|
53
|
+
{#each sections as section (section.title)}
|
|
54
|
+
<div>
|
|
55
|
+
<h3 class="text-sm font-semibold text-claude-text dark:text-claude-dark-text uppercase tracking-wide mb-3 opacity-60">
|
|
56
|
+
{section.title}
|
|
57
|
+
</h3>
|
|
58
|
+
<nav class="space-y-2">
|
|
59
|
+
{#each section.items as item (item.label)}
|
|
60
|
+
<a
|
|
61
|
+
href={item.href || '#'}
|
|
62
|
+
onclick={(e) => {
|
|
63
|
+
if (item.href) {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
handleNavClick(item.href);
|
|
66
|
+
}
|
|
67
|
+
}}
|
|
68
|
+
class="block px-3 py-2 rounded-lg transition-colors {isActive(
|
|
69
|
+
item.href
|
|
70
|
+
)
|
|
71
|
+
? 'bg-claude-accent text-white dark:bg-claude-dark-accent'
|
|
72
|
+
: 'text-claude-text dark:text-claude-dark-text hover:bg-claude-bg-secondary dark:hover:bg-claude-dark-bg'}"
|
|
73
|
+
>
|
|
74
|
+
<span class="flex items-center gap-2">
|
|
75
|
+
{#if item.icon}
|
|
76
|
+
<span class="text-sm">{item.icon}</span>
|
|
77
|
+
{/if}
|
|
78
|
+
<span class="text-sm font-medium">{item.label}</span>
|
|
79
|
+
</span>
|
|
80
|
+
</a>
|
|
81
|
+
|
|
82
|
+
<!-- Nested Items -->
|
|
83
|
+
{#if item.children && item.children.length > 0}
|
|
84
|
+
<div class="ml-3 space-y-1 border-l border-claude-border dark:border-claude-dark-border pl-3">
|
|
85
|
+
{#each item.children as child (child.label)}
|
|
86
|
+
<a
|
|
87
|
+
href={child.href || '#'}
|
|
88
|
+
onclick={(e) => {
|
|
89
|
+
if (child.href) {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
handleNavClick(child.href);
|
|
92
|
+
}
|
|
93
|
+
}}
|
|
94
|
+
class="block px-3 py-1 text-xs rounded transition-colors {isActive(
|
|
95
|
+
child.href
|
|
96
|
+
)
|
|
97
|
+
? 'bg-claude-accent text-white dark:bg-claude-dark-accent'
|
|
98
|
+
: 'text-claude-text-secondary dark:text-claude-dark-text-secondary hover:text-claude-text dark:hover:text-claude-dark-text'}"
|
|
99
|
+
>
|
|
100
|
+
{child.label}
|
|
101
|
+
</a>
|
|
102
|
+
{/each}
|
|
103
|
+
</div>
|
|
104
|
+
{/if}
|
|
105
|
+
{/each}
|
|
106
|
+
</nav>
|
|
107
|
+
</div>
|
|
108
|
+
{/each}
|
|
109
|
+
</div>
|
|
110
|
+
</aside>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Tabs component for switching between content panels
|
|
4
|
+
* Usage:
|
|
5
|
+
* <Tabs items={[{label: 'Tab 1', content: 'Content 1'}]} />
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface TabItem {
|
|
9
|
+
label: string;
|
|
10
|
+
content?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
items: TabItem[];
|
|
15
|
+
defaultIndex?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let { items, defaultIndex = 0 }: Props = $props();
|
|
19
|
+
|
|
20
|
+
let activeIndex = $state(0);
|
|
21
|
+
|
|
22
|
+
$effect(() => {
|
|
23
|
+
activeIndex = defaultIndex;
|
|
24
|
+
});
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<div class="my-4">
|
|
28
|
+
<div class="flex border-b border-gray-200 dark:border-gray-700">
|
|
29
|
+
{#each items as item, index (index)}
|
|
30
|
+
<button
|
|
31
|
+
onclick={() => (activeIndex = index)}
|
|
32
|
+
class="px-4 py-2 font-medium transition-colors {activeIndex === index
|
|
33
|
+
? 'border-b-2 border-blue-500 text-blue-600 dark:text-blue-400'
|
|
34
|
+
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100'}"
|
|
35
|
+
>
|
|
36
|
+
{item.label}
|
|
37
|
+
</button>
|
|
38
|
+
{/each}
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="mt-4 rounded-b-lg bg-gray-50 p-4 dark:bg-gray-900/20">
|
|
42
|
+
{#if items[activeIndex]?.content}
|
|
43
|
+
<div class="prose dark:prose-invert">
|
|
44
|
+
{@html items[activeIndex].content}
|
|
45
|
+
</div>
|
|
46
|
+
{/if}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Unit tests for Tabs component
|
|
5
|
+
* Tests component structure and tab handling logic
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
describe('Tabs Component', () => {
|
|
9
|
+
describe('Tab Props', () => {
|
|
10
|
+
it('should accept array of tabs', () => {
|
|
11
|
+
const tabs = [
|
|
12
|
+
{ label: 'Tab 1', content: 'Content 1' },
|
|
13
|
+
{ label: 'Tab 2', content: 'Content 2' }
|
|
14
|
+
];
|
|
15
|
+
expect(Array.isArray(tabs)).toBe(true);
|
|
16
|
+
expect(tabs.length).toBe(2);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should require label for each tab', () => {
|
|
20
|
+
const tab = { label: 'TypeScript', content: 'code' };
|
|
21
|
+
expect(tab.label).toBeDefined();
|
|
22
|
+
expect(typeof tab.label).toBe('string');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should accept optional default active tab index', () => {
|
|
26
|
+
const defaultActive = 0;
|
|
27
|
+
expect(typeof defaultActive).toBe('number');
|
|
28
|
+
expect(defaultActive).toBeGreaterThanOrEqual(0);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('Tab Structure', () => {
|
|
33
|
+
it('should maintain tab order', () => {
|
|
34
|
+
const tabs = [
|
|
35
|
+
{ label: 'First', content: 'First content' },
|
|
36
|
+
{ label: 'Second', content: 'Second content' },
|
|
37
|
+
{ label: 'Third', content: 'Third content' }
|
|
38
|
+
];
|
|
39
|
+
expect(tabs[0].label).toBe('First');
|
|
40
|
+
expect(tabs[1].label).toBe('Second');
|
|
41
|
+
expect(tabs[2].label).toBe('Third');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should allow empty tab list', () => {
|
|
45
|
+
const tabs: any[] = [];
|
|
46
|
+
expect(tabs.length).toBe(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should support single tab', () => {
|
|
50
|
+
const tabs = [{ label: 'Only Tab', content: 'Content' }];
|
|
51
|
+
expect(tabs.length).toBe(1);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('Tab Content', () => {
|
|
56
|
+
it('should support text content', () => {
|
|
57
|
+
const tab = { label: 'Info', content: 'This is text content' };
|
|
58
|
+
expect(typeof tab.content).toBe('string');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should support HTML content', () => {
|
|
62
|
+
const tab = { label: 'Code', content: '<pre>code</pre>' };
|
|
63
|
+
expect(tab.content).toContain('<');
|
|
64
|
+
expect(tab.content).toContain('>');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should support code blocks', () => {
|
|
68
|
+
const tab = {
|
|
69
|
+
label: 'JavaScript',
|
|
70
|
+
content: 'console.log("hello")'
|
|
71
|
+
};
|
|
72
|
+
expect(tab.label).toBeTruthy();
|
|
73
|
+
expect(tab.content).toBeTruthy();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('Keyboard Navigation', () => {
|
|
78
|
+
it('should support keyboard navigation', () => {
|
|
79
|
+
const supportedKeys = ['ArrowLeft', 'ArrowRight', 'Home', 'End'];
|
|
80
|
+
supportedKeys.forEach((key) => {
|
|
81
|
+
expect(supportedKeys).toContain(key);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('Accessibility', () => {
|
|
87
|
+
it('should have proper ARIA attributes', () => {
|
|
88
|
+
const role = 'tablist';
|
|
89
|
+
const tabRole = 'tab';
|
|
90
|
+
const panelRole = 'tabpanel';
|
|
91
|
+
|
|
92
|
+
expect(role).toBe('tablist');
|
|
93
|
+
expect(tabRole).toBe('tab');
|
|
94
|
+
expect(panelRole).toBe('tabpanel');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should maintain tab index', () => {
|
|
98
|
+
const tabIndex = -1; // initially not in tab order
|
|
99
|
+
expect(typeof tabIndex).toBe('number');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateConfig, mergeConfig, DEFAULT_CONFIG } from './config';
|
|
3
|
+
|
|
4
|
+
describe('Config Validation', () => {
|
|
5
|
+
it('should validate a valid config', () => {
|
|
6
|
+
const config = {
|
|
7
|
+
name: 'My Docs',
|
|
8
|
+
docsRoute: '/docs',
|
|
9
|
+
enableSearch: true,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const result = validateConfig(config);
|
|
13
|
+
expect(result.valid).toBe(true);
|
|
14
|
+
expect(result.errors).toHaveLength(0);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should reject non-object config', () => {
|
|
18
|
+
const result = validateConfig('invalid');
|
|
19
|
+
expect(result.valid).toBe(false);
|
|
20
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should validate docsRoute must be a string', () => {
|
|
24
|
+
const config = { docsRoute: 123 };
|
|
25
|
+
const result = validateConfig(config);
|
|
26
|
+
expect(result.valid).toBe(false);
|
|
27
|
+
expect(result.errors.some((e) => e.includes('docsRoute'))).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should validate docsRoute must start with /', () => {
|
|
31
|
+
const config = { docsRoute: 'docs' };
|
|
32
|
+
const result = validateConfig(config);
|
|
33
|
+
expect(result.valid).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should validate layout is one of the allowed values', () => {
|
|
37
|
+
const config = { layout: 'invalid' };
|
|
38
|
+
const result = validateConfig(config);
|
|
39
|
+
expect(result.valid).toBe(false);
|
|
40
|
+
expect(result.errors.some((e) => e.includes('layout'))).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should validate defaultTheme is light or dark', () => {
|
|
44
|
+
const config = { defaultTheme: 'blue' };
|
|
45
|
+
const result = validateConfig(config);
|
|
46
|
+
expect(result.valid).toBe(false);
|
|
47
|
+
expect(result.errors.some((e) => e.includes('defaultTheme'))).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should accept valid theme object', () => {
|
|
51
|
+
const config = {
|
|
52
|
+
theme: {
|
|
53
|
+
primary: '#ff0000',
|
|
54
|
+
secondary: '#00ff00',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const result = validateConfig(config);
|
|
59
|
+
expect(result.valid).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should reject invalid theme object', () => {
|
|
63
|
+
const config = { theme: 'not-an-object' };
|
|
64
|
+
const result = validateConfig(config);
|
|
65
|
+
expect(result.valid).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('Config Merging', () => {
|
|
70
|
+
it('should merge user config with defaults', () => {
|
|
71
|
+
const userConfig = { name: 'Custom Docs' };
|
|
72
|
+
const merged = mergeConfig(userConfig);
|
|
73
|
+
|
|
74
|
+
expect(merged.name).toBe('Custom Docs');
|
|
75
|
+
expect(merged.docsRoute).toBe(DEFAULT_CONFIG.docsRoute);
|
|
76
|
+
expect(merged.enableSearch).toBe(DEFAULT_CONFIG.enableSearch);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should override defaults with user values', () => {
|
|
80
|
+
const userConfig = {
|
|
81
|
+
docsRoute: '/help',
|
|
82
|
+
enableSearch: false,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const merged = mergeConfig(userConfig);
|
|
86
|
+
expect(merged.docsRoute).toBe('/help');
|
|
87
|
+
expect(merged.enableSearch).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should merge nested theme objects', () => {
|
|
91
|
+
const userConfig = {
|
|
92
|
+
theme: {
|
|
93
|
+
primary: '#ff0000',
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const merged = mergeConfig(userConfig);
|
|
98
|
+
expect(merged.theme?.primary).toBe('#ff0000');
|
|
99
|
+
expect(merged.theme?.secondary).toBe(DEFAULT_CONFIG.theme?.secondary);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should merge nested navigation objects', () => {
|
|
103
|
+
const userConfig = {
|
|
104
|
+
navigation: {
|
|
105
|
+
title: 'Custom Nav',
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const merged = mergeConfig(userConfig);
|
|
110
|
+
expect(merged.navigation?.title).toBe('Custom Nav');
|
|
111
|
+
expect(merged.navigation?.autoGenerate).toBe(DEFAULT_CONFIG.navigation?.autoGenerate);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should handle null/undefined values correctly', () => {
|
|
115
|
+
const userConfig: any = {
|
|
116
|
+
name: null,
|
|
117
|
+
docsRoute: undefined,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const merged = mergeConfig(userConfig);
|
|
121
|
+
expect(merged.name).toBeNull();
|
|
122
|
+
expect(merged.docsRoute).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('DEFAULT_CONFIG', () => {
|
|
127
|
+
it('should have a valid default configuration', () => {
|
|
128
|
+
const result = validateConfig(DEFAULT_CONFIG);
|
|
129
|
+
expect(result.valid).toBe(true);
|
|
130
|
+
expect(result.errors).toHaveLength(0);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should have sensible default values', () => {
|
|
134
|
+
expect(DEFAULT_CONFIG.docsRoute).toBe('/docs');
|
|
135
|
+
expect(DEFAULT_CONFIG.enableSearch).toBe(true);
|
|
136
|
+
expect(DEFAULT_CONFIG.darkMode).toBe(true);
|
|
137
|
+
expect(DEFAULT_CONFIG.breadcrumbs).toBe(true);
|
|
138
|
+
expect(DEFAULT_CONFIG.prevNext).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration schema and types for the documentation system
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ThemeConfig {
|
|
6
|
+
/** Primary color (hex, rgb, or CSS color name) */
|
|
7
|
+
primary?: string;
|
|
8
|
+
/** Secondary color */
|
|
9
|
+
secondary?: string;
|
|
10
|
+
/** Text color for light mode */
|
|
11
|
+
textLight?: string;
|
|
12
|
+
/** Text color for dark mode */
|
|
13
|
+
textDark?: string;
|
|
14
|
+
/** Background color for light mode */
|
|
15
|
+
bgLight?: string;
|
|
16
|
+
/** Background color for dark mode */
|
|
17
|
+
bgDark?: string;
|
|
18
|
+
/** Font family for body text */
|
|
19
|
+
fontFamily?: string;
|
|
20
|
+
/** Font family for headings */
|
|
21
|
+
headingFont?: string;
|
|
22
|
+
/** Sidebar background color */
|
|
23
|
+
sidebarBg?: string;
|
|
24
|
+
/** Navbar background color */
|
|
25
|
+
navbarBg?: string;
|
|
26
|
+
/** Code block background color */
|
|
27
|
+
codeBg?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface NavItem {
|
|
31
|
+
label: string;
|
|
32
|
+
href?: string;
|
|
33
|
+
children?: NavItem[];
|
|
34
|
+
icon?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface NavSection {
|
|
38
|
+
title: string;
|
|
39
|
+
items: NavItem[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface NavigationConfig {
|
|
43
|
+
title?: string;
|
|
44
|
+
logo?: string;
|
|
45
|
+
sections?: NavSection[];
|
|
46
|
+
/** Auto-generate navigation from folder structure */
|
|
47
|
+
autoGenerate?: boolean;
|
|
48
|
+
/** Exclude paths from auto-generation */
|
|
49
|
+
excludePaths?: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ConfigOptions {
|
|
53
|
+
/** Name of the documentation project */
|
|
54
|
+
name?: string;
|
|
55
|
+
/** Mount point for docs (e.g., /docs, /help, /guides) */
|
|
56
|
+
docsRoute?: string;
|
|
57
|
+
/** Path to docs folder relative to project root */
|
|
58
|
+
docsFolderPath?: string;
|
|
59
|
+
/** Base URL path for links */
|
|
60
|
+
basePath?: string;
|
|
61
|
+
/** Theme configuration */
|
|
62
|
+
theme?: ThemeConfig;
|
|
63
|
+
/** Navigation configuration */
|
|
64
|
+
navigation?: NavigationConfig;
|
|
65
|
+
/** Enable search functionality */
|
|
66
|
+
enableSearch?: boolean;
|
|
67
|
+
/** Custom CSS file path */
|
|
68
|
+
customCss?: string;
|
|
69
|
+
/** Layout template name (default, minimal, full-width) */
|
|
70
|
+
layout?: 'default' | 'minimal' | 'full-width';
|
|
71
|
+
/** Enable dark mode toggle */
|
|
72
|
+
darkMode?: boolean;
|
|
73
|
+
/** Default theme (light or dark) */
|
|
74
|
+
defaultTheme?: 'light' | 'dark';
|
|
75
|
+
/** Enable breadcrumbs */
|
|
76
|
+
breadcrumbs?: boolean;
|
|
77
|
+
/** Enable prev/next navigation */
|
|
78
|
+
prevNext?: boolean;
|
|
79
|
+
/** Site title for header */
|
|
80
|
+
siteTitle?: string;
|
|
81
|
+
/** Site description for meta tags */
|
|
82
|
+
siteDescription?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Default configuration values
|
|
87
|
+
*/
|
|
88
|
+
export const DEFAULT_CONFIG: ConfigOptions = {
|
|
89
|
+
name: 'Documentation',
|
|
90
|
+
docsRoute: '/docs',
|
|
91
|
+
docsFolderPath: './docs',
|
|
92
|
+
basePath: '/docs',
|
|
93
|
+
enableSearch: true,
|
|
94
|
+
layout: 'default',
|
|
95
|
+
darkMode: true,
|
|
96
|
+
defaultTheme: 'light',
|
|
97
|
+
breadcrumbs: true,
|
|
98
|
+
prevNext: true,
|
|
99
|
+
theme: {
|
|
100
|
+
primary: '#0066cc',
|
|
101
|
+
secondary: '#ff6b6b',
|
|
102
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
103
|
+
headingFont: 'system-ui, -apple-system, sans-serif',
|
|
104
|
+
},
|
|
105
|
+
navigation: {
|
|
106
|
+
autoGenerate: true,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Validates a configuration object
|
|
112
|
+
*/
|
|
113
|
+
export function validateConfig(config: unknown): { valid: boolean; errors: string[] } {
|
|
114
|
+
const errors: string[] = [];
|
|
115
|
+
|
|
116
|
+
if (typeof config !== 'object' || config === null) {
|
|
117
|
+
errors.push('Configuration must be an object');
|
|
118
|
+
return { valid: false, errors };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const cfg = config as Record<string, unknown>;
|
|
122
|
+
|
|
123
|
+
// Validate docsRoute
|
|
124
|
+
if (cfg.docsRoute !== undefined) {
|
|
125
|
+
if (typeof cfg.docsRoute !== 'string') {
|
|
126
|
+
errors.push('docsRoute must be a string');
|
|
127
|
+
} else if (!cfg.docsRoute.startsWith('/')) {
|
|
128
|
+
errors.push('docsRoute must start with /');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Validate layout
|
|
133
|
+
if (cfg.layout !== undefined) {
|
|
134
|
+
if (!['default', 'minimal', 'full-width'].includes(cfg.layout as string)) {
|
|
135
|
+
errors.push('layout must be one of: default, minimal, full-width');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Validate defaultTheme
|
|
140
|
+
if (cfg.defaultTheme !== undefined) {
|
|
141
|
+
if (!['light', 'dark'].includes(cfg.defaultTheme as string)) {
|
|
142
|
+
errors.push('defaultTheme must be either light or dark');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Validate theme config
|
|
147
|
+
if (cfg.theme !== undefined) {
|
|
148
|
+
if (typeof cfg.theme !== 'object' || cfg.theme === null) {
|
|
149
|
+
errors.push('theme must be an object');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Validate navigation config
|
|
154
|
+
if (cfg.navigation !== undefined) {
|
|
155
|
+
if (typeof cfg.navigation !== 'object' || cfg.navigation === null) {
|
|
156
|
+
errors.push('navigation must be an object');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { valid: errors.length === 0, errors };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Merges user config with defaults
|
|
165
|
+
*/
|
|
166
|
+
export function mergeConfig(userConfig: ConfigOptions, defaults: ConfigOptions = DEFAULT_CONFIG): ConfigOptions {
|
|
167
|
+
return {
|
|
168
|
+
...defaults,
|
|
169
|
+
...userConfig,
|
|
170
|
+
theme: {
|
|
171
|
+
...defaults.theme,
|
|
172
|
+
...(userConfig.theme || {}),
|
|
173
|
+
},
|
|
174
|
+
navigation: {
|
|
175
|
+
...defaults.navigation,
|
|
176
|
+
...(userConfig.navigation || {}),
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|