This commit is contained in:
2025-11-12 15:06:04 +01:00
parent e66b530f24
commit d2e27c12a2
7 changed files with 406 additions and 576 deletions
+15 -208
View File
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useRef, useState, useId } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useId } from 'react';
import { createPortal } from 'react-dom';
import {
ChevronIcon,
@@ -23,80 +23,7 @@ import useFloatingMenu from '../ui/useFloatingMenu';
import { getTagColorStyle } from '../utils/colors';
import { useSidebarContext } from './SidebarContext';
const SIDEBAR_WIDTH_STORAGE_KEY = 'documentsSidebarWidth';
const SIDEBAR_WIDTH_EVENT = 'sidebar-width-change';
const MIN_SIDEBAR_WIDTH = 240;
const MAX_SIDEBAR_WIDTH = 640;
const isBrowser = typeof window !== 'undefined';
const isDocumentAvailable = typeof document !== 'undefined';
const notifySidebarWidthChange = (width) => {
if (!isBrowser) {
return;
}
try {
window.dispatchEvent(
new CustomEvent(SIDEBAR_WIDTH_EVENT, {
detail: { width },
}),
);
} catch (error) {
console.warn('Failed to dispatch sidebar width change', error);
}
};
const getSidebarBounds = () => {
if (!isBrowser) {
return {
min: MIN_SIDEBAR_WIDTH,
max: MAX_SIDEBAR_WIDTH,
};
}
const viewportWidth = Math.max(window.innerWidth, 1);
const minFractionWidth = viewportWidth / 6;
const maxFractionWidth = viewportWidth / 3;
const rawMin = Math.max(MIN_SIDEBAR_WIDTH, minFractionWidth);
const rawMax = Math.min(MAX_SIDEBAR_WIDTH, maxFractionWidth);
if (rawMin >= rawMax) {
const fallback = Math.min(Math.max(rawMin, viewportWidth / 4), MAX_SIDEBAR_WIDTH);
return { min: fallback, max: fallback };
}
return { min: rawMin, max: rawMax };
};
const clampSidebarWidth = (value) => {
if (!Number.isFinite(value) || value <= 0) {
return null;
}
const { min, max } = getSidebarBounds();
return Math.min(Math.max(value, min), max);
};
const loadStoredSidebarWidth = () => {
if (!isBrowser) {
return null;
}
try {
const raw = window.localStorage?.getItem(SIDEBAR_WIDTH_STORAGE_KEY);
if (!raw) {
return null;
}
const parsed = parseInt(raw, 10);
return Number.isFinite(parsed) ? parsed : null;
} catch (error) {
console.warn('Failed to read sidebar width', error);
return null;
}
};
const applySidebarWidth = (width) => {
if (!isDocumentAvailable || width == null) {
return;
}
document.documentElement.style.setProperty('--sidebar-width', `${width}px`);
notifySidebarWidthChange(width);
};
import { usePanelManager, usePanelResizeBindings } from '../app/PanelManagerContext';
const FolderNode = ({
node,
@@ -265,7 +192,6 @@ const Sidebar = ({
onUploadFiles,
}) => {
const {
setCollapsed,
neutralHue,
setNeutralHue,
resetNeutralHue,
@@ -278,140 +204,22 @@ const Sidebar = ({
themeMode,
cycleThemeMode,
themeModes,
sidebarSuppressed,
} = useSidebarContext();
const {
sidebarSuppressed,
collapseSidebar,
} = usePanelManager();
const uploadInputRef = useRef(null);
const sidebarRef = useRef(null);
const pendingWidthRef = useRef(null);
const [isResizingSidebar, setIsResizingSidebar] = useState(false);
const [sidebarWidthState, setSidebarWidthState] = useState(() => {
if (!isBrowser) {
return null;
}
const stored = loadStoredSidebarWidth();
return stored != null ? clampSidebarWidth(stored) : null;
});
const {
panelStyle: sidebarStyle,
handleProps: sidebarHandleProps,
isPanelResizing: isResizingSidebar,
} = usePanelResizeBindings('sidebar', { panelRef: sidebarRef });
useEffect(() => {
if (sidebarWidthState != null) {
const clamped = clampSidebarWidth(sidebarWidthState);
if (clamped != null) {
applySidebarWidth(clamped);
}
}
}, [sidebarWidthState]);
useEffect(() => {
if (!isBrowser) {
return undefined;
}
const handleResize = () => {
setSidebarWidthState((prev) => {
if (prev == null) {
return prev;
}
const clamped = clampSidebarWidth(prev);
if (clamped != null && clamped !== prev) {
applySidebarWidth(clamped);
try {
window.localStorage?.setItem(SIDEBAR_WIDTH_STORAGE_KEY, String(Math.round(clamped)));
} catch (error) {
console.warn('Failed to persist sidebar width', error);
}
return clamped;
}
return prev;
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const handleSidebarResizePointerDown = useCallback((event) => {
if (!isBrowser || !sidebarRef.current) {
return;
}
event.preventDefault();
event.stopPropagation();
const pointerId = event.pointerId;
const target = event.currentTarget;
target.setPointerCapture?.(pointerId);
setIsResizingSidebar(true);
const rect = sidebarRef.current.getBoundingClientRect();
const startWidth = rect.width;
const startX = event.clientX;
const updateWidth = (nextWidth) => {
const clamped = clampSidebarWidth(nextWidth);
if (clamped != null) {
pendingWidthRef.current = clamped;
applySidebarWidth(clamped);
}
};
const handlePointerMove = (moveEvent) => {
if (moveEvent.pointerId !== pointerId) {
return;
}
const delta = moveEvent.clientX - startX;
updateWidth(startWidth + delta);
};
const handlePointerUp = (upEvent) => {
if (upEvent.pointerId !== pointerId) {
return;
}
target.releasePointerCapture?.(pointerId);
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
setIsResizingSidebar(false);
if (pendingWidthRef.current != null) {
const finalizedWidth = pendingWidthRef.current;
pendingWidthRef.current = null;
setSidebarWidthState(finalizedWidth);
try {
window.localStorage?.setItem(SIDEBAR_WIDTH_STORAGE_KEY, String(Math.round(finalizedWidth)));
} catch (error) {
console.warn('Failed to persist sidebar width', error);
}
}
};
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
}, []);
const handleSidebarResizeKeyDown = useCallback((event) => {
if (!isBrowser) {
return;
}
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') {
return;
}
const baseWidth = pendingWidthRef.current
?? sidebarWidthState
?? sidebarRef.current?.getBoundingClientRect().width;
if (!Number.isFinite(baseWidth)) {
return;
}
event.preventDefault();
const step = event.shiftKey ? 40 : 20;
const delta = event.key === 'ArrowRight' ? step : -step;
const nextWidth = clampSidebarWidth(baseWidth + delta);
if (nextWidth == null) {
return;
}
setSidebarWidthState(nextWidth);
try {
window.localStorage?.setItem(SIDEBAR_WIDTH_STORAGE_KEY, String(Math.round(nextWidth)));
} catch (error) {
console.warn('Failed to persist sidebar width', error);
}
}, [sidebarWidthState]);
const handleCollapse = useCallback(() => {
setCollapsed(true);
}, [setCollapsed]);
collapseSidebar('sidebar');
}, [collapseSidebar]);
const neutralHueInputId = useId();
const neutralChromaInputId = useId();
const neutralContrastInputId = useId();
@@ -759,13 +567,12 @@ const Sidebar = ({
}
return (
<aside className={sidebarClassNames.join(' ')} ref={sidebarRef}>
<aside className={sidebarClassNames.join(' ')} ref={sidebarRef} style={sidebarStyle}>
<button
type="button"
className={`sidebar__resize-handle${isResizingSidebar ? ' is-active' : ''}`}
aria-label="Resize sidebar"
onPointerDown={handleSidebarResizePointerDown}
onKeyDown={handleSidebarResizeKeyDown}
{...sidebarHandleProps}
>
<span aria-hidden="true" />
</button>
+2 -18
View File
@@ -139,7 +139,6 @@ const loadInitialCollapsedState = (defaultValue) => {
export const SidebarProvider = ({ initialCollapsed = false, children }) => {
const [collapsed, setCollapsedState] = useState(() => loadInitialCollapsedState(initialCollapsed));
const [sidebarSuppressedState, setSidebarSuppressedState] = useState(false);
const initialTheme = useMemo(() => loadInitialThemeSettings(), []);
const [neutralHue, setNeutralHueState] = useState(initialTheme.neutralHue);
const [neutralChroma, setNeutralChromaState] = useState(initialTheme.neutralChroma);
@@ -284,20 +283,10 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => {
setCollapsedState(Boolean(value));
}, []);
const setSidebarSuppressed = useCallback((value) => {
if (typeof value === 'function') {
setSidebarSuppressedState((prev) => Boolean(value(prev)));
return;
}
setSidebarSuppressedState(Boolean(value));
}, []);
const contextValue = useMemo(
() => ({
collapsed,
setCollapsed,
sidebarSuppressed: sidebarSuppressedState,
setSidebarSuppressed,
neutralHue,
setNeutralHue,
neutralChroma,
@@ -318,8 +307,6 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => {
[
collapsed,
setCollapsed,
sidebarSuppressedState,
setSidebarSuppressed,
neutralHue,
setNeutralHue,
neutralChroma,
@@ -347,11 +334,8 @@ export const useSidebarContext = () => {
};
export const useSidebarControls = () => {
const { setCollapsed, setSidebarSuppressed } = useSidebarContext();
const openSidebar = useCallback(() => {
setSidebarSuppressed(false);
setCollapsed(false);
}, [setCollapsed, setSidebarSuppressed]);
const { setCollapsed } = useSidebarContext();
const openSidebar = useCallback(() => setCollapsed(false), [setCollapsed]);
const closeSidebar = useCallback(() => setCollapsed(true), [setCollapsed]);
return { openSidebar, closeSidebar };
};