feat(home): estado vacío amable y vibración háptica al ganar monedas

Un día sin tareas muestra una tarjeta con la mascota del niño en vez de
una pantalla en blanco; la columna de tarde vacía tiene su propio mensaje.
El feedback de recompensa suma una vibración corta (Vibration API) que se
dispara junto al sonido sobre el mismo evento.
This commit is contained in:
Jaume Garriga Maestre
2026-07-12 22:06:03 +02:00
parent 3ec611d373
commit 5c79480aa4
6 changed files with 85 additions and 26 deletions

View File

@@ -22,8 +22,12 @@ import { TaskCardComponent, ToggleEvent } from './task-card.component';
<section class="col">
<h2 class="col__head">🌙 {{ i18n.t('afternoon') }}</h2>
@for (task of afternoon; track task.id) {
<app-task-card [task]="task" [ttsEnabled]="ttsEnabled" (toggle)="toggle.emit($event)" />
@if (afternoon.length) {
@for (task of afternoon; track task.id) {
<app-task-card [task]="task" [ttsEnabled]="ttsEnabled" (toggle)="toggle.emit($event)" />
}
} @else {
<div class="empty">🌈 {{ i18n.t('freeAfternoon') }}</div>
}
</section>
</div>

View File

@@ -17,32 +17,41 @@
</div>
</header>
<!-- Conmutador de modo -->
<div class="modes">
<button type="button" class="modes__btn" [class.modes__btn--on]="mode() === 'BOARD'" (click)="setMode('BOARD')">
🗂️ {{ i18n.t('board') }}
</button>
<button type="button" class="modes__btn" [class.modes__btn--on]="mode() === 'FOCUS'" (click)="setMode('FOCUS')">
🎯 {{ i18n.t('focus') }}
</button>
</div>
<!-- Eventos del día -->
<app-event-banner [events]="t.specialEvents" [ttsEnabled]="t.child.ttsEnabled" />
<!-- Progreso global -->
<app-progress-bar [done]="t.progress.totalDone" [total]="t.progress.total" />
@if (t.progress.total > 0) {
<!-- Conmutador de modo -->
<div class="modes">
<button type="button" class="modes__btn" [class.modes__btn--on]="mode() === 'BOARD'" (click)="setMode('BOARD')">
🗂️ {{ i18n.t('board') }}
</button>
<button type="button" class="modes__btn" [class.modes__btn--on]="mode() === 'FOCUS'" (click)="setMode('FOCUS')">
🎯 {{ i18n.t('focus') }}
</button>
</div>
<!-- Tareas: tablero o foco -->
@if (mode() === 'BOARD') {
<app-board-view
[morning]="t.morning"
[afternoon]="t.afternoon"
[ttsEnabled]="t.child.ttsEnabled"
(toggle)="onToggle($event)"
/>
<!-- Progreso global -->
<app-progress-bar [done]="t.progress.totalDone" [total]="t.progress.total" />
<!-- Tareas: tablero o foco -->
@if (mode() === 'BOARD') {
<app-board-view
[morning]="t.morning"
[afternoon]="t.afternoon"
[ttsEnabled]="t.child.ttsEnabled"
(toggle)="onToggle($event)"
/>
} @else {
<app-focus-view [tasksInput]="allTasks()" [ttsEnabled]="t.child.ttsEnabled" (toggle)="onToggle($event)" />
}
} @else {
<app-focus-view [tasksInput]="allTasks()" [ttsEnabled]="t.child.ttsEnabled" (toggle)="onToggle($event)" />
<!-- Día sin tareas: mensaje amable, nunca una pantalla en blanco. -->
<div class="freeday">
<span class="freeday__mascot">{{ t.child.mascot }}</span>
<p class="freeday__title">{{ i18n.t('emptyDayTitle') }}</p>
<p class="freeday__sub">{{ i18n.t('emptyDaySub') }} 🎉</p>
</div>
}
</main>

View File

@@ -89,6 +89,38 @@
}
}
// Día sin tareas: tarjeta amable con la mascota, nunca una pantalla vacía.
.freeday {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-2);
padding: var(--space-6) var(--space-4);
background: var(--surface);
border-radius: var(--radius-card);
box-shadow: var(--shadow-card);
text-align: center;
animation: slideUp 0.4s ease both;
&__mascot {
font-size: 96px;
line-height: 1;
animation: floatY 3s ease-in-out infinite;
}
&__title {
margin: var(--space-3) 0 0;
font-family: var(--font-display);
font-weight: 700;
font-size: 1.8rem;
color: var(--text-strong);
}
&__sub {
margin: 0;
font-size: 1.2rem;
color: var(--text-1);
}
}
// Moneda voladora: parte de la tarea y viaja al monedero.
.flycoin {
position: fixed;

View File

@@ -127,12 +127,14 @@ export class HomeComponent implements OnInit {
};
this.today.set(updated);
// Feedback positivo solo al ganar monedas (marcar, no desmarcar).
// Feedback positivo solo al ganar monedas (marcar, no desmarcar). Sonido y
// vibración se disparan juntos sobre el mismo evento (armonía multimodal).
if (result.coinsEarned > 0) {
this.flyCoins(ev.x, ev.y, result.coinsEarned);
this.wallet?.bump();
if (current.child.soundEnabled) {
this.sound.playReward();
this.sound.buzz();
}
}