panel
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Sidebar from '../sidebar/Sidebar';
|
import Sidebar from '../sidebar/Sidebar';
|
||||||
import { useSidebarContext } from '../sidebar/SidebarContext';
|
import { useSidebarContext } from '../sidebar/SidebarContext';
|
||||||
|
import { usePanelManager } from './PanelManagerContext';
|
||||||
|
|
||||||
const DocumentsLayout = ({ sidebarProps, children }) => {
|
const DocumentsLayout = ({ sidebarProps, children }) => {
|
||||||
const { collapsed, sidebarSuppressed } = useSidebarContext();
|
const { collapsed } = useSidebarContext();
|
||||||
|
const { sidebarSuppressed } = usePanelManager();
|
||||||
const sidebarHidden = collapsed || sidebarSuppressed;
|
const sidebarHidden = collapsed || sidebarSuppressed;
|
||||||
return (
|
return (
|
||||||
<main className={`documents-main${sidebarHidden ? ' documents-main--sidebar-hidden' : ''}`}>
|
<main className={`documents-main${sidebarHidden ? ' documents-main--sidebar-hidden' : ''}`}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAppShell } from '../appShellContext';
|
import { useAppShell } from '../appShellContext';
|
||||||
import DocumentsLayout from './DocumentsLayout';
|
import DocumentsLayout from './DocumentsLayout';
|
||||||
@@ -6,6 +6,7 @@ import { useWorkspaceSurface } from './useWorkspaceSurface';
|
|||||||
import PanelHeader from '../ui/PanelHeader';
|
import PanelHeader from '../ui/PanelHeader';
|
||||||
import BreadcrumbTrail from '../ui/BreadcrumbTrail';
|
import BreadcrumbTrail from '../ui/BreadcrumbTrail';
|
||||||
import { SidebarProvider, useSidebarContext } from '../sidebar/SidebarContext';
|
import { SidebarProvider, useSidebarContext } from '../sidebar/SidebarContext';
|
||||||
|
import { PanelManagerProvider, usePanelManager } from './PanelManagerContext';
|
||||||
|
|
||||||
const DocumentsRouteContent = () => {
|
const DocumentsRouteContent = () => {
|
||||||
const {
|
const {
|
||||||
@@ -28,105 +29,11 @@ const DocumentsRouteContent = () => {
|
|||||||
notifyApiError,
|
notifyApiError,
|
||||||
} = useAppShell();
|
} = useAppShell();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { collapsed: sidebarCollapsed, sidebarSuppressed, setCollapsed, setSidebarSuppressed } = useSidebarContext();
|
const { collapsed: sidebarCollapsed } = useSidebarContext();
|
||||||
|
const {
|
||||||
const getDetailPanelWidth = useCallback(() => {
|
sidebarSuppressed,
|
||||||
if (typeof window === 'undefined') {
|
expandSidebar,
|
||||||
return null;
|
} = usePanelManager();
|
||||||
}
|
|
||||||
const panelEl = document.querySelector('.detail-panel');
|
|
||||||
if (panelEl) {
|
|
||||||
const rect = panelEl.getBoundingClientRect();
|
|
||||||
if (Number.isFinite(rect?.width)) {
|
|
||||||
return rect.width;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const rootStyles = window.getComputedStyle(document.documentElement);
|
|
||||||
const varValue = rootStyles.getPropertyValue('--detail-panel-width');
|
|
||||||
const parsed = parseFloat(varValue);
|
|
||||||
return Number.isFinite(parsed) ? parsed : null;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getSidebarWidth = useCallback(() => {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const sidebarEl = document.querySelector('.sidebar');
|
|
||||||
if (sidebarEl) {
|
|
||||||
const rect = sidebarEl.getBoundingClientRect();
|
|
||||||
if (Number.isFinite(rect?.width)) {
|
|
||||||
return rect.width;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const rootStyles = window.getComputedStyle(document.documentElement);
|
|
||||||
const varValue = rootStyles.getPropertyValue('--sidebar-width');
|
|
||||||
const parsed = parseFloat(varValue);
|
|
||||||
return Number.isFinite(parsed) ? parsed : null;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const shouldCloseDetailPanelForSidebar = useCallback(() => {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!detailPanelOpen && !previewDocumentId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const detailWidth = getDetailPanelWidth();
|
|
||||||
const sidebarWidth = getSidebarWidth();
|
|
||||||
if (!Number.isFinite(detailWidth) || !Number.isFinite(sidebarWidth)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return detailWidth + sidebarWidth > window.innerWidth * (2 / 3);
|
|
||||||
}, [detailPanelOpen, previewDocumentId, getDetailPanelWidth, getSidebarWidth]);
|
|
||||||
|
|
||||||
const closeAnyDetailPanel = useCallback(() => {
|
|
||||||
if (previewDocumentId && typeof closeDocumentPreview === 'function') {
|
|
||||||
closeDocumentPreview();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (detailPanelOpen && typeof detailPanelProps?.onClose === 'function') {
|
|
||||||
detailPanelProps.onClose();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}, [previewDocumentId, closeDocumentPreview, detailPanelOpen, detailPanelProps]);
|
|
||||||
|
|
||||||
const sidebarWidthRef = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
sidebarWidthRef.current = getSidebarWidth();
|
|
||||||
}, [getSidebarWidth]);
|
|
||||||
|
|
||||||
const expandSidebar = useCallback(() => {
|
|
||||||
if (shouldCloseDetailPanelForSidebar()) {
|
|
||||||
closeAnyDetailPanel();
|
|
||||||
}
|
|
||||||
setSidebarSuppressed(false);
|
|
||||||
setCollapsed(false);
|
|
||||||
}, [setCollapsed, setSidebarSuppressed, shouldCloseDetailPanelForSidebar, closeAnyDetailPanel]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const handleSidebarResize = (event) => {
|
|
||||||
const nextWidth = Number.isFinite(event?.detail?.width)
|
|
||||||
? event.detail.width
|
|
||||||
: getSidebarWidth();
|
|
||||||
const prevWidth = sidebarWidthRef.current;
|
|
||||||
if (Number.isFinite(nextWidth)) {
|
|
||||||
sidebarWidthRef.current = nextWidth;
|
|
||||||
}
|
|
||||||
if (Number.isFinite(nextWidth) && Number.isFinite(prevWidth) && nextWidth <= prevWidth) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (shouldCloseDetailPanelForSidebar()) {
|
|
||||||
closeAnyDetailPanel();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('sidebar-width-change', handleSidebarResize);
|
|
||||||
return () => window.removeEventListener('sidebar-width-change', handleSidebarResize);
|
|
||||||
}, [shouldCloseDetailPanelForSidebar, closeAnyDetailPanel, getSidebarWidth]);
|
|
||||||
|
|
||||||
const sidebarPropsWithActions = useMemo(
|
const sidebarPropsWithActions = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -137,6 +44,8 @@ const DocumentsRouteContent = () => {
|
|||||||
[sidebarProps, openTagsModal, openCorrespondentsModal],
|
[sidebarProps, openTagsModal, openCorrespondentsModal],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sidebarHidden = sidebarCollapsed || sidebarSuppressed;
|
||||||
|
|
||||||
const breadcrumbs = documentsTableProps?.breadcrumbs || null;
|
const breadcrumbs = documentsTableProps?.breadcrumbs || null;
|
||||||
const parentBreadcrumb = useMemo(() => {
|
const parentBreadcrumb = useMemo(() => {
|
||||||
if (!Array.isArray(breadcrumbs) || breadcrumbs.length <= 1) {
|
if (!Array.isArray(breadcrumbs) || breadcrumbs.length <= 1) {
|
||||||
@@ -163,8 +72,6 @@ const DocumentsRouteContent = () => {
|
|||||||
navigate(target);
|
navigate(target);
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
const sidebarHidden = sidebarCollapsed || sidebarSuppressed;
|
|
||||||
|
|
||||||
const { surface } = useWorkspaceSurface({
|
const { surface } = useWorkspaceSurface({
|
||||||
sidebarHidden,
|
sidebarHidden,
|
||||||
onExpandSidebar: expandSidebar,
|
onExpandSidebar: expandSidebar,
|
||||||
@@ -271,7 +178,9 @@ const DocumentsRouteContent = () => {
|
|||||||
|
|
||||||
const DocumentsRoute = () => (
|
const DocumentsRoute = () => (
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
|
<PanelManagerProvider>
|
||||||
<DocumentsRouteContent />
|
<DocumentsRouteContent />
|
||||||
|
</PanelManagerProvider>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,344 @@
|
|||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { useSidebarContext } from '../sidebar/SidebarContext';
|
||||||
|
|
||||||
|
const PanelManagerContext = createContext(null);
|
||||||
|
|
||||||
|
const PANEL_LIMITS = {
|
||||||
|
sidebar: { minRatio: 1 / 6, maxRatio: 1 / 4 },
|
||||||
|
detail: { minRatio: 1 / 4, maxRatio: 3 / 4 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const STORAGE_KEYS = {
|
||||||
|
sidebar: 'papercrate_sidebar_width',
|
||||||
|
detail: 'papercrate_detail_width',
|
||||||
|
};
|
||||||
|
|
||||||
|
const MINIMAL_FREE_RATIO = 1 / 3;
|
||||||
|
|
||||||
|
const clampPanelWidth = (panel, value) => {
|
||||||
|
const numeric = Number(value);
|
||||||
|
const limits = PANEL_LIMITS[panel];
|
||||||
|
if (!limits) {
|
||||||
|
return numeric;
|
||||||
|
}
|
||||||
|
const viewport = window.innerWidth;
|
||||||
|
const minLimit = Math.max(0, Math.round(viewport * limits.minRatio));
|
||||||
|
const rawMax = Math.max(minLimit, Math.round(viewport * limits.maxRatio));
|
||||||
|
const maxAllowed = Math.min(rawMax, viewport - 160);
|
||||||
|
const targetMax = Math.max(minLimit, maxAllowed);
|
||||||
|
return Math.min(Math.max(numeric, minLimit), targetMax);
|
||||||
|
};
|
||||||
|
|
||||||
|
const readStoredWidth = (panel, fallback) => {
|
||||||
|
const raw = window?.localStorage?.getItem(STORAGE_KEYS[panel]);
|
||||||
|
if (!raw) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
const parsed = Number.parseFloat(raw);
|
||||||
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
const persistWidth = (panel, value) => {
|
||||||
|
window?.localStorage?.setItem(STORAGE_KEYS[panel], String(Math.round(value)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyPanelWidthToRoot = (panel, width) => {
|
||||||
|
if (!Number.isFinite(width)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const varName = panel === 'sidebar' ? '--sidebar-width' : '--detail-panel-width';
|
||||||
|
document?.documentElement?.style.setProperty(varName, `${width}px`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PanelManagerProvider = ({ children }) => {
|
||||||
|
const { collapsed, setCollapsed } = useSidebarContext();
|
||||||
|
const initialSidebarWidth = readStoredWidth('sidebar', 320);
|
||||||
|
const initialDetailWidth = readStoredWidth('detail', 420);
|
||||||
|
|
||||||
|
const [sidebarWidth, setSidebarWidthState] = useState(() => clampPanelWidth('sidebar', initialSidebarWidth));
|
||||||
|
const [detailWidth, setDetailWidthState] = useState(() => clampPanelWidth('detail', initialDetailWidth));
|
||||||
|
const [resizingPanel, setResizingPanel] = useState(null);
|
||||||
|
const [sidebarSuppressed, setSidebarSuppressed] = useState(false);
|
||||||
|
const [detailPanelOpen, setDetailPanelOpen] = useState(false);
|
||||||
|
|
||||||
|
const detailCloseHandlerRef = useRef(null);
|
||||||
|
const panelWidthsRef = useRef({ sidebar: sidebarWidth, detail: detailWidth });
|
||||||
|
|
||||||
|
const closeDetailPanel = useCallback(() => {
|
||||||
|
const handler = detailCloseHandlerRef.current;
|
||||||
|
handler?.();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
panelWidthsRef.current.sidebar = sidebarWidth;
|
||||||
|
applyPanelWidthToRoot('sidebar', sidebarWidth);
|
||||||
|
}, [sidebarWidth]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
panelWidthsRef.current.detail = detailWidth;
|
||||||
|
applyPanelWidthToRoot('detail', detailWidth);
|
||||||
|
}, [detailWidth]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
persistWidth('sidebar', sidebarWidth);
|
||||||
|
}, [sidebarWidth]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
persistWidth('detail', detailWidth);
|
||||||
|
}, [detailWidth]);
|
||||||
|
|
||||||
|
const logPanelState = useCallback((panel, action, value) => {
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const sidebarWidth = panelWidthsRef.current.sidebar;
|
||||||
|
const detailWidth = panelWidthsRef.current.detail;
|
||||||
|
const freeSpace = viewportWidth - sidebarWidth - detailWidth;
|
||||||
|
const freeRatio = viewportWidth > 0 ? freeSpace / viewportWidth : 0;
|
||||||
|
|
||||||
|
const meetsThreshold = freeRatio >= MINIMAL_FREE_RATIO;
|
||||||
|
|
||||||
|
if (panel === 'detail' && !collapsed) {
|
||||||
|
if (action === 'opened' || action === 'resized') {
|
||||||
|
setSidebarSuppressed(!meetsThreshold);
|
||||||
|
} else if (action === 'closed') {
|
||||||
|
setSidebarSuppressed(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (panel === 'sidebar' && (action === 'opened' || action === 'resized') && !meetsThreshold && detailPanelOpen) {
|
||||||
|
closeDetailPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedAction = action === 'resized'
|
||||||
|
? `${panel} resized to ${value}px`
|
||||||
|
: `${panel} ${action}`;
|
||||||
|
console.log(normalizedAction, {
|
||||||
|
sidebar: sidebarWidth > 0 ? `${sidebarWidth}px` : 'closed',
|
||||||
|
detail: `${detailWidth}px`,
|
||||||
|
freeSpace,
|
||||||
|
viewportWidth,
|
||||||
|
freeRatio,
|
||||||
|
minimalFreeRatio: MINIMAL_FREE_RATIO,
|
||||||
|
meetsThreshold,
|
||||||
|
});
|
||||||
|
}, [collapsed, sidebarSuppressed, closeDetailPanel, detailPanelOpen]);
|
||||||
|
|
||||||
|
const collapseSidebar = useCallback(() => {
|
||||||
|
if (!collapsed) {
|
||||||
|
setCollapsed(true);
|
||||||
|
setSidebarSuppressed(false);
|
||||||
|
logPanelState('sidebar', 'closed');
|
||||||
|
}
|
||||||
|
}, [collapsed, setCollapsed, logPanelState]);
|
||||||
|
|
||||||
|
const setPanelWidth = useCallback(
|
||||||
|
(panel, width, commit = true) => {
|
||||||
|
const clamped = clampPanelWidth(panel, width);
|
||||||
|
if (!Number.isFinite(clamped)) {
|
||||||
|
return panelWidthsRef.current[panel];
|
||||||
|
}
|
||||||
|
if (panel === 'sidebar') {
|
||||||
|
setSidebarWidthState((prev) => (prev === clamped ? prev : clamped));
|
||||||
|
} else {
|
||||||
|
setDetailWidthState((prev) => (prev === clamped ? prev : clamped));
|
||||||
|
}
|
||||||
|
panelWidthsRef.current[panel] = clamped;
|
||||||
|
applyPanelWidthToRoot(panel, clamped);
|
||||||
|
if (commit) {
|
||||||
|
persistWidth(panel, clamped);
|
||||||
|
}
|
||||||
|
|
||||||
|
logPanelState(panel, 'resized', clamped);
|
||||||
|
return clamped;
|
||||||
|
},
|
||||||
|
[collapsed, logPanelState, sidebarSuppressed],
|
||||||
|
);
|
||||||
|
|
||||||
|
const registerDetailCloseHandler = useCallback((handler = null) => {
|
||||||
|
detailCloseHandlerRef.current = typeof handler === 'function' ? handler : null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setDetailActive = useCallback(
|
||||||
|
(isOpen) => {
|
||||||
|
setDetailPanelOpen(Boolean(isOpen));
|
||||||
|
logPanelState('detail', isOpen ? 'opened' : 'closed');
|
||||||
|
},
|
||||||
|
[logPanelState],
|
||||||
|
);
|
||||||
|
|
||||||
|
const expandSidebar = useCallback(() => {
|
||||||
|
if (collapsed) {
|
||||||
|
setCollapsed(false);
|
||||||
|
}
|
||||||
|
setSidebarSuppressed(false);
|
||||||
|
logPanelState('sidebar', 'opened');
|
||||||
|
}, [collapsed, setCollapsed, logPanelState]);
|
||||||
|
|
||||||
|
const startPanelResize = useCallback((panel) => {
|
||||||
|
setResizingPanel(panel);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const stopPanelResize = useCallback(() => {
|
||||||
|
setResizingPanel(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getPanelWidth = useCallback((panel) => panelWidthsRef.current[panel] || 0, []);
|
||||||
|
|
||||||
|
const contextValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
sidebarWidth,
|
||||||
|
detailWidth,
|
||||||
|
sidebarSuppressed,
|
||||||
|
resizingPanel,
|
||||||
|
setPanelWidth,
|
||||||
|
startPanelResize,
|
||||||
|
stopPanelResize,
|
||||||
|
getPanelWidth,
|
||||||
|
setDetailActive,
|
||||||
|
closeDetailPanel,
|
||||||
|
collapseSidebar,
|
||||||
|
expandSidebar,
|
||||||
|
registerDetailCloseHandler,
|
||||||
|
detailPanelOpen,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
sidebarWidth,
|
||||||
|
detailWidth,
|
||||||
|
sidebarSuppressed,
|
||||||
|
resizingPanel,
|
||||||
|
setPanelWidth,
|
||||||
|
startPanelResize,
|
||||||
|
stopPanelResize,
|
||||||
|
getPanelWidth,
|
||||||
|
setDetailActive,
|
||||||
|
closeDetailPanel,
|
||||||
|
collapseSidebar,
|
||||||
|
expandSidebar,
|
||||||
|
registerDetailCloseHandler,
|
||||||
|
detailPanelOpen,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return <PanelManagerContext.Provider value={contextValue}>{children}</PanelManagerContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePanelManager = () => {
|
||||||
|
const context = useContext(PanelManagerContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('usePanelManager must be used within a PanelManagerProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePanelResizeBindings = (panel, { panelRef = null, enabled = true } = {}) => {
|
||||||
|
const {
|
||||||
|
sidebarWidth,
|
||||||
|
detailWidth,
|
||||||
|
resizingPanel,
|
||||||
|
setPanelWidth,
|
||||||
|
startPanelResize,
|
||||||
|
stopPanelResize,
|
||||||
|
getPanelWidth,
|
||||||
|
} = usePanelManager();
|
||||||
|
|
||||||
|
const liveWidth = panel === 'sidebar' ? sidebarWidth : detailWidth;
|
||||||
|
const latestWidthRef = useRef(liveWidth);
|
||||||
|
const cleanupRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
latestWidthRef.current = liveWidth;
|
||||||
|
}, [liveWidth]);
|
||||||
|
|
||||||
|
const teardownListeners = useCallback(() => {
|
||||||
|
if (cleanupRef.current) {
|
||||||
|
cleanupRef.current();
|
||||||
|
cleanupRef.current = null;
|
||||||
|
}
|
||||||
|
stopPanelResize();
|
||||||
|
}, [stopPanelResize]);
|
||||||
|
|
||||||
|
useEffect(() => () => teardownListeners(), [teardownListeners]);
|
||||||
|
|
||||||
|
const handlePointerDown = useCallback(
|
||||||
|
(event) => {
|
||||||
|
if (!enabled || !panelRef?.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const rect = panelRef.current.getBoundingClientRect();
|
||||||
|
const startWidth = Number.isFinite(rect?.width) ? rect.width : getPanelWidth(panel);
|
||||||
|
if (!Number.isFinite(startWidth)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pointerId = event.pointerId ?? 'mouse';
|
||||||
|
const startX = event.clientX;
|
||||||
|
startPanelResize(panel);
|
||||||
|
event.currentTarget?.setPointerCapture?.(pointerId);
|
||||||
|
let lastWidth = startWidth;
|
||||||
|
|
||||||
|
const handlePointerMove = (moveEvent) => {
|
||||||
|
if (moveEvent.pointerId !== pointerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const delta = panel === 'sidebar'
|
||||||
|
? moveEvent.clientX - startX
|
||||||
|
: startX - moveEvent.clientX;
|
||||||
|
lastWidth = setPanelWidth(panel, startWidth + delta, false);
|
||||||
|
latestWidthRef.current = lastWidth;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePointerUp = (upEvent) => {
|
||||||
|
if (upEvent.pointerId !== pointerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.currentTarget?.releasePointerCapture?.(pointerId);
|
||||||
|
window.removeEventListener('pointermove', handlePointerMove);
|
||||||
|
window.removeEventListener('pointerup', handlePointerUp);
|
||||||
|
setPanelWidth(panel, lastWidth);
|
||||||
|
teardownListeners();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('pointermove', handlePointerMove);
|
||||||
|
window.addEventListener('pointerup', handlePointerUp);
|
||||||
|
cleanupRef.current = () => {
|
||||||
|
window.removeEventListener('pointermove', handlePointerMove);
|
||||||
|
window.removeEventListener('pointerup', handlePointerUp);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[
|
||||||
|
enabled,
|
||||||
|
panelRef,
|
||||||
|
panel,
|
||||||
|
getPanelWidth,
|
||||||
|
startPanelResize,
|
||||||
|
setPanelWidth,
|
||||||
|
teardownListeners,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const panelStyle = enabled && Number.isFinite(liveWidth)
|
||||||
|
? { width: `${liveWidth}px` }
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const handleProps = enabled
|
||||||
|
? {
|
||||||
|
onPointerDown: handlePointerDown,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
panelStyle,
|
||||||
|
handleProps,
|
||||||
|
isPanelResizing: resizingPanel === panel,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PanelManagerContext;
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { SidebarExpandIcon } from '../ui/icons';
|
import { SidebarExpandIcon } from '../ui/icons';
|
||||||
import { createDocumentsSurface } from '../documents/DocumentsPanel';
|
import { createDocumentsSurface } from '../documents/DocumentsPanel';
|
||||||
import { createDocumentViewerSurface } from '../preview/DocumentViewerPanel';
|
import { createDocumentViewerSurface } from '../preview/DocumentViewerPanel';
|
||||||
import createDesktopSurface from '../desktop/createDesktopSurface';
|
import createDesktopSurface from '../desktop/createDesktopSurface';
|
||||||
|
import { usePanelManager } from './PanelManagerContext';
|
||||||
|
|
||||||
export const useWorkspaceSurface = ({
|
export const useWorkspaceSurface = ({
|
||||||
sidebarHidden,
|
sidebarHidden,
|
||||||
@@ -24,6 +25,19 @@ export const useWorkspaceSurface = ({
|
|||||||
parentBreadcrumb,
|
parentBreadcrumb,
|
||||||
onNavigateParent,
|
onNavigateParent,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { registerDetailCloseHandler, setDetailActive } = usePanelManager();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = detailPanelProps?.onClose || null;
|
||||||
|
registerDetailCloseHandler(handler);
|
||||||
|
return () => registerDetailCloseHandler(null);
|
||||||
|
}, [registerDetailCloseHandler, detailPanelProps?.onClose]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDetailActive(Boolean(detailPanelOpen));
|
||||||
|
return () => setDetailActive(false);
|
||||||
|
}, [detailPanelOpen, setDetailActive]);
|
||||||
|
|
||||||
const renderSidebarToggle = useCallback(() => {
|
const renderSidebarToggle = useCallback(() => {
|
||||||
if (!sidebarHidden) {
|
if (!sidebarHidden) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -26,88 +26,8 @@ import BreadcrumbTrail from '../ui/BreadcrumbTrail';
|
|||||||
import { useAssetNavigator } from '../hooks/useAssetNavigator';
|
import { useAssetNavigator } from '../hooks/useAssetNavigator';
|
||||||
import DocumentViewerLayout from './DocumentViewerLayout';
|
import DocumentViewerLayout from './DocumentViewerLayout';
|
||||||
import useViewerLayoutMode from './useViewerLayoutMode';
|
import useViewerLayoutMode from './useViewerLayoutMode';
|
||||||
import { useSidebarContext } from '../sidebar/SidebarContext';
|
import { usePanelResizeBindings } from '../app/PanelManagerContext';
|
||||||
|
|
||||||
const DETAIL_PANEL_WIDTH_STORAGE_KEY = 'detailPanelWidth';
|
|
||||||
const MIN_DETAIL_PANEL_WIDTH = 320;
|
|
||||||
const MAX_DETAIL_PANEL_WIDTH = 960;
|
|
||||||
|
|
||||||
const isBrowser = typeof window !== 'undefined';
|
|
||||||
const isDocumentAvailable = typeof document !== 'undefined';
|
|
||||||
|
|
||||||
const getDetailPanelBounds = () => {
|
|
||||||
if (!isBrowser) {
|
|
||||||
return {
|
|
||||||
min: MIN_DETAIL_PANEL_WIDTH,
|
|
||||||
max: MAX_DETAIL_PANEL_WIDTH,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const viewportWidth = Math.max(window.innerWidth, 1);
|
|
||||||
const minFractionWidth = viewportWidth / 5;
|
|
||||||
const maxFractionWidth = viewportWidth * 0.75;
|
|
||||||
const rawMin = Math.max(MIN_DETAIL_PANEL_WIDTH, minFractionWidth);
|
|
||||||
const rawMax = Math.min(MAX_DETAIL_PANEL_WIDTH, maxFractionWidth);
|
|
||||||
if (rawMin >= rawMax) {
|
|
||||||
const fallback = Math.min(Math.max(rawMin, viewportWidth * 0.5), MAX_DETAIL_PANEL_WIDTH);
|
|
||||||
return { min: fallback, max: fallback };
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
min: rawMin,
|
|
||||||
max: rawMax,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const clampDetailPanelWidth = (value) => {
|
|
||||||
if (!Number.isFinite(value) || value <= 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const { min, max } = getDetailPanelBounds();
|
|
||||||
return Math.min(Math.max(value, min), max);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadStoredDetailPanelWidth = () => {
|
|
||||||
if (!isBrowser) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const raw = window.localStorage?.getItem(DETAIL_PANEL_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 detail panel width', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyDetailPanelWidth = (width) => {
|
|
||||||
if (!isDocumentAvailable || width == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
document.documentElement.style.setProperty('--detail-panel-width', `${width}px`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSidebarWidthFromRoot = () => {
|
|
||||||
if (!isBrowser || !isDocumentAvailable) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const computed = window.getComputedStyle(document.documentElement);
|
|
||||||
const parsed = parseFloat(computed.getPropertyValue('--sidebar-width'));
|
|
||||||
return Number.isFinite(parsed) ? parsed : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const shouldSuppressSidebarForDetailWidth = (detailWidth) => {
|
|
||||||
if (!isBrowser || !Number.isFinite(detailWidth)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const sidebarWidth = getSidebarWidthFromRoot() || 0;
|
|
||||||
const minMainContentWidth = window.innerWidth / 3;
|
|
||||||
const occupiedWidth = detailWidth + sidebarWidth;
|
|
||||||
const availableWidth = window.innerWidth - occupiedWidth;
|
|
||||||
return availableWidth < minMainContentWidth;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createDocumentViewerHeaderActions = ({
|
export const createDocumentViewerHeaderActions = ({
|
||||||
document,
|
document,
|
||||||
@@ -181,7 +101,6 @@ const DocumentViewerPanel = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const isSidebarVariant = variant === 'sidebar';
|
const isSidebarVariant = variant === 'sidebar';
|
||||||
const { setSidebarSuppressed } = useSidebarContext();
|
|
||||||
const sortedCorrespondents = useMemo(
|
const sortedCorrespondents = useMemo(
|
||||||
() => sortCorrespondents(document?.correspondents || []),
|
() => sortCorrespondents(document?.correspondents || []),
|
||||||
[document],
|
[document],
|
||||||
@@ -322,166 +241,14 @@ const DocumentViewerPanel = ({
|
|||||||
|
|
||||||
const panelRef = useRef(null);
|
const panelRef = useRef(null);
|
||||||
const isStackedLayout = useViewerLayoutMode(panelRef, document?.id);
|
const isStackedLayout = useViewerLayoutMode(panelRef, document?.id);
|
||||||
const pendingWidthRef = useRef(null);
|
|
||||||
const [isResizingPanel, setIsResizingPanel] = useState(false);
|
|
||||||
const [detailPanelWidth, setDetailPanelWidth] = useState(() => {
|
|
||||||
if (!isBrowser) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const stored = loadStoredDetailPanelWidth();
|
|
||||||
return stored != null ? clampDetailPanelWidth(stored) : null;
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
const {
|
||||||
if (detailPanelWidth != null) {
|
panelStyle: managedDetailPanelStyle,
|
||||||
const clamped = clampDetailPanelWidth(detailPanelWidth);
|
handleProps: managedResizeHandleProps,
|
||||||
if (clamped != null) {
|
isPanelResizing,
|
||||||
applyDetailPanelWidth(clamped);
|
} = usePanelResizeBindings('detail', { enabled: isSidebarVariant, panelRef });
|
||||||
}
|
const detailPanelStyle = isSidebarVariant ? managedDetailPanelStyle : undefined;
|
||||||
}
|
const resizeHandleProps = isSidebarVariant ? managedResizeHandleProps : {};
|
||||||
}, [detailPanelWidth]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isSidebarVariant) {
|
|
||||||
setSidebarSuppressed(false);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const updateSuppression = () => {
|
|
||||||
if (!isBrowser) {
|
|
||||||
setSidebarSuppressed(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const panelWidth = pendingWidthRef.current
|
|
||||||
?? detailPanelWidth
|
|
||||||
?? panelRef.current?.getBoundingClientRect().width;
|
|
||||||
setSidebarSuppressed(shouldSuppressSidebarForDetailWidth(panelWidth));
|
|
||||||
};
|
|
||||||
|
|
||||||
updateSuppression();
|
|
||||||
const handleWindowResize = () => updateSuppression();
|
|
||||||
window.addEventListener('resize', handleWindowResize);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', handleWindowResize);
|
|
||||||
};
|
|
||||||
}, [isSidebarVariant, detailPanelWidth, setSidebarSuppressed]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isSidebarVariant) {
|
|
||||||
setSidebarSuppressed(false);
|
|
||||||
}
|
|
||||||
}, [isSidebarVariant, setSidebarSuppressed]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isBrowser) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const handleResize = () => {
|
|
||||||
setDetailPanelWidth((prev) => {
|
|
||||||
if (prev == null) {
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
const clamped = clampDetailPanelWidth(prev);
|
|
||||||
if (clamped != null && clamped !== prev) {
|
|
||||||
applyDetailPanelWidth(clamped);
|
|
||||||
try {
|
|
||||||
window.localStorage?.setItem(DETAIL_PANEL_WIDTH_STORAGE_KEY, String(Math.round(clamped)));
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to persist detail panel width', error);
|
|
||||||
}
|
|
||||||
return clamped;
|
|
||||||
}
|
|
||||||
return prev;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
return () => window.removeEventListener('resize', handleResize);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleResizePointerDown = useCallback((event) => {
|
|
||||||
if (!isSidebarVariant || !panelRef.current || !isBrowser) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
const pointerId = event.pointerId;
|
|
||||||
const target = event.currentTarget;
|
|
||||||
target.setPointerCapture?.(pointerId);
|
|
||||||
setIsResizingPanel(true);
|
|
||||||
|
|
||||||
const rect = panelRef.current.getBoundingClientRect();
|
|
||||||
const startWidth = rect.width;
|
|
||||||
const startX = event.clientX;
|
|
||||||
|
|
||||||
const updateWidth = (nextWidth) => {
|
|
||||||
const clamped = clampDetailPanelWidth(nextWidth);
|
|
||||||
if (clamped != null) {
|
|
||||||
pendingWidthRef.current = clamped;
|
|
||||||
applyDetailPanelWidth(clamped);
|
|
||||||
if (isSidebarVariant) {
|
|
||||||
setSidebarSuppressed(shouldSuppressSidebarForDetailWidth(clamped));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePointerMove = (moveEvent) => {
|
|
||||||
if (moveEvent.pointerId !== pointerId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const delta = startX - moveEvent.clientX;
|
|
||||||
updateWidth(startWidth + delta);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePointerUp = (upEvent) => {
|
|
||||||
if (upEvent.pointerId !== pointerId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
target.releasePointerCapture?.(pointerId);
|
|
||||||
window.removeEventListener('pointermove', handlePointerMove);
|
|
||||||
window.removeEventListener('pointerup', handlePointerUp);
|
|
||||||
setIsResizingPanel(false);
|
|
||||||
if (pendingWidthRef.current != null) {
|
|
||||||
const finalizedWidth = pendingWidthRef.current;
|
|
||||||
pendingWidthRef.current = null;
|
|
||||||
setDetailPanelWidth(finalizedWidth);
|
|
||||||
try {
|
|
||||||
window.localStorage?.setItem(DETAIL_PANEL_WIDTH_STORAGE_KEY, String(Math.round(finalizedWidth)));
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to persist detail panel width', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('pointermove', handlePointerMove);
|
|
||||||
window.addEventListener('pointerup', handlePointerUp);
|
|
||||||
}, [isSidebarVariant, setSidebarSuppressed]);
|
|
||||||
|
|
||||||
const handleResizeKeyDown = useCallback((event) => {
|
|
||||||
if (!isSidebarVariant || !isBrowser) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const baseWidth = pendingWidthRef.current
|
|
||||||
?? detailPanelWidth
|
|
||||||
?? panelRef.current?.getBoundingClientRect().width;
|
|
||||||
if (!Number.isFinite(baseWidth)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
const step = event.shiftKey ? 40 : 20;
|
|
||||||
const delta = event.key === 'ArrowLeft' ? step : -step;
|
|
||||||
const nextWidth = clampDetailPanelWidth(baseWidth + delta);
|
|
||||||
if (nextWidth == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setDetailPanelWidth(nextWidth);
|
|
||||||
try {
|
|
||||||
window.localStorage?.setItem(DETAIL_PANEL_WIDTH_STORAGE_KEY, String(Math.round(nextWidth)));
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to persist detail panel width', error);
|
|
||||||
}
|
|
||||||
}, [detailPanelWidth, isSidebarVariant]);
|
|
||||||
|
|
||||||
const viewerClassName = isStackedLayout
|
const viewerClassName = isStackedLayout
|
||||||
? 'document-viewer document-viewer--stacked'
|
? 'document-viewer document-viewer--stacked'
|
||||||
@@ -649,10 +416,9 @@ const DocumentViewerPanel = ({
|
|||||||
const resizeHandle = isSidebarVariant ? (
|
const resizeHandle = isSidebarVariant ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`detail-panel__resize-handle${isResizingPanel ? ' is-active' : ''}`}
|
className={`detail-panel__resize-handle${isPanelResizing ? ' is-active' : ''}`}
|
||||||
aria-label="Resize detail panel"
|
aria-label="Resize detail panel"
|
||||||
onPointerDown={handleResizePointerDown}
|
{...resizeHandleProps}
|
||||||
onKeyDown={handleResizeKeyDown}
|
|
||||||
>
|
>
|
||||||
<span aria-hidden="true" />
|
<span aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
@@ -715,7 +481,11 @@ const DocumentViewerPanel = ({
|
|||||||
if (isSidebarVariant) {
|
if (isSidebarVariant) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<aside className={`detail-panel panel${isResizingPanel ? ' detail-panel--resizing' : ''}`} ref={panelRef}>
|
<aside
|
||||||
|
className={`detail-panel panel${isPanelResizing ? ' detail-panel--resizing' : ''}`}
|
||||||
|
ref={panelRef}
|
||||||
|
style={detailPanelStyle}
|
||||||
|
>
|
||||||
{resizeHandle}
|
{resizeHandle}
|
||||||
<PanelHeader
|
<PanelHeader
|
||||||
leading={headerLeadingContent}
|
leading={headerLeadingContent}
|
||||||
|
|||||||
@@ -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 { createPortal } from 'react-dom';
|
||||||
import {
|
import {
|
||||||
ChevronIcon,
|
ChevronIcon,
|
||||||
@@ -23,80 +23,7 @@ import useFloatingMenu from '../ui/useFloatingMenu';
|
|||||||
|
|
||||||
import { getTagColorStyle } from '../utils/colors';
|
import { getTagColorStyle } from '../utils/colors';
|
||||||
import { useSidebarContext } from './SidebarContext';
|
import { useSidebarContext } from './SidebarContext';
|
||||||
|
import { usePanelManager, usePanelResizeBindings } from '../app/PanelManagerContext';
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
const FolderNode = ({
|
const FolderNode = ({
|
||||||
node,
|
node,
|
||||||
@@ -265,7 +192,6 @@ const Sidebar = ({
|
|||||||
onUploadFiles,
|
onUploadFiles,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
setCollapsed,
|
|
||||||
neutralHue,
|
neutralHue,
|
||||||
setNeutralHue,
|
setNeutralHue,
|
||||||
resetNeutralHue,
|
resetNeutralHue,
|
||||||
@@ -278,140 +204,22 @@ const Sidebar = ({
|
|||||||
themeMode,
|
themeMode,
|
||||||
cycleThemeMode,
|
cycleThemeMode,
|
||||||
themeModes,
|
themeModes,
|
||||||
sidebarSuppressed,
|
|
||||||
} = useSidebarContext();
|
} = useSidebarContext();
|
||||||
|
const {
|
||||||
|
sidebarSuppressed,
|
||||||
|
collapseSidebar,
|
||||||
|
} = usePanelManager();
|
||||||
const uploadInputRef = useRef(null);
|
const uploadInputRef = useRef(null);
|
||||||
const sidebarRef = useRef(null);
|
const sidebarRef = useRef(null);
|
||||||
const pendingWidthRef = useRef(null);
|
const {
|
||||||
const [isResizingSidebar, setIsResizingSidebar] = useState(false);
|
panelStyle: sidebarStyle,
|
||||||
const [sidebarWidthState, setSidebarWidthState] = useState(() => {
|
handleProps: sidebarHandleProps,
|
||||||
if (!isBrowser) {
|
isPanelResizing: isResizingSidebar,
|
||||||
return null;
|
} = usePanelResizeBindings('sidebar', { panelRef: sidebarRef });
|
||||||
}
|
|
||||||
const stored = loadStoredSidebarWidth();
|
|
||||||
return stored != null ? clampSidebarWidth(stored) : null;
|
|
||||||
});
|
|
||||||
|
|
||||||
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(() => {
|
const handleCollapse = useCallback(() => {
|
||||||
setCollapsed(true);
|
collapseSidebar('sidebar');
|
||||||
}, [setCollapsed]);
|
}, [collapseSidebar]);
|
||||||
const neutralHueInputId = useId();
|
const neutralHueInputId = useId();
|
||||||
const neutralChromaInputId = useId();
|
const neutralChromaInputId = useId();
|
||||||
const neutralContrastInputId = useId();
|
const neutralContrastInputId = useId();
|
||||||
@@ -759,13 +567,12 @@ const Sidebar = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={sidebarClassNames.join(' ')} ref={sidebarRef}>
|
<aside className={sidebarClassNames.join(' ')} ref={sidebarRef} style={sidebarStyle}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`sidebar__resize-handle${isResizingSidebar ? ' is-active' : ''}`}
|
className={`sidebar__resize-handle${isResizingSidebar ? ' is-active' : ''}`}
|
||||||
aria-label="Resize sidebar"
|
aria-label="Resize sidebar"
|
||||||
onPointerDown={handleSidebarResizePointerDown}
|
{...sidebarHandleProps}
|
||||||
onKeyDown={handleSidebarResizeKeyDown}
|
|
||||||
>
|
>
|
||||||
<span aria-hidden="true" />
|
<span aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -139,7 +139,6 @@ const loadInitialCollapsedState = (defaultValue) => {
|
|||||||
|
|
||||||
export const SidebarProvider = ({ initialCollapsed = false, children }) => {
|
export const SidebarProvider = ({ initialCollapsed = false, children }) => {
|
||||||
const [collapsed, setCollapsedState] = useState(() => loadInitialCollapsedState(initialCollapsed));
|
const [collapsed, setCollapsedState] = useState(() => loadInitialCollapsedState(initialCollapsed));
|
||||||
const [sidebarSuppressedState, setSidebarSuppressedState] = useState(false);
|
|
||||||
const initialTheme = useMemo(() => loadInitialThemeSettings(), []);
|
const initialTheme = useMemo(() => loadInitialThemeSettings(), []);
|
||||||
const [neutralHue, setNeutralHueState] = useState(initialTheme.neutralHue);
|
const [neutralHue, setNeutralHueState] = useState(initialTheme.neutralHue);
|
||||||
const [neutralChroma, setNeutralChromaState] = useState(initialTheme.neutralChroma);
|
const [neutralChroma, setNeutralChromaState] = useState(initialTheme.neutralChroma);
|
||||||
@@ -284,20 +283,10 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => {
|
|||||||
setCollapsedState(Boolean(value));
|
setCollapsedState(Boolean(value));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setSidebarSuppressed = useCallback((value) => {
|
|
||||||
if (typeof value === 'function') {
|
|
||||||
setSidebarSuppressedState((prev) => Boolean(value(prev)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSidebarSuppressedState(Boolean(value));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const contextValue = useMemo(
|
const contextValue = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
collapsed,
|
collapsed,
|
||||||
setCollapsed,
|
setCollapsed,
|
||||||
sidebarSuppressed: sidebarSuppressedState,
|
|
||||||
setSidebarSuppressed,
|
|
||||||
neutralHue,
|
neutralHue,
|
||||||
setNeutralHue,
|
setNeutralHue,
|
||||||
neutralChroma,
|
neutralChroma,
|
||||||
@@ -318,8 +307,6 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => {
|
|||||||
[
|
[
|
||||||
collapsed,
|
collapsed,
|
||||||
setCollapsed,
|
setCollapsed,
|
||||||
sidebarSuppressedState,
|
|
||||||
setSidebarSuppressed,
|
|
||||||
neutralHue,
|
neutralHue,
|
||||||
setNeutralHue,
|
setNeutralHue,
|
||||||
neutralChroma,
|
neutralChroma,
|
||||||
@@ -347,11 +334,8 @@ export const useSidebarContext = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useSidebarControls = () => {
|
export const useSidebarControls = () => {
|
||||||
const { setCollapsed, setSidebarSuppressed } = useSidebarContext();
|
const { setCollapsed } = useSidebarContext();
|
||||||
const openSidebar = useCallback(() => {
|
const openSidebar = useCallback(() => setCollapsed(false), [setCollapsed]);
|
||||||
setSidebarSuppressed(false);
|
|
||||||
setCollapsed(false);
|
|
||||||
}, [setCollapsed, setSidebarSuppressed]);
|
|
||||||
const closeSidebar = useCallback(() => setCollapsed(true), [setCollapsed]);
|
const closeSidebar = useCallback(() => setCollapsed(true), [setCollapsed]);
|
||||||
return { openSidebar, closeSidebar };
|
return { openSidebar, closeSidebar };
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user