@veluai/velu 0.2.38 → 0.2.40

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 (46) hide show
  1. package/dist/cli.js +42 -42
  2. package/docs/aalam.md +44 -0
  3. package/docs/mintlify-migration.md +9 -3
  4. package/docs/nila.md +13 -0
  5. package/docs/thulir.md +23 -0
  6. package/package.json +5 -2
  7. package/runtime/velu-ui/base.css +9 -0
  8. package/runtime/velu-ui/components/ApiReferencePage.jsx +4 -3
  9. package/runtime/velu-ui/components/ApiSamples.jsx +9 -2
  10. package/runtime/velu-ui/components/Callout.jsx +4 -1
  11. package/runtime/velu-ui/components/Chatbot.jsx +0 -16
  12. package/runtime/velu-ui/components/NavSelect.jsx +2 -1
  13. package/runtime/velu-ui/components/PageFooter.jsx +3 -22
  14. package/runtime/velu-ui/components/PageHeader.jsx +9 -21
  15. package/runtime/velu-ui/components/PageNav.jsx +98 -22
  16. package/runtime/velu-ui/components/PoweredBy.jsx +0 -20
  17. package/runtime/velu-ui/components/Sidebar.jsx +21 -9
  18. package/runtime/velu-ui/components/ThemeToggle.jsx +1 -0
  19. package/runtime/velu-ui/components/TocBar.jsx +21 -2
  20. package/runtime/velu-ui/components/VepaFooter.jsx +19 -17
  21. package/runtime/velu-ui/components/api-page.css +6 -3
  22. package/runtime/velu-ui/components/chatbot.css +5 -4
  23. package/runtime/velu-ui/components/docs-layout.css +99 -114
  24. package/runtime/velu-ui/components/page-footer.css +2 -3
  25. package/runtime/velu-ui/components/page-header.css +6 -0
  26. package/runtime/velu-ui/components/page-nav.css +192 -0
  27. package/runtime/velu-ui/components/sidebar.css +5 -13
  28. package/runtime/velu-ui/primitives/switcher.css +4 -0
  29. package/runtime/velu-ui/styles.css +3 -0
  30. package/runtime/velu-ui/themes/aalam.css +266 -0
  31. package/runtime/velu-ui/themes/nila.css +62 -0
  32. package/runtime/velu-ui/themes/thulir.css +428 -0
  33. package/schema/velu.schema.json +24 -3
  34. package/src/navigation.js +25 -4
  35. package/src/runtime/App.jsx +174 -108
  36. package/templates/starter/ai-tools/claude-code.mdx +1 -0
  37. package/templates/starter/ai-tools/cursor.mdx +1 -0
  38. package/templates/starter/api-reference/introduction.mdx +1 -0
  39. package/templates/starter/development.mdx +1 -0
  40. package/templates/starter/essentials/code.mdx +1 -0
  41. package/templates/starter/essentials/images.mdx +1 -0
  42. package/templates/starter/essentials/markdown.mdx +1 -0
  43. package/templates/starter/essentials/navigation.mdx +1 -0
  44. package/templates/starter/essentials/settings.mdx +1 -0
  45. package/templates/starter/index.mdx +1 -0
  46. package/templates/starter/quickstart.mdx +1 -0
package/docs/aalam.md ADDED
@@ -0,0 +1,44 @@
1
+ # Aalam theme
2
+
3
+ Aalam (ஆலம், banyan) is Velu's Maple equivalent. Use `"theme": "aalam"` in `velu.json`. Running `velu migrate mintlify SOURCE --out DESTINATION` maps Mintlify's `"theme": "maple"` to Aalam automatically.
4
+
5
+ ```json
6
+ {
7
+ "name": "My docs",
8
+ "theme": "aalam",
9
+ "colors": {
10
+ "primary": "#C2410C",
11
+ "light": "#EA580C",
12
+ "dark": "#FB923C"
13
+ }
14
+ }
15
+ ```
16
+
17
+ The desktop layout places branding, search, and navigation in the left sidebar, with tabs above the article. Below 1024px, navigation becomes a left-opening drawer with a labeled section picker, focus trapping, Escape dismissal, and background scroll locking. Below 1280px, a sticky On this page dropdown replaces the right-hand TOC. Drawer and TOC hierarchy use indentation without vertical lines.
18
+
19
+ Aalam shares component styles and responsive behavior with Thulir. Its desktop layout was compared with the cached npm renderer `@mintlify/client@0.0.3566` using the same showcase content. This is not a guarantee of pixel-perfect parity: theme controls, icons, compatibility adapters, generated OpenAPI examples, and custom scripts still require review. The left-opening drawer and line-free navigation intentionally follow Velu's requested behavior.
20
+
21
+ Color semantics match the Mintlify presets: `colors.primary` for the light-mode accent, `colors.light` for the dark-mode accent, and `colors.dark` for CTA colors. The default font is Inter.
22
+
23
+ ## Page icons in the sidebar
24
+
25
+ Pages can show a Lucide icon next to their sidebar label:
26
+
27
+ 1. Frontmatter on the MDX page:
28
+
29
+ ```mdx
30
+ ---
31
+ title: Quickstart
32
+ icon: rocket
33
+ ---
34
+ ```
35
+
36
+ 2. Or a page object in `velu.json` navigation:
37
+
38
+ ```json
39
+ { "page": "quickstart", "icon": "zap" }
40
+ ```
41
+
42
+ Frontmatter is used when the nav entry is a plain path string. A nav `icon` wins when both are set.
43
+
44
+ For the migrated component showcase, run `node packages/velu-cli/test/aalam-browser.mjs` with Playwright installed and the preview running. `VELU_TEST_URL` overrides the default `http://localhost:8473`. The checks cover phone/tablet drawer focus, dismissal, route changes, horizontal overflow, desktop resizing, section selection, content tabs, and sticky TOC heading clearance.
@@ -52,7 +52,7 @@ filenames, endpoint paths, and diagnostic text; review before sharing.
52
52
 
53
53
  | Area | Migration behavior |
54
54
  | --- | --- |
55
- | Config | Projects supported values against the shipped Velu schema; reports unsupported nested fields individually and preserves the original source config |
55
+ | Config | Projects supported values against the shipped Velu schema; maps Mintlify `mint` to `thulir`, `sequoia` to `vepa`, and `maple` to `aalam`; reports unsupported nested fields individually and preserves the original source config |
56
56
  | Legacy config | Converts navigation arrays, prefix-based tabs, anchors, topbar links/CTA, footer socials, and API base URL/example languages |
57
57
  | Navigation | Retains groups, tabs, products, versions, languages, root pages, and supported OpenAPI references; maps simple dropdown containers to tabs and merges global anchors/tabs with a review finding |
58
58
  | Branding | Copies logos/favicons, colors, navbar/footer, contextual menu and SEO; maps `description` to SEO metadata, body font family to `font`, and a dual favicon to one local variant |
@@ -86,6 +86,8 @@ Verify behavior after client-side navigation as well as a full refresh.
86
86
 
87
87
  The `css-dom` finding flags styles aimed at shared chrome IDs such as `#content`,
88
88
  `#sidebar-content`, and `#footer`. Vepa aligns these common hooks with Sequoia,
89
+ while Thulir preserves Mint's `#sidebar`, `#navigation-items`, `#page-title`,
90
+ `#content`, and `#footer` structure,
89
91
  including a content-body-only `#content` and route-specific body remounting, but
90
92
  a shared ID does not guarantee identical nesting or dimensions for every component.
91
93
  Selectors are preserved for review,
@@ -112,9 +114,12 @@ replaced with empty placeholders. Font Awesome/Tabler icon names may need Lucide
112
114
  equivalents. Original references in code examples and prose remain unchanged.
113
115
 
114
116
  Mintlify's `theme: "sequoia"` is converted to `theme: "vepa"` and activates the [Vepa preset](vepa.md).
115
- The preset is not a certification of pixel equality for every component or custom script.
117
+ Mintlify's `theme: "mint"` is converted to `theme: "thulir"` and activates the [Thulir preset](thulir.md).
118
+ Mintlify's `theme: "maple"` is converted to `theme: "aalam"` and activates the [Aalam preset](aalam.md).
119
+ Mintlify's `theme: "aspen"` is converted to `theme: "nila"` and activates the [Nila preset](nila.md).
120
+ The presets are not a certification of pixel equality for every component or custom script.
116
121
 
117
- Features without full Velu equivalents require review: other Mintlify themes, advanced
122
+ Features without full Velu equivalents require review: remaining Mintlify themes, advanced
118
123
  fonts and styling, redirects, analytics/support integrations, hosting settings,
119
124
  authentication, personalization, hidden-page routing, legacy version filtering,
120
125
  complex dropdown/global navigation, AsyncAPI, manually authored `api:` pages,
@@ -137,6 +142,7 @@ Inspected on 2026-09-08:
137
142
  - Published [`@mintlify/cli@4.0.1479`](https://www.npmjs.com/package/@mintlify/cli/v/4.0.1479): command dispatch/options in `bin/cli.js`; starter source in `bin/init.js`.
138
143
  - Published [`@mintlify/common@1.0.1132`](https://www.npmjs.com/package/@mintlify/common/v/1.0.1132): content/snippet categories and `.mintignore` semantics in `dist/getFileCategory.js` and `dist/mintIgnore.js`.
139
144
  - Published [`@mintlify/prebuild@1.0.1286`](https://www.npmjs.com/package/@mintlify/prebuild/v/1.0.1286): split config references and config loading.
145
+ - The CLI-downloaded `@mintlify/client@0.0.3566` renderer: Mint theme dispatch, computed layout geometry, typography, navigation, API samples, responsive behavior, and footer. The renderer was inspected for interoperability research and is not redistributed.
140
146
  - [`docs.json` schema](https://www.mintlify.com/docs.json), [custom scripts documentation](https://www.mintlify.com/docs/customize/custom-scripts), and [official starter](https://github.com/mintlify/starter).
141
147
 
142
148
  The current `@mintlify/validation@0.1.849` tarball was also retrieved, but it did
package/docs/nila.md ADDED
@@ -0,0 +1,13 @@
1
+ # Nila theme
2
+
3
+ Nila is Velu's independently implemented Aspen equivalent. Set `"theme": "nila"` in `velu.json`, or run `velu migrate mintlify SOURCE --out DESTINATION` to map Mintlify's `"theme": "aspen"` automatically. The existing `maple` to `aalam` mapping remains supported.
4
+
5
+ The layout uses a compact 96px desktop header, sidebar group separators, a 632px reading column at a 1440px viewport, page icons, and light/dark/system preferences. Nila shares the responsive navigation foundation with Thulir: a left-opening drawer, labeled section picker, focus trapping, Escape dismissal, scroll locking, and an on-page TOC dropdown when the right rail is hidden. Vertical hierarchy lines remain disabled in the drawer and mobile TOC.
6
+
7
+ The reference is the cached npm renderer `@mintlify/client@0.0.3566`, displaying the same source showcase. Theme matching is not a guarantee of identical component behavior: icon artwork, custom CSS selectors, compatibility adapters, generated API examples and interactive playgrounds still require review.
8
+
9
+ For the migrated showcase, set `VELU_TEST_URL=http://localhost:8474` and run `node packages/velu-cli/test/aalam-browser.mjs`. This shared browser regression covers drawer behavior, section selection, content tabs, responsive widths, and TOC heading clearance.
10
+
11
+ Mintlify migration enables `navigation.preservePagePaths` when the source uses product, version, or language axes. These axes select the navigation context while retaining source page URLs, rather than adding another path prefix. Native Velu configurations retain their existing prefix behavior unless they opt into this setting.
12
+
13
+ The navigation stress fixture adds three products (Platform, Analytics, Automation), two versions each, and 32 guides per product/version pair. With that migrated fixture running on port 8474, `node packages/velu-cli/test/nila-navigation-browser.mjs` checks five viewport widths, sidebar scrolling, product/version switching, direct reloads, horizontal overflow, and nested Escape behavior. This fixture has 220 pages including the existing component/API showcase; its additional Guides tab differs from the original four-tab showcase used by the shared browser test above.
package/docs/thulir.md ADDED
@@ -0,0 +1,23 @@
1
+ # Thulir theme
2
+
3
+ Thulir is Velu's equivalent of Mintlify's `mint` theme. The name means a fresh sprout in Tamil.
4
+
5
+ ```json
6
+ {
7
+ "name": "My docs",
8
+ "theme": "thulir",
9
+ "colors": {
10
+ "primary": "#0D9373",
11
+ "light": "#12A481",
12
+ "dark": "#46D6AD"
13
+ }
14
+ }
15
+ ```
16
+
17
+ `velu migrate mintlify` converts `"theme": "mint"` to `"theme": "thulir"`. Thulir preserves Mint's main renderer hooks, including `#navbar`, `#sidebar`, `#navigation-items`, `#page-title`, `#content`, and `#footer`, so portable custom CSS and browser scripts retain their targets.
18
+
19
+ The preset includes Mint's desktop grid, Inter typography, navigation states, prose rhythm, compact previous/next links, API document and code columns, responsive drawer behavior, light/dark palette, and advanced footer.
20
+
21
+ Mint color semantics are preserved: `colors.primary` is the light-mode accent, `colors.light` is the dark-mode accent, and `colors.dark` supplies the header button background. Missing values fall back to `colors.primary`. Header and footer theme controls share the same light/dark/system preference.
22
+
23
+ The reference is the renderer downloaded by `@mintlify/cli@4.0.1479` (`@mintlify/client@0.0.3566`). Compare identical content at identical viewport sizes when checking parity. The reference fixture covers documentation, GET/POST API pages, desktop/tablet/mobile layouts, and dark mode. Generated OpenAPI example values, icon artwork, and playground interactions can still differ; matching the theme does not imply that every Mintlify component or script has identical behavior.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veluai/velu",
3
- "version": "0.2.38",
3
+ "version": "0.2.40",
4
4
  "type": "module",
5
5
  "bin": "./dist/cli.js",
6
6
  "publishConfig": {
@@ -18,7 +18,10 @@
18
18
  "schema/**",
19
19
  "templates/**",
20
20
  "docs/mintlify-migration.md",
21
- "docs/vepa.md"
21
+ "docs/vepa.md",
22
+ "docs/thulir.md",
23
+ "docs/aalam.md",
24
+ "docs/nila.md"
22
25
  ],
23
26
  "scripts": {
24
27
  "build": "node scripts/build.mjs",
@@ -389,6 +389,14 @@
389
389
  background: var(--muted-color);
390
390
  }
391
391
 
392
+ /* Kill document-level horizontal scroll. Sticky header + fixed rails
393
+ share the viewport scrollport — if anything widens the page, the
394
+ whole chrome (top bar included) slides sideways. Clip here so wide
395
+ children must scroll inside their own containers instead. */
396
+ html {
397
+ overflow-x: clip;
398
+ }
399
+
392
400
  /* Unitless line-height ratios (height ÷ font-size from Figma) */
393
401
  body {
394
402
  font-size: var(--f-body);
@@ -396,6 +404,7 @@ body {
396
404
  font-weight: var(--weight-normal);
397
405
  background: var(--page-bg);
398
406
  color: var(--text-color);
407
+ overflow-x: clip;
399
408
  }
400
409
  h1 { font-size: var(--f-h1); line-height: var(--lh-h1); font-weight: var(--weight-semibold); }
401
410
  h2 { font-size: var(--f-h2); line-height: var(--lh-h2); font-weight: var(--weight-medium); }
@@ -78,7 +78,7 @@ export default function ApiReferencePage({ operation, samples = [], apiOperation
78
78
  className="velu-api-page__tryit"
79
79
  />
80
80
 
81
- {preset === 'vepa' && <div className="velu-api-page__inline-samples"><ApiSamples idPrefix="inline-" samples={samples} responses={operation.responses} preset={preset} title={operation.title} /></div>}
81
+ {(preset === 'vepa' || ['thulir', 'aalam', 'nila'].includes(preset)) && <div className="velu-api-page__inline-samples"><ApiSamples idPrefix="inline-" samples={samples} responses={operation.responses} preset={preset} title={operation.title} /></div>}
82
82
  {children && <div className="velu-api-page__custom-content">{children}</div>}
83
83
 
84
84
  {/* Authorizations */}
@@ -114,6 +114,7 @@ export default function ApiReferencePage({ operation, samples = [], apiOperation
114
114
  {operation.body?.fields?.length > 0 && (
115
115
  <api-section class="velu-api-page__section">
116
116
  <h2 data-component="api-section-heading">Body</h2>
117
+ {['thulir', 'aalam', 'nila'].includes(preset) && operation.body.contentType && <span className="velu-api-body-type">{operation.body.contentType}</span>}
117
118
  {operation.body.fields.map((f, i) => (
118
119
  <Field key={i} name={f.name} type={f.type} required={f.required}>
119
120
  {f.description}
@@ -179,10 +180,10 @@ function EnumHint({ values }) {
179
180
  }
180
181
 
181
182
  function ResponseSections({ responses, preset }) {
182
- if (preset === 'vepa') return <div className="velu-vepa-responses">{responses.map((r, i) => <section key={i}>
183
+ if (preset === 'vepa' || ['thulir', 'aalam', 'nila'].includes(preset)) return <div className="velu-vepa-responses">{responses.map((r, i) => <section key={i}>
183
184
  <div className="velu-vepa-responses__status">{r.status}{r.contentType && ` · ${r.contentType}`}</div>
184
185
  {r.description && <p>{r.description}</p>}
185
- {r.fields?.map((f, j) => <Field key={j} name={f.name} type={f.type} required={f.required}>{f.description}</Field>)}
186
+ {(['thulir', 'aalam', 'nila'].includes(preset) ? [...(r.fields || [])].sort((a, b) => Number(Boolean(b.required)) - Number(Boolean(a.required))) : r.fields)?.map((f, j) => <Field key={f.name || j} name={f.name} type={f.type} required={f.required}>{f.description}</Field>)}
186
187
  </section>)}</div>;
187
188
  return (
188
189
  <AccordionGroup className="velu-api-resp">
@@ -12,12 +12,19 @@ import CodeBlock, { CodeGroup } from './CodeBlock.jsx';
12
12
  export default function ApiSamples({ samples = [], responses = [], preset, title, idPrefix = '' }) {
13
13
  const [selected, setSelected] = React.useState('');
14
14
  const sample = samples.find((s) => s.key === selected) || samples[0];
15
+ // Mint displays JSON request bodies across lines. Keep custom/non-JSON
16
+ // snippets intact, and change whitespace only in a valid JSON body.
17
+ const sampleCode = ['thulir', 'aalam', 'nila'].includes(preset) && sample?.key === 'curl'
18
+ ? sample.code.replace(/(--data )'([\s\S]*)'$/, (original, prefix, body) => {
19
+ try { return `${prefix}'\n${JSON.stringify(JSON.parse(body), null, 2)}\n'`; }
20
+ catch { return original; }
21
+ }) : sample?.code;
15
22
  const withExample = responses.filter((r) => r.example != null);
16
23
  return (
17
24
  <div className="velu-api-samples">
18
- {sample && preset === 'vepa' ? <section id={idPrefix + 'request-example'} className="velu-vepa-request">
25
+ {sample && (preset === 'vepa' || ['thulir', 'aalam', 'nila'].includes(preset)) ? <section id={idPrefix + 'request-example'} className="velu-vepa-request">
19
26
  <div className="velu-vepa-request__header"><span>{title}</span><select aria-label="Request example language" value={sample.key} onChange={(e) => setSelected(e.target.value)}>{samples.map((s) => <option key={s.key} value={s.key}>{s.label}</option>)}</select></div>
20
- <CodeBlock language={sample.language}>{sample.code}</CodeBlock>
27
+ <CodeBlock language={sample.language}>{sampleCode}</CodeBlock>
21
28
  </section> : samples.length > 0 && (
22
29
  <CodeGroup id={idPrefix + 'request-example'} className="velu-api-samples__req">
23
30
  {samples.map((s) => (
@@ -90,7 +90,10 @@ export default function Callout({
90
90
  >
91
91
  {iconNode && (
92
92
  <span className="velu-callout__icon" aria-hidden="true">
93
- {iconNode}
93
+ {type === 'note' && icon == null ? <>
94
+ <span className="velu-callout__default-note">{iconNode}</span>
95
+ <span className="velu-callout__mint-note" style={{ display: 'none' }}>{resolveIcon('circle-alert', { size: '1em' })}</span>
96
+ </> : iconNode}
94
97
  </span>
95
98
  )}
96
99
  {children}
@@ -33,21 +33,6 @@ import Cluster from '../primitives/Cluster.jsx';
33
33
  * demo answer (so dev preview / offline still shows the experience).
34
34
  */
35
35
 
36
- /* ── Brand mark — the Velu double-hook logo ─────────────────────────── */
37
- // `size` is the glyph height; width derives from the 32:24 viewBox.
38
- function VeluMark({ size = 'var(--icon-size-lg)' }) {
39
- return (
40
- <svg
41
- className="velu-chatbot__mark"
42
- viewBox="0 0 32 24"
43
- fill="currentColor"
44
- aria-hidden="true"
45
- style={{ width: `calc(${size} * (32 / 24))`, height: size }}
46
- >
47
- <path d="M 29.656 14.153 C 29.574 14.562 29.351 14.929 29.025 15.193 C 28.699 15.457 28.291 15.601 27.869 15.601 L 21.37 15.601 C 20.534 15.601 19.805 16.163 19.603 16.964 L 18.167 22.637 C 18.068 23.026 17.841 23.372 17.521 23.619 C 17.2 23.866 16.805 24 16.399 24 L 10.438 24 C 10.161 24 9.888 23.938 9.639 23.818 C 9.39 23.698 9.172 23.524 9.001 23.308 C 8.831 23.092 8.712 22.841 8.655 22.574 C 8.598 22.306 8.603 22.029 8.67 21.764 L 10.189 15.764 C 10.287 15.374 10.515 15.029 10.835 14.782 C 11.155 14.535 11.55 14.401 11.956 14.4 L 18.393 14.4 C 19.262 14.4 20.01 13.795 20.18 12.953 L 22.388 2.048 C 22.471 1.639 22.694 1.272 23.019 1.008 C 23.345 0.744 23.754 0.6 24.175 0.6 L 30.177 0.6 C 30.447 0.6 30.713 0.659 30.957 0.773 C 31.201 0.886 31.416 1.052 31.587 1.258 C 31.758 1.464 31.881 1.705 31.946 1.964 C 32.011 2.222 32.017 2.492 31.964 2.753 L 29.656 14.153 Z M 9.611 13.554 C 9.528 13.962 9.305 14.329 8.979 14.593 C 8.653 14.857 8.245 15.001 7.824 15.001 L 1.823 15.001 C 1.553 15.001 1.287 14.942 1.043 14.828 C 0.799 14.714 0.584 14.548 0.413 14.342 C 0.242 14.136 0.119 13.895 0.054 13.637 C -0.011 13.378 -0.017 13.109 0.036 12.848 L 2.344 1.447 C 2.426 1.039 2.649 0.672 2.975 0.408 C 3.301 0.144 3.709 0 4.131 0 L 10.132 0 C 10.402 0 10.668 0.059 10.912 0.173 C 11.155 0.287 11.371 0.453 11.541 0.659 C 11.712 0.865 11.835 1.106 11.9 1.364 C 11.965 1.623 11.972 1.892 11.919 2.153 L 9.611 13.553 L 9.611 13.554 Z" />
48
- </svg>
49
- );
50
- }
51
36
 
52
37
  /* ── "Thinking" loader — 3x3 grid of squares, opacity wave sweeps the
53
38
  anti-diagonal so it ripples top-left → bottom-right. ─────────────── */
@@ -346,7 +331,6 @@ function ChatHeader({ onClose, onNew, onHistory, historyOpen, showClose = true }
346
331
  className="velu-chatbot__header"
347
332
  >
348
333
  <Cluster space="var(--s-2)" align="center">
349
- <VeluMark />
350
334
  <span className="velu-chatbot__brand">
351
335
  <span className="velu-chatbot__brand-name">Velu</span>
352
336
  <span className="velu-chatbot__brand-sep">/</span>
@@ -49,7 +49,7 @@ export default function NavSelect({
49
49
  if (!rootRef.current?.contains(e.target)) setOpen(false);
50
50
  };
51
51
  const onKey = (e) => {
52
- if (e.key === 'Escape') setOpen(false);
52
+ if (e.key === 'Escape') { setOpen(false); rootRef.current?.querySelector('button')?.focus(); }
53
53
  };
54
54
  document.addEventListener('mousedown', onDoc);
55
55
  document.addEventListener('keydown', onKey);
@@ -124,3 +124,4 @@ export default function NavSelect({
124
124
  </div>
125
125
  );
126
126
  }
127
+
@@ -47,26 +47,6 @@ import SocialLinks from './SocialLinks.jsx';
47
47
  * linkComponent?: React.ElementType, className?: string }} props
48
48
  */
49
49
 
50
- /* ── Brand mark — the Velu double-hook logo. Color: currentColor for
51
- the surrounding wordmark; the mark itself uses --accent-color via
52
- the wrapping span. ───────────────────────────────────────────────── */
53
- function VeluMark() {
54
- return (
55
- <svg
56
- className="velu-footer__mark"
57
- viewBox="0 0 32 24"
58
- fill="currentColor"
59
- aria-hidden="true"
60
- >
61
- <path
62
- fillRule="evenodd"
63
- clipRule="evenodd"
64
- d="M29.6564 14.1534C29.5739 14.5616 29.3507 14.9288 29.0248 15.1926C28.699 15.4565 28.2907 15.6007 27.8694 15.6006H21.3702C20.5341 15.6006 19.8051 16.1627 19.6031 16.9638L18.1671 22.6368C18.0685 23.0263 17.8409 23.372 17.5205 23.619C17.2002 23.866 16.8054 24.0001 16.399 24H10.4378C10.1609 24.0001 9.88759 23.9378 9.63868 23.8179C9.38976 23.6979 9.1718 23.5235 9.00135 23.3079C8.8309 23.0923 8.71245 22.8412 8.65499 22.5736C8.59754 22.306 8.6026 22.029 8.66978 21.7636L10.1888 15.7636C10.2874 15.3743 10.5148 15.0287 10.835 14.7817C11.1551 14.5347 11.5496 14.4005 11.9559 14.4004H18.3931C19.2621 14.4004 20.0101 13.7949 20.1801 12.9532L22.3882 2.04774C22.4705 1.63945 22.6936 1.27198 23.0195 1.00793C23.3453 0.74388 23.7538 0.599578 24.1752 0.599605H30.1774C30.4471 0.599529 30.7134 0.658587 30.9572 0.772515C31.201 0.886444 31.4161 1.0524 31.5871 1.25842C31.758 1.46443 31.8806 1.70537 31.9458 1.96383C32.0111 2.2223 32.0175 2.49185 31.9645 2.75305L29.6564 14.1534ZM9.61081 13.5538C9.52825 13.962 9.30507 14.3292 8.97924 14.593C8.65341 14.857 8.24508 15.0011 7.82375 15.001H1.82257C1.55289 15.0011 1.28656 14.942 1.04279 14.8281C0.79902 14.7141 0.583892 14.5482 0.412928 14.3422C0.241965 14.1362 0.11943 13.8952 0.0541639 13.6368C-0.0111017 13.3783 -0.0174699 13.1087 0.0355186 12.8475L2.34359 1.44715C2.42614 1.03904 2.64933 0.671798 2.97516 0.407944C3.30099 0.144089 3.70932 -7.24082e-05 4.13064 7.25899e-08H10.1318C10.4015 -7.55792e-05 10.6678 0.0589818 10.9116 0.172911C11.1554 0.286839 11.3705 0.453799 11.5415 0.659813C11.7124 0.864828 11.835 1.10576 11.9002 1.36423C11.9655 1.62269 11.9719 1.89225 11.9189 2.15344L9.61081 13.553L9.61081 13.5538Z"
65
- />
66
- </svg>
67
- );
68
- }
69
-
70
50
  export default function PageFooter({
71
51
  preset,
72
52
  brand,
@@ -77,13 +57,14 @@ export default function PageFooter({
77
57
  ...rest
78
58
  }) {
79
59
  const Link = linkComponent;
80
- if (preset === 'vepa') return <VepaFooter brand={brand} columns={columns} socials={socials} linkComponent={Link} />;
60
+ if (preset === 'vepa' || ['thulir', 'aalam', 'nila'].includes(preset)) {
61
+ return <VepaFooter preset={preset} brand={brand} columns={columns} socials={socials} linkComponent={Link} />;
62
+ }
81
63
  const brandLabel = brand?.label ?? 'Velu';
82
64
  const brandHref = brand?.href;
83
65
 
84
66
  const brandNode = (
85
67
  <Cluster space="var(--s-3)" align="center" className="velu-footer__brand">
86
- <VeluMark />
87
68
  <span className="velu-footer__wordmark">{brandLabel}</span>
88
69
  </Cluster>
89
70
  );
@@ -48,25 +48,8 @@ import Logo from './Logo.jsx';
48
48
  * linkComponent?: React.ElementType, className?: string }} props
49
49
  */
50
50
 
51
- /* Velu brand mark. Color: --accent-color via the wrapping wordmark
52
- span; the path uses currentColor. Exported so consumers (e.g. the
53
- mobile nav drawer) can render the mark alongside its own chrome. */
54
- export function VeluMark() {
55
- return (
56
- <svg
57
- className="velu-header__mark"
58
- viewBox="0 0 32 24"
59
- fill="currentColor"
60
- aria-hidden="true"
61
- >
62
- <path
63
- fillRule="evenodd"
64
- clipRule="evenodd"
65
- d="M29.6564 14.1534C29.5739 14.5616 29.3507 14.9288 29.0248 15.1926C28.699 15.4565 28.2907 15.6007 27.8694 15.6006H21.3702C20.5341 15.6006 19.8051 16.1627 19.6031 16.9638L18.1671 22.6368C18.0685 23.0263 17.8409 23.372 17.5205 23.619C17.2002 23.866 16.8054 24.0001 16.399 24H10.4378C10.1609 24.0001 9.88759 23.9378 9.63868 23.8179C9.38976 23.6979 9.1718 23.5235 9.00135 23.3079C8.8309 23.0923 8.71245 22.8412 8.65499 22.5736C8.59754 22.306 8.6026 22.029 8.66978 21.7636L10.1888 15.7636C10.2874 15.3743 10.5148 15.0287 10.835 14.7817C11.1551 14.5347 11.5496 14.4005 11.9559 14.4004H18.3931C19.2621 14.4004 20.0101 13.7949 20.1801 12.9532L22.3882 2.04774C22.4705 1.63945 22.6936 1.27198 23.0195 1.00793C23.3453 0.74388 23.7538 0.599578 24.1752 0.599605H30.1774C30.4471 0.599529 30.7134 0.658587 30.9572 0.772515C31.201 0.886444 31.4161 1.0524 31.5871 1.25842C31.758 1.46443 31.8806 1.70537 31.9458 1.96383C32.0111 2.2223 32.0175 2.49185 31.9645 2.75305L29.6564 14.1534ZM9.61081 13.5538C9.52825 13.962 9.30507 14.3292 8.97924 14.593C8.65341 14.857 8.24508 15.0011 7.82375 15.001H1.82257C1.55289 15.0011 1.28656 14.942 1.04279 14.8281C0.79902 14.7141 0.583892 14.5482 0.412928 14.3422C0.241965 14.1362 0.11943 13.8952 0.0541639 13.6368C-0.0111017 13.3783 -0.0174699 13.1087 0.0355186 12.8475L2.34359 1.44715C2.42614 1.03904 2.64933 0.671798 2.97516 0.407944C3.30099 0.144089 3.70932 -7.24082e-05 4.13064 7.25899e-08H10.1318C10.4015 -7.55792e-05 10.6678 0.0589818 10.9116 0.172911C11.1554 0.286839 11.3705 0.453799 11.5415 0.659813C11.7124 0.864828 11.835 1.10576 11.9002 1.36423C11.9655 1.62269 11.9719 1.89225 11.9189 2.15344L9.61081 13.5538Z"
66
- />
67
- </svg>
68
- );
69
- }
51
+ // Kept for callers importing the former mark; built-in branding is text-only.
52
+ export function VeluMark() { return null; }
70
53
 
71
54
  function ActionButton({ action, Link, id }) {
72
55
  const { label, icon, kind = 'outlined', href, onClick, external, component } = action;
@@ -243,6 +226,8 @@ export default function PageHeader({
243
226
  activeTab,
244
227
  breadcrumb,
245
228
  onMenuClick,
229
+ menuOpen,
230
+ menuId,
246
231
  linkComponent = 'a',
247
232
  className = '',
248
233
  ...rest
@@ -280,7 +265,6 @@ export default function PageHeader({
280
265
  <Logo logo={brand.logo} alt={brandLabel} />
281
266
  ) : (
282
267
  <>
283
- <VeluMark />
284
268
  <span className="velu-header__wordmark">{brandLabel}</span>
285
269
  </>
286
270
  )}
@@ -290,7 +274,7 @@ export default function PageHeader({
290
274
  return (
291
275
  <Stack
292
276
  as="header"
293
- space={preset === 'vepa' ? '0px' : 'var(--s0)'}
277
+ space={preset === 'vepa' || ['thulir', 'aalam', 'nila'].includes(preset) ? '0px' : 'var(--s0)'}
294
278
  data-has-tabs={tabs.length > 0 ? 'true' : 'false'}
295
279
  className={`velu-header${scrolled ? ' velu-header--scrolled' : ''}${
296
280
  showTabsRow ? '' : ' velu-header--no-tabs'
@@ -391,6 +375,8 @@ export default function PageHeader({
391
375
  data-component="breadcrumb-list"
392
376
  onClick={onMenuClick}
393
377
  aria-label="Open navigation menu"
378
+ aria-expanded={menuOpen}
379
+ aria-controls={menuId}
394
380
  >
395
381
  {breadcrumb.map((c, i) => {
396
382
  const isLast = i === breadcrumb.length - 1;
@@ -432,6 +418,8 @@ export default function PageHeader({
432
418
  type="button"
433
419
  className="velu-header__menu"
434
420
  aria-label="Open navigation menu"
421
+ aria-expanded={menuOpen}
422
+ aria-controls={menuId}
435
423
  onClick={onMenuClick}
436
424
  >
437
425
  <Menu aria-hidden="true" focusable="false" />
@@ -5,29 +5,23 @@ import Switcher from '../primitives/Switcher.jsx';
5
5
 
6
6
  /**
7
7
  * PageNav — previous / next page navigation for the foot of a docs
8
- * page. Two cards: the previous page (content flush to the inline
9
- * start, a "‹ Previous" label) and the next page (flush to the inline
10
- * end, "Next ›").
8
+ * page.
11
9
  *
12
- * <PageNav
13
- * linkComponent={RouterLink}
14
- * prev={{ label: 'docs.json schema reference', href: '/schema' }}
15
- * next={{ label: 'Pages', href: '/pages' }}
16
- * />
17
- *
18
- * Either side may be omitted (first / last page) — the present card
19
- * keeps its half-width slot. Cards sit side by side and stack on narrow
20
- * widths via <Switcher>. Router-agnostic through `linkComponent`.
10
+ * Default / Thulir: two text cards ("‹ Previous" / "Next ›").
11
+ * Aalam (Maple): padded tray with a featured destination card
12
+ * (title + description + label) and a compact Previous when both
13
+ * sides exist matching Mintlify Maple's pagination.
21
14
  *
22
15
  * @typedef {Object} PageRef
23
- * @property {string} label page title shown on the card
16
+ * @property {string} label
24
17
  * @property {string} [href]
18
+ * @property {string} [description]
25
19
  *
26
- * @param {{ prev?: PageRef, next?: PageRef,
20
+ * @param {{ prev?: PageRef, next?: PageRef, variant?: 'default'|'aalam',
27
21
  * linkComponent?: React.ElementType, className?: string }} props
28
22
  */
29
23
 
30
- function NavCard({ page, dir, Link }) {
24
+ function DefaultCard({ page, dir, Link }) {
31
25
  const isPrev = dir === 'prev';
32
26
  return (
33
27
  <Link
@@ -36,11 +30,7 @@ function NavCard({ page, dir, Link }) {
36
30
  href={page.href ?? '#'}
37
31
  >
38
32
  <pagination-title class="velu-pagenav__title">{page.label}</pagination-title>
39
- <Cluster
40
- space="var(--s-4)"
41
- align="center"
42
- className="velu-pagenav__dir"
43
- >
33
+ <Cluster space="var(--s-4)" align="center" className="velu-pagenav__dir">
44
34
  {isPrev && resolveIcon('chevron-left', { size: '1em' })}
45
35
  <span>{isPrev ? 'Previous' : 'Next'}</span>
46
36
  {!isPrev && resolveIcon('chevron-right', { size: '1em' })}
@@ -49,13 +39,99 @@ function NavCard({ page, dir, Link }) {
49
39
  );
50
40
  }
51
41
 
42
+ /** Maple featured destination card: title + description | Next/Previous. */
43
+ function MapleFeatured({ page, dir, Link }) {
44
+ const isPrev = dir === 'prev';
45
+ const meta = (
46
+ <div className="velu-pagenav__meta">
47
+ <pagination-title class="velu-pagenav__title">{page.label}</pagination-title>
48
+ {page.description ? (
49
+ <span className="velu-pagenav__desc">{page.description}</span>
50
+ ) : null}
51
+ </div>
52
+ );
53
+ const rule = <div className="velu-pagenav__rule" aria-hidden="true" />;
54
+ const label = (
55
+ <div className="velu-pagenav__dir">
56
+ {isPrev ? resolveIcon('chevron-left', { size: 12, strokeWidth: 3 }) : null}
57
+ <span>{isPrev ? 'Previous' : 'Next'}</span>
58
+ {!isPrev ? resolveIcon('chevron-right', { size: 12, strokeWidth: 3 }) : null}
59
+ </div>
60
+ );
61
+ return (
62
+ <Link
63
+ className={`velu-pagenav__featured velu-pagenav__featured--${dir}`}
64
+ data-component={`pagination-${dir}`}
65
+ href={page.href ?? '#'}
66
+ >
67
+ <div className="velu-pagenav__featured-inner">
68
+ {isPrev ? (
69
+ <>
70
+ {label}
71
+ {rule}
72
+ {meta}
73
+ </>
74
+ ) : (
75
+ <>
76
+ {meta}
77
+ {rule}
78
+ {label}
79
+ </>
80
+ )}
81
+ </div>
82
+ </Link>
83
+ );
84
+ }
85
+
86
+ /** Compact Previous control when Next is the featured card. */
87
+ function MapleCompactPrev({ page, Link }) {
88
+ return (
89
+ <Link
90
+ className="velu-pagenav__compact velu-pagenav__compact--prev"
91
+ data-component="pagination-prev"
92
+ href={page.href ?? '#'}
93
+ >
94
+ {resolveIcon('chevron-left', { size: 12, strokeWidth: 3 })}
95
+ <span>Previous</span>
96
+ </Link>
97
+ );
98
+ }
99
+
100
+ function AalamNav({ prev, next, Link }) {
101
+ // Maple: when both exist, Previous is compact and Next is featured.
102
+ // Only-prev / only-next: the single side is the featured card.
103
+ if (prev && next) {
104
+ return (
105
+ <>
106
+ <MapleCompactPrev page={prev} Link={Link} />
107
+ <MapleFeatured page={next} dir="next" Link={Link} />
108
+ </>
109
+ );
110
+ }
111
+ if (next) return <MapleFeatured page={next} dir="next" Link={Link} />;
112
+ if (prev) return <MapleFeatured page={prev} dir="prev" Link={Link} />;
113
+ return null;
114
+ }
115
+
52
116
  export default function PageNav({
53
117
  prev,
54
118
  next,
119
+ variant = 'default',
55
120
  linkComponent = 'a',
56
121
  className = '',
57
122
  ...rest
58
123
  }) {
124
+ if (variant === 'aalam') {
125
+ return (
126
+ <nav
127
+ className={`velu-pagenav velu-pagenav--aalam ${className}`.trim()}
128
+ {...rest}
129
+ >
130
+ <AalamNav prev={prev} next={next} Link={linkComponent} />
131
+ </nav>
132
+ );
133
+ }
134
+
59
135
  return (
60
136
  <Switcher
61
137
  space="var(--s0)"
@@ -64,12 +140,12 @@ export default function PageNav({
64
140
  {...rest}
65
141
  >
66
142
  {prev ? (
67
- <NavCard page={prev} dir="prev" Link={linkComponent} />
143
+ <DefaultCard page={prev} dir="prev" Link={linkComponent} />
68
144
  ) : (
69
145
  <div aria-hidden="true" />
70
146
  )}
71
147
  {next ? (
72
- <NavCard page={next} dir="next" Link={linkComponent} />
148
+ <DefaultCard page={next} dir="next" Link={linkComponent} />
73
149
  ) : (
74
150
  <div aria-hidden="true" />
75
151
  )}
@@ -8,25 +8,6 @@ import React from 'react';
8
8
  * @param {{ href?: string, className?: string }} props
9
9
  */
10
10
 
11
- /* Velu brand mark — same path as PageHeader's mark, sized for the
12
- badge via .velu-powered-by__mark CSS. */
13
- function VeluMark() {
14
- return (
15
- <svg
16
- className="velu-powered-by__mark"
17
- viewBox="0 0 32 24"
18
- fill="currentColor"
19
- aria-hidden="true"
20
- >
21
- <path
22
- fillRule="evenodd"
23
- clipRule="evenodd"
24
- d="M29.6564 14.1534C29.5739 14.5616 29.3507 14.9288 29.0248 15.1926C28.699 15.4565 28.2907 15.6007 27.8694 15.6006H21.3702C20.5341 15.6006 19.8051 16.1627 19.6031 16.9638L18.1671 22.6368C18.0685 23.0263 17.8409 23.372 17.5205 23.619C17.2002 23.866 16.8054 24.0001 16.399 24H10.4378C10.1609 24.0001 9.88759 23.9378 9.63868 23.8179C9.38976 23.6979 9.1718 23.5235 9.00135 23.3079C8.8309 23.0923 8.71245 22.8412 8.65499 22.5736C8.59754 22.306 8.6026 22.029 8.66978 21.7636L10.1888 15.7636C10.2874 15.3743 10.5148 15.0287 10.835 14.7817C11.1551 14.5347 11.5496 14.4005 11.9559 14.4004H18.3931C19.2621 14.4004 20.0101 13.7949 20.1801 12.9532L22.3882 2.04774C22.4705 1.63945 22.6936 1.27198 23.0195 1.00793C23.3453 0.74388 23.7538 0.599578 24.1752 0.599605H30.1774C30.4471 0.599529 30.7134 0.658587 30.9572 0.772515C31.201 0.886444 31.4161 1.0524 31.5871 1.25842C31.758 1.46443 31.8806 1.70537 31.9458 1.96383C32.0111 2.2223 32.0175 2.49185 31.9645 2.75305L29.6564 14.1534ZM9.61081 13.5538C9.52825 13.962 9.30507 14.3292 8.97924 14.593C8.65341 14.857 8.24508 15.0011 7.82375 15.001H1.82257C1.55289 15.0011 1.28656 14.942 1.04279 14.8281C0.79902 14.7141 0.583892 14.5482 0.412928 14.3422C0.241965 14.1362 0.11943 13.8952 0.0541639 13.6368C-0.0111017 13.3783 -0.0174699 13.1087 0.0355186 12.8475L2.34359 1.44715C2.42614 1.03904 2.64933 0.671798 2.97516 0.407944C3.30099 0.144089 3.70932 -7.24082e-05 4.13064 7.25899e-08H10.1318C10.4015 -7.55792e-05 10.6678 0.0589818 10.9116 0.172911C11.1554 0.286839 11.3705 0.453799 11.5415 0.659813C11.7124 0.864828 11.835 1.10576 11.9002 1.36423C11.9655 1.62269 11.9719 1.89225 11.9189 2.15344L9.61081 13.5538Z"
25
- />
26
- </svg>
27
- );
28
- }
29
-
30
11
  export default function PoweredBy({
31
12
  href = 'https://veludocs.com',
32
13
  className = '',
@@ -42,7 +23,6 @@ export default function PoweredBy({
42
23
  >
43
24
  <span className="velu-powered-by__label">Powered by</span>
44
25
  <span className="velu-powered-by__brand">
45
- <VeluMark />
46
26
  <span className="velu-powered-by__wordmark">Velu</span>
47
27
  </span>
48
28
  </a>
@@ -152,16 +152,28 @@ export default function Sidebar({
152
152
  );
153
153
 
154
154
  // Bring the active item into view inside the parent scroll container
155
- // whenever the active route changes e.g. after navigation, the
156
- // selected page is already visible without the user scrolling the
157
- // sidebar manually. Uses the shared util (instead of native
158
- // scrollIntoView) so `scroll-padding-*` on the parent — which the
159
- // docs layout uses to mark the footer-eclipsed band as off-limits —
160
- // is reliably honoured.
155
+ // whenever the active route changes. useLayoutEffect + a frame wait
156
+ // so nested <details> have opened and sticky headings have laid out
157
+ // before we measure otherwise a deep page can stay clipped.
161
158
  const rootRef = React.useRef(null);
162
- React.useEffect(() => {
163
- const el = rootRef.current?.querySelector('[aria-current="page"]');
164
- scrollIntoNearestView(el);
159
+ useIsoLayoutEffect(() => {
160
+ const nav = rootRef.current;
161
+ if (!nav) return undefined;
162
+ let cancelled = false;
163
+ const reveal = () => {
164
+ if (cancelled) return;
165
+ const el = nav.querySelector('[aria-current="page"]');
166
+ if (!el) return;
167
+ // Clear sticky section headings so the active row isn't tucked
168
+ // under a pinned title (honours scroll-padding on the scroller).
169
+ scrollIntoNearestView(el);
170
+ };
171
+ reveal();
172
+ const id = requestAnimationFrame(reveal);
173
+ return () => {
174
+ cancelled = true;
175
+ cancelAnimationFrame(id);
176
+ };
165
177
  }, [activeHref]);
166
178
 
167
179
  // The single gliding active indicator (crimson tint + flush-left bar). One