elder_docs 0.1.2 → 0.1.4
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.
- checksums.yaml +4 -4
- data/README.md +43 -0
- data/frontend/index.html +18 -0
- data/frontend/package.json +28 -0
- data/frontend/postcss.config.js +7 -0
- data/frontend/public/data.js +2 -0
- data/frontend/src/App.jsx +170 -0
- data/frontend/src/UiConfigApp.jsx +11 -0
- data/frontend/src/components/ApiExplorer.jsx +323 -0
- data/frontend/src/components/ContentPanel.jsx +228 -0
- data/frontend/src/components/Sidebar.jsx +157 -0
- data/frontend/src/components/UiConfigurator.jsx +494 -0
- data/frontend/src/contexts/ApiKeyContext.jsx +41 -0
- data/frontend/src/index.css +215 -0
- data/frontend/src/main.jsx +11 -0
- data/frontend/tailwind.config.js +40 -0
- data/frontend/vite.config.js +17 -0
- data/lib/elder_docs/generator.rb +19 -3
- data/lib/elder_docs/version.rb +1 -1
- metadata +18 -3
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
const UiConfigurator = () => {
|
|
4
|
+
const [authenticated, setAuthenticated] = useState(false)
|
|
5
|
+
const [password, setPassword] = useState('')
|
|
6
|
+
const [loading, setLoading] = useState(false)
|
|
7
|
+
const [error, setError] = useState('')
|
|
8
|
+
const [saved, setSaved] = useState(false)
|
|
9
|
+
|
|
10
|
+
const [config, setConfig] = useState({
|
|
11
|
+
font_heading: 'Syne',
|
|
12
|
+
font_body: 'IBM Plex Sans',
|
|
13
|
+
colors: {
|
|
14
|
+
primary: '#f8d447',
|
|
15
|
+
secondary: '#000000',
|
|
16
|
+
background: '#ffffff',
|
|
17
|
+
surface: '#ffffff'
|
|
18
|
+
},
|
|
19
|
+
corner_radius: '0px'
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const availableFonts = [
|
|
23
|
+
'Syne',
|
|
24
|
+
'IBM Plex Sans',
|
|
25
|
+
'Inter',
|
|
26
|
+
'Space Grotesk',
|
|
27
|
+
'Oswald',
|
|
28
|
+
'Fira Code',
|
|
29
|
+
'Roboto',
|
|
30
|
+
'Open Sans'
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
checkAuth()
|
|
35
|
+
}, [])
|
|
36
|
+
|
|
37
|
+
const checkAuth = async () => {
|
|
38
|
+
try {
|
|
39
|
+
const response = await fetch('/docs/ui')
|
|
40
|
+
if (response.ok) {
|
|
41
|
+
const data = await response.json()
|
|
42
|
+
setAuthenticated(true)
|
|
43
|
+
if (data.ui_config) {
|
|
44
|
+
setConfig({
|
|
45
|
+
font_heading: data.ui_config.font_heading || 'Syne',
|
|
46
|
+
font_body: data.ui_config.font_body || 'IBM Plex Sans',
|
|
47
|
+
colors: {
|
|
48
|
+
primary: data.ui_config.colors?.primary || '#f8d447',
|
|
49
|
+
secondary: data.ui_config.colors?.secondary || '#000000',
|
|
50
|
+
background: data.ui_config.colors?.background || '#ffffff',
|
|
51
|
+
surface: data.ui_config.colors?.surface || '#ffffff'
|
|
52
|
+
},
|
|
53
|
+
corner_radius: data.ui_config.corner_radius || '0px'
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
setAuthenticated(false)
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
setAuthenticated(false)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const handleLogin = async (e) => {
|
|
65
|
+
e.preventDefault()
|
|
66
|
+
setLoading(true)
|
|
67
|
+
setError('')
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch('/docs/ui/login', {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: {
|
|
73
|
+
'Content-Type': 'application/json',
|
|
74
|
+
},
|
|
75
|
+
body: JSON.stringify({ password }),
|
|
76
|
+
credentials: 'include'
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const data = await response.json()
|
|
80
|
+
|
|
81
|
+
if (data.success) {
|
|
82
|
+
setAuthenticated(true)
|
|
83
|
+
await checkAuth()
|
|
84
|
+
} else {
|
|
85
|
+
setError(data.error || 'Invalid password')
|
|
86
|
+
}
|
|
87
|
+
} catch (err) {
|
|
88
|
+
setError('Login failed. Please try again.')
|
|
89
|
+
} finally {
|
|
90
|
+
setLoading(false)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const handleSave = async (e) => {
|
|
95
|
+
e.preventDefault()
|
|
96
|
+
setLoading(true)
|
|
97
|
+
setError('')
|
|
98
|
+
setSaved(false)
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const response = await fetch('/docs/ui/config', {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: {
|
|
104
|
+
'Content-Type': 'application/json',
|
|
105
|
+
},
|
|
106
|
+
body: JSON.stringify({
|
|
107
|
+
font_heading: config.font_heading,
|
|
108
|
+
font_body: config.font_body,
|
|
109
|
+
color_primary: config.colors.primary,
|
|
110
|
+
color_secondary: config.colors.secondary,
|
|
111
|
+
color_background: config.colors.background,
|
|
112
|
+
color_surface: config.colors.surface,
|
|
113
|
+
corner_radius: config.corner_radius
|
|
114
|
+
}),
|
|
115
|
+
credentials: 'include'
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const data = await response.json()
|
|
119
|
+
|
|
120
|
+
if (data.success) {
|
|
121
|
+
setSaved(true)
|
|
122
|
+
setTimeout(() => setSaved(false), 3000)
|
|
123
|
+
// Apply changes immediately
|
|
124
|
+
applyConfig(config)
|
|
125
|
+
} else {
|
|
126
|
+
setError(data.error || 'Failed to save configuration')
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
setError('Save failed. Please try again.')
|
|
130
|
+
} finally {
|
|
131
|
+
setLoading(false)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const handleLogout = async () => {
|
|
136
|
+
try {
|
|
137
|
+
await fetch('/docs/ui/logout', {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
credentials: 'include'
|
|
140
|
+
})
|
|
141
|
+
setAuthenticated(false)
|
|
142
|
+
setPassword('')
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.error('Logout failed:', err)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const applyConfig = (newConfig) => {
|
|
149
|
+
const root = document.documentElement
|
|
150
|
+
// Map config colors to CSS variable names used in index.css
|
|
151
|
+
if (newConfig.colors.primary) {
|
|
152
|
+
root.style.setProperty('--bd-yellow', newConfig.colors.primary)
|
|
153
|
+
root.style.setProperty('--bd-primary', newConfig.colors.primary) // Keep for compatibility
|
|
154
|
+
}
|
|
155
|
+
if (newConfig.colors.secondary) {
|
|
156
|
+
root.style.setProperty('--bd-charcoal', newConfig.colors.secondary)
|
|
157
|
+
root.style.setProperty('--bd-border', newConfig.colors.secondary)
|
|
158
|
+
root.style.setProperty('--bd-ink', newConfig.colors.secondary)
|
|
159
|
+
root.style.setProperty('--bd-secondary', newConfig.colors.secondary) // Keep for compatibility
|
|
160
|
+
}
|
|
161
|
+
if (newConfig.colors.background) {
|
|
162
|
+
root.style.setProperty('--bd-white', newConfig.colors.background)
|
|
163
|
+
root.style.setProperty('--bd-background', newConfig.colors.background) // Keep for compatibility
|
|
164
|
+
}
|
|
165
|
+
if (newConfig.colors.surface) {
|
|
166
|
+
root.style.setProperty('--bd-panel', newConfig.colors.surface)
|
|
167
|
+
root.style.setProperty('--bd-surface', newConfig.colors.surface) // Keep for compatibility
|
|
168
|
+
}
|
|
169
|
+
if (newConfig.corner_radius) {
|
|
170
|
+
root.style.setProperty('--bd-radius', newConfig.corner_radius)
|
|
171
|
+
root.style.setProperty('--bd-corner-radius', newConfig.corner_radius) // Keep for compatibility
|
|
172
|
+
}
|
|
173
|
+
if (newConfig.font_heading) {
|
|
174
|
+
root.style.setProperty('--font-heading', `'${newConfig.font_heading}', sans-serif`)
|
|
175
|
+
root.style.setProperty('--bd-font-heading', `'${newConfig.font_heading}', sans-serif`) // Keep for compatibility
|
|
176
|
+
}
|
|
177
|
+
if (newConfig.font_body) {
|
|
178
|
+
root.style.setProperty('--font-body', `'${newConfig.font_body}', sans-serif`)
|
|
179
|
+
root.style.setProperty('--bd-font-body', `'${newConfig.font_body}', sans-serif`) // Keep for compatibility
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Load fonts
|
|
183
|
+
if (newConfig.font_heading) {
|
|
184
|
+
const linkHeading = document.createElement('link')
|
|
185
|
+
linkHeading.href = `https://fonts.googleapis.com/css2?family=${newConfig.font_heading.replace(/\s/g, '+')}:wght@500;600;700&display=swap`
|
|
186
|
+
linkHeading.rel = 'stylesheet'
|
|
187
|
+
if (!document.querySelector(`link[href*="${newConfig.font_heading}"]`)) {
|
|
188
|
+
document.head.appendChild(linkHeading)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (newConfig.font_body) {
|
|
193
|
+
const linkBody = document.createElement('link')
|
|
194
|
+
linkBody.href = `https://fonts.googleapis.com/css2?family=${newConfig.font_body.replace(/\s/g, '+')}:wght@400;500;600&display=swap`
|
|
195
|
+
linkBody.rel = 'stylesheet'
|
|
196
|
+
if (!document.querySelector(`link[href*="${newConfig.font_body}"]`)) {
|
|
197
|
+
document.head.appendChild(linkBody)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!authenticated) {
|
|
203
|
+
return (
|
|
204
|
+
<div className="min-h-screen bg-white flex items-center justify-center p-6">
|
|
205
|
+
<div className="surface rounded-none p-8 max-w-md w-full">
|
|
206
|
+
<h1 className="text-3xl font-black text-black mb-6 font-['Syne'] uppercase">Admin Login</h1>
|
|
207
|
+
<form onSubmit={handleLogin}>
|
|
208
|
+
<div className="mb-4">
|
|
209
|
+
<label className="block text-sm font-bold text-black mb-2">
|
|
210
|
+
Admin Password
|
|
211
|
+
</label>
|
|
212
|
+
<input
|
|
213
|
+
type="password"
|
|
214
|
+
value={password}
|
|
215
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
216
|
+
className="input-field w-full text-sm bg-white border-black text-black"
|
|
217
|
+
placeholder="Enter admin password"
|
|
218
|
+
required
|
|
219
|
+
/>
|
|
220
|
+
</div>
|
|
221
|
+
{error && (
|
|
222
|
+
<div className="mb-4 p-3 bg-red-100 border-2 border-red-500 text-red-900 text-sm font-bold">
|
|
223
|
+
{error}
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
<button
|
|
227
|
+
type="submit"
|
|
228
|
+
disabled={loading}
|
|
229
|
+
className="btn-primary w-full"
|
|
230
|
+
>
|
|
231
|
+
{loading ? 'Logging in...' : 'Login'}
|
|
232
|
+
</button>
|
|
233
|
+
</form>
|
|
234
|
+
<p className="mt-4 text-xs text-black/60">
|
|
235
|
+
Default password: <code className="bg-yellow-100 px-1">admin</code> (or set via <code className="bg-yellow-100 px-1">ELDERDOCS_ADMIN_PASSWORD</code> env var)
|
|
236
|
+
</p>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<div className="min-h-screen bg-white p-6">
|
|
244
|
+
<div className="max-w-4xl mx-auto">
|
|
245
|
+
<div className="surface rounded-none p-8 mb-6">
|
|
246
|
+
<div className="flex items-center justify-between mb-6">
|
|
247
|
+
<h1 className="text-4xl font-black text-black font-['Syne'] uppercase">UI Configuration</h1>
|
|
248
|
+
<button
|
|
249
|
+
onClick={handleLogout}
|
|
250
|
+
className="btn-secondary"
|
|
251
|
+
>
|
|
252
|
+
Logout
|
|
253
|
+
</button>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
{saved && (
|
|
257
|
+
<div className="mb-4 p-3 bg-green-100 border-2 border-green-500 text-green-900 text-sm font-bold">
|
|
258
|
+
✅ Configuration saved successfully! Refresh the page to see changes.
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
|
|
262
|
+
{error && (
|
|
263
|
+
<div className="mb-4 p-3 bg-red-100 border-2 border-red-500 text-red-900 text-sm font-bold">
|
|
264
|
+
{error}
|
|
265
|
+
</div>
|
|
266
|
+
)}
|
|
267
|
+
|
|
268
|
+
<form onSubmit={handleSave}>
|
|
269
|
+
<div className="space-y-6">
|
|
270
|
+
{/* Fonts */}
|
|
271
|
+
<div>
|
|
272
|
+
<h2 className="text-xl font-black text-black mb-4 font-['Syne'] uppercase">Typography</h2>
|
|
273
|
+
<div className="grid grid-cols-2 gap-4">
|
|
274
|
+
<div>
|
|
275
|
+
<label className="block text-sm font-bold text-black mb-2">
|
|
276
|
+
Heading Font
|
|
277
|
+
</label>
|
|
278
|
+
<select
|
|
279
|
+
value={config.font_heading}
|
|
280
|
+
onChange={(e) => setConfig({ ...config, font_heading: e.target.value })}
|
|
281
|
+
className="input-field w-full text-sm bg-white border-black text-black"
|
|
282
|
+
>
|
|
283
|
+
{availableFonts.map(font => (
|
|
284
|
+
<option key={font} value={font}>{font}</option>
|
|
285
|
+
))}
|
|
286
|
+
</select>
|
|
287
|
+
</div>
|
|
288
|
+
<div>
|
|
289
|
+
<label className="block text-sm font-bold text-black mb-2">
|
|
290
|
+
Body Font
|
|
291
|
+
</label>
|
|
292
|
+
<select
|
|
293
|
+
value={config.font_body}
|
|
294
|
+
onChange={(e) => setConfig({ ...config, font_body: e.target.value })}
|
|
295
|
+
className="input-field w-full text-sm bg-white border-black text-black"
|
|
296
|
+
>
|
|
297
|
+
{availableFonts.map(font => (
|
|
298
|
+
<option key={font} value={font}>{font}</option>
|
|
299
|
+
))}
|
|
300
|
+
</select>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
{/* Colors */}
|
|
306
|
+
<div>
|
|
307
|
+
<h2 className="text-xl font-black text-black mb-4 font-['Syne'] uppercase">Colors</h2>
|
|
308
|
+
<div className="grid grid-cols-2 gap-4">
|
|
309
|
+
<div>
|
|
310
|
+
<label className="block text-sm font-bold text-black mb-2">
|
|
311
|
+
Primary Color
|
|
312
|
+
</label>
|
|
313
|
+
<div className="flex gap-2">
|
|
314
|
+
<input
|
|
315
|
+
type="color"
|
|
316
|
+
value={config.colors.primary}
|
|
317
|
+
onChange={(e) => setConfig({
|
|
318
|
+
...config,
|
|
319
|
+
colors: { ...config.colors, primary: e.target.value }
|
|
320
|
+
})}
|
|
321
|
+
className="w-16 h-10 border-2 border-black"
|
|
322
|
+
/>
|
|
323
|
+
<input
|
|
324
|
+
type="text"
|
|
325
|
+
value={config.colors.primary}
|
|
326
|
+
onChange={(e) => setConfig({
|
|
327
|
+
...config,
|
|
328
|
+
colors: { ...config.colors, primary: e.target.value }
|
|
329
|
+
})}
|
|
330
|
+
className="input-field flex-1 text-sm bg-white border-black text-black font-mono"
|
|
331
|
+
placeholder="#f8d447"
|
|
332
|
+
/>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
<div>
|
|
336
|
+
<label className="block text-sm font-bold text-black mb-2">
|
|
337
|
+
Secondary Color
|
|
338
|
+
</label>
|
|
339
|
+
<div className="flex gap-2">
|
|
340
|
+
<input
|
|
341
|
+
type="color"
|
|
342
|
+
value={config.colors.secondary}
|
|
343
|
+
onChange={(e) => setConfig({
|
|
344
|
+
...config,
|
|
345
|
+
colors: { ...config.colors, secondary: e.target.value }
|
|
346
|
+
})}
|
|
347
|
+
className="w-16 h-10 border-2 border-black"
|
|
348
|
+
/>
|
|
349
|
+
<input
|
|
350
|
+
type="text"
|
|
351
|
+
value={config.colors.secondary}
|
|
352
|
+
onChange={(e) => setConfig({
|
|
353
|
+
...config,
|
|
354
|
+
colors: { ...config.colors, secondary: e.target.value }
|
|
355
|
+
})}
|
|
356
|
+
className="input-field flex-1 text-sm bg-white border-black text-black font-mono"
|
|
357
|
+
placeholder="#000000"
|
|
358
|
+
/>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
<div>
|
|
362
|
+
<label className="block text-sm font-bold text-black mb-2">
|
|
363
|
+
Background Color
|
|
364
|
+
</label>
|
|
365
|
+
<div className="flex gap-2">
|
|
366
|
+
<input
|
|
367
|
+
type="color"
|
|
368
|
+
value={config.colors.background}
|
|
369
|
+
onChange={(e) => setConfig({
|
|
370
|
+
...config,
|
|
371
|
+
colors: { ...config.colors, background: e.target.value }
|
|
372
|
+
})}
|
|
373
|
+
className="w-16 h-10 border-2 border-black"
|
|
374
|
+
/>
|
|
375
|
+
<input
|
|
376
|
+
type="text"
|
|
377
|
+
value={config.colors.background}
|
|
378
|
+
onChange={(e) => setConfig({
|
|
379
|
+
...config,
|
|
380
|
+
colors: { ...config.colors, background: e.target.value }
|
|
381
|
+
})}
|
|
382
|
+
className="input-field flex-1 text-sm bg-white border-black text-black font-mono"
|
|
383
|
+
placeholder="#ffffff"
|
|
384
|
+
/>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
<div>
|
|
388
|
+
<label className="block text-sm font-bold text-black mb-2">
|
|
389
|
+
Surface Color
|
|
390
|
+
</label>
|
|
391
|
+
<div className="flex gap-2">
|
|
392
|
+
<input
|
|
393
|
+
type="color"
|
|
394
|
+
value={config.colors.surface}
|
|
395
|
+
onChange={(e) => setConfig({
|
|
396
|
+
...config,
|
|
397
|
+
colors: { ...config.colors, surface: e.target.value }
|
|
398
|
+
})}
|
|
399
|
+
className="w-16 h-10 border-2 border-black"
|
|
400
|
+
/>
|
|
401
|
+
<input
|
|
402
|
+
type="text"
|
|
403
|
+
value={config.colors.surface}
|
|
404
|
+
onChange={(e) => setConfig({
|
|
405
|
+
...config,
|
|
406
|
+
colors: { ...config.colors, surface: e.target.value }
|
|
407
|
+
})}
|
|
408
|
+
className="input-field flex-1 text-sm bg-white border-black text-black font-mono"
|
|
409
|
+
placeholder="#ffffff"
|
|
410
|
+
/>
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
{/* Corner Radius */}
|
|
417
|
+
<div>
|
|
418
|
+
<h2 className="text-xl font-black text-black mb-4 font-['Syne'] uppercase">Styling</h2>
|
|
419
|
+
<div>
|
|
420
|
+
<label className="block text-sm font-bold text-black mb-2">
|
|
421
|
+
Corner Radius: {config.corner_radius}
|
|
422
|
+
</label>
|
|
423
|
+
<input
|
|
424
|
+
type="range"
|
|
425
|
+
min="0"
|
|
426
|
+
max="24"
|
|
427
|
+
value={parseInt(config.corner_radius) || 0}
|
|
428
|
+
onChange={(e) => setConfig({ ...config, corner_radius: `${e.target.value}px` })}
|
|
429
|
+
className="w-full"
|
|
430
|
+
/>
|
|
431
|
+
<div className="flex justify-between text-xs text-black/60 mt-1">
|
|
432
|
+
<span>Sharp (0px)</span>
|
|
433
|
+
<span>Rounded (24px)</span>
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
|
|
438
|
+
{/* Preview */}
|
|
439
|
+
<div>
|
|
440
|
+
<h2 className="text-xl font-black text-black mb-4 font-['Syne'] uppercase">Preview</h2>
|
|
441
|
+
<div className="surface rounded-none p-6" style={{
|
|
442
|
+
backgroundColor: config.colors.surface,
|
|
443
|
+
borderColor: config.colors.secondary,
|
|
444
|
+
borderRadius: config.corner_radius
|
|
445
|
+
}}>
|
|
446
|
+
<div className="pill mb-4" style={{
|
|
447
|
+
backgroundColor: config.colors.primary,
|
|
448
|
+
color: config.colors.secondary,
|
|
449
|
+
borderColor: config.colors.secondary,
|
|
450
|
+
borderRadius: config.corner_radius
|
|
451
|
+
}}>
|
|
452
|
+
Sample Button
|
|
453
|
+
</div>
|
|
454
|
+
<h3 style={{
|
|
455
|
+
fontFamily: `'${config.font_heading}', sans-serif`,
|
|
456
|
+
color: config.colors.secondary
|
|
457
|
+
}}>
|
|
458
|
+
Heading Preview
|
|
459
|
+
</h3>
|
|
460
|
+
<p style={{
|
|
461
|
+
fontFamily: `'${config.font_body}', sans-serif`,
|
|
462
|
+
color: config.colors.secondary
|
|
463
|
+
}}>
|
|
464
|
+
This is how body text will look with your selected fonts and colors.
|
|
465
|
+
</p>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
|
|
469
|
+
<div className="flex gap-4">
|
|
470
|
+
<button
|
|
471
|
+
type="submit"
|
|
472
|
+
disabled={loading}
|
|
473
|
+
className="btn-primary flex-1"
|
|
474
|
+
>
|
|
475
|
+
{loading ? 'Saving...' : 'Save Configuration'}
|
|
476
|
+
</button>
|
|
477
|
+
<button
|
|
478
|
+
type="button"
|
|
479
|
+
onClick={() => applyConfig(config)}
|
|
480
|
+
className="btn-secondary"
|
|
481
|
+
>
|
|
482
|
+
Preview
|
|
483
|
+
</button>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
</form>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export default UiConfigurator
|
|
494
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
const ApiKeyContext = createContext()
|
|
4
|
+
|
|
5
|
+
export const useApiKey = () => {
|
|
6
|
+
const context = useContext(ApiKeyContext)
|
|
7
|
+
if (!context) {
|
|
8
|
+
throw new Error('useApiKey must be used within ApiKeyProvider')
|
|
9
|
+
}
|
|
10
|
+
return context
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ApiKeyProvider = ({ children }) => {
|
|
14
|
+
const [authConfig, setAuthConfig] = useState(() => {
|
|
15
|
+
// Load from localStorage on mount
|
|
16
|
+
const saved = localStorage.getItem('elderdocs_auth_config')
|
|
17
|
+
if (saved) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(saved)
|
|
20
|
+
} catch {
|
|
21
|
+
return { type: 'bearer', value: '' }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { type: 'bearer', value: '' }
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
// Save to localStorage whenever it changes
|
|
29
|
+
if (authConfig) {
|
|
30
|
+
localStorage.setItem('elderdocs_auth_config', JSON.stringify(authConfig))
|
|
31
|
+
} else {
|
|
32
|
+
localStorage.removeItem('elderdocs_auth_config')
|
|
33
|
+
}
|
|
34
|
+
}, [authConfig])
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<ApiKeyContext.Provider value={{ authConfig, setAuthConfig }}>
|
|
38
|
+
{children}
|
|
39
|
+
</ApiKeyContext.Provider>
|
|
40
|
+
)
|
|
41
|
+
}
|