@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,100 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Image component with caption support
4
+ * Provides responsive images with optional captions and alt text
5
+ */
6
+
7
+ interface Props {
8
+ src: string;
9
+ alt: string;
10
+ caption?: string;
11
+ width?: number;
12
+ height?: number;
13
+ zoomable?: boolean;
14
+ }
15
+
16
+ let { src, alt, caption, width, height, zoomable = true }: Props = $props();
17
+
18
+ let isZoomed = $state(false);
19
+
20
+ function handleZoom() {
21
+ if (zoomable) {
22
+ isZoomed = !isZoomed;
23
+ }
24
+ }
25
+
26
+ function handleKeydown(e: KeyboardEvent) {
27
+ if (e.key === 'Escape' && isZoomed) {
28
+ isZoomed = false;
29
+ }
30
+ }
31
+ </script>
32
+
33
+ <svelte:window on:keydown={handleKeydown} />
34
+
35
+ <figure class="my-6 flex flex-col items-center gap-2">
36
+ <div class="relative w-full max-w-4xl overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
37
+ <!-- Zoomed overlay -->
38
+ {#if isZoomed}
39
+ <div
40
+ class="fixed inset-0 z-50 flex cursor-pointer items-center justify-center bg-black/80 backdrop-blur"
41
+ onclick={() => (isZoomed = false)}
42
+ onkeydown={(e) => {
43
+ if (e.key === 'Enter' || e.key === ' ') {
44
+ isZoomed = false;
45
+ }
46
+ }}
47
+ role="button"
48
+ tabindex="0"
49
+ >
50
+ <img
51
+ {src}
52
+ {alt}
53
+ class="max-h-[90vh] max-w-[90vw] rounded-lg object-contain"
54
+ {width}
55
+ {height}
56
+ />
57
+ </div>
58
+ {/if}
59
+
60
+ <!-- Regular image wrapper for clickability -->
61
+ <button
62
+ onclick={handleZoom}
63
+ class="w-full border-0 bg-transparent p-0"
64
+ aria-label={zoomable ? 'Click to zoom image' : ''}
65
+ type="button"
66
+ >
67
+ <img
68
+ {src}
69
+ {alt}
70
+ {width}
71
+ {height}
72
+ class="w-full object-cover {zoomable ? 'cursor-zoom-in hover:opacity-90' : ''} transition-opacity"
73
+ />
74
+ </button>
75
+ </div>
76
+
77
+ {#if caption}
78
+ <figcaption class="text-center text-sm text-gray-600 dark:text-gray-400">
79
+ {caption}
80
+ </figcaption>
81
+ {/if}
82
+ </figure>
83
+
84
+ <style>
85
+ figure {
86
+ margin-top: 1.5rem;
87
+ margin-bottom: 1.5rem;
88
+ }
89
+
90
+ figcaption {
91
+ margin-top: 0.5rem;
92
+ text-align: center;
93
+ font-size: 0.875rem;
94
+ color: #4b5563;
95
+ }
96
+
97
+ :global(.dark) figcaption {
98
+ color: #9ca3af;
99
+ }
100
+ </style>
@@ -0,0 +1,163 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ /**
4
+ * Unit tests for Image component
5
+ * Tests image rendering, optimization, and accessibility
6
+ */
7
+
8
+ describe('Image Component', () => {
9
+ describe('Image Props', () => {
10
+ it('should require src attribute', () => {
11
+ const src = '/images/example.png';
12
+ expect(src).toBeDefined();
13
+ expect(typeof src).toBe('string');
14
+ expect(src.startsWith('/')).toBe(true);
15
+ });
16
+
17
+ it('should require alt text for accessibility', () => {
18
+ const alt = 'A description of the image';
19
+ expect(alt).toBeDefined();
20
+ expect(typeof alt).toBe('string');
21
+ expect(alt.length).toBeGreaterThan(0);
22
+ });
23
+
24
+ it('should accept various image formats', () => {
25
+ const formats = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'];
26
+ const validSrc = 'image.webp';
27
+ expect(formats.some((fmt) => validSrc.endsWith(fmt))).toBe(true);
28
+ });
29
+
30
+ it('should accept optional width and height', () => {
31
+ const width = 800;
32
+ const height = 600;
33
+ expect(typeof width).toBe('number');
34
+ expect(typeof height).toBe('number');
35
+ expect(width).toBeGreaterThan(0);
36
+ expect(height).toBeGreaterThan(0);
37
+ });
38
+
39
+ it('should accept optional caption', () => {
40
+ const caption = 'Figure 1: Example screenshot';
41
+ expect(typeof caption).toBe('string');
42
+ });
43
+ });
44
+
45
+ describe('Image Optimization', () => {
46
+ it('should support responsive images with srcset', () => {
47
+ const srcset =
48
+ '/img/small.webp 640w, /img/medium.webp 1024w, /img/large.webp 1920w';
49
+ expect(srcset).toContain('640w');
50
+ expect(srcset).toContain('1024w');
51
+ expect(srcset).toContain('1920w');
52
+ });
53
+
54
+ it('should support multiple image formats with picture element', () => {
55
+ const formats = ['webp', 'jpg', 'png'];
56
+ formats.forEach((fmt) => {
57
+ expect(['webp', 'jpg', 'png']).toContain(fmt);
58
+ });
59
+ });
60
+
61
+ it('should lazy load images by default', () => {
62
+ const loading = 'lazy';
63
+ expect(['lazy', 'eager']).toContain(loading);
64
+ });
65
+
66
+ it('should support eager loading when needed', () => {
67
+ const loading = 'eager';
68
+ expect(['lazy', 'eager']).toContain(loading);
69
+ });
70
+ });
71
+
72
+ describe('Responsive Behavior', () => {
73
+ it('should support CSS aspect ratio', () => {
74
+ const aspectRatio = '16/9';
75
+ expect(aspectRatio).toContain('/');
76
+ });
77
+
78
+ it('should preserve aspect ratio', () => {
79
+ const width = 800;
80
+ const height = 600;
81
+ const aspectRatio = width / height;
82
+ expect(aspectRatio).toBeCloseTo(1.33, 1);
83
+ });
84
+
85
+ it('should scale to container width', () => {
86
+ const maxWidth = '100%';
87
+ expect(maxWidth).toBe('100%');
88
+ });
89
+ });
90
+
91
+ describe('Figure and Caption', () => {
92
+ it('should wrap in figure element', () => {
93
+ const element = 'figure';
94
+ expect(element).toBe('figure');
95
+ });
96
+
97
+ it('should support caption with figcaption', () => {
98
+ const caption = 'Example figure caption';
99
+ expect(caption).toBeTruthy();
100
+ });
101
+
102
+ it('should associate caption with image', () => {
103
+ const figcaptionId = 'img-caption-1';
104
+ const ariaDescribedBy = 'img-caption-1';
105
+ expect(ariaDescribedBy).toBe(figcaptionId);
106
+ });
107
+ });
108
+
109
+ describe('Accessibility', () => {
110
+ it('should have descriptive alt text', () => {
111
+ const alt = 'A screenshot showing the dashboard with user metrics';
112
+ expect(alt.length).toBeGreaterThan(10);
113
+ });
114
+
115
+ it('should not have empty alt text', () => {
116
+ const alt = 'Important diagram';
117
+ expect(alt.length).toBeGreaterThan(0);
118
+ });
119
+
120
+ it('should avoid decorative image alt text', () => {
121
+ const decorativeAlt = 'spacer';
122
+ const meaningfulAlt = 'Navigation menu icon';
123
+ expect(meaningfulAlt.length).toBeGreaterThan(decorativeAlt.length);
124
+ });
125
+
126
+ it('should have proper figure structure', () => {
127
+ const figureElement = 'figure';
128
+ const figcaptionElement = 'figcaption';
129
+ expect(figureElement).toBeDefined();
130
+ expect(figcaptionElement).toBeDefined();
131
+ });
132
+
133
+ it('should support extended descriptions for complex images', () => {
134
+ const longdesc = '/docs/image-description.html';
135
+ expect(longdesc).toBeTruthy();
136
+ });
137
+ });
138
+
139
+ describe('Dark Mode Support', () => {
140
+ it('should support dark mode styling', () => {
141
+ const lightClass = 'border-gray-200';
142
+ const darkClass = 'dark:border-gray-700';
143
+ expect(darkClass).toContain('dark:');
144
+ });
145
+
146
+ it('should maintain contrast in dark mode', () => {
147
+ const darkModeEnabled = true;
148
+ expect(darkModeEnabled).toBe(true);
149
+ });
150
+ });
151
+
152
+ describe('Browser Support', () => {
153
+ it('should provide fallback for old browsers', () => {
154
+ const hasFallback = true;
155
+ expect(hasFallback).toBe(true);
156
+ });
157
+
158
+ it('should support webp with fallback', () => {
159
+ const hasPicture = true;
160
+ expect(hasPicture).toBe(true);
161
+ });
162
+ });
163
+ });
@@ -0,0 +1,141 @@
1
+ <script lang="ts">
2
+ import { theme } from '$lib/stores/theme';
3
+ import { sidebarOpen } from '$lib/stores/nav';
4
+ import { version } from '$lib/stores/version';
5
+ import { i18n } from '$lib/stores/i18n';
6
+ import { onMount } from 'svelte';
7
+
8
+ let {
9
+ title = 'Docs',
10
+ logo = null,
11
+ onLogoClick = () => {},
12
+ onVersionChange = (ver: string) => {},
13
+ onLanguageChange = (lang: string) => {},
14
+ }: {
15
+ title?: string;
16
+ logo?: string | null;
17
+ onLogoClick?: () => void;
18
+ onVersionChange?: (version: string) => void;
19
+ onLanguageChange?: (language: string) => void;
20
+ } = $props();
21
+
22
+ let isDark = $derived($theme === 'dark');
23
+ let currentVersion = $derived.by(() => $version.current);
24
+ let currentLanguage = $derived.by(() => $i18n.currentLanguage);
25
+ let availableVersions = $derived.by(() => $version.availableVersions);
26
+ let availableLanguages = $derived.by(() => $i18n.availableLanguages);
27
+ let isMounted = $state(false);
28
+
29
+ onMount(() => {
30
+ isMounted = true;
31
+ });
32
+
33
+ function toggleTheme() {
34
+ theme.toggle();
35
+ }
36
+
37
+ function toggleSidebar() {
38
+ sidebarOpen.update(v => !v);
39
+ }
40
+
41
+ function handleVersionChange(e: Event) {
42
+ const select = e.target as HTMLSelectElement;
43
+ const newVersion = select.value;
44
+ version.setVersion(newVersion);
45
+ onVersionChange(newVersion);
46
+ }
47
+
48
+ function handleLanguageChange(e: Event) {
49
+ const select = e.target as HTMLSelectElement;
50
+ const newLanguage = select.value;
51
+ i18n.setLanguage(newLanguage as any);
52
+ onLanguageChange(newLanguage);
53
+ }
54
+ </script>
55
+
56
+ <nav class="sticky top-0 z-40 border-b border-claude-border dark:border-claude-dark-border bg-white dark:bg-claude-dark-bg-secondary backdrop-blur-sm">
57
+ <div class="doc-container flex items-center justify-between h-16 px-4 sm:px-6">
58
+ <!-- Logo / Brand -->
59
+ <button
60
+ onclick={onLogoClick}
61
+ class="flex items-center gap-2 font-semibold text-lg text-claude-text dark:text-claude-dark-text hover:text-claude-accent dark:hover:text-claude-dark-accent transition-colors"
62
+ aria-label="Home"
63
+ >
64
+ {#if logo}
65
+ <img src={logo} alt="Logo" class="h-8 w-8" />
66
+ {/if}
67
+ <span>{title}</span>
68
+ </button>
69
+
70
+ <!-- Mobile Menu Button -->
71
+ <button
72
+ onclick={toggleSidebar}
73
+ class="md:hidden p-2 hover:bg-claude-bg-secondary dark:hover:bg-claude-dark-bg rounded-lg transition-colors"
74
+ aria-label="Toggle menu"
75
+ >
76
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
77
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
78
+ </svg>
79
+ </button>
80
+
81
+ <!-- Right Section: Language, Version, Theme Toggle -->
82
+ <div class="hidden md:flex items-center gap-4">
83
+ <!-- Language Switcher -->
84
+ {#if isMounted && availableLanguages.length > 0}
85
+ <select
86
+ value={currentLanguage}
87
+ onchange={handleLanguageChange}
88
+ class="px-3 py-2 rounded-lg bg-claude-bg-secondary dark:bg-claude-dark-bg border border-claude-border dark:border-claude-dark-border text-claude-text dark:text-claude-dark-text text-sm cursor-pointer hover:border-claude-accent dark:hover:border-claude-dark-accent transition-colors"
89
+ aria-label="Select language"
90
+ >
91
+ {#each availableLanguages as lang (lang.code)}
92
+ <option value={lang.code}>
93
+ {lang.label}
94
+ </option>
95
+ {/each}
96
+ </select>
97
+ {/if}
98
+
99
+ <!-- Version Switcher -->
100
+ {#if isMounted && availableVersions.length > 0}
101
+ <select
102
+ value={currentVersion}
103
+ onchange={handleVersionChange}
104
+ class="px-3 py-2 rounded-lg bg-claude-bg-secondary dark:bg-claude-dark-bg border border-claude-border dark:border-claude-dark-border text-claude-text dark:text-claude-dark-text text-sm cursor-pointer hover:border-claude-accent dark:hover:border-claude-dark-accent transition-colors"
105
+ aria-label="Select version"
106
+ >
107
+ {#each availableVersions as ver (ver.version)}
108
+ <option value={ver.version}>
109
+ {ver.label}
110
+ </option>
111
+ {/each}
112
+ </select>
113
+ {/if}
114
+
115
+ <!-- Dark Mode Toggle -->
116
+ <button
117
+ onclick={toggleTheme}
118
+ class="p-2 rounded-lg bg-claude-bg-secondary dark:bg-claude-dark-bg border border-claude-border dark:border-claude-dark-border hover:border-claude-accent dark:hover:border-claude-dark-accent transition-colors"
119
+ aria-label="Toggle dark mode"
120
+ >
121
+ {#if isDark}
122
+ <!-- Moon Icon -->
123
+ <svg class="w-5 h-5 text-claude-accent dark:text-claude-dark-accent" fill="currentColor" viewBox="0 0 20 20">
124
+ <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
125
+ </svg>
126
+ {:else}
127
+ <!-- Sun Icon -->
128
+ <svg class="w-5 h-5 text-claude-accent" fill="currentColor" viewBox="0 0 20 20">
129
+ <path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4.323 2.323a1 1 0 011.414 0l.707.707a1 1 0 11-1.414 1.414l-.707-.707a1 1 0 010-1.414zm2.828 2.828a1 1 0 011.414 0l.707.707a1 1 0 11-1.414 1.414l-.707-.707a1 1 0 010-1.414zm2.828 2.828a1 1 0 011.414 0l.707.707a1 1 0 11-1.414 1.414l-.707-.707a1 1 0 010-1.414zM10 11a1 1 0 110 2 1 1 0 010-2zm4.464-1.465a1 1 0 111.414-1.414l.707.707a1 1 0 11-1.414 1.414l-.707-.707zm2.828 2.828a1 1 0 111.414-1.414l.707.707a1 1 0 11-1.414 1.414l-.707-.707zm2.828 2.828a1 1 0 111.414-1.414l.707.707a1 1 0 11-1.414 1.414l-.707-.707zM10 18a1 1 0 110-2 1 1 0 010 2zm-4.464-1.465a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 1.414l-.707.707zm-2.828-2.828a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 1.414l-.707.707zm-2.828-2.828a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 1.414l-.707.707z" clip-rule="evenodd" />
130
+ </svg>
131
+ {/if}
132
+ </button>
133
+ </div>
134
+ </div>
135
+ </nav>
136
+
137
+ <style>
138
+ :global(html.dark nav) {
139
+ background-color: #0f0f0f;
140
+ }
141
+ </style>
@@ -0,0 +1,248 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import {
4
+ query,
5
+ results,
6
+ loading,
7
+ resultCount,
8
+ initPagefind,
9
+ updateSearch,
10
+ clearSearch
11
+ } from '$lib/stores/search';
12
+ import { highlightSearchTerms, extractExcerpt } from '$lib/utils/highlight';
13
+
14
+ interface Props {
15
+ showResults?: boolean;
16
+ }
17
+
18
+ let { showResults = true }: Props = $props();
19
+
20
+ let searchInput: HTMLInputElement | undefined;
21
+ let isInitialized = $state(false);
22
+ let searchError = $state<string | null>(null);
23
+ let isDropdownOpen = $state(false);
24
+
25
+ onMount(async () => {
26
+ try {
27
+ await initPagefind();
28
+ isInitialized = true;
29
+ } catch (error) {
30
+ console.error('Failed to initialize search:', error);
31
+ searchError = 'Failed to initialize search';
32
+ }
33
+ });
34
+
35
+ async function handleSearch(e: Event) {
36
+ const target = e.target as HTMLInputElement;
37
+ const searchQuery = target.value;
38
+
39
+ try {
40
+ searchError = null;
41
+ await updateSearch(searchQuery);
42
+ } catch (error) {
43
+ console.error('Search error:', error);
44
+ searchError = 'Search failed. Please try again.';
45
+ }
46
+ }
47
+
48
+ function handleClear() {
49
+ clearSearch();
50
+ if (searchInput) {
51
+ searchInput.focus();
52
+ }
53
+ }
54
+
55
+ function handleKeyDown(e: KeyboardEvent) {
56
+ if (e.key === 'Escape') {
57
+ handleClear();
58
+ isDropdownOpen = false;
59
+ }
60
+ }
61
+
62
+ function handleFocus() {
63
+ isDropdownOpen = true;
64
+ }
65
+
66
+ function handleBlur(e: FocusEvent) {
67
+ // Only close dropdown if focus moves outside the search container
68
+ const target = e.relatedTarget as HTMLElement;
69
+ if (!target || !target.closest('[data-search-container]')) {
70
+ setTimeout(() => {
71
+ isDropdownOpen = false;
72
+ }, 100);
73
+ }
74
+ }
75
+ </script>
76
+
77
+ <div class="relative w-full" data-search-container>
78
+ <!-- Search Input -->
79
+ <div class="relative">
80
+ <input
81
+ bind:this={searchInput}
82
+ type="text"
83
+ placeholder="Search documentation..."
84
+ value={$query}
85
+ oninput={handleSearch}
86
+ onkeydown={handleKeyDown}
87
+ onfocus={handleFocus}
88
+ onblur={handleBlur}
89
+ disabled={!isInitialized}
90
+ class="w-full rounded-lg border border-gray-300 bg-white px-4 py-2 pl-10 text-sm text-gray-900 placeholder-gray-500 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 disabled:opacity-50 transition-all"
91
+ aria-label="Search documentation"
92
+ />
93
+
94
+ <!-- Search Icon -->
95
+ <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
96
+ {#if $loading}
97
+ <svg
98
+ class="h-4 w-4 animate-spin text-gray-400"
99
+ xmlns="http://www.w3.org/2000/svg"
100
+ fill="none"
101
+ viewBox="0 0 24 24"
102
+ >
103
+ <circle
104
+ class="opacity-25"
105
+ cx="12"
106
+ cy="12"
107
+ r="10"
108
+ stroke="currentColor"
109
+ stroke-width="4"
110
+ ></circle>
111
+ <path
112
+ class="opacity-75"
113
+ fill="currentColor"
114
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
115
+ ></path>
116
+ </svg>
117
+ {:else}
118
+ <svg
119
+ class="h-4 w-4 text-gray-400"
120
+ xmlns="http://www.w3.org/2000/svg"
121
+ fill="none"
122
+ viewBox="0 0 24 24"
123
+ stroke="currentColor"
124
+ >
125
+ <path
126
+ stroke-linecap="round"
127
+ stroke-linejoin="round"
128
+ stroke-width="2"
129
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
130
+ />
131
+ </svg>
132
+ {/if}
133
+ </div>
134
+
135
+ <!-- Clear Button -->
136
+ {#if $query}
137
+ <button
138
+ onclick={handleClear}
139
+ class="absolute inset-y-0 right-0 pr-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
140
+ aria-label="Clear search"
141
+ >
142
+ <svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
143
+ <path
144
+ fill-rule="evenodd"
145
+ d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
146
+ clip-rule="evenodd"
147
+ />
148
+ </svg>
149
+ </button>
150
+ {/if}
151
+ </div>
152
+
153
+ <!-- Error Message -->
154
+ {#if searchError}
155
+ <div class="mt-2 rounded-lg bg-red-50 p-3 text-sm text-red-700 dark:bg-red-900/20 dark:text-red-400">
156
+ {searchError}
157
+ </div>
158
+ {/if}
159
+
160
+ <!-- Search Results -->
161
+ {#if showResults && isDropdownOpen && ($query || $results.length > 0)}
162
+ <div class="absolute top-full left-0 right-0 mt-2 max-h-96 overflow-y-auto rounded-lg border border-gray-300 bg-white shadow-lg dark:border-gray-600 dark:bg-gray-800 z-50">
163
+ {#if $loading}
164
+ <div class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
165
+ <div class="inline-block">
166
+ <svg
167
+ class="h-5 w-5 animate-spin"
168
+ xmlns="http://www.w3.org/2000/svg"
169
+ fill="none"
170
+ viewBox="0 0 24 24"
171
+ >
172
+ <circle
173
+ class="opacity-25"
174
+ cx="12"
175
+ cy="12"
176
+ r="10"
177
+ stroke="currentColor"
178
+ stroke-width="4"
179
+ ></circle>
180
+ <path
181
+ class="opacity-75"
182
+ fill="currentColor"
183
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
184
+ ></path>
185
+ </svg>
186
+ </div>
187
+ </div>
188
+ {:else if $results.length === 0 && $query}
189
+ <div class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
190
+ No results found for "<strong>{$query}</strong>"
191
+ </div>
192
+ {:else if $results.length > 0}
193
+ <div class="divide-y divide-gray-200 dark:divide-gray-700">
194
+ {#each $results as result (result.id)}
195
+ <a
196
+ href={result.url}
197
+ class="block px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
198
+ >
199
+ <div class="font-semibold text-gray-900 dark:text-gray-100">
200
+ {@html highlightSearchTerms(result.title, $query)}
201
+ </div>
202
+ {#if result.excerpt || result.content}
203
+ <div class="mt-1 text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
204
+ {@html highlightSearchTerms(
205
+ extractExcerpt(result.content || result.excerpt || '', $query, 200),
206
+ $query
207
+ )}
208
+ </div>
209
+ {/if}
210
+ <div class="mt-2 text-xs text-gray-500 dark:text-gray-500">
211
+ {result.url}
212
+ </div>
213
+ </a>
214
+ {/each}
215
+ </div>
216
+
217
+ {#if $resultCount > 0}
218
+ <div class="border-t border-gray-200 bg-gray-50 px-4 py-2 text-xs text-gray-600 dark:border-gray-700 dark:bg-gray-700/50 dark:text-gray-400">
219
+ {$resultCount} result{$resultCount !== 1 ? 's' : ''} found
220
+ </div>
221
+ {/if}
222
+ {/if}
223
+ </div>
224
+ {/if}
225
+ </div>
226
+
227
+ <style>
228
+ :global(.line-clamp-2) {
229
+ display: -webkit-box;
230
+ -webkit-line-clamp: 2;
231
+ -webkit-box-orient: vertical;
232
+ line-clamp: 2;
233
+ overflow: hidden;
234
+ }
235
+
236
+ :global(mark) {
237
+ background-color: #fef3c7;
238
+ color: #92400e;
239
+ font-weight: 500;
240
+ padding: 0 0.125rem;
241
+ border-radius: 0.125rem;
242
+ }
243
+
244
+ :global(.dark mark) {
245
+ background-color: #78350f;
246
+ color: #fef3c7;
247
+ }
248
+ </style>