From e2ec73c00e1689d0134116fe52a87974c5b8e855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Thu, 16 Apr 2026 15:16:35 +0200 Subject: [PATCH 1/9] Improve atlas field panel styling --- .../components/blocks/atlas/atlas-panels.tsx | 94 ++++++++++++++----- .../components/blocks/atlas/atlas-styles.tsx | 11 +++ ..._id_farm.$calendar.atlas.fields._index.tsx | 9 +- ....$b_id_farm.$calendar.field.new._index.tsx | 21 +++-- ...farm.create.$b_id_farm.$calendar.atlas.tsx | 19 ++-- fdm-app/app/tailwind.css | 12 ++- 6 files changed, 117 insertions(+), 49 deletions(-) diff --git a/fdm-app/app/components/blocks/atlas/atlas-panels.tsx b/fdm-app/app/components/blocks/atlas/atlas-panels.tsx index c5e0632cc..062610125 100644 --- a/fdm-app/app/components/blocks/atlas/atlas-panels.tsx +++ b/fdm-app/app/components/blocks/atlas/atlas-panels.tsx @@ -1,7 +1,7 @@ import type { FeatureCollection } from "geojson" import throttle from "lodash.throttle" import { Check, Info } from "lucide-react" -import type { MapLibreZoomEvent } from "maplibre-gl" +import type { MapGeoJSONFeature, MapLibreZoomEvent } from "maplibre-gl" import { useCallback, useEffect, useState } from "react" import type { MapLayerMouseEvent as MapMouseEvent } from "react-map-gl/maplibre" import { useMap } from "react-map-gl/maplibre" @@ -27,7 +27,7 @@ export function FieldsPanelHover({ clickRedirectsToDetailsPage = false, }: { zoomLevelFields: number - layer: string + layer: string[] | string layerExclude?: string[] | string clickRedirectsToDetailsPage?: boolean }) { @@ -39,11 +39,19 @@ export function FieldsPanelHover({ // Set message about zoom level const zoom = map.getZoom() if (zoom && zoom > zoomLevelFields) { - if (!map.getStyle() || !map.getLayer(layer)) return - + if (!map.getStyle()) return + const layers = Array.isArray(layer) ? layer : [layer] + const validLayers = layers.filter((l) => map.getLayer(l)) + if (validLayers.length === 0) return const features = map.queryRenderedFeatures(evt.point, { - layers: [layer], + layers: validLayers, }) + // Layer, whose id is specified last in the layer prop, has the highest priority + features.sort( + (f1, f2) => + validLayers.indexOf(f2.layer.id) - + validLayers.indexOf(f1.layer.id), + ) if (layerExclude) { const layers = Array.isArray(layerExclude) @@ -61,7 +69,7 @@ export function FieldsPanelHover({ }, ) if (featuresExclude && featuresExclude.length > 0) { - setPanel(null) + setPanel(makePanel({})) return } } @@ -73,13 +81,34 @@ export function FieldsPanelHover({ features[0].properties ) { setPanel( - + makePanel({ + layer: features[0].layer.id, + feature: features[0], + }), + ) + } else { + setPanel(makePanel({})) + } + + function makePanel({ + layer, + feature, + }: { + layer?: string + feature?: MapGeoJSONFeature + }) { + const active = layer && feature + const name = feature + ? layer === "fieldsSaved" + ? features[0].properties.b_name + : features[0].properties.b_lu_name + : "Naam" + return ( + - - {layer === "fieldsSaved" - ? features[0].properties.b_name - : features[0].properties.b_lu_name} - + {name} {layer === "fieldsSaved" ? `${features[0].properties.b_area} ha` @@ -90,26 +119,47 @@ export function FieldsPanelHover({ : "Klik om te verwijderen"} - , + ) - } else { - setPanel(null) } } } } - const throttledUpdatePanel = throttle(updatePanel, 250, { - trailing: true, - }) + // Throttle panel updates to not overwhelm React, the rendering thread etc. + const throttleInterval = 200 + const throttledUpdatePanelInner = throttle( + updatePanel, + throttleInterval, + { + trailing: true, + }, + ) + + // Delay handling of clicks so that if the field selection under the mouse changes we catch it + let delayedUpdateTimeout: ReturnType + const delayedUpdatePanel: typeof updatePanel = (e) => { + delayedUpdateTimeout = setTimeout( + () => throttledUpdatePanelInner(e), + throttleInterval, + ) + } + + // Cancels any timed out invocations and tries to invoke again + const throttledUpdatePanel: typeof updatePanel = (e) => { + clearTimeout(delayedUpdateTimeout) + throttledUpdatePanelInner(e) + } if (map) { map.on("mousemove", throttledUpdatePanel) + map.on("mousedown", delayedUpdatePanel) map.on("click", updatePanel) map.on("zoom", throttledUpdatePanel) map.on("load", updatePanel) return () => { map.off("mousemove", throttledUpdatePanel) + map.off("mousedown", delayedUpdatePanel) map.off("click", updatePanel) map.off("zoom", throttledUpdatePanel) map.off("load", updatePanel) @@ -252,21 +302,21 @@ export function FieldsPanelSelection({ ) setPanel( - + Percelen {fieldCountText} - -
+ +
{cultivations.map((cultivation, _index) => ( // let cultivationCountText = `${cultivation.count + 1} percelen`
)} -
+
-
) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.new._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.new._index.tsx index d824833e3..aba05ab0e 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.new._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.field.new._index.tsx @@ -236,6 +236,7 @@ export default function Index() { const calendar = loaderData.calendar const fieldsSavedStyle = getFieldsStyle(fieldsSavedId) + const fieldsSelectedOutlineStyle = getFieldsStyle("fieldsSelectedOutline") const fieldsSavedOutlineStyle = getFieldsStyle("fieldsSavedOutline") // Set selected fields @@ -306,7 +307,7 @@ export default function Index() { {...viewState} // Use viewState directly ref={mapRef} style={{ - height: "calc(100vh - 64px - 123px)", + height: "calc(100vh - 64px - 123px - 24px)", width: "100%", }} interactive={true} @@ -398,6 +399,12 @@ export default function Index() { layout: layerLayout, } as any)} /> + -
+
-
diff --git a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.atlas.tsx b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.atlas.tsx index dac5e27f4..a56b1b78a 100644 --- a/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.atlas.tsx +++ b/fdm-app/app/routes/farm.create.$b_id_farm.$calendar.atlas.tsx @@ -221,6 +221,7 @@ export default function Index() { const fieldsSaved = loaderData.fieldsSaved const fieldsSavedStyle = getFieldsStyle(fieldsSavedId) + const fieldsSelectedOutlineStyle = getFieldsStyle("fieldsSelectedOutline") const fieldsSavedOutlineStyle = getFieldsStyle("fieldsSavedOutline") // Set selected fields @@ -367,6 +368,12 @@ export default function Index() { layout: layerLayout, } as any)} /> + -
+
-
diff --git a/fdm-app/app/tailwind.css b/fdm-app/app/tailwind.css index 4559fd917..fa24d0f38 100644 --- a/fdm-app/app/tailwind.css +++ b/fdm-app/app/tailwind.css @@ -165,13 +165,19 @@ .fields-panel { position: absolute; + box-sizing: border-box; top: 0; left: 0; - /* max-width: 320px; */ + max-height: 100%; + width: 320px; /* background: #fff; */ /* box-shadow: 0 2px 4px rgba(0,0,0,0.3); */ - padding: 12px 24px; - margin: 20px; + display: flex; + flex-direction: column; + margin-left: 44px; + padding-block: 32px; + gap: 16px; + /* margin: 20px; */ /* font-size: 13px; */ /* line-height: 2; */ /* color: #6b6b76; */ From 9ff980bd54d7aa37a0f4d1d5d48faffb45abd7b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Thu, 16 Apr 2026 15:20:25 +0200 Subject: [PATCH 2/9] Add changeset --- .changeset/moody-berries-wave.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/moody-berries-wave.md diff --git a/.changeset/moody-berries-wave.md b/.changeset/moody-berries-wave.md new file mode 100644 index 000000000..fad036856 --- /dev/null +++ b/.changeset/moody-berries-wave.md @@ -0,0 +1,5 @@ +--- +"@nmi-agro/fdm-app": patch +--- + +The cultivation and field count list seen on field selection atlas pages now becomes scrollable when there are too many different cultivations to display, ensuring that the "Sla geselecteerde percelen op" button is always reachable. From dcd88b290bbbfa87c3c293bf61a40ba132317618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Thu, 16 Apr 2026 15:29:26 +0200 Subject: [PATCH 3/9] Remove click handlers that don't work --- fdm-app/app/components/blocks/atlas/atlas-panels.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/fdm-app/app/components/blocks/atlas/atlas-panels.tsx b/fdm-app/app/components/blocks/atlas/atlas-panels.tsx index 062610125..b1fe4e1a1 100644 --- a/fdm-app/app/components/blocks/atlas/atlas-panels.tsx +++ b/fdm-app/app/components/blocks/atlas/atlas-panels.tsx @@ -154,13 +154,11 @@ export function FieldsPanelHover({ if (map) { map.on("mousemove", throttledUpdatePanel) map.on("mousedown", delayedUpdatePanel) - map.on("click", updatePanel) map.on("zoom", throttledUpdatePanel) map.on("load", updatePanel) return () => { map.off("mousemove", throttledUpdatePanel) map.off("mousedown", delayedUpdatePanel) - map.off("click", updatePanel) map.off("zoom", throttledUpdatePanel) map.off("load", updatePanel) } From a5aeff62bfb0b85ffbcc188165aa9724545193bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Fri, 17 Apr 2026 13:13:48 +0200 Subject: [PATCH 4/9] Nitpicks --- .../components/blocks/atlas/atlas-panels.tsx | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/fdm-app/app/components/blocks/atlas/atlas-panels.tsx b/fdm-app/app/components/blocks/atlas/atlas-panels.tsx index b1fe4e1a1..a066819be 100644 --- a/fdm-app/app/components/blocks/atlas/atlas-panels.tsx +++ b/fdm-app/app/components/blocks/atlas/atlas-panels.tsx @@ -33,6 +33,16 @@ export function FieldsPanelHover({ }) { const { current: map } = useMap() const [panel, setPanel] = useState(null) + const layerIds = Array.isArray(layer) ? layer : [layer] + const excludedLayerIds = layerExclude + ? Array.isArray(layerExclude) + ? layerExclude + : [layerExclude] + : [] + const layerIdsKey = layerIds.join("|") + const excludedLayerIdsKey = excludedLayerIds.join("|") + + // biome-ignore lint/correctness/useExhaustiveDependencies: effective changes in layer and layerExclude are detected through layerIdsKey and excludedLayerIdsKey useEffect(() => { function updatePanel(evt: MapMouseEvent | MapLibreZoomEvent) { if (map) { @@ -40,8 +50,11 @@ export function FieldsPanelHover({ const zoom = map.getZoom() if (zoom && zoom > zoomLevelFields) { if (!map.getStyle()) return - const layers = Array.isArray(layer) ? layer : [layer] - const validLayers = layers.filter((l) => map.getLayer(l)) + if (!("point" in evt)) { + setPanel(makePanel({})) + return + } + const validLayers = layerIds.filter((l) => map.getLayer(l)) if (validLayers.length === 0) return const features = map.queryRenderedFeatures(evt.point, { layers: validLayers, @@ -54,10 +67,7 @@ export function FieldsPanelHover({ ) if (layerExclude) { - const layers = Array.isArray(layerExclude) - ? layerExclude - : [layerExclude] - const validLayers = layers.filter((l) => + const validLayers = excludedLayerIds.filter((l) => map.getLayer(l), ) @@ -161,9 +171,19 @@ export function FieldsPanelHover({ map.off("mousedown", delayedUpdatePanel) map.off("zoom", throttledUpdatePanel) map.off("load", updatePanel) + + // Cancel pending updates + clearTimeout(delayedUpdateTimeout) + throttledUpdatePanelInner.cancel() } } - }, [map, zoomLevelFields, layer, layerExclude, clickRedirectsToDetailsPage]) + }, [ + map, + zoomLevelFields, + layerIdsKey, + excludedLayerIdsKey, + clickRedirectsToDetailsPage, + ]) return panel } From 93ee44ce7c8f9c9f48fc280fc4e7efbc8223f580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Mon, 20 Apr 2026 13:09:53 +0200 Subject: [PATCH 5/9] Add scroll area endpoint fade effect to the field selection list --- .../components/blocks/atlas/atlas-panels.tsx | 118 +++++++++++++----- 1 file changed, 84 insertions(+), 34 deletions(-) diff --git a/fdm-app/app/components/blocks/atlas/atlas-panels.tsx b/fdm-app/app/components/blocks/atlas/atlas-panels.tsx index a066819be..35df57318 100644 --- a/fdm-app/app/components/blocks/atlas/atlas-panels.tsx +++ b/fdm-app/app/components/blocks/atlas/atlas-panels.tsx @@ -2,7 +2,7 @@ import type { FeatureCollection } from "geojson" import throttle from "lodash.throttle" import { Check, Info } from "lucide-react" import type { MapGeoJSONFeature, MapLibreZoomEvent } from "maplibre-gl" -import { useCallback, useEffect, useState } from "react" +import { useCallback, useEffect, useRef, useState } from "react" import type { MapLayerMouseEvent as MapMouseEvent } from "react-map-gl/maplibre" import { useMap } from "react-map-gl/maplibre" import { data, NavLink, useFetcher } from "react-router" @@ -247,6 +247,8 @@ export function FieldsPanelSelection({ const fetcher = useFetcher() const { current: map } = useMap() const [panel, setPanel] = useState(null) + const scrollContainerRef = useRef(null) + const scrollRef = useRef(null) const isSubmitting = fetcher.state !== "idle" @@ -275,6 +277,26 @@ export function FieldsPanelSelection({ [fetcher], ) + function handleScroll( + scrollElement: HTMLDivElement, + scrollContainerElement: HTMLDivElement, + ) { + if (scrollElement.scrollTop > 5) { + scrollContainerElement.dataset.scrollStart = "" + } else { + delete scrollContainerElement.dataset.scrollStart + } + + if ( + scrollElement.scrollHeight - scrollElement.scrollTop > + 5 + scrollElement.offsetHeight + ) { + scrollContainerElement.dataset.scrollEnd = "" + } else { + delete scrollContainerElement.dataset.scrollEnd + } + } + useEffect(() => { function updatePanel() { if (map) { @@ -298,19 +320,18 @@ export function FieldsPanelSelection({ }[], feature, ) => { - if (!feature.properties) return acc + const cropField = feature.properties + if (!cropField) return acc const existingCultivation = acc.find( - (c) => - c.b_lu_name === - feature.properties.b_lu_name, + (c) => c.b_lu_name === cropField.b_lu_name, ) if (existingCultivation) { existingCultivation.count++ } else { acc.push({ - b_lu_name: feature.properties.b_lu_name, + b_lu_name: cropField.b_lu_name, b_lu_croprotation: - feature.properties.b_lu_croprotation, + cropField.b_lu_croprotation, count: 1, }) } @@ -319,6 +340,8 @@ export function FieldsPanelSelection({ [], ) + const pseudoElemClasses = + "before:absolute before:h-16 before:left-0 before:right-0 before:from-card before:to-transparent before:opacity-0 before:transition-opacity before:duration-250 before:pointer-events-none" setPanel( @@ -327,34 +350,46 @@ export function FieldsPanelSelection({ {fieldCountText} - -
- {cultivations.map((cultivation, _index) => ( - // let cultivationCountText = `${cultivation.count + 1} percelen` + +
+
+ {cultivations.map( + (cultivation, _index) => ( + // let cultivationCountText = `${cultivation.count + 1} percelen` -
- -
-

- {cultivation.b_lu_name} -

-

- {`${cultivation.count} percelen`} -

-
-
- ))} +
+ +
+

+ { + cultivation.b_lu_name + } +

+

+ {`${cultivation.count} percelen`} +

+
+
+ ), + )} +
@@ -426,5 +461,20 @@ export function FieldsPanelSelection({ numFieldsSaved, ]) + useEffect(() => { + const scrollElement = scrollRef.current + const scrollContainerElement = scrollContainerRef.current + if (!scrollElement || !scrollContainerElement) return + const handler = () => { + handleScroll(scrollElement, scrollContainerElement) + } + const timeout = setTimeout(handler, 100) + scrollElement.addEventListener("scroll", handler, { passive: true }) + return () => { + scrollElement.removeEventListener("scroll", handler) + clearTimeout(timeout) + } + }) + return panel } From 500ad9c1fe88ebe7751d09d60f5283e603c9398e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Mon, 20 Apr 2026 13:22:52 +0200 Subject: [PATCH 6/9] Hide hover panel when zoomed out too much --- fdm-app/app/components/blocks/atlas/atlas-panels.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/fdm-app/app/components/blocks/atlas/atlas-panels.tsx b/fdm-app/app/components/blocks/atlas/atlas-panels.tsx index 35df57318..9b4034974 100644 --- a/fdm-app/app/components/blocks/atlas/atlas-panels.tsx +++ b/fdm-app/app/components/blocks/atlas/atlas-panels.tsx @@ -132,6 +132,8 @@ export function FieldsPanelHover({ ) } + } else { + setPanel(null) } } } @@ -165,12 +167,11 @@ export function FieldsPanelHover({ map.on("mousemove", throttledUpdatePanel) map.on("mousedown", delayedUpdatePanel) map.on("zoom", throttledUpdatePanel) - map.on("load", updatePanel) + map.once("load", updatePanel) return () => { map.off("mousemove", throttledUpdatePanel) map.off("mousedown", delayedUpdatePanel) map.off("zoom", throttledUpdatePanel) - map.off("load", updatePanel) // Cancel pending updates clearTimeout(delayedUpdateTimeout) @@ -217,7 +218,7 @@ export function FieldsPanelZoom({ } } - const throttledUpdatePanel = throttle(updatePanel, 250, { + const throttledUpdatePanel = throttle(updatePanel, 200, { trailing: true, }) From 5cf34dcd8c693f71785d0e74afb7c34d23ba426e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Mon, 20 Apr 2026 13:31:39 +0200 Subject: [PATCH 7/9] Make add selected fields button full-width --- fdm-app/app/components/blocks/atlas/atlas-panels.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fdm-app/app/components/blocks/atlas/atlas-panels.tsx b/fdm-app/app/components/blocks/atlas/atlas-panels.tsx index 9b4034974..3095c63b5 100644 --- a/fdm-app/app/components/blocks/atlas/atlas-panels.tsx +++ b/fdm-app/app/components/blocks/atlas/atlas-panels.tsx @@ -172,6 +172,7 @@ export function FieldsPanelHover({ map.off("mousemove", throttledUpdatePanel) map.off("mousedown", delayedUpdatePanel) map.off("zoom", throttledUpdatePanel) + map.off("load", updatePanel) // Cancel pending updates clearTimeout(delayedUpdateTimeout) @@ -395,6 +396,7 @@ export function FieldsPanelSelection({