fix: slash menu at caret position, bubble below, Alt+I Quick Insert
CI / lint (push) Successful in 15s
CI / security (push) Successful in 8s
CI / test (push) Successful in 25s
CI / build (push) Successful in 2s

Slash menu now positioned below current line at the / character using monospace caret-to-pixel calc. Bubble menu now below textarea (was above). Quick Insert menu now opens below cursor line via Alt+I shortcut and click button. Status bar shows Alt+I hint.
This commit is contained in:
2026-06-05 22:56:38 -04:00
parent dcf82e7603
commit bc47f79378
+48 -9
View File
@@ -285,7 +285,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
<!-- Status bar -->
<div class="stat">
<div class="stat-left">
<span>Type <strong>/</strong> for commands</span>
<span>Type <strong>/</strong> for commands | <strong>Alt+I</strong> Insert</span>
<span>Ctrl+J AI panel</span>
<span>Ctrl+S save</span>
<span class="stat-ai" id="stat-ai">&#10024; AI processing...</span>
@@ -609,6 +609,32 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
});
})();
// ---- Caret position to pixel (for monospace textarea) ----
function caretToPixel(pos) {
if (pos == null) pos = getPos().s;
var v = val();
var before = v.substring(0, pos);
var lineNum = before.split('\n').length - 1;
var colNum = pos - v.lastIndexOf('\n', pos - 1) - 1;
if (colNum < 0) colNum = 0;
var style = getComputedStyle(ta);
var fontSize = parseFloat(style.fontSize);
var lineH = parseFloat(style.lineHeight);
if (isNaN(lineH)) lineH = fontSize * 1.75;
var charW = fontSize * 0.615;
var wrapRect = editorWrap.getBoundingClientRect();
var taRect = ta.getBoundingClientRect();
// Position relative to editorWrap
var x = taRect.left - wrapRect.left + 2 + colNum * charW;
var y = taRect.top - wrapRect.top + (lineNum + 1) * lineH - ta.scrollTop;
// Clamp
if (x < 4) x = 4;
if (x > wrapRect.width - 310) x = wrapRect.width - 310;
if (y > wrapRect.height - 360) y = wrapRect.height - 360;
if (y < 20) y = 20;
return { x: x, y: y };
}
// ---- Slash menu ----
function initSlashItems() { slashItems = Array.from(slashMenu.querySelectorAll('.slash-item')); }
@@ -631,9 +657,11 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
slashMenu.classList.add('on'); slashVisible = true; slashIdx = -1;
slashSearch.value = ''; slashSearch.focus();
filterSlash('');
// Position
var wr = editorWrap.getBoundingClientRect();
slashMenu.style.left = '40px'; slashMenu.style.top = '80px';
// Position below current line at slash character
var pos = getPos().s - 1; // position of the / character
var pt = caretToPixel(pos);
slashMenu.style.left = pt.x + 'px';
slashMenu.style.top = pt.y + 'px';
}
function hideSlash() { slashMenu.classList.remove('on'); slashVisible = false; resetSlash(); }
@@ -675,9 +703,9 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
function showBubble() {
if (ta.selectionStart === ta.selectionEnd) { hideBubble(); return; }
var tr = ta.getBoundingClientRect();
var top = tr.top - 44;
var top = tr.bottom + 6;
var left = tr.left + (tr.width / 2) - (bubble.offsetWidth / 2);
if (top < 56) top = tr.bottom + 6;
if (top + 36 > window.innerHeight) top = tr.top - 44;
if (left < 6) left = 6;
if (left + bubble.offsetWidth > window.innerWidth - 6) left = window.innerWidth - bubble.offsetWidth - 6;
bubble.style.left = left + 'px'; bubble.style.top = top + 'px';
@@ -706,10 +734,19 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
}
// ---- Quick Insert ----
function showQI() {
qiMenu.classList.remove('on');
// Position below current line, left-aligned
var pt = caretToPixel(null);
pt.x = Math.max(pt.x - 10, 4);
pt.y += 2;
qiMenu.style.left = pt.x + 'px';
qiMenu.style.top = pt.y + 'px';
qiMenu.classList.add('on');
}
function toggleQI() {
var r = document.getElementById('qi-btn').getBoundingClientRect();
qiMenu.style.left = (r.right + 6) + 'px'; qiMenu.style.top = r.top + 'px';
qiMenu.classList.toggle('on');
if (qiMenu.classList.contains('on')) { qiMenu.classList.remove('on'); return; }
showQI();
}
// ---- AI Panel ----
@@ -792,6 +829,8 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
if (e.ctrlKey && e.key === 's') { e.preventDefault(); forceSave(); }
if (e.ctrlKey && e.key === 'b') { e.preventDefault(); wrapSelection('**'); }
if (e.ctrlKey && e.key === 'i') { e.preventDefault(); wrapSelection('*'); }
// Quick Insert: Alt+I
if (e.altKey && e.key === 'i') { e.preventDefault(); showQI(); }
});
ta.addEventListener('keyup', function(e) {