Added toggle for the Folder nav and moved Save Note to the 'File ->' menu
This commit is contained in:
parent
1a28081e70
commit
306c3c1e6e
4 changed files with 205 additions and 28 deletions
59
src/pages.rs
59
src/pages.rs
|
|
@ -167,6 +167,7 @@ fn render_menu_bar(base: &Base<'_>, current_note: Option<&Note>, folders: &[Stri
|
|||
button type="submit" { "New Note" }
|
||||
}
|
||||
}
|
||||
li { button x-on:click="saveCurrentNote()" { "Save Note" } }
|
||||
li { hr; }
|
||||
li {
|
||||
a href=(base.url("/export")) { "Export All Notes" }
|
||||
|
|
@ -259,7 +260,6 @@ fn render_menu_bar(base: &Base<'_>, current_note: Option<&Note>, folders: &[Stri
|
|||
span { "Edit" }
|
||||
ul class="menu-dropdown" {
|
||||
li { button x-on:click="focusEditor()" { "Focus Editor" } }
|
||||
li { button x-on:click="saveCurrentNote()" { "Save Note" } }
|
||||
}
|
||||
}
|
||||
li class="menu-item" tabindex="0"
|
||||
|
|
@ -287,6 +287,17 @@ fn render_menu_bar(base: &Base<'_>, current_note: Option<&Note>, folders: &[Stri
|
|||
"Toggle Preview"
|
||||
}
|
||||
}
|
||||
// Available at every width. The folder nav is a sidebar
|
||||
// sub-section, not a top-level pane, so collapsing it is
|
||||
// useful on mobile too — the pull-tab button is the other
|
||||
// entry point and shares this same state.
|
||||
li {
|
||||
button x-on:click="toggleFolderNav()"
|
||||
x-bind:class="{ 'active-view': folderNavCollapsed }"
|
||||
x-text="folderNavCollapsed ? 'Show Folder Nav' : 'Hide Folder Nav'" {
|
||||
"Toggle Folder Nav"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
li class="menu-item" tabindex="0"
|
||||
|
|
@ -427,26 +438,36 @@ fn render_sidebar(
|
|||
input type="text" name="name" placeholder="New folder name...";
|
||||
button type="submit" { "+ New Folder" }
|
||||
}
|
||||
ul class="folder-nav" {
|
||||
// An empty drop folder is the root: dropping a note on "All
|
||||
// Notes" un-files it, which is the only way back out of a
|
||||
// folder once every note has left it.
|
||||
li class={"nav-item" (if active_folder.is_none() { " active" } else { "" })}
|
||||
data-drop-folder="" {
|
||||
a href=(base.url("/")) {
|
||||
span class="nav-label" { "All Notes" }
|
||||
@if show_nav_count(active_folder, None) {
|
||||
span class="nav-count" { (notes.len()) }
|
||||
div class="folder-nav-container"
|
||||
x-bind:class="{ 'collapsed': folderNavCollapsed }" {
|
||||
button type="button" class="folder-nav-toggle"
|
||||
x-on:click="toggleFolderNav()"
|
||||
x-bind:aria-expanded="(!folderNavCollapsed).toString()"
|
||||
aria-controls="folder-nav-list"
|
||||
aria-label="Toggle folder navigation" {
|
||||
span class="folder-nav-toggle-icon" aria-hidden="true" { "▾" }
|
||||
}
|
||||
ul id="folder-nav-list" class="folder-nav" {
|
||||
// An empty drop folder is the root: dropping a note on "All
|
||||
// Notes" un-files it, which is the only way back out of a
|
||||
// folder once every note has left it.
|
||||
li class={"nav-item" (if active_folder.is_none() { " active" } else { "" })}
|
||||
data-drop-folder="" {
|
||||
a href=(base.url("/")) {
|
||||
span class="nav-label" { "All Notes" }
|
||||
@if show_nav_count(active_folder, None) {
|
||||
span class="nav-count" { (notes.len()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@for folder in folders {
|
||||
li class={"nav-item" (if active_folder == Some(folder.as_str()) { " active" } else { "" })}
|
||||
data-drop-folder=(folder) {
|
||||
a href=(folder_url(base, folder)) {
|
||||
span class="nav-label" { (folder) }
|
||||
@if show_nav_count(active_folder, Some(folder)) {
|
||||
span class="nav-count" { (by_folder.get(folder).map(|v| v.len()).unwrap_or(0)) }
|
||||
@for folder in folders {
|
||||
li class={"nav-item" (if active_folder == Some(folder.as_str()) { " active" } else { "" })}
|
||||
data-drop-folder=(folder) {
|
||||
a href=(folder_url(base, folder)) {
|
||||
span class="nav-label" { (folder) }
|
||||
@if show_nav_count(active_folder, Some(folder)) {
|
||||
span class="nav-count" { (by_folder.get(folder).map(|v| v.len()).unwrap_or(0)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ function app() {
|
|||
// the highlight follows the pointer instead of accumulating on every item
|
||||
// the cursor sweeps across.
|
||||
_dropTarget: null,
|
||||
// Whether the folder navigation is collapsed. Shared between the mobile
|
||||
// pull-tab button and the View menu toggle so they stay in sync.
|
||||
folderNavCollapsed: false,
|
||||
|
||||
init() {
|
||||
// Close menus on any click outside the menu bar. Alpine's `.outside`
|
||||
|
|
@ -258,6 +261,10 @@ function app() {
|
|||
this.editorVisible = !this.editorVisible;
|
||||
},
|
||||
|
||||
toggleFolderNav() {
|
||||
this.folderNavCollapsed = !this.folderNavCollapsed;
|
||||
},
|
||||
|
||||
focusEditor() {
|
||||
const editor = document.getElementById('editor');
|
||||
if (editor) editor.focus();
|
||||
|
|
|
|||
|
|
@ -505,14 +505,7 @@ button, input, select, textarea {
|
|||
Needs its own background because it now overlays the scrolling note items.
|
||||
The z-index is scoped to the sidebar and safe under the stacking-context
|
||||
rule in AGENTS.md §7: no menu dropdowns live in here to trap. */
|
||||
.folder-nav {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
border-bottom: 1px solid var(--ui-border);
|
||||
.folder-nav-container {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
|
|
@ -523,6 +516,52 @@ button, input, select, textarea {
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.folder-nav {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
border-bottom: 1px solid var(--ui-border);
|
||||
}
|
||||
|
||||
/* Pull-tab toggle — hidden on desktop, shown on mobile */
|
||||
.folder-nav-toggle {
|
||||
display: none;
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--app-bg);
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--ui-border);
|
||||
color: var(--app-fg);
|
||||
opacity: 0.6;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.folder-nav-toggle:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.folder-nav-toggle-icon {
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
/* Collapsed state — hides the folder list but keeps the toggle visible */
|
||||
.folder-nav-container.collapsed .folder-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.folder-nav-container.collapsed .folder-nav-toggle-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.nav-item a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -1327,6 +1366,10 @@ button, input, select, textarea {
|
|||
min-height: 44px;
|
||||
}
|
||||
|
||||
.folder-nav-toggle {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.folder-nav {
|
||||
padding: 8px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1274,6 +1274,108 @@ async fn sidebar_shows_folder_navigation() {
|
|||
assert!(body.contains("nav-count"), "should show note counts");
|
||||
}
|
||||
|
||||
/// The folder nav is wrapped in `.folder-nav-container` with a pull-tab toggle
|
||||
/// button. The toggle must be present in the markup at every width (CSS hides
|
||||
/// it on desktop) so mobile users can collapse/expand the folder list.
|
||||
#[tokio::test]
|
||||
async fn folder_nav_container_has_toggle_button() {
|
||||
let f = Fixture::provisioned("folder-nav-toggle");
|
||||
f.post_authed("/folders", "name=Alpha").await;
|
||||
|
||||
let (status, body) = f.get_authed("/").await;
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
|
||||
assert!(
|
||||
body.contains("class=\"folder-nav-container\""),
|
||||
"the folder nav should be wrapped in .folder-nav-container"
|
||||
);
|
||||
assert!(
|
||||
body.contains("class=\"folder-nav-toggle\""),
|
||||
"the pull-tab toggle button should be in the markup"
|
||||
);
|
||||
assert!(
|
||||
body.contains("toggleFolderNav()"),
|
||||
"the toggle button should call toggleFolderNav()"
|
||||
);
|
||||
assert!(
|
||||
body.contains("aria-controls=\"folder-nav-list\""),
|
||||
"the toggle should reference the folder nav list via aria-controls"
|
||||
);
|
||||
}
|
||||
|
||||
/// The View menu has a Hide/Show Folder Nav toggle that shares the same state
|
||||
/// as the pull-tab button.
|
||||
#[tokio::test]
|
||||
async fn view_menu_has_folder_nav_toggle() {
|
||||
let f = Fixture::provisioned("view-menu-folder-nav");
|
||||
|
||||
let (status, body) = f.get_authed("/").await;
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
|
||||
// Find the View menu dropdown and check it contains the folder nav toggle
|
||||
let view_start = body
|
||||
.find("toggleMenu('view')")
|
||||
.expect("the View menu should exist");
|
||||
let view_end = body[view_start..]
|
||||
.find("</ul>")
|
||||
.map(|i| view_start + i)
|
||||
.expect("the View menu dropdown should close");
|
||||
let view_menu = &body[view_start..view_end];
|
||||
|
||||
assert!(
|
||||
view_menu.contains("toggleFolderNav()"),
|
||||
"the View menu should have a folder nav toggle"
|
||||
);
|
||||
assert!(
|
||||
view_menu.contains("Folder Nav"),
|
||||
"the View menu toggle should mention Folder Nav"
|
||||
);
|
||||
}
|
||||
|
||||
/// Save Note lives in the File menu, not the Edit menu.
|
||||
#[tokio::test]
|
||||
async fn save_note_is_in_file_menu() {
|
||||
let f = Fixture::provisioned("save-note-file-menu");
|
||||
let id = f.create_note().await;
|
||||
|
||||
let (status, body) = f.get_authed(&format!("/notes/{id}")).await;
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
|
||||
// Find the File menu dropdown
|
||||
let file_start = body
|
||||
.find("toggleMenu('file')")
|
||||
.expect("the File menu should exist");
|
||||
let file_end = body[file_start..]
|
||||
.find("</ul>")
|
||||
.map(|i| file_start + i)
|
||||
.expect("the File menu dropdown should close");
|
||||
let file_menu = &body[file_start..file_end];
|
||||
|
||||
assert!(
|
||||
file_menu.contains("saveCurrentNote()"),
|
||||
"Save Note should be in the File menu"
|
||||
);
|
||||
assert!(
|
||||
file_menu.contains("Save Note"),
|
||||
"Save Note label should be in the File menu"
|
||||
);
|
||||
|
||||
// Find the Edit menu dropdown
|
||||
let edit_start = body
|
||||
.find("toggleMenu('edit')")
|
||||
.expect("the Edit menu should exist");
|
||||
let edit_end = body[edit_start..]
|
||||
.find("</ul>")
|
||||
.map(|i| edit_start + i)
|
||||
.expect("the Edit menu dropdown should close");
|
||||
let edit_menu = &body[edit_start..edit_end];
|
||||
|
||||
assert!(
|
||||
!edit_menu.contains("saveCurrentNote()"),
|
||||
"Save Note should NOT be in the Edit menu"
|
||||
);
|
||||
}
|
||||
|
||||
/// The `folder-nav` list, and nothing else.
|
||||
///
|
||||
/// Scoped deliberately: `.nav-count` also appears in the inlined stylesheet,
|
||||
|
|
@ -1282,9 +1384,13 @@ async fn sidebar_shows_folder_navigation() {
|
|||
/// to pass while showing the wrong thing.
|
||||
fn folder_nav_region(body: &str) -> &str {
|
||||
let start = body
|
||||
.find("<ul class=\"folder-nav\">")
|
||||
.find("id=\"folder-nav-list\"")
|
||||
.expect("the folder nav should be present");
|
||||
let rest = &body[start..];
|
||||
// Back up to the opening <ul tag
|
||||
let ul_start = body[..start]
|
||||
.rfind("<ul")
|
||||
.expect("the folder nav should have a <ul> tag");
|
||||
let rest = &body[ul_start..];
|
||||
let end = rest.find("</ul>").expect("the folder nav should be closed");
|
||||
&rest[..end]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue