@veluai/velu 0.1.14 → 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,90 @@
1
+ import React from 'react';
2
+ import Cluster from '../primitives/Cluster.jsx';
3
+
4
+ /**
5
+ * SocialLinks — a row of social-platform icon links. Shared by the full
6
+ * PageFooter (centered at its base) and the compact content-foot row (next to
7
+ * "Powered by Velu" when there are no footer link columns).
8
+ *
9
+ * `socials` is `[{ kind, href }]`. Unknown kinds (no icon) are skipped.
10
+ *
11
+ * @param {{ socials?: {kind: string, href: string}[],
12
+ * justify?: string, className?: string }} props
13
+ */
14
+
15
+ /* Inline SVGs, fill: currentColor so they inherit the themed text color. */
16
+ function GithubIcon() {
17
+ return (
18
+ <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
19
+ <path
20
+ fillRule="evenodd"
21
+ clipRule="evenodd"
22
+ d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z"
23
+ />
24
+ </svg>
25
+ );
26
+ }
27
+ function XIcon() {
28
+ return (
29
+ <svg viewBox="0 0 1200 1227" fill="currentColor" aria-hidden="true">
30
+ <path d="M714.163 519.284 1160.89 0h-105.86L667.137 450.887 357.328 0H0l468.492 681.821L0 1226.37h105.866l409.625-476.152 327.181 476.152H1200L714.137 519.284h.026ZM569.165 687.828l-47.468-67.894-377.686-540.24h162.604l304.797 435.991 47.468 67.894 396.2 566.721H892.476L569.165 687.854v-.026Z" />
31
+ </svg>
32
+ );
33
+ }
34
+ function YoutubeIcon() {
35
+ return (
36
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
37
+ <path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
38
+ </svg>
39
+ );
40
+ }
41
+ function LinkedinIcon() {
42
+ return (
43
+ <svg viewBox="0 0 256 256" fill="currentColor" aria-hidden="true">
44
+ <path d="M218.123 218.127h-37.931v-59.403c0-14.165-.253-32.4-19.728-32.4-19.756 0-22.779 15.434-22.779 31.369v60.43h-37.93V95.967h36.413v16.694h.51a39.907 39.907 0 0 1 35.928-19.733c38.445 0 45.533 25.288 45.533 58.186l-.016 67.013ZM56.955 79.27c-12.157.002-22.014-9.852-22.016-22.009-.002-12.157 9.851-22.014 22.008-22.016 12.157-.003 22.014 9.851 22.016 22.008A22.013 22.013 0 0 1 56.955 79.27m18.966 138.858H37.95V95.967h37.97v122.16ZM237.033.018H18.89C8.58-.098.125 8.161-.001 18.471v219.053c.122 10.315 8.576 18.582 18.89 18.474h218.144c10.336.128 18.823-8.139 18.966-18.474V18.454c-.147-10.33-8.635-18.588-18.966-18.453" />
45
+ </svg>
46
+ );
47
+ }
48
+
49
+ export const SOCIAL_ICONS = {
50
+ github: GithubIcon,
51
+ x: XIcon,
52
+ youtube: YoutubeIcon,
53
+ linkedin: LinkedinIcon,
54
+ };
55
+ const SOCIAL_LABELS = {
56
+ github: 'GitHub',
57
+ x: 'X',
58
+ youtube: 'YouTube',
59
+ linkedin: 'LinkedIn',
60
+ };
61
+
62
+ export default function SocialLinks({ socials = [], justify, className = '' }) {
63
+ const known = socials.filter((s) => s && SOCIAL_ICONS[s.kind]);
64
+ if (!known.length) return null;
65
+ return (
66
+ <Cluster
67
+ space="var(--s-2)"
68
+ align="center"
69
+ justify={justify}
70
+ className={`velu-socials ${className}`.trim()}
71
+ >
72
+ {known.map((s, i) => {
73
+ const Icon = SOCIAL_ICONS[s.kind];
74
+ return (
75
+ <a
76
+ key={i}
77
+ href={s.href}
78
+ target="_blank"
79
+ rel="noreferrer"
80
+ aria-label={SOCIAL_LABELS[s.kind] ?? s.kind}
81
+ className="velu-social"
82
+ data-kind={s.kind}
83
+ >
84
+ <Icon />
85
+ </a>
86
+ );
87
+ })}
88
+ </Cluster>
89
+ );
90
+ }
@@ -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); }
@@ -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); }
@@ -262,6 +314,7 @@
262
314
  AND natural height; the Cluster's align: flex-start keeps it from
263
315
  stretching to the taller TryItBar's height). */
264
316
  .velu-api-client__op {
317
+ position: relative;
265
318
  display: flex;
266
319
  align-items: center;
267
320
  gap: var(--s-2);
@@ -274,6 +327,20 @@
274
327
  line-height: var(--lh-h6);
275
328
  flex: none;
276
329
  }
330
+ /* The clickable trigger when the operation is switchable — a bare button
331
+ that keeps the same layout as the static row. */
332
+ .velu-api-client__op-btn {
333
+ display: inline-flex;
334
+ align-items: center;
335
+ gap: var(--s-2);
336
+ margin: 0;
337
+ padding: 0;
338
+ background: transparent;
339
+ border: 0;
340
+ color: inherit;
341
+ font: inherit;
342
+ cursor: pointer;
343
+ }
277
344
  .velu-api-client__op-label {
278
345
  font-weight: var(--weight-medium);
279
346
  }
@@ -281,6 +348,55 @@
281
348
  display: inline-flex;
282
349
  align-items: center;
283
350
  color: var(--muted-color);
351
+ transition: transform 0.15s ease;
352
+ }
353
+ .velu-api-client__op-btn[aria-expanded='true'] .velu-api-client__op-chevron {
354
+ transform: rotate(180deg);
355
+ }
356
+
357
+ /* Operation switcher menu — lists every operation in the tab. */
358
+ .velu-api-client__op-menu {
359
+ position: absolute;
360
+ inset-block-start: calc(100% + var(--s-3));
361
+ inset-inline-start: 0;
362
+ z-index: 5;
363
+ display: flex;
364
+ flex-direction: column;
365
+ gap: 2px;
366
+ margin: 0;
367
+ padding: var(--s-3);
368
+ list-style: none;
369
+ min-inline-size: 16rem;
370
+ max-block-size: 18rem;
371
+ overflow-y: auto;
372
+ background: var(--page-bg);
373
+ border: var(--border-width) solid var(--border-color);
374
+ border-radius: var(--radius-md);
375
+ box-shadow: 0 12px 32px -12px rgba(0, 0, 0, 0.4);
376
+ }
377
+ .velu-api-client__op-item {
378
+ display: flex;
379
+ align-items: center;
380
+ gap: var(--s-2);
381
+ inline-size: 100%;
382
+ padding: var(--s-3) var(--s-2);
383
+ background: transparent;
384
+ border: 0;
385
+ border-radius: var(--radius-sm);
386
+ color: var(--text-color);
387
+ font: inherit;
388
+ font-size: var(--f-h6);
389
+ text-align: start;
390
+ cursor: pointer;
391
+ }
392
+ .velu-api-client__op-item:hover,
393
+ .velu-api-client__op-item--active {
394
+ background: var(--surface-color);
395
+ }
396
+ .velu-api-client__op-item-label {
397
+ white-space: nowrap;
398
+ overflow: hidden;
399
+ text-overflow: ellipsis;
284
400
  }
285
401
 
286
402
  /* Inside ApiClient the outer frame already supplies the box — strip
@@ -100,7 +100,7 @@
100
100
  little breathing room remains around it. Each kind gets its OWN
101
101
  brand color (set via the data-kind attribute below). Monochrome
102
102
  brands (github, x) inherit --text-color so they theme correctly. */
103
- .velu-footer__social {
103
+ .velu-social {
104
104
  display: inline-flex;
105
105
  align-items: center;
106
106
  justify-content: center;
@@ -112,19 +112,19 @@
112
112
  text-decoration: none;
113
113
  transition: opacity 0.12s ease;
114
114
  }
115
- .velu-footer__social:hover {
115
+ .velu-social:hover {
116
116
  opacity: 0.78;
117
117
  }
118
- .velu-footer__social svg {
118
+ .velu-social svg {
119
119
  inline-size: var(--icon-size-lg);
120
120
  block-size: var(--icon-size-lg);
121
121
  }
122
122
  /* Brand colors — YouTube red, LinkedIn blue. GitHub + X stay
123
123
  --text-color (their canonical brand is monochrome and follows the
124
124
  surrounding theme). */
125
- .velu-footer__social[data-kind='youtube'] {
125
+ .velu-social[data-kind='youtube'] {
126
126
  color: #ff0000;
127
127
  }
128
- .velu-footer__social[data-kind='linkedin'] {
128
+ .velu-social[data-kind='linkedin'] {
129
129
  color: #0a66c2;
130
130
  }
@@ -100,6 +100,28 @@
100
100
  color: var(--text-color);
101
101
  }
102
102
 
103
+ /* Configured site logo (replaces the mark + wordmark). Height-constrained to
104
+ the brand row; width scales by aspect ratio up to a sane cap. */
105
+ .velu-logo {
106
+ display: block;
107
+ block-size: 2rem;
108
+ inline-size: auto;
109
+ max-inline-size: 12rem;
110
+ object-fit: contain;
111
+ }
112
+ /* Per-theme swap: light image by default, dark image under [data-theme=dark].
113
+ `display` (not opacity) so the inactive one leaves the a11y tree too. The
114
+ `mono` single-image case has neither modifier and always shows. */
115
+ .velu-logo--dark {
116
+ display: none;
117
+ }
118
+ [data-theme='dark'] .velu-logo--light {
119
+ display: none;
120
+ }
121
+ [data-theme='dark'] .velu-logo--dark {
122
+ display: block;
123
+ }
124
+
103
125
  /* ── Action buttons ─────────────────────────────────────────────────── */
104
126
  .velu-header__action {
105
127
  display: inline-flex;
@@ -136,6 +158,21 @@
136
158
  .velu-header__action--primary:hover {
137
159
  background: color-mix(in srgb, var(--accent-color) 88%, #000);
138
160
  }
161
+ /* Plain text link (navbar.links) — no fill or border, just text that goes
162
+ accent on hover. Transparent border keeps it the same height as the
163
+ buttons beside it; tighter inline padding reads as a link, not a button. */
164
+ .velu-header__action--link {
165
+ background: transparent;
166
+ border: var(--border-width) solid transparent;
167
+ color: var(--text-color);
168
+ padding-inline: var(--s-5);
169
+ }
170
+ .velu-header__action--link:hover {
171
+ color: var(--accent-color);
172
+ }
173
+ .velu-header__action--link:hover .velu-header__action-icon {
174
+ color: var(--accent-color);
175
+ }
139
176
  .velu-header__action-icon {
140
177
  display: inline-flex;
141
178
  flex: none;
@@ -1,6 +1,26 @@
1
1
  /* PoweredBy — "Powered by Velu" badge, right-aligned. Sits above the
2
2
  site footer. All values are tokens; light/dark via [data-theme]. */
3
3
 
4
+ /* Content foot — social icons (left) + the "Powered by" badge (right) in one
5
+ row. Shown at the base of the article column when the footer has no link
6
+ columns (so there's no full footer section). The row owns the vertical
7
+ rhythm; the nested badge drops its own standalone margins. */
8
+ .velu-content-foot {
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: space-between;
12
+ gap: var(--s2);
13
+ margin-block: 2rem;
14
+ padding-inline: var(--s3);
15
+ }
16
+ .velu-content-foot .velu-powered-by {
17
+ margin: 0;
18
+ padding: 0;
19
+ }
20
+ .velu-content-foot .velu-social {
21
+ color: var(--muted-color);
22
+ }
23
+
4
24
  .velu-powered-by {
5
25
  display: flex;
6
26
  justify-content: flex-end;
@@ -8,6 +8,8 @@ export { default as Toc } from './components/Toc.jsx';
8
8
  export { default as TocBar } from './components/TocBar.jsx';
9
9
  export { default as Callout } from './components/Callout.jsx';
10
10
  export { default as ErrorCard } from './components/ErrorCard.jsx';
11
+ export { default as Logo } from './components/Logo.jsx';
12
+ export { default as SocialLinks } from './components/SocialLinks.jsx';
11
13
  export {
12
14
  default as Accordion,
13
15
  AccordionGroup,
@@ -24,6 +26,8 @@ export { default as TryItBar } from './components/TryItBar.jsx';
24
26
  export { default as ApiField } from './components/ApiField.jsx';
25
27
  export { default as ApiClient } from './components/ApiClient.jsx';
26
28
  export { default as ApiSidebar } from './components/ApiSidebar.jsx';
29
+ export { default as ApiReferencePage } from './components/ApiReferencePage.jsx';
30
+ export { default as ApiSamples } from './components/ApiSamples.jsx';
27
31
  export { default as AskBar } from './components/AskBar.jsx';
28
32
  export { default as Chatbot } from './components/Chatbot.jsx';
29
33
  export { default as PageFeedback } from './components/PageFeedback.jsx';