Polish operation form readability
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 02:39:44 +02:00
parent 4e6ba707cf
commit b15b123173
8 changed files with 315 additions and 121 deletions
+8
View File
@@ -7,6 +7,14 @@
# Changelog # Changelog
## Sprint 80 Operation form readability polish (2026-06-20)
- Added structured headings, helper text, field wrappers and action rows to dense raster operation controls.
- Added the same form readability structure to vector clip, buffer and intersect controls.
- Added compact CSS contracts for dataset tool headings, helper text, field grids, action rows and inline error blocks.
- Added static regression coverage for raster/vector operation form readability contracts.
- No API contracts, migrations, backend behavior, provider fetching or AI model behavior changed.
## Sprint 79 Accessibility focus polish (2026-06-20) ## Sprint 79 Accessibility focus polish (2026-06-20)
- Added a shared visible focus-ring contract for primary buttons, workspace navigation, command chips, inspector tabs and dataset action buttons. - Added a shared visible focus-ring contract for primary buttons, workspace navigation, command chips, inspector tabs and dataset action buttons.
@@ -0,0 +1,49 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_dataset_operation_form_css_contracts() -> None:
css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8")
assert ".dataset-tool-heading" in css
assert ".dataset-tool-helper" in css
assert ".dataset-tool-field" in css
assert ".dataset-tool-label" in css
assert ".dataset-tool-error" in css
assert ".dataset-tool-action-row" in css
assert ".dataset-tool-grid" in css
assert "grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));" in css
assert "overflow-wrap: anywhere;" in css
def test_raster_controls_expose_readable_operation_groups() -> None:
raster_controls = (ROOT / "frontend" / "src" / "components" / "datasets" / "RasterControls.tsx").read_text(
encoding="utf-8"
)
assert 'className="dataset-tool-heading"' in raster_controls
assert 'className="dataset-tool-helper"' in raster_controls
assert 'className="dataset-tool-grid"' in raster_controls
assert 'className="dataset-tool-field"' in raster_controls
assert 'className="dataset-tool-label"' in raster_controls
assert 'className="dataset-tool-action-row"' in raster_controls
assert 'className="dataset-tool-error"' in raster_controls
assert "Target CRS for the derived raster artifact." in raster_controls
assert "Tile size must be > 0." in raster_controls
def test_vector_controls_expose_readable_operation_groups() -> None:
vector_controls = (ROOT / "frontend" / "src" / "components" / "datasets" / "VectorControls.tsx").read_text(
encoding="utf-8"
)
assert 'className="dataset-tool-heading"' in vector_controls
assert 'className="dataset-tool-helper"' in vector_controls
assert 'className="dataset-tool-grid"' in vector_controls
assert 'className="dataset-tool-field"' in vector_controls
assert 'className="dataset-tool-label"' in vector_controls
assert 'className="dataset-tool-action-row"' in vector_controls
assert "Clip features to the selected project area." in vector_controls
assert "Intersect with another persisted vector dataset." in vector_controls
+26
View File
@@ -2929,3 +2929,29 @@ Limitations:
Next recommended pass: Next recommended pass:
- Continue with form-level validation/readability polish for dense raster/vector operation panels. - Continue with form-level validation/readability polish for dense raster/vector operation panels.
## Sprint 80 Operation form readability polish (2026-06-20)
Changed:
- Added structured headings, helper text, field wrappers, responsive field grids and action rows to raster operation controls.
- Added equivalent readability structure to vector clip, buffer and intersect controls.
- Added inline dataset tool error styling for raster clip/tile validation messages.
- Added `backend/tests/test_sprint80_operation_form_readability.py`.
- Updated `frontend/README.md`, `docs/TODO.md` and `CHANGELOG.md`.
Tested:
- Red step: `python -m pytest backend/tests/test_sprint80_operation_form_readability.py -q` failed on missing form readability CSS and markup contracts.
- `python -m pytest backend/tests/test_sprint80_operation_form_readability.py -q` (`3 passed`)
- `python -m pytest backend/tests/test_sprint80_operation_form_readability.py backend/tests/test_sprint77_inspector_mobile_polish.py backend/tests/test_sprint29_dataset_components.py backend/tests/test_sprint28_dataset_workflow_hook.py backend/tests/test_vector_operations_service.py backend/tests/test_raster_operations_service.py -q` (`37 passed`)
- `cd frontend && npm run typecheck`
- `cd frontend && npm run build`
- `bash scripts/run_readiness_check.sh` (`273 passed`; frontend typecheck/build passed; Alembic single head `202606120900`)
Open:
- Run full readiness, redeploy Tower and verify live runtime smoke after deployment.
Limitations:
- Frontend readability/presentation polish only; no raster/vector operation behavior, API contract, persistence, migration, provider fetching or AI/model changes.
Next recommended pass:
- Continue with compact empty/error-state polish across QA, exports and AI lab result panels.
+1
View File
@@ -69,6 +69,7 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Add inspector mobile polish for dataset metadata, raster/vector tools and action groups. - [x] Add inspector mobile polish for dataset metadata, raster/vector tools and action groups.
- [x] Add export preview readability polish for large JSON/GeoJSON handoff artifacts. - [x] Add export preview readability polish for large JSON/GeoJSON handoff artifacts.
- [x] Add accessibility focus polish for primary workbench keyboard navigation. - [x] Add accessibility focus polish for primary workbench keyboard navigation.
- [x] Add raster/vector operation form readability polish for dense tool panels.
## Sprint 8 status ## Sprint 8 status
+2
View File
@@ -16,6 +16,8 @@ Export Preview shows a compact JSON summary before the payload and keeps large J
Primary workbench navigation, overview shortcuts, inspector tabs and dataset action buttons now share visible keyboard focus styling. Inspector tabs are also bound to tab panels with ARIA metadata. Primary workbench navigation, overview shortcuts, inspector tabs and dataset action buttons now share visible keyboard focus styling. Inspector tabs are also bound to tab panels with ARIA metadata.
Raster and vector operation panels use structured group headings, compact helper text, field grids, action rows and inline error blocks so dense GIS controls remain scannable in the inspector.
## Scope implemented ## Scope implemented
- API client layer (`src/services/api`) - API client layer (`src/services/api`)
- Project and area list/create flows - Project and area list/create flows
@@ -133,29 +133,39 @@ export function RasterControls({
</div> </div>
) : null} ) : null}
<div className="dataset-tool-group"> <div className="dataset-tool-group">
<label> <h4 className="dataset-tool-heading">Reproject raster</h4>
Reproject CRS <p className="dataset-tool-helper">Create a derived raster in a target CRS with the selected resampling method.</p>
<input <div className="dataset-tool-grid">
value={rasterReprojectCrs} <label className="dataset-tool-field">
onChange={(event) => onSetRasterReprojectCrs(event.target.value)} <span className="dataset-tool-label">Reproject CRS</span>
placeholder="EPSG:31370" <input
/> value={rasterReprojectCrs}
</label> onChange={(event) => onSetRasterReprojectCrs(event.target.value)}
<label> placeholder="EPSG:31370"
Resampling />
<select value={rasterReprojectResampling} onChange={(event) => onSetRasterReprojectResampling(event.target.value)}> <span className="dataset-tool-helper">Target CRS for the derived raster artifact.</span>
<option value="nearest">nearest</option> </label>
<option value="bilinear">bilinear</option> <label className="dataset-tool-field">
<option value="cubic">cubic</option> <span className="dataset-tool-label">Resampling</span>
</select> <select value={rasterReprojectResampling} onChange={(event) => onSetRasterReprojectResampling(event.target.value)}>
</label> <option value="nearest">nearest</option>
<button type="button" onClick={onRunRasterReproject}> <option value="bilinear">bilinear</option>
Reproject raster <option value="cubic">cubic</option>
</button> </select>
<span className="dataset-tool-helper">Nearest preserves classes; bilinear/cubic smooth continuous rasters.</span>
</label>
</div>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunRasterReproject}>
Reproject raster
</button>
</div>
</div> </div>
<div className="dataset-tool-group"> <div className="dataset-tool-group">
<label> <h4 className="dataset-tool-heading">Clip raster</h4>
Clip area <p className="dataset-tool-helper">Clip the selected raster to an existing project area.</p>
<label className="dataset-tool-field">
<span className="dataset-tool-label">Clip area</span>
<select value={selectedClipAreaId} onChange={(event) => onSetSelectedClipAreaId(event.target.value)}> <select value={selectedClipAreaId} onChange={(event) => onSetSelectedClipAreaId(event.target.value)}>
{areas.map((area) => ( {areas.map((area) => (
<option key={area.id} value={area.id}> <option key={area.id} value={area.id}>
@@ -164,43 +174,54 @@ export function RasterControls({
))} ))}
</select> </select>
</label> </label>
<button type="button" onClick={onRunRasterClip} disabled={areas.length === 0}> <div className="dataset-tool-action-row">
Clip raster by area <button type="button" onClick={onRunRasterClip} disabled={areas.length === 0}>
</button> Clip raster by area
{areas.length === 0 ? <p className="error">Create an area before raster clipping.</p> : null} </button>
</div>
{areas.length === 0 ? <p className="dataset-tool-error">Create an area before raster clipping.</p> : null}
</div> </div>
<div className="dataset-tool-group"> <div className="dataset-tool-group">
<label> <h4 className="dataset-tool-heading">Generate tiles</h4>
Tile size <p className="dataset-tool-helper">Create a tile manifest for downstream detection or segmentation runs.</p>
<input <div className="dataset-tool-grid">
type="number" <label className="dataset-tool-field">
min={1} <span className="dataset-tool-label">Tile size</span>
value={rasterTileSize} <input
onChange={(event) => onSetRasterTileSize(Number(event.target.value))} type="number"
/> min={1}
</label> value={rasterTileSize}
<label> onChange={(event) => onSetRasterTileSize(Number(event.target.value))}
Overlap />
<input <span className="dataset-tool-helper">{'Tile size must be > 0.'}</span>
type="number" </label>
min={0} <label className="dataset-tool-field">
value={rasterTileOverlap} <span className="dataset-tool-label">Overlap</span>
onChange={(event) => onSetRasterTileOverlap(Number(event.target.value))} <input
/> type="number"
</label> min={0}
<label> value={rasterTileOverlap}
Tile basename onChange={(event) => onSetRasterTileOverlap(Number(event.target.value))}
<input />
value={rasterTileOutputName} <span className="dataset-tool-helper">Overlap must be smaller than tile size.</span>
onChange={(event) => onSetRasterTileOutputName(event.target.value)} </label>
placeholder="optional" <label className="dataset-tool-field">
/> <span className="dataset-tool-label">Tile basename</span>
</label> <input
<button type="button" onClick={onRunRasterTile} disabled={!isRasterTileInputValid}> value={rasterTileOutputName}
Generate tiles onChange={(event) => onSetRasterTileOutputName(event.target.value)}
</button> placeholder="optional"
/>
<span className="dataset-tool-helper">Optional artifact name prefix for generated tiles.</span>
</label>
</div>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunRasterTile} disabled={!isRasterTileInputValid}>
Generate tiles
</button>
</div>
{!isRasterTileInputValid ? ( {!isRasterTileInputValid ? (
<p className="error"> <p className="dataset-tool-error">
Tile size must be {'>'} 0 and overlap must be {'>='} 0 and smaller than tile size. Tile size must be {'>'} 0 and overlap must be {'>='} 0 and smaller than tile size.
</p> </p>
) : null} ) : null}
@@ -209,46 +230,61 @@ export function RasterControls({
<div> <div>
<p>Use available band indexes from the raster file (1-based).</p> <p>Use available band indexes from the raster file (1-based).</p>
<div className="dataset-tool-group"> <div className="dataset-tool-group">
<p>NDVI</p> <h4 className="dataset-tool-heading">NDVI</h4>
<label> <p className="dataset-tool-helper">Vegetation index from NIR and red bands.</p>
Nir band <div className="dataset-tool-grid">
<input type="number" min={1} value={ndviNirBand} onChange={(event) => onSetNdviNirBand(Number(event.target.value))} /> <label className="dataset-tool-field">
</label> <span className="dataset-tool-label">NIR band</span>
<label> <input type="number" min={1} value={ndviNirBand} onChange={(event) => onSetNdviNirBand(Number(event.target.value))} />
Red band </label>
<input type="number" min={1} value={ndviRedBand} onChange={(event) => onSetNdviRedBand(Number(event.target.value))} /> <label className="dataset-tool-field">
</label> <span className="dataset-tool-label">Red band</span>
<button type="button" onClick={onRunRasterNdvi}> <input type="number" min={1} value={ndviRedBand} onChange={(event) => onSetNdviRedBand(Number(event.target.value))} />
Compute NDVI </label>
</button> </div>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunRasterNdvi}>
Compute NDVI
</button>
</div>
</div> </div>
<div className="dataset-tool-group"> <div className="dataset-tool-group">
<p>NDWI</p> <h4 className="dataset-tool-heading">NDWI</h4>
<label> <p className="dataset-tool-helper">Water index from NIR and green bands.</p>
Nir band <div className="dataset-tool-grid">
<input type="number" min={1} value={ndwiNirBand} onChange={(event) => onSetNdwiNirBand(Number(event.target.value))} /> <label className="dataset-tool-field">
</label> <span className="dataset-tool-label">NIR band</span>
<label> <input type="number" min={1} value={ndwiNirBand} onChange={(event) => onSetNdwiNirBand(Number(event.target.value))} />
Green band </label>
<input type="number" min={1} value={ndwiGreenBand} onChange={(event) => onSetNdwiGreenBand(Number(event.target.value))} /> <label className="dataset-tool-field">
</label> <span className="dataset-tool-label">Green band</span>
<button type="button" onClick={onRunRasterNdwi}> <input type="number" min={1} value={ndwiGreenBand} onChange={(event) => onSetNdwiGreenBand(Number(event.target.value))} />
Compute NDWI </label>
</button> </div>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunRasterNdwi}>
Compute NDWI
</button>
</div>
</div> </div>
<div className="dataset-tool-group"> <div className="dataset-tool-group">
<p>NDBI</p> <h4 className="dataset-tool-heading">NDBI</h4>
<label> <p className="dataset-tool-helper">Built-up index from SWIR and NIR bands.</p>
Swir band <div className="dataset-tool-grid">
<input type="number" min={1} value={ndbiSwirBand} onChange={(event) => onSetNdbiSwirBand(Number(event.target.value))} /> <label className="dataset-tool-field">
</label> <span className="dataset-tool-label">SWIR band</span>
<label> <input type="number" min={1} value={ndbiSwirBand} onChange={(event) => onSetNdbiSwirBand(Number(event.target.value))} />
Nir band </label>
<input type="number" min={1} value={ndbiNirBand} onChange={(event) => onSetNdbiNirBand(Number(event.target.value))} /> <label className="dataset-tool-field">
</label> <span className="dataset-tool-label">NIR band</span>
<button type="button" onClick={onRunRasterNdbi}> <input type="number" min={1} value={ndbiNirBand} onChange={(event) => onSetNdbiNirBand(Number(event.target.value))} />
Compute NDBI </label>
</button> </div>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunRasterNdbi}>
Compute NDBI
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -27,38 +27,57 @@ export function VectorControls({
<div className="dataset-tool-panel vector-tool-panel"> <div className="dataset-tool-panel vector-tool-panel">
<h3>Vector operations</h3> <h3>Vector operations</h3>
<div className="dataset-tool-group"> <div className="dataset-tool-group">
<label> <h4 className="dataset-tool-heading">Clip vector</h4>
Clip area <p className="dataset-tool-helper">Clip features to the selected project area.</p>
<select value={selectedClipAreaId} onChange={(event) => onSetSelectedClipAreaId(event.target.value)}> <div className="dataset-tool-grid">
{areas.map((area) => ( <label className="dataset-tool-field">
<option key={area.id} value={area.id}> <span className="dataset-tool-label">Clip area</span>
{area.name} <select value={selectedClipAreaId} onChange={(event) => onSetSelectedClipAreaId(event.target.value)}>
</option> {areas.map((area) => (
))} <option key={area.id} value={area.id}>
</select> {area.name}
</label> </option>
<button type="button" onClick={onRunVectorClip} disabled={areas.length === 0}> ))}
Run clip </select>
</button> </label>
</div>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunVectorClip} disabled={areas.length === 0}>
Run clip
</button>
</div>
</div> </div>
<button type="button" onClick={onRunVectorBuffer}>
Run buffer (25m)
</button>
<div className="dataset-tool-group"> <div className="dataset-tool-group">
<label> <h4 className="dataset-tool-heading">Buffer vector</h4>
Intersect target <p className="dataset-tool-helper">Create a 25m buffer using the existing vector operation defaults.</p>
<select value={selectedIntersectTargetId} onChange={(event) => onSetSelectedIntersectTargetId(event.target.value)}> <div className="dataset-tool-action-row">
<option value="">auto first vector</option> <button type="button" onClick={onRunVectorBuffer}>
{availableVectorTargets.map((target) => ( Run buffer (25m)
<option key={target.id} value={target.id}> </button>
{target.name} </div>
</option> </div>
))} <div className="dataset-tool-group">
</select> <h4 className="dataset-tool-heading">Intersect vector</h4>
</label> <p className="dataset-tool-helper">Intersect with another persisted vector dataset.</p>
<button type="button" onClick={onRunVectorIntersect}> <div className="dataset-tool-grid">
Run intersect <label className="dataset-tool-field">
</button> <span className="dataset-tool-label">Intersect target</span>
<select value={selectedIntersectTargetId} onChange={(event) => onSetSelectedIntersectTargetId(event.target.value)}>
<option value="">auto first vector</option>
{availableVectorTargets.map((target) => (
<option key={target.id} value={target.id}>
{target.name}
</option>
))}
</select>
<span className="dataset-tool-helper">Leave automatic to use the first available vector target.</span>
</label>
</div>
<div className="dataset-tool-action-row">
<button type="button" onClick={onRunVectorIntersect}>
Run intersect
</button>
</div>
</div> </div>
</div> </div>
) )
+53
View File
@@ -2148,16 +2148,69 @@ button.entity-card {
background: #ffffff; background: #ffffff;
} }
.dataset-tool-heading {
margin: 0;
font-size: 0.92rem;
line-height: 1.25;
}
.dataset-tool-helper {
margin: 0;
color: var(--muted);
font-size: 0.82rem;
line-height: 1.38;
overflow-wrap: anywhere;
}
.dataset-tool-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
gap: 0.52rem;
}
.dataset-tool-group label { .dataset-tool-group label {
min-width: 0; min-width: 0;
} }
.dataset-tool-field {
display: grid;
gap: 0.22rem;
min-width: 0;
}
.dataset-tool-label {
color: var(--text);
font-size: 0.78rem;
font-weight: 800;
line-height: 1.25;
}
.dataset-tool-group input, .dataset-tool-group input,
.dataset-tool-group select { .dataset-tool-group select {
width: 100%; width: 100%;
min-width: 0; min-width: 0;
} }
.dataset-tool-error {
margin: 0;
border: 1px solid rgba(153, 27, 27, 0.24);
border-radius: 8px;
padding: 0.52rem 0.62rem;
background: rgba(153, 27, 27, 0.07);
color: var(--danger);
font-size: 0.82rem;
font-weight: 750;
line-height: 1.35;
overflow-wrap: anywhere;
}
.dataset-tool-action-row {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
.dataset-tool-group button { .dataset-tool-group button {
width: fit-content; width: fit-content;
max-width: 100%; max-width: 100%;