@djangocfg/layouts 1.4.16 → 1.4.17

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "1.4.16",
3
+ "version": "1.4.17",
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.16",
67
- "@djangocfg/og-image": "^1.4.16",
68
- "@djangocfg/ui": "^1.4.16",
66
+ "@djangocfg/api": "^1.4.17",
67
+ "@djangocfg/og-image": "^1.4.17",
68
+ "@djangocfg/ui": "^1.4.17",
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.16",
89
+ "@djangocfg/typescript-config": "^1.4.17",
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
- * Refactored from _old/MainLayout - uses context only!
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
- * - Backdrop overlay
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
- if (!shouldRender) return null;
82
-
83
- // Portal to body to avoid z-index and positioning issues
84
- if (typeof window === 'undefined') return null;
85
-
86
- return createPortal(
87
- <>
88
- {/* Backdrop with fade animation */}
89
- <div
90
- className={`fixed inset-0 z-150 backdrop-blur-sm transition-opacity duration-300 ease-in-out lg:hidden ${
91
- isOpen ? 'opacity-100' : 'opacity-0'
92
- }`}
93
- style={{ backgroundColor: 'rgb(0 0 0 / 0.5)' }}
94
- onClick={handleClose}
95
- aria-hidden="true"
96
- />
97
-
98
- {/* Menu Content with slide animation */}
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
- {/* Scrollable Content */}
130
- <div className="flex-1 overflow-y-auto p-4 space-y-6">
131
- {/* User Menu Card */}
132
- <UserMenu variant="mobile" onNavigate={handleNavigate} />
133
-
134
- {/* Navigation Sections */}
135
- <div className="space-y-6">
136
- {/* Group all single-item sections into "Menu" */}
137
- {singleItemSections.length > 0 && (
138
- <div className="space-y-3">
139
- <h3 className="px-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
140
- Menu
141
- </h3>
142
- <div className="space-y-1">
143
- {singleItemSections.map((section) => {
144
- const item = section.items[0];
145
- if (!item) return null;
146
-
147
- return (
148
- <Link
149
- key={item.path}
150
- href={item.path}
151
- className={`block px-4 py-3 rounded-sm text-base font-medium transition-colors ${
152
- isActive(item.path)
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
- </div>
112
+ );
113
+ })}
188
114
  </div>
189
- ))}
190
- </div>
191
-
192
- {/* Bottom spacer */}
193
- <div style={{ height: '15vh' }}></div>
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
- </div>
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 border-b backdrop-blur-xl z-100 isolate" style={{ backgroundColor: 'hsl(var(--background) / 0.8)', borderColor: 'hsl(var(--border) / 0.3)' }}>
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
- * Adaptive: desktop (static) or mobile (portal)
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 { createPortal } from 'react-dom';
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
- /** Project name */
26
- projectName?: string;
27
- /** Logo component */
28
- logo?: React.ReactNode;
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: Portal-based sidebar with slide animation
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
- projectName = 'Django CFG',
42
- logo,
40
+ onClose,
41
+ onCopyForAI,
43
42
  }: SidebarProps) {
44
43
  const isMobile = useIsMobile();
45
- const [mounted, setMounted] = React.useState(false);
46
44
 
47
- React.useEffect(() => {
48
- setMounted(true);
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
- <aside className="w-64 border-r bg-background flex-shrink-0">
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
- projectName={projectName}
60
- logo={logo}
61
- isMobile={false}
89
+ isMobile={true}
90
+ onCopyForAI={onCopyForAI}
62
91
  />
63
- </aside>
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 { SidebarFooter } from './SidebarFooter';
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
- className={cn(
44
- 'flex flex-col overflow-hidden',
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="flex h-14 items-center border-b px-6 gap-2 flex-shrink-0">
51
- {logo}
52
- <span className="font-semibold text-sm">{projectName}</span>
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
- <SidebarFooter />
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 Tailwind CSS v4 information
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
- * Displays Tailwind CSS v4 features and benefits
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
- {/* Header */}
18
- <div className="mb-3">
19
- <h4 className="text-xs font-semibold text-foreground uppercase tracking-wider mb-2">
20
- Tailwind CSS v4
21
- </h4>
22
- <p className="text-xs text-muted-foreground leading-relaxed mb-3">
23
- This UI library uses Tailwind CSS v4 with CSS-first configuration
24
- </p>
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
- {/* Features List */}
28
- <div className="space-y-2 text-xs">
29
- <FeatureItem text="CSS-first @theme configuration" />
30
- <FeatureItem text="10x faster build times" />
31
- <FeatureItem text="Modern CSS features (color-mix, @property)" />
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: `<Sticky offsetTop={0} disableOnMobile={false}>
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
- const [currentCategory, setCurrentCategory] = useState(defaultCategory);
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
- * Modern, config-driven layout for UI component documentation
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 { Tabs, TabsList, TabsTrigger } from '@djangocfg/ui';
10
- import { Header } from '../components/layout/Header';
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 - Main layout component for UI documentation
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 handleCopyForAI = () => {
47
- return generateAIContext();
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 min-h-screen bg-background">
52
- {/* Header with Copy for AI button */}
53
- <Header
54
- title={title}
55
- projectName={projectName}
56
- logo={logo}
57
- onCopyForAI={handleCopyForAI}
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
- {/* Main Content */}
90
- <main className="flex-1">
91
- <div className="container max-w-7xl mx-auto p-6">
92
- {description && (
93
- <p className="text-sm text-muted-foreground mb-6">
94
- {description}
95
- </p>
96
- )}
97
- {children}
98
- </div>
99
- </main>
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
- * Modern layout with left sidebar navigation (instead of tabs)
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
- 'use client';
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
- * UILayoutSidebar - Layout with left sidebar navigation
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 function UILayoutSidebar({
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;