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 =
- '";
+ 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 = '';
- 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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
PassKeeper
+
+
+
+
+