@djangocfg/layouts 1.4.16 → 1.4.18
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/layouts/AppLayout/layouts/PublicLayout/components/MobileDrawer.tsx +87 -137
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Navigation.tsx +1 -1
- package/src/layouts/UILayout/components/layout/Header/TestValidationButton.tsx +4 -1
- package/src/layouts/UILayout/components/layout/Sidebar/Sidebar.tsx +52 -51
- package/src/layouts/UILayout/components/layout/Sidebar/SidebarContent.tsx +26 -19
- package/src/layouts/UILayout/components/layout/Sidebar/SidebarFooter.tsx +31 -31
- package/src/layouts/UILayout/config/components/layout.config.tsx +22 -4
- package/src/layouts/UILayout/context/ShowcaseContext.tsx +41 -3
- package/src/layouts/UILayout/core/UILayout.tsx +86 -63
- package/src/layouts/UILayout/core/UILayoutSidebar.tsx +4 -137
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.18",
|
|
4
4
|
"description": "Layout system and components for Unrealon applications",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "DjangoCFG",
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
"check": "tsc --noEmit"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"@djangocfg/api": "^1.4.
|
|
67
|
-
"@djangocfg/og-image": "^1.4.
|
|
68
|
-
"@djangocfg/ui": "^1.4.
|
|
66
|
+
"@djangocfg/api": "^1.4.18",
|
|
67
|
+
"@djangocfg/og-image": "^1.4.18",
|
|
68
|
+
"@djangocfg/ui": "^1.4.18",
|
|
69
69
|
"@hookform/resolvers": "^5.2.0",
|
|
70
70
|
"consola": "^3.4.2",
|
|
71
71
|
"lucide-react": "^0.468.0",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"vidstack": "0.6.15"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
|
-
"@djangocfg/typescript-config": "^1.4.
|
|
89
|
+
"@djangocfg/typescript-config": "^1.4.18",
|
|
90
90
|
"@types/node": "^24.7.2",
|
|
91
91
|
"@types/react": "19.2.2",
|
|
92
92
|
"@types/react-dom": "19.2.1",
|
|
@@ -2,15 +2,21 @@
|
|
|
2
2
|
* Mobile Drawer
|
|
3
3
|
*
|
|
4
4
|
* Full-screen slide-in drawer for mobile navigation
|
|
5
|
-
*
|
|
5
|
+
* Uses @djangocfg/ui Drawer component (Vaul-based)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
'use client';
|
|
9
9
|
|
|
10
10
|
import React from 'react';
|
|
11
|
-
import { createPortal } from 'react-dom';
|
|
12
11
|
import Link from 'next/link';
|
|
13
12
|
import { X } from 'lucide-react';
|
|
13
|
+
import {
|
|
14
|
+
Drawer,
|
|
15
|
+
DrawerContent,
|
|
16
|
+
DrawerHeader,
|
|
17
|
+
DrawerTitle,
|
|
18
|
+
DrawerClose,
|
|
19
|
+
} from '@djangocfg/ui/components';
|
|
14
20
|
import { useAppContext } from '../../../context';
|
|
15
21
|
import { useNavigation } from '../../../hooks';
|
|
16
22
|
import { UserMenu } from '../../../components';
|
|
@@ -19,10 +25,10 @@ import { UserMenu } from '../../../components';
|
|
|
19
25
|
* Mobile Drawer Component
|
|
20
26
|
*
|
|
21
27
|
* Features:
|
|
22
|
-
* - Slide-in drawer from right
|
|
28
|
+
* - Slide-in drawer from right (using Vaul)
|
|
23
29
|
* - UserMenu component (authenticated/guest)
|
|
24
30
|
* - Navigation sections
|
|
25
|
-
* -
|
|
31
|
+
* - Smooth animations via Vaul library
|
|
26
32
|
*
|
|
27
33
|
* All data from context!
|
|
28
34
|
*/
|
|
@@ -32,37 +38,6 @@ export function MobileDrawer() {
|
|
|
32
38
|
|
|
33
39
|
const { app, publicLayout } = config;
|
|
34
40
|
|
|
35
|
-
// Track if we should render (stays true during close animation)
|
|
36
|
-
const [shouldRender, setShouldRender] = React.useState(false);
|
|
37
|
-
|
|
38
|
-
// Track animation state separately
|
|
39
|
-
const [isOpen, setIsOpen] = React.useState(false);
|
|
40
|
-
|
|
41
|
-
// Handle opening
|
|
42
|
-
React.useEffect(() => {
|
|
43
|
-
if (mobileDrawerOpen) {
|
|
44
|
-
setShouldRender(true);
|
|
45
|
-
// Trigger animation after render
|
|
46
|
-
requestAnimationFrame(() => {
|
|
47
|
-
requestAnimationFrame(() => {
|
|
48
|
-
setIsOpen(true);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
} else {
|
|
52
|
-
// Start close animation
|
|
53
|
-
setIsOpen(false);
|
|
54
|
-
// Wait for animation to finish before unmounting
|
|
55
|
-
const timer = setTimeout(() => {
|
|
56
|
-
setShouldRender(false);
|
|
57
|
-
}, 300);
|
|
58
|
-
return () => clearTimeout(timer);
|
|
59
|
-
}
|
|
60
|
-
}, [mobileDrawerOpen]);
|
|
61
|
-
|
|
62
|
-
const handleClose = () => {
|
|
63
|
-
closeMobileDrawer();
|
|
64
|
-
};
|
|
65
|
-
|
|
66
41
|
const handleNavigate = () => {
|
|
67
42
|
closeMobileDrawer();
|
|
68
43
|
};
|
|
@@ -78,99 +53,50 @@ export function MobileDrawer() {
|
|
|
78
53
|
[publicLayout.navigation.menuSections]
|
|
79
54
|
);
|
|
80
55
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
<div
|
|
100
|
-
className={`fixed top-0 right-0 bottom-0 w-80 z-200 bg-popover border-l border-border shadow-2xl transition-transform duration-300 ease-in-out lg:hidden ${
|
|
101
|
-
isOpen ? 'translate-x-0' : 'translate-x-full'
|
|
102
|
-
}`}
|
|
103
|
-
role="dialog"
|
|
104
|
-
aria-modal="true"
|
|
105
|
-
aria-label="Mobile navigation menu"
|
|
106
|
-
>
|
|
107
|
-
<div className="flex flex-col h-full">
|
|
108
|
-
{/* Header */}
|
|
109
|
-
<div className="flex items-center justify-between p-4 border-b border-border/30">
|
|
110
|
-
<div className="flex items-center gap-3">
|
|
111
|
-
<img
|
|
112
|
-
src={app.logoPath}
|
|
113
|
-
alt={`${app.name} Logo`}
|
|
114
|
-
className="h-8 w-auto object-contain"
|
|
115
|
-
/>
|
|
116
|
-
<span className="text-lg font-bold text-foreground">
|
|
117
|
-
{app.name}
|
|
118
|
-
</span>
|
|
119
|
-
</div>
|
|
120
|
-
<button
|
|
121
|
-
onClick={handleClose}
|
|
122
|
-
className="p-2 rounded-sm transition-colors hover:bg-accent/50"
|
|
123
|
-
aria-label="Close menu"
|
|
124
|
-
>
|
|
125
|
-
<X className="size-5" />
|
|
126
|
-
</button>
|
|
56
|
+
return (
|
|
57
|
+
<Drawer
|
|
58
|
+
open={mobileDrawerOpen}
|
|
59
|
+
onOpenChange={(open) => !open && closeMobileDrawer()}
|
|
60
|
+
direction="right"
|
|
61
|
+
>
|
|
62
|
+
<DrawerContent direction="right" className="w-80 lg:hidden">
|
|
63
|
+
{/* Header */}
|
|
64
|
+
<DrawerHeader className="flex flex-row items-center justify-between p-4 border-b border-border/30">
|
|
65
|
+
<div className="flex items-center gap-3">
|
|
66
|
+
<img
|
|
67
|
+
src={app.logoPath}
|
|
68
|
+
alt={`${app.name} Logo`}
|
|
69
|
+
className="h-8 w-auto object-contain"
|
|
70
|
+
/>
|
|
71
|
+
<DrawerTitle className="text-lg font-bold text-foreground">
|
|
72
|
+
{app.name}
|
|
73
|
+
</DrawerTitle>
|
|
127
74
|
</div>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
? 'bg-accent text-accent-foreground'
|
|
154
|
-
: 'text-foreground hover:bg-accent hover:text-accent-foreground'
|
|
155
|
-
}`}
|
|
156
|
-
onClick={handleNavigate}
|
|
157
|
-
>
|
|
158
|
-
{item.label}
|
|
159
|
-
</Link>
|
|
160
|
-
);
|
|
161
|
-
})}
|
|
162
|
-
</div>
|
|
163
|
-
</div>
|
|
164
|
-
)}
|
|
165
|
-
|
|
166
|
-
{/* Render multiple-items sections normally */}
|
|
167
|
-
{multipleItemsSections.map((section) => (
|
|
168
|
-
<div key={section.title} className="space-y-3">
|
|
169
|
-
<h3 className="px-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
170
|
-
{section.title}
|
|
171
|
-
</h3>
|
|
172
|
-
<div className="space-y-1">
|
|
173
|
-
{section.items.map((item) => (
|
|
75
|
+
<DrawerClose className="p-2 rounded-sm transition-colors hover:bg-accent/50">
|
|
76
|
+
<X className="size-5" />
|
|
77
|
+
<span className="sr-only">Close menu</span>
|
|
78
|
+
</DrawerClose>
|
|
79
|
+
</DrawerHeader>
|
|
80
|
+
|
|
81
|
+
{/* Scrollable Content */}
|
|
82
|
+
<div className="flex-1 overflow-y-auto p-4 space-y-6">
|
|
83
|
+
{/* User Menu Card */}
|
|
84
|
+
<UserMenu variant="mobile" onNavigate={handleNavigate} />
|
|
85
|
+
|
|
86
|
+
{/* Navigation Sections */}
|
|
87
|
+
<div className="space-y-6">
|
|
88
|
+
{/* Group all single-item sections into "Menu" */}
|
|
89
|
+
{singleItemSections.length > 0 && (
|
|
90
|
+
<div className="space-y-3">
|
|
91
|
+
<h3 className="px-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
92
|
+
Menu
|
|
93
|
+
</h3>
|
|
94
|
+
<div className="space-y-1">
|
|
95
|
+
{singleItemSections.map((section) => {
|
|
96
|
+
const item = section.items[0];
|
|
97
|
+
if (!item) return null;
|
|
98
|
+
|
|
99
|
+
return (
|
|
174
100
|
<Link
|
|
175
101
|
key={item.path}
|
|
176
102
|
href={item.path}
|
|
@@ -183,18 +109,42 @@ export function MobileDrawer() {
|
|
|
183
109
|
>
|
|
184
110
|
{item.label}
|
|
185
111
|
</Link>
|
|
186
|
-
)
|
|
187
|
-
|
|
112
|
+
);
|
|
113
|
+
})}
|
|
188
114
|
</div>
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
{/*
|
|
193
|
-
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{/* Render multiple-items sections normally */}
|
|
119
|
+
{multipleItemsSections.map((section) => (
|
|
120
|
+
<div key={section.title} className="space-y-3">
|
|
121
|
+
<h3 className="px-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
122
|
+
{section.title}
|
|
123
|
+
</h3>
|
|
124
|
+
<div className="space-y-1">
|
|
125
|
+
{section.items.map((item) => (
|
|
126
|
+
<Link
|
|
127
|
+
key={item.path}
|
|
128
|
+
href={item.path}
|
|
129
|
+
className={`block px-4 py-3 rounded-sm text-base font-medium transition-colors ${
|
|
130
|
+
isActive(item.path)
|
|
131
|
+
? 'bg-accent text-accent-foreground'
|
|
132
|
+
: 'text-foreground hover:bg-accent hover:text-accent-foreground'
|
|
133
|
+
}`}
|
|
134
|
+
onClick={handleNavigate}
|
|
135
|
+
>
|
|
136
|
+
{item.label}
|
|
137
|
+
</Link>
|
|
138
|
+
))}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
))}
|
|
194
142
|
</div>
|
|
143
|
+
|
|
144
|
+
{/* Bottom spacer */}
|
|
145
|
+
<div style={{ height: '15vh' }}></div>
|
|
195
146
|
</div>
|
|
196
|
-
</
|
|
197
|
-
|
|
198
|
-
document.body
|
|
147
|
+
</DrawerContent>
|
|
148
|
+
</Drawer>
|
|
199
149
|
);
|
|
200
150
|
}
|
|
@@ -45,7 +45,7 @@ export function Navigation() {
|
|
|
45
45
|
const { app, publicLayout } = config;
|
|
46
46
|
|
|
47
47
|
return (
|
|
48
|
-
<nav className="sticky top-0 w-full
|
|
48
|
+
<nav className="sticky top-0 w-full backdrop-blur-xl z-50 isolate" style={{ backgroundColor: 'hsl(var(--background) / 0.8)', boxShadow: '0 1px 0 0 hsl(var(--border))', zIndex: 50 }}>
|
|
49
49
|
<div className="w-full px-4 sm:px-6 lg:px-8">
|
|
50
50
|
<div className="flex items-center justify-between py-2 min-h-[40px]">
|
|
51
51
|
{/* Left side - Logo and Navigation Menu */}
|
|
@@ -157,6 +157,8 @@ export interface TestValidationButtonProps {
|
|
|
157
157
|
size?: 'default' | 'sm' | 'lg' | 'icon';
|
|
158
158
|
/** Additional CSS classes */
|
|
159
159
|
className?: string;
|
|
160
|
+
/** Always show label (default: responsive - hidden on mobile) */
|
|
161
|
+
showLabel?: boolean;
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
/**
|
|
@@ -169,6 +171,7 @@ export function TestValidationButton({
|
|
|
169
171
|
showInProduction = false,
|
|
170
172
|
size = 'sm',
|
|
171
173
|
className,
|
|
174
|
+
showLabel = false,
|
|
172
175
|
}: TestValidationButtonProps) {
|
|
173
176
|
const [isOpen, setIsOpen] = useState(false);
|
|
174
177
|
|
|
@@ -186,7 +189,7 @@ export function TestValidationButton({
|
|
|
186
189
|
className={className}
|
|
187
190
|
>
|
|
188
191
|
<Bug className="h-4 w-4" />
|
|
189
|
-
<span className="ml-2 hidden sm:inline">Test Validation</span>
|
|
192
|
+
<span className={showLabel ? "ml-2" : "ml-2 hidden sm:inline"}>Test Validation</span>
|
|
190
193
|
</Button>
|
|
191
194
|
</DropdownMenuTrigger>
|
|
192
195
|
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sidebar Component
|
|
3
3
|
* Navigation sidebar for component categories
|
|
4
|
-
*
|
|
4
|
+
* Desktop: sticky with react-sticky-box
|
|
5
|
+
* Mobile: Drawer component (Vaul-based)
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
'use client';
|
|
8
9
|
|
|
9
10
|
import React from 'react';
|
|
10
|
-
import {
|
|
11
|
-
import { cn } from '@djangocfg/ui/lib';
|
|
12
|
-
import { useIsMobile } from '@djangocfg/ui';
|
|
11
|
+
import { useIsMobile, Sticky, Drawer, DrawerContent } from '@djangocfg/ui';
|
|
13
12
|
import type { ComponentCategory } from '../../../types';
|
|
14
13
|
import { SidebarContent } from './SidebarContent';
|
|
15
14
|
|
|
@@ -22,73 +21,75 @@ export interface SidebarProps {
|
|
|
22
21
|
onCategoryChange?: (categoryId: string) => void;
|
|
23
22
|
/** Is sidebar open (mobile only) */
|
|
24
23
|
isOpen?: boolean;
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
|
|
24
|
+
/** Close sidebar callback (mobile only) */
|
|
25
|
+
onClose?: () => void;
|
|
26
|
+
/** Function to generate AI context */
|
|
27
|
+
onCopyForAI?: () => string;
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
/**
|
|
32
31
|
* Sidebar Component
|
|
33
32
|
* Desktop: Always visible static sidebar
|
|
34
|
-
* Mobile:
|
|
33
|
+
* Mobile: Drawer-based sidebar with slide animation
|
|
35
34
|
*/
|
|
36
35
|
export function Sidebar({
|
|
37
36
|
categories,
|
|
38
37
|
currentCategory,
|
|
39
38
|
onCategoryChange,
|
|
40
39
|
isOpen = false,
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
onClose,
|
|
41
|
+
onCopyForAI,
|
|
43
42
|
}: SidebarProps) {
|
|
44
43
|
const isMobile = useIsMobile();
|
|
45
|
-
const [mounted, setMounted] = React.useState(false);
|
|
46
44
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// Desktop sidebar - always visible, no portal
|
|
45
|
+
// Desktop sidebar - uses react-sticky-box via Sticky component
|
|
46
|
+
// Sticks to viewport top (with navbar offset) but stops at parent container boundary (footer)
|
|
47
|
+
// Parent UILayout has isolate + zIndex:0 to prevent overlapping navbar
|
|
52
48
|
if (!isMobile) {
|
|
53
49
|
return (
|
|
54
|
-
<
|
|
50
|
+
<div
|
|
51
|
+
className="w-64 flex-shrink-0 self-stretch bg-background"
|
|
52
|
+
style={{ boxShadow: '-1px 0 0 0 hsl(var(--border))' }}
|
|
53
|
+
>
|
|
54
|
+
<Sticky
|
|
55
|
+
offsetTop={56}
|
|
56
|
+
offsetBottom={0}
|
|
57
|
+
disableOnMobile={false}
|
|
58
|
+
className="w-64"
|
|
59
|
+
>
|
|
60
|
+
<aside
|
|
61
|
+
className="overflow-y-auto w-64 bg-background"
|
|
62
|
+
style={{ maxHeight: 'calc(100vh - 56px)' }}
|
|
63
|
+
>
|
|
64
|
+
<SidebarContent
|
|
65
|
+
categories={categories}
|
|
66
|
+
currentCategory={currentCategory}
|
|
67
|
+
onCategoryChange={onCategoryChange}
|
|
68
|
+
isMobile={false}
|
|
69
|
+
onCopyForAI={onCopyForAI}
|
|
70
|
+
/>
|
|
71
|
+
</aside>
|
|
72
|
+
</Sticky>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Mobile sidebar - uses Drawer component (Vaul-based)
|
|
78
|
+
return (
|
|
79
|
+
<Drawer
|
|
80
|
+
open={isOpen}
|
|
81
|
+
onOpenChange={(open) => !open && onClose?.()}
|
|
82
|
+
direction="right"
|
|
83
|
+
>
|
|
84
|
+
<DrawerContent direction="right" className="w-64">
|
|
55
85
|
<SidebarContent
|
|
56
86
|
categories={categories}
|
|
57
87
|
currentCategory={currentCategory}
|
|
58
88
|
onCategoryChange={onCategoryChange}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
isMobile={false}
|
|
89
|
+
isMobile={true}
|
|
90
|
+
onCopyForAI={onCopyForAI}
|
|
62
91
|
/>
|
|
63
|
-
</
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Mobile sidebar - use portal when open
|
|
68
|
-
if (!isOpen || !mounted) {
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (typeof window === 'undefined') {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return createPortal(
|
|
77
|
-
<aside
|
|
78
|
-
className={cn(
|
|
79
|
-
'fixed inset-y-0 left-0 w-64 z-200 bg-background border-r shadow-lg transition-transform duration-300',
|
|
80
|
-
isOpen ? 'translate-x-0' : '-translate-x-full'
|
|
81
|
-
)}
|
|
82
|
-
>
|
|
83
|
-
<SidebarContent
|
|
84
|
-
categories={categories}
|
|
85
|
-
currentCategory={currentCategory}
|
|
86
|
-
onCategoryChange={onCategoryChange}
|
|
87
|
-
projectName={projectName}
|
|
88
|
-
logo={logo}
|
|
89
|
-
isMobile={true}
|
|
90
|
-
/>
|
|
91
|
-
</aside>,
|
|
92
|
-
document.body
|
|
92
|
+
</DrawerContent>
|
|
93
|
+
</Drawer>
|
|
93
94
|
);
|
|
94
95
|
}
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
'use client';
|
|
7
7
|
|
|
8
8
|
import React from 'react';
|
|
9
|
-
import { cn } from '@djangocfg/ui/lib';
|
|
10
9
|
import type { ComponentCategory } from '../../../types';
|
|
11
10
|
import { SidebarCategory } from './SidebarCategory';
|
|
12
|
-
import {
|
|
11
|
+
import { CopyAIButton } from '../Header/CopyAIButton';
|
|
12
|
+
import { TestValidationButton } from '../Header/TestValidationButton';
|
|
13
13
|
|
|
14
14
|
interface SidebarContentProps {
|
|
15
15
|
/** Available categories */
|
|
@@ -18,12 +18,10 @@ interface SidebarContentProps {
|
|
|
18
18
|
currentCategory?: string;
|
|
19
19
|
/** Category change callback */
|
|
20
20
|
onCategoryChange?: (categoryId: string) => void;
|
|
21
|
-
/** Project name */
|
|
22
|
-
projectName?: string;
|
|
23
|
-
/** Logo component */
|
|
24
|
-
logo?: React.ReactNode;
|
|
25
21
|
/** Is mobile view */
|
|
26
22
|
isMobile: boolean;
|
|
23
|
+
/** Function to generate AI context */
|
|
24
|
+
onCopyForAI?: () => string;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
/**
|
|
@@ -34,22 +32,27 @@ export function SidebarContent({
|
|
|
34
32
|
categories,
|
|
35
33
|
currentCategory,
|
|
36
34
|
onCategoryChange,
|
|
37
|
-
projectName,
|
|
38
|
-
logo,
|
|
39
35
|
isMobile,
|
|
36
|
+
onCopyForAI,
|
|
40
37
|
}: SidebarContentProps) {
|
|
41
38
|
return (
|
|
42
|
-
<div
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
isMobile ? 'h-full' : 'h-screen'
|
|
46
|
-
)}
|
|
47
|
-
>
|
|
48
|
-
{/* Logo - Desktop only */}
|
|
39
|
+
<div className="flex flex-col h-full overflow-hidden">
|
|
40
|
+
|
|
41
|
+
{/* Developer Tools */}
|
|
49
42
|
{!isMobile && (
|
|
50
|
-
<div className="
|
|
51
|
-
{
|
|
52
|
-
|
|
43
|
+
<div className="border-b flex-shrink-0 px-4 py-3 flex gap-2">
|
|
44
|
+
{onCopyForAI && (
|
|
45
|
+
<CopyAIButton
|
|
46
|
+
onCopyForAI={onCopyForAI}
|
|
47
|
+
size="sm"
|
|
48
|
+
showLabel
|
|
49
|
+
className="flex-1"
|
|
50
|
+
/>
|
|
51
|
+
)}
|
|
52
|
+
<TestValidationButton
|
|
53
|
+
size="sm"
|
|
54
|
+
showLabel={false}
|
|
55
|
+
/>
|
|
53
56
|
</div>
|
|
54
57
|
)}
|
|
55
58
|
|
|
@@ -80,7 +83,11 @@ export function SidebarContent({
|
|
|
80
83
|
</div>
|
|
81
84
|
|
|
82
85
|
{/* Footer */}
|
|
83
|
-
<
|
|
86
|
+
<div className="border-t p-4 bg-muted/30">
|
|
87
|
+
<p className="text-xs text-muted-foreground">
|
|
88
|
+
Built with Tailwind CSS v4
|
|
89
|
+
</p>
|
|
90
|
+
</div>
|
|
84
91
|
</div>
|
|
85
92
|
);
|
|
86
93
|
}
|
|
@@ -1,49 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SidebarFooter Component
|
|
3
|
-
* Footer section with
|
|
3
|
+
* Footer section with developer tools (Copy for AI, Test Validation)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
'use client';
|
|
7
7
|
|
|
8
8
|
import React from 'react';
|
|
9
|
+
import { CopyAIButton } from '../Header/CopyAIButton';
|
|
10
|
+
import { TestValidationButton } from '../Header/TestValidationButton';
|
|
11
|
+
|
|
12
|
+
export interface SidebarFooterProps {
|
|
13
|
+
/** Function to generate AI context */
|
|
14
|
+
onCopyForAI?: () => string;
|
|
15
|
+
}
|
|
9
16
|
|
|
10
17
|
/**
|
|
11
18
|
* Sidebar Footer
|
|
12
|
-
*
|
|
19
|
+
* Developer tools: Copy for AI and Test Validation buttons
|
|
13
20
|
*/
|
|
14
|
-
export function SidebarFooter() {
|
|
21
|
+
export function SidebarFooter({ onCopyForAI }: SidebarFooterProps) {
|
|
15
22
|
return (
|
|
16
|
-
<div className="border-t p-4 bg-muted/30">
|
|
17
|
-
{/*
|
|
18
|
-
<div className="
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
<div className="border-t p-4 bg-muted/30 space-y-3">
|
|
24
|
+
{/* Developer Tools */}
|
|
25
|
+
<div className="flex flex-col gap-2">
|
|
26
|
+
{onCopyForAI && (
|
|
27
|
+
<CopyAIButton
|
|
28
|
+
onCopyForAI={onCopyForAI}
|
|
29
|
+
size="sm"
|
|
30
|
+
showLabel
|
|
31
|
+
className="w-full justify-start"
|
|
32
|
+
/>
|
|
33
|
+
)}
|
|
34
|
+
<TestValidationButton
|
|
35
|
+
size="sm"
|
|
36
|
+
showLabel
|
|
37
|
+
className="w-full justify-start"
|
|
38
|
+
/>
|
|
25
39
|
</div>
|
|
26
40
|
|
|
27
|
-
{/*
|
|
28
|
-
<div className="
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
<FeatureItem text="Responsive: px-4 sm:px-6 lg:px-8" />
|
|
41
|
+
{/* Tailwind v4 info */}
|
|
42
|
+
<div className="pt-2 border-t border-border/50">
|
|
43
|
+
<p className="text-xs text-muted-foreground">
|
|
44
|
+
Built with Tailwind CSS v4
|
|
45
|
+
</p>
|
|
33
46
|
</div>
|
|
34
47
|
</div>
|
|
35
48
|
);
|
|
36
49
|
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Feature Item
|
|
40
|
-
* Single feature in the footer list
|
|
41
|
-
*/
|
|
42
|
-
function FeatureItem({ text }: { text: string }) {
|
|
43
|
-
return (
|
|
44
|
-
<div className="flex items-start gap-2">
|
|
45
|
-
<span className="text-primary mt-0.5">✓</span>
|
|
46
|
-
<span className="text-muted-foreground">{text}</span>
|
|
47
|
-
</div>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
@@ -113,16 +113,34 @@ export const LAYOUT_COMPONENTS: ComponentConfig[] = [
|
|
|
113
113
|
{
|
|
114
114
|
name: 'Sticky',
|
|
115
115
|
category: 'layout',
|
|
116
|
-
description: 'Make content sticky on scroll',
|
|
116
|
+
description: 'Make content sticky on scroll with configurable behavior',
|
|
117
117
|
importPath: "import { Sticky } from '@djangocfg/ui';",
|
|
118
|
-
example:
|
|
118
|
+
example: `// Top sticky with react-sticky-box (respects parent container)
|
|
119
|
+
<Sticky offsetTop={56} offsetBottom={0} zIndex={10}>
|
|
119
120
|
<nav className="bg-background border p-4">
|
|
120
121
|
Sticky Navigation
|
|
121
122
|
</nav>
|
|
122
|
-
</Sticky
|
|
123
|
+
</Sticky>
|
|
124
|
+
|
|
125
|
+
// Bottom sticky with native CSS sticky
|
|
126
|
+
<Sticky bottom useNativeSticky offsetBottom={0} zIndex={30}>
|
|
127
|
+
<footer className="bg-background border-t p-4">
|
|
128
|
+
Sticky Footer
|
|
129
|
+
</footer>
|
|
130
|
+
</Sticky>
|
|
131
|
+
|
|
132
|
+
// Props:
|
|
133
|
+
// - offsetTop: number (default: 0) - top offset in pixels
|
|
134
|
+
// - offsetBottom: number (default: 0) - bottom offset in pixels
|
|
135
|
+
// - bottom: boolean (default: false) - stick to bottom instead of top
|
|
136
|
+
// - disabled: boolean (default: false) - disable sticking
|
|
137
|
+
// - disableOnMobile: boolean (default: true) - disable on mobile
|
|
138
|
+
// - useNativeSticky: boolean (default: false) - use CSS sticky instead of react-sticky-box
|
|
139
|
+
// - zIndex: number (default: 10) - z-index value
|
|
140
|
+
// - debug: boolean (default: false) - enable debug logging`,
|
|
123
141
|
preview: (
|
|
124
142
|
<div className="h-[300px] overflow-auto border rounded-md relative">
|
|
125
|
-
<Sticky offsetTop={0} disableOnMobile={false}>
|
|
143
|
+
<Sticky offsetTop={0} disableOnMobile={false} zIndex={10}>
|
|
126
144
|
<div className="bg-primary text-primary-foreground p-3 text-center font-semibold shadow-md">
|
|
127
145
|
Sticky Header (scroll to see effect)
|
|
128
146
|
</div>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Showcase Context
|
|
3
|
-
* Manages navigation state for UILayout
|
|
3
|
+
* Manages navigation state for UILayout with URL hash sync
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
'use client';
|
|
7
7
|
|
|
8
|
-
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
|
8
|
+
import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
|
|
9
9
|
|
|
10
10
|
interface ShowcaseContextValue {
|
|
11
11
|
currentCategory: string;
|
|
@@ -20,7 +20,45 @@ interface ShowcaseProviderProps {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function ShowcaseProvider({ children, defaultCategory = 'overview' }: ShowcaseProviderProps) {
|
|
23
|
-
|
|
23
|
+
// Initialize from URL hash or default
|
|
24
|
+
const [currentCategory, setCurrentCategoryState] = useState(() => {
|
|
25
|
+
if (typeof window !== 'undefined') {
|
|
26
|
+
const hash = window.location.hash.slice(1); // Remove #
|
|
27
|
+
return hash || defaultCategory;
|
|
28
|
+
}
|
|
29
|
+
return defaultCategory;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Update URL hash when category changes
|
|
33
|
+
const setCurrentCategory = useCallback((category: string) => {
|
|
34
|
+
setCurrentCategoryState(category);
|
|
35
|
+
// Update URL hash without triggering navigation
|
|
36
|
+
if (typeof window !== 'undefined') {
|
|
37
|
+
const newUrl = category === defaultCategory
|
|
38
|
+
? window.location.pathname
|
|
39
|
+
: `${window.location.pathname}#${category}`;
|
|
40
|
+
window.history.replaceState(null, '', newUrl);
|
|
41
|
+
}
|
|
42
|
+
}, [defaultCategory]);
|
|
43
|
+
|
|
44
|
+
// Listen for hash changes (back/forward browser buttons)
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const handleHashChange = () => {
|
|
47
|
+
const hash = window.location.hash.slice(1);
|
|
48
|
+
setCurrentCategoryState(hash || defaultCategory);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
window.addEventListener('hashchange', handleHashChange);
|
|
52
|
+
return () => window.removeEventListener('hashchange', handleHashChange);
|
|
53
|
+
}, [defaultCategory]);
|
|
54
|
+
|
|
55
|
+
// Handle initial hash on mount (for SSR hydration)
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const hash = window.location.hash.slice(1);
|
|
58
|
+
if (hash && hash !== currentCategory) {
|
|
59
|
+
setCurrentCategoryState(hash);
|
|
60
|
+
}
|
|
61
|
+
}, []);
|
|
24
62
|
|
|
25
63
|
return (
|
|
26
64
|
<ShowcaseContext.Provider
|
|
@@ -1,30 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* UILayout
|
|
3
|
-
*
|
|
3
|
+
* Config-driven layout with left sidebar navigation
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Desktop: Fixed left sidebar with category navigation
|
|
7
|
+
* - Mobile: Collapsible sidebar via burger menu
|
|
8
|
+
* - No header - designed to be used inside app layout with global navbar
|
|
9
|
+
* - Type-safe: Full TypeScript support
|
|
4
10
|
*/
|
|
5
11
|
|
|
6
12
|
'use client';
|
|
7
13
|
|
|
8
|
-
import React from 'react';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
14
|
+
import React, { useState, useRef } from 'react';
|
|
15
|
+
import { Menu, X } from 'lucide-react';
|
|
16
|
+
import { Button, Sticky } from '@djangocfg/ui';
|
|
17
|
+
import { useIsMobile } from '@djangocfg/ui';
|
|
18
|
+
import { Sidebar } from '../components/layout/Sidebar';
|
|
11
19
|
import { generateAIContext } from '../config';
|
|
12
20
|
import type { UILayoutProps } from '../types';
|
|
13
21
|
|
|
14
22
|
|
|
15
23
|
/**
|
|
16
|
-
* UILayout -
|
|
17
|
-
*
|
|
18
|
-
* Features:
|
|
19
|
-
* - Config-driven: All component data comes from centralized config
|
|
20
|
-
* - "Copy for AI": One-click export of all documentation
|
|
21
|
-
* - Responsive: Auto-converts to mobile sheet menu on mobile
|
|
22
|
-
* - Type-safe: Full TypeScript support
|
|
24
|
+
* UILayout - Layout component with left sidebar navigation
|
|
25
|
+
* Designed to be used inside app layout (no competing header)
|
|
23
26
|
*
|
|
24
27
|
* @example
|
|
25
28
|
* ```tsx
|
|
26
29
|
* <UILayout
|
|
27
|
-
* title="UI Components"
|
|
28
30
|
* categories={CATEGORIES}
|
|
29
31
|
* currentCategory={category}
|
|
30
32
|
* onCategoryChange={setCategory}
|
|
@@ -35,68 +37,89 @@ import type { UILayoutProps } from '../types';
|
|
|
35
37
|
*/
|
|
36
38
|
export function UILayout({
|
|
37
39
|
children,
|
|
38
|
-
title = "UI Component Library",
|
|
39
40
|
description,
|
|
40
41
|
categories,
|
|
41
42
|
currentCategory,
|
|
42
43
|
onCategoryChange,
|
|
43
|
-
logo,
|
|
44
|
-
projectName = "Django CFG UI",
|
|
45
44
|
}: UILayoutProps) {
|
|
46
|
-
const
|
|
47
|
-
|
|
45
|
+
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
|
46
|
+
const isMobile = useIsMobile();
|
|
47
|
+
const mainRef = useRef<HTMLElement>(null);
|
|
48
|
+
|
|
49
|
+
const toggleSidebar = () => {
|
|
50
|
+
setIsSidebarOpen(!isSidebarOpen);
|
|
48
51
|
};
|
|
49
52
|
|
|
53
|
+
const handleCategoryChange = (categoryId: string) => {
|
|
54
|
+
onCategoryChange?.(categoryId);
|
|
55
|
+
// Scroll to top when category changes
|
|
56
|
+
// Try main element first, then window
|
|
57
|
+
if (mainRef.current) {
|
|
58
|
+
mainRef.current.scrollTo({ top: 0, behavior: 'smooth' });
|
|
59
|
+
}
|
|
60
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
61
|
+
// Auto-close sidebar on mobile after selection
|
|
62
|
+
if (isMobile) {
|
|
63
|
+
setIsSidebarOpen(false);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Get current category name for mobile header
|
|
68
|
+
const currentCategoryName = categories.find(c => c.id === currentCategory)?.label || 'Components';
|
|
69
|
+
|
|
50
70
|
return (
|
|
51
|
-
<div className="flex flex-col
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
<div className="flex flex-col flex-1 bg-background isolate" style={{ zIndex: 0 }}>
|
|
72
|
+
<div className="flex flex-1 relative">
|
|
73
|
+
{/* Main Content */}
|
|
74
|
+
<main ref={mainRef} className="flex-1 overflow-y-auto">
|
|
75
|
+
<div className="max-w-7xl mx-auto p-6">
|
|
76
|
+
{description && (
|
|
77
|
+
<p className="text-sm text-muted-foreground mb-6">
|
|
78
|
+
{description}
|
|
79
|
+
</p>
|
|
80
|
+
)}
|
|
81
|
+
{children}
|
|
82
|
+
</div>
|
|
83
|
+
</main>
|
|
84
|
+
|
|
85
|
+
{/* Sidebar - now on the right */}
|
|
86
|
+
<Sidebar
|
|
87
|
+
categories={categories}
|
|
88
|
+
currentCategory={currentCategory}
|
|
89
|
+
onCategoryChange={handleCategoryChange}
|
|
90
|
+
isOpen={isSidebarOpen}
|
|
91
|
+
onClose={() => setIsSidebarOpen(false)}
|
|
92
|
+
onCopyForAI={generateAIContext}
|
|
93
|
+
/>
|
|
59
94
|
|
|
60
|
-
{/* Category Navigation Tabs */}
|
|
61
|
-
<div className="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-3">
|
|
62
|
-
<Tabs
|
|
63
|
-
value={currentCategory}
|
|
64
|
-
onValueChange={onCategoryChange}
|
|
65
|
-
mobileSheet
|
|
66
|
-
mobileTitleText="UI Library"
|
|
67
|
-
mobileSheetTitle="Categories"
|
|
68
|
-
sticky
|
|
69
|
-
>
|
|
70
|
-
<TabsList fullWidth className="h-auto flex-wrap">
|
|
71
|
-
{categories.map((category) => (
|
|
72
|
-
<TabsTrigger
|
|
73
|
-
key={category.id}
|
|
74
|
-
value={category.id}
|
|
75
|
-
flexEqual
|
|
76
|
-
className="gap-2 whitespace-nowrap"
|
|
77
|
-
>
|
|
78
|
-
{category.icon}
|
|
79
|
-
<span>{category.label}</span>
|
|
80
|
-
{category.count !== undefined && (
|
|
81
|
-
<span className="ml-1 text-xs opacity-60">({category.count})</span>
|
|
82
|
-
)}
|
|
83
|
-
</TabsTrigger>
|
|
84
|
-
))}
|
|
85
|
-
</TabsList>
|
|
86
|
-
</Tabs>
|
|
87
95
|
</div>
|
|
88
96
|
|
|
89
|
-
{/*
|
|
90
|
-
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
{/* Mobile Bottom Bar - sticky at bottom to avoid conflict with PublicLayout navbar */}
|
|
98
|
+
{isMobile && (
|
|
99
|
+
<Sticky
|
|
100
|
+
bottom
|
|
101
|
+
useNativeSticky
|
|
102
|
+
offsetBottom={0}
|
|
103
|
+
disableOnMobile={false}
|
|
104
|
+
zIndex={30}
|
|
105
|
+
className="flex items-center justify-between gap-3 px-4 py-3 border-t bg-background"
|
|
106
|
+
>
|
|
107
|
+
<span className="font-medium text-sm truncate">{currentCategoryName}</span>
|
|
108
|
+
<Button
|
|
109
|
+
variant="outline"
|
|
110
|
+
size="icon"
|
|
111
|
+
onClick={toggleSidebar}
|
|
112
|
+
aria-label={isSidebarOpen ? "Close menu" : "Open menu"}
|
|
113
|
+
className="shrink-0"
|
|
114
|
+
>
|
|
115
|
+
{isSidebarOpen ? (
|
|
116
|
+
<X className="h-5 w-5" />
|
|
117
|
+
) : (
|
|
118
|
+
<Menu className="h-5 w-5" />
|
|
119
|
+
)}
|
|
120
|
+
</Button>
|
|
121
|
+
</Sticky>
|
|
122
|
+
)}
|
|
100
123
|
</div>
|
|
101
124
|
);
|
|
102
125
|
}
|
|
@@ -1,144 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* UILayoutSidebar
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Features:
|
|
6
|
-
* - Desktop: Fixed left sidebar with category navigation
|
|
7
|
-
* - Mobile: Collapsible sidebar via burger menu
|
|
8
|
-
* - Sticky header
|
|
9
|
-
* - Responsive content area
|
|
3
|
+
* @deprecated Use UILayout instead - it now has sidebar navigation by default
|
|
10
4
|
*/
|
|
11
5
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import React, { useState } from 'react';
|
|
15
|
-
import { Menu, X } from 'lucide-react';
|
|
16
|
-
import { Button } from '@djangocfg/ui';
|
|
17
|
-
import { useIsMobile } from '@djangocfg/ui';
|
|
18
|
-
import { Header } from '../components/layout/Header';
|
|
19
|
-
import { Sidebar } from '../components/layout/Sidebar';
|
|
20
|
-
import { MobileOverlay } from '../components/layout/MobileOverlay';
|
|
21
|
-
import { generateAIContext } from '../config';
|
|
22
|
-
import type { UILayoutProps } from '../types';
|
|
6
|
+
import { UILayout } from './UILayout';
|
|
23
7
|
|
|
24
8
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* Features:
|
|
28
|
-
* - Config-driven sidebar categories
|
|
29
|
-
* - "Copy for AI": One-click export of documentation
|
|
30
|
-
* - Responsive: Mobile burger menu + overlay
|
|
31
|
-
* - Type-safe: Full TypeScript support
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```tsx
|
|
35
|
-
* <UILayoutSidebar
|
|
36
|
-
* title="UI Components"
|
|
37
|
-
* categories={CATEGORIES}
|
|
38
|
-
* currentCategory={category}
|
|
39
|
-
* onCategoryChange={setCategory}
|
|
40
|
-
* >
|
|
41
|
-
* <CategoryRenderer categoryId={category} />
|
|
42
|
-
* </UILayoutSidebar>
|
|
43
|
-
* ```
|
|
9
|
+
* @deprecated Use UILayout instead
|
|
44
10
|
*/
|
|
45
|
-
export
|
|
46
|
-
children,
|
|
47
|
-
title = "UI Component Library",
|
|
48
|
-
description,
|
|
49
|
-
categories,
|
|
50
|
-
currentCategory,
|
|
51
|
-
onCategoryChange,
|
|
52
|
-
logo,
|
|
53
|
-
projectName = "Django CFG UI",
|
|
54
|
-
}: UILayoutProps) {
|
|
55
|
-
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
|
56
|
-
const isMobile = useIsMobile();
|
|
57
|
-
|
|
58
|
-
const handleCopyForAI = () => {
|
|
59
|
-
return generateAIContext();
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const toggleSidebar = () => {
|
|
63
|
-
setIsSidebarOpen(!isSidebarOpen);
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const handleCategoryChange = (categoryId: string) => {
|
|
67
|
-
onCategoryChange?.(categoryId);
|
|
68
|
-
// Auto-close sidebar on mobile after selection
|
|
69
|
-
if (isMobile) {
|
|
70
|
-
setIsSidebarOpen(false);
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
return (
|
|
75
|
-
<div className="flex flex-col min-h-screen bg-background">
|
|
76
|
-
{/* Header with mobile burger */}
|
|
77
|
-
<div className="sticky top-0 z-40 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
78
|
-
<div className="flex h-14 items-center gap-4 px-4">
|
|
79
|
-
{/* Mobile burger menu */}
|
|
80
|
-
{isMobile && (
|
|
81
|
-
<Button
|
|
82
|
-
variant="ghost"
|
|
83
|
-
size="icon"
|
|
84
|
-
onClick={toggleSidebar}
|
|
85
|
-
className="shrink-0"
|
|
86
|
-
aria-label={isSidebarOpen ? "Close menu" : "Open menu"}
|
|
87
|
-
>
|
|
88
|
-
{isSidebarOpen ? (
|
|
89
|
-
<X className="h-5 w-5" />
|
|
90
|
-
) : (
|
|
91
|
-
<Menu className="h-5 w-5" />
|
|
92
|
-
)}
|
|
93
|
-
</Button>
|
|
94
|
-
)}
|
|
95
|
-
|
|
96
|
-
{/* Header title and actions */}
|
|
97
|
-
<div className="flex-1 flex items-center justify-between">
|
|
98
|
-
<div className="flex items-center gap-2">
|
|
99
|
-
{!isMobile && logo}
|
|
100
|
-
<h1 className="text-lg font-semibold">{title}</h1>
|
|
101
|
-
</div>
|
|
102
|
-
<Header
|
|
103
|
-
title=""
|
|
104
|
-
projectName={projectName}
|
|
105
|
-
logo={null}
|
|
106
|
-
onCopyForAI={handleCopyForAI}
|
|
107
|
-
/>
|
|
108
|
-
</div>
|
|
109
|
-
</div>
|
|
110
|
-
</div>
|
|
111
|
-
|
|
112
|
-
{/* Main layout: Sidebar + Content */}
|
|
113
|
-
<div className="flex flex-1 overflow-hidden">
|
|
114
|
-
{/* Sidebar */}
|
|
115
|
-
<Sidebar
|
|
116
|
-
categories={categories}
|
|
117
|
-
currentCategory={currentCategory}
|
|
118
|
-
onCategoryChange={handleCategoryChange}
|
|
119
|
-
isOpen={isSidebarOpen}
|
|
120
|
-
projectName={projectName}
|
|
121
|
-
logo={logo}
|
|
122
|
-
/>
|
|
123
|
-
|
|
124
|
-
{/* Mobile overlay (backdrop) */}
|
|
125
|
-
<MobileOverlay
|
|
126
|
-
isOpen={isMobile && isSidebarOpen}
|
|
127
|
-
onClose={() => setIsSidebarOpen(false)}
|
|
128
|
-
/>
|
|
129
|
-
|
|
130
|
-
{/* Main Content */}
|
|
131
|
-
<main className="flex-1 overflow-y-auto">
|
|
132
|
-
<div className="container max-w-7xl mx-auto p-6">
|
|
133
|
-
{description && (
|
|
134
|
-
<p className="text-sm text-muted-foreground mb-6">
|
|
135
|
-
{description}
|
|
136
|
-
</p>
|
|
137
|
-
)}
|
|
138
|
-
{children}
|
|
139
|
-
</div>
|
|
140
|
-
</main>
|
|
141
|
-
</div>
|
|
142
|
-
</div>
|
|
143
|
-
);
|
|
144
|
-
}
|
|
11
|
+
export const UILayoutSidebar = UILayout;
|