UI redesign: compact header, group dropdown, slim task cards

- 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>
This commit is contained in:
2026-03-29 20:44:41 +00:00
parent a29d0ba64c
commit 867482c674
4 changed files with 155 additions and 219 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { useRef, useEffect } from "react";
import { useState, useRef, useEffect, useCallback } from "react";
import { Group } from "@/lib/api";
import { useTranslation } from "@/lib/i18n";
@@ -15,61 +15,101 @@ export default function GroupSelector({
selected,
onSelect,
}: GroupSelectorProps) {
const scrollRef = useRef<HTMLDivElement>(null);
const activeRef = useRef<HTMLButtonElement>(null);
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
// Scroll active button into view
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 (activeRef.current && scrollRef.current) {
const container = scrollRef.current;
const btn = activeRef.current;
const containerRect = container.getBoundingClientRect();
const btnRect = btn.getBoundingClientRect();
if (btnRect.left < containerRect.left || btnRect.right > containerRect.right) {
btn.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
if (!open) return;
function handleClick(e: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setOpen(false);
}
}
}, [selected]);
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={scrollRef}
className="flex gap-2 overflow-x-auto scrollbar-hide snap-x snap-mandatory px-4 py-3"
style={{ WebkitOverflowScrolling: "touch" } as React.CSSProperties}
>
<div ref={dropdownRef} className="relative px-4">
{/* Dropdown trigger */}
<button
ref={selected === null ? activeRef : undefined}
onClick={() => onSelect(null)}
className={`flex-shrink-0 px-4 py-2.5 rounded-full text-sm font-medium transition-all whitespace-nowrap snap-center ${
selected === null
? "bg-gray-800 text-white dark:bg-white dark:text-gray-900 shadow-md scale-105 min-h-[44px]"
: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 min-h-[44px]"
}`}
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]"
>
{t("tasks.all")}
</button>
{groups.map((g) => (
<button
key={g.id}
ref={selected === g.id ? activeRef : undefined}
onClick={() => onSelect(g.id)}
className={`flex-shrink-0 px-4 py-2.5 rounded-full text-sm font-medium transition-all flex items-center gap-1.5 whitespace-nowrap snap-center ${
selected === g.id
? "text-white shadow-md scale-105 min-h-[44px]"
: "hover:opacity-80 min-h-[44px]"
}`}
style={{
backgroundColor: selected === g.id ? g.color : undefined,
border: selected !== g.id ? `2px solid ${g.color}` : undefined,
color: selected !== g.id ? g.color : undefined,
}}
{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}
>
{g.icon && <span className="text-lg">{g.icon}</span>}
<span>{g.name}</span>
</button>
))}
<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>
);
}