From 5287b471d08d62238a7ad4cc7135cf18acddc2e4 Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:37:29 -0400 Subject: [PATCH] Redesign project header and study navigation - Header: navigation buttons grouped; Study Guide, Podcast, and Pronunciation Guide move into a Tools menu (disabled until a chunk exists), set apart from Export and Share by a divider. - Only one header dropdown is open at a time, and it closes on an outside click or Escape. - Study page: drop the duplicate Prev/Next and counter at the top; the fixed bottom bar is the single chunk navigation and now shows a readable "Chunk N of M". - Mobile empty-state hint points to Back to Chunks. - Tests: Tools menu, one-dropdown-at-a-time, Escape/outside click. Co-Authored-By: Claude Opus 5.5 --- src/App.test.jsx | 32 +++++ src/components/ProjectHeader.jsx | 195 ++++++++++++++++++------------- src/pages/StudyPage.jsx | 36 +----- 3 files changed, 152 insertions(+), 111 deletions(-) diff --git a/src/App.test.jsx b/src/App.test.jsx index c18da1c..1b3924e 100644 --- a/src/App.test.jsx +++ b/src/App.test.jsx @@ -702,3 +702,35 @@ describe('Chunk builder', () => { expect(await screen.findByRole('button', { name: 'Titus 2' })).toHaveClass('bg-slate-900'); }); }); + +// --------------------------------------------------------------------------- +// Header redesign: Tools menu + one-dropdown-at-a-time +// --------------------------------------------------------------------------- +describe('Header menus', () => { + test('Tools is disabled until a chunk exists, then opens the Pronunciation Guide', async () => { + const user = await loadChapter(); + expect(screen.getByRole('button', { name: /tools/i })).toBeDisabled(); + addChunk('Paul, a servant', 'Paul, a servant'); + await screen.findByText('1 created'); + await user.click(screen.getByRole('button', { name: /tools/i })); + await user.click(screen.getByRole('button', { name: /pronunciation guide/i })); + expect(await screen.findByRole('heading', { name: /pronunciation guide/i })).toBeInTheDocument(); + }); + + test('opening one dropdown closes the other; Escape and outside clicks close it', async () => { + const user = await loadChapter(); + await user.click(screen.getByRole('button', { name: /export/i })); + expect(screen.getByRole('button', { name: /export markdown/i })).toBeInTheDocument(); + + await user.click(screen.getByRole('button', { name: /share/i })); + expect(screen.queryByRole('button', { name: /export markdown/i })).not.toBeInTheDocument(); + expect(screen.getByText(/read-only share link/i)).toBeInTheDocument(); + + await user.keyboard('{Escape}'); + expect(screen.queryByText(/read-only share link/i)).not.toBeInTheDocument(); + + await user.click(screen.getByRole('button', { name: /export/i })); + await user.click(screen.getByText(/scripture & chunks/i)); + expect(screen.queryByRole('button', { name: /export markdown/i })).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/ProjectHeader.jsx b/src/components/ProjectHeader.jsx index 97a8cb4..128aad5 100644 --- a/src/components/ProjectHeader.jsx +++ b/src/components/ProjectHeader.jsx @@ -1,6 +1,6 @@ // Header for an open project (Setup and Study pages): navigation, Claude // prompts, pronunciation guide, export menu, share link, and save/sync status. -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useApp } from '../context/AppContext.js'; import { useShareLink } from '../hooks/useShareLink.js'; import { buildClaudePrompt, buildPodcastPrompt } from '../utils/exportBuilders.js'; @@ -24,8 +24,26 @@ export default function ProjectHeader({ loading = false }) { syncStatus, retrySync, } = useApp(); - const [exportMenuOpen, setExportMenuOpen] = useState(false); - const [sharePanelOpen, setSharePanelOpen] = useState(false); + // Only one dropdown open at a time: 'tools' | 'export' | 'share' | null + const [openMenu, setOpenMenu] = useState(null); + const toggleMenu = (name) => setOpenMenu((current) => (current === name ? null : name)); + const closeMenu = () => setOpenMenu(null); + const menusRef = useRef(null); + + // Close the open dropdown on an outside click or Escape. + useEffect(() => { + if (!openMenu) return; + const onPointerDown = (e) => { + if (!menusRef.current?.contains(e.target)) setOpenMenu(null); + }; + const onKeyDown = (e) => { if (e.key === 'Escape') setOpenMenu(null); }; + document.addEventListener('mousedown', onPointerDown); + document.addEventListener('keydown', onKeyDown); + return () => { + document.removeEventListener('mousedown', onPointerDown); + document.removeEventListener('keydown', onKeyDown); + }; + }, [openMenu]); const share = useShareLink(project?.id); // Copies a prompt, then opens claude.ai so it can be pasted straight in. @@ -40,76 +58,94 @@ export default function ProjectHeader({ loading = false }) { return (
{currentPage !== 'home' && ( - - )} - {project && currentPage === 'study' && ( - +
+ {currentPage !== 'home' && ( + + )} + {project && currentPage === 'study' && ( + + )} +
)} {project && ( -
- - - +
+ {openMenu === 'tools' && allChunks.length > 0 && ( +
+ + + +
+ )} +
+
+ - {exportMenuOpen && ( + {openMenu === 'export' && (
- - - - - {sharePanelOpen && ( + {openMenu === 'share' && (

Read-only share link

@@ -162,30 +199,30 @@ export default function ProjectHeader({ loading = false }) {

)} -
- {loading ? ( - Loading… - ) : saveStatus ? ( - {saveStatus} - ) : ( -   - )} - {syncStatus === 'syncing' &&
Syncing…
} - {syncStatus === 'synced' &&
Synced ✓
} - {syncStatus === 'error' && ( -
- ⚠ Sync failed - -
- )} -
- +
+ {loading ? ( + Loading… + ) : saveStatus ? ( + {saveStatus} + ) : ( +   + )} + {syncStatus === 'syncing' &&
Syncing…
} + {syncStatus === 'synced' &&
Synced ✓
} + {syncStatus === 'error' && ( +
+ ⚠ Sync failed + +
+ )} +
+
); } diff --git a/src/pages/StudyPage.jsx b/src/pages/StudyPage.jsx index 5c9a65d..1b0e4e6 100644 --- a/src/pages/StudyPage.jsx +++ b/src/pages/StudyPage.jsx @@ -282,34 +282,6 @@ export default function StudyPage() {
- {selectedChunk && ( - <> - -
- {`Chunk ${selectedChunkGlobalIndex + 1} of ${allChunks.length}`} -
- - - )} - {!selectedChunk && ( -
- Choose a chunk to study. -
- )}
{[ { id: 'stacked', label: '☰ Stacked' }, @@ -997,7 +969,7 @@ export default function StudyPage() {
) : (
- Use ‹ Prev / Next › above to pick a chunk. + Add a chunk from ← Back to Chunks to start studying. Select a chunk from the left panel to edit its notes and Greek studies.
)} @@ -1005,7 +977,7 @@ export default function StudyPage() { - {/* Prev / Next — fixed bottom bar, visible while scrolling long chunks */} + {/* Prev / Next — fixed bottom bar, stays visible while scrolling long chunks */} {selectedChunk && (
@@ -1017,8 +989,8 @@ export default function StudyPage() { > ← Previous Chunk - - {selectedChunkGlobalIndex + 1} / {allChunks.length} + + {`Chunk ${selectedChunkGlobalIndex + 1} of ${allChunks.length}`}