@lon-ask/dockit 0.1.0 → 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.
Files changed (137) hide show
  1. package/README.md +482 -337
  2. package/SKILL.md +94 -103
  3. package/apps/client/index.html +12 -0
  4. package/apps/client/package.json +26 -0
  5. package/apps/client/src/App.tsx +18 -0
  6. package/apps/client/src/api/client.ts +54 -0
  7. package/apps/client/src/components/BuildPanel.tsx +77 -0
  8. package/apps/client/src/components/DocViewer.tsx +76 -0
  9. package/apps/client/src/components/EntryDetail.tsx +322 -0
  10. package/apps/client/src/components/EntryForm.tsx +117 -0
  11. package/apps/client/src/components/EntryList.tsx +165 -0
  12. package/apps/client/src/components/GlobalSearchBar.tsx +166 -0
  13. package/apps/client/src/components/Layout.tsx +57 -0
  14. package/apps/client/src/components/SearchBar.tsx +103 -0
  15. package/apps/client/src/components/SourceForm.tsx +497 -0
  16. package/apps/client/src/hooks/useTheme.ts +19 -0
  17. package/apps/client/src/index.css +77 -0
  18. package/apps/client/src/main.tsx +13 -0
  19. package/apps/client/src/types.ts +105 -0
  20. package/apps/client/vite.config.ts +13 -0
  21. package/apps/server/dist/core/domain/entry.js +20 -0
  22. package/apps/server/dist/core/domain/entry.js.map +1 -0
  23. package/apps/server/dist/core/domain/errors.js +33 -0
  24. package/apps/server/dist/core/domain/errors.js.map +1 -0
  25. package/apps/server/dist/core/domain/knowledge-graph.js +2 -0
  26. package/apps/server/dist/core/domain/knowledge-graph.js.map +1 -0
  27. package/apps/server/dist/core/domain/types.js +2 -0
  28. package/apps/server/dist/core/domain/types.js.map +1 -0
  29. package/apps/server/dist/core/ports/IBuildRepository.js +2 -0
  30. package/apps/server/dist/core/ports/IBuildRepository.js.map +1 -0
  31. package/apps/server/dist/core/ports/IDocumentNormalizer.js +2 -0
  32. package/apps/server/dist/core/ports/IDocumentNormalizer.js.map +1 -0
  33. package/apps/server/dist/core/ports/IDocumentStore.js +2 -0
  34. package/apps/server/dist/core/ports/IDocumentStore.js.map +1 -0
  35. package/apps/server/dist/core/ports/IEntryReadModel.js +2 -0
  36. package/apps/server/dist/core/ports/IEntryReadModel.js.map +1 -0
  37. package/apps/server/dist/core/ports/IEntryRepository.js +2 -0
  38. package/apps/server/dist/core/ports/IEntryRepository.js.map +1 -0
  39. package/apps/server/dist/core/ports/IKnowledgeGraph.js +2 -0
  40. package/apps/server/dist/core/ports/IKnowledgeGraph.js.map +1 -0
  41. package/apps/server/dist/core/ports/IPathResolver.js +2 -0
  42. package/apps/server/dist/core/ports/IPathResolver.js.map +1 -0
  43. package/apps/server/dist/core/ports/ISearchEngine.js +2 -0
  44. package/apps/server/dist/core/ports/ISearchEngine.js.map +1 -0
  45. package/apps/server/dist/core/ports/ISourceProcessor.js +2 -0
  46. package/apps/server/dist/core/ports/ISourceProcessor.js.map +1 -0
  47. package/apps/server/dist/core/ports/ISourceRepository.js +2 -0
  48. package/apps/server/dist/core/ports/ISourceRepository.js.map +1 -0
  49. package/apps/server/dist/core/usecases/BuildUseCase.js +76 -0
  50. package/apps/server/dist/core/usecases/BuildUseCase.js.map +1 -0
  51. package/apps/server/dist/core/usecases/ConfigUseCase.js +62 -0
  52. package/apps/server/dist/core/usecases/ConfigUseCase.js.map +1 -0
  53. package/apps/server/dist/core/usecases/SearchUseCase.js +17 -0
  54. package/apps/server/dist/core/usecases/SearchUseCase.js.map +1 -0
  55. package/apps/server/dist/index.js +86 -0
  56. package/apps/server/dist/index.js.map +1 -0
  57. package/apps/server/dist/infrastructure/filesystem/FileSystemDocumentStore.js +25 -0
  58. package/apps/server/dist/infrastructure/filesystem/FileSystemDocumentStore.js.map +1 -0
  59. package/apps/server/dist/infrastructure/graph/GraphSearchDecorator.js +42 -0
  60. package/apps/server/dist/infrastructure/graph/GraphSearchDecorator.js.map +1 -0
  61. package/apps/server/dist/infrastructure/graph/GraphifyKnowledgeGraph.js +145 -0
  62. package/apps/server/dist/infrastructure/graph/GraphifyKnowledgeGraph.js.map +1 -0
  63. package/apps/server/dist/infrastructure/graph/index.js +3 -0
  64. package/apps/server/dist/infrastructure/graph/index.js.map +1 -0
  65. package/apps/server/dist/infrastructure/persistence/sqlite/SqliteBuildRepository.js +21 -0
  66. package/apps/server/dist/infrastructure/persistence/sqlite/SqliteBuildRepository.js.map +1 -0
  67. package/apps/server/dist/infrastructure/persistence/sqlite/SqliteEntryReadModel.js +11 -0
  68. package/apps/server/dist/infrastructure/persistence/sqlite/SqliteEntryReadModel.js.map +1 -0
  69. package/apps/server/dist/infrastructure/persistence/sqlite/SqliteEntryRepository.js +59 -0
  70. package/apps/server/dist/infrastructure/persistence/sqlite/SqliteEntryRepository.js.map +1 -0
  71. package/apps/server/dist/infrastructure/persistence/sqlite/SqliteSourceRepository.js +47 -0
  72. package/apps/server/dist/infrastructure/persistence/sqlite/SqliteSourceRepository.js.map +1 -0
  73. package/apps/server/dist/infrastructure/persistence/sqlite/connection.js +50 -0
  74. package/apps/server/dist/infrastructure/persistence/sqlite/connection.js.map +1 -0
  75. package/apps/server/dist/infrastructure/search/SearchEngineFactory.js +32 -0
  76. package/apps/server/dist/infrastructure/search/SearchEngineFactory.js.map +1 -0
  77. package/apps/server/dist/infrastructure/search/json/JsonSearchEngine.js +147 -0
  78. package/apps/server/dist/infrastructure/search/json/JsonSearchEngine.js.map +1 -0
  79. package/apps/server/dist/infrastructure/search/vector/EmbeddingService.js +23 -0
  80. package/apps/server/dist/infrastructure/search/vector/EmbeddingService.js.map +1 -0
  81. package/apps/server/dist/infrastructure/search/vector/VectorSearchEngine.js +378 -0
  82. package/apps/server/dist/infrastructure/search/vector/VectorSearchEngine.js.map +1 -0
  83. package/apps/server/dist/infrastructure/source-processors/AntoraSourceProcessor.js +11 -0
  84. package/apps/server/dist/infrastructure/source-processors/AntoraSourceProcessor.js.map +1 -0
  85. package/apps/server/dist/infrastructure/source-processors/AsciidocSourceProcessor.js +9 -0
  86. package/apps/server/dist/infrastructure/source-processors/AsciidocSourceProcessor.js.map +1 -0
  87. package/apps/server/dist/infrastructure/source-processors/DocumentNormalizer.js +11 -0
  88. package/apps/server/dist/infrastructure/source-processors/DocumentNormalizer.js.map +1 -0
  89. package/apps/server/dist/infrastructure/source-processors/GithubMarkdownSourceProcessor.js +9 -0
  90. package/apps/server/dist/infrastructure/source-processors/GithubMarkdownSourceProcessor.js.map +1 -0
  91. package/apps/server/dist/infrastructure/source-processors/MavenSourceProcessor.js +9 -0
  92. package/apps/server/dist/infrastructure/source-processors/MavenSourceProcessor.js.map +1 -0
  93. package/apps/server/dist/infrastructure/source-processors/PathResolver.js +5 -0
  94. package/apps/server/dist/infrastructure/source-processors/PathResolver.js.map +1 -0
  95. package/apps/server/dist/infrastructure/source-processors/SourceCodeSourceProcessor.js +261 -0
  96. package/apps/server/dist/infrastructure/source-processors/SourceCodeSourceProcessor.js.map +1 -0
  97. package/apps/server/dist/infrastructure/source-processors/ZipSourceProcessor.js +9 -0
  98. package/apps/server/dist/infrastructure/source-processors/ZipSourceProcessor.js.map +1 -0
  99. package/apps/server/dist/mcp-http.js +93 -0
  100. package/apps/server/dist/mcp-http.js.map +1 -0
  101. package/apps/server/dist/mcp.js +339 -0
  102. package/apps/server/dist/mcp.js.map +1 -0
  103. package/apps/server/dist/routes/build.js +89 -0
  104. package/apps/server/dist/routes/build.js.map +1 -0
  105. package/apps/server/dist/routes/entries.js +52 -0
  106. package/apps/server/dist/routes/entries.js.map +1 -0
  107. package/apps/server/dist/routes/graph.js +58 -0
  108. package/apps/server/dist/routes/graph.js.map +1 -0
  109. package/apps/server/dist/routes/search.js +24 -0
  110. package/apps/server/dist/routes/search.js.map +1 -0
  111. package/apps/server/dist/routes/sources.js +100 -0
  112. package/apps/server/dist/routes/sources.js.map +1 -0
  113. package/apps/server/dist/routes/viewer.js +22 -0
  114. package/apps/server/dist/routes/viewer.js.map +1 -0
  115. package/apps/server/dist/services/antora.js +222 -0
  116. package/apps/server/dist/services/antora.js.map +1 -0
  117. package/apps/server/dist/services/asciidoc.js +206 -0
  118. package/apps/server/dist/services/asciidoc.js.map +1 -0
  119. package/apps/server/dist/services/configLoader.js +150 -0
  120. package/apps/server/dist/services/configLoader.js.map +1 -0
  121. package/apps/server/dist/services/githubMarkdown.js +221 -0
  122. package/apps/server/dist/services/githubMarkdown.js.map +1 -0
  123. package/apps/server/dist/services/maven.js +148 -0
  124. package/apps/server/dist/services/maven.js.map +1 -0
  125. package/apps/server/dist/services/normalizer.js +42 -0
  126. package/apps/server/dist/services/normalizer.js.map +1 -0
  127. package/apps/server/dist/services/paths.js +5 -0
  128. package/apps/server/dist/services/paths.js.map +1 -0
  129. package/apps/server/dist/services/textExtractor.js +46 -0
  130. package/apps/server/dist/services/textExtractor.js.map +1 -0
  131. package/apps/server/dist/services/zip.js +63 -0
  132. package/apps/server/dist/services/zip.js.map +1 -0
  133. package/apps/server/package.json +38 -0
  134. package/apps/server/src/infrastructure/search/vector/EmbeddingService.ts +1 -1
  135. package/bin/commands/dev.ts +2 -2
  136. package/bin/commands/serve.ts +2 -2
  137. package/package.json +18 -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 &ldquo;{query}&rdquo;
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 &ldquo;{query}&rdquo;
98
+ </div>
99
+ </>
100
+ )}
101
+ </div>
102
+ );
103
+ }