@veluai/velu 0.2.25 → 0.2.28
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.
- package/README.md +80 -80
- package/dist/cli.js +44 -44
- package/package.json +64 -64
- package/runtime/velu-ui/base.css +320 -320
- package/runtime/velu-ui/components/Accordion.jsx +64 -64
- package/runtime/velu-ui/components/ApiClient.jsx +207 -207
- package/runtime/velu-ui/components/ApiField.jsx +87 -87
- package/runtime/velu-ui/components/ApiPath.jsx +63 -63
- package/runtime/velu-ui/components/ApiReferencePage.jsx +384 -384
- package/runtime/velu-ui/components/ApiSamples.jsx +36 -36
- package/runtime/velu-ui/components/ApiSidebar.jsx +125 -125
- package/runtime/velu-ui/components/AskBar.jsx +71 -71
- package/runtime/velu-ui/components/Callout.jsx +114 -114
- package/runtime/velu-ui/components/Card.jsx +131 -131
- package/runtime/velu-ui/components/ChangelogFilters.jsx +75 -75
- package/runtime/velu-ui/components/Chatbot.jsx +972 -972
- package/runtime/velu-ui/components/CodeBlock.jsx +375 -375
- package/runtime/velu-ui/components/Columns.jsx +56 -56
- package/runtime/velu-ui/components/ContextMenu.jsx +327 -327
- package/runtime/velu-ui/components/ErrorCard.jsx +138 -138
- package/runtime/velu-ui/components/Field.jsx +81 -81
- package/runtime/velu-ui/components/Image.jsx +163 -163
- package/runtime/velu-ui/components/Logo.jsx +31 -31
- package/runtime/velu-ui/components/MethodBadge.jsx +31 -31
- package/runtime/velu-ui/components/NavSelect.jsx +108 -108
- package/runtime/velu-ui/components/NotFound.jsx +63 -63
- package/runtime/velu-ui/components/PageFeedback.jsx +219 -219
- package/runtime/velu-ui/components/PageFooter.jsx +145 -145
- package/runtime/velu-ui/components/PageHeader.jsx +422 -422
- package/runtime/velu-ui/components/PageNav.jsx +77 -77
- package/runtime/velu-ui/components/PoweredBy.jsx +51 -51
- package/runtime/velu-ui/components/Prompt.jsx +115 -115
- package/runtime/velu-ui/components/Search.jsx +460 -460
- package/runtime/velu-ui/components/Sidebar.jsx +246 -246
- package/runtime/velu-ui/components/SocialLinks.jsx +90 -90
- package/runtime/velu-ui/components/Steps.jsx +65 -65
- package/runtime/velu-ui/components/ThemeToggle.jsx +48 -48
- package/runtime/velu-ui/components/Toc.jsx +537 -537
- package/runtime/velu-ui/components/TocBar.jsx +195 -195
- package/runtime/velu-ui/components/Tree.jsx +87 -87
- package/runtime/velu-ui/components/TryItBar.jsx +102 -102
- package/runtime/velu-ui/components/Update.jsx +92 -92
- package/runtime/velu-ui/components/accordion.css +92 -92
- package/runtime/velu-ui/components/api-page.css +208 -208
- package/runtime/velu-ui/components/api.css +635 -635
- package/runtime/velu-ui/components/ask-bar.css +103 -103
- package/runtime/velu-ui/components/card.css +105 -105
- package/runtime/velu-ui/components/changelog-filters.css +43 -43
- package/runtime/velu-ui/components/chatbot.css +687 -687
- package/runtime/velu-ui/components/code-block.css +263 -263
- package/runtime/velu-ui/components/context-menu.css +199 -199
- package/runtime/velu-ui/components/docs-layout.css +822 -822
- package/runtime/velu-ui/components/field.css +82 -82
- package/runtime/velu-ui/components/image.css +237 -237
- package/runtime/velu-ui/components/nav-select.css +157 -157
- package/runtime/velu-ui/components/not-found.css +94 -94
- package/runtime/velu-ui/components/page-feedback.css +261 -261
- package/runtime/velu-ui/components/page-footer.css +130 -130
- package/runtime/velu-ui/components/page-header.css +558 -558
- package/runtime/velu-ui/components/page-nav.css +50 -50
- package/runtime/velu-ui/components/powered-by.css +92 -92
- package/runtime/velu-ui/components/prompt.css +99 -99
- package/runtime/velu-ui/components/search.css +307 -307
- package/runtime/velu-ui/components/sidebar.css +205 -205
- package/runtime/velu-ui/components/steps.css +77 -77
- package/runtime/velu-ui/components/theme-toggle.css +102 -102
- package/runtime/velu-ui/components/toc-bar.css +234 -234
- package/runtime/velu-ui/components/tree.css +49 -49
- package/runtime/velu-ui/components/update.css +111 -111
- package/runtime/velu-ui/index.js +58 -58
- package/runtime/velu-ui/lib/api-send.js +118 -118
- package/runtime/velu-ui/lib/brand-icons.jsx +103 -103
- package/runtime/velu-ui/lib/component-schemas.js +100 -100
- package/runtime/velu-ui/lib/copyText.js +64 -64
- package/runtime/velu-ui/lib/docs-assistant.js +273 -273
- package/runtime/velu-ui/lib/lang-icons.jsx +147 -147
- package/runtime/velu-ui/lib/page-feedback.js +72 -72
- package/runtime/velu-ui/lib/pagefind.js +116 -116
- package/runtime/velu-ui/lib/prism-langs.js +957 -957
- package/runtime/velu-ui/lib/prism-loader.js +74 -74
- package/runtime/velu-ui/lib/resolveIcon.jsx +29 -29
- package/runtime/velu-ui/lib/scrollIntoNearestView.js +66 -66
- package/runtime/velu-ui/mdx-components.jsx +107 -107
- package/runtime/velu-ui/primitives/Cluster.jsx +49 -49
- package/runtime/velu-ui/primitives/Stack.jsx +63 -63
- package/runtime/velu-ui/primitives/Switcher.jsx +57 -57
- package/runtime/velu-ui/primitives/stack.css +3 -3
- package/runtime/velu-ui/primitives/switcher.css +25 -25
- package/runtime/velu-ui/styles.css +48 -48
- package/runtime/velu-ui/tokens.css +4 -4
- package/schema/velu.schema.json +419 -419
- package/src/lib/extract-mdx-error.js +170 -170
- package/src/lib/issues.js +159 -159
- package/src/lib/known-components.js +35 -35
- package/src/navigation.js +459 -459
- package/src/runtime/App.jsx +1754 -1754
- package/src/runtime/ErrorBoundary.jsx +54 -54
- package/src/runtime/client-entry.jsx +27 -27
- package/src/runtime/server-entry.jsx +24 -24
- package/src/template.html +48 -48
- package/templates/starter/ai-tools/claude-code.mdx +26 -26
- package/templates/starter/ai-tools/cursor.mdx +17 -17
- package/templates/starter/api-reference/introduction.mdx +43 -43
- package/templates/starter/development.mdx +19 -19
- package/templates/starter/essentials/code.mdx +29 -29
- package/templates/starter/essentials/images.mdx +29 -29
- package/templates/starter/essentials/markdown.mdx +25 -25
- package/templates/starter/essentials/navigation.mdx +39 -39
- package/templates/starter/essentials/settings.mdx +30 -30
- package/templates/starter/favicon.svg +6 -6
- package/templates/starter/index.mdx +31 -31
- package/templates/starter/openapi.json +160 -160
- package/templates/starter/quickstart.mdx +31 -31
- package/templates/starter/velu.json +40 -40
|
@@ -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
|
+
}
|
|
@@ -1,131 +1,131 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import resolveIcon from '../lib/resolveIcon.jsx';
|
|
3
|
-
import Switcher from '../primitives/Switcher.jsx';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Card + CardGroup — content / horizontal / image / CTA cards.
|
|
7
|
-
*
|
|
8
|
-
* The card itself is NEVER a link and has NO hover state. The only
|
|
9
|
-
* interactive element is the CTA: pass `cta={{ label, href }}` to render a
|
|
10
|
-
* single "Click Here ›" link. Everything else is static presentation.
|
|
11
|
-
*
|
|
12
|
-
* Fully tokenized (border-width, border-color, radius scale, spacing, type,
|
|
13
|
-
* line-height, accent, icon-stroke) → light/dark automatic via [data-theme].
|
|
14
|
-
* SSR-safe & presentational.
|
|
15
|
-
*
|
|
16
|
-
* Variants (combinable):
|
|
17
|
-
* - basic: icon (accent, --f-h2) + title (--f-h4) + body
|
|
18
|
-
* - `horizontal`: icon vertically centered on the left, title+body right
|
|
19
|
-
* - `img`: image fills the top, clipped to the radius
|
|
20
|
-
* - `cta`: { label, href } → the single clickable "Click Here ›" link
|
|
21
|
-
*
|
|
22
|
-
* `icon` is a lucide id string (shared resolveIcon).
|
|
23
|
-
*
|
|
24
|
-
* @param {{ title?: React.ReactNode, icon?: string, horizontal?: boolean,
|
|
25
|
-
* img?: string, imgAlt?: string,
|
|
26
|
-
* cta?: {label: React.ReactNode, href: string},
|
|
27
|
-
* children?: React.ReactNode, className?: string }} props
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
export function Card({
|
|
31
|
-
title,
|
|
32
|
-
icon,
|
|
33
|
-
horizontal = false,
|
|
34
|
-
img,
|
|
35
|
-
imgAlt = '',
|
|
36
|
-
cta,
|
|
37
|
-
children,
|
|
38
|
-
className = '',
|
|
39
|
-
...rest
|
|
40
|
-
}) {
|
|
41
|
-
const cls = [
|
|
42
|
-
'velu-card',
|
|
43
|
-
horizontal && 'velu-card--horizontal',
|
|
44
|
-
img && 'velu-card--image',
|
|
45
|
-
cta && 'velu-card--cta',
|
|
46
|
-
className,
|
|
47
|
-
]
|
|
48
|
-
.filter(Boolean)
|
|
49
|
-
.join(' ');
|
|
50
|
-
|
|
51
|
-
const iconEl = icon ? (
|
|
52
|
-
<span className="velu-card__icon" aria-hidden="true">
|
|
53
|
-
{resolveIcon(icon, { size: '1em' })}
|
|
54
|
-
</span>
|
|
55
|
-
) : null;
|
|
56
|
-
|
|
57
|
-
const ctaEl = cta ? (
|
|
58
|
-
<a className="velu-card__cta" href={cta.href}>
|
|
59
|
-
{cta.label}
|
|
60
|
-
<span className="velu-card__cta-icon" aria-hidden="true">
|
|
61
|
-
{resolveIcon('chevron-right', { size: '1em' })}
|
|
62
|
-
</span>
|
|
63
|
-
</a>
|
|
64
|
-
) : null;
|
|
65
|
-
|
|
66
|
-
const titleEl = title ? (
|
|
67
|
-
<div className="velu-card__title">{title}</div>
|
|
68
|
-
) : null;
|
|
69
|
-
const textEl = children ? (
|
|
70
|
-
<div className="velu-card__text">{children}</div>
|
|
71
|
-
) : null;
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<div className={cls} {...rest}>
|
|
75
|
-
{img && (
|
|
76
|
-
<div className="velu-card__media">
|
|
77
|
-
<img src={img} alt={imgAlt} loading="lazy" />
|
|
78
|
-
</div>
|
|
79
|
-
)}
|
|
80
|
-
|
|
81
|
-
<div className="velu-card__body">
|
|
82
|
-
{horizontal ? (
|
|
83
|
-
<>
|
|
84
|
-
{iconEl}
|
|
85
|
-
<div className="velu-card__content">
|
|
86
|
-
{titleEl}
|
|
87
|
-
{textEl}
|
|
88
|
-
{ctaEl}
|
|
89
|
-
</div>
|
|
90
|
-
</>
|
|
91
|
-
) : (
|
|
92
|
-
<>
|
|
93
|
-
{!img ? iconEl : null}
|
|
94
|
-
{titleEl}
|
|
95
|
-
{textEl}
|
|
96
|
-
{ctaEl}
|
|
97
|
-
</>
|
|
98
|
-
)}
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* CardGroup — a row of Cards that switches to a FULL vertical stack the
|
|
106
|
-
* moment any one card would fall below the per-card minimum. Thin wrapper
|
|
107
|
-
* over the Switcher primitive: just supplies `min` (Switcher does the
|
|
108
|
-
* count-aware threshold math). `limit` hard-caps items per row.
|
|
109
|
-
*/
|
|
110
|
-
export function CardGroup({
|
|
111
|
-
space = 'var(--s0)',
|
|
112
|
-
min = 'var(--card-min, 18ch)',
|
|
113
|
-
limit = 3,
|
|
114
|
-
children,
|
|
115
|
-
className = '',
|
|
116
|
-
...rest
|
|
117
|
-
}) {
|
|
118
|
-
return (
|
|
119
|
-
<Switcher
|
|
120
|
-
className={`velu-card-group ${className}`.trim()}
|
|
121
|
-
space={space}
|
|
122
|
-
min={min}
|
|
123
|
-
limit={limit}
|
|
124
|
-
{...rest}
|
|
125
|
-
>
|
|
126
|
-
{children}
|
|
127
|
-
</Switcher>
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export default Card;
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import resolveIcon from '../lib/resolveIcon.jsx';
|
|
3
|
+
import Switcher from '../primitives/Switcher.jsx';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Card + CardGroup — content / horizontal / image / CTA cards.
|
|
7
|
+
*
|
|
8
|
+
* The card itself is NEVER a link and has NO hover state. The only
|
|
9
|
+
* interactive element is the CTA: pass `cta={{ label, href }}` to render a
|
|
10
|
+
* single "Click Here ›" link. Everything else is static presentation.
|
|
11
|
+
*
|
|
12
|
+
* Fully tokenized (border-width, border-color, radius scale, spacing, type,
|
|
13
|
+
* line-height, accent, icon-stroke) → light/dark automatic via [data-theme].
|
|
14
|
+
* SSR-safe & presentational.
|
|
15
|
+
*
|
|
16
|
+
* Variants (combinable):
|
|
17
|
+
* - basic: icon (accent, --f-h2) + title (--f-h4) + body
|
|
18
|
+
* - `horizontal`: icon vertically centered on the left, title+body right
|
|
19
|
+
* - `img`: image fills the top, clipped to the radius
|
|
20
|
+
* - `cta`: { label, href } → the single clickable "Click Here ›" link
|
|
21
|
+
*
|
|
22
|
+
* `icon` is a lucide id string (shared resolveIcon).
|
|
23
|
+
*
|
|
24
|
+
* @param {{ title?: React.ReactNode, icon?: string, horizontal?: boolean,
|
|
25
|
+
* img?: string, imgAlt?: string,
|
|
26
|
+
* cta?: {label: React.ReactNode, href: string},
|
|
27
|
+
* children?: React.ReactNode, className?: string }} props
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export function Card({
|
|
31
|
+
title,
|
|
32
|
+
icon,
|
|
33
|
+
horizontal = false,
|
|
34
|
+
img,
|
|
35
|
+
imgAlt = '',
|
|
36
|
+
cta,
|
|
37
|
+
children,
|
|
38
|
+
className = '',
|
|
39
|
+
...rest
|
|
40
|
+
}) {
|
|
41
|
+
const cls = [
|
|
42
|
+
'velu-card',
|
|
43
|
+
horizontal && 'velu-card--horizontal',
|
|
44
|
+
img && 'velu-card--image',
|
|
45
|
+
cta && 'velu-card--cta',
|
|
46
|
+
className,
|
|
47
|
+
]
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.join(' ');
|
|
50
|
+
|
|
51
|
+
const iconEl = icon ? (
|
|
52
|
+
<span className="velu-card__icon" aria-hidden="true">
|
|
53
|
+
{resolveIcon(icon, { size: '1em' })}
|
|
54
|
+
</span>
|
|
55
|
+
) : null;
|
|
56
|
+
|
|
57
|
+
const ctaEl = cta ? (
|
|
58
|
+
<a className="velu-card__cta" href={cta.href}>
|
|
59
|
+
{cta.label}
|
|
60
|
+
<span className="velu-card__cta-icon" aria-hidden="true">
|
|
61
|
+
{resolveIcon('chevron-right', { size: '1em' })}
|
|
62
|
+
</span>
|
|
63
|
+
</a>
|
|
64
|
+
) : null;
|
|
65
|
+
|
|
66
|
+
const titleEl = title ? (
|
|
67
|
+
<div className="velu-card__title">{title}</div>
|
|
68
|
+
) : null;
|
|
69
|
+
const textEl = children ? (
|
|
70
|
+
<div className="velu-card__text">{children}</div>
|
|
71
|
+
) : null;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className={cls} {...rest}>
|
|
75
|
+
{img && (
|
|
76
|
+
<div className="velu-card__media">
|
|
77
|
+
<img src={img} alt={imgAlt} loading="lazy" />
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
|
|
81
|
+
<div className="velu-card__body">
|
|
82
|
+
{horizontal ? (
|
|
83
|
+
<>
|
|
84
|
+
{iconEl}
|
|
85
|
+
<div className="velu-card__content">
|
|
86
|
+
{titleEl}
|
|
87
|
+
{textEl}
|
|
88
|
+
{ctaEl}
|
|
89
|
+
</div>
|
|
90
|
+
</>
|
|
91
|
+
) : (
|
|
92
|
+
<>
|
|
93
|
+
{!img ? iconEl : null}
|
|
94
|
+
{titleEl}
|
|
95
|
+
{textEl}
|
|
96
|
+
{ctaEl}
|
|
97
|
+
</>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* CardGroup — a row of Cards that switches to a FULL vertical stack the
|
|
106
|
+
* moment any one card would fall below the per-card minimum. Thin wrapper
|
|
107
|
+
* over the Switcher primitive: just supplies `min` (Switcher does the
|
|
108
|
+
* count-aware threshold math). `limit` hard-caps items per row.
|
|
109
|
+
*/
|
|
110
|
+
export function CardGroup({
|
|
111
|
+
space = 'var(--s0)',
|
|
112
|
+
min = 'var(--card-min, 18ch)',
|
|
113
|
+
limit = 3,
|
|
114
|
+
children,
|
|
115
|
+
className = '',
|
|
116
|
+
...rest
|
|
117
|
+
}) {
|
|
118
|
+
return (
|
|
119
|
+
<Switcher
|
|
120
|
+
className={`velu-card-group ${className}`.trim()}
|
|
121
|
+
space={space}
|
|
122
|
+
min={min}
|
|
123
|
+
limit={limit}
|
|
124
|
+
{...rest}
|
|
125
|
+
>
|
|
126
|
+
{children}
|
|
127
|
+
</Switcher>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export default Card;
|
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
import React, { useEffect, useState } from 'react';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ChangelogFilters — replaces the table of contents on a changelog page with a
|
|
5
|
-
* tag filter. The tag set is passed in (collected at build time from each
|
|
6
|
-
* <Update tags=...>); toggling a tag shows/hides the matching entries purely
|
|
7
|
-
* from the DOM via the `data-update-tags` attribute each <Update> emits.
|
|
8
|
-
*
|
|
9
|
-
* An entry is shown when no tag is selected, or when it carries any selected
|
|
10
|
-
* tag (OR semantics).
|
|
11
|
-
*
|
|
12
|
-
* @param {{ tags?: string[] }} props
|
|
13
|
-
*/
|
|
14
|
-
export default function ChangelogFilters({ tags = [] }) {
|
|
15
|
-
const [selected, setSelected] = useState([]);
|
|
16
|
-
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
if (typeof document === 'undefined') return undefined;
|
|
19
|
-
const sel = selected.map((s) => s.toLowerCase());
|
|
20
|
-
const nodes = document.querySelectorAll('.velu-update');
|
|
21
|
-
nodes.forEach((el) => {
|
|
22
|
-
const own = (el.getAttribute('data-update-tags') || '')
|
|
23
|
-
.split('|')
|
|
24
|
-
.map((t) => t.trim().toLowerCase())
|
|
25
|
-
.filter(Boolean);
|
|
26
|
-
const visible = sel.length === 0 || sel.some((s) => own.includes(s));
|
|
27
|
-
el.hidden = !visible;
|
|
28
|
-
});
|
|
29
|
-
return () => {
|
|
30
|
-
document.querySelectorAll('.velu-update').forEach((el) => {
|
|
31
|
-
el.hidden = false;
|
|
32
|
-
});
|
|
33
|
-
};
|
|
34
|
-
}, [selected]);
|
|
35
|
-
|
|
36
|
-
if (!tags.length) return null;
|
|
37
|
-
|
|
38
|
-
const toggle = (tag) => {
|
|
39
|
-
const lower = tag.toLowerCase();
|
|
40
|
-
setSelected((prev) =>
|
|
41
|
-
prev.some((s) => s.toLowerCase() === lower)
|
|
42
|
-
? prev.filter((s) => s.toLowerCase() !== lower)
|
|
43
|
-
: [...prev, tag],
|
|
44
|
-
);
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<div className="velu-changelog-filters">
|
|
49
|
-
<div className="velu-changelog-filters__heading">Filters</div>
|
|
50
|
-
<div
|
|
51
|
-
className="velu-changelog-filters__list"
|
|
52
|
-
role="group"
|
|
53
|
-
aria-label="Filter updates by tag"
|
|
54
|
-
>
|
|
55
|
-
{tags.map((tag) => {
|
|
56
|
-
const active = selected.some((s) => s.toLowerCase() === tag.toLowerCase());
|
|
57
|
-
return (
|
|
58
|
-
<button
|
|
59
|
-
key={tag}
|
|
60
|
-
type="button"
|
|
61
|
-
className={
|
|
62
|
-
'velu-changelog-filter' +
|
|
63
|
-
(active ? ' velu-changelog-filter--active' : '')
|
|
64
|
-
}
|
|
65
|
-
aria-pressed={active}
|
|
66
|
-
onClick={() => toggle(tag)}
|
|
67
|
-
>
|
|
68
|
-
{tag}
|
|
69
|
-
</button>
|
|
70
|
-
);
|
|
71
|
-
})}
|
|
72
|
-
</div>
|
|
73
|
-
</div>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ChangelogFilters — replaces the table of contents on a changelog page with a
|
|
5
|
+
* tag filter. The tag set is passed in (collected at build time from each
|
|
6
|
+
* <Update tags=...>); toggling a tag shows/hides the matching entries purely
|
|
7
|
+
* from the DOM via the `data-update-tags` attribute each <Update> emits.
|
|
8
|
+
*
|
|
9
|
+
* An entry is shown when no tag is selected, or when it carries any selected
|
|
10
|
+
* tag (OR semantics).
|
|
11
|
+
*
|
|
12
|
+
* @param {{ tags?: string[] }} props
|
|
13
|
+
*/
|
|
14
|
+
export default function ChangelogFilters({ tags = [] }) {
|
|
15
|
+
const [selected, setSelected] = useState([]);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (typeof document === 'undefined') return undefined;
|
|
19
|
+
const sel = selected.map((s) => s.toLowerCase());
|
|
20
|
+
const nodes = document.querySelectorAll('.velu-update');
|
|
21
|
+
nodes.forEach((el) => {
|
|
22
|
+
const own = (el.getAttribute('data-update-tags') || '')
|
|
23
|
+
.split('|')
|
|
24
|
+
.map((t) => t.trim().toLowerCase())
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
const visible = sel.length === 0 || sel.some((s) => own.includes(s));
|
|
27
|
+
el.hidden = !visible;
|
|
28
|
+
});
|
|
29
|
+
return () => {
|
|
30
|
+
document.querySelectorAll('.velu-update').forEach((el) => {
|
|
31
|
+
el.hidden = false;
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
}, [selected]);
|
|
35
|
+
|
|
36
|
+
if (!tags.length) return null;
|
|
37
|
+
|
|
38
|
+
const toggle = (tag) => {
|
|
39
|
+
const lower = tag.toLowerCase();
|
|
40
|
+
setSelected((prev) =>
|
|
41
|
+
prev.some((s) => s.toLowerCase() === lower)
|
|
42
|
+
? prev.filter((s) => s.toLowerCase() !== lower)
|
|
43
|
+
: [...prev, tag],
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="velu-changelog-filters">
|
|
49
|
+
<div className="velu-changelog-filters__heading">Filters</div>
|
|
50
|
+
<div
|
|
51
|
+
className="velu-changelog-filters__list"
|
|
52
|
+
role="group"
|
|
53
|
+
aria-label="Filter updates by tag"
|
|
54
|
+
>
|
|
55
|
+
{tags.map((tag) => {
|
|
56
|
+
const active = selected.some((s) => s.toLowerCase() === tag.toLowerCase());
|
|
57
|
+
return (
|
|
58
|
+
<button
|
|
59
|
+
key={tag}
|
|
60
|
+
type="button"
|
|
61
|
+
className={
|
|
62
|
+
'velu-changelog-filter' +
|
|
63
|
+
(active ? ' velu-changelog-filter--active' : '')
|
|
64
|
+
}
|
|
65
|
+
aria-pressed={active}
|
|
66
|
+
onClick={() => toggle(tag)}
|
|
67
|
+
>
|
|
68
|
+
{tag}
|
|
69
|
+
</button>
|
|
70
|
+
);
|
|
71
|
+
})}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|