@opendocsdev/cli 0.2.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/LICENSE +661 -0
- package/README.md +300 -0
- package/dist/bin/opendocs.js +712 -0
- package/dist/bin/opendocs.js.map +1 -0
- package/dist/templates/api-reference.mdx +308 -0
- package/dist/templates/components.mdx +286 -0
- package/dist/templates/configuration.mdx +190 -0
- package/dist/templates/docs.json +27 -0
- package/dist/templates/introduction.mdx +25 -0
- package/dist/templates/logo.svg +4 -0
- package/dist/templates/quickstart.mdx +59 -0
- package/dist/templates/writing-content.mdx +236 -0
- package/package.json +92 -0
- package/src/engine/astro.config.ts +75 -0
- package/src/engine/src/components/Analytics.astro +57 -0
- package/src/engine/src/components/ApiPlayground.astro +24 -0
- package/src/engine/src/components/Callout.astro +66 -0
- package/src/engine/src/components/Card.astro +75 -0
- package/src/engine/src/components/CardGroup.astro +29 -0
- package/src/engine/src/components/CodeGroup.astro +231 -0
- package/src/engine/src/components/CopyButton.astro +179 -0
- package/src/engine/src/components/Steps.astro +27 -0
- package/src/engine/src/components/Tab.astro +21 -0
- package/src/engine/src/components/TableOfContents.astro +119 -0
- package/src/engine/src/components/Tabs.astro +135 -0
- package/src/engine/src/components/index.ts +107 -0
- package/src/engine/src/components/react/ApiPlayground/AuthSection.tsx +91 -0
- package/src/engine/src/components/react/ApiPlayground/CodeBlock.tsx +66 -0
- package/src/engine/src/components/react/ApiPlayground/CodeSnippets.tsx +66 -0
- package/src/engine/src/components/react/ApiPlayground/CollapsibleSection.tsx +26 -0
- package/src/engine/src/components/react/ApiPlayground/KeyValueEditor.tsx +58 -0
- package/src/engine/src/components/react/ApiPlayground/ResponseDisplay.tsx +109 -0
- package/src/engine/src/components/react/ApiPlayground/Spinner.tsx +6 -0
- package/src/engine/src/components/react/ApiPlayground/constants.ts +16 -0
- package/src/engine/src/components/react/ApiPlayground/generators.test.ts +130 -0
- package/src/engine/src/components/react/ApiPlayground/generators.ts +75 -0
- package/src/engine/src/components/react/ApiPlayground/index.tsx +490 -0
- package/src/engine/src/components/react/ApiPlayground/types.ts +35 -0
- package/src/engine/src/components/react/Callout.tsx +54 -0
- package/src/engine/src/components/react/Card.tsx +48 -0
- package/src/engine/src/components/react/CardGroup.tsx +24 -0
- package/src/engine/src/components/react/FeedbackWidget.tsx +166 -0
- package/src/engine/src/components/react/GitHubLink.tsx +28 -0
- package/src/engine/src/components/react/NavigationCard.tsx +53 -0
- package/src/engine/src/components/react/PageActions.tsx +124 -0
- package/src/engine/src/components/react/PageFooter.tsx +91 -0
- package/src/engine/src/components/react/SearchModal.tsx +358 -0
- package/src/engine/src/components/react/SearchProvider.tsx +37 -0
- package/src/engine/src/components/react/Sidebar.tsx +369 -0
- package/src/engine/src/components/react/SidebarSearchTrigger.tsx +57 -0
- package/src/engine/src/components/react/Steps.tsx +25 -0
- package/src/engine/src/components/react/ThemeToggle.tsx +72 -0
- package/src/engine/src/components/react/index.ts +14 -0
- package/src/engine/src/env.d.ts +10 -0
- package/src/engine/src/layouts/DocsLayout.astro +357 -0
- package/src/engine/src/lib/__tests__/markdown.test.ts +124 -0
- package/src/engine/src/lib/__tests__/mdx-loader.test.ts +205 -0
- package/src/engine/src/lib/config.ts +79 -0
- package/src/engine/src/lib/markdown.ts +54 -0
- package/src/engine/src/lib/mdx-loader.ts +143 -0
- package/src/engine/src/lib/mdx-utils.ts +72 -0
- package/src/engine/src/lib/remark-opendocs.ts +195 -0
- package/src/engine/src/lib/utils.ts +221 -0
- package/src/engine/src/pages/[...slug].astro +115 -0
- package/src/engine/src/pages/index.astro +71 -0
- package/src/engine/src/scripts/mobile-sidebar.ts +56 -0
- package/src/engine/src/scripts/theme-init.ts +25 -0
- package/src/engine/src/styles/global.css +703 -0
- package/src/engine/tailwind.config.mjs +60 -0
- package/src/engine/tsconfig.json +15 -0
- package/src/templates/api-reference.mdx +308 -0
- package/src/templates/components.mdx +286 -0
- package/src/templates/configuration.mdx +190 -0
- package/src/templates/docs.json +27 -0
- package/src/templates/introduction.mdx +25 -0
- package/src/templates/logo.svg +4 -0
- package/src/templates/quickstart.mdx +59 -0
- package/src/templates/writing-content.mdx +236 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Tabs - Content tabs component
|
|
4
|
+
* Server-renders tab buttons to eliminate FOUC
|
|
5
|
+
* Uses Web Components for click handlers
|
|
6
|
+
*/
|
|
7
|
+
interface TabInfo {
|
|
8
|
+
label: string;
|
|
9
|
+
icon?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
"data-tabs"?: string;
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { "data-tabs": tabsJson, class: className } = Astro.props;
|
|
18
|
+
|
|
19
|
+
// Parse tabs from remark plugin (or empty array for fallback)
|
|
20
|
+
const tabs: TabInfo[] = tabsJson ? JSON.parse(tabsJson) : [];
|
|
21
|
+
const showTabs = tabs.length > 1;
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<content-tabs
|
|
25
|
+
class:list={["tabs-container my-6 block", className]}
|
|
26
|
+
data-tabs={tabsJson}
|
|
27
|
+
>
|
|
28
|
+
{showTabs && (
|
|
29
|
+
<nav
|
|
30
|
+
class="tabs-nav flex border-b border-[var(--color-border)]"
|
|
31
|
+
role="tablist"
|
|
32
|
+
aria-label="Content tabs"
|
|
33
|
+
>
|
|
34
|
+
{tabs.map((tab, i) => (
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
role="tab"
|
|
38
|
+
class:list={[
|
|
39
|
+
"px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px",
|
|
40
|
+
i === 0
|
|
41
|
+
? "border-[var(--color-primary)] text-[var(--color-primary)]"
|
|
42
|
+
: "border-transparent text-[var(--color-muted)] hover:text-[var(--color-foreground)] hover:border-[var(--color-border)]",
|
|
43
|
+
]}
|
|
44
|
+
aria-selected={i === 0 ? "true" : "false"}
|
|
45
|
+
tabindex={i === 0 ? 0 : -1}
|
|
46
|
+
data-index={i}
|
|
47
|
+
>
|
|
48
|
+
{tab.icon && (
|
|
49
|
+
<span class="mr-2" aria-hidden="true">
|
|
50
|
+
{tab.icon}
|
|
51
|
+
</span>
|
|
52
|
+
)}
|
|
53
|
+
{tab.label}
|
|
54
|
+
</button>
|
|
55
|
+
))}
|
|
56
|
+
</nav>
|
|
57
|
+
)}
|
|
58
|
+
<div class="tabs-panels pt-4">
|
|
59
|
+
<slot />
|
|
60
|
+
</div>
|
|
61
|
+
</content-tabs>
|
|
62
|
+
|
|
63
|
+
<script>
|
|
64
|
+
class ContentTabsElement extends HTMLElement {
|
|
65
|
+
private panels: HTMLElement[] = [];
|
|
66
|
+
|
|
67
|
+
connectedCallback() {
|
|
68
|
+
const panelsContainer = this.querySelector(".tabs-panels");
|
|
69
|
+
if (!panelsContainer) return;
|
|
70
|
+
|
|
71
|
+
this.panels = Array.from(
|
|
72
|
+
panelsContainer.querySelectorAll(":scope > .tab-panel")
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (this.panels.length === 0) return;
|
|
76
|
+
|
|
77
|
+
// Initialize panels with data-active attribute
|
|
78
|
+
this.initPanels();
|
|
79
|
+
|
|
80
|
+
// If only one panel, just mark it active
|
|
81
|
+
if (this.panels.length === 1) {
|
|
82
|
+
this.panels[0].setAttribute("data-active", "true");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Add click handlers to server-rendered buttons
|
|
87
|
+
this.attachClickHandlers();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private initPanels() {
|
|
91
|
+
this.panels.forEach((panel, index) => {
|
|
92
|
+
panel.setAttribute("role", "tabpanel");
|
|
93
|
+
panel.setAttribute("data-active", String(index === 0));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private attachClickHandlers() {
|
|
98
|
+
const buttons = this.querySelectorAll<HTMLButtonElement>(
|
|
99
|
+
".tabs-nav button"
|
|
100
|
+
);
|
|
101
|
+
buttons.forEach((btn, index) => {
|
|
102
|
+
btn.addEventListener("click", () => this.setActiveTab(index));
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private setActiveTab(index: number) {
|
|
107
|
+
// Update buttons
|
|
108
|
+
const buttons = this.querySelectorAll<HTMLButtonElement>(
|
|
109
|
+
".tabs-nav button"
|
|
110
|
+
);
|
|
111
|
+
buttons.forEach((btn, i) => {
|
|
112
|
+
const isActive = i === index;
|
|
113
|
+
btn.className = this.getTabClass(isActive);
|
|
114
|
+
btn.setAttribute("aria-selected", String(isActive));
|
|
115
|
+
btn.tabIndex = isActive ? 0 : -1;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Update panels
|
|
119
|
+
this.panels.forEach((panel, i) => {
|
|
120
|
+
panel.setAttribute("data-active", String(i === index));
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private getTabClass(isActive: boolean): string {
|
|
125
|
+
const base =
|
|
126
|
+
"px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px";
|
|
127
|
+
const active = "border-[var(--color-primary)] text-[var(--color-primary)]";
|
|
128
|
+
const inactive =
|
|
129
|
+
"border-transparent text-[var(--color-muted)] hover:text-[var(--color-foreground)] hover:border-[var(--color-border)]";
|
|
130
|
+
return `${base} ${isActive ? active : inactive}`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
customElements.define("content-tabs", ContentTabsElement);
|
|
135
|
+
</script>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MDX Components for opendocs
|
|
3
|
+
*
|
|
4
|
+
* These Astro components are available for use in MDX files:
|
|
5
|
+
*
|
|
6
|
+
* - Callout: Info, warning, error, and tip callouts
|
|
7
|
+
* Props: type?: "info" | "warning" | "error" | "tip"
|
|
8
|
+
*
|
|
9
|
+
* - CodeGroup: Tabbed code blocks
|
|
10
|
+
* Props: labels?: string[]
|
|
11
|
+
*
|
|
12
|
+
* - Card: Linked content cards with title and description
|
|
13
|
+
* Props: title: string, description?: string, href?: string, icon?: string
|
|
14
|
+
*
|
|
15
|
+
* - CardGroup: Grid layout wrapper for Card components
|
|
16
|
+
* Props: cols?: 1 | 2 | 3 | 4 (default: 2)
|
|
17
|
+
*
|
|
18
|
+
* - Steps: Numbered step lists with vertical line connector
|
|
19
|
+
* Props: title?: string
|
|
20
|
+
*
|
|
21
|
+
* - Tabs: Tabbed content sections for organizing related content
|
|
22
|
+
* Usage: <Tabs><Tab label="First" icon="🚀">Content</Tab></Tabs>
|
|
23
|
+
*
|
|
24
|
+
* Usage in MDX:
|
|
25
|
+
* ```mdx
|
|
26
|
+
* import { Callout, CodeGroup, Card, Steps } from '../components';
|
|
27
|
+
*
|
|
28
|
+
* <Callout type="info">This is an info callout</Callout>
|
|
29
|
+
*
|
|
30
|
+
* <CodeGroup labels={["npm", "yarn", "pnpm"]}>
|
|
31
|
+
* ```bash
|
|
32
|
+
* npm install
|
|
33
|
+
* ```
|
|
34
|
+
* ```bash
|
|
35
|
+
* yarn
|
|
36
|
+
* ```
|
|
37
|
+
* ```bash
|
|
38
|
+
* pnpm install
|
|
39
|
+
* ```
|
|
40
|
+
* </CodeGroup>
|
|
41
|
+
*
|
|
42
|
+
* <Card title="Getting Started" description="Learn how to get started" href="/getting-started" />
|
|
43
|
+
*
|
|
44
|
+
* <Steps>
|
|
45
|
+
* ### Step 1
|
|
46
|
+
* First, do this...
|
|
47
|
+
*
|
|
48
|
+
* ### Step 2
|
|
49
|
+
* Then, do that...
|
|
50
|
+
* </Steps>
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
// Component type definitions for documentation
|
|
55
|
+
export interface CalloutProps {
|
|
56
|
+
type?: "info" | "warning" | "error" | "tip";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface CodeGroupProps {
|
|
60
|
+
labels?: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface CardProps {
|
|
64
|
+
title: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
href?: string;
|
|
67
|
+
icon?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface CardGroupProps {
|
|
71
|
+
cols?: 1 | 2 | 3 | 4;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface StepsProps {
|
|
75
|
+
title?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface TabsProps {
|
|
79
|
+
// Tabs container has no props, it wraps Tab children
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface TabProps {
|
|
83
|
+
label: string;
|
|
84
|
+
icon?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Analytics Component Props
|
|
89
|
+
* Tracks pageviews to the backend analytics endpoint
|
|
90
|
+
*/
|
|
91
|
+
export interface AnalyticsProps {
|
|
92
|
+
path?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Export component names for reference (actual components are .astro files)
|
|
96
|
+
export const componentNames = [
|
|
97
|
+
"Callout",
|
|
98
|
+
"CodeGroup",
|
|
99
|
+
"Card",
|
|
100
|
+
"CardGroup",
|
|
101
|
+
"Steps",
|
|
102
|
+
"Tabs",
|
|
103
|
+
"Tab",
|
|
104
|
+
"Sidebar",
|
|
105
|
+
"TableOfContents",
|
|
106
|
+
"Analytics",
|
|
107
|
+
] as const;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { AuthState } from "./types";
|
|
3
|
+
import { DEFAULT_AUTH_STATE } from "./constants";
|
|
4
|
+
|
|
5
|
+
interface AuthSectionProps {
|
|
6
|
+
authState: AuthState;
|
|
7
|
+
onAuthChange: (auth: AuthState) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function AuthSection({ authState, onAuthChange }: AuthSectionProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="space-y-3 max-w-full overflow-hidden">
|
|
13
|
+
<select
|
|
14
|
+
value={authState.type}
|
|
15
|
+
onChange={(e) => onAuthChange({ ...authState, type: e.target.value as AuthState["type"] })}
|
|
16
|
+
className="w-full px-3 py-2 text-sm border border-[var(--color-border)] rounded-md bg-[var(--color-surface)] text-[var(--color-foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
|
17
|
+
>
|
|
18
|
+
<option value="none">No Authentication</option>
|
|
19
|
+
<option value="bearer">Bearer Token</option>
|
|
20
|
+
<option value="apikey">API Key</option>
|
|
21
|
+
<option value="basic">Basic Auth</option>
|
|
22
|
+
</select>
|
|
23
|
+
|
|
24
|
+
{authState.type === "bearer" && (
|
|
25
|
+
<input
|
|
26
|
+
type="password"
|
|
27
|
+
value={authState.bearer.token}
|
|
28
|
+
onChange={(e) => onAuthChange({ ...authState, bearer: { token: e.target.value } })}
|
|
29
|
+
placeholder="Enter token"
|
|
30
|
+
className="w-full px-3 py-1.5 text-sm border border-[var(--color-border)] rounded-md bg-[var(--color-surface)] text-[var(--color-foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] font-mono"
|
|
31
|
+
/>
|
|
32
|
+
)}
|
|
33
|
+
|
|
34
|
+
{authState.type === "apikey" && (
|
|
35
|
+
<div className="space-y-2">
|
|
36
|
+
<input
|
|
37
|
+
type="text"
|
|
38
|
+
value={authState.apikey.name}
|
|
39
|
+
onChange={(e) => onAuthChange({ ...authState, apikey: { ...authState.apikey, name: e.target.value } })}
|
|
40
|
+
placeholder="Key name (e.g., X-API-Key)"
|
|
41
|
+
className="w-full px-3 py-1.5 text-sm border border-[var(--color-border)] rounded-md bg-[var(--color-surface)] text-[var(--color-foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] font-mono"
|
|
42
|
+
/>
|
|
43
|
+
<input
|
|
44
|
+
type="password"
|
|
45
|
+
value={authState.apikey.value}
|
|
46
|
+
onChange={(e) => onAuthChange({ ...authState, apikey: { ...authState.apikey, value: e.target.value } })}
|
|
47
|
+
placeholder="Key value"
|
|
48
|
+
className="w-full px-3 py-1.5 text-sm border border-[var(--color-border)] rounded-md bg-[var(--color-surface)] text-[var(--color-foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] font-mono"
|
|
49
|
+
/>
|
|
50
|
+
<select
|
|
51
|
+
value={authState.apikey.location}
|
|
52
|
+
onChange={(e) => onAuthChange({ ...authState, apikey: { ...authState.apikey, location: e.target.value as "header" | "query" } })}
|
|
53
|
+
className="w-full px-3 py-1.5 text-sm border border-[var(--color-border)] rounded-md bg-[var(--color-surface)] text-[var(--color-foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]"
|
|
54
|
+
>
|
|
55
|
+
<option value="header">Header</option>
|
|
56
|
+
<option value="query">Query Parameter</option>
|
|
57
|
+
</select>
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
|
|
61
|
+
{authState.type === "basic" && (
|
|
62
|
+
<div className="space-y-2">
|
|
63
|
+
<input
|
|
64
|
+
type="text"
|
|
65
|
+
value={authState.basic.username}
|
|
66
|
+
onChange={(e) => onAuthChange({ ...authState, basic: { ...authState.basic, username: e.target.value } })}
|
|
67
|
+
placeholder="Username"
|
|
68
|
+
className="w-full px-3 py-1.5 text-sm border border-[var(--color-border)] rounded-md bg-[var(--color-surface)] text-[var(--color-foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] font-mono"
|
|
69
|
+
/>
|
|
70
|
+
<input
|
|
71
|
+
type="password"
|
|
72
|
+
value={authState.basic.password}
|
|
73
|
+
onChange={(e) => onAuthChange({ ...authState, basic: { ...authState.basic, password: e.target.value } })}
|
|
74
|
+
placeholder="Password"
|
|
75
|
+
className="w-full px-3 py-1.5 text-sm border border-[var(--color-border)] rounded-md bg-[var(--color-surface)] text-[var(--color-foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] font-mono"
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{authState.type !== "none" && (
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
onClick={() => onAuthChange(DEFAULT_AUTH_STATE)}
|
|
84
|
+
className="text-sm text-[var(--color-muted)] hover:text-[var(--color-error)]"
|
|
85
|
+
>
|
|
86
|
+
Clear Saved Auth
|
|
87
|
+
</button>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
interface CodeBlockProps {
|
|
4
|
+
code: string;
|
|
5
|
+
language: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Cache for highlighted code to avoid re-highlighting same content
|
|
10
|
+
const highlightCache = new Map<string, string>();
|
|
11
|
+
|
|
12
|
+
export function CodeBlock({ code, language, className = "" }: CodeBlockProps) {
|
|
13
|
+
const [html, setHtml] = useState<string>("");
|
|
14
|
+
const shikiRef = useRef<typeof import("shiki") | null>(null);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
let cancelled = false;
|
|
18
|
+
const cacheKey = `${language}:${code}`;
|
|
19
|
+
|
|
20
|
+
// Check cache first
|
|
21
|
+
const cached = highlightCache.get(cacheKey);
|
|
22
|
+
if (cached) {
|
|
23
|
+
setHtml(cached);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Dynamically import shiki only when needed (bundle-dynamic-imports)
|
|
28
|
+
const highlight = async () => {
|
|
29
|
+
if (!shikiRef.current) {
|
|
30
|
+
shikiRef.current = await import("shiki");
|
|
31
|
+
}
|
|
32
|
+
const result = await shikiRef.current.codeToHtml(code, {
|
|
33
|
+
lang: language,
|
|
34
|
+
themes: {
|
|
35
|
+
light: "github-light",
|
|
36
|
+
dark: "github-dark",
|
|
37
|
+
},
|
|
38
|
+
defaultColor: false,
|
|
39
|
+
});
|
|
40
|
+
if (!cancelled) {
|
|
41
|
+
highlightCache.set(cacheKey, result);
|
|
42
|
+
setHtml(result);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
highlight();
|
|
47
|
+
return () => {
|
|
48
|
+
cancelled = true;
|
|
49
|
+
};
|
|
50
|
+
}, [code, language]);
|
|
51
|
+
|
|
52
|
+
if (!html) {
|
|
53
|
+
return (
|
|
54
|
+
<pre className={`text-sm font-mono bg-[var(--color-surface-raised)] text-[var(--color-foreground)] p-3 sm:p-4 rounded-xl overflow-x-auto border border-[var(--color-border)] max-w-full ${className}`}>
|
|
55
|
+
<code className="break-all whitespace-pre-wrap sm:whitespace-pre">{code}</code>
|
|
56
|
+
</pre>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
className={`max-w-full overflow-hidden [&>pre]:!m-0 [&>pre]:!rounded-xl [&>pre]:!p-3 [&>pre]:sm:!p-4 [&>pre]:text-sm [&>pre]:overflow-x-auto [&>pre]:!bg-[var(--color-surface-raised)] [&>pre]:border [&>pre]:border-[var(--color-border)] [&>pre]:max-w-full [&_code]:break-all [&_code]:whitespace-pre-wrap [&_code]:sm:whitespace-pre ${className}`}
|
|
63
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import type { CodeLanguage } from "./types";
|
|
3
|
+
import { CodeBlock } from "./CodeBlock";
|
|
4
|
+
import { getLanguageForHighlighter } from "./generators";
|
|
5
|
+
|
|
6
|
+
interface CodeSnippetsProps {
|
|
7
|
+
codeSnippet: string;
|
|
8
|
+
currentLang: CodeLanguage;
|
|
9
|
+
onLangChange: (lang: CodeLanguage) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const LANGUAGES: { value: CodeLanguage; label: string }[] = [
|
|
13
|
+
{ value: "curl", label: "curl" },
|
|
14
|
+
{ value: "javascript", label: "JavaScript" },
|
|
15
|
+
{ value: "python", label: "Python" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function CodeSnippets({ codeSnippet, currentLang, onLangChange }: CodeSnippetsProps) {
|
|
19
|
+
const [copied, setCopied] = useState(false);
|
|
20
|
+
|
|
21
|
+
const copyCode = async () => {
|
|
22
|
+
try {
|
|
23
|
+
await navigator.clipboard.writeText(codeSnippet);
|
|
24
|
+
setCopied(true);
|
|
25
|
+
setTimeout(() => setCopied(false), 2000);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.debug("Failed to copy to clipboard:", err);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="p-3 sm:p-4 border-b border-[var(--color-border)] max-w-full overflow-hidden">
|
|
33
|
+
<h4 className="text-xs font-semibold text-[var(--color-muted)] uppercase tracking-wider mb-3">
|
|
34
|
+
Code Snippets
|
|
35
|
+
</h4>
|
|
36
|
+
|
|
37
|
+
<div className="flex flex-wrap gap-1 mb-3 max-w-full">
|
|
38
|
+
{LANGUAGES.map(({ value, label }) => (
|
|
39
|
+
<button
|
|
40
|
+
key={value}
|
|
41
|
+
type="button"
|
|
42
|
+
onClick={() => onLangChange(value)}
|
|
43
|
+
className={`px-2 sm:px-3 py-1 sm:py-1.5 text-xs sm:text-sm font-medium rounded-md transition-colors ${
|
|
44
|
+
currentLang === value
|
|
45
|
+
? "bg-[var(--color-primary-light)] text-[var(--color-primary)]"
|
|
46
|
+
: "text-[var(--color-muted)] hover:text-[var(--color-foreground)] hover:bg-[var(--color-surface-raised)]"
|
|
47
|
+
}`}
|
|
48
|
+
>
|
|
49
|
+
{label}
|
|
50
|
+
</button>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div className="relative max-w-full overflow-hidden">
|
|
55
|
+
<CodeBlock code={codeSnippet} language={getLanguageForHighlighter(currentLang)} />
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
onClick={copyCode}
|
|
59
|
+
className="absolute top-2 right-2 px-1.5 sm:px-2 py-0.5 sm:py-1 text-[10px] sm:text-xs font-medium text-[var(--color-muted)] hover:text-[var(--color-foreground)] bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-sunken)] border border-[var(--color-border)] rounded z-10"
|
|
60
|
+
>
|
|
61
|
+
{copied ? "Copied!" : "Copy"}
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ChevronDown } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
interface CollapsibleSectionProps {
|
|
5
|
+
title: string;
|
|
6
|
+
expanded: boolean;
|
|
7
|
+
onToggle: () => void;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function CollapsibleSection({ title, expanded, onToggle, children, className = "" }: CollapsibleSectionProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div className={`mt-4 border-t border-[var(--color-border)] pt-4 max-w-full overflow-hidden ${className}`}>
|
|
15
|
+
<button
|
|
16
|
+
type="button"
|
|
17
|
+
onClick={onToggle}
|
|
18
|
+
className="flex items-center justify-between w-full text-left py-2 text-sm font-medium text-[var(--color-foreground)] hover:text-[var(--color-muted)]"
|
|
19
|
+
>
|
|
20
|
+
<span className="truncate mr-2">{title}</span>
|
|
21
|
+
<ChevronDown className={`w-4 h-4 flex-shrink-0 transition-transform ${expanded ? "rotate-180" : ""}`} />
|
|
22
|
+
</button>
|
|
23
|
+
{expanded && <div className="mt-2 max-w-full overflow-hidden">{children}</div>}
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { X, Plus } from "lucide-react";
|
|
3
|
+
import type { KeyValue } from "./types";
|
|
4
|
+
|
|
5
|
+
interface KeyValueEditorProps {
|
|
6
|
+
items: KeyValue[];
|
|
7
|
+
onChange: (items: KeyValue[]) => void;
|
|
8
|
+
keyPlaceholder: string;
|
|
9
|
+
valuePlaceholder: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function KeyValueEditor({ items, onChange, keyPlaceholder, valuePlaceholder }: KeyValueEditorProps) {
|
|
13
|
+
const updateItem = (index: number, field: "key" | "value", value: string) => {
|
|
14
|
+
const newItems = [...items];
|
|
15
|
+
newItems[index] = { ...newItems[index], [field]: value };
|
|
16
|
+
onChange(newItems);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const removeItem = (index: number) => {
|
|
20
|
+
onChange(items.filter((_, i) => i !== index));
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const addItem = () => {
|
|
24
|
+
onChange([...items, { key: "", value: "" }]);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="space-y-3 max-w-full overflow-hidden">
|
|
29
|
+
{items.map((item, index) => (
|
|
30
|
+
<div key={index} className="flex flex-col sm:flex-row gap-2 max-w-full">
|
|
31
|
+
<input
|
|
32
|
+
type="text"
|
|
33
|
+
value={item.key}
|
|
34
|
+
onChange={(e) => updateItem(index, "key", e.target.value)}
|
|
35
|
+
placeholder={keyPlaceholder}
|
|
36
|
+
className="w-full sm:flex-1 sm:w-auto min-w-0 px-3 py-1.5 text-sm border border-[var(--color-border)] rounded-md bg-[var(--color-surface)] text-[var(--color-foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] font-mono"
|
|
37
|
+
/>
|
|
38
|
+
<div className="flex gap-2 max-w-full">
|
|
39
|
+
<input
|
|
40
|
+
type="text"
|
|
41
|
+
value={item.value}
|
|
42
|
+
onChange={(e) => updateItem(index, "value", e.target.value)}
|
|
43
|
+
placeholder={valuePlaceholder}
|
|
44
|
+
className="flex-1 min-w-0 px-3 py-1.5 text-sm border border-[var(--color-border)] rounded-md bg-[var(--color-surface)] text-[var(--color-foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] font-mono"
|
|
45
|
+
/>
|
|
46
|
+
<button type="button" onClick={() => removeItem(index)} className="p-1.5 text-[var(--color-muted)] hover:text-[var(--color-error)] flex-shrink-0" title="Remove">
|
|
47
|
+
<X className="w-4 h-4" />
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
))}
|
|
52
|
+
<button type="button" onClick={addItem} className="flex items-center gap-1 text-sm text-[var(--color-primary)] hover:opacity-80">
|
|
53
|
+
<Plus className="w-4 h-4" />
|
|
54
|
+
Add
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React, { useState, useMemo } from "react";
|
|
2
|
+
import type { ResponseState, ErrorState } from "./types";
|
|
3
|
+
import { CodeBlock } from "./CodeBlock";
|
|
4
|
+
import { CollapsibleSection } from "./CollapsibleSection";
|
|
5
|
+
import { Spinner } from "./Spinner";
|
|
6
|
+
|
|
7
|
+
interface ResponseDisplayProps {
|
|
8
|
+
response: ResponseState | null;
|
|
9
|
+
error: ErrorState | null;
|
|
10
|
+
isLoading: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const getStatusColor = (status: number) => {
|
|
14
|
+
if (status >= 200 && status < 300) return "bg-[var(--color-success-light)] text-[var(--color-success)]";
|
|
15
|
+
if (status >= 400 && status < 500) return "bg-[var(--color-warning-light)] text-[var(--color-warning)]";
|
|
16
|
+
if (status >= 500) return "bg-[var(--color-error-light)] text-[var(--color-error)]";
|
|
17
|
+
return "bg-[var(--color-surface-raised)] text-[var(--color-muted)]";
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function ResponseDisplay({ response, error, isLoading }: ResponseDisplayProps) {
|
|
21
|
+
const [showHeaders, setShowHeaders] = useState(false);
|
|
22
|
+
const [copied, setCopied] = useState(false);
|
|
23
|
+
|
|
24
|
+
const formattedBody = useMemo(() => {
|
|
25
|
+
if (!response?.body) return "";
|
|
26
|
+
try {
|
|
27
|
+
// Attempt to pretty-print JSON responses
|
|
28
|
+
return JSON.stringify(JSON.parse(response.body), null, 2);
|
|
29
|
+
} catch {
|
|
30
|
+
// Not valid JSON, return raw response body
|
|
31
|
+
return response.body;
|
|
32
|
+
}
|
|
33
|
+
}, [response?.body]);
|
|
34
|
+
|
|
35
|
+
const copyResponse = async () => {
|
|
36
|
+
try {
|
|
37
|
+
await navigator.clipboard.writeText(formattedBody);
|
|
38
|
+
setCopied(true);
|
|
39
|
+
setTimeout(() => setCopied(false), 2000);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.debug("Failed to copy to clipboard:", err);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="p-3 sm:p-4 max-w-full overflow-hidden">
|
|
47
|
+
<h4 className="text-xs font-semibold text-[var(--color-muted)] uppercase tracking-wider mb-3">Response</h4>
|
|
48
|
+
<div className="min-h-[100px] rounded-md border border-[var(--color-border)] bg-[var(--color-surface-raised)] p-3 sm:p-4 max-w-full overflow-hidden">
|
|
49
|
+
{/* Use ternary for cleaner conditional rendering */}
|
|
50
|
+
{!response && !error && !isLoading ? (
|
|
51
|
+
<p className="text-sm text-[var(--color-muted)] italic">Click "Send Request" to see the response</p>
|
|
52
|
+
) : null}
|
|
53
|
+
|
|
54
|
+
{isLoading ? (
|
|
55
|
+
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
|
|
56
|
+
<Spinner />
|
|
57
|
+
<span>Sending request...</span>
|
|
58
|
+
</div>
|
|
59
|
+
) : null}
|
|
60
|
+
|
|
61
|
+
{error ? (
|
|
62
|
+
<div className="p-3 rounded-md bg-[var(--color-error-light)] border border-[var(--color-error)]">
|
|
63
|
+
<p className="text-sm font-medium text-[var(--color-error)]">{error.title}</p>
|
|
64
|
+
<p className="text-sm text-[var(--color-error)] mt-1 opacity-80">{error.message}</p>
|
|
65
|
+
</div>
|
|
66
|
+
) : null}
|
|
67
|
+
|
|
68
|
+
{response ? (
|
|
69
|
+
<div>
|
|
70
|
+
<div className="flex flex-wrap items-center gap-3 mb-3">
|
|
71
|
+
<span className={`px-2 py-1 text-xs font-mono font-bold rounded ${getStatusColor(response.status)}`}>
|
|
72
|
+
{response.status}
|
|
73
|
+
</span>
|
|
74
|
+
<span className="text-sm text-[var(--color-muted)]">{response.statusText}</span>
|
|
75
|
+
<span className="text-xs text-[var(--color-muted)]">{response.time}ms</span>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div className="relative max-w-full overflow-hidden">
|
|
79
|
+
<CodeBlock code={formattedBody} language="json" className="max-h-[400px]" />
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
onClick={copyResponse}
|
|
83
|
+
className="absolute top-2 right-2 px-1.5 sm:px-2 py-0.5 sm:py-1 text-[10px] sm:text-xs font-medium text-[var(--color-muted)] hover:text-[var(--color-foreground)] bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-sunken)] border border-[var(--color-border)] rounded z-10"
|
|
84
|
+
>
|
|
85
|
+
{copied ? "Copied!" : "Copy"}
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<CollapsibleSection
|
|
90
|
+
title="Response Headers"
|
|
91
|
+
expanded={showHeaders}
|
|
92
|
+
onToggle={() => setShowHeaders(!showHeaders)}
|
|
93
|
+
className="mt-4 border-t border-[var(--color-border)] pt-4"
|
|
94
|
+
>
|
|
95
|
+
<div className="text-xs sm:text-sm font-mono space-y-1 overflow-x-auto max-w-full">
|
|
96
|
+
{Object.entries(response.headers).map(([key, value]) => (
|
|
97
|
+
<div key={key} className="flex flex-col sm:flex-row sm:gap-2 py-1">
|
|
98
|
+
<span className="text-[var(--color-muted)] flex-shrink-0">{key}:</span>
|
|
99
|
+
<span className="text-[var(--color-foreground)] break-all">{value}</span>
|
|
100
|
+
</div>
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
</CollapsibleSection>
|
|
104
|
+
</div>
|
|
105
|
+
) : null}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const METHOD_COLORS: Record<string, string> = {
|
|
2
|
+
GET: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
|
|
3
|
+
POST: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300",
|
|
4
|
+
PUT: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300",
|
|
5
|
+
PATCH: "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300",
|
|
6
|
+
DELETE: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const AUTH_STORAGE_PREFIX = "opendocs_playground_auth_";
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_AUTH_STATE = {
|
|
12
|
+
type: "none" as const,
|
|
13
|
+
bearer: { token: "" },
|
|
14
|
+
apikey: { name: "", value: "", location: "header" as const },
|
|
15
|
+
basic: { username: "", password: "" },
|
|
16
|
+
};
|