@veluai/velu 0.1.13 → 0.1.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 (98) hide show
  1. package/README.md +80 -80
  2. package/dist/cli.js +21 -21
  3. package/package.json +3 -2
  4. package/runtime/velu-ui/components/Accordion.jsx +64 -64
  5. package/runtime/velu-ui/components/ApiClient.jsx +121 -121
  6. package/runtime/velu-ui/components/ApiField.jsx +87 -87
  7. package/runtime/velu-ui/components/ApiPath.jsx +63 -63
  8. package/runtime/velu-ui/components/ApiSidebar.jsx +122 -122
  9. package/runtime/velu-ui/components/AskBar.jsx +71 -71
  10. package/runtime/velu-ui/components/Callout.jsx +114 -114
  11. package/runtime/velu-ui/components/Card.jsx +131 -131
  12. package/runtime/velu-ui/components/Chatbot.jsx +596 -596
  13. package/runtime/velu-ui/components/CodeBlock.jsx +375 -375
  14. package/runtime/velu-ui/components/Columns.jsx +56 -56
  15. package/runtime/velu-ui/components/ErrorCard.jsx +138 -138
  16. package/runtime/velu-ui/components/Field.jsx +81 -81
  17. package/runtime/velu-ui/components/Image.jsx +163 -163
  18. package/runtime/velu-ui/components/Logo.jsx +31 -0
  19. package/runtime/velu-ui/components/MethodBadge.jsx +31 -31
  20. package/runtime/velu-ui/components/NavSelect.jsx +108 -108
  21. package/runtime/velu-ui/components/PageFeedback.jsx +219 -219
  22. package/runtime/velu-ui/components/PageFooter.jsx +145 -213
  23. package/runtime/velu-ui/components/PageHeader.jsx +422 -414
  24. package/runtime/velu-ui/components/PageNav.jsx +77 -77
  25. package/runtime/velu-ui/components/PoweredBy.jsx +51 -51
  26. package/runtime/velu-ui/components/Prompt.jsx +115 -115
  27. package/runtime/velu-ui/components/Search.jsx +411 -366
  28. package/runtime/velu-ui/components/Sidebar.jsx +191 -191
  29. package/runtime/velu-ui/components/SocialLinks.jsx +90 -0
  30. package/runtime/velu-ui/components/Steps.jsx +65 -65
  31. package/runtime/velu-ui/components/ThemeToggle.jsx +48 -48
  32. package/runtime/velu-ui/components/Toc.jsx +537 -537
  33. package/runtime/velu-ui/components/TocBar.jsx +195 -195
  34. package/runtime/velu-ui/components/Tree.jsx +87 -87
  35. package/runtime/velu-ui/components/TryItBar.jsx +90 -90
  36. package/runtime/velu-ui/components/accordion.css +92 -92
  37. package/runtime/velu-ui/components/api.css +479 -479
  38. package/runtime/velu-ui/components/ask-bar.css +94 -94
  39. package/runtime/velu-ui/components/card.css +105 -105
  40. package/runtime/velu-ui/components/chatbot.css +617 -617
  41. package/runtime/velu-ui/components/code-block.css +263 -263
  42. package/runtime/velu-ui/components/docs-layout.css +784 -775
  43. package/runtime/velu-ui/components/field.css +82 -82
  44. package/runtime/velu-ui/components/image.css +237 -237
  45. package/runtime/velu-ui/components/nav-select.css +157 -157
  46. package/runtime/velu-ui/components/page-feedback.css +241 -241
  47. package/runtime/velu-ui/components/page-footer.css +130 -130
  48. package/runtime/velu-ui/components/page-header.css +557 -520
  49. package/runtime/velu-ui/components/page-nav.css +50 -50
  50. package/runtime/velu-ui/components/powered-by.css +86 -66
  51. package/runtime/velu-ui/components/prompt.css +99 -99
  52. package/runtime/velu-ui/components/search.css +307 -307
  53. package/runtime/velu-ui/components/sidebar.css +144 -144
  54. package/runtime/velu-ui/components/steps.css +77 -77
  55. package/runtime/velu-ui/components/theme-toggle.css +101 -101
  56. package/runtime/velu-ui/components/toc-bar.css +234 -234
  57. package/runtime/velu-ui/components/tree.css +49 -49
  58. package/runtime/velu-ui/index.js +48 -46
  59. package/runtime/velu-ui/lib/component-schemas.js +100 -100
  60. package/runtime/velu-ui/lib/copyText.js +64 -64
  61. package/runtime/velu-ui/lib/lang-icons.jsx +147 -147
  62. package/runtime/velu-ui/lib/prism-langs.js +957 -957
  63. package/runtime/velu-ui/lib/prism-loader.js +74 -74
  64. package/runtime/velu-ui/lib/resolveIcon.jsx +29 -29
  65. package/runtime/velu-ui/lib/scrollIntoNearestView.js +66 -66
  66. package/runtime/velu-ui/mdx-components.jsx +105 -105
  67. package/runtime/velu-ui/primitives/Cluster.jsx +49 -49
  68. package/runtime/velu-ui/primitives/Stack.jsx +63 -63
  69. package/runtime/velu-ui/primitives/Switcher.jsx +57 -57
  70. package/runtime/velu-ui/primitives/stack.css +3 -3
  71. package/runtime/velu-ui/primitives/switcher.css +25 -25
  72. package/runtime/velu-ui/styles.css +43 -43
  73. package/runtime/velu-ui/tokens.css +4 -4
  74. package/schema/velu.schema.json +281 -167
  75. package/src/lib/extract-mdx-error.js +170 -170
  76. package/src/lib/issues.js +159 -159
  77. package/src/lib/known-components.js +34 -34
  78. package/src/navigation.js +434 -434
  79. package/src/runtime/App.jsx +1506 -1476
  80. package/src/runtime/ErrorBoundary.jsx +54 -54
  81. package/src/runtime/client-entry.jsx +22 -22
  82. package/src/runtime/server-entry.jsx +16 -16
  83. package/src/template.html +48 -48
  84. package/templates/starter/ai-tools/claude-code.mdx +26 -26
  85. package/templates/starter/ai-tools/cursor.mdx +17 -17
  86. package/templates/starter/api-reference/endpoint/create.mdx +24 -24
  87. package/templates/starter/api-reference/endpoint/get.mdx +27 -27
  88. package/templates/starter/api-reference/introduction.mdx +28 -28
  89. package/templates/starter/development.mdx +19 -19
  90. package/templates/starter/essentials/code.mdx +29 -29
  91. package/templates/starter/essentials/images.mdx +29 -29
  92. package/templates/starter/essentials/markdown.mdx +25 -25
  93. package/templates/starter/essentials/navigation.mdx +39 -39
  94. package/templates/starter/essentials/settings.mdx +30 -30
  95. package/templates/starter/favicon.svg +6 -6
  96. package/templates/starter/index.mdx +31 -31
  97. package/templates/starter/quickstart.mdx +31 -31
  98. 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
+ }
@@ -0,0 +1,90 @@
1
+ import React from 'react';
2
+ import Cluster from '../primitives/Cluster.jsx';
3
+
4
+ /**
5
+ * SocialLinks — a row of social-platform icon links. Shared by the full
6
+ * PageFooter (centered at its base) and the compact content-foot row (next to
7
+ * "Powered by Velu" when there are no footer link columns).
8
+ *
9
+ * `socials` is `[{ kind, href }]`. Unknown kinds (no icon) are skipped.
10
+ *
11
+ * @param {{ socials?: {kind: string, href: string}[],
12
+ * justify?: string, className?: string }} props
13
+ */
14
+
15
+ /* Inline SVGs, fill: currentColor so they inherit the themed text color. */
16
+ function GithubIcon() {
17
+ return (
18
+ <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
19
+ <path
20
+ fillRule="evenodd"
21
+ clipRule="evenodd"
22
+ d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z"
23
+ />
24
+ </svg>
25
+ );
26
+ }
27
+ function XIcon() {
28
+ return (
29
+ <svg viewBox="0 0 1200 1227" fill="currentColor" aria-hidden="true">
30
+ <path d="M714.163 519.284 1160.89 0h-105.86L667.137 450.887 357.328 0H0l468.492 681.821L0 1226.37h105.866l409.625-476.152 327.181 476.152H1200L714.137 519.284h.026ZM569.165 687.828l-47.468-67.894-377.686-540.24h162.604l304.797 435.991 47.468 67.894 396.2 566.721H892.476L569.165 687.854v-.026Z" />
31
+ </svg>
32
+ );
33
+ }
34
+ function YoutubeIcon() {
35
+ return (
36
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
37
+ <path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
38
+ </svg>
39
+ );
40
+ }
41
+ function LinkedinIcon() {
42
+ return (
43
+ <svg viewBox="0 0 256 256" fill="currentColor" aria-hidden="true">
44
+ <path d="M218.123 218.127h-37.931v-59.403c0-14.165-.253-32.4-19.728-32.4-19.756 0-22.779 15.434-22.779 31.369v60.43h-37.93V95.967h36.413v16.694h.51a39.907 39.907 0 0 1 35.928-19.733c38.445 0 45.533 25.288 45.533 58.186l-.016 67.013ZM56.955 79.27c-12.157.002-22.014-9.852-22.016-22.009-.002-12.157 9.851-22.014 22.008-22.016 12.157-.003 22.014 9.851 22.016 22.008A22.013 22.013 0 0 1 56.955 79.27m18.966 138.858H37.95V95.967h37.97v122.16ZM237.033.018H18.89C8.58-.098.125 8.161-.001 18.471v219.053c.122 10.315 8.576 18.582 18.89 18.474h218.144c10.336.128 18.823-8.139 18.966-18.474V18.454c-.147-10.33-8.635-18.588-18.966-18.453" />
45
+ </svg>
46
+ );
47
+ }
48
+
49
+ export const SOCIAL_ICONS = {
50
+ github: GithubIcon,
51
+ x: XIcon,
52
+ youtube: YoutubeIcon,
53
+ linkedin: LinkedinIcon,
54
+ };
55
+ const SOCIAL_LABELS = {
56
+ github: 'GitHub',
57
+ x: 'X',
58
+ youtube: 'YouTube',
59
+ linkedin: 'LinkedIn',
60
+ };
61
+
62
+ export default function SocialLinks({ socials = [], justify, className = '' }) {
63
+ const known = socials.filter((s) => s && SOCIAL_ICONS[s.kind]);
64
+ if (!known.length) return null;
65
+ return (
66
+ <Cluster
67
+ space="var(--s-2)"
68
+ align="center"
69
+ justify={justify}
70
+ className={`velu-socials ${className}`.trim()}
71
+ >
72
+ {known.map((s, i) => {
73
+ const Icon = SOCIAL_ICONS[s.kind];
74
+ return (
75
+ <a
76
+ key={i}
77
+ href={s.href}
78
+ target="_blank"
79
+ rel="noreferrer"
80
+ aria-label={SOCIAL_LABELS[s.kind] ?? s.kind}
81
+ className="velu-social"
82
+ data-kind={s.kind}
83
+ >
84
+ <Icon />
85
+ </a>
86
+ );
87
+ })}
88
+ </Cluster>
89
+ );
90
+ }
@@ -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
+ }