@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.
Files changed (109) hide show
  1. package/README.md +80 -80
  2. package/dist/cli.js +37 -37
  3. package/package.json +64 -64
  4. package/runtime/velu-ui/base.css +320 -320
  5. package/runtime/velu-ui/components/Accordion.jsx +64 -64
  6. package/runtime/velu-ui/components/ApiClient.jsx +207 -207
  7. package/runtime/velu-ui/components/ApiField.jsx +87 -87
  8. package/runtime/velu-ui/components/ApiPath.jsx +63 -63
  9. package/runtime/velu-ui/components/ApiReferencePage.jsx +384 -384
  10. package/runtime/velu-ui/components/ApiSamples.jsx +36 -36
  11. package/runtime/velu-ui/components/ApiSidebar.jsx +122 -122
  12. package/runtime/velu-ui/components/AskBar.jsx +71 -71
  13. package/runtime/velu-ui/components/Callout.jsx +114 -114
  14. package/runtime/velu-ui/components/Card.jsx +131 -131
  15. package/runtime/velu-ui/components/Chatbot.jsx +885 -885
  16. package/runtime/velu-ui/components/CodeBlock.jsx +375 -375
  17. package/runtime/velu-ui/components/Columns.jsx +56 -56
  18. package/runtime/velu-ui/components/ContextMenu.jsx +298 -273
  19. package/runtime/velu-ui/components/ErrorCard.jsx +138 -138
  20. package/runtime/velu-ui/components/Field.jsx +81 -81
  21. package/runtime/velu-ui/components/Image.jsx +163 -163
  22. package/runtime/velu-ui/components/Logo.jsx +31 -31
  23. package/runtime/velu-ui/components/MethodBadge.jsx +31 -31
  24. package/runtime/velu-ui/components/NavSelect.jsx +108 -108
  25. package/runtime/velu-ui/components/NotFound.jsx +63 -63
  26. package/runtime/velu-ui/components/PageFeedback.jsx +219 -219
  27. package/runtime/velu-ui/components/PageFooter.jsx +145 -145
  28. package/runtime/velu-ui/components/PageHeader.jsx +422 -422
  29. package/runtime/velu-ui/components/PageNav.jsx +77 -77
  30. package/runtime/velu-ui/components/PoweredBy.jsx +51 -51
  31. package/runtime/velu-ui/components/Prompt.jsx +115 -115
  32. package/runtime/velu-ui/components/Search.jsx +460 -460
  33. package/runtime/velu-ui/components/Sidebar.jsx +254 -254
  34. package/runtime/velu-ui/components/SocialLinks.jsx +90 -90
  35. package/runtime/velu-ui/components/Steps.jsx +65 -65
  36. package/runtime/velu-ui/components/ThemeToggle.jsx +48 -48
  37. package/runtime/velu-ui/components/Toc.jsx +537 -537
  38. package/runtime/velu-ui/components/TocBar.jsx +195 -195
  39. package/runtime/velu-ui/components/Tree.jsx +87 -87
  40. package/runtime/velu-ui/components/TryItBar.jsx +102 -102
  41. package/runtime/velu-ui/components/accordion.css +92 -92
  42. package/runtime/velu-ui/components/api-page.css +208 -208
  43. package/runtime/velu-ui/components/api.css +635 -635
  44. package/runtime/velu-ui/components/ask-bar.css +94 -94
  45. package/runtime/velu-ui/components/card.css +105 -105
  46. package/runtime/velu-ui/components/chatbot.css +622 -622
  47. package/runtime/velu-ui/components/code-block.css +263 -263
  48. package/runtime/velu-ui/components/context-menu.css +173 -173
  49. package/runtime/velu-ui/components/docs-layout.css +822 -822
  50. package/runtime/velu-ui/components/field.css +82 -82
  51. package/runtime/velu-ui/components/image.css +237 -237
  52. package/runtime/velu-ui/components/nav-select.css +157 -157
  53. package/runtime/velu-ui/components/not-found.css +94 -94
  54. package/runtime/velu-ui/components/page-feedback.css +241 -241
  55. package/runtime/velu-ui/components/page-footer.css +130 -130
  56. package/runtime/velu-ui/components/page-header.css +558 -558
  57. package/runtime/velu-ui/components/page-nav.css +50 -50
  58. package/runtime/velu-ui/components/powered-by.css +92 -92
  59. package/runtime/velu-ui/components/prompt.css +99 -99
  60. package/runtime/velu-ui/components/search.css +307 -307
  61. package/runtime/velu-ui/components/sidebar.css +205 -205
  62. package/runtime/velu-ui/components/steps.css +77 -77
  63. package/runtime/velu-ui/components/theme-toggle.css +102 -102
  64. package/runtime/velu-ui/components/toc-bar.css +234 -234
  65. package/runtime/velu-ui/components/tree.css +49 -49
  66. package/runtime/velu-ui/index.js +54 -54
  67. package/runtime/velu-ui/lib/api-send.js +92 -92
  68. package/runtime/velu-ui/lib/brand-icons.jsx +103 -103
  69. package/runtime/velu-ui/lib/component-schemas.js +100 -100
  70. package/runtime/velu-ui/lib/copyText.js +64 -64
  71. package/runtime/velu-ui/lib/docs-assistant.js +250 -250
  72. package/runtime/velu-ui/lib/lang-icons.jsx +147 -147
  73. package/runtime/velu-ui/lib/pagefind.js +113 -113
  74. package/runtime/velu-ui/lib/prism-langs.js +957 -957
  75. package/runtime/velu-ui/lib/prism-loader.js +74 -74
  76. package/runtime/velu-ui/lib/resolveIcon.jsx +29 -29
  77. package/runtime/velu-ui/lib/scrollIntoNearestView.js +66 -66
  78. package/runtime/velu-ui/mdx-components.jsx +105 -105
  79. package/runtime/velu-ui/primitives/Cluster.jsx +49 -49
  80. package/runtime/velu-ui/primitives/Stack.jsx +63 -63
  81. package/runtime/velu-ui/primitives/Switcher.jsx +57 -57
  82. package/runtime/velu-ui/primitives/stack.css +3 -3
  83. package/runtime/velu-ui/primitives/switcher.css +25 -25
  84. package/runtime/velu-ui/styles.css +46 -46
  85. package/runtime/velu-ui/tokens.css +4 -4
  86. package/schema/velu.schema.json +423 -423
  87. package/src/lib/extract-mdx-error.js +170 -170
  88. package/src/lib/issues.js +159 -159
  89. package/src/lib/known-components.js +34 -34
  90. package/src/navigation.js +443 -443
  91. package/src/runtime/App.jsx +1669 -1668
  92. package/src/runtime/ErrorBoundary.jsx +54 -54
  93. package/src/runtime/client-entry.jsx +22 -22
  94. package/src/runtime/server-entry.jsx +16 -16
  95. package/src/template.html +48 -48
  96. package/templates/starter/ai-tools/claude-code.mdx +26 -26
  97. package/templates/starter/ai-tools/cursor.mdx +17 -17
  98. package/templates/starter/api-reference/introduction.mdx +43 -43
  99. package/templates/starter/development.mdx +19 -19
  100. package/templates/starter/essentials/code.mdx +29 -29
  101. package/templates/starter/essentials/images.mdx +29 -29
  102. package/templates/starter/essentials/markdown.mdx +25 -25
  103. package/templates/starter/essentials/navigation.mdx +39 -39
  104. package/templates/starter/essentials/settings.mdx +30 -30
  105. package/templates/starter/favicon.svg +6 -6
  106. package/templates/starter/index.mdx +31 -31
  107. package/templates/starter/openapi.json +160 -160
  108. package/templates/starter/quickstart.mdx +31 -31
  109. 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
+ }