@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,9 @@
1
1
  import React, { useState, useEffect, useCallback } from "react";
2
- import { theme } from "../theme";
3
2
  import { apiFetch } from "../api";
4
- import { TopBar } from "../components/TopBar";
5
3
  import { Button } from "../components/Button";
6
4
  import { EnvBadge } from "../components/EnvBadge";
7
5
  import { CopyButton } from "../components/CopyButton";
6
+ import { Toolbar, Table, EmptyState } from "../primitives";
8
7
  import type { ClefManifest, DiffResult } from "@clef-sh/core";
9
8
 
10
9
  interface DiffViewProps {
@@ -12,9 +11,14 @@ interface DiffViewProps {
12
11
  }
13
12
 
14
13
  function truncate(s: string, max: number): string {
15
- return s.length > max ? s.slice(0, max) + "\u2026" : s;
14
+ return s.length > max ? s.slice(0, max) + "" : s;
16
15
  }
17
16
 
17
+ const SELECT_CLASSES =
18
+ "rounded-md border border-edge bg-ink-850 px-2.5 py-1 font-mono text-[12px] text-bone cursor-pointer focus:outline-none focus:border-edge-strong";
19
+
20
+ type DiffStatus = "changed" | "identical" | "missing_a" | "missing_b";
21
+
18
22
  export function DiffView({ manifest }: DiffViewProps) {
19
23
  const environments = manifest?.environments ?? [];
20
24
  const namespaces = manifest?.namespaces ?? [];
@@ -65,19 +69,79 @@ export function DiffView({ manifest }: DiffViewProps) {
65
69
  const identicalCount = rows.filter((r) => r.status === "identical").length;
66
70
  const missingRows = rows.filter((r) => r.status === "missing_a" || r.status === "missing_b");
67
71
 
68
- const statusMeta: Record<string, { label: string; color: string }> = {
69
- changed: { label: "Changed", color: theme.yellow },
70
- identical: { label: "Identical", color: theme.textMuted },
71
- missing_a: { label: `Missing in ${envA}`, color: theme.red },
72
- missing_b: { label: `Missing in ${envB}`, color: theme.red },
72
+ const statusMeta: Record<
73
+ DiffStatus,
74
+ { label: string; text: string; bg: string; border: string }
75
+ > = {
76
+ changed: {
77
+ label: "Changed",
78
+ text: "text-warn-500",
79
+ bg: "bg-warn-500/10",
80
+ border: "border-warn-500/30",
81
+ },
82
+ identical: {
83
+ label: "Identical",
84
+ text: "text-ash",
85
+ bg: "bg-ash/10",
86
+ border: "border-ash/20",
87
+ },
88
+ missing_a: {
89
+ label: `Missing in ${envA}`,
90
+ text: "text-stop-500",
91
+ bg: "bg-stop-500/10",
92
+ border: "border-stop-500/30",
93
+ },
94
+ missing_b: {
95
+ label: `Missing in ${envB}`,
96
+ text: "text-stop-500",
97
+ bg: "bg-stop-500/10",
98
+ border: "border-stop-500/30",
99
+ },
73
100
  };
74
101
 
102
+ const summaryPills: Array<{ label: string; text: string; bg: string; border: string }> = [
103
+ {
104
+ label: `${changedCount} changed`,
105
+ text: "text-warn-500",
106
+ bg: "bg-warn-500/10",
107
+ border: "border-warn-500/30",
108
+ },
109
+ ...(missingACount > 0
110
+ ? [
111
+ {
112
+ label: `${missingACount} missing in ${envA}`,
113
+ text: "text-stop-500",
114
+ bg: "bg-stop-500/10",
115
+ border: "border-stop-500/30",
116
+ },
117
+ ]
118
+ : []),
119
+ ...(missingBCount > 0
120
+ ? [
121
+ {
122
+ label: `${missingBCount} missing in ${envB}`,
123
+ text: "text-stop-500",
124
+ bg: "bg-stop-500/10",
125
+ border: "border-stop-500/30",
126
+ },
127
+ ]
128
+ : []),
129
+ {
130
+ label: `${identicalCount} identical`,
131
+ text: "text-ash",
132
+ bg: "bg-ash/10",
133
+ border: "border-ash/20",
134
+ },
135
+ ];
136
+
75
137
  return (
76
- <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
77
- <TopBar
78
- title="Environment Diff"
79
- subtitle="Compare secrets across environments"
80
- actions={
138
+ <div className="flex flex-1 flex-col overflow-hidden">
139
+ <Toolbar>
140
+ <div>
141
+ <Toolbar.Title>Environment Diff</Toolbar.Title>
142
+ <Toolbar.Subtitle>Compare secrets across environments</Toolbar.Subtitle>
143
+ </div>
144
+ <Toolbar.Actions>
81
145
  <Button
82
146
  variant="primary"
83
147
  data-testid="sync-missing-btn"
@@ -86,69 +150,26 @@ export function DiffView({ manifest }: DiffViewProps) {
86
150
  setTimeout(() => setToastVisible(false), 2000);
87
151
  }}
88
152
  >
89
- Sync missing keys {"\u2192"}
153
+ Sync missing keys {""}
90
154
  </Button>
91
- }
92
- />
155
+ </Toolbar.Actions>
156
+ </Toolbar>
93
157
 
94
158
  {/* Toast */}
95
159
  {toastVisible && (
96
160
  <div
97
161
  data-testid="coming-soon-toast"
98
- style={{
99
- position: "fixed",
100
- top: 20,
101
- right: 20,
102
- padding: "10px 18px",
103
- background: theme.surface,
104
- border: `1px solid ${theme.accent}44`,
105
- borderRadius: 8,
106
- fontFamily: theme.sans,
107
- fontSize: 12,
108
- color: theme.accent,
109
- zIndex: 1000,
110
- }}
162
+ className="fixed right-5 top-5 z-[1000] rounded-md border border-gold-500/30 bg-ink-850 px-4 py-2.5 font-sans text-[12px] text-gold-500"
111
163
  >
112
164
  Coming soon
113
165
  </div>
114
166
  )}
115
167
 
116
168
  {/* Controls */}
117
- <div
118
- style={{
119
- padding: "14px 24px",
120
- background: "#0D0F14",
121
- borderBottom: `1px solid ${theme.border}`,
122
- display: "flex",
123
- alignItems: "center",
124
- gap: 12,
125
- flexWrap: "wrap",
126
- }}
127
- >
128
- <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
129
- <span
130
- style={{
131
- fontFamily: theme.sans,
132
- fontSize: 12,
133
- color: theme.textMuted,
134
- }}
135
- >
136
- Namespace
137
- </span>
138
- <select
139
- value={ns}
140
- onChange={(e) => setNs(e.target.value)}
141
- style={{
142
- background: theme.surface,
143
- border: `1px solid ${theme.border}`,
144
- borderRadius: 6,
145
- padding: "5px 10px",
146
- fontFamily: theme.mono,
147
- fontSize: 12,
148
- color: theme.text,
149
- cursor: "pointer",
150
- }}
151
- >
169
+ <div className="flex flex-wrap items-center gap-3 border-b border-edge bg-ink-800 px-6 py-3.5">
170
+ <div className="flex items-center gap-2">
171
+ <span className="font-sans text-[12px] text-ash">Namespace</span>
172
+ <select value={ns} onChange={(e) => setNs(e.target.value)} className={SELECT_CLASSES}>
152
173
  {namespaces.map((n) => (
153
174
  <option key={n.name} value={n.name}>
154
175
  {n.name}
@@ -157,59 +178,17 @@ export function DiffView({ manifest }: DiffViewProps) {
157
178
  </select>
158
179
  </div>
159
180
 
160
- <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
161
- <span
162
- style={{
163
- fontFamily: theme.sans,
164
- fontSize: 12,
165
- color: theme.textMuted,
166
- }}
167
- >
168
- Compare
169
- </span>
170
- <select
171
- value={envA}
172
- onChange={(e) => setEnvA(e.target.value)}
173
- style={{
174
- background: theme.surface,
175
- border: `1px solid ${theme.border}`,
176
- borderRadius: 6,
177
- padding: "5px 10px",
178
- fontFamily: theme.mono,
179
- fontSize: 12,
180
- color: theme.text,
181
- cursor: "pointer",
182
- }}
183
- >
181
+ <div className="flex items-center gap-2">
182
+ <span className="font-sans text-[12px] text-ash">Compare</span>
183
+ <select value={envA} onChange={(e) => setEnvA(e.target.value)} className={SELECT_CLASSES}>
184
184
  {environments.map((e) => (
185
185
  <option key={e.name} value={e.name}>
186
186
  {e.name}
187
187
  </option>
188
188
  ))}
189
189
  </select>
190
- <span
191
- style={{
192
- fontFamily: theme.mono,
193
- fontSize: 12,
194
- color: theme.textDim,
195
- }}
196
- >
197
- {"\u2192"}
198
- </span>
199
- <select
200
- value={envB}
201
- onChange={(e) => setEnvB(e.target.value)}
202
- style={{
203
- background: theme.surface,
204
- border: `1px solid ${theme.border}`,
205
- borderRadius: 6,
206
- padding: "5px 10px",
207
- fontFamily: theme.mono,
208
- fontSize: 12,
209
- color: theme.text,
210
- cursor: "pointer",
211
- }}
212
- >
190
+ <span className="font-mono text-[12px] text-ash-dim">{"→"}</span>
191
+ <select value={envB} onChange={(e) => setEnvB(e.target.value)} className={SELECT_CLASSES}>
213
192
  {environments.map((e) => (
214
193
  <option key={e.name} value={e.name}>
215
194
  {e.name}
@@ -218,317 +197,147 @@ export function DiffView({ manifest }: DiffViewProps) {
218
197
  </select>
219
198
  </div>
220
199
 
221
- <div style={{ flex: 1 }} />
200
+ <div className="flex-1" />
222
201
 
223
- <label style={{ display: "flex", alignItems: "center", gap: 6, cursor: "pointer" }}>
202
+ <label className="flex cursor-pointer items-center gap-1.5">
224
203
  <input
225
204
  type="checkbox"
226
205
  checked={showValues}
227
206
  onChange={(e) => setShowValues(e.target.checked)}
228
207
  data-testid="show-values-toggle"
229
- style={{ accentColor: theme.accent }}
208
+ className="accent-gold-500"
230
209
  />
231
- <span
232
- style={{
233
- fontFamily: theme.sans,
234
- fontSize: 12,
235
- color: theme.textMuted,
236
- }}
237
- >
238
- Show values
239
- </span>
210
+ <span className="font-sans text-[12px] text-ash">Show values</span>
240
211
  </label>
241
212
 
242
- <label style={{ display: "flex", alignItems: "center", gap: 6, cursor: "pointer" }}>
213
+ <label className="flex cursor-pointer items-center gap-1.5">
243
214
  <input
244
215
  type="checkbox"
245
216
  checked={showSame}
246
217
  onChange={(e) => setShowSame(e.target.checked)}
247
- style={{ accentColor: theme.accent }}
218
+ className="accent-gold-500"
248
219
  />
249
- <span
250
- style={{
251
- fontFamily: theme.sans,
252
- fontSize: 12,
253
- color: theme.textMuted,
254
- }}
255
- >
256
- Show identical
257
- </span>
220
+ <span className="font-sans text-[12px] text-ash">Show identical</span>
258
221
  </label>
259
222
  </div>
260
223
 
261
224
  {/* Summary strip */}
262
- <div
263
- style={{
264
- padding: "10px 24px",
265
- display: "flex",
266
- gap: 10,
267
- borderBottom: `1px solid ${theme.border}`,
268
- }}
269
- >
270
- {[
271
- { label: `${changedCount} changed`, color: theme.yellow },
272
- ...(missingACount > 0
273
- ? [{ label: `${missingACount} missing in ${envA}`, color: theme.red }]
274
- : []),
275
- ...(missingBCount > 0
276
- ? [{ label: `${missingBCount} missing in ${envB}`, color: theme.red }]
277
- : []),
278
- { label: `${identicalCount} identical`, color: theme.textMuted },
279
- ].map((p) => (
225
+ <div className="flex gap-2.5 border-b border-edge px-6 py-2.5">
226
+ {summaryPills.map((p) => (
280
227
  <span
281
228
  key={p.label}
282
- style={{
283
- fontFamily: theme.mono,
284
- fontSize: 11,
285
- color: p.color,
286
- background: `${p.color}14`,
287
- border: `1px solid ${p.color}33`,
288
- borderRadius: 20,
289
- padding: "2px 10px",
290
- }}
229
+ className={`rounded-pill border px-2.5 py-px font-mono text-[11px] ${p.text} ${p.bg} ${p.border}`}
291
230
  >
292
231
  {p.label}
293
232
  </span>
294
233
  ))}
295
234
  </div>
296
235
 
297
- <div style={{ flex: 1, overflow: "auto", padding: 24 }}>
298
- {loading && <p style={{ color: theme.textMuted, fontFamily: theme.sans }}>Loading...</p>}
236
+ <div className="flex-1 overflow-auto p-6">
237
+ {loading && <EmptyState title="Loading..." body="Computing diff between environments" />}
299
238
 
300
239
  {!loading && (
301
240
  <>
302
- <div
303
- data-testid="diff-table"
304
- style={{
305
- background: theme.surface,
306
- border: `1px solid ${theme.border}`,
307
- borderRadius: 10,
308
- overflow: "hidden",
309
- }}
310
- >
311
- {/* Header */}
312
- <div
313
- style={{
314
- display: "grid",
315
- gridTemplateColumns: "220px 1fr 1fr 100px",
316
- background: "#0D0F14",
317
- padding: "10px 20px",
318
- borderBottom: `1px solid ${theme.border}`,
319
- }}
320
- >
321
- <span
322
- style={{
323
- fontFamily: theme.sans,
324
- fontSize: 11,
325
- fontWeight: 600,
326
- color: theme.textMuted,
327
- textTransform: "uppercase",
328
- letterSpacing: "0.07em",
329
- }}
330
- >
331
- Key
332
- </span>
333
- <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
334
- <EnvBadge env={envA} small />
335
- <span
336
- style={{
337
- fontFamily: theme.sans,
338
- fontSize: 11,
339
- fontWeight: 600,
340
- color: theme.textMuted,
341
- textTransform: "uppercase",
342
- }}
343
- >
344
- {envA}
345
- </span>
346
- </div>
347
- <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
348
- <EnvBadge env={envB} small />
349
- <span
350
- style={{
351
- fontFamily: theme.sans,
352
- fontSize: 11,
353
- fontWeight: 600,
354
- color: theme.textMuted,
355
- textTransform: "uppercase",
356
- }}
357
- >
358
- {envB}
359
- </span>
360
- </div>
361
- <span
362
- style={{
363
- fontFamily: theme.sans,
364
- fontSize: 11,
365
- fontWeight: 600,
366
- color: theme.textMuted,
367
- textTransform: "uppercase",
368
- }}
369
- >
370
- Status
371
- </span>
372
- </div>
373
-
374
- {filtered.map((row, i) => {
375
- const meta = statusMeta[row.status];
376
- return (
377
- <div
378
- key={row.key}
379
- style={{
380
- display: "grid",
381
- gridTemplateColumns: "220px 1fr 1fr 100px",
382
- padding: "0 20px",
383
- minHeight: 48,
384
- alignItems: "center",
385
- borderBottom: i < filtered.length - 1 ? `1px solid ${theme.border}` : "none",
386
- background:
387
- row.status === "changed"
388
- ? `${theme.yellow}06`
389
- : row.status.startsWith("missing")
390
- ? `${theme.red}06`
391
- : "transparent",
392
- }}
393
- >
394
- <span
395
- style={{
396
- fontFamily: theme.mono,
397
- fontSize: 12,
398
- color: theme.text,
399
- paddingRight: 16,
400
- }}
401
- >
402
- {row.key}
403
- </span>
404
-
405
- {/* Env A value */}
406
- <div style={{ paddingRight: 16 }}>
407
- {row.valueA !== null ? (
408
- <span
409
- style={{
410
- fontFamily: theme.mono,
411
- fontSize: 11,
412
- color: row.status === "changed" ? theme.yellow : theme.textMuted,
413
- background: row.status === "changed" ? theme.yellowDim : "transparent",
414
- padding: row.status === "changed" ? "2px 6px" : "0",
415
- borderRadius: 3,
416
- }}
417
- >
418
- {truncate(row.valueA, 36)}
419
- </span>
420
- ) : (
421
- <span
422
- style={{
423
- fontFamily: theme.mono,
424
- fontSize: 11,
425
- color: theme.textDim,
426
- fontStyle: "italic",
427
- }}
428
- >
429
- {"\u2014"} not set {"\u2014"}
430
- </span>
431
- )}
241
+ <Table data-testid="diff-table">
242
+ <Table.Header>
243
+ <tr>
244
+ <Table.HeaderCell className="w-[220px]">Key</Table.HeaderCell>
245
+ <Table.HeaderCell>
246
+ <div className="flex items-center gap-2">
247
+ <EnvBadge env={envA} small />
248
+ <span>{envA}</span>
432
249
  </div>
433
-
434
- {/* Env B value */}
435
- <div style={{ paddingRight: 16 }}>
436
- {row.valueB !== null ? (
437
- <span
438
- style={{
439
- fontFamily: theme.mono,
440
- fontSize: 11,
441
- color: row.status === "changed" ? theme.blue : theme.textMuted,
442
- background: row.status === "changed" ? theme.blueDim : "transparent",
443
- padding: row.status === "changed" ? "2px 6px" : "0",
444
- borderRadius: 3,
445
- }}
446
- >
447
- {truncate(row.valueB, 36)}
448
- </span>
449
- ) : (
250
+ </Table.HeaderCell>
251
+ <Table.HeaderCell>
252
+ <div className="flex items-center gap-2">
253
+ <EnvBadge env={envB} small />
254
+ <span>{envB}</span>
255
+ </div>
256
+ </Table.HeaderCell>
257
+ <Table.HeaderCell className="w-[120px]">Status</Table.HeaderCell>
258
+ </tr>
259
+ </Table.Header>
260
+ <tbody>
261
+ {filtered.map((row) => {
262
+ const status = row.status as DiffStatus;
263
+ const meta = statusMeta[status];
264
+ const isChanged = status === "changed";
265
+ const isMissing = status === "missing_a" || status === "missing_b";
266
+ const rowBg = isChanged
267
+ ? "bg-warn-500/[0.025]"
268
+ : isMissing
269
+ ? "bg-stop-500/[0.025]"
270
+ : "";
271
+ return (
272
+ <Table.Row
273
+ key={row.key}
274
+ tone={isMissing ? "drift" : undefined}
275
+ className={rowBg}
276
+ >
277
+ <Table.Cell className="font-mono text-[12px] text-bone">{row.key}</Table.Cell>
278
+ <Table.Cell>
279
+ {row.valueA !== null ? (
280
+ <span
281
+ className={`font-mono text-[11px] ${
282
+ isChanged
283
+ ? "rounded-sm bg-warn-500/15 px-1.5 py-0.5 text-warn-500"
284
+ : "text-ash"
285
+ }`}
286
+ >
287
+ {truncate(row.valueA, 36)}
288
+ </span>
289
+ ) : (
290
+ <span className="font-mono text-[11px] italic text-ash-dim">
291
+ {"—"} not set {"—"}
292
+ </span>
293
+ )}
294
+ </Table.Cell>
295
+ <Table.Cell>
296
+ {row.valueB !== null ? (
297
+ <span
298
+ className={`font-mono text-[11px] ${
299
+ isChanged
300
+ ? "rounded-sm bg-blue-400/15 px-1.5 py-0.5 text-blue-400"
301
+ : "text-ash"
302
+ }`}
303
+ >
304
+ {truncate(row.valueB, 36)}
305
+ </span>
306
+ ) : (
307
+ <span className="font-mono text-[11px] italic text-ash-dim">
308
+ {"—"} not set {"—"}
309
+ </span>
310
+ )}
311
+ </Table.Cell>
312
+ <Table.Cell>
450
313
  <span
451
- style={{
452
- fontFamily: theme.mono,
453
- fontSize: 11,
454
- color: theme.textDim,
455
- fontStyle: "italic",
456
- }}
314
+ className={`inline-block rounded-sm border px-2 py-0.5 font-mono text-[10px] font-semibold ${meta.text} ${meta.bg} ${meta.border}`}
457
315
  >
458
- {"\u2014"} not set {"\u2014"}
316
+ {meta.label}
459
317
  </span>
460
- )}
461
- </div>
462
-
463
- {/* Status badge */}
464
- <span
465
- style={{
466
- fontFamily: theme.mono,
467
- fontSize: 10,
468
- fontWeight: 600,
469
- color: meta.color,
470
- background: `${meta.color}18`,
471
- border: `1px solid ${meta.color}33`,
472
- borderRadius: 3,
473
- padding: "2px 8px",
474
- display: "inline-block",
475
- }}
476
- >
477
- {meta.label}
478
- </span>
479
- </div>
480
- );
481
- })}
482
- </div>
318
+ </Table.Cell>
319
+ </Table.Row>
320
+ );
321
+ })}
322
+ </tbody>
323
+ </Table>
483
324
 
484
325
  {/* Inline fix hint */}
485
326
  {missingRows.length > 0 && (
486
327
  <div
487
328
  data-testid="fix-hint"
488
- style={{
489
- marginTop: 20,
490
- padding: "14px 18px",
491
- background: theme.surface,
492
- border: `1px solid ${theme.border}`,
493
- borderRadius: 8,
494
- display: "flex",
495
- flexDirection: "column",
496
- gap: 10,
497
- }}
329
+ className="mt-5 flex flex-col gap-2.5 rounded-md border border-edge bg-ink-850 px-[18px] py-3.5"
498
330
  >
499
331
  {missingRows.map((row) => {
500
332
  const missingEnv = row.status === "missing_a" ? envA : envB;
501
333
  const cmd = `clef set ${ns}/${missingEnv} ${row.key}`;
502
334
  return (
503
- <div
504
- key={row.key}
505
- style={{
506
- display: "flex",
507
- alignItems: "center",
508
- gap: 12,
509
- }}
510
- >
511
- <span style={{ fontSize: 16 }}>{"\uD83D\uDCA1"}</span>
512
- <span
513
- style={{
514
- fontFamily: theme.sans,
515
- fontSize: 12,
516
- color: theme.textMuted,
517
- flex: 1,
518
- }}
519
- >
520
- <strong style={{ color: theme.text }}>{row.key}</strong> is missing in{" "}
335
+ <div key={row.key} className="flex items-center gap-3">
336
+ <span className="text-[16px]">{"💡"}</span>
337
+ <span className="flex-1 font-sans text-[12px] text-ash">
338
+ <strong className="text-bone">{row.key}</strong> is missing in{" "}
521
339
  <EnvBadge env={missingEnv} small />. Run{" "}
522
- <code
523
- style={{
524
- fontFamily: theme.mono,
525
- fontSize: 11,
526
- color: theme.accent,
527
- background: theme.accentDim,
528
- padding: "1px 6px",
529
- borderRadius: 3,
530
- }}
531
- >
340
+ <code className="rounded-sm bg-gold-500/15 px-1.5 py-px font-mono text-[11px] text-gold-500">
532
341
  {cmd}
533
342
  </code>{" "}
534
343
  to add it.