@veluai/velu 0.2.20 → 0.2.23
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/dist/cli.js +52 -35
- package/package.json +1 -1
- package/runtime/velu-ui/components/ChangelogFilters.jsx +75 -0
- package/runtime/velu-ui/components/Chatbot.jsx +125 -38
- package/runtime/velu-ui/components/ContextMenu.jsx +28 -5
- package/runtime/velu-ui/components/Update.jsx +92 -0
- package/runtime/velu-ui/components/changelog-filters.css +43 -0
- package/runtime/velu-ui/components/chatbot.css +57 -0
- package/runtime/velu-ui/components/context-menu.css +26 -0
- package/runtime/velu-ui/components/page-feedback.css +20 -0
- package/runtime/velu-ui/components/update.css +110 -0
- package/runtime/velu-ui/index.js +2 -0
- package/runtime/velu-ui/lib/docs-assistant.js +26 -3
- package/runtime/velu-ui/lib/pagefind.js +4 -1
- package/runtime/velu-ui/mdx-components.jsx +2 -0
- package/runtime/velu-ui/styles.css +2 -0
- package/schema/velu.schema.json +0 -4
- package/src/lib/known-components.js +1 -0
- package/src/runtime/App.jsx +49 -6
- package/src/runtime/client-entry.jsx +6 -1
- package/src/runtime/server-entry.jsx +9 -1
- package/templates/starter/index.mdx +4 -4
- package/templates/starter/velu.json +0 -1
|
@@ -195,48 +195,135 @@ function onSourceClick(e, s, onNavigate) {
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
/* Render partial tokens as paragraphs (split on \n\n, single \n → <br>). */
|
|
198
|
+
/* Flatten the token stream back to a markdown string (code → `…`, citation →
|
|
199
|
+
[n]) so the assistant's markdown answer can be rendered with formatting. */
|
|
200
|
+
function tokensToMarkdown(tokens) {
|
|
201
|
+
return tokens
|
|
202
|
+
.map((tk) => (tk.kind === 'cite' ? `[${tk.n}]` : tk.kind === 'code' ? `\`${tk.v}\`` : tk.v))
|
|
203
|
+
.join('');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const CITE_RE = /\[(\d+)\]/;
|
|
207
|
+
const INLINE_RULES = [
|
|
208
|
+
{ re: /\*\*([^*]+)\*\*/, type: 'bold' },
|
|
209
|
+
{ re: /__([^_]+)__/, type: 'bold' },
|
|
210
|
+
{ re: /\*([^*\n]+)\*/, type: 'italic' },
|
|
211
|
+
{ re: /(?<![A-Za-z0-9_])_([^_\n]+)_(?![A-Za-z0-9_])/, type: 'italic' },
|
|
212
|
+
{ re: /`([^`]+)`/, type: 'code' },
|
|
213
|
+
{ re: /\[([^\]]+)\]\(([^)]+)\)/, type: 'link' },
|
|
214
|
+
{ re: CITE_RE, type: 'cite' },
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
/* Inline markdown → React nodes: **bold**, *italic*, `code`, [text](url) links,
|
|
218
|
+
and [n] citation chips. Builds React elements (never innerHTML) so it's XSS-safe. */
|
|
219
|
+
function renderInline(text, sources, onNavigate, keyBase) {
|
|
220
|
+
const out = [];
|
|
221
|
+
let rest = text;
|
|
222
|
+
let k = 0;
|
|
223
|
+
while (rest) {
|
|
224
|
+
let best = null;
|
|
225
|
+
for (const rule of INLINE_RULES) {
|
|
226
|
+
const m = rule.re.exec(rest);
|
|
227
|
+
if (m && (best === null || m.index < best.m.index)) best = { rule, m };
|
|
228
|
+
}
|
|
229
|
+
if (!best) {
|
|
230
|
+
out.push(<React.Fragment key={`${keyBase}-${k++}`}>{rest}</React.Fragment>);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
const { rule, m } = best;
|
|
234
|
+
if (m.index > 0) out.push(<React.Fragment key={`${keyBase}-${k++}`}>{rest.slice(0, m.index)}</React.Fragment>);
|
|
235
|
+
const key = `${keyBase}-${k++}`;
|
|
236
|
+
if (rule.type === 'bold') out.push(<strong key={key}>{m[1]}</strong>);
|
|
237
|
+
else if (rule.type === 'italic') out.push(<em key={key}>{m[1]}</em>);
|
|
238
|
+
else if (rule.type === 'code') out.push(<code key={key}>{m[1]}</code>);
|
|
239
|
+
else if (rule.type === 'link') out.push(
|
|
240
|
+
<a key={key} href={m[2]} target="_blank" rel="noopener noreferrer">{m[1]}</a>,
|
|
241
|
+
);
|
|
242
|
+
else if (rule.type === 'cite') {
|
|
243
|
+
const n = Number(m[1]);
|
|
244
|
+
const src = sources?.find((s) => s.num === n);
|
|
245
|
+
out.push(
|
|
246
|
+
<a
|
|
247
|
+
key={key}
|
|
248
|
+
className="velu-chatbot__cite"
|
|
249
|
+
href={sourceHref(src) || '#'}
|
|
250
|
+
onClick={(e) => onSourceClick(e, src, onNavigate)}
|
|
251
|
+
title={citeTitle(src)}
|
|
252
|
+
>
|
|
253
|
+
{n}
|
|
254
|
+
</a>,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
rest = rest.slice(m.index + m[0].length);
|
|
258
|
+
}
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* Block-level markdown → React: headings, bullet/numbered lists, fenced code
|
|
263
|
+
blocks, blockquotes, and paragraphs (each rendered through renderInline). */
|
|
198
264
|
function renderStream(tokens, sources, onNavigate) {
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
265
|
+
const text = tokensToMarkdown(tokens);
|
|
266
|
+
const lines = text.replace(/\r/g, '').split('\n');
|
|
267
|
+
const blocks = [];
|
|
268
|
+
let i = 0;
|
|
269
|
+
let b = 0;
|
|
270
|
+
const listRe = /^\s*([-*+]|\d+\.)\s+/;
|
|
271
|
+
while (i < lines.length) {
|
|
272
|
+
const line = lines[i];
|
|
273
|
+
if (!line.trim()) { i++; continue; }
|
|
274
|
+
|
|
275
|
+
if (line.trim().startsWith('```')) {
|
|
276
|
+
const buf = [];
|
|
277
|
+
i++;
|
|
278
|
+
while (i < lines.length && !lines[i].trim().startsWith('```')) { buf.push(lines[i]); i++; }
|
|
279
|
+
i++;
|
|
280
|
+
blocks.push(<pre key={`b${b++}`} className="velu-chatbot__pre"><code>{buf.join('\n')}</code></pre>);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const h = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
285
|
+
if (h) {
|
|
286
|
+
const lvl = Math.min(h[1].length + 2, 6);
|
|
287
|
+
const Tag = `h${lvl}`;
|
|
288
|
+
blocks.push(<Tag key={`b${b++}`}>{renderInline(h[2], sources, onNavigate, `b${b}`)}</Tag>);
|
|
289
|
+
i++;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (listRe.test(line)) {
|
|
294
|
+
const ordered = /^\s*\d+\.\s+/.test(line);
|
|
295
|
+
const items = [];
|
|
296
|
+
while (i < lines.length && listRe.test(lines[i])) {
|
|
297
|
+
const content = lines[i].replace(listRe, '');
|
|
298
|
+
items.push(<li key={`li${b}-${i}`}>{renderInline(content, sources, onNavigate, `li${b}-${i}`)}</li>);
|
|
299
|
+
i++;
|
|
207
300
|
}
|
|
208
|
-
|
|
209
|
-
|
|
301
|
+
const ListTag = ordered ? 'ol' : 'ul';
|
|
302
|
+
blocks.push(<ListTag key={`b${b++}`}>{items}</ListTag>);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (line.trim().startsWith('>')) {
|
|
307
|
+
const buf = [];
|
|
308
|
+
while (i < lines.length && lines[i].trim().startsWith('>')) { buf.push(lines[i].replace(/^\s*>\s?/, '')); i++; }
|
|
309
|
+
blocks.push(<blockquote key={`b${b++}`}>{renderInline(buf.join(' '), sources, onNavigate, `bq${b}`)}</blockquote>);
|
|
310
|
+
continue;
|
|
210
311
|
}
|
|
312
|
+
|
|
313
|
+
const buf = [];
|
|
314
|
+
while (
|
|
315
|
+
i < lines.length && lines[i].trim()
|
|
316
|
+
&& !listRe.test(lines[i]) && !/^#{1,6}\s/.test(lines[i])
|
|
317
|
+
&& !lines[i].trim().startsWith('```') && !lines[i].trim().startsWith('>')
|
|
318
|
+
) { buf.push(lines[i]); i++; }
|
|
319
|
+
const pnodes = [];
|
|
320
|
+
buf.forEach((ln, li) => {
|
|
321
|
+
if (li > 0) pnodes.push(<br key={`p${b}-br${li}`} />);
|
|
322
|
+
pnodes.push(...renderInline(ln, sources, onNavigate, `p${b}-${li}`));
|
|
323
|
+
});
|
|
324
|
+
blocks.push(<p key={`b${b++}`}>{pnodes}</p>);
|
|
211
325
|
}
|
|
212
|
-
return
|
|
213
|
-
<p key={pi}>
|
|
214
|
-
{para.map((tk, i) => {
|
|
215
|
-
if (tk.kind === 'cite') {
|
|
216
|
-
const src = sources?.find((s) => s.num === tk.n);
|
|
217
|
-
return (
|
|
218
|
-
<a
|
|
219
|
-
key={i}
|
|
220
|
-
className="velu-chatbot__cite"
|
|
221
|
-
href={sourceHref(src) || '#'}
|
|
222
|
-
onClick={(e) => onSourceClick(e, src, onNavigate)}
|
|
223
|
-
title={citeTitle(src)}
|
|
224
|
-
>
|
|
225
|
-
{tk.n}
|
|
226
|
-
</a>
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
if (tk.kind === 'code') return <code key={i}>{tk.v}</code>;
|
|
230
|
-
const lines = tk.v.split('\n');
|
|
231
|
-
return lines.map((line, li) => (
|
|
232
|
-
<React.Fragment key={`${i}-${li}`}>
|
|
233
|
-
{li > 0 && <br />}
|
|
234
|
-
{line}
|
|
235
|
-
</React.Fragment>
|
|
236
|
-
));
|
|
237
|
-
})}
|
|
238
|
-
</p>
|
|
239
|
-
));
|
|
326
|
+
return blocks;
|
|
240
327
|
}
|
|
241
328
|
|
|
242
329
|
/* ── Header ─────────────────────────────────────────────────────────── */
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Download,
|
|
10
10
|
Bot,
|
|
11
11
|
Wind,
|
|
12
|
+
Rss,
|
|
12
13
|
} from 'lucide-react';
|
|
13
14
|
import Cluster from '../primitives/Cluster.jsx';
|
|
14
15
|
import {
|
|
@@ -42,11 +43,15 @@ import {
|
|
|
42
43
|
* getMarkdown?: () => string|Promise<string>, // override the .md-twin fetch
|
|
43
44
|
* // (instant preview: no .md twin exists —
|
|
44
45
|
* // the raw source comes from the store)
|
|
46
|
+
* basePath?: string, // subpath prefix (e.g. "/docs") for the
|
|
47
|
+
* // server-served .md twins + /mcp endpoint
|
|
45
48
|
* }} props
|
|
46
49
|
*/
|
|
47
50
|
|
|
48
|
-
// The Markdown URL for a page path (`/` → `/index.md`)
|
|
49
|
-
|
|
51
|
+
// The Markdown URL for a page path (`/` → `/index.md`), under an optional
|
|
52
|
+
// subpath prefix (e.g. "/docs" → "/docs/quickstart.md").
|
|
53
|
+
const mdUrlForPage = (url, basePath = '') =>
|
|
54
|
+
basePath + (url === '/' ? '/index.md' : `${url}.md`);
|
|
50
55
|
|
|
51
56
|
// key → { icon, label, desc, external?, apiOnly?, kind, url? }
|
|
52
57
|
const REGISTRY = {
|
|
@@ -77,6 +82,8 @@ export default function ContextMenu({
|
|
|
77
82
|
options = [],
|
|
78
83
|
onAssistant,
|
|
79
84
|
getMarkdown,
|
|
85
|
+
basePath = '',
|
|
86
|
+
rssHref,
|
|
80
87
|
}) {
|
|
81
88
|
const [open, setOpen] = React.useState(false);
|
|
82
89
|
const [copied, setCopied] = React.useState(false);
|
|
@@ -107,7 +114,7 @@ export default function ContextMenu({
|
|
|
107
114
|
copiedTimer.current = setTimeout(() => setCopied(false), 1600);
|
|
108
115
|
};
|
|
109
116
|
|
|
110
|
-
const mdUrl = mdUrlForPage(pageUrl);
|
|
117
|
+
const mdUrl = mdUrlForPage(pageUrl, basePath);
|
|
111
118
|
|
|
112
119
|
// Build the descriptor list from the configured options (skip API-only items
|
|
113
120
|
// off API pages, and any unsupported keys).
|
|
@@ -166,7 +173,7 @@ export default function ContextMenu({
|
|
|
166
173
|
setOpen(false);
|
|
167
174
|
const origin = window.location.origin;
|
|
168
175
|
const absMd = origin + mdUrl;
|
|
169
|
-
const mcpUrl = `${origin}/mcp`;
|
|
176
|
+
const mcpUrl = `${origin}${basePath}/mcp`;
|
|
170
177
|
// CLI-safe server name (siteName may contain spaces).
|
|
171
178
|
const serverName = (siteName || 'docs').trim().replace(/\s+/g, '-').toLowerCase() || 'docs';
|
|
172
179
|
switch (it.kind) {
|
|
@@ -226,7 +233,20 @@ export default function ContextMenu({
|
|
|
226
233
|
};
|
|
227
234
|
|
|
228
235
|
// Nothing to show → render nothing (keeps the page top clean).
|
|
229
|
-
if (!eyebrow && !items.length) return null;
|
|
236
|
+
if (!eyebrow && !items.length && !rssHref) return null;
|
|
237
|
+
|
|
238
|
+
const rssLink = rssHref ? (
|
|
239
|
+
<a
|
|
240
|
+
className="velu-context-menu__rss"
|
|
241
|
+
href={rssHref}
|
|
242
|
+
target="_blank"
|
|
243
|
+
rel="noopener"
|
|
244
|
+
aria-label="Subscribe to the RSS feed"
|
|
245
|
+
title="RSS feed"
|
|
246
|
+
>
|
|
247
|
+
<Rss size="1em" aria-hidden="true" />
|
|
248
|
+
</a>
|
|
249
|
+
) : null;
|
|
230
250
|
|
|
231
251
|
return (
|
|
232
252
|
<Cluster
|
|
@@ -238,6 +258,7 @@ export default function ContextMenu({
|
|
|
238
258
|
>
|
|
239
259
|
{eyebrow ? <span className="velu-context-bar__eyebrow">{eyebrow}</span> : <span />}
|
|
240
260
|
|
|
261
|
+
<div className="velu-context-bar__actions">
|
|
241
262
|
{items.length > 0 && (
|
|
242
263
|
<div ref={rootRef} className="velu-context-menu" data-open={open ? 'true' : 'false'}>
|
|
243
264
|
<div className="velu-context-menu__split">
|
|
@@ -299,6 +320,8 @@ export default function ContextMenu({
|
|
|
299
320
|
</ul>
|
|
300
321
|
</div>
|
|
301
322
|
)}
|
|
323
|
+
{rssLink}
|
|
324
|
+
</div>
|
|
302
325
|
</Cluster>
|
|
303
326
|
);
|
|
304
327
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import resolveIcon from '../lib/resolveIcon.jsx';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Update — a single changelog entry.
|
|
6
|
+
*
|
|
7
|
+
* A changelog page is just a normal docs page that contains one or more
|
|
8
|
+
* <Update> blocks; there is no special page type. Each entry renders as a
|
|
9
|
+
* two-column row: a sticky left rail with the date/label pill (+ optional
|
|
10
|
+
* description and tags) and a right column with the entry body (the MDX
|
|
11
|
+
* children — usually `##` sub-headings and prose).
|
|
12
|
+
*
|
|
13
|
+
* The section gets a stable `id` derived from the label so it is linkable
|
|
14
|
+
* and shows up in the table of contents. The id MUST match the slug the
|
|
15
|
+
* `extract-toc` remark plugin computes for the same label (see
|
|
16
|
+
* velu-cli/src/mdx-plugins/extract-toc.js → `slugifyUpdate`), so click→scroll
|
|
17
|
+
* and scroll-spy line up.
|
|
18
|
+
*
|
|
19
|
+
* `data-update-tags` (pipe-joined) is emitted so a future tag-filter can show
|
|
20
|
+
* / hide entries purely from the DOM.
|
|
21
|
+
*
|
|
22
|
+
* SSR-safe & presentational.
|
|
23
|
+
*
|
|
24
|
+
* @param {{ label?: string, date?: string, description?: string,
|
|
25
|
+
* tags?: string | string[], rss?: any, children?: React.ReactNode,
|
|
26
|
+
* className?: string }} props
|
|
27
|
+
*/
|
|
28
|
+
export function slugifyUpdate(value) {
|
|
29
|
+
const base = String(value || '')
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
32
|
+
.replace(/^-+|-+$/g, '');
|
|
33
|
+
return `update-${base || 'item'}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function toTagList(value) {
|
|
37
|
+
if (Array.isArray(value)) return value.map(String).filter(Boolean);
|
|
38
|
+
if (value == null || value === false || value === '') return [];
|
|
39
|
+
return [String(value)];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default function Update({
|
|
43
|
+
label,
|
|
44
|
+
date,
|
|
45
|
+
description,
|
|
46
|
+
tags,
|
|
47
|
+
rss, // accepted for authoring parity; RSS metadata is handled at build time
|
|
48
|
+
children,
|
|
49
|
+
className = '',
|
|
50
|
+
...rest
|
|
51
|
+
}) {
|
|
52
|
+
void rss;
|
|
53
|
+
const updateLabel = String(label ?? date ?? 'Update');
|
|
54
|
+
const anchorId = slugifyUpdate(updateLabel);
|
|
55
|
+
const tagList = toTagList(tags);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<section
|
|
59
|
+
id={anchorId}
|
|
60
|
+
data-update-label={updateLabel}
|
|
61
|
+
data-update-tags={tagList.join('|')}
|
|
62
|
+
className={`velu-update ${className}`.trim()}
|
|
63
|
+
{...rest}
|
|
64
|
+
>
|
|
65
|
+
<div className="velu-update__meta">
|
|
66
|
+
<a
|
|
67
|
+
className="velu-update__anchor"
|
|
68
|
+
href={`#${anchorId}`}
|
|
69
|
+
aria-label={`Link to ${updateLabel}`}
|
|
70
|
+
>
|
|
71
|
+
{resolveIcon('link', { size: '1em' })}
|
|
72
|
+
</a>
|
|
73
|
+
<a className="velu-update__label" href={`#${anchorId}`}>
|
|
74
|
+
{updateLabel}
|
|
75
|
+
</a>
|
|
76
|
+
{description ? (
|
|
77
|
+
<div className="velu-update__description">{String(description)}</div>
|
|
78
|
+
) : null}
|
|
79
|
+
{tagList.length ? (
|
|
80
|
+
<div className="velu-update__tags">
|
|
81
|
+
{tagList.map((tag, i) => (
|
|
82
|
+
<span key={`${tag}-${i}`} className="velu-update__tag">
|
|
83
|
+
{tag}
|
|
84
|
+
</span>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
) : null}
|
|
88
|
+
</div>
|
|
89
|
+
{children ? <div className="velu-update__content">{children}</div> : null}
|
|
90
|
+
</section>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* Changelog tag filters — shown in the right rail in place of the TOC on
|
|
2
|
+
changelog pages. Tokenized; light/dark via [data-theme]. */
|
|
3
|
+
|
|
4
|
+
.velu-changelog-filters {
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
gap: var(--s-1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.velu-changelog-filters__heading {
|
|
11
|
+
font-size: var(--f-h6);
|
|
12
|
+
font-weight: var(--weight-semibold);
|
|
13
|
+
color: var(--accent-color);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.velu-changelog-filters__list {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-wrap: wrap;
|
|
19
|
+
gap: var(--s-2);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.velu-changelog-filter {
|
|
23
|
+
cursor: pointer;
|
|
24
|
+
border-radius: 999px;
|
|
25
|
+
padding: 0.22rem 0.65rem;
|
|
26
|
+
font-size: var(--f-h6);
|
|
27
|
+
font-weight: var(--weight-medium);
|
|
28
|
+
color: var(--text-color);
|
|
29
|
+
background: color-mix(in srgb, var(--muted-color) 14%, transparent);
|
|
30
|
+
border: var(--border-width) solid transparent;
|
|
31
|
+
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.velu-changelog-filter:hover {
|
|
35
|
+
border-color: color-mix(in srgb, var(--accent-color) 40%, var(--border-color));
|
|
36
|
+
background: color-mix(in srgb, var(--accent-color) 8%, transparent);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.velu-changelog-filter--active {
|
|
40
|
+
color: var(--accent-color);
|
|
41
|
+
border-color: color-mix(in srgb, var(--accent-color) 55%, transparent);
|
|
42
|
+
background: color-mix(in srgb, var(--accent-color) 12%, transparent);
|
|
43
|
+
}
|
|
@@ -385,6 +385,63 @@
|
|
|
385
385
|
border-radius: var(--radius-sm);
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
+
/* Rendered markdown in answers */
|
|
389
|
+
.velu-chatbot__answer strong {
|
|
390
|
+
font-weight: 600;
|
|
391
|
+
}
|
|
392
|
+
.velu-chatbot__answer em {
|
|
393
|
+
font-style: italic;
|
|
394
|
+
}
|
|
395
|
+
.velu-chatbot__answer a:not(.velu-chatbot__cite) {
|
|
396
|
+
color: var(--accent-color);
|
|
397
|
+
text-decoration: underline;
|
|
398
|
+
text-underline-offset: 2px;
|
|
399
|
+
}
|
|
400
|
+
.velu-chatbot__answer h3,
|
|
401
|
+
.velu-chatbot__answer h4,
|
|
402
|
+
.velu-chatbot__answer h5,
|
|
403
|
+
.velu-chatbot__answer h6 {
|
|
404
|
+
margin: var(--s-2) 0 var(--s-4);
|
|
405
|
+
font-weight: 600;
|
|
406
|
+
line-height: var(--lh-heading, 1.3);
|
|
407
|
+
}
|
|
408
|
+
.velu-chatbot__answer h3 { font-size: var(--f-h5); }
|
|
409
|
+
.velu-chatbot__answer h4 { font-size: var(--f-h6); }
|
|
410
|
+
.velu-chatbot__answer h5,
|
|
411
|
+
.velu-chatbot__answer h6 { font-size: var(--f-h7); }
|
|
412
|
+
.velu-chatbot__answer ul,
|
|
413
|
+
.velu-chatbot__answer ol {
|
|
414
|
+
margin: 0 0 var(--s-3);
|
|
415
|
+
padding-inline-start: var(--s-1);
|
|
416
|
+
}
|
|
417
|
+
.velu-chatbot__answer li {
|
|
418
|
+
margin: 0 0 var(--s-5);
|
|
419
|
+
}
|
|
420
|
+
.velu-chatbot__answer li::marker {
|
|
421
|
+
color: var(--muted-color);
|
|
422
|
+
}
|
|
423
|
+
.velu-chatbot__pre {
|
|
424
|
+
margin: 0 0 var(--s-3);
|
|
425
|
+
padding: var(--s-3);
|
|
426
|
+
overflow-x: auto;
|
|
427
|
+
background: var(--surface-color);
|
|
428
|
+
border: var(--border-width) solid var(--border-color);
|
|
429
|
+
border-radius: var(--radius-sm);
|
|
430
|
+
}
|
|
431
|
+
.velu-chatbot__pre code {
|
|
432
|
+
padding: 0;
|
|
433
|
+
background: none;
|
|
434
|
+
border: none;
|
|
435
|
+
font-size: var(--f-h7);
|
|
436
|
+
white-space: pre;
|
|
437
|
+
}
|
|
438
|
+
.velu-chatbot__answer blockquote {
|
|
439
|
+
margin: 0 0 var(--s-3);
|
|
440
|
+
padding-inline-start: var(--s-3);
|
|
441
|
+
border-inline-start: 2px solid var(--border-color);
|
|
442
|
+
color: var(--muted-color);
|
|
443
|
+
}
|
|
444
|
+
|
|
388
445
|
/* Inline citation chip */
|
|
389
446
|
.velu-chatbot__cite {
|
|
390
447
|
display: inline-flex;
|
|
@@ -23,11 +23,37 @@
|
|
|
23
23
|
color: var(--accent-color);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
.velu-context-bar__actions {
|
|
27
|
+
display: inline-flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
gap: var(--s-2);
|
|
30
|
+
flex: none;
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
.velu-context-menu {
|
|
27
34
|
position: relative;
|
|
28
35
|
flex: none;
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
/* RSS feed link — a round icon button next to "Copy Page" (changelog pages). */
|
|
39
|
+
.velu-context-menu__rss {
|
|
40
|
+
display: inline-flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
inline-size: 2.1rem;
|
|
44
|
+
block-size: 2.1rem;
|
|
45
|
+
border: var(--border-width) solid var(--border-color);
|
|
46
|
+
border-radius: 999px;
|
|
47
|
+
color: var(--muted-color);
|
|
48
|
+
background: var(--page-bg);
|
|
49
|
+
transition: color 0.12s ease, border-color 0.12s ease, background 0.12s ease;
|
|
50
|
+
}
|
|
51
|
+
.velu-context-menu__rss:hover {
|
|
52
|
+
color: var(--accent-color);
|
|
53
|
+
border-color: var(--accent-color);
|
|
54
|
+
background: color-mix(in srgb, var(--accent-color) 8%, transparent);
|
|
55
|
+
}
|
|
56
|
+
|
|
31
57
|
/* The split button: "Copy Page" + a divider + a chevron toggle, joined as one
|
|
32
58
|
segmented control. */
|
|
33
59
|
.velu-context-menu__split {
|
|
@@ -222,6 +222,26 @@
|
|
|
222
222
|
.velu-feedback__heart--3 {
|
|
223
223
|
color: color-mix(in srgb, var(--accent-color) 82%, #fff);
|
|
224
224
|
}
|
|
225
|
+
/* Register the per-heart custom props so they're typed + animatable inside the
|
|
226
|
+
keyframes. WebKit (newer iOS Safari) won't interpolate UNregistered custom
|
|
227
|
+
properties used in @keyframes transforms — without this the hearts never move
|
|
228
|
+
and the burst is invisible. Browsers without @property ignore these rules and
|
|
229
|
+
fall back to the inline values (the prior behaviour). */
|
|
230
|
+
@property --hb-tx {
|
|
231
|
+
syntax: "<length>";
|
|
232
|
+
inherits: false;
|
|
233
|
+
initial-value: 0px;
|
|
234
|
+
}
|
|
235
|
+
@property --hb-ty {
|
|
236
|
+
syntax: "<length>";
|
|
237
|
+
inherits: false;
|
|
238
|
+
initial-value: 0px;
|
|
239
|
+
}
|
|
240
|
+
@property --hb-rot {
|
|
241
|
+
syntax: "<angle>";
|
|
242
|
+
inherits: false;
|
|
243
|
+
initial-value: 0deg;
|
|
244
|
+
}
|
|
225
245
|
@keyframes velu-feedback-hearts {
|
|
226
246
|
0% {
|
|
227
247
|
transform: translate(-50%, 0) rotate(0deg) scale(0.6);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/* Update — changelog entries. Two-column rows: a sticky left rail with the
|
|
2
|
+
date/label pill, and the entry body on the right. All values are tokens;
|
|
3
|
+
light/dark via [data-theme]. */
|
|
4
|
+
|
|
5
|
+
.velu-update {
|
|
6
|
+
display: grid;
|
|
7
|
+
grid-template-columns: minmax(8rem, 11rem) minmax(0, 1fr);
|
|
8
|
+
column-gap: var(--s1);
|
|
9
|
+
row-gap: var(--s-1);
|
|
10
|
+
align-items: start;
|
|
11
|
+
margin-block: var(--s1) var(--s3);
|
|
12
|
+
/* Clear the fixed header when an entry is jumped to via its anchor. */
|
|
13
|
+
scroll-margin-top: calc(var(--velu-header-height) + var(--s0));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.velu-update__meta {
|
|
17
|
+
position: sticky;
|
|
18
|
+
/* Stick just below the fixed header instead of sliding behind it. */
|
|
19
|
+
top: calc(var(--velu-header-height) + var(--s0));
|
|
20
|
+
align-self: start;
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
gap: var(--s-2);
|
|
24
|
+
padding-left: var(--s0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.velu-update__anchor {
|
|
28
|
+
position: absolute;
|
|
29
|
+
left: 0;
|
|
30
|
+
top: 0.3rem;
|
|
31
|
+
display: inline-flex;
|
|
32
|
+
/* Hidden until the entry is hovered/focused (like heading anchors). */
|
|
33
|
+
color: var(--muted-color);
|
|
34
|
+
opacity: 0;
|
|
35
|
+
transition: opacity 0.15s ease, color 0.15s ease;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.velu-update:hover .velu-update__anchor,
|
|
39
|
+
.velu-update:focus-within .velu-update__anchor {
|
|
40
|
+
opacity: 1;
|
|
41
|
+
color: var(--accent-color);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.velu-update__label {
|
|
45
|
+
width: fit-content;
|
|
46
|
+
border-radius: 999px;
|
|
47
|
+
padding: 0.2rem 0.6rem;
|
|
48
|
+
font-size: var(--f-h6);
|
|
49
|
+
font-weight: var(--weight-semibold);
|
|
50
|
+
line-height: 1.2;
|
|
51
|
+
color: var(--page-bg);
|
|
52
|
+
background: var(--accent-color);
|
|
53
|
+
text-decoration: none;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.velu-update__label:hover {
|
|
57
|
+
background: color-mix(in srgb, var(--accent-color) 85%, black);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.velu-update__description {
|
|
61
|
+
color: var(--muted-color);
|
|
62
|
+
font-size: var(--f-h6);
|
|
63
|
+
line-height: var(--lh-h6);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.velu-update__tags {
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-wrap: wrap;
|
|
69
|
+
gap: var(--s-2);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.velu-update__tag {
|
|
73
|
+
font-size: var(--f-small, 0.72rem);
|
|
74
|
+
color: var(--muted-color);
|
|
75
|
+
border: var(--border-width) solid var(--border-color);
|
|
76
|
+
border-radius: 999px;
|
|
77
|
+
padding: 0.1rem 0.45rem;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.velu-update__content {
|
|
81
|
+
min-width: 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.velu-update__content > :first-child {
|
|
85
|
+
margin-top: 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.velu-update__content > :last-child {
|
|
89
|
+
margin-bottom: 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Hidden by a tag filter (DOM-driven). */
|
|
93
|
+
.velu-update[hidden] {
|
|
94
|
+
display: none !important;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@media (max-width: 768px) {
|
|
98
|
+
.velu-update {
|
|
99
|
+
grid-template-columns: minmax(0, 1fr);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.velu-update__meta {
|
|
103
|
+
position: static;
|
|
104
|
+
padding-left: var(--s2);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.velu-update__anchor {
|
|
108
|
+
left: 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
package/runtime/velu-ui/index.js
CHANGED
|
@@ -21,6 +21,8 @@ export { default as Field } from './components/Field.jsx';
|
|
|
21
21
|
export { default as Prompt } from './components/Prompt.jsx';
|
|
22
22
|
export { default as Steps, Step } from './components/Steps.jsx';
|
|
23
23
|
export { default as Tree, Folder, File } from './components/Tree.jsx';
|
|
24
|
+
export { default as Update } from './components/Update.jsx';
|
|
25
|
+
export { default as ChangelogFilters } from './components/ChangelogFilters.jsx';
|
|
24
26
|
export { default as MethodBadge } from './components/MethodBadge.jsx';
|
|
25
27
|
export { default as ApiPath } from './components/ApiPath.jsx';
|
|
26
28
|
export { default as TryItBar } from './components/TryItBar.jsx';
|