Commit 61c96384 authored by boojack's avatar boojack

chore(web): polish dark theme and calendar UI

- simplify ActivityCalendar state handling and shared max-count utilities
- remove calendar ring styling and darken the default dark theme primary colors
- tighten the audio recorder panel layout and action sizing
parent 24fc8ab8
......@@ -3,7 +3,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
import { cn } from "@/lib/utils";
import { DEFAULT_CELL_SIZE, SMALL_CELL_SIZE } from "./constants";
import type { CalendarDayCell, CalendarSize } from "./types";
import { getCellIntensityClass } from "./utils";
import { getCalendarCellStateClass, getCellIntensityClass } from "./utils";
export interface CalendarCellProps {
day: CalendarDayCell;
......@@ -44,8 +44,7 @@ export const CalendarCell = memo((props: CalendarCellProps) => {
const buttonClasses = cn(
baseClasses,
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",
getCalendarCellStateClass(day),
isInteractive ? "cursor-pointer hover:bg-muted/40 hover:border-border/30" : "cursor-default",
);
......
......@@ -23,7 +23,7 @@ const WeekdayHeader = memo(({ weekDays, size }: WeekdayHeaderProps) => (
{weekDays.map((label, index) => (
<div
key={index}
className="flex h-4 items-center justify-center font-medium uppercase tracking-wide text-muted-foreground/60"
className="flex h-4 items-center justify-center uppercase tracking-wide text-muted-foreground/60"
role="columnheader"
aria-label={label}
>
......@@ -35,11 +35,12 @@ const WeekdayHeader = memo(({ weekDays, size }: WeekdayHeaderProps) => (
WeekdayHeader.displayName = "WeekdayHeader";
export const MonthCalendar = memo((props: MonthCalendarProps) => {
const { month, data, maxCount, size = "default", onClick, className, disableTooltips = false } = props;
const { month, data, maxCount, size = "default", onClick, selectedDate, className, disableTooltips = false } = props;
const t = useTranslate();
const { generalSetting } = useInstance();
const today = useTodayDate();
const weekDays = useWeekdayLabels();
const gridStyle = GRID_STYLES[size];
const { weeks, weekDays: rotatedWeekDays } = useCalendarMatrix({
month,
......@@ -47,7 +48,7 @@ export const MonthCalendar = memo((props: MonthCalendarProps) => {
weekDays,
weekStartDayOffset: generalSetting.weekStartDayOffset,
today,
selectedDate: "",
selectedDate: selectedDate ?? "",
});
const flatDays = useMemo(() => weeks.flatMap((week) => week.days), [weeks]);
......@@ -56,7 +57,7 @@ export const MonthCalendar = memo((props: MonthCalendarProps) => {
<div className={cn("flex flex-col", className)} role="grid" aria-label={`Calendar for ${month}`}>
<WeekdayHeader weekDays={rotatedWeekDays} size={size} />
<div className={cn("grid grid-cols-7", GRID_STYLES[size].gap)} role="rowgroup">
<div className={cn("grid grid-cols-7", gridStyle.gap)} role="rowgroup">
{flatDays.map((day) => (
<CalendarCell
key={day.date}
......
......@@ -5,8 +5,8 @@ import { cn } from "@/lib/utils";
import { useTranslate } from "@/utils/i18n";
import { getMaxYear, MIN_YEAR } from "./constants";
import { MonthCalendar } from "./MonthCalendar";
import type { YearCalendarProps } from "./types";
import { calculateYearMaxCount, filterDataByYear, generateMonthsForYear, getMonthLabel } from "./utils";
import type { CalendarData, YearCalendarProps } from "./types";
import { calculateMaxCount, filterDataByYear, generateMonthsForYear, getMonthLabel } from "./utils";
interface YearNavigationProps {
selectedYear: number;
......@@ -70,7 +70,7 @@ YearNavigation.displayName = "YearNavigation";
interface MonthCardProps {
month: string;
data: Record<string, number>;
data: CalendarData;
maxCount: number;
onDateClick: (date: string) => void;
}
......@@ -87,7 +87,7 @@ export const YearCalendar = memo(({ selectedYear, data, onYearChange, onDateClic
const currentYear = useMemo(() => new Date().getFullYear(), []);
const yearData = useMemo(() => filterDataByYear(data, selectedYear), [data, selectedYear]);
const months = useMemo(() => generateMonthsForYear(selectedYear), [selectedYear]);
const yearMaxCount = useMemo(() => calculateYearMaxCount(yearData), [yearData]);
const yearMaxCount = useMemo(() => calculateMaxCount(yearData), [yearData]);
const canGoPrev = selectedYear > MIN_YEAR;
const canGoNext = selectedYear < getMaxYear();
......
export const DAYS_IN_WEEK = 7;
export const MONTHS_IN_YEAR = 12;
export const WEEKEND_DAYS = [0, 6] as const;
export const MIN_COUNT = 1;
export const MIN_YEAR = 1970;
......
export type CalendarSize = "default" | "small";
export type CalendarData = Record<string, number>;
export interface CalendarDayCell {
date: string;
......@@ -7,7 +8,6 @@ export interface CalendarDayCell {
isCurrentMonth: boolean;
isToday: boolean;
isSelected: boolean;
isWeekend: boolean;
}
export interface CalendarDayRow {
......@@ -17,22 +17,22 @@ export interface CalendarDayRow {
export interface CalendarMatrixResult {
weeks: CalendarDayRow[];
weekDays: string[];
maxCount: number;
}
export interface MonthCalendarProps {
month: string;
data: Record<string, number>;
data: CalendarData;
maxCount: number;
size?: CalendarSize;
onClick?: (date: string) => void;
selectedDate?: string;
className?: string;
disableTooltips?: boolean;
}
export interface YearCalendarProps {
selectedYear: number;
data: Record<string, number>;
data: CalendarData;
onYearChange: (year: number) => void;
onDateClick: (date: string) => void;
className?: string;
......
import dayjs from "dayjs";
import { useMemo } from "react";
import { DAYS_IN_WEEK, MIN_COUNT, WEEKEND_DAYS } from "./constants";
import type { CalendarDayCell, CalendarMatrixResult } from "./types";
import { DAYS_IN_WEEK } from "./constants";
import type { CalendarData, CalendarDayCell, CalendarMatrixResult } from "./types";
export interface UseCalendarMatrixParams {
month: string;
data: Record<string, number>;
data: CalendarData;
weekDays: string[];
weekStartDayOffset: number;
today: string;
......@@ -15,7 +15,7 @@ export interface UseCalendarMatrixParams {
const createCalendarDayCell = (
current: dayjs.Dayjs,
monthKey: string,
data: Record<string, number>,
data: CalendarData,
today: string,
selectedDate: string,
): CalendarDayCell => {
......@@ -30,7 +30,6 @@ const createCalendarDayCell = (
isCurrentMonth,
isToday: isoDate === today,
isSelected: isoDate === selectedDate,
isWeekend: WEEKEND_DAYS.includes(current.day() as 0 | 6),
};
};
......@@ -68,7 +67,6 @@ export const useCalendarMatrix = ({
const { calendarStart, dayCount } = calculateCalendarBoundaries(monthStart, weekStartDayOffset);
const weeks: CalendarMatrixResult["weeks"] = [];
let maxCount = 0;
// Iterate through each day in the calendar grid
for (let index = 0; index < dayCount; index += 1) {
......@@ -82,13 +80,11 @@ export const useCalendarMatrix = ({
// Create the day cell object with data and status flags
const dayCell = createCalendarDayCell(current, monthKey, data, today, selectedDate);
weeks[weekIndex].days.push(dayCell);
maxCount = Math.max(maxCount, dayCell.count);
}
return {
weeks,
weekDays: rotatedWeekDays,
maxCount: Math.max(maxCount, MIN_COUNT),
};
}, [month, data, weekDays, weekStartDayOffset, today, selectedDate]);
};
import dayjs from "dayjs";
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
import { cn } from "@/lib/utils";
import { useTranslate } from "@/utils/i18n";
import { CELL_STYLES, INTENSITY_THRESHOLDS, MIN_COUNT, MONTHS_IN_YEAR } from "./constants";
import type { CalendarDayCell } from "./types";
import type { CalendarData, CalendarDayCell } from "./types";
dayjs.extend(isSameOrAfter);
dayjs.extend(isSameOrBefore);
......@@ -22,11 +23,15 @@ export const getCellIntensityClass = (day: CalendarDayCell, maxCount: number): s
return CELL_STYLES.MINIMAL;
};
export const getCalendarCellStateClass = (day: Pick<CalendarDayCell, "isToday" | "isSelected">): string => {
return cn(day.isToday && "font-semibold z-10", day.isSelected && "font-bold z-10");
};
export const generateMonthsForYear = (year: number): string[] => {
return Array.from({ length: MONTHS_IN_YEAR }, (_, i) => dayjs(`${year}-01-01`).add(i, "month").format("YYYY-MM"));
};
export const calculateYearMaxCount = (data: Record<string, number>): number => {
export const calculateMaxCount = (data: CalendarData): number => {
let max = 0;
for (const count of Object.values(data)) {
max = Math.max(max, count);
......@@ -55,10 +60,6 @@ export const filterDataByYear = (data: Record<string, number>, year: number): Re
return filtered;
};
export const hasActivityData = (data: Record<string, number>): boolean => {
return Object.values(data).some((count) => count > 0);
};
export const getTooltipText = (count: number, date: string, t: TranslateFunction): string => {
if (count === 0) {
return date;
......
......@@ -15,12 +15,10 @@ export const AudioRecorderPanel: FC<AudioRecorderPanelProps> = ({ audioRecorder,
return (
<div className="w-full rounded-lg border border-border/60 bg-muted/20 px-2.5 py-2">
<div className="flex items-center gap-2">
<div className="min-w-0 flex-1">
<div className="min-w-0 flex flex-1 gap-2">
<div className="truncate text-sm font-medium text-foreground">
{isRequestingPermission ? t("editor.audio-recorder.requesting-permission") : t("editor.audio-recorder.recording")}
</div>
</div>
<div
className={cn(
"inline-flex shrink-0 items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium",
......@@ -36,9 +34,10 @@ export const AudioRecorderPanel: FC<AudioRecorderPanelProps> = ({ audioRecorder,
)}
{formatAudioTime(elapsedSeconds)}
</div>
</div>
<div className="ml-auto flex shrink-0 items-center gap-1">
<Button variant="ghost" size="icon" onClick={onCancel} aria-label={t("common.cancel")}>
<Button variant="ghost" size="sm" onClick={onCancel} aria-label={t("common.cancel")}>
<XIcon className="size-4" />
</Button>
<Button size="sm" className="gap-1.5" onClick={onStop} disabled={isRequestingPermission}>
......
import dayjs from "dayjs";
import { useMemo, useState } from "react";
import { MonthCalendar } from "@/components/ActivityCalendar";
import { useState } from "react";
import { calculateMaxCount, MonthCalendar } from "@/components/ActivityCalendar";
import { useDateFilterNavigation } from "@/hooks";
import type { StatisticsData } from "@/types/statistics";
import { MonthNavigator } from "./MonthNavigator";
......@@ -15,17 +15,17 @@ const StatisticsView = (props: Props) => {
const navigateToDateFilter = useDateFilterNavigation();
const [visibleMonthString, setVisibleMonthString] = useState(dayjs().format("YYYY-MM"));
const maxCount = useMemo(() => {
const counts = Object.values(activityStats);
return Math.max(...counts, 1);
}, [activityStats]);
return (
<div className="group w-full mt-2 flex flex-col text-muted-foreground animate-fade-in">
<MonthNavigator visibleMonth={visibleMonthString} onMonthChange={setVisibleMonthString} activityStats={activityStats} />
<div className="w-full animate-scale-in">
<MonthCalendar month={visibleMonthString} data={activityStats} maxCount={maxCount} onClick={navigateToDateFilter} />
<MonthCalendar
month={visibleMonthString}
data={activityStats}
maxCount={calculateMaxCount(activityStats)}
onClick={navigateToDateFilter}
/>
</div>
</div>
);
......
......@@ -10,8 +10,8 @@
--popover: oklch(0.17 0.006 265);
--popover-foreground: oklch(0.82 0.005 265);
/* Primary — bright blue, clearly interactive in dark context */
--primary: oklch(0.65 0.15 250);
/* Primary — subdued blue that sits back on dark surfaces */
--primary: oklch(0.42 0.08 250);
--primary-foreground: oklch(0.98 0.003 265);
/* Secondary — subtle elevated surface for secondary buttons */
......@@ -33,7 +33,7 @@
/* Borders — --border for layout dividers, --input for form field borders */
--border: oklch(0.21 0.007 265);
--input: oklch(0.25 0.007 265);
--ring: oklch(0.65 0.15 250);
--ring: oklch(0.34 0.06 250);
/* Sidebar — darkest surface, distinct from background */
--sidebar: oklch(0.07 0.005 265);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment