diff --git a/plugins/voyage/playground/voyage-playground.html b/plugins/voyage/playground/voyage-playground.html index 4242d61..b8f7101 100644 --- a/plugins/voyage/playground/voyage-playground.html +++ b/plugins/voyage/playground/voyage-playground.html @@ -331,31 +331,56 @@ } .voyage-adder-popup[hidden] { display: none; } - /* Gesture 2 — paragraph-anchored always-visible-icon (hover-reveal) */ - .voyage-viewport p { + /* v4.3 Step 18 — numbered-badge gutter + yellow-tint highlight. + Replaces v4.2's pencil-icon hover-reveal (Gesture 2). Badge appears + only on already-anchored paragraphs; ordering numbers match sidebar + jumplist. Body-text never reflowed or recolored — only the gutter + element + tinting. Gesture 1 (text-select adder popup) and Gesture 3 + (page-level note button) remain in place for new annotation creation. */ + .voyage-viewport p, + .voyage-viewport li, + .voyage-viewport h1, + .voyage-viewport h2, + .voyage-viewport h3, + .voyage-viewport h4, + .voyage-viewport h5, + .voyage-viewport h6 { position: relative; } - .voyage-pencil-btn { + .voyage-anchor-badge { position: absolute; - left: -32px; - top: 0; - width: 24px; - height: 24px; + left: -2.5rem; + top: 0.15rem; + width: 1.5rem; + height: 1.5rem; padding: 0; border: none; - background: transparent; - opacity: 0; + border-radius: 50%; + background: var(--color-scope-voyage); + color: #fff; + font-size: var(--font-size-xs, 0.75rem); + font-weight: 600; + line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; cursor: pointer; - transition: opacity 150ms ease; + /* 2-3px accent stripe extending right from the badge into the gutter */ + box-shadow: 0.25rem 0 0 0 var(--color-scope-voyage); + transition: transform 150ms ease; } - .voyage-viewport p:hover .voyage-pencil-btn, - .voyage-pencil-btn:focus-visible { - opacity: 1; + .voyage-anchor-badge:hover { + transform: scale(1.1); } - .voyage-pencil-btn svg { - width: 16px; - height: 16px; - fill: var(--color-text-tertiary); + .voyage-anchor-badge:focus-visible { + outline: 2px solid var(--color-focus-ring, #4d90fe); + outline-offset: 2px; + } + /* Yellow-tint highlight on the anchored span when annotation is active. + Google Docs pattern; rgba value taken from research/04 Dim 4. */ + .voyage-anchor-active { + background: rgba(255, 235, 59, 0.25); + transition: background 150ms ease; } /* Gesture 3 — page-level note button (placeholder; sidebar is Step 10) */ @@ -1946,35 +1971,61 @@ playground first-run shows a complete round-trip-able artifact. openModal({ target: target, line: line, snippet: snippet, storageKey: key }); } - // ---- Gesture 2 — paragraph-anchored pencil icon (hover-reveal) ----- - function injectPencilIcons() { + // ---- v4.3 Step 18 — numbered-badge gutter (replaces pencil-icon) --- + // Iterate active drafts (loaded from storage) and inject a numbered + // circular badge into the gutter of each anchored paragraph. Ordering + // matches the export-flow numbering 1, 2, 3 ... so badges align with + // the Step 19 sidebar jumplist. Body text is never recolored; only + // the gutter element + (when active) yellow-tint highlight on the + // annotated paragraph itself. + function injectAnchorBadges() { var viewport = $('voyage-viewport'); if (!viewport) return; - var paras = viewport.querySelectorAll('p, li'); - for (var i = 0; i < paras.length; i++) { - var p = paras[i]; - if (p.querySelector('.voyage-pencil-btn')) continue; - var btn = document.createElement('button'); - btn.type = 'button'; - btn.className = 'voyage-pencil-btn'; - btn.setAttribute('aria-label', 'Annotér dette avsnittet'); - btn.innerHTML = - ''; - (function (el) { - btn.addEventListener('click', function () { - var ta = $('voyage-paste-input'); - var fm = quickParseFrontmatter(ta ? ta.value : ''); - var key = deriveStorageKey(fm); - var target = deriveHeadingPath(el); - var line = deriveLineNumber(el); - openModal({ target: target, line: line, snippet: '', storageKey: key }); + var ta = $('voyage-paste-input'); + var fm = quickParseFrontmatter(ta ? ta.value : ''); + var key = deriveStorageKey(fm); + var drafts = loadDrafts(key); + if (!Array.isArray(drafts) || drafts.length === 0) return; + var paras = viewport.querySelectorAll('p, li, h1, h2, h3, h4, h5, h6, pre'); + // Sort drafts by line ASC for stable 1..N numbering + var sorted = drafts.slice().sort(function (a, b) { + return (Number(a.line) || 0) - (Number(b.line) || 0); + }); + for (var i = 0; i < sorted.length; i++) { + var d = sorted[i]; + var ln = Number(d.line); + if (!ln || ln < 1 || ln > paras.length) continue; + var p = paras[ln - 1]; + if (!p) continue; + if (p.querySelector('.voyage-anchor-badge')) continue; + var badge = document.createElement('button'); + badge.type = 'button'; + badge.className = 'voyage-anchor-badge'; + badge.setAttribute('data-anchor-id', d.id); + badge.setAttribute('aria-label', 'Annotering ' + (i + 1) + ': ' + (d.target_anchor || 'page')); + badge.textContent = String(i + 1); + (function (el, draftId) { + badge.addEventListener('click', function () { + setActiveAnchor(draftId); }); - })(p); - // Insert at start so it floats in left margin via CSS - p.style.position = 'relative'; - p.insertBefore(btn, p.firstChild); + })(p, d.id); + p.insertBefore(badge, p.firstChild); + } + } + + // Apply yellow-tint highlight to the anchored paragraph for the given + // anchor id; remove the class from any previously-active paragraph. + function setActiveAnchor(anchorId) { + var viewport = $('voyage-viewport'); + if (!viewport) return; + var prev = viewport.querySelectorAll('.voyage-anchor-active'); + for (var i = 0; i < prev.length; i++) prev[i].classList.remove('voyage-anchor-active'); + if (!anchorId) return; + var badge = viewport.querySelector('.voyage-anchor-badge[data-anchor-id="' + anchorId + '"]'); + if (badge && badge.parentElement) { + badge.parentElement.classList.add('voyage-anchor-active'); + badge.parentElement.scrollIntoView({ block: 'center', behavior: 'smooth' }); + badge.focus(); } } @@ -1998,11 +2049,11 @@ playground first-run shows a complete round-trip-able artifact. return btn; } - // Re-run gesture-2 + gesture-3 wiring after each render. + // Re-run anchor-badge (Step 18) + gesture-3 wiring after each render. var originalMountRender = mountRender; mountRender = function (text) { originalMountRender(text); - injectPencilIcons(); + injectAnchorBadges(); ensurePageNoteButton(); }; diff --git a/plugins/voyage/tests/playground/voyage-playground.test.mjs b/plugins/voyage/tests/playground/voyage-playground.test.mjs index c754868..1b0d323 100644 --- a/plugins/voyage/tests/playground/voyage-playground.test.mjs +++ b/plugins/voyage/tests/playground/voyage-playground.test.mjs @@ -226,3 +226,27 @@ test('voyage-playground.html declares relocateAnchorsToBlockBoundaries pure func assert.match(text, /function\s+relocateAnchorsToBlockBoundaries\s*\(\s*text\s*,\s*anchors\s*\)/, 'relocateAnchorsToBlockBoundaries(text, anchors) pure function required'); }); + +test('voyage-playground.html declares .voyage-anchor-badge gutter component (v4.3 Step 18)', () => { + const text = readFileSync(HTML, 'utf-8'); + assert.match(text, /\.voyage-anchor-badge\s*\{/, '.voyage-anchor-badge CSS class required'); + assert.match(text, /position:\s*absolute/, '.voyage-anchor-badge must use absolute positioning'); + assert.match(text, /var\(--color-scope-voyage\)/, 'badge must use --color-scope-voyage token'); +}); + +test('voyage-playground.html declares .voyage-anchor-active yellow-tint highlight (v4.3 Step 18)', () => { + const text = readFileSync(HTML, 'utf-8'); + assert.match(text, /\.voyage-anchor-active\s*\{/, '.voyage-anchor-active CSS class required'); + assert.match(text, /rgba\(255,\s*235,\s*59,\s*0\.25\)/, 'yellow-tint rgba(255, 235, 59, 0.25) required'); +}); + +test('voyage-playground.html does NOT contain v4.2 pencil-icon references (v4.3 Step 18 cleanup)', () => { + const text = readFileSync(HTML, 'utf-8'); + assert.doesNotMatch(text, /voyage-pencil-btn/, 'pencil-btn class must be removed'); + assert.doesNotMatch(text, /injectPencilIcons/, 'injectPencilIcons function must be replaced by injectAnchorBadges'); +}); + +test('voyage-playground.html declares injectAnchorBadges JS function (v4.3 Step 18)', () => { + const text = readFileSync(HTML, 'utf-8'); + assert.match(text, /function\s+injectAnchorBadges\s*\(\s*\)/, 'injectAnchorBadges() function required'); +});