Просмотр исходного кода

Merge pull request #22787 from overleaf/mj-ide-rail

[web] Create rail tabbed layout

GitOrigin-RevId: be54a224087aad8e6e2762d9c26463e37aecd9aa
Mathias Jakobsen 1 год назад
Родитель
Сommit
849275c4b8

+ 2 - 14
services/web/frontend/js/features/ide-redesign/components/main-layout.tsx

@@ -3,15 +3,13 @@ import classNames from 'classnames'
 import { HorizontalResizeHandle } from '@/features/ide-react/components/resize/horizontal-resize-handle'
 import PdfPreview from '@/features/pdf-preview/components/pdf-preview'
 import { Editor } from './editor'
+import { RailLayout } from './rail'
 
 export default function MainLayout() {
   return (
     <div className="ide-redesign-main">
       <div className="ide-skeleton-block">Toolbar</div>
       <div className="ide-redesign-body">
-        <div className="ide-skeleton-block ide-redesign-full-height">
-          Left menu
-        </div>
         <PanelGroup
           autoSaveId="ide-redesign-outer-layout"
           direction="horizontal"
@@ -19,17 +17,7 @@ export default function MainLayout() {
             'ide-panel-group-resizing': false,
           })}
         >
-          <Panel
-            id="ide-redesign-sidebar-panel"
-            order={1}
-            defaultSize={15}
-            minSize={5}
-            maxSize={80}
-          >
-            <div className="ide-skeleton-block ide-redesign-full-height">
-              Side bar
-            </div>
-          </Panel>
+          <RailLayout />
           <HorizontalResizeHandle
             resizable
             hitAreaMargins={{ coarse: 0, fine: 0 }}

+ 108 - 0
services/web/frontend/js/features/ide-redesign/components/rail.tsx

@@ -0,0 +1,108 @@
+import { ReactElement, useCallback, useState } from 'react'
+import { Nav, NavLink, Tab, TabContainer } from 'react-bootstrap-5'
+import MaterialIcon, {
+  AvailableUnfilledIcon,
+} from '@/shared/components/material-icon'
+import { Panel } from 'react-resizable-panels'
+
+type RailElement = {
+  icon: AvailableUnfilledIcon
+  key: string
+  component: ReactElement
+}
+
+const RAIL_TABS: RailElement[] = [
+  // NOTE: The file tree **MUST** be the first (i.e. default) tab in the list
+  //       since the file tree is responsible for opening the initial document.
+  {
+    key: 'file-tree',
+    icon: 'description',
+    component: <>File tree</>,
+  },
+  {
+    key: 'integrations',
+    icon: 'integration_instructions',
+    component: <>Integrations</>,
+  },
+  {
+    key: 'review-panel',
+    icon: 'rate_review',
+    component: <>Review panel</>,
+  },
+  {
+    key: 'chat',
+    icon: 'forum',
+    component: <>Chat</>,
+  },
+  {
+    key: 'errors',
+    icon: 'report',
+    component: <>Errors</>,
+  },
+]
+
+export const RailLayout = () => {
+  const [selectedTab, setSelectedTab] = useState<string | undefined>(
+    RAIL_TABS[0]?.key
+  )
+  return (
+    <TabContainer
+      mountOnEnter // Only render when necessary (so that we can lazy load tab content)
+      unmountOnExit={false} // TODO: Should we unmount the tabs when they're not used?
+      transition={false}
+      defaultActiveKey={selectedTab}
+      onSelect={useCallback(key => setSelectedTab(key ?? undefined), [])}
+      id="ide-rail-tabs"
+    >
+      <div className="ide-rail">
+        <Nav defaultActiveKey={RAIL_TABS[0]?.key} className="flex-column">
+          {RAIL_TABS.map(({ icon, key }) => (
+            <RailTab
+              active={selectedTab === key}
+              key={key}
+              eventKey={key}
+              icon={icon}
+            />
+          ))}
+        </Nav>
+      </div>
+      <Panel
+        id="ide-redesign-sidebar-panel"
+        order={1}
+        defaultSize={15}
+        minSize={5}
+        maxSize={80}
+      >
+        <div className="ide-rail-content">
+          <Tab.Content>
+            {RAIL_TABS.map(({ key, component }) => (
+              <Tab.Pane eventKey={key} key={key}>
+                {component}
+              </Tab.Pane>
+            ))}
+          </Tab.Content>
+        </div>
+      </Panel>
+    </TabContainer>
+  )
+}
+
+const RailTab = ({
+  icon,
+  eventKey,
+  active,
+}: {
+  icon: AvailableUnfilledIcon
+  eventKey: string
+  active: boolean
+}) => {
+  return (
+    <NavLink eventKey={eventKey} className="ide-rail-tab-link">
+      <MaterialIcon
+        className="ide-rail-tab-link-icon"
+        type={icon}
+        unfilled={!active}
+      />
+    </NavLink>
+  )
+}

+ 1 - 0
services/web/frontend/stylesheets/bootstrap-5/pages/all.scss

@@ -7,6 +7,7 @@
 @import 'sidebar-v2-dash-pane';
 @import 'editor/ide';
 @import 'editor/ide-redesign';
+@import 'editor/rail';
 @import 'editor/toolbar';
 @import 'editor/online-users';
 @import 'editor/hotkeys';

+ 57 - 0
services/web/frontend/stylesheets/bootstrap-5/pages/editor/rail.scss

@@ -0,0 +1,57 @@
+:root {
+  --ide-rail-background: #fff;
+  --ide-rail-color: var(--neutral-90);
+  --ide-rail-link-background: #fff;
+  --ide-rail-link-active-background: var(--neutral-10);
+  --ide-rail-link-active-indicator-background: var(--neutral-90);
+}
+
+.ide-rail-tab-link {
+  border-radius: 12px;
+  display: block;
+  height: 48px;
+  width: 48px;
+  text-align: center;
+  padding: 0;
+  color: var(--ide-rail-color);
+  background-color: var(--ide-rail-link-background);
+  position: relative;
+  overflow-y: hidden;
+
+  .ide-rail-tab-link-icon {
+    line-height: 48px;
+    font-size: 20px;
+  }
+
+  &.active {
+    color: var(--ide-rail-color);
+    background-color: var(--ide-rail-link-active-background);
+
+    &::after {
+      $indicator-height: 3px;
+
+      border-radius: 12px;
+      content: '';
+      position: absolute;
+      bottom: -$indicator-height;
+      left: 25%;
+      box-sizing: border-box;
+      width: 50%;
+      height: $indicator-height * 2;
+      border: $indicator-height solid
+        var(--ide-rail-link-active-indicator-background);
+      background-color: var(--ide-rail-link-active-indicator-background);
+    }
+  }
+}
+
+.ide-rail {
+  height: 100%;
+  padding: var(--spacing-02);
+  background: var(--ide-rail-background);
+}
+
+.ide-rail-content {
+  height: 100%;
+  border: 1px solid var(--border-divider);
+}