feat: auto-height, sync scroll, Mermaid QI, table prompt, bubble follow
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:
@@ -52,7 +52,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|||||||
|
|
||||||
/* Textarea pane */
|
/* Textarea pane */
|
||||||
.editor-wrap { flex: 1; overflow-y: auto; position: relative; padding: 24px 16px 160px 48px; }
|
.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); }
|
.textarea::placeholder { color: var(--text3); }
|
||||||
|
|
||||||
/* Quick insert + */
|
/* 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); }
|
.bubble-more-item:hover { background: var(--bg-hover); color: var(--text); }
|
||||||
|
|
||||||
/* Quick Insert menu */
|
/* 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-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 { 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); }
|
.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"></> Code Block</div>
|
<div class="qi-item" data-qi="code"></> Code Block</div>
|
||||||
<div class="qi-item" data-qi="table">📊 Table</div>
|
<div class="qi-item" data-qi="table">📊 Table</div>
|
||||||
<div class="qi-item" data-qi="divider">─ Divider</div>
|
<div class="qi-item" data-qi="divider">─ 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">📐 Diagram</div>
|
||||||
|
<div class="qi-item" data-qi="mermaid-flowchart">🔀 Flowchart</div>
|
||||||
|
<div class="qi-item" data-qi="mermaid-sequence">🔁 Sequence</div>
|
||||||
|
<div class="qi-item" data-qi="mermaid-class">🔂 Class Diagram</div>
|
||||||
|
<div class="qi-item" data-qi="mermaid-state">🔃 State Diagram</div>
|
||||||
|
<div class="qi-item" data-qi="mermaid-er">🔄 ER Diagram</div>
|
||||||
|
<div class="qi-item" data-qi="mermaid-gantt">🔅 Gantt Chart</div>
|
||||||
|
<div class="qi-item" data-qi="mermaid-pie">🔆 Pie Chart</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Slash menu -->
|
<!-- Slash menu -->
|
||||||
@@ -583,6 +592,23 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|||||||
if (previewVisible) renderPreview();
|
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 ----
|
// ---- Resize handle ----
|
||||||
(function() {
|
(function() {
|
||||||
var dragging = false;
|
var dragging = false;
|
||||||
@@ -650,7 +676,6 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|||||||
divider: function() { insertAtCursor('\n---\n'); },
|
divider: function() { insertAtCursor('\n---\n'); },
|
||||||
image: function() { insertAtCursor('', 2); },
|
image: function() { insertAtCursor('', 2); },
|
||||||
link: function() { insertAtCursor('[text](url)', 1); },
|
link: function() { insertAtCursor('[text](url)', 1); },
|
||||||
table: function() { insertAtCursor('\n| Col 1 | Col 2 |\n|-------|-------|\n| | |\n', 2); },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function showSlash() {
|
function showSlash() {
|
||||||
@@ -693,6 +718,37 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
|
|||||||
|
|
||||||
function selectSlashAction(action) {
|
function selectSlashAction(action) {
|
||||||
hideSlash();
|
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; }
|
if (action && action.indexOf('ai-') === 0) { execAIAction(action); ta.focus(); return; }
|
||||||
var fn = SLASH_INSERT[action];
|
var fn = SLASH_INSERT[action];
|
||||||
if (fn) fn(); else if (action) showToast('Action: ' + 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 = [];
|
var qiItems = [];
|
||||||
document.addEventListener('keydown', function(e) {
|
document.addEventListener('keydown', function(e) {
|
||||||
if (!qiMenu.classList.contains('on')) return;
|
if (!qiMenu.classList.contains('on')) return;
|
||||||
if (!qiItems.length) qiItems = Array.from(qiMenu.querySelectorAll('.qi-item'));
|
qiItems = Array.from(qiMenu.querySelectorAll('.qi-item')); // refresh each time
|
||||||
if (e.key === 'Escape') { e.preventDefault(); qiMenu.classList.remove('on'); ta.focus(); }
|
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 === '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 === 'ArrowUp') { e.preventDefault(); qiIdx = (qiIdx - 1 + qiItems.length) % qiItems.length; highlightQI(); }
|
||||||
else if (e.key === 'Enter') {
|
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); });
|
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); });
|
slashSearch.addEventListener('input', function() { filterSlash(slashSearch.value); });
|
||||||
slashMenu.addEventListener('click', function(e) {
|
slashMenu.addEventListener('click', function(e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user