@veluai/velu 0.2.35 → 0.2.37

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 (59) hide show
  1. package/README.md +56 -0
  2. package/dist/cli.js +129 -54
  3. package/docs/mintlify-migration.md +159 -0
  4. package/docs/vepa.md +96 -0
  5. package/package.json +8 -2
  6. package/runtime/velu-ui/components/Accordion.jsx +3 -3
  7. package/runtime/velu-ui/components/ApiClient.jsx +4 -2
  8. package/runtime/velu-ui/components/ApiReferencePage.jsx +23 -15
  9. package/runtime/velu-ui/components/ApiSamples.jsx +9 -4
  10. package/runtime/velu-ui/components/AskBar.jsx +3 -2
  11. package/runtime/velu-ui/components/Callout.jsx +11 -26
  12. package/runtime/velu-ui/components/Card.jsx +16 -12
  13. package/runtime/velu-ui/components/ChangelogFilters.jsx +2 -1
  14. package/runtime/velu-ui/components/Chatbot.jsx +20 -9
  15. package/runtime/velu-ui/components/CodeBlock.jsx +7 -6
  16. package/runtime/velu-ui/components/Columns.jsx +2 -0
  17. package/runtime/velu-ui/components/ContextMenu.jsx +3 -2
  18. package/runtime/velu-ui/components/Field.jsx +2 -2
  19. package/runtime/velu-ui/components/Icon.jsx +1 -0
  20. package/runtime/velu-ui/components/Image.jsx +2 -2
  21. package/runtime/velu-ui/components/MethodBadge.jsx +2 -2
  22. package/runtime/velu-ui/components/NavSelect.jsx +22 -4
  23. package/runtime/velu-ui/components/NotFound.jsx +7 -7
  24. package/runtime/velu-ui/components/PageFeedback.jsx +13 -3
  25. package/runtime/velu-ui/components/PageFooter.jsx +3 -0
  26. package/runtime/velu-ui/components/PageHeader.jsx +37 -15
  27. package/runtime/velu-ui/components/PageNav.jsx +2 -1
  28. package/runtime/velu-ui/components/Prompt.jsx +2 -2
  29. package/runtime/velu-ui/components/Search.jsx +1 -0
  30. package/runtime/velu-ui/components/Sidebar.jsx +18 -6
  31. package/runtime/velu-ui/components/Steps.jsx +4 -4
  32. package/runtime/velu-ui/components/ThemePreferenceMenu.jsx +57 -0
  33. package/runtime/velu-ui/components/ThemeToggle.jsx +4 -1
  34. package/runtime/velu-ui/components/Toc.jsx +6 -4
  35. package/runtime/velu-ui/components/Tree.jsx +6 -5
  36. package/runtime/velu-ui/components/TryItBar.jsx +1 -0
  37. package/runtime/velu-ui/components/Update.jsx +2 -2
  38. package/runtime/velu-ui/components/VepaFooter.jsx +40 -0
  39. package/runtime/velu-ui/components/callout.css +28 -0
  40. package/runtime/velu-ui/components/card.css +9 -0
  41. package/runtime/velu-ui/components/chatbot.css +42 -0
  42. package/runtime/velu-ui/components/docs-layout.css +80 -0
  43. package/runtime/velu-ui/components/page-header.css +6 -0
  44. package/runtime/velu-ui/components/sidebar.css +5 -5
  45. package/runtime/velu-ui/element-selectors.css +66 -0
  46. package/runtime/velu-ui/primitives/Cluster.jsx +12 -0
  47. package/runtime/velu-ui/primitives/Stack.jsx +12 -0
  48. package/runtime/velu-ui/primitives/Switcher.jsx +11 -1
  49. package/runtime/velu-ui/styles.css +52 -35
  50. package/runtime/velu-ui/themes/vepa.css +264 -0
  51. package/schema/velu.schema.json +5 -0
  52. package/src/navigation.js +3 -2
  53. package/src/runtime/App.jsx +144 -52
  54. package/src/runtime/VepaToc.jsx +12 -0
  55. package/src/runtime/client-entry.jsx +23 -0
  56. package/src/runtime/dev-warnings.js +16 -0
  57. package/src/runtime/page-mode.js +54 -0
  58. package/src/runtime/server-entry.jsx +1 -0
  59. package/templates/starter/essentials/settings.mdx +15 -0
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import VepaToc from './VepaToc.jsx';
2
3
  import { Routes, Route, useLocation, useNavigate, Link } from 'react-router-dom';
3
4
  import { MDXProvider } from '@mdx-js/react';
4
5
  // The project's pages + navigation, generated from velu.json by
@@ -6,6 +7,7 @@ import { MDXProvider } from '@mdx-js/react';
6
7
  // url → { Component, frontmatter, toc } (or { missing:true }).
7
8
  import { pages, navigation, site } from 'virtual:velu-site';
8
9
  import { resolve, normalizeUrl } from '../navigation.js';
10
+ import { resolvePageMode, pageModeChrome } from './page-mode.js';
9
11
  import {
10
12
  Stack,
11
13
  Cluster,
@@ -53,6 +55,7 @@ import {
53
55
  ApiField,
54
56
  ApiSidebar,
55
57
  ApiSamples,
58
+ ApiReferencePage,
56
59
  NotFound,
57
60
  VeluMark,
58
61
  } from 'velu-ui';
@@ -683,6 +686,19 @@ function ImageDemo() {
683
686
 
684
687
  const RouterLink = ({ href, ...rest }) => <Link to={href} {...rest} />;
685
688
 
689
+ // Look up a heading by its slug id. Prefers a match INSIDE the article: the
690
+ // layout chrome carries stable, Mintlify-compatible ids for custom CSS
691
+ // (#sidebar, #content, #footer, …), and a heading titled "Sidebar" or "Footer"
692
+ // would slug to the same id — document.getElementById would then return the
693
+ // chrome element (earlier in DOM order) and the TOC would scroll to the wrong
694
+ // place.
695
+ function sectionById(id) {
696
+ if (typeof document === 'undefined' || !id) return null;
697
+ const article = document.querySelector('.velu-docs-layout__article');
698
+ const esc = id.replace(/["\\]/g, '\\$&');
699
+ return article?.querySelector(`[id="${esc}"]`) ?? document.getElementById(id);
700
+ }
701
+
686
702
  // Flatten a nested TOC tree to {id, label, depth} for scroll-spy.
687
703
  function flat(nodes, depth = 0, out = []) {
688
704
  for (const n of nodes) {
@@ -730,7 +746,7 @@ function useScrollSpy(ids) {
730
746
  let best = ids[0];
731
747
  let bestDist = Infinity;
732
748
  for (const id of ids) {
733
- const node = document.getElementById(id);
749
+ const node = sectionById(id);
734
750
  if (!node) continue;
735
751
  const top = node.getBoundingClientRect().top;
736
752
  if (top >= vh) continue;
@@ -842,6 +858,10 @@ function DocsPage() {
842
858
  // No renderable page for this route → show the 404 page (a clean centered
843
859
  // takeover: header + footer stay, the docs sidebar/TOC are hidden).
844
860
  const isNotFound = !PageComponent;
861
+ // Mintlify-compatible page layout (`mode` in MDX frontmatter). Unknown
862
+ // values fall back to the standard sidebar + TOC + footer chrome.
863
+ const chrome = pageModeChrome(resolvePageMode(frontmatter));
864
+ const assistantUnavailable = chrome.assistant && !IS_DEV_PREVIEW && (!assistant || askAiHidden);
845
865
 
846
866
  // Switcher option sets (only render a switcher when an axis has >1
847
867
  // option). Anchors are pinned sidebar links shown in the context zone.
@@ -867,6 +887,7 @@ function DocsPage() {
867
887
  options={languageOptions}
868
888
  linkComponent={RouterLink}
869
889
  ariaLabel="Language"
890
+ idPrefix="localization-select"
870
891
  />
871
892
  );
872
893
  const productSwitcher = productOptions.length > 1 && (
@@ -875,6 +896,7 @@ function DocsPage() {
875
896
  options={productOptions}
876
897
  linkComponent={RouterLink}
877
898
  ariaLabel="Product"
899
+ component="nav-dropdown-products-selector"
878
900
  />
879
901
  );
880
902
 
@@ -897,14 +919,18 @@ function DocsPage() {
897
919
  }
898
920
  descTag.setAttribute('content', frontmatter.description || '');
899
921
  document.documentElement.lang = nav?.activeLanguageCode || 'en';
922
+ // Mirrors the SSR'd attribute so custom CSS scoped with
923
+ // html[data-current-path="/x"] follows client-side navigation too.
924
+ document.documentElement.dataset.currentPath = pathname;
900
925
  }, [pathname, isNotFound, frontmatter.title, frontmatter.description, nav?.activeLanguageCode]);
901
926
 
902
927
  // Frontmatter title needs an id so scroll-spy + click-to-scroll work
903
928
  // against it like any other heading.
904
929
  const pageId = React.useMemo(() => {
905
930
  if (!frontmatter.title) return null;
931
+ if (site.theme === 'vepa') return 'page-title';
906
932
  return new GithubSlugger().slug(frontmatter.title);
907
- }, [frontmatter.title]);
933
+ }, [frontmatter.title, site.theme]);
908
934
 
909
935
  // Combined TOC: the frontmatter title is the page's top-level entry,
910
936
  // and the MDX-derived headings (h2s) become its children. Without this,
@@ -1040,7 +1066,11 @@ function DocsPage() {
1040
1066
  const tocBarToggle = document.querySelector('.velu-toc-bar__toggle');
1041
1067
  if (tocBarToggle) ro.observe(tocBarToggle);
1042
1068
  return () => ro.disconnect();
1043
- }, []);
1069
+ // Re-run when page mode changes: custom/assistant drop the tabs
1070
+ // row and shrink the header. A mount-only observer can leave
1071
+ // --velu-header-height at the two-row 7rem default, which leaves
1072
+ // a gap above the full-page Ask AI canvas.
1073
+ }, [chrome.mode]);
1044
1074
 
1045
1075
  // The asides (left sidebar + right TOC) scroll independently of the
1046
1076
  // page. On scroll/resize we set data-fade-top/-bottom on each scroll
@@ -1161,17 +1191,26 @@ function DocsPage() {
1161
1191
  label: 'Log out',
1162
1192
  onClick: logoutPrivateAccess,
1163
1193
  kind: 'outlined',
1194
+ component: 'logout-link',
1164
1195
  });
1165
1196
  }
1166
1197
 
1167
- const footerRef = React.useRef(null);
1198
+ // Callback ref so this rebinds when the footer mounts/unmounts.
1199
+ // Custom and assistant unmount the footer; a mount-once listener
1200
+ // 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.
1203
+ const [footerEl, setFooterEl] = React.useState(null);
1168
1204
  const [footerOverlap, setFooterOverlap] = React.useState(0);
1169
1205
  React.useEffect(() => {
1170
- const node = footerRef.current;
1171
- if (!node) return;
1206
+ if (!footerEl) {
1207
+ setFooterOverlap(0);
1208
+ return undefined;
1209
+ }
1172
1210
  const update = () => {
1173
- const rect = node.getBoundingClientRect();
1174
- setFooterOverlap(Math.max(0, window.innerHeight - rect.top));
1211
+ const rect = footerEl.getBoundingClientRect();
1212
+ const visible = Math.max(0, window.innerHeight - rect.top);
1213
+ setFooterOverlap(Math.min(visible, footerEl.offsetHeight));
1175
1214
  };
1176
1215
  update();
1177
1216
  window.addEventListener('scroll', update, { passive: true });
@@ -1180,15 +1219,17 @@ function DocsPage() {
1180
1219
  window.removeEventListener('scroll', update);
1181
1220
  window.removeEventListener('resize', update);
1182
1221
  };
1183
- }, []);
1222
+ }, [footerEl]);
1184
1223
 
1185
1224
  // Sticky AskBar fades out once the user scrolls near the PageFeedback
1186
1225
  // widget — so it doesn't sit on top of the page-foot widgets.
1187
- const feedbackRef = React.useRef(null);
1226
+ const [feedbackEl, setFeedbackEl] = React.useState(null);
1188
1227
  const [askBarHidden, setAskBarHidden] = React.useState(false);
1189
1228
  React.useEffect(() => {
1190
- const node = feedbackRef.current;
1191
- if (!node) return;
1229
+ if (!feedbackEl) {
1230
+ setAskBarHidden(false);
1231
+ return undefined;
1232
+ }
1192
1233
  const FADE_BUFFER = 64; // matches the rootMargin below
1193
1234
  const apply = (rect) => {
1194
1235
  // Hide whenever the feedback's top has reached the (effective)
@@ -1198,20 +1239,20 @@ function DocsPage() {
1198
1239
  // exactly at the same line in both scroll directions.
1199
1240
  setAskBarHidden(rect.top < window.innerHeight - FADE_BUFFER);
1200
1241
  };
1201
- apply(node.getBoundingClientRect());
1242
+ apply(feedbackEl.getBoundingClientRect());
1202
1243
  const io = new IntersectionObserver(
1203
1244
  ([entry]) => apply(entry.boundingClientRect),
1204
1245
  // Trigger ~64px before the feedback enters the viewport, so the
1205
1246
  // AskBar fades just as the widget starts to peek up from below.
1206
1247
  { rootMargin: '0px 0px -64px 0px' },
1207
1248
  );
1208
- io.observe(node);
1249
+ io.observe(feedbackEl);
1209
1250
  return () => io.disconnect();
1210
- }, []);
1251
+ }, [feedbackEl]);
1211
1252
 
1212
1253
  const scrollTo = React.useCallback(
1213
1254
  (id) => {
1214
- const node = document.getElementById(id);
1255
+ const node = sectionById(id);
1215
1256
  if (!node) return;
1216
1257
  // 1) Set + lock the active id IMMEDIATELY so the spy doesn't
1217
1258
  // overwrite it during the smooth-scroll animation.
@@ -1242,12 +1283,15 @@ function DocsPage() {
1242
1283
 
1243
1284
  return (
1244
1285
  <div
1286
+ id="body-content"
1245
1287
  className="velu-docs-layout"
1246
- data-chat-open={chatOpen ? 'true' : 'false'}
1288
+ data-site-theme={site.theme ?? 'velu'}
1289
+ data-chat-open={chatOpen && !chrome.assistant ? 'true' : 'false'}
1247
1290
  data-sidebar-open={sidebarOpen ? 'true' : 'false'}
1248
1291
  data-drawer-open={drawerOpen ? 'true' : 'false'}
1249
1292
  data-api={entry?.api ? 'true' : 'false'}
1250
1293
  data-not-found={isNotFound ? 'true' : undefined}
1294
+ data-page-mode={isNotFound ? undefined : chrome.mode}
1251
1295
  >
1252
1296
  {/* Scrim — visible at mobile while the drawer OR the chatbot
1253
1297
  sheet is open. Sits between the article (z-0) and the
@@ -1264,6 +1308,8 @@ function DocsPage() {
1264
1308
  {/* Site header — brand, centered search, right-side actions,
1265
1309
  tabs row. Configurable: pass any number of actions / tabs. */}
1266
1310
  <PageHeader
1311
+ preset={site.theme}
1312
+ id="navbar"
1267
1313
  linkComponent={RouterLink}
1268
1314
  brand={{
1269
1315
  label: site.name,
@@ -1275,10 +1321,10 @@ function DocsPage() {
1275
1321
  <span className="velu-hide-on-mobile">{versionSwitcher}</span>
1276
1322
  )
1277
1323
  }
1278
- tabsTrailing={languageSwitcher || undefined}
1279
1324
  center={
1280
1325
  <Cluster space="var(--s-6)" align="center">
1281
1326
  <Search
1327
+ id="search-bar-entry"
1282
1328
  style={{ inlineSize: '30ch' }}
1283
1329
  unavailable={IS_DEV_PREVIEW}
1284
1330
  search={IS_DEV_PREVIEW ? undefined : searchDocs}
@@ -1287,9 +1333,10 @@ function DocsPage() {
1287
1333
  {/* Ask AI talks to the deployed site's AI backend — hidden in
1288
1334
  the local dev preview where there's nothing to talk to, and
1289
1335
  when the owner's plan/credits disable the assistant. */}
1290
- {!IS_DEV_PREVIEW && !askAiHidden && (
1336
+ {!IS_DEV_PREVIEW && !askAiHidden && !chrome.assistant && (
1291
1337
  <button
1292
1338
  type="button"
1339
+ id="assistant-entry"
1293
1340
  className="velu-header__action velu-header__action--outlined"
1294
1341
  onClick={() => askAI('')}
1295
1342
  >
@@ -1302,11 +1349,24 @@ function DocsPage() {
1302
1349
  </Cluster>
1303
1350
  }
1304
1351
  actions={navActions}
1305
- trailing={<ThemeToggle />}
1306
- onMenuClick={() => setDrawerOpen((v) => !v)}
1307
- breadcrumb={nav?.breadcrumb ?? []}
1352
+ trailing={<ThemeToggle variant={site.theme === 'vepa' ? 'menu' : undefined} />}
1353
+ tabsTrailing={
1354
+ chrome.mode === 'custom' || chrome.mode === 'assistant'
1355
+ ? undefined
1356
+ : languageSwitcher || undefined
1357
+ }
1358
+ onMenuClick={chrome.sidebar ? () => setDrawerOpen((v) => !v) : undefined}
1359
+ breadcrumb={
1360
+ chrome.mode === 'custom' || chrome.mode === 'assistant'
1361
+ ? []
1362
+ : (nav?.breadcrumb ?? [])
1363
+ }
1308
1364
  activeTab={nav?.activeTab}
1309
- tabs={nav?.tabs ?? []}
1365
+ tabs={
1366
+ chrome.mode === 'custom' || chrome.mode === 'assistant'
1367
+ ? []
1368
+ : (nav?.tabs ?? [])
1369
+ }
1310
1370
  />
1311
1371
 
1312
1372
  {/* Fixed left sidebar — pinned to viewport-left below the header.
@@ -1316,6 +1376,7 @@ function DocsPage() {
1316
1376
  honour `inset-block-start` for fixed positioning the same way
1317
1377
  as plain `top`). */}
1318
1378
  <aside
1379
+ id={site.theme === 'vepa' ? 'sidebar-content' : 'sidebar'}
1319
1380
  className="velu-docs-layout__aside velu-docs-layout__aside--left"
1320
1381
  style={{
1321
1382
  /* Bottom edge stays a fixed gap above the viewport bottom,
@@ -1400,6 +1461,8 @@ function DocsPage() {
1400
1461
  <li key={it.href} role="none">
1401
1462
  <a
1402
1463
  role="menuitem"
1464
+ data-component="mobile-nav-tabs-item"
1465
+ data-active={it.href === nav?.activeTab ? 'true' : undefined}
1403
1466
  className="velu-docs-layout__drawer-docselect-item"
1404
1467
  href={it.href}
1405
1468
  tabIndex={navOpen ? 0 : -1}
@@ -1429,10 +1492,11 @@ function DocsPage() {
1429
1492
  <span className="velu-show-on-mobile">{languageSwitcher}</span>
1430
1493
  )}
1431
1494
  {anchors.length > 0 && (
1432
- <ul className="velu-docs-anchors">
1495
+ <ul className="velu-docs-anchors" data-component="nav-anchors">
1433
1496
  {anchors.map((a, i) => (
1434
1497
  <li key={i}>
1435
1498
  <a
1499
+ data-component="nav-anchor"
1436
1500
  className="velu-docs-anchors__link"
1437
1501
  href={a.href}
1438
1502
  target="_blank"
@@ -1457,7 +1521,7 @@ function DocsPage() {
1457
1521
  {/* Hairline separating the top anchor links from the nav sections
1458
1522
  (faithful to the sidebar design's anchor↔sidebar divider). */}
1459
1523
  {anchors.length > 0 && (
1460
- <div className="velu-docs-context-divider" aria-hidden="true" />
1524
+ <sidebar-nav-group-divider class="velu-docs-context-divider" aria-hidden="true" />
1461
1525
  )}
1462
1526
  {/* Only this region scrolls — the context zone above stays
1463
1527
  pinned. The up/down arrows overlay its top/bottom edges and
@@ -1466,6 +1530,7 @@ function DocsPage() {
1466
1530
  nav fully to that end. */}
1467
1531
  <div className="velu-docs-nav-region">
1468
1532
  <div
1533
+ id={site.theme === 'vepa' ? 'navigation-items' : 'sidebar-content'}
1469
1534
  ref={leftAsideRef}
1470
1535
  className="velu-docs-nav-scroll velu-hide-scrollbar"
1471
1536
  style={{
@@ -1533,9 +1598,11 @@ function DocsPage() {
1533
1598
  display:none snap. See docs-layout.css. */}
1534
1599
 
1535
1600
  {/* Fixed right TOC — pinned to viewport-right. Hidden while the
1536
- Ask-AI chatbot is open (the panel takes that edge). */}
1537
- {!chatOpen && (
1601
+ Ask-AI chatbot is open (the panel takes that edge). Wide /
1602
+ custom / frame / center / assistant hide this rail. */}
1603
+ {!chatOpen && chrome.toc && (
1538
1604
  <aside
1605
+ id="table-of-contents"
1539
1606
  ref={rightAsideRef}
1540
1607
  className="velu-docs-layout__aside velu-docs-layout__aside--right velu-hide-scrollbar"
1541
1608
  style={{
@@ -1549,13 +1616,15 @@ function DocsPage() {
1549
1616
  (where the TOC sits for normal pages). */}
1550
1617
  {entry?.api && entry.operation ? (
1551
1618
  <ApiSamples
1619
+ preset={site.theme}
1620
+ title={frontmatter.title}
1552
1621
  samples={entry.samples}
1553
1622
  responses={entry.operation.responses}
1554
1623
  />
1555
1624
  ) : isChangelog && changelogTags.length ? (
1556
1625
  <ChangelogFilters tags={changelogTags} />
1557
1626
  ) : (
1558
- <Toc items={toc} activeId={activeId} onSelect={scrollTo} />
1627
+ site.theme === 'vepa' ? <VepaToc items={pageToc} activeId={activeId} onSelect={scrollTo} /> : <Toc id="table-of-contents-content" items={toc} activeId={activeId} onSelect={scrollTo} />
1559
1628
  )}
1560
1629
  </aside>
1561
1630
  )}
@@ -1578,7 +1647,14 @@ function DocsPage() {
1578
1647
  onAskAI={IS_DEV_PREVIEW || askAiHidden ? undefined : () => askAI('')}
1579
1648
  />
1580
1649
  </main>
1581
- ) : (
1650
+ ) : chrome.assistant && assistantUnavailable ? (
1651
+ <main className="velu-assistant-disabled">
1652
+ <Search
1653
+ unavailable
1654
+ unavailableMessage="Ask AI is not enabled on this site."
1655
+ />
1656
+ </main>
1657
+ ) : chrome.assistant ? null : (
1582
1658
  <>
1583
1659
  {/* Narrow-layout TOC bar — always in DOM, only visible at
1584
1660
  < 1024px (toggled by @container in toc-bar.css). Shares
@@ -1587,9 +1663,13 @@ function DocsPage() {
1587
1663
  data; whichever one is visible at a given width responds
1588
1664
  to the same scroll-spy state. */}
1589
1665
  {/* Changelog tag filters are a wide-layout (right-rail) affordance only;
1590
- on tab/mobile we keep the normal TOC bar (no filters). */}
1666
+ on tab/mobile we keep the normal TOC bar (no filters). Hidden
1667
+ entirely when the page mode has no table of contents. */}
1668
+ {chrome.toc && (
1591
1669
  <TocBar items={toc} activeId={activeId} onSelect={scrollTo} />
1670
+ )}
1592
1671
  <main
1672
+ id="content-area"
1593
1673
  className="velu-docs-layout__main"
1594
1674
  style={{
1595
1675
  /* No padding-block-end — the article's last child (PoweredBy)
@@ -1611,6 +1691,7 @@ function DocsPage() {
1611
1691
  column, and therefore sits BELOW the TocBar rather than
1612
1692
  overlapping it. Chevron flips direction with the
1613
1693
  `data-sidebar-open` data attribute on the layout root. */}
1694
+ {chrome.sidebar && (
1614
1695
  <button
1615
1696
  type="button"
1616
1697
  className="velu-docs-layout__sidebar-toggle"
@@ -1624,11 +1705,13 @@ function DocsPage() {
1624
1705
  })}
1625
1706
  </span>
1626
1707
  </button>
1627
- <div className="velu-docs-layout__article" data-pagefind-body="">
1708
+ )}
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 : ""}>
1628
1710
  {/* Per-page agent/IDE action bar: the section eyebrow + a
1629
1711
  "Copy Page" split-button whose dropdown is driven by the
1630
1712
  Mintlify-compatible `contextual` config. Renders nothing
1631
1713
  when there's neither an eyebrow nor any enabled options. */}
1714
+ {chrome.articleChrome && (
1632
1715
  <ContextMenu
1633
1716
  eyebrow={
1634
1717
  nav?.breadcrumb && nav.breadcrumb.length > 1
@@ -1645,13 +1728,15 @@ function DocsPage() {
1645
1728
  getMarkdown={entry?.getSource}
1646
1729
  rssHref={isChangelog ? `${BASE_PATH}/rss.xml` : undefined}
1647
1730
  />
1731
+ )}
1648
1732
 
1649
1733
  {/* Page hero from MDX frontmatter. `.velu-hero` rules
1650
1734
  (in base.css) keep the title and description tightly
1651
- grouped and create a clear break before the prose body. */}
1652
- {(frontmatter.title || frontmatter.description) && (
1735
+ grouped and create a clear break before the prose body.
1736
+ Custom / frame / assistant are a blank canvas — no auto hero. */}
1737
+ {chrome.hero && (frontmatter.title || frontmatter.description) && (
1653
1738
  <div className="velu-hero">
1654
- {frontmatter.title && <h1 id={pageId}>{frontmatter.title}</h1>}
1739
+ {frontmatter.title && <h1 id={site.theme === 'vepa' ? 'page-title' : pageId}>{frontmatter.title}</h1>}
1655
1740
  {frontmatter.description && (
1656
1741
  <p
1657
1742
  style={{
@@ -1672,10 +1757,10 @@ function DocsPage() {
1672
1757
  level. Falls back to a not-found / missing-file notice
1673
1758
  when the route has no page (or its file is absent). */}
1674
1759
  <MDXProvider components={defaultMdxComponents}>
1675
- <div className="velu-prose">
1760
+ <mdx-content key={site.theme === 'vepa' ? pathname : undefined} id={site.theme === 'vepa' && !entry?.api ? 'content' : undefined} class="velu-prose">
1676
1761
  {PageComponent ? (
1677
1762
  <ErrorBoundary key={pathname} file={entry?.relPath}>
1678
- <PageComponent />
1763
+ {entry?.mdxApi ? <ApiReferencePage {...entry.mdxApi} preset={site.theme}><PageComponent /></ApiReferencePage> : <PageComponent />}
1679
1764
  </ErrorBoundary>
1680
1765
  ) : (
1681
1766
  <p style={{ color: 'var(--muted-color)' }}>
@@ -1684,11 +1769,12 @@ function DocsPage() {
1684
1769
  : 'Page not found.'}
1685
1770
  </p>
1686
1771
  )}
1687
- </div>
1772
+ </mdx-content>
1688
1773
  </MDXProvider>
1689
1774
  {/* Page-foot feedback widget — ref'd so the sticky AskBar
1690
1775
  above can hide as the user scrolls near it. */}
1691
- <div ref={feedbackRef} style={{ marginTop: 'var(--s3)' }}>
1776
+ {chrome.articleChrome && (
1777
+ <div ref={setFeedbackEl} style={{ marginTop: 'var(--s3)' }}>
1692
1778
  {/* key resets the widget's vote state on page change. "Yes"
1693
1779
  submits immediately; "No" submits once its form is sent.
1694
1780
  Handlers are no-ops in the dev preview (pageFeedback null). */}
@@ -1702,10 +1788,12 @@ function DocsPage() {
1702
1788
  }
1703
1789
  />
1704
1790
  </div>
1791
+ )}
1705
1792
  {/* Previous / next page navigation — derived from the
1706
1793
  sidebar reading order of the active section. */}
1707
- {(nav?.prev || nav?.next) && (
1794
+ {chrome.articleChrome && !frontmatter.hidePagination && (nav?.prev || nav?.next) && (
1708
1795
  <PageNav
1796
+ id="pagination"
1709
1797
  style={{ marginTop: 'var(--s2)' }}
1710
1798
  prev={nav?.prev}
1711
1799
  next={nav?.next}
@@ -1719,7 +1807,7 @@ function DocsPage() {
1719
1807
  approaches (see IntersectionObserver above). */}
1720
1808
  {/* Ask-a-question bar — opens the AI chatbot, which runs on the
1721
1809
  deployed site. Hidden in the local dev preview. */}
1722
- {!IS_DEV_PREVIEW && !chatOpen && !askAiHidden && (
1810
+ {!IS_DEV_PREVIEW && !chatOpen && !askAiHidden && chrome.articleChrome && (
1723
1811
  <AskBar
1724
1812
  onSubmit={askAI}
1725
1813
  style={{
@@ -1745,14 +1833,14 @@ function DocsPage() {
1745
1833
  configured social icons sit on the LEFT of this same row (no
1746
1834
  full footer section); with link columns they live in the full
1747
1835
  footer below instead. */}
1748
- {footerHasLinks ? (
1749
- <PoweredBy />
1836
+ {chrome.articleChrome && (footerHasLinks ? (
1837
+ site.theme === 'vepa' ? null : <PoweredBy />
1750
1838
  ) : (
1751
1839
  <div className="velu-content-foot">
1752
1840
  <SocialLinks socials={footerSocials} />
1753
1841
  <PoweredBy />
1754
1842
  </div>
1755
- )}
1843
+ ))}
1756
1844
  </div>
1757
1845
  </main>
1758
1846
  </>
@@ -1763,27 +1851,30 @@ function DocsPage() {
1763
1851
  full width below the article; its raised z-index eclipses the bottoms
1764
1852
  of the fixed sidebar and TOC as the page scrolls into it, and the ref
1765
1853
  is watched so the asides can pad their bottom by the overlap. */}
1766
- {footerHasLinks && (
1767
- <div
1768
- ref={footerRef}
1854
+ {footerHasLinks && chrome.footer && (
1855
+ <advanced-footer
1856
+ id={site.theme === 'vepa' ? undefined : 'footer'}
1857
+ ref={setFooterEl}
1769
1858
  data-velu-footer
1770
1859
  style={{ position: 'relative', zIndex: 20 }}
1771
1860
  >
1772
1861
  <PageFooter
1773
- brand={{ href: site.logo?.href || '/' }}
1862
+ preset={site.theme}
1863
+ brand={{ label: site.name, logo: site.logo, href: site.logo?.href || '/' }}
1774
1864
  columns={site.footer.links}
1775
1865
  socials={footerSocials}
1776
1866
  />
1777
- </div>
1867
+ </advanced-footer>
1778
1868
  )}
1779
1869
 
1780
1870
  {/* Ask-AI chatbot — slides in from the inline-end edge. Hidden when the
1781
1871
  docs owner's plan/credits disable Ask-AI. */}
1782
- {!askAiHidden && (
1872
+ {!askAiHidden && (!chrome.assistant || !assistantUnavailable) && (
1783
1873
  <Chatbot
1784
- open={chatOpen}
1874
+ open={chrome.assistant || chatOpen}
1875
+ layout={chrome.assistant ? 'page' : 'panel'}
1785
1876
  seedQuestion={chatQuestion}
1786
- onClose={() => setChatOpen(false)}
1877
+ onClose={chrome.assistant ? undefined : () => setChatOpen(false)}
1787
1878
  ask={assistant?.ask}
1788
1879
  onFeedback={assistant?.sendFeedback}
1789
1880
  listHistory={assistant?.listConversations}
@@ -1806,6 +1897,7 @@ function DocsPage() {
1806
1897
  // behind the sheet. Desktop keeps the panel open so multi-cite hops
1807
1898
  // stay convenient.
1808
1899
  if (
1900
+ !chrome.assistant &&
1809
1901
  typeof window !== 'undefined' &&
1810
1902
  window.matchMedia('(max-width: 1024px)').matches
1811
1903
  ) {
@@ -1817,7 +1909,7 @@ function DocsPage() {
1817
1909
  let tries = 0;
1818
1910
  const go = () => {
1819
1911
  if (hash) {
1820
- if (document.getElementById(hash)) { scrollTo(hash); return; }
1912
+ if (sectionById(hash)) { scrollTo(hash); return; }
1821
1913
  if (tries++ < 10) { setTimeout(go, 60); return; }
1822
1914
  }
1823
1915
  window.scrollTo({ top: 0, behavior: 'smooth' });
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+
3
+ export default function VepaToc({ items, activeId, onSelect }) {
4
+ const render = (entries) => <ul>{entries.map((item) => <li key={item.id}>
5
+ <a href={`#${item.id}`} aria-current={activeId === item.id ? 'location' : undefined} onClick={(e) => { e.preventDefault(); onSelect(item.id); }}>{item.label}</a>
6
+ {item.children?.length > 0 && render(item.children)}
7
+ </li>)}</ul>;
8
+ return <nav className="velu-vepa-toc" aria-label="On this page">
9
+ <button type="button" onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}>On this page</button>
10
+ {render(items)}
11
+ </nav>;
12
+ }
@@ -1,7 +1,9 @@
1
+ import './dev-warnings.js';
1
2
  import React from 'react';
2
3
  import { hydrateRoot } from 'react-dom/client';
3
4
  import { BrowserRouter } from 'react-router-dom';
4
5
  import 'velu-ui/styles.css';
6
+ import { site } from 'virtual:velu-site';
5
7
  import App from './App.jsx';
6
8
 
7
9
  // Subpath hosting: Vite sets BASE_URL from the build's `base` (e.g. "/docs/").
@@ -9,10 +11,31 @@ import App from './App.jsx';
9
11
  // case ("/") becomes undefined so the router behaves as if mounted at origin.
10
12
  const basename = (import.meta.env.BASE_URL || '/').replace(/\/$/, '') || undefined;
11
13
 
14
+ // Custom JavaScript (Mintlify-compatible): every `.js` file in the project is
15
+ // run on every page. Loaded from an effect so it runs AFTER hydration commits —
16
+ // the page is interactive and a script that mutates the DOM can't cause a
17
+ // hydration mismatch. Classic (non-module) scripts, appended in the discovered
18
+ // order; `async = false` keeps that order for dynamically inserted scripts.
19
+ // Runs once per full page load, not per client-side navigation — same as a
20
+ // <script> tag in the HTML would.
21
+ function CustomScripts() {
22
+ React.useEffect(() => {
23
+ for (const url of site.customScripts ?? []) {
24
+ const el = document.createElement('script');
25
+ el.src = (basename || '') + url;
26
+ el.async = false;
27
+ el.dataset.veluCustom = '';
28
+ document.body.appendChild(el);
29
+ }
30
+ }, []);
31
+ return null;
32
+ }
33
+
12
34
  hydrateRoot(
13
35
  document.getElementById('root'),
14
36
  <BrowserRouter basename={basename}>
15
37
  <App />
38
+ <CustomScripts />
16
39
  </BrowserRouter>
17
40
  );
18
41
 
@@ -0,0 +1,16 @@
1
+ // velu-ui renders component roots as Mintlify-compatible custom tags
2
+ // (`<card>`, `<callout>`, `<code-block>`, …) so project CSS can use element
3
+ // selectors. Browsers handle unknown elements fine (HTMLUnknownElement), and
4
+ // React renders them fine too, but React's DEVELOPMENT build logs
5
+ // "Warning: The tag <card> is unrecognized in this browser. …"
6
+ // once per tag name for the ones without a hyphen. Production builds never
7
+ // emit it. This drops exactly that message in dev (server + browser) so
8
+ // `velu dev` output stays readable; every other console.error passes through.
9
+ if (import.meta.env.DEV && typeof console !== 'undefined') {
10
+ const original = console.error;
11
+ console.error = function veluFilteredError(...args) {
12
+ const first = args[0];
13
+ if (typeof first === 'string' && first.includes('is unrecognized in this browser')) return;
14
+ original.apply(this, args);
15
+ };
16
+ }
@@ -0,0 +1,54 @@
1
+ // Page layout modes — Mintlify-compatible `mode` frontmatter.
2
+ // Unknown / missing values fall back to the standard docs chrome.
3
+
4
+ export const PAGE_MODES = [
5
+ 'default',
6
+ 'wide',
7
+ 'custom',
8
+ 'frame',
9
+ 'center',
10
+ 'assistant',
11
+ ];
12
+
13
+ /**
14
+ * @param {Record<string, unknown>} [frontmatter]
15
+ * @returns {'default' | 'wide' | 'custom' | 'frame' | 'center' | 'assistant'}
16
+ */
17
+ export function resolvePageMode(frontmatter = {}) {
18
+ const raw = String(frontmatter.mode ?? 'default').trim().toLowerCase();
19
+ return PAGE_MODES.includes(raw) ? raw : 'default';
20
+ }
21
+
22
+ /**
23
+ * Which chrome pieces a mode shows. Used by the docs runtime; CSS also
24
+ * keys off `data-page-mode` for the corresponding layout (aside margins,
25
+ * article measure).
26
+ *
27
+ * default — sidebar + TOC + footer (standard docs page)
28
+ * wide — sidebar + footer; no TOC panel; keeps the right gutter
29
+ * custom — no sidebar/TOC/footer; centered landing canvas
30
+ * frame — custom canvas + sidebar (no TOC)
31
+ * center — no sidebar/TOC; centered reading (changelogs)
32
+ * assistant — navbar + full-page Ask AI chat
33
+ *
34
+ * @param {string} mode
35
+ */
36
+ export function pageModeChrome(mode) {
37
+ const m = PAGE_MODES.includes(mode) ? mode : 'default';
38
+ const sidebar = m === 'default' || m === 'wide' || m === 'frame';
39
+ const toc = m === 'default';
40
+ const footer = m === 'default' || m === 'wide' || m === 'frame' || m === 'center';
41
+ const hero = m === 'default' || m === 'wide' || m === 'center';
42
+ return {
43
+ mode: m,
44
+ sidebar,
45
+ toc,
46
+ footer,
47
+ hero,
48
+ // Context menu, feedback, prev/next, AskBar, powered-by — the
49
+ // docs-page chrome around the MDX body. Custom/frame are a canvas;
50
+ // assistant replaces the body with chat.
51
+ articleChrome: hero,
52
+ assistant: m === 'assistant',
53
+ };
54
+ }
@@ -1,3 +1,4 @@
1
+ import './dev-warnings.js';
1
2
  import React from 'react';
2
3
  import { renderToString } from 'react-dom/server';
3
4
  import { StaticRouter } from 'react-router-dom/server';