@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
@@ -63,9 +63,9 @@ function hasActiveDescendant(item, activeHref) {
63
63
  function Icon({ icon }) {
64
64
  const node = resolveIcon(icon);
65
65
  return node ? (
66
- <span className="velu-sidebar__icon" aria-hidden="true">
66
+ <sidebar-group-icon class="velu-sidebar__icon" aria-hidden="true">
67
67
  {node}
68
- </span>
68
+ </sidebar-group-icon>
69
69
  ) : null;
70
70
  }
71
71
 
@@ -99,7 +99,7 @@ function Node({ item, depth }) {
99
99
  return (
100
100
  <li>
101
101
  <details open={hasActiveDescendant(item, activeHref) || undefined}>
102
- <summary className={`${base} velu-sidebar__summary`}>
102
+ <summary className={`${base} velu-sidebar__summary`} data-component="sidebar-group-header">
103
103
  <Icon icon={icon} />
104
104
  <span className="velu-sidebar__label">{label}</span>
105
105
  <Chevron />
@@ -124,10 +124,11 @@ function Node({ item, depth }) {
124
124
  : { href };
125
125
 
126
126
  return (
127
- <li>
127
+ <li data-active={active ? 'true' : undefined}>
128
128
  <LinkTag
129
129
  className={cls}
130
130
  aria-current={active ? 'page' : undefined}
131
+ data-active={active ? 'true' : undefined}
131
132
  {...linkProps}
132
133
  >
133
134
  <Icon icon={icon} />
@@ -228,8 +229,19 @@ export default function Sidebar({
228
229
  separates one section from the next — compact but still
229
230
  visibly grouped. */}
230
231
  {sections.map((section, i) => (
231
- <Stack key={i} space="var(--s-4)">
232
- <h5 className="velu-sidebar__section">
232
+ <Stack
233
+ key={i}
234
+ as="sidebar-group"
235
+ space="var(--s-4)"
236
+ data-active={
237
+ section.items.some(
238
+ (it) => isActive(it, activeHref) || hasActiveDescendant(it, activeHref),
239
+ )
240
+ ? 'true'
241
+ : undefined
242
+ }
243
+ >
244
+ <h5 className="velu-sidebar__section" data-component="sidebar-title">
233
245
  <Icon icon={section.icon} />
234
246
  {section.title}
235
247
  </h5>
@@ -33,7 +33,7 @@ export function Step({
33
33
  ...rest
34
34
  }) {
35
35
  return (
36
- <div className={`velu-step ${className}`.trim()} {...rest}>
36
+ <step className={`velu-step ${className}`.trim()} {...rest}>
37
37
  <div className="velu-step__circle" aria-hidden="true">
38
38
  {index}
39
39
  </div>
@@ -44,7 +44,7 @@ export function Step({
44
44
  {children != null && children !== false && (
45
45
  <div className="velu-step__body">{children}</div>
46
46
  )}
47
- </div>
47
+ </step>
48
48
  );
49
49
  }
50
50
  Step.displayName = 'Step';
@@ -58,8 +58,8 @@ export default function Steps({ children, className = '', ...rest }) {
58
58
  return cloneElement(child, { index: child.props.index ?? i });
59
59
  });
60
60
  return (
61
- <div className={`velu-steps ${className}`.trim()} {...rest}>
61
+ <steps className={`velu-steps ${className}`.trim()} {...rest}>
62
62
  {numbered}
63
- </div>
63
+ </steps>
64
64
  );
65
65
  }
@@ -0,0 +1,57 @@
1
+ import React from 'react';
2
+ import { Sun, Moon, Monitor, Check } from 'lucide-react';
3
+
4
+ export default function ThemePreferenceMenu({ inline = false }) {
5
+ const [open, setOpen] = React.useState(false);
6
+ const [preference, setPreference] = React.useState('system');
7
+ const root = React.useRef(null);
8
+ const trigger = React.useRef(null);
9
+ React.useEffect(() => {
10
+ const media = window.matchMedia('(prefers-color-scheme: dark)');
11
+ const sync = () => {
12
+ let pref = 'system';
13
+ try { pref = localStorage.getItem('velu-theme') || 'system'; } catch {}
14
+ setPreference(pref);
15
+ document.documentElement.dataset.theme = pref === 'system' ? (media.matches ? 'dark' : 'light') : pref;
16
+ };
17
+ sync();
18
+ media.addEventListener('change', sync);
19
+ window.addEventListener('storage', sync);
20
+ window.addEventListener('velu-theme-change', sync);
21
+ return () => { media.removeEventListener('change', sync); window.removeEventListener('storage', sync); window.removeEventListener('velu-theme-change', sync); };
22
+ }, []);
23
+ React.useEffect(() => {
24
+ if (!open) return;
25
+ root.current?.querySelector('[aria-checked="true"]')?.focus();
26
+ const outside = (e) => { if (!root.current?.contains(e.target)) setOpen(false); };
27
+ document.addEventListener('pointerdown', outside);
28
+ return () => document.removeEventListener('pointerdown', outside);
29
+ }, [open]);
30
+ const choose = (value) => {
31
+ setPreference(value);
32
+ try { value === 'system' ? localStorage.removeItem('velu-theme') : localStorage.setItem('velu-theme', value); } catch {}
33
+ document.documentElement.dataset.theme = value === 'system' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : value;
34
+ window.dispatchEvent(new Event('velu-theme-change'));
35
+ setOpen(false);
36
+ trigger.current?.focus();
37
+ };
38
+ const Icon = preference === 'system' ? Monitor : preference === 'dark' ? Moon : Sun;
39
+ if (inline) return <div className="vepa-footer-theme" role="group" aria-label="Color theme">
40
+ {[[Monitor, 'system', 'System'], [Sun, 'light', 'Light'], [Moon, 'dark', 'Dark']].map(([Glyph, value, label]) => <button key={value} type="button" aria-label={`${label} theme`} aria-pressed={preference === value} onClick={() => choose(value)}><Glyph size={16} aria-hidden="true" /></button>)}
41
+ </div>;
42
+ return <div ref={root} className="velu-theme-menu" onKeyDown={(e) => {
43
+ if (e.key === 'Escape') { setOpen(false); trigger.current?.focus(); }
44
+ if (e.key === 'Tab') setOpen(false);
45
+ if (open && ['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(e.key)) {
46
+ e.preventDefault();
47
+ const items = [...root.current.querySelectorAll('[role="menuitemradio"]')];
48
+ const i = items.indexOf(document.activeElement);
49
+ items[e.key === 'Home' ? 0 : e.key === 'End' ? 2 : (i + (e.key === 'ArrowUp' ? 2 : 1)) % 3]?.focus();
50
+ }
51
+ }}>
52
+ <button ref={trigger} type="button" className="velu-theme-menu__trigger" aria-label="Theme preference" aria-haspopup="menu" aria-expanded={open} onClick={() => setOpen(!open)}><Icon size={16} aria-hidden="true" /></button>
53
+ {open && <div className="velu-theme-menu__options" role="menu" aria-label="Theme preference">
54
+ {[[Sun, 'light', 'Light'], [Moon, 'dark', 'Dark'], [Monitor, 'system', 'System']].map(([Glyph, value, label]) => <button key={value} type="button" role="menuitemradio" aria-checked={preference === value} tabIndex={-1} onClick={() => choose(value)}><Glyph size={16} aria-hidden="true" /><span>{label}</span>{preference === value && <Check size={14} aria-hidden="true" />}</button>)}
55
+ </div>}
56
+ </div>;
57
+ }
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { Sun, Moon } from 'lucide-react';
3
+ import ThemePreferenceMenu from './ThemePreferenceMenu.jsx';
3
4
 
4
5
  /**
5
6
  * ThemeToggle — pill-shaped two-state light/dark switch. The thumb
@@ -14,7 +15,8 @@ import { Sun, Moon } from 'lucide-react';
14
15
  * explicit choice (which then overrides the OS preference on future
15
16
  * visits, per the anti-flash script in the template).
16
17
  */
17
- export default function ThemeToggle({ className = '', ...rest }) {
18
+ export default function ThemeToggle({ className = '', variant, ...rest }) {
19
+ if (variant === 'menu') return <ThemePreferenceMenu />;
18
20
  function toggle() {
19
21
  const root = document.documentElement;
20
22
  const next = root.dataset.theme === 'dark' ? 'light' : 'dark';
@@ -32,6 +34,7 @@ export default function ThemeToggle({ className = '', ...rest }) {
32
34
  onClick={toggle}
33
35
  aria-label="Toggle color theme"
34
36
  title="Toggle color theme"
37
+ data-component-name="theme-toggle"
35
38
  className={`velu-theme-toggle ${className}`.trim()}
36
39
  {...rest}
37
40
  >
@@ -127,9 +127,11 @@ const Row = React.forwardRef(function Row(
127
127
  ref
128
128
  ) {
129
129
  return (
130
- <div
130
+ <toc-item
131
131
  ref={ref}
132
132
  data-toc-active={active ? 'true' : undefined}
133
+ data-active={active ? 'true' : undefined}
134
+ data-active-deepest={active ? 'true' : undefined}
133
135
  onMouseEnter={() => onEnter(idx)}
134
136
  onClick={() => onClick(item.id)}
135
137
  style={{
@@ -166,7 +168,7 @@ const Row = React.forwardRef(function Row(
166
168
  >
167
169
  {item.label}
168
170
  </span>
169
- </div>
171
+ </toc-item>
170
172
  );
171
173
  });
172
174
 
@@ -434,7 +436,7 @@ export default function Toc({
434
436
  }, [activeIdx]);
435
437
 
436
438
  return (
437
- <div
439
+ <toc
438
440
  ref={rootRef}
439
441
  onMouseLeave={() => setHoverIdx(null)}
440
442
  style={{
@@ -532,6 +534,6 @@ export default function Toc({
532
534
  fontSize={fontSize}
533
535
  />
534
536
  ))}
535
- </div>
537
+ </toc>
536
538
  );
537
539
  }
@@ -25,9 +25,9 @@ import resolveIcon from '../lib/resolveIcon.jsx';
25
25
 
26
26
  export default function Tree({ children, className = '', ...rest }) {
27
27
  return (
28
- <div className={`velu-tree ${className}`.trim()} {...rest}>
28
+ <tree className={`velu-tree ${className}`.trim()} {...rest}>
29
29
  {children}
30
- </div>
30
+ </tree>
31
31
  );
32
32
  }
33
33
 
@@ -54,6 +54,7 @@ export function Folder({
54
54
  // the <details> is not [open], so the rail collapses with them.
55
55
  return (
56
56
  <details
57
+ data-component="tree-folder"
57
58
  className={`velu-tree__group ${className}`.trim()}
58
59
  open={defaultOpen || undefined}
59
60
  {...rest}
@@ -76,12 +77,12 @@ export function File({
76
77
  ...rest
77
78
  }) {
78
79
  return (
79
- <div
80
- className={`velu-tree__item velu-tree__item--file ${className}`.trim()}
80
+ <tree-file
81
+ class={`velu-tree__item velu-tree__item--file ${className}`.trim()}
81
82
  {...rest}
82
83
  >
83
84
  <ItemIcon icon={icon} />
84
85
  <span className="velu-tree__label">{name}</span>
85
- </div>
86
+ </tree-file>
86
87
  );
87
88
  }
@@ -78,6 +78,7 @@ export default function TryItBar({
78
78
  </div>
79
79
  <button
80
80
  type="button"
81
+ data-component="tryit-button"
81
82
  className="velu-try-it__cta"
82
83
  onClick={onTry}
83
84
  data-loading={loading ? 'true' : undefined}
@@ -55,7 +55,7 @@ export default function Update({
55
55
  const tagList = toTagList(tags);
56
56
 
57
57
  return (
58
- <section
58
+ <update
59
59
  id={anchorId}
60
60
  data-update-label={updateLabel}
61
61
  data-update-tags={tagList.join('|')}
@@ -87,6 +87,6 @@ export default function Update({
87
87
  ) : null}
88
88
  </div>
89
89
  {children ? <div className="velu-update__content">{children}</div> : null}
90
- </section>
90
+ </update>
91
91
  );
92
92
  }
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import Logo from './Logo.jsx';
3
+ import SocialLinks from './SocialLinks.jsx';
4
+ import ThemePreferenceMenu from './ThemePreferenceMenu.jsx';
5
+
6
+ export default function VepaFooter({ brand, columns = [], socials = [], linkComponent: Link = 'a' }) {
7
+ const single = columns.length === 1;
8
+ const completeHeadings = columns.every((c) => c.title);
9
+ return <footer id="footer" className="vepa-footer" data-layout={columns.length <= 3 ? 'compact' : 'expanded'} data-single={single} data-headings={completeHeadings}>
10
+ <div className="vepa-footer__inner">
11
+ <div className="vepa-footer__top">
12
+ <div className="vepa-footer__brand-column">
13
+ <Link href={brand?.href || '/'} className="vepa-footer__brand">
14
+ {brand?.logo ? <Logo logo={brand.logo} alt={brand.label} /> : <span>{brand?.label || 'Velu'}</span>}
15
+ </Link>
16
+ <SocialLinks socials={socials} className="vepa-footer__brand-socials" />
17
+ </div>
18
+ <div className="vepa-footer__columns" style={{ '--footer-columns': columns.length }}>
19
+ {columns.map((column, i) => <div className="vepa-footer__column" key={i}>
20
+ <div className="vepa-footer__links">
21
+ {column.title && !single && <p className="vepa-footer__heading">{column.title}</p>}
22
+ {(column.items || []).map((item, j) => {
23
+ const external = /^https?:\/\//.test(item.href || '');
24
+ const Tag = external ? 'a' : Link;
25
+ return <Tag className="vepa-footer__link" key={j} href={item.href || '#'} {...(external ? { target: '_blank', rel: 'noreferrer' } : {})}>{item.label}</Tag>;
26
+ })}
27
+ </div>
28
+ </div>)}
29
+ </div>
30
+ <SocialLinks socials={socials} className="vepa-footer__end-socials" />
31
+ </div>
32
+ {!completeHeadings && <SocialLinks socials={socials} className="vepa-footer__mobile-socials" />}
33
+ <div className="vepa-footer__divider" />
34
+ <div className="vepa-footer__bottom">
35
+ <a href="https://veludocs.com" target="_blank" rel="noreferrer" className="vepa-footer__powered">Powered by <strong>Velu</strong></a>
36
+ <ThemePreferenceMenu inline />
37
+ </div>
38
+ </div>
39
+ </footer>;
40
+ }
@@ -0,0 +1,28 @@
1
+ /* Callout — admonition box. Static styles live here (not inline) so a
2
+ project's custom CSS can override them with a plain `callout { ... }`
3
+ element selector, as on Mintlify. The per-variant colours arrive as inline
4
+ custom properties (--velu-callout-bg / --velu-callout-stroke) set by
5
+ Callout.jsx from the type / stroke / bg props. */
6
+ .velu-callout {
7
+ display: flow-root; /* contains the floated icon, no clipping */
8
+ padding: var(--s0) var(--s3);
9
+ background: var(--velu-callout-bg);
10
+ border: var(--border-width) solid var(--velu-callout-stroke);
11
+ border-radius: var(--radius-sm);
12
+ color: var(--text-color);
13
+ font-size: var(--f-body);
14
+ line-height: var(--lh-body);
15
+ }
16
+
17
+ /* The icon is FLOATED into the text flow (not a fixed column): it sits on the
18
+ first line and the text wraps beside it, then flows back under it. One
19
+ line-box tall so it's centred on the first line. */
20
+ .velu-callout__icon {
21
+ float: left;
22
+ display: inline-flex;
23
+ align-items: center;
24
+ block-size: calc(var(--f-body) * var(--lh-body));
25
+ margin-inline-end: var(--s-1);
26
+ color: var(--velu-callout-stroke);
27
+ font-size: var(--f-body);
28
+ }
@@ -2,6 +2,7 @@
2
2
  (cta hover, image clip, group grid) so this has CSS. Namespaced. */
3
3
 
4
4
  .velu-card {
5
+ position: relative;
5
6
  display: flex;
6
7
  flex-direction: column;
7
8
  border: var(--border-width) solid var(--border-color);
@@ -11,6 +12,14 @@
11
12
  color: var(--text-color);
12
13
  text-decoration: none;
13
14
  }
15
+ .velu-card__link { position: absolute; inset: 0; z-index: 1; border-radius: inherit; }
16
+ .velu-card:has(> .velu-card__link):hover { border-color: var(--accent-color); }
17
+ .velu-card__link:focus-visible { outline: 2px solid var(--accent-color); outline-offset: -2px; }
18
+ /* Nested controls must sit above the stretched-link overlay, or clicks hit the card href. */
19
+ .velu-card :is(a, button, input, select, textarea, summary, label):not(.velu-card__link) {
20
+ position: relative;
21
+ z-index: 2;
22
+ }
14
23
 
15
24
  /* CTA card: the whole card is the click target (stretched-link pattern —
16
25
  the CTA stays the single real <a>; a ::after overlay makes the entire
@@ -45,6 +45,31 @@
45
45
  0 0 var(--s2) color-mix(in srgb, #000 18%, transparent),
46
46
  0 0 var(--s0) color-mix(in srgb, #000 8%, transparent);
47
47
  }
48
+
49
+ /* Full-page Ask AI (`mode: assistant`). Sits under the navbar and
50
+ replaces the docs body instead of sliding in as a side panel. */
51
+ .velu-chatbot--page {
52
+ inset-block-start: var(--velu-header-height, 3.5rem);
53
+ inset-block-end: 0;
54
+ inset-inline: 0;
55
+ inline-size: 100%;
56
+ max-inline-size: none;
57
+ transform: none;
58
+ border-inline-start: 0;
59
+ border-block-start: var(--border-width) solid var(--surface-color);
60
+ box-shadow: none;
61
+ z-index: 5;
62
+ /* Side gutters so the full-page chat isn't flush to the viewport. */
63
+ padding-inline: var(--s4);
64
+ }
65
+ .velu-chatbot--page .velu-chatbot__header,
66
+ .velu-chatbot--page .velu-chatbot__body,
67
+ .velu-chatbot--page .velu-chatbot__composer,
68
+ .velu-chatbot--page .velu-chatbot__history {
69
+ max-inline-size: 42rem;
70
+ inline-size: 100%;
71
+ margin-inline: auto;
72
+ }
48
73
  /* base.css applies a global `* { max-width: 66ch }`; reset it so the
49
74
  panel's structural elements size to flex/the panel, not 66ch. */
50
75
  .velu-chatbot * {
@@ -118,6 +143,23 @@
118
143
  .velu-chatbot--open {
119
144
  transform: translateY(0);
120
145
  }
146
+ /* Page-mode assistant stays a full-viewport chat, not a bottom sheet. */
147
+ .velu-chatbot--page,
148
+ .velu-chatbot--page.velu-chatbot--open {
149
+ inset-block-start: var(--velu-header-height, 3.5rem);
150
+ inset-block-end: 0;
151
+ inset-inline: 0;
152
+ inline-size: 100%;
153
+ max-inline-size: none;
154
+ block-size: auto;
155
+ transform: none;
156
+ border-radius: 0;
157
+ padding-block-start: 0;
158
+ box-shadow: none;
159
+ }
160
+ .velu-chatbot--page .velu-chatbot__drag-handle {
161
+ display: none;
162
+ }
121
163
  }
122
164
 
123
165
  /* ── Header ─────────────────────────────────────────────────────────── */
@@ -71,8 +71,13 @@
71
71
  }
72
72
 
73
73
  .velu-docs-layout__aside--left {
74
+ /* Shared surface token for the rail and its sticky descendants. Project
75
+ CSS can override this once on #sidebar; section headers and their fade
76
+ then remain visually continuous with the customized rail. */
77
+ --velu-sidebar-bg: var(--page-bg);
74
78
  inset-inline-start: 0;
75
79
  inline-size: var(--velu-sidebar-width);
80
+ background: var(--velu-sidebar-bg);
76
81
  /* Left gutter so the nav sits inset from the viewport edge rather
77
82
  than hugging it. (At mobile the @container block resets this to 0
78
83
  — the drawer manages its own padding.) */
@@ -803,6 +808,81 @@
803
808
  }
804
809
  }
805
810
 
811
+ /* ── Page modes (frontmatter `mode`) ─────────────────────────────────── */
812
+ /* Mintlify-compatible layouts. Default is the current chrome (sidebar +
813
+ TOC + footer) and needs no extra rules. */
814
+
815
+ /* Wide: drop the right rail, keep the right gutter, let the article
816
+ use the extra width inside that gutter. */
817
+ .velu-docs-layout[data-page-mode='wide'] .velu-docs-layout__aside--right,
818
+ .velu-docs-layout[data-page-mode='frame'] .velu-docs-layout__aside--right {
819
+ display: none;
820
+ }
821
+ .velu-docs-layout[data-page-mode='wide'] .velu-docs-layout__article {
822
+ max-inline-size: none;
823
+ }
824
+
825
+ /* Frame is custom canvas + sidebar: no TOC gutter; the landing column
826
+ is centered in the remaining width next to the sidebar. */
827
+ .velu-docs-layout[data-page-mode='frame'] .velu-docs-layout__center {
828
+ margin-inline-end: 0;
829
+ }
830
+
831
+ /* Custom / center / assistant: no sidebar, no TOC. */
832
+ .velu-docs-layout[data-page-mode='custom'] .velu-docs-layout__aside,
833
+ .velu-docs-layout[data-page-mode='center'] .velu-docs-layout__aside,
834
+ .velu-docs-layout[data-page-mode='assistant'] .velu-docs-layout__aside {
835
+ display: none;
836
+ }
837
+ .velu-docs-layout[data-page-mode='custom'] .velu-docs-layout__center,
838
+ .velu-docs-layout[data-page-mode='center'] .velu-docs-layout__center,
839
+ .velu-docs-layout[data-page-mode='assistant'] .velu-docs-layout__center {
840
+ margin-inline: 0;
841
+ }
842
+ .velu-docs-layout[data-page-mode='custom'] .velu-docs-layout__sidebar-toggle,
843
+ .velu-docs-layout[data-page-mode='center'] .velu-docs-layout__sidebar-toggle,
844
+ .velu-docs-layout[data-page-mode='assistant'] .velu-docs-layout__sidebar-toggle,
845
+ .velu-docs-layout[data-page-mode='wide'] .velu-toc-bar,
846
+ .velu-docs-layout[data-page-mode='custom'] .velu-toc-bar,
847
+ .velu-docs-layout[data-page-mode='frame'] .velu-toc-bar,
848
+ .velu-docs-layout[data-page-mode='center'] .velu-toc-bar,
849
+ .velu-docs-layout[data-page-mode='assistant'] .velu-toc-bar {
850
+ display: none;
851
+ }
852
+
853
+ /* Custom / frame are a landing canvas: center a max-w-4xl column.
854
+ Frame keeps the sidebar; custom does not. */
855
+ .velu-docs-layout[data-page-mode='custom'] .velu-docs-layout__main,
856
+ .velu-docs-layout[data-page-mode='frame'] .velu-docs-layout__main {
857
+ padding-block: calc(var(--s5) * 2);
858
+ padding-inline: var(--s4);
859
+ }
860
+ .velu-docs-layout[data-page-mode='custom'] .velu-docs-layout__article,
861
+ .velu-docs-layout[data-page-mode='frame'] .velu-docs-layout__article {
862
+ max-inline-size: 56rem;
863
+ margin-inline: auto;
864
+ }
865
+ .velu-docs-layout[data-page-mode='custom'] .velu-prose > :is(h1, h2, h3, p),
866
+ .velu-docs-layout[data-page-mode='frame'] .velu-prose > :is(h1, h2, h3, p) {
867
+ max-width: none;
868
+ text-align: center;
869
+ }
870
+
871
+ /* Assistant page: the chat fills the viewport below the header. Hide
872
+ the empty centre column when the chat is showing (no disabled-search
873
+ fallback). */
874
+ .velu-docs-layout[data-page-mode='assistant']
875
+ .velu-docs-layout__center:not(:has(.velu-assistant-disabled)) {
876
+ display: none;
877
+ }
878
+ .velu-assistant-disabled {
879
+ display: flex;
880
+ align-items: center;
881
+ justify-content: center;
882
+ min-block-size: 70vh;
883
+ padding: var(--s4) var(--s2);
884
+ }
885
+
806
886
  /* ── 404 takeover ─────────────────────────────────────────────────────── */
807
887
  /* On a not-found route the docs chrome (both fixed asides) is hidden and the
808
888
  centre column spans full width to center the <NotFound> content. The header
@@ -39,6 +39,12 @@
39
39
  color-mix(in srgb, #000 55%, transparent);
40
40
  }
41
41
 
42
+ .velu-header--no-tabs {
43
+ /* Tabs row is gone — restore bottom padding so brand / search /
44
+ actions don't sit on the header hairline. Matches the top inset. */
45
+ padding-block-end: var(--s-1);
46
+ }
47
+
42
48
  /* Once the user has scrolled even a single pixel, the header becomes
43
49
  a translucent frosted-glass overlay so the article content shows
44
50
  through faintly underneath. color-mix keeps the recipe token-driven:
@@ -58,15 +58,15 @@
58
58
  takes over (iOS-list style). It sticks within its section wrapper
59
59
  (Sidebar.jsx renders each section in its own Stack), which bounds
60
60
  the push-out. Full-width opaque background so scrolling items
61
- don't show behind it; --page-bg matches the surroundings, so the
62
- bar is invisible until it's actually covering scrolled content. */
61
+ don't show behind it; --velu-sidebar-bg is owned by the left rail and can
62
+ be customized there, so the bar remains continuous with its surroundings. */
63
63
  position: sticky;
64
64
  inset-block-start: 0;
65
65
  /* Above the items (z-index 1) so scrolled rows tuck UNDER the heading (its
66
66
  opaque bg + the ::after fade hide them), not over it; above the gliding
67
67
  indicator (z-index 0) too. */
68
68
  z-index: 2;
69
- background: var(--page-bg);
69
+ background: var(--velu-sidebar-bg, var(--page-bg));
70
70
  display: flex;
71
71
  align-items: center;
72
72
  gap: var(--s-2);
@@ -95,8 +95,8 @@
95
95
  block-size: var(--s3);
96
96
  background: linear-gradient(
97
97
  to bottom,
98
- var(--page-bg),
99
- color-mix(in srgb, var(--page-bg) 55%, transparent),
98
+ var(--velu-sidebar-bg, var(--page-bg)),
99
+ color-mix(in srgb, var(--velu-sidebar-bg, var(--page-bg)) 55%, transparent),
100
100
  transparent
101
101
  );
102
102
  pointer-events: none;
@@ -0,0 +1,66 @@
1
+ /* Element selectors (Mintlify-compatible).
2
+
3
+ Component roots that have no semantic HTML meaning are rendered as the same
4
+ custom tag names Mintlify uses (`<card>`, `<callout>`, `<code-block>`, …) so
5
+ a project's custom CSS can target them as `card { … }`, exactly like on
6
+ Mintlify. Each element keeps its `velu-*` class, so the component CSS is
7
+ unchanged; this file only gives the unknown tags the display the `div` /
8
+ `span` they replaced had, and exempts them from the `* { max-width }`
9
+ measure reset in base.css (which lists `div` and `span` but obviously not
10
+ these). Class rules (specificity 0,1,0) still beat these tag rules (0,0,1).
11
+
12
+ Roots that must stay native for behaviour or accessibility (`<a>`,
13
+ `<button>`, `<details>`, `<summary>`, `<ul>`, `<textarea>`, headings…)
14
+ carry `data-component="<mintlify-name>"` instead. */
15
+
16
+ /* Block-level: replaced a <div> / <section>. */
17
+ accordion-group,
18
+ advanced-footer,
19
+ api-section,
20
+ callout,
21
+ card,
22
+ card-group,
23
+ chat-assistant-floating-input,
24
+ chat-assistant-sheet-content,
25
+ chat-assistant-sheet-header,
26
+ chat-assistant-disclaimer-text,
27
+ code-block,
28
+ code-group,
29
+ columns,
30
+ contextual-feedback-container,
31
+ contextual-feedback-form,
32
+ feedback-toolbar,
33
+ field,
34
+ mdx-content,
35
+ nav-tabs,
36
+ not-found-container,
37
+ not-found-status-code,
38
+ option-dropdown,
39
+ prompt,
40
+ sidebar-group,
41
+ sidebar-nav-group-divider,
42
+ step,
43
+ steps,
44
+ toc,
45
+ toc-item,
46
+ topbar-right-container,
47
+ tree,
48
+ tree-file,
49
+ update {
50
+ display: block;
51
+ max-width: none;
52
+ }
53
+
54
+ /* Inline: replaced a <span>. */
55
+ breadcrumb-item,
56
+ code-block-icon,
57
+ eyebrow,
58
+ method-pill,
59
+ nav-dropdown-item-icon,
60
+ nav-dropdown-item-title,
61
+ pagination-title,
62
+ sidebar-group-icon,
63
+ starter-question-text {
64
+ display: inline;
65
+ max-width: none;
66
+ }
@@ -22,11 +22,22 @@ import React from 'react';
22
22
  * @param {string} [props.justify='flex-start']
23
23
  * @param {string} [props.align='flex-start']
24
24
  */
25
+
26
+ // React 18 sets props on custom elements (tag names with a hyphen, e.g. the
27
+ // Mintlify-compatible `as="sidebar-group"`) as attributes verbatim, so
28
+ // `className` would come out as a literal className="" attribute. Hand those
29
+ // tags `class` instead. (React 19 maps className for custom elements itself,
30
+ // and accepts `class` too.)
31
+ const isCustomTag = (Tag) => typeof Tag === 'string' && Tag.includes('-');
32
+ const classProp = (Tag, className) =>
33
+ className == null ? {} : { [isCustomTag(Tag) ? 'class' : 'className']: className };
34
+
25
35
  export default function Cluster({
26
36
  as: Tag = 'div',
27
37
  space = 'var(--s1)',
28
38
  justify = 'flex-start',
29
39
  align = 'flex-start',
40
+ className,
30
41
  style,
31
42
  children,
32
43
  ...rest
@@ -41,6 +52,7 @@ export default function Cluster({
41
52
  gap: space,
42
53
  ...style,
43
54
  }}
55
+ {...classProp(Tag, className)}
44
56
  {...rest}
45
57
  >
46
58
  {children}