@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.
Files changed (30) hide show
  1. package/dist/esm/components/ui/Image/Image.styl.js +1 -1
  2. package/dist/esm/components/ui/NavUserHeader/NavUserHeader.js +28 -0
  3. package/dist/esm/components/ui/NavUserHeader/NavUserHeader.styl.js +7 -0
  4. package/dist/esm/index.js +2 -1
  5. package/dist/esm/sybilion-auth/SybilionAuthProvider.js +30 -7
  6. package/dist/esm/sybilion-auth/exchangeSybilionToken.js +6 -2
  7. package/dist/esm/types/src/components/ui/NavUserHeader/NavUserHeader.d.ts +2 -0
  8. package/dist/esm/types/src/components/ui/NavUserHeader/NavUserHeader.types.d.ts +25 -0
  9. package/dist/esm/types/src/components/ui/NavUserHeader/index.d.ts +2 -0
  10. package/dist/esm/types/src/docs/pages/NavUserHeaderPage.d.ts +1 -0
  11. package/dist/esm/types/src/index.d.ts +1 -0
  12. package/dist/esm/types/src/sybilion-auth/SybilionAuthProvider.d.ts +5 -2
  13. package/dist/esm/types/src/sybilion-auth/exchangeSybilionToken.d.ts +3 -1
  14. package/dist/esm/types/src/sybilion-auth/index.d.ts +1 -1
  15. package/docs/standalone-apps.md +137 -21
  16. package/package.json +6 -1
  17. package/src/components/ui/Image/Image.styl +1 -0
  18. package/src/components/ui/NavUserHeader/NavUserHeader.styl +125 -0
  19. package/src/components/ui/NavUserHeader/NavUserHeader.styl.d.ts +28 -0
  20. package/src/components/ui/NavUserHeader/NavUserHeader.tsx +148 -0
  21. package/src/components/ui/NavUserHeader/NavUserHeader.types.ts +27 -0
  22. package/src/components/ui/NavUserHeader/avatar.svg +4 -0
  23. package/src/components/ui/NavUserHeader/index.ts +5 -0
  24. package/src/docs/pages/NavUserHeaderPage.tsx +89 -0
  25. package/src/docs/pages/SybilionAuthProviderPage.tsx +5 -2
  26. package/src/docs/registry.ts +6 -0
  27. package/src/index.ts +1 -0
  28. package/src/sybilion-auth/SybilionAuthProvider.tsx +33 -11
  29. package/src/sybilion-auth/exchangeSybilionToken.ts +5 -1
  30. 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, apiBaseUrl, storageKey, logoutReturnTo, }) {
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 jwt = await exchangeAuth0AccessTokenForSybilionJwt(apiBaseUrl, access);
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
- apiBaseUrl,
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, apiBaseUrl, auth0Domain, auth0ClientId, redirectUri, authorizationParams, sybilionTokenStorageKey = DEFAULT_TOKEN_KEY, logoutReturnTo, }) {
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, { apiBaseUrl: apiBaseUrl, storageKey: sybilionTokenStorageKey, logoutReturnTo: logoutReturnTo, children: children }) }));
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(apiBaseUrl, auth0AccessToken) {
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,2 @@
1
+ export { NavUserHeader } from './NavUserHeader';
2
+ export type { NavUserHeaderProps, NavUserHeaderUser, } from './NavUserHeader.types';
@@ -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
- apiBaseUrl: string;
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, apiBaseUrl, auth0Domain, auth0ClientId, redirectUri, authorizationParams, sybilionTokenStorageKey, logoutReturnTo, }: SybilionAuthProviderProps): JSX.Element;
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(apiBaseUrl: string, auth0AccessToken: string): Promise<string>;
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';
@@ -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, `**SybilionAuthProvider`\*\* + Sybilion API for data—no iframe in the main client.
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 (no random root horizontal gutters).
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
- Pattern: `[src/docs/DocsShell.tsx](https://github.com/Mir-Insight/uilib/blob/main/src/docs/DocsShell.tsx)` — `PageScroll` → `AppShell` → sidebar → `AppShellMainContent` with `AppHeaderHost`, `PageFooter`, main content (`Outlet` / routes). Add `Theme` and optional `SidebarProvider` from `@homecode/ui`.
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_`\* is exposed client-side:
94
+ Env vars depend on bundler—for **Vite** only `import.meta.env.VITE_` is exposed client-side:
28
95
 
29
96
  ```tsx
30
- import { SybilionAuthProvider } from '@sybilion/uilib';
97
+ import { useMemo, type ReactNode } from 'react';
31
98
 
32
- const apiBaseUrl = import.meta.env.VITE_SYBILION_API_BASE_URL as string;
33
- const auth0Domain = import.meta.env.VITE_AUTH0_DOMAIN as string;
34
- const auth0ClientId = import.meta.env.VITE_AUTH0_CLIENT_ID as string;
35
-
36
- <SybilionAuthProvider
37
- apiBaseUrl={apiBaseUrl}
38
- auth0Domain={auth0Domain}
39
- auth0ClientId={auth0ClientId}
40
- redirectUri={window.location.origin}
41
- >
42
- <App />
43
- </SybilionAuthProvider>;
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 → `POST {apiBaseUrl}/v1/auth/login` with `{ identity: "<Auth0 AT>", type: "auth0" }` → Sybilion JWT in response (`data.token` or `token`) → Bearer on API calls.
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** | `apiBaseUrl`, Auth0 `domain` / `clientId`, redirect usually `window.location.origin`. |
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—no host `postMessage` bridge.
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.0",
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",
@@ -3,6 +3,7 @@
3
3
  display inline-block
4
4
  width 100%
5
5
  height 100%
6
+ background-color var(--muted-50)
6
7
 
7
8
  .image
8
9
  display block
@@ -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,5 @@
1
+ export { NavUserHeader } from './NavUserHeader';
2
+ export type {
3
+ NavUserHeaderProps,
4
+ NavUserHeaderUser,
5
+ } from './NavUserHeader.types';
@@ -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 → POST /v1/auth/login → Sybilion JWT. Greenfield installs: docs/standalone-apps.md (yarn add line includes @auth0/auth0-react)."
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>
@@ -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
- apiBaseUrl: string;
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
- apiBaseUrl,
144
+ sdk,
123
145
  storageKey,
124
146
  logoutReturnTo,
125
147
  }: {
126
148
  children: ReactNode;
127
- apiBaseUrl: string;
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 jwt = await exchangeAuth0AccessTokenForSybilionJwt(
178
- apiBaseUrl,
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
- apiBaseUrl,
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
- apiBaseUrl,
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
- apiBaseUrl={apiBaseUrl}
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',
@@ -4,6 +4,7 @@ export type {
4
4
  } from '#uilib/sybilion-auth/SybilionAuthProvider';
5
5
  export {
6
6
  SybilionAuthProvider,
7
+ getSybilionApiOriginFromSdk,
7
8
  useSybilionAuth,
8
9
  sybilionApiFetch,
9
10
  createSybilionApiFetch,