@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,28 @@
1
1
  import React from "react";
2
- import { theme } from "../theme";
2
+ import {
3
+ ArrowLeftRight,
4
+ CheckCircle2,
5
+ Clock,
6
+ FileText,
7
+ Hash,
8
+ KeyRound,
9
+ LayoutGrid,
10
+ Mail,
11
+ RefreshCw,
12
+ RotateCcw,
13
+ Scale,
14
+ ScanSearch,
15
+ Table2,
16
+ Upload,
17
+ Users,
18
+ type LucideIcon,
19
+ } from "lucide-react";
3
20
  import type { ClefManifest, MatrixStatus, GitStatus as GitStatusType } from "@clef-sh/core";
4
21
 
5
22
  export type ViewName =
6
23
  | "matrix"
7
24
  | "editor"
25
+ | "schema"
8
26
  | "diff"
9
27
  | "lint"
10
28
  | "scan"
@@ -15,7 +33,8 @@ export type ViewName =
15
33
  | "backend"
16
34
  | "reset"
17
35
  | "history"
18
- | "manifest";
36
+ | "manifest"
37
+ | "envelope";
19
38
 
20
39
  interface SidebarProps {
21
40
  activeView: ViewName;
@@ -30,6 +49,8 @@ interface SidebarProps {
30
49
  policyOverdueCount: number;
31
50
  }
32
51
 
52
+ type BadgeTone = "stop" | "warn" | "purple";
53
+
33
54
  export function Sidebar({
34
55
  activeView,
35
56
  setView,
@@ -49,135 +70,94 @@ export function Sidebar({
49
70
  const namespaces = manifest?.namespaces ?? [];
50
71
 
51
72
  return (
52
- <div
53
- style={{
54
- width: 220,
55
- // Fixed viewport height (not minHeight) so the flex column can
56
- // actually clip overflow. Paired with overflowY: auto on the nav
57
- // block below, this lets the middle section scroll when the list
58
- // grows or the user zooms in, while the logo and footer stay pinned.
59
- height: "100vh",
60
- background: theme.surface,
61
- borderRight: `1px solid ${theme.border}`,
62
- display: "flex",
63
- flexDirection: "column",
64
- flexShrink: 0,
65
- }}
66
- >
67
- {/* Logo */}
68
- <div
69
- style={{
70
- padding: "20px 20px 16px",
71
- borderBottom: `1px solid ${theme.border}`,
72
- display: "flex",
73
- alignItems: "center",
74
- gap: 10,
75
- }}
76
- >
73
+ // Fixed viewport height (not minHeight) so the flex column can actually
74
+ // clip overflow. Paired with overflow-y-auto on the nav block below, this
75
+ // lets the middle section scroll when the list grows or the user zooms in,
76
+ // while the logo and footer stay pinned.
77
+ <div className="flex h-screen w-[220px] shrink-0 flex-col border-r border-edge bg-ink-850">
78
+ <div className="flex items-center gap-2.5 border-b border-edge px-5 pt-5 pb-4">
77
79
  <div
78
- style={{
79
- width: 30,
80
- height: 30,
81
- background: theme.accentDim,
82
- border: `1px solid ${theme.accent}44`,
83
- borderRadius: 7,
84
- display: "flex",
85
- alignItems: "center",
86
- justifyContent: "center",
87
- color: theme.accent,
88
- fontSize: 15,
89
- }}
80
+ aria-hidden="true"
81
+ className="flex h-[30px] w-[30px] items-center justify-center rounded-md border border-gold-500/30 bg-gold-500/[0.08]"
90
82
  >
91
- {"\u266A"}
83
+ <img
84
+ src="/clef.svg"
85
+ alt=""
86
+ width={12}
87
+ height={20}
88
+ className="[filter:drop-shadow(0_0_8px_rgba(240,165,0,0.33))]"
89
+ />
92
90
  </div>
93
91
  <div>
94
- <div
95
- style={{
96
- fontFamily: theme.sans,
97
- fontWeight: 700,
98
- fontSize: 16,
99
- color: theme.text,
100
- letterSpacing: "-0.02em",
101
- }}
102
- >
103
- clef
92
+ <div className="font-mono text-[18px] font-bold leading-none tracking-[-0.02em] text-bone">
93
+ Clef
104
94
  </div>
105
- <div
106
- style={{
107
- fontFamily: theme.mono,
108
- fontSize: 9,
109
- color: theme.textMuted,
110
- marginTop: -1,
111
- }}
112
- >
95
+ <div className="mt-1 font-mono text-[9px] uppercase tracking-[0.12em] text-ash">
113
96
  {manifest?.sops.default_backend ?? "local"} / main
114
97
  </div>
115
98
  </div>
116
99
  </div>
117
100
 
118
- {/* Nav */}
119
- <div
120
- style={{
121
- padding: "12px 10px",
122
- flex: 1,
123
- // minHeight: 0 is the standard flex quirk — without it, a flex
124
- // child's scrollable content never shrinks below its intrinsic
125
- // size, so overflowY: auto would never actually clip.
126
- minHeight: 0,
127
- overflowY: "auto",
128
- overflowX: "hidden",
129
- }}
130
- >
101
+ {/* min-h-0 is the standard flex quirk — without it, a flex child's
102
+ scrollable content never shrinks below its intrinsic size, so
103
+ overflow-y-auto would never actually clip. */}
104
+ <div className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-2.5 py-3">
131
105
  <NavItem
132
- icon={"\u229E"}
106
+ icon={LayoutGrid}
133
107
  label="Matrix"
134
108
  active={activeView === "matrix"}
135
109
  onClick={() => setView("matrix")}
136
110
  />
137
111
  <NavItem
138
- icon={"\u21C4"}
112
+ icon={ArrowLeftRight}
139
113
  label="Diff"
140
114
  active={activeView === "diff"}
141
115
  onClick={() => setView("diff")}
142
116
  />
143
117
  <NavItem
144
- icon={"\u2714"}
118
+ icon={CheckCircle2}
145
119
  label="Lint"
146
120
  active={activeView === "lint"}
147
121
  onClick={() => setView("lint")}
148
122
  badge={lintErrorCount > 0 ? String(lintErrorCount) : undefined}
149
- badgeColor={theme.red}
123
+ badgeTone="stop"
150
124
  />
151
125
  <NavItem
152
- icon={"\u2315"}
126
+ icon={ScanSearch}
153
127
  label="Scan"
154
128
  active={activeView === "scan"}
155
129
  onClick={() => setView("scan")}
156
130
  badge={scanIssueCount > 0 ? String(scanIssueCount) : undefined}
157
- badgeColor={theme.yellow}
131
+ badgeTone="warn"
132
+ />
133
+ <NavItem
134
+ icon={Table2}
135
+ label="Schema"
136
+ active={activeView === "schema"}
137
+ onClick={() => setView("schema")}
158
138
  />
159
139
  <NavItem
160
- icon={"\u2696"}
140
+ icon={Scale}
161
141
  label="Policy"
162
142
  active={activeView === "policy"}
163
143
  onClick={() => setView("policy")}
164
144
  badge={policyOverdueCount > 0 ? String(policyOverdueCount) : undefined}
165
- badgeColor={theme.red}
145
+ badgeTone="stop"
166
146
  />
167
147
  <NavItem
168
- icon={"\u2B06"}
148
+ icon={Upload}
169
149
  label="Import"
170
150
  active={activeView === "import"}
171
151
  onClick={() => setView("import")}
172
152
  />
173
153
  <NavItem
174
- icon={"\u2662"}
154
+ icon={Users}
175
155
  label="Recipients"
176
156
  active={activeView === "recipients"}
177
157
  onClick={() => setView("recipients")}
178
158
  />
179
159
  <NavItem
180
- icon={"\u2699"}
160
+ icon={KeyRound}
181
161
  label="Service IDs"
182
162
  active={activeView === "identities"}
183
163
  onClick={() => setView("identities")}
@@ -186,44 +166,41 @@ export function Sidebar({
186
166
  ? String(manifest.service_identities.length)
187
167
  : undefined
188
168
  }
189
- badgeColor={theme.purple}
169
+ badgeTone="purple"
190
170
  />
191
171
  <NavItem
192
- icon={"\u21BB"}
172
+ icon={RefreshCw}
193
173
  label="Backend"
194
174
  active={activeView === "backend"}
195
175
  onClick={() => setView("backend")}
196
176
  />
197
177
  <NavItem
198
- icon={"\u2421"}
178
+ icon={RotateCcw}
199
179
  label="Reset"
200
180
  active={activeView === "reset"}
201
181
  onClick={() => setView("reset")}
202
182
  />
203
183
  <NavItem
204
- icon={"\u2630"}
184
+ icon={FileText}
205
185
  label="Manifest"
206
186
  active={activeView === "manifest"}
207
187
  onClick={() => setView("manifest")}
208
188
  />
209
189
  <NavItem
210
- icon={"\u23F1"}
190
+ icon={Mail}
191
+ label="Envelope"
192
+ active={activeView === "envelope"}
193
+ onClick={() => setView("envelope")}
194
+ />
195
+ <NavItem
196
+ icon={Clock}
211
197
  label="History"
212
198
  active={activeView === "history"}
213
199
  onClick={() => setView("history")}
214
200
  />
215
201
 
216
- <div style={{ marginTop: 20, marginBottom: 6, padding: "0 8px" }}>
217
- <span
218
- style={{
219
- fontFamily: theme.sans,
220
- fontSize: 10,
221
- fontWeight: 600,
222
- color: theme.textDim,
223
- letterSpacing: "0.1em",
224
- textTransform: "uppercase",
225
- }}
226
- >
202
+ <div className="mt-5 mb-1.5 px-2">
203
+ <span className="font-sans text-[10px] font-semibold uppercase tracking-[0.1em] text-ash-deep">
227
204
  Namespaces
228
205
  </span>
229
206
  </div>
@@ -235,17 +212,7 @@ export function Sidebar({
235
212
  return (
236
213
  <NavItem
237
214
  key={ns.name}
238
- icon={
239
- <span
240
- style={{
241
- fontFamily: theme.mono,
242
- fontSize: 10,
243
- color: theme.textMuted,
244
- }}
245
- >
246
- //
247
- </span>
248
- }
215
+ icon={Hash}
249
216
  label={ns.name}
250
217
  active={activeView === "editor" && activeNs === ns.name}
251
218
  onClick={() => {
@@ -253,49 +220,21 @@ export function Sidebar({
253
220
  setNs(ns.name);
254
221
  }}
255
222
  badge={hasIssue ? "!" : undefined}
256
- badgeColor={theme.yellow}
223
+ badgeTone="warn"
257
224
  />
258
225
  );
259
226
  })}
260
227
  </div>
261
228
 
262
- {/* Footer */}
263
- <div style={{ padding: "12px 16px", borderTop: `1px solid ${theme.border}` }}>
264
- <div
265
- style={{
266
- display: "flex",
267
- alignItems: "center",
268
- gap: 6,
269
- color: theme.textMuted,
270
- }}
271
- >
272
- <span style={{ fontFamily: theme.mono, fontSize: 10 }}>
273
- {uncommittedCount} uncommitted
274
- </span>
229
+ <div className="border-t border-edge px-4 py-3">
230
+ <div className="flex items-center gap-1.5 text-ash">
231
+ <span className="font-mono text-[10px]">{uncommittedCount} uncommitted</span>
275
232
  </div>
276
- <div style={{ marginTop: 5 }}>
277
- <div
278
- style={{
279
- display: "flex",
280
- alignItems: "center",
281
- gap: 6,
282
- color: theme.green,
283
- }}
284
- >
285
- <span
286
- style={{
287
- display: "inline-block",
288
- width: 6,
289
- height: 6,
290
- borderRadius: "50%",
291
- background: theme.green,
292
- boxShadow: `0 0 5px ${theme.green}`,
293
- }}
294
- />
295
- <span style={{ fontFamily: theme.mono, fontSize: 10 }}>
296
- {manifest?.sops.default_backend ?? "age"} key loaded
297
- </span>
298
- </div>
233
+ <div className="mt-1.5 flex items-center gap-1.5 text-go-500">
234
+ <span className="inline-block h-1.5 w-1.5 rounded-full bg-go-500 shadow-[0_0_5px_var(--color-go-500)]" />
235
+ <span className="font-mono text-[10px]">
236
+ {manifest?.sops.default_backend ?? "age"} key loaded
237
+ </span>
299
238
  </div>
300
239
  </div>
301
240
  </div>
@@ -303,15 +242,25 @@ export function Sidebar({
303
242
  }
304
243
 
305
244
  interface NavItemProps {
306
- icon: React.ReactNode;
245
+ icon: LucideIcon;
307
246
  label: string;
308
247
  active: boolean;
309
248
  onClick: () => void;
310
249
  badge?: string;
311
- badgeColor?: string;
250
+ badgeTone?: BadgeTone;
312
251
  }
313
252
 
314
- function NavItem({ icon, label, active, onClick, badge, badgeColor }: NavItemProps) {
253
+ function NavItem({ icon: Icon, label, active, onClick, badge, badgeTone = "warn" }: NavItemProps) {
254
+ // Active-state treatment is the design-review polish item: 4px gold left
255
+ // rail + gold-500/10 fill + gold-500 text + bold weight. Inactive items get
256
+ // a true hover state (bg-ink-800) so the row feels alive on cursor entry —
257
+ // pre-Phase-3 every nav item was plateau-flat.
258
+ const base =
259
+ "relative flex items-center gap-2.5 rounded-md px-2.5 py-1.5 mb-0.5 cursor-pointer transition-colors";
260
+ const stateClasses = active
261
+ ? "bg-gold-500/10 text-gold-500 font-semibold border-l-4 border-gold-500 pl-[6px]"
262
+ : "border-l-4 border-transparent text-bone hover:bg-ink-800";
263
+
315
264
  return (
316
265
  <div
317
266
  role="button"
@@ -321,57 +270,32 @@ function NavItem({ icon, label, active, onClick, badge, badgeColor }: NavItemPro
321
270
  onKeyDown={(e) => {
322
271
  if (e.key === "Enter") onClick();
323
272
  }}
324
- style={{
325
- display: "flex",
326
- alignItems: "center",
327
- gap: 9,
328
- padding: "7px 10px",
329
- borderRadius: 6,
330
- cursor: "pointer",
331
- background: active ? theme.accentDim : "transparent",
332
- border: active ? `1px solid ${theme.accent}22` : "1px solid transparent",
333
- marginBottom: 2,
334
- transition: "all 0.12s",
335
- position: "relative",
336
- }}
273
+ className={`${base} ${stateClasses}`}
337
274
  >
338
- <span
339
- style={{
340
- color: active ? theme.accent : theme.textMuted,
341
- display: "flex",
342
- alignItems: "center",
343
- width: 14,
344
- }}
345
- >
346
- {icon}
347
- </span>
348
- <span
349
- style={{
350
- fontFamily: theme.sans,
351
- fontSize: 13,
352
- fontWeight: active ? 600 : 400,
353
- color: active ? theme.accent : theme.text,
354
- flex: 1,
355
- }}
356
- >
357
- {label}
358
- </span>
359
- {badge && badgeColor && (
360
- <span
361
- style={{
362
- fontFamily: theme.mono,
363
- fontSize: 9,
364
- fontWeight: 700,
365
- color: badgeColor,
366
- background: `${badgeColor}20`,
367
- border: `1px solid ${badgeColor}44`,
368
- borderRadius: 3,
369
- padding: "1px 5px",
370
- }}
371
- >
372
- {badge}
373
- </span>
374
- )}
275
+ <Icon
276
+ size={14}
277
+ strokeWidth={1.75}
278
+ className={active ? "text-gold-500" : "text-ash"}
279
+ aria-hidden="true"
280
+ />
281
+ <span className="flex-1 font-sans text-[13px]">{label}</span>
282
+ {badge && <NavBadge tone={badgeTone}>{badge}</NavBadge>}
375
283
  </div>
376
284
  );
377
285
  }
286
+
287
+ const BADGE_TONE_CLASSES: Record<BadgeTone, string> = {
288
+ stop: "text-stop-500 bg-stop-500/15 border-stop-500/40",
289
+ warn: "text-warn-500 bg-warn-500/15 border-warn-500/40",
290
+ purple: "text-purple-400 bg-purple-400/15 border-purple-400/40",
291
+ };
292
+
293
+ function NavBadge({ tone, children }: { tone: BadgeTone; children: React.ReactNode }) {
294
+ return (
295
+ <span
296
+ className={`rounded-sm border px-1.5 py-px font-mono text-[9px] font-bold ${BADGE_TONE_CLASSES[tone]}`}
297
+ >
298
+ {children}
299
+ </span>
300
+ );
301
+ }
@@ -1,30 +1,25 @@
1
1
  import React from "react";
2
- import { theme } from "../theme";
3
2
 
4
3
  interface StatusDotProps {
5
4
  status: string;
6
5
  }
7
6
 
8
- const STATUS_COLORS: Record<string, string> = {
9
- ok: theme.green,
10
- missing_keys: theme.red,
11
- schema_warn: theme.yellow,
12
- sops_error: theme.red,
7
+ // Tailwind class sets per status. The shadow is a `shadow-[...]` arbitrary
8
+ // value because Tailwind's preset shadow scale doesn't include
9
+ // "soft glow at 53% opacity"; that's the canonical halo this component shows.
10
+ const STATUS_CLASSES: Record<string, string> = {
11
+ ok: "bg-go-500 shadow-[0_0_6px_rgb(52_211_153_/_0.53)]",
12
+ missing_keys: "bg-stop-500 shadow-[0_0_6px_rgb(248_113_113_/_0.53)]",
13
+ schema_warn: "bg-warn-500 shadow-[0_0_6px_rgb(251_191_36_/_0.53)]",
14
+ sops_error: "bg-stop-500 shadow-[0_0_6px_rgb(248_113_113_/_0.53)]",
13
15
  };
14
16
 
15
17
  export function StatusDot({ status }: StatusDotProps) {
16
- const color = STATUS_COLORS[status] ?? theme.textMuted;
18
+ const tone = STATUS_CLASSES[status] ?? "bg-ash shadow-[0_0_6px_rgb(155_163_183_/_0.53)]";
17
19
  return (
18
20
  <span
19
21
  data-testid="status-dot"
20
- style={{
21
- display: "inline-block",
22
- width: 7,
23
- height: 7,
24
- borderRadius: "50%",
25
- background: color,
26
- boxShadow: `0 0 6px ${color}88`,
27
- }}
22
+ className={`inline-block h-[7px] w-[7px] rounded-full ${tone}`}
28
23
  />
29
24
  );
30
25
  }
@@ -1,5 +1,4 @@
1
1
  import React, { useState, useEffect } from "react";
2
- import { theme } from "../theme";
3
2
  import { apiFetch } from "../api";
4
3
  import { Button } from "./Button";
5
4
  import { EnvBadge } from "./EnvBadge";
@@ -95,90 +94,48 @@ export function SyncPanel({ namespace, onComplete, onCancel }: SyncPanelProps) {
95
94
  return (
96
95
  <div
97
96
  data-testid="sync-panel"
98
- style={{
99
- background: theme.surface,
100
- border: `1px solid ${theme.border}`,
101
- borderRadius: 8,
102
- padding: "16px 20px",
103
- marginTop: 8,
104
- marginBottom: 8,
105
- }}
97
+ className="my-2 rounded-lg border border-edge bg-ink-850 px-5 py-4"
106
98
  >
107
99
  {phase === "loading" && (
108
- <div style={{ fontFamily: theme.sans, fontSize: 13, color: theme.textMuted }}>
109
- Loading sync preview...
110
- </div>
100
+ <div className="font-sans text-[13px] text-ash">Loading sync preview...</div>
111
101
  )}
112
102
 
113
103
  {phase === "preview" && plan && (
114
104
  <>
115
105
  {plan.totalKeys === 0 ? (
116
- <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
117
- <span
118
- data-testid="sync-in-sync"
119
- style={{ fontFamily: theme.sans, fontSize: 13, color: theme.green }}
120
- >
106
+ <div className="flex items-center gap-2.5">
107
+ <span data-testid="sync-in-sync" className="font-sans text-[13px] text-go-500">
121
108
  All environments in sync
122
109
  </span>
123
110
  <Button onClick={onCancel}>Close</Button>
124
111
  </div>
125
112
  ) : (
126
113
  <>
127
- <div
128
- style={{
129
- fontFamily: theme.sans,
130
- fontSize: 13,
131
- fontWeight: 600,
132
- color: theme.text,
133
- marginBottom: 10,
134
- }}
135
- >
114
+ <div className="mb-2.5 font-sans text-[13px] font-semibold text-bone">
136
115
  Sync {namespace} — {plan.totalKeys} key{plan.totalKeys !== 1 ? "s" : ""} to scaffold
137
116
  </div>
138
117
 
139
118
  {plan.hasProtectedEnvs && (
140
- <div
141
- style={{
142
- fontFamily: theme.sans,
143
- fontSize: 12,
144
- color: theme.yellow,
145
- background: theme.yellowDim,
146
- border: `1px solid ${theme.yellow}33`,
147
- borderRadius: 5,
148
- padding: "6px 12px",
149
- marginBottom: 10,
150
- }}
151
- >
119
+ <div className="mb-2.5 rounded border border-warn-500/20 bg-warn-500/10 px-3 py-1.5 font-sans text-[12px] text-warn-500">
152
120
  Includes protected environment(s)
153
121
  </div>
154
122
  )}
155
123
 
156
- <div data-testid="sync-preview-list" style={{ marginBottom: 12 }}>
124
+ <div data-testid="sync-preview-list" className="mb-3">
157
125
  {plan.cells.map((cell) => (
158
126
  <div
159
127
  key={`${cell.namespace}/${cell.environment}`}
160
- style={{
161
- display: "flex",
162
- alignItems: "center",
163
- gap: 8,
164
- padding: "4px 0",
165
- }}
128
+ className="flex items-center gap-2 py-1"
166
129
  >
167
130
  <EnvBadge env={cell.environment} />
168
- <span
169
- style={{
170
- fontFamily: theme.mono,
171
- fontSize: 12,
172
- color: theme.textMuted,
173
- }}
174
- >
131
+ <span className="font-mono text-[12px] text-ash">
175
132
  {cell.missingKeys.join(", ")}
176
133
  </span>
177
134
  </div>
178
135
  ))}
179
136
  </div>
180
137
 
181
- <div style={{ display: "flex", gap: 8 }}>
138
+ <div className="flex gap-2">
182
139
  <Button variant="primary" data-testid="sync-execute-btn" onClick={handleSync}>
183
140
  Sync Now
184
141
  </Button>
@@ -191,15 +148,10 @@ export function SyncPanel({ namespace, onComplete, onCancel }: SyncPanelProps) {
191
148
  </>
192
149
  )}
193
150
 
194
- {phase === "syncing" && (
195
- <div style={{ fontFamily: theme.sans, fontSize: 13, color: theme.accent }}>Syncing...</div>
196
- )}
151
+ {phase === "syncing" && <div className="font-sans text-[13px] text-gold-500">Syncing...</div>}
197
152
 
198
153
  {phase === "done" && result && (
199
- <div
200
- data-testid="sync-done"
201
- style={{ fontFamily: theme.sans, fontSize: 13, color: theme.green }}
202
- >
154
+ <div data-testid="sync-done" className="font-sans text-[13px] text-go-500">
203
155
  Synced {result.totalKeysScaffolded} key{result.totalKeysScaffolded !== 1 ? "s" : ""}{" "}
204
156
  across {result.modifiedCells.length} environment
205
157
  {result.modifiedCells.length !== 1 ? "s" : ""}
@@ -207,8 +159,8 @@ export function SyncPanel({ namespace, onComplete, onCancel }: SyncPanelProps) {
207
159
  )}
208
160
 
209
161
  {phase === "error" && (
210
- <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
211
- <span style={{ fontFamily: theme.sans, fontSize: 13, color: theme.red }}>{error}</span>
162
+ <div className="flex items-center gap-2.5">
163
+ <span className="font-sans text-[13px] text-stop-500">{error}</span>
212
164
  <Button onClick={onCancel}>Close</Button>
213
165
  </div>
214
166
  )}