feat: make sidebar panels resizable (#2094)

- Replaces #1870, which replaced #1682, #1677

This mostly uses the code from there and addresses the comments I've
left. I didn't change the code much.

## Launch Checklist

Makes the sidebar panel resizable

<img width="707" height="511" alt="image"
src="https://github.com/user-attachments/assets/d010df38-faac-441c-bf7f-a4960e471c61"
/>


 - [x] Briefly describe the changes in this PR.
 - [x] Link to related issues.
- [x] Include before/after visuals or gifs if this PR includes visual
changes.
 - [x] Write tests for all new functionality.
 - [x] Add an entry to `CHANGELOG.md` under the `## main` section.

---------

Co-authored-by: Bart Louwers <bart.louwers@gmail.com>
This commit is contained in:
Harel M
2026-09-27 13:44:22 +00:00
committed by GitHub
co-authored by Bart Louwers
parent e4629339e8
commit 2384adf517
17 changed files with 249 additions and 55 deletions
+116 -38
View File
@@ -1,9 +1,22 @@
import React from "react";
import React, { useEffect, useState } from "react";
import { Group, Panel, Separator, useDefaultLayout } from "react-resizable-panels";
import { ScrollContainer } from "./ScrollContainer";
import { type WithTranslation, withTranslation } from "react-i18next";
import { useTranslation } from "react-i18next";
import { IconContext } from "react-icons";
type AppLayoutInternalProps = {
const DEFAULT_LIST_WIDTH = 200;
const DEFAULT_DRAWER_WIDTH = 370;
const DEFAULT_SIDEBAR_WIDTH = DEFAULT_LIST_WIDTH + DEFAULT_DRAWER_WIDTH;
const PANEL_STYLE: React.CSSProperties = { overflow: "hidden" };
const SIDEBAR_LAYOUT_ID = "maputnik:sidebar-layout";
const SIDEBAR_INNER_LAYOUT_ID = "maputnik:sidebar-inner-layout";
const SIDEBAR_PANEL_ID = "sidebar";
const MAP_PANEL_ID = "map";
const LIST_PANEL_ID = "list";
const DRAWER_PANEL_ID = "drawer";
type AppLayoutProps = {
toolbar: React.ReactElement
layerList: React.ReactElement
layerEditor?: React.ReactElement
@@ -11,43 +24,108 @@ type AppLayoutInternalProps = {
map: React.ReactElement
bottom?: React.ReactElement
modals?: React.ReactNode
} & WithTranslation;
};
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
export const AppLayout: React.FC<AppLayoutProps> = (props) => {
const { t, i18n } = useTranslation();
render() {
document.body.dir = this.props.i18n.dir();
const sidebarLayout = useDefaultLayout({
id: SIDEBAR_LAYOUT_ID,
panelIds: [SIDEBAR_PANEL_ID, MAP_PANEL_ID],
});
const innerLayout = useDefaultLayout({
id: SIDEBAR_INNER_LAYOUT_ID,
panelIds: [LIST_PANEL_ID, DRAWER_PANEL_ID],
});
return <IconContext.Provider value={{size: "14px"}}>
<div className="maputnik-layout">
{this.props.toolbar}
<div className="maputnik-layout-main">
{this.props.codeEditor && <div className="maputnik-layout-code-editor">
<ScrollContainer>
{this.props.codeEditor}
// The bottom panel is position: fixed, so it can't be a flex sibling of the
// map panel; it follows the sidebar through this custom property instead.
const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_SIDEBAR_WIDTH);
useEffect(() => {
document.body.dir = i18n.dir();
}, [i18n, i18n.language]);
return <IconContext.Provider value={{size: "14px"}}>
<div
className="maputnik-layout"
style={{"--sidebar-width": `${sidebarWidth}px`} as React.CSSProperties}
>
{props.toolbar}
<div className="maputnik-layout-main">
<Group
className="maputnik-layout-panels"
orientation="horizontal"
id={SIDEBAR_LAYOUT_ID}
defaultLayout={sidebarLayout.defaultLayout}
onLayoutChanged={sidebarLayout.onLayoutChanged}
>
<Panel
id={SIDEBAR_PANEL_ID}
data-wd-key="sidebar-panel"
className={props.codeEditor ? "maputnik-layout-code-editor" : "maputnik-layout-sidebar"}
style={PANEL_STYLE}
defaultSize={`${DEFAULT_SIDEBAR_WIDTH}px`}
minSize="280px"
onResize={({inPixels}) => setSidebarWidth(inPixels)}
>
{props.codeEditor && <ScrollContainer>
{props.codeEditor}
</ScrollContainer>
</div>
}
{!this.props.codeEditor && <>
<div className="maputnik-layout-list">
{this.props.layerList}
</div>
<div className="maputnik-layout-drawer">
<ScrollContainer>
{this.props.layerEditor}
</ScrollContainer>
</div>
</>}
{this.props.map}
</div>
{this.props.bottom && <div className="maputnik-layout-bottom">
{this.props.bottom}
</div>
}
{this.props.modals}
}
{!props.codeEditor && <Group
className="maputnik-layout-sidebar-panels"
orientation="horizontal"
id={SIDEBAR_INNER_LAYOUT_ID}
defaultLayout={innerLayout.defaultLayout}
onLayoutChanged={innerLayout.onLayoutChanged}
>
<Panel
id={LIST_PANEL_ID}
data-wd-key="layer-list-panel"
className="maputnik-layout-list"
style={PANEL_STYLE}
defaultSize={`${DEFAULT_LIST_WIDTH}px`}
minSize="100px"
>
{props.layerList}
</Panel>
<Separator
className="maputnik-layout-resize-handle"
data-wd-key="inner-resize-handle"
title={t("Drag to resize the layer list")}
aria-label={t("Drag to resize the layer list")}
/>
<Panel
id={DRAWER_PANEL_ID}
className="maputnik-layout-drawer"
style={PANEL_STYLE}
defaultSize={`${DEFAULT_DRAWER_WIDTH}px`}
minSize="150px"
>
<ScrollContainer>
{props.layerEditor}
</ScrollContainer>
</Panel>
</Group>
}
</Panel>
<Separator
className="maputnik-layout-resize-handle"
data-wd-key="sidebar-resize-handle"
title={t("Drag to resize the sidebar")}
aria-label={t("Drag to resize the sidebar")}
/>
<Panel id={MAP_PANEL_ID} className="maputnik-layout-map" style={PANEL_STYLE} minSize="200px">
{props.map}
</Panel>
</Group>
</div>
</IconContext.Provider>;
}
}
export const AppLayout = withTranslation()(AppLayoutInternal);
{props.bottom && <div className="maputnik-layout-bottom">
{props.bottom}
</div>
}
{props.modals}
</div>
</IconContext.Provider>;
};