@veluai/velu 0.2.37 → 0.2.39

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 (44) hide show
  1. package/dist/cli.js +41 -41
  2. package/docs/aalam.md +44 -0
  3. package/docs/mintlify-migration.md +8 -3
  4. package/docs/thulir.md +23 -0
  5. package/docs/vepa.md +2 -1
  6. package/package.json +4 -2
  7. package/runtime/velu-ui/base.css +9 -0
  8. package/runtime/velu-ui/components/ApiReferencePage.jsx +4 -3
  9. package/runtime/velu-ui/components/ApiSamples.jsx +9 -2
  10. package/runtime/velu-ui/components/Callout.jsx +4 -1
  11. package/runtime/velu-ui/components/PageFooter.jsx +3 -1
  12. package/runtime/velu-ui/components/PageHeader.jsx +7 -1
  13. package/runtime/velu-ui/components/PageNav.jsx +98 -22
  14. package/runtime/velu-ui/components/Sidebar.jsx +21 -9
  15. package/runtime/velu-ui/components/ThemeToggle.jsx +1 -0
  16. package/runtime/velu-ui/components/TocBar.jsx +21 -2
  17. package/runtime/velu-ui/components/Update.jsx +22 -10
  18. package/runtime/velu-ui/components/VepaFooter.jsx +19 -17
  19. package/runtime/velu-ui/components/api-page.css +6 -3
  20. package/runtime/velu-ui/components/chatbot.css +5 -4
  21. package/runtime/velu-ui/components/docs-layout.css +99 -114
  22. package/runtime/velu-ui/components/page-footer.css +2 -3
  23. package/runtime/velu-ui/components/page-header.css +6 -0
  24. package/runtime/velu-ui/components/page-nav.css +192 -0
  25. package/runtime/velu-ui/components/sidebar.css +5 -13
  26. package/runtime/velu-ui/primitives/switcher.css +4 -0
  27. package/runtime/velu-ui/styles.css +2 -0
  28. package/runtime/velu-ui/themes/aalam.css +266 -0
  29. package/runtime/velu-ui/themes/thulir.css +428 -0
  30. package/runtime/velu-ui/themes/vepa.css +142 -10
  31. package/schema/velu.schema.json +23 -3
  32. package/src/navigation.js +22 -3
  33. package/src/runtime/App.jsx +182 -107
  34. package/templates/starter/ai-tools/claude-code.mdx +1 -0
  35. package/templates/starter/ai-tools/cursor.mdx +1 -0
  36. package/templates/starter/api-reference/introduction.mdx +1 -0
  37. package/templates/starter/development.mdx +1 -0
  38. package/templates/starter/essentials/code.mdx +1 -0
  39. package/templates/starter/essentials/images.mdx +1 -0
  40. package/templates/starter/essentials/markdown.mdx +1 -0
  41. package/templates/starter/essentials/navigation.mdx +1 -0
  42. package/templates/starter/essentials/settings.mdx +1 -0
  43. package/templates/starter/index.mdx +1 -0
  44. package/templates/starter/quickstart.mdx +1 -0
@@ -9,8 +9,8 @@
9
9
  "properties": {
10
10
  "theme": {
11
11
  "type": "string",
12
- "enum": ["velu", "vepa"],
13
- "description": "Visual preset. Vepa follows the Mintlify Sequoia layout; light/dark preference is independent. Defaults to velu."
12
+ "enum": ["velu", "vepa", "thulir", "aalam"],
13
+ "description": "Visual preset. Vepa follows Mintlify Sequoia, Thulir follows Mintlify Mint, and Aalam follows Mintlify Maple; light/dark preference is independent. Defaults to velu."
14
14
  },
15
15
  "$schema": {
16
16
  "type": "string",
@@ -283,14 +283,34 @@
283
283
  },
284
284
  "pages": {
285
285
  "type": "array",
286
- "description": "Ordered list of page paths (no extension, relative to the project root) and/or nested groups.",
286
+ "description": "Ordered list of page paths (no extension, relative to the project root), page objects with optional icons, and/or nested groups.",
287
287
  "items": {
288
288
  "oneOf": [
289
289
  { "type": "string" },
290
+ { "$ref": "#/$defs/pageEntry" },
290
291
  { "$ref": "#/$defs/group" }
291
292
  ]
292
293
  }
293
294
  },
295
+ "pageEntry": {
296
+ "type": "object",
297
+ "additionalProperties": false,
298
+ "required": ["page"],
299
+ "properties": {
300
+ "page": {
301
+ "type": "string",
302
+ "description": "Page path without extension, relative to the project root."
303
+ },
304
+ "icon": {
305
+ "type": "string",
306
+ "description": "Lucide icon id shown beside the page in the sidebar."
307
+ },
308
+ "title": {
309
+ "type": "string",
310
+ "description": "Optional sidebar label override (otherwise frontmatter title)."
311
+ }
312
+ }
313
+ },
294
314
  "group": {
295
315
  "type": "object",
296
316
  "additionalProperties": false,
package/src/navigation.js CHANGED
@@ -187,12 +187,20 @@ function normalizeGroup(g) {
187
187
  }
188
188
 
189
189
  /** A `pages[]` entry is a string (page path), a generated OpenAPI page
190
- * ({ apiPage, label, method }), or a nested group. */
190
+ * ({ apiPage, label, method }), a page object ({ page, icon? }), or a
191
+ * nested group. Page icons also come from MDX frontmatter `icon`. */
191
192
  function normalizePageEntry(entry) {
192
193
  if (typeof entry === 'string') return { kind: 'page', pagePath: entry };
193
194
  if (entry && entry.apiPage) {
194
195
  return { kind: 'page', pagePath: entry.apiPage, label: entry.label, method: entry.method, api: true };
195
196
  }
197
+ // Mintlify / Velu page object: { page: "path", icon?: "rocket", ... }
198
+ if (entry && typeof entry.page === 'string') {
199
+ const node = { kind: 'page', pagePath: entry.page };
200
+ if (entry.icon) node.icon = entry.icon;
201
+ if (entry.title || entry.label) node.label = entry.title ?? entry.label;
202
+ return node;
203
+ }
196
204
  return normalizeGroup(entry);
197
205
  }
198
206
 
@@ -392,13 +400,18 @@ function sidebarSectionsHaveApi(sections) {
392
400
  }
393
401
 
394
402
  /** Flatten sidebar sections (incl. nested groups) to an ordered list of
395
- * page links {label, href} — the page reading order for prev/next. */
403
+ * page links {label, href, description?} — the page reading order for
404
+ * prev/next. */
396
405
  function flattenSidebar(sections) {
397
406
  const out = [];
398
407
  const walk = (items) => {
399
408
  for (const it of items) {
400
409
  if (it.items) walk(it.items);
401
- else out.push({ label: it.label, href: it.href });
410
+ else {
411
+ const page = { label: it.label, href: it.href };
412
+ if (it.description) page.description = it.description;
413
+ out.push(page);
414
+ }
402
415
  }
403
416
  };
404
417
  for (const s of sections) walk(s.items);
@@ -454,6 +467,12 @@ function itemFor(pageNode, pagesMap, ctx) {
454
467
  // method badge); normal pages source their label from frontmatter.
455
468
  const label = pageNode.label ?? fm?.sidebarTitle ?? fm?.title ?? titleCase(lastSeg(href));
456
469
  const item = { label, href };
470
+ const icon = pageNode.icon ?? fm?.icon;
471
+ if (icon) item.icon = icon;
472
+ const description = fm?.description;
473
+ if (typeof description === 'string' && description.trim()) {
474
+ item.description = description.trim();
475
+ }
457
476
  const method = pageNode.method ?? pagesMap[href]?.operation?.method;
458
477
  if (method) item.method = method;
459
478
  return item;
@@ -59,7 +59,7 @@ import {
59
59
  NotFound,
60
60
  VeluMark,
61
61
  } from 'velu-ui';
62
- import { X, ChevronDown, ChevronUp } from 'lucide-react';
62
+ import { X, ChevronDown } from 'lucide-react';
63
63
  import ErrorBoundary from './ErrorBoundary.jsx';
64
64
 
65
65
  // True under `velu dev`, false in the deployed production build (Vite injects
@@ -894,6 +894,7 @@ function DocsPage() {
894
894
  <NavSelect
895
895
  value={nav.activeProduct}
896
896
  options={productOptions}
897
+ icon={productOptions.find((p) => p.active)?.icon}
897
898
  linkComponent={RouterLink}
898
899
  ariaLabel="Product"
899
900
  component="nav-dropdown-products-selector"
@@ -928,7 +929,7 @@ function DocsPage() {
928
929
  // against it like any other heading.
929
930
  const pageId = React.useMemo(() => {
930
931
  if (!frontmatter.title) return null;
931
- if (site.theme === 'vepa') return 'page-title';
932
+ if (site.theme === 'vepa' || ['thulir', 'aalam'].includes(site.theme)) return 'page-title';
932
933
  return new GithubSlugger().slug(frontmatter.title);
933
934
  }, [frontmatter.title, site.theme]);
934
935
 
@@ -962,6 +963,49 @@ function DocsPage() {
962
963
  // of sidebarOpen so neither breakpoint's default leaks into the
963
964
  // other.
964
965
  const [drawerOpen, setDrawerOpen] = React.useState(false);
966
+ const drawerRef = React.useRef(null);
967
+ const [drawerViewport, setDrawerViewport] = React.useState(false);
968
+ React.useEffect(() => {
969
+ const root = drawerRef.current?.parentElement;
970
+ if (!root) return;
971
+ const observer = new ResizeObserver(([entry]) => {
972
+ const narrow = entry.contentRect.width <= (['thulir', 'aalam'].includes(site.theme) ? 1023 : 640);
973
+ setDrawerViewport(narrow);
974
+ if (!narrow) setDrawerOpen(false);
975
+ });
976
+ observer.observe(root);
977
+ return () => observer.disconnect();
978
+ }, [site.theme]);
979
+ React.useEffect(() => {
980
+ if (!drawerOpen || !drawerViewport) return;
981
+ const drawer = drawerRef.current;
982
+ const previousFocus = document.activeElement;
983
+ const previousOverflow = document.body.style.overflow;
984
+ document.body.style.overflow = 'hidden';
985
+ const siblings = [...drawer.parentElement.children].filter(e => e !== drawer && !e.classList.contains('velu-docs-layout__scrim'));
986
+ const inertStates = siblings.map(e => e.inert);
987
+ siblings.forEach(e => { e.inert = true; });
988
+ drawer.querySelector('.velu-docs-layout__drawer-close')?.focus();
989
+ const onKey = e => {
990
+ if (e.key === 'Escape') {
991
+ if (drawer.querySelector('.velu-docs-layout__drawer-docselect[data-open="true"]')) return;
992
+ setDrawerOpen(false); return;
993
+ }
994
+ if (e.key !== 'Tab') return;
995
+ const items = [...drawer.querySelectorAll('a[href],button,input,select,textarea,[tabindex]')].filter(el => !el.disabled && el.tabIndex >= 0 && el.getClientRects().length && getComputedStyle(el).visibility !== 'hidden');
996
+ const first = items[0], last = items.at(-1);
997
+ if (!first) { e.preventDefault(); return; }
998
+ if (e.shiftKey && (document.activeElement === first || !drawer.contains(document.activeElement))) { e.preventDefault(); last.focus(); }
999
+ else if (!e.shiftKey && (document.activeElement === last || !drawer.contains(document.activeElement))) { e.preventDefault(); first.focus(); }
1000
+ };
1001
+ document.addEventListener('keydown', onKey);
1002
+ return () => {
1003
+ document.removeEventListener('keydown', onKey);
1004
+ document.body.style.overflow = previousOverflow;
1005
+ siblings.forEach((e,i) => { e.inert = inertStates[i]; });
1006
+ if (previousFocus?.isConnected) previousFocus.focus({preventScroll:true});
1007
+ };
1008
+ }, [drawerOpen, drawerViewport]);
965
1009
 
966
1010
  // Drawer's nav dropdown — list of section links (Home/Docs/Blog),
967
1011
  // mirrors the desktop tabs. Click-outside + Escape close.
@@ -973,7 +1017,7 @@ function DocsPage() {
973
1017
  if (!navRef.current?.contains(e.target)) setNavOpen(false);
974
1018
  };
975
1019
  const onKey = (e) => {
976
- if (e.key === 'Escape') setNavOpen(false);
1020
+ if (e.key === 'Escape') { setNavOpen(false); navRef.current?.querySelector('button')?.focus(); }
977
1021
  };
978
1022
  document.addEventListener('mousedown', onDocClick);
979
1023
  document.addEventListener('keydown', onKey);
@@ -1072,40 +1116,30 @@ function DocsPage() {
1072
1116
  // a gap above the full-page Ask AI canvas.
1073
1117
  }, [chrome.mode]);
1074
1118
 
1075
- // The asides (left sidebar + right TOC) scroll independently of the
1076
- // page. On scroll/resize we set data-fade-top/-bottom on each scroll
1077
- // region so CSS can fade its edges (and reveal the nav arrows) only
1078
- // when there's content beyond them. Written imperatively (no
1079
- // re-render). Re-binds on chatOpen toggle (the right TOC
1080
- // mounts/unmounts with it).
1119
+ // The right TOC scrolls independently of the page. On scroll/resize
1120
+ // we set data-fade-top/-bottom so CSS can fade its edges only when
1121
+ // there's content beyond them. Written imperatively (no re-render).
1122
+ // Re-binds on chatOpen toggle (the right TOC mounts/unmounts with it).
1081
1123
  const leftAsideRef = React.useRef(null);
1082
1124
  const rightAsideRef = React.useRef(null);
1083
1125
  React.useEffect(() => {
1084
- const els = [leftAsideRef.current, rightAsideRef.current].filter(Boolean);
1085
- if (!els.length) return;
1126
+ const el = rightAsideRef.current;
1127
+ if (!el) return undefined;
1086
1128
  const update = () => {
1087
- for (const el of els) {
1088
- el.dataset.fadeTop = el.scrollTop > 0 ? 'true' : 'false';
1089
- el.dataset.fadeBottom =
1090
- el.scrollTop + el.clientHeight < el.scrollHeight - 1
1091
- ? 'true'
1092
- : 'false';
1093
- }
1129
+ el.dataset.fadeTop = el.scrollTop > 0 ? 'true' : 'false';
1130
+ el.dataset.fadeBottom =
1131
+ el.scrollTop + el.clientHeight < el.scrollHeight - 1
1132
+ ? 'true'
1133
+ : 'false';
1094
1134
  };
1095
1135
  update();
1096
- // Recompute on scroll AND on any size change of the scroll region
1097
- // or its content (ResizeObserver) + window resize — so the fade
1098
- // reflects hidden content persistently, not just during a scroll
1099
- // gesture (e.g. after a route change shifts the nav's height).
1100
1136
  const ro = new ResizeObserver(update);
1101
- els.forEach((el) => {
1102
- el.addEventListener('scroll', update, { passive: true });
1103
- ro.observe(el);
1104
- if (el.firstElementChild) ro.observe(el.firstElementChild);
1105
- });
1137
+ el.addEventListener('scroll', update, { passive: true });
1138
+ ro.observe(el);
1139
+ if (el.firstElementChild) ro.observe(el.firstElementChild);
1106
1140
  window.addEventListener('resize', update);
1107
1141
  return () => {
1108
- els.forEach((el) => el.removeEventListener('scroll', update));
1142
+ el.removeEventListener('scroll', update);
1109
1143
  ro.disconnect();
1110
1144
  window.removeEventListener('resize', update);
1111
1145
  };
@@ -1113,9 +1147,8 @@ function DocsPage() {
1113
1147
 
1114
1148
  // Flag the sidebar section heading currently pinned at the top of the
1115
1149
  // scroll region (CSS sticky gives no "is-stuck" hook). When the pinned
1116
- // heading changes as you scroll, the new one gets data-stuck and
1117
- // animates in (see sidebar.css). Re-binds on page/tab change since the
1118
- // section set changes with it.
1150
+ // heading changes as you scroll, the new one gets data-stuck.
1151
+ // Re-binds on page/tab change since the section set changes with it.
1119
1152
  React.useEffect(() => {
1120
1153
  const scroller = leftAsideRef.current;
1121
1154
  if (!scroller) return;
@@ -1198,8 +1231,8 @@ function DocsPage() {
1198
1231
  // Callback ref so this rebinds when the footer mounts/unmounts.
1199
1232
  // Custom and assistant unmount the footer; a mount-once listener
1200
1233
  // would keep reading the detached node (getBoundingClientRect top
1201
- // is 0) and treat the footer as covering the full viewport, which
1202
- // crushes the fixed sidebar via --velu-aside-bottom.
1234
+ // is 0). footerOverlap still pads the right TOC so the last items
1235
+ // can scroll clear of the footer as it rises under content + TOC.
1203
1236
  const [footerEl, setFooterEl] = React.useState(null);
1204
1237
  const [footerOverlap, setFooterOverlap] = React.useState(0);
1205
1238
  React.useEffect(() => {
@@ -1349,17 +1382,19 @@ function DocsPage() {
1349
1382
  </Cluster>
1350
1383
  }
1351
1384
  actions={navActions}
1352
- trailing={<ThemeToggle variant={site.theme === 'vepa' ? 'menu' : undefined} />}
1385
+ trailing={<ThemeToggle variant={site.theme === 'aalam' ? 'inline' : site.theme === 'vepa' || site.theme === 'thulir' ? 'menu' : undefined} />}
1353
1386
  tabsTrailing={
1354
1387
  chrome.mode === 'custom' || chrome.mode === 'assistant'
1355
1388
  ? undefined
1356
1389
  : languageSwitcher || undefined
1357
1390
  }
1358
1391
  onMenuClick={chrome.sidebar ? () => setDrawerOpen((v) => !v) : undefined}
1392
+ menuOpen={drawerOpen}
1393
+ menuId={site.theme === 'vepa' ? 'sidebar-content' : 'sidebar'}
1359
1394
  breadcrumb={
1360
1395
  chrome.mode === 'custom' || chrome.mode === 'assistant'
1361
1396
  ? []
1362
- : (nav?.breadcrumb ?? [])
1397
+ : ['thulir', 'aalam'].includes(site.theme) ? (nav?.breadcrumb ?? []).slice(-2) : (nav?.breadcrumb ?? [])
1363
1398
  }
1364
1399
  activeTab={nav?.activeTab}
1365
1400
  tabs={
@@ -1369,23 +1404,19 @@ function DocsPage() {
1369
1404
  }
1370
1405
  />
1371
1406
 
1372
- {/* Fixed left sidebar — pinned to viewport-left below the header.
1373
- Does NOT scroll with the page; the footer rises over its bottom
1374
- edge thanks to the higher z-index on the footer below.
1375
- `top` + `bottom` give a robust height (some browsers don't
1376
- honour `inset-block-start` for fixed positioning the same way
1377
- as plain `top`). */}
1407
+ {/* Fixed left sidebar — pinned to viewport-left, full height.
1408
+ The site footer is indented under content + TOC only, so the
1409
+ rail never lifts for the footer. `top` + `bottom` give a
1410
+ robust height (some browsers don't honour `inset-block-start`
1411
+ for fixed positioning the same way as plain `top`). */}
1378
1412
  <aside
1413
+ ref={drawerRef}
1414
+ role={drawerViewport ? 'dialog' : undefined}
1415
+ aria-modal={drawerViewport && drawerOpen ? true : undefined}
1416
+ aria-label={drawerViewport ? 'Navigation' : undefined}
1417
+ inert={drawerViewport && !drawerOpen ? '' : undefined}
1379
1418
  id={site.theme === 'vepa' ? 'sidebar-content' : 'sidebar'}
1380
1419
  className="velu-docs-layout__aside velu-docs-layout__aside--left"
1381
- style={{
1382
- /* Bottom edge stays a fixed gap above the viewport bottom,
1383
- AND lifts to keep that gap above the footer as it scrolls
1384
- into view (footerOverlap = how far the footer intrudes).
1385
- Set as a custom prop so the mobile drawer's
1386
- `inset-block-end: 0` override still wins. */
1387
- '--velu-aside-bottom': `calc(${footerOverlap}px + var(--s4))`,
1388
- }}
1389
1420
  // Close the mobile drawer as soon as a nav link is activated.
1390
1421
  // The pathname effect below also closes it on route change; this
1391
1422
  // covers same-route hash jumps and feels snappier on touch devices.
@@ -1413,11 +1444,11 @@ function DocsPage() {
1413
1444
  ) : (
1414
1445
  <>
1415
1446
  <VeluMark />
1416
- <span className="velu-header__wordmark">{site.name}</span>
1447
+ <span className="velu-header__wordmark" title={site.name}>{site.name}</span>
1417
1448
  </>
1418
1449
  )}
1419
1450
  </RouterLink>
1420
- <ThemeToggle />
1451
+ <ThemeToggle variant={site.theme === 'vepa' || site.theme === 'thulir' ? 'menu' : undefined} />
1421
1452
  <button
1422
1453
  type="button"
1423
1454
  className="velu-docs-layout__drawer-close"
@@ -1427,21 +1458,45 @@ function DocsPage() {
1427
1458
  <X aria-hidden="true" focusable="false" />
1428
1459
  </button>
1429
1460
  </div>
1461
+ {site.theme === 'aalam' && productSwitcher && (
1462
+ <div className="velu-docs-aside-product">{productSwitcher}</div>
1463
+ )}
1464
+ {site.theme === 'aalam' && (
1465
+ <div className="velu-docs-aside-search">
1466
+ <Search
1467
+ id="search-bar-entry-sidebar"
1468
+ unavailable={IS_DEV_PREVIEW}
1469
+ search={IS_DEV_PREVIEW ? undefined : searchDocs}
1470
+ onSelect={(item) => item.href && navigate(item.href)}
1471
+ />
1472
+ </div>
1473
+ )}
1430
1474
  {/* Nav dropdown — custom button + menu, fills drawer width.
1431
1475
  Items mirror the desktop tabs (Home / Docs / Blog).
1432
1476
  Click-outside + Escape close (see navOpen useEffect
1433
- above). Chevron rotates 180° on open. */}
1477
+ above). Chevron rotates 180° on open. Shown only when
1478
+ there is more than one tab to switch between. */}
1479
+ {navItems.length > 1 && (
1434
1480
  <div
1435
1481
  ref={navRef}
1436
1482
  className="velu-docs-layout__drawer-docselect"
1437
1483
  data-open={navOpen ? 'true' : 'false'}
1438
1484
  >
1485
+ <label htmlFor="drawer-section-picker">Documentation section</label>
1439
1486
  <button
1487
+ id="drawer-section-picker"
1440
1488
  type="button"
1441
1489
  className="velu-docs-layout__drawer-docselect-btn"
1442
1490
  onClick={() => setNavOpen((o) => !o)}
1443
1491
  aria-haspopup="menu"
1444
1492
  aria-expanded={navOpen}
1493
+ aria-controls="drawer-section-options"
1494
+ onKeyDown={(e) => {
1495
+ if (e.key === 'ArrowDown') {
1496
+ e.preventDefault(); setNavOpen(true);
1497
+ requestAnimationFrame(() => navRef.current?.querySelector('[role="menuitem"]')?.focus());
1498
+ }
1499
+ }}
1445
1500
  >
1446
1501
  <span className="velu-docs-layout__drawer-docselect-label">
1447
1502
  {activeNavLabel}
@@ -1453,9 +1508,16 @@ function DocsPage() {
1453
1508
  />
1454
1509
  </button>
1455
1510
  <ul
1511
+ id="drawer-section-options"
1456
1512
  className="velu-docs-layout__drawer-docselect-menu"
1457
1513
  role="menu"
1458
1514
  aria-hidden={!navOpen}
1515
+ onKeyDown={(e) => {
1516
+ const items = [...e.currentTarget.querySelectorAll('[role="menuitem"]')];
1517
+ const index = items.indexOf(document.activeElement);
1518
+ const next = e.key === 'ArrowDown' ? (index + 1) % items.length : e.key === 'ArrowUp' ? (index + items.length - 1) % items.length : e.key === 'Home' ? 0 : e.key === 'End' ? items.length - 1 : null;
1519
+ if (next !== null) { e.preventDefault(); items[next]?.focus(); }
1520
+ }}
1459
1521
  >
1460
1522
  {navItems.map((it) => (
1461
1523
  <li key={it.href} role="none">
@@ -1474,24 +1536,25 @@ function DocsPage() {
1474
1536
  ))}
1475
1537
  </ul>
1476
1538
  </div>
1539
+ )}
1477
1540
  {/* Context zone — product switcher + anchors at the top of
1478
1541
  the sidebar (all breakpoints), plus version/language
1479
1542
  switchers that only show on mobile (desktop has them in
1480
- the header). Rendered only when there's something to show
1481
- so simple projects keep a bare sidebar. */}
1482
- {(productSwitcher ||
1483
- versionSwitcher ||
1543
+ the header). Aalam (Maple) puts product above search and
1544
+ pins version to the sidebar footer instead. */}
1545
+ {((productSwitcher && site.theme !== 'aalam') ||
1546
+ (versionSwitcher && site.theme !== 'aalam') ||
1484
1547
  languageSwitcher ||
1485
- anchors.length > 0) && (
1548
+ (anchors.length > 0 && site.theme !== 'aalam')) && (
1486
1549
  <Stack space="var(--s-1)" className="velu-docs-context">
1487
- {productSwitcher}
1488
- {versionSwitcher && (
1550
+ {site.theme !== 'aalam' && productSwitcher}
1551
+ {versionSwitcher && site.theme !== 'aalam' && (
1489
1552
  <span className="velu-show-on-mobile">{versionSwitcher}</span>
1490
1553
  )}
1491
1554
  {languageSwitcher && (
1492
1555
  <span className="velu-show-on-mobile">{languageSwitcher}</span>
1493
1556
  )}
1494
- {anchors.length > 0 && (
1557
+ {anchors.length > 0 && site.theme !== 'aalam' && (
1495
1558
  <ul className="velu-docs-anchors" data-component="nav-anchors">
1496
1559
  {anchors.map((a, i) => (
1497
1560
  <li key={i}>
@@ -1520,26 +1583,50 @@ function DocsPage() {
1520
1583
  )}
1521
1584
  {/* Hairline separating the top anchor links from the nav sections
1522
1585
  (faithful to the sidebar design's anchor↔sidebar divider). */}
1523
- {anchors.length > 0 && (
1586
+ {anchors.length > 0 && site.theme !== 'aalam' && (
1524
1587
  <sidebar-nav-group-divider class="velu-docs-context-divider" aria-hidden="true" />
1525
1588
  )}
1526
1589
  {/* Only this region scrolls — the context zone above stays
1527
- pinned. The up/down arrows overlay its top/bottom edges and
1528
- appear (via the data-fade-* attrs the scroll handler sets)
1529
- when there's content beyond that edge; clicking jumps the
1530
- nav fully to that end. */}
1590
+ pinned. Active page links scroll into view on navigation
1591
+ (Sidebar / ApiSidebar). Aalam (Maple) scrolls the whole
1592
+ left column, including anchors, in one overflow. */}
1531
1593
  <div className="velu-docs-nav-region">
1532
1594
  <div
1533
- id={site.theme === 'vepa' ? 'navigation-items' : 'sidebar-content'}
1595
+ id={site.theme === 'vepa' || ['thulir', 'aalam'].includes(site.theme) ? 'navigation-items' : 'sidebar-content'}
1534
1596
  ref={leftAsideRef}
1535
- className="velu-docs-nav-scroll velu-hide-scrollbar"
1597
+ className={`velu-docs-nav-scroll${site.theme === 'aalam' ? '' : ' velu-hide-scrollbar'}`}
1536
1598
  style={{
1537
- /* Just breathing room the aside's bottom edge already
1538
- stays above the footer (see --velu-aside-bottom). */
1539
- paddingBlockEnd: 'var(--s1)',
1599
+ /* scroll-padding onlykeep content clear of sticky
1600
+ headings / footer without shortening the scrollbar. */
1601
+ scrollPaddingBlockStart: '2.75rem',
1540
1602
  scrollPaddingBlockEnd: 'var(--s1)',
1541
1603
  }}
1542
1604
  >
1605
+ {site.theme === 'aalam' && anchors.length > 0 && (
1606
+ <ul className="velu-docs-anchors" data-component="nav-anchors">
1607
+ {anchors.map((a, i) => (
1608
+ <li key={i}>
1609
+ <a
1610
+ data-component="nav-anchor"
1611
+ className="velu-docs-anchors__link"
1612
+ href={a.href}
1613
+ target="_blank"
1614
+ rel="noreferrer"
1615
+ >
1616
+ {a.icon && (
1617
+ <span
1618
+ className="velu-docs-anchors__icon"
1619
+ aria-hidden="true"
1620
+ >
1621
+ {resolveIcon(a.icon, { size: 16 })}
1622
+ </span>
1623
+ )}
1624
+ <span>{a.label}</span>
1625
+ </a>
1626
+ </li>
1627
+ ))}
1628
+ </ul>
1629
+ )}
1543
1630
  {/* Sidebar STYLE is chosen per-tab, not per-page: if this tab
1544
1631
  holds any API page, the whole tab uses the flat API sidebar
1545
1632
  (so opening an intro page next to API operations doesn't flip
@@ -1562,31 +1649,11 @@ function DocsPage() {
1562
1649
  />
1563
1650
  )}
1564
1651
  </div>
1565
- <button
1566
- type="button"
1567
- className="velu-docs-nav-arrow velu-docs-nav-arrow--up"
1568
- aria-label="Scroll navigation to top"
1569
- onClick={() =>
1570
- leftAsideRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
1571
- }
1572
- >
1573
- <ChevronUp aria-hidden="true" focusable="false" />
1574
- </button>
1575
- <button
1576
- type="button"
1577
- className="velu-docs-nav-arrow velu-docs-nav-arrow--down"
1578
- aria-label="Scroll navigation to bottom"
1579
- onClick={() =>
1580
- leftAsideRef.current?.scrollTo({
1581
- top: leftAsideRef.current.scrollHeight,
1582
- behavior: 'smooth',
1583
- })
1584
- }
1585
- >
1586
- <ChevronDown aria-hidden="true" focusable="false" />
1587
- </button>
1588
1652
  </div>
1589
1653
  </Stack>
1654
+ {site.theme === 'aalam' && versionSwitcher && (
1655
+ <div className="velu-docs-aside-footer">{versionSwitcher}</div>
1656
+ )}
1590
1657
  </aside>
1591
1658
 
1592
1659
  {/* No separate rail element — at narrow widths the sidebar's
@@ -1621,10 +1688,17 @@ function DocsPage() {
1621
1688
  samples={entry.samples}
1622
1689
  responses={entry.operation.responses}
1623
1690
  />
1624
- ) : isChangelog && changelogTags.length ? (
1691
+ ) : isChangelog && changelogTags.length && site.theme !== 'vepa' ? (
1625
1692
  <ChangelogFilters tags={changelogTags} />
1693
+ ) : site.theme === 'vepa' || ['thulir', 'aalam'].includes(site.theme) ? (
1694
+ <>
1695
+ {isChangelog && changelogTags.length > 0 ? (
1696
+ <ChangelogFilters tags={changelogTags} />
1697
+ ) : null}
1698
+ <VepaToc items={pageToc} activeId={['thulir', 'aalam'].includes(site.theme) && activeId === pageId ? pageToc[0]?.id : activeId} onSelect={scrollTo} />
1699
+ </>
1626
1700
  ) : (
1627
- site.theme === 'vepa' ? <VepaToc items={pageToc} activeId={activeId} onSelect={scrollTo} /> : <Toc id="table-of-contents-content" items={toc} activeId={activeId} onSelect={scrollTo} />
1701
+ <Toc id="table-of-contents-content" items={toc} activeId={activeId} onSelect={scrollTo} />
1628
1702
  )}
1629
1703
  </aside>
1630
1704
  )}
@@ -1666,7 +1740,7 @@ function DocsPage() {
1666
1740
  on tab/mobile we keep the normal TOC bar (no filters). Hidden
1667
1741
  entirely when the page mode has no table of contents. */}
1668
1742
  {chrome.toc && (
1669
- <TocBar items={toc} activeId={activeId} onSelect={scrollTo} />
1743
+ <TocBar items={toc} activeId={activeId} onSelect={scrollTo} label={['thulir', 'aalam'].includes(site.theme) ? 'On this page' : undefined} />
1670
1744
  )}
1671
1745
  <main
1672
1746
  id="content-area"
@@ -1706,7 +1780,7 @@ function DocsPage() {
1706
1780
  </span>
1707
1781
  </button>
1708
1782
  )}
1709
- <div id={site.theme === 'vepa' ? undefined : 'content'} data-contextual-actions={site.contextual?.options?.length ? 'true' : 'false'} className="velu-docs-layout__article" data-pagefind-body={chrome.assistant ? undefined : ""}>
1783
+ <div id={site.theme === 'vepa' || ['thulir', 'aalam'].includes(site.theme) ? undefined : 'content'} data-contextual-actions={site.contextual?.options?.length ? 'true' : 'false'} className="velu-docs-layout__article" data-pagefind-body={chrome.assistant ? undefined : ""}>
1710
1784
  {/* Per-page agent/IDE action bar: the section eyebrow + a
1711
1785
  "Copy Page" split-button whose dropdown is driven by the
1712
1786
  Mintlify-compatible `contextual` config. Renders nothing
@@ -1736,7 +1810,7 @@ function DocsPage() {
1736
1810
  Custom / frame / assistant are a blank canvas — no auto hero. */}
1737
1811
  {chrome.hero && (frontmatter.title || frontmatter.description) && (
1738
1812
  <div className="velu-hero">
1739
- {frontmatter.title && <h1 id={site.theme === 'vepa' ? 'page-title' : pageId}>{frontmatter.title}</h1>}
1813
+ {frontmatter.title && <h1 id={site.theme === 'vepa' || ['thulir', 'aalam'].includes(site.theme) ? 'page-title' : pageId}>{frontmatter.title}</h1>}
1740
1814
  {frontmatter.description && (
1741
1815
  <p
1742
1816
  style={{
@@ -1757,7 +1831,7 @@ function DocsPage() {
1757
1831
  level. Falls back to a not-found / missing-file notice
1758
1832
  when the route has no page (or its file is absent). */}
1759
1833
  <MDXProvider components={defaultMdxComponents}>
1760
- <mdx-content key={site.theme === 'vepa' ? pathname : undefined} id={site.theme === 'vepa' && !entry?.api ? 'content' : undefined} class="velu-prose">
1834
+ <mdx-content key={site.theme === 'vepa' ? pathname : undefined} id={(site.theme === 'vepa' || ['thulir', 'aalam'].includes(site.theme)) && !entry?.api ? 'content' : undefined} class="velu-prose">
1761
1835
  {PageComponent ? (
1762
1836
  <ErrorBoundary key={pathname} file={entry?.relPath}>
1763
1837
  {entry?.mdxApi ? <ApiReferencePage {...entry.mdxApi} preset={site.theme}><PageComponent /></ApiReferencePage> : <PageComponent />}
@@ -1773,7 +1847,7 @@ function DocsPage() {
1773
1847
  </MDXProvider>
1774
1848
  {/* Page-foot feedback widget — ref'd so the sticky AskBar
1775
1849
  above can hide as the user scrolls near it. */}
1776
- {chrome.articleChrome && (
1850
+ {chrome.articleChrome && !['thulir', 'aalam'].includes(site.theme) && (
1777
1851
  <div ref={setFeedbackEl} style={{ marginTop: 'var(--s3)' }}>
1778
1852
  {/* key resets the widget's vote state on page change. "Yes"
1779
1853
  submits immediately; "No" submits once its form is sent.
@@ -1794,7 +1868,8 @@ function DocsPage() {
1794
1868
  {chrome.articleChrome && !frontmatter.hidePagination && (nav?.prev || nav?.next) && (
1795
1869
  <PageNav
1796
1870
  id="pagination"
1797
- style={{ marginTop: 'var(--s2)' }}
1871
+ style={['thulir', 'aalam'].includes(site.theme) ? undefined : { marginTop: 'var(--s2)' }}
1872
+ variant={site.theme === 'aalam' ? 'aalam' : 'default'}
1798
1873
  prev={nav?.prev}
1799
1874
  next={nav?.next}
1800
1875
  linkComponent={RouterLink}
@@ -1834,7 +1909,7 @@ function DocsPage() {
1834
1909
  full footer section); with link columns they live in the full
1835
1910
  footer below instead. */}
1836
1911
  {chrome.articleChrome && (footerHasLinks ? (
1837
- site.theme === 'vepa' ? null : <PoweredBy />
1912
+ site.theme === 'vepa' || ['thulir', 'aalam'].includes(site.theme) ? null : <PoweredBy />
1838
1913
  ) : (
1839
1914
  <div className="velu-content-foot">
1840
1915
  <SocialLinks socials={footerSocials} />
@@ -1847,13 +1922,13 @@ function DocsPage() {
1847
1922
  )}
1848
1923
  </div>
1849
1924
 
1850
- {/* Full site footer — only when the config provides link columns. Spans
1851
- full width below the article; its raised z-index eclipses the bottoms
1852
- of the fixed sidebar and TOC as the page scrolls into it, and the ref
1853
- is watched so the asides can pad their bottom by the overlap. */}
1925
+ {/* Full site footer — only when the config provides link columns.
1926
+ Indented under content + TOC (see docs-layout.css); the left
1927
+ rail stays full-height beside it. Right TOC pads for the
1928
+ overlap via footerOverlap below. */}
1854
1929
  {footerHasLinks && chrome.footer && (
1855
1930
  <advanced-footer
1856
- id={site.theme === 'vepa' ? undefined : 'footer'}
1931
+ id={site.theme === 'vepa' || ['thulir', 'aalam'].includes(site.theme) ? undefined : 'footer'}
1857
1932
  ref={setFooterEl}
1858
1933
  data-velu-footer
1859
1934
  style={{ position: 'relative', zIndex: 20 }}
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Claude Code
3
3
  description: Work on your docs with Claude Code.
4
+ icon: bot
4
5
  ---
5
6
 
6
7
  Claude Code can read your `velu.json` and `.mdx` files to draft pages,
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Cursor
3
3
  description: Edit your docs in Cursor.
4
+ icon: sparkles
4
5
  ---
5
6
 
6
7
  Cursor's editor understands your project structure, so it can complete