fix(detail): restore the shell's mobile header/nav CSS
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m6s
PR Checks / Backend Smoke (pull_request) Successful in 8s
PR Checks / Build Backend (no push) (pull_request) Successful in 36s
PR Checks / Build Frontend (no push) (pull_request) Successful in 46s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m19s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 9s
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m6s
PR Checks / Backend Smoke (pull_request) Successful in 8s
PR Checks / Build Backend (no push) (pull_request) Successful in 36s
PR Checks / Build Frontend (no push) (pull_request) Successful in 46s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m19s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 9s
The server/client split moved the header and sticky nav markup into
SchoolDetailShell, but their responsive rules stayed behind in
schoolSections.module.css. CSS Modules hash class names per file, so those
rules stopped matching entirely: on mobile the header details block never
collapsed ("Show all details" e2e journey failed), the toggle itself never
appeared, and the section-nav mobile swap (menu button in, link row out)
never applied.
Ports the stranded blocks into SchoolDetailShell.module.css verbatim: the
768px header block (stacked header, wrapped meta pills, details toggle +
collapse) and the 640px section-nav blocks (scroll-fade mask, bottom-sheet
panel, control swap). Adds a jest guard that every class a components/school
component references is defined in the stylesheet that component imports —
it fails on the three classes that had no rule at all here
(headerDetailsOpen, atEnd, sectionNavBackLabel).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Guards against "stranded" CSS: when markup moves between components, its
|
||||
* rules must move with it. CSS Modules hash class names per file, so a rule
|
||||
* left behind in another stylesheet silently stops applying — exactly how the
|
||||
* header's mobile `.headerDetails { display: none }` (and the section-nav
|
||||
* mobile swap) stopped working when the detail view was split into a client
|
||||
* shell plus server sections.
|
||||
*/
|
||||
|
||||
const SCHOOL_DIR = path.join(__dirname, '..', '..', 'components', 'school');
|
||||
|
||||
function classesDefinedIn(css: string): Set<string> {
|
||||
return new Set(Array.from(css.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g), (m) => m[1]));
|
||||
}
|
||||
|
||||
function classesUsedIn(tsx: string): Set<string> {
|
||||
return new Set(Array.from(tsx.matchAll(/\bstyles\.([_a-zA-Z][\w]*)/g), (m) => m[1]));
|
||||
}
|
||||
|
||||
const components = fs
|
||||
.readdirSync(SCHOOL_DIR)
|
||||
.filter((f) => f.endsWith('.tsx'))
|
||||
.map((file) => {
|
||||
const source = fs.readFileSync(path.join(SCHOOL_DIR, file), 'utf8');
|
||||
const importMatch = source.match(/import\s+styles\s+from\s+['"](.+?\.module\.css)['"]/);
|
||||
return { file, source, stylesheet: importMatch?.[1] };
|
||||
})
|
||||
.filter((c) => c.stylesheet);
|
||||
|
||||
describe('school detail stylesheets', () => {
|
||||
it.each(components.map((c) => [c.file, c.source, c.stylesheet as string]))(
|
||||
'%s only uses classes its own stylesheet defines',
|
||||
(_file, source, stylesheet) => {
|
||||
const css = fs.readFileSync(path.join(SCHOOL_DIR, stylesheet), 'utf8');
|
||||
const defined = classesDefinedIn(css);
|
||||
const missing = Array.from(classesUsedIn(source)).filter((c) => !defined.has(c));
|
||||
expect(missing).toEqual([]);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -667,3 +667,101 @@
|
||||
min-height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.headerContent {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btnAdd,
|
||||
.btnRemove {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.schoolName {
|
||||
font-size: 1.25rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Pills wrap horizontally instead of stacking — short tokens like
|
||||
"Manchester" / "Voluntary aided" fit 2 per row instead of 3 full
|
||||
rows of empty horizontal space. */
|
||||
.meta {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
/* Secondary header info (headteacher, website, pupil count, trust,
|
||||
contact, area) isn't needed above the fold on phones/tablets, so it's
|
||||
collapsed by default and revealed on demand via the "Show all details"
|
||||
link — reclaiming the vertical space so the metrics surface sooner. */
|
||||
.detailsToggle {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.headerDetails {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.headerDetailsOpen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.sectionNavLinks {
|
||||
-webkit-mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent);
|
||||
mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent);
|
||||
}
|
||||
|
||||
/* When scrolled to the end, drop the fade so the last item isn't dimmed. */
|
||||
.sectionNavLinks.atEnd {
|
||||
-webkit-mask-image: none;
|
||||
mask-image: none;
|
||||
}
|
||||
|
||||
.sectionsPanel {
|
||||
position: fixed;
|
||||
top: auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: auto;
|
||||
max-height: 74vh;
|
||||
border-radius: 16px 16px 0 0;
|
||||
padding: 0.5rem 0.6rem calc(0.8rem + env(safe-area-inset-bottom, 0));
|
||||
box-shadow: 0 -10px 40px rgba(26, 22, 18, 0.25);
|
||||
}
|
||||
|
||||
.sectionsItem {
|
||||
padding: 0.7rem 0.6rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Swap which controls show at the mobile breakpoint. Declared last so these
|
||||
display rules win over the base (equal-specificity) declarations above. */
|
||||
@media (max-width: 640px) {
|
||||
.sectionNavLinks,
|
||||
.sectionNavCompare,
|
||||
.sectionNavAll,
|
||||
.sectionNavBackLabel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sectionNavMenu {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.sectionNavCompareIcon {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user