diff --git a/CHANGELOG.md b/CHANGELOG.md index e86a6b5..5867c7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,36 @@ All notable changes to Switchboard Core are documented here. +## v0.4.6 — Sidebar Restructure + +### Added + +- **Sidebar tabs**: Two-tab header ("Notes" / "Outline") replaces the static + sidebar header. Notes tab contains folder tree, tag filter, search, and note + list. Outline tab shows the document heading tree for the active note. + Outline tab disabled (grayed) when no note is selected. +- **Sidebar outline**: Document outline relocated from editor-side panel into + the sidebar's Outline tab. Nested heading hierarchy (H1→H2→H3) with depth + indentation via `padding-left`. Click heading scrolls to it in both preview + and CM6 editor modes. +- **Active heading tracking**: Scroll listener (RAF-throttled) highlights the + current heading in the sidebar outline based on scroll position. Uses + `getBoundingClientRect()` in preview/split mode and CM6 viewport line in + edit mode. + +### Changed + +- Notes package version bumped from 0.6.0 to 0.7.0. +- `EditorPane` exposes headings, scroll function, and active heading index to + parent via callback props (`onHeadingsChange`, `scrollToHeadingRef`, + `onActiveHeadingChange`). +- Sidebar auto-resets to Notes tab when active note is deleted. + +### Removed + +- Old `DocumentOutline` component and `.doc-outline` CSS styles (replaced by + sidebar outline). + ## v0.4.5 — Editor Modes + Document Outline ### Added diff --git a/ROADMAP.md b/ROADMAP.md index e101f4f..4b48c84 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -301,12 +301,12 @@ Zero platform special-casing. Proves the full extension stack E2E. | View mode setting | ✅ | `editor_mode` in manifest settings + localStorage persistence. Toolbar cycles rendered → edit → split. | | Document outline | ✅ | `parseHeadings()` extracts headings (skips code blocks). Collapsible `DocumentOutline` panel with click-to-scroll (CM6 line dispatch or preview `scrollIntoView`). Debounced 300ms updates. | -### v0.4.6 — Sidebar Restructure (planned) +### v0.4.6 — Sidebar Restructure (complete) | Step | Status | Description | |------|--------|-------------| -| Sidebar tabs | | Two-tab header at top of sidebar: "Notes" (folders → tags → search → list) and "Outline" (heading tree for active note). Outline tab enabled only when a note is selected. | -| Heading tree | | Nested heading hierarchy (H1 → H2 → H3) with depth indentation. Click → scroll to heading in editor/preview. Current heading highlighted based on scroll position. | +| Sidebar tabs | ✅ | Two-tab header at top of sidebar: "Notes" (folders → tags → search → list) and "Outline" (heading tree for active note). Outline tab enabled only when a note is selected. | +| Heading tree | ✅ | Nested heading hierarchy (H1 → H2 → H3) with depth indentation. Click → scroll to heading in editor/preview. Current heading highlighted based on scroll position. | ### v0.4.7 — Note Graph (planned) diff --git a/VERSION b/VERSION index 0bfccb0..ef52a64 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.5 +0.4.6 diff --git a/packages/notes/css/main.css b/packages/notes/css/main.css index 10d3ef8..5604cb2 100644 --- a/packages/notes/css/main.css +++ b/packages/notes/css/main.css @@ -632,57 +632,79 @@ font-size: 11px; } -/* ── Document Outline ─────────────────────── */ -.doc-outline { - width: 200px; - min-width: 160px; - border-left: 1px solid var(--border); - background: var(--bg-raised); - overflow-y: auto; +/* ── Sidebar Tabs ────────────────────────── */ +.sidebar-tabs { + display: flex; + border-bottom: 1px solid var(--border); flex-shrink: 0; } -.doc-outline__header { - font-size: 11px; - font-weight: 600; +.sidebar-tabs__tab { + flex: 1; + padding: 10px 14px; + font-size: 13px; + font-weight: 500; color: var(--text-2); - text-transform: uppercase; - letter-spacing: 0.5px; - padding: 8px 10px; - display: flex; - align-items: center; - gap: 6px; + background: none; + border: none; + border-bottom: 2px solid transparent; cursor: pointer; - user-select: none; - border-bottom: 1px solid var(--border); + transition: var(--transition); + text-align: center; + font-family: var(--font); } -.doc-outline__toggle { - font-size: 9px; - color: var(--text-2); -} -.doc-outline__count { +.sidebar-tabs__tab:hover { + color: var(--text); background: var(--bg-hover); - color: var(--text-2); - font-size: 10px; - padding: 0 6px; - border-radius: 8px; - line-height: 16px; } -.doc-outline__item { - padding: 4px 10px; - font-size: 12px; +.sidebar-tabs__tab--active { + color: var(--text); + border-bottom-color: var(--accent); +} +.sidebar-tabs__tab--disabled { + color: var(--text-3); + cursor: default; + opacity: 0.5; +} +.sidebar-tabs__tab--disabled:hover { + background: none; + color: var(--text-3); +} + +/* ── Sidebar Outline ─────────────────────── */ +.sidebar-outline { + flex: 1; + overflow-y: auto; + padding: 8px 0; +} +.sidebar-outline__empty { + padding: 40px 16px; + text-align: center; + color: var(--text-3); + font-size: 13px; +} +.sidebar-outline__item { + padding: 5px 12px; + font-size: 13px; color: var(--text-2); cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: var(--transition); + border-radius: var(--radius); + margin: 1px 6px; } -.doc-outline__item:hover { +.sidebar-outline__item:hover { background: var(--bg-hover); color: var(--text); } -.doc-outline__item--h1 { font-weight: 600; color: var(--text); } -.doc-outline__item--h2 { font-weight: 500; } +.sidebar-outline__item--active { + background: var(--bg-active); + color: var(--text); + font-weight: 500; +} +.sidebar-outline__item--h1 { font-weight: 600; color: var(--text); } +.sidebar-outline__item--h2 { font-weight: 500; } /* ── Responsive ──────────────────────────── */ @media (max-width: 700px) { @@ -691,5 +713,4 @@ .folder-tree { max-height: 30vh; } .notes-editor__body--split { grid-template-columns: 1fr; } .notes-editor__body--split .notes-preview { display: none; } - .doc-outline { display: none; } } diff --git a/packages/notes/js/main.js b/packages/notes/js/main.js index 8741c83..67e4c64 100644 --- a/packages/notes/js/main.js +++ b/packages/notes/js/main.js @@ -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` + + `; + } - 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``; } return html` -
-
- ${collapsed ? '\u25B6' : '\u25BC'} - Outline - ${headings.length} -
- ${!collapsed && headings.map(function(h, i) { + @@ -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 @@
-
- Notes - ${stats && html`${stats.total}`} -
- <${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} /> - -
- ${loading && html`
<${Spinner} size="md" />
`} - ${!loading && notes.length === 0 && html` -
- ${searchTerm ? 'No matching notes' : (activeTag ? 'No notes with tag "' + activeTag + '"' : 'No notes yet. Click + New Note to start.')} -
- `} - ${!loading && notes.map(function(n) { - return html` - <${NoteCard} key=${n.id} note=${n} - active=${n.id === activeId} - onClick=${() => handleSelect(n.id)} /> - `; - })} -
+ <${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} /> + +
+ ${loading && html`
<${Spinner} size="md" />
`} + ${!loading && notes.length === 0 && html` +
+ ${searchTerm ? 'No matching notes' : (activeTag ? 'No notes with tag "' + activeTag + '"' : 'No notes yet. Click + New Note to start.')} +
+ `} + ${!loading && notes.map(function(n) { + return html` + <${NoteCard} key=${n.id} note=${n} + active=${n.id === activeId} + onClick=${() => handleSelect(n.id)} /> + `; + })} +
+ `} + ${sidebarTab === 'outline' && html` + <${SidebarOutline} headings=${sidebarHeadings} + activeHeadingIdx=${activeHeadingIdx} + onScrollToHeading=${function(h) { if (scrollToHeadingRef.current) scrollToHeadingRef.current(h); }} /> + `}
<${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} />
`; } diff --git a/packages/notes/manifest.json b/packages/notes/manifest.json index 24ee0f9..010c216 100644 --- a/packages/notes/manifest.json +++ b/packages/notes/manifest.json @@ -6,9 +6,9 @@ "route": "/s/notes", "auth": "authenticated", "layout": "single", - "version": "0.6.0", + "version": "0.7.0", "icon": "📝", - "description": "Markdown notes surface with rich editor, folders, tags, backlinks, import/export, and document outline.", + "description": "Markdown notes surface with rich editor, folders, tags, backlinks, import/export, sidebar tabs, and document outline.", "author": "switchboard", "permissions": ["db.write"],