mirror of
https://github.com/Zproger/Aurum.git
synced 2026-10-13 11:06:40 +07:00
redesign: YearSelector as a compact dropdown next to MonthSelector
The pill-row year switcher from the previous commit added a whole extra row of buttons above the month pills, which read as cluttered. Replaced it with a single dropdown button (current year + chevron) placed to the right of MonthSelector on all three pages, matching how a lot of finance-app date pickers separate month (frequent) from year (occasional) navigation.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "aurum-frontend",
|
"name": "aurum-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.16.0",
|
"version": "0.16.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface YearSelectorProps {
|
interface YearSelectorProps {
|
||||||
@@ -6,27 +8,73 @@ interface YearSelectorProps {
|
|||||||
onChange: (year: number) => void;
|
onChange: (year: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Compact dropdown, most recent year first — sits to the right of
|
||||||
|
* MonthSelector instead of competing with it as a second row of pills. */
|
||||||
export function YearSelector({ years, year, onChange }: YearSelectorProps) {
|
export function YearSelector({ years, year, onChange }: YearSelectorProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
|
const sortedYears = [...years].sort((a, b) => b - a);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
function handlePointerDown(event: MouseEvent) {
|
||||||
|
if (rootRef.current && !rootRef.current.contains(event.target as Node)) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
|
if (event.key === "Escape") setOpen(false);
|
||||||
|
}
|
||||||
|
document.addEventListener("mousedown", handlePointerDown);
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handlePointerDown);
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 overflow-x-auto pb-1 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
<div ref={rootRef} className="relative shrink-0">
|
||||||
{years.map((value) => {
|
<button
|
||||||
const active = value === year;
|
type="button"
|
||||||
return (
|
onClick={() => setOpen((prev) => !prev)}
|
||||||
<button
|
aria-haspopup="listbox"
|
||||||
key={value}
|
aria-expanded={open}
|
||||||
type="button"
|
className="flex h-9 items-center gap-1.5 rounded-lg border border-border bg-surface-1 px-3 text-sm font-medium text-text-primary transition-colors hover:bg-surface-2"
|
||||||
onClick={() => onChange(value)}
|
>
|
||||||
className={cn(
|
{year}
|
||||||
"shrink-0 rounded-full border px-3.5 py-1.5 text-sm font-medium transition-colors",
|
<ChevronDown size={14} className={cn("text-text-muted transition-transform", open && "rotate-180")} />
|
||||||
active
|
</button>
|
||||||
? "border-text-primary bg-text-primary text-surface-1"
|
|
||||||
: "border-border bg-surface-1 text-text-secondary hover:bg-surface-2"
|
{open && (
|
||||||
)}
|
<ul
|
||||||
>
|
role="listbox"
|
||||||
{value}
|
className="absolute right-0 top-full z-20 mt-1.5 max-h-64 w-24 overflow-y-auto rounded-lg border border-border bg-surface-1 py-1 shadow-lg"
|
||||||
</button>
|
>
|
||||||
);
|
{sortedYears.map((value) => {
|
||||||
})}
|
const active = value === year;
|
||||||
|
return (
|
||||||
|
<li key={value}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="option"
|
||||||
|
aria-selected={active}
|
||||||
|
onClick={() => {
|
||||||
|
onChange(value);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"block w-full px-3 py-1.5 text-left text-sm transition-colors",
|
||||||
|
active ? "bg-surface-2 font-semibold text-text-primary" : "text-text-secondary hover:bg-surface-2"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,9 +54,11 @@ export function BudgetPage() {
|
|||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<AlertBanner />
|
<AlertBanner />
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<MonthSelector month={month} onChange={setMonth} />
|
||||||
|
</div>
|
||||||
<YearSelector years={years ?? [now.getFullYear()]} year={year} onChange={setYear} />
|
<YearSelector years={years ?? [now.getFullYear()]} year={year} onChange={setYear} />
|
||||||
<MonthSelector month={month} onChange={setMonth} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -35,9 +35,11 @@ export function DashboardPage() {
|
|||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<AlertBanner excludeKeys={["risky_allocation_exceeded"]} />
|
<AlertBanner excludeKeys={["risky_allocation_exceeded"]} />
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<MonthSelector month={month} onChange={setMonth} />
|
||||||
|
</div>
|
||||||
<YearSelector years={years ?? [now.getFullYear()]} year={year} onChange={setYear} />
|
<YearSelector years={years ?? [now.getFullYear()]} year={year} onChange={setYear} />
|
||||||
<MonthSelector month={month} onChange={setMonth} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isError && (
|
{isError && (
|
||||||
|
|||||||
@@ -63,7 +63,16 @@ export function TransactionsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="space-y-2">
|
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<MonthSelector
|
||||||
|
month={month}
|
||||||
|
onChange={(value) => {
|
||||||
|
setMonth(value);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<YearSelector
|
<YearSelector
|
||||||
years={years ?? [now.getFullYear()]}
|
years={years ?? [now.getFullYear()]}
|
||||||
year={year}
|
year={year}
|
||||||
@@ -72,13 +81,6 @@ export function TransactionsPage() {
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<MonthSelector
|
|
||||||
month={month}
|
|
||||||
onChange={(value) => {
|
|
||||||
setMonth(value);
|
|
||||||
setPage(1);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={openCreateModal} className="w-full sm:w-auto">
|
<Button onClick={openCreateModal} className="w-full sm:w-auto">
|
||||||
<Plus size={16} />
|
<Plus size={16} />
|
||||||
|
|||||||
Reference in New Issue
Block a user