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