@veluai/velu 0.2.47 → 0.2.49

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.
@@ -121,9 +121,11 @@ The presets are not a certification of pixel equality for every component or cus
121
121
 
122
122
  Features without full Velu equivalents require review: remaining Mintlify themes, advanced
123
123
  fonts and styling, redirects, analytics/support integrations, hosting settings,
124
- authentication, personalization, hidden-page routing, legacy version filtering,
124
+ authentication, standalone public personalization, hidden-page routing, legacy version filtering,
125
125
  complex dropdown/global navigation, AsyncAPI, manually authored `api:` pages,
126
- multi-file/remote endpoint snapshots, OpenAPI overlays and `x-mint` extensions.
126
+ multi-file/remote endpoint snapshots, and OpenAPI overlays.
127
+ OpenAPI `x-mint` / `x-mint-enum` personalization keys are rewritten to `x-velu` /
128
+ `x-velu-enum`. The generator still reads leftover `x-mint` keys in unmigrated specs.
127
129
  Access/visibility settings are reported as errors; they must be migrated before
128
130
  publishing. Unlisted content is copied but Velu currently renders navigation
129
131
  pages only. Snapshot API pages must be regenerated or edited when their specs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veluai/velu",
3
- "version": "0.2.47",
3
+ "version": "0.2.49",
4
4
  "type": "module",
5
5
  "bin": "./dist/cli.js",
6
6
  "publishConfig": {
@@ -11,6 +11,24 @@ import CodeBlock, { CodeGroup } from './CodeBlock.jsx';
11
11
  import ApiSamples from './ApiSamples.jsx';
12
12
  import Callout from './Callout.jsx';
13
13
  import { sendApiRequest } from '../lib/api-send.js';
14
+ import {
15
+ getReaderGroups,
16
+ visibleToGroups,
17
+ initialPlaygroundValues,
18
+ persistPlaygroundValues,
19
+ personalizedServer,
20
+ } from '../lib/reader-user.js';
21
+
22
+ function visibleFields(rows = [], groups) {
23
+ return rows.filter((row) => visibleToGroups(row.groups, groups));
24
+ }
25
+
26
+ function visibleEnum(row, groups) {
27
+ if (!Array.isArray(row?.enum)) return row?.enum;
28
+ const mapping = row.veluEnum || row.mintEnum;
29
+ if (!mapping) return row.enum;
30
+ return row.enum.filter((value) => visibleToGroups(mapping[String(value)], groups));
31
+ }
14
32
 
15
33
  /**
16
34
  * ApiReferencePage — the full, auto-generated reference page for one OpenAPI
@@ -25,24 +43,29 @@ import { sendApiRequest } from '../lib/api-send.js';
25
43
  *
26
44
  * @param {{ operation: object, api?: { proxy?: boolean, server?: string } }} props
27
45
  */
28
- const LOCATION_LABEL = { path: 'Path Parameters', query: 'Query Parameters', header: 'Header Parameters' };
46
+ const LOCATION_LABEL = { path: 'Path Parameters', query: 'Query Parameters', header: 'Header Parameters', cookie: 'Cookie Parameters' };
29
47
 
30
48
  export default function ApiReferencePage({ operation, samples = [], apiOperations = [], api = {}, children, preset }) {
31
49
  const [open, setOpen] = useState(false);
32
- // Which operation the playground is currently showing. Defaults to this
33
- // page's own operation; the dropdown can switch it to any sibling.
34
50
  const [activeId, setActiveId] = useState(operation.id);
51
+ const [groups, setGroups] = useState(() => getReaderGroups());
52
+ useEffect(() => {
53
+ const sync = () => setGroups(getReaderGroups());
54
+ sync();
55
+ if (typeof window === 'undefined') return undefined;
56
+ window.addEventListener('velu:user', sync);
57
+ return () => window.removeEventListener('velu:user', sync);
58
+ }, []);
35
59
 
36
- // The switchable list — fall back to just this page's operation.
37
60
  const ops =
38
61
  apiOperations.length > 0
39
- ? apiOperations
62
+ ? apiOperations.filter((o) => visibleToGroups((o.operation || o).groups, groups))
40
63
  : [{ id: operation.id, method: operation.method, title: operation.title, operation, samples, api }];
41
- const current = ops.find((o) => o.id === activeId) || { operation, samples, api };
64
+ const current = ops.find((o) => o.id === activeId) || ops[0] || { operation, samples, api };
42
65
  // Each operation carries its own (section-level) api config; fall back to
43
66
  // this page's config.
44
67
  const currentApi = current.api ?? api;
45
- const server = currentApi.server || current.operation.servers?.[0] || '';
68
+ const server = personalizedServer(currentApi.server || current.operation.servers?.[0] || '');
46
69
 
47
70
  // While the playground modal is open, close on Escape and lock the
48
71
  // page scroll so the dimmed backdrop reads as a true overlay.
@@ -94,8 +117,8 @@ export default function ApiReferencePage({ operation, samples = [], apiOperation
94
117
  )}
95
118
 
96
119
  {/* Parameters, by location */}
97
- {['path', 'query', 'header'].map((loc) => {
98
- const rows = operation.parameters?.[loc] || [];
120
+ {['path', 'query', 'header', 'cookie'].map((loc) => {
121
+ const rows = visibleFields(operation.parameters?.[loc] || [], groups);
99
122
  if (!rows.length) return null;
100
123
  return (
101
124
  <api-section key={loc} class="velu-api-page__section">
@@ -103,7 +126,7 @@ export default function ApiReferencePage({ operation, samples = [], apiOperation
103
126
  {rows.map((p, i) => (
104
127
  <Field key={i} name={p.name} type={p.type} required={p.required}>
105
128
  {p.description}
106
- {p.enum && <EnumHint values={p.enum} />}
129
+ {visibleEnum(p, groups)?.length ? <EnumHint values={visibleEnum(p, groups)} /> : null}
107
130
  </Field>
108
131
  ))}
109
132
  </api-section>
@@ -115,10 +138,10 @@ export default function ApiReferencePage({ operation, samples = [], apiOperation
115
138
  <api-section class="velu-api-page__section">
116
139
  <h2 data-component="api-section-heading">Body</h2>
117
140
  {['thulir', 'aalam', 'nila', 'kadhir'].includes(preset) && operation.body.contentType && <span className="velu-api-body-type">{operation.body.contentType}</span>}
118
- {operation.body.fields.map((f, i) => (
141
+ {visibleFields(operation.body.fields, groups).map((f, i) => (
119
142
  <Field key={i} name={f.name} type={f.type} required={f.required}>
120
143
  {f.description}
121
- {f.enum && <EnumHint values={f.enum} />}
144
+ {visibleEnum(f, groups)?.length ? <EnumHint values={visibleEnum(f, groups)} /> : null}
122
145
  </Field>
123
146
  ))}
124
147
  </api-section>
@@ -183,7 +206,7 @@ function ResponseSections({ responses, preset }) {
183
206
  if (preset === 'vepa' || ['thulir', 'aalam', 'nila', 'kadhir'].includes(preset)) return <div className="velu-vepa-responses">{responses.map((r, i) => <section key={i}>
184
207
  <div className="velu-vepa-responses__status">{r.status}{r.contentType && ` · ${r.contentType}`}</div>
185
208
  {r.description && <p>{r.description}</p>}
186
- {(['thulir', 'aalam', 'nila', 'kadhir'].includes(preset) ? [...(r.fields || [])].sort((a, b) => Number(Boolean(b.required)) - Number(Boolean(a.required))) : r.fields)?.map((f, j) => <Field key={f.name || j} name={f.name} type={f.type} required={f.required}>{f.description}</Field>)}
209
+ {(['thulir', 'aalam', 'nila', 'kadhir'].includes(preset) ? [...visibleFields(r.fields, getReaderGroups())].sort((a, b) => Number(Boolean(b.required)) - Number(Boolean(a.required))) : visibleFields(r.fields, getReaderGroups()))?.map((f, j) => <Field key={f.name || j} name={f.name} type={f.type} required={f.required}>{f.description}{visibleEnum(f, getReaderGroups())?.length ? <EnumHint values={visibleEnum(f, getReaderGroups())} /> : null}</Field>)}
187
210
  </section>)}</div>;
188
211
  return (
189
212
  <AccordionGroup className="velu-api-resp">
@@ -202,9 +225,10 @@ function ResponseSections({ responses, preset }) {
202
225
  }
203
226
  >
204
227
  {r.description && <p className="velu-api-resp__desc">{r.description}</p>}
205
- {r.fields?.map((f, j) => (
228
+ {visibleFields(r.fields, getReaderGroups()).map((f, j) => (
206
229
  <Field key={j} name={f.name} type={f.type} required={f.required}>
207
230
  {f.description}
231
+ {visibleEnum(f, getReaderGroups())?.length ? <EnumHint values={visibleEnum(f, getReaderGroups())} /> : null}
208
232
  </Field>
209
233
  ))}
210
234
  </Accordion>
@@ -226,7 +250,7 @@ function Playground({
226
250
  proxy,
227
251
  onClose,
228
252
  }) {
229
- const [values, setValues] = useState({});
253
+ const [values, setValues] = useState(() => initialPlaygroundValues(operation));
230
254
  const [bodyText, setBodyText] = useState(
231
255
  operation.body ? JSON.stringify(operation.body.example ?? {}, null, 2) : '',
232
256
  );
@@ -234,7 +258,11 @@ function Playground({
234
258
  const [resp, setResp] = useState(null);
235
259
 
236
260
  const set = (loc, name, v) =>
237
- setValues((prev) => ({ ...prev, [loc]: { ...(prev[loc] || {}), [name]: v } }));
261
+ setValues((prev) => {
262
+ const next = { ...prev, [loc]: { ...(prev[loc] || {}), [name]: v } };
263
+ persistPlaygroundValues(operation.id, next);
264
+ return next;
265
+ });
238
266
 
239
267
  const onSend = useCallback(async () => {
240
268
  setBusy(true);
@@ -251,8 +279,8 @@ function Playground({
251
279
 
252
280
  const sections = [];
253
281
  if (operation.auth?.length) sections.push(['auth', 'Authorization', operation.auth]);
254
- for (const loc of ['path', 'query', 'header']) {
255
- const rows = operation.parameters?.[loc] || [];
282
+ for (const loc of ['path', 'query', 'header', 'cookie']) {
283
+ const rows = visibleFields(operation.parameters?.[loc] || [], getReaderGroups());
256
284
  if (rows.length) sections.push([loc, LOCATION_LABEL[loc].replace(' Parameters', ''), rows]);
257
285
  }
258
286
 
@@ -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,6 +49,17 @@ export { default as Search } from './components/Search.jsx';
49
49
  export { default as pagefindSearch } from './lib/pagefind.js';
50
50
  export { createDocsAssistant } from './lib/docs-assistant.js';
51
51
  export { createPageFeedback } from './lib/page-feedback.js';
52
+ export {
53
+ setReaderIdentity,
54
+ emptyReaderIdentity,
55
+ identityFromUserData,
56
+ getReaderUser,
57
+ getReaderGroups,
58
+ getPlaygroundInputs,
59
+ visibleToGroups,
60
+ filterSidebarSections,
61
+ pageRequiredGroups,
62
+ } from './lib/reader-user.js';
52
63
  export { default as Image } from './components/Image.jsx';
53
64
  export {
54
65
  default as CodeBlock,
@@ -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;