@veluai/velu 0.2.13 → 0.2.15
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 +80 -80
- package/dist/cli.js +37 -37
- package/package.json +64 -64
- package/runtime/velu-ui/base.css +320 -320
- package/runtime/velu-ui/components/Accordion.jsx +64 -64
- package/runtime/velu-ui/components/ApiClient.jsx +207 -207
- package/runtime/velu-ui/components/ApiField.jsx +87 -87
- package/runtime/velu-ui/components/ApiPath.jsx +63 -63
- package/runtime/velu-ui/components/ApiReferencePage.jsx +384 -384
- package/runtime/velu-ui/components/ApiSamples.jsx +36 -36
- package/runtime/velu-ui/components/ApiSidebar.jsx +122 -122
- package/runtime/velu-ui/components/AskBar.jsx +71 -71
- package/runtime/velu-ui/components/Callout.jsx +114 -114
- package/runtime/velu-ui/components/Card.jsx +131 -131
- package/runtime/velu-ui/components/Chatbot.jsx +885 -885
- package/runtime/velu-ui/components/CodeBlock.jsx +375 -375
- package/runtime/velu-ui/components/Columns.jsx +56 -56
- package/runtime/velu-ui/components/ContextMenu.jsx +298 -273
- package/runtime/velu-ui/components/ErrorCard.jsx +138 -138
- package/runtime/velu-ui/components/Field.jsx +81 -81
- package/runtime/velu-ui/components/Image.jsx +163 -163
- package/runtime/velu-ui/components/Logo.jsx +31 -31
- package/runtime/velu-ui/components/MethodBadge.jsx +31 -31
- package/runtime/velu-ui/components/NavSelect.jsx +108 -108
- package/runtime/velu-ui/components/NotFound.jsx +63 -63
- package/runtime/velu-ui/components/PageFeedback.jsx +219 -219
- package/runtime/velu-ui/components/PageFooter.jsx +145 -145
- package/runtime/velu-ui/components/PageHeader.jsx +422 -422
- package/runtime/velu-ui/components/PageNav.jsx +77 -77
- package/runtime/velu-ui/components/PoweredBy.jsx +51 -51
- package/runtime/velu-ui/components/Prompt.jsx +115 -115
- package/runtime/velu-ui/components/Search.jsx +460 -460
- package/runtime/velu-ui/components/Sidebar.jsx +254 -254
- package/runtime/velu-ui/components/SocialLinks.jsx +90 -90
- package/runtime/velu-ui/components/Steps.jsx +65 -65
- package/runtime/velu-ui/components/ThemeToggle.jsx +48 -48
- package/runtime/velu-ui/components/Toc.jsx +537 -537
- package/runtime/velu-ui/components/TocBar.jsx +195 -195
- package/runtime/velu-ui/components/Tree.jsx +87 -87
- package/runtime/velu-ui/components/TryItBar.jsx +102 -102
- package/runtime/velu-ui/components/accordion.css +92 -92
- package/runtime/velu-ui/components/api-page.css +208 -208
- package/runtime/velu-ui/components/api.css +635 -635
- package/runtime/velu-ui/components/ask-bar.css +94 -94
- package/runtime/velu-ui/components/card.css +105 -105
- package/runtime/velu-ui/components/chatbot.css +622 -622
- package/runtime/velu-ui/components/code-block.css +263 -263
- package/runtime/velu-ui/components/context-menu.css +173 -173
- package/runtime/velu-ui/components/docs-layout.css +822 -822
- package/runtime/velu-ui/components/field.css +82 -82
- package/runtime/velu-ui/components/image.css +237 -237
- package/runtime/velu-ui/components/nav-select.css +157 -157
- package/runtime/velu-ui/components/not-found.css +94 -94
- package/runtime/velu-ui/components/page-feedback.css +241 -241
- package/runtime/velu-ui/components/page-footer.css +130 -130
- package/runtime/velu-ui/components/page-header.css +558 -558
- package/runtime/velu-ui/components/page-nav.css +50 -50
- package/runtime/velu-ui/components/powered-by.css +92 -92
- package/runtime/velu-ui/components/prompt.css +99 -99
- package/runtime/velu-ui/components/search.css +307 -307
- package/runtime/velu-ui/components/sidebar.css +205 -205
- package/runtime/velu-ui/components/steps.css +77 -77
- package/runtime/velu-ui/components/theme-toggle.css +102 -102
- package/runtime/velu-ui/components/toc-bar.css +234 -234
- package/runtime/velu-ui/components/tree.css +49 -49
- package/runtime/velu-ui/index.js +54 -54
- package/runtime/velu-ui/lib/api-send.js +92 -92
- package/runtime/velu-ui/lib/brand-icons.jsx +103 -103
- package/runtime/velu-ui/lib/component-schemas.js +100 -100
- package/runtime/velu-ui/lib/copyText.js +64 -64
- package/runtime/velu-ui/lib/docs-assistant.js +250 -250
- package/runtime/velu-ui/lib/lang-icons.jsx +147 -147
- package/runtime/velu-ui/lib/pagefind.js +113 -113
- package/runtime/velu-ui/lib/prism-langs.js +957 -957
- package/runtime/velu-ui/lib/prism-loader.js +74 -74
- package/runtime/velu-ui/lib/resolveIcon.jsx +29 -29
- package/runtime/velu-ui/lib/scrollIntoNearestView.js +66 -66
- package/runtime/velu-ui/mdx-components.jsx +105 -105
- package/runtime/velu-ui/primitives/Cluster.jsx +49 -49
- package/runtime/velu-ui/primitives/Stack.jsx +63 -63
- package/runtime/velu-ui/primitives/Switcher.jsx +57 -57
- package/runtime/velu-ui/primitives/stack.css +3 -3
- package/runtime/velu-ui/primitives/switcher.css +25 -25
- package/runtime/velu-ui/styles.css +46 -46
- package/runtime/velu-ui/tokens.css +4 -4
- package/schema/velu.schema.json +423 -423
- package/src/lib/extract-mdx-error.js +170 -170
- package/src/lib/issues.js +159 -159
- package/src/lib/known-components.js +34 -34
- package/src/navigation.js +443 -443
- package/src/runtime/App.jsx +1669 -1668
- package/src/runtime/ErrorBoundary.jsx +54 -54
- package/src/runtime/client-entry.jsx +22 -22
- package/src/runtime/server-entry.jsx +16 -16
- package/src/template.html +48 -48
- package/templates/starter/ai-tools/claude-code.mdx +26 -26
- package/templates/starter/ai-tools/cursor.mdx +17 -17
- package/templates/starter/api-reference/introduction.mdx +43 -43
- package/templates/starter/development.mdx +19 -19
- package/templates/starter/essentials/code.mdx +29 -29
- package/templates/starter/essentials/images.mdx +29 -29
- package/templates/starter/essentials/markdown.mdx +25 -25
- package/templates/starter/essentials/navigation.mdx +39 -39
- package/templates/starter/essentials/settings.mdx +30 -30
- package/templates/starter/favicon.svg +6 -6
- package/templates/starter/index.mdx +31 -31
- package/templates/starter/openapi.json +160 -160
- package/templates/starter/quickstart.mdx +31 -31
- package/templates/starter/velu.json +41 -41
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import resolveIcon from '../lib/resolveIcon.jsx';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Accordion + AccordionGroup — collapsible disclosure.
|
|
6
|
-
*
|
|
7
|
-
* Native <details>/<summary>: zero JS, SSR-safe, accessible, chevron rotates
|
|
8
|
-
* via CSS on [open] (same pattern as Sidebar). Fully tokenized — border /
|
|
9
|
-
* hover / expanded surface / radius / spacing / type all from base.css
|
|
10
|
-
* tokens, so light + dark theme automatically via [data-theme].
|
|
11
|
-
*
|
|
12
|
-
* - <Accordion title=… defaultOpen?>content</Accordion> — standalone bordered
|
|
13
|
-
* rounded box.
|
|
14
|
-
* - <AccordionGroup><Accordion/>…</AccordionGroup> — items joined into ONE
|
|
15
|
-
* bordered box with dividers between them (no per-item border/radius). A
|
|
16
|
-
* context tells nested Accordions they're grouped so they drop their own
|
|
17
|
-
* chrome; the group container owns the border + radius + dividers.
|
|
18
|
-
*
|
|
19
|
-
* @param {{ title: React.ReactNode, defaultOpen?: boolean,
|
|
20
|
-
* children: React.ReactNode, className?: string }} props
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
const GroupCtx = React.createContext(false);
|
|
24
|
-
|
|
25
|
-
export function Accordion({
|
|
26
|
-
title,
|
|
27
|
-
defaultOpen = false,
|
|
28
|
-
children,
|
|
29
|
-
className = '',
|
|
30
|
-
...rest
|
|
31
|
-
}) {
|
|
32
|
-
const inGroup = React.useContext(GroupCtx);
|
|
33
|
-
const cls = [
|
|
34
|
-
'velu-accordion',
|
|
35
|
-
inGroup ? 'velu-accordion--in-group' : '',
|
|
36
|
-
className,
|
|
37
|
-
]
|
|
38
|
-
.filter(Boolean)
|
|
39
|
-
.join(' ');
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<details className={cls} open={defaultOpen || undefined} {...rest}>
|
|
43
|
-
<summary className="velu-accordion__summary">
|
|
44
|
-
<span className="velu-accordion__chevron" aria-hidden="true">
|
|
45
|
-
{resolveIcon('chevron-right', { size: '1em' })}
|
|
46
|
-
</span>
|
|
47
|
-
<span className="velu-accordion__title">{title}</span>
|
|
48
|
-
</summary>
|
|
49
|
-
<div className="velu-accordion__content">{children}</div>
|
|
50
|
-
</details>
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function AccordionGroup({ children, className = '', ...rest }) {
|
|
55
|
-
return (
|
|
56
|
-
<GroupCtx.Provider value={true}>
|
|
57
|
-
<div className={`velu-accordion-group ${className}`.trim()} {...rest}>
|
|
58
|
-
{children}
|
|
59
|
-
</div>
|
|
60
|
-
</GroupCtx.Provider>
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export default Accordion;
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import resolveIcon from '../lib/resolveIcon.jsx';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Accordion + AccordionGroup — collapsible disclosure.
|
|
6
|
+
*
|
|
7
|
+
* Native <details>/<summary>: zero JS, SSR-safe, accessible, chevron rotates
|
|
8
|
+
* via CSS on [open] (same pattern as Sidebar). Fully tokenized — border /
|
|
9
|
+
* hover / expanded surface / radius / spacing / type all from base.css
|
|
10
|
+
* tokens, so light + dark theme automatically via [data-theme].
|
|
11
|
+
*
|
|
12
|
+
* - <Accordion title=… defaultOpen?>content</Accordion> — standalone bordered
|
|
13
|
+
* rounded box.
|
|
14
|
+
* - <AccordionGroup><Accordion/>…</AccordionGroup> — items joined into ONE
|
|
15
|
+
* bordered box with dividers between them (no per-item border/radius). A
|
|
16
|
+
* context tells nested Accordions they're grouped so they drop their own
|
|
17
|
+
* chrome; the group container owns the border + radius + dividers.
|
|
18
|
+
*
|
|
19
|
+
* @param {{ title: React.ReactNode, defaultOpen?: boolean,
|
|
20
|
+
* children: React.ReactNode, className?: string }} props
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const GroupCtx = React.createContext(false);
|
|
24
|
+
|
|
25
|
+
export function Accordion({
|
|
26
|
+
title,
|
|
27
|
+
defaultOpen = false,
|
|
28
|
+
children,
|
|
29
|
+
className = '',
|
|
30
|
+
...rest
|
|
31
|
+
}) {
|
|
32
|
+
const inGroup = React.useContext(GroupCtx);
|
|
33
|
+
const cls = [
|
|
34
|
+
'velu-accordion',
|
|
35
|
+
inGroup ? 'velu-accordion--in-group' : '',
|
|
36
|
+
className,
|
|
37
|
+
]
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.join(' ');
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<details className={cls} open={defaultOpen || undefined} {...rest}>
|
|
43
|
+
<summary className="velu-accordion__summary">
|
|
44
|
+
<span className="velu-accordion__chevron" aria-hidden="true">
|
|
45
|
+
{resolveIcon('chevron-right', { size: '1em' })}
|
|
46
|
+
</span>
|
|
47
|
+
<span className="velu-accordion__title">{title}</span>
|
|
48
|
+
</summary>
|
|
49
|
+
<div className="velu-accordion__content">{children}</div>
|
|
50
|
+
</details>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function AccordionGroup({ children, className = '', ...rest }) {
|
|
55
|
+
return (
|
|
56
|
+
<GroupCtx.Provider value={true}>
|
|
57
|
+
<div className={`velu-accordion-group ${className}`.trim()} {...rest}>
|
|
58
|
+
{children}
|
|
59
|
+
</div>
|
|
60
|
+
</GroupCtx.Provider>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default Accordion;
|
|
@@ -1,207 +1,207 @@
|
|
|
1
|
-
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
-
import resolveIcon from '../lib/resolveIcon.jsx';
|
|
3
|
-
import Cluster from '../primitives/Cluster.jsx';
|
|
4
|
-
import MethodBadge from './MethodBadge.jsx';
|
|
5
|
-
import TryItBar from './TryItBar.jsx';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* ApiClient — the "try-it" playground layout shell.
|
|
9
|
-
*
|
|
10
|
-
* <ApiClient
|
|
11
|
-
* method="DELETE"
|
|
12
|
-
* label="Trigger Delete"
|
|
13
|
-
* path="/project/preview/{projectId}"
|
|
14
|
-
* description="Trigger a documentation deployment…"
|
|
15
|
-
* onClose={() => …}
|
|
16
|
-
* aside={
|
|
17
|
-
* <CodeGroup>…response / samples / examples…</CodeGroup>
|
|
18
|
-
* }
|
|
19
|
-
* >
|
|
20
|
-
* <AccordionGroup>
|
|
21
|
-
* <Accordion title="Authorization">
|
|
22
|
-
* <ApiField name="Authorization" type="string" required
|
|
23
|
-
* prefix="Bearer" />
|
|
24
|
-
* </Accordion>
|
|
25
|
-
* <Accordion title="Path">
|
|
26
|
-
* <ApiField name="projectId" type="string" required />
|
|
27
|
-
* </Accordion>
|
|
28
|
-
* </AccordionGroup>
|
|
29
|
-
* </ApiClient>
|
|
30
|
-
*
|
|
31
|
-
* ApiClient is a pure layout shell — it owns the frame, the close
|
|
32
|
-
* button, the operation/endpoint head row, and the two-column body.
|
|
33
|
-
* It does NOT re-implement collapsibles or code panels:
|
|
34
|
-
*
|
|
35
|
-
* - LEFT column = `children` — compose the request sections with the
|
|
36
|
-
* shared <Accordion> / <AccordionGroup> + <ApiField>.
|
|
37
|
-
* - RIGHT column = `aside` — compose the response / code-sample
|
|
38
|
-
* panels with the shared <CodeBlock> / <CodeGroup>.
|
|
39
|
-
*
|
|
40
|
-
* CTAs (Send, the operation dropdown) are presentational stubs — wire
|
|
41
|
-
* `onSend` / a real selector when the OpenAPI adapter lands.
|
|
42
|
-
*/
|
|
43
|
-
|
|
44
|
-
export default function ApiClient({
|
|
45
|
-
method = 'GET',
|
|
46
|
-
label,
|
|
47
|
-
path = '',
|
|
48
|
-
description,
|
|
49
|
-
operations = [],
|
|
50
|
-
activeId,
|
|
51
|
-
onSelectOperation,
|
|
52
|
-
onSend,
|
|
53
|
-
sending = false,
|
|
54
|
-
onClose,
|
|
55
|
-
response,
|
|
56
|
-
aside,
|
|
57
|
-
children,
|
|
58
|
-
className = '',
|
|
59
|
-
...rest
|
|
60
|
-
}) {
|
|
61
|
-
const key = String(method).toLowerCase();
|
|
62
|
-
const cls = `velu-api-client velu-api-client--${key} ${className}`.trim();
|
|
63
|
-
|
|
64
|
-
// The operation dropdown is interactive only when there's more than one
|
|
65
|
-
// operation to switch between and a handler to call.
|
|
66
|
-
const canSwitch =
|
|
67
|
-
Array.isArray(operations) && operations.length > 1 && typeof onSelectOperation === 'function';
|
|
68
|
-
const [menuOpen, setMenuOpen] = useState(false);
|
|
69
|
-
const opRef = useRef(null);
|
|
70
|
-
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
if (!menuOpen) return undefined;
|
|
73
|
-
const onDown = (e) => {
|
|
74
|
-
if (opRef.current && !opRef.current.contains(e.target)) setMenuOpen(false);
|
|
75
|
-
};
|
|
76
|
-
const onKey = (e) => {
|
|
77
|
-
if (e.key === 'Escape') setMenuOpen(false);
|
|
78
|
-
};
|
|
79
|
-
document.addEventListener('mousedown', onDown);
|
|
80
|
-
document.addEventListener('keydown', onKey);
|
|
81
|
-
return () => {
|
|
82
|
-
document.removeEventListener('mousedown', onDown);
|
|
83
|
-
document.removeEventListener('keydown', onKey);
|
|
84
|
-
};
|
|
85
|
-
}, [menuOpen]);
|
|
86
|
-
|
|
87
|
-
return (
|
|
88
|
-
<div className={cls} {...rest}>
|
|
89
|
-
{/* Close button row — only when shown as a modal (onClose set). */}
|
|
90
|
-
{onClose && (
|
|
91
|
-
<div className="velu-api-client__close-row">
|
|
92
|
-
<button
|
|
93
|
-
type="button"
|
|
94
|
-
className="velu-api-client__close"
|
|
95
|
-
onClick={onClose}
|
|
96
|
-
aria-label="Close"
|
|
97
|
-
>
|
|
98
|
-
{resolveIcon('x', { size: '1em' })}
|
|
99
|
-
</button>
|
|
100
|
-
</div>
|
|
101
|
-
)}
|
|
102
|
-
|
|
103
|
-
{/* Head — operation dropdown (dummy) + TryItBar relabeled "Send".
|
|
104
|
-
Cluster keeps each at its natural width: the dropdown hugs its
|
|
105
|
-
text (flex: none), the bar fills the rest (flex: 1). When the
|
|
106
|
-
bar can't keep its min width, Cluster wraps it below the
|
|
107
|
-
dropdown. */}
|
|
108
|
-
<Cluster
|
|
109
|
-
space="var(--s-1)"
|
|
110
|
-
align="flex-start"
|
|
111
|
-
className="velu-api-client__head"
|
|
112
|
-
>
|
|
113
|
-
<div className="velu-api-client__op" ref={opRef}>
|
|
114
|
-
{canSwitch ? (
|
|
115
|
-
<button
|
|
116
|
-
type="button"
|
|
117
|
-
className="velu-api-client__op-btn"
|
|
118
|
-
onClick={() => setMenuOpen((v) => !v)}
|
|
119
|
-
aria-haspopup="listbox"
|
|
120
|
-
aria-expanded={menuOpen}
|
|
121
|
-
>
|
|
122
|
-
<MethodBadge method={method} />
|
|
123
|
-
{label && (
|
|
124
|
-
<span className="velu-api-client__op-label">{label}</span>
|
|
125
|
-
)}
|
|
126
|
-
<span className="velu-api-client__op-chevron" aria-hidden="true">
|
|
127
|
-
{resolveIcon('chevron-down', { size: '1em' })}
|
|
128
|
-
</span>
|
|
129
|
-
</button>
|
|
130
|
-
) : (
|
|
131
|
-
<>
|
|
132
|
-
<MethodBadge method={method} />
|
|
133
|
-
{label && (
|
|
134
|
-
<span className="velu-api-client__op-label">{label}</span>
|
|
135
|
-
)}
|
|
136
|
-
<span className="velu-api-client__op-chevron" aria-hidden="true">
|
|
137
|
-
{resolveIcon('chevron-down', { size: '1em' })}
|
|
138
|
-
</span>
|
|
139
|
-
</>
|
|
140
|
-
)}
|
|
141
|
-
{canSwitch && menuOpen && (
|
|
142
|
-
<ul
|
|
143
|
-
className="velu-api-client__op-menu velu-hide-scrollbar"
|
|
144
|
-
role="listbox"
|
|
145
|
-
>
|
|
146
|
-
{operations.map((o) => (
|
|
147
|
-
<li key={o.id}>
|
|
148
|
-
<button
|
|
149
|
-
type="button"
|
|
150
|
-
role="option"
|
|
151
|
-
aria-selected={o.id === activeId}
|
|
152
|
-
className={`velu-api-client__op-item${
|
|
153
|
-
o.id === activeId ? ' velu-api-client__op-item--active' : ''
|
|
154
|
-
}`}
|
|
155
|
-
onClick={() => {
|
|
156
|
-
onSelectOperation(o.id);
|
|
157
|
-
setMenuOpen(false);
|
|
158
|
-
}}
|
|
159
|
-
>
|
|
160
|
-
<MethodBadge method={o.method} />
|
|
161
|
-
<span className="velu-api-client__op-item-label">
|
|
162
|
-
{o.title}
|
|
163
|
-
</span>
|
|
164
|
-
</button>
|
|
165
|
-
</li>
|
|
166
|
-
))}
|
|
167
|
-
</ul>
|
|
168
|
-
)}
|
|
169
|
-
</div>
|
|
170
|
-
<TryItBar
|
|
171
|
-
method={method}
|
|
172
|
-
path={path}
|
|
173
|
-
cta="Send"
|
|
174
|
-
onTry={onSend}
|
|
175
|
-
loading={sending}
|
|
176
|
-
className="velu-api-client__bar"
|
|
177
|
-
/>
|
|
178
|
-
</Cluster>
|
|
179
|
-
|
|
180
|
-
{/* Body grid (areas: form / response / aside). Desktop: form fills the
|
|
181
|
-
left column, response + aside stack in the right. Mobile (single
|
|
182
|
-
column): the live response moves ABOVE the form so a Send result is
|
|
183
|
-
visible without scrolling past the inputs; the form and code samples
|
|
184
|
-
follow. See api.css. */}
|
|
185
|
-
<div
|
|
186
|
-
className="velu-api-client__body"
|
|
187
|
-
data-has-response={response != null ? 'true' : undefined}
|
|
188
|
-
>
|
|
189
|
-
<div className="velu-api-client__left">
|
|
190
|
-
{label && (
|
|
191
|
-
<h3 className="velu-api-client__title">{label}</h3>
|
|
192
|
-
)}
|
|
193
|
-
{description && (
|
|
194
|
-
<p className="velu-api-client__description">{description}</p>
|
|
195
|
-
)}
|
|
196
|
-
{children}
|
|
197
|
-
</div>
|
|
198
|
-
{response != null && (
|
|
199
|
-
<div className="velu-api-client__response">{response}</div>
|
|
200
|
-
)}
|
|
201
|
-
{aside != null && (
|
|
202
|
-
<div className="velu-api-client__right">{aside}</div>
|
|
203
|
-
)}
|
|
204
|
-
</div>
|
|
205
|
-
</div>
|
|
206
|
-
);
|
|
207
|
-
}
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import resolveIcon from '../lib/resolveIcon.jsx';
|
|
3
|
+
import Cluster from '../primitives/Cluster.jsx';
|
|
4
|
+
import MethodBadge from './MethodBadge.jsx';
|
|
5
|
+
import TryItBar from './TryItBar.jsx';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ApiClient — the "try-it" playground layout shell.
|
|
9
|
+
*
|
|
10
|
+
* <ApiClient
|
|
11
|
+
* method="DELETE"
|
|
12
|
+
* label="Trigger Delete"
|
|
13
|
+
* path="/project/preview/{projectId}"
|
|
14
|
+
* description="Trigger a documentation deployment…"
|
|
15
|
+
* onClose={() => …}
|
|
16
|
+
* aside={
|
|
17
|
+
* <CodeGroup>…response / samples / examples…</CodeGroup>
|
|
18
|
+
* }
|
|
19
|
+
* >
|
|
20
|
+
* <AccordionGroup>
|
|
21
|
+
* <Accordion title="Authorization">
|
|
22
|
+
* <ApiField name="Authorization" type="string" required
|
|
23
|
+
* prefix="Bearer" />
|
|
24
|
+
* </Accordion>
|
|
25
|
+
* <Accordion title="Path">
|
|
26
|
+
* <ApiField name="projectId" type="string" required />
|
|
27
|
+
* </Accordion>
|
|
28
|
+
* </AccordionGroup>
|
|
29
|
+
* </ApiClient>
|
|
30
|
+
*
|
|
31
|
+
* ApiClient is a pure layout shell — it owns the frame, the close
|
|
32
|
+
* button, the operation/endpoint head row, and the two-column body.
|
|
33
|
+
* It does NOT re-implement collapsibles or code panels:
|
|
34
|
+
*
|
|
35
|
+
* - LEFT column = `children` — compose the request sections with the
|
|
36
|
+
* shared <Accordion> / <AccordionGroup> + <ApiField>.
|
|
37
|
+
* - RIGHT column = `aside` — compose the response / code-sample
|
|
38
|
+
* panels with the shared <CodeBlock> / <CodeGroup>.
|
|
39
|
+
*
|
|
40
|
+
* CTAs (Send, the operation dropdown) are presentational stubs — wire
|
|
41
|
+
* `onSend` / a real selector when the OpenAPI adapter lands.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
export default function ApiClient({
|
|
45
|
+
method = 'GET',
|
|
46
|
+
label,
|
|
47
|
+
path = '',
|
|
48
|
+
description,
|
|
49
|
+
operations = [],
|
|
50
|
+
activeId,
|
|
51
|
+
onSelectOperation,
|
|
52
|
+
onSend,
|
|
53
|
+
sending = false,
|
|
54
|
+
onClose,
|
|
55
|
+
response,
|
|
56
|
+
aside,
|
|
57
|
+
children,
|
|
58
|
+
className = '',
|
|
59
|
+
...rest
|
|
60
|
+
}) {
|
|
61
|
+
const key = String(method).toLowerCase();
|
|
62
|
+
const cls = `velu-api-client velu-api-client--${key} ${className}`.trim();
|
|
63
|
+
|
|
64
|
+
// The operation dropdown is interactive only when there's more than one
|
|
65
|
+
// operation to switch between and a handler to call.
|
|
66
|
+
const canSwitch =
|
|
67
|
+
Array.isArray(operations) && operations.length > 1 && typeof onSelectOperation === 'function';
|
|
68
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
69
|
+
const opRef = useRef(null);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!menuOpen) return undefined;
|
|
73
|
+
const onDown = (e) => {
|
|
74
|
+
if (opRef.current && !opRef.current.contains(e.target)) setMenuOpen(false);
|
|
75
|
+
};
|
|
76
|
+
const onKey = (e) => {
|
|
77
|
+
if (e.key === 'Escape') setMenuOpen(false);
|
|
78
|
+
};
|
|
79
|
+
document.addEventListener('mousedown', onDown);
|
|
80
|
+
document.addEventListener('keydown', onKey);
|
|
81
|
+
return () => {
|
|
82
|
+
document.removeEventListener('mousedown', onDown);
|
|
83
|
+
document.removeEventListener('keydown', onKey);
|
|
84
|
+
};
|
|
85
|
+
}, [menuOpen]);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className={cls} {...rest}>
|
|
89
|
+
{/* Close button row — only when shown as a modal (onClose set). */}
|
|
90
|
+
{onClose && (
|
|
91
|
+
<div className="velu-api-client__close-row">
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
className="velu-api-client__close"
|
|
95
|
+
onClick={onClose}
|
|
96
|
+
aria-label="Close"
|
|
97
|
+
>
|
|
98
|
+
{resolveIcon('x', { size: '1em' })}
|
|
99
|
+
</button>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
{/* Head — operation dropdown (dummy) + TryItBar relabeled "Send".
|
|
104
|
+
Cluster keeps each at its natural width: the dropdown hugs its
|
|
105
|
+
text (flex: none), the bar fills the rest (flex: 1). When the
|
|
106
|
+
bar can't keep its min width, Cluster wraps it below the
|
|
107
|
+
dropdown. */}
|
|
108
|
+
<Cluster
|
|
109
|
+
space="var(--s-1)"
|
|
110
|
+
align="flex-start"
|
|
111
|
+
className="velu-api-client__head"
|
|
112
|
+
>
|
|
113
|
+
<div className="velu-api-client__op" ref={opRef}>
|
|
114
|
+
{canSwitch ? (
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
className="velu-api-client__op-btn"
|
|
118
|
+
onClick={() => setMenuOpen((v) => !v)}
|
|
119
|
+
aria-haspopup="listbox"
|
|
120
|
+
aria-expanded={menuOpen}
|
|
121
|
+
>
|
|
122
|
+
<MethodBadge method={method} />
|
|
123
|
+
{label && (
|
|
124
|
+
<span className="velu-api-client__op-label">{label}</span>
|
|
125
|
+
)}
|
|
126
|
+
<span className="velu-api-client__op-chevron" aria-hidden="true">
|
|
127
|
+
{resolveIcon('chevron-down', { size: '1em' })}
|
|
128
|
+
</span>
|
|
129
|
+
</button>
|
|
130
|
+
) : (
|
|
131
|
+
<>
|
|
132
|
+
<MethodBadge method={method} />
|
|
133
|
+
{label && (
|
|
134
|
+
<span className="velu-api-client__op-label">{label}</span>
|
|
135
|
+
)}
|
|
136
|
+
<span className="velu-api-client__op-chevron" aria-hidden="true">
|
|
137
|
+
{resolveIcon('chevron-down', { size: '1em' })}
|
|
138
|
+
</span>
|
|
139
|
+
</>
|
|
140
|
+
)}
|
|
141
|
+
{canSwitch && menuOpen && (
|
|
142
|
+
<ul
|
|
143
|
+
className="velu-api-client__op-menu velu-hide-scrollbar"
|
|
144
|
+
role="listbox"
|
|
145
|
+
>
|
|
146
|
+
{operations.map((o) => (
|
|
147
|
+
<li key={o.id}>
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
role="option"
|
|
151
|
+
aria-selected={o.id === activeId}
|
|
152
|
+
className={`velu-api-client__op-item${
|
|
153
|
+
o.id === activeId ? ' velu-api-client__op-item--active' : ''
|
|
154
|
+
}`}
|
|
155
|
+
onClick={() => {
|
|
156
|
+
onSelectOperation(o.id);
|
|
157
|
+
setMenuOpen(false);
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
<MethodBadge method={o.method} />
|
|
161
|
+
<span className="velu-api-client__op-item-label">
|
|
162
|
+
{o.title}
|
|
163
|
+
</span>
|
|
164
|
+
</button>
|
|
165
|
+
</li>
|
|
166
|
+
))}
|
|
167
|
+
</ul>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
<TryItBar
|
|
171
|
+
method={method}
|
|
172
|
+
path={path}
|
|
173
|
+
cta="Send"
|
|
174
|
+
onTry={onSend}
|
|
175
|
+
loading={sending}
|
|
176
|
+
className="velu-api-client__bar"
|
|
177
|
+
/>
|
|
178
|
+
</Cluster>
|
|
179
|
+
|
|
180
|
+
{/* Body grid (areas: form / response / aside). Desktop: form fills the
|
|
181
|
+
left column, response + aside stack in the right. Mobile (single
|
|
182
|
+
column): the live response moves ABOVE the form so a Send result is
|
|
183
|
+
visible without scrolling past the inputs; the form and code samples
|
|
184
|
+
follow. See api.css. */}
|
|
185
|
+
<div
|
|
186
|
+
className="velu-api-client__body"
|
|
187
|
+
data-has-response={response != null ? 'true' : undefined}
|
|
188
|
+
>
|
|
189
|
+
<div className="velu-api-client__left">
|
|
190
|
+
{label && (
|
|
191
|
+
<h3 className="velu-api-client__title">{label}</h3>
|
|
192
|
+
)}
|
|
193
|
+
{description && (
|
|
194
|
+
<p className="velu-api-client__description">{description}</p>
|
|
195
|
+
)}
|
|
196
|
+
{children}
|
|
197
|
+
</div>
|
|
198
|
+
{response != null && (
|
|
199
|
+
<div className="velu-api-client__response">{response}</div>
|
|
200
|
+
)}
|
|
201
|
+
{aside != null && (
|
|
202
|
+
<div className="velu-api-client__right">{aside}</div>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|