@sybilion/uilib 1.2.9 → 1.2.10
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/NavUserHeader/NavUserHeader.js +8 -3
- package/dist/esm/components/widgets/SybilionAppHeader/SybilionAppHeader.js +2 -2
- package/dist/esm/components/widgets/SybilionSignInPanel/SybilionSignInPanel.styl.js +1 -1
- package/dist/esm/contexts/theme-context.js +44 -0
- package/dist/esm/docs/lib/theme.js +35 -3
- package/dist/esm/index.js +2 -0
- package/dist/esm/types/src/components/ui/NavUserHeader/NavUserHeader.d.ts +1 -1
- package/dist/esm/types/src/components/ui/NavUserHeader/NavUserHeader.types.d.ts +3 -0
- package/dist/esm/types/src/components/widgets/SybilionAppHeader/SybilionAppHeader.d.ts +5 -1
- package/dist/esm/types/src/contexts/theme-context.d.ts +20 -0
- package/dist/esm/types/src/docs/contexts/theme-context.d.ts +1 -10
- package/dist/esm/types/src/docs/lib/theme.d.ts +5 -1
- package/dist/esm/types/src/index.d.ts +2 -0
- package/docs/standalone-apps.md +22 -21
- package/package.json +1 -1
- package/src/components/ui/NavUserHeader/NavUserHeader.tsx +10 -2
- package/src/components/ui/NavUserHeader/NavUserHeader.types.ts +3 -0
- package/src/components/widgets/SybilionAppHeader/SybilionAppHeader.tsx +8 -0
- package/src/components/widgets/SybilionSignInPanel/SybilionSignInPanel.styl +4 -1
- package/src/contexts/theme-context.tsx +106 -0
- package/src/docs/App/ThemeToggle.tsx +1 -1
- package/src/docs/contexts/theme-context.tsx +8 -68
- package/src/docs/index.tsx +1 -1
- package/src/docs/lib/theme.ts +13 -2
- package/src/docs/pages/ChartAreaInteractivePage.tsx +1 -1
- package/src/index.ts +2 -0
- package/dist/esm/docs/contexts/theme-context.js +0 -14
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
2
2
|
import cn from 'classnames';
|
|
3
|
-
import { useTheme } from '../../../
|
|
3
|
+
import { useTheme } from '../../../contexts/theme-context.js';
|
|
4
4
|
import { UserCircleIcon, SunIcon, MoonIcon, SignOutIcon } from '@phosphor-icons/react';
|
|
5
5
|
import { ChevronDownIcon } from 'lucide-react';
|
|
6
6
|
import { Avatar } from '../Avatar/Avatar.js';
|
|
@@ -9,8 +9,13 @@ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuLab
|
|
|
9
9
|
import { Image } from '../Image/Image.js';
|
|
10
10
|
import S from './NavUserHeader.styl.js';
|
|
11
11
|
|
|
12
|
-
function NavUserHeader({ variant = 'default', isLoading = false, isAuthenticated, user = null, menuItems, onLogout, signInSlot, onSignInClick, }) {
|
|
13
|
-
const
|
|
12
|
+
function NavUserHeader({ variant = 'default', isLoading = false, isAuthenticated, user = null, menuItems, onLogout, signInSlot, onSignInClick, theme: themeFromHost, onThemeToggle: onThemeToggleFromHost, }) {
|
|
13
|
+
const docsTheme = useTheme();
|
|
14
|
+
const hostControlsTheme = themeFromHost !== undefined && onThemeToggleFromHost !== undefined;
|
|
15
|
+
const theme = hostControlsTheme ? themeFromHost : docsTheme.theme;
|
|
16
|
+
const toggleTheme = hostControlsTheme
|
|
17
|
+
? onThemeToggleFromHost
|
|
18
|
+
: docsTheme.toggleTheme;
|
|
14
19
|
const authenticated = isAuthenticated ?? true;
|
|
15
20
|
const avatarUrl = user?.avatar ?? '';
|
|
16
21
|
const userName = user?.name ?? '';
|
|
@@ -11,8 +11,8 @@ import '@phosphor-icons/react';
|
|
|
11
11
|
import 'lucide-react';
|
|
12
12
|
import S from './SybilionAppHeader.styl.js';
|
|
13
13
|
|
|
14
|
-
function SybilionAppHeader({ pageHeaderId, actionsAnchorId = PAGE_HEADER_ACTIONS_ID, actionsAnchorClassName, pathname, onNavigate, authenticated, defaultApps, appsStorageKey, logo, logoAreaClassName, welcomeBannerOffset, ...navUserHeaderProps }) {
|
|
15
|
-
return (jsxs(AppHeaderPortal, { pageHeaderId: pageHeaderId, children: [jsx("div", { className: cn(S.logoArea, welcomeBannerOffset && S.logoAreaWithBanner, logoAreaClassName), children: jsx(Link, { to: "/", className: S.logoLink, children: logo ?? jsx(Logo, { size: "md", "aria-hidden": true }) }) }), jsx(WorkspaceAppSwitcher, { pathname: pathname, onNavigate: onNavigate, authenticated: authenticated, defaultApps: defaultApps, appsStorageKey: appsStorageKey }), jsx(Gap, {}),
|
|
14
|
+
function SybilionAppHeader({ pageHeaderId, actionsAnchorId = PAGE_HEADER_ACTIONS_ID, actionsAnchorClassName, actionsStart, actionsEnd, pathname, onNavigate, authenticated, defaultApps, appsStorageKey, logo, logoAreaClassName, welcomeBannerOffset, ...navUserHeaderProps }) {
|
|
15
|
+
return (jsxs(AppHeaderPortal, { pageHeaderId: pageHeaderId, children: [jsx("div", { className: cn(S.logoArea, welcomeBannerOffset && S.logoAreaWithBanner, logoAreaClassName), children: jsx(Link, { to: "/", className: S.logoLink, children: logo ?? jsx(Logo, { size: "md", "aria-hidden": true }) }) }), jsx(WorkspaceAppSwitcher, { pathname: pathname, onNavigate: onNavigate, authenticated: authenticated, defaultApps: defaultApps, appsStorageKey: appsStorageKey }), jsx(Gap, {}), jsxs("div", { id: actionsAnchorId, className: cn(S.actionsAnchor, actionsAnchorClassName), children: [actionsStart, jsx(NavUserHeader, { ...navUserHeaderProps }), actionsEnd] })] }));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export { SybilionAppHeader };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import styleInject from 'style-inject';
|
|
2
2
|
|
|
3
|
-
var css_248z = ".SybilionSignInPanel_socialButtonContainer__lKNpp{display:flex;gap:10px;margin-bottom:10px;width:100%}.SybilionSignInPanel_socialButton__TqlKb{border:1px solid var(--border);border-radius:14px;flex:1;font-size:14px;font-weight:600;height:48px;padding:14px 16px}.SybilionSignInPanel_errorMessage__o8Q-m{color:red;font-size:14px;margin-bottom:10px;padding:10px}.SybilionSignInPanel_forgotPassword__47q20{margin-bottom:24px;text-align:right}.SybilionSignInPanel_forgotPasswordLink__lOJCv{color:var(--primary);font-family:Manrope,sans-serif;font-size:14px;font-weight:500;text-decoration:none;transition:opacity .2s}.SybilionSignInPanel_forgotPasswordLink__lOJCv:hover{opacity:.8}.SybilionSignInPanel_version__d5KAz{box-sizing:border-box;color:var(--muted-foreground);display:block;font-size:14px;margin-top:var(--p-8);opacity:.5;padding-bottom:var(--p-2);text-align:center;transition:opacity .3s ease-out;white-space:nowrap;width:
|
|
3
|
+
var css_248z = ".SybilionSignInPanel_socialButtonContainer__lKNpp{display:flex;gap:10px;margin-bottom:10px;width:100%}.SybilionSignInPanel_socialButton__TqlKb{border:1px solid var(--border);border-radius:14px;flex:1;font-size:14px;font-weight:600;height:48px;padding:14px 16px}.SybilionSignInPanel_errorMessage__o8Q-m{color:red;font-size:14px;margin-bottom:10px;padding:10px}.SybilionSignInPanel_forgotPassword__47q20{margin-bottom:24px;text-align:right}.SybilionSignInPanel_forgotPasswordLink__lOJCv{color:var(--primary);font-family:Manrope,sans-serif;font-size:14px;font-weight:500;text-decoration:none;transition:opacity .2s}.SybilionSignInPanel_forgotPasswordLink__lOJCv:hover{opacity:.8}.SybilionSignInPanel_version__d5KAz{bottom:0;box-sizing:border-box;color:var(--muted-foreground);display:block;font-size:14px;margin-top:var(--p-8);opacity:.5;padding-bottom:var(--p-2);position:absolute;right:0;text-align:center;transition:opacity .3s ease-out;white-space:nowrap;width:50%}";
|
|
4
4
|
var S = {"socialButtonContainer":"SybilionSignInPanel_socialButtonContainer__lKNpp","socialButton":"SybilionSignInPanel_socialButton__TqlKb","errorMessage":"SybilionSignInPanel_errorMessage__o8Q-m","forgotPassword":"SybilionSignInPanel_forgotPassword__47q20","forgotPasswordLink":"SybilionSignInPanel_forgotPasswordLink__lOJCv","version":"SybilionSignInPanel_version__d5KAz"};
|
|
5
5
|
styleInject(css_248z);
|
|
6
6
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { createContext, useMemo, useState, useCallback, useEffect, useContext } from 'react';
|
|
3
|
+
import { Theme } from '@homecode/ui';
|
|
4
|
+
import { getThemeConfig } from '../docs/lib/theme.js';
|
|
5
|
+
|
|
6
|
+
const ThemeContext = createContext({
|
|
7
|
+
theme: 'light',
|
|
8
|
+
isDarkMode: false,
|
|
9
|
+
setTheme: () => { },
|
|
10
|
+
toggleTheme: () => { },
|
|
11
|
+
});
|
|
12
|
+
function ThemeProvider({ children, allowLocalStorage = true, activeColor, getThemeConfig: getThemeConfigProp, }) {
|
|
13
|
+
const getThemeConfig$1 = getThemeConfigProp ?? getThemeConfig;
|
|
14
|
+
const themeConfigOptions = useMemo(() => ({ activeColor }), [activeColor]);
|
|
15
|
+
const [theme, setTheme] = useState(() => {
|
|
16
|
+
return localStorage.getItem('theme') || 'light';
|
|
17
|
+
});
|
|
18
|
+
const [currThemeConfig, setCurrThemeConfig] = useState(() => getThemeConfig$1(theme === 'dark', themeConfigOptions));
|
|
19
|
+
const toggleTheme = useCallback(() => {
|
|
20
|
+
setTheme(t => (t === 'dark' ? 'light' : 'dark'));
|
|
21
|
+
}, []);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
setCurrThemeConfig(getThemeConfig$1(theme === 'dark', themeConfigOptions));
|
|
24
|
+
}, [theme, getThemeConfig$1, themeConfigOptions]);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const root = document.documentElement;
|
|
27
|
+
const effectiveTheme = theme;
|
|
28
|
+
root.classList.remove('light', 'dark');
|
|
29
|
+
root.classList.add(effectiveTheme);
|
|
30
|
+
if (allowLocalStorage) {
|
|
31
|
+
localStorage.setItem('theme', theme);
|
|
32
|
+
}
|
|
33
|
+
}, [theme, allowLocalStorage]);
|
|
34
|
+
const value = useMemo(() => ({
|
|
35
|
+
theme,
|
|
36
|
+
isDarkMode: theme === 'dark',
|
|
37
|
+
setTheme,
|
|
38
|
+
toggleTheme,
|
|
39
|
+
}), [theme, toggleTheme]);
|
|
40
|
+
return (jsxs(ThemeContext.Provider, { value: value, children: [jsx(Theme, { config: currThemeConfig }), children] }));
|
|
41
|
+
}
|
|
42
|
+
const useTheme = () => useContext(ThemeContext);
|
|
43
|
+
|
|
44
|
+
export { ThemeProvider, useTheme };
|
|
@@ -2,8 +2,10 @@ import { ThemeHelpers, ThemeDefaults } from '@homecode/ui';
|
|
|
2
2
|
|
|
3
3
|
const { colors, getColors, getConfig } = ThemeDefaults;
|
|
4
4
|
getColors();
|
|
5
|
-
getConfig();
|
|
6
|
-
|
|
5
|
+
const defaultConfig = getConfig();
|
|
6
|
+
const alphaMods = [0, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
|
|
7
|
+
const DEFAULT_THEME_ACTIVE_COLOR = '#9c59ff';
|
|
8
|
+
const colorsConfig = {
|
|
7
9
|
light: {
|
|
8
10
|
...ThemeHelpers.colorsConfigToVars({
|
|
9
11
|
...getColors({
|
|
@@ -20,4 +22,34 @@ getConfig();
|
|
|
20
22
|
}),
|
|
21
23
|
}),
|
|
22
24
|
},
|
|
23
|
-
}
|
|
25
|
+
};
|
|
26
|
+
function getThemeConfig(isDarkTheme, options) {
|
|
27
|
+
const activeColor = options?.activeColor ?? DEFAULT_THEME_ACTIVE_COLOR;
|
|
28
|
+
return {
|
|
29
|
+
...defaultConfig,
|
|
30
|
+
...colorsConfig[isDarkTheme ? 'dark' : 'light'],
|
|
31
|
+
...ThemeHelpers.colorsConfigToVars({
|
|
32
|
+
active: {
|
|
33
|
+
color: activeColor,
|
|
34
|
+
mods: {
|
|
35
|
+
// @ts-ignore
|
|
36
|
+
alpha: alphaMods,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
danger: {
|
|
40
|
+
color: '#ee0000',
|
|
41
|
+
mods: {
|
|
42
|
+
alpha: alphaMods,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
warning: {
|
|
46
|
+
color: '#ffa500',
|
|
47
|
+
mods: {
|
|
48
|
+
alpha: alphaMods,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { DEFAULT_THEME_ACTIVE_COLOR, colorsConfig, getThemeConfig };
|
package/dist/esm/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { ThemeProvider, useTheme } from './contexts/theme-context.js';
|
|
2
|
+
export { DEFAULT_THEME_ACTIVE_COLOR } from './docs/lib/theme.js';
|
|
1
3
|
export { SybilionAuthProvider, createSybilionApiFetch, getSybilionApiOriginFromSdk, sybilionApiFetch, useSybilionApiFetch, useSybilionAuth } from './sybilion-auth/SybilionAuthProvider.js';
|
|
2
4
|
export { SYBILION_AUTH_LOGIN_PATH, normalizeApiBaseUrl } from './sybilion-auth/authPaths.js';
|
|
3
5
|
export { exchangeAuth0AccessTokenForSybilionJwt } from './sybilion-auth/exchangeSybilionToken.js';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { NavUserHeaderProps } from './NavUserHeader.types';
|
|
2
|
-
export declare function NavUserHeader({ variant, isLoading, isAuthenticated, user, menuItems, 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;
|
|
2
|
+
export declare function NavUserHeader({ variant, isLoading, isAuthenticated, user, menuItems, onLogout, signInSlot, onSignInClick, theme: themeFromHost, onThemeToggle: onThemeToggleFromHost, }: 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;
|
|
@@ -18,4 +18,7 @@ export type NavUserHeaderProps = {
|
|
|
18
18
|
/** Replaces default “Log in” control when signed out. */
|
|
19
19
|
signInSlot?: ReactNode;
|
|
20
20
|
onSignInClick?: () => void;
|
|
21
|
+
/** When both are set, theme row uses these instead of uilib `ThemeProvider` context. */
|
|
22
|
+
theme?: 'light' | 'dark';
|
|
23
|
+
onThemeToggle?: () => void;
|
|
21
24
|
};
|
|
@@ -5,10 +5,14 @@ export type SybilionAppHeaderProps = WorkspaceAppSwitcherProps & NavUserHeaderPr
|
|
|
5
5
|
pageHeaderId?: string;
|
|
6
6
|
actionsAnchorId?: string;
|
|
7
7
|
actionsAnchorClassName?: string;
|
|
8
|
+
/** Renders before `NavUserHeader` inside the page actions anchor (e.g. impersonation). */
|
|
9
|
+
actionsStart?: ReactNode;
|
|
10
|
+
/** Renders after `NavUserHeader` inside the page actions anchor (e.g. notifications). */
|
|
11
|
+
actionsEnd?: ReactNode;
|
|
8
12
|
/** Branded markup; omit for default lucide tile + «Sybilion». */
|
|
9
13
|
logo?: ReactNode;
|
|
10
14
|
logoAreaClassName?: string;
|
|
11
15
|
/** Applies vertical offset when a welcome banner consumes top space (CSS `--welcome-banner-height` on shell). */
|
|
12
16
|
welcomeBannerOffset?: boolean;
|
|
13
17
|
};
|
|
14
|
-
export declare function SybilionAppHeader({ pageHeaderId, actionsAnchorId, actionsAnchorClassName, pathname, onNavigate, authenticated, defaultApps, appsStorageKey, logo, logoAreaClassName, welcomeBannerOffset, ...navUserHeaderProps }: SybilionAppHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export declare function SybilionAppHeader({ pageHeaderId, actionsAnchorId, actionsAnchorClassName, actionsStart, actionsEnd, pathname, onNavigate, authenticated, defaultApps, appsStorageKey, logo, logoAreaClassName, welcomeBannerOffset, ...navUserHeaderProps }: SybilionAppHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getThemeConfig as defaultGetThemeConfig, type GetThemeConfigOptions } from '#uilib/docs/lib/theme';
|
|
2
|
+
export type ThemeMode = 'light' | 'dark';
|
|
3
|
+
export type GetThemeConfigFn = (isDarkTheme: boolean, options?: GetThemeConfigOptions) => ReturnType<typeof defaultGetThemeConfig>;
|
|
4
|
+
export type { GetThemeConfigOptions };
|
|
5
|
+
export type ThemeProviderProps = {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
/** When false, DOM classes still update but theme is not persisted to `localStorage`. */
|
|
8
|
+
allowLocalStorage?: boolean;
|
|
9
|
+
/** Override docs default active/accent token (`DEFAULT_THEME_ACTIVE_COLOR`). Passed into `getThemeConfig`. */
|
|
10
|
+
activeColor?: string;
|
|
11
|
+
/** Homecode theme config; defaults to uilib docs palette. */
|
|
12
|
+
getThemeConfig?: GetThemeConfigFn;
|
|
13
|
+
};
|
|
14
|
+
export declare function ThemeProvider({ children, allowLocalStorage, activeColor, getThemeConfig: getThemeConfigProp, }: ThemeProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export declare const useTheme: () => {
|
|
16
|
+
theme: ThemeMode;
|
|
17
|
+
isDarkMode: boolean;
|
|
18
|
+
setTheme: (theme: ThemeMode) => void;
|
|
19
|
+
toggleTheme: () => void;
|
|
20
|
+
};
|
|
@@ -1,10 +1 @@
|
|
|
1
|
-
export type ThemeMode
|
|
2
|
-
export declare function ThemeProvider({ children }: {
|
|
3
|
-
children: React.ReactNode;
|
|
4
|
-
}): import("react/jsx-runtime").JSX.Element;
|
|
5
|
-
export declare const useTheme: () => {
|
|
6
|
-
theme: ThemeMode;
|
|
7
|
-
isDarkMode: boolean;
|
|
8
|
-
setTheme: (theme: ThemeMode) => void;
|
|
9
|
-
toggleTheme: () => void;
|
|
10
|
-
};
|
|
1
|
+
export { ThemeProvider, useTheme, type GetThemeConfigFn, type GetThemeConfigOptions, type ThemeMode, type ThemeProviderProps, } from '#uilib/contexts/theme-context';
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
export declare const DEFAULT_THEME_ACTIVE_COLOR = "#9c59ff";
|
|
2
|
+
export type GetThemeConfigOptions = {
|
|
3
|
+
activeColor?: string;
|
|
4
|
+
};
|
|
1
5
|
export declare const colorsConfig: {
|
|
2
6
|
light: {};
|
|
3
7
|
dark: {};
|
|
4
8
|
};
|
|
5
|
-
export declare function getThemeConfig(isDarkTheme: boolean): any;
|
|
9
|
+
export declare function getThemeConfig(isDarkTheme: boolean, options?: GetThemeConfigOptions): any;
|
package/docs/standalone-apps.md
CHANGED
|
@@ -392,7 +392,11 @@ Wire **`authenticated`**, **`user`** / **`isAuthenticated`**, **`theme`** / **`o
|
|
|
392
392
|
|
|
393
393
|
#### Sidebar (`AppSidebar.tsx`)
|
|
394
394
|
|
|
395
|
-
App-specific sidebar component — keeps the navigation surface out of `AppLayout` so the shell stays generic.
|
|
395
|
+
App-specific sidebar component — keeps the navigation surface out of `AppLayout` so the shell stays generic.
|
|
396
|
+
|
|
397
|
+
**One sidebar group (default):** Standalone apps use **a single `SidebarGroup`** for the whole sidebar menu unless the **product owner explicitly asks** for another section separated into its own group. **`SidebarDatasetsItemsGrouped`** ([`src/components/widgets/SidebarDatasetsItemsGrouped/SidebarDatasetsItemsGrouped.tsx`](../../src/components/widgets/SidebarDatasetsItemsGrouped/SidebarDatasetsItemsGrouped.tsx)) already renders **one** outer `SidebarGroup` — put primary routes (Home, Datasets, …) in **`preItems`** or **`postItems`** so nav and dataset rows share that group instead of wrapping routes in a separate **`SidebarGroup`** above/below the widget. Apps without datasets: one **`SidebarGroup`** + **`SidebarMenu`** / **`SidebarMenuItem`** only.
|
|
398
|
+
|
|
399
|
+
Collapsible dataset clusters + nested rows: demo **`src/docs/pages/SidebarDatasetsItemsGroupedPage.tsx`** (slug **`sidebar-datasets-items-grouped`**). Other primitives: **`Sidebar`** + **`SidebarContent`** + **`SidebarMenu*`**.
|
|
396
400
|
|
|
397
401
|
```tsx
|
|
398
402
|
import { useEffect, useState } from 'react';
|
|
@@ -403,8 +407,6 @@ import {
|
|
|
403
407
|
SidebarContent,
|
|
404
408
|
SidebarDatasetsItemsGrouped,
|
|
405
409
|
type SidebarDatasetsItemsGroupedDataset,
|
|
406
|
-
SidebarGroup,
|
|
407
|
-
SidebarMenu,
|
|
408
410
|
SidebarMenuButton,
|
|
409
411
|
SidebarMenuItem,
|
|
410
412
|
} from '@sybilion/uilib';
|
|
@@ -427,27 +429,26 @@ export function AppSidebar() {
|
|
|
427
429
|
return (
|
|
428
430
|
<Sidebar variant="inset" collapsible="offcanvas">
|
|
429
431
|
<SidebarContent>
|
|
430
|
-
<SidebarGroup>
|
|
431
|
-
<SidebarMenu>
|
|
432
|
-
<SidebarMenuItem>
|
|
433
|
-
<SidebarMenuButton asChild>
|
|
434
|
-
<NavLink to="/" end>
|
|
435
|
-
Home
|
|
436
|
-
</NavLink>
|
|
437
|
-
</SidebarMenuButton>
|
|
438
|
-
</SidebarMenuItem>
|
|
439
|
-
<SidebarMenuItem>
|
|
440
|
-
<SidebarMenuButton asChild>
|
|
441
|
-
<NavLink to="/datasets">Datasets</NavLink>
|
|
442
|
-
</SidebarMenuButton>
|
|
443
|
-
</SidebarMenuItem>
|
|
444
|
-
</SidebarMenu>
|
|
445
|
-
</SidebarGroup>
|
|
446
|
-
|
|
447
432
|
<SidebarDatasetsItemsGrouped
|
|
448
433
|
groupBy="regions"
|
|
449
434
|
datasets={datasets}
|
|
450
435
|
selectedDatasetId={selectedDatasetId}
|
|
436
|
+
preItems={
|
|
437
|
+
<>
|
|
438
|
+
<SidebarMenuItem>
|
|
439
|
+
<SidebarMenuButton asChild>
|
|
440
|
+
<NavLink to="/" end>
|
|
441
|
+
Home
|
|
442
|
+
</NavLink>
|
|
443
|
+
</SidebarMenuButton>
|
|
444
|
+
</SidebarMenuItem>
|
|
445
|
+
<SidebarMenuItem>
|
|
446
|
+
<SidebarMenuButton asChild>
|
|
447
|
+
<NavLink to="/datasets">Datasets</NavLink>
|
|
448
|
+
</SidebarMenuButton>
|
|
449
|
+
</SidebarMenuItem>
|
|
450
|
+
</>
|
|
451
|
+
}
|
|
451
452
|
onDatasetClick={id => {
|
|
452
453
|
setSelectedDatasetId(id);
|
|
453
454
|
navigate(`/datasets/${id}`);
|
|
@@ -530,7 +531,7 @@ Composition: `PageScroll` → `AppShell` → `AppSidebar` → `AppShellMainConte
|
|
|
530
531
|
| `NavUserHeader` | Header user menu (avatar, account, theme, logout). |
|
|
531
532
|
| `Sidebar`, `SidebarProvider` | Collapsible rail + context (`@sybilion/uilib`). Wrap `SidebarProvider` above `AppLayout`; render `Sidebar` inside `AppShell` (usually via `AppSidebar`). |
|
|
532
533
|
| `AppSidebar` | App-specific component (`src/AppSidebar.tsx`) composing `Sidebar` + nav links + product widgets. Keeps `AppLayout` generic. |
|
|
533
|
-
| `SidebarDatasetsItemsGrouped` |
|
|
534
|
+
| `SidebarDatasetsItemsGrouped` | Single-sidebar-group widget: **`preItems`** / **`postItems`** for main nav + collapsible dataset groups (`regions` / `target_type` / `categories`) + **`onDatasetClick`**. Prefer this over stacking multiple **`SidebarGroup`** blocks unless explicitly requested. Source: **`src/components/widgets/SidebarDatasetsItemsGrouped/SidebarDatasetsItemsGrouped.tsx`**. |
|
|
534
535
|
| `SidebarTrigger` | Toggle sidebar visibility (especially mobile / `offcanvas`). |
|
|
535
536
|
| `PageFooter` | Standard footer; requires `versionLink` + `versionLabel`. |
|
|
536
537
|
| `Gap` | Spacing primitive between flex children. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import cn from 'classnames';
|
|
2
2
|
|
|
3
|
-
import { useTheme } from '#uilib/
|
|
3
|
+
import { useTheme } from '#uilib/contexts/theme-context';
|
|
4
4
|
import {
|
|
5
5
|
MoonIcon,
|
|
6
6
|
SignOutIcon,
|
|
@@ -33,8 +33,16 @@ export function NavUserHeader({
|
|
|
33
33
|
onLogout,
|
|
34
34
|
signInSlot,
|
|
35
35
|
onSignInClick,
|
|
36
|
+
theme: themeFromHost,
|
|
37
|
+
onThemeToggle: onThemeToggleFromHost,
|
|
36
38
|
}: NavUserHeaderProps) {
|
|
37
|
-
const
|
|
39
|
+
const docsTheme = useTheme();
|
|
40
|
+
const hostControlsTheme =
|
|
41
|
+
themeFromHost !== undefined && onThemeToggleFromHost !== undefined;
|
|
42
|
+
const theme = hostControlsTheme ? themeFromHost : docsTheme.theme;
|
|
43
|
+
const toggleTheme = hostControlsTheme
|
|
44
|
+
? onThemeToggleFromHost
|
|
45
|
+
: docsTheme.toggleTheme;
|
|
38
46
|
const authenticated = isAuthenticated ?? true;
|
|
39
47
|
|
|
40
48
|
const avatarUrl = user?.avatar ?? '';
|
|
@@ -20,4 +20,7 @@ export type NavUserHeaderProps = {
|
|
|
20
20
|
/** Replaces default “Log in” control when signed out. */
|
|
21
21
|
signInSlot?: ReactNode;
|
|
22
22
|
onSignInClick?: () => void;
|
|
23
|
+
/** When both are set, theme row uses these instead of uilib `ThemeProvider` context. */
|
|
24
|
+
theme?: 'light' | 'dark';
|
|
25
|
+
onThemeToggle?: () => void;
|
|
23
26
|
};
|
|
@@ -20,6 +20,10 @@ export type SybilionAppHeaderProps = WorkspaceAppSwitcherProps &
|
|
|
20
20
|
pageHeaderId?: string;
|
|
21
21
|
actionsAnchorId?: string;
|
|
22
22
|
actionsAnchorClassName?: string;
|
|
23
|
+
/** Renders before `NavUserHeader` inside the page actions anchor (e.g. impersonation). */
|
|
24
|
+
actionsStart?: ReactNode;
|
|
25
|
+
/** Renders after `NavUserHeader` inside the page actions anchor (e.g. notifications). */
|
|
26
|
+
actionsEnd?: ReactNode;
|
|
23
27
|
/** Branded markup; omit for default lucide tile + «Sybilion». */
|
|
24
28
|
logo?: ReactNode;
|
|
25
29
|
logoAreaClassName?: string;
|
|
@@ -31,6 +35,8 @@ export function SybilionAppHeader({
|
|
|
31
35
|
pageHeaderId,
|
|
32
36
|
actionsAnchorId = PAGE_HEADER_ACTIONS_ID,
|
|
33
37
|
actionsAnchorClassName,
|
|
38
|
+
actionsStart,
|
|
39
|
+
actionsEnd,
|
|
34
40
|
pathname,
|
|
35
41
|
onNavigate,
|
|
36
42
|
authenticated,
|
|
@@ -67,7 +73,9 @@ export function SybilionAppHeader({
|
|
|
67
73
|
id={actionsAnchorId}
|
|
68
74
|
className={cn(S.actionsAnchor, actionsAnchorClassName)}
|
|
69
75
|
>
|
|
76
|
+
{actionsStart}
|
|
70
77
|
<NavUserHeader {...navUserHeaderProps} />
|
|
78
|
+
{actionsEnd}
|
|
71
79
|
</div>
|
|
72
80
|
</AppHeaderPortal>
|
|
73
81
|
);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react';
|
|
9
|
+
|
|
10
|
+
import { Theme as ThemeRoot } from '@homecode/ui';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
getThemeConfig as defaultGetThemeConfig,
|
|
14
|
+
type GetThemeConfigOptions,
|
|
15
|
+
} from '#uilib/docs/lib/theme';
|
|
16
|
+
|
|
17
|
+
export type ThemeMode = 'light' | 'dark';
|
|
18
|
+
|
|
19
|
+
export type GetThemeConfigFn = (
|
|
20
|
+
isDarkTheme: boolean,
|
|
21
|
+
options?: GetThemeConfigOptions,
|
|
22
|
+
) => ReturnType<typeof defaultGetThemeConfig>;
|
|
23
|
+
|
|
24
|
+
export type { GetThemeConfigOptions };
|
|
25
|
+
|
|
26
|
+
const ThemeContext = createContext<{
|
|
27
|
+
theme: ThemeMode;
|
|
28
|
+
isDarkMode: boolean;
|
|
29
|
+
setTheme: (theme: ThemeMode) => void;
|
|
30
|
+
toggleTheme: () => void;
|
|
31
|
+
}>({
|
|
32
|
+
theme: 'light',
|
|
33
|
+
isDarkMode: false,
|
|
34
|
+
setTheme: () => {},
|
|
35
|
+
toggleTheme: () => {},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type ThemeProviderProps = {
|
|
39
|
+
children: React.ReactNode;
|
|
40
|
+
/** When false, DOM classes still update but theme is not persisted to `localStorage`. */
|
|
41
|
+
allowLocalStorage?: boolean;
|
|
42
|
+
/** Override docs default active/accent token (`DEFAULT_THEME_ACTIVE_COLOR`). Passed into `getThemeConfig`. */
|
|
43
|
+
activeColor?: string;
|
|
44
|
+
/** Homecode theme config; defaults to uilib docs palette. */
|
|
45
|
+
getThemeConfig?: GetThemeConfigFn;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function ThemeProvider({
|
|
49
|
+
children,
|
|
50
|
+
allowLocalStorage = true,
|
|
51
|
+
activeColor,
|
|
52
|
+
getThemeConfig: getThemeConfigProp,
|
|
53
|
+
}: ThemeProviderProps) {
|
|
54
|
+
const getThemeConfig = getThemeConfigProp ?? defaultGetThemeConfig;
|
|
55
|
+
|
|
56
|
+
const themeConfigOptions = useMemo<GetThemeConfigOptions>(
|
|
57
|
+
() => ({ activeColor }),
|
|
58
|
+
[activeColor],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const [theme, setTheme] = useState<ThemeMode>(() => {
|
|
62
|
+
return (localStorage.getItem('theme') as ThemeMode) || 'light';
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const [currThemeConfig, setCurrThemeConfig] = useState(() =>
|
|
66
|
+
getThemeConfig(theme === 'dark', themeConfigOptions),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const toggleTheme = useCallback(() => {
|
|
70
|
+
setTheme(t => (t === 'dark' ? 'light' : 'dark'));
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
setCurrThemeConfig(getThemeConfig(theme === 'dark', themeConfigOptions));
|
|
75
|
+
}, [theme, getThemeConfig, themeConfigOptions]);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
const root = document.documentElement;
|
|
79
|
+
const effectiveTheme = theme;
|
|
80
|
+
|
|
81
|
+
root.classList.remove('light', 'dark');
|
|
82
|
+
root.classList.add(effectiveTheme);
|
|
83
|
+
if (allowLocalStorage) {
|
|
84
|
+
localStorage.setItem('theme', theme);
|
|
85
|
+
}
|
|
86
|
+
}, [theme, allowLocalStorage]);
|
|
87
|
+
|
|
88
|
+
const value = useMemo(
|
|
89
|
+
() => ({
|
|
90
|
+
theme,
|
|
91
|
+
isDarkMode: theme === 'dark',
|
|
92
|
+
setTheme,
|
|
93
|
+
toggleTheme,
|
|
94
|
+
}),
|
|
95
|
+
[theme, toggleTheme],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<ThemeContext.Provider value={value}>
|
|
100
|
+
<ThemeRoot config={currThemeConfig} />
|
|
101
|
+
{children}
|
|
102
|
+
</ThemeContext.Provider>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const useTheme = () => useContext(ThemeContext);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Button } from '#uilib/components/ui/Button';
|
|
2
|
-
import { useTheme } from '#uilib/
|
|
2
|
+
import { useTheme } from '#uilib/contexts/theme-context';
|
|
3
3
|
import { MoonIcon, SunIcon } from '@phosphor-icons/react';
|
|
4
4
|
|
|
5
5
|
export function ThemeToggle() {
|
|
@@ -1,68 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import { Theme as ThemeRoot } from '@homecode/ui';
|
|
10
|
-
|
|
11
|
-
import { getThemeConfig } from '../lib/theme';
|
|
12
|
-
|
|
13
|
-
export type ThemeMode = 'light' | 'dark';
|
|
14
|
-
|
|
15
|
-
const ThemeContext = createContext<{
|
|
16
|
-
theme: ThemeMode;
|
|
17
|
-
isDarkMode: boolean;
|
|
18
|
-
setTheme: (theme: ThemeMode) => void;
|
|
19
|
-
toggleTheme: () => void;
|
|
20
|
-
}>({
|
|
21
|
-
theme: 'light',
|
|
22
|
-
isDarkMode: false,
|
|
23
|
-
setTheme: () => {},
|
|
24
|
-
toggleTheme: () => {},
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
|
28
|
-
const [theme, setTheme] = useState<ThemeMode>(() => {
|
|
29
|
-
return (localStorage.getItem('theme') as ThemeMode) || 'light';
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const [currThemeConfig, setCurrThemeConfig] = useState(() =>
|
|
33
|
-
getThemeConfig(theme === 'dark'),
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
const toggleTheme = useCallback(() => {
|
|
37
|
-
setTheme(theme === 'dark' ? 'light' : 'dark');
|
|
38
|
-
}, [theme, setTheme]);
|
|
39
|
-
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
setCurrThemeConfig(getThemeConfig(theme === 'dark'));
|
|
42
|
-
}, [theme]);
|
|
43
|
-
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
const root = document.documentElement;
|
|
46
|
-
const effectiveTheme = theme;
|
|
47
|
-
|
|
48
|
-
root.classList.remove('light', 'dark');
|
|
49
|
-
root.classList.add(effectiveTheme);
|
|
50
|
-
localStorage.setItem('theme', theme);
|
|
51
|
-
}, [theme]);
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<ThemeContext.Provider
|
|
55
|
-
value={{
|
|
56
|
-
theme,
|
|
57
|
-
isDarkMode: theme === 'dark',
|
|
58
|
-
setTheme,
|
|
59
|
-
toggleTheme,
|
|
60
|
-
}}
|
|
61
|
-
>
|
|
62
|
-
<ThemeRoot config={currThemeConfig} />
|
|
63
|
-
{children}
|
|
64
|
-
</ThemeContext.Provider>
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export const useTheme = () => useContext(ThemeContext);
|
|
1
|
+
export {
|
|
2
|
+
ThemeProvider,
|
|
3
|
+
useTheme,
|
|
4
|
+
type GetThemeConfigFn,
|
|
5
|
+
type GetThemeConfigOptions,
|
|
6
|
+
type ThemeMode,
|
|
7
|
+
type ThemeProviderProps,
|
|
8
|
+
} from '#uilib/contexts/theme-context';
|
package/src/docs/index.tsx
CHANGED
package/src/docs/lib/theme.ts
CHANGED
|
@@ -7,6 +7,12 @@ const defaultConfig = getConfig();
|
|
|
7
7
|
|
|
8
8
|
const alphaMods = [0, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
|
|
9
9
|
|
|
10
|
+
export const DEFAULT_THEME_ACTIVE_COLOR = '#9c59ff';
|
|
11
|
+
|
|
12
|
+
export type GetThemeConfigOptions = {
|
|
13
|
+
activeColor?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
10
16
|
export const colorsConfig = {
|
|
11
17
|
light: {
|
|
12
18
|
...ThemeHelpers.colorsConfigToVars({
|
|
@@ -26,13 +32,18 @@ export const colorsConfig = {
|
|
|
26
32
|
},
|
|
27
33
|
};
|
|
28
34
|
|
|
29
|
-
export function getThemeConfig(
|
|
35
|
+
export function getThemeConfig(
|
|
36
|
+
isDarkTheme: boolean,
|
|
37
|
+
options?: GetThemeConfigOptions,
|
|
38
|
+
) {
|
|
39
|
+
const activeColor = options?.activeColor ?? DEFAULT_THEME_ACTIVE_COLOR;
|
|
40
|
+
|
|
30
41
|
return {
|
|
31
42
|
...defaultConfig,
|
|
32
43
|
...colorsConfig[isDarkTheme ? 'dark' : 'light'],
|
|
33
44
|
...ThemeHelpers.colorsConfigToVars({
|
|
34
45
|
active: {
|
|
35
|
-
color:
|
|
46
|
+
color: activeColor,
|
|
36
47
|
mods: {
|
|
37
48
|
// @ts-ignore
|
|
38
49
|
alpha: alphaMods,
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
import { ForecastItemData } from '#uilib/components/ui/ChartAreaInteractive/ChartLines';
|
|
10
10
|
import { PageContentSection } from '#uilib/components/ui/Page';
|
|
11
11
|
import { Tabs, TabsList, TabsTrigger } from '#uilib/components/ui/Tabs';
|
|
12
|
-
import { useTheme } from '#uilib/
|
|
12
|
+
import { useTheme } from '#uilib/contexts/theme-context';
|
|
13
13
|
import type { ForecastData } from '#uilib/types/forecast-data';
|
|
14
14
|
|
|
15
15
|
import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
|
package/src/index.ts
CHANGED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import 'react/jsx-runtime';
|
|
2
|
-
import { createContext, useContext } from 'react';
|
|
3
|
-
import '@homecode/ui';
|
|
4
|
-
import '../lib/theme.js';
|
|
5
|
-
|
|
6
|
-
const ThemeContext = createContext({
|
|
7
|
-
theme: 'light',
|
|
8
|
-
isDarkMode: false,
|
|
9
|
-
setTheme: () => { },
|
|
10
|
-
toggleTheme: () => { },
|
|
11
|
-
});
|
|
12
|
-
const useTheme = () => useContext(ThemeContext);
|
|
13
|
-
|
|
14
|
-
export { useTheme };
|