diff --git a/apps/geolibre-desktop/src/components/layout/DesktopShell.tsx b/apps/geolibre-desktop/src/components/layout/DesktopShell.tsx index ae656a816..90d197caf 100644 --- a/apps/geolibre-desktop/src/components/layout/DesktopShell.tsx +++ b/apps/geolibre-desktop/src/components/layout/DesktopShell.tsx @@ -866,6 +866,10 @@ export function DesktopShell({ useNetcdfIdentify(mapControllerRef, mapReadyGeneration); const [layerPanelWidth, setLayerPanelWidth] = useState(initialSidePanelWidth); const [stylePanelWidth, setStylePanelWidth] = useState(initialSidePanelWidth); + const [stylePanelOpenRequest, setStylePanelOpenRequest] = useState(0); + const openStylePanel = useCallback(() => { + setStylePanelOpenRequest((request) => request + 1); + }, []); const [notebookPanelWidth, setNotebookPanelWidth] = useState(DEFAULT_NOTEBOOK_PANEL_WIDTH); // Opening the notebook (Processing → Jupyter Notebook) splits the workspace // 50/50 between the map and the notebook: we size the notebook to half of the @@ -2245,6 +2249,9 @@ export function DesktopShell({ onOpenRasterStylePanel={() => openRasterLayerPanel(createAppAPI(mapControllerRef)) } + onOpenStylePanel={ + layoutOptions.stylePanelVisible ? openStylePanel : undefined + } onOpenRasterSubset={setRasterSubsetLayer} collapsed={collapsed} onCollapsedChange={onCollapsedChange} @@ -2268,6 +2275,7 @@ export function DesktopShell({ onOpenRasterStylePanel={() => openRasterLayerPanel(createAppAPI(mapControllerRef)) } + onOpenStylePanel={layoutOptions.stylePanelVisible ? openStylePanel : undefined} onOpenRasterSubset={setRasterSubsetLayer} autoCollapse={ storymapPresenting || @@ -2493,6 +2501,7 @@ export function DesktopShell({ void; /** Open the floating Add Raster Layer panel for advanced raster styling. */ onOpenRasterStylePanel: () => void; + /** + * Select the target layer and expand the built-in Style panel. Left undefined + * when that panel is hidden (Settings → "Show Style panel"), which also hides + * the menu item — the panel is not mounted then, so the request would be + * dropped rather than queued. + */ + onOpenStylePanel?: () => void; /** * Open the floating Extract Subset panel for a COG/WMS/XYZ layer, letting the * user draw a bounding box and export a clipped GeoTIFF. @@ -608,6 +615,7 @@ export function LayerPanel({ onCancelGeometryEdit, onMaterializeDuckDBLayer, onOpenRasterStylePanel, + onOpenStylePanel, onOpenRasterSubset, autoCollapse = false, collapsed: controlledCollapsed, @@ -3364,6 +3372,17 @@ export function LayerPanel({ action item below has no such focus target, so each lets Radix dismiss the menu on select rather than leaving it pinned open. */} + {onOpenStylePanel && ( + { + selectLayer(layer.id); + onOpenStylePanel(); + }} + > + + {t("layers.openStylePanel")} + + )} { addLayerGroup(undefined, moveIds); diff --git a/apps/geolibre-desktop/src/components/panels/StylePanel.tsx b/apps/geolibre-desktop/src/components/panels/StylePanel.tsx index 3a4da86b9..39d6bcf95 100644 --- a/apps/geolibre-desktop/src/components/panels/StylePanel.tsx +++ b/apps/geolibre-desktop/src/components/panels/StylePanel.tsx @@ -173,6 +173,8 @@ function labelOverrideInvalid( interface StylePanelProps { mapControllerRef: RefObject; onResizeStart: (event: ReactPointerEvent) => void; + /** Incremented when another part of the UI explicitly requests this panel. */ + openRequest?: number; /** * When this flips to `true` the panel collapses to its thin rail (it is not * unmounted). Used to clear room when the notebook opens beside the map; the @@ -984,6 +986,7 @@ function RasterStyleSlider({ export function StylePanel({ mapControllerRef, onResizeStart, + openRequest = 0, autoCollapse = false, collapsed: controlledCollapsed, onCollapsedChange, @@ -998,8 +1001,8 @@ export function StylePanel({ const updateLayer = useAppStore((s) => s.updateLayer); const moveLayer = useAppStore((s) => s.moveLayer); const projectName = useAppStore((s) => s.projectName); - // Style starts on its rail on every platform. Selecting a real layer below - // expands it; selecting the special Background row does not. + // Style starts on its rail on every platform and remains there until the + // user explicitly expands it. const [internalCollapsed, setInternalCollapsed] = useState(true); // In the shared right-sidebar mode the parent owns collapse (controlled); // otherwise the panel manages it locally. `setIsCollapsed` routes to whichever @@ -1013,23 +1016,19 @@ export function StylePanel({ }, [isControlled, onCollapsedChange], ); - // Selecting a real layer expands the panel from its rail. Skipped while - // `autoCollapse` holds it closed (the notebook or a story-map presentation - // owns the workspace), so a selection made there cannot pop Style back open - // over them and defeat the auto-collapse below. - const previousSelectedLayerId = useRef(selectedLayerId); + // An explicit request (Layers → "Open Style panel") expands the panel from its + // rail. Skipped while `autoCollapse` holds it closed (the notebook or a + // story-map presentation owns the workspace), so a request made there cannot + // pop Style back open over them: the `autoCollapse` effect below acts only on + // transitions, so an expand that slipped through would stick until the + // notebook was closed and reopened. The request is still consumed so it does + // not fire later. + const previousOpenRequest = useRef(openRequest); useEffect(() => { - const previous = previousSelectedLayerId.current; - previousSelectedLayerId.current = selectedLayerId; - if ( - !autoCollapse && - selectedLayerId && - selectedLayerId !== previous && - layers.some((candidate) => candidate.id === selectedLayerId) - ) { - setIsCollapsed(false); - } - }, [autoCollapse, layers, selectedLayerId, setIsCollapsed]); + if (openRequest === previousOpenRequest.current) return; + previousOpenRequest.current = openRequest; + if (!autoCollapse) setIsCollapsed(false); + }, [autoCollapse, openRequest, setIsCollapsed]); // Collapse to the rail when `autoCollapse` flips on (e.g. the notebook opens), // and restore the prior expand/collapse state when it flips back off (notebook // closes). Both act only on the transition so the user can still toggle the diff --git a/apps/geolibre-desktop/src/i18n/locales/en.json b/apps/geolibre-desktop/src/i18n/locales/en.json index 70840f530..a58416746 100644 --- a/apps/geolibre-desktop/src/i18n/locales/en.json +++ b/apps/geolibre-desktop/src/i18n/locales/en.json @@ -4892,6 +4892,7 @@ "bindWindowCumulative": "Everything up to the current step (cumulative)", "bindCancel": "Cancel", "bindConfirm": "Bind", + "openStylePanel": "Open Style panel", "openRasterStylePanel": "Edit raster style…", "exportRasterSuccess": "Raster exported.", "exportRasterError": "Could not export this raster.", diff --git a/docs/tutorials/first-map.md b/docs/tutorials/first-map.md index 6ac00df73..481628947 100644 --- a/docs/tutorials/first-map.md +++ b/docs/tutorials/first-map.md @@ -21,7 +21,7 @@ See [Adding Data](../user-guide/adding-data.md) for every supported source. ## 3. Style the layer -1. Select the `countries` layer in the Layers panel. The [Style panel](../user-guide/styling.md) opens on the right. +1. Select the `countries` layer in the Layers panel, then expand the [Style panel](../user-guide/styling.md) on the right if it is collapsed. 2. Adjust the **Fill color**, **Outline color**, and **Fill opacity** to taste. 3. To make a choropleth, set **Style type** to **Graduated**, pick a numeric field (for example a population or GDP column), choose a **Colormap**, and click **Apply style type**.