@veluai/velu 0.2.35 → 0.2.36

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 (45) hide show
  1. package/README.md +39 -0
  2. package/dist/cli.js +57 -49
  3. package/package.json +1 -1
  4. package/runtime/velu-ui/components/Accordion.jsx +3 -3
  5. package/runtime/velu-ui/components/ApiClient.jsx +4 -2
  6. package/runtime/velu-ui/components/ApiReferencePage.jsx +12 -12
  7. package/runtime/velu-ui/components/ApiSamples.jsx +2 -2
  8. package/runtime/velu-ui/components/AskBar.jsx +3 -2
  9. package/runtime/velu-ui/components/Callout.jsx +11 -26
  10. package/runtime/velu-ui/components/Card.jsx +9 -8
  11. package/runtime/velu-ui/components/ChangelogFilters.jsx +2 -1
  12. package/runtime/velu-ui/components/Chatbot.jsx +11 -5
  13. package/runtime/velu-ui/components/CodeBlock.jsx +7 -6
  14. package/runtime/velu-ui/components/Columns.jsx +1 -0
  15. package/runtime/velu-ui/components/ContextMenu.jsx +3 -2
  16. package/runtime/velu-ui/components/Field.jsx +2 -2
  17. package/runtime/velu-ui/components/Icon.jsx +1 -0
  18. package/runtime/velu-ui/components/Image.jsx +2 -2
  19. package/runtime/velu-ui/components/MethodBadge.jsx +2 -2
  20. package/runtime/velu-ui/components/NavSelect.jsx +22 -4
  21. package/runtime/velu-ui/components/NotFound.jsx +7 -7
  22. package/runtime/velu-ui/components/PageFeedback.jsx +13 -3
  23. package/runtime/velu-ui/components/PageHeader.jsx +24 -9
  24. package/runtime/velu-ui/components/PageNav.jsx +2 -1
  25. package/runtime/velu-ui/components/Prompt.jsx +2 -2
  26. package/runtime/velu-ui/components/Search.jsx +1 -0
  27. package/runtime/velu-ui/components/Sidebar.jsx +18 -6
  28. package/runtime/velu-ui/components/Steps.jsx +4 -4
  29. package/runtime/velu-ui/components/ThemeToggle.jsx +1 -0
  30. package/runtime/velu-ui/components/Toc.jsx +6 -4
  31. package/runtime/velu-ui/components/Tree.jsx +6 -5
  32. package/runtime/velu-ui/components/TryItBar.jsx +1 -0
  33. package/runtime/velu-ui/components/Update.jsx +2 -2
  34. package/runtime/velu-ui/components/callout.css +28 -0
  35. package/runtime/velu-ui/components/docs-layout.css +5 -0
  36. package/runtime/velu-ui/components/sidebar.css +5 -5
  37. package/runtime/velu-ui/element-selectors.css +66 -0
  38. package/runtime/velu-ui/primitives/Cluster.jsx +12 -0
  39. package/runtime/velu-ui/primitives/Stack.jsx +12 -0
  40. package/runtime/velu-ui/primitives/Switcher.jsx +11 -1
  41. package/runtime/velu-ui/styles.css +51 -35
  42. package/src/runtime/App.jsx +43 -11
  43. package/src/runtime/client-entry.jsx +23 -0
  44. package/src/runtime/dev-warnings.js +16 -0
  45. package/src/runtime/server-entry.jsx +1 -0
@@ -68,9 +68,13 @@ export function VeluMark() {
68
68
  );
69
69
  }
70
70
 
71
- function ActionButton({ action, Link }) {
72
- const { label, icon, kind = 'outlined', href, onClick, external } = action;
71
+ function ActionButton({ action, Link, id }) {
72
+ const { label, icon, kind = 'outlined', href, onClick, external, component } = action;
73
73
  const cls = `velu-header__action velu-header__action--${kind}`;
74
+ // Mintlify-compatible hooks: `#topbar-cta-button` on the first primary
75
+ // action, `[data-component="navbar-link"]` on every action (overridable per
76
+ // action, e.g. component: 'logout-link').
77
+ const hooks = { id, 'data-component': component ?? 'navbar-link' };
74
78
  const content = (
75
79
  <>
76
80
  {icon && (
@@ -87,13 +91,13 @@ function ActionButton({ action, Link }) {
87
91
  ? { target: '_blank', rel: 'noreferrer' }
88
92
  : {};
89
93
  return (
90
- <Tag className={cls} href={href} {...extraProps}>
94
+ <Tag className={cls} href={href} {...hooks} {...extraProps}>
91
95
  {content}
92
96
  </Tag>
93
97
  );
94
98
  }
95
99
  return (
96
- <button type="button" className={cls} onClick={onClick}>
100
+ <button type="button" className={cls} onClick={onClick} {...hooks}>
97
101
  {content}
98
102
  </button>
99
103
  );
@@ -111,6 +115,8 @@ function HeaderTab({ tab, isActive, Link }) {
111
115
  className={cls}
112
116
  href={href}
113
117
  aria-current={isActive ? 'page' : undefined}
118
+ data-active={isActive ? 'true' : undefined}
119
+ data-component="nav-tabs-item"
114
120
  {...extraProps}
115
121
  >
116
122
  {icon && (
@@ -243,6 +249,7 @@ export default function PageHeader({
243
249
  const Link = linkComponent;
244
250
  const brandLabel = brand?.label ?? 'Velu';
245
251
  const brandHref = brand?.href;
252
+ const firstPrimary = actions.findIndex((a) => a.kind === 'primary');
246
253
 
247
254
  // Track whether the page has been scrolled so the header can flip
248
255
  // from solid to translucent / frosted. Edge-triggered (only updates
@@ -288,7 +295,7 @@ export default function PageHeader({
288
295
  className="velu-header__col velu-header__col--brand"
289
296
  >
290
297
  {brandHref ? (
291
- <Link href={brandHref} className="velu-header__brand-link">
298
+ <Link href={brandHref} className="velu-header__brand-link" data-component="nav-logo">
292
299
  {brandBlock}
293
300
  </Link>
294
301
  ) : (
@@ -306,6 +313,7 @@ export default function PageHeader({
306
313
 
307
314
  {(actions.length > 0 || trailing) && (
308
315
  <Cluster
316
+ as="topbar-right-container"
309
317
  space="var(--s-2)"
310
318
  align="center"
311
319
  className="velu-header__col velu-header__col--actions"
@@ -316,7 +324,12 @@ export default function PageHeader({
316
324
  {actions.length > 0 && (
317
325
  <div className="velu-header__actions-list">
318
326
  {actions.map((a, i) => (
319
- <ActionButton key={i} action={a} Link={Link} />
327
+ <ActionButton
328
+ key={i}
329
+ action={a}
330
+ Link={Link}
331
+ id={i === firstPrimary ? 'topbar-cta-button' : undefined}
332
+ />
320
333
  ))}
321
334
  </div>
322
335
  )}
@@ -341,6 +354,7 @@ export default function PageHeader({
341
354
  tabsTrailing ||
342
355
  onMenuClick) && (
343
356
  <Cluster
357
+ as="nav-tabs"
344
358
  space="var(--s0)"
345
359
  align="center"
346
360
  className="velu-header__tabs"
@@ -367,6 +381,7 @@ export default function PageHeader({
367
381
  <button
368
382
  type="button"
369
383
  className="velu-header__crumbs"
384
+ data-component="breadcrumb-list"
370
385
  onClick={onMenuClick}
371
386
  aria-label="Open navigation menu"
372
387
  >
@@ -381,13 +396,13 @@ export default function PageHeader({
381
396
  focusable="false"
382
397
  />
383
398
  )}
384
- <span
385
- className={`velu-header__crumb${
399
+ <breadcrumb-item
400
+ class={`velu-header__crumb${
386
401
  isLast ? ' velu-header__crumb--current' : ''
387
402
  }`}
388
403
  >
389
404
  {c.label}
390
- </span>
405
+ </breadcrumb-item>
391
406
  </React.Fragment>
392
407
  );
393
408
  })}
@@ -32,9 +32,10 @@ function NavCard({ page, dir, Link }) {
32
32
  return (
33
33
  <Link
34
34
  className={`velu-pagenav__card velu-pagenav__card--${dir}`}
35
+ data-component={`pagination-${dir}`}
35
36
  href={page.href ?? '#'}
36
37
  >
37
- <span className="velu-pagenav__title">{page.label}</span>
38
+ <pagination-title class="velu-pagenav__title">{page.label}</pagination-title>
38
39
  <Cluster
39
40
  space="var(--s-4)"
40
41
  align="center"
@@ -69,7 +69,7 @@ export default function Prompt({
69
69
  }
70
70
 
71
71
  return (
72
- <div className={`velu-prompt ${className}`.trim()} {...rest}>
72
+ <prompt className={`velu-prompt ${className}`.trim()} {...rest}>
73
73
  {title != null && (
74
74
  <div className="velu-prompt__title">{title}</div>
75
75
  )}
@@ -110,6 +110,6 @@ export default function Prompt({
110
110
  {copied ? 'Copied' : 'Copy Prompt'}
111
111
  </button>
112
112
  </Cluster>
113
- </div>
113
+ </prompt>
114
114
  );
115
115
  }
@@ -287,6 +287,7 @@ function SearchPalette({ results, search, placeholder, onSelect, onClose }) {
287
287
  </span>
288
288
  <input
289
289
  ref={inputRef}
290
+ id="search-input"
290
291
  className="velu-search__input"
291
292
  placeholder={placeholder}
292
293
  value={query}
@@ -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
  }
@@ -32,6 +32,7 @@ export default function ThemeToggle({ className = '', ...rest }) {
32
32
  onClick={toggle}
33
33
  aria-label="Toggle color theme"
34
34
  title="Toggle color theme"
35
+ data-component-name="theme-toggle"
35
36
  className={`velu-theme-toggle ${className}`.trim()}
36
37
  {...rest}
37
38
  >
@@ -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,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
+ }
@@ -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.) */
@@ -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}
@@ -20,11 +20,22 @@ import React from 'react';
20
20
  * @param {number|null} [props.splitAfter=null] 1-based child index; everything
21
21
  * after it is pushed to the bottom (margin-block-end:auto on that child)
22
22
  */
23
+
24
+ // React 18 sets props on custom elements (tag names with a hyphen, e.g. the
25
+ // Mintlify-compatible `as="sidebar-group"`) as attributes verbatim, so
26
+ // `className` would come out as a literal className="" attribute. Hand those
27
+ // tags `class` instead. (React 19 maps className for custom elements itself,
28
+ // and accepts `class` too.)
29
+ const isCustomTag = (Tag) => typeof Tag === 'string' && Tag.includes('-');
30
+ const classProp = (Tag, className) =>
31
+ className == null ? {} : { [isCustomTag(Tag) ? 'class' : 'className']: className };
32
+
23
33
  const Stack = React.forwardRef(function Stack(
24
34
  {
25
35
  as: Tag = 'div',
26
36
  space = 'var(--s1)',
27
37
  splitAfter = null,
38
+ className,
28
39
  style,
29
40
  children,
30
41
  ...rest
@@ -53,6 +64,7 @@ const Stack = React.forwardRef(function Stack(
53
64
  ...(splitAfter ? { blockSize: '100%' } : null),
54
65
  ...style,
55
66
  }}
67
+ {...classProp(Tag, className)}
56
68
  {...rest}
57
69
  >
58
70
  {kids}
@@ -22,6 +22,16 @@ import React from 'react';
22
22
  * React-native + SSR-safe + tokenized (vs. the browser-only web component
23
23
  * which injects a runtime <style>). Needs its own CSS (styles `> *`).
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 Switcher({
26
36
  as: Tag = 'div',
27
37
  space = 'var(--s1)',
@@ -42,7 +52,7 @@ export default function Switcher({
42
52
 
43
53
  return (
44
54
  <Tag
45
- className={`velu-switcher ${className}`.trim()}
55
+ {...classProp(Tag, `velu-switcher ${className}`.trim())}
46
56
  data-limit-exceeded={exceeded ? '' : undefined}
47
57
  style={{
48
58
  '--switcher-space': space,