Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,6 @@ db.sqlite3
.vscode/
*.iml
.devcontainer

# Cursor rules
.cursorrules
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -26,14 +27,17 @@ and this project adheres to

- 🔥(backend) remove treebeard form for the document admin #1470


## [3.8.0] - 2025-10-14

### Added

- ✨(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
Expand Down
47 changes: 47 additions & 0 deletions src/frontend/apps/e2e/__tests__/app-impress/left-panel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
});
});
1 change: 1 addition & 0 deletions src/frontend/apps/impress/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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<MainLayoutContentProps>) {
const { isDesktop } = useResponsiveStore();
const { colorsTokens } = useCunninghamTheme();
const { t } = useTranslation();
const currentBackgroundColor = !isDesktop ? 'white' : backgroundColor;

const mainContent = (
<Box
as="main"
role="main"
aria-label={t('Main content')}
id={MAIN_LAYOUT_ID}
$align="center"
$flex={1}
$width="100%"
$height={`calc(100dvh - ${HEADER_HEIGHT}px)`}
$padding={{
all: isDesktop ? 'base' : '0',
}}
$background={
currentBackgroundColor === 'white'
? colorsTokens['greyscale-000']
: colorsTokens['greyscale-050']
}
$css={css`
overflow-y: auto;
overflow-x: clip;
`}
>
{children}
</Box>
);

if (!isDesktop) {
return (
<>
<LeftPanel />
{mainContent}
</>
);
}

if (enableResizablePanel) {
return (
<ResizableLeftPanel
leftPanel={<LeftPanel />}
onResizingChange={onResizingChange}
>
{mainContent}
</ResizableLeftPanel>
);
}

return (
<>
<Box
$css={css`
width: 300px;
min-width: 300px;
border-right: 1px solid ${colorsTokens['greyscale-200']};
`}
>
<LeftPanel />
</Box>
{mainContent}
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
aria-label={`${t('Open document {{title}}', { title: docTitle })}`}
$css={css`
text-align: left;
min-width: 0;
`}
>
<Box $width="16px" $height="16px">
Expand All @@ -180,8 +181,10 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
display: flex;
flex-direction: row;
width: 100%;
min-width: 0;
gap: 0.5rem;
align-items: center;
overflow: hidden;
`}
>
<Text $css={ItemTextCss} $size="sm" $variation="1000">
Expand Down
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not introduce in this PR but I can see there is differences on how the actions buttons are displayed between the top parent and the children, I think it should be &:focus-visible here.

&:focus-within {
.doc-tree-root-item-actions {
opacity: 1;
}

Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -241,7 +240,7 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
}
}
&:hover,
&:focus-within {
&:focus-visible {
.doc-tree-root-item-actions {
opacity: 1;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,10 @@ export const LeftPanel = () => {
{isDesktop && (
<Box
data-testid="left-panel-desktop"
$css={`
$css={css`
height: calc(100vh - ${HEADER_HEIGHT}px);
width: 300px;
min-width: 300px;
width: 100%;
overflow: hidden;
border-right: 1px solid ${colorsTokens['greyscale-200']};
background-color: ${colorsTokens['greyscale-000']};
`}
className="--docs--left-panel-desktop"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import {
ImperativePanelHandle,
Panel,
PanelGroup,
PanelResizeHandle,
} from 'react-resizable-panels';

import { useCunninghamTheme } from '@/cunningham';

type ResizableLeftPanelProps = {
leftPanel: React.ReactNode;
children: React.ReactNode;
minPanelSizePx?: number;
maxPanelSizePx?: number;
onResizingChange?: (isResizing: boolean) => void;
};

export const ResizableLeftPanel = ({
leftPanel,
children,
minPanelSizePx = 300,
maxPanelSizePx = 450,
onResizingChange,
}: ResizableLeftPanelProps) => {
const { colorsTokens } = useCunninghamTheme();
const ref = useRef<ImperativePanelHandle>(null);
const resizeTimeoutRef = useRef<number | undefined>(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 (
<PanelGroup autoSaveId="docs-left-panel-persistence" direction="horizontal">
<Panel
ref={ref}
order={0}
defaultSize={minPanelSize}
minSize={minPanelSize}
maxSize={maxPanelSize}
>
{leftPanel}
</Panel>
<PanelResizeHandle
style={{
borderRightWidth: '1px',
borderRightStyle: 'solid',
borderRightColor: colorsTokens['greyscale-200'],
width: '1px',
cursor: 'col-resize',
}}
/>
<Panel order={1}>{children}</Panel>
</PanelGroup>
);
};
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './LeftPanel';
export * from './ResizableLeftPanel';
Loading
Loading