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:
2026-05-09 03:41:58 +02:00
parent 4d5a5bd3ea
commit 8618aa1be3
58 changed files with 2923 additions and 0 deletions
@@ -0,0 +1,44 @@
---
name: format_datetime_short
kind: function
lang: ts
domain: core
version: "1.0.0"
purity: pure
signature: "formatDateTimeShort(iso: string): string"
description: "Formatea un string ISO datetime a formato corto \"dd/mm/yy hh:mm\". ISO invalido retorna string vacio."
tags: [format, datetime, date, display, utility]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params:
- name: iso
desc: "String ISO 8601 con fecha y hora (ej: \"2026-05-09T14:30:00.000Z\" o \"2026-05-09T14:30:00\"). Cualquier string que `new Date()` no pueda parsear retorna \"\"."
output: "String con formato \"dd/mm/yy hh:mm\" usando la hora local del entorno. String vacio si el ISO es invalido."
tested: true
tests:
- "ISO valido retorna dd/mm/yy hh:mm"
- "string vacio retorna vacio"
- "ISO invalido retorna vacio"
- "padding de dia y mes con cero"
test_file_path: "frontend/functions/core/format_datetime_short.test.ts"
file_path: "frontend/functions/core/format_datetime_short.ts"
---
## Ejemplo
```typescript
formatDateTimeShort("2026-05-09T14:30:00") // "09/05/26 14:30"
formatDateTimeShort("2026-01-01T00:00:00") // "01/01/26 00:00"
formatDateTimeShort("not-a-date") // ""
formatDateTimeShort("") // ""
```
## Notas
Extraida de `apps/kanban/frontend/src/components/format.ts` lineas 32-41.
Usa `Date` nativo del entorno. La hora mostrada es **hora local** del navegador/runtime (getHours, getMinutes).
El ano se trunca a 2 digitos (slice(-2)), correcto para fechas 2000-2099.
@@ -0,0 +1,28 @@
import { describe, it, expect } from "vitest";
import { formatDateTimeShort } from "./format_datetime_short";
// Nota: la funcion usa hora local del entorno (getHours/getMinutes).
// Los tests usan fechas con offset conocido o verifican el patron de formato.
describe("formatDateTimeShort", () => {
it("string vacio retorna vacio", () => {
expect(formatDateTimeShort("")).toBe("");
});
it("ISO invalido retorna vacio", () => {
expect(formatDateTimeShort("not-a-date")).toBe("");
expect(formatDateTimeShort("2026-13-01")).toBe("");
});
it("ISO valido retorna dd/mm/yy hh:mm", () => {
// Verificamos el patron sin asumir timezone: resultado debe tener forma xx/xx/xx xx:xx
const result = formatDateTimeShort("2026-05-09T14:30:00");
expect(result).toMatch(/^\d{2}\/\d{2}\/\d{2} \d{2}:\d{2}$/);
});
it("padding de dia y mes con cero", () => {
// Dia 1, mes enero: debe ser "01/01/..."
const result = formatDateTimeShort("2026-01-01T00:00:00");
expect(result).toMatch(/^01\/01\//);
});
});
@@ -0,0 +1,14 @@
/**
* Formatea un string ISO a "dd/mm/yy hh:mm".
* Retorna "" si el ISO es invalido o no parseable.
*/
export function formatDateTimeShort(iso: string): string {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return "";
const dd = String(d.getDate()).padStart(2, "0");
const mm = String(d.getMonth() + 1).padStart(2, "0");
const yy = String(d.getFullYear()).slice(-2);
const hh = String(d.getHours()).padStart(2, "0");
const mi = String(d.getMinutes()).padStart(2, "0");
return `${dd}/${mm}/${yy} ${hh}:${mi}`;
}
@@ -0,0 +1,59 @@
---
name: format_duration
kind: function
lang: ts
domain: core
version: "1.0.0"
purity: pure
signature: "formatDuration(ms: number): string"
description: "Formatea una duracion en milisegundos a string humano escalado. Escala: m | h Xm | D Xh | S XD | M XS. NaN, Infinity o negativo retornan \"0m\"."
tags: [format, duration, time, display, utility]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params:
- name: ms
desc: "Duracion en milisegundos. Debe ser un numero finito no negativo. NaN, Infinity o negativos producen \"0m\"."
output: "String humano escalado segun magnitud: minutos (Xm), horas+minutos (Xh Ym), dias+horas (XD Yh), semanas+dias (XS YD), meses+semanas (XM YS). Menor de un minuto devuelve \"0m\"."
tested: true
tests:
- "menos de un minuto retorna 0m"
- "exactamente 1 hora retorna 1h"
- "1h 30m retorna 1h 30m"
- "1 dia sin horas retorna 1D"
- "2 dias 3 horas retorna 2D 3h"
- "1 semana sin dias retorna 1S"
- "2 semanas 3 dias retorna 2S 3D"
- "1 mes sin semanas retorna 1M"
- "2 meses 1 semana retorna 2M 1S"
- "NaN retorna 0m"
- "negativo retorna 0m"
- "Infinity retorna 0m"
test_file_path: "frontend/functions/core/format_duration.test.ts"
file_path: "frontend/functions/core/format_duration.ts"
---
## Ejemplo
```typescript
formatDuration(0) // "0m"
formatDuration(30_000) // "0m" (30s < 1min)
formatDuration(90_000) // "1m"
formatDuration(3_600_000) // "1h"
formatDuration(5_400_000) // "1h 30m"
formatDuration(86_400_000) // "1D"
formatDuration(97_200_000) // "1D 3h"
formatDuration(604_800_000) // "1S"
formatDuration(2_592_000_000) // "1M"
formatDuration(NaN) // "0m"
formatDuration(-1) // "0m"
```
## Notas
Extraida de `apps/kanban/frontend/src/components/format.ts` lineas 9-30.
Funcion pura sin dependencias externas. Usa constantes locales privadas (MIN, HOUR, DAY, WEEK, MONTH).
Escala en ingles/abreviada: m=minutos, h=horas, D=dias, S=semanas, M=meses.
@@ -0,0 +1,61 @@
import { describe, it, expect } from "vitest";
import { formatDuration } from "./format_duration";
const MIN = 60_000;
const HOUR = 60 * MIN;
const DAY = 24 * HOUR;
const WEEK = 7 * DAY;
const MONTH = 30 * DAY;
describe("formatDuration", () => {
it("menos de un minuto retorna 0m", () => {
expect(formatDuration(0)).toBe("0m");
expect(formatDuration(30_000)).toBe("0m");
expect(formatDuration(59_999)).toBe("0m");
});
it("exactamente 1 hora retorna 1h", () => {
expect(formatDuration(HOUR)).toBe("1h");
});
it("1h 30m retorna 1h 30m", () => {
expect(formatDuration(HOUR + 30 * MIN)).toBe("1h 30m");
});
it("1 dia sin horas retorna 1D", () => {
expect(formatDuration(DAY)).toBe("1D");
});
it("2 dias 3 horas retorna 2D 3h", () => {
expect(formatDuration(2 * DAY + 3 * HOUR)).toBe("2D 3h");
});
it("1 semana sin dias retorna 1S", () => {
expect(formatDuration(WEEK)).toBe("1S");
});
it("2 semanas 3 dias retorna 2S 3D", () => {
expect(formatDuration(2 * WEEK + 3 * DAY)).toBe("2S 3D");
});
it("1 mes sin semanas retorna 1M", () => {
expect(formatDuration(MONTH)).toBe("1M");
});
it("2 meses 1 semana retorna 2M 1S", () => {
expect(formatDuration(2 * MONTH + WEEK)).toBe("2M 1S");
});
it("NaN retorna 0m", () => {
expect(formatDuration(NaN)).toBe("0m");
});
it("negativo retorna 0m", () => {
expect(formatDuration(-1)).toBe("0m");
expect(formatDuration(-999)).toBe("0m");
});
it("Infinity retorna 0m", () => {
expect(formatDuration(Infinity)).toBe("0m");
});
});
@@ -0,0 +1,36 @@
// Escala unidades segun magnitud: m | h Xm | D Xh | S XD | M XS.
// <1 minuto cae como "0m" para mantener la unidad mas pequena coherente.
const MIN = 60_000;
const HOUR = 60 * MIN;
const DAY = 24 * HOUR;
const WEEK = 7 * DAY;
const MONTH = 30 * DAY;
/**
* Formatea una duracion en milisegundos a string humano escalado.
*
* Escala: m | h Xm | D Xh | S XD | M XS.
* NaN, Infinity o negativo retornan "0m".
*/
export function formatDuration(ms: number): string {
if (!Number.isFinite(ms) || ms < 0) return "0m";
if (ms < HOUR) return `${Math.floor(ms / MIN)}m`;
if (ms < DAY) {
const h = Math.floor(ms / HOUR);
const m = Math.floor((ms % HOUR) / MIN);
return m === 0 ? `${h}h` : `${h}h ${m}m`;
}
if (ms < WEEK) {
const d = Math.floor(ms / DAY);
const h = Math.floor((ms % DAY) / HOUR);
return h === 0 ? `${d}D` : `${d}D ${h}h`;
}
if (ms < MONTH) {
const w = Math.floor(ms / WEEK);
const d = Math.floor((ms % WEEK) / DAY);
return d === 0 ? `${w}S` : `${w}S ${d}D`;
}
const m = Math.floor(ms / MONTH);
const w = Math.floor((ms % MONTH) / WEEK);
return w === 0 ? `${m}M` : `${m}M ${w}S`;
}
+54
View File
@@ -0,0 +1,54 @@
---
name: month_grid
kind: function
lang: ts
domain: core
version: "1.0.0"
purity: pure
signature: "monthGrid(year: number, month: number): { date: string | null; inMonth: boolean }[]"
description: "Genera la cuadricula mensual con inicio en lunes (Mon-first ISO). Retorna array de celdas multiplo de 7 con padding nulo al inicio y final. Util para calendarios y vistas de mes."
tags: [calendar, date, grid, month, utility]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params:
- name: year
desc: "Ano completo del mes a generar (ej: 2026). Rango valido: cualquier ano soportado por Date nativo."
- name: month
desc: "Mes 1-12 donde 1=Enero y 12=Diciembre."
output: "Array de MonthGridCell ({ date: string|null; inMonth: boolean }) con longitud siempre multiplo de 7. Celdas de padding tienen date=null e inMonth=false. Celdas del mes tienen date en formato YYYY-MM-DD e inMonth=true."
tested: true
tests:
- "enero 2026 empieza en jueves (dow=3)"
- "febrero 2024 bisiesto tiene 29 dias"
- "longitud siempre multiplo de 7"
- "primer dia inMonth es el primero del mes"
- "ultimo dia inMonth es el ultimo del mes"
- "celdas de padding tienen date null"
- "mes que empieza en lunes no tiene padding inicial"
test_file_path: "frontend/functions/core/month_grid.test.ts"
file_path: "frontend/functions/core/month_grid.ts"
---
## Ejemplo
```typescript
const cells = monthGrid(2026, 5); // Mayo 2026 (1 mayo = viernes => firstDow=4)
// cells.length es multiplo de 7 (ej: 35)
// cells[0..3] => { date: null, inMonth: false } (padding lun-jue)
// cells[4] => { date: "2026-05-01", inMonth: true }
// cells[34] => { date: "2026-05-31", inMonth: true }
monthGrid(2026, 1); // Enero 2026: jueves=dow 3, 3 celdas padding inicial
```
## Notas
Extraida de `apps/kanban/frontend/src/components/CalendarView.tsx` lineas 72-84.
Reemplaza la dependencia de `dayjs` por `Date` nativo para mantener la funcion sin dependencias externas.
Algoritmo: `firstDow = (new Date(year, month-1, 1).getDay() + 6) % 7` convierte domingo=0 de JS a lunes=0 ISO.
`new Date(year, month, 0).getDate()` obtiene el ultimo dia del mes (dia 0 del mes siguiente = ultimo del actual).
Exporta tambien el tipo `MonthGridCell` para uso con TypeScript estricto.
@@ -0,0 +1,55 @@
import { describe, it, expect } from "vitest";
import { monthGrid } from "./month_grid";
describe("monthGrid", () => {
it("longitud siempre multiplo de 7", () => {
for (let m = 1; m <= 12; m++) {
expect(monthGrid(2026, m).length % 7).toBe(0);
}
});
it("enero 2026 empieza en jueves (dow=3)", () => {
// 1 enero 2026 es jueves. Mon-first => dow=3 (0=lun,1=mar,2=mie,3=jue)
const cells = monthGrid(2026, 1);
expect(cells[0].date).toBeNull();
expect(cells[1].date).toBeNull();
expect(cells[2].date).toBeNull();
expect(cells[3].date).toBe("2026-01-01");
expect(cells[3].inMonth).toBe(true);
});
it("febrero 2024 bisiesto tiene 29 dias", () => {
const cells = monthGrid(2024, 2);
const inMonth = cells.filter((c) => c.inMonth);
expect(inMonth).toHaveLength(29);
expect(inMonth[28].date).toBe("2024-02-29");
});
it("primer dia inMonth es el primero del mes", () => {
const cells = monthGrid(2026, 5);
const first = cells.find((c) => c.inMonth);
expect(first?.date).toBe("2026-05-01");
});
it("ultimo dia inMonth es el ultimo del mes", () => {
const cells = monthGrid(2026, 5); // mayo tiene 31 dias
const last = [...cells].reverse().find((c) => c.inMonth);
expect(last?.date).toBe("2026-05-31");
});
it("celdas de padding tienen date null", () => {
const cells = monthGrid(2026, 1);
const padding = cells.filter((c) => !c.inMonth);
for (const p of padding) {
expect(p.date).toBeNull();
}
});
it("mes que empieza en lunes no tiene padding inicial", () => {
// Buscamos un mes que empiece en lunes. Enero 2024: lunes.
// 1 enero 2024 es lunes => firstDow=0 => no padding inicial
const cells = monthGrid(2024, 1);
expect(cells[0].date).toBe("2024-01-01");
expect(cells[0].inMonth).toBe(true);
});
});
+40
View File
@@ -0,0 +1,40 @@
export interface MonthGridCell {
date: string | null;
inMonth: boolean;
}
/**
* Genera la cuadricula mensual con inicio en lunes (ISO week).
*
* @param year Año completo (ej: 2026).
* @param month Mes 1-12 (Enero = 1).
* @returns Array de celdas con longitud multiplo de 7.
* Celdas de padding tienen date=null e inMonth=false.
* Celdas del mes tienen date="YYYY-MM-DD" e inMonth=true.
*/
export function monthGrid(year: number, month: number): MonthGridCell[] {
// getDay() devuelve 0=Dom..6=Sab. Convertir a 0=Lun..6=Dom.
const firstDow = (new Date(year, month - 1, 1).getDay() + 6) % 7;
const lastDay = new Date(year, month, 0).getDate();
const cells: MonthGridCell[] = [];
// Padding inicial
for (let i = 0; i < firstDow; i++) {
cells.push({ date: null, inMonth: false });
}
// Dias del mes
for (let d = 1; d <= lastDay; d++) {
const mm = String(month).padStart(2, "0");
const dd = String(d).padStart(2, "0");
cells.push({ date: `${year}-${mm}-${dd}`, inMonth: true });
}
// Padding final hasta multiplo de 7
while (cells.length % 7 !== 0) {
cells.push({ date: null, inMonth: false });
}
return cells;
}
@@ -0,0 +1,51 @@
---
name: string_hash_palette
kind: function
lang: ts
domain: core
version: "1.0.0"
purity: pure
signature: "stringHashPalette(s: string, palette: string[]): string"
description: "Mapea un string a un elemento de la paleta usando hash DJB-31 deterministico (h = (h*31 + charCodeAt) >>> 0). Util para asignar colores consistentes a tags, usuarios u otros identificadores de texto."
tags: [hash, color, palette, string, deterministic, utility]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params:
- name: s
desc: "String de entrada a hashear. Cualquier string Unicode. String vacio produce h=0, retorna palette[0]."
- name: palette
desc: "Array de strings (colores u otros valores) a usar como paleta. No debe estar vacio (lanza Error si lo esta)."
output: "Un elemento de palette seleccionado deterministicamente por h % palette.length. El mismo string siempre retorna el mismo elemento para la misma paleta."
tested: true
tests:
- "string vacio retorna palette[0]"
- "mismo string siempre retorna mismo color"
- "strings distintos pueden dar colores distintos"
- "paleta vacia lanza error"
- "paleta de un elemento siempre retorna ese elemento"
test_file_path: "frontend/functions/core/string_hash_palette.test.ts"
file_path: "frontend/functions/core/string_hash_palette.ts"
---
## Ejemplo
```typescript
const TAG_PALETTE = ["blue", "cyan", "teal", "green", "lime", "yellow", "orange", "red", "pink", "grape", "violet", "indigo"];
stringHashPalette("frontend", TAG_PALETTE) // deterministico, ej: "teal"
stringHashPalette("backend", TAG_PALETTE) // deterministico, diferente
stringHashPalette("frontend", TAG_PALETTE) // mismo resultado siempre
stringHashPalette("x", []) // throws Error("palette must not be empty")
```
## Notas
Extraida de `apps/kanban/frontend/src/components/colors.ts` lineas 70-76 (funcion `tagColor`).
Generalizada para aceptar cualquier paleta en vez de la constante `TAG_PALETTE` hardcodeada.
El hash DJB-31 (variante de DJB2 con multiplicador 31) produce distribucion uniforme suficiente para asignacion de colores. No es criptografico.
El operador `>>> 0` garantiza entero de 32 bits sin signo, evitando overflow en strings largos.
@@ -0,0 +1,33 @@
import { describe, it, expect } from "vitest";
import { stringHashPalette } from "./string_hash_palette";
const PALETTE = ["blue", "cyan", "teal", "green", "lime", "yellow", "orange", "red", "pink", "grape", "violet", "indigo"];
describe("stringHashPalette", () => {
it("string vacio retorna palette[0]", () => {
// h empieza en 0, loop no itera, h%n = 0 => palette[0]
expect(stringHashPalette("", PALETTE)).toBe(PALETTE[0]);
});
it("mismo string siempre retorna mismo color", () => {
const a = stringHashPalette("frontend", PALETTE);
const b = stringHashPalette("frontend", PALETTE);
expect(a).toBe(b);
});
it("strings distintos pueden dar colores distintos", () => {
const colors = new Set(["alpha", "beta", "gamma", "delta", "epsilon"].map((s) => stringHashPalette(s, PALETTE)));
// Con 5 strings distintos y paleta de 12 es muy probable obtener al menos 2 distintos
expect(colors.size).toBeGreaterThan(1);
});
it("paleta vacia lanza error", () => {
expect(() => stringHashPalette("hello", [])).toThrow("palette must not be empty");
});
it("paleta de un elemento siempre retorna ese elemento", () => {
expect(stringHashPalette("anything", ["only"])).toBe("only");
expect(stringHashPalette("", ["only"])).toBe("only");
expect(stringHashPalette("xyz", ["only"])).toBe("only");
});
});
@@ -0,0 +1,16 @@
/**
* Mapea un string a un color de la paleta usando hash DJB-31 deterministico.
*
* Hash: h = (h * 31 + charCodeAt(i)) >>> 0 (unsigned 32-bit).
* El indice es h % palette.length.
*
* @throws Error si palette esta vacio.
*/
export function stringHashPalette(s: string, palette: string[]): string {
if (palette.length === 0) throw new Error("palette must not be empty");
let h = 0;
for (let i = 0; i < s.length; i++) {
h = (h * 31 + s.charCodeAt(i)) >>> 0;
}
return palette[h % palette.length];
}