From c86f87f707df36bca91455c2d530800b6747b9d2 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 7 Jun 2026 21:08:34 -0400 Subject: [PATCH] Caret spotlight: brighten the family + margin brackets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking into a colored word brightens its whole rhyme family (dark ink on lit chips) and draws a soft rounded bracket in the left gutter spanning the family's lines — rhyme and alliteration each get their own bracket, gated to their toggle. No dimming of the rest; additive. Co-Authored-By: Claude Opus 4.8 (1M context) --- static/index.html | 93 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 83 insertions(+), 10 deletions(-) diff --git a/static/index.html b/static/index.html index 02bf443..4829dd9 100644 --- a/static/index.html +++ b/static/index.html @@ -108,13 +108,24 @@ } .editor-shell.rhythm #highlight, .editor-shell.rhythm #stresslayer, - .editor-shell.rhythm #editor { line-height: 2.35; } + .editor-shell.rhythm #editor { line-height: 3.2; } #highlight { pointer-events: none; color: var(--ink); z-index: 1; } #highlight .anno { color: #6a5f52; } + #gutter { position: absolute; inset: 0; pointer-events: none; z-index: 3; overflow: hidden; } + #gutter .bracket { + position: absolute; left: 7px; width: 7px; + border: 2.5px solid currentColor; border-right: 0; + border-top-left-radius: 7px; border-bottom-left-radius: 7px; + } + #gutter .bracket.allit { left: 15px; width: 5px; border-width: 2px; } + #gutter .tick { + position: absolute; left: 7px; width: 7px; height: 2.5px; border-radius: 999px; + } + #gutter .tick.allit { left: 15px; width: 5px; } #editor::selection { background: rgba(232,129,74,0.22); color: transparent; } #stresslayer { pointer-events: none; @@ -277,6 +288,7 @@
+
const editor = document.getElementById('editor'); const highlight = document.getElementById('highlight'); const stresslayer = document.getElementById('stresslayer'); +const gutter = document.getElementById('gutter'); + const stressToggle = document.getElementById('stressToggle'); stressToggle.checked = false; stressToggle.addEventListener('change', ()=>{ @@ -375,6 +389,7 @@ function debounce(fn, ms){ let t; return (...a)=>{ clearTimeout(t); t=setTimeout let analysis = null; // last server response let backendOk = true; let focusGid = null; // spotlight: the family under the caret +let focusAllit = null; // alliteration group under the caret function caretGid(){ if(!analysis || editor.selectionStart !== editor.selectionEnd) return null; @@ -393,9 +408,22 @@ function caretGid(){ return best ? best.g : null; } +function caretAllit(){ + if(!analysis || !analysis.allit || !allitToggle.checked) return null; + if(editor.selectionStart !== editor.selectionEnd) return null; + const pos = editor.selectionStart; + const before = editor.value.slice(0, pos); + const ln = before.split('\n').length - 1; + const col = pos - (before.lastIndexOf('\n') + 1); + const line = editor.value.split('\n')[ln]; + if(!analysis.lines || analysis.lines[ln] !== line) return null; + const hit = analysis.allit.find(t=>t.l===ln && t.s<=col && colmemberLines.has(+m.dataset.l)); + if(marks.length < 2) return; + const off = editor.scrollTop; + let top = Infinity, bot = -Infinity; + const centers = []; + marks.forEach(m=>{ + const t = m.offsetTop - off, h2 = m.offsetHeight; + top = Math.min(top, t); bot = Math.max(bot, t + h2); + centers.push(t + h2 / 2); + }); + const br = document.createElement('div'); + br.className = 'bracket' + (cls ? ' ' + cls : ''); + br.style.top = (top + 5) + 'px'; + br.style.height = (bot - top - 10) + 'px'; + br.style.color = `color-mix(in srgb, ${color} 32%, transparent)`; + gutter.appendChild(br); + centers.forEach(c=>{ + const tk = document.createElement('div'); + tk.className = 'tick' + (cls ? ' ' + cls : ''); + tk.style.top = (c - 1.25) + 'px'; + tk.style.background = `color-mix(in srgb, ${color} 45%, transparent)`; + gutter.appendChild(tk); + }); +} + +function renderGutter(){ + gutter.innerHTML = ''; + if(!analysis) return; + if(focusGid !== null && rhymeToggle.checked){ + const g = analysis.groups.find(x=>x.id === focusGid); + if(g){ + const ls = new Set(); + analysis.tokens.forEach(t=>{ if(t.g === focusGid) ls.add(t.l); }); + spineFor(ls, `var(--r${g.color % COLORS})`, ''); + } + } + if(focusAllit !== null){ + const ls = new Set(); + analysis.allit.forEach(t=>{ if(t.g === focusAllit) ls.add(t.l); }); + spineFor(ls, `var(--r${focusAllit % COLORS})`, 'allit'); + } +} + + function syncLayerHeights(){ [highlight, stresslayer].forEach(l=>{ let sp = l.querySelector('.lspacer'); @@ -668,11 +743,9 @@ function renderStress(lines){ lines.forEach((line, i)=>{ const fresh = analysis && analysis.lines[i] === line; let raw = []; - if(fresh){ - raw = byLine[i] || []; - }else if(analysis && typeof analysis.lines[i] === 'string'){ - const old = analysis.lines[i]; - let cp = 0; + if(fresh){ raw = byLine[i] || []; } + else if(analysis && typeof analysis.lines[i] === 'string'){ + const old = analysis.lines[i]; let cp = 0; const n = Math.min(old.length, line.length); while(cp < n && old[cp] === line[cp]) cp++; raw = (byLine[i] || []).filter(s=>s.e <= cp); @@ -682,7 +755,7 @@ function renderStress(lines){ spans.forEach(s=>{ if(s.s < pos) return; h2 += esc(line.slice(pos, s.s)); - const dots = [...s.st].map(c=> c === '0' ? '○' : '●').join(''); + const dots = [...s.st].map(c=> c === '0' ? '\u25CB' : '\u25CF').join(''); const col = cmap[i] != null ? ` style="color:var(--r${cmap[i]})"` : ''; h2 += `${esc(line.slice(s.s, s.e))}${dots}`; pos = s.e; @@ -732,7 +805,7 @@ function buildReadout(){ } editor.addEventListener('input', ()=>{ focusGid = caretGid(); render(); analyzeSoon(); }); -editor.addEventListener('scroll', ()=>{ highlight.scrollTop = stresslayer.scrollTop = editor.scrollTop; highlight.scrollLeft = stresslayer.scrollLeft = editor.scrollLeft; }); +editor.addEventListener('scroll', ()=>{ highlight.scrollTop = stresslayer.scrollTop = editor.scrollTop; highlight.scrollLeft = stresslayer.scrollLeft = editor.scrollLeft; renderGutter(); }); editor.addEventListener('keyup', ()=>{ updateSpotlight(); buildReadout(); }); editor.addEventListener('click', ()=>{ updateSpotlight(); buildReadout(); });