feat(books): enhance filter UI with nested groups and improvements

- fix nested group ALL/ANY toggle visibility and empty-rules group handling
- add FilterSummary component with color-coded field/operator/value display
- add collapsible filter preview in LensView with header toggle
- raise MAX_DEPTH to 5 to match backend validation
- add ruleToParts helper for structured rule rendering
- update filter chip styling: emerald fields, muted operators, primary values
- style ALL/ANY badges with sky-blue and amber respectively
- add segmented sort chip with rose accent and icon
- rebuild @projectx/types package to include tag and collection in RuleField
This commit is contained in:
neon
2026-03-02 16:15:14 -06:00
parent ed6c7428ec
commit ec5dda57b0
20 changed files with 692 additions and 100 deletions
+3
View File
@@ -1,5 +1,8 @@
import type { MetadataProviderKey } from './metadata-fetch';
export const BOOK_FORMATS = ['epub', 'pdf', 'mobi', 'azw3', 'cbz', 'cbr', 'cb7', 'fb2'] as const;
export type BookFormat = (typeof BOOK_FORMATS)[number];
export type BookFileRef = {
id: number;
format: string | null;
+9 -5
View File
@@ -8,6 +8,8 @@ export type RuleField =
| 'pageCount'
| 'author'
| 'genre'
| 'tag'
| 'collection'
| 'format'
| 'addedAt'
| 'fileAvailability'
@@ -44,11 +46,13 @@ export type RuleOperator =
export const FIELD_OPERATORS: Record<RuleField, RuleOperator[]> = {
title: ['contains', 'notContains', 'startsWith', 'endsWith', 'eq', 'notEq', 'isEmpty', 'isNotEmpty'],
publisher: ['contains', 'notContains', 'isEmpty', 'isNotEmpty'],
language: ['eq', 'notEq', 'isEmpty', 'isNotEmpty'],
series: ['contains', 'notContains', 'isEmpty', 'isNotEmpty'],
publisher: ['contains', 'notContains', 'eq', 'notEq', 'includesAny', 'excludesAll', 'isEmpty', 'isNotEmpty'],
language: ['eq', 'notEq', 'includesAny', 'excludesAll', 'isEmpty', 'isNotEmpty'],
series: ['contains', 'notContains', 'eq', 'notEq', 'includesAny', 'excludesAll', 'isEmpty', 'isNotEmpty'],
author: ['includesAny', 'includesAll', 'excludesAll', 'isEmpty', 'isNotEmpty'],
genre: ['includesAny', 'includesAll', 'excludesAll', 'isEmpty', 'isNotEmpty'],
tag: ['includesAny', 'includesAll', 'excludesAll', 'isEmpty', 'isNotEmpty'],
collection: ['includesAny', 'excludesAll', 'isEmpty', 'isNotEmpty'],
format: ['includesAny', 'excludesAll'],
publishedYear: ['eq', 'notEq', 'gt', 'gte', 'lt', 'lte', 'between', 'isEmpty', 'isNotEmpty'],
seriesIndex: ['eq', 'notEq', 'gt', 'gte', 'lt', 'lte', 'between', 'isEmpty', 'isNotEmpty'],
@@ -104,9 +108,9 @@ export type GroupRule = {
rules: (Rule | GroupRule)[]
}
export type SortField = 'author' | 'title' | 'series' | 'seriesIndex' | 'addedAt' | 'publishedYear' | 'pageCount'
export type SortField = 'author' | 'title' | 'series' | 'seriesIndex' | 'addedAt' | 'updatedAt' | 'publishedYear' | 'pageCount'
export const SORT_FIELDS: SortField[] = ['author', 'title', 'series', 'seriesIndex', 'addedAt', 'publishedYear', 'pageCount']
export const SORT_FIELDS: SortField[] = ['author', 'title', 'series', 'seriesIndex', 'addedAt', 'updatedAt', 'publishedYear', 'pageCount']
export type SortSpec = {
field: SortField