feat(ui): apply approved A3 shared design foundation

This commit is contained in:
cesnimda
2026-11-03 03:08:13 +01:00
parent d3d3383764
commit bdd1de0dd3
43 changed files with 1074 additions and 43 deletions
+226
View File
@@ -0,0 +1,226 @@
# Jobjakt — A3 Final Design Approval and Implementation Authorisation
I approve **A3 as the final Jobjakt UI/UX design foundation** and authorise its phased implementation across the application.
I have reviewed the design direction and am happy with the result.
This approval includes the consolidated A3 prototype, its established design system, navigation approach, layouts and connected workflows.
## 1. Confirmed design decisions
Preserve the approved A3 combination:
- Original A1 dashboard.
- D1 visual foundation.
- B1 application tracking.
- Labelled, grouped and collapsible desktop sidebar.
- Hybrid mobile navigation.
- CV editor on the left with live preview on the right.
- Full application-detail pages with contextual information on the right.
- Optional quick-preview panels.
- Career Profile overview first, with immediate editing actions.
- Calendar agenda first, with an optional month view.
- Automation controls under Settings.
- Contextual AI assistance.
- Optional, resumable dashboard onboarding checklist with on-demand walkthroughs.
Maintain the selected light/dark visual treatment and consistent responsive behaviour.
Do not restart design exploration or generate another five concepts.
Minor adjustments for implementation feasibility, accessibility and usability are permitted.
Consult me before making substantial changes to the approved visual direction or user journeys.
## 2. Implementation approval
I authorise the actual implementation of the approved A3 redesign within the appropriate Jobjakt development branches.
This includes:
- Shared design system.
- Typography and colours.
- Navigation.
- Dashboard.
- Application tracking.
- Application details.
- Job discovery.
- Career Profile.
- CV Builder.
- CV import.
- Cover letters.
- Calendar.
- Correspondence.
- Documents.
- Settings.
- Automation controls.
- Administration.
- Onboarding.
- Mobile layouts.
- Light/dark modes.
- Accessibility improvements.
The implementation must cover the actual application, not merely reproduce static prototypes.
## 3. Preserve existing functionality
The design handoff identifies 57 preservation groups and 34 routes.
Treat these as mandatory preservation requirements.
Do not remove, simplify or hide advanced functionality merely because it was not shown in the prototypes.
Preserve:
- Existing user data.
- Career Profile ownership and persistence.
- CV revisions and undo/redo.
- Document imports and exports.
- Application records.
- Authentication.
- AI permissions and qualification restrictions.
- Dashboard customisation.
- Administrative permissions.
- Existing integrations.
- English/Norwegian support.
- Future European localisation.
- Database compatibility.
- Existing accessibility functionality.
Maintain all relevant RD21 data-integrity protections.
## 4. Implementation sequence
Use the implementation milestones documented in the A3 handoff.
Subject to the authoritative dependencies and Phase 10 gates, organise implementation approximately as follows:
**UI-01 — Shared design system and navigation**
Implement foundational styles, typography, components, navigation, themes and responsive structures.
**UI-02 — Dashboard and applications**
Implement dashboard layouts, interactive widgets, application tracking, Kanban and application-detail experiences.
**UI-03 — Career Profile and CV workflows**
Implement the approved Career Profile, CV Editor, template, import and cover-letter experiences.
**UI-04 — Job discovery and application journeys**
Implement approved job-discovery layouts and connections to the existing application workflows.
Coordinate with INT-GLOBAL-JOBS-01 to avoid conflicting implementations.
**UI-05 — Supporting workflows**
Implement calendar, reminders, correspondence, documents, administration, settings and automation controls.
**UI-06 — Onboarding and mobile refinement**
Complete guided optional onboarding, mobile navigation, small-screen layouts and responsive interaction parity.
**UI-07 — Integrated qualification**
Run full preservation, accessibility, responsive, browser and application regression acceptance.
Use the authoritative A3 implementation plan where it specifies more precise dependencies or acceptance requirements.
## 5. Autonomous development
Continue working through the implementation milestones without asking me to approve every ordinary change.
You may autonomously:
- Implement approved designs.
- Refactor affected frontend components.
- Introduce reusable design-system components.
- Improve responsive layouts.
- Fix implementation defects.
- Add tests.
- Update translations.
- Run regression checks.
- Commit validated checkpoints.
- Update documentation.
Pause only when a genuine owner decision, mandatory gate or substantial design change is required.
If a task is blocked, record the blocker and continue independently eligible work.
Do not bypass Phase 10 cleanup approvals, required integration baselines or other authoritative development-entry gates.
## 6. Coordinate with the remaining roadmap
Continue the existing direct roadmap execution.
Do not use the autonomous orchestrator.
Coordinate A3 implementation with:
- Phase 10.
- Product Review.
- International Global Job Search.
- Full European localisation.
- Multilingual homepage redesign.
- Security audit.
- Release readiness.
Avoid duplicate implementations and unnecessary rewrites.
Use established accepted baselines, isolated branches and documented dependencies.
Do not treat provisional mockup content as actual product functionality.
## 7. Validation requirements
For each implementation milestone:
- Run relevant frontend tests.
- Run required backend tests where workflows are affected.
- Validate browser journeys.
- Validate mobile layouts.
- Check keyboard accessibility.
- Check translation behaviour.
- Check light/dark modes.
- Test preservation requirements.
- Compare implementation against the approved A3 design.
- Record regressions and failures.
The prototype validation results are useful design evidence, but they do not qualify the implemented application.
Real screen-reader, physical-device and human accessibility tests remain pending until actually completed.
Do not claim unperformed tests passed.
## 8. Git and release restrictions
The latest design checkpoint is `237c79b8`.
The roadmap continuation checkpoint is `286535be`.
Verify the actual commits, branches and ancestry before integrating work.
Preserve the existing worktrees and acceptance records.
Keep validated changes in development branches and commit checkpoints.
Do not spend time repairing Gitea as part of this design implementation.
If publication fails, preserve the local work, record the issue and continue eligible work without falsely claiming publication success.
Never merge into main or deploy production without explicit approval.
## 9. Final objective
Deliver the approved A3 experience throughout the real Jobjakt application.
I want a cohesive, modern, approachable and powerful product, with equally strong desktop and mobile experiences.
Preserve the application's advanced capabilities while making everyday workflows easier to understand and use.
**This is my final approval of the A3 design and authorisation to begin its staged development implementation.**
It is not approval for production deployment or for bypassing outstanding security and release gates.
Continue with the next authorised implementation milestone, maintaining durable progress reports and proceeding through independently eligible work without unnecessary interruptions.
+30
View File
@@ -0,0 +1,30 @@
# UI-01 — approved A3 foundation implementation
Status: A3-IM01 implementation and scoped validation prepared for Gate F owner review; UI-01 navigation (A3-IM02) remains pending. Implementation branch `codex/a3-ui-01`, isolated integration worktree. Base `d3d338376444b8abdcaf7200f682faa3daf1ad10`; design source `237c79b8cb3e280d7e4d1ef95cdbb2c77bb81c69`; roadmap receipt `286535bef046417a09a156685c937e8be9155147`.
Owner's 3 November 2026 attachment explicitly approves the final A3 foundation and staged real-application implementation. Verbatim authorisation is retained in A3_APPROVAL.md. Gates C/D/E are approved for that scope; deployment, Phase 10, security/provider/native/device/release gates are not waived.
## Task ledger and verification plan
- [x] Verify branch/commits and use accepted integration baseline; preserve existing worktrees. Created implementation branch in the already isolated integration worktree, keeping its original branch ref unchanged. No wholesale merge of the older design tree.
- [x] Baseline: four existing theme/dialog suites, 28 tests PASSED before production edits.
- [x] A3-IM01: approved D1 palette, typography, quiet surfaces and shared controls implemented through existing theme.ts. Added visual-contract regression checks; retained semantic contrast, theme/system persistence, focus, reduced-motion, density and dialog checks.
- [x] Targeted tests, types, lint, production build and actual application browser checks in EN/NB, both themes and mobile/desktop. See VALIDATION.md for exact source scopes and pending human/zoom checks.
- [ ] Commit validated checkpoint, attempt ordinary development-branch publication and independently read back the remote ref. Preserve local work if unreachable; no infrastructure repair.
- [ ] Gate F review for A3-IM01 before next source-defined implementation increment. UI-01 navigation is A3-IM02, not silently treated as complete by palette changes.
Scope reaches the existing ThemeProvider and every MUI theme consumer, including navigation semantic colours and shared fields/buttons/cards/dialogs. Preserve component APIs, routes, application state, server data ownership, RD21 and physical CV/PDF renderers. No dependency, API or database changes. Existing semantic status colours must retain readable contrast on all new surfaces; dividers are decorative and do not replace stronger essential field boundaries.
Ruling: use targeted shared-theme/dialog/state checks and relevant browser workflows rather than thousands of unrelated tests, following the owner's explicit evidence/testing instructions. Backend tests are unnecessary unless backend behaviour is changed. Prototype tests are not application qualification.
Ruling: approved global MUI roles will also be inherited by existing public React consumers; this is shared theme plumbing, not selection or implementation of a new homepage layout/claims/SEO. Keep the existing gradient API and physical document rendering untouched. Later launch-site decisions remain separate.
Rollback: revert this UI increment on its development branch; no persisted data/schema change. Re-run affected theme/layout checks. Never reset unrelated work or alter the autonomous orchestrator.
## Implemented behaviour
Shared MUI theme now uses approved violet canvas/text/accent/navigation roles in both schemes; existing state synchronisation/storage remains unchanged. App chrome uses the approved system font stack, more readable body rhythm and responsive page-title hierarchy. Shared cards use restrained borders without floating section shadows. Buttons use sentence case and 44px base height; outlined fields retain their notched focus indicator and now have a 44px base height. Existing density hit-region rules remain in force. Actual navigation structure and feature layouts are not represented as implemented yet.
Contrast verification exposed the old dark error role at 4.275:1 on the new elevated surface. Corrected dark error main/darker fills to #FF8A80/#FF7067 with explicit dark contrast text; thresholds were not lowered. Final semantic contrast tests pass, and code review checked standard alert text against composited surfaces. CV/template/letter previews use isolated document HTML; physical renderer source, fonts, metrics, fixtures and historical document acceptance were not changed.
Independent code review: no important/critical findings. Accepted minor recommendation to assert every collected scoped axe violation array, rather than only colour contrast. Reviewer's 16 theme tests overlap the final 30 and must not be added to them. Initial two-width browser artifact was superseded by the final four-width run, not added as extra coverage.
+33
View File
@@ -0,0 +1,33 @@
# A3-IM01 — implementation validation
3 November 2026. This qualifies a bounded shared-theme increment in the actual React application. It does not qualify the entire A3 redesign, Phase 10, Product Review, international discovery, European localisation, security or release readiness.
| Check | Result | Scope |
|---|---|---|
| Existing baseline | PASSED: 28 tests / 4 suites | Theme contrast/state, loaded company states and dialog flow before edits. BASELINE.json |
| A3 visual contract before implementation | FAILED as expected: 2 tests | Old light/dark canvas values differed from approved A3; no production code had been changed |
| First new-palette regression | FAILED, corrected | Dark error text 4.275:1 on new elevated surface; changed colour, retained 4.5 threshold |
| Final targeted theme/state/dialog tests | PASSED: 30 / 5 suites | Includes the baseline 28 and 2 new A3 visual-contract cases; not 58 independent tests. THEME_TESTS.json |
| Additional preservation checks | PASSED: 5 / 2 suites | Existing CV thumbnail/template rendering and shell navigation; disjoint from the 30 theme/state/dialog cases. PRESERVATION_TESTS.json |
| Typecheck | PASSED | npm run typecheck, no incremental cache |
| Lint | PASSED | npm run lint, zero-warning policy |
| Static production build | PASSED | npm run build with NEXT_PUBLIC_API_BASE_URL=/api for controlled loopback browser checks; no deployment |
| Actual React screen matrix | PASSED: 32 conditions | Login and loaded Companies × 320/390/768/1440px × EN/NB × light/dark |
| Company dialog keyboard journeys | PASSED: 16 | Keyboard edit/open, visible focus, Escape and focus return; actual MUI controls |
| Scoped axe sweeps | PASSED: 48, zero returned violations | 32 main surfaces + 16 dialogs; rule tags WCAG2 A/AA, 2.1AA, 2.2AA. Excludes shell/navigation from the main-surface sweep; not full WCAG conformance |
| Actual form validation | PASSED: 16 screen variants | Empty login submit marks required fields invalid and focuses first field; no authentication POST |
| Root reflow / colours / locale | PASSED in screen matrix | Actual built app body palette and html lang/scheme, no root overflow at the tested widths |
| Source review | PASSED for bounded patch | Independent reviewer found no important/critical issue; physical document frames/renderers retain font ownership |
| Graph | AST-only refresh completed | Generated graph untracked; no model/semantic graph call |
`evidence/BROWSER.json` records every screen/dialog scope, page exceptions, attempted writes and blocked external requests; `scripts/verify-a3-theme.mjs` reproduces it from the static export served locally on port 4338 by the existing serve-export script. All API responses are controlled synthetic fixtures, all write methods are rejected, and every remote request is aborted. No backend, production data, model, identity provider or mailbox was contacted. This is actual application rendering with controlled service responses, not live-service acceptance.
The first browser run reached all its two-width screen/dialog assertions but failed its final no-external-attempt assertion because the existing page head requests Google Fonts. The harness was corrected to permit only that **blocked** known font origin, retaining all aborts and rejecting any unexpected origin. The final four-width run supersedes that result. No remote font was downloaded. Removal/localisation of existing font imports requires separate caller/renderer review; it was not performed here.
Lint initially rejected CommonJS require imports in the new browser harness. Converted the harness to the project's existing ES-module script convention rather than suppressing lint rules or changing lint configuration. The final .mjs run and full lint supersede the failed intermediate harness check. No application source changed during this conversion.
Evidence has 32 populated screen screenshots. The built application's sidebar structure is still the existing one: only its shared semantic colours changed. Do not present these captures as completed A3 navigation or A1 dashboard implementation. The prototype's 480 conditions/50 axe sweeps are separate historical design evidence and are not included in these application counts.
NOT RUN: real backend flows, full application regression, native screen reader, physical touch/software keyboard, actual browser zoom, native-language review, final integrated security, live provider/model, physical PDF/container requalification, backup/restore or hosted CI. Source-defined acceptance for those scopes remains open. Pending zoom/human checks prevent a claim of complete accessibility acceptance.
Reproduce sequentially from job-tracker-ui with installed dependencies: targeted Jest paths recorded in native JSON reports, npm run typecheck, npm run lint, NEXT_PUBLIC_API_BASE_URL=/api npm run build, serve-export on local port 4338, then `rtk proxy node scripts/verify-a3-theme.mjs`. Rebuild when application source changes; the evidence manifest binds source and exported assets to this checkpoint.
File diff suppressed because one or more lines are too long
+505
View File
@@ -0,0 +1,505 @@
{
"rows": [
{
"screen": "login",
"width": 320,
"language": "en",
"mode": "light",
"geometry": {
"overflow": false,
"background": "rgb(255, 255, 255)"
},
"violations": []
},
{
"screen": "companies",
"width": 320,
"language": "en",
"mode": "light",
"geometry": {
"overflow": false,
"background": "rgb(255, 255, 255)"
},
"violations": []
},
{
"screen": "company-dialog",
"width": 320,
"language": "en",
"mode": "light",
"violations": []
},
{
"screen": "login",
"width": 320,
"language": "en",
"mode": "dark",
"geometry": {
"overflow": false,
"background": "rgb(40, 35, 52)"
},
"violations": []
},
{
"screen": "companies",
"width": 320,
"language": "en",
"mode": "dark",
"geometry": {
"overflow": false,
"background": "rgb(40, 35, 52)"
},
"violations": []
},
{
"screen": "company-dialog",
"width": 320,
"language": "en",
"mode": "dark",
"violations": []
},
{
"screen": "login",
"width": 320,
"language": "nb",
"mode": "light",
"geometry": {
"overflow": false,
"background": "rgb(255, 255, 255)"
},
"violations": []
},
{
"screen": "companies",
"width": 320,
"language": "nb",
"mode": "light",
"geometry": {
"overflow": false,
"background": "rgb(255, 255, 255)"
},
"violations": []
},
{
"screen": "company-dialog",
"width": 320,
"language": "nb",
"mode": "light",
"violations": []
},
{
"screen": "login",
"width": 320,
"language": "nb",
"mode": "dark",
"geometry": {
"overflow": false,
"background": "rgb(40, 35, 52)"
},
"violations": []
},
{
"screen": "companies",
"width": 320,
"language": "nb",
"mode": "dark",
"geometry": {
"overflow": false,
"background": "rgb(40, 35, 52)"
},
"violations": []
},
{
"screen": "company-dialog",
"width": 320,
"language": "nb",
"mode": "dark",
"violations": []
},
{
"screen": "login",
"width": 390,
"language": "en",
"mode": "light",
"geometry": {
"overflow": false,
"background": "rgb(255, 255, 255)"
},
"violations": []
},
{
"screen": "companies",
"width": 390,
"language": "en",
"mode": "light",
"geometry": {
"overflow": false,
"background": "rgb(255, 255, 255)"
},
"violations": []
},
{
"screen": "company-dialog",
"width": 390,
"language": "en",
"mode": "light",
"violations": []
},
{
"screen": "login",
"width": 390,
"language": "en",
"mode": "dark",
"geometry": {
"overflow": false,
"background": "rgb(40, 35, 52)"
},
"violations": []
},
{
"screen": "companies",
"width": 390,
"language": "en",
"mode": "dark",
"geometry": {
"overflow": false,
"background": "rgb(40, 35, 52)"
},
"violations": []
},
{
"screen": "company-dialog",
"width": 390,
"language": "en",
"mode": "dark",
"violations": []
},
{
"screen": "login",
"width": 390,
"language": "nb",
"mode": "light",
"geometry": {
"overflow": false,
"background": "rgb(255, 255, 255)"
},
"violations": []
},
{
"screen": "companies",
"width": 390,
"language": "nb",
"mode": "light",
"geometry": {
"overflow": false,
"background": "rgb(255, 255, 255)"
},
"violations": []
},
{
"screen": "company-dialog",
"width": 390,
"language": "nb",
"mode": "light",
"violations": []
},
{
"screen": "login",
"width": 390,
"language": "nb",
"mode": "dark",
"geometry": {
"overflow": false,
"background": "rgb(40, 35, 52)"
},
"violations": []
},
{
"screen": "companies",
"width": 390,
"language": "nb",
"mode": "dark",
"geometry": {
"overflow": false,
"background": "rgb(40, 35, 52)"
},
"violations": []
},
{
"screen": "company-dialog",
"width": 390,
"language": "nb",
"mode": "dark",
"violations": []
},
{
"screen": "login",
"width": 768,
"language": "en",
"mode": "light",
"geometry": {
"overflow": false,
"background": "rgb(255, 255, 255)"
},
"violations": []
},
{
"screen": "companies",
"width": 768,
"language": "en",
"mode": "light",
"geometry": {
"overflow": false,
"background": "rgb(255, 255, 255)"
},
"violations": []
},
{
"screen": "company-dialog",
"width": 768,
"language": "en",
"mode": "light",
"violations": []
},
{
"screen": "login",
"width": 768,
"language": "en",
"mode": "dark",
"geometry": {
"overflow": false,
"background": "rgb(40, 35, 52)"
},
"violations": []
},
{
"screen": "companies",
"width": 768,
"language": "en",
"mode": "dark",
"geometry": {
"overflow": false,
"background": "rgb(40, 35, 52)"
},
"violations": []
},
{
"screen": "company-dialog",
"width": 768,
"language": "en",
"mode": "dark",
"violations": []
},
{
"screen": "login",
"width": 768,
"language": "nb",
"mode": "light",
"geometry": {
"overflow": false,
"background": "rgb(255, 255, 255)"
},
"violations": []
},
{
"screen": "companies",
"width": 768,
"language": "nb",
"mode": "light",
"geometry": {
"overflow": false,
"background": "rgb(255, 255, 255)"
},
"violations": []
},
{
"screen": "company-dialog",
"width": 768,
"language": "nb",
"mode": "light",
"violations": []
},
{
"screen": "login",
"width": 768,
"language": "nb",
"mode": "dark",
"geometry": {
"overflow": false,
"background": "rgb(40, 35, 52)"
},
"violations": []
},
{
"screen": "companies",
"width": 768,
"language": "nb",
"mode": "dark",
"geometry": {
"overflow": false,
"background": "rgb(40, 35, 52)"
},
"violations": []
},
{
"screen": "company-dialog",
"width": 768,
"language": "nb",
"mode": "dark",
"violations": []
},
{
"screen": "login",
"width": 1440,
"language": "en",
"mode": "light",
"geometry": {
"overflow": false,
"background": "rgb(255, 255, 255)"
},
"violations": []
},
{
"screen": "companies",
"width": 1440,
"language": "en",
"mode": "light",
"geometry": {
"overflow": false,
"background": "rgb(255, 255, 255)"
},
"violations": []
},
{
"screen": "company-dialog",
"width": 1440,
"language": "en",
"mode": "light",
"violations": []
},
{
"screen": "login",
"width": 1440,
"language": "en",
"mode": "dark",
"geometry": {
"overflow": false,
"background": "rgb(40, 35, 52)"
},
"violations": []
},
{
"screen": "companies",
"width": 1440,
"language": "en",
"mode": "dark",
"geometry": {
"overflow": false,
"background": "rgb(40, 35, 52)"
},
"violations": []
},
{
"screen": "company-dialog",
"width": 1440,
"language": "en",
"mode": "dark",
"violations": []
},
{
"screen": "login",
"width": 1440,
"language": "nb",
"mode": "light",
"geometry": {
"overflow": false,
"background": "rgb(255, 255, 255)"
},
"violations": []
},
{
"screen": "companies",
"width": 1440,
"language": "nb",
"mode": "light",
"geometry": {
"overflow": false,
"background": "rgb(255, 255, 255)"
},
"violations": []
},
{
"screen": "company-dialog",
"width": 1440,
"language": "nb",
"mode": "light",
"violations": []
},
{
"screen": "login",
"width": 1440,
"language": "nb",
"mode": "dark",
"geometry": {
"overflow": false,
"background": "rgb(40, 35, 52)"
},
"violations": []
},
{
"screen": "companies",
"width": 1440,
"language": "nb",
"mode": "dark",
"geometry": {
"overflow": false,
"background": "rgb(40, 35, 52)"
},
"violations": []
},
{
"screen": "company-dialog",
"width": 1440,
"language": "nb",
"mode": "dark",
"violations": []
}
],
"errors": [],
"writes": [],
"blockedExternalRequests": [
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com",
"https://fonts.googleapis.com"
],
"scope": "actual static-export React application with controlled API; existing remote font requests aborted; no backend or provider qualification"
}
@@ -0,0 +1 @@
{"numFailedTestSuites":0,"numFailedTests":0,"numPassedTestSuites":2,"numPassedTests":5,"numPendingTestSuites":0,"numPendingTests":0,"numRuntimeErrorTestSuites":0,"numTodoTests":0,"numTotalTestSuites":2,"numTotalTests":5,"openHandles":[],"snapshot":{"added":0,"didUpdate":false,"failure":false,"filesAdded":0,"filesRemoved":0,"filesRemovedList":[],"filesUnmatched":0,"filesUpdated":0,"matched":0,"total":0,"unchecked":0,"uncheckedKeysByFile":[],"unmatched":0,"updated":0},"startTime":1793671502665,"success":true,"testResults":[{"assertionResults":[{"ancestorTitles":[],"duration":66,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"template thumbnails retain distinct server-rendered samples in noninteractive sandboxed frames","invocations":1,"location":null,"numPassingAsserts":6,"retryReasons":[],"startAt":1793671505757,"status":"passed","title":"template thumbnails retain distinct server-rendered samples in noninteractive sandboxed frames"}],"endTime":1793671505824,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\cv-template-thumbnail.test.tsx","startTime":1793671503519,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":[],"duration":258,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"language controls identify Bokmål with its native name and language code","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"startAt":1793671506673,"status":"passed","title":"language controls identify Bokmål with its native name and language code"},{"ancestorTitles":[],"duration":108,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"[P5-01 PRES-E07 E08] grouped navigation exposes the current child without marking the parent as the page","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"startAt":1793671506931,"status":"passed","title":"[P5-01 PRES-E07 E08] grouped navigation exposes the current child without marking the parent as the page"},{"ancestorTitles":[],"duration":222,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"[P5-01 PRES-E07 E08] collapsed secondary destinations remain keyboard-operable","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793671507039,"status":"passed","title":"[P5-01 PRES-E07 E08] collapsed secondary destinations remain keyboard-operable"},{"ancestorTitles":[],"duration":142,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"[P5-01 PRES-E08] successful shell navigation focuses the new route heading and exposes a skip link","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793671507261,"status":"passed","title":"[P5-01 PRES-E08] successful shell navigation focuses the new route heading and exposes a skip link"}],"endTime":1793671507403,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\app-shell-navigation.test.tsx","startTime":1793671505840,"status":"passed","summary":""}],"wasInterrupted":false}
@@ -0,0 +1,150 @@
{
"recordedAtUtc": "2026-11-03T02:07:53.565494+00:00",
"base": "d3d338376444b8abdcaf7200f682faa3daf1ad10",
"design": "237c79b8cb3e280d7e4d1ef95cdbb2c77bb81c69",
"policy": "Text and JSON LF-normalised SHA-256; PNG/exported asset bytes raw",
"sources": {
"job-tracker-ui/src/theme.ts": "6b3c28a2a920e53e01ed9ee12ee2f5cdaa0fe7c3752415a97ca544c82fce8cfa",
"job-tracker-ui/src/ThemeProvider.tsx": "1d25fd091ca632f7671e413db24e917d4ca4a375fddade0ca1ff397a8ae9f950",
"job-tracker-ui/src/themePrefs.ts": "d775741cd6b2fc7ac59dca13e5e6cf5ffd3856cf4a2e011948b2dfb0d9c319ca",
"job-tracker-ui/src/themeBootstrap.ts": "0035725a9d43077ede757c1f20382465a6f04511b63fb22ac65adcfb8ce5f2d0",
"job-tracker-ui/src/a3-theme-contract.test.ts": "039382d49d84f84df81d583079f217a21ca0be4b9c90c82f2da744998fbfe73d",
"job-tracker-ui/scripts/verify-a3-theme.mjs": "3d550137be4500b0a1711b88d34d01592ab33e2b9dad261a19b033dc4db609c0",
"job-tracker-ui/package.json": "3745394e406cefa40d30a87e840bccb359be147bac6ffba736e2ae90898a8626",
"job-tracker-ui/package-lock.json": "bf31e90d7af03e8af059db2aaf893925b9e8881a30a1688034ed88ef45d4fcab",
"job-tracker-ui/src/App.tsx": "28d906c0f04d82da41125d4722a04dce2b2c1573e333b68a0ef83018d6f94c9f",
"docs/pre-phase10-ux/preservation.json": "a4c2a5a0244ea89381008218de32daee1f615a250eb3808b60e0ee103c674d4d",
"docs/ui-modernisation/A3_APPROVAL.md": "54243c73fcb15e9c03b81e2c6ee3cb0ed3485067891243a33da9eb83fb383fca"
},
"evidence": {
"BASELINE.json": "2cf5b006ab9a3c499be32c0e3c3e4aff69708819b6516c6044b8db835dbfab74",
"BROWSER.json": "f16dd11c041d3f9a57474acc8f3efd71639f048448894f9c1d97b114996d4261",
"companies-en-dark-1440.png": "26ef08a0df3b17e33499cc2a0a6a78916702e4bed6d9254bfb60f23deaf0d6c3",
"companies-en-dark-320.png": "b18d4e604f40a17bfdf2624d5823c17d467e03583beccc0e497893ee51c735ce",
"companies-en-dark-390.png": "8f17a05b8b22cced76ef47326863b8317ed71ea2807db301cf762ad2f869ac28",
"companies-en-dark-768.png": "ba34903cae2b5de339b667e3e6817b821c46e761d8467d876e1ca2bd0ce0202e",
"companies-en-light-1440.png": "7ae163a26e395005cc09b2cca8f492ec64b52aae19be6251ef169f6f62a9645c",
"companies-en-light-320.png": "bc4d8a94bf354c0968ea47d97bd59b56e42fa9e9730a4e59d4671060062f8b0d",
"companies-en-light-390.png": "b01d0ffb8ca2703bc75dcf5ab1c34ac442737ba5d5f9792a1627eb959f665553",
"companies-en-light-768.png": "826d7b6707f238703a0a4a74d81c1e5542bcc7f4d1da876e12557fce003b31b9",
"companies-nb-dark-1440.png": "579072333acc7fefd677277083f9c9d2792523ef12d7ace02afe973f47c36c7f",
"companies-nb-dark-320.png": "62795143d79221b2711077d5c0cdd5766d920418036f18340221a018c845e2a4",
"companies-nb-dark-390.png": "e10db421d105c130e7265667b03f8018a934a826ecfcf2500880f89a3379590e",
"companies-nb-dark-768.png": "fe2003b73991b7b80c206a0b2baa8fce90c9a994a24d0ab9813aa018e94b5c12",
"companies-nb-light-1440.png": "6b3c2f917fb33db424f81be3777969122504455f380db3e5729cad26475487a2",
"companies-nb-light-320.png": "5b8b170cdd93f9ee73049a9e307f3c7e8663c640f6cdd3b5e657d519c194f796",
"companies-nb-light-390.png": "d18c51480b8fb8f75247118f3bbaeec55f0702e29a988aaf07bc143caf0ae913",
"companies-nb-light-768.png": "b430df3421e72a6040730d5dff90e8584cb7926452d537d6fa5ac50353b742a8",
"login-en-dark-1440.png": "7f14b204ac297a11d0f66ae14630fc178db576003f5739c8837c0d89a8c671b5",
"login-en-dark-320.png": "1349497aa9a8c69135fd65a1b033c235519fea6158574df0bfe480168366fbe6",
"login-en-dark-390.png": "e1bc62e9622daed83e702bece9fbbde56a0eb8b44d662436c3c88884dee53571",
"login-en-dark-768.png": "398df1bc62c65b52808dfc4060a15f3ddda4ecd53600e1f0f1f48116d46330e8",
"login-en-light-1440.png": "adf362ed3cb066a868d466317a5a5f1f9e32e80292d780a43b435e67d8250229",
"login-en-light-320.png": "4b0c2f278b6224969bf8d16e73a4bce317948b6fa9b5f2a46ddb4c1692848455",
"login-en-light-390.png": "526ae4c4c998c766ad4cb55ccce65db6dd6448c6ca3a88d5fa497d17ffff006a",
"login-en-light-768.png": "569319b19e4bb37955a805778a87ca4dfd2b13c4c8c33c4ea9d2e0418f9e25cd",
"login-nb-dark-1440.png": "e3dfcad81696cb40e33acae6b2ae9553eb773d5c02e84f9b5974f978a2345389",
"login-nb-dark-320.png": "810c6f9b9dd368de5836620004f777b6c7bfd38d20bd6f9a41bef43082ae40b6",
"login-nb-dark-390.png": "5dd78b744b92c33f70f40848c905a26f38184324cf77f51caf3d947d59d22902",
"login-nb-dark-768.png": "a95543cb6c60c308f756d4f16f480be75d20b03fd9ab855ad3abcd56434bda2c",
"login-nb-light-1440.png": "4ca9d0de9a2aaf217c484073f0b3ffffba91d33bfaceef313bc748ec3ad19cc3",
"login-nb-light-320.png": "d2d9729bcc9c3cf8d3b8044c9b6e63d463a84b2f7aabb362ca5b7bebd9538804",
"login-nb-light-390.png": "7fd0429224e620c58552a9e1ada72e830eb10b3ef392b76f0074a4e6d343f42f",
"login-nb-light-768.png": "8f7d2a44913d5cc3dc835b9bde07f04fd56a300cc9bf502a34825777eb02cb74",
"PRESERVATION_TESTS.json": "e3e0007714e0ea38daf016516f8442ec785ca6f588f6ffafe915723a32c34fd6",
"THEME_TESTS.json": "eaccb949dc12cfc0845292bde4dfe2cda1a8af7c107357c52dc68aa2c4c3f195"
},
"exportAssets": {
"404.html": "44f4f5ea405e5f3c283757c06815b9d76b80ccded65975710f79eb08cc686fe9",
"favicon.ico": "8d4923e2c487805ea9f852a685b2cf0fb10512ded8290d2a5c37d1cc8fd0702a",
"favicon.svg": "63ecf8f37f8bb146ecfae3917187c6b692d4e801641e8a314486a4007912f3a3",
"index.html": "62802e5c9baf3cbb913a5602d23193265f35e68b70b262b262d509b5ad8d9e41",
"index.txt": "f0e4bed88cef037cb296f31ccdada5fa0698b673ea8816de1e1901f7a5cf7510",
"logo192.png": "bc652031597fabd19b9f14e22a71f809490d218037521b3b64d013418c1390b4",
"logo512.png": "5129e9bc1ba812b711fd9a646d3a38a906d365a26ee685df183b8752458944d4",
"manifest.json": "2667ce63c2e0835b7007239816a5597639321d333da396a78638dfa9e8a7c935",
"robots.txt": "e5ab0d231eeb01b4a982d1c79a6729cac9797ad15a69247e4f28ba6afc149b4c",
"_not-found.html": "44f4f5ea405e5f3c283757c06815b9d76b80ccded65975710f79eb08cc686fe9",
"_not-found.txt": "6ee9df2ea06c8d6b46b4a2e397cbb0a209381f31125f5aa60d790ad65f2c7e5f",
"__next._full.txt": "f0e4bed88cef037cb296f31ccdada5fa0698b673ea8816de1e1901f7a5cf7510",
"__next._tree.txt": "b9100c9cbea4f7b3ba2f511d63c7d4b9d82ddf8b305cbefc4709fde4e99f742b",
"__next.__PAGE__.txt": "a9259f14c2060132db3929f867737f869b58e47eeb73f7306a101565860da7f6",
"mockups/dashboard.svg": "318be3adb1324c26eb30f05572a29528a3e38331c6a604652eef75b54f8c29dc",
"mockups/pipeline.svg": "db2ef41faabeb90e7ce85413e168581686c54535d970fc7d8deda3888a479de4",
"mockups/workspace.svg": "9bbab22349147c7b01d6e18c93c622a6f8aed6dedaf23715189ab54d60e1ddbd",
"_not-found/__next._full.txt": "6ee9df2ea06c8d6b46b4a2e397cbb0a209381f31125f5aa60d790ad65f2c7e5f",
"_not-found/__next._tree.txt": "128650e2c58388ed26616d9820dcbd1a477dbc1f8674d57742c69b584ced1f94",
"_next/static/chunks/0-_aq6ipzcshj.js": "39f90c215bf52aef6274ed3dcc0f8af32e21a37fe8845b3bca0d6ea60da6a8c2",
"_next/static/chunks/07vdc-duql714.js": "5a4840d1ba6b65782459968940a2f4cf973c99f0f77890f4171c4f420f339353",
"_next/static/chunks/0bdt7ln111t48.js": "6184b1a61f22e2a640c17941634fdc75678ce93d142848d9f07f163e0d1fd201",
"_next/static/chunks/0bwvt3mysnclc.js": "7705667a1b75812b7833d9c9dff47fe023015e58e382c40f4507d9948c973af4",
"_next/static/chunks/0cz1d0mv5g_q7.js": "0973c1d64c88adc8e3c950410cb58b288f72118d5965b78049438deb8f2f9683",
"_next/static/chunks/0enhs8x_8oeah.js": "44c76f6178d407e1f147fdf70015fd05c21e58ed220fc922d23dfd1e0f8631bd",
"_next/static/chunks/0jcvc3qm6-lyk.js": "7c27e1fe373b822adc68c63096a5bf38444ad605b9743f0920271652989c56f1",
"_next/static/chunks/0lxzrfwn1fphq.js": "1f1792240611f3fabb14afa6b66f80de20b035a3d0ceee2e40f5f6f589129774",
"_next/static/chunks/0q20g9o8x-k8z.js": "3d4d9086985419aa8866b032158196c774acb8bf318f3c8245508c8daa9c5148",
"_next/static/chunks/0q_pwu9jxv4-d.js": "42a43b3d7245a413c7c29b4cbe87823037a6dbc637e2e282c92405470e0f575d",
"_next/static/chunks/0w69-xg9od0ja.js": "88a7bd64f4fddf57cdbf3e993be2a44e48d5b313e434038bc9a7337d107ae162",
"_next/static/chunks/11-48p8kygxgp.js": "57eb9059599b5a5a30598d0a334912d58af571c0ffe28448ca76ad77c8ea8df9",
"_next/static/chunks/13uq1xe3s3l_c.js": "dd5b46dbf29a692062dcedc2604ff9c254db1abfcf9d39f30ab96b2de87d1707",
"_next/static/chunks/14-lfugawba2w.js": "6a5780ca3c84d7f130f3c0a14431c85c7a1e799e2645b8c171161dab40a64360",
"_next/static/chunks/1435in_k_-dhc.js": "4757476512d5c4dd25f9264a18b304ed4824b23aff1c09e134fcbff21d9c36b6",
"_next/static/chunks/145oicobmjvm7.js": "d6bbbdeed1381652dc7412e5ce673a077579ecd5cee8934cb6b533c814098764",
"_next/static/chunks/14e7ovj0s3elm.js": "070567dd4d23fb9fa0b9ba132185a697af6578045d43b2b1c7bbb2a1594d935d",
"_next/static/chunks/154b5a6du9olf.js": "49e4df3a3e10119ddc34f84767883b9228ff6798a07fd759c2fb8e2be62fe9fb",
"_next/static/chunks/16lfece6cnlln.js": "716363ba044f30b0e70440756b4e5eafa2d25f2e1628e855e3bc4ab9eabee3d3",
"_next/static/chunks/1d0xrdyyzw7qc.js": "c11668ff1e152008e0d7b4454af12eb36c97df46a5860938e2eb5d8429b374a8",
"_next/static/chunks/1h7j7ef9e6i41.js": "cb6cd497566dc188b67d09b9c031ef9bbf36f2b85d0aac02621f56daa44faa34",
"_next/static/chunks/1ke8pzecwofrg.css": "32e9ab963ebadebcbe2c68dffb24cfbfc8cc24255ad91b48b7941fc777bf7439",
"_next/static/chunks/1m3dt58dmprbw.js": "3a80b00707e6a884a03fa636260511585f1760f37c24fd4861ead110e6b3e985",
"_next/static/chunks/1req80b9jj38r.js": "eee4d2e3789e1215195dba99ad0f09d34f8c83da848322b76054722233e9dae3",
"_next/static/chunks/1vlgr-jtg9qr0.js": "6e41421a30b7313860e86c2069ed9dde394e29d004eeb42af46b2d2d124a1acb",
"_next/static/chunks/1w3q8yrcjn2jk.js": "365200e241ee9290e93d7c2fdba169f2b0a8e774541e3cbf3f6f2781110fa644",
"_next/static/chunks/1xn023_28rgzj.js": "4f7f2108d030a48d039930736c92a55710975dddf419fe673f84d92899b1f575",
"_next/static/chunks/1xwc-b795syag.css": "f73c002233f931ca14d0ae58815c47ca9f2659f25b1547a6d1fdca12a2a00b3f",
"_next/static/chunks/1zi8i6dnxiac_.js": "8aea28bbe6a138e5d4b619429ca71570883a7aee974b86d5a4a746d90f704dbc",
"_next/static/chunks/21n31t_q2ou2m.js": "bbd6a5dc061a64d5b7817babeeb5c2f90aae8ba21cff54fea1064c39f497af6a",
"_next/static/chunks/22uh2tvaw80j2.js": "dafe6b4e23dc5c1d32f8b4a60c561238d2d5e7545f23ebc5b3f7304b48626dbf",
"_next/static/chunks/22wx8--1b9tnq.js": "3f7d8e762371262c6a740b4dac21725b424847319afac46055e97074a99ff5f5",
"_next/static/chunks/245ibf0f31ne8.js": "8b27ab5d720bd278b5d73a7e824bfcccf4cc893ced9fa6f1937bb2bc7a65f967",
"_next/static/chunks/280cq9ekejv1f.js": "01de8c1c3555cdd8d711763065ea9d8aec2968c3053f61254e43e26c999027bf",
"_next/static/chunks/2br3ad_-bc7k9.js": "80fa55f7a32db4d743fda36747c36994fde00a0f53f2eb25b2a6d07c6fb97cd7",
"_next/static/chunks/2gd9s_n-0nl1p.js": "31e2811be51a3c4d0ff2d1e3cea756e7fd8d57ef89141b5de69f638c1301d039",
"_next/static/chunks/2j9f-6w2x4auf.js": "a23068fc68514dcaa1375de8802bf7f9bdfc561835e4fe47b9e6fe8f7372f85c",
"_next/static/chunks/2s2gjr6ro25dj.js": "7489e2ac285a05b563c2bcb4eaf9274bf930f2a7385f17d6d5e6850064b88b10",
"_next/static/chunks/2u5qve004opku.js": "be388d9a6aa423082bafb4584c3facc65ddfed71bcfa5c1d06b097b408c5ff97",
"_next/static/chunks/2wae6urahmel1.js": "ae86575883781981cc373601a07009101d81eba801f40d4607e6f3f168777a5b",
"_next/static/chunks/2xlg4_1fj8fxh.js": "8ab4ba521db0167f3dbf684787ebf643cbc66fa7ffff7cdc65a8d8076a926155",
"_next/static/chunks/2z42el0b2mp8_.js": "86ff990ef32c90c4839583e4f1d1bcbddb165dad3141b9fcd2ecec67f4ff24b8",
"_next/static/chunks/334vtqp3-g1j6.js": "c33d42da97f3af7388e29273297e9f42c67f54b7f5ec77a5d0637e2da06d7f7e",
"_next/static/chunks/3720519r84mh6.js": "54358db392a13914e3363046d594e824b1a3ef987913d95002bf48267dded4aa",
"_next/static/chunks/3bqn9b34na0b9.js": "f1b8ebcc1b22251c33c9f3a5d11c1f893cad2da2e8744a441e790f7347aa9e73",
"_next/static/chunks/3ckk_j5ril9ag.js": "87f631dae7b648d32171e9e05a3e000753ecb54f8d3b86a4401c0e4ab4d73553",
"_next/static/chunks/3f96r6vscobou.js": "d4b452ff3f1433f8fe1df705a8f592326f51b5a8846e6c2790d28df5a977aed3",
"_next/static/chunks/3gkylew_qn7cw.js": "7e6c74c81be0c41ab433e36d65b914f62c48f332922e27f7cc953476f4dd78cb",
"_next/static/chunks/3mowdlh-jq9ii.js": "2c6ccd1500d28ec0c7312cb08ee221f0e4184ba21465ee166a054aa9a6c7f3ed",
"_next/static/chunks/3os11ovm65tyl.js": "c5a908bd61f53f0d75927efb71e53aef8fdd9b2e4fef412a167990dbc702c166",
"_next/static/chunks/3q4v10rrqy40n.js": "d87ff7b7a0f07d0d92eab3876481eb686a12d9c43d8b92cbe406aefa227a11c1",
"_next/static/chunks/3q80eur194l-9.js": "7b52d29843c23a0ed74056eeb1704de14488f36e9d05f2f8271434c61051dce0",
"_next/static/chunks/3u61o8t7m_jza.js": "86231c12e0f8cacc8638bed0bc4b34f645fc35e60de342dc154d3cce0c9a5afa",
"_next/static/chunks/40y_aqqilokr2.js": "1f5a5260316134d6bffe949400752ee6b42addf908ae714ef4ae4ebde916dddd",
"_next/static/chunks/43b5x8qjjh3ax.js": "6ee7261cf9a63e0e6ea181bbb09125e09948588b6e10b5c02727c686a6062a3a",
"_next/static/chunks/442d7ylb3po0z.js": "d95cedfbdcd00c674fd718cbbf391ec01c0728018b0db2461a06df7074049132",
"_next/static/chunks/turbopack-25fzg9ay_1ss-.js": "c40f39e993a30c96d6247bb3a7b581538bb046efe8ef8ec07bea3ba32291750f",
"_next/static/ibUkuY7Y_5wUxZxd0jW38/_buildManifest.js": "9476e0afd3f9da68a64e6ec58631191fd3eaca62c9f5a377e7517fe9a89e59ea",
"_next/static/ibUkuY7Y_5wUxZxd0jW38/_clientMiddlewareManifest.js": "5483e3295dc35dfe29ea932efc85341c916f10ff78ff5b0e965e003710893a4a",
"_next/static/ibUkuY7Y_5wUxZxd0jW38/_ssgManifest.js": "678f6ce2cb80b1fe72fc67e7412be6e2ab6ada083111b64f7c40d35e3cba5e00",
"_not-found/__next._not-found/__PAGE__.txt": "b29ec33fb211daa1fd701159576a82422aecee42bc24fa6927e28f4e22e3df0c"
},
"commands": {
"typecheck": "npm run typecheck: exit 0",
"lint": "npm run lint: exit 0",
"build": "NEXT_PUBLIC_API_BASE_URL=/api npm run build: exit 0",
"browser": "node scripts/verify-a3-theme.mjs: exit 0"
},
"limits": [
"synthetic API fixture browser lane",
"Google font requests aborted, not downloaded",
"no backend or native-device/security/release acceptance"
]
}
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

@@ -0,0 +1,74 @@
// Real built React application; controlled API responses only, never a live service.
import { chromium } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
const out = path.resolve(import.meta.dirname, '../../docs/ui-modernisation/evidence');
(async () => {
const browser = await chromium.launch();
const rows = [], errors = [], writes = [], external = [];
try {
for (const width of [320, 390, 768, 1440]) for (const language of ['en', 'nb']) for (const mode of ['light', 'dark']) {
const context = await browser.newContext({ viewport: { width, height: 1000 }, reducedMotion: 'reduce' });
const page = await context.newPage();
page.on('pageerror', error => errors.push(error.message));
await page.addInitScript(({ language, mode }) => {
localStorage.setItem('uiLanguage', language);
localStorage.setItem('jobtracker.themeMode', mode);
}, { language, mode });
await page.route('**/*', route => {
const request = route.request(), url = new URL(request.url());
if (!['localhost', '127.0.0.1'].includes(url.hostname)) { external.push(url.origin); return route.abort(); }
if (!url.pathname.startsWith('/api/')) return route.continue();
if (request.method() !== 'GET') { writes.push({ method: request.method(), path: url.pathname }); return route.abort(); }
const fixtures = {
'/api/auth/config': { requireAuth: true, allowRegistration: true, googleEnabled: false, microsoftEnabled: false, turnstileEnabled: false },
'/api/auth/me': { id: 'a3-synthetic-owner', sessionContext: 'a3-synthetic-context', email: 'alex@example.test', roles: [], uiLanguage: language === 'nb' ? 'nb-NO' : 'en' },
'/api/companies': [{ id: 7, name: 'Nordlys synthetic company', location: 'Tromsø', source: 'Manual' }],
'/api/jobapplications/reminders': [], '/api/notifications/unread-count': { count: 0 },
};
return route.fulfill({ status: Object.hasOwn(fixtures, url.pathname) ? 200 : 404, contentType: 'application/json', body: JSON.stringify(fixtures[url.pathname] ?? { message: 'Not part of this controlled theme check' }) });
});
for (const screen of ['login', 'companies']) {
await page.goto(`http://localhost:4338/${screen}`);
await page.locator('main h1').waitFor();
assert.equal(await page.locator('html').getAttribute('data-color-scheme'), mode);
assert.equal(await page.locator('html').getAttribute('lang'), language === 'nb' ? 'nb-NO' : 'en');
if (screen === 'companies') await page.getByText('Nordlys synthetic company', { exact: true }).waitFor();
const geometry = await page.evaluate(() => ({ overflow: document.documentElement.scrollWidth > innerWidth + 1, background: getComputedStyle(document.body).backgroundColor }));
assert.equal(geometry.overflow, false);
assert.equal(geometry.background, mode === 'light' ? 'rgb(255, 255, 255)' : 'rgb(40, 35, 52)');
const scan = await new AxeBuilder({ page }).include('main').withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa']).analyze();
rows.push({ screen, width, language, mode, geometry, violations: scan.violations });
await page.screenshot({ path: path.join(out, `${screen}-${language}-${mode}-${width}.png`), fullPage: true });
assert.deepEqual(scan.violations, [], `${screen}/${mode}/${language}/${width} scoped accessibility`);
if (screen === 'login') {
await page.locator('button[type=submit]').click();
assert.ok(await page.locator('input[aria-invalid=true]').count() >= 1);
assert.equal(await page.locator('input').first().evaluate(node => node === document.activeElement), true);
assert.equal(await page.locator('input').first().evaluate(node => getComputedStyle(node.closest('.MuiOutlinedInput-root').querySelector('fieldset')).borderTopWidth), '3px');
} else {
const edit = page.locator('[data-company-edit="7"]'); await edit.focus();
await page.keyboard.press('Tab'); await page.keyboard.press('Shift+Tab');
assert.equal(await edit.evaluate(node => node === document.activeElement), true);
assert.equal(await edit.evaluate(node => getComputedStyle(node).outlineWidth), '3px');
await edit.press('Enter'); await page.getByRole('dialog').waitFor();
const dialog = await new AxeBuilder({ page }).include('[role=dialog]').withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa']).analyze();
rows.push({ screen: 'company-dialog', width, language, mode, violations: dialog.violations });
assert.deepEqual(dialog.violations, []);
await page.keyboard.press('Escape'); await page.getByRole('dialog').waitFor({ state: 'hidden' });
assert.equal(await edit.evaluate(node => node === document.activeElement), true);
}
}
await context.close();
}
assert.deepEqual(errors, []); assert.deepEqual(writes, []);
// Existing font stylesheet requests are blocked, not fetched or needed by A3's system stack.
assert.ok(external.every(origin => origin === 'https://fonts.googleapis.com'), 'unexpected external request');
console.log('PASS: 32 actual-application screen conditions, 16 company-dialog keyboard journeys; controlled API only.');
} finally {
fs.writeFileSync(path.join(out, 'BROWSER.json'), JSON.stringify({ rows, errors, writes, blockedExternalRequests: external, scope: 'actual static-export React application with controlled API; existing remote font requests aborted; no backend or provider qualification' }, null, 2));
await browser.close();
}
})().catch(error => { console.error(error); process.exitCode = 1; });
@@ -0,0 +1,17 @@
import { getTheme } from "./theme";
// These values bind the shared theme to the owner's approved A3 visual contract.
test.each([
["light", "#ffffff", "#f5f4f9", "#694394", "#302744", "#695e78", "#ddd6e7"],
["dark", "#282334", "#352d43", "#d7b6f7", "#edf0f6", "#cec2da", "#5e526e"],
] as const)("%s uses the approved A3 semantic roles", (mode, canvas, surface, accent, ink, muted, divider) => {
const theme = getTheme(mode);
const palette = theme.colorSchemes[mode].palette;
expect(palette.background.default.toLowerCase()).toBe(canvas);
expect(palette.background.elevated.toLowerCase()).toBe(surface);
expect(palette.primary.main.toLowerCase()).toBe(accent);
expect(palette.text.primary.toLowerCase()).toBe(ink);
expect(palette.text.secondary.toLowerCase()).toBe(muted);
expect(palette.divider.toLowerCase()).toBe(divider);
expect(theme.typography.button.textTransform).toBe("none");
});
+36 -43
View File
@@ -4,17 +4,16 @@ type PaletteLike = Record<string, any>;
export type ContentDensity = "comfortable" | "compact";
// Existing provisional brand hue. Dark mode uses a lighter tone for readable text;
// this does not settle the Phase 3 brand/identity decision.
const ACCENT = "#5B5BD6";
const DARK_ACCENT = lighten(ACCENT, 0.45);
// Approved A3/D1 application palette; document renderers own their own colours.
const ACCENT = "#694394";
const DARK_ACCENT = "#D7B6F7";
// The existing inverse sidebar remains inverse in both modes. These are MUI palette
// roles so feature code does not have to invent colors for this surface.
const navigation = {
background: "#0F172A", border: "rgba(255,255,255,0.08)",
text: "#E2E8F0", muted: "#94A3B8", hover: "rgba(255,255,255,0.06)",
selected: "rgba(99,102,241,0.18)", selectedText: "#FFFFFF", selectedIcon: "#A5B4FC",
background: "#302744", border: "#5E526E",
text: "#EDF0F6", muted: "#CEC2DA", hover: "rgba(215,182,247,0.10)",
selected: "rgba(215,182,247,0.18)", selectedText: "#FFFFFF", selectedIcon: DARK_ACCENT,
};
function buildPrimary(main: string) {
@@ -28,11 +27,11 @@ function buildPrimary(main: string) {
}
function buildLightPalette(): PaletteLike {
const textPrimary = "#1B1B1F";
const textSecondary = "#46464F";
const textPrimary = "#302744";
const textSecondary = "#695E78";
const secondaryMain = "#5A5C78";
const divider = "#EFEDF4";
const divider = "#DDD6E7";
const background = "#FFFFFF";
const disabled = "#777680";
@@ -42,7 +41,7 @@ function buildLightPalette(): PaletteLike {
primary: { ...buildPrimary(ACCENT), contrastText: "#FFFFFF" },
contrastThreshold: 4.5,
navigation,
focus: { main: ACCENT, inverse: "#A5B4FC" },
focus: { main: ACCENT, inverse: DARK_ACCENT },
border: { subtle: divider, default: "#777680", strong: textSecondary },
secondary: {
lighter: "#E0E0FF",
@@ -97,9 +96,7 @@ function buildLightPalette(): PaletteLike {
disabled,
},
divider,
// Soft grey app background with white paper gives the layered dashboard look
// from the product mockups; cards/inputs (paper) sit above it.
background: { default: "#F4F6FB", paper: background, elevated: background },
background: { default: background, paper: background, elevated: "#F5F4F9" },
action: {
hover: alpha(ACCENT, 0.05),
selected: alpha(ACCENT, 0.12),
@@ -111,11 +108,11 @@ function buildLightPalette(): PaletteLike {
}
function buildDarkPalette(): PaletteLike {
const bg = "#0B0B0E";
const paper = "#111116";
const divider = alpha("#FFFFFF", 0.10);
const textPrimary = "#EAEAF2";
const textSecondary = alpha(textPrimary, 0.72);
const bg = "#282334";
const paper = "#282334";
const divider = "#5E526E";
const textPrimary = "#EDF0F6";
const textSecondary = "#CEC2DA";
const secondaryMain = "#A4A6C8";
const disabled = alpha(textPrimary, 0.5);
@@ -125,8 +122,8 @@ function buildDarkPalette(): PaletteLike {
primary: { ...buildPrimary(DARK_ACCENT), contrastText: paper },
contrastThreshold: 4.5,
navigation,
focus: { main: DARK_ACCENT, inverse: "#A5B4FC" },
border: { subtle: divider, default: "#777680", strong: "#B8B8C6" },
focus: { main: DARK_ACCENT, inverse: DARK_ACCENT },
border: { subtle: divider, default: "#9B8EAA", strong: "#CEC2DA" },
secondary: {
lighter: alpha(secondaryMain, 0.22),
light: alpha(secondaryMain, 0.14),
@@ -137,8 +134,9 @@ function buildDarkPalette(): PaletteLike {
error: {
lighter: alpha("#DE3730", 0.18),
light: alpha("#DE3730", 0.12),
main: "#FF5A52",
dark: "#FF3B33",
main: "#FF8A80",
dark: "#FF7067",
contrastText: paper,
darker: "#FFC2BE",
},
warning: {
@@ -180,7 +178,7 @@ function buildDarkPalette(): PaletteLike {
disabled,
},
divider,
background: { default: bg, paper, elevated: "#1C1C24" },
background: { default: bg, paper, elevated: "#352D43" },
action: {
hover: alpha(DARK_ACCENT, 0.10),
selected: alpha(DARK_ACCENT, 0.18),
@@ -196,10 +194,7 @@ function buildCustomShadows(palette: any) {
const primaryColor = palette.primary.main;
return {
button: `0px 0.711px 1.422px 0px ${alpha(shadowColor, 0.05)}`,
// Mockup cards float on the grey page background with no visible border, just a soft,
// fairly generous shadow (dashboard stat tiles, pipeline columns' cards, job-workspace
// panels) -- the old 1px "section" shadow read as flat/bordered rather than elevated.
section: `0px 1px 2px 0px ${alpha(shadowColor, 0.04)}, 0px 8px 24px -12px ${alpha(shadowColor, 0.12)}`,
section: "none",
tooltip: `0px 12px 16px -4px ${alpha(shadowColor, 0.08)}, 0px 4px 6px -2px ${alpha(shadowColor, 0.03)}`,
focus: `0px 0px 0px 3px ${palette.focus.main}`,
// Gradient CTA (mockup's "Tailor my CV" / "See the interface tour" buttons) gets its own
@@ -218,10 +213,10 @@ export const GRADIENT_CTA = "linear-gradient(90deg, #6366F1 0%, #22D3EE 100%)";
function buildTypography() {
return {
fontFamily: "'Archivo', system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif",
fontFamily: "'Trebuchet MS', system-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif",
letterSpacing: 0,
// Opt-in application hierarchy; legacy page/marketing variants stay unchanged.
pageTitle: { fontWeight: 700, fontSize: "1.5rem", lineHeight: 1.25, letterSpacing: "-0.01em", "@media (min-width: 768px)": { fontSize: "1.75rem", lineHeight: 34 / 28 } },
pageTitle: { fontWeight: 700, fontSize: "1.75rem", lineHeight: 1.25, letterSpacing: "-0.01em", "@media (min-width: 768px)": { fontSize: "2rem", lineHeight: 1.25 } },
sectionTitle: { fontWeight: 600, fontSize: "1.125rem", lineHeight: 24 / 18 },
pageDescription: { fontWeight: 400, fontSize: "0.875rem", lineHeight: 20 / 14 },
// Mockup headings (F:\Pictures\website\jobtracker\new) are bold/black-weight display
@@ -237,12 +232,12 @@ function buildTypography() {
h6: { fontWeight: 600, fontSize: 16, lineHeight: "20px" },
subtitle1: { fontWeight: 600, fontSize: 14, lineHeight: "18px" },
subtitle2: { fontWeight: 600, fontSize: 13, lineHeight: "17px" },
body1: { fontWeight: 400, fontSize: 14, lineHeight: "18px" },
body2: { fontWeight: 400, fontSize: 13, lineHeight: "17px" },
body1: { fontWeight: 400, fontSize: 15, lineHeight: 1.55 },
body2: { fontWeight: 400, fontSize: 14, lineHeight: 1.5 },
caption: { fontWeight: 400, fontSize: 12, lineHeight: "16px", letterSpacing: 0 },
overline: { fontWeight: 700, fontSize: 11, lineHeight: "14px", letterSpacing: "0.08em", textTransform: "uppercase" as const },
caption1: { fontWeight: 500, fontSize: 12, lineHeight: "16px", letterSpacing: 0 },
button: { textTransform: "capitalize" as const, fontWeight: 600 },
button: { textTransform: "none" as const, fontWeight: 600 },
};
}
@@ -269,9 +264,7 @@ export const getTheme = (_mode: "light" | "dark") => {
light: { palette: lightPalette, customShadows: buildCustomShadows(lightPalette) },
dark: { palette: darkPalette, customShadows: buildCustomShadows(darkPalette) },
},
// Mockup radius reads noticeably rounder than the previous 10/8px defaults across cards,
// stat tiles, kanban cards and buttons.
shape: { borderRadius: 14 },
shape: { borderRadius: 8 },
typography: buildTypography() as any,
} as any) as any;
@@ -376,9 +369,8 @@ export const getTheme = (_mode: "light" | "dark") => {
defaultProps: { elevation: 0 },
styleOverrides: {
root: ({ theme }: any) => ({
// No border -- mockup cards float on the grey page background purely via shadow.
border: "none",
borderRadius: 16,
border: `1px solid ${theme.vars.palette.divider}`,
borderRadius: 8,
backgroundImage: "none",
boxShadow: theme.vars.customShadows.section,
}),
@@ -395,8 +387,8 @@ export const getTheme = (_mode: "light" | "dark") => {
styleOverrides: {
paper: ({ theme }: any) => ({
backgroundColor: theme.vars.palette.background.elevated,
border: "none",
borderRadius: 20,
border: `1px solid ${theme.vars.palette.divider}`,
borderRadius: 12,
boxShadow: theme.vars.customShadows.tooltip,
}),
},
@@ -438,7 +430,8 @@ export const getTheme = (_mode: "light" | "dark") => {
defaultProps: { disableFocusRipple: true },
styleOverrides: {
root: ({ theme }: any) => ({
borderRadius: 10,
borderRadius: 7,
minHeight: 44,
boxShadow: theme.vars.customShadows.button,
"&:hover": { boxShadow: theme.vars.customShadows.button },
}),
@@ -459,7 +452,7 @@ export const getTheme = (_mode: "light" | "dark") => {
background: theme.vars.palette.background.default,
paddingLeft: 10,
paddingRight: 10,
minHeight: 42,
minHeight: 44,
display: "flex",
alignItems: "center",
// Use the existing label notch: an outer outline crosses floating labels.