From a833fdc7a161c31d1c561e1217d587fa9f578f0f Mon Sep 17 00:00:00 2001 From: Cyril Date: Mon, 13 Oct 2025 13:59:01 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20add=20resizable=20left=20?= =?UTF-8?q?panel=20on=20desktop=20with=20persistence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mainlayout and leftpanel updated with resizable panel saved in localstorage Signed-off-by: Cyril ✨(frontend) show full nested doc names with horizontal scroll support horizontal overflow enabled and opacity used for sticky actions visibility Signed-off-by: Cyril ✨(frontend) show full nested doc names with horizontal scroll support horizontal overflow enabled and opacity used for sticky actions visibility Signed-off-by: Cyril ✨(frontend) add resizable-panels lib also used in our shared ui kit needed for adaptable ui consistent with our shared ui kit components Signed-off-by: Cyril --- .gitignore | 3 + CHANGELOG.md | 6 +- .../__tests__/app-impress/left-panel.spec.ts | 57 +++++++++ src/frontend/apps/impress/package.json | 1 + .../doc-tree/components/DocSubPageItem.tsx | 3 + .../docs/doc-tree/components/DocTree.tsx | 3 +- .../left-panel/components/LeftPanel.tsx | 6 +- .../components/ResizableLeftPanel.tsx | 110 +++++++++++++++++ .../features/left-panel/components/index.tsx | 1 + .../apps/impress/src/layouts/MainLayout.tsx | 115 +++++++++++++----- .../impress/src/pages/docs/[id]/index.tsx | 2 +- src/frontend/yarn.lock | 5 + 12 files changed, 274 insertions(+), 38 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/left-panel/components/ResizableLeftPanel.tsx diff --git a/.gitignore b/.gitignore index ec7fe59c3e..6eea8d7e7c 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,6 @@ db.sqlite3 .vscode/ *.iml .devcontainer + +# Cursor rules +.cursorrules diff --git a/CHANGELOG.md b/CHANGELOG.md index f2cc21f8ec..812679eebf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to ## Fixed - πŸ›(frontend) fix duplicate document entries in grid #1479 +- πŸ›(frontend) show full nested doc names with ajustable bar #1456 ## [3.8.2] - 2025-10-17 @@ -30,7 +31,6 @@ and this project adheres to - πŸ”₯(backend) remove treebeard form for the document admin #1470 - ## [3.8.0] - 2025-10-14 ### Added @@ -38,6 +38,10 @@ and this project adheres to - ✨(frontend) add pdf block to the editor #1293 - ✨List and restore deleted docs #1450 +### Fixed + +- πŸ›(frontend) show full nested doc names with ajustable bar #1456 + ### Changed - ♻️(frontend) Refactor Auth component for improved redirection logic #1461 diff --git a/src/frontend/apps/e2e/__tests__/app-impress/left-panel.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/left-panel.spec.ts index bfed1af1db..558f28bd89 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/left-panel.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/left-panel.spec.ts @@ -1,5 +1,7 @@ import { expect, test } from '@playwright/test'; +import { createDoc } from './utils-common'; + test.describe('Left panel desktop', () => { test.beforeEach(async ({ page }) => { await page.goto('/'); @@ -11,6 +13,53 @@ test.describe('Left panel desktop', () => { await expect(page.getByTestId('home-button')).toBeVisible(); await expect(page.getByTestId('new-doc-button')).toBeVisible(); }); + + test('checks resize handle is present and functional on document page', async ({ + page, + browserName, + }) => { + // On home page, resize handle should NOT be present + let resizeHandle = page.locator('[data-panel-resize-handle-id]'); + await expect(resizeHandle).toBeHidden(); + + // Create and navigate to a document + await createDoc(page, 'doc-resize-test', browserName, 1); + + // Now resize handle should be visible on document page + resizeHandle = page.locator('[data-panel-resize-handle-id]').first(); + await expect(resizeHandle).toBeVisible(); + + const leftPanel = page.getByTestId('left-panel-desktop'); + await expect(leftPanel).toBeVisible(); + + // Get initial panel width + const initialBox = await leftPanel.boundingBox(); + expect(initialBox).not.toBeNull(); + + // Get handle position + const handleBox = await resizeHandle.boundingBox(); + expect(handleBox).not.toBeNull(); + + // Test resize by dragging the handle + await page.mouse.move( + handleBox!.x + handleBox!.width / 2, + handleBox!.y + handleBox!.height / 2, + ); + await page.mouse.down(); + await page.mouse.move( + handleBox!.x + 100, + handleBox!.y + handleBox!.height / 2, + ); + await page.mouse.up(); + + // Wait for resize to complete + await page.waitForTimeout(200); + + // Verify the panel has been resized + const newBox = await leftPanel.boundingBox(); + expect(newBox).not.toBeNull(); + expect(newBox!.width).toBeGreaterThan(initialBox!.width); + }); }); test.describe('Left panel mobile', () => { @@ -47,4 +96,12 @@ test.describe('Left panel mobile', () => { await expect(languageButton).toBeInViewport(); await expect(logoutButton).toBeInViewport(); }); + + test('checks resize handle is not present on mobile', async ({ page }) => { + await page.goto('/'); + + // Verify the resize handle is NOT present on mobile + const resizeHandle = page.locator('[data-panel-resize-handle-id]'); + await expect(resizeHandle).toBeHidden(); + }); }); diff --git a/src/frontend/apps/impress/package.json b/src/frontend/apps/impress/package.json index a7f19b6aa1..9a3467279d 100644 --- a/src/frontend/apps/impress/package.json +++ b/src/frontend/apps/impress/package.json @@ -60,6 +60,7 @@ "react-dom": "*", "react-i18next": "15.7.3", "react-intersection-observer": "9.16.0", + "react-resizable-panels": "3.0.6", "react-select": "5.10.2", "styled-components": "6.1.19", "use-debounce": "10.0.6", diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx index 37d7c2bf68..83948e0a14 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx @@ -163,6 +163,7 @@ export const DocSubPageItem = (props: TreeViewNodeProps) => { aria-label={`${t('Open document {{title}}', { title: docTitle })}`} $css={css` text-align: left; + min-width: 0; `} > @@ -180,8 +181,10 @@ export const DocSubPageItem = (props: TreeViewNodeProps) => { display: flex; flex-direction: row; width: 100%; + min-width: 0; gap: 0.5rem; align-items: center; + overflow: hidden; `} > diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTree.tsx b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTree.tsx index d29be8763d..1a0c3864be 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTree.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTree.tsx @@ -184,7 +184,6 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => { /* Remove outline from TreeViewItem wrapper elements */ .c__tree-view--row { outline: none !important; - &:focus-visible { outline: none !important; } @@ -241,7 +240,7 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => { } } &:hover, - &:focus-within { + &:focus-visible { .doc-tree-root-item-actions { opacity: 1; } diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx index 0d929bcdb7..ff6ef43c25 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx @@ -39,12 +39,10 @@ export const LeftPanel = () => { {isDesktop && ( ` + ${({ $isResizing }) => $isResizing && `body * { transition: none !important; }`} +`; + +// Convert a target pixel width to a percentage of the current viewport width. +// react-resizable-panels expects sizes in %, not px. +const calculateDefaultSize = (targetWidth: number) => { + const windowWidth = window.innerWidth; + return (targetWidth / windowWidth) * 100; +}; + +type ResizableLeftPanelProps = { + leftPanel: React.ReactNode; + children: React.ReactNode; + minPanelSizePx?: number; + maxPanelSizePx?: number; +}; + +export const ResizableLeftPanel = ({ + leftPanel, + children, + minPanelSizePx = 300, + maxPanelSizePx = 450, +}: ResizableLeftPanelProps) => { + const [isResizing, setIsResizing] = useState(false); + const { colorsTokens } = useCunninghamTheme(); + const ref = useRef(null); + const resizeTimeoutRef = useRef(undefined); + + const [minPanelSize, setMinPanelSize] = useState(0); + const [maxPanelSize, setMaxPanelSize] = useState(0); + + // Single resize listener that handles both panel size updates and transition disabling + useEffect(() => { + const handleResize = () => { + // Update panel sizes (px -> %) + const min = Math.round(calculateDefaultSize(minPanelSizePx)); + const max = Math.round( + Math.min(calculateDefaultSize(maxPanelSizePx), 40), + ); + setMinPanelSize(min); + setMaxPanelSize(max); + + // Temporarily disable transitions to avoid flicker + setIsResizing(true); + if (resizeTimeoutRef.current) { + clearTimeout(resizeTimeoutRef.current); + } + resizeTimeoutRef.current = window.setTimeout(() => { + setIsResizing(false); + }, 150); + }; + + handleResize(); + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + if (resizeTimeoutRef.current) { + clearTimeout(resizeTimeoutRef.current); + } + }; + }, [minPanelSizePx, maxPanelSizePx]); + + return ( + <> + + + + {leftPanel} + + + {children} + + + ); +}; diff --git a/src/frontend/apps/impress/src/features/left-panel/components/index.tsx b/src/frontend/apps/impress/src/features/left-panel/components/index.tsx index aedb0f9e9d..d146f3859b 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/index.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/index.tsx @@ -1 +1,2 @@ export * from './LeftPanel'; +export * from './ResizableLeftPanel'; diff --git a/src/frontend/apps/impress/src/layouts/MainLayout.tsx b/src/frontend/apps/impress/src/layouts/MainLayout.tsx index 29fad38e8f..f7e6cec0d3 100644 --- a/src/frontend/apps/impress/src/layouts/MainLayout.tsx +++ b/src/frontend/apps/impress/src/layouts/MainLayout.tsx @@ -6,23 +6,21 @@ import { Box } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; import { Header } from '@/features/header'; import { HEADER_HEIGHT } from '@/features/header/conf'; -import { LeftPanel } from '@/features/left-panel'; -import { MAIN_LAYOUT_ID } from '@/layouts/conf'; +import { LeftPanel, ResizableLeftPanel } from '@/features/left-panel'; import { useResponsiveStore } from '@/stores'; +import { MAIN_LAYOUT_ID } from './conf'; + type MainLayoutProps = { backgroundColor?: 'white' | 'grey'; + enableResizablePanel?: boolean; }; export function MainLayout({ children, backgroundColor = 'white', + enableResizablePanel = false, }: PropsWithChildren) { - const { isDesktop } = useResponsiveStore(); - const { colorsTokens } = useCunninghamTheme(); - const currentBackgroundColor = !isDesktop ? 'white' : backgroundColor; - const { t } = useTranslation(); - return (
@@ -30,33 +28,90 @@ export function MainLayout({ $direction="row" $margin={{ top: `${HEADER_HEIGHT}px` }} $width="100%" + $height={`calc(100dvh - ${HEADER_HEIGHT}px)`} > - - {children} - + ); } + +export interface MainLayoutContentProps { + backgroundColor: 'white' | 'grey'; + enableResizablePanel?: boolean; +} + +export function MainLayoutContent({ + children, + backgroundColor, + enableResizablePanel = false, +}: PropsWithChildren) { + const { isDesktop } = useResponsiveStore(); + const { colorsTokens } = useCunninghamTheme(); + const { t } = useTranslation(); + const currentBackgroundColor = !isDesktop ? 'white' : backgroundColor; + + const mainContent = ( + + {children} + + ); + + if (!isDesktop) { + return ( + <> + + {mainContent} + + ); + } + + if (enableResizablePanel) { + return ( + }> + {mainContent} + + ); + } + + return ( + <> + + + + {mainContent} + + ); +} diff --git a/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx b/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx index d2ef5e8b5f..febb27f50c 100644 --- a/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx +++ b/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx @@ -47,7 +47,7 @@ export function DocLayout() { return subPageToTree(doc.results); }} > - + diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock index 379e67a788..6d049f6871 100644 --- a/src/frontend/yarn.lock +++ b/src/frontend/yarn.lock @@ -13793,6 +13793,11 @@ react-resizable-panels@2.1.7: resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz#afd29d8a3d708786a9f95183a38803c89f13c2e7" integrity sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA== +react-resizable-panels@3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-3.0.6.tgz#8183132ea13a09821e9c93962ed49f240cdcfd3f" + integrity sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew== + react-select@5.10.2: version "5.10.2" resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.10.2.tgz#8dffc69dfd7d74684d9613e6eb27204e3b99e127"