- Removed logo/brand from header - Group selector as dropdown (not horizontal scroll) - Compact task cards (single line, less padding) - Status filter pills smaller - Sticky header 44px Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
116 lines
4.2 KiB
TypeScript
116 lines
4.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useRef, useEffect, useCallback } from "react";
|
|
import { Group } from "@/lib/api";
|
|
import { useTranslation } from "@/lib/i18n";
|
|
|
|
interface GroupSelectorProps {
|
|
groups: Group[];
|
|
selected: string | null;
|
|
onSelect: (id: string | null) => void;
|
|
}
|
|
|
|
export default function GroupSelector({
|
|
groups,
|
|
selected,
|
|
onSelect,
|
|
}: GroupSelectorProps) {
|
|
const { t } = useTranslation();
|
|
const [open, setOpen] = useState(false);
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
|
|
const selectedGroup = selected ? groups.find((g) => g.id === selected) : null;
|
|
|
|
const handleSelect = useCallback((id: string | null) => {
|
|
onSelect(id);
|
|
setOpen(false);
|
|
}, [onSelect]);
|
|
|
|
// Close on outside click
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
function handleClick(e: MouseEvent) {
|
|
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
|
setOpen(false);
|
|
}
|
|
}
|
|
document.addEventListener("mousedown", handleClick);
|
|
return () => document.removeEventListener("mousedown", handleClick);
|
|
}, [open]);
|
|
|
|
// Close on escape
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
function handleKey(e: KeyboardEvent) {
|
|
if (e.key === "Escape") setOpen(false);
|
|
}
|
|
document.addEventListener("keydown", handleKey);
|
|
return () => document.removeEventListener("keydown", handleKey);
|
|
}, [open]);
|
|
|
|
return (
|
|
<div ref={dropdownRef} className="relative px-4">
|
|
{/* Dropdown trigger */}
|
|
<button
|
|
onClick={() => setOpen(!open)}
|
|
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-sm font-medium min-h-[40px]"
|
|
>
|
|
{selectedGroup ? (
|
|
<>
|
|
{selectedGroup.icon && <span className="text-base">{selectedGroup.icon}</span>}
|
|
<span style={{ color: selectedGroup.color }}>{selectedGroup.name}</span>
|
|
</>
|
|
) : (
|
|
<span>{t("tasks.all")}</span>
|
|
)}
|
|
<svg
|
|
className={`w-4 h-4 text-muted transition-transform ${open ? "rotate-180" : ""}`}
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
strokeWidth={2}
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
|
|
{/* Dropdown menu */}
|
|
{open && (
|
|
<div className="absolute left-4 top-full mt-1 z-50 min-w-[200px] bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-xl py-1 animate-fadeIn">
|
|
<button
|
|
onClick={() => handleSelect(null)}
|
|
className={`w-full flex items-center gap-2.5 px-4 py-2.5 text-sm text-left hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors ${
|
|
selected === null ? "font-semibold bg-gray-50 dark:bg-gray-800/50" : ""
|
|
}`}
|
|
>
|
|
<span className="w-5 text-center text-base">*</span>
|
|
<span>{t("tasks.all")}</span>
|
|
{selected === null && (
|
|
<svg className="w-4 h-4 ml-auto text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
{groups.map((g) => (
|
|
<button
|
|
key={g.id}
|
|
onClick={() => handleSelect(g.id)}
|
|
className={`w-full flex items-center gap-2.5 px-4 py-2.5 text-sm text-left hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors ${
|
|
selected === g.id ? "font-semibold bg-gray-50 dark:bg-gray-800/50" : ""
|
|
}`}
|
|
>
|
|
<span className="w-5 text-center text-base">{g.icon || ""}</span>
|
|
<span style={{ color: g.color }}>{g.name}</span>
|
|
{selected === g.id && (
|
|
<svg className="w-4 h-4 ml-auto text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|