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-02 10:35:02 +01:00 committed by Copybot
parent afcac7af86
commit ea1fc5f74e
27 changed files with 819 additions and 1 deletions

19
package-lock.json generated
View file

@ -39625,6 +39625,16 @@
"node": ">=0.10.0"
}
},
"node_modules/react-resizable-panels": {
"version": "0.0.55",
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-0.0.55.tgz",
"integrity": "sha512-J/LTFzUEjJiqwSjVh8gjUXkQDA8MRPjARASfn++d2+KOgA+9UcRYUfE3QBJixer2vkk+ffQ4cq3QzWzzHgqYpQ==",
"dev": true,
"peerDependencies": {
"react": "^16.14.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-resize-detector": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-7.1.2.tgz",
@ -48067,6 +48077,7 @@
"react-i18next": "^11.18.6",
"react-linkify": "^1.0.0-alpha",
"react-refresh": "^0.14.0",
"react-resizable-panels": "^0.0.55",
"react2angular": "^4.0.6",
"react2angular-shared-context": "^1.1.0",
"requirejs": "^2.3.6",
@ -56670,6 +56681,7 @@
"react-i18next": "^11.18.6",
"react-linkify": "^1.0.0-alpha",
"react-refresh": "^0.14.0",
"react-resizable-panels": "^0.0.55",
"react2angular": "^4.0.6",
"react2angular-shared-context": "^1.1.0",
"recurly": "^4.0.0",
@ -81001,6 +81013,13 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
"integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ=="
},
"react-resizable-panels": {
"version": "0.0.55",
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-0.0.55.tgz",
"integrity": "sha512-J/LTFzUEjJiqwSjVh8gjUXkQDA8MRPjARASfn++d2+KOgA+9UcRYUfE3QBJixer2vkk+ffQ4cq3QzWzzHgqYpQ==",
"dev": true,
"requires": {}
},
"react-resize-detector": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-7.1.2.tgz",

View file

@ -873,9 +873,14 @@ const ProjectController = {
!Features.hasFeature('saas') ||
req.query?.personal_access_token === 'true'
const idePageReact = req.query?.['ide-page'] === 'react'
const template =
detachRole === 'detached'
? 'project/editor_detached'
? // TODO: Create React version of detached page
'project/editor_detached'
: idePageReact
? 'project/ide-react'
: 'project/editor'
res.render(template, {

View file

@ -41,6 +41,7 @@ meta(name="ol-hasTrackChangesFeature", data-type="boolean" content=hasTrackChang
meta(name="ol-mathJax3Path" content=mathJax3Path)
meta(name="ol-completedTutorials", data-type="json" content=user.completedTutorials)
meta(name="ol-projectTags" data-type="json" content=projectTags)
meta(name="ol-idePageReact", data-type="boolean" content=idePageReact)
- var fileActionI18n = ['edited', 'renamed', 'created', 'deleted'].reduce((acc, i) => {acc[i] = translate('file_action_' + i); return acc}, {})
meta(name="ol-fileActionI18n" data-type="json" content=fileActionI18n)

View file

@ -0,0 +1,21 @@
extends ../layout
block vars
- var suppressNavbar = true
- var suppressFooter = true
- var suppressSkipToContent = true
- metadata.robotsNoindexNofollow = true
block entrypointVar
- entrypoint = 'pages/ide'
block content
main#ide-root
block append meta
include ./editor/meta
block prepend foot-scripts
each file in (useOpenTelemetry ? entrypointScripts("tracing") : [])
script(type="text/javascript", nonce=scriptNonce, src=file)
script(type="text/javascript", nonce=scriptNonce, src=(wsUrl || '/socket.io') + '/socket.io.js')

View file

@ -926,6 +926,7 @@
"resend_group_invite": "",
"resend_managed_user_invite": "",
"resending_confirmation_email": "",
"resize": "",
"resolve": "",
"resolved_comments": "",
"restore_file": "",
@ -1178,6 +1179,10 @@
"toolbar_table_insert_table_lowercase": "",
"toolbar_toggle_symbol_palette": "",
"toolbar_undo": "",
"tooltip_hide_filetree": "",
"tooltip_hide_pdf": "",
"tooltip_show_filetree": "",
"tooltip_show_pdf": "",
"total_per_month": "",
"total_per_year": "",
"total_with_subtotal_and_tax": "",

View file

@ -0,0 +1,22 @@
import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n'
import { GenericErrorBoundaryFallback } from '@/shared/components/generic-error-boundary-fallback'
import withErrorBoundary from '@/infrastructure/error-boundary'
import IdePage from '@/features/ide-react/components/layout/ide-page'
function IdeRoot() {
// Check that we haven't inadvertently loaded Angular
// TODO: Remove this before rolling out this component to any users
if (typeof window.angular !== 'undefined') {
throw new Error('Angular detected. This page must not load Angular.')
}
const { isReady } = useWaitForI18n()
if (!isReady) {
return null
}
return <IdePage />
}
export default withErrorBoundary(IdeRoot, GenericErrorBoundaryFallback)

View file

@ -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>
)
}

View file

@ -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}
/>
)
}

View file

@ -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>
)
}

View file

@ -0,0 +1,7 @@
import React from 'react'
export default function PlaceholderChat() {
return (
<aside className="chat ide-react-placeholder-chat">Chat placeholder</aside>
)
}

View file

@ -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>
)
}

View file

@ -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}
/>
)
}

View file

@ -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>
)
}

View file

@ -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>
)
}

View file

@ -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}
/>
)
}

View file

@ -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>
)
}

View file

@ -0,0 +1,19 @@
import { PanelResizeHandle } from 'react-resizable-panels'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { PanelResizeHandleProps } from 'react-resizable-panels/dist/declarations/src/PanelResizeHandle'
export const HorizontalResizeHandle: FC<PanelResizeHandleProps> = ({
children,
...props
}) => {
const { t } = useTranslation()
return (
<PanelResizeHandle {...props}>
<div className="horizontal-resize-handle" title={t('resize')}>
{children}
</div>
</PanelResizeHandle>
)
}

View file

@ -0,0 +1,48 @@
import classNames from 'classnames'
import Tooltip from '@/shared/components/tooltip'
type HorizontalTogglerType = 'west' | 'east'
type HorizontalTogglerProps = {
id: string
isOpen: boolean
setIsOpen: (isClosed: boolean) => void
togglerType: HorizontalTogglerType
tooltipWhenOpen: string
tooltipWhenClosed: string
}
export function HorizontalToggler({
id,
isOpen,
setIsOpen,
togglerType,
tooltipWhenOpen,
tooltipWhenClosed,
}: HorizontalTogglerProps) {
const description = isOpen ? tooltipWhenOpen : tooltipWhenClosed
return (
<Tooltip
id={id}
description={description}
overlayProps={{
placement: togglerType === 'east' ? 'left' : 'right',
}}
>
<button
className={classNames(
'custom-toggler',
`custom-toggler-${togglerType}`,
{
'custom-toggler-open': isOpen,
'custom-toggler-closed': !isOpen,
}
)}
aria-label={description}
title=""
onClick={() => setIsOpen(!isOpen)}
/>
</Tooltip>
)
}

View file

@ -0,0 +1,13 @@
import { PanelResizeHandle } from 'react-resizable-panels'
import { useTranslation } from 'react-i18next'
import { PanelResizeHandleProps } from 'react-resizable-panels/dist/declarations/src/PanelResizeHandle'
export function VerticalResizeHandle(props: PanelResizeHandleProps) {
const { t } = useTranslation()
return (
<PanelResizeHandle {...props}>
<div className="vertical-resize-handle" title={t('resize')} />
</PanelResizeHandle>
)
}

View file

@ -0,0 +1,19 @@
import { RefObject, useEffect } from 'react'
import { ImperativePanelHandle } from 'react-resizable-panels'
export default function useCollapsiblePanel(
panelIsOpen: boolean,
panelRef: RefObject<ImperativePanelHandle>
) {
useEffect(() => {
const panel = panelRef.current
if (!panel) {
return
}
if (panelIsOpen) {
panel.expand()
} else {
panel.collapse()
}
}, [panelIsOpen, panelRef])
}

View file

@ -0,0 +1,66 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { ImperativePanelHandle } from 'react-resizable-panels'
import { PanelGroupOnLayout } from 'react-resizable-panels/src/types'
export default function useFixedSizeColumn(
defaultSize: number,
isOpen: boolean
) {
const fixedPanelRef = useRef<ImperativePanelHandle>(null)
const fixedPanelWidthRef = useRef({ size: defaultSize, pixels: 0 })
const [initialLayoutDone, setInitialLayoutDone] = useState(false)
const measureFixedPanelSizePixels = useCallback(() => {
return fixedPanelRef.current?.getSize('pixels') || 0
}, [fixedPanelRef])
const handleLayout = useCallback(
sizes => {
// Measure the pixel width here because it's not always up to date in the
// panel's onResize
fixedPanelWidthRef.current = {
size: sizes[0],
pixels: measureFixedPanelSizePixels(),
}
setInitialLayoutDone(true)
},
[measureFixedPanelSizePixels]
) as PanelGroupOnLayout
useEffect(() => {
if (!isOpen) {
return
}
// Only start watching for resizes once the initial layout is done,
// otherwise we could measure the fixed column while it has zero width and
// collapse it
if (!initialLayoutDone || !fixedPanelRef.current) {
return
}
const fixedPanelElement = document.querySelector(
`[data-panel-id="${fixedPanelRef.current.getId()}"]`
)
const panelGroupElement = fixedPanelElement?.closest('[data-panel-group]')
if (!panelGroupElement || !fixedPanelElement) {
return
}
const resizeObserver = new ResizeObserver(() => {
fixedPanelRef.current?.resize(fixedPanelWidthRef.current.pixels, 'pixels')
})
resizeObserver.observe(panelGroupElement)
return () => resizeObserver.unobserve(panelGroupElement)
}, [fixedPanelRef, measureFixedPanelSizePixels, initialLayoutDone, isOpen])
return {
fixedPanelRef,
fixedPanelWidthRef,
handleLayout,
}
}

View file

@ -0,0 +1,13 @@
// Configure dynamically loaded assets (via webpack) to be downloaded from CDN
import '../utils/webpack-public-path'
// Set up error reporting, including Sentry
import '../infrastructure/error-reporter'
import ReactDOM from 'react-dom'
import IdeRoot from '../features/ide-react/components/ide-root'
const element = document.getElementById('ide-root')
if (element) {
ReactDOM.render(<IdeRoot />, element)
}

View file

@ -0,0 +1,27 @@
import LayoutWithPlaceholders from '@/features/ide-react/components/layout/layout-with-placeholders'
export default {
title: 'Editor / Page Layout',
component: LayoutWithPlaceholders,
decorators: [
(Story: any) => (
<div
style={{ position: 'absolute', inset: '1em', border: 'solid #ccc 1px' }}
>
<Story />
</div>
),
],
}
export const Persisted = {
args: {
shouldPersistLayout: true,
},
}
export const Unpersisted = {
args: {
shouldPersistLayout: false,
},
}

View file

@ -19,6 +19,7 @@
@import './editor/dictionary.less';
@import './editor/compile-button.less';
@import './editor/figure-modal.less';
@import './editor/ide-react.less';
@ui-layout-toggler-def-height: 50px;
@ui-resizer-size: 7px;

View file

@ -0,0 +1,90 @@
#ide-react-page {
height: 100vh;
}
.ide-react-main {
height: 100%;
display: flex;
flex-direction: column;
.toolbar.toolbar-header {
position: static;
flex-grow: 0;
color: var(--neutral-20);
}
}
.ide-react-body {
flex-grow: 1;
}
.horizontal-resize-handle {
width: @ui-resizer-size !important;
height: 100%;
// Enable ::before and ::after pseudo-elements to position themselves correctly
position: relative;
background-color: @editor-resizer-bg-color;
.custom-toggler {
padding: 0;
border-width: 0;
}
&::before,
&::after {
// This SVG has the colour hard-coded to the current value of @ol-blue-gray-2, so if we changed @ol-blue-gray-2,
// we'd have to change this SVG too
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='7' height='18' viewBox='0 0 7 18'%3E%3Cpath d='M2 0h3v3H2zM2 5h3v3H2zM2 10h3v3H2zM2 15h3v3H2z' style='fill:%239da7b7'/%3E%3C/svg%3E");
display: block;
position: absolute;
text-align: center;
left: 0;
width: 7px;
height: 18px;
}
&::before {
top: 25%;
}
&::after {
top: 75%;
}
}
.vertical-resize-handle {
height: 6px;
background-color: @vertical-resizable-resizer-bg;
&:hover {
background-color: @vertical-resizable-resizer-hover-bg;
}
&::after {
// This SVG has the colour hard-coded to the current value of @ol-blue-gray-2, so if we changed @ol-blue-gray-2,
// we'd have to change this SVG too
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='6' viewBox='0 0 18 6'%3E%3Cpath d='M0 1.5h3v3H0zM5 1.5h3v3H5zM10 1.5h3v3h-3zM15 1.5h3v3h-3z' style='fill:%239da7b7'/%3E%3C/svg%3E");
display: block;
text-align: center;
line-height: 0;
}
}
// Styles for placeholder elements that will eventually be replaced
.ide-react-placeholder-chat {
background-color: var(--editor-toolbar-bg);
color: var(--neutral-20);
height: 100%;
}
.ide-react-placeholder-editor-sidebar {
height: 100%;
background-color: @file-tree-bg;
color: var(--neutral-20);
}
.ide-react-placeholder-symbol-palette {
height: 100%;
background-color: @symbol-palette-bg;
color: var(--neutral-20);
}

View file

@ -1467,6 +1467,7 @@
"resending_confirmation_email": "Resending confirmation email",
"reset_password": "Reset Password",
"reset_your_password": "Reset your password",
"resize": "Resize",
"resolve": "Resolve",
"resolved_comments": "Resolved comments",
"restore": "Restore",

View file

@ -330,6 +330,7 @@
"react-refresh": "^0.14.0",
"react2angular": "^4.0.6",
"react2angular-shared-context": "^1.1.0",
"react-resizable-panels": "^0.0.55",
"requirejs": "^2.3.6",
"samlp": "^7.0.2",
"sandboxed-module": "overleaf/node-sandboxed-module#cafa2d60f17ce75cc023e6f296eb8de79d92d35d",