@veluai/velu 0.1.13 → 0.1.15

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 (98) hide show
  1. package/README.md +80 -80
  2. package/dist/cli.js +21 -21
  3. package/package.json +3 -2
  4. package/runtime/velu-ui/components/Accordion.jsx +64 -64
  5. package/runtime/velu-ui/components/ApiClient.jsx +121 -121
  6. package/runtime/velu-ui/components/ApiField.jsx +87 -87
  7. package/runtime/velu-ui/components/ApiPath.jsx +63 -63
  8. package/runtime/velu-ui/components/ApiSidebar.jsx +122 -122
  9. package/runtime/velu-ui/components/AskBar.jsx +71 -71
  10. package/runtime/velu-ui/components/Callout.jsx +114 -114
  11. package/runtime/velu-ui/components/Card.jsx +131 -131
  12. package/runtime/velu-ui/components/Chatbot.jsx +596 -596
  13. package/runtime/velu-ui/components/CodeBlock.jsx +375 -375
  14. package/runtime/velu-ui/components/Columns.jsx +56 -56
  15. package/runtime/velu-ui/components/ErrorCard.jsx +138 -138
  16. package/runtime/velu-ui/components/Field.jsx +81 -81
  17. package/runtime/velu-ui/components/Image.jsx +163 -163
  18. package/runtime/velu-ui/components/Logo.jsx +31 -0
  19. package/runtime/velu-ui/components/MethodBadge.jsx +31 -31
  20. package/runtime/velu-ui/components/NavSelect.jsx +108 -108
  21. package/runtime/velu-ui/components/PageFeedback.jsx +219 -219
  22. package/runtime/velu-ui/components/PageFooter.jsx +145 -213
  23. package/runtime/velu-ui/components/PageHeader.jsx +422 -414
  24. package/runtime/velu-ui/components/PageNav.jsx +77 -77
  25. package/runtime/velu-ui/components/PoweredBy.jsx +51 -51
  26. package/runtime/velu-ui/components/Prompt.jsx +115 -115
  27. package/runtime/velu-ui/components/Search.jsx +411 -366
  28. package/runtime/velu-ui/components/Sidebar.jsx +191 -191
  29. package/runtime/velu-ui/components/SocialLinks.jsx +90 -0
  30. package/runtime/velu-ui/components/Steps.jsx +65 -65
  31. package/runtime/velu-ui/components/ThemeToggle.jsx +48 -48
  32. package/runtime/velu-ui/components/Toc.jsx +537 -537
  33. package/runtime/velu-ui/components/TocBar.jsx +195 -195
  34. package/runtime/velu-ui/components/Tree.jsx +87 -87
  35. package/runtime/velu-ui/components/TryItBar.jsx +90 -90
  36. package/runtime/velu-ui/components/accordion.css +92 -92
  37. package/runtime/velu-ui/components/api.css +479 -479
  38. package/runtime/velu-ui/components/ask-bar.css +94 -94
  39. package/runtime/velu-ui/components/card.css +105 -105
  40. package/runtime/velu-ui/components/chatbot.css +617 -617
  41. package/runtime/velu-ui/components/code-block.css +263 -263
  42. package/runtime/velu-ui/components/docs-layout.css +784 -775
  43. package/runtime/velu-ui/components/field.css +82 -82
  44. package/runtime/velu-ui/components/image.css +237 -237
  45. package/runtime/velu-ui/components/nav-select.css +157 -157
  46. package/runtime/velu-ui/components/page-feedback.css +241 -241
  47. package/runtime/velu-ui/components/page-footer.css +130 -130
  48. package/runtime/velu-ui/components/page-header.css +557 -520
  49. package/runtime/velu-ui/components/page-nav.css +50 -50
  50. package/runtime/velu-ui/components/powered-by.css +86 -66
  51. package/runtime/velu-ui/components/prompt.css +99 -99
  52. package/runtime/velu-ui/components/search.css +307 -307
  53. package/runtime/velu-ui/components/sidebar.css +144 -144
  54. package/runtime/velu-ui/components/steps.css +77 -77
  55. package/runtime/velu-ui/components/theme-toggle.css +101 -101
  56. package/runtime/velu-ui/components/toc-bar.css +234 -234
  57. package/runtime/velu-ui/components/tree.css +49 -49
  58. package/runtime/velu-ui/index.js +48 -46
  59. package/runtime/velu-ui/lib/component-schemas.js +100 -100
  60. package/runtime/velu-ui/lib/copyText.js +64 -64
  61. package/runtime/velu-ui/lib/lang-icons.jsx +147 -147
  62. package/runtime/velu-ui/lib/prism-langs.js +957 -957
  63. package/runtime/velu-ui/lib/prism-loader.js +74 -74
  64. package/runtime/velu-ui/lib/resolveIcon.jsx +29 -29
  65. package/runtime/velu-ui/lib/scrollIntoNearestView.js +66 -66
  66. package/runtime/velu-ui/mdx-components.jsx +105 -105
  67. package/runtime/velu-ui/primitives/Cluster.jsx +49 -49
  68. package/runtime/velu-ui/primitives/Stack.jsx +63 -63
  69. package/runtime/velu-ui/primitives/Switcher.jsx +57 -57
  70. package/runtime/velu-ui/primitives/stack.css +3 -3
  71. package/runtime/velu-ui/primitives/switcher.css +25 -25
  72. package/runtime/velu-ui/styles.css +43 -43
  73. package/runtime/velu-ui/tokens.css +4 -4
  74. package/schema/velu.schema.json +281 -167
  75. package/src/lib/extract-mdx-error.js +170 -170
  76. package/src/lib/issues.js +159 -159
  77. package/src/lib/known-components.js +34 -34
  78. package/src/navigation.js +434 -434
  79. package/src/runtime/App.jsx +1506 -1476
  80. package/src/runtime/ErrorBoundary.jsx +54 -54
  81. package/src/runtime/client-entry.jsx +22 -22
  82. package/src/runtime/server-entry.jsx +16 -16
  83. package/src/template.html +48 -48
  84. package/templates/starter/ai-tools/claude-code.mdx +26 -26
  85. package/templates/starter/ai-tools/cursor.mdx +17 -17
  86. package/templates/starter/api-reference/endpoint/create.mdx +24 -24
  87. package/templates/starter/api-reference/endpoint/get.mdx +27 -27
  88. package/templates/starter/api-reference/introduction.mdx +28 -28
  89. package/templates/starter/development.mdx +19 -19
  90. package/templates/starter/essentials/code.mdx +29 -29
  91. package/templates/starter/essentials/images.mdx +29 -29
  92. package/templates/starter/essentials/markdown.mdx +25 -25
  93. package/templates/starter/essentials/navigation.mdx +39 -39
  94. package/templates/starter/essentials/settings.mdx +30 -30
  95. package/templates/starter/favicon.svg +6 -6
  96. package/templates/starter/index.mdx +31 -31
  97. package/templates/starter/quickstart.mdx +31 -31
  98. package/templates/starter/velu.json +33 -33
@@ -1,122 +1,122 @@
1
- import React from 'react';
2
- import Stack from '../primitives/Stack.jsx';
3
- import resolveIcon from '../lib/resolveIcon.jsx';
4
- import MethodBadge from './MethodBadge.jsx';
5
-
6
- /**
7
- * ApiSidebar — navigation for API reference pages. Like <Sidebar> but
8
- * flat (no nested folders): every entry is a single endpoint shown as
9
- * a method pill + name. Entries are organized into groups, each with
10
- * an optional icon.
11
- *
12
- * <ApiSidebar
13
- * activeHref="/api/trigger"
14
- * linkComponent={RouterLink}
15
- * sections={[
16
- * {
17
- * title: 'Admin',
18
- * icon: 'rocket',
19
- * endpoints: [
20
- * { method: 'POST', label: 'Trigger', href: '/api/trigger' },
21
- * { method: 'GET', label: 'Get deployment-status',
22
- * href: '/api/status' },
23
- * { method: 'DELETE', label: 'Trigger Preview deployment',
24
- * href: '/api/preview' },
25
- * ],
26
- * },
27
- * ]}
28
- * />
29
- *
30
- * Vertical rhythm = the Stack primitive (DRY, same as <Sidebar>);
31
- * api.css holds only the rail / active state / row layout / icon size.
32
- * Selection is route-driven (`endpoint.href === activeHref`, or
33
- * `endpoint.active: true`). Router-agnostic via `linkComponent`.
34
- *
35
- * @typedef {Object} ApiEndpoint
36
- * @property {string} method HTTP method — drives the MethodBadge color
37
- * @property {string} label endpoint display name
38
- * @property {string} [href]
39
- * @property {boolean} [active]
40
- *
41
- * @param {{ sections: { title?: string, icon?: string|React.ReactNode,
42
- * endpoints: ApiEndpoint[] }[], activeHref?: string,
43
- * linkComponent?: React.ElementType, className?: string }} props
44
- */
45
-
46
- const Ctx = React.createContext({ activeHref: undefined, Link: 'a' });
47
-
48
- function Icon({ icon }) {
49
- const node = resolveIcon(icon);
50
- return node ? (
51
- <span className="velu-api-sidebar__icon" aria-hidden="true">
52
- {node}
53
- </span>
54
- ) : null;
55
- }
56
-
57
- function Endpoint({ endpoint }) {
58
- const { activeHref, Link } = React.useContext(Ctx);
59
- const { method = 'GET', label, href = '#', active } = endpoint;
60
- const isActive = Boolean(
61
- active || (href != null && href === activeHref),
62
- );
63
- const cls = `velu-api-sidebar__item${
64
- isActive ? ' velu-api-sidebar__item--active' : ''
65
- }`;
66
- return (
67
- <li>
68
- <Link
69
- className={cls}
70
- href={href}
71
- aria-current={isActive ? 'page' : undefined}
72
- >
73
- <MethodBadge method={method} />
74
- <span className="velu-api-sidebar__label">{label}</span>
75
- </Link>
76
- </li>
77
- );
78
- }
79
-
80
- export default function ApiSidebar({
81
- sections = [],
82
- activeHref,
83
- linkComponent = 'a',
84
- className = '',
85
- ...rest
86
- }) {
87
- const ctx = React.useMemo(
88
- () => ({ activeHref, Link: linkComponent }),
89
- [activeHref, linkComponent],
90
- );
91
- return (
92
- <Ctx.Provider value={ctx}>
93
- <Stack
94
- as="nav"
95
- space="var(--s1)"
96
- className={`velu-api-sidebar ${className}`.trim()}
97
- aria-label="API reference"
98
- {...rest}
99
- >
100
- {sections.map((section, i) => (
101
- <React.Fragment key={i}>
102
- {section.title != null && (
103
- <h5 className="velu-api-sidebar__section">
104
- <Icon icon={section.icon} />
105
- {section.title}
106
- </h5>
107
- )}
108
- <Stack
109
- as="ul"
110
- space="var(--s0)"
111
- className="velu-api-sidebar__list"
112
- >
113
- {(section.endpoints ?? []).map((ep, j) => (
114
- <Endpoint key={j} endpoint={ep} />
115
- ))}
116
- </Stack>
117
- </React.Fragment>
118
- ))}
119
- </Stack>
120
- </Ctx.Provider>
121
- );
122
- }
1
+ import React from 'react';
2
+ import Stack from '../primitives/Stack.jsx';
3
+ import resolveIcon from '../lib/resolveIcon.jsx';
4
+ import MethodBadge from './MethodBadge.jsx';
5
+
6
+ /**
7
+ * ApiSidebar — navigation for API reference pages. Like <Sidebar> but
8
+ * flat (no nested folders): every entry is a single endpoint shown as
9
+ * a method pill + name. Entries are organized into groups, each with
10
+ * an optional icon.
11
+ *
12
+ * <ApiSidebar
13
+ * activeHref="/api/trigger"
14
+ * linkComponent={RouterLink}
15
+ * sections={[
16
+ * {
17
+ * title: 'Admin',
18
+ * icon: 'rocket',
19
+ * endpoints: [
20
+ * { method: 'POST', label: 'Trigger', href: '/api/trigger' },
21
+ * { method: 'GET', label: 'Get deployment-status',
22
+ * href: '/api/status' },
23
+ * { method: 'DELETE', label: 'Trigger Preview deployment',
24
+ * href: '/api/preview' },
25
+ * ],
26
+ * },
27
+ * ]}
28
+ * />
29
+ *
30
+ * Vertical rhythm = the Stack primitive (DRY, same as <Sidebar>);
31
+ * api.css holds only the rail / active state / row layout / icon size.
32
+ * Selection is route-driven (`endpoint.href === activeHref`, or
33
+ * `endpoint.active: true`). Router-agnostic via `linkComponent`.
34
+ *
35
+ * @typedef {Object} ApiEndpoint
36
+ * @property {string} method HTTP method — drives the MethodBadge color
37
+ * @property {string} label endpoint display name
38
+ * @property {string} [href]
39
+ * @property {boolean} [active]
40
+ *
41
+ * @param {{ sections: { title?: string, icon?: string|React.ReactNode,
42
+ * endpoints: ApiEndpoint[] }[], activeHref?: string,
43
+ * linkComponent?: React.ElementType, className?: string }} props
44
+ */
45
+
46
+ const Ctx = React.createContext({ activeHref: undefined, Link: 'a' });
47
+
48
+ function Icon({ icon }) {
49
+ const node = resolveIcon(icon);
50
+ return node ? (
51
+ <span className="velu-api-sidebar__icon" aria-hidden="true">
52
+ {node}
53
+ </span>
54
+ ) : null;
55
+ }
56
+
57
+ function Endpoint({ endpoint }) {
58
+ const { activeHref, Link } = React.useContext(Ctx);
59
+ const { method = 'GET', label, href = '#', active } = endpoint;
60
+ const isActive = Boolean(
61
+ active || (href != null && href === activeHref),
62
+ );
63
+ const cls = `velu-api-sidebar__item${
64
+ isActive ? ' velu-api-sidebar__item--active' : ''
65
+ }`;
66
+ return (
67
+ <li>
68
+ <Link
69
+ className={cls}
70
+ href={href}
71
+ aria-current={isActive ? 'page' : undefined}
72
+ >
73
+ <MethodBadge method={method} />
74
+ <span className="velu-api-sidebar__label">{label}</span>
75
+ </Link>
76
+ </li>
77
+ );
78
+ }
79
+
80
+ export default function ApiSidebar({
81
+ sections = [],
82
+ activeHref,
83
+ linkComponent = 'a',
84
+ className = '',
85
+ ...rest
86
+ }) {
87
+ const ctx = React.useMemo(
88
+ () => ({ activeHref, Link: linkComponent }),
89
+ [activeHref, linkComponent],
90
+ );
91
+ return (
92
+ <Ctx.Provider value={ctx}>
93
+ <Stack
94
+ as="nav"
95
+ space="var(--s1)"
96
+ className={`velu-api-sidebar ${className}`.trim()}
97
+ aria-label="API reference"
98
+ {...rest}
99
+ >
100
+ {sections.map((section, i) => (
101
+ <React.Fragment key={i}>
102
+ {section.title != null && (
103
+ <h5 className="velu-api-sidebar__section">
104
+ <Icon icon={section.icon} />
105
+ {section.title}
106
+ </h5>
107
+ )}
108
+ <Stack
109
+ as="ul"
110
+ space="var(--s0)"
111
+ className="velu-api-sidebar__list"
112
+ >
113
+ {(section.endpoints ?? []).map((ep, j) => (
114
+ <Endpoint key={j} endpoint={ep} />
115
+ ))}
116
+ </Stack>
117
+ </React.Fragment>
118
+ ))}
119
+ </Stack>
120
+ </Ctx.Provider>
121
+ );
122
+ }
@@ -1,71 +1,71 @@
1
- import React, { useState } from 'react';
2
- import resolveIcon from '../lib/resolveIcon.jsx';
3
-
4
- /**
5
- * AskBar — an "ask a question" input card.
6
- *
7
- * <AskBar onSubmit={(q) => …} />
8
- *
9
- * The component owns only the card visuals (border, glow-on-focus,
10
- * multi-line input, circular send button). PLACEMENT is the consumer's
11
- * job — the docs page pins it `position: absolute` to the bottom of
12
- * the article column so content scrolls behind its opaque surface.
13
- *
14
- * - `placeholder` input placeholder (default "Ask a question…")
15
- * - `onSubmit(value)` fired on the send button or Enter (Shift+Enter
16
- * inserts a newline). Dummy-friendly — omit it and the bar is inert.
17
- *
18
- * The send affordance is a circular accent button with an up-arrow;
19
- * the card border glows in the accent color while the input is focused.
20
- */
21
-
22
- export default function AskBar({
23
- placeholder = 'Ask a question…',
24
- onSubmit,
25
- className = '',
26
- ...rest
27
- }) {
28
- const [value, setValue] = useState('');
29
-
30
- function submit() {
31
- const q = value.trim();
32
- if (!q) return;
33
- onSubmit?.(q);
34
- setValue('');
35
- }
36
-
37
- function onKeyDown(e) {
38
- // Enter sends; Shift+Enter inserts a newline.
39
- if (e.key === 'Enter' && !e.shiftKey) {
40
- e.preventDefault();
41
- submit();
42
- }
43
- }
44
-
45
- // Disable the send button until there's non-whitespace input. The
46
- // CSS reads :disabled to render it muted + un-hoverable; React reads
47
- // the attribute to short-circuit clicks too.
48
- const isEmpty = value.trim().length === 0;
49
-
50
- return (
51
- <div className={`velu-ask-bar ${className}`.trim()} {...rest}>
52
- <textarea
53
- className="velu-ask-bar__input"
54
- placeholder={placeholder}
55
- value={value}
56
- onChange={(e) => setValue(e.target.value)}
57
- onKeyDown={onKeyDown}
58
- rows={2}
59
- />
60
- <button
61
- type="button"
62
- className="velu-ask-bar__send"
63
- onClick={submit}
64
- disabled={isEmpty}
65
- aria-label="Ask"
66
- >
67
- {resolveIcon('arrow-up', { size: '1em' })}
68
- </button>
69
- </div>
70
- );
71
- }
1
+ import React, { useState } from 'react';
2
+ import resolveIcon from '../lib/resolveIcon.jsx';
3
+
4
+ /**
5
+ * AskBar — an "ask a question" input card.
6
+ *
7
+ * <AskBar onSubmit={(q) => …} />
8
+ *
9
+ * The component owns only the card visuals (border, glow-on-focus,
10
+ * multi-line input, circular send button). PLACEMENT is the consumer's
11
+ * job — the docs page pins it `position: absolute` to the bottom of
12
+ * the article column so content scrolls behind its opaque surface.
13
+ *
14
+ * - `placeholder` input placeholder (default "Ask a question…")
15
+ * - `onSubmit(value)` fired on the send button or Enter (Shift+Enter
16
+ * inserts a newline). Dummy-friendly — omit it and the bar is inert.
17
+ *
18
+ * The send affordance is a circular accent button with an up-arrow;
19
+ * the card border glows in the accent color while the input is focused.
20
+ */
21
+
22
+ export default function AskBar({
23
+ placeholder = 'Ask a question…',
24
+ onSubmit,
25
+ className = '',
26
+ ...rest
27
+ }) {
28
+ const [value, setValue] = useState('');
29
+
30
+ function submit() {
31
+ const q = value.trim();
32
+ if (!q) return;
33
+ onSubmit?.(q);
34
+ setValue('');
35
+ }
36
+
37
+ function onKeyDown(e) {
38
+ // Enter sends; Shift+Enter inserts a newline.
39
+ if (e.key === 'Enter' && !e.shiftKey) {
40
+ e.preventDefault();
41
+ submit();
42
+ }
43
+ }
44
+
45
+ // Disable the send button until there's non-whitespace input. The
46
+ // CSS reads :disabled to render it muted + un-hoverable; React reads
47
+ // the attribute to short-circuit clicks too.
48
+ const isEmpty = value.trim().length === 0;
49
+
50
+ return (
51
+ <div className={`velu-ask-bar ${className}`.trim()} {...rest}>
52
+ <textarea
53
+ className="velu-ask-bar__input"
54
+ placeholder={placeholder}
55
+ value={value}
56
+ onChange={(e) => setValue(e.target.value)}
57
+ onKeyDown={onKeyDown}
58
+ rows={2}
59
+ />
60
+ <button
61
+ type="button"
62
+ className="velu-ask-bar__send"
63
+ onClick={submit}
64
+ disabled={isEmpty}
65
+ aria-label="Ask"
66
+ >
67
+ {resolveIcon('arrow-up', { size: '1em' })}
68
+ </button>
69
+ </div>
70
+ );
71
+ }
@@ -1,114 +1,114 @@
1
- import React from 'react';
2
- import resolveIcon from '../lib/resolveIcon.jsx';
3
-
4
- /**
5
- * Callout — admonition box (note/warning/info/tip/check/danger/callout).
6
- *
7
- * Fully tokenized: each `type` maps to its Figma bg + stroke token pair
8
- * (base.css, light + dark), so theming is automatic via [data-theme]. Border
9
- * + icon use the stroke; surface uses the bg; body uses --text-color.
10
- * Spacing/radius from the modular scale.
11
- *
12
- * Layout: the icon is FLOATED into the text flow (not a fixed column) — it
13
- * sits on the first line and the text wraps beside it, then subsequent/wrapped
14
- * lines flow back UNDER the icon (no hanging indent). `display: flow-root`
15
- * contains the float cleanly (no clipping, no clearfix hack).
16
- *
17
- * Content-driven & extensible: `icon` is a **lucide icon id string**
18
- * (kebab-case), resolved by the shared resolveIcon util (also accepts a
19
- * node). `stroke` / `bg` override the variant colors for custom callouts.
20
- *
21
- * SSR-safe & presentational (no effects/refs).
22
- *
23
- * @param {{ type?: 'note'|'warning'|'info'|'tip'|'check'|'danger'|'callout',
24
- * icon?: string|React.ReactNode, stroke?: string, bg?: string,
25
- * children: React.ReactNode, className?: string }} props
26
- */
27
-
28
- // Variant icon defaults are lucide IDS (not components) → one resolution path.
29
- const VARIANTS = {
30
- note: { bg: '--note-bg-color', stroke: '--note-stroke-color', icon: 'pencil' },
31
- warning: {
32
- bg: '--warning-bg-color',
33
- stroke: '--warning-stroke-color',
34
- icon: 'triangle-alert',
35
- },
36
- info: { bg: '--info-bg-color', stroke: '--info-stroke-color', icon: 'info' },
37
- tip: {
38
- bg: '--tip-bg-color',
39
- stroke: '--tip-stroke-color',
40
- icon: 'lightbulb',
41
- },
42
- check: {
43
- bg: '--check-bg-color',
44
- stroke: '--check-stroke-color',
45
- icon: 'circle-check',
46
- },
47
- danger: {
48
- bg: '--danger-bg-color',
49
- stroke: '--danger-stroke-color',
50
- icon: 'shield-alert',
51
- },
52
- callout: {
53
- bg: '--callout-bg-color',
54
- stroke: '--callout-stroke-color',
55
- icon: 'message-circle',
56
- },
57
- };
58
-
59
- export default function Callout({
60
- type = 'note',
61
- icon,
62
- stroke,
63
- bg,
64
- children,
65
- className = '',
66
- style,
67
- ...rest
68
- }) {
69
- const v = VARIANTS[type] || VARIANTS.note;
70
- const strokeColor = stroke ?? `var(${v.stroke})`;
71
- const bgColor = bg ?? `var(${v.bg})`;
72
- // size only; stroke weight comes from the global --icon-stroke token
73
- // (.lucide rule in base.css), not set per-icon.
74
- const iconNode = resolveIcon(icon ?? v.icon, { size: '1em' });
75
-
76
- return (
77
- <div
78
- className={className}
79
- style={{
80
- display: 'flow-root', // contains the floated icon, no clipping
81
- padding: 'var(--s0) var(--s3)',
82
- background: bgColor,
83
- border: `var(--border-width) solid ${strokeColor}`,
84
- borderRadius: 'var(--radius-sm)',
85
- color: 'var(--text-color)',
86
- fontSize: 'var(--f-body)',
87
- lineHeight: 'var(--lh-body)',
88
- ...style,
89
- }}
90
- {...rest}
91
- >
92
- {iconNode && (
93
- <span
94
- aria-hidden="true"
95
- style={{
96
- float: 'left',
97
- display: 'inline-flex',
98
- alignItems: 'center',
99
- // one line-box tall (body font-size × body line-height token) so
100
- // the icon is centered on the FIRST line; text wraps beside it
101
- // then flows back under it (no hanging indent).
102
- blockSize: 'calc(var(--f-body) * var(--lh-body))',
103
- marginInlineEnd: 'var(--s-1)',
104
- color: strokeColor,
105
- fontSize: 'var(--f-body)',
106
- }}
107
- >
108
- {iconNode}
109
- </span>
110
- )}
111
- {children}
112
- </div>
113
- );
114
- }
1
+ import React from 'react';
2
+ import resolveIcon from '../lib/resolveIcon.jsx';
3
+
4
+ /**
5
+ * Callout — admonition box (note/warning/info/tip/check/danger/callout).
6
+ *
7
+ * Fully tokenized: each `type` maps to its Figma bg + stroke token pair
8
+ * (base.css, light + dark), so theming is automatic via [data-theme]. Border
9
+ * + icon use the stroke; surface uses the bg; body uses --text-color.
10
+ * Spacing/radius from the modular scale.
11
+ *
12
+ * Layout: the icon is FLOATED into the text flow (not a fixed column) — it
13
+ * sits on the first line and the text wraps beside it, then subsequent/wrapped
14
+ * lines flow back UNDER the icon (no hanging indent). `display: flow-root`
15
+ * contains the float cleanly (no clipping, no clearfix hack).
16
+ *
17
+ * Content-driven & extensible: `icon` is a **lucide icon id string**
18
+ * (kebab-case), resolved by the shared resolveIcon util (also accepts a
19
+ * node). `stroke` / `bg` override the variant colors for custom callouts.
20
+ *
21
+ * SSR-safe & presentational (no effects/refs).
22
+ *
23
+ * @param {{ type?: 'note'|'warning'|'info'|'tip'|'check'|'danger'|'callout',
24
+ * icon?: string|React.ReactNode, stroke?: string, bg?: string,
25
+ * children: React.ReactNode, className?: string }} props
26
+ */
27
+
28
+ // Variant icon defaults are lucide IDS (not components) → one resolution path.
29
+ const VARIANTS = {
30
+ note: { bg: '--note-bg-color', stroke: '--note-stroke-color', icon: 'pencil' },
31
+ warning: {
32
+ bg: '--warning-bg-color',
33
+ stroke: '--warning-stroke-color',
34
+ icon: 'triangle-alert',
35
+ },
36
+ info: { bg: '--info-bg-color', stroke: '--info-stroke-color', icon: 'info' },
37
+ tip: {
38
+ bg: '--tip-bg-color',
39
+ stroke: '--tip-stroke-color',
40
+ icon: 'lightbulb',
41
+ },
42
+ check: {
43
+ bg: '--check-bg-color',
44
+ stroke: '--check-stroke-color',
45
+ icon: 'circle-check',
46
+ },
47
+ danger: {
48
+ bg: '--danger-bg-color',
49
+ stroke: '--danger-stroke-color',
50
+ icon: 'shield-alert',
51
+ },
52
+ callout: {
53
+ bg: '--callout-bg-color',
54
+ stroke: '--callout-stroke-color',
55
+ icon: 'message-circle',
56
+ },
57
+ };
58
+
59
+ export default function Callout({
60
+ type = 'note',
61
+ icon,
62
+ stroke,
63
+ bg,
64
+ children,
65
+ className = '',
66
+ style,
67
+ ...rest
68
+ }) {
69
+ const v = VARIANTS[type] || VARIANTS.note;
70
+ const strokeColor = stroke ?? `var(${v.stroke})`;
71
+ const bgColor = bg ?? `var(${v.bg})`;
72
+ // size only; stroke weight comes from the global --icon-stroke token
73
+ // (.lucide rule in base.css), not set per-icon.
74
+ const iconNode = resolveIcon(icon ?? v.icon, { size: '1em' });
75
+
76
+ return (
77
+ <div
78
+ className={className}
79
+ style={{
80
+ display: 'flow-root', // contains the floated icon, no clipping
81
+ padding: 'var(--s0) var(--s3)',
82
+ background: bgColor,
83
+ border: `var(--border-width) solid ${strokeColor}`,
84
+ borderRadius: 'var(--radius-sm)',
85
+ color: 'var(--text-color)',
86
+ fontSize: 'var(--f-body)',
87
+ lineHeight: 'var(--lh-body)',
88
+ ...style,
89
+ }}
90
+ {...rest}
91
+ >
92
+ {iconNode && (
93
+ <span
94
+ aria-hidden="true"
95
+ style={{
96
+ float: 'left',
97
+ display: 'inline-flex',
98
+ alignItems: 'center',
99
+ // one line-box tall (body font-size × body line-height token) so
100
+ // the icon is centered on the FIRST line; text wraps beside it
101
+ // then flows back under it (no hanging indent).
102
+ blockSize: 'calc(var(--f-body) * var(--lh-body))',
103
+ marginInlineEnd: 'var(--s-1)',
104
+ color: strokeColor,
105
+ fontSize: 'var(--f-body)',
106
+ }}
107
+ >
108
+ {iconNode}
109
+ </span>
110
+ )}
111
+ {children}
112
+ </div>
113
+ );
114
+ }