feat(i18n): add new localization strings for settings and two-step login across multiple languages

style: adjust grid layout for app main and add responsive styles for settings category

style: enhance management styles with new settings category layout and tabs

style: improve responsive design for settings modules and submodules
This commit is contained in:
shuaiplus
2026-07-03 19:19:24 +08:00
parent 6284c632de
commit c7eb6c663d
12 changed files with 963 additions and 310 deletions
+479 -292
View File
@@ -9,8 +9,11 @@ import ConfirmDialog from '@/components/ConfirmDialog';
interface SettingsPageProps {
profile: Profile;
totpEnabled: boolean;
themePreference: ThemePreference;
lockTimeoutMinutes: 0 | 1 | 5 | 15 | 30;
sessionTimeoutAction: 'lock' | 'logout';
onThemePreferenceChange: (preference: ThemePreference) => void;
onVerifyMasterPassword: (email: string, password: string) => Promise<void>;
onChangePassword: (currentPassword: string, nextPassword: string, nextPassword2: string) => Promise<void>;
onSavePasswordHint: (masterPasswordHint: string) => Promise<void>;
onEnableTotp: (secret: string, token: string, masterPassword: string) => Promise<void>;
@@ -27,11 +30,15 @@ interface SettingsPageProps {
onNotify?: (type: 'success' | 'error' | 'warning', text: string) => void;
}
type ThemePreference = 'system' | 'light' | 'dark';
type SettingsSection = 'appearance' | 'session' | 'masterPassword' | 'twoStep' | 'keys';
type MasterPasswordPromptAction =
| 'enableTotp'
| 'recovery'
| 'apiKey'
| 'rotateApiKey'
| 'manageTotp'
| 'createPasskey'
| 'enablePasskeyDirectUnlock'
| 'deletePasskey';
@@ -99,12 +106,18 @@ export default function SettingsPage(props: SettingsPageProps) {
const [accountPasskeyName, setAccountPasskeyName] = useState(t('txt_account_passkey'));
const [accountPasskeyDirectUnlock, setAccountPasskeyDirectUnlock] = useState(false);
const [accountPasskeyPromptId, setAccountPasskeyPromptId] = useState<string | null>(null);
const [createPasskeyDialogOpen, setCreatePasskeyDialogOpen] = useState(false);
const [createPasskeyMasterPassword, setCreatePasskeyMasterPassword] = useState('');
const [rotateApiKeyConfirmOpen, setRotateApiKeyConfirmOpen] = useState(false);
const [apiKeyDialogOpen, setApiKeyDialogOpen] = useState(false);
const [totpManageDialogOpen, setTotpManageDialogOpen] = useState(false);
const [recoveryCodeDialogOpen, setRecoveryCodeDialogOpen] = useState(false);
const [totpManagePassword, setTotpManagePassword] = useState('');
const [masterPasswordPrompt, setMasterPasswordPrompt] = useState<MasterPasswordPromptAction | null>(null);
const [masterPasswordPromptValue, setMasterPasswordPromptValue] = useState('');
const [masterPasswordPromptSubmitting, setMasterPasswordPromptSubmitting] = useState(false);
const [selectedLocale, setSelectedLocale] = useState<Locale>(() => getLocale());
const [activeSection, setActiveSection] = useState<SettingsSection>('appearance');
useEffect(() => {
clearLegacyTotpSetupSecrets();
@@ -179,6 +192,7 @@ export default function SettingsPage(props: SettingsPageProps) {
} else if (masterPasswordPrompt === 'recovery') {
const code = await props.onGetRecoveryCode(masterPassword);
setRecoveryCode(code);
setRecoveryCodeDialogOpen(true);
props.onNotify?.('success', t('txt_recovery_code_loaded'));
} else if (masterPasswordPrompt === 'apiKey') {
const key = await props.onGetApiKey(masterPassword);
@@ -189,9 +203,14 @@ export default function SettingsPage(props: SettingsPageProps) {
setApiKey(key);
setApiKeyDialogOpen(true);
props.onNotify?.('success', t('txt_api_key_rotated'));
} else if (masterPasswordPrompt === 'manageTotp') {
await props.onVerifyMasterPassword(props.profile.email, masterPassword);
setTotpManagePassword(masterPassword);
setTotpManageDialogOpen(true);
} else if (masterPasswordPrompt === 'createPasskey') {
const credential = await props.onCreateAccountPasskey(accountPasskeyName, masterPassword, accountPasskeyDirectUnlock);
if (credential) await refreshAccountPasskeys();
await props.onVerifyMasterPassword(props.profile.email, masterPassword);
setCreatePasskeyMasterPassword(masterPassword);
setCreatePasskeyDialogOpen(true);
} else if (masterPasswordPrompt === 'enablePasskeyDirectUnlock') {
if (!accountPasskeyPromptId) throw new Error(t('txt_account_passkey_not_found'));
await props.onEnableAccountPasskeyDirectUnlock(accountPasskeyPromptId, masterPassword);
@@ -218,13 +237,15 @@ export default function SettingsPage(props: SettingsPageProps) {
? t('txt_view_recovery_code')
: masterPasswordPrompt === 'rotateApiKey'
? t('txt_rotate_api_key')
: masterPasswordPrompt === 'createPasskey'
? t('txt_add_account_passkey')
: masterPasswordPrompt === 'enablePasskeyDirectUnlock'
? t('txt_enable_passkey_direct_unlock')
: masterPasswordPrompt === 'deletePasskey'
? t('txt_delete_account_passkey')
: t('txt_view_api_key');
: masterPasswordPrompt === 'manageTotp'
? t('txt_totp')
: masterPasswordPrompt === 'createPasskey'
? t('txt_add_account_passkey')
: masterPasswordPrompt === 'enablePasskeyDirectUnlock'
? t('txt_enable_passkey_direct_unlock')
: masterPasswordPrompt === 'deletePasskey'
? t('txt_delete_account_passkey')
: t('txt_view_api_key');
function accountPasskeyStatusText(credential: AccountPasskeyCredential): string {
if (credential.prfStatus === 0) return t('txt_direct_unlock');
@@ -239,126 +260,398 @@ export default function SettingsPage(props: SettingsPageProps) {
window.location.reload();
}
function closeTotpManageDialog(): void {
setTotpManageDialogOpen(false);
setTotpManagePassword('');
}
async function enableTotpFromManageDialog(): Promise<void> {
if (totpLocked) return;
if (!secret.trim() || !token.trim()) {
props.onNotify?.('error', t('txt_secret_and_code_are_required'));
return;
}
try {
await props.onEnableTotp(secret, token, totpManagePassword);
setTotpLocked(true);
} catch (error) {
props.onNotify?.('error', error instanceof Error ? error.message : t('txt_enable_totp_failed'));
}
}
function closeCreatePasskeyDialog(): void {
setCreatePasskeyDialogOpen(false);
setCreatePasskeyMasterPassword('');
setAccountPasskeyName(t('txt_account_passkey'));
setAccountPasskeyDirectUnlock(false);
}
async function submitCreatePasskeyDialog(): Promise<void> {
if (!createPasskeyMasterPassword || masterPasswordPromptSubmitting) return;
setMasterPasswordPromptSubmitting(true);
try {
const credential = await props.onCreateAccountPasskey(accountPasskeyName, createPasskeyMasterPassword, accountPasskeyDirectUnlock);
if (credential) await refreshAccountPasskeys();
closeCreatePasskeyDialog();
} catch (error) {
props.onNotify?.('error', error instanceof Error ? error.message : t('txt_account_passkeys_load_failed'));
} finally {
setMasterPasswordPromptSubmitting(false);
}
}
const settingsSections: Array<{ id: SettingsSection; label: string }> = [
{ id: 'appearance', label: t('txt_settings_appearance') },
{ id: 'session', label: t('txt_session_timeout') },
{ id: 'masterPassword', label: t('txt_master_password') },
{ id: 'twoStep', label: t('txt_two_step_login') },
{ id: 'keys', label: t('txt_keys') },
];
return (
<div className="settings-modules-grid">
<section className="card settings-module">
<h3>{t('txt_session_timeout')}</h3>
<div className="session-timeout-fields">
<label className="field">
<span>{t('txt_timeout_time')}</span>
<select
className="input"
value={String(props.lockTimeoutMinutes)}
onInput={(e) => props.onLockTimeoutChange(Number((e.currentTarget as HTMLSelectElement).value) as 0 | 1 | 5 | 15 | 30)}
<div className="settings-page-categorized">
<div className="settings-category-layout">
<nav className="settings-category-tabs" aria-label={t('nav_account_settings')}>
{settingsSections.map((section) => (
<button
key={section.id}
type="button"
className={`settings-category-tab ${activeSection === section.id ? 'active' : ''}`}
onClick={() => setActiveSection(section.id)}
>
{LOCK_TIMEOUT_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{t(option.labelKey)}
</option>
))}
</select>
</label>
<label className="field">
<span>{t('txt_timeout_action')}</span>
<select
className="input"
value={props.sessionTimeoutAction}
onInput={(e) => props.onSessionTimeoutActionChange((e.currentTarget as HTMLSelectElement).value === 'logout' ? 'logout' : 'lock')}
>
<option value="logout">{t('txt_timeout_action_logout')}</option>
<option value="lock">{t('txt_timeout_action_lock')}</option>
</select>
</label>
</div>
</section>
{section.label}
</button>
))}
</nav>
<section className="card settings-module">
<h3>{t('txt_language')}</h3>
<label className="field">
<span>{t('txt_display_language')}</span>
<select
className="input"
value={selectedLocale}
onInput={(e) => void changeLocale((e.currentTarget as HTMLSelectElement).value as Locale)}
>
{AVAILABLE_LOCALES.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<div className="field-help">{t('txt_language_saved_locally')}</div>
</label>
</section>
<section className="settings-category-panel">
{activeSection === 'appearance' && (
<div className="settings-section-stack">
<section className="settings-submodule">
<label className="field">
<span>{t('txt_theme')}</span>
<select
className="input"
value={props.themePreference}
onInput={(e) => props.onThemePreferenceChange((e.currentTarget as HTMLSelectElement).value as ThemePreference)}
>
<option value="system">{t('txt_use_system_theme')}</option>
<option value="light">{t('txt_light_theme')}</option>
<option value="dark">{t('txt_dark_theme')}</option>
</select>
<div className="field-help">{t('txt_theme_saved_locally')}</div>
</label>
</section>
<section className="card settings-module">
<h3>{t('txt_change_master_password')}</h3>
<section className="settings-submodule">
<label className="field">
<span>{t('txt_display_language')}</span>
<select
className="input"
value={selectedLocale}
onInput={(e) => void changeLocale((e.currentTarget as HTMLSelectElement).value as Locale)}
>
{AVAILABLE_LOCALES.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<div className="field-help">{t('txt_display_language_help')}</div>
</label>
</section>
</div>
)}
{activeSection === 'session' && (
<div className="settings-section-stack">
<section className="settings-submodule">
<div className="session-timeout-fields">
<label className="field">
<span>{t('txt_timeout_time')}</span>
<select
className="input"
value={String(props.lockTimeoutMinutes)}
onInput={(e) => props.onLockTimeoutChange(Number((e.currentTarget as HTMLSelectElement).value) as 0 | 1 | 5 | 15 | 30)}
>
{LOCK_TIMEOUT_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{t(option.labelKey)}
</option>
))}
</select>
</label>
<label className="field">
<span>{t('txt_timeout_action')}</span>
<select
className="input"
value={props.sessionTimeoutAction}
onInput={(e) => props.onSessionTimeoutActionChange((e.currentTarget as HTMLSelectElement).value === 'logout' ? 'logout' : 'lock')}
>
<option value="logout">{t('txt_timeout_action_logout')}</option>
<option value="lock">{t('txt_timeout_action_lock')}</option>
</select>
</label>
</div>
</section>
</div>
)}
{activeSection === 'masterPassword' && (
<div className="settings-section-stack">
<section className="settings-submodule">
<h3>{t('txt_change_master_password')}</h3>
<label className="field">
<span>{t('txt_current_password')}</span>
<input
className="input"
type="password"
value={currentPassword}
onInput={(e) => setCurrentPassword((e.currentTarget as HTMLInputElement).value)}
/>
</label>
<div className="settings-vertical-fields">
<label className="field">
<span>{t('txt_new_password')}</span>
<input className="input" type="password" value={newPassword} onInput={(e) => setNewPassword((e.currentTarget as HTMLInputElement).value)} />
</label>
<label className="field">
<span>{t('txt_confirm_password')}</span>
<input className="input" type="password" value={newPassword2} onInput={(e) => setNewPassword2((e.currentTarget as HTMLInputElement).value)} />
</label>
</div>
<button
type="button"
className="btn btn-danger"
onClick={() => void props.onChangePassword(currentPassword, newPassword, newPassword2)}
>
<KeyRound size={14} className="btn-icon" />
{t('txt_change_password')}
</button>
</section>
<section className="settings-submodule">
<label className="field">
<span>{t('txt_password_hint_optional')}</span>
<input
className="input"
maxLength={120}
value={passwordHint}
placeholder={t('txt_password_hint_placeholder')}
onInput={(e) => setPasswordHint((e.currentTarget as HTMLInputElement).value)}
/>
<div className="field-help">{t('txt_password_hint_register_help')}</div>
</label>
<button
type="button"
className="btn btn-secondary"
onClick={() => void props.onSavePasswordHint(passwordHint)}
>
{t('txt_save')}
</button>
</section>
<section className="settings-submodule account-passkeys-module">
<div className="settings-module-head">
<h3>{t('txt_account_passkeys')}</h3>
<button
type="button"
className="btn btn-secondary small"
disabled={accountPasskeysLoading}
title={t('txt_refresh')}
aria-label={t('txt_refresh')}
onClick={() => void refreshAccountPasskeys()}
>
<RefreshCw size={14} className="btn-icon" />
{t('txt_refresh')}
</button>
</div>
<p className="muted-inline settings-field-note">{t('txt_account_passkey_login_only_help')}</p>
<div className="actions">
<button
type="button"
className="btn btn-primary"
disabled={masterPasswordPromptSubmitting}
onClick={() => openMasterPasswordPrompt('createPasskey')}
>
<KeyRound size={14} className="btn-icon" />
{t('txt_add_account_passkey')}
</button>
</div>
<div className="account-passkeys-list">
{accountPasskeysLoading ? (
<div className="settings-module-placeholder">
<RefreshCw size={20} />
<span>{t('txt_loading')}</span>
</div>
) : accountPasskeys.length === 0 ? (
<div className="settings-module-placeholder">
<KeyRound size={20} />
<span>{t('txt_no_account_passkeys')}</span>
</div>
) : (
accountPasskeys.map((credential) => (
<div key={credential.id} className="account-passkey-row">
<div className="account-passkey-main">
<strong>{credential.name || t('txt_account_passkey')}</strong>
<small>{t('txt_created_value', { value: formatDateTime(credential.creationDate) })}</small>
</div>
<span className={`account-passkey-status account-passkey-status-${credential.prfStatus}`}>
{accountPasskeyStatusText(credential)}
</span>
<div className="actions account-passkey-actions">
{credential.prfStatus === 1 && (
<button
type="button"
className="btn btn-secondary small"
disabled={masterPasswordPromptSubmitting}
onClick={() => openMasterPasswordPrompt('enablePasskeyDirectUnlock', credential.id)}
>
<ShieldCheck size={14} className="btn-icon" />
{t('txt_enable_passkey_direct_unlock')}
</button>
)}
<button
type="button"
className="btn btn-danger small"
disabled={masterPasswordPromptSubmitting}
onClick={() => openMasterPasswordPrompt('deletePasskey', credential.id)}
>
<Trash2 size={14} className="btn-icon" />
{t('txt_delete')}
</button>
</div>
</div>
))
)}
</div>
</section>
</div>
)}
{activeSection === 'twoStep' && (
<div className="settings-section-stack">
<section className="settings-submodule two-step-recovery-warning">
<div className="two-step-warning-head">
<ShieldOff size={16} aria-hidden="true" />
<strong>{t('txt_warning')}</strong>
</div>
<p>{t('txt_two_step_recovery_code_warning')}</p>
<button type="button" className="btn btn-danger" onClick={() => openMasterPasswordPrompt('recovery')}>
{t('txt_view_recovery_code')}
</button>
</section>
<section className="settings-submodule two-step-providers-module">
<h3>{t('txt_providers')}</h3>
<div className="two-step-provider-list">
<div className="two-step-provider-row">
<div className="two-step-provider-icon">
<ShieldCheck size={28} />
</div>
<div className="two-step-provider-copy">
<div className="two-step-provider-title">
<strong>{t('txt_authenticator_app')}</strong>
{totpLocked && <span className="two-step-enabled-badge">{t('txt_enabled')}</span>}
</div>
<span>{t('txt_authenticator_app_help')}</span>
</div>
<button type="button" className="btn btn-secondary" onClick={() => openMasterPasswordPrompt('manageTotp')}>
{t('txt_manage')}
</button>
</div>
<div className="two-step-provider-row">
<div className="two-step-provider-icon">
<KeyRound size={28} />
</div>
<div className="two-step-provider-copy">
<strong>{t('txt_passkeys')}</strong>
<span>{t('txt_passkey_provider_help')}</span>
</div>
<button type="button" className="btn btn-secondary" disabled>
{t('txt_manage')}
</button>
</div>
<div className="two-step-provider-row">
<div className="two-step-provider-icon two-step-provider-yubico">yubico</div>
<div className="two-step-provider-copy">
<strong>{t('txt_yubico_otp_security_key')}</strong>
<span>{t('txt_yubico_otp_security_key_help')}</span>
</div>
<button type="button" className="btn btn-secondary" disabled>
{t('txt_manage')}
</button>
</div>
</div>
</section>
</div>
)}
{activeSection === 'keys' && (
<div className="settings-section-stack">
<section className="settings-submodule sensitive-action">
<div>
<h3>{t('txt_api_key')}</h3>
<p className="muted-inline settings-field-note">{t('txt_api_key_dialog_intro')}</p>
</div>
<div className="actions">
<button type="button" className="btn btn-secondary" onClick={() => openMasterPasswordPrompt('apiKey')}>
<KeyRound size={14} className="btn-icon" />
{t('txt_view_api_key')}
</button>
<button
type="button"
className="btn btn-secondary"
onClick={() => setRotateApiKeyConfirmOpen(true)}
>
<RefreshCw size={14} className="btn-icon" />
{t('txt_rotate_api_key')}
</button>
</div>
</section>
</div>
)}
</section>
</div>
<ConfirmDialog
open={masterPasswordPrompt !== null}
title={masterPasswordPromptTitle}
message={t('txt_enter_master_password_to_continue')}
confirmText={t('txt_continue')}
cancelText={t('txt_cancel')}
confirmDisabled={masterPasswordPromptSubmitting || !masterPasswordPromptValue.trim()}
cancelDisabled={masterPasswordPromptSubmitting}
onConfirm={() => void submitMasterPasswordPrompt()}
onCancel={closeMasterPasswordPrompt}
>
<label className="field">
<span>{t('txt_current_password')}</span>
<span>{t('txt_master_password')}</span>
<input
className="input"
type="password"
value={currentPassword}
onInput={(e) => setCurrentPassword((e.currentTarget as HTMLInputElement).value)}
autoComplete="current-password"
value={masterPasswordPromptValue}
onInput={(e) => setMasterPasswordPromptValue((e.currentTarget as HTMLInputElement).value)}
/>
</label>
<div className="field-grid">
<label className="field">
<span>{t('txt_new_password')}</span>
<input className="input" type="password" value={newPassword} onInput={(e) => setNewPassword((e.currentTarget as HTMLInputElement).value)} />
</label>
<label className="field">
<span>{t('txt_confirm_password')}</span>
<input className="input" type="password" value={newPassword2} onInput={(e) => setNewPassword2((e.currentTarget as HTMLInputElement).value)} />
</label>
</div>
<button
type="button"
className="btn btn-danger"
onClick={() => void props.onChangePassword(currentPassword, newPassword, newPassword2)}
>
<KeyRound size={14} className="btn-icon" />
{t('txt_change_password')}
</button>
</section>
<section className="card settings-module">
<h3>{t('txt_password_hint_optional')}</h3>
<label className="field">
<span>{t('txt_password_hint')}</span>
<input
className="input"
maxLength={120}
value={passwordHint}
placeholder={t('txt_password_hint_placeholder')}
onInput={(e) => setPasswordHint((e.currentTarget as HTMLInputElement).value)}
/>
<div className="field-help">{t('txt_password_hint_register_help')}</div>
</label>
<button
type="button"
className="btn btn-secondary"
onClick={() => void props.onSavePasswordHint(passwordHint)}
>
{t('txt_save_profile')}
</button>
</section>
<section className="card settings-module">
<div className="settings-module-head">
<h3>{t('txt_totp')}</h3>
{totpLocked && (
<span className="totp-status-pill">
<ShieldCheck size={14} aria-hidden="true" />
{t('txt_enabled')}
</span>
)}
</div>
<div className="totp-grid">
<div className="totp-qr">
<img src={qrDataUrl} alt="TOTP QR" />
</div>
<div>
</ConfirmDialog>
<ConfirmDialog
open={totpManageDialogOpen}
title={t('txt_totp')}
message={totpLocked ? t('txt_totp_enabled') : t('txt_totp_manage_intro')}
hideCancel
hideConfirm
closeButton
onConfirm={() => {}}
onCancel={closeTotpManageDialog}
>
<div className="totp-manage-dialog-body">
<div className="totp-grid">
<div className="totp-qr">
<img src={qrDataUrl} alt="TOTP QR" />
</div>
<div>
<label className="field">
<span>{t('txt_authenticator_key')}</span>
@@ -395,196 +688,90 @@ export default function SettingsPage(props: SettingsPageProps) {
<input className="input" value={token} disabled={totpLocked} onInput={(e) => setToken((e.currentTarget as HTMLInputElement).value)} />
</label>
<div className="actions">
<button type="button" className="btn btn-primary" disabled={totpLocked} onClick={() => void enableTotp()}>
<ShieldCheck size={14} className="btn-icon" />
{totpLocked ? t('txt_enabled') : t('txt_enable_totp')}
</button>
<button type="button" className="btn btn-danger" disabled={!totpLocked} onClick={props.onOpenDisableTotp}>
<ShieldOff size={14} className="btn-icon" />
{t('txt_disable_totp')}
</button>
</div>
</div>
</div>
</div>
</section>
<section className="card settings-module account-passkeys-module">
<div className="settings-module-head">
<h3>{t('txt_account_passkeys')}</h3>
<button
type="button"
className="btn btn-secondary small"
disabled={accountPasskeysLoading}
title={t('txt_refresh')}
aria-label={t('txt_refresh')}
onClick={() => void refreshAccountPasskeys()}
>
<RefreshCw size={14} className="btn-icon" />
{t('txt_refresh')}
</button>
</div>
<div className="field-grid">
<label className="field">
<span>{t('txt_passkey_name')}</span>
<input
className="input"
maxLength={128}
value={accountPasskeyName}
placeholder={t('txt_account_passkey_name_placeholder')}
onInput={(e) => setAccountPasskeyName((e.currentTarget as HTMLInputElement).value)}
/>
</label>
<div className="field account-passkey-mode-field">
<span>{t('txt_account_passkey_mode')}</span>
<label className="account-passkey-toggle">
<input
type="checkbox"
checked={accountPasskeyDirectUnlock}
onInput={(e) => setAccountPasskeyDirectUnlock((e.currentTarget as HTMLInputElement).checked)}
/>
<span>{t('txt_account_passkey_direct_unlock_mode')}</span>
</label>
<div className="field-help">
{accountPasskeyDirectUnlock ? t('txt_account_passkey_direct_unlock_help') : t('txt_account_passkey_login_only_help')}
</div>
</div>
</div>
<div className="actions">
<button
type="button"
className="btn btn-primary"
disabled={masterPasswordPromptSubmitting}
onClick={() => openMasterPasswordPrompt('createPasskey')}
>
<KeyRound size={14} className="btn-icon" />
{t('txt_add_account_passkey')}
</button>
</div>
<div className="account-passkeys-list">
{accountPasskeysLoading ? (
<div className="settings-module-placeholder">
<RefreshCw size={20} />
<span>{t('txt_loading')}</span>
</div>
) : accountPasskeys.length === 0 ? (
<div className="settings-module-placeholder">
<KeyRound size={20} />
<span>{t('txt_no_account_passkeys')}</span>
</div>
) : (
accountPasskeys.map((credential) => (
<div key={credential.id} className="account-passkey-row">
<div className="account-passkey-main">
<strong>{credential.name || t('txt_account_passkey')}</strong>
<small>{t('txt_created_value', { value: formatDateTime(credential.creationDate) })}</small>
</div>
<span className={`account-passkey-status account-passkey-status-${credential.prfStatus}`}>
{accountPasskeyStatusText(credential)}
</span>
<div className="actions account-passkey-actions">
{credential.prfStatus === 1 && (
<button
type="button"
className="btn btn-secondary small"
disabled={masterPasswordPromptSubmitting}
onClick={() => openMasterPasswordPrompt('enablePasskeyDirectUnlock', credential.id)}
>
<ShieldCheck size={14} className="btn-icon" />
{t('txt_enable_passkey_direct_unlock')}
</button>
)}
{totpLocked ? (
<button
type="button"
className="btn btn-danger small"
disabled={masterPasswordPromptSubmitting}
onClick={() => openMasterPasswordPrompt('deletePasskey', credential.id)}
className="btn btn-danger"
onClick={() => {
closeTotpManageDialog();
props.onOpenDisableTotp();
}}
>
<Trash2 size={14} className="btn-icon" />
{t('txt_delete')}
<ShieldOff size={14} className="btn-icon" />
{t('txt_disable_totp')}
</button>
</div>
) : (
<button type="button" className="btn btn-primary" disabled={!totpManagePassword} onClick={() => void enableTotpFromManageDialog()}>
<ShieldCheck size={14} className="btn-icon" />
{t('txt_enable_totp')}
</button>
)}
</div>
))
)}
</div>
</section>
<section className="settings-module sensitive-actions-module">
<div className="sensitive-actions-grid">
<div className="sensitive-action">
<div>
<h4>{t('txt_recovery_code')}</h4>
<p className="muted-inline settings-field-note">
{t('txt_this_is_a_one_time_code_after_it_is_used_a_new_code_is_generated_automatically')}
</p>
</div>
<div className="actions">
<button type="button" className="btn btn-secondary" onClick={() => openMasterPasswordPrompt('recovery')}>
<ShieldCheck size={14} className="btn-icon" />
{t('txt_view_recovery_code')}
</button>
<button
type="button"
className="btn btn-secondary"
disabled={!recoveryCode}
onClick={() => {
void copyTextToClipboard(recoveryCode, { successMessage: t('txt_recovery_code_copied') });
}}
>
<Clipboard size={14} className="btn-icon" />
{t('txt_copy_code')}
</button>
</div>
{recoveryCode && (
<div className="recovery-code-card">
<div className="recovery-code-value">{recoveryCode}</div>
</div>
)}
</div>
<div className="sensitive-action">
<div>
<h4>{t('txt_api_key')}</h4>
<p className="muted-inline settings-field-note">{t('txt_api_key_dialog_intro')}</p>
</div>
<div className="actions">
<button type="button" className="btn btn-secondary" onClick={() => openMasterPasswordPrompt('apiKey')}>
<KeyRound size={14} className="btn-icon" />
{t('txt_view_api_key')}
</button>
<button
type="button"
className="btn btn-secondary"
onClick={() => setRotateApiKeyConfirmOpen(true)}
>
<RefreshCw size={14} className="btn-icon" />
{t('txt_rotate_api_key')}
</button>
</div>
</div>
</div>
</section>
</ConfirmDialog>
<ConfirmDialog
open={masterPasswordPrompt !== null}
title={masterPasswordPromptTitle}
message={t('txt_enter_master_password_to_continue')}
confirmText={t('txt_continue')}
open={recoveryCodeDialogOpen}
title={`${t('txt_two_step_login')} ${t('txt_recovery_code')}`}
message={t('txt_your_two_step_recovery_code')}
hideConfirm
closeButton
cancelText={t('txt_close')}
onConfirm={() => {}}
onCancel={() => setRecoveryCodeDialogOpen(false)}
afterActions={(
<button
type="button"
className="btn btn-primary dialog-btn"
disabled={!recoveryCode}
onClick={() => {
void copyTextToClipboard(recoveryCode, { successMessage: t('txt_recovery_code_copied') });
}}
>
<Clipboard size={14} className="btn-icon" />
{t('txt_copy_code')}
</button>
)}
>
<div className="two-step-recovery-code-dialog-value">{recoveryCode}</div>
</ConfirmDialog>
<ConfirmDialog
open={createPasskeyDialogOpen}
title={t('txt_add_account_passkey')}
message={t('txt_name_account_passkey_after_verification')}
confirmText={t('txt_save')}
cancelText={t('txt_cancel')}
confirmDisabled={masterPasswordPromptSubmitting || !masterPasswordPromptValue.trim()}
confirmDisabled={masterPasswordPromptSubmitting}
cancelDisabled={masterPasswordPromptSubmitting}
onConfirm={() => void submitMasterPasswordPrompt()}
onCancel={closeMasterPasswordPrompt}
onConfirm={() => void submitCreatePasskeyDialog()}
onCancel={closeCreatePasskeyDialog}
>
<label className="field">
<span>{t('txt_master_password')}</span>
<span>{t('txt_passkey_name')}</span>
<input
className="input"
type="password"
autoComplete="current-password"
value={masterPasswordPromptValue}
onInput={(e) => setMasterPasswordPromptValue((e.currentTarget as HTMLInputElement).value)}
maxLength={128}
value={accountPasskeyName}
placeholder={t('txt_account_passkey_name_placeholder')}
onInput={(e) => setAccountPasskeyName((e.currentTarget as HTMLInputElement).value)}
/>
<div className="field-help">{t('txt_account_passkey_name_help')}</div>
</label>
<div className="field account-passkey-mode-field">
<span>{t('txt_account_passkey_mode')}</span>
<label className="account-passkey-toggle">
<input
type="checkbox"
checked={accountPasskeyDirectUnlock}
onInput={(e) => setAccountPasskeyDirectUnlock((e.currentTarget as HTMLInputElement).checked)}
/>
<span>{t('txt_account_passkey_direct_unlock_mode')}</span>
</label>
<div className="field-help">
{accountPasskeyDirectUnlock ? t('txt_account_passkey_direct_unlock_help') : t('txt_account_passkey_login_only_help')}
</div>
</div>
</ConfirmDialog>
<ConfirmDialog
open={apiKeyDialogOpen}