mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
feat: add TOTP secret input actions and enhance dark mode styles
This commit is contained in:
@@ -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]);
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user