/* =========================================================== Files Plugin — Verstak v2 Frontend Bundle Contract: window.VerstakPluginRegister(id, { components }) =========================================================== */ (function () { 'use strict'; 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;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: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}', '.files-breadcrumb-item{color:#4ecca3;cursor:pointer;padding:.1rem .3rem;border-radius:3px;white-space:nowrap}', '.files-breadcrumb-item:hover{background:#1a2a3a}', '.files-breadcrumb-current{color:#ddd;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}', '.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-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)}', '.files-header{position:sticky;top:0;background:#101028;color:#8b8ba8;font-size:.7rem;text-transform:uppercase;letter-spacing:.04em;z-index:1}', '.files-item{font-size:.84rem;cursor:pointer}', '.files-item:hover{background:#17172d}', '.files-item.selected{background:#1a2a3a}', '.files-namecell{display:flex;align-items:center;gap:.55rem;min-width:0}', '.files-item-icon{width:1.25rem;height:1.25rem;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;color:#8b8ba8}', '.files-item-icon svg{display:block;width:16px;height:16px}', '.files-item-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}', '.files-item-meta{font-size:.74rem;color:#777;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}', '.files-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:.35rem;white-space:nowrap}', '.files-empty,.files-loading{flex:1;display:flex;align-items:center;justify-content:center;color:#666;font-size:.9rem;padding:2rem}', '.files-error{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#e74c3c;gap:.5rem;padding:1rem}', '.files-error-msg{font-size:.85rem;color:#aaa;max-width:420px;text-align:center}', '.files-panel{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border-top:1px solid #16213e;flex-shrink:0;background:#12122a}', '.files-create-input,.files-rename-input{flex:1;min-width:160px}', '@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:hover{background:#2a2a4e}', '.files-ctx-menu-item.danger{color:#e74c3c}', '.files-ctx-menu-item.danger:hover{background:#2a2a4e;color:#ff6b6b}', '.files-ctx-menu-sep{height:1px;background:#333;margin:4px 8px}', '.files-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:10000;display:flex;align-items:center;justify-content:center}', '.files-modal{width:400px;max-width:90vw;padding:24px;background:#1a1a2e;border:1px solid #333;border-radius:12px;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;box-shadow:0 12px 40px rgba(0,0,0,.5)}', '.files-modal-title{font-size:.95rem;line-height:1.5;margin-bottom:20px;word-wrap:break-word}', '.files-modal-actions{display:flex;justify-content:flex-end;gap:8px}', '.files-modal-btn{font-size:.82rem;padding:.4rem 1rem;border:1px solid #333;border-radius:6px;cursor:pointer;font-family:inherit}', '.files-modal-btn.cancel{background:#2a2a4e;color:#ccc;border-color:#444}', '.files-modal-btn.cancel:hover{background:#3a3a5e}', '.files-modal-btn.confirm{background:#4ecca3;color:#111;border-color:#4ecca3}', '.files-modal-btn.confirm:hover{background:#3dbb92}', '.files-modal-btn.danger{background:#e74c3c;color:#fff;border-color:#e74c3c}', '.files-modal-btn.danger:hover{background:#c0392b}', '.files-dragging{opacity:.5}', '.files-drag-over{outline:2px dashed #4ecca3;outline-offset:-2px;background:rgba(78,204,163,.08)}' ].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 ''; } function cleanPath(path) { return String(path || '').split('/').filter(Boolean).join('/'); } function parentPath(path) { path = cleanPath(path); var idx = path.lastIndexOf('/'); return idx === -1 ? '' : path.slice(0, idx); } function baseName(path) { path = cleanPath(path); var idx = path.lastIndexOf('/'); return idx === -1 ? path : path.slice(idx + 1); } function extension(name) { var dot = String(name || '').lastIndexOf('.'); return dot > 0 ? name.slice(dot + 1).toLowerCase() : ''; } var FILE_ICONS = { folder: 'M3 5a2 2 0 0 1 2-2h5l2 3h7a2 2 0 0 1 2 2v1H3V5Zm0 6h18v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7Z', markdown: 'M5 3h10l4 4v14H5V3Zm9 1.5V8h3.5L14 4.5ZM8 11h8v2H8v-2Zm0 4h8v2H8v-2Z', image: 'M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z', video: 'M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z', audio: 'M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z', archive: 'M20.54 5.23l-1.39-1.68C18.88 3.21 18.47 3 18 3H6c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6.02 3 6.5V19c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6.5c0-.48-.17-.93-.46-1.27zM6.24 5h11.52l.81.97H5.44l.8-.97zM5 19V8h14v11H5zm8.5-8v-1.5h-3V11H8l4 4 4-4h-2.5z', pdf: 'M20 2H8c-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-8.5 7.5c0 .83-.67 1.5-1.5 1.5H9v2H7.5V7H10c.83 0 1.5.67 1.5 1.5v1zm5 2c0 .83-.67 1.5-1.5 1.5h-2.5V7H15c.83 0 1.5.67 1.5 1.5v3zm4-3H19v1h1.5V11H19v2h-1.5V7h3v1.5zM9 9.5h1v-1H9v1zM5 6H3v14c0 1.1.9 2 2 2h14v-2H5V6zm10 5.5h1v-3h-1v3z', document: 'M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z', spreadsheet: 'M8 2h8l6 6v12c0 1.1-.9 2-2 2H8c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2zm7 1.5V8h4.5L15 3.5zM10 14l-2 4h1.5l.5-1h2l.5 1h1.5l-2-4H10zm.8 2L12 14.3 13.2 16h-2.4z', presentation: 'M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11zm-6-3.5V13h-1.5v4h-1V13H8v-1.5h4V16.5zm1.5 0V13H15c.5 0 .75-.25.75-.5s-.25-.5-.75-.5h-1.5v-1.5H15c1.1 0 2 .9 2 2s-.9 2-2 2h-.5v1.5h-1z', code: 'M22 21H2V3h20v18zM4 5v14h16V5H4zm3 4h10v2H7V9zm0 4h6v2H7v-2z', database: 'M12 2C7.58 2 4 3.79 4 6v12c0 2.21 3.58 4 8 4s8-1.79 8-4V6c0-2.21-3.58-4-8-4zm0 2c3.87 0 6 1.5 6 2s-2.13 2-6 2-6-1.5-6-2 2.13-2 6-2zM4 13.5V10c0 .5 2.13 2 6 2v3.5c-2.14-.39-3.5-1.14-4-1.67z', font: 'M10 4v4h-4v2h4v10h2V10h4V8h-4V4h-2z', config: 'M6 2h9l5 5v15H6V2Zm8 1.5V8h4.5L14 3.5Z', generic: 'M6 2h9l5 5v15H6V2Zm8 1.5V8h4.5L14 3.5Z' }; var EXT_MAP = { md: 'markdown', markdown: 'markdown', jpg: 'image', jpeg: 'image', png: 'image', gif: 'image', webp: 'image', svg: 'image', bmp: 'image', ico: 'image', mp4: 'video', webm: 'video', mkv: 'video', avi: 'video', mov: 'video', mp3: 'audio', wav: 'audio', flac: 'audio', ogg: 'audio', m4a: 'audio', aac: 'audio', zip: 'archive', rar: 'archive', '7z': 'archive', tar: 'archive', gz: 'archive', bz2: 'archive', pdf: 'pdf', doc: 'document', docx: 'document', xls: 'spreadsheet', xlsx: 'spreadsheet', csv: 'spreadsheet', tsv: 'spreadsheet', ppt: 'presentation', pptx: 'presentation', js: 'code', jsx: 'code', mjs: 'code', cjs: 'code', ts: 'code', tsx: 'code', py: 'code', go: 'code', rs: 'code', c: 'code', cpp: 'code', h: 'code', hpp: 'code', css: 'code', scss: 'code', sass: 'code', less: 'code', html: 'code', htm: 'code', php: 'code', java: 'code', swift: 'code', kotlin: 'code', rb: 'code', sh: 'code', bash: 'code', zsh: 'code', json: 'json', yaml: 'json', yml: 'json', toml: 'json', xml: 'code', sql: 'database', db: 'database', sqlite: 'database', env: 'config', ini: 'config', cfg: 'config', conf: 'config', ttf: 'font', otf: 'font', woff: 'font', woff2: 'font' }; function fileIcon(entry) { if (entry.type === 'folder') return svgIcon(FILE_ICONS.folder); var ext = (entry.extension || extension(entry.name)).toLowerCase(); var category = EXT_MAP[ext] || 'generic'; if (category === 'json') return '{ }'; return svgIcon(FILE_ICONS[category] || FILE_ICONS.generic); } function formatSize(bytes) { if (!bytes) return ''; if (bytes < 1024) return bytes + ' B'; if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / 1048576).toFixed(1) + ' MB'; } function formatDate(value) { if (!value) return ''; var date = new Date(value); if (isNaN(date.getTime())) return ''; return date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } function typeLabel(entry) { if (entry.type === 'folder') return 'folder'; return (entry.extension || extension(entry.name) || 'file').toLowerCase(); } function confirmModal(message, options) { options = options || {}; var confirmText = options.confirmText || 'Confirm'; var cancelText = options.cancelText || 'Cancel'; var isDanger = !!options.danger; return new Promise(function (resolve) { var overlay = el('div', { className: 'files-modal-overlay' }); var modal = el('div', { className: 'files-modal' }, [ el('div', { className: 'files-modal-title', textContent: message }), el('div', { className: 'files-modal-actions' }, [ el('button', { className: 'files-modal-btn cancel', textContent: cancelText, onClick: function () { cleanup(); resolve(false); } }), el('button', { className: 'files-modal-btn confirm' + (isDanger ? ' danger' : ''), textContent: confirmText, onClick: function () { cleanup(); resolve(true); } }) ]) ]); overlay.appendChild(modal); document.body.appendChild(overlay); overlay.querySelector('.files-modal-btn').focus(); function onKeydown(e) { if (e.key === 'Escape') { cleanup(); resolve(false); } } document.addEventListener('keydown', onKeydown); function cleanup() { document.removeEventListener('keydown', onKeydown); if (overlay.parentNode) overlay.parentNode.removeChild(overlay); } }); } var FilesView = { mount: function (containerEl, props, api) { injectStyles(); containerEl.innerHTML = ''; containerEl.className = 'files-root'; containerEl.setAttribute('tabindex', '0'); containerEl.setAttribute('data-plugin-id', 'verstak.files'); 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 = ''; var entries = []; var selectedPaths = {}; var lastClickedPath = ''; var filterText = ''; var sortMode = 'folder-name'; var createMode = ''; var renameTarget = null; var disposed = false; var historyStack = ['']; var historyIndex = 0; var navigatingHistory = false; function scopedPath(local) { local = cleanPath(local); return workspaceRoot ? (local ? workspaceRoot + '/' + local : workspaceRoot) : local; } function localPath(full) { full = cleanPath(full); if (!workspaceRoot) return full; if (full === workspaceRoot) return ''; return full.indexOf(workspaceRoot + '/') === 0 ? full.slice(workspaceRoot.length + 1) : full; } 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 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']), el('option', { value: 'name-asc' }, ['Name']), el('option', { value: 'type' }, ['Type']), el('option', { value: 'modified-desc' }, ['Modified']), 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); }); containerEl.appendChild(toolbar); var listContainer = el('div', { className: 'files-list', 'data-files-list': '' }); containerEl.appendChild(listContainer); var createPanel = el('div', { className: 'files-panel', style: { display: 'none' } }); var createInput = el('input', { className: 'files-create-input', 'data-files-create-input': '' }); var createConfirm = el('button', { className: 'files-toolbar-btn', 'data-files-create-confirm': '' }, ['Create']); var createCancel = el('button', { className: 'files-toolbar-btn' }, ['Cancel']); createPanel.appendChild(createInput); createPanel.appendChild(createConfirm); createPanel.appendChild(createCancel); containerEl.appendChild(createPanel); var renamePanel = el('div', { className: 'files-panel', style: { display: 'none' } }); var renameInput = el('input', { className: 'files-rename-input', 'data-files-rename-input': '' }); var renameConfirm = el('button', { className: 'files-toolbar-btn', 'data-files-rename-confirm': '' }, ['Rename']); var renameCancel = el('button', { className: 'files-toolbar-btn' }, ['Cancel']); renamePanel.appendChild(renameInput); renamePanel.appendChild(renameConfirm); renamePanel.appendChild(renameCancel); containerEl.appendChild(renamePanel); function selectedEntry() { var keys = Object.keys(selectedPaths); if (keys.length === 0) return null; return entries.find(function (entry) { return entry.relativePath === keys[0]; }) || null; } function selectedCount() { return Object.keys(selectedPaths).length; } function updateButtons() { var count = selectedCount(); upBtn.disabled = !currentPath; openBtn.disabled = count !== 1; renameBtn.disabled = count !== 1; trashBtn.disabled = count === 0; pasteBtn.disabled = !window.__filesClipboard; } function updateHistoryButtons() { backBtn.disabled = historyIndex <= 0; forwardBtn.disabled = historyIndex >= historyStack.length - 1; } function updateBreadcrumb() { breadcrumb.innerHTML = ''; var root = el('span', { className: currentPath ? 'files-breadcrumb-item' : 'files-breadcrumb-current', onClick: function () { navigateTo(''); } }, [workspaceName]); breadcrumb.appendChild(root); if (!currentPath) { updateButtons(); return; } var parts = currentPath.split('/'); var acc = ''; parts.forEach(function (part, index) { breadcrumb.appendChild(el('span', { className: 'files-breadcrumb-sep' }, ['/'])); acc += (acc ? '/' : '') + part; var cls = index === parts.length - 1 ? 'files-breadcrumb-current' : 'files-breadcrumb-item'; (function (path) { breadcrumb.appendChild(el('span', { className: cls, onClick: function () { if (cls !== 'files-breadcrumb-current') navigateTo(path); } }, [part])); })(acc); }); updateButtons(); } function visibleEntries() { var q = filterText.trim().toLowerCase(); var out = entries.filter(function (entry) { if (entry.isHidden || entry.isReserved || entry.name === '.verstak') return false; return !q || entry.name.toLowerCase().indexOf(q) !== -1; }); out.sort(function (a, b) { if (sortMode === 'folder-name') { if (a.type === 'folder' && b.type !== 'folder') return -1; if (a.type !== 'folder' && b.type === 'folder') return 1; } if (sortMode === 'type') { var typeCmp = typeLabel(a).localeCompare(typeLabel(b)); if (typeCmp) return typeCmp; } if (sortMode === 'modified-desc') { var timeCmp = new Date(b.modifiedAt || 0).getTime() - new Date(a.modifiedAt || 0).getTime(); if (timeCmp) return timeCmp; } if (sortMode === 'size-desc') { var sizeCmp = (b.size || 0) - (a.size || 0); if (sizeCmp) return sizeCmp; } return a.name.localeCompare(b.name); }); return out; } function selectEntry(entry, event) { if (!entry) { selectedPaths = {}; lastClickedPath = ''; renderList(); return; } var targetPath = entry.relativePath; var visible = visibleEntries(); if (event && (event.ctrlKey || event.metaKey)) { if (selectedPaths[targetPath]) { delete selectedPaths[targetPath]; } else { selectedPaths[targetPath] = true; } lastClickedPath = targetPath; } else if (event && event.shiftKey && lastClickedPath) { var lastIdx = -1; var targetIdx = -1; for (var i = 0; i < visible.length; i++) { if (visible[i].relativePath === lastClickedPath) lastIdx = i; if (visible[i].relativePath === targetPath) targetIdx = i; } if (lastIdx !== -1 && targetIdx !== -1) { var lo = Math.min(lastIdx, targetIdx); var hi = Math.max(lastIdx, targetIdx); for (var j = lo; j <= hi; j++) { selectedPaths[visible[j].relativePath] = true; } } } else { selectedPaths = {}; selectedPaths[targetPath] = true; lastClickedPath = targetPath; } renderList(); } function renderList() { listContainer.innerHTML = ''; var header = el('div', { className: 'files-header' }, [ el('span', {}, ['Name']), el('span', {}, ['Type']), el('span', {}, ['Size']), el('span', {}, ['Modified']), el('span', {}, ['Actions']) ]); listContainer.appendChild(header); var shown = visibleEntries(); if (shown.length === 0) { listContainer.appendChild(el('div', { className: 'files-empty' }, [filterText ? 'No matches' : 'Empty folder'])); updateButtons(); return; } shown.forEach(function (entry) { var row = el('div', { className: 'files-item' + (selectedPaths[entry.relativePath] ? ' selected' : ''), 'data-file-name': entry.name, 'data-file-type': entry.type, 'data-file-path': entry.relativePath, draggable: 'true', tabindex: '0', onClick: function (e) { selectEntry(entry, e); }, onDblclick: function () { openEntry(entry); }, onDragstart: function (e) { var paths = []; if (selectedPaths[entry.relativePath] && selectedCount() > 1) { paths = Object.keys(selectedPaths); } else { selectedPaths = {}; selectedPaths[entry.relativePath] = true; lastClickedPath = entry.relativePath; renderList(); paths = [entry.relativePath]; } e.dataTransfer.setData('application/files-paths', JSON.stringify(paths)); e.dataTransfer.effectAllowed = 'move'; row.classList.add('files-dragging'); }, onDragend: function () { row.classList.remove('files-dragging'); } }, [ el('div', { className: 'files-namecell' }, [ el('span', { className: 'files-item-icon', innerHTML: fileIcon(entry) }), el('span', { className: 'files-item-name', textContent: entry.name, title: entry.name }) ]), el('span', { className: 'files-item-meta' }, [typeLabel(entry)]), 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 ]) ]); listContainer.appendChild(row); }); updateButtons(); } function loadEntries() { selectedPaths = {}; lastClickedPath = ''; listContainer.innerHTML = ''; listContainer.appendChild(el('div', { className: 'files-loading' }, ['Loading...'])); updateBreadcrumb(); api.files.list(scopedPath(currentPath)).then(function (result) { if (disposed) return; entries = result || []; renderList(); }).catch(function (err) { if (disposed) return; listContainer.innerHTML = ''; listContainer.appendChild(el('div', { className: 'files-error' }, [ el('div', {}, ['Failed to load files']), el('div', { className: 'files-error-msg' }, [(err && err.message) ? err.message : String(err)]) ])); }); } function navigateTo(path) { var newPath = cleanPath(path); if (!navigatingHistory) { if (historyIndex < historyStack.length - 1) { historyStack = historyStack.slice(0, historyIndex + 1); } if (historyStack[historyStack.length - 1] !== newPath) { historyStack.push(newPath); historyIndex = historyStack.length - 1; } } currentPath = newPath; cancelCreate(); cancelRename(); updateHistoryButtons(); loadEntries(); } function goBack() { if (historyIndex <= 0) return; historyIndex--; navigatingHistory = true; navigateTo(historyStack[historyIndex]); navigatingHistory = false; } function goForward() { if (historyIndex >= historyStack.length - 1) return; historyIndex++; navigatingHistory = true; navigateTo(historyStack[historyIndex]); navigatingHistory = false; } function goUp() { if (currentPath) navigateTo(parentPath(currentPath)); } function openEntry(entry) { if (!entry) return; if (entry.type === 'folder') { navigateTo(localPath(entry.relativePath)); return; } var ext = entry.extension ? '.' + entry.extension : (extension(entry.name) ? '.' + extension(entry.name) : ''); var entryLocalPath = localPath(entry.relativePath); var isMd = ext === '.md' || ext === '.markdown'; var isNotes = entryLocalPath.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' : (mode === 'markdown' ? 'Markdown file name' : 'Text file name'); createPanel.style.display = 'flex'; createInput.focus(); } function cancelCreate() { createMode = ''; createPanel.style.display = 'none'; } function confirmCreate() { var name = createInput.value.trim(); if (!name) return; var mode = createMode; if (createMode === 'markdown' && !/\.(md|markdown)$/i.test(name)) name += '.md'; if (createMode === 'text' && !/\.[^/.]+$/.test(name)) name += '.txt'; var full = scopedPath(currentPath ? currentPath + '/' + name : name); var promise = mode === 'folder' ? api.files.createFolder(full) : api.files.writeText(full, '', { createIfMissing: true, overwrite: false }); promise.then(function () { cancelCreate(); loadEntries(); if (mode !== 'folder') { var ext = extension(name); api.workbench.openResource({ kind: 'vault-file', path: full, mode: 'edit', extension: ext ? '.' + ext : '', context: { sourcePluginId: 'verstak.files', sourceView: 'files' } }).catch(function () {}); } }).catch(function (err) { createInput.value = ''; createInput.placeholder = 'Error: ' + ((err && err.message) ? err.message : String(err)); }); } function beginRename(entry) { entry = entry || selectedEntry(); if (!entry) return; renameTarget = entry; renameInput.value = entry.name; renamePanel.style.display = 'flex'; renameInput.focus(); renameInput.select(); } function cancelRename() { renameTarget = null; renamePanel.style.display = 'none'; } function confirmRename() { if (!renameTarget) return; var newName = renameInput.value.trim(); if (!newName || newName === renameTarget.name) { cancelRename(); return; } var from = renameTarget.relativePath; var targetParent = parentPath(from); var to = targetParent ? targetParent + '/' + newName : newName; api.files.move(from, to, { overwrite: false }).then(function () { cancelRename(); loadEntries(); }).catch(function (err) { renameInput.value = renameTarget.name; renameInput.placeholder = 'Error: ' + ((err && err.message) ? err.message : String(err)); }); } function trashEntry(entry) { var count = selectedCount(); if (entry) { confirmModal('Move "' + entry.name + '" to trash?', { danger: true }).then(function (ok) { if (!ok) return; api.files.trash(entry.relativePath).then(function () { loadEntries(); }).catch(function (err) { window.alert((err && err.message) ? err.message : String(err)); }); }); } else if (count > 1) { confirmModal('Move ' + count + ' items to trash?', { danger: true }).then(function (ok) { if (!ok) return; var paths = Object.keys(selectedPaths); Promise.allSettled(paths.map(function (p) { return api.files.trash(p); })).then(function () { loadEntries(); }); }); } else { var single = selectedEntry(); if (!single) return; trashEntry(single); } } 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); createCancel.addEventListener('click', cancelCreate); renameConfirm.addEventListener('click', confirmRename); renameCancel.addEventListener('click', cancelRename); createInput.addEventListener('keydown', function (event) { if (event.key === 'Enter') confirmCreate(); if (event.key === 'Escape') cancelCreate(); }); renameInput.addEventListener('keydown', function (event) { if (event.key === 'Enter') confirmRename(); if (event.key === 'Escape') cancelRename(); }); /* --- Context menu --- */ var ctxMenu = el('div', { className: 'files-ctx-menu', style: { display: 'none' } }); document.body.appendChild(ctxMenu); var ctxTarget = null; function hideCtxMenu() { ctxMenu.style.display = 'none'; ctxTarget = null; } function ctxItem(label, cls, onClick) { return el('div', { className: 'files-ctx-menu-item' + (cls ? ' ' + cls : ''), onClick: function (e) { e.stopPropagation(); hideCtxMenu(); onClick(); } }, [label]); } function ctxSep() { return el('div', { className: 'files-ctx-menu-sep' }); } function showCtxMenu(x, y, entry) { ctxTarget = entry; ctxMenu.innerHTML = ''; if (entry) { 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); })); ctxMenu.appendChild(ctxItem('Copy', '', function () { copyEntry(entry); })); ctxMenu.appendChild(ctxSep()); ctxMenu.appendChild(ctxItem('Move to Trash', 'danger', function () { trashEntry(entry); })); } 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(ctxSep()); ctxMenu.appendChild(ctxItem('Paste', '', function () { pasteEntry(); })); } } ctxMenu.style.display = 'block'; var mw = ctxMenu.offsetWidth; var mh = ctxMenu.offsetHeight; var vw = window.innerWidth; var vh = window.innerHeight; if (x + mw > vw) x = vw - mw - 4; if (y + mh > vh) y = vh - mh - 4; if (x < 0) x = 0; if (y < 0) y = 0; ctxMenu.style.left = x + 'px'; ctxMenu.style.top = y + 'px'; } function duplicateEntry(entry) { if (!entry) return; console.log('[files] Duplicate:', entry.relativePath); var name = entry.name; var dot = name.lastIndexOf('.'); var base = dot > 0 ? name.slice(0, dot) : name; var ext = dot > 0 ? name.slice(dot) : ''; var from = scopedPath(currentPath ? currentPath + '/' + name : name); var maxAttempts = 100; function tryName(n) { var newName = n === 1 ? base + ' (copy)' + ext : base + ' (copy ' + n + ')' + ext; var to = scopedPath(currentPath ? currentPath + '/' + newName : newName); return api.files.metadata(to).then(function () { if (n >= maxAttempts) { console.error('[files] Duplicate failed: all ' + maxAttempts + ' name variations are taken'); return null; } return tryName(n + 1); }, function () { return api.files.readText(from).then(function (content) { return api.files.writeText(to, content, { createIfMissing: true, overwrite: false }); }); }); } tryName(1).then(function (result) { if (result !== null) loadEntries(); }).catch(function (err) { console.error('[files] Duplicate failed:', err); }); } function cutEntry(entry) { if (!entry) return; if (entry.type === 'folder') { console.log('[files] Cut for folders not yet supported'); return; } console.log('[files] Cut:', entry.relativePath); window.__filesClipboard = { action: 'cut', path: entry.relativePath, name: entry.name }; 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 }; updateButtons(); } function pasteEntry() { var clip = window.__filesClipboard; if (!clip || !clip.path) return; var from = clip.path; 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 () { return api.files.readText(from).then(function (content) { return api.files.writeText(to, content, { createIfMissing: true, overwrite: false }); }).then(function () { if (clip.action === 'cut') { return api.files.trash(from); } }); }); } tryName(1).then(function (result) { if (result !== null) { if (clip.action === 'cut') window.__filesClipboard = null; loadEntries(); console.log('[files] Pasted:', clip.action, from); } }).catch(function (err) { console.error('[files] Paste failed:', err); }); } var onDocClick = function (e) { if (!ctxMenu.contains(e.target)) hideCtxMenu(); }; var onDocKeydown = function (e) { if (e.key === 'Escape') hideCtxMenu(); }; document.addEventListener('click', onDocClick); document.addEventListener('keydown', onDocKeydown); listContainer.addEventListener('contextmenu', function (e) { e.preventDefault(); var row = e.target.closest('.files-item'); var entry = null; if (row) { var fp = row.getAttribute('data-file-path'); entry = entries.find(function (en) { return en.relativePath === fp; }) || null; } showCtxMenu(e.clientX, e.clientY, entry); }); listContainer.addEventListener('click', function (e) { if (!e.target.closest('.files-item')) { selectEntry(null); } }); function moveFiles(sourcePaths, targetDirPath) { var promises = sourcePaths.filter(function (p) { return parentPath(p) !== targetDirPath; }).map(function (p) { var name = baseName(p); var to = targetDirPath ? targetDirPath + '/' + name : name; return api.files.move(p, to, { overwrite: false }); }); if (promises.length === 0) return Promise.resolve(); return Promise.allSettled(promises).then(function () { loadEntries(); }); } listContainer.addEventListener('dragover', function (e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; var row = e.target.closest('.files-item'); if (row) { row.classList.add('files-drag-over'); } else { listContainer.classList.add('files-drag-over'); } }); listContainer.addEventListener('dragleave', function (e) { var row = e.target.closest('.files-item'); if (row) row.classList.remove('files-drag-over'); if (!row) listContainer.classList.remove('files-drag-over'); }); listContainer.addEventListener('drop', function (e) { e.preventDefault(); listContainer.classList.remove('files-drag-over'); var rows = listContainer.querySelectorAll('.files-drag-over'); for (var i = 0; i < rows.length; i++) rows[i].classList.remove('files-drag-over'); var raw = e.dataTransfer.getData('application/files-paths'); if (!raw) return; var sourcePaths; try { sourcePaths = JSON.parse(raw); } catch (err) { return; } 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); } }); containerEl.addEventListener('keydown', function (event) { if (event.target && ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].indexOf(event.target.tagName) !== -1) return; var ITEM_HEIGHT = 38; var PAGE_ITEMS = 10; function findCurrentIndex(visible) { if (lastClickedPath) { for (var i = 0; i < visible.length; i++) { if (visible[i].relativePath === lastClickedPath) return i; } } var selKeys = Object.keys(selectedPaths); if (selKeys.length > 0) { for (var j = 0; j < visible.length; j++) { if (selectedPaths[visible[j].relativePath]) return j; } } return 0; } function clamp(val, lo, hi) { return val < lo ? lo : (val > hi ? hi : val); } function scrollToRow(idx) { var rows = listContainer.querySelectorAll('.files-item'); if (rows[idx]) rows[idx].scrollIntoView({ block: 'nearest' }); } function scrollToItem(idx) { var rows = listContainer.querySelectorAll('.files-item'); if (rows[idx]) { var row = rows[idx]; var offset = row.offsetTop - listContainer.offsetTop; if (row.offsetTop < listContainer.scrollTop) { listContainer.scrollTop = offset; } else if (row.offsetTop + row.offsetHeight > listContainer.scrollTop + listContainer.clientHeight) { listContainer.scrollTop = offset + row.offsetHeight - listContainer.clientHeight; } } } function navigateToIndex(targetIdx, extend) { var visible = visibleEntries(); if (visible.length === 0) return; targetIdx = clamp(targetIdx, 0, visible.length - 1); if (extend) { var anchorIdx = findCurrentIndex(visible); var lo = Math.min(anchorIdx, targetIdx); var hi = Math.max(anchorIdx, targetIdx); for (var k = lo; k <= hi; k++) { selectedPaths[visible[k].relativePath] = true; } } else { selectedPaths = {}; selectedPaths[visible[targetIdx].relativePath] = true; lastClickedPath = visible[targetIdx].relativePath; } renderList(); scrollToRow(targetIdx); } function scrollOnly(idx) { var visible = visibleEntries(); if (visible.length === 0) return; idx = clamp(idx, 0, visible.length - 1); scrollToItem(idx); } var key = event.key; var shift = event.shiftKey; var ctrl = event.ctrlKey || event.metaKey; if (key === 'Enter') { openEntry(selectedEntry()); return; } if (key === 'Delete' || key === 'Backspace') { trashEntry(); return; } if (key === 'F2') { beginRename(); return; } if (key === 'ArrowLeft' && event.altKey) { event.preventDefault(); goBack(); return; } if (key === 'ArrowRight' && event.altKey) { event.preventDefault(); goForward(); return; } if (key === '[' && ctrl) { event.preventDefault(); goBack(); return; } if (key === ']' && ctrl) { event.preventDefault(); goForward(); return; } if (key === 'a' && (ctrl || event.metaKey)) { event.preventDefault(); var vis = visibleEntries(); selectedPaths = {}; vis.forEach(function (entry) { selectedPaths[entry.relativePath] = true; }); lastClickedPath = vis.length > 0 ? vis[vis.length - 1].relativePath : ''; renderList(); return; } if (key === 'ArrowDown' || key === 'ArrowUp' || key === 'Home' || key === 'End' || key === 'PageDown' || key === 'PageUp') { event.preventDefault(); } var visible = visibleEntries(); if (visible.length === 0) return; var curIdx = findCurrentIndex(visible); if (ctrl && (key === 'ArrowDown' || key === 'ArrowUp')) { var scrollIdx = key === 'ArrowDown' ? clamp(curIdx + 1, 0, visible.length - 1) : clamp(curIdx - 1, 0, visible.length - 1); scrollOnly(scrollIdx); return; } if (key === 'ArrowDown') { navigateToIndex(curIdx + 1, shift); } else if (key === 'ArrowUp') { navigateToIndex(curIdx - 1, shift); } else if (key === 'Home') { navigateToIndex(0, shift); } else if (key === 'End') { navigateToIndex(visible.length - 1, shift); } else if (key === 'PageDown') { var pageSize = listContainer.clientHeight ? Math.floor(listContainer.clientHeight / ITEM_HEIGHT) : PAGE_ITEMS; navigateToIndex(curIdx + pageSize, shift); } else if (key === 'PageUp') { var pageSizeUp = listContainer.clientHeight ? Math.floor(listContainer.clientHeight / ITEM_HEIGHT) : PAGE_ITEMS; navigateToIndex(curIdx - pageSizeUp, shift); } }); updateHistoryButtons(); loadEntries(); containerEl.__filesCleanup = function () { disposed = true; document.removeEventListener('click', onDocClick); document.removeEventListener('keydown', onDocKeydown); if (ctxMenu && ctxMenu.parentNode) ctxMenu.parentNode.removeChild(ctxMenu); }; }, unmount: function (containerEl) { if (containerEl.__filesCleanup) { containerEl.__filesCleanup(); containerEl.__filesCleanup = null; } containerEl.innerHTML = ''; } }; window.VerstakPluginRegister('verstak.files', { components: { FilesView: FilesView } }); })();