@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
@@ -45,9 +45,10 @@ export default function ChangelogFilters({ tags = [] }) {
45
45
  };
46
46
 
47
47
  return (
48
- <div className="velu-changelog-filters">
48
+ <div id="changelog-filters" className="velu-changelog-filters">
49
49
  <div className="velu-changelog-filters__heading">Filters</div>
50
50
  <div
51
+ id="changelog-filters-content"
51
52
  className="velu-changelog-filters__list"
52
53
  role="group"
53
54
  aria-label="Filter updates by tag"
@@ -336,9 +336,10 @@ function renderStream(tokens, sources, onNavigate) {
336
336
  }
337
337
 
338
338
  /* ── Header ─────────────────────────────────────────────────────────── */
339
- function ChatHeader({ onClose, onNew, onHistory, historyOpen }) {
339
+ function ChatHeader({ onClose, onNew, onHistory, historyOpen, showClose = true }) {
340
340
  return (
341
341
  <Cluster
342
+ as="chat-assistant-sheet-header"
342
343
  space="var(--s-2)"
343
344
  justify="space-between"
344
345
  align="center"
@@ -369,6 +370,7 @@ function ChatHeader({ onClose, onNew, onHistory, historyOpen }) {
369
370
  >
370
371
  {resolveIcon('plus', { size: '1em' })}
371
372
  </button>
373
+ {showClose && (
372
374
  <button
373
375
  type="button"
374
376
  className="velu-chatbot__iconbtn"
@@ -377,6 +379,7 @@ function ChatHeader({ onClose, onNew, onHistory, historyOpen }) {
377
379
  >
378
380
  {resolveIcon('x', { size: '1em' })}
379
381
  </button>
382
+ )}
380
383
  </Cluster>
381
384
  </Cluster>
382
385
  );
@@ -461,7 +464,7 @@ function Welcome({ onPick, suggestions }) {
461
464
  >
462
465
  <Cluster space="var(--s-2)" align="center">
463
466
  {resolveIcon(s.icon, { size: '1em' })}
464
- <span>{s.label}</span>
467
+ <starter-question-text>{s.label}</starter-question-text>
465
468
  </Cluster>
466
469
  {resolveIcon('arrow-right', { size: '1em' })}
467
470
  </button>
@@ -616,6 +619,8 @@ function Composer({ value, onChange, onSubmit, disabled }) {
616
619
  <div className="velu-chatbot__input-wrap">
617
620
  <textarea
618
621
  ref={taRef}
622
+ id="chat-assistant-textarea"
623
+ data-component="chat-assistant-input"
619
624
  className="velu-chatbot__input"
620
625
  placeholder="Ask anything about the docs…"
621
626
  rows={1}
@@ -625,6 +630,7 @@ function Composer({ value, onChange, onSubmit, disabled }) {
625
630
  />
626
631
  <button
627
632
  type="button"
633
+ data-component="chat-assistant-send-button"
628
634
  className={`velu-chatbot__send${active ? ' is-active' : ''}`}
629
635
  onClick={onSubmit}
630
636
  disabled={!active}
@@ -633,9 +639,9 @@ function Composer({ value, onChange, onSubmit, disabled }) {
633
639
  {resolveIcon('arrow-up', { size: '1em' })}
634
640
  </button>
635
641
  </div>
636
- <div className="velu-chatbot__foot">
642
+ <chat-assistant-disclaimer-text class="velu-chatbot__foot">
637
643
  <kbd>Enter</kbd> to send · <kbd>⇧ Enter</kbd> for newline
638
- </div>
644
+ </chat-assistant-disclaimer-text>
639
645
  </div>
640
646
  );
641
647
  }
@@ -651,6 +657,7 @@ export default function Chatbot({
651
657
  listHistory,
652
658
  loadConversation,
653
659
  suggestions,
660
+ layout = 'panel',
654
661
  className = '',
655
662
  ...rest
656
663
  }) {
@@ -878,8 +885,9 @@ export default function Chatbot({
878
885
  const isBottomSheet = () =>
879
886
  typeof window !== 'undefined' &&
880
887
  window.matchMedia('(max-width: 1024px)').matches;
888
+ const isPage = layout === 'page';
881
889
  const onDragStart = (e) => {
882
- if (!isBottomSheet()) return;
890
+ if (isPage || !isBottomSheet()) return;
883
891
  dragRef.current = { active: true, startY: e.clientY, currentY: 0 };
884
892
  e.currentTarget.setPointerCapture?.(e.pointerId);
885
893
  };
@@ -902,7 +910,7 @@ export default function Chatbot({
902
910
  if (!open) setDragOffset(0);
903
911
  }, [open]);
904
912
 
905
- const cls = `velu-chatbot${open ? ' velu-chatbot--open' : ''} ${className}`.trim();
913
+ const cls = `velu-chatbot${open || isPage ? ' velu-chatbot--open' : ''}${isPage ? ' velu-chatbot--page' : ''} ${className}`.trim();
906
914
  const dragStyle =
907
915
  dragOffset > 0
908
916
  ? { transform: `translateY(${dragOffset}px)`, transition: 'none' }
@@ -910,8 +918,10 @@ export default function Chatbot({
910
918
 
911
919
  return (
912
920
  <aside
921
+ id="chat-assistant-sheet"
922
+ data-component="chat-assistant-sheet"
913
923
  className={cls}
914
- aria-hidden={!open}
924
+ aria-hidden={isPage ? false : !open}
915
925
  aria-label="Ask AI"
916
926
  style={dragStyle}
917
927
  {...rest}
@@ -933,6 +943,7 @@ export default function Chatbot({
933
943
  onNew={newChat}
934
944
  onHistory={() => setHistoryOpen((o) => !o)}
935
945
  historyOpen={historyOpen}
946
+ showClose={!isPage}
936
947
  />
937
948
 
938
949
  {historyOpen && (
@@ -944,7 +955,7 @@ export default function Chatbot({
944
955
  />
945
956
  )}
946
957
 
947
- <div className="velu-chatbot__body" ref={bodyRef}>
958
+ <chat-assistant-sheet-content class="velu-chatbot__body" ref={bodyRef}>
948
959
  {messages.length === 0 ? (
949
960
  <Welcome onPick={(t) => send(t)} suggestions={suggestions} />
950
961
  ) : (
@@ -968,7 +979,7 @@ export default function Chatbot({
968
979
  )}
969
980
  </Stack>
970
981
  )}
971
- </div>
982
+ </chat-assistant-sheet-content>
972
983
 
973
984
  <Composer
974
985
  value={input}
@@ -90,9 +90,9 @@ function TabLabel({ icon, label }) {
90
90
  return (
91
91
  <>
92
92
  {icon != null && (
93
- <span className="velu-code-block__tab-icon" aria-hidden="true">
93
+ <code-block-icon class="velu-code-block__tab-icon" aria-hidden="true">
94
94
  {typeof icon === 'string' ? resolveIcon(icon, { size: '1em' }) : icon}
95
- </span>
95
+ </code-block-icon>
96
96
  )}
97
97
  <span className="velu-code-block__tab-label">{label}</span>
98
98
  </>
@@ -143,6 +143,7 @@ function CopyButton({ code }) {
143
143
  type="button"
144
144
  onClick={copy}
145
145
  className="velu-code-block__copy"
146
+ data-component="code-block-copy-button"
146
147
  aria-label={copied ? 'Copied' : 'Copy code'}
147
148
  title={copied ? 'Copied' : 'Copy'}
148
149
  >
@@ -242,7 +243,7 @@ export function CodeBlock({
242
243
  const label = filename || title;
243
244
  const tabIcon = pickIcon({ icon, withIcon, language });
244
245
  return (
245
- <div className={cls} {...rest}>
246
+ <code-block class={cls} {...rest}>
246
247
  {label && (
247
248
  <div className="velu-code-block__header">
248
249
  <Tab active icon={tabIcon} label={label} />
@@ -254,7 +255,7 @@ export function CodeBlock({
254
255
  lineNumbers={lineNumbers}
255
256
  highlight={highlight}
256
257
  />
257
- </div>
258
+ </code-block>
258
259
  );
259
260
  }
260
261
  CodeBlock.displayName = 'CodeBlock';
@@ -340,7 +341,7 @@ export function CodeGroup({
340
341
  }
341
342
 
342
343
  return (
343
- <div className={cls} {...rest}>
344
+ <code-group class={cls} {...rest}>
344
345
  <div className="velu-code-block__header">
345
346
  {canLeft && (
346
347
  <button
@@ -391,7 +392,7 @@ export function CodeGroup({
391
392
  lineNumbers={lineNumbers}
392
393
  highlight={highlight}
393
394
  />
394
- </div>
395
+ </code-group>
395
396
  );
396
397
  }
397
398
 
@@ -44,10 +44,12 @@ export default function Columns({
44
44
  }) {
45
45
  return (
46
46
  <Switcher
47
+ as="columns"
47
48
  className={`velu-columns ${className}`.trim()}
48
49
  space={space}
49
50
  min={min}
50
51
  limit={cols}
52
+ style={{ '--vepa-columns': Math.max(1, Math.min(4, Number(cols) || 2)) }}
51
53
  {...rest}
52
54
  >
53
55
  {children}
@@ -256,11 +256,11 @@ export default function ContextMenu({
256
256
  className="velu-context-bar"
257
257
  data-pagefind-ignore=""
258
258
  >
259
- {eyebrow ? <span className="velu-context-bar__eyebrow">{eyebrow}</span> : <span />}
259
+ {eyebrow ? <eyebrow className="velu-context-bar__eyebrow">{eyebrow}</eyebrow> : <span />}
260
260
 
261
261
  <div className="velu-context-bar__actions">
262
262
  {items.length > 0 && (
263
- <div ref={rootRef} className="velu-context-menu" data-open={open ? 'true' : 'false'}>
263
+ <div ref={rootRef} id="page-context-menu" className="velu-context-menu" data-open={open ? 'true' : 'false'}>
264
264
  <div className="velu-context-menu__split">
265
265
  <button
266
266
  type="button"
@@ -275,6 +275,7 @@ export default function ContextMenu({
275
275
  </button>
276
276
  <button
277
277
  type="button"
278
+ id="page-context-menu-button"
278
279
  className="velu-context-menu__toggle"
279
280
  aria-haspopup="menu"
280
281
  aria-expanded={open}
@@ -46,7 +46,7 @@ export default function Field({
46
46
  }) {
47
47
  const hasDefault = defaultValue !== undefined && defaultValue !== null;
48
48
  return (
49
- <div className={`velu-field ${className}`.trim()} {...rest}>
49
+ <field className={`velu-field ${className}`.trim()} {...rest}>
50
50
  {/* Cluster handles the wrap-friendly inline row layout — chips +
51
51
  name + type + required + default + post — with a 16px gap and
52
52
  baseline alignment so the bold name doesn't shift vertically
@@ -76,6 +76,6 @@ export default function Field({
76
76
  {children != null && children !== false && (
77
77
  <div className="velu-field__body">{children}</div>
78
78
  )}
79
- </div>
79
+ </field>
80
80
  );
81
81
  }
@@ -40,6 +40,7 @@ export default function Icon({
40
40
  src={icon}
41
41
  alt={ariaLabel || ''}
42
42
  className={classes}
43
+ data-component="icon"
43
44
  style={sharedStyle}
44
45
  aria-hidden={ariaLabel ? undefined : true}
45
46
  {...rest}
@@ -149,14 +149,14 @@ export default function Image({
149
149
 
150
150
  if (caption) {
151
151
  return (
152
- <figure className={cls} {...rest}>
152
+ <figure className={cls} data-component={chrome === 'frame' ? 'frame' : undefined} {...rest}>
153
153
  {body}
154
154
  <figcaption className="velu-image__caption">{caption}</figcaption>
155
155
  </figure>
156
156
  );
157
157
  }
158
158
  return (
159
- <div className={cls} {...rest}>
159
+ <div className={cls} data-component={chrome === 'frame' ? 'frame' : undefined} {...rest}>
160
160
  {body}
161
161
  </div>
162
162
  );
@@ -24,8 +24,8 @@ export default function MethodBadge({
24
24
  const safe = METHODS.includes(key) ? key : 'get';
25
25
  const cls = `velu-method-badge velu-method-badge--${safe} ${className}`.trim();
26
26
  return (
27
- <span className={cls} {...rest}>
27
+ <method-pill class={cls} {...rest}>
28
28
  {String(method).toUpperCase()}
29
- </span>
29
+ </method-pill>
30
30
  );
31
31
  }
@@ -30,9 +30,16 @@ export default function NavSelect({
30
30
  bare = false,
31
31
  linkComponent = 'a',
32
32
  className = '',
33
+ // Mintlify-compatible hooks: id prefix for the trigger/menu
34
+ // (e.g. 'localization-select' → #localization-select-trigger /
35
+ // #localization-select-content / #localization-select-item) and the
36
+ // data-component family ('nav-dropdown' → nav-dropdown-trigger, …).
37
+ idPrefix,
38
+ component = 'nav-dropdown',
33
39
  ...rest
34
40
  }) {
35
41
  const Link = linkComponent;
42
+ const hookId = (suffix) => (idPrefix ? `${idPrefix}-${suffix}` : undefined);
36
43
  const [open, setOpen] = React.useState(false);
37
44
  const rootRef = React.useRef(null);
38
45
 
@@ -61,6 +68,8 @@ export default function NavSelect({
61
68
  >
62
69
  <button
63
70
  type="button"
71
+ id={hookId('trigger')}
72
+ data-component={`${component}-trigger`}
64
73
  className="velu-nav-select__btn"
65
74
  aria-haspopup="menu"
66
75
  aria-expanded={open}
@@ -82,23 +91,32 @@ export default function NavSelect({
82
91
  focusable="false"
83
92
  />
84
93
  </button>
85
- <ul className="velu-nav-select__menu" role="menu" aria-hidden={!open}>
94
+ <ul
95
+ id={hookId('content')}
96
+ data-component={`${component}-content`}
97
+ className="velu-nav-select__menu"
98
+ role="menu"
99
+ aria-hidden={!open}
100
+ >
86
101
  {options.map((o, i) => (
87
102
  <li key={o.href ?? i} role="none">
88
103
  <Link
89
104
  role="menuitem"
105
+ id={i === 0 ? hookId('item') : undefined}
106
+ data-component={`${component}-item`}
107
+ data-active={o.active ? 'true' : undefined}
90
108
  className={`velu-nav-select__item${o.active ? ' velu-nav-select__item--active' : ''}`}
91
109
  href={o.href}
92
110
  tabIndex={open ? 0 : -1}
93
111
  onClick={() => setOpen(false)}
94
112
  >
95
113
  {o.icon && (
96
- <span className="velu-nav-select__item-icon" aria-hidden="true">
114
+ <nav-dropdown-item-icon class="velu-nav-select__item-icon" aria-hidden="true">
97
115
  {resolveIcon(o.icon, { size: '1em' })}
98
- </span>
116
+ </nav-dropdown-item-icon>
99
117
  )}
100
118
  {o.code && <span className="velu-nav-select__code">{o.code}</span>}
101
- <span>{o.label}</span>
119
+ <nav-dropdown-item-title>{o.label}</nav-dropdown-item-title>
102
120
  </Link>
103
121
  </li>
104
122
  ))}
@@ -27,15 +27,15 @@ export default function NotFound({
27
27
  }) {
28
28
  const Link = linkComponent;
29
29
  return (
30
- <section className="velu-404">
30
+ <not-found-container class="velu-404">
31
31
  <p className="velu-404__eyebrow">{eyebrow}</p>
32
- <div className="velu-404__num" aria-hidden="true">
32
+ <not-found-status-code class="velu-404__num" aria-hidden="true">
33
33
  404
34
- </div>
35
- <h1 className="velu-404__title">{title}</h1>
36
- <p className="velu-404__body">{body}</p>
34
+ </not-found-status-code>
35
+ <h1 className="velu-404__title" data-component="not-found-title">{title}</h1>
36
+ <p className="velu-404__body" data-component="not-found-description">{body}</p>
37
37
  <div className="velu-404__actions">
38
- <Link className="velu-404__btn velu-404__btn--primary" href={homeHref}>
38
+ <Link className="velu-404__btn velu-404__btn--primary" data-component="not-found-recommended-page-link" href={homeHref}>
39
39
  <span className="velu-404__btn-ic" aria-hidden="true">
40
40
  {resolveIcon('house', { size: '1em' })}
41
41
  </span>
@@ -58,6 +58,6 @@ export default function NotFound({
58
58
  </button>
59
59
  )}
60
60
  </div>
61
- </section>
61
+ </not-found-container>
62
62
  );
63
63
  }
@@ -110,16 +110,19 @@ export default function PageFeedback({
110
110
 
111
111
  return (
112
112
  <Stack
113
+ as="contextual-feedback-container"
113
114
  space="var(--s0)"
114
115
  className={`velu-feedback ${className}`.trim()}
115
116
  {...rest}
116
117
  >
117
- <Cluster space="var(--s0)" align="center" className="velu-feedback__bar">
118
+ <Cluster as="feedback-toolbar" space="var(--s0)" align="center" className="velu-feedback__bar">
118
119
  <span className="velu-feedback__question">{question}</span>
119
120
 
120
121
  <Cluster space="var(--s-2)" align="center">
121
122
  <button
122
123
  type="button"
124
+ id="feedback-thumbs-up"
125
+ data-component="contextual-feedback-button"
123
126
  className={`velu-feedback__btn${
124
127
  vote === 'yes' ? ' velu-feedback__btn--chosen' : ''
125
128
  }`}
@@ -132,6 +135,8 @@ export default function PageFeedback({
132
135
  </button>
133
136
  <button
134
137
  type="button"
138
+ id="feedback-thumbs-down"
139
+ data-component="contextual-feedback-button"
135
140
  className={`velu-feedback__btn${
136
141
  vote === 'no' ? ' velu-feedback__btn--chosen' : ''
137
142
  }`}
@@ -151,8 +156,8 @@ export default function PageFeedback({
151
156
  </Cluster>
152
157
 
153
158
  {showForm && (
154
- <Stack space="var(--s1)" className="velu-feedback__form">
155
- <h3 className="velu-feedback__form-title">
159
+ <Stack as="contextual-feedback-form" id="feedback-form" space="var(--s1)" className="velu-feedback__form">
160
+ <h3 className="velu-feedback__form-title" data-component="contextual-feedback-form-title">
156
161
  How can we improve our product?
157
162
  </h3>
158
163
 
@@ -176,6 +181,8 @@ export default function PageFeedback({
176
181
  </Stack>
177
182
 
178
183
  <textarea
184
+ id="feedback-form-input"
185
+ data-component="contextual-feedback-input"
179
186
  className="velu-feedback__detail"
180
187
  placeholder="(optional) Could you share more about your experience?"
181
188
  value={detail}
@@ -186,6 +193,7 @@ export default function PageFeedback({
186
193
  <Cluster space="var(--s-1)">
187
194
  <button
188
195
  type="button"
196
+ id="feedback-form-cancel"
189
197
  className="velu-feedback__action velu-feedback__action--cancel"
190
198
  onClick={cancel}
191
199
  >
@@ -193,6 +201,8 @@ export default function PageFeedback({
193
201
  </button>
194
202
  <button
195
203
  type="button"
204
+ id="feedback-form-submit"
205
+ data-component="contextual-feedback-form-submit-button"
196
206
  className="velu-feedback__action velu-feedback__action--submit"
197
207
  onClick={submit}
198
208
  >
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import VepaFooter from './VepaFooter.jsx';
2
3
  import Stack from '../primitives/Stack.jsx';
3
4
  import Cluster from '../primitives/Cluster.jsx';
4
5
  import SocialLinks from './SocialLinks.jsx';
@@ -67,6 +68,7 @@ function VeluMark() {
67
68
  }
68
69
 
69
70
  export default function PageFooter({
71
+ preset,
70
72
  brand,
71
73
  columns = [],
72
74
  socials = [],
@@ -75,6 +77,7 @@ export default function PageFooter({
75
77
  ...rest
76
78
  }) {
77
79
  const Link = linkComponent;
80
+ if (preset === 'vepa') return <VepaFooter brand={brand} columns={columns} socials={socials} linkComponent={Link} />;
78
81
  const brandLabel = brand?.label ?? 'Velu';
79
82
  const brandHref = brand?.href;
80
83
 
@@ -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', 'data-component-name': kind === 'primary' ? 'primary-header-button' : undefined };
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 && (
@@ -226,6 +232,7 @@ function KebabMenu({ actions, Link }) {
226
232
  }
227
233
 
228
234
  export default function PageHeader({
235
+ preset,
229
236
  brand,
230
237
  brandTrailing,
231
238
  center,
@@ -243,6 +250,7 @@ export default function PageHeader({
243
250
  const Link = linkComponent;
244
251
  const brandLabel = brand?.label ?? 'Velu';
245
252
  const brandHref = brand?.href;
253
+ const firstPrimary = actions.findIndex((a) => a.kind === 'primary');
246
254
 
247
255
  // Track whether the page has been scrolled so the header can flip
248
256
  // from solid to translucent / frosted. Edge-triggered (only updates
@@ -259,6 +267,12 @@ export default function PageHeader({
259
267
  return () => window.removeEventListener('scroll', onScroll);
260
268
  }, []);
261
269
 
270
+ const showTabsRow =
271
+ tabs.length > 0 ||
272
+ (breadcrumb && breadcrumb.length > 0) ||
273
+ Boolean(tabsTrailing) ||
274
+ Boolean(onMenuClick);
275
+
262
276
  // A configured logo replaces the mark + wordmark entirely (Mintlify-style).
263
277
  const brandBlock = (
264
278
  <Cluster space="var(--s-3)" align="center" className="velu-header__brand" style={{ flexWrap: 'nowrap' }}>
@@ -276,8 +290,11 @@ export default function PageHeader({
276
290
  return (
277
291
  <Stack
278
292
  as="header"
279
- space="var(--s0)"
280
- className={`velu-header${scrolled ? ' velu-header--scrolled' : ''} ${className}`.trim()}
293
+ space={preset === 'vepa' ? '0px' : 'var(--s0)'}
294
+ data-has-tabs={tabs.length > 0 ? 'true' : 'false'}
295
+ className={`velu-header${scrolled ? ' velu-header--scrolled' : ''}${
296
+ showTabsRow ? '' : ' velu-header--no-tabs'
297
+ } ${className}`.trim()}
281
298
  {...rest}
282
299
  >
283
300
  {/* Top row — 3-column grid: brand | center | actions. */}
@@ -288,7 +305,7 @@ export default function PageHeader({
288
305
  className="velu-header__col velu-header__col--brand"
289
306
  >
290
307
  {brandHref ? (
291
- <Link href={brandHref} className="velu-header__brand-link">
308
+ <Link href={brandHref} className="velu-header__brand-link" data-component="nav-logo">
292
309
  {brandBlock}
293
310
  </Link>
294
311
  ) : (
@@ -306,6 +323,7 @@ export default function PageHeader({
306
323
 
307
324
  {(actions.length > 0 || trailing) && (
308
325
  <Cluster
326
+ as="topbar-right-container"
309
327
  space="var(--s-2)"
310
328
  align="center"
311
329
  className="velu-header__col velu-header__col--actions"
@@ -316,7 +334,12 @@ export default function PageHeader({
316
334
  {actions.length > 0 && (
317
335
  <div className="velu-header__actions-list">
318
336
  {actions.map((a, i) => (
319
- <ActionButton key={i} action={a} Link={Link} />
337
+ <ActionButton
338
+ key={i}
339
+ action={a}
340
+ Link={Link}
341
+ id={i === firstPrimary ? 'topbar-cta-button' : undefined}
342
+ />
320
343
  ))}
321
344
  </div>
322
345
  )}
@@ -336,11 +359,9 @@ export default function PageHeader({
336
359
  takes their place. Burger stays at the trailing edge via
337
360
  margin-inline-start: auto on the burger button itself, so
338
361
  its position is unaffected by the breadcrumb's length. */}
339
- {(tabs.length > 0 ||
340
- (breadcrumb && breadcrumb.length > 0) ||
341
- tabsTrailing ||
342
- onMenuClick) && (
362
+ {showTabsRow && (
343
363
  <Cluster
364
+ as="nav-tabs"
344
365
  space="var(--s0)"
345
366
  align="center"
346
367
  className="velu-header__tabs"
@@ -367,6 +388,7 @@ export default function PageHeader({
367
388
  <button
368
389
  type="button"
369
390
  className="velu-header__crumbs"
391
+ data-component="breadcrumb-list"
370
392
  onClick={onMenuClick}
371
393
  aria-label="Open navigation menu"
372
394
  >
@@ -381,13 +403,13 @@ export default function PageHeader({
381
403
  focusable="false"
382
404
  />
383
405
  )}
384
- <span
385
- className={`velu-header__crumb${
406
+ <breadcrumb-item
407
+ class={`velu-header__crumb${
386
408
  isLast ? ' velu-header__crumb--current' : ''
387
409
  }`}
388
410
  >
389
411
  {c.label}
390
- </span>
412
+ </breadcrumb-item>
391
413
  </React.Fragment>
392
414
  );
393
415
  })}
@@ -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}