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
+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;
}
}