Polish map workspace density
This commit is contained in:
@@ -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