@veluai/velu 0.2.47 → 0.2.48

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veluai/velu",
3
- "version": "0.2.47",
3
+ "version": "0.2.48",
4
4
  "type": "module",
5
5
  "bin": "./dist/cli.js",
6
6
  "publishConfig": {
@@ -3,6 +3,7 @@ import React, {
3
3
  isValidElement,
4
4
  useCallback,
5
5
  useEffect,
6
+ useId,
6
7
  useRef,
7
8
  useState,
8
9
  } from 'react';
@@ -152,9 +153,165 @@ function CopyButton({ code }) {
152
153
  );
153
154
  }
154
155
 
155
- function CodeBody({ code, language, lineNumbers, highlight }) {
156
+ const REPORT_REASONS = [
157
+ 'This code is incorrect',
158
+ 'This example is outdated',
159
+ "This example doesn't run",
160
+ 'Something else',
161
+ ];
162
+
163
+ function reportBody({ code, language, label, reason, detail }) {
164
+ const pageUrl =
165
+ typeof window !== 'undefined' ? window.location.href.split('#')[0] : '';
166
+ return [
167
+ reason,
168
+ detail,
169
+ label ? `Example: ${label}` : '',
170
+ language && language !== 'text' ? `Language: ${language}` : '',
171
+ pageUrl,
172
+ code ? `\`\`\`${language && language !== 'text' ? language : ''}\n${code}\n\`\`\`` : '',
173
+ ]
174
+ .filter(Boolean)
175
+ .join('\n\n');
176
+ }
177
+
178
+ function ReportIssue({ open, onOpen, onClose }) {
179
+ return (
180
+ <button
181
+ type="button"
182
+ className="velu-code-block__report-btn"
183
+ data-component="code-block-report-button"
184
+ aria-label={open ? 'Close report' : 'Report issue'}
185
+ title="Report issue"
186
+ aria-expanded={open}
187
+ onClick={() => (open ? onClose() : onOpen())}
188
+ >
189
+ {resolveIcon(open ? 'x' : 'flag', { size: '1em' })}
190
+ </button>
191
+ );
192
+ }
193
+
194
+ function ReportForm({ code, language, label, onClose }) {
195
+ const reasonName = useId();
196
+ const [reason, setReason] = useState('');
197
+ const [detail, setDetail] = useState('');
198
+ const [sent, setSent] = useState(false);
199
+ const canSubmit = Boolean(reason || detail.trim());
200
+
201
+ useEffect(() => {
202
+ const onKey = (e) => {
203
+ if (e.key === 'Escape') onClose();
204
+ };
205
+ document.addEventListener('keydown', onKey);
206
+ return () => document.removeEventListener('keydown', onKey);
207
+ }, [onClose]);
208
+
209
+ useEffect(() => {
210
+ if (!sent) return;
211
+ const t = setTimeout(onClose, 1600);
212
+ return () => clearTimeout(t);
213
+ }, [sent, onClose]);
214
+
215
+ function submit(e) {
216
+ e.preventDefault();
217
+ if (!canSubmit || sent) return;
218
+ const snippet = normalize(code);
219
+ const payload = {
220
+ code: snippet,
221
+ language,
222
+ label,
223
+ reason,
224
+ detail: detail.trim(),
225
+ };
226
+ payload.body = reportBody(payload);
227
+ if (typeof window !== 'undefined') {
228
+ window.dispatchEvent(new CustomEvent('velu:report-code', { detail: payload }));
229
+ }
230
+ setSent(true);
231
+ }
232
+
233
+ return (
234
+ <form className="velu-code-block__report" onSubmit={submit}>
235
+ {sent ? (
236
+ <div className="velu-code-block__report-thanks" role="status">
237
+ {resolveIcon('check', { size: '1.1em' })}
238
+ <span>Thanks — we&apos;ll take a look.</span>
239
+ </div>
240
+ ) : (
241
+ <>
242
+ <p className="velu-code-block__report-title">
243
+ Report an issue with this example
244
+ </p>
245
+ <fieldset className="velu-code-block__report-reasons">
246
+ <legend className="velu-code-block__report-legend">
247
+ What&apos;s wrong?
248
+ </legend>
249
+ {REPORT_REASONS.map((r) => (
250
+ <label key={r} className="velu-code-block__report-reason">
251
+ <input
252
+ type="radio"
253
+ name={reasonName}
254
+ value={r}
255
+ checked={reason === r}
256
+ onChange={() => setReason(r)}
257
+ />
258
+ <span>{r}</span>
259
+ </label>
260
+ ))}
261
+ </fieldset>
262
+ <textarea
263
+ className="velu-code-block__report-detail"
264
+ placeholder="What should we fix?"
265
+ rows={3}
266
+ value={detail}
267
+ onChange={(e) => setDetail(e.target.value)}
268
+ />
269
+ <div className="velu-code-block__report-actions">
270
+ <button
271
+ type="button"
272
+ className="velu-code-block__report-cancel"
273
+ onClick={onClose}
274
+ >
275
+ Cancel
276
+ </button>
277
+ <button
278
+ type="submit"
279
+ className="velu-code-block__report-submit"
280
+ disabled={!canSubmit}
281
+ >
282
+ Submit
283
+ </button>
284
+ </div>
285
+ </>
286
+ )}
287
+ </form>
288
+ );
289
+ }
290
+
291
+ function useCodeReportsEnabled() {
292
+ const [enabled, setEnabled] = useState(false);
293
+ useEffect(() => {
294
+ // `velu dev` is a local preview — reports only exist on the published site.
295
+ if (import.meta.env.DEV) return undefined;
296
+ const sync = () => {
297
+ setEnabled(
298
+ typeof document !== 'undefined' &&
299
+ document.documentElement.dataset.veluCodeReports === 'on',
300
+ );
301
+ };
302
+ sync();
303
+ window.addEventListener('velu:code-reports-enabled', sync);
304
+ return () => window.removeEventListener('velu:code-reports-enabled', sync);
305
+ }, []);
306
+ return enabled;
307
+ }
308
+
309
+ function CodeBody({ code, language, lineNumbers, highlight, label }) {
310
+ const reportsEnabled = useCodeReportsEnabled();
156
311
  const canonical = resolveLang(language) || 'text';
157
312
  const highlightSet = parseHighlight(highlight);
313
+ const [reportOpen, setReportOpen] = useState(false);
314
+ const closeReport = useCallback(() => setReportOpen(false), []);
158
315
 
159
316
  // Initial render uses 'text' (no highlighting) — this guarantees SSR
160
317
  // and the first client render produce identical HTML regardless of
@@ -175,45 +332,67 @@ function CodeBody({ code, language, lineNumbers, highlight }) {
175
332
  }, [canonical]);
176
333
 
177
334
  return (
178
- <div className="velu-code-block__body">
179
- <CopyButton code={normalize(code)} />
180
- <Highlight
181
- code={normalize(code)}
182
- language={renderLang}
183
- theme={NO_THEME}
184
- >
185
- {({ tokens, getLineProps, getTokenProps }) => (
186
- <pre className="velu-code-block__pre">
187
- {tokens.map((line, i) => {
188
- const lineProps = getLineProps({ line });
189
- const isHi = highlightSet?.has(i + 1);
190
- return (
191
- <div
192
- key={i}
193
- {...lineProps}
194
- className={`velu-code-block__line ${
195
- lineProps.className || ''
196
- } ${isHi ? 'velu-code-block__line--highlight' : ''}`.trim()}
197
- >
198
- {lineNumbers && (
199
- <span
200
- className="velu-code-block__line-number"
201
- aria-hidden="true"
202
- >
203
- {i + 1}
204
- </span>
205
- )}
206
- <span className="velu-code-block__line-code">
207
- {line.map((token, j) => (
208
- <span key={j} {...getTokenProps({ token })} />
209
- ))}
210
- </span>
211
- </div>
212
- );
213
- })}
214
- </pre>
335
+ <div
336
+ className={`velu-code-block__body${
337
+ reportOpen ? ' velu-code-block__body--report' : ''
338
+ }`}
339
+ >
340
+ <div className="velu-code-block__actions">
341
+ {reportsEnabled && (
342
+ <ReportIssue
343
+ open={reportOpen}
344
+ onOpen={() => setReportOpen(true)}
345
+ onClose={closeReport}
346
+ />
215
347
  )}
216
- </Highlight>
348
+ {(!reportOpen || !reportsEnabled) && <CopyButton code={normalize(code)} />}
349
+ </div>
350
+ {reportsEnabled && reportOpen ? (
351
+ <ReportForm
352
+ code={code}
353
+ language={language}
354
+ label={label}
355
+ onClose={closeReport}
356
+ />
357
+ ) : (
358
+ <Highlight
359
+ code={normalize(code)}
360
+ language={renderLang}
361
+ theme={NO_THEME}
362
+ >
363
+ {({ tokens, getLineProps, getTokenProps }) => (
364
+ <pre className="velu-code-block__pre">
365
+ {tokens.map((line, i) => {
366
+ const lineProps = getLineProps({ line });
367
+ const isHi = highlightSet?.has(i + 1);
368
+ return (
369
+ <div
370
+ key={i}
371
+ {...lineProps}
372
+ className={`velu-code-block__line ${
373
+ lineProps.className || ''
374
+ } ${isHi ? 'velu-code-block__line--highlight' : ''}`.trim()}
375
+ >
376
+ {lineNumbers && (
377
+ <span
378
+ className="velu-code-block__line-number"
379
+ aria-hidden="true"
380
+ >
381
+ {i + 1}
382
+ </span>
383
+ )}
384
+ <span className="velu-code-block__line-code">
385
+ {line.map((token, j) => (
386
+ <span key={j} {...getTokenProps({ token })} />
387
+ ))}
388
+ </span>
389
+ </div>
390
+ );
391
+ })}
392
+ </pre>
393
+ )}
394
+ </Highlight>
395
+ )}
217
396
  </div>
218
397
  );
219
398
  }
@@ -254,6 +433,7 @@ export function CodeBlock({
254
433
  language={language}
255
434
  lineNumbers={lineNumbers}
256
435
  highlight={highlight}
436
+ label={label}
257
437
  />
258
438
  </code-block>
259
439
  );
@@ -333,6 +513,8 @@ export function CodeGroup({
333
513
  lineNumbers = false,
334
514
  highlight,
335
515
  children: code,
516
+ title,
517
+ filename,
336
518
  } = block;
337
519
  const cls = `velu-code-block velu-code-block--group ${className}`.trim();
338
520
 
@@ -391,6 +573,7 @@ export function CodeGroup({
391
573
  language={language}
392
574
  lineNumbers={lineNumbers}
393
575
  highlight={highlight}
576
+ label={title || filename}
394
577
  />
395
578
  </code-group>
396
579
  );
@@ -107,21 +107,31 @@
107
107
  }
108
108
 
109
109
  /* Body: the code area, monospace, horizontally scrollable for long lines.
110
- Positioned so the copy button can absolutely anchor to its top-left. */
110
+ Positioned so the action cluster can absolutely anchor to its top-right. */
111
111
  .velu-code-block__body {
112
112
  position: relative;
113
113
  background: var(--page-bg);
114
114
  padding: var(--s0);
115
+ padding-inline-end: calc(var(--s0) + 4.25em);
115
116
  overflow-x: auto;
116
117
  }
118
+ .velu-code-block__body--report {
119
+ overflow: visible;
120
+ padding-inline-end: var(--s0);
121
+ }
117
122
 
118
- /* Copy button — always visible. Frosted background so it remains
119
- legible over whatever code sits beneath it. */
120
- .velu-code-block__copy {
123
+ /* Copy + report — frosted cluster so they stay legible over the code. */
124
+ .velu-code-block__actions {
121
125
  position: absolute;
122
126
  inset-block-start: var(--s-2);
123
127
  inset-inline-end: var(--s-2);
124
- z-index: 1;
128
+ z-index: 2;
129
+ display: flex;
130
+ align-items: center;
131
+ gap: var(--s-3);
132
+ }
133
+ .velu-code-block__copy,
134
+ .velu-code-block__report-btn {
125
135
  display: inline-flex;
126
136
  align-items: center;
127
137
  justify-content: center;
@@ -136,8 +146,117 @@
136
146
  color: var(--muted-color);
137
147
  cursor: pointer;
138
148
  }
139
- .velu-code-block__copy:hover {
149
+ .velu-code-block__copy:hover,
150
+ .velu-code-block__report-btn:hover {
151
+ color: var(--text-color);
152
+ }
153
+
154
+ /* Report-issue panel replaces the snippet so a wrong example can be flagged
155
+ without leaving the block. */
156
+ .velu-code-block__report {
157
+ display: flex;
158
+ flex-direction: column;
159
+ gap: var(--s-2);
160
+ min-block-size: 8rem;
161
+ padding-inline-end: 2em;
162
+ margin: 0;
163
+ border: 0;
164
+ }
165
+ .velu-code-block__report-title {
166
+ margin: 0;
167
+ font-size: var(--f-h5);
168
+ font-weight: var(--weight-medium);
169
+ line-height: var(--lh-h5);
170
+ color: var(--text-color);
171
+ }
172
+ .velu-code-block__report-reasons {
173
+ display: flex;
174
+ flex-direction: column;
175
+ gap: var(--s-3);
176
+ margin: 0;
177
+ padding: 0;
178
+ border: 0;
179
+ }
180
+ .velu-code-block__report-legend {
181
+ padding: 0;
182
+ margin-block-end: var(--s-3);
183
+ font-size: var(--f-h6);
184
+ color: var(--muted-color);
185
+ }
186
+ .velu-code-block__report-reason {
187
+ display: flex;
188
+ align-items: center;
189
+ gap: var(--s-2);
190
+ font-size: var(--f-h6);
191
+ line-height: var(--lh-h6);
192
+ color: var(--text-color);
193
+ cursor: pointer;
194
+ }
195
+ .velu-code-block__report-reason input {
196
+ margin: 0;
197
+ accent-color: var(--accent-color);
198
+ }
199
+ .velu-code-block__report-detail {
200
+ display: block;
201
+ width: 100%;
202
+ resize: vertical;
203
+ min-block-size: 4.5em;
204
+ padding: var(--s-2) var(--s-1);
205
+ background: var(--surface-color);
206
+ border: var(--border-width) solid var(--border-color);
207
+ border-radius: var(--radius-sm);
208
+ color: var(--text-color);
209
+ font: inherit;
210
+ font-size: var(--f-h6);
211
+ line-height: var(--lh-h6);
212
+ }
213
+ .velu-code-block__report-detail:focus {
214
+ outline: 2px solid var(--accent-color);
215
+ outline-offset: -1px;
216
+ }
217
+ .velu-code-block__report-detail::placeholder {
218
+ color: var(--muted-color);
219
+ }
220
+ .velu-code-block__report-actions {
221
+ display: flex;
222
+ justify-content: flex-end;
223
+ gap: var(--s-2);
224
+ }
225
+ .velu-code-block__report-cancel,
226
+ .velu-code-block__report-submit {
227
+ padding: var(--s-3) var(--s-1);
228
+ border-radius: var(--radius-sm);
229
+ font: inherit;
230
+ font-size: var(--f-h6);
231
+ font-weight: var(--weight-medium);
232
+ line-height: var(--lh-h6);
233
+ cursor: pointer;
234
+ }
235
+ .velu-code-block__report-cancel {
236
+ background: transparent;
237
+ border: var(--border-width) solid var(--border-color);
238
+ color: var(--text-color);
239
+ }
240
+ .velu-code-block__report-submit {
241
+ background: var(--accent-color);
242
+ border: var(--border-width) solid var(--accent-color);
243
+ color: var(--page-bg);
244
+ }
245
+ .velu-code-block__report-submit:disabled {
246
+ opacity: 0.45;
247
+ cursor: not-allowed;
248
+ }
249
+ .velu-code-block__report-thanks {
250
+ display: flex;
251
+ align-items: center;
252
+ gap: var(--s-2);
253
+ min-block-size: 6rem;
140
254
  color: var(--text-color);
255
+ font-size: var(--f-h5);
256
+ font-weight: var(--weight-medium);
257
+ }
258
+ .velu-code-block__report-thanks svg {
259
+ color: var(--accent-color);
141
260
  }
142
261
 
143
262
  .velu-code-block__pre {
@@ -49,7 +49,9 @@ export function createPageFeedback({ apiBase, host } = {}) {
49
49
  // sensible default when the reader didn't pick a reason (e.g. on "Yes").
50
50
  reason_text:
51
51
  (reason && String(reason).trim()) || (helpful ? 'Helpful' : 'Not helpful'),
52
- details: detail ? String(detail) : null,
52
+ details: detail
53
+ ? String(detail).slice(0, 16000)
54
+ : null,
53
55
  };
54
56
  try {
55
57
  await fetch(url, {
@@ -66,7 +68,25 @@ export function createPageFeedback({ apiBase, host } = {}) {
66
68
  }
67
69
  }
68
70
 
69
- return { submit };
71
+ /**
72
+ * Paid-plan flag for the code-block Report issue control. Fail closed.
73
+ */
74
+ async function config() {
75
+ const cfgUrl = `${base}/api/v1/public/feedback/config`;
76
+ try {
77
+ const res = await fetch(cfgUrl, {
78
+ credentials: 'include',
79
+ headers: { 'X-Velu-Site-Host': siteHost(host) },
80
+ });
81
+ if (!res.ok) return { codeReportsEnabled: false };
82
+ const data = await res.json();
83
+ return { codeReportsEnabled: !!data.code_reports_enabled };
84
+ } catch {
85
+ return { codeReportsEnabled: false };
86
+ }
87
+ }
88
+
89
+ return { submit, config };
70
90
  }
71
91
 
72
92
  export default createPageFeedback;
@@ -304,7 +304,9 @@
304
304
  :is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) .velu-code-block { border-radius: 16px; border-color: rgb(from var(--vepa-gray-950) r g b / 10%); }
305
305
  :is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) .velu-code-block :is(pre,code) { font: 400 14px/24px var(--font-mono); }
306
306
  :is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) .velu-code-block__body { padding: 16px; border-radius: 16px; }
307
- :is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) .velu-code-block__copy { border: 0; background: transparent; width: 24px; height: 24px; top: 12px; right: 12px; }
307
+ :is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) .velu-code-block__actions { top: 12px; right: 12px; }
308
+ :is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) .velu-code-block__copy,
309
+ :is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) .velu-code-block__report-btn { border: 0; background: transparent; width: 24px; height: 24px; }
308
310
  :is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) .velu-code-block__header { height: 36px; padding: 0 12px; align-items: stretch; }
309
311
  :is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) .velu-code-block__tabs { gap: 16px; }
310
312
  :is([data-site-theme='thulir'],[data-site-theme='aalam'],[data-site-theme='nila'],[data-site-theme='kadhir']) .velu-code-block__tab { padding: 0 6px; border-radius: 0; font: 400 12px/20px var(--font-sans); height: 36px; }
@@ -849,6 +849,31 @@ function DocsPage() {
849
849
  return createPageFeedback({ apiBase, host: site.assistant?.host });
850
850
  }, []);
851
851
 
852
+ React.useEffect(() => {
853
+ if (IS_DEV_PREVIEW) return undefined;
854
+ const onReport = (e) => {
855
+ const d = e.detail || {};
856
+ pageFeedback?.submit({
857
+ helpful: false,
858
+ reason: 'Incorrect code example',
859
+ detail: d.body || d.detail || '',
860
+ });
861
+ };
862
+ window.addEventListener('velu:report-code', onReport);
863
+ return () => window.removeEventListener('velu:report-code', onReport);
864
+ }, [pageFeedback]);
865
+
866
+ React.useEffect(() => {
867
+ if (IS_DEV_PREVIEW || !pageFeedback?.config) return undefined;
868
+ let alive = true;
869
+ pageFeedback.config().then((cfg) => {
870
+ if (!alive || !cfg.codeReportsEnabled) return;
871
+ document.documentElement.dataset.veluCodeReports = 'on';
872
+ window.dispatchEvent(new Event('velu:code-reports-enabled'));
873
+ }).catch(() => {});
874
+ return () => { alive = false; };
875
+ }, [pageFeedback]);
876
+
852
877
  const frontmatter = entry?.frontmatter ?? {};
853
878
  const PageComponent = entry?.Component ?? null;
854
879
  const pageToc = entry?.toc ?? [];