feat(entropy): Enhance entropy generation UX and fix resets

This commit introduces several improvements to the entropy generation and application state management:

1.  **Implement Dice Entropy Stats Panel:**
    - After generating entropy from dice rolls, a detailed statistics panel is now displayed for user review.
    - This panel includes roll distribution, chi-square analysis, and a preview of the generated seed.
    - Users can now choose to "Continue with this Seed" or "Roll Again" to discard and restart, improving user control and confidence in the entropy quality.

2.  **Fix UI Layering and Overflow:**
    - Increased the header's `z-index` to `z-[100]` to ensure it always remains on top of other components, fixing an issue where the "Reset All" button was inaccessible.
    - Made the main content area for entropy components scrollable to prevent the new stats panels from overflowing the viewport on smaller screens.

3.  **Improve "Reset All" Functionality:**
    - The "Reset All" button now correctly resets the internal state of the `DiceEntropy` and `CameraEntropy` components.
    - This is achieved by adding a `resetCounter` to the `App` state and passing it into the `key` prop of the entropy components, forcing a full remount on reset.
This commit is contained in:
LC mac
2026-02-10 23:02:13 +08:00
parent a67a2159f2
commit f52186f2e7
3 changed files with 108 additions and 51 deletions

View File

@@ -76,6 +76,7 @@ function App() {
const [sessionItems, setSessionItems] = useState<StorageItem[]>([]); const [sessionItems, setSessionItems] = useState<StorageItem[]>([]);
const [clipboardEvents, setClipboardEvents] = useState<ClipboardEvent[]>([]); const [clipboardEvents, setClipboardEvents] = useState<ClipboardEvent[]>([]);
const [showLockConfirm, setShowLockConfirm] = useState(false); const [showLockConfirm, setShowLockConfirm] = useState(false);
const [resetCounter, setResetCounter] = useState(0);
// Krux integration state // Krux integration state
const [encryptionMode, setEncryptionMode] = useState<'pgp' | 'krux' | 'seedqr'>('pgp'); const [encryptionMode, setEncryptionMode] = useState<'pgp' | 'krux' | 'seedqr'>('pgp');
@@ -467,17 +468,17 @@ function App() {
const handleToggleNetwork = () => { const handleToggleNetwork = () => {
setIsNetworkBlocked(!isNetworkBlocked); setIsNetworkBlocked(!isNetworkBlocked);
if (!isNetworkBlocked) { if (!isNetworkBlocked) {
// Block network // Block network
console.log('🚫 Network BLOCKED - No external requests allowed'); console.log('🚫 Network BLOCKED - No external requests allowed');
// Optional: Override fetch/XMLHttpRequest // Optional: Override fetch/XMLHttpRequest
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
(window as any).__originalFetch = window.fetch; (window as any).__originalFetch = window.fetch;
// Create a mock fetch function with proper type assertion // Create a mock fetch function with proper type assertion
const mockFetch = (async () => Promise.reject(new Error('Network blocked by user'))) as unknown as typeof window.fetch; const mockFetch = (async () => Promise.reject(new Error('Network blocked by user'))) as unknown as typeof window.fetch;
window.fetch = mockFetch; window.fetch = mockFetch;
} }
} else { } else {
// Unblock network // Unblock network
console.log('🌐 Network ACTIVE'); console.log('🌐 Network ACTIVE');
@@ -494,8 +495,8 @@ function App() {
}; };
const handleResetAll = () => { const handleResetAll = () => {
if (window.confirm("Reset entire app? This will clear all seeds, passwords, and generated data.")) { if (window.confirm('⚠️ Reset ALL data? This will clear everything including any displayed entropy analysis.')) {
// Clear all state // Clear component state
setMnemonic(''); setMnemonic('');
setGeneratedSeed(''); setGeneratedSeed('');
setBackupMessagePassword(''); setBackupMessagePassword('');
@@ -508,15 +509,23 @@ function App() {
setRestoreInput(''); setRestoreInput('');
setDecryptedRestoredMnemonic(null); setDecryptedRestoredMnemonic(null);
setError(''); setError('');
setEntropySource(null);
setEntropyStats(null);
setSeedForBlender(''); setSeedForBlender('');
// Clear session
// Clear storage and session
localStorage.clear();
sessionStorage.clear();
destroySessionKey(); destroySessionKey();
_setEncryptedMnemonicCache(null); _setEncryptedMnemonicCache(null);
// Go to Create tab (fresh start)
// Force SeedBlender to remount (resets its internal state) // Force remount of key-driven components
setResetCounter(prev => prev + 1);
setBlenderResetKey(prev => prev + 1); setBlenderResetKey(prev => prev + 1);
// Go to Create tab (fresh start)
setActiveTab('create'); setActiveTab('create');
console.log('✅ All data reset');
} }
}; };
@@ -698,6 +707,7 @@ function App() {
{/* Camera Entropy Component */} {/* Camera Entropy Component */}
{entropySource === 'camera' && !generatedSeed && ( {entropySource === 'camera' && !generatedSeed && (
<CameraEntropy <CameraEntropy
key={`camera-${resetCounter}`} // Force remount on reset
wordCount={seedWordCount} wordCount={seedWordCount}
onEntropyGenerated={handleEntropyGenerated} onEntropyGenerated={handleEntropyGenerated}
onCancel={() => setEntropySource(null)} onCancel={() => setEntropySource(null)}
@@ -708,6 +718,7 @@ function App() {
{/* Dice Entropy Component */} {/* Dice Entropy Component */}
{entropySource === 'dice' && !generatedSeed && ( {entropySource === 'dice' && !generatedSeed && (
<DiceEntropy <DiceEntropy
key={`dice-${resetCounter}`} // Force remount on reset
wordCount={seedWordCount} wordCount={seedWordCount}
onEntropyGenerated={handleEntropyGenerated} onEntropyGenerated={handleEntropyGenerated}
onCancel={() => setEntropySource(null)} onCancel={() => setEntropySource(null)}
@@ -735,7 +746,7 @@ function App() {
</span> </span>
</div> </div>
<div className="relative"> <div className="relative">
<p <p
className="font-mono text-xs text-[#39ff14] break-words leading-relaxed blur-sensitive" className="font-mono text-xs text-[#39ff14] break-words leading-relaxed blur-sensitive"
title="Hover to reveal seed" title="Hover to reveal seed"
style={{ textShadow: '0 0 5px rgba(57,255,20,0.5)' }} style={{ textShadow: '0 0 5px rgba(57,255,20,0.5)' }}
@@ -793,9 +804,8 @@ function App() {
onFocus={(e) => e.target.classList.remove('blur-sensitive')} onFocus={(e) => e.target.classList.remove('blur-sensitive')}
onBlur={(e) => mnemonic && e.target.classList.add('blur-sensitive')} onBlur={(e) => mnemonic && e.target.classList.add('blur-sensitive')}
placeholder="Enter your 12 or 24 word seed phrase..." placeholder="Enter your 12 or 24 word seed phrase..."
className={`w-full h-32 p-3 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg font-mono text-sm text-[#00f0ff] placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all relative overflow-hidden ${ className={`w-full h-32 p-3 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg font-mono text-sm text-[#00f0ff] placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all relative overflow-hidden ${mnemonic ? 'blur-sensitive' : ''
mnemonic ? 'blur-sensitive' : '' }`}
}`}
style={{ style={{
backgroundImage: 'repeating-linear-gradient(0deg, rgba(0,240,255,0.03) 0px, transparent 1px, transparent 2px, rgba(0,240,255,0.03) 3px)', backgroundImage: 'repeating-linear-gradient(0deg, rgba(0,240,255,0.03) 0px, transparent 1px, transparent 2px, rgba(0,240,255,0.03) 3px)',
textShadow: '0 0 5px rgba(0,240,255,0.5)' textShadow: '0 0 5px rgba(0,240,255,0.5)'
@@ -1123,7 +1133,7 @@ function App() {
<div className="p-4 bg-[#0a0a0f] rounded-xl border-2 border-[#39ff14] shadow-[0_0_20px_rgba(57,255,20,0.3)]"> <div className="p-4 bg-[#0a0a0f] rounded-xl border-2 border-[#39ff14] shadow-[0_0_20px_rgba(57,255,20,0.3)]">
<div className="relative"> <div className="relative">
<p <p
className="font-mono text-center text-base break-words text-[#39ff14] blur-sensitive" className="font-mono text-center text-base break-words text-[#39ff14] blur-sensitive"
title="Hover to reveal" title="Hover to reveal"
style={{ textShadow: '0 0 8px rgba(57,255,20,0.8)' }} style={{ textShadow: '0 0 8px rgba(57,255,20,0.8)' }}

View File

@@ -28,6 +28,7 @@ const DiceEntropy: React.FC<DiceEntropyProps> = ({
const [error, setError] = useState(''); const [error, setError] = useState('');
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
const [stats, setStats] = useState<DiceStats | null>(null); const [stats, setStats] = useState<DiceStats | null>(null);
const [generatedMnemonic, setGeneratedMnemonic] = useState<string>('');
const validateDiceRolls = (input: string): { valid: boolean; error: string } => { const validateDiceRolls = (input: string): { valid: boolean; error: string } => {
const clean = input.replace(/\s/g, ''); const clean = input.replace(/\s/g, '');
@@ -118,14 +119,11 @@ const DiceEntropy: React.FC<DiceEntropyProps> = ({
// Generate mnemonic // Generate mnemonic
const mnemonic = await generateMnemonicFromDice(clean); const mnemonic = await generateMnemonicFromDice(clean);
// Show stats first // Show stats FIRST
setStats(diceStats); setStats(diceStats);
setGeneratedMnemonic(mnemonic); // Store mnemonic for later
setProcessing(false); setProcessing(false);
// DON'T call onEntropyGenerated yet - let user review stats first
// Then notify parent after a brief delay so user sees stats
setTimeout(() => {
onEntropyGenerated(mnemonic, diceStats);
}, 100);
}; };
const generateMnemonicFromDice = async (diceRolls: string): Promise<string> => { const generateMnemonicFromDice = async (diceRolls: string): Promise<string> => {
@@ -152,8 +150,9 @@ const DiceEntropy: React.FC<DiceEntropyProps> = ({
}; };
return ( return (
<div className="space-y-4"> <div className="space-y-4 max-h-[calc(100vh-200px)] overflow-y-auto">
{!stats && ( {/* INPUT FORM - Show only when stats are NOT shown */}
{!stats && !processing && (
<> <>
<div className="p-4 bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/30 space-y-3"> <div className="p-4 bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/30 space-y-3">
<div className="flex items-center gap-2"><Dices size={20} className="text-[#00f0ff]" /><h3 className="text-sm font-bold text-[#00f0ff] uppercase">Dice Roll Entropy</h3></div> <div className="flex items-center gap-2"><Dices size={20} className="text-[#00f0ff]" /><h3 className="text-sm font-bold text-[#00f0ff] uppercase">Dice Roll Entropy</h3></div>
@@ -168,7 +167,7 @@ const DiceEntropy: React.FC<DiceEntropyProps> = ({
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest">Enter Dice Rolls</label> <label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest">Enter Dice Rolls</label>
<textarea value={rolls} onChange={(e) => { setRolls(e.target.value.replace(/[^1-6\s]/g, '')); setError(''); }} placeholder="99+ dice rolls (e.g., 16345...)" className="w-full h-32 p-3 bg-[#0a0a0f] border-2 border-[#00f0ff]/50 rounded-lg font-mono text-sm text-[#00f0ff] placeholder:text-[10px] placeholder:text-[#6ef3f7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all resize-none" /> <textarea value={rolls} onChange={(e) => { setRolls(e.target.value.replace(/[^1-6\s]/g, '')); setError(''); }} placeholder="99+ dice rolls (e.g., 16345...)" className="w-full h-32 p-3 bg-[#0a0a0f] border-2 border-[#00f0ff]/50 rounded-lg font-mono text-xs text-[#00f0ff] placeholder:text-[10px] placeholder:text-[#6ef3f7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all resize-none" />
<p className="text-[10px] text-[#6ef3f7]">Current: {rolls.replace(/\s/g, '').length} rolls {rolls.replace(/\s/g, '').length >= 99 && ' ✓'}</p> <p className="text-[10px] text-[#6ef3f7]">Current: {rolls.replace(/\s/g, '').length} rolls {rolls.replace(/\s/g, '').length >= 99 && ' ✓'}</p>
</div> </div>
{error && (<div className="flex items-start gap-2 p-3 bg-[#0a0a0f] border border-[#ff006e] rounded-lg"><AlertCircle size={16} className="text-[#ff006e] shrink-0 mt-0.5" /><p className="text-xs text-[#ff006e]">{error}</p></div>)} {error && (<div className="flex items-start gap-2 p-3 bg-[#0a0a0f] border border-[#ff006e] rounded-lg"><AlertCircle size={16} className="text-[#ff006e] shrink-0 mt-0.5" /><p className="text-xs text-[#ff006e]">{error}</p></div>)}
@@ -184,16 +183,25 @@ const DiceEntropy: React.FC<DiceEntropyProps> = ({
</> </>
)} )}
{stats && ( {/* PROCESSING STATE */}
<div className="p-4 bg-[#0a0a0f] rounded-xl border-2 border-[#39ff14] shadow-[0_0_30px_rgba(57,255,20,0.3)] space-y-4"> {processing && (
<div className="p-6 bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/30 text-center space-y-3">
<div className="animate-spin mx-auto w-12 h-12 border-4 border-[#00f0ff]/30 border-t-[#00f0ff] rounded-full" />
<p className="text-sm text-[#00f0ff]">Processing entropy...</p>
</div>
)}
{/* STATS DISPLAY - Show after generation */}
{stats && !processing && (
<div className="p-6 bg-[#16213e] rounded-xl border-2 border-[#39ff14] shadow-[0_0_30px_rgba(57,255,20,0.3)] space-y-4 mb-6">
<div className="flex items-center gap-2 text-[#39ff14]"><CheckCircle2 size={24} /><h3 className="text-sm font-bold uppercase">Dice Entropy Analysis</h3></div> <div className="flex items-center gap-2 text-[#39ff14]"><CheckCircle2 size={24} /><h3 className="text-sm font-bold uppercase">Dice Entropy Analysis</h3></div>
<div className="space-y-3 text-xs"> <div className="space-y-3 text-xs">
<div><p className="text-[#00f0ff] font-bold mb-1">Primary Source:</p><p className="text-[#6ef3f7]">Physical Dice Rolls</p></div> <div><p className="text-[#00f0ff] font-bold mb-1">Primary Source:</p><p className="text-[#6ef3f7]">Physical Dice Rolls</p></div>
<div> <div>
<p className="text-[#00f0ff] font-bold mb-2">ROLL STATISTICS:</p> <p className="text-[#00f0ff] font-bold mb-2">ROLL STATISTICS:</p>
<div className="grid grid-cols-2 gap-x-4 gap-y-1 font-mono text-[10px]"> <div className="grid grid-cols-2 gap-2 font-mono text-[10px]">
<div>Total rolls:</div><div className="text-[#39ff14]">{stats.length}</div> <div>Total rolls:</div><div className="text-[#39ff14]">{stats.length}</div>
<div>Chi-square test:</div><div className="text-[#39ff14]">{stats.chiSquare.toFixed(2)} (pass &lt; 15.5)</div> <div>Chi-square test:</div><div className="text-[#39ff14]">{stats.chiSquare.toFixed(2)} (pass &lt; 15)</div>
<div>Validation:</div><div className="text-[#39ff14]">{stats.passed ? '✅ Passed' : '❌ Failed'}</div> <div>Validation:</div><div className="text-[#39ff14]">{stats.passed ? '✅ Passed' : '❌ Failed'}</div>
</div> </div>
</div> </div>
@@ -215,9 +223,31 @@ const DiceEntropy: React.FC<DiceEntropyProps> = ({
</div> </div>
<p className="text-[9px] text-[#6ef3f7] mt-2">Expected: ~16.67% per face</p> <p className="text-[9px] text-[#6ef3f7] mt-2">Expected: ~16.67% per face</p>
</div> </div>
<div>
<p className="text-[#00f0ff] font-bold mb-2">GENERATED SEED:</p>
<div className="p-3 bg-[#0a0a0f] rounded-lg border border-[#39ff14]/50">
<p className="font-mono text-[10px] text-[#39ff14] blur-sensitive" title="Hover to reveal">
{generatedMnemonic}
</p>
<p className="text-[9px] text-[#6ef3f7] mt-1">
👆 Hover to reveal - Write this down securely
</p>
</div>
</div>
<div>
<p className="text-[#00f0ff] font-bold mb-2">HOW SEED IS GENERATED:</p>
<div className="space-y-1 text-[10px] text-[#6ef3f7]">
<div>1. Physical dice rolls ({stats.length} values)</div>
<div>2. Statistical validation (χ²={stats.chiSquare.toFixed(2)})</div>
<div>3. Combined with timing entropy</div>
<div>4. Mixed with {stats.interactionSamples} interaction samples</div>
<div>5. Enhanced with crypto.getRandomValues() (32 bytes)</div>
<div>6. Final hash {wordCount === 12 ? '128' : '256'} bits {wordCount} BIP39 words</div>
</div>
</div>
<div> <div>
<p className="text-[#00f0ff] font-bold mb-2">MIXED WITH:</p> <p className="text-[#00f0ff] font-bold mb-2">MIXED WITH:</p>
<div className="space-y-1 text-[#6ef3f7] text-[10px]"> <div className="space-y-1 text-[#6ef3f7]">
<div>- crypto.getRandomValues() </div> <div>- crypto.getRandomValues() </div>
<div>- performance.now() </div> <div>- performance.now() </div>
<div>- Interaction timing ({stats.interactionSamples} samples) </div> <div>- Interaction timing ({stats.interactionSamples} samples) </div>
@@ -230,6 +260,28 @@ const DiceEntropy: React.FC<DiceEntropyProps> = ({
</div> </div>
</div> </div>
</div> </div>
{/* Action Buttons */}
<div className="pt-4 border-t border-[#00f0ff]/30 space-y-3">
<button
onClick={() => {
// Send to parent
onEntropyGenerated(generatedMnemonic, stats);
}}
className="w-full py-3 bg-gradient-to-r from-[#ff006e] to-[#ff4d8f] text-white text-sm rounded-xl font-bold uppercase hover:shadow-[0_0_30px_rgba(255,0,110,0.8)] transition-all"
>
Continue with this Seed
</button>
<button
onClick={() => {
// Reset and try again
setStats(null); setGeneratedMnemonic(''); setRolls(''); setError('');
}}
className="w-full py-2 bg-[#16213e] border-2 border-[#00f0ff] text-[#00f0ff] rounded-lg text-sm font-bold hover:bg-[#00f0ff]/20 transition-all"
>
Roll Again
</button>
</div>
</div> </div>
)} )}
</div> </div>

View File

@@ -47,9 +47,9 @@ const Header: React.FC<HeaderProps> = ({
onResetAll onResetAll
}) => { }) => {
return ( return (
<header className="sticky top-0 z-50 bg-[#0a0a0f] border-b border-[#00f0ff30] backdrop-blur-sm"> <header className="sticky top-0 z-[100] bg-[#0a0a0f] border-b border-[#00f0ff30] backdrop-blur-sm">
<div className="w-full px-4 py-3 space-y-3"> <div className="w-full px-4 py-3 space-y-3">
{/* ROW 1: Logo + App Info (LEFT) | Reset (RIGHT) */} {/* ROW 1: Logo + App Info (LEFT) | Reset (RIGHT) */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -96,11 +96,10 @@ const Header: React.FC<HeaderProps> = ({
{/* Network Block toggle */} {/* Network Block toggle */}
<button <button
onClick={onToggleNetwork} onClick={onToggleNetwork}
className={`flex items-center gap-1 px-2.5 py-1.5 text-xs rounded-lg font-medium transition-all whitespace-nowrap ${ className={`flex items-center gap-1 px-2.5 py-1.5 text-xs rounded-lg font-medium transition-all whitespace-nowrap ${isNetworkBlocked
isNetworkBlocked
? 'bg-[#16213e] border border-[#ff006e] text-[#ff006e] hover:bg-[#ff006e20]' ? 'bg-[#16213e] border border-[#ff006e] text-[#ff006e] hover:bg-[#ff006e20]'
: 'bg-[#16213e] border border-[#39ff14] text-[#39ff14] hover:bg-[#39ff1420]' : 'bg-[#16213e] border border-[#39ff14] text-[#39ff14] hover:bg-[#39ff1420]'
}`} }`}
title={isNetworkBlocked ? 'Network BLOCKED' : 'Network ACTIVE'} title={isNetworkBlocked ? 'Network BLOCKED' : 'Network ACTIVE'}
> >
<span className="text-sm">{isNetworkBlocked ? '🚫' : '🌐'}</span> <span className="text-sm">{isNetworkBlocked ? '🚫' : '🌐'}</span>
@@ -114,11 +113,10 @@ const Header: React.FC<HeaderProps> = ({
{/* ROW 3: Navigation Tabs */} {/* ROW 3: Navigation Tabs */}
<div className="grid grid-cols-4 gap-2"> <div className="grid grid-cols-4 gap-2">
<button <button
className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${ className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${activeTab === 'create'
activeTab === 'create'
? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]' ? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
: 'bg-[#0a0a0f] text-[#9d84b7] border-2 border-[#00f0ff30] hover:text-[#6ef3f7] hover:border-[#00f0ff50]' : 'bg-[#0a0a0f] text-[#9d84b7] border-2 border-[#00f0ff30] hover:text-[#6ef3f7] hover:border-[#00f0ff50]'
}`} }`}
style={activeTab === 'create' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined} style={activeTab === 'create' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
onClick={() => onRequestTabChange('create')} onClick={() => onRequestTabChange('create')}
> >
@@ -126,11 +124,10 @@ const Header: React.FC<HeaderProps> = ({
</button> </button>
<button <button
className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${ className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${activeTab === 'backup'
activeTab === 'backup'
? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]' ? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
: 'bg-[#0a0a0f] text-[#9d84b7] border-2 border-[#00f0ff30] hover:text-[#6ef3f7] hover:border-[#00f0ff50]' : 'bg-[#0a0a0f] text-[#9d84b7] border-2 border-[#00f0ff30] hover:text-[#6ef3f7] hover:border-[#00f0ff50]'
}`} }`}
style={activeTab === 'backup' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined} style={activeTab === 'backup' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
onClick={() => onRequestTabChange('backup')} onClick={() => onRequestTabChange('backup')}
> >
@@ -138,11 +135,10 @@ const Header: React.FC<HeaderProps> = ({
</button> </button>
<button <button
className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${ className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${activeTab === 'restore'
activeTab === 'restore'
? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]' ? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
: 'bg-[#0a0a0f] text-[#9d84b7] border-2 border-[#00f0ff30] hover:text-[#6ef3f7] hover:border-[#00f0ff50]' : 'bg-[#0a0a0f] text-[#9d84b7] border-2 border-[#00f0ff30] hover:text-[#6ef3f7] hover:border-[#00f0ff50]'
}`} }`}
style={activeTab === 'restore' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined} style={activeTab === 'restore' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
onClick={() => onRequestTabChange('restore')} onClick={() => onRequestTabChange('restore')}
> >
@@ -150,11 +146,10 @@ const Header: React.FC<HeaderProps> = ({
</button> </button>
<button <button
className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${ className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${activeTab === 'seedblender'
activeTab === 'seedblender'
? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]' ? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
: 'bg-[#0a0a0f] text-[#9d84b7] border-2 border-[#00f0ff30] hover:text-[#6ef3f7] hover:border-[#00f0ff50]' : 'bg-[#0a0a0f] text-[#9d84b7] border-2 border-[#00f0ff30] hover:text-[#6ef3f7] hover:border-[#00f0ff50]'
}`} }`}
style={activeTab === 'seedblender' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined} style={activeTab === 'seedblender' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
onClick={() => onRequestTabChange('seedblender')} onClick={() => onRequestTabChange('seedblender')}
> >