Polish data 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 06:45:54 +02:00
parent fc3ddf3c49
commit 5a00d3525b
9 changed files with 400 additions and 220 deletions
+8
View File
@@ -7,6 +7,14 @@
# Changelog # Changelog
## 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.
- Split Data workspace panels into named form/list/catalog blocks to reduce form-first scanning friction.
- Restyled dataset upload as an embedded source-data block while preserving the existing upload flow.
- Added static regression coverage for Data workspace selected-summary, upload and catalog density contracts.
- No API contracts, migrations, backend behavior, provider fetching or AI model behavior changed.
## Sprint 83 Workspace panel hierarchy polish (2026-06-20) ## Sprint 83 Workspace panel hierarchy polish (2026-06-20)
- Made the Overview readiness strip a calmer section surface with tighter status tiles. - Made the Overview readiness strip a calmer section surface with tighter status tiles.
@@ -0,0 +1,49 @@
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_project_and_area_panels_expose_selected_summary_regions() -> None:
project_panel = (ROOT / "frontend" / "src" / "components" / "project" / "ProjectPanel.tsx").read_text(
encoding="utf-8"
)
area_panel = (ROOT / "frontend" / "src" / "components" / "project" / "AreaPanel.tsx").read_text(
encoding="utf-8"
)
assert "const selectedProject = projects.find" in project_panel
assert 'className="data-selection-summary data-selection-summary-project"' in project_panel
assert 'className="data-panel-form-block"' in project_panel
assert 'className="data-panel-list-block"' in project_panel
assert "const selectedArea = areas.find" in area_panel
assert 'className="data-selection-summary data-selection-summary-area"' in area_panel
assert 'className="data-panel-form-block"' in area_panel
assert 'className="data-panel-list-block"' in area_panel
def test_dataset_panel_separates_selected_upload_and_catalog_regions() -> None:
dataset_panel = (ROOT / "frontend" / "src" / "components" / "datasets" / "DatasetPanel.tsx").read_text(
encoding="utf-8"
)
assert "const selectedDataset = datasets.find" in dataset_panel
assert 'className="data-selection-summary data-selection-summary-dataset"' in dataset_panel
assert 'className="dataset-upload-block data-panel-form-block"' in dataset_panel
assert 'className="data-panel-list-block dataset-catalog-block"' in dataset_panel
assert 'className="data-section-label"' in dataset_panel
def test_data_workspace_density_css_contracts() -> None:
css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8")
assert ".data-selection-summary" in css
assert ".data-selection-summary strong" in css
assert ".data-panel-form-block" in css
assert ".data-panel-list-block" in css
assert ".data-section-label" in css
assert "grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));" in css
assert "background: linear-gradient(180deg, #ffffff, #f7fbf8);" in css
assert "border-left: 4px solid var(--accent);" in css
+27
View File
@@ -3044,3 +3044,30 @@ Limitations:
Next recommended pass: Next recommended pass:
- Continue with Data workspace panel hierarchy and selected-item density after this pass is deployed and visually checked. - Continue with Data workspace panel hierarchy and selected-item density after this pass is deployed and visually checked.
## Sprint 84 Data workspace density polish (2026-06-20)
Changed:
- Added selected-summary regions to Project, AOI and Dataset panels so the active data context appears before creation/upload forms.
- Split Project and AOI panels into `data-panel-form-block` and `data-panel-list-block` regions.
- Split Dataset catalog into selected dataset, upload source data and available dataset regions.
- Restyled dataset upload as an embedded source-data block while preserving the existing upload flow.
- Added `backend/tests/test_sprint84_data_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 Data workspace had no console errors or horizontal overflow, but the first Data viewport was still form-first and visually heavy.
- Red step: `python -m pytest backend/tests/test_sprint84_data_workspace_density.py -q` failed on missing selected-summary and named form/list/catalog regions.
- `python -m pytest backend/tests/test_sprint84_data_workspace_density.py -q` (`3 passed`)
- `python -m pytest backend/tests/test_sprint84_data_workspace_density.py backend/tests/test_sprint83_workspace_panel_hierarchy.py backend/tests/test_sprint50_workspace_usability_polish.py backend/tests/test_sprint29_dataset_components.py backend/tests/test_sprint68_dataset_catalog_density.py backend/tests/test_sprint69_dataset_action_polish.py backend/tests/test_sprint47_workbench_interaction_smoke.py -q` (`20 passed`)
- `cd frontend && npm run typecheck`
- `cd frontend && npm run build`
Open:
- Full readiness, deployment and live browser verification still need to run for this pass.
Limitations:
- Frontend Data workspace presentation hierarchy only; no upload behavior, API contract, persistence, migration, provider fetching or AI/model changes.
Next recommended pass:
- Continue with Map workspace panel hierarchy and layer-control density after this pass is deployed and visually checked.
+1
View File
@@ -73,6 +73,7 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Add compact loading/error/empty/result state polish across QA, exports and AI labs. - [x] Add compact loading/error/empty/result state polish across QA, exports and AI labs.
- [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.
## Sprint 8 status ## Sprint 8 status
+1
View File
@@ -238,6 +238,7 @@ QA/QC, exports and AI lab result panels use shared loading, error, empty and rea
- The workbench shell includes a compact command bar, consistent raised/sunken surfaces, structured empty states and scroll-safe AI result tables to keep the V1 workflow usable across desktop and mobile widths. - The workbench shell includes a compact command bar, consistent raised/sunken surfaces, structured empty states and scroll-safe AI result tables to keep the V1 workflow usable across desktop and mobile widths.
- The shell includes a keyboard skip link, an explicit primary workspace navigation label and compact horizontal context/navigation rails on narrow screens so active workspace content is reachable without a tall preamble. - The shell includes a keyboard skip link, an explicit primary workspace navigation label and compact horizontal context/navigation rails on narrow screens so active workspace content is reachable without a tall preamble.
- The Overview workspace separates readiness, status tiles and recommended next actions into distinct visual hierarchy regions, keeping the first screen scannable without changing workflow behavior. - The Overview workspace separates readiness, status tiles and recommended next actions into distinct visual hierarchy regions, keeping the first screen scannable without changing workflow behavior.
- The Data workspace surfaces selected project, AOI and dataset context before creation/upload forms, then separates form and catalog/list regions for faster scanning.
- The Export Center includes a handoff readiness summary, grouped artifact actions and provenance-rich export cards so report/GeoJSON handoff stays understandable in long-running demo projects. - The Export Center includes a handoff readiness summary, grouped artifact actions and provenance-rich export cards so report/GeoJSON handoff stays understandable in long-running demo projects.
## Workbench shell refactor ## Workbench shell refactor
@@ -105,6 +105,7 @@ export function DatasetPanel({
onOpenDatasetExport, onOpenDatasetExport,
}: DatasetPanelProps) { }: DatasetPanelProps) {
const readyDatasets = datasets.filter((dataset) => dataset.status === 'ready').length const readyDatasets = datasets.filter((dataset) => dataset.status === 'ready').length
const selectedDataset = datasets.find((dataset) => dataset.id === selectedDatasetId)
const roleSummaries = [ const roleSummaries = [
{ {
key: 'selected', key: 'selected',
@@ -146,6 +147,14 @@ export function DatasetPanel({
<span className="count-pill">{readyDatasets}/{datasets.length} ready</span> <span className="count-pill">{readyDatasets}/{datasets.length} ready</span>
</div> </div>
<div className="data-selection-summary data-selection-summary-dataset">
<span>Selected dataset</span>
<strong>{selectedDataset?.name ?? 'No dataset selected'}</strong>
<small>{selectedDataset ? `${datasetRoleLabel(normalizeDatasetRole(selectedDataset))} / ${selectedDataset.status}` : 'Inspect a dataset to load details and tools.'}</small>
</div>
<div className="dataset-upload-block data-panel-form-block">
<p className="data-section-label">Upload source data</p>
<form className="dataset-upload-form" onSubmit={onUploadDataset}> <form className="dataset-upload-form" onSubmit={onUploadDataset}>
<label> <label>
Type Type
@@ -192,6 +201,7 @@ export function DatasetPanel({
Upload dataset Upload dataset
</button> </button>
</form> </form>
</div>
<div className="dataset-role-summary-grid" aria-label="Dataset role summary"> <div className="dataset-role-summary-grid" aria-label="Dataset role summary">
{roleSummaries.map((summary) => ( {roleSummaries.map((summary) => (
@@ -210,6 +220,8 @@ export function DatasetPanel({
<p>Upload vector, GeoJSON or raster data after selecting a project.</p> <p>Upload vector, GeoJSON or raster data after selecting a project.</p>
</div> </div>
) : null} ) : null}
<div className="data-panel-list-block dataset-catalog-block">
<p className="data-section-label">Available datasets</p>
<ul className="dataset-list"> <ul className="dataset-list">
{datasets.map((dataset) => { {datasets.map((dataset) => {
const datasetRole = normalizeDatasetRole(dataset) const datasetRole = normalizeDatasetRole(dataset)
@@ -282,6 +294,7 @@ export function DatasetPanel({
) )
})} })}
</ul> </ul>
</div>
</section> </section>
) )
} }
@@ -30,6 +30,8 @@ export function AreaPanel({
onUpdateAreaForm, onUpdateAreaForm,
onSelectMapArea, onSelectMapArea,
}: AreaPanelProps): JSX.Element { }: AreaPanelProps): JSX.Element {
const selectedArea = areas.find((area) => area.id === selectedMapAreaId)
return ( return (
<section data-testid="area-panel"> <section data-testid="area-panel">
<div className="panel-title-row"> <div className="panel-title-row">
@@ -41,6 +43,14 @@ export function AreaPanel({
</div> </div>
<p className="muted">{selectedProject ? `Project: ${selectedProject.name}` : 'Select a project first'}</p> <p className="muted">{selectedProject ? `Project: ${selectedProject.name}` : 'Select a project first'}</p>
<div className="data-selection-summary data-selection-summary-area">
<span>Shown on map</span>
<strong>{selectedArea?.name ?? 'No AOI selected'}</strong>
<small>{selectedArea?.area_m2 ? `${selectedArea.area_m2.toFixed(2)} m2` : 'Select an AOI with geometry for spatial review.'}</small>
</div>
<div className="data-panel-form-block">
<p className="data-section-label">Create spatial scope</p>
<form className="compact-form" onSubmit={onCreateArea}> <form className="compact-form" onSubmit={onCreateArea}>
<label> <label>
AOI name AOI name
@@ -70,9 +80,12 @@ export function AreaPanel({
Create area Create area
</button> </button>
</form> </form>
</div>
{loadingAreas ? <p>Loading areas...</p> : null} {loadingAreas ? <p>Loading areas...</p> : null}
{areas.length === 0 ? <p>No areas yet</p> : null} {areas.length === 0 ? <p>No areas yet</p> : null}
<div className="data-panel-list-block">
<p className="data-section-label">Saved AOIs</p>
<ul className="entity-list"> <ul className="entity-list">
{areas.map((area) => ( {areas.map((area) => (
<li className={selectedMapAreaId === area.id ? 'entity-card entity-card-active' : 'entity-card'} key={area.id}> <li className={selectedMapAreaId === area.id ? 'entity-card entity-card-active' : 'entity-card'} key={area.id}>
@@ -93,6 +106,7 @@ export function AreaPanel({
</li> </li>
))} ))}
</ul> </ul>
</div>
</section> </section>
) )
} }
@@ -26,6 +26,8 @@ export function ProjectPanel({
onSelectProject, onSelectProject,
onLoadDemoWorkflow, onLoadDemoWorkflow,
}: ProjectPanelProps): JSX.Element { }: ProjectPanelProps): JSX.Element {
const selectedProject = projects.find((project) => project.id === selectedProjectId)
return ( return (
<section className="workspace-panel" data-testid="project-panel"> <section className="workspace-panel" data-testid="project-panel">
<div className="panel-title-row"> <div className="panel-title-row">
@@ -37,6 +39,14 @@ export function ProjectPanel({
</div> </div>
{loadingProjects ? <p>Loading projects...</p> : null} {loadingProjects ? <p>Loading projects...</p> : null}
<div className="data-selection-summary data-selection-summary-project">
<span>Selected project</span>
<strong>{selectedProject?.name ?? 'No active project'}</strong>
<small>{selectedProject?.region ?? 'Create or load a project to start the workbench.'}</small>
</div>
<div className="data-panel-form-block">
<p className="data-section-label">Create context</p>
<form className="compact-form" onSubmit={onCreateProject}> <form className="compact-form" onSubmit={onCreateProject}>
<label> <label>
Name Name
@@ -66,6 +76,7 @@ export function ProjectPanel({
Create project Create project
</button> </button>
</form> </form>
</div>
<div className="demo-actions"> <div className="demo-actions">
<button className="secondary-action" type="button" onClick={onLoadDemoWorkflow} disabled={loadingDemoWorkflow} data-testid="load-demo-workflow"> <button className="secondary-action" type="button" onClick={onLoadDemoWorkflow} disabled={loadingDemoWorkflow} data-testid="load-demo-workflow">
@@ -74,6 +85,8 @@ export function ProjectPanel({
{demoWorkflowMessage ? <p>{demoWorkflowMessage}</p> : null} {demoWorkflowMessage ? <p>{demoWorkflowMessage}</p> : null}
</div> </div>
<div className="data-panel-list-block">
<p className="data-section-label">Saved projects</p>
<ul className="entity-list"> <ul className="entity-list">
{projects.map((project) => ( {projects.map((project) => (
<li key={project.id}> <li key={project.id}>
@@ -96,6 +109,7 @@ export function ProjectPanel({
</li> </li>
) : null} ) : null}
</ul> </ul>
</div>
</section> </section>
) )
} }
+57 -4
View File
@@ -1019,15 +1019,68 @@ section th {
gap: 0.7rem; gap: 0.7rem;
} }
.dataset-upload-form { .data-selection-summary {
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); display: grid;
align-items: end; grid-template-columns: minmax(0, 1fr);
gap: 0.16rem;
border: 1px solid rgba(15, 118, 110, 0.26);
border-left: 4px solid var(--accent);
border-radius: 8px;
padding: 0.68rem 0.75rem;
background: linear-gradient(180deg, #ffffff, #f7fbf8);
box-shadow: 0 1px 0 rgba(19, 32, 24, 0.03);
}
.data-selection-summary span,
.data-selection-summary small,
.data-section-label {
color: var(--muted);
font-size: 0.74rem;
font-weight: 850;
letter-spacing: 0.04em;
line-height: 1.2;
text-transform: uppercase;
}
.data-selection-summary strong {
min-width: 0;
overflow-wrap: anywhere;
font-size: 0.98rem;
line-height: 1.24;
}
.data-selection-summary small {
letter-spacing: 0;
text-transform: none;
}
.data-panel-form-block,
.data-panel-list-block {
display: grid;
gap: 0.56rem;
min-width: 0;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: 8px;
padding: 0.85rem; padding: 0.72rem;
background: var(--panel-soft); background: var(--panel-soft);
} }
.data-panel-list-block {
background: #ffffff;
}
.data-section-label {
margin: 0;
}
.dataset-upload-form {
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
align-items: end;
border: 0;
padding: 0;
background: transparent;
}
.dataset-upload-form label, .dataset-upload-form label,
.file-input-label { .file-input-label {
min-width: 0; min-width: 0;