Monorepo for Tangled tangled.org

[feature] - Add copy-to-clipboard button for markdown code blocks #1114

Summary#

  • Injects a copy button into .prose pre.chroma code blocks via client-side DOM injection in base.html
  • Scoped to .prose so blob view source code (which has its own UX) is unaffected
  • Handles HTMX-swapped content (dynamic comment loading) via htmx:afterSettle listener

Test plan#

  • go test ./appview/pages/markup/... passes (no regressions)
  • Verify button appears on hover over code blocks in rendered markdown
  • Verify clicking copies code and shows check icon briefly
  • Verify dark mode styling
  • Verify blob view code blocks do NOT get copy buttons
Labels

None yet.

assignee

None yet.

Participants 3
AT URI
at://did:plc:c7frv4rcitff3p2nh7of5bcv/sh.tangled.repo.pull/3mgbmydvrsw22
+87
Diff #1
+39
appview/pages/templates/layouts/base.html
··· 97 97 {{ template "layouts/fragments/footer" . }} 98 98 </footer> 99 99 {{ end }} 100 + 101 + <script> 102 + (function() { 103 + var copyIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>'; 104 + var checkIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>'; 105 + 106 + function addCopyButtons(root) { 107 + var blocks = (root || document).querySelectorAll('.prose pre.chroma'); 108 + blocks.forEach(function(pre) { 109 + if (pre.querySelector('.code-copy-btn')) return; 110 + pre.style.position = 'relative'; 111 + var btn = document.createElement('button'); 112 + btn.className = 'code-copy-btn'; 113 + btn.type = 'button'; 114 + btn.title = 'Copy code'; 115 + btn.setAttribute('aria-label', 'Copy code to clipboard'); 116 + btn.innerHTML = copyIcon; 117 + btn.addEventListener('click', function() { 118 + var code = pre.querySelector('code'); 119 + var text = code ? code.innerText : pre.innerText; 120 + navigator.clipboard.writeText(text).then(function() { 121 + btn.innerHTML = checkIcon; 122 + btn.classList.add('copied'); 123 + setTimeout(function() { 124 + btn.innerHTML = copyIcon; 125 + btn.classList.remove('copied'); 126 + }, 2000); 127 + }); 128 + }); 129 + pre.appendChild(btn); 130 + }); 131 + } 132 + 133 + addCopyButtons(); 134 + document.body.addEventListener('htmx:afterSettle', function(e) { 135 + addCopyButtons(e.detail.elt); 136 + }); 137 + })(); 138 + </script> 100 139 </body> 101 140 </html> 102 141 {{ end }}
+48
input.css
··· 220 220 @apply flex justify-center my-4 overflow-x-auto bg-transparent border-0; 221 221 } 222 222 223 + .prose pre.chroma { 224 + position: relative; 225 + } 226 + 227 + .code-copy-btn { 228 + position: absolute; 229 + top: 0.5rem; 230 + right: 0.5rem; 231 + padding: 0.375rem; 232 + border: none; 233 + border-radius: 0.25rem; 234 + cursor: pointer; 235 + color: #6c6f85; 236 + background: rgba(239, 241, 245, 0.8); 237 + opacity: 0; 238 + transition: opacity 0.15s ease, color 0.15s ease, background 0.15s ease; 239 + line-height: 0; 240 + z-index: 1; 241 + } 242 + 243 + .prose pre.chroma:hover .code-copy-btn, 244 + .code-copy-btn:focus { 245 + opacity: 1; 246 + } 247 + 248 + .code-copy-btn:hover { 249 + background: rgba(239, 241, 245, 1); 250 + color: #4c4f69; 251 + } 252 + 253 + .code-copy-btn.copied { 254 + color: #40a02b; 255 + } 256 + 223 257 /* Base callout */ 224 258 details[data-callout] { 225 259 @apply border-l-4 pl-3 py-2 text-gray-800 dark:text-gray-200 my-4; ··· 1008 1042 .chroma .gl { 1009 1043 text-decoration: underline; 1010 1044 } 1045 + 1046 + .code-copy-btn { 1047 + color: #8087a2; 1048 + background: rgba(36, 39, 58, 0.8); 1049 + } 1050 + 1051 + .code-copy-btn:hover { 1052 + background: rgba(36, 39, 58, 1); 1053 + color: #cad3f5; 1054 + } 1055 + 1056 + .code-copy-btn.copied { 1057 + color: #a6da95; 1058 + } 1011 1059 } 1012 1060 1013 1061 actor-typeahead {

History

5 rounds 5 comments
sign up or login to add to the discussion
1 commit
expand
appview/pages: add copy-to-clipboard button for markdown code blocks
no conflicts, ready to merge
expand 2 comments

works brilliantly! one nit: the copy button seems to add an extra newline for each line on my browser. do you notice the same behavior on your end?

will do a more thorough code review shortly!

I still need to look into this!! I asked Claude in the meantime, and it thinks you're right - there's an unnecessary carriage return

1 commit
expand
appview/pages: add copy-to-clipboard button for markdown code blocks
expand 0 comments
1 commit
expand
appview/pages: add copy-to-clipboard button for markdown code blocks
expand 0 comments
1 commit
expand
appview/pages: add copy-to-clipboard button for markdown code blocks
expand 3 comments

Thank you for the contribution! Few feedbacks:

  • here we can use icon template like others.
  • can we use tailwindcss for styling? using @applysyntax in css file.

i would prefer if we added this button using a goldmark extension instead of a clientside script!

Thank you for the feedback! I've updating the diff to use goldmark and apply tailwind classes.

1 commit
expand
appview/pages: add copy-to-clipboard button for markdown code blocks
expand 0 comments