feat: finalize Forge editor — Tab indent, Ctrl+K, auto-focus, AI status, French errors
CI / lint (push) Failing after 3s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / security (push) Successful in -1s

Tab/Shift+Tab: indent/dedent list items. Ctrl+K: link insertion prompt. Qi button: position:fixed (always visible). Auto-focus textarea on load. AI ready indicator in status bar. All error messages in French. Force-save shows 'Sauvegarde' toast.
This commit is contained in:
2026-06-10 09:09:53 -04:00
parent 8cf7b55747
commit 0cdebc5b40
+47 -13
View File
@@ -56,7 +56,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
.textarea::placeholder { color: var(--text3); }
/* Quick insert + */
.qi-btn { position: absolute; left: 14px; top: 24px; width: 26px; height: 26px; border-radius: 50%; border: 1px solid var(--border); background: var(--bg2); color: var(--text3); cursor: pointer; font-size: 16px; font-weight: 700; display: flex; align-items: center; justify-content: center; z-index: 10; transition: all 100ms; }
.qi-btn { position: fixed; top: 56px; left: 48px; width: 26px; height: 26px; border-radius: 50%; border: 1px solid var(--border); background: var(--bg2); color: var(--text3); cursor: pointer; font-size: 16px; font-weight: 700; display: flex; align-items: center; justify-content: center; z-index: 50; transition: all 100ms; }
.qi-btn:hover { color: var(--text); border-color: var(--accent); background: var(--accent-bg); }
/* Preview pane */
@@ -331,7 +331,9 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
<span>Type <strong>/</strong> for commands | <strong>Alt+I</strong> Insert</span>
<span>Ctrl+J AI panel</span>
<span>Ctrl+S save</span>
<span>Ctrl+K link</span>
<span class="stat-ai" id="stat-ai">&#10024; AI processing...</span>
<span id="stat-ai-ready" style="display:none;color:var(--ok)">&#10024; AI ready</span>
</div>
<div class="stat-right">
<span class="stat-file" id="stat-file"></span>
@@ -436,6 +438,25 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
replaceLine(l.text.replace(/^\/\s*/, ''));
}
// ---- Tab indentation ----
function indentLine() {
var l = getLine();
if (/^(\s*)([-*]|\d+\.)\s/.test(l.text)) { replaceLine(' ' + l.text); }
else { insertAtCursor(' '); }
}
function dedentLine() {
var l = getLine();
if (/^ /.test(l.text)) { replaceLine(l.text.replace(/^ /, '')); }
}
function promptLink() {
var sel = val().substring(ta.selectionStart, ta.selectionEnd);
var url = prompt('URL:', 'https://');
if (url) {
if (sel) { wrapSelection('[', '](' + url + ')'); }
else { insertAtCursor('[text](' + url + ')', 1); }
}
}
// ---- Dirty / Save ----
function markDirty() {
if (!isDirty) { isDirty = true; updateSaveDot(); }
@@ -465,13 +486,13 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
isDirty = false; originalContent = content;
saveDot.className = 'save-dot ok'; saveLabel.textContent = 'Saved';
}).catch(function(e) {
saveDot.className = 'save-dot err'; saveLabel.textContent = 'Error';
showToast('Save failed: ' + e.message, 'err');
saveDot.className = 'save-dot err'; saveLabel.textContent = 'Erreur';
showToast('Echec sauvegarde: ' + e.message, 'err');
});
}
function forceSave() {
if (!fileVault || !filePath) { showToast('No file loaded. Use ?vault=X&path=Y', 'err'); return; }
if (!fileVault || !filePath) { showToast('Aucun fichier charge.', 'err'); return; }
clearTimeout(saveTimer);
saveDot.className = 'save-dot saving'; saveLabel.textContent = 'Saving...';
var content = val();
@@ -484,8 +505,8 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
saveDot.className = 'save-dot ok'; saveLabel.textContent = 'Saved';
showToast('Saved', 'ok');
}).catch(function(e) {
saveDot.className = 'save-dot err'; saveLabel.textContent = 'Error';
showToast('Save failed: ' + e.message, 'err');
saveDot.className = 'save-dot err'; saveLabel.textContent = 'Erreur';
showToast('Echec sauvegarde: ' + e.message, 'err');
});
}
@@ -523,8 +544,8 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
schedulePreview();
})
.catch(function(e) {
ta.value = '# Error\n\nCould not load: ' + (fileVault || '?') + '/' + (filePath || '?') + '\n\n' + e.message;
statFile.textContent = 'Load error';
ta.value = '# Erreur\n\nImpossible de charger: ' + (fileVault || '?') + '/' + (filePath || '?') + '\n\n' + e.message;
statFile.textContent = 'Erreur chargement';
originalContent = ta.value;
updateStats();
});
@@ -538,8 +559,8 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
aiConfigured = s.configured === true;
updateAIButton();
if (aiConfigured) aiCall(endpoint, text, extra, callback);
else { showToast('AI not configured', 'err'); hideAIProcessing(); }
}).catch(function() { showToast('AI status check failed', 'err'); hideAIProcessing(); });
else { showToast('AI non configuree', 'err'); hideAIProcessing(); }
}).catch(function() { showToast('Verification AI echouee', 'err'); hideAIProcessing(); });
return;
}
showAIProcessing();
@@ -570,8 +591,13 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
}
function updateAIButton() {
if (aiConfigured) { btnAI.className = 'btn btn-ai'; btnAI.title = 'AI Panel (Ctrl+J)'; }
else { btnAI.className = 'btn btn-ai off'; btnAI.title = 'AI not configured'; }
if (aiConfigured) {
btnAI.className = 'btn btn-ai'; btnAI.title = 'AI Panel (Ctrl+J)';
document.getElementById('stat-ai-ready').style.display = 'inline';
} else {
btnAI.className = 'btn btn-ai off'; btnAI.title = 'AI not configured';
document.getElementById('stat-ai-ready').style.display = 'none';
}
}
function showAIProcessing() { statAI.classList.add('on'); }
@@ -646,7 +672,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
processPreviewBlocks();
_previewLoading = false;
}).catch(function(e) {
previewContent.innerHTML = '<div class="preview-loading">Render error: ' + e.message + '</div>';
previewContent.innerHTML = '<div class="preview-loading">Erreur de rendu</div>';
_previewLoading = false;
});
}
@@ -1048,6 +1074,12 @@ 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('*'); }
if (e.ctrlKey && e.key === 'k') { e.preventDefault(); promptLink(); }
// Tab: indent list items
if (e.key === 'Tab') {
e.preventDefault();
if (e.shiftKey) { dedentLine(); } else { indentLine(); }
}
// Quick Insert: Alt+I
if (e.altKey && e.key === 'i') { e.preventDefault(); showQI(); }
});
@@ -1177,6 +1209,8 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig
loadFile();
checkAIStatus();
updateStats();
// Auto-focus after short delay (wait for iframe to fully render)
setTimeout(function() { ta.focus(); }, 200);
// ---- Process preview blocks (Mermaid + highlight.js) ----
function processPreviewBlocks() {