From f129b07dc92cc55738f71eab480c15de59785a94 Mon Sep 17 00:00:00 2001
From: Yusef Ouda <YusefOuda@users.noreply.github.com>
Date: Sun, 7 Jul 2024 16:10:41 -0500
Subject: [PATCH] Adds ability to resize sidebar (#2993)

* Adds ability to resize sidebar

* Adds release notes

* Changes to feature

* lint

* change translateX to use % for both states

* vrt

* set max sidebar width, cleanup

* set min and max widths

* min width to 200px

* changes resizable sidebar to use re-resizable instead off css resize

* vrt

* vrt
---
 packages/desktop-client/package.json          |   1 +
 .../src/components/sidebar/Sidebar.tsx        | 142 +++++++++++-------
 .../src/components/sidebar/index.tsx          |   8 +-
 packages/loot-core/src/types/prefs.d.ts       |   1 +
 upcoming-release-notes/2993.md                |   6 +
 yarn.lock                                     |  11 ++
 6 files changed, 111 insertions(+), 58 deletions(-)
 create mode 100644 upcoming-release-notes/2993.md

diff --git a/packages/desktop-client/package.json b/packages/desktop-client/package.json
index 9dcbf85b9..d7a7de862 100644
--- a/packages/desktop-client/package.json
+++ b/packages/desktop-client/package.json
@@ -47,6 +47,7 @@
     "memoize-one": "^6.0.0",
     "pikaday": "1.8.2",
     "promise-retry": "^2.0.1",
+    "re-resizable": "^6.9.17",
     "react": "18.2.0",
     "react-aria-components": "^1.2.1",
     "react-dnd": "^16.0.1",
diff --git a/packages/desktop-client/src/components/sidebar/Sidebar.tsx b/packages/desktop-client/src/components/sidebar/Sidebar.tsx
index 77e863ccf..f39629292 100644
--- a/packages/desktop-client/src/components/sidebar/Sidebar.tsx
+++ b/packages/desktop-client/src/components/sidebar/Sidebar.tsx
@@ -1,6 +1,8 @@
 import React, { useRef, useState } from 'react';
 import { useDispatch } from 'react-redux';
 
+import { Resizable } from 're-resizable';
+
 import {
   closeBudget,
   moveAccount,
@@ -12,9 +14,11 @@ import { useAccounts } from '../../hooks/useAccounts';
 import { useGlobalPref } from '../../hooks/useGlobalPref';
 import { useLocalPref } from '../../hooks/useLocalPref';
 import { useNavigate } from '../../hooks/useNavigate';
+import { useResizeObserver } from '../../hooks/useResizeObserver';
 import { SvgExpandArrow } from '../../icons/v0';
 import { SvgReports, SvgWallet } from '../../icons/v1';
 import { SvgCalendar } from '../../icons/v2';
+import { useResponsive } from '../../ResponsiveProvider';
 import { styles, theme } from '../../style';
 import { Button } from '../common/Button';
 import { InitialFocus } from '../common/InitialFocus';
@@ -30,20 +34,28 @@ import { useSidebar } from './SidebarProvider';
 import { ToggleButton } from './ToggleButton';
 import { Tools } from './Tools';
 
-export const SIDEBAR_WIDTH = 240;
-
 export function Sidebar() {
   const hasWindowButtons = !Platform.isBrowser && Platform.OS === 'mac';
 
   const dispatch = useDispatch();
   const sidebar = useSidebar();
   const accounts = useAccounts();
+  const { width } = useResponsive();
   const [showClosedAccounts, setShowClosedAccountsPref] = useLocalPref(
     'ui.showClosedAccounts',
   );
   const [isFloating = false, setFloatingSidebarPref] =
     useGlobalPref('floatingSidebar');
 
+  const [_sidebarWidth, setSidebarWidth] = useLocalPref('sidebarWidth');
+  const DEFAULT_SIDEBAR_WIDTH = 240;
+  const MAX_SIDEBAR_WIDTH = width / 3;
+  const MIN_SIDEBAR_WIDTH = 200;
+  const sidebarWidth = Math.min(
+    MAX_SIDEBAR_WIDTH,
+    Math.max(MIN_SIDEBAR_WIDTH, _sidebarWidth || DEFAULT_SIDEBAR_WIDTH),
+  );
+
   async function onReorder(
     id: string,
     dropPos: 'top' | 'bottom',
@@ -70,72 +82,96 @@ export function Sidebar() {
     setShowClosedAccountsPref(!showClosedAccounts);
   };
 
+  const containerRef = useResizeObserver(rect => {
+    setSidebarWidth(rect.width);
+  });
+
   return (
-    <View
-      style={{
-        width: SIDEBAR_WIDTH,
-        color: theme.sidebarItemText,
-        backgroundColor: theme.sidebarBackground,
-        '& .float': {
-          opacity: isFloating ? 1 : 0,
-          transition: 'opacity .25s, width .25s',
-          width: hasWindowButtons || isFloating ? null : 0,
-        },
-        '&:hover .float': {
-          opacity: 1,
-          width: hasWindowButtons ? null : 'auto',
-        },
-        flex: 1,
-        ...styles.darkScrollbar,
+    <Resizable
+      defaultSize={{
+        width: sidebarWidth,
+        height: '100%',
+      }}
+      maxWidth={MAX_SIDEBAR_WIDTH}
+      minWidth={MIN_SIDEBAR_WIDTH}
+      enable={{
+        top: false,
+        right: true,
+        bottom: false,
+        left: false,
+        topRight: false,
+        bottomRight: false,
+        bottomLeft: false,
+        topLeft: false,
       }}
     >
       <View
+        innerRef={containerRef}
         style={{
-          paddingTop: 35,
-          height: 30,
-          flexDirection: 'row',
-          alignItems: 'center',
-          margin: '0 8px 23px 20px',
-          transition: 'padding .4s',
-          ...(hasWindowButtons && {
-            paddingTop: 20,
-            justifyContent: 'flex-start',
-          }),
+          color: theme.sidebarItemText,
+          height: '100%',
+          backgroundColor: theme.sidebarBackground,
+          '& .float': {
+            opacity: isFloating ? 1 : 0,
+            transition: 'opacity .25s, width .25s',
+            width: hasWindowButtons || isFloating ? null : 0,
+          },
+          '&:hover .float': {
+            opacity: 1,
+            width: hasWindowButtons ? null : 'auto',
+          },
+          flex: 1,
+          ...styles.darkScrollbar,
         }}
       >
-        <EditableBudgetName />
+        <View
+          style={{
+            paddingTop: 35,
+            height: 30,
+            flexDirection: 'row',
+            alignItems: 'center',
+            margin: '0 8px 23px 20px',
+            transition: 'padding .4s',
+            ...(hasWindowButtons && {
+              paddingTop: 20,
+              justifyContent: 'flex-start',
+            }),
+          }}
+        >
+          <EditableBudgetName />
 
-        <View style={{ flex: 1, flexDirection: 'row' }} />
+          <View style={{ flex: 1, flexDirection: 'row' }} />
 
-        {!sidebar.alwaysFloats && (
-          <ToggleButton isFloating={isFloating} onFloat={onFloat} />
-        )}
-      </View>
+          {!sidebar.alwaysFloats && (
+            <ToggleButton isFloating={isFloating} onFloat={onFloat} />
+          )}
+        </View>
 
-      <View style={{ overflow: 'auto' }}>
-        <Item title="Budget" Icon={SvgWallet} to="/budget" />
-        <Item title="Reports" Icon={SvgReports} to="/reports" />
+        <View style={{ overflow: 'auto' }}>
+          <Item title="Budget" Icon={SvgWallet} to="/budget" />
+          <Item title="Reports" Icon={SvgReports} to="/reports" />
 
-        <Item title="Schedules" Icon={SvgCalendar} to="/schedules" />
+          <Item title="Schedules" Icon={SvgCalendar} to="/schedules" />
 
-        <Tools />
+          <Tools />
 
-        <View
-          style={{
-            height: 1,
-            backgroundColor: theme.sidebarItemBackgroundHover,
-            marginTop: 15,
-            flexShrink: 0,
-          }}
-        />
+          <View
+            style={{
+              height: 1,
+              backgroundColor: theme.sidebarItemBackgroundHover,
+              marginTop: 15,
+              flexShrink: 0,
+            }}
+          />
 
-        <Accounts
-          onAddAccount={onAddAccount}
-          onToggleClosedAccounts={onToggleClosedAccounts}
-          onReorder={onReorder}
-        />
+          <Accounts
+            onAddAccount={onAddAccount}
+            onToggleClosedAccounts={onToggleClosedAccounts}
+            onReorder={onReorder}
+          />
+        </View>
       </View>
-    </View>
+    </Resizable>
   );
 }
 
diff --git a/packages/desktop-client/src/components/sidebar/index.tsx b/packages/desktop-client/src/components/sidebar/index.tsx
index 59f7b1628..ba5185138 100644
--- a/packages/desktop-client/src/components/sidebar/index.tsx
+++ b/packages/desktop-client/src/components/sidebar/index.tsx
@@ -4,7 +4,7 @@ import { useGlobalPref } from '../../hooks/useGlobalPref';
 import { useResponsive } from '../../ResponsiveProvider';
 import { View } from '../common/View';
 
-import { SIDEBAR_WIDTH, Sidebar } from './Sidebar';
+import { Sidebar } from './Sidebar';
 import { useSidebar } from './SidebarProvider';
 
 export function FloatableSidebar() {
@@ -42,10 +42,8 @@ export function FloatableSidebar() {
             : '0 15px 30px 0 rgba(0,0,0,0.25), 0 3px 15px 0 rgba(0,0,0,.5)',
         transform: `translateY(${!sidebarShouldFloat ? -12 : 0}px)
                       translateX(${
-                        sidebarShouldFloat && sidebar.hidden
-                          ? -SIDEBAR_WIDTH
-                          : 0
-                      }px)`,
+                        sidebarShouldFloat && sidebar.hidden ? '-100' : '0'
+                      }%)`,
         transition:
           'transform .5s, box-shadow .5s, border-radius .5s, bottom .5s',
       }}
diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts
index 81c51951c..2ce56c31c 100644
--- a/packages/loot-core/src/types/prefs.d.ts
+++ b/packages/loot-core/src/types/prefs.d.ts
@@ -53,6 +53,7 @@ export type LocalPrefs = Partial<
     reportsViewLegend: boolean;
     reportsViewSummary: boolean;
     reportsViewLabel: boolean;
+    sidebarWidth: number;
     'mobile.showSpentColumn': boolean;
   } & Record<`flags.${FeatureFlag}`, boolean>
 >;
diff --git a/upcoming-release-notes/2993.md b/upcoming-release-notes/2993.md
new file mode 100644
index 000000000..bf7643988
--- /dev/null
+++ b/upcoming-release-notes/2993.md
@@ -0,0 +1,6 @@
+---
+category: Features
+authors: [YusefOuda]
+---
+
+Adds ability to resize sidebar.
diff --git a/yarn.lock b/yarn.lock
index 3261a182b..ac628b4f5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -100,6 +100,7 @@ __metadata:
     memoize-one: "npm:^6.0.0"
     pikaday: "npm:1.8.2"
     promise-retry: "npm:^2.0.1"
+    re-resizable: "npm:^6.9.17"
     react: "npm:18.2.0"
     react-aria-components: "npm:^1.2.1"
     react-dnd: "npm:^16.0.1"
@@ -15469,6 +15470,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"re-resizable@npm:^6.9.17":
+  version: 6.9.17
+  resolution: "re-resizable@npm:6.9.17"
+  peerDependencies:
+    react: ^16.13.1 || ^17.0.0 || ^18.0.0
+    react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0
+  checksum: 768c3a0fe39d6916caf4e003240d326d62c4d7512c7d3115cc2a98085416fdba80097afdbb93df57b69543c41ce56b33589f2fea6987cd5149faa83cf11c8ba1
+  languageName: node
+  linkType: hard
+
 "react-aria-components@npm:^1.2.1":
   version: 1.2.1
   resolution: "react-aria-components@npm:1.2.1"
-- 
GitLab