nandi/gleanpublic Fork 0
d64ded7
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.

feat: add popover for annotations instead of auto scrollingUnverified

Julien Robert committed 2026-06-24T11:31:33+02:00 Browse files
d64ded7 parent: a86e383
modified internal/tmpl/article_detail.html +93 -17
@@ -121,24 +121,18 @@
121121 </button>
122122 </div>
123123
124- <form id="annotation-form" hx-post="/library/create" hx-target="#annotations-list" hx-swap="beforeend"
124+ <form id="comment-form" hx-post="/library/create" hx-target="#annotations-list" hx-swap="beforeend"
125125 hx-on::after-request="this.reset()"
126126 class="bg-spot-surface rounded-xl shadow-spot p-4 mb-4 space-y-3">
127127 {{csrfInput .CSRFToken}}
128128 <input type="hidden" name="feed_url" value="{{.Article.FeedURL}}">
129129 <input type="hidden" name="article_url" value="{{if .Article.URL.Valid}}{{.Article.URL.String}}{{end}}">
130- <div id="quote-highlight" class="hidden mb-2">
131- <blockquote id="quote-preview" class="border-l-2 border-spot-green pl-3 text-sm text-spot-secondary italic"></blockquote>
132- </div>
133- <input type="hidden" name="quote" id="quote-input">
134- <div>
135- <textarea name="note" rows="2" placeholder="Add a note..."
136- class="w-full bg-spot-hover text-spot-text rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder resize-none"></textarea>
137- </div>
130+ <textarea name="note" rows="2" placeholder="Add a comment..."
131+ class="w-full bg-spot-hover text-spot-text rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder resize-none"></textarea>
138132 <div class="flex gap-2">
139133 <input type="text" name="tags" placeholder="Tags (comma separated)"
140- class="flex-1 bg-spot-hover text-spot-text rounded-pill px-5 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">
141- <button type="submit" class="bg-spot-green text-white rounded-pill px-5 py-2 text-sm font-bold uppercase tracking-button hover:brightness-110 transition">Annotate</button>
134+ class="flex-1 min-w-0 bg-spot-hover text-spot-text rounded-pill px-5 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">
135+ <button type="submit" class="bg-spot-green text-white rounded-pill px-5 py-2 text-sm font-bold uppercase tracking-button hover:brightness-110 transition shrink-0">Comment</button>
142136 </div>
143137 </form>
144138
@@ -151,6 +145,31 @@
151145 </div>
152146 </section>
153147
148+ <div id="annotation-popover" class="hidden fixed z-50 w-[22rem] max-w-[calc(100vw-2rem)] bg-spot-surface rounded-xl shadow-spot-heavy border border-spot-divider p-4 space-y-3">
149+ <div class="flex items-center justify-between">
150+ <span class="text-[10px] font-bold uppercase tracking-button text-spot-secondary">Annotate</span>
151+ <button type="button" id="annotation-popover-close" aria-label="Close" class="text-spot-secondary hover:text-spot-text transition">
152+ <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
153+ </button>
154+ </div>
155+ <form id="annotation-form" hx-post="/library/create" hx-target="#annotations-list" hx-swap="beforeend" class="space-y-3">
156+ {{csrfInput .CSRFToken}}
157+ <input type="hidden" name="feed_url" value="{{.Article.FeedURL}}">
158+ <input type="hidden" name="article_url" value="{{if .Article.URL.Valid}}{{.Article.URL.String}}{{end}}">
159+ <div id="quote-highlight" class="hidden mb-1">
160+ <blockquote id="quote-preview" class="border-l-2 border-spot-green pl-3 text-sm text-spot-secondary italic"></blockquote>
161+ </div>
162+ <input type="hidden" name="quote" id="quote-input">
163+ <textarea name="note" rows="2" placeholder="Add a note..."
164+ class="w-full bg-spot-hover text-spot-text rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder resize-none"></textarea>
165+ <div class="flex gap-2">
166+ <input type="text" name="tags" placeholder="Tags (comma separated)"
167+ class="flex-1 min-w-0 bg-spot-hover text-spot-text rounded-pill px-5 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">
168+ <button type="submit" class="bg-spot-green text-white rounded-pill px-5 py-2 text-sm font-bold uppercase tracking-button hover:brightness-110 transition shrink-0">Annotate</button>
169+ </div>
170+ </form>
171+ </div>
172+
154173 <div class="flex items-center justify-between mt-8 mb-4">
155174 <a href="javascript:history.back()" class="text-sm text-spot-secondary hover:text-spot-text inline-flex items-center gap-1.5 transition">
156175 <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5"/></svg>
@@ -172,8 +191,53 @@
172191 var quoteInput = document.getElementById('quote-input');
173192 var quotePreview = document.getElementById('quote-preview');
174193 var quoteHighlight = document.getElementById('quote-highlight');
194+ var popover = document.getElementById('annotation-popover');
195+ var closeBtn = document.getElementById('annotation-popover-close');
196+ var noteField = form.querySelector('textarea[name="note"]');
197+
198+ function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
199+
200+ function position(rect) {
201+ var margin = 8;
202+ var popRect = popover.getBoundingClientRect();
203+ var width = popRect.width;
204+ var height = popRect.height;
205+
206+ var left = clamp(rect.left + (rect.width - width) / 2, margin, document.documentElement.clientWidth - width - margin);
207+
208+ var top = rect.bottom + margin;
209+ if (document.documentElement.clientHeight - rect.bottom < height + margin && rect.top > height + margin) {
210+ top = rect.top - height - margin;
211+ }
212+ top = Math.max(margin, top);
213+
214+ popover.style.left = left + 'px';
215+ popover.style.top = top + 'px';
216+ }
217+
218+ function openForSelection(sel, text) {
219+ if (text.length > 1000) text = text.substring(0, 1000);
220+ quoteInput.value = text;
221+ quotePreview.textContent = text;
222+ quoteHighlight.classList.remove('hidden');
223+ popover.classList.remove('hidden');
224+ position(sel.getRangeAt(0).getBoundingClientRect());
225+ noteField.focus();
226+ }
227+
228+ function close() {
229+ popover.classList.add('hidden');
230+ popover.style.top = '';
231+ popover.style.left = '';
232+ form.reset();
233+ quoteInput.value = '';
234+ quoteHighlight.classList.add('hidden');
235+ var sel = window.getSelection();
236+ if (sel) sel.removeAllRanges();
237+ }
175238
176239 document.addEventListener('mouseup', function(e) {
240+ if (!popover.classList.contains('hidden') && popover.contains(e.target)) return;
177241 var sel = window.getSelection();
178242 var text = sel.toString().trim();
179243 if (!text) return;
@@ -181,12 +245,20 @@
181245 var body = e.target.closest('.article-body');
182246 if (!body) return;
183247
184- if (text.length > 1000) text = text.substring(0, 1000);
185- quoteInput.value = text;
186- quotePreview.textContent = text;
187- quoteHighlight.classList.remove('hidden');
188- form.scrollIntoView({ behavior: 'smooth', block: 'center' });
189- form.querySelector('textarea[name="note"]').focus();
248+ openForSelection(sel, text);
249+ });
250+
251+ closeBtn.addEventListener('click', close);
252+
253+ document.addEventListener('mousedown', function(e) {
254+ if (popover.classList.contains('hidden')) return;
255+ if (popover.contains(e.target)) return;
256+ if (e.target.closest('.article-body')) return;
257+ close();
258+ });
259+
260+ document.addEventListener('keydown', function(e) {
261+ if (e.key === 'Escape' && !popover.classList.contains('hidden')) close();
190262 });
191263
192264 var origReset = form.reset.bind(form);
@@ -196,6 +268,10 @@
196268 quoteHighlight.classList.add('hidden');
197269 };
198270
271+ form.addEventListener('htmx:afterRequest', function(e) {
272+ if (e.detail && e.detail.successful) close();
273+ });
274+
199275 function highlightQuotes() {
200276 var el = document.getElementById('annotation-quotes');
201277 if (!el) return;
@@ -121,24 +121,18 @@
121 </button>121 </button>
122 </div>122 </div>
123 123
124- <form id="annotation-form" hx-post="/library/create" hx-target="#annotations-list" hx-swap="beforeend"124+ <form id="comment-form" hx-post="/library/create" hx-target="#annotations-list" hx-swap="beforeend"
125 hx-on::after-request="this.reset()"125 hx-on::after-request="this.reset()"
126 class="bg-spot-surface rounded-xl shadow-spot p-4 mb-4 space-y-3">126 class="bg-spot-surface rounded-xl shadow-spot p-4 mb-4 space-y-3">
127 {{csrfInput .CSRFToken}}127 {{csrfInput .CSRFToken}}
128 <input type="hidden" name="feed_url" value="{{.Article.FeedURL}}">128 <input type="hidden" name="feed_url" value="{{.Article.FeedURL}}">
129 <input type="hidden" name="article_url" value="{{if .Article.URL.Valid}}{{.Article.URL.String}}{{end}}">129 <input type="hidden" name="article_url" value="{{if .Article.URL.Valid}}{{.Article.URL.String}}{{end}}">
130- <div id="quote-highlight" class="hidden mb-2">130+ <textarea name="note" rows="2" placeholder="Add a comment..."
131- <blockquote id="quote-preview" class="border-l-2 border-spot-green pl-3 text-sm text-spot-secondary italic"></blockquote>131+ class="w-full bg-spot-hover text-spot-text rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder resize-none"></textarea>
132- </div>
133- <input type="hidden" name="quote" id="quote-input">
134- <div>
135- <textarea name="note" rows="2" placeholder="Add a note..."
136- class="w-full bg-spot-hover text-spot-text rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder resize-none"></textarea>
137- </div>
138 <div class="flex gap-2">132 <div class="flex gap-2">
139 <input type="text" name="tags" placeholder="Tags (comma separated)"133 <input type="text" name="tags" placeholder="Tags (comma separated)"
140- class="flex-1 bg-spot-hover text-spot-text rounded-pill px-5 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">134+ class="flex-1 min-w-0 bg-spot-hover text-spot-text rounded-pill px-5 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">
141- <button type="submit" class="bg-spot-green text-white rounded-pill px-5 py-2 text-sm font-bold uppercase tracking-button hover:brightness-110 transition">Annotate</button>135+ <button type="submit" class="bg-spot-green text-white rounded-pill px-5 py-2 text-sm font-bold uppercase tracking-button hover:brightness-110 transition shrink-0">Comment</button>
142 </div>136 </div>
143 </form>137 </form>
144 138
@@ -151,6 +145,31 @@
151 </div>145 </div>
152 </section>146 </section>
153 147
148+ <div id="annotation-popover" class="hidden fixed z-50 w-[22rem] max-w-[calc(100vw-2rem)] bg-spot-surface rounded-xl shadow-spot-heavy border border-spot-divider p-4 space-y-3">
149+ <div class="flex items-center justify-between">
150+ <span class="text-[10px] font-bold uppercase tracking-button text-spot-secondary">Annotate</span>
151+ <button type="button" id="annotation-popover-close" aria-label="Close" class="text-spot-secondary hover:text-spot-text transition">
152+ <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
153+ </button>
154+ </div>
155+ <form id="annotation-form" hx-post="/library/create" hx-target="#annotations-list" hx-swap="beforeend" class="space-y-3">
156+ {{csrfInput .CSRFToken}}
157+ <input type="hidden" name="feed_url" value="{{.Article.FeedURL}}">
158+ <input type="hidden" name="article_url" value="{{if .Article.URL.Valid}}{{.Article.URL.String}}{{end}}">
159+ <div id="quote-highlight" class="hidden mb-1">
160+ <blockquote id="quote-preview" class="border-l-2 border-spot-green pl-3 text-sm text-spot-secondary italic"></blockquote>
161+ </div>
162+ <input type="hidden" name="quote" id="quote-input">
163+ <textarea name="note" rows="2" placeholder="Add a note..."
164+ class="w-full bg-spot-hover text-spot-text rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder resize-none"></textarea>
165+ <div class="flex gap-2">
166+ <input type="text" name="tags" placeholder="Tags (comma separated)"
167+ class="flex-1 min-w-0 bg-spot-hover text-spot-text rounded-pill px-5 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder">
168+ <button type="submit" class="bg-spot-green text-white rounded-pill px-5 py-2 text-sm font-bold uppercase tracking-button hover:brightness-110 transition shrink-0">Annotate</button>
169+ </div>
170+ </form>
171+ </div>
172+
154 <div class="flex items-center justify-between mt-8 mb-4">173 <div class="flex items-center justify-between mt-8 mb-4">
155 <a href="javascript:history.back()" class="text-sm text-spot-secondary hover:text-spot-text inline-flex items-center gap-1.5 transition">174 <a href="javascript:history.back()" class="text-sm text-spot-secondary hover:text-spot-text inline-flex items-center gap-1.5 transition">
156 <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5"/></svg>175 <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5"/></svg>
@@ -172,8 +191,53 @@
172 var quoteInput = document.getElementById('quote-input');191 var quoteInput = document.getElementById('quote-input');
173 var quotePreview = document.getElementById('quote-preview');192 var quotePreview = document.getElementById('quote-preview');
174 var quoteHighlight = document.getElementById('quote-highlight');193 var quoteHighlight = document.getElementById('quote-highlight');
194+ var popover = document.getElementById('annotation-popover');
195+ var closeBtn = document.getElementById('annotation-popover-close');
196+ var noteField = form.querySelector('textarea[name="note"]');
197+
198+ function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
199+
200+ function position(rect) {
201+ var margin = 8;
202+ var popRect = popover.getBoundingClientRect();
203+ var width = popRect.width;
204+ var height = popRect.height;
205+
206+ var left = clamp(rect.left + (rect.width - width) / 2, margin, document.documentElement.clientWidth - width - margin);
207+
208+ var top = rect.bottom + margin;
209+ if (document.documentElement.clientHeight - rect.bottom < height + margin && rect.top > height + margin) {
210+ top = rect.top - height - margin;
211+ }
212+ top = Math.max(margin, top);
213+
214+ popover.style.left = left + 'px';
215+ popover.style.top = top + 'px';
216+ }
217+
218+ function openForSelection(sel, text) {
219+ if (text.length > 1000) text = text.substring(0, 1000);
220+ quoteInput.value = text;
221+ quotePreview.textContent = text;
222+ quoteHighlight.classList.remove('hidden');
223+ popover.classList.remove('hidden');
224+ position(sel.getRangeAt(0).getBoundingClientRect());
225+ noteField.focus();
226+ }
227+
228+ function close() {
229+ popover.classList.add('hidden');
230+ popover.style.top = '';
231+ popover.style.left = '';
232+ form.reset();
233+ quoteInput.value = '';
234+ quoteHighlight.classList.add('hidden');
235+ var sel = window.getSelection();
236+ if (sel) sel.removeAllRanges();
237+ }
175 238
176 document.addEventListener('mouseup', function(e) {239 document.addEventListener('mouseup', function(e) {
240+ if (!popover.classList.contains('hidden') && popover.contains(e.target)) return;
177 var sel = window.getSelection();241 var sel = window.getSelection();
178 var text = sel.toString().trim();242 var text = sel.toString().trim();
179 if (!text) return;243 if (!text) return;
@@ -181,12 +245,20 @@
181 var body = e.target.closest('.article-body');245 var body = e.target.closest('.article-body');
182 if (!body) return;246 if (!body) return;
183 247
184- if (text.length > 1000) text = text.substring(0, 1000);248+ openForSelection(sel, text);
185- quoteInput.value = text;249+ });
186- quotePreview.textContent = text;250+
187- quoteHighlight.classList.remove('hidden');251+ closeBtn.addEventListener('click', close);
188- form.scrollIntoView({ behavior: 'smooth', block: 'center' });252+
189- form.querySelector('textarea[name="note"]').focus();253+ document.addEventListener('mousedown', function(e) {
254+ if (popover.classList.contains('hidden')) return;
255+ if (popover.contains(e.target)) return;
256+ if (e.target.closest('.article-body')) return;
257+ close();
258+ });
259+
260+ document.addEventListener('keydown', function(e) {
261+ if (e.key === 'Escape' && !popover.classList.contains('hidden')) close();
190 });262 });
191 263
192 var origReset = form.reset.bind(form);264 var origReset = form.reset.bind(form);
@@ -196,6 +268,10 @@
196 quoteHighlight.classList.add('hidden');268 quoteHighlight.classList.add('hidden');
197 };269 };
198 270
271+ form.addEventListener('htmx:afterRequest', function(e) {
272+ if (e.detail && e.detail.successful) close();
273+ });
274+
199 function highlightQuotes() {275 function highlightQuotes() {
200 var el = document.getElementById('annotation-quotes');276 var el = document.getElementById('annotation-quotes');
201 if (!el) return;277 if (!el) return;