@sybilion/uilib 1.2.0 → 1.2.1
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/esm/components/ui/Image/Image.styl.js +1 -1
- package/dist/esm/components/ui/NavUserHeader/NavUserHeader.js +28 -0
- package/dist/esm/components/ui/NavUserHeader/NavUserHeader.styl.js +7 -0
- package/dist/esm/index.js +2 -1
- package/dist/esm/sybilion-auth/SybilionAuthProvider.js +30 -7
- package/dist/esm/sybilion-auth/exchangeSybilionToken.js +6 -2
- package/dist/esm/types/src/components/ui/NavUserHeader/NavUserHeader.d.ts +2 -0
- package/dist/esm/types/src/components/ui/NavUserHeader/NavUserHeader.types.d.ts +25 -0
- package/dist/esm/types/src/components/ui/NavUserHeader/index.d.ts +2 -0
- package/dist/esm/types/src/docs/pages/NavUserHeaderPage.d.ts +1 -0
- package/dist/esm/types/src/index.d.ts +1 -0
- package/dist/esm/types/src/sybilion-auth/SybilionAuthProvider.d.ts +5 -2
- package/dist/esm/types/src/sybilion-auth/exchangeSybilionToken.d.ts +3 -1
- package/dist/esm/types/src/sybilion-auth/index.d.ts +1 -1
- package/docs/standalone-apps.md +137 -21
- package/package.json +6 -1
- package/src/components/ui/Image/Image.styl +1 -0
- package/src/components/ui/NavUserHeader/NavUserHeader.styl +125 -0
- package/src/components/ui/NavUserHeader/NavUserHeader.styl.d.ts +28 -0
- package/src/components/ui/NavUserHeader/NavUserHeader.tsx +148 -0
- package/src/components/ui/NavUserHeader/NavUserHeader.types.ts +27 -0
- package/src/components/ui/NavUserHeader/avatar.svg +4 -0
- package/src/components/ui/NavUserHeader/index.ts +5 -0
- package/src/docs/pages/NavUserHeaderPage.tsx +89 -0
- package/src/docs/pages/SybilionAuthProviderPage.tsx +5 -2
- package/src/docs/registry.ts +6 -0
- package/src/index.ts +1 -0
- package/src/sybilion-auth/SybilionAuthProvider.tsx +33 -11
- package/src/sybilion-auth/exchangeSybilionToken.ts +5 -1
- package/src/sybilion-auth/index.ts +1 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import styleInject from 'style-inject';
|
|
2
2
|
|
|
3
|
-
var css_248z = ".Image_container__beG4N{display:inline-block;height:100%;position:relative;width:100%}.Image_image__Kkmwo{border-radius:inherit;display:block;height:100%;-o-object-fit:cover;object-fit:cover;width:100%}.Image_image__Kkmwo.Image_loading__Hi33D{opacity:0}.Image_fallback__N1ndD{align-items:center;border-radius:inherit;display:flex;height:100%;justify-content:center;left:0;position:absolute;top:0;width:100%}";
|
|
3
|
+
var css_248z = ".Image_container__beG4N{background-color:var(--muted-50);display:inline-block;height:100%;position:relative;width:100%}.Image_image__Kkmwo{border-radius:inherit;display:block;height:100%;-o-object-fit:cover;object-fit:cover;width:100%}.Image_image__Kkmwo.Image_loading__Hi33D{opacity:0}.Image_fallback__N1ndD{align-items:center;border-radius:inherit;display:flex;height:100%;justify-content:center;left:0;position:absolute;top:0;width:100%}";
|
|
4
4
|
var S = {"container":"Image_container__beG4N","image":"Image_image__Kkmwo","loading":"Image_loading__Hi33D","fallback":"Image_fallback__N1ndD"};
|
|
5
5
|
styleInject(css_248z);
|
|
6
6
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import cn from 'classnames';
|
|
3
|
+
import { UserCircleIcon, SunIcon, MoonIcon, SignOutIcon } from '@phosphor-icons/react';
|
|
4
|
+
import { ChevronDownIcon } from 'lucide-react';
|
|
5
|
+
import { Avatar } from '../Avatar/Avatar.js';
|
|
6
|
+
import { Button } from '../Button/Button.js';
|
|
7
|
+
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, DropdownMenuItem } from '../DropdownMenu/DropdownMenu.js';
|
|
8
|
+
import { Image } from '../Image/Image.js';
|
|
9
|
+
import S from './NavUserHeader.styl.js';
|
|
10
|
+
|
|
11
|
+
function NavUserHeader({ variant = 'default', isLoading = false, isAuthenticated, user = null, menuItems, theme, onThemeToggle, onLogout, signInSlot, onSignInClick, }) {
|
|
12
|
+
const authenticated = isAuthenticated ?? true;
|
|
13
|
+
const avatarUrl = user?.avatar ?? '';
|
|
14
|
+
const userName = user?.name ?? '';
|
|
15
|
+
const userEmail = user?.email ?? '';
|
|
16
|
+
if (isLoading) {
|
|
17
|
+
return (jsxs(Button, { variant: "ghost", size: "sm", disabled: true, className: S.loadingButton, children: [jsx("div", { className: S.avatarSkeleton }), jsx("div", { className: S.textSkeleton })] }));
|
|
18
|
+
}
|
|
19
|
+
if (!authenticated) {
|
|
20
|
+
if (signInSlot) {
|
|
21
|
+
return signInSlot;
|
|
22
|
+
}
|
|
23
|
+
return (jsxs(Button, { variant: "ghost", size: "sm", className: S.loginButton, type: "button", onClick: onSignInClick, children: [jsx(UserCircleIcon, { className: S.iconLg }), jsx("span", { children: "Log in" })] }));
|
|
24
|
+
}
|
|
25
|
+
return (jsxs(DropdownMenu, { children: [jsx(DropdownMenuTrigger, { asChild: true, children: jsxs(Button, { variant: "ghost", size: "sm", className: cn(S.userButton, variant === 'compact' && S.compact), children: [jsx(Avatar, { className: S.avatar, children: jsx(Image, { url: avatarUrl, alt: userName, fallback: jsx("div", { className: S.avatarFallback }) }) }), variant === 'default' && (jsxs(Fragment, { children: [jsxs("div", { className: S.userInfo, children: [jsx("span", { className: `${S.userName} ph-no-capture`, children: userName }), jsx("span", { className: S.userEmail, children: userEmail })] }), jsx(ChevronDownIcon, { className: S.iconSm })] }))] }) }), jsxs(DropdownMenuContent, { className: S.dropdownContent, align: "end", elevation: "md", children: [jsx(DropdownMenuLabel, { className: S.userLabel, children: jsxs("div", { className: S.userLabelContent, children: [jsx(Avatar, { className: S.avatar, children: jsx(Image, { url: avatarUrl, alt: userName, fallback: jsx("div", { className: S.avatarFallback }) }) }), jsxs("div", { className: S.userDetails, children: [jsx("span", { className: `${S.userDetailName} ph-no-capture`, children: userName }), jsx("span", { className: S.userDetailEmail, children: userEmail })] })] }) }), jsx(DropdownMenuSeparator, {}), jsxs(DropdownMenuGroup, { children: [menuItems, onThemeToggle ? (jsx(DropdownMenuItem, { onSelect: () => onThemeToggle(), children: theme === 'dark' ? (jsxs(Fragment, { children: [jsx(SunIcon, {}), "Light theme"] })) : (jsxs(Fragment, { children: [jsx(MoonIcon, {}), "Dark theme"] })) })) : null] }), jsx(DropdownMenuSeparator, {}), jsxs(DropdownMenuItem, { variant: "destructive", onSelect: () => onLogout(), children: [jsx(SignOutIcon, {}), "Log out"] })] })] }));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { NavUserHeader };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import styleInject from 'style-inject';
|
|
2
|
+
|
|
3
|
+
var css_248z = ".NavUserHeader_loadingButton__UM1cm{gap:.5rem}.NavUserHeader_avatarSkeleton__bdWE3{border-radius:9999px;height:2rem;width:2rem}.NavUserHeader_avatarSkeleton__bdWE3,.NavUserHeader_textSkeleton__AOp9E{animation:NavUserHeader_pulse__GWYG6 2s cubic-bezier(.4,0,.6,1) infinite;background-color:var(--color-muted)}.NavUserHeader_textSkeleton__AOp9E{border-radius:.25rem;height:1rem;width:5rem}.NavUserHeader_loginButton__QA4q6{gap:.5rem}.NavUserHeader_iconLg__246wU{height:1.25rem;width:1.25rem}.NavUserHeader_iconSm__CWx2D{height:.75rem;width:.75rem}.NavUserHeader_menuIcon__u0VeA{height:1rem;margin-right:.5rem;width:1rem}.NavUserHeader_dropdownContent__djONy{width:14rem}.NavUserHeader_userButton__sBFb-{gap:.5rem;height:52px;padding:var(--p-2)}.NavUserHeader_userButton__sBFb-.NavUserHeader_compact__UfbAp{background-color:transparent!important;padding:0;transition:transform .2s ease-in-out}.NavUserHeader_userButton__sBFb-.NavUserHeader_compact__UfbAp:hover{transform:scale(1.1)}.NavUserHeader_avatar__Yksm-{height:2rem;width:2rem}.NavUserHeader_avatarImage__d6rzY{border-radius:inherit;height:100%;-o-object-fit:cover;object-fit:cover;width:100%}.NavUserHeader_avatarFallback__TKi2T{background-color:var(--color-primary);background:url(avatar.svg) no-repeat 50%;border-radius:inherit;color:var(--color-primary-foreground);height:100%;width:100%}.NavUserHeader_userInfo__kReZ2{align-items:flex-start;display:flex;flex-direction:column;gap:.25rem;text-align:left}.NavUserHeader_userName__8xek-{font-size:var(--text-sm);font-weight:400;line-height:1;max-width:7.5rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.NavUserHeader_userEmail__HKdtn{color:var(--sb-slate-500);font-size:var(--text-xs);line-height:1}.NavUserHeader_userLabel__DAxki{font-weight:400;padding:0}.NavUserHeader_userLabelContent__n5ngd{align-items:center;display:flex;font-size:.875rem;gap:.5rem;padding:.5rem;text-align:left}.NavUserHeader_userDetails__5r-k8{display:grid;flex:1;font-size:.875rem;line-height:1.25;text-align:left}.NavUserHeader_userDetailName__EajCc{font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.NavUserHeader_userDetailEmail__LsK5z{color:var(--color-muted-foreground);font-size:.75rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@keyframes NavUserHeader_pulse__GWYG6{0%,to{opacity:1}50%{opacity:.5}}";
|
|
4
|
+
var S = {"loadingButton":"NavUserHeader_loadingButton__UM1cm","avatarSkeleton":"NavUserHeader_avatarSkeleton__bdWE3","pulse":"NavUserHeader_pulse__GWYG6","textSkeleton":"NavUserHeader_textSkeleton__AOp9E","loginButton":"NavUserHeader_loginButton__QA4q6","iconLg":"NavUserHeader_iconLg__246wU","iconSm":"NavUserHeader_iconSm__CWx2D","menuIcon":"NavUserHeader_menuIcon__u0VeA","dropdownContent":"NavUserHeader_dropdownContent__djONy","userButton":"NavUserHeader_userButton__sBFb-","compact":"NavUserHeader_compact__UfbAp","avatar":"NavUserHeader_avatar__Yksm-","avatarImage":"NavUserHeader_avatarImage__d6rzY","avatarFallback":"NavUserHeader_avatarFallback__TKi2T","userInfo":"NavUserHeader_userInfo__kReZ2","userName":"NavUserHeader_userName__8xek-","userEmail":"NavUserHeader_userEmail__HKdtn","userLabel":"NavUserHeader_userLabel__DAxki","userLabelContent":"NavUserHeader_userLabelContent__n5ngd","userDetails":"NavUserHeader_userDetails__5r-k8","userDetailName":"NavUserHeader_userDetailName__EajCc","userDetailEmail":"NavUserHeader_userDetailEmail__LsK5z"};
|
|
5
|
+
styleInject(css_248z);
|
|
6
|
+
|
|
7
|
+
export { S as default };
|
package/dist/esm/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { SybilionAuthProvider, createSybilionApiFetch, sybilionApiFetch, useSybilionApiFetch, useSybilionAuth } from './sybilion-auth/SybilionAuthProvider.js';
|
|
1
|
+
export { SybilionAuthProvider, createSybilionApiFetch, getSybilionApiOriginFromSdk, sybilionApiFetch, useSybilionApiFetch, useSybilionAuth } from './sybilion-auth/SybilionAuthProvider.js';
|
|
2
2
|
export { SYBILION_AUTH_LOGIN_PATH, normalizeApiBaseUrl } from './sybilion-auth/authPaths.js';
|
|
3
3
|
export { exchangeAuth0AccessTokenForSybilionJwt } from './sybilion-auth/exchangeSybilionToken.js';
|
|
4
4
|
export { ChatContext, ChatProvider, useChat, useChats, useChatsForDataset, useChatsForScopeId, useCurrentChat } from './contexts/chat-context.js';
|
|
@@ -42,6 +42,7 @@ export { LabelWithId } from './components/ui/LabelWithId/LabelWithId.js';
|
|
|
42
42
|
export { LegacyPlatformLink } from './components/ui/LegacyPlatformLink/LegacyPlatformLink.js';
|
|
43
43
|
export { Logo } from './components/ui/Logo/Logo.js';
|
|
44
44
|
export { MobileAdaptiveSelector } from './components/ui/MobileAdaptiveSelector/MobileAdaptiveSelector.js';
|
|
45
|
+
export { NavUserHeader } from './components/ui/NavUserHeader/NavUserHeader.js';
|
|
45
46
|
export { NumberControl } from './components/ui/NumberControl/NumberControl.js';
|
|
46
47
|
export { AppShell, AppShellMainContent } from './components/ui/Page/AppShell/AppShell.js';
|
|
47
48
|
export { PageHeader } from './components/ui/Page/PageHeader/PageHeader.js';
|
|
@@ -2,9 +2,30 @@ import { jsx } from 'react/jsx-runtime';
|
|
|
2
2
|
import { Auth0Provider, useAuth0 } from '@auth0/auth0-react';
|
|
3
3
|
import { createContext, useContext, useMemo, useRef, useState, useCallback, useEffect } from 'react';
|
|
4
4
|
import { normalizeApiBaseUrl } from './authPaths.js';
|
|
5
|
-
import { exchangeAuth0AccessTokenForSybilionJwt } from './exchangeSybilionToken.js';
|
|
6
5
|
|
|
7
6
|
const DEFAULT_TOKEN_KEY = 'sybilion.standalone.jwt';
|
|
7
|
+
function sybilionJwtFromLoginResponse(body) {
|
|
8
|
+
const t = body.data?.token ?? body.token;
|
|
9
|
+
if (!t)
|
|
10
|
+
throw new Error('Sybilion auth: missing token in login response');
|
|
11
|
+
return t;
|
|
12
|
+
}
|
|
13
|
+
/** Origin (`scheme://host:port`) for paths like `/api/v1/...`; derived from SDK URL layout. */
|
|
14
|
+
function getSybilionApiOriginFromSdk(sdk) {
|
|
15
|
+
const loginUrl = sdk.http.buildUrl('/v1/auth/login');
|
|
16
|
+
try {
|
|
17
|
+
const baseHref = typeof window !== 'undefined' && window.location?.href
|
|
18
|
+
? window.location.href
|
|
19
|
+
: 'http://localhost/';
|
|
20
|
+
const u = new URL(loginUrl, baseHref);
|
|
21
|
+
if (!u.pathname.endsWith('/v1/auth/login'))
|
|
22
|
+
return '';
|
|
23
|
+
return u.origin;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
8
29
|
const SybilionAuthContext = createContext(null);
|
|
9
30
|
function useSybilionAuth() {
|
|
10
31
|
const v = useContext(SybilionAuthContext);
|
|
@@ -51,7 +72,7 @@ function writeLs(key, value) {
|
|
|
51
72
|
/* quota / blocked */
|
|
52
73
|
}
|
|
53
74
|
}
|
|
54
|
-
function InnerSybilionSession({ children,
|
|
75
|
+
function InnerSybilionSession({ children, sdk, storageKey, logoutReturnTo, }) {
|
|
55
76
|
const auth0 = useAuth0();
|
|
56
77
|
const auth0Ref = useRef(auth0);
|
|
57
78
|
auth0Ref.current = auth0;
|
|
@@ -71,6 +92,7 @@ function InnerSybilionSession({ children, apiBaseUrl, storageKey, logoutReturnTo
|
|
|
71
92
|
logoutParams: returnTo ? { returnTo } : undefined,
|
|
72
93
|
});
|
|
73
94
|
}, [persistToken, logoutReturnTo]);
|
|
95
|
+
const apiBaseUrl = useMemo(() => getSybilionApiOriginFromSdk(sdk), [sdk]);
|
|
74
96
|
useEffect(() => {
|
|
75
97
|
if (!auth0.isAuthenticated || !auth0.user?.sub) {
|
|
76
98
|
persistToken(null);
|
|
@@ -88,7 +110,8 @@ function InnerSybilionSession({ children, apiBaseUrl, storageKey, logoutReturnTo
|
|
|
88
110
|
setExchangeError(null);
|
|
89
111
|
try {
|
|
90
112
|
const access = await auth0Ref.current.getAccessTokenSilently();
|
|
91
|
-
const
|
|
113
|
+
const loginBody = await sdk.auth.loginWithAuth0Identity(access);
|
|
114
|
+
const jwt = sybilionJwtFromLoginResponse(loginBody);
|
|
92
115
|
if (cancelled)
|
|
93
116
|
return;
|
|
94
117
|
persistToken(jwt);
|
|
@@ -111,7 +134,7 @@ function InnerSybilionSession({ children, apiBaseUrl, storageKey, logoutReturnTo
|
|
|
111
134
|
cancelled = true;
|
|
112
135
|
};
|
|
113
136
|
}, [
|
|
114
|
-
|
|
137
|
+
sdk,
|
|
115
138
|
auth0.isAuthenticated,
|
|
116
139
|
auth0.user?.sub,
|
|
117
140
|
persistToken,
|
|
@@ -163,7 +186,7 @@ function InnerSybilionSession({ children, apiBaseUrl, storageKey, logoutReturnTo
|
|
|
163
186
|
]);
|
|
164
187
|
return (jsx(SybilionAuthContext.Provider, { value: value, children: children }));
|
|
165
188
|
}
|
|
166
|
-
function SybilionAuthProvider({ children,
|
|
189
|
+
function SybilionAuthProvider({ children, sdk, auth0Domain, auth0ClientId, redirectUri, authorizationParams, sybilionTokenStorageKey = DEFAULT_TOKEN_KEY, logoutReturnTo, }) {
|
|
167
190
|
const mergedAuthParams = useMemo(() => ({
|
|
168
191
|
redirect_uri: authorizationParams?.redirect_uri ?? redirectUri,
|
|
169
192
|
audience: authorizationParams?.audience ?? `https://${auth0Domain}/api/v2/`,
|
|
@@ -179,7 +202,7 @@ function SybilionAuthProvider({ children, apiBaseUrl, auth0Domain, auth0ClientId
|
|
|
179
202
|
const cookieOpts = typeof window !== 'undefined'
|
|
180
203
|
? { cookieDomain: window.location.hostname }
|
|
181
204
|
: {};
|
|
182
|
-
return (jsx(Auth0Provider, { domain: auth0Domain, clientId: auth0ClientId, authorizationParams: mergedAuthParams, cacheLocation: "localstorage", useRefreshTokens: true, ...cookieOpts, children: jsx(InnerSybilionSession, {
|
|
205
|
+
return (jsx(Auth0Provider, { domain: auth0Domain, clientId: auth0ClientId, authorizationParams: mergedAuthParams, cacheLocation: "localstorage", useRefreshTokens: true, ...cookieOpts, children: jsx(InnerSybilionSession, { sdk: sdk, storageKey: sybilionTokenStorageKey, logoutReturnTo: logoutReturnTo, children: children }) }));
|
|
183
206
|
}
|
|
184
207
|
|
|
185
|
-
export { SybilionAuthProvider, createSybilionApiFetch, sybilionApiFetch, useSybilionApiFetch, useSybilionAuth };
|
|
208
|
+
export { SybilionAuthProvider, createSybilionApiFetch, getSybilionApiOriginFromSdk, sybilionApiFetch, useSybilionApiFetch, useSybilionAuth };
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { normalizeApiBaseUrl, SYBILION_AUTH_LOGIN_PATH } from './authPaths.js';
|
|
2
2
|
|
|
3
|
+
/** Default API segment before `/v1/...`; matches {@link createSybilionSDK} default `apiPrefix`. */
|
|
4
|
+
const SYBILION_AUTH_API_PREFIX = '/api';
|
|
3
5
|
/** POST `{ identity: auth0AccessToken, type: 'auth0' }` → Sybilion API JWT string. */
|
|
4
|
-
async function exchangeAuth0AccessTokenForSybilionJwt(
|
|
6
|
+
async function exchangeAuth0AccessTokenForSybilionJwt(
|
|
7
|
+
/** API origin only (no trailing slash), same as SDK `baseUrl`. */
|
|
8
|
+
apiBaseUrl, auth0AccessToken) {
|
|
5
9
|
const base = normalizeApiBaseUrl(apiBaseUrl);
|
|
6
|
-
const res = await fetch(`${base}${SYBILION_AUTH_LOGIN_PATH}`, {
|
|
10
|
+
const res = await fetch(`${base}${SYBILION_AUTH_API_PREFIX}${SYBILION_AUTH_LOGIN_PATH}`, {
|
|
7
11
|
method: 'POST',
|
|
8
12
|
headers: {
|
|
9
13
|
Accept: 'application/json',
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { NavUserHeaderProps } from './NavUserHeader.types';
|
|
2
|
+
export declare function NavUserHeader({ variant, isLoading, isAuthenticated, user, menuItems, theme, onThemeToggle, onLogout, signInSlot, onSignInClick, }: NavUserHeaderProps): string | number | bigint | true | Iterable<import("react").ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<import("react").ReactNode>> | import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export type NavUserHeaderUser = {
|
|
3
|
+
name: string;
|
|
4
|
+
email: string;
|
|
5
|
+
/** Passed to avatar `Image` as `src`. */
|
|
6
|
+
avatar?: string;
|
|
7
|
+
};
|
|
8
|
+
export type NavUserHeaderProps = {
|
|
9
|
+
variant?: 'default' | 'compact';
|
|
10
|
+
isLoading?: boolean;
|
|
11
|
+
/** When false, signed-out branch is shown. Defaults to true when omitted. */
|
|
12
|
+
isAuthenticated?: boolean;
|
|
13
|
+
/** Present when authenticated: shown in trigger and dropdown label. */
|
|
14
|
+
user?: NavUserHeaderUser | null;
|
|
15
|
+
/** Rows inside the menu above theme toggle and logout. Use `DropdownMenuItem` nodes. */
|
|
16
|
+
menuItems?: ReactNode;
|
|
17
|
+
/** Current theme drives the toggle row label/icons. */
|
|
18
|
+
theme: 'light' | 'dark';
|
|
19
|
+
/** When set, renders the light/dark theme menu row. */
|
|
20
|
+
onThemeToggle?: () => void;
|
|
21
|
+
onLogout: () => void;
|
|
22
|
+
/** Replaces default “Log in” control when signed out. */
|
|
23
|
+
signInSlot?: ReactNode;
|
|
24
|
+
onSignInClick?: () => void;
|
|
25
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function NavUserHeaderPage(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -31,6 +31,7 @@ export * from './components/ui/LabelWithId';
|
|
|
31
31
|
export * from './components/ui/LegacyPlatformLink';
|
|
32
32
|
export * from './components/ui/Logo';
|
|
33
33
|
export * from './components/ui/MobileAdaptiveSelector';
|
|
34
|
+
export * from './components/ui/NavUserHeader';
|
|
34
35
|
export * from './components/ui/NumberControl';
|
|
35
36
|
export * from './components/ui/Page';
|
|
36
37
|
export * from './components/ui/Progress';
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { type RedirectLoginOptions } from '@auth0/auth0-react';
|
|
2
|
+
import type { SybilionSDK } from '@sybilion/sdk';
|
|
2
3
|
import type { JSX, ReactNode } from 'react';
|
|
4
|
+
/** Origin (`scheme://host:port`) for paths like `/api/v1/...`; derived from SDK URL layout. */
|
|
5
|
+
export declare function getSybilionApiOriginFromSdk(sdk: SybilionSDK): string;
|
|
3
6
|
export type SybilionAuthProviderProps = {
|
|
4
7
|
children: ReactNode;
|
|
5
|
-
|
|
8
|
+
sdk: SybilionSDK;
|
|
6
9
|
auth0Domain: string;
|
|
7
10
|
auth0ClientId: string;
|
|
8
11
|
redirectUri: string;
|
|
@@ -36,4 +39,4 @@ export declare function sybilionApiFetch(apiBaseUrl: string, bearerToken: string
|
|
|
36
39
|
export declare function createSybilionApiFetch(apiBaseUrl: string, getSybilionAccessToken: () => Promise<string | null>): (path: string, init?: RequestInit) => Promise<Response>;
|
|
37
40
|
/** Authenticated fetch using {@link useSybilionAuth} context. */
|
|
38
41
|
export declare function useSybilionApiFetch(): (path: string, init?: RequestInit) => Promise<Response>;
|
|
39
|
-
export declare function SybilionAuthProvider({ children,
|
|
42
|
+
export declare function SybilionAuthProvider({ children, sdk, auth0Domain, auth0ClientId, redirectUri, authorizationParams, sybilionTokenStorageKey, logoutReturnTo, }: SybilionAuthProviderProps): JSX.Element;
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
/** POST `{ identity: auth0AccessToken, type: 'auth0' }` → Sybilion API JWT string. */
|
|
2
|
-
export declare function exchangeAuth0AccessTokenForSybilionJwt(
|
|
2
|
+
export declare function exchangeAuth0AccessTokenForSybilionJwt(
|
|
3
|
+
/** API origin only (no trailing slash), same as SDK `baseUrl`. */
|
|
4
|
+
apiBaseUrl: string, auth0AccessToken: string): Promise<string>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export type { SybilionAuthProviderProps, SybilionAuthContextValue, } from '#uilib/sybilion-auth/SybilionAuthProvider';
|
|
2
|
-
export { SybilionAuthProvider, useSybilionAuth, sybilionApiFetch, createSybilionApiFetch, useSybilionApiFetch, } from '#uilib/sybilion-auth/SybilionAuthProvider';
|
|
2
|
+
export { SybilionAuthProvider, getSybilionApiOriginFromSdk, useSybilionAuth, sybilionApiFetch, createSybilionApiFetch, useSybilionApiFetch, } from '#uilib/sybilion-auth/SybilionAuthProvider';
|
|
3
3
|
export { SYBILION_AUTH_LOGIN_PATH, normalizeApiBaseUrl, } from '#uilib/sybilion-auth/authPaths';
|
|
4
4
|
export { exchangeAuth0AccessTokenForSybilionJwt } from '#uilib/sybilion-auth/exchangeSybilionToken';
|
package/docs/standalone-apps.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Standalone Sybilion apps (@sybilion/uilib)
|
|
2
2
|
|
|
3
|
-
Greenfield SPA on **your own origin**: `@sybilion/uilib` for layout/UI,
|
|
3
|
+
Greenfield SPA on **your own origin**: `@sybilion/uilib` for layout/UI, **SybilionAuthProvider** + Sybilion API for data—no iframe in the main client.
|
|
4
4
|
|
|
5
|
-
**Agents / humans:** Use `AppShell` + `AppShellMainContent`; stick to uilib spacing primitives (
|
|
5
|
+
**Agents / humans:** Use `AppShell` + `AppShellMainContent`; stick to uilib spacing primitives (e.g. `Gap`) instead of ad hoc root horizontal gutters.
|
|
6
6
|
|
|
7
7
|
## 1. Dependencies and global CSS
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
yarn add react react-dom react-router-dom @auth0/auth0-react @sybilion/uilib
|
|
10
|
+
yarn add react react-dom react-router-dom @auth0/auth0-react @sybilion/uilib @sybilion/sdk
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
Import tokens/fonts once:
|
|
@@ -18,32 +18,122 @@ import '@sybilion/uilib/standalone-global.css';
|
|
|
18
18
|
|
|
19
19
|
## 2. Layout (AppShell)
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
### Minimal example
|
|
22
|
+
|
|
23
|
+
`AppHeaderHost` is the empty header bar; put triggers and `NavUserHeader` (a.k.a. the user / account menu) inside `AppHeaderPortal` so they render in that bar.
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import type { ReactNode } from 'react';
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
AppHeaderHost,
|
|
30
|
+
AppHeaderPortal,
|
|
31
|
+
AppShell,
|
|
32
|
+
AppShellMainContent,
|
|
33
|
+
Gap,
|
|
34
|
+
NavUserHeader,
|
|
35
|
+
PageFooter,
|
|
36
|
+
PageScroll,
|
|
37
|
+
} from '@sybilion/uilib';
|
|
38
|
+
|
|
39
|
+
export function AppLayout({ children }: { children: ReactNode }) {
|
|
40
|
+
return (
|
|
41
|
+
<PageScroll>
|
|
42
|
+
<AppShell>
|
|
43
|
+
{/* Optional: sidebar as a sibling before main, e.g. <Sidebar … /> */}
|
|
44
|
+
<AppShellMainContent
|
|
45
|
+
header={<AppHeaderHost />}
|
|
46
|
+
footer={<PageFooter versionLink="/releases" versionLabel="0.0.0" />}
|
|
47
|
+
>
|
|
48
|
+
<AppHeaderPortal>
|
|
49
|
+
<Gap />
|
|
50
|
+
<NavUserHeader
|
|
51
|
+
theme="light"
|
|
52
|
+
onThemeToggle={() => undefined}
|
|
53
|
+
onLogout={() => undefined}
|
|
54
|
+
isAuthenticated={false}
|
|
55
|
+
/>
|
|
56
|
+
</AppHeaderPortal>
|
|
57
|
+
{children}
|
|
58
|
+
</AppShellMainContent>
|
|
59
|
+
</AppShell>
|
|
60
|
+
</PageScroll>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Wire `NavUserHeader` to real auth (`useSybilionAuth`, theme context, etc.). Demo page in repo: `src/docs/pages/NavUserHeaderPage.tsx` (slug `nav-user-header`).
|
|
66
|
+
|
|
67
|
+
### Full pattern
|
|
68
|
+
|
|
69
|
+
Reference: `[src/docs/DocsShell.tsx](https://github.com/Mir-Insight/uilib/blob/main/src/docs/DocsShell.tsx)` — `PageScroll` → `AppShell` → sidebar → `AppShellMainContent` with `AppHeaderHost`, `PageFooter`, and routed body (`Outlet`). Add `Theme` and optional `SidebarProvider` from `@homecode/ui`.
|
|
70
|
+
|
|
71
|
+
### Glossary (high-use pieces)
|
|
72
|
+
|
|
73
|
+
| Component / API | What it is for |
|
|
74
|
+
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
75
|
+
| `**PageScroll**` | Page-level vertical scroll wrapper; usual outer shell for the app body. |
|
|
76
|
+
| `**AppShell**` | Layout grid container; typically holds a sidebar (optional) + main column. |
|
|
77
|
+
| `**AppShellMainContent**` | Main column with slots: `header`, scrollable body (`children`), `footer`. |
|
|
78
|
+
| `**AppHeaderHost**` | Renders the top header anchor (DOM id `page-header`); stays empty on purpose. |
|
|
79
|
+
| `**AppHeaderPortal**` | Portals children into `AppHeaderHost` — put header actions, `NavUserHeader`, theme toggle here. |
|
|
80
|
+
| `**NavUserHeader**` | Header user menu: avatar, account dropdown, theme row, logout; **not** a bare `DropdownMenu`. |
|
|
81
|
+
| `**Sidebar`\*\* | Collapsible app sidebar / nav rail (pair with `SidebarProvider` from `@homecode/ui` when needed). |
|
|
82
|
+
| `**PageFooter**` | Standard app footer (logo, links, version badge). Requires `versionLink` + `versionLabel`. |
|
|
83
|
+
| `**Gap**` | Horizontal or vertical spacing primitive between flex children (prefer over raw margins). |
|
|
84
|
+
| `**SybilionAuthProvider**` | Auth0 + Sybilion JWT bootstrap for the whole app (see §3). |
|
|
85
|
+
| `**useSybilionAuth**` | Current user, tokens, login/logout inside the provider tree. |
|
|
86
|
+
| `**useSybilionApiFetch**` | Authenticated `fetch` to the Sybilion API using the stored JWT. |
|
|
87
|
+
| `**Breadcrumb**`, `**BreadcrumbList`, …\*\* | Accessible breadcrumb trail for the page header (compose `BreadcrumbItem` / `BreadcrumbLink` / `BreadcrumbPage`). |
|
|
88
|
+
| `**Button`**, `**Card\*\*` | Primary actions and grouped content blocks—default building blocks for screens inside the layout body. |
|
|
22
89
|
|
|
23
90
|
## 3. Auth (`SybilionAuthProvider`)
|
|
24
91
|
|
|
25
92
|
Use inside `BrowserRouter` if redirects hit a callback route.
|
|
26
93
|
|
|
27
|
-
Env vars depend on bundler—for **Vite** only `import.meta.env.VITE_
|
|
94
|
+
Env vars depend on bundler—for **Vite** only `import.meta.env.VITE_` is exposed client-side:
|
|
28
95
|
|
|
29
96
|
```tsx
|
|
30
|
-
import {
|
|
97
|
+
import { useMemo, type ReactNode } from 'react';
|
|
31
98
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
99
|
+
import { SybilionAuthProvider } from '@sybilion/uilib';
|
|
100
|
+
import { createSybilionSDK } from '@sybilion/sdk';
|
|
101
|
+
|
|
102
|
+
const sybilionJwtKey = 'sybilion.standalone.jwt';
|
|
103
|
+
|
|
104
|
+
export function AppProviders({ children }: { children: ReactNode }) {
|
|
105
|
+
const apiBaseUrl = import.meta.env.VITE_SYBILION_API_BASE_URL as string;
|
|
106
|
+
const auth0Domain = import.meta.env.VITE_AUTH0_DOMAIN as string;
|
|
107
|
+
const auth0ClientId = import.meta.env.VITE_AUTH0_CLIENT_ID as string;
|
|
108
|
+
|
|
109
|
+
const sdk = useMemo(
|
|
110
|
+
() =>
|
|
111
|
+
createSybilionSDK({
|
|
112
|
+
baseUrl: apiBaseUrl,
|
|
113
|
+
apiPrefix: '/api',
|
|
114
|
+
getToken: () =>
|
|
115
|
+
typeof localStorage !== 'undefined'
|
|
116
|
+
? localStorage.getItem(sybilionJwtKey) ?? undefined
|
|
117
|
+
: undefined,
|
|
118
|
+
}),
|
|
119
|
+
[apiBaseUrl],
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<SybilionAuthProvider
|
|
124
|
+
sdk={sdk}
|
|
125
|
+
sybilionTokenStorageKey={sybilionJwtKey}
|
|
126
|
+
auth0Domain={auth0Domain}
|
|
127
|
+
auth0ClientId={auth0ClientId}
|
|
128
|
+
redirectUri={window.location.origin}
|
|
129
|
+
>
|
|
130
|
+
{children}
|
|
131
|
+
</SybilionAuthProvider>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
44
134
|
```
|
|
45
135
|
|
|
46
|
-
**Flow:** Auth0 SPA → `
|
|
136
|
+
**Flow:** Auth0 SPA → `sdk.auth.loginWithAuth0Identity(<Auth0 AT>)` → Sybilion JWT (`data.token` / `token`) persisted → same `sdk` + `getToken` supply Bearer on API calls; `useSybilionApiFetch()` uses `getSybilionApiOriginFromSdk(sdk)` for URLs (paths like `/api/v1/...`).
|
|
47
137
|
|
|
48
138
|
**Defaults** for `authorizationParams` match sybilion-client (Management audience + `openid profile email offline_access …`); override if your Auth0 SPA needs a Resource Server audience (backend confirms).
|
|
49
139
|
|
|
@@ -51,13 +141,39 @@ const auth0ClientId = import.meta.env.VITE_AUTH0_CLIENT_ID as string;
|
|
|
51
141
|
| ---------------- | ------------------------------------------------------------------------------------- |
|
|
52
142
|
| **Auth0** | Callback, logout, and web origins → your URLs (+ previews). |
|
|
53
143
|
| **Sybilion API** | CORS → your deploy `Origin`. |
|
|
54
|
-
| **App** | `
|
|
144
|
+
| **App** | `createSybilionSDK` (`baseUrl`, `apiPrefix`), pass **`sdk`** to `SybilionAuthProvider`; align `sybilionTokenStorageKey` with `getToken`; Auth0 `domain` / `clientId`; redirect usually `window.location.origin`. |
|
|
55
145
|
|
|
56
146
|
**Hooks:** `useSybilionAuth()`, `useSybilionApiFetch()` (or `createSybilionApiFetch` / `sybilionApiFetch` helpers).
|
|
57
147
|
|
|
58
148
|
## 4. Data
|
|
59
149
|
|
|
60
|
-
Fetch Sybilion API with the JWT above
|
|
150
|
+
Fetch the Sybilion API with the JWT above (e.g. via `useSybilionApiFetch`).
|
|
151
|
+
|
|
152
|
+
### `@sybilion/sdk`
|
|
153
|
+
|
|
154
|
+
Install the typed client alongside uilib:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
yarn add @sybilion/sdk
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Configure **`createSybilionSDK`** with the same instance you pass to **`SybilionAuthProvider`** (`sdk` prop). Use **`baseUrl`**: API origin only (no trailing slash), default **`apiPrefix: '/api'`** so requests hit `{baseUrl}/api/v1/...`. **`getToken`** should read the same storage key as **`sybilionTokenStorageKey`** on the provider (default `sybilion.standalone.jwt`).
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { createSybilionSDK } from '@sybilion/sdk';
|
|
164
|
+
|
|
165
|
+
const sdk = createSybilionSDK({
|
|
166
|
+
baseUrl: apiBaseUrl,
|
|
167
|
+
apiPrefix: '/api',
|
|
168
|
+
getToken: () => localStorage.getItem('sybilion.standalone.jwt') ?? undefined,
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- **`sdk.auth`** — `loginWithAuth0Identity`, `getMe`, `updateMe` (Auth0 bootstrap + user profile).
|
|
173
|
+
- **`sdk.raw`** — thin `GET`/`POST`/… wrappers for `/v1/...` paths (e.g. `raw.analyses.driversMapOnce`; returns parsed JSON, no app-specific shaping).
|
|
174
|
+
- **`sdk.resources`** — higher-level helpers (datasets, drivers, subscriptions) that compose `raw` calls with parsing where applicable.
|
|
175
|
+
|
|
176
|
+
See the package README: [`@sybilion/sdk`](https://www.npmjs.com/package/@sybilion/sdk) — in this monorepo, [`../../sdk/README.md`](../../sdk/README.md).
|
|
61
177
|
|
|
62
178
|
## Related
|
|
63
179
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sybilion/uilib",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Sybilion Design System — React UI components (Webpack + Stylus)",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -106,6 +106,7 @@
|
|
|
106
106
|
},
|
|
107
107
|
"peerDependencies": {
|
|
108
108
|
"@auth0/auth0-react": "^2.3.1",
|
|
109
|
+
"@sybilion/sdk": ">=0.0.1",
|
|
109
110
|
"react": ">=18.0.0",
|
|
110
111
|
"react-dom": ">=18.0.0",
|
|
111
112
|
"react-router-dom": ">=6.0.0"
|
|
@@ -113,10 +114,14 @@
|
|
|
113
114
|
"peerDependenciesMeta": {
|
|
114
115
|
"@auth0/auth0-react": {
|
|
115
116
|
"optional": true
|
|
117
|
+
},
|
|
118
|
+
"@sybilion/sdk": {
|
|
119
|
+
"optional": true
|
|
116
120
|
}
|
|
117
121
|
},
|
|
118
122
|
"devDependencies": {
|
|
119
123
|
"@auth0/auth0-react": "^2.3.1",
|
|
124
|
+
"@sybilion/sdk": "file:../sdk",
|
|
120
125
|
"@babel/core": "^7.20.12",
|
|
121
126
|
"@babel/preset-typescript": "^7.21.0",
|
|
122
127
|
"@homecode/ui": "^4.30.6",
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
.loadingButton
|
|
2
|
+
gap 0.5rem
|
|
3
|
+
|
|
4
|
+
.avatarSkeleton
|
|
5
|
+
height 2rem
|
|
6
|
+
width 2rem
|
|
7
|
+
border-radius 9999px
|
|
8
|
+
background-color var(--color-muted)
|
|
9
|
+
animation pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite
|
|
10
|
+
|
|
11
|
+
.textSkeleton
|
|
12
|
+
height 1rem
|
|
13
|
+
width 5rem
|
|
14
|
+
border-radius 0.25rem
|
|
15
|
+
background-color var(--color-muted)
|
|
16
|
+
animation pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite
|
|
17
|
+
|
|
18
|
+
.loginButton
|
|
19
|
+
gap 0.5rem
|
|
20
|
+
|
|
21
|
+
.iconLg
|
|
22
|
+
height 1.25rem
|
|
23
|
+
width 1.25rem
|
|
24
|
+
|
|
25
|
+
.iconSm
|
|
26
|
+
height 0.75rem
|
|
27
|
+
width 0.75rem
|
|
28
|
+
|
|
29
|
+
.menuIcon
|
|
30
|
+
margin-right 0.5rem
|
|
31
|
+
height 1rem
|
|
32
|
+
width 1rem
|
|
33
|
+
|
|
34
|
+
.dropdownContent
|
|
35
|
+
width 14rem
|
|
36
|
+
|
|
37
|
+
.userButton
|
|
38
|
+
gap 0.5rem
|
|
39
|
+
height 52px
|
|
40
|
+
padding var(--p-2)
|
|
41
|
+
|
|
42
|
+
&.compact
|
|
43
|
+
padding 0
|
|
44
|
+
background-color transparent !important
|
|
45
|
+
transition transform 0.2s ease-in-out
|
|
46
|
+
|
|
47
|
+
&:hover
|
|
48
|
+
transform scale(1.1)
|
|
49
|
+
|
|
50
|
+
.avatar
|
|
51
|
+
height 2rem
|
|
52
|
+
width 2rem
|
|
53
|
+
|
|
54
|
+
.avatarImage
|
|
55
|
+
width 100%
|
|
56
|
+
height 100%
|
|
57
|
+
object-fit cover
|
|
58
|
+
border-radius inherit
|
|
59
|
+
|
|
60
|
+
.avatarFallback
|
|
61
|
+
background-color var(--color-primary)
|
|
62
|
+
color var(--color-primary-foreground)
|
|
63
|
+
background url('./avatar.svg') no-repeat center center
|
|
64
|
+
width 100%
|
|
65
|
+
height 100%
|
|
66
|
+
border-radius inherit
|
|
67
|
+
|
|
68
|
+
.userInfo
|
|
69
|
+
display flex
|
|
70
|
+
flex-direction column
|
|
71
|
+
align-items flex-start
|
|
72
|
+
text-align left
|
|
73
|
+
gap 0.25rem
|
|
74
|
+
|
|
75
|
+
.userName
|
|
76
|
+
font-size var(--text-sm)
|
|
77
|
+
font-weight 400
|
|
78
|
+
line-height 1
|
|
79
|
+
text-overflow ellipsis
|
|
80
|
+
overflow hidden
|
|
81
|
+
white-space nowrap
|
|
82
|
+
max-width 7.5rem
|
|
83
|
+
|
|
84
|
+
.userEmail
|
|
85
|
+
font-size var(--text-xs)
|
|
86
|
+
color var(--sb-slate-500)
|
|
87
|
+
line-height 1
|
|
88
|
+
|
|
89
|
+
.userLabel
|
|
90
|
+
padding 0
|
|
91
|
+
font-weight normal
|
|
92
|
+
|
|
93
|
+
.userLabelContent
|
|
94
|
+
display flex
|
|
95
|
+
align-items center
|
|
96
|
+
gap 0.5rem
|
|
97
|
+
padding 0.5rem
|
|
98
|
+
text-align left
|
|
99
|
+
font-size 0.875rem
|
|
100
|
+
|
|
101
|
+
.userDetails
|
|
102
|
+
display grid
|
|
103
|
+
flex 1
|
|
104
|
+
text-align left
|
|
105
|
+
font-size 0.875rem
|
|
106
|
+
line-height 1.25
|
|
107
|
+
|
|
108
|
+
.userDetailName
|
|
109
|
+
text-overflow ellipsis
|
|
110
|
+
overflow hidden
|
|
111
|
+
white-space nowrap
|
|
112
|
+
font-weight 500
|
|
113
|
+
|
|
114
|
+
.userDetailEmail
|
|
115
|
+
color var(--color-muted-foreground)
|
|
116
|
+
text-overflow ellipsis
|
|
117
|
+
overflow hidden
|
|
118
|
+
white-space nowrap
|
|
119
|
+
font-size 0.75rem
|
|
120
|
+
|
|
121
|
+
@keyframes pulse
|
|
122
|
+
0%, 100%
|
|
123
|
+
opacity 1
|
|
124
|
+
50%
|
|
125
|
+
opacity 0.5
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// This file is automatically generated.
|
|
2
|
+
// Please do not change this file!
|
|
3
|
+
interface CssExports {
|
|
4
|
+
'avatar': string;
|
|
5
|
+
'avatarFallback': string;
|
|
6
|
+
'avatarImage': string;
|
|
7
|
+
'avatarSkeleton': string;
|
|
8
|
+
'compact': string;
|
|
9
|
+
'dropdownContent': string;
|
|
10
|
+
'iconLg': string;
|
|
11
|
+
'iconSm': string;
|
|
12
|
+
'loadingButton': string;
|
|
13
|
+
'loginButton': string;
|
|
14
|
+
'menuIcon': string;
|
|
15
|
+
'pulse': string;
|
|
16
|
+
'textSkeleton': string;
|
|
17
|
+
'userButton': string;
|
|
18
|
+
'userDetailEmail': string;
|
|
19
|
+
'userDetailName': string;
|
|
20
|
+
'userDetails': string;
|
|
21
|
+
'userEmail': string;
|
|
22
|
+
'userInfo': string;
|
|
23
|
+
'userLabel': string;
|
|
24
|
+
'userLabelContent': string;
|
|
25
|
+
'userName': string;
|
|
26
|
+
}
|
|
27
|
+
export const cssExports: CssExports;
|
|
28
|
+
export default cssExports;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import cn from 'classnames';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
MoonIcon,
|
|
5
|
+
SignOutIcon,
|
|
6
|
+
SunIcon,
|
|
7
|
+
UserCircleIcon,
|
|
8
|
+
} from '@phosphor-icons/react';
|
|
9
|
+
import { ChevronDownIcon } from 'lucide-react';
|
|
10
|
+
|
|
11
|
+
import { Avatar } from '../Avatar';
|
|
12
|
+
import { Button } from '../Button';
|
|
13
|
+
import {
|
|
14
|
+
DropdownMenu,
|
|
15
|
+
DropdownMenuContent,
|
|
16
|
+
DropdownMenuGroup,
|
|
17
|
+
DropdownMenuItem,
|
|
18
|
+
DropdownMenuLabel,
|
|
19
|
+
DropdownMenuSeparator,
|
|
20
|
+
DropdownMenuTrigger,
|
|
21
|
+
} from '../DropdownMenu';
|
|
22
|
+
import { Image } from '../Image';
|
|
23
|
+
import S from './NavUserHeader.styl';
|
|
24
|
+
import type { NavUserHeaderProps } from './NavUserHeader.types';
|
|
25
|
+
|
|
26
|
+
export function NavUserHeader({
|
|
27
|
+
variant = 'default',
|
|
28
|
+
isLoading = false,
|
|
29
|
+
isAuthenticated,
|
|
30
|
+
user = null,
|
|
31
|
+
menuItems,
|
|
32
|
+
theme,
|
|
33
|
+
onThemeToggle,
|
|
34
|
+
onLogout,
|
|
35
|
+
signInSlot,
|
|
36
|
+
onSignInClick,
|
|
37
|
+
}: NavUserHeaderProps) {
|
|
38
|
+
const authenticated = isAuthenticated ?? true;
|
|
39
|
+
|
|
40
|
+
const avatarUrl = user?.avatar ?? '';
|
|
41
|
+
const userName = user?.name ?? '';
|
|
42
|
+
const userEmail = user?.email ?? '';
|
|
43
|
+
|
|
44
|
+
if (isLoading) {
|
|
45
|
+
return (
|
|
46
|
+
<Button variant="ghost" size="sm" disabled className={S.loadingButton}>
|
|
47
|
+
<div className={S.avatarSkeleton} />
|
|
48
|
+
<div className={S.textSkeleton} />
|
|
49
|
+
</Button>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!authenticated) {
|
|
54
|
+
if (signInSlot) {
|
|
55
|
+
return signInSlot;
|
|
56
|
+
}
|
|
57
|
+
return (
|
|
58
|
+
<Button
|
|
59
|
+
variant="ghost"
|
|
60
|
+
size="sm"
|
|
61
|
+
className={S.loginButton}
|
|
62
|
+
type="button"
|
|
63
|
+
onClick={onSignInClick}
|
|
64
|
+
>
|
|
65
|
+
<UserCircleIcon className={S.iconLg} />
|
|
66
|
+
<span>Log in</span>
|
|
67
|
+
</Button>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<DropdownMenu>
|
|
73
|
+
<DropdownMenuTrigger asChild>
|
|
74
|
+
<Button
|
|
75
|
+
variant="ghost"
|
|
76
|
+
size="sm"
|
|
77
|
+
className={cn(S.userButton, variant === 'compact' && S.compact)}
|
|
78
|
+
>
|
|
79
|
+
<Avatar className={S.avatar}>
|
|
80
|
+
<Image
|
|
81
|
+
url={avatarUrl}
|
|
82
|
+
alt={userName}
|
|
83
|
+
fallback={<div className={S.avatarFallback} />}
|
|
84
|
+
/>
|
|
85
|
+
</Avatar>
|
|
86
|
+
{variant === 'default' && (
|
|
87
|
+
<>
|
|
88
|
+
<div className={S.userInfo}>
|
|
89
|
+
<span className={`${S.userName} ph-no-capture`}>
|
|
90
|
+
{userName}
|
|
91
|
+
</span>
|
|
92
|
+
<span className={S.userEmail}>{userEmail}</span>
|
|
93
|
+
</div>
|
|
94
|
+
<ChevronDownIcon className={S.iconSm} />
|
|
95
|
+
</>
|
|
96
|
+
)}
|
|
97
|
+
</Button>
|
|
98
|
+
</DropdownMenuTrigger>
|
|
99
|
+
<DropdownMenuContent
|
|
100
|
+
className={S.dropdownContent}
|
|
101
|
+
align="end"
|
|
102
|
+
elevation="md"
|
|
103
|
+
>
|
|
104
|
+
<DropdownMenuLabel className={S.userLabel}>
|
|
105
|
+
<div className={S.userLabelContent}>
|
|
106
|
+
<Avatar className={S.avatar}>
|
|
107
|
+
<Image
|
|
108
|
+
url={avatarUrl}
|
|
109
|
+
alt={userName}
|
|
110
|
+
fallback={<div className={S.avatarFallback} />}
|
|
111
|
+
/>
|
|
112
|
+
</Avatar>
|
|
113
|
+
<div className={S.userDetails}>
|
|
114
|
+
<span className={`${S.userDetailName} ph-no-capture`}>
|
|
115
|
+
{userName}
|
|
116
|
+
</span>
|
|
117
|
+
<span className={S.userDetailEmail}>{userEmail}</span>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</DropdownMenuLabel>
|
|
121
|
+
<DropdownMenuSeparator />
|
|
122
|
+
<DropdownMenuGroup>
|
|
123
|
+
{menuItems}
|
|
124
|
+
{onThemeToggle ? (
|
|
125
|
+
<DropdownMenuItem onSelect={() => onThemeToggle()}>
|
|
126
|
+
{theme === 'dark' ? (
|
|
127
|
+
<>
|
|
128
|
+
<SunIcon />
|
|
129
|
+
Light theme
|
|
130
|
+
</>
|
|
131
|
+
) : (
|
|
132
|
+
<>
|
|
133
|
+
<MoonIcon />
|
|
134
|
+
Dark theme
|
|
135
|
+
</>
|
|
136
|
+
)}
|
|
137
|
+
</DropdownMenuItem>
|
|
138
|
+
) : null}
|
|
139
|
+
</DropdownMenuGroup>
|
|
140
|
+
<DropdownMenuSeparator />
|
|
141
|
+
<DropdownMenuItem variant="destructive" onSelect={() => onLogout()}>
|
|
142
|
+
<SignOutIcon />
|
|
143
|
+
Log out
|
|
144
|
+
</DropdownMenuItem>
|
|
145
|
+
</DropdownMenuContent>
|
|
146
|
+
</DropdownMenu>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type NavUserHeaderUser = {
|
|
4
|
+
name: string;
|
|
5
|
+
email: string;
|
|
6
|
+
/** Passed to avatar `Image` as `src`. */
|
|
7
|
+
avatar?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type NavUserHeaderProps = {
|
|
11
|
+
variant?: 'default' | 'compact';
|
|
12
|
+
isLoading?: boolean;
|
|
13
|
+
/** When false, signed-out branch is shown. Defaults to true when omitted. */
|
|
14
|
+
isAuthenticated?: boolean;
|
|
15
|
+
/** Present when authenticated: shown in trigger and dropdown label. */
|
|
16
|
+
user?: NavUserHeaderUser | null;
|
|
17
|
+
/** Rows inside the menu above theme toggle and logout. Use `DropdownMenuItem` nodes. */
|
|
18
|
+
menuItems?: ReactNode;
|
|
19
|
+
/** Current theme drives the toggle row label/icons. */
|
|
20
|
+
theme: 'light' | 'dark';
|
|
21
|
+
/** When set, renders the light/dark theme menu row. */
|
|
22
|
+
onThemeToggle?: () => void;
|
|
23
|
+
onLogout: () => void;
|
|
24
|
+
/** Replaces default “Log in” control when signed out. */
|
|
25
|
+
signInSlot?: ReactNode;
|
|
26
|
+
onSignInClick?: () => void;
|
|
27
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none">
|
|
2
|
+
<path fill="#ECFEFF" d="M0 16C0 7.163 7.163 0 16 0s16 7.163 16 16-7.163 16-16 16S0 24.837 0 16" />
|
|
3
|
+
<path fill="#22D3EE" fill-rule="evenodd" d="M17.4 9.454h-2.8V8h2.8zm1.4 1.455V9.454h-1.4v1.455zm0 2.91v-2.91h1.4v2.91zm-1.4 1.454v-1.455h1.4v1.455zm-2.8 0h2.8v1.454h-2.8zm-1.4-1.455v1.455h1.4v-1.455zm0-2.91v2.91h-1.4v-2.91zm0 0h1.4V9.455h-1.4zm6.3 11.638h-7V21.09h-2.1v-1.455h1.4v-1.454h2.1v-1.454h-2.1v1.454h-1.4v1.454H9v1.455h1.4v1.455h2.1V24h7zm2.1-1.455v1.455h-2.1V21.09zm0-1.455H23v1.455h-1.4zm-1.4-1.454v1.454h1.4v-1.454zm0 0h-2.1v-1.454h2.1z" clip-rule="evenodd" />
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { DropdownMenuItem } from '#uilib/components/ui/DropdownMenu';
|
|
4
|
+
import { NavUserHeader } from '#uilib/components/ui/NavUserHeader';
|
|
5
|
+
import { PageContentSection } from '#uilib/components/ui/Page';
|
|
6
|
+
import { GearSixIcon, UserCircleIcon } from '@phosphor-icons/react';
|
|
7
|
+
|
|
8
|
+
import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
|
|
9
|
+
import { DocsHeaderActions } from '../docsHeaderActions';
|
|
10
|
+
|
|
11
|
+
export default function NavUserHeaderPage() {
|
|
12
|
+
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
document.documentElement.dataset.theme = theme;
|
|
16
|
+
return () => {
|
|
17
|
+
delete document.documentElement.dataset.theme;
|
|
18
|
+
};
|
|
19
|
+
}, [theme]);
|
|
20
|
+
|
|
21
|
+
const onThemeToggle = useCallback(() => {
|
|
22
|
+
setTheme(t => (t === 'dark' ? 'light' : 'dark'));
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const customMenuItems = (
|
|
26
|
+
<>
|
|
27
|
+
<DropdownMenuItem>
|
|
28
|
+
<UserCircleIcon />
|
|
29
|
+
Account (custom)
|
|
30
|
+
</DropdownMenuItem>
|
|
31
|
+
<DropdownMenuItem>
|
|
32
|
+
<GearSixIcon />
|
|
33
|
+
Settings (custom)
|
|
34
|
+
</DropdownMenuItem>
|
|
35
|
+
</>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
<AppPageHeader
|
|
41
|
+
breadcrumbs={[{ label: 'NavUserHeader' }]}
|
|
42
|
+
title="NavUserHeader"
|
|
43
|
+
subheader="User menu with label, custom rows, theme toggle, and logout."
|
|
44
|
+
actions={<DocsHeaderActions />}
|
|
45
|
+
/>
|
|
46
|
+
<PageContentSection
|
|
47
|
+
style={{ display: 'flex', flexWrap: 'wrap', gap: '2rem' }}
|
|
48
|
+
>
|
|
49
|
+
<div>
|
|
50
|
+
<p style={{ marginBottom: 8, fontSize: 12 }}>
|
|
51
|
+
Signed in · default variant
|
|
52
|
+
</p>
|
|
53
|
+
<NavUserHeader
|
|
54
|
+
user={{
|
|
55
|
+
name: 'Demo Analyst',
|
|
56
|
+
email: 'demo@sybilion.io',
|
|
57
|
+
avatar: '',
|
|
58
|
+
}}
|
|
59
|
+
theme={theme}
|
|
60
|
+
onThemeToggle={onThemeToggle}
|
|
61
|
+
onLogout={() => {
|
|
62
|
+
console.info('[docs] logout');
|
|
63
|
+
}}
|
|
64
|
+
menuItems={customMenuItems}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
<div>
|
|
68
|
+
<p style={{ marginBottom: 8, fontSize: 12 }}>
|
|
69
|
+
Signed in · compact variant
|
|
70
|
+
</p>
|
|
71
|
+
<NavUserHeader
|
|
72
|
+
variant="compact"
|
|
73
|
+
user={{
|
|
74
|
+
name: 'Compact',
|
|
75
|
+
email: 'compact@sybilion.io',
|
|
76
|
+
avatar: '',
|
|
77
|
+
}}
|
|
78
|
+
theme={theme}
|
|
79
|
+
onThemeToggle={onThemeToggle}
|
|
80
|
+
onLogout={() => {
|
|
81
|
+
console.info('[docs] logout compact');
|
|
82
|
+
}}
|
|
83
|
+
menuItems={customMenuItems}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
</PageContentSection>
|
|
87
|
+
</>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -9,7 +9,7 @@ export default function SybilionAuthProviderPage() {
|
|
|
9
9
|
<AppPageHeader
|
|
10
10
|
breadcrumbs={[{ label: 'SybilionAuthProvider' }]}
|
|
11
11
|
title="SybilionAuthProvider"
|
|
12
|
-
subheader="Auth0 SPA →
|
|
12
|
+
subheader="Auth0 SPA → sdk.auth.loginWithAuth0Identity → Sybilion JWT. Pass createSybilionSDK instance via sdk prop; greenfield: docs/standalone-apps.md (yarn add includes @sybilion/sdk, @auth0/auth0-react)."
|
|
13
13
|
actions={<DocsHeaderActions />}
|
|
14
14
|
/>
|
|
15
15
|
<PageContentSection title="Exports">
|
|
@@ -18,7 +18,10 @@ export default function SybilionAuthProviderPage() {
|
|
|
18
18
|
</p>
|
|
19
19
|
<ul>
|
|
20
20
|
<li>
|
|
21
|
-
<code>SybilionAuthProvider</code>
|
|
21
|
+
<code>SybilionAuthProvider</code> (<code>sdk</code> prop)
|
|
22
|
+
</li>
|
|
23
|
+
<li>
|
|
24
|
+
<code>getSybilionApiOriginFromSdk</code>
|
|
22
25
|
</li>
|
|
23
26
|
<li>
|
|
24
27
|
<code>useSybilionAuth</code>
|
package/src/docs/registry.ts
CHANGED
|
@@ -66,6 +66,12 @@ export const DOC_REGISTRY: DocEntry[] = [
|
|
|
66
66
|
section: 'Navigation',
|
|
67
67
|
load: () => import('./pages/BreadcrumbPage'),
|
|
68
68
|
},
|
|
69
|
+
{
|
|
70
|
+
slug: 'nav-user-header',
|
|
71
|
+
title: 'NavUserHeader',
|
|
72
|
+
section: 'Navigation',
|
|
73
|
+
load: () => import('./pages/NavUserHeaderPage'),
|
|
74
|
+
},
|
|
69
75
|
{
|
|
70
76
|
slug: 'card',
|
|
71
77
|
title: 'Card',
|
package/src/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ export * from './components/ui/LabelWithId';
|
|
|
31
31
|
export * from './components/ui/LegacyPlatformLink';
|
|
32
32
|
export * from './components/ui/Logo';
|
|
33
33
|
export * from './components/ui/MobileAdaptiveSelector';
|
|
34
|
+
export * from './components/ui/NavUserHeader';
|
|
34
35
|
export * from './components/ui/NumberControl';
|
|
35
36
|
export * from './components/ui/Page';
|
|
36
37
|
export * from './components/ui/Progress';
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type RedirectLoginOptions,
|
|
4
4
|
useAuth0,
|
|
5
5
|
} from '@auth0/auth0-react';
|
|
6
|
+
import type { LoginTokenResponse, SybilionSDK } from '@sybilion/sdk';
|
|
6
7
|
import type { JSX, ReactNode } from 'react';
|
|
7
8
|
import {
|
|
8
9
|
createContext,
|
|
@@ -15,13 +16,34 @@ import {
|
|
|
15
16
|
} from 'react';
|
|
16
17
|
|
|
17
18
|
import { normalizeApiBaseUrl } from '#uilib/sybilion-auth/authPaths';
|
|
18
|
-
import { exchangeAuth0AccessTokenForSybilionJwt } from '#uilib/sybilion-auth/exchangeSybilionToken';
|
|
19
19
|
|
|
20
20
|
const DEFAULT_TOKEN_KEY = 'sybilion.standalone.jwt';
|
|
21
21
|
|
|
22
|
+
function sybilionJwtFromLoginResponse(body: LoginTokenResponse): string {
|
|
23
|
+
const t = body.data?.token ?? body.token;
|
|
24
|
+
if (!t) throw new Error('Sybilion auth: missing token in login response');
|
|
25
|
+
return t;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Origin (`scheme://host:port`) for paths like `/api/v1/...`; derived from SDK URL layout. */
|
|
29
|
+
export function getSybilionApiOriginFromSdk(sdk: SybilionSDK): string {
|
|
30
|
+
const loginUrl = sdk.http.buildUrl('/v1/auth/login');
|
|
31
|
+
try {
|
|
32
|
+
const baseHref =
|
|
33
|
+
typeof window !== 'undefined' && window.location?.href
|
|
34
|
+
? window.location.href
|
|
35
|
+
: 'http://localhost/';
|
|
36
|
+
const u = new URL(loginUrl, baseHref);
|
|
37
|
+
if (!u.pathname.endsWith('/v1/auth/login')) return '';
|
|
38
|
+
return u.origin;
|
|
39
|
+
} catch {
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
22
44
|
export type SybilionAuthProviderProps = {
|
|
23
45
|
children: ReactNode;
|
|
24
|
-
|
|
46
|
+
sdk: SybilionSDK;
|
|
25
47
|
auth0Domain: string;
|
|
26
48
|
auth0ClientId: string;
|
|
27
49
|
redirectUri: string;
|
|
@@ -119,12 +141,12 @@ function writeLs(key: string, value: string | null): void {
|
|
|
119
141
|
|
|
120
142
|
function InnerSybilionSession({
|
|
121
143
|
children,
|
|
122
|
-
|
|
144
|
+
sdk,
|
|
123
145
|
storageKey,
|
|
124
146
|
logoutReturnTo,
|
|
125
147
|
}: {
|
|
126
148
|
children: ReactNode;
|
|
127
|
-
|
|
149
|
+
sdk: SybilionSDK;
|
|
128
150
|
storageKey: string;
|
|
129
151
|
logoutReturnTo?: string;
|
|
130
152
|
}): JSX.Element {
|
|
@@ -155,6 +177,8 @@ function InnerSybilionSession({
|
|
|
155
177
|
});
|
|
156
178
|
}, [persistToken, logoutReturnTo]);
|
|
157
179
|
|
|
180
|
+
const apiBaseUrl = useMemo(() => getSybilionApiOriginFromSdk(sdk), [sdk]);
|
|
181
|
+
|
|
158
182
|
useEffect(() => {
|
|
159
183
|
if (!auth0.isAuthenticated || !auth0.user?.sub) {
|
|
160
184
|
persistToken(null);
|
|
@@ -174,10 +198,8 @@ function InnerSybilionSession({
|
|
|
174
198
|
setExchangeError(null);
|
|
175
199
|
try {
|
|
176
200
|
const access = await auth0Ref.current.getAccessTokenSilently();
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
access,
|
|
180
|
-
);
|
|
201
|
+
const loginBody = await sdk.auth.loginWithAuth0Identity(access);
|
|
202
|
+
const jwt = sybilionJwtFromLoginResponse(loginBody);
|
|
181
203
|
if (cancelled) return;
|
|
182
204
|
persistToken(jwt);
|
|
183
205
|
} catch (e) {
|
|
@@ -198,7 +220,7 @@ function InnerSybilionSession({
|
|
|
198
220
|
cancelled = true;
|
|
199
221
|
};
|
|
200
222
|
}, [
|
|
201
|
-
|
|
223
|
+
sdk,
|
|
202
224
|
auth0.isAuthenticated,
|
|
203
225
|
auth0.user?.sub,
|
|
204
226
|
persistToken,
|
|
@@ -270,7 +292,7 @@ function InnerSybilionSession({
|
|
|
270
292
|
|
|
271
293
|
export function SybilionAuthProvider({
|
|
272
294
|
children,
|
|
273
|
-
|
|
295
|
+
sdk,
|
|
274
296
|
auth0Domain,
|
|
275
297
|
auth0ClientId,
|
|
276
298
|
redirectUri,
|
|
@@ -311,7 +333,7 @@ export function SybilionAuthProvider({
|
|
|
311
333
|
{...cookieOpts}
|
|
312
334
|
>
|
|
313
335
|
<InnerSybilionSession
|
|
314
|
-
|
|
336
|
+
sdk={sdk}
|
|
315
337
|
storageKey={sybilionTokenStorageKey}
|
|
316
338
|
logoutReturnTo={logoutReturnTo}
|
|
317
339
|
>
|
|
@@ -3,13 +3,17 @@ import {
|
|
|
3
3
|
normalizeApiBaseUrl,
|
|
4
4
|
} from '#uilib/sybilion-auth/authPaths';
|
|
5
5
|
|
|
6
|
+
/** Default API segment before `/v1/...`; matches {@link createSybilionSDK} default `apiPrefix`. */
|
|
7
|
+
const SYBILION_AUTH_API_PREFIX = '/api';
|
|
8
|
+
|
|
6
9
|
/** POST `{ identity: auth0AccessToken, type: 'auth0' }` → Sybilion API JWT string. */
|
|
7
10
|
export async function exchangeAuth0AccessTokenForSybilionJwt(
|
|
11
|
+
/** API origin only (no trailing slash), same as SDK `baseUrl`. */
|
|
8
12
|
apiBaseUrl: string,
|
|
9
13
|
auth0AccessToken: string,
|
|
10
14
|
): Promise<string> {
|
|
11
15
|
const base = normalizeApiBaseUrl(apiBaseUrl);
|
|
12
|
-
const res = await fetch(`${base}${SYBILION_AUTH_LOGIN_PATH}`, {
|
|
16
|
+
const res = await fetch(`${base}${SYBILION_AUTH_API_PREFIX}${SYBILION_AUTH_LOGIN_PATH}`, {
|
|
13
17
|
method: 'POST',
|
|
14
18
|
headers: {
|
|
15
19
|
Accept: 'application/json',
|