Skip to content

Commit f4461f3

Browse files
MrFlounderclaude
andcommitted
feat(md): add inline editing with split view, diff highlights, and save
Add an Edit button to the `crab md` rendered view. Clicking it opens a split-view editor (textarea left, live preview right) with 150ms debounced re-rendering. Changed and added blocks are highlighted with colored left borders (yellow for changed, green for added) using block-level diffing. Save button downloads the edited markdown as `<name>.edited.md`. Supports Cmd/Ctrl+S and Escape shortcuts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 967eb91 commit f4461f3

1 file changed

Lines changed: 199 additions & 5 deletions

File tree

src/crabcode

Lines changed: 199 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9170,12 +9170,14 @@ MDHEAD
91709170
body {
91719171
max-width: 980px;
91729172
margin: 0 auto;
9173-
padding: 45px 45px 80px;
9173+
padding: 0;
91749174
background: #ffffff;
9175+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
91759176
}
91769177
.markdown-body {
91779178
min-width: 200px;
91789179
max-width: 980px;
9180+
padding: 24px 45px 80px;
91799181
}
91809182
@media (prefers-color-scheme: dark) {
91819183
body { background: #0d1117; }
@@ -9185,15 +9187,95 @@ MDHEAD
91859187
}
91869188
}
91879189
@media (max-width: 767px) {
9188-
body { padding: 15px 15px 60px; }
9190+
.markdown-body { padding: 15px 15px 60px; }
91899191
}
91909192
@media print {
91919193
body { max-width: none; padding: 0; }
9194+
#md-toolbar { display: none; }
9195+
}
9196+
/* Toolbar */
9197+
#md-toolbar {
9198+
position: sticky; top: 0; z-index: 100;
9199+
display: flex; justify-content: space-between; align-items: center;
9200+
padding: 8px 16px;
9201+
background: #f6f8fa; border-bottom: 1px solid #d0d7de;
9202+
font-size: 14px;
9203+
}
9204+
#md-filename { font-weight: 600; color: #57606a; }
9205+
#md-toolbar button {
9206+
padding: 5px 16px; font-size: 14px; border-radius: 6px;
9207+
border: 1px solid rgba(27,31,36,0.15); cursor: pointer; margin-left: 8px;
9208+
font-weight: 500;
9209+
}
9210+
.btn-primary { background: #2ea44f !important; color: #fff !important; border-color: rgba(27,31,36,0.15) !important; }
9211+
.btn-primary:hover { background: #2c974b !important; }
9212+
.btn-default { background: #f6f8fa; color: #24292f; }
9213+
.btn-default:hover { background: #e1e4e8; }
9214+
@media (prefers-color-scheme: dark) {
9215+
#md-toolbar { background: #161b22; border-bottom-color: #30363d; }
9216+
#md-filename { color: #8b949e; }
9217+
.btn-default { background: #21262d; color: #c9d1d9; border-color: #30363d; }
9218+
.btn-default:hover { background: #30363d; }
9219+
}
9220+
/* Edit mode layout */
9221+
body.edit-mode { max-width: none; }
9222+
body.edit-mode #md-container { display: flex; height: calc(100vh - 45px); }
9223+
body.edit-mode #md-editor { display: flex !important; flex: 1; min-width: 0; border-right: 1px solid #d0d7de; }
9224+
body.edit-mode .markdown-body { flex: 1; min-width: 0; overflow-y: auto; padding: 24px 45px 80px; max-width: none; }
9225+
#md-source {
9226+
width: 100%; height: 100%; border: none; outline: none; resize: none;
9227+
padding: 24px; font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,monospace;
9228+
font-size: 14px; line-height: 1.6; background: #fff; color: #24292f; tab-size: 4;
9229+
}
9230+
@media (prefers-color-scheme: dark) {
9231+
body.edit-mode #md-editor { border-right-color: #30363d; }
9232+
#md-source { background: #0d1117; color: #e6edf3; }
9233+
}
9234+
/* Diff highlights */
9235+
.diff-added {
9236+
background: rgba(46,160,67,0.15); border-left: 3px solid #2ea44f;
9237+
padding-left: 8px; margin-left: -11px;
9238+
}
9239+
.diff-changed {
9240+
background: rgba(210,153,34,0.15); border-left: 3px solid #d29922;
9241+
padding-left: 8px; margin-left: -11px;
9242+
}
9243+
@media (prefers-color-scheme: dark) {
9244+
.diff-added { background: rgba(46,160,67,0.1); }
9245+
.diff-changed { background: rgba(210,153,34,0.1); }
9246+
}
9247+
/* Diff legend */
9248+
#diff-legend {
9249+
display: none; padding: 8px 16px; font-size: 12px; color: #57606a;
9250+
border-bottom: 1px solid #d0d7de; background: #f6f8fa;
9251+
}
9252+
#diff-legend.visible { display: flex; gap: 16px; align-items: center; }
9253+
#diff-legend span { display: inline-flex; align-items: center; gap: 4px; }
9254+
#diff-legend .swatch { width: 12px; height: 12px; border-radius: 2px; display: inline-block; }
9255+
@media (prefers-color-scheme: dark) {
9256+
#diff-legend { background: #161b22; border-bottom-color: #30363d; color: #8b949e; }
91929257
}
91939258
</style>
91949259
</head>
91959260
<body>
9196-
<article class="markdown-body"></article>
9261+
<div id="md-toolbar">
9262+
<span id="md-filename"></span>
9263+
<div>
9264+
<button id="btn-edit" class="btn-primary" title="Edit markdown">Edit</button>
9265+
<button id="btn-save" class="btn-primary" style="display:none" title="Download edited file">Save</button>
9266+
<button id="btn-cancel" class="btn-default" style="display:none" title="Exit edit mode">Cancel</button>
9267+
</div>
9268+
</div>
9269+
<div id="diff-legend">
9270+
<span><span class="swatch" style="background:#d29922"></span> Changed</span>
9271+
<span><span class="swatch" style="background:#2ea44f"></span> Added</span>
9272+
</div>
9273+
<div id="md-container">
9274+
<article class="markdown-body" id="md-preview"></article>
9275+
<div id="md-editor" style="display:none">
9276+
<textarea id="md-source" spellcheck="false"></textarea>
9277+
</div>
9278+
</div>
91979279
<script>
91989280
MDCSS
91999281
cat "$MD_ASSETS_DIR/marked.min.js"
@@ -9214,9 +9296,121 @@ MDCSS
92149296
var bytes=new Uint8Array(bin.length);
92159297
for(var i=0;i<bin.length;i++)bytes[i]=bin.charCodeAt(i);
92169298
var src=new TextDecoder('utf-8').decode(bytes);
9217-
document.querySelector('.markdown-body').innerHTML=marked.parse(src);
9218-
document.querySelectorAll('pre code').forEach(function(b){hljs.highlightElement(b);});
9299+
window.__originalMd=src;
9300+
window.__currentMd=src;
9301+
document.getElementById('md-preview').innerHTML=marked.parse(src);
9302+
document.querySelectorAll('#md-preview pre code').forEach(function(b){hljs.highlightElement(b);});
9303+
document.getElementById('md-filename').textContent=document.title;
92199304
MDSCRIPT
9305+
echo "</script>"
9306+
echo "<script>"
9307+
cat << 'MDEDITJS'
9308+
(function(){
9309+
var preview=document.getElementById('md-preview');
9310+
var editor=document.getElementById('md-editor');
9311+
var source=document.getElementById('md-source');
9312+
var btnEdit=document.getElementById('btn-edit');
9313+
var btnSave=document.getElementById('btn-save');
9314+
var btnCancel=document.getElementById('btn-cancel');
9315+
var legend=document.getElementById('diff-legend');
9316+
9317+
function renderPreview(md,showDiff){
9318+
preview.innerHTML=marked.parse(md);
9319+
document.querySelectorAll('#md-preview pre code').forEach(function(b){hljs.highlightElement(b);});
9320+
if(showDiff) applyDiffHighlights();
9321+
}
9322+
9323+
function splitBlocks(text){
9324+
return text.split(/\n{2,}/).filter(function(b){return b.trim().length>0;});
9325+
}
9326+
9327+
function applyDiffHighlights(){
9328+
var origBlocks=splitBlocks(window.__originalMd);
9329+
var currBlocks=splitBlocks(window.__currentMd);
9330+
var children=preview.children;
9331+
var hasChanges=false;
9332+
// Walk through rendered children and mark changed/added blocks
9333+
var ci=0;
9334+
for(var i=0;i<children.length&&ci<Math.max(origBlocks.length,currBlocks.length);i++){
9335+
// Skip non-element nodes
9336+
if(children[i].nodeType!==1)continue;
9337+
if(ci>=origBlocks.length){
9338+
children[i].classList.add('diff-added');
9339+
hasChanges=true;
9340+
}else if(ci<currBlocks.length&&origBlocks[ci].trim()!==currBlocks[ci].trim()){
9341+
children[i].classList.add('diff-changed');
9342+
hasChanges=true;
9343+
}
9344+
ci++;
9345+
}
9346+
legend.className=hasChanges?'visible':'';
9347+
}
9348+
9349+
function enterEditMode(){
9350+
document.body.classList.add('edit-mode');
9351+
source.value=window.__currentMd;
9352+
btnEdit.style.display='none';
9353+
btnSave.style.display='';
9354+
btnCancel.style.display='';
9355+
legend.className='';
9356+
// Clear diff highlights for split view
9357+
Array.from(preview.children).forEach(function(el){
9358+
el.classList.remove('diff-added','diff-changed');
9359+
});
9360+
source.focus();
9361+
}
9362+
9363+
function exitEditMode(){
9364+
document.body.classList.remove('edit-mode');
9365+
btnEdit.style.display='';
9366+
btnSave.style.display='none';
9367+
btnCancel.style.display='none';
9368+
var hasChanges=window.__currentMd!==window.__originalMd;
9369+
renderPreview(window.__currentMd,hasChanges);
9370+
}
9371+
9372+
btnEdit.addEventListener('click',enterEditMode);
9373+
btnCancel.addEventListener('click',exitEditMode);
9374+
9375+
// Live preview with debounce
9376+
var timer;
9377+
source.addEventListener('input',function(){
9378+
clearTimeout(timer);
9379+
timer=setTimeout(function(){
9380+
window.__currentMd=source.value;
9381+
renderPreview(window.__currentMd,true);
9382+
},150);
9383+
});
9384+
9385+
// Save/download
9386+
btnSave.addEventListener('click',function(){
9387+
var content=window.__currentMd;
9388+
var blob=new Blob([content],{type:'text/markdown;charset=utf-8'});
9389+
var url=URL.createObjectURL(blob);
9390+
var a=document.createElement('a');
9391+
var origName=document.title.replace(/\.[^.]+$/,'');
9392+
a.href=url;
9393+
a.download=origName+'.edited.md';
9394+
document.body.appendChild(a);
9395+
a.click();
9396+
document.body.removeChild(a);
9397+
URL.revokeObjectURL(url);
9398+
});
9399+
9400+
// Keyboard shortcuts
9401+
document.addEventListener('keydown',function(e){
9402+
if((e.metaKey||e.ctrlKey)&&e.key==='s'){
9403+
if(document.body.classList.contains('edit-mode')){
9404+
e.preventDefault();
9405+
btnSave.click();
9406+
}
9407+
}
9408+
if(e.key==='Escape'&&document.body.classList.contains('edit-mode')){
9409+
exitEditMode();
9410+
}
9411+
});
9412+
})();
9413+
MDEDITJS
92209414
echo "</script>"
92219415
echo "</body>"
92229416
echo "</html>"

0 commit comments

Comments
 (0)