@raystack/chronicle 0.1.0-canary.5a2be79

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.
Files changed (107) hide show
  1. package/bin/chronicle.js +2 -0
  2. package/dist/cli/index.js +9980 -0
  3. package/next.config.mjs +10 -0
  4. package/package.json +63 -0
  5. package/source.config.ts +50 -0
  6. package/src/app/[[...slug]]/layout.tsx +15 -0
  7. package/src/app/[[...slug]]/page.tsx +57 -0
  8. package/src/app/api/apis-proxy/route.ts +59 -0
  9. package/src/app/api/health/route.ts +3 -0
  10. package/src/app/api/search/route.ts +90 -0
  11. package/src/app/apis/[[...slug]]/layout.module.css +22 -0
  12. package/src/app/apis/[[...slug]]/layout.tsx +26 -0
  13. package/src/app/apis/[[...slug]]/page.tsx +57 -0
  14. package/src/app/layout.tsx +26 -0
  15. package/src/app/llms-full.txt/route.ts +18 -0
  16. package/src/app/llms.txt/route.ts +15 -0
  17. package/src/app/providers.tsx +8 -0
  18. package/src/cli/commands/build.ts +32 -0
  19. package/src/cli/commands/dev.ts +33 -0
  20. package/src/cli/commands/init.ts +155 -0
  21. package/src/cli/commands/serve.ts +53 -0
  22. package/src/cli/commands/start.ts +33 -0
  23. package/src/cli/index.ts +21 -0
  24. package/src/cli/utils/config.ts +43 -0
  25. package/src/cli/utils/index.ts +3 -0
  26. package/src/cli/utils/process.ts +7 -0
  27. package/src/cli/utils/resolve.ts +4 -0
  28. package/src/cli/utils/scaffold.ts +131 -0
  29. package/src/components/api/code-snippets.module.css +7 -0
  30. package/src/components/api/code-snippets.tsx +76 -0
  31. package/src/components/api/endpoint-page.module.css +58 -0
  32. package/src/components/api/endpoint-page.tsx +283 -0
  33. package/src/components/api/field-row.module.css +126 -0
  34. package/src/components/api/field-row.tsx +204 -0
  35. package/src/components/api/field-section.module.css +24 -0
  36. package/src/components/api/field-section.tsx +100 -0
  37. package/src/components/api/index.ts +8 -0
  38. package/src/components/api/json-editor.module.css +9 -0
  39. package/src/components/api/json-editor.tsx +61 -0
  40. package/src/components/api/key-value-editor.module.css +13 -0
  41. package/src/components/api/key-value-editor.tsx +62 -0
  42. package/src/components/api/method-badge.module.css +4 -0
  43. package/src/components/api/method-badge.tsx +29 -0
  44. package/src/components/api/response-panel.module.css +8 -0
  45. package/src/components/api/response-panel.tsx +44 -0
  46. package/src/components/common/breadcrumb.tsx +3 -0
  47. package/src/components/common/button.tsx +3 -0
  48. package/src/components/common/callout.module.css +7 -0
  49. package/src/components/common/callout.tsx +27 -0
  50. package/src/components/common/code-block.tsx +3 -0
  51. package/src/components/common/dialog.tsx +3 -0
  52. package/src/components/common/index.ts +10 -0
  53. package/src/components/common/input-field.tsx +3 -0
  54. package/src/components/common/sidebar.tsx +3 -0
  55. package/src/components/common/switch.tsx +3 -0
  56. package/src/components/common/table.tsx +3 -0
  57. package/src/components/common/tabs.tsx +3 -0
  58. package/src/components/mdx/code.module.css +42 -0
  59. package/src/components/mdx/code.tsx +27 -0
  60. package/src/components/mdx/details.module.css +37 -0
  61. package/src/components/mdx/details.tsx +18 -0
  62. package/src/components/mdx/image.tsx +38 -0
  63. package/src/components/mdx/index.tsx +35 -0
  64. package/src/components/mdx/link.tsx +38 -0
  65. package/src/components/mdx/mermaid.module.css +9 -0
  66. package/src/components/mdx/mermaid.tsx +37 -0
  67. package/src/components/mdx/paragraph.module.css +8 -0
  68. package/src/components/mdx/paragraph.tsx +19 -0
  69. package/src/components/mdx/table.tsx +40 -0
  70. package/src/components/ui/breadcrumbs.tsx +72 -0
  71. package/src/components/ui/client-theme-switcher.tsx +18 -0
  72. package/src/components/ui/footer.module.css +27 -0
  73. package/src/components/ui/footer.tsx +30 -0
  74. package/src/components/ui/search.module.css +104 -0
  75. package/src/components/ui/search.tsx +202 -0
  76. package/src/lib/api-routes.ts +120 -0
  77. package/src/lib/config.ts +55 -0
  78. package/src/lib/get-llm-text.ts +10 -0
  79. package/src/lib/index.ts +2 -0
  80. package/src/lib/openapi.ts +188 -0
  81. package/src/lib/remark-unused-directives.ts +30 -0
  82. package/src/lib/schema.ts +99 -0
  83. package/src/lib/snippet-generators.ts +87 -0
  84. package/src/lib/source.ts +67 -0
  85. package/src/themes/default/Layout.module.css +81 -0
  86. package/src/themes/default/Layout.tsx +133 -0
  87. package/src/themes/default/Page.module.css +46 -0
  88. package/src/themes/default/Page.tsx +21 -0
  89. package/src/themes/default/Toc.module.css +48 -0
  90. package/src/themes/default/Toc.tsx +66 -0
  91. package/src/themes/default/font.ts +6 -0
  92. package/src/themes/default/index.ts +13 -0
  93. package/src/themes/paper/ChapterNav.module.css +71 -0
  94. package/src/themes/paper/ChapterNav.tsx +96 -0
  95. package/src/themes/paper/Layout.module.css +33 -0
  96. package/src/themes/paper/Layout.tsx +25 -0
  97. package/src/themes/paper/Page.module.css +174 -0
  98. package/src/themes/paper/Page.tsx +107 -0
  99. package/src/themes/paper/ReadingProgress.module.css +132 -0
  100. package/src/themes/paper/ReadingProgress.tsx +294 -0
  101. package/src/themes/paper/index.ts +8 -0
  102. package/src/themes/registry.ts +14 -0
  103. package/src/types/config.ts +69 -0
  104. package/src/types/content.ts +35 -0
  105. package/src/types/index.ts +3 -0
  106. package/src/types/theme.ts +22 -0
  107. package/tsconfig.json +30 -0
@@ -0,0 +1,283 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback } from 'react'
4
+ import type { OpenAPIV3 } from 'openapi-types'
5
+ import { Flex, Text, Headline, Button, CodeBlock } from '@raystack/apsara'
6
+ import { MethodBadge } from './method-badge'
7
+ import { FieldSection } from './field-section'
8
+ import { KeyValueEditor, type KeyValueEntry } from './key-value-editor'
9
+ import { CodeSnippets } from './code-snippets'
10
+ import { ResponsePanel } from './response-panel'
11
+ import { flattenSchema, generateExampleJson, type SchemaField } from '@/lib/schema'
12
+ import styles from './endpoint-page.module.css'
13
+
14
+ interface EndpointPageProps {
15
+ method: string
16
+ path: string
17
+ operation: OpenAPIV3.OperationObject
18
+ serverUrl: string
19
+ specName: string
20
+ auth?: { type: string; header: string; placeholder?: string }
21
+ }
22
+
23
+ export function EndpointPage({ method, path, operation, serverUrl, specName, auth }: EndpointPageProps) {
24
+ const params = (operation.parameters ?? []) as OpenAPIV3.ParameterObject[]
25
+ const body = getRequestBody(operation.requestBody as OpenAPIV3.RequestBodyObject | undefined)
26
+
27
+ const headerFields = paramsToFields(params.filter((p) => p.in === 'header'))
28
+ const headerLocations = Object.fromEntries(headerFields.map((f) => [f.name, 'header']))
29
+ const pathFields = paramsToFields(params.filter((p) => p.in === 'path'))
30
+ const pathLocations = Object.fromEntries(pathFields.map((f) => [f.name, 'path']))
31
+ const queryFields = paramsToFields(params.filter((p) => p.in === 'query'))
32
+ const queryLocations = Object.fromEntries(queryFields.map((f) => [f.name, 'query']))
33
+ const responses = getResponseSections(operation.responses as Record<string, OpenAPIV3.ResponseObject>)
34
+
35
+ // State for editable fields
36
+ const [customHeaders, setCustomHeaders] = useState<KeyValueEntry[]>(() => {
37
+ const initial: KeyValueEntry[] = []
38
+ if (auth) initial.push({ key: auth.header, value: '' })
39
+ return initial
40
+ })
41
+ const [headerValues, setHeaderValues] = useState<Record<string, unknown>>({})
42
+ const [pathValues, setPathValues] = useState<Record<string, unknown>>({})
43
+ const [queryValues, setQueryValues] = useState<Record<string, unknown>>({})
44
+ const [bodyValues, setBodyValues] = useState<Record<string, unknown>>(() => {
45
+ try { return body?.jsonExample ? JSON.parse(body.jsonExample) : {} }
46
+ catch { return {} }
47
+ })
48
+ const [bodyJsonStr, setBodyJsonStr] = useState(body?.jsonExample ?? '{}')
49
+ const [responseBody, setResponseBody] = useState<{ status: number; statusText: string; body: unknown } | null>(null)
50
+ const [loading, setLoading] = useState(false)
51
+
52
+ // Two-way sync: fields → JSON
53
+ const handleBodyValuesChange = useCallback((values: Record<string, unknown>) => {
54
+ setBodyValues(values)
55
+ setBodyJsonStr(JSON.stringify(values, null, 2))
56
+ }, [])
57
+
58
+ // Two-way sync: JSON → fields
59
+ const handleBodyJsonChange = useCallback((jsonStr: string) => {
60
+ setBodyJsonStr(jsonStr)
61
+ try {
62
+ setBodyValues(JSON.parse(jsonStr))
63
+ } catch { /* ignore invalid JSON while typing */ }
64
+ }, [])
65
+
66
+ // Try it handler
67
+ const handleTryIt = useCallback(async () => {
68
+ setLoading(true)
69
+ setResponseBody(null)
70
+
71
+ let resolvedPath = path
72
+ for (const [key, value] of Object.entries(pathValues)) {
73
+ resolvedPath = resolvedPath.replace(`{${key}}`, encodeURIComponent(String(value)))
74
+ }
75
+
76
+ const queryEntries = Object.entries(queryValues).filter(([, v]) => v !== undefined && v !== '')
77
+ const queryString = queryEntries
78
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
79
+ .join('&')
80
+ const fullPath = queryString ? `${resolvedPath}?${queryString}` : resolvedPath
81
+
82
+ const reqHeaders: Record<string, string> = {}
83
+ for (const [key, value] of Object.entries(headerValues)) {
84
+ if (value !== undefined && value !== null && value !== '') reqHeaders[key] = String(value)
85
+ }
86
+ for (const entry of customHeaders) {
87
+ if (entry.key && entry.value) reqHeaders[entry.key] = entry.value
88
+ }
89
+ if (body && bodyJsonStr) {
90
+ reqHeaders['Content-Type'] = body.contentType ?? 'application/json'
91
+ }
92
+
93
+ try {
94
+ const res = await fetch('/api/apis-proxy', {
95
+ method: 'POST',
96
+ headers: { 'Content-Type': 'application/json' },
97
+ body: JSON.stringify({
98
+ specName,
99
+ method,
100
+ path: fullPath,
101
+ headers: reqHeaders,
102
+ body: body ? bodyValues : undefined,
103
+ }),
104
+ })
105
+ const data = await res.json()
106
+ if (data.status !== undefined) {
107
+ setResponseBody(data)
108
+ } else {
109
+ setResponseBody({ status: res.status, statusText: res.statusText, body: data.error ?? data })
110
+ }
111
+ } catch (err) {
112
+ console.error('API request failed:', err)
113
+ setResponseBody({ status: 0, statusText: 'Error', body: 'Failed to send request' })
114
+ } finally {
115
+ setLoading(false)
116
+ }
117
+ }, [specName, method, path, pathValues, queryValues, headerValues, customHeaders, bodyValues, bodyJsonStr, body])
118
+
119
+ // Snippet display values
120
+ const fullUrl = '{domain}' + path
121
+ const snippetHeaders: Record<string, string> = {}
122
+ if (auth) {
123
+ snippetHeaders[auth.header] = auth.placeholder ?? 'YOUR_API_KEY'
124
+ }
125
+ if (body) {
126
+ snippetHeaders['Content-Type'] = body.contentType ?? 'application/json'
127
+ }
128
+
129
+ return (
130
+ <div className={styles.layout}>
131
+ <Flex direction="column" className={styles.left}>
132
+ {operation.summary && (
133
+ <Headline size="small" as="h1" className={styles.title}>{operation.summary}</Headline>
134
+ )}
135
+ {operation.description && (
136
+ <Text size={3} className={styles.description}>{operation.description}</Text>
137
+ )}
138
+
139
+ <Flex align="center" className={styles.methodPath}>
140
+ <MethodBadge method={method} />
141
+ <Text size={3} className={styles.path}>{path}</Text>
142
+ <Button variant="solid" size="small" className={styles.tryButton} onClick={handleTryIt} disabled={loading}>
143
+ {loading ? 'Sending...' : 'Send'}
144
+ </Button>
145
+ </Flex>
146
+
147
+ <FieldSection
148
+ title="Headers"
149
+ fields={headerFields}
150
+ locations={headerLocations}
151
+ editable
152
+ values={headerValues}
153
+ onValuesChange={setHeaderValues}
154
+ >
155
+ <KeyValueEditor entries={customHeaders} onChange={setCustomHeaders} />
156
+ </FieldSection>
157
+ <FieldSection
158
+ title="Path"
159
+ fields={pathFields}
160
+ locations={pathLocations}
161
+ editable
162
+ values={pathValues}
163
+ onValuesChange={setPathValues}
164
+ />
165
+ <FieldSection
166
+ title="Query Parameters"
167
+ fields={queryFields}
168
+ locations={queryLocations}
169
+ editable
170
+ values={queryValues}
171
+ onValuesChange={setQueryValues}
172
+ />
173
+ {body && (
174
+ <FieldSection
175
+ title="Body"
176
+ label={body?.contentType}
177
+ fields={body?.fields ?? []}
178
+ jsonExample={bodyJsonStr}
179
+ editableJson
180
+ onJsonChange={handleBodyJsonChange}
181
+ alwaysShow
182
+ editable
183
+ values={bodyValues}
184
+ onValuesChange={handleBodyValuesChange}
185
+ />
186
+ )}
187
+
188
+ {responses.map((resp) => (
189
+ <FieldSection
190
+ key={resp.status}
191
+ title={`${resp.status}${resp.description ? ` — ${resp.description}` : ''}`}
192
+ fields={resp.fields}
193
+ jsonExample={resp.jsonExample}
194
+ />
195
+ ))}
196
+ </Flex>
197
+ <Flex direction="column" className={styles.right}>
198
+ <CodeSnippets
199
+ method={method}
200
+ url={fullUrl}
201
+ headers={snippetHeaders}
202
+ body={body ? bodyJsonStr : undefined}
203
+ />
204
+ <ResponsePanel responses={responses} />
205
+ {responseBody && (
206
+ <Flex direction="column" gap="small">
207
+ <Text size={3} weight="medium">
208
+ Response — {responseBody.status} {responseBody.statusText}
209
+ </Text>
210
+ <CodeBlock>
211
+ <CodeBlock.Header>
212
+ <CodeBlock.CopyButton />
213
+ </CodeBlock.Header>
214
+ <CodeBlock.Content>
215
+ <CodeBlock.Code language="json">
216
+ {typeof responseBody.body === 'string'
217
+ ? (responseBody.body || 'No response body')
218
+ : (JSON.stringify(responseBody.body, null, 2) ?? 'No response body')}
219
+ </CodeBlock.Code>
220
+ </CodeBlock.Content>
221
+ </CodeBlock>
222
+ </Flex>
223
+ )}
224
+ </Flex>
225
+ </div>
226
+ )
227
+ }
228
+
229
+ function paramsToFields(params: OpenAPIV3.ParameterObject[]): SchemaField[] {
230
+ return params.map((p) => {
231
+ const schema = (p.schema ?? {}) as OpenAPIV3.SchemaObject
232
+ return {
233
+ name: p.name,
234
+ type: schema.type ? String(schema.type) : 'string',
235
+ required: p.required ?? false,
236
+ description: p.description,
237
+ default: schema.default,
238
+ }
239
+ })
240
+ }
241
+
242
+ interface RequestBody {
243
+ contentType: string
244
+ fields: SchemaField[]
245
+ jsonExample: string
246
+ }
247
+
248
+ function getRequestBody(body: OpenAPIV3.RequestBodyObject | undefined): RequestBody | null {
249
+ if (!body?.content) return null
250
+ const contentType = Object.keys(body.content)[0]
251
+ if (!contentType) return null
252
+ const schema = body.content[contentType]?.schema as OpenAPIV3.SchemaObject | undefined
253
+ if (!schema) return null
254
+ return {
255
+ contentType,
256
+ fields: flattenSchema(schema),
257
+ jsonExample: JSON.stringify(generateExampleJson(schema), null, 2),
258
+ }
259
+ }
260
+
261
+ interface ResponseSection {
262
+ status: string
263
+ description?: string
264
+ fields: SchemaField[]
265
+ jsonExample?: string
266
+ }
267
+
268
+ function getResponseSections(responses: Record<string, OpenAPIV3.ResponseObject>): ResponseSection[] {
269
+ return Object.entries(responses).map(([status, resp]) => {
270
+ const content = resp.content ?? {}
271
+ const contentType = Object.keys(content)[0]
272
+ const schema = contentType
273
+ ? (content[contentType]?.schema as OpenAPIV3.SchemaObject | undefined)
274
+ : undefined
275
+
276
+ return {
277
+ status,
278
+ description: resp.description,
279
+ fields: schema ? flattenSchema(schema) : [],
280
+ jsonExample: schema ? JSON.stringify(generateExampleJson(schema), null, 2) : undefined,
281
+ }
282
+ })
283
+ }
@@ -0,0 +1,126 @@
1
+ .row {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--rs-space-2);
5
+ padding: var(--rs-space-4) 0;
6
+ }
7
+
8
+ .row + .row {
9
+ border-top: 1px solid var(--rs-color-border-base-primary);
10
+ }
11
+
12
+ .main {
13
+ gap: var(--rs-space-2);
14
+ }
15
+
16
+ .badges {
17
+ gap: var(--rs-space-3);
18
+ flex-wrap: wrap;
19
+ }
20
+
21
+ .name {
22
+ font-family: monospace;
23
+ font-size: 13px;
24
+ color: var(--rs-color-foreground-base-primary);
25
+ }
26
+
27
+ .type {
28
+ font-family: monospace;
29
+ font-size: 12px;
30
+ padding: 1px var(--rs-space-2);
31
+ border-radius: 4px;
32
+ background: var(--rs-color-background-neutral-secondary);
33
+ color: var(--rs-color-foreground-base-secondary);
34
+ }
35
+
36
+ .location {
37
+ font-size: 12px;
38
+ padding: 1px var(--rs-space-2);
39
+ border-radius: 4px;
40
+ background: var(--rs-color-background-neutral-secondary);
41
+ color: var(--rs-color-foreground-base-secondary);
42
+ }
43
+
44
+ .required {
45
+ font-size: 11px;
46
+ padding: 1px var(--rs-space-2);
47
+ border-radius: 4px;
48
+ background: var(--rs-color-background-danger-primary);
49
+ color: var(--rs-color-foreground-danger-primary);
50
+ }
51
+
52
+ .description {
53
+ color: var(--rs-color-foreground-base-secondary);
54
+ font-size: 13px;
55
+ }
56
+
57
+ .example {
58
+ color: var(--rs-color-foreground-base-secondary);
59
+ font-size: 12px;
60
+ }
61
+
62
+ .example code {
63
+ font-family: monospace;
64
+ background: var(--rs-color-background-neutral-secondary);
65
+ padding: 1px var(--rs-space-2);
66
+ border-radius: 3px;
67
+ }
68
+
69
+ .accordion {
70
+ border: none;
71
+ }
72
+
73
+ .accordion :global([class*="accordion-header"]) {
74
+ margin: 0;
75
+ }
76
+
77
+ .accordion button {
78
+ min-height: unset;
79
+ padding: 0;
80
+ border: none;
81
+ background: transparent;
82
+ box-shadow: none;
83
+ }
84
+
85
+ .accordion button:hover,
86
+ .accordion button:focus-visible {
87
+ background: transparent;
88
+ }
89
+
90
+ .accordion :global([class*="accordion-content-inner"]) {
91
+ padding: var(--rs-space-3) 0 0 0;
92
+ border: none;
93
+ box-shadow: none;
94
+ }
95
+
96
+ .children {
97
+ display: flex;
98
+ flex-direction: column;
99
+ padding-left: var(--rs-space-5);
100
+ width: 100%;
101
+ }
102
+
103
+ .trigger {
104
+ color: var(--rs-color-foreground-base-secondary);
105
+ }
106
+
107
+ .fieldInfo {
108
+ flex: 1;
109
+ min-width: 0;
110
+ }
111
+
112
+ .fieldInput {
113
+ flex: 1;
114
+ min-width: 0;
115
+ }
116
+
117
+ .arrayItems {
118
+ gap: var(--rs-space-3);
119
+ }
120
+
121
+ .arrayItem {
122
+ align-items: center;
123
+ border: 1px solid var(--rs-color-border-base-primary);
124
+ border-radius: 8px;
125
+ padding: var(--rs-space-3) var(--rs-space-4);
126
+ }
@@ -0,0 +1,204 @@
1
+ 'use client'
2
+
3
+ import { Flex, Text, Accordion, InputField, Switch, Select, IconButton } from '@raystack/apsara'
4
+ import { TrashIcon, PlusIcon } from '@heroicons/react/24/outline'
5
+ import type { SchemaField } from '@/lib/schema'
6
+ import styles from './field-row.module.css'
7
+
8
+ interface FieldRowProps {
9
+ field: SchemaField
10
+ location?: string
11
+ editable?: boolean
12
+ value?: unknown
13
+ onChange?: (name: string, value: unknown) => void
14
+ }
15
+
16
+ export function FieldRow({ field, location, editable, value, onChange }: FieldRowProps) {
17
+ const hasChildren = field.children && field.children.length > 0
18
+ const isArray = field.type.endsWith('[]')
19
+
20
+ const label = (
21
+ <Flex align="center" className={styles.badges}>
22
+ <Text size={2} className={styles.name}>{field.name}</Text>
23
+ <Text size={1} className={styles.type}>{field.type}</Text>
24
+ {location && <Text size={1} className={styles.location}>{location}</Text>}
25
+ {field.required && <Text size={1} className={styles.required}>required</Text>}
26
+ </Flex>
27
+ )
28
+
29
+ if (hasChildren && !isArray) {
30
+ const objValue = (value ?? {}) as Record<string, unknown>
31
+ return (
32
+ <div className={styles.row}>
33
+ <Accordion collapsible className={styles.accordion}>
34
+ <Accordion.Item value={field.name}>
35
+ <Accordion.Trigger className={styles.trigger}>{label}</Accordion.Trigger>
36
+ <Accordion.Content>
37
+ <div className={styles.children}>
38
+ {field.children!.map((child) => (
39
+ <FieldRow
40
+ key={child.name}
41
+ field={child}
42
+ editable={editable}
43
+ value={objValue[child.name]}
44
+ onChange={editable ? (name, val) => {
45
+ onChange?.(field.name, { ...objValue, [name]: val })
46
+ } : undefined}
47
+ />
48
+ ))}
49
+ </div>
50
+ </Accordion.Content>
51
+ </Accordion.Item>
52
+ </Accordion>
53
+ </div>
54
+ )
55
+ }
56
+
57
+ if (isArray && editable) {
58
+ const items = (Array.isArray(value) ? value : []) as unknown[]
59
+ const itemChildren = field.children
60
+
61
+ return (
62
+ <div className={styles.row}>
63
+ <Flex direction="column" className={styles.main}>
64
+ <Flex align="center" justify="between">
65
+ {label}
66
+ <IconButton size={1} onClick={() => {
67
+ const newItem = itemChildren ? {} : ''
68
+ onChange?.(field.name, [...items, newItem])
69
+ }}>
70
+ <PlusIcon width={14} height={14} />
71
+ </IconButton>
72
+ </Flex>
73
+ {field.description && <Text size={2} className={styles.description}>{field.description}</Text>}
74
+ <Flex direction="column" className={styles.arrayItems}>
75
+ {items.map((item, i) => (
76
+ <Flex key={i} align="start" gap="small" className={styles.arrayItem}>
77
+ {itemChildren ? (
78
+ <Flex direction="column" className={styles.children}>
79
+ {itemChildren.map((child) => (
80
+ <FieldRow
81
+ key={child.name}
82
+ field={child}
83
+ editable
84
+ value={(item as Record<string, unknown>)?.[child.name]}
85
+ onChange={(name, val) => {
86
+ const updated = [...items]
87
+ updated[i] = { ...(updated[i] as Record<string, unknown>), [name]: val }
88
+ onChange?.(field.name, updated)
89
+ }}
90
+ />
91
+ ))}
92
+ </Flex>
93
+ ) : (
94
+ <EditableInput
95
+ field={{
96
+ ...field,
97
+ name: `${field.name}[${i}]`,
98
+ type: field.type.replace('[]', ''),
99
+ }}
100
+ value={item}
101
+ onChange={(_, val) => {
102
+ const updated = [...items]
103
+ updated[i] = val
104
+ onChange?.(field.name, updated)
105
+ }}
106
+ />
107
+ )}
108
+ <IconButton size={1} onClick={() => {
109
+ const updated = items.filter((_, j) => j !== i)
110
+ onChange?.(field.name, updated)
111
+ }}>
112
+ <TrashIcon width={14} height={14} />
113
+ </IconButton>
114
+ </Flex>
115
+ ))}
116
+ </Flex>
117
+ </Flex>
118
+ </div>
119
+ )
120
+ }
121
+
122
+ // Leaf field — inline layout
123
+ return (
124
+ <div className={styles.row}>
125
+ <Flex align="center" gap="medium">
126
+ <Flex direction="column" gap="extra-small" className={styles.fieldInfo}>
127
+ {label}
128
+ {field.description && <Text size={2} className={styles.description}>{field.description}</Text>}
129
+ </Flex>
130
+ {editable ? (
131
+ <div className={styles.fieldInput}>
132
+ <EditableInput field={field} value={value} onChange={onChange} />
133
+ </div>
134
+ ) : (
135
+ field.default !== undefined && (
136
+ <Text size={1} className={styles.example}>
137
+ Default: <code>{JSON.stringify(field.default)}</code>
138
+ </Text>
139
+ )
140
+ )}
141
+ </Flex>
142
+ </div>
143
+ )
144
+ }
145
+
146
+ function EditableInput({
147
+ field,
148
+ value,
149
+ onChange,
150
+ }: {
151
+ field: SchemaField
152
+ value: unknown
153
+ onChange?: (name: string, value: unknown) => void
154
+ }) {
155
+ if (field.enum) {
156
+ const enumMap = new Map(field.enum.map((opt) => [String(opt), opt]))
157
+ return (
158
+ <Select value={String(value ?? '')} onValueChange={(v) => onChange?.(field.name, enumMap.get(v) ?? v)}>
159
+ <Select.Trigger size="small">
160
+ <Select.Value placeholder={`Select ${field.name}`} />
161
+ </Select.Trigger>
162
+ <Select.Content>
163
+ {field.enum.map((opt) => (
164
+ <Select.Item key={String(opt)} value={String(opt)}>
165
+ {String(opt)}
166
+ </Select.Item>
167
+ ))}
168
+ </Select.Content>
169
+ </Select>
170
+ )
171
+ }
172
+
173
+ const baseType = field.type.replace('[]', '').replace(/\(.*\)/, '')
174
+
175
+ if (baseType === 'boolean') {
176
+ return (
177
+ <Switch
178
+ checked={Boolean(value)}
179
+ onCheckedChange={(checked) => onChange?.(field.name, checked)}
180
+ />
181
+ )
182
+ }
183
+
184
+ if (baseType === 'integer' || baseType === 'number') {
185
+ return (
186
+ <InputField
187
+ size="small"
188
+ type="number"
189
+ placeholder={field.description ?? field.name}
190
+ value={String(value ?? '')}
191
+ onChange={(e) => onChange?.(field.name, Number(e.target.value))}
192
+ />
193
+ )
194
+ }
195
+
196
+ return (
197
+ <InputField
198
+ size="small"
199
+ placeholder={field.description ?? field.name}
200
+ value={String(value ?? '')}
201
+ onChange={(e) => onChange?.(field.name, e.target.value)}
202
+ />
203
+ )
204
+ }
@@ -0,0 +1,24 @@
1
+ .header {
2
+ padding-bottom: var(--rs-space-3);
3
+ }
4
+
5
+ .label {
6
+ font-family: monospace;
7
+ color: var(--rs-color-foreground-base-secondary);
8
+ font-size: 13px;
9
+ }
10
+
11
+ .separator {
12
+ height: 1px;
13
+ background: var(--rs-color-border-base-primary);
14
+ }
15
+
16
+ .tabs {
17
+ margin-top: var(--rs-space-3);
18
+ }
19
+
20
+ .noFields {
21
+ color: var(--rs-color-foreground-base-secondary);
22
+ padding: var(--rs-space-5);
23
+ }
24
+