Hover tooltips, clickable cross-references, easier highlighting

- Tooltips on hover and keyboard focus across the app: icon-only buttons show
  their label, existing titles show in the app's style without the browser's
  delay, and the reader's main buttons show shortcut hints ("Search (/)").
  Touch screens are unaffected.
- Reader: cross-references (under verses and in the Cross-refs tab) open the
  passage's text in the side panel without leaving the chapter; "Open chapter"
  goes there when wanted
- Reader: the verse panel now has a Highlight row of colours for one-tap
  whole-verse highlighting (tap again to remove, Clear for all, with Undo);
  selecting words still highlights just those
- Fix: a cross-reference preview was cleared when React ran the panel's
  effect twice (development builds)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-01 16:31:27 -04:00
1 parent bb80623c45
commit 7f0256be06
9 files changed
+394 -28

No files matched your search

+112
View File
@@ -0,0 +1,112 @@
// Hover (and keyboard-focus) tooltips for the whole app, without touching every button:
// - anything with data-tip="…" shows that text (used for shortcut hints like "Search (/)")
// - anything with title="…" shows it too, in the app's style and without the browser's long delay
// - an icon-only button (no visible words) shows its aria-label
// Touch screens have no hover, so nothing appears there.
import { useEffect, useRef, useState } from 'react';
import { themeVars } from './reader/readerThemes.js';
const SHOW_DELAY = 450;
const SELECTOR = '[data-tip], [title], button[aria-label], a[aria-label], [role="button"][aria-label], [role="tab"][aria-label]';
const canHover = () => (typeof window.matchMedia === 'function' ? window.matchMedia('(hover: hover)').matches : true);
// The words a tooltip should show for an element, or '' for none. A title is moved to data-tip the first
// time (so the browser's own tooltip doesn't also appear); a title on a button with no other name becomes
// its aria-label so screen readers keep it.
export function tipFor(el) {
if (!(el instanceof HTMLElement)) return '';
if (el.hasAttribute('title')) {
const title = el.getAttribute('title').trim();
if (title) {
if (!el.getAttribute('aria-label') && el.textContent.replace(/\s+/g, '').length <= 2) el.setAttribute('aria-label', title);
if (!el.hasAttribute('data-tip')) el.setAttribute('data-tip', title);
}
el.removeAttribute('title');
}
const tip = el.getAttribute('data-tip');
if (tip) return tip;
// Icon-only controls (little or no text of their own) explain themselves with their label
const label = el.getAttribute('aria-label');
const visible = el.textContent.replace(/\s+/g, '').length;
return label && visible <= 2 ? label : '';
}
export default function Tooltips({ theme }) {
const [tip, setTip] = useState(null); // { text, x, y, below }
const timer = useRef(null);
const current = useRef(null);
const box = useRef(null);
useEffect(() => {
const hide = () => {
window.clearTimeout(timer.current);
if (current.current) current.current.removeAttribute('aria-describedby');
current.current = null;
setTip(null);
};
const show = (el, text) => {
const r = el.getBoundingClientRect();
current.current = el;
el.setAttribute('aria-describedby', 'app-tooltip');
setTip({ text, x: r.left + r.width / 2, y: r.top, bottom: r.bottom });
};
const find = (node) => (node instanceof Element ? node.closest(SELECTOR) : null);
const onOver = (e) => {
if (!canHover()) return;
const el = find(e.target);
if (!el || el === current.current) return;
hide();
const text = tipFor(el);
if (!text || el.disabled) return;
timer.current = window.setTimeout(() => show(el, text), SHOW_DELAY);
};
const onOut = (e) => {
const el = find(e.target);
if (!el || (e.relatedTarget instanceof Node && el.contains(e.relatedTarget))) return;
hide();
};
const onFocus = (e) => {
const el = find(e.target);
if (!el || !el.matches(':focus-visible')) return; // only keyboard focus, not a mouse click
const text = tipFor(el);
if (text) { window.clearTimeout(timer.current); show(el, text); }
};
const onKey = (e) => { if (e.key === 'Escape') hide(); };
document.addEventListener('pointerover', onOver);
document.addEventListener('pointerout', onOut);
document.addEventListener('pointerdown', hide, true);
document.addEventListener('focusin', onFocus);
document.addEventListener('focusout', hide);
document.addEventListener('keydown', onKey);
window.addEventListener('scroll', hide, true);
window.addEventListener('resize', hide);
return () => {
window.clearTimeout(timer.current);
document.removeEventListener('pointerover', onOver);
document.removeEventListener('pointerout', onOut);
document.removeEventListener('pointerdown', hide, true);
document.removeEventListener('focusin', onFocus);
document.removeEventListener('focusout', hide);
document.removeEventListener('keydown', onKey);
window.removeEventListener('scroll', hide, true);
window.removeEventListener('resize', hide);
};
}, []);
if (!tip) return null;
// Centred over the control, kept inside the window, and flipped below it when there's no room above
const width = box.current?.offsetWidth ?? 0;
const left = Math.max(8, Math.min(tip.x - width / 2, window.innerWidth - width - 8));
const flip = tip.y < 40;
return (
<div
ref={box} id="app-tooltip" role="tooltip" style={{ ...themeVars(theme), left, top: flip ? tip.bottom + 8 : tip.y - 8, transform: flip ? 'none' : 'translateY(-100%)' }}
className="pointer-events-none fixed z-[120] max-w-[16rem] rounded-lg bg-[var(--r-text)] px-2.5 py-1.5 font-sans text-xs leading-snug text-[var(--r-bg)] shadow-lg"
>
{tip.text}
</div>
);
}
+88
View File
@@ -0,0 +1,88 @@
import { render, screen, fireEvent, act } from '@testing-library/react';
import { vi, beforeEach, afterEach, test, expect } from 'vitest';
import Tooltips, { tipFor } from './Tooltips.jsx';
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
const over = (el) => act(() => { fireEvent.pointerOver(el); vi.advanceTimersByTime(600); });
function setup() {
render(
<>
<button aria-label="Search" data-tip="Search (/)">🔍</button>
<button aria-label="Close">×</button>
<button title="Out a level (Shift+Tab)">←</button>
<button aria-label="Save changes">Save changes</button>
<button disabled aria-label="Nothing">·</button>
<Tooltips theme="paper" />
</>,
);
}
test('shows a data-tip after a short pause, and hides when the pointer leaves', () => {
setup();
const btn = screen.getByLabelText('Search');
over(btn);
expect(screen.getByRole('tooltip')).toHaveTextContent('Search (/)');
expect(btn).toHaveAttribute('aria-describedby', 'app-tooltip');
act(() => { fireEvent.pointerOut(btn); });
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
expect(btn).not.toHaveAttribute('aria-describedby');
});
test('does not appear before the pause', () => {
setup();
act(() => { fireEvent.pointerOver(screen.getByLabelText('Search')); vi.advanceTimersByTime(100); });
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
});
test('an icon-only button shows its label', () => {
setup();
over(screen.getByLabelText('Close'));
expect(screen.getByRole('tooltip')).toHaveTextContent('Close');
});
test('a title is shown in the app style and the browser\'s own tooltip is turned off', () => {
setup();
const btn = screen.getByText('←');
over(btn);
expect(screen.getByRole('tooltip')).toHaveTextContent('Out a level (Shift+Tab)');
expect(btn).not.toHaveAttribute('title');
expect(btn).toHaveAttribute('aria-label', 'Out a level (Shift+Tab)'); // a screen reader still gets its name
});
test('a button that already says what it is gets no tooltip, and neither does a disabled one', () => {
setup();
over(screen.getByText('Save changes'));
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
over(screen.getByLabelText('Nothing'));
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
});
test('clicking, scrolling or Escape dismisses it', () => {
setup();
const btn = screen.getByLabelText('Search');
over(btn);
act(() => { fireEvent.pointerDown(btn); });
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
over(btn);
act(() => { fireEvent.keyDown(document, { key: 'Escape' }); });
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
over(btn);
act(() => { fireEvent.scroll(window); });
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
});
test('nothing shows on a device without hover (touch)', () => {
vi.stubGlobal('matchMedia', () => ({ matches: false }));
setup();
over(screen.getByLabelText('Search'));
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
vi.unstubAllGlobals();
});
test('tipFor ignores things that are not elements', () => {
expect(tipFor(null)).toBe('');
expect(tipFor(document.createTextNode('x'))).toBe('');
});
+12 -12
View File
@@ -9,9 +9,9 @@ export function TopBar({ title, onBack, onOpenNavigator, onSearch, onBookmarks,
return (
<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">
<button type="button" onClick={onBack} className="flex h-9 items-center gap-0.5 rounded-lg pl-1 pr-2 text-[var(--r-sub)] transition hover:bg-black/5" aria-label="Back to home"><ChevronLeft /><span className="hidden text-xs sm:inline">Home</span></button>
<button type="button" onClick={onBack} className="flex h-9 items-center gap-0.5 rounded-lg pl-1 pr-2 text-[var(--r-sub)] transition hover:bg-black/5" aria-label="Back to home" data-tip="Back to home"><ChevronLeft /><span className="hidden text-xs sm:inline">Home</span></button>
<button type="button" onClick={onOpenNavigator}
className="mx-auto flex min-w-0 items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium text-[var(--r-text)] hover:bg-black/5">
className="mx-auto flex min-w-0 items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium text-[var(--r-text)] hover:bg-black/5" data-tip="Go to a book and chapter (G)">
<span className="truncate">{title}</span><ChevronDown />
</button>
{onDoneDrawing ? (
@@ -21,12 +21,12 @@ export function TopBar({ title, onBack, onOpenNavigator, onSearch, onBookmarks,
</button>
) : (
<>
<button type="button" onClick={onSearch} className={iconBtn} aria-label="Search"><SearchIcon /></button>
<button type="button" onClick={onBookmarks} className={`${iconBtn} relative ${bookmarkCount ? 'text-[var(--r-accent)]' : ''}`} aria-label="Bookmarks">
<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">
<BookmarkIcon filled={bookmarkCount > 0} />
</button>
<button type="button" onClick={onCompare} className={`${iconBtn} ${compareActive ? 'text-[var(--r-accent)]' : ''}`} aria-label="Compare translations" aria-pressed={!!compareActive} title="Compare translations"><CompareIcon /></button>
<button type="button" onClick={onSettings} className={`${iconBtn} w-10 text-sm font-medium`} aria-label="Reading settings">Aa</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={onSettings} className={`${iconBtn} w-10 text-sm font-medium`} aria-label="Reading settings" data-tip="Text size, typeface and theme (,)">Aa</button>
</>
)}
</div>
@@ -42,11 +42,11 @@ export function BottomBar({
return (
<div className="fixed inset-x-0 bottom-0 z-30 border-t border-[var(--r-line)] bg-[var(--r-bar)] pb-[env(safe-area-inset-bottom)] font-sans">
<div className="mx-auto flex h-14 max-w-4xl items-center gap-0.5 px-2">
<button type="button" onClick={onPrev} disabled={prevDisabled} className={iconBtn} aria-label="Previous chapter"><ChevronLeft /></button>
<button type="button" onClick={onAudio} className={`${iconBtn} ${audioActive ? active : ''}`} aria-label="Listen"><HeadphonesIcon /></button>
<button type="button" onClick={onPen} className={iconBtn} aria-label="Draw notes"><PenIcon /></button>
<button type="button" onClick={onCrossRefs} className={`${iconBtn} ${crossRefsOn ? active : ''}`} aria-label="Show cross-references under verses"><LinkIcon /></button>
<button type="button" onClick={onFocus} className={iconBtn} aria-label="Hide controls"><FocusIcon /></button>
<button type="button" onClick={onPrev} disabled={prevDisabled} className={iconBtn} aria-label="Previous chapter" data-tip="Previous chapter (←)"><ChevronLeft /></button>
<button type="button" onClick={onAudio} className={`${iconBtn} ${audioActive ? active : ''}`} aria-label="Listen" data-tip="Listen to this chapter"><HeadphonesIcon /></button>
<button type="button" onClick={onPen} className={iconBtn} aria-label="Draw notes" data-tip="Draw notes on this page"><PenIcon /></button>
<button type="button" onClick={onCrossRefs} className={`${iconBtn} ${crossRefsOn ? active : ''}`} aria-label="Show cross-references under verses" data-tip="Show cross-references under each verse"><LinkIcon /></button>
<button type="button" onClick={onFocus} className={iconBtn} aria-label="Hide controls" data-tip="Hide the controls (F)"><FocusIcon /></button>
<div className="mx-auto" />
{primary && (
<button type="button" onClick={primary.onClick}
@@ -54,7 +54,7 @@ export function BottomBar({
{primary.done ? null : <CheckIcon />}{primary.label}
</button>
)}
<button type="button" onClick={onNext} disabled={nextDisabled} className={iconBtn} aria-label="Next chapter"><ChevronRight /></button>
<button type="button" onClick={onNext} disabled={nextDisabled} className={iconBtn} aria-label="Next chapter" data-tip="Next chapter (→)"><ChevronRight /></button>
</div>
</div>
);
+6 -2
View File
@@ -18,7 +18,7 @@ export default function ReaderText({
title, translation, translationId, notice, verses, loading, error, layout, fontSize, wide,
font = 'serif', spacing = 'normal', showVerseNumbers = true, parallel = null, showDiff = true,
bookmarks, bookKey, chapter, selectedVerse, onSelectVerse, renderVerseText, searchLower,
crossRefs, showCrossRefs, onSelectionEnd, footer,
crossRefs, showCrossRefs, onOpenCrossRef, onSelectionEnd, footer,
}) {
// How much the second Bible's wording differs from the first, over the chapter and verse by verse
const diff = useMemo(
@@ -56,7 +56,11 @@ export default function ReaderText({
const chips = (v) => showCrossRefs && crossRefs?.[v.number]?.length > 0 && (
<span className="ml-1 inline-flex flex-wrap gap-1 align-baseline font-sans">
{crossRefs[v.number].slice(0, 4).map((ref, i) => (
<span key={i} className="rounded-full border border-[var(--r-line)] px-1.5 py-px text-[10px] text-[var(--r-sub)]">{formatCrossRef(ref)}</span>
// Opens that passage in the side panel; stopPropagation so the verse itself isn't also toggled
<button key={i} type="button" onClick={(e) => { e.stopPropagation(); onOpenCrossRef?.(v.number, ref); }}
onKeyDown={(e) => e.stopPropagation()}
className="rounded-full border border-[var(--r-line)] px-1.5 py-px text-[10px] text-[var(--r-sub)] hover:border-[var(--r-accent)] hover:text-[var(--r-accent)]"
data-tip={`Read ${formatCrossRef(ref)} in the side panel`}>{formatCrossRef(ref)}</button>
))}
</span>
);
+95 -12
View File
@@ -1,6 +1,6 @@
// The single home for per-verse tools: original words, cross-references,
// commentary, and people & places, plus bookmark / copy.
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import Sheet from './Sheet.jsx';
import { CommentaryTab, PeoplePlacesTab } from '../StudyPanel.jsx';
import { BookmarkIcon, CopyIcon } from '../../utils/bibleUtils.jsx';
@@ -8,6 +8,7 @@ import { formatCrossRef, speakOriginalWord } from '../../utils/wordStudy.js';
import { READER_TRANSLATIONS, fetchChapter } from '../../utils/bibleText.js';
import { diffVerses, differenceLabel } from '../../utils/textDiff.js';
import { DiffText } from './DiffText.jsx';
import { HIGHLIGHT_COLORS, colorTitle } from '../../utils/highlightColors.js';
const TABS = [['commentary', 'Commentary'], ['compare', 'Compare'], ['original', 'Original'], ['refs', 'Cross-refs'], ['people', 'People & places']];
@@ -37,18 +38,59 @@ function OriginalTab({ words }) {
);
}
function RefsTab({ refs, loading, onJump }) {
// A cross-referenced passage shown in the panel: its words in the translation being read, without
// leaving the chapter. "Open chapter" goes there when you want more.
function RefPreview({ refItem, translation, onOpen, onClose }) {
const [state, setState] = useState({ key: '', verses: [], error: '' });
const key = `${translation}/${refItem.book}/${refItem.chapter}`;
useEffect(() => {
const controller = new AbortController();
fetchChapter(translation, refItem.book, refItem.chapter, { signal: controller.signal })
.then(({ verses }) => setState({ key, verses, error: '' }))
.catch((err) => { if (!controller.signal.aborted) setState({ key, verses: [], error: err.message || 'Unable to load that passage.' }); });
return () => controller.abort();
}, [key]); // eslint-disable-line react-hooks/exhaustive-deps
const end = refItem.endVerse && refItem.endVerse > refItem.verse ? refItem.endVerse : refItem.verse;
const picked = state.key === key ? state.verses.filter((v) => v.number >= refItem.verse && v.number <= end) : [];
const loading = state.key !== key;
return (
<div className="mb-3 rounded-xl border border-[var(--r-line)] bg-[var(--r-bg)] p-3" role="region" aria-label={`Cross-reference ${formatCrossRef(refItem)}`}>
<div className="mb-1.5 flex items-center justify-between gap-2">
<p className="text-sm font-medium">{formatCrossRef(refItem)} <span className="text-xs font-normal text-[var(--r-sub)]">{translation}</span></p>
<button type="button" onClick={onClose} aria-label="Close cross-reference" className="rounded p-1 text-[var(--r-sub)] hover:bg-black/5">×</button>
</div>
{loading ? <p className="text-sm text-[var(--r-sub)]">Loading…</p>
: state.error ? <p className="text-sm text-[var(--r-danger,#b42318)]">{state.error}</p>
: picked.length === 0 ? <p className="text-sm text-[var(--r-sub)]">Those verses weren’t found.</p>
: (
<p className="font-serif text-[15px] leading-relaxed">
{picked.map((v) => <span key={v.number}>{picked.length > 1 && <sup className="mr-0.5 font-sans text-[0.65em] text-[var(--r-sub)]">{v.number}</sup>}{v.text}{' '}</span>)}
</p>
)}
<button type="button" onClick={() => onOpen(refItem.book, refItem.chapter, refItem.verse)} className="mt-2 text-xs font-medium text-[var(--r-accent)] hover:underline">Open chapter</button>
</div>
);
}
const sameRef = (a, b) => a && b && a.book === b.book && a.chapter === b.chapter && a.verse === b.verse && (a.endVerse ?? a.verse) === (b.endVerse ?? b.verse);
function RefsTab({ refs, loading, active, onPick, preview }) {
if (loading && !refs) return <p className="text-sm text-[var(--r-sub)]">Loading…</p>;
if (!refs?.length) return <p className="text-sm text-[var(--r-sub)]">No cross-references for this verse.</p>;
return (
<div className="flex flex-wrap gap-1.5">
{refs.slice(0, 20).map((ref, i) => (
<button key={i} type="button" onClick={() => onJump(ref.book, ref.chapter, ref.verse)}
className="rounded-full border border-[var(--r-line)] px-2.5 py-1 text-xs hover:bg-black/5">
{formatCrossRef(ref)}
</button>
))}
</div>
<>
{preview}
<p className="mb-1.5 text-[11px] text-[var(--r-sub)]">Tap one to read it here.</p>
<div className="flex flex-wrap gap-1.5">
{refs.slice(0, 20).map((ref, i) => (
<button key={i} type="button" onClick={() => onPick(ref)} aria-pressed={sameRef(active, ref)}
className={`rounded-full border px-2.5 py-1 text-xs ${sameRef(active, ref) ? 'border-[var(--r-text)] bg-[var(--r-text)] text-[var(--r-bg)]' : 'border-[var(--r-line)] hover:bg-black/5'}`}>
{formatCrossRef(ref)}
</button>
))}
</div>
</>
);
}
@@ -99,10 +141,27 @@ function CompareTab({ book, chapter, verse, current }) {
export default function VerseSheet({
verse, book, bookName, chapter, translation, words, crossRefs, crossRefsLoading, onLoadCrossRefs,
bookmarkColor, onToggleBookmark, onCycleColor, onJump, onClose,
bookmarkColor, onToggleBookmark, onCycleColor, onJump, onClose, request,
verseHighlights = [], colorLabels = {}, onHighlightVerse, onClearVerseHighlights,
}) {
const [tab, setTab] = useState('commentary');
const [copied, setCopied] = useState(false);
const [preview, setPreview] = useState(null); // the cross-reference being read in the panel
const appliedRequest = useRef(0);
// Leaving this verse closes the preview; a cross-reference tapped in the text opens it here. (Tracks the
// previous verse so React's double-run of effects in development can't clear a preview it just opened.)
const previousVerse = useRef(null);
useEffect(() => {
const verseChanged = previousVerse.current !== verse?.number;
previousVerse.current = verse?.number;
if (request && request.verse === verse?.number && request.n !== appliedRequest.current) {
appliedRequest.current = request.n;
setTab('refs');
setPreview(request.ref);
} else if (verseChanged) {
setPreview(null);
}
}, [verse?.number, request?.n]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => { if (verse && tab === 'refs') onLoadCrossRefs(); }, [verse?.number, tab]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => { setCopied(false); }, [verse?.number]);
if (!verse) return null;
@@ -134,6 +193,27 @@ export default function VerseSheet({
</>
)}
>
{onHighlightVerse && (
// One-tap highlighting of the whole verse (to mark just some words, select them in the text)
<div className="mb-3 flex flex-wrap items-center gap-1.5" role="group" aria-label="Highlight this verse">
<span className="mr-1 text-xs text-[var(--r-sub)]">Highlight</span>
{HIGHLIGHT_COLORS.map((color) => {
const on = verseHighlights.some((h) => h.color === color && h.startOffset === 0 && h.endOffset >= verse.text.length);
return (
<button key={color} type="button" onClick={() => onHighlightVerse(color)} aria-pressed={on}
aria-label={`Highlight verse ${colorTitle(color, colorLabels).toLowerCase()}`} data-tip={`${colorTitle(color, colorLabels)}${on ? ' — tap to remove' : ''}`}
className={`h-6 w-6 rounded-full border-2 transition hover:scale-110 ${on ? 'border-[var(--r-text)]' : 'border-transparent ring-1 ring-black/10'}`}
style={{ backgroundColor: color }}>
{on && <span className="text-[10px] leading-none text-[#2b2620]" aria-hidden="true">✓</span>}
</button>
);
})}
{verseHighlights.length > 0 && (
<button type="button" onClick={onClearVerseHighlights} className="ml-1 text-xs text-[var(--r-sub)] underline hover:text-[var(--r-text)]">Clear</button>
)}
<span className="basis-full text-[11px] text-[var(--r-sub)]">Or select words in the text to highlight just those.</span>
</div>
)}
<div role="tablist" aria-label="About this verse" className="mb-3 flex gap-1.5 overflow-x-auto pb-1">
{TABS.map(([id, label]) => (
<button key={id} type="button" role="tab" aria-selected={tab === id} onClick={() => setTab(id)}
@@ -145,7 +225,10 @@ export default function VerseSheet({
{tab === 'commentary' && <CommentaryTab book={book} chapter={chapter} verse={verse.number} />}
{tab === 'compare' && <CompareTab book={book} chapter={chapter} verse={verse.number} current={translation} />}
{tab === 'original' && <OriginalTab words={words} />}
{tab === 'refs' && <RefsTab refs={crossRefs} loading={crossRefsLoading} onJump={onJump} />}
{tab === 'refs' && (
<RefsTab refs={crossRefs} loading={crossRefsLoading} active={preview} onPick={setPreview}
preview={preview && <RefPreview refItem={preview} translation={translation} onOpen={onJump} onClose={() => setPreview(null)} />} />
)}
{tab === 'people' && <PeoplePlacesTab book={book} chapter={chapter} verse={verse.number} />}
</Sheet>
);
+1 -1
View File
@@ -83,7 +83,7 @@ export default function PageShell({ title, onBack, actions, active = null, maxWi
</nav>
<div className="ml-auto flex items-center gap-2">
{openPalette && (
<button type="button" onClick={openPalette} aria-label="Search and go to"
<button type="button" onClick={openPalette} aria-label="Search and go to" data-tip="Search studies, notes and passages (⌘K)"
className="flex items-center gap-2 rounded-full border border-[var(--r-line)] bg-[var(--r-bg)] px-3 py-1 text-xs text-[var(--r-sub)] hover:bg-black/5">
<SearchIcon /><span className="hidden sm:inline">Search</span><kbd className="hidden rounded border border-[var(--r-line)] px-1 text-[10px] md:inline">⌘K</kbd>
</button>