React IDE page shell (#14988)

* React IDE page shell

* Set the maximum height of the symbol palette to 336px

* Tidy export

* Remove unnecessary destructuring

* Update comment

* Optimize toggle

Co-authored-by: Alf Eaton <alf.eaton@overleaf.com>

* Change snap-to-collapse threshold to 5%

* Synchronize left column width between history and editor views and remove duplication in ide-page

* Replace resizer dots with SVG

* Rermove unnecessary import and comment the remaining ones

* Use block prepend to avoid duplication

* Improve vertical content divider styling

* Implement fixed width during container resize on left column

* Change IDE page file extension

* Refactor fixed-size panel into a hook and use for chat panel

---------

Co-authored-by: Alf Eaton <alf.eaton@overleaf.com>
GitOrigin-RevId: aa881e48a2838a192b6f8f9e16e561f5cd706bd3
This commit is contained in:
Tim Down
2023-10-03 08:04:04 +00:00
committed by Copybot
co-authored by Alf Eaton
parent afcac7af86
commit ea1fc5f74e
27 changed files with 819 additions and 1 deletions
@@ -0,0 +1,11 @@
import LayoutWithPlaceholders from '@/features/ide-react/components/layout/layout-with-placeholders'
// This is filled with placeholder content while the real content is migrated
// away from Angular
export default function IdePage() {
return (
<div id="ide-react-page">
<LayoutWithPlaceholders shouldPersistLayout />
</div>
)
}
@@ -0,0 +1,49 @@
import { useState } from 'react'
import PlaceholderHeader from '@/features/ide-react/components/layout/placeholder/placeholder-header'
import PlaceholderChat from '@/features/ide-react/components/layout/placeholder/placeholder-chat'
import PlaceholderHistory from '@/features/ide-react/components/layout/placeholder/placeholder-history'
import PlaceholderEditorMainContent from '@/features/ide-react/components/layout/placeholder/placeholder-editor-main-content'
import MainLayout from '@/features/ide-react/components/layout/main-layout'
export default function LayoutWithPlaceholders({
shouldPersistLayout,
}: {
shouldPersistLayout: boolean
}) {
const [chatIsOpen, setChatIsOpen] = useState(false)
const [historyIsOpen, setHistoryIsOpen] = useState(false)
const [leftColumnDefaultSize, setLeftColumnDefaultSize] = useState(20)
const headerContent = (
<PlaceholderHeader
chatIsOpen={chatIsOpen}
setChatIsOpen={setChatIsOpen}
historyIsOpen={historyIsOpen}
setHistoryIsOpen={setHistoryIsOpen}
/>
)
const chatContent = <PlaceholderChat />
const mainContent = historyIsOpen ? (
<PlaceholderHistory
shouldPersistLayout
leftColumnDefaultSize={leftColumnDefaultSize}
setLeftColumnDefaultSize={setLeftColumnDefaultSize}
/>
) : (
<PlaceholderEditorMainContent
shouldPersistLayout
leftColumnDefaultSize={leftColumnDefaultSize}
setLeftColumnDefaultSize={setLeftColumnDefaultSize}
/>
)
return (
<MainLayout
headerContent={headerContent}
chatContent={chatContent}
mainContent={mainContent}
chatIsOpen={chatIsOpen}
shouldPersistLayout={shouldPersistLayout}
/>
)
}
@@ -0,0 +1,66 @@
import { Panel, PanelGroup } from 'react-resizable-panels'
import { ReactNode } from 'react'
import { HorizontalResizeHandle } from '../resize/horizontal-resize-handle'
import useFixedSizeColumn from '@/features/ide-react/hooks/use-fixed-size-column'
import useCollapsiblePanel from '@/features/ide-react/hooks/use-collapsible-panel'
const CHAT_DEFAULT_SIZE = 20
type PageProps = {
headerContent: ReactNode
chatContent: ReactNode
mainContent: ReactNode
chatIsOpen: boolean
shouldPersistLayout: boolean
}
// The main area below the header is split into two: the main content and chat.
// The reason for not splitting the left column containing the file tree and
// outline here is that the history view has its own file tree, so it is more
// convenient to replace the whole of the main content when in history view.
export default function MainLayout({
headerContent,
chatContent,
mainContent,
chatIsOpen,
shouldPersistLayout,
}: PageProps) {
const { fixedPanelRef: chatPanelRef, handleLayout } = useFixedSizeColumn(
CHAT_DEFAULT_SIZE,
chatIsOpen
)
useCollapsiblePanel(chatIsOpen, chatPanelRef)
return (
<div className="ide-react-main">
{headerContent}
<div className="ide-react-body">
<PanelGroup
autoSaveId={shouldPersistLayout ? 'ide-react-chat-layout' : undefined}
direction="horizontal"
onLayout={handleLayout}
>
<Panel id="main" order={1}>
{mainContent}
</Panel>
{chatIsOpen ? (
<>
<HorizontalResizeHandle />
<Panel
ref={chatPanelRef}
id="chat"
order={2}
defaultSize={CHAT_DEFAULT_SIZE}
minSize={5}
collapsible
>
{chatContent}
</Panel>
</>
) : null}
</PanelGroup>
</div>
</div>
)
}
@@ -0,0 +1,7 @@
import React from 'react'
export default function PlaceholderChat() {
return (
<aside className="chat ide-react-placeholder-chat">Chat placeholder</aside>
)
}
@@ -0,0 +1,90 @@
import React, { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
ImperativePanelHandle,
Panel,
PanelGroup,
} from 'react-resizable-panels'
import { HorizontalResizeHandle } from '@/features/ide-react/components/resize/horizontal-resize-handle'
import { HorizontalToggler } from '@/features/ide-react/components/resize/horizontal-toggler'
import { VerticalResizeHandle } from '@/features/ide-react/components/resize/vertical-resize-handle'
import useCollapsiblePanel from '@/features/ide-react/hooks/use-collapsible-panel'
type PlaceholderEditorAndPdfProps = {
shouldPersistLayout?: boolean
}
export default function PlaceholderEditorAndPdf({
shouldPersistLayout = false,
}: PlaceholderEditorAndPdfProps) {
const { t } = useTranslation()
const [pdfIsOpen, setPdfIsOpen] = useState(false)
const [symbolPaletteIsOpen, setSymbolPaletteIsOpen] = useState(false)
const pdfPanelRef = useRef<ImperativePanelHandle>(null)
useCollapsiblePanel(pdfIsOpen, pdfPanelRef)
return (
<PanelGroup
autoSaveId={
shouldPersistLayout ? 'ide-react-editor-and-pdf-layout' : undefined
}
direction="horizontal"
>
<Panel defaultSize={50}>
<PanelGroup
autoSaveId={
shouldPersistLayout
? 'ide-react-editor-and-symbol-palette-layout'
: undefined
}
direction="vertical"
units="pixels"
>
<Panel id="editor" order={1}>
Editor placeholder
<br />
<button onClick={() => setSymbolPaletteIsOpen(value => !value)}>
Toggle symbol palette
</button>
</Panel>
{symbolPaletteIsOpen ? (
<>
<VerticalResizeHandle id="editor-symbol-palette" />
<Panel
id="symbol-palette"
order={2}
defaultSize={250}
minSize={250}
maxSize={336}
>
<div className="ide-react-placeholder-symbol-palette ">
Symbol palette placeholder
</div>
</Panel>
</>
) : null}
</PanelGroup>
</Panel>
<HorizontalResizeHandle>
<HorizontalToggler
id="editor-pdf"
togglerType="east"
isOpen={pdfIsOpen}
setIsOpen={pdfIsOpen => setPdfIsOpen(pdfIsOpen)}
tooltipWhenOpen={t('tooltip_hide_pdf')}
tooltipWhenClosed={t('tooltip_show_pdf')}
/>
</HorizontalResizeHandle>
<Panel
ref={pdfPanelRef}
defaultSize={50}
minSize={5}
collapsible
onCollapse={collapsed => setPdfIsOpen(!collapsed)}
>
PDF placeholder
</Panel>
</PanelGroup>
)
}
@@ -0,0 +1,38 @@
import React, { useState } from 'react'
import TwoColumnMainContent from '@/features/ide-react/components/layout/two-column-main-content'
import PlaceholderEditorAndPdf from '@/features/ide-react/components/layout/placeholder/placeholder-editor-and-pdf'
import PlaceholderEditorSidebar from '@/features/ide-react/components/layout/placeholder/placeholder-editor-sidebar'
type PlaceholderEditorMainContentProps = {
shouldPersistLayout: boolean
leftColumnDefaultSize: number
setLeftColumnDefaultSize: React.Dispatch<React.SetStateAction<number>>
}
export default function PlaceholderEditorMainContent({
shouldPersistLayout,
leftColumnDefaultSize,
setLeftColumnDefaultSize,
}: PlaceholderEditorMainContentProps) {
const [leftColumnIsOpen, setLeftColumnIsOpen] = useState(true)
const leftColumnContent = (
<PlaceholderEditorSidebar shouldPersistLayout={shouldPersistLayout} />
)
const rightColumnContent = (
<PlaceholderEditorAndPdf shouldPersistLayout={shouldPersistLayout} />
)
return (
<TwoColumnMainContent
leftColumnId="editor-left-column"
leftColumnContent={leftColumnContent}
leftColumnDefaultSize={leftColumnDefaultSize}
setLeftColumnDefaultSize={setLeftColumnDefaultSize}
rightColumnContent={rightColumnContent}
leftColumnIsOpen={leftColumnIsOpen}
setLeftColumnIsOpen={setLeftColumnIsOpen}
shouldPersistLayout={shouldPersistLayout}
/>
)
}
@@ -0,0 +1,26 @@
import React from 'react'
import { Panel, PanelGroup } from 'react-resizable-panels'
import { VerticalResizeHandle } from '@/features/ide-react/components/resize/vertical-resize-handle'
type PlaceholderHeaderProps = {
shouldPersistLayout: boolean
}
export default function PlaceholderEditorSidebar({
shouldPersistLayout,
}: PlaceholderHeaderProps) {
return (
<aside className="ide-react-placeholder-editor-sidebar">
<PanelGroup
autoSaveId={
shouldPersistLayout ? 'ide-react-editor-sidebar-layout' : undefined
}
direction="vertical"
>
<Panel defaultSize={75}>File tree placeholder</Panel>
<VerticalResizeHandle />
<Panel defaultSize={25}>File outline placeholder</Panel>
</PanelGroup>
</aside>
)
}
@@ -0,0 +1,42 @@
import React from 'react'
import ChatToggleButton from '@/features/editor-navigation-toolbar/components/chat-toggle-button'
import HistoryToggleButton from '@/features/editor-navigation-toolbar/components/history-toggle-button'
type PlaceholderHeaderProps = {
chatIsOpen: boolean
setChatIsOpen: (chatIsOpen: boolean) => void
historyIsOpen: boolean
setHistoryIsOpen: (chatIsOpen: boolean) => void
}
export default function PlaceholderHeader({
chatIsOpen,
setChatIsOpen,
historyIsOpen,
setHistoryIsOpen,
}: PlaceholderHeaderProps) {
function toggleChatOpen() {
setChatIsOpen(!chatIsOpen)
}
function toggleHistoryOpen() {
setHistoryIsOpen(!historyIsOpen)
}
return (
<header className="toolbar toolbar-header">
<div className="toolbar-left">Header placeholder</div>
<div className="toolbar-right">
<HistoryToggleButton
historyIsOpen={historyIsOpen}
onClick={toggleHistoryOpen}
/>
<ChatToggleButton
chatIsOpen={chatIsOpen}
onClick={toggleChatOpen}
unreadMessageCount={0}
/>
</div>
</header>
)
}
@@ -0,0 +1,38 @@
import React, { useState } from 'react'
import TwoColumnMainContent from '@/features/ide-react/components/layout/two-column-main-content'
type PlaceholderHistoryProps = {
shouldPersistLayout: boolean
leftColumnDefaultSize: number
setLeftColumnDefaultSize: React.Dispatch<React.SetStateAction<number>>
}
export default function PlaceholderHistory({
shouldPersistLayout,
leftColumnDefaultSize,
setLeftColumnDefaultSize,
}: PlaceholderHistoryProps) {
const [leftColumnIsOpen, setLeftColumnIsOpen] = useState(true)
const leftColumnContent = (
<aside className="ide-react-placeholder-editor-sidebar">
History file tree placeholder
</aside>
)
const rightColumnContent = (
<div>History document diff viewer and versions list placeholder</div>
)
return (
<TwoColumnMainContent
leftColumnId="editor-left-column"
leftColumnContent={leftColumnContent}
leftColumnDefaultSize={leftColumnDefaultSize}
setLeftColumnDefaultSize={setLeftColumnDefaultSize}
rightColumnContent={rightColumnContent}
leftColumnIsOpen={leftColumnIsOpen}
setLeftColumnIsOpen={setLeftColumnIsOpen}
shouldPersistLayout={shouldPersistLayout}
/>
)
}
@@ -0,0 +1,80 @@
import React, { ReactNode, useEffect } from 'react'
import { Panel, PanelGroup } from 'react-resizable-panels'
import { HorizontalResizeHandle } from '@/features/ide-react/components/resize/horizontal-resize-handle'
import { useTranslation } from 'react-i18next'
import { HorizontalToggler } from '@/features/ide-react/components/resize/horizontal-toggler'
import useFixedSizeColumn from '@/features/ide-react/hooks/use-fixed-size-column'
import useCollapsiblePanel from '@/features/ide-react/hooks/use-collapsible-panel'
type TwoColumnMainContentProps = {
leftColumnId: string
leftColumnContent: ReactNode
leftColumnDefaultSize: number
setLeftColumnDefaultSize: React.Dispatch<React.SetStateAction<number>>
rightColumnContent: ReactNode
leftColumnIsOpen: boolean
setLeftColumnIsOpen: (
leftColumnIsOpen: TwoColumnMainContentProps['leftColumnIsOpen']
) => void
shouldPersistLayout?: boolean
}
export default function TwoColumnMainContent({
leftColumnId,
leftColumnContent,
leftColumnDefaultSize,
setLeftColumnDefaultSize,
rightColumnContent,
leftColumnIsOpen,
setLeftColumnIsOpen,
shouldPersistLayout = false,
}: TwoColumnMainContentProps) {
const { t } = useTranslation()
const {
fixedPanelRef: leftColumnPanelRef,
fixedPanelWidthRef: leftColumnWidthRef,
handleLayout,
} = useFixedSizeColumn(leftColumnDefaultSize, leftColumnIsOpen)
useCollapsiblePanel(leftColumnIsOpen, leftColumnPanelRef)
// Update the left column default size on unmount rather than doing it on
// every resize, which causes ResizeObserver errors
useEffect(() => {
if (leftColumnWidthRef.current) {
setLeftColumnDefaultSize(leftColumnWidthRef.current.size)
}
}, [leftColumnWidthRef, setLeftColumnDefaultSize])
return (
<PanelGroup
autoSaveId={
shouldPersistLayout ? 'ide-react-main-content-layout' : undefined
}
direction="horizontal"
onLayout={handleLayout}
>
<Panel
ref={leftColumnPanelRef}
defaultSize={leftColumnDefaultSize}
minSize={5}
collapsible
onCollapse={collapsed => setLeftColumnIsOpen(!collapsed)}
>
{leftColumnIsOpen ? leftColumnContent : null}
</Panel>
<HorizontalResizeHandle>
<HorizontalToggler
id={leftColumnId}
togglerType="west"
isOpen={leftColumnIsOpen}
setIsOpen={isOpen => setLeftColumnIsOpen(isOpen)}
tooltipWhenOpen={t('tooltip_hide_filetree')}
tooltipWhenClosed={t('tooltip_show_filetree')}
/>
</HorizontalResizeHandle>
<Panel>{rightColumnContent}</Panel>
</PanelGroup>
)
}