diff --git a/apps/geolibre-desktop/src/components/layout/DesktopShell.tsx b/apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
index 2b70d0824..b20e5758d 100644
--- a/apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
+++ b/apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
@@ -703,7 +703,14 @@ export function DesktopShell({
el.className = "contents";
return el;
});
- const activePanelId = useRightPanelState().activeId;
+ const rightPanelState = useRightPanelState();
+ const activePanelId = rightPanelState.activeId;
+ const replaceStylePanelIds = rightPanelState.visibleIds.filter(
+ (id) => rightPanelState.panelDocks[id] === "replace-style",
+ );
+ const replaceLayersPanelIds = rightPanelState.visibleIds.filter(
+ (id) => rightPanelState.panelDocks[id] === "replace-layers",
+ );
const activePanel = activePanelId ? getRightPanel(activePanelId) : undefined;
// The plugins in VIEWER_BLOCKED_PLUGIN_IDS paint drawing and editing controls
// onto the map, which the read-only viewer preset cannot hide the way it
@@ -2075,55 +2082,13 @@ export function DesktopShell({
{/* Map-only / hidden-panels embeds show nothing but the map: skip the
whole left side-dock (Layers, plugin panels, and the shared rail that
hosts the Browser entry), not just the built-in Layers panel. */}
- {layoutOptions.panelsHidden ? null : replaceLayersPanelId && !layoutOptions.viewer ? (
- // Shared-rail mode on the Layers (left) side: the plugin panel shares
- // the Layers sidebar surface, so a single rail lists both the workbench
- // and Layers instead of the two positional plugin slots flanking it.
-
- }
- // The Browser docks here on by default but must not bury Layers:
- // start with Layers expanded and Browser a collapsed rail entry.
- // On a phone-width viewport both start collapsed (panels overlay
- // there), matching the mobile "panels default collapsed" behavior.
- initialBuiltinExpanded={
- replaceLayersPanelId === BROWSER_PANEL_ID && !getIsMobileViewport()
- }
- // The story-map presentation is the only standalone Layers
- // autoCollapse trigger (the notebook collapses Style, not Layers).
- forceBuiltinCollapsed={storymapPresenting}
- renderBuiltin={({ collapsed, onCollapsedChange }) => (
-
- openRasterLayerPanel(createAppAPI(mapControllerRef))
- }
- onOpenRasterSubset={setRasterSubsetLayer}
- collapsed={collapsed}
- onCollapsedChange={onCollapsedChange}
- hideOwnRail
- />
- )}
- />
-
- ) : (
+ {layoutOptions.panelsHidden ? null : (
<>
+ {/* The positional plugin docks flank whichever middle surface the
+ Layers side shows (the shared rail or the standalone Layers
+ panel): a panel moved to left/right-of-layers must stay
+ reachable while a shared-rail panel such as the Browser is
+ open. */}
{!layoutOptions.viewer ? (
) : null}
- {layoutOptions.layerPanelVisible ? (
+ {replaceLayersPanelId && !layoutOptions.viewer ? (
+ // Shared-rail mode on the Layers (left) side: the plugin panel shares
+ // the Layers sidebar surface, so a single rail lists both the workbench
+ // and Layers instead of the built-in panel standing on its own.
+
+ }
+ // The Browser docks here on by default but must not bury Layers:
+ // start with Layers expanded and Browser a collapsed rail entry.
+ // On a phone-width viewport both start collapsed (panels overlay
+ // there), matching the mobile "panels default collapsed" behavior.
+ initialBuiltinExpanded={
+ replaceLayersPanelId === BROWSER_PANEL_ID && !getIsMobileViewport()
+ }
+ // The story-map presentation is the only standalone Layers
+ // autoCollapse trigger (the notebook collapses Style, not Layers).
+ forceBuiltinCollapsed={storymapPresenting}
+ renderBuiltin={({ collapsed, onCollapsedChange }) => (
+
+ openRasterLayerPanel(createAppAPI(mapControllerRef))
+ }
+ onOpenRasterSubset={setRasterSubsetLayer}
+ collapsed={collapsed}
+ onCollapsedChange={onCollapsedChange}
+ hideOwnRail
+ />
+ )}
+ />
+
+ ) : layoutOptions.layerPanelVisible ? (
{layoutOptions.viewer ? (
@@ -2311,45 +2324,13 @@ export function DesktopShell({
{/* Same as the left dock: a map-only / hidden-panels embed skips the
entire right side-dock (Style, plugin panels, and their shared rail). */}
- {layoutOptions.panelsHidden || layoutOptions.viewer ? null : replaceStylePanelId ? (
- // Shared-rail mode (issue #765): the plugin panel shares the Style
- // sidebar surface, so a single rail lists both the workbench and Style
- // instead of the two positional plugin slots flanking the Style panel.
-
- }
- // Mirror the standalone Style panel's autoCollapse triggers so the
- // notebook / story-map presentation collapses Style here too.
- // `autoCollapsedPanel` is omitted because it is always null in a
- // shared-rail mode (the panel is the sole active one).
- forceBuiltinCollapsed={notebookOpen || storymapPresenting}
- renderBuiltin={({ collapsed, onCollapsedChange }) => (
-
- )}
- />
-
- ) : (
+ {layoutOptions.panelsHidden || layoutOptions.viewer ? null : (
<>
+ {/* Shared-rail panels such as Comments must not remove the ordinary
+ positional docks: enabled Web Services panels still live in
+ left/right-of-style and need their vertical rail entries. Both
+ flank whichever middle surface applies, so they are rendered
+ once here rather than duplicated per branch. */}
- {/* The notebook claims the workspace's right half, so the Style panel
+ {replaceStylePanelId ? (
+
+ }
+ // Mirror the standalone Style panel's autoCollapse triggers so the
+ // notebook / story-map presentation collapses Style here too.
+ // `autoCollapsedPanel` is omitted because it is always null in a
+ // shared-rail mode (the panel is the sole active one).
+ forceBuiltinCollapsed={notebookOpen || storymapPresenting}
+ renderBuiltin={({ collapsed, onCollapsedChange }) => (
+
+ )}
+ />
+
+ ) : /* The notebook claims the workspace's right half, so the Style panel
collapses to its rail while the notebook is open (Processing →
Jupyter Notebook) rather than unmounting; the user can re-expand it.
- A story map presentation collapses it for the same reason. */}
- {layoutOptions.stylePanelVisible ? (
+ A story map presentation collapses it for the same reason. */
+ layoutOptions.stylePanelVisible ? (
{
+ if (show) {
+ openRightPanel(COMMENTS_PANEL_ID);
+ collapseRightPanel(COMMENTS_PANEL_ID);
+ } else {
+ closeRightPanel(COMMENTS_PANEL_ID);
+ }
+ };
// A field a deep-link asked us to focus once its section renders; cleared
// after the focus lands so a later open without a focus request stays put.
const [pendingFocus, setPendingFocus] = useState(null);
@@ -1264,6 +1276,13 @@ export function SettingsDialog({
>
{t("settings.layout.showBrowserPanel")}
+ toggleCommentsPanel(checked === true)}
+ onSelect={(event: Event) => event.preventDefault()}
+ >
+ {t("settings.layout.showCommentsPanel")}
+ {
@@ -1738,6 +1757,16 @@ export function SettingsDialog({
{t("settings.layout.showBrowserPanel")}
+
{showsAdvancedNotices(desktopSettings.uiProfile) ? (
diff --git a/apps/geolibre-desktop/src/components/panels/PluginRightPanel.tsx b/apps/geolibre-desktop/src/components/panels/PluginRightPanel.tsx
index 70f60d190..361eb3952 100644
--- a/apps/geolibre-desktop/src/components/panels/PluginRightPanel.tsx
+++ b/apps/geolibre-desktop/src/components/panels/PluginRightPanel.tsx
@@ -2,6 +2,7 @@ import {
closeRightPanel,
collapseRightPanel,
getRightPanel,
+ listRightPanels,
moveActiveRightPanelDock,
openRightPanel,
type RightPanelDock,
@@ -15,10 +16,8 @@ import {
Combine,
PanelLeft,
PanelLeftClose,
- PanelLeftOpen,
PanelRight,
PanelRightClose,
- PanelRightOpen,
X,
} from "lucide-react";
import {
@@ -81,7 +80,7 @@ interface PluginRightPanelProps {
*/
export function PluginRightPanel({ dock, contentEl, width, onWidthChange }: PluginRightPanelProps) {
const { t } = useTranslation();
- const { activeId, collapsed, dock: activeDock } = useRightPanelState();
+ const { activeId, collapsed, dock: activeDock, panelDocks } = useRightPanelState();
const contentRef = useRef(null);
const panel = activeId ? getRightPanel(activeId) : undefined;
@@ -96,6 +95,17 @@ export function PluginRightPanel({ dock, contentEl, width, onWidthChange }: Plug
// move buttons and no rail of its own; its collapsed entry lives in that single
// shared rail instead.
const isSharedRail = dock === "replace-style" || dock === "replace-layers";
+ // Every panel docked here that is not currently expanded: the displaced ones
+ // plus the active panel when it is collapsed to this rail. The expanded panel
+ // is excluded — it renders in full below, so a rail entry would duplicate it
+ // and be announced as collapsed when it is not.
+ const dockPanels = isSharedRail
+ ? []
+ : listRightPanels().filter((candidate) =>
+ candidate.id === activeId
+ ? activeDock === dock && collapsed
+ : panelDocks[candidate.id] === dock,
+ );
// Adopt the shared content host (rendered once by the shell) into this slot
// while it owns the panel. appendChild moves the element, so stepping the
@@ -112,10 +122,48 @@ export function PluginRightPanel({ dock, contentEl, width, onWidthChange }: Plug
wrapper.appendChild(contentEl);
}, [matched, contentEl, collapsed]);
- if (!matched || !panel) return null;
+ const panelRail =
+ dockPanels.length > 0 ? (
+
+ ) : null;
+
+ if (!matched || !panel) return panelRail;
// When collapsed in shared-rail mode the host's single shared rail shows this
// panel's entry, so render nothing here (no second rail beside Style).
if (isSharedRail && collapsed) return null;
+ // Collapsed at a positional dock: `panelRail` already carries this panel's
+ // entry (alongside any other panel docked here), so nothing else renders and
+ // everything below this point runs expanded.
+ if (collapsed) return panelRail;
const handleResizeStart = (event: ReactPointerEvent) => {
event.preventDefault();
@@ -147,15 +195,6 @@ export function PluginRightPanel({ dock, contentEl, width, onWidthChange }: Plug
el.addEventListener("pointercancel", handleEnd);
};
- const railIcon =
- panel.icon && isImageSource(panel.icon) ? (
-
- ) : isLayersSide ? (
-
- ) : (
-
- );
-
const borderSide = isLayersSide ? "md:border-e" : "md:border-s";
// Dock names describe the LTR arrangement, so the visual move-left/right
// actions and their guards swap in a right-to-left layout.
@@ -164,16 +203,12 @@ export function PluginRightPanel({ dock, contentEl, width, onWidthChange }: Plug
const canMoveRight = activeDock !== (isRtl ? "left-of-layers" : "right-of-style");
return (
-
+ {panelRail}
+ >
);
}
diff --git a/apps/geolibre-desktop/src/components/panels/SharedSidebar.tsx b/apps/geolibre-desktop/src/components/panels/SharedSidebar.tsx
index f8f4ac3fc..e71f96114 100644
--- a/apps/geolibre-desktop/src/components/panels/SharedSidebar.tsx
+++ b/apps/geolibre-desktop/src/components/panels/SharedSidebar.tsx
@@ -15,6 +15,8 @@ interface SharedSidebarProps {
side: SharedSide;
/** Id of the active plugin panel docked with `replace-layers`/`replace-style`. */
pluginId: string;
+ /** Other enabled panels merged into this same shared rail. */
+ additionalPanelIds?: string[];
/** The active panel's shared content host (see {@link PluginRightPanel}). */
pluginContentEl: HTMLElement;
/** Shared plugin-panel width in px, owned by the shell. */
@@ -78,6 +80,7 @@ interface RailEntry {
export function SharedSidebar({
side,
pluginId,
+ additionalPanelIds = [],
pluginContentEl,
pluginWidth,
onPluginWidthChange,
@@ -95,7 +98,8 @@ export function SharedSidebar({
// which is the desired "collapsed by default" behavior on reopen.
const [builtinOptedIn, setBuiltinOptedIn] = useState(initialBuiltinExpanded);
- const pluginExpanded = activeId === pluginId && !collapsed;
+ const panelIds = [pluginId, ...additionalPanelIds.filter((id) => id !== pluginId)];
+ const pluginExpanded = panelIds.includes(activeId ?? "") && !collapsed;
// The plugin displaces the built-in panel: one shared surface, one expanded
// panel at a time. `forceBuiltinCollapsed` gates this too (it only gates, never
// clears the opt-in, so the panel restores when the trigger lifts).
@@ -105,9 +109,9 @@ export function SharedSidebar({
// Switching back to the plugin forgets the built-in opt-in, so a later collapse
// of the plugin lands on the shared rail (both collapsed) rather than
// surprising the user by auto-expanding the built-in panel.
- const expandPlugin = () => {
+ const expandPlugin = (id: string) => {
setBuiltinOptedIn(false);
- openRightPanel(pluginId);
+ openRightPanel(id);
};
const collapsePlugin = () => collapseRightPanel(pluginId);
const expandBuiltin = () => {
@@ -117,23 +121,23 @@ export function SharedSidebar({
};
const collapseBuiltin = () => setBuiltinOptedIn(false);
- const panel = getRightPanel(pluginId);
- const pluginIcon =
- panel?.icon && isImageSource(panel.icon) ? (
-
- ) : (
-
- );
-
- const entries: RailEntry[] = [
- {
- id: pluginId,
- title: panel?.title ?? pluginId,
- icon: pluginIcon,
- active: pluginExpanded,
- onToggle: pluginExpanded ? collapsePlugin : expandPlugin,
- },
- ];
+ const entries: RailEntry[] = panelIds.map((id) => {
+ const panel = getRightPanel(id);
+ const icon =
+ panel?.icon && isImageSource(panel.icon) ? (
+
+ ) : (
+
+ );
+ const expanded = activeId === id && !collapsed;
+ return {
+ id,
+ title: panel?.title ?? id,
+ icon,
+ active: expanded,
+ onToggle: expanded ? collapsePlugin : () => expandPlugin(id),
+ };
+ });
if (builtinVisible) {
entries.push({
// Namespaced so the built-in entry's React key cannot collide with a plugin
diff --git a/apps/geolibre-desktop/src/components/panels/StylePanel.tsx b/apps/geolibre-desktop/src/components/panels/StylePanel.tsx
index 5322eeb7a..2926ad74d 100644
--- a/apps/geolibre-desktop/src/components/panels/StylePanel.tsx
+++ b/apps/geolibre-desktop/src/components/panels/StylePanel.tsx
@@ -83,8 +83,6 @@ import {
useRef,
useState,
} from "react";
-import { getIsMobileViewport } from "../../hooks/useIsMobileViewport";
-import { isMobile } from "../../lib/is-mobile";
import { loadedVectorTileFeatures } from "../../hooks/useVectorTileGeometryBackfill";
import { clamp } from "../../lib/clamp";
import {
@@ -997,14 +995,9 @@ export function StylePanel({
const updateLayer = useAppStore((s) => s.updateLayer);
const moveLayer = useAppStore((s) => s.moveLayer);
const projectName = useAppStore((s) => s.projectName);
- // Start collapsed on a narrow viewport *or* on a mobile platform. The viewport
- // check alone misses the iPad: it is ~1024pt wide, so it reads as a desktop
- // and opens Layers and Style at once, squeezing the map into a narrow strip
- // between them. Initial state only — the user can expand the panel, and the
- // choice sticks for the session.
- const [internalCollapsed, setInternalCollapsed] = useState(
- () => getIsMobileViewport() || isMobile(),
- );
+ // Style starts on its rail on every platform. Selecting a real layer below
+ // expands it; selecting the special Background row does not.
+ 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
// owner applies so every existing call site keeps working.
@@ -1017,6 +1010,23 @@ 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);
+ 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]);
// 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/hooks/useRegisterCommentsPanel.ts b/apps/geolibre-desktop/src/hooks/useRegisterCommentsPanel.ts
index b1784de4b..1c9cabab8 100644
--- a/apps/geolibre-desktop/src/hooks/useRegisterCommentsPanel.ts
+++ b/apps/geolibre-desktop/src/hooks/useRegisterCommentsPanel.ts
@@ -1,5 +1,6 @@
-import { collapseRightPanel, openRightPanel, registerRightPanel } from "@geolibre/plugins";
+import { registerRightPanel } from "@geolibre/plugins";
import { useEffect } from "react";
+import i18n from "../i18n";
/** Stable id of the Comments right panel. */
export const COMMENTS_PANEL_ID = "comments";
@@ -8,19 +9,19 @@ export const COMMENTS_PANEL_ID = "comments";
* Registers the Comments panel as a dockable right panel sharing the Style (right)
* sidebar's rail (`replace-style`).
*
- * Opens and collapses on mount so it is active and present as a rail entry on the
- * right sidebar by default.
+ * Unlike the Browser panel, Comments is opt-in: opening it on mount would
+ * displace Browser because dockable panels share one active registry slot.
*/
export function useRegisterCommentsPanel(): void {
useEffect(() => {
+ // i18n.t (not the useTranslation hook) so registration carries no
+ // render-time dependency; the rail entry re-resolves the getter on render.
const dispose = registerRightPanel({
id: COMMENTS_PANEL_ID,
- title: "Comments",
+ title: () => i18n.t("comments.title"),
dock: "replace-style",
render: () => {},
});
- openRightPanel(COMMENTS_PANEL_ID);
- collapseRightPanel(COMMENTS_PANEL_ID);
return dispose;
}, []);
}
diff --git a/apps/geolibre-desktop/src/hooks/useRightPanels.ts b/apps/geolibre-desktop/src/hooks/useRightPanels.ts
index a56bf0bec..ad4b6ebb0 100644
--- a/apps/geolibre-desktop/src/hooks/useRightPanels.ts
+++ b/apps/geolibre-desktop/src/hooks/useRightPanels.ts
@@ -54,7 +54,8 @@ export function useAutoCollapsedPanel(): AutoCollapsedPanel {
const selectReplaceStylePanelId = (): string | null => {
const snapshot = getRightPanelSnapshot();
- return snapshot.dock === "replace-style" ? snapshot.activeId : null;
+ if (snapshot.dock === "replace-style") return snapshot.activeId;
+ return snapshot.visibleIds.find((id) => snapshot.panelDocks[id] === "replace-style") ?? null;
};
/**
@@ -77,7 +78,8 @@ export function useReplaceStylePanelId(): string | null {
const selectReplaceLayersPanelId = (): string | null => {
const snapshot = getRightPanelSnapshot();
- return snapshot.dock === "replace-layers" ? snapshot.activeId : null;
+ if (snapshot.dock === "replace-layers") return snapshot.activeId;
+ return snapshot.visibleIds.find((id) => snapshot.panelDocks[id] === "replace-layers") ?? null;
};
/**
diff --git a/apps/geolibre-desktop/src/i18n/locales/ar.json b/apps/geolibre-desktop/src/i18n/locales/ar.json
index 24e5d334b..d2a58476a 100644
--- a/apps/geolibre-desktop/src/i18n/locales/ar.json
+++ b/apps/geolibre-desktop/src/i18n/locales/ar.json
@@ -1908,6 +1908,7 @@
"showLayersPanel": "إظهار لوحة الطبقات",
"showStylePanel": "إظهار لوحة النمط",
"showBrowserPanel": "إظهار لوحة المستعرض",
+ "showCommentsPanel": "إظهار لوحة التعليقات",
"showAttributePanel": "إظهار لوحة السمات",
"urlParamsNote": "تظل معلمات التخطيط في عنوان URL سارية عند وجودها في روابط العارض المشتركة."
},
@@ -5329,6 +5330,7 @@
"invalidKmlCoordinatesById": "تعذّر على مُصدّر KML كتابة المعلم ذي المعرّف {{id}} لأن إحداثياته غير صالحة."
},
"comments": {
+ "title": "التعليقات",
"addDialogTitle": "إضافة تعليق مراجعة",
"addDialogDescription": "انشر ملاحظة مثبتة على هذا المشروع لمراجعة الفريق أو للرجوع إليها دون اتصال.",
"authorNameLabel": "اسمك (يُطلب مرة واحدة ويُحفظ تلقائيًا)",
diff --git a/apps/geolibre-desktop/src/i18n/locales/de.json b/apps/geolibre-desktop/src/i18n/locales/de.json
index 6a0cbd93e..a0e7e6948 100644
--- a/apps/geolibre-desktop/src/i18n/locales/de.json
+++ b/apps/geolibre-desktop/src/i18n/locales/de.json
@@ -1749,6 +1749,7 @@
"showLayersPanel": "Ebenen-Bedienfeld anzeigen",
"showStylePanel": "Stil-Bedienfeld anzeigen",
"showBrowserPanel": "Browser-Bedienfeld anzeigen",
+ "showCommentsPanel": "Kommentar-Bedienfeld anzeigen",
"showAttributePanel": "Attribut-Bedienfeld anzeigen",
"urlParamsNote": "URL-Layout-Parameter gelten weiterhin, wenn sie in gemeinsam genutzten Viewer-Links vorhanden sind."
},
@@ -5030,6 +5031,7 @@
"invalidKmlCoordinatesById": "Der KML-Export konnte Feature-ID {{id}} nicht schreiben, da sie ungültige Koordinaten hat."
},
"comments": {
+ "title": "Kommentare",
"addDialogTitle": "Prüfkommentar hinzufügen",
"addDialogDescription": "Eine verankerte Notiz zu diesem Projekt für die Teamprüfung oder als Offline-Referenz veröffentlichen.",
"authorNameLabel": "Ihr Name (wird einmal abgefragt und automatisch gespeichert)",
diff --git a/apps/geolibre-desktop/src/i18n/locales/en.json b/apps/geolibre-desktop/src/i18n/locales/en.json
index d46148cb0..6f8c5789b 100644
--- a/apps/geolibre-desktop/src/i18n/locales/en.json
+++ b/apps/geolibre-desktop/src/i18n/locales/en.json
@@ -1749,6 +1749,7 @@
"showLayersPanel": "Show Layers panel",
"showStylePanel": "Show Style panel",
"showBrowserPanel": "Show Browser panel",
+ "showCommentsPanel": "Show Comments panel",
"showAttributePanel": "Show Attribute panel",
"urlParamsNote": "URL layout parameters still apply when present in shared viewer links."
},
@@ -5030,6 +5031,7 @@
"moreValues": "+{{total}} more values"
},
"comments": {
+ "title": "Comments",
"addDialogTitle": "Add Review Comment",
"addDialogDescription": "Post an anchored note on this project for team review or offline reference.",
"authorNameLabel": "Your Name (Asked once, saved automatically)",
diff --git a/apps/geolibre-desktop/src/i18n/locales/es.json b/apps/geolibre-desktop/src/i18n/locales/es.json
index f24038c9d..dc77ec0df 100644
--- a/apps/geolibre-desktop/src/i18n/locales/es.json
+++ b/apps/geolibre-desktop/src/i18n/locales/es.json
@@ -1749,6 +1749,7 @@
"showLayersPanel": "Mostrar panel de capas",
"showStylePanel": "Mostrar panel de estilo",
"showBrowserPanel": "Mostrar panel del explorador",
+ "showCommentsPanel": "Mostrar panel de comentarios",
"showAttributePanel": "Mostrar panel de atributos",
"urlParamsNote": "Los parámetros de diseño de la URL siguen aplicándose cuando están presentes en los enlaces de visor compartidos."
},
@@ -5030,6 +5031,7 @@
"invalidKmlCoordinatesById": "La exportación KML no pudo escribir la entidad con ID {{id}} porque tiene coordenadas no válidas."
},
"comments": {
+ "title": "Comentarios",
"addDialogTitle": "Añadir comentario de revisión",
"addDialogDescription": "Publique una nota anclada en este proyecto para la revisión del equipo o como referencia sin conexión.",
"authorNameLabel": "Su nombre (se pregunta una vez y se guarda automáticamente)",
diff --git a/apps/geolibre-desktop/src/i18n/locales/fr.json b/apps/geolibre-desktop/src/i18n/locales/fr.json
index 910c750e6..1139ed045 100644
--- a/apps/geolibre-desktop/src/i18n/locales/fr.json
+++ b/apps/geolibre-desktop/src/i18n/locales/fr.json
@@ -1749,6 +1749,7 @@
"showLayersPanel": "Afficher le panneau Couches",
"showStylePanel": "Afficher le panneau Style",
"showBrowserPanel": "Afficher le panneau Explorateur",
+ "showCommentsPanel": "Afficher le panneau Commentaires",
"showAttributePanel": "Afficher le panneau Attributs",
"urlParamsNote": "Les paramètres de disposition dans l'URL s'appliquent toujours lorsqu'ils sont présents dans les liens de visualisation partagés."
},
@@ -5030,6 +5031,7 @@
"invalidKmlCoordinatesById": "L'export KML n'a pas pu écrire l'entité d'ID {{id}} car ses coordonnées sont invalides."
},
"comments": {
+ "title": "Commentaires",
"addDialogTitle": "Ajouter un commentaire de revue",
"addDialogDescription": "Publiez une note ancrée sur ce projet pour la revue d'équipe ou comme référence hors ligne.",
"authorNameLabel": "Votre nom (demandé une fois, enregistré automatiquement)",
diff --git a/apps/geolibre-desktop/src/i18n/locales/hi.json b/apps/geolibre-desktop/src/i18n/locales/hi.json
index 85a395af7..838e1c620 100644
--- a/apps/geolibre-desktop/src/i18n/locales/hi.json
+++ b/apps/geolibre-desktop/src/i18n/locales/hi.json
@@ -1749,6 +1749,7 @@
"showLayersPanel": "लेयर पैनल दिखाएँ",
"showStylePanel": "स्टाइल पैनल दिखाएँ",
"showBrowserPanel": "ब्राउज़र पैनल दिखाएँ",
+ "showCommentsPanel": "टिप्पणियाँ पैनल दिखाएँ",
"showAttributePanel": "एट्रिब्यूट पैनल दिखाएँ",
"urlParamsNote": "साझा किए गए व्यूअर लिंक में मौजूद होने पर URL लेआउट पैरामीटर अभी भी लागू होते हैं।"
},
@@ -5030,6 +5031,7 @@
"invalidKmlCoordinatesById": "KML निर्यात ID {{id}} वाला फ़ीचर नहीं लिख सका क्योंकि उसके निर्देशांक अमान्य हैं।"
},
"comments": {
+ "title": "टिप्पणियाँ",
"addDialogTitle": "समीक्षा टिप्पणी जोड़ें",
"addDialogDescription": "टीम समीक्षा या ऑफ़लाइन संदर्भ के लिए इस प्रोजेक्ट पर एक संलग्न नोट पोस्ट करें।",
"authorNameLabel": "आपका नाम (एक बार पूछा जाता है, स्वतः सहेजा जाता है)",
diff --git a/apps/geolibre-desktop/src/i18n/locales/id.json b/apps/geolibre-desktop/src/i18n/locales/id.json
index 16a81268c..9270bfe75 100644
--- a/apps/geolibre-desktop/src/i18n/locales/id.json
+++ b/apps/geolibre-desktop/src/i18n/locales/id.json
@@ -1709,6 +1709,7 @@
"showLayersPanel": "Tampilkan panel Layer",
"showStylePanel": "Tampilkan panel Gaya",
"showBrowserPanel": "Tampilkan panel Browser",
+ "showCommentsPanel": "Tampilkan panel Komentar",
"showAttributePanel": "Tampilkan panel Atribut",
"urlParamsNote": "Parameter tata letak URL tetap berlaku jika ada pada tautan penampil yang dibagikan."
},
@@ -4955,6 +4956,7 @@
"invalidKmlCoordinatesById": "Ekspor KML tidak dapat menulis fitur dengan ID {{id}} karena koordinatnya tidak valid."
},
"comments": {
+ "title": "Komentar",
"addDialogTitle": "Tambah Komentar Tinjauan",
"addDialogDescription": "Kirim catatan yang tertambat pada proyek ini untuk tinjauan tim atau rujukan luring.",
"authorNameLabel": "Nama Anda (ditanyakan sekali, tersimpan otomatis)",
diff --git a/apps/geolibre-desktop/src/i18n/locales/it.json b/apps/geolibre-desktop/src/i18n/locales/it.json
index ebfa01d67..2132252e1 100644
--- a/apps/geolibre-desktop/src/i18n/locales/it.json
+++ b/apps/geolibre-desktop/src/i18n/locales/it.json
@@ -1749,6 +1749,7 @@
"showLayersPanel": "Mostra pannello Livelli",
"showStylePanel": "Mostra pannello Stile",
"showBrowserPanel": "Mostra pannello Browser",
+ "showCommentsPanel": "Mostra pannello Commenti",
"showAttributePanel": "Mostra pannello Attributi",
"urlParamsNote": "I parametri di layout nell'URL si applicano comunque quando presenti nei link di visualizzazione condivisi."
},
@@ -5030,6 +5031,7 @@
"invalidKmlCoordinatesById": "L'esportazione KML non ha potuto scrivere l'elemento con ID {{id}} perché ha coordinate non valide."
},
"comments": {
+ "title": "Commenti",
"addDialogTitle": "Aggiungi commento di revisione",
"addDialogDescription": "Pubblica una nota ancorata su questo progetto per la revisione del team o come riferimento offline.",
"authorNameLabel": "Il tuo nome (richiesto una volta, salvato automaticamente)",
diff --git a/apps/geolibre-desktop/src/i18n/locales/ja.json b/apps/geolibre-desktop/src/i18n/locales/ja.json
index 4f70d6c5d..beaa11c93 100644
--- a/apps/geolibre-desktop/src/i18n/locales/ja.json
+++ b/apps/geolibre-desktop/src/i18n/locales/ja.json
@@ -1709,6 +1709,7 @@
"showLayersPanel": "レイヤーパネルを表示",
"showStylePanel": "スタイルパネルを表示",
"showBrowserPanel": "ブラウザパネルを表示",
+ "showCommentsPanel": "コメントパネルを表示",
"showAttributePanel": "属性パネルを表示",
"urlParamsNote": "共有ビューアーのリンクにURLレイアウトパラメータが含まれる場合は、引き続き適用されます。"
},
@@ -4955,6 +4956,7 @@
"invalidKmlCoordinatesById": "座標が不正なため、KML エクスポートで ID {{id}} のフィーチャを書き出せませんでした。"
},
"comments": {
+ "title": "コメント",
"addDialogTitle": "レビューコメントを追加",
"addDialogDescription": "チームレビューやオフラインでの参照用に、このプロジェクトへ位置を固定したメモを投稿します。",
"authorNameLabel": "お名前(初回のみ入力、自動的に保存されます)",
diff --git a/apps/geolibre-desktop/src/i18n/locales/ka.json b/apps/geolibre-desktop/src/i18n/locales/ka.json
index 7583fc80b..19eb7194b 100644
--- a/apps/geolibre-desktop/src/i18n/locales/ka.json
+++ b/apps/geolibre-desktop/src/i18n/locales/ka.json
@@ -1749,6 +1749,7 @@
"showLayersPanel": "შრეების პანელის ჩვენება",
"showStylePanel": "სტილის პანელის ჩვენება",
"showBrowserPanel": "ბრაუზერის პანელის ჩვენება",
+ "showCommentsPanel": "კომენტარების პანელის ჩვენება",
"showAttributePanel": "ატრიბუტების პანელის ჩვენება",
"urlParamsNote": "URL-ის განლაგების პარამეტრები კვლავ მოქმედებს, როცა გაზიარებულ სანახავ ბმულებშია."
},
@@ -5030,6 +5031,7 @@
"invalidKmlCoordinatesById": "KML ექსპორტმა ვერ ჩაწერა ობიექტი ID-ით {{id}}, რადგან მას არასწორი კოორდინატები აქვს."
},
"comments": {
+ "title": "კომენტარები",
"addDialogTitle": "მიმოხილვის კომენტარის დამატება",
"addDialogDescription": "გამოაქვეყნეთ ამ პროექტზე მიმაგრებული ჩანაწერი გუნდური მიმოხილვისთვის ან ოფლაინ მითითებისთვის.",
"authorNameLabel": "თქვენი სახელი (იკითხება ერთხელ და ინახება ავტომატურად)",
diff --git a/apps/geolibre-desktop/src/i18n/locales/ko.json b/apps/geolibre-desktop/src/i18n/locales/ko.json
index 7960c2382..b25dc1725 100644
--- a/apps/geolibre-desktop/src/i18n/locales/ko.json
+++ b/apps/geolibre-desktop/src/i18n/locales/ko.json
@@ -1709,6 +1709,7 @@
"showLayersPanel": "레이어 패널 표시",
"showStylePanel": "스타일 패널 표시",
"showBrowserPanel": "브라우저 패널 표시",
+ "showCommentsPanel": "댓글 패널 표시",
"showAttributePanel": "속성 패널 표시",
"urlParamsNote": "공유된 뷰어 링크에 URL 레이아웃 매개변수가 있으면 계속 적용됩니다."
},
@@ -4955,6 +4956,7 @@
"invalidKmlCoordinatesById": "좌표가 올바르지 않아 KML 내보내기에서 ID {{id}} 피처를 기록하지 못했습니다."
},
"comments": {
+ "title": "댓글",
"addDialogTitle": "검토 의견 추가",
"addDialogDescription": "팀 검토나 오프라인 참고용으로 이 프로젝트에 위치가 고정된 메모를 게시합니다.",
"authorNameLabel": "이름 (한 번만 묻고 자동으로 저장됩니다)",
diff --git a/apps/geolibre-desktop/src/i18n/locales/nl.json b/apps/geolibre-desktop/src/i18n/locales/nl.json
index 32c43ece0..a50e619fc 100644
--- a/apps/geolibre-desktop/src/i18n/locales/nl.json
+++ b/apps/geolibre-desktop/src/i18n/locales/nl.json
@@ -1749,6 +1749,7 @@
"showLayersPanel": "Paneel Lagen weergeven",
"showStylePanel": "Paneel Stijl weergeven",
"showBrowserPanel": "Paneel Browser weergeven",
+ "showCommentsPanel": "Paneel Opmerkingen weergeven",
"showAttributePanel": "Paneel Attributen weergeven",
"urlParamsNote": "URL-lay-outparameters blijven van toepassing wanneer aanwezig in gedeelde viewerlinks."
},
@@ -5030,6 +5031,7 @@
"invalidKmlCoordinatesById": "De KML-export kon object met ID {{id}} niet wegschrijven omdat het ongeldige coördinaten heeft."
},
"comments": {
+ "title": "Opmerkingen",
"addDialogTitle": "Revisieopmerking toevoegen",
"addDialogDescription": "Plaats een verankerde notitie bij dit project voor teamrevisie of offline naslag.",
"authorNameLabel": "Je naam (eenmalig gevraagd, automatisch opgeslagen)",
diff --git a/apps/geolibre-desktop/src/i18n/locales/pt.json b/apps/geolibre-desktop/src/i18n/locales/pt.json
index 535248530..eca29d25c 100644
--- a/apps/geolibre-desktop/src/i18n/locales/pt.json
+++ b/apps/geolibre-desktop/src/i18n/locales/pt.json
@@ -1749,6 +1749,7 @@
"showLayersPanel": "Mostrar painel de camadas",
"showStylePanel": "Mostrar painel de estilo",
"showBrowserPanel": "Mostrar painel do navegador",
+ "showCommentsPanel": "Mostrar painel de comentários",
"showAttributePanel": "Mostrar painel de atributos",
"urlParamsNote": "Os parâmetros de layout da URL ainda se aplicam quando presentes em links de visualizador compartilhados."
},
@@ -5030,6 +5031,7 @@
"invalidKmlCoordinatesById": "A exportação KML não conseguiu gravar a feição de ID {{id}} porque ela tem coordenadas inválidas."
},
"comments": {
+ "title": "Comentários",
"addDialogTitle": "Adicionar comentário de revisão",
"addDialogDescription": "Publique uma nota ancorada neste projeto para revisão da equipe ou referência offline.",
"authorNameLabel": "Seu nome (solicitado uma vez, salvo automaticamente)",
diff --git a/apps/geolibre-desktop/src/i18n/locales/ru.json b/apps/geolibre-desktop/src/i18n/locales/ru.json
index b9f8a5509..915cae89f 100644
--- a/apps/geolibre-desktop/src/i18n/locales/ru.json
+++ b/apps/geolibre-desktop/src/i18n/locales/ru.json
@@ -1829,6 +1829,7 @@
"showLayersPanel": "Показывать панель слоёв",
"showStylePanel": "Показывать панель стилей",
"showBrowserPanel": "Показывать панель обозревателя",
+ "showCommentsPanel": "Показывать панель комментариев",
"showAttributePanel": "Показывать панель атрибутов",
"urlParamsNote": "Параметры макета URL по-прежнему применяются, если они присутствуют в ссылках общего просмотра."
},
@@ -5180,6 +5181,7 @@
"invalidKmlCoordinatesById": "Экспорт в KML не смог записать объект с ID {{id}}, так как у него некорректные координаты."
},
"comments": {
+ "title": "Комментарии",
"addDialogTitle": "Добавить комментарий к рецензии",
"addDialogDescription": "Опубликуйте привязанную заметку к этому проекту для командной рецензии или как офлайн-справку.",
"authorNameLabel": "Ваше имя (спрашивается один раз и сохраняется автоматически)",
diff --git a/apps/geolibre-desktop/src/i18n/locales/tr.json b/apps/geolibre-desktop/src/i18n/locales/tr.json
index da16d2dee..d4b63a8ff 100644
--- a/apps/geolibre-desktop/src/i18n/locales/tr.json
+++ b/apps/geolibre-desktop/src/i18n/locales/tr.json
@@ -1749,6 +1749,7 @@
"showLayersPanel": "Katmanlar panelini göster",
"showStylePanel": "Stil panelini göster",
"showBrowserPanel": "Gözatıcı panelini göster",
+ "showCommentsPanel": "Yorumlar panelini göster",
"showAttributePanel": "Öznitelik panelini göster",
"urlParamsNote": "URL düzen parametreleri, paylaşılan görüntüleyici bağlantılarında mevcut olduğunda hâlâ geçerlidir."
},
@@ -5030,6 +5031,7 @@
"invalidKmlCoordinatesById": "Koordinatları geçersiz olduğu için KML dışa aktarımı {{id}} kimlikli öğeyi yazamadı."
},
"comments": {
+ "title": "Yorumlar",
"addDialogTitle": "İnceleme Yorumu Ekle",
"addDialogDescription": "Ekip incelemesi veya çevrimdışı başvuru için bu projeye sabitlenmiş bir not gönderin.",
"authorNameLabel": "Adınız (bir kez sorulur, otomatik kaydedilir)",
diff --git a/apps/geolibre-desktop/src/i18n/locales/zh.json b/apps/geolibre-desktop/src/i18n/locales/zh.json
index 8f9533925..85253329b 100644
--- a/apps/geolibre-desktop/src/i18n/locales/zh.json
+++ b/apps/geolibre-desktop/src/i18n/locales/zh.json
@@ -1709,6 +1709,7 @@
"showLayersPanel": "显示图层面板",
"showStylePanel": "显示样式面板",
"showBrowserPanel": "显示浏览器面板",
+ "showCommentsPanel": "显示评论面板",
"showAttributePanel": "显示属性面板",
"urlParamsNote": "在共享查看器链接中存在时,URL 布局参数仍然生效。"
},
@@ -4955,6 +4956,7 @@
"invalidKmlCoordinatesById": "KML 导出无法写入 ID 为 {{id}} 的要素,因为它的坐标无效。"
},
"comments": {
+ "title": "评论",
"addDialogTitle": "添加审阅意见",
"addDialogDescription": "在此项目上发布一条锚定的批注,供团队审阅或离线参考。",
"authorNameLabel": "您的姓名(仅询问一次,自动保存)",
diff --git a/docs/plugin-api.md b/docs/plugin-api.md
index d75dbeaf9..72ab53dad 100644
--- a/docs/plugin-api.md
+++ b/docs/plugin-api.md
@@ -189,8 +189,8 @@ export type GeoLibreRightPanelDock =
| "left-of-layers" // left of the Layers panel
| "right-of-layers" // between the Layers panel and the map
| "left-of-style" // between the map and the Style panel
- | "right-of-style" // right of the Style panel (default)
- | "replace-style" // share the Style sidebar's single rail (shared-rail mode)
+ | "right-of-style" // right of the Style panel
+ | "replace-style" // share the Style sidebar's single rail (shared-rail mode, default)
| "replace-layers"; // share the Layers sidebar's single rail (shared-rail mode)
export interface GeoLibreRightPanelRegistration {
@@ -206,7 +206,7 @@ export interface GeoLibreRightPanelRegistration {
// the next registry mutation, and must re-read the panel itself on language
// change. A plain string is frozen at registration time.
title: string | (() => string);
- /** Initial dock position; "right-of-style" (default). */
+ /** Initial dock position; "replace-style" (default). */
dock?: GeoLibreRightPanelDock;
/** Optional rail icon: a URL or data: URI rendered as an image. */
icon?: string;
diff --git a/packages/plugins/src/index.ts b/packages/plugins/src/index.ts
index 6a287ee13..ecf794ad5 100644
--- a/packages/plugins/src/index.ts
+++ b/packages/plugins/src/index.ts
@@ -12,6 +12,7 @@ export {
getActiveRightPanelDock,
RIGHT_PANEL_DOCKS,
isRightPanelCollapsed,
+ isRightPanelVisible,
getRightPanel,
listRightPanels,
getRightPanelSnapshot,
diff --git a/packages/plugins/src/plugins/maplibre-annotations.ts b/packages/plugins/src/plugins/maplibre-annotations.ts
index 9598c7d77..c04deef53 100644
--- a/packages/plugins/src/plugins/maplibre-annotations.ts
+++ b/packages/plugins/src/plugins/maplibre-annotations.ts
@@ -103,7 +103,7 @@ export const maplibreAnnotationsPlugin: GeoLibrePlugin = {
unregisterRightPanelDisposer = app.registerRightPanel({
id: "geolibre-elements-panel",
title: () => labels.elementsPanelTitle || "Elements",
- dock: "right-of-style",
+ dock: "replace-style",
render: (container) => renderElementsPanel(container),
});
}
diff --git a/packages/plugins/src/plugins/maplibre-arcgis-hub.ts b/packages/plugins/src/plugins/maplibre-arcgis-hub.ts
index 59e20a4e7..fd237a370 100644
--- a/packages/plugins/src/plugins/maplibre-arcgis-hub.ts
+++ b/packages/plugins/src/plugins/maplibre-arcgis-hub.ts
@@ -474,7 +474,7 @@ export const maplibreArcGisHubPlugin: GeoLibrePlugin = {
app.registerRightPanel?.({
id: PANEL_ID,
title: "ArcGIS Hub",
- dock: "right-of-style",
+ dock: "replace-style",
defaultWidth: 360,
render: (container) => {
panelContainer = container;
diff --git a/packages/plugins/src/plugins/maplibre-earthdata-gis.ts b/packages/plugins/src/plugins/maplibre-earthdata-gis.ts
index 1bca20324..b824e41db 100644
--- a/packages/plugins/src/plugins/maplibre-earthdata-gis.ts
+++ b/packages/plugins/src/plugins/maplibre-earthdata-gis.ts
@@ -1544,7 +1544,7 @@ export const maplibreEarthdataGisPlugin: GeoLibrePlugin = {
app.registerRightPanel?.({
id: PANEL_ID,
title: "Earthdata GIS",
- dock: "right-of-style",
+ dock: "replace-style",
defaultWidth: 340,
render: (container) => {
mountPanel(container);
diff --git a/packages/plugins/src/plugins/maplibre-geolens.ts b/packages/plugins/src/plugins/maplibre-geolens.ts
index 963850526..8bb309d51 100644
--- a/packages/plugins/src/plugins/maplibre-geolens.ts
+++ b/packages/plugins/src/plugins/maplibre-geolens.ts
@@ -1888,7 +1888,7 @@ function createGeoLensPlugin(config: GeoLensPluginConfig): GeoLibrePlugin {
app.registerRightPanel?.({
id: config.id,
title: config.name,
- dock: "right-of-style",
+ dock: "replace-style",
defaultWidth: 340,
render: (container) => {
mountPanel(container);
diff --git a/packages/plugins/src/plugins/maplibre-graticule.ts b/packages/plugins/src/plugins/maplibre-graticule.ts
index f949d12cd..9308497ff 100644
--- a/packages/plugins/src/plugins/maplibre-graticule.ts
+++ b/packages/plugins/src/plugins/maplibre-graticule.ts
@@ -1251,7 +1251,7 @@ export const maplibreGraticulePlugin: GeoLibrePlugin = {
app.registerRightPanel?.({
id: PANEL_ID,
title: () => labels.getTitle?.() ?? labels.title,
- dock: "right-of-style",
+ dock: "replace-style",
render: (container) => renderPanel(container),
}) ?? null;
diff --git a/packages/plugins/src/plugins/maplibre-h3.ts b/packages/plugins/src/plugins/maplibre-h3.ts
index 26b00fde6..8eb6bbd91 100644
--- a/packages/plugins/src/plugins/maplibre-h3.ts
+++ b/packages/plugins/src/plugins/maplibre-h3.ts
@@ -761,7 +761,7 @@ export const maplibreH3Plugin: GeoLibrePlugin = {
app.registerRightPanel?.({
id: PANEL_ID,
title: () => labels.getTitle?.() ?? labels.title,
- dock: "right-of-style",
+ dock: "replace-style",
defaultWidth: 340,
render: (container) => renderPanel(container),
}) ?? null;
diff --git a/packages/plugins/src/plugins/maplibre-huggingface.ts b/packages/plugins/src/plugins/maplibre-huggingface.ts
index 715802aff..553b42c85 100644
--- a/packages/plugins/src/plugins/maplibre-huggingface.ts
+++ b/packages/plugins/src/plugins/maplibre-huggingface.ts
@@ -2190,7 +2190,7 @@ export const maplibreHuggingFacePlugin: GeoLibrePlugin = (() => {
app.registerRightPanel?.({
id: HUGGINGFACE_PLUGIN_ID,
title: "Hugging Face",
- dock: "right-of-style",
+ dock: "replace-style",
defaultWidth: 340,
render: (container) => {
mountPanel(container);
diff --git a/packages/plugins/src/plugins/maplibre-open-data-catalogs.ts b/packages/plugins/src/plugins/maplibre-open-data-catalogs.ts
index 3af2ae66f..4390ab09e 100644
--- a/packages/plugins/src/plugins/maplibre-open-data-catalogs.ts
+++ b/packages/plugins/src/plugins/maplibre-open-data-catalogs.ts
@@ -202,7 +202,7 @@ function createCatalogPlugin(options: {
app.registerRightPanel?.({
id: options.id,
title: options.name,
- dock: "right-of-style",
+ dock: "replace-style",
defaultWidth: 360,
render(container) {
container.replaceChildren();
diff --git a/packages/plugins/src/plugins/maplibre-openaerialmap.ts b/packages/plugins/src/plugins/maplibre-openaerialmap.ts
index c753cf698..5db1fd1bd 100644
--- a/packages/plugins/src/plugins/maplibre-openaerialmap.ts
+++ b/packages/plugins/src/plugins/maplibre-openaerialmap.ts
@@ -1332,7 +1332,7 @@ export const maplibreOpenAerialMapPlugin: GeoLibrePlugin = {
app.registerRightPanel?.({
id: PANEL_ID,
title: "OpenAerialMap",
- dock: "right-of-style",
+ dock: "replace-style",
defaultWidth: 340,
render: (container) => {
mountPanel(container);
diff --git a/packages/plugins/src/plugins/maplibre-source-coop.ts b/packages/plugins/src/plugins/maplibre-source-coop.ts
index 046b90079..64fcfa2b1 100644
--- a/packages/plugins/src/plugins/maplibre-source-coop.ts
+++ b/packages/plugins/src/plugins/maplibre-source-coop.ts
@@ -1133,7 +1133,7 @@ function createSourceCoopPlugin(config: SourceCoopPluginConfig): GeoLibrePlugin
app.registerRightPanel?.({
id: config.id,
title: config.name,
- dock: "right-of-style",
+ dock: "replace-style",
defaultWidth: 340,
render: (container) => {
mountPanel(container);
diff --git a/packages/plugins/src/plugins/maplibre-stac.ts b/packages/plugins/src/plugins/maplibre-stac.ts
index ae64c5852..829e3c4d2 100644
--- a/packages/plugins/src/plugins/maplibre-stac.ts
+++ b/packages/plugins/src/plugins/maplibre-stac.ts
@@ -1046,7 +1046,7 @@ export const maplibreStacCatalogsPlugin: GeoLibrePlugin = {
app.registerRightPanel?.({
id: PANEL_ID,
title: () => labels.getTitle?.() ?? labels.title,
- dock: "right-of-style",
+ dock: "replace-style",
defaultWidth: 380,
render(container) {
mountPanel(container);
diff --git a/packages/plugins/src/right-panel-registry.ts b/packages/plugins/src/right-panel-registry.ts
index 52480e903..8867b6221 100644
--- a/packages/plugins/src/right-panel-registry.ts
+++ b/packages/plugins/src/right-panel-registry.ts
@@ -53,7 +53,7 @@ const ALL_DOCKS: readonly RightPanelDock[] = Object.freeze([
"replace-layers",
] as const);
-const DEFAULT_DOCK: RightPanelDock = "right-of-style";
+const DEFAULT_DOCK: RightPanelDock = "replace-style";
function normalizeDock(dock: unknown): RightPanelDock {
return ALL_DOCKS.includes(dock as RightPanelDock) ? (dock as RightPanelDock) : DEFAULT_DOCK;
@@ -70,11 +70,17 @@ export interface RightPanelSnapshot {
/** Whether the active panel is collapsed to its rail. */
collapsed: boolean;
/**
- * Where the active panel docks, or null when none is open. Defaults to the
- * panel's declared `dock` ("far-right"); the user steps it with the panel's
- * move buttons (or a plugin via {@link setActiveRightPanelDock}).
+ * Where the active panel docks, or null when none is open. A panel already
+ * enabled in a rail reopens at its remembered dock ({@link panelDocks});
+ * otherwise it starts from its declared `dock` (the shared Style rail by
+ * default). The user steps it with the panel's move buttons (or a plugin via
+ * {@link setActiveRightPanelDock}).
*/
dock: RightPanelDock | null;
+ /** Panels enabled in their dock rails, including inactive displaced panels. */
+ visibleIds: readonly string[];
+ /** Last chosen dock for each visible panel. */
+ panelDocks: Readonly>;
/** Monotonic counter bumped on every registry mutation. */
version: number;
}
@@ -92,17 +98,28 @@ let collapsed = false;
// Where the active panel currently docks; reset when the active panel changes
// so each panel starts from its own declared `dock`.
let activeDock: RightPanelDock | null = null;
+const visibleIds = new Set();
+const panelDocks = new Map();
let version = 0;
let snapshot: RightPanelSnapshot = {
activeId: null,
collapsed: false,
dock: null,
+ visibleIds: [],
+ panelDocks: {},
version: 0,
};
function emit(): void {
version += 1;
- snapshot = { activeId, collapsed, dock: activeDock, version };
+ snapshot = {
+ activeId,
+ collapsed,
+ dock: activeDock,
+ visibleIds: [...visibleIds],
+ panelDocks: Object.fromEntries(panelDocks),
+ version,
+ };
for (const listener of listeners) {
listener();
}
@@ -172,6 +189,8 @@ export function unregisterRightPanel(id: string): void {
collapsed = false;
activeDock = null;
}
+ visibleIds.delete(id);
+ panelDocks.delete(id);
registry.delete(id);
titleResolver.delete(id);
emit();
@@ -190,6 +209,9 @@ export function openRightPanel(id: string): boolean {
return false;
}
if (activeId === id && !collapsed) return true;
+ const wasVisible = visibleIds.has(id);
+ visibleIds.add(id);
+ if (!wasVisible) panelDocks.set(id, normalizeDock(panel.dock));
const wasInactive = activeId !== id;
// A different panel taking over displaces the current owner; release it
// (onClose) so a plugin can free resources allocated for its panel.
@@ -199,8 +221,10 @@ export function openRightPanel(id: string): boolean {
if (displacedId !== null) {
runHook(displacedId, "onClose", registry.get(displacedId)?.onClose);
}
- // A new panel starts from its own declared dock, not the previous user move.
- if (wasInactive) activeDock = normalizeDock(panel.dock);
+ // A panel taking over restores the dock it was last at (set just above from
+ // its declared `dock` the first time it becomes visible), not the dock the
+ // panel it displaced happened to be moved to.
+ if (wasInactive) activeDock = panelDocks.get(id) ?? normalizeDock(panel.dock);
activeId = id;
collapsed = false;
emit();
@@ -226,7 +250,12 @@ export function collapseRightPanel(id: string): void {
* Close the active panel. No-op unless `id` is active.
*/
export function closeRightPanel(id: string): void {
- if (activeId !== id) return;
+ if (!visibleIds.delete(id)) return;
+ panelDocks.delete(id);
+ if (activeId !== id) {
+ emit();
+ return;
+ }
activeId = null;
collapsed = false;
activeDock = null;
@@ -246,6 +275,7 @@ export function setActiveRightPanelDock(dock: RightPanelDock): void {
return;
}
activeDock = dock;
+ panelDocks.set(activeId, dock);
emit();
}
@@ -262,6 +292,7 @@ export function moveActiveRightPanelDock(direction: "left" | "right"): void {
const nextIndex = direction === "left" ? index - 1 : index + 1;
if (nextIndex < 0 || nextIndex >= RIGHT_PANEL_DOCKS.length) return;
activeDock = RIGHT_PANEL_DOCKS[nextIndex];
+ panelDocks.set(activeId, activeDock);
emit();
}
@@ -280,6 +311,11 @@ export function isRightPanelCollapsed(): boolean {
return collapsed;
}
+/** Whether a registered panel is enabled in a dock rail. */
+export function isRightPanelVisible(id: string): boolean {
+ return visibleIds.has(id);
+}
+
/**
* Look up a registered right panel by id. Title is always resolved to a string
* by re-running the panel's title resolver on every call. The registry does
@@ -329,6 +365,15 @@ export function __resetRightPanelRegistryForTests(): void {
activeId = null;
collapsed = false;
activeDock = null;
+ visibleIds.clear();
+ panelDocks.clear();
version = 0;
- snapshot = { activeId: null, collapsed: false, dock: null, version: 0 };
+ snapshot = {
+ activeId: null,
+ collapsed: false,
+ dock: null,
+ visibleIds: [],
+ panelDocks: {},
+ version: 0,
+ };
}
diff --git a/packages/plugins/src/types.ts b/packages/plugins/src/types.ts
index dfe7b2bc9..6c95e21f1 100644
--- a/packages/plugins/src/types.ts
+++ b/packages/plugins/src/types.ts
@@ -859,8 +859,8 @@ export interface GeoLibreRightPanelRegistration {
title: string | (() => string);
/**
* Where the panel docks initially: one of the four positional docks
- * (`left-of-layers`, `right-of-layers`, `left-of-style`, or `right-of-style`,
- * the default), or a shared-rail mode (`replace-style` / `replace-layers`).
+ * (`left-of-layers`, `right-of-layers`, `left-of-style`, or `right-of-style`),
+ * or a shared-rail mode (`replace-style`, the default, or `replace-layers`).
* With a positional dock the built-in panel on the docked side (Layers on the
* left, Style on the right) collapses to its rail while the plugin panel is
* expanded next to it, and the user can move the panel between positions at
diff --git a/tests/right-panel-registry.test.ts b/tests/right-panel-registry.test.ts
index bad7340a7..bc1923e51 100644
--- a/tests/right-panel-registry.test.ts
+++ b/tests/right-panel-registry.test.ts
@@ -106,12 +106,46 @@ describe("right-panel registry", () => {
assert.deepEqual(calls, ["a:close", "b:open"]);
});
- it("defaults to right-of-style and honors a declared dock", () => {
+ it("keeps displaced panels visible in their rails until explicitly closed", () => {
+ registerRightPanel(testPanel({ id: "browser", title: "Browser", dock: "replace-layers" }));
+ registerRightPanel(testPanel({ id: "comments", title: "Comments", dock: "replace-style" }));
+
+ openRightPanel("browser");
+ openRightPanel("comments");
+
+ assert.equal(getActiveRightPanel(), "comments");
+ assert.deepEqual(getRightPanelSnapshot().visibleIds, ["browser", "comments"]);
+
+ closeRightPanel("browser");
+ assert.equal(getActiveRightPanel(), "comments");
+ assert.deepEqual(getRightPanelSnapshot().visibleIds, ["comments"]);
+ // Closing also forgets the panel's remembered dock, so re-enabling it later
+ // starts from its declared dock rather than wherever it was last moved.
+ assert.equal("browser" in getRightPanelSnapshot().panelDocks, false);
+ });
+
+ it("preserves each visible panel's merged dock while another panel is active", () => {
+ registerRightPanel(testPanel({ id: "comments", title: "Comments", dock: "replace-style" }));
+ registerRightPanel(testPanel({ id: "catalog", title: "Catalog", dock: "right-of-style" }));
+
+ openRightPanel("comments");
+ openRightPanel("catalog");
+ setActiveRightPanelDock("replace-style");
+ openRightPanel("comments");
+
+ assert.equal(getActiveRightPanel(), "comments");
+ assert.equal(getRightPanelSnapshot().panelDocks.comments, "replace-style");
+ assert.equal(getRightPanelSnapshot().panelDocks.catalog, "replace-style");
+ // Re-activating a still-visible panel restores its remembered dock.
+ assert.equal(getActiveRightPanelDock(), "replace-style");
+ });
+
+ it("defaults to the shared Style rail and honors a declared dock", () => {
registerRightPanel(testPanel({ id: "r", title: "R" }));
registerRightPanel(testPanel({ id: "l", title: "L", dock: "left-of-layers" }));
openRightPanel("r");
- assert.equal(getActiveRightPanelDock(), "right-of-style");
- assert.equal(getRightPanelSnapshot().dock, "right-of-style");
+ assert.equal(getActiveRightPanelDock(), "replace-style");
+ assert.equal(getRightPanelSnapshot().dock, "replace-style");
openRightPanel("l");
assert.equal(getActiveRightPanelDock(), "left-of-layers");
});
@@ -120,7 +154,7 @@ describe("right-panel registry", () => {
registerRightPanel(testPanel({ id: "a", title: "A" }));
registerRightPanel(testPanel({ id: "b", title: "B" }));
openRightPanel("a");
- assert.equal(getActiveRightPanelDock(), "right-of-style");
+ assert.equal(getActiveRightPanelDock(), "replace-style");
setActiveRightPanelDock("left-of-style");
assert.equal(getActiveRightPanelDock(), "left-of-style");
@@ -142,7 +176,7 @@ describe("right-panel registry", () => {
// Opening another panel resets to that panel's declared dock.
openRightPanel("b");
- assert.equal(getActiveRightPanelDock(), "right-of-style");
+ assert.equal(getActiveRightPanelDock(), "replace-style");
// Closing clears the dock entirely.
closeRightPanel("b");
assert.equal(getActiveRightPanelDock(), null);
@@ -191,14 +225,14 @@ describe("right-panel registry", () => {
assert.equal(getActiveRightPanelDock(), "right-of-layers");
});
- it("falls back to right-of-style for an unknown declared dock", () => {
+ it("falls back to the shared Style rail for an unknown declared dock", () => {
registerRightPanel(
testPanel({
dock: "nonsense" as unknown as GeoLibreRightPanelRegistration["dock"],
}),
);
openRightPanel("workbench");
- assert.equal(getActiveRightPanelDock(), "right-of-style");
+ assert.equal(getActiveRightPanelDock(), "replace-style");
});
it("ignores dock changes when no panel is active", () => {