import { PANEL_NAME_NAVIGATION, PANEL_NAME_NOTES } from '@/Constants/Constants' import { ElementIds } from '@/Constants/ElementIDs' import useIsTabletOrMobileScreen from '@/Hooks/useIsTabletOrMobileScreen' import { ErrorBoundary } from '@/Utils/ErrorBoundary' import { ApplicationEvent, classNames, PrefKey } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { useCallback, useEffect, useState } from 'react' import { usePrevious } from '../ContentListView/Calendar/usePrevious' import ContentListView from '../ContentListView/ContentListView' import NoteGroupView from '../NoteGroupView/NoteGroupView' import PanelResizer, { PanelResizeType, PanelSide, ResizeFinishCallback } from '../PanelResizer/PanelResizer' import { AppPaneId, AppPaneIdToDivId } from './AppPaneMetadata' import { useResponsiveAppPane } from './ResponsivePaneProvider' import Navigation from '../Tags/Navigation' import { useApplication } from '../ApplicationProvider' import { animatePaneEntranceTransitionFromOffscreenToTheRight, animatePaneExitTransitionOffscreenToTheRight, } from '@/Components/Panes/PaneAnimator' import { isPanesChangeLeafDismiss, isPanesChangePush } from '@/Controllers/PaneController/panesForLayout' import { log, LoggingDomain } from '@/Logging' const NAVIGATION_PANEL_MIN_WIDTH = 48 const ITEMS_PANEL_MIN_WIDTH = 200 const PLACEHOLDER_NAVIGATION_PANEL_WIDTH = 220 const PLACEHOLDER_NOTES_PANEL_WIDTH = 400 const PanesSystemComponent = () => { const application = useApplication() const isTabletOrMobileScreenWrapped = useIsTabletOrMobileScreen() const { isTabletOrMobile, isTablet, isMobile } = isTabletOrMobileScreenWrapped const previousIsTabletOrMobileWrapped = usePrevious(isTabletOrMobileScreenWrapped) const paneController = useResponsiveAppPane() const previousPaneController = usePrevious(paneController) const [renderPanes, setRenderPanes] = useState([]) const [panesPendingEntrance, setPanesPendingEntrance] = useState([]) const [panesPendingExit, setPanesPendingExit] = useState([]) const viewControllerManager = application.getViewControllerManager() const [navigationPanelWidth, setNavigationPanelWidth] = useState( application.getPreference(PrefKey.TagsPanelWidth, PLACEHOLDER_NAVIGATION_PANEL_WIDTH), ) const [navigationRef, setNavigationRef] = useState(null) const [itemsPanelWidth, setItemsPanelWidth] = useState( application.getPreference(PrefKey.NotesPanelWidth, PLACEHOLDER_NOTES_PANEL_WIDTH), ) const [listRef, setListRef] = useState(null) const showPanelResizers = !isTabletOrMobile const [_editorRef, setEditorRef] = useState(null) const animationsSupported = isMobile useEffect(() => { if (!animationsSupported) { return } const panes = paneController.panes const previousPanes = previousPaneController?.panes if (!previousPanes) { setPanesPendingEntrance([]) return } const isPush = isPanesChangePush(previousPanes, panes) if (isPush) { setPanesPendingEntrance([panes[panes.length - 1]]) } }, [paneController.panes, previousPaneController?.panes, animationsSupported]) useEffect(() => { if (!animationsSupported) { return } const panes = paneController.panes const previousPanes = previousPaneController?.panes if (!previousPanes) { setPanesPendingExit([]) return } const isExit = isPanesChangeLeafDismiss(previousPanes, panes) if (isExit) { setPanesPendingExit([previousPanes[previousPanes.length - 1]]) } }, [paneController.panes, previousPaneController?.panes, animationsSupported]) useEffect(() => { setRenderPanes(paneController.panes) }, [paneController.panes]) useEffect(() => { if (!panesPendingEntrance || panesPendingEntrance?.length === 0) { return } if (panesPendingEntrance.length > 1) { console.warn('More than one pane pending entrance. This is not supported.') return } void animatePaneEntranceTransitionFromOffscreenToTheRight(AppPaneIdToDivId[panesPendingEntrance[0]]).then(() => { setPanesPendingEntrance([]) }) }, [panesPendingEntrance]) useEffect(() => { if (!panesPendingExit || panesPendingExit?.length === 0) { return } if (panesPendingExit.length > 1) { console.warn('More than one pane pending exit. This is not supported.') return } void animatePaneExitTransitionOffscreenToTheRight(AppPaneIdToDivId[panesPendingExit[0]]).then(() => { setPanesPendingExit([]) }) }, [panesPendingExit]) useEffect(() => { const removeObserver = application.addEventObserver(async () => { const width = application.getPreference(PrefKey.TagsPanelWidth, PLACEHOLDER_NAVIGATION_PANEL_WIDTH) setNavigationPanelWidth(width) }, ApplicationEvent.PreferencesChanged) return () => { removeObserver() } }, [application]) const navigationPanelResizeWidthChangeCallback = useCallback((width: number) => { setNavigationPanelWidth(width) }, []) const itemsPanelResizeWidthChangeCallback = useCallback((width: number) => { setItemsPanelWidth(width) }, []) const handleInitialItemsListPanelWidthLoad = useCallback((width: number) => { setItemsPanelWidth(width) }, []) const navigationPanelResizeFinishCallback: ResizeFinishCallback = useCallback( (width, _lastLeft, _isMaxWidth, isCollapsed) => { application.publishPanelDidResizeEvent(PANEL_NAME_NAVIGATION, width, isCollapsed) }, [application], ) const itemsPanelResizeFinishCallback: ResizeFinishCallback = useCallback( (width, _lastLeft, _isMaxWidth, isCollapsed) => { application.publishPanelDidResizeEvent(PANEL_NAME_NOTES, width, isCollapsed) }, [application], ) useEffect(() => { if (isTablet && !previousIsTabletOrMobileWrapped?.isTablet) { if (paneController.selectedPane !== AppPaneId.Navigation) { paneController.removePane(AppPaneId.Navigation) } } else if ( !isTablet && previousIsTabletOrMobileWrapped?.isTablet && !paneController.panes.includes(AppPaneId.Navigation) ) { paneController.insertPaneAtIndex(AppPaneId.Navigation, 0) } }, [isTablet, paneController, previousIsTabletOrMobileWrapped]) const computeStylesForContainer = (): React.CSSProperties => { const panes = paneController.panes const numPanes = panes.length if (isMobile) { return {} } switch (numPanes) { case 1: { return { gridTemplateColumns: 'auto', } } case 2: { if (paneController.focusModeEnabled) { return { gridTemplateColumns: '0 1fr', } } if (isTablet) { return { gridTemplateColumns: '1fr 2fr', } } else { if (panes[0] === AppPaneId.Navigation) { return { gridTemplateColumns: `${navigationPanelWidth}px auto`, } } else { return { gridTemplateColumns: `${itemsPanelWidth}px auto`, } } } } case 3: { if (paneController.focusModeEnabled) { return { gridTemplateColumns: '0 0 1fr', } } return { gridTemplateColumns: `${navigationPanelWidth}px ${itemsPanelWidth}px 2fr`, } } default: return {} } } const computeClassesForPane = (_paneId: AppPaneId, isPendingEntrance: boolean, index: number): string => { const common = `app-pane app-pane-${index + 1} h-full content` if (isMobile) { return `absolute top-0 left-0 w-full flex flex-col ${common} ${ isPendingEntrance ? 'translate-x-[100%]' : 'translate-x-0 ' }` } else { return `flex flex-col relative overflow-hidden ${common}` } } const computeClassesForContainer = (): string => { if (isMobile) { return 'w-full' } return 'grid' } const renderPanesWithPendingExit = [...renderPanes, ...panesPendingExit] log(LoggingDomain.Panes, 'Rendering panes', renderPanesWithPendingExit) return (
{renderPanesWithPendingExit.map((pane, index) => { const isPendingEntrance = panesPendingEntrance?.includes(pane) const className = computeClassesForPane(pane, isPendingEntrance ?? false, index) if (pane === AppPaneId.Navigation) { return ( {showPanelResizers && navigationRef && ( )} ) } else if (pane === AppPaneId.Items) { return ( {showPanelResizers && listRef && ( )} ) } else if (pane === AppPaneId.Editor) { return ( setEditorRef(ref)} className={className} application={application} /> ) } })}
) } export default observer(PanesSystemComponent)