web: single-screen redesign with the literature levers
The dashboard is now a single-screen app shell: - app-bar page tabs (Model + placeholder Explainer/Findings/Guides) via lightweight hash routing (usePage); the wordmark links home - a 2x2 graph grid that fills the viewport, rows sized to half its height so the four cells always show; a + Add tile is the 4th cell, a 5th scenario scrolls (with a reserved scrollbar gutter) - a left settings sidebar exposing the forwarding composite as grouped levers (novelty, harm awareness, program strength, plus baseline forwarding and education budget); the card is a CSS container so the sliders stack to width - a full-width dock (legend, full-width player, Export / Reach over time / Data table); the reach chart and data table open as overlays - the illustrative note moved under the hero ScenarioToolbar is removed (the + tile and the dock Export replace it). Deviates from the original ui-spec, which is amended with a pointer.
This commit is contained in:
parent
430d59a2e8
commit
4ac7ba1624
15 changed files with 832 additions and 280 deletions
|
|
@ -5,8 +5,10 @@ import LeverSlider from './LeverSlider.vue'
|
|||
import { useSimStore } from '@/composables/useSimStore'
|
||||
import { formatPct } from '@/lib/format'
|
||||
|
||||
// The world card (spec slice 6): two headline levers, the advanced grid,
|
||||
// and the inline spot for engine 400s. Lever changes hit the store
|
||||
// The world card (spec slice 6, extended 2026-06-18 for the literature
|
||||
// levers): the forwarding composite and the program, grouped so the levers
|
||||
// read as a structured model rather than a flat wall, plus the advanced
|
||||
// grid and the inline spot for engine 400s. Lever changes hit the store
|
||||
// immediately and rerun every panel after the shared debounce.
|
||||
|
||||
const store = useSimStore()
|
||||
|
|
@ -18,12 +20,15 @@ const educationDisplay = computed(() => {
|
|||
return `${base.value.numEducated} · ${formatPct(share)}`
|
||||
})
|
||||
|
||||
const offendsForwardProb = computed(
|
||||
() => store.state.validationError?.includes('forwardProb') ?? false,
|
||||
)
|
||||
const offendsNumEducated = computed(
|
||||
() => store.state.validationError?.includes('numEducated') ?? false,
|
||||
)
|
||||
// The engine 400 names the offending JSON field; mark the matching lever.
|
||||
const offends = (field: string) =>
|
||||
computed(() => store.state.validationError?.includes(field) ?? false)
|
||||
|
||||
const offendsForwardProb = offends('forwardProb')
|
||||
const offendsNovelty = offends('novelty')
|
||||
const offendsHarmAwareness = offends('harmAwareness')
|
||||
const offendsNumEducated = offends('numEducated')
|
||||
const offendsProgramEffect = offends('programEffect')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -32,28 +37,74 @@ const offendsNumEducated = computed(
|
|||
<p v-if="store.state.validationError" class="invalid-note" role="alert">
|
||||
{{ store.state.validationError }}
|
||||
</p>
|
||||
<LeverSlider
|
||||
label="Chance to forward"
|
||||
hint="per friendship, per round"
|
||||
:value="base.forwardProb"
|
||||
:min="store.state.bounds?.forwardProb.min ?? 0"
|
||||
:max="store.state.bounds?.forwardProb.max ?? 1"
|
||||
:step="0.01"
|
||||
:display-value="formatPct(base.forwardProb * 100)"
|
||||
:invalid="offendsForwardProb"
|
||||
@update="(value) => store.setBaseField('forwardProb', value)"
|
||||
/>
|
||||
<LeverSlider
|
||||
label="Education budget"
|
||||
hint="students the program reaches"
|
||||
:value="base.numEducated"
|
||||
:min="0"
|
||||
:max="base.numStudents"
|
||||
:step="1"
|
||||
:display-value="educationDisplay"
|
||||
:invalid="offendsNumEducated"
|
||||
@update="(value) => store.setBaseField('numEducated', value)"
|
||||
/>
|
||||
|
||||
<div class="group">
|
||||
<p class="group-label">The fake</p>
|
||||
<LeverSlider
|
||||
label="Chance to forward"
|
||||
hint="baseline, per friendship"
|
||||
:value="base.forwardProb"
|
||||
:min="store.state.bounds?.forwardProb.min ?? 0"
|
||||
:max="store.state.bounds?.forwardProb.max ?? 1"
|
||||
:step="0.01"
|
||||
:display-value="formatPct(base.forwardProb * 100)"
|
||||
:invalid="offendsForwardProb"
|
||||
@update="(value) => store.setBaseField('forwardProb', value)"
|
||||
/>
|
||||
<LeverSlider
|
||||
label="Novelty / shock"
|
||||
hint="how new or shocking it feels"
|
||||
:value="base.novelty"
|
||||
:min="store.state.bounds?.novelty.min ?? 0"
|
||||
:max="store.state.bounds?.novelty.max ?? 1"
|
||||
:step="0.05"
|
||||
:display-value="formatPct(base.novelty * 100)"
|
||||
:invalid="offendsNovelty"
|
||||
@update="(value) => store.setBaseField('novelty', value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<p class="group-label">The year group</p>
|
||||
<LeverSlider
|
||||
label="Harm awareness"
|
||||
hint="shared AI-literacy in the year"
|
||||
:value="base.harmAwareness"
|
||||
:min="store.state.bounds?.harmAwareness.min ?? 0"
|
||||
:max="store.state.bounds?.harmAwareness.max ?? 1"
|
||||
:step="0.05"
|
||||
:display-value="formatPct(base.harmAwareness * 100)"
|
||||
:invalid="offendsHarmAwareness"
|
||||
@update="(value) => store.setBaseField('harmAwareness', value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<p class="group-label">The program</p>
|
||||
<LeverSlider
|
||||
label="Education budget"
|
||||
hint="students the program reaches"
|
||||
:value="base.numEducated"
|
||||
:min="0"
|
||||
:max="base.numStudents"
|
||||
:step="1"
|
||||
:display-value="educationDisplay"
|
||||
:invalid="offendsNumEducated"
|
||||
@update="(value) => store.setBaseField('numEducated', value)"
|
||||
/>
|
||||
<LeverSlider
|
||||
label="Program strength"
|
||||
hint="how much it cuts an educated student's sharing"
|
||||
:value="base.programEffect"
|
||||
:min="store.state.bounds?.programEffect.min ?? 0"
|
||||
:max="store.state.bounds?.programEffect.max ?? 1"
|
||||
:step="0.05"
|
||||
:display-value="formatPct(base.programEffect * 100)"
|
||||
:invalid="offendsProgramEffect"
|
||||
@update="(value) => store.setBaseField('programEffect', value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AdvancedFields />
|
||||
</section>
|
||||
</template>
|
||||
|
|
@ -65,6 +116,10 @@ const offendsNumEducated = computed(
|
|||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 20px 22px;
|
||||
/* The card is a container so its sliders and advanced grid adapt to the
|
||||
sidebar width (narrow -> stacked), not the viewport width. */
|
||||
container-type: inline-size;
|
||||
container-name: controls;
|
||||
}
|
||||
|
||||
h3 {
|
||||
|
|
@ -75,6 +130,19 @@ h3 {
|
|||
color: var(--ink-4);
|
||||
}
|
||||
|
||||
.group + .group {
|
||||
margin-top: 22px;
|
||||
border-top: 1px solid var(--border-soft);
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.group-label {
|
||||
margin-top: 16px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
.invalid-note {
|
||||
margin-top: 8px;
|
||||
font-size: 12.5px;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue