diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index 378157818c..d9ba1b195d 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -4239,10 +4239,10 @@ dropHint = "Open files to get started" dropToAdd = "Drop files to add" duplicateFailed = "Could not duplicate file" expand = "Expand sidebar" +folders = "Folders" googleDrive = "Google Drive" googleDriveDisabled = "Google Drive is not configured" -leaveMyFiles = "Leave File library" -library = "PDF Library" +library = "Files" myFiles = "File library" noFiles = "No files yet" openFileManager = "Browse all files & folders" @@ -4351,7 +4351,6 @@ removeVersion = "Remove this version" rename = "Rename" renamed = "Renamed" renameFolder = "Rename folder" -resizeFolderTree = "Resize folder tree (arrow keys, Shift for bigger steps; double-click to auto-fit)" save = "Save" saveToServer = "Save to server" saveToServerDisabledHint = "Saving to the server isn't enabled on this server. Ask your admin to enable it." diff --git a/frontend/editor/src/core/components/filesPage/FolderTreePanel.css b/frontend/editor/src/core/components/filesPage/FolderTreePanel.css deleted file mode 100644 index 4a98babcbc..0000000000 --- a/frontend/editor/src/core/components/filesPage/FolderTreePanel.css +++ /dev/null @@ -1,190 +0,0 @@ -/* Secondary navigator panel that slides out from the main FileSidebar - when the user enters the My Files workbench. - - Pattern: - - Outer panel is a flex item whose `width` transitions. The inner - content keeps a fixed natural width and the outer `overflow: hidden` - clips it during the slide so the user sees the panel grow from the - edge rather than the content shrinking. - - Inner content separately fades + nudges in for an attentive feel. - - Styled to mirror the main FileSidebar - same toolbar background, same - section-header treatment, same icon weight - so the two read as one - unified surface. */ - -.folder-tree-panel { - width: 0; - flex-shrink: 0; - background: var(--c-bg-raised); - border-right: 0 solid var(--c-border-subtle); - overflow: hidden; - height: 100%; - pointer-events: none; - position: relative; -} - -.folder-tree-panel[data-active="true"] { - width: var(--folder-tree-panel-width, 16rem); - border-right-width: 1px; - pointer-events: auto; -} - -.folder-tree-panel-inner { - display: flex; - flex-direction: column; - height: 100%; - width: 100%; - overflow-y: auto; - opacity: 0; - transform: translateX(-1rem); - /* Only fade/slide; width is driven by the inline custom property so - * dragging the resizer doesn't animate. */ - transition: - opacity 0.18s ease, - transform 0.26s cubic-bezier(0.22, 0.61, 0.36, 1); -} - -.folder-tree-panel[data-active="true"] .folder-tree-panel-inner { - opacity: 1; - transform: translateX(0); - transition-delay: 0.04s; -} - -/* Drag handle on the right edge. */ -.folder-tree-panel-resizer { - position: absolute; - top: 0; - right: -3px; - width: 6px; - height: 100%; - cursor: col-resize; - z-index: 2; - background: transparent; - transition: background-color 0.15s ease; -} - -.folder-tree-panel-resizer:hover, -.folder-tree-panel-resizer:focus-visible { - background: color-mix(in srgb, var(--c-primary) 35%, transparent); - outline: none; -} - -/* Section header - matches .file-sidebar-section-header in FileSidebar.css */ -.folder-tree-panel-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 18px 6px 18px; - margin-top: 4px; - flex-shrink: 0; -} - -.folder-tree-panel-title { - font-size: 13px; - font-weight: 600; - letter-spacing: 0.02em; - color: var(--c-text-subtle); - text-transform: uppercase; -} - -/* Tree rows - mirror .file-sidebar-action-row from FileSidebar.css so the - slide-out folder navigator reads as a continuation of the main sidebar's - design language. Same row height, padding, font weight, muted icon - treatment. The hover/active state uses the same --hover-bg pill that - the sidebar's other rows do, with no heavy accent bar. */ - -.files-page-tree-list { - display: flex; - flex-direction: column; - padding: 4px 0 12px; -} - -.files-page-tree-node { - display: flex; - align-items: center; - height: 32px; - padding: 0 14px; - border-radius: 4px; - margin: 0 8px; - cursor: pointer; - user-select: none; - color: var(--c-text-muted); - font-size: 14px; - position: relative; - transition: background-color 0.15s ease; - flex-shrink: 0; -} - -.files-page-tree-node:hover { - background: var(--c-hover); -} - -.files-page-tree-node.is-active { - background: var(--c-hover); - color: var(--c-text); - font-weight: 500; -} - -.files-page-tree-node.is-drop-target { - background: color-mix(in srgb, var(--c-primary) 12%, transparent); - box-shadow: inset 0 0 0 1px var(--c-primary); - color: var(--c-text); -} - -.files-page-tree-toggle { - display: inline-flex; - align-items: center; - justify-content: center; - width: 16px; - height: 16px; - color: var(--c-text-subtle); - flex-shrink: 0; -} - -.files-page-tree-toggle svg { - font-size: 16px !important; -} - -.files-page-tree-spacer { - display: inline-block; - width: 16px; - height: 16px; - flex-shrink: 0; -} - -.files-page-tree-icon { - display: inline-flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - color: var(--c-text-subtle); - margin-left: 8px; - font-size: 18px; -} - -.files-page-tree-icon svg { - font-size: 18px !important; -} - -.files-page-tree-name { - flex: 1; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - margin-left: 12px; -} - -.files-page-tree-count { - color: var(--c-text-subtle); - font-size: 12px; - flex-shrink: 0; - margin-left: 8px; -} - -@media (max-width: 900px) { - /* Cap the user width on narrow viewports so the tree can't squeeze - * out the file grid. The custom property is still honoured but capped. */ - .folder-tree-panel[data-active="true"] { - width: min(var(--folder-tree-panel-width, 14rem), 14rem); - } -} diff --git a/frontend/editor/src/core/components/filesPage/FolderTreePanel.tsx b/frontend/editor/src/core/components/filesPage/FolderTreePanel.tsx deleted file mode 100644 index b56915b757..0000000000 --- a/frontend/editor/src/core/components/filesPage/FolderTreePanel.tsx +++ /dev/null @@ -1,169 +0,0 @@ -/** Folder tree navigator panel rendered next to FileSidebar on /files. */ - -import React, { useCallback, useEffect, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import { FolderTreeSidebar } from "@app/components/filesPage/FolderTreeSidebar"; -import { useFilesPage } from "@app/contexts/FilesPageContext"; -import { useFolders } from "@app/contexts/FolderContext"; -import { FileId } from "@app/types/file"; -import { FolderId, FolderRecord } from "@app/types/folder"; -import { - MIN_WIDTH, - MAX_WIDTH, - clamp, - computeAutoFitWidth, - loadPersistedWidth, - savePersistedWidth, -} from "@app/components/filesPage/folderTreeWidth"; - -import "@app/components/filesPage/FolderTreePanel.css"; - -interface FolderTreePanelProps { - active: boolean; -} - -export function FolderTreePanel({ active }: FolderTreePanelProps) { - const { t } = useTranslation(); - const { - fileCountsByFolder, - openNewFolderDialog, - openRenameFolderDialog, - promptDeleteFolder, - moveFilesTo, - } = useFilesPage(); - const folders = useFolders(); - const rootLabel = t("filesPage.allFiles", "All files"); - - const [width, setWidth] = useState(() => { - const persisted = loadPersistedWidth(); - return persisted ?? 256; - }); - const userSetRef = useRef(loadPersistedWidth() !== null); - - // Auto-fit to the longest folder name on first render and whenever the - // folder list grows; skipped once the user manually resizes. - useEffect(() => { - if (userSetRef.current) return; - const auto = computeAutoFitWidth(folders.folders, rootLabel); - setWidth(auto); - }, [folders.folders, rootLabel]); - - const dragStateRef = useRef<{ - startX: number; - startWidth: number; - } | null>(null); - - const onMouseMove = useCallback((e: MouseEvent) => { - const state = dragStateRef.current; - if (!state) return; - const next = clamp(state.startWidth + (e.clientX - state.startX)); - setWidth(next); - }, []); - - const onMouseUp = useCallback(() => { - const state = dragStateRef.current; - if (!state) return; - dragStateRef.current = null; - document.removeEventListener("mousemove", onMouseMove); - document.removeEventListener("mouseup", onMouseUp); - document.body.style.removeProperty("cursor"); - document.body.style.removeProperty("user-select"); - userSetRef.current = true; - setWidth((current) => { - savePersistedWidth(current); - return current; - }); - }, [onMouseMove]); - - const onMouseDown = useCallback( - (e: React.MouseEvent) => { - e.preventDefault(); - dragStateRef.current = { startX: e.clientX, startWidth: width }; - document.addEventListener("mousemove", onMouseMove); - document.addEventListener("mouseup", onMouseUp); - document.body.style.cursor = "col-resize"; - document.body.style.userSelect = "none"; - }, - [onMouseMove, onMouseUp, width], - ); - - const onKeyDown = useCallback( - (e: React.KeyboardEvent) => { - const step = e.shiftKey ? 32 : 8; - let next: number | null = null; - if (e.key === "ArrowLeft") next = clamp(width - step); - else if (e.key === "ArrowRight") next = clamp(width + step); - else if (e.key === "Home") next = MIN_WIDTH; - else if (e.key === "End") next = MAX_WIDTH; - if (next === null) return; - e.preventDefault(); - userSetRef.current = true; - setWidth(next); - savePersistedWidth(next); - }, - [width], - ); - - return ( -
-
-
- - {t("fileSidebar.myFiles", "File library")} - -
- - - openRenameFolderDialog(folder) - } - onDeleteFolder={promptDeleteFolder} - onMoveFilesIntoFolder={async ( - targetId: FolderId | null, - fileIds: FileId[], - ) => { - if (fileIds.length === 0) return; - await moveFilesTo(fileIds, targetId); - }} - /> -
- {active && ( -
{ - const auto = computeAutoFitWidth(folders.folders, rootLabel); - userSetRef.current = false; - setWidth(auto); - savePersistedWidth(auto); - }} - /> - )} -
- ); -} diff --git a/frontend/editor/src/core/components/filesPage/FolderTreeSidebar.css b/frontend/editor/src/core/components/filesPage/FolderTreeSidebar.css new file mode 100644 index 0000000000..3db59595ae --- /dev/null +++ b/frontend/editor/src/core/components/filesPage/FolderTreeSidebar.css @@ -0,0 +1,99 @@ +/* Folder tree rows. They sit in the file sidebar's Folders section, so they take the + same row height, padding and muted icon treatment as .file-sidebar-action-row, and + the same hover pill rather than an accent bar. */ + +.files-page-tree-list { + display: flex; + flex-direction: column; + padding: 4px 0 12px; +} + +.files-page-tree-node { + display: flex; + align-items: center; + height: 32px; + padding: 0 14px; + border-radius: 4px; + margin: 0 8px; + cursor: pointer; + user-select: none; + color: var(--c-text-muted); + font-size: 14px; + position: relative; + transition: background-color 0.15s ease; + flex-shrink: 0; +} + +.files-page-tree-node:hover { + background: var(--c-hover); +} + +.files-page-tree-node.is-active { + background: var(--c-hover); + color: var(--c-text); + font-weight: 500; +} + +.files-page-tree-node.is-drop-target { + background: color-mix(in srgb, var(--c-primary) 12%, transparent); + box-shadow: inset 0 0 0 1px var(--c-primary); + color: var(--c-text); +} + +.files-page-tree-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + color: var(--c-text-subtle); + flex-shrink: 0; +} + +.files-page-tree-toggle svg { + font-size: 16px !important; +} + +.files-page-tree-spacer { + display: inline-block; + width: 16px; + height: 16px; + flex-shrink: 0; +} + +.files-page-tree-icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: var(--c-text-subtle); + margin-left: 8px; + font-size: 18px; +} + +.files-page-tree-icon svg { + font-size: 18px !important; +} + +.files-page-tree-name { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-left: 12px; +} + +.files-page-tree-count { + color: var(--c-text-subtle); + font-size: 12px; + flex-shrink: 0; + margin-left: 8px; +} + +@media (max-width: 900px) { + /* Cap the user width on narrow viewports so the tree can't squeeze + * out the file grid. The custom property is still honoured but capped. */ + .folder-tree-panel[data-active="true"] { + width: min(var(--folder-tree-panel-width, 14rem), 14rem); + } +} diff --git a/frontend/editor/src/core/components/filesPage/FolderTreeSidebar.tsx b/frontend/editor/src/core/components/filesPage/FolderTreeSidebar.tsx index 597b0782e8..eb86e1105c 100644 --- a/frontend/editor/src/core/components/filesPage/FolderTreeSidebar.tsx +++ b/frontend/editor/src/core/components/filesPage/FolderTreeSidebar.tsx @@ -27,6 +27,7 @@ import { serialiseFilesPageDragPayload, } from "@app/components/filesPage/dragDrop"; import { useDropTarget } from "@app/components/filesPage/useDropTarget"; +import "@app/components/filesPage/FolderTreeSidebar.css"; /** * Hard cap on folder-tree render depth. The backend already enforces an @@ -54,14 +55,9 @@ interface FolderTreeSidebarProps { ) => Promise | void; } -// This component is always rendered inside FolderTreePanel, which supplies -// its own
diff --git a/frontend/editor/src/core/components/shared/WorkbenchBar.tsx b/frontend/editor/src/core/components/shared/WorkbenchBar.tsx index 12f6094b7e..5602dcd183 100644 --- a/frontend/editor/src/core/components/shared/WorkbenchBar.tsx +++ b/frontend/editor/src/core/components/shared/WorkbenchBar.tsx @@ -52,6 +52,7 @@ import { } from "@app/types/workbenchBar"; import InsertDriveFileOutlinedIcon from "@mui/icons-material/InsertDriveFileOutlined"; import FolderOutlinedIcon from "@mui/icons-material/FolderOutlined"; +import FolderOpenOutlinedIcon from "@mui/icons-material/FolderOpenOutlined"; import WorkbenchBarDesktopActions from "@app/components/shared/workbenchBar/WorkbenchBarDesktopActions"; import WorkbenchBarMobileActions from "@app/components/shared/workbenchBar/WorkbenchBarMobileActions"; import WorkbenchBarToolbarHandle from "@app/components/shared/workbenchBar/WorkbenchBarToolbarHandle"; @@ -413,6 +414,11 @@ export default function WorkbenchBar({ label: t("workbenchBar.activeFiles", "Active Files"), icon: , }, + { + value: "myFiles", + label: t("fileSidebar.myFiles", "File library"), + icon: , + }, ...(selectedTool === "multiTool" ? [ { @@ -519,7 +525,7 @@ export default function WorkbenchBar({
)} - {(hasFiles || isCustomView) && ( + {(hasFiles || isCustomView || currentView === "myFiles") && ( className="workbench-bar-views" size="sm" diff --git a/frontend/editor/src/core/pages/HomePage.tsx b/frontend/editor/src/core/pages/HomePage.tsx index 5fdfb1233d..e0096675bd 100644 --- a/frontend/editor/src/core/pages/HomePage.tsx +++ b/frontend/editor/src/core/pages/HomePage.tsx @@ -27,7 +27,6 @@ import { isApplyingRestoredView } from "@app/services/workbenchSession"; import { useViewer } from "@app/contexts/ViewerContext"; import { useLocation, useNavigate } from "react-router-dom"; import AppsIcon from "@mui/icons-material/AppsRounded"; -import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder"; import RightSidebar from "@app/components/tools/RightSidebar"; @@ -57,7 +56,6 @@ import { } from "@app/contexts/FilesPageContext"; import { useFolders } from "@app/contexts/FolderContext"; import { useFileHandler } from "@app/hooks/useFileHandler"; -import { FolderTreePanel } from "@app/components/filesPage/FolderTreePanel"; import type { FileSidebarProps } from "@app/components/shared/FileSidebar"; import { Button } from "@app/ui/Button"; @@ -265,21 +263,6 @@ export default function HomePage() { } }, [navigationState.workbench, location.pathname, navigate]); - // Auto-collapse the FileSidebar while on /files; restore the user's persisted - // preference on leave. Auto-collapse doesn't write to storage so deep-linking - // to /files won't overwrite what the user actually chose. - const prevWorkbenchRef = useRef(navigationState.workbench); - useEffect(() => { - const prev = prevWorkbenchRef.current; - const curr = navigationState.workbench; - if (curr === "myFiles" && prev !== "myFiles") { - if (!fileSidebarCollapsed) setFileSidebarCollapsed(true); - } else if (curr !== "myFiles" && prev === "myFiles") { - setFileSidebarCollapsed(readPersistedSidebarCollapsed()); - } - prevWorkbenchRef.current = curr; - // fileSidebarCollapsed read as snapshot on transition only. - }, [navigationState.workbench]); // Imperative, so the toggle still works while reading. Never persisted: not a preference. const prevReaderModeRef = useRef(readerMode); useEffect(() => { @@ -718,30 +701,14 @@ export default function HomePage() { bg="var(--c-bg)" >
- - ) : undefined - } - active={navigationState.workbench === "myFiles"} - // Forced: a deep link to /files has no transition to collapse on. - collapsed={ - navigationState.workbench === "myFiles" || - fileSidebarCollapsed - } + collapsed={fileSidebarCollapsed} onToggleCollapse={handleSidebarToggle} onOpenSettings={() => setConfigModalOpen(true)} />
- {!hideToolPanel && } @@ -756,29 +723,15 @@ export default function HomePage() { ); } -interface MyFilesAwareFileSidebarProps extends FileSidebarProps { - active: boolean; -} - -/** Wraps FileSidebar with /files-aware overrides when `active`. */ -const MyFilesAwareFileSidebar = forwardRef< - HTMLDivElement, - MyFilesAwareFileSidebarProps ->(function MyFilesAwareFileSidebar(props, ref) { - const { active, ...rest } = props; - if (!active) { - return ; - } - return ; -}); - -const MyFilesSidebarOverrides = forwardRef( - function MyFilesSidebarOverrides(props, ref) { +/** Uploads land in the folder you have open, and New folder sits with the tree. */ +const FolderAwareFileSidebar = forwardRef( + function FolderAwareFileSidebar(props, ref) { const { t } = useTranslation(); const filesPage = useFilesPage(); const folders = useFolders(); const { addFiles } = useFileHandler(); + const browsingLibrary = useNavigationState().workbench === "myFiles"; const handleUpload = useCallback( async (files: File[]) => { const added = await addFiles(files, { skipWorkspaceDispatch: true }); @@ -805,8 +758,11 @@ const MyFilesSidebarOverrides = forwardRef( , label: t("filesPage.newFolder", "New folder"), diff --git a/frontend/editor/src/core/tests/stubbed/files-page.spec.ts b/frontend/editor/src/core/tests/stubbed/files-page.spec.ts index 619712f9b0..6f753d9d40 100644 --- a/frontend/editor/src/core/tests/stubbed/files-page.spec.ts +++ b/frontend/editor/src/core/tests/stubbed/files-page.spec.ts @@ -822,10 +822,10 @@ test.describe("Files page", () => { }); }); - test.describe("Folder tree panel resize", () => { + test.describe("Folder tree", () => { test.use({ autoGoto: false }); - test("Resize handle is present and keyboard-adjustable", async ({ + test("lives in the file sidebar, not a panel of its own", async ({ page, }) => { await stubStorageApis(page); @@ -833,27 +833,14 @@ test.describe("Files page", () => { { id: "alpha", name: "alpha.pdf", remoteStorageId: null }, ]); await gotoFilesPage(page); - const handle = page.locator(".folder-tree-panel-resizer").first(); - await expect(handle).toBeVisible(); - const before = await page.evaluate(() => { - const el = document.querySelector( - ".folder-tree-panel[data-active='true']", - ) as HTMLElement | null; - return el?.getBoundingClientRect().width ?? 0; - }); - await handle.focus(); - await page.keyboard.press("ArrowRight"); - await page.keyboard.press("ArrowRight"); - await page.keyboard.press("ArrowRight"); - await page.keyboard.press("ArrowRight"); - const after = await page.evaluate(() => { - const el = document.querySelector( - ".folder-tree-panel[data-active='true']", - ) as HTMLElement | null; - return el?.getBoundingClientRect().width ?? 0; - }); - // Four 8px steps = +32px. - expect(after).toBeGreaterThanOrEqual(before + 24); + + const tree = page.getByRole("tree", { name: /Folders/i }); + await expect(tree).toBeVisible(); + await expect( + tree.getByRole("treeitem", { name: /All files/i }), + ).toBeVisible(); + // The library is an ordinary view now, so it adds no column of its own. + await expect(page.locator(".folder-tree-panel")).toHaveCount(0); }); }); });