diff --git a/plugins/voyage/playground/voyage-playground.html b/plugins/voyage/playground/voyage-playground.html index 8700a84..b92393d 100644 --- a/plugins/voyage/playground/voyage-playground.html +++ b/plugins/voyage/playground/voyage-playground.html @@ -239,6 +239,174 @@ color: #fff; border-color: var(--color-accent, #4a86e8); } + + /* Step 10 — sidebar + critique-card-list + tabs */ + .voyage-sidebar { + position: fixed; + top: 0; + right: 0; + bottom: 0; + width: 320px; + background: var(--color-bg, #fff); + border-left: 1px solid var(--color-border, #e3e6eb); + box-shadow: -2px 0 8px rgba(0,0,0,0.05); + transform: translateX(0); + transition: transform 200ms ease; + z-index: 900; + display: flex; + flex-direction: column; + } + .voyage-sidebar[aria-hidden="true"] { + transform: translateX(calc(320px - 40px)); /* leave 40px icon-rail */ + } + .voyage-sidebar__rail { + position: absolute; + left: 0; + top: 0; + width: 40px; + bottom: 0; + background: var(--color-bg-soft, #fafbfc); + border-right: 1px solid var(--color-border, #e3e6eb); + display: flex; + align-items: flex-start; + justify-content: center; + padding-top: var(--space-3, 0.75rem); + } + .voyage-sidebar__panel { + flex: 1; + margin-left: 40px; + display: flex; + flex-direction: column; + overflow: hidden; + } + .voyage-sidebar[aria-hidden="true"] .voyage-sidebar__panel { visibility: hidden; } + + /* 2-state FAB toggle (per critical decision #4) */ + .voyage-fab { + position: relative; + width: 32px; + height: 32px; + padding: 0; + border: 1px solid var(--color-border, #e3e6eb); + background: var(--color-bg, #fff); + border-radius: 50%; + cursor: pointer; + font-size: 1rem; + line-height: 30px; + text-align: center; + } + .voyage-fab[aria-expanded="true"]::before { content: "›"; } + .voyage-fab[aria-expanded="false"]::before { content: "‹"; } + .voyage-fab__badge { + position: absolute; + top: -6px; + right: -6px; + min-width: 18px; + height: 18px; + padding: 0 4px; + background: var(--color-accent, #4a86e8); + color: #fff; + border-radius: 9px; + font-size: 0.625rem; + line-height: 18px; + text-align: center; + } + .voyage-fab__badge[hidden] { display: none; } + + /* Tabs */ + [role="tablist"].voyage-tabs { + display: flex; + border-bottom: 1px solid var(--color-border, #e3e6eb); + } + [role="tab"].voyage-tab { + flex: 1; + padding: var(--space-3, 0.75rem); + border: none; + background: transparent; + cursor: pointer; + font: inherit; + font-size: 0.875rem; + color: var(--color-text-muted, #5e6470); + border-bottom: 2px solid transparent; + } + [role="tab"].voyage-tab[aria-selected="true"] { + color: var(--color-text, #1a1d23); + border-bottom-color: var(--color-accent, #4a86e8); + font-weight: 600; + } + + /* findings + critique-card list */ + .voyage-findings { + flex: 1; + overflow: auto; + padding: var(--space-3, 0.75rem); + display: flex; + flex-direction: column; + gap: var(--space-2, 0.5rem); + } + .critique-card { + padding: var(--space-3, 0.75rem); + background: var(--color-bg, #fff); + border: 1px solid var(--color-border, #e3e6eb); + border-radius: var(--radius-sm, 4px); + cursor: pointer; + } + .critique-card:hover { background: var(--color-bg-hover, #f0f2f5); } + .critique-card__header { + display: flex; + align-items: center; + gap: var(--space-2, 0.5rem); + font-size: 0.75rem; + } + .intent-badge { + display: inline-block; + padding: 2px 6px; + border-radius: var(--radius-sm, 4px); + color: #fff; + font-size: 0.625rem; + font-weight: 600; + text-transform: uppercase; + } + .intent-badge--fix { background: #2c8a3e; } + .intent-badge--change { background: #4a86e8; } + .intent-badge--question { background: #d8a23a; } + .intent-badge--block { background: #c54545; } + .critique-card__id { + font-family: var(--font-mono, "JetBrains Mono", monospace); + color: var(--color-text-muted, #5e6470); + } + .critique-card__snippet { + margin: var(--space-2, 0.5rem) 0; + padding: var(--space-2, 0.5rem); + background: var(--color-bg-soft, #fafbfc); + border-left: 2px solid var(--color-border, #e3e6eb); + font-style: italic; + font-size: 0.8125rem; + } + .critique-card__comment { + font-size: 0.875rem; + color: var(--color-text, #1a1d23); + } + .critique-card__status { + margin-top: var(--space-2, 0.5rem); + font-size: 0.75rem; + color: var(--color-text-muted, #5e6470); + } + .critique-card__status--exported { color: var(--color-success, #2c8a3e); } + + .lint-annotation-glow { + animation: voyageGlow 1s ease; + } + @keyframes voyageGlow { + 0% { background-color: transparent; } + 30% { background-color: rgba(74, 134, 232, 0.15); } + 100% { background-color: transparent; } + } + + /* Make room for sidebar in main layout */ + @media (min-width: 1024px) { + main { margin-right: 320px; } + } @@ -287,6 +455,68 @@ + + +
' + + '' + escapeHtmlInline(annot.intent || 'change') + '' + + '' + escapeHtmlInline(annot.id) + '' + + (annot.line ? 'linje ' + annot.line + '' : '') + + '
' + + (annot.snippet ? '
' + escapeHtmlInline(annot.snippet) + '
' : '') + + '
' + escapeHtmlInline(annot.comment || '') + '
' + + '
' + + (annot.exported ? 'Eksportert' : 'Pending') + + '
'; + card.addEventListener('click', function () { + scrollToAnchor(annot); + }); + card.addEventListener('keydown', function (e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + scrollToAnchor(annot); + } + }); + return card; + } + + function scrollToAnchor(annot) { + if (!annot || !annot.line) return; + var viewport = $('voyage-viewport'); + if (!viewport) return; + var ps = viewport.querySelectorAll('p, h1, h2, h3, h4, h5, h6, li, pre'); + var target = ps[annot.line - 1]; + if (!target) return; + target.scrollIntoView({ behavior: 'smooth', block: 'center' }); + target.classList.add('lint-annotation-glow'); + setTimeout(function () { target.classList.remove('lint-annotation-glow'); }, 1000); + } + + function refreshSidebar() { + var ta = $('voyage-paste-input'); + var fm = quickParseFrontmatter(ta ? ta.value : ''); + var key = deriveStorageKey(fm); + var drafts = loadDrafts(key); + + // Sort by line ascending (Hypothes.is sort-by-location pattern) + var sortedDrafts = drafts.slice().sort(function (a, b) { + var la = a.line == null ? Infinity : Number(a.line); + var lb = b.line == null ? Infinity : Number(b.line); + return la - lb; + }); + + var pendingCount = sortedDrafts.filter(function (d) { return !d.exported; }).length; + var historyCount = sortedDrafts.filter(function (d) { return d.exported; }).length; + + var draftsPanel = $('voyage-tab-drafts-panel'); + var historyPanel = $('voyage-tab-history-panel'); + if (draftsPanel) { + draftsPanel.innerHTML = ''; + var pending = sortedDrafts.filter(function (d) { return !d.exported; }); + if (pending.length === 0) { + draftsPanel.innerHTML = '

Ingen drafts enda. Bruk en av annotation-gesturene over.

'; + } else { + for (var i = 0; i < pending.length; i++) draftsPanel.appendChild(renderCritiqueCard(pending[i])); + } + } + if (historyPanel) { + historyPanel.innerHTML = ''; + var hist = sortedDrafts.filter(function (d) { return d.exported; }); + if (hist.length === 0) { + historyPanel.innerHTML = '

Ingen eksporterte revisjoner enda.

'; + } else { + for (var j = 0; j < hist.length; j++) historyPanel.appendChild(renderCritiqueCard(hist[j])); + } + } + + // Update tab counts + var draftsCountEl = $('voyage-tab-drafts-count'); + if (draftsCountEl) draftsCountEl.textContent = '(' + pendingCount + ' drafts)'; + var historyCountEl = $('voyage-tab-history-count'); + if (historyCountEl) historyCountEl.textContent = '(' + historyCount + ' historiske)'; + + // Update FAB badge + var badge = $('voyage-fab-badge'); + if (badge) { + if (pendingCount > 0) { + badge.textContent = String(pendingCount); + badge.hidden = false; + } else { + badge.hidden = true; + badge.textContent = '0'; + } + } + } + + function selectTab(tabId) { + var tabs = document.querySelectorAll('[role="tab"].voyage-tab'); + for (var i = 0; i < tabs.length; i++) { + var selected = tabs[i].id === tabId; + tabs[i].setAttribute('aria-selected', selected ? 'true' : 'false'); + tabs[i].tabIndex = selected ? 0 : -1; + var panelId = tabs[i].getAttribute('aria-controls'); + var panel = panelId ? document.getElementById(panelId) : null; + if (panel) panel.hidden = !selected; + } + } + + function wireSidebar() { + var toggle = $('voyage-sidebar-toggle'); + var sidebar = $('voyage-sidebar'); + if (toggle && sidebar) { + toggle.addEventListener('click', function () { + var hidden = sidebar.getAttribute('aria-hidden') === 'true'; + sidebar.setAttribute('aria-hidden', hidden ? 'false' : 'true'); + toggle.setAttribute('aria-expanded', hidden ? 'true' : 'false'); + }); + } + var draftsTab = $('voyage-tab-drafts'); + var historyTab = $('voyage-tab-history'); + if (draftsTab) draftsTab.addEventListener('click', function () { selectTab('voyage-tab-drafts'); }); + if (historyTab) historyTab.addEventListener('click', function () { selectTab('voyage-tab-history'); }); + // Tab keyboard arrow nav + document.querySelectorAll('[role="tab"].voyage-tab').forEach(function (t) { + t.addEventListener('keydown', function (e) { + if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') { + e.preventDefault(); + var tabs = Array.from(document.querySelectorAll('[role="tab"].voyage-tab')); + var idx = tabs.indexOf(t); + var next = e.key === 'ArrowRight' + ? tabs[(idx + 1) % tabs.length] + : tabs[(idx - 1 + tabs.length) % tabs.length]; + selectTab(next.id); + next.focus(); + } + }); + }); + } + + // Hook saveModalAsAnnotation -> refreshSidebar + var originalSave = saveModalAsAnnotation; + saveModalAsAnnotation = function () { + var result = originalSave(); + refreshSidebar(); + return result; + }; + + // Hook mountRender -> refreshSidebar (already chained in Step 9) + var prevMountRender = mountRender; + mountRender = function (text) { + prevMountRender(text); + refreshSidebar(); + }; + // Event wiring on DOMContentLoaded function init() { var renderBtn = $('voyage-render-btn'); @@ -831,6 +1227,10 @@ playground first-run shows a complete round-trip-able artifact. }); } wireModal(); + + // Step 10 — sidebar wiring + initial render + wireSidebar(); + refreshSidebar(); } if (document.readyState === 'loading') { diff --git a/plugins/voyage/tests/playground/voyage-playground.test.mjs b/plugins/voyage/tests/playground/voyage-playground.test.mjs index ee88670..e36c877 100644 --- a/plugins/voyage/tests/playground/voyage-playground.test.mjs +++ b/plugins/voyage/tests/playground/voyage-playground.test.mjs @@ -112,3 +112,15 @@ test('voyage-playground.html declares 300ms grace constant (Step 9 adder-popup g const text = readFileSync(HTML, 'utf-8'); assert.match(text, /300\s*ms|GRACE_MS\s*=\s*300|ADDER_GRACE_MS/i, '300ms grace period for adder-popup must be present'); }); + +// --- Step 10 — sidebar with tabs + critique-card-list ---------------- + +test('voyage-playground.html includes role="tablist" (Step 10 sidebar tabs A11Y)', () => { + const text = readFileSync(HTML, 'utf-8'); + assert.match(text, /role="tablist"/, 'sidebar must declare role="tablist" for A11Y'); +}); + +test('voyage-playground.html declares tabindex (Step 10 focus management)', () => { + const text = readFileSync(HTML, 'utf-8'); + assert.match(text, /tabindex/i, 'sidebar tabs must use tabindex for keyboard focus management'); +});