A vibe coded tangled fork which supports pijul.

appview/pages: gutter button that links code line

Signed-off-by: Lewis <lewis@tangled.org>

authored by

Lewis and committed by tangled.org a251bd9e f17ad0ca

+134
+132
appview/pages/templates/fragments/line-quote-button.html
··· 1 + {{ define "fragments/line-quote-button" }} 2 + <button 3 + id="line-quote-btn" 4 + type="button" 5 + aria-label="Quote line in comment" 6 + class="hidden fixed z-50 p-0.5 rounded bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 text-gray-500 dark:text-gray-400 hover:text-black dark:hover:text-white hover:bg-gray-200 dark:hover:bg-gray-600 cursor-pointer shadow-sm transition-opacity opacity-0" 7 + style="pointer-events: none;" 8 + > 9 + {{ i "message-square-quote" "w-3.5 h-3.5" }} 10 + </button> 11 + <script> 12 + (() => { 13 + const btn = document.getElementById('line-quote-btn'); 14 + if (!btn) return; 15 + 16 + let currentAnchor = null; 17 + let currentFileName = null; 18 + 19 + const findTextarea = () => 20 + document.getElementById('pull-comment-textarea') 21 + || document.getElementById('comment-textarea'); 22 + 23 + const findLineEl = (el) => 24 + el?.closest?.('.line') 25 + || el?.closest?.('span[id*="-O"]') 26 + || el?.closest?.('span[id*="-N"]'); 27 + 28 + const getAnchor = (lineEl) => { 29 + const link = lineEl.querySelector('a[href^="#"]'); 30 + return link ? link.getAttribute('href').slice(1) : lineEl.id || null; 31 + }; 32 + 33 + const getFileName = (lineEl) => { 34 + const details = lineEl.closest('details[id^="file-"]'); 35 + if (details) return details.id.replace(/^file-/, ''); 36 + const bc = document.getElementById('breadcrumbs'); 37 + if (!bc) return null; 38 + const els = bc.querySelectorAll('.text-bold'); 39 + return els.length > 0 ? els[els.length - 1].textContent.trim() : null; 40 + }; 41 + 42 + const show = (lineEl) => { 43 + if (!findTextarea()) return; 44 + const anchor = getAnchor(lineEl); 45 + if (!anchor) return; 46 + 47 + currentAnchor = anchor; 48 + currentFileName = getFileName(lineEl); 49 + 50 + const rect = lineEl.getBoundingClientRect(); 51 + Object.assign(btn.style, { 52 + top: `${rect.top + rect.height / 2 - btn.offsetHeight / 2}px`, 53 + left: `${rect.left + 4}px`, 54 + opacity: '1', 55 + pointerEvents: 'auto', 56 + }); 57 + btn.classList.remove('hidden'); 58 + }; 59 + 60 + const hide = () => { 61 + Object.assign(btn.style, { opacity: '0', pointerEvents: 'none' }); 62 + setTimeout(() => { if (btn.style.opacity === '0') btn.classList.add('hidden'); }, 150); 63 + }; 64 + 65 + let hoverTarget = null; 66 + 67 + document.addEventListener('mouseover', (e) => { 68 + const lineEl = findLineEl(e.target); 69 + if (lineEl && lineEl !== hoverTarget) { 70 + hoverTarget = lineEl; 71 + show(lineEl); 72 + } 73 + }); 74 + 75 + document.addEventListener('mouseout', (e) => { 76 + const lineEl = findLineEl(e.target); 77 + if (!lineEl) return; 78 + if (findLineEl(e.relatedTarget) === lineEl) return; 79 + if (e.relatedTarget === btn || btn.contains(e.relatedTarget)) return; 80 + hoverTarget = null; 81 + hide(); 82 + }); 83 + 84 + btn.addEventListener('mouseleave', (e) => { 85 + if (!findLineEl(e.relatedTarget)) { 86 + hoverTarget = null; 87 + hide(); 88 + } 89 + }); 90 + 91 + btn.addEventListener('click', (e) => { 92 + e.preventDefault(); 93 + e.stopPropagation(); 94 + const textarea = findTextarea(); 95 + if (!textarea || !currentAnchor) return; 96 + 97 + const lineNum = currentAnchor.match(/[ON]?(\d+)(?:-[ON]?\d+)?$/)?.[1]; 98 + if (!lineNum) return; 99 + 100 + const label = currentFileName ? `${currentFileName}:${lineNum}` : `L${lineNum}`; 101 + const md = `[\`${label}\`](${window.location.pathname}#${currentAnchor})`; 102 + 103 + const { selectionStart: start, selectionEnd: end, value } = textarea; 104 + const before = value.slice(0, start); 105 + const after = value.slice(end); 106 + 107 + let prefix = ''; 108 + let suffix = ''; 109 + if (start === end && before.length > 0) { 110 + const currentLine = before.slice(before.lastIndexOf('\n') + 1); 111 + if (currentLine.length > 0) { 112 + const nextNl = after.indexOf('\n'); 113 + const restOfLine = nextNl === -1 ? after : after.slice(0, nextNl); 114 + if (restOfLine.trim().length === 0) { 115 + prefix = '\n'; 116 + } else { 117 + prefix = before.endsWith(' ') ? '' : ' '; 118 + suffix = after.startsWith(' ') ? '' : ' '; 119 + } 120 + } 121 + } 122 + 123 + textarea.value = before + prefix + md + suffix + after; 124 + const pos = start + prefix.length + md.length + suffix.length; 125 + textarea.selectionStart = textarea.selectionEnd = pos; 126 + textarea.focus(); 127 + textarea.dispatchEvent(new Event('input', { bubbles: true })); 128 + textarea.dispatchEvent(new Event('keyup', { bubbles: true })); 129 + }); 130 + })(); 131 + </script> 132 + {{ end }}
+1
appview/pages/templates/repo/blob.html
··· 121 121 </div> 122 122 {{ end }} 123 123 {{ template "fragments/multiline-select" }} 124 + {{ template "fragments/line-quote-button" }} 124 125 <script> 125 126 (() => { 126 127 const abortController = new AbortController();
+1
appview/pages/templates/repo/fragments/diff.html
··· 12 12 {{ block "diffLayout" . }} {{ end }} 13 13 {{ template "fragments/resizable" }} 14 14 {{ template "activeFileHighlight" }} 15 + {{ template "fragments/line-quote-button" }} 15 16 {{ end }} 16 17 17 18 {{ define "diffTopbar" }}