diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index 3bd0180..cd49fc1 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -1535,7 +1535,7 @@ export default function App() { const isKnownAppRoute = APP_ROUTES.has(routeLocation) || isPublicSendRoute || isImportHashRoute; const isUnknownRoute = isMalformedSendRoute || (phase === 'app' ? !isKnownAppRoute : !isKnownAuthRoute && !APP_ROUTES.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 demoDomainRules = useMemo(() => ({ equivalentDomains: [ diff --git a/webapp/src/components/VaultPage.tsx b/webapp/src/components/VaultPage.tsx index f28ecc3..9615a0f 100644 --- a/webapp/src/components/VaultPage.tsx +++ b/webapp/src/components/VaultPage.tsx @@ -1115,6 +1115,7 @@ const folderName = useCallback((id: string | null | undefined): string => { busy={busy} loading={props.loading} error={props.error} + folders={props.folders} searchInput={searchInput} sortMode={sortMode} sortMenuOpen={sortMenuOpen} @@ -1141,6 +1142,7 @@ const folderName = useCallback((id: string | null | undefined): string => { onToggleSortMenu={handleToggleSortMenu} onSelectSortMode={handleSelectSortMode} onDuplicateModeChange={setDuplicateMode} + onChangeFilter={setSidebarFilter} onSyncVault={handleSyncVault} onOpenBulkDelete={handleOpenBulkDelete} onSelectDuplicates={handleSelectDuplicates} diff --git a/webapp/src/components/vault/VaultListPanel.tsx b/webapp/src/components/vault/VaultListPanel.tsx index 24bd57e..d598924 100644 --- a/webapp/src/components/vault/VaultListPanel.tsx +++ b/webapp/src/components/vault/VaultListPanel.tsx @@ -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 { 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 type { Cipher } from '@/lib/types'; +import type { Cipher, Folder } from '@/lib/types'; import { t } from '@/lib/i18n'; import { CreateTypeIcon, @@ -27,6 +50,7 @@ interface VaultListPanelProps { busy: boolean; loading: boolean; error: string; + folders: Folder[]; searchInput: string; sortMode: VaultSortMode; sortMenuOpen: boolean; @@ -53,6 +77,7 @@ interface VaultListPanelProps { onToggleSortMenu: () => void; onSelectSortMode: (value: VaultSortMode) => void; onDuplicateModeChange: (value: DuplicateDetectionMode) => void; + onChangeFilter: (filter: SidebarFilter) => void; onSyncVault: () => void; onOpenBulkDelete: () => void; onSelectDuplicates: () => void; @@ -80,6 +105,16 @@ interface CipherListItemProps { 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 duplicateGroupHue = props.duplicateGroupIndex === null ? null : (props.duplicateGroupIndex * 137.508) % 360; return ( @@ -115,14 +150,116 @@ const CipherListItem = memo(function CipherListItem(props: CipherListItemProps) }); export default function VaultListPanel(props: VaultListPanelProps) { + const [mobileFilterOpen, setMobileFilterOpen] = useState(null); + const mobileFilterRef = useRef(null); const createTypeOptions = getCreateTypeOptions(); const duplicateDetectionOptions = getDuplicateDetectionOptions(); const vaultSortOptions = getVaultSortOptions(); - const createMenu = ( -
+ const duplicateModeOptions: MobileFilterOption[] = duplicateDetectionOptions.map((option) => ({ + value: option.value, + label: option.label, + icon: option.value === 'login-site' ? : option.value === 'exact' ? : , + active: props.duplicateMode === option.value, + onSelect: () => props.onDuplicateModeChange(option.value), + })); + const menuFilterOptions: MobileFilterOption[] = [ + { value: 'all', label: t('txt_all_items'), icon: , active: props.sidebarFilter.kind === 'all', onSelect: () => props.onChangeFilter({ kind: 'all' }) }, + { value: 'favorite', label: t('txt_favorites'), icon: , active: props.sidebarFilter.kind === 'favorite', onSelect: () => props.onChangeFilter({ kind: 'favorite' }) }, + { value: 'archive', label: t('txt_archive'), icon: , active: props.sidebarFilter.kind === 'archive', onSelect: () => props.onChangeFilter({ kind: 'archive' }) }, + { value: 'trash', label: t('txt_trash'), icon: , active: props.sidebarFilter.kind === 'trash', onSelect: () => props.onChangeFilter({ kind: 'trash' }) }, + { value: 'duplicates', label: t('txt_duplicates'), icon: , active: props.sidebarFilter.kind === 'duplicates', onSelect: () => props.onChangeFilter({ kind: 'duplicates' }) }, + ]; + const typeMobileFilterOptions: MobileFilterOption[] = [ + { value: 'login', label: t('txt_login'), icon: , active: props.sidebarFilter.kind === 'type' && props.sidebarFilter.value === 'login', onSelect: () => props.onChangeFilter({ kind: 'type', value: 'login' }) }, + { value: 'card', label: t('txt_card'), icon: , active: props.sidebarFilter.kind === 'type' && props.sidebarFilter.value === 'card', onSelect: () => props.onChangeFilter({ kind: 'type', value: 'card' }) }, + { value: 'identity', label: t('txt_identity'), icon: , active: props.sidebarFilter.kind === 'type' && props.sidebarFilter.value === 'identity', onSelect: () => props.onChangeFilter({ kind: 'type', value: 'identity' }) }, + { value: 'note', label: t('txt_note'), icon: , active: props.sidebarFilter.kind === 'type' && props.sidebarFilter.value === 'note', onSelect: () => props.onChangeFilter({ kind: 'type', value: 'note' }) }, + { value: 'ssh', label: t('txt_ssh_key'), icon: , 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: , 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: , + 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[] + ) => ( +
+ {mobileFilterOpen === key && ( +
+ {options.map((option) => ( + + ))} +
+ )} +
+ ); + + const createMenu = ( +
+ )} + + {props.sidebarFilter.kind === 'trash' && ( + + )} + {props.sidebarFilter.kind === 'archive' && ( + + )} + {props.sidebarFilter.kind !== 'trash' && props.sidebarFilter.kind !== 'archive' && props.sidebarFilter.kind !== 'duplicates' && ( + + )} + {props.sidebarFilter.kind !== 'trash' && props.sidebarFilter.kind !== 'archive' && props.sidebarFilter.kind !== 'duplicates' && ( + + )} + + + ) : ( + <> +
+ {props.sidebarFilter.kind === 'duplicates' && props.isMobileLayout ? ( +
+ {renderMobileFilterMenu('duplicate', t('txt_duplicate_detection_mode'), duplicateModeSelected, , duplicateModeOptions)} +
+ ) : ( + <> + props.onSearchInput((e.currentTarget as HTMLInputElement).value)} + onCompositionStart={props.onSearchCompositionStart} + onCompositionEnd={(e) => props.onSearchCompositionEnd((e.currentTarget as HTMLInputElement).value)} + onKeyDown={(e) => { + if (e.key !== 'Escape' || !props.searchInput) return; + e.preventDefault(); + props.onClearSearch(); + }} + /> + {!!props.searchInput && ( + + )} + + )} +
+ {props.sidebarFilter.kind === 'duplicates' && !props.isMobileLayout && ( +
+ {renderMobileFilterMenu('duplicate', t('txt_duplicate_detection_mode'), duplicateModeSelected, , duplicateModeOptions)} +
+ )} +
+ + {props.sortMenuOpen && ( +
+ {vaultSortOptions.map((option) => ( + + ))} +
+ )} +
+ + {!props.isMobileLayout && props.sidebarFilter !== undefined && createMenu} )}
-
- - {props.sortMenuOpen && ( -
- {vaultSortOptions.map((option) => ( - - ))} -
- )} -
-
- {t('txt_total_items_count', { count: props.totalCipherCount })} -
- -
-
- {props.sidebarFilter.kind === 'duplicates' && !props.isMobileLayout && ( - - )} - {props.sidebarFilter.kind === 'duplicates' && props.duplicateMode === 'exact' && ( - - )} - {props.selectedCount > 0 && props.sidebarFilter.kind === 'trash' && ( - - )} - {props.selectedCount > 0 && props.sidebarFilter.kind === 'archive' && ( - - )} - {props.selectedCount > 0 && props.sidebarFilter.kind !== 'trash' && props.sidebarFilter.kind !== 'archive' && props.sidebarFilter.kind !== 'duplicates' && ( - - )} - {props.selectedCount > 0 && props.sidebarFilter.kind !== 'trash' && props.sidebarFilter.kind !== 'archive' && props.sidebarFilter.kind !== 'duplicates' && ( - - )} - {props.selectedCount > 0 && ( - - )} - - {props.sidebarFilter.kind !== 'duplicates' && ( - - )} - {props.sidebarFilter.kind !== 'duplicates' && ( - props.isMobileLayout && typeof document !== 'undefined' - ? props.mobileFabVisible ? createPortal(createMenu, document.body) : null - : createMenu + {props.isMobileLayout && ( +
+ {renderMobileFilterMenu('menu', t('txt_menu'), menuFilterSelected, , menuFilterOptions)} + {renderMobileFilterMenu('type', t('txt_type'), typeFilterSelected, , typeMobileFilterOptions)} + {renderMobileFilterMenu('folder', t('txt_folder'), folderFilterSelected, , folderMobileFilterOptions)} +
)}
- + {!props.selectedCount && props.isMobileLayout && props.sidebarFilter.kind !== 'duplicates' && typeof document !== 'undefined' && props.mobileFabVisible + ? createPortal(createMenu, document.body) + : null}
props.onScroll((event.currentTarget as HTMLDivElement).scrollTop)}> {props.loading && !props.filteredCiphers.length && } {!props.loading && !!props.error && !props.filteredCiphers.length && ( diff --git a/webapp/src/lib/i18n/locales/en.ts b/webapp/src/lib/i18n/locales/en.ts index 5941f96..51010c7 100644 --- a/webapp/src/lib/i18n/locales/en.ts +++ b/webapp/src/lib/i18n/locales/en.ts @@ -758,6 +758,7 @@ const en: Record = { "txt_search_sends": "Search sends...", "txt_session_refresh_failed": "Session refresh failed. Please sign in again.", "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_esc": "Clear search (Esc)", "txt_sort": "Sort", diff --git a/webapp/src/lib/i18n/locales/es.ts b/webapp/src/lib/i18n/locales/es.ts index 7d221ab..5b22444 100644 --- a/webapp/src/lib/i18n/locales/es.ts +++ b/webapp/src/lib/i18n/locales/es.ts @@ -758,6 +758,7 @@ const es: Record = { "txt_search_sends": "Buscar envíos...", "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_items_count": "Buscar entre {count} elementos...", "txt_clear_search": "Limpiar búsqueda", "txt_clear_search_esc": "Limpiar búsqueda (Esc)", "txt_sort": "Ordenar", diff --git a/webapp/src/lib/i18n/locales/ru.ts b/webapp/src/lib/i18n/locales/ru.ts index 50d9375..66ddfbf 100644 --- a/webapp/src/lib/i18n/locales/ru.ts +++ b/webapp/src/lib/i18n/locales/ru.ts @@ -758,6 +758,7 @@ const ru: Record = { "txt_search_sends": "Поиск отправляет...", "txt_session_refresh_failed": "Не удалось обновить сеанс. Войдите снова.", "txt_search_your_secure_vault": "Найдите свое безопасное хранилище...", + "txt_search_items_count": "Поиск по {count} элементам...", "txt_clear_search": "Очистить поиск", "txt_clear_search_esc": "Очистить поиск (Esc)", "txt_sort": "Сортировать", diff --git a/webapp/src/lib/i18n/locales/zh-CN.ts b/webapp/src/lib/i18n/locales/zh-CN.ts index 997674a..a388606 100644 --- a/webapp/src/lib/i18n/locales/zh-CN.ts +++ b/webapp/src/lib/i18n/locales/zh-CN.ts @@ -758,6 +758,7 @@ const zhCN: Record = { "txt_search_sends": "搜索 Send...", "txt_session_refresh_failed": "会话刷新失败,请重新登录", "txt_search_your_secure_vault": "搜索你的密码库...", + "txt_search_items_count": "共 {count} 项中搜索...", "txt_clear_search": "清空搜索", "txt_clear_search_esc": "清空搜索(Esc)", "txt_sort": "排序", diff --git a/webapp/src/lib/i18n/locales/zh-TW.ts b/webapp/src/lib/i18n/locales/zh-TW.ts index dbed9e0..230b8a4 100644 --- a/webapp/src/lib/i18n/locales/zh-TW.ts +++ b/webapp/src/lib/i18n/locales/zh-TW.ts @@ -758,6 +758,7 @@ const zhTW: Record = { "txt_search_sends": "搜索 Send...", "txt_session_refresh_failed": "會話刷新失敗,請重新登入", "txt_search_your_secure_vault": "搜索你的密碼庫...", + "txt_search_items_count": "在共 {count} 項中搜索...", "txt_clear_search": "清空搜索", "txt_clear_search_esc": "清空搜索(Esc)", "txt_sort": "排序", diff --git a/webapp/src/styles.css b/webapp/src/styles.css index c817b75..54b6f55 100644 --- a/webapp/src/styles.css +++ b/webapp/src/styles.css @@ -596,7 +596,7 @@ h4 { } .list-head { - margin-bottom: 8px; + margin-bottom: 5px; } .list-count { @@ -1069,6 +1069,7 @@ textarea { .list-panel { padding: 6px; border-radius: var(--radius-md); + margin-top: 5px; } .list-item { diff --git a/webapp/src/styles/dark.css b/webapp/src/styles/dark.css index 9148fcf..b7cf91c 100644 --- a/webapp/src/styles/dark.css +++ b/webapp/src/styles/dark.css @@ -70,6 +70,7 @@ :root[data-theme='dark'] .textarea, :root[data-theme='dark'] select.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 textarea, :root[data-theme='dark'] .dialog select { @@ -79,6 +80,13 @@ 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'] .textarea::placeholder, :root[data-theme='dark'] input::placeholder, diff --git a/webapp/src/styles/forms.css b/webapp/src/styles/forms.css index 3f09d1a..fd9b5c4 100644 --- a/webapp/src/styles/forms.css +++ b/webapp/src/styles/forms.css @@ -31,19 +31,12 @@ } select.input { - @apply py-0 pr-[42px]; + @apply py-0 pr-3.5; line-height: 1.5; - appearance: none; - -webkit-appearance: none; - -moz-appearance: none; - background-image: - 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; + appearance: auto; + -webkit-appearance: auto; + -moz-appearance: auto; + background-image: none; } input[type='file'].input { diff --git a/webapp/src/styles/responsive.css b/webapp/src/styles/responsive.css index 25bcd69..79a0959 100644 --- a/webapp/src/styles/responsive.css +++ b/webapp/src/styles/responsive.css @@ -302,10 +302,15 @@ } .list-head { - @apply grid items-center gap-2; + @apply grid items-center gap-1.5; 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 { grid-column: auto; @apply w-auto whitespace-nowrap text-xs; @@ -316,15 +321,50 @@ } .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 { - @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 { - @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 { @@ -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]; } + .list-count-status { + @apply mb-1 px-1; + } + .mobile-fab-wrap { @apply fixed right-3.5 z-[45]; bottom: calc(14px + var(--mobile-tabbar-height, 70px) + env(safe-area-inset-bottom)); diff --git a/webapp/src/styles/vault.css b/webapp/src/styles/vault.css index 87c5ef9..b8bef70 100644 --- a/webapp/src/styles/vault.css +++ b/webapp/src/styles/vault.css @@ -221,24 +221,110 @@ select.input.duplicate-mode-toolbar-select { @apply h-[34px] w-auto min-w-[156px] max-w-full; } +.duplicate-mode-head-menu { + @apply min-w-0; +} + .duplicate-mode-toolbar-select { @apply w-auto max-w-[170px] shrink-0; } .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; } .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 { - @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 { @@ -262,6 +348,10 @@ select.input.duplicate-mode-toolbar-select { @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 { background: #e9f1ff; border-color: #a9c2ee; @@ -303,6 +393,30 @@ select.input.duplicate-mode-toolbar-select { @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 { @apply min-h-0 overflow-auto p-2; /* Virtualized rows update top/bottom spacers while scrolling. Chrome's scroll anchoring