@marlinjai/clearify 1.20.0 → 2.0.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-XR4WG237.js → chunk-BCEGDIR6.js} +2 -2
- package/dist/node/{chunk-HZRIVY25.js → chunk-CK2SLBAG.js} +1 -1
- package/dist/node/{chunk-JZZ2BNIL.js → chunk-GPBEDIOY.js} +2 -2
- package/dist/node/{chunk-2WML2D77.js → chunk-PGTNVDNF.js} +61 -31
- package/dist/node/{chunk-GWYMXKDL.js → chunk-RW4IKHY4.js} +1 -1
- package/dist/node/core/config.d.ts +23 -11
- package/dist/node/core/config.js +1 -1
- package/dist/node/{hub-register-7NF5BP3E.js → hub-register-QB73GUF2.js} +6 -3
- package/dist/node/node/build.js +3 -3
- package/dist/node/node/check.js +2 -2
- package/dist/node/node/index.js +5 -5
- package/dist/node/node/init.js +1 -1
- package/dist/node/types/index.d.ts +66 -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
package/dist/config-schema.json
CHANGED
|
@@ -116,9 +116,6 @@
|
|
|
116
116
|
"description": {
|
|
117
117
|
"type": "string"
|
|
118
118
|
},
|
|
119
|
-
"href": {
|
|
120
|
-
"type": "string"
|
|
121
|
-
},
|
|
122
119
|
"repo": {
|
|
123
120
|
"type": "string"
|
|
124
121
|
},
|
|
@@ -144,64 +141,165 @@
|
|
|
144
141
|
"group": {
|
|
145
142
|
"type": "string"
|
|
146
143
|
},
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
144
|
+
"source": {
|
|
145
|
+
"oneOf": [
|
|
146
|
+
{
|
|
147
|
+
"type": "object",
|
|
148
|
+
"properties": {
|
|
149
|
+
"kind": {
|
|
150
|
+
"type": "string",
|
|
151
|
+
"const": "none"
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"required": [
|
|
155
|
+
"kind"
|
|
156
|
+
],
|
|
157
|
+
"additionalProperties": false
|
|
160
158
|
},
|
|
161
|
-
|
|
162
|
-
"type": "
|
|
159
|
+
{
|
|
160
|
+
"type": "object",
|
|
161
|
+
"properties": {
|
|
162
|
+
"kind": {
|
|
163
|
+
"type": "string",
|
|
164
|
+
"const": "git"
|
|
165
|
+
},
|
|
166
|
+
"repo": {
|
|
167
|
+
"type": "string"
|
|
168
|
+
},
|
|
169
|
+
"ref": {
|
|
170
|
+
"type": "string"
|
|
171
|
+
},
|
|
172
|
+
"path": {
|
|
173
|
+
"type": "string"
|
|
174
|
+
},
|
|
175
|
+
"sparse": {
|
|
176
|
+
"type": "boolean"
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
"required": [
|
|
180
|
+
"kind",
|
|
181
|
+
"repo"
|
|
182
|
+
],
|
|
183
|
+
"additionalProperties": false
|
|
163
184
|
},
|
|
164
|
-
|
|
165
|
-
"type": "
|
|
185
|
+
{
|
|
186
|
+
"type": "object",
|
|
187
|
+
"properties": {
|
|
188
|
+
"kind": {
|
|
189
|
+
"type": "string",
|
|
190
|
+
"const": "url"
|
|
191
|
+
},
|
|
192
|
+
"url": {
|
|
193
|
+
"type": "string"
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
"required": [
|
|
197
|
+
"kind",
|
|
198
|
+
"url"
|
|
199
|
+
],
|
|
200
|
+
"additionalProperties": false
|
|
166
201
|
},
|
|
167
|
-
|
|
168
|
-
"type": "
|
|
202
|
+
{
|
|
203
|
+
"type": "object",
|
|
204
|
+
"properties": {
|
|
205
|
+
"kind": {
|
|
206
|
+
"type": "string",
|
|
207
|
+
"const": "inline"
|
|
208
|
+
},
|
|
209
|
+
"markdown": {
|
|
210
|
+
"type": "string"
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
"required": [
|
|
214
|
+
"kind",
|
|
215
|
+
"markdown"
|
|
216
|
+
],
|
|
217
|
+
"additionalProperties": false
|
|
169
218
|
}
|
|
170
|
-
|
|
171
|
-
"required": [
|
|
172
|
-
"repo"
|
|
173
|
-
],
|
|
174
|
-
"additionalProperties": false
|
|
219
|
+
]
|
|
175
220
|
},
|
|
176
|
-
"
|
|
177
|
-
"
|
|
221
|
+
"placement": {
|
|
222
|
+
"oneOf": [
|
|
178
223
|
{
|
|
179
|
-
"type": "
|
|
180
|
-
"
|
|
224
|
+
"type": "object",
|
|
225
|
+
"properties": {
|
|
226
|
+
"kind": {
|
|
227
|
+
"type": "string",
|
|
228
|
+
"const": "card"
|
|
229
|
+
},
|
|
230
|
+
"href": {
|
|
231
|
+
"type": "string"
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
"required": [
|
|
235
|
+
"kind",
|
|
236
|
+
"href"
|
|
237
|
+
],
|
|
238
|
+
"additionalProperties": false
|
|
181
239
|
},
|
|
182
240
|
{
|
|
183
|
-
"type": "
|
|
184
|
-
"
|
|
241
|
+
"type": "object",
|
|
242
|
+
"properties": {
|
|
243
|
+
"kind": {
|
|
244
|
+
"type": "string",
|
|
245
|
+
"const": "tab"
|
|
246
|
+
},
|
|
247
|
+
"sections": {
|
|
248
|
+
"anyOf": [
|
|
249
|
+
{
|
|
250
|
+
"type": "string",
|
|
251
|
+
"const": "all"
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
"type": "string",
|
|
255
|
+
"const": "public"
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"type": "array",
|
|
259
|
+
"items": {
|
|
260
|
+
"type": "string"
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
]
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
"required": [
|
|
267
|
+
"kind"
|
|
268
|
+
],
|
|
269
|
+
"additionalProperties": false
|
|
185
270
|
},
|
|
186
271
|
{
|
|
187
|
-
"type": "
|
|
188
|
-
"
|
|
189
|
-
"
|
|
190
|
-
|
|
272
|
+
"type": "object",
|
|
273
|
+
"properties": {
|
|
274
|
+
"kind": {
|
|
275
|
+
"type": "string",
|
|
276
|
+
"const": "nested"
|
|
277
|
+
},
|
|
278
|
+
"into": {
|
|
279
|
+
"type": "string"
|
|
280
|
+
},
|
|
281
|
+
"docsPath": {
|
|
282
|
+
"type": "string"
|
|
283
|
+
},
|
|
284
|
+
"group": {
|
|
285
|
+
"type": "string"
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
"required": [
|
|
289
|
+
"kind",
|
|
290
|
+
"into"
|
|
291
|
+
],
|
|
292
|
+
"additionalProperties": false
|
|
191
293
|
}
|
|
192
294
|
]
|
|
193
|
-
},
|
|
194
|
-
"injectInto": {
|
|
195
|
-
"type": "string"
|
|
196
|
-
},
|
|
197
|
-
"docsPath": {
|
|
198
|
-
"type": "string"
|
|
199
295
|
}
|
|
200
296
|
},
|
|
201
297
|
"required": [
|
|
202
298
|
"name",
|
|
203
299
|
"description",
|
|
204
|
-
"status"
|
|
300
|
+
"status",
|
|
301
|
+
"source",
|
|
302
|
+
"placement"
|
|
205
303
|
],
|
|
206
304
|
"additionalProperties": false
|
|
207
305
|
}
|
|
@@ -8,12 +8,12 @@ import {
|
|
|
8
8
|
} from "./chunk-SCZZB7OE.js";
|
|
9
9
|
import {
|
|
10
10
|
clearifyPlugin
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-RW4IKHY4.js";
|
|
12
12
|
import {
|
|
13
13
|
loadUserConfig,
|
|
14
14
|
resolveConfig,
|
|
15
15
|
resolveSections
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-PGTNVDNF.js";
|
|
17
17
|
import {
|
|
18
18
|
buildSectionData
|
|
19
19
|
} from "./chunk-CTL4YL7C.js";
|
|
@@ -282,7 +282,7 @@ ${hasInternal ? `
|
|
|
282
282
|
`);
|
|
283
283
|
const secretMode = options.secretMode ?? "prompt";
|
|
284
284
|
if (options.hub) {
|
|
285
|
-
const { registerWithHub } = await import("./hub-register-
|
|
285
|
+
const { registerWithHub } = await import("./hub-register-QB73GUF2.js");
|
|
286
286
|
await registerWithHub({
|
|
287
287
|
projectName: detectProjectName(),
|
|
288
288
|
hubToken: options.hubToken,
|
|
@@ -298,7 +298,7 @@ ${hasInternal ? `
|
|
|
298
298
|
});
|
|
299
299
|
rl.close();
|
|
300
300
|
if (answer.toLowerCase() === "y") {
|
|
301
|
-
const { registerWithHub } = await import("./hub-register-
|
|
301
|
+
const { registerWithHub } = await import("./hub-register-QB73GUF2.js");
|
|
302
302
|
await registerWithHub({
|
|
303
303
|
projectName: detectProjectName(),
|
|
304
304
|
hubToken: options.hubToken,
|
|
@@ -42,28 +42,44 @@ var OpenAPIConfigSchema = z.object({
|
|
|
42
42
|
basePath: z.string().default("/api"),
|
|
43
43
|
generatePages: z.boolean().default(true)
|
|
44
44
|
}).optional();
|
|
45
|
+
var HubProjectSourceSchema = z.discriminatedUnion("kind", [
|
|
46
|
+
z.object({ kind: z.literal("none") }),
|
|
47
|
+
z.object({
|
|
48
|
+
kind: z.literal("git"),
|
|
49
|
+
repo: z.string(),
|
|
50
|
+
ref: z.string().optional(),
|
|
51
|
+
path: z.string().optional(),
|
|
52
|
+
sparse: z.boolean().optional()
|
|
53
|
+
}),
|
|
54
|
+
z.object({ kind: z.literal("url"), url: z.string() }),
|
|
55
|
+
z.object({ kind: z.literal("inline"), markdown: z.string() })
|
|
56
|
+
]);
|
|
57
|
+
var HubProjectPlacementSchema = z.discriminatedUnion("kind", [
|
|
58
|
+
z.object({ kind: z.literal("card"), href: z.string() }),
|
|
59
|
+
z.object({
|
|
60
|
+
kind: z.literal("tab"),
|
|
61
|
+
sections: z.union([z.literal("all"), z.literal("public"), z.array(z.string())]).optional()
|
|
62
|
+
}),
|
|
63
|
+
z.object({
|
|
64
|
+
kind: z.literal("nested"),
|
|
65
|
+
into: z.string(),
|
|
66
|
+
docsPath: z.string().optional(),
|
|
67
|
+
group: z.string().optional()
|
|
68
|
+
})
|
|
69
|
+
]);
|
|
45
70
|
var HubProjectSchema = z.object({
|
|
46
71
|
name: z.string(),
|
|
47
72
|
description: z.string(),
|
|
48
|
-
href: z.string().optional(),
|
|
49
73
|
repo: z.string().optional(),
|
|
50
74
|
status: z.enum(["active", "beta", "planned", "deprecated"]).default("active"),
|
|
51
75
|
icon: z.string().optional(),
|
|
52
76
|
tags: z.array(z.string()).optional(),
|
|
53
77
|
group: z.string().optional(),
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
embedSections: z.union([
|
|
57
|
-
z.literal("all"),
|
|
58
|
-
z.literal("public"),
|
|
59
|
-
z.array(z.string())
|
|
60
|
-
]).optional(),
|
|
61
|
-
injectInto: z.string().optional(),
|
|
62
|
-
docsPath: z.string().optional()
|
|
78
|
+
source: HubProjectSourceSchema,
|
|
79
|
+
placement: HubProjectPlacementSchema
|
|
63
80
|
});
|
|
64
81
|
var HubProjectPartialSchema = z.object({
|
|
65
82
|
description: z.string(),
|
|
66
|
-
href: z.string().optional(),
|
|
67
83
|
repo: z.string().optional(),
|
|
68
84
|
status: z.enum(["active", "beta", "planned", "deprecated"]).default("active"),
|
|
69
85
|
icon: z.string().optional(),
|
|
@@ -335,8 +351,14 @@ function buildStagingOverlay(stagingDir, originalDir, injections) {
|
|
|
335
351
|
symlinkSync(sourcePath, destPath);
|
|
336
352
|
}
|
|
337
353
|
}
|
|
354
|
+
function isReservedSource(source) {
|
|
355
|
+
return source.kind === "url" || source.kind === "inline";
|
|
356
|
+
}
|
|
338
357
|
async function scanHubProjects(config, root) {
|
|
339
|
-
|
|
358
|
+
const hasGitTabOrNested = config.hub?.projects?.some(
|
|
359
|
+
(p) => p.source.kind === "git" && (p.placement.kind === "tab" || p.placement.kind === "nested")
|
|
360
|
+
) ?? false;
|
|
361
|
+
if (!config.hub?.scan && !hasGitTabOrNested) {
|
|
340
362
|
return config;
|
|
341
363
|
}
|
|
342
364
|
const scannedProjects = [];
|
|
@@ -350,15 +372,17 @@ async function scanHubProjects(config, root) {
|
|
|
350
372
|
if (!childConfig.hubProject) continue;
|
|
351
373
|
const name = childConfig.name ?? basename(dirname(configPath));
|
|
352
374
|
const hp = childConfig.hubProject;
|
|
375
|
+
const href = childConfig.siteUrl;
|
|
353
376
|
scannedProjects.push({
|
|
354
377
|
name,
|
|
355
378
|
description: hp.description,
|
|
356
|
-
href: hp.href ?? childConfig.siteUrl,
|
|
357
379
|
repo: hp.repo,
|
|
358
380
|
status: hp.status,
|
|
359
381
|
icon: hp.icon,
|
|
360
382
|
tags: hp.tags,
|
|
361
|
-
group: hp.group
|
|
383
|
+
group: hp.group,
|
|
384
|
+
source: { kind: "none" },
|
|
385
|
+
placement: { kind: "card", href: href ?? "#" }
|
|
362
386
|
});
|
|
363
387
|
} catch {
|
|
364
388
|
}
|
|
@@ -369,18 +393,26 @@ async function scanHubProjects(config, root) {
|
|
|
369
393
|
...scannedProjects.filter((p) => !manualNames.has(p.name)),
|
|
370
394
|
...config.hub?.projects ?? []
|
|
371
395
|
];
|
|
396
|
+
for (const project of merged) {
|
|
397
|
+
if (isReservedSource(project.source)) {
|
|
398
|
+
throw new Error(
|
|
399
|
+
`Hub project "${project.name}": source.kind "${project.source.kind}" is reserved and not implemented. Use source.kind "none" or "git".`
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
372
403
|
const embedSections = [];
|
|
373
404
|
for (const project of merged) {
|
|
374
|
-
if (project.
|
|
405
|
+
if (project.source.kind !== "git" || project.placement.kind !== "tab") continue;
|
|
375
406
|
const cacheDir = config.hub?.cacheDir ? resolve(root, config.hub.cacheDir) : resolve(root, "node_modules/.cache/clearify-remote");
|
|
376
407
|
try {
|
|
377
408
|
const { resolveRemoteSource } = await import("./core/remote.js");
|
|
378
|
-
const {
|
|
409
|
+
const { kind: _kind, ...gitSource } = project.source;
|
|
410
|
+
const { localPath } = await resolveRemoteSource(gitSource, cacheDir);
|
|
379
411
|
const remoteConfig = await loadUserConfig(localPath);
|
|
380
412
|
const remoteSections = remoteConfig.sections ?? [
|
|
381
413
|
{ label: remoteConfig.name ?? project.name, docsDir: remoteConfig.docsDir ?? "./docs" }
|
|
382
414
|
];
|
|
383
|
-
const filter = project.
|
|
415
|
+
const filter = project.placement.sections ?? "public";
|
|
384
416
|
const filtered = remoteSections.filter((s) => {
|
|
385
417
|
if (filter === "all") return true;
|
|
386
418
|
if (filter === "public") return !s.draft;
|
|
@@ -400,46 +432,44 @@ async function scanHubProjects(config, root) {
|
|
|
400
432
|
exclude: s.exclude
|
|
401
433
|
});
|
|
402
434
|
}
|
|
403
|
-
console.log(` Hub
|
|
435
|
+
console.log(` Hub tab "${project.name}": ${filtered.length} sections pulled`);
|
|
404
436
|
} catch (err) {
|
|
405
437
|
console.warn(
|
|
406
|
-
`
|
|
438
|
+
` Hub tab "${project.name}" failed:`,
|
|
407
439
|
err instanceof Error ? err.message : err
|
|
408
440
|
);
|
|
409
441
|
}
|
|
410
442
|
}
|
|
411
443
|
const injectionsBySection = /* @__PURE__ */ new Map();
|
|
412
444
|
for (const project of merged) {
|
|
413
|
-
if (project.
|
|
445
|
+
if (project.source.kind !== "git" || project.placement.kind !== "nested") continue;
|
|
414
446
|
const cacheDir = config.hub?.cacheDir ? resolve(root, config.hub.cacheDir) : resolve(root, "node_modules/.cache/clearify-remote");
|
|
415
447
|
try {
|
|
416
448
|
const { resolveRemoteSource } = await import("./core/remote.js");
|
|
417
|
-
const {
|
|
418
|
-
const
|
|
449
|
+
const { kind: _kind, ...gitSource } = project.source;
|
|
450
|
+
const { localPath, cached } = await resolveRemoteSource(gitSource, cacheDir);
|
|
451
|
+
const docsPath = project.placement.docsPath ?? "docs";
|
|
419
452
|
const absoluteDocsPath = resolve(localPath, docsPath);
|
|
420
453
|
if (!existsSync(absoluteDocsPath)) {
|
|
421
454
|
console.warn(
|
|
422
|
-
`
|
|
455
|
+
` Hub nested "${project.name}": docs path "${docsPath}" not found in cloned repo.`
|
|
423
456
|
);
|
|
424
457
|
continue;
|
|
425
458
|
}
|
|
426
|
-
const targetLabel = project.
|
|
427
|
-
if (!targetLabel) {
|
|
428
|
-
console.warn(` \u26A0 Hub inject "${project.name}": missing injectInto field.`);
|
|
429
|
-
continue;
|
|
430
|
-
}
|
|
459
|
+
const targetLabel = project.placement.into;
|
|
431
460
|
if (!injectionsBySection.has(targetLabel)) {
|
|
432
461
|
injectionsBySection.set(targetLabel, /* @__PURE__ */ new Map());
|
|
433
462
|
}
|
|
434
463
|
const slugifiedName = slugify(project.name);
|
|
435
|
-
const
|
|
464
|
+
const groupSeg = project.placement.group;
|
|
465
|
+
const relPath = groupSeg ? `projects/${groupSeg}/${slugifiedName}` : `projects/${slugifiedName}`;
|
|
436
466
|
injectionsBySection.get(targetLabel).set(relPath, absoluteDocsPath);
|
|
437
467
|
console.log(
|
|
438
|
-
` Hub
|
|
468
|
+
` Hub nested "${project.name}": ${cached ? "cached" : "cloned"} to ${relPath}`
|
|
439
469
|
);
|
|
440
470
|
} catch (err) {
|
|
441
471
|
console.warn(
|
|
442
|
-
`
|
|
472
|
+
` Hub nested "${project.name}" failed:`,
|
|
443
473
|
err instanceof Error ? err.message : err
|
|
444
474
|
);
|
|
445
475
|
}
|
|
@@ -37,7 +37,6 @@ declare const ClearifyDataSchema: z.ZodObject<{
|
|
|
37
37
|
projects: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
38
38
|
name: z.ZodString;
|
|
39
39
|
description: z.ZodString;
|
|
40
|
-
href: z.ZodOptional<z.ZodString>;
|
|
41
40
|
repo: z.ZodOptional<z.ZodString>;
|
|
42
41
|
status: z.ZodDefault<z.ZodEnum<{
|
|
43
42
|
active: "active";
|
|
@@ -48,20 +47,33 @@ declare const ClearifyDataSchema: z.ZodObject<{
|
|
|
48
47
|
icon: z.ZodOptional<z.ZodString>;
|
|
49
48
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
50
49
|
group: z.ZodOptional<z.ZodString>;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}>>;
|
|
56
|
-
git: z.ZodOptional<z.ZodObject<{
|
|
50
|
+
source: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
51
|
+
kind: z.ZodLiteral<"none">;
|
|
52
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
53
|
+
kind: z.ZodLiteral<"git">;
|
|
57
54
|
repo: z.ZodString;
|
|
58
55
|
ref: z.ZodOptional<z.ZodString>;
|
|
59
56
|
path: z.ZodOptional<z.ZodString>;
|
|
60
57
|
sparse: z.ZodOptional<z.ZodBoolean>;
|
|
61
|
-
}, z.core.$strip
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
59
|
+
kind: z.ZodLiteral<"url">;
|
|
60
|
+
url: z.ZodString;
|
|
61
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
62
|
+
kind: z.ZodLiteral<"inline">;
|
|
63
|
+
markdown: z.ZodString;
|
|
64
|
+
}, z.core.$strip>], "kind">;
|
|
65
|
+
placement: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
66
|
+
kind: z.ZodLiteral<"card">;
|
|
67
|
+
href: z.ZodString;
|
|
68
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
69
|
+
kind: z.ZodLiteral<"tab">;
|
|
70
|
+
sections: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodLiteral<"public">, z.ZodArray<z.ZodString>]>>;
|
|
71
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
72
|
+
kind: z.ZodLiteral<"nested">;
|
|
73
|
+
into: z.ZodString;
|
|
74
|
+
docsPath: z.ZodOptional<z.ZodString>;
|
|
75
|
+
group: z.ZodOptional<z.ZodString>;
|
|
76
|
+
}, z.core.$strip>], "kind">;
|
|
65
77
|
}, z.core.$strip>>>;
|
|
66
78
|
scan: z.ZodOptional<z.ZodString>;
|
|
67
79
|
cacheDir: z.ZodOptional<z.ZodString>;
|
package/dist/node/core/config.js
CHANGED
|
@@ -79,6 +79,7 @@ async function secretExists(token, owner, repo, name) {
|
|
|
79
79
|
const res = await ghApiFetch(url, { headers: { Authorization: `token ${token}` } });
|
|
80
80
|
return res.status === 200;
|
|
81
81
|
}
|
|
82
|
+
var __internal = { encryptSecret };
|
|
82
83
|
async function promptSecretMode() {
|
|
83
84
|
const { createInterface } = await import("readline");
|
|
84
85
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -212,12 +213,13 @@ async function updateHubRegistry(token, hub, project) {
|
|
|
212
213
|
name: project.name,
|
|
213
214
|
description: project.description,
|
|
214
215
|
status: "active",
|
|
215
|
-
|
|
216
|
-
|
|
216
|
+
source: {
|
|
217
|
+
kind: "git",
|
|
217
218
|
repo: project.gitRepoUrl,
|
|
218
219
|
ref: project.gitRef,
|
|
219
220
|
path: project.docsPath
|
|
220
|
-
}
|
|
221
|
+
},
|
|
222
|
+
placement: { kind: "tab" }
|
|
221
223
|
};
|
|
222
224
|
if (existingIdx >= 0) {
|
|
223
225
|
currentData.hub.projects[existingIdx] = entry;
|
|
@@ -396,5 +398,6 @@ function printSuccessFooter(currentRepo) {
|
|
|
396
398
|
`);
|
|
397
399
|
}
|
|
398
400
|
export {
|
|
401
|
+
__internal,
|
|
399
402
|
registerWithHub
|
|
400
403
|
};
|
package/dist/node/node/build.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildSite
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-BCEGDIR6.js";
|
|
4
4
|
import "../chunk-PRTER35L.js";
|
|
5
5
|
import "../chunk-SCZZB7OE.js";
|
|
6
|
-
import "../chunk-
|
|
6
|
+
import "../chunk-RW4IKHY4.js";
|
|
7
7
|
import "../chunk-B2Q23JW3.js";
|
|
8
|
-
import "../chunk-
|
|
8
|
+
import "../chunk-PGTNVDNF.js";
|
|
9
9
|
import "../chunk-AJBCHKBP.js";
|
|
10
10
|
import "../chunk-V7LLYIRO.js";
|
|
11
11
|
import "../chunk-CTL4YL7C.js";
|
package/dist/node/node/check.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
checkFrontmatter,
|
|
3
3
|
checkLinks
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-CK2SLBAG.js";
|
|
5
|
+
import "../chunk-PGTNVDNF.js";
|
|
6
6
|
import "../chunk-AJBCHKBP.js";
|
|
7
7
|
import "../chunk-V7LLYIRO.js";
|
|
8
8
|
import "../chunk-CTL4YL7C.js";
|
package/dist/node/node/index.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildSite
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-BCEGDIR6.js";
|
|
4
4
|
import {
|
|
5
5
|
remarkMermaidToComponent
|
|
6
6
|
} from "../chunk-PRTER35L.js";
|
|
7
7
|
import "../chunk-SCZZB7OE.js";
|
|
8
8
|
import {
|
|
9
9
|
clearifyPlugin
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-RW4IKHY4.js";
|
|
11
11
|
import "../chunk-B2Q23JW3.js";
|
|
12
12
|
import {
|
|
13
13
|
init
|
|
14
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-GPBEDIOY.js";
|
|
15
15
|
import {
|
|
16
16
|
checkFrontmatter,
|
|
17
17
|
checkLinks
|
|
18
|
-
} from "../chunk-
|
|
18
|
+
} from "../chunk-CK2SLBAG.js";
|
|
19
19
|
import {
|
|
20
20
|
loadUserConfig,
|
|
21
21
|
resolveConfig
|
|
22
|
-
} from "../chunk-
|
|
22
|
+
} from "../chunk-PGTNVDNF.js";
|
|
23
23
|
import "../chunk-AJBCHKBP.js";
|
|
24
24
|
import "../chunk-V7LLYIRO.js";
|
|
25
25
|
import "../chunk-CTL4YL7C.js";
|
package/dist/node/node/init.js
CHANGED
|
@@ -58,7 +58,7 @@ interface ClearifyConfig {
|
|
|
58
58
|
[key: string]: string | undefined;
|
|
59
59
|
};
|
|
60
60
|
hub?: HubConfig;
|
|
61
|
-
hubProject?:
|
|
61
|
+
hubProject?: HubProjectPartial;
|
|
62
62
|
customCss?: string;
|
|
63
63
|
headTags?: string[];
|
|
64
64
|
includeReadme?: boolean;
|
|
@@ -92,10 +92,54 @@ interface RouteEntry {
|
|
|
92
92
|
componentPath?: string;
|
|
93
93
|
redirectTo?: string;
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Where a hub project's content comes from.
|
|
97
|
+
*
|
|
98
|
+
* `none`: no content pulled, card links out only.
|
|
99
|
+
* `git`: sparse-clone a repo subdirectory.
|
|
100
|
+
* `url`: reserved for future remote-docs fetch, not implemented.
|
|
101
|
+
* `inline`: reserved for future inline markdown, not implemented.
|
|
102
|
+
*/
|
|
103
|
+
type HubProjectSource = {
|
|
104
|
+
kind: 'none';
|
|
105
|
+
} | {
|
|
106
|
+
kind: 'git';
|
|
107
|
+
repo: string;
|
|
108
|
+
/** branch, tag, or SHA. Default: 'main' */
|
|
109
|
+
ref?: string;
|
|
110
|
+
/** subdirectory to sparse-checkout. Default: 'docs/public' */
|
|
111
|
+
path?: string;
|
|
112
|
+
/** defaults to true when path is set */
|
|
113
|
+
sparse?: boolean;
|
|
114
|
+
} | {
|
|
115
|
+
kind: 'url';
|
|
116
|
+
url: string;
|
|
117
|
+
} | {
|
|
118
|
+
kind: 'inline';
|
|
119
|
+
markdown: string;
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* How a hub project shows up in the hub UI.
|
|
123
|
+
*
|
|
124
|
+
* `card`: grid entry that links to an external href.
|
|
125
|
+
* `tab`: full tab in the hub nav, rendering the cloned sections.
|
|
126
|
+
* `nested`: folded into an existing hub section as a subfolder.
|
|
127
|
+
*/
|
|
128
|
+
type HubProjectPlacement = {
|
|
129
|
+
kind: 'card';
|
|
130
|
+
href: string;
|
|
131
|
+
} | {
|
|
132
|
+
kind: 'tab';
|
|
133
|
+
sections?: 'all' | 'public' | string[];
|
|
134
|
+
} | {
|
|
135
|
+
kind: 'nested';
|
|
136
|
+
into: string;
|
|
137
|
+
docsPath?: string;
|
|
138
|
+
group?: string;
|
|
139
|
+
};
|
|
95
140
|
interface HubProject {
|
|
96
141
|
name: string;
|
|
97
142
|
description: string;
|
|
98
|
-
href?: string;
|
|
99
143
|
repo?: string;
|
|
100
144
|
status?: 'active' | 'beta' | 'planned' | 'deprecated';
|
|
101
145
|
icon?: string;
|
|
@@ -103,11 +147,25 @@ interface HubProject {
|
|
|
103
147
|
group?: string;
|
|
104
148
|
hubUrl?: string;
|
|
105
149
|
hubName?: string;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
150
|
+
source: HubProjectSource;
|
|
151
|
+
placement: HubProjectPlacement;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The subset of HubProject fields a sub-project can declare about itself in
|
|
155
|
+
* `clearify.config.ts`. The hub scanner composes this with the sub-project's
|
|
156
|
+
* `siteUrl` and assigns `source` + `placement` on its own side.
|
|
157
|
+
*/
|
|
158
|
+
interface HubProjectPartial {
|
|
159
|
+
description: string;
|
|
160
|
+
repo?: string;
|
|
161
|
+
status?: 'active' | 'beta' | 'planned' | 'deprecated';
|
|
162
|
+
icon?: string;
|
|
163
|
+
tags?: string[];
|
|
164
|
+
group?: string;
|
|
165
|
+
/** URL of the parent hub site, used by the Sidebar back-link. */
|
|
166
|
+
hubUrl?: string;
|
|
167
|
+
/** Display name for the Sidebar back-link. Default: 'Hub'. */
|
|
168
|
+
hubName?: string;
|
|
111
169
|
}
|
|
112
170
|
interface HubConfig {
|
|
113
171
|
projects: HubProject[];
|
|
@@ -135,4 +193,4 @@ interface ClearifyDataConfig {
|
|
|
135
193
|
}
|
|
136
194
|
declare function defineConfig(config: Partial<ClearifyConfig>): Partial<ClearifyConfig>;
|
|
137
195
|
|
|
138
|
-
export { type ClearifyConfig, type ClearifyDataConfig, type DocCategory, type HubConfig, type HubProject, type NavigationItem, type PageFrontmatter, type RemoteGitSource, type ResolvedSection, type RouteEntry, type SectionConfig, type SectionNavigation, defineConfig };
|
|
196
|
+
export { type ClearifyConfig, type ClearifyDataConfig, type DocCategory, type HubConfig, type HubProject, type HubProjectPartial, type HubProjectPlacement, type HubProjectSource, type NavigationItem, type PageFrontmatter, type RemoteGitSource, type ResolvedSection, type RouteEntry, type SectionConfig, type SectionNavigation, defineConfig };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
clearifyPlugin
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-RW4IKHY4.js";
|
|
4
4
|
import "../chunk-B2Q23JW3.js";
|
|
5
|
-
import "../chunk-
|
|
5
|
+
import "../chunk-PGTNVDNF.js";
|
|
6
6
|
import "../chunk-AJBCHKBP.js";
|
|
7
7
|
import "../chunk-V7LLYIRO.js";
|
|
8
8
|
import "../chunk-CTL4YL7C.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marlinjai/clearify",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "An open-source documentation site generator. Turn markdown into beautiful docs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"postbuild": "node scripts/generate-schema.mjs",
|
|
29
29
|
"dev:self": "node bin/clearify.js dev",
|
|
30
30
|
"prepare": "husky",
|
|
31
|
-
"release": "semantic-release"
|
|
31
|
+
"release": "semantic-release",
|
|
32
|
+
"test": "vitest run"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
34
35
|
"@inquirer/password": "^5.0.11",
|
|
@@ -82,7 +83,8 @@
|
|
|
82
83
|
"puppeteer": "^24.37.2",
|
|
83
84
|
"semantic-release": "^25.0.3",
|
|
84
85
|
"tsup": "^8.3.6",
|
|
85
|
-
"typescript": "^5.7.3"
|
|
86
|
+
"typescript": "^5.7.3",
|
|
87
|
+
"vitest": "^4.1.4"
|
|
86
88
|
},
|
|
87
89
|
"packageManager": "pnpm@9.15.0",
|
|
88
90
|
"license": "MIT",
|
package/src/theme/Sidebar.tsx
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import React, { useState, useEffect, useRef } from 'react';
|
|
2
2
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
|
-
import type { NavigationItem, SectionNavigation,
|
|
4
|
+
import type { NavigationItem, SectionNavigation, HubProjectPartial } from '../types/index.js';
|
|
5
5
|
|
|
6
6
|
interface SidebarProps {
|
|
7
7
|
sections: SectionNavigation[];
|
|
8
8
|
open: boolean;
|
|
9
9
|
onClose: () => void;
|
|
10
|
-
hubProject?:
|
|
10
|
+
hubProject?: HubProjectPartial;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function findActiveSection(pathname: string, sections: SectionNavigation[]): string {
|
|
@@ -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}
|