style: enhance ActivityCalendar components with improved styling and layout adjustments

This commit is contained in:
Johnny 2026-01-30 00:13:58 +08:00
parent fcb9e377c1
commit f7a81296fb
6 changed files with 53 additions and 44 deletions

View File

@ -26,7 +26,7 @@ export const CalendarCell = memo((props: CalendarCellProps) => {
const smallExtraClasses = size === "small" ? `${SMALL_CELL_SIZE.dimensions} min-h-0` : "";
const baseClasses = cn(
"aspect-square w-full flex items-center justify-center text-center transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:ring-offset-2 select-none",
"aspect-square w-full flex items-center justify-center text-center transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:ring-offset-2 select-none border border-border/10 bg-muted/20",
sizeConfig.font,
sizeConfig.borderRadius,
smallExtraClasses,
@ -35,7 +35,7 @@ export const CalendarCell = memo((props: CalendarCellProps) => {
const ariaLabel = day.isSelected ? `${tooltipText} (selected)` : tooltipText;
if (!day.isCurrentMonth) {
return <div className={cn(baseClasses, "text-muted-foreground/30 bg-transparent cursor-default")}>{day.label}</div>;
return <div className={cn(baseClasses, "text-muted-foreground/30 bg-transparent border-transparent cursor-default")}>{day.label}</div>;
}
const intensityClass = getCellIntensityClass(day, maxCount);
@ -45,7 +45,7 @@ export const CalendarCell = memo((props: CalendarCellProps) => {
intensityClass,
day.isToday && "ring-2 ring-primary/30 ring-offset-1 font-semibold z-10",
day.isSelected && "ring-2 ring-primary ring-offset-1 font-bold z-10",
isInteractive ? "cursor-pointer hover:scale-110 hover:shadow-md hover:z-20" : "cursor-default",
isInteractive ? "cursor-pointer hover:bg-muted/40 hover:border-border/30" : "cursor-default",
);
const button = (

View File

@ -3,7 +3,6 @@ import { useInstance } from "@/contexts/InstanceContext";
import { cn } from "@/lib/utils";
import { useTranslate } from "@/utils/i18n";
import { CalendarCell } from "./CalendarCell";
import { DEFAULT_CELL_SIZE, SMALL_CELL_SIZE } from "./constants";
import { useTodayDate, useWeekdayLabels } from "./hooks";
import type { MonthCalendarProps } from "./types";
import { useCalendarMatrix } from "./useCalendar";
@ -28,19 +27,19 @@ export const MonthCalendar = memo((props: MonthCalendarProps) => {
selectedDate: "",
});
const sizeConfig = size === "small" ? SMALL_CELL_SIZE : DEFAULT_CELL_SIZE;
const gridGap = size === "small" ? "gap-x-3 gap-y-3" : "gap-x-3.5 gap-y-3.5";
return (
<div className={cn("flex flex-col gap-2", className)}>
<div className={cn("grid grid-cols-7", sizeConfig.gap, "text-muted-foreground mb-1", size === "small" ? "text-[10px]" : "text-xs")}>
<div className={cn("grid grid-cols-7", gridGap, "text-muted-foreground mb-1", size === "small" ? "text-[10px]" : "text-xs")}>
{rotatedWeekDays.map((label, index) => (
<div key={index} className="flex h-4 items-center justify-center text-muted-foreground/60 font-medium">
<div key={index} className="flex h-4 items-center justify-center text-muted-foreground/70 font-medium uppercase tracking-wide">
{label}
</div>
))}
</div>
<div className={cn("grid grid-cols-7", sizeConfig.gap)}>
<div className={cn("grid grid-cols-7 px-2", gridGap)}>
{weeks.map((week, weekIndex) =>
week.days.map((day, dayIndex) => {
const tooltipText = getTooltipText(day.count, day.date, t);

View File

@ -30,18 +30,20 @@ export const YearCalendar = ({ selectedYear, data, onYearChange, onDateClick, cl
const handleToday = () => onYearChange(currentYear);
return (
<div className={cn("w-full flex flex-col gap-6 p-2 md:p-0 select-none", className)}>
<div className="flex items-center justify-between pb-4 px-2 pt-2">
<h2 className="text-3xl font-bold text-foreground tracking-tight leading-none">{selectedYear}</h2>
<div className={cn("w-full flex flex-col gap-6 px-4 sm:px-0 py-4 select-none", className)}>
<div className="flex items-center justify-between px-1">
<div className="flex items-baseline gap-3">
<h2 className="text-2xl md:text-3xl font-semibold text-foreground tracking-tight leading-none">{selectedYear}</h2>
</div>
<div className="inline-flex items-center gap-1 shrink-0">
<div className="inline-flex items-center gap-1 shrink-0 rounded-lg border border-border/20 bg-muted/20 p-1">
<Button
variant="ghost"
size="sm"
onClick={handlePrevYear}
disabled={!canGoPrev}
aria-label="Previous year"
className="h-9 w-9 p-0 rounded-md hover:bg-secondary/80 text-muted-foreground hover:text-foreground"
className="h-8 w-8 p-0 rounded-md hover:bg-muted/30 text-muted-foreground hover:text-foreground"
>
<ChevronLeftIcon className="w-5 h-5" />
</Button>
@ -53,8 +55,8 @@ export const YearCalendar = ({ selectedYear, data, onYearChange, onDateClick, cl
disabled={isCurrentYear}
aria-label={t("common.today")}
className={cn(
"h-9 px-4 rounded-md text-sm font-medium transition-colors",
isCurrentYear ? "bg-secondary/50 text-muted-foreground cursor-default" : "hover:bg-secondary/80 text-foreground",
"h-8 px-3 rounded-md text-[11px] font-semibold uppercase tracking-[0.18em] transition-colors",
isCurrentYear ? "bg-muted/30 text-muted-foreground cursor-default" : "hover:bg-muted/30 text-foreground",
)}
>
{t("common.today")}
@ -66,7 +68,7 @@ export const YearCalendar = ({ selectedYear, data, onYearChange, onDateClick, cl
onClick={handleNextYear}
disabled={!canGoNext}
aria-label="Next year"
className="h-9 w-9 p-0 rounded-md hover:bg-secondary/80 text-muted-foreground hover:text-foreground"
className="h-8 w-8 p-0 rounded-md hover:bg-muted/30 text-muted-foreground hover:text-foreground"
>
<ChevronRightIcon className="w-5 h-5" />
</Button>
@ -75,13 +77,15 @@ export const YearCalendar = ({ selectedYear, data, onYearChange, onDateClick, cl
<TooltipProvider>
<div className="w-full animate-fade-in">
<div className="grid gap-6 grid-cols-1 sm:grid-cols-2 md:grid-cols-3">
<div className="grid gap-6 md:gap-7 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{months.map((month) => (
<div
key={month}
className="flex flex-col gap-3 rounded-lg p-3 hover:bg-secondary/40 transition-colors cursor-default border border-transparent hover:border-border/50"
className="flex flex-col gap-3 rounded-2xl border border-border/20 bg-muted/10 p-4 shadow-sm hover:shadow-md transition-shadow cursor-default"
>
<div className="text-xs font-bold text-foreground/80 uppercase tracking-widest pl-1">{getMonthLabel(month)}</div>
<div className="text-[11px] font-semibold text-muted-foreground uppercase tracking-[0.22em] pl-1">
{getMonthLabel(month)}
</div>
<MonthCalendar month={month} data={yearData} maxCount={yearMaxCount} size="small" onClick={onDateClick} />
</div>
))}

View File

@ -14,22 +14,22 @@ export const INTENSITY_THRESHOLDS = {
} as const;
export const CELL_STYLES = {
HIGH: "bg-primary text-primary-foreground shadow-sm",
MEDIUM: "bg-primary/80 text-primary-foreground shadow-sm",
LOW: "bg-primary/60 text-primary-foreground shadow-sm",
MINIMAL: "bg-primary/40 text-foreground",
EMPTY: "bg-secondary/30 text-muted-foreground hover:bg-secondary/50",
HIGH: "bg-primary text-primary-foreground shadow-sm border-transparent",
MEDIUM: "bg-primary/85 text-primary-foreground shadow-sm border-transparent",
LOW: "bg-primary/70 text-primary-foreground border-transparent",
MINIMAL: "bg-primary/50 text-foreground border-transparent",
EMPTY: "bg-muted/20 text-muted-foreground hover:bg-muted/30 border-border/10",
} as const;
export const SMALL_CELL_SIZE = {
font: "text-xs",
dimensions: "w-8 h-8 mx-auto",
borderRadius: "rounded-md",
gap: "gap-1",
font: "text-[11px]",
dimensions: "w-full h-full",
borderRadius: "rounded-lg",
gap: "gap-1.5",
} as const;
export const DEFAULT_CELL_SIZE = {
font: "text-xs",
borderRadius: "rounded-md",
gap: "gap-1.5",
borderRadius: "rounded-lg",
gap: "gap-2",
} as const;

View File

@ -1,6 +1,6 @@
import { LatLng } from "leaflet";
import { uniqBy } from "lodash-es";
import { FileIcon, LinkIcon, LoaderIcon, MapPinIcon, Maximize2Icon, MoreHorizontalIcon, PlusIcon, type LucideIcon } from "lucide-react";
import { FileIcon, LinkIcon, LoaderIcon, type LucideIcon, MapPinIcon, Maximize2Icon, MoreHorizontalIcon, PlusIcon } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useDebounce } from "react-use";
import { useReverseGeocoding } from "@/components/map";
@ -106,9 +106,12 @@ const InsertMenu = (props: InsertMenuProps) => {
setLocationDialogOpen(false);
}, [location]);
const handlePositionChange = useCallback((position: LatLng) => {
location.handlePositionChange(position);
}, [location]);
const handlePositionChange = useCallback(
(position: LatLng) => {
location.handlePositionChange(position);
},
[location],
);
const handleToggleFocusMode = useCallback(() => {
onToggleFocusMode?.();

View File

@ -1,5 +1,5 @@
import dayjs from "dayjs";
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { useState } from "react";
import { YearCalendar } from "@/components/ActivityCalendar";
import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
@ -31,33 +31,36 @@ export const MonthNavigator = ({ visibleMonth, onMonthChange, activityStats }: M
};
return (
<div className="w-full mb-2 flex flex-row justify-between items-center gap-1">
<div className="w-full mb-2 flex flex-row justify-between items-center gap-2">
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<button className="px-2 py-1 -ml-2 rounded-md hover:bg-secondary/50 text-sm text-foreground font-semibold transition-colors flex items-center gap-1 select-none group">
<button className="py-1 text-sm text-foreground font-medium transition-colors flex items-center select-none">
{currentMonth.toLocaleString(i18n.language, { year: "numeric", month: "long" })}
<ChevronDownIcon className="w-3.5 h-3.5 text-muted-foreground group-hover:text-foreground transition-colors" />
</button>
</DialogTrigger>
<DialogContent className="p-0 border-none bg-background md:max-w-4xl" size="2xl" showCloseButton={false}>
<DialogContent
className="p-0 border border-border/20 bg-background md:max-w-6xl w-[min(100vw-24px,1200px)] max-h-[85vh] overflow-auto rounded-2xl shadow-2xl"
size="2xl"
showCloseButton={false}
>
<DialogTitle className="sr-only">Select Month</DialogTitle>
<YearCalendar selectedYear={currentYear} data={activityStats} onYearChange={handleYearChange} onDateClick={handleDateClick} />
</DialogContent>
</Dialog>
<div className="flex justify-end items-center shrink-0 gap-0.5">
<div className="flex justify-end items-center shrink-0">
<button
className="p-1 rounded-md hover:bg-secondary/50 text-muted-foreground hover:text-foreground transition-all"
className="h-8 w-8 rounded-lg hover:border-border/40 hover:bg-muted/30 text-muted-foreground hover:text-foreground transition-all"
onClick={handlePrevMonth}
aria-label="Previous month"
>
<ChevronLeftIcon className="w-4 h-4" />
<ChevronLeftIcon className="w-4 h-4 mx-auto" />
</button>
<button
className="p-1 rounded-md hover:bg-secondary/50 text-muted-foreground hover:text-foreground transition-all"
className="h-8 w-8 rounded-lg hover:border-border/40 hover:bg-muted/30 text-muted-foreground hover:text-foreground transition-all"
onClick={handleNextMonth}
aria-label="Next month"
>
<ChevronRightIcon className="w-4 h-4" />
<ChevronRightIcon className="w-4 h-4 mx-auto" />
</button>
</div>
</div>