mirror of
https://github.com/kccleoc/seedpgp-web.git
synced 2026-03-07 09:57:50 +08:00
feat: Implement 'Lock/Edit' mode with blur and confirmation dialog
This commit is contained in:
18
src/components/Footer.tsx
Normal file
18
src/components/Footer.tsx
Normal 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;
|
||||
@@ -3,6 +3,7 @@ import { Shield, Lock } from 'lucide-react';
|
||||
import SecurityBadge from './badges/SecurityBadge';
|
||||
import StorageBadge from './badges/StorageBadge';
|
||||
import ClipboardBadge from './badges/ClipboardBadge';
|
||||
import EditLockBadge from './badges/EditLockBadge';
|
||||
|
||||
interface StorageItem {
|
||||
key: string;
|
||||
@@ -29,6 +30,8 @@ interface HeaderProps {
|
||||
encryptedMnemonicCache: any;
|
||||
handleLockAndClear: () => void;
|
||||
appVersion: string;
|
||||
isLocked: boolean;
|
||||
onToggleLock: () => void;
|
||||
}
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({
|
||||
@@ -42,7 +45,9 @@ const Header: React.FC<HeaderProps> = ({
|
||||
setActiveTab,
|
||||
encryptedMnemonicCache,
|
||||
handleLockAndClear,
|
||||
appVersion
|
||||
appVersion,
|
||||
isLocked,
|
||||
onToggleLock
|
||||
}) => {
|
||||
return (
|
||||
<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">
|
||||
{/* Left: Logo & Title */}
|
||||
<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" />
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<p className="text-xs text-slate-400">OpenPGP-secured BIP39 backup</p>
|
||||
</div>
|
||||
@@ -70,6 +75,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||
<div onClick={onOpenClipboardModal} className="cursor-pointer">
|
||||
<ClipboardBadge events={events} onOpenClipboardModal={onOpenClipboardModal} />
|
||||
</div>
|
||||
<EditLockBadge isLocked={isLocked} onToggle={onToggleLock} />
|
||||
</div>
|
||||
|
||||
{/* Right: Action Buttons */}
|
||||
@@ -84,13 +90,13 @@ const Header: React.FC<HeaderProps> = ({
|
||||
</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')}
|
||||
>
|
||||
Backup
|
||||
</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')}
|
||||
>
|
||||
Restore
|
||||
@@ -107,6 +113,7 @@ const Header: React.FC<HeaderProps> = ({
|
||||
<div onClick={onOpenClipboardModal} className="cursor-pointer">
|
||||
<ClipboardBadge events={events} onOpenClipboardModal={onOpenClipboardModal} />
|
||||
</div>
|
||||
<EditLockBadge isLocked={isLocked} onToggle={onToggleLock} />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -52,7 +52,7 @@ export const PgpKeyInput: React.FC<PgpKeyInputProps> = ({
|
||||
|
||||
return (
|
||||
<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">
|
||||
{Icon && <Icon size={14} />} {label}
|
||||
</span>
|
||||
@@ -69,16 +69,17 @@ export const PgpKeyInput: React.FC<PgpKeyInputProps> = ({
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<textarea
|
||||
className={`w-full h-40 p-3 bg-slate-50 border rounded-xl text-xs font-mono transition-colors resize-none focus:outline-none focus:ring-2 focus:ring-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}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
readOnly={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="text-blue-600 font-bold flex flex-col items-center animate-bounce">
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-teal-50/90 rounded-xl border-2 border-dashed border-teal-500 pointer-events-none z-10">
|
||||
<div className="text-teal-600 font-bold flex flex-col items-center animate-bounce">
|
||||
<Upload size={24} />
|
||||
<span className="text-sm mt-2">Drop Key File Here</span>
|
||||
</div>
|
||||
|
||||
@@ -159,7 +159,7 @@ export default function QRScanner({ onScanSuccess, onClose }: QRScannerProps) {
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
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} />
|
||||
Use Camera
|
||||
@@ -184,7 +184,7 @@ export default function QRScanner({ onScanSuccess, onClose }: QRScannerProps) {
|
||||
{/* Info Box */}
|
||||
<div className="pt-4 border-t border-slate-200">
|
||||
<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>
|
||||
<p><strong>Camera:</strong> Requires HTTPS or localhost</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 */}
|
||||
{scanMode === 'file' && scanning && (
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -17,7 +17,7 @@ export function ReadOnly({ isReadOnly, onToggle, buildHash, appVersion }: ReadOn
|
||||
type="checkbox"
|
||||
checked={isReadOnly}
|
||||
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">
|
||||
Read-only Mode
|
||||
|
||||
28
src/components/badges/EditLockBadge.tsx
Normal file
28
src/components/badges/EditLockBadge.tsx
Normal 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;
|
||||
@@ -20,7 +20,7 @@ const StorageBadge: React.FC<StorageBadgeProps> = ({ localItems, sessionItems })
|
||||
const status = sensitiveCount > 0 ? 'Warning' : totalItems > 0 ? 'Active' : 'Empty';
|
||||
const colorClass =
|
||||
status === 'Warning' ? 'text-amber-500/80' :
|
||||
status === 'Active' ? 'text-blue-500/80' :
|
||||
status === 'Active' ? 'text-teal-500/80' :
|
||||
'text-green-500/80';
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user