feat: Implement 'Lock/Edit' mode with blur and confirmation dialog

This commit is contained in:
LC mac
2026-01-31 01:25:27 +08:00
parent 7564ddc7c9
commit 2a7ac1cce0
10 changed files with 158 additions and 52 deletions

View File

@@ -17,11 +17,11 @@ import { validateBip39Mnemonic } from './lib/bip39';
import { buildPlaintext, encryptToSeedPgp, decryptSeedPgp } from './lib/seedpgp'; import { buildPlaintext, encryptToSeedPgp, decryptSeedPgp } from './lib/seedpgp';
import * as openpgp from 'openpgp'; import * as openpgp from 'openpgp';
import { SecurityWarnings } from './components/SecurityWarnings'; import { SecurityWarnings } from './components/SecurityWarnings';
import { ReadOnly } from './components/ReadOnly';
import { getSessionKey, encryptJsonToBlob, destroySessionKey, EncryptedBlob } from './lib/sessionCrypto'; import { getSessionKey, encryptJsonToBlob, destroySessionKey, EncryptedBlob } from './lib/sessionCrypto';
import Header from './components/Header'; import Header from './components/Header';
import { StorageDetails } from './components/StorageDetails'; import { StorageDetails } from './components/StorageDetails';
import { ClipboardDetails } from './components/ClipboardDetails'; import { ClipboardDetails } from './components/ClipboardDetails';
import Footer from './components/Footer';
console.log("OpenPGP.js version:", openpgp.config.versionString); console.log("OpenPGP.js version:", openpgp.config.versionString);
@@ -64,6 +64,7 @@ function App() {
const [localItems, setLocalItems] = useState<StorageItem[]>([]); const [localItems, setLocalItems] = useState<StorageItem[]>([]);
const [sessionItems, setSessionItems] = useState<StorageItem[]>([]); const [sessionItems, setSessionItems] = useState<StorageItem[]>([]);
const [clipboardEvents, setClipboardEvents] = useState<ClipboardEvent[]>([]); const [clipboardEvents, setClipboardEvents] = useState<ClipboardEvent[]>([]);
const [showLockConfirm, setShowLockConfirm] = useState(false);
const SENSITIVE_PATTERNS = ['key', 'mnemonic', 'seed', 'private', 'secret', 'pgp', 'password']; const SENSITIVE_PATTERNS = ['key', 'mnemonic', 'seed', 'private', 'secret', 'pgp', 'password'];
@@ -100,21 +101,7 @@ function App() {
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
useEffect(() => {
// When entering read-only mode, clear sensitive data for security.
if (isReadOnly) {
setMnemonic('');
setBackupMessagePassword('');
setRestoreMessagePassword('');
setPublicKeyInput('');
setPrivateKeyInput('');
setPrivateKeyPassphrase('');
setQrPayload('');
setRestoreInput('');
setDecryptedRestoredMnemonic(null);
setError('');
}
}, [isReadOnly]);
// Cleanup session key on component unmount // Cleanup session key on component unmount
useEffect(() => { useEffect(() => {
@@ -319,6 +306,21 @@ function App() {
setShowQRScanner(false); setShowQRScanner(false);
}; };
const handleToggleLock = () => {
if (!isReadOnly) {
// About to lock - show confirmation
setShowLockConfirm(true);
} else {
// Unlocking - no confirmation needed
setIsReadOnly(false);
}
};
const confirmLock = () => {
setIsReadOnly(true);
setShowLockConfirm(false);
};
return ( return (
<div className="min-h-screen bg-slate-800 text-slate-100"> <div className="min-h-screen bg-slate-800 text-slate-100">
@@ -334,6 +336,8 @@ function App() {
encryptedMnemonicCache={encryptedMnemonicCache} encryptedMnemonicCache={encryptedMnemonicCache}
handleLockAndClear={handleLockAndClear} handleLockAndClear={handleLockAndClear}
appVersion={__APP_VERSION__} appVersion={__APP_VERSION__}
isLocked={isReadOnly}
onToggleLock={handleToggleLock}
/> />
<main className="max-w-7xl mx-auto px-6 py-4"> <main className="max-w-7xl mx-auto px-6 py-4">
<div className="p-6 md:p-8 space-y-6"> <div className="p-6 md:p-8 space-y-6">
@@ -350,10 +354,10 @@ function App() {
{/* Info Banner */} {/* Info Banner */}
{recipientFpr && activeTab === 'backup' && ( {recipientFpr && activeTab === 'backup' && (
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg flex items-start gap-3 text-blue-800 text-xs animate-in fade-in"> <div className="p-3 bg-teal-50 border border-teal-200 rounded-lg flex items-start gap-3 text-teal-800 text-xs animate-in fade-in">
<Info size={16} className="shrink-0 mt-0.5" /> <Info size={16} className="shrink-0 mt-0.5" />
<div> <div>
<strong>Recipient Key:</strong> <code className="bg-blue-100 px-1.5 py-0.5 rounded font-mono">{recipientFpr}</code> <strong>Recipient Key:</strong> <code className="bg-teal-100 px-1.5 py-0.5 rounded font-mono">{recipientFpr}</code>
</div> </div>
</div> </div>
)} )}
@@ -364,9 +368,11 @@ function App() {
{activeTab === 'backup' ? ( {activeTab === 'backup' ? (
<> <>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-semibold text-slate-700">BIP39 Mnemonic</label> <label className="text-sm font-semibold text-slate-200">BIP39 Mnemonic</label>
<textarea <textarea
className="w-full h-32 p-4 bg-slate-50 border border-slate-200 rounded-xl text-sm font-mono text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all resize-none" className={`w-full h-32 p-4 bg-slate-50 border border-slate-200 rounded-xl text-sm font-mono text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-teal-500 transition-all resize-none ${
isReadOnly ? 'blur-sm select-none' : ''
}`}
data-sensitive="BIP39 Mnemonic" data-sensitive="BIP39 Mnemonic"
placeholder="Enter your 12 or 24 word seed phrase..." placeholder="Enter your 12 or 24 word seed phrase..."
@@ -399,9 +405,11 @@ function App() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-semibold text-slate-700">SEEDPGP1 Payload</label> <label className="text-sm font-semibold text-slate-200">SEEDPGP1 Payload</label>
<textarea <textarea
className="w-full h-32 p-4 bg-slate-50 border border-slate-200 rounded-xl text-xs font-mono text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all resize-none" className={`w-full h-32 p-4 bg-slate-50 border border-slate-200 rounded-xl text-xs font-mono text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-teal-500 transition-all resize-none ${
isReadOnly ? 'blur-sm select-none' : ''
}`}
placeholder="SEEDPGP1:0:ABCD:..." placeholder="SEEDPGP1:0:ABCD:..."
value={restoreInput} value={restoreInput}
@@ -428,7 +436,9 @@ function App() {
<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-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all" className={`w-full pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 transition-all ${
isReadOnly ? 'blur-sm select-none' : ''
}`}
placeholder="Unlock private key..." placeholder="Unlock private key..."
value={privateKeyPassphrase} value={privateKeyPassphrase}
onChange={(e) => setPrivateKeyPassphrase(e.target.value)} onChange={(e) => setPrivateKeyPassphrase(e.target.value)}
@@ -443,7 +453,7 @@ function App() {
{/* Security Panel */} {/* Security Panel */}
<div className="space-y-2"> {/* Added space-y-2 wrapper */} <div className="space-y-2"> {/* Added space-y-2 wrapper */}
<label className="text-sm font-semibold text-slate-700 flex items-center gap-2"> <label className="text-sm font-semibold text-slate-200 flex items-center gap-2">
<Lock size={14} /> SECURITY OPTIONS <Lock size={14} /> SECURITY OPTIONS
</label> </label>
@@ -456,7 +466,9 @@ function App() {
<Lock className="absolute left-3 top-3 text-slate-400" size={16} /> <Lock className="absolute left-3 top-3 text-slate-400" size={16} />
<input <input
type="password" type="password"
className="w-full pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all" className={`w-full pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 transition-all ${
isReadOnly ? 'blur-sm select-none' : ''
}`}
placeholder="Optional password..." placeholder="Optional password..."
value={activeTab === 'backup' ? backupMessagePassword : restoreMessagePassword} value={activeTab === 'backup' ? backupMessagePassword : restoreMessagePassword}
onChange={(e) => activeTab === 'backup' ? setBackupMessagePassword(e.target.value) : setRestoreMessagePassword(e.target.value)} onChange={(e) => activeTab === 'backup' ? setBackupMessagePassword(e.target.value) : setRestoreMessagePassword(e.target.value)}
@@ -475,7 +487,7 @@ function App() {
checked={hasBip39Passphrase} checked={hasBip39Passphrase}
onChange={(e) => setHasBip39Passphrase(e.target.checked)} onChange={(e) => setHasBip39Passphrase(e.target.checked)}
disabled={isReadOnly} disabled={isReadOnly}
className="rounded text-blue-600 focus:ring-2 focus:ring-blue-500 transition-all" className="rounded text-teal-600 focus:ring-2 focus:ring-teal-500 transition-all"
/> />
<span className="text-xs font-medium text-slate-700 group-hover:text-slate-900 transition-colors"> <span className="text-xs font-medium text-slate-700 group-hover:text-slate-900 transition-colors">
BIP39 25th word active BIP39 25th word active
@@ -484,12 +496,6 @@ function App() {
</div> </div>
)} )}
<ReadOnly
isReadOnly={isReadOnly}
onToggle={setIsReadOnly}
appVersion={__APP_VERSION__}
buildHash={__BUILD_HASH__}
/>
</div> </div>
{/* Action Button */} {/* Action Button */}
@@ -497,7 +503,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-blue-600 to-blue-700 text-white rounded-xl font-bold flex items-center justify-center gap-2 hover:from-blue-700 hover:to-blue-800 transition-all shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:from-blue-600 disabled:hover:to-blue-700" className="w-full py-4 bg-gradient-to-r from-teal-500 to-cyan-600 text-white rounded-xl font-bold flex items-center justify-center gap-2 hover:from-teal-600 hover:to-cyan-700 transition-all shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:from-teal-500 disabled:hover:to-cyan-600"
> >
{loading ? ( {loading ? (
<RefreshCw className="animate-spin" size={20} /> <RefreshCw className="animate-spin" size={20} />
@@ -526,7 +532,7 @@ function App() {
{/* QR Output */} {/* QR Output */}
{qrPayload && activeTab === 'backup' && ( {qrPayload && activeTab === 'backup' && (
<div className="pt-6 border-t border-slate-200 space-y-6 animate-in fade-in slide-in-from-bottom-4"> <div className="pt-6 border-t border-slate-200 space-y-6 animate-in fade-in slide-in-from-bottom-4">
<div className="flex justify-center"> <div className={isReadOnly ? 'blur-lg' : ''}>
<QrDisplay value={qrPayload} /> <QrDisplay value={qrPayload} />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
@@ -549,7 +555,7 @@ function App() {
readOnly readOnly
value={qrPayload} value={qrPayload}
onFocus={(e) => e.currentTarget.select()} onFocus={(e) => e.currentTarget.select()}
className="w-full h-28 p-3 bg-slate-900 rounded-xl font-mono text-[10px] text-green-400 placeholder:text-slate-500 border border-slate-700 shadow-inner leading-relaxed resize-none focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full h-28 p-3 bg-slate-900 rounded-xl font-mono text-[10px] text-green-400 placeholder:text-slate-500 border border-slate-700 shadow-inner leading-relaxed resize-none focus:outline-none focus:ring-2 focus:ring-teal-500"
/> />
<p className="text-[11px] text-slate-500"> <p className="text-[11px] text-slate-500">
Tip: click the box to select all, or use Copy. Tip: click the box to select all, or use Copy.
@@ -575,7 +581,9 @@ function App() {
</div> </div>
<div className="p-6 bg-white rounded-xl border-2 border-green-200 shadow-sm"> <div className="p-6 bg-white rounded-xl border-2 border-green-200 shadow-sm">
<p className="font-mono text-center text-lg text-slate-800 tracking-wide leading-relaxed break-words"> <p className={`font-mono text-center text-lg text-slate-800 tracking-wide leading-relaxed break-words ${
isReadOnly ? 'blur-md select-none' : ''
}`}>
{decryptedRestoredMnemonic} {decryptedRestoredMnemonic}
</p> </p>
</div> </div>
@@ -584,6 +592,11 @@ function App() {
)} )}
</div> </div>
</main> </main>
<Footer
appVersion={__APP_VERSION__}
buildHash={__BUILD_HASH__}
buildTimestamp={__BUILD_TIMESTAMP__}
/>
{/* QR Scanner Modal */} {/* QR Scanner Modal */}
{showQRScanner && ( {showQRScanner && (
@@ -650,8 +663,45 @@ function App() {
</div> </div>
</div> </div>
)} )}
{/* Lock Confirmation Modal */}
{showLockConfirm && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50">
<div className="bg-slate-800 rounded-xl border border-slate-700 p-6 max-w-md w-full mx-4 shadow-2xl">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<Lock className="w-5 h-5 text-amber-500" />
Lock Sensitive Data?
</h3>
<div className="text-sm text-slate-300 space-y-3 mb-6">
<p>This will:</p>
<ul className="list-disc list-inside space-y-1 text-slate-400">
<li>Blur all sensitive data (mnemonics, keys, passwords)</li>
<li>Disable all inputs</li>
<li>Prevent clipboard operations</li>
</ul>
<p className="text-xs text-slate-500 mt-2">
Use this when showing the app to others or stepping away from your device.
</p>
</div>
<div className="flex gap-3">
<button
className="flex-1 py-2 bg-slate-700 hover:bg-slate-600 rounded-lg transition-colors"
onClick={() => setShowLockConfirm(false)}
>
Cancel
</button>
<button
className="flex-1 py-2 bg-amber-500 hover:bg-amber-600 text-white font-semibold rounded-lg transition-colors"
onClick={confirmLock}
>
Lock Data
</button>
</div>
</div>
</div>
)}
</div> </div>
); );
} }
export default App; export default App;

18
src/components/Footer.tsx Normal file
View File

@@ -0,0 +1,18 @@
import React from 'react';
interface FooterProps {
appVersion: string;
buildHash: string;
buildTimestamp: string;
}
const Footer: React.FC<FooterProps> = ({ appVersion, buildHash, buildTimestamp }) => {
return (
<footer className="text-center text-xs text-slate-400 p-4">
<p>SeedPGP v{appVersion} build {buildHash} {buildTimestamp}</p>
<p className="mt-1">Never share your private keys or seed phrases. Always verify on an airgapped device.</p>
</footer>
);
};
export default Footer;

View File

@@ -3,6 +3,7 @@ import { Shield, Lock } 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;
@@ -29,6 +30,8 @@ interface HeaderProps {
encryptedMnemonicCache: any; encryptedMnemonicCache: any;
handleLockAndClear: () => void; handleLockAndClear: () => void;
appVersion: string; appVersion: string;
isLocked: boolean;
onToggleLock: () => void;
} }
const Header: React.FC<HeaderProps> = ({ const Header: React.FC<HeaderProps> = ({
@@ -42,7 +45,9 @@ const Header: React.FC<HeaderProps> = ({
setActiveTab, setActiveTab,
encryptedMnemonicCache, encryptedMnemonicCache,
handleLockAndClear, handleLockAndClear,
appVersion appVersion,
isLocked,
onToggleLock
}) => { }) => {
return ( return (
<header className="sticky top-0 z-50 bg-slate-900 border-b border-slate-800 backdrop-blur-sm"> <header className="sticky top-0 z-50 bg-slate-900 border-b border-slate-800 backdrop-blur-sm">
@@ -50,12 +55,12 @@ const Header: React.FC<HeaderProps> = ({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{/* Left: Logo & Title */} {/* Left: Logo & Title */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center"> <div className="w-10 h-10 bg-teal-500 rounded-lg flex items-center justify-center">
<Shield className="w-6 h-6 text-white" /> <Shield className="w-6 h-6 text-white" />
</div> </div>
<div> <div>
<h1 className="text-lg font-semibold text-white"> <h1 className="text-lg font-semibold text-white">
SeedPGP <span className="text-blue-400">v{appVersion}</span> SeedPGP <span className="text-teal-400">v{appVersion}</span>
</h1> </h1>
<p className="text-xs text-slate-400">OpenPGP-secured BIP39 backup</p> <p className="text-xs text-slate-400">OpenPGP-secured BIP39 backup</p>
</div> </div>
@@ -70,6 +75,7 @@ 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>
{/* Right: Action Buttons */} {/* Right: Action Buttons */}
@@ -84,13 +90,13 @@ const Header: React.FC<HeaderProps> = ({
</button> </button>
)} )}
<button <button
className={`px-4 py-2 rounded-lg ${activeTab === 'backup' ? 'bg-blue-500 hover:bg-blue-600' : 'bg-slate-700 hover:bg-slate-600'}`} className={`px-4 py-2 rounded-lg ${activeTab === 'backup' ? 'bg-teal-500 hover:bg-teal-600' : 'bg-slate-700 hover:bg-slate-600'}`}
onClick={() => setActiveTab('backup')} onClick={() => setActiveTab('backup')}
> >
Backup Backup
</button> </button>
<button <button
className={`px-4 py-2 rounded-lg ${activeTab === 'restore' ? 'bg-blue-500 hover:bg-blue-600' : 'bg-slate-700 hover:bg-slate-600'}`} className={`px-4 py-2 rounded-lg ${activeTab === 'restore' ? 'bg-teal-500 hover:bg-teal-600' : 'bg-slate-700 hover:bg-slate-600'}`}
onClick={() => setActiveTab('restore')} onClick={() => setActiveTab('restore')}
> >
Restore Restore
@@ -107,6 +113,7 @@ 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>
</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-slate-700 flex items-center justify-between"> <label className="text-sm font-semibold text-slate-200 flex items-center justify-between">
<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,16 +69,17 @@ export const PgpKeyInput: React.FC<PgpKeyInputProps> = ({
onDrop={handleDrop} onDrop={handleDrop}
> >
<textarea <textarea
className={`w-full h-40 p-3 bg-slate-50 border rounded-xl text-xs font-mono transition-colors resize-none focus:outline-none focus:ring-2 focus:ring-blue-500 ${isDragging && !readOnly ? 'border-blue-500 bg-blue-50' : 'border-slate-200' className={`w-full h-40 p-3 bg-slate-50 border rounded-xl text-xs font-mono transition-colors resize-none focus:outline-none focus:ring-2 focus:ring-teal-500 ${isDragging && !readOnly ? 'border-teal-500 bg-teal-50' : 'border-slate-200'} ${
}`} readOnly ? 'blur-sm select-none' : ''
}`}
placeholder={placeholder} placeholder={placeholder}
value={value} value={value}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
readOnly={readOnly} readOnly={readOnly}
/> />
{isDragging && !readOnly && ( {isDragging && !readOnly && (
<div className="absolute inset-0 flex items-center justify-center bg-blue-50/90 rounded-xl border-2 border-dashed border-blue-500 pointer-events-none z-10"> <div className="absolute inset-0 flex items-center justify-center bg-teal-50/90 rounded-xl border-2 border-dashed border-teal-500 pointer-events-none z-10">
<div className="text-blue-600 font-bold flex flex-col items-center animate-bounce"> <div className="text-teal-600 font-bold flex flex-col items-center animate-bounce">
<Upload size={24} /> <Upload size={24} />
<span className="text-sm mt-2">Drop Key File Here</span> <span className="text-sm mt-2">Drop Key File Here</span>
</div> </div>

View File

@@ -159,7 +159,7 @@ export default function QRScanner({ onScanSuccess, onClose }: QRScannerProps) {
<div className="space-y-3"> <div className="space-y-3">
<button <button
onClick={startCamera} onClick={startCamera}
className="w-full py-4 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-xl font-semibold flex items-center justify-center gap-2 hover:from-blue-700 hover:to-blue-800 transition-all shadow-lg" className="w-full py-4 bg-gradient-to-r from-teal-500 to-cyan-600 text-white rounded-xl font-semibold flex items-center justify-center gap-2 hover:from-teal-600 hover:to-cyan-700 transition-all shadow-lg"
> >
<Camera size={20} /> <Camera size={20} />
Use Camera Use Camera
@@ -184,7 +184,7 @@ export default function QRScanner({ onScanSuccess, onClose }: QRScannerProps) {
{/* Info Box */} {/* Info Box */}
<div className="pt-4 border-t border-slate-200"> <div className="pt-4 border-t border-slate-200">
<div className="flex gap-2 text-xs text-slate-600 leading-relaxed"> <div className="flex gap-2 text-xs text-slate-600 leading-relaxed">
<Info size={14} className="shrink-0 mt-0.5 text-blue-600" /> <Info size={14} className="shrink-0 mt-0.5 text-teal-600" />
<div> <div>
<p><strong>Camera:</strong> Requires HTTPS or localhost</p> <p><strong>Camera:</strong> Requires HTTPS or localhost</p>
<p className="mt-1"><strong>Upload:</strong> Screenshot QR from Backup tab for testing</p> <p className="mt-1"><strong>Upload:</strong> Screenshot QR from Backup tab for testing</p>
@@ -210,7 +210,7 @@ export default function QRScanner({ onScanSuccess, onClose }: QRScannerProps) {
{/* File Processing View */} {/* File Processing View */}
{scanMode === 'file' && scanning && ( {scanMode === 'file' && scanning && (
<div className="py-8 text-center"> <div className="py-8 text-center">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-4 border-slate-200 border-t-blue-600"></div> <div className="inline-block animate-spin rounded-full h-8 w-8 border-4 border-slate-200 border-t-teal-600"></div>
<p className="mt-3 text-sm text-slate-600">Processing image...</p> <p className="mt-3 text-sm text-slate-600">Processing image...</p>
</div> </div>
)} )}

View File

@@ -17,7 +17,7 @@ export function ReadOnly({ isReadOnly, onToggle, buildHash, appVersion }: ReadOn
type="checkbox" type="checkbox"
checked={isReadOnly} checked={isReadOnly}
onChange={(e) => onToggle(e.target.checked)} onChange={(e) => onToggle(e.target.checked)}
className="rounded text-blue-600 focus:ring-2 focus:ring-blue-500 transition-all" className="rounded text-teal-600 focus:ring-2 focus:ring-teal-500 transition-all"
/> />
<span className="text-xs font-medium text-slate-700 group-hover:text-slate-900 transition-colors"> <span className="text-xs font-medium text-slate-700 group-hover:text-slate-900 transition-colors">
Read-only Mode Read-only Mode

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { Lock, Unlock } from 'lucide-react';
interface EditLockBadgeProps {
isLocked: boolean;
onToggle: () => void;
}
const EditLockBadge: React.FC<EditLockBadgeProps> = ({ isLocked, onToggle }) => {
return (
<button
onClick={onToggle}
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg border transition-all hover:scale-105 ${
isLocked
? 'text-amber-500 bg-amber-500/10 border-amber-500/30 font-semibold'
: 'text-green-500 bg-green-500/10 border-green-500/30'
}`}
title={isLocked ? 'Click to unlock and edit' : 'Click to lock and blur sensitive data'}
>
{isLocked ? <Lock className="w-3.5 h-3.5" /> : <Unlock className="w-3.5 h-3.5" />}
<span className="text-xs font-medium">
{isLocked ? 'Locked' : 'Edit'}
</span>
</button>
);
};
export default EditLockBadge;

View File

@@ -20,7 +20,7 @@ const StorageBadge: React.FC<StorageBadgeProps> = ({ localItems, sessionItems })
const status = sensitiveCount > 0 ? 'Warning' : totalItems > 0 ? 'Active' : 'Empty'; const status = sensitiveCount > 0 ? 'Warning' : totalItems > 0 ? 'Active' : 'Empty';
const colorClass = const colorClass =
status === 'Warning' ? 'text-amber-500/80' : status === 'Warning' ? 'text-amber-500/80' :
status === 'Active' ? 'text-blue-500/80' : status === 'Active' ? 'text-teal-500/80' :
'text-green-500/80'; 'text-green-500/80';
return ( return (

1
src/vite-env.d.ts vendored
View File

@@ -8,3 +8,4 @@ declare module '*.css' {
declare const __APP_VERSION__: string; declare const __APP_VERSION__: string;
declare const __BUILD_HASH__: string; declare const __BUILD_HASH__: string;
declare const __BUILD_TIMESTAMP__: string;

View File

@@ -29,5 +29,6 @@ export default defineConfig({
define: { define: {
'__APP_VERSION__': JSON.stringify(appVersion), '__APP_VERSION__': JSON.stringify(appVersion),
'__BUILD_HASH__': JSON.stringify(gitHash), '__BUILD_HASH__': JSON.stringify(gitHash),
'__BUILD_TIMESTAMP__': JSON.stringify(new Date().toISOString()),
} }
}) })