|
|
|
|
@ -14,9 +14,6 @@ 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; } }
|
|
|
|
|
@ -71,8 +68,7 @@ textarea { resize: vertical; min-height: 120px; }
|
|
|
|
|
|
|
|
|
|
<main>
|
|
|
|
|
<aside class="sidebar">
|
|
|
|
|
<details class="panel stack" data-panel-key="context" open>
|
|
|
|
|
<summary><h2>Context & view</h2></summary>
|
|
|
|
|
<div class="panel stack">
|
|
|
|
|
<div>
|
|
|
|
|
<h2>Context</h2>
|
|
|
|
|
<div class="muted" id="context">Loading…</div>
|
|
|
|
|
@ -81,41 +77,41 @@ textarea { resize: vertical; min-height: 120px; }
|
|
|
|
|
<h2>View</h2>
|
|
|
|
|
<div class="muted" id="view-hint">—</div>
|
|
|
|
|
</div>
|
|
|
|
|
</details>
|
|
|
|
|
<details class="panel stack" data-panel-key="ai-stream" open>
|
|
|
|
|
<summary><h2>AI Stream</h2></summary>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="panel stack">
|
|
|
|
|
<h2>AI Stream</h2>
|
|
|
|
|
<div id="ai" class="muted">Loading…</div>
|
|
|
|
|
</details>
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
<section class="stack" id="work">
|
|
|
|
|
<details class="panel" id="issues" data-panel-key="issues" open>
|
|
|
|
|
<summary><h2>Issues</h2></summary>
|
|
|
|
|
<div id="issues-content" class="muted">Loading…</div>
|
|
|
|
|
</details>
|
|
|
|
|
<details class="panel" id="prs" data-panel-key="pull-requests" open>
|
|
|
|
|
<summary><h2>Pull Requests</h2></summary>
|
|
|
|
|
<div id="prs-content" class="muted">Loading…</div>
|
|
|
|
|
</details>
|
|
|
|
|
<details class="panel" data-panel-key="widgets" open>
|
|
|
|
|
<summary><h2>Live Widgets</h2></summary>
|
|
|
|
|
<div class="panel" id="issues">
|
|
|
|
|
<h2>Issues</h2>
|
|
|
|
|
<div class="muted">Loading…</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="panel" id="prs">
|
|
|
|
|
<h2>Pull Requests</h2>
|
|
|
|
|
<div class="muted">Loading…</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="panel">
|
|
|
|
|
<h2>Live Widgets</h2>
|
|
|
|
|
<div id="widgets" class="stack"></div>
|
|
|
|
|
</details>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<aside class="sidebar">
|
|
|
|
|
<details class="panel stack" data-panel-key="layout" open>
|
|
|
|
|
<summary><h2>Layout</h2></summary>
|
|
|
|
|
<div class="panel stack">
|
|
|
|
|
<h2>Layout</h2>
|
|
|
|
|
<div class="muted" id="layout-hint">—</div>
|
|
|
|
|
</details>
|
|
|
|
|
<details class="panel stack" data-panel-key="markdown" open>
|
|
|
|
|
<summary><h2>Markdown</h2></summary>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="panel stack">
|
|
|
|
|
<h2>Markdown</h2>
|
|
|
|
|
<textarea id="md-input" placeholder="# Write markdown here...
|
|
|
|
|
- bullet
|
|
|
|
|
- **bold**
|
|
|
|
|
- `code`"></textarea>
|
|
|
|
|
<div id="md-preview" style="margin-top:8px;"></div>
|
|
|
|
|
</details>
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
@ -147,27 +143,6 @@ 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'; }
|
|
|
|
|
@ -187,14 +162,14 @@ textarea { resize: vertical; min-height: 120px; }
|
|
|
|
|
'<div class="label">PRs</div><div class="value">' + (data.pull_requests?.length || 0) + '</div></div>';
|
|
|
|
|
qs('#view-hint').textContent = 'Active view: ' + (data.view || 'dashboard');
|
|
|
|
|
|
|
|
|
|
const issuesBox = qs('#issues-content');
|
|
|
|
|
const issuesBox = qs('#issues');
|
|
|
|
|
const openIssues = (data.issues || []).filter(i => i.state === 'open').slice(0, 12);
|
|
|
|
|
issuesBox.innerHTML = (openIssues.length ? openIssues.map(i => '<div style="margin:6px 0;"><a href="' + escAttr(i.url) + '" target="_blank">#' + i.number + ' ' + escapeHtml(i.title) + '</a>' +
|
|
|
|
|
issuesBox.innerHTML = '<h2>Issues</h2>' + (openIssues.length ? openIssues.map(i => '<div style="margin:6px 0;"><a href="' + escAttr(i.url) + '" target="_blank">#' + i.number + ' ' + escapeHtml(i.title) + '</a>' +
|
|
|
|
|
'<div class="muted">' + (i.labels || []).map(l => '<span class="pill">' + escapeHtml(String(l)) + '</span>').join(' ') + '</div></div>').join('') : '<div class="muted">No open issues.</div>');
|
|
|
|
|
|
|
|
|
|
const prsBox = qs('#prs-content');
|
|
|
|
|
const prsBox = qs('#prs');
|
|
|
|
|
const openPrs = (data.pull_requests || []).slice(0, 12);
|
|
|
|
|
prsBox.innerHTML = (openPrs.length ? openPrs.map(p => '<div style="margin:6px 0;"><a href="' + escAttr(p.url) + '" target="_blank">#' + p.number + ' ' + escapeHtml(p.title) + '</a>' +
|
|
|
|
|
prsBox.innerHTML = '<h2>Pull Requests</h2>' + (openPrs.length ? openPrs.map(p => '<div style="margin:6px 0;"><a href="' + escAttr(p.url) + '" target="_blank">#' + p.number + ' ' + escapeHtml(p.title) + '</a>' +
|
|
|
|
|
'<div class="muted">' + escapeHtml(p.state) + ' by ' + escapeHtml(String(p.user || '')) + '</div></div>').join('') : '<div class="muted">No PRs.</div>');
|
|
|
|
|
|
|
|
|
|
paintDeltas(data.deltas || []);
|
|
|
|
|
@ -211,11 +186,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-content').innerHTML = mockIssues.map(i => '<div style="margin:6px 0;"><a href="' + escAttr(i.url) + '">#' + i.number + ' ' + escapeHtml(i.title) + '</a><div class="muted">' + (i.labels||[]).map(l=>'<span class="pill">'+escapeHtml(String(l))+'</span>').join(' ') + '</div></div>').join('');
|
|
|
|
|
qs('#issues').innerHTML = '<h2>Issues</h2>' + mockIssues.map(i => '<div style="margin:6px 0;"><a href="' + escAttr(i.url) + '">#' + i.number + ' ' + escapeHtml(i.title) + '</a><div class="muted">' + (i.labels||[]).map(l=>'<span class="pill">'+escapeHtml(String(l))+'</span>').join(' ') + '</div></div>').join('');
|
|
|
|
|
const mockPrs = [
|
|
|
|
|
{ number: 4, title: 'Agent UI delta binding', state: 'open', user: 'turdbot', url: '#4' },
|
|
|
|
|
];
|
|
|
|
|
qs('#prs-content').innerHTML = mockPrs.map(p => '<div style="margin:6px 0;"><a href="' + escAttr(p.url) + '">#' + p.number + ' ' + escapeHtml(p.title) + '</a><div class="muted">' + escapeHtml(p.state) + ' by ' + escapeHtml(String(p.user||'')) + '</div></div>').join('');
|
|
|
|
|
qs('#prs').innerHTML = '<h2>Pull Requests</h2>' + mockPrs.map(p => '<div style="margin:6px 0;"><a href="' + escAttr(p.url) + '">#' + p.number + ' ' + escapeHtml(p.title) + '</a><div class="muted">' + escapeHtml(p.state) + ' by ' + escapeHtml(String(p.user||'')) + '</div></div>').join('');
|
|
|
|
|
paintDeltas([
|
|
|
|
|
{panel:'triage', action:'open', target:'#1', priority:'high'},
|
|
|
|
|
{panel:'review', action:'review', target:'#4', priority:'medium'},
|
|
|
|
|
|