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.
- 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 +154 -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 +470 -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,228 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import ReactMarkdown from 'react-markdown'
|
|
3
|
+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
|
4
|
+
import { oneLight } from 'react-syntax-highlighter/dist/cjs/styles/prism'
|
|
5
|
+
|
|
6
|
+
const ContentPanel = ({ data, activeView, selectedItem }) => {
|
|
7
|
+
if (!selectedItem) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex-1 p-10 overflow-y-auto bg-white">
|
|
10
|
+
<div className="surface rounded-none p-12 text-center reveal" style={{ animationDelay: '120ms' }}>
|
|
11
|
+
<div className="pill text-black mb-4 bg-yellow-400 border-black">Awaiting Selection</div>
|
|
12
|
+
<div className="text-2xl font-black text-black uppercase">Pick a route to start.</div>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (selectedItem.type === 'article') {
|
|
19
|
+
const article = data?.articles?.find(a => a.id === selectedItem.id)
|
|
20
|
+
|
|
21
|
+
if (!article) {
|
|
22
|
+
return (
|
|
23
|
+
<div className="flex-1 p-10 overflow-y-auto bg-white">
|
|
24
|
+
<div className="surface rounded-none p-12 text-center reveal" style={{ animationDelay: '120ms' }}>
|
|
25
|
+
<div className="pill text-black mb-4 bg-yellow-400 border-black">Article Not Found</div>
|
|
26
|
+
<div className="text-2xl font-black text-black uppercase">The selected article could not be found.</div>
|
|
27
|
+
{data?.articles?.length > 0 && (
|
|
28
|
+
<div className="mt-4 text-sm text-black/70">
|
|
29
|
+
Available articles: {data.articles.length}
|
|
30
|
+
</div>
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="flex-1 p-10 overflow-y-auto bg-white">
|
|
39
|
+
<div className="surface rounded-none p-10 reveal" style={{ animationDelay: '150ms' }}>
|
|
40
|
+
<h1 className="text-4xl font-black text-black mb-6 font-['Syne'] uppercase">{article.title}</h1>
|
|
41
|
+
<div className="prose prose-base max-w-none text-black">
|
|
42
|
+
<ReactMarkdown
|
|
43
|
+
components={{
|
|
44
|
+
h1: ({node, ...props}) => <h1 className="text-3xl font-bold text-black mb-5 mt-8 font-['Syne'] uppercase" {...props} />,
|
|
45
|
+
h2: ({node, ...props}) => <h2 className="text-2xl font-bold text-black mb-4 mt-6 font-['Syne'] uppercase" {...props} />,
|
|
46
|
+
h3: ({node, ...props}) => <h3 className="text-xl font-bold text-black mb-3 mt-4 font-['Syne'] uppercase" {...props} />,
|
|
47
|
+
p: ({node, ...props}) => <p className="text-base text-black/90 mb-4 leading-relaxed font-medium" {...props} />,
|
|
48
|
+
code({ node, inline, className, children, ...props }) {
|
|
49
|
+
const match = /language-(\w+)/.exec(className || '')
|
|
50
|
+
return !inline && match ? (
|
|
51
|
+
<div className="my-5 border-2 border-black">
|
|
52
|
+
<SyntaxHighlighter
|
|
53
|
+
style={oneLight}
|
|
54
|
+
language={match[1]}
|
|
55
|
+
PreTag="div"
|
|
56
|
+
customStyle={{
|
|
57
|
+
margin: 0,
|
|
58
|
+
padding: '18px',
|
|
59
|
+
fontSize: '15px',
|
|
60
|
+
backgroundColor: '#fff',
|
|
61
|
+
border: 'none',
|
|
62
|
+
borderRadius: '0'
|
|
63
|
+
}}
|
|
64
|
+
{...props}
|
|
65
|
+
>
|
|
66
|
+
{String(children).replace(/\n$/, '')}
|
|
67
|
+
</SyntaxHighlighter>
|
|
68
|
+
</div>
|
|
69
|
+
) : (
|
|
70
|
+
<code className="bg-yellow-100 px-2 py-1 font-mono text-sm border border-black text-black font-bold" {...props}>
|
|
71
|
+
{children}
|
|
72
|
+
</code>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
{article.markdown_content}
|
|
78
|
+
</ReactMarkdown>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (selectedItem.type === 'endpoint') {
|
|
86
|
+
const path = selectedItem.path
|
|
87
|
+
const method = selectedItem.method
|
|
88
|
+
const operation = data.openapi.paths[path]?.[method]
|
|
89
|
+
if (!operation) return null
|
|
90
|
+
|
|
91
|
+
const parameters = operation.parameters || []
|
|
92
|
+
const requestBody = operation.requestBody
|
|
93
|
+
const responses = operation.responses || {}
|
|
94
|
+
const servers = data.openapi.servers || []
|
|
95
|
+
const serverUrl = data.api_server || servers[0]?.url || ''
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className="flex-1 p-10 overflow-y-auto space-y-8 bg-white">
|
|
99
|
+
<div className="surface rounded-none p-8 reveal" style={{ animationDelay: '160ms' }}>
|
|
100
|
+
<div className="flex flex-wrap items-center gap-4 mb-6">
|
|
101
|
+
<span className="chip bg-yellow-400 text-black border-black">
|
|
102
|
+
{method}
|
|
103
|
+
</span>
|
|
104
|
+
<span className="text-3xl font-black tracking-tight text-black font-['Syne']">{path}</span>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{operation.summary && (
|
|
108
|
+
<p className="text-xl font-bold text-black mb-4 border-l-4 border-yellow-400 pl-4">{operation.summary}</p>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
{operation.description && (
|
|
112
|
+
<div className="text-sm text-black/80 leading-relaxed space-y-3 font-medium">
|
|
113
|
+
<ReactMarkdown>{operation.description}</ReactMarkdown>
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{parameters.length > 0 && (
|
|
119
|
+
<section className="surface rounded-none p-8 reveal" style={{ animationDelay: '200ms' }}>
|
|
120
|
+
<div className="text-xs uppercase tracking-[0.3em] text-black/60 mb-6 font-bold">Parameters</div>
|
|
121
|
+
<div className="divide-y-2 divide-black">
|
|
122
|
+
{parameters.map((param, idx) => (
|
|
123
|
+
<div key={idx} className="py-4 grid grid-cols-12 gap-4 text-sm text-black">
|
|
124
|
+
<div className="col-span-3">
|
|
125
|
+
<div className="text-xs uppercase tracking-[0.3em] text-black/50 mb-1 font-bold">{param.in}</div>
|
|
126
|
+
<div className="font-bold text-black text-lg">{param.name}</div>
|
|
127
|
+
</div>
|
|
128
|
+
<div className="col-span-2">
|
|
129
|
+
<span className="chip bg-white text-black">
|
|
130
|
+
{param.required ? 'Required' : 'Optional'}
|
|
131
|
+
</span>
|
|
132
|
+
</div>
|
|
133
|
+
<div className="col-span-7 font-medium">{param.description || '-'}</div>
|
|
134
|
+
</div>
|
|
135
|
+
))}
|
|
136
|
+
</div>
|
|
137
|
+
</section>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
{requestBody && (
|
|
141
|
+
<section className="surface rounded-none p-8 reveal" style={{ animationDelay: '220ms' }}>
|
|
142
|
+
<div className="text-xs uppercase tracking-[0.3em] text-black/60 mb-6 font-bold">Request Body</div>
|
|
143
|
+
<div className="space-y-4">
|
|
144
|
+
{requestBody.content && Object.entries(requestBody.content).map(([contentType, content]) => (
|
|
145
|
+
<div key={contentType}>
|
|
146
|
+
<div className="text-xs uppercase tracking-[0.3em] text-black/50 mb-2 font-bold">Content-Type · {contentType}</div>
|
|
147
|
+
{content.schema && (
|
|
148
|
+
<div className="rounded-none border-2 border-black bg-white">
|
|
149
|
+
<SyntaxHighlighter language="json" style={oneLight} customStyle={{ margin: 0, padding: '18px', fontSize: '15px', backgroundColor: '#fff' }}>
|
|
150
|
+
{JSON.stringify(buildExampleFromSchema(content.schema), null, 2)}
|
|
151
|
+
</SyntaxHighlighter>
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
</section>
|
|
158
|
+
)}
|
|
159
|
+
|
|
160
|
+
{Object.keys(responses).length > 0 && (
|
|
161
|
+
<section className="surface rounded-none p-8 reveal" style={{ animationDelay: '240ms' }}>
|
|
162
|
+
<div className="text-xs uppercase tracking-[0.3em] text-black/60 mb-6 font-bold">Responses</div>
|
|
163
|
+
{Object.entries(responses).map(([statusCode, response]) => (
|
|
164
|
+
<div key={statusCode} className="mb-6 last:mb-0">
|
|
165
|
+
<div className="flex items-center mb-3 gap-3 text-black">
|
|
166
|
+
<span className={`chip ${statusCode.startsWith('2') ? 'bg-green-100' : 'bg-yellow-100'} border-black`}>
|
|
167
|
+
{statusCode}
|
|
168
|
+
</span>
|
|
169
|
+
<span className="text-sm font-bold">{response.description}</span>
|
|
170
|
+
</div>
|
|
171
|
+
{response.content && Object.entries(response.content).map(([contentType, content]) => (
|
|
172
|
+
<div key={contentType}>
|
|
173
|
+
{content.schema && (
|
|
174
|
+
<div className="rounded-none border-2 border-black bg-white">
|
|
175
|
+
<SyntaxHighlighter language="json" style={oneLight} customStyle={{ margin: 0, padding: '18px', fontSize: '15px', backgroundColor: '#fff' }}>
|
|
176
|
+
{JSON.stringify(buildExampleFromSchema(content.schema), null, 2)}
|
|
177
|
+
</SyntaxHighlighter>
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
))}
|
|
182
|
+
</div>
|
|
183
|
+
))}
|
|
184
|
+
</section>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return null
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function buildExampleFromSchema(schema) {
|
|
194
|
+
if (!schema) return {}
|
|
195
|
+
|
|
196
|
+
if (schema.example) return schema.example
|
|
197
|
+
|
|
198
|
+
if (schema.type === 'object' && schema.properties) {
|
|
199
|
+
const example = {}
|
|
200
|
+
Object.entries(schema.properties).forEach(([key, prop]) => {
|
|
201
|
+
example[key] = buildExampleFromSchema(prop)
|
|
202
|
+
})
|
|
203
|
+
return example
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (schema.type === 'array' && schema.items) {
|
|
207
|
+
return [buildExampleFromSchema(schema.items)]
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Default values based on type
|
|
211
|
+
switch (schema.type) {
|
|
212
|
+
case 'string':
|
|
213
|
+
return schema.enum ? schema.enum[0] : 'string'
|
|
214
|
+
case 'number':
|
|
215
|
+
case 'integer':
|
|
216
|
+
return 0
|
|
217
|
+
case 'boolean':
|
|
218
|
+
return false
|
|
219
|
+
case 'array':
|
|
220
|
+
return []
|
|
221
|
+
case 'object':
|
|
222
|
+
return {}
|
|
223
|
+
default:
|
|
224
|
+
return null
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export default ContentPanel
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
const Sidebar = ({ data, activeView, setActiveView, selectedItem, setSelectedItem }) => {
|
|
4
|
+
const openapiPaths = data?.openapi?.paths || {}
|
|
5
|
+
const articles = data?.articles || []
|
|
6
|
+
|
|
7
|
+
const badgeColor = (method) => {
|
|
8
|
+
switch (method) {
|
|
9
|
+
case 'get':
|
|
10
|
+
return 'bg-emerald-100 text-emerald-900 border-emerald-900'
|
|
11
|
+
case 'post':
|
|
12
|
+
return 'bg-blue-100 text-blue-900 border-blue-900'
|
|
13
|
+
case 'put':
|
|
14
|
+
return 'bg-amber-100 text-amber-900 border-amber-900'
|
|
15
|
+
case 'delete':
|
|
16
|
+
return 'bg-red-100 text-red-900 border-red-900'
|
|
17
|
+
default:
|
|
18
|
+
return 'bg-gray-100 text-gray-900 border-gray-900'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const renderApiNavigation = () => (
|
|
23
|
+
Object.entries(openapiPaths).map(([path, methods], sectionIdx) => (
|
|
24
|
+
<div
|
|
25
|
+
key={path}
|
|
26
|
+
className="mb-6 reveal"
|
|
27
|
+
style={{ animationDelay: `${120 + sectionIdx * 60}ms` }}
|
|
28
|
+
>
|
|
29
|
+
<p className="text-[0.6rem] uppercase tracking-[0.35em] text-black/40 mb-2 px-1 font-bold">{path}</p>
|
|
30
|
+
<div className="space-y-2">
|
|
31
|
+
{Object.entries(methods).map(([method, operation], idx) => {
|
|
32
|
+
const isSelected = selectedItem?.type === 'endpoint' &&
|
|
33
|
+
selectedItem?.path === path &&
|
|
34
|
+
selectedItem?.method === method
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<button
|
|
38
|
+
key={`${path}-${method}`}
|
|
39
|
+
onClick={() => setSelectedItem({ type: 'endpoint', path, method })}
|
|
40
|
+
className={`nav-card w-full text-left px-4 py-3 flex items-center gap-3 ${
|
|
41
|
+
isSelected ? 'nav-card--active' : ''
|
|
42
|
+
}`}
|
|
43
|
+
style={{ animationDelay: `${200 + idx * 40}ms` }}
|
|
44
|
+
>
|
|
45
|
+
<span className={`chip ${badgeColor(method)}`}>
|
|
46
|
+
{method}
|
|
47
|
+
</span>
|
|
48
|
+
<span className={`text-[0.8rem] font-bold tracking-[0.05em] ${isSelected ? 'text-black' : 'text-black/80'}`}>
|
|
49
|
+
{operation.summary || path}
|
|
50
|
+
</span>
|
|
51
|
+
</button>
|
|
52
|
+
)
|
|
53
|
+
})}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
))
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const renderArticlesNavigation = () => {
|
|
60
|
+
if (!articles || !Array.isArray(articles) || articles.length === 0) {
|
|
61
|
+
return (
|
|
62
|
+
<div className="text-center p-8 text-black/60">
|
|
63
|
+
<p className="text-sm font-medium">No guides available</p>
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
{articles.map((article, idx) => {
|
|
71
|
+
if (!article || !article.id) return null
|
|
72
|
+
|
|
73
|
+
const isSelected = selectedItem?.type === 'article' && selectedItem?.id === article.id
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<button
|
|
77
|
+
key={article.id}
|
|
78
|
+
onClick={() => {
|
|
79
|
+
if (article && article.id) {
|
|
80
|
+
setSelectedItem({ type: 'article', id: article.id })
|
|
81
|
+
}
|
|
82
|
+
}}
|
|
83
|
+
className={`nav-card w-full text-left px-4 py-3 ${
|
|
84
|
+
isSelected ? 'nav-card--active' : ''
|
|
85
|
+
} reveal`}
|
|
86
|
+
style={{ animationDelay: `${180 + idx * 60}ms` }}
|
|
87
|
+
>
|
|
88
|
+
<span className={`text-[0.82rem] font-bold tracking-[0.05em] ${isSelected ? 'text-black' : 'text-black/80'}`}>
|
|
89
|
+
{article.title || 'Untitled'}
|
|
90
|
+
</span>
|
|
91
|
+
</button>
|
|
92
|
+
)
|
|
93
|
+
})}
|
|
94
|
+
</>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className="w-80 surface border-r-0 border-black/10 flex flex-col h-full relative overflow-hidden" style={{ borderRightWidth: '3px' }}>
|
|
100
|
+
<div className="p-6 border-b-0 border-black/10 relative" style={{ borderBottomWidth: '3px' }}>
|
|
101
|
+
<div className="pill text-[0.65rem] text-black mb-3 bg-white">ElderDocs</div>
|
|
102
|
+
<h1 className="font-black text-3xl tracking-tight text-black uppercase font-['Syne']">API Space</h1>
|
|
103
|
+
<div className="mt-4 mb-2">
|
|
104
|
+
<a
|
|
105
|
+
href="/docs/ui"
|
|
106
|
+
target="_blank"
|
|
107
|
+
rel="noopener noreferrer"
|
|
108
|
+
className="text-xs text-black/60 hover:text-black underline font-medium"
|
|
109
|
+
>
|
|
110
|
+
⚙️ Configure UI
|
|
111
|
+
</a>
|
|
112
|
+
</div>
|
|
113
|
+
<div className="flex gap-2 mt-6">
|
|
114
|
+
<button
|
|
115
|
+
onClick={() => {
|
|
116
|
+
setActiveView('api')
|
|
117
|
+
// Auto-select first endpoint if none selected
|
|
118
|
+
if (!selectedItem || selectedItem.type !== 'endpoint') {
|
|
119
|
+
const firstPath = Object.keys(openapiPaths)[0]
|
|
120
|
+
if (firstPath) {
|
|
121
|
+
const firstMethod = Object.keys(openapiPaths[firstPath])[0]
|
|
122
|
+
setSelectedItem({ type: 'endpoint', path: firstPath, method: firstMethod })
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}}
|
|
126
|
+
className={`flex-1 btn-secondary ${
|
|
127
|
+
activeView === 'api' ? 'bg-yellow-400 !text-black !border-black' : ''
|
|
128
|
+
}`}
|
|
129
|
+
>
|
|
130
|
+
API
|
|
131
|
+
</button>
|
|
132
|
+
<button
|
|
133
|
+
onClick={() => {
|
|
134
|
+
setActiveView('articles')
|
|
135
|
+
// Auto-select first article if none selected or if current selection is not an article
|
|
136
|
+
if (!selectedItem || selectedItem.type !== 'article') {
|
|
137
|
+
if (articles && Array.isArray(articles) && articles.length > 0 && articles[0] && articles[0].id) {
|
|
138
|
+
setSelectedItem({ type: 'article', id: articles[0].id })
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}}
|
|
142
|
+
className={`flex-1 btn-secondary ${
|
|
143
|
+
activeView === 'articles' ? 'bg-yellow-400 !text-black !border-black' : ''
|
|
144
|
+
}`}
|
|
145
|
+
>
|
|
146
|
+
Guides
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="flex-1 overflow-y-auto p-5 space-y-4 relative bg-white">
|
|
151
|
+
{activeView === 'api' ? renderApiNavigation() : renderArticlesNavigation()}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default Sidebar
|