elder_docs 0.1.0 → 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,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