An API you can curl, or open in a browser, to receive Bluesky data as markdown!
11
fork

Configure Feed

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

Add accessible theme controls and align follow prompt with terminal UI

j4ck.xyz 6fee90b2 5199eaad

+348 -113
+107 -64
app/components/FollowPrompt.module.css
··· 1 1 @keyframes slideUp { 2 - from { opacity: 0; transform: translateY(16px); } 3 - to { opacity: 1; transform: translateY(0); } 2 + from { 3 + opacity: 0; 4 + transform: translateY(20px) scale(0.97); 5 + } 6 + to { 7 + opacity: 1; 8 + transform: translateY(0) scale(1); 9 + } 4 10 } 5 11 6 12 @keyframes slideDown { 7 - from { opacity: 1; transform: translateY(0); } 8 - to { opacity: 0; transform: translateY(16px); } 13 + from { 14 + opacity: 1; 15 + transform: translateY(0) scale(1); 16 + } 17 + to { 18 + opacity: 0; 19 + transform: translateY(20px) scale(0.97); 20 + } 9 21 } 10 22 11 23 .card { 12 24 position: fixed; 13 25 bottom: 24px; 14 26 right: 24px; 15 - width: 290px; 16 - background: var(--bg); 17 - border: 1px solid var(--border); 18 - border-radius: 16px; 19 - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08); 27 + width: min(340px, calc(100vw - 32px)); 28 + background: color-mix(in oklch, var(--surface) 92%, var(--bg) 8%); 29 + border: 1px solid color-mix(in oklch, var(--line-strong) 66%, var(--blue-accent) 34%); 30 + border-radius: var(--radius-lg); 31 + box-shadow: 32 + 0 24px 46px -30px color-mix(in oklch, var(--bg) 58%, transparent), 33 + 0 0 0 1px color-mix(in oklch, var(--blue-accent) 12%, transparent); 20 34 z-index: 100; 21 35 overflow: hidden; 22 - animation: slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); 23 - } 24 - 25 - @media (prefers-color-scheme: dark) { 26 - .card { 27 - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(0, 0, 0, 0.3); 28 - } 36 + animation: slideUp 220ms var(--ease-out); 29 37 } 30 38 31 39 .cardLeaving { 32 - animation: slideDown 0.25s ease forwards; 40 + animation: slideDown 160ms ease forwards; 33 41 } 34 42 35 43 @media (max-width: 400px) { 36 44 .card { 37 - left: 16px; 38 - right: 16px; 45 + left: 14px; 46 + right: 14px; 39 47 width: auto; 40 - bottom: 16px; 48 + bottom: 14px; 41 49 } 42 50 } 43 51 44 - /* Top strip – subtle blue gradient */ 45 52 .strip { 46 - height: 3px; 47 - background: linear-gradient(90deg, #0085ff, #4da6ff); 53 + height: 1px; 54 + background: linear-gradient( 55 + 90deg, 56 + transparent, 57 + color-mix(in oklch, var(--blue-accent-soft) 70%, transparent), 58 + transparent 59 + ); 48 60 } 49 61 50 62 .body { 51 - padding: 16px 16px 14px; 63 + padding: 14px; 52 64 } 53 65 54 66 .header { 55 67 display: flex; 56 - align-items: flex-start; 57 - gap: 12px; 58 - margin-bottom: 12px; 68 + align-items: center; 69 + gap: 10px; 70 + margin-bottom: 10px; 59 71 } 60 72 61 73 .avatar { 62 - width: 46px; 63 - height: 46px; 74 + width: 40px; 75 + height: 40px; 64 76 border-radius: 50%; 65 77 object-fit: cover; 66 78 flex-shrink: 0; 67 - border: 2px solid var(--border); 68 - background: var(--bg-2); 79 + border: 1px solid var(--line); 80 + background: var(--surface-2); 69 81 } 70 82 71 83 .avatarFallback { 72 - width: 46px; 73 - height: 46px; 84 + width: 40px; 85 + height: 40px; 74 86 border-radius: 50%; 75 - background: linear-gradient(135deg, #0085ff, #4da6ff); 87 + background: color-mix(in oklch, var(--surface-2) 80%, var(--blue-accent) 20%); 76 88 display: flex; 77 89 align-items: center; 78 90 justify-content: center; 79 - font-size: 1.4rem; 91 + font-size: 0.95rem; 80 92 flex-shrink: 0; 81 93 } 82 94 ··· 86 98 } 87 99 88 100 .name { 89 - font-size: 0.9rem; 101 + font-family: var(--sans); 102 + font-size: 0.84rem; 90 103 font-weight: 700; 91 104 color: var(--text); 92 105 white-space: nowrap; 93 106 overflow: hidden; 94 107 text-overflow: ellipsis; 95 - line-height: 1.3; 108 + line-height: 1.2; 109 + letter-spacing: 0.01em; 110 + margin-bottom: 2px; 96 111 } 97 112 98 113 .handle { 99 - font-size: 0.78rem; 100 - color: var(--text-2); 114 + font-family: var(--mono); 115 + font-size: 0.68rem; 116 + color: var(--text-muted); 117 + letter-spacing: 0.04em; 101 118 white-space: nowrap; 102 119 overflow: hidden; 103 120 text-overflow: ellipsis; 104 121 } 105 122 106 123 .dismiss { 107 - width: 26px; 108 - height: 26px; 124 + width: 24px; 125 + height: 24px; 109 126 border-radius: 50%; 110 - border: 1px solid var(--border); 111 - background: transparent; 127 + border: 1px solid var(--line); 128 + background: var(--surface); 112 129 cursor: pointer; 113 - color: var(--text-3); 114 - font-size: 0.85rem; 130 + color: var(--text-muted); 131 + font-size: 0.75rem; 115 132 display: flex; 116 133 align-items: center; 117 134 justify-content: center; 118 135 flex-shrink: 0; 119 - transition: background 0.12s, color 0.12s; 136 + transition: border-color 130ms ease, color 130ms ease, background-color 130ms ease; 120 137 line-height: 1; 121 138 padding: 0; 122 139 } 123 140 124 141 .dismiss:hover { 125 - background: var(--bg-2); 126 - color: var(--text); 142 + background: var(--surface-2); 143 + color: color-mix(in oklch, var(--text) 72%, var(--blue-accent) 28%); 144 + border-color: color-mix(in oklch, var(--line-strong) 60%, var(--blue-accent) 40%); 145 + } 146 + 147 + .dismiss:active { 148 + transform: translateY(1px); 127 149 } 128 150 129 151 .label { 130 152 font-size: 0.78rem; 131 - color: var(--text-2); 153 + color: var(--text-soft); 132 154 margin-bottom: 10px; 133 155 line-height: 1.45; 134 156 } 135 157 136 158 .followBtn { 137 - display: flex; 159 + display: inline-flex; 138 160 align-items: center; 139 161 justify-content: center; 140 - gap: 7px; 162 + gap: 8px; 141 163 width: 100%; 142 - padding: 10px 0; 143 - background: #0085ff; 144 - color: #fff; 145 - border: none; 146 - border-radius: 10px; 147 - font-size: 0.88rem; 164 + min-height: 36px; 165 + padding: 0 12px; 166 + background: var(--text); 167 + color: var(--bg); 168 + border: 1px solid var(--line-strong); 169 + border-radius: var(--radius-sm); 170 + font-size: 0.7rem; 148 171 font-weight: 600; 172 + font-family: var(--mono); 149 173 cursor: pointer; 150 174 text-decoration: none; 151 - transition: background 0.15s, transform 0.1s, box-shadow 0.15s; 175 + text-transform: uppercase; 176 + letter-spacing: 0.08em; 177 + box-shadow: 178 + 0 0 0 1px color-mix(in oklch, var(--blue-accent) 16%, transparent), 179 + 0 0 0 0 var(--blue-glow); 180 + transition: transform 120ms ease, box-shadow 180ms ease, opacity 120ms ease; 152 181 } 153 182 154 183 .followBtn:hover { 155 - background: #006ad4; 156 - box-shadow: 0 4px 14px rgba(0, 133, 255, 0.3); 184 + transform: translateY(-1px); 185 + box-shadow: 186 + 0 0 0 1px color-mix(in oklch, var(--blue-accent) 24%, transparent), 187 + 0 14px 26px -16px var(--blue-glow); 157 188 text-decoration: none; 158 - color: #fff; 189 + color: var(--bg); 159 190 } 160 191 161 192 .followBtn:active { 162 - transform: scale(0.98); 193 + transform: translateY(0); 194 + opacity: 0.9; 163 195 } 164 196 165 197 .bskyIcon { 166 - width: 16px; 167 - height: 16px; 198 + width: 13px; 199 + height: 13px; 168 200 flex-shrink: 0; 201 + opacity: 0.92; 202 + } 203 + 204 + @media (prefers-reduced-motion: reduce) { 205 + .card, 206 + .cardLeaving, 207 + .followBtn, 208 + .dismiss { 209 + animation: none !important; 210 + transition: none !important; 211 + } 169 212 }
+12 -8
app/components/FollowPrompt.tsx
··· 18 18 const [visible, setVisible] = useState(false) 19 19 const [leaving, setLeaving] = useState(false) 20 20 const [profile, setProfile] = useState<Profile | null>(null) 21 + const [mounted, setMounted] = useState(false) 22 + 23 + useEffect(() => { 24 + setMounted(true) 25 + }, []) 21 26 22 27 useEffect(() => { 28 + if (!mounted) return 23 29 try { 24 30 if (localStorage.getItem(STORAGE_KEY)) return 25 31 } catch { ··· 28 34 // Delay so the page loads first 29 35 const t = setTimeout(() => setVisible(true), 1800) 30 36 return () => clearTimeout(t) 31 - }, []) 37 + }, [mounted]) 32 38 33 39 useEffect(() => { 34 40 if (!visible) return ··· 46 52 setTimeout(() => setVisible(false), 280) 47 53 } 48 54 49 - if (!visible) return null 55 + if (!mounted || !visible) return null 50 56 51 57 const displayName = profile?.displayName || 'jack' 52 58 const handle = profile?.handle || 'j4ck.xyz' 53 59 54 60 return ( 55 - <div className={`${s.card} ${leaving ? s.cardLeaving : ''}`} role="complementary" aria-label="Follow the creator"> 61 + <section className={`${s.card} ${leaving ? s.cardLeaving : ''}`} role="complementary" aria-label="Follow the creator"> 56 62 <div className={s.strip} /> 57 63 <div className={s.body}> 58 64 <div className={s.header}> ··· 65 71 <div className={s.name}>{displayName}</div> 66 72 <div className={s.handle}>@{handle}</div> 67 73 </div> 68 - <button className={s.dismiss} onClick={dismiss} aria-label="Dismiss"> 74 + <button type="button" className={s.dismiss} onClick={dismiss} aria-label="Dismiss follow prompt"> 69 75 70 76 </button> 71 77 </div> 72 78 73 - <p className={s.label}> 74 - Built by jack — follow to hear about updates and new tools. 75 - </p> 79 + <p className={s.label}>Built by jack. Follow for updates and new tools.</p> 76 80 77 81 <a 78 82 href={FOLLOW_URL} ··· 88 92 Follow on Bluesky 89 93 </a> 90 94 </div> 91 - </div> 95 + </section> 92 96 ) 93 97 }
+67 -32
app/globals.css
··· 7 7 } 8 8 9 9 :root { 10 - --bg: oklch(96.9% 0.004 250); 11 - --surface: oklch(99.2% 0.002 250); 12 - --surface-2: oklch(94.8% 0.005 250); 13 - --line: oklch(84.8% 0.006 250); 14 - --line-strong: oklch(66.8% 0.01 250); 15 - --text: oklch(17.6% 0.006 250); 16 - --text-soft: oklch(30.8% 0.007 250); 17 - --text-dim: oklch(43.5% 0.008 250); 18 - --text-muted: oklch(53.8% 0.008 250); 19 - --danger: oklch(56% 0.2 26); 20 - --blue-accent: oklch(58% 0.22 257); 21 - --blue-accent-soft: oklch(72% 0.15 255); 22 - --blue-glow: oklch(60% 0.22 256 / 0.35); 10 + --bg: oklch(9.8% 0.008 250); 11 + --surface: oklch(12.4% 0.008 250); 12 + --surface-2: oklch(16% 0.009 250); 13 + --line: oklch(23.2% 0.009 250); 14 + --line-strong: oklch(36.6% 0.01 250); 15 + --text: oklch(95.2% 0.003 250); 16 + --text-soft: oklch(86.2% 0.004 250); 17 + --text-dim: oklch(73.2% 0.005 250); 18 + --text-muted: oklch(61.2% 0.006 250); 19 + --danger: oklch(69% 0.18 27); 20 + --blue-accent: oklch(74% 0.18 255); 21 + --blue-accent-soft: oklch(84% 0.12 254); 22 + --blue-glow: oklch(73% 0.2 255 / 0.4); 23 23 24 24 --radius-xs: 6px; 25 25 --radius-sm: 8px; ··· 32 32 --sans: 'Manrope', 'Segoe UI', sans-serif; 33 33 --display: 'Bricolage Grotesque', 'Manrope', sans-serif; 34 34 35 - color-scheme: light dark; 35 + color-scheme: dark; 36 + } 37 + 38 + :root[data-theme='dark'] { 39 + --bg: oklch(9.8% 0.008 250); 40 + --surface: oklch(12.4% 0.008 250); 41 + --surface-2: oklch(16% 0.009 250); 42 + --line: oklch(23.2% 0.009 250); 43 + --line-strong: oklch(36.6% 0.01 250); 44 + --text: oklch(95.2% 0.003 250); 45 + --text-soft: oklch(86.2% 0.004 250); 46 + --text-dim: oklch(73.2% 0.005 250); 47 + --text-muted: oklch(61.2% 0.006 250); 48 + --danger: oklch(69% 0.18 27); 49 + --blue-accent: oklch(74% 0.18 255); 50 + --blue-accent-soft: oklch(84% 0.12 254); 51 + --blue-glow: oklch(73% 0.2 255 / 0.4); 52 + color-scheme: dark; 53 + } 54 + 55 + :root[data-theme='light'] { 56 + --bg: oklch(95.8% 0.004 250); 57 + --surface: oklch(98.4% 0.002 250); 58 + --surface-2: oklch(93.8% 0.005 250); 59 + --line: oklch(84.2% 0.006 250); 60 + --line-strong: oklch(65.4% 0.01 250); 61 + --text: oklch(18% 0.006 250); 62 + --text-soft: oklch(31.2% 0.007 250); 63 + --text-dim: oklch(44.2% 0.008 250); 64 + --text-muted: oklch(54.2% 0.008 250); 65 + --danger: oklch(56% 0.2 26); 66 + --blue-accent: oklch(58% 0.22 257); 67 + --blue-accent-soft: oklch(72% 0.15 255); 68 + --blue-glow: oklch(60% 0.22 256 / 0.3); 69 + color-scheme: light; 36 70 } 37 71 38 - @media (prefers-color-scheme: dark) { 39 - :root { 40 - --bg: oklch(11.4% 0.008 250); 41 - --surface: oklch(14.6% 0.008 250); 42 - --surface-2: oklch(18.3% 0.009 250); 43 - --line: oklch(25.8% 0.009 250); 44 - --line-strong: oklch(41.2% 0.01 250); 45 - --text: oklch(95.2% 0.003 250); 46 - --text-soft: oklch(86.2% 0.004 250); 47 - --text-dim: oklch(73.2% 0.005 250); 48 - --text-muted: oklch(61.2% 0.006 250); 49 - --danger: oklch(69% 0.18 27); 50 - --blue-accent: oklch(74% 0.18 255); 51 - --blue-accent-soft: oklch(84% 0.12 254); 52 - --blue-glow: oklch(73% 0.2 255 / 0.42); 72 + @media (prefers-color-scheme: light) { 73 + :root:not([data-theme]) { 74 + --bg: oklch(95.8% 0.004 250); 75 + --surface: oklch(98.4% 0.002 250); 76 + --surface-2: oklch(93.8% 0.005 250); 77 + --line: oklch(84.2% 0.006 250); 78 + --line-strong: oklch(65.4% 0.01 250); 79 + --text: oklch(18% 0.006 250); 80 + --text-soft: oklch(31.2% 0.007 250); 81 + --text-dim: oklch(44.2% 0.008 250); 82 + --text-muted: oklch(54.2% 0.008 250); 83 + --danger: oklch(56% 0.2 26); 84 + --blue-accent: oklch(58% 0.22 257); 85 + --blue-accent-soft: oklch(72% 0.15 255); 86 + --blue-glow: oklch(60% 0.22 256 / 0.3); 87 + color-scheme: light; 53 88 } 54 89 } 55 90 ··· 67 102 body { 68 103 min-height: 100vh; 69 104 background: 70 - radial-gradient(circle at 12% -8%, color-mix(in oklch, var(--line-strong) 26%, transparent), transparent 52%), 71 - radial-gradient(circle at 90% 0%, color-mix(in oklch, var(--line-strong) 22%, transparent), transparent 44%), 72 - radial-gradient(circle at 56% -18%, color-mix(in oklch, var(--blue-accent) 34%, transparent), transparent 47%), 105 + radial-gradient(circle at 12% -8%, color-mix(in oklch, var(--line-strong) 10%, transparent), transparent 56%), 106 + radial-gradient(circle at 90% 0%, color-mix(in oklch, var(--line-strong) 8%, transparent), transparent 48%), 107 + radial-gradient(circle at 56% -18%, color-mix(in oklch, var(--blue-accent) 8%, transparent), transparent 60%), 73 108 var(--bg); 74 109 } 75 110
+17
app/layout.tsx
··· 35 35 } 36 36 37 37 export default function RootLayout({ children }: { children: React.ReactNode }) { 38 + const themeInitScript = ` 39 + (() => { 40 + try { 41 + const key = 'bsky-md-theme'; 42 + const value = localStorage.getItem(key); 43 + if (value === 'dark' || value === 'light') { 44 + document.documentElement.setAttribute('data-theme', value); 45 + } else { 46 + document.documentElement.removeAttribute('data-theme'); 47 + } 48 + } catch { 49 + document.documentElement.removeAttribute('data-theme'); 50 + } 51 + })(); 52 + ` 53 + 38 54 return ( 39 55 <html lang="en"> 40 56 <head> 41 57 <link rel="preconnect" href="https://fonts.googleapis.com" /> 42 58 <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" /> 43 59 <link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,700;12..96,800&family=JetBrains+Mono:wght@400;500;600&family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet" /> 60 + <script dangerouslySetInnerHTML={{ __html: themeInitScript }} /> 44 61 </head> 45 62 <body> 46 63 {children}
+61 -1
app/page.module.css
··· 42 42 linear-gradient(to right, color-mix(in oklch, var(--line) 72%, transparent) 1px, transparent 1px), 43 43 linear-gradient(to bottom, color-mix(in oklch, var(--line) 72%, transparent) 1px, transparent 1px); 44 44 background-size: 30px 30px; 45 - opacity: 0.28; 45 + opacity: 0.11; 46 46 mask-image: radial-gradient(circle at 50% 36%, #000 38%, transparent 84%); 47 47 } 48 48 ··· 66 66 gap: 20px; 67 67 } 68 68 69 + .navRight { 70 + display: flex; 71 + align-items: center; 72 + gap: 14px; 73 + } 74 + 69 75 .logo { 70 76 display: inline-flex; 71 77 align-items: center; ··· 88 94 display: flex; 89 95 gap: clamp(12px, 2vw, 26px); 90 96 align-items: center; 97 + } 98 + 99 + .themeSwitch { 100 + display: inline-flex; 101 + align-items: center; 102 + gap: 2px; 103 + border: 1px solid var(--line); 104 + border-radius: 999px; 105 + background: color-mix(in oklch, var(--surface) 86%, transparent); 106 + padding: 2px; 107 + } 108 + 109 + .themeBtn { 110 + height: 28px; 111 + min-width: 54px; 112 + border: 1px solid transparent; 113 + background: transparent; 114 + color: var(--text-dim); 115 + border-radius: 999px; 116 + font-family: var(--mono); 117 + font-size: 0.64rem; 118 + letter-spacing: 0.08em; 119 + text-transform: uppercase; 120 + padding: 0 10px; 121 + } 122 + 123 + .themeBtn:hover { 124 + color: color-mix(in oklch, var(--text) 72%, var(--blue-accent) 28%); 125 + } 126 + 127 + .themeBtnActive { 128 + background: var(--surface-2); 129 + border-color: color-mix(in oklch, var(--line-strong) 58%, var(--blue-accent) 42%); 130 + color: color-mix(in oklch, var(--text) 72%, var(--blue-accent) 28%); 91 131 } 92 132 93 133 .navLinks a { ··· 756 796 .nav { 757 797 width: min(1120px, 100% - 28px); 758 798 min-height: 58px; 799 + flex-wrap: wrap; 800 + row-gap: 10px; 801 + padding: 10px 0; 802 + } 803 + 804 + .navRight { 805 + width: 100%; 806 + justify-content: space-between; 807 + gap: 10px; 808 + flex-wrap: wrap; 759 809 } 760 810 761 811 .navLinks { 762 812 gap: 11px; 813 + flex-wrap: wrap; 763 814 } 764 815 765 816 .hero, ··· 804 855 } 805 856 806 857 @media (max-width: 540px) { 858 + .themeSwitch { 859 + margin-left: auto; 860 + } 861 + 862 + .themeBtn { 863 + min-width: 48px; 864 + padding: 0 8px; 865 + } 866 + 807 867 .infoStrip { 808 868 grid-template-columns: 1fr; 809 869 }
+84 -8
app/page.tsx
··· 2 2 3 3 import { useState, useCallback, useRef, useEffect } from 'react' 4 4 import s from './page.module.css' 5 + import FollowPrompt from './components/FollowPrompt' 6 + 7 + type ThemeSetting = 'system' | 'dark' | 'light' 8 + 9 + const THEME_KEY = 'bsky-md-theme' 10 + 11 + function isThemeSetting(value: string | null): value is ThemeSetting { 12 + return value === 'system' || value === 'dark' || value === 'light' 13 + } 14 + 15 + function getStoredThemeSetting(): ThemeSetting { 16 + if (typeof window === 'undefined') return 'system' 17 + try { 18 + const stored = localStorage.getItem(THEME_KEY) 19 + if (isThemeSetting(stored)) return stored 20 + } catch { 21 + // no-op 22 + } 23 + return 'system' 24 + } 25 + 26 + function applyThemeSetting(setting: ThemeSetting) { 27 + const root = document.documentElement 28 + if (setting === 'system') { 29 + root.removeAttribute('data-theme') 30 + return 31 + } 32 + root.setAttribute('data-theme', setting) 33 + } 5 34 6 35 // ── URL parser ──────────────────────────────────────────────────────────────── 7 36 ··· 98 127 export default function Home() { 99 128 const [input, setInput] = useState('') 100 129 const [parsed, setParsed] = useState<Parsed | null>(null) 130 + const [theme, setTheme] = useState<ThemeSetting>('system') 101 131 const [viewMode, setViewMode] = useState<'post' | 'thread'>('thread') 102 132 const [markdown, setMarkdown] = useState<string | null>(null) 103 133 const [loading, setLoading] = useState(false) ··· 194 224 195 225 useEffect(() => { 196 226 fetch('/skill.md').then(r => r.text()).then(setSkillMd).catch(() => {}) 227 + }, []) 228 + 229 + useEffect(() => { 230 + const stored = getStoredThemeSetting() 231 + setTheme(stored) 232 + applyThemeSetting(stored) 233 + }, []) 234 + 235 + const setThemePreference = useCallback((next: ThemeSetting) => { 236 + setTheme(next) 237 + applyThemeSetting(next) 238 + try { 239 + localStorage.setItem(THEME_KEY, next) 240 + } catch { 241 + // no-op 242 + } 197 243 }, []) 198 244 199 245 const activePath = parsed ? getPath(parsed, parsed.isPost ? viewMode : 'post') : null ··· 207 253 <span className={s.logoMark} aria-hidden="true">&gt;</span> 208 254 bsky.md 209 255 </a> 210 - <nav className={s.navLinks}> 211 - <a href="/trending">Trending</a> 212 - <a href="/llms.txt">llms.txt</a> 213 - <a href="/cli">CLI</a> 214 - <a href="https://tangled.org/j4ck.xyz/bsky-md" target="_blank" rel="noopener noreferrer"> 215 - Source 216 - </a> 217 - </nav> 256 + <div className={s.navRight}> 257 + <nav className={s.navLinks}> 258 + <a href="/trending">Trending</a> 259 + <a href="/llms.txt">llms.txt</a> 260 + <a href="/cli">CLI</a> 261 + <a href="https://tangled.org/j4ck.xyz/bsky-md" target="_blank" rel="noopener noreferrer"> 262 + Source 263 + </a> 264 + </nav> 265 + <div className={s.themeSwitch} role="group" aria-label="Theme preference"> 266 + <button 267 + type="button" 268 + className={`${s.themeBtn} ${theme === 'system' ? s.themeBtnActive : ''}`} 269 + onClick={() => setThemePreference('system')} 270 + aria-pressed={theme === 'system'} 271 + > 272 + Auto 273 + </button> 274 + <button 275 + type="button" 276 + className={`${s.themeBtn} ${theme === 'dark' ? s.themeBtnActive : ''}`} 277 + onClick={() => setThemePreference('dark')} 278 + aria-pressed={theme === 'dark'} 279 + > 280 + Dark 281 + </button> 282 + <button 283 + type="button" 284 + className={`${s.themeBtn} ${theme === 'light' ? s.themeBtnActive : ''}`} 285 + onClick={() => setThemePreference('light')} 286 + aria-pressed={theme === 'light'} 287 + > 288 + Light 289 + </button> 290 + </div> 291 + </div> 218 292 </div> 219 293 </header> 220 294 ··· 445 519 </div> 446 520 <p className={s.footerNote}>Content-Type: text/markdown · bsky.md</p> 447 521 </footer> 522 + 523 + <FollowPrompt /> 448 524 </div> 449 525 ) 450 526 }