diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c97bd1..e86a6b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,36 @@ All notable changes to Switchboard Core are documented here. +## v0.4.5 — Editor Modes + Document Outline + +### Added + +- **Tri-state view mode**: Notes open in rendered (read-only) mode by default. + Toolbar button cycles through Edit (CM6) and Split (side-by-side) modes. + Replaces the old binary preview toggle. +- **Split view**: Side-by-side CM6 editor + rendered preview using CSS grid. + CM6 instance preserved when toggling between edit and split modes. +- **Synced scroll**: In split mode, scrolling the CM6 editor proportionally + scrolls the preview pane via linear ratio mapping on `scrollDOM`. +- **View mode persistence**: User's preferred mode saved to localStorage + (`sw.storage.local('notes')`). New `editor_mode` manifest setting with + admin-level default (rendered / edit / split). +- **Document outline**: Collapsible TOC panel adjacent to the editor body. + `parseHeadings()` extracts heading level, text, and line number (skips + fenced code blocks). Click heading to scroll — uses CM6 + `EditorView.scrollIntoView()` in edit/split mode, DOM `scrollIntoView()` + in rendered mode. Updates on body change with 300ms debounce. + +### Changed + +- Notes package version bumped from 0.5.0 to 0.6.0. +- Editor body wrapped in `.notes-editor__content` flex container to + accommodate the outline panel. +- Wikilink click handling active in both rendered and split modes. +- Mobile: split view collapses to single column, outline panel hidden. + +--- + ## v0.4.4 — Rich Editor + Import/Export ### Added diff --git a/ROADMAP.md b/ROADMAP.md index 58ae6f7..e101f4f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # Switchboard Core — Roadmap -## Current: v0.4.4 — Rich Editor + Import/Export +## Current: v0.4.5 — Editor Modes + Document Outline Fork of chat-switchboard, gutted to a pure extension platform. All AI/chat features removed from the kernel. What remains is the minimum viable @@ -291,15 +291,15 @@ Zero platform special-casing. Proves the full extension stack E2E. | Import .md | ✅ | Import button in topbar. File picker for `.md`/`.markdown`/`.txt`. Parses YAML frontmatter for title and tags. Creates note via API with tags. | | Notes package v0.5.0 | ✅ | Manifest version bumped from 0.4.0 → 0.5.0. | -### v0.4.5 — Editor Modes + Document Outline (planned) +### v0.4.5 — Editor Modes + Document Outline (complete) | Step | Status | Description | |------|--------|-------------| -| Default-rendered mode | | Preview is the initial state on note open. "Edit" button enters CM6. Save exits back to rendered view. | -| Split view | | Side-by-side layout: CM6 left, rendered preview right. New `viewMode` state: `rendered` / `edit` / `split`. CSS grid two-column in `.notes-editor__body`. | -| Synced scroll | | Split mode scroll sync. CM6 `scrollDOM` scroll listener maps position ratio to preview `scrollTop`. Linear mapping. | -| View mode setting | | Add `editor_mode` to manifest `settings` (rendered / split / edit). Persist preference. Toolbar cycles modes. | -| Document outline | | Parse headings from body. Render as collapsible TOC panel adjacent to editor. Click heading → scroll CM6 or preview to target. Updates on body change via debounced parse. | +| Default-rendered mode | ✅ | Preview is the initial state on note open. "Edit" button enters CM6. Tri-state `viewMode`: `rendered` / `edit` / `split`. | +| Split view | ✅ | Side-by-side layout: CM6 left, rendered preview right. CSS grid two-column in `.notes-editor__body--split`. | +| Synced scroll | ✅ | Split mode scroll sync. CM6 `scrollDOM` scroll listener maps position ratio to preview `scrollTop`. Linear mapping. | +| 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) diff --git a/VERSION b/VERSION index 6f2743d..0bfccb0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.4 +0.4.5 diff --git a/packages/notes/README.md b/packages/notes/README.md index f67bb1d..55c0fca 100644 --- a/packages/notes/README.md +++ b/packages/notes/README.md @@ -21,6 +21,7 @@ Core CRUD with markdown textarea and inline preview. - v0.4.2: Tags + enhanced search - v0.4.3: Backlinks + `[[wikilinks]]` - v0.4.4: Rich editor (CodeMirror 6) + import/export +- v0.4.5: Editor modes (rendered/edit/split) + document outline ## Data diff --git a/packages/notes/css/main.css b/packages/notes/css/main.css index 3db4610..10d3ef8 100644 --- a/packages/notes/css/main.css +++ b/packages/notes/css/main.css @@ -257,6 +257,14 @@ border-color: var(--accent); } +/* ── Editor Content (body + outline wrapper) */ +.notes-editor__content { + flex: 1; + display: flex; + min-height: 0; + overflow: hidden; +} + /* ── Editor Body ─────────────────────────── */ .notes-editor__body { flex: 1; @@ -264,6 +272,13 @@ min-height: 0; overflow: hidden; } +.notes-editor__body--split { + display: grid; + grid-template-columns: 1fr 1fr; +} +.notes-editor__body--split .notes-preview { + border-left: 1px solid var(--border); +} .notes-editor__textarea { flex: 1; resize: none; @@ -617,9 +632,64 @@ 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; + flex-shrink: 0; +} +.doc-outline__header { + font-size: 11px; + font-weight: 600; + color: var(--text-2); + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 8px 10px; + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + user-select: none; + border-bottom: 1px solid var(--border); +} +.doc-outline__toggle { + font-size: 9px; + color: var(--text-2); +} +.doc-outline__count { + 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; + color: var(--text-2); + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: var(--transition); +} +.doc-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; } + /* ── Responsive ──────────────────────────── */ @media (max-width: 700px) { .notes-sidebar { width: 100%; border-right: none; border-bottom: 1px solid var(--border); max-height: 40vh; } .notes-app { flex-direction: column; } .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 e59cf23..8741c83 100644 --- a/packages/notes/js/main.js +++ b/packages/notes/js/main.js @@ -1,5 +1,5 @@ /** - * Notes — Surface Entry Point (v0.5.0) + * Notes — Surface Entry Point (v0.6.0) * * Markdown notes surface using the SDK: * sw.api.ext('notes') — scoped API client @@ -607,11 +607,80 @@ } + // ═══════════════════════════════════════════ + // Heading parser + Document Outline + // ═══════════════════════════════════════════ + + function parseHeadings(body) { + if (!body) return []; + var lines = body.split('\n'); + var result = []; + var inCode = false; + for (var i = 0; i < lines.length; i++) { + var line = lines[i]; + if (line.trim().indexOf('```') === 0) { inCode = !inCode; continue; } + if (inCode) continue; + var m = line.match(/^(#{1,6})\s+(.+)/); + if (m) { + result.push({ level: m[1].length, text: m[2].replace(/[*_`\[\]]/g, '').trim(), line: i }); + } + } + return result; + } + + function DocumentOutline({ headings, viewMode, cmEditorRef, previewRef }) { + var [collapsed, setCollapsed] = useState(false); + + if (!headings || headings.length === 0) return null; + + 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' }) }); + } + } + + return html` +