mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
Polish vault icons and mobile layout
This commit is contained in:
@@ -36,6 +36,7 @@ interface VaultPageProps {
|
|||||||
ciphers: Cipher[];
|
ciphers: Cipher[];
|
||||||
folders: Folder[];
|
folders: Folder[];
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
error: string;
|
||||||
emailForReprompt: string;
|
emailForReprompt: string;
|
||||||
onRefresh: () => Promise<void>;
|
onRefresh: () => Promise<void>;
|
||||||
onCreate: (draft: VaultDraft, attachments?: File[]) => Promise<void>;
|
onCreate: (draft: VaultDraft, attachments?: File[]) => Promise<void>;
|
||||||
@@ -1021,6 +1022,7 @@ const folderName = useCallback((id: string | null | undefined): string => {
|
|||||||
<VaultListPanel
|
<VaultListPanel
|
||||||
busy={busy}
|
busy={busy}
|
||||||
loading={props.loading}
|
loading={props.loading}
|
||||||
|
error={props.error}
|
||||||
searchInput={searchInput}
|
searchInput={searchInput}
|
||||||
sortMode={sortMode}
|
sortMode={sortMode}
|
||||||
sortMenuOpen={sortMenuOpen}
|
sortMenuOpen={sortMenuOpen}
|
||||||
@@ -1140,7 +1142,20 @@ const folderName = useCallback((id: string | null | undefined): string => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isEditing && !selectedCipher && (props.loading ? <LoadingState card lines={5} /> : <div className="empty card">{t('txt_select_an_item')}</div>)}
|
{!isEditing && !selectedCipher && (
|
||||||
|
props.loading
|
||||||
|
? <LoadingState card lines={5} />
|
||||||
|
: props.error
|
||||||
|
? (
|
||||||
|
<div className="empty card vault-error-state">
|
||||||
|
<strong>{props.error}</strong>
|
||||||
|
<button type="button" className="btn btn-secondary small" disabled={busy} onClick={handleSyncVault}>
|
||||||
|
{t('txt_retry_sync')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: <div className="empty card">{t('txt_select_an_item')}</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { createPortal } from 'preact/compat';
|
import { createPortal } from 'preact/compat';
|
||||||
import { useMemo, useState } from 'preact/hooks';
|
import { useMemo, useState } from 'preact/hooks';
|
||||||
import { Archive, Clipboard, Download, Eye, EyeOff, ExternalLink, Paperclip, Pencil, RotateCcw, Trash2, X } from 'lucide-preact';
|
import { Archive, Clipboard, Download, Eye, EyeOff, ExternalLink, Folder, Paperclip, Pencil, RotateCcw, Trash2, X } from 'lucide-preact';
|
||||||
import { useDialogLifecycle } from '@/components/ConfirmDialog';
|
import { useDialogLifecycle } from '@/components/ConfirmDialog';
|
||||||
import type { Cipher } from '@/lib/types';
|
import type { Cipher } from '@/lib/types';
|
||||||
import { t } from '@/lib/i18n';
|
import { t } from '@/lib/i18n';
|
||||||
import {
|
import {
|
||||||
TOTP_PERIOD_SECONDS,
|
TOTP_PERIOD_SECONDS,
|
||||||
TOTP_RING_CIRCUMFERENCE,
|
TOTP_RING_CIRCUMFERENCE,
|
||||||
|
VaultListIcon,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
formatAttachmentSize,
|
formatAttachmentSize,
|
||||||
formatHistoryTime,
|
formatHistoryTime,
|
||||||
@@ -115,8 +116,18 @@ export default function VaultDetailView(props: VaultDetailViewProps) {
|
|||||||
{(Number(props.selectedCipher.reprompt || 0) !== 1 || props.repromptApprovedCipherId === props.selectedCipher.id) && (
|
{(Number(props.selectedCipher.reprompt || 0) !== 1 || props.repromptApprovedCipherId === props.selectedCipher.id) && (
|
||||||
<>
|
<>
|
||||||
<div className="card">
|
<div className="card">
|
||||||
|
<div className="detail-title-row">
|
||||||
|
<span className="detail-title-icon" aria-hidden="true">
|
||||||
|
<VaultListIcon cipher={props.selectedCipher} />
|
||||||
|
</span>
|
||||||
|
<div className="detail-title-main">
|
||||||
<h3 className="detail-title">{props.selectedCipher.decName || t('txt_no_name')}</h3>
|
<h3 className="detail-title">{props.selectedCipher.decName || t('txt_no_name')}</h3>
|
||||||
<div className="detail-sub">{props.folderName(props.selectedCipher.folderId)}</div>
|
<div className="detail-folder-line">
|
||||||
|
<Folder size={13} aria-hidden="true" />
|
||||||
|
<span>{props.folderName(props.selectedCipher.folderId)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{isArchived && <div className="list-badge archive-badge">{t('txt_archived')}</div>}
|
{isArchived && <div className="list-badge archive-badge">{t('txt_archived')}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ interface VirtualRange {
|
|||||||
interface VaultListPanelProps {
|
interface VaultListPanelProps {
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
error: string;
|
||||||
searchInput: string;
|
searchInput: string;
|
||||||
sortMode: VaultSortMode;
|
sortMode: VaultSortMode;
|
||||||
sortMenuOpen: boolean;
|
sortMenuOpen: boolean;
|
||||||
@@ -238,6 +239,14 @@ export default function VaultListPanel(props: VaultListPanelProps) {
|
|||||||
|
|
||||||
<div className="list-panel" ref={props.listPanelRef} onScroll={(event) => props.onScroll((event.currentTarget as HTMLDivElement).scrollTop)}>
|
<div className="list-panel" ref={props.listPanelRef} onScroll={(event) => props.onScroll((event.currentTarget as HTMLDivElement).scrollTop)}>
|
||||||
{props.loading && !props.filteredCiphers.length && <LoadingState lines={7} compact />}
|
{props.loading && !props.filteredCiphers.length && <LoadingState lines={7} compact />}
|
||||||
|
{!props.loading && !!props.error && !props.filteredCiphers.length && (
|
||||||
|
<div className="empty vault-error-state">
|
||||||
|
<strong>{props.error}</strong>
|
||||||
|
<button type="button" className="btn btn-secondary small" disabled={props.busy} onClick={props.onSyncVault}>
|
||||||
|
{t('txt_retry_sync')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{!!props.filteredCiphers.length && (
|
{!!props.filteredCiphers.length && (
|
||||||
<div style={{ paddingTop: `${props.virtualRange.padTop}px`, paddingBottom: `${props.virtualRange.padBottom}px` }}>
|
<div style={{ paddingTop: `${props.virtualRange.padTop}px`, paddingBottom: `${props.virtualRange.padBottom}px` }}>
|
||||||
{props.visibleCiphers.map((cipher) => (
|
{props.visibleCiphers.map((cipher) => (
|
||||||
@@ -253,7 +262,7 @@ export default function VaultListPanel(props: VaultListPanelProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!props.loading && !props.filteredCiphers.length && <div className="empty">{t('txt_no_items')}</div>}
|
{!props.loading && !props.error && !props.filteredCiphers.length && <div className="empty">{t('txt_no_items')}</div>}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ import type { ComponentChildren } from 'preact';
|
|||||||
import { Globe } from 'lucide-preact';
|
import { Globe } from 'lucide-preact';
|
||||||
import type { Cipher } from '@/lib/types';
|
import type { Cipher } from '@/lib/types';
|
||||||
import {
|
import {
|
||||||
|
getWebsiteIconImageUrl,
|
||||||
getWebsiteIconStatus,
|
getWebsiteIconStatus,
|
||||||
markWebsiteIconErrored,
|
|
||||||
markWebsiteIconLoaded,
|
|
||||||
preloadWebsiteIcon,
|
preloadWebsiteIcon,
|
||||||
subscribeWebsiteIconStatus,
|
subscribeWebsiteIconStatus,
|
||||||
} from '@/lib/website-icon-cache';
|
} from '@/lib/website-icon-cache';
|
||||||
@@ -24,17 +23,23 @@ export default function WebsiteIcon(props: WebsiteIconProps) {
|
|||||||
const nodeRef = useRef<HTMLSpanElement | null>(null);
|
const nodeRef = useRef<HTMLSpanElement | null>(null);
|
||||||
const [shouldLoad, setShouldLoad] = useState(() => (host ? getWebsiteIconStatus(host) === 'loaded' : true));
|
const [shouldLoad, setShouldLoad] = useState(() => (host ? getWebsiteIconStatus(host) === 'loaded' : true));
|
||||||
const [status, setStatus] = useState(() => (host ? getWebsiteIconStatus(host) : 'idle'));
|
const [status, setStatus] = useState(() => (host ? getWebsiteIconStatus(host) : 'idle'));
|
||||||
|
const [imageUrl, setImageUrl] = useState(() => (host ? getWebsiteIconImageUrl(host) : ''));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!host) {
|
if (!host) {
|
||||||
setShouldLoad(true);
|
setShouldLoad(true);
|
||||||
setStatus('idle');
|
setStatus('idle');
|
||||||
|
setImageUrl('');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nextStatus = getWebsiteIconStatus(host);
|
const nextStatus = getWebsiteIconStatus(host);
|
||||||
setShouldLoad(nextStatus === 'loaded');
|
setShouldLoad(nextStatus === 'loaded');
|
||||||
setStatus(nextStatus);
|
setStatus(nextStatus);
|
||||||
return subscribeWebsiteIconStatus(host, setStatus);
|
setImageUrl(getWebsiteIconImageUrl(host));
|
||||||
|
return subscribeWebsiteIconStatus(host, (next) => {
|
||||||
|
setStatus(next);
|
||||||
|
setImageUrl(getWebsiteIconImageUrl(host));
|
||||||
|
});
|
||||||
}, [host]);
|
}, [host]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -70,7 +75,9 @@ export default function WebsiteIcon(props: WebsiteIconProps) {
|
|||||||
if (!host || !src || !shouldLoad || status === 'loaded' || status === 'error') return;
|
if (!host || !src || !shouldLoad || status === 'loaded' || status === 'error') return;
|
||||||
let disposed = false;
|
let disposed = false;
|
||||||
void preloadWebsiteIcon(host, src).then((nextStatus) => {
|
void preloadWebsiteIcon(host, src).then((nextStatus) => {
|
||||||
if (!disposed) setStatus(nextStatus);
|
if (disposed) return;
|
||||||
|
setStatus(nextStatus);
|
||||||
|
setImageUrl(getWebsiteIconImageUrl(host));
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
@@ -84,16 +91,13 @@ export default function WebsiteIcon(props: WebsiteIconProps) {
|
|||||||
return (
|
return (
|
||||||
<span className="list-icon-stack" ref={nodeRef}>
|
<span className="list-icon-stack" ref={nodeRef}>
|
||||||
{status !== 'loaded' && <span className="list-icon-fallback">{props.fallback ?? <Globe size={18} />}</span>}
|
{status !== 'loaded' && <span className="list-icon-fallback">{props.fallback ?? <Globe size={18} />}</span>}
|
||||||
{status === 'loaded' && (
|
{status === 'loaded' && imageUrl && (
|
||||||
<img
|
<img
|
||||||
className="list-icon loaded"
|
className="list-icon loaded"
|
||||||
src={src}
|
src={imageUrl}
|
||||||
alt=""
|
alt=""
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
referrerPolicy="no-referrer"
|
|
||||||
onLoad={() => markWebsiteIconLoaded(host)}
|
|
||||||
onError={() => markWebsiteIconErrored(host)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
type WebsiteIconStatus = 'idle' | 'loading' | 'loaded' | 'error';
|
type WebsiteIconStatus = 'idle' | 'loading' | 'loaded' | 'error';
|
||||||
|
|
||||||
|
const ICON_LOAD_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
interface WebsiteIconRecord {
|
interface WebsiteIconRecord {
|
||||||
status: WebsiteIconStatus;
|
status: WebsiteIconStatus;
|
||||||
promise: Promise<WebsiteIconStatus> | null;
|
promise: Promise<WebsiteIconStatus> | null;
|
||||||
|
imageUrl: string | null;
|
||||||
listeners: Set<(status: WebsiteIconStatus) => void>;
|
listeners: Set<(status: WebsiteIconStatus) => void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,6 +17,7 @@ function ensureRecord(host: string): WebsiteIconRecord {
|
|||||||
record = {
|
record = {
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
promise: null,
|
promise: null,
|
||||||
|
imageUrl: null,
|
||||||
listeners: new Set(),
|
listeners: new Set(),
|
||||||
};
|
};
|
||||||
iconRecords.set(host, record);
|
iconRecords.set(host, record);
|
||||||
@@ -34,6 +38,11 @@ export function getWebsiteIconStatus(host: string): WebsiteIconStatus {
|
|||||||
return ensureRecord(host).status;
|
return ensureRecord(host).status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getWebsiteIconImageUrl(host: string): string {
|
||||||
|
if (!host) return '';
|
||||||
|
return ensureRecord(host).imageUrl || '';
|
||||||
|
}
|
||||||
|
|
||||||
export function subscribeWebsiteIconStatus(host: string, listener: (status: WebsiteIconStatus) => void): () => void {
|
export function subscribeWebsiteIconStatus(host: string, listener: (status: WebsiteIconStatus) => void): () => void {
|
||||||
if (!host) return () => undefined;
|
if (!host) return () => undefined;
|
||||||
const record = ensureRecord(host);
|
const record = ensureRecord(host);
|
||||||
@@ -43,10 +52,13 @@ export function subscribeWebsiteIconStatus(host: string, listener: (status: Webs
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function markWebsiteIconLoaded(host: string): void {
|
export function markWebsiteIconLoaded(host: string, imageUrl?: string): void {
|
||||||
if (!host) return;
|
if (!host) return;
|
||||||
const record = ensureRecord(host);
|
const record = ensureRecord(host);
|
||||||
record.promise = null;
|
record.promise = null;
|
||||||
|
if (imageUrl) {
|
||||||
|
record.imageUrl = imageUrl;
|
||||||
|
}
|
||||||
notifyRecord(host, 'loaded');
|
notifyRecord(host, 'loaded');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,9 +66,19 @@ export function markWebsiteIconErrored(host: string): void {
|
|||||||
if (!host) return;
|
if (!host) return;
|
||||||
const record = ensureRecord(host);
|
const record = ensureRecord(host);
|
||||||
record.promise = null;
|
record.promise = null;
|
||||||
|
record.imageUrl = null;
|
||||||
notifyRecord(host, 'error');
|
notifyRecord(host, 'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function blobToDataUrl(blob: Blob): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => resolve(typeof reader.result === 'string' ? reader.result : '');
|
||||||
|
reader.onerror = () => reject(reader.error || new Error('Failed to read icon'));
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function preloadWebsiteIcon(host: string, src: string): Promise<WebsiteIconStatus> {
|
export function preloadWebsiteIcon(host: string, src: string): Promise<WebsiteIconStatus> {
|
||||||
if (!host) return Promise.resolve('error');
|
if (!host) return Promise.resolve('error');
|
||||||
|
|
||||||
@@ -68,21 +90,31 @@ export function preloadWebsiteIcon(host: string, src: string): Promise<WebsiteIc
|
|||||||
return record.promise;
|
return record.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
record.status = 'loading';
|
notifyRecord(host, 'loading');
|
||||||
record.promise = new Promise<WebsiteIconStatus>((resolve) => {
|
record.promise = (async () => {
|
||||||
const img = new Image();
|
const controller = new AbortController();
|
||||||
img.decoding = 'async';
|
const timeout = window.setTimeout(() => controller.abort(), ICON_LOAD_TIMEOUT_MS);
|
||||||
img.referrerPolicy = 'no-referrer';
|
try {
|
||||||
img.onload = () => {
|
const resp = await fetch(src, {
|
||||||
markWebsiteIconLoaded(host);
|
cache: 'force-cache',
|
||||||
resolve('loaded');
|
signal: controller.signal,
|
||||||
};
|
|
||||||
img.onerror = () => {
|
|
||||||
markWebsiteIconErrored(host);
|
|
||||||
resolve('error');
|
|
||||||
};
|
|
||||||
img.src = src;
|
|
||||||
});
|
});
|
||||||
|
if (!resp.ok) throw new Error('Icon unavailable');
|
||||||
|
const contentType = String(resp.headers.get('Content-Type') || '').toLowerCase();
|
||||||
|
if (!contentType.startsWith('image/')) throw new Error('Icon response is not an image');
|
||||||
|
const blob = await resp.blob();
|
||||||
|
if (!blob.size) throw new Error('Icon response is empty');
|
||||||
|
const imageUrl = await blobToDataUrl(blob);
|
||||||
|
if (!imageUrl) throw new Error('Icon response is empty');
|
||||||
|
markWebsiteIconLoaded(host, imageUrl);
|
||||||
|
return 'loaded';
|
||||||
|
} catch {
|
||||||
|
markWebsiteIconErrored(host);
|
||||||
|
return 'error';
|
||||||
|
} finally {
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
return record.promise;
|
return record.promise;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
|
|
||||||
:root[data-theme='dark'] .muted,
|
:root[data-theme='dark'] .muted,
|
||||||
:root[data-theme='dark'] .detail-sub,
|
:root[data-theme='dark'] .detail-sub,
|
||||||
|
:root[data-theme='dark'] .detail-folder-line,
|
||||||
:root[data-theme='dark'] .field-help,
|
:root[data-theme='dark'] .field-help,
|
||||||
:root[data-theme='dark'] .list-sub,
|
:root[data-theme='dark'] .list-sub,
|
||||||
:root[data-theme='dark'] .kv-label,
|
:root[data-theme='dark'] .kv-label,
|
||||||
@@ -296,3 +297,8 @@
|
|||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
-webkit-backdrop-filter: blur(8px);
|
-webkit-backdrop-filter: blur(8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root[data-theme='dark'] .not-found-code {
|
||||||
|
background: color-mix(in srgb, var(--primary) 18%, var(--panel));
|
||||||
|
color: var(--primary-strong);
|
||||||
|
}
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ input[type='file'].input::file-selector-button:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.or {
|
.or {
|
||||||
@apply my-2.5 text-center text-slate-700;
|
@apply text-center text-slate-700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-help {
|
.field-help {
|
||||||
|
|||||||
@@ -61,15 +61,15 @@
|
|||||||
|
|
||||||
@media (max-width: 1180px) {
|
@media (max-width: 1180px) {
|
||||||
.auth-page {
|
.auth-page {
|
||||||
@apply items-start p-3.5;
|
@apply items-center p-3.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.standalone-shell {
|
.standalone-shell {
|
||||||
@apply w-full max-w-[460px] gap-2.5 pt-3;
|
@apply w-full max-w-[460px] gap-2.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.standalone-brand-outside {
|
.standalone-brand-outside {
|
||||||
@apply justify-start;
|
@apply justify-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.standalone-brand-logo {
|
.standalone-brand-logo {
|
||||||
@@ -663,6 +663,49 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
|
.settings-module h3 {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-module .field,
|
||||||
|
.auth-card .field {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-module .field > span,
|
||||||
|
.auth-card .field > span {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-module .field-grid,
|
||||||
|
.auth-card .field-grid,
|
||||||
|
.session-timeout-fields {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-module .btn,
|
||||||
|
.auth-card .btn:not(.full) {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-mask.totp-scan-mask {
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
background: #0f172a;
|
||||||
|
backdrop-filter: none;
|
||||||
|
-webkit-backdrop-filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-card.totp-scan-dialog {
|
||||||
|
width: 100vw;
|
||||||
|
max-width: none;
|
||||||
|
height: 100dvh;
|
||||||
|
max-height: 100dvh;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
.backup-interval-row {
|
.backup-interval-row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -664,18 +664,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dialog-mask.totp-scan-mask {
|
.dialog-mask.totp-scan-mask {
|
||||||
@apply block p-0;
|
@apply grid place-items-center p-5;
|
||||||
background: #0f172a;
|
background: rgba(15, 23, 42, 0.78);
|
||||||
backdrop-filter: none;
|
|
||||||
-webkit-backdrop-filter: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-card.totp-scan-dialog {
|
.dialog-card.totp-scan-dialog {
|
||||||
@apply flex h-dvh w-screen max-w-none flex-col overflow-hidden rounded-none border-0 p-0 text-left;
|
@apply flex w-full max-w-[560px] flex-col overflow-hidden rounded-3xl border-0 p-0 text-left;
|
||||||
max-height: 100dvh;
|
height: min(720px, calc(100dvh - 48px));
|
||||||
|
max-height: calc(100dvh - 48px);
|
||||||
background: #0f172a;
|
background: #0f172a;
|
||||||
color: #f8fafc;
|
color: #f8fafc;
|
||||||
box-shadow: none;
|
box-shadow: 0 28px 80px rgba(2, 6, 23, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
.totp-scan-head {
|
.totp-scan-head {
|
||||||
@@ -698,8 +697,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.totp-scan-frame {
|
.totp-scan-frame {
|
||||||
@apply relative min-h-0 flex-1 overflow-hidden rounded-none;
|
@apply relative min-h-0 flex-1 overflow-hidden;
|
||||||
aspect-ratio: auto;
|
|
||||||
background: #0f172a;
|
background: #0f172a;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -738,6 +736,39 @@
|
|||||||
@apply flex min-h-full flex-col;
|
@apply flex min-h-full flex-col;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-title-row {
|
||||||
|
@apply flex min-w-0 items-center gap-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-title-icon {
|
||||||
|
@apply flex h-11 w-11 shrink-0 items-center justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-title-icon .list-icon-wrap,
|
||||||
|
.detail-title-icon .list-icon-stack,
|
||||||
|
.detail-title-icon .list-icon,
|
||||||
|
.detail-title-icon .list-icon-fallback {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-title-main {
|
||||||
|
@apply min-w-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-folder-line {
|
||||||
|
@apply mt-1 flex min-w-0 items-center gap-1.5 text-xs font-semibold;
|
||||||
|
color: #667085;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-folder-line span {
|
||||||
|
@apply min-w-0 overflow-hidden text-ellipsis whitespace-nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-folder-line svg {
|
||||||
|
@apply shrink-0;
|
||||||
|
}
|
||||||
|
|
||||||
.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(300px, 1fr));
|
grid-template-columns: repeat(var(--totp-columns, 1), minmax(300px, 1fr));
|
||||||
@@ -953,3 +984,12 @@
|
|||||||
@apply grid min-h-[120px] place-items-center;
|
@apply grid min-h-[120px] place-items-center;
|
||||||
color: #667085;
|
color: #667085;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vault-error-state {
|
||||||
|
@apply gap-3 text-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vault-error-state strong {
|
||||||
|
@apply text-sm;
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user