@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 } 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 } from "../primitives";
6
5
  import type { ClefManifest } from "@clef-sh/core";
7
6
  import type { ViewName } from "../components/Sidebar";
8
7
 
@@ -27,6 +26,12 @@ interface ApplyResult {
27
26
 
28
27
  type ImportFormatOption = "auto" | "dotenv" | "json" | "yaml";
29
28
 
29
+ const SELECT_CLASSES =
30
+ "w-full rounded-md border border-edge bg-ink-850 px-2.5 py-1.5 font-sans text-[13px] text-bone outline-none cursor-pointer focus-visible:border-gold-500";
31
+
32
+ const TEXTAREA_CLASSES =
33
+ "w-full box-border rounded-lg border border-edge bg-ink-850 p-3.5 font-mono text-[12px] text-bone outline-none resize-y focus-visible:border-gold-500";
34
+
30
35
  export function ImportScreen({ manifest, setView }: ImportScreenProps) {
31
36
  const [step, setStep] = useState<1 | 2 | 3>(1);
32
37
  const [namespace, setNamespace] = useState("");
@@ -39,7 +44,6 @@ export function ImportScreen({ manifest, setView }: ImportScreenProps) {
39
44
  const [loading, setLoading] = useState(false);
40
45
  const [error, setError] = useState<string | null>(null);
41
46
 
42
- // Set defaults from manifest
43
47
  useEffect(() => {
44
48
  if (manifest) {
45
49
  if (!namespace && manifest.namespaces.length > 0) {
@@ -98,7 +102,6 @@ export function ImportScreen({ manifest, setView }: ImportScreenProps) {
98
102
  setLoading(true);
99
103
  setError(null);
100
104
 
101
- // Keys to import: wouldImport + any overwrite-toggled ones
102
105
  const keysToImport = [
103
106
  ...preview.wouldImport,
104
107
  ...preview.wouldSkip.filter((s) => overwriteKeys.includes(s.key)).map((s) => s.key),
@@ -125,7 +128,6 @@ export function ImportScreen({ manifest, setView }: ImportScreenProps) {
125
128
 
126
129
  const data: ApplyResult = await res.json();
127
130
  setApplyResult(data);
128
- // Clear content after successful apply
129
131
  setContent("");
130
132
  setStep(3);
131
133
  } catch (err) {
@@ -155,67 +157,43 @@ export function ImportScreen({ manifest, setView }: ImportScreenProps) {
155
157
  overwriteKeys.filter((k) => preview?.wouldSkip.some((s) => s.key === k)).length;
156
158
 
157
159
  return (
158
- <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
159
- <TopBar title="Import" subtitle="clef import — bulk migrate secrets" />
160
+ <div className="flex flex-1 flex-col overflow-hidden">
161
+ <Toolbar>
162
+ <div>
163
+ <Toolbar.Title>Import</Toolbar.Title>
164
+ <Toolbar.Subtitle>clef import — bulk migrate secrets</Toolbar.Subtitle>
165
+ </div>
166
+ </Toolbar>
160
167
 
161
- <div style={{ flex: 1, overflow: "auto", padding: 24 }}>
162
- <div style={{ maxWidth: 620, margin: "0 auto" }}>
168
+ <div className="flex-1 overflow-auto p-6">
169
+ <div className="mx-auto max-w-[620px]">
163
170
  {/* Step indicator */}
164
- <div
165
- style={{
166
- display: "flex",
167
- alignItems: "center",
168
- gap: 0,
169
- marginBottom: 32,
170
- }}
171
- >
171
+ <div className="mb-8 flex items-center">
172
172
  {([1, 2, 3] as const).map((s, i) => (
173
173
  <React.Fragment key={s}>
174
- <div
175
- style={{
176
- display: "flex",
177
- alignItems: "center",
178
- gap: 8,
179
- }}
180
- >
174
+ <div className="flex items-center gap-2">
181
175
  <div
182
- style={{
183
- width: 24,
184
- height: 24,
185
- borderRadius: "50%",
186
- background: step >= s ? theme.accent : theme.surface,
187
- border: `1px solid ${step >= s ? theme.accent : theme.border}`,
188
- display: "flex",
189
- alignItems: "center",
190
- justifyContent: "center",
191
- fontFamily: theme.mono,
192
- fontSize: 11,
193
- fontWeight: 700,
194
- color: step >= s ? "#000" : theme.textDim,
195
- }}
176
+ className={`flex h-6 w-6 items-center justify-center rounded-full font-mono text-[11px] font-bold ${
177
+ step >= s
178
+ ? "bg-gold-500 border border-gold-500 text-ink-950"
179
+ : "bg-ink-850 border border-edge text-ash-dim"
180
+ }`}
196
181
  >
197
182
  {s}
198
183
  </div>
199
184
  <span
200
- style={{
201
- fontFamily: theme.sans,
202
- fontSize: 12,
203
- color: step >= s ? theme.text : theme.textDim,
204
- fontWeight: step === s ? 600 : 400,
205
- }}
185
+ className={`font-sans text-[12px] ${
186
+ step >= s ? "text-bone" : "text-ash-dim"
187
+ } ${step === s ? "font-semibold" : "font-normal"}`}
206
188
  >
207
189
  {s === 1 ? "Source" : s === 2 ? "Preview" : "Done"}
208
190
  </span>
209
191
  </div>
210
192
  {i < 2 && (
211
193
  <div
212
- style={{
213
- flex: 1,
214
- height: 1,
215
- background: step > s ? theme.accent : theme.border,
216
- margin: "0 12px",
217
- minWidth: 40,
218
- }}
194
+ className={`mx-3 h-px min-w-[40px] flex-1 ${
195
+ step > s ? "bg-gold-500" : "bg-edge"
196
+ }`}
219
197
  />
220
198
  )}
221
199
  </React.Fragment>
@@ -223,18 +201,7 @@ export function ImportScreen({ manifest, setView }: ImportScreenProps) {
223
201
  </div>
224
202
 
225
203
  {error && (
226
- <div
227
- style={{
228
- background: theme.redDim,
229
- border: `1px solid ${theme.red}44`,
230
- borderRadius: 8,
231
- padding: "12px 16px",
232
- marginBottom: 16,
233
- fontFamily: theme.sans,
234
- fontSize: 13,
235
- color: theme.red,
236
- }}
237
- >
204
+ <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">
238
205
  {error}
239
206
  </div>
240
207
  )}
@@ -242,49 +209,49 @@ export function ImportScreen({ manifest, setView }: ImportScreenProps) {
242
209
  {/* ── Step 1: Source ─────────────────────────────────────────── */}
243
210
  {step === 1 && (
244
211
  <div>
245
- {/* Target selectors */}
246
- <div style={{ marginBottom: 20 }}>
212
+ <div className="mb-5">
247
213
  <Label>Target</Label>
248
- <div style={{ display: "flex", gap: 12 }}>
249
- <div style={{ flex: 1 }}>
214
+ <div className="flex gap-3">
215
+ <div className="flex-1">
250
216
  <SubLabel>Namespace</SubLabel>
251
- <Select value={namespace} onChange={(e) => setNamespace(e.target.value)}>
217
+ <select
218
+ value={namespace}
219
+ onChange={(e) => setNamespace(e.target.value)}
220
+ className={SELECT_CLASSES}
221
+ >
252
222
  {namespaces.map((ns) => (
253
223
  <option key={ns.name} value={ns.name}>
254
224
  {ns.name}
255
225
  </option>
256
226
  ))}
257
- </Select>
227
+ </select>
258
228
  </div>
259
- <div style={{ flex: 1 }}>
229
+ <div className="flex-1">
260
230
  <SubLabel>Environment</SubLabel>
261
- <Select value={environment} onChange={(e) => setEnvironment(e.target.value)}>
231
+ <select
232
+ value={environment}
233
+ onChange={(e) => setEnvironment(e.target.value)}
234
+ className={SELECT_CLASSES}
235
+ >
262
236
  {environments.map((env) => (
263
237
  <option key={env.name} value={env.name}>
264
238
  {env.name}
265
239
  </option>
266
240
  ))}
267
- </Select>
241
+ </select>
268
242
  </div>
269
243
  </div>
270
244
  </div>
271
245
 
272
- {/* Format selector */}
273
- <div style={{ marginBottom: 20 }}>
246
+ <div className="mb-5">
274
247
  <Label>Format</Label>
275
- <div style={{ display: "flex", gap: 16 }}>
248
+ <div className="flex gap-4">
276
249
  {(["auto", "dotenv", "json", "yaml"] as const).map((f) => (
277
250
  <label
278
251
  key={f}
279
- style={{
280
- display: "flex",
281
- alignItems: "center",
282
- gap: 6,
283
- cursor: "pointer",
284
- fontFamily: theme.sans,
285
- fontSize: 13,
286
- color: format === f ? theme.text : theme.textMuted,
287
- }}
252
+ className={`flex cursor-pointer items-center gap-1.5 font-sans text-[13px] ${
253
+ format === f ? "text-bone" : "text-ash"
254
+ }`}
288
255
  >
289
256
  <input
290
257
  type="radio"
@@ -292,7 +259,7 @@ export function ImportScreen({ manifest, setView }: ImportScreenProps) {
292
259
  value={f}
293
260
  checked={format === f}
294
261
  onChange={() => setFormat(f)}
295
- style={{ accentColor: theme.accent }}
262
+ className="accent-gold-500"
296
263
  />
297
264
  {f === "auto" ? "Auto" : f}
298
265
  </label>
@@ -300,8 +267,7 @@ export function ImportScreen({ manifest, setView }: ImportScreenProps) {
300
267
  </div>
301
268
  </div>
302
269
 
303
- {/* Content textarea */}
304
- <div style={{ marginBottom: 8 }}>
270
+ <div className="mb-2">
305
271
  <Label>Paste secrets</Label>
306
272
  <textarea
307
273
  value={content}
@@ -314,36 +280,11 @@ export function ImportScreen({ manifest, setView }: ImportScreenProps) {
314
280
  : "DB_HOST=localhost\nDB_PORT=5432\n# Comments are ignored"
315
281
  }
316
282
  rows={12}
317
- style={{
318
- width: "100%",
319
- background: theme.surface,
320
- border: `1px solid ${theme.border}`,
321
- borderRadius: 8,
322
- padding: 14,
323
- fontFamily: theme.mono,
324
- fontSize: 12,
325
- color: theme.text,
326
- resize: "vertical",
327
- outline: "none",
328
- boxSizing: "border-box",
329
- }}
283
+ className={TEXTAREA_CLASSES}
330
284
  />
331
285
  </div>
332
286
 
333
- {/* Privacy notice */}
334
- <div
335
- style={{
336
- marginBottom: 24,
337
- padding: "10px 14px",
338
- background: theme.surface,
339
- border: `1px solid ${theme.border}`,
340
- borderRadius: 6,
341
- fontFamily: theme.sans,
342
- fontSize: 11,
343
- color: theme.textMuted,
344
- lineHeight: 1.5,
345
- }}
346
- >
287
+ <div className="mb-6 rounded-md border border-edge bg-ink-850 px-3.5 py-2.5 font-sans text-[11px] leading-relaxed text-ash">
347
288
  Values are sent directly to the local Clef server (127.0.0.1) and encrypted
348
289
  immediately. They are never stored in browser memory beyond this session.
349
290
  </div>
@@ -361,56 +302,38 @@ export function ImportScreen({ manifest, setView }: ImportScreenProps) {
361
302
  {/* ── Step 2: Preview ────────────────────────────────────────── */}
362
303
  {step === 2 && preview && (
363
304
  <div>
364
- <div
365
- style={{
366
- fontFamily: theme.sans,
367
- fontSize: 13,
368
- color: theme.textMuted,
369
- marginBottom: 20,
370
- }}
371
- >
305
+ <div className="mb-5 font-sans text-[13px] text-ash">
372
306
  Importing to{" "}
373
- <span style={{ color: theme.accent, fontWeight: 600 }}>
307
+ <span className="font-semibold text-gold-500">
374
308
  {namespace}/{environment}
375
309
  </span>
376
310
  . {preview.totalKeys} key{preview.totalKeys !== 1 ? "s" : ""} parsed.
377
311
  </div>
378
312
 
379
- {/* Warnings */}
380
313
  {preview.warnings.length > 0 && (
381
- <div style={{ marginBottom: 16 }}>
314
+ <div className="mb-4">
382
315
  {preview.warnings.map((w, i) => (
383
- <div
384
- key={i}
385
- style={{
386
- fontFamily: theme.mono,
387
- fontSize: 11,
388
- color: theme.yellow,
389
- marginBottom: 4,
390
- }}
391
- >
316
+ <div key={i} className="mb-1 font-mono text-[11px] text-warn-500">
392
317
  &#9888; {w}
393
318
  </div>
394
319
  ))}
395
320
  </div>
396
321
  )}
397
322
 
398
- {/* Would import */}
399
323
  {preview.wouldImport.length > 0 && (
400
- <div style={{ marginBottom: 16 }}>
401
- <SectionLabel color={theme.green}>
324
+ <div className="mb-4">
325
+ <SectionLabel toneClass="text-go-500">
402
326
  New keys ({preview.wouldImport.length})
403
327
  </SectionLabel>
404
328
  {preview.wouldImport.map((key) => (
405
- <KeyRow key={key} icon="\u2192" iconColor={theme.green} label={key} />
329
+ <KeyRow key={key} icon="" iconClass="text-go-500" label={key} />
406
330
  ))}
407
331
  </div>
408
332
  )}
409
333
 
410
- {/* Would skip / overwrite toggles */}
411
334
  {preview.wouldSkip.length > 0 && (
412
- <div style={{ marginBottom: 16 }}>
413
- <SectionLabel color={theme.yellow}>
335
+ <div className="mb-4">
336
+ <SectionLabel toneClass="text-warn-500">
414
337
  Already exists ({preview.wouldSkip.length}) — toggle to overwrite
415
338
  </SectionLabel>
416
339
  {preview.wouldSkip.map(({ key, reason }) => {
@@ -418,45 +341,28 @@ export function ImportScreen({ manifest, setView }: ImportScreenProps) {
418
341
  return (
419
342
  <div
420
343
  key={key}
421
- style={{
422
- display: "flex",
423
- alignItems: "center",
424
- gap: 10,
425
- padding: "6px 10px",
426
- borderRadius: 6,
427
- marginBottom: 4,
428
- background: willOverwrite ? theme.yellowDim : "transparent",
429
- border: `1px solid ${willOverwrite ? theme.yellow + "44" : theme.border}`,
430
- }}
344
+ className={`mb-1 flex items-center gap-2.5 rounded-md border px-2.5 py-1.5 ${
345
+ willOverwrite
346
+ ? "border-warn-500/30 bg-warn-500/10"
347
+ : "border-edge bg-transparent"
348
+ }`}
431
349
  >
432
350
  <input
433
351
  type="checkbox"
434
352
  checked={willOverwrite}
435
353
  onChange={() => toggleOverwrite(key)}
436
- style={{ accentColor: theme.yellow }}
354
+ className="accent-warn-500"
437
355
  id={`overwrite-${key}`}
438
356
  />
439
357
  <label
440
358
  htmlFor={`overwrite-${key}`}
441
- style={{
442
- fontFamily: theme.mono,
443
- fontSize: 12,
444
- color: willOverwrite ? theme.yellow : theme.textMuted,
445
- flex: 1,
446
- cursor: "pointer",
447
- }}
359
+ className={`flex-1 cursor-pointer font-mono text-[12px] ${
360
+ willOverwrite ? "text-warn-500" : "text-ash"
361
+ }`}
448
362
  >
449
363
  {key}
450
364
  </label>
451
- <span
452
- style={{
453
- fontFamily: theme.sans,
454
- fontSize: 11,
455
- color: theme.textDim,
456
- }}
457
- >
458
- {reason}
459
- </span>
365
+ <span className="font-sans text-[11px] text-ash-dim">{reason}</span>
460
366
  </div>
461
367
  );
462
368
  })}
@@ -464,20 +370,12 @@ export function ImportScreen({ manifest, setView }: ImportScreenProps) {
464
370
  )}
465
371
 
466
372
  {preview.wouldImport.length === 0 && preview.wouldSkip.length === 0 && (
467
- <div
468
- style={{
469
- fontFamily: theme.sans,
470
- fontSize: 13,
471
- color: theme.textMuted,
472
- padding: "24px 0",
473
- textAlign: "center",
474
- }}
475
- >
373
+ <div className="py-6 text-center font-sans text-[13px] text-ash">
476
374
  No importable keys found.
477
375
  </div>
478
376
  )}
479
377
 
480
- <div style={{ display: "flex", gap: 10, marginTop: 24 }}>
378
+ <div className="mt-6 flex gap-2.5">
481
379
  <Button variant="ghost" onClick={() => setStep(1)}>
482
380
  Back
483
381
  </Button>
@@ -497,80 +395,52 @@ export function ImportScreen({ manifest, setView }: ImportScreenProps) {
497
395
  {/* ── Step 3: Done ───────────────────────────────────────────── */}
498
396
  {step === 3 && applyResult && (
499
397
  <div>
500
- <div
501
- style={{
502
- display: "flex",
503
- flexDirection: "column",
504
- alignItems: "center",
505
- paddingTop: 20,
506
- paddingBottom: 32,
507
- }}
508
- >
398
+ <div className="flex flex-col items-center pt-5 pb-8">
509
399
  <div
510
- style={{
511
- width: 56,
512
- height: 56,
513
- borderRadius: "50%",
514
- background: applyResult.failed.length > 0 ? theme.redDim : theme.greenDim,
515
- border: `1px solid ${applyResult.failed.length > 0 ? theme.red + "44" : theme.green + "44"}`,
516
- display: "flex",
517
- alignItems: "center",
518
- justifyContent: "center",
519
- fontSize: 24,
520
- color: applyResult.failed.length > 0 ? theme.red : theme.green,
521
- marginBottom: 16,
522
- }}
400
+ className={`mb-4 flex h-14 w-14 items-center justify-center rounded-full text-[24px] ${
401
+ applyResult.failed.length > 0
402
+ ? "border border-stop-500/30 bg-stop-500/10 text-stop-500"
403
+ : "border border-go-500/30 bg-go-500/10 text-go-500"
404
+ }`}
523
405
  >
524
- {applyResult.failed.length > 0 ? "\u26a0" : "\u2713"}
406
+ {applyResult.failed.length > 0 ? "" : ""}
525
407
  </div>
526
-
527
408
  <div
528
- style={{
529
- fontFamily: theme.sans,
530
- fontWeight: 600,
531
- fontSize: 16,
532
- color: applyResult.failed.length > 0 ? theme.yellow : theme.green,
533
- marginBottom: 8,
534
- }}
409
+ className={`mb-2 font-sans text-[16px] font-semibold ${
410
+ applyResult.failed.length > 0 ? "text-warn-500" : "text-go-500"
411
+ }`}
535
412
  >
536
413
  {applyResult.failed.length > 0
537
414
  ? "Import completed with errors"
538
415
  : "Import complete"}
539
416
  </div>
540
-
541
- <div
542
- style={{
543
- fontFamily: theme.mono,
544
- fontSize: 12,
545
- color: theme.textMuted,
546
- }}
547
- >
417
+ <div className="font-mono text-[12px] text-ash">
548
418
  {applyResult.imported.length} imported, {applyResult.skipped.length} skipped,{" "}
549
419
  {applyResult.failed.length} failed
550
420
  </div>
551
421
  </div>
552
422
 
553
423
  {applyResult.imported.length > 0 && (
554
- <div style={{ marginBottom: 16 }}>
555
- <SectionLabel color={theme.green}>
424
+ <div className="mb-4">
425
+ <SectionLabel toneClass="text-go-500">
556
426
  Imported ({applyResult.imported.length})
557
427
  </SectionLabel>
558
428
  {applyResult.imported.map((key) => (
559
- <KeyRow key={key} icon="\u2713" iconColor={theme.green} label={key} />
429
+ <KeyRow key={key} icon="" iconClass="text-go-500" label={key} />
560
430
  ))}
561
431
  </div>
562
432
  )}
563
433
 
564
434
  {applyResult.failed.length > 0 && (
565
- <div style={{ marginBottom: 16 }}>
566
- <SectionLabel color={theme.red}>
435
+ <div className="mb-4">
436
+ <SectionLabel toneClass="text-stop-500">
567
437
  Failed ({applyResult.failed.length})
568
438
  </SectionLabel>
569
439
  {applyResult.failed.map(({ key, error: keyError }) => (
570
440
  <KeyRow
571
441
  key={key}
572
- icon="\u2717"
573
- iconColor={theme.red}
442
+ icon=""
443
+ iconClass="text-stop-500"
574
444
  label={key}
575
445
  note={keyError}
576
446
  />
@@ -578,7 +448,7 @@ export function ImportScreen({ manifest, setView }: ImportScreenProps) {
578
448
  </div>
579
449
  )}
580
450
 
581
- <div style={{ display: "flex", gap: 10, marginTop: 24 }}>
451
+ <div className="mt-6 flex gap-2.5">
582
452
  <Button variant="primary" onClick={() => setView("matrix")}>
583
453
  View in Matrix
584
454
  </Button>
@@ -596,80 +466,20 @@ export function ImportScreen({ manifest, setView }: ImportScreenProps) {
596
466
 
597
467
  function Label({ children }: { children: React.ReactNode }) {
598
468
  return (
599
- <div
600
- style={{
601
- fontFamily: theme.sans,
602
- fontSize: 12,
603
- fontWeight: 600,
604
- color: theme.textMuted,
605
- marginBottom: 8,
606
- letterSpacing: "0.05em",
607
- textTransform: "uppercase",
608
- }}
609
- >
469
+ <div className="mb-2 font-sans text-[12px] font-semibold uppercase tracking-[0.05em] text-ash">
610
470
  {children}
611
471
  </div>
612
472
  );
613
473
  }
614
474
 
615
475
  function SubLabel({ children }: { children: React.ReactNode }) {
616
- return (
617
- <div
618
- style={{
619
- fontFamily: theme.sans,
620
- fontSize: 11,
621
- color: theme.textDim,
622
- marginBottom: 4,
623
- }}
624
- >
625
- {children}
626
- </div>
627
- );
628
- }
629
-
630
- function Select({
631
- value,
632
- onChange,
633
- children,
634
- }: {
635
- value: string;
636
- onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
637
- children: React.ReactNode;
638
- }) {
639
- return (
640
- <select
641
- value={value}
642
- onChange={onChange}
643
- style={{
644
- width: "100%",
645
- background: theme.surface,
646
- border: `1px solid ${theme.border}`,
647
- borderRadius: 6,
648
- padding: "7px 10px",
649
- fontFamily: theme.sans,
650
- fontSize: 13,
651
- color: theme.text,
652
- outline: "none",
653
- cursor: "pointer",
654
- }}
655
- >
656
- {children}
657
- </select>
658
- );
476
+ return <div className="mb-1 font-sans text-[11px] text-ash-dim">{children}</div>;
659
477
  }
660
478
 
661
- function SectionLabel({ children, color }: { children: React.ReactNode; color: string }) {
479
+ function SectionLabel({ children, toneClass }: { children: React.ReactNode; toneClass: string }) {
662
480
  return (
663
481
  <div
664
- style={{
665
- fontFamily: theme.sans,
666
- fontSize: 11,
667
- fontWeight: 600,
668
- color,
669
- letterSpacing: "0.06em",
670
- textTransform: "uppercase",
671
- marginBottom: 8,
672
- }}
482
+ className={`mb-2 font-sans text-[11px] font-semibold uppercase tracking-[0.06em] ${toneClass}`}
673
483
  >
674
484
  {children}
675
485
  </div>
@@ -678,33 +488,20 @@ function SectionLabel({ children, color }: { children: React.ReactNode; color: s
678
488
 
679
489
  function KeyRow({
680
490
  icon,
681
- iconColor,
491
+ iconClass,
682
492
  label,
683
493
  note,
684
494
  }: {
685
495
  icon: string;
686
- iconColor: string;
496
+ iconClass: string;
687
497
  label: string;
688
498
  note?: string;
689
499
  }) {
690
500
  return (
691
- <div
692
- style={{
693
- display: "flex",
694
- alignItems: "center",
695
- gap: 10,
696
- padding: "5px 10px",
697
- borderRadius: 6,
698
- marginBottom: 3,
699
- }}
700
- >
701
- <span style={{ color: iconColor, fontFamily: theme.mono, fontSize: 13 }}>{icon}</span>
702
- <span style={{ fontFamily: theme.mono, fontSize: 12, color: theme.text, flex: 1 }}>
703
- {label}
704
- </span>
705
- {note && (
706
- <span style={{ fontFamily: theme.sans, fontSize: 11, color: theme.textDim }}>{note}</span>
707
- )}
501
+ <div className="mb-px flex items-center gap-2.5 rounded-md px-2.5 py-1">
502
+ <span className={`font-mono text-[13px] ${iconClass}`}>{icon}</span>
503
+ <span className="flex-1 font-mono text-[12px] text-bone">{label}</span>
504
+ {note && <span className="font-sans text-[11px] text-ash-dim">{note}</span>}
708
505
  </div>
709
506
  );
710
507
  }