mirror of
https://github.com/kccleoc/seedpgp-web.git
synced 2026-03-06 17:37:51 +08:00
feat: transform UI to dark cyberpunk theme
- Eliminate all white/light boxes and backgrounds - Fix drag-drop zone with neon cyberpunk colors (#00f0ff, #ff006e, #16213e) - Fix restored mnemonic display with matrix green (#39ff14) - Fix security options panel with dark gradient - Fix all remaining slate-700/slate-800 labels to cyberpunk neon - Fix info banners and text colors - Update badge components with cyberpunk color scheme - Apply consistent dark theme across all components
This commit is contained in:
BIN
Screenshot 2026-02-08 at 21.14.14.png
Normal file
BIN
Screenshot 2026-02-08 at 21.14.14.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
965
src/App.tsx
965
src/App.tsx
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ interface FooterProps {
|
||||
|
||||
const Footer: React.FC<FooterProps> = ({ appVersion, buildHash, buildTimestamp }) => {
|
||||
return (
|
||||
<footer className="text-center text-xs text-slate-400 p-4">
|
||||
<footer className="text-center text-xs text-[#6ef3f7] p-4">
|
||||
<p>SeedPGP v{appVersion} • build {buildHash} • {buildTimestamp}</p>
|
||||
<p className="mt-1">Never share your private keys or seed phrases. Always verify on an airgapped device.</p>
|
||||
</footer>
|
||||
|
||||
@@ -50,19 +50,19 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onToggleLock
|
||||
}) => {
|
||||
return (
|
||||
<header className="sticky top-0 z-50 bg-slate-900 border-b border-slate-800 backdrop-blur-sm">
|
||||
<header className="sticky top-0 z-50 bg-[#0a0a0f] border-b border-[#00f0ff]/30 backdrop-blur-sm">
|
||||
<div className="max-w-7xl mx-auto px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Left: Logo & Title */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-teal-500 rounded-lg flex items-center justify-center">
|
||||
<Shield className="w-6 h-6 text-white" />
|
||||
<div className="w-10 h-10 bg-[#00f0ff] rounded-lg flex items-center justify-center shadow-[0_0_15px_rgba(0,240,255,0.5)]">
|
||||
<Shield className="w-6 h-6 text-[#0a0a0f]" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-white">
|
||||
SeedPGP <span className="text-teal-400">v{appVersion}</span>
|
||||
<h1 className="text-lg font-semibold text-[#00f0ff]" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
|
||||
SeedPGP <span className="text-[#ff006e]">v{appVersion}</span>
|
||||
</h1>
|
||||
<p className="text-xs text-slate-400">OpenPGP-secured BIP39 backup</p>
|
||||
<p className="text-xs text-[#6ef3f7]">OpenPGP-secured BIP39 backup</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,35 +83,36 @@ const Header: React.FC<HeaderProps> = ({
|
||||
{encryptedMnemonicCache && (
|
||||
<button
|
||||
onClick={handleLockAndClear}
|
||||
className="flex items-center gap-2 text-sm text-red-400 bg-slate-800/50 px-3 py-1.5 rounded-lg hover:bg-red-900/50 transition-colors"
|
||||
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 transition-all hover:shadow-[0_0_15px_rgba(255,0,110,0.5)]"
|
||||
>
|
||||
<Lock size={16} />
|
||||
<span>Lock/Clear</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={`px-4 py-2 rounded-lg ${activeTab === 'backup' ? 'bg-teal-500 hover:bg-teal-600' : 'bg-slate-700 hover:bg-slate-600'}`}
|
||||
onClick={() => onRequestTabChange('backup')}
|
||||
>
|
||||
Backup
|
||||
</button>
|
||||
<button
|
||||
className={`px-4 py-2 rounded-lg ${activeTab === 'restore' ? 'bg-teal-500 hover:bg-teal-600' : 'bg-slate-700 hover:bg-slate-600'}`}
|
||||
<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}
|
||||
onClick={() => onRequestTabChange('backup')}
|
||||
>
|
||||
Backup
|
||||
</button> <button
|
||||
className={`px-4 py-2 rounded-lg font-medium ${activeTab === 'restore' ? '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 === 'restore' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
|
||||
onClick={() => onRequestTabChange('restore')}
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
<button
|
||||
className={`px-4 py-2 rounded-lg ${activeTab === 'seedblender' ? 'bg-teal-500 hover:bg-teal-600' : 'bg-slate-700 hover:bg-slate-600'}`}
|
||||
onClick={() => onRequestTabChange('seedblender')}
|
||||
>
|
||||
Seed Blender
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className={`px-4 py-2 rounded-lg font-medium ${activeTab === 'seedblender' ? '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 === 'seedblender' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
|
||||
onClick={() => onRequestTabChange('seedblender')}
|
||||
>
|
||||
Seed Blender
|
||||
</button> </div>
|
||||
</div>
|
||||
|
||||
{/* Mobile: Stack monitoring badges */}
|
||||
<div className="md:hidden flex items-center gap-3 mt-3 pt-3 border-t border-slate-800">
|
||||
<div className="md:hidden flex items-center gap-3 mt-3 pt-3 border-t border-[#00f0ff]/30">
|
||||
<SecurityBadge onClick={onOpenSecurityModal} />
|
||||
<div onClick={onOpenStorageModal} className="cursor-pointer">
|
||||
<StorageBadge localItems={localItems} sessionItems={sessionItems} />
|
||||
|
||||
@@ -52,12 +52,12 @@ export const PgpKeyInput: React.FC<PgpKeyInputProps> = ({
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-semibold text-slate-200 flex items-center justify-between">
|
||||
<label className="text-sm font-semibold text-[#00f0ff] flex items-center justify-between" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
|
||||
<span className="flex items-center gap-2">
|
||||
{Icon && <Icon size={14} />} {label}
|
||||
</span>
|
||||
{!readOnly && (
|
||||
<span className="text-[10px] text-slate-400 font-normal bg-slate-100 px-2 py-0.5 rounded-full border border-slate-200">
|
||||
<span className="text-[10px] text-[#6ef3f7] font-normal bg-[#16213e] px-2 py-0.5 rounded-full border border-[#00f0ff]/30">
|
||||
Drag & Drop .asc file
|
||||
</span>
|
||||
)}
|
||||
@@ -69,7 +69,7 @@ export const PgpKeyInput: React.FC<PgpKeyInputProps> = ({
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<textarea
|
||||
className={`w-full h-40 p-3 bg-slate-50 border rounded-xl text-xs font-mono transition-colors resize-none focus:outline-none focus:ring-2 focus:ring-teal-500 ${isDragging && !readOnly ? 'border-teal-500 bg-teal-50' : 'border-slate-200'} ${
|
||||
className={`w-full h-40 p-3 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-xl text-xs font-mono text-[#00f0ff] placeholder-[#9d84b7] transition-colors resize-none focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] ${isDragging && !readOnly ? 'border-[#ff006e] bg-[#16213e]' : 'border-[#00f0ff]/50'} ${
|
||||
readOnly ? 'blur-sm select-none' : ''
|
||||
}`}
|
||||
placeholder={placeholder}
|
||||
@@ -78,8 +78,8 @@ export const PgpKeyInput: React.FC<PgpKeyInputProps> = ({
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
{isDragging && !readOnly && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-teal-50/90 rounded-xl border-2 border-dashed border-teal-500 pointer-events-none z-10">
|
||||
<div className="text-teal-600 font-bold flex flex-col items-center animate-bounce">
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-[#16213e]/90 rounded-xl border-2 border-dashed border-[#ff006e] pointer-events-none z-10">
|
||||
<div className="text-[#ff006e] font-bold flex flex-col items-center animate-bounce" style={{ textShadow: '0 0 10px rgba(255,0,110,0.5)' }}>
|
||||
<Upload size={24} />
|
||||
<span className="text-sm mt-2">Drop Key File Here</span>
|
||||
</div>
|
||||
|
||||
@@ -128,43 +128,43 @@ export default function QRScanner({ onScanSuccess, onClose, onError }: QRScanner
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center z-50">
|
||||
<div className="bg-slate-800 rounded-xl border border-slate-700 p-6 max-w-md w-full mx-4 shadow-2xl">
|
||||
<div className="bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/50 p-6 max-w-md w-full mx-4 shadow-[0_0_40px_rgba(0,240,255,0.3)]">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold text-[#00f0ff] flex items-center gap-2" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
|
||||
<Camera size={20} />
|
||||
Scan QR Code
|
||||
</h3>
|
||||
<button onClick={onClose} className="p-2 hover:bg-slate-700 rounded-lg transition-colors">
|
||||
<X size={20} className="text-slate-400" />
|
||||
<button onClick={onClose} className="p-2 hover:bg-[#1a1a2e] rounded-lg transition-colors border-2 border-[#00f0ff]/30">
|
||||
<X size={20} className="text-[#6ef3f7]" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{internalError && (
|
||||
<div className="mb-4 p-3 bg-red-500/10 border border-red-500/50 rounded-lg flex items-start gap-2 text-red-400 text-sm">
|
||||
<div className="mb-4 p-3 bg-[#ff006e]/10 border-2 border-[#ff006e]/30 rounded-lg flex items-start gap-2 text-[#ff006e] text-sm">
|
||||
<AlertCircle size={16} className="shrink-0 mt-0.5" />
|
||||
<span>{internalError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="mb-4 p-3 bg-green-500/10 border border-green-500/50 rounded-lg flex items-center gap-2 text-green-400 text-sm">
|
||||
<div className="mb-4 p-3 bg-[#39ff14]/10 border-2 border-[#39ff14]/30 rounded-lg flex items-center gap-2 text-[#39ff14] text-sm">
|
||||
<CheckCircle2 size={16} />
|
||||
<span>QR Code detected!</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative bg-black rounded-lg overflow-hidden">
|
||||
<div className="relative bg-black rounded-lg overflow-hidden border-2 border-[#00f0ff]/30">
|
||||
<video ref={videoRef} className="w-full h-64 object-cover" playsInline muted />
|
||||
<canvas ref={canvasRef} className="hidden" />
|
||||
</div>
|
||||
|
||||
{!hasPermission && !internalError && (
|
||||
<p className="text-sm text-slate-400 mt-3 text-center">Requesting camera access...</p>
|
||||
<p className="text-sm text-[#6ef3f7] mt-3 text-center">Requesting camera access...</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full mt-4 py-2 bg-slate-700 hover:bg-slate-600 rounded-lg text-white font-medium transition-colors"
|
||||
className="w-full mt-4 py-2 bg-[#1a1a2e] hover:bg-[#16213e] rounded-lg text-[#00f0ff] font-medium transition-all border-2 border-[#00f0ff]/50 hover:shadow-[0_0_15px_rgba(0,240,255,0.3)]"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
@@ -93,26 +93,26 @@ export const QrDisplay: React.FC<QrDisplayProps> = ({ value }) => {
|
||||
if (!dataUrl) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-white p-6 rounded-lg inline-block shadow-lg">
|
||||
<div className="border-4 border-[#00f0ff] rounded-xl shadow-[0_0_40px_rgba(0,240,255,0.6)] p-4 bg-[#0a0a0f] space-y-4">
|
||||
<div className="bg-[#16213e] p-6 rounded-lg inline-block shadow-[0_0_20px_rgba(0,240,255,0.3)] border-2 border-[#00f0ff]/30">
|
||||
<img src={dataUrl} alt="QR Code" className="w-full h-auto" />
|
||||
</div>
|
||||
|
||||
{debugInfo && (
|
||||
<div className="text-xs text-slate-500 font-mono">
|
||||
<div className="text-xs text-[#6ef3f7] font-mono">
|
||||
{debugInfo}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-teal-600 hover:bg-teal-700 text-white rounded-lg transition-colors"
|
||||
className="flex items-center gap-2 px-4 py-2 bg-[#00f0ff] hover:bg-[#00f0ff]/80 text-[#0a0a0f] rounded-lg transition-all hover:shadow-[0_0_15px_rgba(0,240,255,0.5)]"
|
||||
>
|
||||
<Download size={16} />
|
||||
Download QR Code
|
||||
</button>
|
||||
|
||||
<p className="text-xs text-slate-500">
|
||||
<p className="text-xs text-[#6ef3f7]">
|
||||
Downloads as: SeedPGP_{new Date().toISOString().split('T')[0]}_HHMMSS.png
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -11,26 +11,26 @@ const CSP_POLICY = `default-src 'self'; script-src 'self'; style-src 'self' 'uns
|
||||
|
||||
export function ReadOnly({ isReadOnly, onToggle, buildHash, appVersion }: ReadOnlyProps) {
|
||||
return (
|
||||
<div className="pt-3 border-t border-slate-300">
|
||||
<div className="pt-3 border-t border-[#00f0ff]/30">
|
||||
<label className="flex items-center gap-2 cursor-pointer group">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isReadOnly}
|
||||
onChange={(e) => onToggle(e.target.checked)}
|
||||
className="rounded text-teal-600 focus:ring-2 focus:ring-teal-500 transition-all"
|
||||
className="rounded text-[#00f0ff] focus:ring-2 focus:ring-[#00f0ff] transition-all"
|
||||
/>
|
||||
<span className="text-xs font-medium text-slate-700 group-hover:text-slate-900 transition-colors">
|
||||
<span className="text-xs font-medium text-[#6ef3f7] group-hover:text-[#00f0ff] transition-colors">
|
||||
Read-only Mode
|
||||
</span>
|
||||
</label>
|
||||
{isReadOnly && (
|
||||
<div className="mt-4 p-3 bg-slate-800 text-slate-200 rounded-lg text-xs space-y-2 animate-in fade-in">
|
||||
<div className="mt-4 p-3 bg-[#16213e] text-[#6ef3f7] rounded-lg text-xs space-y-2 animate-in fade-in border-2 border-[#00f0ff]/30">
|
||||
<p className="font-bold flex items-center gap-2"><WifiOff size={14} /> Network & Persistence Disabled</p>
|
||||
<div className="font-mono text-[10px] space-y-1">
|
||||
<p><span className="font-semibold text-slate-400">Version:</span> {appVersion}</p>
|
||||
<p><span className="font-semibold text-slate-400">Build:</span> {buildHash}</p>
|
||||
<p className="pt-1 font-semibold text-slate-400">Content Security Policy:</p>
|
||||
<p className="text-sky-300 break-words">{CSP_POLICY}</p>
|
||||
<p><span className="font-semibold text-[#9d84b7]">Version:</span> {appVersion}</p>
|
||||
<p><span className="font-semibold text-[#9d84b7]">Build:</span> {buildHash}</p>
|
||||
<p className="pt-1 font-semibold text-[#9d84b7]">Content Security Policy:</p>
|
||||
<p className="text-[#00f0ff] break-words">{CSP_POLICY}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -39,8 +39,8 @@ export const SecurityWarnings: React.FC = () => {
|
||||
description="If hosted online: DNS, HTTPS, CDN, and browser can see usage patterns. Use offline/local for maximum security."
|
||||
/>
|
||||
|
||||
<div className="pt-3 border-t border-slate-600 text-xs text-slate-400">
|
||||
<strong className="text-slate-300">Recommendation:</strong>{' '}
|
||||
<div className="pt-3 border-t border-[#00f0ff]/30 text-xs text-[#6ef3f7]">
|
||||
<strong className="text-[#00f0ff]">Recommendation:</strong>{' '}
|
||||
Use this tool on a dedicated offline device. Clear browser data after each use. Never use on shared/public computers.
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,8 +59,8 @@ const Warning = ({
|
||||
<div className="flex gap-2 text-sm">
|
||||
<span className="text-lg flex-shrink-0">{icon}</span>
|
||||
<div>
|
||||
<div className="font-semibold text-slate-200 mb-1">{title}</div>
|
||||
<div className="text-slate-400 leading-relaxed">{description}</div>
|
||||
<div className="font-semibold text-[#00f0ff] mb-1" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>{title}</div>
|
||||
<div className="text-[#6ef3f7] leading-relaxed">{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -253,82 +253,82 @@ export function SeedBlender({ onDirtyStateChange, setMnemonicForBackup, requestT
|
||||
};
|
||||
|
||||
const getBorderColor = (isValid: boolean | null) => {
|
||||
if (isValid === true) return 'border-green-500 focus:ring-green-500';
|
||||
if (isValid === false) return 'border-red-500 focus:ring-red-500';
|
||||
return 'border-slate-200 focus:ring-teal-500';
|
||||
if (isValid === true) return 'border-[#39ff14] focus:ring-[#39ff14]';
|
||||
if (isValid === false) return 'border-[#ff006e] focus:ring-[#ff006e]';
|
||||
return 'border-[#00f0ff]/50 focus:ring-[#00f0ff]';
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-6 pb-20">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold text-slate-100">Seed Blender</h2>
|
||||
<button onClick={handleLockAndClear} className="flex items-center gap-2 text-sm text-red-400 bg-slate-800/50 px-3 py-1.5 rounded-lg hover:bg-red-900/50"><Lock size={16} /><span>Lock/Clear</span></button>
|
||||
<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>
|
||||
|
||||
<div className="p-6 bg-slate-700/50 rounded-xl border border-slate-600">
|
||||
<h3 className="font-semibold text-lg mb-4 text-slate-200">Step 1: Input Mnemonics</h3>
|
||||
<div className="p-6 bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/30">
|
||||
<h3 className="font-semibold text-lg mb-4 text-[#00f0ff]" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Step 1: Input Mnemonics</h3>
|
||||
<div className="space-y-4">
|
||||
{entries.map((entry, index) => (
|
||||
<div key={entry.id} className="p-3 bg-slate-800/50 rounded-lg">
|
||||
<div key={entry.id} className="p-3 bg-[#1a1a2e] rounded-lg border-2 border-[#00f0ff]/20">
|
||||
{entry.passwordRequired ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between"><label className="text-sm font-semibold text-slate-200">Decrypt {entry.inputType.toUpperCase()} Mnemonic</label><button onClick={() => updateEntry(index, createNewEntry())} className="text-xs text-slate-400 hover:text-white">× Cancel</button></div>
|
||||
<p className="text-xs text-slate-400 truncate">Payload: <code className="text-slate-300">{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-slate-50 border-2 border-slate-200 rounded-lg text-sm font-mono text-slate-900 focus:outline-none focus:ring-2 focus:ring-purple-500" /><button onClick={() => handleDecrypt(index)} className="px-4 bg-purple-600 text-white rounded-lg font-semibold hover:bg-purple-700"><Key size={16}/></button></div>
|
||||
{entry.error && <p className="text-xs text-red-400">{entry.error}</p>}
|
||||
<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]">× 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>
|
||||
{entry.error && <p className="text-xs text-[#ff006e]">{entry.error}</p>}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<div className="flex flex-col sm:flex-row items-start gap-2">
|
||||
<div className="relative w-full">
|
||||
<textarea value={entry.rawInput} onChange={(e) => updateEntry(index, { rawInput: e.target.value, decryptedMnemonic: e.target.value, isValid: null, error: null })} placeholder={`Mnemonic #${index + 1} (12 or 24 words)`} className={`w-full h-28 sm:h-24 p-3 pr-10 bg-slate-50 border-2 rounded-lg text-sm font-mono text-slate-900 placeholder:text-slate-400 ${getBorderColor(entry.isValid)}`} />
|
||||
{entry.isValid === true && <CheckCircle2 className="absolute top-3 right-3 text-green-500" />}
|
||||
{entry.isValid === false && <AlertTriangle className="absolute top-3 right-3 text-red-500" />}
|
||||
<textarea value={entry.rawInput} onChange={(e) => updateEntry(index, { rawInput: e.target.value, decryptedMnemonic: e.target.value, isValid: null, error: null })} placeholder={`Mnemonic #${index + 1} (12 or 24 words)`} className={`w-full h-28 sm:h-24 p-3 pr-10 bg-[#16213e] border-2 rounded-lg text-sm font-mono text-[#00f0ff] placeholder-[#9d84b7] ${getBorderColor(entry.isValid)}`} />
|
||||
{entry.isValid === true && <CheckCircle2 className="absolute top-3 right-3 text-[#39ff14]" />}
|
||||
{entry.isValid === false && <AlertTriangle className="absolute top-3 right-3 text-[#ff006e]" />}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<button onClick={() => handleScan(index)} className="p-3 h-full bg-purple-600/20 text-purple-300 hover:bg-purple-600/50 hover:text-white rounded-md"><QrCode size={20} /></button>
|
||||
<button onClick={() => handleRemoveEntry(entry.id)} className="p-3 h-full bg-red-600/20 text-red-400 hover:bg-red-600/50 hover:text-white rounded-md"><X size={20} /></button>
|
||||
<button onClick={() => handleScan(index)} className="p-3 h-full bg-[#ff006e]/20 text-[#ff006e] hover:bg-[#ff006e]/50 hover:text-white rounded-md border-2 border-[#ff006e]/30"><QrCode size={20} /></button>
|
||||
<button onClick={() => handleRemoveEntry(entry.id)} className="p-3 h-full bg-[#ff006e]/20 text-[#ff006e] hover:bg-[#ff006e]/50 hover:text-white rounded-md border-2 border-[#ff006e]/30"><X size={20} /></button>
|
||||
</div>
|
||||
</div>
|
||||
{entry.error && <p className="text-xs text-red-400 px-1">{entry.error}</p>}
|
||||
{entry.error && <p className="text-xs text-[#ff006e] px-1">{entry.error}</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<button onClick={handleAddEntry} className="w-full py-2.5 bg-slate-600/70 hover:bg-slate-600 rounded-lg font-semibold flex items-center justify-center gap-2"><Plus size={16} /> Add Another Mnemonic</button>
|
||||
<button onClick={handleAddEntry} className="w-full py-2.5 bg-[#1a1a2e] hover:bg-[#16213e] text-[#00f0ff] rounded-lg font-semibold flex items-center justify-center gap-2 border-2 border-[#00f0ff]/50"><Plus size={16} /> Add Another Mnemonic</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-slate-700/50 rounded-xl border border-slate-600 min-h-[10rem]">
|
||||
<h3 className="font-semibold text-lg mb-4 text-slate-200">Step 2: Blended Preview</h3>
|
||||
{blending ? <p className="text-sm text-slate-400">Blending...</p> : !blendError && blendedResult ? (<div className="space-y-4 animate-in fade-in">{xorStrength?.isWeak && (<div className="p-3 bg-amber-500/10 border border-amber-500/30 text-amber-300 rounded-lg text-sm flex gap-3"><AlertTriangle /><div><span className="font-bold">Weak XOR Result:</span> Detected only {xorStrength.uniqueBytes} unique bytes. This can happen if seeds are identical or too similar.</div></div>)}<div className="space-y-1"><label className="text-xs font-semibold text-slate-400">Blended Mnemonic (12-word)</label><p data-sensitive="Blended Mnemonic (12-word)" className="p-3 bg-slate-800 rounded-md font-mono text-sm text-slate-100 break-words">{blendedResult.blendedMnemonic12}</p></div>{blendedResult.blendedMnemonic24 && (<div className="space-y-1"><label className="text-xs font-semibold text-slate-400">Blended Mnemonic (24-word)</label><p data-sensitive="Blended Mnemonic (24-word)" className="p-3 bg-slate-800 rounded-md font-mono text-sm text-slate-100 break-words">{blendedResult.blendedMnemonic24}</p></div>)}</div>) : (<p className="text-sm text-slate-400">{blendError || 'Previews will appear here once you enter one or more valid mnemonics.'}</p>)}
|
||||
<div className="p-6 bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/30 min-h-[10rem]">
|
||||
<h3 className="font-semibold text-lg mb-4 text-[#00f0ff]" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Step 2: Blended Preview</h3>
|
||||
{blending ? <p className="text-sm text-[#6ef3f7]">Blending...</p> : !blendError && blendedResult ? (<div className="space-y-4 animate-in fade-in">{xorStrength?.isWeak && (<div className="p-3 bg-[#ff006e]/10 border-2 border-[#ff006e]/30 text-[#ff006e] rounded-lg text-sm flex gap-3"><AlertTriangle /><div><span className="font-bold">Weak XOR Result:</span> Detected only {xorStrength.uniqueBytes} unique bytes. This can happen if seeds are identical or too similar.</div></div>)}<div className="space-y-1"><label className="text-xs font-semibold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Blended Mnemonic (12-word)</label><p data-sensitive="Blended Mnemonic (12-word)" className="p-3 bg-[#1a1a2e] rounded-md font-mono text-sm text-[#00f0ff] break-words border-2 border-[#00f0ff]/30">{blendedResult.blendedMnemonic12}</p></div>{blendedResult.blendedMnemonic24 && (<div className="space-y-1"><label className="text-xs font-semibold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Blended Mnemonic (24-word)</label><p data-sensitive="Blended Mnemonic (24-word)" className="p-3 bg-[#1a1a2e] rounded-md font-mono text-sm text-[#00f0ff] break-words border-2 border-[#00f0ff]/30">{blendedResult.blendedMnemonic24}</p></div>)}</div>) : (<p className="text-sm text-[#6ef3f7]">{blendError || 'Previews will appear here once you enter one or more valid mnemonics.'}</p>)}
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-slate-700/50 rounded-xl border border-slate-600">
|
||||
<h3 className="font-semibold text-lg mb-4 text-slate-200">Step 3: Input Dice Rolls</h3>
|
||||
<div className="p-6 bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/30">
|
||||
<h3 className="font-semibold text-lg mb-4 text-[#00f0ff]" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Step 3: Input Dice Rolls</h3>
|
||||
<div className="space-y-4">
|
||||
<textarea value={diceRolls} onChange={(e) => setDiceRolls(e.target.value.replace(/[^1-6]/g, ''))} placeholder="Enter 99+ dice rolls (e.g., 16345...)" className="w-full h-32 p-3 bg-slate-50 border-2 border-slate-200 rounded-lg text-lg font-mono text-slate-900 placeholder:text-slate-400" />
|
||||
{dicePatternWarning && (<div className="p-3 bg-amber-500/10 border border-amber-500/30 text-amber-300 rounded-lg text-sm flex gap-3"><AlertTriangle /><p><span className="font-bold">Warning:</span> {dicePatternWarning}</p></div>)}
|
||||
{diceStats && diceStats.length > 0 && (<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-center"><div className="p-3 bg-slate-800 rounded-lg"><p className="text-xs text-slate-400">Rolls</p><p className="text-lg font-bold">{diceStats.length}</p></div><div className="p-3 bg-slate-800 rounded-lg"><p className="text-xs text-slate-400">Entropy (bits)</p><p className="text-lg font-bold">{diceStats.estimatedEntropyBits.toFixed(1)}</p></div><div className="p-3 bg-slate-800 rounded-lg"><p className="text-xs text-slate-400">Mean</p><p className="text-lg font-bold">{diceStats.mean.toFixed(2)}</p></div><div className="p-3 bg-slate-800 rounded-lg"><p className="text-xs text-slate-400">Chi-Square</p><p className={`text-lg font-bold ${diceStats.chiSquare > 11.07 ? 'text-amber-400' : ''}`}>{diceStats.chiSquare.toFixed(2)}</p></div></div>)}
|
||||
{diceOnlyMnemonic && (<div className="space-y-1 pt-2"><label className="text-xs font-semibold text-slate-400">Dice-Only Preview Mnemonic</label><p data-sensitive="Dice-Only Preview Mnemonic" className="p-3 bg-slate-800 rounded-md font-mono text-sm text-slate-100 break-words">{diceOnlyMnemonic}</p></div>)}
|
||||
<textarea value={diceRolls} onChange={(e) => setDiceRolls(e.target.value.replace(/[^1-6]/g, ''))} placeholder="Enter 99+ dice rolls (e.g., 16345...)" className="w-full h-32 p-3 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-lg font-mono text-[#00f0ff] placeholder-[#9d84b7]" />
|
||||
{dicePatternWarning && (<div className="p-3 bg-[#ff006e]/10 border-2 border-[#ff006e]/30 text-[#ff006e] rounded-lg text-sm flex gap-3"><AlertTriangle /><p><span className="font-bold">Warning:</span> {dicePatternWarning}</p></div>)}
|
||||
{diceStats && diceStats.length > 0 && (<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-center"><div className="p-3 bg-[#1a1a2e] rounded-lg border-2 border-[#00f0ff]/30"><p className="text-xs text-[#6ef3f7]">Rolls</p><p className="text-lg font-bold text-[#00f0ff]">{diceStats.length}</p></div><div className="p-3 bg-[#1a1a2e] rounded-lg border-2 border-[#00f0ff]/30"><p className="text-xs text-[#6ef3f7]">Entropy (bits)</p><p className="text-lg font-bold text-[#00f0ff]">{diceStats.estimatedEntropyBits.toFixed(1)}</p></div><div className="p-3 bg-[#1a1a2e] rounded-lg border-2 border-[#00f0ff]/30"><p className="text-xs text-[#6ef3f7]">Mean</p><p className="text-lg font-bold text-[#00f0ff]">{diceStats.mean.toFixed(2)}</p></div><div className="p-3 bg-[#1a1a2e] rounded-lg border-2 border-[#00f0ff]/30"><p className="text-xs text-[#6ef3f7]">Chi-Square</p><p className={`text-lg font-bold ${diceStats.chiSquare > 11.07 ? 'text-[#ff006e]' : 'text-[#00f0ff]'}`}>{diceStats.chiSquare.toFixed(2)}</p></div></div>)}
|
||||
{diceOnlyMnemonic && (<div className="space-y-1 pt-2"><label className="text-xs font-semibold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Dice-Only Preview Mnemonic</label><p data-sensitive="Dice-Only Preview Mnemonic" className="p-3 bg-[#1a1a2e] rounded-md font-mono text-sm text-[#00f0ff] break-words border-2 border-[#00f0ff]/30">{diceOnlyMnemonic}</p></div>)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-slate-900/70 rounded-xl border-2 border-teal-500/50 shadow-lg">
|
||||
<h3 className="font-semibold text-lg mb-4 text-slate-200">Step 4: Generate Final Mnemonic</h3>
|
||||
<div className="p-6 bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/50 shadow-[0_0_20px_rgba(0,240,255,0.3)]">
|
||||
<h3 className="font-semibold text-lg mb-4 text-[#00f0ff]" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Step 4: Generate Final Mnemonic</h3>
|
||||
{finalMnemonic ? (
|
||||
<div className="p-4 bg-gradient-to-br from-green-50 to-emerald-50 border-2 border-green-300 rounded-2xl shadow-lg">
|
||||
<div className="flex items-center justify-between mb-4"><span className="font-bold text-green-700 flex items-center gap-2 text-lg"><CheckCircle2 size={22} /> Final Mnemonic Generated</span><button onClick={() => setFinalMnemonic(null)} className="p-2.5 hover:bg-green-100 rounded-xl"><EyeOff size={22} /></button></div>
|
||||
<div className="p-6 bg-white rounded-xl border-2 border-green-200 shadow-sm"><p data-sensitive="Final Blended Mnemonic" className="font-mono text-center text-lg text-slate-800 break-words">{finalMnemonic}</p></div>
|
||||
<div className="mt-4 p-3 bg-red-500/10 text-red-300 rounded-lg text-xs flex gap-2"><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="p-4 bg-[#0a0a0f] border-2 border-[#39ff14] rounded-2xl shadow-[0_0_20px_rgba(57,255,20,0.3)]">
|
||||
<div className="flex items-center justify-between mb-4"><span className="font-bold text-[#39ff14] flex items-center gap-2 text-lg" style={{ textShadow: '0 0 10px rgba(57,255,20,0.8)' }}><CheckCircle2 size={22} /> Final Mnemonic Generated</span><button onClick={() => setFinalMnemonic(null)} className="p-2.5 hover:bg-[#16213e] rounded-xl transition-all text-[#39ff14] hover:shadow-[0_0_15px_rgba(57,255,20,0.5)] flex items-center gap-2"><EyeOff size={22} /> Hide</button></div>
|
||||
<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-slate-700 text-white rounded-lg font-semibold flex items-center justify-center gap-2"><QrCode size={16}/> Export as QR</button>
|
||||
<button onClick={handleTransfer} className="w-full py-2.5 bg-teal-600 text-white rounded-lg font-semibold flex items-center justify-center gap-2"><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-[#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>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<><p className="text-sm text-slate-400 mb-4">Once you have entered valid mnemonics and at least 50 dice rolls, you can generate the final mnemonic.</p><button onClick={handleFinalMix} disabled={!blendedResult || !diceRolls || diceRolls.length < 50 || mixing} className="w-full py-3 bg-gradient-to-r from-teal-500 to-cyan-600 text-white rounded-xl font-bold flex items-center justify-center gap-2 disabled:opacity-50">{mixing ? <RefreshCw className="animate-spin" size={20} /> : <Sparkles size={20} />}{mixing ? 'Generating...' : 'Mix Mnemonic + Dice'}</button></>
|
||||
<><p className="text-sm text-[#6ef3f7] mb-4">Once you have entered valid mnemonics and at least 50 dice rolls, you can generate the final mnemonic.</p><button onClick={handleFinalMix} disabled={!blendedResult || !diceRolls || diceRolls.length < 50 || mixing} className="w-full py-3 bg-gradient-to-r from-[#00f0ff] to-[#0066ff] text-[#16213e] rounded-xl font-bold flex items-center justify-center gap-2 disabled:opacity-50 hover:shadow-[0_0_20px_rgba(0,240,255,0.5)]">{mixing ? <RefreshCw className="animate-spin" size={20} /> : <Sparkles size={20} />}{mixing ? 'Generating...' : 'Mix Mnemonic + Dice'}</button></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -340,11 +340,11 @@ export function SeedBlender({ onDirtyStateChange, setMnemonicForBackup, requestT
|
||||
/>}
|
||||
{showFinalQR && finalMnemonic && (
|
||||
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center" onClick={() => setShowFinalQR(false)}>
|
||||
<div className="bg-white rounded-2xl p-4" onClick={e => e.stopPropagation()}>
|
||||
<div className="bg-[#16213e] rounded-2xl p-4 border-2 border-[#00f0ff]/50" onClick={e => e.stopPropagation()}>
|
||||
<QrDisplay value={finalMnemonic} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@ const ClipboardBadge: React.FC<ClipboardBadgeProps> = ({ events, onOpenClipboard
|
||||
// Determine badge style based on clipboard count
|
||||
const badgeStyle =
|
||||
count === 0
|
||||
? "text-green-500 bg-green-500/10 border-green-500/20" // Safe
|
||||
? "text-[#39ff14] bg-[#39ff14]/10 border-[#39ff14]/20" // Safe
|
||||
: count < 5
|
||||
? "text-amber-500 bg-amber-500/10 border-amber-500/30 font-semibold" // Warning
|
||||
: "text-red-500 bg-red-500/10 border-red-500/30 font-bold animate-pulse"; // Danger
|
||||
? "text-[#ff006e] bg-[#ff006e]/10 border-[#ff006e]/30 font-semibold" // Warning
|
||||
: "text-[#ff006e] bg-[#ff006e]/10 border-[#ff006e]/30 font-bold animate-pulse"; // Danger
|
||||
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -12,8 +12,8 @@ const EditLockBadge: React.FC<EditLockBadgeProps> = ({ isLocked, onToggle }) =>
|
||||
onClick={onToggle}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg border transition-all hover:scale-105 ${
|
||||
isLocked
|
||||
? 'text-amber-500 bg-amber-500/10 border-amber-500/30 font-semibold'
|
||||
: 'text-green-500 bg-green-500/10 border-green-500/30'
|
||||
? 'text-[#ff006e] bg-[#ff006e]/10 border-[#ff006e]/30 font-semibold'
|
||||
: 'text-[#39ff14] bg-[#39ff14]/10 border-[#39ff14]/30'
|
||||
}`}
|
||||
title={isLocked ? 'Click to unlock and edit' : 'Click to lock and blur sensitive data'}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user