This commit is contained in:
timka123 committed 2026-10-18 18:02:05 +03:00
commit 17a0a3c4c9
121 files changed
+17641

No files matched your search

@@ -0,0 +1,108 @@
/**
* Today, laid out on a clock: every interval in its real place.
*
* Three categories (focus / short / long break) so a legend is mandatory, and
* each block is also labelled on hover — colour never carries identity alone.
*/
import { Box, Stack, Tooltip, Typography, useTheme } from "@mui/material";
import { KIND_LABEL } from "../../lib/format";
import { kindColor, type VizPalette } from "../../lib/viz";
import ChartFrame from "./ChartFrame";
interface Session {
id: number;
kind: string;
status: string;
started_at: string;
minutes: number;
task: string | null;
}
const LANE = 34;
export default function SessionTimeline({
sessions,
palette,
}: {
sessions: Session[];
palette: VizPalette;
}) {
const theme = useTheme();
const done = sessions.filter((item) => item.minutes > 0);
if (!done.length) {
return (
<ChartFrame title="Today's timeline" caption="Nothing recorded yet — start a pomodoro.">
<Box sx={{ height: LANE, borderRadius: 1, bgcolor: theme.palette.action.hover }} />
</ChartFrame>
);
}
const hourOf = (iso: string) => {
const date = new Date(iso);
return date.getHours() + date.getMinutes() / 60;
};
const first = Math.floor(Math.min(...done.map((item) => hourOf(item.started_at))));
const last = Math.ceil(
Math.max(...done.map((item) => hourOf(item.started_at) + item.minutes / 60)),
);
const span = Math.max(last - first, 1);
return (
<ChartFrame
title="Today's timeline"
caption={`${first}:00 → ${last}:00`}
legend={[
{ label: KIND_LABEL.work, color: palette.work },
{ label: KIND_LABEL.short_break, color: palette.shortBreak },
{ label: KIND_LABEL.long_break, color: palette.longBreak },
]}
>
<Box sx={{ position: "relative", height: LANE, borderRadius: 1, bgcolor: theme.palette.action.hover }}>
{done.map((item) => {
const start = (hourOf(item.started_at) - first) / span;
const width = item.minutes / 60 / span;
return (
<Tooltip
key={item.id}
title={
<Box>
<Typography variant="caption" sx={{ display: "block", fontWeight: 600 }}>
{KIND_LABEL[item.kind] ?? item.kind} · {item.minutes} min
</Typography>
<Typography variant="caption" sx={{ display: "block" }}>
{new Date(item.started_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
{item.task ? ` · ${item.task}` : ""}
{item.status === "abandoned" ? " · abandoned" : ""}
</Typography>
</Box>
}
>
<Box
sx={{
position: "absolute",
left: `${start * 100}%`,
width: `calc(${Math.max(width * 100, 1)}% - 2px)`,
top: 4,
bottom: 4,
borderRadius: "4px",
bgcolor: kindColor(item.kind, palette),
opacity: item.status === "abandoned" ? 0.4 : 1,
border: item.status === "abandoned" ? `1px dashed ${palette.axis}` : "none",
}}
/>
</Tooltip>
);
})}
</Box>
<Stack direction="row" justifyContent="space-between" sx={{ mt: 0.5 }}>
<Typography variant="caption" color="text.secondary">
{String(first).padStart(2, "0")}:00
</Typography>
<Typography variant="caption" color="text.secondary">
{String(last).padStart(2, "0")}:00
</Typography>
</Stack>
</ChartFrame>
);
}