@djangocfg/layouts 2.1.422 → 2.1.424

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 (29) hide show
  1. package/package.json +17 -17
  2. package/src/components/errors/ErrorsTracker/components/ErrorToast.tsx +6 -4
  3. package/src/components/errors/ErrorsTracker/providers/ErrorTrackingProvider.tsx +12 -1
  4. package/src/components/errors/ErrorsTracker/utils/formatters.ts +19 -5
  5. package/src/layouts/AuthLayout/AuthLayout.tsx +8 -11
  6. package/src/layouts/AuthLayout/README.md +50 -18
  7. package/src/layouts/AuthLayout/components/shared/AuthConsent.tsx +46 -0
  8. package/src/layouts/AuthLayout/components/shared/index.ts +2 -2
  9. package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +12 -25
  10. package/src/layouts/AuthLayout/context.tsx +0 -4
  11. package/src/layouts/AuthLayout/shells/AuthShell.tsx +10 -1
  12. package/src/layouts/AuthLayout/shells/FullSplitShell.tsx +73 -0
  13. package/src/layouts/AuthLayout/shells/types.ts +14 -4
  14. package/src/layouts/AuthLayout/styles/auth.css +62 -40
  15. package/src/layouts/AuthLayout/styles/fullsplit-shell.css +163 -0
  16. package/src/layouts/AuthLayout/styles/split-shell.css +74 -71
  17. package/src/layouts/AuthLayout/types.ts +6 -6
  18. package/src/layouts/ProfileLayout/ProfileDialog/ProfileDialog.tsx +26 -2
  19. package/src/layouts/ProfileLayout/ProfileDialog/index.ts +2 -0
  20. package/src/layouts/ProfileLayout/ProfileDialog/store.ts +39 -7
  21. package/src/layouts/ProfileLayout/ProfileForm/index.tsx +1 -2
  22. package/src/layouts/ProfileLayout/README.md +34 -2
  23. package/src/layouts/ProfileLayout/components/EditableField.tsx +4 -0
  24. package/src/layouts/ProfileLayout/hooks/index.ts +1 -1
  25. package/src/layouts/ProfileLayout/hooks/useProfileTabs.ts +14 -6
  26. package/src/layouts/ProfileLayout/index.ts +2 -1
  27. package/src/layouts/ProfileLayout/types.ts +3 -2
  28. package/src/testing/MockAuthFormProvider.tsx +0 -3
  29. package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +0 -72
@@ -1,19 +1,51 @@
1
1
  import { create } from 'zustand';
2
2
 
3
- import type { ProfileTabValue } from '../hooks/useProfileTabs';
3
+ import type { ProfileTabValueOrCustom } from '../hooks/useProfileTabs';
4
+ import type { ProfileSlots, ProfileTab } from '../types';
4
5
 
5
- interface ProfileDialogState {
6
+ /**
7
+ * Content the dialog renders into <ProfileForm>. All optional — anything
8
+ * omitted in `open()` falls back to ProfileForm's own default. Lets any caller
9
+ * open the profile dialog with custom tabs/slots/flags in one line.
10
+ */
11
+ export interface ProfileDialogContent {
12
+ initialTab?: ProfileTabValueOrCustom;
13
+ tabs?: ProfileTab[];
14
+ slots?: ProfileSlots;
15
+ enable2FA?: boolean;
16
+ enableAPIKeys?: boolean;
17
+ enableDeleteAccount?: boolean;
18
+ title?: string;
19
+ }
20
+
21
+ interface ProfileDialogState extends ProfileDialogContent {
6
22
  isOpen: boolean;
7
- initialTab: ProfileTabValue | undefined;
8
- open: (options?: { initialTab?: ProfileTabValue }) => void;
23
+ open: (options?: ProfileDialogContent) => void;
9
24
  close: () => void;
10
25
  toggle: () => void;
11
26
  }
12
27
 
28
+ /** Content fields reset to undefined on close (keeps `isOpen` separate). */
29
+ const EMPTY_CONTENT: Required<{ [K in keyof ProfileDialogContent]: undefined }> = {
30
+ initialTab: undefined,
31
+ tabs: undefined,
32
+ slots: undefined,
33
+ enable2FA: undefined,
34
+ enableAPIKeys: undefined,
35
+ enableDeleteAccount: undefined,
36
+ title: undefined,
37
+ };
38
+
13
39
  export const useProfileDialogStore = create<ProfileDialogState>((set) => ({
14
40
  isOpen: false,
15
- initialTab: undefined,
16
- open: (options) => set({ isOpen: true, initialTab: options?.initialTab }),
17
- close: () => set({ isOpen: false, initialTab: undefined }),
41
+ ...EMPTY_CONTENT,
42
+ open: (options) =>
43
+ set({
44
+ isOpen: true,
45
+ // Replace (not merge) content so a previous open()'s tabs/flags don't leak.
46
+ ...EMPTY_CONTENT,
47
+ ...options,
48
+ }),
49
+ close: () => set({ isOpen: false, ...EMPTY_CONTENT }),
18
50
  toggle: () => set((state) => ({ isOpen: !state.isOpen })),
19
51
  }));
@@ -16,7 +16,6 @@ import { ApiKeySection, ProfileHeader, ProfileTab, TwoFactorSection } from '../c
16
16
  import { ProfileProvider, useProfileContext } from './context';
17
17
  import { useProfileTabs } from '../hooks/useProfileTabs';
18
18
  import type { ProfileFormProps } from '../types';
19
- import type { ProfileTabValue } from '../hooks/useProfileTabs';
20
19
 
21
20
  // ─────────────────────────────────────────────────────────────────────────────
22
21
  // Built-in tab panels
@@ -64,7 +63,7 @@ function ProfileContent({
64
63
  });
65
64
 
66
65
  const handleTabChange = React.useCallback(
67
- (value: string) => setTab(value as ProfileTabValue),
66
+ (value: string) => setTab(value),
68
67
  [setTab],
69
68
  );
70
69
 
@@ -28,6 +28,7 @@ import { ProfileDialog, useProfileDialogStore } from '@djangocfg/layouts';
28
28
 
29
29
  // Open on a specific tab
30
30
  useProfileDialogStore.getState().open({ initialTab: 'security' });
31
+ // …or with custom tabs/slots — see "Opening with custom content" below.
31
32
 
32
33
  // In your layout (lazy-loaded)
33
34
  <ProfileDialog />
@@ -44,7 +45,7 @@ useProfileDialogStore.getState().open({ initialTab: 'security' });
44
45
  | `tabs` | `[]` | Extra `ProfileTab[]` appended after built-in tabs |
45
46
  | `slots` | — | Named slots: `headerMenuItems`, `headerBadge`, `headerAfter`, `footer` |
46
47
  | `title` | i18n | Page title |
47
- | `defaultTab` | — | Initial active tab. When provided (e.g. by `ProfileDialog`), tabs start at this value. |
48
+ | `defaultTab` | — | Initial active tab. Accepts a built-in value or a custom tab's `value`. When provided (e.g. by `ProfileDialog`), tabs start at this value. |
48
49
 
49
50
  ## Global Profile Dialog
50
51
 
@@ -62,6 +63,37 @@ open();
62
63
  open({ initialTab: 'security' });
63
64
  ```
64
65
 
66
+ ### Opening with custom content
67
+
68
+ `open()` accepts the same content props as `ProfileForm` (all optional), so any
69
+ caller can open the dialog with custom tabs / slots / flags — and start on a
70
+ custom tab — in one line:
71
+
72
+ ```tsx
73
+ useProfileDialogStore.getState().open({
74
+ initialTab: 'api-keys',
75
+ tabs: [{ value: 'api-keys', label: 'API keys', content: <MyApiKeysTab /> }],
76
+ enableAPIKeys: false, // hide the built-in API Keys tab
77
+ });
78
+ ```
79
+
80
+ The `open()` payload is typed as `ProfileDialogContent`:
81
+
82
+ | Field | Type | Notes |
83
+ |-------|------|-------|
84
+ | `initialTab` | `string` | Built-in (`'profile' \| 'security' \| 'api-keys'`) **or** a custom tab's `value`. |
85
+ | `tabs` | `ProfileTab[]` | Extra tabs appended after the built-in ones. |
86
+ | `slots` | `ProfileSlots` | `headerMenuItems`, `headerBadge`, `headerAfter`, `footer`. |
87
+ | `enable2FA` | `boolean` | Override the Security tab. |
88
+ | `enableAPIKeys` | `boolean` | Override the built-in API Keys tab. |
89
+ | `enableDeleteAccount` | `boolean` | Override Delete Account in the header dropdown. |
90
+ | `title` | `string` | Dialog title (the `<ProfileDialog title>` prop wins if both set). |
91
+
92
+ Each `open()` **replaces** the previous content (it does not merge), and `close()`
93
+ clears it — so a bare `open()` always shows the default profile. Omitted fields
94
+ fall through to `ProfileForm`'s own defaults, keeping `open()` / `open({ initialTab })`
95
+ backward compatible.
96
+
65
97
  Wire it into `PrivateLayout` for global access:
66
98
 
67
99
  ```tsx
@@ -94,7 +126,7 @@ ProfileLayout/
94
126
  │ └── context.tsx Root context (labels, onLogout, onFieldSave)
95
127
  ├── ProfileDialog/
96
128
  │ ├── ProfileDialog.tsx Dialog wrapper around ProfileForm
97
- │ └── store.ts Zustand store (isOpen, initialTab, open/close)
129
+ │ └── store.ts Zustand store (isOpen + ProfileDialogContent: tabs/slots/flags/initialTab/title)
98
130
  ├── hooks/
99
131
  │ └── useProfileTabs.ts Local tab state (useState)
100
132
  ├── types.ts ProfileFormProps, ProfileTab, ProfileSlots
@@ -4,6 +4,7 @@ import React, { useEffect, useState } from 'react';
4
4
  import { parsePhoneNumberFromString } from 'libphonenumber-js';
5
5
 
6
6
  import { Button, Input, PhoneInput } from '@djangocfg/ui-core/components';
7
+ import { toast } from '@djangocfg/ui-core/hooks';
7
8
  import { cn } from '@djangocfg/ui-core/lib';
8
9
 
9
10
  import { useProfileContext } from '../ProfileForm/context';
@@ -52,6 +53,9 @@ export const EditableField = ({
52
53
  try {
53
54
  await onSave(editValue);
54
55
  setIsEditing(false);
56
+ } catch {
57
+ // Keep editing mode open with the entered value so the user can retry.
58
+ toast.error(labels.failedToUpdate);
55
59
  } finally {
56
60
  setIsSaving(false);
57
61
  }
@@ -1,2 +1,2 @@
1
1
  export { useProfileTabs } from './useProfileTabs';
2
- export type { ProfileTabValue, UseProfileTabsOptions } from './useProfileTabs';
2
+ export type { ProfileTabValue, ProfileTabValueOrCustom, UseProfileTabsOptions } from './useProfileTabs';
@@ -2,14 +2,22 @@
2
2
 
3
3
  import { useMemo, useState, useCallback } from 'react';
4
4
 
5
+ /** Built-in tab values. */
5
6
  export type ProfileTabValue = 'profile' | 'security' | 'api-keys';
6
7
 
8
+ /**
9
+ * Any tab value: a built-in one, or a custom tab's `value` string.
10
+ * `(string & {})` keeps the built-in literals as autocomplete hints while
11
+ * still accepting arbitrary strings (custom tabs added via `tabs`).
12
+ */
13
+ export type ProfileTabValueOrCustom = ProfileTabValue | (string & {});
14
+
7
15
  export interface UseProfileTabsOptions {
8
16
  enable2FA?: boolean;
9
17
  enableAPIKeys?: boolean;
10
18
  extraTabValues?: string[];
11
- /** Initial active tab. Defaults to `'profile'`. */
12
- defaultTab?: ProfileTabValue;
19
+ /** Initial active tab. Defaults to `'profile'`. Accepts custom tab values. */
20
+ defaultTab?: ProfileTabValueOrCustom;
13
21
  }
14
22
 
15
23
  /**
@@ -25,18 +33,18 @@ export function useProfileTabs(options: UseProfileTabsOptions = {}) {
25
33
  const { enable2FA, enableAPIKeys, extraTabValues = [], defaultTab } = options;
26
34
 
27
35
  const allowed = useMemo(() => {
28
- const base: ProfileTabValue[] = ['profile'];
36
+ const base: ProfileTabValueOrCustom[] = ['profile'];
29
37
  if (enable2FA) base.push('security');
30
38
  if (enableAPIKeys) base.push('api-keys');
31
- return [...base, ...extraTabValues] as ProfileTabValue[];
39
+ return [...base, ...extraTabValues];
32
40
  }, [enable2FA, enableAPIKeys, extraTabValues]);
33
41
 
34
- const [tab, setTabState] = useState<ProfileTabValue>(
42
+ const [tab, setTabState] = useState<ProfileTabValueOrCustom>(
35
43
  defaultTab && allowed.includes(defaultTab) ? defaultTab : 'profile',
36
44
  );
37
45
 
38
46
  const setTab = useCallback(
39
- (value: ProfileTabValue) => {
47
+ (value: ProfileTabValueOrCustom) => {
40
48
  if (allowed.includes(value)) {
41
49
  setTabState(value);
42
50
  }
@@ -2,6 +2,7 @@ export { ProfileForm, useProfileContext } from './ProfileForm';
2
2
  export { ProfileProvider } from './ProfileForm/context';
3
3
  export { useProfileTabs } from './hooks';
4
4
  export { ProfileDialog, useProfileDialogStore } from './ProfileDialog';
5
+ export type { ProfileDialogProps, ProfileDialogContent } from './ProfileDialog';
5
6
  export type { ProfileLabels } from './ProfileForm/context';
6
7
  export type { ProfileFormProps, ProfileLayoutProps, ProfileTab, ProfileSlots } from './types';
7
- export type { ProfileTabValue, UseProfileTabsOptions } from './hooks';
8
+ export type { ProfileTabValue, ProfileTabValueOrCustom, UseProfileTabsOptions } from './hooks';
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
 
3
- import type { ProfileTabValue } from './hooks/useProfileTabs';
3
+ import type { ProfileTabValueOrCustom } from './hooks/useProfileTabs';
4
4
 
5
5
  // ─────────────────────────────────────────────────────────────────────────────
6
6
  // Slot + Tab types
@@ -39,8 +39,9 @@ export interface ProfileFormProps {
39
39
  /**
40
40
  * When provided, the active tab is controlled locally (no URL sync).
41
41
  * Useful for dialogs where query-string pollution is undesirable.
42
+ * Accepts a built-in value or a custom tab's `value`.
42
43
  */
43
- defaultTab?: ProfileTabValue;
44
+ defaultTab?: ProfileTabValueOrCustom;
44
45
  }
45
46
 
46
47
  /** @deprecated Use ProfileFormProps instead */
@@ -53,7 +53,6 @@ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
53
53
  termsUrl,
54
54
  privacyUrl,
55
55
  enableGithubAuth = false,
56
- enable2FASetup = true,
57
56
  logoUrl,
58
57
  redirectUrl = '/dashboard',
59
58
 
@@ -152,7 +151,6 @@ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
152
151
  termsUrl,
153
152
  privacyUrl,
154
153
  enableGithubAuth,
155
- enable2FASetup,
156
154
  logoUrl,
157
155
  redirectUrl,
158
156
  };
@@ -176,7 +174,6 @@ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
176
174
  termsUrl,
177
175
  privacyUrl,
178
176
  enableGithubAuth,
179
- enable2FASetup,
180
177
  logoUrl,
181
178
  redirectUrl,
182
179
  ]);
@@ -1,72 +0,0 @@
1
- 'use client';
2
-
3
- import React, { memo, useMemo } from 'react';
4
-
5
- import { useAppT } from '@djangocfg/i18n';
6
- import { Checkbox } from '@djangocfg/ui-core/components';
7
-
8
- export interface TermsCheckboxProps {
9
- checked: boolean;
10
- onChange: (checked: boolean) => void;
11
- termsUrl?: string;
12
- privacyUrl?: string;
13
- disabled?: boolean;
14
- className?: string;
15
- }
16
-
17
- /**
18
- * TermsCheckbox - Compact terms acceptance checkbox.
19
- *
20
- * Memoised: re-renders only when checked, termsUrl, privacyUrl, disabled
21
- * or className change. `onChange` is compared by reference — callers
22
- * should stabilise it with useCallback.
23
- */
24
- function TermsCheckboxRaw({
25
- checked,
26
- onChange,
27
- termsUrl,
28
- privacyUrl,
29
- disabled = false,
30
- className = '',
31
- }: TermsCheckboxProps) {
32
- const t = useAppT();
33
- const labels = useMemo(() => ({
34
- agree: t('layouts.auth.terms.agree'),
35
- and: t('layouts.auth.terms.and'),
36
- terms: t('layouts.auth.terms.terms'),
37
- privacy: t('layouts.auth.terms.privacy'),
38
- }), [t]);
39
-
40
- // Don't render if no links provided
41
- if (!termsUrl && !privacyUrl) {
42
- return null;
43
- }
44
-
45
- return (
46
- <div className={`auth-terms ${className}`}>
47
- <Checkbox
48
- id="auth-terms"
49
- checked={checked}
50
- onCheckedChange={onChange}
51
- disabled={disabled}
52
- className="auth-terms-checkbox"
53
- />
54
- <label htmlFor="auth-terms">
55
- {labels.agree}{' '}
56
- {termsUrl && (
57
- <a href={termsUrl} target="_blank" rel="noopener noreferrer">
58
- {labels.terms}
59
- </a>
60
- )}
61
- {termsUrl && privacyUrl && ` ${labels.and} `}
62
- {privacyUrl && (
63
- <a href={privacyUrl} target="_blank" rel="noopener noreferrer">
64
- {labels.privacy}
65
- </a>
66
- )}
67
- </label>
68
- </div>
69
- );
70
- }
71
-
72
- export const TermsCheckbox = memo(TermsCheckboxRaw);