mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
feat: enhance mobile layout and accessibility across components
- Added mobile layout support in AdminPage, SecurityDevicesPage, SendsPage, and VaultPage. - Implemented responsive design adjustments including mobile sidebar and panel transitions. - Updated table structures to include data labels for better accessibility. - Introduced new translations for mobile-specific UI elements. - Enhanced styles for mobile views, including button adjustments and sidebar behaviors.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'preact/hooks';
|
||||
import { CheckCheck, 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 type { Send, SendDraft } from '@/lib/types';
|
||||
import { t } from '@/lib/i18n';
|
||||
|
||||
@@ -16,6 +16,7 @@ interface SendsPageProps {
|
||||
|
||||
type SendTypeFilter = 'all' | 'text' | 'file';
|
||||
const AUTO_COPY_KEY = 'nodewarden.send.auto_copy_link.v1';
|
||||
const MOBILE_LAYOUT_QUERY = '(max-width: 900px)';
|
||||
|
||||
function daysFromNow(iso: string | null | undefined, fallback: number): string {
|
||||
if (!iso) return String(fallback);
|
||||
@@ -67,6 +68,9 @@ export default function SendsPage(props: SendsPageProps) {
|
||||
const [draft, setDraft] = useState<SendDraft | null>(null);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [selectedMap, setSelectedMap] = useState<Record<string, boolean>>({});
|
||||
const [isMobileLayout, setIsMobileLayout] = useState(false);
|
||||
const [mobilePanel, setMobilePanel] = useState<'list' | 'detail' | 'edit'>('list');
|
||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
||||
const [autoCopyLink, setAutoCopyLink] = useState<boolean>(() => {
|
||||
try {
|
||||
return localStorage.getItem(AUTO_COPY_KEY) === '1';
|
||||
@@ -75,6 +79,27 @@ export default function SendsPage(props: SendsPageProps) {
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
|
||||
const media = window.matchMedia(MOBILE_LAYOUT_QUERY);
|
||||
const sync = () => setIsMobileLayout(media.matches);
|
||||
sync();
|
||||
if (typeof media.addEventListener === 'function') {
|
||||
media.addEventListener('change', sync);
|
||||
return () => media.removeEventListener('change', sync);
|
||||
}
|
||||
media.addListener(sync);
|
||||
return () => media.removeListener(sync);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onToggleSidebar = () => {
|
||||
setMobileSidebarOpen((open) => !open);
|
||||
};
|
||||
window.addEventListener('nodewarden:toggle-sidebar', onToggleSidebar);
|
||||
return () => window.removeEventListener('nodewarden:toggle-sidebar', onToggleSidebar);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(AUTO_COPY_KEY, autoCopyLink ? '1' : '0');
|
||||
@@ -83,6 +108,19 @@ export default function SendsPage(props: SendsPageProps) {
|
||||
}
|
||||
}, [autoCopyLink]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMobileLayout) {
|
||||
setMobilePanel('list');
|
||||
setMobileSidebarOpen(false);
|
||||
return;
|
||||
}
|
||||
if (isEditing) {
|
||||
setMobilePanel('edit');
|
||||
} else if (!selectedId) {
|
||||
setMobilePanel('list');
|
||||
}
|
||||
}, [isMobileLayout, isEditing, selectedId]);
|
||||
|
||||
const filteredSends = useMemo(() => {
|
||||
const q = search.trim().toLowerCase();
|
||||
return props.sends.filter((send) => {
|
||||
@@ -141,6 +179,7 @@ export default function SendsPage(props: SendsPageProps) {
|
||||
setIsCreating(false);
|
||||
setDraft(null);
|
||||
setShowPassword(false);
|
||||
if (isMobileLayout) setMobilePanel('detail');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
@@ -153,6 +192,7 @@ export default function SendsPage(props: SendsPageProps) {
|
||||
if (selectedId === send.id) setSelectedId(null);
|
||||
setIsEditing(false);
|
||||
setDraft(null);
|
||||
if (isMobileLayout) setMobilePanel('list');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
@@ -176,8 +216,17 @@ export default function SendsPage(props: SendsPageProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="vault-grid">
|
||||
<aside className="sidebar">
|
||||
<div className={`vault-grid ${isMobileLayout ? `mobile-panel-${mobilePanel}` : ''}`}>
|
||||
{isMobileLayout && mobileSidebarOpen && <div className="mobile-sidebar-mask" onClick={() => setMobileSidebarOpen(false)} />}
|
||||
<aside className={`sidebar ${isMobileLayout ? 'mobile-sidebar-sheet' : ''} ${isMobileLayout && mobileSidebarOpen ? 'open' : ''}`}>
|
||||
{isMobileLayout && (
|
||||
<div className="mobile-sidebar-head">
|
||||
<div className="mobile-sidebar-title">{t('txt_all_sends')}</div>
|
||||
<button type="button" className="mobile-sidebar-close" onClick={() => setMobileSidebarOpen(false)} aria-label={t('txt_close')}>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="sidebar-block">
|
||||
<div className="sidebar-title">{t('txt_all_sends')}</div>
|
||||
<button type="button" className={`tree-btn ${typeFilter === 'all' ? 'active' : ''}`} onClick={() => setTypeFilter('all')}>
|
||||
@@ -206,7 +255,7 @@ export default function SendsPage(props: SendsPageProps) {
|
||||
value={search}
|
||||
onInput={(e) => setSearch((e.currentTarget as HTMLInputElement).value)}
|
||||
/>
|
||||
<button type="button" className="btn btn-secondary small" disabled={busy || props.loading} onClick={() => void props.onRefresh()}>
|
||||
<button type="button" className="btn btn-secondary small list-icon-btn" disabled={busy || props.loading} onClick={() => void props.onRefresh()}>
|
||||
<RefreshCw size={14} className="btn-icon" /> {t('txt_refresh')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -235,16 +284,20 @@ export default function SendsPage(props: SendsPageProps) {
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary small"
|
||||
className="btn btn-primary small mobile-fab-trigger"
|
||||
disabled={busy}
|
||||
aria-label={t('txt_add')}
|
||||
title={t('txt_add')}
|
||||
onClick={() => {
|
||||
setIsCreating(true);
|
||||
setIsEditing(true);
|
||||
setDraft(buildDefaultDraft());
|
||||
setShowPassword(false);
|
||||
if (isMobileLayout) setMobilePanel('edit');
|
||||
setMobileSidebarOpen(false);
|
||||
}}
|
||||
>
|
||||
<Plus size={14} className="btn-icon" /> {t('txt_add')}
|
||||
<Plus size={14} className="btn-icon" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="list-panel">
|
||||
@@ -269,6 +322,8 @@ export default function SendsPage(props: SendsPageProps) {
|
||||
setIsEditing(false);
|
||||
setIsCreating(false);
|
||||
setDraft(null);
|
||||
if (isMobileLayout) setMobilePanel('detail');
|
||||
setMobileSidebarOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="list-icon-wrap">
|
||||
@@ -289,7 +344,29 @@ export default function SendsPage(props: SendsPageProps) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="detail-col">
|
||||
<section className={`detail-col ${isMobileLayout ? 'mobile-detail-sheet' : ''} ${isMobileLayout && mobilePanel !== 'list' ? 'open' : ''}`}>
|
||||
{isMobileLayout && mobilePanel !== 'list' && (
|
||||
<div className="mobile-panel-head">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary small mobile-panel-back"
|
||||
onClick={() => {
|
||||
if (isEditing) {
|
||||
setIsEditing(false);
|
||||
setIsCreating(false);
|
||||
setDraft(null);
|
||||
setShowPassword(false);
|
||||
setMobilePanel(selectedSend ? 'detail' : 'list');
|
||||
} else {
|
||||
setMobilePanel('list');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ChevronLeft size={14} className="btn-icon" />
|
||||
{t('txt_back')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{isEditing && draft && (
|
||||
<div className="card">
|
||||
<h3 className="detail-title">{isCreating ? t('txt_new_send') : t('txt_edit_send')}</h3>
|
||||
@@ -369,7 +446,18 @@ export default function SendsPage(props: SendsPageProps) {
|
||||
<button type="button" className="btn btn-primary small" disabled={busy} onClick={() => void saveDraft()}>
|
||||
<Save size={14} className="btn-icon" /> {t('txt_save')}
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary small" disabled={busy} onClick={() => { setIsEditing(false); setIsCreating(false); setDraft(null); setShowPassword(false); }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary small"
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
setIsEditing(false);
|
||||
setIsCreating(false);
|
||||
setDraft(null);
|
||||
setShowPassword(false);
|
||||
if (isMobileLayout) setMobilePanel(selectedSend ? 'detail' : 'list');
|
||||
}}
|
||||
>
|
||||
<X size={14} className="btn-icon" /> {t('txt_cancel')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user