From 063c29a8350f77958da87c2562ff7e0aefefdd9e Mon Sep 17 00:00:00 2001 From: root Date: Mon, 1 Jun 2026 03:56:30 +1000 Subject: [PATCH] feat(ui): drag-drop capture onto the main panel Drops into #main POST /api/capture/upload one file at a time, with space_id pre-filled from localStorage.last_space_id (set whenever the space view renders). Co-Authored-By: Claude Opus 4.7 --- public/app.js | 2 ++ public/components/dropzone.js | 42 +++++++++++++++++++++++++++++++++++ public/views/space.js | 1 + 3 files changed, 45 insertions(+) create mode 100644 public/components/dropzone.js diff --git a/public/app.js b/public/app.js index 8eb0cde..96227f3 100644 --- a/public/app.js +++ b/public/app.js @@ -9,6 +9,7 @@ import { renderTopbar } from './components/topbar.js'; import { renderRightrail } from './components/rightrail.js'; import { emit } from './state.js'; import { el, mount } from './dom.js'; +import { attachDropzone } from './components/dropzone.js'; const VIEWS = { home: () => import('./views/home.js'), @@ -56,6 +57,7 @@ async function init() { renderTopbar(document.getElementById('topbar')); renderSidebar(document.getElementById('sidebar')); renderRightrail(document.getElementById('rightrail')); + attachDropzone(document.getElementById('main')); route(renderView); pollPending(); setInterval(pollPending, 15000); diff --git a/public/components/dropzone.js b/public/components/dropzone.js new file mode 100644 index 0000000..fef03e5 --- /dev/null +++ b/public/components/dropzone.js @@ -0,0 +1,42 @@ +// Drag-drop wrapper for /api/capture/upload. Pre-fills space_id from +// localStorage.last_space_id (set when the space view renders). + +export function attachDropzone(target) { + function highlight(on) { + target.style.outline = on ? '2px dashed var(--accent)' : ''; + target.style.outlineOffset = on ? '-6px' : ''; + } + let counter = 0; // dragenter/leave on children would otherwise toggle + target.addEventListener('dragenter', e => { e.preventDefault(); counter++; if (counter === 1) highlight(true); }); + target.addEventListener('dragleave', () => { counter = Math.max(0, counter - 1); if (counter === 0) highlight(false); }); + target.addEventListener('dragover', e => { e.preventDefault(); }); + target.addEventListener('drop', async e => { + e.preventDefault(); + counter = 0; + highlight(false); + const files = [...(e.dataTransfer?.files || [])]; + if (!files.length) return; + const space_id = localStorage.getItem('last_space_id'); + if (!space_id) { + alert('Open a space first so we know where to drop these.'); + return; + } + const token = localStorage.getItem('void_token') || ''; + let ok = 0, fail = 0; + for (const f of files) { + const fd = new FormData(); + fd.append('file', f); + fd.append('space_id', space_id); + try { + const res = await fetch('/api/capture/upload', { + method: 'POST', + headers: { Authorization: 'Bearer ' + token }, + body: fd + }); + if (res.ok) ok++; else fail++; + } catch { fail++; } + } + const msg = `${ok} file${ok === 1 ? '' : 's'} queued` + (fail ? ` (${fail} failed)` : ''); + console.log(msg); + }); +} diff --git a/public/views/space.js b/public/views/space.js index cd0313c..757333a 100644 --- a/public/views/space.js +++ b/public/views/space.js @@ -33,6 +33,7 @@ export async function render(main, ctx) { el('p', { class: 'view-sub muted' }, 'Loading …') ); + localStorage.setItem('last_space_id', id); let space; try { space = await api.get('/api/spaces/' + id); } catch (e) {