@djangocfg/ui-tools 2.1.284 → 2.1.286
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/dist/DocsLayout-BCVU6TTX.cjs +2027 -0
- package/dist/DocsLayout-BCVU6TTX.cjs.map +1 -0
- package/dist/DocsLayout-ERETJLLV.mjs +2020 -0
- package/dist/DocsLayout-ERETJLLV.mjs.map +1 -0
- package/dist/{PlaygroundLayout-O52C6HK5.css → DocsLayout-MBFIB4NO.css} +1 -1
- package/dist/{PrettyCode.client-SGDGQTYT.cjs → PrettyCode.client-5GABIN2I.cjs} +57 -35
- package/dist/PrettyCode.client-5GABIN2I.cjs.map +1 -0
- package/dist/{PrettyCode.client-DW5LTG47.mjs → PrettyCode.client-IZTXXYHG.mjs} +57 -35
- package/dist/PrettyCode.client-IZTXXYHG.mjs.map +1 -0
- package/dist/chunk-IULI4XII.cjs +1129 -0
- package/dist/chunk-IULI4XII.cjs.map +1 -0
- package/dist/chunk-VZGQC3NG.mjs +1100 -0
- package/dist/chunk-VZGQC3NG.mjs.map +1 -0
- package/dist/index.cjs +88 -552
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -6
- package/dist/index.d.ts +18 -6
- package/dist/index.mjs +25 -496
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/src/tools/OpenapiViewer/.claude/.sidecar/activity.jsonl +4 -0
- package/src/tools/OpenapiViewer/.claude/.sidecar/map_cache.json +30 -0
- package/src/tools/OpenapiViewer/.claude/.sidecar/usage.json +5 -0
- package/src/tools/OpenapiViewer/.claude/project-map.md +23 -0
- package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +28 -2
- package/src/tools/OpenapiViewer/README.md +104 -51
- package/src/tools/OpenapiViewer/components/DocsLayout/ApiIntroSection.tsx +64 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/DocsView.tsx +137 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc.tsx +268 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +139 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar.tsx +211 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/SlideInPlayground.tsx +101 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/TryItSheet.tsx +57 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/anchor.ts +11 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/grouping.ts +71 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/index.tsx +166 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/schemaFields.ts +121 -0
- package/src/tools/OpenapiViewer/components/DocsLayout/sidebarLabel.ts +60 -0
- package/src/tools/OpenapiViewer/components/index.ts +5 -2
- package/src/tools/OpenapiViewer/components/shared/BodyFormEditor.tsx +422 -0
- package/src/tools/OpenapiViewer/components/shared/EndpointDraftSync.tsx +108 -0
- package/src/tools/OpenapiViewer/components/shared/EndpointResetButton.tsx +50 -0
- package/src/tools/OpenapiViewer/components/{PlaygroundLayout → shared}/RequestPanel.tsx +174 -87
- package/src/tools/OpenapiViewer/components/shared/SendButton.tsx +91 -0
- package/src/tools/OpenapiViewer/components/{PlaygroundLayout → shared}/ui.tsx +5 -4
- package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +82 -8
- package/src/tools/OpenapiViewer/hooks/useEndpointDraft.ts +142 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +126 -13
- package/src/tools/OpenapiViewer/index.tsx +3 -7
- package/src/tools/OpenapiViewer/lazy.tsx +6 -27
- package/src/tools/OpenapiViewer/types.ts +44 -0
- package/src/tools/OpenapiViewer/utils/formatters.ts +2 -23
- package/src/tools/OpenapiViewer/utils/index.ts +3 -1
- package/src/tools/OpenapiViewer/utils/schemaExport.ts +206 -0
- package/src/tools/OpenapiViewer/utils/url.ts +202 -0
- package/src/tools/PrettyCode/PrettyCode.client.tsx +42 -8
- package/src/tools/PrettyCode/index.tsx +6 -0
- package/dist/PlaygroundLayout-DHUATCHB.cjs +0 -798
- package/dist/PlaygroundLayout-DHUATCHB.cjs.map +0 -1
- package/dist/PlaygroundLayout-NONWOVQR.mjs +0 -791
- package/dist/PlaygroundLayout-NONWOVQR.mjs.map +0 -1
- package/dist/PrettyCode.client-DW5LTG47.mjs.map +0 -1
- package/dist/PrettyCode.client-SGDGQTYT.cjs.map +0 -1
- package/dist/chunk-5FKE7OME.cjs +0 -369
- package/dist/chunk-5FKE7OME.cjs.map +0 -1
- package/dist/chunk-BKWDHJKF.mjs +0 -356
- package/dist/chunk-BKWDHJKF.mjs.map +0 -1
- package/src/tools/OpenapiViewer/components/PlaygroundLayout/EndpointList.tsx +0 -228
- package/src/tools/OpenapiViewer/components/PlaygroundLayout/index.tsx +0 -107
- /package/dist/{PlaygroundLayout-O52C6HK5.css.map → DocsLayout-MBFIB4NO.css.map} +0 -0
- /package/src/tools/OpenapiViewer/components/{PlaygroundLayout → shared}/ResponsePanel.tsx +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-tools",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.286",
|
|
4
4
|
"description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-tools",
|
|
@@ -90,8 +90,8 @@
|
|
|
90
90
|
"check": "tsc --noEmit"
|
|
91
91
|
},
|
|
92
92
|
"peerDependencies": {
|
|
93
|
-
"@djangocfg/i18n": "^2.1.
|
|
94
|
-
"@djangocfg/ui-core": "^2.1.
|
|
93
|
+
"@djangocfg/i18n": "^2.1.286",
|
|
94
|
+
"@djangocfg/ui-core": "^2.1.286",
|
|
95
95
|
"consola": "^3.4.2",
|
|
96
96
|
"lucide-react": "^0.545.0",
|
|
97
97
|
"react": "^19.1.0",
|
|
@@ -133,10 +133,10 @@
|
|
|
133
133
|
"@maplibre/maplibre-gl-geocoder": "^1.7.0"
|
|
134
134
|
},
|
|
135
135
|
"devDependencies": {
|
|
136
|
-
"@djangocfg/i18n": "^2.1.
|
|
136
|
+
"@djangocfg/i18n": "^2.1.286",
|
|
137
137
|
"@djangocfg/playground": "workspace:*",
|
|
138
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
139
|
-
"@djangocfg/ui-core": "^2.1.
|
|
138
|
+
"@djangocfg/typescript-config": "^2.1.286",
|
|
139
|
+
"@djangocfg/ui-core": "^2.1.286",
|
|
140
140
|
"@types/mapbox__mapbox-gl-draw": "^1.4.8",
|
|
141
141
|
"@types/node": "^24.7.2",
|
|
142
142
|
"@types/react": "^19.1.0",
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{"ts":"2026-04-22T13:11:47.381936Z","action":"map","tokens":2297,"model":"deepseek/deepseek-v3.2-20251201","details":{"directories":7,"entry_points":7}}
|
|
2
|
+
{"ts":"2026-04-22T13:12:54.534624Z","action":"map","tokens":0,"model":"cache","details":{"directories":7,"entry_points":5}}
|
|
3
|
+
{"ts":"2026-04-22T13:14:16.326555Z","action":"map","tokens":0,"model":"cache","details":{"directories":7,"entry_points":5}}
|
|
4
|
+
{"ts":"2026-04-22T13:15:40.112071Z","action":"map","tokens":0,"model":"cache","details":{"directories":7,"entry_points":5}}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
".": {
|
|
3
|
+
"hash": "1f34f83a5cbb5246",
|
|
4
|
+
"annotation": "Primary component exports and entry points for the OpenAPI viewer library."
|
|
5
|
+
},
|
|
6
|
+
"components": {
|
|
7
|
+
"hash": "dcdc3e0b3362edb8",
|
|
8
|
+
"annotation": "Re-export barrel file for the library's public component APIs."
|
|
9
|
+
},
|
|
10
|
+
"components/DocsLayout": {
|
|
11
|
+
"hash": "c22ac1e1ddff2ec6",
|
|
12
|
+
"annotation": "Main documentation layout component and its sub-components, handling sidebar, API display, and interactive playground sheet."
|
|
13
|
+
},
|
|
14
|
+
"components/shared": {
|
|
15
|
+
"hash": "9dc41c51197bb209",
|
|
16
|
+
"annotation": "Shared UI panels (request/response editors) used by the DocsLayout and mobile interactions."
|
|
17
|
+
},
|
|
18
|
+
"context": {
|
|
19
|
+
"hash": "1d8732f878128b05",
|
|
20
|
+
"annotation": "React context for managing the playground state, such as endpoint drafts and API calls."
|
|
21
|
+
},
|
|
22
|
+
"hooks": {
|
|
23
|
+
"hash": "b1e8093c6694ba36",
|
|
24
|
+
"annotation": "Custom React hooks for managing state like API schemas, endpoint drafts, and responsive behavior."
|
|
25
|
+
},
|
|
26
|
+
"utils": {
|
|
27
|
+
"hash": "e88f8c7b59cf3776",
|
|
28
|
+
"annotation": "Utility functions for API key management, URL building, schema exporting, and data formatting."
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Project Map
|
|
2
|
+
> react-component-library — An interactive OpenAPI 3.x viewer component with a Stripe/Scalar-style documentation layout and integrated playground.
|
|
3
|
+
> Generated: 2026-04-22T13:15:40.111463+00:00
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
- `components/` — Re-export barrel file for the library's public component APIs. **[entry: index.ts]**
|
|
8
|
+
- `components/DocsLayout/` — Main documentation layout component and its sub-components, handling sidebar, API display, and interactive playground sheet. **[entry: index.tsx]**
|
|
9
|
+
- `components/shared/` — Shared UI panels (request/response editors) used by the DocsLayout and mobile interactions.
|
|
10
|
+
- `context/` — React context for managing the playground state, such as endpoint drafts and API calls.
|
|
11
|
+
- `hooks/` — Custom React hooks for managing state like API schemas, endpoint drafts, and responsive behavior. **[entry: index.ts]**
|
|
12
|
+
- `utils/` — Utility functions for API key management, URL building, schema exporting, and data formatting. **[entry: index.ts]**
|
|
13
|
+
|
|
14
|
+
## Entry Points
|
|
15
|
+
|
|
16
|
+
- `./index.tsx`
|
|
17
|
+
- `components/index.ts`
|
|
18
|
+
- `components/DocsLayout/index.tsx`
|
|
19
|
+
- `hooks/index.ts`
|
|
20
|
+
- `utils/index.ts`
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
Model: cache | Tokens: 0
|
|
@@ -6,7 +6,8 @@ import type { PlaygroundConfig } from './types';
|
|
|
6
6
|
export default defineStory({
|
|
7
7
|
title: 'Tools/OpenapiViewer',
|
|
8
8
|
component: Playground,
|
|
9
|
-
description:
|
|
9
|
+
description:
|
|
10
|
+
'Interactive OpenAPI schema viewer. Sidebar + docs longread with a slide-in two-column playground (Request + Response) when an endpoint is selected.',
|
|
10
11
|
});
|
|
11
12
|
|
|
12
13
|
// ============================================================================
|
|
@@ -43,7 +44,7 @@ const MANY_SCHEMAS = [
|
|
|
43
44
|
// ============================================================================
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
|
-
* Interactive
|
|
47
|
+
* Interactive — default schema is adjustable.
|
|
47
48
|
*/
|
|
48
49
|
export const Interactive = () => {
|
|
49
50
|
const [defaultSchema] = useSelect('defaultSchema', {
|
|
@@ -65,6 +66,31 @@ export const Interactive = () => {
|
|
|
65
66
|
);
|
|
66
67
|
};
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* With baseUrl override — forces a specific request host, ignoring
|
|
71
|
+
* `schema.servers[0].url`. Useful when docs are hosted on a different
|
|
72
|
+
* origin than the API, or when the schema has no servers entry.
|
|
73
|
+
*/
|
|
74
|
+
export const WithBaseUrl = () => {
|
|
75
|
+
const [baseUrl] = useValue('baseUrl', {
|
|
76
|
+
defaultValue: 'https://petstore3.swagger.io/api/v3',
|
|
77
|
+
label: 'Base URL',
|
|
78
|
+
description: 'Overrides schema.servers[0].url for every request',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const config: PlaygroundConfig = {
|
|
82
|
+
schemas: [PETSTORE_SCHEMA],
|
|
83
|
+
defaultSchemaId: 'petstore',
|
|
84
|
+
baseUrl,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className="min-h-[600px]">
|
|
89
|
+
<Playground config={config} />
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
68
94
|
/**
|
|
69
95
|
* Single schema — no Combobox switcher shown (schemas.length === 1).
|
|
70
96
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OpenapiViewer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Interactive OpenAPI 3.x viewer in a Stripe/Scalar-style layout: sidebar with grouped endpoints on the left, scrolling documentation longread in the middle, and a slide-in two-column playground that opens on demand from the right.
|
|
4
4
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
@@ -17,7 +17,7 @@ import { Playground } from '@djangocfg/ui-tools/openapi';
|
|
|
17
17
|
/>
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Lazy variant for production (~400KB code-split):
|
|
21
21
|
|
|
22
22
|
```tsx
|
|
23
23
|
import { LazyOpenapiViewer } from '@djangocfg/ui-tools/openapi/lazy';
|
|
@@ -27,35 +27,65 @@ import { LazyOpenapiViewer } from '@djangocfg/ui-tools/openapi/lazy';
|
|
|
27
27
|
|
|
28
28
|
## Layout
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Base state — two columns, docs on the full remaining width:
|
|
31
31
|
|
|
32
32
|
```
|
|
33
|
-
|
|
34
|
-
│
|
|
35
|
-
│
|
|
36
|
-
│
|
|
37
|
-
│
|
|
38
|
-
│
|
|
39
|
-
│
|
|
40
|
-
│
|
|
41
|
-
│ …
|
|
42
|
-
│
|
|
43
|
-
│
|
|
44
|
-
|
|
45
|
-
└──────────────────┴─────────────────────┴─────────────────────┘
|
|
33
|
+
┌───────────────┬──────────────────────────────────────────────────┐
|
|
34
|
+
│ SIDEBAR │ DOCS (longread, scrolls) │
|
|
35
|
+
│ 260 px │ 1fr │
|
|
36
|
+
│ │ │
|
|
37
|
+
│ 🔍 Search │ # Petstore API │
|
|
38
|
+
│ Pets │ Intro from info.description │
|
|
39
|
+
│ Add a pet │ │
|
|
40
|
+
│ Find by ID │ ## POST /pet │
|
|
41
|
+
│ … │ Description, parameters (form table), body │
|
|
42
|
+
│ Users │ fields, responses. │
|
|
43
|
+
│ … │ │
|
|
44
|
+
└───────────────┴──────────────────────────────────────────────────┘
|
|
46
45
|
```
|
|
47
46
|
|
|
48
|
-
|
|
47
|
+
When **Try it** is clicked, a slide-in playground opens from the right. It starts narrow (Request only) and widens to two columns once a response arrives:
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
```
|
|
50
|
+
┌───────────────┬──────────────────┬─ PLAYGROUND (slide-in) ───────┐
|
|
51
|
+
│ SIDEBAR │ DOCS │ Request │ Response │
|
|
52
|
+
│ │ │ Form body │ 200 OK │
|
|
53
|
+
│ │ │ Auth & Headers │ JsonTree │
|
|
54
|
+
│ │ │ cURL │ │
|
|
55
|
+
│ │ │ [ Send Request ] │
|
|
56
|
+
└───────────────┴──────────────────┴───────────────────────────────┘
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- Sidebar click → smooth-scroll to that section (no playground).
|
|
60
|
+
- Scrollspy highlights the active section in the sidebar as you read.
|
|
61
|
+
- Per-endpoint **Try it** loads the endpoint and slides the playground in.
|
|
62
|
+
- Widths are `clamp()`-driven: narrow ≈ `clamp(380px, 30vw, 480px)`, wide ≈ `clamp(720px, 60vw, 1280px)`.
|
|
63
|
+
- Close with `×` or `Esc`. On viewports below 1024px the panel becomes a `ResponsiveSheet` (mobile-friendly bottom drawer).
|
|
64
|
+
|
|
65
|
+
## Request body — form-first
|
|
66
|
+
|
|
67
|
+
When the OpenAPI schema describes the body, the playground renders a **form** built from `schema.properties`: primitives get typed inputs, enums become dropdowns, arrays have add/remove, nested objects indent. A `Form / JSON` toggle in the Body section lets you drop into raw JSON for corner cases. Body state is always stored as JSON under the hood, so toggling views is lossless.
|
|
68
|
+
|
|
69
|
+
The docs page mirrors this by rendering a **fields table** of the same schema (name, type, required, description) — no more opaque `object Created user object`.
|
|
70
|
+
|
|
71
|
+
## Copy for AI
|
|
72
|
+
|
|
73
|
+
Each endpoint has a Copy button that serialises just that endpoint as compact markdown — good for pasting into an LLM prompt without blowing the context window. The API intro section has a Copy-schema dropdown with three flavours (raw JSON, compact dereferenced JSON, markdown summary) and shows byte size after the first copy so you can pick one that fits.
|
|
74
|
+
|
|
75
|
+
Auth values (API keys, Bearer tokens) are never included in any export.
|
|
76
|
+
|
|
77
|
+
## Drafts & auth persistence
|
|
78
|
+
|
|
79
|
+
- **Per-endpoint drafts** (parameters + body) are saved to `localStorage` under `openapi-playground:draft:{schemaId}:{method}:{path}`. Switching endpoints and coming back restores your inputs; reloading the page keeps them too.
|
|
80
|
+
- **Auth session** (selected API key, manual Bearer token) lives in `sessionStorage` under `openapi-playground:auth:*`. Survives a reload within the same tab, dies when the tab closes — safer default for secrets than `localStorage`.
|
|
81
|
+
- A small **Reset** button in the Request panel clears the current endpoint's draft (parameters + body) without touching auth.
|
|
52
82
|
|
|
53
83
|
## File Structure
|
|
54
84
|
|
|
55
85
|
```
|
|
56
86
|
OpenapiViewer/
|
|
57
|
-
├── index.tsx #
|
|
58
|
-
├── lazy.tsx #
|
|
87
|
+
├── index.tsx # <Playground config={…} />
|
|
88
|
+
├── lazy.tsx # <LazyOpenapiViewer />
|
|
59
89
|
├── types.ts # All TypeScript types
|
|
60
90
|
├── constants.ts # HTTP method/status colour maps
|
|
61
91
|
├── README.md # ← you are here
|
|
@@ -64,58 +94,81 @@ OpenapiViewer/
|
|
|
64
94
|
│ └── PlaygroundContext.tsx # Global state (endpoint, request, response, auth)
|
|
65
95
|
│
|
|
66
96
|
├── hooks/
|
|
67
|
-
│ ├── useOpenApiSchema.ts # Fetches & parses
|
|
68
|
-
│
|
|
97
|
+
│ ├── useOpenApiSchema.ts # Fetches & parses schema; dereferences $refs
|
|
98
|
+
│ ├── useEndpointDraft.ts # localStorage draft per endpoint
|
|
99
|
+
│ └── useMobile.ts
|
|
69
100
|
│
|
|
70
101
|
├── utils/
|
|
71
|
-
│ ├──
|
|
72
|
-
│ ├──
|
|
73
|
-
│ ├──
|
|
102
|
+
│ ├── url.ts # UrlBuilder + path/query/baseUrl helpers
|
|
103
|
+
│ ├── schemaExport.ts # Dereferenced/compact/markdown export for LLMs
|
|
104
|
+
│ ├── formatters.ts # HTTP status / method helpers, JSON validation
|
|
105
|
+
│ ├── versionManager.ts
|
|
106
|
+
│ ├── apiKeyManager.ts
|
|
74
107
|
│ └── index.ts
|
|
75
108
|
│
|
|
76
109
|
└── components/
|
|
77
|
-
├── index.ts # Re-exports
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
110
|
+
├── index.ts # Re-exports DocsLayout
|
|
111
|
+
├── DocsLayout/ # Default (only) layout
|
|
112
|
+
│ ├── index.tsx # Root: sidebar + docs + slide-in playground
|
|
113
|
+
│ ├── Sidebar.tsx # Grouped endpoint list, scrollspy highlight
|
|
114
|
+
│ ├── DocsView.tsx # Longread scroll container + scrollspy
|
|
115
|
+
│ ├── EndpointDoc.tsx # One endpoint as a prose section + fields table
|
|
116
|
+
│ ├── ApiIntroSection.tsx # Top intro + Copy-for-AI dropdown
|
|
117
|
+
│ ├── SchemaCopyMenu.tsx # Per-schema copy flavours
|
|
118
|
+
│ ├── SlideInPlayground.tsx# Right slide-in overlay
|
|
119
|
+
│ ├── TryItSheet.tsx # Mobile/tablet fallback via ResponsiveSheet
|
|
120
|
+
│ ├── anchor.ts # Stable section anchors for deep-linking
|
|
121
|
+
│ ├── grouping.ts # Shared group+sort used by sidebar and docs
|
|
122
|
+
│ ├── schemaFields.ts # JSON Schema → flat field rows
|
|
123
|
+
│ └── sidebarLabel.ts # Common-prefix strip + tooltip helpers
|
|
124
|
+
│
|
|
125
|
+
└── shared/ # Panels reused by SlideInPlayground + TryItSheet
|
|
126
|
+
├── RequestPanel.tsx
|
|
127
|
+
├── ResponsePanel.tsx
|
|
128
|
+
├── SendButton.tsx
|
|
129
|
+
├── BodyFormEditor.tsx # Recursive JSON-Schema form renderer
|
|
130
|
+
├── EndpointDraftSync.tsx# Headless draft ↔ context sync
|
|
131
|
+
├── EndpointResetButton.tsx
|
|
132
|
+
└── ui.tsx # MethodBadge, StatusBadge, Panel, atoms
|
|
84
133
|
```
|
|
85
134
|
|
|
86
|
-
## Key Design Decisions
|
|
87
|
-
|
|
88
|
-
| Topic | Decision |
|
|
89
|
-
|---|---|
|
|
90
|
-
| **No stepper** | All three panels visible simultaneously on desktop; tabs on mobile |
|
|
91
|
-
| **Collapsed sections** | Auth & Headers and cURL start collapsed to reduce visual noise |
|
|
92
|
-
| **JSON normalisation** | `ResponsePanel` always tries `JSON.parse` before rendering; falls back to `<pre>` for non-JSON bodies |
|
|
93
|
-
| **Hover-only toolbar** | `PrettyCode` language badge + copy button appear only on hover |
|
|
94
|
-
| **Data before JSX** | All derived values (`isFiltered`, `hasCurl`, `epPath`, …) computed before `return` — no logic in JSX |
|
|
95
|
-
| **Static configs** | `JSON_TREE_CONFIG`, `MOBILE_TABS`, style maps are module-level constants |
|
|
96
|
-
| **Lazy loading** | `PlaygroundLayout` is lazy-loaded via `Suspense` to keep the initial bundle small |
|
|
97
|
-
|
|
98
135
|
## Config Reference
|
|
99
136
|
|
|
100
137
|
```ts
|
|
101
138
|
interface PlaygroundConfig {
|
|
102
|
-
/**
|
|
139
|
+
/** OpenAPI 3.x schemas (fetched on mount). */
|
|
103
140
|
schemas: SchemaSource[];
|
|
104
|
-
/**
|
|
141
|
+
/** Which schema to load first. Defaults to `schemas[0].id`. */
|
|
105
142
|
defaultSchemaId?: string;
|
|
143
|
+
/** Global request base URL. Wins over `schema.servers[0].url`. */
|
|
144
|
+
baseUrl?: string;
|
|
145
|
+
/** Optional API keys for the X-API-Key picker. */
|
|
146
|
+
apiKeys?: ApiKey[];
|
|
147
|
+
apiKeysLoading?: boolean;
|
|
106
148
|
}
|
|
107
149
|
|
|
108
150
|
interface SchemaSource {
|
|
109
|
-
id: string;
|
|
110
|
-
name: string;
|
|
111
|
-
url: string;
|
|
151
|
+
id: string;
|
|
152
|
+
name: string;
|
|
153
|
+
url: string;
|
|
154
|
+
/** Per-schema base URL override (wins over PlaygroundConfig.baseUrl). */
|
|
155
|
+
baseUrl?: string;
|
|
112
156
|
}
|
|
113
157
|
```
|
|
114
158
|
|
|
159
|
+
### baseUrl resolution
|
|
160
|
+
|
|
161
|
+
Priority (highest wins):
|
|
162
|
+
|
|
163
|
+
1. `SchemaSource.baseUrl`
|
|
164
|
+
2. `PlaygroundConfig.baseUrl`
|
|
165
|
+
3. `schema.servers[0].url` (from the OpenAPI document)
|
|
166
|
+
4. `''` — endpoint paths are treated as relative
|
|
167
|
+
|
|
115
168
|
## Auth
|
|
116
169
|
|
|
117
170
|
Bearer token priority (highest wins):
|
|
118
171
|
|
|
119
172
|
1. Manual token entered in the **Auth & Headers** section
|
|
120
173
|
2. JWT from `localStorage` key `auth_token`
|
|
121
|
-
3. X-API-Key header (when an API key is selected
|
|
174
|
+
3. X-API-Key header (when an API key is selected and `config.apiKeys` is provided)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import { MarkdownMessage } from '../../../../components/markdown';
|
|
6
|
+
import type { ApiEndpoint, OpenApiInfo, OpenApiSchema } from '../../types';
|
|
7
|
+
import { SchemaCopyMenu } from './SchemaCopyMenu';
|
|
8
|
+
|
|
9
|
+
interface ApiIntroSectionProps {
|
|
10
|
+
info: OpenApiInfo;
|
|
11
|
+
schema: OpenApiSchema | null;
|
|
12
|
+
endpoints: ApiEndpoint[];
|
|
13
|
+
resolvedBaseUrl?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ApiIntroSection({ info, schema, endpoints, resolvedBaseUrl }: ApiIntroSectionProps) {
|
|
17
|
+
return (
|
|
18
|
+
<section className="pb-10 mb-10 border-b">
|
|
19
|
+
<div className="flex items-start justify-between gap-4 flex-wrap">
|
|
20
|
+
<div className="flex items-center gap-3 flex-wrap min-w-0">
|
|
21
|
+
<h1 className="text-3xl font-semibold tracking-tight text-foreground leading-tight">
|
|
22
|
+
{info.title}
|
|
23
|
+
</h1>
|
|
24
|
+
<span className="inline-flex items-center rounded-full bg-muted px-2 py-0.5 font-mono text-[11px] text-muted-foreground">
|
|
25
|
+
v{info.version}
|
|
26
|
+
</span>
|
|
27
|
+
</div>
|
|
28
|
+
<SchemaCopyMenu
|
|
29
|
+
schema={schema}
|
|
30
|
+
endpoints={endpoints}
|
|
31
|
+
baseUrl={resolvedBaseUrl}
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
{info.description && (
|
|
36
|
+
<div className="mt-4 text-muted-foreground">
|
|
37
|
+
<MarkdownMessage content={info.description} />
|
|
38
|
+
</div>
|
|
39
|
+
)}
|
|
40
|
+
|
|
41
|
+
{info.servers && info.servers.length > 0 && (
|
|
42
|
+
<div className="mt-6 space-y-2">
|
|
43
|
+
<h4 className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/60">
|
|
44
|
+
Base URL
|
|
45
|
+
</h4>
|
|
46
|
+
<div className="space-y-1.5">
|
|
47
|
+
{info.servers.map((s, i) => (
|
|
48
|
+
<div key={`${s.url}-${i}`} className="flex items-baseline gap-2 flex-wrap">
|
|
49
|
+
<code className="font-mono text-xs px-2 py-1 rounded bg-muted border">
|
|
50
|
+
{s.url}
|
|
51
|
+
</code>
|
|
52
|
+
{s.description && (
|
|
53
|
+
<span className="text-xs text-muted-foreground">
|
|
54
|
+
{s.description}
|
|
55
|
+
</span>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
</section>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
import type { ApiEndpoint, OpenApiInfo, OpenApiSchema } from '../../types';
|
|
6
|
+
import { deduplicateEndpoints } from '../../utils/versionManager';
|
|
7
|
+
import { ApiIntroSection } from './ApiIntroSection';
|
|
8
|
+
import { EndpointDoc } from './EndpointDoc';
|
|
9
|
+
import { endpointAnchor } from './anchor';
|
|
10
|
+
|
|
11
|
+
export interface DocsViewHandle {
|
|
12
|
+
scrollToAnchor: (anchor: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface DocsViewProps {
|
|
16
|
+
info: OpenApiInfo | null;
|
|
17
|
+
rawSchema: OpenApiSchema | null;
|
|
18
|
+
resolvedBaseUrl?: string;
|
|
19
|
+
endpoints: ApiEndpoint[];
|
|
20
|
+
selectedVersion: string;
|
|
21
|
+
loadedEndpoint: ApiEndpoint | null;
|
|
22
|
+
onTryEndpoint: (ep: ApiEndpoint) => void;
|
|
23
|
+
onActiveChange: (anchor: string | null) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const DocsView = React.forwardRef<DocsViewHandle, DocsViewProps>(function DocsView(
|
|
27
|
+
{
|
|
28
|
+
info,
|
|
29
|
+
rawSchema,
|
|
30
|
+
resolvedBaseUrl,
|
|
31
|
+
endpoints,
|
|
32
|
+
selectedVersion,
|
|
33
|
+
loadedEndpoint,
|
|
34
|
+
onTryEndpoint,
|
|
35
|
+
onActiveChange,
|
|
36
|
+
},
|
|
37
|
+
ref,
|
|
38
|
+
) {
|
|
39
|
+
const scrollRef = useRef<HTMLDivElement | null>(null);
|
|
40
|
+
|
|
41
|
+
const visibleEndpoints = useMemo(
|
|
42
|
+
() => deduplicateEndpoints(endpoints, selectedVersion),
|
|
43
|
+
[endpoints, selectedVersion],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Scroll a given section into view. Imperative handle so the
|
|
47
|
+
// sidebar (not a descendant) can trigger this without props drilling.
|
|
48
|
+
const scrollToAnchor = useCallback((anchor: string) => {
|
|
49
|
+
const root = scrollRef.current;
|
|
50
|
+
if (!root) return;
|
|
51
|
+
const el = root.querySelector<HTMLElement>(`[data-endpoint-anchor="${anchor}"]`);
|
|
52
|
+
if (!el) return;
|
|
53
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
React.useImperativeHandle(ref, () => ({ scrollToAnchor }), [scrollToAnchor]);
|
|
57
|
+
|
|
58
|
+
// Scrollspy: pick the topmost endpoint section whose top is near the
|
|
59
|
+
// upper third of the viewport. Runs on every scroll event (rAF-throttled)
|
|
60
|
+
// — an IntersectionObserver has flaky behaviour for "which one is active"
|
|
61
|
+
// when several sections overlap the root margin band at once.
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const root = scrollRef.current;
|
|
64
|
+
if (!root) return;
|
|
65
|
+
|
|
66
|
+
let rafId = 0;
|
|
67
|
+
let lastActive: string | null = null;
|
|
68
|
+
|
|
69
|
+
const compute = () => {
|
|
70
|
+
rafId = 0;
|
|
71
|
+
const sections = root.querySelectorAll<HTMLElement>('[data-endpoint-anchor]');
|
|
72
|
+
if (sections.length === 0) return;
|
|
73
|
+
const rootTop = root.getBoundingClientRect().top;
|
|
74
|
+
const threshold = rootTop + root.clientHeight * 0.25;
|
|
75
|
+
let active: string | null = null;
|
|
76
|
+
for (const s of Array.from(sections)) {
|
|
77
|
+
const top = s.getBoundingClientRect().top;
|
|
78
|
+
if (top <= threshold) {
|
|
79
|
+
active = s.dataset.endpointAnchor ?? null;
|
|
80
|
+
} else {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (active !== lastActive) {
|
|
85
|
+
lastActive = active;
|
|
86
|
+
onActiveChange(active);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const onScroll = () => {
|
|
91
|
+
if (rafId) return;
|
|
92
|
+
rafId = requestAnimationFrame(compute);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
compute();
|
|
96
|
+
root.addEventListener('scroll', onScroll, { passive: true });
|
|
97
|
+
return () => {
|
|
98
|
+
root.removeEventListener('scroll', onScroll);
|
|
99
|
+
if (rafId) cancelAnimationFrame(rafId);
|
|
100
|
+
};
|
|
101
|
+
}, [visibleEndpoints, onActiveChange]);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div ref={scrollRef} className="flex-1 overflow-y-auto min-h-0">
|
|
105
|
+
<div className="mx-auto w-full max-w-[860px] px-6 md:px-10 lg:px-14 py-12">
|
|
106
|
+
{info && (
|
|
107
|
+
<ApiIntroSection
|
|
108
|
+
info={info}
|
|
109
|
+
schema={rawSchema}
|
|
110
|
+
endpoints={visibleEndpoints}
|
|
111
|
+
resolvedBaseUrl={resolvedBaseUrl}
|
|
112
|
+
/>
|
|
113
|
+
)}
|
|
114
|
+
{visibleEndpoints.length === 0 ? (
|
|
115
|
+
<div className="py-16 text-center text-sm text-muted-foreground">
|
|
116
|
+
No endpoints to display.
|
|
117
|
+
</div>
|
|
118
|
+
) : (
|
|
119
|
+
<div className="divide-y divide-border/60">
|
|
120
|
+
{visibleEndpoints.map((ep) => {
|
|
121
|
+
const isLoaded =
|
|
122
|
+
loadedEndpoint?.method === ep.method && loadedEndpoint?.path === ep.path;
|
|
123
|
+
return (
|
|
124
|
+
<EndpointDoc
|
|
125
|
+
key={`${ep.method}-${ep.path}`}
|
|
126
|
+
endpoint={ep}
|
|
127
|
+
isLoadedInPlayground={isLoaded}
|
|
128
|
+
onTryIt={() => onTryEndpoint(ep)}
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
})}
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
});
|