diff --git a/e2e/tests/journeys.spec.ts b/e2e/tests/journeys.spec.ts
index 9dabf5d..6d39a70 100644
--- a/e2e/tests/journeys.spec.ts
+++ b/e2e/tests/journeys.spec.ts
@@ -561,6 +561,10 @@ test('a desktop postcode search opens on the map with the list beside it', async
const pane = page.locator('[class*="mapListPane"]');
const card = pane.locator('[data-urn]').first();
await expect(card).toBeVisible({ timeout: 15_000 });
+ // The list view's own row, not a cut-down card: it carries the same View
+ // link and Compare button.
+ await expect(card.getByRole('link', { name: 'View', exact: true })).toBeVisible();
+ await expect(card.getByRole('button', { name: /Compar/ })).toBeVisible();
await expect(page.locator('.sc-pin').first()).toBeVisible({ timeout: 15_000 });
// The split runs to the bottom of the screen rather than stopping short.
@@ -629,7 +633,8 @@ for (const width of [360, 390, 402, 430]) {
const small = await page.evaluate(() => {
const toolbar = document.querySelector('[class*="resultsToolbar"]');
const fabEl = document.querySelector('[class*="viewFab"]');
- return [...(toolbar?.querySelectorAll('a, button, input, select') ?? []), fabEl]
+ const closeEl = document.querySelector('[class*="closeSheetBtn"]');
+ return [...(toolbar?.querySelectorAll('a, button, input, select') ?? []), fabEl, closeEl]
.filter((el): el is HTMLElement => !!el && !!(el as HTMLElement).offsetParent)
.map((el) => ({ t: el.innerText?.trim().slice(0, 24) || el.getAttribute('aria-label'),
w: el.getBoundingClientRect().width, h: el.getBoundingClientRect().height }))
diff --git a/nextjs-app/__tests__/components/ResultsMapView.test.tsx b/nextjs-app/__tests__/components/ResultsMapView.test.tsx
index 6045375..435edc9 100644
--- a/nextjs-app/__tests__/components/ResultsMapView.test.tsx
+++ b/nextjs-app/__tests__/components/ResultsMapView.test.tsx
@@ -104,11 +104,11 @@ it('selects the pin from the card, and the card from the pin', async () => {
fireEvent.click(within(card(1)).getByText(/pupils/));
expect(screen.getByTestId('map')).toHaveAttribute('data-selected', '1');
- expect(card(1).className).toMatch(/compactItemSelected/);
+ expect(card(1).className).toMatch(/mapRowSelected/);
fireEvent.click(screen.getByRole('button', { name: 'pin' }));
- expect(card(2).className).toMatch(/compactItemSelected/);
- expect(card(1).className).not.toMatch(/compactItemSelected/);
+ expect(card(2).className).toMatch(/mapRowSelected/);
+ expect(card(1).className).not.toMatch(/mapRowSelected/);
});
it('clicking a card\'s link or button does not also select it', async () => {
@@ -121,8 +121,9 @@ it('clicking a card\'s link or button does not also select it', async () => {
it('shows the England comparison for mainstream schools only, and never a placeholder 0%', async () => {
const { container } = await renderMap();
const card = (urn: number) => container.querySelector(`[data-urn="${urn}"]`) as HTMLElement;
- expect(card(2)).toHaveTextContent('52% RWM -10 pts · 269 pupils');
- expect(card(3)).toHaveTextContent('62 pupils');
+ expect(card(2)).toHaveTextContent('52%Reading, Writing & Maths-10 pts vs national');
+ expect(card(2)).toHaveTextContent('269pupils');
+ expect(card(3)).toHaveTextContent('62pupils');
expect(card(3)).not.toHaveTextContent(/%|pts/);
});
@@ -152,3 +153,13 @@ it('builds no list cards on a phone, where the pane is hidden', async () => {
// The count stays: it is the pane's heading, shown above the map.
expect(screen.getByRole('heading', { name: /3 schools within/ })).toBeInTheDocument();
});
+
+it('draws the list view\'s own row beside the map, with the same content', async () => {
+ const { container } = await renderMap();
+ const beside = (container.querySelector('[data-urn="2"]') as HTMLElement).textContent;
+
+ fireEvent.click(screen.getByRole('button', { name: 'List' }));
+ const row = screen.getByRole('link', { name: 'Southmead Primary School' }).closest('[class~="row"]')!;
+ expect(row.parentElement?.className).toMatch(/schoolList/);
+ expect(row.textContent).toBe(beside);
+});
diff --git a/nextjs-app/components/HomeView.module.css b/nextjs-app/components/HomeView.module.css
index 862abfe..11271a8 100644
--- a/nextjs-app/components/HomeView.module.css
+++ b/nextjs-app/components/HomeView.module.css
@@ -591,7 +591,7 @@
.mapViewContainer {
display: grid;
- grid-template-columns: minmax(340px, 420px) minmax(0, 1fr);
+ grid-template-columns: minmax(360px, 460px) minmax(0, 1fr);
height: calc(100dvh - var(--map-top) - var(--map-bottom));
min-height: 480px;
background: var(--bg-card);
@@ -631,117 +631,22 @@
overflow-y: auto;
padding: 0.125rem 1rem 1rem;
scrollbar-width: thin;
+ /* The rows lay themselves out by this list's width (SchoolRow.module.css),
+ which here is always narrow, whatever the screen. */
+ container: results / inline-size;
}
-/* Compact School Item: the list pane's card, and the phone's bottom sheet. */
-.compactItem {
- display: flex;
- flex-direction: column;
- gap: 0.4375rem;
- padding: 0.75rem 0.875rem;
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: 10px;
+/* A row in the list beside the map: clicking it picks its pin. */
+.mapRow {
cursor: pointer;
- transition: border-color var(--transition), box-shadow var(--transition);
+ border-radius: 10px;
}
-.compactItem:hover {
- border-color: var(--border-strong);
+.mapRowSelected > * {
+ outline: 2px solid var(--brand);
+ outline-offset: 1px;
}
-.compactItemSelected,
-.compactItemSelected:hover {
- border-color: var(--brand);
- box-shadow: 0 0 0 2px rgba(var(--brand-rgb), 0.28);
-}
-
-.compactItemHeader {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: 0.625rem;
-}
-
-.compactItemName {
- font-family: var(--font-display);
- font-size: 0.9375rem;
- font-weight: 700;
- line-height: 1.3;
- color: var(--text-primary);
- text-decoration: none;
-}
-
-.compactItemName:hover {
- color: var(--brand-strong);
- text-decoration: underline;
-}
-
-.distanceBadge {
- flex-shrink: 0;
- padding: 0.125rem 0.375rem;
- font-size: 0.75rem;
- font-weight: 700;
- background: var(--brand);
- color: var(--brand-on);
- border-radius: 4px;
- white-space: nowrap;
-}
-
-.compactItemTags {
- display: flex;
- flex-wrap: wrap;
- gap: 0.375rem;
-}
-
-.compactBadge,
-.compactTag {
- padding: 0.125rem 0.4375rem;
- border-radius: 4px;
- font-size: 0.6875rem;
- font-weight: 600;
- white-space: nowrap;
-}
-
-.compactTag {
- background: var(--bg-secondary);
- color: var(--text-secondary);
- font-weight: 500;
-}
-
-.ofsted1,
-.ofsted2 { background: var(--status-above-bg); color: var(--status-above); }
-.ofsted3 { background: var(--status-below-bg); color: var(--status-below); }
-.ofsted4 { background: var(--status-below); color: var(--text-inverse); }
-.ofstedRc { background: var(--phase-secondary-text); color: var(--text-inverse); }
-.ofstedInspected { background: var(--phase-primary-bg); color: var(--phase-primary-text); }
-.ofstedPending { background: var(--border); color: var(--text-muted); }
-
-.compactItemFooter {
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 0.75rem;
-}
-
-.compactStat {
- font-size: 0.8125rem;
- color: var(--text-secondary);
-}
-
-.compactStat strong {
- font-size: 0.9375rem;
- color: var(--text-primary);
-}
-
-.deltaUp { color: var(--status-above); font-weight: 600; }
-.deltaDown { color: var(--status-below); font-weight: 600; }
-
-.compactItemActions {
- display: flex;
- gap: 0.5rem;
- flex-shrink: 0;
-}
.sectionHeader {
@@ -779,6 +684,8 @@
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1.25rem;
+ /* The rows lay themselves out by this list's width (SchoolRow.module.css). */
+ container: results / inline-size;
}
/* Staggered fade-in for rows */
@@ -881,33 +788,37 @@
animation: slideUpSheet 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
- .bottomSheet .compactItem {
- border: none;
- box-shadow: none;
- background: transparent;
- padding: 1rem;
- cursor: default;
- }
+ /* A 30px circle, drawn by ::before, inside a 44px target (MOBILE.md). */
.closeSheetBtn {
position: absolute;
- top: -12px;
- right: -12px;
- width: 30px;
- height: 30px;
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: 50%;
+ top: -19px;
+ right: -15px;
+ width: 44px;
+ height: 44px;
+ padding: 0;
+ background: none;
+ border: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
color: var(--text-secondary);
cursor: pointer;
- box-shadow: 0 2px 8px rgba(var(--shadow-rgb), 0.1);
z-index: 10;
}
+ .closeSheetBtn::before {
+ content: '';
+ position: absolute;
+ inset: 7px;
+ z-index: -1;
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 50%;
+ box-shadow: 0 2px 8px rgba(var(--shadow-rgb), 0.1);
+ }
+
@keyframes slideUpSheet {
from {
transform: translateY(120%);
@@ -935,6 +846,13 @@
display: none;
}
+ /* The sheet holds one results row, which is the card itself. It is not
+ inside a `results` container, so give it one: a phone-width sheet takes
+ the row's narrow layout. */
+ .bottomSheet {
+ container: results / inline-size;
+ }
+
.mapListPane .resultsHeader {
padding: 0.625rem 0.875rem;
}
diff --git a/nextjs-app/components/HomeView.tsx b/nextjs-app/components/HomeView.tsx
index 8d6918a..1b0d6a5 100644
--- a/nextjs-app/components/HomeView.tsx
+++ b/nextjs-app/components/HomeView.tsx
@@ -16,7 +16,6 @@ import { HeroIllustration } from './Illustration';
import { useComparisonContext } from '@/context/ComparisonContext';
import { fetchSchools, fetchLAaverages, fetchNationalAverages } from '@/lib/api';
import type { SchoolsResponse, Filters, School } from '@/lib/types';
-import { schoolUrl, buildOfstedListBadge, isSpecialSchool, listRwmValue } from '@/lib/utils';
import { track } from '@/lib/analytics';
import styles from './HomeView.module.css';
@@ -558,6 +557,32 @@ export function HomeView({ initialSchools, filters, totalSchools, howItWorks, ed
const isMapView = initialSchools.schools.length > 0 && resultsView === 'map' && isLocationSearch;
+ // One school as a results row: the list view, the list beside the map and
+ // the phone's bottom sheet all draw the same thing.
+ const renderRow = (school: School) => (
+ school.attainment_8_score != null ? (
+