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", () => {