@clef-sh/ui 0.1.20 → 0.1.21

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 (103) hide show
  1. package/dist/client/assets/index-DPWHjBbB.js +34 -0
  2. package/dist/client/assets/index-qsLTYpc9.css +2 -0
  3. package/dist/client/clef.svg +2 -0
  4. package/dist/client/index.html +3 -31
  5. package/dist/client-lib/components/Button.d.ts +1 -1
  6. package/dist/client-lib/components/Button.d.ts.map +1 -1
  7. package/dist/client-lib/components/CopyButton.d.ts.map +1 -1
  8. package/dist/client-lib/components/EnvBadge.d.ts.map +1 -1
  9. package/dist/client-lib/components/MatrixGrid.d.ts.map +1 -1
  10. package/dist/client-lib/components/Sidebar.d.ts +1 -1
  11. package/dist/client-lib/components/Sidebar.d.ts.map +1 -1
  12. package/dist/client-lib/components/StatusDot.d.ts.map +1 -1
  13. package/dist/client-lib/components/SyncPanel.d.ts.map +1 -1
  14. package/dist/client-lib/components/TopBar.d.ts +6 -0
  15. package/dist/client-lib/components/TopBar.d.ts.map +1 -1
  16. package/dist/client-lib/primitives/Badge.d.ts +11 -0
  17. package/dist/client-lib/primitives/Badge.d.ts.map +1 -0
  18. package/dist/client-lib/primitives/Card.d.ts +28 -0
  19. package/dist/client-lib/primitives/Card.d.ts.map +1 -0
  20. package/dist/client-lib/primitives/Dialog.d.ts +30 -0
  21. package/dist/client-lib/primitives/Dialog.d.ts.map +1 -0
  22. package/dist/client-lib/primitives/EmptyState.d.ts +10 -0
  23. package/dist/client-lib/primitives/EmptyState.d.ts.map +1 -0
  24. package/dist/client-lib/primitives/Field.d.ts +36 -0
  25. package/dist/client-lib/primitives/Field.d.ts.map +1 -0
  26. package/dist/client-lib/primitives/Input.d.ts +6 -0
  27. package/dist/client-lib/primitives/Input.d.ts.map +1 -0
  28. package/dist/client-lib/primitives/Stat.d.ts +11 -0
  29. package/dist/client-lib/primitives/Stat.d.ts.map +1 -0
  30. package/dist/client-lib/primitives/Table.d.ts +37 -0
  31. package/dist/client-lib/primitives/Table.d.ts.map +1 -0
  32. package/dist/client-lib/primitives/Tabs.d.ts +29 -0
  33. package/dist/client-lib/primitives/Tabs.d.ts.map +1 -0
  34. package/dist/client-lib/primitives/Toast.d.ts +16 -0
  35. package/dist/client-lib/primitives/Toast.d.ts.map +1 -0
  36. package/dist/client-lib/primitives/Toolbar.d.ts +29 -0
  37. package/dist/client-lib/primitives/Toolbar.d.ts.map +1 -0
  38. package/dist/client-lib/primitives/index.d.ts +23 -0
  39. package/dist/client-lib/primitives/index.d.ts.map +1 -0
  40. package/dist/client-lib/theme.d.ts +18 -41
  41. package/dist/client-lib/theme.d.ts.map +1 -1
  42. package/dist/server/api.d.ts.map +1 -1
  43. package/dist/server/api.js +215 -0
  44. package/dist/server/api.js.map +1 -1
  45. package/dist/server/envelope.d.ts +15 -0
  46. package/dist/server/envelope.d.ts.map +1 -0
  47. package/dist/server/envelope.js +310 -0
  48. package/dist/server/envelope.js.map +1 -0
  49. package/package.json +7 -2
  50. package/src/client/App.tsx +16 -41
  51. package/src/client/components/Button.tsx +13 -22
  52. package/src/client/components/CopyButton.tsx +5 -12
  53. package/src/client/components/EnvBadge.tsx +30 -15
  54. package/src/client/components/MatrixGrid.tsx +108 -252
  55. package/src/client/components/Sidebar.tsx +123 -199
  56. package/src/client/components/StatusDot.tsx +10 -15
  57. package/src/client/components/SyncPanel.tsx +14 -62
  58. package/src/client/components/TopBar.tsx +11 -36
  59. package/src/client/index.html +1 -30
  60. package/src/client/main.tsx +1 -0
  61. package/src/client/primitives/Badge.test.tsx +47 -0
  62. package/src/client/primitives/Badge.tsx +64 -0
  63. package/src/client/primitives/Card.test.tsx +50 -0
  64. package/src/client/primitives/Card.tsx +85 -0
  65. package/src/client/primitives/Dialog.test.tsx +55 -0
  66. package/src/client/primitives/Dialog.tsx +96 -0
  67. package/src/client/primitives/EmptyState.test.tsx +25 -0
  68. package/src/client/primitives/EmptyState.tsx +38 -0
  69. package/src/client/primitives/Field.test.tsx +46 -0
  70. package/src/client/primitives/Field.tsx +95 -0
  71. package/src/client/primitives/Input.tsx +26 -0
  72. package/src/client/primitives/Stat.test.tsx +32 -0
  73. package/src/client/primitives/Stat.tsx +52 -0
  74. package/src/client/primitives/Table.test.tsx +58 -0
  75. package/src/client/primitives/Table.tsx +113 -0
  76. package/src/client/primitives/Tabs.test.tsx +44 -0
  77. package/src/client/primitives/Tabs.tsx +100 -0
  78. package/src/client/primitives/Toast.test.tsx +77 -0
  79. package/src/client/primitives/Toast.tsx +89 -0
  80. package/src/client/primitives/Toolbar.test.tsx +50 -0
  81. package/src/client/primitives/Toolbar.tsx +86 -0
  82. package/src/client/primitives/index.ts +43 -0
  83. package/src/client/public/clef.svg +2 -0
  84. package/src/client/screens/BackendScreen.tsx +104 -363
  85. package/src/client/screens/DiffView.tsx +187 -378
  86. package/src/client/screens/EnvelopeScreen.test.tsx +542 -0
  87. package/src/client/screens/EnvelopeScreen.tsx +948 -0
  88. package/src/client/screens/GitLogView.tsx +48 -106
  89. package/src/client/screens/ImportScreen.tsx +105 -308
  90. package/src/client/screens/LintView.tsx +184 -379
  91. package/src/client/screens/ManifestScreen.tsx +283 -445
  92. package/src/client/screens/MatrixView.tsx +75 -91
  93. package/src/client/screens/NamespaceEditor.tsx +234 -609
  94. package/src/client/screens/PolicyView.tsx +183 -453
  95. package/src/client/screens/RecipientsScreen.tsx +71 -350
  96. package/src/client/screens/ResetScreen.tsx +67 -237
  97. package/src/client/screens/ScanScreen.tsx +85 -249
  98. package/src/client/screens/SchemaEditor.test.tsx +237 -0
  99. package/src/client/screens/SchemaEditor.tsx +435 -0
  100. package/src/client/screens/ServiceIdentitiesScreen.tsx +251 -788
  101. package/src/client/styles.css +77 -0
  102. package/src/client/theme.ts +27 -48
  103. package/dist/client/assets/index-Db6WgHgY.js +0 -38
@@ -1,10 +1,11 @@
1
1
  import React, { useState, useEffect, useCallback } from "react";
2
- import { theme, SEVERITY_META, CATEGORY_META } from "../theme";
2
+ import { RefreshCw } from "lucide-react";
3
3
  import { apiFetch } from "../api";
4
- import { TopBar } from "../components/TopBar";
5
4
  import { Button } from "../components/Button";
6
5
  import { EnvBadge } from "../components/EnvBadge";
7
6
  import { CopyButton } from "../components/CopyButton";
7
+ import { Toolbar, Card, Badge, EmptyState } from "../primitives";
8
+ import type { BadgeTone } from "../primitives";
8
9
  import type { ViewName } from "../components/Sidebar";
9
10
  import type { LintResult, LintIssue } from "@clef-sh/core";
10
11
 
@@ -13,8 +14,77 @@ interface LintViewProps {
13
14
  setNs: (ns: string) => void;
14
15
  }
15
16
 
17
+ type Severity = "error" | "warning" | "info";
18
+ type Category = "matrix" | "schema" | "sops";
19
+
20
+ const SEVERITY_TW: Record<
21
+ Severity,
22
+ {
23
+ label: string;
24
+ icon: string;
25
+ text: string;
26
+ bg: string;
27
+ border: string;
28
+ rowStripe: string;
29
+ }
30
+ > = {
31
+ error: {
32
+ label: "Error",
33
+ icon: "✕",
34
+ text: "text-stop-500",
35
+ bg: "bg-stop-500/10",
36
+ border: "border-stop-500/30",
37
+ rowStripe: "border-l-[3px] border-l-stop-500/40",
38
+ },
39
+ warning: {
40
+ label: "Warning",
41
+ icon: "⚠",
42
+ text: "text-warn-500",
43
+ bg: "bg-warn-500/10",
44
+ border: "border-warn-500/30",
45
+ rowStripe: "border-l-[3px] border-l-warn-500/40",
46
+ },
47
+ info: {
48
+ label: "Info",
49
+ icon: "i",
50
+ text: "text-blue-400",
51
+ bg: "bg-blue-400/10",
52
+ border: "border-blue-400/30",
53
+ rowStripe: "border-l-[3px] border-l-blue-400/40",
54
+ },
55
+ };
56
+
57
+ const CATEGORY_TW: Record<Category, { label: string; tone: BadgeTone }> = {
58
+ matrix: { label: "Matrix", tone: "gold" },
59
+ schema: { label: "Schema", tone: "blue" },
60
+ sops: { label: "SOPS", tone: "purple" },
61
+ };
62
+
63
+ const FILTER_TW: Record<string, { text: string; bgActive: string; borderActive: string }> = {
64
+ all: {
65
+ text: "text-ash",
66
+ bgActive: "bg-ash/15",
67
+ borderActive: "border-ash/30",
68
+ },
69
+ error: {
70
+ text: "text-stop-500",
71
+ bgActive: "bg-stop-500/15",
72
+ borderActive: "border-stop-500/40",
73
+ },
74
+ warning: {
75
+ text: "text-warn-500",
76
+ bgActive: "bg-warn-500/15",
77
+ borderActive: "border-warn-500/40",
78
+ },
79
+ info: {
80
+ text: "text-blue-400",
81
+ bgActive: "bg-blue-400/15",
82
+ borderActive: "border-blue-400/40",
83
+ },
84
+ };
85
+
16
86
  export function LintView({ setView, setNs }: LintViewProps) {
17
- const [filter, setFilter] = useState("all");
87
+ const [filter, setFilter] = useState<string>("all");
18
88
  const [dismissed, setDismissed] = useState<number[]>([]);
19
89
  const [lintResult, setLintResult] = useState<LintResult | null>(null);
20
90
  const [loading, setLoading] = useState(false);
@@ -60,123 +130,84 @@ export function LintView({ setView, setNs }: LintViewProps) {
60
130
 
61
131
  const allClear = visible.length === 0;
62
132
 
133
+ const severityFilters: Array<{
134
+ key: string;
135
+ label: string;
136
+ count: number;
137
+ }> = [
138
+ { key: "all", label: "All issues", count: issues.length },
139
+ { key: "error", label: "Errors", count: errors.length },
140
+ { key: "warning", label: "Warnings", count: warnings.length },
141
+ { key: "info", label: "Info", count: infos.length },
142
+ ];
143
+
63
144
  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
- />
145
+ <div className="flex flex-1 flex-col overflow-hidden">
146
+ <Toolbar>
147
+ <div>
148
+ <Toolbar.Title>Lint</Toolbar.Title>
149
+ <Toolbar.Subtitle>clef lint &mdash; full repo health check</Toolbar.Subtitle>
150
+ </div>
151
+ <Toolbar.Actions>
152
+ <Button onClick={loadLint}>
153
+ <RefreshCw size={12} className="mr-1 inline-block align-[-2px]" />
154
+ Re-run
155
+ </Button>
156
+ {errors.length === 0 && <Button variant="primary">All clear &mdash; commit</Button>}
157
+ </Toolbar.Actions>
158
+ </Toolbar>
75
159
 
76
160
  {/* Summary bar — only shown when there are issues */}
77
161
  {!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
- >
162
+ <div className="flex flex-wrap items-center gap-2.5 border-b border-edge bg-ink-800 px-6 py-3.5">
89
163
  {/* 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
- }}
164
+ {severityFilters.map((f) => {
165
+ const tw = FILTER_TW[f.key];
166
+ const active = filter === f.key;
167
+ const baseClasses =
168
+ "flex items-center gap-1.5 rounded-pill border px-3 py-1 font-sans text-[12px] transition-colors";
169
+ const activeClasses = active
170
+ ? `${tw.text} ${tw.bgActive} ${tw.borderActive} font-semibold`
171
+ : "text-ash border-edge font-normal hover:border-edge-strong";
172
+ return (
173
+ <button
174
+ key={f.key}
175
+ data-testid={`filter-${f.key}`}
176
+ onClick={() => setFilter(f.key)}
177
+ className={`${baseClasses} ${activeClasses}`}
143
178
  >
144
- {f.count}
145
- </span>
146
- {f.label}
147
- </button>
148
- ))}
179
+ <span className={`font-mono text-[11px] font-bold ${tw.text}`}>{f.count}</span>
180
+ {f.label}
181
+ </button>
182
+ );
183
+ })}
149
184
 
150
- <div style={{ flex: 1 }} />
185
+ <div className="flex-1" />
151
186
 
152
187
  {/* Category filters */}
153
188
  {(["matrix", "schema", "sops"] as const).map((cat) => {
154
- const m = CATEGORY_META[cat];
189
+ const m = CATEGORY_TW[cat];
190
+ const active = filter === cat;
155
191
  return (
156
192
  <button
157
193
  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
- }}
194
+ onClick={() => setFilter(active ? "all" : cat)}
195
+ className={`rounded-sm border px-2.5 py-1 font-mono text-[10px] font-semibold uppercase tracking-[0.06em] transition-colors ${
196
+ active
197
+ ? "bg-ink-800 border-edge-strong text-bone"
198
+ : "border-edge-strong text-ash-dim hover:text-ash"
199
+ }`}
171
200
  >
172
- {m.label}
201
+ <Badge tone={m.tone} variant={active ? "solid" : "outline"}>
202
+ {m.label}
203
+ </Badge>
173
204
  </button>
174
205
  );
175
206
  })}
176
207
  </div>
177
208
  )}
178
209
 
179
- <div style={{ flex: 1, overflow: "auto", padding: 24 }}>
210
+ <div className="flex-1 overflow-auto p-6">
180
211
  {loading && (
181
212
  <>
182
213
  <style>{`
@@ -191,103 +222,35 @@ export function LintView({ setView, setNs }: LintViewProps) {
191
222
  0%, 100% { opacity: 0.4; }
192
223
  50% { opacity: 1; }
193
224
  }
225
+ .clef-scan-line { animation: clef-scan-line 1.8s ease-in-out infinite; transform-origin: left; opacity: 0; }
226
+ .clef-scan-line-0 { animation-delay: 0s; width: 120px; }
227
+ .clef-scan-line-1 { animation-delay: 0.3s; width: 90px; }
228
+ .clef-scan-line-2 { animation-delay: 0.6s; width: 105px; }
229
+ .clef-scan-glow { animation: clef-scan-glow 1.8s ease-in-out infinite; }
194
230
  `}</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) => (
231
+ <div className="flex items-center justify-center px-6 py-12">
232
+ <div className="min-w-[200px] rounded-card border border-edge bg-ink-850 px-10 py-7 text-center">
233
+ <div className="mb-4 flex flex-col gap-1.5">
234
+ {[0, 1, 2].map((i) => (
215
235
  <div
216
236
  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
- }}
237
+ className={`h-[3px] rounded-sm bg-gold-500 clef-scan-line clef-scan-line-${i}`}
226
238
  />
227
239
  ))}
228
240
  </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>
241
+ <div className="font-mono text-[11px] text-ash clef-scan-glow">Linting...</div>
239
242
  </div>
240
243
  </div>
241
244
  </>
242
245
  )}
243
246
 
244
247
  {!loading && allClear && (
245
- <div
248
+ <EmptyState
246
249
  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>
250
+ icon={<span className="text-go-500">{"✓"}</span>}
251
+ title="All clear"
252
+ body={`No issues found across ${fileCount} files`}
253
+ />
291
254
  )}
292
255
 
293
256
  {/* Grouped issues */}
@@ -296,127 +259,58 @@ export function LintView({ setView, setNs }: LintViewProps) {
296
259
  (["error", "warning", "info"] as const).map((sev) => {
297
260
  const group = visible.filter((i) => i.severity === sev);
298
261
  if (!group.length) return null;
299
- const meta = SEVERITY_META[sev];
262
+ const meta = SEVERITY_TW[sev];
300
263
 
301
264
  return (
302
- <div key={sev} style={{ marginBottom: 24 }}>
265
+ <div key={sev} className="mb-6">
303
266
  {/* Group header */}
304
- <div
305
- style={{
306
- display: "flex",
307
- alignItems: "center",
308
- gap: 10,
309
- marginBottom: 10,
310
- }}
311
- >
267
+ <div className="mb-2.5 flex items-center gap-2.5">
312
268
  <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
- }}
269
+ className={`flex h-[22px] w-[22px] items-center justify-center rounded-full border font-mono text-[11px] font-bold ${meta.bg} ${meta.border} ${meta.text}`}
327
270
  >
328
271
  {meta.icon}
329
272
  </div>
330
- <span
331
- style={{
332
- fontFamily: theme.sans,
333
- fontWeight: 600,
334
- fontSize: 13,
335
- color: meta.color,
336
- }}
337
- >
273
+ <span className={`font-sans text-[13px] font-semibold ${meta.text}`}>
338
274
  {meta.label}s
339
275
  </span>
340
276
  <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
- }}
277
+ className={`rounded-pill border px-2 py-px font-mono text-[10px] ${meta.bg} ${meta.border} ${meta.text}`}
350
278
  >
351
279
  {group.length}
352
280
  </span>
353
281
  </div>
354
282
 
355
283
  {/* Issue cards */}
356
- <div
357
- style={{
358
- background: theme.surface,
359
- border: `1px solid ${theme.border}`,
360
- borderRadius: 10,
361
- overflow: "hidden",
362
- }}
363
- >
284
+ <Card>
364
285
  {group.map((issue, i) => {
365
- const catMeta = CATEGORY_META[issue.category] ?? {
286
+ const catKey = (issue.category as Category) ?? "matrix";
287
+ const catMeta = CATEGORY_TW[catKey] ?? {
366
288
  label: issue.category,
367
- color: theme.textMuted,
289
+ tone: "default" as BadgeTone,
368
290
  };
369
291
  const fileParts = issue.file?.split("/") ?? [];
370
292
  const envName =
371
293
  fileParts.length >= 2
372
294
  ? fileParts[fileParts.length - 1]?.replace(".enc.yaml", "")
373
295
  : undefined;
296
+ const isLast = i === group.length - 1;
374
297
 
375
298
  return (
376
299
  <div
377
300
  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
- }}
301
+ className={`flex items-start gap-3.5 px-[18px] py-3.5 ${meta.rowStripe} ${
302
+ !isLast ? "border-b border-edge" : ""
303
+ }`}
387
304
  >
388
305
  {/* 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>
306
+ <div className="shrink-0 pt-0.5">
307
+ <Badge tone={catMeta.tone}>{catMeta.label}</Badge>
406
308
  </div>
407
309
 
408
310
  {/* Main content */}
409
- <div style={{ flex: 1, minWidth: 0 }}>
311
+ <div className="min-w-0 flex-1">
410
312
  {/* File + key */}
411
- <div
412
- style={{
413
- display: "flex",
414
- alignItems: "center",
415
- gap: 8,
416
- marginBottom: 4,
417
- flexWrap: "wrap",
418
- }}
419
- >
313
+ <div className="mb-1 flex flex-wrap items-center gap-2">
420
314
  <span
421
315
  data-testid={`file-ref-${issue.file}`}
422
316
  role="link"
@@ -425,41 +319,18 @@ export function LintView({ setView, setNs }: LintViewProps) {
425
319
  onKeyDown={(e) => {
426
320
  if (e.key === "Enter") handleNavigate(issue);
427
321
  }}
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
- }}
322
+ className={`font-mono text-[12px] font-semibold text-gold-500 ${
323
+ issue.file
324
+ ? "cursor-pointer underline decoration-gold-500/40 decoration-dotted"
325
+ : "cursor-default no-underline"
326
+ }`}
438
327
  >
439
328
  {issue.file}
440
329
  </span>
441
330
  {issue.key && (
442
331
  <>
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
- >
332
+ <span className="font-mono text-[11px] text-ash-dim">{"→"}</span>
333
+ <span className="rounded-sm border border-edge-strong bg-ink-900 px-[7px] py-px font-mono text-[11px] text-bone">
463
334
  {issue.key}
464
335
  </span>
465
336
  </>
@@ -469,46 +340,16 @@ export function LintView({ setView, setNs }: LintViewProps) {
469
340
 
470
341
  {/* Message */}
471
342
  <div
472
- style={{
473
- fontFamily: theme.sans,
474
- fontSize: 12,
475
- color: theme.textMuted,
476
- marginBottom: issue.fixCommand ? 10 : 0,
477
- }}
343
+ className={`font-sans text-[12px] text-ash ${issue.fixCommand ? "mb-2.5" : ""}`}
478
344
  >
479
345
  {issue.message}
480
346
  </div>
481
347
 
482
348
  {/* Fix command */}
483
349
  {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
- >
350
+ <div className="flex w-fit items-center gap-2 rounded-md border border-edge-strong bg-ink-800 px-2.5 py-1.5">
351
+ <span className="font-mono text-[11px] text-go-500">$</span>
352
+ <span className="font-mono text-[11px] text-bone">
512
353
  {issue.fixCommand}
513
354
  </span>
514
355
  <CopyButton text={issue.fixCommand} />
@@ -521,62 +362,26 @@ export function LintView({ setView, setNs }: LintViewProps) {
521
362
  onClick={() => setDismissed((d) => [...d, issue._idx])}
522
363
  title="Dismiss"
523
364
  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
- }}
365
+ className="shrink-0 cursor-pointer border-none bg-transparent px-1 text-[16px] leading-none text-ash-dim transition-colors hover:text-bone"
535
366
  >
536
- {"\u00D7"}
367
+ {"×"}
537
368
  </button>
538
369
  </div>
539
370
  );
540
371
  })}
541
- </div>
372
+ </Card>
542
373
  </div>
543
374
  );
544
375
  })}
545
376
 
546
377
  {/* Footer hint */}
547
378
  {!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
379
+ <div className="mt-2 flex items-center gap-3 rounded-md border border-edge bg-ink-850 px-4 py-3">
380
+ <span className="text-[14px]">{"💡"}</span>
381
+ <span className="font-sans text-[12px] text-ash">
382
+ Fix all errors before committing. Warnings and info items won&apos;t block commits but
569
383
  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
- >
384
+ <code className="rounded-sm bg-gold-500/15 px-1.5 py-px font-mono text-[11px] text-gold-500">
580
385
  clef lint --fix
581
386
  </code>{" "}
582
387
  to auto-resolve safe issues.