tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

commit 180995da1577e851a1269b73a3945e0478455d14
parent 54822e93e8ae3d7740ae0a2dbf0941ca678ee458
Author: James Eversole <james@eversole.co>
Date:   Mon, 19 Oct 2026 21:16:27 -0500

Align definition controls and prevent adjacent button overlap

Override the global button minimum height, use compact 20px controls, and center measured button boxes on declaration text. Reveal offscreen controls before measuring their height.

Add browser geometry regressions for alignment, scrolling and non-overlap at 320, 390 and 1440px. Validation: nix flake check, generated JS parity, site build and Chromium IDE suite.

Diffstat:
Mext/demo-site/dist/definition-inspector.js | 7++++++-
Mext/demo-site/src/definition-inspector.ts | 7++++++-
Mext/demo-site/style.css | 2+-
Mext/demo-site/test/syntax-probe.mjs | 26++++++++++++++++++++++++++
4 files changed, 39 insertions(+), 3 deletions(-)

diff --git a/ext/demo-site/dist/definition-inspector.js b/ext/demo-site/dist/definition-inspector.js @@ -60,8 +60,13 @@ export function definitionInspector(input, request) { const rect = wrapper.getBoundingClientRect(); for (const [name, button] of buttons) { const span = declarations[declarationIndices.get(name) ?? -1]; - const top = span ? span.getBoundingClientRect().top - rect.top : -100; button.textContent = forcedColors.matches ? `▶ ${name}` : '▶'; + // Offscreen controls have [hidden], so reveal before measuring height. + button.hidden = false; + // Inline spans report the glyph box, not the line box. Center the + // actual control on the first text fragment (also handles wrapped names). + const text = span?.getClientRects()[0]; + const top = text ? text.top + text.height / 2 - rect.top - button.offsetHeight / 2 : -100; button.style.top = `${top}px`; button.hidden = !forcedColors.matches && (top < 0 || top + 20 > input.clientHeight); } diff --git a/ext/demo-site/src/definition-inspector.ts b/ext/demo-site/src/definition-inspector.ts @@ -41,8 +41,13 @@ export function definitionInspector(input: HTMLTextAreaElement, request: (id: nu const rect = wrapper.getBoundingClientRect(); for (const [name, button] of buttons) { const span = declarations[declarationIndices.get(name) ?? -1]; - const top = span ? span.getBoundingClientRect().top - rect.top : -100; button.textContent = forcedColors.matches ? `▶ ${name}` : '▶'; + // Offscreen controls have [hidden], so reveal before measuring height. + button.hidden = false; + // Inline spans report the glyph box, not the line box. Center the + // actual control on the first text fragment (also handles wrapped names). + const text = span?.getClientRects()[0]; + const top = text ? text.top + text.height / 2 - rect.top - button.offsetHeight / 2 : -100; button.style.top = `${top}px`; button.hidden = !forcedColors.matches && (top < 0 || top + 20 > input.clientHeight); } if (selected && !card.hidden) { diff --git a/ext/demo-site/style.css b/ext/demo-site/style.css @@ -63,7 +63,7 @@ header nav{align-items:center;gap:12px}#theme-separator{color:var(--muted)} /* Definition controls live beside the source, not over its editable text. */ #source-editor.syntax-input{padding-left:48px} .definition-gutter{position:absolute;inset:0 auto 0 0;width:38px;overflow:hidden;pointer-events:none;z-index:2} -.definition-gutter button{position:absolute;left:5px;width:28px;height:24px;padding:0;border:0;background:transparent;color:#bfe2ba;font-size:13px;pointer-events:auto} +.definition-gutter button{position:absolute;left:7px;width:24px;height:20px;min-height:0;display:grid;place-items:center;line-height:1;padding:0;border:0;background:transparent;color:#bfe2ba;font-size:13px;pointer-events:auto} .definition-gutter button:hover,.definition-gutter button[aria-expanded="true"]{background:#385045;color:#fff} .definition-card{position:absolute;left:40px;right:12px;z-index:3;max-height:260px;overflow:auto;margin:0;padding:12px;border:1px solid #587362;border-radius:8px;background:#21352b;color:#e3edde;min-width:0} .definition-card-heading{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.definition-card-heading strong{flex:1;overflow-wrap:anywhere} diff --git a/ext/demo-site/test/syntax-probe.mjs b/ext/demo-site/test/syntax-probe.mjs @@ -7,6 +7,32 @@ export async function syntaxProbe(){ const sourceMirror=source.closest('.syntax-editor').querySelector('pre'); await new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r))); assert(Math.abs(sourceMirror.scrollHeight-source.scrollHeight)<=2,'Source mirror wrapping matches native editor: '+JSON.stringify({viewport:innerWidth,input:source.scrollHeight,mirror:sourceMirror.scrollHeight,width:source.clientWidth,mirrorWidth:sourceMirror.style.width,lineHeight:getComputedStyle(source).lineHeight})); + // Real geometry catches inherited global button sizing and glyph/line-box + // differences. The IDE harness runs this at 320, 390 and 1440 pixels. + const checkGutter=()=>{ + const definitions=[...sourceMirror.querySelectorAll('.syntax-definition')]; + let visible=0;const controls=[]; + for(const button of source.closest('.syntax-editor').querySelectorAll('.definition-gutter button')){ + if(button.hidden)continue; + const name=button.getAttribute('aria-label').replace('Inspect compiled definition ',''); + const text=definitions.find(span=>span.textContent===name)?.getClientRects()[0]; + assert(text,'Gutter control has declaration text: '+name); + const control=button.getBoundingClientRect();controls.push(control); + assert(control.height<=parseFloat(getComputedStyle(source).lineHeight)-2,'Definition control leaves space between adjacent lines'); + assert(Math.abs(control.top+control.height/2-text.top-text.height/2)<=1,'Definition control centered on '+name+' at '+innerWidth+'px: '+JSON.stringify({controlTop:control.top,controlHeight:control.height,textTop:text.top,textHeight:text.height})); + visible++; + } + controls.sort((a,b)=>a.top-b.top); + for(let i=1;i<controls.length;i++)assert(controls[i].top>=controls[i-1].bottom+2,'Sequential definition controls do not overlap'); + assert(visible>0,'Visible definition controls tested'); + }; + const oldScroll=source.scrollTop; + const firstDefinition=sourceMirror.querySelector('.syntax-definition').getClientRects()[0]; + source.scrollTop=Math.max(0,oldScroll+firstDefinition.top-source.getBoundingClientRect().top-48);source.dispatchEvent(new Event('scroll')); + checkGutter(); + source.scrollTop=Math.min(source.scrollTop+96,source.scrollHeight-source.clientHeight);source.dispatchEvent(new Event('scroll')); + checkGutter(); + source.scrollTop=oldScroll;source.dispatchEvent(new Event('scroll')); assert(document.querySelector('#source .syntax-definition'),'Read-only source gets the same tokens'); assert(wrapper.querySelector('[aria-hidden=true]')&&input.tagName==='TEXTAREA','Decorative mirror leaves native accessible textarea intact'); const saved=input.value;