Feat v0.4.5 editor modes outline (#27)
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 #27.
This commit is contained in:
30
CHANGELOG.md
30
CHANGELOG.md
@@ -2,6 +2,36 @@
|
|||||||
|
|
||||||
All notable changes to Switchboard Core are documented here.
|
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
|
## v0.4.4 — Rich Editor + Import/Export
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
14
ROADMAP.md
14
ROADMAP.md
@@ -1,6 +1,6 @@
|
|||||||
# Switchboard Core — Roadmap
|
# 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
|
Fork of chat-switchboard, gutted to a pure extension platform. All AI/chat
|
||||||
features removed from the kernel. What remains is the minimum viable
|
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. |
|
| 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. |
|
| 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 |
|
| Step | Status | Description |
|
||||||
|------|--------|-------------|
|
|------|--------|-------------|
|
||||||
| Default-rendered mode | | Preview is the initial state on note open. "Edit" button enters CM6. Save exits back to rendered view. |
|
| 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. New `viewMode` state: `rendered` / `edit` / `split`. CSS grid two-column in `.notes-editor__body`. |
|
| 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. |
|
| 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. |
|
| View mode setting | ✅ | `editor_mode` in manifest settings + localStorage persistence. Toolbar cycles rendered → edit → split. |
|
||||||
| 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. |
|
| 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 (planned)
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ Core CRUD with markdown textarea and inline preview.
|
|||||||
- v0.4.2: Tags + enhanced search
|
- v0.4.2: Tags + enhanced search
|
||||||
- v0.4.3: Backlinks + `[[wikilinks]]`
|
- v0.4.3: Backlinks + `[[wikilinks]]`
|
||||||
- v0.4.4: Rich editor (CodeMirror 6) + import/export
|
- v0.4.4: Rich editor (CodeMirror 6) + import/export
|
||||||
|
- v0.4.5: Editor modes (rendered/edit/split) + document outline
|
||||||
|
|
||||||
## Data
|
## Data
|
||||||
|
|
||||||
|
|||||||
@@ -257,6 +257,14 @@
|
|||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Editor Content (body + outline wrapper) */
|
||||||
|
.notes-editor__content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Editor Body ─────────────────────────── */
|
/* ── Editor Body ─────────────────────────── */
|
||||||
.notes-editor__body {
|
.notes-editor__body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -264,6 +272,13 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
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 {
|
.notes-editor__textarea {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
resize: none;
|
resize: none;
|
||||||
@@ -617,9 +632,64 @@
|
|||||||
font-size: 11px;
|
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 ──────────────────────────── */
|
/* ── Responsive ──────────────────────────── */
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
.notes-sidebar { width: 100%; border-right: none; border-bottom: 1px solid var(--border); max-height: 40vh; }
|
.notes-sidebar { width: 100%; border-right: none; border-bottom: 1px solid var(--border); max-height: 40vh; }
|
||||||
.notes-app { flex-direction: column; }
|
.notes-app { flex-direction: column; }
|
||||||
.folder-tree { max-height: 30vh; }
|
.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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Notes — Surface Entry Point (v0.5.0)
|
* Notes — Surface Entry Point (v0.6.0)
|
||||||
*
|
*
|
||||||
* Markdown notes surface using the SDK:
|
* Markdown notes surface using the SDK:
|
||||||
* sw.api.ext('notes') — scoped API client
|
* 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`
|
||||||
|
<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) {
|
||||||
|
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); }}>
|
||||||
|
${h.text}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }) {
|
||||||
var [title, setTitle] = useState(note ? note.title : '');
|
var [title, setTitle] = useState(note ? note.title : '');
|
||||||
var [body, setBody] = useState(note ? note.body : '');
|
var [body, setBody] = useState(note ? note.body : '');
|
||||||
var [noteTags, setNoteTags] = useState([]);
|
var [noteTags, setNoteTags] = useState([]);
|
||||||
var [preview, setPreview] = useState(false);
|
var [viewMode, setViewMode] = useState(_modeStore.get('editor_mode') || 'rendered');
|
||||||
var [dirty, setDirty] = useState(false);
|
var [dirty, setDirty] = useState(false);
|
||||||
var [saving, setSaving] = useState(false);
|
var [saving, setSaving] = useState(false);
|
||||||
var [saveCount, setSaveCount] = useState(0);
|
var [saveCount, setSaveCount] = useState(0);
|
||||||
@@ -634,7 +703,7 @@
|
|||||||
bodyRef.current = note.body || '';
|
bodyRef.current = note.body || '';
|
||||||
setNoteTags(note.tags || []);
|
setNoteTags(note.tags || []);
|
||||||
setDirty(false);
|
setDirty(false);
|
||||||
setPreview(false);
|
setViewMode(_modeStore.get('editor_mode') || 'rendered');
|
||||||
// update CM6 content if editor exists
|
// update CM6 content if editor exists
|
||||||
if (cmEditorRef.current) {
|
if (cmEditorRef.current) {
|
||||||
cmEditorRef.current.setValue(note.body || '');
|
cmEditorRef.current.setValue(note.body || '');
|
||||||
@@ -643,8 +712,9 @@
|
|||||||
}, [note ? note.id : null]);
|
}, [note ? note.id : null]);
|
||||||
|
|
||||||
// ── CM6 lifecycle ─────────────────────────
|
// ── CM6 lifecycle ─────────────────────────
|
||||||
|
var needsCM = viewMode !== 'rendered';
|
||||||
useEffect(function() {
|
useEffect(function() {
|
||||||
if (!hasCM || preview || !note) return;
|
if (!hasCM || !needsCM || !note) return;
|
||||||
var el = cmContainerRef.current;
|
var el = cmContainerRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
@@ -698,7 +768,7 @@
|
|||||||
cmEditorRef.current = null;
|
cmEditorRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [note ? note.id : null, preview]);
|
}, [note ? note.id : null, needsCM]);
|
||||||
|
|
||||||
function handleTitleChange(e) {
|
function handleTitleChange(e) {
|
||||||
setTitle(e.target.value);
|
setTitle(e.target.value);
|
||||||
@@ -807,7 +877,7 @@
|
|||||||
var previewRef = useRef(null);
|
var previewRef = useRef(null);
|
||||||
useEffect(function() {
|
useEffect(function() {
|
||||||
var el = previewRef.current;
|
var el = previewRef.current;
|
||||||
if (!el || !preview) return;
|
if (!el || viewMode === 'edit') return;
|
||||||
function handleClick(e) {
|
function handleClick(e) {
|
||||||
var target = e.target;
|
var target = e.target;
|
||||||
if (target.classList && target.classList.contains('wikilink')) {
|
if (target.classList && target.classList.contains('wikilink')) {
|
||||||
@@ -818,7 +888,35 @@
|
|||||||
}
|
}
|
||||||
el.addEventListener('click', handleClick);
|
el.addEventListener('click', handleClick);
|
||||||
return function() { el.removeEventListener('click', handleClick); };
|
return function() { el.removeEventListener('click', handleClick); };
|
||||||
}, [preview, onNavigateToTitle]);
|
}, [viewMode, onNavigateToTitle]);
|
||||||
|
|
||||||
|
// ── Synced scroll in split mode ──
|
||||||
|
useEffect(function() {
|
||||||
|
if (viewMode !== 'split' || !cmEditorRef.current) return;
|
||||||
|
var view = cmEditorRef.current.getView();
|
||||||
|
if (!view) return;
|
||||||
|
var scrollDOM = view.scrollDOM;
|
||||||
|
var previewEl = previewRef.current;
|
||||||
|
if (!scrollDOM || !previewEl) return;
|
||||||
|
var syncing = false;
|
||||||
|
function onEditorScroll() {
|
||||||
|
if (syncing) return;
|
||||||
|
syncing = true;
|
||||||
|
var ratio = scrollDOM.scrollTop / (scrollDOM.scrollHeight - scrollDOM.clientHeight || 1);
|
||||||
|
previewEl.scrollTop = ratio * (previewEl.scrollHeight - previewEl.clientHeight);
|
||||||
|
syncing = false;
|
||||||
|
}
|
||||||
|
scrollDOM.addEventListener('scroll', onEditorScroll);
|
||||||
|
return function() { scrollDOM.removeEventListener('scroll', onEditorScroll); };
|
||||||
|
}, [viewMode, note ? note.id : null]);
|
||||||
|
|
||||||
|
// ── Document outline headings ──
|
||||||
|
var [headings, setHeadings] = useState([]);
|
||||||
|
useEffect(function() {
|
||||||
|
debounce('outline', function() {
|
||||||
|
setHeadings(parseHeadings(body));
|
||||||
|
}, 300);
|
||||||
|
}, [body]);
|
||||||
|
|
||||||
// build indented folder list for the select dropdown
|
// build indented folder list for the select dropdown
|
||||||
var folderOptions = useMemo(function() {
|
var folderOptions = useMemo(function() {
|
||||||
@@ -860,13 +958,25 @@
|
|||||||
|
|
||||||
// ── Render editor body ─────────────────────
|
// ── Render editor body ─────────────────────
|
||||||
function renderEditorBody() {
|
function renderEditorBody() {
|
||||||
if (preview) {
|
if (viewMode === 'rendered') {
|
||||||
return html`<div class="notes-preview" ref=${previewRef} dangerouslySetInnerHTML=${{ __html: previewHtml }} />`;
|
return html`<div class="notes-preview" ref=${previewRef} dangerouslySetInnerHTML=${{ __html: previewHtml }} />`;
|
||||||
}
|
}
|
||||||
|
if (viewMode === 'split') {
|
||||||
|
var editor = hasCM
|
||||||
|
? html`<div class="notes-editor__cm" ref=${cmContainerRef} />`
|
||||||
|
: html`<textarea class="notes-editor__textarea" ref=${textareaRef}
|
||||||
|
value=${body} onInput=${handleBodyChange}
|
||||||
|
onKeyDown=${handleKeyDown}
|
||||||
|
placeholder="Start writing in Markdown…" />`;
|
||||||
|
return [
|
||||||
|
editor,
|
||||||
|
html`<div class="notes-preview" ref=${previewRef} dangerouslySetInnerHTML=${{ __html: previewHtml }} />`
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// edit mode
|
||||||
if (hasCM) {
|
if (hasCM) {
|
||||||
return html`<div class="notes-editor__cm" ref=${cmContainerRef} />`;
|
return html`<div class="notes-editor__cm" ref=${cmContainerRef} />`;
|
||||||
}
|
}
|
||||||
// fallback textarea
|
|
||||||
return html`<textarea class="notes-editor__textarea" ref=${textareaRef}
|
return html`<textarea class="notes-editor__textarea" ref=${textareaRef}
|
||||||
value=${body} onInput=${handleBodyChange}
|
value=${body} onInput=${handleBodyChange}
|
||||||
onKeyDown=${handleKeyDown}
|
onKeyDown=${handleKeyDown}
|
||||||
@@ -894,9 +1004,12 @@
|
|||||||
<span class=${dirty ? 'notes-saved notes-saved--dirty' : 'notes-saved'}>
|
<span class=${dirty ? 'notes-saved notes-saved--dirty' : 'notes-saved'}>
|
||||||
${saving ? 'Saving…' : (dirty ? 'Unsaved' : 'Saved')}
|
${saving ? 'Saving…' : (dirty ? 'Unsaved' : 'Saved')}
|
||||||
</span>
|
</span>
|
||||||
<button class="notes-toggle ${preview ? 'notes-toggle--active' : ''}"
|
<button class="notes-toggle" onClick=${function() {
|
||||||
onClick=${() => setPreview(!preview)}>
|
var next = viewMode === 'rendered' ? 'edit' : viewMode === 'edit' ? 'split' : 'rendered';
|
||||||
${preview ? 'Edit' : 'Preview'}
|
_modeStore.set('editor_mode', next);
|
||||||
|
setViewMode(next);
|
||||||
|
}}>
|
||||||
|
${viewMode === 'rendered' ? 'Edit' : viewMode === 'edit' ? 'Split' : 'Read'}
|
||||||
</button>
|
</button>
|
||||||
<button class="notes-btn" onClick=${handleExport} title="Export as .md">Export</button>
|
<button class="notes-btn" onClick=${handleExport} title="Export as .md">Export</button>
|
||||||
<button class="notes-btn" onClick=${handlePin}
|
<button class="notes-btn" onClick=${handlePin}
|
||||||
@@ -908,8 +1021,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<${TagInput} tags=${noteTags} allTags=${allTags} onChange=${handleTagsChange} />
|
<${TagInput} tags=${noteTags} allTags=${allTags} onChange=${handleTagsChange} />
|
||||||
<div class="notes-editor__body">
|
<div class="notes-editor__content">
|
||||||
${renderEditorBody()}
|
<div class="notes-editor__body ${viewMode === 'split' ? 'notes-editor__body--split' : ''}">
|
||||||
|
${renderEditorBody()}
|
||||||
|
</div>
|
||||||
|
<${DocumentOutline} headings=${headings} viewMode=${viewMode}
|
||||||
|
cmEditorRef=${cmEditorRef} previewRef=${previewRef} />
|
||||||
</div>
|
</div>
|
||||||
<${BacklinksPanel} noteId=${note.id} onNavigate=${onNavigate} />
|
<${BacklinksPanel} noteId=${note.id} onNavigate=${onNavigate} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
"route": "/s/notes",
|
"route": "/s/notes",
|
||||||
"auth": "authenticated",
|
"auth": "authenticated",
|
||||||
"layout": "single",
|
"layout": "single",
|
||||||
"version": "0.5.0",
|
"version": "0.6.0",
|
||||||
"icon": "📝",
|
"icon": "📝",
|
||||||
"description": "Markdown notes surface with rich editor, folders, tags, backlinks, and import/export.",
|
"description": "Markdown notes surface with rich editor, folders, tags, backlinks, import/export, and document outline.",
|
||||||
"author": "switchboard",
|
"author": "switchboard",
|
||||||
|
|
||||||
"permissions": ["db.write"],
|
"permissions": ["db.write"],
|
||||||
@@ -92,6 +92,12 @@
|
|||||||
"label": "Default View",
|
"label": "Default View",
|
||||||
"description": "Default note list view: recent or pinned",
|
"description": "Default note list view: recent or pinned",
|
||||||
"default": "recent"
|
"default": "recent"
|
||||||
|
},
|
||||||
|
"editor_mode": {
|
||||||
|
"type": "string",
|
||||||
|
"label": "Default Editor Mode",
|
||||||
|
"description": "How notes open: rendered (read-only preview), edit (CodeMirror editor), or split (side-by-side)",
|
||||||
|
"default": "rendered"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user