@veluai/velu 0.1.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.
Files changed (90) hide show
  1. package/dist/cli.js +11 -0
  2. package/package.json +52 -0
  3. package/runtime/velu-ui/base.css +311 -0
  4. package/runtime/velu-ui/components/Accordion.jsx +64 -0
  5. package/runtime/velu-ui/components/ApiClient.jsx +121 -0
  6. package/runtime/velu-ui/components/ApiField.jsx +87 -0
  7. package/runtime/velu-ui/components/ApiPath.jsx +63 -0
  8. package/runtime/velu-ui/components/ApiSidebar.jsx +122 -0
  9. package/runtime/velu-ui/components/AskBar.jsx +71 -0
  10. package/runtime/velu-ui/components/Callout.jsx +114 -0
  11. package/runtime/velu-ui/components/Card.jsx +131 -0
  12. package/runtime/velu-ui/components/Chatbot.jsx +596 -0
  13. package/runtime/velu-ui/components/CodeBlock.jsx +375 -0
  14. package/runtime/velu-ui/components/Columns.jsx +56 -0
  15. package/runtime/velu-ui/components/Field.jsx +81 -0
  16. package/runtime/velu-ui/components/Image.jsx +163 -0
  17. package/runtime/velu-ui/components/MethodBadge.jsx +31 -0
  18. package/runtime/velu-ui/components/NavSelect.jsx +108 -0
  19. package/runtime/velu-ui/components/PageFeedback.jsx +219 -0
  20. package/runtime/velu-ui/components/PageFooter.jsx +213 -0
  21. package/runtime/velu-ui/components/PageHeader.jsx +414 -0
  22. package/runtime/velu-ui/components/PageNav.jsx +77 -0
  23. package/runtime/velu-ui/components/PoweredBy.jsx +51 -0
  24. package/runtime/velu-ui/components/Prompt.jsx +115 -0
  25. package/runtime/velu-ui/components/Search.jsx +366 -0
  26. package/runtime/velu-ui/components/Sidebar.jsx +191 -0
  27. package/runtime/velu-ui/components/Steps.jsx +65 -0
  28. package/runtime/velu-ui/components/ThemeToggle.jsx +48 -0
  29. package/runtime/velu-ui/components/Toc.jsx +537 -0
  30. package/runtime/velu-ui/components/TocBar.jsx +195 -0
  31. package/runtime/velu-ui/components/Tree.jsx +87 -0
  32. package/runtime/velu-ui/components/TryItBar.jsx +90 -0
  33. package/runtime/velu-ui/components/accordion.css +92 -0
  34. package/runtime/velu-ui/components/api.css +479 -0
  35. package/runtime/velu-ui/components/ask-bar.css +94 -0
  36. package/runtime/velu-ui/components/card.css +105 -0
  37. package/runtime/velu-ui/components/chatbot.css +617 -0
  38. package/runtime/velu-ui/components/code-block.css +263 -0
  39. package/runtime/velu-ui/components/docs-layout.css +775 -0
  40. package/runtime/velu-ui/components/field.css +82 -0
  41. package/runtime/velu-ui/components/image.css +237 -0
  42. package/runtime/velu-ui/components/nav-select.css +157 -0
  43. package/runtime/velu-ui/components/page-feedback.css +241 -0
  44. package/runtime/velu-ui/components/page-footer.css +130 -0
  45. package/runtime/velu-ui/components/page-header.css +520 -0
  46. package/runtime/velu-ui/components/page-nav.css +50 -0
  47. package/runtime/velu-ui/components/powered-by.css +66 -0
  48. package/runtime/velu-ui/components/prompt.css +99 -0
  49. package/runtime/velu-ui/components/search.css +307 -0
  50. package/runtime/velu-ui/components/sidebar.css +144 -0
  51. package/runtime/velu-ui/components/steps.css +77 -0
  52. package/runtime/velu-ui/components/theme-toggle.css +70 -0
  53. package/runtime/velu-ui/components/toc-bar.css +234 -0
  54. package/runtime/velu-ui/components/tree.css +49 -0
  55. package/runtime/velu-ui/index.js +45 -0
  56. package/runtime/velu-ui/lib/copyText.js +64 -0
  57. package/runtime/velu-ui/lib/lang-icons.jsx +156 -0
  58. package/runtime/velu-ui/lib/prism-langs.js +957 -0
  59. package/runtime/velu-ui/lib/prism-loader.js +74 -0
  60. package/runtime/velu-ui/lib/resolveIcon.jsx +29 -0
  61. package/runtime/velu-ui/lib/scrollIntoNearestView.js +66 -0
  62. package/runtime/velu-ui/mdx-components.jsx +85 -0
  63. package/runtime/velu-ui/primitives/Cluster.jsx +49 -0
  64. package/runtime/velu-ui/primitives/Stack.jsx +63 -0
  65. package/runtime/velu-ui/primitives/Switcher.jsx +57 -0
  66. package/runtime/velu-ui/primitives/stack.css +3 -0
  67. package/runtime/velu-ui/primitives/switcher.css +25 -0
  68. package/runtime/velu-ui/styles.css +43 -0
  69. package/runtime/velu-ui/tokens.css +4 -0
  70. package/schema/velu.schema.json +167 -0
  71. package/src/navigation.js +434 -0
  72. package/src/runtime/App.jsx +1473 -0
  73. package/src/runtime/client-entry.jsx +22 -0
  74. package/src/runtime/server-entry.jsx +16 -0
  75. package/src/template.html +48 -0
  76. package/templates/starter/ai-tools/claude-code.mdx +26 -0
  77. package/templates/starter/ai-tools/cursor.mdx +17 -0
  78. package/templates/starter/api-reference/endpoint/create.mdx +24 -0
  79. package/templates/starter/api-reference/endpoint/get.mdx +27 -0
  80. package/templates/starter/api-reference/introduction.mdx +28 -0
  81. package/templates/starter/development.mdx +19 -0
  82. package/templates/starter/essentials/code.mdx +28 -0
  83. package/templates/starter/essentials/images.mdx +29 -0
  84. package/templates/starter/essentials/markdown.mdx +25 -0
  85. package/templates/starter/essentials/navigation.mdx +39 -0
  86. package/templates/starter/essentials/settings.mdx +30 -0
  87. package/templates/starter/favicon.svg +6 -0
  88. package/templates/starter/index.mdx +31 -0
  89. package/templates/starter/quickstart.mdx +31 -0
  90. package/templates/starter/velu.json +33 -0
@@ -0,0 +1,775 @@
1
+ /* DocsLayout — the chrome of a Velu docs page (header, fixed left
2
+ sidebar, center column, fixed right TOC, footer). The interesting
3
+ bit lives in the @container rule at the bottom: at narrow widths
4
+ the right TOC is hidden and the center column expands, with no
5
+ media queries. Container query → reacts to the layout wrapper's
6
+ inline-size, not the viewport, so the same component works in any
7
+ surrounding shell (modal, split view, etc).
8
+
9
+ All values are tokens; light/dark via [data-theme]. */
10
+
11
+ .velu-docs-layout {
12
+ /* `inline-size` containment lets @container queries below react to
13
+ this element's width. `docs` is the named container so component
14
+ CSS elsewhere can target it specifically. */
15
+ container-type: inline-size;
16
+ container-name: docs;
17
+
18
+ /* Rail width — exposed as a custom property so the actual rail
19
+ element AND the centre-column margin that tracks it can reference
20
+ a single source of truth. Set OUTSIDE the @container block so the
21
+ value is unconditionally inherited by descendants (defining it
22
+ inside the container query was leaving it un-set for descendants
23
+ in some cascade edge cases, so the rail collapsed to 0 width). */
24
+ --velu-rail-width: 3.5rem; /* 56px */
25
+ /* Left sidebar column width — single source of truth so the aside
26
+ and the centre-column margin that reserves space for it stay in
27
+ sync. Wide enough that nav labels don't wrap given the left inset
28
+ gutter on the aside. */
29
+ --velu-sidebar-width: 18rem; /* 288px */
30
+ /* Right column reserve — the TOC aside width AND the centre's right
31
+ margin. Constant whether the TOC or the chatbot is shown, so the
32
+ article never shifts when the chatbot opens. The chatbot is wider
33
+ than this and simply slides OVER the right edge as a floating
34
+ panel (its own --vchat-width), rather than reflowing the article. */
35
+ --velu-aside-right-width: 17.5rem; /* 280px */
36
+
37
+ display: flex;
38
+ flex-direction: column;
39
+ min-height: 100vh;
40
+ background: var(--page-bg);
41
+ }
42
+
43
+ /* ── Asides (fixed left sidebar + right TOC) ───────────────────────── */
44
+ .velu-docs-layout__aside {
45
+ position: fixed;
46
+ /* Span full viewport height so the aside extends behind the
47
+ translucent header (its top shows through the header's frost on
48
+ scroll). Inner padding-top pushes the nav content down below the
49
+ header band; scroll-padding-top keeps auto-scroll out of it. */
50
+ inset-block-start: 0;
51
+ inset-block-end: 0;
52
+ overflow-y: auto;
53
+ /* Horizontal clip: the left aside animates its `inline-size` down
54
+ to a thin rail when collapsed, and its child nav (which keeps
55
+ its full intrinsic width) MUST be visually cropped to the new
56
+ box. `overflow-y: auto` + `overflow-x: clip` is allowed (clip
57
+ is the spec partner that works with a scrollable y-axis) and
58
+ gives reliable visual clipping that pure `overflow: hidden`
59
+ can interact badly with on a `position: fixed` element. */
60
+ overflow-x: clip;
61
+ overscroll-behavior: contain;
62
+ padding-block-start: calc(var(--velu-header-height) + var(--s3));
63
+ scroll-padding-block-start: var(--velu-header-height);
64
+ z-index: 10;
65
+ /* `inline-size` is animated for the narrow-width collapse
66
+ (240px ↔ rail). `transform` is animated for the mobile drawer
67
+ (translateX(-100%) ↔ 0). Both timings live on the same property
68
+ list so each animation runs only when its property actually
69
+ changes at the active breakpoint. */
70
+ transition: inline-size 0.2s ease, transform 0.25s ease;
71
+ }
72
+
73
+ .velu-docs-layout__aside--left {
74
+ inset-inline-start: 0;
75
+ inline-size: var(--velu-sidebar-width);
76
+ /* Left gutter so the nav sits inset from the viewport edge rather
77
+ than hugging it. (At mobile the @container block resets this to 0
78
+ — the drawer manages its own padding.) */
79
+ padding-inline-start: var(--s5);
80
+ padding-inline-end: 0;
81
+ /* The aside itself does NOT scroll — it's a flex column whose context
82
+ zone (anchors/switchers) stays pinned and whose nav region
83
+ (.velu-docs-nav-scroll) scrolls on its own. Overrides the base
84
+ aside's `overflow-y: auto`. */
85
+ display: flex;
86
+ flex-direction: column;
87
+ overflow: hidden;
88
+ /* Bottom boundary: end the column a fixed gap above the viewport
89
+ bottom (mirrors the pinned top), so the nav scrolls within a
90
+ contained panel rather than running to the screen edge. The value
91
+ is driven by --velu-aside-bottom (set inline in App.jsx) so the
92
+ edge lifts to stay above the footer as it scrolls in; falls back
93
+ to a static gap. The mobile drawer resets this to 0. */
94
+ inset-block-end: var(--velu-aside-bottom, var(--s4));
95
+ }
96
+ /* Stack wrapper fills the aside so the inner nav region can flex-grow
97
+ and own the scroll. */
98
+ .velu-docs-aside-stack {
99
+ flex: 1 1 auto;
100
+ min-block-size: 0;
101
+ }
102
+ /* Region wrapping the scroll area + the overlay arrows. Fills the
103
+ remaining aside height; relative so the arrows position to its
104
+ top/bottom edges. */
105
+ .velu-docs-nav-region {
106
+ position: relative;
107
+ flex: 1 1 auto;
108
+ min-block-size: 0;
109
+ display: flex;
110
+ flex-direction: column;
111
+ }
112
+ .velu-docs-nav-scroll {
113
+ flex: 1 1 auto;
114
+ min-block-size: 0;
115
+ overflow-y: auto;
116
+ }
117
+
118
+ /* Up / down scroll arrows — circular buttons centered on the top and
119
+ bottom edges of the scroll region. Hidden by default; revealed (via
120
+ the sibling combinator off the scroll div's data-fade-* attrs) only
121
+ when there's content beyond that edge. Clicking smooth-scrolls the
122
+ nav fully to that end (handlers in App.jsx). */
123
+ .velu-docs-nav-arrow {
124
+ position: absolute;
125
+ inset-inline-start: 50%;
126
+ display: inline-flex;
127
+ align-items: center;
128
+ justify-content: center;
129
+ inline-size: 2rem;
130
+ block-size: 2rem;
131
+ padding: 0;
132
+ border: var(--border-width) solid var(--border-color);
133
+ border-radius: 999px;
134
+ background: var(--page-bg);
135
+ color: var(--muted-color);
136
+ cursor: pointer;
137
+ z-index: 3;
138
+ opacity: 0;
139
+ pointer-events: none;
140
+ transform: translateX(-50%) scale(0.9);
141
+ transition: opacity 0.15s ease, transform 0.15s ease, color 0.12s ease,
142
+ border-color 0.12s ease;
143
+ box-shadow: 0 var(--s-4) var(--s-1) color-mix(in srgb, #000 10%, transparent);
144
+ }
145
+ .velu-docs-nav-arrow:hover {
146
+ color: var(--accent-color);
147
+ border-color: var(--accent-color);
148
+ }
149
+ .velu-docs-nav-arrow svg {
150
+ inline-size: 1.1em;
151
+ block-size: 1.1em;
152
+ }
153
+ .velu-docs-nav-arrow--up {
154
+ inset-block-start: var(--s-3);
155
+ }
156
+ .velu-docs-nav-arrow--down {
157
+ inset-block-end: var(--s-3);
158
+ }
159
+ /* Reveal only while hovering the sidebar AND there's content beyond
160
+ that edge. */
161
+ .velu-docs-layout__aside--left:hover
162
+ .velu-docs-nav-scroll[data-fade-top='true']
163
+ ~ .velu-docs-nav-arrow--up,
164
+ .velu-docs-layout__aside--left:hover
165
+ .velu-docs-nav-scroll[data-fade-bottom='true']
166
+ ~ .velu-docs-nav-arrow--down {
167
+ opacity: 1;
168
+ pointer-events: auto;
169
+ transform: translateX(-50%) scale(1);
170
+ }
171
+
172
+ /* Edge fade: the scroll regions (left nav + right TOC) fade to
173
+ transparent at whichever edge has more content beyond it. The
174
+ --vf-top / --vf-bottom lengths default to 0 (no fade) and are
175
+ switched on by the data-fade-* attrs that App.jsx sets from scroll
176
+ position — so the top only fades once you've scrolled down, and the
177
+ bottom fade vanishes when you reach the end. */
178
+ .velu-docs-nav-scroll,
179
+ .velu-docs-layout__aside--right {
180
+ --vf-top: 0px;
181
+ --vf-bottom: 0px;
182
+ -webkit-mask-image: linear-gradient(
183
+ to bottom,
184
+ transparent,
185
+ #000 var(--vf-top),
186
+ #000 calc(100% - var(--vf-bottom)),
187
+ transparent
188
+ );
189
+ mask-image: linear-gradient(
190
+ to bottom,
191
+ transparent,
192
+ #000 var(--vf-top),
193
+ #000 calc(100% - var(--vf-bottom)),
194
+ transparent
195
+ );
196
+ }
197
+ /* Top fade only on the right TOC — the left nav uses sticky section
198
+ headers as its top treatment, so a top fade would just blur the
199
+ pinned heading. */
200
+ .velu-docs-layout__aside--right[data-fade-top='true'] {
201
+ --vf-top: var(--s2);
202
+ }
203
+ /* The left nav's under-heading fade (the ::after below each sticky
204
+ section heading) is shown only while the nav is scrolled up, so a
205
+ section's first item isn't dimmed at rest. */
206
+ .velu-docs-nav-scroll[data-fade-top='true'] .velu-sidebar__section::after {
207
+ opacity: 1;
208
+ }
209
+ .velu-docs-nav-scroll[data-fade-bottom='true'],
210
+ .velu-docs-layout__aside--right[data-fade-bottom='true'] {
211
+ --vf-bottom: var(--s2);
212
+ }
213
+
214
+ .velu-docs-layout__aside--right {
215
+ inset-inline-end: 0;
216
+ inline-size: var(--velu-aside-right-width);
217
+ padding-inline: var(--s1);
218
+ }
219
+
220
+ /* Scrollbars hide at rest and reveal only while the cursor is over the
221
+ column. Applies to the right TOC aside (scrolls directly) and the
222
+ left nav's inner scroll region (.velu-docs-nav-scroll). The gutter
223
+ stays reserved (scrollbar-width: thin from .velu-hide-scrollbar) —
224
+ only the thumb's color toggles — so revealing it never shifts
225
+ content. Overrides .velu-hide-scrollbar's always-on thumb. */
226
+ .velu-docs-layout__aside--right,
227
+ .velu-docs-nav-scroll {
228
+ scrollbar-color: transparent transparent;
229
+ }
230
+ .velu-docs-layout__aside--right:hover,
231
+ .velu-docs-layout__aside--left:hover .velu-docs-nav-scroll {
232
+ scrollbar-color: var(--border-color) transparent;
233
+ }
234
+ .velu-docs-layout__aside--right::-webkit-scrollbar-thumb,
235
+ .velu-docs-nav-scroll::-webkit-scrollbar-thumb {
236
+ background: transparent;
237
+ }
238
+ .velu-docs-layout__aside--right:hover::-webkit-scrollbar-thumb,
239
+ .velu-docs-layout__aside--left:hover .velu-docs-nav-scroll::-webkit-scrollbar-thumb {
240
+ background: var(--border-color);
241
+ }
242
+ .velu-docs-layout__aside--right:hover::-webkit-scrollbar-thumb:hover,
243
+ .velu-docs-layout__aside--left:hover .velu-docs-nav-scroll::-webkit-scrollbar-thumb:hover {
244
+ background: var(--muted-color);
245
+ }
246
+
247
+ /* ── Center column ─────────────────────────────────────────────────── */
248
+ /* Reserves the aside widths via margin so the article never slides
249
+ under them. The end margin collapses to 0 while the chatbot is
250
+ open (the right TOC is hidden then, the chatbot uses that slot).
251
+ Both can be overridden by the container query below. */
252
+ .velu-docs-layout__center {
253
+ position: relative;
254
+ min-inline-size: 0;
255
+ margin-inline-start: var(--velu-sidebar-width);
256
+ /* Right reserve stays constant whether the right TOC or the chatbot
257
+ occupies it — so opening the chatbot does NOT shift the article.
258
+ The chatbot's desktop width matches this reserve (chatbot.css) so
259
+ it never overlaps the content. */
260
+ margin-inline-end: var(--velu-aside-right-width);
261
+ /* Tracks the sidebar's `inline-size` animation at narrow widths so
262
+ the article slides in sync with the collapse, no snap. Same 0.2s
263
+ timing as the aside's own `inline-size` transition. */
264
+ transition: margin-inline-start 0.2s ease;
265
+ }
266
+
267
+ /* <main> inside the centre column owns the article-area padding —
268
+ defined here (not inline) so the narrow + sidebar-closed @container
269
+ rule below can override just the inline-start side. */
270
+ .velu-docs-layout__main {
271
+ padding-block-start: var(--s3);
272
+ padding-inline: var(--s4);
273
+ }
274
+
275
+ /* Article wrapper inside <main> — caps the prose at 46rem and centres
276
+ it by default. Container query below switches it to left-aligned
277
+ when the sidebar is closed at narrow widths, so the article hugs
278
+ the freed space rather than centring inside it (which would leave
279
+ the perceived "ghost sidebar" on the left). */
280
+ .velu-docs-layout__article {
281
+ max-inline-size: 46rem;
282
+ margin-inline: auto;
283
+ }
284
+
285
+ /* Inner sidebar content (the <Sidebar> nav root, direct child of the
286
+ left aside) cross-fades during the collapse animation. Hidden when
287
+ the layout's `data-sidebar-open='false'` flag is set — see the
288
+ @container block below for the actual opacity:0 toggle, which is
289
+ scoped to narrow widths only (sidebar can't collapse at wide). */
290
+ .velu-docs-layout__aside--left > * {
291
+ transition: opacity 0.15s ease;
292
+ }
293
+
294
+ /* ── Scrim (mobile-only) ───────────────────────────────────────────── */
295
+ /* Backdrop layer behind the mobile drawer — dims + blurs the article
296
+ underneath and absorbs pointer events so nothing beneath is
297
+ clickable while the drawer is open. Always in the DOM so the
298
+ opacity + visibility transition runs in both directions; toggled
299
+ visible via [data-sidebar-open='true'] in the @container block
300
+ at the bottom. */
301
+ .velu-docs-layout__scrim {
302
+ /* Hidden until the mobile @container block flips display on. */
303
+ display: none;
304
+ position: fixed;
305
+ inset: 0;
306
+ z-index: 34;
307
+ background: color-mix(in srgb, #000 28%, transparent);
308
+ backdrop-filter: blur(4px);
309
+ -webkit-backdrop-filter: blur(4px);
310
+ /* Closed state — invisible + non-interactive. The
311
+ [data-sidebar-open='true'] rule below flips both back on. */
312
+ opacity: 0;
313
+ pointer-events: none;
314
+ visibility: hidden;
315
+ transition:
316
+ opacity 0.25s ease,
317
+ visibility 0s linear 0.25s;
318
+ cursor: pointer;
319
+ }
320
+
321
+ /* ── Drawer chrome (mobile-only) ───────────────────────────────────── */
322
+ /* Drawer-only elements (brand bar + docs-set selector) — hidden at
323
+ wide / narrow widths; the @container block at the bottom flips them
324
+ on at mobile. Defined here (above the @container blocks) so source
325
+ order works in the cascade's favor. */
326
+ .velu-docs-layout__drawer-head,
327
+ .velu-docs-layout__drawer-docselect {
328
+ display: none;
329
+ }
330
+ .velu-docs-layout__drawer-brand {
331
+ display: inline-flex;
332
+ align-items: center;
333
+ gap: var(--s-3);
334
+ color: inherit;
335
+ text-decoration: none;
336
+ }
337
+ .velu-docs-layout__drawer-brand .velu-header__mark {
338
+ inline-size: 2em;
339
+ block-size: 1.5em;
340
+ color: var(--accent-color);
341
+ }
342
+ .velu-docs-layout__drawer-close {
343
+ display: inline-flex;
344
+ align-items: center;
345
+ justify-content: center;
346
+ inline-size: 1.5rem;
347
+ block-size: 1.5rem;
348
+ padding: 0;
349
+ background: transparent;
350
+ border: 0;
351
+ color: var(--text-color);
352
+ cursor: pointer;
353
+ }
354
+ .velu-docs-layout__drawer-close svg {
355
+ inline-size: 1.5rem;
356
+ block-size: 1.5rem;
357
+ stroke-width: 1;
358
+ }
359
+ .velu-docs-layout__drawer-docselect {
360
+ position: relative;
361
+ }
362
+ .velu-docs-layout__drawer-docselect-btn {
363
+ display: flex;
364
+ align-items: center;
365
+ justify-content: space-between;
366
+ inline-size: 100%;
367
+ /* 8px top/bottom — matches the modular-scale step --s-3 (~0.5rem). */
368
+ padding-block: var(--s-3);
369
+ padding-inline: var(--s0);
370
+ background: var(--page-bg);
371
+ border: var(--border-width) solid var(--border-color);
372
+ border-radius: var(--radius-sm);
373
+ font: inherit;
374
+ font-size: var(--f-h5);
375
+ color: var(--text-color);
376
+ text-align: start;
377
+ cursor: pointer;
378
+ transition: border-color 0.12s ease;
379
+ }
380
+ .velu-docs-layout__drawer-docselect-btn:hover {
381
+ border-color: var(--accent-color);
382
+ }
383
+ .velu-docs-layout__drawer-docselect-label {
384
+ flex: 1;
385
+ min-inline-size: 0;
386
+ overflow: hidden;
387
+ text-overflow: ellipsis;
388
+ white-space: nowrap;
389
+ }
390
+ .velu-docs-layout__drawer-docselect-chev {
391
+ inline-size: 1em;
392
+ block-size: 1em;
393
+ stroke-width: 1;
394
+ /* Match the label color (not the muted variant the kebab/search
395
+ icons use) — the chevron reads as part of the trigger text, not
396
+ a secondary indicator. */
397
+ color: var(--text-color);
398
+ stroke: currentColor;
399
+ flex: none;
400
+ transition: transform 0.18s ease;
401
+ }
402
+ .velu-docs-layout__drawer-docselect[data-open='true']
403
+ .velu-docs-layout__drawer-docselect-chev {
404
+ transform: rotate(180deg);
405
+ }
406
+
407
+ /* Menu — anchored below the trigger, full width of the wrapper.
408
+ Always in the DOM so the open/close transition can run in both
409
+ directions; visibility delayed so the closed menu isn't focusable
410
+ or click-able. */
411
+ .velu-docs-layout__drawer-docselect-menu {
412
+ position: absolute;
413
+ inset-block-start: calc(100% + var(--s-3));
414
+ inset-inline-start: 0;
415
+ inset-inline-end: 0;
416
+ margin: 0;
417
+ padding-block: var(--s-2);
418
+ padding-inline: 0;
419
+ list-style: none;
420
+ background: var(--page-bg);
421
+ border: var(--border-width) solid var(--border-color);
422
+ border-radius: var(--radius-sm);
423
+ box-shadow: 0 var(--s-2) var(--s1)
424
+ color-mix(in srgb, #000 12%, transparent);
425
+ opacity: 0;
426
+ visibility: hidden;
427
+ transform: translateY(-0.25rem);
428
+ pointer-events: none;
429
+ transition:
430
+ opacity 0.15s ease,
431
+ transform 0.15s ease,
432
+ visibility 0s linear 0.15s;
433
+ z-index: 1;
434
+ }
435
+ .velu-docs-layout__drawer-docselect[data-open='true']
436
+ .velu-docs-layout__drawer-docselect-menu {
437
+ opacity: 1;
438
+ visibility: visible;
439
+ transform: translateY(0);
440
+ pointer-events: auto;
441
+ transition:
442
+ opacity 0.15s ease,
443
+ transform 0.15s ease,
444
+ visibility 0s;
445
+ }
446
+ .velu-docs-layout__drawer-docselect-item {
447
+ display: block;
448
+ padding-block: var(--s-2);
449
+ padding-inline: var(--s0);
450
+ font-size: var(--f-h5);
451
+ color: var(--text-color);
452
+ text-decoration: none;
453
+ cursor: pointer;
454
+ transition: background 0.12s ease, color 0.12s ease;
455
+ }
456
+ .velu-docs-layout__drawer-docselect-item:hover {
457
+ background: var(--surface-color);
458
+ color: var(--accent-color);
459
+ }
460
+
461
+ /* ── Sidebar toggle (narrow-width only) ────────────────────────────── */
462
+ /* Small chevron button straddling the sidebar/article boundary.
463
+ `position: fixed` (not absolute) so it stays pinned to the
464
+ viewport while the article scrolls — the chevron is always
465
+ reachable, never buried below the fold. It sits one button-radius
466
+ below the header and is centred on the boundary between the
467
+ sidebar/rail and the article column. That boundary is exposed
468
+ via the `--velu-sidebar-boundary` custom property (defaults to
469
+ 240px when the sidebar is open; collapses to the rail width
470
+ when closed — see @container rules below).
471
+
472
+ Hidden by default; revealed only at narrow widths. */
473
+ .velu-docs-layout__sidebar-toggle {
474
+ display: none;
475
+ position: fixed;
476
+ /* Toggle sits well below the TocBar so it doesn't crowd the
477
+ bar's bottom border. --s5 (~49px) clears the TocBar height
478
+ itself; adding --s2 (~25px) gives a clear breathing gap. */
479
+ inset-block-start: calc(var(--velu-header-height) + var(--s5) + var(--s2));
480
+ /* Sidebar's right edge minus half the toggle's own width — centres
481
+ the 2rem button on the divider. Tracks the actual sidebar width. */
482
+ inset-inline-start: calc(var(--velu-sidebar-boundary, var(--velu-sidebar-width)) - 1rem);
483
+ inline-size: 2rem;
484
+ block-size: 2rem;
485
+ align-items: center;
486
+ justify-content: center;
487
+ padding: 0;
488
+ border: var(--border-width) solid var(--border-color);
489
+ border-radius: 999px;
490
+ background: var(--page-bg);
491
+ color: var(--muted-color);
492
+ cursor: pointer;
493
+ transition: border-color 0.12s ease, color 0.12s ease,
494
+ inset-inline-start 0.2s ease;
495
+ z-index: 11;
496
+ }
497
+ .velu-docs-layout__sidebar-toggle:hover {
498
+ border-color: var(--accent-color);
499
+ color: var(--accent-color);
500
+ }
501
+
502
+ /* ── Narrow-width layout ───────────────────────────────────────────── */
503
+ /* Container query (not media query) so the layout reacts to the
504
+ wrapper's inline-size — works the same inside a modal, split pane,
505
+ or shrunken window. At < 1024px the right TOC drops out and the
506
+ centre column expands to fill its slot; the left sidebar becomes
507
+ toggleable via the sidebar-toggle button (default open, per
508
+ design). Closing pushes the centre column over rather than overlaying. */
509
+ @container docs (max-width: 1024px) {
510
+ .velu-docs-layout__aside--right {
511
+ display: none;
512
+ }
513
+ .velu-docs-layout__center {
514
+ margin-inline-end: 0;
515
+ }
516
+ .velu-docs-layout__sidebar-toggle {
517
+ display: inline-flex;
518
+ }
519
+ /* Hairline divider on the left sidebar — only at narrow widths,
520
+ where the sidebar visually separates from the article column.
521
+ Same --surface-color recipe as the header/footer rails. The big
522
+ desktop left-inset is dialed back here (tablet); mobile resets it
523
+ to 0 via the drawer block below. */
524
+ .velu-docs-layout__aside--left {
525
+ border-inline-end: var(--border-width) solid var(--surface-color);
526
+ padding-inline-start: var(--s2);
527
+ }
528
+ /* Sidebar closed → shrink the aside's `inline-size` from 240px to
529
+ the rail width. The aside stays in the DOM and keeps its own
530
+ border-inline-end — that border IS the rail's right edge when
531
+ collapsed, no separate rail element needed. `overflow-x: clip`
532
+ on the aside (set above) crops the still-rendered Sidebar nav
533
+ to the narrowed box; the opacity rule below fades the nav out
534
+ so the collapsed state reads as a clean stub.
535
+ The aside's `transition: inline-size 0.2s ease` (set above) is
536
+ what makes the collapse animate; the centre column tracks via
537
+ its own `margin-inline-start` transition. */
538
+ .velu-docs-layout[data-sidebar-open='false']
539
+ .velu-docs-layout__aside--left {
540
+ inline-size: var(--velu-rail-width);
541
+ /* Collapsed state is a thin stub — the (faded) nav is irrelevant,
542
+ so suppress the y-scrollbar that would otherwise appear over
543
+ the rail. `overflow-y: auto` is restored implicitly at wide
544
+ widths / open state by the base `.velu-docs-layout__aside`
545
+ rule above. */
546
+ overflow-y: hidden;
547
+ }
548
+ /* Fade out the Sidebar nav while the aside shrinks — opacity
549
+ transitions in parallel with `inline-size`, so the user sees a
550
+ single coherent collapse (shrink + fade) rather than a snap. */
551
+ .velu-docs-layout[data-sidebar-open='false']
552
+ .velu-docs-layout__aside--left > * {
553
+ opacity: 0;
554
+ pointer-events: none;
555
+ }
556
+ .velu-docs-layout[data-sidebar-open='false']
557
+ .velu-docs-layout__center {
558
+ margin-inline-start: var(--velu-rail-width);
559
+ }
560
+ /* Toggle position tracks the active boundary directly — when the
561
+ sidebar is closed the boundary moves from 240px (sidebar's right
562
+ edge) to var(--velu-rail-width) (rail's right edge), so the
563
+ toggle slides with it. Overriding the property here (instead of
564
+ going through a custom-prop indirection) keeps the transition
565
+ reliable across engines. */
566
+ .velu-docs-layout[data-sidebar-open='false']
567
+ .velu-docs-layout__sidebar-toggle {
568
+ inset-inline-start: calc(var(--velu-rail-width) - 1rem);
569
+ }
570
+ /* At narrow widths the article drops its 46rem cap and fills the
571
+ available main width. Cap survives at wide widths so the 46rem
572
+ prose column lives between the two asides. */
573
+ .velu-docs-layout__article {
574
+ max-inline-size: none;
575
+ margin-inline: 0;
576
+ }
577
+ }
578
+
579
+ /* ── Mobile-width layout (< 640px) ─────────────────────────────────── */
580
+ /* At mobile widths the centre column reclaims full width regardless
581
+ of sidebar state. The sidebar becomes a RIGHT-edge drawer (anchored
582
+ to the trailing edge, slides in from the right) — opened by the
583
+ header's burger OR by tapping the breadcrumb strip, both of which
584
+ call `onMenuClick` → toggles the layout's `data-drawer-open` flag.
585
+ Note: mobile uses its OWN `data-drawer-open` attribute, separate
586
+ from `data-sidebar-open` (which drives the narrow-width rail
587
+ collapse). Two independent states avoid the SSR/hydration default
588
+ collision that would force the drawer to render open on refresh.
589
+ The floating chevron toggle is removed; it was a wide-layout
590
+ affordance and is redundant at mobile. */
591
+ @container docs (max-width: 640px) {
592
+ .velu-docs-layout__sidebar-toggle {
593
+ display: none;
594
+ }
595
+ .velu-docs-layout__center,
596
+ .velu-docs-layout[data-sidebar-open='false'] .velu-docs-layout__center {
597
+ margin-inline-start: 0;
598
+ }
599
+ .velu-docs-layout__aside--left {
600
+ /* Anchor to the right edge instead of the left — override the
601
+ base `inset-inline-start: 0` from .velu-docs-layout__aside--left
602
+ so the drawer slides in from the trailing edge. Full viewport
603
+ height comes from the base `inset-block: 0 0` on the aside.
604
+ Drop the header-clearance top padding — the drawer has its own
605
+ brand bar. Inline padding stays at 0 — the Sidebar items have
606
+ their own `padding-inline: var(--s0)` (16px), and the drawer
607
+ chrome below gets matching `padding-inline` directly, so the
608
+ inner content lines up 16px from the drawer edge without
609
+ double-padding. */
610
+ inset-inline-start: auto;
611
+ inset-inline-end: 0;
612
+ /* Full-height drawer — no bottom boundary gap (overrides the
613
+ desktop panel inset). */
614
+ inset-block-end: 0;
615
+ inline-size: min(20rem, 85vw);
616
+ padding-block-start: var(--s0);
617
+ padding-inline: 0;
618
+ /* Closed default — fully off the trailing edge. The
619
+ data-sidebar-open='true' rule below brings it back in. */
620
+ transform: translateX(100%);
621
+ /* Above the sticky header (z-index 30 in page-header.css) so the
622
+ drawer covers the header bar too, but below the search palette
623
+ (z-index 60) and chatbot (z-index 50). The aside already spans
624
+ the full viewport via the base `inset-block: 0 0`. */
625
+ z-index: 35;
626
+ /* Solid body background — the aside has no background by default
627
+ (it inherits the layout's page-bg by virtue of being a child),
628
+ but at mobile it's a `position: fixed` drawer that overlays the
629
+ article + header, so transparency would let everything beneath
630
+ bleed through. Explicit --page-bg makes the drawer opaque. */
631
+ background: var(--page-bg);
632
+ }
633
+ /* Shadow only when the drawer is OPEN — when closed the drawer
634
+ sits off-screen right via `translateX(100%)`, and a left-edge
635
+ shadow would bleed leftward into the visible viewport (a ghost
636
+ band along the right edge). Gating to the open state keeps the
637
+ depth cue while the drawer is visible and hides it the rest of
638
+ the time. */
639
+ .velu-docs-layout[data-drawer-open='true']
640
+ .velu-docs-layout__aside--left {
641
+ transform: translateX(0);
642
+ box-shadow:
643
+ calc(-1 * var(--s0)) 0 var(--s2)
644
+ color-mix(in srgb, #000 18%, transparent);
645
+ }
646
+ /* Drop the narrow-width opacity fade / overflow-hidden on the
647
+ drawer's inner content — at mobile the narrow-width rules above
648
+ (data-sidebar-open='false') still apply because sidebarOpen
649
+ defaults to true and may flip via the chevron at narrow widths,
650
+ but at mobile we want the inner nav fully visible and the
651
+ drawer scrollable regardless of those narrow rules. */
652
+ .velu-docs-layout__aside--left > * {
653
+ opacity: 1;
654
+ pointer-events: auto;
655
+ }
656
+ /* The aside stays a non-scrolling flex column on mobile too; the
657
+ nav region (.velu-docs-nav-scroll) owns the scroll, with the
658
+ context zone pinned above it. */
659
+ .velu-docs-layout__aside--left {
660
+ overflow: hidden;
661
+ }
662
+ /* Drawer chrome — brand bar + docs-set selector — visible only at
663
+ mobile (default-hidden above). Both elements get `padding-inline:
664
+ var(--s0)` so their content lines up at the same 16px-from-edge
665
+ position the Sidebar items below them use (which inherit
666
+ `padding-inline: var(--s0)` from sidebar.css). */
667
+ .velu-docs-layout__drawer-head {
668
+ display: flex;
669
+ align-items: center;
670
+ gap: var(--s-1);
671
+ padding-inline: var(--s0);
672
+ }
673
+ .velu-docs-layout__drawer-head .velu-theme-toggle {
674
+ margin-inline-start: auto;
675
+ }
676
+ .velu-docs-layout__drawer-docselect {
677
+ display: block;
678
+ margin-inline: var(--s0);
679
+ /* Inset 16px from each drawer edge; Stack (column flex with
680
+ align-items: stretch) stretches it across the remaining width.
681
+ `align-self: stretch` explicit for engines that don't honor
682
+ the parent's default stretch. */
683
+ align-self: stretch;
684
+ }
685
+ /* Scrim — present at mobile; toggled on by the mobile nav drawer
686
+ here. The chatbot's bottom-sheet variant exists at the wider
687
+ `< 1024px` threshold, so the chatbot-driven scrim is enabled in
688
+ a SECOND @container block below (at the same wider threshold) —
689
+ keeping the drawer-only scrim scoped to mobile and the
690
+ chatbot-driven scrim available to both narrow and mobile. */
691
+ .velu-docs-layout__scrim {
692
+ display: block;
693
+ }
694
+ .velu-docs-layout[data-drawer-open='true'] .velu-docs-layout__scrim {
695
+ opacity: 1;
696
+ pointer-events: auto;
697
+ visibility: visible;
698
+ transition:
699
+ opacity 0.25s ease,
700
+ visibility 0s;
701
+ }
702
+ }
703
+
704
+ /* ── Chatbot scrim (narrow + mobile, < 1024px) ─────────────────────── */
705
+ /* The chatbot becomes a bottom sheet at < 1024px (see chatbot.css);
706
+ the scrim follows the same threshold so the dim+blur layer appears
707
+ over the article whenever the sheet is up. The mobile-only
708
+ `data-drawer-open` trigger lives in the < 640px block above —
709
+ separate because the drawer itself doesn't exist at narrow widths. */
710
+ @container docs (max-width: 1024px) {
711
+ .velu-docs-layout__scrim {
712
+ display: block;
713
+ }
714
+ .velu-docs-layout[data-chat-open='true'] .velu-docs-layout__scrim {
715
+ opacity: 1;
716
+ pointer-events: auto;
717
+ visibility: visible;
718
+ transition:
719
+ opacity 0.25s ease,
720
+ visibility 0s;
721
+ }
722
+ }
723
+
724
+ /* ── Sidebar context zone + responsive switcher helpers ────────────── */
725
+ /* Product / version / language switchers + anchors above the group
726
+ nav. Inset matches the Sidebar items (which carry padding-inline:
727
+ var(--s0) from sidebar.css). */
728
+ .velu-docs-context {
729
+ flex: none;
730
+ padding-inline: var(--s0);
731
+ }
732
+ .velu-docs-anchors {
733
+ list-style: none;
734
+ margin: 0;
735
+ padding: 0;
736
+ display: flex;
737
+ flex-direction: column;
738
+ gap: var(--s-3);
739
+ }
740
+ .velu-docs-anchors__link {
741
+ display: flex;
742
+ align-items: center;
743
+ gap: var(--s-3);
744
+ color: var(--text-color);
745
+ text-decoration: none;
746
+ font-size: var(--f-h6);
747
+ transition: color 0.12s ease;
748
+ }
749
+ .velu-docs-anchors__link:hover {
750
+ color: var(--accent-color);
751
+ }
752
+ .velu-docs-anchors__icon {
753
+ display: inline-flex;
754
+ flex: none;
755
+ color: var(--muted-color);
756
+ }
757
+ .velu-docs-anchors__link:hover .velu-docs-anchors__icon {
758
+ color: var(--accent-color);
759
+ }
760
+
761
+ /* Show/hide helpers, scoped to the docs container's mobile breakpoint.
762
+ Version + language switchers live in the header at desktop/tablet
763
+ (hide-on-mobile) and re-appear in the drawer context zone on mobile
764
+ (show-on-mobile). */
765
+ .velu-show-on-mobile {
766
+ display: none;
767
+ }
768
+ @container docs (max-width: 640px) {
769
+ .velu-hide-on-mobile {
770
+ display: none !important;
771
+ }
772
+ .velu-show-on-mobile {
773
+ display: block;
774
+ }
775
+ }