Merge pull request 'fix(detail): restore the shell's mobile header/nav CSS' (#85) from perf/server-client-split into main
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 46s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 55s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 2m13s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 2s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Successful in 1m12s
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 46s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 55s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 2m13s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 2s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Successful in 1m12s
Reviewed-on: #85
This commit was merged in pull request #85.
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