@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.
Files changed (134) hide show
  1. package/COMPONENTS.md +365 -0
  2. package/COVERAGE_REPORT.md +663 -0
  3. package/README.md +42 -0
  4. package/SEARCH_VERIFICATION.md +229 -0
  5. package/TEST_SUMMARY.md +344 -0
  6. package/bin/init.js +821 -0
  7. package/docs/E2E_TESTS.md +354 -0
  8. package/docs/TESTING.md +754 -0
  9. package/docs/de/index.md +41 -0
  10. package/docs/en/COMPONENTS.md +443 -0
  11. package/docs/en/api/examples.md +100 -0
  12. package/docs/en/api/overview.md +69 -0
  13. package/docs/en/components/index.md +622 -0
  14. package/docs/en/config/navigation.md +505 -0
  15. package/docs/en/config/theme-and-colors.md +395 -0
  16. package/docs/en/getting-started/integration.md +406 -0
  17. package/docs/en/guides/common-setups.md +651 -0
  18. package/docs/en/index.md +243 -0
  19. package/docs/en/markdown.md +102 -0
  20. package/docs/en/routing.md +64 -0
  21. package/docs/en/setup.md +52 -0
  22. package/docs/en/troubleshooting.md +704 -0
  23. package/docs/es/index.md +41 -0
  24. package/docs/fr/index.md +41 -0
  25. package/docs/ja/index.md +41 -0
  26. package/package.json +40 -0
  27. package/pagefind.toml +8 -0
  28. package/postcss.config.js +5 -0
  29. package/src/app.css +119 -0
  30. package/src/app.d.ts +13 -0
  31. package/src/app.html +11 -0
  32. package/src/lib/assets/favicon.svg +1 -0
  33. package/src/lib/components/APITable.svelte +120 -0
  34. package/src/lib/components/APITable.test.ts +153 -0
  35. package/src/lib/components/Breadcrumbs.svelte +85 -0
  36. package/src/lib/components/Breadcrumbs.test.ts +148 -0
  37. package/src/lib/components/Callout.svelte +60 -0
  38. package/src/lib/components/Callout.test.ts +100 -0
  39. package/src/lib/components/CodeBlock.svelte +68 -0
  40. package/src/lib/components/CodeBlock.test.ts +133 -0
  41. package/src/lib/components/DocLayout.svelte +84 -0
  42. package/src/lib/components/Footer.svelte +78 -0
  43. package/src/lib/components/Image.svelte +100 -0
  44. package/src/lib/components/Image.test.ts +163 -0
  45. package/src/lib/components/Navbar.svelte +141 -0
  46. package/src/lib/components/Search.svelte +248 -0
  47. package/src/lib/components/Sidebar.svelte +110 -0
  48. package/src/lib/components/Tabs.svelte +48 -0
  49. package/src/lib/components/Tabs.test.ts +102 -0
  50. package/src/lib/config.test.ts +140 -0
  51. package/src/lib/config.ts +179 -0
  52. package/src/lib/configIntegration.test.ts +272 -0
  53. package/src/lib/configLoader.ts +231 -0
  54. package/src/lib/configParser.test.ts +217 -0
  55. package/src/lib/configParser.ts +234 -0
  56. package/src/lib/index.ts +34 -0
  57. package/src/lib/integration.test.ts +426 -0
  58. package/src/lib/navigationBuilder.test.ts +338 -0
  59. package/src/lib/navigationBuilder.ts +268 -0
  60. package/src/lib/performance.test.ts +369 -0
  61. package/src/lib/routing.test.ts +202 -0
  62. package/src/lib/routing.ts +127 -0
  63. package/src/lib/search-functionality.test.ts +493 -0
  64. package/src/lib/stores/i18n.test.ts +180 -0
  65. package/src/lib/stores/i18n.ts +143 -0
  66. package/src/lib/stores/nav.ts +36 -0
  67. package/src/lib/stores/search.test.ts +140 -0
  68. package/src/lib/stores/search.ts +162 -0
  69. package/src/lib/stores/theme.ts +59 -0
  70. package/src/lib/stores/version.test.ts +139 -0
  71. package/src/lib/stores/version.ts +111 -0
  72. package/src/lib/themeCustomization.test.ts +223 -0
  73. package/src/lib/themeCustomization.ts +212 -0
  74. package/src/lib/utils/highlight.test.ts +136 -0
  75. package/src/lib/utils/highlight.ts +100 -0
  76. package/src/lib/utils/index.ts +7 -0
  77. package/src/lib/utils/markdown.test.ts +357 -0
  78. package/src/lib/utils/markdown.ts +77 -0
  79. package/src/routes/+layout.server.ts +1 -0
  80. package/src/routes/+layout.svelte +28 -0
  81. package/src/routes/+page.svelte +165 -0
  82. package/static/robots.txt +3 -0
  83. package/svelte.config.js +18 -0
  84. package/tailwind.config.ts +55 -0
  85. package/template-starter/.github/workflows/build.yml +40 -0
  86. package/template-starter/.github/workflows/deploy-github-pages.yml +47 -0
  87. package/template-starter/.github/workflows/deploy-netlify.yml +41 -0
  88. package/template-starter/.github/workflows/deploy-vercel.yml +64 -0
  89. package/template-starter/NPM-PACKAGE-SETUP.md +233 -0
  90. package/template-starter/README.md +320 -0
  91. package/template-starter/docs/_config.json +39 -0
  92. package/template-starter/docs/api/components.md +257 -0
  93. package/template-starter/docs/api/overview.md +169 -0
  94. package/template-starter/docs/guides/configuration.md +145 -0
  95. package/template-starter/docs/guides/github-pages-deployment.md +254 -0
  96. package/template-starter/docs/guides/netlify-deployment.md +159 -0
  97. package/template-starter/docs/guides/vercel-deployment.md +131 -0
  98. package/template-starter/docs/index.md +49 -0
  99. package/template-starter/docs/setup.md +149 -0
  100. package/template-starter/package.json +31 -0
  101. package/template-starter/pagefind.toml +3 -0
  102. package/template-starter/postcss.config.js +5 -0
  103. package/template-starter/src/app.css +34 -0
  104. package/template-starter/src/app.d.ts +13 -0
  105. package/template-starter/src/app.html +11 -0
  106. package/template-starter/src/lib/components/APITable.svelte +120 -0
  107. package/template-starter/src/lib/components/APITable.test.ts +19 -0
  108. package/template-starter/src/lib/components/Breadcrumbs.svelte +85 -0
  109. package/template-starter/src/lib/components/Breadcrumbs.test.ts +19 -0
  110. package/template-starter/src/lib/components/Callout.svelte +60 -0
  111. package/template-starter/src/lib/components/Callout.test.ts +16 -0
  112. package/template-starter/src/lib/components/CodeBlock.svelte +68 -0
  113. package/template-starter/src/lib/components/CodeBlock.test.ts +12 -0
  114. package/template-starter/src/lib/components/DocLayout.svelte +84 -0
  115. package/template-starter/src/lib/components/Footer.svelte +78 -0
  116. package/template-starter/src/lib/components/Image.svelte +100 -0
  117. package/template-starter/src/lib/components/Image.test.ts +15 -0
  118. package/template-starter/src/lib/components/Navbar.svelte +141 -0
  119. package/template-starter/src/lib/components/Search.svelte +248 -0
  120. package/template-starter/src/lib/components/Sidebar.svelte +110 -0
  121. package/template-starter/src/lib/components/Tabs.svelte +48 -0
  122. package/template-starter/src/lib/components/Tabs.test.ts +17 -0
  123. package/template-starter/src/lib/index.ts +15 -0
  124. package/template-starter/src/routes/+layout.svelte +28 -0
  125. package/template-starter/src/routes/+page.svelte +92 -0
  126. package/template-starter/svelte.config.js +17 -0
  127. package/template-starter/tailwind.config.ts +17 -0
  128. package/template-starter/tsconfig.json +13 -0
  129. package/template-starter/vite.config.ts +6 -0
  130. package/tests/e2e/example.spec.ts +345 -0
  131. package/tsconfig.json +20 -0
  132. package/vite.config.ts +6 -0
  133. package/vitest.config.ts +34 -0
  134. 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
+ });