feat: auto-height, sync scroll, Mermaid QI, table prompt, bubble follow
CI / lint (push) Successful in 15s
CI / security (push) Successful in 10s
CI / build (push) Has been cancelled
CI / test (push) Has been cancelled

Textarea overflow:hidden + auto-height via scrollHeight to eliminate double scrollbar. Sync scroll between editor and preview panes (bidirectional percentage). Alt+I menu now includes 7 Mermaid diagram templates (flowchart, sequence, class, state, ER, gantt, pie). Table now prompts for cols/rows via prompt(). Bubble menu follows cursor via document selectionchange event.
This commit is contained in:
2026-06-05 23:33:37 -04:00
parent bbe109e4e5
commit 218c77dafb
+73 -6
View File
@@ -52,7 +52,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
/* Textarea pane */
.editor-wrap { flex: 1; overflow-y: auto; position: relative; padding: 24px 16px 160px 48px; }
.textarea { width: 100%; min-height: 100%; resize: none; background: transparent; border: none; outline: none; color: var(--text); font-family: var(--mono); font-size: 13.5px; line-height: 1.75; tab-size: 2; }
.textarea { width: 100%; height: auto; min-height: 100%; resize: none; overflow: hidden; background: transparent; border: none; outline: none; color: var(--text); font-family: var(--mono); font-size: 13.5px; line-height: 1.75; tab-size: 2; }
.textarea::placeholder { color: var(--text3); }
/* Quick insert + */
@@ -130,7 +130,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
.bubble-more-item:hover { background: var(--bg-hover); color: var(--text); }
/* Quick Insert menu */
.qi-menu { position: absolute; z-index: 190; width: 170px; background: var(--bg4); border: 1px solid var(--border2); border-radius: var(--R); box-shadow: var(--shadow); display: none; padding: 3px; }
.qi-menu { position: absolute; z-index: 190; width: 200px; max-height: 380px; overflow-y: auto; background: var(--bg4); border: 1px solid var(--border2); border-radius: var(--R); box-shadow: var(--shadow); display: none; padding: 3px; }
.qi-menu.on { display: block; animation: fadeIn 100ms; }
.qi-item { display: flex; align-items: center; gap: 7px; padding: 6px 9px; font-size: 12px; color: var(--text2); cursor: pointer; border-radius: var(--r); }
.qi-item:hover { background: var(--bg-hover); color: var(--text); }
@@ -201,6 +201,15 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
<div class="qi-item" data-qi="code">&#60;/&#62; Code Block</div>
<div class="qi-item" data-qi="table">&#128202; Table</div>
<div class="qi-item" data-qi="divider">&#9472; Divider</div>
<div style="height:1px;margin:3px 6px;background:var(--border)"></div>
<div style="font-size:10px;font-weight:600;color:var(--text3);padding:4px 9px 2px;text-transform:uppercase;letter-spacing:0.4px">&#128208; Diagram</div>
<div class="qi-item" data-qi="mermaid-flowchart">&#128256; Flowchart</div>
<div class="qi-item" data-qi="mermaid-sequence">&#128257; Sequence</div>
<div class="qi-item" data-qi="mermaid-class">&#128258; Class Diagram</div>
<div class="qi-item" data-qi="mermaid-state">&#128259; State Diagram</div>
<div class="qi-item" data-qi="mermaid-er">&#128260; ER Diagram</div>
<div class="qi-item" data-qi="mermaid-gantt">&#128261; Gantt Chart</div>
<div class="qi-item" data-qi="mermaid-pie">&#128262; Pie Chart</div>
</div>
<!-- Slash menu -->
@@ -583,6 +592,23 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
if (previewVisible) renderPreview();
}
// ---- Sync scroll between editor and preview ----
var _syncScrolling = false;
editorWrap.addEventListener('scroll', function() {
if (!previewVisible || _syncScrolling) return;
_syncScrolling = true;
var pct = editorWrap.scrollTop / (editorWrap.scrollHeight - editorWrap.clientHeight);
previewPane.scrollTop = pct * (previewPane.scrollHeight - previewPane.clientHeight);
setTimeout(function() { _syncScrolling = false; }, 50);
});
previewPane.addEventListener('scroll', function() {
if (_syncScrolling) return;
_syncScrolling = true;
var pct = previewPane.scrollTop / (previewPane.scrollHeight - previewPane.clientHeight);
editorWrap.scrollTop = pct * (editorWrap.scrollHeight - editorWrap.clientHeight);
setTimeout(function() { _syncScrolling = false; }, 50);
});
// ---- Resize handle ----
(function() {
var dragging = false;
@@ -650,7 +676,6 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
divider: function() { insertAtCursor('\n---\n'); },
image: function() { insertAtCursor('![](url)', 2); },
link: function() { insertAtCursor('[text](url)', 1); },
table: function() { insertAtCursor('\n| Col 1 | Col 2 |\n|-------|-------|\n| | |\n', 2); },
};
function showSlash() {
@@ -693,6 +718,37 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
function selectSlashAction(action) {
hideSlash();
// Handle QI-specific actions
if (action && action.indexOf('mermaid-') === 0) {
var type = action.replace('mermaid-', '');
var templates = {
flowchart: 'flowchart TD\n A[Start] --> B[Process] --> C[End]',
sequence: 'sequenceDiagram\n Alice->>Bob: Hello\n Bob-->>Alice: Hi!',
class: 'classDiagram\n class Animal {\n +name: string\n +eat()\n }\n class Dog {\n +bark()\n }\n Animal <|-- Dog',
state: 'stateDiagram-v2\n [*] --> Idle\n Idle --> Processing\n Processing --> Done\n Done --> [*]',
er: 'erDiagram\n CUSTOMER ||--o{ ORDER : places\n ORDER ||--|{ LINE_ITEM : contains',
gantt: 'gantt\n title Project Plan\n dateFormat YYYY-MM-DD\n section Phase 1\n Task A :a1, 2026-01-01, 30d\n Task B :after a1, 20d',
pie: 'pie title Distribution\n "Category A" : 45\n "Category B" : 30\n "Category C" : 25'
};
var tpl = templates[type] || 'flowchart TD\n A --> B';
insertAtCursor('\n```mermaid\n' + tpl + '\n```\n', 12);
ta.focus(); return;
}
// Table: prompt for dimensions
if (action === 'table') {
var cols = parseInt(prompt('Number of columns?', '3'));
var rows = parseInt(prompt('Number of rows?', '3'));
if (!cols || !rows || cols < 1 || rows < 1) { ta.focus(); return; }
var hdr = '|'; var sep = '|';
for (var c = 1; c <= cols; c++) { hdr += ' Col ' + c + ' |'; sep += '-------|'; }
var body = '';
for (var r = 1; r <= rows; r++) {
body += '\n|';
for (var cc = 1; cc <= cols; cc++) body += ' |';
}
insertAtCursor('\n' + hdr + '\n' + sep + body + '\n', 2);
ta.focus(); return;
}
if (action && action.indexOf('ai-') === 0) { execAIAction(action); ta.focus(); return; }
var fn = SLASH_INSERT[action];
if (fn) fn(); else if (action) showToast('Action: ' + action);
@@ -847,8 +903,8 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
var qiItems = [];
document.addEventListener('keydown', function(e) {
if (!qiMenu.classList.contains('on')) return;
if (!qiItems.length) qiItems = Array.from(qiMenu.querySelectorAll('.qi-item'));
if (e.key === 'Escape') { e.preventDefault(); qiMenu.classList.remove('on'); ta.focus(); }
qiItems = Array.from(qiMenu.querySelectorAll('.qi-item')); // refresh each time
if (e.key === 'Escape') { e.preventDefault(); qiMenu.classList.remove('on'); qiIdx = -1; ta.focus(); }
else if (e.key === 'ArrowDown') { e.preventDefault(); qiIdx = (qiIdx + 1) % qiItems.length; highlightQI(); }
else if (e.key === 'ArrowUp') { e.preventDefault(); qiIdx = (qiIdx - 1 + qiItems.length) % qiItems.length; highlightQI(); }
else if (e.key === 'Enter') {
@@ -872,8 +928,19 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
}
});
ta.addEventListener('input', function() { updateStats(); markDirty(); });
ta.addEventListener('input', function() {
updateStats(); markDirty();
// Auto-height: grow textarea to fit content (no double scrollbar)
ta.style.height = 'auto';
ta.style.height = ta.scrollHeight + 'px';
});
ta.addEventListener('mouseup', function() { setTimeout(showBubble, 50); });
// Bubble follows cursor on any selection change (keyboard too)
document.addEventListener('selectionchange', function() {
if (document.activeElement !== ta) return;
clearTimeout(window._bubbleTimer);
window._bubbleTimer = setTimeout(showBubble, 80);
});
slashSearch.addEventListener('input', function() { filterSlash(slashSearch.value); });
slashMenu.addEventListener('click', function(e) {