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' && (
-
- ← Home
-
- )}
- {project && currentPage === 'study' && (
-
setCurrentPage('setup')}
- className="rounded-xl border border-white/15 bg-white/10 px-4 py-2 text-sm text-white transition hover:bg-white/15"
- >
- ← Back to Chunks
-
+
+ {currentPage !== 'home' && (
+
+ ← Home
+
+ )}
+ {project && currentPage === 'study' && (
+ setCurrentPage('setup')}
+ className="rounded-xl border border-white/15 bg-white/10 px-4 py-2 text-sm text-white transition hover:bg-white/15"
+ >
+ ← Back to Chunks
+
+ )}
+
)}
{project && (
-
-
copyPrompt(buildClaudePrompt(project))}
- disabled={allChunks.length === 0}
- className="rounded-md bg-violet-500 px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-violet-400 disabled:cursor-not-allowed disabled:bg-slate-500"
- title="Copies your study notes as a Claude prompt, then opens claude.ai so you can paste and go"
- >
- Study Guide → Claude ↗
-
-
copyPrompt(buildPodcastPrompt(project, authUser?.podcastName))}
- disabled={allChunks.length === 0}
- title="Copies a podcast script prompt for Claude, then opens claude.ai — set your show name in Account Settings first"
- className="rounded-md bg-fuchsia-500 px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-fuchsia-400 disabled:cursor-not-allowed disabled:bg-slate-500"
- >
- 🎙 Podcast → Claude ↗
-
-
setCurrentPage('pronunciation')}
- disabled={allChunks.length === 0}
- title="Open the pronunciation guide — all Greek & Hebrew words with transliterations"
- className="rounded-md bg-amber-500 px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-amber-400 disabled:cursor-not-allowed disabled:bg-slate-500"
- >
- 🗣 Pronunciation Guide
-
+
setExportMenuOpen((v) => !v)}
+ onClick={() => toggleMenu('tools')}
+ aria-expanded={openMenu === 'tools'}
+ disabled={allChunks.length === 0}
+ title={allChunks.length === 0 ? 'Add a chunk first to use these tools' : undefined}
+ className="flex items-center gap-1.5 rounded-md bg-violet-500 px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-violet-400 disabled:cursor-not-allowed disabled:bg-slate-500 disabled:opacity-60 disabled:shadow-none"
+ >
+ 🧰 Tools
+
+ {openMenu === 'tools' && allChunks.length > 0 && (
+
+ { copyPrompt(buildClaudePrompt(project)); closeMenu(); }}
+ title="Copies your study notes as a Claude prompt, then opens claude.ai so you can paste and go"
+ className="block w-full px-4 py-2 text-left text-sm text-slate-700 hover:bg-slate-50"
+ >
+ Study Guide → Claude ↗
+
+ { copyPrompt(buildPodcastPrompt(project, authUser?.podcastName)); closeMenu(); }}
+ title="Copies a podcast script prompt for Claude, then opens claude.ai — set your show name in Account Settings first"
+ className="block w-full px-4 py-2 text-left text-sm text-slate-700 hover:bg-slate-50"
+ >
+ 🎙 Podcast → Claude ↗
+
+ { setCurrentPage('pronunciation'); closeMenu(); }}
+ title="Open the pronunciation guide — all Greek & Hebrew words with transliterations"
+ className="block w-full px-4 py-2 text-left text-sm text-slate-700 hover:bg-slate-50"
+ >
+ 🗣 Pronunciation Guide
+
+
+ )}
+
+
+
toggleMenu('export')}
+ aria-expanded={openMenu === 'export'}
className="flex items-center gap-1.5 rounded-md bg-sky-500 px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-sky-400"
>
⬇ Export
- {exportMenuOpen && (
+ {openMenu === 'export' && (
-
{ exportProjectHtml(project); setExportMenuOpen(false); }}
+ { exportProjectHtml(project); closeMenu(); }}
className="block w-full px-4 py-2 text-left text-sm text-slate-700 hover:bg-slate-50">
Export HTML
- { exportProjectDocx(project); setExportMenuOpen(false); }}
+ { exportProjectDocx(project); closeMenu(); }}
className="block w-full px-4 py-2 text-left text-sm text-slate-700 hover:bg-slate-50">
Export DOCX
- { exportProjectMarkdown(project); setExportMenuOpen(false); }}
+ { exportProjectMarkdown(project); closeMenu(); }}
title="Handy for Obsidian, Notion, or any Markdown-based notes app"
className="block w-full px-4 py-2 text-left text-sm text-slate-700 hover:bg-slate-50">
Export Markdown
- { printProjectPdf(project); setExportMenuOpen(false); }}
+ { printProjectPdf(project); closeMenu(); }}
title="Opens a print-friendly version in a new tab — choose 'Save as PDF' in the print dialog"
className="block w-full px-4 py-2 text-left text-sm text-slate-700 hover:bg-slate-50">
🖨 Print / Save PDF
@@ -120,12 +156,13 @@ export default function ProjectHeader({ loading = false }) {
setSharePanelOpen((v) => !v)}
+ onClick={() => toggleMenu('share')}
+ aria-expanded={openMenu === 'share'}
className={`rounded-md px-4 py-2 text-sm font-medium shadow-sm transition ${share.shareToken ? 'bg-teal-600 text-white hover:bg-teal-500' : 'border border-slate-300 bg-white text-slate-700 hover:bg-slate-50'}`}
>
🔗 {share.shareToken ? 'Shared' : 'Share'}
- {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
-
- Retry
-
-
- )}
-
-
+
+ {loading ? (
+
Loading…
+ ) : saveStatus ? (
+
{saveStatus}
+ ) : (
+
+ )}
+ {syncStatus === 'syncing' &&
Syncing…
}
+ {syncStatus === 'synced' &&
Synced ✓
}
+ {syncStatus === 'error' && (
+
+ ⚠ Sync failed
+
+ Retry
+
+
+ )}
+
+
);
}
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 && (
- <>
-
- ‹ Prev
-
-
- {`Chunk ${selectedChunkGlobalIndex + 1} of ${allChunks.length}`}
-
-
= allChunks.length - 1}
- className="rounded-2xl border border-slate-300 bg-white px-3 py-2 text-sm font-semibold text-slate-700 transition hover:border-slate-400 disabled:cursor-not-allowed disabled:opacity-40"
- >
- Next ›
-
- >
- )}
- {!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}`}