@veluai/velu 0.2.31 → 0.2.33
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
|
@@ -179,8 +179,9 @@ function citeTitle(s) {
|
|
|
179
179
|
return seg.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
/* Source click: SPA-navigate same-origin targets via onNavigate (
|
|
183
|
-
|
|
182
|
+
/* Source click: SPA-navigate same-origin targets via onNavigate (host decides
|
|
183
|
+
whether to keep the panel open — desktop multi-cite hops vs mobile sheet
|
|
184
|
+
close); otherwise let the <a href> navigate. */
|
|
184
185
|
function onSourceClick(e, s, onNavigate) {
|
|
185
186
|
const href = sourceHref(s);
|
|
186
187
|
if (!href) { e.preventDefault(); return; }
|
|
@@ -189,7 +190,15 @@ function onSourceClick(e, s, onNavigate) {
|
|
|
189
190
|
const u = new URL(href, window.location.origin);
|
|
190
191
|
if (u.origin === window.location.origin) {
|
|
191
192
|
e.preventDefault();
|
|
192
|
-
|
|
193
|
+
// Prefer the docs route path (e.g. "/installation") over the absolute
|
|
194
|
+
// URL pathname ("/docs/installation"). React Router's basename is
|
|
195
|
+
// already "/docs" on subpath hosts — passing the full pathname makes
|
|
196
|
+
// navigate() produce /docs/docs/... and 404.
|
|
197
|
+
const route =
|
|
198
|
+
s?.path && s.path.startsWith('/')
|
|
199
|
+
? `${s.path}${u.search || ''}${u.hash || ''}`
|
|
200
|
+
: `${u.pathname}${u.search}${u.hash}`;
|
|
201
|
+
onNavigate(route);
|
|
193
202
|
}
|
|
194
203
|
} catch { /* malformed — fall back to href */ }
|
|
195
204
|
}
|
package/src/runtime/App.jsx
CHANGED
|
@@ -71,6 +71,13 @@ const IS_DEV_PREVIEW = import.meta.env.DEV;
|
|
|
71
71
|
// root-relative and need this prefix to resolve under a subpath.
|
|
72
72
|
const BASE_PATH = (import.meta.env.BASE_URL || '/').replace(/\/$/, '');
|
|
73
73
|
|
|
74
|
+
function docsAuthActionUrl(action) {
|
|
75
|
+
const url = new URL(window.location.href);
|
|
76
|
+
url.searchParams.set('__velu_auth', action);
|
|
77
|
+
url.hash = '';
|
|
78
|
+
return url.toString();
|
|
79
|
+
}
|
|
80
|
+
|
|
74
81
|
// Flatten a sidebar section's items (which may nest into sub-groups) into the
|
|
75
82
|
// flat endpoint list <ApiSidebar> wants. API tabs are flat in practice; this
|
|
76
83
|
// only guards the rare case of a normal nested group living beside generated
|
|
@@ -782,7 +789,10 @@ function DocsPage() {
|
|
|
782
789
|
// (dev preview, or a site that hasn't enabled the assistant).
|
|
783
790
|
const assistant = React.useMemo(() => {
|
|
784
791
|
if (IS_DEV_PREVIEW) return null;
|
|
785
|
-
const
|
|
792
|
+
const configuredBase = site.assistant?.apiBase;
|
|
793
|
+
const apiBase = configuredBase === '__VELU_SAME_ORIGIN__'
|
|
794
|
+
? `${window.location.origin}${BASE_PATH}`
|
|
795
|
+
: configuredBase;
|
|
786
796
|
if (!apiBase) return null;
|
|
787
797
|
return createDocsAssistant({ apiBase, host: site.assistant?.host });
|
|
788
798
|
}, []);
|
|
@@ -806,7 +816,10 @@ function DocsPage() {
|
|
|
806
816
|
// the widget stays purely visual.
|
|
807
817
|
const pageFeedback = React.useMemo(() => {
|
|
808
818
|
if (IS_DEV_PREVIEW) return null;
|
|
809
|
-
const
|
|
819
|
+
const configuredBase = site.assistant?.apiBase;
|
|
820
|
+
const apiBase = configuredBase === '__VELU_SAME_ORIGIN__'
|
|
821
|
+
? `${window.location.origin}${BASE_PATH}`
|
|
822
|
+
: configuredBase;
|
|
810
823
|
if (!apiBase) return null;
|
|
811
824
|
return createPageFeedback({ apiBase, host: site.assistant?.host });
|
|
812
825
|
}, []);
|
|
@@ -1095,6 +1108,27 @@ function DocsPage() {
|
|
|
1095
1108
|
// Header top-right actions, built from velu.json's `navbar`: secondary
|
|
1096
1109
|
// `links` (plain text links) then the `primary` CTA (a filled button, or an
|
|
1097
1110
|
// outlined GitHub button). Empty when unconfigured — no placeholder buttons.
|
|
1111
|
+
const [privateAccess, setPrivateAccess] = React.useState(false);
|
|
1112
|
+
React.useEffect(() => {
|
|
1113
|
+
if (IS_DEV_PREVIEW) return undefined;
|
|
1114
|
+
let alive = true;
|
|
1115
|
+
fetch(docsAuthActionUrl('session'), { credentials: 'same-origin' })
|
|
1116
|
+
.then((response) => response.ok ? response.json() : null)
|
|
1117
|
+
.then((value) => { if (alive) setPrivateAccess(value?.private && value?.authenticated); })
|
|
1118
|
+
.catch(() => {});
|
|
1119
|
+
return () => { alive = false; };
|
|
1120
|
+
}, []);
|
|
1121
|
+
|
|
1122
|
+
const logoutPrivateAccess = React.useCallback(async () => {
|
|
1123
|
+
const response = await fetch(docsAuthActionUrl('logout'), {
|
|
1124
|
+
method: 'POST',
|
|
1125
|
+
credentials: 'same-origin',
|
|
1126
|
+
headers: { 'content-type': 'application/json' },
|
|
1127
|
+
body: '{}',
|
|
1128
|
+
});
|
|
1129
|
+
if (response.ok) window.location.reload();
|
|
1130
|
+
}, []);
|
|
1131
|
+
|
|
1098
1132
|
const navActions = [];
|
|
1099
1133
|
for (const l of site.navbar?.links ?? []) {
|
|
1100
1134
|
navActions.push({
|
|
@@ -1115,6 +1149,13 @@ function DocsPage() {
|
|
|
1115
1149
|
external: p.external,
|
|
1116
1150
|
});
|
|
1117
1151
|
}
|
|
1152
|
+
if (privateAccess) {
|
|
1153
|
+
navActions.push({
|
|
1154
|
+
label: 'Log out',
|
|
1155
|
+
onClick: logoutPrivateAccess,
|
|
1156
|
+
kind: 'outlined',
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1118
1159
|
|
|
1119
1160
|
const footerRef = React.useRef(null);
|
|
1120
1161
|
const [footerOverlap, setFooterOverlap] = React.useState(0);
|
|
@@ -1277,6 +1318,16 @@ function DocsPage() {
|
|
|
1277
1318
|
`inset-block-end: 0` override still wins. */
|
|
1278
1319
|
'--velu-aside-bottom': `calc(${footerOverlap}px + var(--s4))`,
|
|
1279
1320
|
}}
|
|
1321
|
+
// Close the mobile drawer as soon as a nav link is activated.
|
|
1322
|
+
// The pathname effect below also closes it on route change; this
|
|
1323
|
+
// covers same-route hash jumps and feels snappier on touch devices.
|
|
1324
|
+
onClick={(e) => {
|
|
1325
|
+
if (!drawerOpen) return;
|
|
1326
|
+
const a = e.target.closest?.('a[href]');
|
|
1327
|
+
if (!a) return;
|
|
1328
|
+
setDrawerOpen(false);
|
|
1329
|
+
setNavOpen(false);
|
|
1330
|
+
}}
|
|
1280
1331
|
>
|
|
1281
1332
|
{/* Drawer body — Stack with 32px gap composes the slots
|
|
1282
1333
|
(topbar / tab-dropdown / context / nav). The aside is a flex
|
|
@@ -1732,7 +1783,27 @@ function DocsPage() {
|
|
|
1732
1783
|
loadConversation={assistant?.loadConversation}
|
|
1733
1784
|
suggestions={site.assistant?.suggestions}
|
|
1734
1785
|
onNavigate={(to) => {
|
|
1735
|
-
|
|
1786
|
+
// Strip the Vite base path so react-router basename is not applied
|
|
1787
|
+
// twice (e.g. "/docs/installation" → "/installation" when
|
|
1788
|
+
// BASE_PATH="/docs"). Absolute Ask-AI citation URLs include /docs.
|
|
1789
|
+
let dest = to || '/';
|
|
1790
|
+
if (BASE_PATH) {
|
|
1791
|
+
if (dest === BASE_PATH || dest === `${BASE_PATH}/`) dest = '/';
|
|
1792
|
+
else if (dest.startsWith(`${BASE_PATH}/`)) {
|
|
1793
|
+
dest = dest.slice(BASE_PATH.length) || '/';
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
navigate(dest);
|
|
1797
|
+
// Ask AI becomes a bottom sheet below 1024px (chatbot.css) and covers
|
|
1798
|
+
// the article — close it so the user lands on the linked page, not
|
|
1799
|
+
// behind the sheet. Desktop keeps the panel open so multi-cite hops
|
|
1800
|
+
// stay convenient.
|
|
1801
|
+
if (
|
|
1802
|
+
typeof window !== 'undefined' &&
|
|
1803
|
+
window.matchMedia('(max-width: 1024px)').matches
|
|
1804
|
+
) {
|
|
1805
|
+
setChatOpen(false);
|
|
1806
|
+
}
|
|
1736
1807
|
// After the destination page mounts, scroll to the cited section
|
|
1737
1808
|
// (its heading id) — or the top of the page when there's no anchor.
|
|
1738
1809
|
const hash = to.includes('#') ? to.slice(to.indexOf('#') + 1) : '';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { PageHeader, ThemeToggle } from 'velu-ui';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Static private-reader shell. This is rendered by the same Vite SSR bundle as
|
|
6
|
+
* normal documentation pages, so its header, logo, and theme control stay in
|
|
7
|
+
* lockstep with velu-ui. Authentication behavior is attached by the generated
|
|
8
|
+
* page's nonce-bearing script because the protected client bundle is not
|
|
9
|
+
* available before login.
|
|
10
|
+
*/
|
|
11
|
+
export default function PrivateLoginPage({ site }) {
|
|
12
|
+
const logo = site.logo
|
|
13
|
+
? {
|
|
14
|
+
light: '?__velu_auth=logo-light',
|
|
15
|
+
dark: '?__velu_auth=logo-dark',
|
|
16
|
+
mono: site.logo.mono,
|
|
17
|
+
}
|
|
18
|
+
: undefined;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="velu-private-login">
|
|
22
|
+
<PageHeader
|
|
23
|
+
brand={{ label: site.name, logo }}
|
|
24
|
+
trailing={<ThemeToggle id="theme-toggle" />}
|
|
25
|
+
/>
|
|
26
|
+
<main className="velu-private-login__main">
|
|
27
|
+
<form className="velu-private-login__form" id="form">
|
|
28
|
+
<h1>Private documentation</h1>
|
|
29
|
+
<p>Enter your password to access {site.name}.</p>
|
|
30
|
+
<label htmlFor="password">Password</label>
|
|
31
|
+
<input
|
|
32
|
+
id="password"
|
|
33
|
+
type="password"
|
|
34
|
+
autoComplete="current-password"
|
|
35
|
+
maxLength={256}
|
|
36
|
+
required
|
|
37
|
+
autoFocus
|
|
38
|
+
/>
|
|
39
|
+
<div className="velu-private-login__error" id="error" role="alert" />
|
|
40
|
+
<button className="velu-private-login__continue" type="submit">
|
|
41
|
+
Continue
|
|
42
|
+
</button>
|
|
43
|
+
</form>
|
|
44
|
+
</main>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -5,7 +5,9 @@ import { StaticRouter } from 'react-router-dom/server';
|
|
|
5
5
|
// stylesheet in Vite's SSR module graph so dev-server.js can collect and
|
|
6
6
|
// inline it into <head> (flicker-free SSR).
|
|
7
7
|
import 'velu-ui/styles.css';
|
|
8
|
+
import { site } from 'virtual:velu-site';
|
|
8
9
|
import App from './App.jsx';
|
|
10
|
+
import PrivateLoginPage from './PrivateLoginPage.jsx';
|
|
9
11
|
|
|
10
12
|
// Subpath hosting: mirror the client's basename (from Vite's BASE_URL). The
|
|
11
13
|
// prerender is driven by root-relative route paths ("/foo"), so the location
|
|
@@ -22,3 +24,7 @@ export async function render(url) {
|
|
|
22
24
|
</StaticRouter>
|
|
23
25
|
);
|
|
24
26
|
}
|
|
27
|
+
|
|
28
|
+
export function renderPrivateLoginPage() {
|
|
29
|
+
return renderToString(<PrivateLoginPage site={site} />);
|
|
30
|
+
}
|