Voer de visuele audit door: fundament, donkere werkstand, kaart en schil

Achttien bevindingen uit de audit, met de metingen erbij in de betreffende
CSS-secties.

Fundament. Dertien tokens werden op 87 plaatsen aangeroepen zonder ooit
gedefinieerd te zijn; in de browser gaf getPropertyValue('--gi-text') een lege
string terug, dus die kleurregels vielen terug op overerving. Vorm en hoogte
zijn terug naar de schaal: radius via token van 17% naar 81%, unieke schaduwen
van 115 naar 71, losse kleurwaarden van 928 naar 352.

Typografie. De stylesheets declareerden negen gewichten terwijl er per familie
twee gezichten geladen werden, en font-synthesis-weight: none verbiedt
namaakvet. Alles van 650 tot 850 kwam dus als 600 op het scherm: 138 van de 148
declaraties rendereden identiek. Nu vier stappen die alle vier ook geladen
worden, en Manrope draagt de koppen zoals op de landingspagina.

Donkere werkstand. De werkbank staat tegen luchtbeelden aan; een lichte schil
daarnaast laat de kaart altijd verliezen. De landingspagina blijft licht.

Kaart. De ondergrond wordt ontkleurd en gedempt zodat alleen de eigen data nog
kleur draagt, en de symbologie staat niet langer als losse hexwaarden in
GeoMap. De zes zwevende vlakken zijn er drie: statuskaart, staprail en
resultaatchip bleken al één component te zijn die de opmaak over drie hoeken
van de kaart uiteentrok.

Schil. Eén kopbalk in plaats van drie (215px chroom naar 100px), het raster van
AI-vragen zonder botsingen, kapitalen op één niveau in plaats van 57
declaraties, en het leverancierslogo uit de navigatiebalk.

Van 47 echte !important naar 1, gemeten door telkens twee productiebuilds op
berekende stijlen te vergelijken. Contrast: nul tekstelementen onder AA op zes
werkbladen, in beide werkstanden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Jens
2026-08-23 14:59:45 +02:00
co-authored by Claude Opus 5
parent 4ecec721b1
commit 438d0ec083
15 changed files with 2167 additions and 1202 deletions
+12 -6
View File
@@ -33,21 +33,18 @@ interface LandingPageProps {
const capabilityItems = [
{
icon: MapPinned,
number: '01',
title: 'Eén kaartgerichte werkruimte',
description:
'Selecteer een gebied in België of de Belgische Noordzee en werk verder vanuit dezelfde ruimtelijke context.',
},
{
icon: Database,
number: '02',
title: 'Bronnen blijven herkenbaar',
description:
'Autoriteit, meetmoment, dekking, CRS en beperkingen blijven zichtbaar in plaats van achter één generieke kaartlaag te verdwijnen.',
},
{
icon: ShieldCheck,
number: '03',
title: 'Kwaliteit vóór resultaat',
description:
'Vergelijk referentie- en kandidaatgegevens, controleer bewijs en exporteer pas wanneer de context klopt.',
@@ -70,6 +67,7 @@ export function LandingPage({
const [password, setPassword] = useState('')
const [pendingAction, setPendingAction] = useState<'operator' | 'guest' | null>(null)
const [authError, setAuthError] = useState<string | null>(null)
const [attempted, setAttempted] = useState(false)
const [showPassword, setShowPassword] = useState(false)
const [menuOpen, setMenuOpen] = useState(false)
const usernameRef = useRef<HTMLInputElement | null>(null)
@@ -84,6 +82,7 @@ export function LandingPage({
const submitLogin = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
setPendingAction('operator')
setAttempted(true)
setAuthError(null)
try {
const session = await login(username.trim(), password)
@@ -98,6 +97,7 @@ export function LandingPage({
const submitGuestLogin = async () => {
setMenuOpen(false)
setPendingAction('guest')
setAttempted(true)
setAuthError(null)
if (typeof accessPanelRef.current?.scrollIntoView === 'function') {
accessPanelRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' })
@@ -278,7 +278,7 @@ export function LandingPage({
</button>
</div>
{authError || serviceError ? (
{authError || (serviceError && attempted) ? (
<p className="landing-login-error" role="alert">{authError ?? serviceError}</p>
) : null}
@@ -290,6 +290,13 @@ export function LandingPage({
<LogIn aria-hidden="true" />
{pendingAction === 'operator' ? 'Veilig aanmelden…' : 'Inloggen als operator'}
</button>
{/* De storingsmelding stond als rood blok midden in de hero,
voordat de bezoeker iets gedaan had. Zolang er nog niets
geprobeerd is, is het een mededeling en geen fout. */}
{serviceError && !attempted && !authError ? (
<p className="landing-login-note">{serviceError}</p>
) : null}
</form>
{guestAccessEnabled ? (
@@ -326,11 +333,10 @@ export function LandingPage({
</div>
<div className="landing-capability-grid">
{capabilityItems.map(({ icon: Icon, number, title, description }) => (
{capabilityItems.map(({ icon: Icon, title, description }) => (
<article key={title} className="landing-capability">
<div className="landing-capability-topline">
<span className="landing-capability-icon" aria-hidden="true"><Icon /></span>
<small>{number}</small>
</div>
<h3>{title}</h3>
<p>{description}</p>
@@ -172,28 +172,24 @@ export function DatasetPanel({
key: 'selected',
label: 'Geselecteerd',
count: selectedDatasetId ? 1 : 0,
hint: 'Actief',
className: 'dataset-role-selected',
},
{
key: 'reference',
label: 'Referentie',
count: primaryDatasets.filter((dataset) => normalizeDatasetRole(dataset) === 'reference').length,
hint: 'Officieel',
className: 'dataset-role-reference',
},
{
key: 'candidate',
label: 'Resultaat',
count: primaryDatasets.filter((dataset) => normalizeDatasetRole(dataset) === 'candidate').length,
hint: 'Afgeleid',
className: 'dataset-role-candidate',
},
{
key: 'source',
label: 'Basisbron',
count: primaryDatasets.filter((dataset) => normalizeDatasetRole(dataset) === 'source').length,
hint: 'Ingeladen',
className: 'dataset-role-source',
},
]
@@ -277,9 +273,8 @@ export function DatasetPanel({
<div className="dataset-role-summary-grid" aria-label="Dataset role summary">
{roleSummaries.map((summary) => (
<div className={`dataset-role-summary ${summary.className}`} key={summary.key}>
<span>{summary.label}</span>
<strong>{summary.count}</strong>
<small>{summary.hint}</small>
<span>{summary.label}</span>
</div>
))}
</div>
@@ -17,10 +17,10 @@ describe('AiPipelineIllustration', () => {
/>,
)
expect(screen.getByText('CUDA gereed')).toBeTruthy()
expect(screen.getByText('GPU gereed')).toBeTruthy()
expect(screen.getByRole('tab', { name: /Detecties/ }).textContent).toContain('volgende stap')
fireEvent.click(screen.getByRole('tab', { name: /NVIDIA GPU/ }))
expect(screen.getByRole('tabpanel').textContent).toContain('Lokale PyTorch-inferentie')
fireEvent.click(screen.getByRole('tab', { name: /Berekening/ }))
expect(screen.getByRole('tabpanel').textContent).toContain('De herkenning draait lokaal')
})
})
@@ -12,8 +12,8 @@ interface AiPipelineIllustrationProps {
const pipelineStages = [
{ key: 'imagery', label: 'Orthofoto', title: 'Gegeorefereerd bronbeeld', detail: 'CRS, resolutie en ruimtelijke dekking blijven bij de dataset bewaard.', icon: Image },
{ key: 'tiles', label: 'Beeldtegels', title: 'Controleerbare tilevoorbereiding', detail: 'Overlap en tile-identiteit houden detecties herleidbaar naar hun bronpixel.', icon: Boxes },
{ key: 'gpu', label: 'NVIDIA GPU', title: 'Lokale PyTorch-inferentie', detail: 'GeoIntel gebruikt de server-GPU en faalt gesloten wanneer CUDA vereist maar niet beschikbaar is.', icon: Cpu },
{ key: 'tiles', label: 'Beeldtegels', title: 'Beeld opgedeeld in controleerbare tegels', detail: 'Elke tegel houdt zijn overlap en herkomst bij, zodat elke detectie terug te voeren is op de bronpixel.', icon: Boxes },
{ key: 'gpu', label: 'Berekening', title: 'De herkenning draait lokaal', detail: 'De analyse gebruikt de GPU van de server. Is die niet beschikbaar, dan stopt de analyse in plaats van een resultaat te maken waarop u niet kunt bouwen.', icon: Cpu },
{ key: 'detections', label: 'Detecties', title: 'Gegeorefereerde gebouwobjecten', detail: 'Confidence, modelversie, brontegel en geometrie worden als reproduceerbaar resultaat bewaard.', icon: ScanSearch },
{ key: 'quality', label: 'QA-bewijs', title: 'Controle vóór vrijgave', detail: 'Precision, recall, IoU en foutbewijs bepalen of een resultaat alleen verkennend of operationeel bruikbaar is.', icon: BadgeCheck },
] as const
@@ -36,15 +36,15 @@ export function AiPipelineIllustration({
<div className="ai-pipeline-heading">
<div>
<p className="eyebrow">Van pixel naar bewijs</p>
<h3 id="ai-pipeline-title">PyTorch-keten op de NVIDIA-server</h3>
<h3 id="ai-pipeline-title">Van luchtbeeld naar controleerbare detectie</h3>
<p>Open een schakel om te zien welke technische context GeoIntel door de volledige analyse bewaart.</p>
</div>
<span className={gpuReady ? 'ai-pipeline-gpu ai-pipeline-gpu-ready' : 'ai-pipeline-gpu'}>
<i /> {gpuReady ? 'CUDA gereed' : 'CUDA controleren'}
<i /> {gpuReady ? 'GPU gereed' : 'GPU controleren'}
</span>
</div>
<div className="ai-pipeline-track" role="tablist" aria-label="PyTorch-analysekten">
<div className="ai-pipeline-track" role="tablist" aria-label="Stappen in de analyseketen">
<span className="ai-pipeline-flow" aria-hidden="true" />
{pipelineStages.map(({ key, label, icon: Icon }, index) => (
<button
@@ -275,8 +275,8 @@ export function ExportCenter({
})}
{availableLatestArtifacts.length === 0 ? (
<div className="result-state result-state-empty latest-download-empty">
<strong>Nog geen downloads gemaakt</strong>
<p>Kies hieronder een rapport of de actieve kaartlaag.</p>
<strong>Nog geen recente downloads</strong>
<p>De laatst bewaarde bestanden verschijnen hier.</p>
</div>
) : null}
</div>
@@ -377,8 +377,8 @@ export function ExportCenter({
) : null}
{exports.length === 0 ? (
<div className="result-state result-state-empty">
<strong>Nog geen downloads gemaakt.</strong>
<p>Kies hierboven een leesbaar rapport, projectoverzicht of kaartlaag.</p>
<strong>Nog niets bewaard</strong>
<p>Maak een leesbaar rapport, een projectoverzicht, of bewaar de actieve kaartlaag.</p>
</div>
) : null}
</div>
@@ -0,0 +1,91 @@
/**
* Kaartsymbologie.
*
* De kleuren van de kaartlagen stonden als losse hexwaarden door GeoMap.tsx
* heen. Het waren framework-standaardkleuren die bij geen enkel token uit het
* designsysteem hoorden, en op de donkere operationele ondergrond vielen de
* donkere varianten volledig weg.
*
* Deze waarden horen bij de donkere werkstand en zijn afgestemd op de tokens
* in geointel-system.css, sectie "Operationeel donker". Ze staan hier als
* letterlijke waarden omdat MapLibre paint-eigenschappen geen CSS-variabelen
* kunnen lezen; wijzigt een token, dan wijzigt zijn tegenhanger hier mee.
*/
export const mapSymbology = {
/** Officiële grenzen. Komt overeen met --gi-brand-500. */
boundary: '#45cfb4',
boundaryStrong: '#38c0a5',
/** Verandering tussen twee meetmomenten. */
added: '#4cc48d',
removed: '#ec7d76',
modified: '#dda45e',
unchanged: '#6fb0dd',
/** Eigen ingeladen data. Oranje blijft de tegenkleur van het teal-merk. */
dataFill: '#fb923c',
dataLine: '#f97316',
/** Actieve selectie. Geel leest op elke ondergrond. */
selectionFill: '#fde047',
selectionLine: '#fbbf24',
/** Water en bathymetrie. */
waterFill: '#38bdf8',
waterLine: '#7dd3fc',
/** Modelresultaten uit beeldanalyse. */
detectionFill: '#a78bfa',
detectionLine: '#c4b5fd',
/** Randen van puntsymbolen, tegen elke vulkleur. */
pointStroke: '#ffffff',
/** Terugvalkleur wanneer een feature geen bekende klasse heeft. */
fallback: '#94a3b8',
} as const
export type KaartWerkstand = 'dark' | 'light'
/**
* De verf van de ondergrond.
*
* De tegels zijn consumentenkaarten: rode snelwegen, groene bossen,
* POI-drukte. In beide werkstanden worden ze ontkleurd zodat alleen de eigen
* data nog kleur draagt; het verschil zit in hoe ver ze gedempt worden.
*
* De featurekleuren hierboven blijven in beide werkstanden gelijk. Het zijn
* middentonen die op een lichte én een donkere ondergrond leesbaar zijn, en
* één set houdt de betekenis van een kleur constant wanneer een gebruiker
* tussen de twee wisselt.
*/
export function basemapPaint(werkstand: KaartWerkstand): Record<string, number> {
if (werkstand === 'light') {
return {
'raster-saturation': -0.62,
'raster-brightness-min': 0.32,
'raster-brightness-max': 1,
'raster-contrast': -0.14,
'raster-hue-rotate': 140,
'raster-opacity': 0.92,
}
}
return {
'raster-saturation': -0.78,
'raster-brightness-min': 0.03,
'raster-brightness-max': 0.44,
'raster-contrast': -0.08,
'raster-hue-rotate': 140,
'raster-opacity': 0.9,
}
}
export function basemapGround(werkstand: KaartWerkstand): string {
return werkstand === 'light' ? '#eef2f0' : '#0a100e'
}
export function huidigeWerkstand(): KaartWerkstand {
if (typeof document === 'undefined') return 'dark'
return document.body.dataset.theme === 'light' ? 'light' : 'dark'
}
@@ -11,7 +11,6 @@ import {
} from 'lucide-react'
import type { WorkspaceKey } from '../overview/OverviewWorkspace'
import { GeoIntelMark } from '../brand/GeoIntelBrand'
import { ItWorxSignature } from '../brand/ItWorxSignature'
export interface WorkspaceNavigationItem {
key: WorkspaceKey
@@ -86,7 +85,6 @@ export function WorkbenchNavigation({
</div>
))}
</nav>
<ItWorxSignature />
</aside>
)
}