@clef-sh/ui 0.1.20 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/index-DPWHjBbB.js +34 -0
- package/dist/client/assets/index-qsLTYpc9.css +2 -0
- package/dist/client/clef.svg +2 -0
- package/dist/client/index.html +3 -31
- package/dist/client-lib/components/Button.d.ts +1 -1
- package/dist/client-lib/components/Button.d.ts.map +1 -1
- package/dist/client-lib/components/CopyButton.d.ts.map +1 -1
- package/dist/client-lib/components/EnvBadge.d.ts.map +1 -1
- package/dist/client-lib/components/MatrixGrid.d.ts.map +1 -1
- package/dist/client-lib/components/Sidebar.d.ts +1 -1
- package/dist/client-lib/components/Sidebar.d.ts.map +1 -1
- package/dist/client-lib/components/StatusDot.d.ts.map +1 -1
- package/dist/client-lib/components/SyncPanel.d.ts.map +1 -1
- package/dist/client-lib/components/TopBar.d.ts +6 -0
- package/dist/client-lib/components/TopBar.d.ts.map +1 -1
- package/dist/client-lib/primitives/Badge.d.ts +11 -0
- package/dist/client-lib/primitives/Badge.d.ts.map +1 -0
- package/dist/client-lib/primitives/Card.d.ts +28 -0
- package/dist/client-lib/primitives/Card.d.ts.map +1 -0
- package/dist/client-lib/primitives/Dialog.d.ts +30 -0
- package/dist/client-lib/primitives/Dialog.d.ts.map +1 -0
- package/dist/client-lib/primitives/EmptyState.d.ts +10 -0
- package/dist/client-lib/primitives/EmptyState.d.ts.map +1 -0
- package/dist/client-lib/primitives/Field.d.ts +36 -0
- package/dist/client-lib/primitives/Field.d.ts.map +1 -0
- package/dist/client-lib/primitives/Input.d.ts +6 -0
- package/dist/client-lib/primitives/Input.d.ts.map +1 -0
- package/dist/client-lib/primitives/Stat.d.ts +11 -0
- package/dist/client-lib/primitives/Stat.d.ts.map +1 -0
- package/dist/client-lib/primitives/Table.d.ts +37 -0
- package/dist/client-lib/primitives/Table.d.ts.map +1 -0
- package/dist/client-lib/primitives/Tabs.d.ts +29 -0
- package/dist/client-lib/primitives/Tabs.d.ts.map +1 -0
- package/dist/client-lib/primitives/Toast.d.ts +16 -0
- package/dist/client-lib/primitives/Toast.d.ts.map +1 -0
- package/dist/client-lib/primitives/Toolbar.d.ts +29 -0
- package/dist/client-lib/primitives/Toolbar.d.ts.map +1 -0
- package/dist/client-lib/primitives/index.d.ts +23 -0
- package/dist/client-lib/primitives/index.d.ts.map +1 -0
- package/dist/client-lib/theme.d.ts +18 -41
- package/dist/client-lib/theme.d.ts.map +1 -1
- package/dist/server/api.d.ts.map +1 -1
- package/dist/server/api.js +215 -0
- package/dist/server/api.js.map +1 -1
- package/dist/server/envelope.d.ts +15 -0
- package/dist/server/envelope.d.ts.map +1 -0
- package/dist/server/envelope.js +310 -0
- package/dist/server/envelope.js.map +1 -0
- package/package.json +7 -2
- package/src/client/App.tsx +16 -41
- package/src/client/components/Button.tsx +13 -22
- package/src/client/components/CopyButton.tsx +5 -12
- package/src/client/components/EnvBadge.tsx +30 -15
- package/src/client/components/MatrixGrid.tsx +108 -252
- package/src/client/components/Sidebar.tsx +123 -199
- package/src/client/components/StatusDot.tsx +10 -15
- package/src/client/components/SyncPanel.tsx +14 -62
- package/src/client/components/TopBar.tsx +11 -36
- package/src/client/index.html +1 -30
- package/src/client/main.tsx +1 -0
- package/src/client/primitives/Badge.test.tsx +47 -0
- package/src/client/primitives/Badge.tsx +64 -0
- package/src/client/primitives/Card.test.tsx +50 -0
- package/src/client/primitives/Card.tsx +85 -0
- package/src/client/primitives/Dialog.test.tsx +55 -0
- package/src/client/primitives/Dialog.tsx +96 -0
- package/src/client/primitives/EmptyState.test.tsx +25 -0
- package/src/client/primitives/EmptyState.tsx +38 -0
- package/src/client/primitives/Field.test.tsx +46 -0
- package/src/client/primitives/Field.tsx +95 -0
- package/src/client/primitives/Input.tsx +26 -0
- package/src/client/primitives/Stat.test.tsx +32 -0
- package/src/client/primitives/Stat.tsx +52 -0
- package/src/client/primitives/Table.test.tsx +58 -0
- package/src/client/primitives/Table.tsx +113 -0
- package/src/client/primitives/Tabs.test.tsx +44 -0
- package/src/client/primitives/Tabs.tsx +100 -0
- package/src/client/primitives/Toast.test.tsx +77 -0
- package/src/client/primitives/Toast.tsx +89 -0
- package/src/client/primitives/Toolbar.test.tsx +50 -0
- package/src/client/primitives/Toolbar.tsx +86 -0
- package/src/client/primitives/index.ts +43 -0
- package/src/client/public/clef.svg +2 -0
- package/src/client/screens/BackendScreen.tsx +104 -363
- package/src/client/screens/DiffView.tsx +187 -378
- package/src/client/screens/EnvelopeScreen.test.tsx +542 -0
- package/src/client/screens/EnvelopeScreen.tsx +948 -0
- package/src/client/screens/GitLogView.tsx +48 -106
- package/src/client/screens/ImportScreen.tsx +105 -308
- package/src/client/screens/LintView.tsx +184 -379
- package/src/client/screens/ManifestScreen.tsx +283 -445
- package/src/client/screens/MatrixView.tsx +75 -91
- package/src/client/screens/NamespaceEditor.tsx +234 -609
- package/src/client/screens/PolicyView.tsx +183 -453
- package/src/client/screens/RecipientsScreen.tsx +71 -350
- package/src/client/screens/ResetScreen.tsx +67 -237
- package/src/client/screens/ScanScreen.tsx +85 -249
- package/src/client/screens/SchemaEditor.test.tsx +237 -0
- package/src/client/screens/SchemaEditor.tsx +435 -0
- package/src/client/screens/ServiceIdentitiesScreen.tsx +251 -788
- package/src/client/styles.css +77 -0
- package/src/client/theme.ts +27 -48
- package/dist/client/assets/index-Db6WgHgY.js +0 -38
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{/*
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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={
|
|
106
|
+
icon={LayoutGrid}
|
|
133
107
|
label="Matrix"
|
|
134
108
|
active={activeView === "matrix"}
|
|
135
109
|
onClick={() => setView("matrix")}
|
|
136
110
|
/>
|
|
137
111
|
<NavItem
|
|
138
|
-
icon={
|
|
112
|
+
icon={ArrowLeftRight}
|
|
139
113
|
label="Diff"
|
|
140
114
|
active={activeView === "diff"}
|
|
141
115
|
onClick={() => setView("diff")}
|
|
142
116
|
/>
|
|
143
117
|
<NavItem
|
|
144
|
-
icon={
|
|
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
|
-
|
|
123
|
+
badgeTone="stop"
|
|
150
124
|
/>
|
|
151
125
|
<NavItem
|
|
152
|
-
icon={
|
|
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
|
-
|
|
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={
|
|
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
|
-
|
|
145
|
+
badgeTone="stop"
|
|
166
146
|
/>
|
|
167
147
|
<NavItem
|
|
168
|
-
icon={
|
|
148
|
+
icon={Upload}
|
|
169
149
|
label="Import"
|
|
170
150
|
active={activeView === "import"}
|
|
171
151
|
onClick={() => setView("import")}
|
|
172
152
|
/>
|
|
173
153
|
<NavItem
|
|
174
|
-
icon={
|
|
154
|
+
icon={Users}
|
|
175
155
|
label="Recipients"
|
|
176
156
|
active={activeView === "recipients"}
|
|
177
157
|
onClick={() => setView("recipients")}
|
|
178
158
|
/>
|
|
179
159
|
<NavItem
|
|
180
|
-
icon={
|
|
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
|
-
|
|
169
|
+
badgeTone="purple"
|
|
190
170
|
/>
|
|
191
171
|
<NavItem
|
|
192
|
-
icon={
|
|
172
|
+
icon={RefreshCw}
|
|
193
173
|
label="Backend"
|
|
194
174
|
active={activeView === "backend"}
|
|
195
175
|
onClick={() => setView("backend")}
|
|
196
176
|
/>
|
|
197
177
|
<NavItem
|
|
198
|
-
icon={
|
|
178
|
+
icon={RotateCcw}
|
|
199
179
|
label="Reset"
|
|
200
180
|
active={activeView === "reset"}
|
|
201
181
|
onClick={() => setView("reset")}
|
|
202
182
|
/>
|
|
203
183
|
<NavItem
|
|
204
|
-
icon={
|
|
184
|
+
icon={FileText}
|
|
205
185
|
label="Manifest"
|
|
206
186
|
active={activeView === "manifest"}
|
|
207
187
|
onClick={() => setView("manifest")}
|
|
208
188
|
/>
|
|
209
189
|
<NavItem
|
|
210
|
-
icon={
|
|
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
|
|
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
|
-
|
|
223
|
+
badgeTone="warn"
|
|
257
224
|
/>
|
|
258
225
|
);
|
|
259
226
|
})}
|
|
260
227
|
</div>
|
|
261
228
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
277
|
-
<
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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:
|
|
245
|
+
icon: LucideIcon;
|
|
307
246
|
label: string;
|
|
308
247
|
active: boolean;
|
|
309
248
|
onClick: () => void;
|
|
310
249
|
badge?: string;
|
|
311
|
-
|
|
250
|
+
badgeTone?: BadgeTone;
|
|
312
251
|
}
|
|
313
252
|
|
|
314
|
-
function NavItem({ icon, label, active, onClick, badge,
|
|
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
|
-
|
|
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
|
-
<
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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"
|
|
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
|
-
|
|
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
|
|
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
|
|
211
|
-
<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
|
)}
|