App quality: offline badge, per-note history, keyboard study mode, notes import
- Study cards say whether the Bible text works offline, with a button to save the missing book (notes are always kept on the device). - Each note keeps its last 12 versions with Restore (History under each note). - Keyboard-only study: 1/2/3 write in Observe/Interpret/Apply, C/W/X/P switch resource tabs, V compares, Esc leaves a note box; listed under "?". - Export > Import notes: bring Word/Markdown/text notes into the matching chunks without overwriting anything; can mark them as not your own. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
648c3206df
commit
67645cf1df
14 files changed
+421
-7
No files matched your search
@@ -1,9 +1,10 @@
|
||||
// Observe / Interpret / Apply as an accordion (one open at a time), plus a "More"
|
||||
// area for background notes and tags so the page stays quiet.
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import SessionNotes from './SessionNotes.jsx';
|
||||
import FirstDraft from './FirstDraft.jsx';
|
||||
import { usePasteCheck } from './PastePrompt.jsx';
|
||||
import { listNoteVersions, recordNoteVersion } from '../../utils/noteHistory.js';
|
||||
import DictateButton, { appendSpoken } from '../DictateButton.jsx';
|
||||
import { ownShare, ownWords, pastedWords, clearPastes } from '../../utils/ownWork.js';
|
||||
import { templateFromChunk, templatePrompt } from '../../utils/studyTemplates.js';
|
||||
@@ -20,11 +21,25 @@ const SECTIONS = [
|
||||
const areaCls = 'w-full resize-y rounded-xl border border-[var(--r-line)] bg-[var(--r-bg)] px-3.5 py-3 font-serif text-[15px] leading-7 text-[var(--r-text)] placeholder:font-sans placeholder:text-[13px] placeholder:leading-5 placeholder:text-[var(--r-sub)] focus:outline-none focus:ring-2 focus:ring-[var(--r-line)]';
|
||||
const words = (s) => (s ?? '').trim().split(/\s+/).filter(Boolean).length;
|
||||
|
||||
export default function NotesPane({ chunk, updateChunk, allTags, templateId, saveTemplate, notify, onPray }) {
|
||||
export default function NotesPane({ chunk, updateChunk, allTags, templateId, saveTemplate, notify, onPray, focusRequest }) {
|
||||
const [open, setOpen] = useState(() => {
|
||||
try { return localStorage.getItem('studyNotesOpen') || 'observation'; } catch { return 'observation'; }
|
||||
});
|
||||
const [more, setMore] = useState(false);
|
||||
// A keyboard jump (1 / 2 / 3 on the Study page): open that note and put the cursor in it.
|
||||
const areas = useRef({});
|
||||
const wanted = useRef(null);
|
||||
useEffect(() => {
|
||||
if (!focusRequest) return;
|
||||
wanted.current = focusRequest.field;
|
||||
setOpen(focusRequest.field);
|
||||
}, [focusRequest]);
|
||||
useEffect(() => {
|
||||
if (wanted.current && wanted.current === open) {
|
||||
const el = areas.current[open];
|
||||
if (el) { el.focus(); const end = el.value.length; el.setSelectionRange(end, end); wanted.current = null; }
|
||||
}
|
||||
});
|
||||
const [tplName, setTplName] = useState('');
|
||||
const [tplOpen, setTplOpen] = useState(false);
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
@@ -42,6 +57,25 @@ export default function NotesPane({ chunk, updateChunk, allTags, templateId, sav
|
||||
|
||||
const share = ownShare(chunk);
|
||||
const pasteCheck = usePasteCheck(chunk, updateChunk);
|
||||
// Earlier versions of one note: kept when you start and stop editing it; "History" lists them with Restore.
|
||||
const [historyField, setHistoryField] = useState(null);
|
||||
const [versions, setVersions] = useState([]);
|
||||
useEffect(() => { setHistoryField(null); }, [chunk.id]);
|
||||
const keepVersion = (field) => {
|
||||
const list = recordNoteVersion(chunk.id, field, chunk[field]);
|
||||
if (historyField === field) setVersions(list);
|
||||
};
|
||||
const openHistory = (field) => {
|
||||
if (historyField === field) { setHistoryField(null); return; }
|
||||
recordNoteVersion(chunk.id, field, chunk[field]);
|
||||
setVersions(listNoteVersions(chunk.id, field));
|
||||
setHistoryField(field);
|
||||
};
|
||||
const restore = (field, text) => {
|
||||
recordNoteVersion(chunk.id, field, chunk[field]); // so restoring can itself be undone
|
||||
updateChunk(chunk.id, { [field]: text });
|
||||
setVersions(listNoteVersions(chunk.id, field));
|
||||
};
|
||||
|
||||
return (
|
||||
<section aria-label="Study notes" className="space-y-2">
|
||||
@@ -72,9 +106,21 @@ export default function NotesPane({ chunk, updateChunk, allTags, templateId, sav
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="px-4 pb-4">
|
||||
<textarea aria-label={label} value={chunk[field] ?? ''} rows={7} placeholder={templatePrompt(templateId, field) ?? placeholder}
|
||||
onPaste={pasteCheck.onPaste(field)} onChange={(e) => updateChunk(chunk.id, { [field]: e.target.value })} className={areaCls} />
|
||||
<textarea ref={(el) => { areas.current[field] = el; }} aria-label={label} value={chunk[field] ?? ''} rows={7} placeholder={templatePrompt(templateId, field) ?? placeholder}
|
||||
onFocus={() => keepVersion(field)} onBlur={() => keepVersion(field)} onPaste={pasteCheck.onPaste(field)} onChange={(e) => updateChunk(chunk.id, { [field]: e.target.value })} className={areaCls} />
|
||||
{pasteCheck.prompt(field)}
|
||||
<button type="button" onClick={() => openHistory(field)} aria-expanded={historyField === field} className="mt-1.5 mr-3 text-xs text-[var(--r-sub)] hover:text-[var(--r-text)]">History</button>
|
||||
{historyField === field && (
|
||||
<ul aria-label={`${label} history`} className="mt-1.5 space-y-1.5 rounded-xl border border-[var(--r-line)] p-2 text-xs">
|
||||
{versions.length === 0 && <li className="text-[var(--r-sub)]">No earlier versions yet. They are saved as you edit.</li>}
|
||||
{versions.map((v) => (
|
||||
<li key={v.at} className="flex items-baseline justify-between gap-3">
|
||||
<span className="min-w-0"><span className="text-[var(--r-sub)]">{new Date(v.at).toLocaleString([], { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })} · {words(v.text)} words · </span><span className="font-serif">{v.text.length > 70 ? `${v.text.slice(0, 70)}…` : v.text}</span></span>
|
||||
<button type="button" onClick={() => restore(field, v.text)} disabled={v.text === chunk[field]} className="shrink-0 text-[var(--r-accent)] hover:underline disabled:opacity-40 disabled:no-underline">Restore</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{pasted > 0 && (
|
||||
<p className="mt-1.5 text-xs text-[var(--r-sub)]">
|
||||
{pasted} word{pasted === 1 ? '' : 's'} here were pasted, so they don't count as your own work.{' '}
|
||||
|
||||
Reference in new issue
Block a user