From 321c8e58d1ff72fc3d29730210e88c9c64e583ec Mon Sep 17 00:00:00 2001 From: mirivlad Date: Sun, 21 Jun 2026 23:23:07 +0800 Subject: [PATCH] feat: Notes plugin + Files improvements + Default editor wiki links plugins/files: - SVG icons for toolbar actions (iconButton helper) - Multi-select cut/copy/paste (Ctrl+X/C/V) - Mouse back/forward button navigation (history stack) - Navigation history persisted per workspace (window.__filesHistoryByWorkspace) - Context menu: icons, notes integration (Create Note, Open Overview) - Drag-and-drop folder target fix plugins/default-editor: - Wiki links [[Title]] in notes context (renderInline with isNotesContext) - Save/Read via Notes API when editorMode === 'notes-markdown' - Internal link click handler (open note via api.request.open) plugins/platform-test: - Mouse Event Inspector card for debugging back/forward buttons plugins/notes (new): - Basic notes plugin scaffold (plugin.json + frontend entry) - Contributes workspace item 'Notes' --- plugins/default-editor/frontend/src/index.js | 61 ++- plugins/files/frontend/src/index.js | 401 ++++++++++++---- plugins/notes/frontend/src/index.js | 464 +++++++++++++++++++ plugins/notes/plugin.json | 39 ++ plugins/platform-test/frontend/dist/index.js | 139 ++++++ plugins/platform-test/frontend/src/index.js | 139 ++++++ 6 files changed, 1133 insertions(+), 110 deletions(-) create mode 100644 plugins/notes/frontend/src/index.js create mode 100644 plugins/notes/plugin.json diff --git a/plugins/default-editor/frontend/src/index.js b/plugins/default-editor/frontend/src/index.js index 5728f9c..c8fd70e 100644 --- a/plugins/default-editor/frontend/src/index.js +++ b/plugins/default-editor/frontend/src/index.js @@ -84,8 +84,12 @@ return escapeHtml(s).replace(/"/g, '"'); } - function renderInline(text) { + function renderInline(text, isNotesContext) { var html = escapeHtml(text); + // Internal wiki links [[Title]] — only render in notes context + if (isNotesContext) { + html = html.replace(/\[\[([^\]]+)\]\]/g, '$1'); + } html = html.replace(/`([^`\n]+)`/g, '$1'); html = html.replace(/!\[([^\]]*)\]\((https?:\/\/[^)]+)\)/g, '$1'); html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^)]+|mailto:[^)]+)\)/g, '$1'); @@ -95,7 +99,7 @@ return html; } - function renderMarkdown(text) { + function renderMarkdown(text, isNotesContext) { var lines = String(text || '').split(/\r?\n/); var out = []; var inCode = false; @@ -113,14 +117,14 @@ function closeTable() { if (!table.length) return; out.push('' + table.map(function (row) { - return '' + row.map(function (cell) { return ''; }).join('') + ''; + return '' + row.map(function (cell) { return ''; }).join('') + ''; }).join('') + '
' + renderInline(cell.trim()) + '
' + renderInline(cell.trim(), isNotesContext) + '
'); table = []; } function pushParagraph(line) { closeList(); closeTable(); - if (line.trim()) out.push('

' + renderInline(line) + '

'); + if (line.trim()) out.push('

' + renderInline(line, isNotesContext) + '

'); } lines.forEach(function (line) { @@ -154,7 +158,7 @@ if (heading) { closeList(); closeTable(); - out.push('' + renderInline(heading[2]) + ''); + out.push('' + renderInline(heading[2], isNotesContext) + ''); return; } @@ -169,7 +173,7 @@ if (quote) { closeList(); closeTable(); - out.push('
' + renderInline(quote[1]) + '
'); + out.push('
' + renderInline(quote[1], isNotesContext) + '
'); return; } @@ -185,9 +189,9 @@ listType = desired; } if (task) { - out.push('
  • ' + renderInline(task[2]) + '
  • '); + out.push('
  • ' + renderInline(task[2], isNotesContext) + '
  • '); } else { - out.push('
  • ' + renderInline((ordered || unordered)[1]) + '
  • '); + out.push('
  • ' + renderInline((ordered || unordered)[1], isNotesContext) + '
  • '); } return; } @@ -337,7 +341,7 @@ } function updatePreview() { - if (previewEl) previewEl.innerHTML = isMarkdown ? renderMarkdown(currentContent) : '
    ' + escapeHtml(currentContent) + '
    '; + if (previewEl) previewEl.innerHTML = isMarkdown ? renderMarkdown(currentContent, editorMode === 'notes-markdown') : '
    ' + escapeHtml(currentContent) + '
    '; } function syncFromTextarea() { @@ -404,7 +408,15 @@ if (!dirty || disposed) return Promise.resolve(); saveState = 'saving'; updateStatus(); - return api.files.writeText(resourcePath, currentContent, { createIfMissing: false, overwrite: true }).then(function () { + var savePromise; + if (editorMode === 'notes-markdown') { + savePromise = api.backend.call('SaveNote', resourcePath, currentContent).then(function (errStr) { + if (errStr) throw new Error(errStr); + }); + } else { + savePromise = api.files.writeText(resourcePath, currentContent, { createIfMissing: false, overwrite: true }); + } + return savePromise.then(function () { if (disposed) return; savedContent = currentContent; dirty = false; @@ -430,7 +442,16 @@ if (dirty && !window.confirm('Discard unsaved changes and reload from disk?')) return; editorWrap.innerHTML = ''; editorWrap.appendChild(el('div', { className: 'de-loading' }, ['Loading...'])); - api.files.readText(resourcePath).then(function (content) { + var readPromise; + if (editorMode === 'notes-markdown') { + readPromise = api.backend.call('ReadNote', resourcePath).then(function (result) { + var content = Array.isArray(result) ? result[0] : result; + return content == null ? '' : content; + }); + } else { + readPromise = api.files.readText(resourcePath); + } + readPromise.then(function (content) { if (disposed) return; currentContent = String(content == null ? '' : content); savedContent = currentContent; @@ -485,6 +506,24 @@ reloadFromDisk(); + containerEl.addEventListener('click', function (event) { + var link = event.target.closest('.internal-link'); + if (!link) return; + event.preventDefault(); + var noteTitle = link.getAttribute('data-note-link'); + if (!noteTitle) return; + var parentPath = resourcePath; + var idx = parentPath.indexOf('/Notes/'); + if (idx !== -1) { + parentPath = parentPath.substring(0, idx); + } + api.request.open({ + path: parentPath + '/Notes/' + noteTitle + '.md', + mode: 'view', + context: { notesMode: true } + }); + }); + containerEl.__deCleanup = function () { disposed = true; if (saveTimer) clearTimeout(saveTimer); diff --git a/plugins/files/frontend/src/index.js b/plugins/files/frontend/src/index.js index 8cf49f1..a5b2a58 100644 --- a/plugins/files/frontend/src/index.js +++ b/plugins/files/frontend/src/index.js @@ -17,7 +17,10 @@ var STYLES = [ '.files-root{display:flex;flex-direction:column;height:100%;min-height:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;color:#e0e0e0;background:#0d0d1a;outline:none}', '.files-toolbar{display:flex;align-items:center;gap:.45rem;padding:.5rem .75rem;border-bottom:1px solid #16213e;flex-shrink:0;background:#12122a;flex-wrap:wrap}', - '.files-toolbar-btn,.files-row-btn{font-size:.75rem;padding:.28rem .58rem;border:1px solid #333;border-radius:4px;background:#1a1a2e;color:#ccc;cursor:pointer}', + '.files-toolbar-btn,.files-row-btn{display:inline-flex;align-items:center;justify-content:center;border:1px solid #333;border-radius:4px;background:#1a1a2e;color:#ccc;cursor:pointer;line-height:1}', + '.files-toolbar-btn{width:2rem;height:2rem;padding:0}', + '.files-row-btn{width:1.75rem;height:1.75rem;padding:0}', + '.files-toolbar-btn svg,.files-row-btn svg{width:16px;height:16px;display:block;fill:currentColor}', '.files-toolbar-btn:hover,.files-row-btn:hover{background:#2a2a4e;border-color:#4ecca3}', '.files-toolbar-btn:disabled,.files-row-btn:disabled{opacity:.45;cursor:default;border-color:#333}', '.files-breadcrumb{display:flex;align-items:center;gap:.25rem;font-size:.8rem;color:#8b8ba8;min-width:160px;flex:1;overflow:hidden}', @@ -27,7 +30,7 @@ '.files-breadcrumb-sep{color:#555}', '.files-filter,.files-sort,.files-create-input,.files-rename-input{font-size:.78rem;padding:.32rem .5rem;border:1px solid #333;border-radius:4px;background:#0d0d1a;color:#e0e0e0;outline:none}', '.files-filter{width:11rem}', - '.files-sort{width:9.5rem}', + '.files-sort{width:9.5rem;appearance:none;background-color:#0d0d1a;background-image:linear-gradient(45deg,transparent 50%,#8b8ba8 50%),linear-gradient(135deg,#8b8ba8 50%,transparent 50%);background-position:calc(100% - 14px) 50%,calc(100% - 9px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:1.6rem}', '.files-filter:focus,.files-sort:focus,.files-create-input:focus,.files-rename-input:focus{border-color:#4ecca3}', '.files-list{flex:1;overflow:auto;min-height:0}', '.files-header,.files-item{display:grid;grid-template-columns:minmax(160px,1fr) 90px 90px 150px 220px;align-items:center;gap:.5rem;padding:.38rem .75rem;border-bottom:1px solid rgba(22,33,62,.55)}', @@ -49,6 +52,7 @@ '@media(max-width:760px){.files-header,.files-item{grid-template-columns:minmax(130px,1fr) 70px 0 0 150px}.files-header span:nth-child(3),.files-header span:nth-child(4),.files-item-meta.hide-narrow{display:none}.files-toolbar{align-items:stretch}.files-filter,.files-sort{width:100%}}', '.files-ctx-menu{position:fixed;z-index:9999;min-width:180px;background:#1a1a2e;border:1px solid #333;border-radius:6px;padding:6px 0;box-shadow:0 8px 24px rgba(0,0,0,.5);font-size:.84rem;color:#e0e0e0;user-select:none}', '.files-ctx-menu-item{padding:6px 16px;cursor:pointer;white-space:nowrap;display:flex;align-items:center;gap:.5rem}', + '.files-ctx-menu-item svg{width:14px;height:14px;fill:currentColor;opacity:.9}', '.files-ctx-menu-item:hover{background:#2a2a4e}', '.files-ctx-menu-item.danger{color:#e74c3c}', '.files-ctx-menu-item.danger:hover{background:#2a2a4e;color:#ff6b6b}', @@ -93,6 +97,33 @@ return ''; } + var ACTION_ICONS = { + back: 'M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.42-1.41L7.83 13H20v-2z', + forward: 'M4 13h12.17l-5.59 5.59L12 20l8-8-8-8-1.42 1.41L16.17 11H4v2z', + up: 'M4 12l1.41 1.41L11 7.83V20h2V7.83l5.59 5.58L20 12 12 4l-8 8z', + refresh: 'M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z', + folderAdd: 'M20 6h-8.17l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-1 8h-3v3h-2v-3h-3v-2h3V9h2v3h3v2z', + markdownAdd: 'M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 8V4l5 5h-5zm-6 6h2v-3l2 3h1l2-3v3h2v-6h-2l-2.5 3.5L9 10H7v6z', + textAdd: 'M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 8V4l5 5h-5zM8 13h8v2H8v-2zm0 4h8v2H8v-2z', + 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', + 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', + trash: 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM8 9h8v10H8V9zm7.5-5-1-1h-5l-1 1H5v2h14V4z', + cut: 'M9.64 7.64c.23-.5.36-1.05.36-1.64 0-2.21-1.79-4-4-4S2 3.79 2 6s1.79 4 4 4c.59 0 1.14-.13 1.64-.36L10 12l-2.36 2.36C7.14 14.13 6.59 14 6 14c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4c0-.59-.13-1.14-.36-1.64L12 14l7 7h3L9.64 7.64zM6 8c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm0 12c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm6-8.5c-.28 0-.5.22-.5.5s.22.5.5.5.5-.22.5-.5-.22-.5-.5-.5zM19 3l-6 6 2 2 7-8h-3z', + copy: 'M16 1H4c-1.1 0-2 .9-2 2v12h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z', + paste: 'M19 2h-4.18C14.4.84 13.3 0 12 0S9.6.84 9.18 2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm7 18H5V4h2v3h10V4h2v16z' + }; + + function iconButton(action, title, iconKey, onClick, extraClass) { + return el('button', { + className: (extraClass || 'files-toolbar-btn'), + 'data-files-action': action, + title: title, + 'aria-label': title, + innerHTML: svgIcon(ACTION_ICONS[iconKey || action] || ACTION_ICONS.open), + onClick: onClick + }); + } + function cleanPath(path) { return String(path || '').split('/').filter(Boolean).join('/'); } @@ -222,7 +253,10 @@ var workspaceNode = props && props.workspaceNode; var workspaceRoot = cleanPath(props && (props.workspaceRootPath || (workspaceNode && (workspaceNode.rootPath || workspaceNode.name || workspaceNode.id))) || ''); var workspaceName = workspaceRoot || (workspaceNode && (workspaceNode.name || workspaceNode.title || workspaceNode.id)) || 'Workspace'; - var currentPath = ''; + window.__filesHistoryByWorkspace = window.__filesHistoryByWorkspace || {}; + var historyKey = workspaceRoot || workspaceName; + var savedHistory = window.__filesHistoryByWorkspace[historyKey] || { stack: [''], index: 0, currentPath: '' }; + var currentPath = cleanPath(savedHistory.currentPath || ''); var entries = []; var selectedPaths = {}; var lastClickedPath = ''; @@ -231,8 +265,12 @@ var createMode = ''; var renameTarget = null; var disposed = false; - var historyStack = ['']; - var historyIndex = 0; + var historyStack = Array.isArray(savedHistory.stack) && savedHistory.stack.length ? savedHistory.stack.map(cleanPath) : [currentPath]; + var historyIndex = Math.max(0, Math.min(Number(savedHistory.index) || 0, historyStack.length - 1)); + if (historyStack[historyIndex] !== currentPath) { + historyStack = [currentPath]; + historyIndex = 0; + } var navigatingHistory = false; function scopedPath(local) { @@ -249,17 +287,19 @@ var toolbar = el('div', { className: 'files-toolbar' }); var breadcrumb = el('div', { className: 'files-breadcrumb' }); - var backBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'back', title: 'Back (Alt+Left)' }, ['\u2190']); - var forwardBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'forward', title: 'Forward (Alt+Right)' }, ['\u2192']); - var upBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'up', title: 'Up' }, ['Up']); - var refreshBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'refresh', title: 'Refresh' }, ['Refresh']); - var newFolderBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'new-folder' }, ['+ Folder']); - var newMdBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'new-markdown' }, ['+ Markdown']); - var newTextBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'new-text' }, ['+ Text']); - var openBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'open' }, ['Open']); - var renameBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'rename' }, ['Rename']); - var trashBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'trash' }, ['Trash']); - var pasteBtn = el('button', { className: 'files-toolbar-btn', 'data-files-action': 'paste' }, ['Paste']); + var backBtn = iconButton('back', 'Back', 'back', goBack); + var forwardBtn = iconButton('forward', 'Forward', 'forward', goForward); + var upBtn = iconButton('up', 'Up', 'up', goUp); + var refreshBtn = iconButton('refresh', 'Refresh', 'refresh', loadEntries); + var newFolderBtn = iconButton('new-folder', 'New folder', 'folderAdd', function () { startCreate('folder'); }); + var newMdBtn = iconButton('new-markdown', 'New markdown file', 'markdownAdd', function () { startCreate('markdown'); }); + var newTextBtn = iconButton('new-text', 'New text file', 'textAdd', function () { startCreate('text'); }); + var openBtn = iconButton('open', 'Open', 'open', function () { openEntry(selectedEntry()); }); + var renameBtn = iconButton('rename', 'Rename', 'rename', function () { beginRename(); }); + var trashBtn = iconButton('trash', 'Move to trash', 'trash', function () { trashEntry(); }); + var cutBtn = iconButton('cut', 'Cut', 'cut', function () { cutSelection(); }); + var copyBtn = iconButton('copy', 'Copy', 'copy', function () { copySelection(); }); + var pasteBtn = iconButton('paste', 'Paste', 'paste', function () { pasteEntry(); }); var filterInput = el('input', { className: 'files-filter', 'data-files-filter': '', placeholder: 'Filter current folder' }); var sortSelect = el('select', { className: 'files-sort', 'data-files-sort': '' }, [ el('option', { value: 'folder-name' }, ['Folders + name']), @@ -269,7 +309,7 @@ el('option', { value: 'size-desc' }, ['Size']) ]); toolbar.appendChild(breadcrumb); - [backBtn, forwardBtn, upBtn, refreshBtn, newFolderBtn, newMdBtn, newTextBtn, openBtn, renameBtn, trashBtn, pasteBtn, filterInput, sortSelect].forEach(function (node) { toolbar.appendChild(node); }); + [backBtn, forwardBtn, upBtn, refreshBtn, newFolderBtn, newMdBtn, newTextBtn, openBtn, renameBtn, trashBtn, cutBtn, copyBtn, pasteBtn, filterInput, sortSelect].forEach(function (node) { toolbar.appendChild(node); }); containerEl.appendChild(toolbar); var listContainer = el('div', { className: 'files-list', 'data-files-list': '' }); @@ -299,6 +339,12 @@ return entries.find(function (entry) { return entry.relativePath === keys[0]; }) || null; } + function selectedEntries() { + return Object.keys(selectedPaths).map(function (path) { + return entries.find(function (entry) { return entry.relativePath === path; }) || null; + }).filter(Boolean); + } + function selectedCount() { return Object.keys(selectedPaths).length; } @@ -309,7 +355,9 @@ openBtn.disabled = count !== 1; renameBtn.disabled = count !== 1; trashBtn.disabled = count === 0; - pasteBtn.disabled = !window.__filesClipboard; + cutBtn.disabled = count === 0; + copyBtn.disabled = count === 0; + pasteBtn.disabled = !(window.__filesClipboard && window.__filesClipboard.items && window.__filesClipboard.items.length); } function updateHistoryButtons() { @@ -317,6 +365,14 @@ forwardBtn.disabled = historyIndex >= historyStack.length - 1; } + function saveHistoryState() { + window.__filesHistoryByWorkspace[historyKey] = { + stack: historyStack.slice(), + index: historyIndex, + currentPath: currentPath + }; + } + function updateBreadcrumb() { breadcrumb.innerHTML = ''; var root = el('span', { className: currentPath ? 'files-breadcrumb-item' : 'files-breadcrumb-current', onClick: function () { navigateTo(''); } }, [workspaceName]); @@ -458,10 +514,9 @@ el('span', { className: 'files-item-meta hide-narrow' }, [entry.type === 'folder' ? '' : formatSize(entry.size)]), el('span', { className: 'files-item-meta hide-narrow' }, [formatDate(entry.modifiedAt)]), el('div', { className: 'files-row-actions' }, [ - el('button', { className: 'files-row-btn', onClick: function (event) { event.stopPropagation(); openEntry(entry); } }, ['Open']), - el('button', { className: 'files-row-btn', onClick: function (event) { event.stopPropagation(); beginRename(entry); } }, ['Rename']), - el('button', { className: 'files-row-btn', onClick: function (event) { event.stopPropagation(); trashEntry(entry); } }, ['Trash']), - entry.type === 'folder' ? el('button', { className: 'files-row-btn', onClick: function (event) { event.stopPropagation(); navigateTo(localPath(entry.relativePath)); startCreate('markdown'); } }, ['New here']) : null + iconButton('row-open', 'Open', 'open', function (event) { event.stopPropagation(); openEntry(entry); }, 'files-row-btn'), + iconButton('row-rename', 'Rename', 'rename', function (event) { event.stopPropagation(); beginRename(entry); }, 'files-row-btn'), + iconButton('row-trash', 'Move to trash', 'trash', function (event) { event.stopPropagation(); trashEntry(entry); }, 'files-row-btn') ]) ]); listContainer.appendChild(row); @@ -504,6 +559,7 @@ cancelCreate(); cancelRename(); updateHistoryButtons(); + saveHistoryState(); loadEntries(); } @@ -661,17 +717,6 @@ } } - backBtn.addEventListener('click', goBack); - forwardBtn.addEventListener('click', goForward); - refreshBtn.addEventListener('click', loadEntries); - upBtn.addEventListener('click', goUp); - newFolderBtn.addEventListener('click', function () { startCreate('folder'); }); - newMdBtn.addEventListener('click', function () { startCreate('markdown'); }); - newTextBtn.addEventListener('click', function () { startCreate('text'); }); - openBtn.addEventListener('click', function () { openEntry(selectedEntry()); }); - renameBtn.addEventListener('click', function () { beginRename(); }); - trashBtn.addEventListener('click', function () { trashEntry(); }); - pasteBtn.addEventListener('click', function () { pasteEntry(); }); filterInput.addEventListener('input', function () { filterText = filterInput.value; renderList(); }); sortSelect.addEventListener('change', function () { sortMode = sortSelect.value; renderList(); }); createConfirm.addEventListener('click', confirmCreate); @@ -691,11 +736,15 @@ ctxTarget = null; } - function ctxItem(label, cls, onClick) { + function ctxItem(label, cls, onClick, action, iconKey) { return el('div', { className: 'files-ctx-menu-item' + (cls ? ' ' + cls : ''), + 'data-files-menu-action': action || '', onClick: function (e) { e.stopPropagation(); hideCtxMenu(); onClick(); } - }, [label]); + }, [ + iconKey ? el('span', { innerHTML: svgIcon(ACTION_ICONS[iconKey] || ACTION_ICONS.open) }) : null, + label + ]); } function ctxSep() { @@ -706,25 +755,77 @@ ctxTarget = entry; ctxMenu.innerHTML = ''; if (entry) { + if (!selectedPaths[entry.relativePath]) { + selectedPaths = {}; + selectedPaths[entry.relativePath] = true; + lastClickedPath = entry.relativePath; + renderList(); + } var isFolder = entry.type === 'folder'; - ctxMenu.appendChild(ctxItem(isFolder ? 'Open Folder' : 'Open', '', function () { openEntry(entry); })); - ctxMenu.appendChild(ctxSep()); - ctxMenu.appendChild(ctxItem('Rename', '', function () { beginRename(entry); })); - ctxMenu.appendChild(ctxItem('Duplicate', '', function () { duplicateEntry(entry); })); - ctxMenu.appendChild(ctxSep()); - ctxMenu.appendChild(ctxItem('Cut', '', function () { cutEntry(entry); })); - if (entry.type !== 'folder') { - ctxMenu.appendChild(ctxItem('Copy', '', function () { copyEntry(entry); })); + ctxMenu.appendChild(ctxItem(isFolder ? 'Open Folder' : 'Open', '', function () { openEntry(entry); }, 'open', 'open')); + var entryLocalPath = localPath(entry.relativePath); + var isNotes = entryLocalPath === 'Notes' || entryLocalPath.split('/')[0] === 'Notes'; + if (isNotes) { + ctxMenu.appendChild(ctxSep()); + if (isFolder) { + ctxMenu.appendChild(ctxItem('Create Note', '', function () { + var title = prompt('Note title:'); + if (!title) return; + api.backend.call('NormalizeNoteTitle', title).then(function (result) { + var safeTitle = Array.isArray(result) ? result[0] : (typeof result === 'string' ? result : title); + if (!safeTitle) safeTitle = title; + return api.backend.call('CreateNote', scopedPath(entryLocalPath), safeTitle).then(function (cr) { + var crErr = Array.isArray(cr) ? cr[1] : ''; + if (crErr) { window.alert(crErr); return; } + loadEntries(); + var crValue = Array.isArray(cr) ? cr[0] : cr; + var notePath = (crValue && crValue.path) ? crValue.path : ''; + if (notePath) { + api.workbench.openResource({ + kind: 'vault-file', + path: localPath(notePath), + mode: 'edit', + extension: '.md', + context: { notesMode: true, sourcePluginId: 'verstak.files' } + }).catch(function () {}); + } + }); + }).catch(function (err) { window.alert('Failed to create note: ' + (err.message || String(err))); }); + }, 'create-note', 'markdownAdd')); + ctxMenu.appendChild(ctxItem('Open Overview', '', function () { + api.backend.call('EnsureOverview', scopedPath(entryLocalPath)).then(function (result) { + var ovValue = Array.isArray(result) ? result[0] : result; + var overviewPath = (ovValue && ovValue.path) ? ovValue.path : ''; + if (overviewPath) { + api.workbench.openResource({ + kind: 'vault-file', + path: localPath(overviewPath), + mode: 'view', + extension: '.md', + context: { notesMode: true, sourcePluginId: 'verstak.files' } + }).catch(function () {}); + } + }).catch(function (err) { window.alert('Failed to open overview: ' + (err.message || String(err))); }); + }, 'open-overview', 'open')); + } } ctxMenu.appendChild(ctxSep()); - ctxMenu.appendChild(ctxItem('Move to Trash', 'danger', function () { trashEntry(entry); })); + ctxMenu.appendChild(ctxItem('Rename', '', function () { beginRename(entry); }, 'rename', 'rename')); + if (entry.type !== 'folder') { + ctxMenu.appendChild(ctxItem('Duplicate', '', function () { duplicateEntry(entry); }, 'duplicate', 'copy')); + } + ctxMenu.appendChild(ctxSep()); + ctxMenu.appendChild(ctxItem('Cut', '', function () { cutSelection(); }, 'cut', 'cut')); + ctxMenu.appendChild(ctxItem('Copy', '', function () { copySelection(); }, 'copy', 'copy')); + ctxMenu.appendChild(ctxSep()); + ctxMenu.appendChild(ctxItem('Move to Trash', 'danger', function () { trashEntry(); }, 'trash', 'trash')); } else { - ctxMenu.appendChild(ctxItem('New Folder', '', function () { startCreate('folder'); })); - ctxMenu.appendChild(ctxItem('New Markdown', '', function () { startCreate('markdown'); })); - ctxMenu.appendChild(ctxItem('New Text', '', function () { startCreate('text'); })); - if (window.__filesClipboard) { + ctxMenu.appendChild(ctxItem('New Folder', '', function () { startCreate('folder'); }, 'new-folder', 'folderAdd')); + ctxMenu.appendChild(ctxItem('New Markdown', '', function () { startCreate('markdown'); }, 'new-markdown', 'markdownAdd')); + ctxMenu.appendChild(ctxItem('New Text', '', function () { startCreate('text'); }, 'new-text', 'textAdd')); + if (window.__filesClipboard && window.__filesClipboard.items && window.__filesClipboard.items.length) { ctxMenu.appendChild(ctxSep()); - ctxMenu.appendChild(ctxItem('Paste', '', function () { pasteEntry(); })); + ctxMenu.appendChild(ctxItem('Paste', '', function () { pasteEntry(); }, 'paste', 'paste')); } } ctxMenu.style.display = 'block'; @@ -773,64 +874,73 @@ }); } - function cutEntry(entry) { - if (!entry) return; - console.log('[files] Cut:', entry.relativePath); - window.__filesClipboard = { action: 'cut', path: entry.relativePath, name: entry.name, isFolder: entry.type === 'folder' }; + function clipboardItemsFromSelection() { + var selected = selectedEntries(); + if (selected.length === 0) return []; + return selected.map(function (entry) { + return { path: entry.relativePath, name: entry.name, type: entry.type }; + }); + } + + function setClipboard(action, items) { + if (!items || !items.length) return; + window.__filesClipboard = { + action: action, + workspaceRoot: workspaceRoot, + items: items + }; updateButtons(); } - function copyEntry(entry) { - if (!entry) return; - if (entry.type === 'folder') { console.log('[files] Copy for folders not yet supported'); return; } - console.log('[files] Copy:', entry.relativePath); - window.__filesClipboard = { action: 'copy', path: entry.relativePath, name: entry.name, isFolder: false }; - updateButtons(); + function cutSelection() { + setClipboard('cut', clipboardItemsFromSelection()); + } + + function copySelection() { + var items = clipboardItemsFromSelection().filter(function (item) { return item.type !== 'folder'; }); + if (items.length === 0) return; + setClipboard('copy', items); + } + + function uniqueDestinationName(name, occupied) { + if (!occupied[name]) return name; + var dot = name.lastIndexOf('.'); + var base = dot > 0 ? name.slice(0, dot) : name; + var ext = dot > 0 ? name.slice(dot) : ''; + for (var i = 2; i < 100; i += 1) { + var candidate = base + ' (' + i + ')' + ext; + if (!occupied[candidate]) return candidate; + } + return base + ' (' + Date.now() + ')' + ext; } function pasteEntry() { var clip = window.__filesClipboard; - if (!clip || !clip.path) return; - var fromRelative = clip.path; - var fromScoped = scopedPath(fromRelative); - var isFolder = clip.isFolder; - var clipName = clip.name; - var dot = clipName.lastIndexOf('.'); - var base = dot > 0 ? clipName.slice(0, dot) : clipName; - var ext = dot > 0 ? clipName.slice(dot) : ''; - var maxAttempts = 100; - - function tryName(n) { - var newName = n === 1 ? clipName : base + ' (' + n + ')' + ext; - var to = scopedPath(currentPath ? currentPath + '/' + newName : newName); - return api.files.metadata(to).then(function () { - if (n >= maxAttempts) { - console.error('[files] Paste failed: all ' + maxAttempts + ' name variations are taken'); - return null; - } - return tryName(n + 1); - }, function () { - if (isFolder && clip.action === 'cut') { - return api.files.move(fromRelative, to, { overwrite: false }).then(function () { - return true; - }); - } - return api.files.readText(fromScoped).then(function (content) { - return api.files.writeText(to, content, { createIfMissing: true, overwrite: false }); - }).then(function () { - if (clip.action === 'cut') { - return api.files.trash(fromRelative); - } - }); - }); + if (!clip || !clip.items || clip.items.length === 0) return; + if (clip.workspaceRoot && clip.workspaceRoot !== workspaceRoot) { + window.alert('Clipboard items belong to another workspace.'); + return; } + var destinationDir = scopedPath(currentPath); + var occupied = {}; + entries.forEach(function (entry) { occupied[entry.name] = true; }); - tryName(1).then(function (result) { - if (result !== null) { - if (clip.action === 'cut') window.__filesClipboard = null; - loadEntries(); - console.log('[files] Pasted:', clip.action, from); + var tasks = clip.items.map(function (item) { + var newName = uniqueDestinationName(item.name, occupied); + occupied[newName] = true; + var to = destinationDir ? destinationDir + '/' + newName : newName; + if (clip.action === 'cut') { + if (item.path === to || to.indexOf(item.path + '/') === 0) return Promise.resolve(); + return api.files.move(item.path, to, { overwrite: false }); } + return api.files.readText(item.path).then(function (content) { + return api.files.writeText(to, content, { createIfMissing: true, overwrite: false }); + }); + }); + + Promise.allSettled(tasks).then(function () { + if (clip.action === 'cut') window.__filesClipboard = null; + loadEntries(); }).catch(function (err) { console.error('[files] Paste failed:', err); }); @@ -901,11 +1011,81 @@ if (!sourcePaths || !sourcePaths.length) return; var row = e.target.closest('.files-item'); if (row && row.getAttribute('data-file-type') === 'folder') { - var targetRel = row.getAttribute('data-file-path'); - moveFiles(sourcePaths, targetRel); + moveFiles(sourcePaths, row.getAttribute('data-file-path')); + } else { + moveFiles(sourcePaths, scopedPath(currentPath)); } }); + var lastMouseHistoryAt = 0; + var lastMouseHistoryButton = 0; + function mouseHistoryButton(event) { + if (event.button === 3 || event.button === 8 || event.buttons === 8 || event.buttons === 128 || event.which === 8) return 'back'; + if (event.button === 4 || event.button === 9 || event.buttons === 16 || event.buttons === 256 || event.which === 9) return 'forward'; + return ''; + } + + function handleMouseHistory(event) { + var button = mouseHistoryButton(event); + if (!button) return; + + event.preventDefault(); + event.stopPropagation(); + + var now = Date.now(); + if (button === lastMouseHistoryButton && now - lastMouseHistoryAt < 120) return; + lastMouseHistoryButton = button; + lastMouseHistoryAt = now; + + try { + if (window.localStorage && window.localStorage.getItem('verstak-debug') === 'true') { + console.log('[debug] [Files] mouse history event', { + type: event.type, + direction: button, + button: event.button, + buttons: event.buttons, + which: event.which, + pointerType: event.pointerType || '', + currentPath: currentPath + }); + } + } catch (err) {} + + if (button === 'back') goBack(); + else goForward(); + } + + function handleWindowHistoryKey(event) { + if (event.defaultPrevented) return; + if (event.target && ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].indexOf(event.target.tagName) !== -1) return; + + var key = event.key || ''; + var ctrl = event.ctrlKey || event.metaKey; + var direction = ''; + if (key === 'ArrowLeft' && event.altKey) direction = 'back'; + else if (key === 'ArrowRight' && event.altKey) direction = 'forward'; + else if (key === '[' && ctrl) direction = 'back'; + else if (key === ']' && ctrl) direction = 'forward'; + else if (key === 'BrowserBack' || key === 'XF86Back' || event.keyCode === 166) direction = 'back'; + else if (key === 'BrowserForward' || key === 'XF86Forward' || event.keyCode === 167) direction = 'forward'; + if (!direction) return; + + event.preventDefault(); + event.stopPropagation(); + if (direction === 'back') goBack(); + else goForward(); + } + + containerEl.addEventListener('mousedown', handleMouseHistory, true); + containerEl.addEventListener('pointerdown', handleMouseHistory, true); + window.addEventListener('pointerdown', handleMouseHistory, true); + document.addEventListener('pointerdown', handleMouseHistory, true); + window.addEventListener('mousedown', handleMouseHistory, true); + document.addEventListener('mousedown', handleMouseHistory, true); + window.addEventListener('mouseup', handleMouseHistory, true); + window.addEventListener('auxclick', handleMouseHistory, true); + window.addEventListener('keydown', handleWindowHistoryKey); + containerEl.addEventListener('keydown', function (event) { if (event.target && ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].indexOf(event.target.tagName) !== -1) return; @@ -1006,6 +1186,21 @@ renderList(); return; } + if (ctrl && key.toLowerCase() === 'x') { + event.preventDefault(); + cutSelection(); + return; + } + if (ctrl && key.toLowerCase() === 'c') { + event.preventDefault(); + copySelection(); + return; + } + if (ctrl && key.toLowerCase() === 'v') { + event.preventDefault(); + pasteEntry(); + return; + } if (key === 'ArrowDown' || key === 'ArrowUp' || key === 'Home' || key === 'End' || key === 'PageDown' || key === 'PageUp') { event.preventDefault(); @@ -1045,6 +1240,14 @@ disposed = true; document.removeEventListener('click', onDocClick); document.removeEventListener('keydown', onDocKeydown); + window.removeEventListener('mousedown', handleMouseHistory, true); + window.removeEventListener('pointerdown', handleMouseHistory, true); + document.removeEventListener('pointerdown', handleMouseHistory, true); + containerEl.removeEventListener('pointerdown', handleMouseHistory, true); + document.removeEventListener('mousedown', handleMouseHistory, true); + window.removeEventListener('mouseup', handleMouseHistory, true); + window.removeEventListener('auxclick', handleMouseHistory, true); + window.removeEventListener('keydown', handleWindowHistoryKey); if (ctxMenu && ctxMenu.parentNode) ctxMenu.parentNode.removeChild(ctxMenu); }; }, diff --git a/plugins/notes/frontend/src/index.js b/plugins/notes/frontend/src/index.js new file mode 100644 index 0000000..582acc6 --- /dev/null +++ b/plugins/notes/frontend/src/index.js @@ -0,0 +1,464 @@ +/* =========================================================== + Notes Plugin — Verstak v2 Frontend Bundle + Contract: window.VerstakPluginRegister(id, { components }) + =========================================================== */ + +(function () { + 'use strict'; + + function injectStyles() { + if (document.getElementById('notes-style-injected')) return; + var style = document.createElement('style'); + style.id = 'notes-style-injected'; + style.textContent = STYLES; + document.head.appendChild(style); + } + + var STYLES = [ + '.notes-root{display:flex;flex-direction:column;height:100%;min-height:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;color:#e0e0e0;background:#0d0d1a;outline:none}', + '.notes-toolbar{display:flex;align-items:center;gap:.45rem;padding:.5rem .75rem;border-bottom:1px solid #16213e;flex-shrink:0;background:#12122a;flex-wrap:wrap}', + '.notes-btn{font-size:.78rem;padding:.32rem .65rem;border:1px solid #333;border-radius:4px;background:#1a1a2e;color:#ccc;cursor:pointer;display:inline-flex;align-items:center;gap:.35rem}', + '.notes-btn:hover{background:#2a2a4e;border-color:#4ecca3}', + '.notes-btn:disabled{opacity:.45;cursor:default}', + '.notes-btn.primary{background:#1a3a2a;border-color:#4ecca3;color:#4ecca3}', + '.notes-btn.primary:hover{background:#2a4a3a}', + '.notes-list{flex:1;overflow:auto;min-height:0}', + '.notes-item{display:flex;align-items:center;gap:.5rem;padding:.45rem .75rem;border-bottom:1px solid rgba(22,33,62,.55);cursor:pointer;font-size:.85rem}', + '.notes-item:hover{background:#17172d}', + '.notes-item.overview{background:#111126;border-left:2px solid #4ecca3}', + '.notes-item.selected{background:#1a2a3a}', + '.notes-item-icon{width:1.25rem;height:1.25rem;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;color:#8b8ba8}', + '.notes-item-icon svg{width:16px;height:16px;display:block;fill:currentColor}', + '.notes-item-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}', + '.notes-item-actions{display:flex;gap:.25rem;opacity:0;transition:opacity .15s}', + '.notes-item:hover .notes-item-actions{opacity:1}', + '.notes-item-btn{width:1.5rem;height:1.5rem;display:inline-flex;align-items:center;justify-content:center;border:1px solid #333;border-radius:3px;background:transparent;color:#888;cursor:pointer;padding:0;font-size:.7rem}', + '.notes-item-btn:hover{background:#2a2a4e;border-color:#4ecca3;color:#4ecca3}', + '.notes-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#666;font-size:.85rem;padding:2rem;gap:.5rem}', + '.notes-empty-hint{font-size:.75rem;color:#555}', + '.notes-error{flex:1;display:flex;align-items:center;justify-content:center;color:#e74c3c;padding:1rem;font-size:.85rem}', + '.notes-panel{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border-top:1px solid #16213e;flex-shrink:0;background:#12122a}', + '.notes-input{flex:1;font-size:.78rem;padding:.32rem .5rem;border:1px solid #333;border-radius:4px;background:#0d0d1a;color:#e0e0e0;outline:none;min-width:120px}', + '.notes-input:focus{border-color:#4ecca3}', + '.notes-title-bar{padding:.4rem .75rem;font-size:.72rem;color:#8b8ba8;background:#101028;border-bottom:1px solid #16213e;text-transform:uppercase;letter-spacing:.04em}', + '.notes-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:10000;display:flex;align-items:center;justify-content:center}', + '.notes-modal{width:380px;max-width:90vw;padding:20px;background:#1a1a2e;border:1px solid #333;border-radius:10px;color:#e0e0e0;font-family:inherit;box-shadow:0 12px 40px rgba(0,0,0,.5)}', + '.notes-modal-title{font-size:.9rem;font-weight:600;margin-bottom:12px}', + '.notes-modal-msg{font-size:.82rem;color:#aaa;margin-bottom:16px;word-wrap:break-word}', + '.notes-modal-actions{display:flex;justify-content:flex-end;gap:8px}', + '.notes-modal-btn{font-size:.8rem;padding:.35rem .9rem;border:1px solid #333;border-radius:5px;cursor:pointer;font-family:inherit}', + '.notes-modal-btn.cancel{background:#2a2a4e;color:#ccc}', + '.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-status{font-size:.72rem;color:#8b8ba8;padding:.15rem .5rem;white-space:nowrap}', + '.notes-status.success{color:#4ecca3}', + '.notes-status.error{color:#e74c3c}', + '.notes-status.loading{color:#79c0ff}', + '@media(max-width:600px){.notes-toolbar{flex-direction:column;align-items:stretch}}' + ].join('\n'); + + function el(tag, attrs, children) { + var elem = document.createElement(tag); + if (attrs) { + Object.keys(attrs).forEach(function (k) { + if (k === 'className') elem.className = attrs[k]; + else if (k === 'style' && typeof attrs[k] === 'object') Object.assign(elem.style, attrs[k]); + else if (k.slice(0, 2) === 'on') elem.addEventListener(k.slice(2).toLowerCase(), attrs[k]); + else if (k === 'innerHTML') elem.innerHTML = attrs[k]; + else if (k === 'textContent') elem.textContent = attrs[k]; + else elem.setAttribute(k, attrs[k]); + }); + } + if (children) { + (Array.isArray(children) ? children : [children]).forEach(function (c) { + if (c == null) return; + elem.appendChild(typeof c === 'string' ? document.createTextNode(c) : c); + }); + } + return elem; + } + + function svgIcon(path) { + return ''; + } + + var ICONS = { + note: 'M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 9h-1v1c0 .83-.67 1.5-1.5 1.5S9 12.83 9 12V9.5c0-.83.67-1.5 1.5-1.5S12 8.67 12 9.5v1h1v-1c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v2.5c0 .83-.67 1.5-1.5 1.5s-1.5-.67-1.5-1.5v-1z', + overview: 'M4 6H2v14c0 1.1.9 2 2 2h14v-2H4V6zm16-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-1 9h-4v4h-2v-4H9V9h4V5h2v4h4v2z', + 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' + }; + + function iconSvg(name) { + return ''; + } + + function fileName(path) { + var parts = String(path || '').split('/'); + return parts[parts.length - 1] || ''; + } + + var NotesView = { + mount: function (containerEl, props, api) { + injectStyles(); + containerEl.innerHTML = ''; + containerEl.className = 'notes-root'; + containerEl.setAttribute('data-plugin-id', 'verstak.notes'); + + var workspaceNode = props && props.workspaceNode; + var workspaceRoot = (workspaceNode && (workspaceNode.rootPath || workspaceNode.name || workspaceNode.id)) || ''; + var workspaceName = workspaceRoot || (workspaceNode && (workspaceNode.name || workspaceNode.title)) || 'Workspace'; + + var notes = []; + var selectedPath = ''; + var statusText = ''; + var statusClass = ''; + var disposed = false; + + function notesParent() { + return workspaceRoot || ''; + } + + // ─── Backend bridge ────────────────────────────────────── + // Uses api.backend.call() from VerstakPluginAPI — the official bridge. + // Direct window.go.api.App access is NOT allowed per platform rules. + + // ─── UI Elements ──────────────────────────────────────── + + var toolbar = el('div', { className: 'notes-toolbar' }); + var overviewBtn = el('button', { className: 'notes-btn primary', 'data-action': 'overview', innerHTML: iconSvg('overview') + ' Overview' }); + var createBtn = el('button', { className: 'notes-btn', 'data-action': 'create', innerHTML: iconSvg('add') + ' New Note' }); + var statusEl = el('span', { className: 'notes-status' }); + toolbar.appendChild(overviewBtn); + toolbar.appendChild(createBtn); + toolbar.appendChild(el('span', { style: { flex: '1' } })); + toolbar.appendChild(statusEl); + containerEl.appendChild(toolbar); + + var titleBar = el('div', { className: 'notes-title-bar' }, ['Notes in ' + workspaceName]); + containerEl.appendChild(titleBar); + + var listContainer = el('div', { className: 'notes-list', 'data-notes-list': '' }); + containerEl.appendChild(listContainer); + + var createPanel = el('div', { className: 'notes-panel', style: { display: 'none' } }); + var createInput = el('input', { className: 'notes-input', 'data-notes-create-input': '', placeholder: 'Note title' }); + var createConfirm = el('button', { className: 'notes-btn', textContent: 'Create' }); + var createCancel = el('button', { className: 'notes-btn', textContent: 'Cancel' }); + createPanel.appendChild(createInput); + createPanel.appendChild(createConfirm); + createPanel.appendChild(createCancel); + containerEl.appendChild(createPanel); + + var renamePanel = el('div', { className: 'notes-panel', style: { display: 'none' } }); + var renameInput = el('input', { className: 'notes-input', 'data-notes-rename-input': '', placeholder: 'New title' }); + var renameConfirm = el('button', { className: 'notes-btn', textContent: 'Rename' }); + var renameCancel = el('button', { className: 'notes-btn', textContent: 'Cancel' }); + renamePanel.appendChild(renameInput); + renamePanel.appendChild(renameConfirm); + renamePanel.appendChild(renameCancel); + containerEl.appendChild(renamePanel); + + // ─── Core Functions ───────────────────────────────────── + + function setStatus(text, cls) { + statusText = text; + statusClass = cls || ''; + if (statusEl) { + statusEl.textContent = text; + statusEl.className = 'notes-status' + (cls ? ' ' + cls : ''); + } + if (text) { + setTimeout(function () { + if (!disposed && statusEl && statusEl.textContent === text) { + statusEl.textContent = ''; + statusEl.className = 'notes-status'; + } + }, 4000); + } + } + + function loadNotes() { + listContainer.innerHTML = ''; + listContainer.appendChild(el('div', { className: 'notes-empty' }, ['Loading...'])); + + var parent = notesParent(); + api.backend.call('ListNotes', parent).then(function (result) { + if (disposed) return; + var unpack = unpackResult(result); + if (unpack.error) { + renderEmpty('Could not load notes: ' + unpack.error); + return; + } + notes = unpack.value || []; + renderList(); + }).catch(function (err) { + if (disposed) return; + renderEmpty('Error: ' + (err.message || err)); + }); + } + + function renderList() { + listContainer.innerHTML = ''; + if (!notes || notes.length === 0) { + renderEmpty('No notes yet'); + return; + } + notes.forEach(function (note) { + var row = el('div', { + className: 'notes-item' + (note.isOverview ? ' overview' : '') + (note.path === selectedPath ? ' selected' : ''), + 'data-note-path': note.path, + 'data-note-title': note.title, + onClick: function () { selectNote(note); }, + onDblclick: function () { openNote(note); } + }, [ + el('span', { className: 'notes-item-icon', innerHTML: note.isOverview ? iconSvg('overview') : iconSvg('note') }), + el('span', { className: 'notes-item-name', textContent: note.title || fileName(note.path), title: note.title || note.path }), + el('span', { className: 'notes-item-actions' }, [ + el('button', { + className: 'notes-item-btn', + title: 'Open', + innerHTML: iconSvg('open'), + onClick: function (e) { e.stopPropagation(); openNote(note); } + }), + !note.isOverview ? el('button', { + className: 'notes-item-btn', + title: 'Rename', + innerHTML: iconSvg('rename'), + onClick: function (e) { e.stopPropagation(); beginRename(note); } + }) : null + ]) + ]); + listContainer.appendChild(row); + }); + } + + function renderEmpty(msg) { + listContainer.innerHTML = ''; + listContainer.appendChild(el('div', { className: 'notes-empty' }, [ + el('div', {}, [iconSvg('note')]), + el('div', {}, [msg]), + el('div', { className: 'notes-empty-hint' }, ['Click "New Note" to create one']) + ])); + } + + function selectNote(note) { + selectedPath = note.path; + renderList(); + } + + function openNote(note) { + if (!note) return; + var ext = '.md'; + api.workbench.openResource({ + kind: 'vault-file', + path: note.path, + mode: 'view', + extension: ext, + context: { + sourcePluginId: 'verstak.notes', + sourceView: 'notes', + isInsideNotesFolder: true, + notesMode: true, + notesScopePath: notesParent() + } + }).catch(function (err) { console.error('[notes] openResource:', err); }); + } + + function openOverview() { + setStatus('Opening overview...', 'loading'); + var parent = notesParent(); + api.backend.call('EnsureOverview', parent).then(function (result) { + if (disposed) return; + var unpack = unpackResult(result); + if (unpack.error) { + setStatus('Error: ' + unpack.error, 'error'); + return; + } + var overviewPath = (unpack.value && unpack.value.path) || ''; + if (!overviewPath) { + setStatus('Failed to get overview path', 'error'); + return; + } + loadNotes(); + api.workbench.openResource({ + kind: 'vault-file', + path: overviewPath, + mode: 'view', + extension: '.md', + context: { + sourcePluginId: 'verstak.notes', + sourceView: 'notes', + isInsideNotesFolder: true, + notesMode: true, + notesScopePath: notesParent() + } + }).catch(function (err) { setStatus('Error opening overview: ' + (err.message || err), 'error'); }); + }).catch(function (err) { + setStatus('Error: ' + (err.message || err), 'error'); + }); + } + + // ─── Create ──────────────────────────────────────────── + + function showCreate() { + createInput.value = ''; + createPanel.style.display = 'flex'; + createInput.focus(); + } + + function hideCreate() { + createPanel.style.display = 'none'; + } + + function confirmCreate() { + var title = createInput.value.trim(); + if (!title) return; + setStatus('Creating note...', 'loading'); + var parent = notesParent(); + api.backend.call('CreateNote', parent, title).then(function (result) { + if (disposed) return; + var unpack = unpackResult(result); + if (unpack.error) { + setStatus('Error: ' + unpack.error, 'error'); + return; + } + var data = unpack.value || {}; + if (data.conflict) { + showConflictModal(title, data.path); + return; + } + hideCreate(); + setStatus('Note created', 'success'); + loadNotes(); + // Open the newly created note + if (data.path) { + api.workbench.openResource({ + kind: 'vault-file', + path: data.path, + mode: 'edit', + extension: '.md', + context: { + sourcePluginId: 'verstak.notes', + sourceView: 'notes', + isInsideNotesFolder: true, + notesMode: true, + notesScopePath: parent + } + }).catch(function () {}); + } + }).catch(function (err) { + setStatus('Error: ' + (err.message || err), 'error'); + }); + } + + // ─── Rename ───────────────────────────────────────────── + + function beginRename(note) { + renameTarget = note; + renameInput.value = note.title || fileName(note.path); + renamePanel.style.display = 'flex'; + renameInput.focus(); + renameInput.select(); + } + + function hideRename() { + renameTarget = null; + renamePanel.style.display = 'none'; + } + + function confirmRename() { + if (!renameTarget) return; + var newTitle = renameInput.value.trim(); + if (!newTitle) return; + setStatus('Renaming...', 'loading'); + api.backend.call('RenameNote', renameTarget.path, newTitle).then(function (result) { + if (disposed) return; + var unpack = unpackResult(result); + if (unpack.error) { + setStatus('Error: ' + unpack.error, 'error'); + return; + } + var data = unpack.value || {}; + if (data.conflict) { + showConflictModal(newTitle, data.path); + return; + } + hideRename(); + setStatus('Note renamed', 'success'); + loadNotes(); + }).catch(function (err) { + setStatus('Error: ' + (err.message || err), 'error'); + }); + } + + // ─── Conflict Modal ───────────────────────────────────── + + function showConflictModal(title, existingPath) { + var overlay = el('div', { className: 'notes-modal-overlay' }); + var modal = el('div', { className: 'notes-modal' }, [ + el('div', { className: 'notes-modal-title' }, ['Name Conflict']), + el('div', { className: 'notes-modal-msg' }, [ + 'A note with the title "' + title + '" already exists.', + ' Please choose a different title.' + ].join('')), + el('div', { className: 'notes-modal-actions' }, [ + el('button', { className: 'notes-modal-btn confirm', textContent: 'OK', onClick: function () { overlay.remove(); createInput.focus(); } }) + ]) + ]); + overlay.appendChild(modal); + document.body.appendChild(overlay); + } + + // ─── Event Wiring ─────────────────────────────────────── + + overviewBtn.addEventListener('click', openOverview); + createBtn.addEventListener('click', showCreate); + createConfirm.addEventListener('click', confirmCreate); + createCancel.addEventListener('click', hideCreate); + renameConfirm.addEventListener('click', confirmRename); + renameCancel.addEventListener('click', hideRename); + createInput.addEventListener('keydown', function (e) { + if (e.key === 'Enter') confirmCreate(); + if (e.key === 'Escape') hideCreate(); + }); + renameInput.addEventListener('keydown', function (e) { + if (e.key === 'Enter') confirmRename(); + if (e.key === 'Escape') hideRename(); + }); + + // ─── Init ─────────────────────────────────────────────── + + loadNotes(); + + containerEl.__notesCleanup = function () { + disposed = true; + }; + }, + + unmount: function (containerEl) { + if (containerEl.__notesCleanup) { + containerEl.__notesCleanup(); + containerEl.__notesCleanup = null; + } + containerEl.innerHTML = ''; + } + }; + + function unpackResult(result) { + if (Array.isArray(result) && result.length === 2) { + return { value: result[0], error: result[1] || '' }; + } + if (result && result.value !== undefined && result.error !== undefined) { + return result; + } + return { value: result, error: '' }; + } + + window.VerstakPluginRegister('verstak.notes', { + components: { NotesView: NotesView } + }); +})(); diff --git a/plugins/notes/plugin.json b/plugins/notes/plugin.json new file mode 100644 index 0000000..6b9af22 --- /dev/null +++ b/plugins/notes/plugin.json @@ -0,0 +1,39 @@ +{ + "schemaVersion": 1, + "id": "verstak.notes", + "name": "Notes", + "version": "0.1.0", + "apiVersion": "0.1.0", + "description": "Workspace-scoped notes manager with create, rename, overview, and workbench integration.", + "source": "official", + "icon": "file-text", + "provides": [ + "verstak/notes/v1" + ], + "requires": [ + "verstak/core/files/v1", + "verstak/core/workbench/v1" + ], + "optionalRequires": [ + "verstak/core/notes/v1" + ], + "permissions": [ + "files.read", + "files.write", + "workbench.open", + "ui.register" + ], + "frontend": { + "entry": "frontend/src/index.js" + }, + "contributes": { + "workspaceItems": [ + { + "id": "verstak.notes.workspace", + "title": "Notes", + "icon": "file-text", + "component": "NotesView" + } + ] + } +} diff --git a/plugins/platform-test/frontend/dist/index.js b/plugins/platform-test/frontend/dist/index.js index 16750fd..cd2d0a6 100644 --- a/plugins/platform-test/frontend/dist/index.js +++ b/plugins/platform-test/frontend/dist/index.js @@ -458,12 +458,151 @@ apiStatusList, ]); + /* ── Mouse Event Inspector ──────────────────────────────────── */ + var mouseLog = []; + var mouseCapturing = false; + var mouseHandlers = []; + var mouseEventTypes = ['pointerdown', 'pointerup', 'mousedown', 'mouseup', 'auxclick']; + var mouseLogContainer = el('pre', { + className: 'pt-mouse-log', + style: { + background: '#0d0d1a', + color: '#4ecca3', + padding: '0.75rem', + borderRadius: '6px', + fontSize: '0.75rem', + fontFamily: 'monospace', + maxHeight: '300px', + overflow: 'auto', + whiteSpace: 'pre-wrap', + wordBreak: 'break-all', + margin: '0.5rem 0', + lineHeight: '1.5', + }, + }, ['']); + + var mouseCountSpan = span('pt-list-value', '0'); + + function renderMouseLog() { + if (mouseLog.length === 0) { + mouseLogContainer.textContent = '(no events captured)'; + mouseCountSpan.textContent = '0'; + return; + } + var lines = mouseLog.map(function (e, i) { + return '[' + (i + 1) + '] ' + e.type + ' button=' + e.button + ' buttons=' + e.buttons + + ' which=' + e.which + ' pointerType=' + e.pointerType + + ' defaultPrevented=' + e.defaultPrevented + + ' target=' + e.targetTag + (e.targetClass ? '.' + e.targetClass : ''); + }); + mouseLogContainer.textContent = lines.join('\n'); + mouseCountSpan.textContent = String(mouseLog.length); + mouseLogContainer.scrollTop = mouseLogContainer.scrollHeight; + } + + function onMouseEvent(e) { + mouseLog.push({ + type: e.type, + button: e.button, + buttons: e.buttons, + which: e.which, + pointerType: e.pointerType || '', + defaultPrevented: e.defaultPrevented, + targetTag: e.target ? e.target.tagName : '', + targetClass: e.target ? (e.target.className || '') : '', + time: Date.now(), + }); + if (mouseLog.length > 200) mouseLog.shift(); + renderMouseLog(); + } + + function startMouseCapture() { + if (mouseCapturing) return; + mouseCapturing = true; + mouseEventTypes.forEach(function (type) { + var handler = function (e) { onMouseEvent(e); }; + window.addEventListener(type, handler, true); + mouseHandlers.push({ type: type, handler: handler }); + }); + startStopBtn.textContent = '■ Stop Capture'; + startStopBtn.setAttribute('data-mouse-capturing', 'true'); + renderMouseLog(); + } + + function stopMouseCapture() { + if (!mouseCapturing) return; + mouseCapturing = false; + mouseHandlers.forEach(function (h) { + window.removeEventListener(h.type, h.handler, true); + }); + mouseHandlers = []; + startStopBtn.textContent = '▶ Start Capture'; + startStopBtn.setAttribute('data-mouse-capturing', 'false'); + } + + var startStopBtn = el('button', { + className: 'btn btn-primary', + style: { marginRight: '0.5rem' }, + onClick: function () { + if (mouseCapturing) { stopMouseCapture(); } else { startMouseCapture(); } + }, + }, ['▶ Start Capture']); + + var clearBtn = el('button', { + className: 'btn btn-secondary', + style: { marginRight: '0.5rem' }, + onClick: function () { + mouseLog = []; + renderMouseLog(); + }, + }, ['Clear']); + + var copyBtn = el('button', { + className: 'btn btn-secondary', + onClick: function () { + var json = JSON.stringify(mouseLog, null, 2); + if (navigator.clipboard) { + navigator.clipboard.writeText(json).then(function () { + copyBtn.textContent = '✓ Copied!'; + setTimeout(function () { copyBtn.textContent = 'Copy JSON'; }, 1500); + }); + } else { + var ta = document.createElement('textarea'); + ta.value = json; + document.body.appendChild(ta); + ta.select(); + document.execCommand('copy'); + document.body.removeChild(ta); + copyBtn.textContent = '✓ Copied!'; + setTimeout(function () { copyBtn.textContent = 'Copy JSON'; }, 1500); + } + }, + }, ['Copy JSON']); + + trackCleanup(function () { stopMouseCapture(); }); + + var mouseCard = div('pt-card', [ + el('h3', { className: 'pt-card-title' }, ['Mouse Event Inspector']), + el('p', { style: { margin: '0 0 0.5rem', color: '#a0a0b8', fontSize: '0.8rem' } }, [ + 'Captures ALL mouse/pointer events on window. Press back/forward buttons to see what WebKitGTK reports.', + ]), + el('div', { style: { display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem' } }, [ + startStopBtn, + clearBtn, + copyBtn, + span('pt-list-label', ' Events:'), + mouseCountSpan, + ]), + mouseLogContainer, + ]); + /* ── Assemble ──────────────────────────────────────────────── */ containerEl.appendChild(header); containerEl.appendChild(badgeRow); containerEl.appendChild(bridgeCard); containerEl.appendChild(testsCard); containerEl.appendChild(capsCard); + containerEl.appendChild(mouseCard); containerEl.appendChild(infoCard); containerEl.appendChild(apiCard); }, diff --git a/plugins/platform-test/frontend/src/index.js b/plugins/platform-test/frontend/src/index.js index 16750fd..cd2d0a6 100644 --- a/plugins/platform-test/frontend/src/index.js +++ b/plugins/platform-test/frontend/src/index.js @@ -458,12 +458,151 @@ apiStatusList, ]); + /* ── Mouse Event Inspector ──────────────────────────────────── */ + var mouseLog = []; + var mouseCapturing = false; + var mouseHandlers = []; + var mouseEventTypes = ['pointerdown', 'pointerup', 'mousedown', 'mouseup', 'auxclick']; + var mouseLogContainer = el('pre', { + className: 'pt-mouse-log', + style: { + background: '#0d0d1a', + color: '#4ecca3', + padding: '0.75rem', + borderRadius: '6px', + fontSize: '0.75rem', + fontFamily: 'monospace', + maxHeight: '300px', + overflow: 'auto', + whiteSpace: 'pre-wrap', + wordBreak: 'break-all', + margin: '0.5rem 0', + lineHeight: '1.5', + }, + }, ['']); + + var mouseCountSpan = span('pt-list-value', '0'); + + function renderMouseLog() { + if (mouseLog.length === 0) { + mouseLogContainer.textContent = '(no events captured)'; + mouseCountSpan.textContent = '0'; + return; + } + var lines = mouseLog.map(function (e, i) { + return '[' + (i + 1) + '] ' + e.type + ' button=' + e.button + ' buttons=' + e.buttons + + ' which=' + e.which + ' pointerType=' + e.pointerType + + ' defaultPrevented=' + e.defaultPrevented + + ' target=' + e.targetTag + (e.targetClass ? '.' + e.targetClass : ''); + }); + mouseLogContainer.textContent = lines.join('\n'); + mouseCountSpan.textContent = String(mouseLog.length); + mouseLogContainer.scrollTop = mouseLogContainer.scrollHeight; + } + + function onMouseEvent(e) { + mouseLog.push({ + type: e.type, + button: e.button, + buttons: e.buttons, + which: e.which, + pointerType: e.pointerType || '', + defaultPrevented: e.defaultPrevented, + targetTag: e.target ? e.target.tagName : '', + targetClass: e.target ? (e.target.className || '') : '', + time: Date.now(), + }); + if (mouseLog.length > 200) mouseLog.shift(); + renderMouseLog(); + } + + function startMouseCapture() { + if (mouseCapturing) return; + mouseCapturing = true; + mouseEventTypes.forEach(function (type) { + var handler = function (e) { onMouseEvent(e); }; + window.addEventListener(type, handler, true); + mouseHandlers.push({ type: type, handler: handler }); + }); + startStopBtn.textContent = '■ Stop Capture'; + startStopBtn.setAttribute('data-mouse-capturing', 'true'); + renderMouseLog(); + } + + function stopMouseCapture() { + if (!mouseCapturing) return; + mouseCapturing = false; + mouseHandlers.forEach(function (h) { + window.removeEventListener(h.type, h.handler, true); + }); + mouseHandlers = []; + startStopBtn.textContent = '▶ Start Capture'; + startStopBtn.setAttribute('data-mouse-capturing', 'false'); + } + + var startStopBtn = el('button', { + className: 'btn btn-primary', + style: { marginRight: '0.5rem' }, + onClick: function () { + if (mouseCapturing) { stopMouseCapture(); } else { startMouseCapture(); } + }, + }, ['▶ Start Capture']); + + var clearBtn = el('button', { + className: 'btn btn-secondary', + style: { marginRight: '0.5rem' }, + onClick: function () { + mouseLog = []; + renderMouseLog(); + }, + }, ['Clear']); + + var copyBtn = el('button', { + className: 'btn btn-secondary', + onClick: function () { + var json = JSON.stringify(mouseLog, null, 2); + if (navigator.clipboard) { + navigator.clipboard.writeText(json).then(function () { + copyBtn.textContent = '✓ Copied!'; + setTimeout(function () { copyBtn.textContent = 'Copy JSON'; }, 1500); + }); + } else { + var ta = document.createElement('textarea'); + ta.value = json; + document.body.appendChild(ta); + ta.select(); + document.execCommand('copy'); + document.body.removeChild(ta); + copyBtn.textContent = '✓ Copied!'; + setTimeout(function () { copyBtn.textContent = 'Copy JSON'; }, 1500); + } + }, + }, ['Copy JSON']); + + trackCleanup(function () { stopMouseCapture(); }); + + var mouseCard = div('pt-card', [ + el('h3', { className: 'pt-card-title' }, ['Mouse Event Inspector']), + el('p', { style: { margin: '0 0 0.5rem', color: '#a0a0b8', fontSize: '0.8rem' } }, [ + 'Captures ALL mouse/pointer events on window. Press back/forward buttons to see what WebKitGTK reports.', + ]), + el('div', { style: { display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem' } }, [ + startStopBtn, + clearBtn, + copyBtn, + span('pt-list-label', ' Events:'), + mouseCountSpan, + ]), + mouseLogContainer, + ]); + /* ── Assemble ──────────────────────────────────────────────── */ containerEl.appendChild(header); containerEl.appendChild(badgeRow); containerEl.appendChild(bridgeCard); containerEl.appendChild(testsCard); containerEl.appendChild(capsCard); + containerEl.appendChild(mouseCard); containerEl.appendChild(infoCard); containerEl.appendChild(apiCard); },