diff --git a/extensions/extension/manifest.json b/extensions/extension/manifest.json index c771e04..c2271a5 100644 --- a/extensions/extension/manifest.json +++ b/extensions/extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "gnexus-creds", - "version": "0.1.1", + "version": "0.2.0", "description": "Browser extension for gnexus-creds secret manager.", "permissions": ["storage", "activeTab", "alarms"], "host_permissions": ["https://*/*", "http://*/*"], diff --git a/extensions/extension/pack-src/config.json b/extensions/extension/pack-src/config.json index 9bd18d3..0a8d3e0 100644 --- a/extensions/extension/pack-src/config.json +++ b/extensions/extension/pack-src/config.json @@ -7,5 +7,76 @@ "use": "Use", "dismiss": "Dismiss", "selectAccount": "Select account" + }, + "stringsByLocale": { + "en": { + "use": "Use", + "dismiss": "Dismiss", + "selectAccount": "Select account", + "drawerTitle": "Settings", + "serverUrl": "Server URL", + "apiToken": "API token", + "tokenHint": "Create a token with read, reveal, write scopes on the website.", + "save": "Save", + "openApp": "Open gnexus-creds", + "openAppTitle": "Open gnexus-creds", + "refresh": "Refresh", + "settingsTitle": "Settings", + "searchPlaceholder": "Search secrets...", + "noSecrets": "No secrets", + "saveCredentials": "Save credentials for {source}?", + "account": "Account:", + "credentialsSaved": "Credentials saved", + "enterToken": "Enter API token", + "settingsSaved": "Settings saved", + "error": "Error", + "openOnSite": "Open on gnexus-creds" + }, + "uk": { + "use": "Використати", + "dismiss": "Приховати", + "selectAccount": "Виберіть акаунт", + "drawerTitle": "Налаштування", + "serverUrl": "Адреса сервера", + "apiToken": "API-токен", + "tokenHint": "Створіть токен зі scopes read, reveal, write на сайті.", + "save": "Зберегти", + "openApp": "Відкрити gnexus-creds", + "openAppTitle": "Відкрити gnexus-creds", + "refresh": "Оновити", + "settingsTitle": "Налаштування", + "searchPlaceholder": "Пошук секретів...", + "noSecrets": "Немає секретів", + "saveCredentials": "Зберегти дані для {source}?", + "account": "Акаунт:", + "credentialsSaved": "Дані збережено", + "enterToken": "Вкажіть API-токен", + "settingsSaved": "Налаштування збережено", + "error": "Помилка", + "openOnSite": "Відкрити на gnexus-creds" + }, + "ru": { + "use": "Использовать", + "dismiss": "Скрыть", + "selectAccount": "Выберите аккаунт", + "drawerTitle": "Настройки", + "serverUrl": "URL сервера", + "apiToken": "API-токен", + "tokenHint": "Создайте токен со scopes read, reveal, write на сайте.", + "save": "Сохранить", + "openApp": "Открыть gnexus-creds", + "openAppTitle": "Открыть gnexus-creds", + "refresh": "Обновить", + "settingsTitle": "Настройки", + "searchPlaceholder": "Поиск секретов...", + "noSecrets": "Нет секретов", + "saveCredentials": "Сохранить данные для {source}?", + "account": "Аккаунт:", + "credentialsSaved": "Данные сохранены", + "enterToken": "Введите API-токен", + "settingsSaved": "Настройки сохранены", + "error": "Ошибка", + "openOnSite": "Открыть на gnexus-creds" + } } -} +} \ No newline at end of file diff --git a/extensions/extension/src/background.js b/extensions/extension/src/background.js index 64308c2..99d4472 100644 --- a/extensions/extension/src/background.js +++ b/extensions/extension/src/background.js @@ -52,6 +52,27 @@ }); } +// --- Shared UI language ------------------------------------------------------- +// The service backend decides the UI language (locale_effective on GET +// /api/v1/me = settings override, else the auth-account locale). The +// background caches it in chrome.storage.local.uiLocale for the popup and +// the autofill card; strings themselves ship in the design pack. +async function syncUiLocale(reason) { + const { token, baseUrl } = await getSettings(); + if (!token) { + await chrome.storage.local.remove("uiLocale"); + return; + } + try { + const me = await getMe(token, baseUrl); + const locale = String(me?.locale_effective || me?.locale || "en").trim().toLowerCase(); + await chrome.storage.local.set({ uiLocale: locale || "en" }); + if (reason) console.debug(`[gnexus-creds] ui locale (${reason}):`, locale); + } catch { + // keep the previously cached locale + } +} + chrome.alarms.create("refresh-secrets-cache", { periodInMinutes: 15 }); // --- Design-resource pack sync ---------------------------------------------- @@ -147,9 +168,11 @@ // These top-level registrations survive the MV3 service worker sleeping. chrome.runtime.onInstalled.addListener((details) => { refreshDesignPack(details.reason === "update" ? "update" : "install"); + syncUiLocale("install"); }); chrome.runtime.onStartup.addListener(() => { refreshDesignPack("startup"); + syncUiLocale("startup"); }); chrome.alarms.onAlarm.addListener(async (alarm) => { @@ -159,6 +182,7 @@ } catch { // ignore — no token or network error } + await syncUiLocale("alarm"); }); chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { @@ -167,7 +191,11 @@ const { token, baseUrl } = await getSettings(); switch (message.type) { case "GET_SETTINGS": - sendResponse({ ok: true, data: await getSettings() }); + { + sendResponse({ ok: true, data: await getSettings() }); + // every popup open re-syncs the cached UI language (cheap /me) + syncUiLocale("popup"); + } break; case "SAVE_SETTINGS": await setSettings(message.payload); @@ -177,6 +205,13 @@ case "VERIFY_TOKEN": { const me = await getMe(message.payload.token, message.payload.baseUrl); + // cache the per-user UI language while we hold a fresh /me + try { + const locale = String(me?.locale_effective || me?.locale || "en").trim().toLowerCase(); + await chrome.storage.local.set({ uiLocale: locale || "en" }); + } catch { + // ignore + } sendResponse({ ok: true, data: me }); } break; diff --git a/extensions/extension/src/content.js b/extensions/extension/src/content.js index 1d17197..84a1361 100644 --- a/extensions/extension/src/content.js +++ b/extensions/extension/src/content.js @@ -17,8 +17,16 @@ pack: null, config: GnPack.DEFAULT_CONFIG, tables: GnCredsDetect.keywordTables, + uiLocale: "", // resolved from the backend by the background worker }; +// Localized strings for the card: pack stringsByLocale[uiLocale] over the +// legacy/bundled defaults. Read per card build so a locale change on the +// backend shows up without reloading the page. +function cardStrings() { + return GnPack.resolveLocalized(packState.config, packState.uiLocale); +} + // --- Messaging helper --- function send(type, payload) { @@ -189,25 +197,27 @@ if (secrets.length === 1) { const secret = secrets[0]; + const strings = cardStrings(); card.innerHTML = `
${escapeHtml(secret.title)}
- - + +
`; } else { + const strings = cardStrings(); const rows = secrets.map((s) => `
${escapeHtml(s.title)}
- +
`).join(""); card.innerHTML = ` -
${escapeHtml(packState.config.strings.selectAccount)}
+
${escapeHtml(strings.selectAccount)}
${rows}
- +
`; } @@ -385,6 +395,13 @@ } catch (err) { console.debug("[gnexus-creds] design pack unavailable, using bundled defaults:", err?.message); } + try { + packState.uiLocale = + String((await chrome.storage.local.get("uiLocale")).uiLocale || "").trim().toLowerCase(); + } catch { + // no storage access — English bundled strings + packState.uiLocale = ""; + } if (document.readyState === "loading") { await new Promise((resolve) => document.addEventListener("DOMContentLoaded", resolve, { once: true }) diff --git a/extensions/extension/src/pack-shared.js b/extensions/extension/src/pack-shared.js index b11aee9..cac9410 100644 --- a/extensions/extension/src/pack-shared.js +++ b/extensions/extension/src/pack-shared.js @@ -164,24 +164,66 @@ } const TIMING_LIMITS = { scanDebounceMs: [30, 2000], cardExitMs: [50, 2000] }; - const STRING_KEYS = ["use", "dismiss", "selectAccount"]; + const STRING_KEYS = [ + "use", "dismiss", "selectAccount", + // popup UI + "drawerTitle", "serverUrl", "apiToken", "tokenHint", "save", "openApp", + "openAppTitle", "refresh", "settingsTitle", "searchPlaceholder", + "noSecrets", "saveCredentials", "account", "credentialsSaved", + "enterToken", "settingsSaved", "error", "openOnSite", + ]; + const LOCALIZED_LOCALES = ["en", "uk", "ru"]; + + // English strings as shipped pre-localization; every other locale in + // stringsByLocale must cover the same keys (falls back to these). + const DEFAULT_STRINGS = { + use: "Use", + dismiss: "Dismiss", + selectAccount: "Select account", + drawerTitle: "Settings", + serverUrl: "Server URL", + apiToken: "API token", + tokenHint: "Create a token with read, reveal, write scopes on the website.", + save: "Save", + openApp: "Open gnexus-creds", + openAppTitle: "Open gnexus-creds", + refresh: "Refresh", + settingsTitle: "Settings", + searchPlaceholder: "Search secrets...", + noSecrets: "No secrets", + saveCredentials: "Save credentials for {source}?", + account: "Account:", + credentialsSaved: "Credentials saved", + enterToken: "Enter API token", + settingsSaved: "Settings saved", + error: "Error", + openOnSite: "Open on gnexus-creds", + }; + + const DEFAULT_STRINGS_BY_LOCALE = { + en: DEFAULT_STRINGS, + uk: {}, + ru: {}, + }; function resolveString(value, fallback) { if (typeof value !== "string") return fallback; const trimmed = value.trim(); - if (!trimmed || trimmed.length > 60) return fallback; + if (!trimmed || trimmed.length > 200) return fallback; return trimmed; } /** * A pack config object may only carry keys the code already consumes: - * two timings and the three card strings. Anything else is ignored; - * out-of-range timings and empty/oversized strings fall back to defaults. + * two timings, legacy card strings, and per-locale strings. Anything + * else is ignored; out-of-range timings and empty/oversized strings + * fall back to defaults. */ function resolveConfig(raw) { const resolved = { timings: Object.assign({}, DEFAULT_CONFIG.timings), strings: Object.assign({}, DEFAULT_CONFIG.strings), + stringsByLocale: Object.assign({}, DEFAULT_STRINGS_BY_LOCALE), }; if (!isPlainObject(raw)) return resolved; if (isPlainObject(raw.timings)) { @@ -194,12 +236,43 @@ } if (isPlainObject(raw.strings)) { for (const key of STRING_KEYS) { - resolved.strings[key] = resolveString(raw.strings[key], DEFAULT_CONFIG.strings[key]); + // never materialize keys the pack didn't send — undefined values + // would leak into consumers that spread this over the defaults + if (raw.strings[key] === undefined) continue; + // ultimate fallback is the bundled English default + const prior = resolved.strings[key] ?? DEFAULT_STRINGS[key]; + resolved.strings[key] = resolveString(raw.strings[key], prior); + } + } + if (isPlainObject(raw.stringsByLocale)) { + for (const locale of LOCALIZED_LOCALES) { + const rawStrings = raw.stringsByLocale[locale]; + // per-key fallback is the bundled English default (DEFAULT_STRINGS) + // — the merge may only override known keys with valid strings. + const merged = Object.assign({}, DEFAULT_STRINGS_BY_LOCALE[locale]); + if (isPlainObject(rawStrings)) { + for (const key of STRING_KEYS) { + merged[key] = resolveString(rawStrings[key], DEFAULT_STRINGS[key]); + } + } + resolved.stringsByLocale[locale] = merged; } } return resolved; } + /** + * Strings for one UI language: the per-locale pack dictionary (already + * resolved and clamped) over the legacy/bundled defaults. Unknown or + * empty lang falls back to English via DEFAULT_STRINGS. + */ + function resolveLocalized(config, lang) { + const source = config || DEFAULT_CONFIG; + const localized = source.stringsByLocale instanceof Object ? source.stringsByLocale : {}; + const byLocale = LOCALIZED_LOCALES.includes(lang) ? localized[lang] : null; + return Object.assign({}, DEFAULT_STRINGS, source.strings || {}, byLocale || {}); + } + // Shape validation with no clamping here — values were verified against // pack.json hashes in the background before this was stored. function validateStoredPack(stored, ownVersion) { @@ -262,6 +335,7 @@ EMPTY_TABLES, isCompatible, resolveConfig, + resolveLocalized, resolveTables, validateStoredPack, load, diff --git a/extensions/extension/src/popup/popup.js b/extensions/extension/src/popup/popup.js index ca7267c..f665a37 100644 --- a/extensions/extension/src/popup/popup.js +++ b/extensions/extension/src/popup/popup.js @@ -31,23 +31,58 @@ const openSiteBtn = document.getElementById("open-site"); const openSettingsBtn = document.getElementById("open-settings"); +// UI language: the backend-decided locale (cached by the background) picks +// the dictionary from the design pack; unknown locale falls back to English. +// popup.js is a module, so the top-level await completes before the drawer +// below is created or any UI renders. +async function loadStrings() { + try { + const uiLocale = String( + (await chrome.storage.local.get("uiLocale")).uiLocale || "", + ).trim().toLowerCase(); + const stored = (await chrome.storage.local.get("designPack")).designPack; + const pack = window.GnPack + ? GnPack.validateStoredPack(stored, chrome.runtime.getManifest().version) + : null; + const config = pack?.texts?.["config.json"] + ? GnPack.resolveConfig(JSON.parse(pack.texts["config.json"])) + : GnPack.DEFAULT_CONFIG; + return { uiLocale, strings: GnPack.resolveLocalized(config, uiLocale) }; + } catch { + return { uiLocale: "", strings: GnPack.resolveLocalized(GnPack.DEFAULT_CONFIG, "") }; + } +} + +const applyStaticStrings = (loc) => { + const byId = (id) => document.getElementById(id); + if (byId("refresh")) byId("refresh").title = loc.refresh; + if (byId("open-site")) byId("open-site").title = loc.openAppTitle; + if (byId("open-settings")) byId("open-settings").title = loc.settingsTitle; + if (byId("search")) byId("search").placeholder = loc.searchPlaceholder; + if (document.querySelector("#empty-state .empty-state-text")) { + document.querySelector("#empty-state .empty-state-text").textContent = loc.noSecrets; + } +}; + +const { uiLocale, strings: loc } = await loadStrings(); +document.documentElement.lang = uiLocale || "en"; +applyStaticStrings(loc); + const settingsDrawer = Drawer.create("settings-drawer", { - title: "Settings", + title: loc.drawerTitle, body: () => { const div = document.createElement("div"); div.innerHTML = `
-
-
@@ -56,9 +91,9 @@ return div; }, actions: () => { - const saveBtn = Helper.template.createElement("button", { class: "btn btn-primary" }, "Save"); + const saveBtn = Helper.template.createElement("button", { class: "btn btn-primary" }, loc.save); saveBtn.id = "save-settings"; - const openBtn = Helper.template.createElement("button", { class: "btn btn-secondary" }, "Open gnexus-creds"); + const openBtn = Helper.template.createElement("button", { class: "btn btn-secondary" }, loc.openApp); openBtn.id = "open-site-settings"; return [saveBtn, openBtn]; }, @@ -111,13 +146,13 @@ const username = usernameField?.value || ""; card.innerHTML = ` -
Save credentials for ${escapeHtml(pending.source)}?
+
${escapeHtml(loc.saveCredentials.replace("{source}", pending.source || ""))}
-

Account: ${escapeHtml(username)}

+

${escapeHtml(loc.account)} ${escapeHtml(username)}

`; @@ -139,7 +174,7 @@ pendingSaveWrap.classList.add("d-none"); pendingSaveWrap.innerHTML = ""; mainErrorWrap.classList.remove("d-none"); - showWrapAlert(mainErrorWrap, "success", "Credentials saved"); + showWrapAlert(mainErrorWrap, "success", loc.credentialsSaved); loadSecrets(); } catch (err) { mainErrorWrap.classList.remove("d-none"); @@ -234,7 +269,7 @@ } catch (err) { secretsList.innerHTML = ""; emptyState.classList.remove("d-none"); - emptyState.querySelector(".empty-state-text").textContent = `Error: ${err.message}`; + emptyState.querySelector(".empty-state-text").textContent = `${loc.error}: ${err.message}`; } finally { loaderWrap.classList.add("d-none"); } @@ -267,7 +302,7 @@
${escapeHtml(secret.title)}
${meta ? `
${meta}
` : ""} - `; @@ -285,14 +320,14 @@ const token = tokenInput.value.trim(); if (!token) { - showWrapAlert(settingsErrorWrap, "error", "Enter API token"); + showWrapAlert(settingsErrorWrap, "error", loc.enterToken); return; } try { await sendMessage("VERIFY_TOKEN", { token, baseUrl }); await sendMessage("SAVE_SETTINGS", { token, baseUrl }); - showWrapAlert(settingsSuccessWrap, "success", "Settings saved"); + showWrapAlert(settingsSuccessWrap, "success", loc.settingsSaved); tokenInput.value = ""; setTimeout(() => { settingsDrawer.close(); diff --git a/extensions/extension/tools/test-pack-shared.js b/extensions/extension/tools/test-pack-shared.js index dc9d922..b279c5c 100644 --- a/extensions/extension/tools/test-pack-shared.js +++ b/extensions/extension/tools/test-pack-shared.js @@ -33,7 +33,27 @@ }); check("defaults on junk config", pack.resolveConfig("not an object").timings, { scanDebounceMs: 300, cardExitMs: 280 }); check("junk string falls back", pack.resolveConfig({ strings: { use: 42 } }).strings.use, "Use"); - check("oversized string falls back", pack.resolveConfig({ strings: { use: "x".repeat(61) } }).strings.use.length, "Use".length); + check("no undefined keys materialized", Object.keys(pack.resolveConfig({ strings: {} }).strings).length, Object.keys(pack.DEFAULT_CONFIG.strings).length); + check("oversized string falls back", pack.resolveConfig({ strings: { use: "x".repeat(201) } }).strings.use.length, "Use".length); + check("string within cap kept (tokenHint is 63)", pack.resolveConfig({ strings: { tokenHint: "x".repeat(63) } }).strings.tokenHint.length, 63); + + // --- resolveLocalized: per-locale dictionaries -------------------------------- + { + const cfg = pack.resolveConfig({ stringsByLocale: { + en: null, + ru: { use: " Использовать ", junkKey: "!", error: "" }, + uk: "junk", + } }); + check("unknown lang -> defaults", pack.resolveLocalized(cfg, "de").use, "Use"); + check("empty lang -> defaults", pack.resolveLocalized(cfg, "").error, "Error"); + check("ru sanitized and defaulted", { + use: pack.resolveLocalized(cfg, "ru").use, + error: pack.resolveLocalized(cfg, "ru").error, + dismiss: pack.resolveLocalized(cfg, "ru").dismiss, + }, { use: "Использовать", error: "Error", dismiss: "Dismiss" }); + check("uk junk locale falls back", pack.resolveLocalized(cfg, "uk").selectAccount, "Select account"); + check("junk config -> english defaults", pack.resolveLocalized(null, "ru").use, "Use"); + } } // --- resolveTables: additive merge over bundled defaults --------------------- diff --git a/extensions/pack/pack.json b/extensions/pack/pack.json index ade95a2..1235afb 100644 --- a/extensions/pack/pack.json +++ b/extensions/pack/pack.json @@ -1,8 +1,8 @@ { - "packVersion": 13, - "minExtensionVersion": "0.1.1", + "packVersion": 18, + "minExtensionVersion": "0.2.0", "maxExtensionVersion": null, - "publishedAt": "2026-10-02T07:12:50.458Z", + "publishedAt": "2026-10-02T08:43:57.810Z", "files": { "content.css": { "sha256": "e17800f82cbde350b6fe3c60746136260844ede20a5f1d4fbfd2d4930fcd6005", @@ -17,8 +17,8 @@ "size": 2211 }, "config.json": { - "sha256": "89dc827789c9901d96d17842bca85418f19757c589ff4c80c2dde4bc52d38e64", - "size": 160 + "sha256": "19c7b3cfc7b42da9f87dcb6d5d9a55af8be976b8a1b1ab3ad4cd75c62a3b1ba9", + "size": 2973 } } } diff --git a/extensions/pack/v18/config.json b/extensions/pack/v18/config.json new file mode 100644 index 0000000..0a8d3e0 --- /dev/null +++ b/extensions/pack/v18/config.json @@ -0,0 +1,82 @@ +{ + "timings": { + "scanDebounceMs": 300, + "cardExitMs": 280 + }, + "strings": { + "use": "Use", + "dismiss": "Dismiss", + "selectAccount": "Select account" + }, + "stringsByLocale": { + "en": { + "use": "Use", + "dismiss": "Dismiss", + "selectAccount": "Select account", + "drawerTitle": "Settings", + "serverUrl": "Server URL", + "apiToken": "API token", + "tokenHint": "Create a token with read, reveal, write scopes on the website.", + "save": "Save", + "openApp": "Open gnexus-creds", + "openAppTitle": "Open gnexus-creds", + "refresh": "Refresh", + "settingsTitle": "Settings", + "searchPlaceholder": "Search secrets...", + "noSecrets": "No secrets", + "saveCredentials": "Save credentials for {source}?", + "account": "Account:", + "credentialsSaved": "Credentials saved", + "enterToken": "Enter API token", + "settingsSaved": "Settings saved", + "error": "Error", + "openOnSite": "Open on gnexus-creds" + }, + "uk": { + "use": "Використати", + "dismiss": "Приховати", + "selectAccount": "Виберіть акаунт", + "drawerTitle": "Налаштування", + "serverUrl": "Адреса сервера", + "apiToken": "API-токен", + "tokenHint": "Створіть токен зі scopes read, reveal, write на сайті.", + "save": "Зберегти", + "openApp": "Відкрити gnexus-creds", + "openAppTitle": "Відкрити gnexus-creds", + "refresh": "Оновити", + "settingsTitle": "Налаштування", + "searchPlaceholder": "Пошук секретів...", + "noSecrets": "Немає секретів", + "saveCredentials": "Зберегти дані для {source}?", + "account": "Акаунт:", + "credentialsSaved": "Дані збережено", + "enterToken": "Вкажіть API-токен", + "settingsSaved": "Налаштування збережено", + "error": "Помилка", + "openOnSite": "Відкрити на gnexus-creds" + }, + "ru": { + "use": "Использовать", + "dismiss": "Скрыть", + "selectAccount": "Выберите аккаунт", + "drawerTitle": "Настройки", + "serverUrl": "URL сервера", + "apiToken": "API-токен", + "tokenHint": "Создайте токен со scopes read, reveal, write на сайте.", + "save": "Сохранить", + "openApp": "Открыть gnexus-creds", + "openAppTitle": "Открыть gnexus-creds", + "refresh": "Обновить", + "settingsTitle": "Настройки", + "searchPlaceholder": "Поиск секретов...", + "noSecrets": "Нет секретов", + "saveCredentials": "Сохранить данные для {source}?", + "account": "Аккаунт:", + "credentialsSaved": "Данные сохранены", + "enterToken": "Введите API-токен", + "settingsSaved": "Настройки сохранены", + "error": "Ошибка", + "openOnSite": "Открыть на gnexus-creds" + } + } +} \ No newline at end of file diff --git a/extensions/pack/v18/content.css b/extensions/pack/v18/content.css new file mode 100644 index 0000000..e508290 --- /dev/null +++ b/extensions/pack/v18/content.css @@ -0,0 +1,116 @@ +/* Card styles for the closed Shadow DOM root (content.js injects this file's + text into the shadow root; it is never added to the page stylesheet, so the + host page can neither restyle the card nor see our class names). + Palette and geometry mirror gnexus-ui-kit 1.0 — keep in sync with lib/. */ + +.gnexus-creds-autofill-container { + position: fixed; + top: 16px; + left: 50%; + transform: translateX(-50%); + z-index: 2147483647; + display: flex; + flex-direction: column; + gap: 8px; + width: max-content; + max-width: calc(100% - 32px); + pointer-events: none; +} + +.gnexus-creds-autofill-card { + pointer-events: auto; + background: #16161e; + border: 2px solid rgba(192, 202, 245, 0.24); + border-left: 6px solid #7aa2f7; + color: #c0caf5; + font-family: 'IBM Plex Mono', ui-monospace, monospace; + font-size: 14px; + border-radius: 6px; + box-shadow: 0 14px 36px rgba(22, 22, 30, 0.36); + padding: 15px 18px; + min-width: 300px; + max-width: 460px; + width: 100%; + opacity: 0; + transform: translateY(-12px); + transition: opacity 0.28s ease, transform 0.28s ease; +} + +.gnexus-creds-autofill-card-visible { + opacity: 1; + transform: translateY(0); +} + +.gnexus-creds-autofill-card-exit { + opacity: 0; + transform: translateY(-12px); + transition: opacity 0.25s ease-in, transform 0.25s ease-in; +} + +.gnexus-creds-autofill-title { + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + color: #7aa2f7; + line-height: 1.25; + word-break: break-word; + margin: 0 0 12px; + font-size: 14px; +} + +.gnexus-creds-autofill-list { + max-height: 200px; + overflow-y: auto; + margin-bottom: 12px; +} + +.gnexus-creds-autofill-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 10px 0; + border-bottom: 2px solid rgba(192, 202, 245, 0.08); +} + +.gnexus-creds-autofill-row:last-child { + border-bottom: none; +} + +.gnexus-creds-autofill-row-title { + font-size: 13px; + color: #c0caf5; + word-break: break-word; +} + +.gnexus-creds-autofill-actions { + display: flex; + gap: 10px; + justify-content: flex-end; +} + +.gnexus-creds-autofill-btn { + background: #16161e; + border: 2px solid rgba(192, 202, 245, 0.24); + border-left: 6px solid #7aa2f7; + color: #7aa2f7; + padding: 8px 16px; + border-radius: 6px; + font-family: inherit; + font-size: 13px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + cursor: pointer; + line-height: 1; + flex-shrink: 0; +} + +.gnexus-creds-autofill-btn-secondary { + border-left-width: 2px; + color: #a9b1d6; +} + +.gnexus-creds-autofill-btn:hover { + opacity: 0.85; +} \ No newline at end of file diff --git a/extensions/pack/v18/popup.css b/extensions/pack/v18/popup.css new file mode 100644 index 0000000..ecb886d --- /dev/null +++ b/extensions/pack/v18/popup.css @@ -0,0 +1,22 @@ +body { + width: 420px; + min-height: 500px; +} + +.page-header .page-header-content { + display: flex; + flex-direction: row; + align-items: center; +} + +.page-header-logo { + width: 28px; + height: 28px; + margin-right: 8px; +} + +.list-subtitle { + display: flex; + flex-wrap: wrap; + gap: 4px; +} diff --git a/extensions/pack/v18/tables.json b/extensions/pack/v18/tables.json new file mode 100644 index 0000000..c22d391 --- /dev/null +++ b/extensions/pack/v18/tables.json @@ -0,0 +1,255 @@ +{ + "usernameKeywords": [ + [ + "username", + 6 + ], + [ + "user_name", + 6 + ], + [ + "user_id", + 4 + ], + [ + "userid", + 4 + ], + [ + "login", + 5 + ], + [ + "log_in", + 3 + ], + [ + "logon", + 3 + ], + [ + "signin", + 2 + ], + [ + "email", + 5 + ], + [ + "e_mail", + 5 + ], + [ + "mail", + 3 + ], + [ + "correo", + 4 + ], + [ + "courriel", + 4 + ], + [ + "phone", + 4 + ], + [ + "tel", + 2 + ], + [ + "telefon", + 4 + ], + [ + "mobile", + 2 + ], + [ + "account", + 2 + ], + [ + "nick", + 3 + ], + [ + "handle", + 2 + ], + [ + "member", + 2 + ], + [ + "benutzer", + 5 + ], + [ + "benutzername", + 6 + ], + [ + "kennung", + 3 + ], + [ + "логин", + 6 + ], + [ + "пользователь", + 5 + ], + [ + "юзер", + 4 + ], + [ + "почта", + 5 + ], + [ + "телефон", + 4 + ], + [ + "usuario", + 5 + ], + [ + "utilisateur", + 4 + ] + ], + "passwordKeywords": [ + [ + "password", + 8 + ], + [ + "passwd", + 8 + ], + [ + "pwd", + 8 + ], + [ + "pass", + 7 + ], + [ + "pw", + 5 + ], + [ + "passwort", + 8 + ], + [ + "motdepasse", + 8 + ], + [ + "mot_de_passe", + 8 + ], + [ + "contrasena", + 8 + ], + [ + "contrasenia", + 8 + ], + [ + "senha", + 8 + ], + [ + "haslo", + 6 + ], + [ + "geslo", + 6 + ], + [ + "пароль", + 8 + ], + [ + "пассворд", + 6 + ] + ], + "antiKeywords": [ + "cc", + "card", + "cvc", + "cvv", + "security_code", + "search", + "query", + "filter", + "coupon", + "promo", + "newsletter", + "subscribe", + "subject", + "message", + "comment", + "first_name", + "lastname", + "last_name", + "fname", + "lname", + "birth", + "dob", + "zip", + "postal", + "amount", + "price", + "total", + "qty", + "country", + "city", + "street", + "address", + "otp", + "one_time", + "onetime", + "one_time_code", + "verification", + "confirm", + "repeat", + "retype", + "again" + ], + "autocompleteTokens": { + "username": { + "username": 9, + "password": 0 + }, + "current_password": { + "username": 0, + "password": 9 + }, + "new_password": { + "username": 0, + "password": 7 + }, + "email": { + "username": 7, + "password": 0 + }, + "tel": { + "username": 3, + "password": 0 + } + } +}