- Add settings-security.ts with renderSecuritySection / teardownSecuritySection
- Three states: amber warning (no QR), green status (QR set up), modal overlay (show/print SVG)
- Device list with inline revoke; passphrase collected via prompt()
- QR payload never written to chrome.storage; only recovery_qr_generated_at timestamp stored
- Add generate_recovery_qr / unwrap_recovery_qr message types to messages.ts + POPUP_ONLY_TYPES
- Add SW handlers in popup-only.ts delegating to wasm_generate_recovery_qr / wasm_unwrap_recovery_qr
- Declare wasm_generate_recovery_qr and wasm_unwrap_recovery_qr in wasm.d.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Notes, custom-fields disclosure, attachments disclosure, and form-actions
in fullscreen logins now sit inside a .form-lower wrapper with the same
max-width: 960px; margin: 0 auto envelope as .form-grid above. Removes
the visual rhythm break at the 2-col -> full-width transition.
Popup keeps its current single-column behavior (gated on surface flag).
Add data-field-kind attribute to renderConcealedRow so wireFieldHandlers
can distinguish password fields from other concealed rows (TOTP secrets,
CVV, PIN, private keys). Apply colorizePassword() on reveal when kind is
"password"; plain textContent otherwise. Pass kind through renderSections
for custom-section password fields.
Import colorizePassword and post-process .revealed value cells after
innerHTML render, replacing escaped-HTML text with colored spans via
the valueStore plaintext lookup.
Programmatic input.value = newPassword does not fire input events, so
the strength-meter listener at shared/form-affordances/password-tools.ts:65
never re-rates the new value — meter stays stuck on the prior reading.
Extract applyGeneratedPassword(input, value) helper that sets value, type,
then dispatches new InputEvent('input', { bubbles: true }). Vitest covers
the dispatch + a sanity check that bubbling listeners fire.
The form pane gets a flex column layout: scrollable content above,
sticky save bar at bottom. Bar uses translucent fill with backdrop-blur
and a 24px gradient fade so content scrolls under it. Save / cancel
buttons reuse the form's existing handlers via externalActions flag.
renderForm() takes an optional { surface: 'popup' | 'fullscreen' }
parameter. When 'fullscreen', the Identity and Credentials field
groups render as glass cards inside a .form-grid (two columns,
stacks at <=720px). Popup keeps its single-column layout.
Restructures the unlock screen so the form sits in a glass card with
a primary 'unlock vault' button. Logo, brand, and tagline are grouped
as a lockup. Open-vault and settings are demoted to secondary buttons.
Body gets the .surface-backdrop wrapper.
- Show revoked devices in collapsible section with strikethrough styling
- Fetch revoked.json via new list_revoked message + router case
- Registration flow uses register_device WASM API (private keys internal)
- Display revoked_by and timestamp for each revoked entry
- Update setup wizard to use new register_device API
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Brand name uses capital R in user-facing text — extension UI strings,
CLI clap help / descriptions / error prose, markdown docs. Lowercase
preserved for the binary command, crate names, npm package, file
paths, env vars, and code identifiers.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Whole-branch review recommendation: switch renderFormHeader's signature
from positional (titleText) to options ({ titleText }) so Phase 3 can
add 'dirty' (and any future hooks like a save-keybinding hint) without
touching all 7 call sites in lockstep with the unsaved-guard work.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Code-review feedback on Task 8: the conditional empty
<div style="margin-bottom:16px;"> spacer was an inline-styled magic
number and the 6-line header pattern was duplicated across all 7 typed
forms.
Now:
- .form-header class owns the bottom margin in both stylesheets.
- :has(+ .form-subtitle) selector drops the margin when a subtitle
follows, so spacing tokens stay in CSS instead of inline styles.
- renderFormHeader(titleText) shared helper collapses the 6-line
duplication to a one-liner per form. item-form.ts (type-selection
screen) is unaffected — it uses a different header structure.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
All seven type forms plus the type-selection screen now show a small
'esc to cancel' subtitle under the heading when rendered in the
fullscreen vault tab (isInTab() === true). The subtitle is suppressed
in the popup, where esc has the more general meaning of closing the
popup. .form-subtitle class is shared between popup and vault
stylesheets so future hooks can reuse it.
Dynamic dirty-state ('unsaved · esc to cancel') wiring is deferred to
Phase 3 (unsaved-changes guard).
Plan 2026-04-30 fullscreen UX phase 1 task 8.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Code-review feedback on Task 7: the same Array<[name, renderForm]> of
all 7 typed forms appeared in three test files (required-pill,
popout-button, popout-button-fullscreen). A new typed form would have
required updating all three.
Now defined once in __tests__/_typed-forms.ts. Future typed-form
additions get regression coverage automatically.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The ⤴ popout button is meaningless when the form is already in
vault.html — gate it on !isInTab(). Affects all seven type forms plus
the type-selection screen. Regression tests cover both popup (button
present) and fullscreen (button absent) contexts via it.each across
all 7 forms.
Plan 2026-04-30 fullscreen UX phase 1 task 7.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
▦ trash and ⌬ devices in the popup settings panel now match the
fullscreen sidebar's glyph language. Lowercased labels match the brand.
Plan 2026-04-30 fullscreen UX phase 1 task 6.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Code-review feedback on Task 4:
- Test expanded from login-only to it.each across all 7 type forms
(14 assertions total). A future revert to <span class="req">*</span>
in any form now fails CI.
- .label .req rule removed from popup/styles.css and vault/vault.css —
zero consumers after the REQUIRED_PILL_HTML migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces ten <span class="req">*</span> sites across all seven type
forms with the shared REQUIRED_PILL_HTML snippet ('required' badge).
Adds a regression test pinning the new HTML in the login form.
Plan 2026-04-30 fullscreen UX phase 1 task 4.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New vault.html#import panel with a file picker, parse-preview
("N logins, M notes, K skipped — proceed?"), confirm/cancel
buttons, inline progress, and a post-import warnings list. The
popup's settings-vault view links to it via a new
"LastPass CSV →" button next to "Backup & restore →".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes three audit gaps in one pass:
1. Sync now button in the popup settings view (📤). Triggers the existing
{ type: 'sync' } SW message and surfaces success / failure inline. The
SW message was already wired but had no UI entry point.
2. Device registration from the popup. The "Register this device" button
on the devices view used to error out with a "not yet implemented"
message; it now opens an inline name input (default = browser+OS), and
on confirm sends a new register_this_device SW message that generates
an ed25519 keypair via WASM, persists private_key + name to
chrome.storage.local, and writes the public key to the remote
devices.json. No setup-wizard detour.
3. Vault tab is now an authorized sender for popup-only SW messages. The
router accepts vault.html alongside popup.html, so the fullscreen tab
can drive the same flows. Test covers acceptance from the vault tab.
New SW message: register_this_device { name }. Added to PopupMessage and
POPUP_ONLY_TYPES, handled in router/popup-only.ts.
Tests: 5 new vitest cases (3 in settings.test.ts, 2 in devices.test.ts)
+ 1 router test for vault-tab acceptance. All 194 extension tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduce shared/state.ts as a service-locator so popup components
(item-detail, item-form, trash, devices, settings, etc.) work in both
the popup and vault tab bundles. Both entry points register themselves
as the host; components import from shared/state instead of popup.ts.
Vault.ts now delegates to the real popup components, removing ~300 lines
of placeholder renderers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Search no longer auto-focuses; use "/" to focus it
- Typing in search no longer re-renders the entire view, just the
item list — fixes backwards text caused by cursor reset to pos 0
- Arrow keys also update list without full re-render
- Enter opens the selected item even when search is focused
Co-Authored-By: Claude <noreply@anthropic.com>
- Login and secure_note types stay in popup without attachment UI
- All other types (identity, card, key, totp, document) auto-redirect
to full tab when selected
- Attachments only shown for login/secure_note when opened in tab
Co-Authored-By: Claude <noreply@anthropic.com>
Forms can now be opened in a full browser tab via the ⤴ button,
solving Chrome's popup closure on file picker interaction. Deep
linking via URL params preserves view, item type, and item ID.
Also removes the unused dropdown picker code from item-list.ts.
Co-Authored-By: Claude <noreply@anthropic.com>
Clicking "+ new" now navigates to a type selection view instead of
showing a dropdown that gets clipped by popup bounds. The selection
view displays all item types as buttons in a scrollable list.
Co-Authored-By: Claude <noreply@anthropic.com>
Adds View variants, render cases, teardown calls, and entry points
in settings menu for trash and devices.
Co-Authored-By: Claude <noreply@anthropic.com>
Shows button when item.field_history is non-empty. Navigates to
field-history screen with historyItemId set.
Co-Authored-By: Claude <noreply@anthropic.com>
Shows current + historical values for tracked fields (password/concealed).
Click to reveal, copy button per entry (plaintext stored in a module-level
Map, never embedded in the DOM). Grouped by field name if multiple tracked
fields exist. Adds historyItemId to PopupState and 'field-history' to View.
Co-Authored-By: Claude <noreply@anthropic.com>
Shows registered devices with "← you" indicator on current device.
Revoke button on other devices. Unregistered banner if current
device not in list.
Co-Authored-By: Claude <noreply@anthropic.com>
Shows trashed items sorted newest-first with restore buttons.
Empty trash button purges all items + orphan blobs. Header shows
count and days until oldest auto-purges.
Co-Authored-By: Claude <noreply@anthropic.com>