Polish workbench visual shell
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -459,6 +459,22 @@ function App(): JSX.Element {
|
||||
</div>
|
||||
<p>{activeWorkspaceItem.description}</p>
|
||||
</div>
|
||||
<div className="workspace-command-bar" aria-label="Workspace shortcuts">
|
||||
<div className="workspace-nav-cluster">
|
||||
{workspaceNavItems.slice(0, 5).map((item) => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
className={item.key === activeWorkspace ? 'command-chip command-chip-active' : 'command-chip'}
|
||||
onClick={() => setActiveWorkspace(item.key)}
|
||||
aria-pressed={item.key === activeWorkspace}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<span>{selectedProject ? `Active project: ${selectedProject.name}` : 'Start with a project or load the demo workflow.'}</span>
|
||||
</div>
|
||||
|
||||
{activeWorkspace === 'overview' ? (
|
||||
<div className="workspace-stack">
|
||||
|
||||
@@ -70,7 +70,7 @@ export function DatasetPanel({
|
||||
const readyDatasets = datasets.filter((dataset) => dataset.status === 'ready').length
|
||||
|
||||
return (
|
||||
<section data-testid="dataset-panel">
|
||||
<section className="workspace-panel" data-testid="dataset-panel">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
<p className="eyebrow">Data catalog</p>
|
||||
@@ -127,7 +127,12 @@ export function DatasetPanel({
|
||||
</form>
|
||||
|
||||
{loadingDatasets ? <p>Loading datasets...</p> : null}
|
||||
{datasets.length === 0 ? <p>No datasets yet</p> : null}
|
||||
{datasets.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<strong>No datasets yet</strong>
|
||||
<p>Upload vector, GeoJSON or raster data after selecting a project.</p>
|
||||
</div>
|
||||
) : null}
|
||||
<ul className="dataset-list">
|
||||
{datasets.map((dataset) => (
|
||||
<li className={dataset.id === selectedDatasetId ? 'dataset-card dataset-card-active' : 'dataset-card'} key={dataset.id}>
|
||||
@@ -144,7 +149,7 @@ export function DatasetPanel({
|
||||
{dataset.status === 'ready' ? 'ready' : dataset.status === 'failed' ? 'failed' : 'pending'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="dataset-metrics">
|
||||
<div className="dataset-metrics data-density-grid">
|
||||
<span>size: {formatBytes(dataset.size_bytes)}</span>
|
||||
<span>features: {dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 'n/a'}</span>
|
||||
<span>bbox: {formatBounds(dataset.bounds_json ?? dataset.vector_summary?.bounds_json)}</span>
|
||||
|
||||
@@ -85,7 +85,7 @@ export function DetectionLab({
|
||||
onRunQa,
|
||||
}: DetectionLabProps): JSX.Element {
|
||||
return (
|
||||
<section>
|
||||
<section className="workspace-panel">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
<p className="eyebrow">Object detection</p>
|
||||
@@ -166,7 +166,7 @@ export function DetectionLab({
|
||||
</div>
|
||||
{detectionRunError ? <p className="error">{detectionRunError}</p> : null}
|
||||
{detectionRunResult ? (
|
||||
<div>
|
||||
<div className="result-summary-card">
|
||||
<p>Status: {detectionRunResult.status}</p>
|
||||
<p>Message: {detectionRunResult.message}</p>
|
||||
<p>Analysis run: {detectionRunResult.analysis_run_id}</p>
|
||||
@@ -221,26 +221,28 @@ export function DetectionLab({
|
||||
{loadingDetectionResults ? <p>Loading detection results...</p> : null}
|
||||
<p>Detections loaded: {detectionItems.length}</p>
|
||||
{detectionItems.length > 0 ? (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Class</th>
|
||||
<th>Confidence</th>
|
||||
<th>Model</th>
|
||||
<th>Source tile</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{detectionItems.map((detection) => (
|
||||
<tr key={detection.id}>
|
||||
<td>{detection.class_name}</td>
|
||||
<td>{detection.confidence.toFixed(2)}</td>
|
||||
<td>{detection.model_name}</td>
|
||||
<td>{detection.source_tile_path || 'n/a'}</td>
|
||||
<div className="table-scroll">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Class</th>
|
||||
<th>Confidence</th>
|
||||
<th>Model</th>
|
||||
<th>Source tile</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{detectionItems.map((detection) => (
|
||||
<tr key={detection.id}>
|
||||
<td>{detection.class_name}</td>
|
||||
<td>{detection.confidence.toFixed(2)}</td>
|
||||
<td>{detection.model_name}</td>
|
||||
<td>{detection.source_tile_path || 'n/a'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="lab-block">
|
||||
@@ -261,7 +263,7 @@ export function DetectionLab({
|
||||
</button>
|
||||
{detectionQaError ? <p className="error">{detectionQaError}</p> : null}
|
||||
{detectionQaResult ? (
|
||||
<div>
|
||||
<div className="result-summary-card">
|
||||
<p>Status: {detectionQaResult.status}</p>
|
||||
<p>Quality check: {detectionQaResult.quality_check_id}</p>
|
||||
<p>Precision: {detectionQaResult.precision?.toFixed(3) ?? 'n/a'}</p>
|
||||
|
||||
@@ -27,7 +27,7 @@ export function ProjectPanel({
|
||||
onLoadDemoWorkflow,
|
||||
}: ProjectPanelProps): JSX.Element {
|
||||
return (
|
||||
<section data-testid="project-panel">
|
||||
<section className="workspace-panel" data-testid="project-panel">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
<p className="eyebrow">Context</p>
|
||||
@@ -89,7 +89,12 @@ export function ProjectPanel({
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{projects.length === 0 ? <li>No projects yet</li> : null}
|
||||
{projects.length === 0 ? (
|
||||
<li className="empty-state">
|
||||
<strong>No projects yet</strong>
|
||||
<p>Create a project or load the offline demo workflow to populate the workbench.</p>
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -85,7 +85,7 @@ export function SegmentationLab({
|
||||
onRunQa,
|
||||
}: SegmentationLabProps): JSX.Element {
|
||||
return (
|
||||
<section>
|
||||
<section className="workspace-panel">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
<p className="eyebrow">Polygon segmentation</p>
|
||||
@@ -163,7 +163,7 @@ export function SegmentationLab({
|
||||
) : null}
|
||||
{segmentationRunError ? <p className="error">{segmentationRunError}</p> : null}
|
||||
{segmentationRunResult ? (
|
||||
<div>
|
||||
<div className="result-summary-card">
|
||||
<p>Status: {segmentationRunResult.status}</p>
|
||||
<p>Message: {segmentationRunResult.message}</p>
|
||||
<p>Analysis run: {segmentationRunResult.analysis_run_id}</p>
|
||||
@@ -218,30 +218,32 @@ export function SegmentationLab({
|
||||
{loadingSegmentationResults ? <p>Loading segmentation results...</p> : null}
|
||||
<p>Segmentations loaded: {segmentationItems.length}</p>
|
||||
{segmentationItems.length > 0 ? (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Class</th>
|
||||
<th>Confidence</th>
|
||||
<th>Area m2</th>
|
||||
<th>Model</th>
|
||||
<th>Tile</th>
|
||||
<th>Mask path</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{segmentationItems.map((segmentation) => (
|
||||
<tr key={segmentation.id}>
|
||||
<td>{segmentation.class_name}</td>
|
||||
<td>{segmentation.confidence?.toFixed(2) ?? 'n/a'}</td>
|
||||
<td>{segmentation.area_m2?.toFixed(2) ?? 'n/a'}</td>
|
||||
<td>{segmentation.model_name}</td>
|
||||
<td>{segmentation.source_tile_path || (segmentation.tile_index ?? 'n/a')}</td>
|
||||
<td>{segmentation.mask_path || 'n/a'}</td>
|
||||
<div className="table-scroll">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Class</th>
|
||||
<th>Confidence</th>
|
||||
<th>Area m2</th>
|
||||
<th>Model</th>
|
||||
<th>Tile</th>
|
||||
<th>Mask path</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{segmentationItems.map((segmentation) => (
|
||||
<tr key={segmentation.id}>
|
||||
<td>{segmentation.class_name}</td>
|
||||
<td>{segmentation.confidence?.toFixed(2) ?? 'n/a'}</td>
|
||||
<td>{segmentation.area_m2?.toFixed(2) ?? 'n/a'}</td>
|
||||
<td>{segmentation.model_name}</td>
|
||||
<td>{segmentation.source_tile_path || (segmentation.tile_index ?? 'n/a')}</td>
|
||||
<td>{segmentation.mask_path || 'n/a'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="lab-block">
|
||||
@@ -262,7 +264,7 @@ export function SegmentationLab({
|
||||
</button>
|
||||
{segmentationQaError ? <p className="error">{segmentationQaError}</p> : null}
|
||||
{segmentationQaResult ? (
|
||||
<div>
|
||||
<div className="result-summary-card">
|
||||
<p>Status: {segmentationQaResult.status}</p>
|
||||
<p>Quality check: {segmentationQaResult.quality_check_id}</p>
|
||||
<p>Precision: {segmentationQaResult.precision?.toFixed(3) ?? 'n/a'}</p>
|
||||
|
||||
+152
-17
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user