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;
|
word-break: break-all;
|
||||||
margin-bottom: var(--space-3);
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -582,6 +624,17 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<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>
|
<label for="voyage-paste-input" class="visually-hidden">Lim inn artifact-innhold (brief.md / plan.md / review.md)</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
|
@ -1228,6 +1281,147 @@ playground first-run shows a complete round-trip-able artifact.
|
||||||
return parts.join('');
|
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 ----------------------------------------------------
|
// ---- DOM wiring ----------------------------------------------------
|
||||||
function $(id) { return document.getElementById(id); }
|
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');
|
const text = readFileSync(HTML, 'utf-8');
|
||||||
assert.match(text, /clipboard\.writeText/, 'navigator.clipboard.writeText path required for command-copy');
|
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