Add Apple Pencil support, reading plan, full-text search, and inline ink
- Apple Pencil hover cursor: previews brush size/shape before contact - Tilt shading: tilted pen strokes widen and reduce thinning via tiltX/Y - Two-finger undo in draw mode (non-stylus two-finger tap) - Copy & open Claude buttons: copies study/podcast prompt then opens claude.ai/new - Inline ink canvas in study notes (DrawCanvas embedded in Notes tab) - Full-text search across all project notes with 300ms debounce - Reading plan: set a book + week goal, mark chapters read from the reader Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
837b81b038
commit
902fb5da02
7 files changed
+367
-21
No files matched your search
+147
-1
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useApp } from '../context/AppContext.js';
|
||||
import { bookOptions, formatRelativeDate } from '../App.jsx';
|
||||
import { bookOptions, CHAPTER_COUNTS, formatRelativeDate } from '../App.jsx';
|
||||
|
||||
export default function HomePage() {
|
||||
const {
|
||||
@@ -10,6 +11,10 @@ export default function HomePage() {
|
||||
homeSearch, setHomeSearch,
|
||||
homeSort, setHomeSort,
|
||||
homeTagFilter, setHomeTagFilter,
|
||||
homeFullTextResults,
|
||||
readingPlan,
|
||||
createReadingPlan,
|
||||
clearReadingPlan,
|
||||
audioBook, setAudioBook,
|
||||
audioNarrator, setAudioNarrator,
|
||||
audioState,
|
||||
@@ -145,6 +150,41 @@ export default function HomePage() {
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
{/* Full-text note search results */}
|
||||
{homeFullTextResults !== null && (
|
||||
<div className="mb-4 rounded-3xl border border-slate-200 bg-white p-5 shadow-panel">
|
||||
<p className="mb-3 text-sm font-semibold text-slate-700">
|
||||
{homeFullTextResults.length === 0
|
||||
? 'No notes match your search.'
|
||||
: `Notes matching "${homeSearch.trim()}" — ${homeFullTextResults.reduce((n, r) => n + r.matches.length, 0)} result${homeFullTextResults.reduce((n, r) => n + r.matches.length, 0) !== 1 ? 's' : ''} across ${homeFullTextResults.length} project${homeFullTextResults.length !== 1 ? 's' : ''}`}
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
{homeFullTextResults.map(({ projectId, projectTitle, matches }) => (
|
||||
<div key={projectId}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => resumeProject(projectId)}
|
||||
className="mb-1.5 text-sm font-semibold text-violet-700 hover:underline"
|
||||
>
|
||||
{projectTitle} →
|
||||
</button>
|
||||
<div className="space-y-1.5">
|
||||
{matches.map(({ chunkId, ref, field, snippet }) => (
|
||||
<div key={chunkId} className="rounded-2xl bg-slate-50 px-3 py-2">
|
||||
<span className="text-xs font-semibold uppercase tracking-wide text-slate-400">{ref} · {field}</span>
|
||||
<p className="mt-0.5 text-sm text-slate-700 leading-snug">{snippet}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reading plan card */}
|
||||
<ReadingPlanCard readingPlan={readingPlan} createReadingPlan={createReadingPlan} clearReadingPlan={clearReadingPlan} openBibleReader={openBibleReader} />
|
||||
|
||||
<div className="mb-4 flex flex-col gap-3 rounded-3xl border border-slate-200 bg-white p-6 shadow-panel sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base font-semibold text-slate-900">Listen to BSB Audio</h3>
|
||||
@@ -317,3 +357,109 @@ export default function HomePage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReadingPlanCard({ readingPlan, createReadingPlan, clearReadingPlan, openBibleReader }) {
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [planBook, setPlanBook] = useState(bookOptions[0].abbrev);
|
||||
const [planWeeks, setPlanWeeks] = useState(4);
|
||||
|
||||
if (readingPlan) {
|
||||
const pct = readingPlan.totalChapters > 0
|
||||
? Math.round((readingPlan.chaptersRead.length / readingPlan.totalChapters) * 100)
|
||||
: 0;
|
||||
const daysLeft = Math.max(0, Math.ceil((readingPlan.targetDate - Date.now()) / 86400000));
|
||||
const chapLeft = readingPlan.totalChapters - readingPlan.chaptersRead.length;
|
||||
const paceNeeded = daysLeft > 0 ? (chapLeft / daysLeft).toFixed(1) : '—';
|
||||
return (
|
||||
<div className="mb-4 rounded-3xl border border-emerald-200 bg-emerald-50 p-5 shadow-panel">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-widest text-emerald-600">Reading Plan</p>
|
||||
<h3 className="mt-1 text-base font-semibold text-slate-900">{readingPlan.bookName}</h3>
|
||||
<p className="mt-0.5 text-sm text-slate-600">
|
||||
{readingPlan.chaptersRead.length} / {readingPlan.totalChapters} chapters · {daysLeft} day{daysLeft !== 1 ? 's' : ''} left · {paceNeeded} ch/day needed
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={openBibleReader}
|
||||
className="rounded-xl bg-emerald-600 px-3 py-1.5 text-sm font-semibold text-white hover:bg-emerald-500"
|
||||
>
|
||||
Read
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { if (window.confirm('Clear reading plan?')) clearReadingPlan(); }}
|
||||
className="rounded-xl border border-emerald-300 px-3 py-1.5 text-sm text-emerald-700 hover:bg-emerald-100"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 h-2.5 w-full overflow-hidden rounded-full bg-emerald-200">
|
||||
<div className="h-full rounded-full bg-emerald-500 transition-all" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<p className="mt-1 text-right text-xs text-emerald-700">{pct}%</p>
|
||||
{readingPlan.chaptersRead.length > 0 && (
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
Read: ch. {readingPlan.chaptersRead.slice(0, 12).join(', ')}{readingPlan.chaptersRead.length > 12 ? '…' : ''}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4 rounded-3xl border border-slate-200 bg-white p-5 shadow-panel">
|
||||
{showForm ? (
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<span className="text-sm font-semibold text-slate-700">Read</span>
|
||||
<select
|
||||
value={planBook}
|
||||
onChange={(e) => setPlanBook(e.target.value)}
|
||||
className="rounded-xl border border-slate-300 bg-slate-50 px-2 py-1.5 text-sm text-slate-900 focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
|
||||
>
|
||||
{bookOptions.map((b) => (
|
||||
<option key={b.abbrev} value={b.abbrev}>{b.name} ({CHAPTER_COUNTS[b.abbrev] ?? '?'} ch)</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="text-sm text-slate-600">in</span>
|
||||
<select
|
||||
value={planWeeks}
|
||||
onChange={(e) => setPlanWeeks(Number(e.target.value))}
|
||||
className="rounded-xl border border-slate-300 bg-slate-50 px-2 py-1.5 text-sm text-slate-900 focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
|
||||
>
|
||||
{[1,2,3,4,6,8,12,16,26,52].map((w) => <option key={w} value={w}>{w} week{w !== 1 ? 's' : ''}</option>)}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const book = bookOptions.find((b) => b.abbrev === planBook);
|
||||
createReadingPlan(planBook, book?.name ?? planBook, planWeeks);
|
||||
setShowForm(false);
|
||||
}}
|
||||
className="rounded-xl bg-emerald-600 px-3 py-1.5 text-sm font-semibold text-white hover:bg-emerald-500"
|
||||
>
|
||||
Start plan
|
||||
</button>
|
||||
<button type="button" onClick={() => setShowForm(false)} className="text-sm text-slate-400 hover:text-slate-600">Cancel</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-slate-700">Reading Plan</p>
|
||||
<p className="text-xs text-slate-500">Set a goal to read through a book, track chapters as you go.</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowForm(true)}
|
||||
className="shrink-0 rounded-xl border border-slate-300 px-3 py-1.5 text-sm font-semibold text-slate-700 hover:bg-slate-50"
|
||||
>
|
||||
Set goal
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user