responsive-system 1.1.5 → 1.2.1
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 +2 -1
- package/scripts/postinstall.js +2 -3
- package/src/App.css +42 -0
- package/src/App.tsx +29 -0
- package/src/assets/react.svg +1 -0
- package/src/components/LayoutSwitcher.tsx +62 -0
- package/src/components/ResponsiveDemo.tsx +282 -0
- package/src/components/layout/Footer.tsx +90 -0
- package/src/components/layout/Header.tsx +105 -0
- package/src/components/layout/Navigation.tsx +96 -0
- package/src/components/layout/Sidebar.tsx +108 -0
- package/src/components/layout/index.ts +4 -0
- package/src/config/layout.ts +61 -0
- package/src/constants/breakpoints.ts +48 -0
- package/src/context/NavigationContext.tsx +32 -0
- package/src/context/ResponsiveLayoutContext.tsx +37 -0
- package/src/context/SidebarContext.tsx +26 -0
- package/src/context/index.ts +4 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useLayout.ts +27 -0
- package/src/hooks/useResponsive.ts +189 -0
- package/src/hooks/useResponsiveLayout.ts +51 -0
- package/src/index.css +1 -0
- package/src/index.ts +100 -0
- package/src/layouts/DashboardLayout.tsx +76 -0
- package/src/layouts/DefaultLayout.tsx +30 -0
- package/src/layouts/MainLayout.tsx +38 -0
- package/src/layouts/MinimalLayout.tsx +20 -0
- package/src/layouts/SidebarLayout.tsx +36 -0
- package/src/layouts/index.ts +5 -0
- package/src/main.tsx +10 -0
- package/src/pages/ResponsiveTestPage.tsx +400 -0
- package/src/providers/ResponsiveLayoutProvider.tsx +92 -0
- package/src/providers/ResponsiveProvider.tsx +18 -0
- package/src/providers/index.ts +3 -0
- package/src/types/responsive.ts +64 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useResponsiveLayout } from '../../hooks'
|
|
3
|
+
import { useNavigation } from '../../context'
|
|
4
|
+
|
|
5
|
+
const Navigation = () => {
|
|
6
|
+
const { isMobile, breakpoint } = useResponsiveLayout()
|
|
7
|
+
const { currentPage, setCurrentPage } = useNavigation()
|
|
8
|
+
const [sidebarOpen, setSidebarOpen] = useState(false)
|
|
9
|
+
|
|
10
|
+
const pages = [
|
|
11
|
+
{ id: 'demo', name: 'Demo' },
|
|
12
|
+
{ id: 'test', name: 'Suite de Test' }
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="sticky top-0 z-50">
|
|
17
|
+
<nav className="bg-gradient-to-r from-gray-900 via-black to-gray-900 border-b border-cyan-500/20 shadow-2xl relative">
|
|
18
|
+
<div className="w-full">
|
|
19
|
+
<div className="px-4 py-4">
|
|
20
|
+
<div className="flex items-center justify-between">
|
|
21
|
+
<div className="flex items-center space-x-2">
|
|
22
|
+
<div className="flex items-center space-x-2">
|
|
23
|
+
<div className="w-1.5 h-1.5 bg-cyan-400 rounded-full shadow-lg shadow-cyan-400/50 animate-pulse"></div>
|
|
24
|
+
<h3 className="text-base font-black text-white tracking-tight">
|
|
25
|
+
Sistema Responsivo
|
|
26
|
+
</h3>
|
|
27
|
+
</div>
|
|
28
|
+
<div className="px-2 py-0.5 text-cyan-400 font-mono bg-black/50 border border-cyan-500/30 rounded text-xs font-bold tracking-widest">
|
|
29
|
+
{breakpoint.toUpperCase()}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div className="flex items-center space-x-2">
|
|
34
|
+
{/* Botones visibles solo en desktop */}
|
|
35
|
+
{!isMobile && pages.map(page => (
|
|
36
|
+
<button
|
|
37
|
+
key={page.id}
|
|
38
|
+
onClick={() => setCurrentPage(page.id as 'demo' | 'test')}
|
|
39
|
+
className={`px-3 py-1.5 rounded-lg transition-all font-bold text-xs tracking-wide border ${
|
|
40
|
+
currentPage === page.id
|
|
41
|
+
? 'bg-cyan-500/20 text-cyan-400 border-cyan-500/50'
|
|
42
|
+
: 'bg-black/50 text-gray-400 hover:text-gray-300 border-gray-700 hover:border-gray-600'
|
|
43
|
+
}`}
|
|
44
|
+
>
|
|
45
|
+
{page.name}
|
|
46
|
+
</button>
|
|
47
|
+
))}
|
|
48
|
+
|
|
49
|
+
{/* Menú hamburguesa para móvil - A LA DERECHA */}
|
|
50
|
+
{isMobile && (
|
|
51
|
+
<button
|
|
52
|
+
onClick={() => setSidebarOpen(!sidebarOpen)}
|
|
53
|
+
className="p-2 rounded-lg text-gray-300 hover:text-cyan-400 hover:bg-cyan-500/10 transition-colors"
|
|
54
|
+
>
|
|
55
|
+
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
56
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
57
|
+
</svg>
|
|
58
|
+
</button>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{/* Menú móvil desplegable - TRADICIONAL */}
|
|
66
|
+
{isMobile && sidebarOpen && (
|
|
67
|
+
<div className="absolute top-full left-0 right-0 bg-gradient-to-r from-gray-900 via-black to-gray-900 border-b border-cyan-500/20 shadow-2xl z-50">
|
|
68
|
+
<div className="p-4">
|
|
69
|
+
{/* Navigation - VERTICAL como menú tradicional */}
|
|
70
|
+
<nav className="space-y-2">
|
|
71
|
+
{pages.map((page) => (
|
|
72
|
+
<button
|
|
73
|
+
key={page.id}
|
|
74
|
+
onClick={() => {
|
|
75
|
+
setCurrentPage(page.id as 'demo' | 'test')
|
|
76
|
+
setSidebarOpen(false)
|
|
77
|
+
}}
|
|
78
|
+
className={`w-full text-left px-4 py-3 rounded-lg transition-all font-bold text-sm tracking-wide border ${
|
|
79
|
+
currentPage === page.id
|
|
80
|
+
? 'bg-cyan-500/20 text-cyan-400 border-cyan-500/50'
|
|
81
|
+
: 'bg-black/50 text-gray-400 hover:text-gray-300 border-gray-700 hover:border-gray-600'
|
|
82
|
+
}`}
|
|
83
|
+
>
|
|
84
|
+
{page.name}
|
|
85
|
+
</button>
|
|
86
|
+
))}
|
|
87
|
+
</nav>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</nav>
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default Navigation
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useResponsiveLayout } from '../../hooks'
|
|
2
|
+
import { useNavigation, useSidebar } from '../../context'
|
|
3
|
+
|
|
4
|
+
const Sidebar = () => {
|
|
5
|
+
const { isMobile, isTablet, breakpoint } = useResponsiveLayout()
|
|
6
|
+
const { currentPage, setCurrentPage } = useNavigation()
|
|
7
|
+
const { sidebarOpen, setSidebarOpen } = useSidebar()
|
|
8
|
+
|
|
9
|
+
const menuItems = [
|
|
10
|
+
{ id: 'test', label: 'Suite de Test' },
|
|
11
|
+
{ id: 'demo', label: 'Demo' },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
{/* Hamburger button para móvil */}
|
|
17
|
+
{isMobile && (
|
|
18
|
+
<button
|
|
19
|
+
onClick={() => setSidebarOpen(true)}
|
|
20
|
+
className="fixed top-4 left-4 z-50 p-2 rounded-lg text-gray-300 hover:text-cyan-400 hover:bg-cyan-500/10 transition-colors bg-black/50 border border-gray-700"
|
|
21
|
+
>
|
|
22
|
+
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
23
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
24
|
+
</svg>
|
|
25
|
+
</button>
|
|
26
|
+
)}
|
|
27
|
+
|
|
28
|
+
{/* Sidebar desktop */}
|
|
29
|
+
<aside className={`
|
|
30
|
+
bg-gradient-to-b from-gray-900 to-black border-r border-cyan-500/20
|
|
31
|
+
${isMobile ? 'hidden' : 'w-64 flex-shrink-0'}
|
|
32
|
+
${isTablet ? 'w-56' : 'w-64'}
|
|
33
|
+
`}>
|
|
34
|
+
<div className="p-6 flex flex-col h-full">
|
|
35
|
+
{/* Logo y Breakpoint */}
|
|
36
|
+
<div className="flex items-center space-x-3 mb-8">
|
|
37
|
+
<div className="w-8 h-8 bg-cyan-500 rounded-lg flex items-center justify-center">
|
|
38
|
+
<span className="text-white font-bold text-sm">RS</span>
|
|
39
|
+
</div>
|
|
40
|
+
<div>
|
|
41
|
+
<span className="text-white font-bold text-lg">Sistema Responsivo</span>
|
|
42
|
+
<div className="px-2 py-0.5 text-cyan-400 font-mono bg-black/50 border border-cyan-500/30 rounded text-xs font-bold tracking-widest mt-1">
|
|
43
|
+
{breakpoint.toUpperCase()}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
{/* Navigation */}
|
|
49
|
+
<nav className="space-y-2">
|
|
50
|
+
{menuItems.map((item) => (
|
|
51
|
+
<button
|
|
52
|
+
key={item.id}
|
|
53
|
+
onClick={() => setCurrentPage(item.id as 'demo' | 'test')}
|
|
54
|
+
className={`w-full flex items-center px-4 py-3 rounded-lg transition-all group text-left ${
|
|
55
|
+
currentPage === item.id
|
|
56
|
+
? 'bg-cyan-500/20 text-cyan-400 border border-cyan-500/50'
|
|
57
|
+
: 'text-gray-300 hover:text-cyan-400 hover:bg-cyan-500/10'
|
|
58
|
+
}`}
|
|
59
|
+
>
|
|
60
|
+
<span className="font-medium">{item.label}</span>
|
|
61
|
+
</button>
|
|
62
|
+
))}
|
|
63
|
+
</nav>
|
|
64
|
+
|
|
65
|
+
</div>
|
|
66
|
+
</aside>
|
|
67
|
+
|
|
68
|
+
{/* Sidebar móvil desplegable */}
|
|
69
|
+
{isMobile && sidebarOpen && (
|
|
70
|
+
<div className="fixed inset-0 z-40 bg-black/50" onClick={() => setSidebarOpen(false)}>
|
|
71
|
+
<div className="fixed top-0 left-0 w-64 h-full bg-gradient-to-b from-gray-900 to-black border-r border-cyan-500/20">
|
|
72
|
+
<div className="p-6 flex flex-col h-full pt-20">
|
|
73
|
+
{/* Logo */}
|
|
74
|
+
<div className="flex items-center space-x-3 mb-8">
|
|
75
|
+
<div className="w-8 h-8 bg-cyan-500 rounded-lg flex items-center justify-center">
|
|
76
|
+
<span className="text-white font-bold text-sm">RS</span>
|
|
77
|
+
</div>
|
|
78
|
+
<span className="text-white font-bold text-lg">Sistema Responsivo</span>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{/* Navigation */}
|
|
82
|
+
<nav className="space-y-2">
|
|
83
|
+
{menuItems.map((item) => (
|
|
84
|
+
<button
|
|
85
|
+
key={item.id}
|
|
86
|
+
onClick={() => {
|
|
87
|
+
setCurrentPage(item.id as 'demo' | 'test')
|
|
88
|
+
setSidebarOpen(false)
|
|
89
|
+
}}
|
|
90
|
+
className={`w-full flex items-center px-4 py-3 rounded-lg transition-all group text-left ${
|
|
91
|
+
currentPage === item.id
|
|
92
|
+
? 'bg-cyan-500/20 text-cyan-400 border border-cyan-500/50'
|
|
93
|
+
: 'text-gray-300 hover:text-cyan-400 hover:bg-cyan-500/10'
|
|
94
|
+
}`}
|
|
95
|
+
>
|
|
96
|
+
<span className="font-medium">{item.label}</span>
|
|
97
|
+
</button>
|
|
98
|
+
))}
|
|
99
|
+
</nav>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default Sidebar
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface LayoutConfig {
|
|
2
|
+
name: string
|
|
3
|
+
description: string
|
|
4
|
+
components: string[]
|
|
5
|
+
spacing: string
|
|
6
|
+
responsive: {
|
|
7
|
+
mobile: string
|
|
8
|
+
tablet: string
|
|
9
|
+
desktop: string
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const LAYOUT_CONFIG: Record<string, LayoutConfig> = {
|
|
14
|
+
default: {
|
|
15
|
+
name: 'Default',
|
|
16
|
+
description: 'Navbar arriba, body central, footer abajo',
|
|
17
|
+
components: ['Navigation', 'Footer'],
|
|
18
|
+
spacing: 'p-4 md:p-6 lg:p-8',
|
|
19
|
+
responsive: {
|
|
20
|
+
mobile: 'p-4',
|
|
21
|
+
tablet: 'p-6',
|
|
22
|
+
desktop: 'p-8'
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
sidebar: {
|
|
26
|
+
name: 'Sidebar',
|
|
27
|
+
description: 'Sidebar izquierda, contenido principal',
|
|
28
|
+
components: ['Sidebar'],
|
|
29
|
+
spacing: 'p-4 md:p-6',
|
|
30
|
+
responsive: {
|
|
31
|
+
mobile: 'p-4',
|
|
32
|
+
tablet: 'p-6',
|
|
33
|
+
desktop: 'p-6'
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
dashboard: {
|
|
37
|
+
name: 'Dashboard',
|
|
38
|
+
description: 'Header + Sidebar + Main + Footer',
|
|
39
|
+
components: ['Header', 'Sidebar', 'Footer'],
|
|
40
|
+
spacing: 'p-4 md:p-6 lg:p-8',
|
|
41
|
+
responsive: {
|
|
42
|
+
mobile: 'p-4',
|
|
43
|
+
tablet: 'p-6',
|
|
44
|
+
desktop: 'p-8'
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
minimal: {
|
|
48
|
+
name: 'Minimal',
|
|
49
|
+
description: 'Solo contenido, sin navegación',
|
|
50
|
+
components: [],
|
|
51
|
+
spacing: 'p-4 md:p-6',
|
|
52
|
+
responsive: {
|
|
53
|
+
mobile: 'p-4',
|
|
54
|
+
tablet: 'p-6',
|
|
55
|
+
desktop: 'p-6'
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const DEFAULT_LAYOUT = 'default'
|
|
61
|
+
export const AVAILABLE_LAYOUTS = Object.keys(LAYOUT_CONFIG)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Breakpoint } from '../types/responsive'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default breakpoint values
|
|
5
|
+
* Deben coincidir con tailwind.config.js
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_BREAKPOINTS: Record<Breakpoint, number> = {
|
|
8
|
+
xs: 475,
|
|
9
|
+
sm: 640,
|
|
10
|
+
md: 768,
|
|
11
|
+
lg: 1024,
|
|
12
|
+
xl: 1280,
|
|
13
|
+
'2xl': 1536,
|
|
14
|
+
'3xl': 1920,
|
|
15
|
+
'4xl': 2560,
|
|
16
|
+
'5xl': 3840
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get current breakpoint based on window width
|
|
21
|
+
*/
|
|
22
|
+
export const getCurrentBreakpoint = (width: number): Breakpoint => {
|
|
23
|
+
if (width >= DEFAULT_BREAKPOINTS['5xl']) return '5xl'
|
|
24
|
+
if (width >= DEFAULT_BREAKPOINTS['4xl']) return '4xl'
|
|
25
|
+
if (width >= DEFAULT_BREAKPOINTS['3xl']) return '3xl'
|
|
26
|
+
if (width >= DEFAULT_BREAKPOINTS['2xl']) return '2xl'
|
|
27
|
+
if (width >= DEFAULT_BREAKPOINTS.xl) return 'xl'
|
|
28
|
+
if (width >= DEFAULT_BREAKPOINTS.lg) return 'lg'
|
|
29
|
+
if (width >= DEFAULT_BREAKPOINTS.md) return 'md'
|
|
30
|
+
if (width >= DEFAULT_BREAKPOINTS.sm) return 'sm'
|
|
31
|
+
return 'xs'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get breakpoint index (for comparisons)
|
|
36
|
+
*/
|
|
37
|
+
export const getBreakpointIndex = (breakpoint: Breakpoint): number => {
|
|
38
|
+
const breakpoints: Breakpoint[] = ['xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl']
|
|
39
|
+
return breakpoints.indexOf(breakpoint)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get breakpoint value in pixels
|
|
44
|
+
*/
|
|
45
|
+
export const getBreakpointValue = (breakpoint: Breakpoint): number => {
|
|
46
|
+
return DEFAULT_BREAKPOINTS[breakpoint]
|
|
47
|
+
}
|
|
48
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
interface NavigationContextType {
|
|
4
|
+
currentPage: 'demo' | 'test'
|
|
5
|
+
setCurrentPage: (page: 'demo' | 'test') => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const NavigationContext = createContext<NavigationContextType>({
|
|
9
|
+
currentPage: 'test',
|
|
10
|
+
setCurrentPage: () => {}
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export const useNavigation = () => useContext(NavigationContext)
|
|
14
|
+
|
|
15
|
+
interface NavigationProviderProps {
|
|
16
|
+
children: React.ReactNode
|
|
17
|
+
defaultPage?: 'demo' | 'test'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const NavigationProvider: React.FC<NavigationProviderProps> = ({
|
|
21
|
+
children,
|
|
22
|
+
defaultPage = 'test'
|
|
23
|
+
}) => {
|
|
24
|
+
const [currentPage, setCurrentPage] = useState<'demo' | 'test'>(defaultPage)
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<NavigationContext.Provider value={{ currentPage, setCurrentPage }}>
|
|
28
|
+
{children}
|
|
29
|
+
</NavigationContext.Provider>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
2
|
+
import type { ResponsiveState } from '../types/responsive'
|
|
3
|
+
import type { LayoutConfig } from '../config/layout'
|
|
4
|
+
|
|
5
|
+
export interface ResponsiveLayoutState {
|
|
6
|
+
// Estado del sistema responsivo
|
|
7
|
+
responsive: ResponsiveState
|
|
8
|
+
|
|
9
|
+
// Estado del layout
|
|
10
|
+
layout: {
|
|
11
|
+
current: string
|
|
12
|
+
config: LayoutConfig
|
|
13
|
+
setLayout: (layout: string) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Utilidades de layout
|
|
17
|
+
layoutUtils: {
|
|
18
|
+
getContainerClass: () => string
|
|
19
|
+
getMainClass: () => string
|
|
20
|
+
hasSidebar: () => boolean
|
|
21
|
+
hasHeader: () => boolean
|
|
22
|
+
hasFooter: () => boolean
|
|
23
|
+
hasNavigation: () => boolean
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ResponsiveLayoutContext = createContext<ResponsiveLayoutState | undefined>(undefined)
|
|
28
|
+
|
|
29
|
+
export const useResponsiveLayoutContext = () => {
|
|
30
|
+
const context = useContext(ResponsiveLayoutContext)
|
|
31
|
+
if (!context) {
|
|
32
|
+
throw new Error('useResponsiveLayoutContext must be used within a ResponsiveLayoutProvider')
|
|
33
|
+
}
|
|
34
|
+
return context
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { ResponsiveLayoutContext }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
interface SidebarContextType {
|
|
4
|
+
sidebarOpen: boolean
|
|
5
|
+
setSidebarOpen: (open: boolean) => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const SidebarContext = createContext<SidebarContextType | undefined>(undefined)
|
|
9
|
+
|
|
10
|
+
export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
11
|
+
const [sidebarOpen, setSidebarOpen] = useState(false)
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<SidebarContext.Provider value={{ sidebarOpen, setSidebarOpen }}>
|
|
15
|
+
{children}
|
|
16
|
+
</SidebarContext.Provider>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const useSidebar = () => {
|
|
21
|
+
const context = useContext(SidebarContext)
|
|
22
|
+
if (!context) {
|
|
23
|
+
throw new Error('useSidebar must be used within a SidebarProvider')
|
|
24
|
+
}
|
|
25
|
+
return context
|
|
26
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useResponsiveLayout } from './index'
|
|
2
|
+
|
|
3
|
+
export const useLayout = () => {
|
|
4
|
+
const responsiveLayout = useResponsiveLayout()
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
// Layout actual
|
|
8
|
+
current: responsiveLayout.layout.current,
|
|
9
|
+
config: responsiveLayout.layout.config,
|
|
10
|
+
setLayout: responsiveLayout.layout.setLayout,
|
|
11
|
+
|
|
12
|
+
// Utilidades
|
|
13
|
+
...responsiveLayout.layoutUtils,
|
|
14
|
+
|
|
15
|
+
// Helpers específicos
|
|
16
|
+
isDefaultLayout: responsiveLayout.isDefaultLayout(),
|
|
17
|
+
isSidebarLayout: responsiveLayout.isSidebarLayout(),
|
|
18
|
+
isDashboardLayout: responsiveLayout.isDashboardLayout(),
|
|
19
|
+
isMinimalLayout: responsiveLayout.isMinimalLayout(),
|
|
20
|
+
|
|
21
|
+
// Grid helpers
|
|
22
|
+
grid: responsiveLayout.grid,
|
|
23
|
+
|
|
24
|
+
// Spacing helpers
|
|
25
|
+
spacing: responsiveLayout.spacing,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useMemo } from 'react'
|
|
2
|
+
import type {
|
|
3
|
+
ResponsiveState,
|
|
4
|
+
Breakpoint
|
|
5
|
+
} from '../types/responsive'
|
|
6
|
+
import {
|
|
7
|
+
getCurrentBreakpoint,
|
|
8
|
+
getBreakpointIndex
|
|
9
|
+
} from '../constants/breakpoints'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Debounce utility
|
|
13
|
+
*/
|
|
14
|
+
function debounce<T extends (...args: unknown[]) => void>(
|
|
15
|
+
func: T,
|
|
16
|
+
wait: number
|
|
17
|
+
): (...args: Parameters<T>) => void {
|
|
18
|
+
let timeout: ReturnType<typeof setTimeout> | null = null
|
|
19
|
+
return (...args: Parameters<T>) => {
|
|
20
|
+
if (timeout) clearTimeout(timeout)
|
|
21
|
+
timeout = setTimeout(() => func(...args), wait)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get orientation based on dimensions
|
|
27
|
+
*/
|
|
28
|
+
function getOrientation(width: number, height: number): 'landscape' | 'portrait' {
|
|
29
|
+
return width >= height ? 'landscape' : 'portrait'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Hook principal useResponsive
|
|
34
|
+
* Provee información sobre el breakpoint actual y helpers para responsive
|
|
35
|
+
*/
|
|
36
|
+
export const useResponsive = (): ResponsiveState => {
|
|
37
|
+
const [dimensions, setDimensions] = useState({
|
|
38
|
+
width: typeof window !== 'undefined' ? window.innerWidth : 1024,
|
|
39
|
+
height: typeof window !== 'undefined' ? window.innerHeight : 768
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// Función para actualizar dimensiones
|
|
43
|
+
const updateDimensions = useCallback(() => {
|
|
44
|
+
setDimensions({
|
|
45
|
+
width: window.innerWidth,
|
|
46
|
+
height: window.innerHeight
|
|
47
|
+
})
|
|
48
|
+
}, [])
|
|
49
|
+
|
|
50
|
+
// Debounced update para optimizar performance
|
|
51
|
+
const debouncedUpdateDimensions = useMemo(
|
|
52
|
+
() => debounce(updateDimensions, 100),
|
|
53
|
+
[updateDimensions]
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// Effect para escuchar cambios de tamaño
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (typeof window === 'undefined') return
|
|
59
|
+
|
|
60
|
+
window.addEventListener('resize', debouncedUpdateDimensions)
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
window.removeEventListener('resize', debouncedUpdateDimensions)
|
|
64
|
+
}
|
|
65
|
+
}, [debouncedUpdateDimensions])
|
|
66
|
+
|
|
67
|
+
const { width, height } = dimensions
|
|
68
|
+
|
|
69
|
+
// Calcular breakpoint actual
|
|
70
|
+
const breakpoint = useMemo(() => getCurrentBreakpoint(width), [width])
|
|
71
|
+
|
|
72
|
+
// Calcular orientación
|
|
73
|
+
const orientation = useMemo(() => getOrientation(width, height), [width, height])
|
|
74
|
+
|
|
75
|
+
// Helpers booleanos por breakpoint específico
|
|
76
|
+
const isXs = breakpoint === 'xs'
|
|
77
|
+
const isSm = breakpoint === 'sm'
|
|
78
|
+
const isMd = breakpoint === 'md'
|
|
79
|
+
const isLg = breakpoint === 'lg'
|
|
80
|
+
const isXl = breakpoint === 'xl'
|
|
81
|
+
const is2Xl = breakpoint === '2xl'
|
|
82
|
+
const is3Xl = breakpoint === '3xl'
|
|
83
|
+
const is4Xl = breakpoint === '4xl'
|
|
84
|
+
const is5Xl = breakpoint === '5xl'
|
|
85
|
+
|
|
86
|
+
// Helpers booleanos agrupados
|
|
87
|
+
const isMobile = isXs || isSm
|
|
88
|
+
const isTablet = isMd
|
|
89
|
+
const isDesktop = isLg || isXl || is2Xl || is3Xl || is4Xl || is5Xl
|
|
90
|
+
const isSmall = isXs || isSm || isMd
|
|
91
|
+
const isLarge = isLg || isXl || is2Xl || is3Xl || is4Xl || is5Xl
|
|
92
|
+
const isUltraWide = is3Xl || is4Xl || is5Xl
|
|
93
|
+
const is4K = is4Xl || is5Xl
|
|
94
|
+
const is5K = is5Xl
|
|
95
|
+
|
|
96
|
+
// Helpers de orientación
|
|
97
|
+
const isPortrait = orientation === 'portrait'
|
|
98
|
+
const isLandscape = orientation === 'landscape'
|
|
99
|
+
|
|
100
|
+
// Funciones de comparación de breakpoints
|
|
101
|
+
const isBreakpointUp = useCallback((bp: Breakpoint): boolean => {
|
|
102
|
+
return getBreakpointIndex(breakpoint) >= getBreakpointIndex(bp)
|
|
103
|
+
}, [breakpoint])
|
|
104
|
+
|
|
105
|
+
const isBreakpointDown = useCallback((bp: Breakpoint): boolean => {
|
|
106
|
+
return getBreakpointIndex(breakpoint) <= getBreakpointIndex(bp)
|
|
107
|
+
}, [breakpoint])
|
|
108
|
+
|
|
109
|
+
const isBreakpointBetween = useCallback((min: Breakpoint, max: Breakpoint): boolean => {
|
|
110
|
+
const current = getBreakpointIndex(breakpoint)
|
|
111
|
+
return current >= getBreakpointIndex(min) && current <= getBreakpointIndex(max)
|
|
112
|
+
}, [breakpoint])
|
|
113
|
+
|
|
114
|
+
// Funciones de comparación de ancho
|
|
115
|
+
const isWidthUp = useCallback((minWidth: number): boolean => {
|
|
116
|
+
return width >= minWidth
|
|
117
|
+
}, [width])
|
|
118
|
+
|
|
119
|
+
const isWidthDown = useCallback((maxWidth: number): boolean => {
|
|
120
|
+
return width <= maxWidth
|
|
121
|
+
}, [width])
|
|
122
|
+
|
|
123
|
+
const isWidthBetween = useCallback((minWidth: number, maxWidth: number): boolean => {
|
|
124
|
+
return width >= minWidth && width <= maxWidth
|
|
125
|
+
}, [width])
|
|
126
|
+
|
|
127
|
+
// Funciones de comparación de altura
|
|
128
|
+
const isHeightUp = useCallback((minHeight: number): boolean => {
|
|
129
|
+
return height >= minHeight
|
|
130
|
+
}, [height])
|
|
131
|
+
|
|
132
|
+
const isHeightDown = useCallback((maxHeight: number): boolean => {
|
|
133
|
+
return height <= maxHeight
|
|
134
|
+
}, [height])
|
|
135
|
+
|
|
136
|
+
const isHeightBetween = useCallback((minHeight: number, maxHeight: number): boolean => {
|
|
137
|
+
return height >= minHeight && height <= maxHeight
|
|
138
|
+
}, [height])
|
|
139
|
+
|
|
140
|
+
// Debug mode
|
|
141
|
+
const debug = false
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
// Estado básico
|
|
145
|
+
breakpoint,
|
|
146
|
+
width,
|
|
147
|
+
height,
|
|
148
|
+
orientation,
|
|
149
|
+
|
|
150
|
+
// Helpers booleanos específicos
|
|
151
|
+
isXs,
|
|
152
|
+
isSm,
|
|
153
|
+
isMd,
|
|
154
|
+
isLg,
|
|
155
|
+
isXl,
|
|
156
|
+
is2Xl,
|
|
157
|
+
is3Xl,
|
|
158
|
+
is4Xl,
|
|
159
|
+
is5Xl,
|
|
160
|
+
|
|
161
|
+
// Helpers booleanos agrupados
|
|
162
|
+
isMobile,
|
|
163
|
+
isTablet,
|
|
164
|
+
isDesktop,
|
|
165
|
+
isSmall,
|
|
166
|
+
isLarge,
|
|
167
|
+
isUltraWide,
|
|
168
|
+
is4K,
|
|
169
|
+
is5K,
|
|
170
|
+
|
|
171
|
+
// Helpers de orientación
|
|
172
|
+
isPortrait,
|
|
173
|
+
isLandscape,
|
|
174
|
+
|
|
175
|
+
// Funciones de comparación
|
|
176
|
+
isBreakpointUp,
|
|
177
|
+
isBreakpointDown,
|
|
178
|
+
isBreakpointBetween,
|
|
179
|
+
isWidthUp,
|
|
180
|
+
isWidthDown,
|
|
181
|
+
isWidthBetween,
|
|
182
|
+
isHeightUp,
|
|
183
|
+
isHeightDown,
|
|
184
|
+
isHeightBetween,
|
|
185
|
+
|
|
186
|
+
// Debug
|
|
187
|
+
debug
|
|
188
|
+
}
|
|
189
|
+
}
|