@umituz/web-design-system 2.4.2 → 2.6.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/web-design-system",
3
- "version": "2.4.2",
3
+ "version": "2.6.0",
4
4
  "private": false,
5
5
  "description": "Web Design System - Atomic Design components (Atoms, Molecules, Organisms, Templates) for React applications",
6
6
  "main": "./src/index.ts",
@@ -77,6 +77,7 @@
77
77
  "react-day-picker": ">=9.0.0",
78
78
  "react-dom": ">=19.0.0",
79
79
  "react-i18next": ">=13.0.0",
80
+ "react-router-dom": ">=6.22.0",
80
81
  "react-syntax-highlighter": ">=15.0.0",
81
82
  "tailwind-merge": ">=2.0.0"
82
83
  },
@@ -100,6 +101,7 @@
100
101
  "@radix-ui/react-tooltip": "^1.2.7",
101
102
  "@types/react": "^19.0.0",
102
103
  "@types/react-dom": "^19.0.0",
104
+ "@types/react-router-dom": "^6.22.0",
103
105
  "@types/react-syntax-highlighter": "^15.5.13",
104
106
  "class-variance-authority": "^0.7.1",
105
107
  "clsx": "^2.1.1",
@@ -108,6 +110,7 @@
108
110
  "react-day-picker": "^9.14.0",
109
111
  "react-dom": "^19.0.0",
110
112
  "react-i18next": "^13.0.0",
113
+ "react-router-dom": "^6.22.0",
111
114
  "react-syntax-highlighter": "^16.1.1",
112
115
  "tailwind-merge": "^3.5.0",
113
116
  "typescript": "~5.9.2"
@@ -3,6 +3,7 @@
3
3
  * @description Navigation breadcrumb with home icon
4
4
  */
5
5
 
6
+ // @ts-expect-error - react-router-dom is a peer dependency
6
7
  import { Link } from 'react-router-dom';
7
8
  import type { BaseProps } from '../../domain/types';
8
9
 
@@ -69,7 +69,7 @@ export const Comments = ({
69
69
  ...config,
70
70
  };
71
71
 
72
- window.giscus.render(rootRef.current, defaultConfig);
72
+ window.giscus.render(rootRef.current, defaultConfig as unknown as Record<string, unknown>);
73
73
  }
74
74
  });
75
75
 
@@ -0,0 +1,308 @@
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-expect-error - react-router-dom is a peer dependency
8
+ import { Link, useLocation } from 'react-router-dom';
9
+ import React from 'react';
10
+ import type { BaseProps } from '../../domain/types';
11
+
12
+ export interface NavItem {
13
+ name: string;
14
+ path: string;
15
+ }
16
+
17
+ export interface MainNavbarLanguage {
18
+ code: string;
19
+ name: string;
20
+ flag: string;
21
+ }
22
+
23
+ export interface MainNavbarProps extends BaseProps {
24
+ logo?: React.ReactNode;
25
+ appName: string;
26
+ navItems: NavItem[];
27
+ supportedLanguages: Record<string, MainNavbarLanguage>;
28
+ currentLanguage: string;
29
+ onLanguageChange: (code: string) => void;
30
+ theme: 'light' | 'dark';
31
+ onThemeToggle: () => void;
32
+ githubUrl?: string;
33
+ githubLabel?: string;
34
+ translations?: {
35
+ language: string;
36
+ switchToMode: (mode: string) => string;
37
+ lightMode: string;
38
+ darkMode: string;
39
+ github: string;
40
+ };
41
+ }
42
+
43
+ export const MainNavbar = ({
44
+ logo,
45
+ appName,
46
+ navItems,
47
+ supportedLanguages,
48
+ currentLanguage,
49
+ onLanguageChange,
50
+ theme,
51
+ onThemeToggle,
52
+ githubUrl,
53
+ githubLabel = 'GitHub',
54
+ translations = {
55
+ language: 'Language',
56
+ switchToMode: (mode: string) => `Switch to ${mode} mode`,
57
+ lightMode: 'Light Mode',
58
+ darkMode: 'Dark Mode',
59
+ github: 'GitHub',
60
+ },
61
+ className,
62
+ }: MainNavbarProps) => {
63
+ const [isOpen, setIsOpen] = useState(false);
64
+ const [isLangOpen, setIsLangOpen] = useState(false);
65
+ const location = useLocation();
66
+ const langDropdownRef = useRef<HTMLDivElement>(null);
67
+
68
+ const navItemsMemo = useMemo(() => navItems, [JSON.stringify(navItems)]);
69
+
70
+ useEffect(() => {
71
+ const handleClickOutside = (event: MouseEvent) => {
72
+ if (langDropdownRef.current && !langDropdownRef.current.contains(event.target as Node)) {
73
+ setIsLangOpen(false);
74
+ }
75
+ };
76
+
77
+ if (isLangOpen) {
78
+ document.addEventListener('mousedown', handleClickOutside);
79
+ return () => {
80
+ document.removeEventListener('mousedown', handleClickOutside);
81
+ };
82
+ }
83
+ }, [isLangOpen]);
84
+
85
+ return (
86
+ <nav className={`bg-bg-primary sticky top-0 z-50 border-b border-border transition-theme ${className || ''}`}>
87
+ <div className="max-w-7xl mx-auto px-4">
88
+ <div className="flex justify-between h-16 items-center">
89
+ {/* Logo */}
90
+ <Link to="/" className="flex items-center gap-2">
91
+ {logo || <span className="text-xl font-bold text-text-primary">{appName}</span>}
92
+ </Link>
93
+
94
+ {/* Desktop Menu */}
95
+ <div className="hidden md:flex items-center space-x-6">
96
+ {navItemsMemo.map((item) => {
97
+ const isActive = location.pathname === item.path;
98
+ return (
99
+ <Link
100
+ key={item.path}
101
+ to={item.path}
102
+ className={`font-medium transition-colors transition-theme ${
103
+ isActive ? 'text-primary-light' : 'text-text-secondary hover:text-primary-light'
104
+ }`}
105
+ >
106
+ {item.name}
107
+ </Link>
108
+ );
109
+ })}
110
+ </div>
111
+
112
+ {/* Actions */}
113
+ <div className="flex items-center gap-2 md:gap-3">
114
+ {/* Language Selector */}
115
+ <div className="relative" ref={langDropdownRef}>
116
+ <button
117
+ onClick={() => setIsLangOpen(!isLangOpen)}
118
+ 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 hidden md:block"
119
+ title={translations.language}
120
+ type="button"
121
+ >
122
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
123
+ <path d="m5 8 6 6" />
124
+ <path d="m4 14 6-6 2-3" />
125
+ <path d="M2 5h12" />
126
+ <path d="M7 2h1" />
127
+ <path d="m22 22-5-10-5 10" />
128
+ <path d="M14 18h6" />
129
+ </svg>
130
+ </button>
131
+
132
+ {isLangOpen && (
133
+ <div className="absolute right-0 top-full mt-2 w-48 bg-bg-card rounded-lg border border-border shadow-xl z-50">
134
+ {Object.entries(supportedLanguages).map(([code, { name, flag }]) => (
135
+ <button
136
+ key={code}
137
+ onClick={() => {
138
+ onLanguageChange(code);
139
+ setIsLangOpen(false);
140
+ }}
141
+ className={`w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-bg-tertiary transition-colors transition-theme ${
142
+ currentLanguage === code ? 'bg-bg-tertiary text-primary-light' : 'text-text-secondary'
143
+ }`}
144
+ type="button"
145
+ >
146
+ <span className="text-xl">{flag}</span>
147
+ <span className="text-sm">{name}</span>
148
+ {currentLanguage === code && (
149
+ <span className="ml-auto text-primary-light">✓</span>
150
+ )}
151
+ </button>
152
+ ))}
153
+ </div>
154
+ )}
155
+ </div>
156
+
157
+ {/* Theme Toggle */}
158
+ <button
159
+ onClick={onThemeToggle}
160
+ 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"
161
+ title={translations.switchToMode(theme === 'dark' ? 'light' : 'dark')}
162
+ aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
163
+ type="button"
164
+ >
165
+ {theme === 'dark' ? (
166
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
167
+ <circle cx="12" cy="12" r="4" />
168
+ <path d="M12 2v2" />
169
+ <path d="M12 20v2" />
170
+ <path d="m4.93 4.93 1.41 1.41" />
171
+ <path d="m17.66 17.66 1.41 1.41" />
172
+ <path d="M2 12h2" />
173
+ <path d="M20 12h2" />
174
+ <path d="m6.34 17.66-1.41 1.41" />
175
+ <path d="m19.07 4.93-1.41 1.41" />
176
+ </svg>
177
+ ) : (
178
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
179
+ <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
180
+ </svg>
181
+ )}
182
+ </button>
183
+
184
+ {/* GitHub */}
185
+ {githubUrl && (
186
+ <a
187
+ href={githubUrl}
188
+ target="_blank"
189
+ rel="noopener noreferrer"
190
+ className="hidden md: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"
191
+ >
192
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
193
+ <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"/>
194
+ </svg>
195
+ <span className="font-medium">{githubLabel}</span>
196
+ </a>
197
+ )}
198
+
199
+ {/* Mobile Button */}
200
+ <button
201
+ onClick={() => setIsOpen(!isOpen)}
202
+ className="md:hidden text-text-secondary"
203
+ type="button"
204
+ aria-label="Toggle menu"
205
+ >
206
+ {isOpen ? (
207
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
208
+ <path d="M18 6L6 18M6 6l12 12" />
209
+ </svg>
210
+ ) : (
211
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
212
+ <line x1="4" x2="20" y1="12" y2="12" />
213
+ <line x1="4" x2="20" y1="6" y2="6" />
214
+ <line x1="4" x2="20" y1="18" y2="18" />
215
+ </svg>
216
+ )}
217
+ </button>
218
+ </div>
219
+ </div>
220
+ </div>
221
+
222
+ {/* Mobile Menu */}
223
+ {isOpen && (
224
+ <div className="md:hidden bg-bg-secondary border-t border-border transition-theme">
225
+ <div className="px-4 py-4 space-y-2">
226
+ {/* Theme Toggle Mobile */}
227
+ <button
228
+ onClick={onThemeToggle}
229
+ 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"
230
+ type="button"
231
+ >
232
+ {theme === 'dark' ? (
233
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
234
+ <circle cx="12" cy="12" r="4" />
235
+ <path d="M12 2v2" />
236
+ <path d="M12 20v2" />
237
+ <path d="m4.93 4.93 1.41 1.41" />
238
+ <path d="m17.66 17.66 1.41 1.41" />
239
+ <path d="M2 12h2" />
240
+ <path d="M20 12h2" />
241
+ <path d="m6.34 17.66-1.41 1.41" />
242
+ <path d="m19.07 4.93-1.41 1.41" />
243
+ </svg>
244
+ ) : (
245
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
246
+ <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
247
+ </svg>
248
+ )}
249
+ <span>{theme === 'dark' ? translations.lightMode : translations.darkMode}</span>
250
+ </button>
251
+
252
+ {/* Language Selector Mobile */}
253
+ <div className="px-4 py-2">
254
+ <div className="text-text-secondary text-sm mb-2">{translations.language}</div>
255
+ <div className="grid grid-cols-2 gap-2">
256
+ {Object.entries(supportedLanguages).map(([code, { name, flag }]) => (
257
+ <button
258
+ key={code}
259
+ onClick={() => onLanguageChange(code)}
260
+ className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-all transition-theme ${
261
+ currentLanguage === code
262
+ ? 'bg-primary-light text-text-primary'
263
+ : 'bg-bg-primary text-text-secondary hover:bg-bg-tertiary'
264
+ }`}
265
+ type="button"
266
+ >
267
+ <span>{flag}</span>
268
+ <span>{name}</span>
269
+ </button>
270
+ ))}
271
+ </div>
272
+ </div>
273
+
274
+ {navItemsMemo.map((item) => {
275
+ const isActive = location.pathname === item.path;
276
+ return (
277
+ <Link
278
+ key={item.path}
279
+ to={item.path}
280
+ className={`block px-4 py-2 rounded-lg font-medium transition-theme ${
281
+ isActive ? 'text-primary-light bg-bg-tertiary' : 'text-text-secondary hover:bg-bg-tertiary'
282
+ }`}
283
+ onClick={() => setIsOpen(false)}
284
+ >
285
+ {item.name}
286
+ </Link>
287
+ );
288
+ })}
289
+
290
+ {githubUrl && (
291
+ <a
292
+ href={githubUrl}
293
+ target="_blank"
294
+ rel="noopener noreferrer"
295
+ 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"
296
+ >
297
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
298
+ <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"/>
299
+ </svg>
300
+ <span>{githubLabel}</span>
301
+ </a>
302
+ )}
303
+ </div>
304
+ </div>
305
+ )}
306
+ </nav>
307
+ );
308
+ };
@@ -149,3 +149,7 @@ export type { FilterBarProps, Category, SortOption } from './FilterBar';
149
149
 
150
150
  export { FilterSidebar } from './FilterSidebar';
151
151
  export type { FilterSidebarProps } from './FilterSidebar';
152
+
153
+ // NEW: Navigation Components
154
+ export { MainNavbar } from './MainNavbar';
155
+ export type { MainNavbarProps, NavItem, MainNavbarLanguage } from './MainNavbar';