feat(app): Add Create Seed tab and enhance Seed Blender workflow

This major update introduces a new "Create" tab for generating fresh BIP39 mnemonic seeds and significantly improves the entire application workflow, particularly the interaction with the Seed Blender.

** New Features & Enhancements**

*   **Create Seed Tab**:
    *   Add a new "Create" tab as the default view for generating 12 or 24-word BIP39 seeds.
    *   Implement a destination selector, allowing users to send the newly generated seed directly to the "Backup" tab for encryption or to the "Seed Blender" for advanced operations.
    *   The UI automatically switches to the chosen destination tab after generation for a seamless workflow.

*   **Seed Blender Integration**:
    *   Generated seeds sent to the Seed Blender are now automatically added to the list of inputs.
    *   The Seed Blender's state is now preserved when switching between tabs, preventing data loss and allowing users to accumulate seeds from the Create tab.

*   **Global Reset Functionality**:
    *   A "Reset All" button has been added to the main header for a global application reset.
    *   This action clears all component states (including the Seed Blender's internal state), passwords, generated data, and the in-memory session key, returning the app to a fresh initial state.

*   **UI/UX Polish**:
    *   The "Use This Seed for Backup" button in the Seed Blender has been restyled to match the application's cyberpunk aesthetic and its text clarified.
    *   The "Create" tab UI is cleared automatically after a seed is generated and the user is navigated away, ensuring a clean slate for the next use.

**🔒 Security Fixes**

*   **Auto-Clear Passwords**: Password and passphrase fields in both the "Backup" and "Restore" tabs are now automatically cleared from the UI and state after a successful encryption or decryption operation. This prevents sensitive data from lingering in the application.
*   **Robust Seed Generation**: The seed generation process now uses the secure `crypto.getRandomValues` Web API to generate entropy before converting it to a mnemonic.

**🐛 Bug Fixes**

*   **Seed Blender State**:
    *   Fixed a critical bug where the Seed Blender's internal state was lost when switching tabs. The component is now kept mounted but hidden via CSS.
    *   Resolved an issue where a seed sent from the "Create" tab could be added multiple times to the blender. A `useRef` guard now prevents duplicates.
    *   Corrected a race condition where transferring a blended seed to the "Backup" tab would clear the blender's state before the data could be used. The auto-clear has been removed in favor of the manual "Reset All" button.
This commit is contained in:
LC mac
2026-02-08 23:36:33 +08:00
parent 7c4fc1460c
commit a0133369b6
4 changed files with 723 additions and 440 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "seedpgp-web",
"private": true,
"version": "1.4.4",
"version": "1.4.5",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -42,7 +42,7 @@ interface ClipboardEvent {
}
function App() {
const [activeTab, setActiveTab] = useState<'backup' | 'restore' | 'seedblender'>('backup');
const [activeTab, setActiveTab] = useState<'create' | 'backup' | 'restore' | 'seedblender'>('create');
const [mnemonic, setMnemonic] = useState('');
const [backupMessagePassword, setBackupMessagePassword] = useState('');
const [restoreMessagePassword, setRestoreMessagePassword] = useState('');
@@ -76,7 +76,12 @@ function App() {
const [encryptionMode, setEncryptionMode] = useState<'pgp' | 'krux' | 'seedqr'>('pgp');
const [seedQrFormat, setSeedQrFormat] = useState<'standard' | 'compact'>('standard');
const [generatedSeed, setGeneratedSeed] = useState<string>('');
const [seedWordCount, setSeedWordCount] = useState<12 | 24>(24);
const [seedDestination, setSeedDestination] = useState<'backup' | 'seedblender'>('backup');
const [detectedMode, setDetectedMode] = useState<EncryptionMode | null>(null);
const [seedForBlender, setSeedForBlender] = useState<string>('');
const [blenderResetKey, setBlenderResetKey] = useState(0);
const SENSITIVE_PATTERNS = ['key', 'mnemonic', 'seed', 'private', 'secret', 'pgp', 'password'];
@@ -246,6 +251,45 @@ function App() {
}
};
const generateNewSeed = async () => {
try {
setLoading(true);
setError('');
// Generate random entropy
const entropyLength = seedWordCount === 12 ? 16 : 32; // 128 bits for 12 words, 256 for 24
const entropy = new Uint8Array(entropyLength);
crypto.getRandomValues(entropy);
// Convert to mnemonic using your existing lib
const { entropyToMnemonic } = await import('./lib/seedblend');
const newMnemonic = await entropyToMnemonic(entropy);
setGeneratedSeed(newMnemonic);
// Set mnemonic for backup if that's the destination
if (seedDestination === 'backup') {
setMnemonic(newMnemonic);
} else if (seedDestination === 'seedblender') {
setSeedForBlender(newMnemonic);
}
// Auto-switch to chosen destination after generation
setTimeout(() => {
setActiveTab(seedDestination);
// Reset Create tab state after switching
setTimeout(() => {
setGeneratedSeed('');
}, 300);
}, 1500);
} catch (e) {
setError(e instanceof Error ? e.message : 'Seed generation failed');
} finally {
setLoading(false);
}
};
const handleBackup = async () => {
setLoading(true);
setError('');
@@ -298,6 +342,10 @@ function App() {
const blob = await encryptJsonToBlob({ mnemonic, timestamp: Date.now() });
setEncryptedMnemonicCache(blob);
setMnemonic(''); // Clear plaintext mnemonic
// Clear password after successful encryption (security best practice)
setBackupMessagePassword('');
setPrivateKeyPassphrase(''); // Also clear PGP passphrase if used
} catch (e) {
setError(e instanceof Error ? e.message : 'Encryption failed');
} finally {
@@ -402,6 +450,12 @@ function App() {
// Temporarily display the mnemonic and then clear it
setDecryptedRestoredMnemonic(result.w);
// Clear passwords after successful decryption (security best practice)
setRestoreMessagePassword('');
setPrivateKeyPassphrase('');
// Also clear restore input after successful decrypt
setRestoreInput('');
setTimeout(() => {
setDecryptedRestoredMnemonic(null);
}, 10000); // Auto-clear after 10 seconds
@@ -446,15 +500,37 @@ function App() {
setShowLockConfirm(false);
};
const handleRequestTabChange = (newTab: 'backup' | 'restore' | 'seedblender') => {
if (activeTab === 'seedblender' && isBlenderDirty) {
if (window.confirm("You have unsaved data in the Seed Blender. Are you sure you want to leave? All progress will be lost.")) {
setActiveTab(newTab);
setIsBlenderDirty(false); // Reset dirty state on leaving
}
// else: user cancelled, do nothing
} else {
const handleRequestTabChange = (newTab: 'create' | 'backup' | 'restore' | 'seedblender') => {
// Allow free navigation - no warnings
// User can manually reset Seed Blender with "Reset All" button
setActiveTab(newTab);
};
const handleResetAll = () => {
if (window.confirm("Reset entire app? This will clear all seeds, passwords, and generated data.")) {
// Clear all state
setMnemonic('');
setGeneratedSeed('');
setBackupMessagePassword('');
setRestoreMessagePassword('');
setPublicKeyInput('');
setPrivateKeyInput('');
setPrivateKeyPassphrase('');
setQrPayload('');
setRecipientFpr('');
setRestoreInput('');
setDecryptedRestoredMnemonic(null);
setError('');
setSeedForBlender('');
setIsBlenderDirty(false);
// Clear session
destroySessionKey();
setEncryptedMnemonicCache(null);
// Go to Create tab (fresh start)
// Force SeedBlender to remount (resets its internal state)
setBlenderResetKey(prev => prev + 1);
setActiveTab('create');
}
};
@@ -509,6 +585,7 @@ function App() {
appVersion={__APP_VERSION__}
isLocked={isReadOnly}
onToggleLock={handleToggleLock}
onResetAll={handleResetAll}
/>
<main className="max-w-7xl mx-auto px-6 py-4">
<div className="bg-[#1a1a2e] rounded-xl border-2 border-[#00f0ff]/30 shadow-[0_0_30px_rgba(0,240,255,0.3)] p-8">
@@ -540,8 +617,144 @@ function App() {
{/* Main Content Grid */}
<div className="grid gap-6 md:grid-cols-3 md:items-start">
<div className="md:col-span-2 space-y-6">
{activeTab === 'backup' ? (
<div className={activeTab === 'create' ? 'block' : 'hidden'}>
<div className="space-y-6">
<div className="text-center space-y-4">
<div className="inline-flex items-center gap-2 px-4 py-2 bg-[#16213e] border border-[#00f0ff]/50 rounded-lg">
<span className="text-sm font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
Generate New Seed
</span>
</div>
<p className="text-sm text-[#6ef3f7]">
Create a fresh BIP39 mnemonic for a new wallet
</p>
</div>
{/* Word count selector */}
<div className="space-y-3">
<label className="text-xs font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
Seed Length
</label>
<div className="flex gap-4 justify-center">
<button
onClick={() => setSeedWordCount(12)}
className={`px-8 py-3 rounded-lg font-medium transition-all ${seedWordCount === 12
? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
: 'bg-[#16213e] text-[#9d84b7] border-2 border-[#00f0ff]/30 hover:text-[#6ef3f7] hover:border-[#00f0ff]/50'
}`}
style={seedWordCount === 12 ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
>
12 Words
</button>
<button
onClick={() => setSeedWordCount(24)}
className={`px-8 py-3 rounded-lg font-medium transition-all ${seedWordCount === 24
? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
: 'bg-[#16213e] text-[#9d84b7] border-2 border-[#00f0ff]/30 hover:text-[#6ef3f7] hover:border-[#00f0ff]/50'
}`}
style={seedWordCount === 24 ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
>
24 Words
</button>
</div>
</div>
{/* Destination selector */}
<div className="space-y-3">
<label className="text-xs font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
Send Generated Seed To
</label>
<div className="flex gap-4 justify-center">
<label className={`flex-1 p-4 rounded-lg border-2 cursor-pointer transition-all ${seedDestination === 'backup'
? 'bg-[#1a1a2e] border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
: 'bg-[#16213e] border-[#00f0ff]/30 hover:border-[#00f0ff]/50'
}`}>
<input
type="radio"
name="destination"
value="backup"
checked={seedDestination === 'backup'}
onChange={(e) => setSeedDestination(e.target.value as 'backup' | 'seedblender')}
className="hidden"
/>
<div className="text-center space-y-1">
<div className={`text-lg font-bold ${seedDestination === 'backup' ? 'text-[#00f0ff]' : 'text-[#9d84b7]'}`}
style={seedDestination === 'backup' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}>
📦 Backup
</div>
<p className="text-xs text-[#6ef3f7]">
Encrypt immediately
</p>
</div>
</label>
<label className={`flex-1 p-4 rounded-lg border-2 cursor-pointer transition-all ${seedDestination === 'seedblender'
? 'bg-[#1a1a2e] border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
: 'bg-[#16213e] border-[#00f0ff]/30 hover:border-[#00f0ff]/50'
}`}>
<input
type="radio"
name="destination"
value="seedblender"
checked={seedDestination === 'seedblender'}
onChange={(e) => setSeedDestination(e.target.value as 'backup' | 'seedblender')}
className="hidden"
/>
<div className="text-center space-y-1">
<div className={`text-lg font-bold ${seedDestination === 'seedblender' ? 'text-[#00f0ff]' : 'text-[#9d84b7]'}`}
style={seedDestination === 'seedblender' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}>
🎲 Seed Blender
</div>
<p className="text-xs text-[#6ef3f7]">
Use for XOR blending
</p>
</div>
</label>
</div>
</div>
{/* Generate button */}
<button
onClick={generateNewSeed}
disabled={loading || isReadOnly}
className="w-full py-4 bg-gradient-to-r from-[#ff006e] to-[#ff4d8f] text-white rounded-xl font-bold uppercase tracking-wider flex items-center justify-center gap-2 hover:shadow-[0_0_30px_rgba(255,0,110,0.8)] active:scale-95 transition-all disabled:opacity-50 disabled:cursor-not-allowed border border-[#ff006e]"
style={{ textShadow: '0 0 10px rgba(255,255,255,0.8)' }}
>
{loading ? (
<>
<RefreshCw className="animate-spin" size={20} />
Generating...
</>
) : (
<>
<RefreshCw size={20} />
Generate {seedWordCount}-Word Seed
</>
)}
</button>
{/* Display generated seed */}
{generatedSeed && (
<div className="p-6 bg-[#0a0a0f] border-2 border-[#39ff14] rounded-lg shadow-[0_0_30px_rgba(57,255,20,0.4)] space-y-4 animate-in zoom-in-95">
<div className="flex items-center justify-between">
<span className="font-bold text-[#39ff14] flex items-center gap-2" style={{ textShadow: '0 0 10px rgba(57,255,20,0.8)' }}>
<CheckCircle2 size={20} /> Generated Successfully
</span>
</div>
<div className="p-4 bg-[#16213e] rounded-lg border border-[#39ff14]/50">
<p className="font-mono text-sm text-[#39ff14] break-words leading-relaxed" style={{ textShadow: '0 0 5px rgba(57,255,20,0.5)' }}>
{generatedSeed}
</p>
</div>
<p className="text-xs text-[#6ef3f7] text-center">
Switching to {seedDestination === 'backup' ? 'Backup' : 'Seed Blender'} tab...
</p>
</div>
)}
</div>
</div>
<div className={activeTab === 'backup' ? 'block' : 'hidden'}>
<div className="space-y-2">
<label className="text-sm font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>BIP39 Mnemonic</label>
<textarea
@@ -568,9 +781,8 @@ function App() {
onChange={setPublicKeyInput}
readOnly={isReadOnly}
/>
</>
) : activeTab === 'restore' ? (
<>
</div>
<div className={activeTab === 'restore' ? 'block' : 'hidden'}>
<div className="space-y-4">
{/* File Upload Zone */}
<div
@@ -658,18 +870,21 @@ function App() {
</div>
</div>
)}
</>
) : (
</div>
<div className={activeTab === 'seedblender' ? 'block' : 'hidden'}>
<SeedBlender
key={blenderResetKey}
onDirtyStateChange={setIsBlenderDirty}
setMnemonicForBackup={setMnemonic}
requestTabChange={handleRequestTabChange}
incomingSeed={seedForBlender}
onSeedReceived={() => setSeedForBlender('')}
/>
)}
</div>
</div>
{/* Security Panel */}
{activeTab !== 'seedblender' && (
{activeTab !== 'seedblender' && activeTab !== 'create' && (
<div className="space-y-2"> {/* Added space-y-2 wrapper */}
<label className="text-sm font-semibold text-[#00f0ff] flex items-center gap-2" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
<Lock size={14} /> SECURITY OPTIONS

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Shield, Lock } from 'lucide-react';
import { Shield, Lock, RefreshCw } from 'lucide-react';
import SecurityBadge from './badges/SecurityBadge';
import StorageBadge from './badges/StorageBadge';
import ClipboardBadge from './badges/ClipboardBadge';
@@ -25,13 +25,14 @@ interface HeaderProps {
sessionItems: StorageItem[];
events: ClipboardEvent[];
onOpenClipboardModal: () => void;
activeTab: 'backup' | 'restore' | 'seedblender';
onRequestTabChange: (tab: 'backup' | 'restore' | 'seedblender') => void;
activeTab: 'create' | 'backup' | 'restore' | 'seedblender';
onRequestTabChange: (tab: 'create' | 'backup' | 'restore' | 'seedblender') => void;
encryptedMnemonicCache: any;
handleLockAndClear: () => void;
appVersion: string;
isLocked: boolean;
onToggleLock: () => void;
onResetAll: () => void; // NEW
}
const Header: React.FC<HeaderProps> = ({
@@ -47,7 +48,8 @@ const Header: React.FC<HeaderProps> = ({
handleLockAndClear,
appVersion,
isLocked,
onToggleLock
onToggleLock,
onResetAll
}) => {
return (
<header className="sticky top-0 z-50 bg-[#0a0a0f] border-b border-[#00f0ff]/30 backdrop-blur-sm">
@@ -89,6 +91,24 @@ const Header: React.FC<HeaderProps> = ({
<span>Lock/Clear</span>
</button>
)}
{/* Reset All button - left side */}
<button
onClick={onResetAll}
className="px-4 py-2 bg-[#16213e] border-2 border-[#ff006e] text-[#ff006e] rounded-lg font-medium hover:bg-[#ff006e] hover:text-white transition-all flex items-center gap-2"
>
<RefreshCw size={16} />
Reset All
</button>
{encryptedMnemonicCache && (
<div className="h-8 w-px bg-[#00f0ff]/30 mx-2"></div>
)}
<button
className={`px-4 py-2 rounded-lg font-medium ${activeTab === 'create' ? 'bg-[#1a1a2e] text-[#00f0ff] border-b-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)] relative' : 'bg-[#0a0a0f] text-[#9d84b7] hover:text-[#6ef3f7] hover:bg-[#16213e] transition-all'}`}
style={activeTab === 'create' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
onClick={() => onRequestTabChange('create')}
>
Create
</button>
<button
className={`px-4 py-2 rounded-lg font-medium ${activeTab === 'backup' ? 'bg-[#1a1a2e] text-[#00f0ff] border-b-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)] relative' : 'bg-[#0a0a0f] text-[#9d84b7] hover:text-[#6ef3f7] hover:bg-[#16213e] transition-all'}`}
style={activeTab === 'backup' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}

View File

@@ -4,7 +4,7 @@
* @description This component provides a full UI for the multi-step seed blending process,
* handling various input formats, per-row decryption, and final output actions.
*/
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useRef } from 'react';
import { QrCode, X, Plus, CheckCircle2, AlertTriangle, RefreshCw, Sparkles, EyeOff, Lock, Key, ArrowRight } from 'lucide-react';
import QRScanner from './QRScanner';
import { decryptFromSeed, detectEncryptionMode } from '../lib/seedpgp';
@@ -53,10 +53,13 @@ const createNewEntry = (): MnemonicEntry => ({
interface SeedBlenderProps {
onDirtyStateChange: (isDirty: boolean) => void;
setMnemonicForBackup: (mnemonic: string) => void;
requestTabChange: (tab: 'backup' | 'restore' | 'seedblender') => void;
requestTabChange: (tab: 'create' | 'backup' | 'restore' | 'seedblender') => void;
incomingSeed?: string; // NEW: seed from Create tab
onSeedReceived?: () => void; // NEW: callback after seed added
}
export function SeedBlender({ onDirtyStateChange, setMnemonicForBackup, requestTabChange }: SeedBlenderProps) {
export function SeedBlender({ onDirtyStateChange, setMnemonicForBackup, requestTabChange, incomingSeed, onSeedReceived }: SeedBlenderProps) {
const processedSeedsRef = useRef<Set<string>>(new Set());
const [entries, setEntries] = useState<MnemonicEntry[]>([createNewEntry()]);
const [showQRScanner, setShowQRScanner] = useState(false);
const [scanTargetIndex, setScanTargetIndex] = useState<number | null>(null);
@@ -77,6 +80,39 @@ export function SeedBlender({ onDirtyStateChange, setMnemonicForBackup, requestT
onDirtyStateChange(isDirty);
}, [entries, diceRolls, onDirtyStateChange]);
const addSeedEntry = (seed: string) => {
setEntries(currentEntries => {
const emptyEntryIndex = currentEntries.findIndex(e => !e.rawInput.trim());
if (emptyEntryIndex !== -1) {
return currentEntries.map((entry, index) =>
index === emptyEntryIndex
? { ...entry, rawInput: seed, decryptedMnemonic: seed, isValid: null, error: null }
: entry
);
} else {
const newEntry = createNewEntry();
newEntry.rawInput = seed;
newEntry.decryptedMnemonic = seed;
return [...currentEntries, newEntry];
}
});
};
useEffect(() => {
if (incomingSeed && incomingSeed.trim()) {
// Check if we've already processed this exact seed
if (!processedSeedsRef.current.has(incomingSeed)) {
const isDuplicate = entries.some(e => e.decryptedMnemonic === incomingSeed);
if (!isDuplicate) {
addSeedEntry(incomingSeed);
processedSeedsRef.current.add(incomingSeed);
}
}
// Always notify parent to clear the incoming seed
onSeedReceived?.();
}
}, [incomingSeed]);
const handleLockAndClear = () => {
setEntries([createNewEntry()]);
setBlendedResult(null);
@@ -230,7 +266,7 @@ export function SeedBlender({ onDirtyStateChange, setMnemonicForBackup, requestT
mnemonic = (await decryptFromSeed({ frameText: entry.rawInput, messagePassword: entry.passwordInput, mode: 'pgp' })).w;
}
updateEntry(index, { rawInput: mnemonic, decryptedMnemonic: mnemonic, isEncrypted: false, passwordRequired: false, error: null });
} catch(e: any) {
} catch (e: any) {
updateEntry(index, { error: e.message || "Decryption failed" });
}
};
@@ -242,14 +278,17 @@ export function SeedBlender({ onDirtyStateChange, setMnemonicForBackup, requestT
const outputBits = blendedResult.blendedEntropy.length >= 32 ? 256 : 128;
const result = await mixWithDiceAsync(blendedResult.blendedEntropy, diceRolls, outputBits);
setFinalMnemonic(result.finalMnemonic);
} catch(e) { setFinalMnemonic(null); } finally { setMixing(false); }
} catch (e) { setFinalMnemonic(null); } finally { setMixing(false); }
};
const handleTransfer = () => {
if (!finalMnemonic) return;
// Set mnemonic for backup
setMnemonicForBackup(finalMnemonic);
handleLockAndClear();
// Switch to backup tab
requestTabChange('backup');
// DON'T auto-clear - user can use "Reset All" button if they want to start fresh
// This preserves the blended seed in case user wants to come back and export QR
};
const getBorderColor = (isValid: boolean | null) => {
@@ -261,9 +300,10 @@ export function SeedBlender({ onDirtyStateChange, setMnemonicForBackup, requestT
return (
<>
<div className="space-y-6 pb-20">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-[#00f0ff]" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Seed Blender</h2>
<button onClick={handleLockAndClear} className="flex items-center gap-2 text-sm text-[#ff006e] bg-[#16213e] px-3 py-1.5 rounded-lg hover:bg-[#ff006e]/20 border-2 border-[#ff006e]/50"><Lock size={16} /><span>Lock/Clear</span></button>
<div className="mb-6">
<h2 className="text-lg font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
Seed Blender
</h2>
</div>
<div className="p-6 bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/30">
@@ -275,7 +315,7 @@ export function SeedBlender({ onDirtyStateChange, setMnemonicForBackup, requestT
<div className="space-y-2">
<div className="flex items-center justify-between"><label className="text-sm font-semibold text-[#00f0ff]">Decrypt {entry.inputType.toUpperCase()} Mnemonic</label><button onClick={() => updateEntry(index, createNewEntry())} className="text-xs text-[#6ef3f7] hover:text-[#00f0ff]">&times; Cancel</button></div>
<p className="text-xs text-[#6ef3f7] truncate">Payload: <code className="text-[#9d84b7]">{entry.rawInput.substring(0, 40)}...</code></p>
<div className="flex gap-2"><input type="password" placeholder="Enter passphrase to decrypt..." value={entry.passwordInput} onChange={(e) => updateEntry(index, { passwordInput: e.target.value })} className="w-full p-2 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-sm font-mono text-[#00f0ff] placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)]" /><button onClick={() => handleDecrypt(index)} className="px-4 bg-[#ff006e] text-white rounded-lg font-semibold hover:bg-[#ff4d8f] hover:shadow-[0_0_15px_rgba(255,0,110,0.5)]"><Key size={16}/></button></div>
<div className="flex gap-2"><input type="password" placeholder="Enter passphrase to decrypt..." value={entry.passwordInput} onChange={(e) => updateEntry(index, { passwordInput: e.target.value })} className="w-full p-2 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-sm font-mono text-[#00f0ff] placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)]" /><button onClick={() => handleDecrypt(index)} className="px-4 bg-[#ff006e] text-white rounded-lg font-semibold hover:bg-[#ff4d8f] hover:shadow-[0_0_15px_rgba(255,0,110,0.5)]"><Key size={16} /></button></div>
{entry.error && <p className="text-xs text-[#ff006e]">{entry.error}</p>}
</div>
) : (
@@ -323,8 +363,16 @@ export function SeedBlender({ onDirtyStateChange, setMnemonicForBackup, requestT
<div className="p-6 bg-[#0a0a0f] rounded-xl border-2 border-[#39ff14] shadow-[0_0_20px_rgba(57,255,20,0.3)]"><p data-sensitive="Final Blended Mnemonic" className="font-mono text-center text-lg break-words text-[#39ff14]">{finalMnemonic}</p></div>
<div className="mt-4 p-3 bg-[#ff006e]/10 text-[#ff006e] rounded-lg text-xs flex gap-2 border-2 border-[#ff006e]/30"><AlertTriangle size={16} className="shrink-0 mt-0.5" /><span><strong>Security Warning:</strong> Write this down immediately. Do not save it digitally.</span></div>
<div className="grid grid-cols-2 gap-3 mt-4">
<button onClick={() => setShowFinalQR(true)} className="w-full py-2.5 bg-[#1a1a2e] text-[#00f0ff] rounded-lg font-semibold flex items-center justify-center gap-2 border-2 border-[#00f0ff]/50 hover:bg-[#16213e] hover:shadow-[0_0_15px_rgba(0,240,255,0.3)]"><QrCode size={16}/> Export as QR</button>
<button onClick={handleTransfer} className="w-full py-2.5 bg-[#00f0ff] text-[#16213e] rounded-lg font-semibold flex items-center justify-center gap-2 border-2 border-[#00f0ff] hover:bg-[#00f0ff]/80 hover:shadow-[0_0_15px_rgba(0,240,255,0.5)]"><ArrowRight size={16}/> Transfer to Backup</button>
<button onClick={() => setShowFinalQR(true)} className="w-full py-2.5 bg-[#1a1a2e] text-[#00f0ff] rounded-lg font-semibold flex items-center justify-center gap-2 border-2 border-[#00f0ff]/50 hover:bg-[#16213e] hover:shadow-[0_0_15px_rgba(0,240,255,0.3)]"><QrCode size={16} /> Export as QR</button>
<button
onClick={handleTransfer}
className="w-full py-2.5 bg-gradient-to-r from-[#ff006e] to-[#ff4d8f] text-white rounded-lg font-bold uppercase tracking-wider flex items-center justify-center gap-2 hover:shadow-[0_0_30px_rgba(255,0,110,0.8)] active:scale-95 transition-all disabled:opacity-50 disabled:cursor-not-allowed border border-[#ff006e]"
style={{ textShadow: '0 0 10px rgba(255,255,255,0.8)' }}
disabled={!finalMnemonic}
>
<ArrowRight size={20} />
Send to Backup Tab
</button>
</div>
</div>
) : (