@veluai/velu 0.2.36 → 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.
@@ -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,
@@ -1493,7 +1530,7 @@ function DocsPage() {
1493
1530
  nav fully to that end. */}
1494
1531
  <div className="velu-docs-nav-region">
1495
1532
  <div
1496
- id="sidebar-content"
1533
+ id={site.theme === 'vepa' ? 'navigation-items' : 'sidebar-content'}
1497
1534
  ref={leftAsideRef}
1498
1535
  className="velu-docs-nav-scroll velu-hide-scrollbar"
1499
1536
  style={{
@@ -1561,8 +1598,9 @@ function DocsPage() {
1561
1598
  display:none snap. See docs-layout.css. */}
1562
1599
 
1563
1600
  {/* Fixed right TOC — pinned to viewport-right. Hidden while the
1564
- Ask-AI chatbot is open (the panel takes that edge). */}
1565
- {!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 && (
1566
1604
  <aside
1567
1605
  id="table-of-contents"
1568
1606
  ref={rightAsideRef}
@@ -1578,13 +1616,15 @@ function DocsPage() {
1578
1616
  (where the TOC sits for normal pages). */}
1579
1617
  {entry?.api && entry.operation ? (
1580
1618
  <ApiSamples
1619
+ preset={site.theme}
1620
+ title={frontmatter.title}
1581
1621
  samples={entry.samples}
1582
1622
  responses={entry.operation.responses}
1583
1623
  />
1584
1624
  ) : isChangelog && changelogTags.length ? (
1585
1625
  <ChangelogFilters tags={changelogTags} />
1586
1626
  ) : (
1587
- <Toc id="table-of-contents-content" 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} />
1588
1628
  )}
1589
1629
  </aside>
1590
1630
  )}
@@ -1607,7 +1647,14 @@ function DocsPage() {
1607
1647
  onAskAI={IS_DEV_PREVIEW || askAiHidden ? undefined : () => askAI('')}
1608
1648
  />
1609
1649
  </main>
1610
- ) : (
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 : (
1611
1658
  <>
1612
1659
  {/* Narrow-layout TOC bar — always in DOM, only visible at
1613
1660
  < 1024px (toggled by @container in toc-bar.css). Shares
@@ -1616,8 +1663,11 @@ function DocsPage() {
1616
1663
  data; whichever one is visible at a given width responds
1617
1664
  to the same scroll-spy state. */}
1618
1665
  {/* Changelog tag filters are a wide-layout (right-rail) affordance only;
1619
- 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 && (
1620
1669
  <TocBar items={toc} activeId={activeId} onSelect={scrollTo} />
1670
+ )}
1621
1671
  <main
1622
1672
  id="content-area"
1623
1673
  className="velu-docs-layout__main"
@@ -1641,6 +1691,7 @@ function DocsPage() {
1641
1691
  column, and therefore sits BELOW the TocBar rather than
1642
1692
  overlapping it. Chevron flips direction with the
1643
1693
  `data-sidebar-open` data attribute on the layout root. */}
1694
+ {chrome.sidebar && (
1644
1695
  <button
1645
1696
  type="button"
1646
1697
  className="velu-docs-layout__sidebar-toggle"
@@ -1654,11 +1705,13 @@ function DocsPage() {
1654
1705
  })}
1655
1706
  </span>
1656
1707
  </button>
1657
- <div id="content" 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 : ""}>
1658
1710
  {/* Per-page agent/IDE action bar: the section eyebrow + a
1659
1711
  "Copy Page" split-button whose dropdown is driven by the
1660
1712
  Mintlify-compatible `contextual` config. Renders nothing
1661
1713
  when there's neither an eyebrow nor any enabled options. */}
1714
+ {chrome.articleChrome && (
1662
1715
  <ContextMenu
1663
1716
  eyebrow={
1664
1717
  nav?.breadcrumb && nav.breadcrumb.length > 1
@@ -1675,13 +1728,15 @@ function DocsPage() {
1675
1728
  getMarkdown={entry?.getSource}
1676
1729
  rssHref={isChangelog ? `${BASE_PATH}/rss.xml` : undefined}
1677
1730
  />
1731
+ )}
1678
1732
 
1679
1733
  {/* Page hero from MDX frontmatter. `.velu-hero` rules
1680
1734
  (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) && (
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) && (
1683
1738
  <div className="velu-hero">
1684
- {frontmatter.title && <h1 id={pageId}>{frontmatter.title}</h1>}
1739
+ {frontmatter.title && <h1 id={site.theme === 'vepa' ? 'page-title' : pageId}>{frontmatter.title}</h1>}
1685
1740
  {frontmatter.description && (
1686
1741
  <p
1687
1742
  style={{
@@ -1702,10 +1757,10 @@ function DocsPage() {
1702
1757
  level. Falls back to a not-found / missing-file notice
1703
1758
  when the route has no page (or its file is absent). */}
1704
1759
  <MDXProvider components={defaultMdxComponents}>
1705
- <mdx-content class="velu-prose">
1760
+ <mdx-content key={site.theme === 'vepa' ? pathname : undefined} id={site.theme === 'vepa' && !entry?.api ? 'content' : undefined} class="velu-prose">
1706
1761
  {PageComponent ? (
1707
1762
  <ErrorBoundary key={pathname} file={entry?.relPath}>
1708
- <PageComponent />
1763
+ {entry?.mdxApi ? <ApiReferencePage {...entry.mdxApi} preset={site.theme}><PageComponent /></ApiReferencePage> : <PageComponent />}
1709
1764
  </ErrorBoundary>
1710
1765
  ) : (
1711
1766
  <p style={{ color: 'var(--muted-color)' }}>
@@ -1718,7 +1773,8 @@ function DocsPage() {
1718
1773
  </MDXProvider>
1719
1774
  {/* Page-foot feedback widget — ref'd so the sticky AskBar
1720
1775
  above can hide as the user scrolls near it. */}
1721
- <div ref={feedbackRef} style={{ marginTop: 'var(--s3)' }}>
1776
+ {chrome.articleChrome && (
1777
+ <div ref={setFeedbackEl} style={{ marginTop: 'var(--s3)' }}>
1722
1778
  {/* key resets the widget's vote state on page change. "Yes"
1723
1779
  submits immediately; "No" submits once its form is sent.
1724
1780
  Handlers are no-ops in the dev preview (pageFeedback null). */}
@@ -1732,9 +1788,10 @@ function DocsPage() {
1732
1788
  }
1733
1789
  />
1734
1790
  </div>
1791
+ )}
1735
1792
  {/* Previous / next page navigation — derived from the
1736
1793
  sidebar reading order of the active section. */}
1737
- {(nav?.prev || nav?.next) && (
1794
+ {chrome.articleChrome && !frontmatter.hidePagination && (nav?.prev || nav?.next) && (
1738
1795
  <PageNav
1739
1796
  id="pagination"
1740
1797
  style={{ marginTop: 'var(--s2)' }}
@@ -1750,7 +1807,7 @@ function DocsPage() {
1750
1807
  approaches (see IntersectionObserver above). */}
1751
1808
  {/* Ask-a-question bar — opens the AI chatbot, which runs on the
1752
1809
  deployed site. Hidden in the local dev preview. */}
1753
- {!IS_DEV_PREVIEW && !chatOpen && !askAiHidden && (
1810
+ {!IS_DEV_PREVIEW && !chatOpen && !askAiHidden && chrome.articleChrome && (
1754
1811
  <AskBar
1755
1812
  onSubmit={askAI}
1756
1813
  style={{
@@ -1776,14 +1833,14 @@ function DocsPage() {
1776
1833
  configured social icons sit on the LEFT of this same row (no
1777
1834
  full footer section); with link columns they live in the full
1778
1835
  footer below instead. */}
1779
- {footerHasLinks ? (
1780
- <PoweredBy />
1836
+ {chrome.articleChrome && (footerHasLinks ? (
1837
+ site.theme === 'vepa' ? null : <PoweredBy />
1781
1838
  ) : (
1782
1839
  <div className="velu-content-foot">
1783
1840
  <SocialLinks socials={footerSocials} />
1784
1841
  <PoweredBy />
1785
1842
  </div>
1786
- )}
1843
+ ))}
1787
1844
  </div>
1788
1845
  </main>
1789
1846
  </>
@@ -1794,15 +1851,16 @@ function DocsPage() {
1794
1851
  full width below the article; its raised z-index eclipses the bottoms
1795
1852
  of the fixed sidebar and TOC as the page scrolls into it, and the ref
1796
1853
  is watched so the asides can pad their bottom by the overlap. */}
1797
- {footerHasLinks && (
1854
+ {footerHasLinks && chrome.footer && (
1798
1855
  <advanced-footer
1799
- id="footer"
1800
- ref={footerRef}
1856
+ id={site.theme === 'vepa' ? undefined : 'footer'}
1857
+ ref={setFooterEl}
1801
1858
  data-velu-footer
1802
1859
  style={{ position: 'relative', zIndex: 20 }}
1803
1860
  >
1804
1861
  <PageFooter
1805
- brand={{ href: site.logo?.href || '/' }}
1862
+ preset={site.theme}
1863
+ brand={{ label: site.name, logo: site.logo, href: site.logo?.href || '/' }}
1806
1864
  columns={site.footer.links}
1807
1865
  socials={footerSocials}
1808
1866
  />
@@ -1811,11 +1869,12 @@ function DocsPage() {
1811
1869
 
1812
1870
  {/* Ask-AI chatbot — slides in from the inline-end edge. Hidden when the
1813
1871
  docs owner's plan/credits disable Ask-AI. */}
1814
- {!askAiHidden && (
1872
+ {!askAiHidden && (!chrome.assistant || !assistantUnavailable) && (
1815
1873
  <Chatbot
1816
- open={chatOpen}
1874
+ open={chrome.assistant || chatOpen}
1875
+ layout={chrome.assistant ? 'page' : 'panel'}
1817
1876
  seedQuestion={chatQuestion}
1818
- onClose={() => setChatOpen(false)}
1877
+ onClose={chrome.assistant ? undefined : () => setChatOpen(false)}
1819
1878
  ask={assistant?.ask}
1820
1879
  onFeedback={assistant?.sendFeedback}
1821
1880
  listHistory={assistant?.listConversations}
@@ -1838,6 +1897,7 @@ function DocsPage() {
1838
1897
  // behind the sheet. Desktop keeps the panel open so multi-cite hops
1839
1898
  // stay convenient.
1840
1899
  if (
1900
+ !chrome.assistant &&
1841
1901
  typeof window !== 'undefined' &&
1842
1902
  window.matchMedia('(max-width: 1024px)').matches
1843
1903
  ) {
@@ -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
+