mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:21:10 +02:00
feat(todo/web): replace dead filter system with working tag filtering
Remove unused TaskFilters component (priorities, sort, search, completed toggle — none were wired to the board view). Rename PillNav pill from "Filter" to "Tags" and show TagStrip instead. Connect TagStrip tag selection to BoardViewRenderer via shared context so selecting tags actually filters displayed tasks. Clean up viewStore by removing dead filter state (filterPriorities, filterLabelIds, filterSearchQuery). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c6ed652b32
commit
ce3ed10b60
8 changed files with 71 additions and 1026 deletions
|
|
@ -1,37 +1,31 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
import { viewStore } from '$lib/stores/view.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { DotsThree, Plus, X } from '@manacore/shared-icons';
|
||||
import TagStripModal from './TagStripModal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
|
||||
interface Props {
|
||||
/** Whether the filter strip below is visible (affects vertical position) */
|
||||
filterStripVisible?: boolean;
|
||||
}
|
||||
|
||||
let { filterStripVisible = false }: Props = $props();
|
||||
const activeTagFilter: { readonly ids: string[]; set(ids: string[]): void } =
|
||||
getContext('activeTagFilter');
|
||||
|
||||
let showModal = $state(false);
|
||||
|
||||
function handleTagClick(tagId: string) {
|
||||
const current = viewStore.filterLabelIds;
|
||||
const current = activeTagFilter.ids;
|
||||
if (current.includes(tagId)) {
|
||||
viewStore.setFilterLabelIds(current.filter((id) => id !== tagId));
|
||||
activeTagFilter.set(current.filter((id) => id !== tagId));
|
||||
} else {
|
||||
viewStore.setFilterLabelIds([...current, tagId]);
|
||||
activeTagFilter.set([...current, tagId]);
|
||||
}
|
||||
}
|
||||
|
||||
function isTagSelected(tagId: string): boolean {
|
||||
return viewStore.filterLabelIds.includes(tagId);
|
||||
return activeTagFilter.ids.includes(tagId);
|
||||
}
|
||||
|
||||
const hasSelectedTags = $derived(viewStore.filterLabelIds.length > 0);
|
||||
const hasSelectedTags = $derived(activeTagFilter.ids.length > 0);
|
||||
|
||||
function handleOpenModal() {
|
||||
showModal = true;
|
||||
|
|
@ -48,13 +42,13 @@
|
|||
const hasTags = $derived(tagsCtx.value.length > 0);
|
||||
</script>
|
||||
|
||||
<div class="tag-strip-wrapper" class:above-filter-strip={filterStripVisible}>
|
||||
<div class="tag-strip-wrapper">
|
||||
<div class="tag-strip-container">
|
||||
<!-- Clear Filter Button (always rendered to prevent layout shift) -->
|
||||
<button
|
||||
class="clear-filter-pill glass-tag"
|
||||
class:hidden={!hasSelectedTags}
|
||||
onclick={() => viewStore.setFilterLabelIds([])}
|
||||
onclick={() => activeTagFilter.set([])}
|
||||
title={$t('filters.clearFilter')}
|
||||
disabled={!hasSelectedTags}
|
||||
>
|
||||
|
|
@ -117,11 +111,6 @@
|
|||
transition: bottom 0.2s ease;
|
||||
}
|
||||
|
||||
/* When filter strip is also visible, stack above it */
|
||||
.tag-strip-wrapper.above-filter-strip {
|
||||
bottom: calc(110px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.tag-strip-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -1,630 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import type { TaskPriority } from '@todo/shared';
|
||||
import { getContext } from 'svelte';
|
||||
import type { Tag } from '@manacore/shared-tags';
|
||||
|
||||
const tagsCtx: { readonly value: Tag[] } = getContext('tags');
|
||||
import type { SortBy, SortOrder } from '$lib/stores/view.svelte';
|
||||
import { CaretDown, Check, CheckCircle, MagnifyingGlass, X } from '@manacore/shared-icons';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
// Layout
|
||||
variant: 'strip' | 'bar';
|
||||
|
||||
// Filter state (owned by parent)
|
||||
selectedPriorities: TaskPriority[];
|
||||
selectedLabelIds: string[];
|
||||
searchQuery: string;
|
||||
|
||||
// Callbacks
|
||||
onPrioritiesChange: (priorities: TaskPriority[]) => void;
|
||||
onLabelsChange: (labelIds: string[]) => void;
|
||||
onSearchChange: (query: string) => void;
|
||||
onClearFilters: () => void;
|
||||
|
||||
// Sort (strip variant)
|
||||
sortBy?: SortBy;
|
||||
sortOrder?: SortOrder;
|
||||
onSortChange?: (sortBy: SortBy) => void;
|
||||
|
||||
// Feature toggles
|
||||
showSort?: boolean;
|
||||
showSearch?: boolean;
|
||||
showLabels?: boolean;
|
||||
showCompleted?: boolean;
|
||||
showTags?: boolean;
|
||||
|
||||
// Completed toggle
|
||||
isCompletedVisible?: boolean;
|
||||
onToggleCompleted?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
variant,
|
||||
selectedPriorities,
|
||||
selectedLabelIds,
|
||||
searchQuery,
|
||||
onPrioritiesChange,
|
||||
onLabelsChange,
|
||||
onSearchChange,
|
||||
onClearFilters,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
onSortChange,
|
||||
showSort = false,
|
||||
showSearch = false,
|
||||
showLabels = false,
|
||||
showCompleted = false,
|
||||
showTags = false,
|
||||
isCompletedVisible = false,
|
||||
onToggleCompleted,
|
||||
}: Props = $props();
|
||||
|
||||
let priorities = $derived([
|
||||
{
|
||||
value: 'urgent' as TaskPriority,
|
||||
label: $t('priority.urgent'),
|
||||
color: '#ef4444',
|
||||
bgColor: 'bg-red-500',
|
||||
},
|
||||
{
|
||||
value: 'high' as TaskPriority,
|
||||
label: $t('priority.high'),
|
||||
color: '#f97316',
|
||||
bgColor: 'bg-orange-500',
|
||||
},
|
||||
{
|
||||
value: 'medium' as TaskPriority,
|
||||
label: $t('priority.medium'),
|
||||
color: '#eab308',
|
||||
bgColor: 'bg-yellow-500',
|
||||
},
|
||||
{
|
||||
value: 'low' as TaskPriority,
|
||||
label: $t('priority.low'),
|
||||
color: '#3b82f6',
|
||||
bgColor: 'bg-blue-500',
|
||||
},
|
||||
]);
|
||||
|
||||
let sortOptions = $derived([
|
||||
{ id: 'dueDate' as SortBy, label: $t('filters.date') },
|
||||
{ id: 'priority' as SortBy, label: $t('filters.priorityShort') },
|
||||
{ id: 'title' as SortBy, label: $t('filters.name') },
|
||||
]);
|
||||
|
||||
// Dropdown states
|
||||
let showLabelsDropdown = $state(false);
|
||||
|
||||
let hasActiveFilters = $derived(
|
||||
selectedPriorities.length > 0 || selectedLabelIds.length > 0 || searchQuery.trim() !== ''
|
||||
);
|
||||
|
||||
function togglePriority(priority: TaskPriority) {
|
||||
if (selectedPriorities.includes(priority)) {
|
||||
onPrioritiesChange(selectedPriorities.filter((p) => p !== priority));
|
||||
} else {
|
||||
onPrioritiesChange([...selectedPriorities, priority]);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleLabel(labelId: string) {
|
||||
if (selectedLabelIds.includes(labelId)) {
|
||||
onLabelsChange(selectedLabelIds.filter((id) => id !== labelId));
|
||||
} else {
|
||||
onLabelsChange([...selectedLabelIds, labelId]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if variant === 'strip'}
|
||||
<!-- ==================== STRIP VARIANT ==================== -->
|
||||
<div class="filter-strip-wrapper">
|
||||
<div class="filter-strip-container">
|
||||
<!-- Clear Filter Button -->
|
||||
<button
|
||||
class="clear-filter-pill glass-pill"
|
||||
class:hidden={!hasActiveFilters}
|
||||
onclick={onClearFilters}
|
||||
title={$t('filters.clearFilter')}
|
||||
disabled={!hasActiveFilters}
|
||||
>
|
||||
<X size={16} weight="bold" />
|
||||
<span class="pill-label">Filter</span>
|
||||
</button>
|
||||
|
||||
<!-- Tag Chips -->
|
||||
{#if showTags}
|
||||
<button
|
||||
class="label-pill glass-pill"
|
||||
onclick={() => goto('/tags')}
|
||||
title={$t('filters.manageTags')}
|
||||
>
|
||||
<span class="pill-label label-text">Tags:</span>
|
||||
</button>
|
||||
{#if tagsCtx.value.length > 0}
|
||||
{#each tagsCtx.value as tag (tag.id)}
|
||||
<button
|
||||
class="tag-chip glass-pill"
|
||||
class:selected={selectedLabelIds.includes(tag.id)}
|
||||
onclick={() => toggleLabel(tag.id)}
|
||||
title={tag.name}
|
||||
style="--tag-color: {tag.color || '#6b7280'}"
|
||||
>
|
||||
<span class="tag-dot"></span>
|
||||
<span class="pill-label">{tag.name}</span>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
<span class="strip-divider"></span>
|
||||
{/if}
|
||||
|
||||
<!-- Filter Label -->
|
||||
<span class="label-pill glass-pill" role="presentation">
|
||||
<span class="pill-label label-text">Filter:</span>
|
||||
</span>
|
||||
|
||||
<!-- Priority Filter Pills -->
|
||||
{#each priorities as priority (priority.value)}
|
||||
<button
|
||||
class="priority-pill glass-pill"
|
||||
class:selected={selectedPriorities.includes(priority.value)}
|
||||
onclick={() => togglePriority(priority.value)}
|
||||
title={priority.label}
|
||||
style="--priority-color: {priority.color}"
|
||||
>
|
||||
<span class="priority-dot"></span>
|
||||
<span class="pill-label">{priority.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
<!-- Sort Pills -->
|
||||
{#if showSort && onSortChange}
|
||||
<span class="strip-divider"></span>
|
||||
{#each sortOptions as option (option.id)}
|
||||
<button
|
||||
class="sort-pill glass-pill"
|
||||
class:active={sortBy === option.id}
|
||||
onclick={() => onSortChange(option.id)}
|
||||
title={$t('tags.sortBy', { values: { field: option.label } })}
|
||||
>
|
||||
<span class="pill-label">{option.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<!-- Show Completed Toggle -->
|
||||
{#if showCompleted && onToggleCompleted}
|
||||
<button
|
||||
class="glass-pill"
|
||||
class:active={isCompletedVisible}
|
||||
onclick={onToggleCompleted}
|
||||
title={isCompletedVisible ? $t('filters.hideCompleted') : $t('filters.showCompleted')}
|
||||
>
|
||||
<CheckCircle size={20} class="pill-icon" />
|
||||
<span class="pill-label">{$t('nav.completed')}</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- ==================== BAR VARIANT ==================== -->
|
||||
<div class="kanban-filters glass-pill-bar rounded-2xl p-4">
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- Row 1: Search and Clear -->
|
||||
{#if showSearch}
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative flex-1 max-w-xs">
|
||||
<MagnifyingGlass
|
||||
size={16}
|
||||
class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
oninput={(e) => onSearchChange(e.currentTarget.value)}
|
||||
placeholder={$t('filters.searchTasks')}
|
||||
class="w-full pl-10 pr-8 py-2 text-sm bg-background border border-border rounded-lg outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary placeholder:text-muted-foreground transition-all"
|
||||
/>
|
||||
{#if searchQuery}
|
||||
<button
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-muted-foreground hover:text-foreground rounded-full hover:bg-muted transition-colors"
|
||||
onclick={() => onSearchChange('')}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if hasActiveFilters}
|
||||
<button
|
||||
class="ml-auto px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors flex items-center gap-2"
|
||||
onclick={onClearFilters}
|
||||
>
|
||||
<X size={16} />
|
||||
{$t('filters.resetFilters')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Row 2: Filter Pills -->
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<!-- Priority filters -->
|
||||
<div class="filter-group flex items-center gap-2">
|
||||
<span class="text-xs font-medium text-muted-foreground uppercase tracking-wide"
|
||||
>{$t('task.priority')}</span
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
{#each priorities as priority}
|
||||
<button
|
||||
class="filter-pill px-3 py-1.5 text-xs font-medium rounded-full transition-all border {selectedPriorities.includes(
|
||||
priority.value
|
||||
)
|
||||
? `${priority.bgColor} text-white border-transparent shadow-sm`
|
||||
: 'bg-background border-border text-foreground hover:border-primary/50 hover:bg-muted/50'}"
|
||||
onclick={() => togglePriority(priority.value)}
|
||||
>
|
||||
{priority.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Labels filter -->
|
||||
{#if showLabels}
|
||||
<div class="h-6 w-px bg-border hidden sm:block"></div>
|
||||
|
||||
<div class="filter-group flex items-center gap-2 relative">
|
||||
<span class="text-xs font-medium text-muted-foreground uppercase tracking-wide"
|
||||
>Tags</span
|
||||
>
|
||||
<button
|
||||
class="flex items-center gap-2 px-3 py-1.5 text-sm bg-background border border-border rounded-lg hover:border-primary/50 hover:bg-muted/50 transition-all"
|
||||
onclick={() => (showLabelsDropdown = !showLabelsDropdown)}
|
||||
>
|
||||
{#if selectedLabelIds.length > 0}
|
||||
<div class="flex items-center gap-1">
|
||||
{#each selectedLabelIds.slice(0, 3) as labelId}
|
||||
{@const label = tagsCtx.value.find((l) => l.id === labelId)}
|
||||
{#if label}
|
||||
<div
|
||||
class="w-3 h-3 rounded-full ring-2 ring-background"
|
||||
style="background-color: {label.color}"
|
||||
></div>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if selectedLabelIds.length > 3}
|
||||
<span class="text-xs text-muted-foreground">+{selectedLabelIds.length - 3}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-muted-foreground">{$t('filters.select')}</span>
|
||||
{/if}
|
||||
<CaretDown
|
||||
size={16}
|
||||
class="text-muted-foreground transition-transform {showLabelsDropdown
|
||||
? 'rotate-180'
|
||||
: ''}"
|
||||
/>
|
||||
</button>
|
||||
|
||||
{#if showLabelsDropdown}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||
<div class="fixed inset-0 z-40" onclick={() => (showLabelsDropdown = false)}></div>
|
||||
<div
|
||||
class="absolute top-full left-0 mt-2 z-50 min-w-[220px] bg-popover border border-border rounded-xl shadow-lg p-2 animate-in fade-in slide-in-from-top-2 duration-150"
|
||||
>
|
||||
{#if tagsCtx.value.length === 0}
|
||||
<p class="text-sm text-muted-foreground p-3 text-center">
|
||||
{$t('filters.noTagsAvailable')}
|
||||
</p>
|
||||
{:else}
|
||||
<div class="max-h-[200px] overflow-y-auto">
|
||||
{#each tagsCtx.value as label}
|
||||
<button
|
||||
class="w-full flex items-center gap-3 px-3 py-2 text-sm rounded-lg hover:bg-muted/50 transition-colors"
|
||||
onclick={() => toggleLabel(label.id)}
|
||||
>
|
||||
<div
|
||||
class="w-4 h-4 rounded-full flex-shrink-0 ring-2 ring-offset-2 ring-offset-popover transition-all {selectedLabelIds.includes(
|
||||
label.id
|
||||
)
|
||||
? 'ring-primary'
|
||||
: 'ring-transparent'}"
|
||||
style="background-color: {label.color}"
|
||||
></div>
|
||||
<span class="flex-1 text-left truncate">{label.name}</span>
|
||||
{#if selectedLabelIds.includes(label.id)}
|
||||
<Check size={16} class="text-primary" />
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
/* ==================== STRIP VARIANT STYLES ==================== */
|
||||
.filter-strip-wrapper {
|
||||
position: fixed;
|
||||
bottom: calc(70px + env(safe-area-inset-bottom, 0px));
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 49;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
pointer-events: none;
|
||||
transition: bottom 0.2s ease;
|
||||
}
|
||||
|
||||
.filter-strip-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
background: transparent;
|
||||
pointer-events: auto;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 0.5rem 2rem;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.filter-strip-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Glass pill styling */
|
||||
.glass-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 9999px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
:global(.dark) .glass-pill {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.glass-pill:hover {
|
||||
transform: scale(1.05);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-color: rgba(0, 0, 0, 0.15);
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
:global(.dark) .glass-pill:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.glass-pill:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.glass-pill.active {
|
||||
background: rgba(139, 92, 246, 0.15);
|
||||
border-color: rgba(139, 92, 246, 0.3);
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
:global(.dark) .glass-pill.active {
|
||||
background: rgba(139, 92, 246, 0.25);
|
||||
border-color: rgba(139, 92, 246, 0.4);
|
||||
}
|
||||
|
||||
.pill-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pill-label {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:global(.dark) .pill-label {
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
.glass-pill.active .pill-label {
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
/* Label pill (section header) */
|
||||
.label-pill {
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.label-pill .label-text {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
:global(.dark) .label-pill .label-text {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.label-pill:hover .label-text {
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
/* Tag chips */
|
||||
.tag-chip .tag-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--tag-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tag-chip.selected {
|
||||
background: var(--tag-color) !important;
|
||||
border-color: var(--tag-color) !important;
|
||||
}
|
||||
|
||||
.tag-chip.selected .tag-dot {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.tag-chip.selected .pill-label {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.strip-divider {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:global(.dark) .strip-divider {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
/* Priority pills */
|
||||
.priority-pill.selected {
|
||||
background: var(--priority-color) !important;
|
||||
border-color: var(--priority-color) !important;
|
||||
}
|
||||
|
||||
.priority-pill.selected .priority-dot {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.priority-pill.selected .pill-label {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.priority-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--priority-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Clear filter pill */
|
||||
.clear-filter-pill {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.1) !important;
|
||||
border-color: rgba(239, 68, 68, 0.3) !important;
|
||||
}
|
||||
|
||||
.clear-filter-pill .pill-label {
|
||||
color: #ef4444;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:global(.dark) .clear-filter-pill {
|
||||
color: #f87171;
|
||||
background: rgba(239, 68, 68, 0.15) !important;
|
||||
border-color: rgba(239, 68, 68, 0.3) !important;
|
||||
}
|
||||
|
||||
:global(.dark) .clear-filter-pill .pill-label {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.clear-filter-pill:hover:not(.hidden) {
|
||||
background: rgba(239, 68, 68, 0.2) !important;
|
||||
border-color: rgba(239, 68, 68, 0.5) !important;
|
||||
}
|
||||
|
||||
.clear-filter-pill.hidden {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Responsive strip */
|
||||
@media (max-width: 640px) {
|
||||
.filter-strip-wrapper {
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.filter-strip-container {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.pill-label {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== BAR VARIANT STYLES ==================== */
|
||||
.glass-pill-bar {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
:global(.dark) .glass-pill-bar {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
/* Animation utilities */
|
||||
.animate-in {
|
||||
animation: animateIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
--tw-enter-opacity: 0;
|
||||
}
|
||||
|
||||
.slide-in-from-top-2 {
|
||||
--tw-enter-translate-y: -0.5rem;
|
||||
}
|
||||
|
||||
@keyframes animateIn {
|
||||
from {
|
||||
opacity: var(--tw-enter-opacity, 1);
|
||||
transform: translateY(var(--tw-enter-translate-y, 0));
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { getContext, type Snippet } from 'svelte';
|
||||
import { getContext, hasContext, type Snippet } from 'svelte';
|
||||
import type { Task } from '@todo/shared';
|
||||
import type { LocalBoardView } from '$lib/data/local-store';
|
||||
import { groupTasksByView, getDropActionUpdate } from '$lib/data/view-grouping';
|
||||
|
|
@ -37,8 +37,19 @@
|
|||
// Get tasks from context (set by layout)
|
||||
const tasksCtx: { readonly value: Task[] } = getContext('tasks');
|
||||
|
||||
// Group tasks by the current view configuration
|
||||
let columns = $derived(groupTasksByView(view, tasksCtx.value));
|
||||
// Active tag filter (set by TagStrip via layout context)
|
||||
const activeTagFilter: { readonly ids: string[] } | null = hasContext('activeTagFilter')
|
||||
? getContext('activeTagFilter')
|
||||
: null;
|
||||
|
||||
// Filter tasks by selected tags, then group by view configuration
|
||||
let filteredTasks = $derived.by(() => {
|
||||
const tagIds = activeTagFilter?.ids ?? [];
|
||||
if (tagIds.length === 0) return tasksCtx.value;
|
||||
return tasksCtx.value.filter((t) => t.labels?.some((l) => tagIds.includes(l.id)));
|
||||
});
|
||||
|
||||
let columns = $derived(groupTasksByView(view, filteredTasks));
|
||||
|
||||
// ─── Task Callbacks ──────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
* View Store - Manages current view state using Svelte 5 runes
|
||||
*/
|
||||
|
||||
import type { TaskPriority } from '@todo/shared';
|
||||
import { TodoEvents } from '@manacore/shared-utils/analytics';
|
||||
|
||||
export type ViewType = 'inbox' | 'today' | 'upcoming' | 'label' | 'completed' | 'search';
|
||||
|
|
@ -17,11 +16,6 @@ let sortBy = $state<SortBy>('order');
|
|||
let sortOrder = $state<SortOrder>('asc');
|
||||
let showCompleted = $state(false);
|
||||
|
||||
// Filter state (used by TaskFilters strip in list view)
|
||||
let filterPriorities = $state<TaskPriority[]>([]);
|
||||
let filterLabelIds = $state<string[]>([]);
|
||||
let filterSearchQuery = $state('');
|
||||
|
||||
export const viewStore = {
|
||||
// Getters
|
||||
get currentView() {
|
||||
|
|
@ -42,15 +36,6 @@ export const viewStore = {
|
|||
get showCompleted() {
|
||||
return showCompleted;
|
||||
},
|
||||
get filterPriorities() {
|
||||
return filterPriorities;
|
||||
},
|
||||
get filterLabelIds() {
|
||||
return filterLabelIds;
|
||||
},
|
||||
get filterSearchQuery() {
|
||||
return filterSearchQuery;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set current view to inbox
|
||||
|
|
@ -141,39 +126,6 @@ export const viewStore = {
|
|||
showCompleted = !showCompleted;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set filter priorities
|
||||
*/
|
||||
setFilterPriorities(priorities: TaskPriority[]) {
|
||||
filterPriorities = priorities;
|
||||
if (priorities.length > 0) TodoEvents.filterUsed('priority');
|
||||
},
|
||||
|
||||
/**
|
||||
* Set filter label IDs
|
||||
*/
|
||||
setFilterLabelIds(ids: string[]) {
|
||||
filterLabelIds = ids;
|
||||
if (ids.length > 0) TodoEvents.filterUsed('label');
|
||||
},
|
||||
|
||||
/**
|
||||
* Set filter search query
|
||||
*/
|
||||
setFilterSearchQuery(query: string) {
|
||||
filterSearchQuery = query;
|
||||
if (query.trim()) TodoEvents.filterUsed('search');
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
clearFilters() {
|
||||
filterPriorities = [];
|
||||
filterLabelIds = [];
|
||||
filterSearchQuery = '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset to default state
|
||||
*/
|
||||
|
|
@ -184,8 +136,5 @@ export const viewStore = {
|
|||
sortBy = 'order';
|
||||
sortOrder = 'asc';
|
||||
showCompleted = false;
|
||||
filterPriorities = [];
|
||||
filterLabelIds = [];
|
||||
filterSearchQuery = '';
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,93 +1,45 @@
|
|||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { viewStore } from './view.svelte';
|
||||
|
||||
describe('viewStore filter methods', () => {
|
||||
describe('viewStore', () => {
|
||||
beforeEach(() => {
|
||||
viewStore.reset();
|
||||
});
|
||||
|
||||
// Filter state defaults
|
||||
describe('initial state', () => {
|
||||
it('has empty filter priorities', () => {
|
||||
expect(viewStore.filterPriorities).toEqual([]);
|
||||
});
|
||||
|
||||
it('has empty filter labels', () => {
|
||||
expect(viewStore.filterLabelIds).toEqual([]);
|
||||
});
|
||||
|
||||
it('has empty filter search query', () => {
|
||||
expect(viewStore.filterSearchQuery).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
// Setters
|
||||
describe('setFilterPriorities', () => {
|
||||
it('sets priorities', () => {
|
||||
viewStore.setFilterPriorities(['high', 'urgent']);
|
||||
expect(viewStore.filterPriorities).toEqual(['high', 'urgent']);
|
||||
});
|
||||
|
||||
it('can set to empty array', () => {
|
||||
viewStore.setFilterPriorities(['high']);
|
||||
viewStore.setFilterPriorities([]);
|
||||
expect(viewStore.filterPriorities).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setFilterLabelIds', () => {
|
||||
it('sets label IDs', () => {
|
||||
viewStore.setFilterLabelIds(['label-1', 'label-2']);
|
||||
expect(viewStore.filterLabelIds).toEqual(['label-1', 'label-2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setFilterSearchQuery', () => {
|
||||
it('sets search query', () => {
|
||||
viewStore.setFilterSearchQuery('hello');
|
||||
expect(viewStore.filterSearchQuery).toBe('hello');
|
||||
});
|
||||
});
|
||||
|
||||
// clearFilters
|
||||
describe('clearFilters', () => {
|
||||
it('resets all filter state', () => {
|
||||
viewStore.setFilterPriorities(['urgent']);
|
||||
viewStore.setFilterLabelIds(['label-1']);
|
||||
viewStore.setFilterSearchQuery('test');
|
||||
|
||||
viewStore.clearFilters();
|
||||
|
||||
expect(viewStore.filterPriorities).toEqual([]);
|
||||
expect(viewStore.filterLabelIds).toEqual([]);
|
||||
expect(viewStore.filterSearchQuery).toBe('');
|
||||
});
|
||||
|
||||
it('does not affect non-filter state', () => {
|
||||
viewStore.setSort('priority', 'desc');
|
||||
viewStore.toggleShowCompleted();
|
||||
viewStore.setFilterPriorities(['high']);
|
||||
|
||||
viewStore.clearFilters();
|
||||
|
||||
expect(viewStore.sortBy).toBe('priority');
|
||||
expect(viewStore.sortOrder).toBe('desc');
|
||||
expect(viewStore.showCompleted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// reset
|
||||
describe('reset', () => {
|
||||
it('resets filter state along with everything else', () => {
|
||||
viewStore.setFilterPriorities(['urgent']);
|
||||
it('resets all state to defaults', () => {
|
||||
viewStore.setSort('title', 'desc');
|
||||
viewStore.toggleShowCompleted();
|
||||
|
||||
viewStore.reset();
|
||||
|
||||
expect(viewStore.filterPriorities).toEqual([]);
|
||||
expect(viewStore.sortBy).toBe('order');
|
||||
expect(viewStore.sortOrder).toBe('asc');
|
||||
expect(viewStore.showCompleted).toBe(false);
|
||||
expect(viewStore.currentView).toBe('inbox');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sort', () => {
|
||||
it('sets sort options', () => {
|
||||
viewStore.setSort('priority', 'desc');
|
||||
expect(viewStore.sortBy).toBe('priority');
|
||||
expect(viewStore.sortOrder).toBe('desc');
|
||||
});
|
||||
|
||||
it('toggles sort order', () => {
|
||||
viewStore.toggleSortOrder();
|
||||
expect(viewStore.sortOrder).toBe('desc');
|
||||
viewStore.toggleSortOrder();
|
||||
expect(viewStore.sortOrder).toBe('asc');
|
||||
});
|
||||
});
|
||||
|
||||
describe('showCompleted', () => {
|
||||
it('toggles show completed', () => {
|
||||
expect(viewStore.showCompleted).toBe(false);
|
||||
viewStore.toggleShowCompleted();
|
||||
expect(viewStore.showCompleted).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,188 +0,0 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import type { Task, Label } from '@todo/shared';
|
||||
import { applyTaskFilters, type TaskFilterCriteria } from './task-filters';
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
function makeLabel(overrides: Partial<Label>): Label {
|
||||
return {
|
||||
id: 'l',
|
||||
userId: 'user-1',
|
||||
name: 'Label',
|
||||
color: '#000',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// Helper to create a minimal task for testing
|
||||
function makeTask(overrides: Partial<Task> = {}): Task {
|
||||
return {
|
||||
id: 'task-1',
|
||||
userId: 'user-1',
|
||||
title: 'Test Task',
|
||||
priority: 'medium',
|
||||
status: 'pending',
|
||||
isCompleted: false,
|
||||
order: 0,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const emptyFilters: TaskFilterCriteria = {
|
||||
priorities: [],
|
||||
labelIds: [],
|
||||
searchQuery: '',
|
||||
};
|
||||
|
||||
describe('applyTaskFilters', () => {
|
||||
const tasks: Task[] = [
|
||||
makeTask({ id: '1', title: 'Buy groceries', priority: 'low' }),
|
||||
makeTask({
|
||||
id: '2',
|
||||
title: 'Urgent meeting',
|
||||
priority: 'urgent',
|
||||
labels: [makeLabel({ id: 'label-1', name: 'Work', color: '#f00' })],
|
||||
}),
|
||||
makeTask({
|
||||
id: '3',
|
||||
title: 'Write report',
|
||||
priority: 'high',
|
||||
description: 'Quarterly financial report',
|
||||
labels: [
|
||||
makeLabel({ id: 'label-1', name: 'Work', color: '#f00' }),
|
||||
makeLabel({ id: 'label-2', name: 'Important', color: '#0f0' }),
|
||||
],
|
||||
}),
|
||||
makeTask({ id: '4', title: 'Relax', priority: 'low' }),
|
||||
];
|
||||
|
||||
it('returns all tasks when no filters are active', () => {
|
||||
const result = applyTaskFilters(tasks, emptyFilters);
|
||||
expect(result).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('returns empty array for empty input', () => {
|
||||
const result = applyTaskFilters([], emptyFilters);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
// Priority filtering
|
||||
describe('priority filter', () => {
|
||||
it('filters by single priority', () => {
|
||||
const result = applyTaskFilters(tasks, { ...emptyFilters, priorities: ['urgent'] });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('2');
|
||||
});
|
||||
|
||||
it('filters by multiple priorities', () => {
|
||||
const result = applyTaskFilters(tasks, {
|
||||
...emptyFilters,
|
||||
priorities: ['low', 'high'],
|
||||
});
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result.map((t) => t.id).sort()).toEqual(['1', '3', '4']);
|
||||
});
|
||||
|
||||
it('returns nothing when priority matches no tasks', () => {
|
||||
const result = applyTaskFilters(tasks, { ...emptyFilters, priorities: ['medium'] });
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// Label filtering
|
||||
describe('label filter', () => {
|
||||
it('filters by single label', () => {
|
||||
const result = applyTaskFilters(tasks, { ...emptyFilters, labelIds: ['label-1'] });
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((t) => t.id).sort()).toEqual(['2', '3']);
|
||||
});
|
||||
|
||||
it('filters by label that only one task has', () => {
|
||||
const result = applyTaskFilters(tasks, { ...emptyFilters, labelIds: ['label-2'] });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('3');
|
||||
});
|
||||
|
||||
it('matches tasks having any of the filter labels (OR logic)', () => {
|
||||
const result = applyTaskFilters(tasks, {
|
||||
...emptyFilters,
|
||||
labelIds: ['label-1', 'label-2'],
|
||||
});
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('excludes tasks with no labels', () => {
|
||||
const result = applyTaskFilters(tasks, { ...emptyFilters, labelIds: ['label-1'] });
|
||||
expect(result.find((t) => t.id === '1')).toBeUndefined();
|
||||
expect(result.find((t) => t.id === '4')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// Search query filtering
|
||||
describe('search query filter', () => {
|
||||
it('filters by title match (case insensitive)', () => {
|
||||
const result = applyTaskFilters(tasks, { ...emptyFilters, searchQuery: 'urgent' });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('2');
|
||||
});
|
||||
|
||||
it('filters by description match', () => {
|
||||
const result = applyTaskFilters(tasks, { ...emptyFilters, searchQuery: 'quarterly' });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('3');
|
||||
});
|
||||
|
||||
it('is case insensitive', () => {
|
||||
const result = applyTaskFilters(tasks, { ...emptyFilters, searchQuery: 'BUY' });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('1');
|
||||
});
|
||||
|
||||
it('ignores whitespace-only query', () => {
|
||||
const result = applyTaskFilters(tasks, { ...emptyFilters, searchQuery: ' ' });
|
||||
expect(result).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('matches partial strings', () => {
|
||||
const result = applyTaskFilters(tasks, { ...emptyFilters, searchQuery: 'rep' });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('3');
|
||||
});
|
||||
});
|
||||
|
||||
// Combined filters
|
||||
describe('combined filters', () => {
|
||||
it('applies priority + label filter together', () => {
|
||||
const result = applyTaskFilters(tasks, {
|
||||
...emptyFilters,
|
||||
priorities: ['high', 'urgent'],
|
||||
labelIds: ['label-1'],
|
||||
});
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((t) => t.id).sort()).toEqual(['2', '3']);
|
||||
});
|
||||
|
||||
it('applies all filters together', () => {
|
||||
const result = applyTaskFilters(tasks, {
|
||||
priorities: ['high'],
|
||||
labelIds: ['label-1'],
|
||||
searchQuery: 'report',
|
||||
});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('3');
|
||||
});
|
||||
|
||||
it('returns empty when combined filters contradict', () => {
|
||||
const result = applyTaskFilters(tasks, {
|
||||
priorities: ['low'],
|
||||
labelIds: ['label-1'], // tasks 1,4 are low but have no label-1
|
||||
searchQuery: '',
|
||||
});
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import type { Task, TaskPriority } from '@todo/shared';
|
||||
|
||||
export interface TaskFilterCriteria {
|
||||
priorities: TaskPriority[];
|
||||
labelIds: string[];
|
||||
searchQuery: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filter criteria to a list of tasks.
|
||||
* Pure function — no store dependency — easy to test.
|
||||
*/
|
||||
export function applyTaskFilters(tasks: Task[], filters: TaskFilterCriteria): Task[] {
|
||||
let filtered = tasks;
|
||||
|
||||
if (filters.priorities.length > 0) {
|
||||
filtered = filtered.filter((t) => filters.priorities.includes(t.priority));
|
||||
}
|
||||
|
||||
if (filters.labelIds.length > 0) {
|
||||
filtered = filtered.filter((t) => t.labels?.some((l) => filters.labelIds.includes(l.id)));
|
||||
}
|
||||
|
||||
if (filters.searchQuery.trim()) {
|
||||
const q = filters.searchQuery.toLowerCase();
|
||||
filtered = filtered.filter(
|
||||
(t) => t.title.toLowerCase().includes(q) || t.description?.toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
|
@ -28,9 +28,7 @@
|
|||
} from '@manacore/shared-stores';
|
||||
import { linkLocalStore, linkMutations } from '@manacore/shared-links';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
import TaskFilters from '$lib/components/TaskFilters.svelte';
|
||||
import { viewStore, type SortBy } from '$lib/stores/view.svelte';
|
||||
import type { TaskPriority } from '@todo/shared';
|
||||
import TagStrip from '$lib/components/TagStrip.svelte';
|
||||
import {
|
||||
THEME_DEFINITIONS,
|
||||
DEFAULT_THEME_VARIANTS,
|
||||
|
|
@ -67,9 +65,21 @@
|
|||
// Use first board view as the single active view
|
||||
let activeView = $derived(boardViews.value[0] ?? null);
|
||||
|
||||
// ─── Active Tag Filter (shared between TagStrip + BoardViewRenderer) ───
|
||||
let activeTagFilterIds = $state<string[]>([]);
|
||||
const activeTagFilter = {
|
||||
get ids() {
|
||||
return activeTagFilterIds;
|
||||
},
|
||||
set(ids: string[]) {
|
||||
activeTagFilterIds = ids;
|
||||
},
|
||||
};
|
||||
|
||||
// Provide data to child components via Svelte context
|
||||
setContext('tasks', allTasks);
|
||||
setContext('tags', allTags);
|
||||
setContext('activeTagFilter', activeTagFilter);
|
||||
setContext('activeView', {
|
||||
get value() {
|
||||
return activeView;
|
||||
|
|
@ -224,21 +234,21 @@
|
|||
// User email for user dropdown — empty string for guests so PillNav shows login button
|
||||
let userEmail = $derived(authStore.isAuthenticated ? authStore.user?.email || 'Menü' : '');
|
||||
|
||||
// Toggle FilterStrip visibility
|
||||
function handleFilterToggle() {
|
||||
// Toggle TagStrip visibility
|
||||
function handleTagStripToggle() {
|
||||
todoSettings.toggleFilterStrip();
|
||||
}
|
||||
|
||||
// Keep navRoutes for keyboard shortcuts (Ctrl+1-3)
|
||||
const viewRoutes: Record<string, string> = { fokus: '/' };
|
||||
|
||||
// Filter, Tags, and Layout stay as standalone pills (toggle behavior, not navigation)
|
||||
// Tags and Layout stay as standalone pills (toggle behavior, not navigation)
|
||||
let baseNavItems = $derived<PillNavItem[]>([
|
||||
{
|
||||
href: '/',
|
||||
label: 'Filter',
|
||||
icon: 'filter',
|
||||
onClick: handleFilterToggle,
|
||||
label: 'Tags',
|
||||
icon: 'tag',
|
||||
onClick: handleTagStripToggle,
|
||||
active: isFilterStripVisible,
|
||||
},
|
||||
...($page.url.pathname === '/' || $page.url.pathname === ''
|
||||
|
|
@ -460,25 +470,9 @@
|
|||
{spotlightActions}
|
||||
/>
|
||||
|
||||
<!-- Unified filter strip (tags + priorities + sort, toggled via Filter pill) -->
|
||||
<!-- Tag strip (toggled via Tags pill) -->
|
||||
{#if isFilterStripVisible}
|
||||
<TaskFilters
|
||||
variant="strip"
|
||||
selectedPriorities={viewStore.filterPriorities}
|
||||
selectedLabelIds={viewStore.filterLabelIds}
|
||||
searchQuery={viewStore.filterSearchQuery}
|
||||
onPrioritiesChange={(p: TaskPriority[]) => viewStore.setFilterPriorities(p)}
|
||||
onLabelsChange={(ids: string[]) => viewStore.setFilterLabelIds(ids)}
|
||||
onSearchChange={(q: string) => viewStore.setFilterSearchQuery(q)}
|
||||
onClearFilters={() => viewStore.clearFilters()}
|
||||
sortBy={viewStore.sortBy}
|
||||
onSortChange={(s: SortBy) => viewStore.setSort(s, viewStore.sortOrder)}
|
||||
showSort={true}
|
||||
showCompleted={true}
|
||||
showTags={true}
|
||||
isCompletedVisible={viewStore.showCompleted}
|
||||
onToggleCompleted={() => viewStore.toggleShowCompleted()}
|
||||
/>
|
||||
<TagStrip />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue