Feat v0.4.4 rich editor import export
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
26
CHANGELOG.md
26
CHANGELOG.md
@@ -2,6 +2,32 @@
|
|||||||
|
|
||||||
All notable changes to Switchboard Core are documented here.
|
All notable changes to Switchboard Core are documented here.
|
||||||
|
|
||||||
|
## v0.4.4 — Rich Editor + Import/Export
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **CodeMirror 6 integration**: Notes editor now uses the vendored CM6 bundle
|
||||||
|
(`CM.noteEditor()`) for rich markdown editing with syntax highlighting,
|
||||||
|
wikilink autocomplete (`[[` triggers note title completion), and inline
|
||||||
|
preview decorations for headings, code blocks, and blockquotes.
|
||||||
|
- **CM6 dynamic loading**: Bundle loaded via `<script>` tag at boot time.
|
||||||
|
Falls back to plain textarea if CM6 is unavailable.
|
||||||
|
- **Export as .md**: Export button in editor header downloads the note as a
|
||||||
|
Markdown file with YAML frontmatter (title, tags, created date).
|
||||||
|
- **Import .md**: Import button in topbar allows uploading `.md`/`.markdown`/`.txt`
|
||||||
|
files. Parses YAML frontmatter for title and tags, creates note via API.
|
||||||
|
- **CSS for CM6**: Editor fills the container with `max-height: none` override
|
||||||
|
and proper scroller padding.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Notes package version bumped from 0.4.0 to 0.5.0.
|
||||||
|
- `EditorPane` component refactored to support CM6 with mutable refs for
|
||||||
|
title/body (required for CM6 callback closures).
|
||||||
|
- Ctrl/Cmd+S save works in both CM6 and textarea fallback modes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v0.4.3 — Backlinks + Wikilinks
|
## v0.4.3 — Backlinks + Wikilinks
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
13
ROADMAP.md
13
ROADMAP.md
@@ -1,6 +1,6 @@
|
|||||||
# Switchboard Core — Roadmap
|
# Switchboard Core — Roadmap
|
||||||
|
|
||||||
## Current: v0.4.3 — Backlinks + Wikilinks
|
## Current: v0.4.4 — Rich Editor + Import/Export
|
||||||
|
|
||||||
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
|
||||||
@@ -281,10 +281,15 @@ Zero platform special-casing. Proves the full extension stack E2E.
|
|||||||
| Backlinks panel | ✅ | Collapsible panel below editor showing all notes linking to current note. Click to navigate. Hidden when empty. |
|
| Backlinks panel | ✅ | Collapsible panel below editor showing all notes linking to current note. Click to navigate. Hidden when empty. |
|
||||||
| Stats update | ✅ | `/stats` includes `links` count. Notes package version bumped to 0.4.0. |
|
| Stats update | ✅ | `/stats` includes `links` count. Notes package version bumped to 0.4.0. |
|
||||||
|
|
||||||
### v0.4.4 — Rich Editor + Import/Export (planned)
|
### v0.4.4 — Rich Editor + Import/Export (complete)
|
||||||
|
|
||||||
- Vendored CodeMirror 6 markdown bundle
|
| Step | Status | Description |
|
||||||
- Markdown file import/export
|
|------|--------|-------------|
|
||||||
|
| CM6 integration | ✅ | Load vendored CodeMirror 6 bundle via dynamic `<script>` tag. `CM.noteEditor()` factory with markdown syntax highlighting, wikilink autocomplete, and live preview decorations. Textarea fallback if CM6 unavailable. |
|
||||||
|
| Editor wiring | ✅ | EditorPane uses CM6 with `onChange` → auto-save debounce, `onLink` → wikilink navigation, `linkCompleter` → search API autocomplete. Ctrl/Cmd+S force save. Editor fills container via CSS override. |
|
||||||
|
| Export as .md | ✅ | Export button in editor header. Downloads note as `.md` file with YAML frontmatter (title, tags, created). Pure client-side Blob download. |
|
||||||
|
| 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.5.0 — MVP
|
## v0.5.0 — MVP
|
||||||
|
|
||||||
|
|||||||
@@ -281,6 +281,20 @@
|
|||||||
color: var(--text-3);
|
color: var(--text-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── CodeMirror 6 container ─────────────── */
|
||||||
|
.notes-editor__cm {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.notes-editor__cm .cm-editor {
|
||||||
|
height: 100%;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
.notes-editor__cm .cm-scroller {
|
||||||
|
overflow: auto;
|
||||||
|
padding: 12px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Preview ─────────────────────────────── */
|
/* ── Preview ─────────────────────────────── */
|
||||||
.notes-preview {
|
.notes-preview {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* Notes — Surface Entry Point (v0.4.0)
|
* Notes — Surface Entry Point (v0.5.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
|
||||||
* sw.ui.* — primitive components
|
* sw.ui.* — primitive components
|
||||||
* sw.shell.Topbar — navigation bar
|
* sw.shell.Topbar — navigation bar
|
||||||
|
* window.CM — CodeMirror 6 bundle (optional)
|
||||||
*/
|
*/
|
||||||
(async function () {
|
(async function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
@@ -15,6 +16,22 @@
|
|||||||
var base = window.__BASE__ || '';
|
var base = window.__BASE__ || '';
|
||||||
var ver = window.__VERSION__ || '0';
|
var ver = window.__VERSION__ || '0';
|
||||||
|
|
||||||
|
// ── Dynamic Script Loader ────────────────────
|
||||||
|
function _loadScript(src) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
if (document.querySelector('script[src*="' + src.split('?')[0] + '"]')) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var s = document.createElement('script');
|
||||||
|
s.src = base + src;
|
||||||
|
s.type = 'module';
|
||||||
|
s.onload = resolve;
|
||||||
|
s.onerror = function() { resolve(); }; // Non-fatal
|
||||||
|
document.head.appendChild(s);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── Boot SDK ───────────────────────────────
|
// ── Boot SDK ───────────────────────────────
|
||||||
try {
|
try {
|
||||||
if (!window.preact) {
|
if (!window.preact) {
|
||||||
@@ -32,6 +49,10 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Load CM6 bundle (optional — falls back to textarea) ──
|
||||||
|
await _loadScript('/vendor/codemirror/codemirror.bundle.js?v=' + ver);
|
||||||
|
var hasCM = !!(window.CM && window.CM.noteEditor);
|
||||||
|
|
||||||
var { html } = window;
|
var { html } = window;
|
||||||
var { useState, useEffect, useCallback, useRef, useMemo } = hooks;
|
var { useState, useEffect, useCallback, useRef, useMemo } = hooks;
|
||||||
var { render } = preact;
|
var { render } = preact;
|
||||||
@@ -537,6 +558,56 @@
|
|||||||
// EditorPane — note editor + preview + tags
|
// EditorPane — note editor + preview + tags
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// Export / Import helpers
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
|
function exportNoteAsMarkdown(note, tags) {
|
||||||
|
var parts = ['---'];
|
||||||
|
parts.push('title: ' + (note.title || 'Untitled'));
|
||||||
|
if (tags && tags.length) parts.push('tags: [' + tags.join(', ') + ']');
|
||||||
|
if (note.created_at) parts.push('created: ' + note.created_at);
|
||||||
|
parts.push('---');
|
||||||
|
parts.push('');
|
||||||
|
parts.push(note.body || '');
|
||||||
|
var text = parts.join('\n');
|
||||||
|
var blob = new Blob([text], { type: 'text/markdown;charset=utf-8' });
|
||||||
|
var url = URL.createObjectURL(blob);
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = (note.title || 'Untitled').replace(/[^a-zA-Z0-9_\- ]/g, '').trim() + '.md';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFrontmatter(text) {
|
||||||
|
var result = { title: '', tags: [], body: text };
|
||||||
|
if (!text.startsWith('---')) return result;
|
||||||
|
var end = text.indexOf('\n---', 3);
|
||||||
|
if (end === -1) return result;
|
||||||
|
var fm = text.substring(3, end).trim();
|
||||||
|
var bodyStart = end + 4;
|
||||||
|
// skip leading newline after frontmatter
|
||||||
|
if (text[bodyStart] === '\n') bodyStart++;
|
||||||
|
result.body = text.substring(bodyStart);
|
||||||
|
var lines = fm.split('\n');
|
||||||
|
for (var i = 0; i < lines.length; i++) {
|
||||||
|
var line = lines[i].trim();
|
||||||
|
if (line.startsWith('title:')) {
|
||||||
|
result.title = line.substring(6).trim().replace(/^["']|["']$/g, '');
|
||||||
|
} else if (line.startsWith('tags:')) {
|
||||||
|
var tagStr = line.substring(5).trim();
|
||||||
|
// parse [tag1, tag2] or tag1, tag2
|
||||||
|
tagStr = tagStr.replace(/^\[|\]$/g, '');
|
||||||
|
result.tags = tagStr.split(',').map(function(t) { return t.trim().toLowerCase(); }).filter(Boolean);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
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 : '');
|
||||||
@@ -546,28 +617,102 @@
|
|||||||
var [saving, setSaving] = useState(false);
|
var [saving, setSaving] = useState(false);
|
||||||
var [saveCount, setSaveCount] = useState(0);
|
var [saveCount, setSaveCount] = useState(0);
|
||||||
var textareaRef = useRef(null);
|
var textareaRef = useRef(null);
|
||||||
|
var cmContainerRef = useRef(null);
|
||||||
|
var cmEditorRef = useRef(null);
|
||||||
|
|
||||||
|
// keep mutable refs for the latest title/body so CM6 callbacks see current values
|
||||||
|
var titleRef = useRef(title);
|
||||||
|
var bodyRef = useRef(body);
|
||||||
|
titleRef.current = title;
|
||||||
|
bodyRef.current = body;
|
||||||
|
|
||||||
// sync when note changes
|
// sync when note changes
|
||||||
useEffect(function() {
|
useEffect(function() {
|
||||||
if (note) {
|
if (note) {
|
||||||
setTitle(note.title || '');
|
setTitle(note.title || '');
|
||||||
setBody(note.body || '');
|
setBody(note.body || '');
|
||||||
|
titleRef.current = note.title || '';
|
||||||
|
bodyRef.current = note.body || '';
|
||||||
setNoteTags(note.tags || []);
|
setNoteTags(note.tags || []);
|
||||||
setDirty(false);
|
setDirty(false);
|
||||||
setPreview(false);
|
setPreview(false);
|
||||||
|
// update CM6 content if editor exists
|
||||||
|
if (cmEditorRef.current) {
|
||||||
|
cmEditorRef.current.setValue(note.body || '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [note ? note.id : null]);
|
}, [note ? note.id : null]);
|
||||||
|
|
||||||
|
// ── CM6 lifecycle ─────────────────────────
|
||||||
|
useEffect(function() {
|
||||||
|
if (!hasCM || preview || !note) return;
|
||||||
|
var el = cmContainerRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
// destroy previous
|
||||||
|
if (cmEditorRef.current) {
|
||||||
|
cmEditorRef.current.destroy();
|
||||||
|
cmEditorRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var editor = CM.noteEditor(el, {
|
||||||
|
value: bodyRef.current,
|
||||||
|
darkMode: document.documentElement.getAttribute('data-theme') !== 'light',
|
||||||
|
onChange: function(text) {
|
||||||
|
bodyRef.current = text;
|
||||||
|
setBody(text);
|
||||||
|
setDirty(true);
|
||||||
|
debounce('save', function() { doSave(titleRef.current, text); }, 1000);
|
||||||
|
},
|
||||||
|
onLink: function(linkTitle) {
|
||||||
|
if (onNavigateToTitle) onNavigateToTitle(linkTitle);
|
||||||
|
},
|
||||||
|
linkCompleter: async function(query) {
|
||||||
|
try {
|
||||||
|
var res = await api.get('/search?q=' + encodeURIComponent(query));
|
||||||
|
var items = (res && res.data) || res || [];
|
||||||
|
if (!Array.isArray(items)) items = [];
|
||||||
|
return items.map(function(n) { return { label: n.title, id: n.id }; });
|
||||||
|
} catch (e) { return []; }
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
cmEditorRef.current = editor;
|
||||||
|
|
||||||
|
// Ctrl/Cmd+S via CM6 keymap injection
|
||||||
|
var view = editor.getView();
|
||||||
|
if (view) {
|
||||||
|
var handleSaveKey = function(e) {
|
||||||
|
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
doSave(titleRef.current, bodyRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
view.dom.addEventListener('keydown', handleSaveKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.focus();
|
||||||
|
|
||||||
|
return function() {
|
||||||
|
if (cmEditorRef.current) {
|
||||||
|
cmEditorRef.current.destroy();
|
||||||
|
cmEditorRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [note ? note.id : null, preview]);
|
||||||
|
|
||||||
function handleTitleChange(e) {
|
function handleTitleChange(e) {
|
||||||
setTitle(e.target.value);
|
setTitle(e.target.value);
|
||||||
|
titleRef.current = e.target.value;
|
||||||
setDirty(true);
|
setDirty(true);
|
||||||
debounce('save', function() { doSave(e.target.value, body); }, 1000);
|
debounce('save', function() { doSave(e.target.value, bodyRef.current); }, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBodyChange(e) {
|
function handleBodyChange(e) {
|
||||||
setBody(e.target.value);
|
setBody(e.target.value);
|
||||||
|
bodyRef.current = e.target.value;
|
||||||
setDirty(true);
|
setDirty(true);
|
||||||
debounce('save', function() { doSave(title, e.target.value); }, 1000);
|
debounce('save', function() { doSave(titleRef.current, e.target.value); }, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doSave(t, b) {
|
async function doSave(t, b) {
|
||||||
@@ -608,6 +753,11 @@
|
|||||||
onDelete();
|
onDelete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleExport() {
|
||||||
|
if (!note) return;
|
||||||
|
exportNoteAsMarkdown(note, noteTags);
|
||||||
|
}
|
||||||
|
|
||||||
function handleKeyDown(e) {
|
function handleKeyDown(e) {
|
||||||
// Tab inserts two spaces
|
// Tab inserts two spaces
|
||||||
if (e.key === 'Tab') {
|
if (e.key === 'Tab') {
|
||||||
@@ -619,6 +769,7 @@
|
|||||||
ta.value = val.substring(0, start) + ' ' + val.substring(end);
|
ta.value = val.substring(0, start) + ' ' + val.substring(end);
|
||||||
ta.selectionStart = ta.selectionEnd = start + 2;
|
ta.selectionStart = ta.selectionEnd = start + 2;
|
||||||
setBody(ta.value);
|
setBody(ta.value);
|
||||||
|
bodyRef.current = ta.value;
|
||||||
setDirty(true);
|
setDirty(true);
|
||||||
debounce('save', function() { doSave(title, ta.value); }, 1000);
|
debounce('save', function() { doSave(title, ta.value); }, 1000);
|
||||||
}
|
}
|
||||||
@@ -708,6 +859,21 @@
|
|||||||
return html`<div class="notes-editor__empty">Select a note or create a new one</div>`;
|
return html`<div class="notes-editor__empty">Select a note or create a new one</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Render editor body ─────────────────────
|
||||||
|
function renderEditorBody() {
|
||||||
|
if (preview) {
|
||||||
|
return html`<div class="notes-preview" ref=${previewRef} dangerouslySetInnerHTML=${{ __html: previewHtml }} />`;
|
||||||
|
}
|
||||||
|
if (hasCM) {
|
||||||
|
return html`<div class="notes-editor__cm" ref=${cmContainerRef} />`;
|
||||||
|
}
|
||||||
|
// fallback textarea
|
||||||
|
return html`<textarea class="notes-editor__textarea" ref=${textareaRef}
|
||||||
|
value=${body} onInput=${handleBodyChange}
|
||||||
|
onKeyDown=${handleKeyDown}
|
||||||
|
placeholder="Start writing in Markdown…" />`;
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="notes-editor">
|
<div class="notes-editor">
|
||||||
<div class="notes-editor__header">
|
<div class="notes-editor__header">
|
||||||
@@ -733,6 +899,7 @@
|
|||||||
onClick=${() => setPreview(!preview)}>
|
onClick=${() => setPreview(!preview)}>
|
||||||
${preview ? 'Edit' : 'Preview'}
|
${preview ? 'Edit' : 'Preview'}
|
||||||
</button>
|
</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}
|
||||||
title=${note.pinned ? 'Unpin' : 'Pin'}>
|
title=${note.pinned ? 'Unpin' : 'Pin'}>
|
||||||
${note.pinned ? '📌' : '📍'}
|
${note.pinned ? '📌' : '📍'}
|
||||||
@@ -743,13 +910,7 @@
|
|||||||
</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__body">
|
||||||
${preview
|
${renderEditorBody()}
|
||||||
? html`<div class="notes-preview" ref=${previewRef} dangerouslySetInnerHTML=${{ __html: previewHtml }} />`
|
|
||||||
: html`<textarea class="notes-editor__textarea" ref=${textareaRef}
|
|
||||||
value=${body} onInput=${handleBodyChange}
|
|
||||||
onKeyDown=${handleKeyDown}
|
|
||||||
placeholder="Start writing in Markdown…" />`
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<${BacklinksPanel} noteId=${note.id} onNavigate=${onNavigate} />
|
<${BacklinksPanel} noteId=${note.id} onNavigate=${onNavigate} />
|
||||||
</div>
|
</div>
|
||||||
@@ -986,6 +1147,46 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Import .md file ─────────────────────
|
||||||
|
function handleImport() {
|
||||||
|
var input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.md,.markdown,.txt';
|
||||||
|
input.style.display = 'none';
|
||||||
|
input.onchange = async function() {
|
||||||
|
var file = input.files && input.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
try {
|
||||||
|
var text = await new Promise(function(resolve, reject) {
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function() { resolve(reader.result); };
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsText(file);
|
||||||
|
});
|
||||||
|
var parsed = parseFrontmatter(text);
|
||||||
|
var noteTitle = parsed.title || file.name.replace(/\.(md|markdown|txt)$/i, '') || 'Imported';
|
||||||
|
var payload = { title: noteTitle, body: parsed.body };
|
||||||
|
if (activeFolderId) payload.folder_id = activeFolderId;
|
||||||
|
var newNote = await api.post('/notes', payload);
|
||||||
|
if (newNote && newNote.id) {
|
||||||
|
if (parsed.tags.length) {
|
||||||
|
await api.put('/tags/' + newNote.id, { tags: parsed.tags });
|
||||||
|
}
|
||||||
|
setActiveId(newNote.id);
|
||||||
|
loadNote(newNote.id);
|
||||||
|
loadNotes();
|
||||||
|
loadStats();
|
||||||
|
loadTags();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Import failed:', e);
|
||||||
|
}
|
||||||
|
document.body.removeChild(input);
|
||||||
|
};
|
||||||
|
document.body.appendChild(input);
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Search ────────────────────────────────
|
// ── Search ────────────────────────────────
|
||||||
function handleSearch(e) {
|
function handleSearch(e) {
|
||||||
var val = e.target.value;
|
var val = e.target.value;
|
||||||
@@ -996,6 +1197,7 @@
|
|||||||
return html`
|
return html`
|
||||||
<${Topbar} title="Notes">
|
<${Topbar} title="Notes">
|
||||||
<${Button} variant="primary" size="sm" onClick=${handleNew}>+ New Note<//>
|
<${Button} variant="primary" size="sm" onClick=${handleNew}>+ New Note<//>
|
||||||
|
<${Button} variant="secondary" size="sm" onClick=${handleImport}>Import .md<//>
|
||||||
<//>
|
<//>
|
||||||
<div class="notes-app">
|
<div class="notes-app">
|
||||||
<div class="notes-sidebar">
|
<div class="notes-sidebar">
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
"route": "/s/notes",
|
"route": "/s/notes",
|
||||||
"auth": "authenticated",
|
"auth": "authenticated",
|
||||||
"layout": "single",
|
"layout": "single",
|
||||||
"version": "0.4.0",
|
"version": "0.5.0",
|
||||||
"icon": "📝",
|
"icon": "📝",
|
||||||
"description": "Markdown notes surface with folders, tags, and backlinks.",
|
"description": "Markdown notes surface with rich editor, folders, tags, backlinks, and import/export.",
|
||||||
"author": "switchboard",
|
"author": "switchboard",
|
||||||
|
|
||||||
"permissions": ["db.write"],
|
"permissions": ["db.write"],
|
||||||
|
|||||||
Reference in New Issue
Block a user