Files
Password-Manager/index.html
T
Zaki 4b15811221 feat(import): JSON / CSV vault import with heuristic column mapping
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.
2026-05-23 05:30:08 +01:00

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>