forked from akai/readest
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:
@@ -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')}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user