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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ and this project adheres to
- 🐛(frontend) reduce no access image size from 450 to 300 #1463
- 🐛(frontend) preserve interlink style on drag-and-drop in editor #1460
- ✨(frontend) load docs logo from public folder via url #1462
- 🐛(frontend) show full nested doc names with ajustable bar #1456

## [3.7.0] - 2025-09-12

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
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
110 changes: 83 additions & 27 deletions src/frontend/apps/impress/src/layouts/MainLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PropsWithChildren } from 'react';
import { PropsWithChildren, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';

Expand All @@ -10,52 +10,108 @@ import { LeftPanel } from '@/features/left-panel';
import { MAIN_LAYOUT_ID } from '@/layouts/conf';
import { useResponsiveStore } from '@/stores';

import { ResizableLeftPanel } from './components/ResizableLeftPanel';

type MainLayoutProps = {
backgroundColor?: 'white' | 'grey';
enableResizablePanel?: boolean;
};

export function MainLayout({
children,
backgroundColor = 'white',
enableResizablePanel = false,
}: PropsWithChildren<MainLayoutProps>) {
const { isDesktop } = useResponsiveStore();
const { colorsTokens } = useCunninghamTheme();
const currentBackgroundColor = !isDesktop ? 'white' : backgroundColor;
const { t } = useTranslation();

const [isResizing, setIsResizing] = useState(false);

// Main content area (same for all layouts)
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>
);

// Render layout based on device and resizable panel setting
const renderContent = () => {
// Mobile: simple layout
if (!isDesktop) {
return (
<>
<LeftPanel />
{mainContent}
</>
);
}

// Desktop with resizable panel
if (enableResizablePanel) {
return (
<ResizableLeftPanel onResizingChange={setIsResizing}>
{mainContent}
</ResizableLeftPanel>
);
}

// Desktop with fixed panel
return (
<>
<Box
$css={css`
width: 300px;
min-width: 300px;
border-right: 1px solid ${colorsTokens['greyscale-200']};
`}
>
<LeftPanel />
</Box>
{mainContent}
</>
);
};

return (
<Box className="--docs--main-layout">
<Box
className={`--docs--main-layout ${isResizing ? 'resizing' : ''}`}
$css={css`
&.resizing * {
transition: none !important;
}
`}
>
<Header />
<Box
$direction="row"
$margin={{ top: `${HEADER_HEIGHT}px` }}
$width="100%"
$height={`calc(100dvh - ${HEADER_HEIGHT}px)`}
>
<LeftPanel />
<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>
{renderContent()}
</Box>
</Box>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import {
ImperativePanelHandle,
Panel,
PanelGroup,
PanelResizeHandle,
} from 'react-resizable-panels';

import { useCunninghamTheme } from '@/cunningham';
import { LeftPanel } from '@/features/left-panel';

const MIN_PANEL_SIZE_PX = 300;
const MAX_PANEL_SIZE_PX = 450;

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

export const ResizableLeftPanel = ({
children,
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(MIN_PANEL_SIZE_PX));
const max = Math.round(
Math.min(calculateDefaultSize(MAX_PANEL_SIZE_PX), 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]);

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>
);
};
1 change: 1 addition & 0 deletions src/frontend/apps/impress/src/layouts/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ResizableLeftPanel';
2 changes: 1 addition & 1 deletion src/frontend/apps/impress/src/pages/docs/[id]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function DocLayout() {
return subPageToTree(doc.results);
}}
>
<MainLayout>
<MainLayout enableResizablePanel={true}>
<DocPage id={id} />
</MainLayout>
</TreeProvider>
Expand Down
Loading
Loading