diff --git a/AGENTS.md b/AGENTS.md index e2b23bb..29a304b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,7 +52,7 @@ Re-filing a note has **three entry points, and they are not equivalent**. `File Drag-and-drop is the third path and **does work on touch** — this was previously documented here as impossible, which was wrong. On a touch screen the gesture is a *long press* rather than an immediate drag, verified working on iOS 18.7 / Firefox 156. It is not universal: it requires JS, and in engines without touch support for HTML5 drag the same gesture scrolls the sidebar instead. So a phone's three paths are all live; do not assume drag is desktop-only when adding UI around it. -Two touch-specific details follow from the long press. `.note-item` sets `-webkit-touch-callout: none` so Safari's long-press menu does not compete with the gesture, while leaving `user-select` alone so preview text stays copyable. And the `dragstart` text-selection guard in `app.js` is gated on `matchMedia('(hover: hover)')` — a long press creates a selection *before* the drag starts, so an ungated guard cancels the gesture itself. `.folder-nav` is `position: sticky` inside the scrolling `.sidebar`, because nothing auto-scrolls a nested overflow container mid-drag and the folders are the drop targets. +Two touch-specific details follow from the long press. `.note-item` sets `-webkit-touch-callout: none` so Safari's long-press menu does not compete with the gesture, while leaving `user-select` alone so preview text stays copyable. And the `dragstart` text-selection guard in `app.js` is gated on `matchMedia('(hover: hover)')` — a long press creates a selection *before* the drag starts, so an ungated guard cancels the gesture itself. `.folder-nav` is `position: sticky` inside the scrolling `.sidebar` on desktop, because nothing auto-scrolls a nested overflow container mid-drag and the folders are the drop targets. On mobile, the sidebar does not scroll; instead `.folder-nav` and `.note-list` each scroll independently within their own bounded areas, and the pull-tab toggle stays visible above the scrolling folder list. Neither entry point may carry `.desktop-only`. That class hides at `max-width: 600px`, and the bar is already hidden above 600px by a `min-width` query; adding the class composes to hiding it at *every* width and phones lose re-filing entirely. That shipped once. No Rust test can observe `display: none`, so `move_entry_points_are_not_desktop_only` pins the markup contract instead. All forms post the same `folder` field, where an empty value means the root. See §11 for the rename rule that makes a move destructive if done any other way, and for why user-supplied folder names force the flyout's `max-width`/wrapping. diff --git a/Cargo.lock b/Cargo.lock index c366aa9..d076c96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -386,7 +386,7 @@ dependencies = [ [[package]] name = "eternalibre-notes" -version = "0.2.0" +version = "0.2.1" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index a8a003a..aa50a0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "eternalibre-notes" -version = "0.2.0" +version = "0.2.1" edition = "2021" license = "GPL-3.0-or-later" description = "EternaLibre Notes - a server-side rendered, copyleft Markdown notes application" diff --git a/README.md b/README.md index e31a177..946fae5 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,12 @@ binds to all interfaces by default, so you can reach your notes from a phone or another machine on your network, protected by a TOTP code from your authenticator app. No telemetry, no accounts, no third-party services. +## What's new in 0.2.1 + +- **Collapsible folder navigation** — a pull-tab button on mobile and a `View → Hide/Show Folder Nav` toggle at any width let you collapse the folder list to give the note list more room +- **Independent mobile scrolling** — the folder nav and note list each scroll within their own bounded area on phones, instead of sharing one scroll position +- **Save Note moved** — the action now lives in the `File` menu alongside `New Note`, rather than under `Edit` + ## What's new in 0.2 - **Virtual folders** — organize notes into folders without creating real directories. A folder is encoded as a `[Folder]uuid.md` filename prefix, so a single notes directory can hold many of them diff --git a/static/style.css b/static/style.css index 439b71f..9d1b447 100644 --- a/static/style.css +++ b/static/style.css @@ -1370,7 +1370,26 @@ button, input, select, textarea { display: flex; } + /* Independent scrolling: folder nav and note list each scroll within + their own area. The sidebar itself does not scroll on mobile. The + pull-tab button stays visible above the scrolling folder list. */ + .sidebar { + overflow: hidden; + } + + .folder-nav-container { + position: static; + display: flex; + flex-direction: column; + max-height: 40%; + flex-shrink: 0; + } + .folder-nav { + overflow-y: auto; + flex: 1; + min-height: 0; + -webkit-overflow-scrolling: touch; padding: 8px; } @@ -1380,6 +1399,10 @@ button, input, select, textarea { } .note-list { + overflow-y: auto; + flex: 1; + min-height: 0; + -webkit-overflow-scrolling: touch; padding: 8px; } diff --git a/tests/auth_gating.rs b/tests/auth_gating.rs index 64905d6..efc890c 100644 --- a/tests/auth_gating.rs +++ b/tests/auth_gating.rs @@ -1376,6 +1376,42 @@ async fn save_note_is_in_file_menu() { ); } +/// On mobile, the folder nav and note list scroll independently within their +/// own bounded areas. The sidebar itself does not scroll, and the pull-tab +/// toggle stays visible above the scrolling folder list. +#[tokio::test] +async fn mobile_folder_nav_scrolls_independently() { + let f = Fixture::provisioned("mobile-independent-scroll"); + f.post_authed("/folders", "name=Alpha").await; + + let (status, body) = f.get_authed("/").await; + assert_eq!(status, StatusCode::OK); + + // The mobile CSS must make the sidebar non-scrollable and give the + // folder nav and note list their own scroll areas. + let mobile_css_start = body + .find("@media (max-width: 600px)") + .expect("mobile media query should exist"); + let mobile_css = &body[mobile_css_start..]; + + assert!( + mobile_css.contains(".sidebar") && mobile_css.contains("overflow: hidden"), + "mobile sidebar should not scroll" + ); + assert!( + mobile_css.contains(".folder-nav") && mobile_css.contains("overflow-y: auto"), + "folder nav should scroll independently" + ); + assert!( + mobile_css.contains(".note-list") && mobile_css.contains("overflow-y: auto"), + "note list should scroll independently" + ); + assert!( + mobile_css.contains("max-height: 40%"), + "folder nav container should have a max height" + ); +} + /// The `folder-nav` list, and nothing else. /// /// Scoped deliberately: `.nav-count` also appears in the inlined stylesheet,