@clef-sh/ui 0.1.13-beta.88

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 (70) hide show
  1. package/README.md +38 -0
  2. package/dist/client/assets/index-CVpAmirt.js +26 -0
  3. package/dist/client/favicon-96x96.png +0 -0
  4. package/dist/client/favicon.ico +0 -0
  5. package/dist/client/favicon.svg +16 -0
  6. package/dist/client/index.html +50 -0
  7. package/dist/client-lib/api.d.ts +3 -0
  8. package/dist/client-lib/api.d.ts.map +1 -0
  9. package/dist/client-lib/components/Button.d.ts +10 -0
  10. package/dist/client-lib/components/Button.d.ts.map +1 -0
  11. package/dist/client-lib/components/CopyButton.d.ts +6 -0
  12. package/dist/client-lib/components/CopyButton.d.ts.map +1 -0
  13. package/dist/client-lib/components/EnvBadge.d.ts +7 -0
  14. package/dist/client-lib/components/EnvBadge.d.ts.map +1 -0
  15. package/dist/client-lib/components/MatrixGrid.d.ts +13 -0
  16. package/dist/client-lib/components/MatrixGrid.d.ts.map +1 -0
  17. package/dist/client-lib/components/Sidebar.d.ts +16 -0
  18. package/dist/client-lib/components/Sidebar.d.ts.map +1 -0
  19. package/dist/client-lib/components/StatusDot.d.ts +6 -0
  20. package/dist/client-lib/components/StatusDot.d.ts.map +1 -0
  21. package/dist/client-lib/components/TopBar.d.ts +9 -0
  22. package/dist/client-lib/components/TopBar.d.ts.map +1 -0
  23. package/dist/client-lib/index.d.ts +12 -0
  24. package/dist/client-lib/index.d.ts.map +1 -0
  25. package/dist/client-lib/theme.d.ts +42 -0
  26. package/dist/client-lib/theme.d.ts.map +1 -0
  27. package/dist/server/api.d.ts +11 -0
  28. package/dist/server/api.d.ts.map +1 -0
  29. package/dist/server/api.js +1020 -0
  30. package/dist/server/api.js.map +1 -0
  31. package/dist/server/index.d.ts +12 -0
  32. package/dist/server/index.d.ts.map +1 -0
  33. package/dist/server/index.js +231 -0
  34. package/dist/server/index.js.map +1 -0
  35. package/package.json +74 -0
  36. package/src/client/App.tsx +205 -0
  37. package/src/client/api.test.tsx +94 -0
  38. package/src/client/api.ts +30 -0
  39. package/src/client/components/Button.tsx +52 -0
  40. package/src/client/components/CopyButton.test.tsx +43 -0
  41. package/src/client/components/CopyButton.tsx +36 -0
  42. package/src/client/components/EnvBadge.tsx +32 -0
  43. package/src/client/components/MatrixGrid.tsx +265 -0
  44. package/src/client/components/Sidebar.tsx +337 -0
  45. package/src/client/components/StatusDot.tsx +30 -0
  46. package/src/client/components/TopBar.tsx +50 -0
  47. package/src/client/index.html +50 -0
  48. package/src/client/index.ts +18 -0
  49. package/src/client/main.tsx +15 -0
  50. package/src/client/public/favicon-96x96.png +0 -0
  51. package/src/client/public/favicon.ico +0 -0
  52. package/src/client/public/favicon.svg +16 -0
  53. package/src/client/screens/BackendScreen.test.tsx +611 -0
  54. package/src/client/screens/BackendScreen.tsx +836 -0
  55. package/src/client/screens/DiffView.test.tsx +130 -0
  56. package/src/client/screens/DiffView.tsx +547 -0
  57. package/src/client/screens/GitLogView.test.tsx +113 -0
  58. package/src/client/screens/GitLogView.tsx +192 -0
  59. package/src/client/screens/ImportScreen.tsx +710 -0
  60. package/src/client/screens/LintView.test.tsx +143 -0
  61. package/src/client/screens/LintView.tsx +589 -0
  62. package/src/client/screens/MatrixView.test.tsx +138 -0
  63. package/src/client/screens/MatrixView.tsx +143 -0
  64. package/src/client/screens/NamespaceEditor.test.tsx +694 -0
  65. package/src/client/screens/NamespaceEditor.tsx +1122 -0
  66. package/src/client/screens/RecipientsScreen.tsx +696 -0
  67. package/src/client/screens/ScanScreen.test.tsx +323 -0
  68. package/src/client/screens/ScanScreen.tsx +523 -0
  69. package/src/client/screens/ServiceIdentitiesScreen.tsx +1398 -0
  70. package/src/client/theme.ts +48 -0
@@ -0,0 +1,589 @@
1
+ import React, { useState, useEffect, useCallback } from "react";
2
+ import { theme, SEVERITY_META, CATEGORY_META } from "../theme";
3
+ import { apiFetch } from "../api";
4
+ import { TopBar } from "../components/TopBar";
5
+ import { Button } from "../components/Button";
6
+ import { EnvBadge } from "../components/EnvBadge";
7
+ import { CopyButton } from "../components/CopyButton";
8
+ import type { ViewName } from "../components/Sidebar";
9
+ import type { LintResult, LintIssue } from "@clef-sh/core";
10
+
11
+ interface LintViewProps {
12
+ setView: (view: ViewName) => void;
13
+ setNs: (ns: string) => void;
14
+ }
15
+
16
+ export function LintView({ setView, setNs }: LintViewProps) {
17
+ const [filter, setFilter] = useState("all");
18
+ const [dismissed, setDismissed] = useState<number[]>([]);
19
+ const [lintResult, setLintResult] = useState<LintResult | null>(null);
20
+ const [loading, setLoading] = useState(false);
21
+
22
+ const loadLint = useCallback(async () => {
23
+ setLoading(true);
24
+ try {
25
+ const res = await apiFetch("/api/lint");
26
+ if (res.ok) {
27
+ setLintResult(await res.json());
28
+ }
29
+ } catch {
30
+ // Silently fail — user can retry
31
+ } finally {
32
+ setLoading(false);
33
+ }
34
+ }, []);
35
+
36
+ useEffect(() => {
37
+ loadLint();
38
+ }, [loadLint]);
39
+
40
+ const issues = lintResult?.issues ?? [];
41
+ const fileCount = lintResult?.fileCount ?? 0;
42
+
43
+ const errors = issues.filter((i) => i.severity === "error");
44
+ const warnings = issues.filter((i) => i.severity === "warning");
45
+ const infos = issues.filter((i) => i.severity === "info");
46
+
47
+ const visible = issues
48
+ .map((issue, idx) => ({ ...issue, _idx: idx }))
49
+ .filter((i) => !dismissed.includes(i._idx))
50
+ .filter((i) => filter === "all" || i.severity === filter || i.category === filter);
51
+
52
+ const handleNavigate = (issue: LintIssue) => {
53
+ if (issue.file) {
54
+ const parts = issue.file.split("/");
55
+ const nsName = parts[parts.length - 2] ?? parts[0];
56
+ setNs(nsName);
57
+ setView("editor");
58
+ }
59
+ };
60
+
61
+ const allClear = visible.length === 0;
62
+
63
+ return (
64
+ <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
65
+ <TopBar
66
+ title="Lint"
67
+ subtitle={"clef lint \u2014 full repo health check"}
68
+ actions={
69
+ <>
70
+ <Button onClick={loadLint}>{"\u21BB"} Re-run</Button>
71
+ {errors.length === 0 && <Button variant="primary">All clear {"\u2014"} commit</Button>}
72
+ </>
73
+ }
74
+ />
75
+
76
+ {/* Summary bar — only shown when there are issues */}
77
+ {!loading && !allClear && (
78
+ <div
79
+ style={{
80
+ padding: "14px 24px",
81
+ background: "#0D0F14",
82
+ borderBottom: `1px solid ${theme.border}`,
83
+ display: "flex",
84
+ alignItems: "center",
85
+ gap: 10,
86
+ flexWrap: "wrap",
87
+ }}
88
+ >
89
+ {/* Severity filters */}
90
+ {[
91
+ {
92
+ key: "all",
93
+ label: "All issues",
94
+ count: issues.length,
95
+ color: theme.textMuted,
96
+ },
97
+ {
98
+ key: "error",
99
+ label: "Errors",
100
+ count: errors.length,
101
+ color: theme.red,
102
+ },
103
+ {
104
+ key: "warning",
105
+ label: "Warnings",
106
+ count: warnings.length,
107
+ color: theme.yellow,
108
+ },
109
+ {
110
+ key: "info",
111
+ label: "Info",
112
+ count: infos.length,
113
+ color: theme.blue,
114
+ },
115
+ ].map((f) => (
116
+ <button
117
+ key={f.key}
118
+ data-testid={`filter-${f.key}`}
119
+ onClick={() => setFilter(f.key)}
120
+ style={{
121
+ display: "flex",
122
+ alignItems: "center",
123
+ gap: 6,
124
+ padding: "5px 12px",
125
+ borderRadius: 20,
126
+ cursor: "pointer",
127
+ fontFamily: theme.sans,
128
+ fontSize: 12,
129
+ fontWeight: filter === f.key ? 600 : 400,
130
+ color: filter === f.key ? f.color : theme.textMuted,
131
+ background: filter === f.key ? `${f.color}18` : "transparent",
132
+ border: `1px solid ${filter === f.key ? `${f.color}55` : theme.border}`,
133
+ transition: "all 0.12s",
134
+ }}
135
+ >
136
+ <span
137
+ style={{
138
+ fontFamily: theme.mono,
139
+ fontSize: 11,
140
+ fontWeight: 700,
141
+ color: f.color,
142
+ }}
143
+ >
144
+ {f.count}
145
+ </span>
146
+ {f.label}
147
+ </button>
148
+ ))}
149
+
150
+ <div style={{ flex: 1 }} />
151
+
152
+ {/* Category filters */}
153
+ {(["matrix", "schema", "sops"] as const).map((cat) => {
154
+ const m = CATEGORY_META[cat];
155
+ return (
156
+ <button
157
+ key={cat}
158
+ onClick={() => setFilter(filter === cat ? "all" : cat)}
159
+ style={{
160
+ padding: "4px 10px",
161
+ borderRadius: 4,
162
+ cursor: "pointer",
163
+ fontFamily: theme.mono,
164
+ fontSize: 10,
165
+ fontWeight: 600,
166
+ color: filter === cat ? m.color : theme.textDim,
167
+ background: filter === cat ? `${m.color}18` : "transparent",
168
+ border: `1px solid ${filter === cat ? `${m.color}55` : theme.borderLight}`,
169
+ letterSpacing: "0.06em",
170
+ }}
171
+ >
172
+ {m.label}
173
+ </button>
174
+ );
175
+ })}
176
+ </div>
177
+ )}
178
+
179
+ <div style={{ flex: 1, overflow: "auto", padding: 24 }}>
180
+ {loading && (
181
+ <>
182
+ <style>{`
183
+ @keyframes clef-scan-line {
184
+ 0% { transform: scaleX(0); opacity: 0; }
185
+ 10% { opacity: 1; }
186
+ 50% { transform: scaleX(1); opacity: 1; }
187
+ 80% { transform: scaleX(1); opacity: 0.3; }
188
+ 100% { transform: scaleX(0); opacity: 0; }
189
+ }
190
+ @keyframes clef-scan-glow {
191
+ 0%, 100% { opacity: 0.4; }
192
+ 50% { opacity: 1; }
193
+ }
194
+ `}</style>
195
+ <div
196
+ style={{
197
+ display: "flex",
198
+ alignItems: "center",
199
+ justifyContent: "center",
200
+ padding: "48px 24px",
201
+ }}
202
+ >
203
+ <div
204
+ style={{
205
+ background: theme.surface,
206
+ border: `1px solid ${theme.border}`,
207
+ borderRadius: 10,
208
+ padding: "28px 40px",
209
+ textAlign: "center",
210
+ minWidth: 200,
211
+ }}
212
+ >
213
+ <div style={{ marginBottom: 16, display: "flex", flexDirection: "column", gap: 6 }}>
214
+ {[0, 0.3, 0.6].map((delay, i) => (
215
+ <div
216
+ key={i}
217
+ style={{
218
+ height: 3,
219
+ borderRadius: 2,
220
+ background: theme.accent,
221
+ transformOrigin: "left",
222
+ animation: `clef-scan-line 1.8s ease-in-out ${delay}s infinite`,
223
+ opacity: 0,
224
+ width: [120, 90, 105][i],
225
+ }}
226
+ />
227
+ ))}
228
+ </div>
229
+ <div
230
+ style={{
231
+ fontFamily: theme.mono,
232
+ fontSize: 11,
233
+ color: theme.textMuted,
234
+ animation: "clef-scan-glow 1.8s ease-in-out infinite",
235
+ }}
236
+ >
237
+ Linting...
238
+ </div>
239
+ </div>
240
+ </div>
241
+ </>
242
+ )}
243
+
244
+ {!loading && allClear && (
245
+ <div
246
+ data-testid="all-clear"
247
+ style={{
248
+ display: "flex",
249
+ flexDirection: "column",
250
+ alignItems: "center",
251
+ justifyContent: "center",
252
+ gap: 14,
253
+ padding: "60px 0",
254
+ }}
255
+ >
256
+ <div
257
+ style={{
258
+ width: 56,
259
+ height: 56,
260
+ borderRadius: "50%",
261
+ background: theme.greenDim,
262
+ border: `1px solid ${theme.green}44`,
263
+ display: "flex",
264
+ alignItems: "center",
265
+ justifyContent: "center",
266
+ fontSize: 24,
267
+ }}
268
+ >
269
+ {"\u2713"}
270
+ </div>
271
+ <div
272
+ style={{
273
+ fontFamily: theme.sans,
274
+ fontWeight: 600,
275
+ fontSize: 16,
276
+ color: theme.green,
277
+ }}
278
+ >
279
+ All clear
280
+ </div>
281
+ <div
282
+ style={{
283
+ fontFamily: theme.mono,
284
+ fontSize: 12,
285
+ color: theme.textMuted,
286
+ }}
287
+ >
288
+ No issues found across {fileCount} files
289
+ </div>
290
+ </div>
291
+ )}
292
+
293
+ {/* Grouped issues */}
294
+ {!loading &&
295
+ !allClear &&
296
+ (["error", "warning", "info"] as const).map((sev) => {
297
+ const group = visible.filter((i) => i.severity === sev);
298
+ if (!group.length) return null;
299
+ const meta = SEVERITY_META[sev];
300
+
301
+ return (
302
+ <div key={sev} style={{ marginBottom: 24 }}>
303
+ {/* Group header */}
304
+ <div
305
+ style={{
306
+ display: "flex",
307
+ alignItems: "center",
308
+ gap: 10,
309
+ marginBottom: 10,
310
+ }}
311
+ >
312
+ <div
313
+ style={{
314
+ width: 22,
315
+ height: 22,
316
+ borderRadius: "50%",
317
+ background: meta.bg,
318
+ border: `1px solid ${meta.color}44`,
319
+ display: "flex",
320
+ alignItems: "center",
321
+ justifyContent: "center",
322
+ fontFamily: theme.mono,
323
+ fontSize: 11,
324
+ fontWeight: 700,
325
+ color: meta.color,
326
+ }}
327
+ >
328
+ {meta.icon}
329
+ </div>
330
+ <span
331
+ style={{
332
+ fontFamily: theme.sans,
333
+ fontWeight: 600,
334
+ fontSize: 13,
335
+ color: meta.color,
336
+ }}
337
+ >
338
+ {meta.label}s
339
+ </span>
340
+ <span
341
+ style={{
342
+ fontFamily: theme.mono,
343
+ fontSize: 10,
344
+ color: meta.color,
345
+ background: meta.bg,
346
+ border: `1px solid ${meta.color}33`,
347
+ borderRadius: 10,
348
+ padding: "1px 8px",
349
+ }}
350
+ >
351
+ {group.length}
352
+ </span>
353
+ </div>
354
+
355
+ {/* Issue cards */}
356
+ <div
357
+ style={{
358
+ background: theme.surface,
359
+ border: `1px solid ${theme.border}`,
360
+ borderRadius: 10,
361
+ overflow: "hidden",
362
+ }}
363
+ >
364
+ {group.map((issue, i) => {
365
+ const catMeta = CATEGORY_META[issue.category] ?? {
366
+ label: issue.category,
367
+ color: theme.textMuted,
368
+ };
369
+ const fileParts = issue.file?.split("/") ?? [];
370
+ const envName =
371
+ fileParts.length >= 2
372
+ ? fileParts[fileParts.length - 1]?.replace(".enc.yaml", "")
373
+ : undefined;
374
+
375
+ return (
376
+ <div
377
+ key={issue._idx}
378
+ style={{
379
+ display: "flex",
380
+ alignItems: "flex-start",
381
+ borderBottom: i < group.length - 1 ? `1px solid ${theme.border}` : "none",
382
+ borderLeft: `3px solid ${meta.color}66`,
383
+ transition: "background 0.1s",
384
+ padding: "14px 18px",
385
+ gap: 14,
386
+ }}
387
+ >
388
+ {/* Category badge */}
389
+ <div style={{ flexShrink: 0, paddingTop: 2 }}>
390
+ <span
391
+ style={{
392
+ fontFamily: theme.mono,
393
+ fontSize: 9,
394
+ fontWeight: 700,
395
+ color: catMeta.color,
396
+ background: `${catMeta.color}18`,
397
+ border: `1px solid ${catMeta.color}33`,
398
+ borderRadius: 3,
399
+ padding: "2px 6px",
400
+ letterSpacing: "0.07em",
401
+ textTransform: "uppercase",
402
+ }}
403
+ >
404
+ {catMeta.label}
405
+ </span>
406
+ </div>
407
+
408
+ {/* Main content */}
409
+ <div style={{ flex: 1, minWidth: 0 }}>
410
+ {/* File + key */}
411
+ <div
412
+ style={{
413
+ display: "flex",
414
+ alignItems: "center",
415
+ gap: 8,
416
+ marginBottom: 4,
417
+ flexWrap: "wrap",
418
+ }}
419
+ >
420
+ <span
421
+ data-testid={`file-ref-${issue.file}`}
422
+ role="link"
423
+ tabIndex={0}
424
+ onClick={() => handleNavigate(issue)}
425
+ onKeyDown={(e) => {
426
+ if (e.key === "Enter") handleNavigate(issue);
427
+ }}
428
+ style={{
429
+ fontFamily: theme.mono,
430
+ fontSize: 12,
431
+ fontWeight: 600,
432
+ color: theme.accent,
433
+ cursor: issue.file ? "pointer" : "default",
434
+ textDecoration: issue.file ? "underline" : "none",
435
+ textDecorationColor: `${theme.accent}55`,
436
+ textDecorationStyle: "dotted",
437
+ }}
438
+ >
439
+ {issue.file}
440
+ </span>
441
+ {issue.key && (
442
+ <>
443
+ <span
444
+ style={{
445
+ fontFamily: theme.mono,
446
+ fontSize: 11,
447
+ color: theme.textDim,
448
+ }}
449
+ >
450
+ {"\u2192"}
451
+ </span>
452
+ <span
453
+ style={{
454
+ fontFamily: theme.mono,
455
+ fontSize: 11,
456
+ color: theme.text,
457
+ background: "#1A1F2B",
458
+ border: `1px solid ${theme.borderLight}`,
459
+ borderRadius: 3,
460
+ padding: "1px 7px",
461
+ }}
462
+ >
463
+ {issue.key}
464
+ </span>
465
+ </>
466
+ )}
467
+ {envName && <EnvBadge env={envName} small />}
468
+ </div>
469
+
470
+ {/* Message */}
471
+ <div
472
+ style={{
473
+ fontFamily: theme.sans,
474
+ fontSize: 12,
475
+ color: theme.textMuted,
476
+ marginBottom: issue.fixCommand ? 10 : 0,
477
+ }}
478
+ >
479
+ {issue.message}
480
+ </div>
481
+
482
+ {/* Fix command */}
483
+ {issue.fixCommand && (
484
+ <div
485
+ style={{
486
+ display: "flex",
487
+ alignItems: "center",
488
+ gap: 8,
489
+ background: "#0D0F14",
490
+ border: `1px solid ${theme.borderLight}`,
491
+ borderRadius: 6,
492
+ padding: "6px 10px",
493
+ width: "fit-content",
494
+ }}
495
+ >
496
+ <span
497
+ style={{
498
+ fontFamily: theme.mono,
499
+ fontSize: 11,
500
+ color: theme.green,
501
+ }}
502
+ >
503
+ $
504
+ </span>
505
+ <span
506
+ style={{
507
+ fontFamily: theme.mono,
508
+ fontSize: 11,
509
+ color: theme.text,
510
+ }}
511
+ >
512
+ {issue.fixCommand}
513
+ </span>
514
+ <CopyButton text={issue.fixCommand} />
515
+ </div>
516
+ )}
517
+ </div>
518
+
519
+ {/* Dismiss */}
520
+ <button
521
+ onClick={() => setDismissed((d) => [...d, issue._idx])}
522
+ title="Dismiss"
523
+ aria-label="Dismiss issue"
524
+ style={{
525
+ background: "none",
526
+ border: "none",
527
+ cursor: "pointer",
528
+ color: theme.textDim,
529
+ fontSize: 16,
530
+ flexShrink: 0,
531
+ padding: "0 4px",
532
+ lineHeight: 1,
533
+ transition: "color 0.1s",
534
+ }}
535
+ >
536
+ {"\u00D7"}
537
+ </button>
538
+ </div>
539
+ );
540
+ })}
541
+ </div>
542
+ </div>
543
+ );
544
+ })}
545
+
546
+ {/* Footer hint */}
547
+ {!loading && !allClear && (
548
+ <div
549
+ style={{
550
+ marginTop: 8,
551
+ padding: "12px 16px",
552
+ background: theme.surface,
553
+ border: `1px solid ${theme.border}`,
554
+ borderRadius: 8,
555
+ display: "flex",
556
+ alignItems: "center",
557
+ gap: 12,
558
+ }}
559
+ >
560
+ <span style={{ fontSize: 14 }}>{"\uD83D\uDCA1"}</span>
561
+ <span
562
+ style={{
563
+ fontFamily: theme.sans,
564
+ fontSize: 12,
565
+ color: theme.textMuted,
566
+ }}
567
+ >
568
+ Fix all errors before committing. Warnings and info items won't block commits but
569
+ should be reviewed. Run{" "}
570
+ <code
571
+ style={{
572
+ fontFamily: theme.mono,
573
+ fontSize: 11,
574
+ color: theme.accent,
575
+ background: theme.accentDim,
576
+ padding: "1px 6px",
577
+ borderRadius: 3,
578
+ }}
579
+ >
580
+ clef lint --fix
581
+ </code>{" "}
582
+ to auto-resolve safe issues.
583
+ </span>
584
+ </div>
585
+ )}
586
+ </div>
587
+ </div>
588
+ );
589
+ }