@raystack/chronicle 0.7.3 → 0.8.0

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 (46) hide show
  1. package/dist/cli/index.js +4 -1
  2. package/package.json +2 -1
  3. package/src/components/api/api-code-snippet.module.css +23 -0
  4. package/src/components/api/api-code-snippet.tsx +64 -0
  5. package/src/components/api/api-field-list.module.css +76 -0
  6. package/src/components/api/api-field-list.tsx +91 -0
  7. package/src/components/api/api-overview.module.css +65 -0
  8. package/src/components/api/api-overview.tsx +216 -0
  9. package/src/components/api/api-response-panel.module.css +62 -0
  10. package/src/components/api/api-response-panel.tsx +54 -0
  11. package/src/components/api/index.ts +5 -6
  12. package/src/components/api/json-editor.tsx +8 -8
  13. package/src/components/api/method-badge.tsx +2 -2
  14. package/src/components/api/playground-dialog.module.css +342 -0
  15. package/src/components/api/playground-dialog.tsx +583 -0
  16. package/src/lib/api-routes.ts +37 -8
  17. package/src/lib/openapi.ts +26 -0
  18. package/src/lib/schema.ts +45 -3
  19. package/src/lib/source.ts +57 -23
  20. package/src/lib/use-api-operation.ts +15 -0
  21. package/src/pages/ApiLayout.module.css +1 -0
  22. package/src/pages/ApiPage.tsx +7 -38
  23. package/src/pages/DocsPage.tsx +40 -1
  24. package/src/server/api/apis-proxy.ts +8 -1
  25. package/src/server/entry-server.tsx +2 -2
  26. package/src/server/routes/[...slug].md.ts +1 -0
  27. package/src/server/routes/apis/[...slug].md.ts +181 -0
  28. package/src/server/vite-config.ts +2 -0
  29. package/src/themes/default/Layout.module.css +53 -0
  30. package/src/themes/default/Layout.tsx +162 -11
  31. package/src/themes/paper/Page.module.css +7 -2
  32. package/src/themes/paper/Page.tsx +8 -6
  33. package/src/themes/paper/Skeleton.tsx +9 -0
  34. package/src/types/config.ts +1 -0
  35. package/src/components/api/code-snippets.module.css +0 -7
  36. package/src/components/api/code-snippets.tsx +0 -76
  37. package/src/components/api/endpoint-page.module.css +0 -58
  38. package/src/components/api/endpoint-page.tsx +0 -283
  39. package/src/components/api/field-row.module.css +0 -126
  40. package/src/components/api/field-row.tsx +0 -204
  41. package/src/components/api/field-section.module.css +0 -24
  42. package/src/components/api/field-section.tsx +0 -100
  43. package/src/components/api/key-value-editor.module.css +0 -13
  44. package/src/components/api/key-value-editor.tsx +0 -62
  45. package/src/components/api/response-panel.module.css +0 -8
  46. package/src/components/api/response-panel.tsx +0 -44
package/dist/cli/index.js CHANGED
@@ -337,6 +337,7 @@ async function createViteConfig(options) {
337
337
  const contentMirror = path6.resolve(packageRoot, ".content");
338
338
  return {
339
339
  root: packageRoot,
340
+ publicDir: path6.resolve(projectRoot, "public"),
340
341
  configFile: false,
341
342
  plugins: [
342
343
  nitro({
@@ -420,6 +421,7 @@ async function createViteConfig(options) {
420
421
  },
421
422
  nitro: {
422
423
  logLevel: 2,
424
+ publicAssets: [{ dir: path6.resolve(projectRoot, "public") }],
423
425
  output: {
424
426
  dir: resolveOutputDir(projectRoot, preset)
425
427
  }
@@ -514,7 +516,8 @@ var contentEntrySchema = z.object({
514
516
  dir: dirNameSchema,
515
517
  label: z.string().min(1),
516
518
  description: z.string().optional(),
517
- icon: z.string().optional()
519
+ icon: z.string().optional(),
520
+ index_page: z.string().optional()
518
521
  });
519
522
  var badgeVariantSchema = z.enum([
520
523
  "accent",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raystack/chronicle",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
4
4
  "description": "Config-driven documentation framework",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -46,6 +46,7 @@
46
46
  "@opentelemetry/resources": "^2.6.1",
47
47
  "@opentelemetry/sdk-metrics": "^2.6.1",
48
48
  "@opentelemetry/semantic-conventions": "^1.40.0",
49
+ "@radix-ui/react-icons": "^1.3.2",
49
50
  "@raystack/apsara": "1.0.0-rc.4",
50
51
  "@shikijs/rehype": "^4.0.2",
51
52
  "@vitejs/plugin-react": "^6.0.1",
@@ -0,0 +1,23 @@
1
+ .container {
2
+ border: 0.5px solid var(--rs-color-border-base-primary);
3
+ border-radius: var(--rs-radius-2);
4
+ overflow: hidden;
5
+ width: 100%;
6
+ }
7
+
8
+ .header {
9
+ justify-content: space-between;
10
+ }
11
+
12
+ .title {
13
+ font-size: var(--rs-font-size-regular);
14
+ font-weight: var(--rs-font-weight-regular);
15
+ line-height: var(--rs-line-height-regular);
16
+ letter-spacing: var(--rs-letter-spacing-regular);
17
+ color: var(--rs-color-foreground-base-primary);
18
+ white-space: nowrap;
19
+ }
20
+
21
+ .body {
22
+ background: var(--rs-color-background-base-primary);
23
+ }
@@ -0,0 +1,64 @@
1
+ 'use client'
2
+
3
+ import { useMemo, useState } from 'react'
4
+ import { CodeBlock, Flex } from '@raystack/apsara'
5
+ import {
6
+ generateCurl,
7
+ generatePython,
8
+ generateGo,
9
+ generateTypeScript,
10
+ } from '@/lib/snippet-generators'
11
+ import styles from './api-code-snippet.module.css'
12
+
13
+ interface ApiCodeSnippetProps {
14
+ title: string
15
+ method: string
16
+ url: string
17
+ headers: Record<string, string>
18
+ body?: string
19
+ }
20
+
21
+ const languages = [
22
+ { value: 'curl', label: 'cURL', lang: 'bash', generate: generateCurl },
23
+ { value: 'python', label: 'Python', lang: 'python', generate: generatePython },
24
+ { value: 'go', label: 'Go', lang: 'go', generate: generateGo },
25
+ { value: 'typescript', label: 'TypeScript', lang: 'typescript', generate: generateTypeScript },
26
+ ]
27
+
28
+ export function ApiCodeSnippet({ title, method, url, headers, body }: ApiCodeSnippetProps) {
29
+ const [selected, setSelected] = useState('curl')
30
+ const current = languages.find((l) => l.value === selected) ?? languages[0]
31
+
32
+ const code = useMemo(
33
+ () => current.generate({ method, url, headers, body }),
34
+ [current.generate, method, url, headers, body],
35
+ )
36
+
37
+ return (
38
+ <CodeBlock
39
+ value={selected}
40
+ onValueChange={setSelected}
41
+ className={styles.container}
42
+ >
43
+ <CodeBlock.Header className={styles.header}>
44
+ <CodeBlock.Label className={styles.title}>{title}</CodeBlock.Label>
45
+ <Flex align='center' gap={4}>
46
+ <CodeBlock.LanguageSelect>
47
+ <CodeBlock.LanguageSelectTrigger />
48
+ <CodeBlock.LanguageSelectContent>
49
+ {languages.map((l) => (
50
+ <CodeBlock.LanguageSelectItem key={l.value} value={l.value}>
51
+ {l.label}
52
+ </CodeBlock.LanguageSelectItem>
53
+ ))}
54
+ </CodeBlock.LanguageSelectContent>
55
+ </CodeBlock.LanguageSelect>
56
+ <CodeBlock.CopyButton />
57
+ </Flex>
58
+ </CodeBlock.Header>
59
+ <CodeBlock.Content className={styles.body}>
60
+ <CodeBlock.Code value={selected} language={current.lang}>{code}</CodeBlock.Code>
61
+ </CodeBlock.Content>
62
+ </CodeBlock>
63
+ )
64
+ }
@@ -0,0 +1,76 @@
1
+ .sectionTitle {
2
+ font-size: var(--rs-font-size-large);
3
+ font-weight: var(--rs-font-weight-medium);
4
+ line-height: var(--rs-line-height-large);
5
+ letter-spacing: var(--rs-letter-spacing-large);
6
+ color: var(--rs-color-foreground-base-primary);
7
+ }
8
+
9
+ .fieldItem {
10
+ padding-bottom: var(--rs-space-5);
11
+ border-bottom: 0.5px solid var(--rs-color-border-base-primary);
12
+ }
13
+
14
+ .fieldItem:last-child {
15
+ border-bottom: none;
16
+ padding-bottom: 0;
17
+ }
18
+
19
+ .fieldType {
20
+ font-size: var(--rs-font-size-small);
21
+ line-height: var(--rs-line-height-small);
22
+ letter-spacing: var(--rs-letter-spacing-small);
23
+ color: var(--rs-color-foreground-base-secondary);
24
+ }
25
+
26
+ .fieldDescription {
27
+ font-size: var(--rs-font-size-small);
28
+ line-height: var(--rs-line-height-small);
29
+ letter-spacing: var(--rs-letter-spacing-small);
30
+ color: var(--rs-color-foreground-base-secondary);
31
+ }
32
+
33
+ .fieldExample {
34
+ font-size: var(--rs-font-size-small);
35
+ line-height: var(--rs-line-height-small);
36
+ color: var(--rs-color-foreground-base-tertiary);
37
+ }
38
+
39
+ .fieldExample code {
40
+ font-family: var(--rs-font-mono);
41
+ font-size: var(--rs-font-size-mono-small);
42
+ background: var(--rs-color-background-neutral-secondary);
43
+ padding: 1px var(--rs-space-2);
44
+ border-radius: 3px;
45
+ }
46
+
47
+ .statusDescription {
48
+ font-size: var(--rs-font-size-regular);
49
+ line-height: var(--rs-line-height-regular);
50
+ color: var(--rs-color-foreground-base-primary);
51
+ }
52
+
53
+ .expandButton {
54
+ padding: var(--rs-space-3) var(--rs-space-4);
55
+ border: 1px solid var(--rs-color-border-base-primary);
56
+ border-radius: var(--rs-radius-2);
57
+ background: var(--rs-color-background-base-secondary);
58
+ cursor: pointer;
59
+ width: 100%;
60
+ color: var(--rs-color-foreground-base-primary);
61
+ }
62
+
63
+ .expandButton:hover {
64
+ background: var(--rs-color-background-neutral-secondary);
65
+ }
66
+
67
+ .expandLabel {
68
+ font-size: var(--rs-font-size-small);
69
+ line-height: var(--rs-line-height-small);
70
+ color: var(--rs-color-foreground-base-primary);
71
+ }
72
+
73
+ .childFields {
74
+ padding-left: var(--rs-space-5);
75
+ margin-top: var(--rs-space-3);
76
+ }
@@ -0,0 +1,91 @@
1
+ 'use client'
2
+
3
+ import { useState, type ReactNode } from 'react'
4
+ import { Badge, Flex } from '@raystack/apsara'
5
+ import { ChevronRightIcon, ChevronDownIcon } from '@radix-ui/react-icons'
6
+ import type { SchemaField } from '@/lib/schema'
7
+ import styles from './api-field-list.module.css'
8
+
9
+ interface ApiFieldSectionProps {
10
+ title: string
11
+ fields: SchemaField[]
12
+ headerRight?: ReactNode
13
+ description?: string
14
+ }
15
+
16
+ export function ApiFieldSection({ title, fields, headerRight, description }: ApiFieldSectionProps) {
17
+ if (fields.length === 0 && !description) return null
18
+
19
+ return (
20
+ <Flex direction="column" gap={6}>
21
+ <Flex align="center" justify="between">
22
+ <span className={styles.sectionTitle}>{title}</span>
23
+ {headerRight && (
24
+ <Flex align="center" gap={3}>
25
+ {headerRight}
26
+ </Flex>
27
+ )}
28
+ </Flex>
29
+ {description && <span className={styles.statusDescription}>{description}</span>}
30
+ <Flex direction="column" gap={5}>
31
+ {fields.map((field) => (
32
+ <FieldItem key={field.name} field={field} />
33
+ ))}
34
+ </Flex>
35
+ </Flex>
36
+ )
37
+ }
38
+
39
+ function FieldItem({ field }: { field: SchemaField }) {
40
+ const hasChildren = field.children && field.children.length > 0
41
+
42
+ return (
43
+ <Flex direction="column" gap={4} className={styles.fieldItem}>
44
+ <Flex align="center" gap={3}>
45
+ <Badge variant="neutral" size="micro">{field.name}</Badge>
46
+ <span className={styles.fieldType}>{field.type}</span>
47
+ {field.required && <Badge variant="danger" size="micro">required</Badge>}
48
+ </Flex>
49
+ {field.description && (
50
+ <span className={styles.fieldDescription}>{field.description}</span>
51
+ )}
52
+ {field.example !== undefined && (
53
+ <span className={styles.fieldExample}>Example: <code>{JSON.stringify(field.example)}</code></span>
54
+ )}
55
+ {hasChildren && <ExpandableChildren field={field} />}
56
+ </Flex>
57
+ )
58
+ }
59
+
60
+ function ExpandableChildren({ field }: { field: SchemaField }) {
61
+ const [expanded, setExpanded] = useState(false)
62
+
63
+ return (
64
+ <Flex direction="column">
65
+ <Flex
66
+ align="center"
67
+ justify="between"
68
+ className={styles.expandButton}
69
+ onClick={() => setExpanded(!expanded)}
70
+ role="button"
71
+ tabIndex={0}
72
+ >
73
+ <span className={styles.expandLabel}>
74
+ {expanded ? 'Hide' : 'Show'} child attributes
75
+ </span>
76
+ {expanded ? (
77
+ <ChevronDownIcon width={16} height={16} />
78
+ ) : (
79
+ <ChevronRightIcon width={16} height={16} />
80
+ )}
81
+ </Flex>
82
+ {expanded && (
83
+ <Flex direction="column" gap={5} className={styles.childFields}>
84
+ {field.children!.map((child) => (
85
+ <FieldItem key={child.name} field={child} />
86
+ ))}
87
+ </Flex>
88
+ )}
89
+ </Flex>
90
+ )
91
+ }
@@ -0,0 +1,65 @@
1
+ .layout {
2
+ align-items: flex-start;
3
+ justify-content: space-between;
4
+ padding-left: var(--rs-space-9);
5
+ padding-right: var(--rs-space-9);
6
+ width: 100%;
7
+ }
8
+
9
+ .left {
10
+ min-width: 0;
11
+ flex: 0 1 545px;
12
+ }
13
+
14
+ .right {
15
+ min-width: 376px;
16
+ max-width: 500px;
17
+ width: 100%;
18
+ }
19
+
20
+ .title {
21
+ font-family: var(--rs-font-title);
22
+ font-size: var(--rs-font-size-t3);
23
+ font-weight: var(--rs-font-weight-medium);
24
+ line-height: var(--rs-line-height-t3);
25
+ letter-spacing: var(--rs-letter-spacing-t3);
26
+ color: var(--rs-color-foreground-base-primary);
27
+ margin: 0;
28
+ }
29
+
30
+ .description {
31
+ font-size: var(--rs-font-size-regular);
32
+ line-height: var(--rs-line-height-regular);
33
+ letter-spacing: var(--rs-letter-spacing-regular);
34
+ color: var(--rs-color-foreground-base-secondary);
35
+ margin: 0;
36
+ }
37
+
38
+ .methodBar {
39
+ padding: var(--rs-space-3) 0;
40
+ border-radius: var(--rs-radius-2);
41
+ }
42
+
43
+ .path {
44
+ font-family: var(--rs-font-mono);
45
+ font-size: var(--rs-font-size-mono-regular);
46
+ line-height: var(--rs-line-height-regular);
47
+ color: var(--rs-color-foreground-base-primary);
48
+ }
49
+
50
+ .divider {
51
+ padding: 0;
52
+ margin: var(--rs-space-4) 0;
53
+ }
54
+
55
+ @media (max-width: 1100px) {
56
+ .layout {
57
+ flex-direction: column;
58
+ gap: var(--rs-space-9);
59
+ }
60
+
61
+ .left,
62
+ .right {
63
+ width: 100%;
64
+ }
65
+ }
@@ -0,0 +1,216 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import type { OpenAPIV3 } from 'openapi-types'
5
+ import { Flex, Button, Menu, CopyButton, Separator } from '@raystack/apsara'
6
+ import { ChevronDownIcon } from '@radix-ui/react-icons'
7
+ import { MethodBadge } from '@/components/api/method-badge'
8
+ import { ApiCodeSnippet } from './api-code-snippet'
9
+ import { ApiResponsePanel } from './api-response-panel'
10
+ import { flattenSchema, generateExampleJson, type SchemaField } from '@/lib/schema'
11
+ import { ApiFieldSection } from './api-field-list'
12
+ import { toKind } from '@/lib/schema'
13
+ import styles from './api-overview.module.css'
14
+
15
+ interface ApiOverviewProps {
16
+ method: string
17
+ path: string
18
+ operation: OpenAPIV3.OperationObject
19
+ serverUrl: string
20
+ specName: string
21
+ auth?: { type: string; header: string; placeholder?: string }
22
+ }
23
+
24
+ export function ApiOverview({ method, path, operation, auth }: ApiOverviewProps) {
25
+ const params = (operation.parameters ?? []) as OpenAPIV3.ParameterObject[]
26
+ const body = getRequestBody(operation.requestBody as OpenAPIV3.RequestBodyObject | undefined)
27
+
28
+ const headerFields = paramsToFields(params.filter((p) => p.in === 'header'))
29
+ const pathFields = paramsToFields(params.filter((p) => p.in === 'path'))
30
+ const queryFields = paramsToFields(params.filter((p) => p.in === 'query'))
31
+ const responses = getResponseSections(operation.responses as Record<string, OpenAPIV3.ResponseObject>)
32
+
33
+ const authFields: SchemaField[] = auth
34
+ ? [{ name: auth.header, type: 'String', kind: 'string' as const, required: false }]
35
+ : headerFields.length > 0
36
+ ? headerFields
37
+ : []
38
+
39
+ const fullUrl = '{domain}' + path
40
+ const snippetHeaders: Record<string, string> = {}
41
+ if (auth) snippetHeaders[auth.header] = auth.placeholder ?? 'YOUR_API_KEY'
42
+ if (body) snippetHeaders['Content-Type'] = body.contentType ?? 'application/json'
43
+
44
+
45
+ const hasSections = authFields.length > 0 || pathFields.length > 0 ||
46
+ queryFields.length > 0 || (body && body.fields.length > 0) || responses.length > 0
47
+
48
+ return (
49
+ <Flex className={styles.layout}>
50
+ <Flex direction='column' gap={9} className={styles.left}>
51
+ <Flex direction='column' gap={7}>
52
+ <Flex direction='column' gap={4}>
53
+ {operation.summary && (
54
+ <h1 className={styles.title}>{operation.summary}</h1>
55
+ )}
56
+ {operation.description && (
57
+ <p className={styles.description}>{operation.description}</p>
58
+ )}
59
+ </Flex>
60
+ <Flex align='center' gap={3} className={styles.methodBar}>
61
+ <MethodBadge method={method} />
62
+ <span className={styles.path}>{path}</span>
63
+ <CopyButton text={path} size={2} />
64
+ </Flex>
65
+ </Flex>
66
+
67
+ {hasSections && (
68
+ <Flex direction='column' gap={6}>
69
+ {authFields.length > 0 && (
70
+ <ApiFieldSection title="Authorisations" fields={authFields} />
71
+ )}
72
+
73
+ {authFields.length > 0 && (queryFields.length > 0 || pathFields.length > 0 || (body && body.fields.length > 0) || responses.length > 0) && (
74
+ <Separator className={styles.divider} />
75
+ )}
76
+
77
+ {pathFields.length > 0 && (
78
+ <ApiFieldSection title="Path Parameters" fields={pathFields} />
79
+ )}
80
+
81
+ {pathFields.length > 0 && (queryFields.length > 0 || (body && body.fields.length > 0) || responses.length > 0) && (
82
+ <Separator className={styles.divider} />
83
+ )}
84
+
85
+ {queryFields.length > 0 && (
86
+ <ApiFieldSection title="Query Parameters" fields={queryFields} />
87
+ )}
88
+
89
+ {queryFields.length > 0 && ((body && body.fields.length > 0) || responses.length > 0) && (
90
+ <Separator className={styles.divider} />
91
+ )}
92
+
93
+ {body && body.fields.length > 0 && (
94
+ <ApiFieldSection title="Request Body" fields={body.fields} />
95
+ )}
96
+
97
+ {body && body.fields.length > 0 && responses.length > 0 && (
98
+ <Separator className={styles.divider} />
99
+ )}
100
+
101
+ {responses.length > 0 && (
102
+ <ResponseSection responses={responses} />
103
+ )}
104
+ </Flex>
105
+ )}
106
+ </Flex>
107
+
108
+ <Flex direction='column' gap={8} className={styles.right}>
109
+ <ApiCodeSnippet
110
+ title={operation.summary ?? `${method.toUpperCase()} ${path}`}
111
+ method={method}
112
+ url={fullUrl}
113
+ headers={snippetHeaders}
114
+ body={body ? body.jsonExample : undefined}
115
+ />
116
+ <ApiResponsePanel responses={responses} />
117
+ </Flex>
118
+ </Flex>
119
+ )
120
+ }
121
+
122
+ function ResponseSection({ responses }: { responses: ResponseSectionData[] }) {
123
+ const [selectedStatus, setSelectedStatus] = useState(responses[0]?.status ?? '200')
124
+ if (responses.length === 0) return null
125
+ const active = responses.find((r) => r.status === selectedStatus) ?? responses[0]
126
+
127
+ return (
128
+ <ApiFieldSection
129
+ title="Response"
130
+ fields={active.fields}
131
+ description={active.description}
132
+ headerRight={
133
+ <>
134
+ {active.contentType && (
135
+ <span className={styles.path}>{active.contentType}</span>
136
+ )}
137
+ <Menu>
138
+ <Menu.Trigger
139
+ render={
140
+ <Button variant="text" color="neutral" size="small" trailingIcon={<ChevronDownIcon />} />
141
+ }
142
+ >
143
+ {active.status}
144
+ </Menu.Trigger>
145
+ <Menu.Content>
146
+ {responses.map((resp) => (
147
+ <Menu.Item key={resp.status} onClick={() => setSelectedStatus(resp.status)}>
148
+ {resp.status}{resp.description ? ` — ${resp.description}` : ''}
149
+ </Menu.Item>
150
+ ))}
151
+ </Menu.Content>
152
+ </Menu>
153
+ </>
154
+ }
155
+ />
156
+ )
157
+ }
158
+
159
+ function paramsToFields(params: OpenAPIV3.ParameterObject[]): SchemaField[] {
160
+ return params.map((p) => {
161
+ const schema = (p.schema ?? {}) as OpenAPIV3.SchemaObject
162
+ return {
163
+ name: p.name,
164
+ type: schema.type ? String(schema.type) : 'string',
165
+ kind: toKind(schema.type),
166
+ required: p.required ?? false,
167
+ description: p.description,
168
+ default: schema.default,
169
+ }
170
+ })
171
+ }
172
+
173
+ interface RequestBody {
174
+ contentType: string
175
+ fields: SchemaField[]
176
+ jsonExample: string
177
+ }
178
+
179
+ function getRequestBody(body: OpenAPIV3.RequestBodyObject | undefined): RequestBody | null {
180
+ if (!body?.content) return null
181
+ const contentType = Object.keys(body.content)[0]
182
+ if (!contentType) return null
183
+ const schema = body.content[contentType]?.schema as OpenAPIV3.SchemaObject | undefined
184
+ if (!schema) return null
185
+ return {
186
+ contentType,
187
+ fields: flattenSchema(schema),
188
+ jsonExample: JSON.stringify(generateExampleJson(schema), null, 2),
189
+ }
190
+ }
191
+
192
+ interface ResponseSectionData {
193
+ status: string
194
+ description?: string
195
+ contentType?: string
196
+ fields: SchemaField[]
197
+ jsonExample?: string
198
+ }
199
+
200
+ function getResponseSections(responses: Record<string, OpenAPIV3.ResponseObject>): ResponseSectionData[] {
201
+ return Object.entries(responses).map(([status, resp]) => {
202
+ const content = resp.content ?? {}
203
+ const contentType = Object.keys(content)[0]
204
+ const schema = contentType
205
+ ? (content[contentType]?.schema as OpenAPIV3.SchemaObject | undefined)
206
+ : undefined
207
+
208
+ return {
209
+ status,
210
+ description: resp.description,
211
+ contentType,
212
+ fields: schema ? flattenSchema(schema) : [],
213
+ jsonExample: schema ? JSON.stringify(generateExampleJson(schema), null, 2) : undefined,
214
+ }
215
+ })
216
+ }
@@ -0,0 +1,62 @@
1
+ .label {
2
+ font-size: var(--rs-font-size-small);
3
+ line-height: var(--rs-line-height-small);
4
+ letter-spacing: var(--rs-letter-spacing-small);
5
+ color: var(--rs-color-foreground-base-primary);
6
+ }
7
+
8
+ .container {
9
+ border: 0.5px solid var(--rs-color-border-base-primary);
10
+ border-radius: var(--rs-radius-2);
11
+ overflow: hidden;
12
+ height: 440px;
13
+ width: 100%;
14
+ }
15
+
16
+ .header {
17
+ padding: var(--rs-space-3) var(--rs-space-5);
18
+ background: var(--rs-color-background-base-secondary);
19
+ border-bottom: 0.5px solid var(--rs-color-border-base-primary);
20
+ flex-shrink: 0;
21
+ }
22
+
23
+ .tab {
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: center;
27
+ height: 20px;
28
+ padding: 0 var(--rs-space-2);
29
+ border: 0.5px solid var(--rs-color-border-base-primary);
30
+ border-radius: var(--rs-radius-2);
31
+ background: transparent;
32
+ cursor: pointer;
33
+ font-family: var(--rs-font-body);
34
+ font-size: var(--rs-font-size-mini);
35
+ font-weight: var(--rs-font-weight-medium);
36
+ line-height: var(--rs-line-height-mini);
37
+ letter-spacing: var(--rs-letter-spacing-mini);
38
+ color: var(--rs-color-foreground-base-secondary);
39
+ }
40
+
41
+ .tab:hover {
42
+ background: var(--rs-color-background-neutral-secondary);
43
+ }
44
+
45
+ .tabActive {
46
+ background: var(--rs-color-background-neutral-primary);
47
+ border-color: var(--rs-color-border-base-secondary);
48
+ color: var(--rs-color-foreground-base-primary);
49
+ }
50
+
51
+ .body {
52
+ flex: 1;
53
+ min-height: 0;
54
+ overflow-x: hidden;
55
+ overflow-y: auto;
56
+ background: var(--rs-color-background-base-primary);
57
+ }
58
+
59
+ .codeBlock {
60
+ border: none;
61
+ border-radius: 0;
62
+ }
@@ -0,0 +1,54 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { CodeBlock, CopyButton, Flex } from '@raystack/apsara'
5
+ import styles from './api-response-panel.module.css'
6
+
7
+ interface ResponseData {
8
+ status: string
9
+ description?: string
10
+ jsonExample?: string
11
+ }
12
+
13
+ interface ApiResponsePanelProps {
14
+ responses: ResponseData[]
15
+ }
16
+
17
+ export function ApiResponsePanel({ responses }: ApiResponsePanelProps) {
18
+ const [selected, setSelected] = useState(responses[0]?.status ?? '')
19
+
20
+ if (responses.length === 0) return null
21
+
22
+ const active = responses.find((r) => r.status === selected) ?? responses[0]
23
+ const displayJson = active.jsonExample ?? '{}'
24
+
25
+ return (
26
+ <Flex direction='column' gap={4}>
27
+ <span className={styles.label}>Response:</span>
28
+ <Flex direction='column' className={styles.container}>
29
+ <Flex align='center' justify='between' className={styles.header}>
30
+ <Flex align='center' gap={3}>
31
+ {responses.map((resp) => (
32
+ <button
33
+ key={resp.status}
34
+ type="button"
35
+ className={`${styles.tab} ${resp.status === active.status ? styles.tabActive : ''}`}
36
+ onClick={() => setSelected(resp.status)}
37
+ >
38
+ {resp.status}
39
+ </button>
40
+ ))}
41
+ </Flex>
42
+ <CopyButton text={displayJson} size={3} />
43
+ </Flex>
44
+ <div className={styles.body}>
45
+ <CodeBlock hideLineNumbers className={styles.codeBlock}>
46
+ <CodeBlock.Content>
47
+ <CodeBlock.Code language="json">{displayJson}</CodeBlock.Code>
48
+ </CodeBlock.Content>
49
+ </CodeBlock>
50
+ </div>
51
+ </Flex>
52
+ </Flex>
53
+ )
54
+ }