Feat v0.4.6 sidebar restructure (#28)
All checks were successful
All checks were successful
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #28.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Notes — Surface Entry Point (v0.6.0)
|
||||
* Notes — Surface Entry Point (v0.7.0)
|
||||
*
|
||||
* Markdown notes surface using the SDK:
|
||||
* sw.api.ext('notes') — scoped API client
|
||||
@@ -628,44 +628,38 @@
|
||||
return result;
|
||||
}
|
||||
|
||||
function DocumentOutline({ headings, viewMode, cmEditorRef, previewRef }) {
|
||||
var [collapsed, setCollapsed] = useState(false);
|
||||
// ═══════════════════════════════════════════
|
||||
// SidebarTabs — Notes / Outline tab header
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
if (!headings || headings.length === 0) return null;
|
||||
function SidebarTabs({ activeTab, onTabChange, noteSelected }) {
|
||||
return html`
|
||||
<div class="sidebar-tabs">
|
||||
<button class="sidebar-tabs__tab ${activeTab === 'notes' ? 'sidebar-tabs__tab--active' : ''}"
|
||||
onClick=${function() { onTabChange('notes'); }}>Notes</button>
|
||||
<button class="sidebar-tabs__tab ${activeTab === 'outline' ? 'sidebar-tabs__tab--active' : ''} ${!noteSelected ? 'sidebar-tabs__tab--disabled' : ''}"
|
||||
onClick=${function() { if (noteSelected) onTabChange('outline'); }}>Outline</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function scrollToHeading(heading) {
|
||||
// scroll preview in rendered / split
|
||||
if (viewMode !== 'edit' && previewRef.current) {
|
||||
var targets = previewRef.current.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
for (var i = 0; i < targets.length; i++) {
|
||||
if (targets[i].textContent.trim() === heading.text) {
|
||||
targets[i].scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// scroll CM6 in edit / split
|
||||
if (viewMode !== 'rendered' && cmEditorRef.current) {
|
||||
var view = cmEditorRef.current.getView();
|
||||
if (!view) return;
|
||||
var lineNum = Math.min(heading.line + 1, view.state.doc.lines);
|
||||
var lineObj = view.state.doc.line(lineNum);
|
||||
view.dispatch({ effects: CM.EditorView.scrollIntoView(lineObj.from, { y: 'start' }) });
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// SidebarOutline — heading tree in sidebar
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
function SidebarOutline({ headings, activeHeadingIdx, onScrollToHeading }) {
|
||||
if (!headings || headings.length === 0) {
|
||||
return html`<div class="sidebar-outline__empty">No headings found</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="doc-outline">
|
||||
<div class="doc-outline__header" onClick=${function() { setCollapsed(!collapsed); }}>
|
||||
<span class="doc-outline__toggle">${collapsed ? '\u25B6' : '\u25BC'}</span>
|
||||
<span>Outline</span>
|
||||
<span class="doc-outline__count">${headings.length}</span>
|
||||
</div>
|
||||
${!collapsed && headings.map(function(h, i) {
|
||||
<div class="sidebar-outline">
|
||||
${headings.map(function(h, i) {
|
||||
return html`
|
||||
<div key=${i} class="doc-outline__item doc-outline__item--h${h.level}"
|
||||
style="padding-left: ${8 + (h.level - 1) * 12}px"
|
||||
onClick=${function() { scrollToHeading(h); }}>
|
||||
<div key=${i} class="sidebar-outline__item sidebar-outline__item--h${h.level} ${i === activeHeadingIdx ? 'sidebar-outline__item--active' : ''}"
|
||||
style="padding-left: ${10 + (h.level - 1) * 14}px"
|
||||
onClick=${function() { onScrollToHeading(h); }}>
|
||||
${h.text}
|
||||
</div>
|
||||
`;
|
||||
@@ -676,7 +670,7 @@
|
||||
|
||||
var _modeStore = sw.storage.local('notes');
|
||||
|
||||
function EditorPane({ note, folders, allTags, onSave, onDelete, onRefresh, onTagsChanged, onNavigate, onNavigateToTitle }) {
|
||||
function EditorPane({ note, folders, allTags, onSave, onDelete, onRefresh, onTagsChanged, onNavigate, onNavigateToTitle, onHeadingsChange, scrollToHeadingRef, onActiveHeadingChange }) {
|
||||
var [title, setTitle] = useState(note ? note.title : '');
|
||||
var [body, setBody] = useState(note ? note.body : '');
|
||||
var [noteTags, setNoteTags] = useState([]);
|
||||
@@ -914,10 +908,82 @@
|
||||
var [headings, setHeadings] = useState([]);
|
||||
useEffect(function() {
|
||||
debounce('outline', function() {
|
||||
setHeadings(parseHeadings(body));
|
||||
var h = parseHeadings(body);
|
||||
setHeadings(h);
|
||||
if (onHeadingsChange) onHeadingsChange(h);
|
||||
}, 300);
|
||||
}, [body]);
|
||||
|
||||
// ── Expose scrollToHeading to parent via ref ──
|
||||
useEffect(function() {
|
||||
if (!scrollToHeadingRef) return;
|
||||
scrollToHeadingRef.current = function(heading) {
|
||||
// scroll preview in rendered / split
|
||||
if (viewMode !== 'edit' && previewRef.current) {
|
||||
var targets = previewRef.current.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
for (var i = 0; i < targets.length; i++) {
|
||||
if (targets[i].textContent.trim() === heading.text) {
|
||||
targets[i].scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// scroll CM6 in edit / split
|
||||
if (viewMode !== 'rendered' && cmEditorRef.current) {
|
||||
var view = cmEditorRef.current.getView();
|
||||
if (!view) return;
|
||||
var lineNum = Math.min(heading.line + 1, view.state.doc.lines);
|
||||
var lineObj = view.state.doc.line(lineNum);
|
||||
view.dispatch({ effects: CM.EditorView.scrollIntoView(lineObj.from, { y: 'start' }) });
|
||||
}
|
||||
};
|
||||
}, [viewMode]);
|
||||
|
||||
// ── Active heading tracking on scroll ──
|
||||
useEffect(function() {
|
||||
if (!onActiveHeadingChange || !note) return;
|
||||
var scrollEl = null;
|
||||
var usePreview = viewMode !== 'edit';
|
||||
if (usePreview && previewRef.current) {
|
||||
scrollEl = previewRef.current;
|
||||
} else if (viewMode !== 'rendered' && cmEditorRef.current) {
|
||||
var view = cmEditorRef.current.getView();
|
||||
if (view) scrollEl = view.scrollDOM;
|
||||
}
|
||||
if (!scrollEl) return;
|
||||
|
||||
var ticking = false;
|
||||
function onScroll() {
|
||||
if (ticking) return;
|
||||
ticking = true;
|
||||
requestAnimationFrame(function() {
|
||||
ticking = false;
|
||||
if (usePreview && previewRef.current) {
|
||||
var els = previewRef.current.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
var active = -1;
|
||||
for (var i = 0; i < els.length; i++) {
|
||||
if (els[i].getBoundingClientRect().top <= previewRef.current.getBoundingClientRect().top + 30) {
|
||||
active = i;
|
||||
}
|
||||
}
|
||||
onActiveHeadingChange(active);
|
||||
} else if (cmEditorRef.current) {
|
||||
var v = cmEditorRef.current.getView();
|
||||
if (!v) return;
|
||||
var topLine = v.state.doc.lineAt(v.viewport.from).number - 1;
|
||||
var active = -1;
|
||||
for (var i = 0; i < headings.length; i++) {
|
||||
if (headings[i].line <= topLine) active = i;
|
||||
}
|
||||
onActiveHeadingChange(active);
|
||||
}
|
||||
});
|
||||
}
|
||||
scrollEl.addEventListener('scroll', onScroll);
|
||||
onScroll(); // initial check
|
||||
return function() { scrollEl.removeEventListener('scroll', onScroll); };
|
||||
}, [viewMode, note ? note.id : null, headings]);
|
||||
|
||||
// build indented folder list for the select dropdown
|
||||
var folderOptions = useMemo(function() {
|
||||
var list = folders || [];
|
||||
@@ -1025,8 +1091,6 @@
|
||||
<div class="notes-editor__body ${viewMode === 'split' ? 'notes-editor__body--split' : ''}">
|
||||
${renderEditorBody()}
|
||||
</div>
|
||||
<${DocumentOutline} headings=${headings} viewMode=${viewMode}
|
||||
cmEditorRef=${cmEditorRef} previewRef=${previewRef} />
|
||||
</div>
|
||||
<${BacklinksPanel} noteId=${note.id} onNavigate=${onNavigate} />
|
||||
</div>
|
||||
@@ -1050,6 +1114,10 @@
|
||||
var [showUnfiled, setShowUnfiled] = useState(false);
|
||||
var [allTags, setAllTags] = useState([]);
|
||||
var [activeTag, setActiveTag] = useState(null);
|
||||
var [sidebarTab, setSidebarTab] = useState('notes');
|
||||
var [sidebarHeadings, setSidebarHeadings] = useState([]);
|
||||
var [activeHeadingIdx, setActiveHeadingIdx] = useState(-1);
|
||||
var scrollToHeadingRef = useRef(null);
|
||||
|
||||
// ── Load note list ────────────────────────
|
||||
var loadNotes = useCallback(async function() {
|
||||
@@ -1161,6 +1229,9 @@
|
||||
function handleDelete() {
|
||||
setActiveId(null);
|
||||
setActiveNote(null);
|
||||
setSidebarTab('notes');
|
||||
setSidebarHeadings([]);
|
||||
setActiveHeadingIdx(-1);
|
||||
loadNotes();
|
||||
loadStats();
|
||||
loadTags();
|
||||
@@ -1317,38 +1388,42 @@
|
||||
<//>
|
||||
<div class="notes-app">
|
||||
<div class="notes-sidebar">
|
||||
<div class="notes-sidebar__header">
|
||||
<span class="notes-sidebar__title">Notes</span>
|
||||
${stats && html`<span class="notes-sidebar__count">${stats.total}</span>`}
|
||||
</div>
|
||||
<${FolderTree} folders=${folders}
|
||||
activeFolderId=${activeFolderId} showUnfiled=${showUnfiled}
|
||||
onSelectFolder=${handleSelectFolder} onSelectAll=${handleSelectAll}
|
||||
onSelectUnfiled=${handleSelectUnfiled}
|
||||
onCreateFolder=${handleCreateFolder}
|
||||
onRenameFolder=${handleRenameFolder}
|
||||
onDeleteFolder=${handleDeleteFolder}
|
||||
onDropNote=${handleDropNote} />
|
||||
<${TagFilter} tags=${allTags} activeTag=${activeTag} onSelect=${handleTagFilter} />
|
||||
<div class="notes-search">
|
||||
<input type="text" placeholder="Search notes…"
|
||||
value=${searchTerm} onInput=${handleSearch} />
|
||||
</div>
|
||||
<div class="notes-list">
|
||||
${loading && html`<div class="notes-list__loading"><${Spinner} size="md" /></div>`}
|
||||
${!loading && notes.length === 0 && html`
|
||||
<div class="notes-list__empty">
|
||||
${searchTerm ? 'No matching notes' : (activeTag ? 'No notes with tag "' + activeTag + '"' : 'No notes yet. Click + New Note to start.')}
|
||||
</div>
|
||||
`}
|
||||
${!loading && notes.map(function(n) {
|
||||
return html`
|
||||
<${NoteCard} key=${n.id} note=${n}
|
||||
active=${n.id === activeId}
|
||||
onClick=${() => handleSelect(n.id)} />
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
<${SidebarTabs} activeTab=${sidebarTab} onTabChange=${setSidebarTab} noteSelected=${!!activeNote} />
|
||||
${sidebarTab === 'notes' && html`
|
||||
<${FolderTree} folders=${folders}
|
||||
activeFolderId=${activeFolderId} showUnfiled=${showUnfiled}
|
||||
onSelectFolder=${handleSelectFolder} onSelectAll=${handleSelectAll}
|
||||
onSelectUnfiled=${handleSelectUnfiled}
|
||||
onCreateFolder=${handleCreateFolder}
|
||||
onRenameFolder=${handleRenameFolder}
|
||||
onDeleteFolder=${handleDeleteFolder}
|
||||
onDropNote=${handleDropNote} />
|
||||
<${TagFilter} tags=${allTags} activeTag=${activeTag} onSelect=${handleTagFilter} />
|
||||
<div class="notes-search">
|
||||
<input type="text" placeholder="Search notes…"
|
||||
value=${searchTerm} onInput=${handleSearch} />
|
||||
</div>
|
||||
<div class="notes-list">
|
||||
${loading && html`<div class="notes-list__loading"><${Spinner} size="md" /></div>`}
|
||||
${!loading && notes.length === 0 && html`
|
||||
<div class="notes-list__empty">
|
||||
${searchTerm ? 'No matching notes' : (activeTag ? 'No notes with tag "' + activeTag + '"' : 'No notes yet. Click + New Note to start.')}
|
||||
</div>
|
||||
`}
|
||||
${!loading && notes.map(function(n) {
|
||||
return html`
|
||||
<${NoteCard} key=${n.id} note=${n}
|
||||
active=${n.id === activeId}
|
||||
onClick=${() => handleSelect(n.id)} />
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
${sidebarTab === 'outline' && html`
|
||||
<${SidebarOutline} headings=${sidebarHeadings}
|
||||
activeHeadingIdx=${activeHeadingIdx}
|
||||
onScrollToHeading=${function(h) { if (scrollToHeadingRef.current) scrollToHeadingRef.current(h); }} />
|
||||
`}
|
||||
</div>
|
||||
<${EditorPane} note=${activeNote} folders=${folders} allTags=${allTags}
|
||||
onSave=${handleRefresh}
|
||||
@@ -1356,7 +1431,10 @@
|
||||
onRefresh=${handleRefresh}
|
||||
onTagsChanged=${handleTagsChanged}
|
||||
onNavigate=${handleSelect}
|
||||
onNavigateToTitle=${handleNavigateToTitle} />
|
||||
onNavigateToTitle=${handleNavigateToTitle}
|
||||
onHeadingsChange=${setSidebarHeadings}
|
||||
scrollToHeadingRef=${scrollToHeadingRef}
|
||||
onActiveHeadingChange=${setActiveHeadingIdx} />
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user