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.

Fix annotation hover

+134 -88
+67 -63
docs/design.md
··· 12 12 13 13 **Color-block rhythm (landing page):** Cream/forest hero → white card sections → House Green (`#1E3932`) feature band with white text → cream utility zone → House Green footer. 14 14 15 + Logo is a stylized bee: body with horizontal stripes (like text lines on a page), semi-transparent wings that evoke open book pages, round eyes, curved antennae, and a small smile. The bee represents gleaning (collecting nectar/knowledge), social behavior (hives/communities), and reading (the striped body reads like lines of text, wings like turning pages). 16 + 15 17 ## 2. Color Palette 16 18 17 19 ### Primary Greens 18 20 19 - | Name | Hex | Role | 20 - | --------------- | --------- | ------------------------------------------------ | 21 - | Green Accent | `#00754A` | CTAs, active states, link hovers, brand accent | 22 - | Green Dark | `#006241` | Headings on landing page, stronger brand moments | 23 - | House Green | `#1E3932` | Dark bands, footer, feature sections | 24 - | Green Uplift | `#2b5148` | Decorative accents, mid-dark green | 25 - | Green Light | `#d4e9e2` | Light green utility surfaces, valid-state tints | 21 + | Name | Hex | Role | 22 + | ------------ | --------- | ------------------------------------------------ | 23 + | Green Accent | `#00754A` | CTAs, active states, link hovers, brand accent | 24 + | Green Dark | `#006241` | Headings on landing page, stronger brand moments | 25 + | House Green | `#1E3932` | Dark bands, footer, feature sections | 26 + | Green Uplift | `#2b5148` | Decorative accents, mid-dark green | 27 + | Green Light | `#d4e9e2` | Light green utility surfaces, valid-state tints | 26 28 27 29 ### Dark Theme (default) 28 30 29 - | Token | Value | Use | 30 - | ------------------ | ------------------------------ | ------------------------------------ | 31 - | `--spot-bg` | `#0f1f1a` | Page background, sidebar | 32 - | `--spot-surface` | `#152b24` | Card background | 33 - | `--spot-hover` | `#1a362e` | Hover state, input background | 34 - | `--spot-text` | `#ffffff` | Primary text | 35 - | `--spot-secondary` | `rgba(255,255,255,0.70)` | Secondary/metadata text | 36 - | `--spot-body` | `rgba(255,255,255,0.87)` | Body copy, article content | 37 - | `--spot-muted` | `rgba(255,255,255,0.25)` | Disabled/tertiary text | 38 - | `--spot-divider` | `rgba(255,255,255,0.08)` | Borders, dividers | 39 - | `--spot-outline` | `rgba(255,255,255,0.20)` | Button borders, input borders | 31 + | Token | Value | Use | 32 + | ------------------ | ------------------------ | ----------------------------- | 33 + | `--spot-bg` | `#0f1f1a` | Page background, sidebar | 34 + | `--spot-surface` | `#152b24` | Card background | 35 + | `--spot-hover` | `#1a362e` | Hover state, input background | 36 + | `--spot-text` | `#ffffff` | Primary text | 37 + | `--spot-secondary` | `rgba(255,255,255,0.70)` | Secondary/metadata text | 38 + | `--spot-body` | `rgba(255,255,255,0.87)` | Body copy, article content | 39 + | `--spot-muted` | `rgba(255,255,255,0.25)` | Disabled/tertiary text | 40 + | `--spot-divider` | `rgba(255,255,255,0.08)` | Borders, dividers | 41 + | `--spot-outline` | `rgba(255,255,255,0.20)` | Button borders, input borders | 40 42 41 43 ### Light Theme 42 44 43 - | Token | Value | Use | 44 - | ------------------ | ------------------------------ | ------------------------------------ | 45 - | `--spot-bg` | `#f2f0eb` | Page canvas (warm cream) | 46 - | `--spot-surface` | `#ffffff` | Card background | 47 - | `--spot-hover` | `#edebe9` | Hover state (ceramic) | 48 - | `--spot-text` | `rgba(0,0,0,0.87)` | Primary text (warm black) | 49 - | `--spot-secondary` | `rgba(0,0,0,0.58)` | Secondary/metadata text | 50 - | `--spot-body` | `rgba(0,0,0,0.70)` | Body copy | 51 - | `--spot-muted` | `rgba(0,0,0,0.25)` | Disabled/tertiary text | 52 - | `--spot-divider` | `rgba(0,0,0,0.08)` | Borders, dividers | 53 - | `--spot-outline` | `rgba(0,0,0,0.15)` | Button borders, input borders | 45 + | Token | Value | Use | 46 + | ------------------ | ------------------ | ----------------------------- | 47 + | `--spot-bg` | `#f2f0eb` | Page canvas (warm cream) | 48 + | `--spot-surface` | `#ffffff` | Card background | 49 + | `--spot-hover` | `#edebe9` | Hover state (ceramic) | 50 + | `--spot-text` | `rgba(0,0,0,0.87)` | Primary text (warm black) | 51 + | `--spot-secondary` | `rgba(0,0,0,0.58)` | Secondary/metadata text | 52 + | `--spot-body` | `rgba(0,0,0,0.70)` | Body copy | 53 + | `--spot-muted` | `rgba(0,0,0,0.25)` | Disabled/tertiary text | 54 + | `--spot-divider` | `rgba(0,0,0,0.08)` | Borders, dividers | 55 + | `--spot-outline` | `rgba(0,0,0,0.15)` | Button borders, input borders | 54 56 55 57 ### Semantic 56 58 57 - | Name | Hex | Use | 58 - | ----- | --------- | ---------------- | 59 - | Red | `#c82014` | Errors, likes | 60 - | Orange| `#ffa42b` | Ratings, warnings| 61 - | Blue | `#539df5` | External links | 59 + | Name | Hex | Use | 60 + | ------ | --------- | ----------------- | 61 + | Red | `#c82014` | Errors, likes | 62 + | Orange | `#ffa42b` | Ratings, warnings | 63 + | Blue | `#539df5` | External links | 62 64 63 65 ## 3. Typography 64 66 ··· 66 68 67 69 **Global:** `letter-spacing: -0.01em` on body 68 70 69 - | Role | Size | Weight | Tailwind Class | 70 - | ------------- | ---------- | ------ | ------------------ | 71 - | Page title | 24px | 700 | `text-2xl font-bold` | 72 - | Section title | 18px | 600 | `text-lg font-semibold` | 73 - | Body | 14px | 400 | `text-sm` | 74 - | Small/meta | 12px | 400 | `text-xs` | 75 - | Button label | 14px | 700 | `text-sm font-bold uppercase tracking-button` | 76 - | Micro | 10px | 400 | `text-[10px]` | 71 + | Role | Size | Weight | Tailwind Class | 72 + | ------------- | ---- | ------ | --------------------------------------------- | 73 + | Page title | 24px | 700 | `text-2xl font-bold` | 74 + | Section title | 18px | 600 | `text-lg font-semibold` | 75 + | Body | 14px | 400 | `text-sm` | 76 + | Small/meta | 12px | 400 | `text-xs` | 77 + | Button label | 14px | 700 | `text-sm font-bold uppercase tracking-button` | 78 + | Micro | 10px | 400 | `text-[10px]` | 77 79 78 80 ## 4. Components 79 81 ··· 82 84 All buttons use full-pill radius (`rounded-pill` = `9999px`). 83 85 84 86 **Primary Filled:** 87 + 85 88 ``` 86 89 bg-spot-green text-white rounded-pill px-5 py-2 text-sm font-bold uppercase tracking-button hover:brightness-110 transition 87 90 ``` 88 91 89 92 **Primary Outlined:** 93 + 90 94 ``` 91 95 border border-spot-outline text-spot-text rounded-pill px-4 py-1.5 text-xs font-bold uppercase tracking-button hover:border-spot-text transition 92 96 ``` ··· 101 105 102 106 ### Shadows 103 107 104 - | Token | Value | Use | 105 - | -------------- | ---------------------------------------------------- | ------------ | 106 - | `shadow-spot` | `0 0 0.5px rgba(0,0,0,0.14), 0 1px 1px rgba(0,0,0,0.24)` | Cards | 107 - | `shadow-spot-heavy` | `0 0 6px rgba(0,0,0,0.24), 0 8px 12px rgba(0,0,0,0.14)` | Modals, hero | 108 + | Token | Value | Use | 109 + | ------------------- | -------------------------------------------------------- | ------------ | 110 + | `shadow-spot` | `0 0 0.5px rgba(0,0,0,0.14), 0 1px 1px rgba(0,0,0,0.24)` | Cards | 111 + | `shadow-spot-heavy` | `0 0 6px rgba(0,0,0,0.24), 0 8px 12px rgba(0,0,0,0.14)` | Modals, hero | 108 112 109 113 ### Navigation 110 114 ··· 134 138 135 139 ### Responsive Breakpoints 136 140 137 - | Name | Width | Nav behavior | 138 - | ------- | --------- | ----------------------------------- | 139 - | Mobile | < 768px | Bottom tab nav, stacked layouts | 140 - | Tablet | 768–1023px| Bottom nav, wider gutters | 141 - | Desktop | 1024px+ | Sidebar nav, 3-column grids | 141 + | Name | Width | Nav behavior | 142 + | ------- | ---------- | ------------------------------- | 143 + | Mobile | < 768px | Bottom tab nav, stacked layouts | 144 + | Tablet | 768–1023px | Bottom nav, wider gutters | 145 + | Desktop | 1024px+ | Sidebar nav, 3-column grids | 142 146 143 147 ## 6. Tailwind Config 144 148 ··· 154 158 155 159 ## 8. Page Structure 156 160 157 - | Page | Layout | Key Features | 158 - | -------------- | ------------------------------ | --------------------------------------- | 159 - | Index (landing)| Full-width, no sidebar | Hero with mockup, feature cols, dark band, CTA | 160 - | Login | Centered card | Bluesky + Atmosphere sign-in buttons | 161 - | Dashboard | 2/3 + 1/3 grid | Articles + trending/recommendations sidebar | 162 - | Articles | Full-width list | Keyboard nav (j/k/o/m), mark-all-read | 163 - | Article Detail | `max-w-3xl` centered | Content, like/share/read buttons, annotations | 164 - | Feeds | 2/3 + 1/3 grid | Feed list with categories + add/import sidebar | 165 - | Trending | Full-width list | Like/annotation counts on each article | 166 - | Discover | Mixed grid | Recommendations + people + browse all | 167 - | Annotations | Full-width list | Filter by article URL, load more | 168 - | Profile | `max-w-2xl` centered | Avatar, stats, feeds, annotations | 161 + | Page | Layout | Key Features | 162 + | --------------- | ---------------------- | ---------------------------------------------- | 163 + | Index (landing) | Full-width, no sidebar | Hero with mockup, feature cols, dark band, CTA | 164 + | Login | Centered card | Bluesky + Atmosphere sign-in buttons | 165 + | Dashboard | 2/3 + 1/3 grid | Articles + trending/recommendations sidebar | 166 + | Articles | Full-width list | Keyboard nav (j/k/o/m), mark-all-read | 167 + | Article Detail | `max-w-3xl` centered | Content, like/share/read buttons, annotations | 168 + | Feeds | 2/3 + 1/3 grid | Feed list with categories + add/import sidebar | 169 + | Trending | Full-width list | Like/annotation counts on each article | 170 + | Discover | Mixed grid | Recommendations + people + browse all | 171 + | Annotations | Full-width list | Filter by article URL, load more | 172 + | Profile | `max-w-2xl` centered | Avatar, stats, feeds, annotations |
+33 -9
internal/tmpl/article_detail.html
··· 66 66 <section> 67 67 <h2 class="text-lg font-semibold text-spot-text mb-4">Annotations</h2> 68 68 69 - <form hx-post="/library/create" hx-target="#annotations-list" hx-swap="beforeend" 69 + <form id="annotation-form" hx-post="/library/create" hx-target="#annotations-list" hx-swap="beforeend" 70 70 hx-on::after-request="this.reset()" 71 71 class="bg-spot-surface rounded-xl shadow-spot p-4 mb-4 space-y-3"> 72 72 {{csrfInput .CSRFToken}} 73 73 <input type="hidden" name="feed_url" value="{{.Article.FeedURL}}"> 74 74 <input type="hidden" name="article_url" value="{{if .Article.URL.Valid}}{{.Article.URL.String}}{{end}}"> 75 + <div id="quote-highlight" class="hidden mb-2"> 76 + <blockquote id="quote-preview" class="border-l-2 border-spot-green pl-3 text-sm text-spot-secondary italic"></blockquote> 77 + </div> 78 + <input type="hidden" name="quote" id="quote-input"> 75 79 <div> 76 80 <textarea name="note" rows="2" placeholder="Add a note..." 77 81 class="w-full bg-spot-hover text-spot-text rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder resize-none"></textarea> 78 82 </div> 79 83 <div class="flex gap-2"> 80 - <input type="text" name="quote" placeholder="Quote (optional)" 81 - class="flex-1 bg-spot-hover text-spot-text rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder"> 82 84 <input type="text" name="tags" placeholder="Tags (comma separated)" 83 85 class="flex-1 bg-spot-hover text-spot-text rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder"> 86 + <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> 84 87 </div> 85 - <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> 86 88 </form> 87 89 88 90 <div id="annotations-list" class="space-y-3"> ··· 97 99 98 100 <script> 99 101 (function() { 100 - var readScrollPos = 0; 101 - var articleBody = document.querySelector('.article-body'); 102 - if (articleBody) { 103 - readScrollPos = window.scrollY; 104 - } 102 + var form = document.getElementById('annotation-form'); 103 + var quoteInput = document.getElementById('quote-input'); 104 + var quotePreview = document.getElementById('quote-preview'); 105 + var quoteHighlight = document.getElementById('quote-highlight'); 106 + 107 + document.addEventListener('mouseup', function(e) { 108 + var sel = window.getSelection(); 109 + var text = sel.toString().trim(); 110 + if (!text) return; 111 + 112 + var body = e.target.closest('.article-body'); 113 + if (!body) return; 114 + 115 + if (text.length > 1000) text = text.substring(0, 1000); 116 + quoteInput.value = text; 117 + quotePreview.textContent = text; 118 + quoteHighlight.classList.remove('hidden'); 119 + form.scrollIntoView({ behavior: 'smooth', block: 'center' }); 120 + form.querySelector('textarea[name="note"]').focus(); 121 + }); 122 + 123 + var origReset = form.reset.bind(form); 124 + form.reset = function() { 125 + origReset(); 126 + quoteInput.value = ''; 127 + quoteHighlight.classList.add('hidden'); 128 + }; 105 129 106 130 document.addEventListener('keydown', function(e) { 107 131 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
+27 -7
internal/tmpl/base.html
··· 220 220 } 221 221 updateThemeIcons(document.documentElement.getAttribute('data-theme') || 'dark'); 222 222 223 - function toggleAnnotate(btn) { 224 - var card = btn.closest('article'); 225 - var form = card.querySelector('.annotate-form'); 226 - form.classList.toggle('hidden'); 227 - if (!form.classList.contains('hidden')) { 228 - form.querySelector('input[name="quote"]').focus(); 229 - } 223 + function closeAnnotate(el) { 224 + var form = el.closest('.annotate-form'); 225 + form.classList.add('hidden'); 226 + form.querySelector('form').reset(); 230 227 } 228 + 229 + function openAnnotate(article, quote) { 230 + var form = article.querySelector('.annotate-form'); 231 + var quoteInput = form.querySelector('input[name="quote"]'); 232 + if (quote) quoteInput.value = quote; 233 + form.classList.remove('hidden'); 234 + (quote ? form.querySelector('input[name="note"]') : quoteInput).focus(); 235 + } 236 + 237 + document.addEventListener('mouseup', function(e) { 238 + var sel = window.getSelection(); 239 + var text = sel.toString().trim(); 240 + if (!text) return; 241 + 242 + var article = e.target.closest('article[data-article-id]'); 243 + if (!article) return; 244 + 245 + var form = article.querySelector('.annotate-form'); 246 + if (form && !form.classList.contains('hidden')) return; 247 + 248 + if (text.length > 500) text = text.substring(0, 500); 249 + openAnnotate(article, text); 250 + }); 231 251 232 252 document.addEventListener('keydown', function(e) { 233 253 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
+7 -9
internal/tmpl/partials/article-card.html
··· 1 1 {{define "article-card.html"}} 2 - <article data-article-id="{{.ID}}" class="group bg-spot-surface rounded-xl px-5 py-4 hover:bg-spot-hover-50 transition shadow-spot relative"> 2 + <article data-article-id="{{.ID}}" data-feed-url="{{.FeedURL}}" data-article-url="{{if .URL.Valid}}{{.URL.String}}{{end}}" class="bg-spot-surface rounded-xl px-5 py-4 hover:bg-spot-hover-50 transition shadow-spot relative"> 3 3 <div class="flex items-start justify-between gap-4"> 4 4 <div class="min-w-0 flex-1"> 5 5 <a href="/articles/{{.ID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a> ··· 14 14 </div> 15 15 <div class="flex flex-col items-center gap-1 shrink-0"> 16 16 {{template "like-button.html" .}} 17 - <button type="button" onclick="toggleAnnotate(this)" class="opacity-0 group-hover:opacity-100 text-spot-muted hover:text-spot-green text-xs transition mt-1" title="Annotate"> 18 - <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="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"/></svg> 19 - </button> 20 17 </div> 21 18 </div> 22 19 <div class="annotate-form hidden mt-3 pt-3 border-t border-spot-divider"> 23 - <form hx-post="/library/create" hx-swap="none" hx-on::after-request="toggleAnnotate(this.closest('.annotate-form').previousElementSibling)"> 20 + <form hx-post="/library/create" hx-swap="none" hx-on::after-request="closeAnnotate(this)" class="space-y-2"> 24 21 <input type="hidden" name="feed_url" value="{{.FeedURL}}"> 25 22 <input type="hidden" name="article_url" value="{{if .URL.Valid}}{{.URL.String}}{{end}}"> 23 + <input type="text" name="quote" placeholder="Quote a passage..." 24 + class="w-full bg-spot-hover text-spot-text rounded-pill px-4 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder"> 26 25 <div class="flex gap-2"> 27 - <input type="text" name="quote" placeholder="Quote a passage..." 28 - class="flex-1 bg-spot-hover text-spot-text rounded-pill px-4 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder"> 29 - <input type="text" name="note" placeholder="Note (optional)" 26 + <input type="text" name="note" placeholder="Add a note..." 30 27 class="flex-1 bg-spot-hover text-spot-text rounded-pill px-4 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-spot-green placeholder:text-spot-placeholder"> 31 - <button type="submit" class="bg-spot-green text-white rounded-pill px-4 py-1.5 text-xs font-bold uppercase tracking-button hover:brightness-110 transition">Save</button> 28 + <button type="submit" class="bg-spot-green text-white rounded-pill px-4 py-1.5 text-xs font-bold uppercase tracking-button hover:brightness-110 transition">Annotate</button> 29 + <button type="button" onclick="closeAnnotate(this)" class="text-spot-secondary hover:text-spot-text px-2 text-sm transition">&times;</button> 32 30 </div> 33 31 </form> 34 32 </div>