Reader: Highlights button in the top bar

A highlighter button beside Bookmarks opens your highlights (by colour);
Bookmarks now opens the bookmarks. Each shows in the accent colour when you
have some. "H" opens the highlights.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-01 16:34:02 -04:00
1 parent 22f9700798
commit a9ca647d5a
6 files changed
+45 -9

No files matched your search

+1
View File
@@ -15,6 +15,7 @@ const BY_PAGE = {
['G', 'Pick a book and chapter'], ['G', 'Pick a book and chapter'],
['B', 'Bookmark the selected verse'], ['B', 'Bookmark the selected verse'],
['C', 'Compare translations'], ['C', 'Compare translations'],
['H', 'Your highlights'],
[',', 'Reading settings'], [',', 'Reading settings'],
['F', 'Hide or show the controls'], ['F', 'Hide or show the controls'],
], ],
+5 -3
View File
@@ -1,6 +1,6 @@
// Everything you have marked in the Bible: highlights (grouped and filtered by colour, each colour // Everything you have marked in the Bible: highlights (grouped and filtered by colour, each colour
// named by you — "Promises", "Commands") and bookmarks. Opens from the bookmark button in the top bar. // named by you — "Promises", "Commands") and bookmarks. Opens from the bookmark button in the top bar.
import { useState } from 'react'; import { useEffect, useState } from 'react';
import Sheet from './Sheet.jsx'; import Sheet from './Sheet.jsx';
import { CloseIcon } from './icons.jsx'; import { CloseIcon } from './icons.jsx';
import { COLOR_NAMES, COLOR_SUGGESTIONS, HIGHLIGHT_COLORS, colorTitle } from '../../utils/highlightColors.js'; import { COLOR_NAMES, COLOR_SUGGESTIONS, HIGHLIGHT_COLORS, colorTitle } from '../../utils/highlightColors.js';
@@ -8,9 +8,11 @@ import { COLOR_NAMES, COLOR_SUGGESTIONS, HIGHLIGHT_COLORS, colorTitle } from '..
const tabBtn = (active) => `rounded-full border px-3 py-1 text-xs font-medium ${active ? 'border-[var(--r-text)] bg-[var(--r-text)] text-[var(--r-bg)]' : 'border-[var(--r-line)] text-[var(--r-sub)] hover:bg-black/5'}`; const tabBtn = (active) => `rounded-full border px-3 py-1 text-xs font-medium ${active ? 'border-[var(--r-text)] bg-[var(--r-text)] text-[var(--r-bg)]' : 'border-[var(--r-line)] text-[var(--r-sub)] hover:bg-black/5'}`;
export default function MarksSheet({ export default function MarksSheet({
open, onClose, bookmarks, highlights, colorLabels, onSetLabel, onJump, onRemoveBookmark, onRemoveHighlight, open, onClose, initialTab = 'highlights', bookmarks, highlights, colorLabels, onSetLabel, onJump, onRemoveBookmark, onRemoveHighlight,
}) { }) {
const [tab, setTab] = useState('highlights'); const [tab, setTab] = useState(initialTab);
// Opening from the Highlights button shows highlights; from the Bookmarks button, bookmarks
useEffect(() => { if (open) setTab(initialTab); }, [open, initialTab]);
const [filter, setFilter] = useState(null); // a highlight colour, or null for all const [filter, setFilter] = useState(null); // a highlight colour, or null for all
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
+6 -3
View File
@@ -1,11 +1,11 @@
// Top and bottom chrome for the reader. Everything stays one tap away; focus // Top and bottom chrome for the reader. Everything stays one tap away; focus
// mode hides both. // mode hides both.
import { ChevronLeft, ChevronRight, ChevronDown, SearchIcon, LinkIcon, PenIcon, HeadphonesIcon, FocusIcon, CheckIcon, CompareIcon } from './icons.jsx'; import { ChevronLeft, ChevronRight, ChevronDown, SearchIcon, LinkIcon, PenIcon, HeadphonesIcon, FocusIcon, CheckIcon, CompareIcon, HighlighterIcon } from './icons.jsx';
import { BookmarkIcon } from '../../utils/bibleUtils.jsx'; import { BookmarkIcon } from '../../utils/bibleUtils.jsx';
const iconBtn = 'flex h-9 w-9 items-center justify-center rounded-lg text-[var(--r-sub)] transition hover:bg-black/5 disabled:opacity-30 disabled:hover:bg-transparent'; const iconBtn = 'flex h-9 w-9 items-center justify-center rounded-lg text-[var(--r-sub)] transition hover:bg-black/5 disabled:opacity-30 disabled:hover:bg-transparent';
export function TopBar({ title, onBack, onOpenNavigator, onSearch, onBookmarks, bookmarkCount, onCompare, compareActive, onSettings, onDoneDrawing }) { export function TopBar({ title, onBack, onOpenNavigator, onSearch, onBookmarks, bookmarkCount, onHighlights, highlightCount, onCompare, compareActive, onSettings, onDoneDrawing }) {
return ( return (
<div className="safe-top sticky top-0 z-30 border-b border-[var(--r-line)] bg-[var(--r-bar)] font-sans"> <div className="safe-top sticky top-0 z-30 border-b border-[var(--r-line)] bg-[var(--r-bar)] font-sans">
<div className="mx-auto flex h-12 max-w-4xl items-center gap-1 px-2"> <div className="mx-auto flex h-12 max-w-4xl items-center gap-1 px-2">
@@ -22,7 +22,10 @@ export function TopBar({ title, onBack, onOpenNavigator, onSearch, onBookmarks,
) : ( ) : (
<> <>
<button type="button" onClick={onSearch} className={iconBtn} aria-label="Search" data-tip="Search (/)"><SearchIcon /></button> <button type="button" onClick={onSearch} className={iconBtn} aria-label="Search" data-tip="Search (/)"><SearchIcon /></button>
<button type="button" onClick={onBookmarks} className={`${iconBtn} relative ${bookmarkCount ? 'text-[var(--r-accent)]' : ''}`} aria-label="Bookmarks" data-tip="Bookmarks and highlights"> <button type="button" onClick={onHighlights} className={`${iconBtn} ${highlightCount ? 'text-[var(--r-accent)]' : ''}`} aria-label="Highlights" data-tip="Your highlights, by colour (H)">
<HighlighterIcon />
</button>
<button type="button" onClick={onBookmarks} className={`${iconBtn} relative ${bookmarkCount ? 'text-[var(--r-accent)]' : ''}`} aria-label="Bookmarks" data-tip="Your bookmarks">
<BookmarkIcon filled={bookmarkCount > 0} /> <BookmarkIcon filled={bookmarkCount > 0} />
</button> </button>
<button type="button" onClick={onCompare} className={`${iconBtn} ${compareActive ? 'text-[var(--r-accent)]' : ''}`} aria-label="Compare translations" aria-pressed={!!compareActive} data-tip="Compare translations (C)"><CompareIcon /></button> <button type="button" onClick={onCompare} className={`${iconBtn} ${compareActive ? 'text-[var(--r-accent)]' : ''}`} aria-label="Compare translations" aria-pressed={!!compareActive} data-tip="Compare translations (C)"><CompareIcon /></button>
+1
View File
@@ -12,3 +12,4 @@ export const HeadphonesIcon = () => <svg {...base}><path d="M3.5 12V10a6.5 6.5 0
export const FocusIcon = () => <svg {...base}><path d="M3 7V4h3M17 7V4h-3M3 13v3h3M17 13v3h-3" /></svg>; export const FocusIcon = () => <svg {...base}><path d="M3 7V4h3M17 7V4h-3M3 13v3h3M17 13v3h-3" /></svg>;
export const CheckIcon = () => <svg {...base}><path d="M4.5 10.5l3.5 3.5 7.5-8" /></svg>; export const CheckIcon = () => <svg {...base}><path d="M4.5 10.5l3.5 3.5 7.5-8" /></svg>;
export const CompareIcon = () => <svg {...base}><rect x="2.5" y="3.5" width="6.5" height="13" rx="1.3" /><rect x="11" y="3.5" width="6.5" height="13" rx="1.3" /><path d="M4.5 7h2.5M4.5 10h2.5M13 7h2.5M13 10h2.5" /></svg>; export const CompareIcon = () => <svg {...base}><rect x="2.5" y="3.5" width="6.5" height="13" rx="1.3" /><rect x="11" y="3.5" width="6.5" height="13" rx="1.3" /><path d="M4.5 7h2.5M4.5 10h2.5M13 7h2.5M13 10h2.5" /></svg>;
export const HighlighterIcon = () => <svg {...base}><path d="M12.5 3.5l4 4-6.8 6.8-4-4 6.8-6.8z" /><path d="M5.7 10.3l-1.6 4.4 1.2 1.2 4.4-1.6" /><path d="M3 17.2h6" /></svg>;
+7 -3
View File
@@ -102,7 +102,7 @@ export default function ReaderPage() {
const parallelChapter = useParallelChapter(parallelId, readerBookAbbrev, readerChapter); const parallelChapter = useParallelChapter(parallelId, readerBookAbbrev, readerChapter);
const parallelSource = READER_TRANSLATIONS.find((t) => t.id === parallelId); const parallelSource = READER_TRANSLATIONS.find((t) => t.id === parallelId);
const [sheet, setSheet] = useState(null); // null | 'nav' | 'settings' | 'search' | 'bookmarks' | 'audio' const [sheet, setSheet] = useState(null); // null | 'nav' | 'settings' | 'search' | 'bookmarks' | 'highlights' | 'audio'
const [chromeVisible, setChromeVisible] = useState(true); const [chromeVisible, setChromeVisible] = useState(true);
const [readerDrawMode, setReaderDrawMode] = useState(false); const [readerDrawMode, setReaderDrawMode] = useState(false);
const [selectionPicker, setSelectionPicker] = useState(null); const [selectionPicker, setSelectionPicker] = useState(null);
@@ -275,6 +275,7 @@ export default function ReaderPage() {
else if (e.key === 'g' || e.key === 'G') { e.preventDefault(); openSheet('nav'); } else if (e.key === 'g' || e.key === 'G') { e.preventDefault(); openSheet('nav'); }
else if (e.key === ',') { e.preventDefault(); openSheet('settings'); } else if (e.key === ',') { e.preventDefault(); openSheet('settings'); }
else if (e.key === 'c' || e.key === 'C') { e.preventDefault(); openSheet('compare'); } else if (e.key === 'c' || e.key === 'C') { e.preventDefault(); openSheet('compare'); }
else if (e.key === 'h' || e.key === 'H') { e.preventDefault(); openSheet('highlights'); }
else if (e.key === 'f' || e.key === 'F') setChromeVisible((v) => !v); else if (e.key === 'f' || e.key === 'F') setChromeVisible((v) => !v);
else if ((e.key === 'b' || e.key === 'B') && selectedKey) { else if ((e.key === 'b' || e.key === 'B') && selectedKey) {
readerBookmarks[selectedKey] ? removeBookmark(selectedKey, readerBookmarks[selectedKey]) : toggleReaderBookmark(selectedKey); readerBookmarks[selectedKey] ? removeBookmark(selectedKey, readerBookmarks[selectedKey]) : toggleReaderBookmark(selectedKey);
@@ -420,7 +421,9 @@ export default function ReaderPage() {
onOpenNavigator={() => openSheet('nav')} onOpenNavigator={() => openSheet('nav')}
onSearch={() => openSheet('search')} onSearch={() => openSheet('search')}
onBookmarks={() => openSheet('bookmarks')} onBookmarks={() => openSheet('bookmarks')}
bookmarkCount={bookmarkEntries.length + highlightEntries.length} bookmarkCount={bookmarkEntries.length}
onHighlights={() => openSheet('highlights')}
highlightCount={highlightEntries.length}
onCompare={() => openSheet('compare')} compareActive={!!parallelId} onCompare={() => openSheet('compare')} compareActive={!!parallelId}
onSettings={() => openSheet('settings')} onSettings={() => openSheet('settings')}
onDoneDrawing={readerDrawMode ? () => setReaderDrawMode(false) : undefined} onDoneDrawing={readerDrawMode ? () => setReaderDrawMode(false) : undefined}
@@ -596,7 +599,8 @@ export default function ReaderPage() {
onJump={jump} onJump={jump}
/> />
<MarksSheet <MarksSheet
open={sheet === 'bookmarks'} onClose={closeSheet} open={sheet === 'bookmarks' || sheet === 'highlights'} onClose={closeSheet}
initialTab={sheet === 'bookmarks' ? 'bookmarks' : 'highlights'}
bookmarks={bookmarkEntries} highlights={highlightEntries} bookmarks={bookmarkEntries} highlights={highlightEntries}
colorLabels={readerColorLabels ?? {}} onSetLabel={setReaderColorLabel} colorLabels={readerColorLabels ?? {}} onSetLabel={setReaderColorLabel}
onJump={jump} onJump={jump}
+25
View File
@@ -117,6 +117,31 @@ test('tapping a verse colour again takes it off, and Clear removes them all (wit
Object.assign(ctx, saved); Object.assign(ctx, saved);
}); });
test('a Highlights button sits in the top bar and opens the highlights; Bookmarks opens the bookmarks', () => {
const saved = { ...ctx };
Object.assign(ctx, {
readerTextHighlights: [{ id: 'h1', book: 'JHN', chapter: 3, verse: 2, startOffset: 0, endOffset: 4, color: '#bbf7d0', text: 'This' }],
readerBookmarks: { 'JHN-3-1': '#fef08a' },
});
render(<ReaderPage />);
const highlights = screen.getByLabelText('Highlights');
expect(highlights.nextElementSibling).toBe(screen.getByLabelText('Bookmarks')); // beside the bookmarks button
fireEvent.click(highlights);
expect(screen.getByRole('tab', { name: /highlights \(1\)/i })).toHaveAttribute('aria-selected', 'true');
expect(screen.getByText(/“This”/)).toBeTruthy();
fireEvent.click(screen.getByLabelText('Close'));
fireEvent.click(screen.getByLabelText('Bookmarks'));
expect(screen.getByRole('tab', { name: /bookmarks \(1\)/i })).toHaveAttribute('aria-selected', 'true');
expect(screen.getByText('John 3:1')).toBeTruthy();
Object.assign(ctx, saved);
});
test('the H key opens the highlights', () => {
render(<ReaderPage />);
fireEvent.keyDown(window, { key: 'h' });
expect(screen.getByRole('dialog', { name: /bookmarks & highlights/i })).toBeTruthy();
});
test('tapping a verse selects it', () => { test('tapping a verse selects it', () => {
render(<ReaderPage />); render(<ReaderPage />);
fireEvent.click(screen.getByText(/This man came to Jesus/)); fireEvent.click(screen.getByText(/This man came to Jesus/));