@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.
- package/dist/client/assets/index-DPWHjBbB.js +34 -0
- package/dist/client/assets/index-qsLTYpc9.css +2 -0
- package/dist/client/clef.svg +2 -0
- package/dist/client/index.html +3 -31
- package/dist/client-lib/components/Button.d.ts +1 -1
- package/dist/client-lib/components/Button.d.ts.map +1 -1
- package/dist/client-lib/components/CopyButton.d.ts.map +1 -1
- package/dist/client-lib/components/EnvBadge.d.ts.map +1 -1
- package/dist/client-lib/components/MatrixGrid.d.ts.map +1 -1
- package/dist/client-lib/components/Sidebar.d.ts +1 -1
- package/dist/client-lib/components/Sidebar.d.ts.map +1 -1
- package/dist/client-lib/components/StatusDot.d.ts.map +1 -1
- package/dist/client-lib/components/SyncPanel.d.ts.map +1 -1
- package/dist/client-lib/components/TopBar.d.ts +6 -0
- package/dist/client-lib/components/TopBar.d.ts.map +1 -1
- package/dist/client-lib/primitives/Badge.d.ts +11 -0
- package/dist/client-lib/primitives/Badge.d.ts.map +1 -0
- package/dist/client-lib/primitives/Card.d.ts +28 -0
- package/dist/client-lib/primitives/Card.d.ts.map +1 -0
- package/dist/client-lib/primitives/Dialog.d.ts +30 -0
- package/dist/client-lib/primitives/Dialog.d.ts.map +1 -0
- package/dist/client-lib/primitives/EmptyState.d.ts +10 -0
- package/dist/client-lib/primitives/EmptyState.d.ts.map +1 -0
- package/dist/client-lib/primitives/Field.d.ts +36 -0
- package/dist/client-lib/primitives/Field.d.ts.map +1 -0
- package/dist/client-lib/primitives/Input.d.ts +6 -0
- package/dist/client-lib/primitives/Input.d.ts.map +1 -0
- package/dist/client-lib/primitives/Stat.d.ts +11 -0
- package/dist/client-lib/primitives/Stat.d.ts.map +1 -0
- package/dist/client-lib/primitives/Table.d.ts +37 -0
- package/dist/client-lib/primitives/Table.d.ts.map +1 -0
- package/dist/client-lib/primitives/Tabs.d.ts +29 -0
- package/dist/client-lib/primitives/Tabs.d.ts.map +1 -0
- package/dist/client-lib/primitives/Toast.d.ts +16 -0
- package/dist/client-lib/primitives/Toast.d.ts.map +1 -0
- package/dist/client-lib/primitives/Toolbar.d.ts +29 -0
- package/dist/client-lib/primitives/Toolbar.d.ts.map +1 -0
- package/dist/client-lib/primitives/index.d.ts +23 -0
- package/dist/client-lib/primitives/index.d.ts.map +1 -0
- package/dist/client-lib/theme.d.ts +18 -41
- package/dist/client-lib/theme.d.ts.map +1 -1
- package/dist/server/api.d.ts.map +1 -1
- package/dist/server/api.js +215 -0
- package/dist/server/api.js.map +1 -1
- package/dist/server/envelope.d.ts +15 -0
- package/dist/server/envelope.d.ts.map +1 -0
- package/dist/server/envelope.js +310 -0
- package/dist/server/envelope.js.map +1 -0
- package/package.json +7 -2
- package/src/client/App.tsx +16 -41
- package/src/client/components/Button.tsx +13 -22
- package/src/client/components/CopyButton.tsx +5 -12
- package/src/client/components/EnvBadge.tsx +30 -15
- package/src/client/components/MatrixGrid.tsx +108 -252
- package/src/client/components/Sidebar.tsx +123 -199
- package/src/client/components/StatusDot.tsx +10 -15
- package/src/client/components/SyncPanel.tsx +14 -62
- package/src/client/components/TopBar.tsx +11 -36
- package/src/client/index.html +1 -30
- package/src/client/main.tsx +1 -0
- package/src/client/primitives/Badge.test.tsx +47 -0
- package/src/client/primitives/Badge.tsx +64 -0
- package/src/client/primitives/Card.test.tsx +50 -0
- package/src/client/primitives/Card.tsx +85 -0
- package/src/client/primitives/Dialog.test.tsx +55 -0
- package/src/client/primitives/Dialog.tsx +96 -0
- package/src/client/primitives/EmptyState.test.tsx +25 -0
- package/src/client/primitives/EmptyState.tsx +38 -0
- package/src/client/primitives/Field.test.tsx +46 -0
- package/src/client/primitives/Field.tsx +95 -0
- package/src/client/primitives/Input.tsx +26 -0
- package/src/client/primitives/Stat.test.tsx +32 -0
- package/src/client/primitives/Stat.tsx +52 -0
- package/src/client/primitives/Table.test.tsx +58 -0
- package/src/client/primitives/Table.tsx +113 -0
- package/src/client/primitives/Tabs.test.tsx +44 -0
- package/src/client/primitives/Tabs.tsx +100 -0
- package/src/client/primitives/Toast.test.tsx +77 -0
- package/src/client/primitives/Toast.tsx +89 -0
- package/src/client/primitives/Toolbar.test.tsx +50 -0
- package/src/client/primitives/Toolbar.tsx +86 -0
- package/src/client/primitives/index.ts +43 -0
- package/src/client/public/clef.svg +2 -0
- package/src/client/screens/BackendScreen.tsx +104 -363
- package/src/client/screens/DiffView.tsx +187 -378
- package/src/client/screens/EnvelopeScreen.test.tsx +542 -0
- package/src/client/screens/EnvelopeScreen.tsx +948 -0
- package/src/client/screens/GitLogView.tsx +48 -106
- package/src/client/screens/ImportScreen.tsx +105 -308
- package/src/client/screens/LintView.tsx +184 -379
- package/src/client/screens/ManifestScreen.tsx +283 -445
- package/src/client/screens/MatrixView.tsx +75 -91
- package/src/client/screens/NamespaceEditor.tsx +234 -609
- package/src/client/screens/PolicyView.tsx +183 -453
- package/src/client/screens/RecipientsScreen.tsx +71 -350
- package/src/client/screens/ResetScreen.tsx +67 -237
- package/src/client/screens/ScanScreen.tsx +85 -249
- package/src/client/screens/SchemaEditor.test.tsx +237 -0
- package/src/client/screens/SchemaEditor.tsx +435 -0
- package/src/client/screens/ServiceIdentitiesScreen.tsx +251 -788
- package/src/client/styles.css +77 -0
- package/src/client/theme.ts +27 -48
- 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
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
173
|
+
</Toolbar.Actions>
|
|
174
|
+
)}
|
|
175
|
+
</Toolbar>
|
|
184
176
|
|
|
185
|
-
<div
|
|
186
|
-
<div
|
|
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
|
-
|
|
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}
|
|
201
|
+
<div key={t} className="mb-px">
|
|
253
202
|
{t}
|
|
254
203
|
</div>
|
|
255
204
|
))}
|
|
256
205
|
</div>
|
|
257
206
|
)}
|
|
258
|
-
<div
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
<
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
);
|