Skip to content

Commit 2370f10

Browse files
Copilot0xrinegade
andcommitted
Implement comprehensive theme system with 10 themes and redesigned NetworkSelector
Co-authored-by: 0xrinegade <[email protected]>
1 parent 9378e2a commit 2370f10

17 files changed

+7607
-1712
lines changed

package-lock.json

Lines changed: 4057 additions & 1540 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/Layout.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { AppContext } from '@/contexts/AppContext';
1313
// Import components
1414
import { NetworkSelector } from '@/components/NetworkSelector';
1515
import LanguageSelector from '@/components/LanguageSelector';
16-
import ThemeToggle from '@/components/ThemeToggle';
16+
import ThemeSelector from '@/components/ThemeSelector';
1717
import OnboardingModal from '@/components/OnboardingModal';
1818
import PWAInstallButton from '@/components/PWAInstallButton';
1919
import OfflineIndicator from '@/components/OfflineIndicator';
@@ -148,6 +148,9 @@ export default function Layout({ children, title = 'OpenSVM P2P Exchange' }) {
148148

149149
{/* Header Controls - Simplified and ASCII styled */}
150150
<div className="ascii-header-controls">
151+
{/* Theme selector */}
152+
<ThemeSelector />
153+
151154
{/* Network selector */}
152155
<NetworkSelector
153156
networks={networks}

src/components/NetworkSelector.js

Lines changed: 23 additions & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
22

33
/**
44
* NetworkSelector component for selecting between different SVM networks
5+
* Now using ASCII dropdown style matching ProfileDropdown
56
*
67
* @param {Object} props - Component props
78
* @param {Object} props.networks - Object containing network configurations
@@ -11,210 +12,62 @@ import React, { useState, useRef, useEffect } from 'react';
1112
*/
1213
export const NetworkSelector = ({ networks, selectedNetwork, onSelectNetwork }) => {
1314
const [isOpen, setIsOpen] = useState(false);
14-
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 });
15-
const [focusedIndex, setFocusedIndex] = useState(-1);
1615
const dropdownRef = useRef(null);
17-
const buttonRef = useRef(null);
18-
const optionRefs = useRef([]);
1916
const network = networks[selectedNetwork];
2017

2118
const networkKeys = Object.keys(networks);
2219

23-
// Custom SVG icon component based on provided SVG file
24-
const DropdownIcon = ({ className }) => (
25-
<svg
26-
className={className}
27-
width="24"
28-
height="24"
29-
viewBox="0 0 900 762"
30-
fill="currentColor"
31-
xmlns="http://www.w3.org/2000/svg"
32-
aria-hidden="true"
33-
>
34-
<g transform="translate(0.000000,762.000000) scale(0.100000,-0.100000)">
35-
<path d="M4185 6864 c-207 -16 -356 -44 -640 -119 -59 -16 -187 -60 -230 -80 -16 -7 -55 -23 -85 -35 -67 -26 -277 -132 -331 -166 -21 -13 -41 -24 -45 -24 -3 0 -23 -13 -44 -30 -21 -16 -40 -30 -44 -30 -3 0 -25 -14 -48 -30 -24 -17 -54 -38 -68 -47 -135 -90 -407 -342 -536 -495 -98 -117 -264 -346 -264 -365 0 -3 -6 -14 -14 -22 -59 -69 -232 -440 -290 -625 -36 -114 -53 -176 -71 -256 -9 -41 -21 -95 -27 -120 -28 -127 -48 -353 -48 -564 0 -296 20 -466 87 -731 14 -55 29 -116 33 -135 12 -50 58 -180 75 -212 8 -14 15 -33 15 -42 0 -8 9 -32 19 -53 11 -21 31 -65 46 -98 14 -33 32 -71 39 -85 68 -130 155 -275 198 -331 15 -20 28 -40 28 -43 0 -9 90 -125 179 -231 134 -158 325 -338 491 -461 65 -49 210 -147 230 -157 8 -4 38 -21 65 -37 162 -97 425 -213 615 -270 144 -43 166 -49 393 -95 250 -50 756 -51 1002 0 39 8 106 21 150 30 76 15 155 35 220 57 17 5 64 20 106 33 42 13 85 28 95 33 11 6 60 27 109 47 183 76 300 138 486 261 295 194 540 425 760 719 77 102 139 192 139 201 0 2 12 23 27 47 52 80 138 246 180 347 68 162 93 229 93 247 0 11 4 23 9 28 12 13 57 184 95 365 52 249 64 676 27 955 -12 83 -25 168 -30 190 -5 22 -19 81 -31 130 -35 151 -115 400 -149 463 -6 9 -24 49 -41 87 -35 79 -32 72 -90 180 -224 413 -554 779 -945 1048 -93 64 -120 81 -145 93 -8 4 -33 19 -55 33 -98 61 -364 184 -490 226 -39 13 -81 28 -95 33 -78 32 -354 94 -520 117 -110 16 -521 28 -635 19z m500 -225 c39 -5 102 -14 140 -19 445 -60 913 -257 1285 -540 119 -91 147 -115 295 -265 301 -303 490 -600 650 -1025 19 -51 72 -230 79 -265 3 -16 14 -71 25 -122 59 -269 76 -637 41 -860 -6 -37 -18 -111 -26 -163 -8 -52 -21 -120 -28 -150 -8 -30 -18 -73 -24 -95 -20 -82 -92 -296 -121 -359 -180 -396 -348 -642 -626 -919 -104 -103 -348 -307 -368 -307 -2 0 -35 -20 -73 -45 -37 -25 -74 -45 -82 -45 -8 0 -26 -6 -40 -14 -120 -65 -422 -65 -727 -1 -337 70 -409 79 -670 79 -192 0 -268 -3 -330 -16 -44 -9 -123 -24 -175 -33 -52 -9 -126 -23 -165 -31 -177 -35 -260 -44 -410 -44 -254 0 -318 22 -575 197 -73 50 -260 216 -365 325 -99 102 -260 299 -312 383 -123 196 -206 356 -275 535 -54 137 -108 307 -108 337 0 12 -5 34 -11 50 -12 32 -41 204 -60 353 -16 128 -16 411 0 540 18 145 52 343 62 362 5 9 9 26 9 38 0 36 92 323 131 409 119 261 256 494 385 653 33 40 70 86 83 103 52 63 272 275 367 351 159 128 336 239 519 328 55 27 108 53 118 58 9 4 43 18 75 29 31 11 71 27 87 34 30 14 186 61 280 85 96 24 207 43 405 70 84 11 456 11 535 -1z"/>
36-
<path d="M4837 4045 c-233 -234 -431 -425 -439 -425 -8 0 -196 182 -419 405 -222 223 -411 405 -420 405 -16 0 -159 -141 -159 -157 0 -14 978 -987 995 -991 19 -4 1034 1006 1035 1028 0 18 -139 160 -157 160 -6 0 -202 -191 -436 -425z"/>
37-
</g>
38-
</svg>
39-
);
40-
41-
// Calculate dropdown position
42-
const calculateDropdownPosition = () => {
43-
if (buttonRef.current) {
44-
const buttonRect = buttonRef.current.getBoundingClientRect();
45-
const dropdownWidth = 200; // Approximate dropdown width
46-
const dropdownHeight = 300; // Max dropdown height
47-
48-
let top = buttonRect.bottom + 4;
49-
let left = buttonRect.right - dropdownWidth;
50-
51-
// Adjust if dropdown would go off screen
52-
if (left < 8) {
53-
left = buttonRect.left;
54-
}
55-
56-
if (top + dropdownHeight > window.innerHeight - 8) {
57-
top = buttonRect.top - dropdownHeight - 4;
58-
}
59-
60-
setDropdownPosition({ top, left });
61-
}
62-
};
63-
6420
// Close dropdown when clicking outside
6521
useEffect(() => {
66-
const handleClickOutside = (event) => {
22+
function handleClickOutside(event) {
6723
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
6824
setIsOpen(false);
69-
setFocusedIndex(-1);
7025
}
71-
};
26+
}
7227

7328
document.addEventListener('mousedown', handleClickOutside);
7429
return () => {
7530
document.removeEventListener('mousedown', handleClickOutside);
7631
};
7732
}, []);
78-
79-
// Focus management for accessibility
80-
useEffect(() => {
81-
if (isOpen && focusedIndex >= 0 && optionRefs.current[focusedIndex]) {
82-
optionRefs.current[focusedIndex].focus();
83-
}
84-
}, [isOpen, focusedIndex]);
85-
86-
// Update position when opening dropdown
87-
useEffect(() => {
88-
if (isOpen) {
89-
calculateDropdownPosition();
90-
window.addEventListener('resize', calculateDropdownPosition);
91-
window.addEventListener('scroll', calculateDropdownPosition);
92-
93-
return () => {
94-
window.removeEventListener('resize', calculateDropdownPosition);
95-
window.removeEventListener('scroll', calculateDropdownPosition);
96-
};
97-
}
98-
}, [isOpen]);
99-
100-
// Keyboard navigation
101-
const handleKeyDown = (event) => {
102-
switch (event.key) {
103-
case 'ArrowDown':
104-
event.preventDefault();
105-
if (!isOpen) {
106-
setIsOpen(true);
107-
calculateDropdownPosition();
108-
setFocusedIndex(0);
109-
} else if (networkKeys.length > 0) {
110-
setFocusedIndex(prev =>
111-
prev < networkKeys.length - 1 ? prev + 1 : 0
112-
);
113-
}
114-
break;
115-
case 'ArrowUp':
116-
event.preventDefault();
117-
if (isOpen && networkKeys.length > 0) {
118-
setFocusedIndex(prev =>
119-
prev > 0 ? prev - 1 : networkKeys.length - 1
120-
);
121-
}
122-
break;
123-
case 'Enter':
124-
case ' ':
125-
event.preventDefault();
126-
if (!isOpen) {
127-
setIsOpen(true);
128-
calculateDropdownPosition();
129-
setFocusedIndex(0);
130-
} else if (focusedIndex >= 0 && networkKeys[focusedIndex]) {
131-
handleNetworkSelect(networkKeys[focusedIndex]);
132-
}
133-
break;
134-
case 'Escape':
135-
event.preventDefault();
136-
setIsOpen(false);
137-
setFocusedIndex(-1);
138-
buttonRef.current?.focus();
139-
break;
140-
default:
141-
break;
142-
}
143-
};
14433

14534
const handleNetworkSelect = (networkKey) => {
14635
onSelectNetwork(networkKey);
14736
setIsOpen(false);
148-
setFocusedIndex(-1);
149-
buttonRef.current?.focus(); // Return focus to trigger button
150-
};
151-
152-
const handleToggleDropdown = () => {
153-
if (!isOpen) {
154-
calculateDropdownPosition();
155-
setFocusedIndex(0);
156-
} else {
157-
setFocusedIndex(-1);
158-
}
159-
setIsOpen(!isOpen);
16037
};
16138

16239
return (
163-
<div className="network-selector" ref={dropdownRef}>
40+
<div className="ascii-dropdown-container" ref={dropdownRef}>
16441
<button
165-
ref={buttonRef}
166-
className="network-selector-button"
167-
onClick={handleToggleDropdown}
168-
onKeyDown={handleKeyDown}
42+
className="ascii-header-control ascii-dropdown-trigger"
43+
onClick={() => setIsOpen(!isOpen)}
16944
aria-expanded={isOpen}
170-
aria-haspopup="listbox"
171-
aria-label={`Current network: ${network.name}. Press Enter or Space to open network options`}
45+
aria-haspopup="true"
46+
aria-label={`Current network: ${network.name}`}
17247
>
17348
<div
17449
className="network-indicator"
17550
style={{ backgroundColor: network.color }}
17651
/>
177-
<span className="network-selector-text">{network.name}</span>
178-
<DropdownIcon
179-
className={`network-dropdown-icon ${isOpen ? 'open' : ''}`}
180-
/>
52+
{network.name}
18153
</button>
18254

18355
{isOpen && (
184-
<>
185-
<div className="dropdown-backdrop" onClick={() => setIsOpen(false)} />
186-
<div
187-
className="network-selector-dropdown"
188-
role="listbox"
189-
style={{
190-
position: 'fixed',
191-
top: `${dropdownPosition.top}px`,
192-
left: `${dropdownPosition.left}px`,
193-
zIndex: 99999
194-
}}
195-
>
196-
{Object.entries(networks).map(([key, network], index) => (
56+
<div className="ascii-dropdown-menu">
57+
{Object.entries(networks).map(([key, networkOption]) => (
58+
<button
59+
key={key}
60+
className={`ascii-dropdown-item ${key === selectedNetwork ? 'active' : ''}`}
61+
onClick={() => handleNetworkSelect(key)}
62+
>
19763
<div
198-
key={key}
199-
ref={el => optionRefs.current[index] = el}
200-
className={`network-option ${key === selectedNetwork ? 'active' : ''} ${
201-
focusedIndex === index ? 'focused' : ''
202-
}`}
203-
onClick={() => handleNetworkSelect(key)}
204-
onKeyDown={handleKeyDown}
205-
role="option"
206-
aria-selected={key === selectedNetwork}
207-
tabIndex={-1}
208-
>
209-
<div
210-
className="network-indicator"
211-
style={{ backgroundColor: network.color }}
212-
/>
213-
<span className="network-option-name">{network.name}</span>
214-
</div>
215-
))}
216-
</div>
217-
</>
64+
className="network-indicator"
65+
style={{ backgroundColor: networkOption.color }}
66+
/>
67+
{networkOption.name}
68+
</button>
69+
))}
70+
</div>
21871
)}
21972
</div>
22073
);

src/components/ThemeSelector.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import React, { useState, useRef, useEffect } from 'react';
2+
3+
const ThemeSelector = () => {
4+
const [selectedTheme, setSelectedTheme] = useState('grayscale');
5+
const [isOpen, setIsOpen] = useState(false);
6+
const dropdownRef = useRef(null);
7+
8+
// Available themes
9+
const themes = [
10+
{ key: 'grayscale', label: 'GRAYSCALE', description: 'Monospace ASCII terminal style' },
11+
{ key: 'corporate', label: 'CORPORATE', description: 'Clean blue professional design' },
12+
{ key: 'retro', label: 'RETRO', description: '80s neon cyberpunk aesthetic' },
13+
{ key: 'terminal', label: 'TERMINAL', description: 'Green on black hacker style' },
14+
{ key: 'minimal', label: 'MINIMAL', description: 'Clean whitespace design' },
15+
{ key: 'cyberpunk', label: 'CYBERPUNK', description: 'Dark futuristic interface' },
16+
{ key: 'organic', label: 'ORGANIC', description: 'Earth tones natural design' },
17+
{ key: 'high-contrast', label: 'HIGH CONTRAST', description: 'Accessibility black/white' },
18+
{ key: 'pastel', label: 'PASTEL', description: 'Soft colors gentle design' },
19+
{ key: 'blueprint', label: 'BLUEPRINT', description: 'Technical drawing style' },
20+
];
21+
22+
useEffect(() => {
23+
// Load saved theme from localStorage
24+
const savedTheme = localStorage.getItem('theme') || 'grayscale';
25+
setSelectedTheme(savedTheme);
26+
applyTheme(savedTheme);
27+
}, []);
28+
29+
// Close dropdown when clicking outside
30+
useEffect(() => {
31+
function handleClickOutside(event) {
32+
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
33+
setIsOpen(false);
34+
}
35+
}
36+
37+
document.addEventListener('mousedown', handleClickOutside);
38+
return () => {
39+
document.removeEventListener('mousedown', handleClickOutside);
40+
};
41+
}, []);
42+
43+
const applyTheme = (themeKey) => {
44+
const root = document.documentElement;
45+
const body = document.body;
46+
47+
// Remove all existing theme classes
48+
themes.forEach(theme => {
49+
root.classList.remove(`theme-${theme.key}`);
50+
body.classList.remove(`theme-${theme.key}`);
51+
});
52+
53+
// Add new theme class
54+
root.classList.add(`theme-${themeKey}`);
55+
body.classList.add(`theme-${themeKey}`);
56+
57+
// Set theme attribute for CSS selectors
58+
root.setAttribute('data-theme', themeKey);
59+
body.setAttribute('data-theme', themeKey);
60+
};
61+
62+
const handleThemeSelect = (themeKey) => {
63+
setSelectedTheme(themeKey);
64+
localStorage.setItem('theme', themeKey);
65+
applyTheme(themeKey);
66+
setIsOpen(false);
67+
};
68+
69+
const currentTheme = themes.find(theme => theme.key === selectedTheme);
70+
71+
return (
72+
<div className="ascii-dropdown-container" ref={dropdownRef}>
73+
<button
74+
className="ascii-header-control ascii-dropdown-trigger"
75+
onClick={() => setIsOpen(!isOpen)}
76+
aria-expanded={isOpen}
77+
aria-haspopup="true"
78+
aria-label={`Current theme: ${currentTheme?.label}`}
79+
>
80+
{currentTheme?.label}
81+
</button>
82+
83+
{isOpen && (
84+
<div className="ascii-dropdown-menu theme-selector-menu">
85+
{themes.map((theme) => (
86+
<button
87+
key={theme.key}
88+
className={`ascii-dropdown-item theme-option ${theme.key === selectedTheme ? 'active' : ''}`}
89+
onClick={() => handleThemeSelect(theme.key)}
90+
>
91+
<div className="theme-option-content">
92+
<span className="theme-name">{theme.label}</span>
93+
<span className="theme-description">{theme.description}</span>
94+
</div>
95+
</button>
96+
))}
97+
</div>
98+
)}
99+
</div>
100+
);
101+
};
102+
103+
export default ThemeSelector;

src/index.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* Import ASCII theme and component styles */
22
@import './styles/ascii-theme.css';
3+
@import './styles/themes/index.css';
34
@import './styles/responsive.css';
45
@import './styles/components.css';
56
@import './styles/wallet-status.css';

src/styles/header-controls.css

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,13 @@
4242
/* ===== Z-INDEX MANAGEMENT ===== */
4343
/* Ensure dropdowns appear above all other content */
4444
.network-selector-dropdown,
45-
.language-selector-dropdown {
45+
.language-selector-dropdown,
46+
.ascii-dropdown-menu {
47+
z-index: 99999 !important;
48+
}
49+
50+
/* Ensure theme selector dropdown appears above all content */
51+
.theme-selector-menu {
4652
z-index: 99999 !important;
4753
}
4854

0 commit comments

Comments
 (0)