diff --git a/plugins/notes/frontend/src/index.js b/plugins/notes/frontend/src/index.js index 1768be2..de7bded 100644 --- a/plugins/notes/frontend/src/index.js +++ b/plugins/notes/frontend/src/index.js @@ -50,6 +50,8 @@ '.notes-modal-btn.cancel:hover{background:#3a3a5e}', '.notes-modal-btn.confirm{background:#4ecca3;color:#111;border-color:#4ecca3}', '.notes-modal-btn.confirm:hover{background:#3dbb92}', + '.notes-modal-btn.danger{background:#7a2a35;color:#fff;border-color:#b84a5a}', + '.notes-modal-btn.danger:hover{background:#963545}', '.notes-status{font-size:.72rem;color:#8b8ba8;padding:.15rem .5rem;white-space:nowrap}', '.notes-status.success{color:#4ecca3}', '.notes-status.error{color:#e74c3c}', @@ -88,7 +90,8 @@ add: 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z', rename: 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z', open: 'M14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7zM5 5h6V3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-6h-2v6H5V5z', - search: 'M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z' + search: 'M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z', + trash: 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM8 9h8v10H8V9zm7.5-5-1-1h-5l-1 1H5v2h14V4z' }; function iconSvg(name) { @@ -336,8 +339,16 @@ el('button', { className: 'notes-item-btn', title: 'Rename', + 'data-note-action': 'rename', innerHTML: iconSvg('rename'), onClick: function (e) { e.stopPropagation(); beginRename(note); } + }), + el('button', { + className: 'notes-item-btn', + title: 'Move to Trash', + 'data-note-action': 'trash', + innerHTML: iconSvg('trash'), + onClick: function (e) { e.stopPropagation(); confirmTrashNote(note); } }) ]) ]); @@ -460,6 +471,24 @@ }); } + // ─── Trash ────────────────────────────────────────────── + + function confirmTrashNote(note) { + if (!note) return; + showTrashModal(note).then(function (confirmed) { + if (!confirmed || disposed) return; + setStatus('Moving note to trash...', 'loading'); + api.files.trash(note.path).then(function () { + if (disposed) return; + if (selectedPath === note.path) selectedPath = ''; + setStatus('Note moved to trash', 'success'); + loadNotes(); + }).catch(function (err) { + setStatus('Error: ' + (err.message || err), 'error'); + }); + }); + } + // ─── Conflict Modal ───────────────────────────────────── function showConflictModal(title, existingPath) { @@ -478,6 +507,28 @@ document.body.appendChild(overlay); } + function showTrashModal(note) { + return new Promise(function (resolve) { + var overlay = el('div', { className: 'notes-modal-overlay' }); + function close(value) { + overlay.remove(); + resolve(value); + } + var modal = el('div', { className: 'notes-modal' }, [ + el('div', { className: 'notes-modal-title' }, ['Move Note to Trash']), + el('div', { className: 'notes-modal-msg' }, [ + 'Move "' + (note.title || fileName(note.path)) + '" to trash?' + ]), + el('div', { className: 'notes-modal-actions' }, [ + el('button', { className: 'notes-modal-btn cancel', textContent: 'Cancel', onClick: function () { close(false); } }), + el('button', { className: 'notes-modal-btn danger', 'data-notes-confirm-trash': '', textContent: 'Move to Trash', onClick: function () { close(true); } }) + ]) + ]); + overlay.appendChild(modal); + document.body.appendChild(overlay); + }); + } + // ─── Event Wiring ─────────────────────────────────────── createBtn.addEventListener('click', showCreate); diff --git a/plugins/notes/plugin.json b/plugins/notes/plugin.json index d9e6451..4c2d3ff 100644 --- a/plugins/notes/plugin.json +++ b/plugins/notes/plugin.json @@ -4,7 +4,7 @@ "name": "Notes", "version": "0.1.0", "apiVersion": "0.1.0", - "description": "Workspace-scoped notes manager for Markdown files in canonical Notes folders with create, rename, and Workbench integration.", + "description": "Workspace-scoped notes manager for Markdown files in canonical Notes folders with create, rename, trash, and Workbench integration.", "source": "official", "icon": "edit", "provides": [ @@ -17,6 +17,7 @@ "permissions": [ "files.read", "files.write", + "files.delete", "workbench.open", "ui.register" ], diff --git a/scripts/smoke-notes-plugin.js b/scripts/smoke-notes-plugin.js index 45772e0..f6e0ea4 100755 --- a/scripts/smoke-notes-plugin.js +++ b/scripts/smoke-notes-plugin.js @@ -170,6 +170,16 @@ function makeApi(options = {}) { entries.set(toRelativePath, entry); entries.delete(fromRelativePath); }, + trash: async (relativePath) => { + if (!entries.has(relativePath)) throw new Error(`not-found: ${relativePath}`); + entries.delete(relativePath); + return { + originalPath: relativePath, + trashPath: `.verstak/trash/files/mock/${path.basename(relativePath)}`, + trashId: 'mock', + deletedAt: new Date().toISOString(), + }; + }, }, workbench: { openResource: async (request) => { @@ -229,6 +239,18 @@ async function mountNotes(api) { throw new Error('create note did not open the newly created file'); } + const trashButton = walk(container, (node) => node.getAttribute && node.getAttribute('data-note-action') === 'trash'); + if (!trashButton) throw new Error('trash note button not found'); + trashButton.click(); + await flush(); + const trashConfirm = walk(document.body, (node) => node.getAttribute && node.getAttribute('data-notes-confirm-trash') !== undefined); + if (!trashConfirm) throw new Error('trash confirmation button not found'); + trashConfirm.click(); + await flush(); + if (createApi.entries.has('Project/Notes/First_Note.md')) { + throw new Error('trash note did not remove the markdown file from active entries'); + } + console.log('notes plugin smoke passed'); })().catch((err) => { console.error(err);