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
131 lines
3.0 KiB
TypeScript
131 lines
3.0 KiB
TypeScript
/**
|
|
* Small shared pieces for the compare sections: the section shell, the
|
|
* row-label + per-school-cell grid, and tone-mapped chips. Copy passed into
|
|
* these comes verbatim from the reviewed mockups
|
|
* (docs/superpowers/specs/mockups/) — do not paraphrase it here.
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import type { CSSProperties, ReactNode } from 'react';
|
|
|
|
import type { School } from '@/lib/types';
|
|
import { CHART_COLORS, CHART_TEXT_COLORS, shortName } from '@/lib/utils';
|
|
import styles from './compareSections.module.css';
|
|
|
|
export function Section({
|
|
title,
|
|
how,
|
|
children,
|
|
}: {
|
|
title: string;
|
|
how?: ReactNode;
|
|
children: ReactNode;
|
|
}) {
|
|
return (
|
|
<section className={styles.section}>
|
|
<h2 className={styles.sectionTitle}>{title}</h2>
|
|
{how && <p className={styles.how}>{how}</p>}
|
|
{children}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export function SectionGrid({
|
|
schools,
|
|
children,
|
|
}: {
|
|
schools: School[];
|
|
children: ReactNode;
|
|
}) {
|
|
return (
|
|
<div
|
|
className={styles.grid}
|
|
style={{ '--school-count': schools.length } as CSSProperties}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function RowLabel({ children, tip }: { children: ReactNode; tip?: string }) {
|
|
return (
|
|
<div className={styles.rowLabel}>
|
|
{children}
|
|
{tip && (
|
|
<span className={styles.help} title={tip} aria-label={tip}>
|
|
?
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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,
|
|
children,
|
|
}: {
|
|
school: School;
|
|
index: number;
|
|
children: ReactNode;
|
|
}) {
|
|
return (
|
|
<div
|
|
className={styles.cell}
|
|
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>
|
|
);
|
|
}
|
|
|
|
export type ChipTone = 'good' | 'warn' | 'bad' | 'neutral';
|
|
|
|
const CHIP_TONE_CLASS: Record<ChipTone, string> = {
|
|
good: styles.chipGood,
|
|
warn: styles.chipWarn,
|
|
bad: styles.chipBad,
|
|
neutral: styles.chipNeutral,
|
|
};
|
|
|
|
export function Chip({ tone, children }: { tone: ChipTone; children: ReactNode }) {
|
|
return <span className={`${styles.chip} ${CHIP_TONE_CLASS[tone]}`}>{children}</span>;
|
|
}
|
|
|
|
export const sectionStyles = styles;
|