web: the dashboard page, slice 3 of M3
The milestone 2 parity page becomes the spec's dashboard: app bar with wordmark, disclaimer badge and About popover, theme toggle (persisted, defaults to prefers-color-scheme, applied pre-mount to avoid a flash), hero with the preset narrative and live toned percentages, scenario toolbar, panel cards with skeleton loading and dim-while-refreshing, legend, and footer. ComparisonTable evolves into the collapsed ResultsTable (test moved along, plus a formatPct rounding case). NetworkView renders the real topology with the spec's shape encodings. The layout is seeded d3-force (mulberry32 random source, seeded initial positions, 300 synchronous ticks, cached per graph hash); anisotropic forceX/forceY pulls settle the cloud into the wide card shape so the fit stays uniform-scale and the spacing organic. Justin asked for a taller network area (380x230) and a reading caption under the legend; the caption is study copy, so StudyPreset gains a readingCaption field. Slices still to come keep their controls visibly parked: Export and the panel kebab render disabled until slices 11 and 7.
This commit is contained in:
parent
2f21248c20
commit
71baf10e73
28 changed files with 1766 additions and 300 deletions
|
|
@ -1,40 +1,51 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { fetchDefaultConfig, runComparison } from '@/lib/api'
|
||||
import type { ComparisonResponse } from '@/types/api'
|
||||
import ComparisonTable from '@/components/ComparisonTable.vue'
|
||||
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 ResultsTable from '@/components/ResultsTable.vue'
|
||||
import ScenarioToolbar from '@/components/ScenarioToolbar.vue'
|
||||
import { useSimStore } from '@/composables/useSimStore'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
|
||||
const comparison = ref<ComparisonResponse | null>(null)
|
||||
const error = ref<string | null>(null)
|
||||
const store = useSimStore()
|
||||
useTheme() // resolve and apply the theme before first paint of the app
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const config = await fetchDefaultConfig()
|
||||
comparison.value = await runComparison(config)
|
||||
} catch (cause) {
|
||||
error.value = cause instanceof Error ? cause.message : String(cause)
|
||||
}
|
||||
onMounted(() => {
|
||||
void store.initialize()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<h1>spreadlab</h1>
|
||||
<p>
|
||||
How a non-consensual deepfake spreads through a simulated school year
|
||||
group, and what an education program changes. Illustrative, not
|
||||
validated.
|
||||
</p>
|
||||
<p v-if="error" role="alert">Could not reach the spreadlab API: {{ error }}</p>
|
||||
<ComparisonTable v-else-if="comparison" :comparison="comparison" />
|
||||
<p v-else>Running scenarios…</p>
|
||||
<AppBar />
|
||||
<main class="page">
|
||||
<HeroHeadline />
|
||||
<ScenarioToolbar />
|
||||
<ErrorBanner />
|
||||
<PanelGrid />
|
||||
<LegendRow />
|
||||
<ResultsTable
|
||||
:panels="store.state.panels"
|
||||
:results-by-panel-id="store.state.resultsByPanelId"
|
||||
:base="store.state.base"
|
||||
/>
|
||||
</main>
|
||||
<FooterDisclaimer />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
main {
|
||||
max-width: 48rem;
|
||||
.page {
|
||||
max-width: 1240px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1rem;
|
||||
padding: 36px 28px 30px;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.page {
|
||||
padding: 22px 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue