A social RSS reader built on the AT Protocol. glean.at
glean atproto atmosphere rss feed social app
14
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add annotation highlight toggle and styling

+87 -1
+85 -1
internal/tmpl/article_detail.html
··· 78 78 </div> 79 79 {{end}} 80 80 81 + {{if .Annotations}} 82 + <script id="annotation-quotes" type="application/json">[{{range $i, $a := .Annotations}}{{if $i}},{{end}}{{if $a.Quote.Valid}}{"id":{{$a.ID}},"quote":{{js $a.Quote.String}}}{{end}}{{end}}]</script> 83 + {{end}} 84 + 81 85 {{if and (not .Article.Content.Valid) (not .Article.FullContent.Valid) .Article.URL.Valid (not (isEmbedURL .Article.URL.String))}} 82 86 <div id="article-content" class="mt-6"> 83 87 <button hx-post="/articles/{{.Article.ID}}/fetch-content" hx-target="#article-content" hx-swap="outerHTML" ··· 94 98 <hr class="my-8 border-spot-divider"> 95 99 96 100 <section> 97 - <h2 class="text-lg font-semibold text-spot-text mb-4">Annotations</h2> 101 + <div class="flex items-center justify-between mb-4"> 102 + <h2 class="text-lg font-semibold text-spot-text">Annotations</h2> 103 + <button id="toggle-highlights" class="text-xs text-spot-secondary hover:text-spot-text transition inline-flex items-center gap-1.5"> 104 + <span id="toggle-highlights-icon" class="w-4 h-4 inline-flex"> 105 + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"/></svg> 106 + </span> 107 + <span id="toggle-highlights-label">Hide highlights</span> 108 + </button> 109 + </div> 98 110 99 111 <form id="annotation-form" hx-post="/library/create" hx-target="#annotations-list" hx-swap="beforeend" 100 112 hx-on::after-request="this.reset()" ··· 161 173 quoteInput.value = ''; 162 174 quoteHighlight.classList.add('hidden'); 163 175 }; 176 + 177 + function highlightQuotes() { 178 + var el = document.getElementById('annotation-quotes'); 179 + if (!el) return; 180 + var quotes = JSON.parse(el.textContent); 181 + el.remove(); 182 + 183 + var body = document.querySelector('.article-body'); 184 + if (!body) return; 185 + 186 + var treeWalker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT); 187 + var textNodes = []; 188 + while (treeWalker.nextNode()) textNodes.push(treeWalker.currentNode); 189 + 190 + quotes.forEach(function(a) { 191 + if (!a.quote) return; 192 + var target = a.quote.trim(); 193 + if (!target) return; 194 + 195 + for (var i = 0; i < textNodes.length; i++) { 196 + var node = textNodes[i]; 197 + var idx = node.nodeValue.indexOf(target); 198 + if (idx === -1) continue; 199 + 200 + var range = document.createRange(); 201 + range.setStart(node, idx); 202 + range.setEnd(node, idx + target.length); 203 + 204 + var mark = document.createElement('mark'); 205 + mark.setAttribute('data-annotation', a.id); 206 + mark.className = 'annotation-highlight cursor-pointer'; 207 + mark.addEventListener('click', function() { 208 + var card = document.getElementById('annotation-' + this.getAttribute('data-annotation')); 209 + if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' }); 210 + }); 211 + range.surroundContents(mark); 212 + 213 + textNodes = []; 214 + treeWalker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT); 215 + while (treeWalker.nextNode()) textNodes.push(treeWalker.currentNode); 216 + break; 217 + } 218 + }); 219 + } 220 + 221 + var highlightsEnabled = localStorage.getItem('annotation-highlights') !== 'false'; 222 + 223 + function applyHighlightsState() { 224 + var marks = document.querySelectorAll('.annotation-highlight'); 225 + marks.forEach(function(m) { 226 + m.style.backgroundColor = highlightsEnabled ? '' : 'transparent'; 227 + m.style.borderBottomColor = highlightsEnabled ? '' : 'transparent'; 228 + }); 229 + var btn = document.getElementById('toggle-highlights'); 230 + var label = document.getElementById('toggle-highlights-label'); 231 + if (btn) label.textContent = highlightsEnabled ? 'Hide highlights' : 'Show highlights'; 232 + } 233 + 234 + if (highlightsEnabled) highlightQuotes(); 235 + applyHighlightsState(); 236 + 237 + document.getElementById('toggle-highlights').addEventListener('click', function() { 238 + highlightsEnabled = !highlightsEnabled; 239 + localStorage.setItem('annotation-highlights', highlightsEnabled); 240 + 241 + var body = document.querySelector('.article-body'); 242 + if (highlightsEnabled && body && !document.querySelector('.annotation-highlight') && document.getElementById('annotation-quotes')) { 243 + highlightQuotes(); 244 + } 245 + 246 + applyHighlightsState(); 247 + }); 164 248 165 249 document.addEventListener('keydown', function(e) { 166 250 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
+2
static/input.css
··· 67 67 .article-body iframe[src*="youtube.com"], .article-body iframe[src*="youtube-nocookie.com"], .article-body iframe[src*="vimeo.com"], .article-body iframe[src*="spotify.com"], .article-body iframe[src*="soundcloud.com"], .article-body iframe[src*="bandcamp.com"] { @apply w-full aspect-video } 68 68 .article-body del { @apply line-through text-spot-secondary } 69 69 .article-body mark { @apply bg-spot-orange/30 px-1 rounded } 70 + .article-body .annotation-highlight { @apply bg-spot-green/25 border-b-2 border-spot-green/50 px-0.5 rounded-sm transition-colors } 71 + .article-body .annotation-highlight:hover { @apply bg-spot-green/40 } 70 72 } 71 73 72 74 @layer utilities {