From ee6bbc464d1f813f8d9ad27b8855df9e016b0778 Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 13 Jul 2026 15:28:45 +0200 Subject: [PATCH] improved mobile writing experience --- components/navbar/ProjectNavbar.module.css | 21 --------------------- components/navbar/ProjectNavbarMobile.tsx | 2 +- components/project/EditorFooter.module.css | 15 +++++++++++++++ components/project/ProjectWorkspace.tsx | 20 +++++++++++++++++++- 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/components/navbar/ProjectNavbar.module.css b/components/navbar/ProjectNavbar.module.css index e6f8871e..3e6e5d17 100644 --- a/components/navbar/ProjectNavbar.module.css +++ b/components/navbar/ProjectNavbar.module.css @@ -91,27 +91,6 @@ background-color: var(--secondary-hover); } -/* The "leave edit mode" checkmark: a filled pill that reads as the primary - * action of the edit-mode island, standing apart from the ghost undo/redo icons - * beside it via the inverted text colour. */ -.edit_done { - color: var(--main-bg); - background-color: var(--primary-text); -} - -/* The global `svg { color: var(--primary-text) }` rule targets the icon element - * directly, so it overrides the inverted `color` inherited from this pill and - * paints the checkmark the same shade as the pill's background (invisible in - * dark mode). Re-inherit the button's colour so the icon stays legible. */ -.edit_done svg { - color: inherit; -} - -.edit_done:hover { - background-color: var(--primary-text); - opacity: 0.85; -} - @media (max-width: 767px) { .container { /* Push the bar below the status bar / Dynamic Island. --navbar-height diff --git a/components/navbar/ProjectNavbarMobile.tsx b/components/navbar/ProjectNavbarMobile.tsx index 6bae196f..283dbd4e 100644 --- a/components/navbar/ProjectNavbarMobile.tsx +++ b/components/navbar/ProjectNavbarMobile.tsx @@ -97,7 +97,7 @@ const ProjectNavbarMobile = () => { keeps the left side empty. */} {isInProject && projectId && mobileEditMode && (
-
+
runHistory("undo")}> diff --git a/components/project/EditorFooter.module.css b/components/project/EditorFooter.module.css index aa5eef90..78f7c889 100644 --- a/components/project/EditorFooter.module.css +++ b/components/project/EditorFooter.module.css @@ -73,4 +73,19 @@ transform: translateX(-50%); bottom: calc(16px + var(--safe-bottom)); } + + /* Drop the page count on phone: it's redundant in the default endless-scroll + * reader and is the pill's widest element. Hiding it keeps the row a compact + * cluster of toggles that clears the bottom-left Edit (pen) button — without a + * width cap, which would clip the timer's live readout when it's running. */ + .pages, + .divider { + display: none; + } + + /* Roomier, thumb-friendly tap targets now that the row is short. */ + .action { + width: 34px; + height: 32px; + } } diff --git a/components/project/ProjectWorkspace.tsx b/components/project/ProjectWorkspace.tsx index f041934e..fffa3ebf 100644 --- a/components/project/ProjectWorkspace.tsx +++ b/components/project/ProjectWorkspace.tsx @@ -1,6 +1,6 @@ "use client"; -import { useContext, useState } from "react"; +import { useContext, useEffect, useState } from "react"; import { useViewContext } from "@src/context/ViewContext"; import { ProjectContext } from "@src/context/ProjectContext"; import { useActiveEditor } from "@src/lib/editor/use-active-editor"; @@ -31,6 +31,24 @@ const ProjectWorkspace = () => { const isPhone = useIsPhone(); const activeEditor = useActiveEditor(); + // iOS WKWebView anchoring guard. The app shell is pinned to the viewport + // (100vh, overflow hidden) and only the inner editor container is meant to + // scroll. But with the on-screen keyboard up, a contenteditable edit — most + // reproducibly deleting/joining an empty node — can make WebKit scroll the + // *document itself* to chase the caret. That drags the whole shell up: the + // (relatively positioned) navbar slides off the top and the editor looks + // unanchored. Nothing at the window level is ever supposed to scroll here, so + // snap it straight back to the top. The listener is on window and + // non-capturing, so it never fires for the inner container's own scroll. + useEffect(() => { + if (!isPhone) return; + const anchor = () => { + if (window.scrollY !== 0 || window.scrollX !== 0) window.scrollTo(0, 0); + }; + window.addEventListener("scroll", anchor, { passive: true }); + return () => window.removeEventListener("scroll", anchor); + }, [isPhone]); + // Enter edit mode and drop the caret into the reader's current editor so the // keyboard comes up straight away. Focus must happen SYNCHRONOUSLY inside this // tap gesture — iOS only raises the keyboard when focus() runs in the same