@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,143 @@
|
|
|
1
|
+
import { writable } from 'svelte/store';
|
|
2
|
+
|
|
3
|
+
export type Language = 'en' | 'fr' | 'es' | 'de' | 'ja';
|
|
4
|
+
|
|
5
|
+
export interface LanguageMetadata {
|
|
6
|
+
code: Language;
|
|
7
|
+
label: string;
|
|
8
|
+
nativeLabel: string;
|
|
9
|
+
direction: 'ltr' | 'rtl';
|
|
10
|
+
isDefault?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface I18nConfig {
|
|
14
|
+
currentLanguage: Language;
|
|
15
|
+
availableLanguages: LanguageMetadata[];
|
|
16
|
+
defaultLanguage: Language;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createI18nStore() {
|
|
20
|
+
// Default language configuration
|
|
21
|
+
const defaultConfig: I18nConfig = {
|
|
22
|
+
currentLanguage: 'en',
|
|
23
|
+
defaultLanguage: 'en',
|
|
24
|
+
availableLanguages: [
|
|
25
|
+
{
|
|
26
|
+
code: 'en',
|
|
27
|
+
label: 'English',
|
|
28
|
+
nativeLabel: 'English',
|
|
29
|
+
direction: 'ltr',
|
|
30
|
+
isDefault: true,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
code: 'fr',
|
|
34
|
+
label: 'Français',
|
|
35
|
+
nativeLabel: 'Français',
|
|
36
|
+
direction: 'ltr',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
code: 'es',
|
|
40
|
+
label: 'Español',
|
|
41
|
+
nativeLabel: 'Español',
|
|
42
|
+
direction: 'ltr',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
code: 'de',
|
|
46
|
+
label: 'Deutsch',
|
|
47
|
+
nativeLabel: 'Deutsch',
|
|
48
|
+
direction: 'ltr',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
code: 'ja',
|
|
52
|
+
label: '日本語',
|
|
53
|
+
nativeLabel: '日本語',
|
|
54
|
+
direction: 'ltr',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const getInitialLanguage = (): Language => {
|
|
60
|
+
if (typeof window === 'undefined') {
|
|
61
|
+
return defaultConfig.defaultLanguage;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
// Check localStorage for stored language preference
|
|
66
|
+
const stored = localStorage?.getItem?.('docs-language') as Language | null;
|
|
67
|
+
if (stored && defaultConfig.availableLanguages.some((l) => l.code === stored)) {
|
|
68
|
+
return stored;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check browser language preference
|
|
72
|
+
const browserLang = navigator?.language?.split('-')[0].toLowerCase();
|
|
73
|
+
const matchedLang = defaultConfig.availableLanguages.find(
|
|
74
|
+
(l) => l.code === (browserLang as Language)
|
|
75
|
+
);
|
|
76
|
+
if (matchedLang) {
|
|
77
|
+
return matchedLang.code;
|
|
78
|
+
}
|
|
79
|
+
} catch (e) {
|
|
80
|
+
// localStorage or navigator may not be available in some environments
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return defaultConfig.defaultLanguage;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const { subscribe, set, update } = writable<I18nConfig>({
|
|
87
|
+
...defaultConfig,
|
|
88
|
+
currentLanguage: getInitialLanguage(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
subscribe,
|
|
93
|
+
setLanguage: (language: Language) => {
|
|
94
|
+
update((config) => {
|
|
95
|
+
if (config.availableLanguages.some((l) => l.code === language)) {
|
|
96
|
+
if (typeof window !== 'undefined') {
|
|
97
|
+
try {
|
|
98
|
+
localStorage?.setItem?.('docs-language', language);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
// localStorage may not be available
|
|
101
|
+
}
|
|
102
|
+
// Update document lang attribute
|
|
103
|
+
if (document?.documentElement) {
|
|
104
|
+
document.documentElement.lang = language;
|
|
105
|
+
// Update document dir attribute
|
|
106
|
+
const metadata = config.availableLanguages.find((l) => l.code === language);
|
|
107
|
+
if (metadata) {
|
|
108
|
+
document.documentElement.dir = metadata.direction;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { ...config, currentLanguage: language };
|
|
113
|
+
}
|
|
114
|
+
return config;
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
addLanguage: (metadata: LanguageMetadata) => {
|
|
118
|
+
update((config) => ({
|
|
119
|
+
...config,
|
|
120
|
+
availableLanguages: [
|
|
121
|
+
...config.availableLanguages.filter((l) => l.code !== metadata.code),
|
|
122
|
+
metadata,
|
|
123
|
+
],
|
|
124
|
+
}));
|
|
125
|
+
},
|
|
126
|
+
removeLanguage: (language: Language) => {
|
|
127
|
+
update((config) => ({
|
|
128
|
+
...config,
|
|
129
|
+
availableLanguages: config.availableLanguages.filter((l) => l.code !== language),
|
|
130
|
+
}));
|
|
131
|
+
},
|
|
132
|
+
getLanguageMetadata: (language: Language): LanguageMetadata | undefined => {
|
|
133
|
+
let metadata: LanguageMetadata | undefined;
|
|
134
|
+
const unsubscribe = subscribe((config) => {
|
|
135
|
+
metadata = config.availableLanguages.find((l) => l.code === language);
|
|
136
|
+
});
|
|
137
|
+
unsubscribe();
|
|
138
|
+
return metadata;
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const i18n = createI18nStore();
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { writable } from 'svelte/store';
|
|
2
|
+
|
|
3
|
+
export interface NavItem {
|
|
4
|
+
label: string;
|
|
5
|
+
href?: string;
|
|
6
|
+
children?: NavItem[];
|
|
7
|
+
icon?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface NavConfig {
|
|
11
|
+
title: string;
|
|
12
|
+
sections: NavSection[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface NavSection {
|
|
16
|
+
title: string;
|
|
17
|
+
items: NavItem[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createNavStore() {
|
|
21
|
+
const { subscribe, set, update } = writable<NavConfig>({
|
|
22
|
+
title: 'Documentation',
|
|
23
|
+
sections: [],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
subscribe,
|
|
28
|
+
setNav: (config: NavConfig) => set(config),
|
|
29
|
+
updateNav: (fn: (nav: NavConfig) => NavConfig) => update(fn),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const nav = createNavStore();
|
|
34
|
+
|
|
35
|
+
// Store for mobile sidebar visibility
|
|
36
|
+
export const sidebarOpen = writable(false);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
query,
|
|
4
|
+
results,
|
|
5
|
+
loading,
|
|
6
|
+
resultCount,
|
|
7
|
+
initPagefind,
|
|
8
|
+
updateSearch,
|
|
9
|
+
clearSearch,
|
|
10
|
+
performSearch,
|
|
11
|
+
subscribeToSearch
|
|
12
|
+
} from './search';
|
|
13
|
+
|
|
14
|
+
describe('Search Store', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
clearSearch();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('query store', () => {
|
|
20
|
+
it('should initialize with empty string', () => {
|
|
21
|
+
let currentQuery = '';
|
|
22
|
+
query.subscribe((q) => {
|
|
23
|
+
currentQuery = q;
|
|
24
|
+
});
|
|
25
|
+
expect(currentQuery).toBe('');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('results store', () => {
|
|
30
|
+
it('should initialize with empty array', () => {
|
|
31
|
+
let currentResults: any[] = [];
|
|
32
|
+
results.subscribe((r) => {
|
|
33
|
+
currentResults = r;
|
|
34
|
+
});
|
|
35
|
+
expect(currentResults).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('loading store', () => {
|
|
40
|
+
it('should initialize as false', () => {
|
|
41
|
+
let isLoading = false;
|
|
42
|
+
loading.subscribe((l) => {
|
|
43
|
+
isLoading = l;
|
|
44
|
+
});
|
|
45
|
+
expect(isLoading).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('resultCount store', () => {
|
|
50
|
+
it('should initialize with 0', () => {
|
|
51
|
+
let count = 0;
|
|
52
|
+
resultCount.subscribe((c) => {
|
|
53
|
+
count = c;
|
|
54
|
+
});
|
|
55
|
+
expect(count).toBe(0);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('clearSearch', () => {
|
|
60
|
+
it('should clear query and results', async () => {
|
|
61
|
+
query.set('test query');
|
|
62
|
+
let queries: string[] = [];
|
|
63
|
+
let resultsList: any[] = [];
|
|
64
|
+
|
|
65
|
+
const unsubscribeQuery = query.subscribe((q) => {
|
|
66
|
+
queries.push(q);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const unsubscribeResults = results.subscribe((r) => {
|
|
70
|
+
resultsList.push(r);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
clearSearch();
|
|
74
|
+
|
|
75
|
+
// Allow time for updates
|
|
76
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
77
|
+
|
|
78
|
+
expect(queries[queries.length - 1]).toBe('');
|
|
79
|
+
expect(resultsList[resultsList.length - 1]).toEqual([]);
|
|
80
|
+
unsubscribeQuery();
|
|
81
|
+
unsubscribeResults();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('updateSearch', () => {
|
|
86
|
+
it('should update query store', async () => {
|
|
87
|
+
let currentQuery = '';
|
|
88
|
+
const unsubscribe = query.subscribe((q) => {
|
|
89
|
+
currentQuery = q;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Note: This will fail without Pagefind initialized,
|
|
93
|
+
// but we're testing the store behavior
|
|
94
|
+
try {
|
|
95
|
+
await updateSearch('test');
|
|
96
|
+
} catch {
|
|
97
|
+
// Expected to fail without Pagefind
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
expect(currentQuery).toBe('test');
|
|
101
|
+
unsubscribe();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('subscribeToSearch', () => {
|
|
106
|
+
it('should unsubscribe properly', () => {
|
|
107
|
+
const mockCallback = vi.fn();
|
|
108
|
+
const unsubscribe = subscribeToSearch(mockCallback, 100);
|
|
109
|
+
|
|
110
|
+
query.set('test');
|
|
111
|
+
|
|
112
|
+
// Unsubscribe immediately
|
|
113
|
+
unsubscribe();
|
|
114
|
+
|
|
115
|
+
// Query should still update but callback should not be called
|
|
116
|
+
// (because we unsubscribed)
|
|
117
|
+
expect(mockCallback).not.toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('Search results type', () => {
|
|
122
|
+
it('should have correct result properties', () => {
|
|
123
|
+
const mockResult = {
|
|
124
|
+
id: 'test-1',
|
|
125
|
+
url: '/test',
|
|
126
|
+
title: 'Test Title',
|
|
127
|
+
content: 'Test content',
|
|
128
|
+
excerpt: 'Test excerpt',
|
|
129
|
+
meta: { category: 'test' }
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
expect(mockResult.id).toBe('test-1');
|
|
133
|
+
expect(mockResult.url).toBe('/test');
|
|
134
|
+
expect(mockResult.title).toBe('Test Title');
|
|
135
|
+
expect(mockResult.content).toBe('Test content');
|
|
136
|
+
expect(mockResult.excerpt).toBe('Test excerpt');
|
|
137
|
+
expect(mockResult.meta).toEqual({ category: 'test' });
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { writable, derived, type Writable, type Readable } from 'svelte/store';
|
|
2
|
+
|
|
3
|
+
export interface SearchResult {
|
|
4
|
+
id: string;
|
|
5
|
+
url: string;
|
|
6
|
+
title: string;
|
|
7
|
+
content: string;
|
|
8
|
+
excerpt?: string;
|
|
9
|
+
meta?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SearchFilters {
|
|
13
|
+
limit?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Main search query store
|
|
17
|
+
const searchQuery: Writable<string> = writable('');
|
|
18
|
+
|
|
19
|
+
// Search results store
|
|
20
|
+
const searchResults: Writable<SearchResult[]> = writable([]);
|
|
21
|
+
|
|
22
|
+
// Loading state store
|
|
23
|
+
const isSearching: Writable<boolean> = writable(false);
|
|
24
|
+
|
|
25
|
+
// Pagefind index reference
|
|
26
|
+
let pagefindIndex: any = null;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initialize Pagefind when the client loads
|
|
30
|
+
*/
|
|
31
|
+
export async function initPagefind(): Promise<void> {
|
|
32
|
+
if (typeof window === 'undefined') {
|
|
33
|
+
console.warn('Pagefind can only be initialized on the client side');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Check if pagefind is already available globally (from build output)
|
|
39
|
+
if ((window as any).pagefind) {
|
|
40
|
+
pagefindIndex = (window as any).pagefind;
|
|
41
|
+
console.log('Pagefind initialized successfully (from global)');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Dynamically import Pagefind from the build output
|
|
46
|
+
// Use string concatenation to prevent Vite from resolving this at build time
|
|
47
|
+
const pagefindPath = '/' + 'pagefind' + '/' + 'pagefind.js';
|
|
48
|
+
const pagefind = await import(/* @vite-ignore */ pagefindPath);
|
|
49
|
+
pagefindIndex = pagefind;
|
|
50
|
+
console.log('Pagefind initialized successfully (from dynamic import)');
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Failed to initialize Pagefind:', error);
|
|
53
|
+
console.warn(
|
|
54
|
+
'Search functionality will not work. Make sure to run `npm run build` to generate search index.'
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Perform a search query
|
|
61
|
+
*/
|
|
62
|
+
export async function performSearch(query: string, filters?: SearchFilters): Promise<SearchResult[]> {
|
|
63
|
+
if (!query.trim()) {
|
|
64
|
+
searchResults.set([]);
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!pagefindIndex) {
|
|
69
|
+
console.warn('Pagefind not yet initialized');
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
isSearching.set(true);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const results = await pagefindIndex.search(query);
|
|
77
|
+
const limit = filters?.limit || 20;
|
|
78
|
+
|
|
79
|
+
// Convert Pagefind results to our SearchResult format
|
|
80
|
+
const formattedResults: SearchResult[] = results.results
|
|
81
|
+
.slice(0, limit)
|
|
82
|
+
.map((result: any) => ({
|
|
83
|
+
id: result.id,
|
|
84
|
+
url: result.url,
|
|
85
|
+
title: result.meta?.title || 'Untitled',
|
|
86
|
+
content: result.meta?.content || '',
|
|
87
|
+
excerpt: result.excerpt || '',
|
|
88
|
+
meta: result.meta || {}
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
searchResults.set(formattedResults);
|
|
92
|
+
return formattedResults;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('Search error:', error);
|
|
95
|
+
searchResults.set([]);
|
|
96
|
+
return [];
|
|
97
|
+
} finally {
|
|
98
|
+
isSearching.set(false);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get reactive search results
|
|
104
|
+
*/
|
|
105
|
+
export const results: Readable<SearchResult[]> = derived(
|
|
106
|
+
searchResults,
|
|
107
|
+
($results) => $results
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get reactive search query
|
|
112
|
+
*/
|
|
113
|
+
export const query = searchQuery;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get reactive loading state
|
|
117
|
+
*/
|
|
118
|
+
export const loading = isSearching;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Computed derived store for result count
|
|
122
|
+
*/
|
|
123
|
+
export const resultCount: Readable<number> = derived(
|
|
124
|
+
searchResults,
|
|
125
|
+
($results) => $results.length
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Clear search results and query
|
|
130
|
+
*/
|
|
131
|
+
export function clearSearch(): void {
|
|
132
|
+
searchQuery.set('');
|
|
133
|
+
searchResults.set([]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Update search query and perform search
|
|
138
|
+
*/
|
|
139
|
+
export async function updateSearch(newQuery: string, filters?: SearchFilters): Promise<SearchResult[]> {
|
|
140
|
+
searchQuery.set(newQuery);
|
|
141
|
+
return performSearch(newQuery, filters);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Subscribe to search query changes with debouncing
|
|
146
|
+
*/
|
|
147
|
+
export function subscribeToSearch(
|
|
148
|
+
callback: (query: string) => Promise<void>,
|
|
149
|
+
delayMs: number = 300
|
|
150
|
+
): () => void {
|
|
151
|
+
let timeout: ReturnType<typeof setTimeout>;
|
|
152
|
+
|
|
153
|
+
const unsubscribe = searchQuery.subscribe((query) => {
|
|
154
|
+
clearTimeout(timeout);
|
|
155
|
+
timeout = setTimeout(() => callback(query), delayMs);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return () => {
|
|
159
|
+
clearTimeout(timeout);
|
|
160
|
+
unsubscribe();
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { writable } from 'svelte/store';
|
|
2
|
+
|
|
3
|
+
type Theme = 'light' | 'dark';
|
|
4
|
+
|
|
5
|
+
function createThemeStore() {
|
|
6
|
+
// Check localStorage or system preference on initialization
|
|
7
|
+
const getInitialTheme = (): Theme => {
|
|
8
|
+
if (typeof window === 'undefined') {
|
|
9
|
+
return 'light'; // SSR default
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Check localStorage
|
|
13
|
+
const stored = localStorage.getItem('theme') as Theme | null;
|
|
14
|
+
if (stored) {
|
|
15
|
+
return stored;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Check system preference
|
|
19
|
+
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
20
|
+
return 'dark';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return 'light';
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const { subscribe, set, update } = writable<Theme>(getInitialTheme());
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
subscribe,
|
|
30
|
+
set: (theme: Theme) => {
|
|
31
|
+
if (typeof window !== 'undefined') {
|
|
32
|
+
localStorage.setItem('theme', theme);
|
|
33
|
+
// Apply theme to document
|
|
34
|
+
if (theme === 'dark') {
|
|
35
|
+
document.documentElement.classList.add('dark');
|
|
36
|
+
} else {
|
|
37
|
+
document.documentElement.classList.remove('dark');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
set(theme);
|
|
41
|
+
},
|
|
42
|
+
toggle: () => {
|
|
43
|
+
update((theme) => {
|
|
44
|
+
const newTheme = theme === 'dark' ? 'light' : 'dark';
|
|
45
|
+
if (typeof window !== 'undefined') {
|
|
46
|
+
localStorage.setItem('theme', newTheme);
|
|
47
|
+
if (newTheme === 'dark') {
|
|
48
|
+
document.documentElement.classList.add('dark');
|
|
49
|
+
} else {
|
|
50
|
+
document.documentElement.classList.remove('dark');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return newTheme;
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const theme = createThemeStore();
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { version } from './version';
|
|
3
|
+
|
|
4
|
+
// Mock localStorage
|
|
5
|
+
const localStorageMock = (() => {
|
|
6
|
+
let store: Record<string, string> = {};
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
getItem: (key: string) => store[key] || null,
|
|
10
|
+
setItem: (key: string, value: string) => {
|
|
11
|
+
store[key] = value.toString();
|
|
12
|
+
},
|
|
13
|
+
removeItem: (key: string) => {
|
|
14
|
+
delete store[key];
|
|
15
|
+
},
|
|
16
|
+
clear: () => {
|
|
17
|
+
store = {};
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
})();
|
|
21
|
+
|
|
22
|
+
Object.defineProperty(window, 'localStorage', {
|
|
23
|
+
value: localStorageMock,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('version store', () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
localStorage.clear();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should initialize with default version', () => {
|
|
32
|
+
let currentVersion = '';
|
|
33
|
+
const unsubscribe = version.subscribe((config) => {
|
|
34
|
+
currentVersion = config.current;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(currentVersion).toBe('2.0');
|
|
38
|
+
unsubscribe();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should have multiple versions available', () => {
|
|
42
|
+
let versions = [];
|
|
43
|
+
const unsubscribe = version.subscribe((config) => {
|
|
44
|
+
versions = config.availableVersions.map((v) => v.version);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(versions).toContain('2.0');
|
|
48
|
+
expect(versions).toContain('1.5');
|
|
49
|
+
expect(versions).toContain('1.0');
|
|
50
|
+
expect(versions.length).toBe(3);
|
|
51
|
+
unsubscribe();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should mark v2.0 as latest', () => {
|
|
55
|
+
let latestVersion = null;
|
|
56
|
+
const unsubscribe = version.subscribe((config) => {
|
|
57
|
+
const latest = config.availableVersions.find((v) => v.isLatest);
|
|
58
|
+
latestVersion = latest?.version;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(latestVersion).toBe('2.0');
|
|
62
|
+
unsubscribe();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should allow changing version', () => {
|
|
66
|
+
let currentVersion = '';
|
|
67
|
+
const unsubscribe = version.subscribe((config) => {
|
|
68
|
+
currentVersion = config.current;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
version.setVersion('1.5');
|
|
72
|
+
expect(currentVersion).toBe('1.5');
|
|
73
|
+
|
|
74
|
+
version.setVersion('1.0');
|
|
75
|
+
expect(currentVersion).toBe('1.0');
|
|
76
|
+
|
|
77
|
+
unsubscribe();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should persist version preference to localStorage', () => {
|
|
81
|
+
version.setVersion('1.5');
|
|
82
|
+
expect(localStorage.getItem('docs-version')).toBe('1.5');
|
|
83
|
+
|
|
84
|
+
version.setVersion('1.0');
|
|
85
|
+
expect(localStorage.getItem('docs-version')).toBe('1.0');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should ignore setting invalid version', () => {
|
|
89
|
+
let currentVersion = '';
|
|
90
|
+
const unsubscribe = version.subscribe((config) => {
|
|
91
|
+
currentVersion = config.current;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const initialVersion = currentVersion;
|
|
95
|
+
version.setVersion('99.0');
|
|
96
|
+
expect(currentVersion).toBe(initialVersion);
|
|
97
|
+
|
|
98
|
+
unsubscribe();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should allow adding new versions', () => {
|
|
102
|
+
let versions = [];
|
|
103
|
+
const unsubscribe = version.subscribe((config) => {
|
|
104
|
+
versions = config.availableVersions.map((v) => v.version);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
version.addVersion({
|
|
108
|
+
version: '3.0',
|
|
109
|
+
label: 'v3.0',
|
|
110
|
+
isLatest: false,
|
|
111
|
+
status: 'beta',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(versions).toContain('3.0');
|
|
115
|
+
unsubscribe();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should allow removing versions', () => {
|
|
119
|
+
let versions = [];
|
|
120
|
+
const unsubscribe = version.subscribe((config) => {
|
|
121
|
+
versions = config.availableVersions.map((v) => v.version);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const initialCount = versions.length;
|
|
125
|
+
version.removeVersion('1.0');
|
|
126
|
+
|
|
127
|
+
expect(versions.length).toBe(initialCount - 1);
|
|
128
|
+
expect(versions).not.toContain('1.0');
|
|
129
|
+
unsubscribe();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should retrieve version metadata', () => {
|
|
133
|
+
const metadata = version.getVersionMetadata('2.0');
|
|
134
|
+
expect(metadata).toBeDefined();
|
|
135
|
+
expect(metadata?.version).toBe('2.0');
|
|
136
|
+
expect(metadata?.isLatest).toBe(true);
|
|
137
|
+
expect(metadata?.status).toBe('stable');
|
|
138
|
+
});
|
|
139
|
+
});
|