4b15811221
Round-trip companion to the existing doExport(). Supports two file
formats with auto-detection (extension + first-char sniff):
JSON
====
Native shape produced by doExport() AND a forgiving fallback for any
flat array of entry objects with site/url + password fields. Accepts:
- { version, exported_at, entries: [...] } (native)
- [{ ... }, { ... }] (flat array)
- mixed keys: site|url|name, username|user|login|email, etc.
CSV
===
RFC-4180-ish parser (~30 lines): quoted fields, escaped "", commas
inside quotes, CRLF line endings. No streaming since password-manager
imports are realistically MB-scale at most.
Heuristic column mapping (case + underscore tolerant) covers the
common exporters out of the box:
Site/URL : name, title, url, site, website, login_uri, login_url
Username : login_username, username, user, login, email
Password : login_password, password, pass, pwd
Folder : folder, group, category, path, collection
Tags : tags, labels (comma/semicolon-split)
Notes : notes, note, comment (short notes joined into tags)
TOTP : login_totp, totp, otpauth, authenticator, two_factor
If the TOTP column holds a full otpauth:// URI it's parsed and only
the secret param is stored — same path used by the slide-over TOTP
field. Invalid base32 TOTP secrets are dropped silently rather than
failing the whole import.
Backend
=======
New endpoint: POST /entries/bulk-import
Body: { entries: [{ site, username, encrypted_password, iv, folder,
tags, totp_secret, totp_iv }, ... ] }
Caps at 10,000 entries per request as a sanity bound. Inserts inside
a single SQLite transaction — partial failure rolls back cleanly, the
user retries from the same source file. Returns { imported: N }.
Rows missing site or ciphertext are skipped within the transaction
(not failed) so one bad row in a 500-entry import doesn't blow up
the whole batch.
Client flow
===========
doImport():
1. Hidden <input type="file" accept=".json,.csv"> picker
2. Read text, detect format, route to parseEntriesFromJSON or CSV
3. confirmDialog preview: count + first 3 sample sites + skipped rows
4. On confirm: encryptImportEntry() each plaintext entry with the
current vault key (reuses encryptPwd / base32Decode validation)
5. Single POST to /entries/bulk-import
6. Reload entries, refresh UI, trigger HIBP scan if enabled
UI
==
Two entry points (mirroring Export):
- Sidebar "Import vault" nav item, next to "Export vault"
- Settings panel "Import" section with descriptive blurb
Both call doImport(). New i-log-in icon added to the SVG sprite (mirror
of i-log-out used by Export).
Limitations
===========
- No de-duplication: importing the same file twice yields duplicate
entries. Trade-off to keep the v1 simple — the user can sort it
out with the existing trash/multi-select UI.
- No password-protected vault formats (Bitwarden encrypted JSON,
KeePass kdbx). Only plaintext exports — same trade-off as
doExport() which produces plaintext JSON.
530 lines
31 KiB
HTML
530 lines
31 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" data-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Vault</title>
|
|
<link rel="stylesheet" href="css/style.css">
|
|
</head>
|
|
<body>
|
|
|
|
<!-- ============================================================ -->
|
|
<!-- ICON SPRITE (Lucide subset) -->
|
|
<!-- ============================================================ -->
|
|
<svg xmlns="http://www.w3.org/2000/svg" style="display:none">
|
|
<symbol id="i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></symbol>
|
|
<symbol id="i-unlock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/></symbol>
|
|
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></symbol>
|
|
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5v14"/></symbol>
|
|
<symbol id="i-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></symbol>
|
|
<symbol id="i-star" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></symbol>
|
|
<symbol id="i-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></symbol>
|
|
<symbol id="i-eye" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></symbol>
|
|
<symbol id="i-eye-off" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.88 9.88a3 3 0 1 0 4.24 4.24M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61M2 2l20 20"/></symbol>
|
|
<symbol id="i-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></symbol>
|
|
<symbol id="i-edit" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></symbol>
|
|
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></symbol>
|
|
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z"/></symbol>
|
|
<symbol id="i-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></symbol>
|
|
<symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79Z"/></symbol>
|
|
<symbol id="i-globe" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10Z"/></symbol>
|
|
<symbol id="i-dice" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8" cy="8" r="1.2" fill="currentColor"/><circle cx="16" cy="8" r="1.2" fill="currentColor"/><circle cx="12" cy="12" r="1.2" fill="currentColor"/><circle cx="8" cy="16" r="1.2" fill="currentColor"/><circle cx="16" cy="16" r="1.2" fill="currentColor"/></symbol>
|
|
<symbol id="i-tag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.59 13.41 13.42 20.58a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82Z"/><line x1="7" y1="7" x2="7.01" y2="7"/></symbol>
|
|
<symbol id="i-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></symbol>
|
|
<symbol id="i-log-in" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></symbol>
|
|
<symbol id="i-rotate-ccw" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/></symbol>
|
|
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></symbol>
|
|
<symbol id="i-command" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 3a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3H6a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3V6a3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3h12a3 3 0 0 0 3-3 3 3 0 0 0-3-3Z"/></symbol>
|
|
<symbol id="i-log-out" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></symbol>
|
|
<symbol id="i-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></symbol>
|
|
<symbol id="i-list" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></symbol>
|
|
<symbol id="i-more" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1.4" fill="currentColor"/><circle cx="12" cy="12" r="1.4" fill="currentColor"/><circle cx="12" cy="19" r="1.4" fill="currentColor"/></symbol>
|
|
<symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol>
|
|
<symbol id="i-empty-vault" viewBox="0 0 120 120" fill="none">
|
|
<circle cx="60" cy="60" r="50" fill="var(--accent-soft)"/>
|
|
<rect x="40" y="50" width="40" height="35" rx="4" fill="var(--bg-elev)" stroke="var(--accent)" stroke-width="2"/>
|
|
<path d="M48 50V40a12 12 0 0 1 24 0v10" fill="none" stroke="var(--accent)" stroke-width="2" stroke-linecap="round"/>
|
|
<circle cx="60" cy="65" r="3" fill="var(--accent)"/>
|
|
<line x1="60" y1="68" x2="60" y2="74" stroke="var(--accent)" stroke-width="2" stroke-linecap="round"/>
|
|
</symbol>
|
|
<symbol id="i-empty-trash" viewBox="0 0 120 120" fill="none">
|
|
<circle cx="60" cy="60" r="50" fill="var(--accent-soft)"/>
|
|
<path d="M40 45h40M48 45v-5a3 3 0 0 1 3-3h18a3 3 0 0 1 3 3v5M44 45v35a4 4 0 0 0 4 4h24a4 4 0 0 0 4-4V45" stroke="var(--accent)" stroke-width="2" stroke-linecap="round" fill="none"/>
|
|
<path d="M54 55v22M66 55v22" stroke="var(--accent)" stroke-width="2" stroke-linecap="round"/>
|
|
</symbol>
|
|
<symbol id="i-empty-search" viewBox="0 0 120 120" fill="none">
|
|
<circle cx="60" cy="60" r="50" fill="var(--accent-soft)"/>
|
|
<circle cx="55" cy="55" r="14" stroke="var(--accent)" stroke-width="2.5" fill="none"/>
|
|
<path d="m66 66 11 11" stroke="var(--accent)" stroke-width="2.5" stroke-linecap="round"/>
|
|
</symbol>
|
|
</svg>
|
|
|
|
<!-- ============================================================ -->
|
|
<!-- TOAST CONTAINER -->
|
|
<!-- ============================================================ -->
|
|
<div id="toastContainer" class="toast-container"></div>
|
|
|
|
<!-- ============================================================ -->
|
|
<!-- IDLE WARNING (auto-lock countdown) -->
|
|
<!-- ============================================================ -->
|
|
<div id="idleWarning" class="idle-warning is-hidden">
|
|
<svg class="idle-icon"><use href="#i-lock"/></svg>
|
|
<h3>Auto-lock</h3>
|
|
<p>Vault locks in <b id="idleCountdown">30</b>s due to inactivity</p>
|
|
<button class="btn btn-primary btn-block" id="idleStayBtn">Stay unlocked</button>
|
|
</div>
|
|
|
|
<!-- ============================================================ -->
|
|
<!-- AUTH SCREEN (login + register) -->
|
|
<!-- ============================================================ -->
|
|
<section id="authScreen" class="auth-screen">
|
|
<div class="auth-card">
|
|
<div class="auth-brand">
|
|
<svg class="auth-brand-icon"><use href="#i-lock"/></svg>
|
|
<h1>Vault</h1>
|
|
<p>Your passwords, encrypted locally.</p>
|
|
</div>
|
|
|
|
<div class="auth-tabs">
|
|
<button class="auth-tab is-active" data-tab="login">Login</button>
|
|
<button class="auth-tab" data-tab="register">Register</button>
|
|
</div>
|
|
|
|
<div id="authHint" class="auth-hint"></div>
|
|
|
|
<form id="loginForm" class="auth-form" autocomplete="off">
|
|
<label class="field">
|
|
<span>Username</span>
|
|
<input id="loginUsername" type="text" required>
|
|
</label>
|
|
<label class="field">
|
|
<span>Master password</span>
|
|
<input id="loginPassword" type="password" required>
|
|
</label>
|
|
<button id="loginBtn" type="submit" class="btn btn-primary btn-block">
|
|
<svg><use href="#i-unlock"/></svg>
|
|
Unlock
|
|
</button>
|
|
<button id="passkeyBtn" type="button" class="btn btn-ghost btn-block">
|
|
<svg><use href="#i-lock"/></svg>
|
|
Use a passkey
|
|
</button>
|
|
</form>
|
|
|
|
<form id="registerForm" class="auth-form is-hidden" autocomplete="off">
|
|
<label class="field">
|
|
<span>Username <span class="hint">min 3 chars</span></span>
|
|
<input id="regUsername" type="text" required minlength="3">
|
|
</label>
|
|
<label class="field">
|
|
<span>Master password <span class="hint">min 8 chars</span></span>
|
|
<input id="regPassword" type="password" required minlength="8">
|
|
<div class="strength-bar" id="regStrengthBar"></div>
|
|
</label>
|
|
<button id="registerBtn" type="submit" class="btn btn-primary btn-block">
|
|
<svg><use href="#i-plus"/></svg>
|
|
Create vault
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ============================================================ -->
|
|
<!-- APP SHELL (sidebar + main + slide-over) -->
|
|
<!-- ============================================================ -->
|
|
<div id="appShell" class="app-shell is-hidden">
|
|
|
|
<!-- ---- SIDEBAR ----------------------------------------- -->
|
|
<aside class="sidebar">
|
|
<div class="sidebar-brand">
|
|
<svg><use href="#i-lock"/></svg>
|
|
<span>Vault</span>
|
|
</div>
|
|
|
|
<nav class="sidebar-nav">
|
|
<button class="nav-item is-active" data-view="all">
|
|
<svg><use href="#i-globe"/></svg>
|
|
<span>All items</span>
|
|
<span class="nav-count" id="countAll">0</span>
|
|
</button>
|
|
<button class="nav-item" data-view="favorites">
|
|
<svg><use href="#i-star"/></svg>
|
|
<span>Favorites</span>
|
|
<span class="nav-count" id="countFav">0</span>
|
|
</button>
|
|
</nav>
|
|
|
|
<div class="sidebar-section">
|
|
<div class="sidebar-section-header">
|
|
<span>Folders</span>
|
|
<button class="icon-btn icon-btn-sm" id="addFolderBtn" title="New folder">
|
|
<svg><use href="#i-plus"/></svg>
|
|
</button>
|
|
</div>
|
|
<nav class="sidebar-nav" id="foldersList"></nav>
|
|
</div>
|
|
|
|
<div class="sidebar-section">
|
|
<div class="sidebar-section-header"><span>Tags</span></div>
|
|
<nav class="sidebar-nav" id="tagsList"></nav>
|
|
</div>
|
|
|
|
<div class="sidebar-section">
|
|
<div class="sidebar-section-header"><span>Tools</span></div>
|
|
<nav class="sidebar-nav">
|
|
<button class="nav-item" id="sidebarGenBtn">
|
|
<svg><use href="#i-dice"/></svg>
|
|
<span>Generator</span>
|
|
</button>
|
|
<button class="nav-item" id="sidebarImportBtn">
|
|
<svg><use href="#i-log-in"/></svg>
|
|
<span>Import vault</span>
|
|
</button>
|
|
<button class="nav-item" id="sidebarExportBtn">
|
|
<svg><use href="#i-log-out"/></svg>
|
|
<span>Export vault</span>
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<div class="sidebar-bottom">
|
|
<button class="nav-item" data-view="trash">
|
|
<svg><use href="#i-trash"/></svg>
|
|
<span>Trash</span>
|
|
<span class="nav-count" id="countTrash">0</span>
|
|
</button>
|
|
<button class="nav-item" id="settingsBtn">
|
|
<svg><use href="#i-settings"/></svg>
|
|
<span>Settings</span>
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- ---- MAIN -------------------------------------------- -->
|
|
<main class="main">
|
|
<header class="topbar">
|
|
<div class="search">
|
|
<svg><use href="#i-search"/></svg>
|
|
<input id="searchInput" type="search" placeholder="Search…" autocomplete="off">
|
|
<kbd>Ctrl+K</kbd>
|
|
</div>
|
|
<div class="topbar-actions">
|
|
<div class="view-toggle">
|
|
<button class="view-btn is-active" id="viewCardsBtn" data-view="cards" title="Cards view">
|
|
<svg><use href="#i-grid"/></svg>
|
|
</button>
|
|
<button class="view-btn" id="viewListBtn" data-view="list" title="List view">
|
|
<svg><use href="#i-list"/></svg>
|
|
</button>
|
|
</div>
|
|
<button class="icon-btn" id="themeBtn" title="Toggle theme">
|
|
<svg class="theme-icon theme-icon-dark"><use href="#i-sun"/></svg>
|
|
<svg class="theme-icon theme-icon-light"><use href="#i-moon"/></svg>
|
|
</button>
|
|
<button class="btn btn-primary btn-sm" id="newEntryBtn">
|
|
<svg><use href="#i-plus"/></svg>
|
|
New
|
|
</button>
|
|
<div class="user-menu">
|
|
<button class="user-chip" id="userChip">
|
|
<span id="userName">user</span>
|
|
</button>
|
|
<div class="user-dropdown is-hidden" id="userDropdown">
|
|
<button class="dropdown-item" id="lockBtn">
|
|
<svg><use href="#i-lock"/></svg> Lock vault
|
|
</button>
|
|
<button class="dropdown-item" id="logoutBtn">
|
|
<svg><use href="#i-log-out"/></svg> Sign out
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="content">
|
|
<div class="content-header">
|
|
<h2 id="contentTitle">All items</h2>
|
|
<span class="content-meta" id="contentMeta">0 items</span>
|
|
</div>
|
|
|
|
<!-- Entry grid -->
|
|
<div id="entryGrid" class="entry-grid"></div>
|
|
|
|
<!-- Empty state — illustration + message are filled dynamically -->
|
|
<div id="emptyState" class="empty-state is-hidden">
|
|
<svg class="empty-illustration" id="emptyIllustration"><use href="#i-empty-vault"/></svg>
|
|
<h3 id="emptyTitle">Nothing here yet</h3>
|
|
<p id="emptyMessage">Click <b>+ New</b> to add your first password.</p>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- ---- SLIDE-OVER DETAIL ------------------------------- -->
|
|
<aside class="slideover" id="slideover">
|
|
<header class="slideover-header">
|
|
<h3 id="slideoverTitle">Entry</h3>
|
|
<button class="icon-btn" id="slideoverClose"><svg><use href="#i-x"/></svg></button>
|
|
</header>
|
|
<div class="slideover-body" id="slideoverBody"></div>
|
|
</aside>
|
|
|
|
<!-- ---- SETTINGS SLIDE-OVER ----------------------------- -->
|
|
<aside class="slideover" id="settingsPanel">
|
|
<header class="slideover-header">
|
|
<h3>Settings</h3>
|
|
<button class="icon-btn" id="settingsClose"><svg><use href="#i-x"/></svg></button>
|
|
</header>
|
|
<div class="slideover-body">
|
|
<div class="slideover-field">
|
|
<div class="slideover-field-label">Appearance</div>
|
|
<div class="setting-row">
|
|
<span>Theme</span>
|
|
<select id="settingTheme">
|
|
<option value="dark">Dark</option>
|
|
<option value="light">Light</option>
|
|
</select>
|
|
</div>
|
|
<div class="setting-row">
|
|
<span>Compact action menu (⋯)</span>
|
|
<label class="toggle">
|
|
<input type="checkbox" id="settingCompact">
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="setting-row">
|
|
<span>Mask usernames (ab***)</span>
|
|
<label class="toggle">
|
|
<input type="checkbox" id="settingMaskUser">
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="slideover-field">
|
|
<div class="slideover-field-label">Security</div>
|
|
<div class="setting-row">
|
|
<span>Auto-lock after (minutes)</span>
|
|
<select id="settingAutoLock">
|
|
<option value="0">Never</option>
|
|
<option value="1">1</option>
|
|
<option value="5">5</option>
|
|
<option value="15">15</option>
|
|
<option value="30">30</option>
|
|
<option value="60">60</option>
|
|
</select>
|
|
</div>
|
|
<div class="setting-row">
|
|
<span>Ask before moving to trash</span>
|
|
<label class="toggle">
|
|
<input type="checkbox" id="settingAskDelete">
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
</div>
|
|
<div class="setting-row">
|
|
<span>
|
|
Check passwords against breach database (HIBP)
|
|
<small class="setting-hint">Only the first 5 chars of the SHA-1 are sent — your passwords never leave the machine.</small>
|
|
</span>
|
|
<label class="toggle">
|
|
<input type="checkbox" id="settingHIBP">
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="slideover-field">
|
|
<div class="slideover-field-label">Clipboard privacy</div>
|
|
<p style="font-size:12px;color:var(--text-dim);margin:0 0 8px;line-height:1.5">
|
|
Windows keeps copied passwords in <b>clipboard history (Win+V)</b>.
|
|
To prevent that, disable history in <i>Settings → System → Clipboard</i>.
|
|
</p>
|
|
<button class="btn btn-ghost btn-sm" id="openClipboardSettings">Why?</button>
|
|
</div>
|
|
|
|
<div class="slideover-field">
|
|
<div class="slideover-field-label">Export</div>
|
|
<p style="font-size:12px;color:var(--text-dim);margin:0 0 8px;line-height:1.5">
|
|
Download all your passwords as JSON. Requires your master password — file is plaintext.
|
|
</p>
|
|
<button class="btn btn-ghost btn-sm" id="exportBtn">
|
|
<svg><use href="#i-log-out"/></svg> Export vault
|
|
</button>
|
|
</div>
|
|
|
|
<div class="slideover-field">
|
|
<div class="slideover-field-label">Import</div>
|
|
<p style="font-size:12px;color:var(--text-dim);margin:0 0 8px;line-height:1.5">
|
|
Import a vault from a JSON export (this app) or a CSV file
|
|
(Bitwarden, KeePass, Chrome, 1Password…). Entries are added
|
|
to your current vault — duplicates are NOT removed.
|
|
</p>
|
|
<button class="btn btn-ghost btn-sm" id="importBtn">
|
|
<svg><use href="#i-log-in"/></svg> Import vault
|
|
</button>
|
|
</div>
|
|
|
|
<div class="slideover-field">
|
|
<div class="slideover-field-label">Account</div>
|
|
<p style="font-size:12px;color:var(--text-dim);margin:0 0 8px">
|
|
Signed in as <b id="settingUser"></b>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- ============================================================ -->
|
|
<!-- MODAL: Re-auth (for export and other sensitive ops) -->
|
|
<!-- ============================================================ -->
|
|
<div id="reauthModal" class="modal is-hidden" role="dialog" aria-modal="true">
|
|
<div class="modal-backdrop" data-close></div>
|
|
<div class="modal-panel modal-panel-sm">
|
|
<header class="modal-header">
|
|
<h3 id="reauthTitle">Confirm master password</h3>
|
|
<button class="icon-btn" data-close><svg><use href="#i-x"/></svg></button>
|
|
</header>
|
|
<form id="reauthForm" class="modal-body" autocomplete="off">
|
|
<p style="margin:0 0 12px;color:var(--text-dim);font-size:13px" id="reauthMessage">
|
|
This action requires your master password.
|
|
</p>
|
|
<label class="field">
|
|
<span>Master password</span>
|
|
<input id="reauthPassword" type="password" required autofocus>
|
|
</label>
|
|
</form>
|
|
<footer class="modal-footer">
|
|
<button type="button" class="btn btn-ghost" data-close>Cancel</button>
|
|
<button type="submit" form="reauthForm" class="btn btn-primary" id="reauthConfirm">
|
|
<svg><use href="#i-check"/></svg> Confirm
|
|
</button>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- ============================================================ -->
|
|
<!-- MODAL: New / Edit Entry -->
|
|
<!-- ============================================================ -->
|
|
<div id="entryModal" class="modal is-hidden" role="dialog" aria-modal="true">
|
|
<div class="modal-backdrop" data-close></div>
|
|
<div class="modal-panel">
|
|
<header class="modal-header">
|
|
<h3 id="entryModalTitle">New entry</h3>
|
|
<button class="icon-btn" data-close><svg><use href="#i-x"/></svg></button>
|
|
</header>
|
|
<form id="entryForm" class="modal-body" autocomplete="off">
|
|
<input type="hidden" id="entryId">
|
|
<label class="field">
|
|
<span>Website / App</span>
|
|
<input id="entrySite" type="text" required placeholder="example.com">
|
|
</label>
|
|
<label class="field">
|
|
<span>Username / Email</span>
|
|
<input id="entryUsername" type="text" placeholder="me@example.com">
|
|
</label>
|
|
<label class="field">
|
|
<span>Password</span>
|
|
<div class="input-with-action">
|
|
<input id="entryPassword" type="password" required placeholder="••••••••">
|
|
<button type="button" class="icon-btn" id="entryPwToggle"><svg><use href="#i-eye"/></svg></button>
|
|
<button type="button" class="icon-btn" id="entryPwGen" title="Generate"><svg><use href="#i-dice"/></svg></button>
|
|
</div>
|
|
<div class="strength-bar" id="entryStrengthBar"></div>
|
|
</label>
|
|
<label class="field">
|
|
<span>Folder</span>
|
|
<select id="entryFolder"></select>
|
|
</label>
|
|
<label class="field">
|
|
<span>Tags <span class="hint">enter to add</span></span>
|
|
<div class="chip-input" id="entryTagsInput">
|
|
<input id="entryTagsField" type="text" placeholder="add a tag…" autocomplete="off">
|
|
</div>
|
|
<input type="hidden" id="entryTags">
|
|
</label>
|
|
</form>
|
|
<footer class="modal-footer">
|
|
<button class="btn btn-ghost" data-close>Cancel</button>
|
|
<button class="btn btn-primary" id="entrySaveBtn">
|
|
<svg><use href="#i-check"/></svg> Save
|
|
</button>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============================================================ -->
|
|
<!-- MODAL: Password Generator -->
|
|
<!-- ============================================================ -->
|
|
<div id="genModal" class="modal is-hidden" role="dialog" aria-modal="true">
|
|
<div class="modal-backdrop" data-close></div>
|
|
<div class="modal-panel modal-panel-sm">
|
|
<header class="modal-header">
|
|
<h3>Password generator</h3>
|
|
<button class="icon-btn" data-close><svg><use href="#i-x"/></svg></button>
|
|
</header>
|
|
<div class="modal-body">
|
|
<div class="gen-preview" id="genPreview">click regenerate</div>
|
|
<label class="field">
|
|
<span>Length <b id="genLenLabel">16</b></span>
|
|
<input id="genLen" type="range" min="6" max="128" value="16">
|
|
</label>
|
|
<div class="gen-options">
|
|
<label class="check"><input type="checkbox" id="genUpper" checked><span>A-Z</span></label>
|
|
<label class="check"><input type="checkbox" id="genLower" checked><span>a-z</span></label>
|
|
<label class="check"><input type="checkbox" id="genNum" checked><span>0-9</span></label>
|
|
<label class="check"><input type="checkbox" id="genSym" checked><span>!@#</span></label>
|
|
</div>
|
|
</div>
|
|
<footer class="modal-footer">
|
|
<button class="btn btn-ghost" id="genRegen"><svg><use href="#i-rotate-ccw"/></svg> Regenerate</button>
|
|
<button class="btn btn-ghost" id="genCopy"><svg><use href="#i-copy"/></svg> Copy</button>
|
|
<button class="btn btn-primary" id="genUse"><svg><use href="#i-check"/></svg> Use</button>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============================================================ -->
|
|
<!-- CONFIRM / PROMPT MODAL (replaces native confirm/prompt) -->
|
|
<!-- ============================================================ -->
|
|
<div id="confirmModal" class="modal is-hidden" role="dialog" aria-modal="true">
|
|
<div class="modal-backdrop" data-confirm-cancel></div>
|
|
<div class="modal-panel modal-panel-sm">
|
|
<header class="modal-header">
|
|
<h3 id="confirmTitle">Confirm</h3>
|
|
<button class="icon-btn" data-confirm-cancel><svg><use href="#i-x"/></svg></button>
|
|
</header>
|
|
<form id="confirmForm" class="modal-body">
|
|
<p id="confirmMessage" style="margin:0 0 12px;color:var(--text-dim);font-size:13px;line-height:1.5">
|
|
Are you sure?
|
|
</p>
|
|
<label class="field is-hidden" id="confirmInputField">
|
|
<input id="confirmInput" type="text" autocomplete="off">
|
|
</label>
|
|
</form>
|
|
<footer class="modal-footer">
|
|
<button type="button" class="btn btn-ghost" data-confirm-cancel id="confirmCancelBtn">Cancel</button>
|
|
<button type="submit" form="confirmForm" class="btn btn-primary" id="confirmOkBtn">
|
|
<svg><use href="#i-check"/></svg> Confirm
|
|
</button>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============================================================ -->
|
|
<!-- COMMAND PALETTE (Cmd+K) — phase 3, shell only for now -->
|
|
<!-- ============================================================ -->
|
|
<div id="cmdPalette" class="cmd-palette is-hidden" role="dialog" aria-modal="true">
|
|
<div class="modal-backdrop" data-close></div>
|
|
<div class="cmd-panel">
|
|
<div class="cmd-input">
|
|
<svg><use href="#i-command"/></svg>
|
|
<input id="cmdInput" type="text" placeholder="Type a command or search…">
|
|
</div>
|
|
<div class="cmd-results" id="cmdResults"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="js/app.js"></script>
|
|
</body>
|
|
</html>
|