@veluai/velu 0.1.0

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 (90) hide show
  1. package/dist/cli.js +11 -0
  2. package/package.json +52 -0
  3. package/runtime/velu-ui/base.css +311 -0
  4. package/runtime/velu-ui/components/Accordion.jsx +64 -0
  5. package/runtime/velu-ui/components/ApiClient.jsx +121 -0
  6. package/runtime/velu-ui/components/ApiField.jsx +87 -0
  7. package/runtime/velu-ui/components/ApiPath.jsx +63 -0
  8. package/runtime/velu-ui/components/ApiSidebar.jsx +122 -0
  9. package/runtime/velu-ui/components/AskBar.jsx +71 -0
  10. package/runtime/velu-ui/components/Callout.jsx +114 -0
  11. package/runtime/velu-ui/components/Card.jsx +131 -0
  12. package/runtime/velu-ui/components/Chatbot.jsx +596 -0
  13. package/runtime/velu-ui/components/CodeBlock.jsx +375 -0
  14. package/runtime/velu-ui/components/Columns.jsx +56 -0
  15. package/runtime/velu-ui/components/Field.jsx +81 -0
  16. package/runtime/velu-ui/components/Image.jsx +163 -0
  17. package/runtime/velu-ui/components/MethodBadge.jsx +31 -0
  18. package/runtime/velu-ui/components/NavSelect.jsx +108 -0
  19. package/runtime/velu-ui/components/PageFeedback.jsx +219 -0
  20. package/runtime/velu-ui/components/PageFooter.jsx +213 -0
  21. package/runtime/velu-ui/components/PageHeader.jsx +414 -0
  22. package/runtime/velu-ui/components/PageNav.jsx +77 -0
  23. package/runtime/velu-ui/components/PoweredBy.jsx +51 -0
  24. package/runtime/velu-ui/components/Prompt.jsx +115 -0
  25. package/runtime/velu-ui/components/Search.jsx +366 -0
  26. package/runtime/velu-ui/components/Sidebar.jsx +191 -0
  27. package/runtime/velu-ui/components/Steps.jsx +65 -0
  28. package/runtime/velu-ui/components/ThemeToggle.jsx +48 -0
  29. package/runtime/velu-ui/components/Toc.jsx +537 -0
  30. package/runtime/velu-ui/components/TocBar.jsx +195 -0
  31. package/runtime/velu-ui/components/Tree.jsx +87 -0
  32. package/runtime/velu-ui/components/TryItBar.jsx +90 -0
  33. package/runtime/velu-ui/components/accordion.css +92 -0
  34. package/runtime/velu-ui/components/api.css +479 -0
  35. package/runtime/velu-ui/components/ask-bar.css +94 -0
  36. package/runtime/velu-ui/components/card.css +105 -0
  37. package/runtime/velu-ui/components/chatbot.css +617 -0
  38. package/runtime/velu-ui/components/code-block.css +263 -0
  39. package/runtime/velu-ui/components/docs-layout.css +775 -0
  40. package/runtime/velu-ui/components/field.css +82 -0
  41. package/runtime/velu-ui/components/image.css +237 -0
  42. package/runtime/velu-ui/components/nav-select.css +157 -0
  43. package/runtime/velu-ui/components/page-feedback.css +241 -0
  44. package/runtime/velu-ui/components/page-footer.css +130 -0
  45. package/runtime/velu-ui/components/page-header.css +520 -0
  46. package/runtime/velu-ui/components/page-nav.css +50 -0
  47. package/runtime/velu-ui/components/powered-by.css +66 -0
  48. package/runtime/velu-ui/components/prompt.css +99 -0
  49. package/runtime/velu-ui/components/search.css +307 -0
  50. package/runtime/velu-ui/components/sidebar.css +144 -0
  51. package/runtime/velu-ui/components/steps.css +77 -0
  52. package/runtime/velu-ui/components/theme-toggle.css +70 -0
  53. package/runtime/velu-ui/components/toc-bar.css +234 -0
  54. package/runtime/velu-ui/components/tree.css +49 -0
  55. package/runtime/velu-ui/index.js +45 -0
  56. package/runtime/velu-ui/lib/copyText.js +64 -0
  57. package/runtime/velu-ui/lib/lang-icons.jsx +156 -0
  58. package/runtime/velu-ui/lib/prism-langs.js +957 -0
  59. package/runtime/velu-ui/lib/prism-loader.js +74 -0
  60. package/runtime/velu-ui/lib/resolveIcon.jsx +29 -0
  61. package/runtime/velu-ui/lib/scrollIntoNearestView.js +66 -0
  62. package/runtime/velu-ui/mdx-components.jsx +85 -0
  63. package/runtime/velu-ui/primitives/Cluster.jsx +49 -0
  64. package/runtime/velu-ui/primitives/Stack.jsx +63 -0
  65. package/runtime/velu-ui/primitives/Switcher.jsx +57 -0
  66. package/runtime/velu-ui/primitives/stack.css +3 -0
  67. package/runtime/velu-ui/primitives/switcher.css +25 -0
  68. package/runtime/velu-ui/styles.css +43 -0
  69. package/runtime/velu-ui/tokens.css +4 -0
  70. package/schema/velu.schema.json +167 -0
  71. package/src/navigation.js +434 -0
  72. package/src/runtime/App.jsx +1473 -0
  73. package/src/runtime/client-entry.jsx +22 -0
  74. package/src/runtime/server-entry.jsx +16 -0
  75. package/src/template.html +48 -0
  76. package/templates/starter/ai-tools/claude-code.mdx +26 -0
  77. package/templates/starter/ai-tools/cursor.mdx +17 -0
  78. package/templates/starter/api-reference/endpoint/create.mdx +24 -0
  79. package/templates/starter/api-reference/endpoint/get.mdx +27 -0
  80. package/templates/starter/api-reference/introduction.mdx +28 -0
  81. package/templates/starter/development.mdx +19 -0
  82. package/templates/starter/essentials/code.mdx +28 -0
  83. package/templates/starter/essentials/images.mdx +29 -0
  84. package/templates/starter/essentials/markdown.mdx +25 -0
  85. package/templates/starter/essentials/navigation.mdx +39 -0
  86. package/templates/starter/essentials/settings.mdx +30 -0
  87. package/templates/starter/favicon.svg +6 -0
  88. package/templates/starter/index.mdx +31 -0
  89. package/templates/starter/quickstart.mdx +31 -0
  90. package/templates/starter/velu.json +33 -0
@@ -0,0 +1,434 @@
1
+ import GithubSlugger from 'github-slugger';
2
+ import ISO6391 from 'iso-639-1';
3
+
4
+ /**
5
+ * navigation.js — the PURE, framework-agnostic navigation model. No
6
+ * `fs`, no React, no Node-only imports: this file is imported by BOTH
7
+ * the Node-side Vite plugin (to normalize + walk) AND the browser
8
+ * runtime (to resolve the active page → sidebar/header). Sharing the
9
+ * exact same code on both sides is what keeps SSR and client render
10
+ * in lockstep (no hydration drift).
11
+ *
12
+ * Model: `velu.json`'s `navigation` is normalized into a canonical
13
+ * tree whose containment order is FIXED, regardless of how the author
14
+ * nested things, and identical to the URL-prefix order:
15
+ *
16
+ * AXIS_ORDER: product → version → language → tabs → groups → pages
17
+ * URL: /<product?>/<version?>/<language?>/<page-path>
18
+ *
19
+ * Default axis values contribute no URL segment. Dropdowns are out of
20
+ * scope by design.
21
+ */
22
+
23
+ export const AXIS_ORDER = ['product', 'version', 'language'];
24
+
25
+ // `language` in velu.json is stored as an ISO 639-1 code (e.g. "en");
26
+ // the switcher shows the code + its native name. iso-639-1 covers the
27
+ // full set of codes; unknown/invalid codes fall back to the code.
28
+ function languageName(code) {
29
+ const c = String(code).toLowerCase();
30
+ return ISO6391.getNativeName(c) || code;
31
+ }
32
+
33
+ /** Fresh slugger per call → pure (no cross-call dedup state). */
34
+ export function slugify(s) {
35
+ return new GithubSlugger().slug(String(s ?? ''));
36
+ }
37
+
38
+ /**
39
+ * Map a content page path to its in-context URL leaf:
40
+ * "index" → "/"
41
+ * "sync/index" → "/sync"
42
+ * "sync/github" → "/sync/github"
43
+ * Defined ONCE so collect + resolve never disagree.
44
+ */
45
+ export function urlForPath(pagePath) {
46
+ const segs = String(pagePath)
47
+ .replace(/^\/+|\/+$/g, '')
48
+ .split('/')
49
+ .filter(Boolean);
50
+ if (segs[segs.length - 1] === 'index') segs.pop();
51
+ return '/' + segs.join('/');
52
+ }
53
+
54
+ /** Normalize a request/location path: drop query+hash, collapse a
55
+ * trailing slash (except root), ensure a leading slash. Used on both
56
+ * sides so the active-page lookup matches identically. */
57
+ export function normalizeUrl(url) {
58
+ let u = String(url).split('#')[0].split('?')[0];
59
+ // Decode percent-encoding so non-ASCII slugs (e.g. an "Español"
60
+ // language → "/español") match whether the browser sends the path
61
+ // encoded ("/espa%C3%B1ol") or decoded.
62
+ try {
63
+ u = decodeURI(u);
64
+ } catch {
65
+ /* malformed escape — leave as-is */
66
+ }
67
+ if (!u.startsWith('/')) u = '/' + u;
68
+ if (u.length > 1) u = u.replace(/\/+$/, '');
69
+ return u || '/';
70
+ }
71
+
72
+ const titleCase = (s) =>
73
+ String(s)
74
+ .replace(/[-_]+/g, ' ')
75
+ .replace(/\b\w/g, (c) => c.toUpperCase());
76
+
77
+ const lastSeg = (url) => {
78
+ const segs = String(url).split('/').filter(Boolean);
79
+ return segs[segs.length - 1] ?? '';
80
+ };
81
+
82
+ // ── Normalization ──────────────────────────────────────────────────
83
+
84
+ /**
85
+ * Build the canonical tree from a raw `navigation` object. At each
86
+ * level we honor a fixed precedence (products > versions > languages >
87
+ * tabs > leaf), so canonically-authored configs (axes nested in
88
+ * AXIS_ORDER, or only one axis) collapse to the intended tree. A leaf
89
+ * level mixes anchors + groups + pages as siblings.
90
+ */
91
+ export function normalizeNavigation(raw) {
92
+ const root = { kind: 'root', children: normalizeChildren(raw ?? {}) };
93
+ applyDefaults(root.children);
94
+ return root;
95
+ }
96
+
97
+ function normalizeChildren(obj) {
98
+ // Anchors are pinned links that may sit alongside ANY level (incl.
99
+ // next to tabs as "global" anchors), so collect them regardless of
100
+ // which grouping branch this container uses.
101
+ const anchors = Array.isArray(obj.anchors)
102
+ ? obj.anchors.map(normalizeAnchor)
103
+ : [];
104
+ if (Array.isArray(obj.products))
105
+ return [...anchors, ...obj.products.map(normalizeProduct)];
106
+ if (Array.isArray(obj.versions))
107
+ return [...anchors, ...obj.versions.map(normalizeVersion)];
108
+ if (Array.isArray(obj.languages))
109
+ return [...anchors, ...obj.languages.map(normalizeLanguage)];
110
+ if (Array.isArray(obj.tabs))
111
+ return [...anchors, ...obj.tabs.map(normalizeTab)];
112
+ const out = [...anchors];
113
+ if (Array.isArray(obj.groups)) out.push(...obj.groups.map(normalizeGroup));
114
+ if (Array.isArray(obj.pages)) out.push(...obj.pages.map(normalizePageEntry));
115
+ return out;
116
+ }
117
+
118
+ function normalizeProduct(p) {
119
+ return {
120
+ kind: 'product',
121
+ label: p.name ?? p.product,
122
+ slug: slugify(p.product),
123
+ icon: p.icon,
124
+ color: p.color,
125
+ default: Boolean(p.default),
126
+ children: normalizeChildren(p),
127
+ };
128
+ }
129
+
130
+ function normalizeVersion(v) {
131
+ return {
132
+ kind: 'version',
133
+ label: v.version,
134
+ slug: slugify(v.version),
135
+ default: Boolean(v.default),
136
+ children: normalizeChildren(v),
137
+ };
138
+ }
139
+
140
+ function normalizeLanguage(l) {
141
+ const code = String(l.language);
142
+ return {
143
+ kind: 'language',
144
+ code,
145
+ label: languageName(code),
146
+ slug: slugify(code),
147
+ default: Boolean(l.default),
148
+ children: normalizeChildren(l),
149
+ };
150
+ }
151
+
152
+ function normalizeTab(t) {
153
+ return {
154
+ kind: 'tab',
155
+ label: t.tab,
156
+ slug: slugify(t.tab),
157
+ icon: t.icon,
158
+ href: t.href,
159
+ children: normalizeChildren(t),
160
+ };
161
+ }
162
+
163
+ function normalizeAnchor(a) {
164
+ return {
165
+ kind: 'anchor',
166
+ label: a.anchor,
167
+ icon: a.icon,
168
+ href: a.href,
169
+ color: a.color,
170
+ };
171
+ }
172
+
173
+ function normalizeGroup(g) {
174
+ const pages = (g.pages ?? []).map(normalizePageEntry);
175
+ // A group `root` is its landing page. velu-ui's Sidebar section
176
+ // heading isn't a link, so surface root as the group's FIRST item
177
+ // (its label comes from the root page's frontmatter, like any page).
178
+ if (g.root) pages.unshift({ kind: 'page', pagePath: g.root });
179
+ return {
180
+ kind: 'group',
181
+ label: g.group,
182
+ icon: g.icon,
183
+ expanded: Boolean(g.expanded),
184
+ root: g.root,
185
+ children: pages,
186
+ };
187
+ }
188
+
189
+ /** A `pages[]` entry is either a string (page path) or a nested group. */
190
+ function normalizePageEntry(entry) {
191
+ if (typeof entry === 'string') return { kind: 'page', pagePath: entry };
192
+ return normalizeGroup(entry);
193
+ }
194
+
195
+ /**
196
+ * Ensure each axis sibling-set has exactly one default: if the author
197
+ * marked none, the first declared becomes default (so an axis never
198
+ * prefixes every URL). Recurses through the whole tree.
199
+ */
200
+ function applyDefaults(children) {
201
+ for (const kind of AXIS_ORDER) {
202
+ const sibs = children.filter((c) => c.kind === kind);
203
+ if (sibs.length && !sibs.some((s) => s.default)) sibs[0].default = true;
204
+ }
205
+ for (const c of children) if (c.children) applyDefaults(c.children);
206
+ }
207
+
208
+ // ── Walking ────────────────────────────────────────────────────────
209
+
210
+ /**
211
+ * Walk the tree to every page leaf, computing its full URL (with the
212
+ * accumulated NON-default axis prefix) and its ancestor chain. Pure —
213
+ * the Node side adds fs existence on top (see navigation-node.js); the
214
+ * runtime uses it directly for resolution. Returns:
215
+ * [{ pagePath, url, ctx, ancestors:Node[] }]
216
+ */
217
+ export function walkPages(tree) {
218
+ const out = [];
219
+ const visit = (node, ctx, ancestors) => {
220
+ if (node.kind === 'page') {
221
+ out.push({
222
+ pagePath: node.pagePath,
223
+ url: urlInCtx(node.pagePath, ctx),
224
+ ctx: { ...ctx },
225
+ ancestors,
226
+ node,
227
+ });
228
+ return;
229
+ }
230
+ let next = ctx;
231
+ if (node.kind === 'product')
232
+ next = { ...ctx, product: node.default ? null : node.slug };
233
+ else if (node.kind === 'version')
234
+ next = { ...ctx, version: node.default ? null : node.slug };
235
+ else if (node.kind === 'language')
236
+ next = { ...ctx, language: node.default ? null : node.slug };
237
+ const nextAncestors =
238
+ node.kind === 'root' ? ancestors : [...ancestors, node];
239
+ for (const c of node.children ?? []) visit(c, next, nextAncestors);
240
+ };
241
+ visit(tree, { product: null, version: null, language: null }, []);
242
+ return out;
243
+ }
244
+
245
+ /** Compose a page URL from a page path + an axis context. */
246
+ function urlInCtx(pagePath, ctx) {
247
+ const prefix = [ctx.product, ctx.version, ctx.language].filter(Boolean);
248
+ const leaf = urlForPath(pagePath).split('/').filter(Boolean);
249
+ return '/' + [...prefix, ...leaf].join('/');
250
+ }
251
+
252
+ // ── Resolution (runtime) ───────────────────────────────────────────
253
+
254
+ /**
255
+ * Resolve the active page for a URL into everything the chrome needs.
256
+ * `pagesMap` is url → { frontmatter, ... } (from the virtual module),
257
+ * used to source sidebar labels from page frontmatter titles.
258
+ *
259
+ * Returns null when no page matches (404). Otherwise:
260
+ * {
261
+ * url, frontmatter,
262
+ * sidebarSections, anchors,
263
+ * tabs, activeTab,
264
+ * products, activeProduct,
265
+ * versions, activeVersion,
266
+ * languages, activeLanguage,
267
+ * breadcrumb,
268
+ * }
269
+ */
270
+ export function resolve(url, tree, pagesMap = {}) {
271
+ const norm = normalizeUrl(url);
272
+ const entries = walkPages(tree);
273
+ const match = entries.find((e) => e.url === norm);
274
+ if (!match) return null;
275
+
276
+ const { ctx, ancestors } = match;
277
+ const ancOf = (kind) => ancestors.filter((a) => a.kind === kind).pop();
278
+ const tabAnc = ancOf('tab');
279
+ const axisAnc = ancestors
280
+ .filter((a) => AXIS_ORDER.includes(a.kind))
281
+ .pop();
282
+ const productAnc = ancOf('product');
283
+ const versionAnc = ancOf('version');
284
+ const languageAnc = ancOf('language');
285
+
286
+ // Sidebar/anchor container = nearest tab, else nearest axis node,
287
+ // else the root. Its group/page children form the sidebar; its
288
+ // anchor children form the context-zone anchors.
289
+ const container = tabAnc || axisAnc || tree;
290
+ const containerKids = container.children ?? [];
291
+ const sidebarSections = buildSidebar(
292
+ containerKids.filter((n) => n.kind === 'group' || n.kind === 'page'),
293
+ pagesMap,
294
+ ctx,
295
+ );
296
+ // Anchors are gathered from every container along the active path —
297
+ // root (global), the active product/version/language, and the active
298
+ // tab — so global anchors show on every page while tab-scoped ones
299
+ // show only within their tab.
300
+ const anchors = [tree, productAnc, versionAnc, languageAnc, tabAnc]
301
+ .filter(Boolean)
302
+ .flatMap((c) => (c.children ?? []).filter((n) => n.kind === 'anchor'))
303
+ .map((a) => ({ label: a.label, icon: a.icon, href: a.href, external: true }));
304
+
305
+ // Tabs live directly under the innermost axis node (or root).
306
+ const tabParent = axisAnc || tree;
307
+ const tabNodes = (tabParent.children ?? []).filter((n) => n.kind === 'tab');
308
+ const firstUrlUnder = (node) => {
309
+ const e = entries.find((en) => en.ancestors.includes(node));
310
+ return e ? e.url : node.href ?? '#';
311
+ };
312
+ const tabs = tabNodes.map((t) => ({
313
+ label: t.label,
314
+ icon: t.icon,
315
+ href: firstUrlUnder(t),
316
+ }));
317
+ const activeTab = tabAnc ? firstUrlUnder(tabAnc) : undefined;
318
+
319
+ // Switchers: sibling axis options + the URL to land on when chosen
320
+ // (first page of that option's subtree). Each option set is scoped
321
+ // to the active outer axis (versions within the active product, etc).
322
+ const axisOptions = (kind, parent) =>
323
+ (parent?.children ?? tree.children ?? [])
324
+ .filter((n) => n.kind === kind)
325
+ .map((n) => ({
326
+ label: n.label,
327
+ code: n.code, // language code (undefined for product/version)
328
+ slug: n.slug,
329
+ icon: n.icon,
330
+ default: n.default,
331
+ href: firstUrlUnder(n),
332
+ active: n === ancOf(kind),
333
+ }));
334
+ const products = axisOptions('product', tree);
335
+ const versions = axisOptions('version', productAnc ?? tree);
336
+ const languages = axisOptions('language', versionAnc ?? productAnc ?? tree);
337
+
338
+ // Breadcrumb: tab → group ancestors → page title.
339
+ const groupAncestors = ancestors.filter((a) => a.kind === 'group');
340
+ const pageTitle = pagesMap[norm]?.frontmatter?.title ?? titleCase(lastSeg(norm));
341
+ const breadcrumb = [
342
+ ...(tabAnc ? [{ label: tabAnc.label }] : []),
343
+ ...groupAncestors.map((g) => ({ label: g.label })),
344
+ { label: pageTitle },
345
+ ];
346
+
347
+ // Prev / next = neighbors in the sidebar's reading order (the
348
+ // flattened page sequence of the active section).
349
+ const ordered = flattenSidebar(sidebarSections);
350
+ const idx = ordered.findIndex((p) => p.href === norm);
351
+ const prev = idx > 0 ? ordered[idx - 1] : undefined;
352
+ const next = idx >= 0 && idx < ordered.length - 1 ? ordered[idx + 1] : undefined;
353
+
354
+ return {
355
+ url: norm,
356
+ frontmatter: pagesMap[norm]?.frontmatter ?? {},
357
+ sidebarSections,
358
+ anchors,
359
+ tabs,
360
+ activeTab,
361
+ products,
362
+ activeProduct: productAnc?.label,
363
+ versions,
364
+ activeVersion: versionAnc?.label,
365
+ languages,
366
+ activeLanguage: languageAnc?.label,
367
+ activeLanguageCode: languageAnc?.code,
368
+ breadcrumb,
369
+ prev,
370
+ next,
371
+ };
372
+ }
373
+
374
+ /** Flatten sidebar sections (incl. nested groups) to an ordered list of
375
+ * page links {label, href} — the page reading order for prev/next. */
376
+ function flattenSidebar(sections) {
377
+ const out = [];
378
+ const walk = (items) => {
379
+ for (const it of items) {
380
+ if (it.items) walk(it.items);
381
+ else out.push({ label: it.label, href: it.href });
382
+ }
383
+ };
384
+ for (const s of sections) walk(s.items);
385
+ return out;
386
+ }
387
+
388
+ /**
389
+ * Map a list of (group|page) nodes to the velu-ui Sidebar `sections`
390
+ * shape: [{ title, icon, items:[{ label, href, icon, items }] }].
391
+ * Loose top-level pages (not inside a group) collect into a leading
392
+ * untitled section. Page labels come from frontmatter title, falling
393
+ * back to a title-cased URL segment (the SAME fallback on both render
394
+ * sides). Sidebar derives open/active state from `activeHref`, so we
395
+ * don't compute it here.
396
+ */
397
+ export function buildSidebar(nodes, pagesMap, ctx) {
398
+ const sections = [];
399
+ let loose = null;
400
+ for (const n of nodes) {
401
+ if (n.kind === 'group') {
402
+ sections.push({
403
+ title: n.label,
404
+ icon: n.icon,
405
+ items: itemsFor(n.children, pagesMap, ctx),
406
+ });
407
+ } else if (n.kind === 'page') {
408
+ if (!loose) {
409
+ loose = { title: '', items: [] };
410
+ sections.push(loose);
411
+ }
412
+ loose.items.push(itemFor(n, pagesMap, ctx));
413
+ }
414
+ }
415
+ return sections;
416
+ }
417
+
418
+ function itemsFor(nodes, pagesMap, ctx) {
419
+ return (nodes ?? []).map((n) =>
420
+ n.kind === 'group'
421
+ ? {
422
+ label: n.label,
423
+ icon: n.icon,
424
+ items: itemsFor(n.children, pagesMap, ctx),
425
+ }
426
+ : itemFor(n, pagesMap, ctx),
427
+ );
428
+ }
429
+
430
+ function itemFor(pageNode, pagesMap, ctx) {
431
+ const href = urlInCtx(pageNode.pagePath, ctx);
432
+ const fm = pagesMap[href]?.frontmatter;
433
+ return { label: fm?.title ?? titleCase(lastSeg(href)), href };
434
+ }