feat: update mobile layout query to 1180px and enhance icon loading experience

This commit is contained in:
shuaiplus
2026-04-25 03:19:06 +08:00
parent e4bc1b9bbe
commit a1f7250e90
7 changed files with 78 additions and 32 deletions
+1 -1
View File
@@ -217,7 +217,7 @@ export default function App() {
useEffect(() => { useEffect(() => {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return; if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
const media = window.matchMedia('(max-width: 900px)'); const media = window.matchMedia('(max-width: 1180px)');
const sync = () => setMobileLayout(media.matches); const sync = () => setMobileLayout(media.matches);
sync(); sync();
if (typeof media.addEventListener === 'function') { if (typeof media.addEventListener === 'function') {
+1 -1
View File
@@ -20,7 +20,7 @@ interface SendsPageProps {
type SendTypeFilter = 'all' | 'text' | 'file'; type SendTypeFilter = 'all' | 'text' | 'file';
const AUTO_COPY_KEY = 'nodewarden.send.auto_copy_link.v1'; const AUTO_COPY_KEY = 'nodewarden.send.auto_copy_link.v1';
const MOBILE_LAYOUT_QUERY = '(max-width: 900px)'; const MOBILE_LAYOUT_QUERY = '(max-width: 1180px)';
function daysFromNow(iso: string | null | undefined, fallback: number): string { function daysFromNow(iso: string | null | undefined, fallback: number): string {
if (!iso) return String(fallback); if (!iso) return String(fallback);
+19 -11
View File
@@ -73,23 +73,31 @@ function TotpListIcon({ cipher }: { cipher: Cipher }) {
const uri = firstCipherUri(cipher); const uri = firstCipherUri(cipher);
const host = hostFromUri(uri); const host = hostFromUri(uri);
const [errored, setErrored] = useState(() => (host ? failedIconHosts.has(host) : false)); const [errored, setErrored] = useState(() => (host ? failedIconHosts.has(host) : false));
const [loaded, setLoaded] = useState(false);
useEffect(() => { useEffect(() => {
setErrored(host ? failedIconHosts.has(host) : false); setErrored(host ? failedIconHosts.has(host) : false);
setLoaded(false);
}, [host]); }, [host]);
if (host && !errored) { if (host && !errored) {
return ( return (
<img <span className="list-icon-stack">
className="list-icon" <span className={`list-icon-fallback ${loaded ? 'hidden' : ''}`}>
src={websiteIconUrl(host)} <Globe size={18} />
alt="" </span>
loading="lazy" <img
referrerPolicy="no-referrer" className={`list-icon ${loaded ? 'loaded' : ''}`}
onError={() => { src={websiteIconUrl(host)}
failedIconHosts.add(host); alt=""
setErrored(true); loading="lazy"
}} referrerPolicy="no-referrer"
/> onLoad={() => setLoaded(true)}
onError={() => {
failedIconHosts.add(host);
setErrored(true);
}}
/>
</span>
); );
} }
return ( return (
@@ -36,7 +36,7 @@ export const CREATE_TYPE_OPTIONS: TypeOption[] = [
]; ];
export const VAULT_SORT_STORAGE_KEY = 'nodewarden.vault.sort.v1'; export const VAULT_SORT_STORAGE_KEY = 'nodewarden.vault.sort.v1';
export const MOBILE_LAYOUT_QUERY = '(max-width: 900px)'; export const MOBILE_LAYOUT_QUERY = '(max-width: 1180px)';
export const VAULT_LIST_ROW_HEIGHT = 74; export const VAULT_LIST_ROW_HEIGHT = 74;
export const VAULT_LIST_OVERSCAN = 10; export const VAULT_LIST_OVERSCAN = 10;
export const VAULT_SORT_OPTIONS: Array<{ value: VaultSortMode; label: string }> = [ export const VAULT_SORT_OPTIONS: Array<{ value: VaultSortMode; label: string }> = [
@@ -433,23 +433,31 @@ export function VaultListIcon({ cipher }: { cipher: Cipher }) {
const uri = firstCipherUri(cipher); const uri = firstCipherUri(cipher);
const host = hostFromUri(uri); const host = hostFromUri(uri);
const [errored, setErrored] = useState(() => (host ? failedIconHosts.has(host) : false)); const [errored, setErrored] = useState(() => (host ? failedIconHosts.has(host) : false));
const [loaded, setLoaded] = useState(false);
useEffect(() => { useEffect(() => {
setErrored(host ? failedIconHosts.has(host) : false); setErrored(host ? failedIconHosts.has(host) : false);
setLoaded(false);
}, [host]); }, [host]);
if (host && !errored) { if (host && !errored) {
return ( return (
<img <span className="list-icon-stack">
className="list-icon" <span className={`list-icon-fallback ${loaded ? 'hidden' : ''}`}>
src={websiteIconUrl(host)} <TypeIcon type={Number(cipher.type || 1)} />
alt="" </span>
loading="lazy" <img
referrerPolicy="no-referrer" className={`list-icon ${loaded ? 'loaded' : ''}`}
onError={() => { src={websiteIconUrl(host)}
failedIconHosts.add(host); alt=""
setErrored(true); loading="lazy"
}} referrerPolicy="no-referrer"
/> onLoad={() => setLoaded(true)}
onError={() => {
failedIconHosts.add(host);
setErrored(true);
}}
/>
</span>
); );
} }
return ( return (
+7 -3
View File
@@ -59,7 +59,7 @@
} }
} }
@media (max-width: 900px) { @media (max-width: 1180px) {
.auth-page { .auth-page {
@apply items-start p-3.5; @apply items-start p-3.5;
} }
@@ -114,6 +114,10 @@
@apply h-[34px] w-[34px]; @apply h-[34px] w-[34px];
} }
.brand-wordmark {
@apply hidden;
}
.mobile-page-title { .mobile-page-title {
@apply inline; @apply inline;
} }
@@ -410,7 +414,7 @@
.detail-actions { .detail-actions {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
gap: 10px; gap: 5px;
} }
.detail-actions .actions { .detail-actions .actions {
@@ -624,7 +628,7 @@
} }
} }
@media (max-width: 900px) { @media (max-width: 1180px) {
.backup-grid { .backup-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
+1 -1
View File
@@ -29,7 +29,7 @@
--dur-fast: 180ms; --dur-fast: 180ms;
--dur-medium: 240ms; --dur-medium: 240ms;
--dur-panel: 280ms; --dur-panel: 280ms;
--actions-gap: clamp(0px, calc((100vw - 520px) * 1), 10px); --actions-gap: clamp(5px, calc((100vw - 520px) * 1), 10px);
} }
:root[data-theme='dark'] { :root[data-theme='dark'] {
+29 -3
View File
@@ -1,6 +1,6 @@
.vault-grid { .vault-grid {
@apply grid h-full min-h-0 gap-3 p-0.5; @apply grid h-full min-h-0 gap-3 p-0.5;
grid-template-columns: 240px minmax(420px, 46%) minmax(575px, 1fr); grid-template-columns: 270px minmax(400px, 36%) minmax(400px, 1fr);
} }
.sidebar, .sidebar,
@@ -355,12 +355,38 @@
.list-icon { .list-icon {
@apply h-6 w-6 rounded-md; @apply h-6 w-6 rounded-md;
opacity: 1;
transition: opacity var(--dur-fast) var(--ease-smooth);
}
.list-icon-stack {
@apply grid h-6 w-6 place-items-center;
}
.list-icon-stack > .list-icon,
.list-icon-stack > .list-icon-fallback {
grid-area: 1 / 1;
}
.list-icon-stack > .list-icon {
opacity: 0;
}
.list-icon-stack > .list-icon.loaded {
opacity: 1;
}
.list-icon-fallback.hidden {
opacity: 0;
} }
.list-icon-fallback { .list-icon-fallback {
@apply grid place-items-center; @apply grid place-items-center;
color: #64748b; color: #64748b;
transition: color var(--dur-fast) var(--ease-smooth), transform 240ms var(--ease-out-soft); transition:
color var(--dur-fast) var(--ease-smooth),
opacity var(--dur-fast) var(--ease-smooth),
transform 240ms var(--ease-out-soft);
} }
.list-icon-fallback svg { .list-icon-fallback svg {
@@ -615,7 +641,7 @@
.totp-codes-list { .totp-codes-list {
@apply grid w-full items-start gap-2.5; @apply grid w-full items-start gap-2.5;
grid-template-columns: repeat(var(--totp-columns, 1), minmax(320px, 1fr)); grid-template-columns: repeat(var(--totp-columns, 1), minmax(300px, 1fr));
} }
.totp-code-row { .totp-code-row {