@veluai/velu 0.2.36 → 0.2.38

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.
@@ -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';
@@ -691,7 +694,7 @@ const RouterLink = ({ href, ...rest }) => <Link to={href} {...rest} />;
691
694
  // place.
692
695
  function sectionById(id) {
693
696
  if (typeof document === 'undefined' || !id) return null;
694
- const article = document.getElementById('content');
697
+ const article = document.querySelector('.velu-docs-layout__article');
695
698
  const esc = id.replace(/["\\]/g, '\\$&');
696
699
  return article?.querySelector(`[id="${esc}"]`) ?? document.getElementById(id);
697
700
  }
@@ -855,6 +858,10 @@ function DocsPage() {
855
858
  // No renderable page for this route → show the 404 page (a clean centered
856
859
  // takeover: header + footer stay, the docs sidebar/TOC are hidden).
857
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);
858
865
 
859
866
  // Switcher option sets (only render a switcher when an axis has >1
860
867
  // option). Anchors are pinned sidebar links shown in the context zone.
@@ -921,8 +928,9 @@ function DocsPage() {
921
928
  // against it like any other heading.
922
929
  const pageId = React.useMemo(() => {
923
930
  if (!frontmatter.title) return null;
931
+ if (site.theme === 'vepa') return 'page-title';
924
932
  return new GithubSlugger().slug(frontmatter.title);
925
- }, [frontmatter.title]);
933
+ }, [frontmatter.title, site.theme]);
926
934
 
927
935
  // Combined TOC: the frontmatter title is the page's top-level entry,
928
936
  // and the MDX-derived headings (h2s) become its children. Without this,
@@ -1058,7 +1066,11 @@ function DocsPage() {
1058
1066
  const tocBarToggle = document.querySelector('.velu-toc-bar__toggle');
1059
1067
  if (tocBarToggle) ro.observe(tocBarToggle);
1060
1068
  return () => ro.disconnect();
1061
- }, []);
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]);
1062
1074
 
1063
1075
  // The asides (left sidebar + right TOC) scroll independently of the
1064
1076
  // page. On scroll/resize we set data-fade-top/-bottom on each scroll
@@ -1183,14 +1195,22 @@ function DocsPage() {
1183
1195
  });
1184
1196
  }
1185
1197
 
1186
- 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);
1187
1204
  const [footerOverlap, setFooterOverlap] = React.useState(0);
1188
1205
  React.useEffect(() => {
1189
- const node = footerRef.current;
1190
- if (!node) return;
1206
+ if (!footerEl) {
1207
+ setFooterOverlap(0);
1208
+ return undefined;
1209
+ }
1191
1210
  const update = () => {
1192
- const rect = node.getBoundingClientRect();
1193
- 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));
1194
1214
  };
1195
1215
  update();
1196
1216
  window.addEventListener('scroll', update, { passive: true });
@@ -1199,15 +1219,17 @@ function DocsPage() {
1199
1219
  window.removeEventListener('scroll', update);
1200
1220
  window.removeEventListener('resize', update);
1201
1221
  };
1202
- }, []);
1222
+ }, [footerEl]);
1203
1223
 
1204
1224
  // Sticky AskBar fades out once the user scrolls near the PageFeedback
1205
1225
  // widget — so it doesn't sit on top of the page-foot widgets.
1206
- const feedbackRef = React.useRef(null);
1226
+ const [feedbackEl, setFeedbackEl] = React.useState(null);
1207
1227
  const [askBarHidden, setAskBarHidden] = React.useState(false);
1208
1228
  React.useEffect(() => {
1209
- const node = feedbackRef.current;
1210
- if (!node) return;
1229
+ if (!feedbackEl) {
1230
+ setAskBarHidden(false);
1231
+ return undefined;
1232
+ }
1211
1233
  const FADE_BUFFER = 64; // matches the rootMargin below
1212
1234
  const apply = (rect) => {
1213
1235
  // Hide whenever the feedback's top has reached the (effective)
@@ -1217,16 +1239,16 @@ function DocsPage() {
1217
1239
  // exactly at the same line in both scroll directions.
1218
1240
  setAskBarHidden(rect.top < window.innerHeight - FADE_BUFFER);
1219
1241
  };
1220
- apply(node.getBoundingClientRect());
1242
+ apply(feedbackEl.getBoundingClientRect());
1221
1243
  const io = new IntersectionObserver(
1222
1244
  ([entry]) => apply(entry.boundingClientRect),
1223
1245
  // Trigger ~64px before the feedback enters the viewport, so the
1224
1246
  // AskBar fades just as the widget starts to peek up from below.
1225
1247
  { rootMargin: '0px 0px -64px 0px' },
1226
1248
  );
1227
- io.observe(node);
1249
+ io.observe(feedbackEl);
1228
1250
  return () => io.disconnect();
1229
- }, []);
1251
+ }, [feedbackEl]);
1230
1252
 
1231
1253
  const scrollTo = React.useCallback(
1232
1254
  (id) => {
@@ -1263,11 +1285,13 @@ function DocsPage() {
1263
1285
  <div
1264
1286
  id="body-content"
1265
1287
  className="velu-docs-layout"
1266
- data-chat-open={chatOpen ? 'true' : 'false'}
1288
+ data-site-theme={site.theme ?? 'velu'}
1289
+ data-chat-open={chatOpen && !chrome.assistant ? 'true' : 'false'}
1267
1290
  data-sidebar-open={sidebarOpen ? 'true' : 'false'}
1268
1291
  data-drawer-open={drawerOpen ? 'true' : 'false'}
1269
1292
  data-api={entry?.api ? 'true' : 'false'}
1270
1293
  data-not-found={isNotFound ? 'true' : undefined}
1294
+ data-page-mode={isNotFound ? undefined : chrome.mode}
1271
1295
  >
1272
1296
  {/* Scrim — visible at mobile while the drawer OR the chatbot
1273
1297
  sheet is open. Sits between the article (z-0) and the
@@ -1284,6 +1308,7 @@ function DocsPage() {
1284
1308
  {/* Site header — brand, centered search, right-side actions,
1285
1309
  tabs row. Configurable: pass any number of actions / tabs. */}
1286
1310
  <PageHeader
1311
+ preset={site.theme}
1287
1312
  id="navbar"
1288
1313
  linkComponent={RouterLink}
1289
1314
  brand={{
@@ -1296,7 +1321,6 @@ function DocsPage() {
1296
1321
  <span className="velu-hide-on-mobile">{versionSwitcher}</span>
1297
1322
  )
1298
1323
  }
1299
- tabsTrailing={languageSwitcher || undefined}
1300
1324
  center={
1301
1325
  <Cluster space="var(--s-6)" align="center">
1302
1326
  <Search
@@ -1309,7 +1333,7 @@ function DocsPage() {
1309
1333
  {/* Ask AI talks to the deployed site's AI backend — hidden in
1310
1334
  the local dev preview where there's nothing to talk to, and
1311
1335
  when the owner's plan/credits disable the assistant. */}
1312
- {!IS_DEV_PREVIEW && !askAiHidden && (
1336
+ {!IS_DEV_PREVIEW && !askAiHidden && !chrome.assistant && (
1313
1337
  <button
1314
1338
  type="button"
1315
1339
  id="assistant-entry"
@@ -1325,11 +1349,24 @@ function DocsPage() {
1325
1349
  </Cluster>
1326
1350
  }
1327
1351
  actions={navActions}
1328
- trailing={<ThemeToggle />}
1329
- onMenuClick={() => setDrawerOpen((v) => !v)}
1330
- 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
+ }
1331
1364
  activeTab={nav?.activeTab}
1332
- tabs={nav?.tabs ?? []}
1365
+ tabs={
1366
+ chrome.mode === 'custom' || chrome.mode === 'assistant'
1367
+ ? []
1368
+ : (nav?.tabs ?? [])
1369
+ }
1333
1370
  />
1334
1371
 
1335
1372
  {/* Fixed left sidebar — pinned to viewport-left below the header.
@@ -1339,7 +1376,7 @@ function DocsPage() {
1339
1376
  honour `inset-block-start` for fixed positioning the same way
1340
1377
  as plain `top`). */}
1341
1378
  <aside
1342
- id="sidebar"
1379
+ id={site.theme === 'vepa' ? 'sidebar-content' : 'sidebar'}
1343
1380
  className="velu-docs-layout__aside velu-docs-layout__aside--left"
1344
1381
  style={{
1345
1382
  /* Bottom edge stays a fixed gap above the viewport bottom,
@@ -1380,7 +1417,7 @@ function DocsPage() {
1380
1417
  </>
1381
1418
  )}
1382
1419
  </RouterLink>
1383
- <ThemeToggle />
1420
+ <ThemeToggle variant={site.theme === 'vepa' ? 'menu' : undefined} />
1384
1421
  <button
1385
1422
  type="button"
1386
1423
  className="velu-docs-layout__drawer-close"
@@ -1393,7 +1430,9 @@ function DocsPage() {
1393
1430
  {/* Nav dropdown — custom button + menu, fills drawer width.
1394
1431
  Items mirror the desktop tabs (Home / Docs / Blog).
1395
1432
  Click-outside + Escape close (see navOpen useEffect
1396
- above). Chevron rotates 180° on open. */}
1433
+ above). Chevron rotates 180° on open. Shown only when
1434
+ there is more than one tab to switch between. */}
1435
+ {navItems.length > 1 && (
1397
1436
  <div
1398
1437
  ref={navRef}
1399
1438
  className="velu-docs-layout__drawer-docselect"
@@ -1437,6 +1476,7 @@ function DocsPage() {
1437
1476
  ))}
1438
1477
  </ul>
1439
1478
  </div>
1479
+ )}
1440
1480
  {/* Context zone — product switcher + anchors at the top of
1441
1481
  the sidebar (all breakpoints), plus version/language
1442
1482
  switchers that only show on mobile (desktop has them in
@@ -1493,7 +1533,7 @@ function DocsPage() {
1493
1533
  nav fully to that end. */}
1494
1534
  <div className="velu-docs-nav-region">
1495
1535
  <div
1496
- id="sidebar-content"
1536
+ id={site.theme === 'vepa' ? 'navigation-items' : 'sidebar-content'}
1497
1537
  ref={leftAsideRef}
1498
1538
  className="velu-docs-nav-scroll velu-hide-scrollbar"
1499
1539
  style={{
@@ -1561,8 +1601,9 @@ function DocsPage() {
1561
1601
  display:none snap. See docs-layout.css. */}
1562
1602
 
1563
1603
  {/* Fixed right TOC — pinned to viewport-right. Hidden while the
1564
- Ask-AI chatbot is open (the panel takes that edge). */}
1565
- {!chatOpen && (
1604
+ Ask-AI chatbot is open (the panel takes that edge). Wide /
1605
+ custom / frame / center / assistant hide this rail. */}
1606
+ {!chatOpen && chrome.toc && (
1566
1607
  <aside
1567
1608
  id="table-of-contents"
1568
1609
  ref={rightAsideRef}
@@ -1578,11 +1619,20 @@ function DocsPage() {
1578
1619
  (where the TOC sits for normal pages). */}
1579
1620
  {entry?.api && entry.operation ? (
1580
1621
  <ApiSamples
1622
+ preset={site.theme}
1623
+ title={frontmatter.title}
1581
1624
  samples={entry.samples}
1582
1625
  responses={entry.operation.responses}
1583
1626
  />
1584
- ) : isChangelog && changelogTags.length ? (
1627
+ ) : isChangelog && changelogTags.length && site.theme !== 'vepa' ? (
1585
1628
  <ChangelogFilters tags={changelogTags} />
1629
+ ) : site.theme === 'vepa' ? (
1630
+ <>
1631
+ {isChangelog && changelogTags.length > 0 ? (
1632
+ <ChangelogFilters tags={changelogTags} />
1633
+ ) : null}
1634
+ <VepaToc items={pageToc} activeId={activeId} onSelect={scrollTo} />
1635
+ </>
1586
1636
  ) : (
1587
1637
  <Toc id="table-of-contents-content" items={toc} activeId={activeId} onSelect={scrollTo} />
1588
1638
  )}
@@ -1607,7 +1657,14 @@ function DocsPage() {
1607
1657
  onAskAI={IS_DEV_PREVIEW || askAiHidden ? undefined : () => askAI('')}
1608
1658
  />
1609
1659
  </main>
1610
- ) : (
1660
+ ) : chrome.assistant && assistantUnavailable ? (
1661
+ <main className="velu-assistant-disabled">
1662
+ <Search
1663
+ unavailable
1664
+ unavailableMessage="Ask AI is not enabled on this site."
1665
+ />
1666
+ </main>
1667
+ ) : chrome.assistant ? null : (
1611
1668
  <>
1612
1669
  {/* Narrow-layout TOC bar — always in DOM, only visible at
1613
1670
  < 1024px (toggled by @container in toc-bar.css). Shares
@@ -1616,8 +1673,11 @@ function DocsPage() {
1616
1673
  data; whichever one is visible at a given width responds
1617
1674
  to the same scroll-spy state. */}
1618
1675
  {/* Changelog tag filters are a wide-layout (right-rail) affordance only;
1619
- on tab/mobile we keep the normal TOC bar (no filters). */}
1676
+ on tab/mobile we keep the normal TOC bar (no filters). Hidden
1677
+ entirely when the page mode has no table of contents. */}
1678
+ {chrome.toc && (
1620
1679
  <TocBar items={toc} activeId={activeId} onSelect={scrollTo} />
1680
+ )}
1621
1681
  <main
1622
1682
  id="content-area"
1623
1683
  className="velu-docs-layout__main"
@@ -1641,6 +1701,7 @@ function DocsPage() {
1641
1701
  column, and therefore sits BELOW the TocBar rather than
1642
1702
  overlapping it. Chevron flips direction with the
1643
1703
  `data-sidebar-open` data attribute on the layout root. */}
1704
+ {chrome.sidebar && (
1644
1705
  <button
1645
1706
  type="button"
1646
1707
  className="velu-docs-layout__sidebar-toggle"
@@ -1654,11 +1715,13 @@ function DocsPage() {
1654
1715
  })}
1655
1716
  </span>
1656
1717
  </button>
1657
- <div id="content" className="velu-docs-layout__article" data-pagefind-body="">
1718
+ )}
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 : ""}>
1658
1720
  {/* Per-page agent/IDE action bar: the section eyebrow + a
1659
1721
  "Copy Page" split-button whose dropdown is driven by the
1660
1722
  Mintlify-compatible `contextual` config. Renders nothing
1661
1723
  when there's neither an eyebrow nor any enabled options. */}
1724
+ {chrome.articleChrome && (
1662
1725
  <ContextMenu
1663
1726
  eyebrow={
1664
1727
  nav?.breadcrumb && nav.breadcrumb.length > 1
@@ -1675,13 +1738,15 @@ function DocsPage() {
1675
1738
  getMarkdown={entry?.getSource}
1676
1739
  rssHref={isChangelog ? `${BASE_PATH}/rss.xml` : undefined}
1677
1740
  />
1741
+ )}
1678
1742
 
1679
1743
  {/* Page hero from MDX frontmatter. `.velu-hero` rules
1680
1744
  (in base.css) keep the title and description tightly
1681
- grouped and create a clear break before the prose body. */}
1682
- {(frontmatter.title || frontmatter.description) && (
1745
+ grouped and create a clear break before the prose body.
1746
+ Custom / frame / assistant are a blank canvas — no auto hero. */}
1747
+ {chrome.hero && (frontmatter.title || frontmatter.description) && (
1683
1748
  <div className="velu-hero">
1684
- {frontmatter.title && <h1 id={pageId}>{frontmatter.title}</h1>}
1749
+ {frontmatter.title && <h1 id={site.theme === 'vepa' ? 'page-title' : pageId}>{frontmatter.title}</h1>}
1685
1750
  {frontmatter.description && (
1686
1751
  <p
1687
1752
  style={{
@@ -1702,10 +1767,10 @@ function DocsPage() {
1702
1767
  level. Falls back to a not-found / missing-file notice
1703
1768
  when the route has no page (or its file is absent). */}
1704
1769
  <MDXProvider components={defaultMdxComponents}>
1705
- <mdx-content class="velu-prose">
1770
+ <mdx-content key={site.theme === 'vepa' ? pathname : undefined} id={site.theme === 'vepa' && !entry?.api ? 'content' : undefined} class="velu-prose">
1706
1771
  {PageComponent ? (
1707
1772
  <ErrorBoundary key={pathname} file={entry?.relPath}>
1708
- <PageComponent />
1773
+ {entry?.mdxApi ? <ApiReferencePage {...entry.mdxApi} preset={site.theme}><PageComponent /></ApiReferencePage> : <PageComponent />}
1709
1774
  </ErrorBoundary>
1710
1775
  ) : (
1711
1776
  <p style={{ color: 'var(--muted-color)' }}>
@@ -1718,7 +1783,8 @@ function DocsPage() {
1718
1783
  </MDXProvider>
1719
1784
  {/* Page-foot feedback widget — ref'd so the sticky AskBar
1720
1785
  above can hide as the user scrolls near it. */}
1721
- <div ref={feedbackRef} style={{ marginTop: 'var(--s3)' }}>
1786
+ {chrome.articleChrome && (
1787
+ <div ref={setFeedbackEl} style={{ marginTop: 'var(--s3)' }}>
1722
1788
  {/* key resets the widget's vote state on page change. "Yes"
1723
1789
  submits immediately; "No" submits once its form is sent.
1724
1790
  Handlers are no-ops in the dev preview (pageFeedback null). */}
@@ -1732,9 +1798,10 @@ function DocsPage() {
1732
1798
  }
1733
1799
  />
1734
1800
  </div>
1801
+ )}
1735
1802
  {/* Previous / next page navigation — derived from the
1736
1803
  sidebar reading order of the active section. */}
1737
- {(nav?.prev || nav?.next) && (
1804
+ {chrome.articleChrome && !frontmatter.hidePagination && (nav?.prev || nav?.next) && (
1738
1805
  <PageNav
1739
1806
  id="pagination"
1740
1807
  style={{ marginTop: 'var(--s2)' }}
@@ -1750,7 +1817,7 @@ function DocsPage() {
1750
1817
  approaches (see IntersectionObserver above). */}
1751
1818
  {/* Ask-a-question bar — opens the AI chatbot, which runs on the
1752
1819
  deployed site. Hidden in the local dev preview. */}
1753
- {!IS_DEV_PREVIEW && !chatOpen && !askAiHidden && (
1820
+ {!IS_DEV_PREVIEW && !chatOpen && !askAiHidden && chrome.articleChrome && (
1754
1821
  <AskBar
1755
1822
  onSubmit={askAI}
1756
1823
  style={{
@@ -1776,14 +1843,14 @@ function DocsPage() {
1776
1843
  configured social icons sit on the LEFT of this same row (no
1777
1844
  full footer section); with link columns they live in the full
1778
1845
  footer below instead. */}
1779
- {footerHasLinks ? (
1780
- <PoweredBy />
1846
+ {chrome.articleChrome && (footerHasLinks ? (
1847
+ site.theme === 'vepa' ? null : <PoweredBy />
1781
1848
  ) : (
1782
1849
  <div className="velu-content-foot">
1783
1850
  <SocialLinks socials={footerSocials} />
1784
1851
  <PoweredBy />
1785
1852
  </div>
1786
- )}
1853
+ ))}
1787
1854
  </div>
1788
1855
  </main>
1789
1856
  </>
@@ -1794,15 +1861,16 @@ function DocsPage() {
1794
1861
  full width below the article; its raised z-index eclipses the bottoms
1795
1862
  of the fixed sidebar and TOC as the page scrolls into it, and the ref
1796
1863
  is watched so the asides can pad their bottom by the overlap. */}
1797
- {footerHasLinks && (
1864
+ {footerHasLinks && chrome.footer && (
1798
1865
  <advanced-footer
1799
- id="footer"
1800
- ref={footerRef}
1866
+ id={site.theme === 'vepa' ? undefined : 'footer'}
1867
+ ref={setFooterEl}
1801
1868
  data-velu-footer
1802
1869
  style={{ position: 'relative', zIndex: 20 }}
1803
1870
  >
1804
1871
  <PageFooter
1805
- brand={{ href: site.logo?.href || '/' }}
1872
+ preset={site.theme}
1873
+ brand={{ label: site.name, logo: site.logo, href: site.logo?.href || '/' }}
1806
1874
  columns={site.footer.links}
1807
1875
  socials={footerSocials}
1808
1876
  />
@@ -1811,11 +1879,12 @@ function DocsPage() {
1811
1879
 
1812
1880
  {/* Ask-AI chatbot — slides in from the inline-end edge. Hidden when the
1813
1881
  docs owner's plan/credits disable Ask-AI. */}
1814
- {!askAiHidden && (
1882
+ {!askAiHidden && (!chrome.assistant || !assistantUnavailable) && (
1815
1883
  <Chatbot
1816
- open={chatOpen}
1884
+ open={chrome.assistant || chatOpen}
1885
+ layout={chrome.assistant ? 'page' : 'panel'}
1817
1886
  seedQuestion={chatQuestion}
1818
- onClose={() => setChatOpen(false)}
1887
+ onClose={chrome.assistant ? undefined : () => setChatOpen(false)}
1819
1888
  ask={assistant?.ask}
1820
1889
  onFeedback={assistant?.sendFeedback}
1821
1890
  listHistory={assistant?.listConversations}
@@ -1838,6 +1907,7 @@ function DocsPage() {
1838
1907
  // behind the sheet. Desktop keeps the panel open so multi-cite hops
1839
1908
  // stay convenient.
1840
1909
  if (
1910
+ !chrome.assistant &&
1841
1911
  typeof window !== 'undefined' &&
1842
1912
  window.matchMedia('(max-width: 1024px)').matches
1843
1913
  ) {
@@ -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
+ }
@@ -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
+ }
@@ -28,3 +28,18 @@ applied as the body font.
28
28
  `favicon` is a path relative to your project root, e.g.
29
29
  `/favicon.svg`.
30
30
  </Callout>
31
+
32
+ ## Page layout
33
+
34
+ Set `mode` in a page's frontmatter to change its chrome. `default` is a
35
+ sidebar plus table of contents; `wide` drops the right panel; `custom` is
36
+ navbar-only; `frame` keeps the sidebar on a custom canvas; `center` is for
37
+ changelogs; `assistant` is a full-page Ask AI chat.
38
+
39
+ ```mdx
40
+ ---
41
+ title: "Changelog"
42
+ mode: "center"
43
+ ---
44
+ ```
45
+