web: mobile pass, slice 10 of M3
The MobileScoreStrip lands: sticky under the app bar, one pill per panel with the round-synced colored share, horizontally scrollable (the only horizontal scroller, verified at 320 px), tapping scrolls to the panel (instant under reduced motion, scroll-margin clears the sticky stack). Touch targets reach 44 px on mobile: player buttons, panel kebabs, and toolbar buttons; Export collapses to its icon so the toolbar stays one row down to 320 px.
This commit is contained in:
parent
e0ecc5c6ba
commit
609ba6f0dd
5 changed files with 163 additions and 3 deletions
117
web/src/components/MobileScoreStrip.vue
Normal file
117
web/src/components/MobileScoreStrip.vue
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useSimStore } from '@/composables/useSimStore'
|
||||
import { roundPct } from '@/lib/format'
|
||||
import { reachedCountAtRound } from '@/lib/reach'
|
||||
|
||||
// Mobile only (spec section 9): a sticky strip of one pill per panel so
|
||||
// the comparison survives the single-column stack. The percentages follow
|
||||
// the playhead like the panel cards; tapping a chip scrolls to its panel.
|
||||
|
||||
const store = useSimStore()
|
||||
|
||||
interface StripChip {
|
||||
panelId: string
|
||||
label: string
|
||||
pctText: string
|
||||
tone: 'good' | 'bad' | 'pending'
|
||||
}
|
||||
|
||||
const chips = computed<StripChip[]>(() =>
|
||||
store.state.panels.map((panel) => {
|
||||
const result = store.state.resultsByPanelId[panel.id]
|
||||
if (!result) return { panelId: panel.id, label: panel.label, pctText: '…', tone: 'pending' }
|
||||
const numStudents = store.effectiveConfig(panel).numStudents
|
||||
const pct = (reachedCountAtRound(result.reachedAtRound, store.state.round) / numStudents) * 100
|
||||
return {
|
||||
panelId: panel.id,
|
||||
label: panel.label,
|
||||
pctText: `${roundPct(pct)}%`,
|
||||
tone: roundPct(pct) <= store.preset.toneThresholdPct ? 'good' : 'bad',
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
function scrollToPanel(panelId: string) {
|
||||
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
document
|
||||
.getElementById(`panel-${panelId}`)
|
||||
?.scrollIntoView({ behavior: reducedMotion ? 'auto' : 'smooth', block: 'start' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="strip">
|
||||
<button
|
||||
v-for="chip in chips"
|
||||
:key="chip.panelId"
|
||||
class="s-chip"
|
||||
type="button"
|
||||
@click="scrollToPanel(chip.panelId)"
|
||||
>
|
||||
<span class="s-label">{{ chip.label }}</span>
|
||||
<b :class="chip.tone">{{ chip.pctText }}</b>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.strip {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.strip {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
margin-top: 18px;
|
||||
position: sticky;
|
||||
top: 56px;
|
||||
z-index: 10;
|
||||
background: var(--bg);
|
||||
padding: 10px 0;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.s-chip {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 44px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
padding: 6px 14px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 550;
|
||||
color: var(--ink-3);
|
||||
box-shadow: var(--shadow);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.s-label {
|
||||
max-width: 130px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.s-chip b {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
b.bad {
|
||||
color: var(--spread);
|
||||
}
|
||||
|
||||
b.good {
|
||||
color: var(--edu);
|
||||
}
|
||||
|
||||
b.pending {
|
||||
color: var(--ink-4);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue