Refactor workbench into task-based UI shell
This commit is contained in:
@@ -7,6 +7,15 @@
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Sprint 49 Workbench shell UI refactor (2026-06-17)
|
||||||
|
|
||||||
|
- Replaced the one-page workbench panel stack with a task-based UI shell.
|
||||||
|
- Added primary workspaces for Overview, Data, Map, QA/QC, AI Labs, Exports and System.
|
||||||
|
- Added a persistent top context bar for active project, AOI, dataset and layer state.
|
||||||
|
- Moved dataset details into a persistent right-side inspector instead of leaving them below the full workflow.
|
||||||
|
- Kept existing hooks, API contracts, backend behavior, migrations, provider behavior and AI configuration unchanged.
|
||||||
|
- Added static regression coverage for the new shell regions and workspace navigation anchors.
|
||||||
|
|
||||||
## Sprint 48 Backend API contract audit (2026-06-17)
|
## Sprint 48 Backend API contract audit (2026-06-17)
|
||||||
|
|
||||||
- Added `scripts/audit_api_contracts.py` to compare the active FastAPI route surface with `docs/API_CONTRACTS.md`.
|
- Added `scripts/audit_api_contracts.py` to compare the active FastAPI route surface with `docs/API_CONTRACTS.md`.
|
||||||
|
|||||||
@@ -18,15 +18,18 @@ def test_app_uses_quality_and_map_presentational_components() -> None:
|
|||||||
assert "<GeoMap" not in app
|
assert "<GeoMap" not in app
|
||||||
|
|
||||||
|
|
||||||
def test_map_workspace_is_promoted_before_dense_workflow_grid() -> None:
|
def test_map_workspace_lives_in_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")
|
||||||
styles = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8")
|
styles = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert app.index("<MapWorkspace") < app.index('<main className="workspace-grid">')
|
assert "activeWorkspace === 'map'" in app
|
||||||
assert "grid-template-columns: repeat(12, minmax(0, 1fr));" in styles
|
assert app.index("activeWorkspace === 'map'") < app.index("<MapWorkspace")
|
||||||
assert ".workspace-grid > section" in styles
|
assert 'className="workbench-main"' in app
|
||||||
assert "max-height: 42rem;" in styles
|
assert 'className="workbench-inspector"' in app
|
||||||
assert "overflow: auto;" in styles
|
assert ".workbench-layout" in styles
|
||||||
|
assert ".workbench-sidebar" in styles
|
||||||
|
assert ".workbench-main" in styles
|
||||||
|
assert ".workbench-inspector" in styles
|
||||||
|
|
||||||
|
|
||||||
def test_quality_results_panel_owns_persisted_quality_check_markup() -> None:
|
def test_quality_results_panel_owns_persisted_quality_check_markup() -> None:
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ def test_app_entrypoint_has_clean_encoding_and_react_imports() -> None:
|
|||||||
app = app_path.read_text(encoding="utf-8")
|
app = app_path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert not app_bytes.startswith(b"\xef\xbb\xbf")
|
assert not app_bytes.startswith(b"\xef\xbb\xbf")
|
||||||
assert "import { useMemo } from 'react'" in app
|
assert "import { useMemo, useState } from 'react'" in app
|
||||||
assert "FormEvent" not in app
|
assert "FormEvent" not in app
|
||||||
assert "useEffect" not in app
|
assert "useEffect" not in app
|
||||||
assert "useState" not in app
|
assert "const [activeWorkspace, setActiveWorkspace] = useState<WorkspaceKey>('overview')" in app
|
||||||
|
|
||||||
|
|
||||||
def test_demo_workflow_hook_owns_demo_api_and_cross_module_selection() -> None:
|
def test_demo_workflow_hook_owns_demo_api_and_cross_module_selection() -> None:
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
assert "type WorkspaceKey" in app
|
||||||
|
assert "workspaceNavItems" in app
|
||||||
|
assert 'data-testid={`workspace-nav-${item.key}`}' in app
|
||||||
|
assert 'className="workbench-layout"' in app
|
||||||
|
assert 'className="workbench-sidebar"' in app
|
||||||
|
assert 'className="workbench-main"' in app
|
||||||
|
assert 'className="workbench-inspector"' in app
|
||||||
|
assert "activeWorkspace === 'data'" in app
|
||||||
|
assert "activeWorkspace === 'map'" in app
|
||||||
|
assert "activeWorkspace === 'analysis'" in app
|
||||||
|
assert "activeWorkspace === 'ai'" in app
|
||||||
|
assert "activeWorkspace === 'exports'" in app
|
||||||
|
assert "activeWorkspace === 'system'" in app
|
||||||
|
|
||||||
|
|
||||||
|
def test_workbench_shell_css_provides_navigation_main_and_inspector_regions() -> None:
|
||||||
|
css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert ".workbench-topbar" in css
|
||||||
|
assert ".context-bar" in css
|
||||||
|
assert ".workbench-layout" in css
|
||||||
|
assert ".workbench-sidebar" in css
|
||||||
|
assert ".nav-item-active" in css
|
||||||
|
assert ".workbench-main" in css
|
||||||
|
assert ".workbench-inspector" in css
|
||||||
|
assert ".workspace-grid-data" in css
|
||||||
|
assert ".workspace-grid-ai" in css
|
||||||
@@ -2042,3 +2042,28 @@ Limitations:
|
|||||||
|
|
||||||
Next recommended pass:
|
Next recommended pass:
|
||||||
- Run release readiness, deploy Tower and decide whether to manually acknowledge the current collation version.
|
- Run release readiness, deploy Tower and decide whether to manually acknowledge the current collation version.
|
||||||
|
|
||||||
|
## Sprint 49 Workbench shell UI refactor (2026-06-17)
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
- Audited the live workbench UI and confirmed the main usability issue was information architecture: all V1 workflows were mounted as one long vertical panel stack.
|
||||||
|
- Refactored the frontend into a task-based workbench shell with Overview, Data, Map, QA/QC, AI Labs, Exports and System workspaces.
|
||||||
|
- Added a persistent top context bar for active project, AOI, dataset and layer state.
|
||||||
|
- Moved selected dataset details into a persistent right-side inspector while keeping the same dataset/raster/vector operation callbacks.
|
||||||
|
- Added stable primary navigation test anchors.
|
||||||
|
- Documented the new frontend shell structure and updated the changelog.
|
||||||
|
|
||||||
|
Tested:
|
||||||
|
- `cd frontend && npm run typecheck`
|
||||||
|
- `cd frontend && npm run build`
|
||||||
|
- Local Vite visual audit on `http://127.0.0.1:5175`
|
||||||
|
|
||||||
|
Open:
|
||||||
|
- Local Vite visual audit shows `Request failed (404)` when no local backend/proxy target is available. Docker/nginx same-origin proxy behavior remains the production path.
|
||||||
|
- The next pass should run full release readiness and deploy to Tower for browser-facing verification on `http://192.168.10.150:1202`.
|
||||||
|
|
||||||
|
Limitations:
|
||||||
|
- This pass intentionally changes UI structure only. It does not add product capabilities, alter API contracts, change migrations, fetch live providers or enable new AI models.
|
||||||
|
|
||||||
|
Next recommended pass:
|
||||||
|
- Run full readiness, rebuild/deploy Tower, then perform a live browser smoke through the new workbench navigation.
|
||||||
|
|||||||
@@ -312,3 +312,10 @@ This file now starts with the current implementation status. Older preparation/b
|
|||||||
- [x] Add implementation ticket index and tickets.
|
- [x] Add implementation ticket index and tickets.
|
||||||
- [x] Add API example payloads.
|
- [x] Add API example payloads.
|
||||||
- [x] Add final pre-Codex checklist.
|
- [x] Add final pre-Codex checklist.
|
||||||
|
|
||||||
|
# Current Workbench UI
|
||||||
|
|
||||||
|
- [x] Replace the one-page workflow panel stack with a task-based workbench shell.
|
||||||
|
- [x] Add persistent project/AOI/dataset/layer context.
|
||||||
|
- [x] Move selected dataset details into a persistent inspector.
|
||||||
|
- [ ] Deploy the shell refactor to Tower and run live browser smoke on port 1202.
|
||||||
|
|||||||
+12
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
React + TypeScript + MapLibre foundation for project/area/dataset workflow.
|
React + TypeScript + MapLibre foundation for project/area/dataset workflow.
|
||||||
|
|
||||||
The workbench is intentionally map-first: `MapWorkspace` is promoted above the dense workflow grid so GIS context stays visible before lower-detail provider, AI, QA and export panels. Long workflow panels are scroll-contained by CSS rather than stretching every grid row.
|
The workbench now uses a task-based shell instead of a single long panel stack. `App.tsx` still owns shared orchestration state, but the UI is organized into Overview, Data, Map, QA/QC, AI Labs, Exports and System workspaces with a persistent top context bar and right-side dataset inspector.
|
||||||
|
|
||||||
## Scope implemented
|
## Scope implemented
|
||||||
- API client layer (`src/services/api`)
|
- API client layer (`src/services/api`)
|
||||||
@@ -220,6 +220,17 @@ The workbench is intentionally map-first: `MapWorkspace` is promoted above the d
|
|||||||
- Offline demo workflow orchestration lives in `src/hooks/useDemoWorkflow.ts`, because it coordinates project, dataset, map, QA/QC, detection, segmentation and export state after the backend fixture seed.
|
- Offline demo workflow orchestration lives in `src/hooks/useDemoWorkflow.ts`, because it coordinates project, dataset, map, QA/QC, detection, segmentation and export state after the backend fixture seed.
|
||||||
- Workbench bootstrap and reload effects live in `src/hooks/useWorkbenchBootstrap.ts`, keeping `App.tsx` focused on composing hooks into panels.
|
- Workbench bootstrap and reload effects live in `src/hooks/useWorkbenchBootstrap.ts`, keeping `App.tsx` focused on composing hooks into panels.
|
||||||
|
|
||||||
|
## Workbench shell refactor
|
||||||
|
|
||||||
|
- The primary UI is organized around `workspaceNavItems` in `src/App.tsx`.
|
||||||
|
- The shell regions are:
|
||||||
|
- `workbench-topbar`: active project, AOI, dataset and layer context.
|
||||||
|
- `workbench-sidebar`: primary navigation between workflow workspaces.
|
||||||
|
- `workbench-main`: one active workflow at a time.
|
||||||
|
- `workbench-inspector`: persistent selected dataset details and raster/vector operation controls.
|
||||||
|
- Existing API calls, hooks, MapLibre rendering and QA/AI/export flows are unchanged.
|
||||||
|
- Stable navigation test anchors use `data-testid="workspace-nav-{workspace}"`.
|
||||||
|
|
||||||
## Raster dependency visibility
|
## Raster dependency visibility
|
||||||
|
|
||||||
Raster metadata and raster ops may remain unavailable when backend raster stack is missing. In that case:
|
Raster metadata and raster ops may remain unavailable when backend raster stack is missing. In that case:
|
||||||
|
|||||||
+362
-248
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import './styles/app.css'
|
import './styles/app.css'
|
||||||
import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel'
|
import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel'
|
||||||
import { DatasetDetailPanel } from './components/datasets/DatasetDetailPanel'
|
import { DatasetDetailPanel } from './components/datasets/DatasetDetailPanel'
|
||||||
@@ -30,7 +30,20 @@ function isVectorDatasetType(datasetType: string): boolean {
|
|||||||
return datasetType === 'vector' || datasetType === 'geojson'
|
return datasetType === 'vector' || datasetType === 'geojson'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WorkspaceKey = 'overview' | 'data' | 'map' | 'analysis' | 'ai' | 'exports' | 'system'
|
||||||
|
|
||||||
|
const workspaceNavItems: Array<{ key: WorkspaceKey; label: string; description: string }> = [
|
||||||
|
{ key: 'overview', label: 'Overview', description: 'V1 readiness and next action' },
|
||||||
|
{ key: 'data', label: 'Data', description: 'Projects, AOI and datasets' },
|
||||||
|
{ key: 'map', label: 'Map', description: 'Layer review and inspection' },
|
||||||
|
{ key: 'analysis', label: 'QA/QC', description: 'Quality checks and change detection' },
|
||||||
|
{ key: 'ai', label: 'AI Labs', description: 'Detection and segmentation' },
|
||||||
|
{ key: 'exports', label: 'Exports', description: 'Artifacts and downloads' },
|
||||||
|
{ key: 'system', label: 'System', description: 'Provider capabilities' },
|
||||||
|
]
|
||||||
|
|
||||||
function App(): JSX.Element {
|
function App(): JSX.Element {
|
||||||
|
const [activeWorkspace, setActiveWorkspace] = useState<WorkspaceKey>('overview')
|
||||||
const {
|
const {
|
||||||
projects,
|
projects,
|
||||||
selectedProject,
|
selectedProject,
|
||||||
@@ -370,269 +383,370 @@ function App(): JSX.Element {
|
|||||||
resetExportsForProject,
|
resetExportsForProject,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selectedArea = areas.find((area) => area.id === selectedMapAreaId) ?? null
|
||||||
|
const activeWorkspaceItem = workspaceNavItems.find((item) => item.key === activeWorkspace) ?? workspaceNavItems[0]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-shell">
|
<div className="app-shell workbench-shell">
|
||||||
<header>
|
<header className="workbench-topbar">
|
||||||
<h1>GeoIntel Kempen V1 Workbench</h1>
|
<div className="brand-block">
|
||||||
<p>Raster/vector workbench with QA/QC, exports, change detection and AI foundation layers.</p>
|
<p className="eyebrow">GeoAI Workbench</p>
|
||||||
|
<h1>GeoIntel Kempen</h1>
|
||||||
|
</div>
|
||||||
|
<div className="context-bar" aria-label="Active workbench context">
|
||||||
|
<div>
|
||||||
|
<span>Project</span>
|
||||||
|
<strong>{selectedProject?.name ?? 'No project'}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>AOI</span>
|
||||||
|
<strong>{selectedArea?.name ?? (areas.length > 0 ? 'Select area' : 'No AOI')}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Dataset</span>
|
||||||
|
<strong>{selectedDataset?.name ?? (datasets.length > 0 ? 'Select dataset' : 'No dataset')}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Layer</span>
|
||||||
|
<strong>{mapFeatureCollection ? `${mapFeatureCount} features` : 'No active layer'}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{errorMessage ? <p className="error">{errorMessage}</p> : null}
|
{errorMessage ? <p className="error">{errorMessage}</p> : null}
|
||||||
|
|
||||||
<WorkbenchStatusStrip
|
<div className="workbench-layout">
|
||||||
selectedProject={selectedProject}
|
<aside className="workbench-sidebar" aria-label="Workbench navigation">
|
||||||
areas={areas}
|
<nav>
|
||||||
datasets={datasets}
|
{workspaceNavItems.map((item) => (
|
||||||
qualityChecks={qualityChecks}
|
<button
|
||||||
exports={exports}
|
key={item.key}
|
||||||
activeLayerFeatureCount={mapFeatureCount}
|
type="button"
|
||||||
selectedAreaHasGeometry={Boolean(areaFeatureCollection)}
|
className={item.key === activeWorkspace ? 'nav-item nav-item-active' : 'nav-item'}
|
||||||
/>
|
onClick={() => setActiveWorkspace(item.key)}
|
||||||
|
aria-current={item.key === activeWorkspace ? 'page' : undefined}
|
||||||
|
data-testid={`workspace-nav-${item.key}`}
|
||||||
|
>
|
||||||
|
<span>{item.label}</span>
|
||||||
|
<small>{item.description}</small>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
<MapWorkspace
|
<main className="workbench-main">
|
||||||
areas={areas}
|
<div className="workspace-heading">
|
||||||
selectedMapAreaId={selectedMapAreaId}
|
<div>
|
||||||
areaFeatureCollection={areaFeatureCollection}
|
<p className="eyebrow">Workspace</p>
|
||||||
mapFeatureCollection={mapFeatureCollection}
|
<h2>{activeWorkspaceItem.label}</h2>
|
||||||
mapLayerLabel={mapLayerLabel}
|
</div>
|
||||||
mapLayerVisible={mapLayerVisible}
|
<p>{activeWorkspaceItem.description}</p>
|
||||||
mapLayerOpacity={mapLayerOpacity}
|
</div>
|
||||||
areaLayerVisible={areaLayerVisible}
|
|
||||||
areaLayerOpacity={areaLayerOpacity}
|
|
||||||
mapFeatureCount={mapFeatureCount}
|
|
||||||
areaFeatureCount={areaFeatureCount}
|
|
||||||
selectedMapFeature={selectedMapFeature}
|
|
||||||
onSelectMapArea={setSelectedMapAreaId}
|
|
||||||
onSetAreaLayerVisible={setAreaLayerVisible}
|
|
||||||
onSetAreaLayerOpacity={setAreaLayerOpacity}
|
|
||||||
onSetMapLayerVisible={setMapLayerVisible}
|
|
||||||
onSetMapLayerOpacity={setMapLayerOpacity}
|
|
||||||
onSelectMapFeature={setSelectedMapFeature}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<main className="workspace-grid">
|
{activeWorkspace === 'overview' ? (
|
||||||
<ProjectPanel
|
<div className="workspace-stack">
|
||||||
projects={projects}
|
<WorkbenchStatusStrip
|
||||||
selectedProjectId={selectedProjectId}
|
selectedProject={selectedProject}
|
||||||
loadingProjects={loadingProjects}
|
areas={areas}
|
||||||
projectForm={projectForm}
|
datasets={datasets}
|
||||||
loadingDemoWorkflow={loadingDemoWorkflow}
|
qualityChecks={qualityChecks}
|
||||||
demoWorkflowMessage={demoWorkflowMessage}
|
exports={exports}
|
||||||
onCreateProject={createProject}
|
activeLayerFeatureCount={mapFeatureCount}
|
||||||
onUpdateProjectForm={setProjectForm}
|
selectedAreaHasGeometry={Boolean(areaFeatureCollection)}
|
||||||
onSelectProject={setSelectedProjectId}
|
/>
|
||||||
onLoadDemoWorkflow={loadDemoWorkflow}
|
<section className="overview-actions">
|
||||||
/>
|
<div>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div className="quick-action-grid">
|
||||||
|
<button type="button" onClick={() => setActiveWorkspace('data')}>
|
||||||
|
Open data setup
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => setActiveWorkspace('map')}>
|
||||||
|
Inspect map
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => setActiveWorkspace('analysis')}>
|
||||||
|
Review QA/QC
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => setActiveWorkspace('exports')}>
|
||||||
|
Manage exports
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<AreaPanel
|
{activeWorkspace === 'data' ? (
|
||||||
areas={areas}
|
<div className="workspace-grid workspace-grid-data">
|
||||||
selectedProject={selectedProject}
|
<ProjectPanel
|
||||||
selectedProjectId={selectedProjectId}
|
projects={projects}
|
||||||
loadingAreas={loadingAreas}
|
selectedProjectId={selectedProjectId}
|
||||||
areaForm={areaForm}
|
loadingProjects={loadingProjects}
|
||||||
selectedMapAreaId={selectedMapAreaId}
|
projectForm={projectForm}
|
||||||
onCreateArea={createArea}
|
loadingDemoWorkflow={loadingDemoWorkflow}
|
||||||
onUpdateAreaForm={setAreaForm}
|
demoWorkflowMessage={demoWorkflowMessage}
|
||||||
onSelectMapArea={setSelectedMapAreaId}
|
onCreateProject={createProject}
|
||||||
/>
|
onUpdateProjectForm={setProjectForm}
|
||||||
|
onSelectProject={setSelectedProjectId}
|
||||||
|
onLoadDemoWorkflow={loadDemoWorkflow}
|
||||||
|
/>
|
||||||
|
|
||||||
<DatasetPanel
|
<AreaPanel
|
||||||
selectedProjectId={selectedProjectId}
|
areas={areas}
|
||||||
areas={areas}
|
selectedProject={selectedProject}
|
||||||
datasets={datasets}
|
selectedProjectId={selectedProjectId}
|
||||||
datasetForm={datasetForm}
|
loadingAreas={loadingAreas}
|
||||||
loadingDatasets={loadingDatasets}
|
areaForm={areaForm}
|
||||||
onDatasetFormChange={setDatasetForm}
|
selectedMapAreaId={selectedMapAreaId}
|
||||||
onUploadDataset={uploadDataset}
|
onCreateArea={createArea}
|
||||||
onLoadDatasetDetails={loadDatasetDetails}
|
onUpdateAreaForm={setAreaForm}
|
||||||
onRefreshMetadata={refreshMetadata}
|
onSelectMapArea={setSelectedMapAreaId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ProviderPanel
|
<DatasetPanel
|
||||||
providers={providers}
|
selectedProjectId={selectedProjectId}
|
||||||
loadingCapabilities={loadingCapabilities}
|
areas={areas}
|
||||||
capabilitiesError={capabilitiesError}
|
datasets={datasets}
|
||||||
onRefresh={loadCapabilities}
|
datasetForm={datasetForm}
|
||||||
/>
|
loadingDatasets={loadingDatasets}
|
||||||
|
onDatasetFormChange={setDatasetForm}
|
||||||
|
onUploadDataset={uploadDataset}
|
||||||
|
onLoadDatasetDetails={loadDatasetDetails}
|
||||||
|
onRefreshMetadata={refreshMetadata}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<ChangeDetectionPanel
|
{activeWorkspace === 'map' ? (
|
||||||
vectorDatasets={availableVectorDatasets}
|
<MapWorkspace
|
||||||
sourceDatasetId={changeSourceDatasetId}
|
areas={areas}
|
||||||
targetDatasetId={changeTargetDatasetId}
|
selectedMapAreaId={selectedMapAreaId}
|
||||||
iouThreshold={changeIouThreshold}
|
areaFeatureCollection={areaFeatureCollection}
|
||||||
includeUnchanged={changeIncludeUnchanged}
|
mapFeatureCollection={mapFeatureCollection}
|
||||||
running={runningChangeDetection}
|
mapLayerLabel={mapLayerLabel}
|
||||||
result={changeDetectionResult}
|
mapLayerVisible={mapLayerVisible}
|
||||||
error={changeDetectionError}
|
mapLayerOpacity={mapLayerOpacity}
|
||||||
onSourceDatasetChange={setChangeSourceDatasetId}
|
areaLayerVisible={areaLayerVisible}
|
||||||
onTargetDatasetChange={setChangeTargetDatasetId}
|
areaLayerOpacity={areaLayerOpacity}
|
||||||
onIouThresholdChange={setChangeIouThreshold}
|
mapFeatureCount={mapFeatureCount}
|
||||||
onIncludeUnchangedChange={setChangeIncludeUnchanged}
|
areaFeatureCount={areaFeatureCount}
|
||||||
onRun={runChangeDetection}
|
selectedMapFeature={selectedMapFeature}
|
||||||
/>
|
onSelectMapArea={setSelectedMapAreaId}
|
||||||
|
onSetAreaLayerVisible={setAreaLayerVisible}
|
||||||
|
onSetAreaLayerOpacity={setAreaLayerOpacity}
|
||||||
|
onSetMapLayerVisible={setMapLayerVisible}
|
||||||
|
onSetMapLayerOpacity={setMapLayerOpacity}
|
||||||
|
onSelectMapFeature={setSelectedMapFeature}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<DetectionLab
|
{activeWorkspace === 'analysis' ? (
|
||||||
detectionModels={detectionModels}
|
<div className="workspace-grid workspace-grid-analysis">
|
||||||
loadingDetectionModels={loadingDetectionModels}
|
<ChangeDetectionPanel
|
||||||
detectionModelError={detectionModelError}
|
vectorDatasets={availableVectorDatasets}
|
||||||
selectedDetectionDatasetId={selectedDetectionDatasetId}
|
sourceDatasetId={changeSourceDatasetId}
|
||||||
selectedDetectionModelId={selectedDetectionModelId}
|
targetDatasetId={changeTargetDatasetId}
|
||||||
detectionTileManifestPath={detectionTileManifestPath}
|
iouThreshold={changeIouThreshold}
|
||||||
detectionConfidenceThreshold={detectionConfidenceThreshold}
|
includeUnchanged={changeIncludeUnchanged}
|
||||||
runningDetection={runningDetection}
|
running={runningChangeDetection}
|
||||||
detectionRunResult={detectionRunResult}
|
result={changeDetectionResult}
|
||||||
detectionRunError={detectionRunError}
|
error={changeDetectionError}
|
||||||
detectionRuns={detectionRuns}
|
onSourceDatasetChange={setChangeSourceDatasetId}
|
||||||
selectedDetectionRunId={selectedDetectionRunId}
|
onTargetDatasetChange={setChangeTargetDatasetId}
|
||||||
detectionItems={detectionItems}
|
onIouThresholdChange={setChangeIouThreshold}
|
||||||
detectionClassFilter={detectionClassFilter}
|
onIncludeUnchangedChange={setChangeIncludeUnchanged}
|
||||||
detectionMinConfidenceFilter={detectionMinConfidenceFilter}
|
onRun={runChangeDetection}
|
||||||
loadingDetectionResults={loadingDetectionResults}
|
/>
|
||||||
detectionReferenceDatasetId={detectionReferenceDatasetId}
|
|
||||||
detectionQaResult={detectionQaResult}
|
|
||||||
detectionQaError={detectionQaError}
|
|
||||||
runningDetectionQa={runningDetectionQa}
|
|
||||||
selectedProjectId={selectedProjectId}
|
|
||||||
rasterDatasets={rasterDatasets}
|
|
||||||
referenceDatasets={referenceDatasets}
|
|
||||||
onLoadModels={loadDetectionModels}
|
|
||||||
onSelectDataset={setSelectedDetectionDatasetId}
|
|
||||||
onSelectModel={setSelectedDetectionModelId}
|
|
||||||
onSetConfidenceThreshold={setDetectionConfidenceThreshold}
|
|
||||||
onSetTileManifestPath={setDetectionTileManifestPath}
|
|
||||||
onRunDetection={runDetection}
|
|
||||||
onLoadRuns={() => loadDetectionRuns()}
|
|
||||||
onSelectRun={setSelectedDetectionRunId}
|
|
||||||
onSetClassFilter={setDetectionClassFilter}
|
|
||||||
onSetMinConfidenceFilter={setDetectionMinConfidenceFilter}
|
|
||||||
onLoadResults={() => loadDetectionResults()}
|
|
||||||
onSelectReferenceDataset={setDetectionReferenceDatasetId}
|
|
||||||
onRunQa={runDetectionQa}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SegmentationLab
|
<QualityResultsPanel
|
||||||
segmentationModels={segmentationModels}
|
selectedProjectId={selectedProjectId}
|
||||||
loadingSegmentationModels={loadingSegmentationModels}
|
qualityChecks={qualityChecks}
|
||||||
segmentationModelError={segmentationModelError}
|
qualityChecksError={qualityChecksError}
|
||||||
selectedSegmentationDatasetId={selectedSegmentationDatasetId}
|
onRefresh={() => loadQualityChecks()}
|
||||||
selectedSegmentationModelId={selectedSegmentationModelId}
|
/>
|
||||||
segmentationConfidenceThreshold={segmentationConfidenceThreshold}
|
</div>
|
||||||
runningSegmentation={runningSegmentation}
|
) : null}
|
||||||
segmentationRunResult={segmentationRunResult}
|
|
||||||
segmentationRunError={segmentationRunError}
|
|
||||||
segmentationRuns={segmentationRuns}
|
|
||||||
selectedSegmentationRunId={selectedSegmentationRunId}
|
|
||||||
segmentationItems={segmentationItems}
|
|
||||||
segmentationClassFilter={segmentationClassFilter}
|
|
||||||
segmentationMinConfidenceFilter={segmentationMinConfidenceFilter}
|
|
||||||
loadingSegmentationResults={loadingSegmentationResults}
|
|
||||||
segmentationReferenceDatasetId={segmentationReferenceDatasetId}
|
|
||||||
segmentationQaResult={segmentationQaResult}
|
|
||||||
segmentationQaError={segmentationQaError}
|
|
||||||
runningSegmentationQa={runningSegmentationQa}
|
|
||||||
selectedProjectId={selectedProjectId}
|
|
||||||
rasterDatasets={rasterDatasets}
|
|
||||||
referenceDatasets={referenceDatasets}
|
|
||||||
selectedSegmentationModelConfigured={Boolean(selectedSegmentationModel?.configured)}
|
|
||||||
selectedSegmentationModelLimitation={selectedSegmentationModel?.limitation_message ?? null}
|
|
||||||
onLoadModels={loadSegmentationModels}
|
|
||||||
onSelectDataset={setSelectedSegmentationDatasetId}
|
|
||||||
onSelectModel={setSelectedSegmentationModelId}
|
|
||||||
onSetConfidenceThreshold={setSegmentationConfidenceThreshold}
|
|
||||||
onRunSegmentation={runSegmentation}
|
|
||||||
onLoadRuns={() => loadSegmentationRuns()}
|
|
||||||
onSelectRun={setSelectedSegmentationRunId}
|
|
||||||
onSetClassFilter={setSegmentationClassFilter}
|
|
||||||
onSetMinConfidenceFilter={setSegmentationMinConfidenceFilter}
|
|
||||||
onLoadResults={() => loadSegmentationResults()}
|
|
||||||
onSelectReferenceDataset={setSegmentationReferenceDatasetId}
|
|
||||||
onRunQa={runSegmentationQa}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<QualityResultsPanel
|
{activeWorkspace === 'ai' ? (
|
||||||
selectedProjectId={selectedProjectId}
|
<div className="workspace-grid workspace-grid-ai">
|
||||||
qualityChecks={qualityChecks}
|
<DetectionLab
|
||||||
qualityChecksError={qualityChecksError}
|
detectionModels={detectionModels}
|
||||||
onRefresh={() => loadQualityChecks()}
|
loadingDetectionModels={loadingDetectionModels}
|
||||||
/>
|
detectionModelError={detectionModelError}
|
||||||
|
selectedDetectionDatasetId={selectedDetectionDatasetId}
|
||||||
|
selectedDetectionModelId={selectedDetectionModelId}
|
||||||
|
detectionTileManifestPath={detectionTileManifestPath}
|
||||||
|
detectionConfidenceThreshold={detectionConfidenceThreshold}
|
||||||
|
runningDetection={runningDetection}
|
||||||
|
detectionRunResult={detectionRunResult}
|
||||||
|
detectionRunError={detectionRunError}
|
||||||
|
detectionRuns={detectionRuns}
|
||||||
|
selectedDetectionRunId={selectedDetectionRunId}
|
||||||
|
detectionItems={detectionItems}
|
||||||
|
detectionClassFilter={detectionClassFilter}
|
||||||
|
detectionMinConfidenceFilter={detectionMinConfidenceFilter}
|
||||||
|
loadingDetectionResults={loadingDetectionResults}
|
||||||
|
detectionReferenceDatasetId={detectionReferenceDatasetId}
|
||||||
|
detectionQaResult={detectionQaResult}
|
||||||
|
detectionQaError={detectionQaError}
|
||||||
|
runningDetectionQa={runningDetectionQa}
|
||||||
|
selectedProjectId={selectedProjectId}
|
||||||
|
rasterDatasets={rasterDatasets}
|
||||||
|
referenceDatasets={referenceDatasets}
|
||||||
|
onLoadModels={loadDetectionModels}
|
||||||
|
onSelectDataset={setSelectedDetectionDatasetId}
|
||||||
|
onSelectModel={setSelectedDetectionModelId}
|
||||||
|
onSetConfidenceThreshold={setDetectionConfidenceThreshold}
|
||||||
|
onSetTileManifestPath={setDetectionTileManifestPath}
|
||||||
|
onRunDetection={runDetection}
|
||||||
|
onLoadRuns={() => loadDetectionRuns()}
|
||||||
|
onSelectRun={setSelectedDetectionRunId}
|
||||||
|
onSetClassFilter={setDetectionClassFilter}
|
||||||
|
onSetMinConfidenceFilter={setDetectionMinConfidenceFilter}
|
||||||
|
onLoadResults={() => loadDetectionResults()}
|
||||||
|
onSelectReferenceDataset={setDetectionReferenceDatasetId}
|
||||||
|
onRunQa={runDetectionQa}
|
||||||
|
/>
|
||||||
|
|
||||||
<ExportCenter
|
<SegmentationLab
|
||||||
selectedProjectId={selectedProjectId}
|
segmentationModels={segmentationModels}
|
||||||
selectedDataset={selectedDataset}
|
loadingSegmentationModels={loadingSegmentationModels}
|
||||||
selectedDetectionRunId={selectedDetectionRunId}
|
segmentationModelError={segmentationModelError}
|
||||||
selectedSegmentationRunId={selectedSegmentationRunId}
|
selectedSegmentationDatasetId={selectedSegmentationDatasetId}
|
||||||
exports={exports}
|
selectedSegmentationModelId={selectedSegmentationModelId}
|
||||||
latestExport={latestExport}
|
segmentationConfidenceThreshold={segmentationConfidenceThreshold}
|
||||||
exportError={exportError}
|
runningSegmentation={runningSegmentation}
|
||||||
loadingExports={loadingExports}
|
segmentationRunResult={segmentationRunResult}
|
||||||
exporting={exporting}
|
segmentationRunError={segmentationRunError}
|
||||||
onRefresh={() => loadExports()}
|
segmentationRuns={segmentationRuns}
|
||||||
onExportDataset={exportSelectedDatasetGeoJson}
|
selectedSegmentationRunId={selectedSegmentationRunId}
|
||||||
onExportDetectionRun={exportSelectedDetectionRunGeoJson}
|
segmentationItems={segmentationItems}
|
||||||
onExportSegmentationRun={exportSelectedSegmentationRunGeoJson}
|
segmentationClassFilter={segmentationClassFilter}
|
||||||
onExportProjectMetadata={exportProjectMetadata}
|
segmentationMinConfidenceFilter={segmentationMinConfidenceFilter}
|
||||||
onExportProjectReport={exportProjectReport}
|
loadingSegmentationResults={loadingSegmentationResults}
|
||||||
onPreviewContent={previewExportContent}
|
segmentationReferenceDatasetId={segmentationReferenceDatasetId}
|
||||||
onDownload={downloadExportArtifact}
|
segmentationQaResult={segmentationQaResult}
|
||||||
/>
|
segmentationQaError={segmentationQaError}
|
||||||
<ExportPreview content={exportPreview} />
|
runningSegmentationQa={runningSegmentationQa}
|
||||||
</main>
|
selectedProjectId={selectedProjectId}
|
||||||
|
rasterDatasets={rasterDatasets}
|
||||||
|
referenceDatasets={referenceDatasets}
|
||||||
|
selectedSegmentationModelConfigured={Boolean(selectedSegmentationModel?.configured)}
|
||||||
|
selectedSegmentationModelLimitation={selectedSegmentationModel?.limitation_message ?? null}
|
||||||
|
onLoadModels={loadSegmentationModels}
|
||||||
|
onSelectDataset={setSelectedSegmentationDatasetId}
|
||||||
|
onSelectModel={setSelectedSegmentationModelId}
|
||||||
|
onSetConfidenceThreshold={setSegmentationConfidenceThreshold}
|
||||||
|
onRunSegmentation={runSegmentation}
|
||||||
|
onLoadRuns={() => loadSegmentationRuns()}
|
||||||
|
onSelectRun={setSelectedSegmentationRunId}
|
||||||
|
onSetClassFilter={setSegmentationClassFilter}
|
||||||
|
onSetMinConfidenceFilter={setSegmentationMinConfidenceFilter}
|
||||||
|
onLoadResults={() => loadSegmentationResults()}
|
||||||
|
onSelectReferenceDataset={setSegmentationReferenceDatasetId}
|
||||||
|
onRunQa={runSegmentationQa}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<DatasetDetailPanel
|
{activeWorkspace === 'exports' ? (
|
||||||
areas={areas}
|
<div className="workspace-grid workspace-grid-exports">
|
||||||
availableVectorTargets={availableVectorTargets}
|
<ExportCenter
|
||||||
selectedDatasetId={selectedDatasetId}
|
selectedProjectId={selectedProjectId}
|
||||||
selectedDataset={selectedDataset}
|
selectedDataset={selectedDataset}
|
||||||
selectedDatasetSummary={selectedDatasetSummary}
|
selectedDetectionRunId={selectedDetectionRunId}
|
||||||
selectedRasterMetadata={selectedRasterMetadata}
|
selectedSegmentationRunId={selectedSegmentationRunId}
|
||||||
selectedRasterStats={selectedRasterStats}
|
exports={exports}
|
||||||
rasterPreview={rasterPreview}
|
latestExport={latestExport}
|
||||||
rasterUnavailableMessage={rasterUnavailableMessage}
|
exportError={exportError}
|
||||||
selectedClipAreaId={selectedClipAreaId}
|
loadingExports={loadingExports}
|
||||||
selectedIntersectTargetId={selectedIntersectTargetId}
|
exporting={exporting}
|
||||||
rasterTileSize={rasterTileSize}
|
onRefresh={() => loadExports()}
|
||||||
rasterTileOverlap={rasterTileOverlap}
|
onExportDataset={exportSelectedDatasetGeoJson}
|
||||||
rasterTileOutputName={rasterTileOutputName}
|
onExportDetectionRun={exportSelectedDetectionRunGeoJson}
|
||||||
rasterReprojectCrs={rasterReprojectCrs}
|
onExportSegmentationRun={exportSelectedSegmentationRunGeoJson}
|
||||||
rasterReprojectResampling={rasterReprojectResampling}
|
onExportProjectMetadata={exportProjectMetadata}
|
||||||
ndviNirBand={ndviNirBand}
|
onExportProjectReport={exportProjectReport}
|
||||||
ndviRedBand={ndviRedBand}
|
onPreviewContent={previewExportContent}
|
||||||
ndwiGreenBand={ndwiGreenBand}
|
onDownload={downloadExportArtifact}
|
||||||
ndwiNirBand={ndwiNirBand}
|
/>
|
||||||
ndbiSwirBand={ndbiSwirBand}
|
<ExportPreview content={exportPreview} />
|
||||||
ndbiNirBand={ndbiNirBand}
|
</div>
|
||||||
isRasterTileInputValid={isRasterTileInputValid}
|
) : null}
|
||||||
jobs={jobs}
|
|
||||||
loadingDatasetDetails={loadingDatasetDetails}
|
{activeWorkspace === 'system' ? (
|
||||||
datasetDetailError={datasetDetailError}
|
<ProviderPanel
|
||||||
isVectorDatasetType={isVectorDatasetType}
|
providers={providers}
|
||||||
onSetSelectedClipAreaId={setSelectedClipAreaId}
|
loadingCapabilities={loadingCapabilities}
|
||||||
onSetSelectedIntersectTargetId={setSelectedIntersectTargetId}
|
capabilitiesError={capabilitiesError}
|
||||||
onSetRasterTileSize={setRasterTileSize}
|
onRefresh={loadCapabilities}
|
||||||
onSetRasterTileOverlap={setRasterTileOverlap}
|
/>
|
||||||
onSetRasterTileOutputName={setRasterTileOutputName}
|
) : null}
|
||||||
onSetRasterReprojectCrs={setRasterReprojectCrs}
|
</main>
|
||||||
onSetRasterReprojectResampling={setRasterReprojectResampling}
|
|
||||||
onSetNdviNirBand={setNdviNirBand}
|
<aside className="workbench-inspector" aria-label="Current selection inspector">
|
||||||
onSetNdviRedBand={setNdviRedBand}
|
<DatasetDetailPanel
|
||||||
onSetNdwiGreenBand={setNdwiGreenBand}
|
areas={areas}
|
||||||
onSetNdwiNirBand={setNdwiNirBand}
|
availableVectorTargets={availableVectorTargets}
|
||||||
onSetNdbiSwirBand={setNdbiSwirBand}
|
selectedDatasetId={selectedDatasetId}
|
||||||
onSetNdbiNirBand={setNdbiNirBand}
|
selectedDataset={selectedDataset}
|
||||||
onRunRasterInspect={runRasterInspect}
|
selectedDatasetSummary={selectedDatasetSummary}
|
||||||
onRunRasterPreview={runRasterPreview}
|
selectedRasterMetadata={selectedRasterMetadata}
|
||||||
onRunRasterStats={runRasterStats}
|
selectedRasterStats={selectedRasterStats}
|
||||||
onRunRasterReproject={runRasterReproject}
|
rasterPreview={rasterPreview}
|
||||||
onRunRasterClip={runRasterClip}
|
rasterUnavailableMessage={rasterUnavailableMessage}
|
||||||
onRunRasterTile={runRasterTile}
|
selectedClipAreaId={selectedClipAreaId}
|
||||||
onRunRasterNdvi={runRasterNdvi}
|
selectedIntersectTargetId={selectedIntersectTargetId}
|
||||||
onRunRasterNdwi={runRasterNdwi}
|
rasterTileSize={rasterTileSize}
|
||||||
onRunRasterNdbi={runRasterNdbi}
|
rasterTileOverlap={rasterTileOverlap}
|
||||||
onRunVectorClip={runVectorClip}
|
rasterTileOutputName={rasterTileOutputName}
|
||||||
onRunVectorBuffer={runVectorBuffer}
|
rasterReprojectCrs={rasterReprojectCrs}
|
||||||
onRunVectorIntersect={() => runVectorIntersect(availableVectorTargets)}
|
rasterReprojectResampling={rasterReprojectResampling}
|
||||||
onPickDerivedDataset={pickDerivedDataset}
|
ndviNirBand={ndviNirBand}
|
||||||
/>
|
ndviRedBand={ndviRedBand}
|
||||||
|
ndwiGreenBand={ndwiGreenBand}
|
||||||
|
ndwiNirBand={ndwiNirBand}
|
||||||
|
ndbiSwirBand={ndbiSwirBand}
|
||||||
|
ndbiNirBand={ndbiNirBand}
|
||||||
|
isRasterTileInputValid={isRasterTileInputValid}
|
||||||
|
jobs={jobs}
|
||||||
|
loadingDatasetDetails={loadingDatasetDetails}
|
||||||
|
datasetDetailError={datasetDetailError}
|
||||||
|
isVectorDatasetType={isVectorDatasetType}
|
||||||
|
onSetSelectedClipAreaId={setSelectedClipAreaId}
|
||||||
|
onSetSelectedIntersectTargetId={setSelectedIntersectTargetId}
|
||||||
|
onSetRasterTileSize={setRasterTileSize}
|
||||||
|
onSetRasterTileOverlap={setRasterTileOverlap}
|
||||||
|
onSetRasterTileOutputName={setRasterTileOutputName}
|
||||||
|
onSetRasterReprojectCrs={setRasterReprojectCrs}
|
||||||
|
onSetRasterReprojectResampling={setRasterReprojectResampling}
|
||||||
|
onSetNdviNirBand={setNdviNirBand}
|
||||||
|
onSetNdviRedBand={setNdviRedBand}
|
||||||
|
onSetNdwiGreenBand={setNdwiGreenBand}
|
||||||
|
onSetNdwiNirBand={setNdwiNirBand}
|
||||||
|
onSetNdbiSwirBand={setNdbiSwirBand}
|
||||||
|
onSetNdbiNirBand={setNdbiNirBand}
|
||||||
|
onRunRasterInspect={runRasterInspect}
|
||||||
|
onRunRasterPreview={runRasterPreview}
|
||||||
|
onRunRasterStats={runRasterStats}
|
||||||
|
onRunRasterReproject={runRasterReproject}
|
||||||
|
onRunRasterClip={runRasterClip}
|
||||||
|
onRunRasterTile={runRasterTile}
|
||||||
|
onRunRasterNdvi={runRasterNdvi}
|
||||||
|
onRunRasterNdwi={runRasterNdwi}
|
||||||
|
onRunRasterNdbi={runRasterNdbi}
|
||||||
|
onRunVectorClip={runVectorClip}
|
||||||
|
onRunVectorBuffer={runVectorBuffer}
|
||||||
|
onRunVectorIntersect={() => runVectorIntersect(availableVectorTargets)}
|
||||||
|
onPickDerivedDataset={pickDerivedDataset}
|
||||||
|
/>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -506,3 +506,355 @@ section li strong + div {
|
|||||||
margin-top: 0.6rem;
|
margin-top: 0.6rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workbench-shell {
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
padding: 0;
|
||||||
|
background: #eef4f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-topbar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 20;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(16rem, 26rem) minmax(0, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
padding: 0.85rem 1rem;
|
||||||
|
background: rgba(248, 251, 249, 0.96);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-block h1 {
|
||||||
|
font-size: 1.45rem;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-bar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-bar > div {
|
||||||
|
min-height: 4.2rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.62rem 0.72rem;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-bar span {
|
||||||
|
display: block;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-bar strong {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 14rem minmax(0, 1fr) 24rem;
|
||||||
|
gap: 0;
|
||||||
|
height: calc(100vh - 5.95rem);
|
||||||
|
min-height: 42rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-sidebar,
|
||||||
|
.workbench-inspector {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
border-color: var(--line);
|
||||||
|
background: #f8fbf9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-sidebar {
|
||||||
|
border-right: 1px solid var(--line);
|
||||||
|
padding: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-sidebar nav {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 4.2rem;
|
||||||
|
border-color: transparent;
|
||||||
|
padding: 0.66rem 0.72rem;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover:not(:disabled) {
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item span,
|
||||||
|
.nav-item small {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item span {
|
||||||
|
font-size: 0.96rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item small {
|
||||||
|
margin-top: 0.22rem;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.77rem;
|
||||||
|
font-weight: 550;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item-active {
|
||||||
|
border-color: rgba(15, 118, 110, 0.34);
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: inset 4px 0 0 var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-main {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-inspector {
|
||||||
|
border-left: 1px solid var(--line);
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-inspector > section {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-heading {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: end;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.85rem 1rem;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-heading h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-heading p:last-child {
|
||||||
|
max-width: 32rem;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-stack {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-grid {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-grid-data {
|
||||||
|
grid-template-columns: minmax(16rem, 0.75fr) minmax(18rem, 0.95fr) minmax(22rem, 1.35fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-grid-analysis,
|
||||||
|
.workspace-grid-ai,
|
||||||
|
.workspace-grid-exports {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-grid-data > section,
|
||||||
|
.workspace-grid-analysis > section,
|
||||||
|
.workspace-grid-ai > section,
|
||||||
|
.workspace-grid-exports > section,
|
||||||
|
.workspace-grid-data > section:nth-child(n),
|
||||||
|
.workspace-grid-analysis > section:nth-child(n),
|
||||||
|
.workspace-grid-ai > section:nth-child(n),
|
||||||
|
.workspace-grid-exports > section:nth-child(n) {
|
||||||
|
grid-column: auto;
|
||||||
|
max-height: none;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-grid-ai > section {
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(16rem, 24rem);
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-actions h2 {
|
||||||
|
max-width: 42rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-action-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-action-grid button {
|
||||||
|
min-height: 4.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section form,
|
||||||
|
section > div:not(.map-controls):not(.feature-inspector):not(.quick-action-grid) {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section button + button {
|
||||||
|
margin-top: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section table {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.86rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section th,
|
||||||
|
section td {
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
section th {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-inspector .job-result,
|
||||||
|
.workbench-inspector pre {
|
||||||
|
max-height: 16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-inspector h2 {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
margin: 0 -1rem 0.8rem;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
padding: 0.35rem 1rem 0.75rem;
|
||||||
|
background: #f8fbf9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-main .map-container {
|
||||||
|
height: calc(100vh - 23rem);
|
||||||
|
min-height: 28rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1280px) {
|
||||||
|
.workbench-layout {
|
||||||
|
grid-template-columns: 12rem minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-inspector {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.workbench-topbar,
|
||||||
|
.workbench-layout,
|
||||||
|
.workspace-grid-data,
|
||||||
|
.workspace-grid-analysis,
|
||||||
|
.workspace-grid-ai,
|
||||||
|
.workspace-grid-exports,
|
||||||
|
.overview-actions {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-layout {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: 5.8rem;
|
||||||
|
z-index: 10;
|
||||||
|
border-right: 0;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-sidebar nav,
|
||||||
|
.context-bar,
|
||||||
|
.quick-action-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
min-height: 3.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-heading,
|
||||||
|
.status-strip-header {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-heading p:last-child,
|
||||||
|
.status-next-action {
|
||||||
|
margin-top: 0.45rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-main .map-container {
|
||||||
|
height: 28rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 620px) {
|
||||||
|
.workbench-topbar,
|
||||||
|
.workbench-main,
|
||||||
|
.workbench-inspector,
|
||||||
|
.workbench-sidebar {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-bar,
|
||||||
|
.workbench-sidebar nav,
|
||||||
|
.quick-action-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-strip-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user