feat: add offline mode notice and related styles to enhance user experience during offline access

This commit is contained in:
shuaiplus
2026-07-10 22:24:06 +08:00
parent 0e46cd371f
commit aae614a079
8 changed files with 105 additions and 2 deletions
+35 -2
View File
@@ -1,8 +1,9 @@
import { useState } from 'preact/hooks';
import { ArrowLeft, Eye, EyeOff, KeyRound, LogIn, LogOut, Unlock, UserPlus } from 'lucide-preact';
import { useEffect, useState } from 'preact/hooks';
import { AlertTriangle, ArrowLeft, Eye, EyeOff, KeyRound, LogIn, LogOut, Unlock, UserPlus } from 'lucide-preact';
import NetworkStatusBadge from '@/components/NetworkStatusBadge';
import StandalonePageFrame from '@/components/StandalonePageFrame';
import { t } from '@/lib/i18n';
import { getCurrentNetworkStatus, subscribeNetworkStatus, type NetworkStatus } from '@/lib/network-status';
interface LoginValues {
email: string;
@@ -81,6 +82,36 @@ function PasswordField(props: {
);
}
function OfflineModeNotice() {
const [status, setStatus] = useState<NetworkStatus>(getCurrentNetworkStatus);
useEffect(() => subscribeNetworkStatus(setStatus), []);
if (status !== 'offline') return null;
return (
<div className="offline-mode-notice" role="alert" aria-live="assertive">
<div>
<strong>{t('txt_offline_mode_notice_title')}</strong>
<div className="offline-shortcut-list">
<div className="offline-shortcut-row">
<span className="offline-shortcut-label">{t('txt_offline_mode_notice_windows')}</span>
<span className="offline-shortcut-value">
<span className="offline-shortcut-chord"><kbd>Ctrl</kbd><span>+</span><kbd>F5</kbd></span>
</span>
</div>
<div className="offline-shortcut-row">
<span className="offline-shortcut-label">{t('txt_offline_mode_notice_macos')}</span>
<span className="offline-shortcut-value">
<span className="offline-shortcut-chord"><kbd>Command</kbd><span>+</span><kbd>Shift</kbd><span>+</span><kbd>R</kbd></span>
</span>
</div>
</div>
</div>
</div>
);
}
export default function AuthViews(props: AuthViewsProps) {
const loginBusy = props.pendingAction === 'login';
const passkeyBusy = props.pendingAction === 'passkey';
@@ -99,6 +130,7 @@ export default function AuthViews(props: AuthViewsProps) {
props.onSubmitUnlock();
}}
>
<OfflineModeNotice />
<p className="muted standalone-muted">{props.emailForLock}</p>
<input type="text" value={props.emailForLock} autoComplete="username" readOnly hidden tabIndex={-1} aria-hidden="true" />
<PasswordField
@@ -245,6 +277,7 @@ export default function AuthViews(props: AuthViewsProps) {
props.onSubmitLogin();
}}
>
<OfflineModeNotice />
{passkeyPasswordPending ? (
<>
<p className="muted standalone-muted">{props.pendingPasskeyPasswordEmail}</p>
+3
View File
@@ -1012,6 +1012,9 @@ const en: Record<string, string> = {
"txt_online": "Online",
"txt_offline": "Offline",
"txt_offline_vault_readonly": "Offline mode is read-only. Connect to NodeWarden before changing your vault.",
"txt_offline_mode_notice_title": "You are in offline mode. If this looks wrong, force refresh.",
"txt_offline_mode_notice_windows": "Windows:",
"txt_offline_mode_notice_macos": "macOS:",
"txt_submit": "Submit",
"txt_sync": "Sync",
"txt_sync_vault": "Sync Vault",
+3
View File
@@ -1012,6 +1012,9 @@ const es: Record<string, string> = {
"txt_online": "En línea",
"txt_offline": "Sin conexión",
"txt_offline_vault_readonly": "El modo sin conexión es de solo lectura. Conecta con NodeWarden antes de cambiar la bóveda.",
"txt_offline_mode_notice_title": "Estás en modo sin conexión. Si parece un error, fuerza la recarga.",
"txt_offline_mode_notice_windows": "Windows:",
"txt_offline_mode_notice_macos": "macOS:",
"txt_submit": "Enviar",
"txt_sync": "Sincronizar",
"txt_sync_vault": "Sincronizar bóveda",
+3
View File
@@ -1012,6 +1012,9 @@ const fi: Record<string, string> = {
"txt_online": "Verkossa",
"txt_offline": "Offline",
"txt_offline_vault_readonly": "Offline-tila on vain luku -muodossa. Yhdistä NodeWardeniin ennen kuin muutat holviasi.",
"txt_offline_mode_notice_title": "Olet offline-tilassa. Jos tämä vaikuttaa virheeltä, tee pakotettu päivitys.",
"txt_offline_mode_notice_windows": "Windows:",
"txt_offline_mode_notice_macos": "macOS:",
"txt_submit": "Lähetä",
"txt_sync": "Synkronoi",
"txt_sync_vault": "Synkronoi holvi",
+3
View File
@@ -1012,6 +1012,9 @@ const ru: Record<string, string> = {
"txt_online": "Онлайн",
"txt_offline": "Офлайн",
"txt_offline_vault_readonly": "Автономный режим доступен только для чтения. Подключитесь к NodeWarden, чтобы изменить хранилище.",
"txt_offline_mode_notice_title": "Вы в автономном режиме. Если это ошибка, выполните принудительное обновление.",
"txt_offline_mode_notice_windows": "Windows:",
"txt_offline_mode_notice_macos": "macOS:",
"txt_submit": "Отправить",
"txt_sync": "Синхронизировать",
"txt_sync_vault": "Синхронизировать хранилище",
+3
View File
@@ -1012,6 +1012,9 @@ const zhCN: Record<string, string> = {
"txt_online": "在线",
"txt_offline": "离线",
"txt_offline_vault_readonly": "当前为离线模式,只能查看密码库。连接到 NodeWarden 后才能修改。",
"txt_offline_mode_notice_title": "你正处于离线模式,若误判请强制刷新",
"txt_offline_mode_notice_windows": "Windows",
"txt_offline_mode_notice_macos": "macOS",
"txt_submit": "提交",
"txt_sync": "同步",
"txt_sync_vault": "同步",
+3
View File
@@ -1012,6 +1012,9 @@ const zhTW: Record<string, string> = {
"txt_online": "在線",
"txt_offline": "離線",
"txt_offline_vault_readonly": "目前為離線模式,只能查看密碼庫。連線到 NodeWarden 後才能修改。",
"txt_offline_mode_notice_title": "你正處於離線模式,若誤判請強制重新整理",
"txt_offline_mode_notice_windows": "Windows",
"txt_offline_mode_notice_macos": "macOS",
"txt_submit": "提交",
"txt_sync": "同步",
"txt_sync_vault": "同步",
+52
View File
@@ -492,6 +492,58 @@
transform: translateY(-50%);
}
.offline-mode-notice {
@apply mb-4 flex items-start gap-3 rounded-xl border px-3.5 py-3 text-left text-sm leading-relaxed;
background: color-mix(in srgb, var(--warning) 10%, var(--panel));
border-color: color-mix(in srgb, var(--warning) 42%, var(--line));
color: var(--text);
}
.offline-mode-notice svg {
@apply mt-0.5 shrink-0;
color: var(--warning);
}
.offline-mode-notice strong {
@apply block text-center text-[13px] font-extrabold;
color: color-mix(in srgb, var(--warning) 82%, var(--text));
}
.offline-mode-notice > div {
@apply min-w-0 flex-1;
}
.offline-shortcut-list {
@apply mt-2 grid gap-1.5;
grid-template-columns: max-content minmax(0, 1fr);
}
.offline-shortcut-row {
display: contents;
}
.offline-shortcut-label {
@apply text-xs font-extrabold;
color: color-mix(in srgb, var(--warning) 76%, var(--text));
}
.offline-shortcut-value {
@apply flex min-w-0 flex-wrap items-center gap-1.5;
color: var(--muted-strong);
}
.offline-shortcut-chord {
@apply inline-flex items-center gap-1 whitespace-nowrap;
}
.offline-shortcut-chord kbd {
@apply inline-flex min-h-6 items-center rounded-md border px-1.5 font-mono text-[12px] font-bold leading-none;
background: color-mix(in srgb, var(--panel) 88%, var(--warning));
border-color: color-mix(in srgb, var(--warning) 35%, var(--line));
box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--warning) 26%, transparent);
color: var(--text);
}
.standalone-muted {
@apply text-left;
}