feat: add TOTP secret input actions and enhance dark mode styles

This commit is contained in:
shuaiplus
2026-04-27 02:15:41 +08:00
parent bfd347a52c
commit 575cf7ca79
5 changed files with 173 additions and 25 deletions
+4 -2
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'preact/hooks';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { CheckCheck, ChevronLeft, Copy, Eye, EyeOff, File, FileText, LayoutGrid, Pencil, Plus, RefreshCw, Save, Send as SendIcon, Trash2, X } from 'lucide-preact';
import { copyTextToClipboard } from '@/lib/clipboard';
import type { Send, SendDraft } from '@/lib/types';
@@ -79,6 +79,7 @@ export default function SendsPage(props: SendsPageProps) {
const [isMobileLayout, setIsMobileLayout] = useState(getInitialIsMobileLayout);
const [mobilePanel, setMobilePanel] = useState<'list' | 'detail' | 'edit'>('list');
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const mobileSidebarToggleKeyRef = useRef(props.mobileSidebarToggleKey);
const [autoCopyLink, setAutoCopyLink] = useState<boolean>(() => {
try {
return localStorage.getItem(AUTO_COPY_KEY) === '1';
@@ -108,7 +109,8 @@ export default function SendsPage(props: SendsPageProps) {
}, []);
useEffect(() => {
if (!props.mobileSidebarToggleKey) return;
if (props.mobileSidebarToggleKey === mobileSidebarToggleKeyRef.current) return;
mobileSidebarToggleKeyRef.current = props.mobileSidebarToggleKey;
setMobileSidebarOpen((open) => !open);
}, [props.mobileSidebarToggleKey]);
+30 -19
View File
@@ -269,7 +269,33 @@ export default function SettingsPage(props: SettingsPageProps) {
<div>
<label className="field">
<span>{t('txt_authenticator_key')}</span>
<input className="input" value={secret} disabled={totpLocked} onInput={(e) => setSecret((e.currentTarget as HTMLInputElement).value.toUpperCase())} />
<div className="totp-secret-input-wrap">
<input className="input totp-secret-input" value={secret} disabled={totpLocked} onInput={(e) => setSecret((e.currentTarget as HTMLInputElement).value.toUpperCase())} />
<div className="totp-secret-actions">
<button
type="button"
className="btn btn-secondary small totp-secret-icon-btn"
disabled={totpLocked}
title={t('txt_regenerate')}
aria-label={t('txt_regenerate')}
onClick={() => setSecret(randomBase32Secret(32))}
>
<RefreshCw size={14} className="btn-icon" />
</button>
<button
type="button"
className="btn btn-secondary small totp-secret-icon-btn"
disabled={totpLocked}
title={t('txt_copy_secret')}
aria-label={t('txt_copy_secret')}
onClick={() => {
void copyTextToClipboard(secret, { successMessage: t('txt_secret_copied') });
}}
>
<Clipboard size={14} className="btn-icon" />
</button>
</div>
</div>
</label>
<label className="field">
<span>{t('txt_verification_code')}</span>
@@ -280,29 +306,14 @@ export default function SettingsPage(props: SettingsPageProps) {
<ShieldCheck size={14} className="btn-icon" />
{totpLocked ? t('txt_enabled') : t('txt_enable_totp')}
</button>
<button type="button" className="btn btn-secondary" disabled={totpLocked} onClick={() => setSecret(randomBase32Secret(32))}>
<RefreshCw size={14} className="btn-icon" />
{t('txt_regenerate')}
</button>
<button
type="button"
className="btn btn-secondary"
disabled={totpLocked}
onClick={() => {
void copyTextToClipboard(secret, { successMessage: t('txt_secret_copied') });
}}
>
<Clipboard size={14} className="btn-icon" />
{t('txt_copy_secret')}
<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>
<button type="button" className="btn btn-danger" disabled={!totpLocked} onClick={props.onOpenDisableTotp}>
<ShieldOff size={14} className="btn-icon" />
{t('txt_disable_totp')}
</button>
</section>
<section className="card settings-module">
+3 -1
View File
@@ -127,6 +127,7 @@ export default function VaultPage(props: VaultPageProps) {
const folderSortMenuRef = useRef<HTMLDivElement | null>(null);
const attachmentInputRef = useRef<HTMLInputElement | null>(null);
const listPanelRef = useRef<HTMLDivElement | null>(null);
const mobileSidebarToggleKeyRef = useRef(props.mobileSidebarToggleKey);
const suppressNextSortScrollRef = useRef(false);
const sshSeedTicketRef = useRef(0);
const sshFingerprintTicketRef = useRef(0);
@@ -147,7 +148,8 @@ export default function VaultPage(props: VaultPageProps) {
}, []);
useEffect(() => {
if (!props.mobileSidebarToggleKey) return;
if (props.mobileSidebarToggleKey === mobileSidebarToggleKeyRef.current) return;
mobileSidebarToggleKeyRef.current = props.mobileSidebarToggleKey;
setMobileSidebarOpen((open) => !open);
}, [props.mobileSidebarToggleKey]);
+115 -3
View File
@@ -166,10 +166,23 @@
-webkit-backdrop-filter: blur(12px);
}
:root[data-theme='dark'] .user-chip {
background: color-mix(in srgb, var(--panel) 86%, transparent);
border-color: var(--line);
color: var(--text);
box-shadow: var(--shadow-sm);
}
:root[data-theme='dark'] .user-chip:hover {
background: var(--panel-subtle);
border-color: color-mix(in srgb, var(--primary) 24%, var(--line));
}
/* ── dark mode depth ── */
:root[data-theme='dark'] .card,
:root[data-theme='dark'] .list-panel,
:root[data-theme='dark'] .sidebar-block {
:root[data-theme='dark'] .sidebar-block,
:root[data-theme='dark'] .mobile-sidebar-sheet {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.20), 0 8px 24px rgba(0, 0, 0, 0.16);
}
@@ -181,6 +194,105 @@
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.24), 0 0 0 1px rgba(139, 184, 255, 0.12);
}
:root[data-theme='dark'] .list-item.active {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), inset 4px 0 0 rgba(139, 184, 255, 0.70);
:root[data-theme='dark'] .mobile-sidebar-sheet,
:root[data-theme='dark'] .mobile-sidebar-close,
:root[data-theme='dark'] .table tr,
:root[data-theme='dark'] .settings-subcard,
:root[data-theme='dark'] .import-summary-table-wrap,
:root[data-theme='dark'] .backup-help-bubble,
:root[data-theme='dark'] .backup-recommendation-card,
:root[data-theme='dark'] .backup-recommendation-dav-item,
:root[data-theme='dark'] .backup-browser-path,
:root[data-theme='dark'] .backup-browser-list,
:root[data-theme='dark'] .restore-progress-card,
:root[data-theme='dark'] .restore-progress-current,
:root[data-theme='dark'] .restore-progress-elapsed {
background: var(--panel);
border-color: var(--line);
color: var(--text);
}
:root[data-theme='dark'] .mobile-sidebar-title,
:root[data-theme='dark'] .import-summary-close,
:root[data-theme='dark'] .backup-recommendation-group-title,
:root[data-theme='dark'] .backup-browser-path strong,
:root[data-theme='dark'] .restore-progress-current strong,
:root[data-theme='dark'] .custom-field-check span,
:root[data-theme='dark'] .notes {
color: var(--text);
}
:root[data-theme='dark'] .backup-help-bubble::before {
background: var(--panel);
border-color: var(--line);
}
:root[data-theme='dark'] .mobile-sidebar-close:hover,
:root[data-theme='dark'] .mobile-sidebar-sheet .tree-btn.active,
:root[data-theme='dark'] .mobile-settings-link.active,
:root[data-theme='dark'] .backup-destination-item.active,
:root[data-theme='dark'] .backup-interval-preset.active {
background: color-mix(in srgb, var(--primary) 14%, var(--panel));
color: var(--primary-strong);
}
:root[data-theme='dark'] .table td,
:root[data-theme='dark'] .attachment-row,
:root[data-theme='dark'] .custom-field-card,
:root[data-theme='dark'] .kv-line,
:root[data-theme='dark'] .kv-row,
:root[data-theme='dark'] .import-summary-table th,
:root[data-theme='dark'] .import-summary-table td,
:root[data-theme='dark'] .restore-progress-card,
:root[data-theme='dark'] .restore-progress-current,
:root[data-theme='dark'] .restore-progress-elapsed {
border-color: var(--line-soft);
}
:root[data-theme='dark'] .import-summary-table th {
background: var(--panel-muted);
color: var(--muted-strong);
}
:root[data-theme='dark'] .import-summary-failed-list {
background: color-mix(in srgb, var(--danger) 12%, var(--panel));
border-color: color-mix(in srgb, var(--danger) 34%, var(--line));
color: var(--danger);
}
:root[data-theme='dark'] .backup-help-trigger,
:root[data-theme='dark'] .backup-destination-type,
:root[data-theme='dark'] .backup-interval-preset,
:root[data-theme='dark'] .restore-progress-meter {
background: var(--panel-muted);
border-color: var(--line);
color: var(--muted-strong);
}
:root[data-theme='dark'] .backup-destination-item:hover,
:root[data-theme='dark'] .backup-interval-preset:hover:not(:disabled) {
background: var(--panel-subtle);
border-color: color-mix(in srgb, var(--primary) 34%, var(--line));
color: var(--primary-strong);
}
:root[data-theme='dark'] .backup-help-bubble,
:root[data-theme='dark'] .backup-recommendation-step,
:root[data-theme='dark'] .backup-recommendation-inline-note,
:root[data-theme='dark'] .backup-recommendation-linked-item,
:root[data-theme='dark'] .backup-browser-meta,
:root[data-theme='dark'] .backup-browser-empty,
:root[data-theme='dark'] .backup-inline-note,
:root[data-theme='dark'] .restore-progress-kicker,
:root[data-theme='dark'] .restore-progress-subtitle,
:root[data-theme='dark'] .restore-progress-current p,
:root[data-theme='dark'] .restore-progress-item,
:root[data-theme='dark'] .check-line {
color: var(--muted);
}
:root[data-theme='dark'] .restore-progress-overlay {
background: var(--overlay-strong);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
+21
View File
@@ -392,6 +392,27 @@
@apply h-[180px] w-[180px] rounded-lg bg-white;
}
.totp-secret-input-wrap {
@apply relative;
}
.totp-secret-input {
padding-right: 84px;
}
.totp-secret-actions {
@apply absolute right-2 top-1/2 inline-flex items-center gap-1;
transform: translateY(-50%);
}
.totp-secret-icon-btn {
@apply h-8 w-8 min-w-8 gap-0 rounded-lg p-0;
}
.totp-secret-icon-btn .btn-icon {
@apply m-0;
}
.section-head {
@apply mb-2.5 flex items-center justify-between;
}