@clidey/dory 1.0.5 → 1.0.7

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 (63) hide show
  1. package/bin/dist/dory.js +36 -2
  2. package/bin/dist/dory.js.map +1 -1
  3. package/index.html +0 -1
  4. package/package.json +16 -22
  5. package/src/components/hooks.tsx +2 -1
  6. package/src/components/store.ts +5 -4
  7. package/src/config/mdx.js +8 -3
  8. package/src/config/mdx.ts +8 -3
  9. package/src/embed/widget-iframe.ts +10 -3
  10. package/src/entry-server.tsx +11 -7
  11. package/src/main.tsx +5 -1
  12. package/src/mdx/accordion.tsx +11 -8
  13. package/src/mdx/api.tsx +2 -352
  14. package/src/mdx/async-api.tsx +124 -295
  15. package/src/mdx/callout.tsx +22 -24
  16. package/src/mdx/card.tsx +20 -22
  17. package/src/mdx/chip.tsx +14 -0
  18. package/src/mdx/code.tsx +133 -110
  19. package/src/mdx/expandable.tsx +8 -8
  20. package/src/mdx/fence-meta.test.ts +39 -0
  21. package/src/mdx/fence-meta.ts +61 -0
  22. package/src/mdx/fence.tsx +155 -44
  23. package/src/mdx/icon-grid.tsx +29 -0
  24. package/src/mdx/icon.tsx +2 -2
  25. package/src/mdx/latex.tsx +14 -7
  26. package/src/mdx/list.tsx +3 -3
  27. package/src/mdx/mdx.tsx +7 -1
  28. package/src/mdx/mermaid.tsx +81 -11
  29. package/src/mdx/open-api.tsx +396 -239
  30. package/src/mdx/source.tsx +148 -40
  31. package/src/mdx/specimen.tsx +69 -0
  32. package/src/mdx/step.tsx +22 -30
  33. package/src/mdx/table.tsx +4 -9
  34. package/src/mdx/tabs.tsx +39 -0
  35. package/src/mdx/tag.tsx +13 -21
  36. package/src/mdx/text.tsx +17 -9
  37. package/src/mdx/type-scale.tsx +28 -0
  38. package/src/mdx/websocket-playground.tsx +221 -704
  39. package/src/plugins/code-meta.ts +23 -0
  40. package/src/plugins/frontmatter-dev-server.ts +6 -2
  41. package/src/plugins/html-files-middleware.ts +9 -2
  42. package/src/plugins/html-metadata-injector.ts +4 -3
  43. package/src/plugins/llm-txt-dev-server.ts +5 -2
  44. package/src/plugins/llm-txt-generator.ts +2 -1
  45. package/src/plugins/prerender.ts +7 -4
  46. package/src/plugins/relative-assets.test.ts +26 -0
  47. package/src/plugins/relative-assets.ts +80 -0
  48. package/src/plugins/robots-generator.ts +6 -4
  49. package/src/plugins/sanitize.test.ts +5 -0
  50. package/src/plugins/sanitize.ts +2 -0
  51. package/src/plugins/shared.ts +17 -0
  52. package/src/plugins/sitemap-generator.ts +2 -1
  53. package/src/theme.scss +282 -3
  54. package/src/types/config.ts +3 -0
  55. package/src/ui/header.tsx +4 -3
  56. package/src/ui/layout.tsx +22 -4
  57. package/src/ui/page-meta.tsx +37 -0
  58. package/src/ui/prev-next-link.tsx +4 -3
  59. package/src/ui/table-of-content.tsx +11 -3
  60. package/src/utils/base-path.test.ts +46 -0
  61. package/src/utils/base-path.ts +41 -0
  62. package/vite.config.ssr.ts +3 -0
  63. package/vite.config.ts +3 -0
@@ -1,311 +1,468 @@
1
+ import { Button, Sheet, SheetContent, SheetHeader, SheetTitle } from "@clidey/ux"
2
+ import classNames from "classnames"
3
+ import { Check, ChevronRight, Copy, Play } from "lucide-react"
4
+ import { h } from "preact"
1
5
  import { useState } from "preact/hooks"
2
- import { APIGroup } from "./api"
3
- import { APIPlayground, PlayButton, type Server } from "./api-playground"
4
- import { Fence } from "./fence"
6
+ import { APIPlayground, type Server } from "./api-playground"
7
+ import { CodeFrame, CodeGroup } from "./code"
8
+ import { Fence, Pre } from "./fence"
5
9
  import { Tag } from "./tag"
6
- import { Col, Row } from "./text"
7
- import { Sheet, SheetContent, SheetHeader, SheetTitle, Button } from "@clidey/ux"
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Field list used by the API pages and exposed to MDX as Properties/Property.
13
+ // ---------------------------------------------------------------------------
8
14
 
9
15
  export function Properties({ children }: { children: React.ReactNode }) {
10
- return (
11
- <div className="my-2">
12
- <ul
13
- role="list"
14
- className="m-0 max-w-[calc(var(--container-lg)-(--spacing(8)))] list-none divide-y divide-zinc-900/5 p-0 dark:divide-white/10"
15
- >
16
- {children}
17
- </ul>
18
- </div>
19
- )
16
+ return <div className="dory-fields not-prose my-2 divide-y divide-[var(--border)]">{children}</div>
20
17
  }
21
18
 
19
+ /** One field: mono name, blue type, orange REQUIRED, optional default pill, muted description. */
22
20
  export function Property({
23
21
  name,
24
22
  children,
25
23
  type,
26
24
  required = false,
25
+ defaultValue,
27
26
  }: {
28
27
  name: string
29
- children: React.ReactNode
28
+ children?: React.ReactNode
30
29
  type?: string
31
30
  required?: boolean
31
+ defaultValue?: unknown
32
32
  }) {
33
33
  return (
34
- <div className="m-0 px-0 first:pt-0 last:pb-0">
35
- <div className="my-4 flex flex-wrap items-center gap-x-3 gap-2">
36
- <div className="flex gap-2 items-center grow">
37
- <Tag className="px-4 py-1">{name}</Tag>
38
- <p className="font-mono text-xs text-muted-foreground/80">
39
- {type}
40
- </p>
41
- <div className="flex grow justify-end ">
42
- {required && <span className="text-rose-400 ml-1 text-xs">Required</span>}
43
- </div>
44
- </div>
45
- <p className="w-full flex-none [&>:first-child]:mt-0 [&>:last-child]:mb-0 text-xs text-muted-foreground">
46
- {children}
47
- </p>
34
+ <div className="py-3.5">
35
+ <div className="flex flex-wrap items-baseline gap-x-2.5 gap-y-1">
36
+ <span className="font-mono text-sm font-semibold text-foreground">{name}</span>
37
+ {type && <span className="font-mono text-xs text-[var(--primary)]">{type}</span>}
38
+ {required && <span className="font-mono text-[10px] font-semibold uppercase tracking-[0.12em] text-[var(--brand-readable,#b65401)]">required</span>}
39
+ {defaultValue !== undefined && defaultValue !== null && (
40
+ <span className="rounded-[5px] border border-[var(--border)] bg-muted px-1.5 py-px font-mono text-[11px] text-muted-foreground">
41
+ default: {typeof defaultValue === 'string' ? defaultValue : JSON.stringify(defaultValue)}
42
+ </span>
43
+ )}
48
44
  </div>
45
+ {children && <div className="mt-1 text-sm leading-relaxed text-muted-foreground [&_p]:mb-0">{children}</div>}
49
46
  </div>
50
47
  )
51
48
  }
52
49
 
53
- interface ParamFieldProps {
54
- body: string;
55
- type: string;
56
- default?: string | number;
57
- required?: boolean;
58
- children: React.ReactNode;
50
+ export function ParamField({ body, type, default: defaultValue, required, children }: {
51
+ body: string
52
+ type: string
53
+ default?: string | number
54
+ required?: boolean
55
+ children: React.ReactNode
56
+ }) {
57
+ return <Property name={body} type={type} required={required} defaultValue={defaultValue}>{children}</Property>
59
58
  }
60
59
 
61
- export function ParamField({ body, type, default: defaultValue, required, children }: ParamFieldProps) {
62
- return (
63
- <Property name={body} type={type} required={required}>
64
- {children}
65
- {defaultValue != null && (
66
- <span className="text-muted-foreground ml-1">(default: {defaultValue})</span>
67
- )}
68
- </Property>
69
- );
60
+ export function ResponseField({ name, type, required, children }: {
61
+ name: string
62
+ type: string
63
+ required?: boolean
64
+ children: React.ReactNode
65
+ }) {
66
+ return <Property name={name} type={type} required={required}>{children}</Property>
70
67
  }
71
68
 
72
- interface ResponseFieldProps {
73
- name: string;
74
- type: string;
75
- required?: boolean;
76
- children: React.ReactNode;
77
- }
69
+ // ---------------------------------------------------------------------------
70
+ // OpenAPI types (the subset Dory renders)
71
+ // ---------------------------------------------------------------------------
78
72
 
79
- export function ResponseField({ name, type, required, children }: ResponseFieldProps) {
80
- return (
81
- <Property name={name} type={type} required={required}>
82
- {children}
83
- </Property>
84
- );
73
+ interface Schema {
74
+ $ref?: string
75
+ type?: string
76
+ format?: string
77
+ enum?: unknown[]
78
+ items?: Schema
79
+ properties?: Record<string, Schema>
80
+ required?: string[]
81
+ description?: string
82
+ default?: unknown
83
+ example?: unknown
85
84
  }
86
85
 
87
- interface Schema {
88
- $ref?: string;
89
- type?: string;
90
- items?: Schema;
91
- properties?: Record<string, Schema>;
92
- required?: boolean;
93
- description?: string;
94
- default?: string | number;
86
+ interface SecurityScheme {
87
+ type: 'apiKey' | 'http' | 'oauth2' | 'openIdConnect'
88
+ in?: 'header' | 'query' | 'cookie'
89
+ name?: string
90
+ scheme?: string
91
+ description?: string
95
92
  }
96
93
 
97
94
  interface Components {
98
- schemas?: Record<string, Schema>;
95
+ schemas?: Record<string, Schema>
96
+ securitySchemes?: Record<string, SecurityScheme>
99
97
  }
100
98
 
101
99
  interface Parameter {
102
- name: string;
103
- schema?: Schema;
104
- required?: boolean;
105
- description?: string;
106
- }
107
-
108
- interface Content {
109
- schema?: Schema;
110
- }
111
-
112
- interface RequestBody {
113
- description?: string;
114
- content?: {
115
- 'application/json'?: Content;
116
- };
100
+ name: string
101
+ in?: 'query' | 'path' | 'header' | 'cookie'
102
+ schema?: Schema
103
+ required?: boolean
104
+ description?: string
117
105
  }
118
106
 
119
- interface Response {
120
- content?: {
121
- 'application/json'?: Content;
122
- };
107
+ interface MediaTypes {
108
+ 'application/json'?: { schema?: Schema; example?: unknown }
123
109
  }
124
110
 
125
111
  interface PathMethod {
126
- description?: string;
127
- parameters?: Parameter[];
128
- requestBody?: RequestBody;
129
- responses: Record<string, Response>;
112
+ summary?: string
113
+ description?: string
114
+ parameters?: Parameter[]
115
+ requestBody?: { description?: string; required?: boolean; content?: MediaTypes }
116
+ responses: Record<string, { description?: string; content?: MediaTypes }>
117
+ security?: Array<Record<string, string[]>>
130
118
  }
131
119
 
132
120
  export interface OpenAPIJson {
133
- paths: Record<string, Record<string, PathMethod>>;
134
- components: Components;
135
- servers?: Server[];
121
+ paths: Record<string, Record<string, PathMethod | Parameter[]>>
122
+ components: Components
123
+ servers?: Server[]
124
+ security?: Array<Record<string, string[]>>
136
125
  }
137
126
 
138
- function resolveSchema(schema: Schema | undefined, components: Components): Schema {
139
- if (!schema) return {};
127
+ // ---------------------------------------------------------------------------
128
+ // Schema helpers
129
+ // ---------------------------------------------------------------------------
140
130
 
131
+ function resolveSchema(schema: Schema | undefined, components: Components): Schema {
132
+ if (!schema) return {}
141
133
  if (schema.$ref) {
142
- const refPath = schema.$ref.replace(/^#\/components\/schemas\//, '');
143
- return resolveSchema(components.schemas?.[refPath], components);
134
+ const name = schema.$ref.replace(/^#\/components\/schemas\//, '')
135
+ return resolveSchema(components.schemas?.[name], components)
144
136
  }
145
-
146
137
  if (schema.type === 'array' && schema.items) {
147
- return {
148
- ...schema,
149
- items: resolveSchema(schema.items, components)
150
- };
138
+ return { ...schema, items: resolveSchema(schema.items, components) }
151
139
  }
152
-
153
140
  if (schema.properties) {
154
- const resolvedProps: Record<string, Schema> = {};
155
- for (const [key, value] of Object.entries(schema.properties)) {
156
- resolvedProps[key] = resolveSchema(value, components);
141
+ const properties: Record<string, Schema> = {}
142
+ for (const [key, value] of Object.entries(schema.properties)) properties[key] = resolveSchema(value, components)
143
+ return { ...schema, properties }
144
+ }
145
+ return schema
146
+ }
147
+
148
+ /** Type label in the PDF style: `enum<string>`, `string · date-time`, `array<object>`. */
149
+ function typeLabel(schema: Schema): string {
150
+ if (schema.enum) return `enum<${schema.type ?? 'string'}>`
151
+ if (schema.type === 'array') return `array<${schema.items ? typeLabel(schema.items) : 'any'}>`
152
+ if (schema.format) return `${schema.type ?? 'string'} · ${schema.format}`
153
+ return schema.type ?? 'object'
154
+ }
155
+
156
+ /** Builds an example value from a schema, preferring declared examples and defaults. */
157
+ function exampleOf(schema: Schema, depth = 0): unknown {
158
+ if (schema.example !== undefined) return schema.example
159
+ if (schema.default !== undefined) return schema.default
160
+ if (schema.enum?.length) return schema.enum[0]
161
+ switch (schema.type) {
162
+ case 'string':
163
+ if (schema.format === 'date-time') return '2026-10-19T09:41:00Z'
164
+ if (schema.format === 'date') return '2026-10-19'
165
+ if (schema.format === 'uuid') return '8f2a91c4-1b7e-4c7a-9f3d-2e6b5a0c1d8e'
166
+ if (schema.format === 'email') return 'user@example.com'
167
+ return 'string'
168
+ case 'integer': return 0
169
+ case 'number': return 0
170
+ case 'boolean': return true
171
+ case 'array': return schema.items && depth < 4 ? [exampleOf(schema.items, depth + 1)] : []
172
+ default: {
173
+ if (!schema.properties || depth > 4) return {}
174
+ const out: Record<string, unknown> = {}
175
+ for (const [key, value] of Object.entries(schema.properties)) out[key] = exampleOf(value, depth + 1)
176
+ return out
157
177
  }
158
- return {
159
- ...schema,
160
- properties: resolvedProps
161
- };
162
178
  }
179
+ }
163
180
 
164
- return schema;
181
+ function describeDefault(schema: Schema, description?: string): string | undefined {
182
+ if (description) return description
183
+ if (schema.description) return schema.description
184
+ return undefined
185
+ }
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // Pieces
189
+ // ---------------------------------------------------------------------------
190
+
191
+ function SectionTitle({ children, note }: { children: string; note?: string }) {
192
+ return (
193
+ <div className="mt-10 mb-1">
194
+ <h3 className="dory-h3 font-heading">{children}</h3>
195
+ {note && <p className="mt-0.5 font-mono text-xs text-muted-foreground">{note}</p>}
196
+ </div>
197
+ )
198
+ }
199
+
200
+ /** Field rows for an object schema, with nested objects behind a "Show N child attributes" toggle. */
201
+ function SchemaFields({ schema, components }: { schema: Schema; components: Components }) {
202
+ const target = schema.type === 'array' && schema.items ? schema.items : schema
203
+ const properties = Object.entries(target.properties ?? {})
204
+ if (properties.length === 0) return null
205
+ return (
206
+ <Properties>
207
+ {properties.map(([name, raw]) => {
208
+ const prop = resolveSchema(raw, components)
209
+ const nested = prop.type === 'array' && prop.items?.properties ? prop.items : prop.properties ? prop : null
210
+ return (
211
+ <Property key={name} name={name} type={typeLabel(prop)} required={target.required?.includes(name)} defaultValue={prop.default}>
212
+ {describeDefault(prop)}
213
+ {nested && <ChildAttributes schema={nested} components={components} />}
214
+ </Property>
215
+ )
216
+ })}
217
+ </Properties>
218
+ )
219
+ }
220
+
221
+ function ChildAttributes({ schema, components }: { schema: Schema; components: Components }) {
222
+ const [open, setOpen] = useState(false)
223
+ const count = Object.keys(schema.properties ?? {}).length
224
+ if (count === 0) return null
225
+ return (
226
+ <div className="mt-3">
227
+ <button
228
+ type="button"
229
+ onClick={() => setOpen((v) => !v)}
230
+ aria-expanded={open}
231
+ className="flex w-full items-center gap-2 rounded-lg border border-[var(--border)] bg-muted px-4 py-2.5 text-left text-sm font-medium text-foreground"
232
+ >
233
+ <ChevronRight aria-hidden="true" className={classNames('h-3.5 w-3.5 transition-transform', { 'rotate-90': open })} />
234
+ {open ? 'Hide' : 'Show'} {count} child attribute{count === 1 ? '' : 's'}
235
+ </button>
236
+ {open && <div className="pl-4"><SchemaFields schema={schema} components={components} /></div>}
237
+ </div>
238
+ )
239
+ }
240
+
241
+ function CopyUrl({ value }: { value: string }) {
242
+ const [copied, setCopied] = useState(false)
243
+ return (
244
+ <button
245
+ type="button"
246
+ aria-label="Copy URL"
247
+ onClick={() => navigator.clipboard.writeText(value).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1000) })}
248
+ className="rounded-md p-1.5 text-muted-foreground transition hover:bg-muted hover:text-foreground"
249
+ >
250
+ {copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
251
+ </button>
252
+ )
253
+ }
254
+
255
+ function statusTone(code: string): string {
256
+ if (code.startsWith('2')) return 'bg-green-600'
257
+ if (code === '401' || code === '403' || code.startsWith('5')) return 'bg-[var(--destructive)]'
258
+ return 'bg-[var(--highlight)]'
259
+ }
260
+
261
+ /** Response examples per status code, with a colored dot per status in the tab strip. */
262
+ function ResponseTabs({ responses, components }: { responses: PathMethod['responses']; components: Components }) {
263
+ const entries = Object.entries(responses)
264
+ const [active, setActive] = useState(0)
265
+ const current = entries[active]
266
+ if (!current) return null
267
+ const media = current[1].content?.['application/json']
268
+ const example = media?.example ?? (media?.schema ? exampleOf(resolveSchema(media.schema, components)) : { description: current[1].description })
269
+ const code = JSON.stringify(example, null, 2)
270
+
271
+ return (
272
+ <CodeFrame
273
+ code={code}
274
+ header={
275
+ <div role="tablist" className="flex min-h-10 items-end gap-4 border-b border-[var(--border)] bg-muted pl-4 pr-12">
276
+ {entries.map(([status], index) => (
277
+ <button
278
+ key={status}
279
+ type="button"
280
+ role="tab"
281
+ aria-selected={index === active}
282
+ onClick={() => setActive(index)}
283
+ className={classNames('-mb-px flex items-center gap-1.5 border-b-2 pb-2.5 pt-3 font-mono text-xs transition', {
284
+ 'border-[var(--primary)] text-foreground': index === active,
285
+ 'border-transparent text-muted-foreground hover:text-foreground': index !== active,
286
+ })}
287
+ >
288
+ <span aria-hidden="true" className={classNames('size-1.5 rounded-full', statusTone(status))} />
289
+ {status}
290
+ </button>
291
+ ))}
292
+ </div>
293
+ }
294
+ >
295
+ <div className="overflow-x-auto py-3 pl-4 pr-12 font-mono text-[12.5px] leading-[1.7]">
296
+ <Fence language="json">{code}</Fence>
297
+ </div>
298
+ </CodeFrame>
299
+ )
300
+ }
301
+
302
+ /** cURL / JavaScript / Python samples generated from the operation. */
303
+ function RequestSamples({ method, url, headers, body }: { method: string; url: string; headers: Record<string, string>; body?: unknown }) {
304
+ const headerLines = Object.entries(headers)
305
+ const json = body === undefined ? undefined : JSON.stringify(body)
306
+ const curl = [
307
+ `curl -X ${method} ${url} \\`,
308
+ ...headerLines.map(([key, value]) => ` -H "${key}: ${value}" \\`),
309
+ ...(json ? [` -d '${json}'`] : []),
310
+ ].join('\n').replace(/ \\$/, '')
311
+ const js = [
312
+ `const response = await fetch("${url}", {`,
313
+ ` method: "${method}",`,
314
+ ` headers: ${JSON.stringify(headers, null, 2).replace(/\n/g, '\n ')},`,
315
+ ...(json ? [` body: JSON.stringify(${JSON.stringify(body, null, 2).replace(/\n/g, '\n ')}),`] : []),
316
+ `});`,
317
+ `const data = await response.json();`,
318
+ ].join('\n')
319
+ const py = [
320
+ `import requests`,
321
+ ``,
322
+ `response = requests.${method.toLowerCase()}(`,
323
+ ` "${url}",`,
324
+ ` headers=${JSON.stringify(headers)},`,
325
+ ...(json ? [` json=${JSON.stringify(body)},`] : []),
326
+ `)`,
327
+ `print(response.json())`,
328
+ ].join('\n')
329
+
330
+ // Mirrors what MDX emits for a fence: <pre><code className="language-x" metastring="…">
331
+ const sample = (title: string, language: string, code: string) => (
332
+ <Pre key={title}>{h('code', { className: `language-${language}`, metastring: `title="${title}"` }, code)}</Pre>
333
+ )
334
+ return (
335
+ <CodeGroup title="">
336
+ {sample('cURL', 'bash', curl)}
337
+ {sample('JavaScript', 'javascript', js)}
338
+ {sample('Python', 'python', py)}
339
+ </CodeGroup>
340
+ )
341
+ }
342
+
343
+ // ---------------------------------------------------------------------------
344
+ // Page
345
+ // ---------------------------------------------------------------------------
346
+
347
+ function authFields(security: Array<Record<string, string[]>> | undefined, components: Components) {
348
+ const schemes = components.securitySchemes ?? {}
349
+ const names = (security ?? []).flatMap((requirement) => Object.keys(requirement))
350
+ return names.map((name) => schemes[name]).filter((scheme): scheme is SecurityScheme => Boolean(scheme))
351
+ }
352
+
353
+ function authHeader(scheme: SecurityScheme): [string, string] | null {
354
+ if (scheme.type === 'apiKey' && scheme.in === 'header' && scheme.name) return [scheme.name, '$API_KEY']
355
+ if (scheme.type === 'http') return ['Authorization', `${scheme.scheme === 'basic' ? 'Basic' : 'Bearer'} $TOKEN`]
356
+ if (scheme.type === 'oauth2' || scheme.type === 'openIdConnect') return ['Authorization', 'Bearer $TOKEN']
357
+ return null
165
358
  }
166
359
 
167
360
  export function OpenAPI({ openAPIJson, method: selectedMethod, path: selectedPath }: { openAPIJson: OpenAPIJson, method: string, path: string }) {
168
- const { paths, components } = openAPIJson;
169
- const [showPlayground, setShowPlayground] = useState(false);
361
+ const { paths, components } = openAPIJson
362
+ const [showPlayground, setShowPlayground] = useState(false)
363
+
364
+ const methods = paths[selectedPath]
365
+ const operation = methods?.[selectedMethod.toLowerCase()]
366
+ if (!operation || Array.isArray(operation)) return null
367
+
368
+ const pathParameters = Array.isArray(methods.parameters) ? methods.parameters : []
369
+ const parameters = [...pathParameters, ...(operation.parameters ?? [])]
370
+ const serverUrl = (openAPIJson.servers?.[0]?.url ?? '').replace(/\/$/, '')
371
+ const fullUrl = `${serverUrl}${selectedPath}`
372
+ const schemes = authFields(operation.security ?? openAPIJson.security, components)
373
+ const bodyMedia = operation.requestBody?.content?.['application/json']
374
+ const bodySchema = bodyMedia?.schema ? resolveSchema(bodyMedia.schema, components) : undefined
375
+ const successResponse = Object.entries(operation.responses).find(([code]) => code.startsWith('2'))?.[1]
376
+ const responseSchema = successResponse?.content?.['application/json']?.schema
377
+ ? resolveSchema(successResponse.content['application/json'].schema, components)
378
+ : undefined
170
379
 
171
- const handleShowPlayground = () => {
172
- setShowPlayground(true);
380
+ const headers: Record<string, string> = {}
381
+ for (const scheme of schemes) {
382
+ const header = authHeader(scheme)
383
+ if (header) headers[header[0]] = header[1]
173
384
  }
385
+ if (bodySchema) headers['Content-Type'] = 'application/json'
174
386
 
175
387
  return (
176
- <div className="max-w-none w-full">
388
+ <div className="dory-api max-w-none w-full">
177
389
  <Sheet open={showPlayground} onOpenChange={setShowPlayground} modal={true}>
178
390
  <SheetContent side="bottom" className="h-[90vh] w-full overflow-y-auto p-8">
179
391
  <SheetHeader className="p-0">
180
392
  <SheetTitle>API Playground</SheetTitle>
181
393
  </SheetHeader>
182
- <APIPlayground
183
- method={selectedMethod}
184
- url={selectedPath}
185
- servers={openAPIJson.servers}
186
- />
394
+ <APIPlayground method={selectedMethod} url={selectedPath} servers={openAPIJson.servers} />
187
395
  </SheetContent>
188
396
  </Sheet>
189
- {Object.entries(paths).map(([path, methods]) => {
190
- if (path !== selectedPath) {
191
- return null;
192
- }
193
397
 
194
- return (
195
- <div key={path} className="my-8">
196
- {Object.entries(methods).map(([method, details]) => {
197
- const { description, parameters, requestBody, responses } = details;
198
- const methodUpper = method.toUpperCase();
199
-
200
- if (methodUpper !== selectedMethod) {
201
- return null;
202
- }
203
-
204
- return (
205
- <Row key={`${method}-${path}`} cols={2} className="gap-0">
206
- <Col>
207
- <h2 className="flex items-center mt-4">
208
- <Tag variant="medium" className="mr-2 py-1 px-4 rounded-lg">{methodUpper}</Tag>
209
- <span className="ml-2 font-mono">{path}</span>
210
- </h2>
211
-
212
- {description && <p>{description}</p>}
213
-
214
- {parameters && parameters.length > 0 && (
215
- <div className="mt-4">
216
- <h3 className="font-bold">Parameters</h3>
217
- <Properties>
218
- {parameters.map((param) => (
219
- <ParamField
220
- key={param.name}
221
- body={param.name}
222
- type={param.schema?.type ?? 'string'}
223
- required={param.required}
224
- default={param.schema?.default}
225
- >
226
- {param.description}
227
- </ParamField>
228
- ))}
229
- </Properties>
230
- </div>
231
- )}
232
- {requestBody && (
233
- <div className="mt-4">
234
- <h3 className="font-bold">Request Body</h3>
235
- <p>{requestBody.description}</p>
236
- {requestBody.content?.['application/json']?.schema && (
237
- (() => {
238
- const resolvedSchema = resolveSchema(requestBody.content['application/json'].schema, components);
239
- return (
240
- <Properties>
241
- {Object.entries(resolvedSchema.properties || {}).map(([propName, prop]) => (
242
- <ParamField
243
- key={propName}
244
- body={propName}
245
- type={prop.type ?? 'string'}
246
- required={prop.required}
247
- default={prop.default}
248
- >
249
- {prop.description}
250
- </ParamField>
251
- ))}
252
- </Properties>
253
- );
254
- })()
255
- )}
256
- </div>
257
- )}
258
- {components?.schemas && (
259
- <div>
260
- <h3 className="font-bold">Schemas</h3>
261
- {Object.entries(components.schemas).filter(([, schema]) => schema.properties).map(([name, schema]) => (
262
- <div key={name} className="mt-8">
263
- <h4>{name}</h4>
264
- <Properties>
265
- {Object.entries(schema.properties || {}).map(([propName, prop]) => (
266
- <ResponseField
267
- key={propName}
268
- name={propName}
269
- type={prop.type ?? 'string'}
270
- required={prop.required}
271
- >
272
- {prop.description}
273
- </ResponseField>
274
- ))}
275
- </Properties>
276
- </div>
277
- ))}
278
- </div>
279
- )}
280
- </Col>
281
- <Col sticky>
282
- <Button
283
- onClick={handleShowPlayground}
284
- className='ml-auto mt-3 flex items-center gap-2'>
285
- Try
286
- <PlayButton className="h-4 w-4" />
287
- </Button>
288
- <APIGroup title="Request" tag={methodUpper} label={path}>
289
- {Object.entries(responses).map(([code, response]) => (
290
- <div key={code} title={code}>
291
- {response.content?.['application/json']?.schema && (() => {
292
- const resolvedSchema = resolveSchema(response.content['application/json'].schema, components);
293
- return (
294
- <Fence language="json">
295
- {JSON.stringify(resolvedSchema, null, 2)}
296
- </Fence>
297
- );
298
- })()}
299
- </div>
300
- ))}
301
- </APIGroup>
302
- </Col>
303
- </Row>
304
- )
305
- })}
306
- </div>
307
- )
308
- })}
398
+ {operation.description && <p className="dory-lead -mt-4 mb-6">{operation.description}</p>}
399
+
400
+ <div className="not-prose flex items-center gap-3 rounded-lg border border-[var(--border)] bg-card py-2 pl-3 pr-2">
401
+ <Tag>{selectedMethod}</Tag>
402
+ <span className="min-w-0 flex-1 truncate font-mono text-sm">
403
+ <span className="text-muted-foreground">{serverUrl}</span>
404
+ <span className="font-semibold text-foreground">{selectedPath}</span>
405
+ </span>
406
+ <CopyUrl value={fullUrl} />
407
+ <Button onClick={() => setShowPlayground(true)} className="flex items-center gap-1.5 bg-[var(--primary)] text-white hover:bg-[var(--primary)]/90">
408
+ <Play className="h-3 w-3 fill-current" />
409
+ Try it
410
+ </Button>
411
+ </div>
412
+
413
+ {schemes.length > 0 && (
414
+ <>
415
+ <SectionTitle>Authorization</SectionTitle>
416
+ <Properties>
417
+ {schemes.map((scheme, index) => {
418
+ const header = authHeader(scheme)
419
+ const name = header?.[0] ?? scheme.name ?? scheme.type
420
+ const location = scheme.type === 'apiKey' ? scheme.in ?? 'header' : 'header'
421
+ return (
422
+ <Property key={index} name={name} type={`${location} · string`} required>
423
+ {scheme.description ?? (scheme.type === 'http' ? `${scheme.scheme === 'basic' ? 'Basic' : 'Bearer'} token.` : 'API key.')}
424
+ </Property>
425
+ )
426
+ })}
427
+ </Properties>
428
+ </>
429
+ )}
430
+
431
+ {parameters.length > 0 && (
432
+ <>
433
+ <SectionTitle>Parameters</SectionTitle>
434
+ <Properties>
435
+ {parameters.map((param) => {
436
+ const schema = resolveSchema(param.schema, components)
437
+ return (
438
+ <Property key={`${param.in}-${param.name}`} name={param.name} type={`${param.in ?? 'query'} · ${typeLabel(schema)}`} required={param.required} defaultValue={schema.default}>
439
+ {describeDefault(schema, param.description)}
440
+ </Property>
441
+ )
442
+ })}
443
+ </Properties>
444
+ </>
445
+ )}
446
+
447
+ {bodySchema && (
448
+ <>
449
+ <SectionTitle note="application/json">Body</SectionTitle>
450
+ {operation.requestBody?.description && <p className="mt-2 text-sm text-muted-foreground">{operation.requestBody.description}</p>}
451
+ <SchemaFields schema={bodySchema} components={components} />
452
+ </>
453
+ )}
454
+
455
+ {responseSchema && (
456
+ <>
457
+ <SectionTitle>Response</SectionTitle>
458
+ <SchemaFields schema={responseSchema} components={components} />
459
+ </>
460
+ )}
461
+
462
+ <div className="mt-8">
463
+ <RequestSamples method={selectedMethod} url={fullUrl} headers={headers} body={bodySchema ? exampleOf(bodySchema) : undefined} />
464
+ <ResponseTabs responses={operation.responses} components={components} />
465
+ </div>
309
466
  </div>
310
467
  )
311
468
  }