diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eaf3faa..3a3ccffa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ # Changelog +## Sprint 62 Workbench visual polish (2026-06-18) + +- Added a compact workspace command bar for fast switching between the primary workbench surfaces. +- Polished the shell visual system with raised/sunken surfaces, softer shadows, tighter topbar spacing and more consistent panel styling. +- Replaced raw empty-state text in project/dataset panels with structured empty-state blocks. +- Improved Detection Lab and Segmentation Lab result summaries and wrapped long result tables in scroll-safe containers. +- Improved mobile workbench navigation by using horizontal rails for the primary nav and command chips, reducing vertical crowding without adding new behavior. +- Added static regression coverage for the visual polish contracts. +- No API contracts, migrations, backend behavior, provider fetching or AI model behavior changed. + ## Sprint 61 Golden QA scenario expansion (2026-06-18) - Expanded the deterministic QA/QC golden benchmark from one building scenario to four local fixture scenarios: partial match, perfect match, no-overlap and MultiPolygon match. diff --git a/backend/tests/test_sprint62_frontend_visual_polish.py b/backend/tests/test_sprint62_frontend_visual_polish.py new file mode 100644 index 00000000..237c41ab --- /dev/null +++ b/backend/tests/test_sprint62_frontend_visual_polish.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def test_frontend_shell_has_visual_polish_contracts() -> None: + app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") + css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8") + + assert "workspace-command-bar" in app + assert "workspace-nav-cluster" in app + assert "--surface-raised" in css + assert ".workspace-command-bar" in css + assert ".workspace-panel" in css + assert ".data-density-grid" in css + assert ".result-summary-card" in css + + +def test_primary_panels_use_empty_state_components() -> None: + project_panel = (ROOT / "frontend" / "src" / "components" / "project" / "ProjectPanel.tsx").read_text( + encoding="utf-8" + ) + dataset_panel = (ROOT / "frontend" / "src" / "components" / "datasets" / "DatasetPanel.tsx").read_text( + encoding="utf-8" + ) + detection_lab = (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text( + encoding="utf-8" + ) + segmentation_lab = ( + ROOT / "frontend" / "src" / "components" / "segmentation" / "SegmentationLab.tsx" + ).read_text(encoding="utf-8") + + assert "empty-state" in project_panel + assert "empty-state" in dataset_panel + assert "result-summary-card" in detection_lab + assert "result-summary-card" in segmentation_lab + assert "table-scroll" in detection_lab + assert "table-scroll" in segmentation_lab diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index f875c7fb..1dbecc0e 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -2407,3 +2407,30 @@ Limitations: Next recommended pass: - Continue with frontend visual polish backlog or add a live golden benchmark smoke only if a running PostGIS environment needs that extra release signal. + +## Sprint 62 Workbench visual polish (2026-06-18) + +Changed: +- Added a compact workspace command bar under the active workspace heading for quick movement between the main workbench surfaces. +- Polished the shared frontend visual system with raised/sunken surface tokens, softer shadows, tighter topbar spacing and consistent workspace panel styling. +- Replaced raw project/dataset empty-state text with structured empty-state blocks. +- Wrapped Detection Lab and Segmentation Lab result summaries in scan-friendly result cards and long result tables in scroll-safe containers. +- Improved mobile navigation density by making the primary nav and command chips horizontal rails on narrow screens. +- Added `backend/tests/test_sprint62_frontend_visual_polish.py` to guard the visual polish contracts. +- Updated `frontend/README.md`, `docs/TODO.md` and `CHANGELOG.md`. + +Tested: +- Red step: `cd backend && python -m pytest tests/test_sprint62_frontend_visual_polish.py -q` failed on missing command bar, panel polish and empty/result wrappers. +- `cd backend && python -m pytest tests/test_sprint62_frontend_visual_polish.py tests/test_sprint49_workbench_shell_refactor.py tests/test_sprint50_workspace_usability_polish.py -q` (`6 passed`) +- `cd frontend && npm run typecheck` +- `cd frontend && npm run build` +- Local Chrome/Playwright visual check against `http://127.0.0.1:5175` with live API proxy: desktop and mobile screenshots captured, no console warnings/errors, no horizontal page overflow. + +Open: +- Run full readiness and deploy Tower after this visual polish pass. + +Limitations: +- This is a frontend usability polish pass only. It does not add new API behavior, data processing, provider fetching or AI model functionality. + +Next recommended pass: +- Continue with map/result overlay ergonomics, especially making selected dataset/result provenance easier to see beside the map. diff --git a/docs/TODO.md b/docs/TODO.md index b6f04bf6..6519c172 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -61,6 +61,7 @@ This file now starts with the current implementation status. Older preparation/b - [x] Optional final bootstrap-effect extraction. - [x] Decide next V1 stabilization focus: golden dataset expansion or frontend visual polish backlog. - [x] Expand golden QA/QC benchmark coverage across partial, perfect, no-overlap and MultiPolygon scenarios. +- [x] Improve workbench shell visual polish, mobile navigation density and AI result readability. ## Sprint 8 status @@ -332,3 +333,4 @@ This file now starts with the current implementation status. Older preparation/b - [x] Add browser screenshot artifact automation for visual regression handoff. - [x] Add backend error-envelope audit for expected user-error paths. - [x] Expand golden datasets beyond the original single building QA fixture pair. +- [x] Add workbench visual polish pass for command bar, panel surfaces, empty states and mobile nav density. diff --git a/frontend/README.md b/frontend/README.md index 62901cd0..3bbf6318 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -219,6 +219,7 @@ The workbench now uses a task-based shell instead of a single long panel stack. - Area fallback selection now lives with the owning workflow hooks: clip-area selection in `useDatasetWorkflow.ts` and map-area selection in `useMapWorkspaceState.ts`. - 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. +- 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. ## Workbench shell refactor @@ -230,6 +231,7 @@ The workbench now uses a task-based shell instead of a single long panel stack. - `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}"`. +- Mobile workbench navigation uses horizontal rails for the primary nav and command bar, avoiding a tall menu stack before the active workspace content. ## Raster dependency visibility diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b876726a..dbc34ec9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -459,6 +459,22 @@ function App(): JSX.Element {

{activeWorkspaceItem.description}

+
+
+ {workspaceNavItems.slice(0, 5).map((item) => ( + + ))} +
+ {selectedProject ? `Active project: ${selectedProject.name}` : 'Start with a project or load the demo workflow.'} +
{activeWorkspace === 'overview' ? (
diff --git a/frontend/src/components/datasets/DatasetPanel.tsx b/frontend/src/components/datasets/DatasetPanel.tsx index 7395a33c..ce560486 100644 --- a/frontend/src/components/datasets/DatasetPanel.tsx +++ b/frontend/src/components/datasets/DatasetPanel.tsx @@ -70,7 +70,7 @@ export function DatasetPanel({ const readyDatasets = datasets.filter((dataset) => dataset.status === 'ready').length return ( -
+

Data catalog

@@ -127,7 +127,12 @@ export function DatasetPanel({ {loadingDatasets ?

Loading datasets...

: null} - {datasets.length === 0 ?

No datasets yet

: null} + {datasets.length === 0 ? ( +
+ No datasets yet +

Upload vector, GeoJSON or raster data after selecting a project.

+
+ ) : null}
    {datasets.map((dataset) => (
  • @@ -144,7 +149,7 @@ export function DatasetPanel({ {dataset.status === 'ready' ? 'ready' : dataset.status === 'failed' ? 'failed' : 'pending'}
-
+
size: {formatBytes(dataset.size_bytes)} features: {dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 'n/a'} bbox: {formatBounds(dataset.bounds_json ?? dataset.vector_summary?.bounds_json)} diff --git a/frontend/src/components/detection/DetectionLab.tsx b/frontend/src/components/detection/DetectionLab.tsx index 7ecca5df..c4a90c53 100644 --- a/frontend/src/components/detection/DetectionLab.tsx +++ b/frontend/src/components/detection/DetectionLab.tsx @@ -85,7 +85,7 @@ export function DetectionLab({ onRunQa, }: DetectionLabProps): JSX.Element { return ( -
+

Object detection

@@ -166,7 +166,7 @@ export function DetectionLab({
{detectionRunError ?

{detectionRunError}

: null} {detectionRunResult ? ( -
+

Status: {detectionRunResult.status}

Message: {detectionRunResult.message}

Analysis run: {detectionRunResult.analysis_run_id}

@@ -221,26 +221,28 @@ export function DetectionLab({ {loadingDetectionResults ?

Loading detection results...

: null}

Detections loaded: {detectionItems.length}

{detectionItems.length > 0 ? ( - - - - - - - - - - - {detectionItems.map((detection) => ( - - - - - +
+
ClassConfidenceModelSource tile
{detection.class_name}{detection.confidence.toFixed(2)}{detection.model_name}{detection.source_tile_path || 'n/a'}
+ + + + + + - ))} - -
ClassConfidenceModelSource tile
+ + + {detectionItems.map((detection) => ( + + {detection.class_name} + {detection.confidence.toFixed(2)} + {detection.model_name} + {detection.source_tile_path || 'n/a'} + + ))} + + +
) : null}
@@ -261,7 +263,7 @@ export function DetectionLab({ {detectionQaError ?

{detectionQaError}

: null} {detectionQaResult ? ( -
+

Status: {detectionQaResult.status}

Quality check: {detectionQaResult.quality_check_id}

Precision: {detectionQaResult.precision?.toFixed(3) ?? 'n/a'}

diff --git a/frontend/src/components/project/ProjectPanel.tsx b/frontend/src/components/project/ProjectPanel.tsx index 0e8763ce..339b3f08 100644 --- a/frontend/src/components/project/ProjectPanel.tsx +++ b/frontend/src/components/project/ProjectPanel.tsx @@ -27,7 +27,7 @@ export function ProjectPanel({ onLoadDemoWorkflow, }: ProjectPanelProps): JSX.Element { return ( -
+

Context

@@ -89,7 +89,12 @@ export function ProjectPanel({ ))} - {projects.length === 0 ?
  • No projects yet
  • : null} + {projects.length === 0 ? ( +
  • + No projects yet +

    Create a project or load the offline demo workflow to populate the workbench.

    +
  • + ) : null}
    ) diff --git a/frontend/src/components/segmentation/SegmentationLab.tsx b/frontend/src/components/segmentation/SegmentationLab.tsx index 850a5edd..4427389b 100644 --- a/frontend/src/components/segmentation/SegmentationLab.tsx +++ b/frontend/src/components/segmentation/SegmentationLab.tsx @@ -85,7 +85,7 @@ export function SegmentationLab({ onRunQa, }: SegmentationLabProps): JSX.Element { return ( -
    +

    Polygon segmentation

    @@ -163,7 +163,7 @@ export function SegmentationLab({ ) : null} {segmentationRunError ?

    {segmentationRunError}

    : null} {segmentationRunResult ? ( -
    +

    Status: {segmentationRunResult.status}

    Message: {segmentationRunResult.message}

    Analysis run: {segmentationRunResult.analysis_run_id}

    @@ -218,30 +218,32 @@ export function SegmentationLab({ {loadingSegmentationResults ?

    Loading segmentation results...

    : null}

    Segmentations loaded: {segmentationItems.length}

    {segmentationItems.length > 0 ? ( - - - - - - - - - - - - - {segmentationItems.map((segmentation) => ( - - - - - - - +
    +
    ClassConfidenceArea m2ModelTileMask path
    {segmentation.class_name}{segmentation.confidence?.toFixed(2) ?? 'n/a'}{segmentation.area_m2?.toFixed(2) ?? 'n/a'}{segmentation.model_name}{segmentation.source_tile_path || (segmentation.tile_index ?? 'n/a')}{segmentation.mask_path || 'n/a'}
    + + + + + + + + - ))} - -
    ClassConfidenceArea m2ModelTileMask path
    + + + {segmentationItems.map((segmentation) => ( + + {segmentation.class_name} + {segmentation.confidence?.toFixed(2) ?? 'n/a'} + {segmentation.area_m2?.toFixed(2) ?? 'n/a'} + {segmentation.model_name} + {segmentation.source_tile_path || (segmentation.tile_index ?? 'n/a')} + {segmentation.mask_path || 'n/a'} + + ))} + + +
    ) : null}
    @@ -262,7 +264,7 @@ export function SegmentationLab({ {segmentationQaError ?

    {segmentationQaError}

    : null} {segmentationQaResult ? ( -
    +

    Status: {segmentationQaResult.status}

    Quality check: {segmentationQaResult.quality_check_id}

    Precision: {segmentationQaResult.precision?.toFixed(3) ?? 'n/a'}

    diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index cd9387f9..b4cdc8ce 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -4,6 +4,8 @@ --bg: #eef4f1; --panel: #ffffff; --panel-soft: #f8fbf9; + --surface-raised: #ffffff; + --surface-sunken: #f3f8f5; --text: #132018; --muted: #5f6f67; --line: #cbd8d0; @@ -14,6 +16,7 @@ --warning: #b45309; --danger: #991b1b; --shadow: 0 12px 32px rgba(33, 48, 41, 0.08); + --shadow-soft: 0 8px 24px rgba(33, 48, 41, 0.06); color-scheme: light; } @@ -32,6 +35,8 @@ body { background: linear-gradient(180deg, rgba(15, 118, 110, 0.08), rgba(238, 244, 241, 0) 18rem), var(--bg); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; } button, @@ -183,8 +188,8 @@ section { border: 1px solid var(--line); border-radius: 8px; padding: 1rem; - background: rgba(255, 255, 255, 0.94); - box-shadow: var(--shadow); + background: rgba(255, 255, 255, 0.96); + box-shadow: var(--shadow-soft); } .workspace-grid { @@ -519,12 +524,12 @@ section li strong + div { top: 0; z-index: 20; display: grid; - grid-template-columns: minmax(12rem, 18rem) minmax(0, 1fr); - gap: 0.75rem; + grid-template-columns: minmax(12rem, 17rem) minmax(0, 1fr); + gap: 0.9rem; align-items: center; border-bottom: 1px solid var(--line); - padding: 0.55rem 0.85rem; - background: rgba(248, 251, 249, 0.96); + padding: 0.58rem 1rem; + background: rgba(250, 253, 251, 0.97); backdrop-filter: blur(12px); } @@ -545,11 +550,11 @@ section li strong + div { } .context-bar > div { - min-height: 3.2rem; + min-height: 3rem; border: 1px solid var(--line); - border-radius: 8px; + border-radius: 7px; padding: 0.44rem 0.58rem; - background: #ffffff; + background: var(--surface-raised); } .context-bar span { @@ -581,7 +586,7 @@ section li strong + div { min-height: 0; overflow: auto; border-color: var(--line); - background: #f8fbf9; + background: var(--surface-sunken); } .workbench-sidebar { @@ -636,7 +641,7 @@ section li strong + div { min-width: 0; min-height: 0; overflow: auto; - padding: 0.85rem 1rem 1rem; + padding: 0.9rem 1.05rem 1.15rem; } .workbench-inspector { @@ -656,11 +661,12 @@ section li strong + div { gap: 1rem; align-items: end; justify-content: space-between; - margin-bottom: 0.85rem; + margin-bottom: 0.65rem; border: 1px solid var(--line); - border-radius: 8px; - padding: 0.72rem 0.85rem; - background: #ffffff; + border-radius: 7px; + padding: 0.76rem 0.9rem; + background: var(--surface-raised); + box-shadow: var(--shadow-soft); } .workspace-heading h2 { @@ -674,6 +680,50 @@ section li strong + div { text-align: right; } +.workspace-command-bar { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.8rem; + align-items: center; + margin-bottom: 0.85rem; + border: 1px solid var(--line); + border-radius: 7px; + padding: 0.48rem; + background: rgba(255, 255, 255, 0.74); +} + +.workspace-command-bar > span { + max-width: 24rem; + padding-right: 0.35rem; + color: var(--muted); + font-size: 0.82rem; + line-height: 1.3; + text-align: right; +} + +.workspace-nav-cluster { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} + +.command-chip { + width: auto; + min-height: 2.05rem; + border-color: transparent; + padding: 0.32rem 0.62rem; + background: transparent; + box-shadow: none; + color: var(--muted); + font-size: 0.8rem; +} + +.command-chip-active { + border-color: rgba(15, 118, 110, 0.34); + background: var(--accent-soft); + color: var(--accent-strong); +} + .workspace-stack { display: grid; gap: 1rem; @@ -706,6 +756,11 @@ section li strong + div { overflow: visible; } +.workspace-panel { + border-radius: 7px; + background: var(--surface-raised); +} + .workspace-grid-data > section:nth-child(3) { grid-column: 1 / -1; } @@ -768,6 +823,28 @@ section th { text-transform: uppercase; } +.table-scroll { + width: 100%; + max-height: 22rem; + overflow: auto; + margin-top: 0.75rem; + border: 1px solid var(--line); + border-radius: 7px; + background: #ffffff; +} + +.table-scroll table { + min-width: 42rem; + margin-top: 0; +} + +.table-scroll thead th { + position: sticky; + top: 0; + z-index: 1; + background: #f8fbf9; +} + .workbench-inspector .job-result, .workbench-inspector pre { max-height: 16rem; @@ -854,6 +931,13 @@ section th { background: var(--panel-soft); } +.data-density-grid { + border: 1px solid var(--line); + border-radius: 7px; + padding: 0.55rem; + background: #ffffff; +} + .entity-list, .dataset-list, .model-list, @@ -922,6 +1006,14 @@ button.entity-card { padding: 0.85rem; } +.dataset-card, +.model-card, +.quality-check-card, +.export-card, +.lab-block { + box-shadow: 0 1px 0 rgba(19, 32, 24, 0.03); +} + .dataset-card-active { border-color: rgba(15, 118, 110, 0.5); box-shadow: inset 4px 0 0 var(--accent); @@ -974,6 +1066,24 @@ button.entity-card { width: auto; } +.result-summary-card { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); + gap: 0.45rem 0.7rem; + margin-top: 0.75rem; + border: 1px solid rgba(15, 118, 110, 0.22); + border-radius: 7px; + padding: 0.72rem; + background: var(--accent-soft); +} + +.result-summary-card p { + margin: 0; + color: #23423b; + font-size: 0.86rem; + line-height: 1.35; +} + .quality-summary-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -1056,6 +1166,7 @@ button.entity-card { gap: 0.25rem; margin-top: 0.75rem; background: #ffffff; + color: var(--text); } .empty-state p, @@ -1379,11 +1490,13 @@ button.entity-card { } .workspace-heading, + .workspace-command-bar, .status-strip-header { display: block; } .workspace-heading p:last-child, + .workspace-command-bar > span, .status-next-action { margin-top: 0.45rem; text-align: left; @@ -1419,11 +1532,33 @@ button.entity-card { } .context-bar, - .workbench-sidebar nav, - .quick-action-grid { + .quick-action-grid, + .workspace-nav-cluster { grid-template-columns: 1fr; } + .workbench-sidebar nav { + display: flex; + gap: 0.5rem; + overflow-x: auto; + padding-bottom: 0.2rem; + } + + .workbench-sidebar .nav-item { + min-width: 8.75rem; + } + + .workspace-nav-cluster { + display: flex; + flex-wrap: nowrap; + overflow-x: auto; + padding-bottom: 0.1rem; + } + + .command-chip { + min-width: 6.75rem; + } + .status-strip-grid { grid-template-columns: 1fr; }