Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f2704fd41 | |||
| 7e0406f751 |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 619 B |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,10 @@
|
|||||||
|
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- Full-bleed background for any/maskable -->
|
||||||
|
<rect width="512" height="512" fill="#116FF9"/>
|
||||||
|
<!-- Logo scaled to ~50% centered in safe zone (inner 66% = Android adaptive icon guideline) -->
|
||||||
|
<g transform="translate(256,256) scale(0.5) translate(-380,-380)">
|
||||||
|
<path d="M386.5 183C497.785 183 588 271.2 588 380C588 419.877 575.879 456.986 555.046 488H17.6816C16.5766 481.834 16 475.484 16 469C16 413.617 58.0774 368.061 112.008 362.558C108.771 353.989 107 344.701 107 335C107 291.922 141.922 257 185 257C198.365 257 210.945 260.362 221.94 266.286C258.437 215.895 318.539 183 386.5 183Z" fill="#F6821F"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M92.6568 91.0069C88.7796 262.923 101.55 381.119 143.869 469.459C186.188 557.799 258.092 616.353 372.665 668.892C485.877 616.354 556.929 557.802 598.746 469.461C640.564 381.12 653.181 262.923 649.35 91.0069H92.6568ZM539.796 432.933C570.479 365.533 581.347 278.379 582.419 153.939L582.422 153.432H377.661V593.786L378.405 593.364C458.602 547.962 509.101 500.36 539.796 432.933Z" fill="white"/>
|
||||||
|
<path d="M604.465 305C680.976 305 743 367.233 743 444C743 459.378 740.509 474.172 735.913 488H379V423.553C391.721 397.751 418.287 380 449 380C459.483 380 469.482 382.068 478.613 385.818C500.559 338.11 548.658 305 604.465 305Z" fill="#FD9C33"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1535,7 +1535,7 @@ export default function App() {
|
|||||||
const isKnownAppRoute = APP_ROUTES.has(routeLocation) || isPublicSendRoute || isImportHashRoute;
|
const isKnownAppRoute = APP_ROUTES.has(routeLocation) || isPublicSendRoute || isImportHashRoute;
|
||||||
const isUnknownRoute = isMalformedSendRoute || (phase === 'app' ? !isKnownAppRoute : !isKnownAuthRoute && !APP_ROUTES.has(routeLocation));
|
const isUnknownRoute = isMalformedSendRoute || (phase === 'app' ? !isKnownAppRoute : !isKnownAuthRoute && !APP_ROUTES.has(routeLocation));
|
||||||
const isImportRoute = routeLocation === IMPORT_ROUTE || IMPORT_ROUTE_ALIASES.has(routeLocation);
|
const isImportRoute = routeLocation === IMPORT_ROUTE || IMPORT_ROUTE_ALIASES.has(routeLocation);
|
||||||
const showSidebarToggle = mobileLayout && (location === '/vault' || location === '/sends');
|
const showSidebarToggle = mobileLayout && location === '/sends';
|
||||||
const sidebarToggleTitle = location === '/vault' ? t('txt_folders') : t('txt_type');
|
const sidebarToggleTitle = location === '/vault' ? t('txt_folders') : t('txt_type');
|
||||||
const demoDomainRules = useMemo<DomainRules>(() => ({
|
const demoDomainRules = useMemo<DomainRules>(() => ({
|
||||||
equivalentDomains: [
|
equivalentDomains: [
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ export default function ToastHost({ toasts, onClose }: ToastHostProps) {
|
|||||||
{toasts.map((toast) => (
|
{toasts.map((toast) => (
|
||||||
<li key={toast.id} className={`toast-item ${toast.type}`}>
|
<li key={toast.id} className={`toast-item ${toast.type}`}>
|
||||||
<div className="toast-text">{toast.text}</div>
|
<div className="toast-text">{toast.text}</div>
|
||||||
<button type="button" className="toast-close" onClick={() => onClose(toast.id)}>
|
<button type="button" className="toast-close" onClick={() => onClose(toast.id)} aria-label="关闭通知">
|
||||||
x
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
|
||||||
|
<path d="M3 3l8 8M11 3l-8 8" />
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div className="toast-progress" />
|
<div className="toast-progress" />
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1115,6 +1115,7 @@ const folderName = useCallback((id: string | null | undefined): string => {
|
|||||||
busy={busy}
|
busy={busy}
|
||||||
loading={props.loading}
|
loading={props.loading}
|
||||||
error={props.error}
|
error={props.error}
|
||||||
|
folders={props.folders}
|
||||||
searchInput={searchInput}
|
searchInput={searchInput}
|
||||||
sortMode={sortMode}
|
sortMode={sortMode}
|
||||||
sortMenuOpen={sortMenuOpen}
|
sortMenuOpen={sortMenuOpen}
|
||||||
@@ -1141,6 +1142,7 @@ const folderName = useCallback((id: string | null | undefined): string => {
|
|||||||
onToggleSortMenu={handleToggleSortMenu}
|
onToggleSortMenu={handleToggleSortMenu}
|
||||||
onSelectSortMode={handleSelectSortMode}
|
onSelectSortMode={handleSelectSortMode}
|
||||||
onDuplicateModeChange={setDuplicateMode}
|
onDuplicateModeChange={setDuplicateMode}
|
||||||
|
onChangeFilter={setSidebarFilter}
|
||||||
onSyncVault={handleSyncVault}
|
onSyncVault={handleSyncVault}
|
||||||
onOpenBulkDelete={handleOpenBulkDelete}
|
onOpenBulkDelete={handleOpenBulkDelete}
|
||||||
onSelectDuplicates={handleSelectDuplicates}
|
onSelectDuplicates={handleSelectDuplicates}
|
||||||
|
|||||||
@@ -1,9 +1,32 @@
|
|||||||
import type { RefObject } from 'preact';
|
import type { ComponentChildren, RefObject } from 'preact';
|
||||||
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
import { createPortal } from 'preact/compat';
|
import { createPortal } from 'preact/compat';
|
||||||
import { Archive, ArrowUpDown, Check, CheckCheck, FolderInput, Plus, RefreshCw, RotateCcw, Trash2, X } from 'lucide-preact';
|
import {
|
||||||
|
Archive,
|
||||||
|
ArrowUpDown,
|
||||||
|
Check,
|
||||||
|
CheckCheck,
|
||||||
|
ChevronDown,
|
||||||
|
Copy,
|
||||||
|
CreditCard,
|
||||||
|
Folder as FolderIcon,
|
||||||
|
FolderInput,
|
||||||
|
FolderX,
|
||||||
|
Globe,
|
||||||
|
KeyRound,
|
||||||
|
LayoutGrid,
|
||||||
|
Plus,
|
||||||
|
RefreshCw,
|
||||||
|
RotateCcw,
|
||||||
|
ShieldUser,
|
||||||
|
Star,
|
||||||
|
StickyNote,
|
||||||
|
Trash2,
|
||||||
|
X,
|
||||||
|
} from 'lucide-preact';
|
||||||
import LoadingState from '@/components/LoadingState';
|
import LoadingState from '@/components/LoadingState';
|
||||||
import type { Cipher } from '@/lib/types';
|
import type { Cipher, Folder } from '@/lib/types';
|
||||||
import { t } from '@/lib/i18n';
|
import { t } from '@/lib/i18n';
|
||||||
import {
|
import {
|
||||||
CreateTypeIcon,
|
CreateTypeIcon,
|
||||||
@@ -27,6 +50,7 @@ interface VaultListPanelProps {
|
|||||||
busy: boolean;
|
busy: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
|
folders: Folder[];
|
||||||
searchInput: string;
|
searchInput: string;
|
||||||
sortMode: VaultSortMode;
|
sortMode: VaultSortMode;
|
||||||
sortMenuOpen: boolean;
|
sortMenuOpen: boolean;
|
||||||
@@ -53,6 +77,7 @@ interface VaultListPanelProps {
|
|||||||
onToggleSortMenu: () => void;
|
onToggleSortMenu: () => void;
|
||||||
onSelectSortMode: (value: VaultSortMode) => void;
|
onSelectSortMode: (value: VaultSortMode) => void;
|
||||||
onDuplicateModeChange: (value: DuplicateDetectionMode) => void;
|
onDuplicateModeChange: (value: DuplicateDetectionMode) => void;
|
||||||
|
onChangeFilter: (filter: SidebarFilter) => void;
|
||||||
onSyncVault: () => void;
|
onSyncVault: () => void;
|
||||||
onOpenBulkDelete: () => void;
|
onOpenBulkDelete: () => void;
|
||||||
onSelectDuplicates: () => void;
|
onSelectDuplicates: () => void;
|
||||||
@@ -80,6 +105,16 @@ interface CipherListItemProps {
|
|||||||
onSelectCipher: (cipherId: string) => void;
|
onSelectCipher: (cipherId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MobileFilterMenuKey = 'duplicate' | 'menu' | 'type' | 'folder';
|
||||||
|
|
||||||
|
interface MobileFilterOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
icon: ComponentChildren;
|
||||||
|
active: boolean;
|
||||||
|
onSelect: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
const CipherListItem = memo(function CipherListItem(props: CipherListItemProps) {
|
const CipherListItem = memo(function CipherListItem(props: CipherListItemProps) {
|
||||||
const duplicateGroupHue = props.duplicateGroupIndex === null ? null : (props.duplicateGroupIndex * 137.508) % 360;
|
const duplicateGroupHue = props.duplicateGroupIndex === null ? null : (props.duplicateGroupIndex * 137.508) % 360;
|
||||||
return (
|
return (
|
||||||
@@ -115,14 +150,116 @@ const CipherListItem = memo(function CipherListItem(props: CipherListItemProps)
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default function VaultListPanel(props: VaultListPanelProps) {
|
export default function VaultListPanel(props: VaultListPanelProps) {
|
||||||
|
const [mobileFilterOpen, setMobileFilterOpen] = useState<MobileFilterMenuKey | null>(null);
|
||||||
|
const mobileFilterRef = useRef<HTMLDivElement | null>(null);
|
||||||
const createTypeOptions = getCreateTypeOptions();
|
const createTypeOptions = getCreateTypeOptions();
|
||||||
const duplicateDetectionOptions = getDuplicateDetectionOptions();
|
const duplicateDetectionOptions = getDuplicateDetectionOptions();
|
||||||
const vaultSortOptions = getVaultSortOptions();
|
const vaultSortOptions = getVaultSortOptions();
|
||||||
const createMenu = (
|
const duplicateModeOptions: MobileFilterOption[] = duplicateDetectionOptions.map((option) => ({
|
||||||
<div className="create-menu-wrap mobile-fab-wrap" ref={props.createMenuRef}>
|
value: option.value,
|
||||||
|
label: option.label,
|
||||||
|
icon: option.value === 'login-site' ? <Globe size={14} /> : option.value === 'exact' ? <Copy size={14} /> : <KeyRound size={14} />,
|
||||||
|
active: props.duplicateMode === option.value,
|
||||||
|
onSelect: () => props.onDuplicateModeChange(option.value),
|
||||||
|
}));
|
||||||
|
const menuFilterOptions: MobileFilterOption[] = [
|
||||||
|
{ value: 'all', label: t('txt_all_items'), icon: <LayoutGrid size={14} />, active: props.sidebarFilter.kind === 'all', onSelect: () => props.onChangeFilter({ kind: 'all' }) },
|
||||||
|
{ value: 'favorite', label: t('txt_favorites'), icon: <Star size={14} />, active: props.sidebarFilter.kind === 'favorite', onSelect: () => props.onChangeFilter({ kind: 'favorite' }) },
|
||||||
|
{ value: 'archive', label: t('txt_archive'), icon: <Archive size={14} />, active: props.sidebarFilter.kind === 'archive', onSelect: () => props.onChangeFilter({ kind: 'archive' }) },
|
||||||
|
{ value: 'trash', label: t('txt_trash'), icon: <Trash2 size={14} />, active: props.sidebarFilter.kind === 'trash', onSelect: () => props.onChangeFilter({ kind: 'trash' }) },
|
||||||
|
{ value: 'duplicates', label: t('txt_duplicates'), icon: <Copy size={14} />, active: props.sidebarFilter.kind === 'duplicates', onSelect: () => props.onChangeFilter({ kind: 'duplicates' }) },
|
||||||
|
];
|
||||||
|
const typeMobileFilterOptions: MobileFilterOption[] = [
|
||||||
|
{ value: 'login', label: t('txt_login'), icon: <Globe size={14} />, active: props.sidebarFilter.kind === 'type' && props.sidebarFilter.value === 'login', onSelect: () => props.onChangeFilter({ kind: 'type', value: 'login' }) },
|
||||||
|
{ value: 'card', label: t('txt_card'), icon: <CreditCard size={14} />, active: props.sidebarFilter.kind === 'type' && props.sidebarFilter.value === 'card', onSelect: () => props.onChangeFilter({ kind: 'type', value: 'card' }) },
|
||||||
|
{ value: 'identity', label: t('txt_identity'), icon: <ShieldUser size={14} />, active: props.sidebarFilter.kind === 'type' && props.sidebarFilter.value === 'identity', onSelect: () => props.onChangeFilter({ kind: 'type', value: 'identity' }) },
|
||||||
|
{ value: 'note', label: t('txt_note'), icon: <StickyNote size={14} />, active: props.sidebarFilter.kind === 'type' && props.sidebarFilter.value === 'note', onSelect: () => props.onChangeFilter({ kind: 'type', value: 'note' }) },
|
||||||
|
{ value: 'ssh', label: t('txt_ssh_key'), icon: <KeyRound size={14} />, active: props.sidebarFilter.kind === 'type' && props.sidebarFilter.value === 'ssh', onSelect: () => props.onChangeFilter({ kind: 'type', value: 'ssh' }) },
|
||||||
|
];
|
||||||
|
const folderMobileFilterOptions: MobileFilterOption[] = [
|
||||||
|
{ value: '__none__', label: t('txt_no_folder'), icon: <FolderX size={14} />, active: props.sidebarFilter.kind === 'folder' && props.sidebarFilter.folderId === null, onSelect: () => props.onChangeFilter({ kind: 'folder', folderId: null }) },
|
||||||
|
...props.folders.map((folder) => ({
|
||||||
|
value: folder.id,
|
||||||
|
label: folder.decName || folder.name || folder.id,
|
||||||
|
icon: <FolderIcon size={14} />,
|
||||||
|
active: props.sidebarFilter.kind === 'folder' && props.sidebarFilter.folderId === folder.id,
|
||||||
|
onSelect: () => props.onChangeFilter({ kind: 'folder', folderId: folder.id }),
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
const menuFilterSelected = menuFilterOptions.find((option) => option.active);
|
||||||
|
const typeFilterSelected = typeMobileFilterOptions.find((option) => option.active);
|
||||||
|
const folderFilterSelected = folderMobileFilterOptions.find((option) => option.active);
|
||||||
|
const duplicateModeSelected = duplicateModeOptions.find((option) => option.active);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onPointerDown = (event: Event) => {
|
||||||
|
if (!mobileFilterOpen) return;
|
||||||
|
const target = event.target as Node | null;
|
||||||
|
if (mobileFilterRef.current && target && !mobileFilterRef.current.contains(target)) {
|
||||||
|
setMobileFilterOpen(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') setMobileFilterOpen(null);
|
||||||
|
};
|
||||||
|
document.addEventListener('pointerdown', onPointerDown);
|
||||||
|
document.addEventListener('keydown', onKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('pointerdown', onPointerDown);
|
||||||
|
document.removeEventListener('keydown', onKeyDown);
|
||||||
|
};
|
||||||
|
}, [mobileFilterOpen]);
|
||||||
|
|
||||||
|
const renderMobileFilterMenu = (
|
||||||
|
key: MobileFilterMenuKey,
|
||||||
|
label: string,
|
||||||
|
selected: MobileFilterOption | undefined,
|
||||||
|
fallbackIcon: ComponentChildren,
|
||||||
|
options: MobileFilterOption[]
|
||||||
|
) => (
|
||||||
|
<div className="mobile-vault-filter-control">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-primary small mobile-fab-trigger"
|
className={`mobile-vault-filter-trigger ${mobileFilterOpen === key ? 'active' : ''}`}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-expanded={mobileFilterOpen === key}
|
||||||
|
onClick={() => setMobileFilterOpen((open) => open === key ? null : key)}
|
||||||
|
>
|
||||||
|
<span className="mobile-vault-filter-trigger-icon">{selected?.icon || fallbackIcon}</span>
|
||||||
|
<span className="mobile-vault-filter-trigger-label">{selected?.label || label}</span>
|
||||||
|
<ChevronDown size={13} className="mobile-vault-filter-chevron" />
|
||||||
|
</button>
|
||||||
|
{mobileFilterOpen === key && (
|
||||||
|
<div className="sort-menu mobile-vault-filter-menu" role="menu">
|
||||||
|
{options.map((option) => (
|
||||||
|
<button
|
||||||
|
key={option.value}
|
||||||
|
type="button"
|
||||||
|
className={`sort-menu-item mobile-vault-filter-item ${option.active ? 'active' : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
option.onSelect();
|
||||||
|
setMobileFilterOpen(null);
|
||||||
|
}}
|
||||||
|
role="menuitemradio"
|
||||||
|
aria-checked={option.active}
|
||||||
|
>
|
||||||
|
<span className="mobile-vault-filter-item-main">
|
||||||
|
<span className="mobile-vault-filter-item-icon">{option.icon}</span>
|
||||||
|
<span>{option.label}</span>
|
||||||
|
</span>
|
||||||
|
{option.active ? <Check size={14} /> : <span className="sort-menu-check-placeholder" />}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const createMenu = (
|
||||||
|
<div className={`create-menu-wrap ${props.isMobileLayout ? 'mobile-fab-wrap' : 'desktop-create-menu-wrap'}`} ref={props.createMenuRef}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn btn-primary small ${props.isMobileLayout ? 'mobile-fab-trigger' : 'desktop-create-trigger'}`}
|
||||||
aria-label={t('txt_add')}
|
aria-label={t('txt_add')}
|
||||||
title={t('txt_add')}
|
title={t('txt_add')}
|
||||||
onClick={props.onToggleCreateMenu}
|
onClick={props.onToggleCreateMenu}
|
||||||
@@ -144,24 +281,54 @@ export default function VaultListPanel(props: VaultListPanelProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="list-col">
|
<section className="list-col">
|
||||||
<div className="list-head">
|
<div className="list-toolbar-stack" ref={mobileFilterRef}>
|
||||||
|
<div className={`list-head ${props.selectedCount > 0 ? 'selection-mode' : ''}`}>
|
||||||
|
{props.selectedCount > 0 ? (
|
||||||
|
<>
|
||||||
|
{props.sidebarFilter.kind !== 'duplicates' && (
|
||||||
|
<button type="button" className="btn btn-secondary small" disabled={!props.filteredCiphers.length} onClick={props.onSelectAll}>
|
||||||
|
<CheckCheck size={14} className="btn-icon" /> {t('txt_select_all')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button type="button" className="btn btn-secondary small" onClick={props.onClearSelection}>
|
||||||
|
<X size={14} className="btn-icon" /> {t('txt_cancel')}
|
||||||
|
</button>
|
||||||
|
{props.sidebarFilter.kind === 'trash' && (
|
||||||
|
<button type="button" className="btn btn-secondary small" disabled={props.busy} onClick={props.onBulkRestore}>
|
||||||
|
<RefreshCw size={14} className="btn-icon" /> {t('txt_restore')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{props.sidebarFilter.kind === 'archive' && (
|
||||||
|
<button type="button" className="btn btn-secondary small" disabled={props.busy} onClick={props.onBulkUnarchive}>
|
||||||
|
<RotateCcw size={14} className="btn-icon" /> {t('txt_unarchive')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{props.sidebarFilter.kind !== 'trash' && props.sidebarFilter.kind !== 'archive' && props.sidebarFilter.kind !== 'duplicates' && (
|
||||||
|
<button type="button" className="btn btn-secondary small" disabled={props.busy} onClick={props.onBulkArchive}>
|
||||||
|
<Archive size={14} className="btn-icon" /> {t('txt_archive_selected')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{props.sidebarFilter.kind !== 'trash' && props.sidebarFilter.kind !== 'archive' && props.sidebarFilter.kind !== 'duplicates' && (
|
||||||
|
<button type="button" className="btn btn-secondary small" disabled={props.busy} onClick={props.onOpenMove}>
|
||||||
|
<FolderInput size={14} className="btn-icon" /> {t('txt_move')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button type="button" className="btn btn-danger small" disabled={props.busy} onClick={props.onOpenBulkDelete}>
|
||||||
|
<Trash2 size={14} className="btn-icon" /> {props.sidebarFilter.kind === 'trash' ? t('txt_delete_permanently') : t('txt_delete_selected')}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<div className="search-input-wrap">
|
<div className="search-input-wrap">
|
||||||
{props.sidebarFilter.kind === 'duplicates' && props.isMobileLayout ? (
|
{props.sidebarFilter.kind === 'duplicates' && props.isMobileLayout ? (
|
||||||
<select
|
<div className="duplicate-mode-head-menu">
|
||||||
className="input duplicate-mode-select duplicate-mode-head-select"
|
{renderMobileFilterMenu('duplicate', t('txt_duplicate_detection_mode'), duplicateModeSelected, <Copy size={14} />, duplicateModeOptions)}
|
||||||
value={props.duplicateMode}
|
</div>
|
||||||
aria-label={t('txt_duplicate_detection_mode')}
|
|
||||||
onChange={(event) => props.onDuplicateModeChange((event.currentTarget as HTMLSelectElement).value as DuplicateDetectionMode)}
|
|
||||||
>
|
|
||||||
{duplicateDetectionOptions.map((option) => (
|
|
||||||
<option key={option.value} value={option.value}>{option.label}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<input
|
<input
|
||||||
className="search-input"
|
className="search-input"
|
||||||
placeholder={t('txt_search_your_secure_vault')}
|
placeholder={t('txt_search_items_count', { count: props.totalCipherCount })}
|
||||||
value={props.searchInput}
|
value={props.searchInput}
|
||||||
onInput={(e) => props.onSearchInput((e.currentTarget as HTMLInputElement).value)}
|
onInput={(e) => props.onSearchInput((e.currentTarget as HTMLInputElement).value)}
|
||||||
onCompositionStart={props.onSearchCompositionStart}
|
onCompositionStart={props.onSearchCompositionStart}
|
||||||
@@ -186,15 +353,20 @@ export default function VaultListPanel(props: VaultListPanelProps) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{props.sidebarFilter.kind === 'duplicates' && !props.isMobileLayout && (
|
||||||
|
<div className="duplicate-mode-head-menu">
|
||||||
|
{renderMobileFilterMenu('duplicate', t('txt_duplicate_detection_mode'), duplicateModeSelected, <Copy size={14} />, duplicateModeOptions)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="sort-menu-wrap" ref={props.sortMenuRef}>
|
<div className="sort-menu-wrap" ref={props.sortMenuRef}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`btn btn-secondary small sort-trigger ${props.sortMenuOpen ? 'active' : ''}`}
|
className={`btn btn-secondary small sort-trigger sort-trigger-labeled ${props.sortMenuOpen ? 'active' : ''}`}
|
||||||
aria-label={t('txt_sort')}
|
aria-label={t('txt_sort')}
|
||||||
title={t('txt_sort')}
|
title={t('txt_sort')}
|
||||||
onClick={props.onToggleSortMenu}
|
onClick={props.onToggleSortMenu}
|
||||||
>
|
>
|
||||||
<ArrowUpDown size={14} className="btn-icon" />
|
<ArrowUpDown size={14} className="btn-icon" /> <span>{t('txt_sort')}</span>
|
||||||
</button>
|
</button>
|
||||||
{props.sortMenuOpen && (
|
{props.sortMenuOpen && (
|
||||||
<div className="sort-menu">
|
<div className="sort-menu">
|
||||||
@@ -212,71 +384,24 @@ export default function VaultListPanel(props: VaultListPanelProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="list-count" title={t('txt_total_items_count', { count: props.totalCipherCount })}>
|
|
||||||
{t('txt_total_items_count', { count: props.totalCipherCount })}
|
|
||||||
</div>
|
|
||||||
<button type="button" className="btn btn-secondary small list-icon-btn" disabled={props.busy || props.loading} onClick={props.onSyncVault}>
|
<button type="button" className="btn btn-secondary small list-icon-btn" disabled={props.busy || props.loading} onClick={props.onSyncVault}>
|
||||||
<RefreshCw size={14} className="btn-icon" /> {t('txt_sync_vault')}
|
<RefreshCw size={14} className="btn-icon" /> {t('txt_sync_vault')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
{!props.isMobileLayout && props.sidebarFilter !== undefined && createMenu}
|
||||||
<div className={`toolbar actions ${props.sidebarFilter.kind === 'duplicates' ? 'duplicates-toolbar' : ''}`}>
|
</>
|
||||||
{props.sidebarFilter.kind === 'duplicates' && !props.isMobileLayout && (
|
|
||||||
<select
|
|
||||||
className="input duplicate-mode-select duplicate-mode-toolbar-select"
|
|
||||||
value={props.duplicateMode}
|
|
||||||
aria-label={t('txt_duplicate_detection_mode')}
|
|
||||||
onChange={(event) => props.onDuplicateModeChange((event.currentTarget as HTMLSelectElement).value as DuplicateDetectionMode)}
|
|
||||||
>
|
|
||||||
{duplicateDetectionOptions.map((option) => (
|
|
||||||
<option key={option.value} value={option.value}>{option.label}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
)}
|
|
||||||
{props.sidebarFilter.kind === 'duplicates' && props.duplicateMode === 'exact' && (
|
|
||||||
<button type="button" className="btn btn-secondary small" disabled={!props.filteredCiphers.length || props.busy} onClick={props.onSelectDuplicates}>
|
|
||||||
<Check size={14} className="btn-icon" /> {t('txt_select_duplicate_items')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{props.selectedCount > 0 && props.sidebarFilter.kind === 'trash' && (
|
|
||||||
<button type="button" className="btn btn-secondary small" disabled={props.busy} onClick={props.onBulkRestore}>
|
|
||||||
<RefreshCw size={14} className="btn-icon" /> {t('txt_restore')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{props.selectedCount > 0 && props.sidebarFilter.kind === 'archive' && (
|
|
||||||
<button type="button" className="btn btn-secondary small" disabled={props.busy} onClick={props.onBulkUnarchive}>
|
|
||||||
<RotateCcw size={14} className="btn-icon" /> {t('txt_unarchive')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{props.selectedCount > 0 && props.sidebarFilter.kind !== 'trash' && props.sidebarFilter.kind !== 'archive' && props.sidebarFilter.kind !== 'duplicates' && (
|
|
||||||
<button type="button" className="btn btn-secondary small" disabled={props.busy} onClick={props.onBulkArchive}>
|
|
||||||
<Archive size={14} className="btn-icon" /> {t('txt_archive_selected')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{props.selectedCount > 0 && props.sidebarFilter.kind !== 'trash' && props.sidebarFilter.kind !== 'archive' && props.sidebarFilter.kind !== 'duplicates' && (
|
|
||||||
<button type="button" className="btn btn-secondary small" disabled={props.busy} onClick={props.onOpenMove}>
|
|
||||||
<FolderInput size={14} className="btn-icon" /> {t('txt_move')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{props.selectedCount > 0 && (
|
|
||||||
<button type="button" className="btn btn-secondary small" onClick={props.onClearSelection}>
|
|
||||||
<X size={14} className="btn-icon" /> {t('txt_cancel')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button type="button" className="btn btn-danger small" disabled={!props.selectedCount || props.busy} onClick={props.onOpenBulkDelete}>
|
|
||||||
<Trash2 size={14} className="btn-icon" /> {props.sidebarFilter.kind === 'trash' ? t('txt_delete_permanently') : t('txt_delete_selected')}
|
|
||||||
</button>
|
|
||||||
{props.sidebarFilter.kind !== 'duplicates' && (
|
|
||||||
<button type="button" className="btn btn-secondary small" disabled={!props.filteredCiphers.length} onClick={props.onSelectAll}>
|
|
||||||
<CheckCheck size={14} className="btn-icon" /> {t('txt_select_all')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{props.sidebarFilter.kind !== 'duplicates' && (
|
|
||||||
props.isMobileLayout && typeof document !== 'undefined'
|
|
||||||
? props.mobileFabVisible ? createPortal(createMenu, document.body) : null
|
|
||||||
: createMenu
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{props.isMobileLayout && (
|
||||||
|
<div className="mobile-vault-filter-row" aria-label={t('txt_filter')}>
|
||||||
|
{renderMobileFilterMenu('menu', t('txt_menu'), menuFilterSelected, <LayoutGrid size={14} />, menuFilterOptions)}
|
||||||
|
{renderMobileFilterMenu('type', t('txt_type'), typeFilterSelected, <Globe size={14} />, typeMobileFilterOptions)}
|
||||||
|
{renderMobileFilterMenu('folder', t('txt_folder'), folderFilterSelected, <FolderIcon size={14} />, folderMobileFilterOptions)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!props.selectedCount && props.isMobileLayout && props.sidebarFilter.kind !== 'duplicates' && typeof document !== 'undefined' && props.mobileFabVisible
|
||||||
|
? createPortal(createMenu, document.body)
|
||||||
|
: null}
|
||||||
<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 && (
|
{!props.loading && !!props.error && !props.filteredCiphers.length && (
|
||||||
|
|||||||
@@ -758,6 +758,7 @@ const en: Record<string, string> = {
|
|||||||
"txt_search_sends": "Search sends...",
|
"txt_search_sends": "Search sends...",
|
||||||
"txt_session_refresh_failed": "Session refresh failed. Please sign in again.",
|
"txt_session_refresh_failed": "Session refresh failed. Please sign in again.",
|
||||||
"txt_search_your_secure_vault": "Search your secure vault...",
|
"txt_search_your_secure_vault": "Search your secure vault...",
|
||||||
|
"txt_search_items_count": "Search within {count} items...",
|
||||||
"txt_clear_search": "Clear search",
|
"txt_clear_search": "Clear search",
|
||||||
"txt_clear_search_esc": "Clear search (Esc)",
|
"txt_clear_search_esc": "Clear search (Esc)",
|
||||||
"txt_sort": "Sort",
|
"txt_sort": "Sort",
|
||||||
|
|||||||
@@ -758,6 +758,7 @@ const es: Record<string, string> = {
|
|||||||
"txt_search_sends": "Buscar envíos...",
|
"txt_search_sends": "Buscar envíos...",
|
||||||
"txt_session_refresh_failed": "Error al actualizar la sesión. Inicia sesión de nuevo.",
|
"txt_session_refresh_failed": "Error al actualizar la sesión. Inicia sesión de nuevo.",
|
||||||
"txt_search_your_secure_vault": "Buscar en su bóveda segura...",
|
"txt_search_your_secure_vault": "Buscar en su bóveda segura...",
|
||||||
|
"txt_search_items_count": "Buscar entre {count} elementos...",
|
||||||
"txt_clear_search": "Limpiar búsqueda",
|
"txt_clear_search": "Limpiar búsqueda",
|
||||||
"txt_clear_search_esc": "Limpiar búsqueda (Esc)",
|
"txt_clear_search_esc": "Limpiar búsqueda (Esc)",
|
||||||
"txt_sort": "Ordenar",
|
"txt_sort": "Ordenar",
|
||||||
|
|||||||
@@ -758,6 +758,7 @@ const ru: Record<string, string> = {
|
|||||||
"txt_search_sends": "Поиск отправляет...",
|
"txt_search_sends": "Поиск отправляет...",
|
||||||
"txt_session_refresh_failed": "Не удалось обновить сеанс. Войдите снова.",
|
"txt_session_refresh_failed": "Не удалось обновить сеанс. Войдите снова.",
|
||||||
"txt_search_your_secure_vault": "Найдите свое безопасное хранилище...",
|
"txt_search_your_secure_vault": "Найдите свое безопасное хранилище...",
|
||||||
|
"txt_search_items_count": "Поиск по {count} элементам...",
|
||||||
"txt_clear_search": "Очистить поиск",
|
"txt_clear_search": "Очистить поиск",
|
||||||
"txt_clear_search_esc": "Очистить поиск (Esc)",
|
"txt_clear_search_esc": "Очистить поиск (Esc)",
|
||||||
"txt_sort": "Сортировать",
|
"txt_sort": "Сортировать",
|
||||||
|
|||||||
@@ -758,6 +758,7 @@ const zhCN: Record<string, string> = {
|
|||||||
"txt_search_sends": "搜索 Send...",
|
"txt_search_sends": "搜索 Send...",
|
||||||
"txt_session_refresh_failed": "会话刷新失败,请重新登录",
|
"txt_session_refresh_failed": "会话刷新失败,请重新登录",
|
||||||
"txt_search_your_secure_vault": "搜索你的密码库...",
|
"txt_search_your_secure_vault": "搜索你的密码库...",
|
||||||
|
"txt_search_items_count": "共 {count} 项中搜索...",
|
||||||
"txt_clear_search": "清空搜索",
|
"txt_clear_search": "清空搜索",
|
||||||
"txt_clear_search_esc": "清空搜索(Esc)",
|
"txt_clear_search_esc": "清空搜索(Esc)",
|
||||||
"txt_sort": "排序",
|
"txt_sort": "排序",
|
||||||
|
|||||||
@@ -758,6 +758,7 @@ const zhTW: Record<string, string> = {
|
|||||||
"txt_search_sends": "搜索 Send...",
|
"txt_search_sends": "搜索 Send...",
|
||||||
"txt_session_refresh_failed": "會話刷新失敗,請重新登入",
|
"txt_session_refresh_failed": "會話刷新失敗,請重新登入",
|
||||||
"txt_search_your_secure_vault": "搜索你的密碼庫...",
|
"txt_search_your_secure_vault": "搜索你的密碼庫...",
|
||||||
|
"txt_search_items_count": "在共 {count} 項中搜索...",
|
||||||
"txt_clear_search": "清空搜索",
|
"txt_clear_search": "清空搜索",
|
||||||
"txt_clear_search_esc": "清空搜索(Esc)",
|
"txt_clear_search_esc": "清空搜索(Esc)",
|
||||||
"txt_sort": "排序",
|
"txt_sort": "排序",
|
||||||
|
|||||||
@@ -596,7 +596,7 @@ h4 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list-head {
|
.list-head {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-count {
|
.list-count {
|
||||||
@@ -1069,6 +1069,7 @@ textarea {
|
|||||||
.list-panel {
|
.list-panel {
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-item {
|
.list-item {
|
||||||
|
|||||||
@@ -70,6 +70,7 @@
|
|||||||
:root[data-theme='dark'] .textarea,
|
:root[data-theme='dark'] .textarea,
|
||||||
:root[data-theme='dark'] select.input,
|
:root[data-theme='dark'] select.input,
|
||||||
:root[data-theme='dark'] .search-input,
|
:root[data-theme='dark'] .search-input,
|
||||||
|
:root[data-theme='dark'] .mobile-vault-filter-trigger,
|
||||||
:root[data-theme='dark'] .dialog input,
|
:root[data-theme='dark'] .dialog input,
|
||||||
:root[data-theme='dark'] .dialog textarea,
|
:root[data-theme='dark'] .dialog textarea,
|
||||||
:root[data-theme='dark'] .dialog select {
|
:root[data-theme='dark'] .dialog select {
|
||||||
@@ -79,6 +80,13 @@
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root[data-theme='dark'] .mobile-vault-filter-trigger:hover,
|
||||||
|
:root[data-theme='dark'] .mobile-vault-filter-trigger.active {
|
||||||
|
background: var(--panel-muted);
|
||||||
|
border-color: color-mix(in srgb, var(--primary) 42%, var(--line));
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
:root[data-theme='dark'] .input::placeholder,
|
:root[data-theme='dark'] .input::placeholder,
|
||||||
:root[data-theme='dark'] .textarea::placeholder,
|
:root[data-theme='dark'] .textarea::placeholder,
|
||||||
:root[data-theme='dark'] input::placeholder,
|
:root[data-theme='dark'] input::placeholder,
|
||||||
|
|||||||
@@ -31,19 +31,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
select.input {
|
select.input {
|
||||||
@apply py-0 pr-[42px];
|
@apply py-0 pr-3.5;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
appearance: none;
|
appearance: auto;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: auto;
|
||||||
-moz-appearance: none;
|
-moz-appearance: auto;
|
||||||
background-image:
|
background-image: none;
|
||||||
linear-gradient(45deg, transparent 50%, #365fa8 50%),
|
|
||||||
linear-gradient(135deg, #365fa8 50%, transparent 50%);
|
|
||||||
background-position:
|
|
||||||
calc(100% - 18px) calc(50% - 3px),
|
|
||||||
calc(100% - 12px) calc(50% - 3px);
|
|
||||||
background-size: 6px 6px, 6px 6px;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type='file'].input {
|
input[type='file'].input {
|
||||||
|
|||||||
@@ -209,14 +209,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toast-close {
|
.toast-close {
|
||||||
@apply cursor-pointer border-0 bg-transparent text-xl;
|
@apply flex cursor-pointer items-center justify-center border-0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
transition: transform var(--dur-fast) var(--ease-out-soft), opacity var(--dur-fast) var(--ease-smooth);
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
transition: background 120ms ease, opacity 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-close:hover {
|
.toast-close:hover {
|
||||||
transform: scale(1.08);
|
background: rgba(0, 0, 0, 0.08);
|
||||||
opacity: 0.84;
|
}
|
||||||
|
|
||||||
|
.toast-close:active {
|
||||||
|
background: rgba(0, 0, 0, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-close:focus-visible {
|
||||||
|
outline: 2px solid currentColor;
|
||||||
|
outline-offset: -2px;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-progress {
|
.toast-progress {
|
||||||
|
|||||||
@@ -302,10 +302,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list-head {
|
.list-head {
|
||||||
@apply grid items-center gap-2;
|
@apply grid items-center gap-1.5;
|
||||||
grid-template-columns: minmax(0, 1fr) auto auto auto;
|
grid-template-columns: minmax(0, 1fr) auto auto auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-head.selection-mode {
|
||||||
|
@apply justify-stretch;
|
||||||
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
.list-count {
|
.list-count {
|
||||||
grid-column: auto;
|
grid-column: auto;
|
||||||
@apply w-auto whitespace-nowrap text-xs;
|
@apply w-auto whitespace-nowrap text-xs;
|
||||||
@@ -316,15 +321,50 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.list-head .search-input {
|
.list-head .search-input {
|
||||||
@apply h-[42px] w-full min-w-0 rounded-[14px];
|
@apply h-[34px] w-full min-w-0 rounded-[10px] px-3 py-0 text-[13px] font-semibold;
|
||||||
|
line-height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-vault-filter-row {
|
||||||
|
@apply grid min-w-0 gap-1.5;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-head .duplicate-mode-head-select {
|
.list-head .duplicate-mode-head-select {
|
||||||
@apply h-[34px] min-w-0 w-auto max-w-full rounded-full;
|
@apply h-[34px] min-w-0 w-auto max-w-full rounded-[10px];
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-icon-btn {
|
.list-icon-btn {
|
||||||
@apply w-auto min-w-0 gap-1.5 whitespace-nowrap px-3 py-0 text-[13px];
|
@apply h-[34px] w-auto min-w-0 gap-1.5 whitespace-nowrap rounded-[10px] px-3 py-0 text-[13px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-head.selection-mode > .btn.small {
|
||||||
|
@apply h-[34px] min-w-0 w-full justify-center gap-1 px-2 text-[12px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-head.selection-mode > .btn.small .btn-icon {
|
||||||
|
@apply m-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-trigger {
|
||||||
|
@apply h-[34px] w-[34px] min-w-[34px] rounded-[10px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-trigger.sort-trigger-labeled {
|
||||||
|
@apply h-[34px] w-[34px] min-w-[34px] gap-0 px-0 text-[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-trigger.sort-trigger-labeled .btn-icon {
|
||||||
|
@apply m-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-create-menu-wrap {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duplicate-mode-head-menu .mobile-vault-filter-menu {
|
||||||
|
min-width: max(100%, 190px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar.actions {
|
.toolbar.actions {
|
||||||
@@ -346,6 +386,10 @@
|
|||||||
@apply h-[34px] w-auto min-w-0 gap-1.5 whitespace-nowrap rounded-full px-3 py-0 text-[13px];
|
@apply h-[34px] w-auto min-w-0 gap-1.5 whitespace-nowrap rounded-full px-3 py-0 text-[13px];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-count-status {
|
||||||
|
@apply mb-1 px-1;
|
||||||
|
}
|
||||||
|
|
||||||
.mobile-fab-wrap {
|
.mobile-fab-wrap {
|
||||||
@apply fixed right-3.5 z-[45];
|
@apply fixed right-3.5 z-[45];
|
||||||
bottom: calc(14px + var(--mobile-tabbar-height, 70px) + env(safe-area-inset-bottom));
|
bottom: calc(14px + var(--mobile-tabbar-height, 70px) + env(safe-area-inset-bottom));
|
||||||
|
|||||||
@@ -221,24 +221,110 @@ select.input.duplicate-mode-toolbar-select {
|
|||||||
@apply h-[34px] w-auto min-w-[156px] max-w-full;
|
@apply h-[34px] w-auto min-w-[156px] max-w-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.duplicate-mode-head-menu {
|
||||||
|
@apply min-w-0;
|
||||||
|
}
|
||||||
|
|
||||||
.duplicate-mode-toolbar-select {
|
.duplicate-mode-toolbar-select {
|
||||||
@apply w-auto max-w-[170px] shrink-0;
|
@apply w-auto max-w-[170px] shrink-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-head {
|
.list-head {
|
||||||
@apply mb-2 flex items-center gap-2.5;
|
@apply mb-1.5 flex items-center gap-2;
|
||||||
|
min-height: 34px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-head .search-input-wrap {
|
.list-head.selection-mode {
|
||||||
|
@apply gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-head.selection-mode > .btn.small {
|
||||||
|
@apply min-w-0 flex-1 justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-head .search-input-wrap,
|
||||||
|
.duplicate-mode-head-menu {
|
||||||
@apply min-w-0 flex-auto;
|
@apply min-w-0 flex-auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-head .search-input {
|
.list-head .search-input {
|
||||||
@apply h-[42px];
|
@apply h-[34px] rounded-[10px] px-3 py-0 text-[13px] font-semibold;
|
||||||
|
line-height: 34px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-head .btn {
|
.list-head .btn {
|
||||||
@apply whitespace-nowrap;
|
@apply h-[34px] whitespace-nowrap rounded-[10px] px-3 py-0 text-[13px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-vault-filter-row {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-vault-filter-control {
|
||||||
|
@apply relative min-w-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-vault-filter-trigger {
|
||||||
|
@apply flex h-[34px] w-full min-w-0 cursor-pointer items-center gap-1.5 rounded-[10px] border px-2.5 py-0 text-left text-[13px] font-semibold;
|
||||||
|
background: var(--panel);
|
||||||
|
border-color: rgba(74, 103, 150, 0.28);
|
||||||
|
color: var(--muted-strong);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.74);
|
||||||
|
transition:
|
||||||
|
border-color var(--dur-fast) var(--ease-smooth),
|
||||||
|
background-color var(--dur-fast) var(--ease-smooth),
|
||||||
|
color var(--dur-fast) var(--ease-smooth),
|
||||||
|
box-shadow var(--dur-fast) var(--ease-smooth);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-vault-filter-trigger:hover,
|
||||||
|
.mobile-vault-filter-trigger.active {
|
||||||
|
border-color: rgba(43, 102, 217, 0.46);
|
||||||
|
background: #f8fbff;
|
||||||
|
color: var(--primary-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-vault-filter-trigger:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.14), inset 0 1px 0 rgba(255, 255, 255, 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-vault-filter-trigger-icon,
|
||||||
|
.mobile-vault-filter-item-icon {
|
||||||
|
@apply inline-flex shrink-0 items-center justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-vault-filter-trigger-label {
|
||||||
|
@apply min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-vault-filter-chevron {
|
||||||
|
@apply shrink-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-vault-filter-trigger.active .mobile-vault-filter-chevron {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-vault-filter-menu {
|
||||||
|
@apply left-0 right-auto max-h-[280px] min-w-full overflow-auto;
|
||||||
|
min-width: max(100%, 168px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-vault-filter-control:last-child .mobile-vault-filter-menu {
|
||||||
|
@apply left-auto right-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-vault-filter-item {
|
||||||
|
@apply gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-vault-filter-item-main {
|
||||||
|
@apply flex min-w-0 items-center gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-vault-filter-item-main span:last-child {
|
||||||
|
@apply min-w-0 overflow-hidden text-ellipsis whitespace-nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-count {
|
.list-count {
|
||||||
@@ -262,6 +348,10 @@ select.input.duplicate-mode-toolbar-select {
|
|||||||
@apply w-9 min-w-9 justify-center gap-0 p-0;
|
@apply w-9 min-w-9 justify-center gap-0 p-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sort-trigger.sort-trigger-labeled {
|
||||||
|
@apply h-[34px] w-auto min-w-0 gap-1.5 rounded-[10px] px-3;
|
||||||
|
}
|
||||||
|
|
||||||
.sort-trigger.active {
|
.sort-trigger.active {
|
||||||
background: #e9f1ff;
|
background: #e9f1ff;
|
||||||
border-color: #a9c2ee;
|
border-color: #a9c2ee;
|
||||||
@@ -303,6 +393,30 @@ select.input.duplicate-mode-toolbar-select {
|
|||||||
@apply h-3.5 w-3.5 shrink-0;
|
@apply h-3.5 w-3.5 shrink-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.desktop-create-menu-wrap {
|
||||||
|
@apply shrink-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-create-trigger {
|
||||||
|
@apply h-[34px] w-[34px] min-w-[34px] gap-0 rounded-[10px] p-0 text-[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-create-trigger .btn-icon {
|
||||||
|
@apply m-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duplicates-helper-toolbar {
|
||||||
|
@apply justify-start pb-0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duplicates-helper-toolbar .btn.small {
|
||||||
|
@apply h-[34px] rounded-[10px] px-3 py-0 text-[13px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-count-status {
|
||||||
|
@apply mb-1 pl-1;
|
||||||
|
}
|
||||||
|
|
||||||
.list-panel {
|
.list-panel {
|
||||||
@apply min-h-0 overflow-auto p-2;
|
@apply min-h-0 overflow-auto p-2;
|
||||||
/* Virtualized rows update top/bottom spacers while scrolling. Chrome's scroll anchoring
|
/* Virtualized rows update top/bottom spacers while scrolling. Chrome's scroll anchoring
|
||||||
|
|||||||