@veluai/velu 0.2.46 → 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/dist/cli.js +60 -49
- package/package.json +1 -1
- package/runtime/velu-ui/components/CodeBlock.jsx +222 -39
- package/runtime/velu-ui/components/code-block.css +125 -6
- package/runtime/velu-ui/lib/page-feedback.js +22 -2
- package/runtime/velu-ui/themes/thulir.css +3 -1
- package/schema/velu.schema.json +1 -0
- package/src/navigation.js +1 -0
- package/src/runtime/App.jsx +33 -2
- package/src/runtime/client-entry.jsx +9 -3
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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'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'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
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
{
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
119
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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-
|
|
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; }
|
package/schema/velu.schema.json
CHANGED
|
@@ -320,6 +320,7 @@
|
|
|
320
320
|
"group": { "type": "string", "description": "Sidebar section heading." },
|
|
321
321
|
"icon": { "type": "string", "description": "lucide icon id." },
|
|
322
322
|
"expanded": { "type": "boolean", "description": "Start expanded." },
|
|
323
|
+
"public": { "type": "boolean", "description": "Allow unauthenticated access to this group. Per-page group restrictions still apply." },
|
|
323
324
|
"root": { "type": "string", "description": "Landing page path for the group." },
|
|
324
325
|
"openapi": { "$ref": "#/$defs/openapiRef" },
|
|
325
326
|
"api": { "$ref": "#/$defs/api" },
|
package/src/navigation.js
CHANGED
|
@@ -179,6 +179,7 @@ function normalizeGroup(g) {
|
|
|
179
179
|
if (g.root) pages.unshift({ kind: 'page', pagePath: g.root });
|
|
180
180
|
return {
|
|
181
181
|
kind: 'group',
|
|
182
|
+
...(g.public !== undefined ? { public: g.public } : {}),
|
|
182
183
|
label: g.group,
|
|
183
184
|
icon: g.icon,
|
|
184
185
|
expanded: Boolean(g.expanded),
|
package/src/runtime/App.jsx
CHANGED
|
@@ -89,6 +89,8 @@ function docsAuthActionUrl(action) {
|
|
|
89
89
|
// Resolve it to a relative base. Do not read `window` here: prerender runs
|
|
90
90
|
// this useMemo during renderToString in Node.
|
|
91
91
|
function resolvePublicApiBase(configuredBase) {
|
|
92
|
+
// Private readers must reach the router that verifies and forwards their session.
|
|
93
|
+
if (site.readerPrivate) return BASE_PATH;
|
|
92
94
|
if (!configuredBase) return null;
|
|
93
95
|
if (configuredBase !== '__VELU_SAME_ORIGIN__') return configuredBase;
|
|
94
96
|
return BASE_PATH;
|
|
@@ -801,7 +803,7 @@ function DocsPage() {
|
|
|
801
803
|
// URL path — e.g. essentials/markdown.mdx lives under the "Writing Content"
|
|
802
804
|
// group). Page rows show the path to the page; anchor rows include the page.
|
|
803
805
|
const searchDocs = React.useCallback(async (q) => {
|
|
804
|
-
const results = await pagefindSearch(q);
|
|
806
|
+
const results = site.readerRestricted ? await fetch((import.meta.env.BASE_URL || '/') + 'api/v1/public/search?q=' + encodeURIComponent(q), {credentials:'same-origin',cache:'no-store'}).then(r=>r.ok?r.json():[]) : await pagefindSearch(q);
|
|
805
807
|
if (!Array.isArray(results)) return results;
|
|
806
808
|
return results.map((r) => {
|
|
807
809
|
const res = resolve(r.href.split('#')[0], navigation, pages);
|
|
@@ -847,6 +849,31 @@ function DocsPage() {
|
|
|
847
849
|
return createPageFeedback({ apiBase, host: site.assistant?.host });
|
|
848
850
|
}, []);
|
|
849
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
|
+
|
|
850
877
|
const frontmatter = entry?.frontmatter ?? {};
|
|
851
878
|
const PageComponent = entry?.Component ?? null;
|
|
852
879
|
const pageToc = entry?.toc ?? [];
|
|
@@ -1196,7 +1223,11 @@ function DocsPage() {
|
|
|
1196
1223
|
headers: { 'content-type': 'application/json' },
|
|
1197
1224
|
body: '{}',
|
|
1198
1225
|
});
|
|
1199
|
-
if (response.ok)
|
|
1226
|
+
if (response.ok) {
|
|
1227
|
+
const result = response.status === 204 ? null : await response.json();
|
|
1228
|
+
if (result?.redirect) window.location.assign(result.redirect);
|
|
1229
|
+
else window.location.reload();
|
|
1230
|
+
}
|
|
1200
1231
|
}, []);
|
|
1201
1232
|
|
|
1202
1233
|
const navActions = [];
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import './dev-warnings.js';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { hydrateRoot } from 'react-dom/client';
|
|
3
|
+
import { createRoot, hydrateRoot } from 'react-dom/client';
|
|
4
4
|
import { BrowserRouter } from 'react-router-dom';
|
|
5
5
|
import 'velu-ui/styles.css';
|
|
6
|
-
import { site } from 'virtual:velu-site';
|
|
6
|
+
import { site, initializeReader } from 'virtual:velu-site';
|
|
7
7
|
import App from './App.jsx';
|
|
8
8
|
|
|
9
9
|
// Subpath hosting: Vite sets BASE_URL from the build's `base` (e.g. "/docs/").
|
|
@@ -31,7 +31,9 @@ function CustomScripts() {
|
|
|
31
31
|
return null;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
function mount() {
|
|
35
|
+
const renderRoot = site.guarded ? (_, children) => createRoot(document.getElementById('root')).render(children) : hydrateRoot;
|
|
36
|
+
renderRoot(
|
|
35
37
|
document.getElementById('root'),
|
|
36
38
|
<BrowserRouter basename={basename}>
|
|
37
39
|
<App />
|
|
@@ -39,6 +41,10 @@ hydrateRoot(
|
|
|
39
41
|
</BrowserRouter>
|
|
40
42
|
);
|
|
41
43
|
|
|
44
|
+
}
|
|
45
|
+
if (site.guarded) initializeReader().then(mount).catch(()=>{ document.getElementById("root").textContent="Unable to load documentation. Reload to sign in again."; });
|
|
46
|
+
else mount();
|
|
47
|
+
|
|
42
48
|
// Dev only: the SSR-inlined critical CSS (<style data-velu-ssr>) exists just
|
|
43
49
|
// to avoid FOUC on first paint. By now Vite has injected its own HMR-managed
|
|
44
50
|
// stylesheet from the `velu-ui/styles.css` import above, so drop the stale
|