@veluai/velu 0.2.20 → 0.2.24

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.
@@ -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 paragraphs = [[]];
200
- for (const tk of tokens) {
201
- if (tk.kind === 't' && tk.v.includes('\n\n')) {
202
- const split = tk.v.split('\n\n');
203
- paragraphs[paragraphs.length - 1].push({ kind: 't', v: split[0] });
204
- for (let i = 1; i < split.length; i++) {
205
- paragraphs.push([]);
206
- if (split[i]) paragraphs[paragraphs.length - 1].push({ kind: 't', v: split[i] });
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
- } else {
209
- paragraphs[paragraphs.length - 1].push(tk);
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 paragraphs.map((para, pi) => (
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
- const mdUrlForPage = (url) => (url === '/' ? '/index.md' : `${url}.md`);
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,111 @@
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
+ /* Leave room for the absolute anchor icon at left:0, plus a gap to the pill. */
25
+ padding-left: var(--s1);
26
+ }
27
+
28
+ .velu-update__anchor {
29
+ position: absolute;
30
+ left: 0;
31
+ top: 0.3rem;
32
+ display: inline-flex;
33
+ /* Hidden until the entry is hovered/focused (like heading anchors). */
34
+ color: var(--muted-color);
35
+ opacity: 0;
36
+ transition: opacity 0.15s ease, color 0.15s ease;
37
+ }
38
+
39
+ .velu-update:hover .velu-update__anchor,
40
+ .velu-update:focus-within .velu-update__anchor {
41
+ opacity: 1;
42
+ color: var(--accent-color);
43
+ }
44
+
45
+ .velu-update__label {
46
+ width: fit-content;
47
+ border-radius: 999px;
48
+ padding: 0.2rem 0.6rem;
49
+ font-size: var(--f-h6);
50
+ font-weight: var(--weight-semibold);
51
+ line-height: 1.2;
52
+ color: var(--page-bg);
53
+ background: var(--accent-color);
54
+ text-decoration: none;
55
+ }
56
+
57
+ .velu-update__label:hover {
58
+ background: color-mix(in srgb, var(--accent-color) 85%, black);
59
+ }
60
+
61
+ .velu-update__description {
62
+ color: var(--muted-color);
63
+ font-size: var(--f-h6);
64
+ line-height: var(--lh-h6);
65
+ }
66
+
67
+ .velu-update__tags {
68
+ display: flex;
69
+ flex-wrap: wrap;
70
+ gap: var(--s-2);
71
+ }
72
+
73
+ .velu-update__tag {
74
+ font-size: var(--f-small, 0.72rem);
75
+ color: var(--muted-color);
76
+ border: var(--border-width) solid var(--border-color);
77
+ border-radius: 999px;
78
+ padding: 0.1rem 0.45rem;
79
+ }
80
+
81
+ .velu-update__content {
82
+ min-width: 0;
83
+ }
84
+
85
+ .velu-update__content > :first-child {
86
+ margin-top: 0;
87
+ }
88
+
89
+ .velu-update__content > :last-child {
90
+ margin-bottom: 0;
91
+ }
92
+
93
+ /* Hidden by a tag filter (DOM-driven). */
94
+ .velu-update[hidden] {
95
+ display: none !important;
96
+ }
97
+
98
+ @media (max-width: 768px) {
99
+ .velu-update {
100
+ grid-template-columns: minmax(0, 1fr);
101
+ }
102
+
103
+ .velu-update__meta {
104
+ position: static;
105
+ padding-left: var(--s2);
106
+ }
107
+
108
+ .velu-update__anchor {
109
+ left: 0;
110
+ }
111
+ }
@@ -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';