mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
feat: add backup start time configuration and theme switch functionality
- Introduced BACKUP_DEFAULT_START_TIME constant for backup scheduling. - Updated BackupScheduleConfig interface to include startTime. - Implemented normalizeStartTime function for validating and normalizing start time input. - Enhanced backup settings parsing to accommodate start time. - Added start time input field in BackupDestinationDetail component. - Created ThemeSwitch component for toggling between light and dark themes. - Integrated theme preference management in App component. - Updated styles for dark mode support across the application. - Added translations for theme toggle and backup start time labels.
This commit is contained in:
@@ -57,11 +57,26 @@ const IMPORT_ROUTE_PATHS = [IMPORT_ROUTE, '/tools/import', '/tools/import-export
|
||||
const IMPORT_ROUTE_ALIASES: ReadonlySet<string> = new Set(IMPORT_ROUTE_PATHS.filter((path) => path !== IMPORT_ROUTE));
|
||||
const SETTINGS_HOME_ROUTE = '/settings';
|
||||
const SETTINGS_ACCOUNT_ROUTE = '/settings/account';
|
||||
const THEME_STORAGE_KEY = 'nodewarden.theme.preference.v1';
|
||||
const SIGNALR_RECORD_SEPARATOR = String.fromCharCode(0x1e);
|
||||
const SIGNALR_UPDATE_TYPE_SYNC_VAULT = 5;
|
||||
const SIGNALR_UPDATE_TYPE_LOG_OUT = 11;
|
||||
const SIGNALR_UPDATE_TYPE_DEVICE_STATUS = 12;
|
||||
|
||||
type ThemePreference = 'system' | 'light' | 'dark';
|
||||
|
||||
function readThemePreference(): ThemePreference {
|
||||
if (typeof window === 'undefined') return 'system';
|
||||
const stored = String(window.localStorage.getItem(THEME_STORAGE_KEY) || '').trim();
|
||||
if (stored === 'light' || stored === 'dark' || stored === 'system') return stored;
|
||||
return 'system';
|
||||
}
|
||||
|
||||
function resolveSystemTheme(): 'light' | 'dark' {
|
||||
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return 'light';
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const initialBootstrap = useMemo(() => readInitialAppBootstrapState(), []);
|
||||
const initialInviteCode = useMemo(() => readInviteCodeFromUrl(), []);
|
||||
@@ -100,6 +115,8 @@ export default function App() {
|
||||
const [disableTotpOpen, setDisableTotpOpen] = useState(false);
|
||||
const [disableTotpPassword, setDisableTotpPassword] = useState('');
|
||||
const [recoverValues, setRecoverValues] = useState({ email: '', password: '', recoveryCode: '' });
|
||||
const [themePreference, setThemePreference] = useState<ThemePreference>(() => readThemePreference());
|
||||
const [systemTheme, setSystemTheme] = useState<'light' | 'dark'>(() => resolveSystemTheme());
|
||||
|
||||
const [confirm, setConfirm] = useState<AppConfirmState | null>(null);
|
||||
const [mobileLayout, setMobileLayout] = useState(false);
|
||||
@@ -175,6 +192,39 @@ export default function App() {
|
||||
return () => media.removeListener(sync);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
|
||||
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const sync = () => setSystemTheme(media.matches ? 'dark' : 'light');
|
||||
sync();
|
||||
if (typeof media.addEventListener === 'function') {
|
||||
media.addEventListener('change', sync);
|
||||
return () => media.removeEventListener('change', sync);
|
||||
}
|
||||
media.addListener(sync);
|
||||
return () => media.removeListener(sync);
|
||||
}, []);
|
||||
|
||||
const resolvedTheme = themePreference === 'system' ? systemTheme : themePreference;
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof document === 'undefined') return;
|
||||
document.documentElement.dataset.theme = resolvedTheme;
|
||||
document.documentElement.style.colorScheme = resolvedTheme;
|
||||
}, [resolvedTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.localStorage.setItem(THEME_STORAGE_KEY, themePreference);
|
||||
}, [themePreference]);
|
||||
|
||||
function handleToggleTheme() {
|
||||
setThemePreference((prev) => {
|
||||
const current = prev === 'system' ? systemTheme : prev;
|
||||
return current === 'dark' ? 'light' : 'dark';
|
||||
});
|
||||
}
|
||||
|
||||
function setSession(next: SessionState | null) {
|
||||
sessionRef.current = next;
|
||||
setSessionState(next);
|
||||
@@ -1135,8 +1185,11 @@ export default function App() {
|
||||
settingsAccountRoute={SETTINGS_ACCOUNT_ROUTE}
|
||||
importRoute={IMPORT_ROUTE}
|
||||
isImportRoute={isImportRoute}
|
||||
darkMode={resolvedTheme === 'dark'}
|
||||
themeToggleTitle={resolvedTheme === 'dark' ? t('txt_switch_to_light_mode') : t('txt_switch_to_dark_mode')}
|
||||
onLock={handleLock}
|
||||
onLogout={handleLogout}
|
||||
onToggleTheme={handleToggleTheme}
|
||||
mainRoutesProps={mainRoutesProps}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ArrowUpDown, Cloud, Clock3, Folder as FolderIcon, KeyRound, Lock, LogOut, Send as SendIcon, Settings as SettingsIcon, Shield, ShieldUser } from 'lucide-preact';
|
||||
import { Link } from 'wouter';
|
||||
import AppMainRoutes from '@/components/AppMainRoutes';
|
||||
import ThemeSwitch from '@/components/ThemeSwitch';
|
||||
import type { AppMainRoutesProps } from '@/components/AppMainRoutes';
|
||||
import { t } from '@/lib/i18n';
|
||||
import type { Profile } from '@/lib/types';
|
||||
@@ -15,8 +16,11 @@ interface AppAuthenticatedShellProps {
|
||||
settingsAccountRoute: string;
|
||||
importRoute: string;
|
||||
isImportRoute: boolean;
|
||||
darkMode: boolean;
|
||||
themeToggleTitle: string;
|
||||
onLock: () => void;
|
||||
onLogout: () => void;
|
||||
onToggleTheme: () => void;
|
||||
mainRoutesProps: AppMainRoutesProps;
|
||||
}
|
||||
|
||||
@@ -35,6 +39,7 @@ export default function AppAuthenticatedShell(props: AppAuthenticatedShellProps)
|
||||
<ShieldUser size={16} />
|
||||
<span>{props.profile?.email}</span>
|
||||
</div>
|
||||
<ThemeSwitch checked={props.darkMode} title={props.themeToggleTitle} onToggle={props.onToggleTheme} />
|
||||
<button type="button" className="btn btn-secondary small" onClick={props.onLock}>
|
||||
<Lock size={14} className="btn-icon" /> {t('txt_lock')}
|
||||
</button>
|
||||
@@ -49,6 +54,9 @@ export default function AppAuthenticatedShell(props: AppAuthenticatedShellProps)
|
||||
<FolderIcon size={16} className="btn-icon" />
|
||||
</button>
|
||||
)}
|
||||
<div className="mobile-theme-btn">
|
||||
<ThemeSwitch checked={props.darkMode} title={props.themeToggleTitle} onToggle={props.onToggleTheme} />
|
||||
</div>
|
||||
<button type="button" className="btn btn-secondary small mobile-lock-btn" aria-label={t('txt_lock')} title={t('txt_lock')} onClick={props.onLock}>
|
||||
<Lock size={14} className="btn-icon" />
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
interface ThemeSwitchProps {
|
||||
checked: boolean;
|
||||
title: string;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
export default function ThemeSwitch(props: ThemeSwitchProps) {
|
||||
return (
|
||||
<div className="theme-switch-wrap" title={props.title}>
|
||||
<label className="theme-switch" aria-label={props.title}>
|
||||
<span className="sun" aria-hidden="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g fill="#ffd43b">
|
||||
<circle r={5} cy={12} cx={12} />
|
||||
<path d="m21 13h-1a1 1 0 0 1 0-2h1a1 1 0 0 1 0 2zm-17 0h-1a1 1 0 0 1 0-2h1a1 1 0 0 1 0 2zm13.66-5.66a1 1 0 0 1 -.66-.29 1 1 0 0 1 0-1.41l.71-.71a1 1 0 1 1 1.41 1.41l-.71.71a1 1 0 0 1 -.75.29zm-12.02 12.02a1 1 0 0 1 -.71-.29 1 1 0 0 1 0-1.41l.71-.66a1 1 0 0 1 1.41 1.41l-.71.71a1 1 0 0 1 -.7.24zm6.36-14.36a1 1 0 0 1 -1-1v-1a1 1 0 0 1 2 0v1a1 1 0 0 1 -1 1zm0 17a1 1 0 0 1 -1-1v-1a1 1 0 0 1 2 0v1a1 1 0 0 1 -1 1zm-5.66-14.66a1 1 0 0 1 -.7-.29l-.71-.71a1 1 0 0 1 1.41-1.41l.71.71a1 1 0 0 1 0 1.41 1 1 0 0 1 -.71.29zm12.02 12.02a1 1 0 0 1 -.7-.29l-.66-.71a1 1 0 0 1 1.36-1.36l.71.71a1 1 0 0 1 0 1.41 1 1 0 0 1 -.71.24z" />
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
<span className="moon" aria-hidden="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512">
|
||||
<path d="m223.5 32c-123.5 0-223.5 100.3-223.5 224s100 224 223.5 224c60.6 0 115.5-24.2 155.8-63.4 5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6-96.9 0-175.5-78.8-175.5-176 0-65.8 36-123.1 89.3-153.3 6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z" />
|
||||
</svg>
|
||||
</span>
|
||||
<input type="checkbox" className="theme-switch-input" checked={props.checked} onInput={props.onToggle} />
|
||||
<span className="theme-switch-slider" />
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -256,6 +256,23 @@ export function BackupDestinationDetail(props: BackupDestinationDetailProps) {
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>{t('txt_backup_start_time')}</span>
|
||||
<input
|
||||
className="input"
|
||||
type="time"
|
||||
step={300}
|
||||
value={props.selectedDestination.schedule.startTime || '03:00'}
|
||||
disabled={props.loadingSettings || props.disableWhileBusy}
|
||||
onInput={(event) => props.onUpdateDestination((destination) => ({
|
||||
...destination,
|
||||
schedule: {
|
||||
...destination.schedule,
|
||||
startTime: (event.currentTarget as HTMLInputElement).value || '03:00',
|
||||
},
|
||||
}))}
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>{t('txt_backup_timezone')}</span>
|
||||
<select
|
||||
|
||||
+10
-4
@@ -140,6 +140,7 @@ const messages: Record<Locale, Record<string, string>> = {
|
||||
txt_backup_type: "Backup Type",
|
||||
txt_backup_destination_reserved: "Reserved Slot",
|
||||
txt_backup_time: "Backup Time",
|
||||
txt_backup_start_time: "Start Time",
|
||||
txt_backup_timezone: "Timezone",
|
||||
txt_backup_interval_hours: "Every",
|
||||
txt_backup_interval_hours_suffix: "hours",
|
||||
@@ -164,10 +165,10 @@ const messages: Record<Locale, Record<string, string>> = {
|
||||
txt_backup_include_attachments_help_button: "Attachment backup help",
|
||||
txt_backup_include_attachments_help: "Attachments are stored incrementally in the remote attachments folder, so later backups usually only upload new files. Deleting an attachment locally does not remove earlier remote copies. During restore, NodeWarden reads the required files from the attachments folder and skips any attachment that is no longer available.",
|
||||
txt_backup_enable_schedule: "Enable automatic daily backup",
|
||||
txt_backup_schedule_note: "The worker checks the schedule every 5 minutes and runs the backup as soon as the selected time window is reached.",
|
||||
txt_backup_schedule_note: "The worker checks the schedule every 5 minutes. It starts at the selected time in the selected timezone, then repeats by the chosen hour interval, and resets from that start time each day.",
|
||||
txt_backup_schedule_disabled: "Disabled",
|
||||
txt_backup_schedule_status: "Schedule",
|
||||
txt_backup_schedule_summary: "Daily at {time} ({timezone})",
|
||||
txt_backup_schedule_summary: "Start at {time}, every {interval} hours ({timezone})",
|
||||
txt_backup_schedule_empty: "No automatic backup plans are enabled yet.",
|
||||
txt_backup_last_success: "Last Success",
|
||||
txt_backup_last_target: "Last Target",
|
||||
@@ -572,6 +573,8 @@ const messages: Record<Locale, Record<string, string>> = {
|
||||
txt_submit: "Submit",
|
||||
txt_sync: "Sync",
|
||||
txt_sync_vault: "Sync Vault",
|
||||
txt_switch_to_dark_mode: "Switch to dark mode",
|
||||
txt_switch_to_light_mode: "Switch to light mode",
|
||||
txt_dash: "-",
|
||||
txt_text: "Text",
|
||||
txt_text_2fa_recovered: "2FA recovered",
|
||||
@@ -777,6 +780,7 @@ const zhCNOverrides: Record<string, string> = {
|
||||
txt_backup_type: '备份类型',
|
||||
txt_backup_destination_reserved: '预留位置',
|
||||
txt_backup_time: '备份时间',
|
||||
txt_backup_start_time: '开始时间',
|
||||
txt_backup_timezone: '时区',
|
||||
txt_backup_interval_hours: '每隔',
|
||||
txt_backup_interval_hours_suffix: '小时',
|
||||
@@ -801,10 +805,10 @@ const zhCNOverrides: Record<string, string> = {
|
||||
txt_backup_include_attachments_help_button: '附件备份说明',
|
||||
txt_backup_include_attachments_help: '附件会以增量方式保存在远端的 attachments 文件夹中,后续备份通常只上传新增文件。你在本地删除附件时,已经备份到远端的旧文件不会自动删除。恢复时会按需从 attachments 文件夹读取对应附件,找不到的附件会自动跳过。',
|
||||
txt_backup_enable_schedule: '启用每日自动备份',
|
||||
txt_backup_schedule_note: 'Worker 每 5 分钟检查一次计划,到达你设定的时间窗口后会尽快执行备份。',
|
||||
txt_backup_schedule_note: 'Worker 每 5 分钟检查一次计划。会先按你选择的时区和开始时间起跑,再按小时间隔继续执行;到了下一天,会重新从开始时间开始。',
|
||||
txt_backup_schedule_disabled: '未启用',
|
||||
txt_backup_schedule_status: '计划状态',
|
||||
txt_backup_schedule_summary: '每天 {time}({timezone})',
|
||||
txt_backup_schedule_summary: '从 {time} 开始,每隔 {interval} 小时({timezone})',
|
||||
txt_backup_schedule_empty: '还没有启用任何自动备份计划',
|
||||
txt_backup_last_success: '上次成功时间',
|
||||
txt_backup_last_target: '上次备份位置',
|
||||
@@ -1201,6 +1205,8 @@ const zhCNOverrides: Record<string, string> = {
|
||||
txt_totp_is_enabled_for_this_account: '此账户已启用 TOTP。',
|
||||
txt_total_items_count: '共 {count} 项',
|
||||
txt_totp_verify_failed: 'TOTP 验证失败',
|
||||
txt_switch_to_dark_mode: '切换到暗黑模式',
|
||||
txt_switch_to_light_mode: '切换到明亮模式',
|
||||
txt_trust_this_device_for_30_days: '信任此设备 30 天',
|
||||
txt_type_type: '类型 {type}',
|
||||
txt_unlock_details: '解锁详情',
|
||||
|
||||
+506
-2
@@ -1,16 +1,42 @@
|
||||
:root {
|
||||
--bg: #f3f5f8;
|
||||
--panel: #ffffff;
|
||||
--panel-soft: #f8fafc;
|
||||
--panel-muted: #f5f7fb;
|
||||
--line: #d7dde6;
|
||||
--line-soft: #e1e6ef;
|
||||
--text: #0f172a;
|
||||
--text-soft: #1e293b;
|
||||
--muted: #667085;
|
||||
--muted-strong: #475569;
|
||||
--primary: #2563eb;
|
||||
--primary-hover: #1d4ed8;
|
||||
--danger: #e11d48;
|
||||
--danger-hover: #be123c;
|
||||
--overlay: rgba(15, 23, 42, 0.36);
|
||||
--overlay-strong: rgba(15, 23, 42, 0.5);
|
||||
--radius: 12px;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] {
|
||||
--bg: #08111f;
|
||||
--panel: #0f1b2d;
|
||||
--panel-soft: #122033;
|
||||
--panel-muted: #0c1728;
|
||||
--line: #223249;
|
||||
--line-soft: #1b2a40;
|
||||
--text: #e5edf7;
|
||||
--text-soft: #d4dfef;
|
||||
--muted: #98a8bf;
|
||||
--muted-strong: #b3c0d3;
|
||||
--primary: #69a7ff;
|
||||
--primary-hover: #8abaff;
|
||||
--danger: #ff7a96;
|
||||
--danger-hover: #ff97ac;
|
||||
--overlay: rgba(2, 6, 23, 0.6);
|
||||
--overlay-strong: rgba(2, 6, 23, 0.78);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -27,6 +53,10 @@ body,
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
transition: background-color 0.25s ease, color 0.25s ease;
|
||||
}
|
||||
|
||||
.loading-screen {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
@@ -521,6 +551,90 @@ input[type='file'].input::file-selector-button:hover {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-theme-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.theme-switch-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.theme-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 56px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.theme-switch-input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.theme-switch-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(180deg, #dceaff, #c8dcff);
|
||||
border: 1px solid #9dbbec;
|
||||
transition: 0.25s ease;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.theme-switch-slider::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
border-radius: 999px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
z-index: 2;
|
||||
background: linear-gradient(180deg, #ffffff, #edf4ff);
|
||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.14);
|
||||
transition: 0.25s ease;
|
||||
}
|
||||
|
||||
.theme-switch .sun svg {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 32px;
|
||||
z-index: 1;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.theme-switch .moon svg {
|
||||
fill: #5b86d6;
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 7px;
|
||||
z-index: 1;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
.theme-switch-input:checked + .theme-switch-slider {
|
||||
background: linear-gradient(180deg, #173150, #122742);
|
||||
border-color: #35527a;
|
||||
}
|
||||
|
||||
.theme-switch-input:focus + .theme-switch-slider {
|
||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.22);
|
||||
}
|
||||
|
||||
.theme-switch-input:checked + .theme-switch-slider::before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
.topbar-actions .btn {
|
||||
height: 34px;
|
||||
border-radius: 10px;
|
||||
@@ -1748,7 +1862,7 @@ input[type='file'].input::file-selector-button:hover {
|
||||
}
|
||||
|
||||
.backup-detail-schedule-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
|
||||
}
|
||||
|
||||
.backup-interval-row {
|
||||
@@ -2668,7 +2782,8 @@ input[type='file'].input::file-selector-button:hover {
|
||||
}
|
||||
|
||||
.topbar-actions .user-chip,
|
||||
.topbar-actions > .btn:not(.mobile-sidebar-toggle):not(.mobile-lock-btn) {
|
||||
.topbar-actions > .btn:not(.mobile-sidebar-toggle):not(.mobile-lock-btn),
|
||||
.topbar-actions > .theme-switch-wrap {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -2689,6 +2804,16 @@ input[type='file'].input::file-selector-button:hover {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mobile-theme-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mobile-theme-btn .theme-switch {
|
||||
transform: scale(0.8);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -3249,3 +3374,382 @@ input[type='file'].input::file-selector-button:hover {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] body,
|
||||
:root[data-theme='dark'] #root,
|
||||
:root[data-theme='dark'] .app-page,
|
||||
:root[data-theme='dark'] .auth-page {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .app-shell,
|
||||
:root[data-theme='dark'] .auth-card,
|
||||
:root[data-theme='dark'] .dialog,
|
||||
:root[data-theme='dark'] .standalone-frame,
|
||||
:root[data-theme='dark'] .jwt-warning-box,
|
||||
:root[data-theme='dark'] .backup-operations-sidebar,
|
||||
:root[data-theme='dark'] .backup-destination-sidebar,
|
||||
:root[data-theme='dark'] .backup-detail-panel,
|
||||
:root[data-theme='dark'] .settings-subcard,
|
||||
:root[data-theme='dark'] .backup-recommendation,
|
||||
:root[data-theme='dark'] .backup-browser-card,
|
||||
:root[data-theme='dark'] .backup-settings-card,
|
||||
:root[data-theme='dark'] .backup-destination-card,
|
||||
:root[data-theme='dark'] .backup-operation-card,
|
||||
:root[data-theme='dark'] .list-panel,
|
||||
:root[data-theme='dark'] .card,
|
||||
:root[data-theme='dark'] .sidebar-block,
|
||||
:root[data-theme='dark'] .send-detail-card,
|
||||
:root[data-theme='dark'] .admin-card,
|
||||
:root[data-theme='dark'] .empty {
|
||||
background: var(--panel);
|
||||
border-color: var(--line);
|
||||
color: var(--text);
|
||||
box-shadow: 0 14px 36px rgba(2, 6, 23, 0.34);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .topbar,
|
||||
:root[data-theme='dark'] .mobile-tabbar,
|
||||
:root[data-theme='dark'] .sort-menu,
|
||||
:root[data-theme='dark'] .create-menu,
|
||||
:root[data-theme='dark'] .dialog-card,
|
||||
:root[data-theme='dark'] .mobile-sidebar-sheet,
|
||||
:root[data-theme='dark'] .mobile-detail-sheet,
|
||||
:root[data-theme='dark'] .mobile-editor-sheet {
|
||||
background: var(--panel-soft);
|
||||
border-color: var(--line);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .app-side,
|
||||
:root[data-theme='dark'] .sidebar,
|
||||
:root[data-theme='dark'] .mobile-sidebar-sheet .sidebar-block {
|
||||
background: var(--panel-muted);
|
||||
border-color: var(--line);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .brand,
|
||||
:root[data-theme='dark'] .brand-name,
|
||||
:root[data-theme='dark'] .mobile-page-title,
|
||||
:root[data-theme='dark'] .detail-title,
|
||||
:root[data-theme='dark'] .dialog-title,
|
||||
:root[data-theme='dark'] .standalone-title,
|
||||
:root[data-theme='dark'] .standalone-brand-title,
|
||||
:root[data-theme='dark'] .kv-main strong,
|
||||
:root[data-theme='dark'] .list-title,
|
||||
:root[data-theme='dark'] .sidebar-title,
|
||||
:root[data-theme='dark'] .backup-title,
|
||||
:root[data-theme='dark'] .backup-section-title,
|
||||
:root[data-theme='dark'] h1,
|
||||
:root[data-theme='dark'] h2,
|
||||
:root[data-theme='dark'] h3,
|
||||
:root[data-theme='dark'] h4 {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .muted,
|
||||
:root[data-theme='dark'] .detail-sub,
|
||||
:root[data-theme='dark'] .field-help,
|
||||
:root[data-theme='dark'] .list-sub,
|
||||
:root[data-theme='dark'] .kv-label,
|
||||
:root[data-theme='dark'] .standalone-muted,
|
||||
:root[data-theme='dark'] .standalone-footer,
|
||||
:root[data-theme='dark'] .backup-inline-note,
|
||||
:root[data-theme='dark'] .backup-meta,
|
||||
:root[data-theme='dark'] .backup-browser-empty,
|
||||
:root[data-theme='dark'] .dialog-copy,
|
||||
:root[data-theme='dark'] .or,
|
||||
:root[data-theme='dark'] .txt-muted,
|
||||
:root[data-theme='dark'] .mobile-tab,
|
||||
:root[data-theme='dark'] .side-link,
|
||||
:root[data-theme='dark'] .user-chip,
|
||||
:root[data-theme='dark'] .folder-meta,
|
||||
:root[data-theme='dark'] .list-count {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .user-chip {
|
||||
background: var(--panel-soft);
|
||||
border-color: var(--line);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .side-link:hover,
|
||||
:root[data-theme='dark'] .mobile-tab:hover {
|
||||
background: rgba(105, 167, 255, 0.1);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .side-link.active,
|
||||
:root[data-theme='dark'] .mobile-tab.active,
|
||||
:root[data-theme='dark'] .sort-menu-item.active,
|
||||
:root[data-theme='dark'] .filter-link.active,
|
||||
:root[data-theme='dark'] .folder-link.active,
|
||||
:root[data-theme='dark'] .list-item.active,
|
||||
:root[data-theme='dark'] .backup-mode-pill.active,
|
||||
:root[data-theme='dark'] .segmented-item.active {
|
||||
background: rgba(105, 167, 255, 0.16);
|
||||
border-color: rgba(105, 167, 255, 0.3);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .input,
|
||||
:root[data-theme='dark'] .textarea,
|
||||
:root[data-theme='dark'] select.input,
|
||||
:root[data-theme='dark'] .dialog input,
|
||||
:root[data-theme='dark'] .dialog textarea,
|
||||
:root[data-theme='dark'] .dialog select {
|
||||
background: var(--panel-soft);
|
||||
border-color: #35527a;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .input::placeholder,
|
||||
:root[data-theme='dark'] .textarea::placeholder,
|
||||
:root[data-theme='dark'] input::placeholder,
|
||||
:root[data-theme='dark'] textarea::placeholder {
|
||||
color: #7d8ea8;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .input-readonly {
|
||||
background: #101b2b;
|
||||
color: var(--muted-strong);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .input:disabled,
|
||||
:root[data-theme='dark'] .btn:disabled {
|
||||
background: #162338;
|
||||
border-color: #263851;
|
||||
color: #72849f;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .btn-secondary {
|
||||
background: var(--panel-soft);
|
||||
border-color: #4b78b8;
|
||||
color: #9ec5ff;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .btn-secondary:hover {
|
||||
background: #173150;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .btn-danger {
|
||||
background: #1d1722;
|
||||
border-color: #d85b78;
|
||||
color: #ff9bb0;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .btn-danger:hover {
|
||||
background: #2b1b29;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .btn-primary {
|
||||
color: #08111f;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .toolbar.actions,
|
||||
:root[data-theme='dark'] .list-head,
|
||||
:root[data-theme='dark'] .mobile-panel-head,
|
||||
:root[data-theme='dark'] .backup-recommendation-header,
|
||||
:root[data-theme='dark'] .backup-operations-sidebar,
|
||||
:root[data-theme='dark'] .backup-destination-sidebar,
|
||||
:root[data-theme='dark'] .backup-detail-panel,
|
||||
:root[data-theme='dark'] .dialog-actions,
|
||||
:root[data-theme='dark'] .detail-actions,
|
||||
:root[data-theme='dark'] .topbar,
|
||||
:root[data-theme='dark'] .app-side,
|
||||
:root[data-theme='dark'] .kv-row,
|
||||
:root[data-theme='dark'] .attachment-row,
|
||||
:root[data-theme='dark'] .backup-browser-row,
|
||||
:root[data-theme='dark'] .admin-row,
|
||||
:root[data-theme='dark'] .send-row {
|
||||
border-color: var(--line);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .sidebar,
|
||||
:root[data-theme='dark'] .content,
|
||||
:root[data-theme='dark'] .list-col,
|
||||
:root[data-theme='dark'] .detail-col {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .mobile-sidebar-mask,
|
||||
:root[data-theme='dark'] .dialog-mask,
|
||||
:root[data-theme='dark'] .modal-mask {
|
||||
background: var(--overlay-strong);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .toast {
|
||||
background: #122033;
|
||||
border-color: #253854;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .toast.success {
|
||||
background: #0f2a1f;
|
||||
border-color: #1f5b44;
|
||||
color: #9be2bd;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .toast.error {
|
||||
background: #2a1720;
|
||||
border-color: #6c2b41;
|
||||
color: #ffb1c0;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .toast.warning {
|
||||
background: #2d2413;
|
||||
border-color: #7b6230;
|
||||
color: #f7d48b;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .jwt-warning-head,
|
||||
:root[data-theme='dark'] .jwt-warning-label,
|
||||
:root[data-theme='dark'] .jwt-warning-copy,
|
||||
:root[data-theme='dark'] .jwt-warning-list {
|
||||
color: #f4d48a;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .theme-switch-input:focus + .theme-switch-slider {
|
||||
box-shadow: 0 0 0 2px rgba(105, 167, 255, 0.26);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .search-input,
|
||||
:root[data-theme='dark'] .list-head .search-input,
|
||||
:root[data-theme='dark'] .settings-subcard,
|
||||
:root[data-theme='dark'] .backup-operations-sidebar,
|
||||
:root[data-theme='dark'] .backup-destination-sidebar,
|
||||
:root[data-theme='dark'] .backup-detail-panel,
|
||||
:root[data-theme='dark'] .dialog-card,
|
||||
:root[data-theme='dark'] .backup-browser-path,
|
||||
:root[data-theme='dark'] .backup-browser-list,
|
||||
:root[data-theme='dark'] .backup-schedule-current,
|
||||
:root[data-theme='dark'] .backup-status-card,
|
||||
:root[data-theme='dark'] .totp-qr,
|
||||
:root[data-theme='dark'] .create-menu,
|
||||
:root[data-theme='dark'] .create-menu-item,
|
||||
:root[data-theme='dark'] .sort-menu,
|
||||
:root[data-theme='dark'] .sort-menu-item,
|
||||
:root[data-theme='dark'] .import-export-feature-item,
|
||||
:root[data-theme='dark'] .import-export-feature-icon,
|
||||
:root[data-theme='dark'] .backup-recommendation-card,
|
||||
:root[data-theme='dark'] .backup-recommendation-dav-item,
|
||||
:root[data-theme='dark'] .backup-destination-item,
|
||||
:root[data-theme='dark'] .totp-code-row,
|
||||
:root[data-theme='dark'] .list-item {
|
||||
background: var(--panel-soft);
|
||||
border-color: var(--line);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .list-item:hover,
|
||||
:root[data-theme='dark'] .sort-menu-item:hover,
|
||||
:root[data-theme='dark'] .create-menu-item:hover,
|
||||
:root[data-theme='dark'] .backup-destination-item:hover,
|
||||
:root[data-theme='dark'] .import-export-feature-item:hover {
|
||||
background: #13243a;
|
||||
border-color: #2e4665;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .list-item.active {
|
||||
background: linear-gradient(180deg, #173150, #1a385b);
|
||||
border-color: #38618f;
|
||||
box-shadow: inset 0 0 0 1px rgba(158, 197, 255, 0.08);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .backup-destination-item.active,
|
||||
:root[data-theme='dark'] .backup-interval-preset.active,
|
||||
:root[data-theme='dark'] .tree-btn.active {
|
||||
background: #1d4f99;
|
||||
border-color: #4d86d7;
|
||||
color: #f4f8ff;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .totp-code-name,
|
||||
:root[data-theme='dark'] .backup-destination-name,
|
||||
:root[data-theme='dark'] .backup-browser-entry,
|
||||
:root[data-theme='dark'] .backup-browser-path strong,
|
||||
:root[data-theme='dark'] .backup-status-grid strong,
|
||||
:root[data-theme='dark'] .backup-option-label,
|
||||
:root[data-theme='dark'] .sort-menu-item,
|
||||
:root[data-theme='dark'] .create-menu-item,
|
||||
:root[data-theme='dark'] .tree-btn,
|
||||
:root[data-theme='dark'] .folder-add-btn,
|
||||
:root[data-theme='dark'] .list-icon-fallback,
|
||||
:root[data-theme='dark'] .totp-code-main strong,
|
||||
:root[data-theme='dark'] .totp-timer-value {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .totp-code-username,
|
||||
:root[data-theme='dark'] .backup-destination-meta,
|
||||
:root[data-theme='dark'] .backup-browser-meta,
|
||||
:root[data-theme='dark'] .backup-file-meta,
|
||||
:root[data-theme='dark'] .backup-list,
|
||||
: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-recommendation-referral,
|
||||
:root[data-theme='dark'] .backup-retention-suffix,
|
||||
:root[data-theme='dark'] .backup-inline-suffix,
|
||||
:root[data-theme='dark'] .folder-delete-btn,
|
||||
:root[data-theme='dark'] .folder-add-btn:hover,
|
||||
:root[data-theme='dark'] .tree-label,
|
||||
:root[data-theme='dark'] .list-sub {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .import-export-feature-item p,
|
||||
:root[data-theme='dark'] .import-export-hero-sub,
|
||||
:root[data-theme='dark'] .import-export-panel p,
|
||||
:root[data-theme='dark'] .dialog-message,
|
||||
:root[data-theme='dark'] .local-error,
|
||||
:root[data-theme='dark'] .status-ok {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .backup-destination-type {
|
||||
background: #1d3048;
|
||||
color: #c9d8eb;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .backup-help-trigger {
|
||||
border-color: #38618f;
|
||||
background: #173150;
|
||||
color: #9ec5ff;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .backup-help-trigger:hover,
|
||||
:root[data-theme='dark'] .backup-help-trigger:focus-visible {
|
||||
border-color: #5f92d7;
|
||||
background: #20426a;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .backup-help-bubble {
|
||||
background: var(--panel);
|
||||
border-color: var(--line);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .backup-help-bubble::before {
|
||||
background: var(--panel);
|
||||
border-left-color: var(--line);
|
||||
border-top-color: var(--line);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .dialog-icon {
|
||||
color: #f7d48b;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .local-error {
|
||||
color: #ff9bb0;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .status-ok {
|
||||
color: #9be2bd;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .totp-qr svg,
|
||||
:root[data-theme='dark'] .totp-qr img {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user