feat(voyage): replace pencil-icon with numbered-badge gutter + yellow-tint highlight
Step 18 of v4.3 playground plan. Replaces v4.2 Gesture 2 pencil-icon hover-reveal with numbered circular badges in the left gutter (one per anchored paragraph; ordering matches sidebar jumplist). 2-3px accent stripe extends right from the badge into the gutter. Yellow-tint highlight (rgba 255, 235, 59, 0.25 — Google Docs pattern) applies to the anchored paragraph when an annotation is active. Body text never reflowed or recolored. Gesture 1 (text-select adder) and Gesture 3 (page-level note) remain for new annotation creation. CSS uses --color-scope-voyage token for badge background and stripe. JS adds injectAnchorBadges() + setActiveAnchor() and rewires mountRender. Trace: SC1 + SC6, research/04 Insight 3 + Recommendation marker-design.
This commit is contained in:
parent
75130fe979
commit
84f41014f9
2 changed files with 120 additions and 45 deletions
|
|
@ -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 =
|
||||
'<svg viewBox="0 0 24 24" aria-hidden="true">' +
|
||||
'<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1.003 1.003 0 0 0 0-1.42l-2.34-2.34a1.003 1.003 0 0 0-1.42 0l-1.83 1.83 3.75 3.75 1.84-1.82z"/>' +
|
||||
'</svg>';
|
||||
(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();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue