@veluai/velu 0.2.38 → 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 (41) hide show
  1. package/dist/cli.js +38 -38
  2. package/docs/aalam.md +44 -0
  3. package/docs/mintlify-migration.md +8 -3
  4. package/docs/thulir.md +23 -0
  5. package/package.json +4 -2
  6. package/runtime/velu-ui/base.css +9 -0
  7. package/runtime/velu-ui/components/ApiReferencePage.jsx +4 -3
  8. package/runtime/velu-ui/components/ApiSamples.jsx +9 -2
  9. package/runtime/velu-ui/components/Callout.jsx +4 -1
  10. package/runtime/velu-ui/components/PageFooter.jsx +3 -1
  11. package/runtime/velu-ui/components/PageHeader.jsx +7 -1
  12. package/runtime/velu-ui/components/PageNav.jsx +98 -22
  13. package/runtime/velu-ui/components/Sidebar.jsx +21 -9
  14. package/runtime/velu-ui/components/ThemeToggle.jsx +1 -0
  15. package/runtime/velu-ui/components/TocBar.jsx +21 -2
  16. package/runtime/velu-ui/components/VepaFooter.jsx +19 -17
  17. package/runtime/velu-ui/components/api-page.css +6 -3
  18. package/runtime/velu-ui/components/chatbot.css +5 -4
  19. package/runtime/velu-ui/components/docs-layout.css +99 -114
  20. package/runtime/velu-ui/components/page-footer.css +2 -3
  21. package/runtime/velu-ui/components/page-header.css +6 -0
  22. package/runtime/velu-ui/components/page-nav.css +192 -0
  23. package/runtime/velu-ui/components/sidebar.css +5 -13
  24. package/runtime/velu-ui/primitives/switcher.css +4 -0
  25. package/runtime/velu-ui/styles.css +2 -0
  26. package/runtime/velu-ui/themes/aalam.css +266 -0
  27. package/runtime/velu-ui/themes/thulir.css +428 -0
  28. package/schema/velu.schema.json +23 -3
  29. package/src/navigation.js +22 -3
  30. package/src/runtime/App.jsx +171 -106
  31. package/templates/starter/ai-tools/claude-code.mdx +1 -0
  32. package/templates/starter/ai-tools/cursor.mdx +1 -0
  33. package/templates/starter/api-reference/introduction.mdx +1 -0
  34. package/templates/starter/development.mdx +1 -0
  35. package/templates/starter/essentials/code.mdx +1 -0
  36. package/templates/starter/essentials/images.mdx +1 -0
  37. package/templates/starter/essentials/markdown.mdx +1 -0
  38. package/templates/starter/essentials/navigation.mdx +1 -0
  39. package/templates/starter/essentials/settings.mdx +1 -0
  40. package/templates/starter/index.mdx +1 -0
  41. package/templates/starter/quickstart.mdx +1 -0
@@ -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 variant={site.theme === 'vepa' ? 'menu' : undefined} />
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,6 +1458,19 @@ 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
@@ -1438,12 +1482,21 @@ function DocsPage() {
1438
1482
  className="velu-docs-layout__drawer-docselect"
1439
1483
  data-open={navOpen ? 'true' : 'false'}
1440
1484
  >
1485
+ <label htmlFor="drawer-section-picker">Documentation section</label>
1441
1486
  <button
1487
+ id="drawer-section-picker"
1442
1488
  type="button"
1443
1489
  className="velu-docs-layout__drawer-docselect-btn"
1444
1490
  onClick={() => setNavOpen((o) => !o)}
1445
1491
  aria-haspopup="menu"
1446
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
+ }}
1447
1500
  >
1448
1501
  <span className="velu-docs-layout__drawer-docselect-label">
1449
1502
  {activeNavLabel}
@@ -1455,9 +1508,16 @@ function DocsPage() {
1455
1508
  />
1456
1509
  </button>
1457
1510
  <ul
1511
+ id="drawer-section-options"
1458
1512
  className="velu-docs-layout__drawer-docselect-menu"
1459
1513
  role="menu"
1460
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
+ }}
1461
1521
  >
1462
1522
  {navItems.map((it) => (
1463
1523
  <li key={it.href} role="none">
@@ -1480,21 +1540,21 @@ function DocsPage() {
1480
1540
  {/* Context zone — product switcher + anchors at the top of
1481
1541
  the sidebar (all breakpoints), plus version/language
1482
1542
  switchers that only show on mobile (desktop has them in
1483
- the header). Rendered only when there's something to show
1484
- so simple projects keep a bare sidebar. */}
1485
- {(productSwitcher ||
1486
- 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') ||
1487
1547
  languageSwitcher ||
1488
- anchors.length > 0) && (
1548
+ (anchors.length > 0 && site.theme !== 'aalam')) && (
1489
1549
  <Stack space="var(--s-1)" className="velu-docs-context">
1490
- {productSwitcher}
1491
- {versionSwitcher && (
1550
+ {site.theme !== 'aalam' && productSwitcher}
1551
+ {versionSwitcher && site.theme !== 'aalam' && (
1492
1552
  <span className="velu-show-on-mobile">{versionSwitcher}</span>
1493
1553
  )}
1494
1554
  {languageSwitcher && (
1495
1555
  <span className="velu-show-on-mobile">{languageSwitcher}</span>
1496
1556
  )}
1497
- {anchors.length > 0 && (
1557
+ {anchors.length > 0 && site.theme !== 'aalam' && (
1498
1558
  <ul className="velu-docs-anchors" data-component="nav-anchors">
1499
1559
  {anchors.map((a, i) => (
1500
1560
  <li key={i}>
@@ -1523,26 +1583,50 @@ function DocsPage() {
1523
1583
  )}
1524
1584
  {/* Hairline separating the top anchor links from the nav sections
1525
1585
  (faithful to the sidebar design's anchor↔sidebar divider). */}
1526
- {anchors.length > 0 && (
1586
+ {anchors.length > 0 && site.theme !== 'aalam' && (
1527
1587
  <sidebar-nav-group-divider class="velu-docs-context-divider" aria-hidden="true" />
1528
1588
  )}
1529
1589
  {/* Only this region scrolls — the context zone above stays
1530
- pinned. The up/down arrows overlay its top/bottom edges and
1531
- appear (via the data-fade-* attrs the scroll handler sets)
1532
- when there's content beyond that edge; clicking jumps the
1533
- 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. */}
1534
1593
  <div className="velu-docs-nav-region">
1535
1594
  <div
1536
- id={site.theme === 'vepa' ? 'navigation-items' : 'sidebar-content'}
1595
+ id={site.theme === 'vepa' || ['thulir', 'aalam'].includes(site.theme) ? 'navigation-items' : 'sidebar-content'}
1537
1596
  ref={leftAsideRef}
1538
- className="velu-docs-nav-scroll velu-hide-scrollbar"
1597
+ className={`velu-docs-nav-scroll${site.theme === 'aalam' ? '' : ' velu-hide-scrollbar'}`}
1539
1598
  style={{
1540
- /* Just breathing room the aside's bottom edge already
1541
- stays above the footer (see --velu-aside-bottom). */
1542
- paddingBlockEnd: 'var(--s1)',
1599
+ /* scroll-padding onlykeep content clear of sticky
1600
+ headings / footer without shortening the scrollbar. */
1601
+ scrollPaddingBlockStart: '2.75rem',
1543
1602
  scrollPaddingBlockEnd: 'var(--s1)',
1544
1603
  }}
1545
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
+ )}
1546
1630
  {/* Sidebar STYLE is chosen per-tab, not per-page: if this tab
1547
1631
  holds any API page, the whole tab uses the flat API sidebar
1548
1632
  (so opening an intro page next to API operations doesn't flip
@@ -1565,31 +1649,11 @@ function DocsPage() {
1565
1649
  />
1566
1650
  )}
1567
1651
  </div>
1568
- <button
1569
- type="button"
1570
- className="velu-docs-nav-arrow velu-docs-nav-arrow--up"
1571
- aria-label="Scroll navigation to top"
1572
- onClick={() =>
1573
- leftAsideRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
1574
- }
1575
- >
1576
- <ChevronUp aria-hidden="true" focusable="false" />
1577
- </button>
1578
- <button
1579
- type="button"
1580
- className="velu-docs-nav-arrow velu-docs-nav-arrow--down"
1581
- aria-label="Scroll navigation to bottom"
1582
- onClick={() =>
1583
- leftAsideRef.current?.scrollTo({
1584
- top: leftAsideRef.current.scrollHeight,
1585
- behavior: 'smooth',
1586
- })
1587
- }
1588
- >
1589
- <ChevronDown aria-hidden="true" focusable="false" />
1590
- </button>
1591
1652
  </div>
1592
1653
  </Stack>
1654
+ {site.theme === 'aalam' && versionSwitcher && (
1655
+ <div className="velu-docs-aside-footer">{versionSwitcher}</div>
1656
+ )}
1593
1657
  </aside>
1594
1658
 
1595
1659
  {/* No separate rail element — at narrow widths the sidebar's
@@ -1626,12 +1690,12 @@ function DocsPage() {
1626
1690
  />
1627
1691
  ) : isChangelog && changelogTags.length && site.theme !== 'vepa' ? (
1628
1692
  <ChangelogFilters tags={changelogTags} />
1629
- ) : site.theme === 'vepa' ? (
1693
+ ) : site.theme === 'vepa' || ['thulir', 'aalam'].includes(site.theme) ? (
1630
1694
  <>
1631
1695
  {isChangelog && changelogTags.length > 0 ? (
1632
1696
  <ChangelogFilters tags={changelogTags} />
1633
1697
  ) : null}
1634
- <VepaToc items={pageToc} activeId={activeId} onSelect={scrollTo} />
1698
+ <VepaToc items={pageToc} activeId={['thulir', 'aalam'].includes(site.theme) && activeId === pageId ? pageToc[0]?.id : activeId} onSelect={scrollTo} />
1635
1699
  </>
1636
1700
  ) : (
1637
1701
  <Toc id="table-of-contents-content" items={toc} activeId={activeId} onSelect={scrollTo} />
@@ -1676,7 +1740,7 @@ function DocsPage() {
1676
1740
  on tab/mobile we keep the normal TOC bar (no filters). Hidden
1677
1741
  entirely when the page mode has no table of contents. */}
1678
1742
  {chrome.toc && (
1679
- <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} />
1680
1744
  )}
1681
1745
  <main
1682
1746
  id="content-area"
@@ -1716,7 +1780,7 @@ function DocsPage() {
1716
1780
  </span>
1717
1781
  </button>
1718
1782
  )}
1719
- <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 : ""}>
1720
1784
  {/* Per-page agent/IDE action bar: the section eyebrow + a
1721
1785
  "Copy Page" split-button whose dropdown is driven by the
1722
1786
  Mintlify-compatible `contextual` config. Renders nothing
@@ -1746,7 +1810,7 @@ function DocsPage() {
1746
1810
  Custom / frame / assistant are a blank canvas — no auto hero. */}
1747
1811
  {chrome.hero && (frontmatter.title || frontmatter.description) && (
1748
1812
  <div className="velu-hero">
1749
- {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>}
1750
1814
  {frontmatter.description && (
1751
1815
  <p
1752
1816
  style={{
@@ -1767,7 +1831,7 @@ function DocsPage() {
1767
1831
  level. Falls back to a not-found / missing-file notice
1768
1832
  when the route has no page (or its file is absent). */}
1769
1833
  <MDXProvider components={defaultMdxComponents}>
1770
- <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">
1771
1835
  {PageComponent ? (
1772
1836
  <ErrorBoundary key={pathname} file={entry?.relPath}>
1773
1837
  {entry?.mdxApi ? <ApiReferencePage {...entry.mdxApi} preset={site.theme}><PageComponent /></ApiReferencePage> : <PageComponent />}
@@ -1783,7 +1847,7 @@ function DocsPage() {
1783
1847
  </MDXProvider>
1784
1848
  {/* Page-foot feedback widget — ref'd so the sticky AskBar
1785
1849
  above can hide as the user scrolls near it. */}
1786
- {chrome.articleChrome && (
1850
+ {chrome.articleChrome && !['thulir', 'aalam'].includes(site.theme) && (
1787
1851
  <div ref={setFeedbackEl} style={{ marginTop: 'var(--s3)' }}>
1788
1852
  {/* key resets the widget's vote state on page change. "Yes"
1789
1853
  submits immediately; "No" submits once its form is sent.
@@ -1804,7 +1868,8 @@ function DocsPage() {
1804
1868
  {chrome.articleChrome && !frontmatter.hidePagination && (nav?.prev || nav?.next) && (
1805
1869
  <PageNav
1806
1870
  id="pagination"
1807
- style={{ marginTop: 'var(--s2)' }}
1871
+ style={['thulir', 'aalam'].includes(site.theme) ? undefined : { marginTop: 'var(--s2)' }}
1872
+ variant={site.theme === 'aalam' ? 'aalam' : 'default'}
1808
1873
  prev={nav?.prev}
1809
1874
  next={nav?.next}
1810
1875
  linkComponent={RouterLink}
@@ -1844,7 +1909,7 @@ function DocsPage() {
1844
1909
  full footer section); with link columns they live in the full
1845
1910
  footer below instead. */}
1846
1911
  {chrome.articleChrome && (footerHasLinks ? (
1847
- site.theme === 'vepa' ? null : <PoweredBy />
1912
+ site.theme === 'vepa' || ['thulir', 'aalam'].includes(site.theme) ? null : <PoweredBy />
1848
1913
  ) : (
1849
1914
  <div className="velu-content-foot">
1850
1915
  <SocialLinks socials={footerSocials} />
@@ -1857,13 +1922,13 @@ function DocsPage() {
1857
1922
  )}
1858
1923
  </div>
1859
1924
 
1860
- {/* Full site footer — only when the config provides link columns. Spans
1861
- full width below the article; its raised z-index eclipses the bottoms
1862
- of the fixed sidebar and TOC as the page scrolls into it, and the ref
1863
- 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. */}
1864
1929
  {footerHasLinks && chrome.footer && (
1865
1930
  <advanced-footer
1866
- id={site.theme === 'vepa' ? undefined : 'footer'}
1931
+ id={site.theme === 'vepa' || ['thulir', 'aalam'].includes(site.theme) ? undefined : 'footer'}
1867
1932
  ref={setFooterEl}
1868
1933
  data-velu-footer
1869
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
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Introduction
3
3
  description: How this API reference is generated.
4
+ icon: terminal
4
5
  ---
5
6
 
6
7
  The endpoints in this tab are generated automatically from an OpenAPI
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Development
3
3
  description: Preview your docs locally as you write.
4
+ icon: code
4
5
  ---
5
6
 
6
7
  `velu dev` serves your project with server-side rendering and live
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Code blocks
3
3
  description: Syntax-highlighted code and groups.
4
+ icon: square-code
4
5
  ---
5
6
 
6
7
  Fenced code blocks are upgraded to a rich code component with a copy
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Images
3
3
  description: Add images and visual media.
4
+ icon: image
4
5
  ---
5
6
 
6
7
  Use standard Markdown images, or the `Image` component for captions
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Markdown
3
3
  description: Author with Markdown and components.
4
+ icon: file-text
4
5
  ---
5
6
 
6
7
  Standard Markdown works everywhere: **bold**, _italic_, `inline code`,
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Navigation
3
3
  description: Organize your docs with tabs, groups, and pages.
4
+ icon: map
4
5
  ---
5
6
 
6
7
  `navigation` in `velu.json` is a tree. The pieces, outermost to
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Settings
3
3
  description: Theme your site through velu.json.
4
+ icon: settings
4
5
  ---
5
6
 
6
7
  Your site's look comes from a few top-level keys in `velu.json`.
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Introduction
3
3
  description: Welcome to your Velu documentation site.
4
+ icon: book-open
4
5
  ---
5
6
 
6
7
  This is your new documentation site, rendered by **Velu** from plain
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Quickstart
3
3
  description: Get your documentation site running in a minute.
4
+ icon: rocket
4
5
  ---
5
6
 
6
7
  ## Start the dev server