@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.
- package/package.json +21 -19
- package/src/configurator/private/schema.ts +20 -0
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +17 -1
- package/src/layouts/PrivateLayout/README.md +47 -1
- package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +5 -72
- package/src/layouts/PrivateLayout/components/PrivateSidebarAccount.tsx +47 -96
- package/src/layouts/PrivateLayout/components/SidebarBrand.tsx +36 -17
- package/src/layouts/PrivateLayout/components/SidebarBrandSwitcher.tsx +223 -0
- package/src/layouts/PrivateLayout/components/index.ts +1 -0
- package/src/layouts/PrivateLayout/context.tsx +2 -9
- package/src/layouts/PrivateLayout/hooks/index.ts +1 -5
- package/src/layouts/PrivateLayout/hooks/useHoverExpand.ts +10 -3
- package/src/layouts/PrivateLayout/hooks/useShellVisualState.ts +11 -88
- package/src/layouts/PrivateLayout/hooks/useSidebarDefaultOpen.ts +32 -0
- package/src/layouts/PrivateLayout/index.ts +3 -0
- package/src/layouts/PrivateLayout/types.ts +41 -0
- package/src/layouts/ProfileLayout/ProfileDialog/ProfileDialog.tsx +32 -0
- package/src/layouts/ProfileLayout/ProfileDialog/index.ts +2 -0
- package/src/layouts/ProfileLayout/ProfileDialog/store.ts +19 -0
- package/src/layouts/ProfileLayout/{context.tsx → ProfileForm/context.tsx} +4 -2
- package/src/layouts/ProfileLayout/{ProfileLayout.tsx → ProfileForm/index.tsx} +10 -7
- package/src/layouts/ProfileLayout/README.md +65 -5
- package/src/layouts/ProfileLayout/components/EditableField.tsx +1 -1
- package/src/layouts/ProfileLayout/components/PreferencesSection.tsx +56 -0
- package/src/layouts/ProfileLayout/components/ProfileHeader.tsx +1 -1
- package/src/layouts/ProfileLayout/components/ProfileTab.tsx +17 -11
- package/src/layouts/ProfileLayout/components/index.ts +1 -0
- package/src/layouts/ProfileLayout/hooks/useProfileTabs.ts +17 -12
- package/src/layouts/ProfileLayout/index.ts +5 -4
- package/src/layouts/ProfileLayout/types.ts +11 -1
- package/src/layouts/_components/index.ts +1 -0
- package/src/layouts/types/providers.types.ts +2 -2
- package/src/theme/ThemeStyleBridge.tsx +1 -3
- package/src/theme/index.ts +2 -4
- package/src/theme/buildThemeStyleSheet.ts +0 -71
- package/src/theme/themeStyle.types.ts +0 -89
- 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,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 '
|
|
10
|
-
import { useLogout } from '
|
|
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 '
|
|
15
|
+
import { ApiKeySection, ProfileHeader, ProfileTab, TwoFactorSection } from '../components';
|
|
16
16
|
import { ProfileProvider, useProfileContext } from './context';
|
|
17
|
-
import { useProfileTabs } from '
|
|
18
|
-
import type {
|
|
19
|
-
import type { ProfileTabValue } from '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
10
|
+
import { ProfileForm } from '@djangocfg/layouts';
|
|
9
11
|
|
|
10
|
-
<
|
|
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
|
-
├──
|
|
39
|
-
├──
|
|
40
|
-
|
|
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="
|
|
17
|
-
<
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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.
|
|
24
|
-
<
|
|
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
|
-
*
|
|
16
|
+
* Manages the active profile tab with local React state.
|
|
17
17
|
*
|
|
18
|
-
* Falls back to `'profile'` when
|
|
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
|
|
36
|
-
|
|
37
|
-
[allowed],
|
|
34
|
+
const [tab, setTabState] = useState<ProfileTabValue>(
|
|
35
|
+
defaultTab && allowed.includes(defaultTab) ? defaultTab : 'profile',
|
|
38
36
|
);
|
|
39
37
|
|
|
40
|
-
const
|
|
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 {
|
|
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
|
|
5
|
-
export type {
|
|
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
|
|
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;
|
|
@@ -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 '
|
|
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 '
|
|
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 '
|
|
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
|
|
package/src/theme/index.ts
CHANGED
|
@@ -7,10 +7,8 @@ export type {
|
|
|
7
7
|
ThemeCssVarSidebarKey,
|
|
8
8
|
ThemeStyleConfig,
|
|
9
9
|
ThemeStylePresetId,
|
|
10
|
-
} from '
|
|
10
|
+
} from '@djangocfg/ui-core/styles/presets';
|
|
11
11
|
|
|
12
|
-
export { THEME_STYLE_PRESETS, THEME_STYLE_PRESET_ORDER } from '
|
|
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
|
-
}
|