@veluai/velu 0.2.32 → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veluai/velu",
3
- "version": "0.2.32",
3
+ "version": "0.2.33",
4
4
  "type": "module",
5
5
  "bin": "./dist/cli.js",
6
6
  "publishConfig": {
@@ -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 apiBase = site.assistant?.apiBase;
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 apiBase = site.assistant?.apiBase;
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);
@@ -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
+ }