feat: add resizable navigation drawer and command menu panels (#16612)
## Summary Adds Notion-style resizable panels for the navigation drawer (left sidebar) and command menu (right panel). ## Behavior - **Hover** at panel edge → resize cursor appears - **Click** → collapse/close the panel - **Drag** → resize the panel (5px movement threshold to distinguish from click) ## Constraints | Panel | Min | Max | Default | Collapse Threshold | |-------|-----|-----|---------|-------------------| | Navigation Drawer | 180px | 350px | 220px | 150px | | Command Menu | 320px | 600px | 400px | 250px | ## Performance Optimizations - **CSS variables** for smooth 60fps resize (no React re-renders during drag) - **Table resize observer disabled** during panel resize to prevent expensive recalculations - **React.memo wrapper** on page body to prevent unnecessary re-renders ## Architecture - `useResizablePanel` hook following the same pattern as `useResizeTableHeader` - `ResizablePanelEdge` - resize handle positioned at panel edge - `ResizablePanelGap` - resize handle in the gap between panels - `cssVariableEffect` - Recoil effect to sync CSS variables with state ## Refactoring - Split `recoil-effects.ts` into separate files in `utils/recoil/` (one export per file) - Persist panel widths to localStorage via existing `localStorageEffect`
This commit is contained in:
+1
-16
@@ -4,21 +4,6 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { z } from 'zod';
|
||||
import { cookieStorage } from '~/utils/cookie-storage';
|
||||
|
||||
export const localStorageEffect =
|
||||
<T>(key?: string): AtomEffect<T> =>
|
||||
({ setSelf, onSet, node }) => {
|
||||
const savedValue = localStorage.getItem(key ?? node.key);
|
||||
if (savedValue != null) {
|
||||
setSelf(JSON.parse(savedValue));
|
||||
}
|
||||
|
||||
onSet((newValue, _, isReset) => {
|
||||
isReset
|
||||
? localStorage.removeItem(key ?? node.key)
|
||||
: localStorage.setItem(key ?? node.key, JSON.stringify(newValue));
|
||||
});
|
||||
};
|
||||
|
||||
const customCookieAttributeZodSchema = z.object({
|
||||
cookieAttributes: z.object({
|
||||
expires: z.union([z.number(), z.instanceof(Date)]).optional(),
|
||||
@@ -28,7 +13,7 @@ const customCookieAttributeZodSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
export const isCustomCookiesAttributesValue = (
|
||||
const isCustomCookiesAttributesValue = (
|
||||
value: unknown,
|
||||
): value is { cookieAttributes: Cookies.CookieAttributes } =>
|
||||
customCookieAttributeZodSchema.safeParse(value).success;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { type AtomEffect } from 'recoil';
|
||||
|
||||
export const localStorageEffect =
|
||||
<T>(key?: string): AtomEffect<T> =>
|
||||
({ setSelf, onSet, node }) => {
|
||||
const savedValue = localStorage.getItem(key ?? node.key);
|
||||
if (savedValue != null) {
|
||||
try {
|
||||
setSelf(JSON.parse(savedValue));
|
||||
} catch {
|
||||
// Invalid JSON in localStorage, ignore and use default value
|
||||
localStorage.removeItem(key ?? node.key);
|
||||
}
|
||||
}
|
||||
|
||||
onSet((newValue, _, isReset) => {
|
||||
isReset
|
||||
? localStorage.removeItem(key ?? node.key)
|
||||
: localStorage.setItem(key ?? node.key, JSON.stringify(newValue));
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user