@clidey/dory 1.0.7 → 1.9.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.
- package/README.md +12 -1
- package/bin/chat-server.ts +270 -0
- package/bin/dist/analytics-server.d.ts +11 -0
- package/bin/dist/analytics-server.d.ts.map +1 -0
- package/bin/dist/analytics-server.js +121 -0
- package/bin/dist/analytics-server.js.map +1 -0
- package/bin/dist/automation.d.ts +35 -0
- package/bin/dist/automation.d.ts.map +1 -0
- package/bin/dist/automation.js +259 -0
- package/bin/dist/automation.js.map +1 -0
- package/bin/dist/chat-server.d.ts +8 -0
- package/bin/dist/chat-server.d.ts.map +1 -0
- package/bin/dist/chat-server.js +322 -0
- package/bin/dist/chat-server.js.map +1 -0
- package/bin/dist/check.d.ts +16 -0
- package/bin/dist/check.d.ts.map +1 -0
- package/bin/dist/check.js +247 -0
- package/bin/dist/check.js.map +1 -0
- package/bin/dist/dory.js +252 -402
- package/bin/dist/dory.js.map +1 -1
- package/bin/dist/i18n-init.d.ts +6 -0
- package/bin/dist/i18n-init.d.ts.map +1 -0
- package/bin/dist/i18n-init.js +56 -0
- package/bin/dist/i18n-init.js.map +1 -0
- package/bin/dist/mcp-http.d.ts +8 -0
- package/bin/dist/mcp-http.d.ts.map +1 -0
- package/bin/dist/mcp-http.js +72 -0
- package/bin/dist/mcp-http.js.map +1 -0
- package/bin/dist/mcp.d.ts +2 -0
- package/bin/dist/mcp.d.ts.map +1 -0
- package/bin/dist/mcp.js +81 -0
- package/bin/dist/mcp.js.map +1 -0
- package/bin/dist/preview-entry.d.ts +2 -0
- package/bin/dist/preview-entry.d.ts.map +1 -0
- package/bin/dist/preview-entry.js +6 -0
- package/bin/dist/preview-entry.js.map +1 -0
- package/bin/dist/preview-server.d.ts +5 -0
- package/bin/dist/preview-server.d.ts.map +1 -0
- package/bin/dist/preview-server.js +159 -0
- package/bin/dist/preview-server.js.map +1 -0
- package/bin/dist/project-config.d.ts +22 -0
- package/bin/dist/project-config.d.ts.map +1 -0
- package/bin/dist/project-config.js +44 -0
- package/bin/dist/project-config.js.map +1 -0
- package/dist/index.html +57 -0
- package/dist/runtime/dory-runtime.css +2 -0
- package/dist/runtime/dory-runtime.html +14 -0
- package/dist/runtime/dory-runtime.js +5490 -0
- package/docs/api-playground-demo.mdx +119 -0
- package/docs/api-reference/asyncapi.json +324 -0
- package/docs/api-reference/openapi.json +366 -0
- package/docs/api-reference/operations/create.mdx +5 -0
- package/docs/api-reference/operations/delete.mdx +5 -0
- package/docs/api-reference/operations/get.mdx +5 -0
- package/docs/api-reference/operations/update.mdx +5 -0
- package/docs/api-reference/websocket-operations/receive-chat-message.mdx +5 -0
- package/docs/api-reference/websocket-operations/receive-notifications.mdx +5 -0
- package/docs/api-reference/websocket-operations/receive-presence-update.mdx +5 -0
- package/docs/api-reference/websocket-operations/send-chat-message.mdx +5 -0
- package/docs/api-reference/websocket-operations/send-presence-update.mdx +5 -0
- package/docs/assets/dory.jpeg +0 -0
- package/docs/components/accordion.mdx +145 -0
- package/docs/components/callouts.mdx +134 -0
- package/docs/components/cards.mdx +153 -0
- package/docs/components/code-blocks.mdx +281 -0
- package/docs/components/expandable.mdx +189 -0
- package/docs/components/icons.mdx +71 -0
- package/docs/components/images.mdx +82 -0
- package/docs/components/latex.mdx +148 -0
- package/docs/components/layout.mdx +175 -0
- package/docs/components/lists.mdx +134 -0
- package/docs/components/mermaid.mdx +149 -0
- package/docs/components/overview.mdx +81 -0
- package/docs/components/source.mdx +130 -0
- package/docs/components/steps.mdx +122 -0
- package/docs/components/tables.mdx +114 -0
- package/docs/components/tags.mdx +179 -0
- package/docs/components/typography.mdx +185 -0
- package/docs/configuration/analytics.mdx +45 -0
- package/docs/configuration/chat.mdx +59 -0
- package/docs/configuration/dory-json.mdx +278 -0
- package/docs/configuration/embed.mdx +73 -0
- package/docs/configuration/fonts.mdx +86 -0
- package/docs/configuration/internationalization.mdx +61 -0
- package/docs/configuration/navigation.mdx +217 -0
- package/docs/configuration/theming.mdx +166 -0
- package/docs/dory.json +129 -0
- package/docs/embed-example.html +176 -0
- package/docs/favicon.svg +18 -0
- package/docs/features.mdx +440 -0
- package/docs/getting-started/automation.mdx +76 -0
- package/docs/getting-started/configuration.mdx +95 -0
- package/docs/getting-started/installation.mdx +88 -0
- package/docs/getting-started/quick-start.mdx +53 -0
- package/docs/getting-started/whodb-ee-apps.mdx +122 -0
- package/docs/introduction.mdx +50 -0
- package/docs/logo/logo.svg +18 -0
- package/docs/playgrounds/api-playground.mdx +122 -0
- package/docs/playgrounds/websocket-playground.mdx +98 -0
- package/docs/social-card.png +0 -0
- package/docs/social-card.svg +17 -0
- package/docs/websocket-playground-demo.mdx +135 -0
- package/package.json +23 -17
- package/scripts/verify-runtime-parity.mjs +146 -0
- package/src/app.css +19 -1
- package/src/assets/fonts/HankenGrotesk.ttf +0 -0
- package/src/assets/fonts/OFL.txt +94 -0
- package/src/components/chat-assistant.tsx +229 -0
- package/src/components/dropdown.tsx +3 -3
- package/src/components/error-boundary.tsx +2 -2
- package/src/components/hooks.tsx +20 -1
- package/src/components/navigation.tsx +82 -51
- package/src/components/notification.tsx +2 -3
- package/src/components/routes.tsx +17 -19
- package/src/components/search-index.test.ts +15 -0
- package/src/components/search-index.ts +49 -48
- package/src/components/search.tsx +208 -104
- package/src/components/store.ts +27 -9
- package/src/config/mdx-common.ts +11 -0
- package/src/config/mdx.ts +10 -11
- package/src/embed/loader.ts +0 -3
- package/src/embed/types.ts +0 -2
- package/src/embed/widget-iframe.ts +19 -21
- package/src/embedded-runtime/config.ts +138 -0
- package/src/embedded-runtime/index.tsx +680 -0
- package/src/embedded-runtime/runtime.html +14 -0
- package/src/entry-server.tsx +46 -124
- package/src/main.tsx +16 -4
- package/src/mdx/accordion.tsx +3 -3
- package/src/mdx/api-playground.tsx +18 -16
- package/src/mdx/async-api.tsx +2 -2
- package/src/mdx/card.tsx +3 -2
- package/src/mdx/code.tsx +11 -9
- package/src/mdx/fence.tsx +6 -5
- package/src/mdx/icon-grid.tsx +4 -1
- package/src/mdx/icon-names.test.ts +15 -0
- package/src/mdx/icon-names.ts +60 -0
- package/src/mdx/icon-source-variants.ts +102 -0
- package/src/mdx/icon.tsx +12 -73
- package/src/mdx/latex.tsx +3 -2
- package/src/mdx/mdx.tsx +1 -1
- package/src/mdx/mermaid.tsx +12 -6
- package/src/mdx/open-api.tsx +130 -74
- package/src/mdx/preprocess.test.ts +16 -0
- package/src/mdx/preprocess.ts +61 -0
- package/src/mdx/processor.ts +4 -96
- package/src/mdx/source.tsx +5 -4
- package/src/mdx/specimen.tsx +38 -6
- package/src/mdx/tabs.tsx +29 -21
- package/src/mdx/text.tsx +9 -9
- package/src/mdx/try-it-panel.tsx +284 -0
- package/src/mdx/type-scale.tsx +4 -4
- package/src/mdx/websocket-playground.tsx +21 -14
- package/src/plugins/analytics-injector.ts +8 -0
- package/src/plugins/chat-dev-server.ts +32 -0
- package/src/plugins/docs-assets.test.ts +25 -0
- package/src/plugins/docs-assets.ts +6 -2
- package/src/plugins/frontmatter-dev-server.ts +2 -2
- package/src/plugins/frontmatter-generator.ts +1 -1
- package/src/plugins/html-files-middleware.test.ts +21 -1
- package/src/plugins/html-files-middleware.ts +26 -6
- package/src/plugins/html-metadata-injector.ts +24 -4
- package/src/plugins/llm-txt-dev-server.ts +2 -2
- package/src/plugins/llm-txt-generator.ts +21 -4
- package/src/plugins/prerender.ts +47 -20
- package/src/plugins/robots-generator.ts +1 -1
- package/src/plugins/sanitize.ts +1 -1
- package/src/plugins/shared.ts +6 -18
- package/src/plugins/sitemap-generator.ts +1 -1
- package/src/plugins/social-preview.test.ts +71 -0
- package/src/plugins/social-preview.ts +87 -0
- package/src/plugins/unified-mdx-plugin.ts +1 -1
- package/src/theme.scss +630 -207
- package/src/types/config.ts +53 -1
- package/src/ui/breadcrumb.tsx +17 -0
- package/src/ui/docs-header-view.tsx +32 -0
- package/src/ui/docs-navigation-view.tsx +69 -0
- package/src/ui/docs-page-frame.tsx +50 -0
- package/src/ui/dory-mark.tsx +9 -0
- package/src/ui/header.tsx +33 -90
- package/src/ui/layout.tsx +99 -153
- package/src/ui/mobile-navigation.tsx +10 -8
- package/src/ui/page-actions.tsx +67 -0
- package/src/ui/page-feedback.tsx +50 -0
- package/src/ui/page-footer.tsx +23 -0
- package/src/ui/prev-next-link.tsx +15 -69
- package/src/ui/prev-next-view.tsx +27 -0
- package/src/ui/table-of-content-view.tsx +167 -0
- package/src/ui/table-of-content.tsx +25 -183
- package/src/utils/analytics.test.ts +36 -0
- package/src/utils/analytics.ts +35 -0
- package/src/utils/colors.ts +7 -0
- package/src/utils/content-scope.test.ts +26 -0
- package/src/utils/content-scope.ts +39 -0
- package/src/utils/search-content.ts +12 -0
- package/src/utils/theme.ts +1 -7
- package/vite.config.ssr.ts +3 -3
- package/vite.config.ts +21 -15
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { preprocessMdxTags } from '../plugins/sanitize.ts';
|
|
2
|
+
|
|
3
|
+
/** Browser-safe preprocessing shared by the static build and embedded runtime. */
|
|
4
|
+
export function preprocessMdxSource(content: string, fileName = 'content.mdx'): string {
|
|
5
|
+
let processed = content;
|
|
6
|
+
const frontmatter = processed.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
7
|
+
if (frontmatter) processed = processed.slice(frontmatter[0].length);
|
|
8
|
+
|
|
9
|
+
// The page frame already renders the frontmatter title. Remove only its
|
|
10
|
+
// matching Markdown heading; headings in examples and other titles survive.
|
|
11
|
+
const rawTitle = frontmatter?.[1].match(/^title:\s*(.+?)\s*$/m)?.[1];
|
|
12
|
+
let title = rawTitle;
|
|
13
|
+
if (rawTitle?.startsWith('"')) {
|
|
14
|
+
try { title = JSON.parse(rawTitle) as string; } catch { /* Keep unfamiliar YAML unchanged. */ }
|
|
15
|
+
} else if (rawTitle?.startsWith("'") && rawTitle.endsWith("'")) {
|
|
16
|
+
title = rawTitle.slice(1, -1).replace(/''/g, "'");
|
|
17
|
+
}
|
|
18
|
+
if (title) {
|
|
19
|
+
let fence = '';
|
|
20
|
+
let removed = false;
|
|
21
|
+
processed = processed.split('\n').filter(line => {
|
|
22
|
+
const delimiter = line.trim().match(/^(`{3,}|~{3,})/);
|
|
23
|
+
if (delimiter) {
|
|
24
|
+
if (!fence) fence = delimiter[1];
|
|
25
|
+
else if (delimiter[1][0] === fence[0] && delimiter[1].length >= fence.length) fence = '';
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
if (fence || removed) return true;
|
|
29
|
+
const heading = line.match(/^#\s+(.+?)\s*#*\s*$/);
|
|
30
|
+
if (heading?.[1] !== title) return true;
|
|
31
|
+
removed = true;
|
|
32
|
+
return false;
|
|
33
|
+
}).join('\n');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const strayYaml = processed.match(/^```yaml\n([\s\S]*?)\n---\n?/);
|
|
37
|
+
if (strayYaml) processed = processed.slice(strayYaml[0].length);
|
|
38
|
+
|
|
39
|
+
const transformed = preprocessMdxTags().transform(processed, fileName);
|
|
40
|
+
processed = typeof transformed === 'string' ? transformed : transformed?.code ?? processed;
|
|
41
|
+
|
|
42
|
+
let inCodeBlock = false;
|
|
43
|
+
return processed.split('\n').map(line => {
|
|
44
|
+
if (line.trim().startsWith('```')) {
|
|
45
|
+
inCodeBlock = !inCodeBlock;
|
|
46
|
+
return line;
|
|
47
|
+
}
|
|
48
|
+
if (inCodeBlock) return line;
|
|
49
|
+
|
|
50
|
+
let output = '';
|
|
51
|
+
let lastIndex = 0;
|
|
52
|
+
const inlineCode = /`([^`]+)`/g;
|
|
53
|
+
let match: RegExpExecArray | null;
|
|
54
|
+
while ((match = inlineCode.exec(line)) !== null) {
|
|
55
|
+
output += line.slice(lastIndex, match.index).replace(/<(https?:\/\/[^\s>]+)>/g, '[$1]($1)');
|
|
56
|
+
output += match[0];
|
|
57
|
+
lastIndex = match.index + match[0].length;
|
|
58
|
+
}
|
|
59
|
+
return output + line.slice(lastIndex).replace(/<(https?:\/\/[^\s>]+)>/g, '[$1]($1)');
|
|
60
|
+
}).join('\n');
|
|
61
|
+
}
|
package/src/mdx/processor.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { compile } from '@mdx-js/mdx';
|
|
2
|
-
import {
|
|
3
|
-
import { getMdxConfig } from '../config/mdx';
|
|
2
|
+
import { preprocessMdxSource } from './preprocess.ts';
|
|
3
|
+
import { getMdxConfig } from '../config/mdx.ts';
|
|
4
4
|
|
|
5
5
|
export interface ProcessMdxOptions {
|
|
6
6
|
isDevelopment?: boolean;
|
|
@@ -25,101 +25,9 @@ export async function preprocessMdxContent(
|
|
|
25
25
|
error?: Error;
|
|
26
26
|
}> {
|
|
27
27
|
try {
|
|
28
|
-
|
|
29
|
-
// Frontmatter is parsed separately by the frontmatter-generator plugin.
|
|
30
|
-
let processedContent = content;
|
|
31
|
-
const frontMatterMatch = processedContent.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
32
|
-
if (frontMatterMatch) {
|
|
33
|
-
processedContent = processedContent.slice(frontMatterMatch[0].length);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Step 0b: Strip stray ```yaml metadata blocks that follow frontmatter.
|
|
37
|
-
// Some content generators produce a second YAML block (```yaml...---) after
|
|
38
|
-
// the frontmatter. This is a broken code fence (closed with --- instead of ```)
|
|
39
|
-
// that leaks metadata into the rendered page.
|
|
40
|
-
const yamlBlockMatch = processedContent.match(/^```yaml\n([\s\S]*?)\n---\n?/);
|
|
41
|
-
if (yamlBlockMatch) {
|
|
42
|
-
processedContent = processedContent.slice(yamlBlockMatch[0].length);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Step 1: Apply preprocessor (handles MDX tags, code blocks, etc.)
|
|
46
|
-
const preprocessor = preprocessMdxTags();
|
|
47
|
-
|
|
48
|
-
if (preprocessor.transform) {
|
|
49
|
-
const result = preprocessor.transform(processedContent, fileName);
|
|
50
|
-
if (result && typeof result === 'object' && result.code) {
|
|
51
|
-
processedContent = result.code;
|
|
52
|
-
} else if (typeof result === 'string') {
|
|
53
|
-
processedContent = result;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Step 2: Convert URLs in angle brackets to markdown links
|
|
58
|
-
// Handles cases like <https://example.com> which MDX would interpret as JSX
|
|
59
|
-
// But preserves them inside code blocks
|
|
60
|
-
|
|
61
|
-
// Process line by line to handle code blocks correctly
|
|
62
|
-
const lines = processedContent.split('\n');
|
|
63
|
-
let inCodeBlock = false;
|
|
64
|
-
|
|
65
|
-
processedContent = lines.map(line => {
|
|
66
|
-
// Check if we're entering or leaving a code block (with optional indentation)
|
|
67
|
-
if (line.trim().startsWith('```')) {
|
|
68
|
-
inCodeBlock = !inCodeBlock;
|
|
69
|
-
return line;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Don't process lines inside code blocks
|
|
73
|
-
if (inCodeBlock) {
|
|
74
|
-
return line;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// For lines outside code blocks, preserve inline code
|
|
78
|
-
// More robust regex that handles inline code with potential spaces
|
|
79
|
-
// Matches: `...` including multi-line content
|
|
80
|
-
let result = '';
|
|
81
|
-
let lastIndex = 0;
|
|
82
|
-
|
|
83
|
-
// Find all inline code spans in the line
|
|
84
|
-
const inlineCodeRegex = /`([^`]+)`/g;
|
|
85
|
-
let match;
|
|
86
|
-
|
|
87
|
-
while ((match = inlineCodeRegex.exec(line)) !== null) {
|
|
88
|
-
// Process text before the inline code
|
|
89
|
-
const beforeCode = line.substring(lastIndex, match.index);
|
|
90
|
-
result += beforeCode.replace(/<(https?:\/\/[^\s>]+)>/g, '[$1]($1)');
|
|
91
|
-
|
|
92
|
-
// Keep inline code as-is
|
|
93
|
-
result += match[0];
|
|
94
|
-
|
|
95
|
-
lastIndex = match.index + match[0].length;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Process remaining text after last inline code (or entire line if no inline code)
|
|
99
|
-
const remainingText = line.substring(lastIndex);
|
|
100
|
-
result += remainingText.replace(/<(https?:\/\/[^\s>]+)>/g, '[$1]($1)');
|
|
101
|
-
|
|
102
|
-
return result;
|
|
103
|
-
}).join('\n');
|
|
104
|
-
|
|
105
|
-
// Step 3: Add any future preprocessing steps here
|
|
106
|
-
// Example: processedContent = await yourNewPreprocessor(processedContent);
|
|
107
|
-
// Example: processedContent = removeHTMLComments(processedContent);
|
|
108
|
-
// Example: processedContent = expandCustomShortcodes(processedContent);
|
|
109
|
-
// ALL preprocessing added here will automatically apply to:
|
|
110
|
-
// - CLI commands (verify:content)
|
|
111
|
-
// - Vite build (pnpm run build)
|
|
112
|
-
// - Vite dev server (pnpm run dev)
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
success: true,
|
|
116
|
-
processedContent
|
|
117
|
-
};
|
|
28
|
+
return { success: true, processedContent: preprocessMdxSource(content, fileName) };
|
|
118
29
|
} catch (error) {
|
|
119
|
-
return {
|
|
120
|
-
success: false,
|
|
121
|
-
error: error as Error
|
|
122
|
-
};
|
|
30
|
+
return { success: false, error: error as Error };
|
|
123
31
|
}
|
|
124
32
|
}
|
|
125
33
|
|
package/src/mdx/source.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { CatalogIcon } from '@clidey/ux'
|
|
1
2
|
import classNames from 'classnames'
|
|
2
|
-
|
|
3
|
+
|
|
3
4
|
import { useEffect, useState } from 'preact/hooks'
|
|
4
5
|
import { Fence } from './fence'
|
|
5
6
|
|
|
@@ -121,7 +122,7 @@ export const Source = ({ url, branch = "main", paths }: SourceProps) => {
|
|
|
121
122
|
: 'border-[var(--border)] bg-muted text-foreground hover:border-[var(--primary)]',
|
|
122
123
|
)}
|
|
123
124
|
>
|
|
124
|
-
<
|
|
125
|
+
<CatalogIcon name="file" aria-hidden="true" className="h-3.5 w-3.5 text-muted-foreground" />
|
|
125
126
|
{path}
|
|
126
127
|
{range && <span className="text-muted-foreground">{range}</span>}
|
|
127
128
|
</button>
|
|
@@ -129,7 +130,7 @@ export const Source = ({ url, branch = "main", paths }: SourceProps) => {
|
|
|
129
130
|
</div>
|
|
130
131
|
<div className="dory-code mt-3 overflow-hidden rounded-lg border border-[var(--border)] bg-card">
|
|
131
132
|
<div className="flex min-h-10 items-center gap-2 border-b border-[var(--border)] bg-muted px-4">
|
|
132
|
-
<
|
|
133
|
+
<CatalogIcon name="file" aria-hidden="true" className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
133
134
|
<p className="mr-auto truncate font-mono text-xs text-foreground">
|
|
134
135
|
{repo} · {current.path}
|
|
135
136
|
{current.range && <span className="ml-2 text-muted-foreground">{current.range}</span>}
|
|
@@ -141,7 +142,7 @@ export const Source = ({ url, branch = "main", paths }: SourceProps) => {
|
|
|
141
142
|
className="inline-flex shrink-0 items-center gap-1 text-xs font-medium text-[var(--primary)] hover:underline"
|
|
142
143
|
>
|
|
143
144
|
{openLabel}
|
|
144
|
-
<
|
|
145
|
+
<CatalogIcon name="external" aria-hidden="true" className="h-3 w-3" />
|
|
145
146
|
</a>
|
|
146
147
|
</div>
|
|
147
148
|
{ready ? (
|
package/src/mdx/specimen.tsx
CHANGED
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
import classNames from 'classnames'
|
|
2
2
|
import { Children, isValidElement } from 'preact/compat'
|
|
3
3
|
import { useState } from 'preact/hooks'
|
|
4
|
-
import { fencePropsOf } from './code'
|
|
5
|
-
import { Pre } from './fence'
|
|
4
|
+
import { CopyButton, fencePropsOf } from './code'
|
|
5
|
+
import { Fence, Pre } from './fence'
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/** Props tab body: a PROP / DESCRIPTION header followed by <Prop> rows. */
|
|
8
8
|
export function SpecimenProps({ children }: { children: preact.ComponentChildren }) {
|
|
9
|
-
return
|
|
9
|
+
return (
|
|
10
|
+
<div className="dory-props">
|
|
11
|
+
<div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1.7fr)] gap-6 bg-muted px-5 py-2.5 font-mono text-[11px] uppercase tracking-[0.16em] text-muted-foreground">
|
|
12
|
+
<span>Prop</span>
|
|
13
|
+
<span>Description</span>
|
|
14
|
+
</div>
|
|
15
|
+
{children}
|
|
16
|
+
</div>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** One row of a Specimen's Props tab: mono name with its type underneath, and a description. */
|
|
21
|
+
export function Prop({ name, type, children }: { name: string; type?: string; children: preact.ComponentChildren }) {
|
|
22
|
+
return (
|
|
23
|
+
<div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1.7fr)] gap-6 border-t border-[var(--border)] px-5 py-3.5">
|
|
24
|
+
<div className="min-w-0">
|
|
25
|
+
<p className="font-mono text-[13px] font-semibold text-foreground">{name}</p>
|
|
26
|
+
{type && <p className="mt-0.5 font-mono text-xs text-[var(--primary)]">{type}</p>}
|
|
27
|
+
</div>
|
|
28
|
+
<div className="text-sm leading-relaxed text-muted-foreground [&_p]:mb-0">{children}</div>
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
10
31
|
}
|
|
11
32
|
|
|
12
33
|
type SpecimenTab = 'Preview' | 'Code' | 'Props'
|
|
@@ -61,8 +82,19 @@ export function Specimen({ label, children }: { label?: string; children: preact
|
|
|
61
82
|
</div>
|
|
62
83
|
)}
|
|
63
84
|
</div>
|
|
64
|
-
<div role="tabpanel" className=
|
|
65
|
-
{
|
|
85
|
+
<div role="tabpanel" className={classNames('[&>:first-child]:mt-0 [&>:last-child]:mb-0', { 'p-5': active === 'Preview' })}>
|
|
86
|
+
{active === 'Code'
|
|
87
|
+
? code.map((child, index) => {
|
|
88
|
+
// Bare source with a copy button, without the filename frame a standalone fence gets.
|
|
89
|
+
const fence = fencePropsOf(child as preact.JSX.Element)
|
|
90
|
+
return (
|
|
91
|
+
<div key={index} className="relative px-5 py-4 font-mono text-[12.5px] leading-[1.7]">
|
|
92
|
+
<Fence language={fence.language} meta={{ ...fence.meta, title: undefined }}>{fence.code}</Fence>
|
|
93
|
+
<CopyButton code={fence.code} />
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
: panels[active]}
|
|
66
98
|
</div>
|
|
67
99
|
</div>
|
|
68
100
|
)
|
package/src/mdx/tabs.tsx
CHANGED
|
@@ -1,39 +1,47 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Tabs as UxTabs, TabsContent, TabsList, TabsTrigger } from '@clidey/ux'
|
|
2
2
|
import { Children, isValidElement } from 'preact/compat'
|
|
3
|
-
import { useState } from 'preact/hooks'
|
|
4
3
|
|
|
5
4
|
/** One tab panel inside <Tabs>. Renders its children when active. */
|
|
6
5
|
export function Tab({ children }: { title: string; children: preact.ComponentChildren }) {
|
|
7
6
|
return <>{children}</>
|
|
8
7
|
}
|
|
9
8
|
|
|
10
|
-
/**
|
|
9
|
+
/** UX tab strip for any MDX content, e.g. several diagrams of one thing. */
|
|
11
10
|
export function Tabs({ children }: { children: preact.ComponentChildren }) {
|
|
12
11
|
const panels = Children.toArray(children).filter(isValidElement) as preact.VNode<{ title: string }>[]
|
|
13
|
-
const [active, setActive] = useState(0)
|
|
14
12
|
|
|
15
13
|
return (
|
|
16
|
-
<
|
|
17
|
-
<
|
|
14
|
+
<UxTabs defaultValue="0" className="dory-tabs not-prose my-5">
|
|
15
|
+
<TabsList
|
|
16
|
+
className="max-w-full justify-start overflow-x-auto"
|
|
17
|
+
onKeyDown={(event) => {
|
|
18
|
+
// Radix roving focus does not handle arrow keys through Preact compat.
|
|
19
|
+
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return
|
|
20
|
+
const tabs = Array.from(event.currentTarget.querySelectorAll<HTMLButtonElement>('[role="tab"]:not([disabled])'))
|
|
21
|
+
const current = tabs.indexOf(event.target as HTMLButtonElement)
|
|
22
|
+
if (current < 0 || tabs.length === 0) return
|
|
23
|
+
event.preventDefault()
|
|
24
|
+
const next = event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1
|
|
25
|
+
: (current + (event.key === 'ArrowRight' ? 1 : -1) + tabs.length) % tabs.length
|
|
26
|
+
tabs[next].focus()
|
|
27
|
+
tabs[next].click()
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
18
30
|
{panels.map((panel, index) => (
|
|
19
|
-
<
|
|
31
|
+
<TabsTrigger
|
|
20
32
|
key={index}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
aria-selected={active === index}
|
|
24
|
-
onClick={() => setActive(index)}
|
|
25
|
-
className={classNames('-mb-px border-b-2 pb-2.5 pt-1 text-sm font-medium transition', {
|
|
26
|
-
'border-[var(--primary)] text-foreground': active === index,
|
|
27
|
-
'border-transparent text-muted-foreground hover:text-foreground': active !== index,
|
|
28
|
-
})}
|
|
33
|
+
value={String(index)}
|
|
34
|
+
className="whitespace-nowrap"
|
|
29
35
|
>
|
|
30
36
|
{panel.props.title}
|
|
31
|
-
</
|
|
37
|
+
</TabsTrigger>
|
|
32
38
|
))}
|
|
33
|
-
</
|
|
34
|
-
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
</TabsList>
|
|
40
|
+
{panels.map((panel, index) => (
|
|
41
|
+
<TabsContent key={index} value={String(index)} className="pt-4 [&>:first-child]:mt-0 [&>:last-child]:mb-0">
|
|
42
|
+
{panel}
|
|
43
|
+
</TabsContent>
|
|
44
|
+
))}
|
|
45
|
+
</UxTabs>
|
|
38
46
|
)
|
|
39
47
|
}
|
package/src/mdx/text.tsx
CHANGED
|
@@ -4,36 +4,36 @@ import classNames from "classnames"
|
|
|
4
4
|
import { useState } from "preact/hooks"
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
export const h1 = function H1(props: preact.
|
|
7
|
+
export const h1 = function H1(props: preact.ComponentProps<'h1'>) {
|
|
8
8
|
return <h1 className="dory-h1 font-heading mb-3 mt-10" {...props} />
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export const h2 = function H2({ children, ...props }: preact.
|
|
11
|
+
export const h2 = function H2({ children, ...props }: preact.ComponentProps<'h2'>) {
|
|
12
12
|
return (
|
|
13
|
-
<h2 className="dory-h2 font-heading mb-
|
|
13
|
+
<h2 className="dory-h2 font-heading mb-4 mt-16" {...props}>
|
|
14
14
|
<span>{children}</span>
|
|
15
15
|
{props.id && <a href={`#${props.id}`} className="dory-anchor" aria-label="Link to this section">#</a>}
|
|
16
16
|
</h2>
|
|
17
17
|
)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export const h3 = function H3(props: preact.
|
|
21
|
-
return <h3 className="dory-h3 font-heading mb-
|
|
20
|
+
export const h3 = function H3(props: preact.ComponentProps<'h3'>) {
|
|
21
|
+
return <h3 className="dory-h3 font-heading mb-3 mt-10" {...props} />
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export const h4 = function H4(props: preact.
|
|
24
|
+
export const h4 = function H4(props: preact.ComponentProps<'h4'>) {
|
|
25
25
|
return <h4 className="dory-h4 font-heading mb-1.5 mt-5" {...props} />
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export const h5 = function H5(props: preact.
|
|
28
|
+
export const h5 = function H5(props: preact.ComponentProps<'h5'>) {
|
|
29
29
|
return <h5 className="text-lg font-bold mb-2" {...props} />
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export const h6 = function H6(props: preact.
|
|
32
|
+
export const h6 = function H6(props: preact.ComponentProps<'h6'>) {
|
|
33
33
|
return <h6 className="text-base font-bold mb-2" {...props} />
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export const p = function P(props: preact.
|
|
36
|
+
export const p = function P(props: preact.ComponentProps<'p'>) {
|
|
37
37
|
return <p className="mb-2 inline-block" {...props} />
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { CatalogIcon, Sheet, SheetContent, SheetTitle } from '@clidey/ux'
|
|
2
|
+
import classNames from 'classnames'
|
|
3
|
+
|
|
4
|
+
import { useEffect, useState } from 'preact/hooks'
|
|
5
|
+
import type { Server } from './api-playground'
|
|
6
|
+
import { Fence } from './fence'
|
|
7
|
+
import { Tag } from './tag'
|
|
8
|
+
|
|
9
|
+
/** An editable request value: a body property, a path/query parameter, or an auth credential. */
|
|
10
|
+
export interface TryItField {
|
|
11
|
+
name: string
|
|
12
|
+
/** Short type shown beside the name, e.g. `string`, `integer`, `enum`. */
|
|
13
|
+
type: string
|
|
14
|
+
value: string
|
|
15
|
+
/** Where a parameter goes. Body fields leave this unset. */
|
|
16
|
+
in?: 'path' | 'query' | 'header'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface TryItPanelProps {
|
|
20
|
+
open: boolean
|
|
21
|
+
onOpenChange: (open: boolean) => void
|
|
22
|
+
method: string
|
|
23
|
+
path: string
|
|
24
|
+
servers: Server[]
|
|
25
|
+
parameters: TryItField[]
|
|
26
|
+
body: TryItField[]
|
|
27
|
+
/** Auth headers, e.g. `{ name: 'Authorization', value: 'Bearer ' }`. */
|
|
28
|
+
auth: TryItField[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ResponseData {
|
|
32
|
+
status: number
|
|
33
|
+
statusText: string
|
|
34
|
+
duration: number
|
|
35
|
+
body: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Turns a form string back into JSON for its declared type; unparseable values stay strings. */
|
|
39
|
+
function bodyValue(field: TryItField): unknown {
|
|
40
|
+
if (field.type === 'integer' || field.type === 'number') return Number(field.value)
|
|
41
|
+
if (field.type === 'boolean') return field.value === 'true'
|
|
42
|
+
if (field.type === 'object' || field.type.startsWith('array')) {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(field.value)
|
|
45
|
+
} catch {
|
|
46
|
+
return field.value
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return field.value
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function FieldInput({ field, onChange }: { field: TryItField; onChange: (value: string) => void }) {
|
|
53
|
+
return (
|
|
54
|
+
<label className="block">
|
|
55
|
+
<span className="flex items-baseline gap-2">
|
|
56
|
+
<span className="font-mono text-sm font-semibold text-foreground">{field.name}</span>
|
|
57
|
+
<span className="font-mono text-xs text-[var(--primary)]">{field.type}</span>
|
|
58
|
+
</span>
|
|
59
|
+
<input
|
|
60
|
+
type="text"
|
|
61
|
+
value={field.value}
|
|
62
|
+
onInput={(event) => onChange((event.target as HTMLInputElement).value)}
|
|
63
|
+
className="dory-tryit-input mt-1.5"
|
|
64
|
+
/>
|
|
65
|
+
</label>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Right-hand "Try it" panel for a generated API page: server picker, a form
|
|
71
|
+
* built from the operation's body, parameters and auth, and the live response.
|
|
72
|
+
*/
|
|
73
|
+
export function TryItPanel({ open, onOpenChange, method, path, servers, parameters, body, auth }: TryItPanelProps) {
|
|
74
|
+
const [serverUrl, setServerUrl] = useState(servers[0]?.url ?? '')
|
|
75
|
+
const [params, setParams] = useState(parameters)
|
|
76
|
+
const [bodyFields, setBodyFields] = useState(body)
|
|
77
|
+
const [authFields, setAuthFields] = useState(auth)
|
|
78
|
+
const [headers, setHeaders] = useState<Array<{ key: string; value: string }>>([])
|
|
79
|
+
const tabs = [
|
|
80
|
+
bodyFields.length > 0 && 'Body',
|
|
81
|
+
params.length > 0 && 'Params',
|
|
82
|
+
'Headers',
|
|
83
|
+
authFields.length > 0 && 'Auth',
|
|
84
|
+
].filter((tab): tab is string => Boolean(tab))
|
|
85
|
+
const [tab, setTab] = useState(tabs[0])
|
|
86
|
+
const [loading, setLoading] = useState(false)
|
|
87
|
+
const [response, setResponse] = useState<ResponseData | null>(null)
|
|
88
|
+
const [error, setError] = useState<string | null>(null)
|
|
89
|
+
|
|
90
|
+
// A different operation (client-side navigation) starts from its own defaults.
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
setParams(parameters)
|
|
93
|
+
setBodyFields(body)
|
|
94
|
+
setAuthFields(auth)
|
|
95
|
+
setResponse(null)
|
|
96
|
+
setError(null)
|
|
97
|
+
}, [method, path])
|
|
98
|
+
|
|
99
|
+
const update = (setter: typeof setParams, index: number, value: string) =>
|
|
100
|
+
setter((fields) => fields.map((field, i) => (i === index ? { ...field, value } : field)))
|
|
101
|
+
|
|
102
|
+
async function send() {
|
|
103
|
+
setLoading(true)
|
|
104
|
+
setError(null)
|
|
105
|
+
setResponse(null)
|
|
106
|
+
|
|
107
|
+
let url = `${serverUrl.replace(/\/$/, '')}${path}`
|
|
108
|
+
const query = new URLSearchParams()
|
|
109
|
+
const requestHeaders: Record<string, string> = {}
|
|
110
|
+
for (const param of params) {
|
|
111
|
+
if (param.in === 'path') url = url.replace(`{${param.name}}`, encodeURIComponent(param.value))
|
|
112
|
+
else if (param.in === 'header' && param.value) requestHeaders[param.name] = param.value
|
|
113
|
+
else if (param.value) query.set(param.name, param.value)
|
|
114
|
+
}
|
|
115
|
+
const queryString = query.toString()
|
|
116
|
+
if (queryString) url += `?${queryString}`
|
|
117
|
+
for (const field of authFields) if (field.value.trim()) requestHeaders[field.name] = field.value
|
|
118
|
+
for (const header of headers) if (header.key.trim()) requestHeaders[header.key] = header.value
|
|
119
|
+
|
|
120
|
+
const init: RequestInit = { method: method.toUpperCase(), headers: requestHeaders }
|
|
121
|
+
const filled = bodyFields.filter((field) => field.value !== '')
|
|
122
|
+
if (filled.length > 0) {
|
|
123
|
+
requestHeaders['Content-Type'] = 'application/json'
|
|
124
|
+
init.body = JSON.stringify(Object.fromEntries(filled.map((field) => [field.name, bodyValue(field)])))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const started = performance.now()
|
|
128
|
+
try {
|
|
129
|
+
const res = await fetch(url, init)
|
|
130
|
+
const text = await res.text()
|
|
131
|
+
let pretty = text
|
|
132
|
+
try {
|
|
133
|
+
pretty = JSON.stringify(JSON.parse(text), null, 2)
|
|
134
|
+
} catch {
|
|
135
|
+
// Not JSON; show the raw text.
|
|
136
|
+
}
|
|
137
|
+
setResponse({ status: res.status, statusText: res.statusText, duration: Math.round(performance.now() - started), body: pretty })
|
|
138
|
+
} catch (err) {
|
|
139
|
+
setError(err instanceof Error ? err.message : 'Request failed')
|
|
140
|
+
} finally {
|
|
141
|
+
setLoading(false)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
147
|
+
<SheetContent
|
|
148
|
+
side="right"
|
|
149
|
+
className="dory-tryit w-full gap-0 bg-background sm:max-w-[34rem]"
|
|
150
|
+
footer={
|
|
151
|
+
<div className="flex gap-3">
|
|
152
|
+
<button type="button" className="dory-tryit-cancel" onClick={() => onOpenChange(false)}>Cancel</button>
|
|
153
|
+
<button type="button" className="dory-tryit-send" onClick={send} disabled={loading}>
|
|
154
|
+
<CatalogIcon name="play" aria-hidden="true" className="h-3 w-3 fill-current" />
|
|
155
|
+
{loading ? 'Sending…' : 'Send request'}
|
|
156
|
+
</button>
|
|
157
|
+
</div>
|
|
158
|
+
}
|
|
159
|
+
>
|
|
160
|
+
<div className="-mx-4 -mt-4 flex items-center gap-3 border-b border-[var(--border)] px-5 py-4 pr-12">
|
|
161
|
+
<Tag>{method}</Tag>
|
|
162
|
+
<SheetTitle className="truncate font-mono text-base font-medium">{path}</SheetTitle>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<div className="space-y-6 px-1 pt-5">
|
|
166
|
+
{servers.length > 0 && (
|
|
167
|
+
<label className="block">
|
|
168
|
+
<span className="dory-tryit-label">Server</span>
|
|
169
|
+
<span className="relative mt-2 block">
|
|
170
|
+
<select
|
|
171
|
+
value={serverUrl}
|
|
172
|
+
onChange={(event) => setServerUrl((event.target as HTMLSelectElement).value)}
|
|
173
|
+
className="dory-tryit-input appearance-none pr-9 font-sans"
|
|
174
|
+
>
|
|
175
|
+
{servers.map((server) => (
|
|
176
|
+
<option key={server.url} value={server.url}>
|
|
177
|
+
{server.description ? `${server.description} · ${server.url}` : server.url}
|
|
178
|
+
</option>
|
|
179
|
+
))}
|
|
180
|
+
</select>
|
|
181
|
+
<CatalogIcon name="up-down" aria-hidden="true" className="pointer-events-none absolute right-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
|
182
|
+
</span>
|
|
183
|
+
</label>
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
<div>
|
|
187
|
+
<div role="tablist" className="flex gap-5 border-b border-[var(--border)]">
|
|
188
|
+
{tabs.map((name) => (
|
|
189
|
+
<button
|
|
190
|
+
key={name}
|
|
191
|
+
type="button"
|
|
192
|
+
role="tab"
|
|
193
|
+
aria-selected={tab === name}
|
|
194
|
+
onClick={() => setTab(name)}
|
|
195
|
+
className={classNames('-mb-px border-b-2 pb-2 text-sm font-medium transition', {
|
|
196
|
+
'border-[var(--primary)] text-foreground': tab === name,
|
|
197
|
+
'border-transparent text-muted-foreground hover:text-foreground': tab !== name,
|
|
198
|
+
})}
|
|
199
|
+
>
|
|
200
|
+
{name}
|
|
201
|
+
</button>
|
|
202
|
+
))}
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<div className="space-y-4 pt-4">
|
|
206
|
+
{tab === 'Body' && bodyFields.map((field, index) => (
|
|
207
|
+
<FieldInput key={field.name} field={field} onChange={(value) => update(setBodyFields, index, value)} />
|
|
208
|
+
))}
|
|
209
|
+
{tab === 'Params' && params.map((field, index) => (
|
|
210
|
+
<FieldInput key={`${field.in}-${field.name}`} field={field} onChange={(value) => update(setParams, index, value)} />
|
|
211
|
+
))}
|
|
212
|
+
{tab === 'Auth' && authFields.map((field, index) => (
|
|
213
|
+
<FieldInput key={field.name} field={field} onChange={(value) => update(setAuthFields, index, value)} />
|
|
214
|
+
))}
|
|
215
|
+
{tab === 'Headers' && (
|
|
216
|
+
<>
|
|
217
|
+
{headers.map((header, index) => (
|
|
218
|
+
<div key={index} className="flex gap-2">
|
|
219
|
+
<input
|
|
220
|
+
type="text"
|
|
221
|
+
placeholder="Header"
|
|
222
|
+
value={header.key}
|
|
223
|
+
onInput={(event) => setHeaders((list) => list.map((h, i) => (i === index ? { ...h, key: (event.target as HTMLInputElement).value } : h)))}
|
|
224
|
+
className="dory-tryit-input w-2/5"
|
|
225
|
+
/>
|
|
226
|
+
<input
|
|
227
|
+
type="text"
|
|
228
|
+
placeholder="Value"
|
|
229
|
+
value={header.value}
|
|
230
|
+
onInput={(event) => setHeaders((list) => list.map((h, i) => (i === index ? { ...h, value: (event.target as HTMLInputElement).value } : h)))}
|
|
231
|
+
className="dory-tryit-input flex-1"
|
|
232
|
+
/>
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
aria-label="Remove header"
|
|
236
|
+
onClick={() => setHeaders((list) => list.filter((_, i) => i !== index))}
|
|
237
|
+
className="rounded-lg px-2 text-muted-foreground hover:text-foreground"
|
|
238
|
+
>
|
|
239
|
+
<CatalogIcon name="trash" className="h-4 w-4" />
|
|
240
|
+
</button>
|
|
241
|
+
</div>
|
|
242
|
+
))}
|
|
243
|
+
<button
|
|
244
|
+
type="button"
|
|
245
|
+
onClick={() => setHeaders((list) => [...list, { key: '', value: '' }])}
|
|
246
|
+
className="flex items-center gap-1.5 text-sm font-medium text-[var(--primary)]"
|
|
247
|
+
>
|
|
248
|
+
<CatalogIcon name="plus" className="h-3.5 w-3.5" />
|
|
249
|
+
Add header
|
|
250
|
+
</button>
|
|
251
|
+
</>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<div className="pb-2">
|
|
257
|
+
<div className="flex items-center justify-between">
|
|
258
|
+
<span className="dory-tryit-label">Response</span>
|
|
259
|
+
{response && (
|
|
260
|
+
<span className="flex items-center gap-2 font-mono text-xs">
|
|
261
|
+
<span className={classNames('font-semibold', response.status < 300 ? 'text-green-600' : response.status < 400 ? 'text-[var(--highlight)]' : 'text-[var(--destructive)]')}>
|
|
262
|
+
{response.status} {response.statusText}
|
|
263
|
+
</span>
|
|
264
|
+
<span className="text-muted-foreground">{response.duration}ms</span>
|
|
265
|
+
</span>
|
|
266
|
+
)}
|
|
267
|
+
</div>
|
|
268
|
+
<div className="dory-tryit-response mt-2">
|
|
269
|
+
{error ? (
|
|
270
|
+
<p className="p-4 font-mono text-sm text-[var(--destructive)]">{error}</p>
|
|
271
|
+
) : response ? (
|
|
272
|
+
<div className="overflow-x-auto p-4 font-mono text-[12.5px] leading-[1.7]">
|
|
273
|
+
<Fence language="json">{response.body}</Fence>
|
|
274
|
+
</div>
|
|
275
|
+
) : (
|
|
276
|
+
<p className="flex min-h-36 items-center justify-center text-sm text-muted-foreground">Send the request to see the response</p>
|
|
277
|
+
)}
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</SheetContent>
|
|
282
|
+
</Sheet>
|
|
283
|
+
)
|
|
284
|
+
}
|