- AppLockScreen: pad the lock screen bottom by the on-screen
keyboard height tracked via visualViewport, so the flex-centered
PIN sits above the keyboard on iOS WKWebView where dvh does not
shrink.
- AppLockScreen: skip stickyFocus on mobile. iOS will not pop the
keyboard from a programmatic .focus(), so the cursor would blink
with no input — wait for the user's tap instead.
- PinInput: forward autoFocus to the input when autoFocus or
stickyFocus is set, for more reliable mount-time focus.
- style.ts: give legacy <p><font>...</font></p> its own block
context so iOS Safari applies the inherited line-height.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(applock): show blinking cursor on PIN input
Empty PIN slots used to render nothing, leaving no cue for which
position is active. Add a thin underscore that blinks under the
next-to-fill slot while the input is focused.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(ui): misc settings polish + i18n refresh
- applock screen: switch fixed positioning to full-height so the lock
screen sits inside the safe area
- applock dialog: split the recovery sentence so it reads cleanly
without an em dash
- settings: rename "Interface Language" to "Language"
- translators: drop the "(Unavailable)" suffix from disabled providers;
the row already greys out
- ruler color picker: keep swatches clickable when ruler is off so
users can still set a color before enabling
- refresh translations across all locales for the changed strings
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#2285.
Adds an opt-in 4-digit PIN that gates the library and reader on app
launch. Threat model: casual physical/browser access by another person
on a shared device — peace of mind, not defense against an attacker
with filesystem access. The PIN is stored as a salted PBKDF2-SHA256
hash (100k iterations) in settings.json; the plaintext PIN is never
persisted.
Configured from Settings → Advanced Settings → "Set PIN…" (and
"Change PIN…" / "Disable PIN…" once enabled). The lock screen and the
set/change/disable dialog share a single 4-dot input component
(PinInput) for a consistent UI; the dialog auto-advances focus from
Current → New → Confirm. Lock-on-resume, biometric unlock, and
account-based reset are out of scope for this MVP — disable for now is
"clear app data".
Bundles the previously-missed sync-passphrase i18n strings (PR #4090)
across all 33 locales so no `__STRING_NOT_TRANSLATED__` placeholders
remain in the tree.
New
- src/libs/crypto/applock.ts (PBKDF2 hash/verify; reuses derivePbkdf2Key)
- src/store/appLockStore.ts (gate + dialog state)
- src/components/PinInput.tsx (shared 4-dot input)
- src/components/AppLockScreen.tsx (full-screen lock gate)
- src/components/settings/AppLockDialog.tsx (set/change/disable)
- src/__tests__/libs/crypto/applock.test.ts
Modified
- src/types/settings.ts (pinCodeEnabled / pinCodeHash / pinCodeSalt)
- src/services/constants.ts (default off)
- src/components/Providers.tsx (mount gate + dialog above app shell)
- src/app/library/components/SettingsMenu.tsx (Advanced submenu entries)
- src/styles/globals.css (animate-pin-shake keyframe)
- public/locales/*/translation.json (21 PIN keys + 17 leftover passphrase keys × 33 locales)
Verified
- pnpm test (4018 pass)
- pnpm lint (clean)
- Manual web smoke: Set/Reload-locks/Wrong-PIN/Unlock/Change/Disable
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>