feat: move filter chips to bottom of note list with gradient fade

Move FilterPills and InboxFilterPills from the top (below header) to
a floating position at the bottom of the note list. When position is
"bottom", pills are absolutely positioned with a gradient background
(transparent → card color) to create a "floating above content"
effect. Pills are centered with gap-2 and wrap to multiple lines.
Matches the ui-design.pen "Filter Pills Bar" frame.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Test
2026-03-31 11:38:13 +02:00
parent 517f1c04f5
commit e43e2a7549
3 changed files with 27 additions and 7 deletions

View File

@@ -103,9 +103,7 @@ function NoteListInner({ entries, selection, selectedNote, noteListFilter, onNot
return (
<div className="flex flex-col select-none overflow-hidden border-r border-border bg-card text-foreground" style={{ height: '100%' }}>
<NoteListHeader title={title} typeDocument={typeDocument} isEntityView={isEntityView} isTrashView={isTrashView} trashCount={searched.length} listSort={listSort} listDirection={listDirection} customProperties={customProperties} sidebarCollapsed={sidebarCollapsed} searchVisible={searchVisible} search={search} onSortChange={handleSortChange} onCreateNote={onCreateNote} onOpenType={onReplaceActiveTab} onToggleSearch={toggleSearch} onSearchChange={setSearch} onEmptyTrash={onEmptyTrash} />
{showFilterPills && <FilterPills active={noteListFilter} counts={filterCounts} onChange={onNoteListFilterChange} />}
{isInboxView && onInboxPeriodChange && <InboxFilterPills active={inboxPeriod} counts={inboxCounts} onChange={onInboxPeriodChange} />}
<div className="flex flex-1 flex-col overflow-hidden outline-none" style={{ minHeight: 0 }} tabIndex={0} onKeyDown={noteListKeyboard.handleKeyDown} onFocus={noteListKeyboard.handleFocus} data-testid="note-list-container">
<div className="relative flex flex-1 flex-col overflow-hidden outline-none" style={{ minHeight: 0 }} tabIndex={0} onKeyDown={noteListKeyboard.handleKeyDown} onFocus={noteListKeyboard.handleFocus} data-testid="note-list-container">
<div className="flex-1 overflow-hidden" style={{ minHeight: 0 }}>
{entitySelection ? (
<EntityView entity={entitySelection.entry} groups={searchedGroups} query={query} collapsedGroups={collapsedGroups} sortPrefs={sortPrefs} onToggleGroup={toggleGroup} onSortChange={handleSortChange} renderItem={renderItem} typeEntryMap={typeEntryMap} onClickNote={handleClickNote} />
@@ -114,6 +112,8 @@ function NoteListInner({ entries, selection, selectedNote, noteListFilter, onNot
)}
</div>
{isChangesView && deletedCount > 0 && <DeletedNotesBanner count={deletedCount} />}
{showFilterPills && <FilterPills active={noteListFilter} counts={filterCounts} onChange={onNoteListFilterChange} position="bottom" />}
{isInboxView && onInboxPeriodChange && <InboxFilterPills active={inboxPeriod} counts={inboxCounts} onChange={onInboxPeriodChange} position="bottom" />}
</div>
{multiSelect.isMultiSelecting && (
<BulkActionBar count={multiSelect.selectedPaths.size} isTrashView={isTrashView} isArchivedView={isArchivedView} onArchive={handleBulkArchive} onTrash={handleBulkTrash} onRestore={handleBulkRestore} onDeletePermanently={handleBulkDeletePermanently} onUnarchive={handleBulkUnarchive} onClear={multiSelect.clear} />

View File

@@ -5,6 +5,7 @@ interface FilterPillsProps {
active: NoteListFilter
counts: Record<NoteListFilter, number>
onChange: (filter: NoteListFilter) => void
position?: 'top' | 'bottom'
}
const PILLS: { value: NoteListFilter; label: string }[] = [
@@ -13,9 +14,18 @@ const PILLS: { value: NoteListFilter; label: string }[] = [
{ value: 'trashed', label: 'Trashed' },
]
function FilterPillsInner({ active, counts, onChange }: FilterPillsProps) {
const BOTTOM_GRADIENT = 'linear-gradient(to bottom, transparent 0%, var(--card, #fff) 30%, var(--card, #fff) 100%)'
function FilterPillsInner({ active, counts, onChange, position = 'top' }: FilterPillsProps) {
const isBottom = position === 'bottom'
return (
<div className="flex h-auto min-h-[45px] shrink-0 flex-wrap items-center gap-1 border-b border-border px-4 py-1.5" data-testid="filter-pills">
<div
className={isBottom
? 'absolute bottom-0 left-0 right-0 z-10 flex flex-wrap items-center justify-center gap-2 px-4 py-3'
: 'flex h-auto min-h-[45px] shrink-0 flex-wrap items-center gap-1 border-b border-border px-4 py-1.5'}
style={isBottom ? { background: BOTTOM_GRADIENT } : undefined}
data-testid="filter-pills"
>
{PILLS.map(({ value, label }) => (
<button
key={value}

View File

@@ -5,6 +5,7 @@ interface InboxFilterPillsProps {
active: InboxPeriod
counts: Record<InboxPeriod, number>
onChange: (period: InboxPeriod) => void
position?: 'top' | 'bottom'
}
const PILLS: { value: InboxPeriod; label: string }[] = [
@@ -14,9 +15,18 @@ const PILLS: { value: InboxPeriod; label: string }[] = [
{ value: 'all', label: 'All' },
]
function InboxFilterPillsInner({ active, counts, onChange }: InboxFilterPillsProps) {
const BOTTOM_GRADIENT = 'linear-gradient(to bottom, transparent 0%, var(--card, #fff) 30%, var(--card, #fff) 100%)'
function InboxFilterPillsInner({ active, counts, onChange, position = 'top' }: InboxFilterPillsProps) {
const isBottom = position === 'bottom'
return (
<div className="flex h-auto min-h-[45px] shrink-0 flex-wrap items-center gap-1 border-b border-border px-4 py-1.5" data-testid="inbox-filter-pills">
<div
className={isBottom
? 'absolute bottom-0 left-0 right-0 z-10 flex flex-wrap items-center justify-center gap-2 px-4 py-3'
: 'flex h-auto min-h-[45px] shrink-0 flex-wrap items-center gap-1 border-b border-border px-4 py-1.5'}
style={isBottom ? { background: BOTTOM_GRADIENT } : undefined}
data-testid="inbox-filter-pills"
>
{PILLS.map(({ value, label }) => (
<button
key={value}