@marlinjai/clearify 1.20.0 → 2.1.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/dist/config-schema.json +142 -44
- package/dist/node/{chunk-JZZ2BNIL.js → chunk-GPBEDIOY.js} +2 -2
- package/dist/node/{chunk-HZRIVY25.js → chunk-GZE6QLJT.js} +1 -1
- package/dist/node/{chunk-XR4WG237.js → chunk-PZA4UAFG.js} +42 -2
- package/dist/node/{chunk-GWYMXKDL.js → chunk-Q5FUAK6J.js} +1 -1
- package/dist/node/chunk-TVDWHKQF.js +134 -0
- package/dist/node/{chunk-2WML2D77.js → chunk-ZWF6VCSQ.js} +68 -32
- package/dist/node/core/config.d.ts +23 -11
- package/dist/node/core/config.js +1 -1
- package/dist/node/core/llms-txt.d.ts +54 -0
- package/dist/node/core/llms-txt.js +10 -0
- package/dist/node/{hub-register-7NF5BP3E.js → hub-register-QB73GUF2.js} +6 -3
- package/dist/node/node/build.js +4 -3
- package/dist/node/node/check.js +2 -2
- package/dist/node/node/index.js +6 -5
- package/dist/node/node/init.js +1 -1
- package/dist/node/types/index.d.ts +72 -8
- package/dist/node/vite-plugin/index.js +2 -2
- package/package.json +5 -3
- package/src/theme/Sidebar.tsx +2 -2
- package/src/theme/admin/ProjectsManager.tsx +227 -109
- package/src/theme/components/ProjectGrid.tsx +17 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useState, useCallback } from 'react';
|
|
2
|
-
import type { HubProject } from '../../types/index.js';
|
|
2
|
+
import type { HubProject, HubProjectSource, HubProjectPlacement } from '../../types/index.js';
|
|
3
3
|
import { Modal } from './components/Modal.js';
|
|
4
4
|
import { ConfirmDialog } from './components/ConfirmDialog.js';
|
|
5
5
|
import { useToast, ToastContainer } from './components/Toast.js';
|
|
@@ -16,23 +16,141 @@ const statusColors: Record<string, { bg: string; text: string }> = {
|
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
/* ------------------------------------------------------------------ */
|
|
19
|
-
/*
|
|
19
|
+
/* Draft form shape */
|
|
20
20
|
/* ------------------------------------------------------------------ */
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The form state carries every field for every kind so the user can flip
|
|
23
|
+
* between kinds without losing input. We clean it to the discriminated-
|
|
24
|
+
* union shape before saving.
|
|
25
|
+
*/
|
|
26
|
+
interface DraftProject {
|
|
27
|
+
name: string;
|
|
28
|
+
description: string;
|
|
29
|
+
status: 'active' | 'beta' | 'planned' | 'deprecated';
|
|
30
|
+
icon?: string;
|
|
31
|
+
group?: string;
|
|
32
|
+
tags?: string[];
|
|
33
|
+
repo?: string;
|
|
34
|
+
sourceKind: HubProjectSource['kind'];
|
|
35
|
+
gitRepo: string;
|
|
36
|
+
gitRef: string;
|
|
37
|
+
gitPath: string;
|
|
38
|
+
urlUrl: string;
|
|
39
|
+
inlineMarkdown: string;
|
|
40
|
+
placementKind: HubProjectPlacement['kind'];
|
|
41
|
+
cardHref: string;
|
|
42
|
+
tabSections: 'all' | 'public';
|
|
43
|
+
nestedInto: string;
|
|
44
|
+
nestedDocsPath: string;
|
|
45
|
+
nestedGroup: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function emptyDraft(): DraftProject {
|
|
22
49
|
return {
|
|
23
50
|
name: '',
|
|
24
51
|
description: '',
|
|
25
|
-
mode: 'link',
|
|
26
52
|
status: 'active',
|
|
27
53
|
icon: '',
|
|
28
54
|
group: '',
|
|
29
|
-
href: '',
|
|
30
55
|
tags: [],
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
56
|
+
repo: '',
|
|
57
|
+
sourceKind: 'none',
|
|
58
|
+
gitRepo: '',
|
|
59
|
+
gitRef: 'main',
|
|
60
|
+
gitPath: 'docs/public',
|
|
61
|
+
urlUrl: '',
|
|
62
|
+
inlineMarkdown: '',
|
|
63
|
+
placementKind: 'card',
|
|
64
|
+
cardHref: '',
|
|
65
|
+
tabSections: 'public',
|
|
66
|
+
nestedInto: '',
|
|
67
|
+
nestedDocsPath: 'docs',
|
|
68
|
+
nestedGroup: '',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function projectToDraft(p: HubProject): DraftProject {
|
|
73
|
+
const d = emptyDraft();
|
|
74
|
+
d.name = p.name;
|
|
75
|
+
d.description = p.description;
|
|
76
|
+
d.status = p.status ?? 'active';
|
|
77
|
+
d.icon = p.icon ?? '';
|
|
78
|
+
d.group = p.group ?? '';
|
|
79
|
+
d.tags = p.tags ? [...p.tags] : [];
|
|
80
|
+
d.repo = p.repo ?? '';
|
|
81
|
+
|
|
82
|
+
d.sourceKind = p.source.kind;
|
|
83
|
+
if (p.source.kind === 'git') {
|
|
84
|
+
d.gitRepo = p.source.repo;
|
|
85
|
+
d.gitRef = p.source.ref ?? 'main';
|
|
86
|
+
d.gitPath = p.source.path ?? 'docs/public';
|
|
87
|
+
} else if (p.source.kind === 'url') {
|
|
88
|
+
d.urlUrl = p.source.url;
|
|
89
|
+
} else if (p.source.kind === 'inline') {
|
|
90
|
+
d.inlineMarkdown = p.source.markdown;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
d.placementKind = p.placement.kind;
|
|
94
|
+
if (p.placement.kind === 'card') {
|
|
95
|
+
d.cardHref = p.placement.href;
|
|
96
|
+
} else if (p.placement.kind === 'tab') {
|
|
97
|
+
const s = p.placement.sections;
|
|
98
|
+
d.tabSections = s === 'all' ? 'all' : 'public';
|
|
99
|
+
} else if (p.placement.kind === 'nested') {
|
|
100
|
+
d.nestedInto = p.placement.into;
|
|
101
|
+
d.nestedDocsPath = p.placement.docsPath ?? 'docs';
|
|
102
|
+
d.nestedGroup = p.placement.group ?? '';
|
|
103
|
+
}
|
|
104
|
+
return d;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function draftToProject(d: DraftProject): HubProject {
|
|
108
|
+
let source: HubProjectSource;
|
|
109
|
+
if (d.sourceKind === 'git') {
|
|
110
|
+
const g: Extract<HubProjectSource, { kind: 'git' }> = {
|
|
111
|
+
kind: 'git',
|
|
112
|
+
repo: d.gitRepo,
|
|
113
|
+
};
|
|
114
|
+
if (d.gitRef && d.gitRef !== 'main') g.ref = d.gitRef;
|
|
115
|
+
if (d.gitPath) g.path = d.gitPath;
|
|
116
|
+
source = g;
|
|
117
|
+
} else if (d.sourceKind === 'url') {
|
|
118
|
+
source = { kind: 'url', url: d.urlUrl };
|
|
119
|
+
} else if (d.sourceKind === 'inline') {
|
|
120
|
+
source = { kind: 'inline', markdown: d.inlineMarkdown };
|
|
121
|
+
} else {
|
|
122
|
+
source = { kind: 'none' };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let placement: HubProjectPlacement;
|
|
126
|
+
if (d.placementKind === 'tab') {
|
|
127
|
+
const t: Extract<HubProjectPlacement, { kind: 'tab' }> = { kind: 'tab' };
|
|
128
|
+
if (d.tabSections && d.tabSections !== 'public') t.sections = d.tabSections;
|
|
129
|
+
placement = t;
|
|
130
|
+
} else if (d.placementKind === 'nested') {
|
|
131
|
+
const n: Extract<HubProjectPlacement, { kind: 'nested' }> = {
|
|
132
|
+
kind: 'nested',
|
|
133
|
+
into: d.nestedInto,
|
|
134
|
+
};
|
|
135
|
+
if (d.nestedDocsPath && d.nestedDocsPath !== 'docs') n.docsPath = d.nestedDocsPath;
|
|
136
|
+
if (d.nestedGroup) n.group = d.nestedGroup;
|
|
137
|
+
placement = n;
|
|
138
|
+
} else {
|
|
139
|
+
placement = { kind: 'card', href: d.cardHref };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const project: HubProject = {
|
|
143
|
+
name: d.name.trim(),
|
|
144
|
+
description: d.description.trim(),
|
|
145
|
+
source,
|
|
146
|
+
placement,
|
|
35
147
|
};
|
|
148
|
+
if (d.status && d.status !== 'active') project.status = d.status;
|
|
149
|
+
if (d.icon) project.icon = d.icon;
|
|
150
|
+
if (d.group) project.group = d.group;
|
|
151
|
+
if (d.tags && d.tags.length > 0) project.tags = d.tags;
|
|
152
|
+
if (d.repo) project.repo = d.repo;
|
|
153
|
+
return project;
|
|
36
154
|
}
|
|
37
155
|
|
|
38
156
|
/* ------------------------------------------------------------------ */
|
|
@@ -70,144 +188,138 @@ const primaryBtnStyle: React.CSSProperties = {
|
|
|
70
188
|
};
|
|
71
189
|
|
|
72
190
|
/* ------------------------------------------------------------------ */
|
|
73
|
-
/*
|
|
191
|
+
/* Summary of the source+placement combo for the list view */
|
|
74
192
|
/* ------------------------------------------------------------------ */
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
const mode = cleaned.mode ?? 'link';
|
|
78
|
-
if (mode === 'link') {
|
|
79
|
-
delete cleaned.git;
|
|
80
|
-
delete cleaned.embedSections;
|
|
81
|
-
delete cleaned.injectInto;
|
|
82
|
-
delete cleaned.docsPath;
|
|
83
|
-
// Don't add mode: 'link' if original didn't have it (it's the default)
|
|
84
|
-
if (!original?.mode) delete cleaned.mode;
|
|
85
|
-
} else if (mode === 'embed') {
|
|
86
|
-
delete cleaned.href;
|
|
87
|
-
delete cleaned.injectInto;
|
|
88
|
-
delete cleaned.docsPath;
|
|
89
|
-
} else if (mode === 'inject') {
|
|
90
|
-
delete cleaned.href;
|
|
91
|
-
delete cleaned.embedSections;
|
|
92
|
-
}
|
|
93
|
-
if (!cleaned.icon) delete cleaned.icon;
|
|
94
|
-
if (!cleaned.group) delete cleaned.group;
|
|
95
|
-
if (!cleaned.href) delete cleaned.href;
|
|
96
|
-
if (cleaned.tags && cleaned.tags.length === 0) delete cleaned.tags;
|
|
97
|
-
if (cleaned.git && !cleaned.git.repo) delete cleaned.git;
|
|
98
|
-
if (cleaned.git && !cleaned.git.ref) delete cleaned.git.ref;
|
|
99
|
-
if (cleaned.status === 'active' && !original?.status) delete cleaned.status;
|
|
100
|
-
|
|
101
|
-
// Preserve original field ordering: rebuild using original's key order first
|
|
102
|
-
if (original) {
|
|
103
|
-
const ordered: any = {};
|
|
104
|
-
for (const key of Object.keys(original)) {
|
|
105
|
-
if (key in cleaned) ordered[key] = (cleaned as any)[key];
|
|
106
|
-
}
|
|
107
|
-
for (const key of Object.keys(cleaned)) {
|
|
108
|
-
if (!(key in ordered)) ordered[key] = (cleaned as any)[key];
|
|
109
|
-
}
|
|
110
|
-
return ordered as HubProject;
|
|
111
|
-
}
|
|
112
|
-
return cleaned;
|
|
193
|
+
function combo(project: HubProject): string {
|
|
194
|
+
return `${project.source.kind} + ${project.placement.kind}`;
|
|
113
195
|
}
|
|
114
196
|
|
|
115
197
|
/* ------------------------------------------------------------------ */
|
|
116
198
|
/* Project form */
|
|
117
199
|
/* ------------------------------------------------------------------ */
|
|
118
200
|
function ProjectForm({
|
|
119
|
-
|
|
201
|
+
draft,
|
|
120
202
|
onChange,
|
|
121
203
|
onSave,
|
|
122
204
|
saving,
|
|
123
205
|
}: {
|
|
124
|
-
|
|
125
|
-
onChange: (
|
|
206
|
+
draft: DraftProject;
|
|
207
|
+
onChange: (d: DraftProject) => void;
|
|
126
208
|
onSave: () => void;
|
|
127
209
|
saving: boolean;
|
|
128
210
|
}) {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
const set = <K extends keyof HubProject>(key: K, value: HubProject[K]) =>
|
|
132
|
-
onChange({ ...project, [key]: value });
|
|
211
|
+
const set = <K extends keyof DraftProject>(key: K, value: DraftProject[K]) =>
|
|
212
|
+
onChange({ ...draft, [key]: value });
|
|
133
213
|
|
|
134
214
|
return (
|
|
135
215
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.875rem' }}>
|
|
136
216
|
<div>
|
|
137
217
|
<label style={labelStyle}>Name *</label>
|
|
138
|
-
<input style={inputStyle} value={
|
|
218
|
+
<input style={inputStyle} value={draft.name} onChange={(e) => set('name', e.target.value)} placeholder="my-project" />
|
|
139
219
|
</div>
|
|
140
220
|
<div>
|
|
141
221
|
<label style={labelStyle}>Description *</label>
|
|
142
|
-
<textarea style={{ ...inputStyle, minHeight: 72, resize: 'vertical' }} value={
|
|
222
|
+
<textarea style={{ ...inputStyle, minHeight: 72, resize: 'vertical' }} value={draft.description} onChange={(e) => set('description', e.target.value)} placeholder="A short project description" />
|
|
143
223
|
</div>
|
|
144
224
|
<div>
|
|
145
225
|
<label style={labelStyle}>Icon</label>
|
|
146
|
-
<input style={inputStyle} value={
|
|
226
|
+
<input style={inputStyle} value={draft.icon ?? ''} onChange={(e) => set('icon', e.target.value)} placeholder="e.g. book, code, rocket" />
|
|
147
227
|
</div>
|
|
148
228
|
<div style={{ display: 'flex', gap: '0.75rem' }}>
|
|
149
|
-
<div style={{ flex: 1 }}>
|
|
150
|
-
<label style={labelStyle}>Mode</label>
|
|
151
|
-
<select style={inputStyle} value={mode} onChange={(e) => set('mode', e.target.value as HubProject['mode'])}>
|
|
152
|
-
<option value="link">Link</option>
|
|
153
|
-
<option value="embed">Embed</option>
|
|
154
|
-
<option value="inject">Inject</option>
|
|
155
|
-
</select>
|
|
156
|
-
</div>
|
|
157
229
|
<div style={{ flex: 1 }}>
|
|
158
230
|
<label style={labelStyle}>Status</label>
|
|
159
|
-
<select style={inputStyle} value={
|
|
231
|
+
<select style={inputStyle} value={draft.status} onChange={(e) => set('status', e.target.value as DraftProject['status'])}>
|
|
160
232
|
<option value="active">Active</option>
|
|
161
233
|
<option value="beta">Beta</option>
|
|
162
234
|
<option value="planned">Planned</option>
|
|
163
235
|
<option value="deprecated">Deprecated</option>
|
|
164
236
|
</select>
|
|
165
237
|
</div>
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
238
|
+
<div style={{ flex: 1 }}>
|
|
239
|
+
<label style={labelStyle}>Group</label>
|
|
240
|
+
<input style={inputStyle} value={draft.group ?? ''} onChange={(e) => set('group', e.target.value)} placeholder="e.g. backend, frontend" />
|
|
241
|
+
</div>
|
|
170
242
|
</div>
|
|
171
243
|
|
|
172
|
-
{
|
|
173
|
-
<div>
|
|
174
|
-
<label style={labelStyle}>
|
|
175
|
-
<
|
|
244
|
+
<div style={{ display: 'flex', gap: '0.75rem' }}>
|
|
245
|
+
<div style={{ flex: 1 }}>
|
|
246
|
+
<label style={labelStyle}>Source</label>
|
|
247
|
+
<select
|
|
248
|
+
style={inputStyle}
|
|
249
|
+
value={draft.sourceKind}
|
|
250
|
+
onChange={(e) => set('sourceKind', e.target.value as HubProjectSource['kind'])}
|
|
251
|
+
>
|
|
252
|
+
<option value="none">none (no content pulled)</option>
|
|
253
|
+
<option value="git">git (sparse clone)</option>
|
|
254
|
+
<option value="url" disabled>url (reserved)</option>
|
|
255
|
+
<option value="inline" disabled>inline (reserved)</option>
|
|
256
|
+
</select>
|
|
176
257
|
</div>
|
|
177
|
-
|
|
258
|
+
<div style={{ flex: 1 }}>
|
|
259
|
+
<label style={labelStyle}>Placement</label>
|
|
260
|
+
<select
|
|
261
|
+
style={inputStyle}
|
|
262
|
+
value={draft.placementKind}
|
|
263
|
+
onChange={(e) => set('placementKind', e.target.value as HubProjectPlacement['kind'])}
|
|
264
|
+
>
|
|
265
|
+
<option value="card">card (grid entry, link out)</option>
|
|
266
|
+
<option value="tab">tab (full hub tab)</option>
|
|
267
|
+
<option value="nested">nested (into existing section)</option>
|
|
268
|
+
</select>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
178
271
|
|
|
179
|
-
{
|
|
272
|
+
{draft.sourceKind === 'git' && (
|
|
180
273
|
<>
|
|
181
274
|
<div>
|
|
182
|
-
<label style={labelStyle}>Git
|
|
183
|
-
<input style={inputStyle} value={
|
|
275
|
+
<label style={labelStyle}>Git repo *</label>
|
|
276
|
+
<input style={inputStyle} value={draft.gitRepo} onChange={(e) => set('gitRepo', e.target.value)} placeholder="https://github.com/org/repo.git" />
|
|
184
277
|
</div>
|
|
185
|
-
<div>
|
|
186
|
-
<
|
|
187
|
-
|
|
278
|
+
<div style={{ display: 'flex', gap: '0.75rem' }}>
|
|
279
|
+
<div style={{ flex: 1 }}>
|
|
280
|
+
<label style={labelStyle}>Git ref</label>
|
|
281
|
+
<input style={inputStyle} value={draft.gitRef} onChange={(e) => set('gitRef', e.target.value)} placeholder="main" />
|
|
282
|
+
</div>
|
|
283
|
+
<div style={{ flex: 1 }}>
|
|
284
|
+
<label style={labelStyle}>Git path</label>
|
|
285
|
+
<input style={inputStyle} value={draft.gitPath} onChange={(e) => set('gitPath', e.target.value)} placeholder="docs/public" />
|
|
286
|
+
</div>
|
|
188
287
|
</div>
|
|
189
288
|
</>
|
|
190
289
|
)}
|
|
191
290
|
|
|
192
|
-
{
|
|
291
|
+
{draft.placementKind === 'card' && (
|
|
193
292
|
<div>
|
|
194
|
-
<label style={labelStyle}>
|
|
195
|
-
<
|
|
196
|
-
|
|
197
|
-
|
|
293
|
+
<label style={labelStyle}>Card href *</label>
|
|
294
|
+
<input style={inputStyle} value={draft.cardHref} onChange={(e) => set('cardHref', e.target.value)} placeholder="https://example.com/docs" />
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
297
|
+
|
|
298
|
+
{draft.placementKind === 'tab' && (
|
|
299
|
+
<div>
|
|
300
|
+
<label style={labelStyle}>Tab sections</label>
|
|
301
|
+
<select style={inputStyle} value={draft.tabSections} onChange={(e) => set('tabSections', e.target.value as 'all' | 'public')}>
|
|
302
|
+
<option value="public">public (non-draft)</option>
|
|
303
|
+
<option value="all">all</option>
|
|
198
304
|
</select>
|
|
199
305
|
</div>
|
|
200
306
|
)}
|
|
201
307
|
|
|
202
|
-
{
|
|
308
|
+
{draft.placementKind === 'nested' && (
|
|
203
309
|
<>
|
|
204
310
|
<div>
|
|
205
|
-
<label style={labelStyle}>
|
|
206
|
-
<input style={inputStyle} value={
|
|
311
|
+
<label style={labelStyle}>Nest into (section label) *</label>
|
|
312
|
+
<input style={inputStyle} value={draft.nestedInto} onChange={(e) => set('nestedInto', e.target.value)} placeholder="Documentation" />
|
|
207
313
|
</div>
|
|
208
|
-
<div>
|
|
209
|
-
<
|
|
210
|
-
|
|
314
|
+
<div style={{ display: 'flex', gap: '0.75rem' }}>
|
|
315
|
+
<div style={{ flex: 1 }}>
|
|
316
|
+
<label style={labelStyle}>Docs path (in cloned repo)</label>
|
|
317
|
+
<input style={inputStyle} value={draft.nestedDocsPath} onChange={(e) => set('nestedDocsPath', e.target.value)} placeholder="docs" />
|
|
318
|
+
</div>
|
|
319
|
+
<div style={{ flex: 1 }}>
|
|
320
|
+
<label style={labelStyle}>Nested group (optional)</label>
|
|
321
|
+
<input style={inputStyle} value={draft.nestedGroup} onChange={(e) => set('nestedGroup', e.target.value)} placeholder="Services" />
|
|
322
|
+
</div>
|
|
211
323
|
</div>
|
|
212
324
|
</>
|
|
213
325
|
)}
|
|
@@ -216,7 +328,7 @@ function ProjectForm({
|
|
|
216
328
|
<label style={labelStyle}>Tags (comma-separated)</label>
|
|
217
329
|
<input
|
|
218
330
|
style={inputStyle}
|
|
219
|
-
value={(
|
|
331
|
+
value={(draft.tags ?? []).join(', ')}
|
|
220
332
|
onChange={(e) => set('tags', e.target.value.split(',').map((t) => t.trim()).filter(Boolean))}
|
|
221
333
|
placeholder="api, docs, internal"
|
|
222
334
|
/>
|
|
@@ -224,7 +336,7 @@ function ProjectForm({
|
|
|
224
336
|
|
|
225
337
|
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '0.25rem' }}>
|
|
226
338
|
<button style={primaryBtnStyle} onClick={onSave} disabled={saving}>
|
|
227
|
-
{saving ? 'Saving
|
|
339
|
+
{saving ? 'Saving...' : 'Save Project'}
|
|
228
340
|
</button>
|
|
229
341
|
</div>
|
|
230
342
|
</div>
|
|
@@ -243,7 +355,7 @@ export function ProjectsManager() {
|
|
|
243
355
|
|
|
244
356
|
// modal state
|
|
245
357
|
const [editIndex, setEditIndex] = useState<number | null>(null); // null = closed, -1 = new
|
|
246
|
-
const [draft, setDraft] = useState<
|
|
358
|
+
const [draft, setDraft] = useState<DraftProject>(emptyDraft());
|
|
247
359
|
const [saving, setSaving] = useState(false);
|
|
248
360
|
|
|
249
361
|
// delete confirm
|
|
@@ -318,9 +430,20 @@ export function ProjectsManager() {
|
|
|
318
430
|
show('Name and description are required', 'error');
|
|
319
431
|
return;
|
|
320
432
|
}
|
|
433
|
+
if (draft.sourceKind === 'git' && !draft.gitRepo.trim()) {
|
|
434
|
+
show('Git repo is required for git source', 'error');
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (draft.placementKind === 'card' && !draft.cardHref.trim()) {
|
|
438
|
+
show('Card href is required for card placement', 'error');
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (draft.placementKind === 'nested' && !draft.nestedInto.trim()) {
|
|
442
|
+
show('Nest-into section label is required for nested placement', 'error');
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
321
445
|
|
|
322
|
-
const
|
|
323
|
-
const cleaned = cleanProject(draft, original);
|
|
446
|
+
const cleaned = draftToProject(draft);
|
|
324
447
|
const next = [...projects];
|
|
325
448
|
if (editIndex === -1) {
|
|
326
449
|
next.push(cleaned);
|
|
@@ -360,18 +483,13 @@ export function ProjectsManager() {
|
|
|
360
483
|
|
|
361
484
|
/* ---------- open modal ---------- */
|
|
362
485
|
const openAdd = () => {
|
|
363
|
-
setDraft(
|
|
486
|
+
setDraft(emptyDraft());
|
|
364
487
|
setEditIndex(-1);
|
|
365
488
|
};
|
|
366
489
|
|
|
367
490
|
const openEdit = (index: number) => {
|
|
368
491
|
const p = projects[index];
|
|
369
|
-
setDraft(
|
|
370
|
-
...emptyProject(),
|
|
371
|
-
...p,
|
|
372
|
-
git: p.git ? { ...p.git } : { repo: '', ref: '' },
|
|
373
|
-
tags: p.tags ? [...p.tags] : [],
|
|
374
|
-
});
|
|
492
|
+
setDraft(projectToDraft(p));
|
|
375
493
|
setEditIndex(index);
|
|
376
494
|
};
|
|
377
495
|
|
|
@@ -415,7 +533,7 @@ export function ProjectsManager() {
|
|
|
415
533
|
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.875rem' }}>
|
|
416
534
|
<thead>
|
|
417
535
|
<tr style={{ backgroundColor: 'var(--clearify-bg-secondary)', borderBottom: '1px solid var(--clearify-border)' }}>
|
|
418
|
-
{['Name', 'Description', '
|
|
536
|
+
{['Name', 'Description', 'Source + Placement', 'Group', 'Status', ''].map((h) => (
|
|
419
537
|
<th key={h} style={{ textAlign: 'left', padding: '0.625rem 0.75rem', fontWeight: 600, color: 'var(--clearify-text-secondary)', fontSize: '0.8125rem' }}>{h}</th>
|
|
420
538
|
))}
|
|
421
539
|
</tr>
|
|
@@ -438,8 +556,8 @@ export function ProjectsManager() {
|
|
|
438
556
|
<td style={{ padding: '0.625rem 0.75rem', color: 'var(--clearify-text-secondary)', maxWidth: 250, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
439
557
|
{p.description}
|
|
440
558
|
</td>
|
|
441
|
-
<td style={{ padding: '0.625rem 0.75rem', color: 'var(--clearify-text-secondary)' }}>{p
|
|
442
|
-
<td style={{ padding: '0.625rem 0.75rem', color: 'var(--clearify-text-secondary)' }}>{p.group ?? '
|
|
559
|
+
<td style={{ padding: '0.625rem 0.75rem', color: 'var(--clearify-text-secondary)' }}>{combo(p)}</td>
|
|
560
|
+
<td style={{ padding: '0.625rem 0.75rem', color: 'var(--clearify-text-secondary)' }}>{p.group ?? '-'}</td>
|
|
443
561
|
<td style={{ padding: '0.625rem 0.75rem' }}>
|
|
444
562
|
<span style={{ display: 'inline-block', padding: '0.125rem 0.5rem', borderRadius: '9999px', fontSize: '0.75rem', fontWeight: 600, backgroundColor: colors.bg, color: colors.text }}>
|
|
445
563
|
{st}
|
|
@@ -512,7 +630,7 @@ export function ProjectsManager() {
|
|
|
512
630
|
|
|
513
631
|
{/* Edit / Add modal */}
|
|
514
632
|
<Modal open={editIndex !== null} onClose={() => setEditIndex(null)} title={editIndex === -1 ? 'Add Project' : `Edit: ${editIndex !== null && editIndex >= 0 ? projects[editIndex]?.name ?? '' : ''}`}>
|
|
515
|
-
<ProjectForm
|
|
633
|
+
<ProjectForm draft={draft} onChange={setDraft} onSave={handleSave} saving={saving} />
|
|
516
634
|
</Modal>
|
|
517
635
|
|
|
518
636
|
{/* Delete confirmation */}
|
|
@@ -8,6 +8,22 @@ interface ProjectGridProps {
|
|
|
8
8
|
children?: React.ReactNode;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function projectHref(project: HubProject): string | undefined {
|
|
12
|
+
if (project.placement.kind === 'card') return project.placement.href;
|
|
13
|
+
if (project.placement.kind === 'tab') {
|
|
14
|
+
// Default to the project's own tab path on the hub
|
|
15
|
+
const slug = project.name
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
18
|
+
.replace(/^-|-$/g, '');
|
|
19
|
+
return `/${slug}`;
|
|
20
|
+
}
|
|
21
|
+
if (project.placement.kind === 'nested') {
|
|
22
|
+
return `/${project.placement.into}`;
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
11
27
|
function ProjectGridInner({ cols, projects }: { cols: number; projects: HubProject[] }) {
|
|
12
28
|
return (
|
|
13
29
|
<div
|
|
@@ -23,7 +39,7 @@ function ProjectGridInner({ cols, projects }: { cols: number; projects: HubProje
|
|
|
23
39
|
key={project.name}
|
|
24
40
|
name={project.name}
|
|
25
41
|
description={project.description}
|
|
26
|
-
href={project
|
|
42
|
+
href={projectHref(project)}
|
|
27
43
|
repo={project.repo}
|
|
28
44
|
status={project.status}
|
|
29
45
|
icon={project.icon}
|