From da497885047febc0d6ee407de3fa7e9280b5f939 Mon Sep 17 00:00:00 2001
From: mirivlad
';
+ });
+
+ // Inline code
+ html = html.replace(/`([^`\n]+)`/g, '' + code.trimEnd() + '$1');
+
+ // Headings
+ html = html.replace(/^######\s+(.+)$/gm, '$1
');
+ html = html.replace(/^#####\s+(.+)$/gm, '$1
');
+ html = html.replace(/^####\s+(.+)$/gm, '$1
');
+ html = html.replace(/^###\s+(.+)$/gm, '$1
');
+ html = html.replace(/^##\s+(.+)$/gm, '$1
');
+ html = html.replace(/^#\s+(.+)$/gm, '$1
');
+
+ // Horizontal rule
+ html = html.replace(/^---+$/gm, '
');
+ html = html.replace(/^\*\*\*+$/gm, '
');
+
+ // Bold and italic
+ html = html.replace(/\*\*\*(.+?)\*\*\*/g, '$1');
+ html = html.replace(/\*\*(.+?)\*\*/g, '$1');
+ html = html.replace(/\*(.+?)\*/g, '$1');
+
+ // Strikethrough
+ html = html.replace(/~~(.+?)~~/g, '$1');
+
+ // Blockquote
+ html = html.replace(/^>\s+(.+)$/gm, '$1
');
+
+ // Unordered list items
+ html = html.replace(/^[\-\*]\s+(.+)$/gm, '
+ html = html.replace(/((?:
$1
');
+
+ // Links [text](url) — render as text since we can't navigate internally
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1');
+
+ // Images  — render as placeholder
+ html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '[image: $1]');
+
+ // Paragraphs: double newlines
+ html = html.replace(/\n\n+/g, '
'); + html = '
' + html + '
'; + + // Clean up empty paragraphs + html = html.replace(/\s*<\/p>/g, ''); + html = html.replace(/
\s*(<(?:h[1-6]|ul|ol|pre|blockquote|hr))/g, '$1'); + html = html.replace(/(<\/(?:h[1-6]|ul|ol|pre|blockquote|hr)>)\s*<\/p>/g, '$1'); + + return html; + } + + /* ── Utilities ──────────────────────────────────────────── */ + 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 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 detectMode(props) { + var ctx = props.request && props.request.context; + if (ctx && (ctx.notesMode || ctx.isInsideNotesFolder)) return 'notes-markdown'; + var ext = (props.request && props.request.extension || '').toLowerCase(); + if (ext === '.md' || ext === '.markdown') return 'generic-markdown'; + return 'text'; + } + + function detectContextLabel(mode) { + if (mode === 'notes-markdown') return 'notes'; + if (mode === 'generic-markdown') return 'markdown'; + return 'text'; + } + + function fileName(path) { + if (!path) return ''; + var parts = path.split('/'); + return parts[parts.length - 1]; + } + + /* ── DefaultEditor component ─────────────────────────────── */ + var DefaultEditor = { + mount: function (containerEl, props, api) { + injectStyles(); + containerEl.innerHTML = ''; + containerEl.className = 'de-root'; + + var request = props.request || {}; + var resourcePath = request.path || ''; + var mode = request.mode || 'view'; + var editorMode = detectMode(props); + var isMarkdown = editorMode === 'generic-markdown' || editorMode === 'notes-markdown'; + var previewVisible = isMarkdown && mode === 'view'; + + var currentContent = ''; + var savedContent = ''; + var dirty = false; + var saveState = ''; // '' | 'saved' | 'error' + var saveTimer = null; + var disposed = false; + + // ── Toolbar ────────────────────────────────────────── + var modeLabel = el('span', { className: 'de-toolbar-mode' }, [editorMode]); + var contextLabel = el('span', { className: 'de-toolbar-context' }, [fileName(resourcePath)]); + + var notesBadge = null; + if (editorMode === 'notes-markdown') { + notesBadge = el('span', { className: 'de-notes-badge' }, ['notes context']); + } + + var spacer = el('span', { className: 'de-toolbar-spacer' }); + + var editBtn = null; + var previewBtn = null; + var saveBtn = el('button', { className: 'de-toolbar-btn' }, ['Save']); + saveBtn.disabled = true; + + if (isMarkdown) { + editBtn = el('button', { className: 'de-toolbar-btn' + (mode === 'edit' ? ' active' : '') }, ['Edit']); + previewBtn = el('button', { className: 'de-toolbar-btn' + (previewVisible ? ' active' : '') }, ['Preview']); + if (mode === 'edit') { + previewBtn.classList.remove('active'); + } + } + + var statusEl = el('span', { className: 'de-status' }); + + var toolbarChildren = [modeLabel, contextLabel]; + if (notesBadge) toolbarChildren.push(notesBadge); + toolbarChildren.push(spacer); + if (editBtn) toolbarChildren.push(editBtn); + if (previewBtn) toolbarChildren.push(previewBtn); + toolbarChildren.push(saveBtn); + toolbarChildren.push(statusEl); + + var toolbar = el('div', { className: 'de-toolbar' }, toolbarChildren); + containerEl.appendChild(toolbar); + + // ── Editor area ────────────────────────────────────── + var editorWrap = el('div', { className: 'de-editor-wrap' }); + containerEl.appendChild(editorWrap); + + var textarea = null; + var previewEl = null; + + if (mode === 'edit' || !isMarkdown) { + textarea = el('textarea', { className: 'de-textarea', spellcheck: 'false' }); + textarea.value = ''; + editorWrap.appendChild(textarea); + + textarea.addEventListener('input', function () { + currentContent = textarea.value; + dirty = currentContent !== savedContent; + updateStatus(); + }); + + textarea.addEventListener('keydown', function (e) { + if ((e.ctrlKey || e.metaKey) && e.key === 's') { + e.preventDefault(); + save(); + } + // Tab support + if (e.key === 'Tab') { + e.preventDefault(); + var start = textarea.selectionStart; + var end = textarea.selectionEnd; + textarea.value = textarea.value.substring(0, start) + ' ' + textarea.value.substring(end); + textarea.selectionStart = textarea.selectionEnd = start + 2; + textarea.dispatchEvent(new Event('input')); + } + }); + } + + if (isMarkdown && previewVisible) { + previewEl = el('div', { className: 'de-preview' }); + editorWrap.appendChild(previewEl); + } + + // ── Notes info bar ─────────────────────────────────── + if (editorMode === 'notes-markdown') { + var notesInfo = el('div', { className: 'de-notes-info' }, [ + 'Notes context active — internal links, backlinks, and widgets deferred.' + ]); + containerEl.appendChild(notesInfo); + } + + // ── Status helpers ─────────────────────────────────── + function updateStatus() { + if (saveState === 'error') { + statusEl.textContent = 'Error saving'; + statusEl.className = 'de-status error'; + } else if (saveState === 'saved') { + statusEl.textContent = 'Saved'; + statusEl.className = 'de-status saved'; + } else if (dirty) { + statusEl.textContent = 'Modified'; + statusEl.className = 'de-status dirty'; + } else { + statusEl.textContent = ''; + statusEl.className = 'de-status'; + } + saveBtn.disabled = !dirty; + } + + function updatePreview() { + if (previewEl) { + previewEl.innerHTML = renderMarkdown(currentContent); + } + } + + // ── Save ───────────────────────────────────────────── + function save() { + if (!dirty || disposed) return; + saveState = ''; + updateStatus(); + api.files.writeText(resourcePath, currentContent, { + createIfMissing: false, + overwrite: true + }).then(function () { + if (disposed) return; + savedContent = currentContent; + dirty = false; + saveState = 'saved'; + updateStatus(); + if (saveTimer) clearTimeout(saveTimer); + saveTimer = setTimeout(function () { + if (!disposed) { saveState = ''; updateStatus(); } + }, 2000); + }).catch(function (err) { + if (disposed) return; + saveState = 'error'; + updateStatus(); + console.error('[default-editor] save error:', err); + }); + } + + // ── Toolbar events ─────────────────────────────────── + saveBtn.addEventListener('click', save); + + if (editBtn) { + editBtn.addEventListener('click', function () { + if (mode === 'edit') return; + mode = 'edit'; + previewVisible = false; + rebuildEditorArea(); + }); + } + + if (previewBtn) { + previewBtn.addEventListener('click', function () { + if (previewVisible) return; + previewVisible = true; + mode = isMarkdown ? 'view' : mode; + rebuildEditorArea(); + }); + } + + function rebuildEditorArea() { + editorWrap.innerHTML = ''; + textarea = null; + previewEl = null; + + if (mode === 'edit' || !isMarkdown) { + textarea = el('textarea', { className: 'de-textarea', spellcheck: 'false' }); + textarea.value = currentContent; + editorWrap.appendChild(textarea); + + textarea.addEventListener('input', function () { + currentContent = textarea.value; + dirty = currentContent !== savedContent; + updateStatus(); + }); + + textarea.addEventListener('keydown', function (e) { + if ((e.ctrlKey || e.metaKey) && e.key === 's') { + e.preventDefault(); + save(); + } + if (e.key === 'Tab') { + e.preventDefault(); + var start = textarea.selectionStart; + var end = textarea.selectionEnd; + textarea.value = textarea.value.substring(0, start) + ' ' + textarea.value.substring(end); + textarea.selectionStart = textarea.selectionEnd = start + 2; + textarea.dispatchEvent(new Event('input')); + } + }); + } + + if (isMarkdown && previewVisible) { + previewEl = el('div', { className: 'de-preview' }); + editorWrap.appendChild(previewEl); + updatePreview(); + } + + // Update toolbar button states + if (editBtn) { + editBtn.className = 'de-toolbar-btn' + (mode === 'edit' ? ' active' : ''); + } + if (previewBtn) { + previewBtn.className = 'de-toolbar-btn' + (previewVisible ? ' active' : ''); + } + updateStatus(); + } + + // ── Load file ──────────────────────────────────────── + editorWrap.appendChild(el('div', { className: 'de-loading' }, ['Loading...'])); + + api.files.readText(resourcePath).then(function (content) { + if (disposed) return; + editorWrap.innerHTML = ''; + currentContent = content; + savedContent = content; + dirty = false; + rebuildEditorArea(); + }).catch(function (err) { + if (disposed) return; + editorWrap.innerHTML = ''; + var msg = (err && err.message) ? err.message : String(err); + editorWrap.appendChild(el('div', { className: 'de-error' }, [ + el('div', {}, ['Failed to load file']), + el('div', { className: 'de-error-msg' }, [msg]) + ])); + }); + + // ── Cleanup ────────────────────────────────────────── + containerEl.__deCleanup = function () { + disposed = true; + if (saveTimer) clearTimeout(saveTimer); + }; + + // Set data attributes for testability + containerEl.setAttribute('data-editor-mode', editorMode); + containerEl.setAttribute('data-resource-path', resourcePath); + containerEl.setAttribute('data-request-mode', mode); + }, + + unmount: function (containerEl) { + if (containerEl.__deCleanup) { + containerEl.__deCleanup(); + containerEl.__deCleanup = null; + } + containerEl.innerHTML = ''; + } + }; + + /* ── Register ────────────────────────────────────────────── */ + window.VerstakPluginRegister('verstak.default-editor', { + components: { + DefaultEditor: DefaultEditor + } + }); + +})(); diff --git a/plugins/default-editor/plugin.json b/plugins/default-editor/plugin.json new file mode 100644 index 0000000..e81c2f3 --- /dev/null +++ b/plugins/default-editor/plugin.json @@ -0,0 +1,69 @@ +{ + "schemaVersion": 1, + "id": "verstak.default-editor", + "name": "Default Editor", + "version": "0.1.0", + "apiVersion": "0.1.0", + "description": "Built-in text and markdown editor/viewer for Verstak. Provides openProviders for generic text, generic markdown, and notes-context markdown files.", + "source": "official", + "icon": "edit", + "provides": [ + "verstak/default-editor/v1" + ], + "requires": [ + "verstak/core/files/v1", + "verstak/core/workbench/v1" + ], + "permissions": [ + "files.read", + "files.write", + "workbench.open" + ], + "frontend": { + "entry": "frontend/dist/index.js" + }, + "contributes": { + "openProviders": [ + { + "id": "verstak.default-editor.text", + "title": "Default Text Editor", + "priority": 50, + "component": "DefaultEditor", + "supports": [ + { + "kind": "vault-file", + "extensions": [".txt", ".log", ".conf", ".ini", ".toml", ".yaml", ".yml", ".json", ".csv"], + "mime": ["text/plain", "application/json"], + "contexts": ["generic-text"] + } + ] + }, + { + "id": "verstak.default-editor.markdown", + "title": "Default Markdown Editor", + "priority": 50, + "component": "DefaultEditor", + "supports": [ + { + "kind": "vault-file", + "extensions": [".md", ".markdown"], + "contexts": ["generic-markdown"] + } + ] + }, + { + "id": "verstak.default-editor.notes-markdown", + "title": "Default Notes Markdown Editor", + "priority": 50, + "component": "DefaultEditor", + "supports": [ + { + "kind": "vault-file", + "extensions": [".md", ".markdown"], + "contexts": ["notes-markdown"] + } + ] + } + ] + } +} diff --git a/plugins/files/frontend/src/index.js b/plugins/files/frontend/src/index.js new file mode 100644 index 0000000..8f8e2e0 --- /dev/null +++ b/plugins/files/frontend/src/index.js @@ -0,0 +1,281 @@ +/* =========================================================== + Files Plugin — Verstak v2 Frontend Bundle + Contract: window.VerstakPluginRegister(id, { components }) + =========================================================== */ + +(function () { + 'use strict'; + + /* ── Style injection ─────────────────────────────────────── */ + function injectStyles() { + if (document.getElementById('files-style-injected')) return; + var style = document.createElement('style'); + style.id = 'files-style-injected'; + style.textContent = STYLES; + document.head.appendChild(style); + } + + 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}', + '.files-toolbar{display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0.75rem;border-bottom:1px solid #16213e;flex-shrink:0;background:#12122a}', + '.files-toolbar-btn{font-size:0.75rem;padding:0.25rem 0.6rem;border:1px solid #333;border-radius:4px;background:#1a1a2e;color:#ccc;cursor:pointer}', + '.files-toolbar-btn:hover{background:#2a2a4e;border-color:#4ecca3}', + '.files-toolbar-btn:disabled{opacity:0.4;cursor:default}', + '.files-breadcrumb{display:flex;align-items:center;gap:0.25rem;font-size:0.8rem;color:#8b8ba8;flex:1;min-width:0;overflow:hidden}', + '.files-breadcrumb-item{color:#4ecca3;cursor:pointer;padding:0.1rem 0.3rem;border-radius:3px}', + '.files-breadcrumb-item:hover{background:#1a2a3a}', + '.files-breadcrumb-sep{color:#555}', + '.files-list{flex:1;overflow-y:auto;padding:0.5rem 0}', + '.files-item{display:flex;align-items:center;gap:0.6rem;padding:0.4rem 0.75rem;cursor:pointer;font-size:0.85rem}', + '.files-item:hover{background:#1a1a2e}', + '.files-item.selected{background:#1a2a3a}', + '.files-item-icon{font-size:1rem;width:1.2rem;text-align:center;flex-shrink:0}', + '.files-item-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}', + '.files-item-meta{font-size:0.7rem;color:#666;flex-shrink:0}', + '.files-empty{flex:1;display:flex;align-items:center;justify-content:center;color:#666;font-size:0.9rem}', + '.files-loading{flex:1;display:flex;align-items:center;justify-content:center;color:#666}', + '.files-error{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#e74c3c;gap:0.5rem;padding:1rem}', + '.files-error-msg{font-size:0.85rem;color:#aaa;max-width:400px;text-align:center}', + '.files-create-bar{display:flex;align-items:center;gap:0.5rem;padding:0.5rem 0.75rem;border-top:1px solid #16213e;flex-shrink:0;background:#12122a}', + '.files-create-input{flex:1;font-size:0.8rem;padding:0.3rem 0.5rem;border:1px solid #333;border-radius:4px;background:#0d0d1a;color:#e0e0e0;outline:none}', + '.files-create-input:focus{border-color:#4ecca3}', + ].join('\n'); + + /* ── Utilities ──────────────────────────────────────────── */ + 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 fileIcon(entry) { + if (entry.type === 'folder') return '\uD83D\uDCC1'; + var ext = (entry.extension || '').toLowerCase(); + if (ext === 'md' || ext === 'markdown') return '\uD83D\uDCDD'; + if (ext === 'txt' || ext === 'log') return '\uD83D\uDCC4'; + if (ext === 'json') return '{ }'; + if (ext === 'yaml' || ext === 'yml' || ext === 'toml') return '\u2699\uFE0F'; + return '\uD83D\uDCC1'; + } + + function formatSize(bytes) { + if (bytes == null || bytes === 0) return ''; + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / 1048576).toFixed(1) + ' MB'; + } + + function sortEntries(entries) { + var folders = entries.filter(function (e) { return e.type === 'folder'; }); + var files = entries.filter(function (e) { return e.type !== 'folder'; }); + folders.sort(function (a, b) { return a.name.localeCompare(b.name); }); + files.sort(function (a, b) { return a.name.localeCompare(b.name); }); + return folders.concat(files); + } + + /* ── FilesView component ─────────────────────────────────── */ + var FilesView = { + mount: function (containerEl, props, api) { + injectStyles(); + containerEl.innerHTML = ''; + containerEl.className = 'files-root'; + + var currentPath = ''; + var entries = []; + var disposed = false; + + var toolbar = el('div', { className: 'files-toolbar' }); + var breadcrumb = el('div', { className: 'files-breadcrumb' }); + var refreshBtn = el('button', { className: 'files-toolbar-btn' }, ['Refresh']); + var createFolderBtn = el('button', { className: 'files-toolbar-btn' }, ['+ Folder']); + var createFileBtn = el('button', { className: 'files-toolbar-btn' }, ['+ File']); + toolbar.appendChild(breadcrumb); + toolbar.appendChild(refreshBtn); + toolbar.appendChild(createFolderBtn); + toolbar.appendChild(createFileBtn); + containerEl.appendChild(toolbar); + + var listContainer = el('div', { className: 'files-list' }); + containerEl.appendChild(listContainer); + + var createBar = el('div', { className: 'files-create-bar', style: { display: 'none' } }); + var createInput = el('input', { className: 'files-create-input', placeholder: 'Name...' }); + var createConfirmBtn = el('button', { className: 'files-toolbar-btn' }, ['Create']); + var createCancelBtn = el('button', { className: 'files-toolbar-btn' }, ['Cancel']); + createBar.appendChild(createInput); + createBar.appendChild(createConfirmBtn); + createBar.appendChild(createCancelBtn); + containerEl.appendChild(createBar); + + var createMode = ''; // 'folder' | 'file' | '' + + function updateBreadcrumb() { + breadcrumb.innerHTML = ''; + var rootItem = el('span', { className: 'files-breadcrumb-item', onClick: function () { navigateTo(''); } }, ['Root']); + breadcrumb.appendChild(rootItem); + if (currentPath) { + var parts = currentPath.split('/'); + var accumulated = ''; + parts.forEach(function (part, i) { + breadcrumb.appendChild(el('span', { className: 'files-breadcrumb-sep' }, [' / '])); + accumulated += (accumulated ? '/' : '') + part; + (function (path) { + breadcrumb.appendChild(el('span', { className: 'files-breadcrumb-item', onClick: function () { navigateTo(path); } }, [part])); + })(accumulated); + }); + } + } + + function renderList() { + listContainer.innerHTML = ''; + if (entries.length === 0) { + listContainer.appendChild(el('div', { className: 'files-empty' }, ['Empty folder'])); + return; + } + var sorted = sortEntries(entries); + sorted.forEach(function (entry) { + if (entry.isHidden || entry.isReserved) return; + var item = el('div', { className: 'files-item' }, [ + el('span', { className: 'files-item-icon' }, [fileIcon(entry)]), + el('span', { className: 'files-item-name', textContent: entry.name }), + el('span', { className: 'files-item-meta', textContent: entry.type === 'folder' ? '' : formatSize(entry.size) }), + ]); + if (entry.type === 'folder') { + item.addEventListener('dblclick', function () { + navigateTo(entry.relativePath); + }); + } else { + item.addEventListener('dblclick', function () { + openFile(entry); + }); + } + listContainer.appendChild(item); + }); + } + + function navigateTo(path) { + currentPath = path; + updateBreadcrumb(); + loadEntries(); + } + + function loadEntries() { + listContainer.innerHTML = ''; + listContainer.appendChild(el('div', { className: 'files-loading' }, ['Loading...'])); + api.files.list(currentPath).then(function (result) { + if (disposed) return; + entries = result || []; + renderList(); + }).catch(function (err) { + if (disposed) return; + listContainer.innerHTML = ''; + var msg = (err && err.message) ? err.message : String(err); + listContainer.appendChild(el('div', { className: 'files-error' }, [ + el('div', {}, ['Failed to load files']), + el('div', { className: 'files-error-msg' }, [msg]), + ])); + }); + } + + function openFile(entry) { + var ext = entry.extension ? '.' + entry.extension : ''; + var isMd = ext === '.md' || ext === '.markdown'; + var isNotes = currentPath.split('/')[0] === 'Notes'; + var context = { sourcePluginId: 'verstak.files', sourceView: 'files' }; + if (isMd && isNotes) { + context.isInsideNotesFolder = true; + context.notesMode = true; + } + api.workbench.openResource({ + kind: 'vault-file', + path: entry.relativePath, + mode: 'view', + extension: ext, + context: context, + }).catch(function (err) { + console.error('[files] openResource error:', err); + }); + } + + function startCreate(mode) { + createMode = mode; + createInput.value = ''; + createInput.placeholder = mode === 'folder' ? 'Folder name...' : 'File name (e.g. note.md)...'; + createBar.style.display = 'flex'; + createInput.focus(); + } + + function cancelCreate() { + createMode = ''; + createBar.style.display = 'none'; + } + + function confirmCreate() { + var name = createInput.value.trim(); + if (!name) return; + var path = currentPath ? currentPath + '/' + name : name; + var promise; + if (createMode === 'folder') { + promise = api.files.createFolder(path); + } else { + promise = api.files.writeText(path, '', { createIfMissing: true, overwrite: false }); + } + promise.then(function () { + cancelCreate(); + loadEntries(); + }).catch(function (err) { + var msg = (err && err.message) ? err.message : String(err); + createInput.value = ''; + createInput.placeholder = 'Error: ' + msg; + }); + } + + refreshBtn.addEventListener('click', loadEntries); + createFolderBtn.addEventListener('click', function () { startCreate('folder'); }); + createFileBtn.addEventListener('click', function () { startCreate('file'); }); + createConfirmBtn.addEventListener('click', confirmCreate); + createCancelBtn.addEventListener('click', cancelCreate); + createInput.addEventListener('keydown', function (e) { + if (e.key === 'Enter') confirmCreate(); + if (e.key === 'Escape') cancelCreate(); + }); + + loadEntries(); + + containerEl.__filesCleanup = function () { + disposed = true; + }; + }, + + unmount: function (containerEl) { + if (containerEl.__filesCleanup) { + containerEl.__filesCleanup(); + containerEl.__filesCleanup = null; + } + containerEl.innerHTML = ''; + } + }; + + /* ── Register ────────────────────────────────────────────── */ + window.VerstakPluginRegister('verstak.files', { + components: { + FilesView: FilesView + } + }); + +})(); diff --git a/plugins/files/plugin.json b/plugins/files/plugin.json new file mode 100644 index 0000000..6e48e41 --- /dev/null +++ b/plugins/files/plugin.json @@ -0,0 +1,36 @@ +{ + "schemaVersion": 1, + "id": "verstak.files", + "name": "Files", + "version": "0.1.0", + "apiVersion": "0.1.0", + "description": "Minimal vault file navigator. Shows folders and files, opens files through Workbench openResource.", + "source": "official", + "icon": "folder", + "provides": [ + "verstak/files/v1" + ], + "requires": [ + "verstak/core/files/v1", + "verstak/core/workbench/v1" + ], + "permissions": [ + "files.read", + "files.write", + "workbench.open", + "ui.register" + ], + "frontend": { + "entry": "frontend/dist/index.js" + }, + "contributes": { + "workspaceItems": [ + { + "id": "verstak.files.workspace", + "title": "Files", + "icon": "folder", + "component": "FilesView" + } + ] + } +} diff --git a/plugins/platform-test/plugin.json b/plugins/platform-test/plugin.json index 84dc93e..aae9810 100644 --- a/plugins/platform-test/plugin.json +++ b/plugins/platform-test/plugin.json @@ -86,19 +86,13 @@ { "id": "verstak.platform-test.markdown-diagnostic", "title": "Platform Test Markdown Diagnostic", - "priority": 100, + "priority": 10, "component": "MarkdownDiagnosticProvider", "supports": [ { "kind": "vault-file", "extensions": [".md", ".markdown"], "contexts": ["generic-markdown", "notes-markdown"] - }, - { - "kind": "vault-file", - "mime": ["text/plain"], - "extensions": [".txt", ".log"], - "contexts": ["generic-text"] } ] } diff --git a/scripts/check.sh b/scripts/check.sh index 7a2ec29..59079cd 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -92,6 +92,57 @@ else echo " ⚠️ node not available — skipping frontend smoke" fi +echo "" +echo "[frontend bundle execution]" +if command -v node &>/dev/null; then + BUNDLE_FAILED=0 + for plugin_dir in "$ROOT"/plugins/*/; do + plugin_id=$(basename "$plugin_dir") + manifest="$plugin_dir/plugin.json" + if [ ! -f "$manifest" ]; then continue; fi + # Check if plugin has frontend entry + entry=$(node -e "const m=require('$manifest');console.log(m.frontend&&m.frontend.entry||'')" 2>/dev/null) + if [ -z "$entry" ]; then continue; fi + bundle="$plugin_dir$entry" + if [ ! -f "$bundle" ]; then + echo " ❌ $plugin_id: bundle not found at $entry" + BUNDLE_FAILED=1 + continue + fi + # Execute bundle via new Function() and verify registration + node -e " + const fs = require('fs'); + const content = fs.readFileSync('$bundle', 'utf8'); + // Provide minimal globals + global.window = { VerstakPluginRegister: function(id, def) { global.__registered = id; } }; + global.document = { getElementById: function() { return null; }, createElement: function() { return { style: {}, setAttribute: function(){}, appendChild: function(){} }; }, head: { appendChild: function(){} } }; + try { + new Function(content)(); + if (!global.__registered) { + console.log('ERROR: plugin did not call VerstakPluginRegister'); + process.exit(1); + } + console.log('OK: ' + global.__registered); + } catch(e) { + console.log('ERROR: ' + e.message); + process.exit(1); + } + " 2>&1 | while read -r line; do + if [[ "$line" == ERROR:* ]]; then + echo " ❌ $plugin_id: ${line#ERROR: }" + BUNDLE_FAILED=1 + elif [[ "$line" == OK:* ]]; then + echo " ✅ $plugin_id: ${line#OK: }" + fi + done + done + if [ "$BUNDLE_FAILED" -ne 0 ]; then + FAILED=1 + fi +else + echo " ⚠️ node not available — skipping bundle execution" +fi + echo "" if [ "$FAILED" -eq 0 ]; then echo "✅ all checks passed"