@djangocfg/layouts 2.1.357 → 2.1.359

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 (37) hide show
  1. package/package.json +21 -19
  2. package/src/configurator/private/schema.ts +20 -0
  3. package/src/layouts/PrivateLayout/PrivateLayout.tsx +17 -1
  4. package/src/layouts/PrivateLayout/README.md +47 -1
  5. package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +5 -72
  6. package/src/layouts/PrivateLayout/components/PrivateSidebarAccount.tsx +47 -96
  7. package/src/layouts/PrivateLayout/components/SidebarBrand.tsx +36 -17
  8. package/src/layouts/PrivateLayout/components/SidebarBrandSwitcher.tsx +223 -0
  9. package/src/layouts/PrivateLayout/components/index.ts +1 -0
  10. package/src/layouts/PrivateLayout/context.tsx +2 -9
  11. package/src/layouts/PrivateLayout/hooks/index.ts +1 -5
  12. package/src/layouts/PrivateLayout/hooks/useHoverExpand.ts +10 -3
  13. package/src/layouts/PrivateLayout/hooks/useShellVisualState.ts +11 -88
  14. package/src/layouts/PrivateLayout/hooks/useSidebarDefaultOpen.ts +32 -0
  15. package/src/layouts/PrivateLayout/index.ts +3 -0
  16. package/src/layouts/PrivateLayout/types.ts +41 -0
  17. package/src/layouts/ProfileLayout/ProfileDialog/ProfileDialog.tsx +32 -0
  18. package/src/layouts/ProfileLayout/ProfileDialog/index.ts +2 -0
  19. package/src/layouts/ProfileLayout/ProfileDialog/store.ts +19 -0
  20. package/src/layouts/ProfileLayout/{context.tsx → ProfileForm/context.tsx} +4 -2
  21. package/src/layouts/ProfileLayout/{ProfileLayout.tsx → ProfileForm/index.tsx} +10 -7
  22. package/src/layouts/ProfileLayout/README.md +65 -5
  23. package/src/layouts/ProfileLayout/components/EditableField.tsx +1 -1
  24. package/src/layouts/ProfileLayout/components/PreferencesSection.tsx +56 -0
  25. package/src/layouts/ProfileLayout/components/ProfileHeader.tsx +1 -1
  26. package/src/layouts/ProfileLayout/components/ProfileTab.tsx +17 -11
  27. package/src/layouts/ProfileLayout/components/index.ts +1 -0
  28. package/src/layouts/ProfileLayout/hooks/useProfileTabs.ts +17 -12
  29. package/src/layouts/ProfileLayout/index.ts +5 -4
  30. package/src/layouts/ProfileLayout/types.ts +11 -1
  31. package/src/layouts/_components/index.ts +1 -0
  32. package/src/layouts/types/providers.types.ts +2 -2
  33. package/src/theme/ThemeStyleBridge.tsx +1 -3
  34. package/src/theme/index.ts +2 -4
  35. package/src/theme/buildThemeStyleSheet.ts +0 -71
  36. package/src/theme/themeStyle.types.ts +0 -89
  37. package/src/theme/themeStylePresets.ts +0 -202
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ } from '@djangocfg/ui-core/components';
11
+
12
+ import { ProfileForm } from '../ProfileForm';
13
+ import { useProfileDialogStore } from './store';
14
+
15
+ export interface ProfileDialogProps {
16
+ title?: string;
17
+ }
18
+
19
+ export const ProfileDialog: React.FC<ProfileDialogProps> = ({ title }) => {
20
+ const { isOpen, close, initialTab } = useProfileDialogStore();
21
+
22
+ return (
23
+ <Dialog open={isOpen} onOpenChange={(open) => !open && close()}>
24
+ <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto p-0">
25
+ <DialogHeader className="sr-only">
26
+ <DialogTitle>Profile</DialogTitle>
27
+ </DialogHeader>
28
+ <ProfileForm title={title} defaultTab={initialTab} />
29
+ </DialogContent>
30
+ </Dialog>
31
+ );
32
+ };
@@ -0,0 +1,2 @@
1
+ export { ProfileDialog } from './ProfileDialog';
2
+ export { useProfileDialogStore } from './store';
@@ -0,0 +1,19 @@
1
+ import { create } from 'zustand';
2
+
3
+ import type { ProfileTabValue } from '../hooks/useProfileTabs';
4
+
5
+ interface ProfileDialogState {
6
+ isOpen: boolean;
7
+ initialTab: ProfileTabValue | undefined;
8
+ open: (options?: { initialTab?: ProfileTabValue }) => void;
9
+ close: () => void;
10
+ toggle: () => void;
11
+ }
12
+
13
+ export const useProfileDialogStore = create<ProfileDialogState>((set) => ({
14
+ isOpen: false,
15
+ initialTab: undefined,
16
+ open: (options) => set({ isOpen: true, initialTab: options?.initialTab }),
17
+ close: () => set({ isOpen: false, initialTab: undefined }),
18
+ toggle: () => set((state) => ({ isOpen: !state.isOpen })),
19
+ }));
@@ -6,8 +6,8 @@ import { useAuth } from '@djangocfg/api/auth';
6
6
  import { useAppT } from '@djangocfg/i18n';
7
7
  import { toast } from '@djangocfg/ui-core/hooks';
8
8
 
9
- import { profileLogger } from '../../utils/logger';
10
- import { useLogout } from '../../hooks';
9
+ import { profileLogger } from '../../../utils/logger';
10
+ import { useLogout } from '../../../hooks';
11
11
 
12
12
  // ─────────────────────────────────────────────────────────────────────────────
13
13
  // Types
@@ -19,6 +19,7 @@ export interface ProfileLabels {
19
19
  work: string;
20
20
  security: string;
21
21
  apiKeys: string;
22
+ preferences: string;
22
23
  firstName: string;
23
24
  lastName: string;
24
25
  phone: string;
@@ -79,6 +80,7 @@ export const ProfileProvider: React.FC<ProfileProviderProps> = ({ children, titl
79
80
  work: t('layouts.profilePage.work'),
80
81
  security: t('layouts.profilePage.security'),
81
82
  apiKeys: 'API Keys',
83
+ preferences: 'Preferences',
82
84
  firstName: t('layouts.profilePage.firstName'),
83
85
  lastName: t('layouts.profilePage.lastName'),
84
86
  phone: t('layouts.profilePage.phone'),
@@ -12,11 +12,11 @@ import {
12
12
  TabsTrigger,
13
13
  } from '@djangocfg/ui-core/components';
14
14
 
15
- import { ApiKeySection, ProfileHeader, ProfileTab, TwoFactorSection } from './components';
15
+ import { ApiKeySection, ProfileHeader, ProfileTab, TwoFactorSection } from '../components';
16
16
  import { ProfileProvider, useProfileContext } from './context';
17
- import { useProfileTabs } from './hooks/useProfileTabs';
18
- import type { ProfileLayoutProps } from './types';
19
- import type { ProfileTabValue } from './hooks/useProfileTabs';
17
+ import { useProfileTabs } from '../hooks/useProfileTabs';
18
+ import type { ProfileFormProps } from '../types';
19
+ import type { ProfileTabValue } from '../hooks/useProfileTabs';
20
20
 
21
21
  // ─────────────────────────────────────────────────────────────────────────────
22
22
  // Built-in tab panels
@@ -49,15 +49,18 @@ function ProfileContent({
49
49
  enableDeleteAccount = true,
50
50
  tabs = [],
51
51
  slots,
52
- }: ProfileLayoutProps) {
52
+ defaultTab,
53
+ }: ProfileFormProps) {
53
54
  const { labels } = useProfileContext();
54
55
  const { user, isLoading } = useAuth();
55
56
 
56
57
  const extraTabValues = React.useMemo(() => tabs.map((t) => t.value), [tabs]);
57
- const { tab, setTab } = useProfileTabs({
58
+
59
+ const { tab, setTab, allowed } = useProfileTabs({
58
60
  enable2FA,
59
61
  enableAPIKeys,
60
62
  extraTabValues,
63
+ defaultTab,
61
64
  });
62
65
 
63
66
  const handleTabChange = React.useCallback(
@@ -136,7 +139,7 @@ function ProfileContent({
136
139
  // Export
137
140
  // ─────────────────────────────────────────────────────────────────────────────
138
141
 
139
- export const ProfileLayout: React.FC<ProfileLayoutProps> = ({ title, ...props }) => (
142
+ export const ProfileForm: React.FC<ProfileFormProps> = ({ title, ...props }) => (
140
143
  <ProfileProvider title={title}>
141
144
  <ProfileContent title={title} {...props} />
142
145
  </ProfileProvider>
@@ -4,10 +4,12 @@ User profile page with tabbed interface: **Profile** | **Security** | **API Keys
4
4
 
5
5
  ## Usage
6
6
 
7
+ ### Standalone page
8
+
7
9
  ```tsx
8
- import { ProfileLayout } from '@djangocfg/layouts';
10
+ import { ProfileForm } from '@djangocfg/layouts';
9
11
 
10
- <ProfileLayout
12
+ <ProfileForm
11
13
  enable2FA
12
14
  enableAPIKeys
13
15
  enableDeleteAccount
@@ -19,6 +21,18 @@ import { ProfileLayout } from '@djangocfg/layouts';
19
21
  />
20
22
  ```
21
23
 
24
+ ### Dialog
25
+
26
+ ```tsx
27
+ import { ProfileDialog, useProfileDialogStore } from '@djangocfg/layouts';
28
+
29
+ // Open on a specific tab
30
+ useProfileDialogStore.getState().open({ initialTab: 'security' });
31
+
32
+ // In your layout (lazy-loaded)
33
+ <ProfileDialog />
34
+ ```
35
+
22
36
  ## Props
23
37
 
24
38
  | Prop | Default | Description |
@@ -30,14 +44,60 @@ import { ProfileLayout } from '@djangocfg/layouts';
30
44
  | `tabs` | `[]` | Extra `ProfileTab[]` appended after built-in tabs |
31
45
  | `slots` | — | Named slots: `headerMenuItems`, `headerBadge`, `headerAfter`, `footer` |
32
46
  | `title` | i18n | Page title |
47
+ | `defaultTab` | — | Initial active tab. When provided (e.g. by `ProfileDialog`), tabs start at this value. |
48
+
49
+ ## Global Profile Dialog
50
+
51
+ `ProfileDialog` is a Zustand-driven dialog that can be opened from anywhere:
52
+
53
+ ```tsx
54
+ import { useProfileDialogStore } from '@djangocfg/layouts';
55
+
56
+ const { open, close } = useProfileDialogStore();
57
+
58
+ // Open on Profile tab
59
+ open();
60
+
61
+ // Open on Security tab
62
+ open({ initialTab: 'security' });
63
+ ```
64
+
65
+ Wire it into `PrivateLayout` for global access:
66
+
67
+ ```tsx
68
+ import { PrivateLayout } from '@djangocfg/layouts';
69
+
70
+ <PrivateLayout sidebar={...} header={...}>
71
+ {children}
72
+ </PrivateLayout>
73
+ // ProfileDialog is rendered automatically inside PrivateLayout
74
+ ```
75
+
76
+ ## Sidebar Account Button Modes
77
+
78
+ The footer account button in `PrivateLayout` supports two modes via `HeaderConfig`:
79
+
80
+ ```tsx
81
+ const header: HeaderConfig = {
82
+ // ...
83
+ accountAction: 'menu', // Default — opens DropdownMenu with links, theme, logout
84
+ accountAction: 'dialog', // Opens ProfileDialog instead
85
+ };
86
+ ```
33
87
 
34
88
  ## Architecture
35
89
 
36
90
  ```
37
91
  ProfileLayout/
38
- ├── ProfileLayout.tsx Shell: ProfileProvider → header + Tabs
39
- ├── context.tsx Root context (labels, onLogout, onFieldSave)
40
- ├── types.ts ProfileLayoutProps, ProfileTab, ProfileSlots
92
+ ├── ProfileForm/
93
+ ├── index.tsx Shell: ProfileProvider header + Tabs
94
+ │ └── context.tsx Root context (labels, onLogout, onFieldSave)
95
+ ├── ProfileDialog/
96
+ │ ├── ProfileDialog.tsx Dialog wrapper around ProfileForm
97
+ │ └── store.ts Zustand store (isOpen, initialTab, open/close)
98
+ ├── hooks/
99
+ │ └── useProfileTabs.ts Local tab state (useState)
100
+ ├── types.ts ProfileFormProps, ProfileTab, ProfileSlots
41
101
  └── components/
42
102
  ├── ProfileHeader Avatar + name + dropdown menu
43
103
  ├── ProfileTab Editable fields grid (first_name, last_name, phone, company, position)
@@ -6,7 +6,7 @@ import { parsePhoneNumberFromString } from 'libphonenumber-js';
6
6
  import { Button, Input, PhoneInput } from '@djangocfg/ui-core/components';
7
7
  import { cn } from '@djangocfg/ui-core/lib';
8
8
 
9
- import { useProfileContext } from '../context';
9
+ import { useProfileContext } from '../ProfileForm/context';
10
10
 
11
11
  function formatPhone(raw: string): string {
12
12
  if (!raw) return '';
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Preferences Section
3
+ *
4
+ * Language + theme controls for ProfileForm.
5
+ * Uses ThemeToggle from ui-nextjs and LocaleSwitcherDropdown from _components.
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import React from 'react';
11
+
12
+ import { ThemeToggle } from '@djangocfg/ui-nextjs/theme';
13
+ import { cn } from '@djangocfg/ui-core/lib';
14
+
15
+ import { LocaleSwitcherDropdown } from '../../_components/locale-switcher';
16
+ import { useLayoutI18nOptional } from '../../AppLayout/LayoutI18nProvider';
17
+
18
+ interface PreferencesSectionProps {
19
+ /** Extra className for the root. */
20
+ className?: string;
21
+ }
22
+
23
+ export const PreferencesSection: React.FC<PreferencesSectionProps> = ({
24
+ className,
25
+ }) => {
26
+ const layoutI18n = useLayoutI18nOptional();
27
+
28
+ return (
29
+ <div className={cn('py-0', className)}>
30
+ {layoutI18n && (
31
+ <>
32
+ <div className="flex items-center justify-between py-3">
33
+ <span className="text-sm">Language</span>
34
+ <LocaleSwitcherDropdown
35
+ locale={layoutI18n.locale}
36
+ locales={layoutI18n.locales}
37
+ onChange={layoutI18n.onLocaleChange}
38
+ variant="outline"
39
+ size="sm"
40
+ showCode
41
+ showIcon={false}
42
+ showFlag
43
+ showTriggerLabel
44
+ />
45
+ </div>
46
+ <div className="h-px bg-border/60" />
47
+ </>
48
+ )}
49
+
50
+ <div className="flex items-center justify-between py-3">
51
+ <span className="text-sm">Theme</span>
52
+ <ThemeToggle size="default" />
53
+ </div>
54
+ </div>
55
+ );
56
+ };
@@ -16,7 +16,7 @@ import {
16
16
  } from '@djangocfg/ui-core/components';
17
17
 
18
18
  import { AvatarSection } from './AvatarSection';
19
- import { useProfileContext } from '../context';
19
+ import { useProfileContext } from '../ProfileForm/context';
20
20
  import type { ProfileSlots } from '../types';
21
21
 
22
22
  interface ProfileHeaderProps {
@@ -4,8 +4,8 @@ import React from 'react';
4
4
 
5
5
  import { useAuth } from '@djangocfg/api/auth';
6
6
 
7
- import { EditableField, Section } from '.';
8
- import { useProfileContext } from '../context';
7
+ import { EditableField, PreferencesSection, Section } from '.';
8
+ import { useProfileContext } from '../ProfileForm/context';
9
9
 
10
10
  export const ProfileTab: React.FC = () => {
11
11
  const { labels, onFieldSave } = useProfileContext();
@@ -13,16 +13,22 @@ export const ProfileTab: React.FC = () => {
13
13
  if (!user) return null;
14
14
 
15
15
  return (
16
- <div className="grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-6 pt-4">
17
- <Section title={labels.personalInfo}>
18
- <EditableField label={labels.firstName} value={user.first_name || ''} placeholder={labels.addFirstName} onSave={(v) => onFieldSave('first_name', v)} />
19
- <EditableField label={labels.lastName} value={user.last_name || ''} placeholder={labels.addLastName} onSave={(v) => onFieldSave('last_name', v)} />
20
- <EditableField label={labels.phone} value={user.phone || ''} placeholder={labels.addPhone} onSave={(v) => onFieldSave('phone', v)} type="phone" />
21
- </Section>
16
+ <div className="space-y-6 pt-4">
17
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-6">
18
+ <Section title={labels.personalInfo}>
19
+ <EditableField label={labels.firstName} value={user.first_name || ''} placeholder={labels.addFirstName} onSave={(v) => onFieldSave('first_name', v)} />
20
+ <EditableField label={labels.lastName} value={user.last_name || ''} placeholder={labels.addLastName} onSave={(v) => onFieldSave('last_name', v)} />
21
+ <EditableField label={labels.phone} value={user.phone || ''} placeholder={labels.addPhone} onSave={(v) => onFieldSave('phone', v)} type="phone" />
22
+ </Section>
23
+
24
+ <Section title={labels.work}>
25
+ <EditableField label={labels.company} value={user.company || ''} placeholder={labels.addCompany} onSave={(v) => onFieldSave('company', v)} />
26
+ <EditableField label={labels.position} value={user.position || ''} placeholder={labels.addPosition} onSave={(v) => onFieldSave('position', v)} />
27
+ </Section>
28
+ </div>
22
29
 
23
- <Section title={labels.work}>
24
- <EditableField label={labels.company} value={user.company || ''} placeholder={labels.addCompany} onSave={(v) => onFieldSave('company', v)} />
25
- <EditableField label={labels.position} value={user.position || ''} placeholder={labels.addPosition} onSave={(v) => onFieldSave('position', v)} />
30
+ <Section title={labels.preferences ?? 'Preferences'}>
31
+ <PreferencesSection />
26
32
  </Section>
27
33
  </div>
28
34
  );
@@ -4,6 +4,7 @@ export type { ApiKeyLabels } from './ApiKeySection';
4
4
  export { AvatarSection } from './AvatarSection';
5
5
  export { DeleteAccountSection, DeleteAccountScreen } from './DeleteAccountSection';
6
6
  export { EditableField } from './EditableField';
7
+ export { PreferencesSection } from './PreferencesSection';
7
8
  export { ProfileHeader } from './ProfileHeader';
8
9
  export { ProfileTab } from './ProfileTab';
9
10
  export { Section } from './Section';
@@ -1,8 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useMemo } from 'react';
4
-
5
- import { useQueryState, parseAsStringEnum } from '@djangocfg/ui-core/hooks';
3
+ import { useMemo, useState, useCallback } from 'react';
6
4
 
7
5
  export type ProfileTabValue = 'profile' | 'security' | 'api-keys';
8
6
 
@@ -10,20 +8,21 @@ export interface UseProfileTabsOptions {
10
8
  enable2FA?: boolean;
11
9
  enableAPIKeys?: boolean;
12
10
  extraTabValues?: string[];
11
+ /** Initial active tab. Defaults to `'profile'`. */
12
+ defaultTab?: ProfileTabValue;
13
13
  }
14
14
 
15
15
  /**
16
- * Syncs the active profile tab with the URL query param `?tab=`.
16
+ * Manages the active profile tab with local React state.
17
17
  *
18
- * Falls back to `'profile'` when the param is missing or not allowed.
19
- * Setting the default value clears the key from the URL (no `?tab=profile`).
18
+ * Falls back to `'profile'` when `defaultTab` is missing or not allowed.
20
19
  *
21
20
  * @example
22
- * const { tab, setTab, allowed } = useProfileTabs({ enable2FA: true });
21
+ * const { tab, setTab, allowed } = useProfileTabs({ enable2FA: true, defaultTab: 'security' });
23
22
  * <Tabs value={tab} onValueChange={setTab}>...</Tabs>
24
23
  */
25
24
  export function useProfileTabs(options: UseProfileTabsOptions = {}) {
26
- const { enable2FA, enableAPIKeys, extraTabValues = [] } = options;
25
+ const { enable2FA, enableAPIKeys, extraTabValues = [], defaultTab } = options;
27
26
 
28
27
  const allowed = useMemo(() => {
29
28
  const base: ProfileTabValue[] = ['profile'];
@@ -32,12 +31,18 @@ export function useProfileTabs(options: UseProfileTabsOptions = {}) {
32
31
  return [...base, ...extraTabValues] as ProfileTabValue[];
33
32
  }, [enable2FA, enableAPIKeys, extraTabValues]);
34
33
 
35
- const parser = useMemo(
36
- () => parseAsStringEnum(allowed).withDefault('profile'),
37
- [allowed],
34
+ const [tab, setTabState] = useState<ProfileTabValue>(
35
+ defaultTab && allowed.includes(defaultTab) ? defaultTab : 'profile',
38
36
  );
39
37
 
40
- const [tab, setTab] = useQueryState('tab', parser, { replace: true });
38
+ const setTab = useCallback(
39
+ (value: ProfileTabValue) => {
40
+ if (allowed.includes(value)) {
41
+ setTabState(value);
42
+ }
43
+ },
44
+ [allowed],
45
+ );
41
46
 
42
47
  return { tab, setTab, allowed };
43
48
  }
@@ -1,6 +1,7 @@
1
- export { ProfileLayout, useProfileContext } from './ProfileLayout';
2
- export { ProfileProvider } from './context';
1
+ export { ProfileForm, useProfileContext } from './ProfileForm';
2
+ export { ProfileProvider } from './ProfileForm/context';
3
3
  export { useProfileTabs } from './hooks';
4
- export type { ProfileLabels } from './context';
5
- export type { ProfileLayoutProps, ProfileTab, ProfileSlots } from './types';
4
+ export { ProfileDialog, useProfileDialogStore } from './ProfileDialog';
5
+ export type { ProfileLabels } from './ProfileForm/context';
6
+ export type { ProfileFormProps, ProfileLayoutProps, ProfileTab, ProfileSlots } from './types';
6
7
  export type { ProfileTabValue, UseProfileTabsOptions } from './hooks';
@@ -1,5 +1,7 @@
1
1
  import React from 'react';
2
2
 
3
+ import type { ProfileTabValue } from './hooks/useProfileTabs';
4
+
3
5
  // ─────────────────────────────────────────────────────────────────────────────
4
6
  // Slot + Tab types
5
7
  // ─────────────────────────────────────────────────────────────────────────────
@@ -24,7 +26,7 @@ export interface ProfileSlots {
24
26
  footer?: React.ReactNode;
25
27
  }
26
28
 
27
- export interface ProfileLayoutProps {
29
+ export interface ProfileFormProps {
28
30
  onUnauthenticated?: () => void;
29
31
  title?: string;
30
32
  enable2FA?: boolean;
@@ -34,4 +36,12 @@ export interface ProfileLayoutProps {
34
36
  tabs?: ProfileTab[];
35
37
  /** Named slots for additional content */
36
38
  slots?: ProfileSlots;
39
+ /**
40
+ * When provided, the active tab is controlled locally (no URL sync).
41
+ * Useful for dialogs where query-string pollution is undesirable.
42
+ */
43
+ defaultTab?: ProfileTabValue;
37
44
  }
45
+
46
+ /** @deprecated Use ProfileFormProps instead */
47
+ export type ProfileLayoutProps = ProfileFormProps;
@@ -27,3 +27,4 @@ export type {
27
27
  LocaleSwitcherSharedProps,
28
28
  LocaleSwitcherVariant,
29
29
  } from './LocaleSwitcher';
30
+
@@ -5,10 +5,10 @@
5
5
  * Note: Analytics, PWA, Push, and Error types are defined in their respective modules
6
6
  */
7
7
 
8
- import type { ThemeStyleConfig } from '../../theme/themeStyle.types';
8
+ import type { ThemeStyleConfig } from '@djangocfg/ui-core/styles/presets';
9
9
 
10
10
  // Re-export for consumers that only import from `layouts/types`
11
- export type { ThemeStyleConfig, ThemeCssVarKey, ThemeCssVarMap, ThemeStylePresetId } from '../../theme/themeStyle.types';
11
+ export type { ThemeStyleConfig, ThemeCssVarKey, ThemeCssVarMap, ThemeStylePresetId } from '@djangocfg/ui-core/styles/presets';
12
12
 
13
13
  // ============================================================================
14
14
  // Theme Configuration
@@ -2,9 +2,7 @@
2
2
 
3
3
  import { useEffect, useMemo } from 'react';
4
4
 
5
- import { buildThemeStyleSheet } from './buildThemeStyleSheet';
6
-
7
- import type { ThemeStyleConfig } from './themeStyle.types';
5
+ import { buildThemeStyleSheet, type ThemeStyleConfig } from '@djangocfg/ui-core/styles/presets';
8
6
 
9
7
  const STYLE_ELEMENT_ID = 'djangocfg-baseapp-theme-style';
10
8
 
@@ -7,10 +7,8 @@ export type {
7
7
  ThemeCssVarSidebarKey,
8
8
  ThemeStyleConfig,
9
9
  ThemeStylePresetId,
10
- } from './themeStyle.types';
10
+ } from '@djangocfg/ui-core/styles/presets';
11
11
 
12
- export { THEME_STYLE_PRESETS, THEME_STYLE_PRESET_ORDER } from './themeStylePresets';
13
-
14
- export { buildThemeStyleSheet } from './buildThemeStyleSheet';
12
+ export { THEME_STYLE_PRESETS, THEME_STYLE_PRESET_ORDER, buildThemeStyleSheet } from '@djangocfg/ui-core/styles/presets';
15
13
 
16
14
  export { ThemeStyleBridge, type ThemeStyleBridgeProps } from './ThemeStyleBridge';
@@ -1,71 +0,0 @@
1
- import type { ThemeCssVarMap, ThemeStyleConfig, ThemeStylePresetId } from './themeStyle.types';
2
- import { THEME_STYLE_PRESETS } from './themeStylePresets';
3
-
4
- function mergeLayer(
5
- base: ThemeCssVarMap | undefined,
6
- over: ThemeCssVarMap | undefined
7
- ): ThemeCssVarMap {
8
- return { ...base, ...over };
9
- }
10
-
11
- /**
12
- * Tailwind v4 `rounded-*` utilities are backed by `--radius-*` scale variables (xs/sm/md/…),
13
- * not by our semantic `--radius`.
14
- *
15
- * When semantic `radius` is present, emit the derived scale too so the injected stylesheet
16
- * controls both semantic and Tailwind scale rounding across the UI.
17
- */
18
- function withTailwindRadiusScale(vars: ThemeCssVarMap): Array<[string, string]> {
19
- const entries = Object.entries(vars);
20
- const radius = vars.radius;
21
- if (!radius) return entries;
22
-
23
- const r = String(radius).trim();
24
- if (!r) return entries;
25
-
26
- const scale: Array<[string, string]> = [
27
- ['radius-xs', `calc(${r} - 6px)`],
28
- ['radius-sm', `calc(${r} - 4px)`],
29
- ['radius-md', `calc(${r} - 2px)`],
30
- ['radius-lg', r],
31
- ['radius-xl', `calc(${r} + 4px)`],
32
- ['radius-2xl', `calc(${r} + 8px)`],
33
- ['radius-3xl', `calc(${r} + 12px)`],
34
- ['radius-4xl', `calc(${r} + 16px)`],
35
- ];
36
-
37
- // Put derived values after semantic `radius` so they always win in the injected block.
38
- return [...entries, ...scale];
39
- }
40
-
41
- /**
42
- * Build a small stylesheet fragment for injection after globals.
43
- * Order: preset (if not default) → `vars.light` / `vars.dark`.
44
- */
45
- export function buildThemeStyleSheet(style?: ThemeStyleConfig): string {
46
- if (!style) return '';
47
-
48
- const presetId: ThemeStylePresetId = style.preset ?? 'default';
49
- const preset = THEME_STYLE_PRESETS[presetId] ?? THEME_STYLE_PRESETS.default;
50
-
51
- const light = mergeLayer(preset.light, style.vars?.light);
52
- const dark = mergeLayer(preset.dark, style.vars?.dark);
53
-
54
- const blocks: string[] = [];
55
-
56
- if (Object.keys(light).length > 0) {
57
- const body = withTailwindRadiusScale(light)
58
- .map(([k, v]) => ` --${k}: ${v};`)
59
- .join('\n');
60
- blocks.push(`:root {\n${body}\n}`);
61
- }
62
-
63
- if (Object.keys(dark).length > 0) {
64
- const body = withTailwindRadiusScale(dark)
65
- .map(([k, v]) => ` --${k}: ${v};`)
66
- .join('\n');
67
- blocks.push(`.dark {\n${body}\n}`);
68
- }
69
-
70
- return blocks.join('\n\n');
71
- }