@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.
@@ -0,0 +1,135 @@
1
+ /** Identified-reader personalization (Mintlify-compatible `user` + playground). */
2
+
3
+ export function emptyReaderIdentity() {
4
+ return { user: {}, groups: [], apiPlaygroundInputs: {} };
5
+ }
6
+
7
+ export function identityFromUserData(data) {
8
+ const empty = emptyReaderIdentity();
9
+ if (!data || typeof data !== 'object' || Array.isArray(data)) return empty;
10
+ if (typeof data.expiresAt === 'number' && data.expiresAt * 1000 <= Date.now()) return empty;
11
+ const groups = Array.isArray(data.groups)
12
+ ? [...new Set(data.groups.filter((g) => typeof g === 'string' && g.trim()).map((g) => g.trim()))].slice(0, 200)
13
+ : [];
14
+ const user = data.content && typeof data.content === 'object' && !Array.isArray(data.content) ? data.content : {};
15
+ const apiPlaygroundInputs =
16
+ data.apiPlaygroundInputs && typeof data.apiPlaygroundInputs === 'object' && !Array.isArray(data.apiPlaygroundInputs)
17
+ ? data.apiPlaygroundInputs
18
+ : {};
19
+ return { user, groups, apiPlaygroundInputs };
20
+ }
21
+
22
+ export function pageRequiredGroups(page) {
23
+ const fromFrontmatter = page?.frontmatter?.groups;
24
+ if (Array.isArray(fromFrontmatter) && fromFrontmatter.length) return fromFrontmatter;
25
+ const fromOperation = page?.operation?.groups;
26
+ if (Array.isArray(fromOperation) && fromOperation.length) return fromOperation;
27
+ return [];
28
+ }
29
+
30
+ export function filterSidebarSections(sections = [], groups = getReaderGroups(), pagesMap = {}) {
31
+ const keepItems = (items) =>
32
+ (items || []).flatMap((item) => {
33
+ if (item?.items) {
34
+ const nested = keepItems(item.items);
35
+ return nested.length ? [{ ...item, items: nested }] : [];
36
+ }
37
+ const required = Array.isArray(item?.groups) && item.groups.length
38
+ ? item.groups
39
+ : pageRequiredGroups(pagesMap[item?.href]);
40
+ return visibleToGroups(required, groups) ? [item] : [];
41
+ });
42
+ return (sections || [])
43
+ .map((section) => {
44
+ const items = keepItems(section.items);
45
+ return items.length ? { ...section, items } : null;
46
+ })
47
+ .filter(Boolean);
48
+ }
49
+
50
+ export function setReaderIdentity(identity = {}) {
51
+ const next = {
52
+ user: identity.user && typeof identity.user === 'object' && !Array.isArray(identity.user) ? identity.user : {},
53
+ groups: Array.isArray(identity.groups) ? identity.groups.filter((g) => typeof g === 'string') : [],
54
+ apiPlaygroundInputs:
55
+ identity.apiPlaygroundInputs && typeof identity.apiPlaygroundInputs === 'object' && !Array.isArray(identity.apiPlaygroundInputs)
56
+ ? identity.apiPlaygroundInputs
57
+ : {},
58
+ };
59
+ if (typeof globalThis === 'undefined') return next;
60
+ globalThis.velu = { ...(globalThis.velu || {}), ...next };
61
+ globalThis.mintlify = { ...(globalThis.mintlify || {}), user: next.user };
62
+ if (typeof globalThis.dispatchEvent === 'function' && typeof globalThis.Event === 'function') {
63
+ globalThis.dispatchEvent(new Event('velu:user'));
64
+ globalThis.dispatchEvent(new Event('mintlify:user'));
65
+ }
66
+ return next;
67
+ }
68
+
69
+ export function getReaderUser() {
70
+ return globalThis.velu?.user && typeof globalThis.velu.user === 'object' ? globalThis.velu.user : {};
71
+ }
72
+
73
+ export function getReaderGroups() {
74
+ return Array.isArray(globalThis.velu?.groups) ? globalThis.velu.groups : [];
75
+ }
76
+
77
+ export function getPlaygroundInputs() {
78
+ const inputs = globalThis.velu?.apiPlaygroundInputs;
79
+ return inputs && typeof inputs === 'object' ? inputs : {};
80
+ }
81
+
82
+ export function visibleToGroups(required, groups = getReaderGroups()) {
83
+ if (!Array.isArray(required) || required.length === 0) return true;
84
+ return groups.some((g) => required.includes(g));
85
+ }
86
+
87
+ export function initialPlaygroundValues(operation) {
88
+ const inputs = getPlaygroundInputs();
89
+ const values = { auth: {}, path: {}, query: {}, header: {}, cookie: {} };
90
+ for (const a of operation?.auth || []) {
91
+ const raw = inputs.header?.[a.name];
92
+ if (typeof raw !== 'string' || !raw) continue;
93
+ values.auth[a.name] = a.prefix && raw.startsWith(`${a.prefix} `) ? raw.slice(a.prefix.length + 1) : raw;
94
+ }
95
+ for (const loc of ['path', 'query', 'header', 'cookie']) {
96
+ for (const p of operation?.parameters?.[loc] || []) {
97
+ const raw = inputs[loc]?.[p.name];
98
+ if (typeof raw === 'string') values[loc][p.name] = raw;
99
+ }
100
+ }
101
+ if (typeof globalThis.localStorage === 'undefined') return values;
102
+ try {
103
+ const stored = JSON.parse(globalThis.localStorage.getItem(`velu-playground:${operation?.id}`) || 'null');
104
+ if (stored && typeof stored === 'object') {
105
+ for (const loc of Object.keys(values)) {
106
+ values[loc] = { ...values[loc], ...(stored[loc] || {}) };
107
+ }
108
+ }
109
+ } catch {
110
+ /* ignore malformed local overrides */
111
+ }
112
+ return values;
113
+ }
114
+
115
+ export function persistPlaygroundValues(operationId, values) {
116
+ if (typeof globalThis.localStorage === 'undefined' || !operationId) return;
117
+ try {
118
+ globalThis.localStorage.setItem(`velu-playground:${operationId}`, JSON.stringify(values || {}));
119
+ } catch {
120
+ /* quota / private mode */
121
+ }
122
+ }
123
+
124
+ export function personalizedServer(fallback) {
125
+ const vars = getPlaygroundInputs().server || {};
126
+ let url = (typeof vars.baseUrl === 'string' && vars.baseUrl)
127
+ || (typeof vars.url === 'string' && vars.url)
128
+ || fallback
129
+ || '';
130
+ for (const [key, value] of Object.entries(vars)) {
131
+ if (key === 'baseUrl' || key === 'url' || typeof value !== 'string') continue;
132
+ url = url.replaceAll(`{${key}}`, value);
133
+ }
134
+ return url;
135
+ }
@@ -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; }
package/src/navigation.js CHANGED
@@ -478,5 +478,9 @@ function itemFor(pageNode, pagesMap, ctx) {
478
478
  }
479
479
  const method = pageNode.method ?? pagesMap[href]?.operation?.method;
480
480
  if (method) item.method = method;
481
+ const groups = pageNode.groups
482
+ ?? (Array.isArray(fm?.groups) && fm.groups.length ? fm.groups : undefined)
483
+ ?? pagesMap[href]?.operation?.groups;
484
+ if (Array.isArray(groups) && groups.length) item.groups = groups;
481
485
  return item;
482
486
  }
@@ -58,6 +58,10 @@ import {
58
58
  ApiReferencePage,
59
59
  NotFound,
60
60
  VeluMark,
61
+ setReaderIdentity,
62
+ emptyReaderIdentity,
63
+ identityFromUserData,
64
+ filterSidebarSections,
61
65
  } from 'velu-ui';
62
66
  import { X, ChevronDown } from 'lucide-react';
63
67
  import ErrorBoundary from './ErrorBoundary.jsx';
@@ -107,14 +111,29 @@ function sectionToEndpoints(items) {
107
111
  else
108
112
  out.push({
109
113
  method: it.method,
110
- label: it.label,
114
+ name: it.label,
111
115
  href: it.href,
112
- icon: it.icon,
113
116
  });
114
117
  }
115
118
  return out;
116
119
  }
117
120
 
121
+ function flattenSidebarPages(sections) {
122
+ const out = [];
123
+ const walk = (items) => {
124
+ for (const it of items ?? []) {
125
+ if (it.items) walk(it.items);
126
+ else {
127
+ const page = { label: it.label, href: it.href };
128
+ if (it.description) page.description = it.description;
129
+ out.push(page);
130
+ }
131
+ }
132
+ };
133
+ for (const section of sections ?? []) walk(section.items);
134
+ return out;
135
+ }
136
+
118
137
  const CALLOUT_TYPES = [
119
138
  'note',
120
139
  'warning',
@@ -788,6 +807,53 @@ function DocsPage() {
788
807
  const location = useLocation();
789
808
  const navigate = useNavigate();
790
809
  const pathname = normalizeUrl(location.pathname);
810
+ const [readerUser, setReaderUser] = React.useState(() =>
811
+ typeof window === 'undefined' ? {} : window.velu?.user || {},
812
+ );
813
+ const [readerGroups, setReaderGroups] = React.useState(() =>
814
+ typeof window === 'undefined' ? [] : window.velu?.groups || [],
815
+ );
816
+ const [sharedSession, setSharedSession] = React.useState(null);
817
+ React.useEffect(() => {
818
+ const sync = () => {
819
+ setReaderUser(window.velu?.user || {});
820
+ setReaderGroups(Array.isArray(window.velu?.groups) ? window.velu.groups : []);
821
+ };
822
+ sync();
823
+ window.addEventListener('velu:user', sync);
824
+ return () => window.removeEventListener('velu:user', sync);
825
+ }, []);
826
+ React.useEffect(() => {
827
+ if (IS_DEV_PREVIEW || site.readerPrivate || site.guarded) return undefined;
828
+ let alive = true;
829
+ (async () => {
830
+ try {
831
+ const config = await fetch(`${BASE_PATH}/@velu-reader/personalization`, {
832
+ credentials: 'same-origin',
833
+ cache: 'no-store',
834
+ }).then((response) => (response.ok ? response.json() : null));
835
+ if (!alive || config?.method !== 'shared_session' || !config.infoUrl) return;
836
+ setSharedSession({ loginUrl: config.loginUrl || null, identified: false });
837
+ const response = await fetch(config.infoUrl, { credentials: 'include', cache: 'no-store' });
838
+ if (!alive) return;
839
+ if (!response.ok) {
840
+ setReaderIdentity(emptyReaderIdentity());
841
+ return;
842
+ }
843
+ const identity = identityFromUserData(await response.json());
844
+ setReaderIdentity(identity);
845
+ setSharedSession({
846
+ loginUrl: config.loginUrl || null,
847
+ identified: identity.groups.length > 0 || Object.keys(identity.user).length > 0,
848
+ });
849
+ } catch {
850
+ if (alive) setReaderIdentity(emptyReaderIdentity());
851
+ }
852
+ })();
853
+ return () => {
854
+ alive = false;
855
+ };
856
+ }, []);
791
857
  const entry = pages[pathname];
792
858
  // `navigation`/`pages` are module constants in dev/build (the deps never
793
859
  // change there); the instant preview's virtual-site substitute reassigns
@@ -797,6 +863,18 @@ function DocsPage() {
797
863
  () => resolve(pathname, navigation, pages),
798
864
  [pathname, navigation, pages],
799
865
  );
866
+ const visibleNav = React.useMemo(() => {
867
+ if (!nav || site.readerPrivate || site.guarded) return nav;
868
+ const sidebarSections = filterSidebarSections(nav.sidebarSections, readerGroups, pages);
869
+ const ordered = flattenSidebarPages(sidebarSections);
870
+ const idx = ordered.findIndex((page) => page.href === pathname);
871
+ return {
872
+ ...nav,
873
+ sidebarSections,
874
+ prev: idx > 0 ? ordered[idx - 1] : undefined,
875
+ next: idx >= 0 && idx < ordered.length - 1 ? ordered[idx + 1] : undefined,
876
+ };
877
+ }, [nav, pathname, readerGroups]);
800
878
 
801
879
  // Search source: Pagefind for content/excerpts, but resolve each result's
802
880
  // breadcrumb from the real NAVIGATION (a page's nav group can differ from its
@@ -849,6 +927,31 @@ function DocsPage() {
849
927
  return createPageFeedback({ apiBase, host: site.assistant?.host });
850
928
  }, []);
851
929
 
930
+ React.useEffect(() => {
931
+ if (IS_DEV_PREVIEW) return undefined;
932
+ const onReport = (e) => {
933
+ const d = e.detail || {};
934
+ pageFeedback?.submit({
935
+ helpful: false,
936
+ reason: 'Incorrect code example',
937
+ detail: d.body || d.detail || '',
938
+ });
939
+ };
940
+ window.addEventListener('velu:report-code', onReport);
941
+ return () => window.removeEventListener('velu:report-code', onReport);
942
+ }, [pageFeedback]);
943
+
944
+ React.useEffect(() => {
945
+ if (IS_DEV_PREVIEW || !pageFeedback?.config) return undefined;
946
+ let alive = true;
947
+ pageFeedback.config().then((cfg) => {
948
+ if (!alive || !cfg.codeReportsEnabled) return;
949
+ document.documentElement.dataset.veluCodeReports = 'on';
950
+ window.dispatchEvent(new Event('velu:code-reports-enabled'));
951
+ }).catch(() => {});
952
+ return () => { alive = false; };
953
+ }, [pageFeedback]);
954
+
852
955
  const frontmatter = entry?.frontmatter ?? {};
853
956
  const PageComponent = entry?.Component ?? null;
854
957
  const pageToc = entry?.toc ?? [];
@@ -1232,6 +1335,14 @@ function DocsPage() {
1232
1335
  kind: 'outlined',
1233
1336
  component: 'logout-link',
1234
1337
  });
1338
+ } else if (sharedSession?.loginUrl && !sharedSession.identified) {
1339
+ navActions.push({
1340
+ label: 'Log in',
1341
+ href: sharedSession.loginUrl,
1342
+ kind: 'outlined',
1343
+ external: true,
1344
+ component: 'login-link',
1345
+ });
1235
1346
  }
1236
1347
 
1237
1348
  // Callback ref so this rebinds when the footer mounts/unmounts.
@@ -1651,7 +1762,7 @@ function DocsPage() {
1651
1762
  the sidebar shape). */}
1652
1763
  {nav?.sidebarHasApi ? (
1653
1764
  <ApiSidebar
1654
- sections={(nav?.sidebarSections ?? []).map((s) => ({
1765
+ sections={(visibleNav?.sidebarSections ?? []).map((s) => ({
1655
1766
  title: s.title,
1656
1767
  icon: s.icon,
1657
1768
  endpoints: sectionToEndpoints(s.items),
@@ -1661,7 +1772,7 @@ function DocsPage() {
1661
1772
  />
1662
1773
  ) : (
1663
1774
  <Sidebar
1664
- sections={nav?.sidebarSections ?? []}
1775
+ sections={visibleNav?.sidebarSections ?? []}
1665
1776
  activeHref={pathname}
1666
1777
  linkComponent={RouterLink}
1667
1778
  />
@@ -1852,7 +1963,7 @@ function DocsPage() {
1852
1963
  <mdx-content key={site.theme === 'vepa' ? pathname : undefined} id={(site.theme === 'vepa' || ['thulir', 'aalam', 'nila', 'kadhir'].includes(site.theme)) && !entry?.api ? 'content' : undefined} class="velu-prose">
1853
1964
  {PageComponent ? (
1854
1965
  <ErrorBoundary key={pathname} file={entry?.relPath}>
1855
- {entry?.mdxApi ? <ApiReferencePage {...entry.mdxApi} preset={site.theme}><PageComponent /></ApiReferencePage> : <PageComponent />}
1966
+ {entry?.mdxApi ? <ApiReferencePage {...entry.mdxApi} preset={site.theme}><PageComponent user={readerUser} /></ApiReferencePage> : <PageComponent user={readerUser} />}
1856
1967
  </ErrorBoundary>
1857
1968
  ) : (
1858
1969
  <p style={{ color: 'var(--muted-color)' }}>
@@ -1885,14 +1996,14 @@ function DocsPage() {
1885
1996
  sidebar reading order of the active section. The wrapper
1886
1997
  doubles as the AskBar fade anchor on themes without a
1887
1998
  PageFeedback widget (see the observer above). */}
1888
- {chrome.articleChrome && !frontmatter.hidePagination && (nav?.prev || nav?.next) && (
1999
+ {chrome.articleChrome && !frontmatter.hidePagination && (visibleNav?.prev || visibleNav?.next) && (
1889
2000
  <div ref={setNavAnchorEl}>
1890
2001
  <PageNav
1891
2002
  id="pagination"
1892
2003
  style={['thulir', 'aalam', 'nila', 'kadhir'].includes(site.theme) ? undefined : { marginTop: 'var(--s2)' }}
1893
2004
  variant={site.theme === 'aalam' ? 'aalam' : 'default'}
1894
- prev={nav?.prev}
1895
- next={nav?.next}
2005
+ prev={visibleNav?.prev}
2006
+ next={visibleNav?.next}
1896
2007
  linkComponent={RouterLink}
1897
2008
  />
1898
2009
  </div>