feat: mobile-first redesign and layout improvements

## Major Changes

### Mobile-First Responsive Design
- Converted entire app to mobile-first single-column layout
- Constrained max-width to 448px (mobile phone width)
- Black margins on desktop, centered content
- Removed all multi-column grids (md:grid-cols-3)

### Header Reorganization (3-Row Layout)
- Row 1: App logo + title + version
- Row 2: Security badges + action buttons (Empty, Reset)
- Row 3: Navigation tabs (Create, Backup, Restore, Blender)
- Replaced text buttons with emoji icons (📋 clipboard, 🙈 privacy mask)
- Consistent button sizing across all tabs

### Font Size Reductions
- Reduced all button text sizes for mobile density
- Main buttons: py-4 → py-3, added text-sm
- Labels: text-xs → text-[10px]
- Placeholders: consistent text-[10px] across all inputs
- Input fields: text-sm → text-xs, p-4 → p-3

### Create Tab Improvements
- Changed "GENERATE NEW SEED" from button-style to banner
- Left-aligned banner with gradient background
- Equal-width button grid (12/24 Words, Backup/Seed Blender)
- Used grid-cols-2 for consistent sizing

### Backup Tab Improvements
- Simplified drag-drop area with 📎 emoji
- Reduced padding and text sizes
- Cleaner, shorter copy
- PGP label font size: text-xs → text-[12px]

### SeedBlender Component
- Reorganized mnemonic input cards: textarea on row 1, buttons on row 2
- QR button (left) and X button (right) alignment
- Consistent placeholder text sizing (text-[10px])
- Shortened dice roll placeholder text

### HTTPS Development Server
- Added @vitejs/plugin-basic-ssl for HTTPS in dev mode
- Configured server to listen on 0.0.0.0:5173
- Fixed Web Crypto API issues on mobile (requires secure context)
- Enables testing on iPhone via local network

## Technical Details
- All changes maintain cyberpunk theme and color scheme
- Improved mobile usability and visual consistency
- No functionality changes, pure UI/UX improvements
This commit is contained in:
LC mac
2026-02-09 21:58:18 +08:00
parent 75da988968
commit 185efe454f
9 changed files with 605 additions and 540 deletions

View File

@@ -27,6 +27,7 @@
"@types/qrcode-generator": "^1.0.6", "@types/qrcode-generator": "^1.0.6",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"@vitejs/plugin-basic-ssl": "^1.1.0",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"postcss": "^8.4.49", "postcss": "^8.4.49",
@@ -263,6 +264,8 @@
"@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="], "@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="],
"@vitejs/plugin-basic-ssl": ["@vitejs/plugin-basic-ssl@1.2.0", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" } }, "sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],

View File

@@ -33,6 +33,7 @@
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"@vitejs/plugin-basic-ssl": "^1.1.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",

View File

@@ -565,12 +565,14 @@ function App() {
return ( return (
<div className="min-h-screen bg-[#0a0a0f] bg-[radial-gradient(ellipse_at_top,_#1a1a2e_0%,_#0a0a0f_100%)] relative"> <div className="min-h-screen bg-black">
<div className="max-w-md mx-auto min-h-screen bg-[#0a0a0f] bg-[radial-gradient(ellipse_at_top,#1a1a2e_0%,#0a0a0f_100%)] relative">
{/* Cyberpunk grid overlay */} {/* Cyberpunk grid overlay */}
<div className="absolute inset-0 bg-[linear-gradient(rgba(0,240,255,0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(0,240,255,0.03)_1px,transparent_1px)] bg-[size:50px_50px] pointer-events-none" /> <div className="absolute inset-0 bg-[linear-gradient(rgba(0,240,255,0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(0,240,255,0.03)_1px,transparent_1px)] bg-[size:50px_50px] pointer-events-none" />
{/* Content wrapper */} {/* Content wrapper */}
<div className="relative z-10"> <div className="relative z-10">
<Header <Header
onOpenSecurityModal={() => setShowSecurityModal(true)} onOpenSecurityModal={() => setShowSecurityModal(true)}
localItems={localItems} localItems={localItems}
@@ -587,7 +589,7 @@ function App() {
onToggleLock={handleToggleLock} onToggleLock={handleToggleLock}
onResetAll={handleResetAll} onResetAll={handleResetAll}
/> />
<main className="max-w-7xl mx-auto px-6 py-4"> <main className="w-full px-4 py-3">
<div className="bg-[#1a1a2e] rounded-xl border-2 border-[#00f0ff]/30 shadow-[0_0_30px_rgba(0,240,255,0.3)] p-8"> <div className="bg-[#1a1a2e] rounded-xl border-2 border-[#00f0ff]/30 shadow-[0_0_30px_rgba(0,240,255,0.3)] p-8">
<div className="p-6 md:p-8 space-y-6"> <div className="p-6 md:p-8 space-y-6">
{/* Error Display */} {/* Error Display */}
@@ -615,31 +617,28 @@ function App() {
)} )}
{/* Main Content Grid */} {/* Main Content Grid */}
<div className="grid gap-6 md:grid-cols-3 md:items-start"> <div className="space-y-6">
<div className="md:col-span-2 space-y-6"> <div className="space-y-6">
<div className={activeTab === 'create' ? 'block' : 'hidden'}> <div className={activeTab === 'create' ? 'block' : 'hidden'}>
<div className="space-y-6"> <div className="space-y-6">
<div className="text-center space-y-4"> <div className="space-y-4">
<div className="inline-flex items-center gap-2 px-4 py-2 bg-[#16213e] border border-[#00f0ff]/50 rounded-lg"> <div className="w-full px-6 py-3 bg-gradient-to-r from-[#16213e] to-[#1a1a2e] border-l-4 border-[#00f0ff] rounded-r-lg">
<span className="text-sm font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}> <h2 className="text-xs font-bold text-[#00f0ff] uppercase tracking-widest text-left" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
Generate New Seed Generate New Seed
</span> </h2>
<p className="text-xs text-[#6ef3f7] mt-1 text-left">Create a fresh BIP39 mnemonic for a new wallet</p>
</div> </div>
<p className="text-sm text-[#6ef3f7]">
Create a fresh BIP39 mnemonic for a new wallet
</p>
</div> </div>
{/* Word count selector */} {/* Word count selector */}
<div className="space-y-3"> <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)' }}> <label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest block text-center" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
Seed Length Seed Length
</label> </label>
<div className="flex gap-4 justify-center"> <div className="grid grid-cols-2 gap-3 max-w-sm mx-auto">
<button <button
onClick={() => setSeedWordCount(12)} onClick={() => setSeedWordCount(12)}
className={`px-8 py-3 rounded-lg font-medium transition-all ${seedWordCount === 12 className={`py-2.5 text-sm 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-[#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' : 'bg-[#16213e] text-[#9d84b7] border-2 border-[#00f0ff]/30 hover:text-[#6ef3f7] hover:border-[#00f0ff]/50'
}`} }`}
@@ -649,7 +648,7 @@ function App() {
</button> </button>
<button <button
onClick={() => setSeedWordCount(24)} onClick={() => setSeedWordCount(24)}
className={`px-8 py-3 rounded-lg font-medium transition-all ${seedWordCount === 24 className={`py-2.5 text-sm 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-[#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' : 'bg-[#16213e] text-[#9d84b7] border-2 border-[#00f0ff]/30 hover:text-[#6ef3f7] hover:border-[#00f0ff]/50'
}`} }`}
@@ -662,11 +661,11 @@ function App() {
{/* Destination selector */} {/* Destination selector */}
<div className="space-y-3"> <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)' }}> <label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest block text-center" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
Send Generated Seed To Send Generated Seed To
</label> </label>
<div className="flex gap-4 justify-center"> <div className="grid grid-cols-2 gap-3 max-w-sm mx-auto">
<label className={`flex-1 p-4 rounded-lg border-2 cursor-pointer transition-all ${seedDestination === 'backup' <label className={`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-[#1a1a2e] border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
: 'bg-[#16213e] border-[#00f0ff]/30 hover:border-[#00f0ff]/50' : 'bg-[#16213e] border-[#00f0ff]/30 hover:border-[#00f0ff]/50'
}`}> }`}>
@@ -679,17 +678,17 @@ function App() {
className="hidden" className="hidden"
/> />
<div className="text-center space-y-1"> <div className="text-center space-y-1">
<div className={`text-lg font-bold ${seedDestination === 'backup' ? 'text-[#00f0ff]' : 'text-[#9d84b7]'}`} <div className={`text-sm font-bold ${seedDestination === 'backup' ? 'text-[#00f0ff]' : 'text-[#9d84b7]'}`}
style={seedDestination === 'backup' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}> style={seedDestination === 'backup' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}>
📦 Backup 📦 Backup
</div> </div>
<p className="text-xs text-[#6ef3f7]"> <p className="text-[10px] text-[#6ef3f7]">
Encrypt immediately Encrypt immediately
</p> </p>
</div> </div>
</label> </label>
<label className={`flex-1 p-4 rounded-lg border-2 cursor-pointer transition-all ${seedDestination === 'seedblender' <label className={`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-[#1a1a2e] border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
: 'bg-[#16213e] border-[#00f0ff]/30 hover:border-[#00f0ff]/50' : 'bg-[#16213e] border-[#00f0ff]/30 hover:border-[#00f0ff]/50'
}`}> }`}>
@@ -702,11 +701,11 @@ function App() {
className="hidden" className="hidden"
/> />
<div className="text-center space-y-1"> <div className="text-center space-y-1">
<div className={`text-lg font-bold ${seedDestination === 'seedblender' ? 'text-[#00f0ff]' : 'text-[#9d84b7]'}`} <div className={`text-sm font-bold ${seedDestination === 'seedblender' ? 'text-[#00f0ff]' : 'text-[#9d84b7]'}`}
style={seedDestination === 'seedblender' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}> style={seedDestination === 'seedblender' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}>
🎲 Seed Blender 🎲 Seed Blender
</div> </div>
<p className="text-xs text-[#6ef3f7]"> <p className="text-[10px] text-[#6ef3f7]">
Use for XOR blending Use for XOR blending
</p> </p>
</div> </div>
@@ -718,7 +717,7 @@ function App() {
<button <button
onClick={generateNewSeed} onClick={generateNewSeed}
disabled={loading || isReadOnly} 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]" className="w-full py-3 bg-gradient-to-r from-[#ff006e] to-[#ff4d8f] text-white text-sm 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)' }} style={{ textShadow: '0 0 10px rgba(255,255,255,0.8)' }}
> >
{loading ? ( {loading ? (
@@ -738,12 +737,12 @@ function App() {
{generatedSeed && ( {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="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"> <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)' }}> <span className="font-bold text-sm text-[#39ff14] flex items-center gap-2" style={{ textShadow: '0 0 10px rgba(57,255,20,0.8)' }}>
<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="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)' }}> <p className="font-mono text-xs text-[#39ff14] break-words leading-relaxed" style={{ textShadow: '0 0 5px rgba(57,255,20,0.5)' }}>
{generatedSeed} {generatedSeed}
</p> </p>
</div> </div>
@@ -756,9 +755,9 @@ function App() {
</div> </div>
<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-sm 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 <textarea
className={`w-full h-32 p-4 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 ${isReadOnly ? 'blur-sm select-none' : '' 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' : ''
}`} }`}
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)',
@@ -786,23 +785,23 @@ function App() {
<div className="space-y-4"> <div className="space-y-4">
{/* File Upload Zone */} {/* File Upload Zone */}
<div <div
className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${isDragging ? 'border-[#ff006e] bg-[#16213e] shadow-[0_0_30px_rgba(255,0,110,0.5)]' : 'border-[#00f0ff]/50 bg-[#16213e]' className={`border-2 border-dashed rounded-lg p-6 text-center transition-colors ${isDragging
? 'border-[#ff006e] bg-[#1a1a2e] shadow-[0_0_30px_rgba(255,0,110,0.5)]'
: 'border-[#00f0ff50] bg-[#16213e] hover:border-[#00f0ff] hover:bg-[#1a1a2e]'
}`} }`}
onDrop={handleDrop} onDrop={handleDrop}
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
> >
<div className="space-y-3"> <div className="space-y-2">
<FileKey size={48} className="mx-auto text-[#00f0ff]" /> <div className="text-4xl">📎</div>
<p className="text-sm text-[#00f0ff] font-medium" style={{ textShadow: '0 0 10px rgba(0,240,255,0.5)' }}> <p className="text-xs text-[#00f0ff] font-medium" style={{ textShadow: '0 0 10px rgba(0,240,255,0.5)' }}>
Drag & drop QR image or text file Drag & drop file or click Browse
</p> </p>
<p className="text-xs text-[#6ef3f7]"> <p className="text-[10px] text-[#6ef3f7]">QR image, PGP armor, hex, numeric</p>
Supports: PNG/JPG (QR), TXT/ASC (PGP armor, hex, numeric) <div className="flex gap-2 justify-center pt-1">
</p> <label className="px-3 py-1.5 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-xs font-medium text-[#00f0ff] hover:bg-[#1a1a2e] hover:shadow-[0_0_15px_rgba(0,240,255,0.3)] cursor-pointer transition-all flex items-center gap-2">
<div className="flex gap-3 justify-center pt-2"> 📁 Browse
<label className="px-4 py-2 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-sm font-medium text-[#00f0ff] hover:bg-[#1a1a2e] hover:shadow-[0_0_15px_rgba(0,240,255,0.3)] cursor-pointer transition-all">
📁 Browse Files
<input <input
type="file" type="file"
accept="image/*,.txt,.asc" accept="image/*,.txt,.asc"
@@ -812,7 +811,7 @@ function App() {
</label> </label>
<button <button
onClick={() => setShowQRScanner(true)} onClick={() => setShowQRScanner(true)}
className="px-4 py-2 bg-transparent text-[#00f0ff] rounded-lg font-medium border-2 border-[#00f0ff] hover:bg-[#00f0ff]/10 hover:shadow-[0_0_20px_rgba(0,240,255,0.5)] transition-all flex items-center gap-2" className="px-3 py-1.5 bg-transparent text-[#00f0ff] rounded-lg font-medium border-2 border-[#00f0ff] hover:bg-[#00f0ff]/10 hover:shadow-[0_0_20px_rgba(0,240,255,0.5)] transition-all flex items-center gap-2 text-xs"
> >
📷 Scan QR 📷 Scan QR
</button> </div> </button> </div>
@@ -853,13 +852,13 @@ function App() {
{privateKeyInput && ( {privateKeyInput && (
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Private Key Passphrase</label> <label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Private Key Passphrase</label>
<div className="relative"> <div className="relative">
<Lock className="absolute left-3 top-3 text-[#6ef3f7]" size={16} /> <Lock className="absolute left-3 top-3 text-[#6ef3f7]" size={16} />
<input <input
type="password" type="password"
data-sensitive="Message Password" data-sensitive="Message Password"
className={`w-full pl-10 pr-4 py-2.5 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-[#00f0ff] text-sm placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all ${isReadOnly ? 'blur-sm select-none' : '' className={`w-full pl-10 pr-4 py-2 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-[#00f0ff] text-xs placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all ${isReadOnly ? 'blur-sm select-none' : ''
}`} }`}
style={{ textShadow: '0 0 5px rgba(0,240,255,0.5)' }} style={{ textShadow: '0 0 5px rgba(0,240,255,0.5)' }}
placeholder="Unlock private key..." placeholder="Unlock private key..."
@@ -884,9 +883,8 @@ function App() {
</div> </div>
{/* Security Panel */} {/* Security Panel */}
{activeTab !== 'seedblender' && activeTab !== 'create' && ( {activeTab !== 'seedblender' && activeTab !== 'create' && (<div className="space-y-2"> {/* Added space-y-2 wrapper */}
<div className="space-y-2"> {/* Added space-y-2 wrapper */} <label className="text-xs font-semibold text-[#00f0ff] flex items-center gap-2" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
<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 <Lock size={14} /> SECURITY OPTIONS
</label> </label>
@@ -895,12 +893,12 @@ function App() {
{/* Encryption Mode Toggle */} {/* Encryption Mode Toggle */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Encryption Mode</label> <label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Encryption Mode</label>
<select <select
value={encryptionMode} value={encryptionMode}
onChange={(e) => setEncryptionMode(e.target.value as 'pgp' | 'krux' | 'seedqr')} onChange={(e) => setEncryptionMode(e.target.value as 'pgp' | 'krux' | 'seedqr')}
disabled={isReadOnly} disabled={isReadOnly}
className="w-full px-3 py-2.5 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-[#00f0ff] text-sm focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all appearance-none bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iOCIgPHBhdGggZD0iTTEgMUw2IDZMMTEgMSIgc3Ryb2tlPSIjMDBmMGZmIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvc3ZnPg==')] bg-[length:12px] bg-[position:right_12px_center] bg-no-repeat pr-10" className="w-full px-3 py-2 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-[#00f0ff] text-xs focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all appearance-none bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iOCIgPHBhdGggZD0iTTEgMUw2IDZMMTEgMSIgc3Ryb2tlPSIjMDBmMGZmIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvc3ZnPg==')] bg-[length:12px] bg-[position:right_12px_center] bg-no-repeat pr-10"
style={{ textShadow: '0 0 5px rgba(0,240,255,0.5)' }} style={{ textShadow: '0 0 5px rgba(0,240,255,0.5)' }}
> >
<option value="pgp">PGP (Asymmetric)</option> <option value="pgp">PGP (Asymmetric)</option>
@@ -917,12 +915,12 @@ function App() {
{/* SeedQR Format Toggle */} {/* SeedQR Format Toggle */}
{encryptionMode === 'seedqr' && ( {encryptionMode === 'seedqr' && (
<div className="space-y-2 pt-2"> <div className="space-y-2 pt-2">
<label className="text-xs font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>SeedQR Format</label> <label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>SeedQR Format</label>
<select <select
value={seedQrFormat} value={seedQrFormat}
onChange={(e) => setSeedQrFormat(e.target.value as 'standard' | 'compact')} onChange={(e) => setSeedQrFormat(e.target.value as 'standard' | 'compact')}
disabled={isReadOnly} disabled={isReadOnly}
className="w-full px-3 py-2.5 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-[#00f0ff] text-sm focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all appearance-none bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iOCIgPHBhdGggZD0iTTEgMUw2IDZMMTEgMSIgc3Ryb2tlPSIjMDBmMGZmIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvc3ZnPg==')] bg-[length:12px] bg-[position:right_12px_center] bg-no-repeat pr-10" className="w-full px-3 py-2 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-[#00f0ff] text-xs focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all appearance-none bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iOCIgPHBhdGggZD0iTTEgMUw2IDZMMTEgMSIgc3Ryb2tlPSIjMDBmMGZmIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvc3ZnPg==')] bg-[length:12px] bg-[position:right_12px_center] bg-no-repeat pr-10"
style={{ textShadow: '0 0 5px rgba(0,240,255,0.5)' }} style={{ textShadow: '0 0 5px rgba(0,240,255,0.5)' }}
> >
<option value="standard">Standard (Numeric)</option> <option value="standard">Standard (Numeric)</option>
@@ -946,12 +944,12 @@ function App() {
)} )}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>MESSAGE PASSWORD</label> <label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>MESSAGE PASSWORD</label>
<div className="relative"> <div className="relative">
<Lock className="absolute left-3 top-3 text-[#6ef3f7]" size={16} /> <Lock className="absolute left-3 top-3 text-[#6ef3f7]" size={16} />
<input <input
type="password" type="password"
className={`w-full pl-10 pr-4 py-2.5 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-[#00f0ff] text-sm placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all ${isReadOnly ? 'blur-sm select-none' : '' className={`w-full pl-10 pr-4 py-2 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-[#00f0ff] text-xs placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all ${isReadOnly ? 'blur-sm select-none' : ''
}`} }`}
style={{ textShadow: '0 0 5px rgba(0,240,255,0.5)' }} style={{ textShadow: '0 0 5px rgba(0,240,255,0.5)' }}
placeholder={encryptionMode === 'krux' ? "Required for Krux encryption" : "Optional password..."} placeholder={encryptionMode === 'krux' ? "Required for Krux encryption" : "Optional password..."}
@@ -1001,7 +999,7 @@ function App() {
<button <button
onClick={handleBackup} onClick={handleBackup}
disabled={!mnemonic || loading || isReadOnly} disabled={!mnemonic || 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]" className="w-full py-3 bg-gradient-to-r from-[#ff006e] to-[#ff4d8f] text-white text-sm 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)' }} style={{ textShadow: '0 0 10px rgba(255,255,255,0.8)' }}
> >
{loading ? ( {loading ? (
@@ -1015,7 +1013,7 @@ function App() {
<button <button
onClick={handleRestore} onClick={handleRestore}
disabled={!restoreInput || loading || isReadOnly} disabled={!restoreInput || 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]" className="w-full py-3 bg-gradient-to-r from-[#ff006e] to-[#ff4d8f] text-white text-sm 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)' }} style={{ textShadow: '0 0 10px rgba(255,255,255,0.8)' }}
> >
{loading ? ( {loading ? (
@@ -1037,14 +1035,14 @@ function App() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<label className="text-xs font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}> <label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
Raw payload (copy for backup) Raw payload (copy for backup)
</label> </label>
<button <button
type="button" type="button"
onClick={() => copyToClipboard(qrPayload)} onClick={() => copyToClipboard(qrPayload)}
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg bg-[#16213e] border-2 border-[#00f0ff]/50 text-[#00f0ff] text-xs font-semibold hover:bg-[#1a1a2e] hover:shadow-[0_0_15px_rgba(0,240,255,0.3)] transition-all" className="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg bg-[#16213e] border-2 border-[#00f0ff]/50 text-[#00f0ff] text-xs font-medium hover:bg-[#1a1a2e] hover:shadow-[0_0_15px_rgba(0,240,255,0.3)] transition-all"
> >
{copied ? <CheckCircle2 size={14} /> : <QrCode size={14} />} {copied ? <CheckCircle2 size={14} /> : <QrCode size={14} />}
{copied ? "Copied" : "Copy"} {copied ? "Copied" : "Copy"}
@@ -1072,7 +1070,7 @@ function App() {
{decryptedRestoredMnemonic && activeTab === 'restore' && ( {decryptedRestoredMnemonic && activeTab === 'restore' && (
<div className="pt-6 border-t border-[#00f0ff]/20 animate-in zoom-in-95"> <div className="pt-6 border-t border-[#00f0ff]/20 animate-in zoom-in-95">
<div className="p-6 bg-[#0a0a0f] border-2 border-[#39ff14] rounded-lg shadow-[0_0_30px_rgba(57,255,20,0.4)] relative overflow-hidden"> <div className="p-6 bg-[#0a0a0f] border-2 border-[#39ff14] rounded-lg shadow-[0_0_30px_rgba(57,255,20,0.4)] relative overflow-hidden">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-3">
<span className="font-bold text-[#39ff14] flex items-center gap-2 text-lg" style={{ textShadow: '0 0 10px rgba(57,255,20,0.8)' }}> <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} /> Mnemonic Recovered <CheckCircle2 size={22} /> Mnemonic Recovered
</span> </span>
@@ -1084,8 +1082,8 @@ function App() {
</button> </button>
</div> </div>
<div className="p-6 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-lg break-words text-[#39ff14] selection:bg-[#39ff14] selection:text-[#0a0a0f] ${isReadOnly ? 'blur-md select-none' : '' <p className={`font-mono text-center text-base break-words text-[#39ff14] selection:bg-[#39ff14] selection:text-[#0a0a0f] ${isReadOnly ? 'blur-md select-none' : ''
}`} }`}
style={{ textShadow: '0 0 8px rgba(57,255,20,0.8)' }}> style={{ textShadow: '0 0 8px rgba(57,255,20,0.8)' }}>
{decryptedRestoredMnemonic} {decryptedRestoredMnemonic}
@@ -1096,7 +1094,7 @@ function App() {
)} )}
</div> </div>
</div> {/* Close new cyberpunk main container */} </div> {/* Close new cyberpunk main container */}
</main> </main >
<Footer <Footer
appVersion={__APP_VERSION__} appVersion={__APP_VERSION__}
buildHash={__BUILD_HASH__} buildHash={__BUILD_HASH__}
@@ -1209,6 +1207,7 @@ function App() {
</div> </div>
)} )}
</div> {/* Close relative z-10 content wrapper */} </div> {/* Close relative z-10 content wrapper */}
</div>
</div> /* Close root min-h-screen */ </div> /* Close root min-h-screen */
); );
} }

View File

@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Shield, Lock, RefreshCw } from 'lucide-react'; import { Shield, RefreshCw, Lock, Unlock } 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';
@@ -52,24 +52,28 @@ const Header: React.FC<HeaderProps> = ({
onResetAll onResetAll
}) => { }) => {
return ( return (
<header className="sticky top-0 z-50 bg-[#0a0a0f] border-b border-[#00f0ff]/30 backdrop-blur-sm"> <header className="sticky top-0 z-50 bg-[#0a0a0f] border-b border-[#00f0ff30] backdrop-blur-sm">
<div className="max-w-7xl mx-auto px-6 py-4"> <div className="w-full px-4 py-3 space-y-3">
{/* ROW 1: Logo + App Info */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{/* Left: Logo & Title */}
<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)]">
<Shield className="w-6 h-6 text-[#0a0a0f]" /> <Shield className="w-6 h-6 text-[#0a0a0f]" />
</div> </div>
<div> <div>
<h1 className="text-lg font-semibold text-[#00f0ff]" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}> <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> SeedPGP <span className="text-[#ff006e]">{appVersion}</span>
</h1> </h1>
<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>
</div>
{/* Center: Monitoring Badges */} {/* ROW 2: Monitoring Badges + Action Buttons */}
<div className="hidden md:flex items-center gap-3"> <div className="flex items-center justify-between gap-2 pb-2 border-b border-[#00f0ff20]">
{/* Left: Badges */}
<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">
<StorageBadge localItems={localItems} sessionItems={sessionItems} /> <StorageBadge localItems={localItems} sessionItems={sessionItems} />
@@ -81,66 +85,85 @@ const Header: React.FC<HeaderProps> = ({
</div> </div>
{/* Right: Action Buttons */} {/* Right: Action Buttons */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-2">
{encryptedMnemonicCache && ( {encryptedMnemonicCache && (
<button <button
onClick={handleLockAndClear} onClick={onToggleLock}
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)]" 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"}
> >
<Lock size={16} /> {isLocked ? '🔓' : '🙈'}
<span>Lock/Clear</span>
</button> </button>
)} )}
{/* Reset All button - left side */} <button
onClick={async () => {
try {
await navigator.clipboard.writeText('');
} catch { }
localStorage.clear();
sessionStorage.clear();
}}
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"
>
📋
</button>
<button <button
onClick={onResetAll} 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" 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={16} /> <RefreshCw size={12} />
Reset All Reset
</button> </button>
{encryptedMnemonicCache && ( </div>
<div className="h-8 w-px bg-[#00f0ff]/30 mx-2"></div> </div>
)}
{/* ROW 3: Navigation Tabs */}
<div className="grid grid-cols-4 gap-2">
<button <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'}`} className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${activeTab === 'create'
? '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')}
> >
Create Create
</button> </button>
<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'}`} className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${activeTab === 'backup'
? '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')}
> >
Backup Backup
</button> <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'}`}
<button
className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${activeTab === 'restore'
? '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')}
> >
Restore Restore
</button> </button>
<button <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'}`} className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${activeTab === 'seedblender'
? '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')}
> >
Seed Blender Blender
</button> </div> </button>
</div>
{/* Mobile: Stack monitoring badges */}
<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} />
</div>
<div onClick={onOpenClipboardModal} className="cursor-pointer">
<ClipboardBadge events={events} onOpenClipboardModal={onOpenClipboardModal} />
</div>
<EditLockBadge isLocked={isLocked} onToggle={onToggleLock} />
</div> </div>
</div> </div>
</header> </header>

View File

@@ -52,7 +52,7 @@ export const PgpKeyInput: React.FC<PgpKeyInputProps> = ({
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-semibold text-[#00f0ff] flex items-center justify-between" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}> <label className="text-[12px] font-bold text-[#00f0ff] uppercase tracking-widest flex items-center justify-between" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
{Icon && <Icon size={14} />} {label} {Icon && <Icon size={14} />} {label}
</span> </span>
@@ -69,8 +69,7 @@ export const PgpKeyInput: React.FC<PgpKeyInputProps> = ({
onDrop={handleDrop} onDrop={handleDrop}
> >
<textarea <textarea
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'} ${ 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' : ''
readOnly ? 'blur-sm select-none' : ''
}`} }`}
placeholder={placeholder} placeholder={placeholder}
value={value} value={value}

View File

@@ -319,17 +319,32 @@ export function SeedBlender({ onDirtyStateChange, setMnemonicForBackup, requestT
{entry.error && <p className="text-xs text-[#ff006e]">{entry.error}</p>} {entry.error && <p className="text-xs text-[#ff006e]">{entry.error}</p>}
</div> </div>
) : ( ) : (
<div className="space-y-1"> <div className="space-y-2">
<div className="flex flex-col sm:flex-row items-start gap-2"> {/* Row 1: Textarea only */}
<div className="relative w-full"> <textarea
<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)}`} /> value={entry.rawInput}
{entry.isValid === true && <CheckCircle2 className="absolute top-3 right-3 text-[#39ff14]" />} onChange={(e) => updateEntry(index, { rawInput: e.target.value, decryptedMnemonic: e.target.value, isValid: null, error: null })}
{entry.isValid === false && <AlertTriangle className="absolute top-3 right-3 text-[#ff006e]" />} placeholder={`Mnemonic #${index + 1} (12 or 24 words)`}
</div> 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)}`}
<div className="flex items-center gap-2 shrink-0"> />
<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> {/* Row 2: QR button (left) and X button (right) */}
<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 className="flex items-center justify-between">
</div> <button
onClick={() => handleScan(index)}
className="flex items-center gap-1.5 px-3 py-1.5 bg-[#16213e] border border-[#00f0ff] text-[#00f0ff] text-xs rounded-lg hover:bg-[#00f0ff20] transition-all"
title="Scan QR code"
>
<QrCode size={14} />
<span>Scan QR</span>
</button>
<button
onClick={() => handleRemoveEntry(entry.id)}
className="p-1.5 bg-[#16213e] border border-[#ff006e] text-[#ff006e] rounded-lg hover:bg-[#ff006e20] transition-all"
title="Remove mnemonic"
>
<X size={14} />
</button>
</div> </div>
{entry.error && <p className="text-xs text-[#ff006e] px-1">{entry.error}</p>} {entry.error && <p className="text-xs text-[#ff006e] px-1">{entry.error}</p>}
</div> </div>
@@ -348,7 +363,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="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]" /> <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]" />
{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>)}

View File

@@ -2,6 +2,24 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* Mobile-first: constrain to phone width on all devices */
#root {
max-width: 448px;
/* max-w-md = 28rem = 448px */
margin: 0 auto;
background: black;
}
body {
background: black;
overflow-x: hidden;
}
/* Ensure all content respects mobile width */
* {
max-width: 100%;
}
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',

View File

@@ -26,7 +26,6 @@ import wordlistTxt from '../bip39_wordlist.txt?raw';
// --- Isomorphic Crypto Setup --- // --- Isomorphic Crypto Setup ---
let cryptoPromise: Promise<SubtleCrypto>;
/** /**
* Asynchronously gets the appropriate SubtleCrypto interface, using a singleton * Asynchronously gets the appropriate SubtleCrypto interface, using a singleton
* pattern to ensure the module is loaded only once. * pattern to ensure the module is loaded only once.

View File

@@ -1,5 +1,6 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import basicSsl from '@vitejs/plugin-basic-ssl'
import wasm from 'vite-plugin-wasm' import wasm from 'vite-plugin-wasm'
import topLevelAwait from 'vite-plugin-top-level-await' import topLevelAwait from 'vite-plugin-top-level-await'
import { execSync } from 'child_process' import { execSync } from 'child_process'
@@ -16,6 +17,7 @@ export default defineConfig({
plugins: [ plugins: [
wasm(), wasm(),
topLevelAwait(), topLevelAwait(),
basicSsl(),
react(), react(),
{ {
name: 'html-transform', name: 'html-transform',
@@ -24,6 +26,12 @@ export default defineConfig({
} }
} }
], ],
server: {
host: '0.0.0.0',
port: 5173,
strictPort: true,
https: true,
},
resolve: { resolve: {
alias: { alias: {
buffer: 'buffer', buffer: 'buffer',