@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,8 +1,7 @@
1
1
  import React, { useState, useEffect, useCallback, useRef } 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";
4
+ import { Toolbar, EmptyState } from "../primitives";
6
5
  import type { ClefManifest, Recipient, AgeKeyValidation } from "@clef-sh/core";
7
6
  import type { ViewName } from "../components/Sidebar";
8
7
 
@@ -11,6 +10,12 @@ interface RecipientsScreenProps {
11
10
  setView: (view: ViewName) => void;
12
11
  }
13
12
 
13
+ const KEY_INPUT_BASE =
14
+ "w-full box-border rounded-md bg-ink-950 px-3 py-2 font-mono text-[12px] text-bone outline-none focus-visible:border-gold-500 placeholder:text-ash-dim";
15
+
16
+ const TEXT_INPUT_BASE =
17
+ "w-full box-border rounded-md border border-edge bg-ink-950 px-3 py-2 font-sans text-[13px] text-bone outline-none focus-visible:border-gold-500 placeholder:text-ash-dim";
18
+
14
19
  export function RecipientsScreen({ manifest: _manifest, setView }: RecipientsScreenProps) {
15
20
  const [recipients, setRecipients] = useState<Recipient[]>([]);
16
21
  const [totalFiles, setTotalFiles] = useState(0);
@@ -21,18 +26,15 @@ export function RecipientsScreen({ manifest: _manifest, setView }: RecipientsScr
21
26
  const [loading, setLoading] = useState(false);
22
27
  const [error, setError] = useState<string | null>(null);
23
28
 
24
- // Remove flow state
25
29
  const [removeTarget, setRemoveTarget] = useState<Recipient | null>(null);
26
30
  const [removeStep, setRemoveStep] = useState<0 | 1 | 2>(0);
27
31
  const [acknowledgedWarning, setAcknowledgedWarning] = useState(false);
28
32
 
29
- // Post-removal banner
30
33
  const [removalBanner, setRemovalBanner] = useState<{
31
34
  name: string;
32
35
  targets: string[];
33
36
  } | null>(null);
34
37
 
35
- // Debounce timer ref for key validation
36
38
  const validateTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
37
39
 
38
40
  const loadRecipients = useCallback(async () => {
@@ -52,17 +54,12 @@ export function RecipientsScreen({ manifest: _manifest, setView }: RecipientsScr
52
54
  loadRecipients();
53
55
  }, [loadRecipients]);
54
56
 
55
- // Real-time key validation with debounce
56
57
  useEffect(() => {
57
58
  if (!addKey.trim()) {
58
59
  setKeyValidation(null);
59
60
  return;
60
61
  }
61
-
62
- if (validateTimerRef.current) {
63
- clearTimeout(validateTimerRef.current);
64
- }
65
-
62
+ if (validateTimerRef.current) clearTimeout(validateTimerRef.current);
66
63
  validateTimerRef.current = setTimeout(async () => {
67
64
  try {
68
65
  const res = await apiFetch(
@@ -78,18 +75,14 @@ export function RecipientsScreen({ manifest: _manifest, setView }: RecipientsScr
78
75
  }, 300);
79
76
 
80
77
  return () => {
81
- if (validateTimerRef.current) {
82
- clearTimeout(validateTimerRef.current);
83
- }
78
+ if (validateTimerRef.current) clearTimeout(validateTimerRef.current);
84
79
  };
85
80
  }, [addKey]);
86
81
 
87
82
  const handleAdd = async () => {
88
83
  if (!keyValidation?.valid) return;
89
-
90
84
  setLoading(true);
91
85
  setError(null);
92
-
93
86
  try {
94
87
  const res = await apiFetch("/api/recipients/add", {
95
88
  method: "POST",
@@ -99,13 +92,11 @@ export function RecipientsScreen({ manifest: _manifest, setView }: RecipientsScr
99
92
  label: addLabel.trim() || undefined,
100
93
  }),
101
94
  });
102
-
103
95
  if (!res.ok) {
104
96
  const data = await res.json();
105
97
  setError(data.error ?? "Failed to add recipient");
106
98
  return;
107
99
  }
108
-
109
100
  const data = await res.json();
110
101
  setRecipients(data.recipients);
111
102
  setShowAddForm(false);
@@ -133,33 +124,26 @@ export function RecipientsScreen({ manifest: _manifest, setView }: RecipientsScr
133
124
 
134
125
  const handleRemove = async () => {
135
126
  if (!removeTarget) return;
136
-
137
127
  setLoading(true);
138
128
  setError(null);
139
-
140
129
  try {
141
130
  const res = await apiFetch("/api/recipients/remove", {
142
131
  method: "POST",
143
132
  headers: { "Content-Type": "application/json" },
144
133
  body: JSON.stringify({ key: removeTarget.key }),
145
134
  });
146
-
147
135
  if (!res.ok) {
148
136
  const data = await res.json();
149
137
  setError(data.error ?? "Failed to remove recipient");
150
138
  return;
151
139
  }
152
-
153
140
  const data = await res.json();
154
141
  setRecipients(data.recipients);
155
-
156
- // Show rotation reminder
157
142
  const name = removeTarget.label ?? removeTarget.preview;
158
143
  setRemovalBanner({
159
144
  name,
160
145
  targets: data.rotationReminder ?? [],
161
146
  });
162
-
163
147
  cancelRemove();
164
148
  } catch (err) {
165
149
  setError(err instanceof Error ? err.message : "Failed to remove recipient");
@@ -168,94 +152,59 @@ export function RecipientsScreen({ manifest: _manifest, setView }: RecipientsScr
168
152
  }
169
153
  };
170
154
 
155
+ const keyBorderClass = keyValidation
156
+ ? keyValidation.valid
157
+ ? "border border-go-500/40"
158
+ : "border border-stop-500/40"
159
+ : "border border-edge";
160
+
171
161
  return (
172
- <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
173
- <TopBar
174
- title="Recipients"
175
- subtitle="clef recipients -- manage age encryption keys"
176
- actions={
177
- !showAddForm && removeStep === 0 ? (
162
+ <div className="flex flex-1 flex-col overflow-hidden">
163
+ <Toolbar>
164
+ <div>
165
+ <Toolbar.Title>Recipients</Toolbar.Title>
166
+ <Toolbar.Subtitle>clef recipients -- manage age encryption keys</Toolbar.Subtitle>
167
+ </div>
168
+ {!showAddForm && removeStep === 0 && (
169
+ <Toolbar.Actions>
178
170
  <Button variant="primary" onClick={() => setShowAddForm(true)}>
179
171
  + Add recipient
180
172
  </Button>
181
- ) : undefined
182
- }
183
- />
173
+ </Toolbar.Actions>
174
+ )}
175
+ </Toolbar>
184
176
 
185
- <div style={{ flex: 1, overflow: "auto", padding: 24 }}>
186
- <div style={{ maxWidth: 620, margin: "0 auto" }}>
187
- {/* Error banner */}
177
+ <div className="flex-1 overflow-auto p-6">
178
+ <div className="mx-auto max-w-[620px]">
188
179
  {error && (
189
- <div
190
- style={{
191
- background: theme.redDim,
192
- border: `1px solid ${theme.red}44`,
193
- borderRadius: 8,
194
- padding: "12px 16px",
195
- marginBottom: 16,
196
- fontFamily: theme.sans,
197
- fontSize: 13,
198
- color: theme.red,
199
- }}
200
- >
180
+ <div className="mb-4 rounded-lg border border-stop-500/30 bg-stop-500/10 px-4 py-3 font-sans text-[13px] text-stop-500">
201
181
  {error}
202
182
  </div>
203
183
  )}
204
184
 
205
- {/* Post-removal rotation reminder banner */}
206
185
  {removalBanner && (
207
186
  <div
208
187
  data-testid="rotation-banner"
209
- style={{
210
- background: theme.yellowDim,
211
- border: `1px solid ${theme.yellow}44`,
212
- borderRadius: 8,
213
- padding: "14px 18px",
214
- marginBottom: 20,
215
- }}
188
+ className="mb-5 rounded-lg border border-warn-500/30 bg-warn-500/10 px-4 py-3.5"
216
189
  >
217
- <div
218
- style={{
219
- fontFamily: theme.sans,
220
- fontSize: 13,
221
- fontWeight: 600,
222
- color: theme.yellow,
223
- marginBottom: 8,
224
- }}
225
- >
190
+ <div className="mb-2 font-sans text-[13px] font-semibold text-warn-500">
226
191
  Rotation reminder
227
192
  </div>
228
- <div
229
- style={{
230
- fontFamily: theme.sans,
231
- fontSize: 13,
232
- color: theme.text,
233
- marginBottom: 10,
234
- lineHeight: 1.5,
235
- }}
236
- >
193
+ <div className="mb-2.5 font-sans text-[13px] leading-relaxed text-bone">
237
194
  <strong>{removalBanner.name}</strong> has been removed and files re-encrypted.
238
195
  However, the removed key may still decrypt old versions of these files from git
239
196
  history. Rotate secret values in the following targets to complete revocation:
240
197
  </div>
241
198
  {removalBanner.targets.length > 0 && (
242
- <div
243
- style={{
244
- fontFamily: theme.mono,
245
- fontSize: 11,
246
- color: theme.textMuted,
247
- marginBottom: 12,
248
- paddingLeft: 12,
249
- }}
250
- >
199
+ <div className="mb-3 pl-3 font-mono text-[11px] text-ash">
251
200
  {removalBanner.targets.map((t) => (
252
- <div key={t} style={{ marginBottom: 2 }}>
201
+ <div key={t} className="mb-px">
253
202
  {t}
254
203
  </div>
255
204
  ))}
256
205
  </div>
257
206
  )}
258
- <div style={{ display: "flex", gap: 10 }}>
207
+ <div className="flex gap-2.5">
259
208
  <Button variant="primary" onClick={() => setView("matrix")}>
260
209
  Go to Matrix to rotate
261
210
  </Button>
@@ -266,32 +215,16 @@ export function RecipientsScreen({ manifest: _manifest, setView }: RecipientsScr
266
215
  </div>
267
216
  )}
268
217
 
269
- {/* Add form */}
270
218
  {showAddForm && (
271
219
  <div
272
220
  data-testid="add-form"
273
- style={{
274
- background: theme.surface,
275
- border: `1px solid ${theme.border}`,
276
- borderRadius: 10,
277
- padding: 20,
278
- marginBottom: 24,
279
- }}
221
+ className="mb-6 rounded-lg border border-edge bg-ink-850 p-5"
280
222
  >
281
- <div
282
- style={{
283
- fontFamily: theme.sans,
284
- fontSize: 14,
285
- fontWeight: 600,
286
- color: theme.text,
287
- marginBottom: 16,
288
- }}
289
- >
223
+ <div className="mb-4 font-sans text-[14px] font-semibold text-bone">
290
224
  Add recipient
291
225
  </div>
292
226
 
293
- {/* Key input */}
294
- <div style={{ marginBottom: 14 }}>
227
+ <div className="mb-3.5">
295
228
  <Label>Age public key</Label>
296
229
  <input
297
230
  type="text"
@@ -299,53 +232,19 @@ export function RecipientsScreen({ manifest: _manifest, setView }: RecipientsScr
299
232
  onChange={(e) => setAddKey(e.target.value)}
300
233
  placeholder="age1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"
301
234
  data-testid="add-key-input"
302
- style={{
303
- width: "100%",
304
- background: theme.bg,
305
- border: `1px solid ${
306
- keyValidation
307
- ? keyValidation.valid
308
- ? theme.green + "66"
309
- : theme.red + "66"
310
- : theme.border
311
- }`,
312
- borderRadius: 6,
313
- padding: "8px 12px",
314
- fontFamily: theme.mono,
315
- fontSize: 12,
316
- color: theme.text,
317
- outline: "none",
318
- boxSizing: "border-box",
319
- }}
235
+ className={`${KEY_INPUT_BASE} ${keyBorderClass}`}
320
236
  />
321
237
  {keyValidation && !keyValidation.valid && (
322
- <div
323
- style={{
324
- fontFamily: theme.sans,
325
- fontSize: 11,
326
- color: theme.red,
327
- marginTop: 4,
328
- }}
329
- >
238
+ <div className="mt-1 font-sans text-[11px] text-stop-500">
330
239
  {keyValidation.error}
331
240
  </div>
332
241
  )}
333
242
  {keyValidation?.valid && (
334
- <div
335
- style={{
336
- fontFamily: theme.sans,
337
- fontSize: 11,
338
- color: theme.green,
339
- marginTop: 4,
340
- }}
341
- >
342
- Valid age public key
343
- </div>
243
+ <div className="mt-1 font-sans text-[11px] text-go-500">Valid age public key</div>
344
244
  )}
345
245
  </div>
346
246
 
347
- {/* Label input */}
348
- <div style={{ marginBottom: 14 }}>
247
+ <div className="mb-3.5">
349
248
  <Label>Label (optional)</Label>
350
249
  <input
351
250
  type="text"
@@ -353,41 +252,16 @@ export function RecipientsScreen({ manifest: _manifest, setView }: RecipientsScr
353
252
  onChange={(e) => setAddLabel(e.target.value)}
354
253
  placeholder="e.g. alice@example.com"
355
254
  data-testid="add-label-input"
356
- style={{
357
- width: "100%",
358
- background: theme.bg,
359
- border: `1px solid ${theme.border}`,
360
- borderRadius: 6,
361
- padding: "8px 12px",
362
- fontFamily: theme.sans,
363
- fontSize: 13,
364
- color: theme.text,
365
- outline: "none",
366
- boxSizing: "border-box",
367
- }}
255
+ className={TEXT_INPUT_BASE}
368
256
  />
369
257
  </div>
370
258
 
371
- {/* Re-encryption warning */}
372
- <div
373
- style={{
374
- marginBottom: 16,
375
- padding: "10px 14px",
376
- background: theme.yellowDim,
377
- border: `1px solid ${theme.yellow}44`,
378
- borderRadius: 6,
379
- fontFamily: theme.sans,
380
- fontSize: 12,
381
- color: theme.yellow,
382
- lineHeight: 1.5,
383
- }}
384
- >
259
+ <div className="mb-4 rounded-md border border-warn-500/30 bg-warn-500/10 px-3.5 py-2.5 font-sans text-[12px] leading-relaxed text-warn-500">
385
260
  Adding a recipient will re-encrypt {totalFiles} file
386
261
  {totalFiles !== 1 ? "s" : ""}. This may take a moment.
387
262
  </div>
388
263
 
389
- {/* Actions */}
390
- <div style={{ display: "flex", gap: 10 }}>
264
+ <div className="flex gap-2.5">
391
265
  <Button
392
266
  variant="ghost"
393
267
  onClick={() => {
@@ -411,87 +285,37 @@ export function RecipientsScreen({ manifest: _manifest, setView }: RecipientsScr
411
285
  </div>
412
286
  )}
413
287
 
414
- {/* Remove flow: Step 1 — Revocation warning */}
415
288
  {removeStep === 1 && removeTarget && (
416
289
  <div
417
290
  data-testid="remove-dialog"
418
- style={{
419
- background: theme.surface,
420
- border: `1px solid ${theme.red}44`,
421
- borderRadius: 10,
422
- padding: 20,
423
- marginBottom: 24,
424
- }}
291
+ className="mb-6 rounded-lg border border-stop-500/30 bg-ink-850 p-5"
425
292
  >
426
- <div
427
- style={{
428
- fontFamily: theme.sans,
429
- fontSize: 14,
430
- fontWeight: 600,
431
- color: theme.red,
432
- marginBottom: 12,
433
- }}
434
- >
293
+ <div className="mb-3 font-sans text-[14px] font-semibold text-stop-500">
435
294
  Remove recipient
436
295
  </div>
437
-
438
- <div
439
- style={{
440
- fontFamily: theme.sans,
441
- fontSize: 13,
442
- color: theme.text,
443
- lineHeight: 1.6,
444
- marginBottom: 16,
445
- }}
446
- >
296
+ <div className="mb-4 font-sans text-[13px] leading-relaxed text-bone">
447
297
  You are about to remove{" "}
448
- <strong style={{ color: theme.accent }}>
298
+ <strong className="text-gold-500">
449
299
  {removeTarget.label ?? removeTarget.preview}
450
300
  </strong>{" "}
451
301
  ({removeTarget.preview}). All {totalFiles} encrypted file
452
302
  {totalFiles !== 1 ? "s" : ""} will be re-encrypted without this key.
453
303
  </div>
454
-
455
- <div
456
- style={{
457
- background: theme.redDim,
458
- border: `1px solid ${theme.red}44`,
459
- borderRadius: 6,
460
- padding: "12px 14px",
461
- marginBottom: 16,
462
- fontFamily: theme.sans,
463
- fontSize: 12,
464
- color: theme.red,
465
- lineHeight: 1.5,
466
- }}
467
- >
304
+ <div className="mb-4 rounded-md border border-stop-500/30 bg-stop-500/10 px-3.5 py-3 font-sans text-[12px] leading-relaxed text-stop-500">
468
305
  Re-encryption only removes <em>future</em> access. The removed key can still decrypt
469
306
  old versions from git history. You must rotate all secret values after removal.
470
307
  </div>
471
-
472
- <label
473
- style={{
474
- display: "flex",
475
- alignItems: "flex-start",
476
- gap: 10,
477
- cursor: "pointer",
478
- fontFamily: theme.sans,
479
- fontSize: 13,
480
- color: theme.text,
481
- marginBottom: 16,
482
- }}
483
- >
308
+ <label className="mb-4 flex cursor-pointer items-start gap-2.5 font-sans text-[13px] text-bone">
484
309
  <input
485
310
  type="checkbox"
486
311
  checked={acknowledgedWarning}
487
312
  onChange={(e) => setAcknowledgedWarning(e.target.checked)}
488
313
  data-testid="acknowledge-checkbox"
489
- style={{ accentColor: theme.red, marginTop: 2 }}
314
+ className="mt-0.5 accent-stop-500"
490
315
  />
491
316
  I understand — I will rotate secrets after removal
492
317
  </label>
493
-
494
- <div style={{ display: "flex", gap: 10 }}>
318
+ <div className="flex gap-2.5">
495
319
  <Button variant="ghost" onClick={cancelRemove}>
496
320
  Cancel
497
321
  </Button>
@@ -506,48 +330,23 @@ export function RecipientsScreen({ manifest: _manifest, setView }: RecipientsScr
506
330
  </div>
507
331
  )}
508
332
 
509
- {/* Remove flow: Step 2 — Final confirmation */}
510
333
  {removeStep === 2 && removeTarget && (
511
334
  <div
512
335
  data-testid="remove-confirm-dialog"
513
- style={{
514
- background: theme.surface,
515
- border: `1px solid ${theme.red}44`,
516
- borderRadius: 10,
517
- padding: 20,
518
- marginBottom: 24,
519
- }}
336
+ className="mb-6 rounded-lg border border-stop-500/30 bg-ink-850 p-5"
520
337
  >
521
- <div
522
- style={{
523
- fontFamily: theme.sans,
524
- fontSize: 14,
525
- fontWeight: 600,
526
- color: theme.red,
527
- marginBottom: 12,
528
- }}
529
- >
338
+ <div className="mb-3 font-sans text-[14px] font-semibold text-stop-500">
530
339
  Confirm removal
531
340
  </div>
532
-
533
- <div
534
- style={{
535
- fontFamily: theme.sans,
536
- fontSize: 13,
537
- color: theme.text,
538
- lineHeight: 1.6,
539
- marginBottom: 16,
540
- }}
541
- >
341
+ <div className="mb-4 font-sans text-[13px] leading-relaxed text-bone">
542
342
  This will remove{" "}
543
- <strong style={{ color: theme.accent }}>
343
+ <strong className="text-gold-500">
544
344
  {removeTarget.label ?? removeTarget.preview}
545
345
  </strong>{" "}
546
346
  and re-encrypt all {totalFiles} file{totalFiles !== 1 ? "s" : ""}. This cannot be
547
347
  undone.
548
348
  </div>
549
-
550
- <div style={{ display: "flex", gap: 10 }}>
349
+ <div className="flex gap-2.5">
551
350
  <Button variant="ghost" onClick={cancelRemove}>
552
351
  Cancel
553
352
  </Button>
@@ -558,97 +357,37 @@ export function RecipientsScreen({ manifest: _manifest, setView }: RecipientsScr
558
357
  </div>
559
358
  )}
560
359
 
561
- {/* Recipients list */}
562
360
  {recipients.length === 0 && !showAddForm && (
563
- <div
564
- style={{
565
- textAlign: "center",
566
- paddingTop: 40,
567
- fontFamily: theme.sans,
568
- fontSize: 14,
569
- color: theme.textMuted,
570
- }}
571
- >
572
- No recipients configured. Add an age public key to get started.
573
- </div>
361
+ <EmptyState
362
+ title="No recipients configured"
363
+ body="Add an age public key to get started."
364
+ />
574
365
  )}
575
366
 
576
367
  {recipients.length > 0 && (
577
368
  <div>
578
- <div
579
- style={{
580
- fontFamily: theme.sans,
581
- fontSize: 12,
582
- fontWeight: 600,
583
- color: theme.textMuted,
584
- letterSpacing: "0.05em",
585
- textTransform: "uppercase",
586
- marginBottom: 10,
587
- }}
588
- >
369
+ <div className="mb-2.5 font-sans text-[12px] font-semibold uppercase tracking-[0.05em] text-ash">
589
370
  Recipients ({recipients.length})
590
371
  </div>
591
-
592
372
  {recipients.map((r) => (
593
373
  <div
594
374
  key={r.key}
595
375
  data-testid="recipient-row"
596
- style={{
597
- display: "flex",
598
- alignItems: "center",
599
- gap: 14,
600
- padding: "12px 16px",
601
- background: theme.surface,
602
- border: `1px solid ${theme.border}`,
603
- borderRadius: 8,
604
- marginBottom: 8,
605
- }}
376
+ className="mb-2 flex items-center gap-3.5 rounded-lg border border-edge bg-ink-850 px-4 py-3"
606
377
  >
607
- <div
608
- style={{
609
- width: 32,
610
- height: 32,
611
- borderRadius: 6,
612
- background: theme.accentDim,
613
- border: `1px solid ${theme.accent}44`,
614
- display: "flex",
615
- alignItems: "center",
616
- justifyContent: "center",
617
- fontSize: 14,
618
- flexShrink: 0,
619
- }}
620
- >
621
- {"\uD83D\uDD11"}
378
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md border border-gold-500/30 bg-gold-500/10 text-[14px]">
379
+ {"🔑"}
622
380
  </div>
623
-
624
- <div style={{ flex: 1, minWidth: 0 }}>
381
+ <div className="min-w-0 flex-1">
625
382
  {r.label && (
626
- <div
627
- style={{
628
- fontFamily: theme.sans,
629
- fontSize: 13,
630
- fontWeight: 600,
631
- color: theme.text,
632
- marginBottom: 2,
633
- }}
634
- >
383
+ <div className="mb-0.5 font-sans text-[13px] font-semibold text-bone">
635
384
  {r.label}
636
385
  </div>
637
386
  )}
638
- <div
639
- style={{
640
- fontFamily: theme.mono,
641
- fontSize: 11,
642
- color: theme.textMuted,
643
- overflow: "hidden",
644
- textOverflow: "ellipsis",
645
- whiteSpace: "nowrap",
646
- }}
647
- >
387
+ <div className="overflow-hidden text-ellipsis whitespace-nowrap font-mono text-[11px] text-ash">
648
388
  {r.preview}
649
389
  </div>
650
390
  </div>
651
-
652
391
  <Button
653
392
  variant="ghost"
654
393
  onClick={() => startRemove(r)}
@@ -658,15 +397,7 @@ export function RecipientsScreen({ manifest: _manifest, setView }: RecipientsScr
658
397
  </Button>
659
398
  </div>
660
399
  ))}
661
-
662
- <div
663
- style={{
664
- fontFamily: theme.mono,
665
- fontSize: 11,
666
- color: theme.textDim,
667
- marginTop: 12,
668
- }}
669
- >
400
+ <div className="mt-3 font-mono text-[11px] text-ash-dim">
670
401
  {totalFiles} encrypted file{totalFiles !== 1 ? "s" : ""} in the matrix
671
402
  </div>
672
403
  </div>
@@ -679,17 +410,7 @@ export function RecipientsScreen({ manifest: _manifest, setView }: RecipientsScr
679
410
 
680
411
  function Label({ children }: { children: React.ReactNode }) {
681
412
  return (
682
- <div
683
- style={{
684
- fontFamily: theme.sans,
685
- fontSize: 12,
686
- fontWeight: 600,
687
- color: theme.textMuted,
688
- marginBottom: 6,
689
- letterSpacing: "0.05em",
690
- textTransform: "uppercase",
691
- }}
692
- >
413
+ <div className="mb-1.5 font-sans text-[12px] font-semibold uppercase tracking-[0.05em] text-ash">
693
414
  {children}
694
415
  </div>
695
416
  );