@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,322 @@
1
+ import { useEffect, useState, useCallback } from 'react';
2
+ import { useParams, useNavigate, Link, useSearchParams } from 'react-router-dom';
3
+ import {
4
+ ArrowLeft, Pencil, Trash2, Play, Download, Plus,
5
+ Package, GitBranch, FileArchive, FileText, Github, Loader2,
6
+ CheckCircle, AlertCircle, Clock, MoreHorizontal, FileCode, Network,
7
+ } from 'lucide-react';
8
+ import type { EntryDetail as EntryDetailType, Source, SourceType, SourceConfig } from '../types';
9
+ import { api } from '../api/client';
10
+ import SourceForm from './SourceForm';
11
+ import BuildPanel from './BuildPanel';
12
+ import SearchBar from './SearchBar';
13
+ import DocViewer from './DocViewer';
14
+
15
+ const TYPE_ICONS: Record<SourceType, typeof Package> = {
16
+ zip: FileArchive,
17
+ antora: GitBranch,
18
+ maven: Package,
19
+ asciidoc: FileText,
20
+ 'github-markdown': Github,
21
+ 'source-code': FileCode,
22
+ };
23
+
24
+ const TYPE_LABELS: Record<SourceType, string> = {
25
+ zip: 'ZIP',
26
+ antora: 'Antora',
27
+ maven: 'Maven',
28
+ asciidoc: 'AsciiDoc',
29
+ 'github-markdown': 'GitHub Markdown',
30
+ 'source-code': 'Source Code',
31
+ };
32
+
33
+ const statusConfig: Record<string, { icon: typeof CheckCircle; color: string; bg: string; label: string }> = {
34
+ pending: { icon: Clock, color: 'text-text-muted', bg: 'bg-bg-alt', label: 'Pending' },
35
+ building: { icon: Loader2, color: 'text-warning', bg: 'bg-warning/5', label: 'Building' },
36
+ ready: { icon: CheckCircle, color: 'text-success', bg: 'bg-success/5', label: 'Ready' },
37
+ error: { icon: AlertCircle, color: 'text-danger', bg: 'bg-danger/5', label: 'Error' },
38
+ };
39
+
40
+ export default function EntryDetail() {
41
+ const { id } = useParams<{ id: string }>();
42
+ const navigate = useNavigate();
43
+ const [searchParams] = useSearchParams();
44
+
45
+ const [entry, setEntry] = useState<EntryDetailType | null>(null);
46
+ const [loading, setLoading] = useState(true);
47
+ const [error, setError] = useState<string | null>(null);
48
+ const [sourceFormOpen, setSourceFormOpen] = useState(false);
49
+ const [editingSource, setEditingSource] = useState<Source | null>(null);
50
+ const [formRevision, setFormRevision] = useState(0);
51
+ const [buildKey, setBuildKey] = useState(0);
52
+ const [sourceMenuOpen, setSourceMenuOpen] = useState<string | null>(null);
53
+ const [selectedFile, setSelectedFile] = useState<string | undefined>(undefined);
54
+
55
+ const fetchEntry = useCallback(async () => {
56
+ if (!id) return;
57
+ try {
58
+ const data = await api.entries.get(id);
59
+ setEntry(data);
60
+ setError(null);
61
+ } catch (err) {
62
+ setError((err as Error).message);
63
+ } finally {
64
+ setLoading(false);
65
+ }
66
+ }, [id]);
67
+
68
+ useEffect(() => { fetchEntry(); }, [fetchEntry]);
69
+
70
+ useEffect(() => {
71
+ const doc = searchParams.get('doc');
72
+ if (doc) {
73
+ setSelectedFile(doc);
74
+ }
75
+ }, [searchParams]);
76
+
77
+ const handleDeleteEntry = async () => {
78
+ if (!entry) return;
79
+ if (!confirm(`Delete entry "${entry.name}" and all its data permanently?`)) return;
80
+ try {
81
+ await api.entries.delete(entry.id);
82
+ navigate('/');
83
+ } catch (err) {
84
+ alert(`Failed to delete: ${(err as Error).message}`);
85
+ }
86
+ };
87
+
88
+ const handleAddSource = async (data: { type: SourceType; label: string; config: SourceConfig }) => {
89
+ if (!entry) return;
90
+ await api.sources.create(entry.id, data);
91
+ await fetchEntry();
92
+ };
93
+
94
+ const handleEditSource = async (data: { type: SourceType; label: string; config: SourceConfig }) => {
95
+ if (!editingSource) return;
96
+ await api.sources.update(editingSource.id, { label: data.label, config: data.config });
97
+ setEditingSource(null);
98
+ await fetchEntry();
99
+ };
100
+
101
+ const handleDeleteSource = async (source: Source) => {
102
+ if (!confirm(`Remove source "${source.label}"?`)) return;
103
+ await api.sources.delete(source.id);
104
+ await fetchEntry();
105
+ };
106
+
107
+ const handleBuild = async () => {
108
+ if (!entry) return;
109
+ try {
110
+ await api.build.trigger(entry.id);
111
+ setBuildKey((k) => k + 1);
112
+ await fetchEntry();
113
+ } catch (err) {
114
+ alert(`Build failed: ${(err as Error).message}`);
115
+ }
116
+ };
117
+
118
+ const handleDownloadScript = async () => {
119
+ if (!entry) return;
120
+ try {
121
+ const script = await api.build.cliScript(entry.id);
122
+ const blob = new Blob([script], { type: 'text/plain' });
123
+ const url = URL.createObjectURL(blob);
124
+ const a = document.createElement('a');
125
+ a.href = url;
126
+ const slug = entry.name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
127
+ a.download = `dockit-build-${slug}.sh`;
128
+ a.click();
129
+ URL.revokeObjectURL(url);
130
+ } catch (err) {
131
+ alert(`Failed to generate script: ${(err as Error).message}`);
132
+ }
133
+ };
134
+
135
+ if (loading) {
136
+ return (
137
+ <div className="flex items-center justify-center h-full">
138
+ <Loader2 size={24} className="animate-spin text-text-muted" />
139
+ </div>
140
+ );
141
+ }
142
+
143
+ if (error || !entry) {
144
+ return (
145
+ <div className="flex flex-col items-center justify-center h-full gap-3">
146
+ <AlertCircle size={32} className="text-danger" />
147
+ <p className="text-sm text-danger">{error || 'Entry not found'}</p>
148
+ <Link to="/" className="text-sm text-primary hover:underline">Back to entries</Link>
149
+ </div>
150
+ );
151
+ }
152
+
153
+ const status = statusConfig[entry.status] || statusConfig.pending;
154
+ const StatusIcon = status.icon;
155
+
156
+ return (
157
+ <div className="flex h-full">
158
+ {/* Left panel: config */}
159
+ <div className="w-[420px] shrink-0 border-r border-border overflow-auto p-5">
160
+ {/* Header */}
161
+ <div className="flex items-center gap-2 mb-4">
162
+ <button
163
+ onClick={() => navigate('/')}
164
+ className="p-1.5 rounded-md text-text-dim hover:text-text hover:bg-bg-alt transition-colors"
165
+ >
166
+ <ArrowLeft size={16} />
167
+ </button>
168
+ <div className="flex-1 min-w-0">
169
+ <h1 className="text-lg font-semibold text-text truncate">{entry.name}</h1>
170
+ </div>
171
+ <Link
172
+ to={`/entries/${entry.id}/edit`}
173
+ className="p-1.5 rounded-md text-text-dim hover:text-text hover:bg-bg-alt transition-colors"
174
+ title="Edit"
175
+ >
176
+ <Pencil size={14} />
177
+ </Link>
178
+ <button
179
+ onClick={handleDeleteEntry}
180
+ className="p-1.5 rounded-md text-text-dim hover:text-danger hover:bg-danger/5 transition-colors"
181
+ title="Delete"
182
+ >
183
+ <Trash2 size={14} />
184
+ </button>
185
+ </div>
186
+
187
+ {/* Meta */}
188
+ <div className="flex items-center gap-3 mb-4">
189
+ <span className="text-xs text-text-dim font-mono bg-bg-alt px-2 py-0.5 rounded">{entry.version}</span>
190
+ <span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${status.color} ${status.bg}`}>
191
+ <StatusIcon size={11} className={entry.status === 'building' ? 'animate-spin' : ''} />
192
+ {status.label}
193
+ </span>
194
+ </div>
195
+
196
+ {entry.description && (
197
+ <p className="text-sm text-text-dim mb-4 leading-relaxed">{entry.description}</p>
198
+ )}
199
+
200
+ {/* Actions */}
201
+ <div className="flex gap-2 mb-5">
202
+ <button
203
+ onClick={handleBuild}
204
+ disabled={entry.status === 'building'}
205
+ className="flex-1 flex items-center justify-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium bg-primary text-white hover:bg-primary-hover transition-colors disabled:opacity-50"
206
+ >
207
+ <Play size={14} />
208
+ Build
209
+ </button>
210
+ <button
211
+ onClick={handleDownloadScript}
212
+ className="flex items-center justify-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium ring-1 ring-border text-text-dim hover:bg-bg-alt hover:text-text transition-colors"
213
+ title="Download CLI script"
214
+ >
215
+ <Download size={14} />
216
+ </button>
217
+ </div>
218
+
219
+ {/* Sources */}
220
+ <div className="mb-4">
221
+ <div className="flex items-center justify-between mb-2.5">
222
+ <h3 className="text-xs font-semibold text-text-dim uppercase tracking-wider">
223
+ Sources ({entry.sources.length})
224
+ </h3>
225
+ <button
226
+ onClick={() => { setEditingSource(null); setFormRevision(r => r + 1); setSourceFormOpen(true); }}
227
+ className="flex items-center gap-1 px-2 py-1 rounded-md text-xs font-medium text-primary hover:bg-primary/10 transition-colors"
228
+ >
229
+ <Plus size={12} />
230
+ Add
231
+ </button>
232
+ </div>
233
+
234
+ {entry.sources.length === 0 ? (
235
+ <div className="bg-bg-alt ring-1 ring-border rounded-lg p-4 text-center">
236
+ <p className="text-xs text-text-muted">No sources yet</p>
237
+ </div>
238
+ ) : (
239
+ <div className="space-y-1.5">
240
+ {entry.sources.map((source) => {
241
+ const Icon = TYPE_ICONS[source.type];
242
+ return (
243
+ <div key={source.id} className="flex items-center gap-2.5 bg-surface ring-1 ring-border rounded-lg px-3 py-2 group/src">
244
+ <Icon size={14} className="text-text-muted shrink-0" />
245
+ <div className="flex-1 min-w-0">
246
+ <p className="text-sm font-medium text-text truncate">{source.label}</p>
247
+ <p className="text-[11px] text-text-muted font-mono truncate">{getConfigSummary(source)}</p>
248
+ </div>
249
+ <span className="text-[10px] text-text-muted uppercase tracking-wider shrink-0">{TYPE_LABELS[source.type]}</span>
250
+ {Boolean((source.config as Record<string, unknown>)?.graphifyEnabled) && (
251
+ <Network size={11} className="text-accent shrink-0" aria-label="Knowledge graph enabled" />
252
+ )}
253
+ <div className="relative">
254
+ <button
255
+ onClick={() => setSourceMenuOpen(sourceMenuOpen === source.id ? null : source.id)}
256
+ className="p-1 rounded text-text-muted hover:text-text opacity-0 group-hover/src:opacity-100 transition-all"
257
+ >
258
+ <MoreHorizontal size={13} />
259
+ </button>
260
+ {sourceMenuOpen === source.id && (
261
+ <>
262
+ <div className="fixed inset-0 z-10" onClick={() => setSourceMenuOpen(null)} />
263
+ <div className="absolute right-0 top-full mt-1 z-20 bg-surface ring-1 ring-border rounded-lg shadow-lg py-1 w-28">
264
+ <button
265
+ onClick={() => { setEditingSource(source); setFormRevision(r => r + 1); setSourceFormOpen(true); setSourceMenuOpen(null); }}
266
+ className="w-full text-left px-3 py-1.5 text-xs text-text hover:bg-bg-alt transition-colors"
267
+ >
268
+ Edit
269
+ </button>
270
+ <button
271
+ onClick={() => { handleDeleteSource(source); setSourceMenuOpen(null); }}
272
+ className="w-full text-left px-3 py-1.5 text-xs text-danger hover:bg-danger/5 transition-colors"
273
+ >
274
+ Remove
275
+ </button>
276
+ </div>
277
+ </>
278
+ )}
279
+ </div>
280
+ </div>
281
+ );
282
+ })}
283
+ </div>
284
+ )}
285
+ </div>
286
+
287
+ {/* Build Log */}
288
+ <BuildPanel entryId={entry.id} refreshKey={buildKey} />
289
+ </div>
290
+
291
+ {/* Right panel: search + viewer */}
292
+ <div className="flex-1 flex flex-col overflow-hidden">
293
+ <div className="p-4 border-b border-border">
294
+ <SearchBar entryId={entry.id} onSelectFile={setSelectedFile} scopeLabel="Entry only" />
295
+ </div>
296
+ <div className="flex-1 overflow-auto p-4">
297
+ <DocViewer entryId={entry.id} selectedFile={selectedFile} />
298
+ </div>
299
+ </div>
300
+
301
+ <SourceForm
302
+ key={formRevision}
303
+ open={sourceFormOpen}
304
+ onClose={() => { setSourceFormOpen(false); setEditingSource(null); }}
305
+ onCreate={editingSource ? handleEditSource : handleAddSource}
306
+ initial={editingSource ? { type: editingSource.type, label: editingSource.label, config: editingSource.config } : undefined}
307
+ />
308
+ </div>
309
+ );
310
+ }
311
+
312
+ function getConfigSummary(source: Source): string {
313
+ const c = source.config as Record<string, string>;
314
+ switch (source.type) {
315
+ case 'zip': return c.localPath || c.url || '';
316
+ case 'maven': return `${c.groupId || '?'}:${c.artifactId || '?'}:${c.version || '?'}`;
317
+ case 'antora': return c.localPath || c.repoUrl || c.zipPath || '';
318
+ case 'asciidoc': return c.localPath || c.repoUrl || c.zipPath || '';
319
+ case 'github-markdown': return c.localPath || c.repoUrl || '';
320
+ case 'source-code': return c.localPath || c.repoUrl || c.zipPath || '';
321
+ }
322
+ }
@@ -0,0 +1,117 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useNavigate, useParams } from 'react-router-dom';
3
+ import { ArrowLeft, Save } from 'lucide-react';
4
+ import { api } from '../api/client';
5
+
6
+ export default function EntryForm() {
7
+ const { id } = useParams<{ id: string }>();
8
+ const isEdit = Boolean(id);
9
+ const navigate = useNavigate();
10
+
11
+ const [name, setName] = useState('');
12
+ const [version, setVersion] = useState('');
13
+ const [description, setDescription] = useState('');
14
+ const [saving, setSaving] = useState(false);
15
+ const [error, setError] = useState<string | null>(null);
16
+ const [loading, setLoading] = useState(isEdit);
17
+
18
+ useEffect(() => {
19
+ if (!id) return;
20
+ api.entries.get(id).then((data) => {
21
+ setName(data.name);
22
+ setVersion(data.version);
23
+ setDescription(data.description);
24
+ }).catch((err) => setError((err as Error).message))
25
+ .finally(() => setLoading(false));
26
+ }, [id]);
27
+
28
+ const handleSubmit = async (e: React.FormEvent) => {
29
+ e.preventDefault();
30
+ if (!name.trim() || !version.trim()) {
31
+ setError('Name and version are required');
32
+ return;
33
+ }
34
+ setSaving(true);
35
+ setError(null);
36
+ try {
37
+ if (isEdit && id) {
38
+ await api.entries.update(id, { name: name.trim(), version: version.trim(), description: description.trim() });
39
+ } else {
40
+ await api.entries.create({ name: name.trim(), version: version.trim(), description: description.trim() });
41
+ }
42
+ navigate('/');
43
+ } catch (err) {
44
+ setError((err as Error).message);
45
+ } finally {
46
+ setSaving(false);
47
+ }
48
+ };
49
+
50
+ if (loading) {
51
+ return <div className="py-20 text-center text-sm text-text-muted">Loading...</div>;
52
+ }
53
+
54
+ return (
55
+ <div className="p-6 md:p-8 max-w-xl">
56
+ <button
57
+ onClick={() => navigate(-1)}
58
+ className="flex items-center gap-1.5 text-sm text-text-dim hover:text-text mb-6 transition-colors"
59
+ >
60
+ <ArrowLeft size={16} />
61
+ Back
62
+ </button>
63
+
64
+ <h1 className="text-xl font-semibold text-text mb-6">
65
+ {isEdit ? 'Edit Entry' : 'Create New Entry'}
66
+ </h1>
67
+
68
+ <form onSubmit={handleSubmit} className="space-y-5">
69
+ <div>
70
+ <label className="block text-sm font-medium text-text mb-2">Name</label>
71
+ <input
72
+ type="text"
73
+ value={name}
74
+ onChange={(e) => setName(e.target.value)}
75
+ placeholder="e.g. Quarkus"
76
+ className="w-full px-3.5 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 transition-all"
77
+ />
78
+ </div>
79
+ <div>
80
+ <label className="block text-sm font-medium text-text mb-2">Version</label>
81
+ <input
82
+ type="text"
83
+ value={version}
84
+ onChange={(e) => setVersion(e.target.value)}
85
+ placeholder="e.g. 3.8.0"
86
+ className="w-full px-3.5 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 transition-all"
87
+ />
88
+ </div>
89
+ <div>
90
+ <label className="block text-sm font-medium text-text mb-2">Description <span className="text-text-muted font-normal">(optional)</span></label>
91
+ <textarea
92
+ value={description}
93
+ onChange={(e) => setDescription(e.target.value)}
94
+ placeholder="Brief description of this documentation entry"
95
+ rows={3}
96
+ className="w-full px-3.5 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 transition-all resize-none"
97
+ />
98
+ </div>
99
+
100
+ {error && (
101
+ <div className="px-3 py-2 bg-danger/5 ring-1 ring-danger/20 rounded-lg text-sm text-danger">
102
+ {error}
103
+ </div>
104
+ )}
105
+
106
+ <button
107
+ type="submit"
108
+ disabled={saving}
109
+ className="flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium bg-primary text-white hover:bg-primary-hover transition-colors disabled:opacity-50"
110
+ >
111
+ <Save size={16} />
112
+ {saving ? 'Saving...' : isEdit ? 'Save Changes' : 'Create Entry'}
113
+ </button>
114
+ </form>
115
+ </div>
116
+ );
117
+ }
@@ -0,0 +1,165 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Link, useNavigate } from 'react-router-dom';
3
+ import { Plus, BookOpen, Clock, CheckCircle, AlertCircle, Loader2, Trash2, Package } from 'lucide-react';
4
+ import type { Entry } from '../types';
5
+ import { api } from '../api/client';
6
+
7
+ const statusConfig: Record<string, { icon: typeof CheckCircle; color: string; bg: string; label: string }> = {
8
+ pending: { icon: Clock, color: 'text-text-muted', bg: 'bg-bg-alt', label: 'Pending' },
9
+ building: { icon: Loader2, color: 'text-warning', bg: 'bg-warning/5', label: 'Building' },
10
+ ready: { icon: CheckCircle, color: 'text-success', bg: 'bg-success/5', label: 'Ready' },
11
+ error: { icon: AlertCircle, color: 'text-danger', bg: 'bg-danger/5', label: 'Error' },
12
+ };
13
+
14
+ export default function EntryList() {
15
+ const [entries, setEntries] = useState<Entry[]>([]);
16
+ const [loading, setLoading] = useState(true);
17
+ const [error, setError] = useState<string | null>(null);
18
+ const navigate = useNavigate();
19
+
20
+ const fetchEntries = async () => {
21
+ try {
22
+ setLoading(true);
23
+ const data = await api.entries.list();
24
+ setEntries(data);
25
+ setError(null);
26
+ } catch (err) {
27
+ setError((err as Error).message);
28
+ } finally {
29
+ setLoading(false);
30
+ }
31
+ };
32
+
33
+ useEffect(() => { fetchEntries(); }, []);
34
+
35
+ const handleDelete = async (e: React.MouseEvent, id: string, name: string) => {
36
+ e.stopPropagation();
37
+ if (!confirm(`Delete entry "${name}" and all its data?`)) return;
38
+ try {
39
+ await api.entries.delete(id);
40
+ setEntries((prev) => prev.filter((e) => e.id !== id));
41
+ } catch (err) {
42
+ alert(`Failed to delete: ${(err as Error).message}`);
43
+ }
44
+ };
45
+
46
+ if (loading) {
47
+ return (
48
+ <div className="flex items-center justify-center h-full">
49
+ <Loader2 size={24} className="animate-spin text-text-muted" />
50
+ </div>
51
+ );
52
+ }
53
+
54
+ if (error) {
55
+ return (
56
+ <div className="flex flex-col items-center justify-center h-full gap-3">
57
+ <AlertCircle size={32} className="text-danger" />
58
+ <p className="text-sm text-danger">{error}</p>
59
+ <button onClick={fetchEntries} className="text-sm text-primary hover:underline">Retry</button>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ if (entries.length === 0) {
65
+ return (
66
+ <div className="flex flex-col items-center justify-center h-full gap-5">
67
+ <div className="w-16 h-16 rounded-2xl bg-bg-alt flex items-center justify-center">
68
+ <BookOpen size={28} className="text-text-muted" />
69
+ </div>
70
+ <div className="text-center max-w-xs">
71
+ <h2 className="text-lg font-semibold text-text">No documentation entries yet</h2>
72
+ <p className="text-sm text-text-dim mt-1.5">Create your first entry to start building your documentation hub.</p>
73
+ </div>
74
+ <Link
75
+ to="/entries/new"
76
+ className="flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium bg-primary text-white hover:bg-primary-hover transition-colors"
77
+ >
78
+ <Plus size={16} />
79
+ New Entry
80
+ </Link>
81
+ </div>
82
+ );
83
+ }
84
+
85
+ return (
86
+ <div className="p-6 md:p-8">
87
+ <div className="flex items-center justify-between mb-6">
88
+ <div>
89
+ <h1 className="text-xl font-semibold text-text">Documentation Hub</h1>
90
+ <p className="text-sm text-text-dim mt-0.5">{entries.length} entr{entries.length === 1 ? 'y' : 'ies'}</p>
91
+ </div>
92
+ <Link
93
+ to="/entries/new"
94
+ className="flex items-center gap-1.5 px-3.5 py-2 rounded-lg text-sm font-medium bg-primary text-white hover:bg-primary-hover transition-colors"
95
+ >
96
+ <Plus size={15} />
97
+ New Entry
98
+ </Link>
99
+ </div>
100
+
101
+ <div className="ring-1 ring-border rounded-xl overflow-hidden">
102
+ <table className="w-full">
103
+ <thead>
104
+ <tr className="bg-bg-alt text-xs text-text-dim uppercase tracking-wider">
105
+ <th className="text-left px-4 py-2.5 font-medium">Name</th>
106
+ <th className="text-left px-4 py-2.5 font-medium w-24">Version</th>
107
+ <th className="text-left px-4 py-2.5 font-medium w-20">Status</th>
108
+ <th className="text-left px-4 py-2.5 font-medium w-20">Sources</th>
109
+ <th className="text-left px-4 py-2.5 font-medium hidden md:table-cell">Description</th>
110
+ <th className="w-10"></th>
111
+ </tr>
112
+ </thead>
113
+ <tbody>
114
+ {entries.map((entry) => {
115
+ const status = statusConfig[entry.status] || statusConfig.pending;
116
+ const StatusIcon = status.icon;
117
+ const animClass = entry.status === 'building' ? 'animate-spin' : '';
118
+ return (
119
+ <tr
120
+ key={entry.id}
121
+ onClick={() => navigate(`/entries/${entry.id}`)}
122
+ className="border-t border-border hover:bg-bg-alt/50 cursor-pointer transition-colors group"
123
+ >
124
+ <td className="px-4 py-3">
125
+ <span className="text-sm font-medium text-text group-hover:text-primary transition-colors">
126
+ {entry.name}
127
+ </span>
128
+ </td>
129
+ <td className="px-4 py-3">
130
+ <span className="text-xs text-text-dim font-mono">{entry.version}</span>
131
+ </td>
132
+ <td className="px-4 py-3">
133
+ <span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${status.color} ${status.bg}`}>
134
+ <StatusIcon size={11} className={animClass} />
135
+ {status.label}
136
+ </span>
137
+ </td>
138
+ <td className="px-4 py-3">
139
+ <span className="text-xs text-text-dim">
140
+ {entry.source_count ?? 0}
141
+ </span>
142
+ </td>
143
+ <td className="px-4 py-3 hidden md:table-cell">
144
+ <span className="text-xs text-text-dim truncate block max-w-xs">
145
+ {entry.description || '—'}
146
+ </span>
147
+ </td>
148
+ <td className="px-2 py-3">
149
+ <button
150
+ onClick={(e) => handleDelete(e, entry.id, entry.name)}
151
+ className="p-1 rounded text-text-muted hover:text-danger opacity-0 group-hover:opacity-100 transition-all"
152
+ title="Delete"
153
+ >
154
+ <Trash2 size={14} />
155
+ </button>
156
+ </td>
157
+ </tr>
158
+ );
159
+ })}
160
+ </tbody>
161
+ </table>
162
+ </div>
163
+ </div>
164
+ );
165
+ }