specra 0.1.13 → 0.2.0

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 (276) hide show
  1. package/LICENSE.MD +25 -4
  2. package/README.md +67 -58
  3. package/config/specra.config.schema.json +16 -0
  4. package/config/svelte-config.js +63 -0
  5. package/dist/api-parser.types.d.ts +59 -0
  6. package/dist/api-parser.types.js +5 -0
  7. package/dist/api.types.d.ts +137 -0
  8. package/dist/api.types.js +5 -0
  9. package/dist/category.d.ts +21 -0
  10. package/dist/category.js +48 -0
  11. package/dist/components/ConfigProvider.svelte +13 -0
  12. package/dist/components/ConfigProvider.svelte.d.ts +31 -0
  13. package/dist/components/docs/Accordion.svelte +18 -0
  14. package/dist/components/docs/Accordion.svelte.d.ts +10 -0
  15. package/dist/components/docs/AccordionItem.svelte +41 -0
  16. package/dist/components/docs/AccordionItem.svelte.d.ts +10 -0
  17. package/dist/components/docs/Badge.svelte +28 -0
  18. package/dist/components/docs/Badge.svelte.d.ts +9 -0
  19. package/dist/components/docs/Breadcrumb.svelte +80 -0
  20. package/dist/components/docs/Breadcrumb.svelte.d.ts +8 -0
  21. package/dist/components/docs/Callout.svelte +96 -0
  22. package/dist/components/docs/Callout.svelte.d.ts +10 -0
  23. package/dist/components/docs/Card.svelte +63 -0
  24. package/dist/components/docs/Card.svelte.d.ts +12 -0
  25. package/dist/components/docs/CardGrid.svelte +24 -0
  26. package/dist/components/docs/CardGrid.svelte.d.ts +8 -0
  27. package/dist/components/docs/CategoryIndex.svelte +110 -0
  28. package/dist/components/docs/CategoryIndex.svelte.d.ts +29 -0
  29. package/dist/components/docs/CodeBlock.svelte +172 -0
  30. package/dist/components/docs/CodeBlock.svelte.d.ts +8 -0
  31. package/dist/components/docs/Column.svelte +25 -0
  32. package/dist/components/docs/Column.svelte.d.ts +8 -0
  33. package/dist/components/docs/Columns.svelte +38 -0
  34. package/dist/components/docs/Columns.svelte.d.ts +13 -0
  35. package/dist/components/docs/DevModeBadge.svelte +15 -0
  36. package/dist/components/docs/DevModeBadge.svelte.d.ts +18 -0
  37. package/dist/components/docs/DocBadge.svelte +28 -0
  38. package/dist/components/docs/DocBadge.svelte.d.ts +9 -0
  39. package/dist/components/docs/DocLayout.svelte +107 -0
  40. package/dist/components/docs/DocLayout.svelte.d.ts +32 -0
  41. package/dist/components/docs/DocLoading.svelte +53 -0
  42. package/dist/components/docs/DocLoading.svelte.d.ts +18 -0
  43. package/dist/components/docs/DocMetadata.svelte +106 -0
  44. package/dist/components/docs/DocMetadata.svelte.d.ts +18 -0
  45. package/dist/components/docs/DocNavigation.svelte +56 -0
  46. package/dist/components/docs/DocNavigation.svelte.d.ts +12 -0
  47. package/dist/components/docs/DocTags.svelte +22 -0
  48. package/dist/components/docs/DocTags.svelte.d.ts +6 -0
  49. package/dist/components/docs/DraftBadge.svelte +10 -0
  50. package/dist/components/docs/DraftBadge.svelte.d.ts +18 -0
  51. package/dist/components/docs/Footer.svelte +72 -0
  52. package/dist/components/docs/Footer.svelte.d.ts +7 -0
  53. package/dist/components/docs/Frame.svelte +27 -0
  54. package/dist/components/docs/Frame.svelte.d.ts +9 -0
  55. package/dist/components/docs/Header.svelte +123 -0
  56. package/dist/components/docs/Header.svelte.d.ts +9 -0
  57. package/dist/components/docs/HeaderWithMenu.svelte +34 -0
  58. package/dist/components/docs/HeaderWithMenu.svelte.d.ts +17 -0
  59. package/dist/components/docs/HotReloadIndicator.svelte +44 -0
  60. package/dist/components/docs/HotReloadIndicator.svelte.d.ts +3 -0
  61. package/dist/components/docs/Icon.svelte +103 -0
  62. package/dist/components/docs/Icon.svelte.d.ts +11 -0
  63. package/dist/components/docs/Image.svelte +88 -0
  64. package/dist/components/docs/Image.svelte.d.ts +11 -0
  65. package/dist/components/docs/ImageCard.svelte +91 -0
  66. package/dist/components/docs/ImageCard.svelte.d.ts +12 -0
  67. package/dist/components/docs/ImageCardGrid.svelte +25 -0
  68. package/dist/components/docs/ImageCardGrid.svelte.d.ts +8 -0
  69. package/dist/components/docs/LayoutProviders.svelte +57 -0
  70. package/dist/components/docs/LayoutProviders.svelte.d.ts +9 -0
  71. package/dist/components/docs/Logo.svelte +25 -0
  72. package/dist/components/docs/Logo.svelte.d.ts +11 -0
  73. package/dist/components/docs/Math.svelte +54 -0
  74. package/dist/components/docs/Math.svelte.d.ts +7 -0
  75. package/dist/components/docs/MdxContent.svelte +41 -0
  76. package/dist/components/docs/MdxHotReload.svelte +78 -0
  77. package/dist/components/docs/MdxHotReload.svelte.d.ts +9 -0
  78. package/dist/components/docs/MdxLayout.svelte +16 -0
  79. package/dist/components/docs/MdxLayout.svelte.d.ts +6 -0
  80. package/dist/components/docs/Mermaid.svelte +88 -0
  81. package/dist/components/docs/Mermaid.svelte.d.ts +7 -0
  82. package/dist/components/docs/MobileDocLayout.svelte +211 -0
  83. package/dist/components/docs/MobileDocLayout.svelte.d.ts +35 -0
  84. package/dist/components/docs/MobileSidebar.svelte +122 -0
  85. package/dist/components/docs/MobileSidebar.svelte.d.ts +31 -0
  86. package/dist/components/docs/MobileSidebarWrapper.svelte +122 -0
  87. package/dist/components/docs/MobileSidebarWrapper.svelte.d.ts +32 -0
  88. package/dist/components/docs/NotFoundContent.svelte +40 -0
  89. package/dist/components/docs/NotFoundContent.svelte.d.ts +6 -0
  90. package/dist/components/docs/SearchHighlight.svelte +116 -0
  91. package/dist/components/docs/SearchHighlight.svelte.d.ts +3 -0
  92. package/dist/components/docs/SearchModal.svelte +239 -0
  93. package/dist/components/docs/SearchModal.svelte.d.ts +9 -0
  94. package/dist/components/docs/Sidebar.svelte +69 -0
  95. package/dist/components/docs/Sidebar.svelte.d.ts +31 -0
  96. package/dist/components/docs/SidebarMenuItems.svelte +344 -0
  97. package/dist/components/docs/SidebarMenuItems.svelte.d.ts +33 -0
  98. package/dist/components/docs/SidebarSkeleton.svelte +50 -0
  99. package/dist/components/docs/SidebarSkeleton.svelte.d.ts +18 -0
  100. package/dist/components/docs/SiteBanner.svelte +92 -0
  101. package/dist/components/docs/SiteBanner.svelte.d.ts +7 -0
  102. package/dist/components/docs/Step.svelte +44 -0
  103. package/dist/components/docs/Step.svelte.d.ts +8 -0
  104. package/dist/components/docs/Steps.svelte +15 -0
  105. package/dist/components/docs/Steps.svelte.d.ts +7 -0
  106. package/dist/components/docs/Tab.svelte +40 -0
  107. package/dist/components/docs/Tab.svelte.d.ts +8 -0
  108. package/dist/components/docs/TabGroups.svelte +183 -0
  109. package/dist/components/docs/TabGroups.svelte.d.ts +25 -0
  110. package/dist/components/docs/TableOfContents.svelte +100 -0
  111. package/dist/components/docs/TableOfContents.svelte.d.ts +9 -0
  112. package/dist/components/docs/Tabs.svelte +69 -0
  113. package/dist/components/docs/Tabs.svelte.d.ts +8 -0
  114. package/dist/components/docs/ThemeToggle.svelte +16 -0
  115. package/dist/components/docs/ThemeToggle.svelte.d.ts +18 -0
  116. package/dist/components/docs/Tooltip.svelte +44 -0
  117. package/dist/components/docs/Tooltip.svelte.d.ts +10 -0
  118. package/dist/components/docs/VersionSwitcher.svelte +95 -0
  119. package/dist/components/docs/VersionSwitcher.svelte.d.ts +7 -0
  120. package/dist/components/docs/Video.svelte +84 -0
  121. package/dist/components/docs/Video.svelte.d.ts +12 -0
  122. package/dist/components/docs/api/ApiEndpoint.svelte +61 -0
  123. package/dist/components/docs/api/ApiEndpoint.svelte.d.ts +11 -0
  124. package/dist/components/docs/api/ApiParams.svelte +80 -0
  125. package/dist/components/docs/api/ApiParams.svelte.d.ts +14 -0
  126. package/dist/components/docs/api/ApiPlayground.svelte +259 -0
  127. package/dist/components/docs/api/ApiPlayground.svelte.d.ts +16 -0
  128. package/dist/components/docs/api/ApiReference.svelte +278 -0
  129. package/dist/components/docs/api/ApiReference.svelte.d.ts +23 -0
  130. package/dist/components/docs/api/ApiResponse.svelte +66 -0
  131. package/dist/components/docs/api/ApiResponse.svelte.d.ts +9 -0
  132. package/dist/components/docs/api/index.d.ts +5 -0
  133. package/dist/components/docs/api/index.js +5 -0
  134. package/dist/components/docs/componentTextProps.d.ts +3 -0
  135. package/dist/components/docs/componentTextProps.js +61 -0
  136. package/dist/components/docs/index.d.ts +54 -0
  137. package/dist/components/docs/index.js +56 -0
  138. package/dist/components/global/VersionNotFound.svelte +48 -0
  139. package/dist/components/global/VersionNotFound.svelte.d.ts +7 -0
  140. package/dist/components/global/index.d.ts +1 -0
  141. package/dist/components/global/index.js +1 -0
  142. package/dist/components/index.d.ts +6 -822
  143. package/dist/components/index.js +11 -3854
  144. package/dist/components/ui/Badge.svelte +48 -0
  145. package/dist/components/ui/Badge.svelte.d.ts +15 -0
  146. package/dist/components/ui/Button.svelte +58 -0
  147. package/dist/components/ui/Button.svelte.d.ts +17 -0
  148. package/dist/components/ui/Dialog.svelte +16 -0
  149. package/dist/components/ui/Dialog.svelte.d.ts +9 -0
  150. package/dist/components/ui/DialogClose.svelte +16 -0
  151. package/dist/components/ui/DialogClose.svelte.d.ts +9 -0
  152. package/dist/components/ui/DialogContent.svelte +43 -0
  153. package/dist/components/ui/DialogContent.svelte.d.ts +10 -0
  154. package/dist/components/ui/DialogDescription.svelte +21 -0
  155. package/dist/components/ui/DialogDescription.svelte.d.ts +9 -0
  156. package/dist/components/ui/DialogFooter.svelte +20 -0
  157. package/dist/components/ui/DialogFooter.svelte.d.ts +9 -0
  158. package/dist/components/ui/DialogHeader.svelte +20 -0
  159. package/dist/components/ui/DialogHeader.svelte.d.ts +9 -0
  160. package/dist/components/ui/DialogTitle.svelte +21 -0
  161. package/dist/components/ui/DialogTitle.svelte.d.ts +9 -0
  162. package/dist/components/ui/Input.svelte +23 -0
  163. package/dist/components/ui/Input.svelte.d.ts +8 -0
  164. package/dist/components/ui/Textarea.svelte +19 -0
  165. package/dist/components/ui/Textarea.svelte.d.ts +7 -0
  166. package/dist/components/ui/index.d.ts +11 -0
  167. package/dist/components/ui/index.js +11 -0
  168. package/dist/config.d.ts +8 -0
  169. package/dist/config.js +9 -0
  170. package/dist/config.schema.json +471 -0
  171. package/dist/config.server.d.ts +46 -0
  172. package/dist/config.server.js +149 -0
  173. package/dist/{mdx-ColN3Cyg.d.mts → config.types.d.ts} +22 -75
  174. package/dist/config.types.js +39 -0
  175. package/dist/dev-utils.d.ts +29 -0
  176. package/dist/dev-utils.js +63 -0
  177. package/dist/index.d.ts +19 -4
  178. package/dist/index.js +25 -4861
  179. package/dist/mdx-cache.d.ts +41 -0
  180. package/dist/mdx-cache.js +160 -0
  181. package/dist/mdx-components.js +50 -1931
  182. package/dist/mdx-security.d.ts +76 -0
  183. package/dist/mdx-security.js +217 -0
  184. package/dist/mdx.d.ts +73 -0
  185. package/dist/mdx.js +1099 -0
  186. package/dist/middleware/index.d.ts +1 -0
  187. package/dist/middleware/index.js +2 -0
  188. package/dist/middleware/security.d.ts +22 -47
  189. package/dist/middleware/security.js +111 -137
  190. package/dist/parsers/base-parser.d.ts +14 -0
  191. package/dist/parsers/base-parser.js +1 -0
  192. package/dist/parsers/index.d.ts +16 -0
  193. package/dist/parsers/index.js +51 -0
  194. package/dist/parsers/openapi-parser.d.ts +18 -0
  195. package/dist/parsers/openapi-parser.js +209 -0
  196. package/dist/parsers/postman-parser.d.ts +20 -0
  197. package/dist/parsers/postman-parser.js +260 -0
  198. package/dist/parsers/specra-parser.d.ts +10 -0
  199. package/dist/parsers/specra-parser.js +18 -0
  200. package/dist/redirects.d.ts +12 -0
  201. package/dist/redirects.js +30 -0
  202. package/dist/remark-code-meta.d.ts +6 -0
  203. package/dist/remark-code-meta.js +21 -0
  204. package/dist/sidebar-utils.d.ts +59 -0
  205. package/dist/sidebar-utils.js +144 -0
  206. package/dist/stores/config.d.ts +20 -0
  207. package/dist/stores/config.js +45 -0
  208. package/dist/stores/index.d.ts +4 -0
  209. package/dist/stores/index.js +4 -0
  210. package/dist/stores/sidebar.d.ts +7 -0
  211. package/dist/stores/sidebar.js +12 -0
  212. package/dist/stores/tabs.d.ts +6 -0
  213. package/dist/stores/tabs.js +41 -0
  214. package/dist/stores/theme.d.ts +7 -0
  215. package/dist/stores/theme.js +75 -0
  216. package/dist/{styles.css → styles/globals.css} +136 -6
  217. package/dist/toc.d.ts +9 -0
  218. package/dist/toc.js +15 -0
  219. package/dist/utils.d.ts +13 -0
  220. package/dist/utils.js +30 -0
  221. package/package.json +47 -90
  222. package/dist/app/api/mdx-watch/route.d.mts +0 -10
  223. package/dist/app/api/mdx-watch/route.d.ts +0 -10
  224. package/dist/app/api/mdx-watch/route.js +0 -118
  225. package/dist/app/api/mdx-watch/route.js.map +0 -1
  226. package/dist/app/api/mdx-watch/route.mjs +0 -91
  227. package/dist/app/api/mdx-watch/route.mjs.map +0 -1
  228. package/dist/chunk-6S3EJVEO.mjs +0 -259
  229. package/dist/chunk-6S3EJVEO.mjs.map +0 -1
  230. package/dist/chunk-BE7EROIW.mjs +0 -212
  231. package/dist/chunk-BE7EROIW.mjs.map +0 -1
  232. package/dist/chunk-CWHRZHZO.mjs +0 -168
  233. package/dist/chunk-CWHRZHZO.mjs.map +0 -1
  234. package/dist/chunk-D5VDVYFY.mjs +0 -1325
  235. package/dist/chunk-D5VDVYFY.mjs.map +0 -1
  236. package/dist/chunk-WMCO2UX5.mjs +0 -585
  237. package/dist/chunk-WMCO2UX5.mjs.map +0 -1
  238. package/dist/chunk-XEMGCPZZ.mjs +0 -475
  239. package/dist/chunk-XEMGCPZZ.mjs.map +0 -1
  240. package/dist/components/index.d.mts +0 -822
  241. package/dist/components/index.js.map +0 -1
  242. package/dist/components/index.mjs +0 -3741
  243. package/dist/components/index.mjs.map +0 -1
  244. package/dist/index.d.mts +0 -4
  245. package/dist/index.js.map +0 -1
  246. package/dist/index.mjs +0 -1897
  247. package/dist/index.mjs.map +0 -1
  248. package/dist/layouts/index.d.mts +0 -34
  249. package/dist/layouts/index.d.ts +0 -34
  250. package/dist/layouts/index.js +0 -453
  251. package/dist/layouts/index.js.map +0 -1
  252. package/dist/layouts/index.mjs +0 -173
  253. package/dist/layouts/index.mjs.map +0 -1
  254. package/dist/lib/index.d.mts +0 -583
  255. package/dist/lib/index.d.ts +0 -583
  256. package/dist/lib/index.js +0 -1595
  257. package/dist/lib/index.js.map +0 -1
  258. package/dist/lib/index.mjs +0 -111
  259. package/dist/lib/index.mjs.map +0 -1
  260. package/dist/mdx-ColN3Cyg.d.ts +0 -352
  261. package/dist/mdx-components.d.mts +0 -86
  262. package/dist/mdx-components.d.ts +0 -86
  263. package/dist/mdx-components.js.map +0 -1
  264. package/dist/mdx-components.mjs +0 -206
  265. package/dist/mdx-components.mjs.map +0 -1
  266. package/dist/middleware/security.d.mts +0 -82
  267. package/dist/middleware/security.js.map +0 -1
  268. package/dist/middleware/security.mjs +0 -84
  269. package/dist/middleware/security.mjs.map +0 -1
  270. package/dist/styles.css.map +0 -1
  271. package/dist/styles.d.mts +0 -2
  272. package/dist/styles.d.ts +0 -2
  273. package/dist/styles.js +0 -2
  274. package/dist/styles.js.map +0 -1
  275. package/dist/styles.mjs +0 -1
  276. package/dist/styles.mjs.map +0 -1
@@ -0,0 +1,116 @@
1
+ <script lang="ts">
2
+ import { page } from '$app/stores';
3
+ import { browser } from '$app/environment';
4
+
5
+ const searchQuery = $derived($page.url.searchParams.get('q') || $page.url.searchParams.get('query') || '');
6
+
7
+ $effect(() => {
8
+ if (!browser || !searchQuery) return;
9
+
10
+ // Wait for content to render
11
+ const timer = setTimeout(() => {
12
+ highlightSearchTerms(searchQuery);
13
+ }, 100);
14
+
15
+ return () => {
16
+ clearTimeout(timer);
17
+ removeHighlights();
18
+ };
19
+ });
20
+
21
+ function highlightSearchTerms(query: string) {
22
+ if (!query.trim()) return;
23
+
24
+ // Remove any existing highlights first
25
+ removeHighlights();
26
+
27
+ const contentEl = document.querySelector('[data-doc-content]') || document.querySelector('.prose') || document.querySelector('main');
28
+ if (!contentEl) return;
29
+
30
+ const walker = document.createTreeWalker(contentEl, NodeFilter.SHOW_TEXT, {
31
+ acceptNode(node) {
32
+ // Skip script, style, and already-highlighted nodes
33
+ const parent = node.parentElement;
34
+ if (!parent) return NodeFilter.FILTER_REJECT;
35
+ const tagName = parent.tagName.toLowerCase();
36
+ if (tagName === 'script' || tagName === 'style' || tagName === 'code' || tagName === 'pre') {
37
+ return NodeFilter.FILTER_REJECT;
38
+ }
39
+ if (parent.classList.contains('search-highlight')) {
40
+ return NodeFilter.FILTER_REJECT;
41
+ }
42
+ return NodeFilter.FILTER_ACCEPT;
43
+ }
44
+ });
45
+
46
+ const textNodes: Text[] = [];
47
+ let currentNode: Node | null;
48
+ while ((currentNode = walker.nextNode())) {
49
+ textNodes.push(currentNode as Text);
50
+ }
51
+
52
+ const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
53
+ const regex = new RegExp(`(${escaped})`, 'gi');
54
+
55
+ let firstHighlight: HTMLElement | null = null;
56
+
57
+ for (const textNode of textNodes) {
58
+ const text = textNode.textContent || '';
59
+ if (!regex.test(text)) continue;
60
+
61
+ // Reset regex lastIndex
62
+ regex.lastIndex = 0;
63
+
64
+ const fragment = document.createDocumentFragment();
65
+ let lastIndex = 0;
66
+ let match: RegExpExecArray | null;
67
+
68
+ while ((match = regex.exec(text)) !== null) {
69
+ // Add text before match
70
+ if (match.index > lastIndex) {
71
+ fragment.appendChild(document.createTextNode(text.slice(lastIndex, match.index)));
72
+ }
73
+
74
+ // Add highlighted match
75
+ const mark = document.createElement('mark');
76
+ mark.className = 'search-highlight bg-primary/20 text-primary rounded-sm px-0.5';
77
+ mark.textContent = match[1];
78
+ fragment.appendChild(mark);
79
+
80
+ if (!firstHighlight) {
81
+ firstHighlight = mark;
82
+ }
83
+
84
+ lastIndex = match.index + match[0].length;
85
+ }
86
+
87
+ // Add remaining text
88
+ if (lastIndex < text.length) {
89
+ fragment.appendChild(document.createTextNode(text.slice(lastIndex)));
90
+ }
91
+
92
+ textNode.parentNode?.replaceChild(fragment, textNode);
93
+ }
94
+
95
+ // Scroll to first highlight
96
+ if (firstHighlight) {
97
+ setTimeout(() => {
98
+ firstHighlight?.scrollIntoView({ behavior: 'smooth', block: 'center' });
99
+ }, 200);
100
+ }
101
+ }
102
+
103
+ function removeHighlights() {
104
+ if (!browser) return;
105
+ const highlights = document.querySelectorAll('.search-highlight');
106
+ highlights.forEach((mark) => {
107
+ const parent = mark.parentNode;
108
+ if (parent) {
109
+ parent.replaceChild(document.createTextNode(mark.textContent || ''), mark);
110
+ parent.normalize();
111
+ }
112
+ });
113
+ }
114
+ </script>
115
+
116
+ <!-- SearchHighlight is a side-effect only component, no visible output -->
@@ -0,0 +1,3 @@
1
+ declare const SearchHighlight: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type SearchHighlight = ReturnType<typeof SearchHighlight>;
3
+ export default SearchHighlight;
@@ -0,0 +1,239 @@
1
+ <script lang="ts">
2
+ import { Search, X, FileText, Hash, ArrowUp, ArrowDown, CornerDownLeft, Loader2 } from 'lucide-svelte';
3
+ import { goto } from '$app/navigation';
4
+ import { browser } from '$app/environment';
5
+ import type { SpecraConfig } from '../../config.types.js';
6
+
7
+ interface SearchResult {
8
+ slug: string;
9
+ title: string;
10
+ description?: string;
11
+ excerpt?: string;
12
+ version?: string;
13
+ headings?: Array<{ id: string; title: string }>;
14
+ }
15
+
16
+ interface Props {
17
+ isOpen: boolean;
18
+ onClose: () => void;
19
+ config: SpecraConfig;
20
+ }
21
+
22
+ let { isOpen, onClose, config }: Props = $props();
23
+
24
+ let query = $state('');
25
+ let results = $state<SearchResult[]>([]);
26
+ let isLoading = $state(false);
27
+ let selectedIndex = $state(0);
28
+ let inputEl = $state<HTMLInputElement | null>(null);
29
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
30
+
31
+ const baseUrl = $derived(config.site?.baseUrl || '/');
32
+
33
+ $effect(() => {
34
+ if (isOpen && inputEl) {
35
+ // Focus input when modal opens
36
+ setTimeout(() => inputEl?.focus(), 50);
37
+ }
38
+ if (!isOpen) {
39
+ query = '';
40
+ results = [];
41
+ selectedIndex = 0;
42
+ isLoading = false;
43
+ }
44
+ });
45
+
46
+ $effect(() => {
47
+ if (!browser) return;
48
+
49
+ function handleKeydown(e: KeyboardEvent) {
50
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
51
+ e.preventDefault();
52
+ if (isOpen) {
53
+ onClose();
54
+ }
55
+ }
56
+ }
57
+
58
+ document.addEventListener('keydown', handleKeydown);
59
+ return () => document.removeEventListener('keydown', handleKeydown);
60
+ });
61
+
62
+ function handleSearch(value: string) {
63
+ query = value;
64
+ selectedIndex = 0;
65
+
66
+ if (debounceTimer) clearTimeout(debounceTimer);
67
+
68
+ if (!value.trim()) {
69
+ results = [];
70
+ isLoading = false;
71
+ return;
72
+ }
73
+
74
+ isLoading = true;
75
+ debounceTimer = setTimeout(async () => {
76
+ try {
77
+ const response = await fetch(
78
+ `${baseUrl.replace(/\/$/, '')}/api/search?q=${encodeURIComponent(value.trim())}`
79
+ );
80
+ if (response.ok) {
81
+ const data = await response.json();
82
+ results = data.results || data || [];
83
+ } else {
84
+ results = [];
85
+ }
86
+ } catch {
87
+ results = [];
88
+ } finally {
89
+ isLoading = false;
90
+ }
91
+ }, 300);
92
+ }
93
+
94
+ function navigateToResult(result: SearchResult) {
95
+ const version = result.version || config.site?.activeVersion || 'v1';
96
+ const url = `${baseUrl.replace(/\/$/, '')}/${version}/${result.slug}`;
97
+ goto(url);
98
+ onClose();
99
+ }
100
+
101
+ function handleKeydown(e: KeyboardEvent) {
102
+ switch (e.key) {
103
+ case 'ArrowDown':
104
+ e.preventDefault();
105
+ selectedIndex = Math.min(selectedIndex + 1, results.length - 1);
106
+ break;
107
+ case 'ArrowUp':
108
+ e.preventDefault();
109
+ selectedIndex = Math.max(selectedIndex - 1, 0);
110
+ break;
111
+ case 'Enter':
112
+ e.preventDefault();
113
+ if (results[selectedIndex]) {
114
+ navigateToResult(results[selectedIndex]);
115
+ }
116
+ break;
117
+ case 'Escape':
118
+ e.preventDefault();
119
+ onClose();
120
+ break;
121
+ }
122
+ }
123
+
124
+ function highlightMatch(text: string, searchQuery: string): string {
125
+ if (!searchQuery.trim() || !text) return text;
126
+ const escaped = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
127
+ const regex = new RegExp(`(${escaped})`, 'gi');
128
+ return text.replace(regex, '<mark class="bg-primary/20 text-primary rounded-sm px-0.5">$1</mark>');
129
+ }
130
+ </script>
131
+
132
+ {#if isOpen}
133
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
134
+ <div
135
+ class="fixed inset-0 z-50 flex items-start justify-center pt-[15vh]"
136
+ role="dialog"
137
+ aria-modal="true"
138
+ aria-label="Search documentation"
139
+ onkeydown={handleKeydown}
140
+ >
141
+ <!-- Backdrop -->
142
+ <button
143
+ class="absolute inset-0 bg-background/80 backdrop-blur-sm"
144
+ onclick={onClose}
145
+ aria-label="Close search"
146
+ tabindex="-1"
147
+ ></button>
148
+
149
+ <!-- Modal -->
150
+ <div class="relative w-full max-w-lg mx-4 bg-popover border border-border rounded-xl shadow-2xl overflow-hidden">
151
+ <!-- Search Input -->
152
+ <div class="flex items-center border-b border-border px-4">
153
+ <Search class="h-4 w-4 text-muted-foreground shrink-0" />
154
+ <input
155
+ bind:this={inputEl}
156
+ type="text"
157
+ placeholder={config.search?.placeholder || 'Search documentation...'}
158
+ value={query}
159
+ oninput={(e) => handleSearch(e.currentTarget.value)}
160
+ class="flex-1 h-12 px-3 bg-transparent text-sm text-foreground placeholder:text-muted-foreground outline-none"
161
+ />
162
+ {#if isLoading}
163
+ <Loader2 class="h-4 w-4 text-muted-foreground animate-spin shrink-0" />
164
+ {:else if query}
165
+ <button
166
+ onclick={() => handleSearch('')}
167
+ class="p-1 hover:bg-accent rounded-md"
168
+ aria-label="Clear search"
169
+ >
170
+ <X class="h-3 w-3 text-muted-foreground" />
171
+ </button>
172
+ {/if}
173
+ </div>
174
+
175
+ <!-- Results -->
176
+ <div class="max-h-80 overflow-y-auto">
177
+ {#if results.length > 0}
178
+ <div class="py-2">
179
+ {#each results as result, index}
180
+ <button
181
+ class="w-full flex items-start gap-3 px-4 py-3 text-left transition-colors {index === selectedIndex ? 'bg-accent text-accent-foreground' : 'text-foreground hover:bg-accent/50'}"
182
+ onclick={() => navigateToResult(result)}
183
+ onmouseenter={() => (selectedIndex = index)}
184
+ >
185
+ <FileText class="h-4 w-4 mt-0.5 text-muted-foreground shrink-0" />
186
+ <div class="flex-1 min-w-0">
187
+ <div class="text-sm font-medium truncate">
188
+ {@html highlightMatch(result.title, query)}
189
+ </div>
190
+ {#if result.description || result.excerpt}
191
+ <div class="text-xs text-muted-foreground mt-0.5 line-clamp-2">
192
+ {@html highlightMatch(result.description || result.excerpt || '', query)}
193
+ </div>
194
+ {/if}
195
+ {#if result.headings && result.headings.length > 0}
196
+ <div class="flex flex-wrap gap-1 mt-1">
197
+ {#each result.headings.slice(0, 3) as heading}
198
+ <span class="inline-flex items-center gap-0.5 text-xs text-muted-foreground">
199
+ <Hash class="h-2.5 w-2.5" />
200
+ {heading.title}
201
+ </span>
202
+ {/each}
203
+ </div>
204
+ {/if}
205
+ </div>
206
+ </button>
207
+ {/each}
208
+ </div>
209
+ {:else if query && !isLoading}
210
+ <div class="py-12 text-center">
211
+ <p class="text-sm text-muted-foreground">No results found for "{query}"</p>
212
+ <p class="text-xs text-muted-foreground mt-1">Try different keywords or check spelling</p>
213
+ </div>
214
+ {:else if !query}
215
+ <div class="py-12 text-center">
216
+ <p class="text-sm text-muted-foreground">Start typing to search...</p>
217
+ </div>
218
+ {/if}
219
+ </div>
220
+
221
+ <!-- Footer -->
222
+ <div class="flex items-center gap-4 px-4 py-2 border-t border-border text-xs text-muted-foreground">
223
+ <span class="inline-flex items-center gap-1">
224
+ <ArrowUp class="h-3 w-3" />
225
+ <ArrowDown class="h-3 w-3" />
226
+ navigate
227
+ </span>
228
+ <span class="inline-flex items-center gap-1">
229
+ <CornerDownLeft class="h-3 w-3" />
230
+ open
231
+ </span>
232
+ <span class="inline-flex items-center gap-1">
233
+ <kbd class="px-1.5 py-0.5 rounded border border-border bg-muted text-[10px] font-mono">esc</kbd>
234
+ close
235
+ </span>
236
+ </div>
237
+ </div>
238
+ </div>
239
+ {/if}
@@ -0,0 +1,9 @@
1
+ import type { SpecraConfig } from '../../config.types.js';
2
+ interface Props {
3
+ isOpen: boolean;
4
+ onClose: () => void;
5
+ config: SpecraConfig;
6
+ }
7
+ declare const SearchModal: import("svelte").Component<Props, {}, "">;
8
+ type SearchModal = ReturnType<typeof SearchModal>;
9
+ export default SearchModal;
@@ -0,0 +1,69 @@
1
+ <script lang="ts">
2
+ import type { SpecraConfig } from '../../config.types.js';
3
+ import SidebarMenuItems from './SidebarMenuItems.svelte';
4
+
5
+ interface DocItem {
6
+ title: string;
7
+ slug: string;
8
+ filePath: string;
9
+ section?: string;
10
+ group?: string;
11
+ sidebar?: string;
12
+ sidebar_position?: number;
13
+ categoryLabel?: string;
14
+ categoryPosition?: number;
15
+ categoryCollapsible?: boolean;
16
+ categoryCollapsed?: boolean;
17
+ categoryIcon?: string;
18
+ categoryTabGroup?: string;
19
+ meta?: {
20
+ icon?: string;
21
+ tab_group?: string;
22
+ [key: string]: any;
23
+ };
24
+ }
25
+
26
+ interface Props {
27
+ docs: DocItem[];
28
+ version: string;
29
+ onLinkClick?: () => void;
30
+ config: SpecraConfig;
31
+ activeTabGroup?: string;
32
+ }
33
+
34
+ let { docs, version, onLinkClick, config, activeTabGroup }: Props = $props();
35
+
36
+ let hasTabGroups = $derived(
37
+ config.navigation?.tabGroups && config.navigation.tabGroups.length > 0
38
+ );
39
+ let isFlush = $derived(config.navigation?.sidebarStyle === 'flush');
40
+ let stickyTop = $derived(hasTabGroups ? 'top-[7.5rem]' : 'top-24');
41
+ let maxHeight = $derived(
42
+ hasTabGroups ? 'max-h-[calc(100vh-10rem)]' : 'max-h-[calc(100vh-7rem)]'
43
+ );
44
+ let containerClass = $derived(
45
+ isFlush
46
+ ? `${maxHeight} overflow-y-auto p-4 border-r border-border`
47
+ : `${maxHeight} overflow-y-auto bg-muted/30 dark:bg-muted/10 rounded-2xl p-4 border border-border/50`
48
+ );
49
+ </script>
50
+
51
+ {#if config.navigation?.showSidebar}
52
+ <aside class="w-64 shrink-0 sticky {stickyTop} self-start">
53
+ <div class={containerClass}>
54
+ <div class="flex items-center justify-between mb-4 px-2">
55
+ <h2 class="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Documentation</h2>
56
+ {#if config.features?.showVersionBadge && version}
57
+ <span class="text-xs font-medium text-muted-foreground bg-muted px-2 py-0.5 rounded-md">{version}</span>
58
+ {/if}
59
+ </div>
60
+ <SidebarMenuItems
61
+ {docs}
62
+ {version}
63
+ {onLinkClick}
64
+ {config}
65
+ {activeTabGroup}
66
+ />
67
+ </div>
68
+ </aside>
69
+ {/if}
@@ -0,0 +1,31 @@
1
+ import type { SpecraConfig } from '../../config.types.js';
2
+ interface DocItem {
3
+ title: string;
4
+ slug: string;
5
+ filePath: string;
6
+ section?: string;
7
+ group?: string;
8
+ sidebar?: string;
9
+ sidebar_position?: number;
10
+ categoryLabel?: string;
11
+ categoryPosition?: number;
12
+ categoryCollapsible?: boolean;
13
+ categoryCollapsed?: boolean;
14
+ categoryIcon?: string;
15
+ categoryTabGroup?: string;
16
+ meta?: {
17
+ icon?: string;
18
+ tab_group?: string;
19
+ [key: string]: any;
20
+ };
21
+ }
22
+ interface Props {
23
+ docs: DocItem[];
24
+ version: string;
25
+ onLinkClick?: () => void;
26
+ config: SpecraConfig;
27
+ activeTabGroup?: string;
28
+ }
29
+ declare const Sidebar: import("svelte").Component<Props, {}, "">;
30
+ type Sidebar = ReturnType<typeof Sidebar>;
31
+ export default Sidebar;