Polish map workspace density
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-20 19:59:01 +02:00
parent 1549fd8d7a
commit 8b4a8271ac
7 changed files with 308 additions and 110 deletions
+8
View File
@@ -7,6 +7,14 @@
# Changelog # Changelog
## Sprint 85 Map workspace density polish (2026-06-20)
- Added a compact Map workspace context summary for selected AOI, active layer and rendered feature state.
- Grouped map controls, provenance, map frame and feature inspector into clearer surfaces without changing MapLibre behavior.
- Tightened map toolbar/provenance spacing so Map workspace scans better on desktop and narrow screens.
- Added static regression coverage for Map workspace hierarchy and density contracts.
- No API contracts, migrations, backend behavior, provider fetching or AI model behavior changed.
## Sprint 84 Data workspace density polish (2026-06-20) ## Sprint 84 Data workspace density polish (2026-06-20)
- Added selected-summary regions to Project, AOI and Dataset panels so active context is visible before forms. - Added selected-summary regions to Project, AOI and Dataset panels so active context is visible before forms.
@@ -0,0 +1,48 @@
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_map_workspace_exposes_structured_surfaces() -> None:
map_workspace = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(
encoding="utf-8"
)
assert 'className="map-workspace-shell"' in map_workspace
assert 'className="map-context-summary"' in map_workspace
assert 'aria-label="Map layer status"' in map_workspace
assert 'className="map-control-surface"' in map_workspace
assert 'aria-label="Map workspace controls"' in map_workspace
assert 'className="map-frame-surface"' in map_workspace
assert 'className="map-inspection-surface"' in map_workspace
def test_map_workspace_summary_uses_current_layer_and_selection_state() -> None:
map_workspace = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text(
encoding="utf-8"
)
assert "const selectedMapArea = areas.find" in map_workspace
assert "selectedMapArea?.name ?? 'No area selected'" in map_workspace
assert "mapLayerLabel" in map_workspace
assert "mapLayerSourceLabel" in map_workspace
assert "mapFeatureCount" in map_workspace
assert "areaFeatureCount" in map_workspace
def test_map_workspace_density_css_contracts() -> None:
css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8")
assert ".map-workspace-shell" in css
assert ".map-context-summary" in css
assert ".map-context-summary > div" in css
assert ".map-control-surface" in css
assert ".map-frame-surface" in css
assert ".map-inspection-surface" in css
assert ".map-workspace-shell .map-toolbar" in css
assert "grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));" in css
assert "background: linear-gradient(180deg, #ffffff, #f7fbf8);" in css
assert "border-left: 4px solid var(--accent);" in css
+27
View File
@@ -3074,3 +3074,30 @@ Limitations:
Next recommended pass: Next recommended pass:
- Continue with Map workspace panel hierarchy and layer-control density after this pass is deployed and visually checked. - Continue with Map workspace panel hierarchy and layer-control density after this pass is deployed and visually checked.
## Sprint 85 Map workspace density polish (2026-06-20)
Changed:
- Added a compact Map workspace context summary for selected AOI, active layer and rendered feature state.
- Wrapped existing layer controls and provenance in a dedicated `map-control-surface`.
- Wrapped the existing MapLibre component in a `map-frame-surface` and the selected-feature inspector in a `map-inspection-surface`.
- Added `backend/tests/test_sprint85_map_workspace_density.py`.
- Updated `frontend/README.md`, `docs/TODO.md` and `CHANGELOG.md`.
Tested:
- Live browser pre-check against `http://192.168.10.150:1202` showed Map workspace had no console errors or horizontal overflow, but controls, provenance, map frame and inspector still read as a loose vertical stack.
- Red step: `python -m pytest backend/tests/test_sprint85_map_workspace_density.py -q` failed on missing Map workspace surface and context-summary contracts.
- `python -m pytest backend/tests/test_sprint85_map_workspace_density.py -q` (`3 passed`)
- `python -m pytest backend/tests/test_sprint85_map_workspace_density.py backend/tests/test_sprint63_map_overlay_ergonomics.py backend/tests/test_sprint67_map_empty_state_quick_actions.py backend/tests/test_sprint74_data_map_mobile_polish.py backend/tests/test_sprint47_workbench_interaction_smoke.py -q` (`13 passed`)
- `cd frontend && npm run typecheck`
- `cd frontend && npm run build`
- Full readiness and Tower deploy pending in this pass.
Open:
- None known before full readiness/deploy validation.
Limitations:
- Frontend Map workspace presentation hierarchy only; no overlay behavior, API contract, persistence, migration, provider fetching or AI/model changes.
Next recommended pass:
- Continue with QA/QC workspace result hierarchy and filter density after this pass is deployed and visually checked.
+1
View File
@@ -74,6 +74,7 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Add compact shell density polish for topbar context, mobile navigation and workspace skip flow. - [x] Add compact shell density polish for topbar context, mobile navigation and workspace skip flow.
- [x] Add Overview workspace panel hierarchy polish for readiness and recommended-action regions. - [x] Add Overview workspace panel hierarchy polish for readiness and recommended-action regions.
- [x] Add Data workspace selected-summary and panel density polish. - [x] Add Data workspace selected-summary and panel density polish.
- [x] Add Map workspace panel hierarchy and layer-control density polish.
## Sprint 8 status ## Sprint 8 status
+2
View File
@@ -6,6 +6,8 @@ The workbench now uses a task-based shell instead of a single long panel stack.
Data and Map workspaces include mobile-density CSS for file inputs, dataset action buttons, map toolbar controls, range sliders and empty-map quick actions so narrow screens avoid accidental viewport overflow. Data and Map workspaces include mobile-density CSS for file inputs, dataset action buttons, map toolbar controls, range sliders and empty-map quick actions so narrow screens avoid accidental viewport overflow.
Map workspace now surfaces the selected AOI, active layer and rendered feature state before controls, then separates layer controls, provenance, the MapLibre frame and the feature inspector into focused surfaces. The underlying overlay, opacity and feature-selection behavior is unchanged.
AI Labs uses the same mobile-density baseline for Detection and Segmentation model cards, run forms, QA/result summaries and scroll-safe result tables. This keeps long model ids, tile paths and artifact paths from widening the workbench on phones. AI Labs uses the same mobile-density baseline for Detection and Segmentation model cards, run forms, QA/result summaries and scroll-safe result tables. This keeps long model ids, tile paths and artifact paths from widening the workbench on phones.
Exports and System provider capabilities now use compact, mobile-safe cards for handoff actions, artifact filters and provider metadata. Long provider limitations, export ids and artifact paths wrap inside their cards instead of widening the workbench. Exports and System provider capabilities now use compact, mobile-safe cards for handoff actions, artifact filters and provider metadata. Long provider limitations, export ids and artifact paths wrap inside their cards instead of widening the workbench.
+135 -107
View File
@@ -50,6 +50,7 @@ export function MapWorkspace({
onSetMapLayerOpacity, onSetMapLayerOpacity,
onSelectMapFeature, onSelectMapFeature,
}: MapWorkspaceProps): JSX.Element { }: MapWorkspaceProps): JSX.Element {
const selectedMapArea = areas.find((area) => area.id === selectedMapAreaId)
const featureProperties = selectedMapFeature?.properties ?? null const featureProperties = selectedMapFeature?.properties ?? null
const featureSummaryEntries = featureProperties const featureSummaryEntries = featureProperties
? Object.entries(featureProperties) ? Object.entries(featureProperties)
@@ -58,7 +59,7 @@ export function MapWorkspace({
: [] : []
return ( return (
<section data-testid="map-workspace"> <section className="map-workspace-shell" data-testid="map-workspace">
<div className="panel-title-row"> <div className="panel-title-row">
<div> <div>
<p className="eyebrow">Spatial review</p> <p className="eyebrow">Spatial review</p>
@@ -67,89 +68,110 @@ export function MapWorkspace({
<span className="count-pill">{mapFeatureCollection ? `${mapFeatureCount} features` : 'no layer'}</span> <span className="count-pill">{mapFeatureCollection ? `${mapFeatureCount} features` : 'no layer'}</span>
</div> </div>
<div className="map-toolbar"> <div className="map-context-summary" aria-label="Map layer status">
<label> <div>
Area <span>Area of interest</span>
<select <strong>{selectedMapArea?.name ?? 'No area selected'}</strong>
value={selectedMapAreaId} <small>{areaFeatureCollection ? `${areaFeatureCount} AOI features loaded` : 'AOI overlay disabled'}</small>
onChange={(event) => onSelectMapArea(event.target.value)}
disabled={areas.length === 0}
data-testid="map-area-select"
>
<option value="">No area</option>
{areas.map((area) => (
<option key={area.id} value={area.id}>
{area.name}
</option>
))}
</select>
</label>
<div className="layer-control-card">
<label className="checkbox-row">
<input
checked={areaLayerVisible}
disabled={!areaFeatureCollection}
type="checkbox"
onChange={(event) => onSetAreaLayerVisible(event.target.checked)}
data-testid="map-area-visible"
/>
AOI
</label>
<input
aria-label="Area opacity"
disabled={!areaFeatureCollection}
max="0.7"
min="0.05"
step="0.05"
type="range"
value={areaLayerOpacity}
onChange={(event) => onSetAreaLayerOpacity(Number(event.target.value))}
data-testid="map-area-opacity"
/>
</div> </div>
<div className="layer-control-card"> <div>
<label className="checkbox-row"> <span>Active layer</span>
<input
checked={mapLayerVisible}
disabled={!mapFeatureCollection}
type="checkbox"
onChange={(event) => onSetMapLayerVisible(event.target.checked)}
data-testid="map-layer-visible"
/>
Active layer
</label>
<input
aria-label="Layer opacity"
disabled={!mapFeatureCollection}
max="1"
min="0.05"
step="0.05"
type="range"
value={mapLayerOpacity}
onChange={(event) => onSetMapLayerOpacity(Number(event.target.value))}
data-testid="map-layer-opacity"
/>
</div>
<div className="map-status">
<strong>{mapLayerLabel}</strong> <strong>{mapLayerLabel}</strong>
<span>{areaFeatureCollection ? `${areaFeatureCount} AOI loaded` : 'No AOI loaded'}</span> <small>{mapLayerSourceLabel}</small>
<span>{mapFeatureCollection ? `${mapFeatureCount} features loaded` : 'No vector/result layer loaded'}</span> </div>
<div>
<span>Feature state</span>
<strong>{mapFeatureCollection ? `${mapFeatureCount} rendered features` : 'No layer rendered'}</strong>
<small>{mapLayerProvenance}</small>
</div> </div>
</div> </div>
<div className="layer-provenance-rail" aria-label="Active map layer provenance">
<div> <div className="map-control-surface" aria-label="Map workspace controls">
<span>Layer source</span> <div className="map-toolbar">
<strong>{mapLayerSourceLabel}</strong> <label>
Area
<select
value={selectedMapAreaId}
onChange={(event) => onSelectMapArea(event.target.value)}
disabled={areas.length === 0}
data-testid="map-area-select"
>
<option value="">No area</option>
{areas.map((area) => (
<option key={area.id} value={area.id}>
{area.name}
</option>
))}
</select>
</label>
<div className="layer-control-card">
<label className="checkbox-row">
<input
checked={areaLayerVisible}
disabled={!areaFeatureCollection}
type="checkbox"
onChange={(event) => onSetAreaLayerVisible(event.target.checked)}
data-testid="map-area-visible"
/>
AOI
</label>
<input
aria-label="Area opacity"
disabled={!areaFeatureCollection}
max="0.7"
min="0.05"
step="0.05"
type="range"
value={areaLayerOpacity}
onChange={(event) => onSetAreaLayerOpacity(Number(event.target.value))}
data-testid="map-area-opacity"
/>
</div>
<div className="layer-control-card">
<label className="checkbox-row">
<input
checked={mapLayerVisible}
disabled={!mapFeatureCollection}
type="checkbox"
onChange={(event) => onSetMapLayerVisible(event.target.checked)}
data-testid="map-layer-visible"
/>
Active layer
</label>
<input
aria-label="Layer opacity"
disabled={!mapFeatureCollection}
max="1"
min="0.05"
step="0.05"
type="range"
value={mapLayerOpacity}
onChange={(event) => onSetMapLayerOpacity(Number(event.target.value))}
data-testid="map-layer-opacity"
/>
</div>
<div className="map-status">
<strong>{mapLayerLabel}</strong>
<span>{areaFeatureCollection ? `${areaFeatureCount} AOI loaded` : 'No AOI loaded'}</span>
<span>{mapFeatureCollection ? `${mapFeatureCount} features loaded` : 'No vector/result layer loaded'}</span>
</div>
</div> </div>
<div> <div className="layer-provenance-rail" aria-label="Active map layer provenance">
<span>Provenance</span> <div>
<strong>{mapLayerProvenance}</strong> <span>Layer source</span>
</div> <strong>{mapLayerSourceLabel}</strong>
<div> </div>
<span>Draw state</span> <div>
<strong>{mapFeatureCollection ? `${mapFeatureCount} rendered features` : 'No active vector or result layer'}</strong> <span>Provenance</span>
<strong>{mapLayerProvenance}</strong>
</div>
<div>
<span>Draw state</span>
<strong>{mapFeatureCollection ? `${mapFeatureCount} rendered features` : 'No active vector or result layer'}</strong>
</div>
</div> </div>
</div> </div>
{!mapFeatureCollection ? ( {!mapFeatureCollection ? (
<div className="empty-state map-empty-state"> <div className="empty-state map-empty-state">
<strong>No active vector or result layer</strong> <strong>No active vector or result layer</strong>
@@ -171,37 +193,43 @@ export function MapWorkspace({
)} )}
</div> </div>
) : null} ) : null}
<GeoMap
data={mapFeatureCollection} <div className="map-frame-surface">
areaData={areaFeatureCollection} <GeoMap
visible={mapLayerVisible} data={mapFeatureCollection}
opacity={mapLayerOpacity} areaData={areaFeatureCollection}
areaVisible={areaLayerVisible} visible={mapLayerVisible}
areaOpacity={areaLayerOpacity} opacity={mapLayerOpacity}
onFeatureSelect={onSelectMapFeature} areaVisible={areaLayerVisible}
/> areaOpacity={areaLayerOpacity}
<div className="feature-inspector"> onFeatureSelect={onSelectMapFeature}
<div className="panel-title-row"> />
<h3>Feature inspector</h3> </div>
<span className="count-pill">{selectedMapFeature?.geometry?.type ?? 'none'}</span>
<div className="map-inspection-surface">
<div className="feature-inspector">
<div className="panel-title-row">
<h3>Feature inspector</h3>
<span className="count-pill">{selectedMapFeature?.geometry?.type ?? 'none'}</span>
</div>
{selectedMapFeature ? (
<>
{featureSummaryEntries.length > 0 ? (
<div className="feature-summary-grid" aria-label="Selected feature summary">
{featureSummaryEntries.map(([key, value]) => (
<div className="feature-property-chip" key={key}>
<span>{key}</span>
<strong>{String(value)}</strong>
</div>
))}
</div>
) : null}
<pre className="job-result">{JSON.stringify(selectedMapFeature.properties ?? {}, null, 2)}</pre>
</>
) : (
<p className="muted">Click a visible map feature to inspect its properties.</p>
)}
</div> </div>
{selectedMapFeature ? (
<>
{featureSummaryEntries.length > 0 ? (
<div className="feature-summary-grid" aria-label="Selected feature summary">
{featureSummaryEntries.map(([key, value]) => (
<div className="feature-property-chip" key={key}>
<span>{key}</span>
<strong>{String(value)}</strong>
</div>
))}
</div>
) : null}
<pre className="job-result">{JSON.stringify(selectedMapFeature.properties ?? {}, null, 2)}</pre>
</>
) : (
<p className="muted">Click a visible map feature to inspect its properties.</p>
)}
</div> </div>
</section> </section>
) )
+87 -3
View File
@@ -2007,12 +2007,96 @@ button.entity-card {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.map-toolbar { .map-workspace-shell {
display: grid;
gap: 0.75rem;
}
.map-context-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
gap: 0.6rem;
}
.map-context-summary > div {
min-width: 0;
border: 1px solid var(--line);
border-left: 4px solid var(--accent);
border-radius: 8px;
padding: 0.68rem 0.78rem;
background: linear-gradient(180deg, #ffffff, #f7fbf8);
}
.map-context-summary span,
.map-context-summary small {
display: block;
}
.map-context-summary span {
color: var(--muted);
font-size: 0.68rem;
font-weight: 850;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.map-context-summary strong {
display: block;
margin-top: 0.2rem;
overflow: hidden;
color: var(--ink);
font-size: 0.98rem;
line-height: 1.22;
text-overflow: ellipsis;
white-space: nowrap;
}
.map-context-summary small {
margin-top: 0.25rem;
overflow: hidden;
color: var(--muted);
font-size: 0.78rem;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.map-control-surface,
.map-frame-surface,
.map-inspection-surface {
min-width: 0;
border: 1px solid var(--line);
border-radius: 8px;
padding: 0.72rem;
background: #ffffff;
}
.map-control-surface {
display: grid;
gap: 0.68rem;
}
.map-frame-surface {
padding: 0.52rem;
background: #f7fbf8;
}
.map-frame-surface .map-container {
margin: 0;
}
.map-inspection-surface .feature-inspector {
margin-top: 0;
border-top: 0;
padding-top: 0;
}
.map-workspace-shell .map-toolbar {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
gap: 0.75rem; gap: 0.75rem;
align-items: end; align-items: end;
margin-bottom: 0.85rem; margin-bottom: 0;
} }
.layer-control-card { .layer-control-card {
@@ -2049,7 +2133,7 @@ button.entity-card {
display: grid; display: grid;
grid-template-columns: 0.85fr minmax(0, 1.5fr) 0.85fr; grid-template-columns: 0.85fr minmax(0, 1.5fr) 0.85fr;
gap: 0.65rem; gap: 0.65rem;
margin-bottom: 0.85rem; margin-bottom: 0;
} }
.layer-provenance-rail > div, .layer-provenance-rail > div,