elder_docs 0.1.2 → 0.1.3

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,470 @@
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
+ root.style.setProperty('--bd-primary', newConfig.colors.primary)
151
+ root.style.setProperty('--bd-secondary', newConfig.colors.secondary)
152
+ root.style.setProperty('--bd-background', newConfig.colors.background)
153
+ root.style.setProperty('--bd-surface', newConfig.colors.surface)
154
+ root.style.setProperty('--bd-corner-radius', newConfig.corner_radius)
155
+ root.style.setProperty('--bd-font-heading', `'${newConfig.font_heading}', sans-serif`)
156
+ root.style.setProperty('--bd-font-body', `'${newConfig.font_body}', sans-serif`)
157
+
158
+ // Load fonts
159
+ if (newConfig.font_heading) {
160
+ const linkHeading = document.createElement('link')
161
+ linkHeading.href = `https://fonts.googleapis.com/css2?family=${newConfig.font_heading.replace(/\s/g, '+')}:wght@500;600;700&display=swap`
162
+ linkHeading.rel = 'stylesheet'
163
+ if (!document.querySelector(`link[href*="${newConfig.font_heading}"]`)) {
164
+ document.head.appendChild(linkHeading)
165
+ }
166
+ }
167
+
168
+ if (newConfig.font_body) {
169
+ const linkBody = document.createElement('link')
170
+ linkBody.href = `https://fonts.googleapis.com/css2?family=${newConfig.font_body.replace(/\s/g, '+')}:wght@400;500;600&display=swap`
171
+ linkBody.rel = 'stylesheet'
172
+ if (!document.querySelector(`link[href*="${newConfig.font_body}"]`)) {
173
+ document.head.appendChild(linkBody)
174
+ }
175
+ }
176
+ }
177
+
178
+ if (!authenticated) {
179
+ return (
180
+ <div className="min-h-screen bg-white flex items-center justify-center p-6">
181
+ <div className="surface rounded-none p-8 max-w-md w-full">
182
+ <h1 className="text-3xl font-black text-black mb-6 font-['Syne'] uppercase">Admin Login</h1>
183
+ <form onSubmit={handleLogin}>
184
+ <div className="mb-4">
185
+ <label className="block text-sm font-bold text-black mb-2">
186
+ Admin Password
187
+ </label>
188
+ <input
189
+ type="password"
190
+ value={password}
191
+ onChange={(e) => setPassword(e.target.value)}
192
+ className="input-field w-full text-sm bg-white border-black text-black"
193
+ placeholder="Enter admin password"
194
+ required
195
+ />
196
+ </div>
197
+ {error && (
198
+ <div className="mb-4 p-3 bg-red-100 border-2 border-red-500 text-red-900 text-sm font-bold">
199
+ {error}
200
+ </div>
201
+ )}
202
+ <button
203
+ type="submit"
204
+ disabled={loading}
205
+ className="btn-primary w-full"
206
+ >
207
+ {loading ? 'Logging in...' : 'Login'}
208
+ </button>
209
+ </form>
210
+ <p className="mt-4 text-xs text-black/60">
211
+ 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)
212
+ </p>
213
+ </div>
214
+ </div>
215
+ )
216
+ }
217
+
218
+ return (
219
+ <div className="min-h-screen bg-white p-6">
220
+ <div className="max-w-4xl mx-auto">
221
+ <div className="surface rounded-none p-8 mb-6">
222
+ <div className="flex items-center justify-between mb-6">
223
+ <h1 className="text-4xl font-black text-black font-['Syne'] uppercase">UI Configuration</h1>
224
+ <button
225
+ onClick={handleLogout}
226
+ className="btn-secondary"
227
+ >
228
+ Logout
229
+ </button>
230
+ </div>
231
+
232
+ {saved && (
233
+ <div className="mb-4 p-3 bg-green-100 border-2 border-green-500 text-green-900 text-sm font-bold">
234
+ ✅ Configuration saved successfully! Refresh the page to see changes.
235
+ </div>
236
+ )}
237
+
238
+ {error && (
239
+ <div className="mb-4 p-3 bg-red-100 border-2 border-red-500 text-red-900 text-sm font-bold">
240
+ {error}
241
+ </div>
242
+ )}
243
+
244
+ <form onSubmit={handleSave}>
245
+ <div className="space-y-6">
246
+ {/* Fonts */}
247
+ <div>
248
+ <h2 className="text-xl font-black text-black mb-4 font-['Syne'] uppercase">Typography</h2>
249
+ <div className="grid grid-cols-2 gap-4">
250
+ <div>
251
+ <label className="block text-sm font-bold text-black mb-2">
252
+ Heading Font
253
+ </label>
254
+ <select
255
+ value={config.font_heading}
256
+ onChange={(e) => setConfig({ ...config, font_heading: e.target.value })}
257
+ className="input-field w-full text-sm bg-white border-black text-black"
258
+ >
259
+ {availableFonts.map(font => (
260
+ <option key={font} value={font}>{font}</option>
261
+ ))}
262
+ </select>
263
+ </div>
264
+ <div>
265
+ <label className="block text-sm font-bold text-black mb-2">
266
+ Body Font
267
+ </label>
268
+ <select
269
+ value={config.font_body}
270
+ onChange={(e) => setConfig({ ...config, font_body: e.target.value })}
271
+ className="input-field w-full text-sm bg-white border-black text-black"
272
+ >
273
+ {availableFonts.map(font => (
274
+ <option key={font} value={font}>{font}</option>
275
+ ))}
276
+ </select>
277
+ </div>
278
+ </div>
279
+ </div>
280
+
281
+ {/* Colors */}
282
+ <div>
283
+ <h2 className="text-xl font-black text-black mb-4 font-['Syne'] uppercase">Colors</h2>
284
+ <div className="grid grid-cols-2 gap-4">
285
+ <div>
286
+ <label className="block text-sm font-bold text-black mb-2">
287
+ Primary Color
288
+ </label>
289
+ <div className="flex gap-2">
290
+ <input
291
+ type="color"
292
+ value={config.colors.primary}
293
+ onChange={(e) => setConfig({
294
+ ...config,
295
+ colors: { ...config.colors, primary: e.target.value }
296
+ })}
297
+ className="w-16 h-10 border-2 border-black"
298
+ />
299
+ <input
300
+ type="text"
301
+ value={config.colors.primary}
302
+ onChange={(e) => setConfig({
303
+ ...config,
304
+ colors: { ...config.colors, primary: e.target.value }
305
+ })}
306
+ className="input-field flex-1 text-sm bg-white border-black text-black font-mono"
307
+ placeholder="#f8d447"
308
+ />
309
+ </div>
310
+ </div>
311
+ <div>
312
+ <label className="block text-sm font-bold text-black mb-2">
313
+ Secondary Color
314
+ </label>
315
+ <div className="flex gap-2">
316
+ <input
317
+ type="color"
318
+ value={config.colors.secondary}
319
+ onChange={(e) => setConfig({
320
+ ...config,
321
+ colors: { ...config.colors, secondary: e.target.value }
322
+ })}
323
+ className="w-16 h-10 border-2 border-black"
324
+ />
325
+ <input
326
+ type="text"
327
+ value={config.colors.secondary}
328
+ onChange={(e) => setConfig({
329
+ ...config,
330
+ colors: { ...config.colors, secondary: e.target.value }
331
+ })}
332
+ className="input-field flex-1 text-sm bg-white border-black text-black font-mono"
333
+ placeholder="#000000"
334
+ />
335
+ </div>
336
+ </div>
337
+ <div>
338
+ <label className="block text-sm font-bold text-black mb-2">
339
+ Background Color
340
+ </label>
341
+ <div className="flex gap-2">
342
+ <input
343
+ type="color"
344
+ value={config.colors.background}
345
+ onChange={(e) => setConfig({
346
+ ...config,
347
+ colors: { ...config.colors, background: e.target.value }
348
+ })}
349
+ className="w-16 h-10 border-2 border-black"
350
+ />
351
+ <input
352
+ type="text"
353
+ value={config.colors.background}
354
+ onChange={(e) => setConfig({
355
+ ...config,
356
+ colors: { ...config.colors, background: e.target.value }
357
+ })}
358
+ className="input-field flex-1 text-sm bg-white border-black text-black font-mono"
359
+ placeholder="#ffffff"
360
+ />
361
+ </div>
362
+ </div>
363
+ <div>
364
+ <label className="block text-sm font-bold text-black mb-2">
365
+ Surface Color
366
+ </label>
367
+ <div className="flex gap-2">
368
+ <input
369
+ type="color"
370
+ value={config.colors.surface}
371
+ onChange={(e) => setConfig({
372
+ ...config,
373
+ colors: { ...config.colors, surface: e.target.value }
374
+ })}
375
+ className="w-16 h-10 border-2 border-black"
376
+ />
377
+ <input
378
+ type="text"
379
+ value={config.colors.surface}
380
+ onChange={(e) => setConfig({
381
+ ...config,
382
+ colors: { ...config.colors, surface: e.target.value }
383
+ })}
384
+ className="input-field flex-1 text-sm bg-white border-black text-black font-mono"
385
+ placeholder="#ffffff"
386
+ />
387
+ </div>
388
+ </div>
389
+ </div>
390
+ </div>
391
+
392
+ {/* Corner Radius */}
393
+ <div>
394
+ <h2 className="text-xl font-black text-black mb-4 font-['Syne'] uppercase">Styling</h2>
395
+ <div>
396
+ <label className="block text-sm font-bold text-black mb-2">
397
+ Corner Radius: {config.corner_radius}
398
+ </label>
399
+ <input
400
+ type="range"
401
+ min="0"
402
+ max="24"
403
+ value={parseInt(config.corner_radius) || 0}
404
+ onChange={(e) => setConfig({ ...config, corner_radius: `${e.target.value}px` })}
405
+ className="w-full"
406
+ />
407
+ <div className="flex justify-between text-xs text-black/60 mt-1">
408
+ <span>Sharp (0px)</span>
409
+ <span>Rounded (24px)</span>
410
+ </div>
411
+ </div>
412
+ </div>
413
+
414
+ {/* Preview */}
415
+ <div>
416
+ <h2 className="text-xl font-black text-black mb-4 font-['Syne'] uppercase">Preview</h2>
417
+ <div className="surface rounded-none p-6" style={{
418
+ backgroundColor: config.colors.surface,
419
+ borderColor: config.colors.secondary,
420
+ borderRadius: config.corner_radius
421
+ }}>
422
+ <div className="pill mb-4" style={{
423
+ backgroundColor: config.colors.primary,
424
+ color: config.colors.secondary,
425
+ borderColor: config.colors.secondary,
426
+ borderRadius: config.corner_radius
427
+ }}>
428
+ Sample Button
429
+ </div>
430
+ <h3 style={{
431
+ fontFamily: `'${config.font_heading}', sans-serif`,
432
+ color: config.colors.secondary
433
+ }}>
434
+ Heading Preview
435
+ </h3>
436
+ <p style={{
437
+ fontFamily: `'${config.font_body}', sans-serif`,
438
+ color: config.colors.secondary
439
+ }}>
440
+ This is how body text will look with your selected fonts and colors.
441
+ </p>
442
+ </div>
443
+ </div>
444
+
445
+ <div className="flex gap-4">
446
+ <button
447
+ type="submit"
448
+ disabled={loading}
449
+ className="btn-primary flex-1"
450
+ >
451
+ {loading ? 'Saving...' : 'Save Configuration'}
452
+ </button>
453
+ <button
454
+ type="button"
455
+ onClick={() => applyConfig(config)}
456
+ className="btn-secondary"
457
+ >
458
+ Preview
459
+ </button>
460
+ </div>
461
+ </div>
462
+ </form>
463
+ </div>
464
+ </div>
465
+ </div>
466
+ )
467
+ }
468
+
469
+ export default UiConfigurator
470
+
@@ -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
+ }