@veluai/velu 0.1.15 → 0.1.16

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.
@@ -0,0 +1,388 @@
1
+ import React, { useState, useCallback, useEffect } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import MethodBadge from './MethodBadge.jsx';
4
+ import ApiPath from './ApiPath.jsx';
5
+ import TryItBar from './TryItBar.jsx';
6
+ import ApiClient from './ApiClient.jsx';
7
+ import ApiField from './ApiField.jsx';
8
+ import Field from './Field.jsx';
9
+ import Accordion, { AccordionGroup } from './Accordion.jsx';
10
+ import CodeBlock, { CodeGroup } from './CodeBlock.jsx';
11
+ import ApiSamples from './ApiSamples.jsx';
12
+ import Callout from './Callout.jsx';
13
+ import { sendApiRequest } from '../lib/api-send.js';
14
+
15
+ /**
16
+ * ApiReferencePage — the full, auto-generated reference page for one OpenAPI
17
+ * operation. Driven entirely by the normalized `operation` model (see the
18
+ * CLI's src/openapi/parse.js). Renders the doc body (auth, params, body,
19
+ * responses) and an interactive "Try It" playground that builds + sends the
20
+ * request (through the dev proxy by default — see lib/api-send.js).
21
+ *
22
+ * The right-rail code samples live in the page's right aside (the runtime
23
+ * renders <ApiSamples> there for API pages) — this component owns the main
24
+ * column + the playground only.
25
+ *
26
+ * @param {{ operation: object, api?: { proxy?: boolean, server?: string } }} props
27
+ */
28
+ const LOCATION_LABEL = { path: 'Path Parameters', query: 'Query Parameters', header: 'Header Parameters' };
29
+
30
+ export default function ApiReferencePage({ operation, samples = [], apiOperations = [], api = {} }) {
31
+ const [open, setOpen] = useState(false);
32
+ // Which operation the playground is currently showing. Defaults to this
33
+ // page's own operation; the dropdown can switch it to any sibling.
34
+ const [activeId, setActiveId] = useState(operation.id);
35
+
36
+ // The switchable list — fall back to just this page's operation.
37
+ const ops =
38
+ apiOperations.length > 0
39
+ ? apiOperations
40
+ : [{ id: operation.id, method: operation.method, title: operation.title, operation, samples }];
41
+ const current = ops.find((o) => o.id === activeId) || { operation, samples };
42
+ const server = api.server || current.operation.servers?.[0] || '';
43
+
44
+ // While the playground modal is open, close on Escape and lock the
45
+ // page scroll so the dimmed backdrop reads as a true overlay.
46
+ useEffect(() => {
47
+ if (!open || typeof document === 'undefined') return undefined;
48
+ const onKey = (e) => {
49
+ if (e.key === 'Escape') setOpen(false);
50
+ };
51
+ document.addEventListener('keydown', onKey);
52
+ const prev = document.body.style.overflow;
53
+ document.body.style.overflow = 'hidden';
54
+ return () => {
55
+ document.removeEventListener('keydown', onKey);
56
+ document.body.style.overflow = prev;
57
+ };
58
+ }, [open]);
59
+
60
+ if (!operation) return null;
61
+
62
+ return (
63
+ <div className="velu-api-page">
64
+ {/* The page title + description come from the page frontmatter heading
65
+ (rendered by the runtime), so this component starts at the Try-It bar
66
+ to avoid a duplicate title. */}
67
+ <TryItBar
68
+ method={operation.method}
69
+ path={operation.path}
70
+ cta="Try It"
71
+ onTry={() => {
72
+ setActiveId(operation.id);
73
+ setOpen(true);
74
+ }}
75
+ className="velu-api-page__tryit"
76
+ />
77
+
78
+ {/* Authorizations */}
79
+ {operation.auth?.length > 0 && (
80
+ <section className="velu-api-page__section">
81
+ <h2>Authorizations</h2>
82
+ {operation.auth.map((a, i) => (
83
+ <Field key={i} name={a.name} pre={a.prefix} type={a.type} required={a.required} post={a.in}>
84
+ {a.description}
85
+ </Field>
86
+ ))}
87
+ </section>
88
+ )}
89
+
90
+ {/* Parameters, by location */}
91
+ {['path', 'query', 'header'].map((loc) => {
92
+ const rows = operation.parameters?.[loc] || [];
93
+ if (!rows.length) return null;
94
+ return (
95
+ <section key={loc} className="velu-api-page__section">
96
+ <h2>{LOCATION_LABEL[loc]}</h2>
97
+ {rows.map((p, i) => (
98
+ <Field key={i} name={p.name} type={p.type} required={p.required}>
99
+ {p.description}
100
+ {p.enum && <EnumHint values={p.enum} />}
101
+ </Field>
102
+ ))}
103
+ </section>
104
+ );
105
+ })}
106
+
107
+ {/* Request body */}
108
+ {operation.body?.fields?.length > 0 && (
109
+ <section className="velu-api-page__section">
110
+ <h2>Body</h2>
111
+ {operation.body.fields.map((f, i) => (
112
+ <Field key={i} name={f.name} type={f.type} required={f.required}>
113
+ {f.description}
114
+ {f.enum && <EnumHint values={f.enum} />}
115
+ </Field>
116
+ ))}
117
+ </section>
118
+ )}
119
+
120
+ {/* Responses */}
121
+ {operation.responses?.length > 0 && (
122
+ <section className="velu-api-page__section">
123
+ <h2>Response</h2>
124
+ <ResponseSections responses={operation.responses} />
125
+ </section>
126
+ )}
127
+
128
+ {/* Try-It playground — rendered as an overlay above the page, with a
129
+ blurred backdrop, rather than inline. Portaled to <body> so the
130
+ docs layout's @container containment can't clip the fixed overlay. */}
131
+ {open && typeof document !== 'undefined' &&
132
+ createPortal(
133
+ <div
134
+ className="velu-api-modal"
135
+ role="presentation"
136
+ onClick={() => setOpen(false)}
137
+ >
138
+ <div
139
+ className="velu-api-modal__panel"
140
+ role="dialog"
141
+ aria-modal="true"
142
+ aria-label={`${operation.title} playground`}
143
+ onClick={(e) => e.stopPropagation()}
144
+ >
145
+ <Playground
146
+ key={current.operation.id}
147
+ operation={current.operation}
148
+ samples={current.samples}
149
+ operations={ops}
150
+ activeId={activeId}
151
+ onSelectOperation={setActiveId}
152
+ server={server}
153
+ proxy={api.proxy !== false}
154
+ onClose={() => setOpen(false)}
155
+ />
156
+ </div>
157
+ </div>,
158
+ document.body,
159
+ )}
160
+ </div>
161
+ );
162
+ }
163
+
164
+ function EnumHint({ values }) {
165
+ return (
166
+ <p className="velu-api-page__enum">
167
+ Allowed values:{' '}
168
+ {values.map((v, i) => (
169
+ <code key={i}>{String(v)}</code>
170
+ ))}
171
+ </p>
172
+ );
173
+ }
174
+
175
+ function ResponseSections({ responses }) {
176
+ return (
177
+ <AccordionGroup className="velu-api-resp">
178
+ {responses.map((r, i) => {
179
+ return (
180
+ <Accordion
181
+ key={i}
182
+ defaultOpen
183
+ title={
184
+ <span className="velu-api-resp__head">
185
+ <span className="velu-api-resp__status">{r.status}</span>
186
+ {r.contentType && (
187
+ <span className="velu-api-resp__ctype">{r.contentType}</span>
188
+ )}
189
+ </span>
190
+ }
191
+ >
192
+ {r.description && <p className="velu-api-resp__desc">{r.description}</p>}
193
+ {r.fields?.map((f, j) => (
194
+ <Field key={j} name={f.name} type={f.type} required={f.required}>
195
+ {f.description}
196
+ </Field>
197
+ ))}
198
+ </Accordion>
199
+ );
200
+ })}
201
+ </AccordionGroup>
202
+ );
203
+ }
204
+
205
+ // ── The interactive playground ──────────────────────────────────────────────
206
+
207
+ function Playground({
208
+ operation,
209
+ samples = [],
210
+ operations = [],
211
+ activeId,
212
+ onSelectOperation,
213
+ server,
214
+ proxy,
215
+ onClose,
216
+ }) {
217
+ const [values, setValues] = useState({});
218
+ const [bodyText, setBodyText] = useState(
219
+ operation.body ? JSON.stringify(operation.body.example ?? {}, null, 2) : '',
220
+ );
221
+ const [busy, setBusy] = useState(false);
222
+ const [resp, setResp] = useState(null);
223
+
224
+ const set = (loc, name, v) =>
225
+ setValues((prev) => ({ ...prev, [loc]: { ...(prev[loc] || {}), [name]: v } }));
226
+
227
+ const onSend = useCallback(async () => {
228
+ setBusy(true);
229
+ setResp(null);
230
+ try {
231
+ const result = await sendApiRequest({ operation, server, proxy, values, bodyText });
232
+ setResp(result);
233
+ } catch (err) {
234
+ setResp({ error: String(err?.message || err) });
235
+ } finally {
236
+ setBusy(false);
237
+ }
238
+ }, [operation, server, proxy, values, bodyText]);
239
+
240
+ const sections = [];
241
+ if (operation.auth?.length) sections.push(['auth', 'Authorization', operation.auth]);
242
+ for (const loc of ['path', 'query', 'header']) {
243
+ const rows = operation.parameters?.[loc] || [];
244
+ if (rows.length) sections.push([loc, LOCATION_LABEL[loc].replace(' Parameters', ''), rows]);
245
+ }
246
+
247
+ return (
248
+ <ApiClient
249
+ method={operation.method}
250
+ label={operation.title}
251
+ path={operation.path}
252
+ description={operation.description}
253
+ operations={operations}
254
+ activeId={activeId}
255
+ onSelectOperation={onSelectOperation}
256
+ onSend={onSend}
257
+ sending={busy}
258
+ onClose={onClose}
259
+ aside={
260
+ <div className="velu-api-pg-aside">
261
+ {/* The live response is prepended above the static request /
262
+ response samples as it comes in. Progress is shown by the Send
263
+ button's spinner — no "Sending…" placeholder here. */}
264
+ {resp && <ResponsePanel resp={resp} />}
265
+ <ApiSamples samples={samples} responses={operation.responses} />
266
+ </div>
267
+ }
268
+ >
269
+ <AccordionGroup>
270
+ {sections.map(([loc, title, rows]) => (
271
+ <Accordion key={loc} title={title} defaultOpen>
272
+ {rows.map((f, i) => (
273
+ <ApiField
274
+ key={i}
275
+ name={f.name}
276
+ type={f.type}
277
+ required={f.required}
278
+ prefix={f.prefix}
279
+ value={values[loc]?.[f.name] ?? ''}
280
+ onChange={(e) => set(loc, f.name, e.target.value)}
281
+ >
282
+ {f.description}
283
+ </ApiField>
284
+ ))}
285
+ </Accordion>
286
+ ))}
287
+ {operation.body && (
288
+ <Accordion title="Body" defaultOpen>
289
+ <textarea
290
+ className="velu-api-body-input"
291
+ spellCheck={false}
292
+ rows={Math.min(14, bodyText.split('\n').length + 1)}
293
+ value={bodyText}
294
+ onChange={(e) => setBodyText(e.target.value)}
295
+ />
296
+ </Accordion>
297
+ )}
298
+ </AccordionGroup>
299
+ </ApiClient>
300
+ );
301
+ }
302
+
303
+ /**
304
+ * ResponsePanel — the live-response viewer: a Body / Header tabbed card.
305
+ * - Body: a status-colored Callout (green for 2xx, red otherwise) plus the
306
+ * response body.
307
+ * - Header: the response headers as a key/value table.
308
+ */
309
+ function ResponsePanel({ resp }) {
310
+ const [tab, setTab] = useState('body');
311
+ if (!resp) return null;
312
+
313
+ const isError = resp.error != null;
314
+ const ok = !isError && resp.status >= 200 && resp.status < 300;
315
+ const statusLabel = isError
316
+ ? 'Request failed'
317
+ : `${resp.status}${resp.statusText ? ` - ${resp.statusText}` : ''}`;
318
+ const bodyStr = isError
319
+ ? resp.error
320
+ : typeof resp.body === 'string'
321
+ ? resp.body
322
+ : JSON.stringify(resp.body, null, 2);
323
+ const headerRows = Object.entries(resp.headers || {});
324
+
325
+ // Reuse the CodeGroup's tab chrome (velu-code-block__*) so the Body/Header
326
+ // switcher looks identical to the code-sample tabs.
327
+ const tabCls = (name) =>
328
+ `velu-code-block__tab${tab === name ? ' velu-code-block__tab--active' : ''}`;
329
+
330
+ return (
331
+ <div className="velu-code-block velu-api-resp-panel">
332
+ <div className="velu-code-block__header">
333
+ <div className="velu-code-block__tabs" role="tablist">
334
+ <button
335
+ type="button"
336
+ role="tab"
337
+ aria-selected={tab === 'body'}
338
+ className={tabCls('body')}
339
+ onClick={() => setTab('body')}
340
+ >
341
+ <span className="velu-code-block__tab-label">Body</span>
342
+ </button>
343
+ <button
344
+ type="button"
345
+ role="tab"
346
+ aria-selected={tab === 'header'}
347
+ className={tabCls('header')}
348
+ onClick={() => setTab('header')}
349
+ disabled={!headerRows.length || undefined}
350
+ >
351
+ <span className="velu-code-block__tab-label">Header</span>
352
+ </button>
353
+ </div>
354
+ </div>
355
+
356
+ <div className="velu-api-resp-panel__content">
357
+ {tab === 'body' ? (
358
+ <>
359
+ <Callout
360
+ type={ok ? 'check' : 'danger'}
361
+ className="velu-api-resp-panel__status"
362
+ >
363
+ {statusLabel}
364
+ </Callout>
365
+ {!isError && (
366
+ <div className="velu-api-resp-body__scroll velu-hide-scrollbar">
367
+ <CodeBlock language="json">{bodyStr || '(empty)'}</CodeBlock>
368
+ </div>
369
+ )}
370
+ </>
371
+ ) : (
372
+ <div className="velu-api-resp-headers__scroll velu-hide-scrollbar">
373
+ <table className="velu-api-resp-headers">
374
+ <tbody>
375
+ {headerRows.map(([k, val]) => (
376
+ <tr key={k}>
377
+ <td className="velu-api-resp-headers__key">{k}</td>
378
+ <td className="velu-api-resp-headers__val">{val}</td>
379
+ </tr>
380
+ ))}
381
+ </tbody>
382
+ </table>
383
+ </div>
384
+ )}
385
+ </div>
386
+ </div>
387
+ );
388
+ }
@@ -0,0 +1,36 @@
1
+ import React from 'react';
2
+ import CodeBlock, { CodeGroup } from './CodeBlock.jsx';
3
+
4
+ /**
5
+ * ApiSamples — the right-rail of an API reference page: the request code
6
+ * snippets (curl / JS / Python / Ruby tabs) and the example responses
7
+ * (one tab per status). The runtime renders this in the page's right aside
8
+ * (where the TOC sits for normal pages).
9
+ *
10
+ * @param {{ samples: {key,label,language,code}[], responses: object[] }} props
11
+ */
12
+ export default function ApiSamples({ samples = [], responses = [] }) {
13
+ const withExample = responses.filter((r) => r.example != null);
14
+ return (
15
+ <div className="velu-api-samples">
16
+ {samples.length > 0 && (
17
+ <CodeGroup className="velu-api-samples__req">
18
+ {samples.map((s) => (
19
+ <CodeBlock key={s.key} title={s.label} language={s.language}>
20
+ {s.code}
21
+ </CodeBlock>
22
+ ))}
23
+ </CodeGroup>
24
+ )}
25
+ {withExample.length > 0 && (
26
+ <CodeGroup className="velu-api-samples__res">
27
+ {withExample.map((r) => (
28
+ <CodeBlock key={r.status} title={r.status} language="json">
29
+ {JSON.stringify(r.example, null, 2)}
30
+ </CodeBlock>
31
+ ))}
32
+ </CodeGroup>
33
+ )}
34
+ </div>
35
+ );
36
+ }
@@ -87,7 +87,7 @@ function ExternalIcon() {
87
87
  /** Recursive node. depth 0 = top-level; depth >= 1 = nested (subitem style). */
88
88
  function Node({ item, depth }) {
89
89
  const { activeHref, Link } = React.useContext(SidebarCtx);
90
- const { label, href = '#', icon, external, items } = item;
90
+ const { label, href = '#', icon, external, items, method } = item;
91
91
  const base = depth === 0 ? 'velu-sidebar__item' : 'velu-sidebar__subitem';
92
92
 
93
93
  if (items && items.length) {
@@ -125,7 +125,15 @@ function Node({ item, depth }) {
125
125
  aria-current={active ? 'page' : undefined}
126
126
  {...linkProps}
127
127
  >
128
- <Icon icon={icon} />
128
+ {method ? (
129
+ <span
130
+ className={`velu-sidebar__method velu-method-badge--${String(method).toLowerCase()}`}
131
+ >
132
+ {method}
133
+ </span>
134
+ ) : (
135
+ <Icon icon={icon} />
136
+ )}
129
137
  <span className="velu-sidebar__label">{label}</span>
130
138
  {external ? <ExternalIcon /> : null}
131
139
  </LinkTag>
@@ -21,6 +21,7 @@ export default function TryItBar({
21
21
  path = '',
22
22
  cta = 'Try It',
23
23
  onTry,
24
+ loading = false,
24
25
  className = '',
25
26
  ...rest
26
27
  }) {
@@ -79,10 +80,21 @@ export default function TryItBar({
79
80
  type="button"
80
81
  className="velu-try-it__cta"
81
82
  onClick={onTry}
83
+ data-loading={loading ? 'true' : undefined}
84
+ disabled={loading || undefined}
85
+ aria-busy={loading || undefined}
82
86
  >
83
- {cta}
84
- <span className="velu-try-it__cta-icon" aria-hidden="true">
85
- {resolveIcon('chevron-right', { size: '1em' })}
87
+ {/* Two stacked layers swap with a vertical slide: the label slides
88
+ down and out the bottom while the spinner slides in from the top.
89
+ overflow:hidden on the button clips both off-screen states. */}
90
+ <span className="velu-try-it__cta-label">
91
+ {cta}
92
+ <span className="velu-try-it__cta-icon" aria-hidden="true">
93
+ {resolveIcon('chevron-right', { size: '1em' })}
94
+ </span>
95
+ </span>
96
+ <span className="velu-try-it__cta-spinner" aria-hidden="true">
97
+ {resolveIcon('loader-circle', { size: '1em' })}
86
98
  </span>
87
99
  </button>
88
100
  </Cluster>
@@ -0,0 +1,215 @@
1
+ /* Auto-generated OpenAPI reference page. Composes existing API components; this
2
+ only adds the page-level rhythm, the response tabs, and the playground's
3
+ live-response panel + body input. All tokens — light/dark via [data-theme]. */
4
+
5
+ .velu-api-page__title {
6
+ font-size: var(--f-h1);
7
+ font-weight: var(--weight-bold);
8
+ line-height: 1.1;
9
+ letter-spacing: -0.02em;
10
+ margin: 0 0 var(--s1);
11
+ }
12
+ .velu-api-page__lede {
13
+ color: var(--muted-color);
14
+ margin: 0 0 var(--s2);
15
+ }
16
+ .velu-api-page__tryit {
17
+ margin-block: var(--s2);
18
+ }
19
+ .velu-api-page__section {
20
+ margin-block-start: var(--s3);
21
+ }
22
+ .velu-api-page__section > h2 {
23
+ font-size: var(--f-h3);
24
+ font-weight: var(--weight-semibold);
25
+ margin: 0 0 var(--s1);
26
+ }
27
+ .velu-api-page__enum {
28
+ margin-block-start: var(--s-3);
29
+ color: var(--muted-color);
30
+ font-size: var(--f-h6);
31
+ }
32
+ .velu-api-page__enum code {
33
+ margin-inline-end: var(--s-4);
34
+ }
35
+
36
+ /* ── Response sections (doc body) ──────────────────────────────────────── */
37
+ /* One default-open accordion per status; the summary shows a status pill +
38
+ the content-type. */
39
+ .velu-api-resp__head {
40
+ display: inline-flex;
41
+ align-items: baseline;
42
+ gap: var(--s0);
43
+ }
44
+ /* Plain status code — no pill chrome, no per-status color, regular weight. */
45
+ .velu-api-resp__status {
46
+ font-family: var(--font-mono);
47
+ }
48
+ .velu-api-resp__ctype {
49
+ color: var(--muted-color);
50
+ font-family: var(--font-mono);
51
+ font-size: var(--f-h6);
52
+ }
53
+ .velu-api-resp__desc {
54
+ color: var(--muted-color);
55
+ margin: 0 0 var(--s1);
56
+ }
57
+
58
+ /* ── Playground live-response panel (ApiClient aside) ──────────────────── */
59
+ /* A Body / Header tabbed card reusing the CodeGroup tab chrome
60
+ (.velu-code-block__*): Body shows a status-colored callout + the response
61
+ body; Header shows the response headers as a fixed-height scrollable table. */
62
+ .velu-api-resp-panel__content {
63
+ display: flex;
64
+ flex-direction: column;
65
+ gap: var(--s1);
66
+ padding: var(--s1);
67
+ background: var(--page-bg);
68
+ }
69
+ .velu-api-resp-panel__status {
70
+ /* The Callout owns its color; keep the weight regular. */
71
+ font-size: var(--f-h6);
72
+ }
73
+
74
+ /* Body + Header tabs — fixed height, vertically scrollable, so a long
75
+ response never makes the panel grow unbounded. */
76
+ .velu-api-resp-headers__scroll,
77
+ .velu-api-resp-body__scroll {
78
+ max-block-size: 15rem;
79
+ overflow-y: auto;
80
+ }
81
+ /* The CodeBlock owns its own border/radius; let it fill the scroll box. */
82
+ .velu-api-resp-body__scroll > .velu-code-block {
83
+ border: 0;
84
+ border-radius: 0;
85
+ }
86
+
87
+ /* Response headers table — flush rows with hairline dividers. */
88
+ .velu-api-resp-headers {
89
+ inline-size: 100%;
90
+ border-collapse: collapse;
91
+ font-size: var(--f-h6);
92
+ }
93
+ .velu-api-resp-headers tr + tr td {
94
+ border-block-start: var(--border-width) solid var(--border-color);
95
+ }
96
+ .velu-api-resp-headers td {
97
+ padding: var(--s-2) var(--s-1);
98
+ vertical-align: top;
99
+ word-break: break-word;
100
+ }
101
+ .velu-api-resp-headers__key {
102
+ color: var(--muted-color);
103
+ font-family: var(--font-mono);
104
+ white-space: nowrap;
105
+ padding-inline-end: var(--s1) !important;
106
+ }
107
+ .velu-api-resp-headers__val {
108
+ font-family: var(--font-mono);
109
+ inline-size: 100%;
110
+ }
111
+
112
+ /* Editable JSON body in the playground. */
113
+ .velu-api-body-input {
114
+ inline-size: 100%;
115
+ resize: vertical;
116
+ padding: var(--s0);
117
+ border: var(--border-width) solid var(--border-color);
118
+ border-radius: var(--radius-sm);
119
+ background: var(--page-bg);
120
+ color: var(--text-color);
121
+ font-family: var(--font-mono);
122
+ font-size: var(--f-h6);
123
+ line-height: 1.5;
124
+ }
125
+
126
+ /* ── Right-rail samples ────────────────────────────────────────────────── */
127
+ /* A TOC needs only ~280px, but the request/response code samples were being
128
+ cropped in that width. On API pages widen the right rail — both the aside
129
+ width AND the centre column's right margin read this one var, so they stay
130
+ in sync. (At < 1024px the @container query hides the rail + zeroes the
131
+ margin, so this wider value never affects narrow layouts.) */
132
+ .velu-docs-layout[data-api='true'] {
133
+ --velu-aside-right-width: 28rem; /* 448px */
134
+ }
135
+
136
+ .velu-api-samples {
137
+ display: flex;
138
+ flex-direction: column;
139
+ gap: var(--s2);
140
+ }
141
+
142
+ /* Playground right column: live response (when present) stacked above the
143
+ same request/response code samples shown in the page's right rail. */
144
+ .velu-api-pg-aside {
145
+ display: flex;
146
+ flex-direction: column;
147
+ gap: var(--s2);
148
+ }
149
+ /* Breathing room inside each sample group — the code panels were reading
150
+ as cramped against the rail edge. */
151
+ .velu-api-samples > * {
152
+ margin: 0;
153
+ }
154
+ .velu-api-samples :is(pre, code) {
155
+ font-size: var(--f-h6);
156
+ line-height: 1.6;
157
+ }
158
+
159
+ /* ── Try-It playground modal ───────────────────────────────────────────── */
160
+ /* The playground renders above the page on a dimmed, blurred backdrop
161
+ rather than inline. */
162
+ .velu-api-modal {
163
+ position: fixed;
164
+ inset: 0;
165
+ z-index: 80;
166
+ display: flex;
167
+ align-items: flex-start;
168
+ justify-content: center;
169
+ padding: clamp(var(--s1), 6vh, var(--s5)) var(--s2);
170
+ overflow-y: auto;
171
+ background: color-mix(in srgb, var(--page-bg) 35%, rgba(0, 0, 0, 0.55));
172
+ backdrop-filter: blur(6px) saturate(1.1);
173
+ -webkit-backdrop-filter: blur(6px) saturate(1.1);
174
+ animation: velu-api-modal-in 140ms ease-out;
175
+ }
176
+ .velu-api-modal__panel {
177
+ inline-size: 100%;
178
+ max-inline-size: 64rem;
179
+ border-radius: var(--radius-md);
180
+ box-shadow:
181
+ 0 24px 64px -16px rgba(0, 0, 0, 0.45),
182
+ 0 8px 24px -12px rgba(0, 0, 0, 0.35);
183
+ animation: velu-api-modal-rise 160ms cubic-bezier(0.2, 0.8, 0.2, 1);
184
+ }
185
+ @keyframes velu-api-modal-in {
186
+ from { opacity: 0; }
187
+ to { opacity: 1; }
188
+ }
189
+ @keyframes velu-api-modal-rise {
190
+ from { opacity: 0; transform: translateY(8px) scale(0.99); }
191
+ to { opacity: 1; transform: none; }
192
+ }
193
+ @media (prefers-reduced-motion: reduce) {
194
+ .velu-api-modal,
195
+ .velu-api-modal__panel {
196
+ animation: none;
197
+ }
198
+ }
199
+
200
+ /* Compact method pill in the sidebar (replaces the icon for API endpoints). */
201
+ .velu-sidebar__method {
202
+ flex: none;
203
+ font-family: var(--font-mono);
204
+ font-size: 0.6rem;
205
+ font-weight: var(--weight-bold);
206
+ letter-spacing: 0.03em;
207
+ padding: 0.18em 0.36em;
208
+ border-radius: var(--radius-xs, 4px);
209
+ text-transform: uppercase;
210
+ }
211
+ .velu-sidebar__method.velu-method-badge--get { color: var(--get-pill-color); background: color-mix(in srgb, var(--get-pill-color) 14%, transparent); }
212
+ .velu-sidebar__method.velu-method-badge--post { color: var(--post-pill-color); background: color-mix(in srgb, var(--post-pill-color) 14%, transparent); }
213
+ .velu-sidebar__method.velu-method-badge--put { color: var(--put-pill-color); background: color-mix(in srgb, var(--put-pill-color) 14%, transparent); }
214
+ .velu-sidebar__method.velu-method-badge--patch { color: var(--patch-pill-color); background: color-mix(in srgb, var(--patch-pill-color) 14%, transparent); }
215
+ .velu-sidebar__method.velu-method-badge--delete { color: var(--delete-pill-color); background: color-mix(in srgb, var(--delete-pill-color) 14%, transparent); }