@veluai/velu 0.2.13 → 0.2.15
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/README.md +80 -80
- package/dist/cli.js +37 -37
- package/package.json +64 -64
- package/runtime/velu-ui/base.css +320 -320
- package/runtime/velu-ui/components/Accordion.jsx +64 -64
- package/runtime/velu-ui/components/ApiClient.jsx +207 -207
- package/runtime/velu-ui/components/ApiField.jsx +87 -87
- package/runtime/velu-ui/components/ApiPath.jsx +63 -63
- package/runtime/velu-ui/components/ApiReferencePage.jsx +384 -384
- package/runtime/velu-ui/components/ApiSamples.jsx +36 -36
- package/runtime/velu-ui/components/ApiSidebar.jsx +122 -122
- package/runtime/velu-ui/components/AskBar.jsx +71 -71
- package/runtime/velu-ui/components/Callout.jsx +114 -114
- package/runtime/velu-ui/components/Card.jsx +131 -131
- package/runtime/velu-ui/components/Chatbot.jsx +885 -885
- package/runtime/velu-ui/components/CodeBlock.jsx +375 -375
- package/runtime/velu-ui/components/Columns.jsx +56 -56
- package/runtime/velu-ui/components/ContextMenu.jsx +298 -273
- package/runtime/velu-ui/components/ErrorCard.jsx +138 -138
- package/runtime/velu-ui/components/Field.jsx +81 -81
- package/runtime/velu-ui/components/Image.jsx +163 -163
- package/runtime/velu-ui/components/Logo.jsx +31 -31
- package/runtime/velu-ui/components/MethodBadge.jsx +31 -31
- package/runtime/velu-ui/components/NavSelect.jsx +108 -108
- package/runtime/velu-ui/components/NotFound.jsx +63 -63
- package/runtime/velu-ui/components/PageFeedback.jsx +219 -219
- package/runtime/velu-ui/components/PageFooter.jsx +145 -145
- package/runtime/velu-ui/components/PageHeader.jsx +422 -422
- package/runtime/velu-ui/components/PageNav.jsx +77 -77
- package/runtime/velu-ui/components/PoweredBy.jsx +51 -51
- package/runtime/velu-ui/components/Prompt.jsx +115 -115
- package/runtime/velu-ui/components/Search.jsx +460 -460
- package/runtime/velu-ui/components/Sidebar.jsx +254 -254
- package/runtime/velu-ui/components/SocialLinks.jsx +90 -90
- package/runtime/velu-ui/components/Steps.jsx +65 -65
- package/runtime/velu-ui/components/ThemeToggle.jsx +48 -48
- package/runtime/velu-ui/components/Toc.jsx +537 -537
- package/runtime/velu-ui/components/TocBar.jsx +195 -195
- package/runtime/velu-ui/components/Tree.jsx +87 -87
- package/runtime/velu-ui/components/TryItBar.jsx +102 -102
- package/runtime/velu-ui/components/accordion.css +92 -92
- package/runtime/velu-ui/components/api-page.css +208 -208
- package/runtime/velu-ui/components/api.css +635 -635
- package/runtime/velu-ui/components/ask-bar.css +94 -94
- package/runtime/velu-ui/components/card.css +105 -105
- package/runtime/velu-ui/components/chatbot.css +622 -622
- package/runtime/velu-ui/components/code-block.css +263 -263
- package/runtime/velu-ui/components/context-menu.css +173 -173
- package/runtime/velu-ui/components/docs-layout.css +822 -822
- package/runtime/velu-ui/components/field.css +82 -82
- package/runtime/velu-ui/components/image.css +237 -237
- package/runtime/velu-ui/components/nav-select.css +157 -157
- package/runtime/velu-ui/components/not-found.css +94 -94
- package/runtime/velu-ui/components/page-feedback.css +241 -241
- package/runtime/velu-ui/components/page-footer.css +130 -130
- package/runtime/velu-ui/components/page-header.css +558 -558
- package/runtime/velu-ui/components/page-nav.css +50 -50
- package/runtime/velu-ui/components/powered-by.css +92 -92
- package/runtime/velu-ui/components/prompt.css +99 -99
- package/runtime/velu-ui/components/search.css +307 -307
- package/runtime/velu-ui/components/sidebar.css +205 -205
- package/runtime/velu-ui/components/steps.css +77 -77
- package/runtime/velu-ui/components/theme-toggle.css +102 -102
- package/runtime/velu-ui/components/toc-bar.css +234 -234
- package/runtime/velu-ui/components/tree.css +49 -49
- package/runtime/velu-ui/index.js +54 -54
- package/runtime/velu-ui/lib/api-send.js +92 -92
- package/runtime/velu-ui/lib/brand-icons.jsx +103 -103
- package/runtime/velu-ui/lib/component-schemas.js +100 -100
- package/runtime/velu-ui/lib/copyText.js +64 -64
- package/runtime/velu-ui/lib/docs-assistant.js +250 -250
- package/runtime/velu-ui/lib/lang-icons.jsx +147 -147
- package/runtime/velu-ui/lib/pagefind.js +113 -113
- package/runtime/velu-ui/lib/prism-langs.js +957 -957
- package/runtime/velu-ui/lib/prism-loader.js +74 -74
- package/runtime/velu-ui/lib/resolveIcon.jsx +29 -29
- package/runtime/velu-ui/lib/scrollIntoNearestView.js +66 -66
- package/runtime/velu-ui/mdx-components.jsx +105 -105
- package/runtime/velu-ui/primitives/Cluster.jsx +49 -49
- package/runtime/velu-ui/primitives/Stack.jsx +63 -63
- package/runtime/velu-ui/primitives/Switcher.jsx +57 -57
- package/runtime/velu-ui/primitives/stack.css +3 -3
- package/runtime/velu-ui/primitives/switcher.css +25 -25
- package/runtime/velu-ui/styles.css +46 -46
- package/runtime/velu-ui/tokens.css +4 -4
- package/schema/velu.schema.json +423 -423
- package/src/lib/extract-mdx-error.js +170 -170
- package/src/lib/issues.js +159 -159
- package/src/lib/known-components.js +34 -34
- package/src/navigation.js +443 -443
- package/src/runtime/App.jsx +1669 -1668
- package/src/runtime/ErrorBoundary.jsx +54 -54
- package/src/runtime/client-entry.jsx +22 -22
- package/src/runtime/server-entry.jsx +16 -16
- package/src/template.html +48 -48
- package/templates/starter/ai-tools/claude-code.mdx +26 -26
- package/templates/starter/ai-tools/cursor.mdx +17 -17
- package/templates/starter/api-reference/introduction.mdx +43 -43
- package/templates/starter/development.mdx +19 -19
- package/templates/starter/essentials/code.mdx +29 -29
- package/templates/starter/essentials/images.mdx +29 -29
- package/templates/starter/essentials/markdown.mdx +25 -25
- package/templates/starter/essentials/navigation.mdx +39 -39
- package/templates/starter/essentials/settings.mdx +30 -30
- package/templates/starter/favicon.svg +6 -6
- package/templates/starter/index.mdx +31 -31
- package/templates/starter/openapi.json +160 -160
- package/templates/starter/quickstart.mdx +31 -31
- package/templates/starter/velu.json +41 -41
|
@@ -1,254 +1,254 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { ChevronRight, ExternalLink } from 'lucide-react';
|
|
3
|
-
import Stack from '../primitives/Stack.jsx';
|
|
4
|
-
import resolveIcon from '../lib/resolveIcon.jsx';
|
|
5
|
-
import scrollIntoNearestView from '../lib/scrollIntoNearestView.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Sidebar — docs navigation. Data-driven, recursive (arbitrary depth),
|
|
9
|
-
* tokenized, SSR-safe.
|
|
10
|
-
*
|
|
11
|
-
* Composition: ALL vertical rhythm is the Stack primitive (DRY) — faithful to
|
|
12
|
-
* the source design's flat structure:
|
|
13
|
-
* <Stack as="nav" space=--s1> ← headings + lists alternate here
|
|
14
|
-
* <h5 section/> (uniform --s1 between every child)
|
|
15
|
-
* <Stack as="ul" space=--s0> items </Stack>
|
|
16
|
-
* nested groups: <Stack as="ul" space="0"> (spacing via item padding)
|
|
17
|
-
* sidebar.css holds only what Stack cannot: rails, active state, item rows,
|
|
18
|
-
* chevron, summary, icon sizing.
|
|
19
|
-
*
|
|
20
|
-
* Icons (`section.icon` / `item.icon`) are **lucide icon id strings**
|
|
21
|
-
* (kebab-case), resolved by the shared resolveIcon util (a React node is
|
|
22
|
-
* also accepted). Size + stroke come from .velu-sidebar__icon CSS.
|
|
23
|
-
*
|
|
24
|
-
* Selection is ROUTE-DRIVEN (SSR-correct, reload-safe, URL-shareable): an
|
|
25
|
-
* item is active when its href === activeHref (or active:true). Ancestor
|
|
26
|
-
* groups of the active item auto-open. Router-agnostic via `linkComponent`
|
|
27
|
-
* (defaults to <a>); external items always render a real <a target=_blank>.
|
|
28
|
-
*
|
|
29
|
-
* @typedef {Object} SidebarItem
|
|
30
|
-
* @property {string} label
|
|
31
|
-
* @property {string} [href]
|
|
32
|
-
* @property {string|React.ReactNode} [icon] // lucide id or node
|
|
33
|
-
* @property {boolean} [active]
|
|
34
|
-
* @property {boolean} [external]
|
|
35
|
-
* @property {SidebarItem[]} [items]
|
|
36
|
-
*
|
|
37
|
-
* @param {{ sections: { title: string, icon?: string|React.ReactNode,
|
|
38
|
-
* items: SidebarItem[] }[], activeHref?: string,
|
|
39
|
-
* linkComponent?: React.ElementType, className?: string }} props
|
|
40
|
-
*/
|
|
41
|
-
|
|
42
|
-
const SidebarCtx = React.createContext({ activeHref: undefined, Link: 'a' });
|
|
43
|
-
|
|
44
|
-
// Positioning must happen before paint (no flash); fall back to useEffect on
|
|
45
|
-
// the server so React doesn't warn about useLayoutEffect during SSR.
|
|
46
|
-
const useIsoLayoutEffect =
|
|
47
|
-
typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
|
|
48
|
-
|
|
49
|
-
function isActive(item, activeHref) {
|
|
50
|
-
return Boolean(
|
|
51
|
-
item.active || (item.href != null && item.href === activeHref)
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function hasActiveDescendant(item, activeHref) {
|
|
56
|
-
if (isActive(item, activeHref)) return true;
|
|
57
|
-
return (
|
|
58
|
-
Array.isArray(item.items) &&
|
|
59
|
-
item.items.some((c) => hasActiveDescendant(c, activeHref))
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function Icon({ icon }) {
|
|
64
|
-
const node = resolveIcon(icon);
|
|
65
|
-
return node ? (
|
|
66
|
-
<span className="velu-sidebar__icon" aria-hidden="true">
|
|
67
|
-
{node}
|
|
68
|
-
</span>
|
|
69
|
-
) : null;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function Chevron() {
|
|
73
|
-
return (
|
|
74
|
-
<ChevronRight
|
|
75
|
-
className="velu-sidebar__chevron"
|
|
76
|
-
aria-hidden="true"
|
|
77
|
-
focusable="false"
|
|
78
|
-
/>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function ExternalIcon() {
|
|
83
|
-
return (
|
|
84
|
-
<ExternalLink
|
|
85
|
-
className="velu-sidebar__icon"
|
|
86
|
-
aria-hidden="true"
|
|
87
|
-
focusable="false"
|
|
88
|
-
/>
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Recursive node. depth 0 = top-level; depth >= 1 = nested (subitem style). */
|
|
93
|
-
function Node({ item, depth }) {
|
|
94
|
-
const { activeHref, Link } = React.useContext(SidebarCtx);
|
|
95
|
-
const { label, href = '#', icon, external, items, method } = item;
|
|
96
|
-
const base = depth === 0 ? 'velu-sidebar__item' : 'velu-sidebar__subitem';
|
|
97
|
-
|
|
98
|
-
if (items && items.length) {
|
|
99
|
-
return (
|
|
100
|
-
<li>
|
|
101
|
-
<details open={hasActiveDescendant(item, activeHref) || undefined}>
|
|
102
|
-
<summary className={`${base} velu-sidebar__summary`}>
|
|
103
|
-
<Icon icon={icon} />
|
|
104
|
-
<span className="velu-sidebar__label">{label}</span>
|
|
105
|
-
<Chevron />
|
|
106
|
-
</summary>
|
|
107
|
-
{/* nested list: Stack with no gap (spacing = item padding-block),
|
|
108
|
-
matching the source's stack-s */}
|
|
109
|
-
<Stack as="ul" space="0" className="velu-sidebar__sublist">
|
|
110
|
-
{items.map((child, i) => (
|
|
111
|
-
<Node key={i} item={child} depth={depth + 1} />
|
|
112
|
-
))}
|
|
113
|
-
</Stack>
|
|
114
|
-
</details>
|
|
115
|
-
</li>
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const active = isActive(item, activeHref);
|
|
120
|
-
const cls = base + (active ? ' velu-sidebar__item--active' : '');
|
|
121
|
-
const LinkTag = external ? 'a' : Link;
|
|
122
|
-
const linkProps = external
|
|
123
|
-
? { href, target: '_blank', rel: 'noreferrer' }
|
|
124
|
-
: { href };
|
|
125
|
-
|
|
126
|
-
return (
|
|
127
|
-
<li>
|
|
128
|
-
<LinkTag
|
|
129
|
-
className={cls}
|
|
130
|
-
aria-current={active ? 'page' : undefined}
|
|
131
|
-
{...linkProps}
|
|
132
|
-
>
|
|
133
|
-
{method ? (
|
|
134
|
-
<span
|
|
135
|
-
className={`velu-sidebar__method velu-method-badge--${String(method).toLowerCase()}`}
|
|
136
|
-
>
|
|
137
|
-
{method}
|
|
138
|
-
</span>
|
|
139
|
-
) : (
|
|
140
|
-
<Icon icon={icon} />
|
|
141
|
-
)}
|
|
142
|
-
<span className="velu-sidebar__label">{label}</span>
|
|
143
|
-
{external ? <ExternalIcon /> : null}
|
|
144
|
-
</LinkTag>
|
|
145
|
-
</li>
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export default function Sidebar({
|
|
150
|
-
sections = [],
|
|
151
|
-
activeHref,
|
|
152
|
-
linkComponent = 'a',
|
|
153
|
-
className = '',
|
|
154
|
-
...rest
|
|
155
|
-
}) {
|
|
156
|
-
const ctx = React.useMemo(
|
|
157
|
-
() => ({ activeHref, Link: linkComponent }),
|
|
158
|
-
[activeHref, linkComponent]
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
// Bring the active item into view inside the parent scroll container
|
|
162
|
-
// whenever the active route changes — e.g. after navigation, the
|
|
163
|
-
// selected page is already visible without the user scrolling the
|
|
164
|
-
// sidebar manually. Uses the shared util (instead of native
|
|
165
|
-
// scrollIntoView) so `scroll-padding-*` on the parent — which the
|
|
166
|
-
// docs layout uses to mark the footer-eclipsed band as off-limits —
|
|
167
|
-
// is reliably honoured.
|
|
168
|
-
const rootRef = React.useRef(null);
|
|
169
|
-
React.useEffect(() => {
|
|
170
|
-
const el = rootRef.current?.querySelector('[aria-current="page"]');
|
|
171
|
-
scrollIntoNearestView(el);
|
|
172
|
-
}, [activeHref]);
|
|
173
|
-
|
|
174
|
-
// The single gliding active indicator (crimson tint + flush-left bar). One
|
|
175
|
-
// element animates its position/height to the active item, so selection feels
|
|
176
|
-
// continuous rather than each item popping its own bar. It lives in the scroll
|
|
177
|
-
// content, so its offset is scroll-stable (no re-measure on scroll) and a
|
|
178
|
-
// pinned sticky heading's opaque background covers it when an item tucks under.
|
|
179
|
-
const [ind, setInd] = React.useState({ top: 0, h: 0, show: false });
|
|
180
|
-
const measure = React.useCallback(() => {
|
|
181
|
-
const nav = rootRef.current;
|
|
182
|
-
if (!nav) return;
|
|
183
|
-
const el = nav.querySelector('[aria-current="page"]');
|
|
184
|
-
if (!el) {
|
|
185
|
-
setInd((s) => (s.show ? { ...s, show: false } : s));
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
const top = el.getBoundingClientRect().top - nav.getBoundingClientRect().top;
|
|
189
|
-
const h = el.offsetHeight;
|
|
190
|
-
// Bail when nothing moved → no needless re-render (and no animation jitter).
|
|
191
|
-
setInd((s) =>
|
|
192
|
-
s.show && Math.abs(s.top - top) < 0.5 && Math.abs(s.h - h) < 0.5
|
|
193
|
-
? s
|
|
194
|
-
: { top, h, show: true },
|
|
195
|
-
);
|
|
196
|
-
}, []);
|
|
197
|
-
// Re-measure on selection / tab change…
|
|
198
|
-
useIsoLayoutEffect(() => {
|
|
199
|
-
measure();
|
|
200
|
-
}, [activeHref, sections, measure]);
|
|
201
|
-
// …and whenever the nav's layout changes (a group expands/collapses → the
|
|
202
|
-
// active item moves), plus on viewport resize.
|
|
203
|
-
React.useEffect(() => {
|
|
204
|
-
const nav = rootRef.current;
|
|
205
|
-
if (!nav || typeof ResizeObserver === 'undefined') return undefined;
|
|
206
|
-
const ro = new ResizeObserver(() => measure());
|
|
207
|
-
ro.observe(nav);
|
|
208
|
-
window.addEventListener('resize', measure);
|
|
209
|
-
return () => {
|
|
210
|
-
ro.disconnect();
|
|
211
|
-
window.removeEventListener('resize', measure);
|
|
212
|
-
};
|
|
213
|
-
}, [measure]);
|
|
214
|
-
|
|
215
|
-
return (
|
|
216
|
-
<SidebarCtx.Provider value={ctx}>
|
|
217
|
-
<Stack
|
|
218
|
-
ref={rootRef}
|
|
219
|
-
as="nav"
|
|
220
|
-
space="var(--s0)"
|
|
221
|
-
className={`velu-sidebar ${className}`.trim()}
|
|
222
|
-
aria-label="Documentation"
|
|
223
|
-
{...rest}
|
|
224
|
-
>
|
|
225
|
-
<span
|
|
226
|
-
className="velu-sidebar__indicator"
|
|
227
|
-
aria-hidden="true"
|
|
228
|
-
style={{
|
|
229
|
-
transform: `translateY(${ind.top}px)`,
|
|
230
|
-
height: `${ind.h}px`,
|
|
231
|
-
opacity: ind.show ? 1 : 0,
|
|
232
|
-
}}
|
|
233
|
-
/>
|
|
234
|
-
{/* Each section is its own Stack so the heading sits TIGHT to
|
|
235
|
-
its list (small inner gap), while the nav's larger gap
|
|
236
|
-
separates one section from the next — compact but still
|
|
237
|
-
visibly grouped. */}
|
|
238
|
-
{sections.map((section, i) => (
|
|
239
|
-
<Stack key={i} space="var(--s-4)">
|
|
240
|
-
<h5 className="velu-sidebar__section">
|
|
241
|
-
<Icon icon={section.icon} />
|
|
242
|
-
{section.title}
|
|
243
|
-
</h5>
|
|
244
|
-
<Stack as="ul" space="var(--s-4)" className="velu-sidebar__list">
|
|
245
|
-
{section.items.map((it, j) => (
|
|
246
|
-
<Node key={j} item={it} depth={0} />
|
|
247
|
-
))}
|
|
248
|
-
</Stack>
|
|
249
|
-
</Stack>
|
|
250
|
-
))}
|
|
251
|
-
</Stack>
|
|
252
|
-
</SidebarCtx.Provider>
|
|
253
|
-
);
|
|
254
|
-
}
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChevronRight, ExternalLink } from 'lucide-react';
|
|
3
|
+
import Stack from '../primitives/Stack.jsx';
|
|
4
|
+
import resolveIcon from '../lib/resolveIcon.jsx';
|
|
5
|
+
import scrollIntoNearestView from '../lib/scrollIntoNearestView.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sidebar — docs navigation. Data-driven, recursive (arbitrary depth),
|
|
9
|
+
* tokenized, SSR-safe.
|
|
10
|
+
*
|
|
11
|
+
* Composition: ALL vertical rhythm is the Stack primitive (DRY) — faithful to
|
|
12
|
+
* the source design's flat structure:
|
|
13
|
+
* <Stack as="nav" space=--s1> ← headings + lists alternate here
|
|
14
|
+
* <h5 section/> (uniform --s1 between every child)
|
|
15
|
+
* <Stack as="ul" space=--s0> items </Stack>
|
|
16
|
+
* nested groups: <Stack as="ul" space="0"> (spacing via item padding)
|
|
17
|
+
* sidebar.css holds only what Stack cannot: rails, active state, item rows,
|
|
18
|
+
* chevron, summary, icon sizing.
|
|
19
|
+
*
|
|
20
|
+
* Icons (`section.icon` / `item.icon`) are **lucide icon id strings**
|
|
21
|
+
* (kebab-case), resolved by the shared resolveIcon util (a React node is
|
|
22
|
+
* also accepted). Size + stroke come from .velu-sidebar__icon CSS.
|
|
23
|
+
*
|
|
24
|
+
* Selection is ROUTE-DRIVEN (SSR-correct, reload-safe, URL-shareable): an
|
|
25
|
+
* item is active when its href === activeHref (or active:true). Ancestor
|
|
26
|
+
* groups of the active item auto-open. Router-agnostic via `linkComponent`
|
|
27
|
+
* (defaults to <a>); external items always render a real <a target=_blank>.
|
|
28
|
+
*
|
|
29
|
+
* @typedef {Object} SidebarItem
|
|
30
|
+
* @property {string} label
|
|
31
|
+
* @property {string} [href]
|
|
32
|
+
* @property {string|React.ReactNode} [icon] // lucide id or node
|
|
33
|
+
* @property {boolean} [active]
|
|
34
|
+
* @property {boolean} [external]
|
|
35
|
+
* @property {SidebarItem[]} [items]
|
|
36
|
+
*
|
|
37
|
+
* @param {{ sections: { title: string, icon?: string|React.ReactNode,
|
|
38
|
+
* items: SidebarItem[] }[], activeHref?: string,
|
|
39
|
+
* linkComponent?: React.ElementType, className?: string }} props
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
const SidebarCtx = React.createContext({ activeHref: undefined, Link: 'a' });
|
|
43
|
+
|
|
44
|
+
// Positioning must happen before paint (no flash); fall back to useEffect on
|
|
45
|
+
// the server so React doesn't warn about useLayoutEffect during SSR.
|
|
46
|
+
const useIsoLayoutEffect =
|
|
47
|
+
typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
|
|
48
|
+
|
|
49
|
+
function isActive(item, activeHref) {
|
|
50
|
+
return Boolean(
|
|
51
|
+
item.active || (item.href != null && item.href === activeHref)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function hasActiveDescendant(item, activeHref) {
|
|
56
|
+
if (isActive(item, activeHref)) return true;
|
|
57
|
+
return (
|
|
58
|
+
Array.isArray(item.items) &&
|
|
59
|
+
item.items.some((c) => hasActiveDescendant(c, activeHref))
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function Icon({ icon }) {
|
|
64
|
+
const node = resolveIcon(icon);
|
|
65
|
+
return node ? (
|
|
66
|
+
<span className="velu-sidebar__icon" aria-hidden="true">
|
|
67
|
+
{node}
|
|
68
|
+
</span>
|
|
69
|
+
) : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function Chevron() {
|
|
73
|
+
return (
|
|
74
|
+
<ChevronRight
|
|
75
|
+
className="velu-sidebar__chevron"
|
|
76
|
+
aria-hidden="true"
|
|
77
|
+
focusable="false"
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ExternalIcon() {
|
|
83
|
+
return (
|
|
84
|
+
<ExternalLink
|
|
85
|
+
className="velu-sidebar__icon"
|
|
86
|
+
aria-hidden="true"
|
|
87
|
+
focusable="false"
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Recursive node. depth 0 = top-level; depth >= 1 = nested (subitem style). */
|
|
93
|
+
function Node({ item, depth }) {
|
|
94
|
+
const { activeHref, Link } = React.useContext(SidebarCtx);
|
|
95
|
+
const { label, href = '#', icon, external, items, method } = item;
|
|
96
|
+
const base = depth === 0 ? 'velu-sidebar__item' : 'velu-sidebar__subitem';
|
|
97
|
+
|
|
98
|
+
if (items && items.length) {
|
|
99
|
+
return (
|
|
100
|
+
<li>
|
|
101
|
+
<details open={hasActiveDescendant(item, activeHref) || undefined}>
|
|
102
|
+
<summary className={`${base} velu-sidebar__summary`}>
|
|
103
|
+
<Icon icon={icon} />
|
|
104
|
+
<span className="velu-sidebar__label">{label}</span>
|
|
105
|
+
<Chevron />
|
|
106
|
+
</summary>
|
|
107
|
+
{/* nested list: Stack with no gap (spacing = item padding-block),
|
|
108
|
+
matching the source's stack-s */}
|
|
109
|
+
<Stack as="ul" space="0" className="velu-sidebar__sublist">
|
|
110
|
+
{items.map((child, i) => (
|
|
111
|
+
<Node key={i} item={child} depth={depth + 1} />
|
|
112
|
+
))}
|
|
113
|
+
</Stack>
|
|
114
|
+
</details>
|
|
115
|
+
</li>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const active = isActive(item, activeHref);
|
|
120
|
+
const cls = base + (active ? ' velu-sidebar__item--active' : '');
|
|
121
|
+
const LinkTag = external ? 'a' : Link;
|
|
122
|
+
const linkProps = external
|
|
123
|
+
? { href, target: '_blank', rel: 'noreferrer' }
|
|
124
|
+
: { href };
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<li>
|
|
128
|
+
<LinkTag
|
|
129
|
+
className={cls}
|
|
130
|
+
aria-current={active ? 'page' : undefined}
|
|
131
|
+
{...linkProps}
|
|
132
|
+
>
|
|
133
|
+
{method ? (
|
|
134
|
+
<span
|
|
135
|
+
className={`velu-sidebar__method velu-method-badge--${String(method).toLowerCase()}`}
|
|
136
|
+
>
|
|
137
|
+
{method}
|
|
138
|
+
</span>
|
|
139
|
+
) : (
|
|
140
|
+
<Icon icon={icon} />
|
|
141
|
+
)}
|
|
142
|
+
<span className="velu-sidebar__label">{label}</span>
|
|
143
|
+
{external ? <ExternalIcon /> : null}
|
|
144
|
+
</LinkTag>
|
|
145
|
+
</li>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default function Sidebar({
|
|
150
|
+
sections = [],
|
|
151
|
+
activeHref,
|
|
152
|
+
linkComponent = 'a',
|
|
153
|
+
className = '',
|
|
154
|
+
...rest
|
|
155
|
+
}) {
|
|
156
|
+
const ctx = React.useMemo(
|
|
157
|
+
() => ({ activeHref, Link: linkComponent }),
|
|
158
|
+
[activeHref, linkComponent]
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Bring the active item into view inside the parent scroll container
|
|
162
|
+
// whenever the active route changes — e.g. after navigation, the
|
|
163
|
+
// selected page is already visible without the user scrolling the
|
|
164
|
+
// sidebar manually. Uses the shared util (instead of native
|
|
165
|
+
// scrollIntoView) so `scroll-padding-*` on the parent — which the
|
|
166
|
+
// docs layout uses to mark the footer-eclipsed band as off-limits —
|
|
167
|
+
// is reliably honoured.
|
|
168
|
+
const rootRef = React.useRef(null);
|
|
169
|
+
React.useEffect(() => {
|
|
170
|
+
const el = rootRef.current?.querySelector('[aria-current="page"]');
|
|
171
|
+
scrollIntoNearestView(el);
|
|
172
|
+
}, [activeHref]);
|
|
173
|
+
|
|
174
|
+
// The single gliding active indicator (crimson tint + flush-left bar). One
|
|
175
|
+
// element animates its position/height to the active item, so selection feels
|
|
176
|
+
// continuous rather than each item popping its own bar. It lives in the scroll
|
|
177
|
+
// content, so its offset is scroll-stable (no re-measure on scroll) and a
|
|
178
|
+
// pinned sticky heading's opaque background covers it when an item tucks under.
|
|
179
|
+
const [ind, setInd] = React.useState({ top: 0, h: 0, show: false });
|
|
180
|
+
const measure = React.useCallback(() => {
|
|
181
|
+
const nav = rootRef.current;
|
|
182
|
+
if (!nav) return;
|
|
183
|
+
const el = nav.querySelector('[aria-current="page"]');
|
|
184
|
+
if (!el) {
|
|
185
|
+
setInd((s) => (s.show ? { ...s, show: false } : s));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const top = el.getBoundingClientRect().top - nav.getBoundingClientRect().top;
|
|
189
|
+
const h = el.offsetHeight;
|
|
190
|
+
// Bail when nothing moved → no needless re-render (and no animation jitter).
|
|
191
|
+
setInd((s) =>
|
|
192
|
+
s.show && Math.abs(s.top - top) < 0.5 && Math.abs(s.h - h) < 0.5
|
|
193
|
+
? s
|
|
194
|
+
: { top, h, show: true },
|
|
195
|
+
);
|
|
196
|
+
}, []);
|
|
197
|
+
// Re-measure on selection / tab change…
|
|
198
|
+
useIsoLayoutEffect(() => {
|
|
199
|
+
measure();
|
|
200
|
+
}, [activeHref, sections, measure]);
|
|
201
|
+
// …and whenever the nav's layout changes (a group expands/collapses → the
|
|
202
|
+
// active item moves), plus on viewport resize.
|
|
203
|
+
React.useEffect(() => {
|
|
204
|
+
const nav = rootRef.current;
|
|
205
|
+
if (!nav || typeof ResizeObserver === 'undefined') return undefined;
|
|
206
|
+
const ro = new ResizeObserver(() => measure());
|
|
207
|
+
ro.observe(nav);
|
|
208
|
+
window.addEventListener('resize', measure);
|
|
209
|
+
return () => {
|
|
210
|
+
ro.disconnect();
|
|
211
|
+
window.removeEventListener('resize', measure);
|
|
212
|
+
};
|
|
213
|
+
}, [measure]);
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<SidebarCtx.Provider value={ctx}>
|
|
217
|
+
<Stack
|
|
218
|
+
ref={rootRef}
|
|
219
|
+
as="nav"
|
|
220
|
+
space="var(--s0)"
|
|
221
|
+
className={`velu-sidebar ${className}`.trim()}
|
|
222
|
+
aria-label="Documentation"
|
|
223
|
+
{...rest}
|
|
224
|
+
>
|
|
225
|
+
<span
|
|
226
|
+
className="velu-sidebar__indicator"
|
|
227
|
+
aria-hidden="true"
|
|
228
|
+
style={{
|
|
229
|
+
transform: `translateY(${ind.top}px)`,
|
|
230
|
+
height: `${ind.h}px`,
|
|
231
|
+
opacity: ind.show ? 1 : 0,
|
|
232
|
+
}}
|
|
233
|
+
/>
|
|
234
|
+
{/* Each section is its own Stack so the heading sits TIGHT to
|
|
235
|
+
its list (small inner gap), while the nav's larger gap
|
|
236
|
+
separates one section from the next — compact but still
|
|
237
|
+
visibly grouped. */}
|
|
238
|
+
{sections.map((section, i) => (
|
|
239
|
+
<Stack key={i} space="var(--s-4)">
|
|
240
|
+
<h5 className="velu-sidebar__section">
|
|
241
|
+
<Icon icon={section.icon} />
|
|
242
|
+
{section.title}
|
|
243
|
+
</h5>
|
|
244
|
+
<Stack as="ul" space="var(--s-4)" className="velu-sidebar__list">
|
|
245
|
+
{section.items.map((it, j) => (
|
|
246
|
+
<Node key={j} item={it} depth={0} />
|
|
247
|
+
))}
|
|
248
|
+
</Stack>
|
|
249
|
+
</Stack>
|
|
250
|
+
))}
|
|
251
|
+
</Stack>
|
|
252
|
+
</SidebarCtx.Provider>
|
|
253
|
+
);
|
|
254
|
+
}
|