From 11156be20ecb82c84fd380310f8425afe1c81603 Mon Sep 17 00:00:00 2001 From: rockachopa Date: Tue, 7 Jul 2026 13:18:26 +0000 Subject: [PATCH] feat(dashboard): add WYSIWYG-style command palette, whiteboard, markdown widget, collapsible panels, and live widgets --- frontend/index.html | 369 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 297 insertions(+), 72 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index aef6f14..f0d2c81 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,84 +1,309 @@ - - - Stackchain Dashboard - + + +Stackchain Dashboard + -
-
Stackchain Dashboard
+
+
Stackchain Dashboard
+
+
Live
-
-
- -
-

Work

-
Loading…
-
-
Loading…
-
- -
- + } + + function escapeHtml(s) { return String(s || '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"','\'':'''}[c])); } + function escAttr(s) { return escapeHtml(s); } + + function openModal(id) { qs('#' + id).classList.add('open'); } + function closeModal(id) { qs('#' + id).classList.remove('open'); } + + /* Whiteboard */ + function initWhiteboard() { + const canvas = qs('#wb'); + const ctx = canvas.getContext('2d'); + let drawing = false; + function resize() { + const dpr = window.devicePixelRatio || 1; + const r = canvas.getBoundingClientRect(); + canvas.width = r.width * dpr; + canvas.height = r.height * dpr; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.strokeStyle = '#111'; + ctx.lineWidth = 2; + } + resize(); + window.addEventListener('resize', resize); + const p = (e) => { + const r = canvas.getBoundingClientRect(); + return { x: e.clientX - r.left, y: e.clientY - r.top }; + }; + canvas.addEventListener('pointerdown', (e) => { drawing = true; const pos = p(e); ctx.beginPath(); ctx.moveTo(pos.x, pos.y); }); + canvas.addEventListener('pointermove', (e) => { if (!drawing) return; const pos = p(e); ctx.lineTo(pos.x, pos.y); ctx.stroke(); }); + canvas.addEventListener('pointerup', () => { drawing = false; }); + canvas.addEventListener('pointerleave', () => { drawing = false; }); + qs('#wb-clear').addEventListener('click', () => { const r = canvas.getBoundingClientRect(); ctx.clearRect(0, 0, r.width, r.height); }); + qs('#wb-save').addEventListener('click', () => { const a = document.createElement('a'); a.href = canvas.toDataURL(); a.download = 'whiteboard.png'; a.click(); }); + } + initWhiteboard(); + + /* Markdown widget */ + qs('#close-markdown').addEventListener('click', () => closeModal('markdown-modal')); + qs('#md-input').addEventListener('input', () => renderMarkdown()); + function renderMarkdown() { + const raw = qs('#md-input').value || ''; + const html = raw + .replace(/^#{1,6}\s.*$/gm, (m) => { const lvl = m.match(/^(#{1,6})/)[1].length; return '' + m.replace(/^#{1,6}\s/, '') + ''; }) + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/\*(.+?)\*/g, '$1') + .replace(/`([^`]+)`/g, '$1') + .replace(/^-\s?(.+)$/gm, '
  • $1
  • ') + .replace(/(
  • .*<\/li>)/s, '
      $1
    ') + .replace(/\n/g, '
    '); + qs('#md-preview').innerHTML = '
    ' + escapeHtml(raw) + '
    ' + html + '
    '; + } + + /* Command palette / widgets */ + const commands = [ + { name: 'Open whiteboard', run: () => { openModal('whiteboard-modal'); initWhiteboard(); resizeWhiteboard(); } }, + { name: 'Open markdown widget', run: () => { openModal('markdown-modal'); renderMarkdown(); } }, + { name: 'Focus issues', run: () => qs('#issues').scrollIntoView({ behavior: 'smooth', block: 'start' }) }, + { name: 'Focus PRs', run: () => qs('#prs').scrollIntoView({ behavior: 'smooth', block: 'start' }) }, + { name: 'Focus AI', run: () => qs('#right').scrollIntoView({ behavior: 'smooth', block: 'start' }) }, + { name: 'Refresh now', run: load }, + ]; + function resizeWhiteboard(){ const canvas = qs('#wb'); if(!canvas) return; const r = canvas.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; canvas.width = r.width * dpr; canvas.height = r.height * dpr; canvas.getContext('2d').setTransform(dpr, 0, 0, dpr, 0, 0); canvas.getContext('2d').lineCap = 'round'; canvas.getContext('2d').lineJoin = 'round'; } + function renderCommands(filter) { + const el = qs('#cmd-results'); + const term = String(filter || '').toLowerCase(); + const items = commands.filter(c => c.name.toLowerCase().includes(term)); + el.innerHTML = items.map((c, idx) => '
    ' + escapeHtml(c.name) + '
    ').join(''); + el.querySelectorAll('.cmd-item').forEach((item) => { + item.addEventListener('click', () => { commands[Number(item.dataset.idx)].run(); qs('#cmd-palette').classList.remove('open'); qs('#cmd-input').value = ''; }); + }); + } + qs('#open-palette').addEventListener('click', () => { qs('#cmd-palette').classList.add('open'); qs('#cmd-input').focus(); renderCommands(''); }); + qs('#cmd-input').addEventListener('input', (e) => renderCommands(e.target.value)); + document.addEventListener('keydown', (e) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); qs('#cmd-palette').classList.toggle('open'); if (qs('#cmd-palette').classList.contains('open')) { qs('#cmd-input').focus(); renderCommands(''); } } }); + qs('#close-whiteboard').addEventListener('click', () => closeModal('whiteboard-modal')); + + qs('#refresh').addEventListener('click', load); + load(); + setInterval(load, 8000); + + /* Live widgets */ + const widgetRegistry = { + markdown: () => ({ title: 'Markdown', html: '
    Launches a tiny markdown scratchpad.
    ' }), + countdown: () => ({ title: 'Live clock', html: '
    ' }), + repoAtAGlance: () => ({ title: 'Repo mix', html: '
    Loading…
    ' }), + }; + const widgetOrder = ['countdown', 'repoAtAGlance']; + const widgetsEl = qs('#widgets'); + function paintWidgets() { + widgetsEl.innerHTML = widgetOrder.map(k => { + const w = widgetRegistry[k](); + return '

    ' + escapeHtml(w.title) + '

    ' + w.html + '
    '; + }).join(''); + qs('#widget-markdown-launch')?.addEventListener('click', () => { openModal('markdown-modal'); renderMarkdown(); }); + } + function tickWidgets() { + const clock = qs('#widget-clock'); + if (clock) clock.textContent = fmt(new Date()); + } + paintWidgets(); + setInterval(tickWidgets, 1000); + + window.repoAtAGlance = async function() { + const el = qs('#repo-mix'); + if (!el) return; + try { + const res = await fetch('/api/v1/context', { headers: { Accept: 'application/json' } }); + const data = await res.json(); + const repos = data.repos || []; + const issues = data.issues || []; + const prs = data.pull_requests || []; + el.innerHTML = '
    Repos
    ' + repos.length + '
    ' + + '
    Open issues
    ' + issues.filter(i => i.state === 'open').length + '
    ' + + '
    Open PRs
    ' + prs.length + '
    '; + } catch (e) { + el.innerHTML = '
    Widget unavailable.
    '; + } + }; + setInterval(window.repoAtAGlance, 8000); + window.repoAtAGlance(); +})(); +