diff --git a/frontend/src/App.css b/frontend/src/App.css
index 6606b942..9a6a0fda 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -2528,6 +2528,8 @@ body[data-ui-version] .ant-layout-sider[data-sidebar-panel='true'] {
body[data-ui-version] .ant-layout-sider[data-sidebar-panel='true'][data-sidebar-resizing='true'],
body[data-sidebar-resizing='true'] .ant-layout-sider[data-sidebar-panel='true'] {
transition: none !important;
+ width: var(--gonavi-sidebar-resize-width) !important;
+ flex: 0 0 var(--gonavi-sidebar-resize-width) !important;
}
body[data-ui-version] .ant-layout-sider[data-sidebar-collapsed='true'] {
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 61ec4151..3b57b57c 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -4243,7 +4243,6 @@ function App() {
}, [t]);
const {
- ghostRef,
handleSidebarMouseDown,
sidebarResizeHandleWidth,
siderRef,
@@ -9028,22 +9027,6 @@ function App() {
>
)}
- {/* Ghost Resize Line for Sidebar */}
-
-
{/* Ghost Resize Line for Log Panel */}
();
+
+ setProperty(name: string, value: string) {
+ this.properties.set(name, value);
+ }
+
+ removeProperty(name: string) {
+ const previous = this.properties.get(name) || '';
+ this.properties.delete(name);
+ return previous;
+ }
+
+ getPropertyValue(name: string) {
+ return this.properties.get(name) || '';
+ }
+}
+
class FakeHTMLElement extends FakeAttributeHost {
+ style = new FakeStyle();
+
getBoundingClientRect() {
return { right: 240, width: 240 };
}
@@ -81,7 +101,6 @@ describe('useAppSidebarResize interaction cleanup', () => {
let resize: ReturnType
| null = null;
let fakeWindow: FakeEventTarget & { getComputedStyle: () => { minWidth: string; maxWidth: string }; innerWidth: number };
let fakeDocument: FakeEventTarget & { body: FakeBody };
- let ghost: { style: { display: string; left: string } };
let scheduledFrames: Map;
let nextFrameId: number;
let setSidebarWidth: ReturnType;
@@ -117,8 +136,6 @@ describe('useAppSidebarResize interaction cleanup', () => {
fakeDocument = Object.assign(new FakeEventTarget(), {
body: new FakeBody(),
});
- ghost = { style: { display: 'none', left: '' } };
-
Object.defineProperty(globalThis, 'window', { configurable: true, value: fakeWindow });
Object.defineProperty(globalThis, 'document', { configurable: true, value: fakeDocument });
Object.defineProperty(globalThis, 'HTMLElement', { configurable: true, value: FakeHTMLElement });
@@ -139,7 +156,6 @@ describe('useAppSidebarResize interaction cleanup', () => {
renderer = create();
});
(resize!.siderRef as React.MutableRefObject).current = new FakeHTMLElement();
- (resize!.ghostRef as React.MutableRefObject).current = ghost;
});
afterEach(() => {
@@ -170,7 +186,6 @@ describe('useAppSidebarResize interaction cleanup', () => {
userSelect: 'none',
webkitUserSelect: 'none',
});
- expect(ghost.style.display).toBe('block');
expect(fakeWindow.listenerCount('blur')).toBe(1);
act(() => fakeWindow.dispatch('blur'));
@@ -180,7 +195,6 @@ describe('useAppSidebarResize interaction cleanup', () => {
userSelect: 'text',
webkitUserSelect: 'auto',
});
- expect(ghost.style.display).toBe('none');
expect(fakeDocument.listenerCount('mousemove')).toBe(0);
expect(fakeDocument.listenerCount('mouseup')).toBe(0);
expect(fakeWindow.listenerCount('blur')).toBe(0);
@@ -193,14 +207,36 @@ describe('useAppSidebarResize interaction cleanup', () => {
act(() => fakeDocument.dispatch('mousemove', { buttons: 0, clientX: 260 }));
expect(setSidebarWidth).toHaveBeenCalledWith(300);
- expect(ghost.style.display).toBe('none');
+ expect((resize?.siderRef.current as unknown as FakeHTMLElement).style.getPropertyValue('--gonavi-sidebar-resize-width')).toBe('300px');
expect(fakeDocument.body.style.cursor).toBe('wait');
expect(fakeDocument.body.style.userSelect).toBe('text');
expect(fakeDocument.listenerCount('mousemove')).toBe(0);
expect(fakeWindow.listenerCount('blur')).toBe(0);
});
+ it('previews the sidebar width while dragging and persists it only after release', () => {
+ const sider = resize!.siderRef.current as unknown as FakeHTMLElement;
+
+ beginResize();
+ act(() => fakeDocument.dispatch('mousemove', { buttons: 1, clientX: 260 }));
+
+ expect(setSidebarWidth).not.toHaveBeenCalled();
+ expect(sider.style.getPropertyValue('--gonavi-sidebar-resize-width')).toBe('240px');
+
+ act(() => flushAnimationFrames(scheduledFrames, 1));
+
+ expect(sider.style.getPropertyValue('--gonavi-sidebar-resize-width')).toBe('300px');
+ expect(setSidebarWidth).not.toHaveBeenCalled();
+
+ act(() => fakeDocument.dispatch('mouseup', { clientX: 260 }));
+
+ expect(setSidebarWidth).toHaveBeenCalledTimes(1);
+ expect(setSidebarWidth).toHaveBeenCalledWith(300);
+ });
+
it('cancels pending work and restores interaction state when unmounted mid-resize', () => {
+ const sider = resize!.siderRef.current as unknown as FakeHTMLElement;
+
beginResize();
act(() => fakeDocument.dispatch('mousemove', { buttons: 1, clientX: 250 }));
expect(scheduledFrames.size).toBe(1);
@@ -210,7 +246,7 @@ describe('useAppSidebarResize interaction cleanup', () => {
expect(scheduledFrames.size).toBe(0);
expect(cancelAnimationFrame).toHaveBeenCalledTimes(1);
- expect(ghost.style.display).toBe('none');
+ expect(sider.style.getPropertyValue('--gonavi-sidebar-resize-width')).toBe('');
expect(fakeDocument.body.style).toEqual({
cursor: 'wait',
userSelect: 'text',
@@ -239,5 +275,6 @@ describe('useAppSidebarResize interaction cleanup', () => {
expect(sider.getAttribute('data-sidebar-resizing')).toBe(null);
expect(fakeDocument.body.getAttribute('data-sidebar-resizing')).toBe(null);
+ expect(sider.style.getPropertyValue('--gonavi-sidebar-resize-width')).toBe('');
});
});
diff --git a/frontend/src/hooks/useAppSidebarResize.ts b/frontend/src/hooks/useAppSidebarResize.ts
index 8972fc5d..20d4d35e 100644
--- a/frontend/src/hooks/useAppSidebarResize.ts
+++ b/frontend/src/hooks/useAppSidebarResize.ts
@@ -9,7 +9,6 @@ type SidebarResizeBounds = { minWidth: number; maxWidth: number };
type SidebarResizeDragState = SidebarResizeBounds & {
startX: number;
startWidth: number;
- startGuideLeft: number;
};
type SidebarResizeListeners = {
blur: () => void;
@@ -39,6 +38,8 @@ const clampSidebarResizeWidth = (width: number, bounds: SidebarResizeBounds): nu
Math.max(bounds.minWidth, Math.min(bounds.maxWidth, width))
);
+const SIDEBAR_RESIZE_WIDTH_CSS_VARIABLE = '--gonavi-sidebar-resize-width';
+
type UseAppSidebarResizeOptions = {
effectiveUiScale: number;
setSidebarWidth: (width: number) => void;
@@ -53,7 +54,6 @@ export const useAppSidebarResize = ({
const sidebarDragRef = useRef(null);
const rafRef = useRef(null);
const clearResizingFrameRef = useRef(null);
- const ghostRef = useRef(null);
const siderRef = useRef(null);
const sidebarDragBodyStyleRef = useRef<{ cursor: string; userSelect: string; webkitUserSelect: string } | null>(null);
const sidebarResizeListenersRef = useRef(null);
@@ -80,6 +80,7 @@ export const useAppSidebarResize = ({
sider.setAttribute('data-sidebar-resizing', 'true');
} else {
sider.removeAttribute('data-sidebar-resizing');
+ sider.style.removeProperty(SIDEBAR_RESIZE_WIDTH_CSS_VARIABLE);
}
}
if (typeof document !== 'undefined') {
@@ -91,6 +92,12 @@ export const useAppSidebarResize = ({
}
}, []);
+ const previewSidebarWidth = useCallback((width: number) => {
+ const sider = siderRef.current;
+ if (!(sider instanceof HTMLElement)) return;
+ sider.style.setProperty(SIDEBAR_RESIZE_WIDTH_CSS_VARIABLE, `${width}px`);
+ }, []);
+
const scheduleClearSidebarResizing = useCallback(() => {
cancelClearResizingFrame();
if (typeof window === 'undefined') {
@@ -142,21 +149,20 @@ export const useAppSidebarResize = ({
rafRef.current = null;
}
- if (ghostRef.current) {
- ghostRef.current.style.display = 'none';
- }
detachSidebarResizeListeners();
restoreSidebarDragBodyStyles();
if (commit && dragState) {
const finalMouseX = Number.isFinite(clientX) ? clientX as number : latestMouseX.current;
const delta = finalMouseX - dragState.startX;
- // Keep transition disabled across the state commit + first paint.
- setSidebarResizing(true);
- setSidebarWidthRef.current(clampSidebarResizeWidth(
+ const finalWidth = clampSidebarResizeWidth(
dragState.startWidth + delta,
dragState,
- ));
+ );
+ // Keep transition disabled across the state commit + first paint.
+ previewSidebarWidth(finalWidth);
+ setSidebarResizing(true);
+ setSidebarWidthRef.current(finalWidth);
scheduleClearSidebarResizing();
return;
}
@@ -166,6 +172,7 @@ export const useAppSidebarResize = ({
}, [
cancelClearResizingFrame,
detachSidebarResizeListeners,
+ previewSidebarWidth,
restoreSidebarDragBodyStyles,
scheduleClearSidebarResizing,
setSidebarResizing,
@@ -183,7 +190,6 @@ export const useAppSidebarResize = ({
finishSidebarResize(undefined, false);
cancelClearResizingFrame();
- setSidebarResizing(true);
if (typeof document !== 'undefined') {
sidebarDragBodyStyleRef.current = {
@@ -197,19 +203,15 @@ export const useAppSidebarResize = ({
}
const siderRect = siderRef.current?.getBoundingClientRect();
- const startGuideLeft = siderRect?.right ?? sidebarWidth;
const startWidth = siderRect?.width ?? sidebarWidth;
const resizeBounds = resolveSidebarResizeBounds(siderRef.current);
- if (ghostRef.current) {
- ghostRef.current.style.left = `${startGuideLeft}px`;
- ghostRef.current.style.display = 'block';
- }
+ previewSidebarWidth(startWidth);
+ setSidebarResizing(true);
sidebarDragRef.current = {
startX: e.clientX,
startWidth,
- startGuideLeft,
...resizeBounds,
};
latestMouseX.current = e.clientX;
@@ -225,11 +227,11 @@ export const useAppSidebarResize = ({
rafRef.current = requestAnimationFrame(() => {
rafRef.current = null;
- if (!sidebarDragRef.current || !ghostRef.current) return;
- const { startX, startWidth, startGuideLeft, minWidth, maxWidth } = sidebarDragRef.current;
+ if (!sidebarDragRef.current) return;
+ const { startX, startWidth, minWidth, maxWidth } = sidebarDragRef.current;
const delta = latestMouseX.current - startX;
const newWidth = clampSidebarResizeWidth(startWidth + delta, { minWidth, maxWidth });
- ghostRef.current.style.left = `${startGuideLeft + (newWidth - startWidth)}px`;
+ previewSidebarWidth(newWidth);
});
};
const handleUp = (event: MouseEvent) => finishSidebarResize(event.clientX);
@@ -243,7 +245,7 @@ export const useAppSidebarResize = ({
document.addEventListener('mousemove', handleMove);
document.addEventListener('mouseup', handleUp);
window.addEventListener('blur', handleBlur);
- }, [cancelClearResizingFrame, finishSidebarResize, setSidebarResizing, sidebarWidth]);
+ }, [cancelClearResizingFrame, finishSidebarResize, previewSidebarWidth, setSidebarResizing, sidebarWidth]);
useEffect(() => () => {
finishSidebarResize(undefined, false);
@@ -251,7 +253,6 @@ export const useAppSidebarResize = ({
}, [cancelClearResizingFrame, finishSidebarResize]);
return {
- ghostRef,
handleSidebarMouseDown,
sidebarResizeHandleWidth,
siderRef,