mirror of
https://github.com/kennethreitz/rhymepad.org.git
synced 2026-06-11 17:08:33 +00:00
Caret spotlight: brighten the family + margin brackets
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) <noreply@anthropic.com>
This commit is contained in:
+83
-10
@@ -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 @@
|
||||
<div class="drafts-bar" id="draftsBar"></div>
|
||||
<div class="editor-shell">
|
||||
<div id="stresslayer"></div>
|
||||
<div id="gutter"></div>
|
||||
<div id="highlight"></div>
|
||||
<textarea id="editor" spellcheck="false" placeholder="Start writing. Leave a blank line between stanzas.
|
||||
Rhymes are detected phonetically — same color = same sound.
|
||||
@@ -354,6 +366,8 @@ Double-click any word to look it up on the right."></textarea>
|
||||
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 && col<t.e);
|
||||
return hit ? hit.g : null;
|
||||
}
|
||||
|
||||
function updateSpotlight(){
|
||||
const g = caretGid();
|
||||
if(g !== focusGid){ focusGid = g; render(); }
|
||||
const g = caretGid(), a = caretAllit();
|
||||
if(g !== focusGid || a !== focusAllit){ focusGid = g; focusAllit = a; render(); }
|
||||
}
|
||||
let analyzeSeq = 0; // guards against out-of-order responses
|
||||
|
||||
@@ -620,11 +648,58 @@ function render(){
|
||||
highlight.innerHTML = html;
|
||||
renderStress(lines);
|
||||
syncLayerHeights();
|
||||
renderGutter();
|
||||
highlight.scrollTop = editor.scrollTop;
|
||||
highlight.scrollLeft = editor.scrollLeft;
|
||||
buildReadout();
|
||||
}
|
||||
|
||||
function spineFor(memberLines, color, cls){
|
||||
const marks = [...highlight.querySelectorAll('.lmark')]
|
||||
.filter(m=>memberLines.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 += `<span class="sw">${esc(line.slice(s.s, s.e))}<span class="sd"${col}>${dots}</span></span>`;
|
||||
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(); });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user