mirror of
https://github.com/maputnik/editor.git
synced 2026-10-13 14:33:44 +00:00
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:
+116
-38
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -54,6 +54,8 @@
|
||||
"Delete expression": "Ausdruck löschen",
|
||||
"Delete filter block": "Filterblock löschen",
|
||||
"Deuteranopia filter": "Deuteranopie-Filter",
|
||||
"Drag to resize the layer list": "Ziehen, um die Ebenenliste zu skalieren",
|
||||
"Drag to resize the sidebar": "Ziehen, um die Seitenleiste zu skalieren",
|
||||
"Duplicate": "Duplizieren",
|
||||
"Encoding": "Kodierung",
|
||||
"Enter URL...": "URL eingeben...",
|
||||
|
||||
@@ -54,6 +54,8 @@
|
||||
"Delete expression": "Supprimer l'expression",
|
||||
"Delete filter block": "Supprimer le bloc de filtre",
|
||||
"Deuteranopia filter": "Filtre Deutéranopie",
|
||||
"Drag to resize the layer list": "Glisser pour redimensionner la liste des calques",
|
||||
"Drag to resize the sidebar": "Glisser pour redimensionner la barre latérale",
|
||||
"Duplicate": "Dupliquer",
|
||||
"Encoding": "Encodage",
|
||||
"Enter URL...": "Entrez l'URL...",
|
||||
|
||||
@@ -54,6 +54,8 @@
|
||||
"Delete expression": "מחיקת ביטוי",
|
||||
"Delete filter block": "מחיקת גוש מסנן",
|
||||
"Deuteranopia filter": "Deuteranopia filter",
|
||||
"Drag to resize the layer list": "יש לגרור כדי לשנות את גודל רשימת השכבות",
|
||||
"Drag to resize the sidebar": "יש לגרור כדי לשנות את גודל סרגל הצד",
|
||||
"Duplicate": "שכפול",
|
||||
"Encoding": "קידוד",
|
||||
"Enter URL...": "הכנסו כתובת",
|
||||
|
||||
@@ -54,6 +54,8 @@
|
||||
"Delete expression": "Elimina espressione",
|
||||
"Delete filter block": "Elimina blocco filtro",
|
||||
"Deuteranopia filter": "Filtro deuteranopia",
|
||||
"Drag to resize the layer list": "Trascina per ridimensionare l'elenco dei livelli",
|
||||
"Drag to resize the sidebar": "Trascina per ridimensionare la barra laterale",
|
||||
"Duplicate": "Duplica",
|
||||
"Encoding": "Codifica",
|
||||
"Enter URL...": "Inserisci URL...",
|
||||
|
||||
@@ -54,6 +54,8 @@
|
||||
"Delete expression": "式を削除",
|
||||
"Delete filter block": "フィルタブロックを削除",
|
||||
"Deuteranopia filter": "緑色盲フィルタ",
|
||||
"Drag to resize the layer list": "ドラッグしてレイヤーリストのサイズを変更",
|
||||
"Drag to resize the sidebar": "ドラッグしてサイドバーのサイズを変更",
|
||||
"Duplicate": "複製",
|
||||
"Encoding": "エンコーディング",
|
||||
"Enter URL...": "URLを入力",
|
||||
|
||||
@@ -54,6 +54,8 @@
|
||||
"Delete expression": "표현식 삭제",
|
||||
"Delete filter block": "필터 블록 삭제",
|
||||
"Deuteranopia filter": "녹색맹 필터",
|
||||
"Drag to resize the layer list": "드래그하여 레이어 목록 크기 조정",
|
||||
"Drag to resize the sidebar": "드래그하여 사이드바 크기 조정",
|
||||
"Duplicate": "복제",
|
||||
"Encoding": "인코딩",
|
||||
"Enter URL...": "URL 입력...",
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
"Delete filter block": "Filtre bloğunu sil",
|
||||
"Deuteranopia filter": "Döteranopi filtresi",
|
||||
"Drag and drop a style JSON file here or click to browse": "Bir stil JSON dosyasını buraya sürükleyip bırakın veya göz atmak için tıklayın",
|
||||
"Drag to resize the layer list": "Katman listesini yeniden boyutlandırmak için sürükleyin",
|
||||
"Drag to resize the sidebar": "Kenar çubuğunu yeniden boyutlandırmak için sürükleyin",
|
||||
"Duplicate": "Kopyala",
|
||||
"Encoding": "Kodlama",
|
||||
"Enter URL...": "URL girin...",
|
||||
|
||||
@@ -54,6 +54,8 @@
|
||||
"Delete expression": "删除表达式",
|
||||
"Delete filter block": "删除过滤器块",
|
||||
"Deuteranopia filter": "绿色盲滤镜",
|
||||
"Drag to resize the layer list": "拖动以调整图层列表大小",
|
||||
"Drag to resize the sidebar": "拖动以调整侧边栏大小",
|
||||
"Duplicate": "复制",
|
||||
"Encoding": "编码",
|
||||
"Enter URL...": "输入URL...",
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
.maputnik-map__container {
|
||||
background: white;
|
||||
display: flex;
|
||||
width: vars.$layout-map-width;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&--error {
|
||||
align-items: center;
|
||||
|
||||
+57
-12
@@ -28,31 +28,76 @@
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&-list {
|
||||
width: 200px;
|
||||
background-color: vars.$color-black;
|
||||
// The resizable panel group filling the main area.
|
||||
&-panels {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&-sidebar-panels,
|
||||
&-sidebar,
|
||||
&-code-editor,
|
||||
&-list,
|
||||
&-drawer,
|
||||
&-map {
|
||||
height: 100%;
|
||||
// scroll-container is position: absolute
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&-sidebar,
|
||||
&-code-editor,
|
||||
&-list,
|
||||
&-drawer {
|
||||
width: 370px;
|
||||
background-color: vars.$color-black;
|
||||
// scroll-container is position: absolute
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&-code-editor {
|
||||
width: 570px;
|
||||
background-color: vars.$color-black;
|
||||
// scroll-container is position: absolute
|
||||
&-resize-handle {
|
||||
width: 5px;
|
||||
background-color: transparent;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus-visible {
|
||||
background-color: rgba(vars.$color-lowgray, 0.5);
|
||||
}
|
||||
|
||||
// The separator is keyboard resizable, so it needs a visible focus ring.
|
||||
&:focus-visible {
|
||||
outline: #8e8e8e auto 1px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 3px;
|
||||
height: 30px;
|
||||
border-radius: 2px;
|
||||
background-color: vars.$color-lowgray;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
&:hover::after,
|
||||
&:active::after,
|
||||
&:focus-visible::after {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
&-bottom {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
width: vars.$layout-map-width;
|
||||
inset-inline-start: var(--sidebar-width);
|
||||
inset-inline-end: 0;
|
||||
background-color: vars.$color-black;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,6 @@ $font-family: Roboto, sans-serif;
|
||||
$toolbar-height: 40px;
|
||||
$toolbar-offset: 0;
|
||||
|
||||
$layout-list-width: 200px;
|
||||
$layout-editor-width: 370px;
|
||||
$layout-map-width: calc(100% - #{$layout-list-width + $layout-editor-width});
|
||||
|
||||
// 'menu-down' from 'https://materialdesignicons.com/'
|
||||
// See <https://github.com/Templarian/MaterialDesign/blob/master/LICENSE>
|
||||
$icon-down-arrow: "data:image/svg+xml;charset=utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'><path fill='white' d='M7,10L12,15L17,10H7Z' /></svg>"
|
||||
|
||||
Reference in New Issue
Block a user