@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 { EnvBadge } from "../components/EnvBadge";
6
4
  import { Button } from "../components/Button";
7
5
  import { CopyButton } from "../components/CopyButton";
6
+ import { Toolbar } from "../primitives";
8
7
  import type { ClefManifest } from "@clef-sh/core";
9
8
 
10
9
  interface EnvInfo {
@@ -39,38 +38,42 @@ interface ServiceIdentitiesScreenProps {
39
38
 
40
39
  type View = "list" | "detail" | "create" | "keys" | "update" | "rotate-keys" | "delete-confirm";
41
40
 
41
+ const INPUT_BASE =
42
+ "w-full box-border rounded-md border border-edge bg-ink-850 px-3 py-2 font-mono text-[12px] text-bone outline-none focus-visible:border-gold-500 placeholder:text-ash-dim";
43
+
44
+ const SMALL_INPUT_BASE =
45
+ "rounded-md border border-edge bg-ink-850 px-2 py-1.5 font-mono text-[12px] text-bone outline-none focus-visible:border-gold-500";
46
+
47
+ const BACK_BUTTON =
48
+ "cursor-pointer rounded-md border border-edge-strong bg-transparent px-3 py-1 font-sans text-[12px] text-ash hover:bg-ink-800";
49
+
42
50
  export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenProps) {
43
51
  const [view, setView] = useState<View>("list");
44
52
  const [identities, setIdentities] = useState<IdentityInfo[]>([]);
45
53
  const [selected, setSelected] = useState<string | null>(null);
46
54
  const [error, setError] = useState("");
47
55
 
48
- // Create form state
49
56
  const [name, setName] = useState("");
50
57
  const [description, setDescription] = useState("");
51
58
  const [selectedNamespaces, setSelectedNamespaces] = useState<Set<string>>(new Set());
52
59
  const [envBackends, setEnvBackends] = useState<Record<string, EnvBackendConfig>>({});
53
60
  const [role, setRole] = useState<"ci" | "runtime">("ci");
54
- const [sharedRecipient, setSharedRecipient] = useState(true); // CI default
61
+ const [sharedRecipient, setSharedRecipient] = useState(true);
55
62
  const [sharedRecipientOverridden, setSharedRecipientOverridden] = useState(false);
56
63
  const [creating, setCreating] = useState(false);
57
64
  const [createError, setCreateError] = useState("");
58
65
 
59
- // Post-create / post-rotate keys
60
66
  const [privateKeys, setPrivateKeys] = useState<Record<string, string>>({});
61
67
  const [createdName, setCreatedName] = useState("");
62
68
  const [wasSharedRecipient, setWasSharedRecipient] = useState(false);
63
69
 
64
- // Update form state
65
70
  const [updateEnvBackends, setUpdateEnvBackends] = useState<Record<string, UpdateEnvState>>({});
66
71
  const [updating, setUpdating] = useState(false);
67
72
  const [updateError, setUpdateError] = useState("");
68
73
 
69
- // Rotate state
70
74
  const [rotatingEnv, setRotatingEnv] = useState<string | undefined>(undefined);
71
75
  const [rotatedKeys, setRotatedKeys] = useState<Record<string, string>>({});
72
76
 
73
- // Delete state
74
77
  const [deleting, setDeleting] = useState(false);
75
78
  const [deleteError, setDeleteError] = useState("");
76
79
 
@@ -102,7 +105,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
102
105
  }
103
106
  setEnvBackends(defaults);
104
107
  setRole("ci");
105
- setSharedRecipient(true); // CI default
108
+ setSharedRecipient(true);
106
109
  setSharedRecipientOverridden(false);
107
110
  setCreateError("");
108
111
  setView("create");
@@ -137,8 +140,6 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
137
140
  setView("detail");
138
141
  }, []);
139
142
 
140
- // ── Handlers ──────────────────────────────────────────────────────────────────
141
-
142
143
  async function handleCreate() {
143
144
  setCreating(true);
144
145
  setCreateError("");
@@ -154,9 +155,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
154
155
  description: description.trim(),
155
156
  namespaces: Array.from(selectedNamespaces),
156
157
  };
157
- if (role === "runtime") {
158
- body.packOnly = true;
159
- }
158
+ if (role === "runtime") body.packOnly = true;
160
159
  if (sharedRecipient) {
161
160
  body.sharedRecipient = true;
162
161
  } else if (Object.keys(kmsEnvConfigs).length > 0) {
@@ -269,36 +268,30 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
269
268
  // ── List view ─────────────────────────────────────────────────────────────────
270
269
  if (view === "list") {
271
270
  return (
272
- <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
273
- <TopBar
274
- title="Service Identities"
275
- subtitle="Per-service cryptographic access scoping"
276
- actions={
277
- manifest && (
271
+ <div className="flex flex-1 flex-col overflow-hidden">
272
+ <Toolbar>
273
+ <div>
274
+ <Toolbar.Title>Service Identities</Toolbar.Title>
275
+ <Toolbar.Subtitle>Per-service cryptographic access scoping</Toolbar.Subtitle>
276
+ </div>
277
+ {manifest && (
278
+ <Toolbar.Actions>
278
279
  <Button variant="primary" onClick={openCreate}>
279
280
  + New identity
280
281
  </Button>
281
- )
282
- }
283
- />
284
- <div style={{ flex: 1, overflow: "auto", padding: 24 }}>
285
- <div style={{ maxWidth: 620, margin: "0 auto" }}>
282
+ </Toolbar.Actions>
283
+ )}
284
+ </Toolbar>
285
+ <div className="flex-1 overflow-auto p-6">
286
+ <div className="mx-auto max-w-[620px]">
286
287
  {error && <ErrorBanner>{error}</ErrorBanner>}
287
288
 
288
289
  {identities.length === 0 && (
289
- <div
290
- style={{
291
- textAlign: "center",
292
- padding: "48px 24px",
293
- color: theme.textMuted,
294
- fontFamily: theme.sans,
295
- fontSize: 13,
296
- }}
297
- >
298
- <div style={{ fontSize: 28, marginBottom: 12, opacity: 0.4 }}>{"\uD83D\uDD11"}</div>
290
+ <div className="px-6 py-12 text-center font-sans text-[13px] text-ash">
291
+ <div className="mb-3 text-[28px] opacity-40">{"🔑"}</div>
299
292
  No service identities configured.
300
293
  {manifest && (
301
- <div style={{ marginTop: 16, display: "flex", justifyContent: "center" }}>
294
+ <div className="mt-4 flex justify-center">
302
295
  <Button variant="primary" onClick={openCreate}>
303
296
  Create the first one
304
297
  </Button>
@@ -324,73 +317,28 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
324
317
  setView("detail");
325
318
  }
326
319
  }}
327
- style={{
328
- background: theme.surface,
329
- border: `1px solid ${theme.border}`,
330
- borderRadius: 8,
331
- padding: "16px 20px",
332
- marginBottom: 8,
333
- cursor: "pointer",
334
- transition: "all 0.12s",
335
- }}
336
- onMouseEnter={(e) => {
337
- (e.currentTarget as HTMLDivElement).style.borderColor = theme.borderLight;
338
- }}
339
- onMouseLeave={(e) => {
340
- (e.currentTarget as HTMLDivElement).style.borderColor = theme.border;
341
- }}
320
+ className="mb-2 cursor-pointer rounded-lg border border-edge bg-ink-850 px-5 py-4 transition-colors hover:border-edge-strong"
342
321
  >
343
- <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 8 }}>
344
- <span
345
- style={{
346
- fontFamily: theme.sans,
347
- fontWeight: 600,
348
- fontSize: 14,
349
- color: theme.text,
350
- }}
351
- >
352
- {si.name}
353
- </span>
322
+ <div className="mb-2 flex items-center gap-2.5">
323
+ <span className="font-sans text-[14px] font-semibold text-bone">{si.name}</span>
354
324
  {si.packOnly && (
355
325
  <span
356
326
  data-testid={`si-runtime-badge-${si.name}`}
357
- style={{
358
- fontFamily: theme.mono,
359
- fontSize: 9,
360
- color: theme.yellow,
361
- background: `${theme.yellow}15`,
362
- border: `1px solid ${theme.yellow}33`,
363
- borderRadius: 3,
364
- padding: "1px 6px",
365
- }}
327
+ className="rounded-sm border border-warn-500/20 bg-warn-500/10 px-1.5 py-px font-mono text-[9px] text-warn-500"
366
328
  >
367
329
  runtime
368
330
  </span>
369
331
  )}
370
332
  </div>
371
- <div
372
- style={{
373
- fontFamily: theme.sans,
374
- fontSize: 12,
375
- color: theme.textMuted,
376
- marginBottom: 10,
377
- }}
378
- >
379
- Scoped to: <span style={{ color: theme.text }}>{si.namespaces.join(", ")}</span>
333
+ <div className="mb-2.5 font-sans text-[12px] text-ash">
334
+ Scoped to: <span className="text-bone">{si.namespaces.join(", ")}</span>
380
335
  </div>
381
- <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
336
+ <div className="flex flex-wrap gap-1.5">
382
337
  {Object.entries(si.environments).map(([envName, envInfo]) => (
383
- <span
384
- key={envName}
385
- style={{ display: "inline-flex", alignItems: "center", gap: 4 }}
386
- >
338
+ <span key={envName} className="inline-flex items-center gap-1">
387
339
  <EnvBadge env={envName} small />
388
340
  <span
389
- style={{
390
- fontFamily: theme.mono,
391
- fontSize: 9,
392
- color: envInfo.type === "kms" ? theme.purple : theme.textDim,
393
- }}
341
+ className={`font-mono text-[9px] ${envInfo.type === "kms" ? "text-purple-400" : "text-ash-dim"}`}
394
342
  >
395
343
  {envInfo.type === "kms" ? "KMS" : "age"}
396
344
  </span>
@@ -408,62 +356,42 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
408
356
  // ── Detail view ───────────────────────────────────────────────────────────────
409
357
  if (view === "detail") {
410
358
  return (
411
- <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
412
- <TopBar
413
- title={selectedIdentity?.name ?? selected ?? ""}
414
- subtitle={selectedIdentity?.description}
415
- actions={
416
- <div style={{ display: "flex", gap: 6 }}>
417
- {selectedIdentity && (
418
- <Button
419
- data-testid="update-backends-btn"
420
- variant="ghost"
421
- onClick={() => openUpdate(selectedIdentity)}
422
- >
423
- Update backends
424
- </Button>
425
- )}
426
- <button
427
- data-testid="back-button"
428
- onClick={goList}
429
- style={{
430
- background: "none",
431
- border: `1px solid ${theme.borderLight}`,
432
- borderRadius: 6,
433
- padding: "4px 12px",
434
- cursor: "pointer",
435
- fontFamily: theme.sans,
436
- fontSize: 12,
437
- color: theme.textMuted,
438
- transition: "all 0.12s",
439
- }}
359
+ <div className="flex flex-1 flex-col overflow-hidden">
360
+ <Toolbar>
361
+ <div>
362
+ <Toolbar.Title>{selectedIdentity?.name ?? selected ?? ""}</Toolbar.Title>
363
+ {selectedIdentity?.description && (
364
+ <Toolbar.Subtitle>{selectedIdentity.description}</Toolbar.Subtitle>
365
+ )}
366
+ </div>
367
+ <Toolbar.Actions>
368
+ {selectedIdentity && (
369
+ <Button
370
+ data-testid="update-backends-btn"
371
+ variant="ghost"
372
+ onClick={() => openUpdate(selectedIdentity)}
440
373
  >
441
- {"\u2190"} Back
442
- </button>
443
- </div>
444
- }
445
- />
446
- <div style={{ flex: 1, overflow: "auto", padding: 24 }}>
447
- <div style={{ maxWidth: 620, margin: "0 auto" }}>
374
+ Update backends
375
+ </Button>
376
+ )}
377
+ <button data-testid="back-button" onClick={goList} className={BACK_BUTTON}>
378
+ {"←"} Back
379
+ </button>
380
+ </Toolbar.Actions>
381
+ </Toolbar>
382
+ <div className="flex-1 overflow-auto p-6">
383
+ <div className="mx-auto max-w-[620px]">
448
384
  {error && <ErrorBanner>{error}</ErrorBanner>}
449
385
 
450
386
  {selectedIdentity && (
451
387
  <>
452
- <div style={{ marginBottom: 20 }}>
388
+ <div className="mb-5">
453
389
  <Label>Scoped namespaces</Label>
454
- <div style={{ display: "flex", gap: 6 }}>
390
+ <div className="flex gap-1.5">
455
391
  {selectedIdentity.namespaces.map((ns) => (
456
392
  <span
457
393
  key={ns}
458
- style={{
459
- fontFamily: theme.mono,
460
- fontSize: 11,
461
- color: theme.accent,
462
- background: theme.accentDim,
463
- border: `1px solid ${theme.accent}33`,
464
- borderRadius: 4,
465
- padding: "2px 8px",
466
- }}
394
+ className="rounded border border-gold-500/20 bg-gold-500/[0.08] px-2 py-0.5 font-mono text-[11px] text-gold-500"
467
395
  >
468
396
  {ns}
469
397
  </span>
@@ -474,17 +402,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
474
402
  {selectedIdentity.packOnly && (
475
403
  <div
476
404
  data-testid="runtime-info-banner"
477
- style={{
478
- background: `${theme.yellow}10`,
479
- border: `1px solid ${theme.yellow}33`,
480
- borderRadius: 8,
481
- padding: "10px 16px",
482
- marginBottom: 20,
483
- fontFamily: theme.sans,
484
- fontSize: 12,
485
- color: theme.yellow,
486
- lineHeight: 1.5,
487
- }}
405
+ className="mb-5 rounded-lg border border-warn-500/20 bg-warn-500/10 px-4 py-2.5 font-sans text-[12px] leading-relaxed text-warn-500"
488
406
  >
489
407
  Runtime identity — keys are not registered on encrypted files. This identity can
490
408
  only decrypt packed artifacts.
@@ -503,39 +421,14 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
503
421
  <div
504
422
  key={env.name}
505
423
  data-testid={`env-${env.name}`}
506
- style={{
507
- background: theme.surface,
508
- border: `1px solid ${theme.border}`,
509
- borderRadius: 8,
510
- padding: "16px 20px",
511
- marginBottom: 10,
512
- }}
424
+ className="mb-2.5 rounded-lg border border-edge bg-ink-850 px-5 py-4"
513
425
  >
514
- <div
515
- style={{
516
- display: "flex",
517
- alignItems: "center",
518
- justifyContent: "space-between",
519
- marginBottom: 12,
520
- }}
521
- >
522
- <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
426
+ <div className="mb-3 flex items-center justify-between">
427
+ <div className="flex items-center gap-2.5">
523
428
  <EnvBadge env={env.name} />
524
- {isProtected && (
525
- <span style={{ fontSize: 12, color: theme.red }}>{"\uD83D\uDD12"}</span>
526
- )}
429
+ {isProtected && <span className="text-[12px] text-stop-500">{"🔒"}</span>}
527
430
  {envInfo.type === "kms" && (
528
- <span
529
- style={{
530
- fontFamily: theme.mono,
531
- fontSize: 10,
532
- color: theme.purple,
533
- background: theme.purpleDim,
534
- border: `1px solid ${theme.purple}33`,
535
- borderRadius: 3,
536
- padding: "1px 6px",
537
- }}
538
- >
431
+ <span className="rounded-sm border border-purple-400/20 bg-purple-400/10 px-1.5 py-px font-mono text-[10px] text-purple-400">
539
432
  KMS
540
433
  </span>
541
434
  )}
@@ -545,17 +438,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
545
438
  data-testid={`rotate-${env.name}`}
546
439
  disabled={isRotating}
547
440
  onClick={() => handleRotate(env.name)}
548
- style={{
549
- background: "none",
550
- border: `1px solid ${theme.borderLight}`,
551
- borderRadius: 5,
552
- padding: "3px 10px",
553
- cursor: isRotating ? "default" : "pointer",
554
- fontFamily: theme.sans,
555
- fontSize: 11,
556
- color: isRotating ? theme.textDim : theme.textMuted,
557
- opacity: isRotating ? 0.5 : 1,
558
- }}
441
+ className={`rounded border border-edge-strong px-2.5 py-0.5 font-sans text-[11px] ${isRotating ? "cursor-default text-ash-dim opacity-50" : "cursor-pointer text-ash hover:bg-ink-800"}`}
559
442
  >
560
443
  {isRotating ? "Rotating…" : "Rotate key"}
561
444
  </button>
@@ -563,34 +446,17 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
563
446
  </div>
564
447
 
565
448
  {envInfo.type === "kms" && envInfo.kms && (
566
- <div
567
- style={{ fontFamily: theme.mono, fontSize: 11, color: theme.textMuted }}
568
- >
569
- <div style={{ marginBottom: 8 }}>
570
- Authentication: <span style={{ color: theme.purple }}>IAM + KMS</span>
449
+ <div className="font-mono text-[11px] text-ash">
450
+ <div className="mb-2">
451
+ Authentication: <span className="text-purple-400">IAM + KMS</span>
571
452
  </div>
572
453
  <div>
573
- Provider:{" "}
574
- <span style={{ color: theme.text }}>{envInfo.kms.provider}</span>
454
+ Provider: <span className="text-bone">{envInfo.kms.provider}</span>
575
455
  </div>
576
- <div style={{ marginTop: 4 }}>
577
- Key ID:{" "}
578
- <span style={{ color: theme.text, wordBreak: "break-all" }}>
579
- {envInfo.kms.keyId}
580
- </span>
456
+ <div className="mt-1">
457
+ Key ID: <span className="break-all text-bone">{envInfo.kms.keyId}</span>
581
458
  </div>
582
- <div
583
- style={{
584
- marginTop: 10,
585
- padding: "8px 12px",
586
- background: theme.purpleDim,
587
- border: `1px solid ${theme.purple}33`,
588
- borderRadius: 4,
589
- fontSize: 11,
590
- color: theme.purple,
591
- fontFamily: theme.sans,
592
- }}
593
- >
459
+ <div className="mt-2.5 rounded border border-purple-400/20 bg-purple-400/10 px-3 py-2 font-sans text-[11px] text-purple-400">
594
460
  No keys to provision. CI and runtime authenticate via IAM role with
595
461
  kms:Decrypt permission.
596
462
  </div>
@@ -598,15 +464,13 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
598
464
  )}
599
465
 
600
466
  {envInfo.type === "age" && (
601
- <div
602
- style={{ fontFamily: theme.mono, fontSize: 11, color: theme.textMuted }}
603
- >
604
- <div style={{ marginBottom: 8 }}>
605
- Authentication: <span style={{ color: theme.green }}>age key</span>
467
+ <div className="font-mono text-[11px] text-ash">
468
+ <div className="mb-2">
469
+ Authentication: <span className="text-go-500">age key</span>
606
470
  </div>
607
471
  <div>
608
472
  Public key:{" "}
609
- <span style={{ color: theme.text }}>
473
+ <span className="text-bone">
610
474
  {envInfo.publicKey
611
475
  ? `${envInfo.publicKey.slice(0, 12)}...${envInfo.publicKey.slice(-6)}`
612
476
  : "unknown"}
@@ -618,15 +482,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
618
482
  );
619
483
  })}
620
484
 
621
- <div
622
- style={{
623
- marginTop: 32,
624
- paddingTop: 20,
625
- borderTop: `1px solid ${theme.border}`,
626
- display: "flex",
627
- justifyContent: "flex-end",
628
- }}
629
- >
485
+ <div className="mt-8 flex justify-end border-t border-edge pt-5">
630
486
  <Button
631
487
  data-testid="delete-identity-btn"
632
488
  variant="danger"
@@ -649,39 +505,29 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
649
505
  // ── Delete confirm view ───────────────────────────────────────────────────────
650
506
  if (view === "delete-confirm") {
651
507
  return (
652
- <div
653
- data-testid="delete-confirm-view"
654
- style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}
655
- >
656
- <TopBar title="Delete service identity" subtitle="This action cannot be undone" />
657
- <div style={{ flex: 1, overflow: "auto", padding: 24 }}>
658
- <div style={{ maxWidth: 560, margin: "0 auto" }}>
508
+ <div data-testid="delete-confirm-view" className="flex flex-1 flex-col overflow-hidden">
509
+ <Toolbar>
510
+ <div>
511
+ <Toolbar.Title>Delete service identity</Toolbar.Title>
512
+ <Toolbar.Subtitle>This action cannot be undone</Toolbar.Subtitle>
513
+ </div>
514
+ </Toolbar>
515
+ <div className="flex-1 overflow-auto p-6">
516
+ <div className="mx-auto max-w-[560px]">
659
517
  {deleteError && <ErrorBanner>{deleteError}</ErrorBanner>}
660
518
 
661
- <div
662
- style={{
663
- background: "#1a0a0a",
664
- border: `1px solid ${theme.red}55`,
665
- borderRadius: 8,
666
- padding: "16px 20px",
667
- marginBottom: 24,
668
- fontFamily: theme.sans,
669
- fontSize: 13,
670
- color: theme.red,
671
- }}
672
- >
673
- <div style={{ fontWeight: 600, marginBottom: 8 }}>
674
- Delete <span style={{ fontFamily: theme.mono }}>{selected}</span>?
519
+ <div className="mb-6 rounded-lg border border-stop-500/40 bg-stop-500/[0.06] px-5 py-4 font-sans text-[13px] text-stop-500">
520
+ <div className="mb-2 font-semibold">
521
+ Delete <span className="font-mono">{selected}</span>?
675
522
  </div>
676
- <div style={{ color: theme.textMuted, fontSize: 12, lineHeight: 1.6 }}>
677
- This will remove the identity from{" "}
678
- <span style={{ fontFamily: theme.mono }}>clef.yaml</span> and de-register its
679
- recipients from all scoped encrypted files. Any runtimes currently using this
680
- identity's private key will lose access on the next artifact refresh.
523
+ <div className="text-[12px] leading-relaxed text-ash">
524
+ This will remove the identity from <span className="font-mono">clef.yaml</span> and
525
+ de-register its recipients from all scoped encrypted files. Any runtimes currently
526
+ using this identity's private key will lose access on the next artifact refresh.
681
527
  </div>
682
528
  </div>
683
529
 
684
- <div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
530
+ <div className="flex justify-end gap-2">
685
531
  <Button
686
532
  data-testid="cancel-delete-btn"
687
533
  variant="ghost"
@@ -708,29 +554,17 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
708
554
  // ── Rotate keys result view ───────────────────────────────────────────────────
709
555
  if (view === "rotate-keys") {
710
556
  return (
711
- <div
712
- data-testid="rotate-keys-view"
713
- style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}
714
- >
715
- <TopBar title="Key rotated" subtitle={`New keys for ${selected}`} />
716
- <div style={{ flex: 1, overflow: "auto", padding: 24 }}>
717
- <div style={{ maxWidth: 620, margin: "0 auto" }}>
718
- <div
719
- style={{
720
- background: "#1a1200",
721
- border: `1px solid ${theme.yellow}55`,
722
- borderRadius: 8,
723
- padding: "14px 18px",
724
- marginBottom: 20,
725
- fontFamily: theme.sans,
726
- fontSize: 13,
727
- color: theme.yellow,
728
- display: "flex",
729
- gap: 10,
730
- alignItems: "flex-start",
731
- }}
732
- >
733
- <span style={{ fontSize: 16, flexShrink: 0 }}>⚠</span>
557
+ <div data-testid="rotate-keys-view" className="flex flex-1 flex-col overflow-hidden">
558
+ <Toolbar>
559
+ <div>
560
+ <Toolbar.Title>Key rotated</Toolbar.Title>
561
+ <Toolbar.Subtitle>{`New keys for ${selected}`}</Toolbar.Subtitle>
562
+ </div>
563
+ </Toolbar>
564
+ <div className="flex-1 overflow-auto p-6">
565
+ <div className="mx-auto max-w-[620px]">
566
+ <div className="mb-5 flex items-start gap-2.5 rounded-lg border border-warn-500/40 bg-warn-500/[0.06] px-4 py-3.5 font-sans text-[13px] text-warn-500">
567
+ <span className="shrink-0 text-[16px]">⚠</span>
734
568
  <span>
735
569
  Copy the new private key now — it will not be shown again. Provision it to the
736
570
  runtime and invalidate the old key.
@@ -741,42 +575,19 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
741
575
  {Object.entries(rotatedKeys).map(([envName, key]) => (
742
576
  <div
743
577
  key={envName}
744
- style={{
745
- background: theme.surface,
746
- border: `1px solid ${theme.border}`,
747
- borderRadius: 8,
748
- padding: "14px 18px",
749
- marginBottom: 10,
750
- }}
578
+ className="mb-2.5 rounded-lg border border-edge bg-ink-850 px-4 py-3.5"
751
579
  >
752
- <div
753
- style={{
754
- display: "flex",
755
- alignItems: "center",
756
- justifyContent: "space-between",
757
- marginBottom: 10,
758
- }}
759
- >
580
+ <div className="mb-2.5 flex items-center justify-between">
760
581
  <EnvBadge env={envName} />
761
582
  <CopyButton text={key} />
762
583
  </div>
763
- <div
764
- style={{
765
- fontFamily: theme.mono,
766
- fontSize: 11,
767
- color: theme.textMuted,
768
- wordBreak: "break-all",
769
- background: theme.bg,
770
- borderRadius: 4,
771
- padding: "8px 10px",
772
- }}
773
- >
584
+ <div className="break-all rounded bg-ink-950 px-2.5 py-2 font-mono text-[11px] text-ash">
774
585
  {key}
775
586
  </div>
776
587
  </div>
777
588
  ))}
778
589
 
779
- <div style={{ marginTop: 8, display: "flex", justifyContent: "flex-end" }}>
590
+ <div className="mt-2 flex justify-end">
780
591
  <Button data-testid="rotate-done-btn" variant="primary" onClick={goDetail}>
781
592
  Done
782
593
  </Button>
@@ -804,77 +615,52 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
804
615
  });
805
616
 
806
617
  return (
807
- <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
808
- <TopBar
809
- title="Update backends"
810
- subtitle={`Environment backends for ${selected}`}
811
- actions={
812
- <button
813
- onClick={goDetail}
814
- style={{
815
- background: "none",
816
- border: `1px solid ${theme.borderLight}`,
817
- borderRadius: 6,
818
- padding: "4px 12px",
819
- cursor: "pointer",
820
- fontFamily: theme.sans,
821
- fontSize: 12,
822
- color: theme.textMuted,
823
- }}
824
- >
825
- {"\u2190"} Cancel
618
+ <div className="flex flex-1 flex-col overflow-hidden">
619
+ <Toolbar>
620
+ <div>
621
+ <Toolbar.Title>Update backends</Toolbar.Title>
622
+ <Toolbar.Subtitle>{`Environment backends for ${selected}`}</Toolbar.Subtitle>
623
+ </div>
624
+ <Toolbar.Actions>
625
+ <button onClick={goDetail} className={BACK_BUTTON}>
626
+ {""} Cancel
826
627
  </button>
827
- }
828
- />
829
- <div style={{ flex: 1, overflow: "auto", padding: 24 }}>
830
- <div style={{ maxWidth: 560, margin: "0 auto" }}>
628
+ </Toolbar.Actions>
629
+ </Toolbar>
630
+ <div className="flex-1 overflow-auto p-6">
631
+ <div className="mx-auto max-w-[560px]">
831
632
  {updateError && <ErrorBanner>{updateError}</ErrorBanner>}
832
633
 
833
- <div
834
- style={{
835
- fontFamily: theme.sans,
836
- fontSize: 12,
837
- color: theme.textMuted,
838
- marginBottom: 16,
839
- lineHeight: 1.6,
840
- }}
841
- >
634
+ <div className="mb-4 font-sans text-[12px] leading-relaxed text-ash">
842
635
  Switch age environments to KMS, or update an existing KMS key ID. To revert KMS to
843
636
  age, delete and recreate the identity.
844
637
  </div>
845
638
 
846
- <div style={{ display: "flex", flexDirection: "column", gap: 8, marginBottom: 28 }}>
639
+ <div className="mb-7 flex flex-col gap-2">
847
640
  {environments.map((env) => {
848
641
  const state = updateEnvBackends[env.name];
849
642
  if (!state) return null;
850
-
851
643
  return (
852
644
  <div
853
645
  key={env.name}
854
- style={{
855
- background: theme.surface,
856
- border: `1px solid ${theme.border}`,
857
- borderRadius: 8,
858
- padding: "14px 16px",
859
- }}
646
+ className="rounded-lg border border-edge bg-ink-850 px-4 py-3.5"
860
647
  >
861
648
  <div
862
- style={{
863
- display: "flex",
864
- alignItems: "center",
865
- justifyContent: "space-between",
866
- marginBottom: state.type === "kms" ? 12 : 0,
867
- }}
649
+ className={`flex items-center justify-between ${state.type === "kms" ? "mb-3" : ""}`}
868
650
  >
869
- <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
651
+ <div className="flex items-center gap-2">
870
652
  <EnvBadge env={env.name} />
871
- {env.protected && (
872
- <span style={{ fontSize: 11, color: theme.red }}>{"\uD83D\uDD12"}</span>
873
- )}
653
+ {env.protected && <span className="text-[11px] text-stop-500">{"🔒"}</span>}
874
654
  </div>
875
- <div style={{ display: "flex", gap: 4 }}>
655
+ <div className="flex gap-1">
876
656
  {(["age", "kms"] as const).map((t) => {
877
657
  const locked = state.originalType === "kms" && t === "age";
658
+ const isSelected = state.type === t;
659
+ const buttonClass = isSelected
660
+ ? t === "kms"
661
+ ? "bg-purple-400 border-purple-400 text-ghost"
662
+ : "bg-gold-500 border-gold-500 text-ink-950"
663
+ : "bg-transparent border-edge text-ash";
878
664
  return (
879
665
  <button
880
666
  key={t}
@@ -890,29 +676,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
890
676
  }));
891
677
  }}
892
678
  title={locked ? "KMS → age requires delete and recreate" : undefined}
893
- style={{
894
- background:
895
- state.type === t
896
- ? t === "kms"
897
- ? theme.purple
898
- : theme.accent
899
- : "transparent",
900
- border: `1px solid ${
901
- state.type === t
902
- ? t === "kms"
903
- ? theme.purple
904
- : theme.accent
905
- : theme.border
906
- }`,
907
- borderRadius: 4,
908
- padding: "3px 10px",
909
- cursor: locked ? "not-allowed" : "pointer",
910
- fontFamily: theme.mono,
911
- fontSize: 11,
912
- color: state.type === t ? "#fff" : theme.textMuted,
913
- opacity: locked ? 0.4 : 1,
914
- transition: "all 0.1s",
915
- }}
679
+ className={`rounded border px-2.5 py-0.5 font-mono text-[11px] transition-colors ${buttonClass} ${locked ? "cursor-not-allowed opacity-40" : "cursor-pointer"}`}
916
680
  >
917
681
  {t.toUpperCase()}
918
682
  </button>
@@ -922,7 +686,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
922
686
  </div>
923
687
 
924
688
  {state.type === "kms" && (
925
- <div style={{ display: "flex", gap: 8 }}>
689
+ <div className="flex gap-2">
926
690
  <select
927
691
  value={state.provider}
928
692
  onChange={(e) =>
@@ -931,12 +695,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
931
695
  [env.name]: { ...state, provider: e.target.value },
932
696
  }))
933
697
  }
934
- style={{
935
- ...inputStyle,
936
- width: 90,
937
- flexShrink: 0,
938
- padding: "7px 8px",
939
- }}
698
+ className={`${SMALL_INPUT_BASE} w-[90px] shrink-0`}
940
699
  >
941
700
  <option value="aws">AWS</option>
942
701
  <option value="gcp">GCP</option>
@@ -952,7 +711,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
952
711
  }))
953
712
  }
954
713
  placeholder="arn:aws:kms:… or key resource ID"
955
- style={{ ...inputStyle, flex: 1 }}
714
+ className={`${SMALL_INPUT_BASE} flex-1`}
956
715
  />
957
716
  </div>
958
717
  )}
@@ -961,7 +720,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
961
720
  })}
962
721
  </div>
963
722
 
964
- <div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
723
+ <div className="flex justify-end gap-2">
965
724
  <Button
966
725
  data-testid="update-cancel-btn"
967
726
  variant="ghost"
@@ -988,32 +747,22 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
988
747
  // ── Keys result view (post-creation) ─────────────────────────────────────────
989
748
  if (view === "keys") {
990
749
  const hasAgeKeys = Object.keys(privateKeys).length > 0;
991
- // For shared mode, all entries hold the same key — grab it once
992
750
  const sharedKey = wasSharedRecipient ? Object.values(privateKeys)[0] : undefined;
993
751
  const sharedEnvNames = wasSharedRecipient ? Object.keys(privateKeys) : [];
994
752
 
995
753
  return (
996
- <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
997
- <TopBar title={`${createdName} created`} subtitle="Service identity ready" />
998
- <div style={{ flex: 1, overflow: "auto", padding: 24 }}>
999
- <div style={{ maxWidth: 620, margin: "0 auto" }}>
754
+ <div className="flex flex-1 flex-col overflow-hidden">
755
+ <Toolbar>
756
+ <div>
757
+ <Toolbar.Title>{`${createdName} created`}</Toolbar.Title>
758
+ <Toolbar.Subtitle>Service identity ready</Toolbar.Subtitle>
759
+ </div>
760
+ </Toolbar>
761
+ <div className="flex-1 overflow-auto p-6">
762
+ <div className="mx-auto max-w-[620px]">
1000
763
  {hasAgeKeys && (
1001
- <div
1002
- style={{
1003
- background: "#1a1200",
1004
- border: `1px solid ${theme.yellow}55`,
1005
- borderRadius: 8,
1006
- padding: "14px 18px",
1007
- marginBottom: 20,
1008
- fontFamily: theme.sans,
1009
- fontSize: 13,
1010
- color: theme.yellow,
1011
- display: "flex",
1012
- gap: 10,
1013
- alignItems: "flex-start",
1014
- }}
1015
- >
1016
- <span style={{ fontSize: 16, flexShrink: 0 }}>⚠</span>
764
+ <div className="mb-5 flex items-start gap-2.5 rounded-lg border border-warn-500/40 bg-warn-500/[0.06] px-4 py-3.5 font-sans text-[13px] text-warn-500">
765
+ <span className="shrink-0 text-[16px]">⚠</span>
1017
766
  <span>
1018
767
  {wasSharedRecipient
1019
768
  ? `Copy this key now — it will not be shown again. Set it as CLEF_AGE_KEY in your CI. It decrypts: ${sharedEnvNames.join(", ")}.`
@@ -1023,18 +772,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
1023
772
  )}
1024
773
 
1025
774
  {!hasAgeKeys && (
1026
- <div
1027
- style={{
1028
- background: theme.purpleDim,
1029
- border: `1px solid ${theme.purple}44`,
1030
- borderRadius: 8,
1031
- padding: "14px 18px",
1032
- marginBottom: 20,
1033
- fontFamily: theme.sans,
1034
- fontSize: 13,
1035
- color: theme.purple,
1036
- }}
1037
- >
775
+ <div className="mb-5 rounded-lg border border-purple-400/30 bg-purple-400/10 px-4 py-3.5 font-sans text-[13px] text-purple-400">
1038
776
  All environments use KMS. No private keys to provision — runtimes authenticate via
1039
777
  IAM role.
1040
778
  </div>
@@ -1043,55 +781,20 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
1043
781
  <Label>Private keys</Label>
1044
782
 
1045
783
  {wasSharedRecipient && sharedKey ? (
1046
- // Shared mode: one block, all env badges, labeled CLEF_AGE_KEY
1047
- <div
1048
- style={{
1049
- background: theme.surface,
1050
- border: `1px solid ${theme.accent}44`,
1051
- borderRadius: 8,
1052
- padding: "14px 18px",
1053
- marginBottom: 10,
1054
- }}
1055
- >
1056
- <div
1057
- style={{
1058
- display: "flex",
1059
- alignItems: "center",
1060
- justifyContent: "space-between",
1061
- marginBottom: 10,
1062
- }}
1063
- >
1064
- <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
1065
- <span
1066
- style={{
1067
- fontFamily: theme.mono,
1068
- fontSize: 11,
1069
- color: theme.accent,
1070
- fontWeight: 600,
1071
- }}
1072
- >
784
+ <div className="mb-2.5 rounded-lg border border-gold-500/30 bg-ink-850 px-4 py-3.5">
785
+ <div className="mb-2.5 flex items-center justify-between">
786
+ <div className="flex flex-wrap items-center gap-2">
787
+ <span className="font-mono text-[11px] font-semibold text-gold-500">
1073
788
  CLEF_AGE_KEY
1074
789
  </span>
1075
- <span style={{ fontFamily: theme.sans, fontSize: 11, color: theme.textDim }}>
1076
-
1077
- </span>
790
+ <span className="font-sans text-[11px] text-ash-dim">—</span>
1078
791
  {sharedEnvNames.map((e) => (
1079
792
  <EnvBadge key={e} env={e} small />
1080
793
  ))}
1081
794
  </div>
1082
795
  <CopyButton text={sharedKey} />
1083
796
  </div>
1084
- <div
1085
- style={{
1086
- fontFamily: theme.mono,
1087
- fontSize: 11,
1088
- color: theme.textMuted,
1089
- wordBreak: "break-all",
1090
- background: theme.bg,
1091
- borderRadius: 4,
1092
- padding: "8px 10px",
1093
- }}
1094
- >
797
+ <div className="break-all rounded bg-ink-950 px-2.5 py-2 font-mono text-[11px] text-ash">
1095
798
  {sharedKey}
1096
799
  </div>
1097
800
  </div>
@@ -1099,43 +802,20 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
1099
802
  Object.entries(privateKeys).map(([envName, key]) => (
1100
803
  <div
1101
804
  key={envName}
1102
- style={{
1103
- background: theme.surface,
1104
- border: `1px solid ${theme.border}`,
1105
- borderRadius: 8,
1106
- padding: "14px 18px",
1107
- marginBottom: 10,
1108
- }}
805
+ className="mb-2.5 rounded-lg border border-edge bg-ink-850 px-4 py-3.5"
1109
806
  >
1110
- <div
1111
- style={{
1112
- display: "flex",
1113
- alignItems: "center",
1114
- justifyContent: "space-between",
1115
- marginBottom: 10,
1116
- }}
1117
- >
807
+ <div className="mb-2.5 flex items-center justify-between">
1118
808
  <EnvBadge env={envName} />
1119
809
  <CopyButton text={key} />
1120
810
  </div>
1121
- <div
1122
- style={{
1123
- fontFamily: theme.mono,
1124
- fontSize: 11,
1125
- color: theme.textMuted,
1126
- wordBreak: "break-all",
1127
- background: theme.bg,
1128
- borderRadius: 4,
1129
- padding: "8px 10px",
1130
- }}
1131
- >
811
+ <div className="break-all rounded bg-ink-950 px-2.5 py-2 font-mono text-[11px] text-ash">
1132
812
  {key}
1133
813
  </div>
1134
814
  </div>
1135
815
  ))
1136
816
  )}
1137
817
 
1138
- <div style={{ marginTop: 8, display: "flex", justifyContent: "flex-end" }}>
818
+ <div className="mt-2 flex justify-end">
1139
819
  <Button
1140
820
  variant="primary"
1141
821
  onClick={() => {
@@ -1170,158 +850,96 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
1170
850
  });
1171
851
 
1172
852
  return (
1173
- <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
1174
- <TopBar
1175
- title="New service identity"
1176
- subtitle="Scope cryptographic access to specific namespaces"
1177
- actions={
1178
- <button
1179
- onClick={goList}
1180
- style={{
1181
- background: "none",
1182
- border: `1px solid ${theme.borderLight}`,
1183
- borderRadius: 6,
1184
- padding: "4px 12px",
1185
- cursor: "pointer",
1186
- fontFamily: theme.sans,
1187
- fontSize: 12,
1188
- color: theme.textMuted,
1189
- }}
1190
- >
1191
- {"\u2190"} Cancel
853
+ <div className="flex flex-1 flex-col overflow-hidden">
854
+ <Toolbar>
855
+ <div>
856
+ <Toolbar.Title>New service identity</Toolbar.Title>
857
+ <Toolbar.Subtitle>Scope cryptographic access to specific namespaces</Toolbar.Subtitle>
858
+ </div>
859
+ <Toolbar.Actions>
860
+ <button onClick={goList} className={BACK_BUTTON}>
861
+ {""} Cancel
1192
862
  </button>
1193
- }
1194
- />
1195
- <div style={{ flex: 1, overflow: "auto", padding: 24 }}>
1196
- <div style={{ maxWidth: 560, margin: "0 auto" }}>
863
+ </Toolbar.Actions>
864
+ </Toolbar>
865
+ <div className="flex-1 overflow-auto p-6">
866
+ <div className="mx-auto max-w-[560px]">
1197
867
  {createError && <ErrorBanner>{createError}</ErrorBanner>}
1198
868
 
1199
- {/* Name */}
1200
- <div style={{ marginBottom: 20 }}>
869
+ <div className="mb-5">
1201
870
  <FieldLabel>Name</FieldLabel>
1202
871
  <input
1203
872
  data-testid="si-name-input"
1204
873
  value={name}
1205
874
  onChange={(e) => setName(e.target.value)}
1206
875
  placeholder="e.g. api-gateway"
1207
- style={inputStyle}
876
+ className={INPUT_BASE}
1208
877
  />
1209
878
  {nameError && (
1210
- <div
1211
- style={{
1212
- fontFamily: theme.sans,
1213
- fontSize: 12,
1214
- color: theme.red,
1215
- marginTop: 6,
1216
- }}
1217
- >
1218
- {nameError}
1219
- </div>
879
+ <div className="mt-1.5 font-sans text-[12px] text-stop-500">{nameError}</div>
1220
880
  )}
1221
881
  </div>
1222
882
 
1223
- {/* Description */}
1224
- <div style={{ marginBottom: 24 }}>
883
+ <div className="mb-6">
1225
884
  <FieldLabel>Description (optional)</FieldLabel>
1226
885
  <input
1227
886
  value={description}
1228
887
  onChange={(e) => setDescription(e.target.value)}
1229
888
  placeholder="e.g. API gateway service account"
1230
- style={inputStyle}
889
+ className={INPUT_BASE}
1231
890
  />
1232
891
  </div>
1233
892
 
1234
- {/* Role */}
1235
- <div style={{ marginBottom: 24 }}>
893
+ <div className="mb-6">
1236
894
  <FieldLabel>Role</FieldLabel>
1237
- <div
1238
- style={{
1239
- display: "flex",
1240
- gap: 0,
1241
- borderRadius: 6,
1242
- overflow: "hidden",
1243
- border: `1px solid ${theme.border}`,
1244
- width: "fit-content",
1245
- marginBottom: 8,
1246
- }}
1247
- >
895
+ <div className="mb-2 flex w-fit overflow-hidden rounded-md border border-edge">
1248
896
  {(["ci", "runtime"] as const).map((r) => (
1249
897
  <button
1250
898
  key={r}
1251
899
  data-testid={`role-${r}`}
1252
900
  onClick={() => {
1253
901
  setRole(r);
1254
- // Auto-set shared-recipient to the role's natural default
1255
902
  const newDefault = r === "ci";
1256
903
  setSharedRecipient(newDefault);
1257
904
  setSharedRecipientOverridden(false);
1258
905
  }}
1259
- style={{
1260
- background: role === r ? theme.accent : "transparent",
1261
- border: "none",
1262
- padding: "7px 18px",
1263
- cursor: "pointer",
1264
- fontFamily: theme.sans,
1265
- fontSize: 12,
1266
- fontWeight: role === r ? 600 : 400,
1267
- color: role === r ? "#fff" : theme.textMuted,
1268
- transition: "all 0.12s",
1269
- }}
906
+ className={`cursor-pointer border-none px-4 py-1.5 font-sans text-[12px] transition-colors ${
907
+ role === r
908
+ ? "bg-gold-500 font-semibold text-ink-950"
909
+ : "bg-transparent font-normal text-ash"
910
+ }`}
1270
911
  >
1271
912
  {r === "ci" ? "CI" : "Runtime"}
1272
913
  </button>
1273
914
  ))}
1274
915
  </div>
1275
- <div
1276
- style={{
1277
- fontFamily: theme.sans,
1278
- fontSize: 12,
1279
- color: theme.textMuted,
1280
- lineHeight: 1.5,
1281
- }}
1282
- >
916
+ <div className="font-sans text-[12px] leading-relaxed text-ash">
1283
917
  {role === "ci"
1284
918
  ? "Decrypts files directly. Keys are registered on encrypted SOPS files. Use for CI pipelines and local tools."
1285
919
  : "Decrypts packed artifacts only. Keys are NOT added to encrypted files — smaller blast radius for deployment targets (Lambda, ECS, containers)."}
1286
920
  </div>
1287
921
  </div>
1288
922
 
1289
- {/* Namespaces */}
1290
- <div style={{ marginBottom: 24 }}>
923
+ <div className="mb-6">
1291
924
  <FieldLabel>Namespaces</FieldLabel>
1292
- <div
1293
- style={{
1294
- fontFamily: theme.sans,
1295
- fontSize: 12,
1296
- color: theme.textMuted,
1297
- marginBottom: 10,
1298
- }}
1299
- >
925
+ <div className="mb-2.5 font-sans text-[12px] text-ash">
1300
926
  This identity can decrypt secrets only from the selected namespaces.
1301
927
  </div>
1302
928
  {namespaces.length === 0 && (
1303
- <div style={{ fontFamily: theme.sans, fontSize: 12, color: theme.textDim }}>
929
+ <div className="font-sans text-[12px] text-ash-dim">
1304
930
  No namespaces defined in manifest.
1305
931
  </div>
1306
932
  )}
1307
- <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
933
+ <div className="flex flex-col gap-1.5">
1308
934
  {namespaces.map((ns) => {
1309
935
  const checked = selectedNamespaces.has(ns.name);
1310
936
  return (
1311
937
  <label
1312
938
  key={ns.name}
1313
939
  data-testid={`ns-checkbox-${ns.name}`}
1314
- style={{
1315
- display: "flex",
1316
- alignItems: "center",
1317
- gap: 10,
1318
- padding: "10px 14px",
1319
- background: checked ? theme.accentDim : theme.surface,
1320
- border: `1px solid ${checked ? theme.accent + "55" : theme.border}`,
1321
- borderRadius: 6,
1322
- cursor: "pointer",
1323
- transition: "all 0.1s",
1324
- }}
940
+ className={`flex cursor-pointer items-center gap-2.5 rounded-md border px-3.5 py-2.5 transition-colors ${
941
+ checked ? "border-gold-500/40 bg-gold-500/[0.08]" : "border-edge bg-ink-850"
942
+ }`}
1325
943
  >
1326
944
  <input
1327
945
  type="checkbox"
@@ -1332,23 +950,15 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
1332
950
  else next.delete(ns.name);
1333
951
  setSelectedNamespaces(next);
1334
952
  }}
1335
- style={{ accentColor: theme.accent }}
953
+ className="accent-gold-500"
1336
954
  />
1337
955
  <span
1338
- style={{
1339
- fontFamily: theme.mono,
1340
- fontSize: 12,
1341
- color: checked ? theme.accent : theme.text,
1342
- }}
956
+ className={`font-mono text-[12px] ${checked ? "text-gold-500" : "text-bone"}`}
1343
957
  >
1344
958
  {ns.name}
1345
959
  </span>
1346
960
  {ns.description && (
1347
- <span
1348
- style={{ fontFamily: theme.sans, fontSize: 11, color: theme.textMuted }}
1349
- >
1350
- — {ns.description}
1351
- </span>
961
+ <span className="font-sans text-[11px] text-ash">— {ns.description}</span>
1352
962
  )}
1353
963
  </label>
1354
964
  );
@@ -1356,98 +966,44 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
1356
966
  </div>
1357
967
  </div>
1358
968
 
1359
- {/* Per-environment backend */}
1360
- <div style={{ marginBottom: 28 }}>
1361
- <div
1362
- style={{
1363
- display: "flex",
1364
- alignItems: "center",
1365
- justifyContent: "space-between",
1366
- marginBottom: 6,
1367
- }}
1368
- >
969
+ <div className="mb-7">
970
+ <div className="mb-1.5 flex items-center justify-between">
1369
971
  <FieldLabel>Environment backends</FieldLabel>
1370
- {/* Shared recipient toggle */}
1371
972
  <label
1372
973
  data-testid="shared-recipient-toggle"
1373
- style={{
1374
- display: "flex",
1375
- alignItems: "center",
1376
- gap: 7,
1377
- cursor: "pointer",
1378
- fontFamily: theme.sans,
1379
- fontSize: 11,
1380
- color: sharedRecipient ? theme.accent : theme.textMuted,
1381
- userSelect: "none",
1382
- }}
974
+ className={`flex cursor-pointer select-none items-center gap-1.5 font-sans text-[11px] ${sharedRecipient ? "text-gold-500" : "text-ash"}`}
1383
975
  >
1384
976
  <div
1385
- style={{
1386
- width: 28,
1387
- height: 16,
1388
- borderRadius: 8,
1389
- background: sharedRecipient ? theme.accent : theme.border,
1390
- position: "relative",
1391
- transition: "background 0.15s",
1392
- flexShrink: 0,
1393
- }}
977
+ className={`relative h-4 w-7 shrink-0 rounded-full transition-colors ${sharedRecipient ? "bg-gold-500" : "bg-edge"}`}
1394
978
  >
1395
979
  <div
1396
- style={{
1397
- position: "absolute",
1398
- top: 2,
1399
- left: sharedRecipient ? 14 : 2,
1400
- width: 12,
1401
- height: 12,
1402
- borderRadius: "50%",
1403
- background: "#fff",
1404
- transition: "left 0.15s",
1405
- }}
980
+ className={`absolute top-0.5 h-3 w-3 rounded-full bg-white transition-all ${sharedRecipient ? "left-3.5" : "left-0.5"}`}
1406
981
  />
1407
982
  <input
1408
983
  type="checkbox"
1409
984
  checked={sharedRecipient}
1410
985
  onChange={(e) => {
1411
986
  setSharedRecipient(e.target.checked);
1412
- // Track whether user manually overrode the role's default
1413
987
  const roleDefault = role === "ci";
1414
988
  setSharedRecipientOverridden(e.target.checked !== roleDefault);
1415
989
  }}
1416
- style={{ position: "absolute", opacity: 0, width: 0, height: 0 }}
990
+ className="absolute h-0 w-0 opacity-0"
1417
991
  />
1418
992
  </div>
1419
993
  Shared age key
1420
994
  </label>
1421
995
  </div>
1422
996
 
1423
- <div
1424
- style={{
1425
- fontFamily: theme.sans,
1426
- fontSize: 12,
1427
- color: theme.textMuted,
1428
- marginBottom: 10,
1429
- }}
1430
- >
997
+ <div className="mb-2.5 font-sans text-[12px] text-ash">
1431
998
  {sharedRecipient
1432
999
  ? "One age key pair for all environments — one CI secret, easier to provision."
1433
1000
  : "Age generates a key pair per environment. KMS uses your cloud provider — no key material is provisioned."}
1434
1001
  </div>
1435
1002
 
1436
- {/* Warning when shared-recipient is overridden from role default */}
1437
1003
  {sharedRecipientOverridden && (
1438
1004
  <div
1439
1005
  data-testid="shared-recipient-warning"
1440
- style={{
1441
- background: "#1a1200",
1442
- border: `1px solid ${theme.yellow}55`,
1443
- borderRadius: 6,
1444
- padding: "10px 14px",
1445
- marginBottom: 10,
1446
- fontFamily: theme.sans,
1447
- fontSize: 12,
1448
- color: theme.yellow,
1449
- lineHeight: 1.5,
1450
- }}
1006
+ className="mb-2.5 rounded-md border border-warn-500/40 bg-warn-500/[0.06] px-3.5 py-2.5 font-sans text-[12px] leading-relaxed text-warn-500"
1451
1007
  >
1452
1008
  {role === "ci" && !sharedRecipient
1453
1009
  ? "Most CI pipelines use a single key. Per-environment keys are useful when your CI environments have separate secret access controls (e.g. GitHub environment protection rules)."
@@ -1455,34 +1011,15 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
1455
1011
  </div>
1456
1012
  )}
1457
1013
 
1458
- <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
1014
+ <div className="flex flex-col gap-2">
1459
1015
  {sharedRecipient ? (
1460
- <div
1461
- style={{
1462
- background: theme.accentDim,
1463
- border: `1px solid ${theme.accent}44`,
1464
- borderRadius: 8,
1465
- padding: "14px 16px",
1466
- display: "flex",
1467
- alignItems: "center",
1468
- gap: 12,
1469
- }}
1470
- >
1471
- <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
1016
+ <div className="flex items-center gap-3 rounded-lg border border-gold-500/30 bg-gold-500/[0.08] px-4 py-3.5">
1017
+ <div className="flex flex-wrap gap-1.5">
1472
1018
  {environments.map((env) => (
1473
1019
  <EnvBadge key={env.name} env={env.name} small />
1474
1020
  ))}
1475
1021
  </div>
1476
- <span
1477
- style={{
1478
- fontFamily: theme.mono,
1479
- fontSize: 11,
1480
- color: theme.accent,
1481
- marginLeft: "auto",
1482
- }}
1483
- >
1484
- age (shared)
1485
- </span>
1022
+ <span className="ml-auto font-mono text-[11px] text-gold-500">age (shared)</span>
1486
1023
  </div>
1487
1024
  ) : (
1488
1025
  environments.map((env) => {
@@ -1490,68 +1027,45 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
1490
1027
  return (
1491
1028
  <div
1492
1029
  key={env.name}
1493
- style={{
1494
- background: theme.surface,
1495
- border: `1px solid ${theme.border}`,
1496
- borderRadius: 8,
1497
- padding: "14px 16px",
1498
- }}
1030
+ className="rounded-lg border border-edge bg-ink-850 px-4 py-3.5"
1499
1031
  >
1500
1032
  <div
1501
- style={{
1502
- display: "flex",
1503
- alignItems: "center",
1504
- justifyContent: "space-between",
1505
- marginBottom: cfg.type === "kms" ? 12 : 0,
1506
- }}
1033
+ className={`flex items-center justify-between ${cfg.type === "kms" ? "mb-3" : ""}`}
1507
1034
  >
1508
- <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
1035
+ <div className="flex items-center gap-2">
1509
1036
  <EnvBadge env={env.name} />
1510
1037
  {env.protected && (
1511
- <span style={{ fontSize: 11, color: theme.red }}>{"\uD83D\uDD12"}</span>
1038
+ <span className="text-[11px] text-stop-500">{"🔒"}</span>
1512
1039
  )}
1513
1040
  </div>
1514
- <div style={{ display: "flex", gap: 4 }}>
1515
- {(["age", "kms"] as const).map((t) => (
1516
- <button
1517
- key={t}
1518
- onClick={() =>
1519
- setEnvBackends((prev) => ({
1520
- ...prev,
1521
- [env.name]: { ...cfg, type: t },
1522
- }))
1523
- }
1524
- style={{
1525
- background:
1526
- cfg.type === t
1527
- ? t === "kms"
1528
- ? theme.purple
1529
- : theme.accent
1530
- : "transparent",
1531
- border: `1px solid ${
1532
- cfg.type === t
1533
- ? t === "kms"
1534
- ? theme.purple
1535
- : theme.accent
1536
- : theme.border
1537
- }`,
1538
- borderRadius: 4,
1539
- padding: "3px 10px",
1540
- cursor: "pointer",
1541
- fontFamily: theme.mono,
1542
- fontSize: 11,
1543
- color: cfg.type === t ? "#fff" : theme.textMuted,
1544
- transition: "all 0.1s",
1545
- }}
1546
- >
1547
- {t.toUpperCase()}
1548
- </button>
1549
- ))}
1041
+ <div className="flex gap-1">
1042
+ {(["age", "kms"] as const).map((t) => {
1043
+ const isSelected = cfg.type === t;
1044
+ const buttonClass = isSelected
1045
+ ? t === "kms"
1046
+ ? "bg-purple-400 border-purple-400 text-ghost"
1047
+ : "bg-gold-500 border-gold-500 text-ink-950"
1048
+ : "bg-transparent border-edge text-ash";
1049
+ return (
1050
+ <button
1051
+ key={t}
1052
+ onClick={() =>
1053
+ setEnvBackends((prev) => ({
1054
+ ...prev,
1055
+ [env.name]: { ...cfg, type: t },
1056
+ }))
1057
+ }
1058
+ className={`cursor-pointer rounded border px-2.5 py-0.5 font-mono text-[11px] transition-colors ${buttonClass}`}
1059
+ >
1060
+ {t.toUpperCase()}
1061
+ </button>
1062
+ );
1063
+ })}
1550
1064
  </div>
1551
1065
  </div>
1552
1066
 
1553
1067
  {cfg.type === "kms" && (
1554
- <div style={{ display: "flex", gap: 8 }}>
1068
+ <div className="flex gap-2">
1555
1069
  <select
1556
1070
  value={cfg.provider}
1557
1071
  onChange={(e) =>
@@ -1560,12 +1074,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
1560
1074
  [env.name]: { ...cfg, provider: e.target.value },
1561
1075
  }))
1562
1076
  }
1563
- style={{
1564
- ...inputStyle,
1565
- width: 90,
1566
- flexShrink: 0,
1567
- padding: "7px 8px",
1568
- }}
1077
+ className={`${SMALL_INPUT_BASE} w-[90px] shrink-0`}
1569
1078
  >
1570
1079
  <option value="aws">AWS</option>
1571
1080
  <option value="gcp">GCP</option>
@@ -1580,7 +1089,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
1580
1089
  }))
1581
1090
  }
1582
1091
  placeholder="arn:aws:kms:… or key resource ID"
1583
- style={{ ...inputStyle, flex: 1 }}
1092
+ className={`${SMALL_INPUT_BASE} flex-1`}
1584
1093
  />
1585
1094
  </div>
1586
1095
  )}
@@ -1591,7 +1100,7 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
1591
1100
  </div>
1592
1101
  </div>
1593
1102
 
1594
- <div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
1103
+ <div className="flex justify-end gap-2">
1595
1104
  <Button variant="ghost" onClick={goList} disabled={creating}>
1596
1105
  Cancel
1597
1106
  </Button>
@@ -1614,66 +1123,20 @@ export function ServiceIdentitiesScreen({ manifest }: ServiceIdentitiesScreenPro
1614
1123
 
1615
1124
  function Label({ children }: { children: React.ReactNode }) {
1616
1125
  return (
1617
- <div
1618
- style={{
1619
- fontFamily: theme.sans,
1620
- fontSize: 12,
1621
- fontWeight: 600,
1622
- color: theme.textMuted,
1623
- marginBottom: 6,
1624
- letterSpacing: "0.05em",
1625
- textTransform: "uppercase",
1626
- }}
1627
- >
1126
+ <div className="mb-1.5 font-sans text-[12px] font-semibold uppercase tracking-[0.05em] text-ash">
1628
1127
  {children}
1629
1128
  </div>
1630
1129
  );
1631
1130
  }
1632
1131
 
1633
1132
  function FieldLabel({ children }: { children: React.ReactNode }) {
1634
- return (
1635
- <div
1636
- style={{
1637
- fontFamily: theme.sans,
1638
- fontSize: 12,
1639
- fontWeight: 600,
1640
- color: theme.textMuted,
1641
- marginBottom: 6,
1642
- }}
1643
- >
1644
- {children}
1645
- </div>
1646
- );
1133
+ return <div className="mb-1.5 font-sans text-[12px] font-semibold text-ash">{children}</div>;
1647
1134
  }
1648
1135
 
1649
1136
  function ErrorBanner({ children }: { children: React.ReactNode }) {
1650
1137
  return (
1651
- <div
1652
- style={{
1653
- background: theme.redDim,
1654
- border: `1px solid ${theme.red}44`,
1655
- borderRadius: 8,
1656
- padding: "12px 16px",
1657
- marginBottom: 16,
1658
- fontFamily: theme.sans,
1659
- fontSize: 13,
1660
- color: theme.red,
1661
- }}
1662
- >
1138
+ <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">
1663
1139
  {children}
1664
1140
  </div>
1665
1141
  );
1666
1142
  }
1667
-
1668
- const inputStyle: React.CSSProperties = {
1669
- width: "100%",
1670
- background: theme.surface,
1671
- border: `1px solid ${theme.border}`,
1672
- borderRadius: 6,
1673
- padding: "8px 12px",
1674
- fontFamily: theme.mono,
1675
- fontSize: 12,
1676
- color: theme.text,
1677
- outline: "none",
1678
- boxSizing: "border-box",
1679
- };