spreadlab/web/src/App.vue
Justin Visser 0e3a14a0b3 web: playback, slice 4 of M3
usePlayback drives one global round on a rAF timer: 700 ms per round at
1x, speed pill cycling 0.5/1/2, play-once autoplay after the first load,
no looping; at the final round the play button becomes replay. Under
prefers-reduced-motion there is no autoplay, the page settles on the
final round, and the cadence slows to 1000 ms.

The PlayerBar is the mockup's centered pill with a native range
scrubber (one tick per round, aria-valuetext, seeking pauses) and the
global keyboard map: Space toggles, arrows step, Home/End jump; text
inputs and native control handling are left alone. A single polite live
region announces run updates, pauses, scrub releases, and the final
state.

Nodes reached in the current round pop in (scale 0.6 to 1, 250 ms
ease-out) with a small deterministic stagger so each round reads as a
wave. Per Justin's feedback the panel percentage and reached count now
follow the playhead instead of sitting on the final outcome; the hero
keeps the final numbers, since its sentence claims outcomes.
2026-06-10 17:15:38 +02:00

57 lines
1.5 KiB
Vue

<script setup lang="ts">
import { onMounted } from 'vue'
import AppBar from '@/components/AppBar.vue'
import ErrorBanner from '@/components/ErrorBanner.vue'
import FooterDisclaimer from '@/components/FooterDisclaimer.vue'
import HeroHeadline from '@/components/HeroHeadline.vue'
import LegendRow from '@/components/LegendRow.vue'
import PanelGrid from '@/components/PanelGrid.vue'
import PlayerBar from '@/components/PlayerBar.vue'
import ResultsTable from '@/components/ResultsTable.vue'
import ScenarioToolbar from '@/components/ScenarioToolbar.vue'
import { usePlayback } from '@/composables/usePlayback'
import { useSimStore } from '@/composables/useSimStore'
import { useTheme } from '@/composables/useTheme'
const store = useSimStore()
const playback = usePlayback()
useTheme() // resolve and apply the theme before first paint of the app
onMounted(async () => {
await store.initialize()
playback.autoplayOnce()
})
</script>
<template>
<AppBar />
<main class="page">
<HeroHeadline />
<ScenarioToolbar />
<ErrorBanner />
<PanelGrid />
<LegendRow />
<PlayerBar />
<ResultsTable
:panels="store.state.panels"
:results-by-panel-id="store.state.resultsByPanelId"
:base="store.state.base"
/>
</main>
<FooterDisclaimer />
<div class="visually-hidden" aria-live="polite">{{ store.state.announcement }}</div>
</template>
<style scoped>
.page {
max-width: 1240px;
margin: 0 auto;
padding: 36px 28px 30px;
}
@media (max-width: 760px) {
.page {
padding: 22px 16px;
}
}
</style>