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 = ''; + } 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'); +});