diff --git a/dist/index.html b/dist/index.html index f0d324b..732195d 100644 --- a/dist/index.html +++ b/dist/index.html @@ -111,6 +111,14 @@ var STALE_REACTION = new class StaleReactionError extends Error { message = "The reaction that called `getAbortSignal()` was re-run or destroyed"; }(); var IS_XHTML = !!globalThis.document?.contentType && /* @__PURE__ */ globalThis.document.contentType.includes("xml"); +/** +* `%name%(...)` can only be used during component initialisation +* @param {string} name +* @returns {never} +*/ +function lifecycle_outside_component(name) { + throw new Error(`https://svelte.dev/e/lifecycle_outside_component`); +} //#endregion //#region node_modules/svelte/src/internal/client/errors.js /** @@ -4767,6 +4775,53 @@ function observe_all(context, props) { props(); } if (typeof HTMLElement === "function"); +/** +* `onMount`, like [`$effect`](https://svelte.dev/docs/svelte/$effect), schedules a function to run as soon as the component has been mounted to the DOM. +* Unlike `$effect`, the provided function only runs once. +* +* It must be called during the component's initialisation (but doesn't need to live _inside_ the component; +* it can be called from an external module). If a function is returned _synchronously_ from `onMount`, +* it will be called when the component is unmounted. +* +* `onMount` functions do not run during [server-side rendering](https://svelte.dev/docs/svelte/svelte-server#render). +* +* @template T +* @param {() => NotFunction | Promise> | (() => any)} fn +* @returns {void} +*/ +function onMount(fn) { + if (component_context === null) lifecycle_outside_component("onMount"); + if (legacy_mode_flag && component_context.l !== null) init_update_callbacks(component_context).m.push(fn); + else user_effect(() => { + const cleanup = untrack(fn); + if (typeof cleanup === "function") return cleanup; + }); +} +/** +* Schedules a callback to run immediately before the component is unmounted. +* +* Out of `onMount`, `beforeUpdate`, `afterUpdate` and `onDestroy`, this is the +* only one that runs inside a server-side component. +* +* @param {() => any} fn +* @returns {void} +*/ +function onDestroy(fn) { + if (component_context === null) lifecycle_outside_component("onDestroy"); + onMount(() => () => untrack(fn)); +} +/** +* Legacy-mode: Init callbacks object for onMount/beforeUpdate/afterUpdate +* @param {ComponentContext} context +*/ +function init_update_callbacks(context) { + var l = context.l; + return l.u ??= { + a: [], + b: [], + m: [] + }; +} //#endregion //#region node_modules/svelte/src/internal/disclose-version.js if (typeof window !== "undefined") ((window.__svelte ??= {}).v ??= /* @__PURE__ */ new Set()).add("5"); @@ -5018,6 +5073,7 @@ function generateId() { * @property {string} title - Display name (e.g. "GitHub", "Gmail") * @property {string} [username] - Login username or email (optional) * @property {string} [encryptedPassword] - AES-GCM encrypted password blob (JSON string); optional +* @property {string} [encryptedTotpSecret] - AES-GCM encrypted TOTP base32 secret (JSON string); optional * @property {string} [url] - Website URL * @property {string} [notes] - Free-form notes * @property {string} [groupId] - Reference to a Group id (empty string = no group) @@ -5032,6 +5088,7 @@ function generateId() { * @param {string} data.title * @param {string} [data.username] * @param {string} [data.encryptedPassword] - Must already be encrypted (optional; empty string = no password) +* @param {string} [data.encryptedTotpSecret] - Must already be encrypted (optional; empty = no TOTP) * @param {string} [data.url] * @param {string} [data.notes] * @param {string} [data.groupId] @@ -5045,6 +5102,7 @@ function createEntry(data) { title: data.title.trim(), username: data.username?.trim() || "", encryptedPassword: data.encryptedPassword, + encryptedTotpSecret: data.encryptedTotpSecret, url: data.url?.trim() || "", notes: data.notes?.trim() || "", groupId: data.groupId || "", @@ -5066,6 +5124,7 @@ function updateEntry$1(existing, data) { title: data.title !== void 0 ? data.title.trim() : existing.title, username: data.username !== void 0 ? data.username?.trim() || "" : existing.username, encryptedPassword: data.encryptedPassword !== void 0 ? data.encryptedPassword : existing.encryptedPassword, + encryptedTotpSecret: data.encryptedTotpSecret !== void 0 ? data.encryptedTotpSecret : existing.encryptedTotpSecret, url: data.url !== void 0 ? data.url.trim() : existing.url, notes: data.notes !== void 0 ? data.notes.trim() : existing.notes, groupId: data.groupId !== void 0 ? data.groupId : existing.groupId, @@ -5650,8 +5709,8 @@ async function exportSelected(groupIds = null, options = {}) { sealKey = await deriveKey(password, exportSalt); envelopeSalt = uint8ArrayToBase64(exportSalt); for (const entry of payload.entries) { - if (!entry.encryptedPassword) continue; - entry.encryptedPassword = await encrypt(await decrypt(entry.encryptedPassword, vaultKey), sealKey); + if (entry.encryptedPassword) entry.encryptedPassword = await encrypt(await decrypt(entry.encryptedPassword, vaultKey), sealKey); + if (entry.encryptedTotpSecret) entry.encryptedTotpSecret = await encrypt(await decrypt(entry.encryptedTotpSecret, vaultKey), sealKey); } payload.meta.salt = envelopeSalt; } @@ -5709,11 +5768,16 @@ async function importAll(data, mode = "merge", sourcePassword = "", targetKey = } for (const entry of data.entries) try { let reencryptedEntry = { ...entry }; - if (sourceKey && targetKey && entry.encryptedPassword) reencryptedEntry.encryptedPassword = await encrypt(await decrypt(entry.encryptedPassword, sourceKey), targetKey); - else if (!sourceKey || !targetKey) { - console.warn("Skipping entry (missing source password or target key):", entry.title); - skipped++; - continue; + const hasEncrypted = !!(entry.encryptedPassword || entry.encryptedTotpSecret); + if (sourceKey && targetKey && hasEncrypted) { + if (entry.encryptedPassword) reencryptedEntry.encryptedPassword = await encrypt(await decrypt(entry.encryptedPassword, sourceKey), targetKey); + if (entry.encryptedTotpSecret) reencryptedEntry.encryptedTotpSecret = await encrypt(await decrypt(entry.encryptedTotpSecret, sourceKey), targetKey); + } else if (!sourceKey || !targetKey) { + if (hasEncrypted) { + console.warn("Skipping entry (missing source password or target):", entry.title); + skipped++; + continue; + } } await db.put("entries", reencryptedEntry); importedEntries++; @@ -6533,6 +6597,104 @@ function EntryList($$anchor, $$props) { } delegate(["click"]); //#endregion +//#region src/lib/crypto/totp.js +/** +* TOTP (Time-based One-Time Password) — RFC 6238 / RFC 4226. +* +* Implemented with the browser's native Web Crypto API (HMAC-SHA1), so no +* external crypto dependency. A base32 secret yields 6-digit codes that change +* every 30 seconds, matching common 2FA authenticator apps. +*/ +var DEFAULT_PERIOD = 30; +var DEFAULT_DIGITS = 6; +var BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; +/** +* Decode a base32 string (RFC 4648) into bytes. Accepts whitespace (from +* authenticator-exported secrets). Throws on invalid characters. +* +* @param {string} base32 +* @returns {Uint8Array} +*/ +function base32Decode(base32) { + const clean = String(base32).toUpperCase().replace(/[\s=]/g, ""); + if (!clean) return new Uint8Array(0); + let bits = 0; + let value = 0; + const bytes = []; + for (const ch of clean) { + const idx = BASE32_ALPHABET.indexOf(ch); + if (idx === -1) throw new Error(`Invalid base32 character: "${ch}"`); + value = value << 5 | idx; + bits += 5; + if (bits >= 8) { + bytes.push(value >>> bits - 8 & 255); + bits -= 8; + } + } + return new Uint8Array(bytes); +} +/** +* Extract a raw base32 secret from the user input, accepting either a bare +* base32 string or an otpauth:// URI (which embeds `secret=`). +* +* @param {string} input +* @returns {string} Base32 secret (uppercased, no whitespace) +*/ +function extractSecret(input) { + const text = String(input || "").trim(); + if (!text) return ""; + if (/^otpauth:\/\//i.test(text)) try { + const m = text.match(/[?&]secret=([^&]+)/i); + if (m) return m[1].toUpperCase().replace(/[^A-Z2-7]/g, ""); + } catch {} + return text.toUpperCase().replace(/[\s-]/g, ""); +} +/** +* Generate the 8-byte big-endian counter for a Unix timestamp. +* @param {number} counter +* @returns {Uint8Array} +*/ +function counterBytes(counter) { + const buf = new Uint8Array(8); + for (let i = 7; i >= 0; i--) { + buf[i] = counter & 255; + counter = Math.floor(counter / 256); + } + return buf; +} +/** +* Compute a TOTP code for a secret at a given Unix timestamp. +* +* @param {string} secret - Base32 secret (SCHEME uri also accepted via extractSecret) +* @param {Object} [opts] +* @param {number} [opts.timestamp=Date.now()/1000] - Unix seconds +* @param {number} [opts.period=30] +* @param {number} [opts.digits=6] +* @returns {Promise} Zero-padded code. +*/ +async function generateTotp(secret, { timestamp = Math.floor(Date.now() / 1e3), period = DEFAULT_PERIOD, digits = DEFAULT_DIGITS } = {}) { + const keyBytes = base32Decode(extractSecret(secret)); + if (keyBytes.length === 0) throw new Error("TOTP secret is empty or invalid"); + const counter = Math.floor(timestamp / period); + const key = await crypto.subtle.importKey("raw", keyBytes, { + name: "HMAC", + hash: "SHA-1" + }, false, ["sign"]); + const sig = new Uint8Array(await crypto.subtle.sign("HMAC", key, counterBytes(counter))); + const offset = sig[sig.length - 1] & 15; + return (((sig[offset] & 127) << 24 | sig[offset + 1] << 16 | sig[offset + 2] << 8 | sig[offset + 3]) % Math.pow(10, digits)).toString().padStart(digits, "0"); +} +/** +* Seconds remaining before the current TOTP code expires. +* @param {Object} [opts] +* @param {number} [opts.period=30] +* @param {number} [opts.timestamp=Date.now()/1000] +* @returns {number} 1..period +*/ +function totpRemainingSeconds({ period = DEFAULT_PERIOD, timestamp = Math.floor(Date.now() / 1e3) } = {}) { + return period - timestamp % period; +} +//#endregion //#region src/components/EntryDetail.svelte var root_1$5 = /* @__PURE__ */ from_html(` `); var root_2$3 = /* @__PURE__ */ from_html(`Loading...`); @@ -6542,17 +6704,21 @@ var root_6$2 = /* @__PURE__ */ from_html(`✏️ Edit 🗑 Move to Trash`, 1); var root_8$2 = /* @__PURE__ */ from_html(`Username 📋`); var root_9$1 = /* @__PURE__ */ from_html(`Password 📋`); -var root_10 = /* @__PURE__ */ from_html(`URL 📋`); -var root_11 = /* @__PURE__ */ from_html(`Notes `); -var root_12 = /* @__PURE__ */ from_html(`Move to Trash Move " " to the trash? You can restore it later. Cancel`); -var root_13$1 = /* @__PURE__ */ from_html(`Delete Forever Permanently delete " "? This cannot be undone. Cancel`); -var root_5$3 = /* @__PURE__ */ from_html(` `, 1); +var root_10 = /* @__PURE__ */ from_html(`2FA Code (TOTP) 📋 `); +var root_11 = /* @__PURE__ */ from_html(`URL 📋`); +var root_12 = /* @__PURE__ */ from_html(`Notes `); +var root_13$1 = /* @__PURE__ */ from_html(`Move to Trash Move " " to the trash? You can restore it later. Cancel`); +var root_14 = /* @__PURE__ */ from_html(`Delete Forever Permanently delete " "? This cannot be undone. Cancel`); +var root_5$3 = /* @__PURE__ */ from_html(` `, 1); var root$4 = /* @__PURE__ */ from_html(` `); function EntryDetail($$anchor, $$props) { push($$props, true); let entry = /* @__PURE__ */ state(null); let passwordVisible = /* @__PURE__ */ state(false); let decryptedPassword = /* @__PURE__ */ state(""); + let totpCode = /* @__PURE__ */ state(""); + let totpRemaining = /* @__PURE__ */ state(30); + let totalRemaining = /* @__PURE__ */ state(false); let loading = /* @__PURE__ */ state(true); let error = /* @__PURE__ */ state(""); let showDeleteConfirm = /* @__PURE__ */ state(false); @@ -6566,13 +6732,46 @@ function EntryDetail($$anchor, $$props) { set(error, ""); try { set(entry, await getEntryById($$props.entryId), true); - if (get(entry) && app$1.encryptionKey) set(decryptedPassword, get(entry).encryptedPassword ? await decrypt(get(entry).encryptedPassword, app$1.encryptionKey) : "", true); + if (get(entry) && app$1.encryptionKey) { + set(decryptedPassword, get(entry).encryptedPassword ? await decrypt(get(entry).encryptedPassword, app$1.encryptionKey) : "", true); + if (get(entry).encryptedTotpSecret) { + const secret = await decrypt(get(entry).encryptedTotpSecret, app$1.encryptionKey); + await refreshTotp(secret); + startTotpTimer(secret); + } + } } catch (e) { set(error, "Failed to load entry: " + e.message); } set(loading, false); } loadEntry(); + let totpTimer = null; + async function refreshTotp(secret) { + try { + set(totpCode, await generateTotp(secret), true); + set(totpRemaining, totpRemainingSeconds(), true); + set(totalRemaining, get(totpRemaining) <= 5); + } catch (e) { + set(totpCode, ""); + set(totpRemaining, 0); + } + } + function startTotpTimer(secret) { + stopTotpTimer(); + totpTimer = setInterval(async () => { + if (get(totpRemaining) <= 1) await refreshTotp(secret); + else set(totpRemaining, get(totpRemaining) - 1); + set(totalRemaining, get(totpRemaining) <= 5); + }, 1e3); + } + function stopTotpTimer() { + if (totpTimer) { + clearInterval(totpTimer); + totpTimer = null; + } + } + onDestroy(stopTotpTimer); function showToast(message) { set(toast, message, true); if (toastTimer) clearTimeout(toastTimer); @@ -6726,113 +6925,143 @@ function EntryDetail($$anchor, $$props) { var consequent_7 = ($$anchor) => { var div_13 = root_10(); var div_14 = sibling(child(div_13), 2); - var a = child(div_14); - var text_7 = child(a, true); - reset(a); - var button_7 = sibling(a, 2); + var span_2 = child(div_14); + let classes; + var text_7 = child(span_2, true); + reset(span_2); + var button_7 = sibling(span_2, 2); reset(div_14); + var div_15 = sibling(div_14, 2); + var span_3 = child(div_15); + let classes_1; + var span_4 = sibling(span_3, 2); + var text_8 = child(span_4); + reset(span_4); + reset(div_15); reset(div_13); - template_effect(() => { - set_attribute(a, "href", get(entry).url); - set_text(text_7, get(entry).url); - }); - delegated("click", button_7, () => copyToClipboard(get(entry).url, "URL")); + template_effect(($0) => { + classes = set_class(span_2, 1, "totp-code svelte-dssgjx", null, classes, { "totp-urgent": get(totalRemaining) }); + set_text(text_7, $0); + classes_1 = set_class(span_3, 1, "totp-dot svelte-dssgjx", null, classes_1, { urgent: get(totalRemaining) }); + set_text(text_8, `${get(totpRemaining) ?? ""}s`); + }, [() => get(totpCode) ? String(get(totpCode)).replace(/^(.{3})/, "$1 ") : ""]); + delegated("click", button_7, () => copyToClipboard(get(totpCode), "2FA code")); append($$anchor, div_13); }; if_block(node_5, ($$render) => { - if (get(entry).url) $$render(consequent_7); + if (get(entry).encryptedTotpSecret) $$render(consequent_7); }); var node_6 = sibling(node_5, 2); var consequent_8 = ($$anchor) => { - var div_15 = root_11(); - var div_16 = sibling(child(div_15), 2); - var text_8 = child(div_16, true); + var div_16 = root_11(); + var div_17 = sibling(child(div_16), 2); + var a = child(div_17); + var text_9 = child(a, true); + reset(a); + var button_8 = sibling(a, 2); + reset(div_17); reset(div_16); - reset(div_15); - template_effect(() => set_text(text_8, get(entry).notes)); - append($$anchor, div_15); + template_effect(() => { + set_attribute(a, "href", get(entry).url); + set_text(text_9, get(entry).url); + }); + delegated("click", button_8, () => copyToClipboard(get(entry).url, "URL")); + append($$anchor, div_16); }; if_block(node_6, ($$render) => { - if (get(entry).notes) $$render(consequent_8); + if (get(entry).url) $$render(consequent_8); }); - reset(div_8); - var div_17 = sibling(div_8, 2); - var span_2 = child(div_17); - var text_9 = child(span_2); - reset(span_2); - var span_3 = sibling(span_2, 2); - var text_10 = child(span_3); - reset(span_3); - reset(div_17); - reset(div_5); - var node_7 = sibling(div_5, 2); + var node_7 = sibling(node_6, 2); var consequent_9 = ($$anchor) => { var div_18 = root_12(); - var div_19 = child(div_18); - var p = sibling(child(div_19), 2); - var strong = sibling(child(p)); - var text_11 = child(strong, true); - reset(strong); - next(); - reset(p); - var div_20 = sibling(p, 2); - var button_8 = child(div_20); - var text_12 = child(button_8, true); - reset(button_8); - var button_9 = sibling(button_8, 2); - reset(div_20); + var div_19 = sibling(child(div_18), 2); + var text_10 = child(div_19, true); reset(div_19); reset(div_18); - template_effect(() => { - set_text(text_11, get(entry).title); - button_8.disabled = get(deleting); - set_text(text_12, get(deleting) ? "Moving..." : "Move to Trash"); - }); - delegated("click", div_18, () => set(showDeleteConfirm, false)); - delegated("click", div_19, (e) => e.stopPropagation()); - delegated("click", button_8, handleMoveToTrash); - delegated("click", button_9, () => set(showDeleteConfirm, false)); + template_effect(() => set_text(text_10, get(entry).notes)); append($$anchor, div_18); }; if_block(node_7, ($$render) => { - if (get(showDeleteConfirm)) $$render(consequent_9); + if (get(entry).notes) $$render(consequent_9); }); - var node_8 = sibling(node_7, 2); + reset(div_8); + var div_20 = sibling(div_8, 2); + var span_5 = child(div_20); + var text_11 = child(span_5); + reset(span_5); + var span_6 = sibling(span_5, 2); + var text_12 = child(span_6); + reset(span_6); + reset(div_20); + reset(div_5); + var node_8 = sibling(div_5, 2); var consequent_10 = ($$anchor) => { var div_21 = root_13$1(); var div_22 = child(div_21); - var p_1 = sibling(child(div_22), 2); - var strong_1 = sibling(child(p_1)); - var text_13 = child(strong_1, true); - reset(strong_1); + var p = sibling(child(div_22), 2); + var strong = sibling(child(p)); + var text_13 = child(strong, true); + reset(strong); next(); - reset(p_1); - var div_23 = sibling(p_1, 2); - var button_10 = child(div_23); - var text_14 = child(button_10, true); - reset(button_10); - var button_11 = sibling(button_10, 2); + reset(p); + var div_23 = sibling(p, 2); + var button_9 = child(div_23); + var text_14 = child(button_9, true); + reset(button_9); + var button_10 = sibling(button_9, 2); reset(div_23); reset(div_22); reset(div_21); template_effect(() => { set_text(text_13, get(entry).title); - button_10.disabled = get(deleting); - set_text(text_14, get(deleting) ? "Deleting..." : "Delete Forever"); + button_9.disabled = get(deleting); + set_text(text_14, get(deleting) ? "Moving..." : "Move to Trash"); }); - delegated("click", div_21, () => set(showPermanentDeleteConfirm, false)); + delegated("click", div_21, () => set(showDeleteConfirm, false)); delegated("click", div_22, (e) => e.stopPropagation()); - delegated("click", button_10, handlePermanentDelete); - delegated("click", button_11, () => set(showPermanentDeleteConfirm, false)); + delegated("click", button_9, handleMoveToTrash); + delegated("click", button_10, () => set(showDeleteConfirm, false)); append($$anchor, div_21); }; if_block(node_8, ($$render) => { - if (get(showPermanentDeleteConfirm)) $$render(consequent_10); + if (get(showDeleteConfirm)) $$render(consequent_10); + }); + var node_9 = sibling(node_8, 2); + var consequent_11 = ($$anchor) => { + var div_24 = root_14(); + var div_25 = child(div_24); + var p_1 = sibling(child(div_25), 2); + var strong_1 = sibling(child(p_1)); + var text_15 = child(strong_1, true); + reset(strong_1); + next(); + reset(p_1); + var div_26 = sibling(p_1, 2); + var button_11 = child(div_26); + var text_16 = child(button_11, true); + reset(button_11); + var button_12 = sibling(button_11, 2); + reset(div_26); + reset(div_25); + reset(div_24); + template_effect(() => { + set_text(text_15, get(entry).title); + button_11.disabled = get(deleting); + set_text(text_16, get(deleting) ? "Deleting..." : "Delete Forever"); + }); + delegated("click", div_24, () => set(showPermanentDeleteConfirm, false)); + delegated("click", div_25, (e) => e.stopPropagation()); + delegated("click", button_11, handlePermanentDelete); + delegated("click", button_12, () => set(showPermanentDeleteConfirm, false)); + append($$anchor, div_24); + }; + if_block(node_9, ($$render) => { + if (get(showPermanentDeleteConfirm)) $$render(consequent_11); }); template_effect(($0, $1) => { set_text(text_3, get(entry).title); - set_text(text_9, `Created: ${$0 ?? ""}`); - set_text(text_10, `Updated: ${$1 ?? ""}`); + set_text(text_11, `Created: ${$0 ?? ""}`); + set_text(text_12, `Updated: ${$1 ?? ""}`); }, [() => new Date(get(entry).createdAt).toLocaleString(), () => new Date(get(entry).updatedAt).toLocaleString()]); append($$anchor, fragment); }; @@ -6854,7 +7083,7 @@ var root_3$2 = /* @__PURE__ */ from_html(` `); var root_4$2 = /* @__PURE__ */ from_html(``); var root_7$1 = /* @__PURE__ */ from_html(` `); -var root_2$2 = /* @__PURE__ */ from_html(` Title * Username / Email Password 🎲 URL Group No group Notes Cancel`, 1); +var root_2$2 = /* @__PURE__ */ from_html(` Title * Username / Email Password 🎲 TOTP Secret (2FA) — optional URL Group No group Notes Cancel`, 1); var root$3 = /* @__PURE__ */ from_html(``); function EntryForm($$anchor, $$props) { push($$props, true); @@ -6864,6 +7093,7 @@ function EntryForm($$anchor, $$props) { let url = /* @__PURE__ */ state(""); let notes = /* @__PURE__ */ state(""); let groupId = /* @__PURE__ */ state(""); + let totpSecret = /* @__PURE__ */ state(""); let passwordVisible = /* @__PURE__ */ state(false); let groups = /* @__PURE__ */ state(proxy([])); let loading = /* @__PURE__ */ state(true); @@ -6882,6 +7112,7 @@ function EntryForm($$anchor, $$props) { set(title, entry.title, true); set(username, entry.username, true); set(password, entry.encryptedPassword ? await decrypt(entry.encryptedPassword, app$1.encryptionKey) : "", true); + set(totpSecret, entry.encryptedTotpSecret ? await decrypt(entry.encryptedTotpSecret, app$1.encryptionKey) : "", true); set(url, entry.url || "", true); set(notes, entry.notes || "", true); set(groupId, entry.groupId || "", true); @@ -6912,10 +7143,13 @@ function EntryForm($$anchor, $$props) { return; } const encryptedPassword = get(password) ? await encrypt(get(password), app$1.encryptionKey) : ""; + const cleanTotp = get(totpSecret).trim(); + const encryptedTotpSecret = cleanTotp ? await encrypt(cleanTotp, app$1.encryptionKey) : ""; if (get(isEdit)) await updateEntry(updateEntry$1(await getEntryById($$props.entryId), { title: get(title), username: get(username), encryptedPassword, + encryptedTotpSecret, url: get(url), notes: get(notes), groupId: get(groupId) @@ -6924,6 +7158,7 @@ function EntryForm($$anchor, $$props) { title: get(title), username: get(username), encryptedPassword, + encryptedTotpSecret, url: get(url), notes: get(notes), groupId: get(groupId) @@ -6994,7 +7229,11 @@ function EntryForm($$anchor, $$props) { remove_input_defaults(input_3); reset(div_9); var div_10 = sibling(div_9, 2); - var select = sibling(child(div_10), 2); + var input_4 = sibling(child(div_10), 2); + remove_input_defaults(input_4); + reset(div_10); + var div_11 = sibling(div_10, 2); + var select = sibling(child(div_11), 2); var option = child(select); option.value = option.__value = ""; each(sibling(option), 17, () => get(groups), index, ($$anchor, group) => { @@ -7018,17 +7257,17 @@ function EntryForm($$anchor, $$props) { append($$anchor, fragment_1); }); reset(select); - reset(div_10); - var div_11 = sibling(div_10, 2); - var textarea = sibling(child(div_11), 2); - remove_textarea_child(textarea); reset(div_11); var div_12 = sibling(div_11, 2); - var button_2 = child(div_12); + var textarea = sibling(child(div_12), 2); + remove_textarea_child(textarea); + reset(div_12); + var div_13 = sibling(div_12, 2); + var button_2 = child(div_13); var text_4 = child(button_2, true); reset(button_2); var button_3 = sibling(button_2, 2); - reset(div_12); + reset(div_13); reset(form); template_effect(() => { set_attribute(input_2, "type", get(passwordVisible) ? "text" : "password"); @@ -7044,7 +7283,8 @@ function EntryForm($$anchor, $$props) { bind_value(input_2, () => get(password), ($$value) => set(password, $$value)); delegated("click", button, () => set(passwordVisible, !get(passwordVisible))); delegated("click", button_1, () => set(password, generatePassword({ length: 16 }), true)); - bind_value(input_3, () => get(url), ($$value) => set(url, $$value)); + bind_value(input_3, () => get(totpSecret), ($$value) => set(totpSecret, $$value)); + bind_value(input_4, () => get(url), ($$value) => set(url, $$value)); bind_select_value(select, () => get(groupId), ($$value) => set(groupId, $$value)); bind_value(textarea, () => get(notes), ($$value) => set(notes, $$value)); delegated("click", button_3, function(...$$args) { @@ -8470,6 +8710,34 @@ label { flex-shrink: 0; } + .totp-value.svelte-dssgjx .totp-code:where(.svelte-dssgjx) { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 1.15rem; + font-weight: 700; + letter-spacing: 0.14em; + color: var(--color-primary); + } + .totp-value.svelte-dssgjx .totp-code.totp-urgent:where(.svelte-dssgjx) { + color: var(--color-danger); + } + + .totp-remaining.svelte-dssgjx { + display: flex; + align-items: center; + gap: 6px; + margin-top: 6px; + } + .totp-dot.svelte-dssgjx { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--color-success); + display: inline-block; + } + .totp-dot.urgent.svelte-dssgjx { + background: var(--color-danger); + } + .detail-meta.svelte-dssgjx { display: flex; gap: 16px; diff --git a/src/components/EntryDetail.svelte b/src/components/EntryDetail.svelte index 65a17d1..d99dcbb 100644 --- a/src/components/EntryDetail.svelte +++ b/src/components/EntryDetail.svelte @@ -1,6 +1,8 @@
Move " " to the trash? You can restore it later.
Permanently delete " "? This cannot be undone.