@umituz/web-design-system 3.1.22 → 4.0.0

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.
Files changed (82) hide show
  1. package/README.md +137 -469
  2. package/dist/styles.css +2 -0
  3. package/package.json +30 -31
  4. package/src/__tests__/cn.test.ts +24 -0
  5. package/src/__tests__/components.smoke.test.tsx +78 -0
  6. package/src/__tests__/styles.contract.test.ts +60 -0
  7. package/src/domain/tokens/index.ts +8 -49
  8. package/src/index.ts +8 -10
  9. package/src/infrastructure/constants/component.constants.ts +3 -17
  10. package/src/infrastructure/constants/index.ts +4 -2
  11. package/src/infrastructure/constants/timing.constants.ts +46 -0
  12. package/src/infrastructure/utils/calculation.utils.ts +74 -0
  13. package/src/infrastructure/utils/index.ts +12 -0
  14. package/src/presentation/atoms/IconButton.tsx +55 -0
  15. package/src/presentation/atoms/Progress.tsx +2 -1
  16. package/src/presentation/atoms/Spinner.tsx +1 -1
  17. package/src/presentation/atoms/Switch.tsx +2 -2
  18. package/src/presentation/atoms/Text.tsx +15 -13
  19. package/src/presentation/atoms/Tooltip.tsx +2 -1
  20. package/src/presentation/atoms/index.ts +3 -0
  21. package/src/presentation/hooks/index.ts +0 -3
  22. package/src/presentation/hooks/useClickOutside.ts +14 -10
  23. package/src/presentation/hooks/useClipboard.ts +30 -12
  24. package/src/presentation/hooks/useDebounce.ts +8 -10
  25. package/src/presentation/hooks/useLocalStorage.ts +28 -18
  26. package/src/presentation/hooks/useMediaQuery.ts +2 -1
  27. package/src/presentation/hooks/useScrollLock.ts +40 -12
  28. package/src/presentation/hooks/useToggle.ts +8 -6
  29. package/src/presentation/molecules/ActiveFilterTags.tsx +19 -18
  30. package/src/presentation/molecules/Avatar.tsx +1 -1
  31. package/src/presentation/molecules/CodeBlock.tsx +51 -48
  32. package/src/presentation/molecules/ListItem.tsx +40 -29
  33. package/src/presentation/molecules/Select.tsx +1 -1
  34. package/src/presentation/molecules/Toggle.tsx +4 -4
  35. package/src/presentation/molecules/index.ts +0 -3
  36. package/src/presentation/organisms/Alert.tsx +1 -1
  37. package/src/presentation/organisms/DataTable.tsx +24 -11
  38. package/src/presentation/organisms/Dialog.tsx +1 -1
  39. package/src/presentation/organisms/DropdownMenu.tsx +2 -2
  40. package/src/presentation/organisms/HoverCard.tsx +1 -1
  41. package/src/presentation/organisms/LoadingState.tsx +1 -5
  42. package/src/presentation/organisms/Popover.tsx +1 -1
  43. package/src/presentation/organisms/StatCard.tsx +2 -2
  44. package/src/presentation/organisms/Table.tsx +2 -2
  45. package/src/presentation/organisms/index.ts +0 -60
  46. package/src/styles/compile.css +18 -0
  47. package/src/styles/theme.css +131 -0
  48. package/src/domain/tokens/animation.tokens.ts +0 -87
  49. package/src/domain/tokens/color.tokens.ts +0 -95
  50. package/src/domain/tokens/radius.tokens.ts +0 -27
  51. package/src/domain/tokens/shadow.tokens.ts +0 -25
  52. package/src/domain/tokens/spacing.tokens.ts +0 -65
  53. package/src/domain/tokens/typography.tokens.ts +0 -74
  54. package/src/infrastructure/error/ErrorBoundary.tsx +0 -258
  55. package/src/infrastructure/error/ErrorDisplay.tsx +0 -346
  56. package/src/infrastructure/error/SuspenseWrapper.tsx +0 -133
  57. package/src/infrastructure/error/index.ts +0 -5
  58. package/src/infrastructure/performance/index.ts +0 -92
  59. package/src/infrastructure/performance/useLazyLoading.ts +0 -342
  60. package/src/infrastructure/performance/useMemoryOptimization.ts +0 -293
  61. package/src/infrastructure/performance/usePerformanceMonitor.ts +0 -158
  62. package/src/infrastructure/security/index.ts +0 -10
  63. package/src/infrastructure/security/security-config.ts +0 -171
  64. package/src/infrastructure/security/useFormValidation.ts +0 -216
  65. package/src/infrastructure/security/validation.ts +0 -246
  66. package/src/presentation/hooks/useLanguage.ts +0 -109
  67. package/src/presentation/molecules/Breadcrumb.tsx +0 -54
  68. package/src/presentation/organisms/AlertDialog.tsx +0 -181
  69. package/src/presentation/organisms/Calendar.tsx +0 -79
  70. package/src/presentation/organisms/Comments.tsx +0 -103
  71. package/src/presentation/organisms/ConfirmDialog.tsx +0 -97
  72. package/src/presentation/organisms/FilterBar.tsx +0 -244
  73. package/src/presentation/organisms/FilterSidebar.tsx +0 -172
  74. package/src/presentation/organisms/Footer.tsx +0 -86
  75. package/src/presentation/organisms/FormModal.tsx +0 -126
  76. package/src/presentation/organisms/Grid.tsx +0 -128
  77. package/src/presentation/organisms/ImageLightbox.tsx +0 -252
  78. package/src/presentation/organisms/MainNavbar.tsx +0 -320
  79. package/src/presentation/organisms/Modal.tsx +0 -173
  80. package/src/presentation/organisms/Navbar.tsx +0 -75
  81. package/src/presentation/organisms/NewsletterSignup.tsx +0 -165
  82. package/src/presentation/organisms/QuickActionCard.tsx +0 -93
@@ -1,128 +0,0 @@
1
- /**
2
- * Grid Component (Organism)
3
- * @description Responsive grid system with configurable columns
4
- */
5
-
6
- import { cn } from '../../infrastructure/utils';
7
- import type { BaseProps } from '../../domain/types';
8
-
9
- export interface GridProps extends BaseProps {
10
- children: React.ReactNode;
11
- cols?: {
12
- xs?: number;
13
- sm?: number;
14
- md?: number;
15
- lg?: number;
16
- xl?: number;
17
- '2xl'?: number;
18
- };
19
- gap?: number | string;
20
- className?: string;
21
- }
22
-
23
- /**
24
- * Responsive grid system that adjusts columns based on breakpoint
25
- * @param cols - Number of columns per breakpoint
26
- * @param gap - Gap between items (spacing unit or custom value)
27
- * @param children - Grid items
28
- *
29
- * @example
30
- * ```tsx
31
- * // Auto responsive: 1 col mobile, 2 tablet, 3 desktop
32
- * <Grid cols={{ xs: 1, sm: 2, lg: 3 }}>
33
- * <div>Item 1</div>
34
- * <div>Item 2</div>
35
- * </Grid>
36
- *
37
- * // Fixed 4 columns
38
- * <Grid cols={{ lg: 4 }}>
39
- * {items.map(item => <Card key={item.id}>{item}</Card>)}
40
- * </Grid>
41
- * ```
42
- */
43
- export const Grid = ({ children, cols = {}, gap = 4, className }: GridProps) => {
44
- const {
45
- xs = 1,
46
- sm = xs,
47
- md = sm,
48
- lg = md,
49
- xl = lg,
50
- '2xl': xl2 = xl
51
- } = cols;
52
-
53
- const gridCols = cn(
54
- 'grid',
55
- `grid-cols-${xs}`, // xs always applies (mobile-first)
56
- sm !== xs && `sm:grid-cols-${sm}`,
57
- md !== sm && `md:grid-cols-${md}`,
58
- lg !== md && `lg:grid-cols-${lg}`,
59
- xl !== lg && `xl:grid-cols-${xl}`,
60
- xl2 !== xl && `2xl:grid-cols-${xl2}`
61
- );
62
-
63
- const gapClass = typeof gap === 'number' ? `gap-${gap}` : gap;
64
-
65
- return (
66
- <div className={cn(gridCols, gapClass, className)}>
67
- {children}
68
- </div>
69
- );
70
- };
71
-
72
- Grid.displayName = 'Grid';
73
-
74
- /**
75
- * Grid Item component for custom column spans
76
- */
77
- export interface GridItemProps extends BaseProps {
78
- children: React.ReactNode;
79
- span?: {
80
- xs?: number;
81
- sm?: number;
82
- md?: number;
83
- lg?: number;
84
- xl?: number;
85
- '2xl'?: number;
86
- };
87
- className?: string;
88
- }
89
-
90
- /**
91
- * Grid item with custom column span per breakpoint
92
- * @param span - Number of columns to span (1-12)
93
- *
94
- * @example
95
- * ```tsx
96
- * <Grid cols={{ lg: 4 }}>
97
- * <GridItem span={{ lg: 2 }}>Spans 2 columns</GridItem>
98
- * <GridItem>Regular 1 column</GridItem>
99
- * </Grid>
100
- * ```
101
- */
102
- export const GridItem = ({ children, span = {}, className }: GridItemProps) => {
103
- const {
104
- xs = 1,
105
- sm = xs,
106
- md = sm,
107
- lg = md,
108
- xl = lg,
109
- '2xl': xl2 = xl
110
- } = span;
111
-
112
- const colSpan = cn(
113
- 'col-span-1',
114
- `sm:col-span-${sm}`,
115
- `md:col-span-${md}`,
116
- `lg:col-span-${lg}`,
117
- `xl:col-span-${xl}`,
118
- `2xl:col-span-${xl2}`
119
- );
120
-
121
- return (
122
- <div className={cn(colSpan, className)}>
123
- {children}
124
- </div>
125
- );
126
- };
127
-
128
- GridItem.displayName = 'GridItem';
@@ -1,252 +0,0 @@
1
- /**
2
- * ImageLightbox Component (Organism)
3
- * @description Full-screen image gallery with zoom, navigation, and thumbnails
4
- */
5
-
6
- import { useState, useCallback, useEffect } from 'react';
7
- import { cn } from '../../infrastructure/utils';
8
- import type { BaseProps } from '../../domain/types';
9
- import { Icon } from '../atoms/Icon';
10
-
11
- export interface ImageLightboxImage {
12
- src: string;
13
- alt: string;
14
- title?: string;
15
- }
16
-
17
- export interface ImageLightboxProps extends BaseProps {
18
- images: ImageLightboxImage[];
19
- initialIndex?: number;
20
- isOpen: boolean;
21
- onClose: () => void;
22
- }
23
-
24
- export const ImageLightbox = ({
25
- images,
26
- initialIndex = 0,
27
- isOpen,
28
- onClose,
29
- }: ImageLightboxProps) => {
30
- // Early return if closed
31
- if (!isOpen) return null;
32
-
33
- // Early return if no images
34
- if (!images || images.length === 0) {
35
- return null;
36
- }
37
-
38
- const [currentIndex, setCurrentIndex] = useState(initialIndex);
39
- const [isZoomed, setIsZoomed] = useState(false);
40
-
41
- // Ensure initialIndex is within bounds
42
- const safeIndex = Math.min(Math.max(initialIndex, 0), images.length - 1);
43
- const currentImage = images[safeIndex];
44
-
45
- // Use useCallback to memoize navigation functions
46
- const goToNext = useCallback(() => {
47
- setCurrentIndex((prev) => (prev + 1) % images.length);
48
- }, [images.length]);
49
-
50
- const goToPrevious = useCallback(() => {
51
- setCurrentIndex((prev) => (prev - 1 + images.length) % images.length);
52
- }, [images.length]);
53
-
54
- const handleImageClick = useCallback(() => {
55
- setIsZoomed(!isZoomed);
56
- }, [isZoomed]);
57
-
58
- // Handle keyboard navigation
59
- useEffect(() => {
60
- const handleKeyDown = (e: KeyboardEvent) => {
61
- switch (e.key) {
62
- case 'Escape':
63
- onClose();
64
- break;
65
- case 'ArrowLeft':
66
- goToPrevious();
67
- break;
68
- case 'ArrowRight':
69
- goToNext();
70
- break;
71
- case '+':
72
- case '=':
73
- setIsZoomed(!isZoomed);
74
- break;
75
- }
76
- };
77
-
78
- window.addEventListener('keydown', handleKeyDown);
79
- return () => window.removeEventListener('keydown', handleKeyDown);
80
- }, [onClose, goToPrevious, goToNext, isZoomed]);
81
-
82
- // Prevent body scroll when lightbox is open
83
- useEffect(() => {
84
- const originalOverflow = document.body.style.overflow;
85
- document.body.style.overflow = 'hidden';
86
-
87
- return () => {
88
- document.body.style.overflow = originalOverflow;
89
- };
90
- }, []);
91
-
92
- if (!currentImage) return null;
93
-
94
- return (
95
- <div
96
- className="fixed inset-0 z-50 bg-black/90 flex items-center justify-center p-4"
97
- onClick={onClose}
98
- >
99
- {/* Close button */}
100
- <button
101
- onClick={onClose}
102
- className="absolute top-4 right-4 p-2 bg-black/50 hover:bg-black/70 rounded-full text-white transition-all"
103
- aria-label="Close lightbox"
104
- >
105
- <Icon className="text-white">
106
- <path
107
- strokeLinecap="round"
108
- strokeLinejoin="round"
109
- strokeWidth={2}
110
- d="M6 18L18 6M6 6l12 12"
111
- />
112
- </Icon>
113
- </button>
114
-
115
- {/* Navigation - Previous */}
116
- {images.length > 1 && (
117
- <button
118
- onClick={(e) => {
119
- e.stopPropagation();
120
- goToPrevious();
121
- }}
122
- className="absolute left-4 p-2 bg-black/50 hover:bg-black/70 rounded-full text-white transition-all"
123
- aria-label="Previous image"
124
- >
125
- <Icon className="text-white" size="lg">
126
- <path
127
- strokeLinecap="round"
128
- strokeLinejoin="round"
129
- strokeWidth={2}
130
- d="M15 19l-7-7 7-7"
131
- />
132
- </Icon>
133
- </button>
134
- )}
135
-
136
- {/* Navigation - Next */}
137
- {images.length > 1 && (
138
- <button
139
- onClick={(e) => {
140
- e.stopPropagation();
141
- goToNext();
142
- }}
143
- className="absolute right-4 p-2 bg-black/50 hover:bg-black/70 rounded-full text-white transition-all"
144
- aria-label="Next image"
145
- >
146
- <Icon className="text-white" size="lg">
147
- <path
148
- strokeLinecap="round"
149
- strokeLinejoin="round"
150
- strokeWidth={2}
151
- d="M9 5l7 7-7 7"
152
- />
153
- </Icon>
154
- </button>
155
- )}
156
-
157
- {/* Main Image */}
158
- <div
159
- className="relative max-w-5xl max-h-[90vh] w-full"
160
- onClick={handleImageClick}
161
- >
162
- <img
163
- src={currentImage.src}
164
- alt={currentImage.alt}
165
- className={cn(
166
- 'w-full h-full object-contain',
167
- isZoomed ? 'cursor-zoom-out' : 'cursor-zoom-in'
168
- )}
169
- style={{
170
- maxHeight: '90vh',
171
- objectFit: 'contain',
172
- }}
173
- />
174
-
175
- {/* Zoom indicator */}
176
- <div className="absolute bottom-4 right-4 flex items-center gap-2 bg-black/50 px-3 py-2 rounded-full">
177
- <button
178
- onClick={(e) => {
179
- e.stopPropagation();
180
- setIsZoomed(!isZoomed);
181
- }}
182
- className="p-1 hover:bg-black/70 rounded-full transition-all text-white"
183
- aria-label={isZoomed ? 'Zoom out' : 'Zoom in'}
184
- >
185
- {isZoomed ? (
186
- <Icon className="text-white" size="sm">
187
- <path
188
- strokeLinecap="round"
189
- strokeLinejoin="round"
190
- strokeWidth={2}
191
- d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM13 10H7"
192
- />
193
- </Icon>
194
- ) : (
195
- <Icon className="text-white" size="sm">
196
- <path
197
- strokeLinecap="round"
198
- strokeLinejoin="round"
199
- strokeWidth={2}
200
- d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v6m3-3H7"
201
- />
202
- </Icon>
203
- )}
204
- </button>
205
- </div>
206
- </div>
207
-
208
- {/* Image Counter */}
209
- {images.length > 1 && (
210
- <div className="absolute bottom-4 left-4 bg-black/50 px-4 py-2 rounded-full text-white text-sm">
211
- {currentIndex + 1} / {images.length}
212
- </div>
213
- )}
214
-
215
- {/* Image Title */}
216
- {currentImage.title && (
217
- <div className="absolute bottom-4 left-1/2 -translate-x-1/2 bg-black/50 px-4 py-2 rounded-full text-white text-sm max-w-md text-center">
218
- {currentImage.title}
219
- </div>
220
- )}
221
-
222
- {/* Thumbnails */}
223
- {images.length > 1 && (
224
- <div className="absolute bottom-20 left-1/2 -translate-x-1/2 flex gap-2 max-w-xl overflow-x-auto">
225
- {images.map((image, index) => (
226
- <button
227
- key={index}
228
- onClick={(e) => {
229
- e.stopPropagation();
230
- setCurrentIndex(index);
231
- }}
232
- className={cn(
233
- 'flex-shrink-0 w-16 h-16 rounded-lg overflow-hidden border-2 transition-all',
234
- index === currentIndex
235
- ? 'border-white scale-110'
236
- : 'border-transparent opacity-50 hover:opacity-100'
237
- )}
238
- >
239
- <img
240
- src={image.src}
241
- alt={image.alt}
242
- className="w-full h-full object-cover"
243
- />
244
- </button>
245
- ))}
246
- </div>
247
- )}
248
- </div>
249
- );
250
- };
251
-
252
- ImageLightbox.displayName = 'ImageLightbox';
@@ -1,320 +0,0 @@
1
- /**
2
- * MainNavbar Component (Organism)
3
- * @description Full-featured navigation bar with logo, links, theme toggle, language selector, and mobile menu
4
- */
5
-
6
- import { useState, useEffect, useRef, useMemo } from 'react';
7
- // @ts-ignore - react-router-dom is a peer dependency, may not be available during package build
8
- import { Link, useLocation } from 'react-router-dom';
9
- import React from 'react';
10
- import { Show, Hide } from '../atoms';
11
- import type { BaseProps } from '../../domain/types';
12
-
13
- export interface NavItem {
14
- name: string;
15
- path: string;
16
- }
17
-
18
- export interface MainNavbarLanguage {
19
- code: string;
20
- name: string;
21
- flag: string;
22
- }
23
-
24
- export interface MainNavbarProps extends BaseProps {
25
- logo?: React.ReactNode;
26
- appName: string;
27
- navItems: NavItem[];
28
- supportedLanguages: Record<string, MainNavbarLanguage>;
29
- currentLanguage: string;
30
- onLanguageChange: (code: string) => void;
31
- theme: 'light' | 'dark';
32
- onThemeToggle: () => void;
33
- githubUrl?: string;
34
- githubLabel?: string;
35
- translations?: {
36
- language: string;
37
- switchToMode: (mode: string) => string;
38
- lightMode: string;
39
- darkMode: string;
40
- github: string;
41
- };
42
- }
43
-
44
- export const MainNavbar = ({
45
- logo,
46
- appName,
47
- navItems,
48
- supportedLanguages,
49
- currentLanguage,
50
- onLanguageChange,
51
- theme,
52
- onThemeToggle,
53
- githubUrl,
54
- githubLabel = 'GitHub',
55
- translations = {
56
- language: 'Language',
57
- switchToMode: (mode: string) => `Switch to ${mode} mode`,
58
- lightMode: 'Light Mode',
59
- darkMode: 'Dark Mode',
60
- github: 'GitHub',
61
- },
62
- className,
63
- }: MainNavbarProps) => {
64
- const [isOpen, setIsOpen] = useState(false);
65
- const [isLangOpen, setIsLangOpen] = useState(false);
66
- const location = useLocation();
67
- const langDropdownRef = useRef<HTMLDivElement>(null);
68
-
69
- const navItemsMemo = useMemo(() => navItems, [JSON.stringify(navItems)]);
70
-
71
- useEffect(() => {
72
- const handleClickOutside = (event: MouseEvent) => {
73
- if (langDropdownRef.current && !langDropdownRef.current.contains(event.target as Node)) {
74
- setIsLangOpen(false);
75
- }
76
- };
77
-
78
- if (isLangOpen) {
79
- document.addEventListener('mousedown', handleClickOutside);
80
- return () => {
81
- document.removeEventListener('mousedown', handleClickOutside);
82
- };
83
- }
84
- }, [isLangOpen]);
85
-
86
- return (
87
- <nav className={`bg-bg-primary sticky top-0 z-50 border-b border-border transition-theme ${className || ''}`}>
88
- <div className="max-w-7xl mx-auto px-4">
89
- <div className="flex justify-between h-16 items-center">
90
- {/* Logo + App Name */}
91
- <Link to="/" className="flex items-center gap-2 sm:gap-3">
92
- {logo}
93
- <span className="text-xl font-bold text-text-primary">{appName}</span>
94
- </Link>
95
-
96
- {/* Desktop Menu */}
97
- <Show above="lg">
98
- <div className="flex items-center gap-6 lg:gap-8">
99
- {navItemsMemo.map((item) => {
100
- const isActive = location.pathname === item.path;
101
- return (
102
- <Link
103
- key={item.path}
104
- to={item.path}
105
- className={`font-medium transition-colors transition-theme whitespace-nowrap ${
106
- isActive ? 'text-primary-light' : 'text-text-secondary hover:text-primary-light'
107
- }`}
108
- >
109
- {item.name}
110
- </Link>
111
- );
112
- })}
113
- </div>
114
- </Show>
115
-
116
- {/* Actions */}
117
- <div className="flex items-center gap-2 md:gap-3">
118
- {/* Language Selector */}
119
- <Show above="lg">
120
- <div className="relative" ref={langDropdownRef}>
121
- <button
122
- onClick={() => setIsLangOpen(!isLangOpen)}
123
- 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"
124
- title={translations.language}
125
- type="button"
126
- >
127
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
128
- <path d="m5 8 6 6" />
129
- <path d="m4 14 6-6 2-3" />
130
- <path d="M2 5h12" />
131
- <path d="M7 2h1" />
132
- <path d="m22 22-5-10-5 10" />
133
- <path d="M14 18h6" />
134
- </svg>
135
- </button>
136
-
137
- {isLangOpen && (
138
- <div className="absolute right-0 top-full mt-2 w-48 bg-bg-card rounded-lg border border-border shadow-xl z-50">
139
- {Object.entries(supportedLanguages).map(([code, { name, flag }]) => (
140
- <button
141
- key={code}
142
- onClick={() => {
143
- onLanguageChange(code);
144
- setIsLangOpen(false);
145
- }}
146
- className={`w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-bg-tertiary transition-colors transition-theme ${
147
- currentLanguage === code ? 'bg-bg-tertiary text-primary-light' : 'text-text-secondary'
148
- }`}
149
- type="button"
150
- >
151
- <span className="text-xl">{flag}</span>
152
- <span className="text-sm">{name}</span>
153
- {currentLanguage === code && (
154
- <span className="ml-auto text-primary-light">✓</span>
155
- )}
156
- </button>
157
- ))}
158
- </div>
159
- )}
160
- </div>
161
- </Show>
162
-
163
- {/* Theme Toggle */}
164
- <button
165
- onClick={onThemeToggle}
166
- 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"
167
- title={translations.switchToMode(theme === 'dark' ? 'light' : 'dark')}
168
- aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
169
- type="button"
170
- >
171
- {theme === 'dark' ? (
172
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
173
- <circle cx="12" cy="12" r="4" />
174
- <path d="M12 2v2" />
175
- <path d="M12 20v2" />
176
- <path d="m4.93 4.93 1.41 1.41" />
177
- <path d="m17.66 17.66 1.41 1.41" />
178
- <path d="M2 12h2" />
179
- <path d="M20 12h2" />
180
- <path d="m6.34 17.66-1.41 1.41" />
181
- <path d="m19.07 4.93-1.41 1.41" />
182
- </svg>
183
- ) : (
184
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
185
- <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
186
- </svg>
187
- )}
188
- </button>
189
-
190
- {/* GitHub */}
191
- <Show above="lg">
192
- {githubUrl && (
193
- <a
194
- href={githubUrl}
195
- target="_blank"
196
- rel="noopener noreferrer"
197
- 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"
198
- >
199
- <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
200
- <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"/>
201
- </svg>
202
- <span className="font-medium">{githubLabel}</span>
203
- </a>
204
- )}
205
- </Show>
206
-
207
- {/* Mobile Button */}
208
- <Hide above="lg">
209
- <button
210
- onClick={() => setIsOpen(!isOpen)}
211
- className="text-text-secondary flex"
212
- type="button"
213
- aria-label="Toggle menu"
214
- >
215
- {isOpen ? (
216
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
217
- <path d="M18 6L6 18M6 6l12 12" />
218
- </svg>
219
- ) : (
220
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
221
- <line x1="4" x2="20" y1="12" y2="12" />
222
- <line x1="4" x2="20" y1="6" y2="6" />
223
- <line x1="4" x2="20" y1="18" y2="18" />
224
- </svg>
225
- )}
226
- </button>
227
- </Hide>
228
- </div>
229
- </div>
230
- </div>
231
-
232
- {/* Mobile Menu */}
233
- <Hide above="lg">
234
- {isOpen && (
235
- <div className="bg-bg-secondary border-t border-border transition-theme">
236
- <div className="px-4 py-4 space-y-2">
237
- {/* Theme Toggle Mobile */}
238
- <button
239
- onClick={onThemeToggle}
240
- className="w-full flex items-center gap-3 px-4 py-3 rounded-lg text-text-secondary hover:bg-bg-tertiary transition-all transition-theme"
241
- type="button"
242
- >
243
- {theme === 'dark' ? (
244
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
245
- <circle cx="12" cy="12" r="4" />
246
- <path d="M12 2v2" />
247
- <path d="M12 20v2" />
248
- <path d="m4.93 4.93 1.41 1.41" />
249
- <path d="m17.66 17.66 1.41 1.41" />
250
- <path d="M2 12h2" />
251
- <path d="M20 12h2" />
252
- <path d="m6.34 17.66-1.41 1.41" />
253
- <path d="m19.07 4.93-1.41 1.41" />
254
- </svg>
255
- ) : (
256
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
257
- <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
258
- </svg>
259
- )}
260
- <span>{theme === 'dark' ? translations.lightMode : translations.darkMode}</span>
261
- </button>
262
-
263
- {/* Language Selector Mobile */}
264
- <div className="px-4 py-2">
265
- <div className="text-text-secondary text-sm mb-2">{translations.language}</div>
266
- <div className="grid grid-cols-2 gap-2">
267
- {Object.entries(supportedLanguages).map(([code, { name, flag }]) => (
268
- <button
269
- key={code}
270
- onClick={() => onLanguageChange(code)}
271
- className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-all transition-theme ${
272
- currentLanguage === code
273
- ? 'bg-primary-light text-text-primary'
274
- : 'bg-bg-primary text-text-secondary hover:bg-bg-tertiary'
275
- }`}
276
- type="button"
277
- >
278
- <span>{flag}</span>
279
- <span>{name}</span>
280
- </button>
281
- ))}
282
- </div>
283
- </div>
284
-
285
- {navItemsMemo.map((item) => {
286
- const isActive = location.pathname === item.path;
287
- return (
288
- <Link
289
- key={item.path}
290
- to={item.path}
291
- className={`block px-4 py-2 rounded-lg font-medium transition-theme ${
292
- isActive ? 'text-primary-light bg-bg-tertiary' : 'text-text-secondary hover:bg-bg-tertiary'
293
- }`}
294
- onClick={() => setIsOpen(false)}
295
- >
296
- {item.name}
297
- </Link>
298
- );
299
- })}
300
-
301
- {githubUrl && (
302
- <a
303
- href={githubUrl}
304
- target="_blank"
305
- rel="noopener noreferrer"
306
- className="flex items-center gap-2 px-4 py-2 text-text-secondary hover:text-text-primary rounded-lg hover:bg-bg-tertiary transition-all transition-theme"
307
- >
308
- <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
309
- <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"/>
310
- </svg>
311
- <span>{githubLabel}</span>
312
- </a>
313
- )}
314
- </div>
315
- </div>
316
- )}
317
- </Hide>
318
- </nav>
319
- );
320
- };