mirror of
https://github.com/kccleoc/seedpgp-web.git
synced 2026-03-07 01:47:52 +08:00
Fix TypeScript errors, remove non-functional Empty button, right-align Network Block toggle
- Fix CameraEntropy and DiceEntropy import errors - Fix unused variable warnings in App.tsx and Header.tsx - Remove non-functional Empty storage button from Header - Right-align Network Block toggle button with flex-1 spacer - Add NetworkBlockBadge component file
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
<title>SeedPGP v__APP_VERSION__</title>
|
<title>SeedPGP v__APP_VERSION__</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
112
src/App.tsx
112
src/App.tsx
@@ -68,7 +68,7 @@ function App() {
|
|||||||
const [showQRScanner, setShowQRScanner] = useState(false);
|
const [showQRScanner, setShowQRScanner] = useState(false);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [isReadOnly, setIsReadOnly] = useState(false);
|
const [isReadOnly, setIsReadOnly] = useState(false);
|
||||||
const [encryptedMnemonicCache, setEncryptedMnemonicCache] = useState<EncryptedBlob | null>(null);
|
const [_encryptedMnemonicCache, _setEncryptedMnemonicCache] = useState<EncryptedBlob | null>(null);
|
||||||
const [showSecurityModal, setShowSecurityModal] = useState(false);
|
const [showSecurityModal, setShowSecurityModal] = useState(false);
|
||||||
const [showStorageModal, setShowStorageModal] = useState(false);
|
const [showStorageModal, setShowStorageModal] = useState(false);
|
||||||
const [showClipboardModal, setShowClipboardModal] = useState(false);
|
const [showClipboardModal, setShowClipboardModal] = useState(false);
|
||||||
@@ -88,6 +88,9 @@ function App() {
|
|||||||
const [seedForBlender, setSeedForBlender] = useState<string>('');
|
const [seedForBlender, setSeedForBlender] = useState<string>('');
|
||||||
const [blenderResetKey, setBlenderResetKey] = useState(0);
|
const [blenderResetKey, setBlenderResetKey] = useState(0);
|
||||||
|
|
||||||
|
// Network blocking state
|
||||||
|
const [isNetworkBlocked, setIsNetworkBlocked] = useState(false);
|
||||||
|
|
||||||
// Entropy generation states
|
// Entropy generation states
|
||||||
const [entropySource, setEntropySource] = useState<'camera' | 'dice' | 'audio' | null>(null);
|
const [entropySource, setEntropySource] = useState<'camera' | 'dice' | 'audio' | null>(null);
|
||||||
const [entropyStats, setEntropyStats] = useState<any>(null);
|
const [entropyStats, setEntropyStats] = useState<any>(null);
|
||||||
@@ -335,7 +338,7 @@ function App() {
|
|||||||
await getSessionKey();
|
await getSessionKey();
|
||||||
// Encrypt mnemonic with session key and clear plaintext state
|
// Encrypt mnemonic with session key and clear plaintext state
|
||||||
const blob = await encryptJsonToBlob({ mnemonic, timestamp: Date.now() });
|
const blob = await encryptJsonToBlob({ mnemonic, timestamp: Date.now() });
|
||||||
setEncryptedMnemonicCache(blob);
|
_setEncryptedMnemonicCache(blob);
|
||||||
setMnemonic(''); // Clear plaintext mnemonic
|
setMnemonic(''); // Clear plaintext mnemonic
|
||||||
|
|
||||||
// Clear password after successful encryption (security best practice)
|
// Clear password after successful encryption (security best practice)
|
||||||
@@ -441,7 +444,7 @@ function App() {
|
|||||||
// Encrypt the restored mnemonic with the session key
|
// Encrypt the restored mnemonic with the session key
|
||||||
await getSessionKey();
|
await getSessionKey();
|
||||||
const blob = await encryptJsonToBlob({ mnemonic: result.w, timestamp: Date.now() });
|
const blob = await encryptJsonToBlob({ mnemonic: result.w, timestamp: Date.now() });
|
||||||
setEncryptedMnemonicCache(blob);
|
_setEncryptedMnemonicCache(blob);
|
||||||
|
|
||||||
// Temporarily display the mnemonic and then clear it
|
// Temporarily display the mnemonic and then clear it
|
||||||
setDecryptedRestoredMnemonic(result.w);
|
setDecryptedRestoredMnemonic(result.w);
|
||||||
@@ -462,21 +465,28 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleLock = () => {
|
const handleToggleNetwork = () => {
|
||||||
if (!isReadOnly) {
|
setIsNetworkBlocked(!isNetworkBlocked);
|
||||||
// About to lock - show confirmation
|
|
||||||
setShowLockConfirm(true);
|
if (!isNetworkBlocked) {
|
||||||
|
// Block network
|
||||||
|
console.log('🚫 Network BLOCKED - No external requests allowed');
|
||||||
|
// Optional: Override fetch/XMLHttpRequest
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
(window as any).__originalFetch = window.fetch;
|
||||||
|
// 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;
|
||||||
|
window.fetch = mockFetch;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Unlocking - no confirmation needed
|
// Unblock network
|
||||||
setIsReadOnly(false);
|
console.log('🌐 Network ACTIVE');
|
||||||
|
if ((window as any).__originalFetch) {
|
||||||
|
window.fetch = (window as any).__originalFetch;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmLock = () => {
|
|
||||||
setIsReadOnly(true);
|
|
||||||
setShowLockConfirm(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRequestTabChange = (newTab: 'create' | 'backup' | 'restore' | 'seedblender') => {
|
const handleRequestTabChange = (newTab: 'create' | 'backup' | 'restore' | 'seedblender') => {
|
||||||
// Allow free navigation - no warnings
|
// Allow free navigation - no warnings
|
||||||
// User can manually reset Seed Blender with "Reset All" button
|
// User can manually reset Seed Blender with "Reset All" button
|
||||||
@@ -501,7 +511,7 @@ function App() {
|
|||||||
setSeedForBlender('');
|
setSeedForBlender('');
|
||||||
// Clear session
|
// Clear session
|
||||||
destroySessionKey();
|
destroySessionKey();
|
||||||
setEncryptedMnemonicCache(null);
|
_setEncryptedMnemonicCache(null);
|
||||||
// Go to Create tab (fresh start)
|
// Go to Create tab (fresh start)
|
||||||
// Force SeedBlender to remount (resets its internal state)
|
// Force SeedBlender to remount (resets its internal state)
|
||||||
setBlenderResetKey(prev => prev + 1);
|
setBlenderResetKey(prev => prev + 1);
|
||||||
@@ -558,10 +568,9 @@ function App() {
|
|||||||
onOpenClipboardModal={() => setShowClipboardModal(true)}
|
onOpenClipboardModal={() => setShowClipboardModal(true)}
|
||||||
activeTab={activeTab}
|
activeTab={activeTab}
|
||||||
onRequestTabChange={handleRequestTabChange}
|
onRequestTabChange={handleRequestTabChange}
|
||||||
encryptedMnemonicCache={encryptedMnemonicCache}
|
|
||||||
appVersion={__APP_VERSION__}
|
appVersion={__APP_VERSION__}
|
||||||
isLocked={isReadOnly}
|
isNetworkBlocked={isNetworkBlocked}
|
||||||
onToggleLock={handleToggleLock}
|
onToggleNetwork={handleToggleNetwork}
|
||||||
onResetAll={handleResetAll}
|
onResetAll={handleResetAll}
|
||||||
/>
|
/>
|
||||||
<main className="w-full px-4 py-3">
|
<main className="w-full px-4 py-3">
|
||||||
@@ -725,10 +734,17 @@ function App() {
|
|||||||
<CheckCircle2 size={20} /> Generated Successfully
|
<CheckCircle2 size={20} /> Generated Successfully
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 bg-[#16213e] rounded-lg border border-[#39ff14]/50">
|
<div className="relative">
|
||||||
<p className="font-mono text-xs text-[#39ff14] break-words leading-relaxed" style={{ textShadow: '0 0 5px rgba(57,255,20,0.5)' }}>
|
<p
|
||||||
|
className="font-mono text-xs text-[#39ff14] break-words leading-relaxed blur-sensitive"
|
||||||
|
title="Hover to reveal seed"
|
||||||
|
style={{ textShadow: '0 0 5px rgba(57,255,20,0.5)' }}
|
||||||
|
>
|
||||||
{generatedSeed}
|
{generatedSeed}
|
||||||
</p>
|
</p>
|
||||||
|
<p className="text-[9px] text-[#6ef3f7] mt-2 text-center">
|
||||||
|
👆 Hover to reveal - Write down securely
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -770,20 +786,29 @@ function App() {
|
|||||||
<div className={activeTab === 'backup' ? 'block' : 'hidden'}>
|
<div className={activeTab === 'backup' ? 'block' : 'hidden'}>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>BIP39 Mnemonic</label>
|
<label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>BIP39 Mnemonic</label>
|
||||||
<textarea
|
<div className="relative">
|
||||||
className={`w-full h-32 p-3 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg font-mono text-xs 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 ${isReadOnly ? 'blur-sm select-none' : ''
|
<textarea
|
||||||
|
value={mnemonic}
|
||||||
|
onChange={(e) => setMnemonic(e.target.value)}
|
||||||
|
onFocus={(e) => e.target.classList.remove('blur-sensitive')}
|
||||||
|
onBlur={(e) => mnemonic && e.target.classList.add('blur-sensitive')}
|
||||||
|
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 ${
|
||||||
|
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)'
|
||||||
}}
|
}}
|
||||||
|
data-sensitive="BIP39 Mnemonic"
|
||||||
data-sensitive="BIP39 Mnemonic"
|
readOnly={isReadOnly}
|
||||||
placeholder="Enter your 12 or 24 word seed phrase..."
|
/>
|
||||||
value={mnemonic}
|
{mnemonic && (
|
||||||
onChange={(e) => setMnemonic(e.target.value)}
|
<p className="text-[9px] text-[#6ef3f7] mt-1">
|
||||||
readOnly={isReadOnly}
|
👆 Hover or click to reveal
|
||||||
/>
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PgpKeyInput
|
<PgpKeyInput
|
||||||
@@ -834,7 +859,7 @@ function App() {
|
|||||||
|
|
||||||
{/* Existing restore input textarea stays here */}
|
{/* Existing restore input textarea stays here */}
|
||||||
<textarea
|
<textarea
|
||||||
className={`w-full p-3 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg font-mono text-xs 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 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`}
|
||||||
rows={6}
|
rows={6}
|
||||||
placeholder="Or paste encrypted data here..."
|
placeholder="Or paste encrypted data here..."
|
||||||
value={restoreInput}
|
value={restoreInput}
|
||||||
@@ -1097,11 +1122,18 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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)]">
|
||||||
<p className={`font-mono text-center text-base break-words text-[#39ff14] selection:bg-[#39ff14] selection:text-[#0a0a0f] ${isReadOnly ? 'blur-md select-none' : ''
|
<div className="relative">
|
||||||
}`}
|
<p
|
||||||
style={{ textShadow: '0 0 8px rgba(57,255,20,0.8)' }}>
|
className="font-mono text-center text-base break-words text-[#39ff14] blur-sensitive"
|
||||||
{decryptedRestoredMnemonic}
|
title="Hover to reveal"
|
||||||
</p>
|
style={{ textShadow: '0 0 8px rgba(57,255,20,0.8)' }}
|
||||||
|
>
|
||||||
|
{decryptedRestoredMnemonic}
|
||||||
|
</p>
|
||||||
|
<p className="text-[9px] text-[#6ef3f7] mt-2 text-center">
|
||||||
|
👆 Hover to reveal decrypted seed
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1212,7 +1244,7 @@ function App() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="flex-1 py-2 bg-[#ff006e] hover:bg-[#ff4d8f] text-white font-semibold rounded-lg transition-all hover:shadow-[0_0_15px_rgba(255,0,110,0.5)]"
|
className="flex-1 py-2 bg-[#ff006e] hover:bg-[#ff4d8f] text-white font-semibold rounded-lg transition-all hover:shadow-[0_0_15px_rgba(255,0,110,0.5)]"
|
||||||
onClick={confirmLock}
|
onClick={() => setIsReadOnly(true)}
|
||||||
>
|
>
|
||||||
Lock Data
|
Lock Data
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -168,7 +168,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-xs 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-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" />
|
||||||
<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>)}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Shield, RefreshCw } from 'lucide-react';
|
|||||||
import SecurityBadge from './badges/SecurityBadge';
|
import SecurityBadge from './badges/SecurityBadge';
|
||||||
import StorageBadge from './badges/StorageBadge';
|
import StorageBadge from './badges/StorageBadge';
|
||||||
import ClipboardBadge from './badges/ClipboardBadge';
|
import ClipboardBadge from './badges/ClipboardBadge';
|
||||||
import EditLockBadge from './badges/EditLockBadge';
|
|
||||||
|
|
||||||
interface StorageItem {
|
interface StorageItem {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -27,10 +26,9 @@ interface HeaderProps {
|
|||||||
onOpenClipboardModal: () => void;
|
onOpenClipboardModal: () => void;
|
||||||
activeTab: 'create' | 'backup' | 'restore' | 'seedblender';
|
activeTab: 'create' | 'backup' | 'restore' | 'seedblender';
|
||||||
onRequestTabChange: (tab: 'create' | 'backup' | 'restore' | 'seedblender') => void;
|
onRequestTabChange: (tab: 'create' | 'backup' | 'restore' | 'seedblender') => void;
|
||||||
encryptedMnemonicCache: any;
|
|
||||||
appVersion: string;
|
appVersion: string;
|
||||||
isLocked: boolean;
|
isNetworkBlocked: boolean;
|
||||||
onToggleLock: () => void;
|
onToggleNetwork: () => void;
|
||||||
onResetAll: () => void; // NEW
|
onResetAll: () => void; // NEW
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,17 +41,16 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
onOpenClipboardModal,
|
onOpenClipboardModal,
|
||||||
activeTab,
|
activeTab,
|
||||||
onRequestTabChange,
|
onRequestTabChange,
|
||||||
encryptedMnemonicCache,
|
|
||||||
appVersion,
|
appVersion,
|
||||||
isLocked,
|
isNetworkBlocked,
|
||||||
onToggleLock,
|
onToggleNetwork,
|
||||||
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-50 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 */}
|
{/* 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">
|
||||||
<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)]">
|
<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)]">
|
||||||
@@ -66,11 +63,21 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
<p className="text-xs text-[#6ef3f7]">OpenPGP-secured BIP39 backup</p>
|
<p className="text-xs text-[#6ef3f7]">OpenPGP-secured BIP39 backup</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Reset button - top right */}
|
||||||
|
<button
|
||||||
|
onClick={onResetAll}
|
||||||
|
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-[#16213e] border border-[#ff006e] text-[#ff006e] rounded-lg font-medium hover:bg-[#ff006e20] transition-all"
|
||||||
|
title="Reset all data"
|
||||||
|
>
|
||||||
|
<RefreshCw size={12} />
|
||||||
|
<span className="hidden sm:inline">Reset</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ROW 2: Monitoring Badges + Action Buttons */}
|
{/* ROW 2: Badges (LEFT) | Action Buttons (RIGHT) */}
|
||||||
<div className="flex items-center justify-between gap-2 pb-2 border-b border-[#00f0ff20]">
|
<div className="flex items-center gap-2 pb-2 border-b border-[#00f0ff20]">
|
||||||
{/* Left: Badges */}
|
{/* Left: Monitoring Badges */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<SecurityBadge onClick={onOpenSecurityModal} />
|
<SecurityBadge onClick={onOpenSecurityModal} />
|
||||||
<div onClick={onOpenStorageModal} className="cursor-pointer">
|
<div onClick={onOpenStorageModal} className="cursor-pointer">
|
||||||
@@ -79,40 +86,27 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
<div onClick={onOpenClipboardModal} className="cursor-pointer">
|
<div onClick={onOpenClipboardModal} className="cursor-pointer">
|
||||||
<ClipboardBadge events={events} onOpenClipboardModal={onOpenClipboardModal} />
|
<ClipboardBadge events={events} onOpenClipboardModal={onOpenClipboardModal} />
|
||||||
</div>
|
</div>
|
||||||
<EditLockBadge isLocked={isLocked} onToggle={onToggleLock} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Spacer - pushes right content to the right */}
|
||||||
|
<div className="flex-1"></div>
|
||||||
|
|
||||||
{/* Right: Action Buttons */}
|
{/* Right: Action Buttons */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{encryptedMnemonicCache && (
|
{/* Network Block toggle */}
|
||||||
<button
|
|
||||||
onClick={onToggleLock}
|
|
||||||
className="px-2 py-1.5 text-base bg-[#16213e] border border-[#00f0ff] text-[#00f0ff] rounded-lg font-medium hover:bg-[#00f0ff20] transition-all"
|
|
||||||
title={isLocked ? "Show sensitive data" : "Hide sensitive data"}
|
|
||||||
>
|
|
||||||
{isLocked ? '🔓' : '🙈'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={onToggleNetwork}
|
||||||
try {
|
className={`flex items-center gap-1 px-2.5 py-1.5 text-xs rounded-lg font-medium transition-all whitespace-nowrap ${
|
||||||
await navigator.clipboard.writeText('');
|
isNetworkBlocked
|
||||||
} catch { }
|
? 'bg-[#16213e] border border-[#ff006e] text-[#ff006e] hover:bg-[#ff006e20]'
|
||||||
localStorage.clear();
|
: 'bg-[#16213e] border border-[#39ff14] text-[#39ff14] hover:bg-[#39ff1420]'
|
||||||
sessionStorage.clear();
|
}`}
|
||||||
}}
|
title={isNetworkBlocked ? 'Network BLOCKED' : 'Network ACTIVE'}
|
||||||
className="px-2 py-1.5 text-base bg-[#16213e] border border-[#00f0ff] text-[#00f0ff] rounded-lg font-medium hover:bg-[#00f0ff20] transition-all"
|
|
||||||
title="Clear clipboard and storage"
|
|
||||||
>
|
>
|
||||||
📋
|
<span className="text-sm">{isNetworkBlocked ? '🚫' : '🌐'}</span>
|
||||||
</button>
|
<span className="hidden sm:inline text-[10px]">
|
||||||
|
{isNetworkBlocked ? 'Blocked' : 'Active'}
|
||||||
<button
|
</span>
|
||||||
onClick={onResetAll}
|
|
||||||
className="px-2 py-1.5 text-xs bg-[#16213e] border border-[#ff006e] text-[#ff006e] rounded-lg font-medium hover:bg-[#ff006e20] transition-all whitespace-nowrap flex items-center gap-1"
|
|
||||||
>
|
|
||||||
<RefreshCw size={12} />
|
|
||||||
Reset
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,10 +114,11 @@ 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 ${activeTab === 'create'
|
className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${
|
||||||
? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
|
activeTab === 'create'
|
||||||
: 'bg-[#0a0a0f] text-[#9d84b7] border-2 border-[#00f0ff30] hover:text-[#6ef3f7] hover:border-[#00f0ff50]'
|
? '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]'
|
||||||
|
}`}
|
||||||
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')}
|
||||||
>
|
>
|
||||||
@@ -131,10 +126,11 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${activeTab === 'backup'
|
className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${
|
||||||
? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
|
activeTab === 'backup'
|
||||||
: 'bg-[#0a0a0f] text-[#9d84b7] border-2 border-[#00f0ff30] hover:text-[#6ef3f7] hover:border-[#00f0ff50]'
|
? '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]'
|
||||||
|
}`}
|
||||||
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')}
|
||||||
>
|
>
|
||||||
@@ -142,10 +138,11 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${activeTab === 'restore'
|
className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${
|
||||||
? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
|
activeTab === 'restore'
|
||||||
: 'bg-[#0a0a0f] text-[#9d84b7] border-2 border-[#00f0ff30] hover:text-[#6ef3f7] hover:border-[#00f0ff50]'
|
? '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]'
|
||||||
|
}`}
|
||||||
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')}
|
||||||
>
|
>
|
||||||
@@ -153,10 +150,11 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${activeTab === 'seedblender'
|
className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${
|
||||||
? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
|
activeTab === 'seedblender'
|
||||||
: 'bg-[#0a0a0f] text-[#9d84b7] border-2 border-[#00f0ff30] hover:text-[#6ef3f7] hover:border-[#00f0ff50]'
|
? '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]'
|
||||||
|
}`}
|
||||||
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')}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -311,8 +311,12 @@ export function SeedBlender({ onDirtyStateChange, setMnemonicForBackup, requestT
|
|||||||
<textarea
|
<textarea
|
||||||
value={entry.rawInput}
|
value={entry.rawInput}
|
||||||
onChange={(e) => updateEntry(index, { rawInput: e.target.value, decryptedMnemonic: e.target.value, isValid: null, error: null })}
|
onChange={(e) => updateEntry(index, { rawInput: e.target.value, decryptedMnemonic: e.target.value, isValid: null, error: null })}
|
||||||
|
onFocus={(e) => e.target.classList.remove('blur-sensitive')}
|
||||||
|
onBlur={(e) => entry.rawInput && e.target.classList.add('blur-sensitive')}
|
||||||
placeholder={`Mnemonic #${index + 1} (12 or 24 words)`}
|
placeholder={`Mnemonic #${index + 1} (12 or 24 words)`}
|
||||||
className={`w-full h-24 p-3 bg-[#0a0a0f] border-2 rounded-lg font-mono text-xs 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 ${getBorderColor(entry.isValid)}`}
|
className={`w-full h-24 p-3 bg-[#0a0a0f] border-2 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 ${getBorderColor(entry.isValid)} ${
|
||||||
|
entry.rawInput ? 'blur-sensitive' : ''
|
||||||
|
}`}
|
||||||
/>
|
/>
|
||||||
{/* Row 2: QR button (left) and X button (right) */}
|
{/* Row 2: QR button (left) and X button (right) */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -350,7 +354,7 @@ export function SeedBlender({ onDirtyStateChange, setMnemonicForBackup, requestT
|
|||||||
<div className="p-6 bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/30">
|
<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>
|
<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">
|
<div className="space-y-4">
|
||||||
<textarea value={diceRolls} onChange={(e) => setDiceRolls(e.target.value.replace(/[^1-6]/g, ''))} placeholder="99+ dice rolls (e.g., 16345...)" className="w-full h-32 p-3 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg font-mono text-xs placeholder:text-[10px] placeholder:text-[#6ef3f7]" />
|
<textarea value={diceRolls} onChange={(e) => setDiceRolls(e.target.value.replace(/[^1-6]/g, ''))} placeholder="99+ dice rolls (e.g., 16345...)" className="w-full h-32 p-3 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg font-mono text-sm placeholder:text-[10px] placeholder:text-[#6ef3f7]" />
|
||||||
{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>)}
|
{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>)}
|
{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>)}
|
{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>)}
|
||||||
|
|||||||
28
src/components/badges/NetworkBlockBadge.tsx
Normal file
28
src/components/badges/NetworkBlockBadge.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Wifi, WifiOff } from 'lucide-react';
|
||||||
|
|
||||||
|
interface NetworkBlockBadgeProps {
|
||||||
|
isBlocked: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NetworkBlockBadge: React.FC<NetworkBlockBadgeProps> = ({ isBlocked, onToggle }) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onToggle}
|
||||||
|
className={`flex items-center gap-1.5 px-2 py-1 rounded-lg text-xs font-medium transition-all ${
|
||||||
|
isBlocked
|
||||||
|
? 'bg-[#ff006e20] border border-[#ff006e] text-[#ff006e] hover:bg-[#ff006e30]'
|
||||||
|
: 'bg-[#39ff1420] border border-[#39ff14] text-[#39ff14] hover:bg-[#39ff1430]'
|
||||||
|
}`}
|
||||||
|
title={isBlocked ? 'Network is BLOCKED' : 'Network is ACTIVE'}
|
||||||
|
>
|
||||||
|
{isBlocked ? <WifiOff size={12} /> : <Wifi size={12} />}
|
||||||
|
<span className="hidden sm:inline">
|
||||||
|
{isBlocked ? 'Blocked' : 'Active'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NetworkBlockBadge;
|
||||||
@@ -2,6 +2,19 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* Prevent iOS zoom on input focus by ensuring font-size >= 16px */
|
||||||
|
input, textarea, select {
|
||||||
|
font-size: 16px !important; /* iOS won't zoom if 16px or larger */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For smaller text, use transform scale instead */
|
||||||
|
.text-xs input,
|
||||||
|
.text-xs textarea {
|
||||||
|
font-size: 16px !important;
|
||||||
|
transform: scale(0.75);
|
||||||
|
transform-origin: left top;
|
||||||
|
}
|
||||||
|
|
||||||
/* Mobile-first: constrain to phone width on all devices */
|
/* Mobile-first: constrain to phone width on all devices */
|
||||||
#root {
|
#root {
|
||||||
max-width: 448px;
|
max-width: 448px;
|
||||||
@@ -32,4 +45,25 @@ body {
|
|||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
monospace;
|
monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sensitive data blur protection */
|
||||||
|
.blur-sensitive {
|
||||||
|
filter: blur(6px);
|
||||||
|
transition: filter 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blur-sensitive:hover,
|
||||||
|
.blur-sensitive:focus {
|
||||||
|
filter: blur(0);
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile: tap to reveal */
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
.blur-sensitive:active {
|
||||||
|
filter: blur(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user