- Refactored TimerPage component to improve layout and user experience. - Updated theme configuration to utilize design tokens for better maintainability. - Introduced new tokens for colors and typography to align with design principles. - Simplified state management and improved timer functionality. - Enhanced accessibility and responsiveness of UI elements.
109 lines
3.3 KiB
TypeScript
109 lines
3.3 KiB
TypeScript
/**
|
|
* Pomodoros per day — magnitude over time, so: bars, one hue, baseline at zero.
|
|
*
|
|
* The goal line is the only reference mark; days that cleared it are the darker
|
|
* ramp step, which keeps "did I hit the goal" readable without a second colour.
|
|
*/
|
|
import { Box, Tooltip, Typography } from "@mui/material";
|
|
|
|
import { shortDate } from "../../lib/format";
|
|
import type { VizPalette } from "../../lib/viz";
|
|
import ChartFrame from "./ChartFrame";
|
|
|
|
interface Day {
|
|
date: string;
|
|
pomodoros: number;
|
|
focus_minutes: number;
|
|
goal_met: boolean;
|
|
}
|
|
|
|
const HEIGHT = 160;
|
|
|
|
export default function FocusBars({
|
|
days,
|
|
goal,
|
|
palette,
|
|
title = "Pomodoros per day",
|
|
}: {
|
|
days: Day[];
|
|
goal: number;
|
|
palette: VizPalette;
|
|
title?: string;
|
|
}) {
|
|
const max = Math.max(goal, ...days.map((day) => day.pomodoros), 1);
|
|
const goalY = HEIGHT - (goal / max) * HEIGHT;
|
|
const slotWidth = 100 / Math.max(days.length, 1);
|
|
|
|
return (
|
|
<ChartFrame
|
|
title={title}
|
|
caption={`Last ${days.length} days · goal ${goal}/day`}
|
|
>
|
|
<Box sx={{ position: "relative", height: HEIGHT, display: "flex", alignItems: "flex-end", gap: "2px" }}>
|
|
{goal > 0 && (
|
|
<Box
|
|
sx={{
|
|
position: "absolute",
|
|
left: 0,
|
|
right: 0,
|
|
top: `${goalY}px`,
|
|
borderTop: `1px dashed ${palette.grid}`,
|
|
pointerEvents: "none",
|
|
}}
|
|
>
|
|
<Typography
|
|
variant="caption"
|
|
sx={{ position: "absolute", right: 0, top: -18, color: "text.secondary" }}
|
|
>
|
|
goal
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
{days.map((day) => {
|
|
const height = (day.pomodoros / max) * HEIGHT;
|
|
return (
|
|
<Tooltip
|
|
key={day.date}
|
|
title={
|
|
<Box>
|
|
<Typography variant="caption" sx={{ display: "block", fontWeight: 600 }}>
|
|
{day.date}
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ display: "block" }}>
|
|
{day.pomodoros} pomodoros · {day.focus_minutes} min
|
|
</Typography>
|
|
</Box>
|
|
}
|
|
>
|
|
<Box
|
|
sx={{
|
|
flex: `0 0 calc(${slotWidth}% - 2px)`,
|
|
height: Math.max(height, day.pomodoros > 0 ? 4 : 2),
|
|
minHeight: 2,
|
|
borderRadius: "4px 4px 0 0",
|
|
bgcolor:
|
|
day.pomodoros === 0
|
|
? palette.empty
|
|
: day.goal_met
|
|
? palette.sequential[4]
|
|
: palette.sequential[2],
|
|
transition: "height .3s",
|
|
cursor: "default",
|
|
}}
|
|
/>
|
|
</Tooltip>
|
|
);
|
|
})}
|
|
</Box>
|
|
<Box sx={{ display: "flex", justifyContent: "space-between", mt: 0.5 }}>
|
|
<Typography variant="caption" color="text.secondary">
|
|
{days.length ? shortDate(days[0].date) : ""}
|
|
</Typography>
|
|
<Typography variant="caption" color="text.secondary">
|
|
{days.length ? shortDate(days[days.length - 1].date) : ""}
|
|
</Typography>
|
|
</Box>
|
|
</ChartFrame>
|
|
);
|
|
}
|