@veluai/velu 0.2.38 → 0.2.39
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.
- package/dist/cli.js +38 -38
- package/docs/aalam.md +44 -0
- package/docs/mintlify-migration.md +8 -3
- package/docs/thulir.md +23 -0
- package/package.json +4 -2
- package/runtime/velu-ui/base.css +9 -0
- package/runtime/velu-ui/components/ApiReferencePage.jsx +4 -3
- package/runtime/velu-ui/components/ApiSamples.jsx +9 -2
- package/runtime/velu-ui/components/Callout.jsx +4 -1
- package/runtime/velu-ui/components/PageFooter.jsx +3 -1
- package/runtime/velu-ui/components/PageHeader.jsx +7 -1
- package/runtime/velu-ui/components/PageNav.jsx +98 -22
- package/runtime/velu-ui/components/Sidebar.jsx +21 -9
- package/runtime/velu-ui/components/ThemeToggle.jsx +1 -0
- package/runtime/velu-ui/components/TocBar.jsx +21 -2
- package/runtime/velu-ui/components/VepaFooter.jsx +19 -17
- package/runtime/velu-ui/components/api-page.css +6 -3
- package/runtime/velu-ui/components/chatbot.css +5 -4
- package/runtime/velu-ui/components/docs-layout.css +99 -114
- package/runtime/velu-ui/components/page-footer.css +2 -3
- package/runtime/velu-ui/components/page-header.css +6 -0
- package/runtime/velu-ui/components/page-nav.css +192 -0
- package/runtime/velu-ui/components/sidebar.css +5 -13
- package/runtime/velu-ui/primitives/switcher.css +4 -0
- package/runtime/velu-ui/styles.css +2 -0
- package/runtime/velu-ui/themes/aalam.css +266 -0
- package/runtime/velu-ui/themes/thulir.css +428 -0
- package/schema/velu.schema.json +23 -3
- package/src/navigation.js +22 -3
- package/src/runtime/App.jsx +171 -106
- package/templates/starter/ai-tools/claude-code.mdx +1 -0
- package/templates/starter/ai-tools/cursor.mdx +1 -0
- package/templates/starter/api-reference/introduction.mdx +1 -0
- package/templates/starter/development.mdx +1 -0
- package/templates/starter/essentials/code.mdx +1 -0
- package/templates/starter/essentials/images.mdx +1 -0
- package/templates/starter/essentials/markdown.mdx +1 -0
- package/templates/starter/essentials/navigation.mdx +1 -0
- package/templates/starter/essentials/settings.mdx +1 -0
- package/templates/starter/index.mdx +1 -0
- 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,11 @@ 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
|
-
|
|
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
|
+
The presets are not a certification of pixel equality for every component or custom script.
|
|
116
120
|
|
|
117
|
-
Features without full Velu equivalents require review:
|
|
121
|
+
Features without full Velu equivalents require review: remaining Mintlify themes, advanced
|
|
118
122
|
fonts and styling, redirects, analytics/support integrations, hosting settings,
|
|
119
123
|
authentication, personalization, hidden-page routing, legacy version filtering,
|
|
120
124
|
complex dropdown/global navigation, AsyncAPI, manually authored `api:` pages,
|
|
@@ -137,6 +141,7 @@ Inspected on 2026-09-08:
|
|
|
137
141
|
- 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
142
|
- 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
143
|
- Published [`@mintlify/prebuild@1.0.1286`](https://www.npmjs.com/package/@mintlify/prebuild/v/1.0.1286): split config references and config loading.
|
|
144
|
+
- 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
145
|
- [`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
146
|
|
|
142
147
|
The current `@mintlify/validation@0.1.849` tarball was also retrieved, but it did
|
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.
|
|
3
|
+
"version": "0.2.39",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": "./dist/cli.js",
|
|
6
6
|
"publishConfig": {
|
|
@@ -18,7 +18,9 @@
|
|
|
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"
|
|
22
24
|
],
|
|
23
25
|
"scripts": {
|
|
24
26
|
"build": "node scripts/build.mjs",
|
package/runtime/velu-ui/base.css
CHANGED
|
@@ -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'].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'].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'].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'].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'].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'].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}>{
|
|
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
|
-
{
|
|
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}
|
|
@@ -77,7 +77,9 @@ export default function PageFooter({
|
|
|
77
77
|
...rest
|
|
78
78
|
}) {
|
|
79
79
|
const Link = linkComponent;
|
|
80
|
-
if (preset === 'vepa'
|
|
80
|
+
if (preset === 'vepa' || ['thulir', 'aalam'].includes(preset)) {
|
|
81
|
+
return <VepaFooter preset={preset} brand={brand} columns={columns} socials={socials} linkComponent={Link} />;
|
|
82
|
+
}
|
|
81
83
|
const brandLabel = brand?.label ?? 'Velu';
|
|
82
84
|
const brandHref = brand?.href;
|
|
83
85
|
|
|
@@ -243,6 +243,8 @@ export default function PageHeader({
|
|
|
243
243
|
activeTab,
|
|
244
244
|
breadcrumb,
|
|
245
245
|
onMenuClick,
|
|
246
|
+
menuOpen,
|
|
247
|
+
menuId,
|
|
246
248
|
linkComponent = 'a',
|
|
247
249
|
className = '',
|
|
248
250
|
...rest
|
|
@@ -290,7 +292,7 @@ export default function PageHeader({
|
|
|
290
292
|
return (
|
|
291
293
|
<Stack
|
|
292
294
|
as="header"
|
|
293
|
-
space={preset === 'vepa' ? '0px' : 'var(--s0)'}
|
|
295
|
+
space={preset === 'vepa' || ['thulir', 'aalam'].includes(preset) ? '0px' : 'var(--s0)'}
|
|
294
296
|
data-has-tabs={tabs.length > 0 ? 'true' : 'false'}
|
|
295
297
|
className={`velu-header${scrolled ? ' velu-header--scrolled' : ''}${
|
|
296
298
|
showTabsRow ? '' : ' velu-header--no-tabs'
|
|
@@ -391,6 +393,8 @@ export default function PageHeader({
|
|
|
391
393
|
data-component="breadcrumb-list"
|
|
392
394
|
onClick={onMenuClick}
|
|
393
395
|
aria-label="Open navigation menu"
|
|
396
|
+
aria-expanded={menuOpen}
|
|
397
|
+
aria-controls={menuId}
|
|
394
398
|
>
|
|
395
399
|
{breadcrumb.map((c, i) => {
|
|
396
400
|
const isLast = i === breadcrumb.length - 1;
|
|
@@ -432,6 +436,8 @@ export default function PageHeader({
|
|
|
432
436
|
type="button"
|
|
433
437
|
className="velu-header__menu"
|
|
434
438
|
aria-label="Open navigation menu"
|
|
439
|
+
aria-expanded={menuOpen}
|
|
440
|
+
aria-controls={menuId}
|
|
435
441
|
onClick={onMenuClick}
|
|
436
442
|
>
|
|
437
443
|
<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.
|
|
9
|
-
* start, a "‹ Previous" label) and the next page (flush to the inline
|
|
10
|
-
* end, "Next ›").
|
|
8
|
+
* page.
|
|
11
9
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
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
|
|
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
|
-
<
|
|
143
|
+
<DefaultCard page={prev} dir="prev" Link={linkComponent} />
|
|
68
144
|
) : (
|
|
69
145
|
<div aria-hidden="true" />
|
|
70
146
|
)}
|
|
71
147
|
{next ? (
|
|
72
|
-
<
|
|
148
|
+
<DefaultCard page={next} dir="next" Link={linkComponent} />
|
|
73
149
|
) : (
|
|
74
150
|
<div aria-hidden="true" />
|
|
75
151
|
)}
|
|
@@ -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
|
|
156
|
-
//
|
|
157
|
-
//
|
|
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
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
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
|
|
@@ -16,6 +16,7 @@ import ThemePreferenceMenu from './ThemePreferenceMenu.jsx';
|
|
|
16
16
|
* visits, per the anti-flash script in the template).
|
|
17
17
|
*/
|
|
18
18
|
export default function ThemeToggle({ className = '', variant, ...rest }) {
|
|
19
|
+
if (variant === 'inline') return <ThemePreferenceMenu inline />;
|
|
19
20
|
if (variant === 'menu') return <ThemePreferenceMenu />;
|
|
20
21
|
function toggle() {
|
|
21
22
|
const root = document.documentElement;
|
|
@@ -83,9 +83,11 @@ export default function TocBar({
|
|
|
83
83
|
activeId,
|
|
84
84
|
onSelect = () => {},
|
|
85
85
|
className = '',
|
|
86
|
+
label,
|
|
86
87
|
...rest
|
|
87
88
|
}) {
|
|
88
89
|
const [expanded, setExpanded] = React.useState(false);
|
|
90
|
+
const listId = React.useId();
|
|
89
91
|
|
|
90
92
|
// Reading progress through the page: scrollY / (scrollable-distance).
|
|
91
93
|
// Listens to window scroll + resize; clamped to [0, 1]. Stays at 0
|
|
@@ -138,6 +140,17 @@ export default function TocBar({
|
|
|
138
140
|
// right-rail Toc uses — keeps the active item visible if the list is
|
|
139
141
|
// long enough to scroll inside the bar.
|
|
140
142
|
const rootRef = React.useRef(null);
|
|
143
|
+
React.useEffect(() => {
|
|
144
|
+
if (!expanded) return;
|
|
145
|
+
const close = (event) => {
|
|
146
|
+
if (event.type === 'keydown' && event.key === 'Escape') {
|
|
147
|
+
setExpanded(false); rootRef.current?.querySelector('button')?.focus();
|
|
148
|
+
} else if (event.type === 'pointerdown' && !rootRef.current?.contains(event.target)) setExpanded(false);
|
|
149
|
+
};
|
|
150
|
+
document.addEventListener('keydown', close);
|
|
151
|
+
document.addEventListener('pointerdown', close);
|
|
152
|
+
return () => { document.removeEventListener('keydown', close); document.removeEventListener('pointerdown', close); };
|
|
153
|
+
}, [expanded]);
|
|
141
154
|
React.useEffect(() => {
|
|
142
155
|
if (!expanded || activeId == null) return;
|
|
143
156
|
const el = rootRef.current?.querySelector('[data-toc-bar-active="true"]');
|
|
@@ -156,10 +169,11 @@ export default function TocBar({
|
|
|
156
169
|
className="velu-toc-bar__toggle"
|
|
157
170
|
onClick={() => setExpanded((v) => !v)}
|
|
158
171
|
aria-expanded={expanded}
|
|
172
|
+
aria-controls={listId}
|
|
159
173
|
>
|
|
160
174
|
{/* Reading-progress ring — fills as the reader scrolls. */}
|
|
161
175
|
<ProgressRing progress={progress} />
|
|
162
|
-
<span className="velu-toc-bar__title">{headerLabel}</span>
|
|
176
|
+
<span className="velu-toc-bar__title">{label ?? headerLabel}</span>
|
|
163
177
|
<ChevronDown
|
|
164
178
|
className="velu-toc-bar__chevron"
|
|
165
179
|
aria-hidden="true"
|
|
@@ -171,7 +185,7 @@ export default function TocBar({
|
|
|
171
185
|
parent's `data-expanded` attribute via CSS, not by
|
|
172
186
|
conditional rendering. `aria-hidden` keeps the collapsed
|
|
173
187
|
list out of the accessibility tree. */}
|
|
174
|
-
<ul className="velu-toc-bar__list" aria-hidden={!expanded}>
|
|
188
|
+
<ul id={listId} className="velu-toc-bar__list" aria-hidden={!expanded}>
|
|
175
189
|
{flat.map((item) => {
|
|
176
190
|
const active = item.id === activeId;
|
|
177
191
|
return (
|
|
@@ -183,6 +197,11 @@ export default function TocBar({
|
|
|
183
197
|
}}
|
|
184
198
|
data-toc-bar-active={active ? 'true' : undefined}
|
|
185
199
|
onClick={() => handleSelect(item.id)}
|
|
200
|
+
role="link"
|
|
201
|
+
aria-current={active ? 'location' : undefined}
|
|
202
|
+
onKeyDown={(event) => {
|
|
203
|
+
if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); handleSelect(item.id); }
|
|
204
|
+
}}
|
|
186
205
|
tabIndex={expanded ? 0 : -1}
|
|
187
206
|
>
|
|
188
207
|
{item.label}
|
|
@@ -3,36 +3,38 @@ import Logo from './Logo.jsx';
|
|
|
3
3
|
import SocialLinks from './SocialLinks.jsx';
|
|
4
4
|
import ThemePreferenceMenu from './ThemePreferenceMenu.jsx';
|
|
5
5
|
|
|
6
|
-
export default function VepaFooter({ brand, columns = [], socials = [], linkComponent: Link = 'a' }) {
|
|
6
|
+
export default function VepaFooter({ preset = 'vepa', brand, columns = [], socials = [], linkComponent: Link = 'a' }) {
|
|
7
7
|
const single = columns.length === 1;
|
|
8
8
|
const completeHeadings = columns.every((c) => c.title);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
const root = ['thulir', 'aalam'].includes(preset) ? 'thulir-footer' : 'vepa-footer';
|
|
10
|
+
return <footer id="footer" className={root} data-layout={columns.length <= 3 ? 'compact' : 'expanded'} data-single={single} data-headings={completeHeadings}>
|
|
11
|
+
<div className={`${root}__inner`}>
|
|
12
|
+
<div className={`${root}__top`}>
|
|
13
|
+
<div className={`${root}__brand-column`}>
|
|
14
|
+
<Link href={brand?.href || '/'} className={`${root}__brand`}>
|
|
14
15
|
{brand?.logo ? <Logo logo={brand.logo} alt={brand.label} /> : <span>{brand?.label || 'Velu'}</span>}
|
|
15
16
|
</Link>
|
|
16
|
-
<SocialLinks socials={socials} className="
|
|
17
|
+
{['thulir', 'aalam'].includes(preset) && <SocialLinks socials={socials} className="thulir-footer__brand-mobile-socials" />}
|
|
18
|
+
<SocialLinks socials={socials} className={`${root}__brand-socials`} />
|
|
17
19
|
</div>
|
|
18
|
-
<div className=
|
|
19
|
-
{columns.map((column, i) => <div className=
|
|
20
|
-
<div className=
|
|
21
|
-
{column.title && !single && <p className=
|
|
20
|
+
<div className={`${root}__columns`} style={{ '--footer-columns': columns.length }}>
|
|
21
|
+
{columns.map((column, i) => <div className={`${root}__column`} key={i}>
|
|
22
|
+
<div className={`${root}__links`}>
|
|
23
|
+
{column.title && !single && <p className={`${root}__heading`}>{column.title}</p>}
|
|
22
24
|
{(column.items || []).map((item, j) => {
|
|
23
25
|
const external = /^https?:\/\//.test(item.href || '');
|
|
24
26
|
const Tag = external ? 'a' : Link;
|
|
25
|
-
return <Tag className=
|
|
27
|
+
return <Tag className={`${root}__link`} key={j} href={item.href || '#'} {...(external ? { target: '_blank', rel: 'noreferrer' } : {})}>{item.label}</Tag>;
|
|
26
28
|
})}
|
|
27
29
|
</div>
|
|
28
30
|
</div>)}
|
|
29
31
|
</div>
|
|
30
|
-
<SocialLinks socials={socials} className=
|
|
32
|
+
<SocialLinks socials={socials} className={`${root}__end-socials`} />
|
|
31
33
|
</div>
|
|
32
|
-
{!completeHeadings && <SocialLinks socials={socials} className=
|
|
33
|
-
<div className=
|
|
34
|
-
<div className=
|
|
35
|
-
<a href="https://veludocs.com" target="_blank" rel="noreferrer" className=
|
|
34
|
+
{!completeHeadings && <SocialLinks socials={socials} className={`${root}__mobile-socials`} />}
|
|
35
|
+
<div className={`${root}__divider`} />
|
|
36
|
+
<div className={`${root}__bottom`}>
|
|
37
|
+
<a href="https://veludocs.com" target="_blank" rel="noreferrer" className={`${root}__powered`}>Powered by <strong>Velu</strong></a>
|
|
36
38
|
<ThemePreferenceMenu inline />
|
|
37
39
|
</div>
|
|
38
40
|
</div>
|
|
@@ -71,12 +71,15 @@
|
|
|
71
71
|
font-size: var(--f-h6);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
/* Body + Header tabs — fixed height,
|
|
75
|
-
|
|
74
|
+
/* Body + Header tabs — fixed height, scrollable on both axes. Vertical
|
|
75
|
+
for long responses; horizontal so nowrap header keys stay reachable
|
|
76
|
+
when wider than the panel (document overflow-x is clipped to keep
|
|
77
|
+
sticky chrome from panning). */
|
|
76
78
|
.velu-api-resp-headers__scroll,
|
|
77
79
|
.velu-api-resp-body__scroll {
|
|
78
80
|
max-block-size: 15rem;
|
|
79
|
-
|
|
81
|
+
min-inline-size: 0;
|
|
82
|
+
overflow: auto;
|
|
80
83
|
}
|
|
81
84
|
/* The CodeBlock owns its own border/radius; let it fill the scroll box. */
|
|
82
85
|
.velu-api-resp-body__scroll > .velu-code-block {
|