commit a4528eb1f77521f0f231f95900ac3f7f7d80ea79 Author: Zaki <18zaki18@gmail.com> Date: Wed May 6 20:49:51 2026 +0100 Standard password manager diff --git a/api.php b/api.php new file mode 100644 index 0000000..5063476 --- /dev/null +++ b/api.php @@ -0,0 +1,273 @@ + 'Server error: ' . $e->getMessage()]); + exit; +}); + +header('Content-Type: application/json'); +header('Access-Control-Allow-Origin: *'); +header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); +header('Access-Control-Allow-Headers: Content-Type, Authorization'); + +if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit(0); + +$db_path = __DIR__ . '/vault.db'; +$db = new SQLite3($db_path); +$db->enableExceptions(true); + +// Create tables +$db->exec(" + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + salt TEXT NOT NULL, + encryption_key TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + CREATE TABLE IF NOT EXISTS folders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + name TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + UNIQUE(user_id, name) + ); + CREATE TABLE IF NOT EXISTS vault_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + site TEXT NOT NULL, + username TEXT NOT NULL, + encrypted_password TEXT NOT NULL, + iv TEXT NOT NULL, + encryption_method TEXT DEFAULT 'server', + folder TEXT DEFAULT 'All', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); +"); + +// Fix missing columns (existing databases) +try { $db->exec("ALTER TABLE vault_entries ADD COLUMN encryption_method TEXT DEFAULT 'server'"); } catch (Exception $e) {} +try { $db->exec("ALTER TABLE vault_entries ADD COLUMN folder TEXT DEFAULT 'All'"); } catch (Exception $e) {} + +$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); +$path = str_replace('/password-manager/api.php', '', $path); +$path = str_replace('/api.php', '', $path); +$method = $_SERVER['REQUEST_METHOD']; +$input = json_decode(file_get_contents('php://input'), true) ?? []; + +function genKey($pwd, $salt) { return hash_pbkdf2('sha256', $pwd, $salt, 100000, 32, true); } + +function authenticate($db) { + $headers = getallheaders(); + $token = str_replace('Bearer ', '', $headers['Authorization'] ?? ''); + if (!$token) { http_response_code(401); echo json_encode(['error'=>'No token']); exit; } + $decoded = base64_decode($token); + if (!$decoded) { http_response_code(401); echo json_encode(['error'=>'Bad token']); exit; } + $parts = explode(':', $decoded); + if (count($parts) !== 3) { http_response_code(401); echo json_encode(['error'=>'Token format']); exit; } + $uid = intval($parts[0]); + $key = base64_decode($parts[2]); + $st = $db->prepare('SELECT id, encryption_key FROM users WHERE id=:id'); + $st->bindValue(':id', $uid, SQLITE3_INTEGER); + $user = $st->execute()->fetchArray(SQLITE3_ASSOC); + if (!$user || $user['encryption_key'] !== base64_encode($key)) { http_response_code(401); echo json_encode(['error'=>'Invalid session']); exit; } + return ['userId' => $uid]; +} + +try { + switch (true) { + // Auth + case ($path === '/register' && $method === 'POST'): + $u = trim($input['username'] ?? ''); + $p = $input['masterPassword'] ?? ''; + if (strlen($u) < 3 || strlen($p) < 8) { http_response_code(400); echo json_encode(['error'=>'Min 3/8 chars']); break; } + $st = $db->prepare('SELECT id FROM users WHERE username=:u'); + $st->bindValue(':u', $u, SQLITE3_TEXT); + if ($st->execute()->fetchArray()) { http_response_code(409); echo json_encode(['error'=>'Username exists']); break; } + $salt = bin2hex(random_bytes(32)); + $hash = hash_pbkdf2('sha256', $p, $salt, 100000); + $key = genKey($p, $salt); + $st = $db->prepare('INSERT INTO users (username, password_hash, salt, encryption_key) VALUES (:u, :h, :s, :k)'); + $st->bindValue(':u', $u, SQLITE3_TEXT); + $st->bindValue(':h', $hash, SQLITE3_TEXT); + $st->bindValue(':s', $salt, SQLITE3_TEXT); + $st->bindValue(':k', base64_encode($key), SQLITE3_TEXT); + $st->execute(); + $uid = $db->lastInsertRowID(); + // Insert default folders + $defaultFolders = ['All', 'Social', 'Banking', 'Work', 'Personal']; + $stFolder = $db->prepare('INSERT OR IGNORE INTO folders (user_id, name) VALUES (:uid, :name)'); + foreach ($defaultFolders as $name) { + $stFolder->bindValue(':uid', $uid, SQLITE3_INTEGER); + $stFolder->bindValue(':name', $name, SQLITE3_TEXT); + $stFolder->execute(); + } + $token = base64_encode($uid . ':' . bin2hex(random_bytes(16)) . ':' . base64_encode($key)); + echo json_encode(['message'=>'OK','token'=>$token,'userId'=>$uid,'salt'=>$salt]); + break; + + case ($path === '/login' && $method === 'POST'): + $u = trim($input['username'] ?? ''); + $p = $input['masterPassword'] ?? ''; + $st = $db->prepare('SELECT * FROM users WHERE username=:u'); + $st->bindValue(':u', $u, SQLITE3_TEXT); + $user = $st->execute()->fetchArray(SQLITE3_ASSOC); + if (!$user) { http_response_code(401); echo json_encode(['error'=>'Invalid credentials']); break; } + if (!hash_equals($user['password_hash'], hash_pbkdf2('sha256', $p, $user['salt'], 100000))) { + http_response_code(401); echo json_encode(['error'=>'Invalid credentials']); break; + } + $key = genKey($p, $user['salt']); + $token = base64_encode($user['id'] . ':' . bin2hex(random_bytes(16)) . ':' . base64_encode($key)); + // Ensure default folders exist for existing users (idempotent) + $defaultFolders = ['All', 'Social', 'Banking', 'Work', 'Personal']; + $stFolder = $db->prepare('INSERT OR IGNORE INTO folders (user_id, name) VALUES (:uid, :name)'); + foreach ($defaultFolders as $name) { + $stFolder->bindValue(':uid', $user['id'], SQLITE3_INTEGER); + $stFolder->bindValue(':name', $name, SQLITE3_TEXT); + $stFolder->execute(); + } + echo json_encode(['message'=>'OK','token'=>$token,'userId'=>$user['id'],'salt'=>$user['salt']]); + break; + + // Folders + case ($path === '/folders' && $method === 'GET'): + $auth = authenticate($db); + $st = $db->prepare('SELECT name FROM folders WHERE user_id=:uid ORDER BY name'); + $st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER); + $res = $st->execute(); + $folders = []; + while ($row = $res->fetchArray(SQLITE3_ASSOC)) { + $folders[] = $row['name']; + } + echo json_encode($folders); + break; + + case ($path === '/folders' && $method === 'POST'): + $auth = authenticate($db); + $name = trim($input['name'] ?? ''); + if (!$name) { http_response_code(400); echo json_encode(['error'=>'Folder name required']); break; } + if (strtolower($name) === 'all') { http_response_code(400); echo json_encode(['error'=>'Cannot use "All"']); break; } + $st = $db->prepare('INSERT INTO folders (user_id, name) VALUES (:uid, :name)'); + $st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER); + $st->bindValue(':name', $name, SQLITE3_TEXT); + try { $st->execute(); } + catch (Exception $e) { http_response_code(409); echo json_encode(['error'=>'Folder already exists']); break; } + echo json_encode(['message'=>'Folder created', 'name'=>$name]); + break; + + case (preg_match('/^\/folders\/(.+)$/', $path, $m) && $method === 'DELETE'): + $auth = authenticate($db); + $folderName = urldecode($m[1]); + if ($folderName === 'All') { http_response_code(400); echo json_encode(['error'=>'Cannot delete All']); break; } + $st = $db->prepare('DELETE FROM folders WHERE user_id=:uid AND name=:name'); + $st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER); + $st->bindValue(':name', $folderName, SQLITE3_TEXT); + $st->execute(); + if ($db->changes() === 0) { http_response_code(404); echo json_encode(['error'=>'Folder not found']); break; } + // Update entries that had this folder to 'All' + $stUp = $db->prepare("UPDATE vault_entries SET folder='All' WHERE user_id=:uid AND folder=:f"); + $stUp->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER); + $stUp->bindValue(':f', $folderName, SQLITE3_TEXT); + $stUp->execute(); + echo json_encode(['message'=>'Deleted']); + break; + + // Entries + case ($path === '/entries' && $method === 'GET'): + $auth = authenticate($db); + $q = $_GET['search'] ?? ''; + if ($q) { + $st = $db->prepare('SELECT * FROM vault_entries WHERE user_id=:uid AND (site LIKE :q OR username LIKE :q) ORDER BY updated_at DESC'); + $st->bindValue(':q', "%$q%", SQLITE3_TEXT); + } else { + $st = $db->prepare('SELECT * FROM vault_entries WHERE user_id=:uid ORDER BY updated_at DESC'); + } + $st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER); + $res = $st->execute(); + $entries = []; + while ($r = $res->fetchArray(SQLITE3_ASSOC)) { + $entries[] = [ + 'id' => $r['id'], + 'site' => $r['site'], + 'username' => $r['username'], + 'encrypted_password' => $r['encrypted_password'], + 'iv' => $r['iv'], + 'encryption_method' => $r['encryption_method'] ?? 'server', + 'folder' => $r['folder'] ?? 'All', + 'created_at' => $r['created_at'], + 'updated_at' => $r['updated_at'] + ]; + } + echo json_encode($entries); + break; + + case ($path === '/entries' && $method === 'POST'): + $auth = authenticate($db); + $site = trim($input['site'] ?? ''); + $username = trim($input['username'] ?? ''); + $folder = trim($input['folder'] ?? 'All'); + $encPwd = $input['encrypted_password'] ?? ''; + $iv = $input['iv'] ?? ''; + if (empty($site) || empty($encPwd)) { http_response_code(400); echo json_encode(['error'=>'Site & password required']); break; } + $now = date('Y-m-d H:i:s'); + $st = $db->prepare('INSERT INTO vault_entries (user_id, site, username, encrypted_password, iv, encryption_method, folder, created_at, updated_at) VALUES (:uid,:s,:u,:e,:i,:m,:f,:c,:c)'); + $st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER); + $st->bindValue(':s', $site, SQLITE3_TEXT); + $st->bindValue(':u', $username, SQLITE3_TEXT); + $st->bindValue(':e', $encPwd, SQLITE3_TEXT); + $st->bindValue(':i', $iv, SQLITE3_TEXT); + $st->bindValue(':m', 'client', SQLITE3_TEXT); + $st->bindValue(':f', $folder, SQLITE3_TEXT); + $st->bindValue(':c', $now, SQLITE3_TEXT); + $st->execute(); + echo json_encode(['id'=>$db->lastInsertRowID(), 'site'=>$site, 'username'=>$username, 'folder'=>$folder]); + break; + + case (preg_match('/^\/entries\/(\d+)$/', $path, $m) && $method === 'PUT'): + $auth = authenticate($db); + $site = trim($input['site'] ?? ''); + $username = trim($input['username'] ?? ''); + $encPwd = $input['encrypted_password'] ?? ''; + $iv = $input['iv'] ?? ''; + $folder = trim($input['folder'] ?? 'All'); + if (empty($site) || empty($encPwd)) { http_response_code(400); echo json_encode(['error'=>'Site & password required']); break; } + $now = date('Y-m-d H:i:s'); + $st = $db->prepare('UPDATE vault_entries SET site=:s, username=:u, encrypted_password=:e, iv=:i, folder=:f, updated_at=:c WHERE id=:id AND user_id=:uid'); + $st->bindValue(':s', $site, SQLITE3_TEXT); + $st->bindValue(':u', $username, SQLITE3_TEXT); + $st->bindValue(':e', $encPwd, SQLITE3_TEXT); + $st->bindValue(':i', $iv, SQLITE3_TEXT); + $st->bindValue(':f', $folder, SQLITE3_TEXT); + $st->bindValue(':c', $now, SQLITE3_TEXT); + $st->bindValue(':id', $m[1], SQLITE3_INTEGER); + $st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER); + $st->execute(); + echo json_encode(['message'=>'Updated']); + break; + + case (preg_match('/^\/entries\/(\d+)$/', $path, $m) && $method === 'DELETE'): + $auth = authenticate($db); + $st = $db->prepare('DELETE FROM vault_entries WHERE id=:id AND user_id=:uid'); + $st->bindValue(':id', $m[1], SQLITE3_INTEGER); + $st->bindValue(':uid', $auth['userId'], SQLITE3_INTEGER); + $st->execute(); + echo json_encode(['message'=>'Deleted']); + break; + + default: + http_response_code(404); + echo json_encode(['error'=>'Not found']); + } +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); +} + +$db->close(); +?> \ No newline at end of file diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..bd76339 --- /dev/null +++ b/css/style.css @@ -0,0 +1,426 @@ +:root { + --bg: #0a0e17; + --bg2: #131a24; + --text: #e1e8f0; + --text2: #8899aa; + --accent: #3b82f6; + --danger: #ef4444; + --success: #22c55e; + --warning: #f59e0b; + --border: rgba(255,255,255,0.06); + --card: rgba(18,25,35,0.85); + --input: #10161f; + --vault-bg: rgba(255,255,255,0.03); +} +.light { + --bg: #f4f6f9; + --bg2: #e9ecf1; + --text: #1a1f2e; + --text2: #5a6475; + --card: rgba(255,255,255,0.9); + --input: #ffffff; + --border: rgba(0,0,0,0.08); + --vault-bg: rgba(255,255,255,0.5); +} + +* { margin:0; padding:0; box-sizing:border-box; } +body { + background: linear-gradient(145deg, var(--bg) 0%, var(--bg2) 100%); + font-family: 'Segoe UI', system-ui, sans-serif; + min-height: 100vh; + display: flex; + justify-content: center; + align-items: flex-start; + padding: 1.2rem; + color: var(--text); + transition: background 0.3s, color 0.3s; +} + +.toast-container { + position: fixed; top: 1rem; right: 1rem; z-index: 9999; + display: flex; flex-direction: column; gap: 0.5rem; +} +.toast { + padding: 0.7rem 1.2rem; border-radius: 0.8rem; + font-size: 0.85rem; animation: slideIn 0.3s ease; color: #fff; +} +.toast.error { background: var(--danger); } +.toast.success { background: var(--success); } +@keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} + +.vault { + width: 96%; max-width: 1700px; + background: var(--vault-bg); + backdrop-filter: blur(20px); + border: 1px solid var(--border); + border-radius: 2rem; padding: 1.8rem; margin: 0.5rem auto; + box-shadow: 0 30px 50px rgba(0,0,0,0.4); +} +h1 { + font-size: 2rem; margin-bottom: 0.6rem; + display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; +} +h1 span { + background: var(--accent); padding: 0.2rem 0.7rem; + border-radius: 3rem; font-size: 0.8rem; color: #fff; +} + +/* Auth */ +.auth-section { + background: rgba(0,0,0,0.25); border-radius: 1.5rem; + padding: 1.3rem; margin-bottom: 1rem; border: 1px solid var(--border); +} +.auth-tabs { display: flex; gap: 1rem; margin-bottom: 0.8rem; } +.auth-tab { + background: none; border: none; color: var(--text2); + padding: 0.4rem 0.8rem; cursor: pointer; + border-bottom: 2px solid transparent; font-size: 0.9rem; +} +.auth-tab.active { color: var(--accent); border-bottom-color: var(--accent); } + +input, select, textarea { + flex: 1; min-width: 130px; background: var(--input); + border: 1px solid #2d3748; padding: 0.65rem 1rem; + border-radius: 2rem; color: var(--text); font-size: 0.85rem; + outline: none; font-family: inherit; +} +input:focus, select:focus { border-color: var(--accent); } + +.btn { + background: linear-gradient(135deg, #2563eb, #1d4ed8); + border: none; color: #fff; font-weight: 600; + padding: 0.65rem 1.3rem; border-radius: 2rem; + cursor: pointer; font-size: 0.85rem; transition: 0.2s; white-space: nowrap; +} +.btn:hover { filter: brightness(1.15); transform: scale(1.02); } +.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } +.btn-outline { background: transparent; border: 1px solid #475569; color: var(--text2); } +.btn-sm { padding: 0.35rem 0.9rem; font-size: 0.75rem; } +.btn-xs { padding: 0.2rem 0.5rem; font-size: 0.7rem; } +.btn-danger { background: var(--danger); } + +.input-group { display: flex; gap: 0.5rem; margin: 0.7rem 0; flex-wrap: wrap; align-items: center; } +.toolbar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.7rem; } + +.view-toggle { display: flex; gap: 0.2rem; background: rgba(0,0,0,0.3); padding: 0.2rem; border-radius: 2rem; } +.view-btn { + background: none; border: none; color: var(--text2); + padding: 0.35rem 0.7rem; border-radius: 2rem; cursor: pointer; + font-size: 0.75rem; transition: 0.2s; +} +.view-btn.active { background: var(--accent); color: #fff; } +.view-btn:hover:not(.active) { background: rgba(255,255,255,0.08); } + +.toggles-row { display: flex; gap: 1.2rem; align-items: center; flex-wrap: wrap; } +.toggle-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.75rem; color: var(--text2); } +.toggle-switch { + position: relative; width: 36px; height: 20px; + background: #334155; border-radius: 10px; cursor: pointer; transition: 0.2s; +} +.toggle-switch.active { background: var(--accent); } +.toggle-switch::after { + content: ''; position: absolute; top: 2px; left: 2px; + width: 16px; height: 16px; background: #fff; border-radius: 50%; transition: 0.2s; +} +.toggle-switch.active::after { left: 18px; } + +.status-badge { + background: var(--bg2); padding: 0.25rem 0.8rem; + border-radius: 2rem; font-size: 0.75rem; white-space: nowrap; +} +.hidden { display: none !important; } + +/* Strength */ +.strength-bar { height: 4px; border-radius: 2px; transition: 0.3s; margin-top: 0.2rem; } +.s0 { background: var(--danger); width: 20%; } +.s1 { background: var(--warning); width: 40%; } +.s2 { background: #eab308; width: 60%; } +.s3 { background: #84cc16; width: 80%; } +.s4 { background: var(--success); width: 100%; } + +/* Folders */ +.folders-bar { + display: flex; gap: 0.4rem; margin-bottom: 0.8rem; + flex-wrap: wrap; align-items: center; + padding: 0.4rem 0.6rem; background: rgba(0,0,0,0.2); border-radius: 1rem; +} +.folder-chip { + background: var(--bg2); border: 1px solid var(--border); + color: var(--text2); padding: 0.3rem 0.8rem; border-radius: 2rem; + cursor: pointer; font-size: 0.78rem; transition: 0.2s; white-space: nowrap; +} +.folder-chip:hover { background: var(--accent); color: #fff; border-color: var(--accent); } +.folder-chip.active { background: var(--accent); color: #fff; border-color: var(--accent); } +.folder-count { + background: rgba(0,0,0,0.3); padding: 0.1rem 0.4rem; + border-radius: 1rem; margin-left: 0.3rem; font-size: 0.7rem; +} +.folder-delete-btn { + background: transparent; border: none; color: var(--danger); + cursor: pointer; font-size: 0.7rem; margin-left: 0.2rem; opacity: 0.7; +} +.folder-delete-btn:hover { opacity: 1; } + +.folder-add-btn { + background: transparent; border: 1px dashed #475569; + color: var(--text2); padding: 0.3rem 0.6rem; border-radius: 2rem; + cursor: pointer; font-size: 0.75rem; transition: 0.2s; +} +.folder-add-btn:hover { border-color: var(--accent); color: var(--accent); } + +/* Add form folder select */ +#addFolderSelect { + min-width: 110px; max-width: 150px; + background: var(--input); border: 1px solid #2d3748; + color: var(--text); padding: 0.5rem 0.8rem; border-radius: 2rem; + font-size: 0.8rem; cursor: pointer; +} + +/* Entries */ +#entriesContainer.grid-view { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 0.6rem; +} +#entriesContainer.list-view { display: flex; flex-direction: column; gap: 0.4rem; } +#entriesContainer.compact-view { display: flex; flex-direction: column; gap: 0.2rem; } +#entriesContainer.table-view { overflow-x: auto; } +#entriesContainer.table-view table { width: 100%; border-collapse: collapse; } +#entriesContainer.table-view th { + text-align: left; padding: 0.4rem 0.6rem; + color: var(--text2); font-size: 0.75rem; border-bottom: 1px solid var(--border); +} +#entriesContainer.table-view td { + padding: 0.4rem 0.6rem; font-size: 0.8rem; + border-bottom: 1px solid rgba(255,255,255,0.03); +} + +.entry-card, .entry-row, .entry-compact, .table-row-drag { + cursor: grab; user-select: none; +} +.entry-card.drag-over, .entry-row.drag-over, .entry-compact.drag-over, .table-row-drag.drag-over { + border-color: var(--accent) !important; + box-shadow: 0 0 15px rgba(59,130,246,0.3); +} + +.entry-card { + background: var(--card); + border: 1px solid rgba(255, 255, 255, 0.1); /* brighter border */ + border-radius: 0.9rem; + padding: 1rem 2.5rem 1rem 1rem; + transition: 0.2s; + position: relative; + word-break: break-word; +} + +.entry-card:hover { border-color: rgba(255,255,255,0.15); transform: translateY(-2px); } +.card-site { font-weight: 700; font-size: 0.9rem; color: var(--text); margin-bottom: 0.2rem; } +.card-user { color: var(--text2); font-size: 0.75rem; margin-bottom: 0.3rem; } +.card-folder { + font-size: 0.65rem; color: var(--accent); margin-bottom: 0.3rem; + background: rgba(59,130,246,0.15); display: inline-block; + padding: 0.1rem 0.5rem; border-radius: 1rem; +} +.card-password { + background: var(--bg2); padding: 0.3rem 0.5rem; border-radius: 0.6rem; + display: flex; align-items: center; justify-content: space-between; + font-family: monospace; font-size: 0.75rem; gap: 0.2rem; +} +.entry-row { + background: var(--card); border: 1px solid var(--border); + border-radius: 0.8rem; padding: 0.7rem 2.8rem 0.7rem 0.9rem; + display: flex; justify-content: space-between; align-items: center; + flex-wrap: wrap; gap: 0.4rem; position: relative; +} +.entry-compact { + display: flex; align-items: center; gap: 0.5rem; + padding: 0.35rem 2.8rem 0.35rem 0.7rem; + background: var(--card); border-radius: 0.5rem; + border: 1px solid var(--border); font-size: 0.8rem; position: relative; +} + +.action-btns { position: absolute; top: 4px; right: 6px; display: flex; gap: 4px; z-index: 1; } +.delete-btn { + width: 20px; height: 20px; background: transparent; color: var(--danger); + border: none; cursor: pointer; font-size: 0.9rem; font-weight: 700; + display: flex; align-items: center; justify-content: center; +} +.delete-btn:hover { color: #fff; transform: scale(1.2); } +.light .delete-btn:hover { color: #000 !important; } +.edit-btn { + width: 20px; height: 20px; background: transparent; color: var(--accent); + border: none; cursor: pointer; font-size: 0.75rem; + display: flex; align-items: center; justify-content: center; +} +.edit-btn:hover { color: #fff; transform: scale(1.2); } +.light .edit-btn:hover { color: #000 !important; } + +.entry-info { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; flex: 1; } +.entry-site { font-weight: 700; color: var(--text); } +.entry-user { color: var(--text2); } +.entry-folder { + font-size: 0.7rem; color: var(--accent); + background: rgba(59,130,246,0.15); padding: 0.1rem 0.5rem; border-radius: 1rem; +} +.password-field { + display: flex; align-items: center; gap: 0.3rem; + background: var(--bg2); padding: 0.2rem 0.5rem; border-radius: 2rem; +} +.password-text { font-family: monospace; color: var(--text2); font-size: 0.8rem; } +.icon-btn { + background: none; border: 1px solid #475569; color: var(--text2); + border-radius: 2rem; padding: 0.2rem 0.5rem; font-size: 0.65rem; cursor: pointer; +} +.icon-btn:hover { background: #334155; } + +.search-box { flex: 1; min-width: 160px; } +.idle-warning { + position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); + background: rgba(0,0,0,0.95); color: #fff; padding: 2rem; + border-radius: 2rem; z-index: 10000; text-align: center; display: none; +} +.idle-warning.show { display: block; } + +/* Edit Modal */ +.edit-modal { + position: fixed; top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.7); display: flex; justify-content: center; + align-items: center; z-index: 1001; display: none; +} +.edit-modal.show { display: flex; } +.edit-box { + background: var(--bg2); border-radius: 1.5rem; padding: 1.5rem; + min-width: 380px; max-width: 90%; +} +.edit-box h3 { margin-bottom: 1rem; color: var(--text); } +.edit-box label { color: var(--text2); font-size: 0.8rem; display: block; margin-bottom: 0.2rem; } +.edit-box input, .edit-box select { width: 100%; margin-bottom: 0.5rem; } + +/* Custom modals (folder add/delete) */ +.custom-modal-overlay { + position: fixed; top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.6); display: flex; justify-content: center; + align-items: center; z-index: 10001; display: none; +} +.custom-modal-overlay.show { display: flex; } +.custom-modal { + background: var(--bg2); border: 1px solid var(--border); + border-radius: 1.2rem; padding: 1.5rem; min-width: 300px; max-width: 90%; +} +.custom-modal h3 { margin-bottom: 1rem; color: var(--text); } +.custom-modal input { width: 100%; margin-bottom: 1rem; } +.custom-modal .modal-actions { display: flex; gap: 0.5rem; justify-content: flex-end; } + +/* Confirm popup near element */ +.custom-confirm { + position: fixed; background: var(--bg2); border: 1px solid var(--accent); + border-radius: 0.8rem; padding: 0.7rem 1rem; z-index: 9999; + box-shadow: 0 10px 30px rgba(0,0,0,0.5); display: none; + font-size: 0.8rem; color: var(--text); white-space: nowrap; +} +.custom-confirm.show { display: block; } +.custom-confirm .confirm-text { margin-bottom: 0.5rem; } +.custom-confirm .confirm-btns { display: flex; gap: 0.4rem; } +.custom-confirm .confirm-yes { + background: var(--danger); color: #fff; border: none; + padding: 0.3rem 0.8rem; border-radius: 1.5rem; cursor: pointer; font-size: 0.75rem; +} +.custom-confirm .confirm-no { + background: #334155; color: #fff; border: none; + padding: 0.3rem 0.8rem; border-radius: 1.5rem; cursor: pointer; font-size: 0.75rem; +} + +/* Zigzag toast */ +/* .toast-zigzag { */ + /* position: fixed; z-index: 9998; padding: 0.5rem 0.9rem; */ + /* border-radius: 0.5rem; font-size: 0.78rem; font-weight: 600; */ + /* pointer-events: none; animation: zigzagUp 1.4s ease-out forwards; white-space: nowrap; */ +/* } */ +/* .toast-zigzag.success { background: rgba(34,197,94,0.95); color: #fff; } */ +/* .toast-zigzag.error { background: rgba(239,68,68,0.95); color: #fff; } */ +/* @keyframes zigzagUp { */ + /* 0% { opacity: 1; transform: translate(0,0) scale(1); } */ + /* 15% { opacity: 1; transform: translate(-12px,-12px) scale(1.05); } */ + /* 30% { opacity: 0.9; transform: translate(14px,-24px) scale(1); } */ + /* 50% { opacity: 0.7; transform: translate(-10px,-38px) scale(0.95); } */ + /* 70% { opacity: 0.4; transform: translate(10px,-52px) scale(0.9); } */ + /* 100% { opacity: 0; transform: translate(0,-68px) scale(0.8); } */ +/* } */ +/* Zigzag toast – smaller over time, more transparent */ +.toast-zigzag { + position: fixed; + z-index: 9998; + padding: 0.4rem 0.7rem; + border-radius: 0.5rem; + font-size: 0.72rem; + font-weight: 600; + pointer-events: none; + animation: zigzagUp 1.2s ease-out forwards; + white-space: nowrap; +} +.toast-zigzag.success { + background: rgba(34, 197, 94, 0.95); + color: #fff; +} +.toast-zigzag.error { + background: rgba(239, 68, 68, 0.95); + color: #fff; +} + +@keyframes zigzagUp { + 0% { + opacity: 1; + transform: translate(0, 0) scale(1); + } + 20% { + opacity: 0.9; + transform: translate(-10px, -14px) scale(0.9); + } + 40% { + opacity: 0.65; + transform: translate(12px, -28px) scale(0.75); + } + 70% { + opacity: 0.3; + transform: translate(-8px, -42px) scale(0.6); + } + 100% { + opacity: 0; + transform: translate(0, -60px) scale(0.4); + } +} +@media(max-width:700px) { + .vault { + background: rgba(20, 30, 45, 0.25); /* a bit more visible */ + backdrop-filter: blur(24px); + } + .entry-row { flex-direction: column; } + #entriesContainer.grid-view { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } + .toolbar { flex-direction: column; } + .folders-bar { flex-direction: column; align-items: stretch; } +} +/* Table view improvements */ +#entriesContainer.table-view { + overflow-x: auto; +} + +#entriesContainer.table-view table { + min-width: 600px; /* Ensures columns don't squash too much */ + table-layout: auto; +} + +#entriesContainer.table-view th, +#entriesContainer.table-view td { + white-space: nowrap; + padding: 0.5rem 0.8rem; +} + +/* Limit password cell width and show ellipsis */ +#entriesContainer.table-view td.password-cell { + max-width: 180px; + overflow: hidden; + text-overflow: ellipsis; +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..05bb8ad --- /dev/null +++ b/index.html @@ -0,0 +1,122 @@ + + +
+ + +Vault locks in 30s
Delete "${folderName}"? Entries will move to "All".
+| Site | ' + + (showMail ? 'User | ' : '') + + 'Password | Folder | Actions |
|---|---|---|---|---|
| 🌐 ' + esc(e.site) + ' | ' + + (showMail ? '👤 ' + esc(e.username) + ' | ' : '') + + '•••••••• | ' + + '📁 ' + esc(e.folder || 'All') + ' | ' + + '' + + (showView ? ' ' : '') + + ' ' + + ' ' + + '' + + ' | ' + + '