04/20/2026 enhance the extension 2

This commit is contained in:
2026-04-20 17:51:01 -04:00
parent df8628e71c
commit 27e1c16851
5 changed files with 1488 additions and 1404 deletions
+29 -7
View File
@@ -10,20 +10,35 @@
// ── Idle lock ───────────────────────────────────────────────────────────────── // ── Idle lock ─────────────────────────────────────────────────────────────────
// Lock after 10 minutes of system idle or when the screen is locked. // Default: 10 minutes. User can change via the Account view in the popup.
const IDLE_LOCK_SECONDS = 600; const DEFAULT_IDLE_LOCK_SECONDS = 600;
const IDLE_TIMEOUT_KEY = 'idle_lock_seconds';
chrome.idle.setDetectionInterval(IDLE_LOCK_SECONDS); /** Apply the idle detection interval, reading the user's saved preference. */
async function applyIdleInterval() {
const stored = await chrome.storage.local.get(IDLE_TIMEOUT_KEY);
const seconds = stored[IDLE_TIMEOUT_KEY] ?? DEFAULT_IDLE_LOCK_SECONDS;
if (seconds === 0) {
// "Never" — unregister by setting to Chrome's maximum (the API requires a value).
chrome.idle.setDetectionInterval(3600);
} else {
chrome.idle.setDetectionInterval(Math.max(15, seconds));
}
console.log('[PassKeeper] Idle lock interval:', seconds === 0 ? 'never' : seconds + 's');
}
// Apply on every SW startup (SW can be killed and restarted at any time).
applyIdleInterval();
chrome.idle.onStateChanged.addListener(async (newState) => { chrome.idle.onStateChanged.addListener(async (newState) => {
if (newState === 'idle' || newState === 'locked') { if (newState === 'idle' || newState === 'locked') {
// Check if the user set "Never" before locking.
const stored = await chrome.storage.local.get(IDLE_TIMEOUT_KEY);
if ((stored[IDLE_TIMEOUT_KEY] ?? DEFAULT_IDLE_LOCK_SECONDS) === 0) return;
console.log('[PassKeeper] System', newState, '— locking vault.'); console.log('[PassKeeper] System', newState, '— locking vault.');
// Clear the session (vault key, access token, vault items) so the popup
// requires master password re-entry on next open.
await chrome.storage.session.clear(); await chrome.storage.session.clear();
// Clear the content-script vault cache so suggestions stop showing.
await chrome.storage.local.remove('vault_items_cs'); await chrome.storage.local.remove('vault_items_cs');
// Clear all badge text — vault is now locked.
const tabs = await chrome.tabs.query({}); const tabs = await chrome.tabs.query({});
tabs.forEach(tab => { tabs.forEach(tab => {
if (tab.id) chrome.action.setBadgeText({ text: '', tabId: tab.id }); if (tab.id) chrome.action.setBadgeText({ text: '', tabId: tab.id });
@@ -88,6 +103,13 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
// Popup updated the idle lock timeout → re-apply immediately.
if (msg.type === 'SET_IDLE_TIMEOUT') {
applyIdleInterval();
sendResponse({ ok: true });
return false;
}
// Content script requests opening the vault tab (from suggestion dropdown). // Content script requests opening the vault tab (from suggestion dropdown).
if (msg.type === 'OPEN_VAULT') { if (msg.type === 'OPEN_VAULT') {
chrome.tabs.create({ url: 'https://pwkeeper.ngodanguyen.tech/vault' }); chrome.tabs.create({ url: 'https://pwkeeper.ngodanguyen.tech/vault' });
File diff suppressed because it is too large Load Diff
+316 -20
View File
@@ -9,8 +9,7 @@
body { body {
width: 320px; width: 320px;
font-family: font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 13px; font-size: 13px;
color: #1a1a2e; color: #1a1a2e;
background: #fff; background: #fff;
@@ -22,6 +21,7 @@ body {
flex-direction: column; flex-direction: column;
min-height: 0; min-height: 0;
} }
.view.hidden { .view.hidden {
display: none; display: none;
} }
@@ -35,10 +35,12 @@ body {
background: linear-gradient(135deg, #c0392b, #922b21); background: linear-gradient(135deg, #c0392b, #922b21);
color: #fff; color: #fff;
} }
.login-logo { .login-logo {
font-size: 36px; font-size: 36px;
margin-bottom: 8px; margin-bottom: 8px;
} }
.login-header h1 { .login-header h1 {
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
@@ -48,6 +50,7 @@ body {
.login-body { .login-body {
padding: 18px 20px 20px; padding: 18px 20px 20px;
} }
.pk-hint { .pk-hint {
font-size: 12px; font-size: 12px;
color: #666; color: #666;
@@ -58,6 +61,7 @@ body {
.form-group { .form-group {
margin-bottom: 11px; margin-bottom: 11px;
} }
.form-group label { .form-group label {
display: block; display: block;
font-size: 11px; font-size: 11px;
@@ -65,6 +69,7 @@ body {
font-weight: 500; font-weight: 500;
margin-bottom: 3px; margin-bottom: 3px;
} }
.form-group input { .form-group input {
width: 100%; width: 100%;
padding: 8px 11px; padding: 8px 11px;
@@ -74,10 +79,9 @@ body {
outline: none; outline: none;
background: #fff; background: #fff;
color: #1a1a2e; color: #1a1a2e;
transition: transition: border-color 0.15s, box-shadow 0.15s;
border-color 0.15s,
box-shadow 0.15s;
} }
.form-group input:focus { .form-group input:focus {
border-color: #c0392b; border-color: #c0392b;
box-shadow: 0 0 0 2px rgba(192, 57, 43, 0.12); box-shadow: 0 0 0 2px rgba(192, 57, 43, 0.12);
@@ -98,13 +102,16 @@ body {
margin-top: 10px; margin-top: 10px;
transition: background 0.15s; transition: background 0.15s;
} }
.btn-primary:hover { .btn-primary:hover {
background: #a93226; background: #a93226;
} }
.btn-primary:disabled { .btn-primary:disabled {
background: #e8a49e; background: #e8a49e;
cursor: default; cursor: default;
} }
.btn-sm { .btn-sm {
width: auto; width: auto;
padding: 6px 16px; padding: 6px 16px;
@@ -125,6 +132,7 @@ body {
margin-top: 6px; margin-top: 6px;
transition: background 0.15s; transition: background 0.15s;
} }
.btn-ghost:hover { .btn-ghost:hover {
background: #fdf2f1; background: #fdf2f1;
} }
@@ -148,10 +156,12 @@ body {
padding: 6px 10px; padding: 6px 10px;
gap: 7px; gap: 7px;
} }
.search-icon { .search-icon {
flex-shrink: 0; flex-shrink: 0;
opacity: 0.6; opacity: 0.6;
} }
#vault-search { #vault-search {
flex: 1; flex: 1;
border: none; border: none;
@@ -160,6 +170,7 @@ body {
outline: none; outline: none;
color: #1a1a2e; color: #1a1a2e;
} }
#vault-search::placeholder { #vault-search::placeholder {
color: #9ca3af; color: #9ca3af;
} }
@@ -181,6 +192,7 @@ body {
transition: background 0.15s; transition: background 0.15s;
flex-shrink: 0; flex-shrink: 0;
} }
.btn-vault-link:hover { .btn-vault-link:hover {
background: #2d2d4e; background: #2d2d4e;
} }
@@ -201,6 +213,7 @@ body {
flex-shrink: 0; flex-shrink: 0;
transition: background 0.15s; transition: background 0.15s;
} }
.btn-add:hover { .btn-add:hover {
background: #e5e7eb; background: #e5e7eb;
} }
@@ -215,6 +228,7 @@ body {
overflow-x: auto; overflow-x: auto;
scrollbar-width: none; scrollbar-width: none;
} }
.vault-tabs::-webkit-scrollbar { .vault-tabs::-webkit-scrollbar {
display: none; display: none;
} }
@@ -229,14 +243,14 @@ body {
cursor: pointer; cursor: pointer;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
white-space: nowrap; white-space: nowrap;
transition: transition: color 0.15s, border-color 0.15s;
color 0.15s,
border-color 0.15s;
margin-bottom: -1px; margin-bottom: -1px;
} }
.tab-btn:hover { .tab-btn:hover {
color: #1a1a2e; color: #1a1a2e;
} }
.tab-btn.active { .tab-btn.active {
color: #1a1a2e; color: #1a1a2e;
border-bottom-color: #1a1a2e; border-bottom-color: #1a1a2e;
@@ -250,9 +264,11 @@ body {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: #e5e7eb transparent; scrollbar-color: #e5e7eb transparent;
} }
.vault-list::-webkit-scrollbar { .vault-list::-webkit-scrollbar {
width: 4px; width: 4px;
} }
.vault-list::-webkit-scrollbar-thumb { .vault-list::-webkit-scrollbar-thumb {
background: #e5e7eb; background: #e5e7eb;
border-radius: 2px; border-radius: 2px;
@@ -267,9 +283,11 @@ body {
cursor: default; cursor: default;
transition: background 0.1s; transition: background 0.1s;
} }
.vault-item:hover { .vault-item:hover {
background: #f9fafb; background: #f9fafb;
} }
.vault-item:last-child { .vault-item:last-child {
border-bottom: none; border-bottom: none;
} }
@@ -286,15 +304,19 @@ body {
flex-shrink: 0; flex-shrink: 0;
overflow: hidden; overflow: hidden;
} }
.item-avatar.color-red { .item-avatar.color-red {
background: #7f1d1d; background: #7f1d1d;
} }
.item-avatar.color-green { .item-avatar.color-green {
background: #14532d; background: #14532d;
} }
.item-avatar.color-purple { .item-avatar.color-purple {
background: #3b0764; background: #3b0764;
} }
.item-avatar.color-teal { .item-avatar.color-teal {
background: #134e4a; background: #134e4a;
} }
@@ -309,6 +331,7 @@ body {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
} }
.item-site { .item-site {
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
@@ -317,6 +340,7 @@ body {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.item-name { .item-name {
font-size: 11px; font-size: 11px;
color: #9ca3af; color: #9ca3af;
@@ -343,14 +367,14 @@ body {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: transition: background 0.12s, color 0.12s;
background 0.12s,
color 0.12s;
} }
.btn-item-action:hover { .btn-item-action:hover {
background: #f3f4f6; background: #f3f4f6;
color: #1a1a2e; color: #1a1a2e;
} }
.btn-item-action svg { .btn-item-action svg {
width: 16px; width: 16px;
height: 16px; height: 16px;
@@ -378,6 +402,7 @@ body {
padding: 7px 10px; padding: 7px 10px;
margin-bottom: 10px; margin-bottom: 10px;
} }
.pk-error.hidden { .pk-error.hidden {
display: none; display: none;
} }
@@ -389,6 +414,7 @@ body {
padding: 28px 16px; padding: 28px 16px;
font-size: 13px; font-size: 13px;
} }
.pk-loading.hidden, .pk-loading.hidden,
.pk-empty.hidden { .pk-empty.hidden {
display: none; display: none;
@@ -400,9 +426,11 @@ body {
border-top: 1px solid #f0f0f0; border-top: 1px solid #f0f0f0;
background: #fff; background: #fff;
} }
.bottom-nav.hidden { .bottom-nav.hidden {
display: none; display: none;
} }
.nav-btn { .nav-btn {
flex: 1; flex: 1;
display: flex; display: flex;
@@ -419,9 +447,11 @@ body {
font-weight: 500; font-weight: 500;
transition: color 0.15s; transition: color 0.15s;
} }
.nav-btn:hover { .nav-btn:hover {
color: #374151; color: #374151;
} }
.nav-btn.active { .nav-btn.active {
color: #1a1a2e; color: #1a1a2e;
} }
@@ -432,24 +462,32 @@ body {
inset: 0; inset: 0;
background: rgba(0, 0, 0, 0.45); background: rgba(0, 0, 0, 0.45);
display: flex; display: flex;
align-items: flex-start; /* anchor to top so card is never cut off */ align-items: flex-start;
/* anchor to top so card is never cut off */
justify-content: center; justify-content: center;
z-index: 9999; z-index: 9999;
overflow-y: auto; /* allow scrolling if popup height is small */ overflow-y: auto;
padding: 16px 14px; /* breathing room from top/bottom edges */ /* allow scrolling if popup height is small */
padding: 16px 14px;
/* breathing room from top/bottom edges */
} }
.save-overlay.hidden { .save-overlay.hidden {
display: none; display: none;
} }
.save-modal { .save-modal {
background: #fff; background: #fff;
border-radius: 12px; border-radius: 12px;
padding: 18px 16px 14px; padding: 18px 16px 14px;
width: 100%; /* fill the overlay width minus its padding */ width: 100%;
/* fill the overlay width minus its padding */
max-width: 288px; max-width: 288px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.22); box-shadow: 0 12px 40px rgba(0, 0, 0, 0.22);
flex-shrink: 0; /* never crush the card */ flex-shrink: 0;
/* never crush the card */
} }
.save-modal-header { .save-modal-header {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -459,6 +497,7 @@ body {
color: #1a1a2e; color: #1a1a2e;
margin-bottom: 5px; margin-bottom: 5px;
} }
.save-modal-sub { .save-modal-sub {
font-size: 11px; font-size: 11px;
color: #6b7280; color: #6b7280;
@@ -466,14 +505,17 @@ body {
word-break: break-all; word-break: break-all;
line-height: 1.4; line-height: 1.4;
} }
.save-modal-actions { .save-modal-actions {
display: flex; display: flex;
gap: 8px; gap: 8px;
margin-top: 10px; margin-top: 10px;
} }
.save-modal-actions .btn-primary { .save-modal-actions .btn-primary {
margin-top: 0; margin-top: 0;
} }
.save-modal-actions .btn-ghost { .save-modal-actions .btn-ghost {
margin-top: 0; margin-top: 0;
} }
@@ -489,11 +531,10 @@ body {
background: #fff; background: #fff;
outline: none; outline: none;
cursor: pointer; cursor: pointer;
transition: transition: border-color 0.15s, box-shadow 0.15s;
border-color 0.15s,
box-shadow 0.15s;
appearance: auto; appearance: auto;
} }
.save-folder-select:focus { .save-folder-select:focus {
border-color: #c0392b; border-color: #c0392b;
box-shadow: 0 0 0 2px rgba(192, 57, 43, 0.12); box-shadow: 0 0 0 2px rgba(192, 57, 43, 0.12);
@@ -505,31 +546,36 @@ body {
border-bottom: 2px solid #16a34a; border-bottom: 2px solid #16a34a;
padding: 12px 14px 10px; padding: 12px 14px 10px;
} }
.gen-suggestion-label { .gen-suggestion-label {
font-size: 10px; font-size: 10px;
color: #374151; color: #374151;
font-weight: 500; font-weight: 500;
margin-bottom: 4px; margin-bottom: 4px;
} }
.gen-suggestion-row { .gen-suggestion-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
.gen-output { .gen-output {
flex: 1; flex: 1;
font-family: "Courier New", Courier, monospace; font-family: 'Courier New', Courier, monospace;
font-size: 13px; font-size: 13px;
font-weight: 700; font-weight: 700;
color: #1a1a2e; color: #1a1a2e;
word-break: break-all; word-break: break-all;
min-width: 0; min-width: 0;
} }
.gen-suggestion-actions { .gen-suggestion-actions {
display: flex; display: flex;
gap: 4px; gap: 4px;
flex-shrink: 0; flex-shrink: 0;
} }
.gen-icon-btn { .gen-icon-btn {
background: none; background: none;
border: none; border: none;
@@ -542,6 +588,7 @@ body {
justify-content: center; justify-content: center;
transition: background 0.12s; transition: background 0.12s;
} }
.gen-icon-btn:hover { .gen-icon-btn:hover {
background: rgba(26, 115, 232, 0.1); background: rgba(26, 115, 232, 0.1);
} }
@@ -550,6 +597,7 @@ body {
padding: 6px 14px 2px; padding: 6px 14px 2px;
min-height: 22px; min-height: 22px;
} }
.gen-strength-label { .gen-strength-label {
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 700;
@@ -561,16 +609,19 @@ body {
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
} }
.gen-row { .gen-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
} }
.gen-label { .gen-label {
font-size: 12px; font-size: 12px;
color: #374151; color: #374151;
white-space: nowrap; white-space: nowrap;
} }
.gen-length-num { .gen-length-num {
width: 42px; width: 42px;
padding: 3px 5px; padding: 3px 5px;
@@ -581,15 +632,18 @@ body {
color: #1a1a2e; color: #1a1a2e;
outline: none; outline: none;
} }
.gen-length-num:focus { .gen-length-num:focus {
border-color: #1a73e8; border-color: #1a73e8;
} }
.gen-slider { .gen-slider {
width: 100%; width: 100%;
accent-color: #1a73e8; accent-color: #1a73e8;
cursor: pointer; cursor: pointer;
height: 4px; height: 4px;
} }
.gen-check { .gen-check {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -599,6 +653,7 @@ body {
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
} }
.gen-check input[type="checkbox"] { .gen-check input[type="checkbox"] {
width: 15px; width: 15px;
height: 15px; height: 15px;
@@ -606,3 +661,244 @@ body {
cursor: pointer; cursor: pointer;
flex-shrink: 0; flex-shrink: 0;
} }
/* ── Account view ────────────────────────────────────────────────── */
.acct-header {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 20px 14px;
background: linear-gradient(135deg, #1a1a2e, #2d2d4e);
color: #fff;
}
.acct-logo {
font-size: 28px;
margin-bottom: 6px;
}
.acct-header h2 {
font-size: 16px;
font-weight: 600;
letter-spacing: 0.3px;
}
.acct-body {
padding: 12px 16px 16px;
display: flex;
flex-direction: column;
gap: 0;
}
.acct-section {
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.acct-section:last-child {
border-bottom: none;
}
.acct-section-title {
font-size: 12px;
font-weight: 600;
color: #1a1a2e;
margin-bottom: 4px;
}
.acct-hint {
font-size: 11px;
color: #6b7280;
margin-bottom: 8px;
line-height: 1.4;
}
.acct-select {
width: 100%;
padding: 7px 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 12px;
color: #1a1a2e;
background: #fff;
outline: none;
cursor: pointer;
transition: border-color 0.15s;
}
.acct-select:focus {
border-color: #c0392b;
box-shadow: 0 0 0 2px rgba(192, 57, 43, 0.12);
}
.acct-saved {
font-size: 11px;
color: #16a34a;
font-weight: 600;
margin-top: 5px;
}
.acct-saved.hidden {
display: none;
}
.acct-link-btn {
display: block;
text-align: center;
text-decoration: none;
padding: 8px;
border-radius: 6px;
background: #1a1a2e;
color: #fff;
font-size: 12px;
font-weight: 600;
transition: background 0.15s;
}
.acct-link-btn:hover {
background: #2d2d4e;
}
.acct-section-danger .acct-section-title {
color: #b91c1c;
}
.acct-section-danger .btn-ghost {
color: #b91c1c;
border-color: #fca5a5;
font-size: 12px;
padding: 7px;
margin-top: 0;
}
.acct-section-danger .btn-ghost:hover {
background: #fef2f2;
}
/* ── Add Item view ───────────────────────────────────────────────── */
.add-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px 9px;
background: #fff;
border-bottom: 1px solid #f0f0f0;
}
.add-back-btn {
background: none;
border: none;
cursor: pointer;
color: #6b7280;
padding: 4px;
border-radius: 5px;
display: flex;
align-items: center;
transition: background 0.12s, color 0.12s;
}
.add-back-btn:hover {
background: #f3f4f6;
color: #1a1a2e;
}
.add-header-title {
font-size: 14px;
font-weight: 600;
color: #1a1a2e;
}
.add-body {
padding: 12px 14px 16px;
overflow-y: auto;
max-height: 380px;
scrollbar-width: thin;
scrollbar-color: #e5e7eb transparent;
}
.add-body::-webkit-scrollbar {
width: 4px;
}
.add-body::-webkit-scrollbar-thumb {
background: #e5e7eb;
border-radius: 2px;
}
.add-required {
color: #c0392b;
font-size: 10px;
}
.add-pw-row {
display: flex;
align-items: center;
gap: 4px;
}
.add-pw-input {
flex: 1;
padding: 8px 11px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 13px;
outline: none;
background: #fff;
color: #1a1a2e;
transition: border-color 0.15s, box-shadow 0.15s;
min-width: 0;
}
.add-pw-input:focus {
border-color: #c0392b;
box-shadow: 0 0 0 2px rgba(192, 57, 43, 0.12);
}
.add-icon-btn {
width: 32px;
height: 32px;
flex-shrink: 0;
background: #f3f4f6;
border: none;
border-radius: 6px;
cursor: pointer;
color: #6b7280;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.12s, color 0.12s;
}
.add-icon-btn:hover {
background: #e5e7eb;
color: #1a1a2e;
}
.add-gen-btn {
background: #1a1a2e;
color: #fff;
}
.add-gen-btn:hover {
background: #2d2d4e;
color: #fff;
}
.add-notes {
width: 100%;
padding: 8px 11px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 13px;
outline: none;
background: #fff;
color: #1a1a2e;
resize: none;
font-family: inherit;
transition: border-color 0.15s, box-shadow 0.15s;
}
.add-notes:focus {
border-color: #c0392b;
box-shadow: 0 0 0 2px rgba(192, 57, 43, 0.12);
}
+333 -411
View File
@@ -1,417 +1,339 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" /> <head>
<title>PassKeeper</title> <meta charset="UTF-8">
<link rel="stylesheet" href="popup.css" /> <title>PassKeeper</title>
</head> <link rel="stylesheet" href="popup.css">
<body> </head>
<div id="app">
<!-- ── Login view ──────────────────────────────────────────────────── --> <body>
<div id="view-login" class="view hidden"> <div id="app">
<div class="login-header">
<span class="login-logo">🔒</span> <!-- ── Login view ──────────────────────────────────────────────────── -->
<h1>PassKeeper</h1> <div id="view-login" class="view hidden">
</div> <div class="login-header">
<div class="login-body"> <span class="login-logo">🔒</span>
<p id="login-error" class="pk-error hidden"></p> <h1>PassKeeper</h1>
<div class="form-group">
<label for="login-email">Email</label>
<input
type="email"
id="login-email"
placeholder="you@example.com"
autocomplete="email"
/>
</div>
<div class="form-group">
<label for="login-password">Master Password</label>
<input
type="password"
id="login-password"
placeholder="Master password"
autocomplete="current-password"
/>
</div>
<button id="btn-login" class="btn-primary">Unlock Vault</button>
</div>
</div> </div>
<div class="login-body">
<!-- ── MFA view ────────────────────────────────────────────────────── --> <p id="login-error" class="pk-error hidden"></p>
<div id="view-mfa" class="view hidden"> <div class="form-group">
<div class="login-header"> <label for="login-email">Email</label>
<span class="login-logo">🔐</span> <input type="email" id="login-email" placeholder="you@example.com" autocomplete="email">
<h1>Two-Factor Auth</h1>
</div> </div>
<div class="login-body"> <div class="form-group">
<p class="pk-hint">Enter your 6-digit authenticator code.</p> <label for="login-password">Master Password</label>
<p id="mfa-error" class="pk-error hidden"></p> <input type="password" id="login-password" placeholder="Master password" autocomplete="current-password">
<div class="form-group">
<label for="mfa-code">Verification Code</label>
<input
type="text"
id="mfa-code"
inputmode="numeric"
maxlength="6"
placeholder="000000"
autocomplete="one-time-code"
/>
</div>
<button id="btn-mfa-verify" class="btn-primary">Verify</button>
<button id="btn-mfa-back" class="btn-ghost">← Back</button>
</div> </div>
<button id="btn-login" class="btn-primary">Unlock Vault</button>
</div> </div>
<!-- ── Unlock-only view (tokens from web app, need vault key) ─────── -->
<div id="view-unlock" class="view hidden">
<div class="login-header">
<span class="login-logo">🔓</span>
<h1>PassKeeper</h1>
<p style="font-size: 12px; opacity: 0.85; margin-top: 4px">
Signed in via web app
</p>
</div>
<div class="login-body">
<p class="pk-hint">Enter your master password to unlock the vault.</p>
<p id="unlock-error" class="pk-error hidden"></p>
<div class="form-group">
<label for="unlock-password">Master Password</label>
<input
type="password"
id="unlock-password"
placeholder="Master password"
autocomplete="current-password"
/>
</div>
<button id="btn-unlock" class="btn-primary">Unlock</button>
<button id="btn-unlock-signout" class="btn-ghost">Sign out</button>
</div>
</div>
<!-- ── Vault view ──────────────────────────────────────────────────── -->
<div id="view-vault" class="view hidden">
<!-- Top bar -->
<div class="vault-topbar">
<div class="search-wrap">
<svg
class="search-icon"
viewBox="0 0 20 20"
fill="none"
width="15"
height="15"
>
<circle
cx="8.5"
cy="8.5"
r="5.5"
stroke="#999"
stroke-width="1.6"
/>
<path
d="M13 13l3.5 3.5"
stroke="#999"
stroke-width="1.6"
stroke-linecap="round"
/>
</svg>
<input
type="text"
id="vault-search"
placeholder="Search your vault"
autocomplete="off"
/>
</div>
<a
id="btn-open-vault"
class="btn-vault-link"
title="Open vault"
target="_blank"
>
Vault
<svg viewBox="0 0 16 16" fill="none" width="12" height="12">
<path
d="M6 3H3a1 1 0 00-1 1v9a1 1 0 001 1h9a1 1 0 001-1v-3"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
<path
d="M9 2h5v5M14 2L8 8"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</a>
<button id="btn-add-item" class="btn-add" title="Add item">+</button>
</div>
<!-- Tabs -->
<div class="vault-tabs" id="vault-tabs">
<button class="tab-btn active" data-tab="relevant">
All relevant
</button>
<button class="tab-btn" data-tab="all">All items</button>
<button class="tab-btn" data-tab="recents">Recents</button>
</div>
<!-- List -->
<div id="vault-spinner" class="pk-loading hidden">Loading vault…</div>
<div id="vault-list" class="vault-list"></div>
<div id="vault-empty" class="pk-empty hidden">No items found.</div>
</div>
<!-- /view-vault -->
<!-- ── Generator view ─────────────────────────────────────────────── -->
<div id="view-generator" class="view hidden">
<div class="gen-suggestion">
<div class="gen-suggestion-label">Password suggestion</div>
<div class="gen-suggestion-row">
<span id="gen-output" class="gen-output"
>Click refresh to generate</span
>
<div class="gen-suggestion-actions">
<button
id="btn-gen-copy"
class="gen-icon-btn"
title="Copy password"
>
<svg viewBox="0 0 24 24" fill="none" width="18" height="18">
<rect
x="9"
y="9"
width="11"
height="11"
rx="2"
stroke="currentColor"
stroke-width="1.8"
/>
<path
d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"
stroke="currentColor"
stroke-width="1.8"
/>
</svg>
</button>
<button
id="btn-gen-refresh"
class="gen-icon-btn"
title="Generate new password"
>
<svg viewBox="0 0 24 24" fill="none" width="18" height="18">
<path
d="M4 4v5h5"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M20 20v-5h-5"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M20 9a8 8 0 00-14.93-2.07M4 15a8 8 0 0014.93 2.07"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
/>
</svg>
</button>
</div>
</div>
</div>
<div class="gen-strength-row">
<span id="gen-strength-label" class="gen-strength-label"></span>
</div>
<div class="gen-body">
<div class="gen-row">
<label for="gen-length" class="gen-label">Password length:</label>
<input
type="number"
id="gen-length-num"
class="gen-length-num"
value="16"
min="8"
max="64"
/>
<span class="gen-label">characters</span>
</div>
<input
type="range"
id="gen-length"
class="gen-slider"
min="8"
max="64"
value="16"
/>
<label class="gen-check">
<input type="checkbox" id="gen-lower" checked />
<span>Lowercase (abc)</span>
</label>
<label class="gen-check">
<input type="checkbox" id="gen-upper" checked />
<span>Uppercase (ABC)</span>
</label>
<label class="gen-check">
<input type="checkbox" id="gen-numbers" checked />
<span>Numbers (123)</span>
</label>
<label class="gen-check">
<input type="checkbox" id="gen-symbols" checked />
<span>Randomized symbols (!#$)</span>
</label>
</div>
</div>
<!-- /view-generator -->
<!-- ── Save-prompt modal overlay (shared across vault/generator views) -->
<div
id="save-prompt-overlay"
class="save-overlay hidden"
role="dialog"
aria-modal="true"
>
<div class="save-modal">
<div class="save-modal-header">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<path
d="M8 10V7a4 4 0 018 0v3"
stroke="#1a1a2e"
stroke-width="1.8"
stroke-linecap="round"
/>
<rect
x="3"
y="10"
width="18"
height="12"
rx="2"
stroke="#1a1a2e"
stroke-width="1.8"
/>
</svg>
<span id="save-prompt-title">Save to PassKeeper?</span>
</div>
<p id="save-prompt-subtitle" class="save-modal-sub"></p>
<div class="form-group">
<label for="save-name">Site name</label>
<input type="text" id="save-name" placeholder="e.g. GitHub" />
</div>
<div class="form-group">
<label for="save-username">Username / Email</label>
<input
type="text"
id="save-username"
placeholder="username or email"
/>
</div>
<div class="form-group">
<label for="save-folder">Folder</label>
<select id="save-folder" class="save-folder-select">
<option value="">— No folder —</option>
</select>
</div>
<div class="save-modal-actions">
<button id="btn-save-yes" class="btn-primary btn-sm">Save</button>
<button id="btn-save-no" class="btn-ghost btn-sm">Not now</button>
</div>
</div>
</div>
<!-- ── Shared bottom nav (visible whenever vault OR generator is shown) -->
<nav class="bottom-nav" id="main-bottom-nav">
<button class="nav-btn active" id="nav-vault">
<svg viewBox="0 0 24 24" fill="none" width="20" height="20">
<rect
x="3"
y="6"
width="18"
height="14"
rx="2"
stroke="currentColor"
stroke-width="1.7"
/>
<circle
cx="12"
cy="13"
r="2.5"
stroke="currentColor"
stroke-width="1.7"
/>
<path
d="M8 6V5a4 4 0 018 0v1"
stroke="currentColor"
stroke-width="1.7"
/>
</svg>
<span>Vault</span>
</button>
<button class="nav-btn" id="nav-generator" title="Password Generator">
<svg viewBox="0 0 24 24" fill="none" width="20" height="20">
<path
d="M12 2a5 5 0 015 5v1h1a2 2 0 012 2v9a2 2 0 01-2 2H6a2 2 0 01-2-2v-9a2 2 0 012-2h1V7a5 5 0 015-5z"
stroke="currentColor"
stroke-width="1.7"
stroke-linejoin="round"
/>
<circle cx="12" cy="14" r="1.8" fill="currentColor" />
<path
d="M12 14v2.5"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
/>
</svg>
<span>Generator</span>
</button>
<button class="nav-btn" id="nav-alerts" title="Security Alerts">
<svg viewBox="0 0 24 24" fill="none" width="20" height="20">
<path
d="M12 3l8.5 15H3.5L12 3z"
stroke="currentColor"
stroke-width="1.7"
stroke-linejoin="round"
/>
<path
d="M12 10v4"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
/>
<circle cx="12" cy="17" r="0.8" fill="currentColor" />
</svg>
<span>Alerts</span>
</button>
<button class="nav-btn" id="nav-account" title="Account">
<svg viewBox="0 0 24 24" fill="none" width="20" height="20">
<circle
cx="12"
cy="8"
r="4"
stroke="currentColor"
stroke-width="1.7"
/>
<path
d="M4 20c0-4 3.6-7 8-7s8 3 8 7"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
/>
</svg>
<span>Account</span>
</button>
</nav>
</div> </div>
<!-- /app -->
<script src="../shared/crypto.js"></script> <!-- ── MFA view ────────────────────────────────────────────────────── -->
<script src="popup.js"></script> <div id="view-mfa" class="view hidden">
</body> <div class="login-header">
</html> <span class="login-logo">🔐</span>
<h1>Two-Factor Auth</h1>
</div>
<div class="login-body">
<p class="pk-hint">Enter your 6-digit authenticator code.</p>
<p id="mfa-error" class="pk-error hidden"></p>
<div class="form-group">
<label for="mfa-code">Verification Code</label>
<input type="text" id="mfa-code" inputmode="numeric" maxlength="6" placeholder="000000"
autocomplete="one-time-code">
</div>
<button id="btn-mfa-verify" class="btn-primary">Verify</button>
<button id="btn-mfa-back" class="btn-ghost">← Back</button>
</div>
</div>
<!-- ── Unlock-only view (tokens from web app, need vault key) ─────── -->
<div id="view-unlock" class="view hidden">
<div class="login-header">
<span class="login-logo">🔓</span>
<h1>PassKeeper</h1>
<p style="font-size:12px;opacity:0.85;margin-top:4px;">Signed in via web app</p>
</div>
<div class="login-body">
<p class="pk-hint">Enter your master password to unlock the vault.</p>
<p id="unlock-error" class="pk-error hidden"></p>
<div class="form-group">
<label for="unlock-password">Master Password</label>
<input type="password" id="unlock-password" placeholder="Master password" autocomplete="current-password">
</div>
<button id="btn-unlock" class="btn-primary">Unlock</button>
<button id="btn-unlock-signout" class="btn-ghost">Sign out</button>
</div>
</div>
<!-- ── Vault view ──────────────────────────────────────────────────── -->
<div id="view-vault" class="view hidden">
<!-- Top bar -->
<div class="vault-topbar">
<div class="search-wrap">
<svg class="search-icon" viewBox="0 0 20 20" fill="none" width="15" height="15">
<circle cx="8.5" cy="8.5" r="5.5" stroke="#999" stroke-width="1.6" />
<path d="M13 13l3.5 3.5" stroke="#999" stroke-width="1.6" stroke-linecap="round" />
</svg>
<input type="text" id="vault-search" placeholder="Search your vault" autocomplete="off">
</div>
<a id="btn-open-vault" class="btn-vault-link" title="Open vault" target="_blank">
Vault
<svg viewBox="0 0 16 16" fill="none" width="12" height="12">
<path d="M6 3H3a1 1 0 00-1 1v9a1 1 0 001 1h9a1 1 0 001-1v-3" stroke="currentColor" stroke-width="1.5"
stroke-linecap="round" />
<path d="M9 2h5v5M14 2L8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</a>
<button id="btn-add-item" class="btn-add" title="Add item">+</button>
</div>
<!-- Tabs -->
<div class="vault-tabs" id="vault-tabs">
<button class="tab-btn active" data-tab="relevant">All relevant</button>
<button class="tab-btn" data-tab="all">All items</button>
<button class="tab-btn" data-tab="recents">Recents</button>
</div>
<!-- List -->
<div id="vault-spinner" class="pk-loading hidden">Loading vault…</div>
<div id="vault-list" class="vault-list"></div>
<div id="vault-empty" class="pk-empty hidden">No items found.</div>
</div><!-- /view-vault -->
<!-- ── Generator view ─────────────────────────────────────────────── -->
<div id="view-generator" class="view hidden">
<div class="gen-suggestion">
<div class="gen-suggestion-label">Password suggestion</div>
<div class="gen-suggestion-row">
<span id="gen-output" class="gen-output">Click refresh to generate</span>
<div class="gen-suggestion-actions">
<button id="btn-gen-copy" class="gen-icon-btn" title="Copy password">
<svg viewBox="0 0 24 24" fill="none" width="18" height="18">
<rect x="9" y="9" width="11" height="11" rx="2" stroke="currentColor" stroke-width="1.8" />
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke="currentColor" stroke-width="1.8" />
</svg>
</button>
<button id="btn-gen-refresh" class="gen-icon-btn" title="Generate new password">
<svg viewBox="0 0 24 24" fill="none" width="18" height="18">
<path d="M4 4v5h5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M20 20v-5h-5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M20 9a8 8 0 00-14.93-2.07M4 15a8 8 0 0014.93 2.07" stroke="currentColor" stroke-width="1.8"
stroke-linecap="round" />
</svg>
</button>
</div>
</div>
</div>
<div class="gen-strength-row">
<span id="gen-strength-label" class="gen-strength-label"></span>
</div>
<div class="gen-body">
<div class="gen-row">
<label for="gen-length" class="gen-label">Password length:</label>
<input type="number" id="gen-length-num" class="gen-length-num" value="16" min="8" max="64">
<span class="gen-label">characters</span>
</div>
<input type="range" id="gen-length" class="gen-slider" min="8" max="64" value="16">
<label class="gen-check">
<input type="checkbox" id="gen-lower" checked>
<span>Lowercase (abc)</span>
</label>
<label class="gen-check">
<input type="checkbox" id="gen-upper" checked>
<span>Uppercase (ABC)</span>
</label>
<label class="gen-check">
<input type="checkbox" id="gen-numbers" checked>
<span>Numbers (123)</span>
</label>
<label class="gen-check">
<input type="checkbox" id="gen-symbols" checked>
<span>Randomized symbols (!#$)</span>
</label>
</div>
</div><!-- /view-generator -->
<!-- ── Add Item view ─────────────────────────────────────────────── -->
<div id="view-add" class="view hidden">
<div class="add-header">
<button id="btn-add-back" class="add-back-btn" title="Back to vault">
<svg viewBox="0 0 24 24" fill="none" width="18" height="18">
<path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</button>
<span class="add-header-title">Add Password</span>
</div>
<div class="add-body">
<p id="add-error" class="pk-error hidden"></p>
<div class="form-group">
<label for="add-name">Site name <span class="add-required">*</span></label>
<input type="text" id="add-name" placeholder="e.g. GitHub" autocomplete="off">
</div>
<div class="form-group">
<label for="add-url">URL</label>
<input type="url" id="add-url" placeholder="https://example.com" autocomplete="off">
</div>
<div class="form-group">
<label for="add-username">Username / Email</label>
<input type="text" id="add-username" placeholder="username or email" autocomplete="off">
</div>
<div class="form-group">
<label for="add-password">Password <span class="add-required">*</span></label>
<div class="add-pw-row">
<input type="password" id="add-password" placeholder="Password" autocomplete="new-password"
class="add-pw-input">
<button id="btn-add-toggle-pw" class="add-icon-btn" type="button" title="Show/hide password">
<svg viewBox="0 0 24 24" fill="none" width="16" height="16">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" stroke="currentColor" stroke-width="1.8" />
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.8" />
</svg>
</button>
<button id="btn-add-generate" class="add-icon-btn add-gen-btn" type="button" title="Generate password">
<svg viewBox="0 0 24 24" fill="none" width="16" height="16">
<path d="M4 4v5h5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M20 20v-5h-5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M20 9a8 8 0 00-14.93-2.07M4 15a8 8 0 0014.93 2.07" stroke="currentColor" stroke-width="1.8"
stroke-linecap="round" />
</svg>
</button>
</div>
</div>
<div class="form-group">
<label for="add-folder">Folder</label>
<select id="add-folder" class="save-folder-select">
<option value="">— No folder —</option>
</select>
</div>
<div class="form-group">
<label for="add-notes">Notes</label>
<textarea id="add-notes" class="add-notes" placeholder="Optional notes" rows="2"
autocomplete="off"></textarea>
</div>
<button id="btn-add-save" class="btn-primary">Save to Vault</button>
</div>
</div><!-- /view-add -->
<div id="view-account" class="view hidden">
<div class="acct-header">
<span class="acct-logo">⚙️</span>
<h2>Account</h2>
</div>
<div class="acct-body">
<div class="acct-section">
<div class="acct-section-title">🔒 Auto-lock</div>
<p class="acct-hint">Lock the vault after this period of system inactivity.</p>
<select id="acct-idle-timeout" class="acct-select">
<option value="60">1 minute</option>
<option value="300">5 minutes</option>
<option value="600" selected>10 minutes (default)</option>
<option value="1800">30 minutes</option>
<option value="0">Never</option>
</select>
<p id="acct-idle-saved" class="acct-saved hidden">✓ Saved</p>
</div>
<div class="acct-section">
<div class="acct-section-title">🌐 Open web vault</div>
<p class="acct-hint">Access all vault features, sharing, and security dashboard.</p>
<a id="acct-open-vault" class="btn-primary acct-link-btn" target="_blank">Open PassKeeper Vault</a>
</div>
<div class="acct-section acct-section-danger">
<div class="acct-section-title">🚪 Sign out</div>
<p class="acct-hint">Clear the local session. You will need your master password to unlock again.</p>
<button id="acct-signout" class="btn-ghost">Sign out</button>
</div>
</div>
</div><!-- /view-account -->
<!-- ── Save-prompt modal overlay (shared across vault/generator views) -->
<div id="save-prompt-overlay" class="save-overlay hidden" role="dialog" aria-modal="true">
<div class="save-modal">
<div class="save-modal-header">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<path d="M8 10V7a4 4 0 018 0v3" stroke="#1a1a2e" stroke-width="1.8" stroke-linecap="round" />
<rect x="3" y="10" width="18" height="12" rx="2" stroke="#1a1a2e" stroke-width="1.8" />
</svg>
<span id="save-prompt-title">Save to PassKeeper?</span>
</div>
<p id="save-prompt-subtitle" class="save-modal-sub"></p>
<div class="form-group">
<label for="save-name">Site name</label>
<input type="text" id="save-name" placeholder="e.g. GitHub">
</div>
<div class="form-group">
<label for="save-username">Username / Email</label>
<input type="text" id="save-username" placeholder="username or email">
</div>
<div class="form-group">
<label for="save-folder">Folder</label>
<select id="save-folder" class="save-folder-select">
<option value="">— No folder —</option>
</select>
</div>
<div class="save-modal-actions">
<button id="btn-save-yes" class="btn-primary btn-sm">Save</button>
<button id="btn-save-no" class="btn-ghost btn-sm">Not now</button>
</div>
</div>
</div>
<!-- ── Shared bottom nav (visible whenever vault OR generator is shown) -->
<nav class="bottom-nav" id="main-bottom-nav">
<button class="nav-btn active" id="nav-vault">
<svg viewBox="0 0 24 24" fill="none" width="20" height="20">
<rect x="3" y="6" width="18" height="14" rx="2" stroke="currentColor" stroke-width="1.7" />
<circle cx="12" cy="13" r="2.5" stroke="currentColor" stroke-width="1.7" />
<path d="M8 6V5a4 4 0 018 0v1" stroke="currentColor" stroke-width="1.7" />
</svg>
<span>Vault</span>
</button>
<button class="nav-btn" id="nav-generator" title="Password Generator">
<svg viewBox="0 0 24 24" fill="none" width="20" height="20">
<path d="M12 2a5 5 0 015 5v1h1a2 2 0 012 2v9a2 2 0 01-2 2H6a2 2 0 01-2-2v-9a2 2 0 012-2h1V7a5 5 0 015-5z"
stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" />
<circle cx="12" cy="14" r="1.8" fill="currentColor" />
<path d="M12 14v2.5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
</svg>
<span>Generator</span>
</button>
<button class="nav-btn" id="nav-alerts" title="Security Alerts">
<svg viewBox="0 0 24 24" fill="none" width="20" height="20">
<path d="M12 3l8.5 15H3.5L12 3z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" />
<path d="M12 10v4" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
<circle cx="12" cy="17" r="0.8" fill="currentColor" />
</svg>
<span>Alerts</span>
</button>
<button class="nav-btn" id="nav-account" title="Account">
<svg viewBox="0 0 24 24" fill="none" width="20" height="20">
<circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="1.7" />
<path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
</svg>
<span>Account</span>
</button>
</nav>
</div><!-- /app -->
<script src="../shared/crypto.js"></script>
<script src="popup.js"></script>
</body>
</html>
+432 -473
View File
File diff suppressed because it is too large Load Diff