@veluai/velu 0.1.15 → 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.
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import resolveIcon from '../lib/resolveIcon.jsx';
3
+
4
+ /**
5
+ * NotFound — the 404 page ("Direction 1 · Classic" from the design):
6
+ * a big accent-colored numeral, a short message, and a three-action row
7
+ * (Back to Home / Search the docs / Ask AI). Centered; the host layout
8
+ * supplies the header + footer. Pure tokens, so light/dark follow
9
+ * [data-theme] automatically.
10
+ *
11
+ * @param {{
12
+ * homeHref?: string,
13
+ * linkComponent?: React.ElementType,
14
+ * onSearch?: () => void,
15
+ * onAskAI?: () => void,
16
+ * eyebrow?: string, title?: string, body?: string,
17
+ * }} props
18
+ */
19
+ export default function NotFound({
20
+ homeHref = '/',
21
+ linkComponent = 'a',
22
+ onSearch,
23
+ onAskAI,
24
+ eyebrow = 'Error 404',
25
+ title = 'This page wandered off',
26
+ body = 'The page you’re looking for doesn’t exist or may have moved. Pick up the trail below.',
27
+ }) {
28
+ const Link = linkComponent;
29
+ return (
30
+ <section className="velu-404">
31
+ <p className="velu-404__eyebrow">{eyebrow}</p>
32
+ <div className="velu-404__num" aria-hidden="true">
33
+ 404
34
+ </div>
35
+ <h1 className="velu-404__title">{title}</h1>
36
+ <p className="velu-404__body">{body}</p>
37
+ <div className="velu-404__actions">
38
+ <Link className="velu-404__btn velu-404__btn--primary" href={homeHref}>
39
+ <span className="velu-404__btn-ic" aria-hidden="true">
40
+ {resolveIcon('house', { size: '1em' })}
41
+ </span>
42
+ Back to Home
43
+ </Link>
44
+ {onSearch && (
45
+ <button type="button" className="velu-404__btn" onClick={onSearch}>
46
+ <span className="velu-404__btn-ic" aria-hidden="true">
47
+ {resolveIcon('search', { size: '1em' })}
48
+ </span>
49
+ Search the docs
50
+ </button>
51
+ )}
52
+ {onAskAI && (
53
+ <button type="button" className="velu-404__btn" onClick={onAskAI}>
54
+ <span className="velu-404__btn-ic" aria-hidden="true">
55
+ {resolveIcon('sparkles', { size: '1em' })}
56
+ </span>
57
+ Ask AI
58
+ </button>
59
+ )}
60
+ </div>
61
+ </section>
62
+ );
63
+ }
@@ -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,208 @@
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
+ /* Breathing room inside each sample group — the code panels were reading
143
+ as cramped against the rail edge. */
144
+ .velu-api-samples > * {
145
+ margin: 0;
146
+ }
147
+ .velu-api-samples :is(pre, code) {
148
+ font-size: var(--f-h6);
149
+ line-height: 1.6;
150
+ }
151
+
152
+ /* ── Try-It playground modal ───────────────────────────────────────────── */
153
+ /* The playground renders above the page on a dimmed, blurred backdrop
154
+ rather than inline. */
155
+ .velu-api-modal {
156
+ position: fixed;
157
+ inset: 0;
158
+ z-index: 80;
159
+ display: flex;
160
+ align-items: flex-start;
161
+ justify-content: center;
162
+ padding: clamp(var(--s1), 6vh, var(--s5)) var(--s2);
163
+ overflow-y: auto;
164
+ background: color-mix(in srgb, var(--page-bg) 35%, rgba(0, 0, 0, 0.55));
165
+ backdrop-filter: blur(6px) saturate(1.1);
166
+ -webkit-backdrop-filter: blur(6px) saturate(1.1);
167
+ animation: velu-api-modal-in 140ms ease-out;
168
+ }
169
+ .velu-api-modal__panel {
170
+ inline-size: 100%;
171
+ max-inline-size: 64rem;
172
+ border-radius: var(--radius-md);
173
+ box-shadow:
174
+ 0 24px 64px -16px rgba(0, 0, 0, 0.45),
175
+ 0 8px 24px -12px rgba(0, 0, 0, 0.35);
176
+ animation: velu-api-modal-rise 160ms cubic-bezier(0.2, 0.8, 0.2, 1);
177
+ }
178
+ @keyframes velu-api-modal-in {
179
+ from { opacity: 0; }
180
+ to { opacity: 1; }
181
+ }
182
+ @keyframes velu-api-modal-rise {
183
+ from { opacity: 0; transform: translateY(8px) scale(0.99); }
184
+ to { opacity: 1; transform: none; }
185
+ }
186
+ @media (prefers-reduced-motion: reduce) {
187
+ .velu-api-modal,
188
+ .velu-api-modal__panel {
189
+ animation: none;
190
+ }
191
+ }
192
+
193
+ /* Compact method pill in the sidebar (replaces the icon for API endpoints). */
194
+ .velu-sidebar__method {
195
+ flex: none;
196
+ font-family: var(--font-mono);
197
+ font-size: 0.6rem;
198
+ font-weight: var(--weight-bold);
199
+ letter-spacing: 0.03em;
200
+ padding: 0.18em 0.36em;
201
+ border-radius: var(--radius-xs, 4px);
202
+ text-transform: uppercase;
203
+ }
204
+ .velu-sidebar__method.velu-method-badge--get { color: var(--get-pill-color); background: color-mix(in srgb, var(--get-pill-color) 14%, transparent); }
205
+ .velu-sidebar__method.velu-method-badge--post { color: var(--post-pill-color); background: color-mix(in srgb, var(--post-pill-color) 14%, transparent); }
206
+ .velu-sidebar__method.velu-method-badge--put { color: var(--put-pill-color); background: color-mix(in srgb, var(--put-pill-color) 14%, transparent); }
207
+ .velu-sidebar__method.velu-method-badge--patch { color: var(--patch-pill-color); background: color-mix(in srgb, var(--patch-pill-color) 14%, transparent); }
208
+ .velu-sidebar__method.velu-method-badge--delete { color: var(--delete-pill-color); background: color-mix(in srgb, var(--delete-pill-color) 14%, transparent); }
@@ -175,9 +175,11 @@
175
175
  color: var(--text-color);
176
176
  }
177
177
  .velu-try-it__cta {
178
+ position: relative;
179
+ overflow: hidden;
178
180
  display: inline-flex;
179
181
  align-items: center;
180
- gap: var(--s-2);
182
+ justify-content: center;
181
183
  padding-block: var(--s-3);
182
184
  padding-inline: var(--s0);
183
185
  background: var(--method-cta, var(--accent-color));
@@ -194,6 +196,56 @@
194
196
  .velu-try-it__cta:hover {
195
197
  background: color-mix(in srgb, var(--method-cta, var(--accent-color)) 88%, #000);
196
198
  }
199
+ .velu-try-it__cta:disabled {
200
+ cursor: default;
201
+ }
202
+
203
+ /* Label (text + chevron) and the loading spinner are two stacked layers
204
+ that swap with a vertical slide: when loading, the label slides DOWN and
205
+ out the bottom while the spinner slides IN from the top. overflow:hidden
206
+ on the button clips the off-screen states. The label stays in normal flow
207
+ so it fixes the button's width; the spinner overlays it. */
208
+ .velu-try-it__cta-label {
209
+ display: inline-flex;
210
+ align-items: center;
211
+ gap: var(--s-2);
212
+ transition: transform 0.22s ease, opacity 0.18s ease;
213
+ }
214
+ .velu-try-it__cta-spinner {
215
+ position: absolute;
216
+ inset: 0;
217
+ display: inline-flex;
218
+ align-items: center;
219
+ justify-content: center;
220
+ transform: translateY(-110%);
221
+ opacity: 0;
222
+ transition: transform 0.22s ease, opacity 0.18s ease;
223
+ }
224
+ .velu-try-it__cta[data-loading='true'] .velu-try-it__cta-label {
225
+ transform: translateY(120%);
226
+ opacity: 0;
227
+ }
228
+ .velu-try-it__cta[data-loading='true'] .velu-try-it__cta-spinner {
229
+ transform: translateY(0);
230
+ opacity: 1;
231
+ }
232
+ .velu-try-it__cta-spinner svg {
233
+ animation: velu-cta-spin 0.7s linear infinite;
234
+ }
235
+ @keyframes velu-cta-spin {
236
+ to {
237
+ transform: rotate(360deg);
238
+ }
239
+ }
240
+ @media (prefers-reduced-motion: reduce) {
241
+ .velu-try-it__cta-label,
242
+ .velu-try-it__cta-spinner {
243
+ transition-duration: 0.01ms;
244
+ }
245
+ .velu-try-it__cta-spinner svg {
246
+ animation-duration: 1.4s;
247
+ }
248
+ }
197
249
  .velu-try-it--get { --method-cta: var(--get-cta-color); }
198
250
  .velu-try-it--post { --method-cta: var(--post-cta-color); }
199
251
  .velu-try-it--put { --method-cta: var(--put-cta-color); }
@@ -213,6 +265,9 @@
213
265
  overflow: hidden;
214
266
  max-inline-size: 100%;
215
267
  min-inline-size: 0;
268
+ /* Own query container so the body grid reflows to the client's width
269
+ (it lives in a modal, outside the docs layout container). */
270
+ container: api-client / inline-size;
216
271
  }
217
272
 
218
273
  /* Modal close button — sits in its own row at the top of the client,
@@ -262,6 +317,7 @@
262
317
  AND natural height; the Cluster's align: flex-start keeps it from
263
318
  stretching to the taller TryItBar's height). */
264
319
  .velu-api-client__op {
320
+ position: relative;
265
321
  display: flex;
266
322
  align-items: center;
267
323
  gap: var(--s-2);
@@ -274,6 +330,20 @@
274
330
  line-height: var(--lh-h6);
275
331
  flex: none;
276
332
  }
333
+ /* The clickable trigger when the operation is switchable — a bare button
334
+ that keeps the same layout as the static row. */
335
+ .velu-api-client__op-btn {
336
+ display: inline-flex;
337
+ align-items: center;
338
+ gap: var(--s-2);
339
+ margin: 0;
340
+ padding: 0;
341
+ background: transparent;
342
+ border: 0;
343
+ color: inherit;
344
+ font: inherit;
345
+ cursor: pointer;
346
+ }
277
347
  .velu-api-client__op-label {
278
348
  font-weight: var(--weight-medium);
279
349
  }
@@ -281,6 +351,55 @@
281
351
  display: inline-flex;
282
352
  align-items: center;
283
353
  color: var(--muted-color);
354
+ transition: transform 0.15s ease;
355
+ }
356
+ .velu-api-client__op-btn[aria-expanded='true'] .velu-api-client__op-chevron {
357
+ transform: rotate(180deg);
358
+ }
359
+
360
+ /* Operation switcher menu — lists every operation in the tab. */
361
+ .velu-api-client__op-menu {
362
+ position: absolute;
363
+ inset-block-start: calc(100% + var(--s-3));
364
+ inset-inline-start: 0;
365
+ z-index: 5;
366
+ display: flex;
367
+ flex-direction: column;
368
+ gap: 2px;
369
+ margin: 0;
370
+ padding: var(--s-3);
371
+ list-style: none;
372
+ min-inline-size: 16rem;
373
+ max-block-size: 18rem;
374
+ overflow-y: auto;
375
+ background: var(--page-bg);
376
+ border: var(--border-width) solid var(--border-color);
377
+ border-radius: var(--radius-md);
378
+ box-shadow: 0 12px 32px -12px rgba(0, 0, 0, 0.4);
379
+ }
380
+ .velu-api-client__op-item {
381
+ display: flex;
382
+ align-items: center;
383
+ gap: var(--s-2);
384
+ inline-size: 100%;
385
+ padding: var(--s-3) var(--s-2);
386
+ background: transparent;
387
+ border: 0;
388
+ border-radius: var(--radius-sm);
389
+ color: var(--text-color);
390
+ font: inherit;
391
+ font-size: var(--f-h6);
392
+ text-align: start;
393
+ cursor: pointer;
394
+ }
395
+ .velu-api-client__op-item:hover,
396
+ .velu-api-client__op-item--active {
397
+ background: var(--surface-color);
398
+ }
399
+ .velu-api-client__op-item-label {
400
+ white-space: nowrap;
401
+ overflow: hidden;
402
+ text-overflow: ellipsis;
284
403
  }
285
404
 
286
405
  /* Inside ApiClient the outer frame already supplies the box — strip
@@ -297,25 +416,62 @@
297
416
  background: transparent;
298
417
  }
299
418
 
300
- /* Two-column body. Falls back to single column on narrow widths via
301
- a Switcher-style flex-wrap trick: flex-basis with a calc() that
302
- flips when the container drops below the threshold. */
419
+ /* Body grid with named areas so the three slots form / live response /
420
+ code samples can be placed AND reordered independently.
421
+ Wide (the client is its own query container, set on .velu-api-client):
422
+ form fills the left column; response sits top-right with the samples
423
+ below it. When there's no response yet, the samples take the whole right
424
+ column.
425
+ Narrow (single column): the live response moves ABOVE the form so a Send
426
+ result is visible without scrolling past the inputs; form then samples
427
+ follow. */
303
428
  .velu-api-client__body {
304
- display: flex;
305
- flex-wrap: wrap;
429
+ display: grid;
430
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
431
+ grid-template-areas: 'form aside';
432
+ align-items: start;
306
433
  gap: var(--s2);
307
434
  padding: var(--s2);
308
435
  }
309
- .velu-api-client__left,
436
+ .velu-api-client__body[data-has-response='true'] {
437
+ grid-template-areas:
438
+ 'form response'
439
+ 'form aside';
440
+ }
441
+ .velu-api-client__left {
442
+ grid-area: form;
443
+ }
444
+ .velu-api-client__response {
445
+ grid-area: response;
446
+ }
310
447
  .velu-api-client__right {
311
- flex-grow: 1;
312
- flex-basis: calc((40rem - 100%) * 999);
448
+ grid-area: aside;
449
+ }
450
+ .velu-api-client__left,
451
+ .velu-api-client__right,
452
+ .velu-api-client__response {
313
453
  display: flex;
314
454
  flex-direction: column;
315
455
  gap: var(--s1);
316
456
  min-inline-size: 0;
317
457
  }
318
458
 
459
+ @container api-client (max-width: 40rem) {
460
+ .velu-api-client__body,
461
+ .velu-api-client__body[data-has-response='true'] {
462
+ grid-template-columns: 1fr;
463
+ grid-template-areas:
464
+ 'form'
465
+ 'aside';
466
+ }
467
+ .velu-api-client__body[data-has-response='true'] {
468
+ grid-template-areas:
469
+ 'response'
470
+ 'form'
471
+ 'aside';
472
+ }
473
+ }
474
+
319
475
  .velu-api-client__title {
320
476
  margin: 0;
321
477
  font-size: var(--f-h3);