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.
@@ -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
+ }