@veluai/velu 0.1.13 → 0.1.14
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 +18 -18
- package/package.json +3 -2
- package/runtime/velu-ui/components/Accordion.jsx +64 -64
- package/runtime/velu-ui/components/ApiClient.jsx +121 -121
- package/runtime/velu-ui/components/ApiField.jsx +87 -87
- package/runtime/velu-ui/components/ApiPath.jsx +63 -63
- 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 +596 -596
- package/runtime/velu-ui/components/CodeBlock.jsx +375 -375
- package/runtime/velu-ui/components/Columns.jsx +56 -56
- 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/MethodBadge.jsx +31 -31
- package/runtime/velu-ui/components/NavSelect.jsx +108 -108
- package/runtime/velu-ui/components/PageFeedback.jsx +219 -219
- package/runtime/velu-ui/components/PageFooter.jsx +213 -213
- package/runtime/velu-ui/components/PageHeader.jsx +414 -414
- 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 +411 -366
- package/runtime/velu-ui/components/Sidebar.jsx +191 -191
- 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 +90 -90
- package/runtime/velu-ui/components/accordion.css +92 -92
- package/runtime/velu-ui/components/api.css +479 -479
- 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 +617 -617
- package/runtime/velu-ui/components/code-block.css +263 -263
- package/runtime/velu-ui/components/docs-layout.css +784 -775
- 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/page-feedback.css +241 -241
- package/runtime/velu-ui/components/page-footer.css +130 -130
- package/runtime/velu-ui/components/page-header.css +520 -520
- package/runtime/velu-ui/components/page-nav.css +50 -50
- package/runtime/velu-ui/components/powered-by.css +66 -66
- 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 +144 -144
- package/runtime/velu-ui/components/steps.css +77 -77
- package/runtime/velu-ui/components/theme-toggle.css +101 -101
- 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 +46 -46
- package/runtime/velu-ui/lib/component-schemas.js +100 -100
- package/runtime/velu-ui/lib/copyText.js +64 -64
- package/runtime/velu-ui/lib/lang-icons.jsx +147 -147
- 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 +43 -43
- package/runtime/velu-ui/tokens.css +4 -4
- package/schema/velu.schema.json +167 -167
- 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 +434 -434
- package/src/runtime/App.jsx +1490 -1476
- 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/endpoint/create.mdx +24 -24
- package/templates/starter/api-reference/endpoint/get.mdx +27 -27
- package/templates/starter/api-reference/introduction.mdx +28 -28
- 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/quickstart.mdx +31 -31
- package/templates/starter/velu.json +33 -33
|
@@ -1,191 +1,191 @@
|
|
|
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
|
-
function isActive(item, activeHref) {
|
|
45
|
-
return Boolean(
|
|
46
|
-
item.active || (item.href != null && item.href === activeHref)
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function hasActiveDescendant(item, activeHref) {
|
|
51
|
-
if (isActive(item, activeHref)) return true;
|
|
52
|
-
return (
|
|
53
|
-
Array.isArray(item.items) &&
|
|
54
|
-
item.items.some((c) => hasActiveDescendant(c, activeHref))
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function Icon({ icon }) {
|
|
59
|
-
const node = resolveIcon(icon);
|
|
60
|
-
return node ? (
|
|
61
|
-
<span className="velu-sidebar__icon" aria-hidden="true">
|
|
62
|
-
{node}
|
|
63
|
-
</span>
|
|
64
|
-
) : null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function Chevron() {
|
|
68
|
-
return (
|
|
69
|
-
<ChevronRight
|
|
70
|
-
className="velu-sidebar__chevron"
|
|
71
|
-
aria-hidden="true"
|
|
72
|
-
focusable="false"
|
|
73
|
-
/>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function ExternalIcon() {
|
|
78
|
-
return (
|
|
79
|
-
<ExternalLink
|
|
80
|
-
className="velu-sidebar__icon"
|
|
81
|
-
aria-hidden="true"
|
|
82
|
-
focusable="false"
|
|
83
|
-
/>
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Recursive node. depth 0 = top-level; depth >= 1 = nested (subitem style). */
|
|
88
|
-
function Node({ item, depth }) {
|
|
89
|
-
const { activeHref, Link } = React.useContext(SidebarCtx);
|
|
90
|
-
const { label, href = '#', icon, external, items } = item;
|
|
91
|
-
const base = depth === 0 ? 'velu-sidebar__item' : 'velu-sidebar__subitem';
|
|
92
|
-
|
|
93
|
-
if (items && items.length) {
|
|
94
|
-
return (
|
|
95
|
-
<li>
|
|
96
|
-
<details open={hasActiveDescendant(item, activeHref) || undefined}>
|
|
97
|
-
<summary className={`${base} velu-sidebar__summary`}>
|
|
98
|
-
<Icon icon={icon} />
|
|
99
|
-
<span className="velu-sidebar__label">{label}</span>
|
|
100
|
-
<Chevron />
|
|
101
|
-
</summary>
|
|
102
|
-
{/* nested list: Stack with no gap (spacing = item padding-block),
|
|
103
|
-
matching the source's stack-s */}
|
|
104
|
-
<Stack as="ul" space="0" className="velu-sidebar__sublist">
|
|
105
|
-
{items.map((child, i) => (
|
|
106
|
-
<Node key={i} item={child} depth={depth + 1} />
|
|
107
|
-
))}
|
|
108
|
-
</Stack>
|
|
109
|
-
</details>
|
|
110
|
-
</li>
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const active = isActive(item, activeHref);
|
|
115
|
-
const cls = base + (active ? ' velu-sidebar__item--active' : '');
|
|
116
|
-
const LinkTag = external ? 'a' : Link;
|
|
117
|
-
const linkProps = external
|
|
118
|
-
? { href, target: '_blank', rel: 'noreferrer' }
|
|
119
|
-
: { href };
|
|
120
|
-
|
|
121
|
-
return (
|
|
122
|
-
<li>
|
|
123
|
-
<LinkTag
|
|
124
|
-
className={cls}
|
|
125
|
-
aria-current={active ? 'page' : undefined}
|
|
126
|
-
{...linkProps}
|
|
127
|
-
>
|
|
128
|
-
<Icon icon={icon} />
|
|
129
|
-
<span className="velu-sidebar__label">{label}</span>
|
|
130
|
-
{external ? <ExternalIcon /> : null}
|
|
131
|
-
</LinkTag>
|
|
132
|
-
</li>
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export default function Sidebar({
|
|
137
|
-
sections = [],
|
|
138
|
-
activeHref,
|
|
139
|
-
linkComponent = 'a',
|
|
140
|
-
className = '',
|
|
141
|
-
...rest
|
|
142
|
-
}) {
|
|
143
|
-
const ctx = React.useMemo(
|
|
144
|
-
() => ({ activeHref, Link: linkComponent }),
|
|
145
|
-
[activeHref, linkComponent]
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
// Bring the active item into view inside the parent scroll container
|
|
149
|
-
// whenever the active route changes — e.g. after navigation, the
|
|
150
|
-
// selected page is already visible without the user scrolling the
|
|
151
|
-
// sidebar manually. Uses the shared util (instead of native
|
|
152
|
-
// scrollIntoView) so `scroll-padding-*` on the parent — which the
|
|
153
|
-
// docs layout uses to mark the footer-eclipsed band as off-limits —
|
|
154
|
-
// is reliably honoured.
|
|
155
|
-
const rootRef = React.useRef(null);
|
|
156
|
-
React.useEffect(() => {
|
|
157
|
-
const el = rootRef.current?.querySelector('[aria-current="page"]');
|
|
158
|
-
scrollIntoNearestView(el);
|
|
159
|
-
}, [activeHref]);
|
|
160
|
-
|
|
161
|
-
return (
|
|
162
|
-
<SidebarCtx.Provider value={ctx}>
|
|
163
|
-
<Stack
|
|
164
|
-
ref={rootRef}
|
|
165
|
-
as="nav"
|
|
166
|
-
space="var(--s0)"
|
|
167
|
-
className={`velu-sidebar ${className}`.trim()}
|
|
168
|
-
aria-label="Documentation"
|
|
169
|
-
{...rest}
|
|
170
|
-
>
|
|
171
|
-
{/* Each section is its own Stack so the heading sits TIGHT to
|
|
172
|
-
its list (small inner gap), while the nav's larger gap
|
|
173
|
-
separates one section from the next — compact but still
|
|
174
|
-
visibly grouped. */}
|
|
175
|
-
{sections.map((section, i) => (
|
|
176
|
-
<Stack key={i} space="var(--s-4)">
|
|
177
|
-
<h5 className="velu-sidebar__section">
|
|
178
|
-
<Icon icon={section.icon} />
|
|
179
|
-
{section.title}
|
|
180
|
-
</h5>
|
|
181
|
-
<Stack as="ul" space="var(--s-4)" className="velu-sidebar__list">
|
|
182
|
-
{section.items.map((it, j) => (
|
|
183
|
-
<Node key={j} item={it} depth={0} />
|
|
184
|
-
))}
|
|
185
|
-
</Stack>
|
|
186
|
-
</Stack>
|
|
187
|
-
))}
|
|
188
|
-
</Stack>
|
|
189
|
-
</SidebarCtx.Provider>
|
|
190
|
-
);
|
|
191
|
-
}
|
|
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
|
+
function isActive(item, activeHref) {
|
|
45
|
+
return Boolean(
|
|
46
|
+
item.active || (item.href != null && item.href === activeHref)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function hasActiveDescendant(item, activeHref) {
|
|
51
|
+
if (isActive(item, activeHref)) return true;
|
|
52
|
+
return (
|
|
53
|
+
Array.isArray(item.items) &&
|
|
54
|
+
item.items.some((c) => hasActiveDescendant(c, activeHref))
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function Icon({ icon }) {
|
|
59
|
+
const node = resolveIcon(icon);
|
|
60
|
+
return node ? (
|
|
61
|
+
<span className="velu-sidebar__icon" aria-hidden="true">
|
|
62
|
+
{node}
|
|
63
|
+
</span>
|
|
64
|
+
) : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function Chevron() {
|
|
68
|
+
return (
|
|
69
|
+
<ChevronRight
|
|
70
|
+
className="velu-sidebar__chevron"
|
|
71
|
+
aria-hidden="true"
|
|
72
|
+
focusable="false"
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function ExternalIcon() {
|
|
78
|
+
return (
|
|
79
|
+
<ExternalLink
|
|
80
|
+
className="velu-sidebar__icon"
|
|
81
|
+
aria-hidden="true"
|
|
82
|
+
focusable="false"
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Recursive node. depth 0 = top-level; depth >= 1 = nested (subitem style). */
|
|
88
|
+
function Node({ item, depth }) {
|
|
89
|
+
const { activeHref, Link } = React.useContext(SidebarCtx);
|
|
90
|
+
const { label, href = '#', icon, external, items } = item;
|
|
91
|
+
const base = depth === 0 ? 'velu-sidebar__item' : 'velu-sidebar__subitem';
|
|
92
|
+
|
|
93
|
+
if (items && items.length) {
|
|
94
|
+
return (
|
|
95
|
+
<li>
|
|
96
|
+
<details open={hasActiveDescendant(item, activeHref) || undefined}>
|
|
97
|
+
<summary className={`${base} velu-sidebar__summary`}>
|
|
98
|
+
<Icon icon={icon} />
|
|
99
|
+
<span className="velu-sidebar__label">{label}</span>
|
|
100
|
+
<Chevron />
|
|
101
|
+
</summary>
|
|
102
|
+
{/* nested list: Stack with no gap (spacing = item padding-block),
|
|
103
|
+
matching the source's stack-s */}
|
|
104
|
+
<Stack as="ul" space="0" className="velu-sidebar__sublist">
|
|
105
|
+
{items.map((child, i) => (
|
|
106
|
+
<Node key={i} item={child} depth={depth + 1} />
|
|
107
|
+
))}
|
|
108
|
+
</Stack>
|
|
109
|
+
</details>
|
|
110
|
+
</li>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const active = isActive(item, activeHref);
|
|
115
|
+
const cls = base + (active ? ' velu-sidebar__item--active' : '');
|
|
116
|
+
const LinkTag = external ? 'a' : Link;
|
|
117
|
+
const linkProps = external
|
|
118
|
+
? { href, target: '_blank', rel: 'noreferrer' }
|
|
119
|
+
: { href };
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<li>
|
|
123
|
+
<LinkTag
|
|
124
|
+
className={cls}
|
|
125
|
+
aria-current={active ? 'page' : undefined}
|
|
126
|
+
{...linkProps}
|
|
127
|
+
>
|
|
128
|
+
<Icon icon={icon} />
|
|
129
|
+
<span className="velu-sidebar__label">{label}</span>
|
|
130
|
+
{external ? <ExternalIcon /> : null}
|
|
131
|
+
</LinkTag>
|
|
132
|
+
</li>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export default function Sidebar({
|
|
137
|
+
sections = [],
|
|
138
|
+
activeHref,
|
|
139
|
+
linkComponent = 'a',
|
|
140
|
+
className = '',
|
|
141
|
+
...rest
|
|
142
|
+
}) {
|
|
143
|
+
const ctx = React.useMemo(
|
|
144
|
+
() => ({ activeHref, Link: linkComponent }),
|
|
145
|
+
[activeHref, linkComponent]
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Bring the active item into view inside the parent scroll container
|
|
149
|
+
// whenever the active route changes — e.g. after navigation, the
|
|
150
|
+
// selected page is already visible without the user scrolling the
|
|
151
|
+
// sidebar manually. Uses the shared util (instead of native
|
|
152
|
+
// scrollIntoView) so `scroll-padding-*` on the parent — which the
|
|
153
|
+
// docs layout uses to mark the footer-eclipsed band as off-limits —
|
|
154
|
+
// is reliably honoured.
|
|
155
|
+
const rootRef = React.useRef(null);
|
|
156
|
+
React.useEffect(() => {
|
|
157
|
+
const el = rootRef.current?.querySelector('[aria-current="page"]');
|
|
158
|
+
scrollIntoNearestView(el);
|
|
159
|
+
}, [activeHref]);
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<SidebarCtx.Provider value={ctx}>
|
|
163
|
+
<Stack
|
|
164
|
+
ref={rootRef}
|
|
165
|
+
as="nav"
|
|
166
|
+
space="var(--s0)"
|
|
167
|
+
className={`velu-sidebar ${className}`.trim()}
|
|
168
|
+
aria-label="Documentation"
|
|
169
|
+
{...rest}
|
|
170
|
+
>
|
|
171
|
+
{/* Each section is its own Stack so the heading sits TIGHT to
|
|
172
|
+
its list (small inner gap), while the nav's larger gap
|
|
173
|
+
separates one section from the next — compact but still
|
|
174
|
+
visibly grouped. */}
|
|
175
|
+
{sections.map((section, i) => (
|
|
176
|
+
<Stack key={i} space="var(--s-4)">
|
|
177
|
+
<h5 className="velu-sidebar__section">
|
|
178
|
+
<Icon icon={section.icon} />
|
|
179
|
+
{section.title}
|
|
180
|
+
</h5>
|
|
181
|
+
<Stack as="ul" space="var(--s-4)" className="velu-sidebar__list">
|
|
182
|
+
{section.items.map((it, j) => (
|
|
183
|
+
<Node key={j} item={it} depth={0} />
|
|
184
|
+
))}
|
|
185
|
+
</Stack>
|
|
186
|
+
</Stack>
|
|
187
|
+
))}
|
|
188
|
+
</Stack>
|
|
189
|
+
</SidebarCtx.Provider>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
import React, { Children, cloneElement, isValidElement } from 'react';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Steps + Step — numbered procedural steps with a continuous accent rail
|
|
5
|
-
* down the left edge.
|
|
6
|
-
*
|
|
7
|
-
* <Steps>
|
|
8
|
-
* <Step title="First Step">These are instructions…</Step>
|
|
9
|
-
* <Step title="Second Step">…</Step>
|
|
10
|
-
* <Step title="Third Step">…</Step>
|
|
11
|
-
* </Steps>
|
|
12
|
-
*
|
|
13
|
-
* Steps assigns 1-based `index` to each <Step> child via cloneElement —
|
|
14
|
-
* callers don't have to number them by hand. A <Step> rendered outside
|
|
15
|
-
* <Steps> falls back to whatever `index` it was given (or 1).
|
|
16
|
-
*
|
|
17
|
-
* Each Step is a 2-column grid:
|
|
18
|
-
*
|
|
19
|
-
* [circle + line] title
|
|
20
|
-
* body
|
|
21
|
-
*
|
|
22
|
-
* The marker column spans both rows. The vertical rail (line below the
|
|
23
|
-
* circle) is `flex: 1`, so it stretches to fill the Step's full height —
|
|
24
|
-
* meaning adjacent steps' rails meet end-to-end with no manual height
|
|
25
|
-
* math. The last step keeps its rail too (matches the design).
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
export function Step({
|
|
29
|
-
title,
|
|
30
|
-
index = 1,
|
|
31
|
-
children,
|
|
32
|
-
className = '',
|
|
33
|
-
...rest
|
|
34
|
-
}) {
|
|
35
|
-
return (
|
|
36
|
-
<div className={`velu-step ${className}`.trim()} {...rest}>
|
|
37
|
-
<div className="velu-step__circle" aria-hidden="true">
|
|
38
|
-
{index}
|
|
39
|
-
</div>
|
|
40
|
-
<div className="velu-step__line" aria-hidden="true" />
|
|
41
|
-
{title != null && (
|
|
42
|
-
<div className="velu-step__title">{title}</div>
|
|
43
|
-
)}
|
|
44
|
-
{children != null && children !== false && (
|
|
45
|
-
<div className="velu-step__body">{children}</div>
|
|
46
|
-
)}
|
|
47
|
-
</div>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
Step.displayName = 'Step';
|
|
51
|
-
|
|
52
|
-
export default function Steps({ children, className = '', ...rest }) {
|
|
53
|
-
let i = 0;
|
|
54
|
-
const numbered = Children.map(children, (child) => {
|
|
55
|
-
if (!isValidElement(child)) return child;
|
|
56
|
-
if (child.type !== Step && child.type?.displayName !== 'Step') return child;
|
|
57
|
-
i += 1;
|
|
58
|
-
return cloneElement(child, { index: child.props.index ?? i });
|
|
59
|
-
});
|
|
60
|
-
return (
|
|
61
|
-
<div className={`velu-steps ${className}`.trim()} {...rest}>
|
|
62
|
-
{numbered}
|
|
63
|
-
</div>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
1
|
+
import React, { Children, cloneElement, isValidElement } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Steps + Step — numbered procedural steps with a continuous accent rail
|
|
5
|
+
* down the left edge.
|
|
6
|
+
*
|
|
7
|
+
* <Steps>
|
|
8
|
+
* <Step title="First Step">These are instructions…</Step>
|
|
9
|
+
* <Step title="Second Step">…</Step>
|
|
10
|
+
* <Step title="Third Step">…</Step>
|
|
11
|
+
* </Steps>
|
|
12
|
+
*
|
|
13
|
+
* Steps assigns 1-based `index` to each <Step> child via cloneElement —
|
|
14
|
+
* callers don't have to number them by hand. A <Step> rendered outside
|
|
15
|
+
* <Steps> falls back to whatever `index` it was given (or 1).
|
|
16
|
+
*
|
|
17
|
+
* Each Step is a 2-column grid:
|
|
18
|
+
*
|
|
19
|
+
* [circle + line] title
|
|
20
|
+
* body
|
|
21
|
+
*
|
|
22
|
+
* The marker column spans both rows. The vertical rail (line below the
|
|
23
|
+
* circle) is `flex: 1`, so it stretches to fill the Step's full height —
|
|
24
|
+
* meaning adjacent steps' rails meet end-to-end with no manual height
|
|
25
|
+
* math. The last step keeps its rail too (matches the design).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export function Step({
|
|
29
|
+
title,
|
|
30
|
+
index = 1,
|
|
31
|
+
children,
|
|
32
|
+
className = '',
|
|
33
|
+
...rest
|
|
34
|
+
}) {
|
|
35
|
+
return (
|
|
36
|
+
<div className={`velu-step ${className}`.trim()} {...rest}>
|
|
37
|
+
<div className="velu-step__circle" aria-hidden="true">
|
|
38
|
+
{index}
|
|
39
|
+
</div>
|
|
40
|
+
<div className="velu-step__line" aria-hidden="true" />
|
|
41
|
+
{title != null && (
|
|
42
|
+
<div className="velu-step__title">{title}</div>
|
|
43
|
+
)}
|
|
44
|
+
{children != null && children !== false && (
|
|
45
|
+
<div className="velu-step__body">{children}</div>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
Step.displayName = 'Step';
|
|
51
|
+
|
|
52
|
+
export default function Steps({ children, className = '', ...rest }) {
|
|
53
|
+
let i = 0;
|
|
54
|
+
const numbered = Children.map(children, (child) => {
|
|
55
|
+
if (!isValidElement(child)) return child;
|
|
56
|
+
if (child.type !== Step && child.type?.displayName !== 'Step') return child;
|
|
57
|
+
i += 1;
|
|
58
|
+
return cloneElement(child, { index: child.props.index ?? i });
|
|
59
|
+
});
|
|
60
|
+
return (
|
|
61
|
+
<div className={`velu-steps ${className}`.trim()} {...rest}>
|
|
62
|
+
{numbered}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Sun, Moon } from 'lucide-react';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* ThemeToggle — pill-shaped two-state light/dark switch. The thumb
|
|
6
|
-
* sits at the inline-start in light mode and slides to the inline-end
|
|
7
|
-
* in dark mode. A sun glyph rides the thumb in light, a moon in dark.
|
|
8
|
-
*
|
|
9
|
-
* SSR-safe by design: markup is identical regardless of theme (the
|
|
10
|
-
* server doesn't know it), and the thumb position + which glyph shows
|
|
11
|
-
* are decided purely by CSS via the `[data-theme]` attribute — NOT by
|
|
12
|
-
* React state. So there is no hydration mismatch and no icon flash. JS
|
|
13
|
-
* only handles the click: it flips `data-theme` and persists the
|
|
14
|
-
* explicit choice (which then overrides the OS preference on future
|
|
15
|
-
* visits, per the anti-flash script in the template).
|
|
16
|
-
*/
|
|
17
|
-
export default function ThemeToggle({ className = '', ...rest }) {
|
|
18
|
-
function toggle() {
|
|
19
|
-
const root = document.documentElement;
|
|
20
|
-
const next = root.dataset.theme === 'dark' ? 'light' : 'dark';
|
|
21
|
-
root.dataset.theme = next;
|
|
22
|
-
try {
|
|
23
|
-
localStorage.setItem('velu-theme', next);
|
|
24
|
-
} catch {
|
|
25
|
-
/* private mode / storage disabled — toggle still works for the session */
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<button
|
|
31
|
-
type="button"
|
|
32
|
-
onClick={toggle}
|
|
33
|
-
aria-label="Toggle color theme"
|
|
34
|
-
title="Toggle color theme"
|
|
35
|
-
className={`velu-theme-toggle ${className}`.trim()}
|
|
36
|
-
{...rest}
|
|
37
|
-
>
|
|
38
|
-
<Sun
|
|
39
|
-
className="velu-theme-toggle__icon velu-theme-toggle__icon--sun"
|
|
40
|
-
aria-hidden="true"
|
|
41
|
-
/>
|
|
42
|
-
<Moon
|
|
43
|
-
className="velu-theme-toggle__icon velu-theme-toggle__icon--moon"
|
|
44
|
-
aria-hidden="true"
|
|
45
|
-
/>
|
|
46
|
-
</button>
|
|
47
|
-
);
|
|
48
|
-
}
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Sun, Moon } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ThemeToggle — pill-shaped two-state light/dark switch. The thumb
|
|
6
|
+
* sits at the inline-start in light mode and slides to the inline-end
|
|
7
|
+
* in dark mode. A sun glyph rides the thumb in light, a moon in dark.
|
|
8
|
+
*
|
|
9
|
+
* SSR-safe by design: markup is identical regardless of theme (the
|
|
10
|
+
* server doesn't know it), and the thumb position + which glyph shows
|
|
11
|
+
* are decided purely by CSS via the `[data-theme]` attribute — NOT by
|
|
12
|
+
* React state. So there is no hydration mismatch and no icon flash. JS
|
|
13
|
+
* only handles the click: it flips `data-theme` and persists the
|
|
14
|
+
* explicit choice (which then overrides the OS preference on future
|
|
15
|
+
* visits, per the anti-flash script in the template).
|
|
16
|
+
*/
|
|
17
|
+
export default function ThemeToggle({ className = '', ...rest }) {
|
|
18
|
+
function toggle() {
|
|
19
|
+
const root = document.documentElement;
|
|
20
|
+
const next = root.dataset.theme === 'dark' ? 'light' : 'dark';
|
|
21
|
+
root.dataset.theme = next;
|
|
22
|
+
try {
|
|
23
|
+
localStorage.setItem('velu-theme', next);
|
|
24
|
+
} catch {
|
|
25
|
+
/* private mode / storage disabled — toggle still works for the session */
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
onClick={toggle}
|
|
33
|
+
aria-label="Toggle color theme"
|
|
34
|
+
title="Toggle color theme"
|
|
35
|
+
className={`velu-theme-toggle ${className}`.trim()}
|
|
36
|
+
{...rest}
|
|
37
|
+
>
|
|
38
|
+
<Sun
|
|
39
|
+
className="velu-theme-toggle__icon velu-theme-toggle__icon--sun"
|
|
40
|
+
aria-hidden="true"
|
|
41
|
+
/>
|
|
42
|
+
<Moon
|
|
43
|
+
className="velu-theme-toggle__icon velu-theme-toggle__icon--moon"
|
|
44
|
+
aria-hidden="true"
|
|
45
|
+
/>
|
|
46
|
+
</button>
|
|
47
|
+
);
|
|
48
|
+
}
|