panels
This commit is contained in:
@@ -3,10 +3,11 @@ import Sidebar from '../sidebar/Sidebar';
|
|||||||
import { useSidebarContext } from '../sidebar/SidebarContext';
|
import { useSidebarContext } from '../sidebar/SidebarContext';
|
||||||
|
|
||||||
const DocumentsLayout = ({ sidebarProps, children }) => {
|
const DocumentsLayout = ({ sidebarProps, children }) => {
|
||||||
const { collapsed } = useSidebarContext();
|
const { collapsed, sidebarSuppressed } = useSidebarContext();
|
||||||
|
const sidebarHidden = collapsed || sidebarSuppressed;
|
||||||
return (
|
return (
|
||||||
<main className={`documents-main${collapsed ? ' documents-main--sidebar-collapsed' : ''}`}>
|
<main className={`documents-main${sidebarHidden ? ' documents-main--sidebar-hidden' : ''}`}>
|
||||||
{!collapsed ? <Sidebar {...sidebarProps} /> : null}
|
{!sidebarHidden ? <Sidebar {...sidebarProps} /> : null}
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef } 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';
|
||||||
@@ -28,9 +28,105 @@ const DocumentsRouteContent = () => {
|
|||||||
notifyApiError,
|
notifyApiError,
|
||||||
} = useAppShell();
|
} = useAppShell();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { collapsed: sidebarCollapsed, setCollapsed } = useSidebarContext();
|
const { collapsed: sidebarCollapsed, sidebarSuppressed, setCollapsed, setSidebarSuppressed } = useSidebarContext();
|
||||||
|
|
||||||
const expandSidebar = useCallback(() => setCollapsed(false), [setCollapsed]);
|
const getDetailPanelWidth = useCallback(() => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
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(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -67,8 +163,10 @@ const DocumentsRouteContent = () => {
|
|||||||
navigate(target);
|
navigate(target);
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
|
const sidebarHidden = sidebarCollapsed || sidebarSuppressed;
|
||||||
|
|
||||||
const { surface } = useWorkspaceSurface({
|
const { surface } = useWorkspaceSurface({
|
||||||
sidebarCollapsed,
|
sidebarHidden,
|
||||||
onExpandSidebar: expandSidebar,
|
onExpandSidebar: expandSidebar,
|
||||||
documentsTableProps,
|
documentsTableProps,
|
||||||
detailPanelProps,
|
detailPanelProps,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { createDocumentViewerSurface } from '../preview/DocumentViewerPanel';
|
|||||||
import createDesktopSurface from '../desktop/createDesktopSurface';
|
import createDesktopSurface from '../desktop/createDesktopSurface';
|
||||||
|
|
||||||
export const useWorkspaceSurface = ({
|
export const useWorkspaceSurface = ({
|
||||||
sidebarCollapsed,
|
sidebarHidden,
|
||||||
onExpandSidebar,
|
onExpandSidebar,
|
||||||
documentsTableProps,
|
documentsTableProps,
|
||||||
detailPanelProps,
|
detailPanelProps,
|
||||||
@@ -25,7 +25,7 @@ export const useWorkspaceSurface = ({
|
|||||||
onNavigateParent,
|
onNavigateParent,
|
||||||
}) => {
|
}) => {
|
||||||
const renderSidebarToggle = useCallback(() => {
|
const renderSidebarToggle = useCallback(() => {
|
||||||
if (!sidebarCollapsed) {
|
if (!sidebarHidden) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -39,7 +39,7 @@ export const useWorkspaceSurface = ({
|
|||||||
<SidebarExpandIcon />
|
<SidebarExpandIcon />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}, [sidebarCollapsed, onExpandSidebar]);
|
}, [sidebarHidden, onExpandSidebar]);
|
||||||
|
|
||||||
const documentsSurface = useMemo(() => {
|
const documentsSurface = useMemo(() => {
|
||||||
if (!documentsTableProps) {
|
if (!documentsTableProps) {
|
||||||
|
|||||||
@@ -26,6 +26,77 @@ 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';
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
export const createDocumentViewerHeaderActions = ({
|
export const createDocumentViewerHeaderActions = ({
|
||||||
document,
|
document,
|
||||||
@@ -99,6 +170,7 @@ 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],
|
||||||
@@ -239,6 +311,174 @@ 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(() => {
|
||||||
|
if (detailPanelWidth != null) {
|
||||||
|
const clamped = clampDetailPanelWidth(detailPanelWidth);
|
||||||
|
if (clamped != null) {
|
||||||
|
applyDetailPanelWidth(clamped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [detailPanelWidth]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSidebarVariant) {
|
||||||
|
setSidebarSuppressed(false);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const updateSuppression = () => {
|
||||||
|
if (!isBrowser) {
|
||||||
|
setSidebarSuppressed(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const panelWidth = pendingWidthRef.current
|
||||||
|
?? detailPanelWidth
|
||||||
|
?? panelRef.current?.getBoundingClientRect().width;
|
||||||
|
const sidebarWidth = getSidebarWidthFromRoot();
|
||||||
|
if (!Number.isFinite(panelWidth)) {
|
||||||
|
setSidebarSuppressed(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const minMainContentWidth = (window.innerWidth * 2) / 5;
|
||||||
|
const occupiedWidth = panelWidth + (Number.isFinite(sidebarWidth) ? sidebarWidth : 0);
|
||||||
|
const availableWidth = window.innerWidth - occupiedWidth;
|
||||||
|
setSidebarSuppressed(availableWidth < minMainContentWidth);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 && isBrowser) {
|
||||||
|
setSidebarSuppressed(clamped > window.innerWidth / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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'
|
||||||
@@ -403,6 +643,18 @@ const DocumentViewerPanel = ({
|
|||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const resizeHandle = isSidebarVariant ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`detail-panel__resize-handle${isResizingPanel ? ' is-active' : ''}`}
|
||||||
|
aria-label="Resize detail panel"
|
||||||
|
onPointerDown={handleResizePointerDown}
|
||||||
|
onKeyDown={handleResizeKeyDown}
|
||||||
|
>
|
||||||
|
<span aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
) : null;
|
||||||
|
|
||||||
const loadingSection = (
|
const loadingSection = (
|
||||||
<div className="document-viewer-panel__body">
|
<div className="document-viewer-panel__body">
|
||||||
<section className="document-viewer document-viewer--loading">
|
<section className="document-viewer document-viewer--loading">
|
||||||
@@ -460,7 +712,8 @@ const DocumentViewerPanel = ({
|
|||||||
if (isSidebarVariant) {
|
if (isSidebarVariant) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<aside className="detail-panel panel" ref={panelRef}>
|
<aside className={`detail-panel panel${isResizingPanel ? ' detail-panel--resizing' : ''}`} ref={panelRef}>
|
||||||
|
{resizeHandle}
|
||||||
<PanelHeader
|
<PanelHeader
|
||||||
leading={headerLeadingContent}
|
leading={headerLeadingContent}
|
||||||
title={headerTitle}
|
title={headerTitle}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useLayoutEffect, useState } from 'react';
|
import { useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
const PORTRAIT_WIDTH_TO_HEIGHT = 1 / Math.sqrt(2); // ≈0.707 (A-series aspect ratio)
|
|
||||||
export const DOCUMENT_VIEWER_PORTRAIT_HEIGHT_RATIO = 0.4;
|
export const DOCUMENT_VIEWER_PORTRAIT_HEIGHT_RATIO = 0.4;
|
||||||
const PORTRAIT_RATIO_STYLE_ID = 'document-viewer-portrait-ratio-style';
|
const PORTRAIT_RATIO_STYLE_ID = 'document-viewer-portrait-ratio-style';
|
||||||
|
|
||||||
@@ -25,14 +24,10 @@ const ensurePortraitRatioStyle = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const computeStackedLayoutBreakpoint = () => {
|
const computeStackedLayoutBreakpoint = () => {
|
||||||
const viewportHeight = typeof window !== 'undefined' ? window.innerHeight : 900;
|
if (typeof window === 'undefined') {
|
||||||
const portraitViewportWidth = viewportHeight
|
return 900;
|
||||||
* DOCUMENT_VIEWER_PORTRAIT_HEIGHT_RATIO
|
}
|
||||||
* PORTRAIT_WIDTH_TO_HEIGHT;
|
return window.innerWidth / 2;
|
||||||
const detailsColumnWidth = 320; // px ~ 20rem for metadata & tabs
|
|
||||||
const gutterAllowance = 48; // padding + grid gap
|
|
||||||
|
|
||||||
return portraitViewportWidth + detailsColumnWidth + gutterAllowance;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useViewerLayoutMode = (ref, dependency) => {
|
export const useViewerLayoutMode = (ref, dependency) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef, useId } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState, useId } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import {
|
import {
|
||||||
ChevronIcon,
|
ChevronIcon,
|
||||||
@@ -24,6 +24,80 @@ import useFloatingMenu from '../ui/useFloatingMenu';
|
|||||||
import { getTagColorStyle } from '../utils/colors';
|
import { getTagColorStyle } from '../utils/colors';
|
||||||
import { useSidebarContext } from './SidebarContext';
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
const FolderNode = ({
|
const FolderNode = ({
|
||||||
node,
|
node,
|
||||||
depth,
|
depth,
|
||||||
@@ -204,8 +278,137 @@ const Sidebar = ({
|
|||||||
themeMode,
|
themeMode,
|
||||||
cycleThemeMode,
|
cycleThemeMode,
|
||||||
themeModes,
|
themeModes,
|
||||||
|
sidebarSuppressed,
|
||||||
} = useSidebarContext();
|
} = useSidebarContext();
|
||||||
const uploadInputRef = useRef(null);
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
setCollapsed(true);
|
||||||
}, [setCollapsed]);
|
}, [setCollapsed]);
|
||||||
@@ -547,8 +750,25 @@ const Sidebar = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const rootNode = folderNodes.get('root');
|
const rootNode = folderNodes.get('root');
|
||||||
|
const sidebarClassNames = ['sidebar'];
|
||||||
|
if (isResizingSidebar) {
|
||||||
|
sidebarClassNames.push('sidebar--resizing');
|
||||||
|
}
|
||||||
|
if (sidebarSuppressed) {
|
||||||
|
sidebarClassNames.push('sidebar--suppressed');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="sidebar">
|
<aside className={sidebarClassNames.join(' ')} ref={sidebarRef}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`sidebar__resize-handle${isResizingSidebar ? ' is-active' : ''}`}
|
||||||
|
aria-label="Resize sidebar"
|
||||||
|
onPointerDown={handleSidebarResizePointerDown}
|
||||||
|
onKeyDown={handleSidebarResizeKeyDown}
|
||||||
|
>
|
||||||
|
<span aria-hidden="true" />
|
||||||
|
</button>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
ref={uploadInputRef}
|
ref={uploadInputRef}
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ 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);
|
||||||
@@ -283,10 +284,20 @@ 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,
|
||||||
@@ -307,6 +318,8 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => {
|
|||||||
[
|
[
|
||||||
collapsed,
|
collapsed,
|
||||||
setCollapsed,
|
setCollapsed,
|
||||||
|
sidebarSuppressedState,
|
||||||
|
setSidebarSuppressed,
|
||||||
neutralHue,
|
neutralHue,
|
||||||
setNeutralHue,
|
setNeutralHue,
|
||||||
neutralChroma,
|
neutralChroma,
|
||||||
@@ -334,8 +347,11 @@ export const useSidebarContext = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useSidebarControls = () => {
|
export const useSidebarControls = () => {
|
||||||
const { setCollapsed } = useSidebarContext();
|
const { setCollapsed, setSidebarSuppressed } = useSidebarContext();
|
||||||
const openSidebar = useCallback(() => setCollapsed(false), [setCollapsed]);
|
const openSidebar = useCallback(() => {
|
||||||
|
setSidebarSuppressed(false);
|
||||||
|
setCollapsed(false);
|
||||||
|
}, [setCollapsed, setSidebarSuppressed]);
|
||||||
const closeSidebar = useCallback(() => setCollapsed(true), [setCollapsed]);
|
const closeSidebar = useCallback(() => setCollapsed(true), [setCollapsed]);
|
||||||
return { openSidebar, closeSidebar };
|
return { openSidebar, closeSidebar };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -101,6 +101,7 @@
|
|||||||
--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||||
|
|
||||||
--detail-panel-width: calc(100vw / 3);
|
--detail-panel-width: calc(100vw / 3);
|
||||||
|
--sidebar-width: 20em;
|
||||||
--documents-grid-title-size: 0.8rem;
|
--documents-grid-title-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,48 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
z-index: 1000000;
|
z-index: 1000000;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-panel--resizing {
|
||||||
|
cursor: col-resize;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-panel__resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -0.4rem;
|
||||||
|
width: 0.8rem;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: col-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-panel__resize-handle span {
|
||||||
|
width: 2px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: transparent;
|
||||||
|
transition: background 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-panel__resize-handle:hover span,
|
||||||
|
.detail-panel__resize-handle:focus-visible span,
|
||||||
|
.detail-panel__resize-handle.is-active span {
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-panel__resize-handle:focus-visible {
|
||||||
|
outline: 2px solid color-mix(in oklch, var(--accent) 60%, transparent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-header {
|
.panel-header {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
z-index: 2000000;
|
z-index: 950000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-floating__label {
|
.panel-floating__label {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.documents-main--sidebar-collapsed {
|
.documents-main--sidebar-hidden {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
box-shadow: -12px 0 24px -12px var(--shadow-faint);
|
box-shadow: -12px 0 24px -12px var(--shadow-faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
.documents-main:not(.documents-main--sidebar-collapsed) .main-content {
|
.documents-main:not(.documents-main--sidebar-hidden) .main-content {
|
||||||
border-left: 1px solid var(--border);
|
border-left: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.documents-main--sidebar-collapsed {
|
.documents-main--sidebar-hidden {
|
||||||
position: relative;
|
position: relative;
|
||||||
grid-template-columns: auto minmax(0, 1fr);
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,10 +128,53 @@
|
|||||||
color: var(--sidebar-fg);
|
color: var(--sidebar-fg);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 20em;
|
width: min(100vw, var(--sidebar-width));
|
||||||
max-width: 20em;
|
max-width: min(100vw, var(--sidebar-width));
|
||||||
flex: 0 0 20em;
|
flex: 0 0 var(--sidebar-width);
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar--resizing {
|
||||||
|
cursor: col-resize;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: -0.4rem;
|
||||||
|
width: 0.8rem;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: col-resize;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__resize-handle span {
|
||||||
|
width: 2px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: transparent;
|
||||||
|
transition: background 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__resize-handle:hover span,
|
||||||
|
.sidebar__resize-handle:focus-visible span,
|
||||||
|
.sidebar__resize-handle.is-active span {
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar__resize-handle:focus-visible {
|
||||||
|
outline: 2px solid color-mix(in oklch, var(--accent) 60%, transparent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar__body {
|
.sidebar__body {
|
||||||
|
|||||||
Reference in New Issue
Block a user