Require explicit map analysis selection
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Jens
2026-08-01 14:05:42 +02:00
parent be38e9a99f
commit 6c1c2b52b6
4 changed files with 306 additions and 111 deletions
+23
View File
@@ -12166,3 +12166,26 @@ Open:
username `ITWorx`; no plaintext password or password hash was printed or changed.
- Targeted backend authentication suite passed from the backend root: 8 tests.
- `bash -n scripts/configure_operator_login.sh` and `git diff --check` passed.
## 2026-08-01 - Explicit map analysis selection and result drawer
### Changed
- Decoupled AOI drawing from analysis: completing a rectangle now stores only
the bounded selection and clears stale results; it does not acquire or query
any theme, source or AI model.
- Added explicit multi-theme selection with a single `Analyseer selectie`
action. The bounded request planner now receives only the themes chosen by
the user.
- Restored Insights as an explicit open/close drawer over the map, with an
accessible toggle and a truthful pre-analysis empty state.
- Shortened theme availability copy, constrained labels and statuses to their
grid columns, and added a compact selection summary/action block.
- Kept full-work-area selection non-executing; it follows the same choose-then-
analyse contract as a drawn rectangle.
### Verification before deployment
- `npm run build` passed TypeScript compilation and the production Vite build.
- `npm run test:unit` passed all 49 tests in 15 files.
- `git diff --check` passed.
+9
View File
@@ -1046,3 +1046,12 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Herstart vanuit `/mnt/user/appdata/geointel` met behoud van de NVIDIA/AI-image.
- [x] Controleer containergezondheid, migraties, frontendproxy en actieve runtimeconfiguratie.
- [x] Werk de operatorconfiguratievoorbeelden bij zodat toekomstige configuratie `ITWorx` gebruikt.
# Sprint 235 - Expliciete kaartanalyse en resultatenlade
- [x] Laat tekenen uitsluitend het werkgebied vastleggen zonder bronnen of modellen te starten.
- [x] Voeg meervoudige, expliciete themakeuze en één duidelijke analyseknop toe.
- [x] Beperk de analysequery tot uitsluitend de gekozen themas.
- [x] Herstel Inzichten als open- en sluitbare lade boven de kaart.
- [x] Verkort bronteksten en voorkom overflow in de themakolom.
- [x] Verifieer TypeScript-build, frontend-unitset en responsive runtime visueel.
+98 -110
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent } from 'react'
import { BoxSelect, MapPinned, SlidersHorizontal, Trash2 } from 'lucide-react'
import { BoxSelect, ChevronLeft, ChevronRight, MapPinned, Play, SlidersHorizontal, Trash2 } from 'lucide-react'
import GeoMap from '../GeoMap'
import type { AreaRead, CoverageResolveResponse, CoverageStatus, DatasetCreateResponse, DetectionQaResult, MapResultExportRequest, MapViewportState, OrthophotoAcquisitionResult, OrthophotoProductRead, ProjectRead, QaComparisonResult, VectorSelectionBBox, VectorSelectionMetric, VectorSelectionResponse } from '../../types'
import { useMapThemeSelectionInsights, type MapThemeAcquisition, type MapThemeQuery } from '../../hooks/useMapThemeSelectionInsights'
@@ -335,60 +335,6 @@ const DATA_THEME_MAP_STYLES: Record<DataThemeId, { fill: string; line: string }>
marine_environment: { fill: '#3475a3', line: '#1c557d' },
}
function datasetAvailabilityLabel(
dataset: DatasetCreateResponse,
partitions: DatasetCreateResponse[] = [dataset],
): string {
const partitionCount = partitions.length
const regionalSuffix = partitionCount > 1 ? ` · ${partitionCount} gemeenten` : ''
if (dataset.dataset_type === 'raster' && ['digitaal_vlaanderen_dhmv', 'spw_terrain'].includes(dataset.source_name ?? '')) {
const resolution = Number(dataset.source_metadata?.['analysis_resolution_m'])
return `${Number.isFinite(resolution) ? `${resolution.toLocaleString('nl-BE')} m` : 'Raster'} hoogtegrid${regionalSuffix}`
}
if (dataset.dataset_type === 'raster' && dataset.source_name === 'vmm_flood_hazard') {
const resolution = Number(dataset.source_metadata?.['analysis_resolution_m'])
return `${Number.isFinite(resolution) ? `${resolution.toLocaleString('nl-BE')} m` : 'Raster'} overstromingsscenario${regionalSuffix}`
}
if (dataset.dataset_type === 'raster' && dataset.source_name === 'spw_bathymetry') {
const resolution = Number(dataset.source_metadata?.['analysis_resolution_m'])
const period = String(dataset.source_metadata?.['survey_period'] ?? '2019-2022')
return `${Number.isFinite(resolution) ? `${resolution.toLocaleString('nl-BE')} m` : 'Raster'} waterbodemhoogte · ${period}`
}
if (dataset.source_name === 'vmm_vha_bathymetry_profiles') {
const profiles = partitions.reduce(
(total, item) => total + (item.feature_count ?? item.vector_summary?.feature_count ?? 0),
0,
)
const documents = partitions.reduce(
(total, item) => total + Number(item.source_metadata?.['document_count'] ?? 0),
0,
)
return `${profiles.toLocaleString('nl-BE')} profielen · ${documents.toLocaleString('nl-BE')} bronbladen${regionalSuffix}`
}
if (dataset.dataset_type === 'raster' && dataset.source_name === 'department_omgeving_thematic_raster') {
const resolution = Number(dataset.source_metadata?.['analysis_resolution_m'])
const year = Number(dataset.source_metadata?.['observation_year'])
const resolutionLabel = Number.isFinite(resolution) ? `${resolution.toLocaleString('nl-BE')} m` : 'Raster'
return `${resolutionLabel} officiële bron${Number.isFinite(year) ? ` · ${year}` : ''}`
}
if (dataset.dataset_type === 'raster' && dataset.source_name === 'spw_walous_land_cover') {
const resolution = Number(dataset.source_metadata?.['analysis_resolution_m'])
const year = Number(dataset.source_metadata?.['observation_year'])
const resolutionLabel = Number.isFinite(resolution) ? `${resolution.toLocaleString('nl-BE')} m` : 'Raster'
return `${resolutionLabel} WALOUS-landbedekking${Number.isFinite(year) ? ` · ${year}` : ''}`
}
if (dataset.source_name === 'ngi_adminvector') {
return `${(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} officiële bestuursgebieden`
}
if (dataset.source_name === 'rbins_msp_2026') {
return `${(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} officiële planobjecten · 2026-2034`
}
if (dataset.source_name === 'rbins_marine_reporting_units') {
return `${(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} officiële zeegebieden`
}
return `${(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} objecten beschikbaar`
}
function datasetSearchText(dataset: DatasetCreateResponse): string {
return [
dataset.name,
@@ -894,6 +840,8 @@ export function MapWorkspace({
const selectedDataset = availableMapDatasets.find((dataset) => dataset.id === selectedMapDatasetId) ?? null
return themeIdForDataset(selectedDataset) ?? 'buildings'
})
const [selectedThemeIds, setSelectedThemeIds] = useState<DataThemeId[]>([])
const [resultsPanelOpen, setResultsPanelOpen] = useState(false)
const activeScopeProject = projects.find((project) => project.id === selectedProjectId) ?? null
const activeScopeLabel = activeScopeProject ? operationalScopeProjectLabel(activeScopeProject) : 'Werkgebied'
const selectedMapArea = areas.find((area) => area.id === selectedMapAreaId)
@@ -1364,7 +1312,11 @@ export function MapWorkspace({
const onDemandThemeActive = analysisMode === 'current' && Boolean(activeOnDemandMapProduct)
const regionalOnDemandThemeActive = regionalScopeSelected && onDemandThemeActive
const activeThemeAvailable = Boolean(activeThemeDataset) || onDemandThemeActive
const coverageSelectionAvailable = analysisMode === 'current' && Boolean(selectedProjectId && selectedMapArea)
const selectedThemes = useMemo(
() => DATA_THEMES.filter((theme) => selectedThemeIds.includes(theme.id)),
[selectedThemeIds],
)
useEffect(() => {
if (
@@ -1763,6 +1715,7 @@ export function MapWorkspace({
setFirstSelectionCorner(null)
clearThemeInsights()
clearTemporalComparison()
setResultsPanelOpen(false)
setBboxSelectionMode(true)
}
@@ -1775,7 +1728,9 @@ export function MapWorkspace({
setSelectionBbox(bbox)
setFirstSelectionCorner(null)
setBboxSelectionMode(false)
void analyzeSelection(bbox, areaIdForSelection(bbox))
clearThemeInsights()
clearTemporalComparison()
setResultsPanelOpen(false)
}
const runAreaExtract = () => {
@@ -1792,6 +1747,7 @@ export function MapWorkspace({
setBboxSelectionMode(false)
setFirstSelectionCorner(null)
setBboxInput(bboxToInputState(null))
setResultsPanelOpen(false)
clearThemeInsights()
clearTemporalComparison()
onClearMapSelectionExtract()
@@ -1929,7 +1885,13 @@ export function MapWorkspace({
if (!dataset && !onDemandProduct) {
return
}
setSelectedThemeIds((current) => (
current.includes(theme.id)
? current.filter((themeId) => themeId !== theme.id)
: [...current, theme.id]
))
setActiveThemeId(theme.id)
clearThemeInsights()
clearTemporalComparison()
if (dataset) {
onOpenDatasetInMap(dataset)
@@ -1938,6 +1900,9 @@ export function MapWorkspace({
const setExplorerMode = (mode: 'current' | 'evolution') => {
setAnalysisMode(mode)
setSelectedThemeIds([])
setResultsPanelOpen(false)
clearThemeInsights()
clearTemporalComparison()
if (mode !== 'evolution' || activeTemporalSeriesGroups.length > 0) {
return
@@ -2029,7 +1994,7 @@ export function MapWorkspace({
}
const availableThemes: Array<MapThemeQuery<DataThemeId>> = []
const resultFeatureLimit = selectionFeatureLimit(bbox)
for (const theme of [activeTheme]) {
for (const theme of selectedThemes) {
const dataset = themeDatasetMap[theme.id]
const partitioned = Boolean(
dataset
@@ -2078,6 +2043,10 @@ export function MapWorkspace({
}
const analyzeSelection = async (bbox: VectorSelectionBBox, areaId?: string) => {
if (analysisMode === 'current' && selectedThemes.length === 0) {
return
}
setResultsPanelOpen(true)
const requestId = mapAnalysisRequestSequence.current + 1
mapAnalysisRequestSequence.current = requestId
const startedAt = Date.now()
@@ -2127,7 +2096,10 @@ export function MapWorkspace({
const handleMapBboxSelect = (bbox: VectorSelectionBBox) => {
setFirstSelectionCorner(null)
setBboxSelectionMode(false)
void analyzeSelection(bbox, areaIdForSelection(bbox))
clearThemeInsights()
clearTemporalComparison()
setResultsPanelOpen(false)
setSelectionBbox(bbox)
}
const runQuickAoiExtract = () => {
@@ -2304,11 +2276,11 @@ export function MapWorkspace({
) : null}
<div className="geo-explorer-layout">
<aside className="geo-theme-panel" aria-label="Datathema kiezen">
<aside className="geo-theme-panel" aria-label="Themas en modellen kiezen">
<div className="geo-panel-heading">
<div>
<h3>Focus op de kaart <small>optioneel</small></h3>
<p>Dit bepaalt de zichtbare laag en hoofdmeting; Inzichten controleert ook de overige beschikbare themas.</p>
<h3>Kies themas</h3>
<p>Alleen uw gekozen bronnen en modellen worden geanalyseerd.</p>
</div>
</div>
<div className="geo-loaded-scope" aria-label="Ingeladen regiobereik">
@@ -2328,15 +2300,13 @@ export function MapWorkspace({
{DATA_THEMES.map((theme) => {
const dataset = themeDatasetMap[theme.id]
const onDemandProduct = onDemandProductMap.get(theme.id)
const partitions = themePartitionMap[theme.id]
const temporalGroups = themeTemporalSeriesMap[theme.id]
const temporalGroup = temporalGroups[0]
const evolutionAvailable = temporalGroups.some((group) => group.items.length >= 2)
const available = !workspaceLoading && (analysisMode === 'current'
? Boolean(dataset || (!readOnly && onDemandProduct))
: Boolean(dataset) && evolutionAvailable)
const active = activeThemeId === theme.id
const temporalRange = temporalGroup ? temporalRangeLabel(temporalGroup.items) : null
const active = selectedThemeIds.includes(theme.id)
return (
<button
className={active ? 'geo-theme-option geo-theme-option-active' : 'geo-theme-option'}
@@ -2351,32 +2321,36 @@ export function MapWorkspace({
<strong>{theme.label}</strong>
<small>
{workspaceLoading
? 'Beschikbaarheid controleren'
? 'Controleren'
: analysisMode === 'evolution'
? evolutionAvailable
? `${temporalGroup.items.length} meetmomenten${temporalRange ? ` · ${temporalRange}` : ''}`
? evolutionAvailable ? `${temporalGroup.items.length} meetmomenten` : 'Geen tijdreeks'
: dataset
? 'Alleen huidige toestand'
: 'Bron nog niet ingeladen'
: dataset
? `${datasetAvailabilityLabel(dataset, partitions)}${onDemandProduct ? ' · zo nodig automatisch aangevuld' : ''}`
? 'Lokaal beschikbaar'
: onDemandProduct
? readOnly ? 'Niet opgenomen in deze demo' : onDemandProduct.availabilityLabel
: 'Bron nog niet ingeladen'}
? readOnly ? 'Niet in demo' : 'Op aanvraag'
: 'Niet beschikbaar'}
</small>
</span>
<i>
{workspaceLoading
? 'Laden'
: analysisMode === 'evolution'
? evolutionAvailable ? 'Tijdreeks' : dataset ? 'Alleen huidig' : 'Ontbreekt'
: dataset ? 'Beschikbaar' : onDemandProduct ? readOnly ? 'Niet in demo' : 'Automatisch' : 'Ontbreekt'}
</i>
<i>{active ? 'Gekozen' : available ? 'Kies' : '—'}</i>
</button>
)
})}
</div>
<div className="geo-theme-actions" aria-live="polite">
<span>{selectedThemes.length === 0 ? 'Nog niets gekozen' : `${selectedThemes.length} gekozen`}</span>
<button
className="primary-action"
type="button"
disabled={!mapSelectionBbox || selectedThemes.length === 0 || themeResultsLoading || mapSelectionLoading}
onClick={() => mapSelectionBbox && void analyzeSelection(mapSelectionBbox, areaIdForSelection(mapSelectionBbox))}
>
<Play aria-hidden="true" />
Analyseer selectie
</button>
<small>{mapSelectionBbox ? 'Start alleen de gekozen analyses.' : 'Teken eerst een gebied.'}</small>
</div>
<div className="geo-source-summary">
<span>{analysisOverlayActive ? 'Actieve analyselaag' : analysisMode === 'evolution' ? 'Tijdreeks' : 'Actieve bron'}</span>
<strong>
@@ -2578,22 +2552,16 @@ export function MapWorkspace({
<p>
{bboxSelectionMode
? 'Sleep nu een rechthoek op de kaart.'
: !activeThemeAvailable
? 'Teken een rechthoek om de databeschikbaarheid voor dit gebied te controleren.'
: regionalRasterThemeActive
? 'Teken een rechthoek; de juiste gemeentelijke rasters worden automatisch gecombineerd.'
: onDemandThemeActive
? 'Teken een rechthoek; officiële regionale kaartbronnen worden begrensd opgehaald, bewaard en hergebruikt.'
: regionalBathymetryThemeActive
? 'Teken een rechthoek of analyseer Vlaanderen; alleen overlappende VHA-partities worden samengevoegd.'
: 'Sleep een rechthoek of analyseer het volledige werkgebied.'}
: mapSelectionBbox
? 'Gebied gekozen. Selecteer links één of meer themas en start de analyse.'
: 'Teken een rechthoek of gebruik het volledige werkgebied. Er wordt nog niets automatisch geladen.'}
</p>
</div>
</div>
<div className="geo-map-actions">
<button
className={bboxSelectionMode ? 'primary-action geo-draw-active' : 'primary-action'}
disabled={(!activeThemeAvailable && !coverageSelectionAvailable) || (analysisMode === 'evolution' && activeTemporalSeries.length < 2) || mapSelectionLoading || themeResultsLoading}
disabled={!selectedProjectId || workspaceLoading || mapSelectionLoading || themeResultsLoading}
type="button"
onClick={startBboxSelection}
>
@@ -2602,17 +2570,19 @@ export function MapWorkspace({
</button>
<button
className="secondary-action"
disabled={(!activeThemeAvailable && !coverageSelectionAvailable) || regionalRasterThemeActive || regionalOnDemandThemeActive || (analysisMode === 'evolution' && activeTemporalSeries.length < 2) || !selectedAreaBbox || mapSelectionLoading || themeResultsLoading}
disabled={!selectedAreaBbox || workspaceLoading || mapSelectionLoading || themeResultsLoading}
type="button"
title={
regionalRasterThemeActive || regionalOnDemandThemeActive
? 'Teken een begrensde rechthoek voor deze regionale analyse.'
: undefined
}
onClick={() => selectedAreaBbox && void analyzeSelection(selectedAreaBbox, selectedMapArea?.id)}
onClick={() => {
if (!selectedAreaBbox) return
clearThemeInsights()
clearTemporalComparison()
setResultsPanelOpen(false)
setSelectionBbox(selectedAreaBbox)
}}
>
<MapPinned aria-hidden="true" />
{regionalRasterThemeActive || regionalOnDemandThemeActive ? 'Selecteer een deelgebied' : 'Volledig werkgebied'}
Volledig werkgebied
</button>
<button className="secondary-action" disabled={!mapSelectionBbox} type="button" onClick={clearAreaSelection}>
<Trash2 aria-hidden="true" />
@@ -2647,14 +2617,14 @@ export function MapWorkspace({
selectionMode={bboxSelectionMode}
hasSelection={Boolean(mapSelectionBbox)}
sourceLoading={workspaceLoading || coverageLoading || officialMapProductsLoading}
sourceReady={activeThemeAvailable && !workspaceLoading && !coverageLoading}
sourceReady={selectedThemes.length > 0 && !workspaceLoading}
processing={liveJourneyProcessing}
validating={liveJourneyValidating}
hasResult={liveJourneyHasResult}
verified={liveJourneyVerified}
error={liveJourneyError}
selectionLabel={mapSelectionBbox ? 'Begrensde kaartselectie' : selectedMapArea?.name ?? 'Nog geen gebied geselecteerd'}
sourceLabel={activeThemeAvailable ? activeTheme.shortLabel : 'Dekking wordt gecontroleerd'}
sourceLabel={selectedThemes.length > 0 ? `${selectedThemes.length} gekozen` : 'Kies themas'}
statusMessage={liveJourneyStatus}
resultLabel={liveJourneyResultLabel}
/>
@@ -2703,16 +2673,28 @@ export function MapWorkspace({
</div>
</div>
<button
className={resultsPanelOpen ? 'geo-results-toggle geo-results-toggle-open' : 'geo-results-toggle'}
type="button"
aria-controls="geo-explorer-results"
aria-expanded={resultsPanelOpen}
onClick={() => setResultsPanelOpen((open) => !open)}
>
{resultsPanelOpen ? <ChevronRight aria-hidden="true" /> : <ChevronLeft aria-hidden="true" />}
<span>{resultsPanelOpen ? 'Sluit inzichten' : 'Inzichten'}</span>
</button>
<aside
id="geo-explorer-results"
className="geo-results-panel"
className={resultsPanelOpen ? 'geo-results-panel geo-results-panel-open' : 'geo-results-panel'}
aria-label="Gebiedsanalyse"
aria-live="polite"
aria-hidden={!resultsPanelOpen}
>
<div className="geo-panel-heading">
<div>
<h3>Inzichten</h3>
<p>Gemeten resultaten en beschikbaarheid voor de volledige selectie.</p>
<p>Resultaten van de gekozen analyses.</p>
</div>
</div>
@@ -2781,12 +2763,17 @@ export function MapWorkspace({
{!mapSelectionBbox ? (
<div className="geo-results-empty">
<strong>Nog geen gebied geselecteerd</strong>
<p>Teken een rechthoek op de kaart. De analyse start automatisch zodra je loslaat.</p>
<p>Teken een rechthoek, kies themas en start daarna de analyse.</p>
</div>
) : mapSelectionLoading || themeResultsLoading || temporalComparisonLoading ? (
<div className="geo-results-loading" role="status">
<span />
<strong>De gekozen officiële bron wordt begrensd geladen en geanalyseerd</strong>
<strong>De gekozen bronnen worden begrensd geladen en geanalyseerd</strong>
</div>
) : analysisMode === 'current' && themeInsights.length === 0 && !mapSelectionResult ? (
<div className="geo-results-empty">
<strong>Nog niet geanalyseerd</strong>
<p>Kies één of meer themas en gebruik Analyseer selectie.</p>
</div>
) : (
<>
@@ -2895,16 +2882,17 @@ export function MapWorkspace({
<div className="geo-theme-results">
<div className="geo-results-title-row">
<h4>Gekozen thema</h4>
<span>{activeSelectionResult ? 'Uitgelezen' : 'Geen meting'}</span>
<h4>Gekozen themas</h4>
<span>{themeResults.length}/{selectedThemes.length} uitgelezen</span>
</div>
{[activeTheme].flatMap((theme) => {
{selectedThemes.flatMap((theme) => {
const dataset = themeDatasetMap[theme.id]
const items = themeResults.filter((result) => result.theme.id === theme.id)
const rows = items.length > 0 ? items : [null]
const themeSupportsCurrentSelection = selectionRelevantThemes.some((candidate) => candidate.id === theme.id)
return rows.map((item, index) => {
const resultDataset = item?.dataset ?? (activeThemeSupportsCurrentSelection ? dataset : undefined)
const unavailableAtScale = !activeThemeSupportsCurrentSelection
const resultDataset = item?.dataset ?? (themeSupportsCurrentSelection ? dataset : undefined)
const unavailableAtScale = !themeSupportsCurrentSelection
return (
<div className="geo-theme-result-row" key={`${theme.id}:${resultDataset?.id ?? index}`}>
<span className={`geo-theme-symbol geo-theme-symbol-${theme.id}`} aria-hidden="true" />
+175
View File
@@ -1647,3 +1647,178 @@ button.overview-command-card { cursor: pointer; }
transition-duration: 0.01ms !important;
}
}
/* Explicit analysis selection and result drawer */
.geo-explorer-layout,
.workbench-shell-guest .geo-explorer-layout {
grid-template-columns: clamp(15rem, 13vw, 18rem) minmax(0, 1fr);
}
.geo-theme-option {
grid-template-columns: 0.55rem minmax(0, 1fr) auto;
gap: var(--gi-space-2);
min-width: 0;
padding: 0.48rem 0.55rem;
}
.geo-theme-option > .geo-theme-symbol {
grid-column: 1;
width: 0.42rem;
}
.geo-theme-option > span:nth-child(2) {
grid-column: 2;
min-width: 0;
}
.geo-theme-option > span:nth-child(2) > :where(strong, small) {
overflow: hidden;
overflow-wrap: normal;
text-overflow: ellipsis;
white-space: nowrap;
}
.geo-theme-option > i {
grid-column: 3;
max-width: 4.25rem;
overflow: hidden;
justify-self: end;
text-overflow: ellipsis;
white-space: nowrap;
}
.geo-theme-actions {
display: grid;
flex: 0 0 auto;
gap: var(--gi-space-2);
padding: var(--gi-space-3) var(--gi-space-4);
border-top: 1px solid var(--gi-line);
background: var(--gi-surface-soft);
}
.geo-theme-actions > span {
color: var(--gi-ink-700);
font-size: var(--gi-text-2xs);
font-weight: 750;
}
.geo-theme-actions button {
display: inline-flex;
gap: var(--gi-space-2);
align-items: center;
justify-content: center;
width: 100%;
}
.geo-theme-actions button svg {
width: 0.9rem;
height: 0.9rem;
}
.geo-theme-actions small {
color: var(--gi-ink-500);
font-size: var(--gi-text-3xs);
}
.geo-results-panel {
visibility: hidden;
animation: none;
pointer-events: none;
transform: translateX(100%);
transition: transform var(--gi-dur-3) var(--gi-ease-out), visibility 0s linear var(--gi-dur-3);
}
.geo-results-panel.geo-results-panel-open {
visibility: visible;
pointer-events: auto;
transform: translateX(0);
transition-delay: 0s;
}
.geo-results-panel::before {
display: none;
}
.geo-results-toggle {
position: absolute;
z-index: 7;
top: 50%;
right: 0;
display: inline-flex;
gap: 0.3rem;
align-items: center;
min-height: 2.5rem;
border: 1px solid rgba(255, 255, 255, 0.2);
border-right: 0;
border-radius: var(--gi-radius-md) 0 0 var(--gi-radius-md);
padding: 0.55rem 0.7rem;
background: var(--gi-brand-800);
color: #fff;
font-size: var(--gi-text-2xs);
font-weight: 750;
box-shadow: var(--gi-shadow-md);
transform: translateY(-50%);
transition: right var(--gi-dur-3) var(--gi-ease-out), background var(--gi-dur-2) var(--gi-ease);
}
.geo-results-toggle:hover,
.geo-results-toggle:focus-visible {
background: var(--gi-brand-700);
}
.geo-results-toggle-open {
right: clamp(23rem, 25vw, 28rem);
}
.geo-results-toggle svg {
width: 0.9rem;
height: 0.9rem;
}
@media (max-width: 1240px) {
.geo-results-panel {
position: absolute;
inset-block: 0;
right: 0;
width: min(28rem, calc(100% - 3rem));
height: 100%;
max-height: none;
overflow-y: auto;
border-top: 0;
border-left: 1px solid var(--gi-line);
box-shadow: var(--gi-shadow-lg);
transform: translateX(100%) !important;
}
.geo-results-panel.geo-results-panel-open {
transform: translateX(0) !important;
}
.geo-results-toggle-open {
right: min(28rem, calc(100% - 3rem));
}
}
@media (max-width: 620px) {
.geo-theme-option > i {
max-width: 3.75rem;
}
.geo-results-toggle span {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.geo-results-panel,
.geo-results-toggle {
transition-duration: 0.01ms !important;
}
}
.geo-source-summary :where(strong, small) {
min-width: 0;
max-width: 100%;
overflow: hidden;
overflow-wrap: anywhere;
text-overflow: ellipsis;
}