@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,9 +1,8 @@
1
1
  import React, { useState, useEffect, useCallback, useRef } from "react";
2
- import { theme, ENV_COLORS } from "../theme";
3
2
  import { apiFetch } from "../api";
4
- import { TopBar } from "../components/TopBar";
5
3
  import { Button } from "../components/Button";
6
4
  import { EnvBadge } from "../components/EnvBadge";
5
+ import { Toolbar } from "../primitives";
7
6
  import type { ClefManifest, DecryptedFile, LintIssue } from "@clef-sh/core";
8
7
 
9
8
  interface EditorRow {
@@ -23,6 +22,22 @@ interface NamespaceEditorProps {
23
22
  manifest: ClefManifest | null;
24
23
  }
25
24
 
25
+ // Per-env underline colors for the tab bar. Mirrors ENV_COLORS but as Tailwind classes.
26
+ const ENV_TAB_BORDER: Record<string, string> = {
27
+ dev: "border-go-500",
28
+ staging: "border-warn-500",
29
+ production: "border-stop-500",
30
+ };
31
+
32
+ const VALUE_INPUT =
33
+ "flex-1 rounded border border-edge-strong bg-ink-800 px-2.5 py-1 font-mono text-[12px] text-bone outline-none focus-visible:border-gold-500";
34
+
35
+ const NEW_KEY_INPUT =
36
+ "shrink-0 basis-[240px] rounded border border-gold-500/40 bg-ink-800 px-2.5 py-1.5 font-mono text-[12px] text-bone outline-none focus-visible:border-gold-500";
37
+
38
+ const NEW_VALUE_INPUT =
39
+ "flex-1 rounded border border-edge bg-ink-800 px-2.5 py-1.5 font-mono text-[12px] text-bone outline-none focus-visible:border-gold-500";
40
+
26
41
  export function NamespaceEditor({ ns, initialEnv, manifest }: NamespaceEditorProps) {
27
42
  const [env, setEnv] = useState(initialEnv ?? "");
28
43
  const [rows, setRows] = useState<EditorRow[]>([]);
@@ -44,7 +59,6 @@ export function NamespaceEditor({ ns, initialEnv, manifest }: NamespaceEditorPro
44
59
 
45
60
  const REVEAL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
46
61
 
47
- // Clear timeout on unmount
48
62
  useEffect(() => () => clearTimeout(revealTimeoutRef.current), []);
49
63
 
50
64
  const environments = manifest?.environments ?? [];
@@ -84,7 +98,7 @@ export function NamespaceEditor({ ns, initialEnv, manifest }: NamespaceEditorPro
84
98
  const backend = data.metadata?.backend ?? "age";
85
99
  const recipientCount = data.metadata?.recipients?.length ?? 0;
86
100
  setSopsInfo(
87
- `encrypted with ${backend} \u00B7 ${recipientCount} recipient${recipientCount !== 1 ? "s" : ""}`,
101
+ `encrypted with ${backend} ยท ${recipientCount} recipient${recipientCount !== 1 ? "s" : ""}`,
88
102
  );
89
103
  } catch {
90
104
  setError("Failed to load namespace data");
@@ -109,7 +123,6 @@ export function NamespaceEditor({ ns, initialEnv, manifest }: NamespaceEditorPro
109
123
  const toggleVisible = (key: string) => {
110
124
  setRows((r) => r.map((row) => (row.key === key ? { ...row, visible: !row.visible } : row)));
111
125
 
112
- // Reset idle timeout on any reveal action
113
126
  clearTimeout(revealTimeoutRef.current);
114
127
  revealTimeoutRef.current = setTimeout(() => {
115
128
  setRows((r) =>
@@ -132,9 +145,6 @@ export function NamespaceEditor({ ns, initialEnv, manifest }: NamespaceEditorPro
132
145
  setSaving(true);
133
146
  setError(null);
134
147
  try {
135
- // Each PUT auto-commits via the transaction manager, matching CLI
136
- // behavior where each `clef set` is its own commit. Serialize so
137
- // each transaction completes before the next starts.
138
148
  let failure: string | null = null;
139
149
  for (const row of dirtyRows) {
140
150
  const payload: Record<string, unknown> = { value: row.value };
@@ -145,18 +155,11 @@ export function NamespaceEditor({ ns, initialEnv, manifest }: NamespaceEditorPro
145
155
  body: JSON.stringify(payload),
146
156
  });
147
157
  if (!res.ok) {
148
- // Bail on first failure โ€” for dirty-tree / recipient errors the
149
- // remaining PUTs will fail the same way, and piling errors on top
150
- // of each other just obscures the root cause.
151
158
  const data = await res.json().catch(() => ({}));
152
159
  failure = data.error || `Failed to save ${row.key}`;
153
160
  break;
154
161
  }
155
162
  }
156
- // Always reload from disk โ€” on success to pick up server-side state
157
- // (lastModified, metadata), on failure to snap the UI back to the
158
- // current on-disk values and re-mask the inputs rather than leaving
159
- // unsaved plaintext edits visible in an illusory "pending" state.
160
163
  await loadData();
161
164
  if (failure) setError(failure);
162
165
  } catch {
@@ -185,10 +188,7 @@ export function NamespaceEditor({ ns, initialEnv, manifest }: NamespaceEditorPro
185
188
  return;
186
189
  }
187
190
  const data = await res.json();
188
- if (data.warning) {
189
- setError(data.warning);
190
- }
191
- // Update local state directly โ€” avoids an extra decrypt round-trip
191
+ if (data.warning) setError(data.warning);
192
192
  setRows((prev) => [
193
193
  ...prev,
194
194
  {
@@ -224,7 +224,6 @@ export function NamespaceEditor({ ns, initialEnv, manifest }: NamespaceEditorPro
224
224
  setError(data.error || "Failed to reset key");
225
225
  return;
226
226
  }
227
- // Update local state directly โ€” avoids an extra decrypt round-trip
228
227
  setRows((prev) =>
229
228
  prev.map((row) =>
230
229
  row.key === key ? { ...row, pending: true, value: "", edited: false } : row,
@@ -282,47 +281,37 @@ export function NamespaceEditor({ ns, initialEnv, manifest }: NamespaceEditorPro
282
281
  const hasChanges = rows.some((r) => r.edited);
283
282
 
284
283
  return (
285
- <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
286
- <TopBar
287
- title={`/${ns}`}
288
- subtitle={`Namespace \u00B7 ${rows.length} keys`}
289
- actions={
290
- <>
291
- {hasChanges && (
292
- <Button
293
- variant="primary"
294
- disabled={saving}
295
- onClick={() => {
296
- if (isProduction) {
297
- setProtectedConfirm("save");
298
- } else {
299
- handleSave();
300
- }
301
- }}
302
- >
303
- {saving ? "Saving..." : "Save"}
304
- </Button>
305
- )}
306
- <Button variant="primary" data-testid="add-key-btn" onClick={() => setAdding(true)}>
307
- + Add key
284
+ <div className="flex flex-1 flex-col overflow-hidden">
285
+ <Toolbar>
286
+ <div>
287
+ <Toolbar.Title>{`/${ns}`}</Toolbar.Title>
288
+ <Toolbar.Subtitle>Namespace ยท {rows.length} keys</Toolbar.Subtitle>
289
+ </div>
290
+ <Toolbar.Actions>
291
+ {hasChanges && (
292
+ <Button
293
+ variant="primary"
294
+ disabled={saving}
295
+ onClick={() => {
296
+ if (isProduction) setProtectedConfirm("save");
297
+ else handleSave();
298
+ }}
299
+ >
300
+ {saving ? "Saving..." : "Save"}
308
301
  </Button>
309
- </>
310
- }
311
- />
302
+ )}
303
+ <Button variant="primary" data-testid="add-key-btn" onClick={() => setAdding(true)}>
304
+ + Add key
305
+ </Button>
306
+ </Toolbar.Actions>
307
+ </Toolbar>
312
308
 
313
309
  {/* Env tabs */}
314
- <div
315
- style={{
316
- display: "flex",
317
- gap: 0,
318
- borderBottom: `1px solid ${theme.border}`,
319
- padding: "0 24px",
320
- background: "#0D0F14",
321
- }}
322
- >
310
+ <div className="flex border-b border-edge bg-ink-800 px-6">
323
311
  {environments.map((e) => {
324
312
  const isActive = env === e.name;
325
- const c = ENV_COLORS[e.name] ?? { color: theme.textMuted };
313
+ const activeBorder = ENV_TAB_BORDER[e.name] ?? "border-ash";
314
+ const borderClass = isActive ? activeBorder : "border-transparent";
326
315
  return (
327
316
  <div
328
317
  key={e.name}
@@ -332,480 +321,214 @@ export function NamespaceEditor({ ns, initialEnv, manifest }: NamespaceEditorPro
332
321
  onKeyDown={(ev) => {
333
322
  if (ev.key === "Enter") setEnv(e.name);
334
323
  }}
335
- style={{
336
- padding: "10px 18px",
337
- cursor: "pointer",
338
- borderBottom: isActive ? `2px solid ${c.color}` : "2px solid transparent",
339
- display: "flex",
340
- alignItems: "center",
341
- gap: 7,
342
- marginBottom: -1,
343
- }}
324
+ className={`-mb-px flex cursor-pointer items-center gap-1.5 border-b-2 px-4 py-2.5 ${borderClass}`}
344
325
  >
345
326
  <EnvBadge env={e.name} small />
346
327
  <span
347
- style={{
348
- fontFamily: theme.sans,
349
- fontSize: 13,
350
- fontWeight: isActive ? 600 : 400,
351
- color: isActive ? theme.text : theme.textMuted,
352
- }}
328
+ className={`font-sans text-[13px] ${isActive ? "font-semibold text-bone" : "font-normal text-ash"}`}
353
329
  >
354
330
  {e.name}
355
331
  </span>
356
332
  </div>
357
333
  );
358
334
  })}
359
- <div style={{ flex: 1 }} />
360
- <div
361
- style={{
362
- padding: "10px 0",
363
- display: "flex",
364
- alignItems: "center",
365
- }}
366
- >
367
- <span
368
- style={{
369
- fontFamily: theme.mono,
370
- fontSize: 10,
371
- color: theme.textMuted,
372
- }}
373
- >
374
- {sopsInfo}
375
- </span>
335
+ <div className="flex-1" />
336
+ <div className="flex items-center py-2.5">
337
+ <span className="font-mono text-[10px] text-ash">{sopsInfo}</span>
376
338
  </div>
377
339
  </div>
378
340
 
379
- <div style={{ flex: 1, overflow: "auto", padding: 24 }}>
341
+ <div className="flex-1 overflow-auto p-6">
380
342
  {/* Production warning */}
381
343
  {isProduction && (
382
344
  <div
383
345
  data-testid="production-warning"
384
- style={{
385
- marginBottom: 20,
386
- padding: "10px 16px",
387
- background: theme.redDim,
388
- border: `1px solid ${theme.red}44`,
389
- borderRadius: 8,
390
- display: "flex",
391
- alignItems: "center",
392
- gap: 10,
393
- }}
346
+ className="mb-5 flex items-center gap-2.5 rounded-lg border border-stop-500/30 bg-stop-500/10 px-4 py-2.5"
394
347
  >
395
- <span style={{ fontSize: 14 }}>{"\uD83D\uDD12"}</span>
396
- <span style={{ fontFamily: theme.sans, fontSize: 12, color: theme.red }}>
348
+ <span className="text-[14px]">{"๐Ÿ”’"}</span>
349
+ <span className="font-sans text-[12px] text-stop-500">
397
350
  <strong>Production environment.</strong> Changes will require confirmation before
398
351
  committing.
399
352
  </span>
400
353
  </div>
401
354
  )}
402
355
 
403
- {loading && <p style={{ color: theme.textMuted, fontFamily: theme.sans }}>Loading...</p>}
356
+ {loading && <p className="font-sans text-ash">Loading...</p>}
404
357
 
405
358
  {error && (
406
- <div
407
- style={{
408
- padding: "12px 16px",
409
- background: theme.redDim,
410
- border: `1px solid ${theme.red}44`,
411
- borderRadius: 8,
412
- fontFamily: theme.sans,
413
- fontSize: 12,
414
- color: theme.red,
415
- marginBottom: 20,
416
- }}
417
- >
359
+ <div className="mb-5 rounded-lg border border-stop-500/30 bg-stop-500/10 px-4 py-3 font-sans text-[12px] text-stop-500">
418
360
  {error}
419
361
  </div>
420
362
  )}
421
363
 
422
364
  {!loading && (
423
365
  <>
424
- {/* Keys table. Rendered even when `error` is set so a failed
425
- save still shows the dirty rows + Save button for retry.
426
- On load failure `rows` is empty, so the table renders
427
- gracefully with no row body. */}
428
- <div
429
- style={{
430
- background: theme.surface,
431
- border: `1px solid ${theme.border}`,
432
- borderRadius: 10,
433
- }}
434
- >
366
+ {/* Keys table */}
367
+ <div className="rounded-card border border-edge bg-ink-850">
435
368
  {/* Header */}
436
- <div
437
- style={{
438
- display: "grid",
439
- gridTemplateColumns: "260px 1fr 90px 36px",
440
- background: "#0D0F14",
441
- padding: "10px 20px",
442
- borderBottom: `1px solid ${theme.border}`,
443
- borderRadius: "10px 10px 0 0",
444
- }}
445
- >
369
+ <div className="grid grid-cols-[260px_1fr_90px_36px] items-center rounded-t-card border-b border-edge bg-ink-800 px-5 py-2.5">
446
370
  {["Key", "Value", "Type", ""].map((h) => (
447
371
  <span
448
372
  key={h || "actions"}
449
- style={{
450
- fontFamily: theme.sans,
451
- fontSize: 11,
452
- fontWeight: 600,
453
- color: theme.textMuted,
454
- textTransform: "uppercase",
455
- letterSpacing: "0.07em",
456
- }}
373
+ className="font-sans text-[11px] font-semibold uppercase tracking-[0.07em] text-ash"
457
374
  >
458
375
  {h}
459
376
  </span>
460
377
  ))}
461
378
  </div>
462
379
 
463
- {rows.map((row, i) => (
464
- <div
465
- key={row.key}
466
- style={{
467
- display: "grid",
468
- gridTemplateColumns: "260px 1fr 90px 36px",
469
- padding: "0 20px",
470
- borderBottom: i < rows.length - 1 ? `1px solid ${theme.border}` : "none",
471
- background: row.pending
472
- ? "#F0A50012"
473
- : row.edited
474
- ? `${theme.accent}08`
475
- : "transparent",
476
- borderLeft: row.pending
477
- ? "3px solid #F0A50088"
478
- : row.edited
479
- ? `2px solid ${theme.accent}`
480
- : "2px solid transparent",
481
- alignItems: "center",
482
- minHeight: 48,
483
- }}
484
- >
485
- {/* Key */}
380
+ {rows.map((row, i) => {
381
+ const rowBg = row.pending
382
+ ? "bg-gold-500/[0.07]"
383
+ : row.edited
384
+ ? "bg-gold-500/[0.03]"
385
+ : "bg-transparent";
386
+ const rowBorderLeft = row.pending
387
+ ? "border-l-[3px] border-gold-500/55"
388
+ : row.edited
389
+ ? "border-l-2 border-gold-500"
390
+ : "border-l-2 border-transparent";
391
+ const rowBorderBottom = i < rows.length - 1 ? "border-b border-edge" : "border-b-0";
392
+ return (
486
393
  <div
487
- style={{
488
- display: "flex",
489
- alignItems: "center",
490
- gap: 8,
491
- paddingRight: 16,
492
- }}
394
+ key={row.key}
395
+ className={`grid min-h-[48px] grid-cols-[260px_1fr_90px_36px] items-center px-5 ${rowBg} ${rowBorderLeft} ${rowBorderBottom}`}
493
396
  >
494
- {row.required && (
495
- <span
496
- style={{
497
- color: theme.accent,
498
- fontSize: 14,
499
- lineHeight: 1,
500
- }}
501
- >
502
- *
503
- </span>
504
- )}
505
- <span
506
- style={{
507
- fontFamily: theme.mono,
508
- fontSize: 12,
509
- color: theme.text,
510
- }}
511
- >
512
- {row.key}
513
- </span>
514
- {row.edited && (
515
- <span
516
- data-testid="dirty-dot"
517
- style={{
518
- width: 6,
519
- height: 6,
520
- borderRadius: "50%",
521
- background: theme.accent,
522
- flexShrink: 0,
523
- display: "inline-block",
524
- }}
525
- />
526
- )}
527
- </div>
397
+ {/* Key */}
398
+ <div className="flex items-center gap-2 pr-4">
399
+ {row.required && (
400
+ <span className="text-[14px] leading-none text-gold-500">*</span>
401
+ )}
402
+ <span className="font-mono text-[12px] text-bone">{row.key}</span>
403
+ {row.edited && (
404
+ <span
405
+ data-testid="dirty-dot"
406
+ className="inline-block h-1.5 w-1.5 shrink-0 rounded-full bg-gold-500"
407
+ />
408
+ )}
409
+ </div>
528
410
 
529
- {/* Value */}
530
- <div
531
- style={{
532
- display: "flex",
533
- alignItems: "center",
534
- gap: 8,
535
- paddingRight: 16,
536
- }}
537
- >
538
- {row.pending && !row.visible ? (
539
- <span
540
- style={{
541
- fontFamily: theme.mono,
542
- fontSize: 11,
543
- fontStyle: "italic",
544
- color: theme.accent,
545
- }}
546
- >
547
- PENDING {"\u2014"} not yet set
548
- </span>
549
- ) : row.visible ? (
550
- <input
551
- type="text"
552
- data-testid={`value-input-${row.key}`}
553
- value={row.value}
554
- onChange={(e) => handleEdit(row.key, e.target.value)}
555
- autoComplete="off"
556
- placeholder={row.pending ? "Enter real value..." : undefined}
557
- style={{
558
- flex: 1,
559
- background: "#0D0F14",
560
- border: `1px solid ${theme.borderLight}`,
561
- borderRadius: 5,
562
- padding: "5px 10px",
563
- fontFamily: theme.mono,
564
- fontSize: 12,
565
- color: theme.text,
566
- outline: "none",
567
- }}
568
- />
569
- ) : (
570
- <span
571
- style={{
572
- fontFamily: theme.mono,
573
- fontSize: 13,
574
- color: theme.textMuted,
575
- letterSpacing: "0.15em",
576
- }}
577
- >
578
- {
579
- "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"
580
- }
581
- </span>
582
- )}
583
- {row.pending && !row.visible ? (
584
- <div style={{ display: "flex", gap: 6 }}>
411
+ {/* Value */}
412
+ <div className="flex items-center gap-2 pr-4">
413
+ {row.pending && !row.visible ? (
414
+ <span className="font-mono text-[11px] italic text-gold-500">
415
+ PENDING {"โ€”"} not yet set
416
+ </span>
417
+ ) : row.visible ? (
418
+ <input
419
+ type="text"
420
+ data-testid={`value-input-${row.key}`}
421
+ value={row.value}
422
+ onChange={(e) => handleEdit(row.key, e.target.value)}
423
+ autoComplete="off"
424
+ placeholder={row.pending ? "Enter real value..." : undefined}
425
+ className={VALUE_INPUT}
426
+ />
427
+ ) : (
428
+ <span className="font-mono text-[13px] tracking-[0.15em] text-ash">
429
+ {"โ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ขโ€ข"}
430
+ </span>
431
+ )}
432
+ {row.pending && !row.visible ? (
433
+ <div className="flex gap-1.5">
434
+ <button
435
+ data-testid={`set-value-${row.key}`}
436
+ onClick={() => toggleVisible(row.key)}
437
+ className="cursor-pointer rounded border border-gold-500/40 bg-gold-500/10 px-2.5 py-0.5 font-sans text-[11px] font-semibold text-gold-500 hover:bg-gold-500/20"
438
+ >
439
+ Set value
440
+ </button>
441
+ <button
442
+ data-testid={`accept-value-${row.key}`}
443
+ onClick={() => handleAccept(row.key)}
444
+ title="Accept the random value as the final secret"
445
+ className="cursor-pointer rounded border border-go-500/40 bg-go-500/10 px-2.5 py-0.5 font-sans text-[11px] font-semibold text-go-500 hover:bg-go-500/20"
446
+ >
447
+ Accept random
448
+ </button>
449
+ </div>
450
+ ) : (
585
451
  <button
586
- data-testid={`set-value-${row.key}`}
452
+ data-testid={`eye-${row.key}`}
587
453
  onClick={() => toggleVisible(row.key)}
588
- style={{
589
- background: `${theme.accent}18`,
590
- border: `1px solid ${theme.accent}55`,
591
- borderRadius: 5,
592
- cursor: "pointer",
593
- color: theme.accent,
594
- padding: "3px 10px",
595
- fontFamily: theme.sans,
596
- fontSize: 11,
597
- fontWeight: 600,
598
- }}
454
+ aria-label={row.visible ? "Hide value" : "Reveal value"}
455
+ className={`flex cursor-pointer items-center bg-transparent p-1 text-[13px] ${row.visible ? "text-gold-500" : "text-ash-dim"}`}
599
456
  >
600
- Set value
457
+ {"๐Ÿ‘"}
601
458
  </button>
602
- <button
603
- data-testid={`accept-value-${row.key}`}
604
- onClick={() => handleAccept(row.key)}
605
- title="Accept the random value as the final secret"
606
- style={{
607
- background: `${theme.green}18`,
608
- border: `1px solid ${theme.green}55`,
609
- borderRadius: 5,
610
- cursor: "pointer",
611
- color: theme.green,
612
- padding: "3px 10px",
613
- fontFamily: theme.sans,
614
- fontSize: 11,
615
- fontWeight: 600,
616
- }}
617
- >
618
- Accept random
619
- </button>
620
- </div>
621
- ) : (
622
- <button
623
- data-testid={`eye-${row.key}`}
624
- onClick={() => toggleVisible(row.key)}
625
- aria-label={row.visible ? "Hide value" : "Reveal value"}
626
- style={{
627
- background: "none",
628
- border: "none",
629
- cursor: "pointer",
630
- color: row.visible ? theme.accent : theme.textDim,
631
- padding: 4,
632
- display: "flex",
633
- alignItems: "center",
634
- fontSize: 13,
635
- }}
636
- >
637
- {"\uD83D\uDC41"}
638
- </button>
639
- )}
640
- </div>
459
+ )}
460
+ </div>
641
461
 
642
- {/* Type */}
643
- <div>
644
- {row.pending ? (
645
- <span
646
- style={{
647
- fontFamily: theme.mono,
648
- fontSize: 10,
649
- fontWeight: 700,
650
- color: theme.accent,
651
- background: `${theme.accent}18`,
652
- border: `1px solid ${theme.accent}33`,
653
- borderRadius: 3,
654
- padding: "2px 7px",
655
- }}
656
- >
657
- PENDING
658
- </span>
659
- ) : (
660
- <span
661
- style={{
662
- fontFamily: theme.mono,
663
- fontSize: 10,
664
- color: theme.blue,
665
- background: theme.blueDim,
666
- border: `1px solid ${theme.blue}33`,
667
- borderRadius: 3,
668
- padding: "2px 7px",
669
- }}
670
- >
671
- {row.type}
672
- </span>
673
- )}
674
- </div>
462
+ {/* Type */}
463
+ <div>
464
+ {row.pending ? (
465
+ <span className="rounded-sm border border-gold-500/20 bg-gold-500/10 px-1.5 py-0.5 font-mono text-[10px] font-bold text-gold-500">
466
+ PENDING
467
+ </span>
468
+ ) : (
469
+ <span className="rounded-sm border border-blue-400/20 bg-blue-400/10 px-1.5 py-0.5 font-mono text-[10px] text-blue-400">
470
+ {row.type}
471
+ </span>
472
+ )}
473
+ </div>
675
474
 
676
- {/* Actions */}
677
- <div style={{ display: "flex", justifyContent: "center", position: "relative" }}>
678
- <button
679
- data-testid={`overflow-${row.key}`}
680
- onClick={() => setOverflowKey(overflowKey === row.key ? null : row.key)}
681
- style={{
682
- background: "none",
683
- border: "none",
684
- cursor: "pointer",
685
- color: theme.textDim,
686
- fontSize: 16,
687
- padding: 4,
688
- }}
689
- >
690
- {"\u22EF"}
691
- </button>
692
- {overflowKey === row.key && (
693
- <div
694
- data-testid={`overflow-menu-${row.key}`}
695
- style={{
696
- position: "absolute",
697
- top: "100%",
698
- right: 0,
699
- zIndex: 10,
700
- background: theme.surface,
701
- border: `1px solid ${theme.border}`,
702
- borderRadius: 6,
703
- padding: 4,
704
- minWidth: 200,
705
- boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
706
- }}
475
+ {/* Actions */}
476
+ <div className="relative flex justify-center">
477
+ <button
478
+ data-testid={`overflow-${row.key}`}
479
+ onClick={() => setOverflowKey(overflowKey === row.key ? null : row.key)}
480
+ className="cursor-pointer bg-transparent p-1 text-[16px] text-ash-dim"
707
481
  >
708
- <button
709
- data-testid={`reset-random-${row.key}`}
710
- onClick={() => {
711
- setOverflowKey(null);
712
- setConfirmReset(row.key);
713
- }}
714
- title="Use this to immediately invalidate a compromised secret while you arrange a replacement."
715
- style={{
716
- display: "block",
717
- width: "100%",
718
- textAlign: "left",
719
- background: "none",
720
- border: "none",
721
- cursor: "pointer",
722
- fontFamily: theme.sans,
723
- fontSize: 12,
724
- color: theme.accent,
725
- padding: "6px 10px",
726
- borderRadius: 4,
727
- }}
728
- onMouseEnter={(e) => {
729
- (e.currentTarget as HTMLElement).style.background = theme.surfaceHover;
730
- }}
731
- onMouseLeave={(e) => {
732
- (e.currentTarget as HTMLElement).style.background = "none";
733
- }}
734
- >
735
- Reset to random (pending)
736
- </button>
737
- <button
738
- data-testid={`delete-key-${row.key}`}
739
- onClick={() => {
740
- setOverflowKey(null);
741
- setConfirmDelete(row.key);
742
- }}
743
- style={{
744
- display: "block",
745
- width: "100%",
746
- textAlign: "left",
747
- background: "none",
748
- border: "none",
749
- cursor: "pointer",
750
- fontFamily: theme.sans,
751
- fontSize: 12,
752
- color: theme.red,
753
- padding: "6px 10px",
754
- borderRadius: 4,
755
- }}
756
- onMouseEnter={(e) => {
757
- (e.currentTarget as HTMLElement).style.background = theme.surfaceHover;
758
- }}
759
- onMouseLeave={(e) => {
760
- (e.currentTarget as HTMLElement).style.background = "none";
761
- }}
482
+ {"โ‹ฏ"}
483
+ </button>
484
+ {overflowKey === row.key && (
485
+ <div
486
+ data-testid={`overflow-menu-${row.key}`}
487
+ className="absolute right-0 top-full z-10 min-w-[200px] rounded-md border border-edge bg-ink-850 p-1 shadow-soft-drop"
762
488
  >
763
- Delete key
764
- </button>
765
- </div>
766
- )}
489
+ <button
490
+ data-testid={`reset-random-${row.key}`}
491
+ onClick={() => {
492
+ setOverflowKey(null);
493
+ setConfirmReset(row.key);
494
+ }}
495
+ title="Use this to immediately invalidate a compromised secret while you arrange a replacement."
496
+ className="block w-full cursor-pointer rounded bg-transparent px-2.5 py-1.5 text-left font-sans text-[12px] text-gold-500 hover:bg-ink-800"
497
+ >
498
+ Reset to random (pending)
499
+ </button>
500
+ <button
501
+ data-testid={`delete-key-${row.key}`}
502
+ onClick={() => {
503
+ setOverflowKey(null);
504
+ setConfirmDelete(row.key);
505
+ }}
506
+ className="block w-full cursor-pointer rounded bg-transparent px-2.5 py-1.5 text-left font-sans text-[12px] text-stop-500 hover:bg-ink-800"
507
+ >
508
+ Delete key
509
+ </button>
510
+ </div>
511
+ )}
512
+ </div>
767
513
  </div>
768
- </div>
769
- ))}
514
+ );
515
+ })}
770
516
 
771
517
  {/* Add key row */}
772
518
  {adding && (
773
- <div
774
- style={{
775
- padding: "12px 20px",
776
- borderTop: `1px solid ${theme.border}`,
777
- display: "flex",
778
- flexDirection: "column",
779
- gap: 10,
780
- }}
781
- >
782
- <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
519
+ <div className="flex flex-col gap-2.5 border-t border-edge px-5 py-3">
520
+ <div className="flex items-center gap-2.5">
783
521
  <input
784
522
  data-testid="new-key-input"
785
523
  placeholder="KEY_NAME"
786
524
  value={newKey}
787
525
  onChange={(e) => setNewKey(e.target.value)}
788
- style={{
789
- flex: "0 0 240px",
790
- background: "#0D0F14",
791
- border: `1px solid ${theme.accent}66`,
792
- borderRadius: 5,
793
- padding: "6px 10px",
794
- fontFamily: theme.mono,
795
- fontSize: 12,
796
- color: theme.text,
797
- outline: "none",
798
- }}
526
+ className={NEW_KEY_INPUT}
799
527
  />
800
528
  {/* Mode toggle */}
801
529
  <div
802
530
  role="radiogroup"
803
- style={{
804
- display: "flex",
805
- border: `1px solid ${theme.border}`,
806
- borderRadius: 6,
807
- overflow: "hidden",
808
- }}
531
+ className="flex overflow-hidden rounded-md border border-edge"
809
532
  >
810
533
  <button
811
534
  data-testid="mode-set-value"
@@ -815,17 +538,11 @@ export function NamespaceEditor({ ns, initialEnv, manifest }: NamespaceEditorPro
815
538
  setAddMode("value");
816
539
  setNewValue("");
817
540
  }}
818
- style={{
819
- fontFamily: theme.sans,
820
- fontSize: 11,
821
- fontWeight: 600,
822
- padding: "4px 14px",
823
- border: "none",
824
- borderRight: `1px solid ${theme.border}`,
825
- background: addMode === "value" ? `${theme.accent}22` : "transparent",
826
- color: addMode === "value" ? theme.accent : theme.textMuted,
827
- cursor: "pointer",
828
- }}
541
+ className={`cursor-pointer border-r border-edge px-3.5 py-1 font-sans text-[11px] font-semibold ${
542
+ addMode === "value"
543
+ ? "bg-gold-500/15 text-gold-500"
544
+ : "bg-transparent text-ash"
545
+ }`}
829
546
  >
830
547
  Set value
831
548
  </button>
@@ -837,22 +554,17 @@ export function NamespaceEditor({ ns, initialEnv, manifest }: NamespaceEditorPro
837
554
  setAddMode("random");
838
555
  setNewValue("");
839
556
  }}
840
- style={{
841
- fontFamily: theme.sans,
842
- fontSize: 11,
843
- fontWeight: 600,
844
- padding: "4px 14px",
845
- border: "none",
846
- background: addMode === "random" ? `${theme.accent}22` : "transparent",
847
- color: addMode === "random" ? theme.accent : theme.textMuted,
848
- cursor: "pointer",
849
- }}
557
+ className={`cursor-pointer px-3.5 py-1 font-sans text-[11px] font-semibold ${
558
+ addMode === "random"
559
+ ? "bg-gold-500/15 text-gold-500"
560
+ : "bg-transparent text-ash"
561
+ }`}
850
562
  >
851
563
  Random (pending)
852
564
  </button>
853
565
  </div>
854
566
  </div>
855
- <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
567
+ <div className="flex items-center gap-2.5">
856
568
  {addMode === "value" ? (
857
569
  <input
858
570
  data-testid="new-value-input"
@@ -861,29 +573,10 @@ export function NamespaceEditor({ ns, initialEnv, manifest }: NamespaceEditorPro
861
573
  value={newValue}
862
574
  onChange={(e) => setNewValue(e.target.value)}
863
575
  autoComplete="off"
864
- style={{
865
- flex: 1,
866
- background: "#0D0F14",
867
- border: `1px solid ${theme.border}`,
868
- borderRadius: 5,
869
- padding: "6px 10px",
870
- fontFamily: theme.mono,
871
- fontSize: 12,
872
- color: theme.text,
873
- outline: "none",
874
- }}
576
+ className={NEW_VALUE_INPUT}
875
577
  />
876
578
  ) : (
877
- <div
878
- style={{
879
- flex: 1,
880
- fontFamily: theme.mono,
881
- fontSize: 11,
882
- fontStyle: "italic",
883
- color: theme.accent,
884
- padding: "6px 10px",
885
- }}
886
- >
579
+ <div className="flex-1 px-2.5 py-1.5 font-mono text-[11px] italic text-gold-500">
887
580
  A cryptographically random placeholder will be generated server-side.
888
581
  </div>
889
582
  )}
@@ -891,11 +584,8 @@ export function NamespaceEditor({ ns, initialEnv, manifest }: NamespaceEditorPro
891
584
  variant="primary"
892
585
  data-testid="add-key-submit"
893
586
  onClick={() => {
894
- if (isProduction) {
895
- setProtectedConfirm("add");
896
- } else {
897
- handleAdd();
898
- }
587
+ if (isProduction) setProtectedConfirm("add");
588
+ else handleAdd();
899
589
  }}
900
590
  >
901
591
  {addMode === "random" ? "Generate random value" : "Add"}
@@ -919,33 +609,18 @@ export function NamespaceEditor({ ns, initialEnv, manifest }: NamespaceEditorPro
919
609
  {confirmReset && (
920
610
  <div
921
611
  data-testid="confirm-reset-dialog"
922
- style={{
923
- marginTop: 16,
924
- padding: "14px 18px",
925
- background: `${theme.accent}0A`,
926
- border: `1px solid ${theme.accent}33`,
927
- borderRadius: 8,
928
- fontFamily: theme.sans,
929
- fontSize: 12,
930
- }}
612
+ className="mt-4 rounded-lg border border-gold-500/30 bg-gold-500/[0.04] px-4 py-3.5 font-sans text-[12px]"
931
613
  >
932
- <p style={{ color: theme.text, margin: "0 0 10px 0" }}>
933
- Reset <strong style={{ fontFamily: theme.mono }}>{confirmReset}</strong> to a
934
- random placeholder? The current value will be overwritten.
614
+ <p className="m-0 mb-2.5 text-bone">
615
+ Reset <strong className="font-mono">{confirmReset}</strong> to a random
616
+ placeholder? The current value will be overwritten.
935
617
  </p>
936
618
  {isProduction && (
937
- <p
938
- style={{
939
- color: theme.red,
940
- margin: "0 0 10px 0",
941
- fontSize: 12,
942
- fontWeight: 600,
943
- }}
944
- >
945
- {"\uD83D\uDD12"} This is a protected environment.
619
+ <p className="m-0 mb-2.5 text-[12px] font-semibold text-stop-500">
620
+ {"๐Ÿ”’"} This is a protected environment.
946
621
  </p>
947
622
  )}
948
- <div style={{ display: "flex", gap: 8 }}>
623
+ <div className="flex gap-2">
949
624
  <Button
950
625
  variant="primary"
951
626
  data-testid="confirm-reset-yes"
@@ -964,34 +639,18 @@ export function NamespaceEditor({ ns, initialEnv, manifest }: NamespaceEditorPro
964
639
  {confirmDelete && (
965
640
  <div
966
641
  data-testid="confirm-delete-dialog"
967
- style={{
968
- marginTop: 16,
969
- padding: "14px 18px",
970
- background: theme.redDim,
971
- border: `1px solid ${theme.red}44`,
972
- borderRadius: 8,
973
- fontFamily: theme.sans,
974
- fontSize: 12,
975
- }}
642
+ className="mt-4 rounded-lg border border-stop-500/30 bg-stop-500/10 px-4 py-3.5 font-sans text-[12px]"
976
643
  >
977
- <p style={{ color: theme.text, margin: "0 0 10px 0" }}>
978
- Permanently delete{" "}
979
- <strong style={{ fontFamily: theme.mono }}>{confirmDelete}</strong> from{" "}
644
+ <p className="m-0 mb-2.5 text-bone">
645
+ Permanently delete <strong className="font-mono">{confirmDelete}</strong> from{" "}
980
646
  <strong>{env}</strong>? This cannot be undone.
981
647
  </p>
982
648
  {isProduction && (
983
- <p
984
- style={{
985
- color: theme.red,
986
- margin: "0 0 10px 0",
987
- fontSize: 12,
988
- fontWeight: 600,
989
- }}
990
- >
991
- {"\uD83D\uDD12"} This is a protected environment.
649
+ <p className="m-0 mb-2.5 text-[12px] font-semibold text-stop-500">
650
+ {"๐Ÿ”’"} This is a protected environment.
992
651
  </p>
993
652
  )}
994
- <div style={{ display: "flex", gap: 8 }}>
653
+ <div className="flex gap-2">
995
654
  <Button
996
655
  variant="danger"
997
656
  data-testid="confirm-delete-yes"
@@ -1010,34 +669,22 @@ export function NamespaceEditor({ ns, initialEnv, manifest }: NamespaceEditorPro
1010
669
  {protectedConfirm && (
1011
670
  <div
1012
671
  data-testid="confirm-protected-dialog"
1013
- style={{
1014
- marginTop: 16,
1015
- padding: "14px 18px",
1016
- background: theme.redDim,
1017
- border: `1px solid ${theme.red}44`,
1018
- borderRadius: 8,
1019
- fontFamily: theme.sans,
1020
- fontSize: 12,
1021
- }}
672
+ className="mt-4 rounded-lg border border-stop-500/30 bg-stop-500/10 px-4 py-3.5 font-sans text-[12px]"
1022
673
  >
1023
- <p style={{ color: theme.text, margin: "0 0 10px 0" }}>
1024
- {"\uD83D\uDD12"}{" "}
1025
- <strong style={{ color: theme.red }}>Protected environment.</strong> You are about
1026
- to {protectedConfirm === "save" ? "commit changes to" : "add a key to"}{" "}
674
+ <p className="m-0 mb-2.5 text-bone">
675
+ {"๐Ÿ”’"} <strong className="text-stop-500">Protected environment.</strong> You are
676
+ about to {protectedConfirm === "save" ? "commit changes to" : "add a key to"}{" "}
1027
677
  <strong>{env}</strong>. Are you sure?
1028
678
  </p>
1029
- <div style={{ display: "flex", gap: 8 }}>
679
+ <div className="flex gap-2">
1030
680
  <Button
1031
681
  variant="primary"
1032
682
  data-testid="confirm-protected-yes"
1033
683
  onClick={async () => {
1034
684
  const action = protectedConfirm;
1035
685
  setProtectedConfirm(null);
1036
- if (action === "save") {
1037
- await handleSave(true);
1038
- } else {
1039
- await handleAdd(true);
1040
- }
686
+ if (action === "save") await handleSave(true);
687
+ else await handleAdd(true);
1041
688
  }}
1042
689
  >
1043
690
  Confirm
@@ -1053,32 +700,13 @@ export function NamespaceEditor({ ns, initialEnv, manifest }: NamespaceEditorPro
1053
700
  )}
1054
701
 
1055
702
  {/* Schema section */}
1056
- <div style={{ marginTop: 24 }}>
1057
- <div
1058
- style={{
1059
- fontFamily: theme.sans,
1060
- fontSize: 11,
1061
- fontWeight: 600,
1062
- color: theme.textMuted,
1063
- textTransform: "uppercase",
1064
- letterSpacing: "0.08em",
1065
- marginBottom: 10,
1066
- }}
1067
- >
1068
- Schema {"\u00B7"} schemas/{ns}.yaml
703
+ <div className="mt-6">
704
+ <div className="mb-2.5 font-sans text-[11px] font-semibold uppercase tracking-[0.08em] text-ash">
705
+ Schema ยท schemas/{ns}.yaml
1069
706
  </div>
1070
707
  <div
1071
708
  data-testid="schema-summary"
1072
- style={{
1073
- background: theme.surface,
1074
- border: `1px solid ${theme.border}`,
1075
- borderRadius: 8,
1076
- padding: "12px 16px",
1077
- fontFamily: theme.mono,
1078
- fontSize: 11,
1079
- color: theme.textMuted,
1080
- lineHeight: 1.7,
1081
- }}
709
+ className="rounded-lg border border-edge bg-ink-850 px-4 py-3 font-mono text-[11px] leading-relaxed text-ash"
1082
710
  >
1083
711
  {(() => {
1084
712
  const errors = lintIssues.filter((i) => i.severity === "error");
@@ -1086,26 +714,23 @@ export function NamespaceEditor({ ns, initialEnv, manifest }: NamespaceEditorPro
1086
714
  if (errors.length === 0 && warnings.length === 0) {
1087
715
  return (
1088
716
  <>
1089
- <span style={{ color: theme.green }}>{"\u2713"}</span> All required keys
1090
- present &nbsp;{"\u00B7"}&nbsp;
1091
- <span style={{ color: theme.green }}>{"\u2713"}</span> All types valid
1092
- &nbsp;{"\u00B7"}&nbsp;
1093
- <span style={{ color: theme.textDim }}>0 warnings</span>
717
+ <span className="text-go-500">{"โœ“"}</span> All required keys present
718
+ &nbsp;ยท&nbsp;
719
+ <span className="text-go-500">{"โœ“"}</span> All types valid &nbsp;ยท&nbsp;
720
+ <span className="text-ash-dim">0 warnings</span>
1094
721
  </>
1095
722
  );
1096
723
  }
1097
724
  return (
1098
725
  <>
1099
726
  {errors.length > 0 && (
1100
- <span style={{ color: theme.red }}>
727
+ <span className="text-stop-500">
1101
728
  {errors.length} error{errors.length !== 1 ? "s" : ""}
1102
729
  </span>
1103
730
  )}
1104
- {errors.length > 0 && warnings.length > 0 && (
1105
- <span> &nbsp;{"\u00B7"}&nbsp; </span>
1106
- )}
731
+ {errors.length > 0 && warnings.length > 0 && <span> &nbsp;ยท&nbsp; </span>}
1107
732
  {warnings.length > 0 && (
1108
- <span style={{ color: theme.yellow }}>
733
+ <span className="text-warn-500">
1109
734
  {warnings.length} warning{warnings.length !== 1 ? "s" : ""}
1110
735
  </span>
1111
736
  )}