@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,148 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Unit tests for Breadcrumbs component
|
|
5
|
+
* Tests breadcrumb navigation structure and routing
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
describe('Breadcrumbs Component', () => {
|
|
9
|
+
describe('Breadcrumb Items', () => {
|
|
10
|
+
it('should accept array of breadcrumb items', () => {
|
|
11
|
+
const items = [
|
|
12
|
+
{ label: 'Home', href: '/' },
|
|
13
|
+
{ label: 'Docs', href: '/docs' },
|
|
14
|
+
{ label: 'API', href: '/docs/api' }
|
|
15
|
+
];
|
|
16
|
+
expect(Array.isArray(items)).toBe(true);
|
|
17
|
+
expect(items.length).toBe(3);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should require label for each item', () => {
|
|
21
|
+
const item = { label: 'Documentation', href: '/docs' };
|
|
22
|
+
expect(item.label).toBeDefined();
|
|
23
|
+
expect(typeof item.label).toBe('string');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should require href for navigation links', () => {
|
|
27
|
+
const item = { label: 'API', href: '/api' };
|
|
28
|
+
expect(item.href).toBeDefined();
|
|
29
|
+
expect(typeof item.href).toBe('string');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should handle home item', () => {
|
|
33
|
+
const homeItem = { label: 'Home', href: '/' };
|
|
34
|
+
expect(homeItem.href).toBe('/');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('Breadcrumb Structure', () => {
|
|
39
|
+
it('should maintain breadcrumb order', () => {
|
|
40
|
+
const breadcrumbs = [
|
|
41
|
+
{ label: 'Home', href: '/' },
|
|
42
|
+
{ label: 'Products', href: '/products' },
|
|
43
|
+
{ label: 'Books', href: '/products/books' },
|
|
44
|
+
{ label: 'JavaScript Guide' }
|
|
45
|
+
];
|
|
46
|
+
expect(breadcrumbs[0].label).toBe('Home');
|
|
47
|
+
expect(breadcrumbs[breadcrumbs.length - 1].label).toBe('JavaScript Guide');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should support single breadcrumb', () => {
|
|
51
|
+
const items = [{ label: 'Home', href: '/' }];
|
|
52
|
+
expect(items.length).toBe(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should support current page as last item without link', () => {
|
|
56
|
+
const items = [
|
|
57
|
+
{ label: 'Home', href: '/' },
|
|
58
|
+
{ label: 'Current Page' }
|
|
59
|
+
];
|
|
60
|
+
const lastItem = items[items.length - 1];
|
|
61
|
+
expect('href' in lastItem).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('Navigation', () => {
|
|
66
|
+
it('should have proper href attributes', () => {
|
|
67
|
+
const item = { label: 'Docs', href: '/docs' };
|
|
68
|
+
expect(item.href).toMatch(/^\//);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should support absolute URLs', () => {
|
|
72
|
+
const item = { label: 'External', href: 'https://example.com' };
|
|
73
|
+
expect(item.href).toContain('http');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should support relative paths', () => {
|
|
77
|
+
const item = { label: 'Sibling', href: '../other' };
|
|
78
|
+
expect(item.href).toContain('..');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('Separators', () => {
|
|
83
|
+
it('should have separators between items', () => {
|
|
84
|
+
const separator = '/';
|
|
85
|
+
expect(typeof separator).toBe('string');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should not show separator after last item', () => {
|
|
89
|
+
const items = [
|
|
90
|
+
{ label: 'Home', href: '/' },
|
|
91
|
+
{ label: 'Current' }
|
|
92
|
+
];
|
|
93
|
+
const showSeparator = (index: number) => index < items.length - 1;
|
|
94
|
+
expect(showSeparator(0)).toBe(true);
|
|
95
|
+
expect(showSeparator(1)).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('Accessibility', () => {
|
|
100
|
+
it('should have nav ARIA landmark', () => {
|
|
101
|
+
const role = 'navigation';
|
|
102
|
+
expect(role).toBe('navigation');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should have proper ARIA attributes', () => {
|
|
106
|
+
const ariaLabel = 'Breadcrumb navigation';
|
|
107
|
+
expect(ariaLabel).toBeTruthy();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should mark current page as current', () => {
|
|
111
|
+
const ariaCurrent = 'page';
|
|
112
|
+
expect(ariaCurrent).toBe('page');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should have proper link semantics', () => {
|
|
116
|
+
const link = { label: 'Docs', href: '/docs' };
|
|
117
|
+
expect(link.href).toBeTruthy();
|
|
118
|
+
expect(link.label).toBeTruthy();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('Styling', () => {
|
|
123
|
+
it('should support custom classes', () => {
|
|
124
|
+
const customClass = 'text-sm text-gray-600';
|
|
125
|
+
expect(customClass).toContain('text-');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should support active/current state styling', () => {
|
|
129
|
+
const activeClass = 'text-gray-900 font-semibold';
|
|
130
|
+
expect(activeClass).toBeTruthy();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should be responsive', () => {
|
|
134
|
+
const isResponsive = true;
|
|
135
|
+
expect(isResponsive).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('Schema/Structured Data', () => {
|
|
140
|
+
it('should support breadcrumbList schema', () => {
|
|
141
|
+
const schema = {
|
|
142
|
+
'@context': 'https://schema.org',
|
|
143
|
+
'@type': 'BreadcrumbList'
|
|
144
|
+
};
|
|
145
|
+
expect(schema['@type']).toBe('BreadcrumbList');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Callout component for documentation
|
|
4
|
+
* Displays alert boxes with different variants: info, warning, success, error
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
type Variant = 'info' | 'warning' | 'success' | 'error';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
variant?: Variant;
|
|
11
|
+
title?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { variant = 'info', title, children }: Props & { children?: any } = $props();
|
|
15
|
+
|
|
16
|
+
const variantConfig: Record<Variant, { bgColor: string; borderColor: string; textColor: string; iconClass: string }> = {
|
|
17
|
+
info: {
|
|
18
|
+
bgColor: 'bg-blue-50 dark:bg-blue-900/20',
|
|
19
|
+
borderColor: 'border-blue-200 dark:border-blue-700',
|
|
20
|
+
textColor: 'text-blue-900 dark:text-blue-100',
|
|
21
|
+
iconClass: 'fa-circle-info text-blue-500'
|
|
22
|
+
},
|
|
23
|
+
warning: {
|
|
24
|
+
bgColor: 'bg-amber-50 dark:bg-amber-900/20',
|
|
25
|
+
borderColor: 'border-amber-200 dark:border-amber-700',
|
|
26
|
+
textColor: 'text-amber-900 dark:text-amber-100',
|
|
27
|
+
iconClass: 'fa-triangle-exclamation text-amber-500'
|
|
28
|
+
},
|
|
29
|
+
success: {
|
|
30
|
+
bgColor: 'bg-green-50 dark:bg-green-900/20',
|
|
31
|
+
borderColor: 'border-green-200 dark:border-green-700',
|
|
32
|
+
textColor: 'text-green-900 dark:text-green-100',
|
|
33
|
+
iconClass: 'fa-circle-check text-green-500'
|
|
34
|
+
},
|
|
35
|
+
error: {
|
|
36
|
+
bgColor: 'bg-red-50 dark:bg-red-900/20',
|
|
37
|
+
borderColor: 'border-red-200 dark:border-red-700',
|
|
38
|
+
textColor: 'text-red-900 dark:text-red-100',
|
|
39
|
+
iconClass: 'fa-circle-xmark text-red-500'
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const config = $derived(variantConfig[variant]);
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<div class="my-4 rounded-lg border-l-4 p-4 {config.bgColor} {config.borderColor} {config.textColor}">
|
|
47
|
+
<div class="flex items-start gap-3">
|
|
48
|
+
<i class="fa-solid {config.iconClass} mt-0.5 flex-shrink-0"></i>
|
|
49
|
+
<div class="flex-1">
|
|
50
|
+
{#if title}
|
|
51
|
+
<h4 class="font-semibold">{title}</h4>
|
|
52
|
+
{/if}
|
|
53
|
+
<div class="text-sm">
|
|
54
|
+
{#if children}
|
|
55
|
+
{@render children()}
|
|
56
|
+
{/if}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Unit tests for Callout component
|
|
5
|
+
* Tests component structure and prop handling
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
describe('Callout Component', () => {
|
|
9
|
+
describe('Props', () => {
|
|
10
|
+
it('should have default variant as info', () => {
|
|
11
|
+
const defaultVariant = 'info';
|
|
12
|
+
expect(['info', 'warning', 'success', 'error']).toContain(defaultVariant);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should accept valid variants', () => {
|
|
16
|
+
const validVariants = ['info', 'warning', 'success', 'error'];
|
|
17
|
+
validVariants.forEach((variant) => {
|
|
18
|
+
expect(validVariants).toContain(variant);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should accept optional title', () => {
|
|
23
|
+
const props = {
|
|
24
|
+
title: 'Important Note',
|
|
25
|
+
variant: 'info' as const
|
|
26
|
+
};
|
|
27
|
+
expect(props.title).toBeDefined();
|
|
28
|
+
expect(typeof props.title).toBe('string');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should accept children slot', () => {
|
|
32
|
+
const hasChildren = true;
|
|
33
|
+
expect(hasChildren).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('Variant Configuration', () => {
|
|
38
|
+
const variantConfig = {
|
|
39
|
+
info: {
|
|
40
|
+
bgColor: 'bg-blue-50 dark:bg-blue-900/20',
|
|
41
|
+
borderColor: 'border-blue-200 dark:border-blue-700',
|
|
42
|
+
textColor: 'text-blue-900 dark:text-blue-100',
|
|
43
|
+
iconClass: 'fa-circle-info text-blue-500'
|
|
44
|
+
},
|
|
45
|
+
warning: {
|
|
46
|
+
bgColor: 'bg-amber-50 dark:bg-amber-900/20',
|
|
47
|
+
borderColor: 'border-amber-200 dark:border-amber-700',
|
|
48
|
+
textColor: 'text-amber-900 dark:text-amber-100',
|
|
49
|
+
iconClass: 'fa-triangle-exclamation text-amber-500'
|
|
50
|
+
},
|
|
51
|
+
success: {
|
|
52
|
+
bgColor: 'bg-green-50 dark:bg-green-900/20',
|
|
53
|
+
borderColor: 'border-green-200 dark:border-green-700',
|
|
54
|
+
textColor: 'text-green-900 dark:text-green-100',
|
|
55
|
+
iconClass: 'fa-circle-check text-green-500'
|
|
56
|
+
},
|
|
57
|
+
error: {
|
|
58
|
+
bgColor: 'bg-red-50 dark:bg-red-900/20',
|
|
59
|
+
borderColor: 'border-red-200 dark:border-red-700',
|
|
60
|
+
textColor: 'text-red-900 dark:text-red-100',
|
|
61
|
+
iconClass: 'fa-circle-xmark text-red-500'
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
it('should have all required properties for each variant', () => {
|
|
66
|
+
Object.entries(variantConfig).forEach(([variant, config]) => {
|
|
67
|
+
expect(config).toHaveProperty('bgColor');
|
|
68
|
+
expect(config).toHaveProperty('borderColor');
|
|
69
|
+
expect(config).toHaveProperty('textColor');
|
|
70
|
+
expect(config).toHaveProperty('iconClass');
|
|
71
|
+
expect(config.bgColor).toBeTruthy();
|
|
72
|
+
expect(config.borderColor).toBeTruthy();
|
|
73
|
+
expect(config.textColor).toBeTruthy();
|
|
74
|
+
expect(config.iconClass).toBeTruthy();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should use correct icon classes for each variant', () => {
|
|
79
|
+
expect(variantConfig.info.iconClass).toContain('circle-info');
|
|
80
|
+
expect(variantConfig.warning.iconClass).toContain('triangle-exclamation');
|
|
81
|
+
expect(variantConfig.success.iconClass).toContain('circle-check');
|
|
82
|
+
expect(variantConfig.error.iconClass).toContain('circle-xmark');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should include dark mode classes', () => {
|
|
86
|
+
Object.values(variantConfig).forEach((config) => {
|
|
87
|
+
expect(config.bgColor).toMatch(/dark:/);
|
|
88
|
+
expect(config.borderColor).toMatch(/dark:/);
|
|
89
|
+
expect(config.textColor).toMatch(/dark:/);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('Accessibility', () => {
|
|
95
|
+
it('should support icon from FontAwesome', () => {
|
|
96
|
+
const iconClass = 'fa-solid fa-circle-info text-blue-500';
|
|
97
|
+
expect(iconClass).toMatch(/fa-(solid|regular|light|brands)/);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* CodeBlock component with copy button
|
|
4
|
+
* Shows code with optional language label
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
code: string;
|
|
9
|
+
language?: string;
|
|
10
|
+
filename?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { code, language = 'plaintext', filename }: Props = $props();
|
|
14
|
+
|
|
15
|
+
let copied = $state(false);
|
|
16
|
+
|
|
17
|
+
function copyToClipboard() {
|
|
18
|
+
navigator.clipboard.writeText(code);
|
|
19
|
+
copied = true;
|
|
20
|
+
setTimeout(() => {
|
|
21
|
+
copied = false;
|
|
22
|
+
}, 2000);
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<div class="my-4 overflow-hidden rounded-lg border border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-900">
|
|
27
|
+
{#if filename}
|
|
28
|
+
<div class="flex items-center justify-between bg-gray-200 px-4 py-2 dark:bg-gray-800">
|
|
29
|
+
<span class="text-sm font-mono font-semibold text-gray-700 dark:text-gray-300">{filename}</span>
|
|
30
|
+
{#if language}
|
|
31
|
+
<span class="text-xs text-gray-600 dark:text-gray-400">{language}</span>
|
|
32
|
+
{/if}
|
|
33
|
+
</div>
|
|
34
|
+
{/if}
|
|
35
|
+
|
|
36
|
+
<div class="relative">
|
|
37
|
+
<button
|
|
38
|
+
onclick={copyToClipboard}
|
|
39
|
+
class="absolute right-2 top-2 rounded bg-gray-300 px-3 py-1 text-xs font-semibold text-gray-700 hover:bg-gray-400 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 transition-colors"
|
|
40
|
+
>
|
|
41
|
+
{#if copied}
|
|
42
|
+
<i class="fa-solid fa-check mr-1"></i>Copied!
|
|
43
|
+
{:else}
|
|
44
|
+
<i class="fa-solid fa-copy mr-1"></i>Copy
|
|
45
|
+
{/if}
|
|
46
|
+
</button>
|
|
47
|
+
|
|
48
|
+
<pre class="overflow-x-auto p-4 font-mono text-sm text-gray-800 dark:text-gray-100"><code>{code}</code></pre>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<style>
|
|
53
|
+
pre {
|
|
54
|
+
background-color: transparent;
|
|
55
|
+
padding: 1rem;
|
|
56
|
+
overflow-x: auto;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
code {
|
|
60
|
+
font-family: 'Courier New', monospace;
|
|
61
|
+
font-size: 0.875rem;
|
|
62
|
+
color: #1f2937;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
:global(.dark) code {
|
|
66
|
+
color: #f3f4f6;
|
|
67
|
+
}
|
|
68
|
+
</style>
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Unit tests for CodeBlock component
|
|
5
|
+
* Tests code formatting and syntax highlighting
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
describe('CodeBlock Component', () => {
|
|
9
|
+
describe('Code Input', () => {
|
|
10
|
+
it('should accept code as string', () => {
|
|
11
|
+
const code = 'console.log("hello")';
|
|
12
|
+
expect(typeof code).toBe('string');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should accept empty code string', () => {
|
|
16
|
+
const code = '';
|
|
17
|
+
expect(code.length).toBe(0);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should preserve whitespace and indentation', () => {
|
|
21
|
+
const code = `function example() {
|
|
22
|
+
return true;
|
|
23
|
+
}`;
|
|
24
|
+
expect(code).toContain(' ');
|
|
25
|
+
expect(code).toContain('\n');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should handle special characters', () => {
|
|
29
|
+
const code = 'const str = "hello<world>&friends";';
|
|
30
|
+
expect(code).toContain('<');
|
|
31
|
+
expect(code).toContain('>');
|
|
32
|
+
expect(code).toContain('&');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('Language Detection', () => {
|
|
37
|
+
it('should support common programming languages', () => {
|
|
38
|
+
const languages = ['javascript', 'typescript', 'python', 'rust', 'bash', 'json', 'html', 'css'];
|
|
39
|
+
languages.forEach((lang) => {
|
|
40
|
+
expect(typeof lang).toBe('string');
|
|
41
|
+
expect(lang.length).toBeGreaterThan(0);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should accept language as prop', () => {
|
|
46
|
+
const language = 'typescript';
|
|
47
|
+
expect(typeof language).toBe('string');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should default to plain text if language not provided', () => {
|
|
51
|
+
const defaultLanguage = 'plaintext';
|
|
52
|
+
expect(defaultLanguage).toBeDefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should detect language from code fence', () => {
|
|
56
|
+
const fence = '```javascript';
|
|
57
|
+
const match = fence.match(/```(\w+)?/);
|
|
58
|
+
expect(match).toBeTruthy();
|
|
59
|
+
if (match && match[1]) {
|
|
60
|
+
expect(match[1]).toBe('javascript');
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('Syntax Highlighting', () => {
|
|
66
|
+
it('should apply syntax highlighting tokens', () => {
|
|
67
|
+
const token = {
|
|
68
|
+
type: 'keyword',
|
|
69
|
+
content: 'function',
|
|
70
|
+
class: 'hljs-keyword'
|
|
71
|
+
};
|
|
72
|
+
expect(token.type).toBeDefined();
|
|
73
|
+
expect(token.content).toBeDefined();
|
|
74
|
+
expect(token.class).toBeTruthy();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should support dark mode highlighting', () => {
|
|
78
|
+
const colorSchemes = ['light', 'dark'];
|
|
79
|
+
colorSchemes.forEach((scheme) => {
|
|
80
|
+
expect(['light', 'dark']).toContain(scheme);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('Copy Button', () => {
|
|
86
|
+
it('should have copy functionality', () => {
|
|
87
|
+
const hasCopyButton = true;
|
|
88
|
+
expect(hasCopyButton).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should copy entire code block', () => {
|
|
92
|
+
const code = 'const x = 42;';
|
|
93
|
+
const copied = code;
|
|
94
|
+
expect(copied).toBe(code);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('Line Numbers', () => {
|
|
99
|
+
it('should support optional line numbers', () => {
|
|
100
|
+
const showLineNumbers = true;
|
|
101
|
+
expect(typeof showLineNumbers).toBe('boolean');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should number lines correctly', () => {
|
|
105
|
+
const lines = [
|
|
106
|
+
'line 1',
|
|
107
|
+
'line 2',
|
|
108
|
+
'line 3'
|
|
109
|
+
];
|
|
110
|
+
lines.forEach((line, index) => {
|
|
111
|
+
expect(index + 1).toBeGreaterThan(0);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('Accessibility', () => {
|
|
117
|
+
it('should be keyboard accessible', () => {
|
|
118
|
+
const canCopyWithKeyboard = true;
|
|
119
|
+
expect(canCopyWithKeyboard).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should have proper ARIA labels', () => {
|
|
123
|
+
const ariaLabel = 'Code block, javascript';
|
|
124
|
+
expect(ariaLabel).toBeTruthy();
|
|
125
|
+
expect(typeof ariaLabel).toBe('string');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should announce copy feedback to screen readers', () => {
|
|
129
|
+
const announcement = 'Code copied to clipboard';
|
|
130
|
+
expect(announcement).toBeTruthy();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import { theme } from '$lib/stores/theme';
|
|
4
|
+
import Navbar from './Navbar.svelte';
|
|
5
|
+
import Sidebar from './Sidebar.svelte';
|
|
6
|
+
import Footer from './Footer.svelte';
|
|
7
|
+
import type { NavSection } from '$lib/stores/nav';
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
title = 'Documentation',
|
|
11
|
+
logo = null,
|
|
12
|
+
sections = [] as NavSection[],
|
|
13
|
+
copyright = '© 2026 Documentation System',
|
|
14
|
+
footerLinks = [] as { label: string; href: string }[],
|
|
15
|
+
children,
|
|
16
|
+
}: {
|
|
17
|
+
title?: string;
|
|
18
|
+
logo?: string | null;
|
|
19
|
+
sections?: NavSection[];
|
|
20
|
+
copyright?: string;
|
|
21
|
+
footerLinks?: { label: string; href: string }[];
|
|
22
|
+
children: any;
|
|
23
|
+
} = $props();
|
|
24
|
+
|
|
25
|
+
let currentPath = $state('/');
|
|
26
|
+
let isMounted = $state(false);
|
|
27
|
+
|
|
28
|
+
onMount(() => {
|
|
29
|
+
// Initialize theme on mount
|
|
30
|
+
const storedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null;
|
|
31
|
+
if (storedTheme) {
|
|
32
|
+
theme.set(storedTheme);
|
|
33
|
+
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
34
|
+
theme.set('dark');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
isMounted = true;
|
|
38
|
+
|
|
39
|
+
// Update currentPath
|
|
40
|
+
currentPath = window.location.pathname;
|
|
41
|
+
|
|
42
|
+
// Listen to route changes
|
|
43
|
+
const handleNavigation = () => {
|
|
44
|
+
currentPath = window.location.pathname;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
window.addEventListener('popstate', handleNavigation);
|
|
48
|
+
return () => {
|
|
49
|
+
window.removeEventListener('popstate', handleNavigation);
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function handleNavigate(path: string) {
|
|
54
|
+
currentPath = path;
|
|
55
|
+
window.history.pushState(null, '', path);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function handleLogoClick() {
|
|
59
|
+
handleNavigate('/');
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<div class="min-h-screen flex flex-col bg-white dark:bg-claude-dark-bg text-claude-text dark:text-claude-dark-text transition-colors">
|
|
64
|
+
<!-- Navbar -->
|
|
65
|
+
<Navbar {title} {logo} onLogoClick={handleLogoClick} />
|
|
66
|
+
|
|
67
|
+
<!-- Main Content Area -->
|
|
68
|
+
<div class="flex flex-1 overflow-hidden">
|
|
69
|
+
<!-- Sidebar -->
|
|
70
|
+
<Sidebar {sections} {currentPath} onNavigate={handleNavigate} />
|
|
71
|
+
|
|
72
|
+
<!-- Main Content -->
|
|
73
|
+
<main class="flex-1 overflow-y-auto">
|
|
74
|
+
<div class="doc-container max-w-4xl mx-auto px-4 sm:px-6 py-8">
|
|
75
|
+
<article class="doc-content">
|
|
76
|
+
{@render children()}
|
|
77
|
+
</article>
|
|
78
|
+
</div>
|
|
79
|
+
</main>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<!-- Footer -->
|
|
83
|
+
<Footer {copyright} links={footerLinks} />
|
|
84
|
+
</div>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
copyright = '© 2026 Documentation System',
|
|
4
|
+
links = [] as { label: string; href: string }[],
|
|
5
|
+
}: {
|
|
6
|
+
copyright?: string;
|
|
7
|
+
links?: { label: string; href: string }[];
|
|
8
|
+
} = $props();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<footer class="border-t border-claude-border dark:border-claude-dark-border bg-white dark:bg-claude-dark-bg-secondary mt-12">
|
|
12
|
+
<div class="doc-container px-4 sm:px-6 py-8">
|
|
13
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 mb-8">
|
|
14
|
+
<!-- About Section -->
|
|
15
|
+
<div>
|
|
16
|
+
<h3 class="font-semibold text-claude-text dark:text-claude-dark-text mb-4">About</h3>
|
|
17
|
+
<p class="text-sm text-claude-text-secondary dark:text-claude-dark-text-secondary">
|
|
18
|
+
Professional documentation system built with SvelteKit and Tailwind CSS.
|
|
19
|
+
</p>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<!-- Links Section -->
|
|
23
|
+
{#if links.length > 0}
|
|
24
|
+
<div>
|
|
25
|
+
<h3 class="font-semibold text-claude-text dark:text-claude-dark-text mb-4">Resources</h3>
|
|
26
|
+
<ul class="space-y-2">
|
|
27
|
+
{#each links as link (link.label)}
|
|
28
|
+
<li>
|
|
29
|
+
<a
|
|
30
|
+
href={link.href}
|
|
31
|
+
class="text-sm text-claude-accent dark:text-claude-dark-accent hover:underline transition-colors"
|
|
32
|
+
>
|
|
33
|
+
{link.label}
|
|
34
|
+
</a>
|
|
35
|
+
</li>
|
|
36
|
+
{/each}
|
|
37
|
+
</ul>
|
|
38
|
+
</div>
|
|
39
|
+
{/if}
|
|
40
|
+
|
|
41
|
+
<!-- Social / Contact -->
|
|
42
|
+
<div>
|
|
43
|
+
<h3 class="font-semibold text-claude-text dark:text-claude-dark-text mb-4">Connect</h3>
|
|
44
|
+
<div class="flex gap-4">
|
|
45
|
+
<a
|
|
46
|
+
href="https://github.com"
|
|
47
|
+
target="_blank"
|
|
48
|
+
rel="noopener noreferrer"
|
|
49
|
+
class="text-claude-text-secondary dark:text-claude-dark-text-secondary hover:text-claude-accent dark:hover:text-claude-dark-accent transition-colors"
|
|
50
|
+
aria-label="GitHub"
|
|
51
|
+
>
|
|
52
|
+
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
|
53
|
+
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.01 12.01 0 0024 12c0-6.63-5.37-12-12-12z" />
|
|
54
|
+
</svg>
|
|
55
|
+
</a>
|
|
56
|
+
<a
|
|
57
|
+
href="https://twitter.com"
|
|
58
|
+
target="_blank"
|
|
59
|
+
rel="noopener noreferrer"
|
|
60
|
+
class="text-claude-text-secondary dark:text-claude-dark-text-secondary hover:text-claude-accent dark:hover:text-claude-dark-accent transition-colors"
|
|
61
|
+
aria-label="Twitter"
|
|
62
|
+
>
|
|
63
|
+
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
|
64
|
+
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417a9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z" />
|
|
65
|
+
</svg>
|
|
66
|
+
</a>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- Copyright -->
|
|
72
|
+
<div class="border-t border-claude-border dark:border-claude-dark-border pt-8">
|
|
73
|
+
<p class="text-sm text-claude-text-secondary dark:text-claude-dark-text-secondary text-center">
|
|
74
|
+
{copyright}
|
|
75
|
+
</p>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</footer>
|