import { WelcomeScreen } from "$components/AppLayout/WelcomeScreen"; import { DocumentTabs } from "$components/DocumentTabs"; import { type EditorProps, EditorWithContainer } from "$components/Editor"; import { ImageDocumentView } from "$components/ImageDocumentView"; import { Preview, type PreviewProps } from "$components/Preview"; import { Sidebar } from "$components/Sidebar"; import { StatusBar, type StatusBarProps } from "$components/StatusBar"; import { Toolbar, type ToolbarProps } from "$components/Toolbar"; import { CHROME_SECTION, NO_MOTION_TRANSITION } from "$constants"; import type { StyleMatch } from "$editor/types"; import { useSkipAnimation } from "$hooks/useMotion"; import { useResizable } from "$hooks/useResizable"; import { useViewportTier } from "$hooks/useViewportTier"; import { EyeIcon } from "$icons"; import { useEditorPresentationActions, useLayoutChromeActions, useWorkspacePanelModeState, useWorkspacePanelSidebarState, useWorkspacePanelStatusBarCollapsed, useWorkspacePanelTopBarsCollapsed, } from "$state/selectors"; import { PanelMode } from "$types"; import { isImageContentType, isImagePath } from "$utils/documents"; import { cn } from "$utils/tw"; import { AnimatePresence, EasingDefinition, motion } from "motion/react"; import { type PointerEventHandler, useCallback, useEffect, useMemo } from "react"; import { DiagnosticsPanel } from "./DiagnosticsPanel"; type K = | "initialText" | "presentation" | "onChange" | "onSave" | "onCursorMove" | "onSelectionChange" | "onStyleMatchesChange" | "styleSelection" | "imageHandlers"; export type WorkspaceEditorProps = Pick; type PK = | "renderResult" | "theme" | "editorLine" | "previewStyle" | "renderedFontFamily" | "locationId" | "docRelPath" | "blobDid" | "onScrollToLine"; export type WorkspacePreviewProps = Pick; export type WorkspaceDiagnosticsProps = { isVisible: boolean; styleCheckEnabled: boolean; matches: StyleMatch[]; onSelectMatch: (match: StyleMatch) => void; onClose: () => void; onOpenSettings: () => void; }; export type WorkspaceWelcomeProps = { isVisible: boolean; hasLocations: boolean; locationCount: number; documentCount: number; onAddLocation: () => void; }; export type WorkspacePanelProps = { toolbar: Pick< ToolbarProps, | "saveStatus" | "atProtoSession" | "hasActiveDocument" | "onSave" | "onAtProtoAuth" | "onNewDocument" | "isNewDocumentDisabled" | "onExportPdf" | "isExportingPdf" | "isPdfExportDisabled" | "onInsertImage" | "onRefresh" >; onOpenWelcomeTab?: () => void; onOpenImportSheet?: () => void; onOpenGithubImportSheet?: () => void; onOpenStandardSiteImportSheet?: () => void; editor: WorkspaceEditorProps; preview: WorkspacePreviewProps; statusBar: StatusBarProps; diagnostics: WorkspaceDiagnosticsProps; welcome?: WorkspaceWelcomeProps; activeDocRelPath?: string; activeDocContentType?: string | null; }; const SPLIT_PANEL_MIN_WIDTH = 280; const SPLIT_PANEL_THRESHOLD = SPLIT_PANEL_MIN_WIDTH * 2 + 96; const SIDEBAR_MIN_WIDTH = 220; const SIDEBAR_MAX_WIDTH = 480; const FALLBACK_VIEWPORT_WIDTH = 1280; type PreviewMode = "reading" | "web"; function previewModeToStyle(mode: PreviewMode): WorkspacePreviewProps["previewStyle"] { return mode === "reading" ? "github" : "pdf"; } function previewStyleToMode(style: WorkspacePreviewProps["previewStyle"]): PreviewMode { return style === "pdf" ? "web" : "reading"; } function PreviewModeButton( { mode, isActive, onSelect }: { mode: PreviewMode; isActive: boolean; onSelect: (m: PreviewMode) => void }, ) { const handleClick = useCallback(() => onSelect(mode), [mode, onSelect]); return ( ); } function PreviewHeader( { previewStyle, onSelectMode }: { previewStyle: WorkspacePreviewProps["previewStyle"]; onSelectMode: (mode: PreviewMode) => void; }, ) { const activeMode = useMemo(() => previewStyleToMode(previewStyle), [previewStyle]); return (
Live Preview
); } type MainPanelProps = { panelMode: PanelMode; editor: WorkspaceEditorProps; preview: WorkspacePreviewProps; welcome?: WorkspaceWelcomeProps; onCreateNewDocument?: () => void; onOpenExistingDocument: () => void; onOpenImportSheet?: () => void; onOpenStandardSiteImportSheet?: () => void; onSelectPreviewMode: (mode: PreviewMode) => void; splitEditorWidth: number; isSplitResizing: boolean; onSplitResizeStart: PointerEventHandler; activeDocRelPath?: string; activeDocContentType?: string | null; }; function getPanelMode(isSplitView: boolean, isPreviewVisible: boolean): PanelMode { if (isSplitView && isPreviewVisible) { return "split"; } if (isPreviewVisible) { return "preview"; } return "editor"; } function MainPanel( { panelMode, editor, preview, welcome, onCreateNewDocument, onOpenExistingDocument, onOpenImportSheet, onOpenStandardSiteImportSheet, onSelectPreviewMode, splitEditorWidth, isSplitResizing, onSplitResizeStart, activeDocRelPath, activeDocContentType, }: MainPanelProps, ) { const isImageDocument = useMemo( () => isImageContentType(activeDocContentType) || (activeDocRelPath ? isImagePath(activeDocRelPath) : false), [activeDocContentType, activeDocRelPath], ); const container = useMemo(() => { if (panelMode === "split") { return { className: "flex min-h-0 min-w-0 shrink-0 flex-col border-r border-stroke-subtle/10", style: { width: `${splitEditorWidth}px` }, }; } return { className: "flex min-h-0 min-w-0 flex-col w-full" }; }, [panelMode, splitEditorWidth]); if (welcome?.isVisible) { return ( ); } if (isImageDocument) { return ; } if (panelMode === "split") { return (
); } return (
{panelMode === "editor" && } {panelMode === "preview" && (
)}
); } type SectionProps = { children: React.ReactNode; initial: { opacity: number; x?: number; y?: number; height?: number | string }; animate: { opacity: number; x?: number; y?: number; height?: number | string }; exit: { opacity: number; x?: number; y?: number; height?: number | string }; transition: { duration: number; ease?: EasingDefinition }; className: string; style?: React.CSSProperties; isVisible: boolean; }; function Section({ children, initial, animate, exit, transition, className, style, isVisible }: SectionProps) { return ( {isVisible && ( {children} )} ); } export function WorkspacePanel( { toolbar, onOpenWelcomeTab, onOpenImportSheet, onOpenGithubImportSheet, onOpenStandardSiteImportSheet, editor, preview, statusBar, diagnostics, welcome, activeDocRelPath, activeDocContentType, }: WorkspacePanelProps, ) { const skipAnimation = useSkipAnimation(); const { viewportWidth } = useViewportTier(FALLBACK_VIEWPORT_WIDTH); const { setMarkdownPreviewStyle } = useEditorPresentationActions(); const { setShowSearch } = useLayoutChromeActions(); const { sidebarCollapsed } = useWorkspacePanelSidebarState(); const { isSplitView, isPreviewVisible } = useWorkspacePanelModeState(); const topBarsCollapsed = useWorkspacePanelTopBarsCollapsed(); const statusBarCollapsed = useWorkspacePanelStatusBarCollapsed(); const panelMode = useMemo(() => getPanelMode(isSplitView, isPreviewVisible), [isSplitView, isPreviewVisible]); const sidebarMaxWidth = Math.max(SIDEBAR_MIN_WIDTH, Math.min(SIDEBAR_MAX_WIDTH, viewportWidth - 280)); const effectiveSidebarVisible = !sidebarCollapsed; const effectiveTabBarVisible = !topBarsCollapsed; const effectiveStatusBarVisible = !statusBarCollapsed; const { size: sidebarWidth, isResizing, startResizing, setSize: setSidebarWidth } = useResizable({ initialSize: 256, minSize: SIDEBAR_MIN_WIDTH, maxSize: sidebarMaxWidth, axis: "x", }); const splitAvailableWidth = Math.max( SPLIT_PANEL_MIN_WIDTH, viewportWidth - (effectiveSidebarVisible ? sidebarWidth : 0), ); const splitMaxWidth = Math.max(SPLIT_PANEL_MIN_WIDTH, splitAvailableWidth - SPLIT_PANEL_MIN_WIDTH); const splitFeasible = splitAvailableWidth >= SPLIT_PANEL_THRESHOLD; const effectivePanelMode = panelMode === "split" && !splitFeasible ? "editor" : panelMode; const { size: splitEditorWidth, isResizing: isSplitResizing, startResizing: startSplitResizing, setSize: setSplitEditorWidth, } = useResizable({ initialSize: Math.round(viewportWidth / 2), minSize: SPLIT_PANEL_MIN_WIDTH, maxSize: splitMaxWidth, axis: "x", }); useEffect(() => { if (sidebarWidth > sidebarMaxWidth) { setSidebarWidth(sidebarMaxWidth); } }, [sidebarWidth, sidebarMaxWidth, setSidebarWidth]); useEffect(() => { if (splitEditorWidth > splitMaxWidth) { setSplitEditorWidth(splitMaxWidth); return; } if (splitEditorWidth < SPLIT_PANEL_MIN_WIDTH) { setSplitEditorWidth(SPLIT_PANEL_MIN_WIDTH); } }, [splitEditorWidth, splitMaxWidth, setSplitEditorWidth]); const handleSidebarResizeStart: PointerEventHandler = useCallback((event) => { event.preventDefault(); startResizing(event.clientX); }, [startResizing]); const handleSplitResizeStart: PointerEventHandler = useCallback((event) => { event.preventDefault(); startSplitResizing(event.clientX); }, [startSplitResizing]); const handleSelectPreviewMode = useCallback((mode: PreviewMode) => { setMarkdownPreviewStyle(previewModeToStyle(mode)); }, [setMarkdownPreviewStyle]); const handleOpenExistingDocument = useCallback(() => { setShowSearch(true); }, [setShowSearch]); const sidebarStyle = useMemo(() => ({ width: `${sidebarWidth}px` }), [sidebarWidth]); const sectionTransition = useMemo(() => skipAnimation ? NO_MOTION_TRANSITION : CHROME_SECTION.TRANSITION, [ skipAnimation, ]); const newDocumentHandler = useMemo(() => toolbar.isNewDocumentDisabled ? void 0 : toolbar.onNewDocument, [ toolbar.isNewDocumentDisabled, toolbar.onNewDocument, ]); const sidebarMotionProps = useMemo( () => ({ initial: { opacity: 0, x: -16 }, animate: { opacity: 1, x: 0 }, exit: { opacity: 0, x: -12 }, transition: sectionTransition, }), [sectionTransition], ); const tabBarMotionProps = useMemo( () => ({ initial: { opacity: 0, y: -8, height: 0 }, animate: { opacity: 1, y: 0, height: "auto" }, exit: { opacity: 0, y: -8, height: 0 }, transition: sectionTransition, }), [sectionTransition], ); const statusBarMotionProps = useMemo( () => ({ initial: { opacity: 0, y: 8, height: 0 }, animate: { opacity: 1, y: 0, height: "auto" }, exit: { opacity: 0, y: 8, height: 0 }, transition: sectionTransition, }), [sectionTransition], ); return (
); }