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:
@@ -1,7 +1,7 @@
|
||||
import { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { springs } from "@/lib/motion";
|
||||
import { ArrowLeft, Settings, Search, Undo2, Redo2, SlidersHorizontal } from "lucide-react";
|
||||
import { ArrowLeft, Settings, Search, Undo2, Redo2, SlidersHorizontal, Filter } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
@@ -138,6 +139,21 @@ export function TopBar() {
|
||||
<div className="flex items-center gap-1">
|
||||
{isBoardView && (
|
||||
<>
|
||||
<AnimatePresence mode="wait">
|
||||
{savingStatus && (
|
||||
<motion.span
|
||||
key={savingStatus}
|
||||
className="font-mono text-xs text-pylon-text-secondary"
|
||||
initial={{ opacity: 0, y: -4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 4 }}
|
||||
transition={springs.snappy}
|
||||
>
|
||||
{savingStatus}
|
||||
</motion.span>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<div className="mx-2 h-4 w-px bg-pylon-text-secondary/20" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
@@ -168,6 +184,21 @@ export function TopBar() {
|
||||
Redo <kbd className="ml-1 font-mono text-[10px] opacity-60">Ctrl+Shift+Z</kbd>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="text-pylon-text-secondary hover:text-pylon-text"
|
||||
onClick={() => document.dispatchEvent(new CustomEvent("toggle-filter-bar"))}
|
||||
>
|
||||
<Filter className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Filter cards <kbd className="ml-1 font-mono text-[10px] opacity-60">/</kbd>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
{isBoardView && board && (
|
||||
@@ -185,37 +216,33 @@ export function TopBar() {
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>Background</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
{(["none", "dots", "grid", "gradient"] as const).map((bg) => (
|
||||
<DropdownMenuItem
|
||||
key={bg}
|
||||
onClick={() => useBoardStore.getState().updateBoardSettings({ ...board.settings, background: bg })}
|
||||
>
|
||||
{bg.charAt(0).toUpperCase() + bg.slice(1)}
|
||||
{board.settings.background === bg && (
|
||||
<span className="ml-auto text-pylon-accent">*</span>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuRadioGroup
|
||||
value={board.settings.background}
|
||||
onValueChange={(v) => useBoardStore.getState().updateBoardSettings({ ...board.settings, background: v as typeof board.settings.background })}
|
||||
>
|
||||
{(["none", "dots", "grid", "gradient"] as const).map((bg) => (
|
||||
<DropdownMenuRadioItem key={bg} value={bg}>
|
||||
{bg.charAt(0).toUpperCase() + bg.slice(1)}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>Attachments</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
<DropdownMenuRadioGroup
|
||||
value={board.settings.attachmentMode}
|
||||
onValueChange={(v) => useBoardStore.getState().updateBoardSettings({ ...board.settings, attachmentMode: v as typeof board.settings.attachmentMode })}
|
||||
>
|
||||
<DropdownMenuRadioItem value="link">Link to original</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="copy">Copy into board</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<AnimatePresence mode="wait">
|
||||
{savingStatus && (
|
||||
<motion.span
|
||||
key={savingStatus}
|
||||
className="mr-2 font-mono text-xs text-pylon-text-secondary"
|
||||
initial={{ opacity: 0, y: -4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 4 }}
|
||||
transition={springs.snappy}
|
||||
>
|
||||
{savingStatus}
|
||||
</motion.span>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user