@veluai/velu 0.2.13 → 0.2.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.
- package/README.md +80 -80
- package/dist/cli.js +37 -37
- 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 +122 -122
- 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/Chatbot.jsx +885 -885
- 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 +298 -273
- 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 +254 -254
- 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/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 +94 -94
- package/runtime/velu-ui/components/card.css +105 -105
- package/runtime/velu-ui/components/chatbot.css +622 -622
- package/runtime/velu-ui/components/code-block.css +263 -263
- package/runtime/velu-ui/components/context-menu.css +173 -173
- 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 +241 -241
- 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/index.js +54 -54
- package/runtime/velu-ui/lib/api-send.js +92 -92
- 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 +250 -250
- package/runtime/velu-ui/lib/lang-icons.jsx +147 -147
- package/runtime/velu-ui/lib/pagefind.js +113 -113
- 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 +105 -105
- 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 +46 -46
- package/runtime/velu-ui/tokens.css +4 -4
- package/schema/velu.schema.json +423 -423
- package/src/lib/extract-mdx-error.js +170 -170
- package/src/lib/issues.js +159 -159
- package/src/lib/known-components.js +34 -34
- package/src/navigation.js +443 -443
- package/src/runtime/App.jsx +1669 -1668
- package/src/runtime/ErrorBoundary.jsx +54 -54
- package/src/runtime/client-entry.jsx +22 -22
- package/src/runtime/server-entry.jsx +16 -16
- 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 +41 -41
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import resolveIcon from '../lib/resolveIcon.jsx';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* NotFound — the 404 page ("Direction 1 · Classic" from the design):
|
|
6
|
-
* a big accent-colored numeral, a short message, and a three-action row
|
|
7
|
-
* (Back to Home / Search the docs / Ask AI). Centered; the host layout
|
|
8
|
-
* supplies the header + footer. Pure tokens, so light/dark follow
|
|
9
|
-
* [data-theme] automatically.
|
|
10
|
-
*
|
|
11
|
-
* @param {{
|
|
12
|
-
* homeHref?: string,
|
|
13
|
-
* linkComponent?: React.ElementType,
|
|
14
|
-
* onSearch?: () => void,
|
|
15
|
-
* onAskAI?: () => void,
|
|
16
|
-
* eyebrow?: string, title?: string, body?: string,
|
|
17
|
-
* }} props
|
|
18
|
-
*/
|
|
19
|
-
export default function NotFound({
|
|
20
|
-
homeHref = '/',
|
|
21
|
-
linkComponent = 'a',
|
|
22
|
-
onSearch,
|
|
23
|
-
onAskAI,
|
|
24
|
-
eyebrow = 'Error 404',
|
|
25
|
-
title = 'This page wandered off',
|
|
26
|
-
body = 'The page you’re looking for doesn’t exist or may have moved. Pick up the trail below.',
|
|
27
|
-
}) {
|
|
28
|
-
const Link = linkComponent;
|
|
29
|
-
return (
|
|
30
|
-
<section className="velu-404">
|
|
31
|
-
<p className="velu-404__eyebrow">{eyebrow}</p>
|
|
32
|
-
<div className="velu-404__num" aria-hidden="true">
|
|
33
|
-
404
|
|
34
|
-
</div>
|
|
35
|
-
<h1 className="velu-404__title">{title}</h1>
|
|
36
|
-
<p className="velu-404__body">{body}</p>
|
|
37
|
-
<div className="velu-404__actions">
|
|
38
|
-
<Link className="velu-404__btn velu-404__btn--primary" href={homeHref}>
|
|
39
|
-
<span className="velu-404__btn-ic" aria-hidden="true">
|
|
40
|
-
{resolveIcon('house', { size: '1em' })}
|
|
41
|
-
</span>
|
|
42
|
-
Back to Home
|
|
43
|
-
</Link>
|
|
44
|
-
{onSearch && (
|
|
45
|
-
<button type="button" className="velu-404__btn" onClick={onSearch}>
|
|
46
|
-
<span className="velu-404__btn-ic" aria-hidden="true">
|
|
47
|
-
{resolveIcon('search', { size: '1em' })}
|
|
48
|
-
</span>
|
|
49
|
-
Search the docs
|
|
50
|
-
</button>
|
|
51
|
-
)}
|
|
52
|
-
{onAskAI && (
|
|
53
|
-
<button type="button" className="velu-404__btn" onClick={onAskAI}>
|
|
54
|
-
<span className="velu-404__btn-ic" aria-hidden="true">
|
|
55
|
-
{resolveIcon('sparkles', { size: '1em' })}
|
|
56
|
-
</span>
|
|
57
|
-
Ask AI
|
|
58
|
-
</button>
|
|
59
|
-
)}
|
|
60
|
-
</div>
|
|
61
|
-
</section>
|
|
62
|
-
);
|
|
63
|
-
}
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import resolveIcon from '../lib/resolveIcon.jsx';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* NotFound — the 404 page ("Direction 1 · Classic" from the design):
|
|
6
|
+
* a big accent-colored numeral, a short message, and a three-action row
|
|
7
|
+
* (Back to Home / Search the docs / Ask AI). Centered; the host layout
|
|
8
|
+
* supplies the header + footer. Pure tokens, so light/dark follow
|
|
9
|
+
* [data-theme] automatically.
|
|
10
|
+
*
|
|
11
|
+
* @param {{
|
|
12
|
+
* homeHref?: string,
|
|
13
|
+
* linkComponent?: React.ElementType,
|
|
14
|
+
* onSearch?: () => void,
|
|
15
|
+
* onAskAI?: () => void,
|
|
16
|
+
* eyebrow?: string, title?: string, body?: string,
|
|
17
|
+
* }} props
|
|
18
|
+
*/
|
|
19
|
+
export default function NotFound({
|
|
20
|
+
homeHref = '/',
|
|
21
|
+
linkComponent = 'a',
|
|
22
|
+
onSearch,
|
|
23
|
+
onAskAI,
|
|
24
|
+
eyebrow = 'Error 404',
|
|
25
|
+
title = 'This page wandered off',
|
|
26
|
+
body = 'The page you’re looking for doesn’t exist or may have moved. Pick up the trail below.',
|
|
27
|
+
}) {
|
|
28
|
+
const Link = linkComponent;
|
|
29
|
+
return (
|
|
30
|
+
<section className="velu-404">
|
|
31
|
+
<p className="velu-404__eyebrow">{eyebrow}</p>
|
|
32
|
+
<div className="velu-404__num" aria-hidden="true">
|
|
33
|
+
404
|
|
34
|
+
</div>
|
|
35
|
+
<h1 className="velu-404__title">{title}</h1>
|
|
36
|
+
<p className="velu-404__body">{body}</p>
|
|
37
|
+
<div className="velu-404__actions">
|
|
38
|
+
<Link className="velu-404__btn velu-404__btn--primary" href={homeHref}>
|
|
39
|
+
<span className="velu-404__btn-ic" aria-hidden="true">
|
|
40
|
+
{resolveIcon('house', { size: '1em' })}
|
|
41
|
+
</span>
|
|
42
|
+
Back to Home
|
|
43
|
+
</Link>
|
|
44
|
+
{onSearch && (
|
|
45
|
+
<button type="button" className="velu-404__btn" onClick={onSearch}>
|
|
46
|
+
<span className="velu-404__btn-ic" aria-hidden="true">
|
|
47
|
+
{resolveIcon('search', { size: '1em' })}
|
|
48
|
+
</span>
|
|
49
|
+
Search the docs
|
|
50
|
+
</button>
|
|
51
|
+
)}
|
|
52
|
+
{onAskAI && (
|
|
53
|
+
<button type="button" className="velu-404__btn" onClick={onAskAI}>
|
|
54
|
+
<span className="velu-404__btn-ic" aria-hidden="true">
|
|
55
|
+
{resolveIcon('sparkles', { size: '1em' })}
|
|
56
|
+
</span>
|
|
57
|
+
Ask AI
|
|
58
|
+
</button>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
</section>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -1,219 +1,219 @@
|
|
|
1
|
-
import React, { useState, useMemo, useId } from 'react';
|
|
2
|
-
import resolveIcon from '../lib/resolveIcon.jsx';
|
|
3
|
-
import Stack from '../primitives/Stack.jsx';
|
|
4
|
-
import Cluster from '../primitives/Cluster.jsx';
|
|
5
|
-
|
|
6
|
-
/* Improvement reasons shown in the "No" follow-up form. */
|
|
7
|
-
const REASONS = [
|
|
8
|
-
'Help me get started faster',
|
|
9
|
-
"Make it easier to find what I'm looking for",
|
|
10
|
-
'Make it easy to understand the product and features',
|
|
11
|
-
'Update this documentation',
|
|
12
|
-
'Something else',
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
/* HeartBurst — a one-shot celebratory burst of hearts. Renders a fan of
|
|
16
|
-
small hearts that pop up-and-out from their anchor point, tilt, and
|
|
17
|
-
fade. Mounted only after a click (never during SSR), so the per-heart
|
|
18
|
-
Math.random() can't cause a hydration mismatch. Re-key it to replay. */
|
|
19
|
-
const HEART_COUNT = 14;
|
|
20
|
-
const HEART_SPREAD = 3.4; // em — horizontal half-width of the fan
|
|
21
|
-
const HEART_PATH =
|
|
22
|
-
'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z';
|
|
23
|
-
|
|
24
|
-
function HeartBurst() {
|
|
25
|
-
const hearts = useMemo(
|
|
26
|
-
() =>
|
|
27
|
-
Array.from({ length: HEART_COUNT }, () => ({
|
|
28
|
-
tx: `${((Math.random() * 2 - 1) * HEART_SPREAD).toFixed(2)}em`,
|
|
29
|
-
ty: `${(-(0.4 + Math.random() * 0.9) * HEART_SPREAD).toFixed(2)}em`,
|
|
30
|
-
rot: `${Math.round(Math.random() * 120 - 60)}deg`, // gentle tilt
|
|
31
|
-
delay: `${(Math.random() * 0.08).toFixed(3)}s`,
|
|
32
|
-
shade: Math.floor(Math.random() * 4),
|
|
33
|
-
})),
|
|
34
|
-
[],
|
|
35
|
-
);
|
|
36
|
-
return (
|
|
37
|
-
<span className="velu-feedback__hearts" aria-hidden="true">
|
|
38
|
-
{hearts.map((h, i) => (
|
|
39
|
-
<svg
|
|
40
|
-
key={i}
|
|
41
|
-
className={`velu-feedback__heart velu-feedback__heart--${h.shade}`}
|
|
42
|
-
viewBox="0 0 24 24"
|
|
43
|
-
style={{
|
|
44
|
-
'--hb-tx': h.tx,
|
|
45
|
-
'--hb-ty': h.ty,
|
|
46
|
-
'--hb-rot': h.rot,
|
|
47
|
-
animationDelay: h.delay,
|
|
48
|
-
}}
|
|
49
|
-
>
|
|
50
|
-
<path d={HEART_PATH} />
|
|
51
|
-
</svg>
|
|
52
|
-
))}
|
|
53
|
-
</span>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* PageFeedback — the "Was this page helpful?" row at the foot of a docs
|
|
59
|
-
* page. A question label + Yes / No buttons (thumbs-up / thumbs-down).
|
|
60
|
-
*
|
|
61
|
-
* <PageFeedback onVote={(v) => …} /> // v is 'yes' | 'no'
|
|
62
|
-
*
|
|
63
|
-
* The chosen button takes an accent state. Choosing "Yes" fires a
|
|
64
|
-
* small burst of hearts; choosing "No" expands an improvement form
|
|
65
|
-
* (reason radio group + optional detail + Submit / Cancel). Once the
|
|
66
|
-
* form is submitted — or "Yes" is chosen — a thank-you line shows.
|
|
67
|
-
*
|
|
68
|
-
* - `question` the prompt (default "Was this page helpful?")
|
|
69
|
-
* - `onVote(vote)` fired on every Yes/No change
|
|
70
|
-
* - `onSubmit({ vote, reason, detail })` fired when the "No" form's
|
|
71
|
-
* Submit button is pressed
|
|
72
|
-
*/
|
|
73
|
-
|
|
74
|
-
export default function PageFeedback({
|
|
75
|
-
question = 'Was this page helpful?',
|
|
76
|
-
onVote,
|
|
77
|
-
onSubmit,
|
|
78
|
-
className = '',
|
|
79
|
-
...rest
|
|
80
|
-
}) {
|
|
81
|
-
const [vote, setVote] = useState(null);
|
|
82
|
-
// Increments each time "Yes" is (re-)chosen — keys <HeartBurst> so
|
|
83
|
-
// the burst replays.
|
|
84
|
-
const [burst, setBurst] = useState(0);
|
|
85
|
-
const [reason, setReason] = useState('');
|
|
86
|
-
const [detail, setDetail] = useState('');
|
|
87
|
-
const [submitted, setSubmitted] = useState(false);
|
|
88
|
-
const radioName = useId();
|
|
89
|
-
|
|
90
|
-
function choose(v) {
|
|
91
|
-
if (v === vote) return; // already selected — no-op
|
|
92
|
-
setVote(v);
|
|
93
|
-
setSubmitted(false);
|
|
94
|
-
onVote?.(v);
|
|
95
|
-
if (v === 'yes') setBurst((b) => b + 1);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function cancel() {
|
|
99
|
-
setVote(null);
|
|
100
|
-
setReason('');
|
|
101
|
-
setDetail('');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function submit() {
|
|
105
|
-
setSubmitted(true);
|
|
106
|
-
onSubmit?.({ vote: 'no', reason, detail });
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const showForm = vote === 'no' && !submitted;
|
|
110
|
-
|
|
111
|
-
return (
|
|
112
|
-
<Stack
|
|
113
|
-
space="var(--s0)"
|
|
114
|
-
className={`velu-feedback ${className}`.trim()}
|
|
115
|
-
{...rest}
|
|
116
|
-
>
|
|
117
|
-
<Cluster space="var(--s0)" align="center" className="velu-feedback__bar">
|
|
118
|
-
<span className="velu-feedback__question">{question}</span>
|
|
119
|
-
|
|
120
|
-
<Cluster space="var(--s-2)" align="center">
|
|
121
|
-
<button
|
|
122
|
-
type="button"
|
|
123
|
-
className={`velu-feedback__btn${
|
|
124
|
-
vote === 'yes' ? ' velu-feedback__btn--chosen' : ''
|
|
125
|
-
}`}
|
|
126
|
-
onClick={() => choose('yes')}
|
|
127
|
-
aria-pressed={vote === 'yes'}
|
|
128
|
-
>
|
|
129
|
-
<span>Yes</span>
|
|
130
|
-
{resolveIcon('thumbs-up', { size: '1em' })}
|
|
131
|
-
{burst > 0 && <HeartBurst key={burst} />}
|
|
132
|
-
</button>
|
|
133
|
-
<button
|
|
134
|
-
type="button"
|
|
135
|
-
className={`velu-feedback__btn${
|
|
136
|
-
vote === 'no' ? ' velu-feedback__btn--chosen' : ''
|
|
137
|
-
}`}
|
|
138
|
-
onClick={() => choose('no')}
|
|
139
|
-
aria-pressed={vote === 'no'}
|
|
140
|
-
>
|
|
141
|
-
<span>No</span>
|
|
142
|
-
{resolveIcon('thumbs-down', { size: '1em' })}
|
|
143
|
-
</button>
|
|
144
|
-
</Cluster>
|
|
145
|
-
|
|
146
|
-
{vote === 'yes' && (
|
|
147
|
-
<span className="velu-feedback__thanks" role="status">
|
|
148
|
-
Thanks for your feedback
|
|
149
|
-
</span>
|
|
150
|
-
)}
|
|
151
|
-
</Cluster>
|
|
152
|
-
|
|
153
|
-
{showForm && (
|
|
154
|
-
<Stack space="var(--s1)" className="velu-feedback__form">
|
|
155
|
-
<h3 className="velu-feedback__form-title">
|
|
156
|
-
How can we improve our product?
|
|
157
|
-
</h3>
|
|
158
|
-
|
|
159
|
-
<Stack as="div" space="var(--s-2)">
|
|
160
|
-
{REASONS.map((r) => (
|
|
161
|
-
<label key={r} className="velu-feedback__radio">
|
|
162
|
-
<input
|
|
163
|
-
type="radio"
|
|
164
|
-
name={radioName}
|
|
165
|
-
value={r}
|
|
166
|
-
checked={reason === r}
|
|
167
|
-
onChange={() => setReason(r)}
|
|
168
|
-
/>
|
|
169
|
-
<span
|
|
170
|
-
className="velu-feedback__radio-mark"
|
|
171
|
-
aria-hidden="true"
|
|
172
|
-
/>
|
|
173
|
-
<span className="velu-feedback__radio-label">{r}</span>
|
|
174
|
-
</label>
|
|
175
|
-
))}
|
|
176
|
-
</Stack>
|
|
177
|
-
|
|
178
|
-
<textarea
|
|
179
|
-
className="velu-feedback__detail"
|
|
180
|
-
placeholder="(optional) Could you share more about your experience?"
|
|
181
|
-
value={detail}
|
|
182
|
-
onChange={(e) => setDetail(e.target.value)}
|
|
183
|
-
rows={3}
|
|
184
|
-
/>
|
|
185
|
-
|
|
186
|
-
<Cluster space="var(--s-1)">
|
|
187
|
-
<button
|
|
188
|
-
type="button"
|
|
189
|
-
className="velu-feedback__action velu-feedback__action--cancel"
|
|
190
|
-
onClick={cancel}
|
|
191
|
-
>
|
|
192
|
-
Cancel
|
|
193
|
-
</button>
|
|
194
|
-
<button
|
|
195
|
-
type="button"
|
|
196
|
-
className="velu-feedback__action velu-feedback__action--submit"
|
|
197
|
-
onClick={submit}
|
|
198
|
-
>
|
|
199
|
-
Submit Feedback
|
|
200
|
-
</button>
|
|
201
|
-
</Cluster>
|
|
202
|
-
</Stack>
|
|
203
|
-
)}
|
|
204
|
-
|
|
205
|
-
{submitted && (
|
|
206
|
-
<Stack
|
|
207
|
-
space="var(--s-5)"
|
|
208
|
-
className="velu-feedback__sent"
|
|
209
|
-
role="status"
|
|
210
|
-
>
|
|
211
|
-
<span className="velu-feedback__sent-title">Feedback sent</span>
|
|
212
|
-
<span className="velu-feedback__sent-note">
|
|
213
|
-
Thanks for feedback
|
|
214
|
-
</span>
|
|
215
|
-
</Stack>
|
|
216
|
-
)}
|
|
217
|
-
</Stack>
|
|
218
|
-
);
|
|
219
|
-
}
|
|
1
|
+
import React, { useState, useMemo, useId } from 'react';
|
|
2
|
+
import resolveIcon from '../lib/resolveIcon.jsx';
|
|
3
|
+
import Stack from '../primitives/Stack.jsx';
|
|
4
|
+
import Cluster from '../primitives/Cluster.jsx';
|
|
5
|
+
|
|
6
|
+
/* Improvement reasons shown in the "No" follow-up form. */
|
|
7
|
+
const REASONS = [
|
|
8
|
+
'Help me get started faster',
|
|
9
|
+
"Make it easier to find what I'm looking for",
|
|
10
|
+
'Make it easy to understand the product and features',
|
|
11
|
+
'Update this documentation',
|
|
12
|
+
'Something else',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
/* HeartBurst — a one-shot celebratory burst of hearts. Renders a fan of
|
|
16
|
+
small hearts that pop up-and-out from their anchor point, tilt, and
|
|
17
|
+
fade. Mounted only after a click (never during SSR), so the per-heart
|
|
18
|
+
Math.random() can't cause a hydration mismatch. Re-key it to replay. */
|
|
19
|
+
const HEART_COUNT = 14;
|
|
20
|
+
const HEART_SPREAD = 3.4; // em — horizontal half-width of the fan
|
|
21
|
+
const HEART_PATH =
|
|
22
|
+
'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z';
|
|
23
|
+
|
|
24
|
+
function HeartBurst() {
|
|
25
|
+
const hearts = useMemo(
|
|
26
|
+
() =>
|
|
27
|
+
Array.from({ length: HEART_COUNT }, () => ({
|
|
28
|
+
tx: `${((Math.random() * 2 - 1) * HEART_SPREAD).toFixed(2)}em`,
|
|
29
|
+
ty: `${(-(0.4 + Math.random() * 0.9) * HEART_SPREAD).toFixed(2)}em`,
|
|
30
|
+
rot: `${Math.round(Math.random() * 120 - 60)}deg`, // gentle tilt
|
|
31
|
+
delay: `${(Math.random() * 0.08).toFixed(3)}s`,
|
|
32
|
+
shade: Math.floor(Math.random() * 4),
|
|
33
|
+
})),
|
|
34
|
+
[],
|
|
35
|
+
);
|
|
36
|
+
return (
|
|
37
|
+
<span className="velu-feedback__hearts" aria-hidden="true">
|
|
38
|
+
{hearts.map((h, i) => (
|
|
39
|
+
<svg
|
|
40
|
+
key={i}
|
|
41
|
+
className={`velu-feedback__heart velu-feedback__heart--${h.shade}`}
|
|
42
|
+
viewBox="0 0 24 24"
|
|
43
|
+
style={{
|
|
44
|
+
'--hb-tx': h.tx,
|
|
45
|
+
'--hb-ty': h.ty,
|
|
46
|
+
'--hb-rot': h.rot,
|
|
47
|
+
animationDelay: h.delay,
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<path d={HEART_PATH} />
|
|
51
|
+
</svg>
|
|
52
|
+
))}
|
|
53
|
+
</span>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* PageFeedback — the "Was this page helpful?" row at the foot of a docs
|
|
59
|
+
* page. A question label + Yes / No buttons (thumbs-up / thumbs-down).
|
|
60
|
+
*
|
|
61
|
+
* <PageFeedback onVote={(v) => …} /> // v is 'yes' | 'no'
|
|
62
|
+
*
|
|
63
|
+
* The chosen button takes an accent state. Choosing "Yes" fires a
|
|
64
|
+
* small burst of hearts; choosing "No" expands an improvement form
|
|
65
|
+
* (reason radio group + optional detail + Submit / Cancel). Once the
|
|
66
|
+
* form is submitted — or "Yes" is chosen — a thank-you line shows.
|
|
67
|
+
*
|
|
68
|
+
* - `question` the prompt (default "Was this page helpful?")
|
|
69
|
+
* - `onVote(vote)` fired on every Yes/No change
|
|
70
|
+
* - `onSubmit({ vote, reason, detail })` fired when the "No" form's
|
|
71
|
+
* Submit button is pressed
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
export default function PageFeedback({
|
|
75
|
+
question = 'Was this page helpful?',
|
|
76
|
+
onVote,
|
|
77
|
+
onSubmit,
|
|
78
|
+
className = '',
|
|
79
|
+
...rest
|
|
80
|
+
}) {
|
|
81
|
+
const [vote, setVote] = useState(null);
|
|
82
|
+
// Increments each time "Yes" is (re-)chosen — keys <HeartBurst> so
|
|
83
|
+
// the burst replays.
|
|
84
|
+
const [burst, setBurst] = useState(0);
|
|
85
|
+
const [reason, setReason] = useState('');
|
|
86
|
+
const [detail, setDetail] = useState('');
|
|
87
|
+
const [submitted, setSubmitted] = useState(false);
|
|
88
|
+
const radioName = useId();
|
|
89
|
+
|
|
90
|
+
function choose(v) {
|
|
91
|
+
if (v === vote) return; // already selected — no-op
|
|
92
|
+
setVote(v);
|
|
93
|
+
setSubmitted(false);
|
|
94
|
+
onVote?.(v);
|
|
95
|
+
if (v === 'yes') setBurst((b) => b + 1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function cancel() {
|
|
99
|
+
setVote(null);
|
|
100
|
+
setReason('');
|
|
101
|
+
setDetail('');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function submit() {
|
|
105
|
+
setSubmitted(true);
|
|
106
|
+
onSubmit?.({ vote: 'no', reason, detail });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const showForm = vote === 'no' && !submitted;
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<Stack
|
|
113
|
+
space="var(--s0)"
|
|
114
|
+
className={`velu-feedback ${className}`.trim()}
|
|
115
|
+
{...rest}
|
|
116
|
+
>
|
|
117
|
+
<Cluster space="var(--s0)" align="center" className="velu-feedback__bar">
|
|
118
|
+
<span className="velu-feedback__question">{question}</span>
|
|
119
|
+
|
|
120
|
+
<Cluster space="var(--s-2)" align="center">
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
className={`velu-feedback__btn${
|
|
124
|
+
vote === 'yes' ? ' velu-feedback__btn--chosen' : ''
|
|
125
|
+
}`}
|
|
126
|
+
onClick={() => choose('yes')}
|
|
127
|
+
aria-pressed={vote === 'yes'}
|
|
128
|
+
>
|
|
129
|
+
<span>Yes</span>
|
|
130
|
+
{resolveIcon('thumbs-up', { size: '1em' })}
|
|
131
|
+
{burst > 0 && <HeartBurst key={burst} />}
|
|
132
|
+
</button>
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
className={`velu-feedback__btn${
|
|
136
|
+
vote === 'no' ? ' velu-feedback__btn--chosen' : ''
|
|
137
|
+
}`}
|
|
138
|
+
onClick={() => choose('no')}
|
|
139
|
+
aria-pressed={vote === 'no'}
|
|
140
|
+
>
|
|
141
|
+
<span>No</span>
|
|
142
|
+
{resolveIcon('thumbs-down', { size: '1em' })}
|
|
143
|
+
</button>
|
|
144
|
+
</Cluster>
|
|
145
|
+
|
|
146
|
+
{vote === 'yes' && (
|
|
147
|
+
<span className="velu-feedback__thanks" role="status">
|
|
148
|
+
Thanks for your feedback
|
|
149
|
+
</span>
|
|
150
|
+
)}
|
|
151
|
+
</Cluster>
|
|
152
|
+
|
|
153
|
+
{showForm && (
|
|
154
|
+
<Stack space="var(--s1)" className="velu-feedback__form">
|
|
155
|
+
<h3 className="velu-feedback__form-title">
|
|
156
|
+
How can we improve our product?
|
|
157
|
+
</h3>
|
|
158
|
+
|
|
159
|
+
<Stack as="div" space="var(--s-2)">
|
|
160
|
+
{REASONS.map((r) => (
|
|
161
|
+
<label key={r} className="velu-feedback__radio">
|
|
162
|
+
<input
|
|
163
|
+
type="radio"
|
|
164
|
+
name={radioName}
|
|
165
|
+
value={r}
|
|
166
|
+
checked={reason === r}
|
|
167
|
+
onChange={() => setReason(r)}
|
|
168
|
+
/>
|
|
169
|
+
<span
|
|
170
|
+
className="velu-feedback__radio-mark"
|
|
171
|
+
aria-hidden="true"
|
|
172
|
+
/>
|
|
173
|
+
<span className="velu-feedback__radio-label">{r}</span>
|
|
174
|
+
</label>
|
|
175
|
+
))}
|
|
176
|
+
</Stack>
|
|
177
|
+
|
|
178
|
+
<textarea
|
|
179
|
+
className="velu-feedback__detail"
|
|
180
|
+
placeholder="(optional) Could you share more about your experience?"
|
|
181
|
+
value={detail}
|
|
182
|
+
onChange={(e) => setDetail(e.target.value)}
|
|
183
|
+
rows={3}
|
|
184
|
+
/>
|
|
185
|
+
|
|
186
|
+
<Cluster space="var(--s-1)">
|
|
187
|
+
<button
|
|
188
|
+
type="button"
|
|
189
|
+
className="velu-feedback__action velu-feedback__action--cancel"
|
|
190
|
+
onClick={cancel}
|
|
191
|
+
>
|
|
192
|
+
Cancel
|
|
193
|
+
</button>
|
|
194
|
+
<button
|
|
195
|
+
type="button"
|
|
196
|
+
className="velu-feedback__action velu-feedback__action--submit"
|
|
197
|
+
onClick={submit}
|
|
198
|
+
>
|
|
199
|
+
Submit Feedback
|
|
200
|
+
</button>
|
|
201
|
+
</Cluster>
|
|
202
|
+
</Stack>
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
{submitted && (
|
|
206
|
+
<Stack
|
|
207
|
+
space="var(--s-5)"
|
|
208
|
+
className="velu-feedback__sent"
|
|
209
|
+
role="status"
|
|
210
|
+
>
|
|
211
|
+
<span className="velu-feedback__sent-title">Feedback sent</span>
|
|
212
|
+
<span className="velu-feedback__sent-note">
|
|
213
|
+
Thanks for feedback
|
|
214
|
+
</span>
|
|
215
|
+
</Stack>
|
|
216
|
+
)}
|
|
217
|
+
</Stack>
|
|
218
|
+
);
|
|
219
|
+
}
|