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>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user