From fb2c3c88f70f755db7988031210d4775641bf254 Mon Sep 17 00:00:00 2001 From: Hugo Bois Date: Wed, 2 Sep 2026 12:24:38 +0200 Subject: [PATCH] made dashboard more consistent between project and project page, fixed sidebar closing on import button, tweaked rendering for collapsed sidebars --- .../dashboard/DashboardModal.module.css | 20 ++++-- components/dashboard/DashboardModal.tsx | 11 ++-- components/editor/EditorPanel.module.css | 16 ----- .../editor/sidebar/EditorSidebar.module.css | 33 ++++++---- .../EditorSidebarNavigation.module.css | 40 +++++++++++- components/navbar/ProjectNavbarMobile.tsx | 30 ++++----- .../navbar/ProjectNavbarMobileMenu.module.css | 24 +++++-- components/navbar/ProjectNavbarMobileMenu.tsx | 16 ++++- components/navbar/useProjectNavbar.ts | 4 ++ .../project/ProjectWorkspace.module.css | 37 +++++++---- components/project/ProjectWorkspace.tsx | 18 ++---- .../project/SplitPanelContainer.module.css | 8 +-- components/projects/ProjectPageContainer.tsx | 12 ++-- src/context/DashboardContext.tsx | 64 ++++++++++++++++++- 14 files changed, 230 insertions(+), 103 deletions(-) diff --git a/components/dashboard/DashboardModal.module.css b/components/dashboard/DashboardModal.module.css index 784acfb9..41837629 100644 --- a/components/dashboard/DashboardModal.module.css +++ b/components/dashboard/DashboardModal.module.css @@ -249,8 +249,8 @@ color: var(--primary-text); } -/* Slide the drawer in from the right on phone so it reads as the same sidebar - * the burger menu opened from, rather than popping in place. */ +/* Slide the drawer in from the right on phone when it genuinely opens (the home + * Settings button, a tool sheet's link) rather than popping in place. */ @keyframes dashboardDrawerIn { from { transform: translateX(100%); @@ -260,10 +260,10 @@ } } -/* Phone: the sidebar nav lives in the burger menu, so the modal drops it and - * renders as a right-side drawer matching that menu (same width and position - * below the navbar) instead of a full-screen pane. Tabs are switched from the - * burger, which re-opens the dashboard on the chosen tab. Content is tightened +/* Phone: on a project the sidebar nav lives in the burger menu, so the modal + * drops it and renders as a right-side drawer matching that menu (same width and + * position below the navbar) instead of a full-screen pane. Tabs are switched from + * the burger, which swaps this drawer in for it on the chosen tab. Content is tightened * and slightly scaled down so the settings sit comfortably in the narrow width. */ @media (max-width: 767px) { .overlay { @@ -291,6 +291,14 @@ animation: dashboardDrawerIn 0.25s ease; } + /* Swapped in for the burger menu, which sits in this exact rect: the drawer + * is already on screen, so it must not slide in again — only its content + * changes. Same in-place replacement the sections list ⇄ section switch does + * below (see [DashboardContext].drawerSwap). */ + .modal.instant { + animation: none; + } + /* The nav sidebar and the content share the drawer, one screen at a time. * Default screen is content; .mobileSections flips to the sections list (used * when the dashboard is opened without a burger menu behind it — e.g. the diff --git a/components/dashboard/DashboardModal.tsx b/components/dashboard/DashboardModal.tsx index b87ee84d..f8ab86f5 100644 --- a/components/dashboard/DashboardModal.tsx +++ b/components/dashboard/DashboardModal.tsx @@ -25,7 +25,7 @@ import DashboardAuth from "./account/DashboardAuth"; import AboutSettings from "./AboutSettings"; const DashboardModal = () => { - const { isOpen, closeDashboard, activeTab, setActiveTab, openedFromMenu, setMobileMenuOpen } = + const { isOpen, closeDashboard, activeTab, setActiveTab, openedFromMenu, swapDrawerScreen, drawerSwap } = useContext(DashboardContext); const t = useTranslations("modal"); const tSidebar = useTranslations("sidebar"); @@ -40,10 +40,13 @@ const DashboardModal = () => { // so preferences/account tabs are reachable, and the back arrow returns to it. const [mobileShowSections, setMobileShowSections] = useState(false); + // Either way this is the same gesture — step back to the list of sections — + // so it looks the same either way: the drawer's content is replaced, with no + // slide. Only the list itself differs (the burger menu in a project, this + // modal's own sidebar elsewhere). const handleBack = () => { if (openedFromMenu) { - closeDashboard(); - setMobileMenuOpen(true); + swapDrawerScreen("menu"); } else { setMobileShowSections(true); } @@ -133,7 +136,7 @@ const DashboardModal = () => { return (
e.stopPropagation()} > diff --git a/components/editor/EditorPanel.module.css b/components/editor/EditorPanel.module.css index 3375e1e8..435d7609 100644 --- a/components/editor/EditorPanel.module.css +++ b/components/editor/EditorPanel.module.css @@ -69,22 +69,6 @@ display: block; } -@media (pointer: coarse) { - .container, - .editor_wrapper, - .page_shift { - -webkit-user-select: none; - user-select: none; - -webkit-touch-callout: none; - } - - .container :global(.ProseMirror) { - -webkit-user-select: text; - user-select: text; - -webkit-touch-callout: default; - } -} - .editor_shadow { position: sticky; top: 0; diff --git a/components/editor/sidebar/EditorSidebar.module.css b/components/editor/sidebar/EditorSidebar.module.css index e7af081a..42b8a5a1 100644 --- a/components/editor/sidebar/EditorSidebar.module.css +++ b/components/editor/sidebar/EditorSidebar.module.css @@ -16,21 +16,17 @@ overflow: hidden; } -.collapsed .element { - visibility: hidden; -} - +/* NB: this module has no `.element` rule of its own on purpose. EditorSidebarFormat + * renders the *navigation* module's `.element` inside this module's + * `.sidebar_content`, and CSS Modules scope the two independently — so a + * `.collapsed .element` selector written here could never match the panel it was + * aimed at, and this drawer went on rendering its full contents while shut. + * `visibility` inherits, so hiding the drawer itself covers whatever panel the + * markup puts inside it. */ .collapsed { width: 0; padding: 0; -} - -.element { - background-color: var(--editor-sidebar); - padding-top: 10px; - padding-bottom: 38px; - border-radius: 35px; - box-shadow: var(--panel-shadow); + visibility: hidden; } /* Phone: the format sidebar becomes a right overlay drawer, mirroring the @@ -53,7 +49,11 @@ /* No drawer background — only the rounded .element panel shows, floating * over the (backdrop-dimmed) editor, matching the desktop look. */ transform: translateX(0); - transition: transform 0.25s ease; + /* No delay on the visibility flip coming back in: the drawer has to be + * rendered before it can slide into view. */ + transition: + transform 0.25s ease, + visibility 0s; pointer-events: auto; } @@ -62,6 +62,13 @@ padding: 12px; transform: translateX(100%); pointer-events: none; + /* Hold the hide until the slide-out has played, then stop rendering the + * drawer entirely — see the matching block in + * EditorSidebarNavigation.module.css for why an off-screen translate is + * not enough on its own. */ + transition: + transform 0.25s ease, + visibility 0s linear 0.25s; } } diff --git a/components/editor/sidebar/EditorSidebarNavigation.module.css b/components/editor/sidebar/EditorSidebarNavigation.module.css index 9923e995..9c4ed185 100644 --- a/components/editor/sidebar/EditorSidebarNavigation.module.css +++ b/components/editor/sidebar/EditorSidebarNavigation.module.css @@ -70,7 +70,11 @@ /* No drawer background — only the rounded .element panel shows, floating * over the (backdrop-dimmed) editor, matching the desktop look. */ transform: translateX(0); - transition: transform 0.25s ease; + /* visibility flips back with no delay on the way in: the drawer has to be + * rendered before it can slide into view. */ + transition: + transform 0.25s ease, + visibility 0s; pointer-events: auto; } @@ -79,11 +83,41 @@ padding: 12px; transform: translateX(-100%); pointer-events: none; + /* + * Stop rendering the drawer once it has finished sliding out. + * + * Translating it off-screen is not enough on iOS. Running the transform + * transition promotes this fixed, full-height element to its own + * compositing layer with a real backing store (the panel, its text, its + * box-shadow), and WebKit keeps that layer after the transition ends — + * a `position: fixed` element with a non-identity transform and a + * declared transition is exactly what it holds on to. From then on the + * layer sits permanently over the panel area at z-index 60, so every + * pan of the board and every scroll of the editor has to recomposite + * against it: smooth until the drawer is first opened, unusable after. + * + * `visibility: hidden` drops it out of rendering entirely and the layer + * with it, while leaving the subtree laid out — the marker gutter is + * measured against the scene list with getBoundingClientRect (see + * EditorSidebarNavigation), which `display: none` would zero. + * + * The 0.25s delay is what lets the slide-out still be seen: visibility + * is not interpolated, so it simply holds `visible` for the length of the + * transform and then flips. The format drawer on the right needs the same + * treatment and now carries it (EditorSidebar.module.css). + */ + visibility: hidden; + transition: + transform 0.25s ease, + visibility 0s linear 0.25s; } - /* Defeat the desktop hide above so the slide-out is actually seen. */ + /* Follow the drawer's own delayed visibility rather than the desktop rule's + * immediate hide, so the slide-out is actually seen. Must be `inherit` and + * not `visible`: an explicit `visible` here would keep the panel rendered + * after the drawer above it has gone hidden, which is the whole problem. */ .collapsed .element { - visibility: visible; + visibility: inherit; } } diff --git a/components/navbar/ProjectNavbarMobile.tsx b/components/navbar/ProjectNavbarMobile.tsx index db9e5027..f3debc17 100644 --- a/components/navbar/ProjectNavbarMobile.tsx +++ b/components/navbar/ProjectNavbarMobile.tsx @@ -55,11 +55,12 @@ const ProjectNavbarMobile = () => { const tSidebar = useTranslations("sidebar"); const { - openDashboard, closeDashboard, isDashboardOpen, mobileMenuOpen, setMobileMenuOpen, + swapDrawerScreen, + drawerSwap, membership, userCtx, canUploadToCloud, @@ -297,7 +298,11 @@ const ProjectNavbarMobile = () => { {isInProject && projectId && ( - setMobileMenuOpen(false)}> + setMobileMenuOpen(false)} + >
{membership ? ( @@ -329,8 +334,10 @@ const ProjectNavbarMobile = () => {
{/* Dashboard settings — the modal drops its sidebar on phone, so - its grouped tabs are navigated from here; each opens the - dashboard directly on that tab. */} + this menu *is* its sections list; each row swaps the dashboard + drawer in for this one on that tab, in place and without a + slide, the way the home dashboard swaps its own list for a + section (see [DashboardContext].swapDrawerScreen). */} {dashboardMenu.map((section) => (
{section.group}
@@ -338,10 +345,7 @@ const ProjectNavbarMobile = () => {
{/* Phone drawer backdrop. - Kept MOUNTED and faded, never conditionally rendered: on - iOS, when the element under a tap is destroyed by its own - click handler, WebKit re-resolves its remembered pointer - position onto whatever now sits beneath the point — the - board — and then re-runs that hit-test + hover chain on - every subsequent layout/style change. Cards sweeping under - the phantom point toggled `.card:hover`'s transitioned - box-shadow frame after frame, freezing drags and pinches - at ~8fps until a tap elsewhere moved the point away. A - surviving backdrop keeps the hover pinned to an element - that still exists, and is inert while hidden. */} + Kept mounted and faded rather than conditionally rendered, + so the dim eases in and out with the drawer slide instead + of popping. It is not left *rendered* while hidden, though + — the stylesheet drops it out of rendering once the fade + is done, which is what keeps its full-viewport fixed layer + from being composited over the panel area for the rest of + the session (see .sidebar_backdrop_hidden). */} {isPhone && (
{ fileInputRef.current?.click(); }; @@ -89,11 +94,6 @@ const ProjectPageContainer = ({ sidebarOpen, setSidebarOpen }: ProjectPageContai setIsCreating(true); }; - const startImport = () => { - if (isPhone) setSidebarOpen(false); - handleImportClick(); - }; - if (isLoading || !projects) return ; const renderMain = () => { @@ -173,7 +173,7 @@ const ProjectPageContainer = ({ sidebarOpen, setSidebarOpen }: ProjectPageContai