mirror of
https://github.com/kccleoc/seedpgp-web.git
synced 2026-03-06 17:37:51 +08:00
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:
3
bun.lock
3
bun.lock
@@ -27,6 +27,7 @@
|
||||
"@types/qrcode-generator": "^1.0.6",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-basic-ssl": "^1.1.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@vitejs/plugin-basic-ssl": "^1.1.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.17",
|
||||
|
||||
931
src/App.tsx
931
src/App.tsx
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
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 StorageBadge from './badges/StorageBadge';
|
||||
import ClipboardBadge from './badges/ClipboardBadge';
|
||||
@@ -52,24 +52,28 @@ const Header: React.FC<HeaderProps> = ({
|
||||
onResetAll
|
||||
}) => {
|
||||
return (
|
||||
<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">
|
||||
<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">
|
||||
|
||||
{/* ROW 1: Logo + App Info */}
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Left: Logo & Title */}
|
||||
<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)]">
|
||||
<Shield className="w-6 h-6 text-[#0a0a0f]" />
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<p className="text-xs text-[#6ef3f7]">OpenPGP-secured BIP39 backup</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center: Monitoring Badges */}
|
||||
<div className="hidden md:flex items-center gap-3">
|
||||
{/* ROW 2: Monitoring Badges + Action Buttons */}
|
||||
<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} />
|
||||
<div onClick={onOpenStorageModal} className="cursor-pointer">
|
||||
<StorageBadge localItems={localItems} sessionItems={sessionItems} />
|
||||
@@ -81,70 +85,89 @@ const Header: React.FC<HeaderProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Right: Action Buttons */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{encryptedMnemonicCache && (
|
||||
<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 transition-all hover:shadow-[0_0_15px_rgba(255,0,110,0.5)]"
|
||||
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"}
|
||||
>
|
||||
<Lock size={16} />
|
||||
<span>Lock/Clear</span>
|
||||
{isLocked ? '🔓' : '🙈'}
|
||||
</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
|
||||
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} />
|
||||
Reset All
|
||||
<RefreshCw size={12} />
|
||||
Reset
|
||||
</button>
|
||||
{encryptedMnemonicCache && (
|
||||
<div className="h-8 w-px bg-[#00f0ff]/30 mx-2"></div>
|
||||
)}
|
||||
<button
|
||||
className={`px-4 py-2 rounded-lg font-medium ${activeTab === 'create' ? 'bg-[#1a1a2e] text-[#00f0ff] border-b-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)] relative' : 'bg-[#0a0a0f] text-[#9d84b7] hover:text-[#6ef3f7] hover:bg-[#16213e] transition-all'}`}
|
||||
style={activeTab === 'create' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
|
||||
onClick={() => onRequestTabChange('create')}
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
<button
|
||||
className={`px-4 py-2 rounded-lg font-medium ${activeTab === 'backup' ? 'bg-[#1a1a2e] text-[#00f0ff] border-b-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)] relative' : 'bg-[#0a0a0f] text-[#9d84b7] hover:text-[#6ef3f7] hover:bg-[#16213e] transition-all'}`}
|
||||
style={activeTab === 'backup' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
|
||||
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 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>
|
||||
</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} />
|
||||
{/* ROW 3: Navigation Tabs */}
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
<button
|
||||
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}
|
||||
onClick={() => onRequestTabChange('create')}
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
|
||||
<button
|
||||
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}
|
||||
onClick={() => onRequestTabChange('backup')}
|
||||
>
|
||||
Backup
|
||||
</button>
|
||||
|
||||
<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}
|
||||
onClick={() => onRequestTabChange('restore')}
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
|
||||
<button
|
||||
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}
|
||||
onClick={() => onRequestTabChange('seedblender')}
|
||||
>
|
||||
Blender
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
export default Header;
|
||||
|
||||
@@ -52,7 +52,7 @@ export const PgpKeyInput: React.FC<PgpKeyInputProps> = ({
|
||||
|
||||
return (
|
||||
<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">
|
||||
{Icon && <Icon size={14} />} {label}
|
||||
</span>
|
||||
@@ -69,9 +69,8 @@ export const PgpKeyInput: React.FC<PgpKeyInputProps> = ({
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<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'} ${
|
||||
readOnly ? 'blur-sm select-none' : ''
|
||||
}`}
|
||||
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}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
|
||||
@@ -319,17 +319,32 @@ export function SeedBlender({ onDirtyStateChange, setMnemonicForBackup, requestT
|
||||
{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-[#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-[#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 className="space-y-2">
|
||||
{/* Row 1: Textarea only */}
|
||||
<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-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)}`}
|
||||
/>
|
||||
{/* Row 2: QR button (left) and X button (right) */}
|
||||
<div className="flex items-center justify-between">
|
||||
<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>
|
||||
{entry.error && <p className="text-xs text-[#ff006e] px-1">{entry.error}</p>}
|
||||
</div>
|
||||
@@ -348,7 +363,7 @@ export function SeedBlender({ onDirtyStateChange, setMnemonicForBackup, requestT
|
||||
<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-[#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>)}
|
||||
{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>)}
|
||||
|
||||
@@ -2,6 +2,24 @@
|
||||
@tailwind components;
|
||||
@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 {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
|
||||
@@ -26,7 +26,6 @@ import wordlistTxt from '../bip39_wordlist.txt?raw';
|
||||
|
||||
// --- Isomorphic Crypto Setup ---
|
||||
|
||||
let cryptoPromise: Promise<SubtleCrypto>;
|
||||
/**
|
||||
* Asynchronously gets the appropriate SubtleCrypto interface, using a singleton
|
||||
* pattern to ensure the module is loaded only once.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import basicSsl from '@vitejs/plugin-basic-ssl'
|
||||
import wasm from 'vite-plugin-wasm'
|
||||
import topLevelAwait from 'vite-plugin-top-level-await'
|
||||
import { execSync } from 'child_process'
|
||||
@@ -16,6 +17,7 @@ export default defineConfig({
|
||||
plugins: [
|
||||
wasm(),
|
||||
topLevelAwait(),
|
||||
basicSsl(),
|
||||
react(),
|
||||
{
|
||||
name: 'html-transform',
|
||||
@@ -24,6 +26,12 @@ export default defineConfig({
|
||||
}
|
||||
}
|
||||
],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
https: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
buffer: 'buffer',
|
||||
|
||||
Reference in New Issue
Block a user