@veluai/velu 0.2.25 → 0.2.27

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 (114) hide show
  1. package/README.md +80 -80
  2. package/dist/cli.js +44 -44
  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 +125 -125
  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/ChangelogFilters.jsx +75 -75
  16. package/runtime/velu-ui/components/Chatbot.jsx +972 -972
  17. package/runtime/velu-ui/components/CodeBlock.jsx +375 -375
  18. package/runtime/velu-ui/components/Columns.jsx +56 -56
  19. package/runtime/velu-ui/components/ContextMenu.jsx +327 -327
  20. package/runtime/velu-ui/components/ErrorCard.jsx +138 -138
  21. package/runtime/velu-ui/components/Field.jsx +81 -81
  22. package/runtime/velu-ui/components/Image.jsx +163 -163
  23. package/runtime/velu-ui/components/Logo.jsx +31 -31
  24. package/runtime/velu-ui/components/MethodBadge.jsx +31 -31
  25. package/runtime/velu-ui/components/NavSelect.jsx +108 -108
  26. package/runtime/velu-ui/components/NotFound.jsx +63 -63
  27. package/runtime/velu-ui/components/PageFeedback.jsx +219 -219
  28. package/runtime/velu-ui/components/PageFooter.jsx +145 -145
  29. package/runtime/velu-ui/components/PageHeader.jsx +422 -422
  30. package/runtime/velu-ui/components/PageNav.jsx +77 -77
  31. package/runtime/velu-ui/components/PoweredBy.jsx +51 -51
  32. package/runtime/velu-ui/components/Prompt.jsx +115 -115
  33. package/runtime/velu-ui/components/Search.jsx +460 -460
  34. package/runtime/velu-ui/components/Sidebar.jsx +246 -246
  35. package/runtime/velu-ui/components/SocialLinks.jsx +90 -90
  36. package/runtime/velu-ui/components/Steps.jsx +65 -65
  37. package/runtime/velu-ui/components/ThemeToggle.jsx +48 -48
  38. package/runtime/velu-ui/components/Toc.jsx +537 -537
  39. package/runtime/velu-ui/components/TocBar.jsx +195 -195
  40. package/runtime/velu-ui/components/Tree.jsx +87 -87
  41. package/runtime/velu-ui/components/TryItBar.jsx +102 -102
  42. package/runtime/velu-ui/components/Update.jsx +92 -92
  43. package/runtime/velu-ui/components/accordion.css +92 -92
  44. package/runtime/velu-ui/components/api-page.css +208 -208
  45. package/runtime/velu-ui/components/api.css +635 -635
  46. package/runtime/velu-ui/components/ask-bar.css +103 -103
  47. package/runtime/velu-ui/components/card.css +105 -105
  48. package/runtime/velu-ui/components/changelog-filters.css +43 -43
  49. package/runtime/velu-ui/components/chatbot.css +687 -687
  50. package/runtime/velu-ui/components/code-block.css +263 -263
  51. package/runtime/velu-ui/components/context-menu.css +199 -199
  52. package/runtime/velu-ui/components/docs-layout.css +822 -822
  53. package/runtime/velu-ui/components/field.css +82 -82
  54. package/runtime/velu-ui/components/image.css +237 -237
  55. package/runtime/velu-ui/components/nav-select.css +157 -157
  56. package/runtime/velu-ui/components/not-found.css +94 -94
  57. package/runtime/velu-ui/components/page-feedback.css +261 -261
  58. package/runtime/velu-ui/components/page-footer.css +130 -130
  59. package/runtime/velu-ui/components/page-header.css +558 -558
  60. package/runtime/velu-ui/components/page-nav.css +50 -50
  61. package/runtime/velu-ui/components/powered-by.css +92 -92
  62. package/runtime/velu-ui/components/prompt.css +99 -99
  63. package/runtime/velu-ui/components/search.css +307 -307
  64. package/runtime/velu-ui/components/sidebar.css +205 -205
  65. package/runtime/velu-ui/components/steps.css +77 -77
  66. package/runtime/velu-ui/components/theme-toggle.css +102 -102
  67. package/runtime/velu-ui/components/toc-bar.css +234 -234
  68. package/runtime/velu-ui/components/tree.css +49 -49
  69. package/runtime/velu-ui/components/update.css +111 -111
  70. package/runtime/velu-ui/index.js +58 -58
  71. package/runtime/velu-ui/lib/api-send.js +118 -118
  72. package/runtime/velu-ui/lib/brand-icons.jsx +103 -103
  73. package/runtime/velu-ui/lib/component-schemas.js +100 -100
  74. package/runtime/velu-ui/lib/copyText.js +64 -64
  75. package/runtime/velu-ui/lib/docs-assistant.js +273 -273
  76. package/runtime/velu-ui/lib/lang-icons.jsx +147 -147
  77. package/runtime/velu-ui/lib/page-feedback.js +72 -72
  78. package/runtime/velu-ui/lib/pagefind.js +116 -116
  79. package/runtime/velu-ui/lib/prism-langs.js +957 -957
  80. package/runtime/velu-ui/lib/prism-loader.js +74 -74
  81. package/runtime/velu-ui/lib/resolveIcon.jsx +29 -29
  82. package/runtime/velu-ui/lib/scrollIntoNearestView.js +66 -66
  83. package/runtime/velu-ui/mdx-components.jsx +107 -107
  84. package/runtime/velu-ui/primitives/Cluster.jsx +49 -49
  85. package/runtime/velu-ui/primitives/Stack.jsx +63 -63
  86. package/runtime/velu-ui/primitives/Switcher.jsx +57 -57
  87. package/runtime/velu-ui/primitives/stack.css +3 -3
  88. package/runtime/velu-ui/primitives/switcher.css +25 -25
  89. package/runtime/velu-ui/styles.css +48 -48
  90. package/runtime/velu-ui/tokens.css +4 -4
  91. package/schema/velu.schema.json +419 -419
  92. package/src/lib/extract-mdx-error.js +170 -170
  93. package/src/lib/issues.js +159 -159
  94. package/src/lib/known-components.js +35 -35
  95. package/src/navigation.js +459 -459
  96. package/src/runtime/App.jsx +1754 -1754
  97. package/src/runtime/ErrorBoundary.jsx +54 -54
  98. package/src/runtime/client-entry.jsx +27 -27
  99. package/src/runtime/server-entry.jsx +24 -24
  100. package/src/template.html +48 -48
  101. package/templates/starter/ai-tools/claude-code.mdx +26 -26
  102. package/templates/starter/ai-tools/cursor.mdx +17 -17
  103. package/templates/starter/api-reference/introduction.mdx +43 -43
  104. package/templates/starter/development.mdx +19 -19
  105. package/templates/starter/essentials/code.mdx +29 -29
  106. package/templates/starter/essentials/images.mdx +29 -29
  107. package/templates/starter/essentials/markdown.mdx +25 -25
  108. package/templates/starter/essentials/navigation.mdx +39 -39
  109. package/templates/starter/essentials/settings.mdx +30 -30
  110. package/templates/starter/favicon.svg +6 -6
  111. package/templates/starter/index.mdx +31 -31
  112. package/templates/starter/openapi.json +160 -160
  113. package/templates/starter/quickstart.mdx +31 -31
  114. package/templates/starter/velu.json +40 -40
@@ -1,246 +1,246 @@
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 } = 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
- <Icon icon={icon} />
134
- <span className="velu-sidebar__label">{label}</span>
135
- {external ? <ExternalIcon /> : null}
136
- </LinkTag>
137
- </li>
138
- );
139
- }
140
-
141
- export default function Sidebar({
142
- sections = [],
143
- activeHref,
144
- linkComponent = 'a',
145
- className = '',
146
- ...rest
147
- }) {
148
- const ctx = React.useMemo(
149
- () => ({ activeHref, Link: linkComponent }),
150
- [activeHref, linkComponent]
151
- );
152
-
153
- // Bring the active item into view inside the parent scroll container
154
- // whenever the active route changes — e.g. after navigation, the
155
- // selected page is already visible without the user scrolling the
156
- // sidebar manually. Uses the shared util (instead of native
157
- // scrollIntoView) so `scroll-padding-*` on the parent — which the
158
- // docs layout uses to mark the footer-eclipsed band as off-limits —
159
- // is reliably honoured.
160
- const rootRef = React.useRef(null);
161
- React.useEffect(() => {
162
- const el = rootRef.current?.querySelector('[aria-current="page"]');
163
- scrollIntoNearestView(el);
164
- }, [activeHref]);
165
-
166
- // The single gliding active indicator (crimson tint + flush-left bar). One
167
- // element animates its position/height to the active item, so selection feels
168
- // continuous rather than each item popping its own bar. It lives in the scroll
169
- // content, so its offset is scroll-stable (no re-measure on scroll) and a
170
- // pinned sticky heading's opaque background covers it when an item tucks under.
171
- const [ind, setInd] = React.useState({ top: 0, h: 0, show: false });
172
- const measure = React.useCallback(() => {
173
- const nav = rootRef.current;
174
- if (!nav) return;
175
- const el = nav.querySelector('[aria-current="page"]');
176
- if (!el) {
177
- setInd((s) => (s.show ? { ...s, show: false } : s));
178
- return;
179
- }
180
- const top = el.getBoundingClientRect().top - nav.getBoundingClientRect().top;
181
- const h = el.offsetHeight;
182
- // Bail when nothing moved → no needless re-render (and no animation jitter).
183
- setInd((s) =>
184
- s.show && Math.abs(s.top - top) < 0.5 && Math.abs(s.h - h) < 0.5
185
- ? s
186
- : { top, h, show: true },
187
- );
188
- }, []);
189
- // Re-measure on selection / tab change…
190
- useIsoLayoutEffect(() => {
191
- measure();
192
- }, [activeHref, sections, measure]);
193
- // …and whenever the nav's layout changes (a group expands/collapses → the
194
- // active item moves), plus on viewport resize.
195
- React.useEffect(() => {
196
- const nav = rootRef.current;
197
- if (!nav || typeof ResizeObserver === 'undefined') return undefined;
198
- const ro = new ResizeObserver(() => measure());
199
- ro.observe(nav);
200
- window.addEventListener('resize', measure);
201
- return () => {
202
- ro.disconnect();
203
- window.removeEventListener('resize', measure);
204
- };
205
- }, [measure]);
206
-
207
- return (
208
- <SidebarCtx.Provider value={ctx}>
209
- <Stack
210
- ref={rootRef}
211
- as="nav"
212
- space="var(--s0)"
213
- className={`velu-sidebar ${className}`.trim()}
214
- aria-label="Documentation"
215
- {...rest}
216
- >
217
- <span
218
- className="velu-sidebar__indicator"
219
- aria-hidden="true"
220
- style={{
221
- transform: `translateY(${ind.top}px)`,
222
- height: `${ind.h}px`,
223
- opacity: ind.show ? 1 : 0,
224
- }}
225
- />
226
- {/* Each section is its own Stack so the heading sits TIGHT to
227
- its list (small inner gap), while the nav's larger gap
228
- separates one section from the next — compact but still
229
- visibly grouped. */}
230
- {sections.map((section, i) => (
231
- <Stack key={i} space="var(--s-4)">
232
- <h5 className="velu-sidebar__section">
233
- <Icon icon={section.icon} />
234
- {section.title}
235
- </h5>
236
- <Stack as="ul" space="var(--s-4)" className="velu-sidebar__list">
237
- {section.items.map((it, j) => (
238
- <Node key={j} item={it} depth={0} />
239
- ))}
240
- </Stack>
241
- </Stack>
242
- ))}
243
- </Stack>
244
- </SidebarCtx.Provider>
245
- );
246
- }
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 } = 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
+ <Icon icon={icon} />
134
+ <span className="velu-sidebar__label">{label}</span>
135
+ {external ? <ExternalIcon /> : null}
136
+ </LinkTag>
137
+ </li>
138
+ );
139
+ }
140
+
141
+ export default function Sidebar({
142
+ sections = [],
143
+ activeHref,
144
+ linkComponent = 'a',
145
+ className = '',
146
+ ...rest
147
+ }) {
148
+ const ctx = React.useMemo(
149
+ () => ({ activeHref, Link: linkComponent }),
150
+ [activeHref, linkComponent]
151
+ );
152
+
153
+ // Bring the active item into view inside the parent scroll container
154
+ // whenever the active route changes — e.g. after navigation, the
155
+ // selected page is already visible without the user scrolling the
156
+ // sidebar manually. Uses the shared util (instead of native
157
+ // scrollIntoView) so `scroll-padding-*` on the parent — which the
158
+ // docs layout uses to mark the footer-eclipsed band as off-limits —
159
+ // is reliably honoured.
160
+ const rootRef = React.useRef(null);
161
+ React.useEffect(() => {
162
+ const el = rootRef.current?.querySelector('[aria-current="page"]');
163
+ scrollIntoNearestView(el);
164
+ }, [activeHref]);
165
+
166
+ // The single gliding active indicator (crimson tint + flush-left bar). One
167
+ // element animates its position/height to the active item, so selection feels
168
+ // continuous rather than each item popping its own bar. It lives in the scroll
169
+ // content, so its offset is scroll-stable (no re-measure on scroll) and a
170
+ // pinned sticky heading's opaque background covers it when an item tucks under.
171
+ const [ind, setInd] = React.useState({ top: 0, h: 0, show: false });
172
+ const measure = React.useCallback(() => {
173
+ const nav = rootRef.current;
174
+ if (!nav) return;
175
+ const el = nav.querySelector('[aria-current="page"]');
176
+ if (!el) {
177
+ setInd((s) => (s.show ? { ...s, show: false } : s));
178
+ return;
179
+ }
180
+ const top = el.getBoundingClientRect().top - nav.getBoundingClientRect().top;
181
+ const h = el.offsetHeight;
182
+ // Bail when nothing moved → no needless re-render (and no animation jitter).
183
+ setInd((s) =>
184
+ s.show && Math.abs(s.top - top) < 0.5 && Math.abs(s.h - h) < 0.5
185
+ ? s
186
+ : { top, h, show: true },
187
+ );
188
+ }, []);
189
+ // Re-measure on selection / tab change…
190
+ useIsoLayoutEffect(() => {
191
+ measure();
192
+ }, [activeHref, sections, measure]);
193
+ // …and whenever the nav's layout changes (a group expands/collapses → the
194
+ // active item moves), plus on viewport resize.
195
+ React.useEffect(() => {
196
+ const nav = rootRef.current;
197
+ if (!nav || typeof ResizeObserver === 'undefined') return undefined;
198
+ const ro = new ResizeObserver(() => measure());
199
+ ro.observe(nav);
200
+ window.addEventListener('resize', measure);
201
+ return () => {
202
+ ro.disconnect();
203
+ window.removeEventListener('resize', measure);
204
+ };
205
+ }, [measure]);
206
+
207
+ return (
208
+ <SidebarCtx.Provider value={ctx}>
209
+ <Stack
210
+ ref={rootRef}
211
+ as="nav"
212
+ space="var(--s0)"
213
+ className={`velu-sidebar ${className}`.trim()}
214
+ aria-label="Documentation"
215
+ {...rest}
216
+ >
217
+ <span
218
+ className="velu-sidebar__indicator"
219
+ aria-hidden="true"
220
+ style={{
221
+ transform: `translateY(${ind.top}px)`,
222
+ height: `${ind.h}px`,
223
+ opacity: ind.show ? 1 : 0,
224
+ }}
225
+ />
226
+ {/* Each section is its own Stack so the heading sits TIGHT to
227
+ its list (small inner gap), while the nav's larger gap
228
+ separates one section from the next — compact but still
229
+ visibly grouped. */}
230
+ {sections.map((section, i) => (
231
+ <Stack key={i} space="var(--s-4)">
232
+ <h5 className="velu-sidebar__section">
233
+ <Icon icon={section.icon} />
234
+ {section.title}
235
+ </h5>
236
+ <Stack as="ul" space="var(--s-4)" className="velu-sidebar__list">
237
+ {section.items.map((it, j) => (
238
+ <Node key={j} item={it} depth={0} />
239
+ ))}
240
+ </Stack>
241
+ </Stack>
242
+ ))}
243
+ </Stack>
244
+ </SidebarCtx.Provider>
245
+ );
246
+ }