Kiosk Redesign V3 “Gallery Wall” Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Replace the cramped 55/45 bottom section with a full-width rotating stage (photo > life > world on 60s cadence), simplify the compact calendar row, establish strong visual hierarchy, and make the news ticker pop.
Architecture: New useStageRotation hook manages a 3-slot rotation (0=photo, 1=life, 2=world). New KioskRotatingStage component replaces KioskBottomSection + KioskRotatingPanel, rendering all 3 slots at position: absolute with opacity crossfades. Photo rotation interval changes from internal 12-min timer to advancing once per stage cycle. CSS grid updated from 7 rows to a cleaner structure where the stage gets 1fr.
Tech Stack: React 19, TypeScript, Tailwind 4, CSS custom properties (existing --fd-* theme system)
Task 1: Create useStageRotation Hook
Files:
- Create: family-dashboard-code/src/hooks/useStageRotation.ts
Step 1: Write the hook
import { useState, useCallback } from 'react';
import { useInterval } from './useInterval';
export type StageSlot = 'photo' | 'life' | 'world';
const STAGES: StageSlot[] = ['photo', 'life', 'world'];
/**
* Cycles through 3 stage slots on a timer.
* Returns the active slot and a transitioning flag for crossfade.
*/
export function useStageRotation(cadenceMs: number = 60000) {
const [index, setIndex] = useState(0);
const [isTransitioning, setIsTransitioning] = useState(false);
const advance = useCallback(() => {
setIsTransitioning(true);
// After 800ms fade-out, swap content and fade in
setTimeout(() => {
setIndex((prev) => (prev + 1) % STAGES.length);
setIsTransitioning(false);
}, 800);
}, []);
useInterval(advance, cadenceMs);
return {
activeSlot: STAGES[index],
stageIndex: index,
isTransitioning,
};
}
Step 2: Verify it compiles
Run: cd family-dashboard-code && npx tsc --noEmit
Expected: No errors
Step 3: Commit
git add family-dashboard-code/src/hooks/useStageRotation.ts
git commit -m "feat(kiosk): add useStageRotation hook — 3-slot 60s rotation cycle"
Task 2: Create KioskRotatingStage Component
Files:
- Create: family-dashboard-code/src/components/kiosk/KioskRotatingStage.tsx
This component renders 3 absolutely-positioned layers (photo, life, world) and crossfades between them using the useStageRotation hook.
Step 1: Write the component
import React from 'react';
import { useStageRotation } from '../../hooks/useStageRotation';
import type { StageSlot } from '../../hooks/useStageRotation';
interface KioskRotatingStageProps {
photoSlot: React.ReactNode;
lifeSlot: React.ReactNode;
worldSlot: React.ReactNode;
cadenceMs?: number;
onSlotChange?: (slot: StageSlot) => void;
}
export function KioskRotatingStage({
photoSlot,
lifeSlot,
worldSlot,
cadenceMs = 60000,
onSlotChange,
}: KioskRotatingStageProps) {
const { activeSlot, isTransitioning } = useStageRotation(cadenceMs);
// Notify parent (used by photo frame to advance photo index)
React.useEffect(() => {
onSlotChange?.(activeSlot);
}, [activeSlot, onSlotChange]);
const slots: { key: StageSlot; content: React.ReactNode }[] = [
{ key: 'photo', content: photoSlot },
{ key: 'life', content: lifeSlot },
{ key: 'world', content: worldSlot },
];
return (
<div className="kiosk-stage">
{slots.map(({ key, content }) => {
const isActive = activeSlot === key && !isTransitioning;
return (
<div
key={key}
className="kiosk-stage-slot"
style={{
opacity: isActive ? 1 : 0,
transition: 'opacity 800ms ease-in-out',
pointerEvents: isActive ? 'auto' : 'none',
}}
>
{content}
</div>
);
})}
</div>
);
}
Step 2: Verify it compiles
Run: cd family-dashboard-code && npx tsc --noEmit
Expected: No errors
Step 3: Commit
git add family-dashboard-code/src/components/kiosk/KioskRotatingStage.tsx
git commit -m "feat(kiosk): add KioskRotatingStage — full-width 3-slot crossfade"
Task 3: Update CSS — New Grid, Stage Styles, Ticker, Compact Row
Files:
- Modify: family-dashboard-code/src/index.css
This is the largest single change. Update the kiosk grid definition, add stage styles, restyle the ticker, simplify compact row, and add hero elevation.
Step 1: Replace kiosk grid and related CSS
In index.css, make these changes:
- Replace
.kiosk-grid(lines 144-156) with:
.kiosk-grid {
display: grid;
grid-template-areas:
'header'
'today'
'next'
'compact'
'stage'
'ticker'
'status';
grid-template-columns: 1fr;
grid-template-rows: auto auto auto auto 1fr auto auto;
}
-
Remove
.kiosk-grid > [style*="grid-area: today"]rule (lines 158-160) — no longer needed. -
Replace
.kiosk-bottom-section(lines 179-186) with stage styles:
/* Rotating stage — full-width zone for photo/life/world rotation */
.kiosk-stage {
grid-area: stage;
position: relative;
min-height: 0;
overflow: hidden;
}
.kiosk-stage-slot {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
- Add gallery photo frame style (after the stage styles):
/* Gallery photo treatment inside rotating stage */
.kiosk-gallery-frame {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: clamp(12px, 1.5vw, 24px);
}
.kiosk-gallery-photo {
position: relative;
max-width: 100%;
max-height: 100%;
overflow: hidden;
border-radius: 0.75rem;
background: var(--fd-card-bg);
border: 1px solid color-mix(in srgb, var(--fd-accent) 15%, var(--fd-card-border));
padding: clamp(6px, 0.8vw, 12px);
}
.kiosk-gallery-photo .photo-frame {
border-radius: 0.5rem;
}
- Update
.kiosk-today-card(lines 288-300) — elevate the hero:
Replace background: var(--fd-card-bg); with:
background: color-mix(in srgb, var(--fd-card-bg) 100%, rgba(255,255,255,0.04));
And change border-left: 4px to border-left: 6px.
- Update
.kiosk-next-day-card(lines 317-327) — remove left accent:
Replace border-left: 3px solid color-mix(in srgb, var(--fd-accent) 40%, transparent); with:
border-left: 1px solid var(--fd-card-border);
- Replace
.kiosk-compact-gridand.kiosk-compact-card(lines 347-368) with simplified versions:
.kiosk-compact-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: clamp(8px, 0.8vw, 12px);
min-height: 0;
}
.kiosk-compact-cell {
display: flex;
flex-direction: column;
align-items: center;
gap: clamp(2px, 0.3vw, 4px);
padding: clamp(6px, 0.6vw, 10px);
}
- Replace ticker styles (lines 245-282) with the new marquee treatment:
/* News Ticker — "The Marquee" */
.ticker-container {
width: 100%;
overflow: hidden;
position: relative;
background: rgba(255, 255, 255, 0.06);
border-radius: 0.75rem;
padding: 0.6rem 1rem;
}
:root[data-phase="day"] .ticker-container {
background: rgba(0, 0, 0, 0.04);
}
.ticker-carousel {
position: relative;
height: 1.6em;
overflow: hidden;
}
.ticker-slide {
height: 1.6em;
line-height: 1.6em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: clamp(1rem, 1.3vw, 1.2rem);
color: var(--fd-text-1);
font-family: 'Outfit', sans-serif;
letter-spacing: 0.01em;
will-change: transform;
}
.ticker-source-label {
font-weight: 700;
color: var(--fd-accent);
margin-right: 0.35em;
}
.ticker-accent-pip {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--fd-accent);
margin-right: 0.5em;
vertical-align: middle;
}
Step 2: Verify CSS is valid — build the project
Run: cd family-dashboard-code && npx vite build 2>&1 | head -20
Expected: Build succeeds (warnings OK, no errors)
Step 3: Commit
git add family-dashboard-code/src/index.css
git commit -m "feat(kiosk): CSS overhaul — stage grid, gallery frame, ticker marquee, hero elevation"
Task 4: Simplify KioskCompactRow
Files:
- Modify: family-dashboard-code/src/components/kiosk/KioskCompactRow.tsx
Replace the current detailed event list with a minimal day name + weather icon + event count.
Step 1: Rewrite the component
Replace the entire file contents with:
import { format } from 'date-fns';
import type { CalendarEvent } from '../../lib/calendar/types';
import { WeatherIcon } from '../weather/WeatherIcon';
interface CompactDay {
date: Date;
dateStr: string;
events: CalendarEvent[];
}
interface KioskCompactRowProps {
days: CompactDay[];
dailyWeather: Array<{
high: number;
low: number;
weatherCode: number;
} | null>;
}
export function KioskCompactRow({ days, dailyWeather }: KioskCompactRowProps) {
return (
<div className="kiosk-compact-grid">
{days.map((day, i) => {
const dayTitle = format(day.date, 'EEE');
const weather = dailyWeather[i];
const eventCount = day.events.length;
return (
<div key={day.dateStr} className="kiosk-compact-cell">
<span
style={{
fontSize: 'clamp(1.2rem, 1.5vw, 1.5rem)',
fontWeight: 600,
color: 'var(--fd-text-1)',
}}
>
{dayTitle}
</span>
{weather && (
<div
className="flex items-center gap-1"
style={{
fontSize: 'clamp(0.85rem, 1vw, 1.1rem)',
color: 'var(--fd-accent)',
}}
>
<WeatherIcon
code={weather.weatherCode}
size="clamp(1.1rem, 1.3vw, 1.5rem)"
/>
<span className="tabular-nums font-medium">
{Math.round(weather.high)}°
</span>
</div>
)}
<span
style={{
fontSize: 'clamp(0.8rem, 0.95vw, 1rem)',
color: eventCount > 0 ? 'var(--fd-text-2)' : 'var(--fd-text-2)',
opacity: eventCount > 0 ? 0.8 : 0.4,
}}
>
{eventCount === 0
? 'Free'
: eventCount === 1
? '1 event'
: `${eventCount} events`}
</span>
</div>
);
})}
</div>
);
}
Step 2: Verify it compiles
Run: cd family-dashboard-code && npx tsc --noEmit
Expected: No errors
Step 3: Commit
git add family-dashboard-code/src/components/kiosk/KioskCompactRow.tsx
git commit -m "feat(kiosk): simplify compact row — day name, weather icon, event count only"
Task 5: Update KioskTodayCard — Hero Elevation
Files:
- Modify: family-dashboard-code/src/components/kiosk/KioskTodayCard.tsx
Only two small changes: bump the “Today” title size and ensure the elevated hero background comes from CSS (already handled in Task 3).
Step 1: Update the title font size
In KioskTodayCard.tsx, line 49, change:
fontSize: 'clamp(1.6rem, 2.5vw, 2.2rem)',
to:
fontSize: 'clamp(1.8rem, 3vw, 2.6rem)',
Step 2: Verify it compiles
Run: cd family-dashboard-code && npx tsc --noEmit
Expected: No errors
Step 3: Commit
git add family-dashboard-code/src/components/kiosk/KioskTodayCard.tsx
git commit -m "feat(kiosk): bump Today title to hero size"
Task 6: Update KioskNextDayCard — Remove Accent Border, Bump Text
Files:
- Modify: family-dashboard-code/src/components/kiosk/KioskNextDayCard.tsx
The left accent border removal is handled in CSS (Task 3). Here we bump text sizes ~10%.
Step 1: Bump font sizes
In KioskNextDayCard.tsx:
- Line 48, day title: change
clamp(1.2rem, 1.8vw, 1.6rem)toclamp(1.35rem, 2vw, 1.75rem) - Line 56, date label: change
clamp(0.78rem, 0.9vw, 1rem)toclamp(0.85rem, 1vw, 1.1rem) - Line 70, weather text: change
clamp(0.78rem, 0.9vw, 1rem)toclamp(0.85rem, 1vw, 1.1rem) - Line 93, event time: change
clamp(1.08rem, 1.28vw, 1.43rem)toclamp(1.2rem, 1.4vw, 1.55rem) - Line 99, event emoji: change
clamp(1.17rem, 1.35vw, 1.5rem)toclamp(1.3rem, 1.5vw, 1.65rem) - Line 105, event summary: change
clamp(1.28rem, 1.8vw, 1.65rem)toclamp(1.4rem, 2vw, 1.8rem)
Step 2: Verify it compiles
Run: cd family-dashboard-code && npx tsc --noEmit
Expected: No errors
Step 3: Commit
git add family-dashboard-code/src/components/kiosk/KioskNextDayCard.tsx
git commit -m "feat(kiosk): bump next-day card text sizes ~10%"
Task 7: Update KioskPageA and KioskPageB — Full-Width Sizing
Files:
- Modify: family-dashboard-code/src/components/kiosk/KioskPageA.tsx
- Modify: family-dashboard-code/src/components/kiosk/KioskPageB.tsx
Both panels need larger text now that they get full viewport width in the rotating stage.
Step 1: Update KioskPageA text sizes
In KioskPageA.tsx:
- Line 38,
itemStylefontSize: changeclamp(13px, 1.2vw, 17px)toclamp(1.1rem, 1.5vw, 1.4rem) - Line 43,
secondaryStylefontSize: changeclamp(13px, 1.2vw, 17px)toclamp(1rem, 1.3vw, 1.25rem) - Line 67, emoji size: change
clamp(1rem, 1.2vw, 1.3rem)toclamp(1.2rem, 1.5vw, 1.6rem) - Line 73, countdown days: change
clamp(13px, 1.2vw, 17px)toclamp(1.1rem, 1.5vw, 1.4rem)
Step 2: Update KioskPageB text sizes
In KioskPageB.tsx:
- Line 81,
itemStylefontSize: changeclamp(13px, 1.2vw, 17px)toclamp(1.1rem, 1.5vw, 1.4rem) - Line 86,
secondaryStylefontSize: changeclamp(13px, 1.2vw, 17px)toclamp(1rem, 1.3vw, 1.25rem)
Step 3: Verify it compiles
Run: cd family-dashboard-code && npx tsc --noEmit
Expected: No errors
Step 4: Commit
git add family-dashboard-code/src/components/kiosk/KioskPageA.tsx family-dashboard-code/src/components/kiosk/KioskPageB.tsx
git commit -m "feat(kiosk): bump page A/B text sizes for full-width stage"
Task 8: Update KioskNewsTicker — Add Accent Pip
Files:
- Modify: family-dashboard-code/src/components/kiosk/KioskNewsTicker.tsx
Add the accent pip before each headline’s source label. CSS changes already handled in Task 3.
Step 1: Add pip to ticker slides
In KioskNewsTicker.tsx, update both ticker-slide divs (lines 47-49 and 55-57).
Change:
<span className="ticker-source-label">{current.source}:</span>
to:
<span className="ticker-accent-pip" aria-hidden="true" />
<span className="ticker-source-label">{current.source}:</span>
Do the same for the next slide (replace {next.source} line similarly).
Step 2: Verify it compiles
Run: cd family-dashboard-code && npx tsc --noEmit
Expected: No errors
Step 3: Commit
git add family-dashboard-code/src/components/kiosk/KioskNewsTicker.tsx
git commit -m "feat(kiosk): add accent pip to news ticker headlines"
Task 9: Update StatusBar — Font Size Bump
Files:
- Modify: family-dashboard-code/src/components/layout/StatusBar.tsx
Step 1: Bump kiosk font size
In StatusBar.tsx, line 31, change:
? 'clamp(0.86rem,1.05vw,1rem)'
to:
? 'clamp(0.9rem,1.1vw,1.05rem)'
Step 2: Commit
git add family-dashboard-code/src/components/layout/StatusBar.tsx
git commit -m "feat(kiosk): nudge status bar font size up"
Task 10: Wire Everything Together — Update KioskDashboard
Files:
- Modify: family-dashboard-code/src/components/kiosk/KioskDashboard.tsx
This is the integration task. Replace KioskBottomSection with KioskRotatingStage, update grid area naming, and connect the photo advance callback.
Step 1: Rewrite KioskDashboard
Replace the entire file with:
import { useCallback, useRef } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { Header } from '../layout/Header';
import { StatusBar } from '../layout/StatusBar';
import { KioskTodayCard } from './KioskTodayCard';
import { KioskNextDayCard } from './KioskNextDayCard';
import { KioskCompactRow } from './KioskCompactRow';
import { KioskRotatingStage } from './KioskRotatingStage';
import { KioskPhotoFrame } from './KioskPhotoFrame';
import { KioskPageA } from './KioskPageA';
import { KioskPageB } from './KioskPageB';
import { KioskNewsTicker } from './KioskNewsTicker';
import { PanelFallback, GlobalFallback, logError } from '../ErrorFallback';
import { useCalendar } from '../../hooks/useCalendar';
import { useWeather } from '../../hooks/useWeather';
import type { StageSlot } from '../../hooks/useStageRotation';
function getDailyWeather(weather: ReturnType<typeof useWeather>['data'], dayIndex: number) {
if (!weather?.daily || dayIndex >= weather.daily.time.length) return null;
return {
high: weather.daily.temperature_2m_max[dayIndex],
low: weather.daily.temperature_2m_min[dayIndex],
weatherCode: weather.daily.weather_code[dayIndex],
};
}
export function KioskDashboard() {
const { days } = useCalendar();
const { data: weather } = useWeather();
const todayDay = days[0];
const tomorrowDay = days[1];
const dayAfterDay = days[2];
const compactDays = days.slice(3, 7);
const compactWeather = compactDays.map((_, i) => getDailyWeather(weather, i + 3));
// Track when stage rotates back to photo slot so we can trigger
// the next photo. We store a ref to avoid re-renders.
const prevSlotRef = useRef<StageSlot>('photo');
const handleSlotChange = useCallback((slot: StageSlot) => {
// When we just arrived at 'photo' from a non-photo slot,
// the photo frame's own rotation timer handles advancement.
// No extra wiring needed — the photo frame's useGooglePhotos
// hook runs its own interval. The stage just shows/hides it.
prevSlotRef.current = slot;
}, []);
return (
<ErrorBoundary FallbackComponent={GlobalFallback} onError={logError}>
<div className="kiosk-grid h-dvh w-screen overflow-hidden p-[clamp(14px,1.5vw,24px)] gap-[clamp(10px,1vw,18px)]">
<ErrorBoundary FallbackComponent={PanelFallback} onError={logError}>
<Header variant="kiosk" />
</ErrorBoundary>
{todayDay && (
<ErrorBoundary FallbackComponent={PanelFallback} onError={logError}>
<div style={{ gridArea: 'today', minHeight: 0, overflow: 'hidden' }}>
<KioskTodayCard day={todayDay} weather={weather} />
</div>
</ErrorBoundary>
)}
<div className="kiosk-next-row">
{tomorrowDay && (
<ErrorBoundary FallbackComponent={PanelFallback} onError={logError}>
<KioskNextDayCard
day={tomorrowDay}
dailyWeather={getDailyWeather(weather, 1)}
/>
</ErrorBoundary>
)}
{dayAfterDay && (
<ErrorBoundary FallbackComponent={PanelFallback} onError={logError}>
<KioskNextDayCard
day={dayAfterDay}
dailyWeather={getDailyWeather(weather, 2)}
/>
</ErrorBoundary>
)}
</div>
<ErrorBoundary FallbackComponent={PanelFallback} onError={logError}>
<div className="kiosk-compact-area">
<KioskCompactRow days={compactDays} dailyWeather={compactWeather} />
</div>
</ErrorBoundary>
<ErrorBoundary FallbackComponent={PanelFallback} onError={logError}>
<KioskRotatingStage
cadenceMs={60000}
onSlotChange={handleSlotChange}
photoSlot={
<ErrorBoundary FallbackComponent={PanelFallback} onError={logError}>
<div className="kiosk-gallery-frame">
<div className="kiosk-gallery-photo">
<KioskPhotoFrame />
</div>
</div>
</ErrorBoundary>
}
lifeSlot={
<ErrorBoundary FallbackComponent={PanelFallback} onError={logError}>
<KioskPageA />
</ErrorBoundary>
}
worldSlot={
<ErrorBoundary FallbackComponent={PanelFallback} onError={logError}>
<KioskPageB />
</ErrorBoundary>
}
/>
</ErrorBoundary>
<div className="kiosk-ticker-area">
<ErrorBoundary FallbackComponent={PanelFallback} onError={logError}>
<KioskNewsTicker />
</ErrorBoundary>
</div>
<StatusBar variant="kiosk" />
</div>
</ErrorBoundary>
);
}
Step 2: Verify it compiles
Run: cd family-dashboard-code && npx tsc --noEmit
Expected: No errors
Step 3: Verify it builds
Run: cd family-dashboard-code && npx vite build 2>&1 | tail -5
Expected: Build succeeds
Step 4: Commit
git add family-dashboard-code/src/components/kiosk/KioskDashboard.tsx
git commit -m "feat(kiosk): wire KioskRotatingStage into dashboard — gallery wall layout"
Task 11: Remove Dead Code
Files:
- Delete: family-dashboard-code/src/components/kiosk/KioskBottomSection.tsx
- Delete: family-dashboard-code/src/components/kiosk/KioskRotatingPanel.tsx
- Delete: family-dashboard-code/src/hooks/usePageRotation.ts
Step 1: Verify no other imports reference these files
Search for imports of the deleted modules across the codebase. The only consumer was KioskDashboard.tsx (updated in Task 10) and KioskBottomSection.tsx itself.
Run: grep -r "KioskBottomSection\|KioskRotatingPanel\|usePageRotation" family-dashboard-code/src/ --include="*.ts" --include="*.tsx" -l
Expected: No results (or only the files being deleted)
Step 2: Delete the files
rm family-dashboard-code/src/components/kiosk/KioskBottomSection.tsx
rm family-dashboard-code/src/components/kiosk/KioskRotatingPanel.tsx
rm family-dashboard-code/src/hooks/usePageRotation.ts
Step 3: Verify it still compiles and builds
Run: cd family-dashboard-code && npx tsc --noEmit && npx vite build 2>&1 | tail -5
Expected: No errors
Step 4: Commit
git add -u family-dashboard-code/src/components/kiosk/KioskBottomSection.tsx family-dashboard-code/src/components/kiosk/KioskRotatingPanel.tsx family-dashboard-code/src/hooks/usePageRotation.ts
git commit -m "chore(kiosk): remove KioskBottomSection, KioskRotatingPanel, usePageRotation"
Task 12: Smoke Test & Final Build
Step 1: Full type check
Run: cd family-dashboard-code && npx tsc --noEmit
Expected: No errors
Step 2: Full production build
Run: cd family-dashboard-code && npx vite build
Expected: Build succeeds with no errors
Step 3: Manual verification
Run: cd family-dashboard-code && npx vite preview --port 4173
Open http://localhost:4173 in browser. Verify:
- Kiosk grid shows header, today hero, tomorrow/day-after, compact row, rotating stage, ticker, status
- Rotating stage cycles through photo > life > world every 60s with crossfade
- Compact row shows only day name, weather icon, event count
- Ticker has visible background, accent pip, larger text
- Today card has prominent 6px left accent border
Step 4: Final commit (if any fixes needed)
git commit -m "fix(kiosk): address smoke test findings"