panel
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user