Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol diffdown.com

feat: add link editing tooltip to WYSIWYG editor; page-style layout

- Click a link in rich text mode to show an inline tooltip with URL input
- Tooltip supports update, remove, and cancel actions
- Editor rich container now uses --bg for contrast; milkdown pane styled as a document page with border, shadow, and padding
- Increase paragraph spacing (1.25em) and reduce list item spacing (0.25em)
- Fix p > li margin bleed in both editor and markdown-body views

+191 -1
+71 -1
static/css/editor.css
··· 83 83 display: flex; 84 84 flex-direction: column; 85 85 align-items: center; 86 - background: var(--bg-card); 86 + background: var(--bg); 87 87 } 88 88 89 89 .editor-rich .milkdown { ··· 91 91 max-width: 720px; 92 92 outline: none; 93 93 cursor: text; 94 + background: var(--bg-card); 95 + border: 1px solid var(--border); 96 + border-radius: var(--radius); 97 + box-shadow: 0 2px 12px rgba(0,0,0,0.08); 98 + padding: 3rem 3.5rem; 94 99 } 95 100 96 101 /* Milkdown prose styles — match markdown.css */ ··· 99 104 min-height: 60vh; 100 105 } 101 106 107 + .editor-rich .milkdown .ProseMirror p { 108 + margin-bottom: 1.25em; 109 + } 110 + 102 111 .editor-rich .milkdown .ProseMirror ul, 103 112 .editor-rich .milkdown .ProseMirror ol { 104 113 padding-left: 1.5rem; 105 114 } 106 115 116 + .editor-rich .milkdown .ProseMirror li { 117 + margin-bottom: 0.25em; 118 + } 119 + 120 + .editor-rich .milkdown .ProseMirror li > p { 121 + margin-bottom: 0; 122 + } 123 + 107 124 /* Source/Wrap/Preview buttons hidden in rich text mode */ 108 125 .source-only { 109 126 display: none; 110 127 } 128 + 129 + /* Link editing tooltip */ 130 + .link-tooltip { 131 + position: fixed; 132 + z-index: 200; 133 + display: none; 134 + align-items: center; 135 + gap: 0.4rem; 136 + background: var(--bg-card); 137 + border: 1px solid var(--border); 138 + border-radius: var(--radius); 139 + padding: 0.35rem 0.5rem; 140 + box-shadow: 0 4px 16px rgba(0,0,0,0.15); 141 + } 142 + 143 + .link-tooltip.visible { 144 + display: flex; 145 + } 146 + 147 + .link-tooltip input { 148 + width: 260px; 149 + border: 1px solid var(--border); 150 + border-radius: var(--radius); 151 + background: var(--bg); 152 + color: var(--text); 153 + padding: 0.2rem 0.5rem; 154 + font-size: 0.83rem; 155 + outline: none; 156 + } 157 + 158 + .link-tooltip input:focus { 159 + border-color: var(--primary); 160 + } 161 + 162 + .link-tooltip button { 163 + background: none; 164 + border: 1px solid var(--border); 165 + border-radius: var(--radius); 166 + color: var(--text-muted); 167 + cursor: pointer; 168 + font-size: 0.8rem; 169 + padding: 0.2rem 0.45rem; 170 + white-space: nowrap; 171 + line-height: 1.4; 172 + } 173 + 174 + .link-tooltip button:first-of-type { 175 + background: var(--primary); 176 + border-color: var(--primary); 177 + color: #fff; 178 + } 179 + 180 + .link-tooltip button:hover { opacity: 0.8; }
+1
static/css/markdown.css
··· 53 53 } 54 54 55 55 .markdown-body li { margin-bottom: 0.25em; } 56 + .markdown-body li > p { margin-bottom: 0; } 56 57 57 58 .markdown-body table { 58 59 width: 100%;
+119
templates/document_edit.html
··· 26 26 <!-- Rich text editor (default) --> 27 27 <div id="editor-rich" class="editor-rich"></div> 28 28 29 + <!-- Link editing tooltip --> 30 + <div id="link-tooltip" class="link-tooltip"> 31 + <input type="url" id="link-tooltip-input" placeholder="https://" spellcheck="false"> 32 + <button id="link-tooltip-confirm">Update</button> 33 + <button id="link-tooltip-remove">Remove</button> 34 + <button id="link-tooltip-cancel">✕</button> 35 + </div> 36 + 29 37 <!-- Source editor (CodeMirror + preview split) --> 30 38 <div id="editor-source" class="editor-split" style="display:none"> 31 39 <div class="editor-pane"> ··· 254 262 saveStatus.className = 'status-error'; 255 263 } 256 264 }; 265 + 266 + // ── Link tooltip ────────────────────────────────────────────────────────── 267 + 268 + const linkTooltipEl = document.getElementById('link-tooltip'); 269 + const linkInput = document.getElementById('link-tooltip-input'); 270 + const linkConfirmBtn = document.getElementById('link-tooltip-confirm'); 271 + const linkRemoveBtn = document.getElementById('link-tooltip-remove'); 272 + const linkCancelBtn = document.getElementById('link-tooltip-cancel'); 273 + 274 + let linkTooltipState = null; // { pmView, pos } 275 + 276 + function findMarkExtent(state, searchPos, markType) { 277 + try { 278 + const $pos = state.doc.resolve(Math.min(searchPos, state.doc.content.size - 1)); 279 + const parent = $pos.parent; 280 + const parentStart = $pos.start(); 281 + let currentHref = null; 282 + parent.forEach((node, offset) => { 283 + const nodeStart = parentStart + offset; 284 + const nodeEnd = nodeStart + node.nodeSize; 285 + const lm = markType.isInSet(node.marks); 286 + if (lm && nodeStart <= searchPos && searchPos <= nodeEnd) currentHref = lm.attrs.href; 287 + }); 288 + if (!currentHref) return { from: -1, to: -1 }; 289 + let linkFrom = -1, linkTo = -1; 290 + parent.forEach((node, offset) => { 291 + const lm = markType.isInSet(node.marks); 292 + if (lm && lm.attrs.href === currentHref) { 293 + const nodeStart = parentStart + offset; 294 + if (linkFrom === -1) linkFrom = nodeStart; 295 + linkTo = nodeStart + node.nodeSize; 296 + } 297 + }); 298 + return { from: linkFrom, to: linkTo }; 299 + } catch(e) { return { from: -1, to: -1 }; } 300 + } 301 + 302 + function showLinkTooltip(pmView, mark, pos) { 303 + linkTooltipState = { pmView, pos }; 304 + linkInput.value = mark.attrs.href || ''; 305 + const coords = pmView.coordsAtPos(pos); 306 + linkTooltipEl.style.left = Math.max(8, coords.left) + 'px'; 307 + linkTooltipEl.style.top = (coords.bottom + 8) + 'px'; 308 + linkTooltipEl.classList.add('visible'); 309 + } 310 + 311 + function hideLinkTooltip() { 312 + linkTooltipEl.classList.remove('visible'); 313 + linkTooltipState = null; 314 + } 315 + 316 + // Prevent buttons from stealing editor focus 317 + [linkConfirmBtn, linkRemoveBtn, linkCancelBtn].forEach(btn => { 318 + btn.addEventListener('mousedown', e => e.preventDefault()); 319 + }); 320 + 321 + linkCancelBtn.addEventListener('click', () => hideLinkTooltip()); 322 + 323 + linkConfirmBtn.addEventListener('click', () => { 324 + if (!linkTooltipState) return; 325 + const { pmView, pos } = linkTooltipState; 326 + const newHref = linkInput.value.trim(); 327 + if (!newHref) return; 328 + const { state, dispatch } = pmView; 329 + const linkType = state.schema.marks.link; 330 + const { from, to } = findMarkExtent(state, pos, linkType); 331 + if (from === -1) return; 332 + dispatch(state.tr.addMark(from, to, linkType.create({ href: newHref }))); 333 + hideLinkTooltip(); 334 + pmView.focus(); 335 + }); 336 + 337 + linkRemoveBtn.addEventListener('click', () => { 338 + if (!linkTooltipState) return; 339 + const { pmView, pos } = linkTooltipState; 340 + const { state, dispatch } = pmView; 341 + const linkType = state.schema.marks.link; 342 + const { from, to } = findMarkExtent(state, pos, linkType); 343 + if (from === -1) return; 344 + dispatch(state.tr.removeMark(from, to, linkType)); 345 + hideLinkTooltip(); 346 + pmView.focus(); 347 + }); 348 + 349 + linkInput.addEventListener('keydown', e => { 350 + if (e.key === 'Enter') linkConfirmBtn.click(); 351 + if (e.key === 'Escape') hideLinkTooltip(); 352 + }); 353 + 354 + function checkForLinkTooltip() { 355 + if (currentMode !== 'rich' || !milkdownEditor) return; 356 + try { 357 + const pmView = milkdownEditor.action(ctx => ctx.get(editorViewCtx)); 358 + const { selection, schema, doc } = pmView.state; 359 + const linkType = schema.marks.link; 360 + const pos = Math.min(selection.from, doc.content.size - 1); 361 + const marks = doc.resolve(pos).marks(); 362 + const linkMark = marks.find(m => m.type === linkType); 363 + if (linkMark) showLinkTooltip(pmView, linkMark, pos); 364 + else hideLinkTooltip(); 365 + } catch(e) { hideLinkTooltip(); } 366 + } 367 + 368 + document.getElementById('editor-rich').addEventListener('click', () => setTimeout(checkForLinkTooltip, 0)); 369 + document.getElementById('editor-rich').addEventListener('keyup', checkForLinkTooltip); 370 + 371 + document.addEventListener('click', e => { 372 + if (!linkTooltipEl.contains(e.target) && !document.getElementById('editor-rich').contains(e.target)) { 373 + hideLinkTooltip(); 374 + } 375 + }); 257 376 258 377 // ── Init ────────────────────────────────────────────────────────────────── 259 378