Compare commits

..
10 Commits
Author SHA1 Message Date
luriasb 85cf67c1b5 Update 02-kids-list-and-profile.md 2026-10-23 00:52:21 -06:00
luriasb cc0180087c paso 7 2026-10-20 11:44:44 -06:00
luriasb ea81e01a0a paso 6 2026-10-20 11:20:49 -06:00
luriasb 0ff530aa22 paso 5 2026-10-20 11:16:58 -06:00
luriasb edcba1a7e4 paso 4 2026-10-20 11:16:21 -06:00
luriasb e2bfbab713 paso 03 2026-10-20 11:10:49 -06:00
luriasb 4ab73ade40 paso 02 2026-10-20 08:39:46 -06:00
luriasb b9be17db97 step 01 2026-10-20 08:35:47 -06:00
luriasb 787522e4ca spec 02 aprobada 2026-10-20 02:30:01 -06:00
luriasb f19fbc5c5d add spec verifier 2026-10-20 02:23:16 -06:00
11 changed files with 781 additions and 15 deletions
+67
View File
@@ -0,0 +1,67 @@
---
description: Verifica, corrige y marca los criterios de aceptación de un spec (specs/*.md) usando Context7 (docs Next.js) y Playwright (comparación visual de pantallas con visión)
mode: all
model: zai-coding-plan/glm-5.3-flash
---
Eres el agente verificador de criterios de aceptación de specs de este proyecto (OpenDayCare, Next.js). Tu misión: revisar, corregir y marcar los checks de la sección "Criterios de aceptación" de un archivo de especificación en `specs/`. Un check se marca **solo con verificación real**, nunca por suposición.
# Flujo de trabajo
## 1. Leer el spec
- Si no se indica cuál, lista los archivos en `specs/` y pregunta cuál verificar.
- Lee el spec completo: "Alcance" (In / Out of scope), "Modelo de datos", "Plan de implementación", "Criterios de aceptación" y "Decisiones".
- No amplíes el alcance: lo que está en "Out of scope" no se verifica ni se implementa, aunque lo detectes.
## 2. Verificar cada criterio
Clasifica cada criterio `- [ ]` según su tipo y verifica con la herramienta adecuada:
### Criterios de Next.js / código
- Usa **Context7** para validar contra la documentación actual de Next.js: primero `resolve-library-id` con "Next.js", luego `query-docs` con el concepto específico del criterio (fuentes con `next/font`, layout, metadata, `Link`, etc.). No confíes en tu memoria: las APIs pueden haber cambiado.
- Lee además la guía relevante en `node_modules/next/dist/docs/`: esta versión del proyecto puede tener breaking changes respecto a tu entrenamiento.
- Revisa el código fuente real en `app/` con read/grep/glob y compáralo con lo que exige el criterio.
### Criterios de pantallas / UI
- Levanta el dev server si no está corriendo (`npm run dev` en background) y espera a que compile.
- Navega con el MCP de Playwright hasta la página implicada.
- **Todos los screenshots y artefactos de Playwright van OBLIGATORIAMENTE en `.playwright-mcp/`** (regla del AGENTS.md del proyecto).
- Toma screenshot a ancho ≥1280px (los specs piden desktop) y compáralo **visualmente** contra la plantilla de referencia: abre `references/pantallas/*.html` con Playwright y captúrala también.
- Usa tu capacidad de visión: lee ambas imágenes y compara concretamente:
- Layout y dimensiones (ej. sidebar de 248px sticky, contenido centrado de 760px, según el spec).
- Colores hex exactos que mencione el spec (fondos, badges, bordes, textos).
- Tipografías y pesos, textos, badges, contadores, iconos, placeholders.
- Solo sentencia una diferencia si la identificaste en la imagen; describe qué y dónde antes de corregir.
### Criterios de build / runtime
- `npm run build` debe completar sin errores.
- `npm run dev` debe compilar y servir la página sin errores de compilación, consola ni hidratación.
## 3. Corregir
- Si un criterio falla, corrige el código respetando las "Decisiones" del spec y las convenciones del repo (ej.: solo utilities Tailwind con valores arbitrarios exactos, sin atributos `style`, `<Link>` para toda navegación, fuentes vía `next/font/google`).
- Tras corregir, **re-verifica el criterio completo** con evidencia nueva (no vale la corrección sin re-test).
## 4. Marcar
- Cambia `- [ ]` → `- [x]` en el archivo del spec **solo** cuando el criterio quedó verificado con evidencia: comando exitoso, screenshot comparado o docs consultadas.
- Si un criterio no se puede verificar (depende de algo out of scope, o falla por algo ajeno al spec), déjalo sin marcar y explícalo en el reporte.
- No edits ninguna otra sección del spec salvo que el usuario lo pida explícitamente.
## 5. Reportar
Cierra siempre con un resumen por criterio:
- ✅ **Verificado**: criterio + evidencia (comando, screenshot en `.playwright-mcp/`, docs).
- 🔧 **Corregido**: qué fallaba, qué cambiaste (archivos) y cómo quedó verificado.
- ⛔ **Bloqueado**: motivo exacto y qué hace falta para desbloquear.
# Reglas del proyecto (AGENTS.md)
- Screenshots y cualquier salida de Playwright → carpeta `.playwright-mcp/`.
- Context7 es la fuente de documentación de frameworks; úsalo siempre para Next.js.
- Lee `node_modules/next/dist/docs/` antes de escribir o corregir código Next.js: puede diferir de lo que conoces.
+14
View File
@@ -0,0 +1,14 @@
---
description: Verifica, corrige y marca los criterios de aceptación de un spec usando el agente spec-verifier (Context7 + Playwright con visión)
agent: spec-verifier
---
Verifica los criterios de aceptación del spec: $ARGUMENTS
Reglas de ejecución:
- Si `$ARGUMENTS` viene vacío, lista los specs disponibles en `specs/` y pregunta cuál verificar. No continúes sin respuesta.
- Sigue tu flujo completo del agente: clasificar y verificar cada criterio (Context7 + `node_modules/next/dist/docs/` para Next.js, Playwright con screenshots en `.playwright-mcp/` para pantallas, `npm run dev` / `npm run build` para build/runtime).
- Corrige lo que falle respetando el "Alcance" y las "Decisiones" del spec, re-verifica y recién entonces marca el check (`- [ ]` → `- [x]`).
- Si TODOS los criterios quedan verificados, actualiza además el **Estado** del spec a "Implementada" (o el equivalente usado en el archivo).
- Cierra con el reporte ✅ verificados · 🔧 corregidos · ⛔ bloqueados (con evidencia y motivo).
+29
View File
@@ -12,3 +12,32 @@ This block is written and re-added by `next dev` — verify at `node_modules/nex
- Playwright Scheenshots y cualquier cosa relacionada a Playwright tiene que estar en la carpeta .playwright-mcp. - Playwright Scheenshots y cualquier cosa relacionada a Playwright tiene que estar en la carpeta .playwright-mcp.
- Context7 Usaremos este MCP para traer la documentación actualizada del framework. - Context7 Usaremos este MCP para traer la documentación actualizada del framework.
## Workflow de specs (spec-driven)
El proyecto usa un flujo spec-driven con tres pasos. Cada spec vive en `specs/NN-slug.md` y tiene un **Estado** (`Draft` → `Approved` → `Implementada`):
| Paso | Comando | Definición |
| --- | --- | --- |
| 1. Diseñar | `/spec <descripción>` | Skill en `.agents/skills/spec/` — redacta el spec en `specs/NN-slug.md` (estado `Draft`) |
| 2. Implementar | `/spec-impl <NN-slug>` | Skill en `.agents/skills/spec-impl/` — crea la rama `spec-NN-slug` e implementa paso a paso |
| 3. Verificar | `/spec-verify <NN-slug>` | Comando en `.opencode/commands/spec-verify.md` — ejecuta el agente `spec-verifier` sobre el spec |
### Agente `spec-verifier`
Definición: `.opencode/agents/spec-verifier.md` (a nivel de proyecto, no global).
- **Modelo:** `zai-coding-plan/glm-5.3-flash` (soporta visión, necesario para comparar screenshots). **Modo:** `all` (agente principal o subagente).
- **Qué hace:** revisa, corrige y marca (`- [ ]` → `- [x]`) los "Criterios de aceptación" de un spec en `specs/`.
- **Criterios de Next.js/código** → Context7 (docs actualizadas de Next.js) + guías locales en `node_modules/next/dist/docs/`.
- **Criterios de pantallas/UI** → Playwright con screenshots obligatoriamente en `.playwright-mcp/`, comparación visual (visión) contra las plantillas de `references/pantallas/*.html` a ancho ≥1280px.
- **Criterios de build/runtime** → `npm run dev` / `npm run build` con salida limpia.
- **Reglas:** nunca marca un check sin evidencia real; nunca implementa nada que esté en "Out of scope" del spec; corrige respetando las "Decisiones" del spec.
- **Uso directo:** seleccionarlo con Tab como agente principal, o lanzarlo como subagente ("usa spec-verifier para verificar el spec 01").
### Comando `/spec-verify`
- Ejecuta el agente `spec-verifier` sobre el spec indicado: `/spec-verify 01-feed-home` (también acepta solo el número o el slug).
- Si se invoca sin argumentos, lista los specs de `specs/` y pregunta cuál verificar.
- Si todos los criterios quedan verificados, actualiza el **Estado** del spec a "Implementada".
- Cierra siempre con un reporte por criterio: ✅ verificados · 🔧 corregidos · ⛔ bloqueados (con evidencia y motivo).
+69
View File
@@ -0,0 +1,69 @@
import Link from "next/link";
import type { Kid } from "@/app/data/kids";
/**
* Colores dinámicos → utilities Tailwind con valores exactos.
* El set de pares del mock es cerrado; nunca se usan atributos style.
*/
const AVATAR_COLORS: Record<string, string> = {
"A9D9E8-1F7A93": "bg-[#A9D9E8] text-[#1F7A93]",
"F4B8CC-C44A7A": "bg-[#F4B8CC] text-[#C44A7A]",
"B9DEC4-3E8B62": "bg-[#B9DEC4] text-[#3E8B62]",
"F4DC8E-9A7B1E": "bg-[#F4DC8E] text-[#9A7B1E]",
"C9B6E8-7B5FC0": "bg-[#C9B6E8] text-[#7B5FC0]",
};
export function avatarClasses(kid: Kid): string {
const key = `${kid.avatar.bg.replace("#", "")}-${kid.avatar.text.replace("#", "")}`;
return AVATAR_COLORS[key] ?? "";
}
/** "2 padres vinculados" | "1 padre vinculado" | "sin padres vinculados". */
function parentsLabel(kid: Kid): string {
if (kid.parents.length === 0) return "sin padres vinculados";
return `${kid.parents.length} ${kid.parents.length === 1 ? "padre" : "padres"} vinculados`;
}
export default function KidCard({ kid }: { kid: Kid }) {
return (
<Link
href={`/kids/${kid.slug}`}
className="flex min-w-0 items-center gap-[14px] rounded-[18px] border border-[#ECE0D0] bg-[#FFFDF9] p-4 shadow-[0_4px_14px_-12px_rgba(120,90,60,.5)] transition duration-150 hover:-translate-y-[2px] hover:border-[#F2A78E]"
>
<div
className={`flex h-12 w-12 flex-none items-center justify-center rounded-full font-display text-[19px] font-semibold ${avatarClasses(kid)}`}
>
{kid.initial}
</div>
<div className="min-w-0 flex-1">
<div className="truncate font-display text-base font-semibold text-[#3F362E]">{kid.name}</div>
<div className="text-[13px] text-[#A89A8B]">
{kid.age} años · {parentsLabel(kid)}
</div>
</div>
{kid.parents.length === 0 ? (
<span className="flex-none rounded-full bg-[#F9D2DE] px-[9px] py-[5px] text-[11px] font-extrabold text-[#C56486]">
VINCULAR
</span>
) : kid.allergies.length > 0 ? (
<span className="flex-none rounded-full bg-[#FBD8CC] px-[9px] py-[5px] text-[11px] font-extrabold text-[#D9684A]">
{kid.allergies[0]}
</span>
) : (
<svg
className="flex-none"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="#CBB89F"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m9 18 6-6-6-6" />
</svg>
)}
</Link>
);
}
+67
View File
@@ -0,0 +1,67 @@
"use client";
import { useState } from "react";
import type { Kid } from "@/app/data/kids";
import KidCard from "@/app/components/KidCard";
/** Compara sin mayúsculas ni acentos: "sofia" encuentra a "Sofía Méndez". */
function normalize(value: string): string {
return value
.normalize("NFD")
.replace(/\p{Diacritic}/gu, "")
.toLowerCase();
}
export default function KidsBrowser({ kids }: { kids: Kid[] }) {
const [query, setQuery] = useState("");
const filtered = kids.filter((kid) => normalize(kid.name).includes(normalize(query)));
return (
<div>
{/* Buscador */}
<div className="mb-[22px] flex items-center gap-[11px] rounded-[14px] border border-[#ECE0D0] bg-[#FFFDF9] px-4 py-3">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="#B0A290"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="11" cy="11" r="7" />
<path d="m21 21-4.3-4.3" />
</svg>
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Buscar niño…"
className="flex-1 border-none bg-transparent text-[15px] text-[#3F362E] outline-none placeholder:text-[#B6A99B]"
/>
</div>
{/* Encabezado de sección: sala + contador + divisor */}
<div className="mb-[14px] flex items-center gap-3">
<span className="text-[12.5px] font-extrabold tracking-[.8px] text-[#3F362E]">SALA SOLES</span>
<span className="text-[13px] text-[#A89A8B]">
{filtered.length} {filtered.length === 1 ? "niño" : "niños"}
</span>
<span className="h-px flex-1 bg-[#E7DAC8]" />
</div>
{/* Grilla filtrada o estado vacío */}
{filtered.length > 0 ? (
<div className="grid grid-cols-2 gap-[14px]">
{filtered.map((kid) => (
<KidCard key={kid.slug} kid={kid} />
))}
</div>
) : (
<div className="rounded-[18px] border border-dashed border-[#DBCDBA] bg-[#FFFDF9] px-4 py-10 text-center text-[14.5px] text-[#94887B]">
No hay niños que coincidan con la búsqueda.
</div>
)}
</div>
);
}
+1 -1
View File
@@ -17,7 +17,7 @@ const NAV_ITEMS: NavItem[] = [
), ),
}, },
{ {
href: "/ninos", href: "/kids",
label: "Niños", label: "Niños",
icon: ( icon: (
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+151
View File
@@ -0,0 +1,151 @@
export type ParentStatus = "activa" | "pendiente";
export interface Parent {
name: string;
/** "Mamá" | "Papá" */
role: string;
/** Inicial para el avatar circular. */
initial: string;
/** Color de fondo del avatar, exacto a la plantilla. */
bg: string;
/** Texto tras "Rol · " en el perfil: "activa" | "invitación enviada". */
note: string;
/** Define el badge ACTIVA (verde) / PENDIENTE (amarillo). */
status: ParentStatus;
}
export interface Kid {
slug: string;
name: string;
/** Inicial para el avatar circular. */
initial: string;
/** Colores del avatar, exactos a la plantilla. */
avatar: { bg: string; text: string };
/** Se renderiza como "X años". */
age: number;
/** Badges de la lista; vacío = chevron. La primera se muestra en la tarjeta. */
allergies: string[];
/** Texto del recuadro naranja del perfil (si no hay, no se muestra). */
allergyNote?: string;
birthDate: string;
room: string;
admission: string;
parents: Parent[];
}
export const kids: Kid[] = [
{
slug: "mateo-fernandez",
name: "Mateo Fernández",
initial: "M",
avatar: { bg: "#A9D9E8", text: "#1F7A93" },
age: 3,
allergies: ["MANÍ"],
allergyNote: "Alergia al maní. Evitar frutos secos. Lleva inhalador en la mochila.",
birthDate: "12 mar 2022",
room: "Soles",
admission: "feb 2025",
parents: [
{ name: "Lucía Fernández", role: "Mamá", initial: "L", bg: "#C9B6E8", note: "activa", status: "activa" },
{ name: "Diego Fernández", role: "Papá", initial: "D", bg: "#A9C7E8", note: "invitación enviada", status: "pendiente" },
],
},
{
slug: "sofia-mendez",
name: "Sofía Méndez",
initial: "S",
avatar: { bg: "#F4B8CC", text: "#C44A7A" },
age: 2,
allergies: [],
birthDate: "3 jul 2023",
room: "Soles",
admission: "mar 2025",
parents: [
{ name: "Valeria Méndez", role: "Mamá", initial: "V", bg: "#F4B8CC", note: "activa", status: "activa" },
],
},
{
slug: "benjamin-ruiz",
name: "Benjamín Ruiz",
initial: "B",
avatar: { bg: "#B9DEC4", text: "#3E8B62" },
age: 3,
allergies: [],
birthDate: "28 nov 2022",
room: "Soles",
admission: "ago 2024",
parents: [
{ name: "Marcela Ruiz", role: "Mamá", initial: "M", bg: "#B9DEC4", note: "activa", status: "activa" },
{ name: "Javier Ruiz", role: "Papá", initial: "J", bg: "#A9C7E8", note: "activa", status: "activa" },
],
},
{
slug: "valentina-soto",
name: "Valentina Soto",
initial: "V",
avatar: { bg: "#F4DC8E", text: "#9A7B1E" },
age: 2,
allergies: [],
birthDate: "9 may 2023",
room: "Soles",
admission: "feb 2025",
parents: [],
},
{
slug: "tomas-diaz",
name: "Tomás Díaz",
initial: "T",
avatar: { bg: "#C9B6E8", text: "#7B5FC0" },
age: 3,
allergies: ["LACTOSA"],
allergyNote: "Intolerancia a la lactosa. Toma leche deslactosada y evita los lácteos en la merienda.",
birthDate: "17 sep 2022",
room: "Soles",
admission: "sep 2023",
parents: [
{ name: "Andrés Díaz", role: "Papá", initial: "A", bg: "#C9B6E8", note: "activa", status: "activa" },
],
},
{
slug: "emma-castro",
name: "Emma Castro",
initial: "E",
avatar: { bg: "#F4B8CC", text: "#C44A7A" },
age: 2,
allergies: [],
birthDate: "2 ene 2023",
room: "Soles",
admission: "mar 2025",
parents: [
{ name: "Camila Castro", role: "Mamá", initial: "C", bg: "#F2937A", note: "activa", status: "activa" },
],
},
{
slug: "lucas-romero",
name: "Lucas Romero",
initial: "L",
avatar: { bg: "#A9D9E8", text: "#1F7A93" },
age: 3,
allergies: [],
birthDate: "30 abr 2022",
room: "Soles",
admission: "feb 2024",
parents: [
{ name: "Martín Romero", role: "Papá", initial: "M", bg: "#A9C7E8", note: "activa", status: "activa" },
],
},
{
slug: "olivia-vega",
name: "Olivia Vega",
initial: "O",
avatar: { bg: "#B9DEC4", text: "#3E8B62" },
age: 2,
allergies: [],
birthDate: "21 oct 2023",
room: "Soles",
admission: "ago 2025",
parents: [
{ name: "Julieta Vega", role: "Mamá", initial: "J", bg: "#F4DC8E", note: "activa", status: "activa" },
],
},
];
+212
View File
@@ -0,0 +1,212 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import Sidebar from "@/app/components/Sidebar";
import { avatarClasses } from "@/app/components/KidCard";
import type { Kid, Parent, ParentStatus } from "@/app/data/kids";
import { kids } from "@/app/data/kids";
/** Página 100% estática que deriva de params: permitido bloquear, sin Suspense. */
export const instant = false;
/** Los 8 slugs del mock se prerenderizan; cualquier otro cae en notFound(). */
export function generateStaticParams() {
return kids.map((kid) => ({ slug: kid.slug }));
}
/**
* Colores dinámicos → utilities Tailwind con valores exactos.
* El set de fondos del mock es cerrado; nunca se usan atributos style.
*/
const PARENT_BG: Record<string, string> = {
C9B6E8: "bg-[#C9B6E8]",
A9C7E8: "bg-[#A9C7E8]",
F4B8CC: "bg-[#F4B8CC]",
B9DEC4: "bg-[#B9DEC4]",
F2937A: "bg-[#F2937A]",
F4DC8E: "bg-[#F4DC8E]",
};
const STATUS_BADGE: Record<ParentStatus, { label: string; classes: string }> = {
activa: { label: "ACTIVA", classes: "bg-[#CFEBD8] text-[#3E9B6C]" },
pendiente: { label: "PENDIENTE", classes: "bg-[#F7E7A6] text-[#9A7B1E]" },
};
function parentClasses(parent: Parent): string {
return PARENT_BG[parent.bg.replace("#", "")] ?? "";
}
function ParentRow({ parent }: { parent: Parent }) {
const badge = STATUS_BADGE[parent.status];
return (
<div className="flex items-center gap-3">
<div
className={`flex h-10 w-10 flex-none items-center justify-center rounded-full font-display text-base font-semibold text-white ${parentClasses(parent)}`}
>
{parent.initial}
</div>
<div className="min-w-0 flex-1">
<div className="text-[14.5px] font-extrabold text-[#3F362E]">{parent.name}</div>
<div className="text-[12.5px] text-[#A89A8B]">
{parent.role} · {parent.note}
</div>
</div>
<span className={`flex-none rounded-full px-[9px] py-1 text-[10.5px] font-extrabold ${badge.classes}`}>
{badge.label}
</span>
</div>
);
}
export default async function KidProfilePage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const kid: Kid | undefined = kids.find((candidate) => candidate.slug === slug);
if (!kid) notFound();
return (
<div className="flex min-h-screen bg-[#F6ECDF]">
<Sidebar activeHref="/kids" />
<main className="h-screen min-w-0 flex-1 overflow-y-auto">
<div className="mx-auto w-full max-w-[820px] px-10 pb-20 pt-[34px]">
{/* Breadcrumb */}
<Link
href="/kids"
className="mb-5 flex items-center gap-[7px] text-[14px] font-bold text-[#94887B]"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m15 18-6-6 6-6" />
</svg>
Volver a Niños
</Link>
<div className="flex flex-wrap items-start gap-[26px]">
{/* Columna izquierda */}
<div className="flex min-w-[300px] flex-1 flex-col gap-[18px]">
{/* Identidad */}
<div className="flex items-center gap-[18px]">
<div
className={`flex h-[84px] w-[84px] flex-none items-center justify-center rounded-full font-display text-[34px] font-semibold ${avatarClasses(kid)}`}
>
{kid.initial}
</div>
<div className="flex-1">
<h1 className="font-display text-[28px] font-semibold leading-[1.2] text-[#3F362E]">{kid.name}</h1>
<p className="mt-[3px] text-[15px] text-[#94887B]">
{kid.age} años · Sala {kid.room}
</p>
</div>
<Link
href="/agregar-nino"
className="rounded-[12px] border-[1.5px] border-[#ECE0D0] bg-[#FFFDF9] px-4 py-[9px] text-sm font-bold text-[#6E6359]"
>
Editar
</Link>
</div>
{/* Alergias y notas (solo si tiene) */}
{kid.allergyNote && (
<div className="flex gap-[14px] rounded-[16px] bg-[#FBDAD6] px-[18px] py-4">
<div className="flex h-10 w-10 flex-none items-center justify-center rounded-[11px] bg-[#F4A8A0]">
<svg
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="#fff"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z" />
<path d="M12 9v4M12 17h.01" />
</svg>
</div>
<div>
<div className="mb-[2px] text-[15px] font-extrabold text-[#C5413A]">Alergias y notas</div>
<div className="text-[14.5px] leading-[1.5] text-[#B25249]">{kid.allergyNote}</div>
</div>
</div>
)}
{/* Datos */}
<div className="overflow-hidden rounded-[16px] border border-[#ECE0D0] bg-[#FFFDF9]">
<div className="flex justify-between border-b border-[#F0E6D8] px-[18px] py-[15px]">
<span className="text-[14.5px] text-[#94887B]">Fecha de nacimiento</span>
<span className="text-[14.5px] font-extrabold text-[#3F362E]">{kid.birthDate}</span>
</div>
<div className="flex justify-between border-b border-[#F0E6D8] px-[18px] py-[15px]">
<span className="text-[14.5px] text-[#94887B]">Sala</span>
<span className="text-[14.5px] font-extrabold text-[#3F362E]">{kid.room}</span>
</div>
<div className="flex justify-between px-[18px] py-[15px]">
<span className="text-[14.5px] text-[#94887B]">Ingreso</span>
<span className="text-[14.5px] font-extrabold text-[#3F362E]">{kid.admission}</span>
</div>
</div>
</div>
{/* Columna derecha */}
<div className="flex w-[300px] flex-none flex-col gap-[14px]">
<Link
href="/resumen-dia"
className="flex w-full items-center justify-center gap-[9px] rounded-[14px] bg-[#3F362E] py-[13px] text-[15px] font-extrabold text-white"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="#fff"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
</svg>
Resumen del día
</Link>
{/* Padres vinculados */}
<div className="rounded-[16px] border border-[#ECE0D0] bg-[#FFFDF9] px-[18px] py-4">
<div className="mb-[14px] text-[12.5px] font-extrabold tracking-[.8px] text-[#8A7C6D]">
PADRES VINCULADOS
</div>
<div className="flex flex-col gap-[14px]">
{kid.parents.map((parent) => (
<ParentRow key={parent.name} parent={parent} />
))}
<Link href="/vincular-padre" className="flex items-center gap-3 pt-2">
<span className="flex h-10 w-10 flex-none items-center justify-center rounded-full border-[1.5px] border-dashed border-[#D8CBBA] text-[#B0A290]">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12 5v14M5 12h14" />
</svg>
</span>
<span className="text-[14.5px] font-extrabold text-[#C5503A]">Vincular otro padre</span>
</Link>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
);
}
+43
View File
@@ -0,0 +1,43 @@
import Link from "next/link";
import KidsBrowser from "@/app/components/KidsBrowser";
import Sidebar from "@/app/components/Sidebar";
import { kids } from "@/app/data/kids";
export default function KidsPage() {
return (
<div className="flex min-h-screen bg-[#F6ECDF]">
<Sidebar activeHref="/kids" />
<main className="h-screen min-w-0 flex-1 overflow-y-auto">
<div className="mx-auto w-full max-w-[880px] px-10 pb-20 pt-[34px]">
{/* Encabezado */}
<div className="mb-[22px] flex items-end justify-between gap-4">
<div>
<div className="mb-1 text-[12.5px] font-extrabold tracking-[.8px] text-[#D9583C]">GESTIÓN</div>
<h1 className="font-display text-[30px] font-semibold leading-[1.2] text-[#3F362E]">Niños</h1>
</div>
<Link
href="/agregar-nino"
className="flex items-center gap-2 rounded-[14px] bg-[linear-gradient(180deg,#F4977E,#EE8164)] px-[18px] py-[11px] text-[14.5px] font-extrabold text-white shadow-[0_8px_18px_-8px_rgba(238,129,100,.7)]"
>
<svg
width="17"
height="17"
viewBox="0 0 24 24"
fill="none"
stroke="#fff"
strokeWidth="2.4"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12 5v14M5 12h14" />
</svg>
Agregar niño
</Link>
</div>
<KidsBrowser kids={kids} />
</div>
</main>
</div>
);
}
+14 -14
View File
@@ -1,6 +1,6 @@
# SPEC 01 — Home: feed de la guardería (plantilla feed.dc.html) # SPEC 01 — Home: feed de la guardería (plantilla feed.dc.html)
> **Estado:** Implementada > **Estado:** Verificada (2026-10-20)
> **Depende de:** — > **Depende de:** —
> **Fecha:** 2026-10-19 > **Fecha:** 2026-10-19
> **Objetivo:** Implementar la plantilla `references/pantallas/feed.dc.html` como página home (`/`) en Next.js, visualmente idéntica al original, sin autenticación ni base de datos. > **Objetivo:** Implementar la plantilla `references/pantallas/feed.dc.html` como página home (`/`) en Next.js, visualmente idéntica al original, sin autenticación ni base de datos.
@@ -9,12 +9,12 @@
**In:** **In:**
- Página `/` (`app/page.tsx`) con: sidebar fijo de 248px (logo OpenDayCare · Sala Soles, botón "Nueva publicación", nav con Feed activo + Niños/Avisos/Mi cuenta, tarjeta de usuario "Caro Giménez" con logout), saludo ("GUARDERÍA · SALA SOLES" / "Buenas, Caro" / "12 niños · martes 17 jun"), caja "Compartí un momento…", separador "PUBLICADO HOY" y los 3 posts de la plantilla (logro, actividad con placeholder de foto, anuncio). - Página `/` (`app/page.tsx`) con: sidebar sticky de 248px (logo OpenDayCare · Sala Soles, botón "Nueva publicación", nav con Feed activo + Niños/Avisos/Mi cuenta, tarjeta de usuario "Caro Giménez" con logout), saludo ("GUARDERÍA · SALA SOLES" / "Buenas, Caro" / "12 niños · martes 17 jun"), caja "Compartí un momento…", separador "PUBLICADO HOY" y los 3 posts de la plantilla (logro, actividad con placeholder de foto, anuncio).
- Datos mock tipados en `app/data/feed.ts` con los 3 posts exactos de la plantilla. - Datos mock tipados en `app/data/feed.ts` con los 3 posts exactos de la plantilla.
- Componentes en `app/components/`: `Sidebar.tsx`, `FeedHeader.tsx`, `ShareBox.tsx`, `PostCard.tsx`. - Componentes en `app/components/`: `Sidebar.tsx`, `FeedHeader.tsx`, `ShareBox.tsx`, `PostCard.tsx`.
- Cambio de fuentes en `app/layout.tsx` (Fredoka + Nunito vía `next/font/google`), `lang="es"` y metadata "OpenDayCare". - Cambio de fuentes en `app/layout.tsx` (Fredoka + Nunito vía `next/font/google`), `lang="es"` y metadata "OpenDayCare".
- Estilos base en `app/globals.css` (fondo #F6ECDF, texto #3F362E, scrollbar custom, tema claro fijo). - Estilos base en `app/globals.css` (fondo #F6ECDF, texto #3F362E, scrollbar custom, tema claro fijo).
- Todos los links como `<Link>` a rutas futuras (`/crear-publicacion`, `/ninos`, `/avisos`, `/mi-cuenta`, `/detalle-publicacion`, `/login`). - Todos los links como `<Link>` a rutas futuras (`/crear-publicacion`, `/ninos`, `/avisos`, `/mi-cuenta`, `/detalle-publicacion`, `/login`, `/foto` — este último en el placeholder de foto, como `foto.dc.html` en la plantilla).
**Out of scope (para specs futuras):** **Out of scope (para specs futuras):**
@@ -66,17 +66,17 @@ Convenciones: los colores van como hex exactos de la plantilla; el mapa `categor
## Criterios de aceptación ## Criterios de aceptación
- [ ] `npm run dev` levanta sin errores y `/` muestra el feed completo: sidebar + saludo + caja + separador + 3 posts. - [x] `npm run dev` levanta sin errores y `/` muestra el feed completo: sidebar + saludo + caja + separador + 3 posts. *(Verificado 2026-10-20 en `localhost:3001` a 1440×900; consola sin errores de compilación ni hidratación. Capturas: `.playwright-mcp/verify-feed-app-1440.png`, `.playwright-mcp/verify-feed-app-bottom.png`.)*
- [ ] `npm run build` completa sin errores. - [x] `npm run build` completa sin errores. *(Next.js 16.4.2 + Turbopack: compilación, TypeScript y prerender de `/` OK.)*
- [ ] A ancho ≥1280px el layout coincide con la plantilla: sidebar 248px sticky, main con scroll propio y contenido centrado de 760px. - [x] A ancho ≥1280px el layout coincide con la plantilla: sidebar 248px sticky, main con scroll propio y contenido centrado de 760px. *(Computed styles: `aside` 248px sticky top-0 h-screen; `main` h-screen overflow-y auto; contenedor max-width 760px centrado — márgenes 211px/211px a 1440px.)*
- [ ] Fredoka en logo, saludo y nombres de autor; Nunito (pesos 400–800) en el resto. - [x] Fredoka en logo, saludo y nombres de autor; Nunito (pesos 400–800) en el resto. *(Computed: Fredoka 17px/600 logo, 30px/600 h1, 16.5px/600 autores; Nunito 400 texto, 600 nav inactivo, 700 likes, 800 eyebrow/nav activo. Fuentes variables self-hosted vía `next/font/google`.)*
- [ ] Post 1: badge LOGRO (fondo `#CFEBD8`, texto `#3E9B6C`), ♥ 3, 💬 1, link "Editar". - [x] Post 1: badge LOGRO (fondo `#CFEBD8`, texto `#3E9B6C`), ♥ 3, 💬 1, link "Editar". *(Computed: `rgb(207,235,216)` / `rgb(62,155,108)`; acciones 3 / 1 / Editar → `/crear-publicacion`.)*
- [ ] Post 2: badge ACTIVIDAD (fondo `#C7E7F1`, texto `#2E89A6`), placeholder dashed "Foto · pintando con témperas", ♥ 5, 💬 2. - [x] Post 2: badge ACTIVIDAD (fondo `#C7E7F1`, texto `#2E89A6`), placeholder dashed "Foto · pintando con témperas", ♥ 5, 💬 2. *(Computed: `rgb(199,231,241)` / `rgb(46,137,166)`; placeholder dashed 1.5px `#DBCDBA`, 200px alto, link a `/foto`; acciones 5 / 2.)*
- [ ] Post 3: avatar megáfono (fondo `#CCD8F4`), badge ANUNCIO, "Para: toda la sala", ♥ 8, 💬 0. - [x] Post 3: avatar megáfono (fondo `#CCD8F4`), badge ANUNCIO, "Para: toda la sala", ♥ 8, 💬 0. *(Computed: avatar 44px `rgb(204,216,244)` con SVG megáfono idéntico a la plantilla; badge `rgb(204,216,244)` / `rgb(78,114,200)`; acciones 8 / 0.)*
- [ ] Todos los links navegan con `<Link>`: `/crear-publicacion` (botón, caja y Editar), `/ninos`, `/avisos`, `/mi-cuenta`, `/detalle-publicacion` (íconos de comentarios), `/login` (logout). - [x] Todos los links navegan con `<Link>`: `/crear-publicacion` (botón, caja y Editar), `/ninos`, `/avisos`, `/mi-cuenta`, `/detalle-publicacion` (íconos de comentarios), `/login` (logout), `/foto` (placeholder de foto). *(Hrefs del DOM verificados; `next/link` importado en Sidebar, ShareBox y PostCard; sin `<a>` planos.)*
- [ ] Colores base exactos: fondo `#F6ECDF`, tarjetas `#FFFDF9` con borde `#ECE0D0`, sombras y radios según plantilla. - [x] Colores base exactos: fondo `#F6ECDF`, tarjetas `#FFFDF9` con borde `#ECE0D0`, sombras y radios según plantilla. *(Computed: body `rgb(246,236,223)`; tarjetas `rgb(255,253,249)`, borde 1px `rgb(236,224,208)`, radio 20px, sombra `rgba(120,90,60,.5) 0 4px 16px -12px`; caja 18px.)*
- [ ] Con el sistema en modo oscuro la página se mantiene en tema claro (sin dark mode). - [x] Con el sistema en modo oscuro la página se mantiene en tema claro (sin dark mode). *(Emulación `prefers-color-scheme: dark` en Playwright: body, aside y tarjetas mantienen sus colores claros. Captura: `.playwright-mcp/verify-feed-darkmode.png`.)*
- [ ] Los componentes nuevos no usan atributos `style`; todo está en utilities Tailwind con valores arbitrarios exactos. - [x] Los componentes nuevos no usan atributos `style`; todo está en utilities Tailwind con valores arbitrarios exactos. *(Grep `style=` en `app/**/*.{tsx,css}`: 0 resultados; los únicos `[style]` del DOM son internos de Next.js — `nextjs-portal`, `next-route-announcer` — fuera de los componentes.)*
## Decisiones ## Decisiones
+114
View File
@@ -0,0 +1,114 @@
# SPEC 02 — Niños: listado y perfil (`/kids` y `/kids/[slug]`)
> **Estado:** Verificada (2026-10-20)
> **Depende de:** SPEC 01
> **Fecha:** 2026-10-20
> **Objetivo:** Implementar las plantillas `references/pantallas/ninos.dc.html` y `references/pantallas/perfil-nino.dc.html` como `/kids` y `/kids/[slug]`, visualmente idénticas, con mock de 8 niños con ficha completa y buscador funcional.
## Alcance
**In:**
- Página `/kids` (`app/kids/page.tsx`): sidebar con "Niños" activo, encabezado (eyebrow "GESTIÓN" + título "Niños"), botón "Agregar niño" (→ `/agregar-nino`), buscador y grilla de 2 columnas con las 8 tarjetas de la plantilla.
- Buscador **funcional** (filtro client-side): filtra por nombre mientras se escribe, el contador "8 niños" se actualiza y muestra estado vacío si no hay coincidencias.
- Página `/kids/[slug]` (`app/kids/[slug]/page.tsx`): breadcrumb "Volver a Niños" (→ `/kids`), avatar 84px + nombre + "3 años · Sala Soles", botón "Editar" (→ `/agregar-nino`), recuadro naranja "Alergias y notas" (solo si el niño tiene notas), tarjeta de datos (nacimiento / sala / ingreso), columna derecha con botón "Resumen del día" (→ `/resumen-dia`), tarjeta "PADRES VINCULADOS" (badges ACTIVA/PENDIENTE) y link "Vincular otro padre" (→ `/vincular-padre`).
- Las 8 fichas de perfil existen: cada slug renderiza los datos propios del niño (nombre, avatar, alergias, fechas y padres), coherentes con la lista.
- Mock tipado en `app/data/kids.ts` con los 8 niños de la plantilla (Mateo con los datos exactos del perfil; los otros 7 con datos inventados coherentes con su fila de la lista).
- Componentes nuevos en `app/components/`: `KidCard.tsx` (tarjeta de la lista) y `KidsBrowser.tsx` (cliente: buscador + contador + grilla + estado vacío).
- Update de `app/components/Sidebar.tsx`: el link "Niños" pasa de `/ninos` a `/kids` (única referencia existente; SPEC 01 la cableó así).
- Hover de tarjeta según plantilla: borde `#F2A78E` + `translateY(-2px)` con transición de 150ms.
**Out of scope (para specs futuras):**
- Páginas `/agregar-nino`, `/vincular-padre` y `/resumen-dia` (hoy dan 404 y está bien).
- Alta/edición real de niños y vinculación real de padres.
- Autenticación, base de datos y persistencia.
- Responsive móvil y dark mode.
## Modelo de datos
```ts
// app/data/kids.ts
export type ParentStatus = "activa" | "pendiente";
export interface Parent {
name: string; // "Lucía Fernández"
role: string; // "Mamá" | "Papá"
initial: string; // "L"
bg: string; // "#C9B6E8"
status: ParentStatus; // badge ACTIVA (verde) | PENDIENTE (amarillo)
}
export interface Kid {
slug: string; // "mateo-fernandez"
name: string; // "Mateo Fernández"
initial: string; // "M"
avatar: { bg: string; text: string }; // { bg: "#A9D9E8", text: "#1F7A93" }
age: number; // 3 → se renderiza "3 años"
allergies: string[]; // ["MANÍ"] — badges de la lista (vacos = sin badge)
allergyNote?: string; // texto del recuadro del perfil (si existe → se muestra)
birthDate: string; // "12 mar 2022"
room: string; // "Soles"
admission: string; // "feb 2025"
parents: Parent[];
}
```
- Los 8 niños con sus colores exactos de la plantilla: Mateo `#A9D9E8/#1F7A93`, Sofía `#F4B8CC/#C44A7A`, Benjamín `#B9DEC4/#3E8B62`, Valentina `#F4DC8E/#9A7B1E`, Tomás `#C9B6E8/#7B5FC0`, Emma `#F4B8CC/#C44A7A`, Lucas `#A9D9E8/#1F7A93`, Olivia `#B9DEC4/#3E8B62`.
- Regla del badge lateral en la lista (deducida de la plantilla): sin padres → badge "VINCULAR" (`#F9D2DE`/`#C56486`); con alergia → primera alergia (`#FBD8CC`/`#D9684A`); resto → chevron `#CBB89F`.
- Padres de Mateo (exactos de la plantilla): Lucía Fernández (Mamá, `#C9B6E8`, activa) y Diego Fernández (Papá, `#A9C7E8`, pendiente "invitación enviada").
Convenciones heredadas de SPEC 01: colores como hex exactos en utilities Tailwind arbitrarias; cero atributos `style`; todo server components salvo `KidsBrowser`.
## Plan de implementación
1. **Mock** — crear `app/data/kids.ts` con los tipos y los 8 niños. Test manual: `npm run dev` compila.
2. **KidCard** — crear `app/components/KidCard.tsx`: avatar circular, nombre, "X años · N padre(s) vinculado(s)", badge según la regla y link a `/kids/[slug]` con hover de plantilla. Test manual: se ve correcta suelta en cualquier página temporal.
3. **KidsBrowser (cliente)** — crear `app/components/KidsBrowser.tsx`: input "Buscar niño…", fila "SALA SOLES + contador + línea divisoria", grilla `grid-cols-2` filtrada por nombre (comparación sin mayúsculas ni acentos vía `normalize("NFD")`) y mensaje de estado vacío.
4. **Página /kids** — crear `app/kids/page.tsx`: Sidebar `activeHref="/kids"`, encabezado, botón "Agregar niño" y `<KidsBrowser kids={kids}>`. Test manual: `http://localhost:3000/kids` muestra las 8 tarjetas y el filtro funciona.
5. **Sidebar** — en `app/components/Sidebar.tsx` cambiar el href de "Niños" de `/ninos` a `/kids`. Test manual: el link navega y ninguna página queda rota.
6. **Página /kids/[slug]** — crear `app/kids/[slug]/page.tsx` con `generateStaticParams` y `notFound()`: en Next.js 16 `params` es una Promise (`const { slug } = await params`). Layout de dos columnas (columna izquierda flexible + derecha fija de 300px), ficha completa, recuadro de alergias condicionado y tarjeta de padres con badges ACTIVA/PENDIENTE. Test manual: `/kids/mateo-fernandez` replica la plantilla y `/kids/valentina-soto` muestra su ficha sin recuadro de alergias ni padres.
7. **Fidelidad visual** — comparación lado a lado de ambas pantallas contra las plantillas a ancho ≥1280px; ajustar diferencias. Capturas de verificación en `.playwright-mcp/`.
## Criterios de aceptación
- [x] `npm run dev` levanta sin errores y `/kids` muestra eyebrow "GESTIÓN", título "Niños", botón "Agregar niño", buscador y las 8 tarjetas en grilla de 2 columnas. *(Verificado 2026-10-20 en `localhost:3001` a 1440×900; consola limpia tras el opt-out `instant = false`. Computed: h1 "Niños" Fredoka 30px, `grid-template-columns: 393px 393px`, 8 tarjetas. Captura: `.playwright-mcp/verify-kids-final.png`.)*
- [x] `npm run build` completa sin errores, incluyendo el prerender de `/kids` y de los 8 slugs. *(Next.js 16.4.2 + Turbopack: compilación y TypeScript OK, 14 páginas estáticas — `/kids` ○ y los 8 slugs listados como estáticos.)*
- [x] Escribir "sof" en el buscador deja solo la tarjeta de Sofía Méndez, el contador se actualiza y escribir algo sin coincidencias muestra el estado vacío. *(Computed: "sof" → 1 tarjeta "Sofía Méndez" y contador "1 niño"; "zzzqx" → 0 tarjetas, mensaje "No hay niños que coincidan con la búsqueda." y "0 niños".)*
- [x] Escribir "sofia" (sin acento) también encuentra a Sofía Méndez. *(Computed: "sofia" sin acento devuelve exactamente esa tarjeta.)*
- [x] Cada tarjeta enlaza con `<Link>` al slug de su niño (8 hrefs distintos) y el badge es correcto: Mateo "MANÍ", Tomás "LACTOSA", Valentina "VINCULAR", resto chevron. *(Computed: 8 hrefs `/kids/<slug>` únicos; badges correctos; `next/link` importado en `KidCard` y 0 `<a>` planos en los archivos nuevos.)*
- [x] `/kids/mateo-fernandez` replica la plantilla: breadcrumb, avatar 84px "M", nombre, "3 años · Sala Soles", botón "Editar", recuadro "Alergias y notas" con el texto exacto ("Alergia al maní. Evitar frutos secos. Lleva inhalador en la mochila."), filas nacimiento/sala/ingreso, botón "Resumen del día" y tarjeta "PADRES VINCULADOS" con Lucía (ACTIVA, verde) y Diego (PENDIENTE, amarillo) + "Vincular otro padre". *(Computed: breadcrumb → `/kids`, avatar 84×84 Fredoka "M", texto de alergias literal, filas "12 mar 2022 / Soles / feb 2025", badges `rgb(207,235,216)`/`rgb(62,155,108)` y `rgb(247,231,166)`/`rgb(154,123,30)`. Captura: `.playwright-mcp/verify-perfil-final.png`; comparación visual contra `perfil-nino.dc.html` sin diferencias.)*
- [x] Los otros 7 slugs muestran la ficha propia de cada niño (nombre, avatar, edad, padres y alergias coherentes con la lista); Valentina no tiene recuadro de alergias ni padres listados. *(Fetch del HTML SSR de cada slug: nombre, inicial de avatar y edad·sala correctos; recuadro de alergias solo en Tomás; filas de padres 1/2/0/1/1/1/1 según la lista; Valentina sin recuadro y sin badges.)*
- [x] `/kids/no-existe` responde 404. *(HTTP 404 real con página de not-found; patrón `generateStaticParams` + `notFound()` tras `await params` validado contra la doc de Next.js (Context7) y `node_modules/next/dist/docs/`.)*
- [x] En el sidebar, "Niños" apunta a `/kids` y aparece activo (fondo `#FBE3D8`, texto `#D9583C`) tanto en `/kids` como en `/kids/[slug]`; en `/` sigue activo "Feed". *(Computed en `/kids`, `/kids/tomas-diaz` y `/`: activo "Niños" con `rgb(251,227,216)`/`rgb(217,88,60)` en lista y ficha; en `/` el activo es "Feed" y "Niños" queda inactivo `rgb(110,99,89)`.)*
- [x] Los links futuros del perfil navegan con `<Link>`: Editar → `/agregar-nino`, Resumen del día → `/resumen-dia`, Vincular otro padre → `/vincular-padre` (hoy 404, aceptado). *(Computed: los 3 hrefs presentes en el DOM vía `next/link`; destinos 404 según lo acordado.)*
- [x] A ancho ≥1280px ambas pantallas coinciden con las plantillas: lista con contenido centrado de 880px y perfil de 820px con columna derecha de 300px. *(Computed a 1440px: lista `max-width: 880px` centrada (left 404 = 248 de sidebar + 156), perfil 820px y columna 300px. Comparación visual lado a lado contra `ninos.dc.html` y `perfil-nino.dc.html` sin diferencias: `.playwright-mcp/spec02-ref-ninos.png` vs `.playwright-mcp/spec02-app-ninos-v2.png`, `.playwright-mcp/spec02-ref-perfil.png` vs `.playwright-mcp/spec02-app-perfil.png`.)*
- [x] Los archivos nuevos no usan atributos `style`; todo es Tailwind con valores arbitrarios exactos. *(Grep `style=` en `app/`: 0 resultados; `tsc --noEmit` y `eslint` limpios sobre los 6 archivos.)*
## Decisiones
- **Sí:** rutas `/kids` y `/kids/[slug]` — decisión explícita del usuario aunque el sidebar decía `/ninos`; se actualiza `Sidebar.tsx` (única referencia, así ningún criterio de SPEC 01 queda huérfano sin notarlo).
- **Sí:** los 8 niños con ficha completa en `app/data/kids.ts` — el perfil es una página por slug real, no un placeholder clonado.
- **Sí:** datos inventados para los otros 7 perfiles, pero coherentes con la lista (misma edad, misma cantidad de padres, mismas alergias); solo Mateo conserva los datos literales de la plantilla.
- **Sí:** buscador funcional con un único componente cliente (`KidsBrowser`) — es la primera pieza interactiva del repo y queda aislada; el resto sigue siendo server components.
- **Sí:** búsqueda solo por nombre, insensible a mayúsculas y acentos — la plantilla dice "Buscar niño…" y en español los acentos son el caso de uso obvio.
- **Sí:** regla de badge deducida de la plantilla (sin padres → VINCULAR, con alergia → alergia, resto → chevron) — los 3 casos existen en el diseño original.
- **Sí:** recuadro de alergias solo cuando `allergyNote` existe — la plantilla no define cómo se ve un perfil sin alergias y omitirlo es lo más fiel.
- **Sí:** perfil compuesto directo en `page.tsx` sin componentes de un solo uso — solo se extraen `KidCard` y `KidsBrowser`, que se reutilizan o aíslan estado.
- **Sí:** `generateStaticParams` + `notFound()` — mock estático: los 8 slugs se prerenderizan y cualquier otro slug da 404 real.
- **No:** filtrar por sala, edad o alergia — no está en la plantilla.
- **No:** páginas `/agregar-nino`, `/vincular-padre` ni `/resumen-dia` — specs futuras.
## Riesgos
| Riesgo | Mitigación |
| --- | --- |
| El buscador cliente rompe la convención server-only del repo | `KidsBrowser` aísla el estado; `KidCard` sigue siendo puro y reutilizable |
| Búsqueda lenta o hidratación inconsistente al filtrar | Filtrado en memoria sobre 8 items; el grid se deriva del mismo array tipado |
| Links a páginas inexistentes confunden en la verificación | Documentado en Out of scope y en criterios: 404 esperado, igual que SPEC 01 |
## Qué **no** está en esta spec
- Páginas agregar/editar niño, vincular padre y resumen del día — cada una en su propia spec.
- Autenticación, base de datos y persistencia.
- Responsive móvil y dark mode.