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) ## 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 - Promoted the nationally federated, map-first GeoIntel workbench from the
release candidate to the final `1.0.0` version. release candidate to the final `1.0.0` version.
- Added a restrained final visual polish pass for the map-first workbench: - 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( map_source = (ROOT / "frontend" / "src" / "components" / "GeoMap.tsx").read_text(
encoding="utf-8" 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_LABEL = 'Mol'" in focus
assert "PRIMARY_FOCUS_REGION = 'Mol, Kempen'" 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 "PRIMARY_FOCUS_AREA_GEOJSON" in project_hook
assert "4.35,51.28" not in project_hook assert "4.35,51.28" not in project_hook
assert "center: PRIMARY_FOCUS_CENTER" in map_source 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: 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") workspace = read("frontend/src/components/map/MapWorkspace.tsx")
assert "useState<WorkspaceKey>('map')" in app assert "useState<WorkspaceKey>('map')" in app
assert "Wat bevindt zich in dit gebied?" in workspace assert "Gebied analyseren" in workspace
assert "Kies een datathema" in workspace assert "<h3>Thema</h3>" in workspace
assert "<h3>Inzichten</h3>" in workspace
assert "Teken rechthoek" in workspace assert "Teken rechthoek" in workspace
assert "Volledig werkgebied" in workspace assert "Volledig werkgebied" in workspace
assert "Alle beschikbare thema" 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 "activeWorkspace === 'map'" in app
assert app.index("activeWorkspace === 'map'") < app.index("<MapWorkspace") 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 'className="workbench-inspector"' in app
assert ".workbench-layout" in styles assert ".workbench-layout" in styles
assert ".workbench-sidebar" 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: def test_app_uses_task_based_workbench_shell() -> None:
app = (ROOT / "frontend" / "src" / "App.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 "type WorkspaceKey" in app assert "type WorkspaceKey" in app
assert "workspaceNavItems" 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-layout"' in app
assert 'className="workbench-sidebar"' in app assert 'className="workbench-sidebar"' in navigation
assert 'className="workbench-main"' in app assert "'workbench-main workbench-main-map' : 'workbench-main'" in app
assert 'className="workbench-inspector"' in app assert 'className="workbench-inspector"' in app
assert "activeWorkspace === 'data'" in app assert "activeWorkspace === 'data'" in app
assert "activeWorkspace === 'map'" 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: def test_workbench_shell_css_provides_navigation_main_and_inspector_regions() -> None:
css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8") 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 ".workbench-topbar" in css
assert ".context-bar" 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 ".workbench-inspector" in css
assert ".workspace-grid-data" in css assert ".workspace-grid-data" in css
assert ".workspace-grid-ai" 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] 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") 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-command-bar" not in app
assert "workspace-nav-cluster" in app assert "workspace-nav-cluster" not in app
assert "--surface-raised" in css assert "WorkbenchNavigation" in app
assert ".workspace-command-bar" in css 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 ".workspace-panel" in css
assert ".data-density-grid" in css assert ".geo-assistant-panel" in css
assert ".result-summary-card" in css
def test_primary_panels_use_empty_state_components() -> None: 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: def test_primary_navigation_has_keyboard_labels() -> None:
app = (ROOT / "frontend" / "src" / "App.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 'aria-label={`Open ${item.label}: ${item.description}`}' in app assert 'aria-label={`Open ${item.label}: ${item.description}`}' in navigation
assert 'aria-label={`Ga naar ${item.label}`}' in app assert "title={item.description}" in navigation
assert "aria-current={item.key === activeWorkspace ? 'page' : undefined}" in navigation
assert "label: 'Bronnen'" in app assert "label: 'Bronnen'" in app
assert "label: 'Kaart'" in app assert "label: 'Kaart'" in app
assert "label: 'Kwaliteit'" 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: def test_workbench_shell_has_skip_link_and_main_focus_target() -> None:
app = (ROOT / "frontend" / "src" / "App.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 'className="skip-link"' in app assert 'className="skip-link"' in app
assert 'href="#workspace-main"' 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 'id="workspace-main"' in app
assert "tabIndex={-1}" in app assert "tabIndex={-1}" in app
def test_mobile_shell_density_contracts_are_defined() -> None: def test_mobile_shell_density_contracts_are_defined() -> None:
css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8") 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" in css
assert ".skip-link:focus-visible" in css assert ".skip-link:focus-visible" in css
assert "scroll-margin-top: 8.5rem;" in css assert "scroll-margin-top: 8.5rem;" in css
assert "grid-template-columns: repeat(4, minmax(6.75rem, 1fr));" in css assert "@media (max-width: 920px)" in atlas
assert "overflow-x: auto;" in css assert "overflow-x: auto;" in atlas
assert "scroll-snap-type: x proximity;" in css assert "flex: 0 0 4.4rem;" in atlas
assert "white-space: nowrap;" in css assert "white-space: nowrap;" in atlas
assert ".context-bar,\n .quick-action-grid,\n .workspace-nav-cluster" not in css assert ".context-bar,\n .quick-action-grid,\n .workspace-nav-cluster" not in css
def test_small_screen_navigation_keeps_labels_readable() -> None: 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 ".workbench-sidebar .nav-item" in css
assert "min-width: 9.5rem;" in css assert "min-width: 4.4rem;" in css
assert ".nav-item small" in css assert ".nav-item-icon" in css
assert "line-height: 1.18;" 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 - 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. 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 - [x] Maak de vijf bestaande officiële beleidsrasters veilig op aanvraag
beschikbaar voor elke begrensde selectie in Vlaanderen, met Dataset-cache, beschikbaar voor elke begrensde selectie in Vlaanderen, met Dataset-cache,
semantische metrics en zonder 1.425 vooraf geladen rasters. 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: 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 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 Stitch-guided Atlas Workbench redesign lives in
the map dominant, places the results rail beside the map when the viewport is `src/styles/atlas-workbench.css`, with the shared icon navigation in
wide enough, preserves a readable two-column map flow on smaller desktops and `components/shell/WorkbenchNavigation.tsx`. The application uses one compact
stacks the context header and navigation explicitly on mobile. It contains no navigation rail, one context bar and task-specific work surfaces. The primary
workflow or API logic. 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 ## Current component boundaries
+10
View File
@@ -8,6 +8,7 @@
"name": "geointel-frontend", "name": "geointel-frontend",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"lucide-react": "^1.25.0",
"maplibre-gl": "^4.7.1", "maplibre-gl": "^4.7.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0"
@@ -2415,6 +2416,15 @@
"yallist": "^3.0.2" "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": { "node_modules/lz-string": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "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" "node": "^20.19.0 || >=22.12.0"
}, },
"dependencies": { "dependencies": {
"lucide-react": "^1.25.0",
"maplibre-gl": "^4.7.1", "maplibre-gl": "^4.7.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0"
+48 -78
View File
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import './styles/app.css' import './styles/app.css'
import './styles/premium.css' import './styles/premium.css'
import './styles/visual-polish.css' import './styles/atlas-workbench.css'
import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel' import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel'
import { GeoAssistantPanel } from './components/assistant/GeoAssistantPanel' import { GeoAssistantPanel } from './components/assistant/GeoAssistantPanel'
import { DatasetPanel } from './components/datasets/DatasetPanel' import { DatasetPanel } from './components/datasets/DatasetPanel'
@@ -18,6 +18,11 @@ import { QualityResultsPanel } from './components/quality/QualityResultsPanel'
import type { DatasetCreateResponse } from './types' import type { DatasetCreateResponse } from './types'
import { ProviderPanel } from './components/providers/ProviderPanel' import { ProviderPanel } from './components/providers/ProviderPanel'
import { SegmentationLab } from './components/segmentation/SegmentationLab' import { SegmentationLab } from './components/segmentation/SegmentationLab'
import {
WorkbenchNavigation,
type WorkspaceNavigationGroup,
type WorkspaceNavigationItem,
} from './components/shell/WorkbenchNavigation'
import { useChangeDetectionWorkflow } from './hooks/useChangeDetectionWorkflow' import { useChangeDetectionWorkflow } from './hooks/useChangeDetectionWorkflow'
import { useDemoWorkflow } from './hooks/useDemoWorkflow' import { useDemoWorkflow } from './hooks/useDemoWorkflow'
import { useCoverageResolver } from './hooks/useCoverageResolver' import { useCoverageResolver } from './hooks/useCoverageResolver'
@@ -51,7 +56,7 @@ function isVectorDatasetType(datasetType: string): boolean {
return datasetType === 'vector' || datasetType === 'geojson' 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: 'overview', label: 'Status', description: 'Beschikbaarheid en aandachtspunten' },
{ key: 'data', label: 'Bronnen', description: 'Gebieden en ingeladen gegevens' }, { key: 'data', label: 'Bronnen', description: 'Gebieden en ingeladen gegevens' },
{ key: 'map', label: 'Kaart', description: 'Selecteren, uitlezen en vergelijken' }, { 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' }, { key: 'system', label: 'Beheer', description: 'Databronnen en systeemstatus' },
] ]
const workspaceNavGroups: Array<{ label: string; keys: WorkspaceKey[] }> = [ const workspaceNavGroups: WorkspaceNavigationGroup[] = [
{ label: 'Verkennen', keys: ['map', 'data'] }, { label: 'Verkennen', keys: ['map', 'data'] },
{ label: 'Vragen', keys: ['assistant'] }, { label: 'Vragen', keys: ['assistant'] },
{ label: 'Analyseren', keys: ['analysis', 'ai'] }, { label: 'Analyseren', keys: ['analysis', 'ai'] },
@@ -779,71 +784,52 @@ function App(): JSX.Element {
> >
Ga naar de werkruimte Ga naar de werkruimte
</a> </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> <p className="sr-only" role="status" aria-live="polite">{workspaceStatusMessage}</p>
{errorMessage ? <p className="error" role="alert">{errorMessage}</p> : null} {errorMessage ? <p className="error" role="alert">{errorMessage}</p> : null}
<div className="workbench-layout"> <div className="workbench-layout">
<aside className="workbench-sidebar" aria-label="Navigatie van de werkruimte"> <WorkbenchNavigation
<nav aria-label="Hoofdonderdelen"> activeWorkspace={activeWorkspace}
{workspaceNavGroups.map((group) => ( groups={workspaceNavGroups}
<div className="nav-group" key={group.label}> items={workspaceNavItems}
<p className="nav-section-label">{group.label}</p> onSelect={setActiveWorkspace}
{group.keys.map((key) => { />
const item = workspaceNavItems.find((candidate) => candidate.key === key)
if (!item) { <div className="workbench-stage">
return null <header className="workbench-topbar">
} <div className="mobile-brand">
return ( <span className="brand-mark" aria-hidden="true">GI</span>
<button <strong>GeoIntel</strong>
key={item.key} </div>
type="button" <div className="context-bar" aria-label="Actieve werkcontext">
className={item.key === activeWorkspace ? 'nav-item nav-item-active' : 'nav-item'} <div>
onClick={() => setActiveWorkspace(item.key)} <span>Werkruimte</span>
aria-current={item.key === activeWorkspace ? 'page' : undefined} <strong title={projectContextLabel}>{projectContextLabel}</strong>
aria-label={`Open ${item.label}: ${item.description}`}
data-testid={`workspace-nav-${item.key}`}
>
<span>{item.label}</span>
<small>{item.description}</small>
</button>
)
})}
</div> </div>
))} <div>
</nav> <span>Gebied</span>
</aside> <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 <main
ref={workbenchMainRef} ref={workbenchMainRef}
className="workbench-main" className={activeWorkspace === 'map' ? 'workbench-main workbench-main-map' : 'workbench-main'}
id="workspace-main" id="workspace-main"
tabIndex={-1} tabIndex={-1}
aria-busy={workspaceDataLoading} aria-busy={workspaceDataLoading}
@@ -868,24 +854,6 @@ function App(): JSX.Element {
) : null} ) : null}
</div> </div>
</div> : null} </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' ? ( {activeWorkspace === 'overview' ? (
<OverviewWorkspace <OverviewWorkspace
selectedProject={selectedProject} selectedProject={selectedProject}
@@ -1330,6 +1298,8 @@ function App(): JSX.Element {
/> />
</aside> </aside>
) : null} ) : null}
</div>
</div>
</div> </div>
</div> </div>
) )
+5 -5
View File
@@ -1868,8 +1868,8 @@ export function MapWorkspace({
<header className="geo-explorer-header"> <header className="geo-explorer-header">
<div> <div>
<p className="eyebrow">{activeScopeLabel} · geografische verkenner</p> <p className="eyebrow">{activeScopeLabel} · geografische verkenner</p>
<h2>Wat bevindt zich in dit gebied?</h2> <h2>Gebied analyseren</h2>
<p>Kies een datathema, teken een rechthoek en lees de beschikbare gegevens meteen uit.</p> <p>Kies een thema, teken een rechthoek en lees de beschikbare gegevens.</p>
</div> </div>
<div className="geo-analysis-mode" role="tablist" aria-label="Analyseperiode"> <div className="geo-analysis-mode" role="tablist" aria-label="Analyseperiode">
<button <button
@@ -1932,8 +1932,8 @@ export function MapWorkspace({
<div className="geo-panel-heading"> <div className="geo-panel-heading">
<span>1</span> <span>1</span>
<div> <div>
<h3>Kies een datathema</h3> <h3>Thema</h3>
<p>Dit zijn databronnen, geen AI-modellen.</p> <p>Kies welke gegevens u wilt meten.</p>
</div> </div>
</div> </div>
<div className="geo-loaded-scope" aria-label="Ingeladen regiobereik"> <div className="geo-loaded-scope" aria-label="Ingeladen regiobereik">
@@ -2307,7 +2307,7 @@ export function MapWorkspace({
<div className="geo-panel-heading"> <div className="geo-panel-heading">
<span>3</span> <span>3</span>
<div> <div>
<h3>Resultaten</h3> <h3>Inzichten</h3>
<p>Alleen gemeten gegevens uit beschikbare bronnen.</p> <p>Alleen gemeten gegevens uit beschikbare bronnen.</p>
</div> </div>
</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;
}
}