Skip to content

Commit c3e6c28

Browse files
sanbuphyclaude
andcommitted
Fix tour panel actions with flushSync, add clear-all to NotesPanel
- Wrap open-notes/open-settings/open-walkinglabs actions in flushSync so DOM updates before next tour step queries the target element - Add clearAll method to useNotesAndBookmarks hook - Add clear button (with confirmation) to NotesPanel header - Auto-close panels on tour prev/stop transitions Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent d4aa09a commit c3e6c28

3 files changed

Lines changed: 257 additions & 56 deletions

File tree

web/src/App.jsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -390,19 +390,25 @@ function AppContent() {
390390
replaceUrlWithHash(tourNotebookId, lang)
391391
}
392392
if (step?.action === 'open-notes') {
393-
setSettingsOpen(false)
394-
setWalkingLabsOpen(false)
395-
setCurrentId(NOTES_SENTINEL)
393+
flushSync(() => {
394+
setSettingsOpen(false)
395+
setWalkingLabsOpen(false)
396+
setCurrentId(NOTES_SENTINEL)
397+
})
396398
}
397399
if (step?.action === 'open-settings') {
398-
setCurrentId(null)
399-
setWalkingLabsOpen(false)
400-
setSettingsOpen(true)
400+
flushSync(() => {
401+
setCurrentId(null)
402+
setWalkingLabsOpen(false)
403+
setSettingsOpen(true)
404+
})
401405
}
402406
if (step?.action === 'open-walkinglabs') {
403-
setCurrentId(null)
404-
setSettingsOpen(false)
405-
setWalkingLabsOpen(true)
407+
flushSync(() => {
408+
setCurrentId(null)
409+
setSettingsOpen(false)
410+
setWalkingLabsOpen(true)
411+
})
406412
}
407413

408414
if (tourStepIndex >= tourSteps.length - 1) {
@@ -473,6 +479,7 @@ function AppContent() {
473479
getSectionNotes={nbm.getSectionNotes}
474480
exportData={nbm.exportData}
475481
importFile={nbm.importFile}
482+
onClearAll={nbm.clearAll}
476483
onSelect={handleSelect}
477484
lang={lang}
478485
/>

web/src/components/NotesPanel.jsx

Lines changed: 177 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,76 @@
1-
import { useRef } from 'react'
1+
import { useRef, useState } from 'react'
22
import { Download, Upload, Star, StickyNote, ExternalLink, Trash2 } from 'lucide-react'
33

44
export default function NotesPanel({
5-
catalog, bookmarks, notes, getSectionNotes, exportData, importFile, onSelect, lang,
5+
catalog, bookmarks, notes, notebooksWithNotes, getSectionNotes, exportData, importFile, onClearAll, onSelect, lang,
66
}) {
77
const fileInputRef = useRef(null)
8+
const [confirmClear, setConfirmClear] = useState(false)
89

910
const bookmarkList = Object.entries(bookmarks).sort((a, b) => b[1].addedAt - a[1].addedAt)
10-
const notebooksWithNotes = new Set()
11+
12+
// Collect all notes across all notebooks
1113
const allNotes = []
12-
for (const [key, note] of Object.entries(notes)) {
13-
const sepIdx = key.indexOf('::')
14-
const notebookId = key.slice(0, sepIdx)
15-
const sectionId = key.slice(sepIdx + 2)
16-
notebooksWithNotes.add(notebookId)
17-
allNotes.push({ notebookId, sectionId, ...note })
14+
for (const [notebookId, noteList] of Object.entries(notes)) {
15+
if (!Array.isArray(noteList)) continue
16+
for (const n of noteList) {
17+
if (!n || typeof n !== 'object') continue
18+
allNotes.push({ notebookId, ...n })
19+
}
1820
}
1921
allNotes.sort((a, b) => b.updatedAt - a.updatedAt)
2022

2123
const findMeta = (id) => catalog.find((n) => n.id === id)
2224

25+
const noteGroups = Object.entries(notes)
26+
.map(([notebookId, noteList]) => {
27+
const list = Array.isArray(noteList)
28+
? noteList
29+
.filter((n) => n && typeof n === 'object')
30+
.slice()
31+
.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))
32+
: []
33+
const meta = findMeta(notebookId)
34+
return {
35+
notebookId,
36+
meta,
37+
notes: list,
38+
latestAt: list[0]?.updatedAt || 0,
39+
}
40+
})
41+
.filter((group) => group.notes.length > 0)
42+
.sort((a, b) => b.latestAt - a.latestAt)
43+
44+
const normalizeText = (text) => String(text || '').replace(/\s+/g, ' ').trim()
45+
46+
const findQuoteTarget = (root, quote) => {
47+
const quoteText = normalizeText(quote)
48+
if (!quoteText) return null
49+
50+
const searchText = quoteText.length > 80 ? quoteText.slice(0, 80) : quoteText
51+
const candidates = root.querySelectorAll(
52+
'.rendered_html p, .rendered_html li, .rendered_html td, .rendered_html th'
53+
)
54+
55+
for (const el of candidates) {
56+
const candidateText = normalizeText(el.textContent)
57+
if (candidateText.includes(searchText) || quoteText.includes(candidateText)) {
58+
return el
59+
}
60+
}
61+
62+
return null
63+
}
64+
65+
const markJumpTarget = (el) => {
66+
el.classList.remove('note-jump-target')
67+
void el.offsetWidth
68+
el.classList.add('note-jump-target')
69+
window.setTimeout(() => {
70+
el.classList.remove('note-jump-target')
71+
}, 1800)
72+
}
73+
2374
const handleExport = () => {
2475
const json = exportData()
2576
const blob = new Blob([json], { type: 'application/json' })
@@ -45,6 +96,25 @@ export default function NotesPanel({
4596
if (fileInputRef.current) fileInputRef.current.value = ''
4697
}
4798

99+
const handleNoteClick = (notebookId, sectionId, quote = '') => {
100+
onSelect(notebookId)
101+
102+
const tryScroll = (retries) => {
103+
const root = document.querySelector('.notebook-content')
104+
const quoteTarget = root ? findQuoteTarget(root, quote) : null
105+
const sectionTarget = sectionId ? document.getElementById(sectionId) : null
106+
const target = quoteTarget || sectionTarget
107+
108+
if (target) {
109+
target.scrollIntoView({ behavior: 'smooth', block: 'center' })
110+
markJumpTarget(target)
111+
} else if (retries > 0) {
112+
setTimeout(() => tryScroll(retries - 1), 100)
113+
}
114+
}
115+
setTimeout(() => tryScroll(20), 150)
116+
}
117+
48118
const t = (zh, en) => lang === 'en' ? en : zh
49119

50120
return (
@@ -62,6 +132,20 @@ export default function NotesPanel({
62132
<Upload className="w-3.5 h-3.5" />
63133
<span>{t('导入', 'Import')}</span>
64134
</button>
135+
{confirmClear ? (
136+
<button onClick={() => { onClearAll?.(); setConfirmClear(false) }}
137+
className="notes-action-btn" style={{ color: 'var(--accent-red, #ef4444)' }}
138+
title={t('确认清空', 'Confirm clear')}>
139+
<Trash2 className="w-3.5 h-3.5" />
140+
<span>{t('确认', 'Confirm')}</span>
141+
</button>
142+
) : (
143+
<button onClick={() => setConfirmClear(true)} className="notes-action-btn"
144+
title={t('清空所有数据', 'Clear all data')}>
145+
<Trash2 className="w-3.5 h-3.5" />
146+
<span>{t('清空', 'Clear')}</span>
147+
</button>
148+
)}
65149
<input ref={fileInputRef} type="file" accept=".json" onChange={handleImport}
66150
style={{ display: 'none' }} />
67151
</div>
@@ -104,29 +188,94 @@ export default function NotesPanel({
104188
<span className="notes-count">{allNotes.length}</span>
105189
</h2>
106190
{allNotes.length === 0 ? (
107-
<p className="notes-empty">{t('还没有笔记。阅读时点击任意节标题旁的笔记图标即可添加。', 'No notes yet. Click the note icon next to any section heading while reading.')}</p>
191+
<p className="notes-empty">{t('还没有笔记。阅读时选中文字,在弹出工具栏中选择"笔记"或"高亮"即可添加。', 'No notes yet. Select text while reading and choose "Note" or "Highlight" from the popup toolbar.')}</p>
108192
) : (
109-
<div className="notes-list">
110-
{allNotes.map((n) => {
111-
const meta = findMeta(n.notebookId)
112-
return (
113-
<button key={`${n.notebookId}::${n.sectionId}`}
114-
className="notes-card notes-card-row"
115-
onClick={() => onSelect(n.notebookId)}>
116-
<div className="notes-card-main">
117-
<span className="notes-card-section">{n.sectionTitle}</span>
118-
<span className="notes-card-preview">{n.text.slice(0, 120)}{n.text.length > 120 ? '…' : ''}</span>
119-
</div>
120-
<div className="notes-card-meta">
121-
<span className="notes-card-notebook">{meta?.title || n.notebookId}</span>
122-
<span className="notes-card-date">
123-
{new Date(n.updatedAt).toLocaleDateString(lang === 'en' ? 'en-US' : 'zh-CN')}
193+
<div className="notes-article-list">
194+
{noteGroups.map((group) => (
195+
<section key={group.notebookId} className="notes-article-group">
196+
<button
197+
className="notes-article-header"
198+
onClick={() => onSelect(group.notebookId)}
199+
>
200+
<div className="notes-article-title-row">
201+
<span className="notes-article-num">
202+
{group.notebookId.match(/^\d+/)?.[0] || ''}
203+
</span>
204+
<span className="notes-article-title">
205+
{group.meta?.title || group.notebookId}
124206
</span>
125-
<ExternalLink className="w-3 h-3 notes-card-icon" />
126207
</div>
208+
<span className="notes-article-count">
209+
{group.notes.length} {t('条', 'notes')}
210+
</span>
127211
</button>
128-
)
129-
})}
212+
213+
<div className="notes-article-items">
214+
{group.notes.map((n) => (
215+
<div key={n.id}
216+
role="button"
217+
tabIndex={0}
218+
className="notes-note-row"
219+
onClick={() => handleNoteClick(group.notebookId, n.sectionId)}
220+
onKeyDown={(e) => {
221+
if (e.key !== 'Enter' && e.key !== ' ') return
222+
e.preventDefault()
223+
handleNoteClick(group.notebookId, n.sectionId)
224+
}}>
225+
<div className="notes-card-main">
226+
<div className="notes-card-meta-row">
227+
<span className="notes-card-section-label">{n.sectionTitle}</span>
228+
</div>
229+
{n.quote && (
230+
<span
231+
className="notes-card-quote notes-card-quote-link"
232+
role="link"
233+
tabIndex={0}
234+
onClick={(e) => {
235+
e.stopPropagation()
236+
handleNoteClick(group.notebookId, n.sectionId, n.quote)
237+
}}
238+
onKeyDown={(e) => {
239+
if (e.key !== 'Enter' && e.key !== ' ') return
240+
e.preventDefault()
241+
e.stopPropagation()
242+
handleNoteClick(group.notebookId, n.sectionId, n.quote)
243+
}}
244+
>
245+
{String(n.quote).slice(0, 150)}{String(n.quote).length > 150 ? '…' : ''}
246+
</span>
247+
)}
248+
{n.text && (
249+
<span
250+
className="notes-card-preview notes-card-preview-link"
251+
role="link"
252+
tabIndex={0}
253+
onClick={(e) => {
254+
e.stopPropagation()
255+
handleNoteClick(group.notebookId, n.sectionId, n.quote)
256+
}}
257+
onKeyDown={(e) => {
258+
if (e.key !== 'Enter' && e.key !== ' ') return
259+
e.preventDefault()
260+
e.stopPropagation()
261+
handleNoteClick(group.notebookId, n.sectionId, n.quote)
262+
}}
263+
>
264+
{String(n.text).slice(0, 100)}{String(n.text).length > 100 ? '…' : ''}
265+
</span>
266+
)}
267+
</div>
268+
<div className="notes-card-meta">
269+
<span className="notes-card-date">
270+
{new Date(n.updatedAt || Date.now()).toLocaleDateString(lang === 'en' ? 'en-US' : 'zh-CN')}
271+
</span>
272+
<ExternalLink className="w-3 h-3 notes-card-icon" />
273+
</div>
274+
</div>
275+
))}
276+
</div>
277+
</section>
278+
))}
130279
</div>
131280
)}
132281
</section>

0 commit comments

Comments
 (0)