Files
fn_registry/frontend/functions/ui/month_heatmap.tsx
T
egutierrez 03568c88e3 chore: auto-commit (57 archivos)
- frontend/functions/core/format_datetime_short.md
- frontend/functions/core/format_datetime_short.test.ts
- frontend/functions/core/format_datetime_short.ts
- frontend/functions/core/format_duration.md
- frontend/functions/core/format_duration.test.ts
- frontend/functions/core/format_duration.ts
- frontend/functions/core/month_grid.md
- frontend/functions/core/month_grid.test.ts
- frontend/functions/core/month_grid.ts
- frontend/functions/core/string_hash_palette.md
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 03:41:58 +02:00

144 lines
4.5 KiB
TypeScript

import { type ReactNode } from "react";
import { Box, Group, Paper, SimpleGrid, Stack, Text } from "@mantine/core";
import { IconCheckbox, IconPlus } from "@tabler/icons-react";
import { monthGrid } from "../core/month_grid";
export interface MonthHeatmapCell {
primary?: number;
secondary?: number;
}
export interface MonthHeatmapProps {
/** Mes a mostrar (se usan year y month, el dia se ignora). */
month: Date;
/** Datos por dia, key "YYYY-MM-DD". */
cells: Map<string, MonthHeatmapCell>;
/** Icono junto al contador primary. Default: IconPlus. */
primaryIcon?: ReactNode;
/** Icono junto al contador secondary. Default: IconCheckbox. */
secondaryIcon?: ReactNode;
/** Color Mantine del counter primary. Default: "blue". */
primaryColor?: string;
/** Color Mantine del counter secondary. Default: "green". */
secondaryColor?: string;
/** Labels de cabecera, 7 elementos empezando por lunes. */
dayLabels?: string[];
/** Altura minima de cada celda en px. Default: 72. */
cellMinHeight?: number;
}
const DEFAULT_DAY_LABELS = ["Lun", "Mar", "Mie", "Jue", "Vie", "Sab", "Dom"];
/**
* Grid mensual de calor (heatmap) con inicio en lunes.
*
* Renderiza 7 columnas con header de dias de la semana.
* Cada celda muestra el numero del dia y hasta dos contadores opcionales
* (primary / secondary) con icono. El dia de hoy recibe borde azul y
* numero en negrita. El fondo de la celda se tinta segun los valores:
* secondary > 0 → verdoso, primary > 0 → azulado.
* Las celdas de relleno (fuera del mes) se renderizan vacias.
*/
export function MonthHeatmap({
month,
cells,
primaryIcon,
secondaryIcon,
primaryColor = "blue",
secondaryColor = "green",
dayLabels = DEFAULT_DAY_LABELS,
cellMinHeight = 72,
}: MonthHeatmapProps) {
const year = month.getFullYear();
const monthNum = month.getMonth() + 1; // getMonth() es 0-based
const grid = monthGrid(year, monthNum);
// Hoy como "YYYY-MM-DD" usando Date nativo
const now = new Date();
const todayStr = [
now.getFullYear(),
String(now.getMonth() + 1).padStart(2, "0"),
String(now.getDate()).padStart(2, "0"),
].join("-");
const resolvedPrimaryIcon = primaryIcon ?? <IconPlus size={10} />;
const resolvedSecondaryIcon = secondaryIcon ?? <IconCheckbox size={10} />;
return (
<Box>
{/* Header dias de la semana */}
<SimpleGrid cols={7} spacing={4} mb={4}>
{dayLabels.map((label) => (
<Text key={label} size="xs" c="dimmed" ta="center" fw={600}>
{label}
</Text>
))}
</SimpleGrid>
{/* Celdas del mes */}
<SimpleGrid cols={7} spacing={4}>
{grid.map((cell, i) => {
if (!cell.date) {
return <Box key={i} style={{ minHeight: cellMinHeight }} />;
}
const data = cells.get(cell.date) ?? {};
const primary = data.primary ?? 0;
const secondary = data.secondary ?? 0;
const dayNum = parseInt(cell.date.slice(8, 10), 10);
const isToday = cell.date === todayStr;
const background =
secondary > 0
? "rgba(81, 207, 102, 0.08)"
: primary > 0
? "rgba(34, 139, 230, 0.06)"
: undefined;
return (
<Paper
key={i}
p={6}
withBorder
radius="sm"
style={{
minHeight: cellMinHeight,
borderColor: isToday ? "var(--mantine-color-blue-5)" : undefined,
background,
}}
>
<Stack gap={2}>
<Text
size="xs"
fw={isToday ? 700 : 500}
c={isToday ? "blue" : undefined}
>
{dayNum}
</Text>
{primary > 0 && (
<Group gap={3} wrap="nowrap" c={primaryColor}>
{resolvedPrimaryIcon}
<Text size="xs" c={primaryColor}>
{primary}
</Text>
</Group>
)}
{secondary > 0 && (
<Group gap={3} wrap="nowrap" c={secondaryColor}>
{resolvedSecondaryIcon}
<Text size="xs" c={secondaryColor}>
{secondary}
</Text>
</Group>
)}
</Stack>
</Paper>
);
})}
</SimpleGrid>
</Box>
);
}