feat(ui): redesign GeoIntel as Atlas Workbench
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-19 14:24:54 +02:00
parent ea27873dce
commit 87345f1a8c
19 changed files with 1817 additions and 631 deletions
+8
View File
@@ -9,6 +9,14 @@
## 1.0.0 - Final Belgium and Belgian North Sea release (2026-07-19)
- Rebuilt the complete frontend presentation as the Stitch-guided GeoIntel
Atlas Workbench. The new shell uses one compact icon navigation rail, one
context bar and task-specific work surfaces across Map, Sources, AI
Questions, Quality, Image Analysis, Downloads, Status and Administration.
- Restored the normal desktop map to one stable theme/map/insight row, made
the map absorb ultrawide space, bounded the mobile theme list and removed
the duplicate workspace command bar. API, database, GIS and AI behavior are
unchanged.
- Promoted the nationally federated, map-first GeoIntel workbench from the
release candidate to the final `1.0.0` version.
- Added a restrained final visual polish pass for the map-first workbench:
@@ -14,7 +14,14 @@ def test_frontend_declares_mol_as_primary_operating_focus() -> None:
map_source = (ROOT / "frontend" / "src" / "components" / "GeoMap.tsx").read_text(
encoding="utf-8"
)
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
navigation = (
ROOT
/ "frontend"
/ "src"
/ "components"
/ "shell"
/ "WorkbenchNavigation.tsx"
).read_text(encoding="utf-8")
assert "PRIMARY_FOCUS_LABEL = 'Mol'" in focus
assert "PRIMARY_FOCUS_REGION = 'Mol, Kempen'" in focus
@@ -29,7 +36,8 @@ def test_frontend_declares_mol_as_primary_operating_focus() -> None:
assert "PRIMARY_FOCUS_AREA_GEOJSON" in project_hook
assert "4.35,51.28" not in project_hook
assert "center: PRIMARY_FOCUS_CENTER" in map_source
assert "GeoAI-werkruimte · gebiedsdata en analyse" in app
assert "GeoIntel" in navigation
assert "Atlas Workbench" in navigation
def test_operator_workflows_put_mol_first_and_name_future_projects() -> None:
@@ -15,8 +15,9 @@ def test_map_first_explorer_is_the_default_product_flow() -> None:
workspace = read("frontend/src/components/map/MapWorkspace.tsx")
assert "useState<WorkspaceKey>('map')" in app
assert "Wat bevindt zich in dit gebied?" in workspace
assert "Kies een datathema" in workspace
assert "Gebied analyseren" in workspace
assert "<h3>Thema</h3>" in workspace
assert "<h3>Inzichten</h3>" in workspace
assert "Teken rechthoek" in workspace
assert "Volledig werkgebied" in workspace
assert "Alle beschikbare thema" in workspace
@@ -24,7 +24,7 @@ def test_map_workspace_lives_in_task_based_workbench_shell() -> None:
assert "activeWorkspace === 'map'" in app
assert app.index("activeWorkspace === 'map'") < app.index("<MapWorkspace")
assert 'className="workbench-main"' in app
assert "'workbench-main workbench-main-map' : 'workbench-main'" in app
assert 'className="workbench-inspector"' in app
assert ".workbench-layout" in styles
assert ".workbench-sidebar" in styles
@@ -8,13 +8,22 @@ ROOT = Path(__file__).resolve().parents[2]
def test_app_uses_task_based_workbench_shell() -> None:
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
navigation = (
ROOT
/ "frontend"
/ "src"
/ "components"
/ "shell"
/ "WorkbenchNavigation.tsx"
).read_text(encoding="utf-8")
assert "type WorkspaceKey" in app
assert "workspaceNavItems" in app
assert 'data-testid={`workspace-nav-${item.key}`}' in app
assert "<WorkbenchNavigation" in app
assert 'data-testid={`workspace-nav-${item.key}`}' in navigation
assert 'className="workbench-layout"' in app
assert 'className="workbench-sidebar"' in app
assert 'className="workbench-main"' in app
assert 'className="workbench-sidebar"' in navigation
assert "'workbench-main workbench-main-map' : 'workbench-main'" in app
assert 'className="workbench-inspector"' in app
assert "activeWorkspace === 'data'" in app
assert "activeWorkspace === 'map'" in app
@@ -26,6 +35,9 @@ def test_app_uses_task_based_workbench_shell() -> None:
def test_workbench_shell_css_provides_navigation_main_and_inspector_regions() -> None:
css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8")
atlas = (ROOT / "frontend" / "src" / "styles" / "atlas-workbench.css").read_text(
encoding="utf-8"
)
assert ".workbench-topbar" in css
assert ".context-bar" in css
@@ -36,3 +48,6 @@ def test_workbench_shell_css_provides_navigation_main_and_inspector_regions() ->
assert ".workbench-inspector" in css
assert ".workspace-grid-data" in css
assert ".workspace-grid-ai" in css
assert ".workbench-stage" in atlas
assert ".context-health" in atlas
assert ".nav-item-icon" in atlas
@@ -6,17 +6,30 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_frontend_shell_has_visual_polish_contracts() -> None:
def test_frontend_shell_has_atlas_workbench_contracts() -> None:
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8")
css = (ROOT / "frontend" / "src" / "styles" / "atlas-workbench.css").read_text(
encoding="utf-8"
)
navigation = (
ROOT
/ "frontend"
/ "src"
/ "components"
/ "shell"
/ "WorkbenchNavigation.tsx"
).read_text(encoding="utf-8")
assert "workspace-command-bar" in app
assert "workspace-nav-cluster" in app
assert "--surface-raised" in css
assert ".workspace-command-bar" in css
assert "workspace-command-bar" not in app
assert "workspace-nav-cluster" not in app
assert "WorkbenchNavigation" in app
assert "lucide-react" in navigation
assert "--atlas-nav-width" in css
assert ".workbench-stage" in css
assert ".geo-explorer-layout" in css
assert "grid-template-columns: 15.75rem minmax(28rem, 1fr) 20rem;" in css
assert ".workspace-panel" in css
assert ".data-density-grid" in css
assert ".result-summary-card" in css
assert ".geo-assistant-panel" in css
def test_primary_panels_use_empty_state_components() -> None:
@@ -20,9 +20,18 @@ def test_global_focus_visible_contracts_are_defined() -> None:
def test_primary_navigation_has_keyboard_labels() -> None:
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
navigation = (
ROOT
/ "frontend"
/ "src"
/ "components"
/ "shell"
/ "WorkbenchNavigation.tsx"
).read_text(encoding="utf-8")
assert 'aria-label={`Open ${item.label}: ${item.description}`}' in app
assert 'aria-label={`Ga naar ${item.label}`}' in app
assert 'aria-label={`Open ${item.label}: ${item.description}`}' in navigation
assert "title={item.description}" in navigation
assert "aria-current={item.key === activeWorkspace ? 'page' : undefined}" in navigation
assert "label: 'Bronnen'" in app
assert "label: 'Kaart'" in app
assert "label: 'Kwaliteit'" in app
@@ -8,32 +8,45 @@ ROOT = Path(__file__).resolve().parents[2]
def test_workbench_shell_has_skip_link_and_main_focus_target() -> None:
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
navigation = (
ROOT
/ "frontend"
/ "src"
/ "components"
/ "shell"
/ "WorkbenchNavigation.tsx"
).read_text(encoding="utf-8")
assert 'className="skip-link"' in app
assert 'href="#workspace-main"' in app
assert 'nav aria-label="Hoofdonderdelen"' in app
assert '<nav aria-label="Hoofdonderdelen">' in navigation
assert 'id="workspace-main"' in app
assert "tabIndex={-1}" in app
def test_mobile_shell_density_contracts_are_defined() -> None:
css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8")
atlas = (ROOT / "frontend" / "src" / "styles" / "atlas-workbench.css").read_text(
encoding="utf-8"
)
assert ".skip-link" in css
assert ".skip-link:focus-visible" in css
assert "scroll-margin-top: 8.5rem;" in css
assert "grid-template-columns: repeat(4, minmax(6.75rem, 1fr));" in css
assert "overflow-x: auto;" in css
assert "scroll-snap-type: x proximity;" in css
assert "white-space: nowrap;" in css
assert "@media (max-width: 920px)" in atlas
assert "overflow-x: auto;" in atlas
assert "flex: 0 0 4.4rem;" in atlas
assert "white-space: nowrap;" in atlas
assert ".context-bar,\n .quick-action-grid,\n .workspace-nav-cluster" not in css
def test_small_screen_navigation_keeps_labels_readable() -> None:
css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8")
css = (ROOT / "frontend" / "src" / "styles" / "atlas-workbench.css").read_text(
encoding="utf-8"
)
assert "@media (max-width: 620px)" in css
assert "@media (max-width: 560px)" in css
assert ".workbench-sidebar .nav-item" in css
assert "min-width: 9.5rem;" in css
assert ".nav-item small" in css
assert "line-height: 1.18;" in css
assert "min-width: 4.4rem;" in css
assert ".nav-item-icon" in css
assert "text-overflow: ellipsis;" in css
+42
View File
@@ -10819,3 +10819,45 @@ Decision:
- RC-11 is complete subject only to binding the accepted final commit and
immutable image in the signed/checksummed package. No RC-12 phase is used.
## 2026-07-19 - Stitch-guided complete Atlas Workbench redesign
Implemented:
- Created the private Stitch project `GeoIntel Complete Workbench Redesign`
and the `GeoIntel Atlas Workbench` design system.
- Generated production references for the Map Explorer, Sources catalog and
local AI Questions workspaces.
- Rebuilt the application shell so the brand lives in one compact navigation
rail and the active work context lives in one 56-pixel top bar.
- Added Lucide icons for the eight workspace destinations and removed the
duplicate command navigation.
- Restyled Map, Sources, AI Questions, Quality, Image Analysis, Downloads,
Status and Administration under one restrained token system.
- Restored the desktop map to a theme/map/insight row, allowed the map to
absorb ultrawide width and bounded the mobile theme list.
- Replaced the old `visual-polish.css` layer with the documented
`atlas-workbench.css` implementation.
Validated during implementation:
- the complete readiness gate passed: 1,052 backend tests, 22 frontend tests,
frontend TypeScript typecheck, the production build and Alembic head
`202607160001`;
- the browser UX audit passed at 390x844, 1366x768 and 2560x1080, including
accessible control names, loading states, keyboard tabs, the advanced
coverage flow, zero horizontal overflow, zero console errors and zero failed
API requests;
- all seven Belgium/North Sea release journeys passed against the live backend
through the redesigned frontend;
- browser review passed at 1280x720, 3440x1200 and 390x844 with no horizontal
document overflow or clipped button/select text;
- Map, Sources, AI Questions, Quality, Image Analysis, Downloads, Status and
Administration were each reviewed against live backend data.
Behavior preservation:
- No API contract, migration, database model, provider behavior, GIS
calculation, model runtime or persistence flow changed.
- Existing map selection, source loading, local Ollama, QA, analysis and
download actions remain wired through their original hooks and services.
+3
View File
@@ -99,6 +99,9 @@ geen open productroadmap meer.
- [x] Maak de vijf bestaande officiële beleidsrasters veilig op aanvraag
beschikbaar voor elke begrensde selectie in Vlaanderen, met Dataset-cache,
semantische metrics en zonder 1.425 vooraf geladen rasters.
- [x] Vervang de gegroeide dashboardpresentatie door de Stitch-gestuurde Atlas
Workbench met een compacte navigatierail, vaste contextbalk, taakgerichte
schermen en gevalideerde desktop-, ultrawide- en mobiele layouts.
Bewuste, niet-blokkerende grenzen:
+91
View File
@@ -0,0 +1,91 @@
# GeoIntel Atlas Workbench
## Purpose
The Atlas Workbench is the visual and interaction system for GeoIntel. It
keeps the product map-first, task-oriented and understandable without hiding
source authority, observation time or GIS limitations.
The redesign was developed in Stitch under:
- project: `GeoIntel Complete Workbench Redesign`
- project id: `18332842433441398219`
- design system: `GeoIntel Atlas Workbench`
- design-system asset: `4667932515738184526`
- Map Explorer screen: `c24113d1f1474c91b332b2365be24993`
- Sources screen: `239796f2d66a44e7983e5fc588305ff8`
- AI Questions screen: `246aa57bbe974861885325ff9d10c6bd`
The React implementation remains the source of truth for behavior. Stitch is
the design reference for hierarchy, spacing and component treatment.
## Product hierarchy
1. `Kaart` is the primary workspace.
2. `Bronnen` explains available and missing official data.
3. `AI-vragen` answers only from persisted area evidence.
4. `Kwaliteit` reviews stored checks and provenance.
5. `Beeldanalyse` contains detection and segmentation workflows.
6. `Downloads` stores and previews traceable outputs.
7. `Status` summarizes readiness and source freshness.
8. `Beheer` exposes provider and technical administration.
The navigation rail is the only persistent workspace navigation. Duplicate
command bars or competing shortcut rows must not be reintroduced.
## Layout rules
- Desktop uses an 88-pixel navigation rail and a 56-pixel context bar.
- The normal map layout is `theme drawer / map / insight inspector`.
- Theme and insight rails stay stable on ultrawide displays; the map receives
the additional width.
- Non-map workspaces use one task header and one primary work surface.
- Panels scroll internally when their function requires stable surrounding
context.
- Mobile navigation is horizontally scrollable. The theme list is bounded so
the map remains reachable early in the page flow.
- Typography never scales with viewport width.
## Visual tokens
| Role | Value |
| --- | --- |
| Canvas | `#f3f6f5` |
| Surface | `#ffffff` |
| Ink | `#152522` |
| Muted text | `#687773` |
| Border | `#dce4e1` |
| Primary teal | `#0b6b62` |
| Context blue | `#315d73` |
| Attention amber | `#a55c23` |
| Error red | `#a33c39` |
| Radius | `4px` |
The UI does not use gradients, decorative blobs, oversized hero sections,
nested cards or pill-heavy status presentation.
## Interaction rules
- One primary action per task surface.
- Use icons plus short labels for navigation and familiar commands.
- `Laatste toestand / Evolutie` is a segmented control.
- Rectangle drawing is the primary spatial-selection action.
- Technical model, provider and provenance detail stays available through
explicit disclosures.
- Empty states state the next useful action in one short sentence.
- Unavailable data or model states remain honest and are never replaced with
fabricated output.
## Implementation boundaries
- Shared shell and navigation:
`frontend/src/components/shell/WorkbenchNavigation.tsx`
- Shared visual implementation:
`frontend/src/styles/atlas-workbench.css`
- Map workflow:
`frontend/src/components/map/MapWorkspace.tsx`
- API, persistence and GIS behavior remain outside the design system.
Any future visual change must retain the browser acceptance widths of
390 pixels, 1280 pixels and 3440 pixels and must pass frontend typecheck,
unit tests, production build and the UX browser audit.
+13 -5
View File
@@ -113,11 +113,19 @@ The workbench uses a task-based shell instead of a single long panel stack. `App
The premium V1 presentation layer lives in `src/styles/premium.css`. It groups navigation by Workspace, Analyze and Deliver, removes the permanent inspector column, gives desktop/ultrawide workspaces stable readable widths and switches narrow screens to full-width content with a horizontally scrollable navigation rail. API calls and workflow state remain owned by the existing hooks.
The final visual polish layer lives in `src/styles/visual-polish.css`. It keeps
the map dominant, places the results rail beside the map when the viewport is
wide enough, preserves a readable two-column map flow on smaller desktops and
stacks the context header and navigation explicitly on mobile. It contains no
workflow or API logic.
The Stitch-guided Atlas Workbench redesign lives in
`src/styles/atlas-workbench.css`, with the shared icon navigation in
`components/shell/WorkbenchNavigation.tsx`. The application uses one compact
navigation rail, one context bar and task-specific work surfaces. The primary
map keeps the theme rail, map and insight rail in one row on normal desktop
and gives additional ultrawide space to the map instead of stretching text
panels. Mobile keeps all workspaces in a horizontally scrollable rail and
bounds the theme list so the map remains reachable early in the flow.
The visual contract, Stitch project and screen references are documented in
`docs/UI_DESIGN_SYSTEM.md`. The redesign changes presentation and information
hierarchy only; API, persistence and GIS behavior remain owned by the existing
hooks and services.
## Current component boundaries
+10
View File
@@ -8,6 +8,7 @@
"name": "geointel-frontend",
"version": "1.0.0",
"dependencies": {
"lucide-react": "^1.25.0",
"maplibre-gl": "^4.7.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
@@ -2415,6 +2416,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "1.25.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.25.0.tgz",
"integrity": "sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+1
View File
@@ -16,6 +16,7 @@
"node": "^20.19.0 || >=22.12.0"
},
"dependencies": {
"lucide-react": "^1.25.0",
"maplibre-gl": "^4.7.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
+48 -78
View File
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import './styles/app.css'
import './styles/premium.css'
import './styles/visual-polish.css'
import './styles/atlas-workbench.css'
import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel'
import { GeoAssistantPanel } from './components/assistant/GeoAssistantPanel'
import { DatasetPanel } from './components/datasets/DatasetPanel'
@@ -18,6 +18,11 @@ import { QualityResultsPanel } from './components/quality/QualityResultsPanel'
import type { DatasetCreateResponse } from './types'
import { ProviderPanel } from './components/providers/ProviderPanel'
import { SegmentationLab } from './components/segmentation/SegmentationLab'
import {
WorkbenchNavigation,
type WorkspaceNavigationGroup,
type WorkspaceNavigationItem,
} from './components/shell/WorkbenchNavigation'
import { useChangeDetectionWorkflow } from './hooks/useChangeDetectionWorkflow'
import { useDemoWorkflow } from './hooks/useDemoWorkflow'
import { useCoverageResolver } from './hooks/useCoverageResolver'
@@ -51,7 +56,7 @@ function isVectorDatasetType(datasetType: string): boolean {
return datasetType === 'vector' || datasetType === 'geojson'
}
const workspaceNavItems: Array<{ key: WorkspaceKey; label: string; description: string }> = [
const workspaceNavItems: WorkspaceNavigationItem[] = [
{ key: 'overview', label: 'Status', description: 'Beschikbaarheid en aandachtspunten' },
{ key: 'data', label: 'Bronnen', description: 'Gebieden en ingeladen gegevens' },
{ key: 'map', label: 'Kaart', description: 'Selecteren, uitlezen en vergelijken' },
@@ -62,7 +67,7 @@ const workspaceNavItems: Array<{ key: WorkspaceKey; label: string; description:
{ key: 'system', label: 'Beheer', description: 'Databronnen en systeemstatus' },
]
const workspaceNavGroups: Array<{ label: string; keys: WorkspaceKey[] }> = [
const workspaceNavGroups: WorkspaceNavigationGroup[] = [
{ label: 'Verkennen', keys: ['map', 'data'] },
{ label: 'Vragen', keys: ['assistant'] },
{ label: 'Analyseren', keys: ['analysis', 'ai'] },
@@ -779,71 +784,52 @@ function App(): JSX.Element {
>
Ga naar de werkruimte
</a>
<header className="workbench-topbar">
<div className="brand-block">
<span className="brand-mark" aria-hidden="true">GI</span>
<div>
<h1>GeoIntel</h1>
<p>GeoAI-werkruimte · gebiedsdata en analyse</p>
</div>
</div>
<div className="context-bar" aria-label="Actieve werkcontext">
<div>
<span>Werkruimte</span>
<strong title={projectContextLabel}>{projectContextLabel}</strong>
</div>
<div>
<span>Gebied</span>
<strong title={areaContextLabel}>{areaContextLabel}</strong>
</div>
<div>
<span>Bron</span>
<strong title={datasetContextLabel}>{datasetContextLabel}</strong>
</div>
<div>
<span>Kaartlaag</span>
<strong title={layerContextLabel}>{layerContextLabel}</strong>
</div>
</div>
</header>
<p className="sr-only" role="status" aria-live="polite">{workspaceStatusMessage}</p>
{errorMessage ? <p className="error" role="alert">{errorMessage}</p> : null}
<div className="workbench-layout">
<aside className="workbench-sidebar" aria-label="Navigatie van de werkruimte">
<nav aria-label="Hoofdonderdelen">
{workspaceNavGroups.map((group) => (
<div className="nav-group" key={group.label}>
<p className="nav-section-label">{group.label}</p>
{group.keys.map((key) => {
const item = workspaceNavItems.find((candidate) => candidate.key === key)
if (!item) {
return null
}
return (
<button
key={item.key}
type="button"
className={item.key === activeWorkspace ? 'nav-item nav-item-active' : 'nav-item'}
onClick={() => setActiveWorkspace(item.key)}
aria-current={item.key === activeWorkspace ? 'page' : undefined}
aria-label={`Open ${item.label}: ${item.description}`}
data-testid={`workspace-nav-${item.key}`}
>
<span>{item.label}</span>
<small>{item.description}</small>
</button>
)
})}
<WorkbenchNavigation
activeWorkspace={activeWorkspace}
groups={workspaceNavGroups}
items={workspaceNavItems}
onSelect={setActiveWorkspace}
/>
<div className="workbench-stage">
<header className="workbench-topbar">
<div className="mobile-brand">
<span className="brand-mark" aria-hidden="true">GI</span>
<strong>GeoIntel</strong>
</div>
<div className="context-bar" aria-label="Actieve werkcontext">
<div>
<span>Werkruimte</span>
<strong title={projectContextLabel}>{projectContextLabel}</strong>
</div>
))}
</nav>
</aside>
<div>
<span>Gebied</span>
<strong title={areaContextLabel}>{areaContextLabel}</strong>
</div>
<div>
<span>Bron</span>
<strong title={datasetContextLabel}>{datasetContextLabel}</strong>
</div>
<div>
<span>Kaartlaag</span>
<strong title={layerContextLabel}>{layerContextLabel}</strong>
</div>
</div>
<div className="context-health" aria-label="Systeemstatus">
<span aria-hidden="true" />
<strong>{workspaceDataLoading ? 'Laden' : errorMessage ? 'Aandacht' : 'Gereed'}</strong>
</div>
</header>
<div className="workbench-content">
<main
ref={workbenchMainRef}
className="workbench-main"
className={activeWorkspace === 'map' ? 'workbench-main workbench-main-map' : 'workbench-main'}
id="workspace-main"
tabIndex={-1}
aria-busy={workspaceDataLoading}
@@ -868,24 +854,6 @@ function App(): JSX.Element {
) : null}
</div>
</div> : null}
<div className="workspace-command-bar" aria-label="Snelkoppelingen">
<div className="workspace-nav-cluster">
{workspaceNavItems.slice(0, 5).map((item) => (
<button
key={item.key}
type="button"
className={item.key === activeWorkspace ? 'command-chip command-chip-active' : 'command-chip'}
onClick={() => setActiveWorkspace(item.key)}
aria-pressed={item.key === activeWorkspace}
aria-label={`Ga naar ${item.label}`}
>
{item.label}
</button>
))}
</div>
<span>{selectedProject ? `Actief bereik: ${projectContextLabel}` : 'De regionale gegevens worden ingeladen.'}</span>
</div>
{activeWorkspace === 'overview' ? (
<OverviewWorkspace
selectedProject={selectedProject}
@@ -1330,6 +1298,8 @@ function App(): JSX.Element {
/>
</aside>
) : null}
</div>
</div>
</div>
</div>
)
+5 -5
View File
@@ -1868,8 +1868,8 @@ export function MapWorkspace({
<header className="geo-explorer-header">
<div>
<p className="eyebrow">{activeScopeLabel} · geografische verkenner</p>
<h2>Wat bevindt zich in dit gebied?</h2>
<p>Kies een datathema, teken een rechthoek en lees de beschikbare gegevens meteen uit.</p>
<h2>Gebied analyseren</h2>
<p>Kies een thema, teken een rechthoek en lees de beschikbare gegevens.</p>
</div>
<div className="geo-analysis-mode" role="tablist" aria-label="Analyseperiode">
<button
@@ -1932,8 +1932,8 @@ export function MapWorkspace({
<div className="geo-panel-heading">
<span>1</span>
<div>
<h3>Kies een datathema</h3>
<p>Dit zijn databronnen, geen AI-modellen.</p>
<h3>Thema</h3>
<p>Kies welke gegevens u wilt meten.</p>
</div>
</div>
<div className="geo-loaded-scope" aria-label="Ingeladen regiobereik">
@@ -2307,7 +2307,7 @@ export function MapWorkspace({
<div className="geo-panel-heading">
<span>3</span>
<div>
<h3>Resultaten</h3>
<h3>Inzichten</h3>
<p>Alleen gemeten gegevens uit beschikbare bronnen.</p>
</div>
</div>
@@ -0,0 +1,90 @@
import {
Activity,
Bot,
Database,
Download,
Map,
ScanSearch,
Settings,
ShieldCheck,
type LucideIcon,
} from 'lucide-react'
import type { WorkspaceKey } from '../overview/OverviewWorkspace'
export interface WorkspaceNavigationItem {
key: WorkspaceKey
label: string
description: string
}
export interface WorkspaceNavigationGroup {
label: string
keys: WorkspaceKey[]
}
interface WorkbenchNavigationProps {
activeWorkspace: WorkspaceKey
groups: WorkspaceNavigationGroup[]
items: WorkspaceNavigationItem[]
onSelect: (workspace: WorkspaceKey) => void
}
const workspaceIcons: Record<WorkspaceKey, LucideIcon> = {
map: Map,
data: Database,
assistant: Bot,
analysis: ShieldCheck,
ai: ScanSearch,
exports: Download,
overview: Activity,
system: Settings,
}
export function WorkbenchNavigation({
activeWorkspace,
groups,
items,
onSelect,
}: WorkbenchNavigationProps): JSX.Element {
return (
<aside className="workbench-sidebar" aria-label="Navigatie van de werkruimte">
<div className="sidebar-brand" aria-label="GeoIntel">
<span className="brand-mark" aria-hidden="true">GI</span>
<span className="sidebar-brand-copy">
<strong>GeoIntel</strong>
<small>Atlas Workbench</small>
</span>
</div>
<nav aria-label="Hoofdonderdelen">
{groups.map((group) => (
<div className="nav-group" key={group.label}>
<p className="nav-section-label">{group.label}</p>
{group.keys.map((key) => {
const item = items.find((candidate) => candidate.key === key)
if (!item) {
return null
}
const Icon = workspaceIcons[item.key]
return (
<button
key={item.key}
type="button"
className={item.key === activeWorkspace ? 'nav-item nav-item-active' : 'nav-item'}
onClick={() => onSelect(item.key)}
aria-current={item.key === activeWorkspace ? 'page' : undefined}
aria-label={`Open ${item.label}: ${item.description}`}
title={item.description}
data-testid={`workspace-nav-${item.key}`}
>
<Icon className="nav-item-icon" aria-hidden="true" strokeWidth={1.8} />
<span>{item.label}</span>
</button>
)
})}
</div>
))}
</nav>
</aside>
)
}
File diff suppressed because it is too large Load Diff
-515
View File
@@ -1,515 +0,0 @@
/* GeoIntel visual polish: a restrained map-first presentation layer. */
:root {
--visual-canvas: #eef2f1;
--visual-surface: #ffffff;
--visual-surface-soft: #f7f9f8;
--visual-ink: #172a2d;
--visual-muted: #657477;
--visual-line: #d7e1df;
--visual-line-strong: #b8c8c4;
--visual-teal: #087d73;
--visual-teal-deep: #056057;
--visual-teal-soft: #e7f4f1;
--visual-amber: #a76616;
--visual-amber-soft: #fff7e9;
}
.workbench-shell {
background: var(--visual-canvas);
color: var(--visual-ink);
}
.app-shell > header.workbench-topbar {
min-height: 4.35rem;
grid-template-columns: minmax(14rem, 17rem) minmax(0, 1fr);
gap: 1.15rem;
padding-inline: clamp(1rem, 1.5vw, 1.75rem);
background: rgba(255, 255, 255, 0.985);
}
.brand-block {
gap: 0.8rem;
}
.brand-mark {
width: 2.35rem;
height: 2.35rem;
flex-basis: 2.35rem;
border: 1px solid #20464a;
border-radius: 8px;
background: #153b40;
box-shadow: 0 2px 5px rgba(21, 59, 64, 0.14);
}
.brand-block h1 {
color: var(--visual-ink);
font-size: 1.08rem;
letter-spacing: 0;
}
.app-shell > header.workbench-topbar .brand-block p {
color: #718083;
font-size: 0.71rem;
}
.context-bar {
border-color: var(--visual-line);
border-radius: 8px;
background: var(--visual-surface-soft);
}
.context-bar > div {
min-height: 2.8rem;
padding-inline: 0.8rem;
}
.context-bar span {
color: #708082;
font-size: 0.6rem;
letter-spacing: 0.03em;
}
.context-bar strong {
color: var(--visual-ink);
font-size: 0.79rem;
}
.workbench-layout {
grid-template-columns: 13.25rem minmax(0, 1fr);
height: calc(100dvh - 4.35rem);
}
.workbench-sidebar {
padding: 1rem 0.8rem;
border-right-color: var(--visual-line);
background: #f6f8f7;
}
.workbench-sidebar nav {
gap: 1.15rem;
}
.nav-group {
gap: 0.28rem;
}
.nav-section-label {
padding-inline: 0.62rem;
color: #7c898a;
font-size: 0.6rem;
letter-spacing: 0.07em;
}
.workbench-sidebar .nav-item {
min-height: 2.55rem;
border-radius: 7px;
padding: 0.48rem 0.7rem;
color: #3e5153;
}
.workbench-sidebar .nav-item span {
font-size: 0.82rem;
font-weight: 750;
}
.workbench-sidebar .nav-item-active {
border-color: #b8d9d2;
background: var(--visual-teal-soft);
color: var(--visual-teal-deep);
box-shadow: inset 3px 0 0 var(--visual-teal);
}
.workbench-main {
padding: 1.25rem clamp(1rem, 1.8vw, 2rem) 2rem;
}
.workbench-main > * {
width: min(100%, 118rem);
}
.workspace-heading {
margin-bottom: 1rem;
padding-bottom: 0.82rem;
border-bottom-color: var(--visual-line);
}
.workspace-heading h2 {
color: var(--visual-ink);
font-size: 1.48rem;
}
.workspace-heading-actions > p {
color: var(--visual-muted);
}
/* Normal desktop keeps the three map steps in one purposeful working row. */
@media (min-width: 1181px) {
.geo-explorer-layout {
grid-template-columns: minmax(15rem, 16.5rem) minmax(0, 1fr) minmax(17rem, 19rem);
gap: 0.85rem;
height: clamp(36rem, calc(100dvh - 9.8rem), 68rem);
min-height: 36rem;
}
.geo-theme-panel,
.geo-results-panel,
.geo-map-stage {
border-color: var(--visual-line);
border-radius: 8px;
box-shadow: 0 1px 2px rgba(23, 42, 45, 0.035);
}
.geo-results-panel {
grid-column: auto;
display: flex;
overflow-y: auto;
}
.geo-map-toolbar {
align-items: flex-start;
}
.geo-map-actions {
flex-wrap: wrap;
justify-content: flex-end;
}
}
@media (min-width: 1800px) {
.workbench-layout {
grid-template-columns: 14rem minmax(0, 1fr);
}
.workbench-main {
padding-inline: clamp(1.5rem, 2.2vw, 3rem);
}
.geo-explorer-layout {
grid-template-columns: minmax(17rem, 19rem) minmax(42rem, 1fr) minmax(21rem, 24rem);
}
}
.geo-explorer {
gap: 0.9rem;
}
.geo-explorer-header {
gap: 1.25rem;
border-bottom-color: var(--visual-line);
padding-bottom: 0.95rem;
}
.geo-explorer-header h2 {
color: var(--visual-ink);
font-size: clamp(1.45rem, 1.8vw, 1.9rem);
font-weight: 760;
}
.geo-explorer-header p:last-child {
color: var(--visual-muted);
font-size: 0.86rem;
}
.geo-analysis-mode {
border-color: var(--visual-line-strong);
background: #f3f7f6;
}
.geo-analysis-mode button {
color: #687879;
}
.geo-analysis-mode button.active {
color: var(--visual-teal-deep);
box-shadow: 0 1px 4px rgba(23, 42, 45, 0.11);
}
.geo-panel-heading > span {
background: var(--visual-ink);
}
.geo-panel-heading h3,
.geo-results-title-row h4 {
color: var(--visual-ink);
}
.geo-panel-heading p,
.geo-results-empty p {
color: var(--visual-muted);
}
.geo-theme-panel,
.geo-results-panel,
.geo-map-stage {
background: var(--visual-surface);
}
.geo-theme-panel,
.geo-results-panel {
padding: 0.85rem;
}
.geo-theme-list {
gap: 0.42rem;
scrollbar-gutter: stable;
}
.geo-theme-option {
min-height: 3.35rem;
border-color: #dce6e3;
border-radius: 7px;
padding: 0.52rem 0.56rem;
}
.geo-theme-option:not(:disabled):hover {
border-color: #98c5ba;
background: #f5faf8;
}
.geo-theme-option-active {
border-color: #6da99b;
background: var(--visual-teal-soft);
box-shadow: inset 3px 0 0 var(--visual-teal);
}
.geo-theme-option strong {
color: #233639;
font-size: 0.8rem;
}
.geo-theme-option small {
color: #6e7d7f;
font-size: 0.66rem;
}
.geo-theme-option i {
color: var(--visual-teal-deep);
font-size: 0.57rem;
letter-spacing: 0.02em;
}
.geo-loaded-scope {
border-color: #d6e5e1;
border-radius: 7px;
background: #f6faf9;
}
.geo-source-summary {
border-top-color: var(--visual-line);
}
.geo-source-summary strong {
color: #223639;
}
.geo-map-stage {
border-color: var(--visual-line-strong);
box-shadow: 0 3px 14px rgba(23, 42, 45, 0.08);
}
.geo-map-toolbar {
border-bottom-color: var(--visual-line);
background: #fbfcfc;
}
.geo-map-actions .primary-action,
.geo-result-next-actions .primary-action {
border-color: var(--visual-teal);
background: var(--visual-teal);
}
.geo-map-actions .primary-action:hover:not(:disabled),
.geo-result-next-actions .primary-action:hover:not(:disabled) {
border-color: var(--visual-teal-deep);
background: var(--visual-teal-deep);
}
.geo-map-canvas {
background: #dce9e6;
}
.geo-map-legend {
border-color: rgba(23, 42, 45, 0.18);
border-radius: 6px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 3px 12px rgba(23, 42, 45, 0.12);
}
.geo-results-panel {
scrollbar-gutter: stable;
}
.geo-results-empty,
.geo-results-loading {
border-color: #cbdad6;
background: #fafcfb;
}
.geo-primary-metrics > div {
border-color: #dbe6e3;
border-radius: 6px;
background: #f7faf9;
}
.geo-primary-metrics > div:first-child {
border-top: 2px solid var(--visual-teal);
}
.geo-primary-metrics strong {
color: #183c3b;
}
.geo-theme-result-row {
border-color: #e0e9e7;
background: #fbfcfc;
}
.geo-theme-result-row:hover {
border-color: #b9d4cc;
background: #f4faf8;
}
.geo-result-next-actions {
border-color: #c4ddd7;
border-radius: 7px;
background: #f2faf7;
}
.geo-result-next-actions strong {
color: #174a42;
}
.geo-result-next-actions small {
color: #607773;
}
@media (max-width: 1180px) and (min-width: 921px) {
.geo-explorer-layout {
grid-template-columns: minmax(15rem, 17rem) minmax(0, 1fr);
grid-template-rows: auto auto;
height: auto;
min-height: 0;
}
.geo-results-panel {
grid-column: 1 / -1;
display: grid;
grid-template-columns: minmax(13rem, 0.7fr) minmax(0, 1.3fr);
align-items: start;
}
}
@media (max-width: 1359px) and (min-width: 921px) {
/* Keep the map readable until the viewport can support a result rail. */
.geo-explorer-layout {
grid-template-columns: minmax(15rem, 17rem) minmax(28rem, 1fr);
grid-template-rows: clamp(34rem, calc(100dvh - 10rem), 54rem) auto;
height: auto;
min-height: 0;
}
.geo-results-panel {
grid-column: 1 / -1;
display: grid;
grid-template-columns: minmax(13rem, 0.7fr) minmax(0, 1.3fr);
align-items: start;
}
}
@media (max-width: 1799px) and (min-width: 1360px) {
/* A two-column work row keeps the selection step wide at standard desktop sizes. */
.geo-explorer-layout {
grid-template-columns: minmax(16rem, 18rem) minmax(34rem, 1fr);
grid-template-rows: clamp(36rem, calc(100dvh - 10rem), 58rem) auto;
height: auto;
min-height: 0;
}
.geo-results-panel {
grid-column: 1 / -1;
display: grid;
grid-template-columns: minmax(14rem, 0.7fr) minmax(0, 1.3fr);
align-items: start;
overflow: visible;
}
}
@media (max-width: 920px) {
.app-shell > header.workbench-topbar {
position: relative;
display: grid;
grid-template-columns: 1fr;
gap: 0.65rem;
min-height: auto;
}
.app-shell > header.workbench-topbar {
padding-block: 0.7rem;
}
.workbench-sidebar {
background: #ffffff;
scrollbar-color: #a9bfba transparent;
scrollbar-width: thin;
}
.workbench-sidebar::-webkit-scrollbar {
height: 4px;
}
.workbench-sidebar::-webkit-scrollbar-thumb {
border-radius: 4px;
background: #a9bfba;
}
.workbench-sidebar::-webkit-scrollbar-track {
background: transparent;
}
.workbench-sidebar .nav-item {
border-color: transparent;
}
.workbench-sidebar .nav-item-active {
border-color: #b8d9d2;
box-shadow: inset 0 -3px 0 var(--visual-teal);
}
.geo-explorer-layout {
gap: 0.75rem;
}
}
@media (max-width: 620px) {
.workbench-main {
padding-inline: 0.75rem;
}
.geo-explorer-header {
gap: 0.8rem;
}
.geo-explorer-header h2 {
font-size: 1.4rem;
}
.geo-theme-panel,
.geo-results-panel {
padding: 0.75rem;
}
.geo-map-stage {
box-shadow: none;
}
.geo-map-canvas .map-container {
min-height: 25rem;
}
}
@media (prefers-reduced-motion: reduce) {
.workbench-shell button {
transition: none;
}
}