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 { 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 { copyTextToClipboard } from '@/lib/clipboard';
|
||||||
import type { Send, SendDraft } from '@/lib/types';
|
import type { Send, SendDraft } from '@/lib/types';
|
||||||
@@ -79,6 +79,7 @@ export default function SendsPage(props: SendsPageProps) {
|
|||||||
const [isMobileLayout, setIsMobileLayout] = useState(getInitialIsMobileLayout);
|
const [isMobileLayout, setIsMobileLayout] = useState(getInitialIsMobileLayout);
|
||||||
const [mobilePanel, setMobilePanel] = useState<'list' | 'detail' | 'edit'>('list');
|
const [mobilePanel, setMobilePanel] = useState<'list' | 'detail' | 'edit'>('list');
|
||||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||||
|
const mobileSidebarToggleKeyRef = useRef(props.mobileSidebarToggleKey);
|
||||||
const [autoCopyLink, setAutoCopyLink] = useState<boolean>(() => {
|
const [autoCopyLink, setAutoCopyLink] = useState<boolean>(() => {
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem(AUTO_COPY_KEY) === '1';
|
return localStorage.getItem(AUTO_COPY_KEY) === '1';
|
||||||
@@ -108,7 +109,8 @@ export default function SendsPage(props: SendsPageProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!props.mobileSidebarToggleKey) return;
|
if (props.mobileSidebarToggleKey === mobileSidebarToggleKeyRef.current) return;
|
||||||
|
mobileSidebarToggleKeyRef.current = props.mobileSidebarToggleKey;
|
||||||
setMobileSidebarOpen((open) => !open);
|
setMobileSidebarOpen((open) => !open);
|
||||||
}, [props.mobileSidebarToggleKey]);
|
}, [props.mobileSidebarToggleKey]);
|
||||||
|
|
||||||
|
|||||||
@@ -269,7 +269,33 @@ export default function SettingsPage(props: SettingsPageProps) {
|
|||||||
<div>
|
<div>
|
||||||
<label className="field">
|
<label className="field">
|
||||||
<span>{t('txt_authenticator_key')}</span>
|
<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>
|
||||||
<label className="field">
|
<label className="field">
|
||||||
<span>{t('txt_verification_code')}</span>
|
<span>{t('txt_verification_code')}</span>
|
||||||
@@ -280,29 +306,14 @@ export default function SettingsPage(props: SettingsPageProps) {
|
|||||||
<ShieldCheck size={14} className="btn-icon" />
|
<ShieldCheck size={14} className="btn-icon" />
|
||||||
{totpLocked ? t('txt_enabled') : t('txt_enable_totp')}
|
{totpLocked ? t('txt_enabled') : t('txt_enable_totp')}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="btn btn-secondary" disabled={totpLocked} onClick={() => setSecret(randomBase32Secret(32))}>
|
<button type="button" className="btn btn-danger" disabled={!totpLocked} onClick={props.onOpenDisableTotp}>
|
||||||
<RefreshCw size={14} className="btn-icon" />
|
<ShieldOff size={14} className="btn-icon" />
|
||||||
{t('txt_regenerate')}
|
{t('txt_disable_totp')}
|
||||||
</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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<section className="card settings-module">
|
<section className="card settings-module">
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ export default function VaultPage(props: VaultPageProps) {
|
|||||||
const folderSortMenuRef = useRef<HTMLDivElement | null>(null);
|
const folderSortMenuRef = useRef<HTMLDivElement | null>(null);
|
||||||
const attachmentInputRef = useRef<HTMLInputElement | null>(null);
|
const attachmentInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const listPanelRef = useRef<HTMLDivElement | null>(null);
|
const listPanelRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const mobileSidebarToggleKeyRef = useRef(props.mobileSidebarToggleKey);
|
||||||
const suppressNextSortScrollRef = useRef(false);
|
const suppressNextSortScrollRef = useRef(false);
|
||||||
const sshSeedTicketRef = useRef(0);
|
const sshSeedTicketRef = useRef(0);
|
||||||
const sshFingerprintTicketRef = useRef(0);
|
const sshFingerprintTicketRef = useRef(0);
|
||||||
@@ -147,7 +148,8 @@ export default function VaultPage(props: VaultPageProps) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!props.mobileSidebarToggleKey) return;
|
if (props.mobileSidebarToggleKey === mobileSidebarToggleKeyRef.current) return;
|
||||||
|
mobileSidebarToggleKeyRef.current = props.mobileSidebarToggleKey;
|
||||||
setMobileSidebarOpen((open) => !open);
|
setMobileSidebarOpen((open) => !open);
|
||||||
}, [props.mobileSidebarToggleKey]);
|
}, [props.mobileSidebarToggleKey]);
|
||||||
|
|
||||||
|
|||||||
+115
-3
@@ -166,10 +166,23 @@
|
|||||||
-webkit-backdrop-filter: blur(12px);
|
-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 ── */
|
/* ── dark mode depth ── */
|
||||||
:root[data-theme='dark'] .card,
|
:root[data-theme='dark'] .card,
|
||||||
:root[data-theme='dark'] .list-panel,
|
: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);
|
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);
|
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 {
|
:root[data-theme='dark'] .mobile-sidebar-sheet,
|
||||||
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-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;
|
@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 {
|
.section-head {
|
||||||
@apply mb-2.5 flex items-center justify-between;
|
@apply mb-2.5 flex items-center justify-between;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user