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 96e8ec18fc..43b02a0a6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ and this project adheres to ### Fixed -πŸ›(service-worker) fix sw registration and page reload logic #1500 +- πŸ›(service-worker) fix sw registration and page reload logic #1500 +- πŸ›(frontend) show full nested doc names with ajustable bar #1456 ## [3.8.1] - 2025-10-17 @@ -26,7 +27,6 @@ and this project adheres to - πŸ”₯(backend) remove treebeard form for the document admin #1470 - ## [3.8.0] - 2025-10-14 ### Added @@ -34,6 +34,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..a6daa2f286 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 @@ -11,6 +11,45 @@ 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', async ({ page }) => { + await page.goto('/'); + + // Verify the resize handle is present on desktop + const 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 +86,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/components/main-layout/MainLayoutContent.tsx b/src/frontend/apps/impress/src/components/main-layout/MainLayoutContent.tsx new file mode 100644 index 0000000000..a4d42254ea --- /dev/null +++ b/src/frontend/apps/impress/src/components/main-layout/MainLayoutContent.tsx @@ -0,0 +1,92 @@ +import { PropsWithChildren } from 'react'; +import { useTranslation } from 'react-i18next'; +import { css } from 'styled-components'; + +import { Box } from '@/components'; +import { useCunninghamTheme } from '@/cunningham'; +import { LeftPanel } from '@/features/left-panel'; +import { MAIN_LAYOUT_ID } from '@/layouts/conf'; +import { useResponsiveStore } from '@/stores'; + +import { HEADER_HEIGHT } from '../../features/header/conf'; +import { ResizableLeftPanel } from '../../features/left-panel/components/ResizableLeftPanel'; + +export interface MainLayoutContentProps { + backgroundColor: 'white' | 'grey'; + enableResizablePanel?: boolean; + onResizingChange?: (isResizing: boolean) => void; +} + +export function MainLayoutContent({ + children, + backgroundColor, + enableResizablePanel = false, + onResizingChange, +}: 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 ( + } + onResizingChange={onResizingChange} + > + {mainContent} + + ); + } + + return ( + <> + + + + {mainContent} + + ); +} 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 && ( void; +}; + +export const ResizableLeftPanel = ({ + leftPanel, + children, + minPanelSizePx = 300, + maxPanelSizePx = 450, + onResizingChange, +}: ResizableLeftPanelProps) => { + const { colorsTokens } = useCunninghamTheme(); + const ref = useRef(null); + const resizeTimeoutRef = useRef(undefined); + + const [minPanelSize, setMinPanelSize] = useState(0); + const [maxPanelSize, setMaxPanelSize] = useState(0); + + // Convert a target pixel width to a percentage of the current viewport width. + // react-resizable-panels expects sizes in %, not px. + const calculateDefaultSize = useCallback((targetWidth: number) => { + const windowWidth = window.innerWidth; + return (targetWidth / windowWidth) * 100; + }, []); + + // 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 + onResizingChange?.(true); + if (resizeTimeoutRef.current) { + clearTimeout(resizeTimeoutRef.current); + } + resizeTimeoutRef.current = window.setTimeout(() => { + onResizingChange?.(false); + }, 150); + }; + + handleResize(); + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + if (resizeTimeoutRef.current) { + clearTimeout(resizeTimeoutRef.current); + } + }; + }, [calculateDefaultSize, onResizingChange, 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..c9f70e8bfd 100644 --- a/src/frontend/apps/impress/src/layouts/MainLayout.tsx +++ b/src/frontend/apps/impress/src/layouts/MainLayout.tsx @@ -1,61 +1,46 @@ -import { PropsWithChildren } from 'react'; -import { useTranslation } from 'react-i18next'; +import { PropsWithChildren, useState } from 'react'; import { css } from 'styled-components'; import { Box } from '@/components'; -import { useCunninghamTheme } from '@/cunningham'; +import { MainLayoutContent } from '@/components/main-layout/MainLayoutContent'; 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 { useResponsiveStore } from '@/stores'; 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(); + const [isResizing, setIsResizing] = useState(false); return ( - +
- - {children} - + ); 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 e18ea817a9..60f76c09da 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"