Polish map workspace density
This commit is contained in:
@@ -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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -50,6 +50,7 @@ export function MapWorkspace({
|
||||
onSetMapLayerOpacity,
|
||||
onSelectMapFeature,
|
||||
}: MapWorkspaceProps): JSX.Element {
|
||||
const selectedMapArea = areas.find((area) => area.id === selectedMapAreaId)
|
||||
const featureProperties = selectedMapFeature?.properties ?? null
|
||||
const featureSummaryEntries = featureProperties
|
||||
? Object.entries(featureProperties)
|
||||
@@ -58,7 +59,7 @@ export function MapWorkspace({
|
||||
: []
|
||||
|
||||
return (
|
||||
<section data-testid="map-workspace">
|
||||
<section className="map-workspace-shell" data-testid="map-workspace">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
<p className="eyebrow">Spatial review</p>
|
||||
@@ -67,89 +68,110 @@ export function MapWorkspace({
|
||||
<span className="count-pill">{mapFeatureCollection ? `${mapFeatureCount} features` : 'no layer'}</span>
|
||||
</div>
|
||||
|
||||
<div className="map-toolbar">
|
||||
<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 className="map-context-summary" aria-label="Map layer status">
|
||||
<div>
|
||||
<span>Area of interest</span>
|
||||
<strong>{selectedMapArea?.name ?? 'No area selected'}</strong>
|
||||
<small>{areaFeatureCollection ? `${areaFeatureCount} AOI features loaded` : 'AOI overlay disabled'}</small>
|
||||
</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">
|
||||
<div>
|
||||
<span>Active layer</span>
|
||||
<strong>{mapLayerLabel}</strong>
|
||||
<span>{areaFeatureCollection ? `${areaFeatureCount} AOI loaded` : 'No AOI loaded'}</span>
|
||||
<span>{mapFeatureCollection ? `${mapFeatureCount} features loaded` : 'No vector/result layer loaded'}</span>
|
||||
<small>{mapLayerSourceLabel}</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Feature state</span>
|
||||
<strong>{mapFeatureCollection ? `${mapFeatureCount} rendered features` : 'No layer rendered'}</strong>
|
||||
<small>{mapLayerProvenance}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="layer-provenance-rail" aria-label="Active map layer provenance">
|
||||
<div>
|
||||
<span>Layer source</span>
|
||||
<strong>{mapLayerSourceLabel}</strong>
|
||||
|
||||
<div className="map-control-surface" aria-label="Map workspace controls">
|
||||
<div className="map-toolbar">
|
||||
<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>
|
||||
<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 className="layer-provenance-rail" aria-label="Active map layer provenance">
|
||||
<div>
|
||||
<span>Layer source</span>
|
||||
<strong>{mapLayerSourceLabel}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
|
||||
{!mapFeatureCollection ? (
|
||||
<div className="empty-state map-empty-state">
|
||||
<strong>No active vector or result layer</strong>
|
||||
@@ -171,37 +193,43 @@ export function MapWorkspace({
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
<GeoMap
|
||||
data={mapFeatureCollection}
|
||||
areaData={areaFeatureCollection}
|
||||
visible={mapLayerVisible}
|
||||
opacity={mapLayerOpacity}
|
||||
areaVisible={areaLayerVisible}
|
||||
areaOpacity={areaLayerOpacity}
|
||||
onFeatureSelect={onSelectMapFeature}
|
||||
/>
|
||||
<div className="feature-inspector">
|
||||
<div className="panel-title-row">
|
||||
<h3>Feature inspector</h3>
|
||||
<span className="count-pill">{selectedMapFeature?.geometry?.type ?? 'none'}</span>
|
||||
|
||||
<div className="map-frame-surface">
|
||||
<GeoMap
|
||||
data={mapFeatureCollection}
|
||||
areaData={areaFeatureCollection}
|
||||
visible={mapLayerVisible}
|
||||
opacity={mapLayerOpacity}
|
||||
areaVisible={areaLayerVisible}
|
||||
areaOpacity={areaLayerOpacity}
|
||||
onFeatureSelect={onSelectMapFeature}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{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>
|
||||
</section>
|
||||
)
|
||||
|
||||
+108
-3
@@ -2007,12 +2007,96 @@ button.entity-card {
|
||||
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;
|
||||
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
|
||||
gap: 0.75rem;
|
||||
align-items: end;
|
||||
margin-bottom: 0.85rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.layer-control-card {
|
||||
@@ -2049,7 +2133,7 @@ button.entity-card {
|
||||
display: grid;
|
||||
grid-template-columns: 0.85fr minmax(0, 1.5fr) 0.85fr;
|
||||
gap: 0.65rem;
|
||||
margin-bottom: 0.85rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.layer-provenance-rail > div,
|
||||
@@ -2645,6 +2729,27 @@ button.entity-card {
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.map-workspace-shell .map-context-summary {
|
||||
grid-template-columns: repeat(auto-fit, minmax(8.25rem, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.map-workspace-shell .map-toolbar {
|
||||
grid-template-columns: repeat(auto-fit, minmax(8.25rem, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.map-workspace-shell .layer-provenance-rail {
|
||||
grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.map-control-surface,
|
||||
.map-frame-surface,
|
||||
.map-inspection-surface {
|
||||
padding: 0.58rem;
|
||||
}
|
||||
|
||||
.layer-control-card {
|
||||
padding: 0.52rem 0.58rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user