fix(compare): mobile measure-first cards to match the mockup
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m2s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 11s
PR Checks / Build Frontend (no push) (pull_request) Successful in 41s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 2m57s
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m2s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 11s
PR Checks / Build Frontend (no push) (pull_request) Successful in 41s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 2m57s
The grid sections (At a glance, Ofsted, Getting a place, Who goes there) collapsed generically on mobile — grey label pills, full names wrapping to 3 lines, no dots — making the page ~2x the mockup's height and 'significantly different' from the mobile design. Each measure is now wrapped in a <Measure> that is display:contents on desktop (so the label + cells still flow into the shared aligned grid, unchanged) and a white card on mobile with compact [dot][short name] [value] rows — matching the mobile mockup. The sticky school bar becomes scrollable short-name pills on mobile too. Adds a shortName() util. Desktop layout is unchanged (display:contents dissolves the wrapper). Validated the card mechanism and real content shapes (report-card cell, badges, %+chip rows) via static previews at both widths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
import { summariseAdmissions } from '@/lib/compareLogic';
|
||||
import type { ComparisonData, School } from '@/lib/types';
|
||||
import { CHART_COLORS } from '@/lib/utils';
|
||||
import { Cell, Chip, RowLabel, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
||||
import { Cell, Chip, Measure, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
||||
|
||||
export function CompareAdmissions({
|
||||
schools,
|
||||
@@ -49,9 +49,10 @@ export function CompareAdmissions({
|
||||
}
|
||||
>
|
||||
<SectionGrid schools={schools}>
|
||||
<RowLabel tip="How many application forms named the school at any preference rank — not the number of families competing head-to-head for a place.">
|
||||
Interest in the school
|
||||
</RowLabel>
|
||||
<Measure
|
||||
tip="How many application forms named the school at any preference rank — not the number of families competing head-to-head for a place."
|
||||
label="Interest in the school"
|
||||
>
|
||||
{schools.map((school, i) => {
|
||||
const a = rows[i];
|
||||
return (
|
||||
@@ -68,7 +69,9 @@ export function CompareAdmissions({
|
||||
);
|
||||
})}
|
||||
|
||||
<RowLabel>First-choice families offered a place</RowLabel>
|
||||
</Measure>
|
||||
|
||||
<Measure label="First-choice families offered a place">
|
||||
{schools.map((school, i) => {
|
||||
const summary = summariseAdmissions(rows[i]);
|
||||
return (
|
||||
@@ -95,7 +98,9 @@ export function CompareAdmissions({
|
||||
);
|
||||
})}
|
||||
|
||||
<RowLabel>What this means</RowLabel>
|
||||
</Measure>
|
||||
|
||||
<Measure label="What this means">
|
||||
{schools.map((school, i) => {
|
||||
const a = rows[i];
|
||||
const summary = summariseAdmissions(a);
|
||||
@@ -118,6 +123,7 @@ export function CompareAdmissions({
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
</Measure>
|
||||
</SectionGrid>
|
||||
</Section>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
type ReportCardSummary,
|
||||
} from '@/lib/compareLogic';
|
||||
import type { Benchmarks, ComparisonData, NationalAverages, School } from '@/lib/types';
|
||||
import { Cell, Chip, RowLabel, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
||||
import { Cell, Chip, Measure, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
||||
|
||||
function ReportCardChips({ summary }: { summary: ReportCardSummary }) {
|
||||
return (
|
||||
@@ -69,7 +69,7 @@ export function CompareAtAGlance({
|
||||
return (
|
||||
<Section title="At a glance" how="The short version — each row below is explained in its own section further down.">
|
||||
<SectionGrid schools={schools}>
|
||||
<RowLabel>Latest Ofsted inspection</RowLabel>
|
||||
<Measure label="Latest Ofsted inspection">
|
||||
{schools.map((school, i) => {
|
||||
const display = ofstedDisplay(data[String(school.urn)]?.ofsted);
|
||||
return (
|
||||
@@ -87,16 +87,16 @@ export function CompareAtAGlance({
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
</Measure>
|
||||
|
||||
<RowLabel
|
||||
<Measure
|
||||
tip={
|
||||
isSecondary
|
||||
? 'Average Attainment 8 score across GCSE subjects (latest year).'
|
||||
: '% of Year 6 pupils reaching the expected standard in reading, writing and maths (latest year).'
|
||||
}
|
||||
label={isSecondary ? 'Attainment 8 score' : 'Children reaching the expected standard'}
|
||||
>
|
||||
{isSecondary ? 'Attainment 8 score' : 'Children reaching the expected standard'}
|
||||
</RowLabel>
|
||||
{schools.map((school, i) => {
|
||||
const value = headlineValues[i];
|
||||
return (
|
||||
@@ -131,8 +131,9 @@ export function CompareAtAGlance({
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
</Measure>
|
||||
|
||||
<RowLabel>Getting a place</RowLabel>
|
||||
<Measure label="Getting a place">
|
||||
{schools.map((school, i) => {
|
||||
const summary = summariseAdmissions(data[String(school.urn)]?.admissions);
|
||||
return (
|
||||
@@ -148,8 +149,9 @@ export function CompareAtAGlance({
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
</Measure>
|
||||
|
||||
<RowLabel>Size</RowLabel>
|
||||
<Measure label="Size">
|
||||
{schools.map((school, i) => {
|
||||
const census = data[String(school.urn)]?.census;
|
||||
const pupils = census?.total_pupils ?? school.total_pupils ?? null;
|
||||
@@ -174,6 +176,7 @@ export function CompareAtAGlance({
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
</Measure>
|
||||
</SectionGrid>
|
||||
</Section>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
import { verdict } from '@/lib/compareLogic';
|
||||
import type { Benchmarks, ComparisonData, School } from '@/lib/types';
|
||||
import { Cell, Chip, RowLabel, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
||||
import { Cell, Chip, Measure, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
||||
|
||||
function pctSplit(part: number | null | undefined, total: number | null | undefined): string | null {
|
||||
if (part == null || total == null || total === 0) return null;
|
||||
@@ -48,7 +48,7 @@ export function CompareCommunity({
|
||||
how="The school's community, from the latest school census. State-school averages are computed from our dataset and shown for context — there's no “right” number here."
|
||||
>
|
||||
<SectionGrid schools={schools}>
|
||||
<RowLabel>Pupils on roll</RowLabel>
|
||||
<Measure label="Pupils on roll">
|
||||
{schools.map((school, i) => {
|
||||
const info = data[String(school.urn)]?.school_info as (School & { gias_total_pupils?: number | null; capacity?: number | null }) | undefined;
|
||||
const census = data[String(school.urn)]?.census;
|
||||
@@ -74,8 +74,9 @@ export function CompareCommunity({
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
</Measure>
|
||||
|
||||
<RowLabel>Girls / boys</RowLabel>
|
||||
<Measure label="Girls / boys">
|
||||
{schools.map((school, i) => {
|
||||
const census = data[String(school.urn)]?.census;
|
||||
const girls = pctSplit(census?.female_pupils, census?.total_pupils);
|
||||
@@ -86,10 +87,12 @@ export function CompareCommunity({
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
</Measure>
|
||||
|
||||
<RowLabel tip="% of pupils eligible for free school meals — a common measure of how many pupils come from lower-income families. Benchmark computed across state schools in our dataset.">
|
||||
Free school meals
|
||||
</RowLabel>
|
||||
<Measure
|
||||
tip="% of pupils eligible for free school meals — a common measure of how many pupils come from lower-income families. Benchmark computed across state schools in our dataset."
|
||||
label="Free school meals"
|
||||
>
|
||||
{schools.map((school, i) => {
|
||||
const fsm = data[String(school.urn)]?.census?.fsm_pct ?? null;
|
||||
return (
|
||||
@@ -104,10 +107,12 @@ export function CompareCommunity({
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
</Measure>
|
||||
|
||||
<RowLabel tip="% of pupils whose first language is known or believed to be other than English. State-school average computed from our dataset.">
|
||||
English as an additional language
|
||||
</RowLabel>
|
||||
<Measure
|
||||
tip="% of pupils whose first language is known or believed to be other than English. State-school average computed from our dataset."
|
||||
label="English as an additional language"
|
||||
>
|
||||
{schools.map((school, i) => {
|
||||
const eal = data[String(school.urn)]?.census?.eal_pct ?? null;
|
||||
return (
|
||||
@@ -116,10 +121,12 @@ export function CompareCommunity({
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
</Measure>
|
||||
|
||||
<RowLabel tip="% of pupils receiving SEN support (not including EHC plans). A high figure can mean the school hosts specialist provision — often a strength, not a warning sign. State-school average computed from our dataset.">
|
||||
Extra learning support (SEN)
|
||||
</RowLabel>
|
||||
<Measure
|
||||
tip="% of pupils receiving SEN support (not including EHC plans). A high figure can mean the school hosts specialist provision — often a strength, not a warning sign. State-school average computed from our dataset."
|
||||
label="Extra learning support (SEN)"
|
||||
>
|
||||
{schools.map((school, i) => {
|
||||
const rows = data[String(school.urn)]?.yearly_data ?? [];
|
||||
let sen: number | null = null;
|
||||
@@ -143,8 +150,9 @@ export function CompareCommunity({
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
</Measure>
|
||||
|
||||
<RowLabel>Faith character</RowLabel>
|
||||
<Measure label="Faith character">
|
||||
{schools.map((school, i) => {
|
||||
const info = data[String(school.urn)]?.school_info;
|
||||
const faith = info?.religious_denomination;
|
||||
@@ -155,8 +163,9 @@ export function CompareCommunity({
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
</Measure>
|
||||
|
||||
<RowLabel>Ages</RowLabel>
|
||||
<Measure label="Ages">
|
||||
{schools.map((school, i) => {
|
||||
const info = data[String(school.urn)]?.school_info;
|
||||
return (
|
||||
@@ -165,8 +174,9 @@ export function CompareCommunity({
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
</Measure>
|
||||
|
||||
<RowLabel>Run by</RowLabel>
|
||||
<Measure label="Run by">
|
||||
{schools.map((school, i) => {
|
||||
const info = data[String(school.urn)]?.school_info;
|
||||
const trust = info?.trust_name;
|
||||
@@ -177,6 +187,7 @@ export function CompareCommunity({
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
</Measure>
|
||||
</SectionGrid>
|
||||
</Section>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
type OfstedDisplay,
|
||||
} from '@/lib/compareLogic';
|
||||
import type { ComparisonData, OfstedInspection, School } from '@/lib/types';
|
||||
import { Cell, Chip, RowLabel, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
||||
import { Cell, Chip, Measure, Section, SectionGrid, sectionStyles as s } from './sectionShared';
|
||||
|
||||
const GRADE_TONE: Record<number, 'good' | 'warn' | 'bad'> = {
|
||||
1: 'good',
|
||||
@@ -157,14 +157,15 @@ export function CompareOfsted({
|
||||
}
|
||||
>
|
||||
<SectionGrid schools={schools}>
|
||||
<RowLabel>Result</RowLabel>
|
||||
<Measure label="Result">
|
||||
{schools.map((school, i) => (
|
||||
<Cell key={school.urn} school={school} index={i}>
|
||||
<ResultCell display={displays[i]} />
|
||||
</Cell>
|
||||
))}
|
||||
</Measure>
|
||||
|
||||
<RowLabel>Inspected</RowLabel>
|
||||
<Measure label="Inspected">
|
||||
{schools.map((school, i) => {
|
||||
const ofsted = data[String(school.urn)]?.ofsted;
|
||||
const age = yearsSince(ofsted?.inspection_date ?? null);
|
||||
@@ -176,9 +177,12 @@ export function CompareOfsted({
|
||||
);
|
||||
})}
|
||||
|
||||
<RowLabel tip="Older-style inspections: one rating per judgement area, where published. New-style inspections: the full report card, one rating per area of school life.">
|
||||
Judgement detail
|
||||
</RowLabel>
|
||||
</Measure>
|
||||
|
||||
<Measure
|
||||
tip="Older-style inspections: one rating per judgement area, where published. New-style inspections: the full report card, one rating per area of school life."
|
||||
label="Judgement detail"
|
||||
>
|
||||
{schools.map((school, i) => {
|
||||
const ofsted = data[String(school.urn)]?.ofsted;
|
||||
return (
|
||||
@@ -196,9 +200,12 @@ export function CompareOfsted({
|
||||
);
|
||||
})}
|
||||
|
||||
<RowLabel tip="Links to the school's page on ofsted.gov.uk, where all its inspection reports are listed.">
|
||||
Ofsted page
|
||||
</RowLabel>
|
||||
</Measure>
|
||||
|
||||
<Measure
|
||||
tip="Links to the school's page on ofsted.gov.uk, where all its inspection reports are listed."
|
||||
label="Ofsted page"
|
||||
>
|
||||
{schools.map((school, i) => {
|
||||
const url =
|
||||
data[String(school.urn)]?.ofsted?.ofsted_page_url ??
|
||||
@@ -211,6 +218,7 @@ export function CompareOfsted({
|
||||
</Cell>
|
||||
);
|
||||
})}
|
||||
</Measure>
|
||||
</SectionGrid>
|
||||
</Section>
|
||||
);
|
||||
|
||||
@@ -31,43 +31,71 @@
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
/* Mobile base: each measure is a card; each cell is a school row led by a
|
||||
colour dot + short name. `display: contents` at ≥761px dissolves the card
|
||||
back into the shared grid. */
|
||||
.measure {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-soft);
|
||||
padding: 0.75rem 0.85rem;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.rowLabel {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.6rem;
|
||||
margin-top: 0.8rem;
|
||||
padding: 0 0 0.1rem;
|
||||
}
|
||||
|
||||
.cell {
|
||||
padding: 0.4rem 0.6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.45rem 0;
|
||||
border-top: 1px solid var(--border-light);
|
||||
margin-top: 0.45rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.cell::before {
|
||||
content: attr(data-school);
|
||||
display: block;
|
||||
font-size: 0.72rem;
|
||||
.cellTag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
width: 5rem;
|
||||
flex: none;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--sc, var(--text-muted));
|
||||
color: var(--sc, var(--text-secondary));
|
||||
}
|
||||
|
||||
.cellDot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: var(--dot, var(--text-muted));
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.big {
|
||||
font-size: 1.35rem;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.small {
|
||||
display: block;
|
||||
flex-basis: 100%;
|
||||
padding-left: 5.5rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.1rem;
|
||||
margin-top: -0.05rem;
|
||||
}
|
||||
|
||||
.chip {
|
||||
@@ -197,20 +225,37 @@
|
||||
gap: 0 0.75rem;
|
||||
}
|
||||
|
||||
/* Dissolve the per-measure card so its label + cells become grid items of
|
||||
.grid, keeping columns aligned across every measure. */
|
||||
.measure {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.cellTag {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rowLabel {
|
||||
background: none;
|
||||
border-radius: 0;
|
||||
margin-top: 0;
|
||||
color: var(--text-secondary);
|
||||
padding: 0.85rem 0.5rem 0.85rem 0;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
|
||||
.cell {
|
||||
display: block;
|
||||
padding: 0.85rem 0.25rem;
|
||||
border-top: none;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.cell::before {
|
||||
content: none;
|
||||
.big {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
.small {
|
||||
flex-basis: auto;
|
||||
padding-left: 0;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
|
||||
import type { School } from '@/lib/types';
|
||||
import { CHART_TEXT_COLORS } from '@/lib/utils';
|
||||
import { CHART_COLORS, CHART_TEXT_COLORS, shortName } from '@/lib/utils';
|
||||
import styles from './compareSections.module.css';
|
||||
|
||||
export function Section({
|
||||
@@ -61,6 +61,29 @@ export function RowLabel({ children, tip }: { children: ReactNode; tip?: string
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* One measure = its row label plus a cell per school. `display: contents` on
|
||||
* desktop (see CSS) makes these flow into the section grid as if this wrapper
|
||||
* weren't here, keeping columns aligned across measures; on mobile the wrapper
|
||||
* becomes a card so each measure reads as its own block.
|
||||
*/
|
||||
export function Measure({
|
||||
label,
|
||||
tip,
|
||||
children,
|
||||
}: {
|
||||
label: ReactNode;
|
||||
tip?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.measure}>
|
||||
<RowLabel tip={tip}>{label}</RowLabel>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Cell({
|
||||
school,
|
||||
index,
|
||||
@@ -73,9 +96,19 @@ export function Cell({
|
||||
return (
|
||||
<div
|
||||
className={styles.cell}
|
||||
data-school={school.school_name}
|
||||
style={{ '--sc': CHART_TEXT_COLORS[index % CHART_TEXT_COLORS.length] } as CSSProperties}
|
||||
style={
|
||||
{
|
||||
'--sc': CHART_TEXT_COLORS[index % CHART_TEXT_COLORS.length],
|
||||
'--dot': CHART_COLORS[index % CHART_COLORS.length],
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
{/* Mobile-only per-school tag (dot + short name); hidden on desktop,
|
||||
where the column header identifies the school. */}
|
||||
<span className={styles.cellTag}>
|
||||
<span className={styles.cellDot} aria-hidden="true" />
|
||||
{shortName(school.school_name)}
|
||||
</span>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user