Files
recordalexia/artifacts/App de rutinas visuales para TDAH/Rutinas TDAH.dc.html
Jaume Garriga Maestre 52e559a159 feat: app completa recordaLexia (fases 1-5)
App web familiar de rutinas visuales para niños con TDAH: muestra cada día el
material del cole y las rutinas de tarde, con gamificación por monedas y tienda
de recompensas. Multi-niño y bilingüe ES/CA. Uso doméstico/homelab.

Backend (Spring Boot 3.5 / Java 21 / Gradle):
- Dominio por capas, PostgreSQL + Liquibase, datos semilla.
- API REST con DTOs: /today, toggle con monedas y bonos de bloque/día, monedero,
  tienda/canje, ajustes y CRUD del panel de padres.
- Seguridad ligera por PIN (BCrypt + sesion en memoria), sin Keycloak.
- Tests JUnit: generacion del dia, monedas/bonos con reversion, canje, seguridad.

Frontend (Angular 19, standalone + signals):
- Perfiles, Home (Tablero y Foco), Tienda y panel de padres (5 pestañas).
- Tipografia OpenDyslexic conmutable (accesibilidad), i18n ES/CA, TTS y sonido.
- Tokens de diseño fieles al handoff (paleta, animaciones, monedas voladoras).

Empaquetado:
- Docker multi-stage + docker-compose (PostgreSQL + backend + Nginx).
- Decisiones de arquitectura documentadas en docs/adr.
2026-06-21 10:48:57 +02:00

631 lines
52 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Nunito:wght@400;600;700;800;900&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body { margin:0; padding:0; height:100%; background:#EAF1F4; font-family:'Nunito',sans-serif; overflow:hidden; }
@keyframes pop { 0%{transform:scale(1)} 40%{transform:scale(1.06)} 100%{transform:scale(1)} }
@keyframes checkPop { 0%{transform:scale(0)} 60%{transform:scale(1.25)} 100%{transform:scale(1)} }
@keyframes floatY { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-8px)} }
@keyframes floatYb { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-14px)} }
@keyframes walletBump { 0%{transform:scale(1)} 35%{transform:scale(1.22) rotate(-4deg)} 100%{transform:scale(1)} }
@keyframes confFall { 0%{transform:translateY(-20vh) rotate(0)} 100%{transform:translateY(110vh) rotate(720deg)} }
@keyframes shake { 0%,100%{transform:translateX(0)} 20%{transform:translateX(-8px)} 40%{transform:translateX(8px)} 60%{transform:translateX(-6px)} 80%{transform:translateX(6px)} }
@keyframes celebPop { 0%{transform:scale(.6);opacity:0} 60%{transform:scale(1.05)} 100%{transform:scale(1);opacity:1} }
@keyframes ringGlow { 0%,100%{box-shadow:0 0 0 0 rgba(242,166,90,.35)} 50%{box-shadow:0 0 0 14px rgba(242,166,90,0)} }
@keyframes slideUp { 0%{transform:translateY(30px);opacity:0} 100%{transform:translateY(0);opacity:1} }
::-webkit-scrollbar{ width:10px;height:10px } ::-webkit-scrollbar-thumb{ background:#cfd9e0;border-radius:8px }
</style>
</helmet>
<div style="position:fixed;inset:0;font-family:'Nunito',sans-serif;color:#2A3142;overflow:hidden;background:radial-gradient(1100px 700px at 12% -10%, #FBF4E9 0%, transparent 55%), radial-gradient(1000px 700px at 110% 120%, #E2F0EC 0%, transparent 55%), #EFF4F6;">
<!-- soft organic blobs -->
<div style="position:absolute;top:-120px;left:-100px;width:420px;height:420px;border-radius:48% 52% 60% 40%/55% 45% 60% 45%;background:#FCEBD3;opacity:.55;filter:blur(2px);"></div>
<div style="position:absolute;bottom:-140px;right:-90px;width:460px;height:460px;border-radius:60% 40% 45% 55%/50% 55% 45% 50%;background:#D7ECE5;opacity:.6;filter:blur(2px);"></div>
<!-- ============ PROFILES ============ -->
<sc-if value="{{ isProfiles }}" hint-placeholder-val="{{ true }}">
<div style="position:relative;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:48px;padding:40px;">
<div style="text-align:center;animation:slideUp .5s ease both;">
<div style="font-size:60px;animation:floatY 3.5s ease-in-out infinite;">🌳</div>
<div style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:46px;letter-spacing:.5px;margin-top:6px;">{{ L.who }}</div>
</div>
<div style="display:flex;gap:36px;flex-wrap:wrap;justify-content:center;">
<sc-for list="{{ profiles }}" as="p" hint-placeholder-count="3">
<div style="{{ p.cardStyle }}">
<button onClick="{{ p.select }}" style="all:unset;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:14px;">
<div style="{{ p.ringStyle }}"><span style="font-size:78px;line-height:1;">{{ p.mascot }}</span></div>
<div style="font-family:'Fredoka',sans-serif;font-weight:600;font-size:30px;letter-spacing:.5px;">{{ p.name }}</div>
<div style="display:flex;align-items:center;gap:7px;background:#FFF6E0;color:#C7912B;border-radius:999px;padding:6px 16px;font-weight:800;font-size:18px;">🪙 {{ p.coins }}</div>
</button>
<div style="display:flex;align-items:center;gap:10px;margin-top:14px;background:#F4F7F9;border-radius:999px;padding:6px 8px;">
<button onClick="{{ p.ageDown }}" style="all:unset;cursor:pointer;width:34px;height:34px;border-radius:50%;background:#fff;display:flex;align-items:center;justify-content:center;font-weight:900;color:#7A879B;box-shadow:0 2px 5px rgba(0,0,0,.06);"></button>
<span style="font-weight:800;font-size:15px;color:#5A6B82;min-width:64px;text-align:center;">{{ p.ageLabel }}</span>
<button onClick="{{ p.ageUp }}" style="all:unset;cursor:pointer;width:34px;height:34px;border-radius:50%;background:#fff;display:flex;align-items:center;justify-content:center;font-weight:900;color:#7A879B;box-shadow:0 2px 5px rgba(0,0,0,.06);">+</button>
</div>
</div>
</sc-for>
</div>
<button onClick="{{ goPin }}" style="all:unset;cursor:pointer;display:flex;align-items:center;gap:9px;color:#8C99AB;font-weight:700;font-size:16px;background:rgba(255,255,255,.6);padding:10px 18px;border-radius:999px;">⚙️ {{ L.padres }}</button>
</div>
</sc-if>
<!-- ============ HOME ============ -->
<sc-if value="{{ isHome }}" hint-placeholder-val="{{ true }}">
<div style="position:relative;height:100%;display:flex;flex-direction:column;padding:22px 30px 88px 30px;">
<!-- header -->
<div style="display:flex;align-items:center;gap:20px;flex:none;">
<div style="flex:1;">
<div style="font-family:'Nunito';font-weight:800;font-size:17px;color:#8C99AB;letter-spacing:.5px;">{{ L.hola }}, {{ profile.name }}! 👋</div>
<div style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:42px;line-height:1;margin-top:2px;">{{ dayName }} <span style="color:#9FB0BD;font-weight:500;font-size:30px;">{{ dayDate }}</span></div>
</div>
<!-- timer -->
<div style="display:flex;align-items:center;gap:14px;background:#fff;border-radius:24px;padding:12px 20px 12px 14px;box-shadow:0 8px 22px rgba(40,60,100,.07);animation:ringGlow 2.6s ease-in-out infinite;">
<div style="position:relative;width:74px;height:74px;border-radius:50%;background:{{ timerRing }};display:flex;align-items:center;justify-content:center;">
<div style="width:56px;height:56px;border-radius:50%;background:#fff;display:flex;align-items:center;justify-content:center;font-size:30px;">🐦</div>
</div>
<div style="line-height:1.05;">
<div style="font-weight:800;font-size:13px;color:#9FB0BD;letter-spacing:.5px;">{{ L.salimos }}</div>
<div style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:30px;color:#E08A3C;">{{ timerMin }} <span style="font-size:16px;color:#C99B6A;">{{ L.min }}</span></div>
</div>
</div>
<!-- wallet -->
<div ref="{{ setWallet }}" style="{{ walletStyle }}">
<span style="font-size:26px;">🪙</span>
<span style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:30px;">{{ coins }}</span>
</div>
<button onClick="{{ goStore }}" style="all:unset;cursor:pointer;width:58px;height:58px;border-radius:20px;background:#fff;box-shadow:0 8px 22px rgba(40,60,100,.07);display:flex;align-items:center;justify-content:center;font-size:28px;">🎁</button>
</div>
<!-- global progress -->
<div style="display:flex;align-items:center;gap:16px;margin:16px 0 14px;flex:none;">
<div style="flex:1;height:18px;background:#E2EAEE;border-radius:999px;overflow:hidden;">
<div style="height:100%;width:{{ globalPct }};background:linear-gradient(90deg,#7FBF6B,#5BC0BE);border-radius:999px;transition:width .5s cubic-bezier(.2,.8,.2,1);"></div>
</div>
<div style="font-family:'Fredoka',sans-serif;font-weight:600;font-size:20px;color:#5A6B82;white-space:nowrap;">{{ globalDone }}/{{ globalTotal }} {{ L.listo }} ✨</div>
</div>
<!-- exam / homework alert (in-flow banner) -->
<sc-if value="{{ hasEvents }}" hint-placeholder-val="{{ false }}">
<div style="display:flex;gap:12px;margin-bottom:14px;flex:none;">
<sc-for list="{{ events }}" as="e" hint-placeholder-count="2">
<div onClick="{{ e.speak }}" style="cursor:pointer;display:flex;align-items:center;gap:12px;background:{{ e.bg }};border:3px solid {{ e.border }};border-radius:18px;padding:10px 18px;box-shadow:0 6px 16px rgba(212,140,40,.12);">
<span style="font-size:30px;">{{ e.icon }}</span>
<div style="line-height:1.05;">
<div style="font-weight:900;font-size:11px;letter-spacing:1px;color:{{ e.border }};">{{ e.kind }}</div>
<div style="font-family:'Fredoka',sans-serif;font-weight:600;font-size:19px;color:#5a4326;">{{ e.title }}</div>
</div>
</div>
</sc-for>
</div>
</sc-if>
<!-- EMPTY STATE -->
<sc-if value="{{ showEmpty }}" hint-placeholder-val="{{ false }}">
<div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;text-align:center;">
<div style="font-size:96px;animation:floatYb 3s ease-in-out infinite;">🏖️</div>
<div style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:40px;">{{ L.vacioT }}</div>
<div style="font-size:24px;color:#7A879B;font-weight:700;">{{ L.vacioS }}</div>
</div>
</sc-if>
<!-- VARIANT A : board -->
<sc-if value="{{ showA }}" hint-placeholder-val="{{ false }}">
<div style="flex:1;display:flex;gap:22px;min-height:0;">
<!-- COLE -->
<div style="flex:1.05;display:flex;flex-direction:column;min-height:0;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;">
<span style="font-size:30px;">🎒</span>
<span style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:25px;">{{ L.cole }}</span>
<span style="margin-left:auto;font-weight:800;color:#7A879B;font-size:16px;background:#fff;padding:5px 13px;border-radius:999px;">{{ coleDone }}/{{ coleTotal }}</span>
</div>
<div style="flex:1;overflow:auto;display:grid;grid-template-columns:1fr 1fr;gap:14px;align-content:start;padding-right:4px;">
<sc-for list="{{ coleItems }}" as="item" hint-placeholder-count="6">
<div ref="{{ item.setRef }}" onClick="{{ item.toggle }}" style="{{ item.cardStyle }}">
<div style="{{ item.tileStyle }}">{{ item.icon }}</div>
<div style="flex:1;min-width:0;"><div style="{{ item.labelStyle }}">{{ item.label }}</div></div>
<button onClick="{{ item.speak }}" style="all:unset;cursor:pointer;width:34px;height:34px;border-radius:50%;background:#F2F6F8;display:flex;align-items:center;justify-content:center;font-size:16px;flex:none;">🔊</button>
<div style="{{ item.checkStyle }}">{{ item.check }}</div>
</div>
</sc-for>
</div>
</div>
<!-- TARDE -->
<div style="flex:1;display:flex;flex-direction:column;min-height:0;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;">
<span style="font-size:30px;">🌙</span>
<span style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:25px;">{{ L.tarde }}</span>
<span style="margin-left:auto;font-weight:800;color:#7A879B;font-size:16px;background:#fff;padding:5px 13px;border-radius:999px;">{{ tardeDone }}/{{ tardeTotal }}</span>
</div>
<div style="flex:1;overflow:auto;display:flex;flex-direction:column;gap:14px;padding-right:4px;">
<sc-for list="{{ tardeItems }}" as="item" hint-placeholder-count="5">
<div ref="{{ item.setRef }}" onClick="{{ item.toggle }}" style="{{ item.cardStyle }}">
<div style="{{ item.tileStyle }}">{{ item.icon }}</div>
<div style="flex:1;min-width:0;"><div style="{{ item.labelStyle }}">{{ item.label }}</div></div>
<button onClick="{{ item.speak }}" style="all:unset;cursor:pointer;width:34px;height:34px;border-radius:50%;background:#F2F6F8;display:flex;align-items:center;justify-content:center;font-size:16px;flex:none;">🔊</button>
<div style="{{ item.checkStyle }}">{{ item.check }}</div>
</div>
</sc-for>
</div>
</div>
</div>
</sc-if>
<!-- VARIANT B : focus -->
<sc-if value="{{ showB }}" hint-placeholder-val="{{ false }}">
<div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:0;gap:22px;">
<div style="display:flex;align-items:center;gap:10px;background:{{ focus.tint }};color:{{ focus.color }};padding:9px 22px;border-radius:999px;font-family:'Fredoka',sans-serif;font-weight:700;font-size:22px;">{{ focus.blockIcon }} {{ focus.blockLabel }}</div>
<div style="display:flex;align-items:center;gap:38px;">
<button onClick="{{ bPrev }}" style="{{ bPrevStyle }}"></button>
<div style="display:flex;flex-direction:column;align-items:center;gap:20px;animation:celebPop .35s ease both;">
<div style="{{ focus.heroTile }}">{{ focus.icon }}</div>
<div style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:46px;text-align:center;text-wrap:balance;max-width:560px;line-height:1.05;">{{ focus.label }}</div>
<div style="display:flex;gap:16px;align-items:center;">
<button onClick="{{ focus.speak }}" style="all:unset;cursor:pointer;width:62px;height:62px;border-radius:50%;background:#fff;box-shadow:0 6px 18px rgba(40,60,100,.1);display:flex;align-items:center;justify-content:center;font-size:28px;">🔊</button>
<button onClick="{{ bDone }}" style="{{ bDoneStyle }}">{{ focus.doneLabel }}</button>
</div>
</div>
<button onClick="{{ bNext }}" style="{{ bNextStyle }}"></button>
</div>
<div style="display:flex;gap:10px;margin-top:8px;">
<sc-for list="{{ dots }}" as="d" hint-placeholder-count="11">
<div style="{{ d.style }}"></div>
</sc-for>
</div>
<div style="font-weight:800;color:#7A879B;font-size:19px;">{{ L.quedan }} {{ remaining }} · {{ L.despues }}: {{ nextLabel }}</div>
</div>
</sc-if>
</div>
</sc-if>
<!-- ============ STORE ============ -->
<sc-if value="{{ isStore }}" hint-placeholder-val="{{ false }}">
<div style="position:relative;height:100%;display:flex;flex-direction:column;padding:24px 36px 36px;">
<div style="display:flex;align-items:center;gap:18px;margin-bottom:22px;flex:none;">
<button onClick="{{ goHome }}" style="all:unset;cursor:pointer;width:54px;height:54px;border-radius:18px;background:#fff;box-shadow:0 6px 16px rgba(40,60,100,.08);display:flex;align-items:center;justify-content:center;font-size:24px;"></button>
<div style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:38px;flex:1;">🎁 {{ L.tienda }}</div>
<div style="display:flex;align-items:center;gap:8px;background:#FFF6E0;color:#C7912B;border-radius:999px;padding:10px 22px;font-family:'Fredoka';font-weight:700;font-size:30px;">🪙 {{ coins }}</div>
</div>
<div style="flex:1;overflow:auto;display:grid;grid-template-columns:repeat(3,1fr);gap:20px;align-content:start;">
<sc-for list="{{ rewards }}" as="r" hint-placeholder-count="6">
<div style="{{ r.cardStyle }}">
<div style="{{ r.tileStyle }}">{{ r.icon }}</div>
<div style="font-family:'Fredoka',sans-serif;font-weight:600;font-size:22px;text-align:center;line-height:1.1;text-wrap:balance;min-height:52px;display:flex;align-items:center;">{{ r.name }}</div>
<div style="display:flex;align-items:center;gap:6px;background:#FFF6E0;color:#C7912B;border-radius:999px;padding:5px 16px;font-family:'Fredoka';font-weight:700;font-size:22px;">🪙 {{ r.cost }}</div>
<button onClick="{{ r.redeem }}" style="{{ r.btnStyle }}">{{ r.btnLabel }}</button>
</div>
</sc-for>
</div>
<sc-if value="{{ toast.show }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute;bottom:34px;left:50%;transform:translateX(-50%);background:#2A3142;color:#fff;padding:16px 28px;border-radius:999px;font-weight:800;font-size:21px;box-shadow:0 12px 30px rgba(0,0,0,.2);animation:slideUp .3s ease both;">{{ toast.msg }}</div>
</sc-if>
</div>
</sc-if>
<!-- ============ PIN ============ -->
<sc-if value="{{ isPin }}" hint-placeholder-val="{{ false }}">
<div style="position:relative;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:30px;">
<button onClick="{{ goProfiles }}" style="all:unset;cursor:pointer;position:absolute;top:26px;left:30px;color:#8C99AB;font-weight:800;font-size:18px;"> {{ L.volver }}</button>
<div style="font-size:50px;">🔒</div>
<div style="font-family:'Fredoka',sans-serif;font-weight:600;font-size:28px;">{{ L.pin }}</div>
<div style="{{ pinDotsStyle }}">
<sc-for list="{{ pinDots }}" as="d" hint-placeholder-count="4">
<div style="{{ d.style }}"></div>
</sc-for>
</div>
<div style="display:grid;grid-template-columns:repeat(3,84px);gap:16px;">
<sc-for list="{{ keys }}" as="k" hint-placeholder-count="12">
<button onClick="{{ k.press }}" style="{{ k.style }}">{{ k.label }}</button>
</sc-for>
</div>
<div style="color:#9FB0BD;font-weight:700;font-size:15px;">PIN demo: 1 2 3 4</div>
</div>
</sc-if>
<!-- ============ PARENTS ============ -->
<sc-if value="{{ isParents }}" hint-placeholder-val="{{ false }}">
<div style="position:relative;height:100%;display:flex;flex-direction:column;background:#F4F7F9;">
<div style="display:flex;align-items:center;gap:16px;padding:18px 30px;background:#fff;border-bottom:1px solid #E6ECF0;flex:none;">
<div style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:26px;flex:none;">👪 {{ L.padres }}</div>
<div style="flex:1;display:flex;gap:8px;justify-content:center;">
<sc-for list="{{ tabs }}" as="t" hint-placeholder-count="5">
<button onClick="{{ t.select }}" style="{{ t.style }}">{{ t.icon }} {{ t.label }}</button>
</sc-for>
</div>
<button onClick="{{ goHome }}" style="all:unset;cursor:pointer;background:#2A3142;color:#fff;font-weight:800;font-size:15px;padding:11px 20px;border-radius:14px;flex:none;">🔓 {{ L.salir }}</button>
</div>
<div style="flex:1;overflow:auto;padding:26px 30px;">
<!-- HORARIO -->
<sc-if value="{{ tabHorario }}" hint-placeholder-val="{{ false }}">
<div style="font-family:'Fredoka';font-weight:600;font-size:22px;margin-bottom:14px;">Horario semanal · material de cada día</div>
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:16px;">
<sc-for list="{{ schedule }}" as="day" hint-placeholder-count="5">
<div style="background:#fff;border-radius:20px;padding:16px;box-shadow:0 4px 14px rgba(40,60,100,.05);min-height:230px;">
<div style="font-family:'Fredoka';font-weight:700;font-size:18px;margin-bottom:12px;text-align:center;color:#5A6B82;">{{ day.name }}</div>
<div style="display:flex;flex-direction:column;gap:9px;">
<sc-for list="{{ day.acts }}" as="a" hint-placeholder-count="2">
<div style="display:flex;align-items:center;gap:9px;background:{{ a.bg }};border-radius:12px;padding:8px 11px;">
<span style="font-size:20px;">{{ a.icon }}</span>
<span style="font-weight:800;font-size:14px;color:{{ a.color }};">{{ a.name }}</span>
</div>
</sc-for>
<button style="all:unset;cursor:pointer;text-align:center;border:2px dashed #D3DCE3;border-radius:12px;padding:7px;color:#9FB0BD;font-weight:800;font-size:13px;">+ Añadir</button>
</div>
</div>
</sc-for>
</div>
</sc-if>
<!-- MATERIALES -->
<sc-if value="{{ tabMateriales }}" hint-placeholder-val="{{ false }}">
<div style="font-family:'Fredoka';font-weight:600;font-size:22px;margin-bottom:14px;">Actividades y su material</div>
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:18px;">
<sc-for list="{{ activities }}" as="act" hint-placeholder-count="4">
<div style="background:#fff;border-radius:20px;padding:18px;box-shadow:0 4px 14px rgba(40,60,100,.05);border-left:8px solid {{ act.color }};">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;">
<span style="font-size:26px;">{{ act.icon }}</span>
<span style="font-family:'Fredoka';font-weight:700;font-size:21px;">{{ act.name }}</span>
</div>
<div style="display:flex;flex-wrap:wrap;gap:9px;">
<sc-for list="{{ act.items }}" as="m" hint-placeholder-count="3">
<div style="display:flex;align-items:center;gap:7px;background:#F4F7F9;border-radius:999px;padding:7px 14px;font-weight:800;font-size:14px;color:#5A6B82;">{{ m.i }} {{ m.n }}</div>
</sc-for>
<button style="all:unset;cursor:pointer;background:#F4F7F9;border:2px dashed #D3DCE3;border-radius:999px;padding:6px 14px;color:#9FB0BD;font-weight:800;font-size:14px;">+ material</button>
</div>
</div>
</sc-for>
<button style="all:unset;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;background:#fff;border:3px dashed #D3DCE3;border-radius:20px;min-height:120px;color:#9FB0BD;font-family:'Fredoka';font-weight:600;font-size:20px;">+ Nueva actividad</button>
</div>
</sc-if>
<!-- EVENTOS -->
<sc-if value="{{ tabEventos }}" hint-placeholder-val="{{ false }}">
<div style="font-family:'Fredoka';font-weight:600;font-size:22px;margin-bottom:14px;">Exámenes y deberes</div>
<div style="display:flex;flex-direction:column;gap:12px;max-width:680px;">
<sc-for list="{{ pEvents }}" as="e" hint-placeholder-count="3">
<div style="display:flex;align-items:center;gap:16px;background:#fff;border-radius:18px;padding:16px 20px;box-shadow:0 4px 14px rgba(40,60,100,.05);">
<div style="width:48px;height:48px;border-radius:14px;background:{{ e.bg }};display:flex;align-items:center;justify-content:center;font-size:26px;">{{ e.icon }}</div>
<div style="flex:1;">
<div style="font-weight:900;font-size:12px;letter-spacing:1px;color:{{ e.color }};">{{ e.kind }}</div>
<div style="font-family:'Fredoka';font-weight:600;font-size:20px;">{{ e.title }}</div>
</div>
<div style="font-weight:800;color:#7A879B;font-size:16px;">📅 {{ e.date }}</div>
</div>
</sc-for>
<button style="all:unset;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;background:#fff;border:3px dashed #D3DCE3;border-radius:18px;padding:18px;color:#9FB0BD;font-family:'Fredoka';font-weight:600;font-size:19px;">+ Añadir examen o deberes</button>
</div>
</sc-if>
<!-- RUTINAS -->
<sc-if value="{{ tabRutinas }}" hint-placeholder-val="{{ false }}">
<div style="font-family:'Fredoka';font-weight:600;font-size:22px;margin-bottom:14px;">Rutinas de la tarde (por día)</div>
<div style="display:flex;gap:8px;margin-bottom:18px;">
<sc-for list="{{ weekChips }}" as="w" hint-placeholder-count="5">
<button onClick="{{ w.select }}" style="{{ w.style }}">{{ w.label }}</button>
</sc-for>
</div>
<div style="display:flex;flex-direction:column;gap:11px;max-width:620px;">
<sc-for list="{{ routineList }}" as="r" hint-placeholder-count="5">
<div style="display:flex;align-items:center;gap:14px;background:#fff;border-radius:16px;padding:13px 18px;box-shadow:0 4px 14px rgba(40,60,100,.05);">
<span style="font-size:26px;">{{ r.icon }}</span>
<span style="flex:1;font-family:'Fredoka';font-weight:600;font-size:19px;">{{ r.name }}</span>
<span style="cursor:grab;color:#C3CDD6;font-size:20px;"></span>
</div>
</sc-for>
<button style="all:unset;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;background:#fff;border:3px dashed #D3DCE3;border-radius:16px;padding:14px;color:#9FB0BD;font-family:'Fredoka';font-weight:600;font-size:18px;">+ Añadir rutina</button>
</div>
</sc-if>
<!-- GAMIFICACIÓN -->
<sc-if value="{{ tabJuego }}" hint-placeholder-val="{{ false }}">
<div style="font-family:'Fredoka';font-weight:600;font-size:22px;margin-bottom:18px;">Recompensas y ajustes</div>
<div style="display:flex;flex-direction:column;gap:14px;max-width:560px;">
<sc-for list="{{ steppers }}" as="s" hint-placeholder-count="3">
<div style="display:flex;align-items:center;gap:16px;background:#fff;border-radius:18px;padding:16px 22px;box-shadow:0 4px 14px rgba(40,60,100,.05);">
<span style="font-size:30px;">{{ s.icon }}</span>
<div style="flex:1;"><div style="font-family:'Fredoka';font-weight:600;font-size:19px;">{{ s.label }}</div><div style="font-size:14px;color:#9FB0BD;font-weight:700;">{{ s.hint }}</div></div>
<div style="display:flex;align-items:center;gap:12px;">
<button onClick="{{ s.dec }}" style="all:unset;cursor:pointer;width:42px;height:42px;border-radius:50%;background:#F4F7F9;display:flex;align-items:center;justify-content:center;font-weight:900;font-size:22px;color:#7A879B;"></button>
<span style="font-family:'Fredoka';font-weight:700;font-size:26px;min-width:56px;text-align:center;">🪙 {{ s.value }}</span>
<button onClick="{{ s.inc }}" style="all:unset;cursor:pointer;width:42px;height:42px;border-radius:50%;background:#F4F7F9;display:flex;align-items:center;justify-content:center;font-weight:900;font-size:22px;color:#7A879B;">+</button>
</div>
</div>
</sc-for>
<sc-for list="{{ toggles }}" as="tg" hint-placeholder-count="2">
<div style="display:flex;align-items:center;gap:16px;background:#fff;border-radius:18px;padding:16px 22px;box-shadow:0 4px 14px rgba(40,60,100,.05);">
<span style="font-size:30px;">{{ tg.icon }}</span>
<div style="flex:1;font-family:'Fredoka';font-weight:600;font-size:19px;">{{ tg.label }}</div>
<button onClick="{{ tg.toggle }}" style="{{ tg.trackStyle }}"><span style="{{ tg.knobStyle }}"></span></button>
</div>
</sc-for>
</div>
</sc-if>
</div>
</div>
</sc-if>
<!-- ============ BOTTOM DEMO DOCK (prototype controls) ============ -->
<sc-if value="{{ showDock }}" hint-placeholder-val="{{ true }}">
<div style="position:fixed;bottom:0;left:0;right:0;z-index:40;display:flex;align-items:center;justify-content:center;gap:8px;padding:8px;background:rgba(255,255,255,.78);backdrop-filter:blur(8px);border-top:1px solid rgba(0,0,0,.05);">
<button onClick="{{ goProfiles }}" style="{{ dockBtn }}">👤 {{ L.perfiles }}</button>
<button onClick="{{ goPin }}" style="{{ dockBtn }}">⚙️ {{ L.padres }}</button>
<button onClick="{{ goStore }}" style="{{ dockBtn }}">🎁 {{ L.tienda }}</button>
<span style="width:1px;height:24px;background:#DDE4E9;"></span>
<button onClick="{{ setVarA }}" style="{{ varAStyle }}">A · Tablero</button>
<button onClick="{{ setVarB }}" style="{{ varBStyle }}">B · Foco</button>
<span style="width:1px;height:24px;background:#DDE4E9;"></span>
<button onClick="{{ toggleEmpty }}" style="{{ dockBtn }}">{{ emptyLabel }}</button>
<button onClick="{{ reset }}" style="{{ dockBtn }}">↺ {{ L.reiniciar }}</button>
<span style="width:1px;height:24px;background:#DDE4E9;"></span>
<button onClick="{{ toggleLang }}" style="{{ langBtn }}">{{ langLabel }}</button>
</div>
</sc-if>
<!-- flying coins layer -->
<div style="position:fixed;inset:0;pointer-events:none;z-index:60;">{{ flyingCoinEls }}</div>
<!-- CELEBRATION -->
<sc-if value="{{ celebrate }}" hint-placeholder-val="{{ false }}">
<div style="position:fixed;inset:0;z-index:80;display:flex;align-items:center;justify-content:center;background:rgba(35,49,66,.45);backdrop-filter:blur(3px);">
<div style="position:absolute;inset:0;overflow:hidden;pointer-events:none;">{{ confettiEls }}</div>
<div style="position:relative;background:#fff;border-radius:36px;padding:46px 64px;text-align:center;box-shadow:0 30px 80px rgba(0,0,0,.3);animation:celebPop .45s cubic-bezier(.2,.8,.2,1) both;">
<div style="font-size:104px;animation:floatYb 2.2s ease-in-out infinite;">🦊🎉</div>
<div style="font-family:'Fredoka',sans-serif;font-weight:700;font-size:54px;color:#2A3142;margin-top:8px;">{{ L.todoListo }}</div>
<div style="font-size:24px;color:#7A879B;font-weight:700;margin-top:6px;">{{ L.biengrande }}</div>
<div style="display:inline-flex;align-items:center;gap:9px;background:#FFF6E0;color:#C7912B;border-radius:999px;padding:12px 26px;font-family:'Fredoka';font-weight:700;font-size:28px;margin-top:22px;">🪙 +{{ coinsPerDay }}</div>
<div style="margin-top:26px;">
<button onClick="{{ dismissCeleb }}" style="all:unset;cursor:pointer;background:linear-gradient(135deg,#7FBF6B,#5BC0BE);color:#fff;font-family:'Fredoka';font-weight:700;font-size:26px;padding:16px 44px;border-radius:999px;box-shadow:0 10px 26px rgba(91,192,190,.4);">{{ L.genial }} 👍</button>
</div>
</div>
</div>
</sc-if>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{&quot;$preview&quot;:{&quot;width&quot;:1280,&quot;height&quot;:800}}">
class Component extends DCLogic {
D = {
es:{ who:'¿QUIÉN ENTRA HOY?', cole:'PARA EL COLE', tarde:'ESTA TARDE', salimos:'SALIMOS EN', min:'min', todoListo:'¡TODO LISTO!', biengrande:'¡Lo has hecho genial!', listo:'listo', quedan:'Quedan', despues:'Después', genial:'¡GENIAL!', tienda:'TIENDA DE PREMIOS', canjear:'CANJEAR', faltan:'Te faltan', monedas:'monedas', padres:'Padres', salir:'Salir', pin:'Introduce el PIN', vacioT:'HOY NO HAY COLE', vacioS:'¡Disfruta de la tarde! 🎉', volver:'Volver', perfiles:'Perfiles', reiniciar:'Reiniciar', hola:'Hola', diaVacio:'Día vacío', diaNormal:'Día normal', hecho:'¡HECHO!', anios:'años' },
ca:{ who:'QUI ENTRA AVUI?', cole:"PER A L'ESCOLA", tarde:'AQUESTA TARDA', salimos:'SORTIM EN', min:'min', todoListo:'TOT FET!', biengrande:'Ho has fet genial!', listo:'fet', quedan:'En queden', despues:'Després', genial:'GENIAL!', tienda:'BOTIGA DE PREMIS', canjear:'BESCANVIAR', faltan:'Et falten', monedas:'monedes', padres:'Pares', salir:'Sortir', pin:'Introdueix el PIN', vacioT:'AVUI NO HI HA ESCOLA', vacioS:'Gaudeix de la tarda! 🎉', volver:'Tornar', perfiles:'Perfils', reiniciar:'Reiniciar', hola:'Hola', diaVacio:'Dia buit', diaNormal:'Dia normal', hecho:'FET!', anios:'anys' }
};
profilesData = [
{ id:'nora', name:'NORA', mascot:'🦊', color:'#F2A65A', coins:42 },
{ id:'leo', name:'LEO', mascot:'🐢', color:'#5BC0BE', coins:28 },
{ id:'mia', name:'MÍA', mascot:'🦉', color:'#A78BD0', coins:55 },
];
coleData = [
{ id:'estuche', icon:'✏️', label:'ESTUCHE', labelCa:'ESTOIG', color:'#F2A65A' },
{ id:'mates', icon:'📘', label:'LIBRO DE MATES', labelCa:'LLIBRE DE MATES', color:'#5B8DEF' },
{ id:'flauta', icon:'🎵', label:'FLAUTA', labelCa:'FLAUTA', color:'#A78BD0' },
{ id:'ropa', icon:'👕', label:'ROPA DE GIMNASIA', labelCa:'ROBA D\u2019EDUCACIÓ FÍSICA', color:'#7FBF6B' },
{ id:'zapas', icon:'👟', label:'ZAPATILLAS', labelCa:'SABATILLES', color:'#7FBF6B' },
{ id:'almuerzo', icon:'🍎', label:'ALMUERZO', labelCa:'ESMORZAR', color:'#F4C95D' },
];
tardeData = [
{ id:'mochila', icon:'🎒', label:'DESHACER LA MOCHILA', labelCa:'BUIDAR LA MOTXILLA', color:'#5BC0BE' },
{ id:'merienda', icon:'🥪', label:'MERENDAR', labelCa:'BERENAR', color:'#F2A65A' },
{ id:'deberes', icon:'📝', label:'HACER LOS DEBERES', labelCa:'FER ELS DEURES', color:'#5B8DEF' },
{ id:'piano', icon:'🎹', label:'PRACTICAR PIANO', labelCa:'PRACTICAR PIANO', color:'#A78BD0' },
{ id:'mesa', icon:'🍽️', label:'RECOGER LA MESA', labelCa:'PARAR TAULA', color:'#7FBF6B' },
];
rewardsData = [
{ icon:'🎮', name:'30 MIN DE TABLET', nameCa:'30 MIN DE TAULETA', cost:20, color:'#5B8DEF' },
{ icon:'🍿', name:'PELI EN FAMILIA', nameCa:'PEL·LÍCULA', cost:50, color:'#A78BD0' },
{ icon:'🛝', name:'TARDE EN EL PARQUE', nameCa:'TARDA AL PARC', cost:40, color:'#7FBF6B' },
{ icon:'🍕', name:'ELIJO LA CENA', nameCa:'TRIO EL SOPAR', cost:30, color:'#F2A65A' },
{ icon:'🌙', name:'30 MIN MÁS DESPIERTO', nameCa:'30 MIN MÉS DESPERT', cost:60, color:'#5BC0BE' },
{ icon:'🦖', name:'SORPRESA DINO', nameCa:'SORPRESA DINO', cost:80, color:'#EC8FA4' },
];
activitiesData = [
{ name:'Gimnasia', icon:'🤸', color:'#7FBF6B', items:[{i:'👕',n:'Equipación'},{i:'👟',n:'Zapatillas'},{i:'🧺',n:'Toalla'},{i:'💧',n:'Agua'}] },
{ name:'Música', icon:'🎵', color:'#A78BD0', items:[{i:'🎵',n:'Flauta'},{i:'📒',n:'Libreta'}] },
{ name:'Matemáticas', icon:'📘', color:'#5B8DEF', items:[{i:'📘',n:'Libro'},{i:'📐',n:'Regla'},{i:'✏️',n:'Estuche'}] },
{ name:'Lengua', icon:'📖', color:'#EC8FA4', items:[{i:'📖',n:'Lectura'},{i:'📓',n:'Cuaderno'}] },
];
state = {
screen:'profiles', lang:'es', variant:'A', profileId:'nora', coins:42,
done:{}, focusIndex:0, celebrate:false, confetti:[], flyingCoins:[], walletPulse:false,
emptyDemo:false, pinInput:'', parentsTab:'horario', routineDay:1,
coinsPerTask:5, coinsPerBlock:10, coinsPerDay:20, sndOn:true, ttsOn:true,
ages:{ nora:7, leo:9, mia:6 }, timer:18*60,
};
componentDidMount(){ this._t = setInterval(()=>{ this.setState(s=>({ timer: s.timer>0 ? s.timer-1 : 0 })); }, 1000); }
componentWillUnmount(){ clearInterval(this._t); }
L(){ return this.D[this.state.lang]; }
allTasks(){ return this.coleData.concat(this.tardeData); }
cardRefs = {};
setCardRef = (id)=> (el)=>{ if(el) this.cardRefs[id]=el; };
speak(text){ if(!this.state.ttsOn) return; try{ const u=new SpeechSynthesisUtterance(text); u.lang=this.state.lang==='ca'?'ca-ES':'es-ES'; u.rate=0.9; u.pitch=1.1; speechSynthesis.cancel(); speechSynthesis.speak(u);}catch(e){} }
ding(){ if(!this.state.sndOn) return; try{ const a=new (window.AudioContext||window.webkitAudioContext)(); const o=a.createOscillator(), g=a.createGain(); o.connect(g); g.connect(a.destination); o.type='sine'; o.frequency.setValueAtTime(660,a.currentTime); o.frequency.setValueAtTime(990,a.currentTime+0.09); g.gain.setValueAtTime(0.12,a.currentTime); g.gain.exponentialRampToValueAtTime(0.001,a.currentTime+0.32); o.start(); o.stop(a.currentTime+0.33);}catch(e){} }
toggleTask(id){
const done={...this.state.done}; const was=done[id]; done[id]=!was;
this.setState({done}, ()=>{ if(!was){ this.launchCoin(id); this.ding(); this.maybeCelebrate(); } });
}
launchCoin(id){
const card=this.cardRefs[id], wal=this.walletEl;
if(!card||!wal){ this.addCoins(); return; }
const c=card.getBoundingClientRect(), w=wal.getBoundingClientRect();
const x=c.left+30, y=c.top+30;
const dx=(w.left+w.width/2)-x, dy=(w.top+w.height/2)-y;
const key='c'+Date.now()+Math.random();
this.setState(s=>({flyingCoins:[...s.flyingCoins,{key,x,y,dx,dy}]}));
setTimeout(()=>{ this.setState(s=>({flyingCoins:s.flyingCoins.filter(f=>f.key!==key)})); this.addCoins(); }, 760);
}
addCoins(){ this.setState(s=>({coins:s.coins+s.coinsPerTask, walletPulse:!s.walletPulse})); }
maybeCelebrate(){
const total=this.allTasks().length;
const d=this.allTasks().filter(t=>this.state.done[t.id]).length;
if(d===total && total>0 && !this.state.celebrate){
setTimeout(()=>this.setState(s=>({celebrate:true, confetti:this.makeConfetti(), coins:s.coins+s.coinsPerDay})), 450);
}
}
makeConfetti(){ const cs=['#5B8DEF','#5BC0BE','#F2A65A','#A78BD0','#7FBF6B','#EC8FA4','#F4C95D']; return Array.from({length:100},(_,i)=>({key:i,left:Math.random()*100,delay:Math.random()*0.7,dur:2.1+Math.random()*1.8,color:cs[i%cs.length],size:7+Math.random()*11})); }
dismissCeleb(){ this.setState({celebrate:false, confetti:[]}); }
// variant B
focusTask(){ const t=this.allTasks(); let i=Math.min(this.state.focusIndex, t.length-1); return t[i]; }
bDone(){ const t=this.focusTask(); if(!t) return; if(!this.state.done[t.id]) this.toggleTask(t.id); const all=this.allTasks(); let n=this.state.focusIndex+1; while(n<all.length && this.state.done[all[n].id]) n++; if(n<all.length) this.setState({focusIndex:n}); }
bNav(d){ const all=this.allTasks(); let i=this.state.focusIndex+d; if(i<0)i=0; if(i>all.length-1)i=all.length-1; this.setState({focusIndex:i}); }
selectProfile(p){ this.setState({screen:'home', profileId:p.id, coins:p.coins, done:{}, focusIndex:0, celebrate:false, emptyDemo:false}); }
changeAge(id,d){ this.setState(s=>{ const ages={...s.ages}; ages[id]=Math.max(4,Math.min(12,(ages[id]||7)+d)); return {ages}; }); }
reset(){ this.setState({done:{}, focusIndex:0, celebrate:false, confetti:[], emptyDemo:false}); }
pressPin(d){ if(d==='del'){ this.setState(s=>({pinInput:s.pinInput.slice(0,-1)})); return; } const v=this.state.pinInput+d; if(v.length>=4){ if(v==='1234'){ this.setState({screen:'parents', pinInput:''}); } else { this.setState({pinInput:''}); } } else { this.setState({pinInput:v}); } }
redeem(r){ if(this.state.coins>=r.cost){ this.setState(s=>({coins:s.coins-r.cost}), ()=>this.flash('🎉 ¡'+(this.state.lang==='ca'?'Bescanviat':'Canjeado')+'!')); } else { this.flash('🙂 '+this.L().faltan+' '+(r.cost-this.state.coins)); } }
flash(msg){ clearTimeout(this._tt); this.setState({toast:{show:true,msg}}); this._tt=setTimeout(()=>this.setState({toast:{show:false,msg}}),1700); }
step(key,d,min,max){ this.setState(s=>({[key]:Math.max(min,Math.min(max,s[key]+d))})); }
renderVals(){
const s=this.state, L=this.L(), lang=s.lang;
const profile=this.profilesData.find(p=>p.id===s.profileId)||this.profilesData[0];
const lbl=(it)=> lang==='ca'? it.labelCa : it.label;
const tint=(hex)=> hex+'24';
const baseCard=(it)=>{ const done=!!s.done[it.id];
const card='display:flex;align-items:center;gap:16px;background:'+(done?tint(it.color):'#fff')+';border:3px solid '+(done?it.color:'#EEF2F6')+';border-radius:26px;padding:14px 16px;cursor:pointer;box-shadow:0 6px 16px rgba(40,60,100,.06);transition:transform .15s,border-color .25s,background .25s;min-height:92px;'+(done?'animation:pop .35s ease;':'');
const tile='width:66px;height:66px;border-radius:20px;display:flex;align-items:center;justify-content:center;font-size:38px;flex:none;background:'+tint(it.color)+';'+(done?'opacity:.85;':'');
const label='font-family:\'Fredoka\',sans-serif;font-weight:600;font-size:21px;letter-spacing:.2px;color:'+(done?'#7A879B':'#2A3142')+';line-height:1.05;text-transform:uppercase;';
const check='width:60px;height:60px;border-radius:50%;flex:none;display:flex;align-items:center;justify-content:center;font-size:30px;color:#fff;font-weight:900;border:3px solid '+(done?it.color:'#DCE3EA')+';background:'+(done?it.color:'#fff')+';'+(done?'animation:checkPop .35s ease;':'');
return {...it, done, label:lbl(it), cardStyle:card, tileStyle:tile, labelStyle:label, checkStyle:check, check:done?'✓':'',
setRef:this.setCardRef(it.id), toggle:()=>this.toggleTask(it.id), speak:(e)=>{e&&e.stopPropagation&&e.stopPropagation();this.speak(lbl(it));}};
};
const coleItems=this.coleData.map(baseCard);
const tardeItems=this.tardeData.map(baseCard);
const coleDone=coleItems.filter(i=>i.done).length, tardeDone=tardeItems.filter(i=>i.done).length;
const gTotal=coleItems.length+tardeItems.length, gDone=coleDone+tardeDone;
// focus B
const all=this.allTasks(); const fi=Math.min(s.focusIndex,all.length-1); const ft=all[fi];
const isCole=fi<this.coleData.length; const fdone=!!s.done[ft.id];
const heroTile='width:200px;height:200px;border-radius:46px;display:flex;align-items:center;justify-content:center;font-size:120px;background:'+tint(ft.color)+';box-shadow:0 16px 40px '+ft.color+'33;animation:floatYb 3s ease-in-out infinite;';
const navBtn=(en)=>'all:unset;cursor:pointer;width:64px;height:64px;border-radius:50%;background:#fff;box-shadow:0 6px 16px rgba(40,60,100,.08);display:flex;align-items:center;justify-content:center;font-size:34px;color:#9FB0BD;'+(en?'':'opacity:.3;pointer-events:none;');
const bDoneStyle='all:unset;cursor:pointer;background:'+(fdone?'#7FBF6B':'linear-gradient(135deg,'+ft.color+',#5BC0BE)')+';color:#fff;font-family:\'Fredoka\';font-weight:700;font-size:30px;padding:18px 46px;border-radius:999px;box-shadow:0 12px 28px '+ft.color+'55;';
const remaining=all.filter(t=>!s.done[t.id]).length;
const nextT=all.slice(fi+1).find(t=>!s.done[t.id]) || all.find(t=>!s.done[t.id]);
const dots=all.map((t,i)=>({ style:'width:14px;height:14px;border-radius:50%;background:'+(s.done[t.id]?'#7FBF6B':(i===fi?'#F2A65A':'#D3DCE3'))+';transition:.3s;'+(i===fi?'transform:scale(1.4);':'') }));
// events
const evRaw=[ {icon:'📋',kind:'EXAMEN',kindCa:'EXAMEN',title:lang==='ca'?'Examen de Llengua':'Examen de Lengua',border:'#E08A3C',bg:'#FFF1DE'},
{icon:'📎',kind:'DEBERES',kindCa:'DEURES',title:lang==='ca'?'Fitxa de mates':'Ficha de mates',border:'#5B8DEF',bg:'#E9F1FF'} ];
const events=evRaw.map(e=>({...e, kind:lang==='ca'?e.kindCa:e.kind, speak:()=>this.speak((lang==='ca'?e.kindCa:e.kind)+'. '+e.title)}));
// rewards
const rewards=this.rewardsData.map(r=>{ const can=s.coins>=r.cost; const name=lang==='ca'?r.nameCa:r.name;
return {...r, name, cardStyle:'display:flex;flex-direction:column;align-items:center;gap:12px;background:#fff;border-radius:26px;padding:20px 16px 18px;box-shadow:0 8px 22px rgba(40,60,100,.06);border-top:6px solid '+r.color+';',
tileStyle:'width:88px;height:88px;border-radius:26px;display:flex;align-items:center;justify-content:center;font-size:52px;background:'+tint(r.color)+';',
btnStyle:'all:unset;cursor:'+(can?'pointer':'default')+';background:'+(can?r.color:'#EEF2F6')+';color:'+(can?'#fff':'#9FB0BD')+';font-family:\'Fredoka\';font-weight:700;font-size:18px;padding:11px 26px;border-radius:999px;white-space:nowrap;',
btnLabel:can?L.canjear:(L.faltan+' '+(r.cost-s.coins)),
redeem:()=>this.redeem(r)};
});
// pin
const pinDots=[0,1,2,3].map(i=>({ style:'width:20px;height:20px;border-radius:50%;background:'+(i<s.pinInput.length?'#5BC0BE':'#D3DCE3')+';' }));
const keyDef=['1','2','3','4','5','6','7','8','9','','0','del'];
const keys=keyDef.map(k=>({ label:k==='del'?'⌫':k, press:k===''?(()=>{}):(()=>this.pressPin(k)),
style:'all:'+(k===''?'unset':'unset')+';cursor:'+(k===''?'default':'pointer')+';width:84px;height:84px;border-radius:24px;background:'+(k===''?'transparent':'#fff')+';'+(k===''?'':'box-shadow:0 5px 14px rgba(40,60,100,.07);')+'display:flex;align-items:center;justify-content:center;font-family:\'Fredoka\';font-weight:600;font-size:34px;color:#2A3142;' }));
// parents tabs
const tabDefs=[ {id:'horario',icon:'📅',label:lang==='ca'?'Horari':'Horario'}, {id:'materiales',icon:'🎒',label:lang==='ca'?'Materials':'Materiales'}, {id:'eventos',icon:'📋',label:lang==='ca'?'Esdeveniments':'Eventos'}, {id:'rutinas',icon:'🌙',label:lang==='ca'?'Rutines':'Rutinas'}, {id:'juego',icon:'🪙',label:lang==='ca'?'Recompenses':'Recompensas'} ];
const tabs=tabDefs.map(t=>({...t, select:()=>this.setState({parentsTab:t.id}), style:'all:unset;cursor:pointer;padding:10px 18px;border-radius:14px;font-family:\'Fredoka\';font-weight:600;font-size:16px;background:'+(s.parentsTab===t.id?'#2A3142':'#F4F7F9')+';color:'+(s.parentsTab===t.id?'#fff':'#5A6B82')+';' }));
const actColor={Gimnasia:'#7FBF6B',Música:'#A78BD0',Matemáticas:'#5B8DEF',Lengua:'#EC8FA4',Plástica:'#F2A65A'};
const actIcon={Gimnasia:'🤸',Música:'🎵',Matemáticas:'📘',Lengua:'📖',Plástica:'🎨'};
const sched=[['Matemáticas','Lengua'],['Matemáticas','Música','Gimnasia'],['Lengua','Plástica'],['Matemáticas','Música'],['Gimnasia','Lengua']];
const dayNames=lang==='ca'?['DILLUNS','DIMARTS','DIMECRES','DIJOUS','DIVENDRES']:['LUNES','MARTES','MIÉRCOLES','JUEVES','VIERNES'];
const schedule=sched.map((acts,i)=>({ name:dayNames[i], acts:acts.map(a=>({name:a,icon:actIcon[a]||'📗',color:actColor[a]||'#5A6B82',bg:(actColor[a]||'#5A6B82')+'1f'})) }));
const activities=this.activitiesData.map(a=>({...a}));
const pEvents=[ {icon:'📋',kind:'EXAMEN',color:'#E08A3C',bg:'#FFF1DE',title:lang==='ca'?'Examen de Llengua':'Examen de Lengua',date:'Mar 17'},
{icon:'📎',kind:'DEBERES',color:'#5B8DEF',bg:'#E9F1FF',title:lang==='ca'?'Fitxa de mates':'Ficha de mates',date:'Mar 17'},
{icon:'📋',kind:'EXAMEN',color:'#E08A3C',bg:'#FFF1DE',title:lang==='ca'?'Examen de Mates':'Examen de Mates',date:'Jue 19'} ];
const weekChips=dayNames.map((d,i)=>({ label:d.slice(0,3), select:()=>this.setState({routineDay:i}), style:'all:unset;cursor:pointer;padding:9px 16px;border-radius:12px;font-weight:800;font-size:14px;background:'+(s.routineDay===i?'#5BC0BE':'#fff')+';color:'+(s.routineDay===i?'#fff':'#5A6B82')+';box-shadow:0 3px 10px rgba(40,60,100,.05);' }));
const baseRoutines=[ {icon:'🎒',name:lang==='ca'?'Buidar la motxilla':'Deshacer la mochila'},{icon:'🥪',name:lang==='ca'?'Berenar':'Merendar'},{icon:'📝',name:lang==='ca'?'Fer els deures':'Hacer los deberes'},{icon:'🍽️',name:lang==='ca'?'Parar taula':'Recoger la mesa'} ];
const routineList=(s.routineDay===1||s.routineDay===3)? baseRoutines.concat([{icon:'🎹',name:lang==='ca'?'Practicar piano':'Practicar piano'}]) : baseRoutines;
const steppers=[ {icon:'✅',label:lang==='ca'?'Monedes per tasca':'Monedas por tarea',hint:lang==='ca'?'En completar cada tasca':'Al completar cada tarea',value:s.coinsPerTask,inc:()=>this.step('coinsPerTask',1,1,20),dec:()=>this.step('coinsPerTask',-1,1,20)},
{icon:'🎯',label:lang==='ca'?'Monedes per bloc':'Monedas por bloque',hint:lang==='ca'?'En acabar cole o tarda':'Al terminar cole o tarde',value:s.coinsPerBlock,inc:()=>this.step('coinsPerBlock',1,0,50),dec:()=>this.step('coinsPerBlock',-1,0,50)},
{icon:'🏆',label:lang==='ca'?'Bonus dia complet':'Bonus día completo',hint:lang==='ca'?'En acabar-ho tot':'Al completar todo el día',value:s.coinsPerDay,inc:()=>this.step('coinsPerDay',5,0,100),dec:()=>this.step('coinsPerDay',-5,0,100)} ];
const tgl=(on)=>'all:unset;cursor:pointer;width:62px;height:34px;border-radius:999px;background:'+(on?'#7FBF6B':'#D3DCE3')+';display:flex;align-items:center;padding:3px;transition:.2s;';
const knob=(on)=>'width:28px;height:28px;border-radius:50%;background:#fff;box-shadow:0 2px 4px rgba(0,0,0,.2);transform:translateX('+(on?'28px':'0')+');transition:.2s;';
const toggles=[ {icon:'🔊',label:lang==='ca'?'So en marcar':'Sonido al marcar',trackStyle:tgl(s.sndOn),knobStyle:knob(s.sndOn),toggle:()=>this.setState(p=>({sndOn:!p.sndOn}))},
{icon:'🗣️',label:lang==='ca'?'Lectura en veu alta':'Lectura en voz alta',trackStyle:tgl(s.ttsOn),knobStyle:knob(s.ttsOn),toggle:()=>this.setState(p=>({ttsOn:!p.ttsOn}))} ];
// timer ring
const frac=s.timer/(20*60); const deg=Math.round(frac*360);
const timerRing='conic-gradient(#F2A65A '+deg+'deg, #FFE6C7 0deg)';
const timerMin=Math.ceil(s.timer/60);
// flying coins
const flyingCoinEls=s.flyingCoins.map(f=>React.createElement('div',{key:f.key, style:{position:'fixed',left:f.x+'px',top:f.y+'px',fontSize:'40px',zIndex:60,['--dx']:f.dx+'px',['--dy']:f.dy+'px',animation:'none',transform:'translate('+f.dx+'px,'+f.dy+'px) scale(.4)',transition:'transform .72s cubic-bezier(.4,0,.5,1), opacity .72s',opacity:0,willChange:'transform'}, ref:(el)=>{ if(el){ el.style.transform='translate(0,0) scale(1)'; el.style.opacity='1'; requestAnimationFrame(()=>requestAnimationFrame(()=>{ el.style.transform='translate('+f.dx+'px,'+f.dy+'px) scale(.4)'; el.style.opacity='0'; })); } }}, '🪙'));
const confettiEls=s.confetti.map(c=>React.createElement('div',{key:c.key, style:{position:'absolute',top:'-5vh',left:c.left+'%',width:c.size+'px',height:(c.size*0.6)+'px',background:c.color,borderRadius:'2px',animation:'confFall '+c.dur+'s linear '+c.delay+'s infinite'}}));
const dockBtn='all:unset;cursor:pointer;padding:7px 13px;border-radius:11px;background:#fff;color:#5A6B82;font-weight:800;font-size:13px;box-shadow:0 2px 6px rgba(40,60,100,.06);';
const segBtn=(on)=>'all:unset;cursor:pointer;padding:7px 13px;border-radius:11px;background:'+(on?'#2A3142':'#fff')+';color:'+(on?'#fff':'#5A6B82')+';font-weight:800;font-size:13px;box-shadow:0 2px 6px rgba(40,60,100,.06);';
return {
L,
isProfiles:s.screen==='profiles', isHome:s.screen==='home', isStore:s.screen==='store', isPin:s.screen==='pin', isParents:s.screen==='parents',
showDock:s.screen!=='pin',
profiles:this.profilesData.map(p=>({...p, ageLabel:(s.ages[p.id]||7)+' '+L.anios,
cardStyle:'background:#fff;border-radius:30px;padding:26px 30px;box-shadow:0 12px 30px rgba(40,60,100,.08);display:flex;flex-direction:column;align-items:center;animation:slideUp .5s ease both;',
ringStyle:'width:140px;height:140px;border-radius:50%;background:'+p.color+'26;display:flex;align-items:center;justify-content:center;animation:floatY 3.5s ease-in-out infinite;',
select:()=>this.selectProfile(p), ageUp:()=>this.changeAge(p.id,1), ageDown:()=>this.changeAge(p.id,-1) })),
profile, coins:s.coins,
walletStyle:'display:flex;align-items:center;gap:8px;background:#FFF6E0;color:#C7912B;border-radius:20px;padding:10px 20px;'+(s.walletPulse!==undefined?'':'')+(true?'animation:'+(s._wp?'':'')+'walletBump .5s ease;':''),
setWallet:(el)=>{this.walletEl=el;},
dayName: lang==='ca'?'DIMARTS':'MARTES', dayDate: lang==='ca'?'17 de juny':'17 de junio',
timerRing, timerMin,
globalDone:gDone, globalTotal:gTotal, globalPct:(gTotal?Math.round(gDone/gTotal*100):0)+'%',
coleItems, tardeItems, coleDone, coleTotal:coleItems.length, tardeDone, tardeTotal:tardeItems.length,
showEmpty:s.emptyDemo, showA:!s.emptyDemo && s.variant==='A', showB:!s.emptyDemo && s.variant==='B',
hasEvents:!s.emptyDemo, events,
focus:{...ft, label:lbl(ft), icon:ft.icon, color:ft.color, tint:tint(ft.color), heroTile,
blockIcon:isCole?'🎒':'🌙', blockLabel:isCole?L.cole:L.tarde, doneLabel:fdone?'✓ '+L.listo.toUpperCase():L.hecho,
speak:()=>this.speak(lbl(ft))},
bDone:()=>this.bDone(), bPrev:()=>this.bNav(-1), bNext:()=>this.bNav(1),
bPrevStyle:navBtn(fi>0), bNextStyle:navBtn(fi<all.length-1), bDoneStyle,
dots, remaining, nextLabel: nextT?lbl(nextT):'🎉',
rewards, toast:s.toast||{show:false,msg:''},
pinDots, pinDotsStyle:'display:flex;gap:16px;'+(s.pinInput.length===0&&false?'animation:shake .4s;':''), keys,
tabs, tabHorario:s.parentsTab==='horario', tabMateriales:s.parentsTab==='materiales', tabEventos:s.parentsTab==='eventos', tabRutinas:s.parentsTab==='rutinas', tabJuego:s.parentsTab==='juego',
schedule, activities, pEvents, weekChips, routineList, steppers, toggles,
celebrate:s.celebrate, confettiEls, flyingCoinEls, coinsPerDay:s.coinsPerDay, dismissCeleb:()=>this.dismissCeleb(),
goHome:()=>this.setState({screen:'home'}), goStore:()=>this.setState({screen:'store'}), goProfiles:()=>this.setState({screen:'profiles'}), goPin:()=>this.setState({screen:'pin',pinInput:''}),
setVarA:()=>this.setState({variant:'A'}), setVarB:()=>this.setState({variant:'B'}),
varAStyle:segBtn(s.variant==='A'), varBStyle:segBtn(s.variant==='B'),
toggleEmpty:()=>this.setState(p=>({emptyDemo:!p.emptyDemo})), emptyLabel:s.emptyDemo?('📋 '+L.diaNormal):('🏖️ '+L.diaVacio),
reset:()=>this.reset(), dockBtn,
toggleLang:()=>this.setState(p=>({lang:p.lang==='es'?'ca':'es'})), langBtn:dockBtn, langLabel:s.lang==='es'?'🌐 CA':'🌐 ES',
};
}
}
</script>
</body>
</html>