@umituz/web-design-system 2.6.4 → 2.7.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 +1 -1
- package/src/domain/types/breakpoint.types.ts +53 -0
- package/src/domain/types/index.ts +11 -0
- package/src/infrastructure/constants/breakpoint.constants.ts +72 -0
- package/src/infrastructure/constants/index.ts +10 -0
- package/src/presentation/atoms/Hide.tsx +58 -0
- package/src/presentation/atoms/Show.tsx +58 -0
- package/src/presentation/atoms/index.ts +6 -0
- package/src/presentation/hooks/index.ts +1 -1
- package/src/presentation/hooks/useMediaQuery.ts +140 -28
- package/src/presentation/organisms/MainNavbar.tsx +69 -58
- package/src/presentation/templates/ResponsiveContainer.tsx +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Responsive System Type Definitions
|
|
3
|
+
* @description Centralized breakpoint types for responsive design
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
|
7
|
+
|
|
8
|
+
export type ScreenSize = Breakpoint;
|
|
9
|
+
|
|
10
|
+
export interface BreakpointValue {
|
|
11
|
+
min: number;
|
|
12
|
+
max?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ResponsiveProps {
|
|
16
|
+
xs?: boolean;
|
|
17
|
+
sm?: boolean;
|
|
18
|
+
md?: boolean;
|
|
19
|
+
lg?: boolean;
|
|
20
|
+
xl?: boolean;
|
|
21
|
+
'2xl'?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ShowProps {
|
|
25
|
+
above?: Breakpoint; // Show on larger screens (min-width)
|
|
26
|
+
below?: Breakpoint; // Show on smaller screens (max-width)
|
|
27
|
+
at?: Breakpoint; // Show only at this breakpoint
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface HideProps {
|
|
31
|
+
above?: Breakpoint; // Hide on larger screens
|
|
32
|
+
below?: Breakpoint; // Hide on smaller screens
|
|
33
|
+
at?: Breakpoint; // Hide only at this breakpoint
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface BreakpointState {
|
|
37
|
+
currentBreakpoint: Breakpoint;
|
|
38
|
+
isXs: boolean;
|
|
39
|
+
isSm: boolean;
|
|
40
|
+
isMd: boolean;
|
|
41
|
+
isLg: boolean;
|
|
42
|
+
isXl: boolean;
|
|
43
|
+
is2Xl: boolean;
|
|
44
|
+
isMobile: boolean; // xs, sm
|
|
45
|
+
isTablet: boolean; // md, lg
|
|
46
|
+
isDesktop: boolean; // xl, 2xl
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface UseBreakpointReturn extends BreakpointState {
|
|
50
|
+
matches: (breakpoint: Breakpoint | Breakpoint[]) => boolean;
|
|
51
|
+
isGreaterThan: (breakpoint: Breakpoint) => boolean;
|
|
52
|
+
isLessThan: (breakpoint: Breakpoint) => boolean;
|
|
53
|
+
}
|
|
@@ -12,3 +12,14 @@ export type {
|
|
|
12
12
|
ChildrenProps,
|
|
13
13
|
PolymorphicProps,
|
|
14
14
|
} from './component.types';
|
|
15
|
+
|
|
16
|
+
export type {
|
|
17
|
+
Breakpoint,
|
|
18
|
+
ScreenSize,
|
|
19
|
+
BreakpointValue,
|
|
20
|
+
ResponsiveProps,
|
|
21
|
+
ShowProps,
|
|
22
|
+
HideProps,
|
|
23
|
+
BreakpointState,
|
|
24
|
+
UseBreakpointReturn,
|
|
25
|
+
} from './breakpoint.types';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Responsive System Constants
|
|
3
|
+
* @description Centralized breakpoint values matching Tailwind CSS defaults
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Breakpoint, BreakpointValue } from '../../domain/types/breakpoint.types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Breakpoint values matching Tailwind CSS default breakpoints
|
|
10
|
+
* https://tailwindcss.com/docs/screens
|
|
11
|
+
*/
|
|
12
|
+
export const BREAKPOINTS: Record<Breakpoint, BreakpointValue> = {
|
|
13
|
+
xs: { min: 0 },
|
|
14
|
+
sm: { min: 640 },
|
|
15
|
+
md: { min: 768 },
|
|
16
|
+
lg: { min: 1024 },
|
|
17
|
+
xl: { min: 1280 },
|
|
18
|
+
'2xl': { min: 1536 },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a media query string for a given breakpoint
|
|
23
|
+
* @param breakpoint - The breakpoint to create a query for
|
|
24
|
+
* @returns CSS media query string (e.g., "(min-width: 640px)")
|
|
25
|
+
*/
|
|
26
|
+
export const createMediaQuery = (breakpoint: Breakpoint): string => {
|
|
27
|
+
const bp = BREAKPOINTS[breakpoint];
|
|
28
|
+
if (bp.max) {
|
|
29
|
+
return `(min-width: ${bp.min}px) and (max-width: ${bp.max}px)`;
|
|
30
|
+
}
|
|
31
|
+
return `(min-width: ${bp.min}px)`;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a max-width media query
|
|
36
|
+
* @param breakpoint - The breakpoint to create a max query for
|
|
37
|
+
* @returns CSS media query string (e.g., "(max-width: 639px)")
|
|
38
|
+
*/
|
|
39
|
+
export const createMaxMediaQuery = (breakpoint: Breakpoint): string => {
|
|
40
|
+
const bp = BREAKPOINTS[breakpoint];
|
|
41
|
+
const maxValue = bp.min - 1;
|
|
42
|
+
return `(max-width: ${maxValue}px)`;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Breakpoint order for comparison operations
|
|
47
|
+
*/
|
|
48
|
+
export const BREAKPOINT_ORDER: Breakpoint[] = ['xs', 'sm', 'md', 'lg', 'xl', '2xl'];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Compare two breakpoints
|
|
52
|
+
* @returns -1 if a < b, 0 if a === b, 1 if a > b
|
|
53
|
+
*/
|
|
54
|
+
export const compareBreakpoints = (a: Breakpoint, b: Breakpoint): number => {
|
|
55
|
+
const indexA = BREAKPOINT_ORDER.indexOf(a);
|
|
56
|
+
const indexB = BREAKPOINT_ORDER.indexOf(b);
|
|
57
|
+
return indexA - indexB;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if breakpoint A is greater than breakpoint B
|
|
62
|
+
*/
|
|
63
|
+
export const isBreakpointGreaterThan = (a: Breakpoint, b: Breakpoint): boolean => {
|
|
64
|
+
return compareBreakpoints(a, b) > 0;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if breakpoint A is less than breakpoint B
|
|
69
|
+
*/
|
|
70
|
+
export const isBreakpointLessThan = (a: Breakpoint, b: Breakpoint): boolean => {
|
|
71
|
+
return compareBreakpoints(a, b) < 0;
|
|
72
|
+
};
|
|
@@ -11,3 +11,13 @@ export {
|
|
|
11
11
|
SIZE_MAP,
|
|
12
12
|
COLOR_MAP,
|
|
13
13
|
} from './component.constants';
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
BREAKPOINTS,
|
|
17
|
+
createMediaQuery,
|
|
18
|
+
createMaxMediaQuery,
|
|
19
|
+
BREAKPOINT_ORDER,
|
|
20
|
+
compareBreakpoints,
|
|
21
|
+
isBreakpointGreaterThan,
|
|
22
|
+
isBreakpointLessThan,
|
|
23
|
+
} from './breakpoint.constants';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hide Component (Atom)
|
|
3
|
+
* @description Conditionally hide content based on screen breakpoint
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useBreakpoint } from '../hooks/useMediaQuery';
|
|
7
|
+
import type { Breakpoint, HideProps } from '../../domain/types/breakpoint.types';
|
|
8
|
+
import type { BaseProps } from '../../domain/types';
|
|
9
|
+
|
|
10
|
+
export interface HideComponentProps extends BaseProps, HideProps {
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Conditionally hide content based on breakpoint
|
|
16
|
+
* @param above - Hide on screens larger than this breakpoint
|
|
17
|
+
* @param below - Hide on screens smaller than this breakpoint
|
|
18
|
+
* @param at - Hide only on this specific breakpoint
|
|
19
|
+
* @param children - Content to hide
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* // Hide on mobile (md and below)
|
|
24
|
+
* <Hide below="md"><DesktopNav /></Hide>
|
|
25
|
+
*
|
|
26
|
+
* // Hide on desktop (lg and above)
|
|
27
|
+
* <Hide above="lg"><MobileNav /></Hide>
|
|
28
|
+
*
|
|
29
|
+
* // Hide only on tablet
|
|
30
|
+
* <Hide at="md"><TabletContent /></Hide>
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export const Hide = ({ above, below, at, children, className }: HideComponentProps) => {
|
|
34
|
+
const { matches, isGreaterThan, isLessThan } = useBreakpoint();
|
|
35
|
+
|
|
36
|
+
// Determine if content should be hidden
|
|
37
|
+
let shouldHide = false;
|
|
38
|
+
|
|
39
|
+
if (above) {
|
|
40
|
+
shouldHide = shouldHide || isGreaterThan(above);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (below) {
|
|
44
|
+
shouldHide = shouldHide || isLessThan(below);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (at) {
|
|
48
|
+
shouldHide = shouldHide || matches(at);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (shouldHide) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return <div className={className || ''}>{children}</div>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
Hide.displayName = 'Hide';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Show Component (Atom)
|
|
3
|
+
* @description Conditionally render content based on screen breakpoint
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useBreakpoint } from '../hooks/useMediaQuery';
|
|
7
|
+
import type { Breakpoint, ShowProps } from '../../domain/types/breakpoint.types';
|
|
8
|
+
import type { BaseProps } from '../../domain/types';
|
|
9
|
+
|
|
10
|
+
export interface ShowComponentProps extends BaseProps, ShowProps {
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Conditionally show content based on breakpoint
|
|
16
|
+
* @param above - Show on screens larger than this breakpoint
|
|
17
|
+
* @param below - Show on screens smaller than this breakpoint
|
|
18
|
+
* @param at - Show only on this specific breakpoint
|
|
19
|
+
* @param children - Content to show
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* // Show on desktop (lg and above)
|
|
24
|
+
* <Show above="lg"><DesktopNav /></Show>
|
|
25
|
+
*
|
|
26
|
+
* // Show on mobile (sm and below)
|
|
27
|
+
* <Show below="md"><MobileNav /></Show>
|
|
28
|
+
*
|
|
29
|
+
* // Show only on tablet
|
|
30
|
+
* <Show at="md"><TabletContent /></Show>
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export const Show = ({ above, below, at, children, className }: ShowComponentProps) => {
|
|
34
|
+
const { matches, isGreaterThan, isLessThan } = useBreakpoint();
|
|
35
|
+
|
|
36
|
+
// Determine if content should be shown
|
|
37
|
+
let shouldShow = true;
|
|
38
|
+
|
|
39
|
+
if (above) {
|
|
40
|
+
shouldShow = shouldShow && isGreaterThan(above);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (below) {
|
|
44
|
+
shouldShow = shouldShow && isLessThan(below);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (at) {
|
|
48
|
+
shouldShow = shouldShow && matches(at);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!shouldShow) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return <div className={className || ''}>{children}</div>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
Show.displayName = 'Show';
|
|
@@ -57,3 +57,9 @@ export type { SwitchProps } from './Switch';
|
|
|
57
57
|
export { Separator } from './Separator';
|
|
58
58
|
|
|
59
59
|
export { Toggle, toggleVariants } from './Toggle';
|
|
60
|
+
|
|
61
|
+
export { Show } from './Show';
|
|
62
|
+
export type { ShowComponentProps } from './Show';
|
|
63
|
+
|
|
64
|
+
export { Hide } from './Hide';
|
|
65
|
+
export type { HideComponentProps } from './Hide';
|
|
@@ -8,7 +8,7 @@ export { useTheme } from './useTheme';
|
|
|
8
8
|
export type { Theme, UseThemeReturn } from './useTheme';
|
|
9
9
|
|
|
10
10
|
export { useMediaQuery, useBreakpoint } from './useMediaQuery';
|
|
11
|
-
export type { Breakpoint } from '
|
|
11
|
+
export type { Breakpoint, UseBreakpointReturn } from '../../domain/types/breakpoint.types';
|
|
12
12
|
|
|
13
13
|
export { useLocalStorage } from './useLocalStorage';
|
|
14
14
|
|
|
@@ -1,28 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* useMediaQuery
|
|
3
|
-
* @description
|
|
2
|
+
* useMediaQuery & useBreakpoint Hooks
|
|
3
|
+
* @description Enhanced responsive breakpoint detection with helper functions
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useEffect, useState } from 'react';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
};
|
|
6
|
+
import { useEffect, useState, useCallback } from 'react';
|
|
7
|
+
import type {
|
|
8
|
+
Breakpoint,
|
|
9
|
+
UseBreakpointReturn,
|
|
10
|
+
} from '../../domain/types/breakpoint.types';
|
|
11
|
+
import {
|
|
12
|
+
BREAKPOINTS,
|
|
13
|
+
createMediaQuery,
|
|
14
|
+
isBreakpointGreaterThan,
|
|
15
|
+
isBreakpointLessThan,
|
|
16
|
+
} from '../../infrastructure/constants/breakpoint.constants';
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Simple media query hook for a single breakpoint
|
|
20
|
+
* @param breakpoint - The breakpoint to check
|
|
21
|
+
* @returns Whether the media query matches
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* const isDesktop = useMediaQuery('lg') // true on lg screens and above
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
18
28
|
export function useMediaQuery(breakpoint: Breakpoint): boolean {
|
|
19
29
|
const [matches, setMatches] = useState(false);
|
|
20
30
|
|
|
21
31
|
useEffect(() => {
|
|
22
|
-
const
|
|
32
|
+
const query = createMediaQuery(breakpoint);
|
|
33
|
+
const media = window.matchMedia(query);
|
|
23
34
|
setMatches(media.matches);
|
|
24
35
|
|
|
25
|
-
const listener = () => setMatches(
|
|
36
|
+
const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
|
|
26
37
|
media.addEventListener('change', listener);
|
|
27
38
|
return () => media.removeEventListener('change', listener);
|
|
28
39
|
}, [breakpoint]);
|
|
@@ -30,17 +41,118 @@ export function useMediaQuery(breakpoint: Breakpoint): boolean {
|
|
|
30
41
|
return matches;
|
|
31
42
|
}
|
|
32
43
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Enhanced breakpoint hook with helper functions
|
|
46
|
+
* @returns Object containing current breakpoint and helper functions
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```tsx
|
|
50
|
+
* const { currentBreakpoint, isMobile, isDesktop } = useBreakpoint()
|
|
51
|
+
*
|
|
52
|
+
* return (
|
|
53
|
+
* <div>
|
|
54
|
+
* {isMobile && <MobileNav />}
|
|
55
|
+
* {isDesktop && <DesktopNav />}
|
|
56
|
+
* </div>
|
|
57
|
+
* )
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function useBreakpoint(): UseBreakpointReturn {
|
|
61
|
+
const [currentBreakpoint, setCurrentBreakpoint] = useState<Breakpoint>(() => {
|
|
62
|
+
// Initialize with current window size
|
|
63
|
+
if (typeof window === 'undefined') return 'lg';
|
|
64
|
+
|
|
65
|
+
const width = window.innerWidth;
|
|
66
|
+
for (const [bp, value] of Object.entries(BREAKPOINTS).reverse()) {
|
|
67
|
+
if (width >= value.min) return bp as Breakpoint;
|
|
68
|
+
}
|
|
69
|
+
return 'xs';
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
// More efficient: track window resize with debounce
|
|
74
|
+
const updateBreakpoint = () => {
|
|
75
|
+
const width = window.innerWidth;
|
|
76
|
+
const sortedBreakpoints = Object.entries(BREAKPOINTS).sort(
|
|
77
|
+
([, a], [, b]) => b.min - a.min
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
for (const [bp, value] of sortedBreakpoints) {
|
|
81
|
+
if (width >= value.min) {
|
|
82
|
+
setCurrentBreakpoint(bp as Breakpoint);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Initial check
|
|
89
|
+
updateBreakpoint();
|
|
90
|
+
|
|
91
|
+
// Debounced resize listener
|
|
92
|
+
let resizeTimer: ReturnType<typeof setTimeout>;
|
|
93
|
+
const handleResize = () => {
|
|
94
|
+
clearTimeout(resizeTimer);
|
|
95
|
+
resizeTimer = setTimeout(updateBreakpoint, 100);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
window.addEventListener('resize', handleResize);
|
|
99
|
+
|
|
100
|
+
return () => {
|
|
101
|
+
window.removeEventListener('resize', handleResize);
|
|
102
|
+
clearTimeout(resizeTimer);
|
|
103
|
+
};
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
// Helper functions
|
|
107
|
+
const matches = useCallback(
|
|
108
|
+
(breakpoint: Breakpoint | Breakpoint[]): boolean => {
|
|
109
|
+
if (Array.isArray(breakpoint)) {
|
|
110
|
+
return breakpoint.includes(currentBreakpoint);
|
|
111
|
+
}
|
|
112
|
+
return currentBreakpoint === breakpoint;
|
|
113
|
+
},
|
|
114
|
+
[currentBreakpoint]
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const isGreaterThan = useCallback(
|
|
118
|
+
(breakpoint: Breakpoint): boolean => {
|
|
119
|
+
return isBreakpointGreaterThan(currentBreakpoint, breakpoint);
|
|
120
|
+
},
|
|
121
|
+
[currentBreakpoint]
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const isLessThan = useCallback(
|
|
125
|
+
(breakpoint: Breakpoint): boolean => {
|
|
126
|
+
return isBreakpointLessThan(currentBreakpoint, breakpoint);
|
|
127
|
+
},
|
|
128
|
+
[currentBreakpoint]
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Computed values
|
|
132
|
+
const isXs = currentBreakpoint === 'xs';
|
|
133
|
+
const isSm = currentBreakpoint === 'sm';
|
|
134
|
+
const isMd = currentBreakpoint === 'md';
|
|
135
|
+
const isLg = currentBreakpoint === 'lg';
|
|
136
|
+
const isXl = currentBreakpoint === 'xl';
|
|
137
|
+
const is2Xl = currentBreakpoint === '2xl';
|
|
138
|
+
|
|
139
|
+
const isMobile = isXs || isSm;
|
|
140
|
+
const isTablet = isMd || isLg;
|
|
141
|
+
const isDesktop = isXl || is2Xl;
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
currentBreakpoint,
|
|
145
|
+
isXs,
|
|
146
|
+
isSm,
|
|
147
|
+
isMd,
|
|
148
|
+
isLg,
|
|
149
|
+
isXl,
|
|
150
|
+
is2Xl,
|
|
151
|
+
isMobile,
|
|
152
|
+
isTablet,
|
|
153
|
+
isDesktop,
|
|
154
|
+
matches,
|
|
155
|
+
isGreaterThan,
|
|
156
|
+
isLessThan,
|
|
157
|
+
};
|
|
46
158
|
}
|
|
@@ -7,6 +7,7 @@ import { useState, useEffect, useRef, useMemo } from 'react';
|
|
|
7
7
|
// @ts-ignore - react-router-dom is a peer dependency, may not be available during package build
|
|
8
8
|
import { Link, useLocation } from 'react-router-dom';
|
|
9
9
|
import React from 'react';
|
|
10
|
+
import { Show, Hide } from '../atoms';
|
|
10
11
|
import type { BaseProps } from '../../domain/types';
|
|
11
12
|
|
|
12
13
|
export interface NavItem {
|
|
@@ -92,33 +93,36 @@ export const MainNavbar = ({
|
|
|
92
93
|
</Link>
|
|
93
94
|
|
|
94
95
|
{/* Desktop Menu */}
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
96
|
+
<Show above="lg">
|
|
97
|
+
<div className="flex items-center space-x-6">
|
|
98
|
+
{navItemsMemo.map((item) => {
|
|
99
|
+
const isActive = location.pathname === item.path;
|
|
100
|
+
return (
|
|
101
|
+
<Link
|
|
102
|
+
key={item.path}
|
|
103
|
+
to={item.path}
|
|
104
|
+
className={`font-medium transition-colors transition-theme ${
|
|
105
|
+
isActive ? 'text-primary-light' : 'text-text-secondary hover:text-primary-light'
|
|
106
|
+
}`}
|
|
107
|
+
>
|
|
108
|
+
{item.name}
|
|
109
|
+
</Link>
|
|
110
|
+
);
|
|
111
|
+
})}
|
|
112
|
+
</div>
|
|
113
|
+
</Show>
|
|
111
114
|
|
|
112
115
|
{/* Actions */}
|
|
113
116
|
<div className="flex items-center gap-2 md:gap-3">
|
|
114
117
|
{/* Language Selector */}
|
|
115
|
-
<
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
<Show above="lg">
|
|
119
|
+
<div className="relative" ref={langDropdownRef}>
|
|
120
|
+
<button
|
|
121
|
+
onClick={() => setIsLangOpen(!isLangOpen)}
|
|
122
|
+
className="p-2 rounded-lg bg-bg-secondary text-text-secondary hover:text-primary-light border border-border hover:border-primary-light transition-all transition-theme"
|
|
123
|
+
title={translations.language}
|
|
124
|
+
type="button"
|
|
125
|
+
>
|
|
122
126
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
123
127
|
<path d="m5 8 6 6" />
|
|
124
128
|
<path d="m4 14 6-6 2-3" />
|
|
@@ -152,7 +156,8 @@ export const MainNavbar = ({
|
|
|
152
156
|
))}
|
|
153
157
|
</div>
|
|
154
158
|
)}
|
|
155
|
-
|
|
159
|
+
</div>
|
|
160
|
+
</Show>
|
|
156
161
|
|
|
157
162
|
{/* Theme Toggle */}
|
|
158
163
|
<button
|
|
@@ -182,46 +187,51 @@ export const MainNavbar = ({
|
|
|
182
187
|
</button>
|
|
183
188
|
|
|
184
189
|
{/* GitHub */}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
<
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
190
|
+
<Show above="lg">
|
|
191
|
+
{githubUrl && (
|
|
192
|
+
<a
|
|
193
|
+
href={githubUrl}
|
|
194
|
+
target="_blank"
|
|
195
|
+
rel="noopener noreferrer"
|
|
196
|
+
className="flex items-center gap-2 px-4 py-2 bg-bg-secondary text-text-secondary rounded-lg border border-border hover:border-primary-light hover:text-text-primary transition-all transition-theme"
|
|
197
|
+
>
|
|
198
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
|
199
|
+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
|
200
|
+
</svg>
|
|
201
|
+
<span className="font-medium">{githubLabel}</span>
|
|
202
|
+
</a>
|
|
203
|
+
)}
|
|
204
|
+
</Show>
|
|
198
205
|
|
|
199
206
|
{/* Mobile Button */}
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
<
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
207
|
+
<Hide above="lg">
|
|
208
|
+
<button
|
|
209
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
210
|
+
className="text-text-secondary flex"
|
|
211
|
+
type="button"
|
|
212
|
+
aria-label="Toggle menu"
|
|
213
|
+
>
|
|
214
|
+
{isOpen ? (
|
|
215
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
216
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
217
|
+
</svg>
|
|
218
|
+
) : (
|
|
219
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
220
|
+
<line x1="4" x2="20" y1="12" y2="12" />
|
|
221
|
+
<line x1="4" x2="20" y1="6" y2="6" />
|
|
222
|
+
<line x1="4" x2="20" y1="18" y2="18" />
|
|
223
|
+
</svg>
|
|
224
|
+
)}
|
|
225
|
+
</button>
|
|
226
|
+
</Hide>
|
|
218
227
|
</div>
|
|
219
228
|
</div>
|
|
220
229
|
</div>
|
|
221
230
|
|
|
222
231
|
{/* Mobile Menu */}
|
|
223
|
-
|
|
224
|
-
|
|
232
|
+
<Hide above="lg">
|
|
233
|
+
{isOpen && (
|
|
234
|
+
<div className="bg-bg-secondary border-t border-border transition-theme">
|
|
225
235
|
<div className="px-4 py-4 space-y-2">
|
|
226
236
|
{/* Theme Toggle Mobile */}
|
|
227
237
|
<button
|
|
@@ -302,7 +312,8 @@ export const MainNavbar = ({
|
|
|
302
312
|
)}
|
|
303
313
|
</div>
|
|
304
314
|
</div>
|
|
305
|
-
|
|
315
|
+
)}
|
|
316
|
+
</Hide>
|
|
306
317
|
</nav>
|
|
307
318
|
);
|
|
308
319
|
};
|
|
@@ -98,7 +98,7 @@ export const ResponsiveContainer = forwardRef<
|
|
|
98
98
|
},
|
|
99
99
|
ref
|
|
100
100
|
) => {
|
|
101
|
-
const breakpoint = useBreakpoint();
|
|
101
|
+
const { currentBreakpoint: breakpoint } = useBreakpoint();
|
|
102
102
|
|
|
103
103
|
// Determine current max width based on breakpoint
|
|
104
104
|
const getCurrentMaxWidth = (): string => {
|