feat(applock): blinking PIN cursor + misc UI polish (#4110)

* 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>
This commit is contained in:
Huang Xin
2026-05-09 22:58:59 +08:00
committed by GitHub
parent 1eae2af23e
commit f6f446e8a0
41 changed files with 106 additions and 131 deletions
@@ -49,7 +49,7 @@ export default function AppLockScreen() {
return (
<div
className='bg-base-100 fixed inset-0 z-[200] flex flex-col items-center justify-center px-6'
className='bg-base-100 full-height inset-0 z-[200] flex flex-col items-center justify-center px-6'
role='dialog'
aria-modal='true'
aria-label={_('App locked')}
+18 -4
View File
@@ -1,7 +1,7 @@
'use client';
import clsx from 'clsx';
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { PIN_LENGTH } from '@/libs/crypto/applock';
@@ -26,12 +26,13 @@ export interface PinInputHandle {
focus: () => void;
}
const PinDot = ({ filled }: { filled: boolean }) => (
const PinDot = ({ filled, active }: { filled: boolean; active: boolean }) => (
<div
className={clsx(
'eink-bordered flex h-12 w-10 items-center justify-center rounded-lg border',
'eink-bordered relative flex h-12 w-10 items-center justify-center rounded-lg border',
'border-base-content/20 bg-base-200/60',
filled && 'border-base-content/40',
active && 'border-base-content/40',
)}
>
<span
@@ -40,6 +41,12 @@ const PinDot = ({ filled }: { filled: boolean }) => (
filled ? 'bg-base-content opacity-100' : 'opacity-0',
)}
/>
{active && !filled && (
<span
aria-hidden='true'
className='bg-base-content animate-pin-cursor-blink absolute bottom-2 left-1/2 h-0.5 w-4 -translate-x-1/2 rounded-full'
/>
)}
</div>
);
@@ -57,6 +64,7 @@ const PinInput = forwardRef<PinInputHandle, PinInputProps>(function PinInput(
ref,
) {
const inputRef = useRef<HTMLInputElement | null>(null);
const [focused, setFocused] = useState(false);
useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus() }));
useEffect(() => {
@@ -93,6 +101,8 @@ const PinInput = forwardRef<PinInputHandle, PinInputProps>(function PinInput(
maxLength={PIN_LENGTH}
value={value}
onChange={handleChange}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
disabled={disabled}
autoComplete={autoComplete}
aria-label={ariaLabel}
@@ -100,7 +110,11 @@ const PinInput = forwardRef<PinInputHandle, PinInputProps>(function PinInput(
/>
<div className={clsx('flex gap-3', shake && 'animate-pin-shake')}>
{Array.from({ length: PIN_LENGTH }).map((_dot, i) => (
<PinDot key={i} filled={i < value.length} />
<PinDot
key={i}
filled={i < value.length}
active={focused && !disabled && i === value.length}
/>
))}
</div>
</label>
@@ -183,7 +183,7 @@ export default function AppLockDialog() {
const description =
mode === 'set'
? _(
'Pick a 4-digit PIN. You will need to enter it every time you open Readest. There is no way to recover a forgotten PIN — clearing the app data is the only way to reset it.',
'Pick a 4-digit PIN. You will need to enter it every time you open Readest. There is no way to recover a forgotten PIN. Clearing the app data is the only way to reset it.',
)
: mode === 'change'
? _('Enter your current PIN, then choose a new 4-digit PIN.')
@@ -273,7 +273,7 @@ const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
<div className='card border-base-200 bg-base-100 border shadow'>
<div className='divide-base-200 divide-y'>
<div className='config-item'>
<span className=''>{_('Interface Language')}</span>
<span className=''>{_('Language')}</span>
<Select
value={getCurrentUILangOption().value}
onChange={handleSelectUILang}
@@ -63,11 +63,11 @@ const ReadingRulerSettings: React.FC<ReadingRulerSettingsProps> = ({
{RULER_COLORS.map(({ value, className, hoverClassName }) => (
<button
key={value}
disabled={!enabled}
// disabled={!enabled}
className={`btn btn-circle btn-sm ${className} ${hoverClassName} ${
color === value ? 'ring-base-content ring-2 ring-offset-1' : ''
} ${!enabled ? 'btn-disabled opacity-50' : ''}`}
onClick={() => onColorChange(value)}
} ${!enabled ? 'opacity-50' : ''}`}
onClick={() => enabled && onColorChange(value)}
/>
))}
</div>