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.

Improve cards and buttons in UI and simplify theme

+123 -125
+12
docs/design.md
··· 91 91 bg-spot-green text-white rounded-pill px-5 py-2 text-sm font-bold uppercase tracking-button hover:brightness-110 transition 92 92 ``` 93 93 94 + Used for CTAs: "Add", "Subscribe", "Annotate", "Save", "Sign in", "Get started", "Login". 95 + 94 96 **Primary Outlined:** 95 97 96 98 ``` 97 99 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 98 100 ``` 101 + 102 + Used for secondary actions: "Refresh feeds", "Mark all read", "Import OPML", "Export OPML", "Fetch full content", "See what's trending", dialog Cancel/Close. 103 + 104 + **Toolbar Micro:** 105 + 106 + ``` 107 + text-spot-text bg-spot-hover text-[10px] uppercase tracking-button px-2.5 py-1 rounded-pill transition 108 + ``` 109 + 110 + Used in-card and in-toolbar for small actions: Like, Read, Original, Share. Default text is `text-spot-text` (never grey). Semantic hover colors override the default on certain buttons: Like → red, Read → green, Share → blue, Original stays `text-spot-text`. 99 111 100 112 ### Cards 101 113
+13 -10
internal/tmpl/article_detail.html
··· 33 33 34 34 <div class="flex items-center gap-2 mt-6"> 35 35 <button hx-post="/articles/{{.Article.ID}}/like?bordered=true" hx-target="this" hx-swap="outerHTML" 36 - class="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 inline-flex items-center gap-1.5"> 37 - <svg class="w-3.5 h-3.5 {{if .HasLiked}}text-spot-red{{else}}text-spot-muted{{end}}" fill="{{if .HasLiked}}currentColor{{else}}none{{end}}" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">{{template "icon-heart"}}</svg> 36 + title="{{if .HasLiked}}Unlike{{else}}Like{{end}}" 37 + class="group inline-flex items-center gap-1.5 text-[10px] uppercase tracking-button px-2.5 py-1 rounded-pill transition {{if .HasLiked}}text-spot-red bg-spot-red/15 hover:bg-spot-red/25{{else}}text-spot-text bg-spot-hover hover:text-spot-red hover:bg-spot-red/15{{end}}"> 38 + <svg class="w-3.5 h-3.5" fill="{{if .HasLiked}}currentColor{{else}}none{{end}}" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">{{template "icon-heart"}}</svg> 38 39 <span>{{.LikeCount}}</span> 39 40 </button> 40 41 41 42 <button hx-post="/articles/{{.Article.ID}}/{{if .ReadState.IsRead}}unread{{else}}read{{end}}" 42 43 hx-target="#read-btn" hx-swap="outerHTML" 43 44 id="read-btn" 44 - class="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"> 45 - {{if .ReadState.IsRead}}Mark unread{{else}}Mark read{{end}} 45 + title="{{if .ReadState.IsRead}}Mark as Unread{{else}}Mark as Read{{end}}" 46 + class="group inline-flex items-center gap-1.5 text-[10px] uppercase tracking-button px-2.5 py-1 rounded-pill transition {{if .ReadState.IsRead}}text-spot-text bg-spot-hover hover:text-spot-green hover:bg-spot-green/15{{else}}text-spot-text bg-spot-hover hover:text-spot-green hover:bg-spot-green/15{{end}}"> 47 + <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg> 48 + <span>{{if .ReadState.IsRead}}Unread{{else}}Read{{end}}</span> 46 49 </button> 47 50 48 51 {{if .Article.URL.Valid}} 49 - <a href="{{.Article.URL.String}}" target="_blank" rel="noopener noreferrer" 50 - class="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 inline-flex items-center gap-1.5"> 52 + <a href="{{.Article.URL.String}}" target="_blank" rel="noopener noreferrer" title="Open original" 53 + class="group inline-flex items-center gap-1.5 text-[10px] text-spot-text uppercase tracking-button px-2.5 py-1 rounded-pill bg-spot-hover hover:brightness-110 transition"> 51 54 <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-4.5-6H18m0 0v4.5m0-4.5L10.5 13.5"/></svg> 52 - Original 55 + <span>Original</span> 53 56 </a> 54 57 55 58 <a href="https://bsky.app/intent/compose?text={{.Article.Title}}%20{{.Article.URL.String}}" 56 - target="_blank" rel="noopener noreferrer" 57 - class="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 inline-flex items-center gap-1.5"> 59 + target="_blank" rel="noopener noreferrer" title="Share on Bluesky" 60 + class="group inline-flex items-center gap-1.5 text-[10px] text-spot-text uppercase tracking-button px-2.5 py-1 rounded-pill bg-spot-hover hover:text-spot-blue hover:bg-spot-blue/15 transition"> 58 61 <span class="w-3.5 h-3.5 inline-flex shrink-0">{{template "icon-bluesky"}}</span> 59 - Share 62 + <span>Share</span> 60 63 </a> 61 64 {{end}} 62 65 </div>
+25 -49
internal/tmpl/base.html
··· 6 6 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> 7 7 <title>Glean</title> 8 8 <script> 9 - (function(){var p=localStorage.getItem('theme')||'system';var r=p==='system'?(window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light'):p;document.documentElement.setAttribute('data-theme',r);var s=localStorage.getItem('shape');if(s)document.documentElement.setAttribute('data-shape',s)})(); 9 + (function(){var p=localStorage.getItem('theme')||'system';var r=p==='system'?(window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light'):p;document.documentElement.setAttribute('data-theme',r)})(); 10 10 </script> 11 11 <link rel="stylesheet" href="/static/output.css"> 12 12 <script src="/static/htmx.min.js"></script> ··· 46 46 <dialog id="confirm-dialog" class="bg-spot-surface rounded-xl p-6 max-w-sm shadow-spot-elevated border border-spot-divider backdrop:bg-black/50" onclick="if(event.target===this)this.close()"> 47 47 <p id="confirm-dialog-msg" class="text-spot-text text-sm mb-6"></p> 48 48 <div class="flex justify-end gap-3"> 49 - <button onclick="document.getElementById('confirm-dialog').close(false)" class="text-sm text-spot-secondary hover:text-spot-text px-4 py-2 rounded-pill border border-spot-outline transition">Cancel</button> 49 + <button onclick="document.getElementById('confirm-dialog').close(false)" class="text-sm text-spot-text px-4 py-2 rounded-pill border border-spot-outline hover:border-spot-text transition">Cancel</button> 50 50 <button id="confirm-dialog-ok" class="text-sm text-white bg-spot-red hover:brightness-110 px-4 py-2 rounded-pill font-bold uppercase tracking-button transition">Confirm</button> 51 51 </div> 52 52 </dialog> ··· 69 69 <kbd class="text-spot-text bg-spot-hover rounded px-1.5 py-0.5 text-xs font-mono text-center min-w-[28px]">m</kbd><span class="text-spot-secondary">Toggle read</span> 70 70 </div> 71 71 </div> 72 - <button onclick="document.getElementById('shortcuts-dialog').close()" class="mt-4 w-full text-sm text-spot-secondary hover:text-spot-text px-4 py-2 rounded-pill border border-spot-outline transition">Close</button> 72 + <button onclick="document.getElementById('shortcuts-dialog').close()" class="mt-4 w-full text-sm text-spot-text px-4 py-2 rounded-pill border border-spot-outline hover:border-spot-text transition">Close</button> 73 73 </dialog> 74 74 {{if or .User (or (eq .ActivePath "/trending") (eq .ActivePath "/stats"))}} 75 75 <aside class="hidden lg:flex flex-col w-60 bg-spot-bg h-screen fixed left-0 top-0 px-3 py-4 z-20"> ··· 120 120 </div> 121 121 </aside> 122 122 123 - <nav class="lg:hidden fixed bottom-0 left-0 right-0 bg-spot-surface border-t border-spot-divider z-30 px-2 py-2"> 123 + <nav id="mobile-nav" class="lg:hidden fixed bottom-0 left-0 right-0 bg-spot-surface/80 backdrop-blur-xl border-t border-spot-divider z-30 px-2 py-2"> 124 124 <div class="flex items-center justify-around"> 125 125 <a href="/dashboard" class="flex flex-col items-center gap-0.5 {{activeClass .ActivePath "/dashboard"}} text-[10px] px-2 py-1"> 126 126 <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 01-1 1"/></svg> ··· 134 134 <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg> 135 135 Trending 136 136 </a> 137 + <a href="/feeds" class="flex flex-col items-center gap-0.5 {{activeClass .ActivePath "/feeds"}} text-[10px] px-2 py-1"> 138 + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 5c7.18 0 13 5.82 13 13M6 11a7 7 0 017 7m-7-1a1 1 0 11-2 0 1 1 0 012 0z"/></svg> 139 + Feeds 140 + </a> 141 + <a href="/library" class="flex flex-col items-center gap-0.5 {{activeClass .ActivePath "/library"}} text-[10px] px-2 py-1"> 142 + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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> 143 + Library 144 + </a> 137 145 </div> 138 146 </nav> 139 147 {{end}} ··· 196 204 <span class="theme-text-light hidden">Light</span> 197 205 <span class="theme-text-system hidden">System</span> 198 206 </button> 199 - <button onclick="toggleShape()" class="text-xs text-spot-secondary hover:text-spot-text hover:underline inline-flex gap-1.5 items-center transition text-left"> 200 - <svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4h16v16H4z"/></svg> 201 - <span class="shape-text">Squared</span> 202 - </button> 203 - <button onclick="document.getElementById('shortcuts-dialog').showModal()" class="text-xs text-spot-secondary hover:text-spot-text hover:underline inline-flex gap-1.5 items-center transition text-left"> 204 - <svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z"/></svg> 205 - Shortcuts 206 - </button> 207 + <button onclick="document.getElementById('shortcuts-dialog').showModal()" class="text-xs text-spot-secondary hover:text-spot-text hover:underline inline-flex gap-1.5 items-center transition text-left"> 208 + <svg class="w-3.5 h-3.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z"/></svg> 209 + Shortcuts 210 + </button> 207 211 </div> 208 212 </div> 209 213 <div class="text-right"> ··· 228 232 </div> 229 233 <div class="border-t border-spot-divider pt-4"> 230 234 <div class="flex flex-wrap items-center gap-3"> 231 - <button onclick="toggleTheme()" class="text-xs text-spot-secondary hover:text-spot-text inline-flex gap-1.5 items-center transition"> 232 - <svg class="w-3.5 h-3.5 theme-icon-dark" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/></svg> 233 - <svg class="w-3.5 h-3.5 theme-icon-light hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/></svg> 234 - <svg class="w-3.5 h-3.5 theme-icon-system hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25A2.25 2.25 0 0 1 5.25 3h13.5A2.25 2.25 0 0 1 21 5.25Z"/></svg> 235 - <span class="theme-text-dark">Dark</span> 236 - <span class="theme-text-light hidden">Light</span> 237 - <span class="theme-text-system hidden">System</span> 238 - </button> 239 - <button onclick="toggleShape()" class="text-xs text-spot-secondary hover:text-spot-text inline-flex gap-1.5 items-center transition"> 240 - <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4h16v16H4z"/></svg> 241 - <span class="shape-text">Squared</span> 242 - </button> 243 - </div> 235 + <button onclick="toggleTheme()" class="text-xs text-spot-secondary hover:text-spot-text inline-flex gap-1.5 items-center transition"> 236 + <svg class="w-3.5 h-3.5 theme-icon-dark" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/></svg> 237 + <svg class="w-3.5 h-3.5 theme-icon-light hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/></svg> 238 + <svg class="w-3.5 h-3.5 theme-icon-system hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25A2.25 2.25 0 0 1 5.25 3h13.5A2.25 2.25 0 0 1 21 5.25Z"/></svg> 239 + <span class="theme-text-dark">Dark</span> 240 + <span class="theme-text-light hidden">Light</span> 241 + <span class="theme-text-system hidden">System</span> 242 + </button> 243 + </div> 244 244 <div class="border-t border-spot-divider mt-4 pt-3"> 245 245 <span class="text-xs text-spot-secondary">&copy; {{now.Format "2006"}} <a href="https://bsky.app/profile/julien.rbrt.fr" class="hover:text-spot-text transition">julien.rbrt.fr</a> &middot; Made in Europe &#127466;&#127482; &middot; <a href="/terms" class="hover:text-spot-text transition">Terms</a></span> 246 246 </div> ··· 276 276 applyTheme(); 277 277 } 278 278 279 - function applyShape() { 280 - var shapePref = localStorage.getItem('shape') || 'rounded'; 281 - if (shapePref === 'squared') { 282 - document.documentElement.setAttribute('data-shape', 'squared'); 283 - } else { 284 - document.documentElement.removeAttribute('data-shape'); 285 - } 286 - document.querySelectorAll('.shape-text').forEach(function(el) { 287 - el.textContent = shapePref === 'squared' ? 'Rounded' : 'Squared'; 288 - }); 289 - } 290 - 291 - function toggleShape() { 292 - var current = localStorage.getItem('shape') || 'rounded'; 293 - var next = current === 'squared' ? 'rounded' : 'squared'; 294 - if (next === 'rounded') { 295 - localStorage.removeItem('shape'); 296 - } else { 297 - localStorage.setItem('shape', next); 298 - } 299 - applyShape(); 300 - } 301 - 302 279 window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() { 303 280 if (themePref === 'system') applyTheme(); 304 281 }); 305 282 306 283 applyTheme(); 307 - applyShape(); 308 284 309 285 // reload page when page restored from bfcache. 310 286 // useful for reloading article read/unread status.
+1 -1
internal/tmpl/login.html
··· 34 34 </div> 35 35 36 36 <a href="/auth/register" 37 - class="w-full flex items-center justify-center gap-3 border border-spot-outline rounded-pill px-4 py-3.5 text-sm font-bold uppercase tracking-button hover:bg-spot-hover-50 transition"> 37 + class="w-full flex items-center justify-center gap-3 border border-spot-outline text-spot-text rounded-pill px-4 py-3.5 text-sm font-bold uppercase tracking-button hover:bg-spot-hover-50 transition"> 38 38 <svg class="h-5 w-auto" fill="currentColor" viewBox="77.86 113.9 129.15 129.15" xmlns="http://www.w3.org/2000/svg"><path d="M148.846 144.562C148.846 159.75 161.158 172.062 176.346 172.062H207.012V185.865H176.346C161.158 185.865 148.846 198.177 148.846 213.365V243.045H136.029V213.365C136.029 198.177 123.717 185.865 108.529 185.865H77.8633V172.062H108.529C123.717 172.062 136.029 159.75 136.029 144.562V113.896H148.846V144.562Z"/></svg> 39 39 Register with Eurosky 40 40 </a>
+25 -17
internal/tmpl/partials/article-card.html
··· 2 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 {{if .IsRead.Bool}}opacity-60{{end}}"> 3 3 <div class="flex items-start justify-between gap-4"> 4 4 <div class="min-w-0 flex-1"> 5 - <div class="flex items-center gap-2"> 6 - {{if not .IsRead.Bool}}<span class="w-2 h-2 rounded-full bg-spot-green shrink-0"></span>{{end}} 7 - <a href="/articles/{{.ID}}{{.NavSuffix}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a> 5 + <div class="flex items-start gap-2.5"> 6 + {{if not .IsRead.Bool}}<span class="w-2.5 h-2.5 rounded-full bg-spot-green shrink-0 mt-1.5"></span>{{end}} 7 + <a href="/articles/{{.ID}}{{.NavSuffix}}" class="font-semibold text-spot-text hover:text-spot-green transition text-[17px] leading-snug">{{.Title}}</a> 8 8 </div> 9 - <div class="text-sm text-spot-secondary mt-1 flex items-center gap-2"> 10 - {{template "favicon" dict "src" .FeedFaviconURL.String "size" "w-4 h-4"}} 11 - {{if .Author.Valid}}{{if .Author.String}}<span>{{.Author.String}}</span>{{end}}{{end}} 12 - {{if .Published.Valid}}<span>{{.Published.Time.Format "Jan 2, 2006 15:04"}}</span>{{end}} 13 - <span class="text-spot-muted"><a href="/articles?feed={{.FeedURL}}" class="hover:text-spot-green transition">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</a></span> 9 + <div class="text-xs text-spot-secondary mt-2 flex items-center gap-1.5 flex-wrap pl-5"> 10 + {{template "favicon" dict "src" .FeedFaviconURL.String "size" "w-3.5 h-3.5"}} 11 + <a href="/articles?feed={{.FeedURL}}" class="hover:text-spot-text transition font-medium">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</a> 12 + {{if .Author.Valid}}{{if .Author.String}}<span class="text-spot-muted">&middot;</span><span>{{.Author.String}}</span>{{end}}{{end}} 13 + {{if .Published.Valid}}<span class="text-spot-muted">&middot;</span><span>{{.Published.Time.Format "Jan 2, 2006 15:04"}}</span>{{end}} 14 14 </div> 15 15 {{if .Summary.Valid}}{{if .Summary.String}} 16 - <p class="text-sm text-spot-secondary mt-2 line-clamp-2">{{plainText .Summary.String}}</p> 16 + <p class="text-sm text-spot-secondary mt-2.5 line-clamp-2 leading-relaxed pl-5">{{plainText .Summary.String}}</p> 17 17 {{end}}{{end}} 18 18 </div> 19 - <div class="flex flex-col items-center gap-3 shrink-0 pt-1"> 20 - {{template "like-button.html" .}} 19 + <div class="grid gap-2 shrink-0 pt-1"> 20 + <div class="flex justify-center">{{template "like-button.html" .}}</div> 21 21 {{if .IsRead.Bool}} 22 - <span id="read-btn-{{.ID}}" class="text-[10px] text-spot-green uppercase tracking-button">Read</span> 22 + <div class="flex justify-center"> 23 + <span id="read-btn-{{.ID}}" title="Read" class="inline-flex items-center gap-1 text-[10px] text-spot-muted uppercase tracking-button px-2 py-0.5 rounded-pill bg-spot-hover"> 24 + <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg> 25 + </span> 26 + </div> 23 27 {{else}} 24 - <button hx-post="/articles/{{.ID}}/read" hx-target="#read-btn-{{.ID}}" hx-swap="outerHTML" id="read-btn-{{.ID}}" class="text-[10px] text-spot-secondary hover:text-spot-text uppercase tracking-button transition">Mark read</button> 28 + <div class="flex justify-center"> 29 + <button hx-post="/articles/{{.ID}}/read" hx-target="#read-btn-{{.ID}}" hx-swap="outerHTML" id="read-btn-{{.ID}}" title="Mark as Read" class="group inline-flex items-center gap-1 text-[10px] text-spot-text uppercase tracking-button px-2 py-0.5 rounded-pill bg-spot-hover hover:text-spot-green hover:bg-spot-green/15 transition"> 30 + <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg> 31 + </button> 32 + </div> 25 33 {{end}} 26 34 </div> 27 35 </div> 28 - <div class="annotate-form hidden mt-3 pt-3 border-t border-spot-divider"> 36 + <div class="annotate-form hidden mt-4 pt-4 border-t border-spot-divider"> 29 37 <form hx-post="/library/create" hx-swap="none" hx-on::after-request="closeAnnotate(this)" class="space-y-2"> 30 38 <input type="hidden" name="feed_url" value="{{.FeedURL}}"> 31 39 <input type="hidden" name="article_url" value="{{if .URL.Valid}}{{.URL.String}}{{end}}"> 32 40 <input type="text" name="quote" placeholder="Quote a passage..." 33 - 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"> 41 + class="w-full 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"> 34 42 <div class="flex gap-2"> 35 43 <input type="text" name="note" placeholder="Add a note..." 36 - 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"> 37 - <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> 44 + 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"> 45 + <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> 38 46 <button type="button" onclick="closeAnnotate(this)" class="text-spot-secondary hover:text-spot-text px-2 text-sm transition">&times;</button> 39 47 </div> 40 48 </form>
+3 -2
internal/tmpl/partials/like-button.html
··· 1 1 {{define "like-button.html"}} 2 2 <button hx-post="/articles/{{.ID}}/like" hx-target="this" hx-swap="outerHTML" 3 - class="text-spot-text rounded-pill px-2 py-1 text-xs font-bold transition inline-flex items-center gap-1 hover:text-spot-green"> 4 - <svg class="w-4 h-4 {{if .HasLiked}}text-spot-red{{else}}text-spot-muted{{end}}" fill="{{if .HasLiked}}currentColor{{else}}none{{end}}" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">{{template "icon-heart"}}</svg> 3 + title="{{if .HasLiked}}Unlike{{else}}Like{{end}}" 4 + class="group inline-flex items-center gap-1 text-[10px] uppercase tracking-button px-2 py-0.5 rounded-pill transition {{if .HasLiked}}text-spot-red bg-spot-red/15 hover:bg-spot-red/25{{else}}text-spot-text bg-spot-hover hover:text-spot-red hover:bg-spot-red/15{{end}}"> 5 + <svg class="w-3 h-3" fill="{{if .HasLiked}}currentColor{{else}}none{{end}}" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">{{template "icon-heart"}}</svg> 5 6 <span>{{.LikeCount}}</span> 6 7 </button> 7 8 {{end}}
+12 -11
internal/tmpl/partials/recommendation-article-card.html
··· 2 2 <article data-article-url="{{.URL}}" 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 - <div class="flex items-center gap-2"> 6 - {{if not .IsRead}}<span class="w-2 h-2 rounded-full bg-spot-green shrink-0"></span>{{end}} 7 - <a href="/articles/{{.ArticleID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a> 5 + <div class="flex items-start gap-2.5"> 6 + {{if not .IsRead}}<span class="w-2.5 h-2.5 rounded-full bg-spot-green shrink-0 mt-1.5"></span>{{end}} 7 + <a href="/articles/{{.ArticleID}}" class="font-semibold text-spot-text hover:text-spot-green transition text-[17px] leading-snug">{{.Title}}</a> 8 8 </div> 9 - <div class="text-sm text-spot-secondary mt-1 flex items-center gap-2"> 10 - {{if .FaviconURL}}{{template "favicon" dict "src" .FaviconURL "size" "w-4 h-4"}}{{end}} 11 - {{if .Author}}<span>{{.Author}}</span>{{end}} 12 - {{if .Published.Valid}}<span>{{.Published.Time.Format "Jan 2"}}</span>{{end}} 13 - <span class="text-spot-muted"><a href="/articles?feed={{.FeedURL}}" class="hover:text-spot-green transition">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</a></span> 9 + <div class="text-xs text-spot-secondary mt-2 flex items-center gap-1.5 flex-wrap pl-5"> 10 + {{if .FaviconURL}}{{template "favicon" dict "src" .FaviconURL "size" "w-3.5 h-3.5"}}{{end}} 11 + <a href="/articles?feed={{.FeedURL}}" class="hover:text-spot-text transition font-medium">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</a> 12 + {{if .Author}}<span class="text-spot-muted">&middot;</span><span>{{.Author}}</span>{{end}} 13 + {{if .Published.Valid}}<span class="text-spot-muted">&middot;</span><span>{{.Published.Time.Format "Jan 2"}}</span>{{end}} 14 14 </div> 15 - {{if .Summary}}<p class="text-sm text-spot-secondary mt-2 line-clamp-2">{{plainText .Summary}}</p>{{end}} 15 + {{if .Summary}}<p class="text-sm text-spot-secondary mt-2.5 line-clamp-2 leading-relaxed pl-5">{{plainText .Summary}}</p>{{end}} 16 16 </div> 17 17 <div class="shrink-0 pt-1"> 18 18 <button hx-post="/recs/dismiss-article" hx-target="closest article" hx-swap="delete" hx-include="#dismiss-{{.ArticleID}}" 19 - class="text-[10px] text-spot-secondary hover:text-spot-text uppercase tracking-button transition flex items-center gap-1"> 19 + class="text-[10px] text-spot-muted hover:text-spot-text uppercase tracking-button transition flex items-center gap-1" 20 + title="Not interested"> 20 21 <input type="hidden" name="article_url" value="{{.URL}}" id="dismiss-{{.ArticleID}}"> 21 22 <svg class="w-3 h-3" 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> 22 23 Dismiss ··· 24 25 </div> 25 26 </div> 26 27 </article> 27 - {{end}} 28 + {{end}}
+6 -6
internal/tmpl/partials/recommendation-feed-card.html
··· 1 1 {{define "recommendation-feed-card.html"}} 2 - <div class="recommendation-card bg-spot-surface rounded-xl p-3 hover:bg-spot-hover-50 transition"> 2 + <div class="recommendation-card bg-spot-surface rounded-xl p-3 hover:bg-spot-hover-50 transition shadow-spot"> 3 3 <div class="flex items-center justify-between gap-2"> 4 4 <a href="/articles?feed={{.feed_url}}" class="min-w-0 flex items-center gap-2 flex-1"> 5 5 {{template "favicon" dict "src" .favicon_url "size" "w-4 h-4"}} ··· 10 10 </a> 11 11 <div class="flex items-center gap-2 shrink-0"> 12 12 <span class="text-xs text-spot-secondary">{{.subscriber_count}} subs</span> 13 - <form hx-post="/recs/dismiss-feed" hx-target="closest .recommendation-card" hx-swap="outerHTML" class="inline"> 13 + <form hx-post="/recs/dismiss-feed" hx-target="closest .recommendation-card" hx-swap="outerHTML" class="inline-flex items-center"> 14 14 {{csrfInput .CSRFToken}} 15 15 <input type="hidden" name="feed_url" value="{{.feed_url}}"> 16 - <button type="submit" title="Not interested" class="text-spot-muted hover:text-spot-red transition -m-1 p-1 rounded-full hover:bg-spot-hover-50 flex items-center"> 17 - <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg> 16 + <button type="submit" title="Not interested" class="text-spot-muted hover:text-spot-red transition p-0.5 rounded hover:bg-spot-hover inline-flex items-center"> 17 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3.5 h-3.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg> 18 18 </button> 19 19 </form> 20 - <form hx-post="/feeds/add" hx-target="closest .recommendation-card" hx-swap="outerHTML" class="inline"> 20 + <form hx-post="/feeds/add" hx-target="closest .recommendation-card" hx-swap="outerHTML" class="inline-flex items-center"> 21 21 {{csrfInput .CSRFToken}} 22 22 <input type="hidden" name="feed_url" value="{{.feed_url}}"> 23 - <button type="submit" class="text-xs font-bold uppercase tracking-button text-spot-green hover:brightness-110 transition">Subscribe</button> 23 + <button type="submit" title="Subscribe" class="text-xs font-bold uppercase tracking-button text-spot-green hover:brightness-110 transition">Subscribe</button> 24 24 </form> 25 25 </div> 26 26 </div>
+10 -8
internal/tmpl/partials/trending-card.html
··· 2 2 <div class="bg-spot-surface rounded-xl px-5 py-4 hover:bg-spot-hover-50 transition shadow-spot"> 3 3 <div class="flex items-start justify-between gap-4"> 4 4 <div class="min-w-0 flex-1"> 5 - <a href="/articles/{{.ArticleID}}" class="font-bold text-spot-text hover:text-spot-green transition text-lg leading-tight">{{.Title}}</a> 6 - <div class="text-sm text-spot-secondary mt-1 flex items-center gap-2"> 7 - {{template "favicon" dict "src" .FaviconURL "size" "w-4 h-4"}} 8 - {{if .Author}}<span>{{.Author}}</span>{{end}} 9 - <span class="text-spot-muted"><a href="/articles?feed={{.FeedURL}}" class="hover:text-spot-green transition">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</a></span> 5 + <div class="flex items-start gap-2.5"> 6 + <a href="/articles/{{.ArticleID}}" class="font-semibold text-spot-text hover:text-spot-green transition text-[17px] leading-snug">{{.Title}}</a> 7 + </div> 8 + <div class="text-xs text-spot-secondary mt-2 flex items-center gap-1.5 flex-wrap pl-0"> 9 + {{template "favicon" dict "src" .FaviconURL "size" "w-3.5 h-3.5"}} 10 + <a href="/articles?feed={{.FeedURL}}" class="hover:text-spot-text transition font-medium">{{if .FeedTitle}}{{.FeedTitle}}{{else}}{{.FeedURL}}{{end}}</a> 11 + {{if .Author}}<span class="text-spot-muted">&middot;</span><span>{{.Author}}</span>{{end}} 10 12 </div> 11 13 {{if .Summary}} 12 - <p class="text-sm text-spot-secondary mt-2 line-clamp-2">{{plainText .Summary}}</p> 14 + <p class="text-sm text-spot-secondary mt-2.5 line-clamp-2 leading-relaxed pl-0">{{plainText .Summary}}</p> 13 15 {{end}} 14 16 </div> 15 - <div class="flex flex-col items-end shrink-0 gap-0.5"> 17 + <div class="flex flex-col items-end shrink-0 gap-1"> 16 18 {{template "like-button.html" (dict "ID" .ArticleID "HasLiked" .HasLiked "LikeCount" .LikeCount)}} 17 - <span class="text-xs text-spot-muted font-bold inline-flex items-center gap-1 px-2"><svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" role="img" aria-label="Annotation"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.184-4.183a1.14 1.14 0 0 1 .778-.332 48.294 48.294 0 0 0 5.83-.498c1.585-.233 2.708-1.626 2.708-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z"/></svg>{{.AnnotationCount}}</span> 19 + <span title="Annotations" class="inline-flex items-center gap-1 text-[10px] text-spot-text uppercase tracking-button px-2 py-0.5 rounded-pill bg-spot-hover"><svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.184-4.183a1.14 1.14 0 0 1 .778-.332 48.294 48.294 0 0 0 5.83-.498c1.585-.233 2.708-1.626 2.708-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z"/></svg><span>{{.AnnotationCount}}</span></span> 18 20 </div> 19 21 </div> 20 22 </div>
+9 -13
static/input.css
··· 19 19 --spot-placeholder: rgba(255,255,255,0.45); 20 20 --spot-active-bg: #ffffff; 21 21 --spot-active-text: #1E3932; 22 - --radius-sm: 0.5rem; 23 - --radius-md: 0.75rem; 24 - --radius-lg: 1rem; 25 - --radius-pill: 9999px; 26 - --radius-full: 50%; 27 22 --spot-shadow: 0 1px 2px rgba(0,0,0,0.40), 0 2px 4px rgba(0,0,0,0.20); 28 23 --spot-shadow-heavy: 0 4px 8px rgba(0,0,0,0.35), 0 12px 24px rgba(0,0,0,0.25); 29 24 --spot-shadow-elevated: 0 8px 16px rgba(0,0,0,0.40), 0 24px 48px rgba(0,0,0,0.30); ··· 48 43 --spot-shadow: 0 1px 2px rgba(0,0,0,0.06), 0 2px 4px rgba(0,0,0,0.04); 49 44 --spot-shadow-heavy: 0 4px 12px rgba(0,0,0,0.08), 0 12px 28px rgba(0,0,0,0.06); 50 45 --spot-shadow-elevated: 0 8px 24px rgba(0,0,0,0.10), 0 24px 48px rgba(0,0,0,0.08); 51 - } 52 - 53 - [data-shape="squared"] { 54 - --radius-sm: 0; 55 - --radius-md: 0; 56 - --radius-lg: 0; 57 - --radius-pill: 0; 58 46 } 59 47 } 60 48 ··· 108 96 color: var(--spot-text); 109 97 } 110 98 99 + #mobile-nav a.bg-spot-hover { 100 + background: transparent !important; 101 + color: #00754A !important; 102 + } 103 + #mobile-nav a { 104 + transition: color 0.15s ease; 105 + } 106 + 111 107 ::-webkit-scrollbar { width: 8px; } 112 108 ::-webkit-scrollbar-track { background: var(--spot-bg); } 113 - ::-webkit-scrollbar-thumb { background: var(--spot-muted); border-radius: var(--radius-md); } 109 + ::-webkit-scrollbar-thumb { background: var(--spot-muted); border-radius: 6px; } 114 110 ::-webkit-scrollbar-thumb:hover { background: var(--spot-outline); } 115 111 116 112 .htmx-swapping {
+7 -8
tailwind.config.js
··· 30 30 } 31 31 }, 32 32 borderRadius: { 33 - DEFAULT: 'var(--radius-sm)', 34 - pill: 'var(--radius-pill)', 35 - 'pill-lg': 'var(--radius-pill)', 36 - sm: 'var(--radius-sm)', 37 - md: 'var(--radius-md)', 38 - lg: 'var(--radius-md)', 39 - xl: 'var(--radius-lg)', 40 - full: 'var(--radius-full)', 33 + DEFAULT: '6px', 34 + sm: '6px', 35 + md: '8px', 36 + lg: '10px', 37 + xl: '12px', 38 + pill: '9999px', 39 + full: '50%', 41 40 }, 42 41 boxShadow: { 43 42 'spot': 'var(--spot-shadow)',