style(ui): flatten activity and offer comparison sections

61 selected tests and build/lint pass. Wider rendered qualification remains in progress; this local checkpoint does not establish visual acceptance.
This commit is contained in:
cesnimda
2026-11-04 00:41:27 +01:00
parent 2218659ecf
commit 69621788ee
43 changed files with 80 additions and 11 deletions
@@ -12,6 +12,10 @@ Owner correction: 3 November 2026. This register extends IM10 and the original 3
| V06 | Remaining routes | Whole-application modernisation has not received current route-by-route visual review | OPEN; all 34 routes explicitly tracked in ROUTE_COVERAGE.json | Review actual screens against A3 and resolve material differences; do not infer completion from shared styles |
| V07 | Connected workflows | Existing accepted journeys predate this visual correction | OPEN | Discovery→review→track; CV create/import→edit→preview→export; application→status→follow-up; Career Profile→document reuse; desktop/mobile |
| V08 | Final acceptance | Automated checks cannot establish owner visual approval or human/device/native-language accessibility | OPEN | Consolidated final visual review and accurately recorded outstanding qualifications |
| V09 | Secondary views | Duplicate Offer Comparison heading and Operations section card around task cards | Implemented bounded correction; 61 component tests passed; 36-configuration successor matrix in progress | Inspect successor phone/tablet/desktop captures and record full result before qualification |
| V10 | Account | Continuous account navigation/profile/password sections retain full surrounding surface cards | CONFIRMED by source plus accepted IM09 account capture; relevant ProfilePage/security/session/theme sources unchanged from773c9431; current shared shell differs and is qualified separately | Review and flatten continuous sections in next eligible account package, preserving draft/jump/security/session/credential boundaries; fresh affected rendered qualification |
| V11 | Offer localisation | Literal date-only values are formatted through local-time Date conversion | REPRODUCED for supported string fixture input: 2026-12-01 formats as12/1/2026 in Europe/Oslo and11/30/2026 in America/Los_Angeles. Actual API model also permits timestamp values, whose precision/timezone must not be guessed | Next bounded fix must preserve literal calendar dates while retaining existing local formatting for genuine timestamps; require timezone regression proof. No claim that deployed API records use date-only serialization |
| V12 | Applications | Current nine-width capture set still shows a collection border around mobile job cards and a large outlined application-detail header; passing axe does not establish A3 visual parity | CONFIRMED by actual `evidence/applications/table-en-light-320.png` and `detail-en-light-1440.png`; full36 journey/144 scan matrix passes functional/accessibility scope | Next core-workflow visual package: flatten collection/header hierarchy and improve density, retain column preferences, selection/bulk actions, every action, query/return state, revisions and right-side context. Compare approved B1/A3 before changing interactions |
The owner-approved design foundation remains original A1 dashboard, D1 visual system, B1 tracking, editor left / preview right, application context right, desktop sidebar and hybrid mobile navigation. No controls, imports, revisions, permissions or provider rights may be removed to obtain visual simplicity.
@@ -0,0 +1,16 @@
# Secondary route visual review — in progress
4 November 2026. Runtime starts at published/verified `2218659ecf692e8e377dd33a9ce6415b01e43032`. The initial visual run was deliberately interrupted after 35 baseline captures when material discrepancies were identified. These images are observations, not a complete passing matrix; see `evidence/secondary/BEFORE_INTERRUPTED.json`.
| Route | Actual phone review | Implementation / preservation |
|---|---|---|
| /companies | One card per company, relationship actions retained; no outer card around the collection | Retain meaningful company object boundary; desktop table and relationship paths remain |
| /offers | Page heading repeated below the shell heading | Removed redundant local heading; retained explanation, All Offers action, named results region, unknown facts and individual offer objects; object headings now h2 under page h1 |
| /operations | Open task cards were enclosed within another section card | Removed outer section surfaces, retaining named sections, individual notification/task objects, progress, permission-dependent actions, polling and focus/error/committed-write protections |
| /analytics | Independent chart/widget surfaces and explicit period/source explanations | Retain distinct data objects; do not remove necessary scope qualifications to save space or imply historical conversion |
| /trash | Existing filters and selection/pagination within results boundary; no additional nested collection card | Empty fixture only; real restore/delete and populated selection remain separately qualified |
| /cv/:slug | Public document route owns its paper and download link | Retain document boundary; no app sidebar/identity fetch introduced; synthetic preview is not physical PDF or deployed anonymous-access acceptance |
PASSED:61 selected component tests/five suites, export build and full lint after bounded Offer Comparison / Operations changes. The 36-configuration successor run writes to `evidence/secondary-after` and is **IN PROGRESS**. Do not claim216 passing captures until its final report succeeds. Source census and independent source review must be reconciled with the successor before publication.
Remaining screenshots and desktop/tablet layouts require inspection. These six routes do not cover all 34 routes or their loading/error/populated variants. Application Add Job/table/board/detail visual matrix is prepared as an opt-in extension of the existing verifier. Its **one-configuration pilot passed** four scans/captures and the controlled search→board→precise status→detail/context→return journey plus Add Job open/Escape and three alias URL checks, with one intercepted fixture status write. The complete36-configuration matrix remains **NOT RUN** until recorded otherwise. A pilot is not full qualification. No new application provider, model, migration or live action is included.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,46 @@
{
"status": "INTERRUPTED",
"runtimeCommit": "2218659ecf692e8e377dd33a9ce6415b01e43032",
"reason": "Material duplicate Offer Comparison heading and nested Operations surfaces reproduced. Baseline run interrupted before unnecessary full matrix; implement corrections then qualify successor.",
"captures": [
"analytics-en-dark-320.png",
"analytics-en-dark-360.png",
"analytics-en-light-320.png",
"analytics-en-light-360.png",
"analytics-nb-dark-320.png",
"analytics-nb-light-320.png",
"companies-en-dark-320.png",
"companies-en-dark-360.png",
"companies-en-light-320.png",
"companies-en-light-360.png",
"companies-nb-dark-320.png",
"companies-nb-light-320.png",
"offers-en-dark-320.png",
"offers-en-dark-360.png",
"offers-en-light-320.png",
"offers-en-light-360.png",
"offers-nb-dark-320.png",
"offers-nb-light-320.png",
"operations-en-dark-320.png",
"operations-en-dark-360.png",
"operations-en-light-320.png",
"operations-en-light-360.png",
"operations-nb-dark-320.png",
"operations-nb-light-320.png",
"public-cv-en-dark-320.png",
"public-cv-en-light-320.png",
"public-cv-en-light-360.png",
"public-cv-nb-dark-320.png",
"public-cv-nb-light-320.png",
"trash-empty-en-dark-320.png",
"trash-empty-en-dark-360.png",
"trash-empty-en-light-320.png",
"trash-empty-en-light-360.png",
"trash-empty-nb-dark-320.png",
"trash-empty-nb-light-320.png"
],
"limits": [
"Saved PNGs are visual baseline observations, not a complete passing regression or 216-state qualification.",
"Ctrl-C ended the owned browser verifier. No app service or provider was stopped."
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@@ -7,12 +7,13 @@ import fs from 'node:fs';
import path from 'node:path';
const compiled = ts.transpileModule(fs.readFileSync(new URL('../src/i18n/translations.ts', import.meta.url), 'utf8'), { compilerOptions: { module: ts.ModuleKind.ESNext } }).outputText;
const { translations } = await import(`data:text/javascript;base64,${Buffer.from(compiled).toString('base64')}`);
const out = path.resolve(import.meta.dirname, '../../docs/ui-modernisation/evidence/secondary/browser');
const visualMatrix = Boolean(process.env.A3_VISUAL_MATRIX);
const out = path.resolve(import.meta.dirname, visualMatrix ? '../../docs/ui-modernisation/visual-correction/evidence/secondary-after' : '../../docs/ui-modernisation/evidence/secondary/browser');
fs.mkdirSync(out, { recursive: true });
const report = { screens: [], journeys: [], errors: [], writes: [], unavailableReads: [], blockedExternal: [] };
const browser = await chromium.launch();
try {
const configurations = process.env.A3_PILOT ? [[320, 'en', 'light']] : [320, 390, 768, 1440].flatMap(width => ['en', 'nb'].flatMap(language => ['light', 'dark'].map(mode => [width, language, mode])));
const configurations = process.env.A3_PILOT ? [[320, 'en', 'light']] : (visualMatrix ? [320, 360, 390, 430, 768, 1024, 1280, 1440, 1920] : [320, 390, 768, 1440]).flatMap(width => ['en', 'nb'].flatMap(language => ['light', 'dark'].map(mode => [width, language, mode])));
for (const [width, language, mode] of configurations) {
console.log(`Checking ${width}/${language}/${mode}`);
const t = key => { assert.ok(translations[language][key], key); return translations[language][key]; };
+2 -2
View File
@@ -102,7 +102,7 @@ test("shows the first 25 of the real total in server application-date order with
expect(await screen.findByText(copy("offerComparisonSampleCount", "en", { shown: 25, total: 61 }))).toBeInTheDocument();
expect(screen.getByText(copy("offerComparisonSampleHelp"))).toBeInTheDocument();
expect(screen.getAllByRole("article")).toHaveLength(25);
expect(screen.getAllByRole("heading", { level: 3 }).map((heading) => heading.textContent)).toEqual(items.map((item) => item.jobTitle));
expect(screen.getAllByRole("heading", { level: 2 }).map((heading) => heading.textContent)).toEqual(items.map((item) => item.jobTitle));
expect(screen.queryByText("Offer role 26")).not.toBeInTheDocument();
expect(mockedApi.get).toHaveBeenCalledTimes(1);
expect(mockedApi.get).toHaveBeenCalledWith("/jobapplications", request);
@@ -150,7 +150,7 @@ test("a live locale change translates loaded offer chrome and preserves recorded
renderPage();
expect(await screen.findByText(notes)).toBeInTheDocument();
changeLanguage("nb");
expect(screen.getByRole("heading", { level: 2, name: translations.nb.offerComparisonTitle })).toBeInTheDocument();
expect(screen.getByRole("region", { name: translations.nb.offerComparisonTitle })).toBeInTheDocument();
const article = screen.getByRole("article", { name: "English source title" });
expect(within(article).getByText(notes)).toBeInTheDocument();
expect(within(article).getByText("GBP package as recorded")).toBeInTheDocument();
@@ -5,7 +5,6 @@ import { api } from "../api";
import { useViewResource } from "../hooks/useViewResource";
import { useI18n } from "../i18n/I18nProvider";
import ViewStateNotice from "../components/ViewStateNotice";
import { PageHeader } from "../layout/PageLayout";
import type { JobApplication } from "../types";
type PagedJobs = { items: JobApplication[]; total: number; page: number; pageSize: number };
@@ -24,8 +23,10 @@ export default function OfferComparisonPage() {
return (
<Stack spacing={2} sx={{ minWidth: 0 }}>
<PageHeader level="h2" title={t("offerComparisonTitle")} description={t("offerComparisonHelp")}
actions={!loading && !resource.error && resource.data.items.length > 0 ? <Button variant="outlined" onClick={() => navigate("/jobs?status=Offer&sortBy=dateApplied&sortDir=asc")}>{t("offerComparisonAllOffers")}</Button> : null} />
<Stack direction={{ xs: "column", md: "row" }} alignItems={{ xs: "flex-start", md: "center" }} gap={1.5}>
<Typography color="text.secondary" sx={{ flex: 1 }}>{t("offerComparisonHelp")}</Typography>
{!loading && !resource.error && resource.data.items.length > 0 ? <Button variant="outlined" onClick={() => navigate("/jobs?status=Offer&sortBy=dateApplied&sortDir=asc")}>{t("offerComparisonAllOffers")}</Button> : null}
</Stack>
<Box ref={resultsRef} role="region" aria-label={t("offerComparisonTitle")} tabIndex={-1} aria-busy={loading} sx={{ minWidth: 0 }}>
<ViewStateNotice compact loading={loading} title={t("offerComparisonLoadFailed")}
error={resource.error ? { ...resource.error, message: resource.error.message || t("offerComparisonLoadFailed") } : null}
@@ -37,7 +38,7 @@ export default function OfferComparisonPage() {
}} />
{!loading && !resource.error && resource.data.items.length === 0 ? (
<Paper variant="outlined" sx={{ p: "var(--app-card-padding)", textAlign: "center" }}>
<Typography component="h3" variant="sectionTitle">{t("offerComparisonEmpty")}</Typography>
<Typography component="h2" variant="sectionTitle">{t("offerComparisonEmpty")}</Typography>
<Typography color="text.secondary" sx={{ mt: 0.5, mb: 2 }}>{t("offerComparisonEmptyHelp")}</Typography>
<Button variant="contained" onClick={() => navigate("/jobs")}>{t("jobApplications")}</Button>
</Paper>
@@ -50,7 +51,7 @@ export default function OfferComparisonPage() {
{resource.data.items.map((offer) => (
<Paper key={offer.id} component="article" aria-label={offer.jobTitle} variant="outlined" sx={{ p: "var(--app-card-padding)", display: "flex", flexDirection: "column", minWidth: 0 }}>
<Stack direction="row" justifyContent="space-between" spacing={1} alignItems="flex-start">
<Box sx={{ minWidth: 0 }}><Typography component="h3" variant="sectionTitle" sx={{ overflowWrap: "anywhere" }}>{offer.jobTitle}</Typography><Typography color="text.secondary" sx={{ overflowWrap: "anywhere" }}>{offer.company?.name}</Typography></Box>
<Box sx={{ minWidth: 0 }}><Typography component="h2" variant="sectionTitle" sx={{ overflowWrap: "anywhere" }}>{offer.jobTitle}</Typography><Typography color="text.secondary" sx={{ overflowWrap: "anywhere" }}>{offer.company?.name}</Typography></Box>
<Chip size="small" color="success" label={t("statusOffer")} sx={{ flexShrink: 0 }} />
</Stack>
<Stack spacing={1.25} sx={{ my: 2 }}>
+2 -2
View File
@@ -137,7 +137,7 @@ export default function OperationsPage() {
{errorDetail !== errorTitle ? <Typography variant="body2">{errorDetail}</Typography> : null}
</Alert> : null}
{loading ? <LinearProgress aria-label={t("operationsLoading")} /> : null}
<PageSection surface title={t("notifications")}>
<PageSection title={t("notifications")}>
{hasLoaded && notifications.length === 0 ? <Typography color="text.secondary">{t("operationsNoNotifications")}</Typography> : null}
<Stack spacing={1.5}>{notifications.map(notification => <Paper key={notification.id} component="article" aria-label={notification.title} variant="outlined" sx={{ p: "var(--app-card-padding)", borderRadius: 1, minWidth: 0, overflowWrap: "anywhere" }}>
<Typography component="h3" variant="sectionTitle">{notification.title}</Typography>
@@ -150,7 +150,7 @@ export default function OperationsPage() {
</Stack>
</Paper>)}</Stack>
</PageSection>
<PageSection surface title={t("operations")}>
<PageSection title={t("operations")}>
{hasLoaded && operations.length === 0 ? <Typography color="text.secondary">{t("operationsEmpty")}</Typography> : null}
<Stack spacing={1.5}>{operations.map(operation => <Paper key={operation.id} component="article" aria-label={taskLabel(operation.taskType)} data-operation-id={operation.id} variant="outlined" sx={{ p: "var(--app-card-padding)", borderRadius: 1, minWidth: 0, overflowWrap: "anywhere" }}>
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, flexWrap: "wrap", minWidth: 0 }}>