feat: Phase 3 - filter bar, keyboard navigation, notifications, comments

- FilterBar component with text search, label chips, due date and priority dropdowns
- "/" keyboard shortcut and toolbar button to toggle filter bar
- Keyboard card navigation with J/K/H/L keys, Enter to open, Escape to clear
- Focus ring on keyboard-selected cards with auto-scroll
- Desktop notifications for due/overdue cards via tauri-plugin-notification
- CommentsSection component with add/delete and relative timestamps
- Filtered card count display in column headers
This commit is contained in:
Your Name
2026-02-16 14:52:08 +02:00
parent e535177914
commit 6340beb5d0
17 changed files with 791 additions and 140 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useRef } from "react";
import { useState, useRef, useEffect } from "react";
import { createPortal } from "react-dom";
import { format } from "date-fns";
import { motion, useReducedMotion, AnimatePresence } from "framer-motion";
@@ -65,9 +65,10 @@ interface CardThumbnailProps {
boardLabels: Label[];
columnId: string;
onCardClick?: (cardId: string) => void;
isFocused?: boolean;
}
export function CardThumbnail({ card, boardLabels, columnId, onCardClick }: CardThumbnailProps) {
export function CardThumbnail({ card, boardLabels, columnId, onCardClick, isFocused }: CardThumbnailProps) {
const prefersReducedMotion = useReducedMotion();
const {
@@ -82,6 +83,14 @@ export function CardThumbnail({ card, boardLabels, columnId, onCardClick }: Card
data: { type: "card", columnId },
});
const cardRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isFocused && cardRef.current) {
cardRef.current.scrollIntoView({ block: "nearest", behavior: "smooth" });
}
}, [isFocused]);
const hasDueDate = card.dueDate != null;
const dueDateStatus = getDueDateStatus(card.dueDate);
@@ -111,7 +120,10 @@ export function CardThumbnail({ card, boardLabels, columnId, onCardClick }: Card
<ContextMenu>
<ContextMenuTrigger asChild>
<motion.button
ref={setNodeRef}
ref={(node) => {
setNodeRef(node);
(cardRef as React.MutableRefObject<HTMLButtonElement | null>).current = node;
}}
style={{
transform: CSS.Transform.toString(transform),
transition,
@@ -119,7 +131,9 @@ export function CardThumbnail({ card, boardLabels, columnId, onCardClick }: Card
opacity: getAgingOpacity(card.updatedAt),
}}
onClick={handleClick}
className="w-full rounded-lg bg-pylon-surface shadow-sm text-left"
className={`w-full rounded-lg bg-pylon-surface shadow-sm text-left ${
isFocused ? "ring-2 ring-pylon-accent ring-offset-2 ring-offset-pylon-column" : ""
}`}
layoutId={`card-${card.id}`}
variants={fadeSlideUp}
initial={prefersReducedMotion ? false : "hidden"}