create-ekka-desktop-app 0.2.2
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/README.md +137 -0
- package/bin/cli.js +72 -0
- package/package.json +23 -0
- package/template/branding/app.json +6 -0
- package/template/branding/icon.icns +0 -0
- package/template/eslint.config.js +98 -0
- package/template/index.html +29 -0
- package/template/package.json +40 -0
- package/template/src/app/App.tsx +24 -0
- package/template/src/demo/DemoApp.tsx +260 -0
- package/template/src/demo/components/Banner.tsx +82 -0
- package/template/src/demo/components/EmptyState.tsx +61 -0
- package/template/src/demo/components/InfoPopover.tsx +171 -0
- package/template/src/demo/components/InfoTooltip.tsx +76 -0
- package/template/src/demo/components/LearnMore.tsx +98 -0
- package/template/src/demo/components/NodeCredentialsOnboarding.tsx +219 -0
- package/template/src/demo/components/SetupWizard.tsx +48 -0
- package/template/src/demo/components/StatusBadge.tsx +83 -0
- package/template/src/demo/components/index.ts +10 -0
- package/template/src/demo/hooks/index.ts +6 -0
- package/template/src/demo/hooks/useAuditEvents.ts +30 -0
- package/template/src/demo/layout/Shell.tsx +110 -0
- package/template/src/demo/layout/Sidebar.tsx +192 -0
- package/template/src/demo/pages/AuditLogPage.tsx +235 -0
- package/template/src/demo/pages/DocGenPage.tsx +874 -0
- package/template/src/demo/pages/HomeSetupPage.tsx +182 -0
- package/template/src/demo/pages/LoginPage.tsx +192 -0
- package/template/src/demo/pages/PathPermissionsPage.tsx +873 -0
- package/template/src/demo/pages/RunnerPage.tsx +445 -0
- package/template/src/demo/pages/SystemPage.tsx +557 -0
- package/template/src/demo/pages/VaultPage.tsx +805 -0
- package/template/src/ekka/__tests__/demo-backend.test.ts +187 -0
- package/template/src/ekka/audit/index.ts +7 -0
- package/template/src/ekka/audit/store.ts +68 -0
- package/template/src/ekka/audit/types.ts +22 -0
- package/template/src/ekka/auth/client.ts +212 -0
- package/template/src/ekka/auth/index.ts +30 -0
- package/template/src/ekka/auth/storage.ts +114 -0
- package/template/src/ekka/auth/types.ts +67 -0
- package/template/src/ekka/backend/demo.ts +151 -0
- package/template/src/ekka/backend/interface.ts +36 -0
- package/template/src/ekka/config.ts +48 -0
- package/template/src/ekka/constants.ts +143 -0
- package/template/src/ekka/errors.ts +54 -0
- package/template/src/ekka/index.ts +516 -0
- package/template/src/ekka/internal/backend.ts +156 -0
- package/template/src/ekka/internal/index.ts +7 -0
- package/template/src/ekka/ops/auth.ts +29 -0
- package/template/src/ekka/ops/debug.ts +68 -0
- package/template/src/ekka/ops/home.ts +101 -0
- package/template/src/ekka/ops/index.ts +16 -0
- package/template/src/ekka/ops/nodeCredentials.ts +131 -0
- package/template/src/ekka/ops/nodeSession.ts +145 -0
- package/template/src/ekka/ops/paths.ts +183 -0
- package/template/src/ekka/ops/runner.ts +86 -0
- package/template/src/ekka/ops/runtime.ts +31 -0
- package/template/src/ekka/ops/setup.ts +47 -0
- package/template/src/ekka/ops/vault.ts +459 -0
- package/template/src/ekka/ops/workflowRuns.ts +116 -0
- package/template/src/ekka/types.ts +82 -0
- package/template/src/ekka/utils/idempotency.ts +14 -0
- package/template/src/ekka/utils/index.ts +7 -0
- package/template/src/ekka/utils/time.ts +77 -0
- package/template/src/main.tsx +12 -0
- package/template/src/vite-env.d.ts +12 -0
- package/template/src-tauri/Cargo.toml +41 -0
- package/template/src-tauri/build.rs +3 -0
- package/template/src-tauri/capabilities/default.json +11 -0
- package/template/src-tauri/icons/icon.icns +0 -0
- package/template/src-tauri/icons/icon.png +0 -0
- package/template/src-tauri/resources/ekka-engine-bootstrap +0 -0
- package/template/src-tauri/src/bootstrap.rs +37 -0
- package/template/src-tauri/src/commands.rs +1215 -0
- package/template/src-tauri/src/device_secret.rs +111 -0
- package/template/src-tauri/src/engine_process.rs +538 -0
- package/template/src-tauri/src/grants.rs +129 -0
- package/template/src-tauri/src/handlers/home.rs +65 -0
- package/template/src-tauri/src/handlers/mod.rs +7 -0
- package/template/src-tauri/src/handlers/paths.rs +128 -0
- package/template/src-tauri/src/handlers/vault.rs +680 -0
- package/template/src-tauri/src/main.rs +243 -0
- package/template/src-tauri/src/node_auth.rs +858 -0
- package/template/src-tauri/src/node_credentials.rs +541 -0
- package/template/src-tauri/src/node_runner.rs +882 -0
- package/template/src-tauri/src/node_vault_crypto.rs +113 -0
- package/template/src-tauri/src/node_vault_store.rs +267 -0
- package/template/src-tauri/src/ops/auth.rs +50 -0
- package/template/src-tauri/src/ops/home.rs +251 -0
- package/template/src-tauri/src/ops/mod.rs +7 -0
- package/template/src-tauri/src/ops/runtime.rs +21 -0
- package/template/src-tauri/src/state.rs +639 -0
- package/template/src-tauri/src/types.rs +84 -0
- package/template/src-tauri/tauri.conf.json +41 -0
- package/template/tsconfig.json +26 -0
- package/template/tsconfig.tsbuildinfo +1 -0
- package/template/vite.config.ts +34 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Learn More Component
|
|
3
|
+
* Expandable section for additional information.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, type CSSProperties, type ReactElement, type ReactNode } from 'react';
|
|
7
|
+
|
|
8
|
+
interface LearnMoreProps {
|
|
9
|
+
title: string;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
defaultExpanded?: boolean;
|
|
12
|
+
darkMode?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function LearnMore({
|
|
16
|
+
title,
|
|
17
|
+
children,
|
|
18
|
+
defaultExpanded = false,
|
|
19
|
+
darkMode = false,
|
|
20
|
+
}: LearnMoreProps): ReactElement {
|
|
21
|
+
const [expanded, setExpanded] = useState(defaultExpanded);
|
|
22
|
+
|
|
23
|
+
const colors = {
|
|
24
|
+
text: darkMode ? '#ffffff' : '#1d1d1f',
|
|
25
|
+
textMuted: darkMode ? '#98989d' : '#6e6e73',
|
|
26
|
+
border: darkMode ? '#3a3a3c' : '#e5e5e5',
|
|
27
|
+
bg: darkMode ? 'rgba(255, 255, 255, 0.03)' : 'rgba(0, 0, 0, 0.02)',
|
|
28
|
+
hover: darkMode ? 'rgba(255, 255, 255, 0.06)' : 'rgba(0, 0, 0, 0.04)',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const styles: Record<string, CSSProperties> = {
|
|
32
|
+
container: {
|
|
33
|
+
border: `1px solid ${colors.border}`,
|
|
34
|
+
borderRadius: '6px',
|
|
35
|
+
overflow: 'hidden',
|
|
36
|
+
},
|
|
37
|
+
header: {
|
|
38
|
+
display: 'flex',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
justifyContent: 'space-between',
|
|
41
|
+
padding: '10px 14px',
|
|
42
|
+
background: colors.bg,
|
|
43
|
+
border: 'none',
|
|
44
|
+
borderRadius: 0,
|
|
45
|
+
width: '100%',
|
|
46
|
+
cursor: 'pointer',
|
|
47
|
+
transition: 'background 0.15s ease',
|
|
48
|
+
},
|
|
49
|
+
title: {
|
|
50
|
+
fontSize: '13px',
|
|
51
|
+
fontWeight: 500,
|
|
52
|
+
color: colors.text,
|
|
53
|
+
margin: 0,
|
|
54
|
+
},
|
|
55
|
+
icon: {
|
|
56
|
+
width: '14px',
|
|
57
|
+
height: '14px',
|
|
58
|
+
color: colors.textMuted,
|
|
59
|
+
transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)',
|
|
60
|
+
transition: 'transform 0.2s ease',
|
|
61
|
+
},
|
|
62
|
+
content: {
|
|
63
|
+
padding: '14px',
|
|
64
|
+
fontSize: '13px',
|
|
65
|
+
lineHeight: 1.6,
|
|
66
|
+
color: colors.textMuted,
|
|
67
|
+
borderTop: `1px solid ${colors.border}`,
|
|
68
|
+
display: expanded ? 'block' : 'none',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div style={styles.container}>
|
|
74
|
+
<button
|
|
75
|
+
style={styles.header}
|
|
76
|
+
onClick={() => setExpanded(!expanded)}
|
|
77
|
+
onMouseEnter={(e) => {
|
|
78
|
+
e.currentTarget.style.background = colors.hover;
|
|
79
|
+
}}
|
|
80
|
+
onMouseLeave={(e) => {
|
|
81
|
+
e.currentTarget.style.background = colors.bg;
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<span style={styles.title}>{title}</span>
|
|
85
|
+
<svg style={styles.icon} viewBox="0 0 16 16" fill="none">
|
|
86
|
+
<path
|
|
87
|
+
d="M4 6l4 4 4-4"
|
|
88
|
+
stroke="currentColor"
|
|
89
|
+
strokeWidth="1.5"
|
|
90
|
+
strokeLinecap="round"
|
|
91
|
+
strokeLinejoin="round"
|
|
92
|
+
/>
|
|
93
|
+
</svg>
|
|
94
|
+
</button>
|
|
95
|
+
<div style={styles.content}>{children}</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node Credentials Onboarding Component
|
|
3
|
+
*
|
|
4
|
+
* Minimal UI for one-time node_id + node_secret input.
|
|
5
|
+
* Stores credentials securely in OS keychain.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useCallback } from 'react';
|
|
9
|
+
import { ekka } from '../../ekka';
|
|
10
|
+
|
|
11
|
+
interface NodeCredentialsOnboardingProps {
|
|
12
|
+
/** Called when credentials are successfully saved */
|
|
13
|
+
onComplete?: () => void;
|
|
14
|
+
/** If true, renders without outer container (for embedding in wizard) */
|
|
15
|
+
embedded?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function NodeCredentialsOnboarding({
|
|
19
|
+
onComplete,
|
|
20
|
+
embedded = false,
|
|
21
|
+
}: NodeCredentialsOnboardingProps) {
|
|
22
|
+
const [nodeId, setNodeId] = useState('');
|
|
23
|
+
const [nodeSecret, setNodeSecret] = useState('');
|
|
24
|
+
const [error, setError] = useState<string | null>(null);
|
|
25
|
+
const [loading, setLoading] = useState(false);
|
|
26
|
+
const [success, setSuccess] = useState(false);
|
|
27
|
+
|
|
28
|
+
// Validation states
|
|
29
|
+
const nodeIdValid = ekka.nodeCredentials.isValidNodeId(nodeId);
|
|
30
|
+
const nodeSecretValid = ekka.nodeCredentials.isValidNodeSecret(nodeSecret);
|
|
31
|
+
const canSubmit = nodeIdValid && nodeSecretValid && !loading;
|
|
32
|
+
|
|
33
|
+
const handleSubmit = useCallback(
|
|
34
|
+
async (e: React.FormEvent) => {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
setError(null);
|
|
37
|
+
setLoading(true);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await ekka.nodeCredentials.set(nodeId, nodeSecret);
|
|
41
|
+
setSuccess(true);
|
|
42
|
+
onComplete?.();
|
|
43
|
+
} catch (err) {
|
|
44
|
+
setError(err instanceof Error ? err.message : 'Failed to save credentials');
|
|
45
|
+
} finally {
|
|
46
|
+
setLoading(false);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
[nodeId, nodeSecret, onComplete]
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const successContent = (
|
|
53
|
+
<div style={styles.card}>
|
|
54
|
+
<div style={styles.successIcon}>✓</div>
|
|
55
|
+
<h2 style={styles.title}>Node Configured</h2>
|
|
56
|
+
<p style={styles.text}>
|
|
57
|
+
Your node credentials have been saved securely.
|
|
58
|
+
The engine will use them automatically on startup.
|
|
59
|
+
</p>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (success) {
|
|
64
|
+
return embedded ? successContent : <div style={styles.container}>{successContent}</div>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const formContent = (
|
|
68
|
+
<div style={styles.card}>
|
|
69
|
+
<h2 style={styles.title}>Configure Node Identity</h2>
|
|
70
|
+
<p style={styles.text}>
|
|
71
|
+
Enter your node credentials to enable headless engine startup.
|
|
72
|
+
These will be stored securely in your system keychain.
|
|
73
|
+
</p>
|
|
74
|
+
|
|
75
|
+
<form onSubmit={handleSubmit} style={styles.form}>
|
|
76
|
+
<div style={styles.field}>
|
|
77
|
+
<label style={styles.label}>Node ID</label>
|
|
78
|
+
<input
|
|
79
|
+
type="text"
|
|
80
|
+
value={nodeId}
|
|
81
|
+
onChange={(e) => setNodeId(e.target.value.trim())}
|
|
82
|
+
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
83
|
+
style={{
|
|
84
|
+
...styles.input,
|
|
85
|
+
borderColor: nodeId && !nodeIdValid ? '#ef4444' : '#d1d5db',
|
|
86
|
+
}}
|
|
87
|
+
disabled={loading}
|
|
88
|
+
/>
|
|
89
|
+
{nodeId && !nodeIdValid && (
|
|
90
|
+
<span style={styles.error}>Must be a valid UUID</span>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div style={styles.field}>
|
|
95
|
+
<label style={styles.label}>Node Secret</label>
|
|
96
|
+
<input
|
|
97
|
+
type="password"
|
|
98
|
+
value={nodeSecret}
|
|
99
|
+
onChange={(e) => setNodeSecret(e.target.value)}
|
|
100
|
+
placeholder="Enter node secret"
|
|
101
|
+
style={{
|
|
102
|
+
...styles.input,
|
|
103
|
+
borderColor: nodeSecret && !nodeSecretValid ? '#ef4444' : '#d1d5db',
|
|
104
|
+
}}
|
|
105
|
+
disabled={loading}
|
|
106
|
+
/>
|
|
107
|
+
{nodeSecret && !nodeSecretValid && (
|
|
108
|
+
<span style={styles.error}>Must be at least 16 characters</span>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{error && <div style={styles.errorBox}>{error}</div>}
|
|
113
|
+
|
|
114
|
+
<button
|
|
115
|
+
type="submit"
|
|
116
|
+
disabled={!canSubmit}
|
|
117
|
+
style={{
|
|
118
|
+
...styles.button,
|
|
119
|
+
opacity: canSubmit ? 1 : 0.5,
|
|
120
|
+
cursor: canSubmit ? 'pointer' : 'not-allowed',
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
{loading ? 'Saving...' : 'Save Credentials'}
|
|
124
|
+
</button>
|
|
125
|
+
</form>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return embedded ? formContent : <div style={styles.container}>{formContent}</div>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
133
|
+
container: {
|
|
134
|
+
display: 'flex',
|
|
135
|
+
justifyContent: 'center',
|
|
136
|
+
alignItems: 'center',
|
|
137
|
+
minHeight: '100vh',
|
|
138
|
+
padding: '1rem',
|
|
139
|
+
backgroundColor: '#f3f4f6',
|
|
140
|
+
},
|
|
141
|
+
card: {
|
|
142
|
+
backgroundColor: '#ffffff',
|
|
143
|
+
borderRadius: '0.5rem',
|
|
144
|
+
padding: '2rem',
|
|
145
|
+
maxWidth: '400px',
|
|
146
|
+
width: '100%',
|
|
147
|
+
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
|
148
|
+
},
|
|
149
|
+
title: {
|
|
150
|
+
margin: '0 0 0.5rem 0',
|
|
151
|
+
fontSize: '1.25rem',
|
|
152
|
+
fontWeight: 600,
|
|
153
|
+
color: '#111827',
|
|
154
|
+
},
|
|
155
|
+
text: {
|
|
156
|
+
margin: '0 0 1.5rem 0',
|
|
157
|
+
fontSize: '0.875rem',
|
|
158
|
+
color: '#6b7280',
|
|
159
|
+
lineHeight: 1.5,
|
|
160
|
+
},
|
|
161
|
+
form: {
|
|
162
|
+
display: 'flex',
|
|
163
|
+
flexDirection: 'column',
|
|
164
|
+
gap: '1rem',
|
|
165
|
+
},
|
|
166
|
+
field: {
|
|
167
|
+
display: 'flex',
|
|
168
|
+
flexDirection: 'column',
|
|
169
|
+
gap: '0.25rem',
|
|
170
|
+
},
|
|
171
|
+
label: {
|
|
172
|
+
fontSize: '0.875rem',
|
|
173
|
+
fontWeight: 500,
|
|
174
|
+
color: '#374151',
|
|
175
|
+
},
|
|
176
|
+
input: {
|
|
177
|
+
padding: '0.5rem 0.75rem',
|
|
178
|
+
fontSize: '0.875rem',
|
|
179
|
+
border: '1px solid #d1d5db',
|
|
180
|
+
borderRadius: '0.375rem',
|
|
181
|
+
outline: 'none',
|
|
182
|
+
fontFamily: 'monospace',
|
|
183
|
+
},
|
|
184
|
+
error: {
|
|
185
|
+
fontSize: '0.75rem',
|
|
186
|
+
color: '#ef4444',
|
|
187
|
+
},
|
|
188
|
+
errorBox: {
|
|
189
|
+
padding: '0.75rem',
|
|
190
|
+
backgroundColor: '#fef2f2',
|
|
191
|
+
border: '1px solid #fecaca',
|
|
192
|
+
borderRadius: '0.375rem',
|
|
193
|
+
fontSize: '0.875rem',
|
|
194
|
+
color: '#b91c1c',
|
|
195
|
+
},
|
|
196
|
+
button: {
|
|
197
|
+
padding: '0.625rem 1rem',
|
|
198
|
+
fontSize: '0.875rem',
|
|
199
|
+
fontWeight: 500,
|
|
200
|
+
color: '#ffffff',
|
|
201
|
+
backgroundColor: '#3b82f6',
|
|
202
|
+
border: 'none',
|
|
203
|
+
borderRadius: '0.375rem',
|
|
204
|
+
marginTop: '0.5rem',
|
|
205
|
+
},
|
|
206
|
+
successIcon: {
|
|
207
|
+
width: '3rem',
|
|
208
|
+
height: '3rem',
|
|
209
|
+
lineHeight: '3rem',
|
|
210
|
+
textAlign: 'center',
|
|
211
|
+
fontSize: '1.5rem',
|
|
212
|
+
color: '#ffffff',
|
|
213
|
+
backgroundColor: '#10b981',
|
|
214
|
+
borderRadius: '50%',
|
|
215
|
+
margin: '0 auto 1rem auto',
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export default NodeCredentialsOnboarding;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-Login Setup Wizard
|
|
3
|
+
*
|
|
4
|
+
* Single-step setup flow: Connect Node (node_id + node_secret)
|
|
5
|
+
*
|
|
6
|
+
* This runs BEFORE login - no user auth required.
|
|
7
|
+
* Home folder grant is handled POST-login via HomeSetupPage.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useCallback } from 'react';
|
|
11
|
+
import type { SetupStatus } from '../../ekka';
|
|
12
|
+
import { NodeCredentialsOnboarding } from './NodeCredentialsOnboarding';
|
|
13
|
+
|
|
14
|
+
interface SetupWizardProps {
|
|
15
|
+
/** Initial setup status */
|
|
16
|
+
initialStatus: SetupStatus;
|
|
17
|
+
/** Called when setup is complete */
|
|
18
|
+
onComplete: () => void;
|
|
19
|
+
/** Dark mode */
|
|
20
|
+
darkMode?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function SetupWizard({
|
|
24
|
+
onComplete,
|
|
25
|
+
darkMode = false,
|
|
26
|
+
}: SetupWizardProps) {
|
|
27
|
+
// Handle node credentials complete
|
|
28
|
+
const handleNodeComplete = useCallback(() => {
|
|
29
|
+
onComplete();
|
|
30
|
+
}, [onComplete]);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div style={{
|
|
34
|
+
minHeight: '100vh',
|
|
35
|
+
backgroundColor: darkMode ? '#1c1c1e' : '#f5f5f7',
|
|
36
|
+
display: 'flex',
|
|
37
|
+
flexDirection: 'column',
|
|
38
|
+
alignItems: 'center',
|
|
39
|
+
justifyContent: 'center',
|
|
40
|
+
padding: '2rem',
|
|
41
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif',
|
|
42
|
+
}}>
|
|
43
|
+
<NodeCredentialsOnboarding onComplete={handleNodeComplete} embedded />
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default SetupWizard;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status Badge Component
|
|
3
|
+
* Visual status indicator with color coding.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { type CSSProperties, type ReactElement } from 'react';
|
|
7
|
+
|
|
8
|
+
interface StatusBadgeProps {
|
|
9
|
+
status: string;
|
|
10
|
+
darkMode?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type StatusColor = 'green' | 'amber' | 'red' | 'blue' | 'gray';
|
|
14
|
+
|
|
15
|
+
function getStatusColor(status: string): StatusColor {
|
|
16
|
+
const normalized = status.toLowerCase();
|
|
17
|
+
|
|
18
|
+
// Success states
|
|
19
|
+
if (['success', 'completed', 'active', 'connected', 'online'].includes(normalized)) {
|
|
20
|
+
return 'green';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Warning states
|
|
24
|
+
if (['warning', 'pending', 'processing', 'running'].includes(normalized)) {
|
|
25
|
+
return 'amber';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Error states
|
|
29
|
+
if (['error', 'failed', 'disconnected', 'offline'].includes(normalized)) {
|
|
30
|
+
return 'red';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Info states
|
|
34
|
+
if (['info', 'new', 'created'].includes(normalized)) {
|
|
35
|
+
return 'blue';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Default
|
|
39
|
+
return 'gray';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function StatusBadge({ status, darkMode = false }: StatusBadgeProps): ReactElement {
|
|
43
|
+
const color = getStatusColor(status);
|
|
44
|
+
|
|
45
|
+
const colorMap: Record<StatusColor, { bg: string; text: string }> = {
|
|
46
|
+
green: {
|
|
47
|
+
bg: darkMode ? '#14532d' : '#dcfce7',
|
|
48
|
+
text: darkMode ? '#4ade80' : '#166534',
|
|
49
|
+
},
|
|
50
|
+
amber: {
|
|
51
|
+
bg: darkMode ? '#422006' : '#fef3c7',
|
|
52
|
+
text: darkMode ? '#fbbf24' : '#92400e',
|
|
53
|
+
},
|
|
54
|
+
red: {
|
|
55
|
+
bg: darkMode ? '#3c1618' : '#fef2f2',
|
|
56
|
+
text: darkMode ? '#fca5a5' : '#991b1b',
|
|
57
|
+
},
|
|
58
|
+
blue: {
|
|
59
|
+
bg: darkMode ? '#1e3a5f' : '#e0f2fe',
|
|
60
|
+
text: darkMode ? '#60a5fa' : '#0369a1',
|
|
61
|
+
},
|
|
62
|
+
gray: {
|
|
63
|
+
bg: darkMode ? '#374151' : '#f3f4f6',
|
|
64
|
+
text: darkMode ? '#9ca3af' : '#6b7280',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const { bg, text } = colorMap[color];
|
|
69
|
+
|
|
70
|
+
const styles: CSSProperties = {
|
|
71
|
+
display: 'inline-flex',
|
|
72
|
+
alignItems: 'center',
|
|
73
|
+
padding: '4px 10px',
|
|
74
|
+
borderRadius: '4px',
|
|
75
|
+
fontSize: '12px',
|
|
76
|
+
fontWeight: 500,
|
|
77
|
+
background: bg,
|
|
78
|
+
color: text,
|
|
79
|
+
textTransform: 'capitalize',
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return <span style={styles}>{status}</span>;
|
|
83
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demo Components
|
|
3
|
+
* Re-exports all demo UI components.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { InfoTooltip } from './InfoTooltip';
|
|
7
|
+
export { LearnMore } from './LearnMore';
|
|
8
|
+
export { EmptyState } from './EmptyState';
|
|
9
|
+
export { StatusBadge } from './StatusBadge';
|
|
10
|
+
export { Banner } from './Banner';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAuditEvents Hook
|
|
3
|
+
* React hook for subscribing to audit events.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useEffect } from 'react';
|
|
7
|
+
import {
|
|
8
|
+
getAuditEvents,
|
|
9
|
+
subscribeToAuditEvents,
|
|
10
|
+
type AuditEvent,
|
|
11
|
+
} from '../../ekka/audit';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Hook to subscribe to and retrieve audit events.
|
|
15
|
+
* Re-renders when events change.
|
|
16
|
+
*/
|
|
17
|
+
export function useAuditEvents(): AuditEvent[] {
|
|
18
|
+
const [events, setEvents] = useState<AuditEvent[]>(getAuditEvents);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
// Subscribe to changes
|
|
22
|
+
const unsubscribe = subscribeToAuditEvents(() => {
|
|
23
|
+
setEvents(getAuditEvents());
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return unsubscribe;
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
return events;
|
|
30
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application Shell
|
|
3
|
+
* Main layout with sidebar navigation and content area
|
|
4
|
+
* Supports light/dark mode
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { CSSProperties, ReactNode } from 'react';
|
|
8
|
+
import { Sidebar, Page } from './Sidebar';
|
|
9
|
+
|
|
10
|
+
interface ShellProps {
|
|
11
|
+
selectedPage: Page;
|
|
12
|
+
onNavigate: (page: Page) => void;
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
darkMode: boolean;
|
|
15
|
+
onToggleDarkMode: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function Shell({ selectedPage, onNavigate, children, darkMode, onToggleDarkMode }: ShellProps) {
|
|
19
|
+
const styles: Record<string, CSSProperties> = {
|
|
20
|
+
shell: {
|
|
21
|
+
display: 'flex',
|
|
22
|
+
height: '100vh',
|
|
23
|
+
width: '100vw',
|
|
24
|
+
overflow: 'hidden',
|
|
25
|
+
background: darkMode ? '#1c1c1e' : '#ffffff',
|
|
26
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif',
|
|
27
|
+
},
|
|
28
|
+
main: {
|
|
29
|
+
flex: 1,
|
|
30
|
+
overflow: 'auto',
|
|
31
|
+
background: darkMode ? '#1c1c1e' : '#ffffff',
|
|
32
|
+
position: 'relative',
|
|
33
|
+
},
|
|
34
|
+
header: {
|
|
35
|
+
position: 'absolute',
|
|
36
|
+
top: '16px',
|
|
37
|
+
right: '24px',
|
|
38
|
+
zIndex: 10,
|
|
39
|
+
},
|
|
40
|
+
toggleButton: {
|
|
41
|
+
display: 'flex',
|
|
42
|
+
alignItems: 'center',
|
|
43
|
+
justifyContent: 'center',
|
|
44
|
+
width: '36px',
|
|
45
|
+
height: '36px',
|
|
46
|
+
background: darkMode ? '#2c2c2e' : '#f5f5f7',
|
|
47
|
+
border: `1px solid ${darkMode ? '#3a3a3c' : '#e5e5e5'}`,
|
|
48
|
+
borderRadius: '8px',
|
|
49
|
+
cursor: 'pointer',
|
|
50
|
+
color: darkMode ? '#ffffff' : '#1d1d1f',
|
|
51
|
+
transition: 'background 0.15s ease',
|
|
52
|
+
},
|
|
53
|
+
content: {
|
|
54
|
+
padding: '32px 40px',
|
|
55
|
+
width: '100%',
|
|
56
|
+
minHeight: 'calc(100vh - 64px)',
|
|
57
|
+
boxSizing: 'border-box' as const,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div style={styles.shell}>
|
|
63
|
+
<Sidebar selectedPage={selectedPage} onNavigate={onNavigate} darkMode={darkMode} />
|
|
64
|
+
<main style={styles.main}>
|
|
65
|
+
<div style={styles.header}>
|
|
66
|
+
<button
|
|
67
|
+
onClick={onToggleDarkMode}
|
|
68
|
+
style={styles.toggleButton}
|
|
69
|
+
onMouseEnter={(e) => {
|
|
70
|
+
e.currentTarget.style.background = darkMode ? '#3a3a3c' : '#e5e5e5';
|
|
71
|
+
}}
|
|
72
|
+
onMouseLeave={(e) => {
|
|
73
|
+
e.currentTarget.style.background = darkMode ? '#2c2c2e' : '#f5f5f7';
|
|
74
|
+
}}
|
|
75
|
+
title={darkMode ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
76
|
+
>
|
|
77
|
+
{darkMode ? <SunIcon /> : <MoonIcon />}
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
<div style={styles.content}>
|
|
81
|
+
{children}
|
|
82
|
+
</div>
|
|
83
|
+
</main>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function SunIcon() {
|
|
89
|
+
return (
|
|
90
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
91
|
+
<circle cx="12" cy="12" r="5" />
|
|
92
|
+
<line x1="12" y1="1" x2="12" y2="3" />
|
|
93
|
+
<line x1="12" y1="21" x2="12" y2="23" />
|
|
94
|
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
|
95
|
+
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
|
96
|
+
<line x1="1" y1="12" x2="3" y2="12" />
|
|
97
|
+
<line x1="21" y1="12" x2="23" y2="12" />
|
|
98
|
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
|
99
|
+
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
|
100
|
+
</svg>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function MoonIcon() {
|
|
105
|
+
return (
|
|
106
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
107
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
108
|
+
</svg>
|
|
109
|
+
);
|
|
110
|
+
}
|