Refine premium workbench UX
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-14 05:10:36 +02:00
parent a18fba8c7a
commit a2d9cef986
15 changed files with 1802 additions and 106 deletions
+64 -28
View File
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useState } from 'react'
import './styles/app.css'
import './styles/premium.css'
import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel'
import { DatasetPanel } from './components/datasets/DatasetPanel'
import { DetectionLab } from './components/detection/DetectionLab'
@@ -45,10 +46,18 @@ const workspaceNavItems: Array<{ key: WorkspaceKey; label: string; description:
{ key: 'system', label: 'System', description: 'Provider capabilities' },
]
const workspaceNavGroups: Array<{ label: string; keys: WorkspaceKey[] }> = [
{ label: 'Workspace', keys: ['overview', 'data', 'map'] },
{ label: 'Analyze', keys: ['analysis', 'ai'] },
{ label: 'Deliver', keys: ['exports', 'system'] },
]
function App(): JSX.Element {
const [activeWorkspace, setActiveWorkspace] = useState<WorkspaceKey>('overview')
const [inspectorOpen, setInspectorOpen] = useState(false)
useEffect(() => {
window.scrollTo({ top: 0, left: 0 })
setInspectorOpen(false)
}, [activeWorkspace])
const {
@@ -643,8 +652,11 @@ function App(): JSX.Element {
</a>
<header className="workbench-topbar">
<div className="brand-block">
<p className="eyebrow">GeoAI Workbench · Mol / Kempen</p>
<h1>GeoIntel Kempen</h1>
<span className="brand-mark" aria-hidden="true">GI</span>
<div>
<h1>GeoIntel</h1>
<p>GeoAI Workbench · Mol / Kempen</p>
</div>
</div>
<div className="context-bar" aria-label="Active workbench context">
<div>
@@ -671,30 +683,54 @@ function App(): JSX.Element {
<div className="workbench-layout">
<aside className="workbench-sidebar" aria-label="Workbench navigation">
<nav aria-label="Primary workspaces">
{workspaceNavItems.map((item) => (
<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} workspace: ${item.description}`}
data-testid={`workspace-nav-${item.key}`}
>
<span>{item.label}</span>
<small>{item.description}</small>
</button>
{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} workspace: ${item.description}`}
data-testid={`workspace-nav-${item.key}`}
>
<span>{item.label}</span>
<small>{item.description}</small>
</button>
)
})}
</div>
))}
</nav>
</aside>
<main className="workbench-main" id="workspace-main" tabIndex={-1}>
<div className="workspace-heading">
<div>
<p className="eyebrow">Workspace</p>
<div className="workspace-heading-copy">
<p className="eyebrow">{selectedProject?.region ?? 'Mol, Kempen'}</p>
<h2>{activeWorkspaceItem.label}</h2>
</div>
<p>{activeWorkspaceItem.description}</p>
<div className="workspace-heading-actions">
<p>{activeWorkspaceItem.description}</p>
{activeWorkspace !== 'overview' ? (
<button
type="button"
className={inspectorOpen ? 'inspector-toggle inspector-toggle-active' : 'inspector-toggle'}
onClick={() => setInspectorOpen((current) => !current)}
aria-expanded={inspectorOpen}
aria-controls="workbench-inspector"
>
{inspectorOpen ? 'Close details' : 'Open details'}
</button>
) : null}
</div>
</div>
<div className="workspace-command-bar" aria-label="Workspace shortcuts">
<div className="workspace-nav-cluster">
@@ -763,12 +799,8 @@ function App(): JSX.Element {
</section>
<section className="overview-actions">
<div className="overview-action-copy">
<p className="eyebrow">Recommended flow</p>
<h2>Build the project, load data, review on map, validate, export.</h2>
<p className="muted">
Keep the daily workflow focused: prepare data first, inspect it spatially, then run QA/QC or AI lab tasks only when the
required datasets are present.
</p>
<p className="eyebrow">Quick access</p>
<h2>Continue working</h2>
</div>
<div className="quick-action-grid overview-quick-actions" aria-label="Recommended next workbench actions">
<button
@@ -845,6 +877,7 @@ function App(): JSX.Element {
onDatasetFormChange={setDatasetForm}
onUploadDataset={uploadDataset}
onLoadDatasetDetails={loadDatasetDetails}
onOpenInspector={() => setInspectorOpen(true)}
onRefreshMetadata={refreshMetadata}
onOpenDatasetInMap={openDatasetInMap}
onOpenDatasetExport={openDatasetExport}
@@ -1080,8 +1113,9 @@ function App(): JSX.Element {
) : null}
</main>
<aside className="workbench-inspector" aria-label="Current selection inspector">
<WorkbenchInspector
{inspectorOpen ? (
<aside className="workbench-inspector" id="workbench-inspector" aria-label="Current selection inspector">
<WorkbenchInspector
selectedProject={selectedProject}
selectedArea={selectedArea}
selectedMapFeature={selectedMapFeature}
@@ -1101,6 +1135,7 @@ function App(): JSX.Element {
onOpenQualityWorkspace={() => setActiveWorkspace('analysis')}
onOpenExportsWorkspace={() => setActiveWorkspace('exports')}
onOpenAiWorkspace={() => setActiveWorkspace('ai')}
onClose={() => setInspectorOpen(false)}
datasetDetailProps={{
areas,
availableVectorTargets,
@@ -1160,8 +1195,9 @@ function App(): JSX.Element {
onRunVectorIntersect: () => runVectorIntersect(availableVectorTargets),
onPickDerivedDataset: pickDerivedDataset,
}}
/>
</aside>
/>
</aside>
) : null}
</div>
</div>
)