@djangocfg/layouts 2.1.426 → 2.1.428

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 (77) hide show
  1. package/README.md +29 -21
  2. package/package.json +15 -17
  3. package/src/components/errors/ErrorsTracker/components/ErrorToast.tsx +19 -0
  4. package/src/components/errors/ErrorsTracker/utils/curl-generator.ts +24 -10
  5. package/src/components/errors/README.md +63 -0
  6. package/src/layouts/AppLayout/BaseApp.tsx +36 -52
  7. package/src/layouts/AppLayout/README.md +79 -64
  8. package/src/layouts/AppLayout/index.ts +12 -19
  9. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +6 -4
  10. package/src/layouts/PrivateLayout/PrivateLayout.tsx +7 -4
  11. package/src/layouts/PrivateLayout/README.md +30 -0
  12. package/src/layouts/PrivateLayout/components/PrivateContent.tsx +6 -2
  13. package/src/layouts/PrivateLayout/components/PrivateSidebarAccount.tsx +105 -70
  14. package/src/layouts/PrivateLayout/hooks/useAuthGuard.ts +12 -3
  15. package/src/layouts/PrivateLayout/types.ts +8 -3
  16. package/src/layouts/PublicLayout/components/UserMenu.tsx +68 -113
  17. package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +0 -6
  18. package/src/layouts/PublicLayout/navbars/MinimalNavbar/index.ts +1 -1
  19. package/src/layouts/SettingsLayout/README.md +258 -0
  20. package/src/layouts/SettingsLayout/SettingsDialog.tsx +101 -0
  21. package/src/layouts/SettingsLayout/SettingsForm.tsx +100 -0
  22. package/src/layouts/SettingsLayout/components/ApiKeySection/ApiKeySection.tsx +192 -0
  23. package/src/layouts/SettingsLayout/components/SettingsNav.tsx +71 -0
  24. package/src/layouts/SettingsLayout/components/SettingsNavItem.tsx +57 -0
  25. package/src/layouts/SettingsLayout/components/SettingsPanel.tsx +48 -0
  26. package/src/layouts/SettingsLayout/components/SettingsSearch.tsx +50 -0
  27. package/src/layouts/SettingsLayout/components/SettingsShell.tsx +77 -0
  28. package/src/layouts/SettingsLayout/components/SettingsTabs.tsx +56 -0
  29. package/src/layouts/{ProfileLayout → SettingsLayout}/components/TwoFactorSection/TwoFactorSection.tsx +84 -130
  30. package/src/layouts/SettingsLayout/components/index.ts +6 -0
  31. package/src/layouts/SettingsLayout/context/SettingsContext.tsx +122 -0
  32. package/src/layouts/SettingsLayout/context/index.ts +2 -0
  33. package/src/layouts/SettingsLayout/hooks/index.ts +12 -0
  34. package/src/layouts/SettingsLayout/hooks/useProfileSave.ts +95 -0
  35. package/src/layouts/SettingsLayout/hooks/useSettingsDialog.ts +52 -0
  36. package/src/layouts/SettingsLayout/hooks/useSettingsSections.ts +123 -0
  37. package/src/layouts/SettingsLayout/hooks/useSettingsUrl.ts +140 -0
  38. package/src/layouts/SettingsLayout/index.ts +67 -0
  39. package/src/layouts/SettingsLayout/sections/AccountSection.tsx +100 -0
  40. package/src/layouts/SettingsLayout/sections/ApiKeysSection.tsx +15 -0
  41. package/src/layouts/SettingsLayout/sections/DeleteAccountRow.tsx +57 -0
  42. package/src/layouts/SettingsLayout/sections/PreferencesRows.tsx +43 -0
  43. package/src/layouts/SettingsLayout/sections/SecuritySection.tsx +15 -0
  44. package/src/layouts/SettingsLayout/sections/builtins.tsx +77 -0
  45. package/src/layouts/SettingsLayout/sections/index.ts +8 -0
  46. package/src/layouts/SettingsLayout/store.ts +47 -0
  47. package/src/layouts/SettingsLayout/types.ts +107 -0
  48. package/src/layouts/index.ts +1 -2
  49. package/src/layouts/types/index.ts +0 -1
  50. package/src/layouts/types/layout.types.ts +0 -4
  51. package/src/utils/logger.ts +9 -4
  52. package/src/layouts/AdminLayout/AdminLayout.tsx +0 -57
  53. package/src/layouts/AdminLayout/index.ts +0 -7
  54. package/src/layouts/AppLayout/AppLayout.tsx +0 -520
  55. package/src/layouts/ProfileLayout/ProfileDialog/ProfileDialog.tsx +0 -56
  56. package/src/layouts/ProfileLayout/ProfileDialog/index.ts +0 -4
  57. package/src/layouts/ProfileLayout/ProfileDialog/store.ts +0 -51
  58. package/src/layouts/ProfileLayout/ProfileForm/context.tsx +0 -123
  59. package/src/layouts/ProfileLayout/ProfileForm/index.tsx +0 -147
  60. package/src/layouts/ProfileLayout/README.md +0 -150
  61. package/src/layouts/ProfileLayout/components/ActionButton.tsx +0 -38
  62. package/src/layouts/ProfileLayout/components/ApiKeySection/ApiKeySection.tsx +0 -197
  63. package/src/layouts/ProfileLayout/components/DeleteAccountSection.tsx +0 -44
  64. package/src/layouts/ProfileLayout/components/EditableField.tsx +0 -128
  65. package/src/layouts/ProfileLayout/components/PreferencesSection.tsx +0 -56
  66. package/src/layouts/ProfileLayout/components/ProfileHeader.tsx +0 -110
  67. package/src/layouts/ProfileLayout/components/ProfileTab.tsx +0 -35
  68. package/src/layouts/ProfileLayout/components/Section.tsx +0 -22
  69. package/src/layouts/ProfileLayout/components/index.ts +0 -11
  70. package/src/layouts/ProfileLayout/hooks/index.ts +0 -2
  71. package/src/layouts/ProfileLayout/hooks/useProfileTabs.ts +0 -56
  72. package/src/layouts/ProfileLayout/index.ts +0 -8
  73. package/src/layouts/ProfileLayout/types.ts +0 -48
  74. /package/src/layouts/{ProfileLayout → SettingsLayout}/components/ApiKeySection/context.tsx +0 -0
  75. /package/src/layouts/{ProfileLayout → SettingsLayout}/components/ApiKeySection/index.ts +0 -0
  76. /package/src/layouts/{ProfileLayout → SettingsLayout}/components/AvatarSection.tsx +0 -0
  77. /package/src/layouts/{ProfileLayout → SettingsLayout}/components/TwoFactorSection/index.ts +0 -0
@@ -1,520 +0,0 @@
1
- /**
2
- * AppLayout - Smart Layout Router with All Providers
3
- *
4
- * Automatically detects route type and applies the correct layout
5
- * Includes all necessary providers: Theme, Auth, Analytics, Centrifugo, Error Tracking
6
- * Simple props-based configuration - no complex configs needed!
7
- *
8
- * @example
9
- * ```tsx
10
- * import { AppLayout } from '@djangocfg/layouts';
11
- *
12
- * <AppLayout
13
- * publicLayout={{
14
- * component: PublicLayout,
15
- * enabledPath: ['/', '/about', '/contact']
16
- * }}
17
- * privateLayout={{
18
- * component: DashboardLayout,
19
- * enabledPath: '/dashboard'
20
- * }}
21
- * adminLayout={{
22
- * component: AdminLayout,
23
- * enabledPath: '/admin'
24
- * }}
25
- * // Paths that render without any layout wrapper (fullscreen pages)
26
- * noLayoutPaths={['/private/terminal', '/embed']}
27
- * >
28
- * {children}
29
- * </AppLayout>
30
- * ```
31
- */
32
-
33
- 'use client';
34
-
35
- import React, { ReactNode, memo, useMemo } from 'react';
36
-
37
- import { ClientOnly, Suspense } from '../../components/core';
38
- import { usePathnameWithoutLocale } from '../../hooks';
39
- import { matchesPath } from '../../utils/pathMatcher';
40
- import { BaseApp } from './BaseApp';
41
- import { useLayoutI18nOptional } from './LayoutI18nProvider';
42
- import { ForcedThemeProvider, ThemeOverride, resolveForcedTheme } from '@djangocfg/ui-core/theme';
43
-
44
- import type {
45
- ThemeConfig,
46
- AnalyticsConfig,
47
- CentrifugoConfig,
48
- ErrorTrackingConfig,
49
- ErrorBoundaryConfig,
50
- SWRConfigOptions,
51
- PwaInstallConfig,
52
- DebugConfig,
53
- I18nLayoutConfig,
54
- } from '../types';
55
- export type { I18nLayoutConfig } from '../types';
56
- import type { AuthConfig } from '@djangocfg/api/auth';
57
- import type { MonitorConfig } from '@djangocfg/monitor';
58
- import type { ThemeOverrideRule } from '@djangocfg/ui-core/theme';
59
-
60
- import type { FloatingNavbarConfig } from '../PublicLayout/navbars/FloatingNavbar';
61
- import type { DefaultFooterConfig } from '../PublicLayout/footers/DefaultFooter/types';
62
-
63
- export type LayoutMode = 'public' | 'private' | 'admin';
64
-
65
- /** `<main>` spacing in `PublicLayout` — offset from navbar / padding before footer. */
66
- export type PublicMainTopSpacing = 'auto' | 'none';
67
-
68
- /** Bottom padding of `<main>` above the footer. `compact` = less than `auto`; `none` = no extra gap. */
69
- export type PublicMainBottomSpacing = 'auto' | 'none' | 'compact';
70
-
71
- /** Shared marketing chrome defaults for `FloatingNavbar` / `DefaultFooter`, merged into your public layout. */
72
- export interface AppLayoutPublicChrome {
73
- navbar?: Partial<FloatingNavbarConfig>;
74
- footer?: Partial<DefaultFooterConfig>;
75
- /**
76
- * Passed through to `PublicLayout` as `contentTopSpacing` / `contentBottomSpacing`.
77
- * Use `bottomSpacing: 'none'` when the page (or footer) should sit flush with no default gap.
78
- */
79
- main?: {
80
- topSpacing?: PublicMainTopSpacing;
81
- bottomSpacing?: PublicMainBottomSpacing;
82
- };
83
- /**
84
- * Full-viewport background layer rendered behind the navbar and all page content.
85
- * Pass a `fixed inset-0 -z-10 pointer-events-none` element — it covers the whole
86
- * viewport (including the sticky navbar area) without affecting layout flow.
87
- *
88
- * Set per-page via `AppLayout publicChrome` or directly on `PublicSiteLayout`.
89
- */
90
- backgroundSlot?: ReactNode;
91
- }
92
-
93
- function mergePartialNavbar(
94
- base?: Partial<FloatingNavbarConfig>,
95
- overlay?: Partial<FloatingNavbarConfig>
96
- ): Partial<FloatingNavbarConfig> | undefined {
97
- if (!base && !overlay) return undefined;
98
- const merged: Partial<FloatingNavbarConfig> = { ...base, ...overlay };
99
- if (base?.shell || overlay?.shell) {
100
- merged.shell = { ...base?.shell, ...overlay?.shell };
101
- }
102
- return merged;
103
- }
104
-
105
- function mergePartialFooter(
106
- base?: Partial<DefaultFooterConfig>,
107
- overlay?: Partial<DefaultFooterConfig>
108
- ): Partial<DefaultFooterConfig> | undefined {
109
- if (!base && !overlay) return undefined;
110
- const merged: Partial<DefaultFooterConfig> = { ...base, ...overlay };
111
- if (base?.shell || overlay?.shell) merged.shell = { ...base?.shell, ...overlay?.shell };
112
- if (base?.brand || overlay?.brand) merged.brand = { ...base?.brand, ...overlay?.brand };
113
- if (base?.menus || overlay?.menus) merged.menus = { ...base?.menus, ...overlay?.menus };
114
- if (base?.meta || overlay?.meta) merged.meta = { ...base?.meta, ...overlay?.meta };
115
- if (base?.social || overlay?.social) merged.social = { ...base?.social, ...overlay?.social };
116
- if (base?.controls || overlay?.controls) {
117
- merged.controls = { ...base?.controls, ...overlay?.controls };
118
- }
119
- return merged;
120
- }
121
-
122
- /**
123
- * Deep-merge public chrome: root `publicChrome` first, then `layouts.publicChrome` overlays.
124
- */
125
- export function mergeAppLayoutPublicChrome(
126
- root?: AppLayoutPublicChrome,
127
- fromLayouts?: AppLayoutPublicChrome
128
- ): AppLayoutPublicChrome | undefined {
129
- if (!root && !fromLayouts) return undefined;
130
- const navbar = mergePartialNavbar(root?.navbar, fromLayouts?.navbar);
131
- const footer = mergePartialFooter(root?.footer, fromLayouts?.footer);
132
- const main =
133
- root?.main || fromLayouts?.main
134
- ? { ...root?.main, ...fromLayouts?.main }
135
- : undefined;
136
- // backgroundSlot: layouts-level overrides root (more specific wins)
137
- const backgroundSlot = fromLayouts?.backgroundSlot ?? root?.backgroundSlot;
138
- if (!navbar && !footer && !main && !backgroundSlot) return undefined;
139
- return { navbar, footer, ...(main ? { main } : {}), ...(backgroundSlot ? { backgroundSlot } : {}) };
140
- }
141
-
142
- /**
143
- * Determine layout mode from pathname and enabledPath props
144
- */
145
- function determineLayoutMode(
146
- pathname: string,
147
- adminLayout?: { component: any; enabledPath?: string | string[] },
148
- privateLayout?: { component: any; enabledPath?: string | string[] },
149
- publicLayout?: { component: any; enabledPath?: string | string[] }
150
- ): LayoutMode {
151
- // Check in order: admin -> private -> public
152
- if (adminLayout && matchesPath(pathname, adminLayout.enabledPath)) return 'admin';
153
- if (privateLayout && matchesPath(pathname, privateLayout.enabledPath)) return 'private';
154
- if (publicLayout && matchesPath(pathname, publicLayout.enabledPath)) return 'public';
155
-
156
- // Default: if no enabledPath specified, use public as fallback
157
- return 'public';
158
- }
159
-
160
- /**
161
- * Props passed to every layout component (`public` / `private` / `admin`).
162
- * Use `publicChrome` to pass defaults for `FloatingNavbar` / `DefaultFooter` from `AppLayout`.
163
- *
164
- * Locale plumbing is no longer threaded as a prop — layouts read it from
165
- * `useLayoutI18nOptional()` (mounted in `BaseApp`).
166
- */
167
- export interface AppLayoutLayoutComponentProps {
168
- children: ReactNode;
169
- publicChrome?: AppLayoutPublicChrome;
170
- }
171
-
172
- /** Layout configuration with component and enabled paths */
173
- interface LayoutConfig {
174
- component: React.ComponentType<AppLayoutLayoutComponentProps>;
175
- enabledPath?: string | string[];
176
- }
177
-
178
- export interface AppLayoutLayoutsConfig {
179
- public?: LayoutConfig;
180
- private?: LayoutConfig;
181
- admin?: LayoutConfig;
182
- noLayoutPaths?: string | string[];
183
- authPath?: string;
184
- /** Merged over root `publicChrome` on `AppLayout` (see `mergeAppLayoutPublicChrome`). */
185
- publicChrome?: AppLayoutPublicChrome;
186
- /**
187
- * Per-route theme overrides. Each rule uses the same matcher as `enabledPath`
188
- * (string, string[], or glob with `*` / `**`). When the pathname matches a
189
- * rule, the forced theme is applied via `next-themes`; when navigation leaves
190
- * the matched path, the user's previous theme choice is restored.
191
- *
192
- * Rules are evaluated top-to-bottom — first match wins.
193
- *
194
- * @example
195
- * ```ts
196
- * themeOverrides: [
197
- * { path: '/', theme: 'dark' }, // marketing landing always dark
198
- * { path: ['/legal', '/legal/**'], theme: 'light' }, // legal always light
199
- * ]
200
- * ```
201
- */
202
- themeOverrides?: ThemeOverrideRule[];
203
- }
204
-
205
- export interface AppLayoutBaseAppConfig {
206
- project?: string;
207
- theme?: ThemeConfig;
208
- auth?: AuthConfig;
209
- analytics?: AnalyticsConfig;
210
- centrifugo?: CentrifugoConfig;
211
- errorTracking?: ErrorTrackingConfig;
212
- swr?: SWRConfigOptions;
213
- errorBoundary?: ErrorBoundaryConfig;
214
- pwaInstall?: PwaInstallConfig;
215
- monitor?: MonitorConfig;
216
- debug?: DebugConfig;
217
- }
218
-
219
- export interface AppLayoutProps {
220
- children: ReactNode;
221
-
222
- /** Compact routing/layout config */
223
- layouts?: AppLayoutLayoutsConfig;
224
- /** Compact providers/base-app config */
225
- baseApp?: AppLayoutBaseAppConfig;
226
-
227
- /** Project name — used as default for monitor.project and debug panel title */
228
- project?: string;
229
-
230
- /** Public layout component with enabled paths */
231
- publicLayout?: LayoutConfig;
232
-
233
- /** Private layout component with enabled paths */
234
- privateLayout?: LayoutConfig;
235
-
236
- /** Admin layout component with enabled paths */
237
- adminLayout?: LayoutConfig;
238
-
239
- /**
240
- * Paths that render without any layout wrapper (fullscreen pages)
241
- * Providers (auth, theme, etc.) are still applied, only layout is skipped
242
- * Useful for: fullscreen terminal, embed pages, print views
243
- */
244
- noLayoutPaths?: string | string[];
245
-
246
- /**
247
- * Auth path prefix. When set, all routes starting with this path
248
- * (and their localized variants like /ru/auth, /en/auth) render without
249
- * any layout wrapper — fullscreen, no navbar/sidebar.
250
- * @default '/auth'
251
- * @example authPath="/auth" → skips layout for /auth, /auth/*, /ru/auth, /ru/auth/*, etc.
252
- */
253
- authPath?: string;
254
-
255
- /** Theme configuration */
256
- theme?: ThemeConfig;
257
-
258
- /** Auth configuration */
259
- auth?: AuthConfig;
260
-
261
- /** Analytics configuration */
262
- analytics?: AnalyticsConfig;
263
-
264
- /** Centrifugo configuration */
265
- centrifugo?: CentrifugoConfig;
266
-
267
- /** Error tracking configuration */
268
- errorTracking?: ErrorTrackingConfig;
269
-
270
- /** SWR configuration */
271
- swr?: SWRConfigOptions;
272
-
273
- /** Error boundary configuration */
274
- errorBoundary?: ErrorBoundaryConfig;
275
-
276
- /** PWA Install configuration */
277
- pwaInstall?: PwaInstallConfig;
278
-
279
- /** i18n configuration for locale switching (applies to all layouts) */
280
- i18n?: I18nLayoutConfig;
281
-
282
- /** Base layer for `publicChrome`; `layouts.publicChrome` overlays this. */
283
- publicChrome?: AppLayoutPublicChrome;
284
-
285
- /**
286
- * Per-route theme overrides. Shortcut for `layouts.themeOverrides`.
287
- * See `AppLayoutLayoutsConfig.themeOverrides` for the full docs.
288
- */
289
- themeOverrides?: ThemeOverrideRule[];
290
-
291
- /** Monitor configuration — initialises window.monitor + auto-captures JS errors & console */
292
- monitor?: MonitorConfig;
293
-
294
- debug?: DebugConfig;
295
- }
296
-
297
- interface AppLayoutContentProps {
298
- children: ReactNode;
299
- publicLayout?: LayoutConfig;
300
- privateLayout?: LayoutConfig;
301
- adminLayout?: LayoutConfig;
302
- noLayoutPaths?: string | string[];
303
- authPath?: string;
304
- publicChrome?: AppLayoutPublicChrome;
305
- themeOverrides?: ThemeOverrideRule[];
306
- }
307
-
308
- /**
309
- * AppLayout Content - Renders layout with all providers
310
- *
311
- * SSR is only enabled for publicLayout.
312
- * Private and admin layouts are wrapped in ClientOnly to avoid hydration mismatch.
313
- */
314
- function AppLayoutContentRaw({
315
- children,
316
- publicLayout,
317
- privateLayout,
318
- adminLayout,
319
- noLayoutPaths,
320
- authPath = '/auth',
321
- publicChrome,
322
- themeOverrides,
323
- }: AppLayoutContentProps) {
324
- // Use pathname without locale prefix for route matching. The current
325
- // locale comes from the LayoutI18nProvider mounted by BaseApp — passing
326
- // it explicitly avoids the regex misfiring on `/ui`, `/ko`, etc.
327
- const i18n = useLayoutI18nOptional();
328
- const pathname = usePathnameWithoutLocale(i18n?.locale);
329
-
330
- // Merge authPath into noLayoutPaths — auth pages are always fullscreen
331
- const effectiveNoLayoutPaths = useMemo(() => {
332
- const base = noLayoutPaths
333
- ? (Array.isArray(noLayoutPaths) ? noLayoutPaths : [noLayoutPaths])
334
- : []
335
- return [...base, authPath]
336
- }, [noLayoutPaths, authPath])
337
-
338
- // Check if current path should skip layout
339
- const shouldSkipLayout = useMemo(
340
- () => matchesPath(pathname, effectiveNoLayoutPaths),
341
- [pathname, effectiveNoLayoutPaths]
342
- );
343
-
344
- const layoutMode = useMemo(
345
- () => determineLayoutMode(
346
- pathname,
347
- adminLayout,
348
- privateLayout,
349
- publicLayout
350
- ),
351
- [pathname, adminLayout, privateLayout, publicLayout]
352
- );
353
-
354
- // Prepare everything above the JSX — no inline conditionals in return().
355
- const hasThemeOverrides = Boolean(themeOverrides && themeOverrides.length > 0);
356
- const forcedTheme = hasThemeOverrides
357
- ? resolveForcedTheme(pathname, themeOverrides)
358
- : null;
359
- const themeOverrideElement = hasThemeOverrides
360
- ? <ThemeOverride pathname={pathname} rules={themeOverrides!} />
361
- : null;
362
-
363
- // Memoize layout element so it doesn't re-render on every pathname change
364
- // that doesn't affect layout mode (e.g. /dashboard/a → /dashboard/b).
365
- const layoutElement = useMemo(() => {
366
- // Skip layout for noLayoutPaths (fullscreen pages)
367
- if (shouldSkipLayout) {
368
- return children;
369
- }
370
-
371
- switch (layoutMode) {
372
- case 'admin':
373
- if (!adminLayout && privateLayout) {
374
- return (
375
- <ClientOnly>
376
- <Suspense>
377
- <privateLayout.component publicChrome={publicChrome}>
378
- {children}
379
- </privateLayout.component>
380
- </Suspense>
381
- </ClientOnly>
382
- );
383
- }
384
- if (!adminLayout) {
385
- return children;
386
- }
387
- return (
388
- <ClientOnly>
389
- <Suspense>
390
- <adminLayout.component publicChrome={publicChrome}>
391
- {children}
392
- </adminLayout.component>
393
- </Suspense>
394
- </ClientOnly>
395
- );
396
-
397
- case 'private':
398
- if (!privateLayout) {
399
- if (publicLayout) {
400
- return (
401
- <publicLayout.component publicChrome={publicChrome}>
402
- {children}
403
- </publicLayout.component>
404
- );
405
- }
406
- return children;
407
- }
408
- return (
409
- <ClientOnly>
410
- <Suspense>
411
- <privateLayout.component publicChrome={publicChrome}>
412
- {children}
413
- </privateLayout.component>
414
- </Suspense>
415
- </ClientOnly>
416
- );
417
-
418
- case 'public':
419
- default:
420
- // Public layout renders with SSR (no ClientOnly wrapper)
421
- if (!publicLayout) {
422
- return children;
423
- }
424
- return (
425
- <publicLayout.component publicChrome={publicChrome}>
426
- {children}
427
- </publicLayout.component>
428
- );
429
- }
430
- }, [shouldSkipLayout, layoutMode, publicLayout, privateLayout, adminLayout, publicChrome, children]);
431
-
432
- // No providers here - all providers now in BaseApp
433
- return (
434
- <ForcedThemeProvider value={forcedTheme}>
435
- {themeOverrideElement}
436
- {layoutElement}
437
- </ForcedThemeProvider>
438
- );
439
- }
440
-
441
- /**
442
- * Memoised layout content wrapper. Re-renders only when layout config props
443
- * change (publicLayout, privateLayout, adminLayout, noLayoutPaths, etc.).
444
- * The `children` prop is compared by reference — the consumer should wrap
445
- * page content in React.memo or use stable element references to avoid
446
- * unnecessary layout re-mounts on every parent render.
447
- */
448
- const AppLayoutContent = memo(AppLayoutContentRaw, (prev, next) => {
449
- return (
450
- prev.children === next.children &&
451
- prev.publicLayout === next.publicLayout &&
452
- prev.privateLayout === next.privateLayout &&
453
- prev.adminLayout === next.adminLayout &&
454
- prev.noLayoutPaths === next.noLayoutPaths &&
455
- prev.authPath === next.authPath &&
456
- prev.publicChrome === next.publicChrome &&
457
- prev.themeOverrides === next.themeOverrides
458
- );
459
- });
460
-
461
- /**
462
- * AppLayout - Main Component with All Providers
463
- */
464
- export function AppLayout(props: AppLayoutProps) {
465
- const layoutsConfig = props.layouts;
466
- const baseAppConfig = props.baseApp;
467
-
468
- const publicLayout = layoutsConfig?.public ?? props.publicLayout;
469
- const privateLayout = layoutsConfig?.private ?? props.privateLayout;
470
- const adminLayout = layoutsConfig?.admin ?? props.adminLayout;
471
- const noLayoutPaths = layoutsConfig?.noLayoutPaths ?? props.noLayoutPaths;
472
- const authPath = layoutsConfig?.authPath ?? props.authPath;
473
- const publicChrome = mergeAppLayoutPublicChrome(props.publicChrome, layoutsConfig?.publicChrome);
474
- const themeOverrides = layoutsConfig?.themeOverrides ?? props.themeOverrides;
475
-
476
- const {
477
- i18n,
478
- children,
479
- } = props;
480
-
481
- const project = baseAppConfig?.project ?? props.project;
482
- const theme = baseAppConfig?.theme ?? props.theme;
483
- const auth = baseAppConfig?.auth ?? props.auth;
484
- const analytics = baseAppConfig?.analytics ?? props.analytics;
485
- const centrifugo = baseAppConfig?.centrifugo ?? props.centrifugo;
486
- const errorTracking = baseAppConfig?.errorTracking ?? props.errorTracking;
487
- const errorBoundary = baseAppConfig?.errorBoundary ?? props.errorBoundary;
488
- const swr = baseAppConfig?.swr ?? props.swr;
489
- const pwaInstall = baseAppConfig?.pwaInstall ?? props.pwaInstall;
490
- const monitor = baseAppConfig?.monitor ?? props.monitor;
491
- const debug = baseAppConfig?.debug ?? props.debug;
492
-
493
- return (
494
- <BaseApp
495
- project={project}
496
- theme={theme}
497
- auth={auth}
498
- analytics={analytics}
499
- centrifugo={centrifugo}
500
- errorTracking={errorTracking}
501
- errorBoundary={errorBoundary}
502
- swr={swr}
503
- pwaInstall={pwaInstall}
504
- monitor={monitor}
505
- debug={debug}
506
- i18n={i18n}
507
- >
508
- <AppLayoutContent
509
- children={children}
510
- publicLayout={publicLayout}
511
- privateLayout={privateLayout}
512
- adminLayout={adminLayout}
513
- noLayoutPaths={noLayoutPaths}
514
- authPath={authPath}
515
- publicChrome={publicChrome}
516
- themeOverrides={themeOverrides}
517
- />
518
- </BaseApp>
519
- );
520
- }
@@ -1,56 +0,0 @@
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 {
21
- isOpen,
22
- close,
23
- initialTab,
24
- tabs,
25
- slots,
26
- enable2FA,
27
- enableAPIKeys,
28
- enableDeleteAccount,
29
- title: storeTitle,
30
- } = useProfileDialogStore();
31
-
32
- // Title precedence: explicit dialog prop > value passed to open() > ProfileForm default.
33
- const resolvedTitle = title ?? storeTitle;
34
-
35
- return (
36
- <Dialog open={isOpen} onOpenChange={(open) => !open && close()}>
37
- <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto p-0">
38
- <DialogHeader className="sr-only">
39
- <DialogTitle>Profile</DialogTitle>
40
- </DialogHeader>
41
- {/* Undefined fields fall through to ProfileForm's own defaults
42
- * (e.g. enableAPIKeys defaults to true), so this stays backward
43
- * compatible with bare open() / open({ initialTab }). */}
44
- <ProfileForm
45
- title={resolvedTitle}
46
- defaultTab={initialTab}
47
- tabs={tabs}
48
- slots={slots}
49
- enable2FA={enable2FA}
50
- enableAPIKeys={enableAPIKeys}
51
- enableDeleteAccount={enableDeleteAccount}
52
- />
53
- </DialogContent>
54
- </Dialog>
55
- );
56
- };
@@ -1,4 +0,0 @@
1
- export { ProfileDialog } from './ProfileDialog';
2
- export type { ProfileDialogProps } from './ProfileDialog';
3
- export { useProfileDialogStore } from './store';
4
- export type { ProfileDialogContent } from './store';
@@ -1,51 +0,0 @@
1
- import { create } from 'zustand';
2
-
3
- import type { ProfileTabValueOrCustom } from '../hooks/useProfileTabs';
4
- import type { ProfileSlots, ProfileTab } from '../types';
5
-
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 {
22
- isOpen: boolean;
23
- open: (options?: ProfileDialogContent) => void;
24
- close: () => void;
25
- toggle: () => void;
26
- }
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
-
39
- export const useProfileDialogStore = create<ProfileDialogState>((set) => ({
40
- isOpen: false,
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 }),
50
- toggle: () => set((state) => ({ isOpen: !state.isOpen })),
51
- }));