feat: add default-editor + files plugins, bundle execution check, fix platform-test diagnostic priority

This commit is contained in:
2026-06-19 16:41:46 +08:00
parent 1d2190fc6c
commit da49788504
7 changed files with 900 additions and 7 deletions
+281
View File
@@ -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
}
});
})();
+36
View File
@@ -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"
}
]
}
}