@lon-ask/dockit 0.1.1 → 0.1.2
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/README.md +482 -337
- package/SKILL.md +94 -103
- package/apps/client/index.html +12 -0
- package/apps/client/package.json +26 -0
- package/apps/client/src/App.tsx +18 -0
- package/apps/client/src/api/client.ts +54 -0
- package/apps/client/src/components/BuildPanel.tsx +77 -0
- package/apps/client/src/components/DocViewer.tsx +76 -0
- package/apps/client/src/components/EntryDetail.tsx +322 -0
- package/apps/client/src/components/EntryForm.tsx +117 -0
- package/apps/client/src/components/EntryList.tsx +165 -0
- package/apps/client/src/components/GlobalSearchBar.tsx +166 -0
- package/apps/client/src/components/Layout.tsx +57 -0
- package/apps/client/src/components/SearchBar.tsx +103 -0
- package/apps/client/src/components/SourceForm.tsx +497 -0
- package/apps/client/src/hooks/useTheme.ts +19 -0
- package/apps/client/src/index.css +77 -0
- package/apps/client/src/main.tsx +13 -0
- package/apps/client/src/types.ts +105 -0
- package/apps/client/vite.config.ts +13 -0
- package/apps/server/dist/core/domain/entry.js +20 -0
- package/apps/server/dist/core/domain/entry.js.map +1 -0
- package/apps/server/dist/core/domain/errors.js +33 -0
- package/apps/server/dist/core/domain/errors.js.map +1 -0
- package/apps/server/dist/core/domain/knowledge-graph.js +2 -0
- package/apps/server/dist/core/domain/knowledge-graph.js.map +1 -0
- package/apps/server/dist/core/domain/types.js +2 -0
- package/apps/server/dist/core/domain/types.js.map +1 -0
- package/apps/server/dist/core/ports/IBuildRepository.js +2 -0
- package/apps/server/dist/core/ports/IBuildRepository.js.map +1 -0
- package/apps/server/dist/core/ports/IDocumentNormalizer.js +2 -0
- package/apps/server/dist/core/ports/IDocumentNormalizer.js.map +1 -0
- package/apps/server/dist/core/ports/IDocumentStore.js +2 -0
- package/apps/server/dist/core/ports/IDocumentStore.js.map +1 -0
- package/apps/server/dist/core/ports/IEntryReadModel.js +2 -0
- package/apps/server/dist/core/ports/IEntryReadModel.js.map +1 -0
- package/apps/server/dist/core/ports/IEntryRepository.js +2 -0
- package/apps/server/dist/core/ports/IEntryRepository.js.map +1 -0
- package/apps/server/dist/core/ports/IKnowledgeGraph.js +2 -0
- package/apps/server/dist/core/ports/IKnowledgeGraph.js.map +1 -0
- package/apps/server/dist/core/ports/IPathResolver.js +2 -0
- package/apps/server/dist/core/ports/IPathResolver.js.map +1 -0
- package/apps/server/dist/core/ports/ISearchEngine.js +2 -0
- package/apps/server/dist/core/ports/ISearchEngine.js.map +1 -0
- package/apps/server/dist/core/ports/ISourceProcessor.js +2 -0
- package/apps/server/dist/core/ports/ISourceProcessor.js.map +1 -0
- package/apps/server/dist/core/ports/ISourceRepository.js +2 -0
- package/apps/server/dist/core/ports/ISourceRepository.js.map +1 -0
- package/apps/server/dist/core/usecases/BuildUseCase.js +76 -0
- package/apps/server/dist/core/usecases/BuildUseCase.js.map +1 -0
- package/apps/server/dist/core/usecases/ConfigUseCase.js +62 -0
- package/apps/server/dist/core/usecases/ConfigUseCase.js.map +1 -0
- package/apps/server/dist/core/usecases/SearchUseCase.js +17 -0
- package/apps/server/dist/core/usecases/SearchUseCase.js.map +1 -0
- package/apps/server/dist/index.js +86 -0
- package/apps/server/dist/index.js.map +1 -0
- package/apps/server/dist/infrastructure/filesystem/FileSystemDocumentStore.js +25 -0
- package/apps/server/dist/infrastructure/filesystem/FileSystemDocumentStore.js.map +1 -0
- package/apps/server/dist/infrastructure/graph/GraphSearchDecorator.js +42 -0
- package/apps/server/dist/infrastructure/graph/GraphSearchDecorator.js.map +1 -0
- package/apps/server/dist/infrastructure/graph/GraphifyKnowledgeGraph.js +145 -0
- package/apps/server/dist/infrastructure/graph/GraphifyKnowledgeGraph.js.map +1 -0
- package/apps/server/dist/infrastructure/graph/index.js +3 -0
- package/apps/server/dist/infrastructure/graph/index.js.map +1 -0
- package/apps/server/dist/infrastructure/persistence/sqlite/SqliteBuildRepository.js +21 -0
- package/apps/server/dist/infrastructure/persistence/sqlite/SqliteBuildRepository.js.map +1 -0
- package/apps/server/dist/infrastructure/persistence/sqlite/SqliteEntryReadModel.js +11 -0
- package/apps/server/dist/infrastructure/persistence/sqlite/SqliteEntryReadModel.js.map +1 -0
- package/apps/server/dist/infrastructure/persistence/sqlite/SqliteEntryRepository.js +59 -0
- package/apps/server/dist/infrastructure/persistence/sqlite/SqliteEntryRepository.js.map +1 -0
- package/apps/server/dist/infrastructure/persistence/sqlite/SqliteSourceRepository.js +47 -0
- package/apps/server/dist/infrastructure/persistence/sqlite/SqliteSourceRepository.js.map +1 -0
- package/apps/server/dist/infrastructure/persistence/sqlite/connection.js +50 -0
- package/apps/server/dist/infrastructure/persistence/sqlite/connection.js.map +1 -0
- package/apps/server/dist/infrastructure/search/SearchEngineFactory.js +32 -0
- package/apps/server/dist/infrastructure/search/SearchEngineFactory.js.map +1 -0
- package/apps/server/dist/infrastructure/search/json/JsonSearchEngine.js +147 -0
- package/apps/server/dist/infrastructure/search/json/JsonSearchEngine.js.map +1 -0
- package/apps/server/dist/infrastructure/search/vector/EmbeddingService.js +23 -0
- package/apps/server/dist/infrastructure/search/vector/EmbeddingService.js.map +1 -0
- package/apps/server/dist/infrastructure/search/vector/VectorSearchEngine.js +378 -0
- package/apps/server/dist/infrastructure/search/vector/VectorSearchEngine.js.map +1 -0
- package/apps/server/dist/infrastructure/source-processors/AntoraSourceProcessor.js +11 -0
- package/apps/server/dist/infrastructure/source-processors/AntoraSourceProcessor.js.map +1 -0
- package/apps/server/dist/infrastructure/source-processors/AsciidocSourceProcessor.js +9 -0
- package/apps/server/dist/infrastructure/source-processors/AsciidocSourceProcessor.js.map +1 -0
- package/apps/server/dist/infrastructure/source-processors/DocumentNormalizer.js +11 -0
- package/apps/server/dist/infrastructure/source-processors/DocumentNormalizer.js.map +1 -0
- package/apps/server/dist/infrastructure/source-processors/GithubMarkdownSourceProcessor.js +9 -0
- package/apps/server/dist/infrastructure/source-processors/GithubMarkdownSourceProcessor.js.map +1 -0
- package/apps/server/dist/infrastructure/source-processors/MavenSourceProcessor.js +9 -0
- package/apps/server/dist/infrastructure/source-processors/MavenSourceProcessor.js.map +1 -0
- package/apps/server/dist/infrastructure/source-processors/PathResolver.js +5 -0
- package/apps/server/dist/infrastructure/source-processors/PathResolver.js.map +1 -0
- package/apps/server/dist/infrastructure/source-processors/SourceCodeSourceProcessor.js +261 -0
- package/apps/server/dist/infrastructure/source-processors/SourceCodeSourceProcessor.js.map +1 -0
- package/apps/server/dist/infrastructure/source-processors/ZipSourceProcessor.js +9 -0
- package/apps/server/dist/infrastructure/source-processors/ZipSourceProcessor.js.map +1 -0
- package/apps/server/dist/mcp-http.js +93 -0
- package/apps/server/dist/mcp-http.js.map +1 -0
- package/apps/server/dist/mcp.js +339 -0
- package/apps/server/dist/mcp.js.map +1 -0
- package/apps/server/dist/routes/build.js +89 -0
- package/apps/server/dist/routes/build.js.map +1 -0
- package/apps/server/dist/routes/entries.js +52 -0
- package/apps/server/dist/routes/entries.js.map +1 -0
- package/apps/server/dist/routes/graph.js +58 -0
- package/apps/server/dist/routes/graph.js.map +1 -0
- package/apps/server/dist/routes/search.js +24 -0
- package/apps/server/dist/routes/search.js.map +1 -0
- package/apps/server/dist/routes/sources.js +100 -0
- package/apps/server/dist/routes/sources.js.map +1 -0
- package/apps/server/dist/routes/viewer.js +22 -0
- package/apps/server/dist/routes/viewer.js.map +1 -0
- package/apps/server/dist/services/antora.js +222 -0
- package/apps/server/dist/services/antora.js.map +1 -0
- package/apps/server/dist/services/asciidoc.js +206 -0
- package/apps/server/dist/services/asciidoc.js.map +1 -0
- package/apps/server/dist/services/configLoader.js +150 -0
- package/apps/server/dist/services/configLoader.js.map +1 -0
- package/apps/server/dist/services/githubMarkdown.js +221 -0
- package/apps/server/dist/services/githubMarkdown.js.map +1 -0
- package/apps/server/dist/services/maven.js +148 -0
- package/apps/server/dist/services/maven.js.map +1 -0
- package/apps/server/dist/services/normalizer.js +42 -0
- package/apps/server/dist/services/normalizer.js.map +1 -0
- package/apps/server/dist/services/paths.js +5 -0
- package/apps/server/dist/services/paths.js.map +1 -0
- package/apps/server/dist/services/textExtractor.js +46 -0
- package/apps/server/dist/services/textExtractor.js.map +1 -0
- package/apps/server/dist/services/zip.js +63 -0
- package/apps/server/dist/services/zip.js.map +1 -0
- package/apps/server/package.json +38 -0
- package/bin/commands/dev.ts +2 -2
- package/bin/commands/serve.ts +2 -2
- package/package.json +17 -3
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
import { Search, FileText, X } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
interface GlobalResult {
|
|
6
|
+
entryId: string;
|
|
7
|
+
entryName: string;
|
|
8
|
+
entryVersion: string;
|
|
9
|
+
path: string;
|
|
10
|
+
title: string;
|
|
11
|
+
headings: string[];
|
|
12
|
+
snippet: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function GlobalSearchBar() {
|
|
16
|
+
const navigate = useNavigate();
|
|
17
|
+
const [query, setQuery] = useState('');
|
|
18
|
+
const [results, setResults] = useState<GlobalResult[]>([]);
|
|
19
|
+
const [loading, setLoading] = useState(false);
|
|
20
|
+
const [open, setOpen] = useState(false);
|
|
21
|
+
const [focused, setFocused] = useState(false);
|
|
22
|
+
const [debounceTimer, setDebounceTimer] = useState<ReturnType<typeof setTimeout>>();
|
|
23
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
24
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
25
|
+
|
|
26
|
+
const doSearch = useCallback(async (q: string) => {
|
|
27
|
+
if (!q.trim()) {
|
|
28
|
+
setResults([]);
|
|
29
|
+
setOpen(false);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
setLoading(true);
|
|
33
|
+
try {
|
|
34
|
+
const data = await fetch(`/api/search?q=${encodeURIComponent(q)}`).then((r) => r.json());
|
|
35
|
+
setResults(data);
|
|
36
|
+
setOpen(true);
|
|
37
|
+
} catch {
|
|
38
|
+
// ignore
|
|
39
|
+
} finally {
|
|
40
|
+
setLoading(false);
|
|
41
|
+
}
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
const handleChange = (value: string) => {
|
|
45
|
+
setQuery(value);
|
|
46
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
47
|
+
const timer = setTimeout(() => doSearch(value), 300);
|
|
48
|
+
setDebounceTimer(timer);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const handler = (e: KeyboardEvent) => {
|
|
53
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
inputRef.current?.focus();
|
|
56
|
+
}
|
|
57
|
+
if (e.key === 'Escape') {
|
|
58
|
+
setOpen(false);
|
|
59
|
+
inputRef.current?.blur();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
window.addEventListener('keydown', handler);
|
|
63
|
+
return () => window.removeEventListener('keydown', handler);
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const handler = (e: MouseEvent) => {
|
|
68
|
+
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
69
|
+
setOpen(false);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
document.addEventListener('mousedown', handler);
|
|
73
|
+
return () => document.removeEventListener('mousedown', handler);
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const handleSelect = (result: GlobalResult) => {
|
|
77
|
+
setOpen(false);
|
|
78
|
+
setQuery('');
|
|
79
|
+
navigate(`/entries/${result.entryId}?doc=${encodeURIComponent(result.path)}`);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const groupedResults: Array<{ entryId: string; entryName: string; entryVersion: string; items: GlobalResult[] }> = [];
|
|
83
|
+
for (const r of results) {
|
|
84
|
+
let group = groupedResults.find((g) => g.entryId === r.entryId);
|
|
85
|
+
if (!group) {
|
|
86
|
+
group = { entryId: r.entryId, entryName: r.entryName, entryVersion: r.entryVersion, items: [] };
|
|
87
|
+
groupedResults.push(group);
|
|
88
|
+
}
|
|
89
|
+
group.items.push(r);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div ref={containerRef} className="relative flex-1 max-w-xl">
|
|
94
|
+
<div className="relative">
|
|
95
|
+
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted" />
|
|
96
|
+
<input
|
|
97
|
+
ref={inputRef}
|
|
98
|
+
type="text"
|
|
99
|
+
value={query}
|
|
100
|
+
onChange={(e) => handleChange(e.target.value)}
|
|
101
|
+
onFocus={() => { setFocused(true); if (results.length > 0) setOpen(true); }}
|
|
102
|
+
placeholder="Search all docs..."
|
|
103
|
+
className="w-full pl-9 pr-16 py-1.5 bg-surface ring-1 ring-border rounded-md text-sm text-text placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-all"
|
|
104
|
+
/>
|
|
105
|
+
{loading && (
|
|
106
|
+
<div className="absolute right-12 top-1/2 -translate-y-1/2">
|
|
107
|
+
<div className="w-3.5 h-3.5 border-2 border-border border-t-primary rounded-full animate-spin" />
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
{query && (
|
|
111
|
+
<button
|
|
112
|
+
onClick={() => { setQuery(''); setResults([]); setOpen(false); }}
|
|
113
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 p-0.5 rounded text-text-muted hover:text-text transition-colors"
|
|
114
|
+
>
|
|
115
|
+
<X size={13} />
|
|
116
|
+
</button>
|
|
117
|
+
)}
|
|
118
|
+
{!query && (
|
|
119
|
+
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
|
120
|
+
<kbd className="hidden sm:inline-flex items-center px-1.5 py-0.5 text-[10px] font-mono text-text-muted bg-bg-alt ring-1 ring-border rounded">
|
|
121
|
+
⌘K
|
|
122
|
+
</kbd>
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{open && groupedResults.length > 0 && (
|
|
128
|
+
<>
|
|
129
|
+
<div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
|
|
130
|
+
<div className="absolute left-0 right-0 z-50 top-full mt-1.5 bg-surface ring-1 ring-border rounded-xl shadow-xl max-h-96 overflow-auto">
|
|
131
|
+
{groupedResults.map((group) => (
|
|
132
|
+
<div key={group.entryId}>
|
|
133
|
+
<div className="px-3 py-1.5 text-[11px] font-semibold text-text-muted uppercase tracking-wider bg-bg-alt/50 border-b border-border">
|
|
134
|
+
{group.entryName} <span className="font-normal lowercase text-text-dim">({group.entryVersion})</span>
|
|
135
|
+
</div>
|
|
136
|
+
{group.items.map((r, i) => (
|
|
137
|
+
<button
|
|
138
|
+
key={`${group.entryId}-${i}`}
|
|
139
|
+
type="button"
|
|
140
|
+
onClick={() => handleSelect(r)}
|
|
141
|
+
className="flex items-start gap-2.5 px-3 py-2.5 hover:bg-bg-alt transition-colors border-b border-border last:border-b-0 w-full text-left cursor-pointer"
|
|
142
|
+
>
|
|
143
|
+
<FileText size={14} className="text-text-muted mt-0.5 shrink-0" />
|
|
144
|
+
<div className="min-w-0 flex-1">
|
|
145
|
+
<div className="text-sm font-medium text-text truncate">{r.title}</div>
|
|
146
|
+
<div className="text-xs text-text-dim mt-0.5 line-clamp-2">{r.snippet}</div>
|
|
147
|
+
</div>
|
|
148
|
+
</button>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
))}
|
|
152
|
+
</div>
|
|
153
|
+
</>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
{open && query && !loading && results.length === 0 && (
|
|
157
|
+
<>
|
|
158
|
+
<div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
|
|
159
|
+
<div className="absolute left-0 right-0 z-50 top-full mt-1.5 bg-surface ring-1 ring-border rounded-xl shadow-xl p-4 text-center text-sm text-text-dim">
|
|
160
|
+
No results found for “{query}”
|
|
161
|
+
</div>
|
|
162
|
+
</>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Link, useLocation } from 'react-router-dom';
|
|
2
|
+
import { BookOpen, Sun, Moon, Plus } from 'lucide-react';
|
|
3
|
+
import { useTheme } from '../hooks/useTheme';
|
|
4
|
+
import GlobalSearchBar from './GlobalSearchBar';
|
|
5
|
+
|
|
6
|
+
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
7
|
+
const location = useLocation();
|
|
8
|
+
const { theme, toggleTheme } = useTheme();
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="flex flex-col h-screen overflow-hidden">
|
|
12
|
+
<header className="h-12 bg-bg-alt border-b border-border flex items-center px-4 gap-4 shrink-0">
|
|
13
|
+
<Link to="/" className="flex items-center gap-2 no-underline shrink-0">
|
|
14
|
+
<BookOpen size={18} className="text-primary" />
|
|
15
|
+
<span className="font-semibold text-sm tracking-tight text-text">Dockit</span>
|
|
16
|
+
</Link>
|
|
17
|
+
|
|
18
|
+
<nav className="flex items-center gap-1">
|
|
19
|
+
<Link
|
|
20
|
+
to="/"
|
|
21
|
+
className={`px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
|
22
|
+
location.pathname === '/'
|
|
23
|
+
? 'bg-primary/10 text-primary'
|
|
24
|
+
: 'text-text-dim hover:text-text'
|
|
25
|
+
}`}
|
|
26
|
+
>
|
|
27
|
+
Entries
|
|
28
|
+
</Link>
|
|
29
|
+
</nav>
|
|
30
|
+
|
|
31
|
+
<GlobalSearchBar />
|
|
32
|
+
|
|
33
|
+
<div className="flex-1" />
|
|
34
|
+
|
|
35
|
+
<Link
|
|
36
|
+
to="/entries/new"
|
|
37
|
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm font-medium bg-primary text-white hover:bg-primary-hover transition-colors"
|
|
38
|
+
>
|
|
39
|
+
<Plus size={14} />
|
|
40
|
+
New
|
|
41
|
+
</Link>
|
|
42
|
+
|
|
43
|
+
<button
|
|
44
|
+
onClick={toggleTheme}
|
|
45
|
+
className="p-1.5 rounded-md text-text-dim hover:text-text hover:bg-surface transition-colors"
|
|
46
|
+
title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
|
|
47
|
+
>
|
|
48
|
+
{theme === 'dark' ? <Sun size={16} /> : <Moon size={16} />}
|
|
49
|
+
</button>
|
|
50
|
+
</header>
|
|
51
|
+
|
|
52
|
+
<main className="flex-1 overflow-auto">
|
|
53
|
+
{children}
|
|
54
|
+
</main>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import { Search, FileText, Filter } from 'lucide-react';
|
|
3
|
+
import type { SearchResult } from '../types';
|
|
4
|
+
import { api } from '../api/client';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
entryId: string;
|
|
8
|
+
onSelectFile: (path: string) => void;
|
|
9
|
+
scopeLabel?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function SearchBar({ entryId, onSelectFile, scopeLabel }: Props) {
|
|
13
|
+
const [query, setQuery] = useState('');
|
|
14
|
+
const [results, setResults] = useState<SearchResult[]>([]);
|
|
15
|
+
const [loading, setLoading] = useState(false);
|
|
16
|
+
const [open, setOpen] = useState(false);
|
|
17
|
+
const [debounceTimer, setDebounceTimer] = useState<ReturnType<typeof setTimeout>>();
|
|
18
|
+
|
|
19
|
+
const doSearch = useCallback(async (q: string) => {
|
|
20
|
+
if (!q.trim()) {
|
|
21
|
+
setResults([]);
|
|
22
|
+
setOpen(false);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
setLoading(true);
|
|
26
|
+
try {
|
|
27
|
+
const data = await api.search(entryId, q);
|
|
28
|
+
setResults(data);
|
|
29
|
+
setOpen(true);
|
|
30
|
+
} catch {
|
|
31
|
+
// ignore
|
|
32
|
+
} finally {
|
|
33
|
+
setLoading(false);
|
|
34
|
+
}
|
|
35
|
+
}, [entryId]);
|
|
36
|
+
|
|
37
|
+
const handleChange = (value: string) => {
|
|
38
|
+
setQuery(value);
|
|
39
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
40
|
+
const timer = setTimeout(() => doSearch(value), 300);
|
|
41
|
+
setDebounceTimer(timer);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="relative">
|
|
46
|
+
<div className="flex items-center gap-2">
|
|
47
|
+
{scopeLabel && (
|
|
48
|
+
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-md text-[11px] font-medium text-text-muted bg-bg-alt ring-1 ring-border shrink-0">
|
|
49
|
+
<Filter size={10} />
|
|
50
|
+
{scopeLabel}
|
|
51
|
+
</span>
|
|
52
|
+
)}
|
|
53
|
+
<div className="relative flex-1">
|
|
54
|
+
<Search size={16} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-text-muted" />
|
|
55
|
+
<input
|
|
56
|
+
type="text"
|
|
57
|
+
value={query}
|
|
58
|
+
onChange={(e) => handleChange(e.target.value)}
|
|
59
|
+
onFocus={() => results.length > 0 && setOpen(true)}
|
|
60
|
+
placeholder="Search documentation..."
|
|
61
|
+
className="w-full pl-10 pr-3 py-2.5 bg-surface ring-1 ring-border rounded-lg text-sm text-text placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-all"
|
|
62
|
+
/>
|
|
63
|
+
{loading && (
|
|
64
|
+
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
|
65
|
+
<div className="w-4 h-4 border-2 border-border border-t-primary rounded-full animate-spin" />
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{open && results.length > 0 && (
|
|
72
|
+
<>
|
|
73
|
+
<div className="fixed inset-0 z-10" onClick={() => setOpen(false)} />
|
|
74
|
+
<div className="absolute z-20 top-full mt-2 w-full bg-surface ring-1 ring-border rounded-xl shadow-lg max-h-80 overflow-auto">
|
|
75
|
+
{results.map((r, i) => (
|
|
76
|
+
<button
|
|
77
|
+
key={i}
|
|
78
|
+
type="button"
|
|
79
|
+
onClick={() => { onSelectFile(r.path); setOpen(false); }}
|
|
80
|
+
className="flex items-start gap-3 px-4 py-3 hover:bg-bg-alt transition-colors border-b border-border last:border-b-0 w-full text-left cursor-pointer"
|
|
81
|
+
>
|
|
82
|
+
<FileText size={16} className="text-text-muted mt-0.5 shrink-0" />
|
|
83
|
+
<div className="min-w-0 flex-1">
|
|
84
|
+
<div className="text-sm font-medium text-text truncate">{r.title}</div>
|
|
85
|
+
<div className="text-xs text-text-dim mt-0.5 line-clamp-2">{r.snippet}</div>
|
|
86
|
+
</div>
|
|
87
|
+
</button>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
</>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{open && query && !loading && results.length === 0 && (
|
|
94
|
+
<>
|
|
95
|
+
<div className="fixed inset-0 z-10" onClick={() => setOpen(false)} />
|
|
96
|
+
<div className="absolute z-20 top-full mt-2 w-full bg-surface ring-1 ring-border rounded-xl shadow-lg p-4 text-center text-sm text-text-dim">
|
|
97
|
+
No results found for “{query}”
|
|
98
|
+
</div>
|
|
99
|
+
</>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|