@djangocfg/layouts 1.2.3 → 1.2.4
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 +5 -5
- package/src/auth/context/AuthContext.tsx +24 -5
- package/src/layouts/AppLayout/components/PackageVersions/packageVersions.config.ts +8 -8
- package/src/layouts/AppLayout/components/UserMenu.tsx +36 -17
- package/src/layouts/AppLayout/layouts/PrivateLayout/PrivateLayout.tsx +2 -13
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardHeader.tsx +4 -89
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Footer.tsx +23 -43
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Navigation.tsx +4 -4
- package/src/layouts/ProfileLayout/components/AvatarSection.tsx +8 -3
- package/src/layouts/UILayout/UIGuideLanding.tsx +7 -33
- package/src/layouts/UILayout/components/Sidebar.tsx +0 -7
- package/src/layouts/UILayout/config/components/blocks.config.tsx +15 -9
- package/src/layouts/UILayout/config/tailwind.config.ts +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"description": "Layout system and components for Unrealon applications",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "DjangoCFG",
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
"check": "tsc --noEmit"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@djangocfg/api": "^1.2.
|
|
57
|
-
"@djangocfg/og-image": "^1.2.
|
|
58
|
-
"@djangocfg/ui": "^1.2.
|
|
56
|
+
"@djangocfg/api": "^1.2.4",
|
|
57
|
+
"@djangocfg/og-image": "^1.2.4",
|
|
58
|
+
"@djangocfg/ui": "^1.2.4",
|
|
59
59
|
"@hookform/resolvers": "^5.2.0",
|
|
60
60
|
"consola": "^3.4.2",
|
|
61
61
|
"lucide-react": "^0.468.0",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"vidstack": "0.6.15"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
-
"@djangocfg/typescript-config": "^1.2.
|
|
79
|
+
"@djangocfg/typescript-config": "^1.2.4",
|
|
80
80
|
"@types/node": "^24.7.2",
|
|
81
81
|
"@types/react": "19.2.2",
|
|
82
82
|
"@types/react-dom": "19.2.1",
|
|
@@ -32,7 +32,18 @@ const hasValidTokens = (): boolean => {
|
|
|
32
32
|
// Internal provider that uses AccountsContext
|
|
33
33
|
const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config }) => {
|
|
34
34
|
const accounts = useAccountsContext();
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
// Smart initial loading state: only true if we don't have tokens yet
|
|
37
|
+
const [isLoading, setIsLoading] = useState(() => {
|
|
38
|
+
// If we already have tokens and profile, don't show loading
|
|
39
|
+
if (typeof window !== 'undefined') {
|
|
40
|
+
const hasTokens = hasValidTokens();
|
|
41
|
+
// Only show loading on initial mount if no tokens
|
|
42
|
+
return !hasTokens;
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
});
|
|
46
|
+
|
|
36
47
|
const [initialized, setInitialized] = useState(false);
|
|
37
48
|
const router = useRouter();
|
|
38
49
|
|
|
@@ -113,7 +124,6 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
113
124
|
|
|
114
125
|
const initializeAuth = async () => {
|
|
115
126
|
authLogger.info('Initializing auth...');
|
|
116
|
-
setIsLoading(true);
|
|
117
127
|
|
|
118
128
|
// Debug token state
|
|
119
129
|
const token = api.getToken();
|
|
@@ -125,7 +135,16 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
125
135
|
const hasTokens = hasValidTokens();
|
|
126
136
|
authLogger.info('Has tokens:', hasTokens);
|
|
127
137
|
|
|
138
|
+
// If profile is already loaded (from SWR cache), we're initialized
|
|
139
|
+
if (user) {
|
|
140
|
+
authLogger.info('Profile already loaded from cache, skipping initialization');
|
|
141
|
+
setInitialized(true);
|
|
142
|
+
setIsLoading(false);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
128
146
|
if (hasTokens) {
|
|
147
|
+
setIsLoading(true);
|
|
129
148
|
try {
|
|
130
149
|
await loadCurrentProfile();
|
|
131
150
|
} catch (error) {
|
|
@@ -133,15 +152,15 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
133
152
|
// If profile loading fails, clear auth state
|
|
134
153
|
clearAuthState('initializeAuth:loadProfileFailed');
|
|
135
154
|
}
|
|
155
|
+
setIsLoading(false);
|
|
136
156
|
} else {
|
|
137
157
|
setInitialized(true);
|
|
158
|
+
setIsLoading(false);
|
|
138
159
|
}
|
|
139
|
-
|
|
140
|
-
setIsLoading(false);
|
|
141
160
|
};
|
|
142
161
|
|
|
143
162
|
initializeAuth();
|
|
144
|
-
}, [initialized, loadCurrentProfile, clearAuthState]);
|
|
163
|
+
}, [initialized, user, loadCurrentProfile, clearAuthState]);
|
|
145
164
|
|
|
146
165
|
// Redirect logic - only for unauthenticated users on protected pages
|
|
147
166
|
useEffect(() => {
|
|
@@ -16,36 +16,36 @@ export interface PackageInfo {
|
|
|
16
16
|
/**
|
|
17
17
|
* Package versions registry
|
|
18
18
|
* Auto-synced from package.json files
|
|
19
|
-
* Last updated: 2025-10-
|
|
19
|
+
* Last updated: 2025-10-24T04:54:59.911Z
|
|
20
20
|
*/
|
|
21
21
|
const PACKAGE_VERSIONS: PackageInfo[] = [
|
|
22
22
|
{
|
|
23
23
|
"name": "@djangocfg/ui",
|
|
24
|
-
"version": "1.2.
|
|
24
|
+
"version": "1.2.4"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"name": "@djangocfg/api",
|
|
28
|
-
"version": "1.2.
|
|
28
|
+
"version": "1.2.4"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"name": "@djangocfg/layouts",
|
|
32
|
-
"version": "1.2.
|
|
32
|
+
"version": "1.2.4"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"name": "@djangocfg/markdown",
|
|
36
|
-
"version": "1.2.
|
|
36
|
+
"version": "1.2.4"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"name": "@djangocfg/og-image",
|
|
40
|
-
"version": "1.2.
|
|
40
|
+
"version": "1.2.4"
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
"name": "@djangocfg/eslint-config",
|
|
44
|
-
"version": "1.2.
|
|
44
|
+
"version": "1.2.4"
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
"name": "@djangocfg/typescript-config",
|
|
48
|
-
"version": "1.2.
|
|
48
|
+
"version": "1.2.4"
|
|
49
49
|
}
|
|
50
50
|
];
|
|
51
51
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { useRouter } from 'next/router';
|
|
12
12
|
import { User, ChevronDown, Crown, LogOut, Settings } from 'lucide-react';
|
|
13
|
-
import { Button, ButtonLink, Card, CardContent } from '@djangocfg/ui/components';
|
|
13
|
+
import { Button, ButtonLink, Card, CardContent, Avatar, AvatarImage, AvatarFallback } from '@djangocfg/ui/components';
|
|
14
14
|
import { ThemeToggle } from '@djangocfg/ui/theme';
|
|
15
15
|
import { useAppContext } from '../context';
|
|
16
16
|
import { useAuth } from '../../../auth';
|
|
@@ -46,6 +46,14 @@ export function UserMenu({ variant, onNavigate }: UserMenuProps) {
|
|
|
46
46
|
|
|
47
47
|
// === DATA PREPARATION (before rendering) ===
|
|
48
48
|
|
|
49
|
+
// Prepare user data
|
|
50
|
+
const userAvatar = user?.avatar || '';
|
|
51
|
+
const userEmail = user?.email || '';
|
|
52
|
+
const displayName = user?.display_username || userEmail || 'User';
|
|
53
|
+
const userInitial = displayName.charAt(0).toUpperCase();
|
|
54
|
+
const dashboardPath = publicLayout.userMenu.dashboardPath;
|
|
55
|
+
const profilePath = publicLayout.userMenu.profilePath;
|
|
56
|
+
|
|
49
57
|
// Handle logout
|
|
50
58
|
const handleLogout = React.useCallback(() => {
|
|
51
59
|
logout();
|
|
@@ -66,7 +74,7 @@ export function UserMenu({ variant, onNavigate }: UserMenuProps) {
|
|
|
66
74
|
type: 'link',
|
|
67
75
|
icon: <Settings className="size-4" />,
|
|
68
76
|
label: 'Profile',
|
|
69
|
-
href:
|
|
77
|
+
href: profilePath,
|
|
70
78
|
variant: 'ghost',
|
|
71
79
|
});
|
|
72
80
|
|
|
@@ -82,7 +90,7 @@ export function UserMenu({ variant, onNavigate }: UserMenuProps) {
|
|
|
82
90
|
});
|
|
83
91
|
|
|
84
92
|
return items;
|
|
85
|
-
}, [
|
|
93
|
+
}, [profilePath, handleLogout]);
|
|
86
94
|
|
|
87
95
|
// === UNIFIED MENU ITEM RENDERER ===
|
|
88
96
|
|
|
@@ -174,9 +182,9 @@ export function UserMenu({ variant, onNavigate }: UserMenuProps) {
|
|
|
174
182
|
{isAuthenticated ? (
|
|
175
183
|
<div className="flex items-center gap-3">
|
|
176
184
|
{/* Dashboard button (only if not on dashboard) */}
|
|
177
|
-
{
|
|
185
|
+
{dashboardPath && !isDashboard && (
|
|
178
186
|
<ButtonLink
|
|
179
|
-
href={
|
|
187
|
+
href={dashboardPath}
|
|
180
188
|
variant="default"
|
|
181
189
|
size="sm"
|
|
182
190
|
className="gap-2"
|
|
@@ -189,13 +197,23 @@ export function UserMenu({ variant, onNavigate }: UserMenuProps) {
|
|
|
189
197
|
{/* User Dropdown */}
|
|
190
198
|
<div className="relative">
|
|
191
199
|
<button
|
|
192
|
-
className="flex items-center gap-2 px-
|
|
200
|
+
className="flex items-center gap-2 px-2 py-1.5 rounded-sm text-sm font-medium transition-colors text-foreground hover:text-primary hover:bg-accent/50 cursor-pointer"
|
|
193
201
|
onClick={toggleUserMenu}
|
|
194
202
|
aria-haspopup="true"
|
|
195
203
|
aria-expanded={userMenuOpen}
|
|
196
204
|
>
|
|
197
|
-
<
|
|
198
|
-
|
|
205
|
+
<Avatar className="h-7 w-7 ring-1 ring-primary/20">
|
|
206
|
+
<AvatarImage
|
|
207
|
+
src={userAvatar}
|
|
208
|
+
alt={displayName}
|
|
209
|
+
/>
|
|
210
|
+
<AvatarFallback className="bg-primary/10 text-primary text-xs">
|
|
211
|
+
{userInitial}
|
|
212
|
+
</AvatarFallback>
|
|
213
|
+
</Avatar>
|
|
214
|
+
<span className="max-w-[120px] truncate">
|
|
215
|
+
{displayName}
|
|
216
|
+
</span>
|
|
199
217
|
<ChevronDown
|
|
200
218
|
className={`size-4 transition-transform ${
|
|
201
219
|
userMenuOpen ? 'rotate-180' : ''
|
|
@@ -207,13 +225,14 @@ export function UserMenu({ variant, onNavigate }: UserMenuProps) {
|
|
|
207
225
|
<>
|
|
208
226
|
{/* Backdrop */}
|
|
209
227
|
<div
|
|
210
|
-
className="fixed inset-0 z-[9995]"
|
|
228
|
+
className="fixed inset-0 z-[9995] bg-transparent"
|
|
211
229
|
onClick={closeUserMenu}
|
|
212
230
|
aria-hidden="true"
|
|
231
|
+
style={{ cursor: 'default' }}
|
|
213
232
|
/>
|
|
214
233
|
{/* Dropdown */}
|
|
215
234
|
<div
|
|
216
|
-
className="absolute top-full right-0 mt-2 w-
|
|
235
|
+
className="absolute top-full right-0 mt-2 w-52 rounded-sm shadow-sm backdrop-blur-xl z-[9996] bg-popover border border-border"
|
|
217
236
|
role="menu"
|
|
218
237
|
aria-label="User menu"
|
|
219
238
|
>
|
|
@@ -222,7 +241,7 @@ export function UserMenu({ variant, onNavigate }: UserMenuProps) {
|
|
|
222
241
|
<div className="px-3 py-2 text-sm mb-2 border-b border-border">
|
|
223
242
|
<div className="text-muted-foreground">Signed in as:</div>
|
|
224
243
|
<div className="font-medium truncate text-popover-foreground mt-1">
|
|
225
|
-
{
|
|
244
|
+
{userEmail}
|
|
226
245
|
</div>
|
|
227
246
|
</div>
|
|
228
247
|
|
|
@@ -253,10 +272,10 @@ export function UserMenu({ variant, onNavigate }: UserMenuProps) {
|
|
|
253
272
|
{/* User Info Header */}
|
|
254
273
|
<div className="flex items-center gap-3 mb-4 p-3 rounded-sm border border-border bg-accent/70">
|
|
255
274
|
<div className="w-10 h-10 rounded-full flex items-center justify-center bg-primary flex-shrink-0 overflow-hidden relative">
|
|
256
|
-
{
|
|
275
|
+
{userAvatar ? (
|
|
257
276
|
<img
|
|
258
|
-
src={
|
|
259
|
-
alt={
|
|
277
|
+
src={userAvatar}
|
|
278
|
+
alt={displayName}
|
|
260
279
|
className="w-10 h-10 rounded-full object-cover"
|
|
261
280
|
/>
|
|
262
281
|
) : (
|
|
@@ -270,7 +289,7 @@ export function UserMenu({ variant, onNavigate }: UserMenuProps) {
|
|
|
270
289
|
Signed in as
|
|
271
290
|
</p>
|
|
272
291
|
<p className="text-sm font-semibold truncate text-foreground">
|
|
273
|
-
{
|
|
292
|
+
{displayName}
|
|
274
293
|
</p>
|
|
275
294
|
</div>
|
|
276
295
|
</div>
|
|
@@ -278,9 +297,9 @@ export function UserMenu({ variant, onNavigate }: UserMenuProps) {
|
|
|
278
297
|
{/* Action Buttons */}
|
|
279
298
|
<div className="space-y-3">
|
|
280
299
|
{/* Dashboard link */}
|
|
281
|
-
{
|
|
300
|
+
{dashboardPath && (
|
|
282
301
|
<ButtonLink
|
|
283
|
-
href={
|
|
302
|
+
href={dashboardPath}
|
|
284
303
|
variant="default"
|
|
285
304
|
size="sm"
|
|
286
305
|
className="w-full h-9 gap-2"
|
|
@@ -38,7 +38,7 @@ export function PrivateLayout({ children }: PrivateLayoutProps) {
|
|
|
38
38
|
|
|
39
39
|
const { privateLayout, routes } = config;
|
|
40
40
|
|
|
41
|
-
// Redirect unauthenticated users
|
|
41
|
+
// Redirect unauthenticated users (only when not loading)
|
|
42
42
|
useEffect(() => {
|
|
43
43
|
if (!isLoading && !isAuthenticated) {
|
|
44
44
|
const redirect = routes.detectors.getUnauthenticatedRedirect(
|
|
@@ -50,19 +50,8 @@ export function PrivateLayout({ children }: PrivateLayoutProps) {
|
|
|
50
50
|
}
|
|
51
51
|
}, [isLoading, isAuthenticated, routes]);
|
|
52
52
|
|
|
53
|
-
// Show loading state while checking auth
|
|
54
|
-
if (isLoading) {
|
|
55
|
-
return (
|
|
56
|
-
<div className="min-h-screen bg-background flex items-center justify-center">
|
|
57
|
-
<div className="flex flex-col items-center gap-4">
|
|
58
|
-
<div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin" />
|
|
59
|
-
<p className="text-sm text-muted-foreground">Loading...</p>
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
53
|
// Don't render content if user is not authenticated
|
|
54
|
+
// Note: Loading state is now handled in AppLayout, so we don't show it here
|
|
66
55
|
if (!isAuthenticated) {
|
|
67
56
|
return null;
|
|
68
57
|
}
|
|
@@ -7,29 +7,18 @@
|
|
|
7
7
|
|
|
8
8
|
'use client';
|
|
9
9
|
|
|
10
|
-
import { Bell
|
|
10
|
+
import { Bell } from 'lucide-react';
|
|
11
11
|
import React from 'react';
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
|
-
Avatar,
|
|
15
|
-
AvatarFallback,
|
|
16
|
-
AvatarImage,
|
|
17
|
-
Badge,
|
|
18
14
|
Button,
|
|
19
|
-
DropdownMenu,
|
|
20
|
-
DropdownMenuContent,
|
|
21
|
-
DropdownMenuItem,
|
|
22
|
-
DropdownMenuLabel,
|
|
23
|
-
DropdownMenuSeparator,
|
|
24
|
-
DropdownMenuTrigger,
|
|
25
15
|
Separator,
|
|
26
16
|
SidebarTrigger,
|
|
27
17
|
} from '@djangocfg/ui';
|
|
28
18
|
import { ThemeToggle } from '@djangocfg/ui/theme';
|
|
29
|
-
import { useAuthDialog } from '../../../../../snippets';
|
|
30
19
|
import { useAppContext } from '../../../context';
|
|
31
|
-
import { useAuth } from '../../../../../auth';
|
|
32
20
|
import { useNavigation } from '../../../hooks';
|
|
21
|
+
import { UserMenu } from '../../../components';
|
|
33
22
|
|
|
34
23
|
/**
|
|
35
24
|
* Dashboard Header Component
|
|
@@ -47,10 +36,7 @@ import { useNavigation } from '../../../hooks';
|
|
|
47
36
|
*/
|
|
48
37
|
export function DashboardHeader() {
|
|
49
38
|
const { config } = useAppContext();
|
|
50
|
-
const { user } = useAuth();
|
|
51
39
|
const { getPageTitle } = useNavigation();
|
|
52
|
-
const { openAuthDialog } = useAuthDialog();
|
|
53
|
-
const { logout } = useAuth();
|
|
54
40
|
|
|
55
41
|
const { privateLayout } = config;
|
|
56
42
|
const pageTitle = getPageTitle();
|
|
@@ -60,10 +46,6 @@ export function DashboardHeader() {
|
|
|
60
46
|
console.log('Notifications clicked');
|
|
61
47
|
};
|
|
62
48
|
|
|
63
|
-
const handleLogin = () => {
|
|
64
|
-
openAuthDialog();
|
|
65
|
-
};
|
|
66
|
-
|
|
67
49
|
return (
|
|
68
50
|
<header className="sticky top-0 py-2 z-10 h-16 flex items-center justify-between px-4 shrink-0 bg-background border-b border-border">
|
|
69
51
|
{/* Left side */}
|
|
@@ -95,75 +77,8 @@ export function DashboardHeader() {
|
|
|
95
77
|
{/* Theme Toggle */}
|
|
96
78
|
<ThemeToggle />
|
|
97
79
|
|
|
98
|
-
{/* User menu
|
|
99
|
-
|
|
100
|
-
<DropdownMenu>
|
|
101
|
-
<DropdownMenuTrigger asChild>
|
|
102
|
-
<Button variant="ghost" className="flex items-center gap-2 p-2">
|
|
103
|
-
<Avatar className="h-8 w-8">
|
|
104
|
-
<AvatarImage
|
|
105
|
-
src={user.avatar || ''}
|
|
106
|
-
alt={user.display_username || user.email || ''}
|
|
107
|
-
/>
|
|
108
|
-
<AvatarFallback className="bg-primary/10 text-primary">
|
|
109
|
-
{user.display_username?.charAt(0)?.toUpperCase() || 'U'}
|
|
110
|
-
</AvatarFallback>
|
|
111
|
-
</Avatar>
|
|
112
|
-
<span className="hidden md:block text-sm font-medium">
|
|
113
|
-
{user.display_username}
|
|
114
|
-
</span>
|
|
115
|
-
</Button>
|
|
116
|
-
</DropdownMenuTrigger>
|
|
117
|
-
|
|
118
|
-
<DropdownMenuContent align="end" className="w-48">
|
|
119
|
-
<DropdownMenuLabel className="p-0 font-normal">
|
|
120
|
-
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
|
121
|
-
<Avatar className="h-8 w-8">
|
|
122
|
-
<AvatarImage
|
|
123
|
-
src={user.avatar || ''}
|
|
124
|
-
alt={user.display_username || user.email || ''}
|
|
125
|
-
/>
|
|
126
|
-
<AvatarFallback className="bg-primary/10 text-primary">
|
|
127
|
-
{user.display_username?.charAt(0)?.toUpperCase() || 'U'}
|
|
128
|
-
</AvatarFallback>
|
|
129
|
-
</Avatar>
|
|
130
|
-
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
131
|
-
<span className="truncate font-semibold">
|
|
132
|
-
{user.display_username || user.full_name || user.email}
|
|
133
|
-
</span>
|
|
134
|
-
<span className="truncate text-xs text-muted-foreground">
|
|
135
|
-
{user.email}
|
|
136
|
-
</span>
|
|
137
|
-
</div>
|
|
138
|
-
</div>
|
|
139
|
-
</DropdownMenuLabel>
|
|
140
|
-
|
|
141
|
-
<DropdownMenuSeparator />
|
|
142
|
-
|
|
143
|
-
<DropdownMenuItem asChild>
|
|
144
|
-
<a
|
|
145
|
-
href={privateLayout.profileHref}
|
|
146
|
-
className="flex items-center gap-2"
|
|
147
|
-
>
|
|
148
|
-
<User className="h-4 w-4" />
|
|
149
|
-
Profile
|
|
150
|
-
</a>
|
|
151
|
-
</DropdownMenuItem>
|
|
152
|
-
|
|
153
|
-
<DropdownMenuSeparator />
|
|
154
|
-
|
|
155
|
-
<DropdownMenuItem onClick={logout} className="gap-2">
|
|
156
|
-
<LogOut className="h-4 w-4" />
|
|
157
|
-
Logout
|
|
158
|
-
</DropdownMenuItem>
|
|
159
|
-
</DropdownMenuContent>
|
|
160
|
-
</DropdownMenu>
|
|
161
|
-
) : (
|
|
162
|
-
<Button onClick={handleLogin} size="sm" className="gap-2">
|
|
163
|
-
<LogIn className="h-4 w-4" />
|
|
164
|
-
<span className="hidden sm:inline">Sign In</span>
|
|
165
|
-
</Button>
|
|
166
|
-
)}
|
|
80
|
+
{/* User menu - unified component */}
|
|
81
|
+
<UserMenu variant="desktop" />
|
|
167
82
|
</div>
|
|
168
83
|
</header>
|
|
169
84
|
);
|
|
@@ -41,7 +41,7 @@ export function Footer() {
|
|
|
41
41
|
<div className="w-full px-4 py-8">
|
|
42
42
|
{/* Project Info */}
|
|
43
43
|
<div className="text-center space-y-4 mb-6">
|
|
44
|
-
<div className="flex items-center justify-center
|
|
44
|
+
<div className="flex items-center justify-center gap-2">
|
|
45
45
|
<div className="w-6 h-6 flex items-center justify-center">
|
|
46
46
|
<img
|
|
47
47
|
src={app.logoPath}
|
|
@@ -95,10 +95,10 @@ export function Footer() {
|
|
|
95
95
|
{/* Bottom Section */}
|
|
96
96
|
<div className="border-t border-border pt-4">
|
|
97
97
|
<div className="text-center space-y-2">
|
|
98
|
-
<div className="text-
|
|
98
|
+
<div className="text-xs text-muted-foreground">
|
|
99
99
|
© {currentYear} {app.name}. All rights reserved.
|
|
100
100
|
</div>
|
|
101
|
-
<div className="text-
|
|
101
|
+
<div className="text-xs text-muted-foreground flex items-center justify-center gap-1">
|
|
102
102
|
Made with ❤️ by{' '}
|
|
103
103
|
<a
|
|
104
104
|
href="https://reforms.ai"
|
|
@@ -152,46 +152,26 @@ export function Footer() {
|
|
|
152
152
|
</div>
|
|
153
153
|
|
|
154
154
|
{/* Right Column - Footer Menu Sections */}
|
|
155
|
-
<div className="grid grid-cols-2
|
|
156
|
-
{footer.menuSections.map((section) =>
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
return (
|
|
176
|
-
<div key={section.title}>
|
|
177
|
-
<h3 className="text-lg font-semibold text-foreground mb-4">
|
|
178
|
-
{section.title}
|
|
179
|
-
</h3>
|
|
180
|
-
<ul className="space-y-2">
|
|
181
|
-
{section.items.map((item) => (
|
|
182
|
-
<li key={item.path}>
|
|
183
|
-
<Link
|
|
184
|
-
href={item.path}
|
|
185
|
-
className="text-muted-foreground hover:text-primary text-sm transition-colors"
|
|
186
|
-
>
|
|
187
|
-
{item.label}
|
|
188
|
-
</Link>
|
|
189
|
-
</li>
|
|
190
|
-
))}
|
|
191
|
-
</ul>
|
|
192
|
-
</div>
|
|
193
|
-
);
|
|
194
|
-
})}
|
|
155
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 flex-1">
|
|
156
|
+
{footer.menuSections.map((section) => (
|
|
157
|
+
<div key={section.title}>
|
|
158
|
+
<h3 className="text-sm font-semibold text-foreground mb-3">
|
|
159
|
+
{section.title}
|
|
160
|
+
</h3>
|
|
161
|
+
<ul className="space-y-2">
|
|
162
|
+
{section.items.map((item) => (
|
|
163
|
+
<li key={item.path}>
|
|
164
|
+
<Link
|
|
165
|
+
href={item.path}
|
|
166
|
+
className="text-muted-foreground hover:text-primary text-sm transition-colors"
|
|
167
|
+
>
|
|
168
|
+
{item.label}
|
|
169
|
+
</Link>
|
|
170
|
+
</li>
|
|
171
|
+
))}
|
|
172
|
+
</ul>
|
|
173
|
+
</div>
|
|
174
|
+
))}
|
|
195
175
|
</div>
|
|
196
176
|
</div>
|
|
197
177
|
|
|
@@ -45,9 +45,9 @@ export function Navigation() {
|
|
|
45
45
|
const { app, publicLayout } = config;
|
|
46
46
|
|
|
47
47
|
return (
|
|
48
|
-
<nav className="sticky top-0 w-full border-b backdrop-blur-xl z-
|
|
48
|
+
<nav className="sticky top-0 w-full border-b backdrop-blur-xl z-[100] bg-background/80 border-border/30 isolate">
|
|
49
49
|
<div className="w-full px-4 sm:px-6 lg:px-8">
|
|
50
|
-
<div className="flex items-center justify-between h-
|
|
50
|
+
<div className="flex items-center justify-between py-2 min-h-[40px]">
|
|
51
51
|
{/* Left side - Logo and Navigation Menu */}
|
|
52
52
|
<div className="flex items-center gap-6">
|
|
53
53
|
{/* Logo */}
|
|
@@ -153,10 +153,10 @@ export function Navigation() {
|
|
|
153
153
|
{isMobile && (
|
|
154
154
|
<button
|
|
155
155
|
onClick={toggleMobileDrawer}
|
|
156
|
-
className="p-
|
|
156
|
+
className="p-2 rounded-md transition-colors hover:bg-accent text-foreground hover:text-primary"
|
|
157
157
|
aria-label="Toggle mobile menu"
|
|
158
158
|
>
|
|
159
|
-
<Menu className="size-
|
|
159
|
+
<Menu className="size-6" />
|
|
160
160
|
</button>
|
|
161
161
|
)}
|
|
162
162
|
</div>
|
|
@@ -72,18 +72,23 @@ export const AvatarSection = () => {
|
|
|
72
72
|
return (
|
|
73
73
|
<div className="flex flex-col items-center mb-4">
|
|
74
74
|
<div className="relative group">
|
|
75
|
-
<Avatar
|
|
75
|
+
<Avatar
|
|
76
|
+
className="aspect-square rounded-full overflow-hidden ring-1 ring-foreground/20 transition-transform group-hover:scale-105"
|
|
77
|
+
style={{ width: '80px', height: '80px' }}
|
|
78
|
+
>
|
|
76
79
|
{avatarPreview ? (
|
|
77
80
|
<img
|
|
78
81
|
src={avatarPreview}
|
|
79
82
|
alt="Avatar preview"
|
|
80
|
-
className="w-full h-full object-cover
|
|
83
|
+
className="w-full h-full object-cover"
|
|
84
|
+
style={{ borderRadius: '50%' }}
|
|
81
85
|
/>
|
|
82
86
|
) : user?.avatar ? (
|
|
83
87
|
<img
|
|
84
88
|
src={user.avatar}
|
|
85
89
|
alt="User avatar"
|
|
86
|
-
className="w-full h-full object-cover
|
|
90
|
+
className="w-full h-full object-cover"
|
|
91
|
+
style={{ borderRadius: '50%' }}
|
|
87
92
|
/>
|
|
88
93
|
) : (
|
|
89
94
|
<AvatarFallback className="text-2xl font-semibold bg-gradient-to-br from-primary to-primary/80 text-primary-foreground">
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
Blocks,
|
|
19
19
|
Github,
|
|
20
20
|
BookOpen,
|
|
21
|
+
Layout,
|
|
22
|
+
CheckCircle2,
|
|
21
23
|
} from 'lucide-react';
|
|
22
24
|
import {
|
|
23
25
|
SuperHero,
|
|
@@ -75,6 +77,7 @@ export function UIGuideLanding({
|
|
|
75
77
|
{ number: "11", label: "Hooks" },
|
|
76
78
|
{ number: "100%", label: "Type Safe" },
|
|
77
79
|
]}
|
|
80
|
+
showBackgroundSwitcher={true}
|
|
78
81
|
/>
|
|
79
82
|
|
|
80
83
|
{/* Features Section using FeatureSection block */}
|
|
@@ -124,21 +127,25 @@ export function UIGuideLanding({
|
|
|
124
127
|
columns={4}
|
|
125
128
|
stats={[
|
|
126
129
|
{
|
|
130
|
+
icon: <Layout className="w-6 h-6" />,
|
|
127
131
|
number: "56+",
|
|
128
132
|
label: "UI Components",
|
|
129
133
|
description: "Production-ready",
|
|
130
134
|
},
|
|
131
135
|
{
|
|
136
|
+
icon: <Blocks className="w-6 h-6" />,
|
|
132
137
|
number: "7",
|
|
133
138
|
label: "Landing Blocks",
|
|
134
139
|
description: "Pre-built sections",
|
|
135
140
|
},
|
|
136
141
|
{
|
|
142
|
+
icon: <Code2 className="w-6 h-6" />,
|
|
137
143
|
number: "11",
|
|
138
144
|
label: "React Hooks",
|
|
139
145
|
description: "Custom utilities",
|
|
140
146
|
},
|
|
141
147
|
{
|
|
148
|
+
icon: <CheckCircle2 className="w-6 h-6" />,
|
|
142
149
|
number: "100%",
|
|
143
150
|
label: "Type Safe",
|
|
144
151
|
description: "Full TypeScript",
|
|
@@ -160,39 +167,6 @@ export function UIGuideLanding({
|
|
|
160
167
|
}}
|
|
161
168
|
background="gradient"
|
|
162
169
|
/>
|
|
163
|
-
|
|
164
|
-
{/* Simple Footer */}
|
|
165
|
-
<footer className="border-t py-8 sm:py-10 md:py-12 bg-background">
|
|
166
|
-
<div className="container max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
167
|
-
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
|
168
|
-
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
169
|
-
<Package className="w-4 h-4" />
|
|
170
|
-
<span>Built with Radix UI + Tailwind CSS</span>
|
|
171
|
-
</div>
|
|
172
|
-
<div className="flex items-center gap-6">
|
|
173
|
-
<button
|
|
174
|
-
onClick={handleBrowseComponents}
|
|
175
|
-
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
176
|
-
>
|
|
177
|
-
Components
|
|
178
|
-
</button>
|
|
179
|
-
<button
|
|
180
|
-
onClick={handleViewBlocks}
|
|
181
|
-
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
182
|
-
>
|
|
183
|
-
Blocks
|
|
184
|
-
</button>
|
|
185
|
-
<button
|
|
186
|
-
onClick={() => window.open(githubUrl, '_blank')}
|
|
187
|
-
className="text-sm text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
|
|
188
|
-
>
|
|
189
|
-
<Github className="w-4 h-4" />
|
|
190
|
-
GitHub
|
|
191
|
-
</button>
|
|
192
|
-
</div>
|
|
193
|
-
</div>
|
|
194
|
-
</div>
|
|
195
|
-
</footer>
|
|
196
170
|
</div>
|
|
197
171
|
);
|
|
198
172
|
}
|
|
@@ -123,13 +123,6 @@ function SidebarContent({
|
|
|
123
123
|
</div>
|
|
124
124
|
</div>
|
|
125
125
|
</div>
|
|
126
|
-
|
|
127
|
-
{/* Footer */}
|
|
128
|
-
<div className="border-t p-4">
|
|
129
|
-
<p className="text-xs text-muted-foreground">
|
|
130
|
-
Built with Radix UI + Tailwind CSS v4
|
|
131
|
-
</p>
|
|
132
|
-
</div>
|
|
133
126
|
</div>
|
|
134
127
|
);
|
|
135
128
|
}
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
StatsSection,
|
|
13
13
|
TestimonialSection,
|
|
14
14
|
} from '@djangocfg/ui/blocks';
|
|
15
|
-
import { Sparkles, BookOpen, Zap, Shield, Code } from 'lucide-react';
|
|
15
|
+
import { Sparkles, BookOpen, Zap, Shield, Code, Users, Building2, TrendingUp, Headphones } from 'lucide-react';
|
|
16
16
|
import type { ComponentConfig } from './types';
|
|
17
17
|
|
|
18
18
|
export const BLOCKS: ComponentConfig[] = [
|
|
@@ -60,7 +60,9 @@ export const BLOCKS: ComponentConfig[] = [
|
|
|
60
60
|
{ number: "6", label: "Hooks" },
|
|
61
61
|
{ number: "100%", label: "Type Safe" }
|
|
62
62
|
]}
|
|
63
|
-
|
|
63
|
+
backgroundVariant="waves"
|
|
64
|
+
backgroundIntensity="medium"
|
|
65
|
+
showBackgroundSwitcher={true}
|
|
64
66
|
/>`,
|
|
65
67
|
preview: (
|
|
66
68
|
<SuperHero
|
|
@@ -82,7 +84,9 @@ export const BLOCKS: ComponentConfig[] = [
|
|
|
82
84
|
{ number: "6", label: "Hooks" },
|
|
83
85
|
{ number: "100%", label: "Type Safe" },
|
|
84
86
|
]}
|
|
85
|
-
|
|
87
|
+
backgroundVariant="waves"
|
|
88
|
+
backgroundIntensity="medium"
|
|
89
|
+
showBackgroundSwitcher={true}
|
|
86
90
|
/>
|
|
87
91
|
),
|
|
88
92
|
},
|
|
@@ -181,18 +185,20 @@ export const BLOCKS: ComponentConfig[] = [
|
|
|
181
185
|
example: `<StatsSection
|
|
182
186
|
title="Our Impact"
|
|
183
187
|
stats={[
|
|
184
|
-
{ number: "10K+", label: "Active Users" },
|
|
185
|
-
{ number: "
|
|
188
|
+
{ icon: <Users className="w-6 h-6" />, number: "10K+", label: "Active Users" },
|
|
189
|
+
{ icon: <Building2 className="w-6 h-6" />, number: "500+", label: "Companies" },
|
|
190
|
+
{ icon: <TrendingUp className="w-6 h-6" />, number: "99.9%", label: "Uptime" },
|
|
191
|
+
{ icon: <Headphones className="w-6 h-6" />, number: "24/7", label: "Support" }
|
|
186
192
|
]}
|
|
187
193
|
/>`,
|
|
188
194
|
preview: (
|
|
189
195
|
<StatsSection
|
|
190
196
|
title="Our Impact"
|
|
191
197
|
stats={[
|
|
192
|
-
{ number: "10K+", label: "Active Users" },
|
|
193
|
-
{ number: "500+", label: "Companies" },
|
|
194
|
-
{ number: "99.9%", label: "Uptime" },
|
|
195
|
-
{ number: "24/7", label: "Support" }
|
|
198
|
+
{ icon: <Users className="w-6 h-6" />, number: "10K+", label: "Active Users" },
|
|
199
|
+
{ icon: <Building2 className="w-6 h-6" />, number: "500+", label: "Companies" },
|
|
200
|
+
{ icon: <TrendingUp className="w-6 h-6" />, number: "99.9%", label: "Uptime" },
|
|
201
|
+
{ icon: <Headphones className="w-6 h-6" />, number: "24/7", label: "Support" }
|
|
196
202
|
]}
|
|
197
203
|
/>
|
|
198
204
|
),
|
|
@@ -30,6 +30,12 @@ export const TAILWIND_GUIDE: TailwindGuide = {
|
|
|
30
30
|
"Use standard Tailwind classes only: py-16 sm:py-20 md:py-24 lg:py-32",
|
|
31
31
|
"Responsive patterns: px-4 sm:px-6 lg:px-8",
|
|
32
32
|
"Container pattern: container max-w-7xl mx-auto",
|
|
33
|
+
"IMPORTANT: Arbitrary values like h-[80px] may NOT work in v4 due to changed syntax",
|
|
34
|
+
"For fixed sizes: Use inline styles style={{ width: '80px', height: '80px' }} - always reliable",
|
|
35
|
+
"Spacing utilities (h-20, p-4, etc.) require --spacing-* variables defined in @theme block before Tailwind import",
|
|
36
|
+
"Import order is critical but hard to fix: changing order breaks other styles",
|
|
37
|
+
"Use aspect-square for maintaining 1:1 ratio (circles, squares)",
|
|
38
|
+
"Use overflow-hidden with rounded-full for perfect circles",
|
|
33
39
|
"Avoid custom utilities like: section-padding, animate-*, shadow-brand",
|
|
34
40
|
"Mobile-first approach with breakpoints: sm: (640px), md: (768px), lg: (1024px), xl: (1280px)",
|
|
35
41
|
"Use CSS variables: var(--color-primary), var(--font-family-sans)"
|
|
@@ -53,6 +59,7 @@ export const TAILWIND_GUIDE: TailwindGuide = {
|
|
|
53
59
|
--color-primary: #3b82f6;
|
|
54
60
|
--color-secondary: #8b5cf6;
|
|
55
61
|
--font-family-sans: ui-sans-serif, system-ui, sans-serif;
|
|
62
|
+
--spacing-20: 5rem; /* Required for h-20, w-20 to work */
|
|
56
63
|
}`
|
|
57
64
|
},
|
|
58
65
|
{
|
|
@@ -65,6 +72,16 @@ export const TAILWIND_GUIDE: TailwindGuide = {
|
|
|
65
72
|
</h1>
|
|
66
73
|
</div>
|
|
67
74
|
</section>`
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
title: "Fixed Sizes with Inline Styles",
|
|
78
|
+
description: "Most reliable way to set exact dimensions in v4",
|
|
79
|
+
code: `<Avatar
|
|
80
|
+
className="aspect-square rounded-full overflow-hidden ring-1 ring-foreground/20"
|
|
81
|
+
style={{ width: '80px', height: '80px' }}
|
|
82
|
+
>
|
|
83
|
+
<AvatarImage src={avatar} alt="User" />
|
|
84
|
+
</Avatar>`
|
|
68
85
|
},
|
|
69
86
|
{
|
|
70
87
|
title: "Component Styling",
|