nandi/gleanpublic⑂ Fork 0
⑂ d1e88c1
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/glean.git
git clone ssh://git@rickub.com/nandi/glean.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Add annotation highlight toggle and stylingUnverified

Julien Robert committed 2026-04-21T22:31:09+02:00 Browse files
d1e88c1 parent: 2fffac8
modified internal/tmpl/article_detail.html +85 -1
@@ -78,6 +78,10 @@
7878 </div>
7979 {{end}}
8080
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+
8185 {{if and (not .Article.Content.Valid) (not .Article.FullContent.Valid) .Article.URL.Valid (not (isEmbedURL .Article.URL.String))}}
8286 <div id="article-content" class="mt-6">
8387 <button hx-post="/articles/{{.Article.ID}}/fetch-content" hx-target="#article-content" hx-swap="outerHTML"
@@ -94,7 +98,15 @@
9498 <hr class="my-8 border-spot-divider">
9599
96100 <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>
98110
99111 <form id="annotation-form" hx-post="/library/create" hx-target="#annotations-list" hx-swap="beforeend"
100112 hx-on::after-request="this.reset()"
@@ -162,6 +174,78 @@
162174 quoteHighlight.classList.add('hidden');
163175 };
164176
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+ });
248+
165249 document.addEventListener('keydown', function(e) {
166250 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
167251 if (e.key === 'l') {
@@ -78,6 +78,10 @@
78 </div>78 </div>
79 {{end}}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 {{if and (not .Article.Content.Valid) (not .Article.FullContent.Valid) .Article.URL.Valid (not (isEmbedURL .Article.URL.String))}}85 {{if and (not .Article.Content.Valid) (not .Article.FullContent.Valid) .Article.URL.Valid (not (isEmbedURL .Article.URL.String))}}
82 <div id="article-content" class="mt-6">86 <div id="article-content" class="mt-6">
83 <button hx-post="/articles/{{.Article.ID}}/fetch-content" hx-target="#article-content" hx-swap="outerHTML"87 <button hx-post="/articles/{{.Article.ID}}/fetch-content" hx-target="#article-content" hx-swap="outerHTML"
@@ -94,7 +98,15 @@
94 <hr class="my-8 border-spot-divider">98 <hr class="my-8 border-spot-divider">
95 99
96 <section>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 <form id="annotation-form" hx-post="/library/create" hx-target="#annotations-list" hx-swap="beforeend"111 <form id="annotation-form" hx-post="/library/create" hx-target="#annotations-list" hx-swap="beforeend"
100 hx-on::after-request="this.reset()"112 hx-on::after-request="this.reset()"
@@ -162,6 +174,78 @@
162 quoteHighlight.classList.add('hidden');174 quoteHighlight.classList.add('hidden');
163 };175 };
164 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+ });
248+
165 document.addEventListener('keydown', function(e) {249 document.addEventListener('keydown', function(e) {
166 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;250 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
167 if (e.key === 'l') {251 if (e.key === 'l') {
modified static/input.css +2 -0
@@ -67,6 +67,8 @@
6767 .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 }
6868 .article-body del { @apply line-through text-spot-secondary }
6969 .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 }
7072 }
7173
7274 @layer utilities {
@@ -67,6 +67,8 @@
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 }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 .article-body del { @apply line-through text-spot-secondary }68 .article-body del { @apply line-through text-spot-secondary }
69 .article-body mark { @apply bg-spot-orange/30 px-1 rounded }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 @layer utilities {74 @layer utilities {