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.
- package/LICENSE.MD +25 -4
- package/README.md +67 -58
- package/config/specra.config.schema.json +16 -0
- package/config/svelte-config.js +63 -0
- package/dist/api-parser.types.d.ts +59 -0
- package/dist/api-parser.types.js +5 -0
- package/dist/api.types.d.ts +137 -0
- package/dist/api.types.js +5 -0
- package/dist/category.d.ts +21 -0
- package/dist/category.js +48 -0
- package/dist/components/ConfigProvider.svelte +13 -0
- package/dist/components/ConfigProvider.svelte.d.ts +31 -0
- package/dist/components/docs/Accordion.svelte +18 -0
- package/dist/components/docs/Accordion.svelte.d.ts +10 -0
- package/dist/components/docs/AccordionItem.svelte +41 -0
- package/dist/components/docs/AccordionItem.svelte.d.ts +10 -0
- package/dist/components/docs/Badge.svelte +28 -0
- package/dist/components/docs/Badge.svelte.d.ts +9 -0
- package/dist/components/docs/Breadcrumb.svelte +80 -0
- package/dist/components/docs/Breadcrumb.svelte.d.ts +8 -0
- package/dist/components/docs/Callout.svelte +96 -0
- package/dist/components/docs/Callout.svelte.d.ts +10 -0
- package/dist/components/docs/Card.svelte +63 -0
- package/dist/components/docs/Card.svelte.d.ts +12 -0
- package/dist/components/docs/CardGrid.svelte +24 -0
- package/dist/components/docs/CardGrid.svelte.d.ts +8 -0
- package/dist/components/docs/CategoryIndex.svelte +110 -0
- package/dist/components/docs/CategoryIndex.svelte.d.ts +29 -0
- package/dist/components/docs/CodeBlock.svelte +172 -0
- package/dist/components/docs/CodeBlock.svelte.d.ts +8 -0
- package/dist/components/docs/Column.svelte +25 -0
- package/dist/components/docs/Column.svelte.d.ts +8 -0
- package/dist/components/docs/Columns.svelte +38 -0
- package/dist/components/docs/Columns.svelte.d.ts +13 -0
- package/dist/components/docs/DevModeBadge.svelte +15 -0
- package/dist/components/docs/DevModeBadge.svelte.d.ts +18 -0
- package/dist/components/docs/DocBadge.svelte +28 -0
- package/dist/components/docs/DocBadge.svelte.d.ts +9 -0
- package/dist/components/docs/DocLayout.svelte +107 -0
- package/dist/components/docs/DocLayout.svelte.d.ts +32 -0
- package/dist/components/docs/DocLoading.svelte +53 -0
- package/dist/components/docs/DocLoading.svelte.d.ts +18 -0
- package/dist/components/docs/DocMetadata.svelte +106 -0
- package/dist/components/docs/DocMetadata.svelte.d.ts +18 -0
- package/dist/components/docs/DocNavigation.svelte +56 -0
- package/dist/components/docs/DocNavigation.svelte.d.ts +12 -0
- package/dist/components/docs/DocTags.svelte +22 -0
- package/dist/components/docs/DocTags.svelte.d.ts +6 -0
- package/dist/components/docs/DraftBadge.svelte +10 -0
- package/dist/components/docs/DraftBadge.svelte.d.ts +18 -0
- package/dist/components/docs/Footer.svelte +72 -0
- package/dist/components/docs/Footer.svelte.d.ts +7 -0
- package/dist/components/docs/Frame.svelte +27 -0
- package/dist/components/docs/Frame.svelte.d.ts +9 -0
- package/dist/components/docs/Header.svelte +123 -0
- package/dist/components/docs/Header.svelte.d.ts +9 -0
- package/dist/components/docs/HeaderWithMenu.svelte +34 -0
- package/dist/components/docs/HeaderWithMenu.svelte.d.ts +17 -0
- package/dist/components/docs/HotReloadIndicator.svelte +44 -0
- package/dist/components/docs/HotReloadIndicator.svelte.d.ts +3 -0
- package/dist/components/docs/Icon.svelte +103 -0
- package/dist/components/docs/Icon.svelte.d.ts +11 -0
- package/dist/components/docs/Image.svelte +88 -0
- package/dist/components/docs/Image.svelte.d.ts +11 -0
- package/dist/components/docs/ImageCard.svelte +91 -0
- package/dist/components/docs/ImageCard.svelte.d.ts +12 -0
- package/dist/components/docs/ImageCardGrid.svelte +25 -0
- package/dist/components/docs/ImageCardGrid.svelte.d.ts +8 -0
- package/dist/components/docs/LayoutProviders.svelte +57 -0
- package/dist/components/docs/LayoutProviders.svelte.d.ts +9 -0
- package/dist/components/docs/Logo.svelte +25 -0
- package/dist/components/docs/Logo.svelte.d.ts +11 -0
- package/dist/components/docs/Math.svelte +54 -0
- package/dist/components/docs/Math.svelte.d.ts +7 -0
- package/dist/components/docs/MdxContent.svelte +41 -0
- package/dist/components/docs/MdxHotReload.svelte +78 -0
- package/dist/components/docs/MdxHotReload.svelte.d.ts +9 -0
- package/dist/components/docs/MdxLayout.svelte +16 -0
- package/dist/components/docs/MdxLayout.svelte.d.ts +6 -0
- package/dist/components/docs/Mermaid.svelte +88 -0
- package/dist/components/docs/Mermaid.svelte.d.ts +7 -0
- package/dist/components/docs/MobileDocLayout.svelte +211 -0
- package/dist/components/docs/MobileDocLayout.svelte.d.ts +35 -0
- package/dist/components/docs/MobileSidebar.svelte +122 -0
- package/dist/components/docs/MobileSidebar.svelte.d.ts +31 -0
- package/dist/components/docs/MobileSidebarWrapper.svelte +122 -0
- package/dist/components/docs/MobileSidebarWrapper.svelte.d.ts +32 -0
- package/dist/components/docs/NotFoundContent.svelte +40 -0
- package/dist/components/docs/NotFoundContent.svelte.d.ts +6 -0
- package/dist/components/docs/SearchHighlight.svelte +116 -0
- package/dist/components/docs/SearchHighlight.svelte.d.ts +3 -0
- package/dist/components/docs/SearchModal.svelte +239 -0
- package/dist/components/docs/SearchModal.svelte.d.ts +9 -0
- package/dist/components/docs/Sidebar.svelte +69 -0
- package/dist/components/docs/Sidebar.svelte.d.ts +31 -0
- package/dist/components/docs/SidebarMenuItems.svelte +344 -0
- package/dist/components/docs/SidebarMenuItems.svelte.d.ts +33 -0
- package/dist/components/docs/SidebarSkeleton.svelte +50 -0
- package/dist/components/docs/SidebarSkeleton.svelte.d.ts +18 -0
- package/dist/components/docs/SiteBanner.svelte +92 -0
- package/dist/components/docs/SiteBanner.svelte.d.ts +7 -0
- package/dist/components/docs/Step.svelte +44 -0
- package/dist/components/docs/Step.svelte.d.ts +8 -0
- package/dist/components/docs/Steps.svelte +15 -0
- package/dist/components/docs/Steps.svelte.d.ts +7 -0
- package/dist/components/docs/Tab.svelte +40 -0
- package/dist/components/docs/Tab.svelte.d.ts +8 -0
- package/dist/components/docs/TabGroups.svelte +183 -0
- package/dist/components/docs/TabGroups.svelte.d.ts +25 -0
- package/dist/components/docs/TableOfContents.svelte +100 -0
- package/dist/components/docs/TableOfContents.svelte.d.ts +9 -0
- package/dist/components/docs/Tabs.svelte +69 -0
- package/dist/components/docs/Tabs.svelte.d.ts +8 -0
- package/dist/components/docs/ThemeToggle.svelte +16 -0
- package/dist/components/docs/ThemeToggle.svelte.d.ts +18 -0
- package/dist/components/docs/Tooltip.svelte +44 -0
- package/dist/components/docs/Tooltip.svelte.d.ts +10 -0
- package/dist/components/docs/VersionSwitcher.svelte +95 -0
- package/dist/components/docs/VersionSwitcher.svelte.d.ts +7 -0
- package/dist/components/docs/Video.svelte +84 -0
- package/dist/components/docs/Video.svelte.d.ts +12 -0
- package/dist/components/docs/api/ApiEndpoint.svelte +61 -0
- package/dist/components/docs/api/ApiEndpoint.svelte.d.ts +11 -0
- package/dist/components/docs/api/ApiParams.svelte +80 -0
- package/dist/components/docs/api/ApiParams.svelte.d.ts +14 -0
- package/dist/components/docs/api/ApiPlayground.svelte +259 -0
- package/dist/components/docs/api/ApiPlayground.svelte.d.ts +16 -0
- package/dist/components/docs/api/ApiReference.svelte +278 -0
- package/dist/components/docs/api/ApiReference.svelte.d.ts +23 -0
- package/dist/components/docs/api/ApiResponse.svelte +66 -0
- package/dist/components/docs/api/ApiResponse.svelte.d.ts +9 -0
- package/dist/components/docs/api/index.d.ts +5 -0
- package/dist/components/docs/api/index.js +5 -0
- package/dist/components/docs/componentTextProps.d.ts +3 -0
- package/dist/components/docs/componentTextProps.js +61 -0
- package/dist/components/docs/index.d.ts +54 -0
- package/dist/components/docs/index.js +56 -0
- package/dist/components/global/VersionNotFound.svelte +48 -0
- package/dist/components/global/VersionNotFound.svelte.d.ts +7 -0
- package/dist/components/global/index.d.ts +1 -0
- package/dist/components/global/index.js +1 -0
- package/dist/components/index.d.ts +6 -822
- package/dist/components/index.js +11 -3854
- package/dist/components/ui/Badge.svelte +48 -0
- package/dist/components/ui/Badge.svelte.d.ts +15 -0
- package/dist/components/ui/Button.svelte +58 -0
- package/dist/components/ui/Button.svelte.d.ts +17 -0
- package/dist/components/ui/Dialog.svelte +16 -0
- package/dist/components/ui/Dialog.svelte.d.ts +9 -0
- package/dist/components/ui/DialogClose.svelte +16 -0
- package/dist/components/ui/DialogClose.svelte.d.ts +9 -0
- package/dist/components/ui/DialogContent.svelte +43 -0
- package/dist/components/ui/DialogContent.svelte.d.ts +10 -0
- package/dist/components/ui/DialogDescription.svelte +21 -0
- package/dist/components/ui/DialogDescription.svelte.d.ts +9 -0
- package/dist/components/ui/DialogFooter.svelte +20 -0
- package/dist/components/ui/DialogFooter.svelte.d.ts +9 -0
- package/dist/components/ui/DialogHeader.svelte +20 -0
- package/dist/components/ui/DialogHeader.svelte.d.ts +9 -0
- package/dist/components/ui/DialogTitle.svelte +21 -0
- package/dist/components/ui/DialogTitle.svelte.d.ts +9 -0
- package/dist/components/ui/Input.svelte +23 -0
- package/dist/components/ui/Input.svelte.d.ts +8 -0
- package/dist/components/ui/Textarea.svelte +19 -0
- package/dist/components/ui/Textarea.svelte.d.ts +7 -0
- package/dist/components/ui/index.d.ts +11 -0
- package/dist/components/ui/index.js +11 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +9 -0
- package/dist/config.schema.json +471 -0
- package/dist/config.server.d.ts +46 -0
- package/dist/config.server.js +149 -0
- package/dist/{mdx-ColN3Cyg.d.mts → config.types.d.ts} +22 -75
- package/dist/config.types.js +39 -0
- package/dist/dev-utils.d.ts +29 -0
- package/dist/dev-utils.js +63 -0
- package/dist/index.d.ts +19 -4
- package/dist/index.js +25 -4861
- package/dist/mdx-cache.d.ts +41 -0
- package/dist/mdx-cache.js +160 -0
- package/dist/mdx-components.js +50 -1931
- package/dist/mdx-security.d.ts +76 -0
- package/dist/mdx-security.js +217 -0
- package/dist/mdx.d.ts +73 -0
- package/dist/mdx.js +1099 -0
- package/dist/middleware/index.d.ts +1 -0
- package/dist/middleware/index.js +2 -0
- package/dist/middleware/security.d.ts +22 -47
- package/dist/middleware/security.js +111 -137
- package/dist/parsers/base-parser.d.ts +14 -0
- package/dist/parsers/base-parser.js +1 -0
- package/dist/parsers/index.d.ts +16 -0
- package/dist/parsers/index.js +51 -0
- package/dist/parsers/openapi-parser.d.ts +18 -0
- package/dist/parsers/openapi-parser.js +209 -0
- package/dist/parsers/postman-parser.d.ts +20 -0
- package/dist/parsers/postman-parser.js +260 -0
- package/dist/parsers/specra-parser.d.ts +10 -0
- package/dist/parsers/specra-parser.js +18 -0
- package/dist/redirects.d.ts +12 -0
- package/dist/redirects.js +30 -0
- package/dist/remark-code-meta.d.ts +6 -0
- package/dist/remark-code-meta.js +21 -0
- package/dist/sidebar-utils.d.ts +59 -0
- package/dist/sidebar-utils.js +144 -0
- package/dist/stores/config.d.ts +20 -0
- package/dist/stores/config.js +45 -0
- package/dist/stores/index.d.ts +4 -0
- package/dist/stores/index.js +4 -0
- package/dist/stores/sidebar.d.ts +7 -0
- package/dist/stores/sidebar.js +12 -0
- package/dist/stores/tabs.d.ts +6 -0
- package/dist/stores/tabs.js +41 -0
- package/dist/stores/theme.d.ts +7 -0
- package/dist/stores/theme.js +75 -0
- package/dist/{styles.css → styles/globals.css} +136 -6
- package/dist/toc.d.ts +9 -0
- package/dist/toc.js +15 -0
- package/dist/utils.d.ts +13 -0
- package/dist/utils.js +30 -0
- package/package.json +47 -90
- package/dist/app/api/mdx-watch/route.d.mts +0 -10
- package/dist/app/api/mdx-watch/route.d.ts +0 -10
- package/dist/app/api/mdx-watch/route.js +0 -118
- package/dist/app/api/mdx-watch/route.js.map +0 -1
- package/dist/app/api/mdx-watch/route.mjs +0 -91
- package/dist/app/api/mdx-watch/route.mjs.map +0 -1
- package/dist/chunk-6S3EJVEO.mjs +0 -259
- package/dist/chunk-6S3EJVEO.mjs.map +0 -1
- package/dist/chunk-BE7EROIW.mjs +0 -212
- package/dist/chunk-BE7EROIW.mjs.map +0 -1
- package/dist/chunk-CWHRZHZO.mjs +0 -168
- package/dist/chunk-CWHRZHZO.mjs.map +0 -1
- package/dist/chunk-D5VDVYFY.mjs +0 -1325
- package/dist/chunk-D5VDVYFY.mjs.map +0 -1
- package/dist/chunk-WMCO2UX5.mjs +0 -585
- package/dist/chunk-WMCO2UX5.mjs.map +0 -1
- package/dist/chunk-XEMGCPZZ.mjs +0 -475
- package/dist/chunk-XEMGCPZZ.mjs.map +0 -1
- package/dist/components/index.d.mts +0 -822
- package/dist/components/index.js.map +0 -1
- package/dist/components/index.mjs +0 -3741
- package/dist/components/index.mjs.map +0 -1
- package/dist/index.d.mts +0 -4
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -1897
- package/dist/index.mjs.map +0 -1
- package/dist/layouts/index.d.mts +0 -34
- package/dist/layouts/index.d.ts +0 -34
- package/dist/layouts/index.js +0 -453
- package/dist/layouts/index.js.map +0 -1
- package/dist/layouts/index.mjs +0 -173
- package/dist/layouts/index.mjs.map +0 -1
- package/dist/lib/index.d.mts +0 -583
- package/dist/lib/index.d.ts +0 -583
- package/dist/lib/index.js +0 -1595
- package/dist/lib/index.js.map +0 -1
- package/dist/lib/index.mjs +0 -111
- package/dist/lib/index.mjs.map +0 -1
- package/dist/mdx-ColN3Cyg.d.ts +0 -352
- package/dist/mdx-components.d.mts +0 -86
- package/dist/mdx-components.d.ts +0 -86
- package/dist/mdx-components.js.map +0 -1
- package/dist/mdx-components.mjs +0 -206
- package/dist/mdx-components.mjs.map +0 -1
- package/dist/middleware/security.d.mts +0 -82
- package/dist/middleware/security.js.map +0 -1
- package/dist/middleware/security.mjs +0 -84
- package/dist/middleware/security.mjs.map +0 -1
- package/dist/styles.css.map +0 -1
- package/dist/styles.d.mts +0 -2
- package/dist/styles.d.ts +0 -2
- package/dist/styles.js +0 -2
- package/dist/styles.js.map +0 -1
- package/dist/styles.mjs +0 -1
- 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,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;
|