From abb59e46031796d62472e9b7e902a8fcceafb2f0 Mon Sep 17 00:00:00 2001
From: Rebeka Dekany <50901361+rebekadekany@users.noreply.github.com>
Date: Wed, 25 Sep 2024 15:46:02 +0200
Subject: [PATCH] Merge pull request #20298 from overleaf/rd-ide-filetree
[web] Migrate the file tree on the editor page to Bootstrap 5
GitOrigin-RevId: e2efec26242c8cdab37a54bc182b83bfb0f1eb3c
---
.../web/frontend/extracted-translations.json | 1 +
.../components/file-tree-context-menu.tsx | 74 ++-
.../file-tree-create/danger-message.jsx | 4 +-
.../file-tree-create-name-input.jsx | 79 ++-
.../file-tree-modal-create-file-body.jsx | 9 +-
.../file-tree-modal-create-file-footer.jsx | 29 +-
.../file-tree-modal-create-file-mode.jsx | 16 +-
.../file-tree-upload-conflicts.tsx | 22 +-
.../modes/file-tree-import-from-project.tsx | 94 ++-
.../modes/file-tree-import-from-url.jsx | 13 +-
.../file-tree/components/file-tree-doc.jsx | 42 +-
.../file-tree/components/file-tree-error.jsx | 6 +-
.../file-tree/components/file-tree-folder.jsx | 57 +-
.../file-tree-item/file-tree-item-inner.tsx | 2 +-
.../file-tree-item-menu-items.jsx | 102 ++-
.../file-tree-item/file-tree-item-menu.jsx | 15 +-
.../modals/file-tree-modal-create-file.jsx | 26 +-
.../modals/file-tree-modal-create-folder.jsx | 55 +-
.../modals/file-tree-modal-delete.jsx | 57 +-
.../modals/file-tree-modal-error.jsx | 30 +-
.../file-tree/util/icon-type-from-name.js | 10 +-
.../components/sidebar/tags-list.tsx | 2 +-
.../bootstrap-5/language-picker.tsx | 5 +-
.../components/types/dropdown-menu-props.ts | 3 +
.../js/shared/components/loading-spinner.tsx | 28 +-
.../js/shared/components/material-icon.tsx | 4 +-
.../stylesheets/app/editor/file-tree.less | 16 +-
.../stylesheets/app/editor/ide-react.less | 5 +-
.../bootstrap-5/abstracts/mixins.scss | 13 -
.../stylesheets/bootstrap-5/base/layout.scss | 4 +
.../bootstrap-5/components/button.scss | 1 +
.../bootstrap-5/components/dropdown-menu.scss | 2 +-
.../bootstrap-5/pages/editor/file-tree.scss | 603 ++++++++++++++++++
.../bootstrap-5/pages/editor/ide.scss | 141 +++-
.../bootstrap-5/pages/editor/outline.scss | 27 +-
services/web/locales/en.json | 1 +
.../file-tree-item-inner.spec.tsx | 2 +-
.../components/file-tree-root.spec.tsx | 17 +-
.../file-tree/flows/delete-entity.spec.tsx | 4 +-
.../file-tree/flows/rename-entity.spec.tsx | 2 +-
40 files changed, 1297 insertions(+), 326 deletions(-)
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 00c8639ed5..3c3d3b4e4f 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -968,6 +968,7 @@
"only_group_admin_or_managers_can_delete_your_account_5": "",
"only_importer_can_refresh": "",
"open_a_file_on_the_left": "",
+ "open_action_menu": "",
"open_advanced_reference_search": "",
"open_file": "",
"open_link": "",
diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-context-menu.tsx b/services/web/frontend/js/features/file-tree/components/file-tree-context-menu.tsx
index a0e9bcbd6f..7386abb218 100644
--- a/services/web/frontend/js/features/file-tree/components/file-tree-context-menu.tsx
+++ b/services/web/frontend/js/features/file-tree/components/file-tree-context-menu.tsx
@@ -1,10 +1,15 @@
import React, { useEffect, useRef } from 'react'
import ReactDOM from 'react-dom'
-import { Dropdown } from 'react-bootstrap'
+import { Dropdown as BS3Dropdown } from 'react-bootstrap'
+import {
+ Dropdown,
+ DropdownMenu,
+} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import { useFileTreeMainContext } from '../contexts/file-tree-main'
import FileTreeItemMenuItems from './file-tree-item/file-tree-item-menu-items'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
function FileTreeContextMenu() {
const { fileTreeReadOnly } = useFileTreeData()
@@ -24,10 +29,10 @@ function FileTreeContextMenu() {
// A11y - Move the focus to the context menu when it opens
function focusContextMenu() {
- const contextMenu = document.querySelector(
+ const BS3contextMenu = document.querySelector(
'[aria-labelledby="dropdown-file-tree-context-menu"]'
) as HTMLElement | null
- contextMenu?.focus()
+ BS3contextMenu?.focus()
}
function close() {
@@ -48,31 +53,58 @@ function FileTreeContextMenu() {
// A11y - Close the context menu when the user presses the Tab key
// Focus should move to the next element in the filetree
- function handleKeyDown(event: React.KeyboardEvent
| + |
|
{newFileCreateMode === 'doc' && (
+
)
diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.jsx
index a769f98756..93da129758 100644
--- a/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.jsx
+++ b/services/web/frontend/js/features/file-tree/components/file-tree-item/file-tree-item-menu-items.jsx
@@ -4,7 +4,12 @@ import * as eventTracking from '../../../../infrastructure/event-tracking'
import { useProjectContext } from '@/shared/context/project-context'
import { MenuItem } from 'react-bootstrap'
+import {
+ DropdownDivider,
+ DropdownItem,
+} from '@/features/ui/components/bootstrap-5/dropdown-menu'
import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
+import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
function FileTreeItemMenuItems() {
const { t } = useTranslation()
@@ -42,31 +47,82 @@ function FileTreeItemMenuItems() {
}, [startUploadingDocOrFile])
return (
- <>
- {canRename ? (
-
- ) : null}
- {downloadPath ? (
-
- ) : null}
- {canDelete ? (
-
- ) : null}
- {canCreate ? (
+
{conflicts.length > 0 && (
<>
@@ -118,7 +118,7 @@ export function FolderUploadConflicts({
}, [setError, conflicts, handleOverwrite, projectId])
return (
- {getConflictText(conflicts, t)} @@ -70,14 +70,14 @@ export function FileUploadConflicts({ )}- + {!hasFolderConflict && ( - + )}
+
)}
@@ -173,7 +179,7 @@ export default function FileTreeImportFromProject() {
/>
{error && {getConflictText(conflicts, t)}
- + {!hasFileConflict && ( - + )} {t('generic_something_went_wrong')} {t('please_refresh')} - +{t('sure_you_want_to_delete')}
- {t('generic_something_went_wrong')}
-
+
{errorMessage()}
-
-
+
+
+
+ {loadingText || t('loading')}…
+
+ }
+ />
)
}
diff --git a/services/web/frontend/js/shared/components/material-icon.tsx b/services/web/frontend/js/shared/components/material-icon.tsx
index c7acac7a14..414eac1a2b 100644
--- a/services/web/frontend/js/shared/components/material-icon.tsx
+++ b/services/web/frontend/js/shared/components/material-icon.tsx
@@ -5,15 +5,17 @@ import { bsVersion } from '@/features/utils/bootstrap-5'
type IconProps = React.ComponentProps<'i'> & {
type: string
accessibilityLabel?: string
+ modifier?: string
}
function MaterialIcon({
type,
className,
accessibilityLabel,
+ modifier,
...rest
}: IconProps) {
- const iconClassName = classNames('material-symbols', className)
+ const iconClassName = classNames('material-symbols', className, modifier)
return (
<>
diff --git a/services/web/frontend/stylesheets/app/editor/file-tree.less b/services/web/frontend/stylesheets/app/editor/file-tree.less
index 4f808b9e01..1d5d1a8b3c 100644
--- a/services/web/frontend/stylesheets/app/editor/file-tree.less
+++ b/services/web/frontend/stylesheets/app/editor/file-tree.less
@@ -422,7 +422,7 @@
}
}
}
-.modal-new-file--list {
+.modal-new-file-list {
background-color: @modal-footer-background-color;
width: 220px;
ul {
@@ -473,24 +473,24 @@
}
.file-tree-modal-alert {
- margin-top: 10px;
- margin-bottom: 0px;
+ margin-top: 12.5px;
}
.btn.modal-new-file-mode {
text-align: left;
+ width: 100%;
}
-.modal-new-file--body {
+.modal-new-file-body {
padding: 20px;
padding-top: (@line-height-computed / 4);
}
-.modal-new-file--body-upload {
+.modal-new-file-body-upload {
padding-top: 20px;
}
-.modal-new-file--body-conflict {
+.modal-new-file-body-conflict {
background-color: @red-10;
border: 1px dashed @red-50;
min-height: 400px;
@@ -512,11 +512,11 @@
}
}
-.modal-new-file--body-upload .uppy-Root {
+.modal-new-file-body-upload .uppy-Root {
font-family: inherit;
}
-.modal-new-file--body-upload .uppy-Dashboard {
+.modal-new-file-body-upload .uppy-Dashboard {
.uppy-Dashboard-inner {
border: none;
}
diff --git a/services/web/frontend/stylesheets/app/editor/ide-react.less b/services/web/frontend/stylesheets/app/editor/ide-react.less
index c2fd23a53b..f6bcfd626d 100644
--- a/services/web/frontend/stylesheets/app/editor/ide-react.less
+++ b/services/web/frontend/stylesheets/app/editor/ide-react.less
@@ -27,7 +27,6 @@
}
.ide-react-main {
- //migrated to SCSS
height: 100%;
display: flex;
flex-direction: column;
@@ -40,7 +39,6 @@
}
.ide-react-body {
- //migrated to SCSS
flex-grow: 1;
background-color: @pdf-bg;
overflow-y: hidden;
@@ -127,7 +125,6 @@
}
.ide-react-editor-sidebar {
- //migrated to SCSS
height: 100%;
background-color: @file-tree-bg;
color: var(--neutral-20);
@@ -140,7 +137,6 @@
}
.ide-react-file-tree-panel {
- //migrated to SCSS
display: flex;
flex-direction: column;
@@ -162,6 +158,7 @@
}
// Styles for placeholder elements that will eventually be replaced
+// Unused, not migrated to SCSS
.ide-react-placeholder-chat {
background-color: var(--editor-toolbar-bg);
color: var(--neutral-20);
diff --git a/services/web/frontend/stylesheets/bootstrap-5/abstracts/mixins.scss b/services/web/frontend/stylesheets/bootstrap-5/abstracts/mixins.scss
index 3a56cd202a..2110f6541a 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/abstracts/mixins.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/abstracts/mixins.scss
@@ -74,19 +74,6 @@
background-color: var(--bg-dark-secondary);
}
-// Filetree
-@mixin file-tree-item-color {
- color: var(--content-primary-dark);
-}
-
-@mixin file-tree-item-hover-bg {
- background-color: var(--bg-dark-secondary);
-}
-
-@mixin file-tree-bg {
- background-color: var(--bg-dark-tertiary);
-}
-
@mixin theme($name) {
@if index($themes, $name) {
[data-theme='#{$name}'] {
diff --git a/services/web/frontend/stylesheets/bootstrap-5/base/layout.scss b/services/web/frontend/stylesheets/bootstrap-5/base/layout.scss
index 268cb8cff4..a0756cdf8f 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/base/layout.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/base/layout.scss
@@ -54,3 +54,7 @@ hr {
.row-spaced-extra-large {
margin-top: calc(var(--line-height-03) * 4);
}
+
+.rotate-180 {
+ transform: rotate(180deg);
+}
diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/button.scss b/services/web/frontend/stylesheets/bootstrap-5/components/button.scss
index af7f274dd9..7f61bb9a0a 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/components/button.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/components/button.scss
@@ -104,6 +104,7 @@
text-decoration: underline;
font-size: inherit;
vertical-align: inherit;
+ border-radius: 0;
&,
&:active,
diff --git a/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss b/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss
index 1b37e6ed3a..f07165a1db 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/components/dropdown-menu.scss
@@ -14,7 +14,7 @@
min-width: 240px;
- &.sm {
+ &.dropdown-menu-sm-width {
min-width: 160px;
}
diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/file-tree.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/file-tree.scss
index 98b42bd95f..7a06a8c927 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/file-tree.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/file-tree.scss
@@ -1,3 +1,19 @@
+:root {
+ --file-tree-item-hover-bg: var(--bg-dark-secondary);
+ --file-tree-item-selected-bg: var(--bg-accent-01);
+ --file-tree-item-color: var(--content-primary-dark);
+ --file-tree-bg: var(--bg-dark-tertiary);
+ --file-tree-item-selected-color: var(--content-primary-dark);
+ --file-tree-line-height: 2.05;
+}
+
+@include theme('light') {
+ --file-tree-item-hover-bg: var(--bg-light-tertiary);
+ --file-tree-item-color: var(--content-secondary);
+ --file-tree-bg: var(--bg-light-primary);
+ --file-tree-item-selected-color: var(--bg-light-primary);
+}
+
.ide-react-file-tree-panel {
display: flex;
flex-direction: column;
@@ -7,3 +23,590 @@
width: 100%;
}
}
+
+.context-menu {
+ position: fixed;
+ z-index: 100;
+}
+
+.editor-sidebar {
+ background-color: var(--file-tree-bg);
+ display: flex;
+ flex-direction: column;
+}
+
+@mixin fake-full-width-bg($bg-color) {
+ &::before {
+ content: '\00a0';
+ position: absolute;
+ width: 9999px;
+ left: -9999px;
+ background-color: $bg-color;
+ }
+}
+
+.file-tree {
+ display: flex !important; // To work around jQuery layout's inline styles
+ flex-direction: column;
+ height: 100%;
+
+ .toolbar.toolbar-filetree {
+ @include toolbar-sm-height;
+ @include toolbar-alt-bg;
+
+ padding: 0 var(--spacing-03);
+ flex-shrink: 0;
+ }
+
+ > file-tree-root,
+ .file-tree-inner {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ overflow-y: auto;
+ width: inherit;
+ height: inherit;
+
+ &.no-toolbar {
+ top: 0;
+ }
+ }
+
+ // TODO; Consolidate with "Project files" in Overleaf
+ h3 {
+ font-size: 1rem;
+ border-bottom: 1px solid var(--border-primary);
+ padding-bottom: var(--spacing-02);
+ margin: var(--spacing-05);
+ }
+
+ &-history {
+ .entity-name {
+ padding-left: var(--spacing-03);
+
+ &.deleted {
+ text-decoration: line-through;
+ }
+ }
+
+ .loading {
+ padding-left: var(--spacing-03);
+ color: var(--content-primary-dark);
+
+ .material-symbols {
+ color: var(--content-primary-dark);
+ }
+ }
+ }
+
+ ul.file-tree-list {
+ margin: 0;
+ overflow: hidden auto;
+ height: 100%;
+ flex-grow: 1;
+ position: relative;
+
+ .entity > ul,
+ ul[role='tree'] {
+ margin-left: var(--spacing-08);
+ }
+
+ &::after {
+ content: '';
+ display: block;
+ min-height: var(--spacing-08);
+ }
+
+ li {
+ line-height: var(--file-tree-line-height);
+ position: relative;
+
+ &:focus {
+ outline: none;
+ }
+
+ .entity {
+ user-select: none;
+
+ &:focus {
+ outline: none;
+ }
+ }
+
+ .entity > .entity-name > button {
+ background-color: transparent;
+ border: 0;
+ padding: 0;
+
+ &:focus {
+ outline: none;
+ }
+
+ &.item-name-button {
+ color: inherit;
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-align: left;
+ padding-right: var(--spacing-09);
+ white-space: pre;
+ }
+ }
+
+ .entity-name {
+ color: var(--file-tree-item-color);
+ cursor: pointer;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &.entity-name-react {
+ text-overflow: clip;
+ }
+
+ &:focus {
+ outline: none;
+ }
+
+ background-color: transparent;
+
+ @include fake-full-width-bg(transparent);
+
+ &:hover {
+ background-color: var(--file-tree-item-hover-bg);
+
+ // When the entity is a subfolder, the DOM element is "indented" via margin-left. This makes the
+ // element not fill the entire file-tree width (as it's spaced from the left-hand side via margin)
+ // and, in consequence, the background gets clipped. The ::before pseudo-selector is used to fill
+ // the empty space.
+ @include fake-full-width-bg(var(--file-tree-item-hover-bg));
+ }
+
+ input {
+ line-height: 1.6;
+ }
+
+ .entity-menu-toggle > .material-symbols {
+ color: var(--content-primary-dark);
+ vertical-align: middle;
+ }
+ }
+
+ .material-symbols {
+ color: var(--content-disabled);
+
+ &.file-tree-icon {
+ margin-right: var(--spacing-02);
+ margin-left: var(--spacing-04);
+ vertical-align: sub;
+
+ &.linked-file-icon {
+ position: relative;
+ left: -2px;
+
+ + .linked-file-highlight {
+ color: inherit;
+ position: relative;
+ top: 4px;
+ width: 0;
+ left: -5px;
+ font-size: 12px;
+ }
+ }
+ }
+
+ &.file-tree-folder-icon {
+ margin-right: var(--spacing-02);
+ vertical-align: sub;
+ }
+
+ &.file-tree-expand-icon {
+ margin-left: var(--spacing-04);
+ vertical-align: sub;
+ }
+ }
+
+ .material-symbols.folder-open,
+ .material-symbols.fa-folder {
+ color: var(--content-disabled);
+ }
+
+ .material-symbols.toggle {
+ width: 24px;
+ padding: var(--spacing-03);
+ font-size: var(--font-size-03);
+ color: var(--content-disabled);
+ }
+
+ .file-tree-dropdown-toggle {
+ display: flex;
+ align-items: center;
+ color: var(--content-primary-dark);
+ line-height: 1.6;
+ font-size: var(--font-size-05);
+ padding: 0 var(--font-size-02) 0 var(--font-size-04);
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
+ }
+
+ &::before {
+ content: '\00B7\00B7\00B7';
+ transform: rotate(90deg);
+ letter-spacing: 0.5px;
+ }
+ }
+
+ &.multi-selected {
+ > .entity {
+ > .entity-name {
+ > div > .material-symbols,
+ > button > .material-symbols,
+ > .material-symbols,
+ .entity-menu-toggle .material-symbols {
+ color: var(--content-primary-dark);
+ }
+
+ > .material-symbols.linked-file-highlight {
+ color: var(--bg-info-01);
+ }
+
+ @include fake-full-width-bg(var(--bg-info-01));
+
+ color: var(--content-primary-dark);
+ font-weight: bold;
+ background-color: var(--bg-info-01);
+
+ &:hover {
+ background-color: var(--bg-info-02);
+
+ @include fake-full-width-bg(var(--bg-info-02));
+ }
+ }
+ }
+ }
+
+ .menu-button {
+ position: absolute;
+ right: 0;
+ top: 3px;
+ }
+
+ .rename-input {
+ display: block;
+ position: absolute;
+ top: 1px;
+ left: 32px;
+ right: 32px;
+ color: var(--content-primary);
+
+ input {
+ width: 100%;
+ }
+ }
+
+ > .entity > .entity-name {
+ .entity-menu-toggle {
+ display: none;
+ }
+ }
+
+ .entity-limit-hit {
+ line-height: var(--file-tree-line-height);
+ color: var(--file-tree-item-color);
+ margin-left: var(--spacing-05);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .entity-limit-hit-message {
+ font-style: italic;
+ }
+
+ .material-symbols .entity-limit-hit-tooltip-trigger {
+ margin-left: var(spacing-03);
+ cursor: pointer;
+ }
+ }
+ }
+
+ &:not(.multi-selected) {
+ ul.file-tree-list li.selected {
+ > .entity {
+ > .entity-name {
+ background-color: var(--file-tree-item-selected-bg);
+ color: var(--file-tree-item-selected-color);
+
+ > div > .material-symbols,
+ > button > .material-symbols,
+ > .material-symbols,
+ .entity-menu-toggle .material-symbols {
+ color: var(--file-tree-item-selected-color);
+ }
+
+ > .material-symbols.linked-file-highlight {
+ color: var(--bg-info-01);
+ }
+
+ @include fake-full-width-bg(var(--file-tree-item-selected-bg));
+
+ font-weight: bold;
+ padding-right: var(--spacing-09);
+
+ .entity-menu-toggle {
+ display: inline-block;
+ background-color: transparent;
+ box-shadow: none;
+ border: 0;
+ padding-right: var(--spacing-02);
+ padding-left: var(--spacing-02);
+ }
+ }
+ }
+ }
+ }
+
+ // while dragging, the previously selected item gets no highlight
+ ul.file-tree-list.file-tree-dragging li.selected .entity .entity-name {
+ @include fake-full-width-bg(transparent);
+
+ font-weight: normal;
+ background-color: transparent;
+ color: var(--file-tree-item-color);
+
+ .material-symbols {
+ color: var(--content-disabled) !important;
+ }
+ }
+
+ // the items being dragged get the full "hover" colour
+ ul.file-tree-list.file-tree-dragging
+ li
+ .entity.file-tree-entity-dragging
+ .entity-name {
+ background-color: fade(var(--file-tree-item-hover-bg), 90%);
+
+ @include fake-full-width-bg(fade(var(--file-tree-item-hover-bg), 90%));
+
+ color: var(--file-tree-item-color);
+
+ .material-symbols {
+ color: var(--content-disabled) !important;
+ }
+ }
+
+ // the drop target gets the "selected" colour
+ ul.file-tree-list.file-tree-dragging
+ li.dnd-droppable-hover
+ .entity
+ .entity-name {
+ background-color: var(--file-tree-item-selected-bg);
+
+ @include fake-full-width-bg(var(--file-tree-item-selected-bg));
+
+ color: var(--file-tree-item-selected-color);
+
+ .material-symbols {
+ color: var(--file-tree-item-selected-color) !important;
+ }
+ }
+
+ .dnd-draggable-preview-layer {
+ position: absolute;
+ pointer-events: none;
+ z-index: 100;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+
+ &.dnd-droppable-hover {
+ border: 3px solid var(--file-tree-item-selected-bg);
+ }
+ }
+
+ .dnd-draggable-preview-item {
+ color: var(--file-tree-item-selected-color);
+ background-color: fade(var(--file-tree-item-selected-bg), 60%);
+ width: 75%;
+ padding-left: var(--spacing-08);
+ line-height: 2.05;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .disconnected-overlay {
+ background-color: var(--file-tree-bg);
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 10;
+ opacity: 0.5;
+ cursor: wait;
+ }
+}
+
+.modal-new-file {
+ padding: 0;
+
+ table {
+ width: 100%;
+ table-layout: fixed;
+
+ td {
+ vertical-align: top;
+ }
+ }
+
+ .toggle-file-type-button {
+ font-size: 80%;
+ margin-top: calc(var(--spacing-05) * -1);
+
+ .btn {
+ display: inline-block;
+ padding: 0;
+ vertical-align: baseline;
+ font-size: inherit;
+ }
+
+ .btn:focus-within {
+ outline: none;
+ text-decoration: none;
+ }
+ }
+}
+
+.modal-new-file-list {
+ background-color: var(--bg-light-secondary);
+ width: 220px;
+
+ ul {
+ li {
+ /* old modal (a) */
+ a {
+ color: var(--content-secondary);
+ padding: var(--spacing-03);
+ display: block;
+ text-decoration: none;
+ }
+
+ /* new modal (button) */
+ .btn {
+ color: var(--content-secondary);
+ padding: var(--spacing-03);
+ }
+
+ .btn:hover {
+ text-decoration: none;
+ }
+
+ .btn:focus {
+ outline: none;
+ text-decoration: none;
+ background-color: white;
+ }
+ }
+
+ li.active {
+ background-color: white;
+
+ /* old modal (a) */
+ a {
+ color: var(--link-ui);
+ }
+
+ /* new modal (button) */
+ .btn {
+ color: var(--link-ui);
+ text-decoration: none;
+ }
+ }
+
+ li:hover {
+ background-color: white;
+ }
+ }
+}
+
+.file-tree-error {
+ text-align: center;
+ color: var(--content-secondary-dark);
+ padding: 20px;
+}
+
+.file-tree-modal-alert {
+ margin-top: var(--spacing-06);
+}
+
+.btn.modal-new-file-mode {
+ justify-content: left;
+ text-align: left;
+ text-decoration: none;
+ width: 100%;
+}
+
+.modal-new-file-body {
+ padding: 20px;
+ padding-top: var(--spacing-03);
+}
+
+.modal-new-file-body-upload {
+ padding-top: 20px;
+}
+
+.modal-new-file-body-conflict {
+ background-color: var(--bg-danger-03);
+ border: 1px dashed var(--border-danger);
+ min-height: 400px;
+ border-radius: 3px;
+ color: var(--content-primary);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: var(--spacing-05);
+}
+
+.modal-footer {
+ .approaching-file-limit {
+ font-weight: bold;
+ }
+
+ .at-file-limit {
+ text-align: left;
+ }
+}
+
+/* stylelint-disable selector-class-pattern */
+.modal-new-file-body-upload .uppy-Root {
+ font-family: inherit;
+}
+
+.modal-new-file-body-upload .uppy-Dashboard {
+ .uppy-Dashboard-inner {
+ border: none;
+ }
+
+ .uppy-Dashboard-dropFilesHereHint {
+ inset: 0;
+ }
+
+ .uppy-Dashboard-AddFiles {
+ margin: 0;
+ border: 1px dashed var(--border-primary);
+ height: 100%;
+
+ .uppy-Dashboard-AddFiles-title {
+ font-size: inherit;
+ }
+ }
+
+ .uppy-Dashboard-AddFiles-title {
+ width: 26em; // sized to create a wrap between the sentences
+ max-width: 100%;
+ }
+}
diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/ide.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/ide.scss
index be6c284cf6..16801b9292 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/ide.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/ide.scss
@@ -27,8 +27,7 @@
}
.ide-react-editor-sidebar {
- @include file-tree-bg;
-
+ background-color: var(--file-tree-bg);
height: 100%;
color: var(--content-secondary-dark);
}
@@ -51,3 +50,141 @@
color: var(--neutral-20);
}
}
+
+.ide-react-symbol-palette {
+ height: 100%;
+ background-color: var(--bg-dark-tertiary);
+ color: var(--neutral-20);
+}
+
+.ide-react-file-tree-panel {
+ display: flex;
+ flex-direction: column;
+
+ // Prevent the file tree expanding beyond the boundary of the panel
+ .file-tree {
+ width: 100%;
+ }
+}
+
+.ide-react-editor-panel {
+ display: flex;
+ flex-direction: column;
+}
+
+// Ensure an element with class "full-size", such as the binary file view, stays within the bounds of the panel
+.ide-react-panel {
+ position: relative;
+ container-type: size;
+}
+
+.ide-panel-group-resizing {
+ background-color: white;
+
+ // Hide panel contents while resizing
+ .ide-react-editor-content,
+ .pdf {
+ display: none !important;
+ }
+}
+
+.horizontal-resize-handle {
+ width: 7px !important;
+ height: 100%;
+
+ // Enable ::before and ::after pseudo-elements to position themselves correctly
+ position: relative;
+ background-color: var(--bg-dark-secondary);
+
+ .custom-toggler {
+ padding: 0;
+ border-width: 0;
+ }
+
+ &.horizontal-resize-handle-enabled {
+ &::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%;
+ }
+ }
+
+ &:not(.horizontal-resize-handle-enabled) {
+ cursor: default;
+ }
+
+ .synctex-controls {
+ left: -8px;
+ margin: 0;
+
+ // Ensure that SyncTex controls appear in front of PDF viewer controls and logs pane
+ z-index: 12;
+ }
+}
+
+.vertical-resize-handle {
+ height: 6px;
+ background-color: var(--bg-dark-secondary);
+
+ &.vertical-resize-handle-enabled {
+ &:hover {
+ background-color: var(--bg-dark-primary);
+ }
+ }
+
+ &:not(.vertical-resize-handle-enabled) {
+ opacity: 0.5;
+ cursor: default;
+ }
+
+ &::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;
+ }
+}
+
+.vertical-resizable-resizer {
+ background-color: var(--bg-dark-secondary);
+
+ &:hover {
+ background-color: var(--bg-dark-primary);
+ }
+
+ &::after {
+ @include heading-sm;
+
+ content: '\00b7\00b7\00b7\00b7';
+ display: block;
+ color: var(--content-disabled);
+ text-align: center;
+ pointer-events: none;
+ }
+}
+
+.vertical-resizable-resizer-disabled {
+ pointer-events: none;
+ opacity: 0.5;
+
+ &::after {
+ opacity: 0.5;
+ }
+}
diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/outline.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/outline.scss
index 4ea50107d0..b03e87b0de 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/outline.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/outline.scss
@@ -51,8 +51,7 @@
}
.outline-header-expand-collapse-btn {
- @include file-tree-item-color;
-
+ color: var(--file-tree-item-color);
display: flex;
align-items: center;
background-color: transparent;
@@ -81,8 +80,8 @@
.outline-header-name {
@include body-sm;
- @include file-tree-item-color;
+ color: var(--file-tree-item-color);
display: inline-block;
font-weight: 700;
margin: 0;
@@ -93,15 +92,13 @@
}
.outline-body {
- @include file-tree-bg;
-
+ background-color: var(--file-tree-bg);
overflow-y: auto;
padding-right: var(--spacing-03);
}
.outline-body-no-elements {
- @include file-tree-item-color;
-
+ color: var(--file-tree-item-color);
text-align: center;
padding: var(--spacing-08) var(--spacing-08) var(--spacing-11)
var(--spacing-08);
@@ -109,15 +106,13 @@
}
.outline-body-link {
- @include file-tree-item-color;
-
+ color: var(--file-tree-item-color);
display: block;
text-decoration: underline;
&:hover,
&:focus {
- @include file-tree-item-color;
-
+ color: var(--file-tree-item-color);
text-decoration: underline;
}
}
@@ -157,8 +152,7 @@
}
.outline-item-expand-collapse-btn {
- @include file-tree-bg;
-
+ background-color: var(--file-tree-bg);
display: inline;
border: 0;
padding: 0;
@@ -176,14 +170,14 @@
}
&:hover {
- @include file-tree-item-hover-bg;
+ background-color: var(--file-tree-item-hover-bg);
}
}
.outline-item-link {
- @include file-tree-item-color;
@include text-truncate;
+ color: var(--file-tree-item-color);
display: inline;
background-color: transparent;
border: 0;
@@ -196,8 +190,7 @@
&:hover,
&:focus {
- @include file-tree-item-hover-bg;
-
+ background-color: var(--file-tree-item-hover-bg);
outline: 0;
}
}
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 9a37accd9b..fb8a2aa340 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -1375,6 +1375,7 @@
"only_group_admin_or_managers_can_delete_your_account_5": "For more information, see the \"Managed Accounts\" section in our terms of use, which you agree to by clicking Accept invitation",
"only_importer_can_refresh": "Only the person who originally imported this __provider__ file can refresh it.",
"open_a_file_on_the_left": "Open a file on the left",
+ "open_action_menu": "Open __name__ action menu",
"open_advanced_reference_search": "Open advanced reference search",
"open_as_template": "Open as Template",
"open_file": "Edit file",
diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.spec.tsx b/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.spec.tsx
index 85a6708b57..411a8b1aff 100644
--- a/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.spec.tsx
+++ b/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.spec.tsx
@@ -116,7 +116,7 @@ describe(' |