feat(voyage): implement dashboard via fleet-grid + fleet-tile with status vocabulary
Step 14 (v4.3 Sesjon 3 — Wave 3) — adds renderDashboard pipeline that turns a ProjectArtifacts struct (produced by loadProjectDirectory in Step 13) into a fleet-grid of fleet-tiles, one per artifact-type (brief / plan / review / research / progress). Status vocabulary: complete, in-progress, blocked, missing, stale Severity mapping: missing → critical, blocked → high, in-progress + stale → medium, complete → low. Severity drives DS color tokens via [data-severity] attribute selectors. When loadProjectDirectory completes, dashboard takes over the main stage (paste-flow elements hidden); topbar updates with project breadcrumb. Step 13's pipeline already calls renderDashboard via graceful-fallback, so wiring is automatic. Test additions (4): fleet-grid + fleet-tile presence, renderDashboard function declaration, status vocabulary completeness.
This commit is contained in:
parent
68842cf773
commit
a479f47b4e
2 changed files with 221 additions and 0 deletions
|
|
@ -535,6 +535,48 @@
|
|||
word-break: break-all;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
/* v4.3 Step 14 — dashboard fleet-grid stage. Tiles inherit DS
|
||||
fleet-tile typography from components-tier3-supplement.css; severity
|
||||
badge inherits DS color-severity-* tokens. data-severity drives
|
||||
border + badge color via attribute selectors below. */
|
||||
#voyage-dashboard[hidden],
|
||||
#voyage-detail[hidden] { display: none; }
|
||||
.voyage-dashboard__page,
|
||||
.voyage-detail__page { padding: 0; }
|
||||
.fleet-tile[data-severity="critical"] { border-left: 3px solid var(--color-severity-critical); }
|
||||
.fleet-tile[data-severity="high"] { border-left: 3px solid var(--color-severity-high); }
|
||||
.fleet-tile[data-severity="medium"] { border-left: 3px solid var(--color-severity-medium); }
|
||||
.fleet-tile[data-severity="low"] { border-left: 3px solid var(--color-state-success); }
|
||||
.fleet-tile__status-badge {
|
||||
display: inline-block;
|
||||
padding: 2px var(--space-2);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
color: #fff;
|
||||
}
|
||||
.fleet-tile__status-badge[data-severity="critical"] { background: var(--color-severity-critical); }
|
||||
.fleet-tile__status-badge[data-severity="high"] { background: var(--color-severity-high); }
|
||||
.fleet-tile__status-badge[data-severity="medium"] { background: var(--color-severity-medium); color: #1a1a1a; }
|
||||
.fleet-tile__status-badge[data-severity="low"] { background: var(--color-state-success); }
|
||||
.fleet-tile__stat {
|
||||
margin-top: var(--space-2);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.voyage-back-btn {
|
||||
margin-bottom: var(--space-4);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
.voyage-back-btn:hover { background: var(--color-bg-soft); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -582,6 +624,17 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- v4.3 Step 14 — Project dashboard mount slot. Hidden until
|
||||
loadProjectDirectory completes; renderDashboard fills with a
|
||||
fleet-grid of fleet-tiles (one per artifact: brief / plan /
|
||||
review / research / progress) plus status vocabulary badges. -->
|
||||
<section id="voyage-dashboard" class="voyage-dashboard__page" aria-label="Project dashboard" hidden></section>
|
||||
|
||||
<!-- v4.3 Step 15 — Artifact-detail mount slot. Hidden until a
|
||||
fleet-tile is clicked; back-to-dashboard returns to dashboard
|
||||
without state-loss. -->
|
||||
<section id="voyage-detail" class="voyage-detail__page" aria-label="Artifact detail" hidden></section>
|
||||
|
||||
<section class="paste-import-row" aria-label="Import artifact for annotation">
|
||||
<label for="voyage-paste-input" class="visually-hidden">Lim inn artifact-innhold (brief.md / plan.md / review.md)</label>
|
||||
<textarea
|
||||
|
|
@ -1228,6 +1281,147 @@ playground first-run shows a complete round-trip-able artifact.
|
|||
return parts.join('');
|
||||
}
|
||||
|
||||
// ---- v4.3 Step 14 — renderDashboard --------------------------------
|
||||
// Build a fleet-grid from a ProjectArtifacts struct (produced by
|
||||
// loadProjectDirectory in Step 13). Each artifact becomes a
|
||||
// fleet-tile with status vocabulary + severity mapping:
|
||||
// complete → low (green)
|
||||
// in-progress → medium (amber)
|
||||
// stale → medium
|
||||
// blocked → high (orange)
|
||||
// missing → critical (red)
|
||||
// Clicking a tile triggers drill-down (Step 15).
|
||||
var __voyageCurrentArtifacts = null;
|
||||
|
||||
function mapStatusToSeverity(status) {
|
||||
if (status === 'missing') return 'critical';
|
||||
if (status === 'blocked') return 'high';
|
||||
if (status === 'in-progress' || status === 'stale') return 'medium';
|
||||
return 'low';
|
||||
}
|
||||
|
||||
function deriveArtifactStatus(entry, key) {
|
||||
if (!entry) return 'missing';
|
||||
var fm = entry.frontmatter || {};
|
||||
if (key === 'brief') {
|
||||
if (fm.brief_quality === 'partial') return 'in-progress';
|
||||
return 'complete';
|
||||
}
|
||||
if (key === 'plan') {
|
||||
if (fm.status === 'partial' || fm.status === 'in-progress') return 'in-progress';
|
||||
return 'complete';
|
||||
}
|
||||
if (key === 'review') {
|
||||
if (fm.verdict === 'BLOCK' || fm.verdict === 'REPLAN') return 'blocked';
|
||||
return 'complete';
|
||||
}
|
||||
if (key === 'progress') {
|
||||
var status = '';
|
||||
try { status = JSON.parse(entry.content || '{}').status || ''; } catch (_) { status = ''; }
|
||||
if (status === 'in-progress') return 'in-progress';
|
||||
if (status === 'failed' || status === 'stopped') return 'blocked';
|
||||
if (status === 'partial') return 'stale';
|
||||
if (status === 'completed') return 'complete';
|
||||
return 'in-progress';
|
||||
}
|
||||
return 'complete';
|
||||
}
|
||||
|
||||
function buildArtifactKeyStat(entry, key, researchCount) {
|
||||
if (key === 'research') {
|
||||
return researchCount + ' research-brief' + (researchCount === 1 ? '' : 's');
|
||||
}
|
||||
if (!entry) return '—';
|
||||
var fm = entry.frontmatter || {};
|
||||
if (key === 'brief') return 'Quality: ' + (fm.brief_quality || 'complete');
|
||||
if (key === 'plan') return 'Profile: ' + (fm.profile || '—');
|
||||
if (key === 'review') return 'Verdict: ' + (fm.verdict || '—');
|
||||
if (key === 'progress') {
|
||||
var s = '';
|
||||
try { s = JSON.parse(entry.content || '{}').status || ''; } catch (_) { s = ''; }
|
||||
return 'Status: ' + (s || '—');
|
||||
}
|
||||
return 'OK';
|
||||
}
|
||||
|
||||
function buildArtifactTiles(a) {
|
||||
var tiles = [];
|
||||
var defs = [
|
||||
{ key: 'brief', title: 'Brief', entry: a.brief },
|
||||
{ key: 'plan', title: 'Plan', entry: a.plan },
|
||||
{ key: 'review', title: 'Review', entry: a.review },
|
||||
{ key: 'research', title: 'Research', entry: (a.research && a.research.length) ? a.research[0] : null },
|
||||
{ key: 'progress', title: 'Progress', entry: a.progress }
|
||||
];
|
||||
for (var i = 0; i < defs.length; i++) {
|
||||
var d = defs[i];
|
||||
var status;
|
||||
if (d.key === 'research') {
|
||||
status = (a.research && a.research.length) ? 'complete' : 'missing';
|
||||
} else {
|
||||
status = deriveArtifactStatus(d.entry, d.key);
|
||||
}
|
||||
tiles.push({
|
||||
key: d.key,
|
||||
title: d.title,
|
||||
status: status,
|
||||
severity: mapStatusToSeverity(status),
|
||||
keyStat: buildArtifactKeyStat(d.entry, d.key, a.research ? a.research.length : 0)
|
||||
});
|
||||
}
|
||||
return tiles;
|
||||
}
|
||||
|
||||
function shortenBasePath(p) {
|
||||
if (!p) return '(unnamed)';
|
||||
var parts = String(p).split('/');
|
||||
return parts[parts.length - 1] || p;
|
||||
}
|
||||
|
||||
function renderDashboard(projectArtifacts, slot) {
|
||||
var host = slot || $('voyage-dashboard');
|
||||
if (!host) return;
|
||||
__voyageCurrentArtifacts = projectArtifacts;
|
||||
|
||||
var tiles = buildArtifactTiles(projectArtifacts);
|
||||
var tilesHtml = tiles.map(function (t) {
|
||||
return '<a class="fleet-tile" data-artifact="' + t.key +
|
||||
'" data-status="' + t.status + '" data-severity="' + t.severity +
|
||||
'" href="#" tabindex="0" role="link" aria-label="' +
|
||||
escapeHtml(t.title + ': ' + t.status) + '">' +
|
||||
'<div class="fleet-tile__row">' +
|
||||
'<span class="fleet-tile__name">' + escapeHtml(t.title) + '</span>' +
|
||||
'<span class="fleet-tile__status-badge" data-severity="' + t.severity + '">' +
|
||||
escapeHtml(t.status) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="fleet-tile__stat">' + escapeHtml(t.keyStat) + '</div>' +
|
||||
'</a>';
|
||||
}).join('');
|
||||
|
||||
var projectName = shortenBasePath(projectArtifacts.basePath);
|
||||
var bodyHtml = '<div class="fleet-grid">' + tilesHtml + '</div>';
|
||||
host.innerHTML = renderPageShell({
|
||||
eyebrow: 'Project dashboard',
|
||||
title: projectName,
|
||||
lede: tiles.length + ' artifacts oppdaget i prosjektmappen.',
|
||||
meta: 'Storage-key: ' + (projectArtifacts.storageKey || '—')
|
||||
}, bodyHtml);
|
||||
host.hidden = false;
|
||||
|
||||
// Hide paste-flow stage; dashboard takes over.
|
||||
var emptyState = $('empty-state'); if (emptyState) emptyState.hidden = true;
|
||||
var pasteRow = document.querySelector('.paste-import-row'); if (pasteRow) pasteRow.hidden = true;
|
||||
var layout = document.querySelector('.voyage-layout'); if (layout) layout.hidden = true;
|
||||
var detail = $('voyage-detail'); if (detail) detail.hidden = true;
|
||||
|
||||
// Update topbar with project breadcrumb.
|
||||
renderTopbar([
|
||||
{ label: 'Voyage', href: '#' },
|
||||
{ label: projectName }
|
||||
]);
|
||||
announce('Dashboard lastet — ' + tiles.length + ' artifacts vist.');
|
||||
}
|
||||
|
||||
// ---- DOM wiring ----------------------------------------------------
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
|
|
|
|||
|
|
@ -149,3 +149,30 @@ test('voyage-playground.html uses clipboard.writeText for copy flow (Step 11 exp
|
|||
const text = readFileSync(HTML, 'utf-8');
|
||||
assert.match(text, /clipboard\.writeText/, 'navigator.clipboard.writeText path required for command-copy');
|
||||
});
|
||||
|
||||
// --- v4.3 Sesjon 3 — Step 14 (dashboard) + Step 15 (drill-down + URL routing) ----
|
||||
|
||||
test('voyage-playground.html declares fleet-grid container (v4.3 Step 14 dashboard)', () => {
|
||||
const text = readFileSync(HTML, 'utf-8');
|
||||
assert.match(text, /fleet-grid/, 'fleet-grid container required for dashboard layout');
|
||||
});
|
||||
|
||||
test('voyage-playground.html declares fleet-tile (v4.3 Step 14 dashboard)', () => {
|
||||
const text = readFileSync(HTML, 'utf-8');
|
||||
assert.match(text, /fleet-tile/, 'fleet-tile required for per-artifact dashboard cell');
|
||||
});
|
||||
|
||||
test('voyage-playground.html declares renderDashboard JS function (v4.3 Step 14)', () => {
|
||||
const text = readFileSync(HTML, 'utf-8');
|
||||
assert.match(text, /function renderDashboard\b/, 'renderDashboard function required');
|
||||
});
|
||||
|
||||
test('voyage-playground.html declares dashboard status vocabulary (v4.3 Step 14)', () => {
|
||||
const text = readFileSync(HTML, 'utf-8');
|
||||
// Status vocabulary per plan: complete, in-progress, blocked, missing, stale
|
||||
assert.match(text, /'complete'/, 'status complete required');
|
||||
assert.match(text, /'in-progress'/, 'status in-progress required');
|
||||
assert.match(text, /'blocked'/, 'status blocked required');
|
||||
assert.match(text, /'missing'/, 'status missing required');
|
||||
assert.match(text, /'stale'/, 'status stale required');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue