Skip to content

Commit a833fdc

Browse files
committed
✨(frontend) add resizable left panel on desktop with persistence
mainlayout and leftpanel updated with resizable panel saved in localstorage Signed-off-by: Cyril <[email protected]> ✨(frontend) show full nested doc names with horizontal scroll support horizontal overflow enabled and opacity used for sticky actions visibility Signed-off-by: Cyril <[email protected]> ✨(frontend) show full nested doc names with horizontal scroll support horizontal overflow enabled and opacity used for sticky actions visibility Signed-off-by: Cyril <[email protected]> ✨(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 <[email protected]>
1 parent b3cc2bf commit a833fdc

File tree

12 files changed

+274
-38
lines changed

12 files changed

+274
-38
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,6 @@ db.sqlite3
7575
.vscode/
7676
*.iml
7777
.devcontainer
78+
79+
# Cursor rules
80+
.cursorrules

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to
99
## Fixed
1010

1111
- 🐛(frontend) fix duplicate document entries in grid #1479
12+
- 🐛(frontend) show full nested doc names with ajustable bar #1456
1213

1314
## [3.8.2] - 2025-10-17
1415

@@ -30,14 +31,17 @@ and this project adheres to
3031

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

33-
3434
## [3.8.0] - 2025-10-14
3535

3636
### Added
3737

3838
- ✨(frontend) add pdf block to the editor #1293
3939
- ✨List and restore deleted docs #1450
4040

41+
### Fixed
42+
43+
- 🐛(frontend) show full nested doc names with ajustable bar #1456
44+
4145
### Changed
4246

4347
- ♻️(frontend) Refactor Auth component for improved redirection logic #1461

src/frontend/apps/e2e/__tests__/app-impress/left-panel.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { expect, test } from '@playwright/test';
22

3+
import { createDoc } from './utils-common';
4+
35
test.describe('Left panel desktop', () => {
46
test.beforeEach(async ({ page }) => {
57
await page.goto('/');
@@ -11,6 +13,53 @@ test.describe('Left panel desktop', () => {
1113
await expect(page.getByTestId('home-button')).toBeVisible();
1214
await expect(page.getByTestId('new-doc-button')).toBeVisible();
1315
});
16+
17+
test('checks resize handle is present and functional on document page', async ({
18+
page,
19+
browserName,
20+
}) => {
21+
// On home page, resize handle should NOT be present
22+
let resizeHandle = page.locator('[data-panel-resize-handle-id]');
23+
await expect(resizeHandle).toBeHidden();
24+
25+
// Create and navigate to a document
26+
await createDoc(page, 'doc-resize-test', browserName, 1);
27+
28+
// Now resize handle should be visible on document page
29+
resizeHandle = page.locator('[data-panel-resize-handle-id]').first();
30+
await expect(resizeHandle).toBeVisible();
31+
32+
const leftPanel = page.getByTestId('left-panel-desktop');
33+
await expect(leftPanel).toBeVisible();
34+
35+
// Get initial panel width
36+
const initialBox = await leftPanel.boundingBox();
37+
expect(initialBox).not.toBeNull();
38+
39+
// Get handle position
40+
const handleBox = await resizeHandle.boundingBox();
41+
expect(handleBox).not.toBeNull();
42+
43+
// Test resize by dragging the handle
44+
await page.mouse.move(
45+
handleBox!.x + handleBox!.width / 2,
46+
handleBox!.y + handleBox!.height / 2,
47+
);
48+
await page.mouse.down();
49+
await page.mouse.move(
50+
handleBox!.x + 100,
51+
handleBox!.y + handleBox!.height / 2,
52+
);
53+
await page.mouse.up();
54+
55+
// Wait for resize to complete
56+
await page.waitForTimeout(200);
57+
58+
// Verify the panel has been resized
59+
const newBox = await leftPanel.boundingBox();
60+
expect(newBox).not.toBeNull();
61+
expect(newBox!.width).toBeGreaterThan(initialBox!.width);
62+
});
1463
});
1564

1665
test.describe('Left panel mobile', () => {
@@ -47,4 +96,12 @@ test.describe('Left panel mobile', () => {
4796
await expect(languageButton).toBeInViewport();
4897
await expect(logoutButton).toBeInViewport();
4998
});
99+
100+
test('checks resize handle is not present on mobile', async ({ page }) => {
101+
await page.goto('/');
102+
103+
// Verify the resize handle is NOT present on mobile
104+
const resizeHandle = page.locator('[data-panel-resize-handle-id]');
105+
await expect(resizeHandle).toBeHidden();
106+
});
50107
});

src/frontend/apps/impress/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"react-dom": "*",
6161
"react-i18next": "15.7.3",
6262
"react-intersection-observer": "9.16.0",
63+
"react-resizable-panels": "3.0.6",
6364
"react-select": "5.10.2",
6465
"styled-components": "6.1.19",
6566
"use-debounce": "10.0.6",

src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
163163
aria-label={`${t('Open document {{title}}', { title: docTitle })}`}
164164
$css={css`
165165
text-align: left;
166+
min-width: 0;
166167
`}
167168
>
168169
<Box $width="16px" $height="16px">
@@ -180,8 +181,10 @@ export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
180181
display: flex;
181182
flex-direction: row;
182183
width: 100%;
184+
min-width: 0;
183185
gap: 0.5rem;
184186
align-items: center;
187+
overflow: hidden;
185188
`}
186189
>
187190
<Text $css={ItemTextCss} $size="sm" $variation="1000">

src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTree.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,6 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
184184
/* Remove outline from TreeViewItem wrapper elements */
185185
.c__tree-view--row {
186186
outline: none !important;
187-
188187
&:focus-visible {
189188
outline: none !important;
190189
}
@@ -241,7 +240,7 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
241240
}
242241
}
243242
&:hover,
244-
&:focus-within {
243+
&:focus-visible {
245244
.doc-tree-root-item-actions {
246245
opacity: 1;
247246
}

src/frontend/apps/impress/src/features/left-panel/components/LeftPanel.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,10 @@ export const LeftPanel = () => {
3939
{isDesktop && (
4040
<Box
4141
data-testid="left-panel-desktop"
42-
$css={`
42+
$css={css`
4343
height: calc(100vh - ${HEADER_HEIGHT}px);
44-
width: 300px;
45-
min-width: 300px;
44+
width: 100%;
4645
overflow: hidden;
47-
border-right: 1px solid ${colorsTokens['greyscale-200']};
4846
background-color: ${colorsTokens['greyscale-000']};
4947
`}
5048
className="--docs--left-panel-desktop"
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
import {
3+
ImperativePanelHandle,
4+
Panel,
5+
PanelGroup,
6+
PanelResizeHandle,
7+
} from 'react-resizable-panels';
8+
import { createGlobalStyle } from 'styled-components';
9+
10+
import { useCunninghamTheme } from '@/cunningham';
11+
12+
interface PanelStyleProps {
13+
$isResizing: boolean;
14+
}
15+
16+
const PanelStyle = createGlobalStyle<PanelStyleProps>`
17+
${({ $isResizing }) => $isResizing && `body * { transition: none !important; }`}
18+
`;
19+
20+
// Convert a target pixel width to a percentage of the current viewport width.
21+
// react-resizable-panels expects sizes in %, not px.
22+
const calculateDefaultSize = (targetWidth: number) => {
23+
const windowWidth = window.innerWidth;
24+
return (targetWidth / windowWidth) * 100;
25+
};
26+
27+
type ResizableLeftPanelProps = {
28+
leftPanel: React.ReactNode;
29+
children: React.ReactNode;
30+
minPanelSizePx?: number;
31+
maxPanelSizePx?: number;
32+
};
33+
34+
export const ResizableLeftPanel = ({
35+
leftPanel,
36+
children,
37+
minPanelSizePx = 300,
38+
maxPanelSizePx = 450,
39+
}: ResizableLeftPanelProps) => {
40+
const [isResizing, setIsResizing] = useState(false);
41+
const { colorsTokens } = useCunninghamTheme();
42+
const ref = useRef<ImperativePanelHandle>(null);
43+
const resizeTimeoutRef = useRef<number | undefined>(undefined);
44+
45+
const [minPanelSize, setMinPanelSize] = useState(0);
46+
const [maxPanelSize, setMaxPanelSize] = useState(0);
47+
48+
// Single resize listener that handles both panel size updates and transition disabling
49+
useEffect(() => {
50+
const handleResize = () => {
51+
// Update panel sizes (px -> %)
52+
const min = Math.round(calculateDefaultSize(minPanelSizePx));
53+
const max = Math.round(
54+
Math.min(calculateDefaultSize(maxPanelSizePx), 40),
55+
);
56+
setMinPanelSize(min);
57+
setMaxPanelSize(max);
58+
59+
// Temporarily disable transitions to avoid flicker
60+
setIsResizing(true);
61+
if (resizeTimeoutRef.current) {
62+
clearTimeout(resizeTimeoutRef.current);
63+
}
64+
resizeTimeoutRef.current = window.setTimeout(() => {
65+
setIsResizing(false);
66+
}, 150);
67+
};
68+
69+
handleResize();
70+
71+
window.addEventListener('resize', handleResize);
72+
73+
return () => {
74+
window.removeEventListener('resize', handleResize);
75+
if (resizeTimeoutRef.current) {
76+
clearTimeout(resizeTimeoutRef.current);
77+
}
78+
};
79+
}, [minPanelSizePx, maxPanelSizePx]);
80+
81+
return (
82+
<>
83+
<PanelStyle $isResizing={isResizing} />
84+
<PanelGroup
85+
autoSaveId="docs-left-panel-persistence"
86+
direction="horizontal"
87+
>
88+
<Panel
89+
ref={ref}
90+
order={0}
91+
defaultSize={minPanelSize}
92+
minSize={minPanelSize}
93+
maxSize={maxPanelSize}
94+
>
95+
{leftPanel}
96+
</Panel>
97+
<PanelResizeHandle
98+
style={{
99+
borderRightWidth: '1px',
100+
borderRightStyle: 'solid',
101+
borderRightColor: colorsTokens['greyscale-200'],
102+
width: '1px',
103+
cursor: 'col-resize',
104+
}}
105+
/>
106+
<Panel order={1}>{children}</Panel>
107+
</PanelGroup>
108+
</>
109+
);
110+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './LeftPanel';
2+
export * from './ResizableLeftPanel';

0 commit comments

Comments
 (0)