From 946eb7ab0fe7f3ea96efd0c9f2131b08433046b9 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Sun, 10 May 2026 16:46:13 +0200 Subject: [PATCH] feat(voyage): implement drill-down + back-nav + URL-parameter project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 15 (v4.3 Sesjon 3 — Wave 3) — wires the dashboard fleet-tiles to a drill-down view with breadcrumb update + back-to-dashboard navigation + browser back/forward restoration via popstate. renderArtifactDetail(artifactKey) renders the chosen artifact into the #voyage-detail slot using renderPageShell + renderArtifact: - brief / plan / review → markdown render - progress → JSON pretty-print in
  - research              → list of all research-briefs
  - missing entry         → "Artifact mangler" placeholder

Click delegation on .fleet-tile[data-artifact] triggers detail render
+ pushDetailURL (?artifact=); data-action=back-to-dashboard
returns to the dashboard view + pushDashboardURL. Topbar breadcrumb
gets a third segment for detail views.

URL-parameter deep-linking: at page-load, ?project=
surfaces a guide-panel hint explaining that webkitdirectory requires
a user-gesture; the hint links to the same Last prosjektmappe button
that wireProjectLoader already exposes. popstate handler restores
the view-state on browser back/forward (no-op when no project loaded).

Test additions (4): renderArtifactDetail function, URLSearchParams
presence, data-action=back-to-dashboard attribute, popstate listener.
---
 .../voyage/playground/voyage-playground.html  | 162 ++++++++++++++++++
 .../playground/voyage-playground.test.mjs     |  24 +++
 2 files changed, 186 insertions(+)

diff --git a/plugins/voyage/playground/voyage-playground.html b/plugins/voyage/playground/voyage-playground.html
index 853d43a..6be85c3 100644
--- a/plugins/voyage/playground/voyage-playground.html
+++ b/plugins/voyage/playground/voyage-playground.html
@@ -1422,6 +1422,163 @@ playground first-run shows a complete round-trip-able artifact.
         announce('Dashboard lastet — ' + tiles.length + ' artifacts vist.');
       }
 
+      // ---- v4.3 Step 15 — drill-down + back-nav + URL routing ----------
+      // Click on a fleet-tile drills into renderArtifactDetail; the
+      // back-to-dashboard button (or breadcrumb-click) returns to the
+      // dashboard without state-loss. URL parameter `?project=` is
+      // additive: at page-load we surface a guide-panel hint because
+      // webkitdirectory cannot be triggered without a user-gesture.
+      // popstate handler keeps browser back/forward in sync with view-state.
+
+      function resolveArtifactEntry(a, key) {
+        if (!a) return null;
+        if (key === 'brief')    return a.brief;
+        if (key === 'plan')     return a.plan;
+        if (key === 'review')   return a.review;
+        if (key === 'progress') return a.progress;
+        if (key === 'research') return (a.research && a.research.length) ? a.research[0] : null;
+        return null;
+      }
+
+      function renderArtifactDetail(artifactKey) {
+        var a = __voyageCurrentArtifacts;
+        if (!a) return;
+        var slot = $('voyage-detail');
+        if (!slot) return;
+
+        var entry = resolveArtifactEntry(a, artifactKey);
+        var bodyHtml;
+        if (artifactKey === 'research') {
+          if (a.research && a.research.length) {
+            bodyHtml = '
    ' + a.research.map(function (r) { + var fm = r.frontmatter || {}; + var title = fm.title || r.path; + return '
  • ' + escapeHtml(title) + + '
    ' + escapeHtml(r.path) + '
  • '; + }).join('') + '
'; + } else { + bodyHtml = '

Ingen research-briefs i prosjektet.

'; + } + } else if (!entry) { + bodyHtml = '

Artifact mangler i prosjektmappen.

'; + } else if (artifactKey === 'progress') { + bodyHtml = '
' + escapeHtml(entry.content || '') + '
'; + } else { + bodyHtml = renderArtifact(entry.content || ''); + } + + var titleMap = { brief: 'Brief', plan: 'Plan', review: 'Review', + research: 'Research', progress: 'Progress' }; + var artifactName = titleMap[artifactKey] || artifactKey; + var projectName = shortenBasePath(a.basePath); + + slot.innerHTML = renderPageShell({ + eyebrow: 'Artifact detail', + title: artifactName, + lede: 'Project: ' + projectName, + meta: entry ? ('path: ' + (entry.path || '')) : 'mangler' + }, '' + bodyHtml); + slot.hidden = false; + + // Hide dashboard + paste-flow stages. + var dash = $('voyage-dashboard'); if (dash) dash.hidden = true; + 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; + + renderTopbar([ + { label: 'Voyage', href: '#' }, + { label: projectName, href: '#' }, + { label: artifactName } + ]); + announce('Detail-visning: ' + artifactName); + } + + function showDashboardFromState() { + if (!__voyageCurrentArtifacts) return; + var detail = $('voyage-detail'); if (detail) detail.hidden = true; + renderDashboard(__voyageCurrentArtifacts); + } + + function pushDashboardURL() { + try { + if (!window.history || !window.history.pushState) return; + var a = __voyageCurrentArtifacts; + var params = new URLSearchParams(window.location.search); + if (a && a.basePath) params.set('project', a.basePath); + params.delete('artifact'); + var qs = params.toString(); + var url = window.location.pathname + (qs ? ('?' + qs) : ''); + window.history.pushState({ view: 'dashboard', basePath: a ? a.basePath : null }, '', url); + } catch (_) { /* file:// + privatmodus */ } + } + + function pushDetailURL(artifactKey) { + try { + if (!window.history || !window.history.pushState) return; + var a = __voyageCurrentArtifacts; + var params = new URLSearchParams(window.location.search); + if (a && a.basePath) params.set('project', a.basePath); + params.set('artifact', artifactKey); + var qs = params.toString(); + var url = window.location.pathname + (qs ? ('?' + qs) : ''); + window.history.pushState({ view: 'detail', basePath: a ? a.basePath : null, artifact: artifactKey }, '', url); + } catch (_) { /* file:// + privatmodus */ } + } + + function wireDashboardNavigation() { + document.addEventListener('click', function (e) { + var tile = e.target && e.target.closest && e.target.closest('.fleet-tile[data-artifact]'); + if (tile) { + e.preventDefault(); + var key = tile.getAttribute('data-artifact'); + if (key) { + renderArtifactDetail(key); + pushDetailURL(key); + } + return; + } + var back = e.target && e.target.closest && e.target.closest('[data-action="back-to-dashboard"]'); + if (back) { + e.preventDefault(); + showDashboardFromState(); + pushDashboardURL(); + } + }); + // Browser back/forward → restore view from history state. + window.addEventListener('popstate', function (e) { + var s = e.state; + if (!s || !__voyageCurrentArtifacts) return; + if (s.view === 'detail' && s.artifact) { + renderArtifactDetail(s.artifact); + } else if (s.view === 'dashboard') { + showDashboardFromState(); + } + }); + } + + function maybeShowProjectURLHint() { + // Caveat per Step 15 spec: webkitdirectory cannot be triggered + // programmatically without a user-gesture, so a `?project=` URL + // surfaces a guide-panel hint instead of attempting auto-load. + try { + var qs = new URLSearchParams(window.location.search); + var projectQ = qs.get('project'); + if (!projectQ) return; + var emptyState = $('empty-state'); + if (!emptyState) return; + var titleEl = emptyState.querySelector('.guide-panel__title'); + var bodyEl = emptyState.querySelector('.guide-panel__body'); + if (titleEl) titleEl.textContent = 'Project deep-link oppdaget'; + if (bodyEl) { + bodyEl.innerHTML = '

URL inneholder ?project=' + + escapeHtml(projectQ) + '. Browseren krever et bruker-klikk ' + + 'før prosjektmappen kan leses; bruk knappen «Last prosjektmappe» ' + + 'eller dra mappen til vinduet for å fortsette.

'; + } + } catch (_) { /* file:// + privatmodus */ } + } + // ---- DOM wiring ---------------------------------------------------- function $(id) { return document.getElementById(id); } @@ -2139,6 +2296,11 @@ playground first-run shows a complete round-trip-able artifact. // Step 12 (v4.3) — drag-drop overlay with webkitGetAsEntry recursive // walker + Firefox 150 Windows UA-guard. wireDragDrop(); + + // Step 15 (v4.3) — fleet-tile click delegation, back-to-dashboard + // handler, popstate restoration, and ?project= URL deep-link hint. + wireDashboardNavigation(); + maybeShowProjectURLHint(); } function setThemeLabel(theme) { diff --git a/plugins/voyage/tests/playground/voyage-playground.test.mjs b/plugins/voyage/tests/playground/voyage-playground.test.mjs index 1718b7a..e724ab6 100644 --- a/plugins/voyage/tests/playground/voyage-playground.test.mjs +++ b/plugins/voyage/tests/playground/voyage-playground.test.mjs @@ -176,3 +176,27 @@ test('voyage-playground.html declares dashboard status vocabulary (v4.3 Step 14) assert.match(text, /'missing'/, 'status missing required'); assert.match(text, /'stale'/, 'status stale required'); }); + +test('voyage-playground.html declares renderArtifactDetail JS function (v4.3 Step 15 drill-down)', () => { + const text = readFileSync(HTML, 'utf-8'); + assert.match(text, /function renderArtifactDetail\b/, 'renderArtifactDetail function required for drill-down'); +}); + +test('voyage-playground.html declares URLSearchParams routing (v4.3 Step 15)', () => { + const text = readFileSync(HTML, 'utf-8'); + // Presence-only: URLSearchParams already used at line 810 for project-key + // derivation; Step 15 adds ?project= dashboard/detail routing. + assert.match(text, /URLSearchParams/, 'URLSearchParams required for ?project= routing'); +}); + +test('voyage-playground.html declares data-action="back-to-dashboard" (v4.3 Step 15 back-nav)', () => { + const text = readFileSync(HTML, 'utf-8'); + // Stricter than Step 14 wording — must appear as data-action attribute + // somewhere in the JS template, not only in HTML comments. + assert.match(text, /data-action="back-to-dashboard"/, 'data-action="back-to-dashboard" required for return-nav handler'); +}); + +test('voyage-playground.html declares popstate handler (v4.3 Step 15 back/forward)', () => { + const text = readFileSync(HTML, 'utf-8'); + assert.match(text, /'popstate'/, 'popstate listener required for browser back/forward'); +});