@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.
Files changed (27) hide show
  1. package/dist/esm/components/ui/NavUserHeader/NavUserHeader.js +8 -3
  2. package/dist/esm/components/widgets/SybilionAppHeader/SybilionAppHeader.js +2 -2
  3. package/dist/esm/components/widgets/SybilionSignInPanel/SybilionSignInPanel.styl.js +1 -1
  4. package/dist/esm/contexts/theme-context.js +44 -0
  5. package/dist/esm/docs/lib/theme.js +35 -3
  6. package/dist/esm/index.js +2 -0
  7. package/dist/esm/types/src/components/ui/NavUserHeader/NavUserHeader.d.ts +1 -1
  8. package/dist/esm/types/src/components/ui/NavUserHeader/NavUserHeader.types.d.ts +3 -0
  9. package/dist/esm/types/src/components/widgets/SybilionAppHeader/SybilionAppHeader.d.ts +5 -1
  10. package/dist/esm/types/src/contexts/theme-context.d.ts +20 -0
  11. package/dist/esm/types/src/docs/contexts/theme-context.d.ts +1 -10
  12. package/dist/esm/types/src/docs/lib/theme.d.ts +5 -1
  13. package/dist/esm/types/src/index.d.ts +2 -0
  14. package/docs/standalone-apps.md +22 -21
  15. package/package.json +1 -1
  16. package/src/components/ui/NavUserHeader/NavUserHeader.tsx +10 -2
  17. package/src/components/ui/NavUserHeader/NavUserHeader.types.ts +3 -0
  18. package/src/components/widgets/SybilionAppHeader/SybilionAppHeader.tsx +8 -0
  19. package/src/components/widgets/SybilionSignInPanel/SybilionSignInPanel.styl +4 -1
  20. package/src/contexts/theme-context.tsx +106 -0
  21. package/src/docs/App/ThemeToggle.tsx +1 -1
  22. package/src/docs/contexts/theme-context.tsx +8 -68
  23. package/src/docs/index.tsx +1 -1
  24. package/src/docs/lib/theme.ts +13 -2
  25. package/src/docs/pages/ChartAreaInteractivePage.tsx +1 -1
  26. package/src/index.ts +2 -0
  27. 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 '../../../docs/contexts/theme-context.js';
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 { toggleTheme, theme } = useTheme();
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, {}), jsx("div", { id: actionsAnchorId, className: cn(S.actionsAnchor, actionsAnchorClassName), children: jsx(NavUserHeader, { ...navUserHeaderProps }) })] }));
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:100%}";
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 = 'light' | 'dark';
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;
@@ -1,3 +1,5 @@
1
+ export * from './contexts/theme-context';
2
+ export { DEFAULT_THEME_ACTIVE_COLOR } from './docs/lib/theme';
1
3
  export * from './sybilion-auth';
2
4
  export * from './types/sybilionDatasetSnapshots';
3
5
  export * from './contexts/chat-context';
@@ -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. Compose your nav from `@sybilion/uilib` primitives (`Sidebar` + `SidebarContent` + `SidebarGroup` + `SidebarMenu*`) and product widgets like `SidebarDatasetsItemsGrouped` (collapsible groups + nested rows for datasets — see demo `src/docs/pages/SidebarDatasetsItemsGroupedPage.tsx`, slug `sidebar-datasets-items-grouped`).
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` | Dataset list widget for the sidebar: collapsible groups (`regions` / `target_type` / `categories`) with nested rows + selection callback. |
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
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.2.9",
3
+ "version": "1.2.10",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1,6 +1,6 @@
1
1
  import cn from 'classnames';
2
2
 
3
- import { useTheme } from '#uilib/docs/contexts/theme-context';
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 { toggleTheme, theme } = useTheme();
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
  );
@@ -35,10 +35,13 @@
35
35
  opacity 0.8
36
36
 
37
37
  .version
38
+ position absolute
39
+ bottom 0
40
+ right 0
38
41
  display block
39
42
  margin-top var(--p-8)
40
43
  padding-bottom var(--p-2)
41
- width 100%
44
+ width 50%
42
45
  box-sizing border-box
43
46
  text-align center
44
47
  font-size 14px
@@ -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/docs/contexts/theme-context';
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
- import {
2
- createContext,
3
- useCallback,
4
- useContext,
5
- useEffect,
6
- useState,
7
- } from 'react';
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';
@@ -9,7 +9,7 @@ const root = createRoot(elem);
9
9
 
10
10
  root.render(
11
11
  <BrowserRouter>
12
- <ThemeProvider>
12
+ <ThemeProvider activeColor="#">
13
13
  <App />
14
14
  </ThemeProvider>
15
15
  </BrowserRouter>,
@@ -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(isDarkTheme: boolean) {
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: '#9c59ff',
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/docs/contexts/theme-context';
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,3 +1,5 @@
1
+ export * from './contexts/theme-context';
2
+ export { DEFAULT_THEME_ACTIVE_COLOR } from './docs/lib/theme';
1
3
  export * from './sybilion-auth';
2
4
  export * from './types/sybilionDatasetSnapshots';
3
5
  export * from './contexts/chat-context';
@@ -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 };