From 27e1c16851848b922ee1ff2f7a98af2b082a33e4 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Mon, 20 Apr 2026 17:51:01 -0400 Subject: [PATCH] 04/20/2026 enhance the extension 2 --- extension/background.js | 36 +- extension/content/content.js | 871 +++++++++++++++------------------ extension/popup/popup.css | 336 ++++++++++++- extension/popup/popup.html | 744 +++++++++++++--------------- extension/popup/popup.js | 905 +++++++++++++++++------------------ 5 files changed, 1488 insertions(+), 1404 deletions(-) diff --git a/extension/background.js b/extension/background.js index 2cb0c2b..9bc84a4 100644 --- a/extension/background.js +++ b/extension/background.js @@ -10,20 +10,35 @@ // ── Idle lock ───────────────────────────────────────────────────────────────── -// Lock after 10 minutes of system idle or when the screen is locked. -const IDLE_LOCK_SECONDS = 600; +// Default: 10 minutes. User can change via the Account view in the popup. +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) => { 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.'); - // 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(); - // Clear the content-script vault cache so suggestions stop showing. await chrome.storage.local.remove('vault_items_cs'); - // Clear all badge text — vault is now locked. const tabs = await chrome.tabs.query({}); tabs.forEach(tab => { 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) => { + // 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). if (msg.type === 'OPEN_VAULT') { chrome.tabs.create({ url: 'https://pwkeeper.ngodanguyen.tech/vault' }); diff --git a/extension/content/content.js b/extension/content/content.js index 96e9b3f..8bfbc63 100644 --- a/extension/content/content.js +++ b/extension/content/content.js @@ -11,12 +11,12 @@ * 6. Watches form submissions → shows save-credentials banner. */ (() => { - "use strict"; + 'use strict'; - const PK_ATTR = "data-pk-decorated"; - const PK_BTN_CLASS = "__pk_btn__"; - const PK_DROPDOWN_ID = "__pk_dropdown__"; - const VAULT_URL = "https://pwkeeper.ngodanguyen.tech/vault"; + const PK_ATTR = 'data-pk-decorated'; + const PK_BTN_CLASS = '__pk_btn__'; + const PK_DROPDOWN_ID = '__pk_dropdown__'; + const VAULT_URL = 'https://pwkeeper.ngodanguyen.tech/vault'; let _bannerEl = null; let _hasNotifiedForm = false; @@ -28,10 +28,7 @@ // ── Helpers ────────────────────────────────────────────────────────────────── function escHtml(str) { - return String(str ?? "") - .replace(/&/g, "&") - .replace(//g, ">"); + return String(str ?? '').replace(/&/g, '&').replace(//g, '>'); } /** @@ -44,39 +41,28 @@ const rect = el.getBoundingClientRect(); if (rect.width === 0 && rect.height === 0) return false; const style = window.getComputedStyle(el); - if ( - style.display === "none" || - style.visibility === "hidden" || - style.opacity === "0" - ) - return false; + if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false; return true; } function visiblePasswordFields() { - return Array.from( - document.querySelectorAll('input[type="password"]'), - ).filter((el) => isVisible(el) && !el.disabled); + return Array.from(document.querySelectorAll('input[type="password"]')) + .filter(el => isVisible(el) && !el.disabled); } function findUsernameField(pwField) { // 1. Walk backwards through all inputs in DOM order. - const all = Array.from(document.querySelectorAll("input")); + const all = Array.from(document.querySelectorAll('input')); const idx = all.indexOf(pwField); for (let i = idx - 1; i >= 0; i--) { const el = all[i]; if (!isVisible(el) || el.disabled) continue; - if (["email", "text", "tel"].includes(el.type)) return el; + if (['email', 'text', 'tel'].includes(el.type)) return el; } // 2. Fallback: search within the same form/ancestor container. - const container = - pwField.closest("form") || - pwField.closest('[role="form"]') || - pwField.parentElement; + const container = pwField.closest('form') || pwField.closest('[role="form"]') || pwField.parentElement; if (container) { - const candidate = container.querySelector( - 'input[type="email"]:not([disabled]), input[type="text"]:not([disabled]), input[type="tel"]:not([disabled])', - ); + const candidate = container.querySelector('input[type="email"]:not([disabled]), input[type="text"]:not([disabled]), input[type="tel"]:not([disabled])'); if (candidate && isVisible(candidate)) return candidate; } return null; @@ -85,14 +71,11 @@ // ── Framework-compatible fill ───────────────────────────────────────────────── function fillField(el, value) { - const nativeSet = Object.getOwnPropertyDescriptor( - HTMLInputElement.prototype, - "value", - )?.set; + const nativeSet = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set; if (nativeSet) nativeSet.call(el, value); else el.value = value; - el.dispatchEvent(new Event("input", { bubbles: true })); - el.dispatchEvent(new Event("change", { bubbles: true })); + el.dispatchEvent(new Event('input', { bubbles: true })); + el.dispatchEvent(new Event('change', { bubbles: true })); } function doAutofill(username, password) { @@ -102,11 +85,9 @@ const usernameField = findUsernameField(pwField); if (usernameField && username) fillField(usernameField, username); if (password) fillField(pwField, password); - [usernameField, pwField].filter(Boolean).forEach((el) => { - el.style.outline = "2px solid #c0392b"; - setTimeout(() => { - el.style.outline = ""; - }, 1500); + [usernameField, pwField].filter(Boolean).forEach(el => { + el.style.outline = '2px solid #c0392b'; + setTimeout(() => { el.style.outline = ''; }, 1500); }); } @@ -118,13 +99,10 @@ */ function positionBtn(btn, field) { const rect = field.getBoundingClientRect(); - if (rect.width === 0) { - btn.style.display = "none"; - return; - } - btn.style.display = "flex"; - btn.style.top = rect.top + rect.height / 2 - 13 + "px"; - btn.style.left = rect.right - 30 + "px"; + if (rect.width === 0) { btn.style.display = 'none'; return; } + btn.style.display = 'flex'; + btn.style.top = (rect.top + rect.height / 2 - 13) + 'px'; + btn.style.left = (rect.right - 30) + 'px'; } // createIconBtn is defined in the Field decoration section below. @@ -149,44 +127,37 @@ var freshItems = _matchingItems; if (!freshItems.length) { try { - var result = await chrome.storage.local.get("vault_items_cs"); + var result = await chrome.storage.local.get('vault_items_cs'); var all = (result && result.vault_items_cs) || []; freshItems = _filterForHost(all); if (freshItems.length) _matchingItems = freshItems; - } catch (e) {} + } catch (e) { } } const rect = anchorField.getBoundingClientRect(); const dropWidth = Math.max(260, rect.width); - const dropdown = document.createElement("div"); + const dropdown = document.createElement('div'); dropdown.id = PK_DROPDOWN_ID; Object.assign(dropdown.style, { - position: "fixed", - top: rect.bottom + 4 + "px", - left: rect.left + "px", - width: dropWidth + "px", - background: "#fff", - border: "1px solid #dadce0", - borderRadius: "10px", - boxShadow: "0 6px 24px rgba(0,0,0,0.18)", - zIndex: "2147483647", - fontFamily: - "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif", - fontSize: "13px", - overflow: "hidden", + position: 'fixed', + top: (rect.bottom + 4) + 'px', + left: rect.left + 'px', + width: dropWidth + 'px', + background: '#fff', + border: '1px solid #dadce0', + borderRadius: '10px', + boxShadow: '0 6px 24px rgba(0,0,0,0.18)', + zIndex: '2147483647', + fontFamily: "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif", + fontSize: '13px', + overflow: 'hidden', }); - if (panel === "more") { + if (panel === 'more') { buildMorePanel(dropdown, anchorField, pwField, freshItems, filterText); } else { - buildCredentialsPanel( - dropdown, - anchorField, - pwField, - freshItems, - filterText, - ); + buildCredentialsPanel(dropdown, anchorField, pwField, freshItems, filterText); } document.body.appendChild(dropdown); @@ -194,163 +165,165 @@ // Reposition on scroll/resize so it stays under the field. function reposition() { const r = anchorField.getBoundingClientRect(); - dropdown.style.top = r.bottom + 4 + "px"; - dropdown.style.left = r.left + "px"; + dropdown.style.top = (r.bottom + 4) + 'px'; + dropdown.style.left = r.left + 'px'; } - window.addEventListener("scroll", reposition, { - passive: true, - capture: true, - }); - window.addEventListener("resize", reposition, { passive: true }); + window.addEventListener('scroll', reposition, { passive: true, capture: true }); + window.addEventListener('resize', reposition, { passive: true }); - // Close on outside mousedown or Escape. + // Close on outside mousedown or keyboard navigation. function onOutside(e) { const btn = _fieldBtnMap.get(anchorField); - if ( - dropdown.contains(e.target) || - e.target === anchorField || - (btn && btn.contains(e.target)) - ) - return; + if (dropdown.contains(e.target) || e.target === anchorField || (btn && btn.contains(e.target))) return; removeDropdown(); - document.removeEventListener("mousedown", onOutside, true); - document.removeEventListener("keydown", onEscape, true); + document.removeEventListener('mousedown', onOutside, true); + document.removeEventListener('keydown', onKeydown, true); } - function onEscape(e) { - if (e.key === "Escape") { + + // Keyboard navigation: Arrow keys move focus between rows; Enter selects; Escape closes. + function onKeydown(e) { + if (e.key === 'Escape') { removeDropdown(); - document.removeEventListener("mousedown", onOutside, true); - document.removeEventListener("keydown", onEscape, true); + document.removeEventListener('mousedown', onOutside, true); + document.removeEventListener('keydown', onKeydown, true); + return; } + if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp' && e.key !== 'Enter') return; + + // Only navigate credential rows (divs with data-pk-row attribute). + var rows = Array.from(dropdown.querySelectorAll('[data-pk-row]')); + if (!rows.length) return; + + e.preventDefault(); // prevent the field from scrolling the page + + if (e.key === 'Enter') { + var focused = dropdown.querySelector('[data-pk-row].pk-row-focused'); + if (focused && focused._pkFill) focused._pkFill(); + return; + } + + var currentIdx = rows.findIndex(function (r) { return r.classList.contains('pk-row-focused'); }); + var nextIdx; + if (e.key === 'ArrowDown') { + nextIdx = currentIdx < rows.length - 1 ? currentIdx + 1 : 0; + } else { + nextIdx = currentIdx > 0 ? currentIdx - 1 : rows.length - 1; + } + + rows.forEach(function (r) { + r.classList.remove('pk-row-focused'); + r.style.background = ''; + }); + rows[nextIdx].classList.add('pk-row-focused'); + rows[nextIdx].style.background = '#e8f0fe'; + rows[nextIdx].scrollIntoView({ block: 'nearest' }); } + setTimeout(function () { - document.addEventListener("mousedown", onOutside, true); - document.addEventListener("keydown", onEscape, true); + document.addEventListener('mousedown', onOutside, true); + document.addEventListener('keydown', onKeydown, true); }, 0); } // ── Credentials panel (main list) ──────────────────────────────────────────── - function buildCredentialsPanel( - dropdown, - anchorField, - pwField, - items, - filterText, - ) { - const q = (filterText || "").trim().toLowerCase(); + function buildCredentialsPanel(dropdown, anchorField, pwField, items, filterText) { + const q = (filterText || '').trim().toLowerCase(); const filtered = q ? items.filter(function (item) { - return ( - ((item.plain && item.plain.username) || "") - .toLowerCase() - .includes(q) || item.name.toLowerCase().includes(q) - ); - }) + return ((item.plain && item.plain.username) || '').toLowerCase().includes(q) || + item.name.toLowerCase().includes(q); + }) : items; - const usernameField = - anchorField.type === "password" - ? findUsernameField(anchorField) - : anchorField; + const usernameField = anchorField.type === 'password' ? findUsernameField(anchorField) : anchorField; if (filtered.length === 0) { // No saved passwords — show a minimal "no items" row + More options. - const empty = document.createElement("div"); + const empty = document.createElement('div'); Object.assign(empty.style, { - padding: "12px 14px", - color: "#5f6368", - fontSize: "12px", + padding: '12px 14px', + color: '#5f6368', + fontSize: '12px', }); - empty.textContent = q - ? "No matches found." - : "No saved passwords for this site."; + empty.textContent = q ? 'No matches found.' : 'No saved passwords for this site.'; dropdown.appendChild(empty); } else { filtered.forEach(function (item) { - const row = document.createElement("div"); + const row = document.createElement('div'); + row.setAttribute('data-pk-row', '1'); // enables keyboard navigation Object.assign(row.style, { - display: "flex", - alignItems: "center", - gap: "10px", - padding: "10px 14px", - cursor: "pointer", - transition: "background 0.1s", + display: 'flex', + alignItems: 'center', + gap: '10px', + padding: '10px 14px', + cursor: 'pointer', + transition: 'background 0.1s', }); row.onmouseenter = function () { - row.style.background = "#f1f3f4"; + if (!row.classList.contains('pk-row-focused')) row.style.background = '#f1f3f4'; }; row.onmouseleave = function () { - row.style.background = ""; + if (!row.classList.contains('pk-row-focused')) row.style.background = ''; }; // Derive display hostname. var siteHost = item.name; if (item.plain && item.plain.url) { - try { - siteHost = new URL(item.plain.url).hostname.replace(/^www\./, ""); - } catch (e) {} + try { siteHost = new URL(item.plain.url).hostname.replace(/^www\./, ''); } catch (e) { } } - var username = escHtml((item.plain && item.plain.username) || ""); + var username = escHtml((item.plain && item.plain.username) || ''); var site = escHtml(siteHost); // Lock icon avatar — filled dark circle like the screenshot. - var avatar = document.createElement("div"); + var avatar = document.createElement('div'); Object.assign(avatar.style, { - width: "34px", - height: "34px", - borderRadius: "50%", - background: "#1a1a2e", - display: "flex", - alignItems: "center", - justifyContent: "center", - flexShrink: "0", + width: '34px', + height: '34px', + borderRadius: '50%', + background: '#1a1a2e', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: '0', }); avatar.innerHTML = '' + '' + '' + - ""; + ''; // Text. - var text = document.createElement("div"); - text.style.cssText = "flex:1;min-width:0;"; + var text = document.createElement('div'); + text.style.cssText = 'flex:1;min-width:0;'; text.innerHTML = - '
' + - site + - "
" + - (username - ? '
' + - username + - "
" - : ""); + '
' + site + '
' + + (username ? '
' + username + '
' : ''); // Edit pencil. - var editBtn = document.createElement("button"); + var editBtn = document.createElement('button'); Object.assign(editBtn.style, { - background: "none", - border: "none", - cursor: "pointer", - padding: "5px", - color: "#1a73e8", - display: "flex", - alignItems: "center", - flexShrink: "0", - borderRadius: "4px", + background: 'none', + border: 'none', + cursor: 'pointer', + padding: '5px', + color: '#1a73e8', + display: 'flex', + alignItems: 'center', + flexShrink: '0', + borderRadius: '4px', }); - editBtn.title = "Edit in PassKeeper"; + editBtn.title = 'Edit in PassKeeper'; editBtn.innerHTML = '' + '' + '' + - ""; - editBtn.addEventListener("mousedown", function (e) { + ''; + editBtn.addEventListener('mousedown', function (e) { e.preventDefault(); e.stopPropagation(); - chrome.runtime - .sendMessage({ type: "OPEN_VAULT" }) - .catch(function () {}); + chrome.runtime.sendMessage({ type: 'OPEN_VAULT' }).catch(function () { }); removeDropdown(); }); @@ -358,57 +331,66 @@ row.appendChild(text); row.appendChild(editBtn); - row.addEventListener("mousedown", function (e) { + // Shared fill action — used by both mousedown and keyboard Enter. + function doFill() { + if (usernameField && item.plain && item.plain.username) fillField(usernameField, item.plain.username); + if (pwField && item.plain && item.plain.password) fillField(pwField, item.plain.password); + + // ✓ Filled flash: replace row content briefly before closing. + row.innerHTML = + '
' + + '' + + '' + + 'Filled
'; + row.style.background = '#f0fdf4'; + + setTimeout(function () { + removeDropdown(); + if (pwField && anchorField !== pwField) pwField.focus(); + }, 600); + } + + row.addEventListener('mousedown', function (e) { if (e.target === editBtn || editBtn.contains(e.target)) return; e.preventDefault(); - if (usernameField && item.plain && item.plain.username) - fillField(usernameField, item.plain.username); - if (pwField && item.plain && item.plain.password) - fillField(pwField, item.plain.password); - removeDropdown(); - if (pwField && anchorField !== pwField) { - setTimeout(function () { - pwField.focus(); - }, 0); - } + doFill(); }); + // Expose doFill for the keyboard Enter handler via a custom property. + row._pkFill = doFill; + dropdown.appendChild(row); }); } // Divider + "More options…" footer — always shown. - var divider = document.createElement("div"); - divider.style.cssText = "height:1px;background:#e8eaed;"; + var divider = document.createElement('div'); + divider.style.cssText = 'height:1px;background:#e8eaed;'; dropdown.appendChild(divider); - var more = document.createElement("div"); + var more = document.createElement('div'); Object.assign(more.style, { - display: "flex", - alignItems: "center", - gap: "10px", - padding: "10px 14px", - cursor: "pointer", - color: "#202124", - fontSize: "13px", - transition: "background 0.1s", + display: 'flex', + alignItems: 'center', + gap: '10px', + padding: '10px 14px', + cursor: 'pointer', + color: '#202124', + fontSize: '13px', + transition: 'background 0.1s', }); - more.onmouseenter = function () { - more.style.background = "#f1f3f4"; - }; - more.onmouseleave = function () { - more.style.background = ""; - }; + more.onmouseenter = function () { more.style.background = '#f1f3f4'; }; + more.onmouseleave = function () { more.style.background = ''; }; more.innerHTML = '' + '' + '' + '' + - "" + + '' + 'More options\u2026'; - more.addEventListener("mousedown", function (e) { + more.addEventListener('mousedown', function (e) { e.preventDefault(); - showDropdown(anchorField, pwField, filterText, "more"); + showDropdown(anchorField, pwField, filterText, 'more'); }); dropdown.appendChild(more); } @@ -417,32 +399,28 @@ function buildMorePanel(dropdown, anchorField, pwField, items, filterText) { // Back header. - var backRow = document.createElement("div"); + var backRow = document.createElement('div'); Object.assign(backRow.style, { - display: "flex", - alignItems: "center", - gap: "6px", - padding: "10px 14px", - cursor: "pointer", - color: "#1a73e8", - fontSize: "13px", - fontWeight: "600", - borderBottom: "1px solid #e8eaed", - transition: "background 0.1s", + display: 'flex', + alignItems: 'center', + gap: '6px', + padding: '10px 14px', + cursor: 'pointer', + color: '#1a73e8', + fontSize: '13px', + fontWeight: '600', + borderBottom: '1px solid #e8eaed', + transition: 'background 0.1s', }); - backRow.onmouseenter = function () { - backRow.style.background = "#f1f3f4"; - }; - backRow.onmouseleave = function () { - backRow.style.background = ""; - }; + backRow.onmouseenter = function () { backRow.style.background = '#f1f3f4'; }; + backRow.onmouseleave = function () { backRow.style.background = ''; }; backRow.innerHTML = '' + '' + - " Back"; - backRow.addEventListener("mousedown", function (e) { + ' Back'; + backRow.addEventListener('mousedown', function (e) { e.preventDefault(); - showDropdown(anchorField, pwField, filterText, "credentials"); + showDropdown(anchorField, pwField, filterText, 'credentials'); }); dropdown.appendChild(backRow); @@ -450,90 +428,67 @@ var menuItems = [ { icon: '', - label: "Report a problem", - action: function () { - chrome.runtime - .sendMessage({ type: "OPEN_VAULT" }) - .catch(function () {}); - removeDropdown(); - }, + label: 'Report a problem', + action: function () { chrome.runtime.sendMessage({ type: 'OPEN_VAULT' }).catch(function () { }); removeDropdown(); }, }, { icon: '', - label: "Generate a password", + label: 'Generate a password', chevron: true, - action: function () { - chrome.runtime - .sendMessage({ type: "OPEN_GENERATOR" }) - .catch(function () {}); - removeDropdown(); - }, + action: function () { chrome.runtime.sendMessage({ type: 'OPEN_GENERATOR' }).catch(function () { }); removeDropdown(); }, }, { icon: '', - label: "Open my vault", - action: function () { - chrome.runtime - .sendMessage({ type: "OPEN_VAULT" }) - .catch(function () {}); - removeDropdown(); - }, + label: 'Open my vault', + action: function () { chrome.runtime.sendMessage({ type: 'OPEN_VAULT' }).catch(function () { }); removeDropdown(); }, }, ]; menuItems.forEach(function (item) { - var row = document.createElement("div"); + var row = document.createElement('div'); Object.assign(row.style, { - display: "flex", - alignItems: "center", - gap: "12px", - padding: "11px 14px", - cursor: "pointer", - color: "#202124", - fontSize: "13px", - transition: "background 0.1s", - borderBottom: "1px solid #f3f4f6", + display: 'flex', + alignItems: 'center', + gap: '12px', + padding: '11px 14px', + cursor: 'pointer', + color: '#202124', + fontSize: '13px', + transition: 'background 0.1s', + borderBottom: '1px solid #f3f4f6', }); - row.onmouseenter = function () { - row.style.background = "#f1f3f4"; - }; - row.onmouseleave = function () { - row.style.background = ""; - }; + row.onmouseenter = function () { row.style.background = '#f1f3f4'; }; + row.onmouseleave = function () { row.style.background = ''; }; - var iconWrap = document.createElement("div"); - iconWrap.style.cssText = - "width:18px;height:18px;display:flex;align-items:center;justify-content:center;flex-shrink:0;"; - iconWrap.innerHTML = - '' + - item.icon + - ""; + var iconWrap = document.createElement('div'); + iconWrap.style.cssText = 'width:18px;height:18px;display:flex;align-items:center;justify-content:center;flex-shrink:0;'; + iconWrap.innerHTML = '' + item.icon + ''; - var label = document.createElement("span"); - label.style.cssText = "flex:1;"; + var label = document.createElement('span'); + label.style.cssText = 'flex:1;'; label.textContent = item.label; row.appendChild(iconWrap); row.appendChild(label); if (item.chevron) { - var chev = document.createElement("div"); + var chev = document.createElement('div'); chev.innerHTML = '' + '' + - ""; + ''; row.appendChild(chev); } else { - var extIcon = document.createElement("div"); + var extIcon = document.createElement('div'); extIcon.innerHTML = '' + '' + '' + - ""; + ''; row.appendChild(extIcon); } - row.addEventListener("mousedown", function (e) { + row.addEventListener('mousedown', function (e) { e.preventDefault(); item.action(); }); @@ -549,7 +504,7 @@ function decorateField(field, pwField) { if (field.getAttribute(PK_ATTR)) return; - field.setAttribute(PK_ATTR, "1"); + field.setAttribute(PK_ATTR, '1'); // Cancel any previous listeners on this field. const prevAC = _fieldAbortMap.get(field); @@ -562,114 +517,82 @@ } function createIconBtn(field, pwField, abortSignal) { - const btn = document.createElement("button"); - btn.type = "button"; + const btn = document.createElement('button'); + btn.type = 'button'; btn.className = PK_BTN_CLASS; - btn.title = "PassKeeper autofill"; - btn.setAttribute("aria-label", "Autofill with PassKeeper"); + btn.title = 'PassKeeper autofill'; + btn.setAttribute('aria-label', 'Autofill with PassKeeper'); btn.style.cssText = [ - "position:fixed", - "width:26px", - "height:26px", - "background:#c0392b", - "border:none", - "border-radius:5px", - "cursor:pointer", - "display:flex", - "align-items:center", - "justify-content:center", - "z-index:2147483646", - "padding:0", - "box-shadow:0 1px 4px rgba(0,0,0,0.3)", - "transition:background 0.15s", - ].join(";"); + 'position:fixed', + 'width:26px', + 'height:26px', + 'background:#c0392b', + 'border:none', + 'border-radius:5px', + 'cursor:pointer', + 'display:flex', + 'align-items:center', + 'justify-content:center', + 'z-index:2147483646', + 'padding:0', + 'box-shadow:0 1px 4px rgba(0,0,0,0.3)', + 'transition:background 0.15s', + ].join(';'); btn.innerHTML = '' + '' + '' + '' + - ""; + ''; - btn.addEventListener("mouseenter", function () { - btn.style.background = "#a93226"; - }); - btn.addEventListener("mouseleave", function () { - btn.style.background = "#c0392b"; - }); + btn.addEventListener('mouseenter', function () { btn.style.background = '#a93226'; }); + btn.addEventListener('mouseleave', function () { btn.style.background = '#c0392b'; }); positionBtn(btn, field); document.body.appendChild(btn); _fieldBtnMap.set(field, btn); // Remove the button when the AbortController fires (re-decoration). - abortSignal.addEventListener("abort", function () { + abortSignal.addEventListener('abort', function () { btn.remove(); _fieldBtnMap.delete(field); }); // Keep button tracked as page scrolls/resizes. - function reposition() { - if (document.body.contains(btn)) positionBtn(btn, field); - } - window.addEventListener("scroll", reposition, { - passive: true, - signal: abortSignal, - }); - window.addEventListener("resize", reposition, { - passive: true, - signal: abortSignal, - }); + function reposition() { if (document.body.contains(btn)) positionBtn(btn, field); } + window.addEventListener('scroll', reposition, { passive: true, signal: abortSignal }); + window.addEventListener('resize', reposition, { passive: true, signal: abortSignal }); // ── All event handlers read _matchingItems at call time, never from closure ── // Show dropdown on focus — reads vault_items fresh from storage each time. - field.addEventListener( - "focus", - function () { - showDropdown(field, pwField, field.value, "credentials"); - }, - { signal: abortSignal }, - ); + field.addEventListener('focus', function () { + showDropdown(field, pwField, field.value, 'credentials'); + }, { signal: abortSignal }); // Re-filter as user types. - field.addEventListener( - "input", - function () { - showDropdown(field, pwField, field.value, "credentials"); - }, - { signal: abortSignal }, - ); + field.addEventListener('input', function () { + showDropdown(field, pwField, field.value, 'credentials'); + }, { signal: abortSignal }); // Dim button when field loses focus and no dropdown is open. - field.addEventListener( - "blur", - function () { - setTimeout(function () { - if (!document.getElementById(PK_DROPDOWN_ID)) - btn.style.opacity = "0.4"; - }, 150); - }, - { signal: abortSignal }, - ); + field.addEventListener('blur', function () { + setTimeout(function () { + if (!document.getElementById(PK_DROPDOWN_ID)) btn.style.opacity = '0.4'; + }, 150); + }, { signal: abortSignal }); - field.addEventListener( - "focus", - function () { - btn.style.opacity = "1"; - }, - { signal: abortSignal }, - ); + field.addEventListener('focus', function () { + btn.style.opacity = '1'; + }, { signal: abortSignal }); // Icon click: toggle dropdown. - btn.addEventListener("mousedown", function (e) { + btn.addEventListener('mousedown', function (e) { e.preventDefault(); e.stopPropagation(); - if (document.getElementById(PK_DROPDOWN_ID)) { - removeDropdown(); - } else { - showDropdown(field, pwField, field.value, "credentials"); - } + if (document.getElementById(PK_DROPDOWN_ID)) { removeDropdown(); } + else { showDropdown(field, pwField, field.value, 'credentials'); } }); return btn; @@ -680,20 +603,13 @@ // does not depend on message delivery from the service worker. var all = []; try { - var result = await chrome.storage.local.get("vault_items_cs"); + var result = await chrome.storage.local.get('vault_items_cs'); all = (result && result.vault_items_cs) || []; - } catch (e) {} + } catch (e) { } _matchingItems = _filterForHost(all); - console.log( - "[PassKeeper] decorateFields: host=" + - location.hostname.replace(/^www\./, "") + - ", matched=" + - _matchingItems.length + - " of " + - all.length + - " items", - ); + console.log('[PassKeeper] decorateFields: host=' + location.hostname.replace(/^www\./, '') + + ', matched=' + _matchingItems.length + ' of ' + all.length + ' items'); // Decorate every visible password field and its paired username field. visiblePasswordFields().forEach(function (pwField) { @@ -706,16 +622,13 @@ // ── Vault item helpers ──────────────────────────────────────────────────────── function _filterForHost(items) { - var host = location.hostname.replace(/^www\./, ""); + var host = location.hostname.replace(/^www\./, ''); return (items || []).filter(function (item) { - if (item.item_type !== "password" || !(item.plain && item.plain.url)) - return false; + if (item.item_type !== 'password' || !(item.plain && item.plain.url)) return false; try { - var h = new URL(item.plain.url).hostname.replace(/^www\./, ""); - return h === host || h.endsWith("." + host) || host.endsWith("." + h); - } catch (e) { - return false; - } + var h = new URL(item.plain.url).hostname.replace(/^www\./, ''); + return h === host || h.endsWith('.' + host) || host.endsWith('.' + h); + } catch (e) { return false; } }); } @@ -725,9 +638,7 @@ if (_hasNotifiedForm) return; if (!visiblePasswordFields().length) return; _hasNotifiedForm = true; - chrome.runtime - .sendMessage({ type: "FORMS_DETECTED" }) - .catch(function () {}); + chrome.runtime.sendMessage({ type: 'FORMS_DETECTED' }).catch(function () { }); } // ── Duplicate detection ─────────────────────────────────────────────────────── @@ -735,23 +646,41 @@ async function classifyCredentials(username, password) { var all = []; try { - var result = await chrome.storage.local.get("vault_items_cs"); + var result = await chrome.storage.local.get('vault_items_cs'); all = (result && result.vault_items_cs) || []; - } catch (e) { - return "new"; - } - if (!all.length) return "new"; + } catch (e) { return 'new'; } + if (!all.length) return 'new'; var siteItems = _filterForHost(all); - if (!siteItems.length) return "new"; + if (!siteItems.length) return 'new'; var exactMatch = siteItems.some(function (item) { - return ( - item.plain && - item.plain.username === username && - item.plain.password === password - ); + return item.plain && item.plain.username === username && item.plain.password === password; }); - return exactMatch ? "same" : "updated"; + return exactMatch ? 'same' : 'updated'; + } + + // ── Save blocklist ──────────────────────────────────────────────────────────── + + const BLOCKLIST_KEY = 'save_blocklist'; + + async function isBlocked(hostname) { + try { + var result = await chrome.storage.local.get(BLOCKLIST_KEY); + var list = (result && result[BLOCKLIST_KEY]) || []; + return list.indexOf(hostname) !== -1; + } catch (e) { return false; } + } + + async function addToBlocklist(hostname) { + try { + var result = await chrome.storage.local.get(BLOCKLIST_KEY); + var list = (result && result[BLOCKLIST_KEY]) || []; + if (list.indexOf(hostname) === -1) { + list.push(hostname); + await chrome.storage.local.set({ [BLOCKLIST_KEY]: list }); + console.log('[PassKeeper] Added to save blocklist:', hostname); + } + } catch (e) { } } // ── Auto-save banner ────────────────────────────────────────────────────────── @@ -759,80 +688,60 @@ function showSaveBanner(username, password, credentialState) { if (_bannerEl) _bannerEl.remove(); - var banner = document.createElement("div"); - banner.id = "__pk_save_banner__"; + var banner = document.createElement('div'); + banner.id = '__pk_save_banner__'; Object.assign(banner.style, { - position: "fixed", - top: "12px", - right: "12px", - zIndex: "2147483647", - background: "#ffffff", - border: "1px solid #e2e8f0", - borderRadius: "10px", - boxShadow: "0 8px 30px rgba(0,0,0,0.15)", - padding: "14px 16px 12px", - fontFamily: - "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif", - fontSize: "13px", - color: "#1a1a2e", - maxWidth: "300px", - minWidth: "240px", + position: 'fixed', + top: '12px', + right: '12px', + zIndex: '2147483647', + background: '#ffffff', + border: '1px solid #e2e8f0', + borderRadius: '10px', + boxShadow: '0 8px 30px rgba(0,0,0,0.15)', + padding: '14px 16px 12px', + fontFamily: "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif", + fontSize: '13px', + color: '#1a1a2e', + maxWidth: '300px', + minWidth: '240px', }); var site = escHtml(location.hostname); var user = escHtml(username); - var title = - credentialState === "updated" - ? "Update in PassKeeper?" - : "Save to PassKeeper?"; + var title = credentialState === 'updated' ? 'Update in PassKeeper?' : 'Save to PassKeeper?'; banner.innerHTML = '
' + '' + - '' + - escHtml(title) + - "" + + '' + escHtml(title) + '' + '' + - "
" + + '' + '

' + - '' + - user + - ' on ' + - site + - "" + - "

" + - '
' + + '' + user + ' on ' + site + '' + + '

' + + '
' + '' + '' + - "
"; + '
' + + ''; document.body.appendChild(banner); _bannerEl = banner; - var dismiss = function () { - if (_bannerEl === banner) { - banner.remove(); - _bannerEl = null; - } - }; - banner.querySelector("#__pk_close__").addEventListener("click", dismiss); - banner.querySelector("#__pk_skip__").addEventListener("click", dismiss); - banner.querySelector("#__pk_save__").addEventListener("click", function () { - console.log( - "[PassKeeper] User chose to save credentials for", - location.hostname, - ); - chrome.runtime - .sendMessage({ - type: "SAVE_CREDENTIALS", - data: { - url: location.href, - siteName: document.title || location.hostname, - username: username, - password: password, - }, - }) - .catch(function () {}); + var dismiss = function () { if (_bannerEl === banner) { banner.remove(); _bannerEl = null; } }; + banner.querySelector('#__pk_close__').addEventListener('click', dismiss); + banner.querySelector('#__pk_skip__').addEventListener('click', dismiss); + banner.querySelector('#__pk_save__').addEventListener('click', function () { + console.log('[PassKeeper] User chose to save credentials for', location.hostname); + chrome.runtime.sendMessage({ + type: 'SAVE_CREDENTIALS', + data: { url: location.href, siteName: document.title || location.hostname, username: username, password: password }, + }).catch(function () { }); + dismiss(); + }); + banner.querySelector('#__pk_never__').addEventListener('click', function () { + addToBlocklist(location.hostname); dismiss(); }); } @@ -840,73 +749,57 @@ // ── Form submission watch ───────────────────────────────────────────────────── function watchSubmissions() { - document.addEventListener( - "submit", - async function (e) { - var form = e.target; - var pwField = form.querySelector( - 'input[type="password"]:not([disabled])', - ); - if (!pwField || !pwField.value) return; + document.addEventListener('submit', async function (e) { + var form = e.target; + var pwField = form.querySelector('input[type="password"]:not([disabled])'); + if (!pwField || !pwField.value) return; - var userField = - findUsernameField(pwField) || - form.querySelector('input[type="email"]:not([disabled])') || - form.querySelector('input[type="text"]:not([disabled])'); + var userField = findUsernameField(pwField) + || form.querySelector('input[type="email"]:not([disabled])') + || form.querySelector('input[type="text"]:not([disabled])'); - var username = - (userField && userField.value && userField.value.trim()) || ""; - var password = pwField.value; - if (!username || !password) return; + var username = (userField && userField.value && userField.value.trim()) || ''; + var password = pwField.value; + if (!username || !password) return; - removeDropdown(); + removeDropdown(); - var credentialState = await classifyCredentials(username, password); - console.log( - "[PassKeeper] Credential state for", - location.hostname, - "\u2192", - credentialState, - ); - if (credentialState === "same") return; + // Check blocklist before doing anything else. + if (await isBlocked(location.hostname)) { + console.log('[PassKeeper] Site is blocklisted, skipping save banner:', location.hostname); + return; + } - setTimeout(function () { - showSaveBanner(username, password, credentialState); - }, 500); - }, - true, - ); + var credentialState = await classifyCredentials(username, password); + console.log('[PassKeeper] Credential state for', location.hostname, '\u2192', credentialState); + if (credentialState === 'same') return; + + setTimeout(function () { showSaveBanner(username, password, credentialState); }, 500); + }, true); } // ── Message listener ────────────────────────────────────────────────────────── chrome.runtime.onMessage.addListener(function (msg, _sender, sendResponse) { - if (msg.type === "DO_AUTOFILL") { + if (msg.type === 'DO_AUTOFILL') { doAutofill(msg.username, msg.password); sendResponse({ ok: true }); } - if (msg.type === "VAULT_UPDATED") { + if (msg.type === 'VAULT_UPDATED') { // Items may arrive in the message payload (best-effort), but the source // of truth is now chrome.storage.local which was already written by the popup. var allItems = msg.vault_items || []; if (allItems.length) { _matchingItems = _filterForHost(allItems); - console.log( - "[PassKeeper] VAULT_UPDATED (message): matched=" + - _matchingItems.length + - " of " + - allItems.length, - ); + console.log('[PassKeeper] VAULT_UPDATED (message): matched=' + _matchingItems.length + ' of ' + allItems.length); } // Always re-decorate (also re-reads storage if message had no items). - document.querySelectorAll("[" + PK_ATTR + "]").forEach(function (el) { + document.querySelectorAll('[' + PK_ATTR + ']').forEach(function (el) { var ac = _fieldAbortMap.get(el); if (ac) ac.abort(); el.removeAttribute(PK_ATTR); }); - document.querySelectorAll("." + PK_BTN_CLASS).forEach(function (el) { - el.remove(); - }); + document.querySelectorAll('.' + PK_BTN_CLASS).forEach(function (el) { el.remove(); }); removeDropdown(); decorateFields(); } @@ -923,25 +816,17 @@ // React instantly when the popup writes fresh vault data to local storage. // This fires in the same tick as the write — no message delivery required. chrome.storage.onChanged.addListener(function (changes, area) { - if (area === "local" && changes.vault_items_cs) { - var allItems = changes.vault_items_cs.newValue || []; + if (area === 'local' && changes.vault_items_cs) { + var allItems = (changes.vault_items_cs.newValue) || []; _matchingItems = _filterForHost(allItems); - console.log( - "[PassKeeper] storage.onChanged: matched=" + - _matchingItems.length + - " of " + - allItems.length + - " items", - ); + console.log('[PassKeeper] storage.onChanged: matched=' + _matchingItems.length + ' of ' + allItems.length + ' items'); // Re-decorate all fields with the fresh items. - document.querySelectorAll("[" + PK_ATTR + "]").forEach(function (el) { + document.querySelectorAll('[' + PK_ATTR + ']').forEach(function (el) { var ac = _fieldAbortMap.get(el); if (ac) ac.abort(); el.removeAttribute(PK_ATTR); }); - document.querySelectorAll("." + PK_BTN_CLASS).forEach(function (el) { - el.remove(); - }); + document.querySelectorAll('.' + PK_BTN_CLASS).forEach(function (el) { el.remove(); }); removeDropdown(); decorateFields(); } @@ -955,9 +840,9 @@ _formObserver.observe(document.body, { childList: true, subtree: true }); } - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", init); + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); } else { init(); } -})(); +})(); \ No newline at end of file diff --git a/extension/popup/popup.css b/extension/popup/popup.css index 4fb9394..0bab615 100644 --- a/extension/popup/popup.css +++ b/extension/popup/popup.css @@ -9,8 +9,7 @@ body { width: 320px; - font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 13px; color: #1a1a2e; background: #fff; @@ -22,6 +21,7 @@ body { flex-direction: column; min-height: 0; } + .view.hidden { display: none; } @@ -35,10 +35,12 @@ body { background: linear-gradient(135deg, #c0392b, #922b21); color: #fff; } + .login-logo { font-size: 36px; margin-bottom: 8px; } + .login-header h1 { font-size: 18px; font-weight: 600; @@ -48,6 +50,7 @@ body { .login-body { padding: 18px 20px 20px; } + .pk-hint { font-size: 12px; color: #666; @@ -58,6 +61,7 @@ body { .form-group { margin-bottom: 11px; } + .form-group label { display: block; font-size: 11px; @@ -65,6 +69,7 @@ body { font-weight: 500; margin-bottom: 3px; } + .form-group input { width: 100%; padding: 8px 11px; @@ -74,10 +79,9 @@ body { outline: none; background: #fff; color: #1a1a2e; - transition: - border-color 0.15s, - box-shadow 0.15s; + transition: border-color 0.15s, box-shadow 0.15s; } + .form-group input:focus { border-color: #c0392b; box-shadow: 0 0 0 2px rgba(192, 57, 43, 0.12); @@ -98,13 +102,16 @@ body { margin-top: 10px; transition: background 0.15s; } + .btn-primary:hover { background: #a93226; } + .btn-primary:disabled { background: #e8a49e; cursor: default; } + .btn-sm { width: auto; padding: 6px 16px; @@ -125,6 +132,7 @@ body { margin-top: 6px; transition: background 0.15s; } + .btn-ghost:hover { background: #fdf2f1; } @@ -148,10 +156,12 @@ body { padding: 6px 10px; gap: 7px; } + .search-icon { flex-shrink: 0; opacity: 0.6; } + #vault-search { flex: 1; border: none; @@ -160,6 +170,7 @@ body { outline: none; color: #1a1a2e; } + #vault-search::placeholder { color: #9ca3af; } @@ -181,6 +192,7 @@ body { transition: background 0.15s; flex-shrink: 0; } + .btn-vault-link:hover { background: #2d2d4e; } @@ -201,6 +213,7 @@ body { flex-shrink: 0; transition: background 0.15s; } + .btn-add:hover { background: #e5e7eb; } @@ -215,6 +228,7 @@ body { overflow-x: auto; scrollbar-width: none; } + .vault-tabs::-webkit-scrollbar { display: none; } @@ -229,14 +243,14 @@ body { cursor: pointer; border-bottom: 2px solid transparent; white-space: nowrap; - transition: - color 0.15s, - border-color 0.15s; + transition: color 0.15s, border-color 0.15s; margin-bottom: -1px; } + .tab-btn:hover { color: #1a1a2e; } + .tab-btn.active { color: #1a1a2e; border-bottom-color: #1a1a2e; @@ -250,9 +264,11 @@ body { scrollbar-width: thin; scrollbar-color: #e5e7eb transparent; } + .vault-list::-webkit-scrollbar { width: 4px; } + .vault-list::-webkit-scrollbar-thumb { background: #e5e7eb; border-radius: 2px; @@ -267,9 +283,11 @@ body { cursor: default; transition: background 0.1s; } + .vault-item:hover { background: #f9fafb; } + .vault-item:last-child { border-bottom: none; } @@ -286,15 +304,19 @@ body { flex-shrink: 0; overflow: hidden; } + .item-avatar.color-red { background: #7f1d1d; } + .item-avatar.color-green { background: #14532d; } + .item-avatar.color-purple { background: #3b0764; } + .item-avatar.color-teal { background: #134e4a; } @@ -309,6 +331,7 @@ body { flex: 1; min-width: 0; } + .item-site { font-size: 12px; font-weight: 600; @@ -317,6 +340,7 @@ body { overflow: hidden; text-overflow: ellipsis; } + .item-name { font-size: 11px; color: #9ca3af; @@ -343,14 +367,14 @@ body { display: flex; align-items: center; justify-content: center; - transition: - background 0.12s, - color 0.12s; + transition: background 0.12s, color 0.12s; } + .btn-item-action:hover { background: #f3f4f6; color: #1a1a2e; } + .btn-item-action svg { width: 16px; height: 16px; @@ -378,6 +402,7 @@ body { padding: 7px 10px; margin-bottom: 10px; } + .pk-error.hidden { display: none; } @@ -389,6 +414,7 @@ body { padding: 28px 16px; font-size: 13px; } + .pk-loading.hidden, .pk-empty.hidden { display: none; @@ -400,9 +426,11 @@ body { border-top: 1px solid #f0f0f0; background: #fff; } + .bottom-nav.hidden { display: none; } + .nav-btn { flex: 1; display: flex; @@ -419,9 +447,11 @@ body { font-weight: 500; transition: color 0.15s; } + .nav-btn:hover { color: #374151; } + .nav-btn.active { color: #1a1a2e; } @@ -432,24 +462,32 @@ body { inset: 0; background: rgba(0, 0, 0, 0.45); 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; z-index: 9999; - overflow-y: auto; /* allow scrolling if popup height is small */ - padding: 16px 14px; /* breathing room from top/bottom edges */ + overflow-y: auto; + /* allow scrolling if popup height is small */ + padding: 16px 14px; + /* breathing room from top/bottom edges */ } + .save-overlay.hidden { display: none; } + .save-modal { background: #fff; border-radius: 12px; 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; 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 { display: flex; align-items: center; @@ -459,6 +497,7 @@ body { color: #1a1a2e; margin-bottom: 5px; } + .save-modal-sub { font-size: 11px; color: #6b7280; @@ -466,14 +505,17 @@ body { word-break: break-all; line-height: 1.4; } + .save-modal-actions { display: flex; gap: 8px; margin-top: 10px; } + .save-modal-actions .btn-primary { margin-top: 0; } + .save-modal-actions .btn-ghost { margin-top: 0; } @@ -489,11 +531,10 @@ body { background: #fff; outline: none; cursor: pointer; - transition: - border-color 0.15s, - box-shadow 0.15s; + transition: border-color 0.15s, box-shadow 0.15s; appearance: auto; } + .save-folder-select:focus { border-color: #c0392b; box-shadow: 0 0 0 2px rgba(192, 57, 43, 0.12); @@ -505,31 +546,36 @@ body { border-bottom: 2px solid #16a34a; padding: 12px 14px 10px; } + .gen-suggestion-label { font-size: 10px; color: #374151; font-weight: 500; margin-bottom: 4px; } + .gen-suggestion-row { display: flex; align-items: center; gap: 8px; } + .gen-output { flex: 1; - font-family: "Courier New", Courier, monospace; + font-family: 'Courier New', Courier, monospace; font-size: 13px; font-weight: 700; color: #1a1a2e; word-break: break-all; min-width: 0; } + .gen-suggestion-actions { display: flex; gap: 4px; flex-shrink: 0; } + .gen-icon-btn { background: none; border: none; @@ -542,6 +588,7 @@ body { justify-content: center; transition: background 0.12s; } + .gen-icon-btn:hover { background: rgba(26, 115, 232, 0.1); } @@ -550,6 +597,7 @@ body { padding: 6px 14px 2px; min-height: 22px; } + .gen-strength-label { font-size: 12px; font-weight: 700; @@ -561,16 +609,19 @@ body { flex-direction: column; gap: 10px; } + .gen-row { display: flex; align-items: center; gap: 6px; } + .gen-label { font-size: 12px; color: #374151; white-space: nowrap; } + .gen-length-num { width: 42px; padding: 3px 5px; @@ -581,15 +632,18 @@ body { color: #1a1a2e; outline: none; } + .gen-length-num:focus { border-color: #1a73e8; } + .gen-slider { width: 100%; accent-color: #1a73e8; cursor: pointer; height: 4px; } + .gen-check { display: flex; align-items: center; @@ -599,6 +653,7 @@ body { cursor: pointer; user-select: none; } + .gen-check input[type="checkbox"] { width: 15px; height: 15px; @@ -606,3 +661,244 @@ body { cursor: pointer; 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); +} \ No newline at end of file diff --git a/extension/popup/popup.html b/extension/popup/popup.html index 6bac1fb..8be2d21 100644 --- a/extension/popup/popup.html +++ b/extension/popup/popup.html @@ -1,417 +1,339 @@ - + - - - PassKeeper - - - -
- -