diff --git a/frontend/index.html b/frontend/index.html index b57c92e..cae60a2 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -14,6 +14,9 @@ header { position: sticky; top: 0; z-index: 20; padding: 12px 16px; display:flex button { background: linear-gradient(180deg,#1f3a5f,#15324d); border:1px solid #2a496e; color:#e5e7eb; padding:8px 12px; border-radius:10px; cursor:pointer; } button:hover { filter: brightness(1.15); } .panel { border: 1px solid #1b2d45; border-radius: 14px; padding: 12px; background: rgba(11,21,38,.92); } +.panel > summary { cursor: pointer; list-style-position: inside; } +.panel > summary h2 { display: inline-block; margin-left: 4px; } +.panel[open] > summary { margin-bottom: 8px; } aside, section { position: relative; z-index: 1; } main { position: relative; z-index: 1; display: grid; grid-template-columns: 300px 1fr 340px; gap: 14px; padding: 14px; } @media (max-width: 1100px) { main { grid-template-columns: 1fr; } } @@ -68,7 +71,8 @@ textarea { resize: vertical; min-height: 120px; }
-
-

Issues

-
Loading…
-
-
-

Pull Requests

-
Loading…
-
-
-

Live Widgets

+
+

Issues

+
Loading…
+
+
+

Pull Requests

+
Loading…
+
+
+

Live Widgets

-
+
@@ -143,6 +147,27 @@ textarea { resize: vertical; min-height: 120px; } (function(){ const qs = (s, el=document) => el.querySelector(s); const fmt = (d) => new Date(d).toLocaleString(); + const PANEL_STATE_KEY = "stackchain.panel-state.v1"; + const panels = Array.from(document.querySelectorAll('details[data-panel-key]')); + let savedState = {}; + try { + savedState = JSON.parse(localStorage.getItem(PANEL_STATE_KEY) || '{}'); + } catch (e) { + savedState = {}; + } + panels.forEach((panel) => { + if (Object.prototype.hasOwnProperty.call(savedState, panel.dataset.panelKey)) { + panel.open = savedState[panel.dataset.panelKey]; + } + panel.addEventListener('toggle', () => { + const state = Object.fromEntries(panels.map((item) => [item.dataset.panelKey, item.open])); + try { + localStorage.setItem(PANEL_STATE_KEY, JSON.stringify(state)); + } catch (e) { + console.warn('Could not persist panel state', e); + } + }); + }); let liveMode = true; function setStatus(msg) { qs('#status').textContent = msg || 'Live'; } @@ -162,14 +187,14 @@ textarea { resize: vertical; min-height: 120px; } '
PRs
' + (data.pull_requests?.length || 0) + '
'; qs('#view-hint').textContent = 'Active view: ' + (data.view || 'dashboard'); - const issuesBox = qs('#issues'); + const issuesBox = qs('#issues-content'); const openIssues = (data.issues || []).filter(i => i.state === 'open').slice(0, 12); - issuesBox.innerHTML = '

Issues

' + (openIssues.length ? openIssues.map(i => '
#' + i.number + ' ' + escapeHtml(i.title) + '' + + issuesBox.innerHTML = (openIssues.length ? openIssues.map(i => '
#' + i.number + ' ' + escapeHtml(i.title) + '' + '
' + (i.labels || []).map(l => '' + escapeHtml(String(l)) + '').join(' ') + '
').join('') : '
No open issues.
'); - const prsBox = qs('#prs'); + const prsBox = qs('#prs-content'); const openPrs = (data.pull_requests || []).slice(0, 12); - prsBox.innerHTML = '

Pull Requests

' + (openPrs.length ? openPrs.map(p => '
#' + p.number + ' ' + escapeHtml(p.title) + '' + + prsBox.innerHTML = (openPrs.length ? openPrs.map(p => '
#' + p.number + ' ' + escapeHtml(p.title) + '' + '
' + escapeHtml(p.state) + ' by ' + escapeHtml(String(p.user || '')) + '
').join('') : '
No PRs.
'); paintDeltas(data.deltas || []); @@ -186,11 +211,11 @@ textarea { resize: vertical; min-height: 120px; } { number: 8, title: 'Live UI implementation', state: 'open', labels: ['frontend'], url: '#8' }, { number: 37, title: 'Docker build and push', state: 'open', labels: ['devops'], url: '#37' }, ]; - qs('#issues').innerHTML = '

Issues

' + mockIssues.map(i => '
#' + i.number + ' ' + escapeHtml(i.title) + '
' + (i.labels||[]).map(l=>''+escapeHtml(String(l))+'').join(' ') + '
').join(''); + qs('#issues-content').innerHTML = mockIssues.map(i => '
#' + i.number + ' ' + escapeHtml(i.title) + '
' + (i.labels||[]).map(l=>''+escapeHtml(String(l))+'').join(' ') + '
').join(''); const mockPrs = [ { number: 4, title: 'Agent UI delta binding', state: 'open', user: 'turdbot', url: '#4' }, ]; - qs('#prs').innerHTML = '

Pull Requests

' + mockPrs.map(p => '
#' + p.number + ' ' + escapeHtml(p.title) + '
' + escapeHtml(p.state) + ' by ' + escapeHtml(String(p.user||'')) + '
').join(''); + qs('#prs-content').innerHTML = mockPrs.map(p => '
#' + p.number + ' ' + escapeHtml(p.title) + '
' + escapeHtml(p.state) + ' by ' + escapeHtml(String(p.user||'')) + '
').join(''); paintDeltas([ {panel:'triage', action:'open', target:'#1', priority:'high'}, {panel:'review', action:'review', target:'#4', priority:'medium'}, diff --git a/tests/test_panel_persistence.py b/tests/test_panel_persistence.py new file mode 100644 index 0000000..cde63a6 --- /dev/null +++ b/tests/test_panel_persistence.py @@ -0,0 +1,22 @@ +from pathlib import Path + + +DASHBOARD = Path("frontend/index.html") + + +def test_dashboard_panels_are_independently_collapsible(): + html = DASHBOARD.read_text() + + for panel_key in ("context", "ai-stream", "issues", "pull-requests", "widgets", "layout", "markdown"): + assert f'