Merge feature/fullscreen-ux-phase-2a: smart-input affordances

Phase 2A of the fullscreen UX redesign — 8 form-level smart-input
affordances (URL fill-from-tab + hostname chip, group autocomplete,
password reveal + strength bar, TOTP live preview + QR decode, notes
monospace toggle), shared between popup and fullscreen vault tabs via
the new extension/src/shared/form-affordances/ module set.

CLI parity:
- relicario rate <passphrase> (zxcvbn score / guess estimate)
- relicario completions <SHELL> (bash/zsh/fish via clap_complete)
- --group <TAB> dynamic enumeration via .relicario/groups.cache
  (plaintext leak surface; opt out with RELICARIO_NO_GROUPS_CACHE=1)
- --totp-qr <path> on add login + edit (rqrr decode)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-05-01 22:37:18 -04:00
26 changed files with 5395 additions and 23 deletions

View File

@@ -6,6 +6,7 @@
import type { PopupMessage, Response } from '../../shared/messages';
import type { Item, ItemId, Manifest, VaultConfig, SetupState, DeviceSettings, TotpConfig, AttachmentRef } from '../../shared/types';
import { DEFAULT_DEVICE_SETTINGS } from '../../shared/types';
import { base32Decode } from '../../shared/base32';
import type { GitHost } from '../git-host';
import { createGitHost, base64ToUint8Array } from '../git-host';
import * as vault from '../vault';
@@ -146,6 +147,52 @@ export async function handle(
case 'rate_passphrase':
return { ok: true, data: state.wasm.rate_passphrase(msg.passphrase) };
case 'get_active_tab_url': {
const tabs = await new Promise<chrome.tabs.Tab[]>((resolve) => {
chrome.tabs.query({ active: true, lastFocusedWindow: true }, (t) => resolve(t));
});
const tab = tabs[0];
if (!tab?.url) return { ok: true, data: null };
// Filter out chrome:// and extension URLs — autofill doesn't apply.
if (/^(chrome|chrome-extension|chrome-search|moz-extension|edge|edge-extension|about|file|view-source|data|devtools|javascript):/i.test(tab.url)) {
return { ok: true, data: null };
}
return { ok: true, data: { url: tab.url, title: tab.title ?? '' } };
}
case 'list_groups': {
if (!state.manifest) return { ok: true, data: { groups: [] } };
const set = new Set<string>();
for (const id in state.manifest.items) {
const g = state.manifest.items[id].group;
if (g) set.add(g);
}
return { ok: true, data: { groups: Array.from(set).sort() } };
}
case 'preview_totp_from_secret': {
const cleaned = msg.secret_b32.toUpperCase().replace(/\s+/g, '').replace(/=+$/, '');
if (cleaned.length < 16 || !/^[A-Z2-7]+$/.test(cleaned)) {
return { ok: false, error: 'invalid base32 secret' };
}
let secretBytes: Uint8Array;
try {
secretBytes = base32Decode(cleaned);
} catch (e) {
return { ok: false, error: `invalid base32: ${e instanceof Error ? e.message : String(e)}` };
}
const cfg = {
secret: Array.from(secretBytes),
algorithm: 'sha1',
digits: 6,
period_seconds: 30,
kind: 'totp',
};
const now = Math.floor(Date.now() / 1000);
const result = state.wasm.totp_compute(JSON.stringify(cfg), BigInt(now));
return { ok: true, data: { code: result.code, expires_at: result.expires_at } };
}
case 'generate_password': {
const password = state.wasm.generate_password(JSON.stringify(msg.request));
return { ok: true, data: { password } };