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

Configure Feed

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

Refine terminal UI styling and add OG card generator

j4ck.xyz db6e8b5a c01b1944

+818 -558
+88 -30
app/globals.css
··· 1 - *, *::before, *::after { 1 + *, 2 + *::before, 3 + *::after { 2 4 box-sizing: border-box; 3 5 margin: 0; 4 6 padding: 0; 5 7 } 6 8 7 9 :root { 8 - --blue: #0085ff; 9 - --blue-dark: #006ad4; 10 - --blue-pale: #eff6ff; 11 - --text: #0f172a; 12 - --text-2: #475569; 13 - --text-3: #94a3b8; 14 - --bg: #ffffff; 15 - --bg-2: #f8fafc; 16 - --border: #e2e8f0; 17 - --radius: 12px; 18 - --mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; 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(59% 0.148 254); 21 + --blue-accent-soft: oklch(76% 0.07 252); 22 + --blue-glow: oklch(64% 0.13 254 / 0.22); 23 + 24 + --radius-xs: 6px; 25 + --radius-sm: 8px; 26 + --radius-md: 12px; 27 + --radius-lg: 18px; 28 + 29 + --ease-out: cubic-bezier(0.22, 1, 0.36, 1); 30 + 31 + --mono: 'JetBrains Mono', 'SFMono-Regular', Menlo, Consolas, monospace; 32 + --sans: 'Manrope', 'Segoe UI', sans-serif; 33 + --display: 'Bricolage Grotesque', 'Manrope', sans-serif; 34 + 19 35 color-scheme: light dark; 20 36 } 21 37 22 38 @media (prefers-color-scheme: dark) { 23 39 :root { 24 - --blue: #4da6ff; 25 - --blue-dark: #79bbff; 26 - --blue-pale: #0c1e3a; 27 - --text: #f1f5f9; 28 - --text-2: #94a3b8; 29 - --text-3: #475569; 30 - --bg: #0d1117; 31 - --bg-2: #161b22; 32 - --border: #30363d; 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(72% 0.13 253); 51 + --blue-accent-soft: oklch(83% 0.06 252); 52 + --blue-glow: oklch(72% 0.15 252 / 0.24); 33 53 } 34 54 } 35 55 36 56 html { 37 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif; 57 + font-family: var(--sans); 38 58 font-size: 16px; 39 59 color: var(--text); 40 60 background: var(--bg); 41 61 -webkit-font-smoothing: antialiased; 62 + -moz-osx-font-smoothing: grayscale; 63 + text-rendering: geometricPrecision; 64 + scroll-behavior: smooth; 65 + } 66 + 67 + body { 68 + min-height: 100vh; 69 + 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) 20%, transparent), transparent 46%), 73 + var(--bg); 74 + } 75 + 76 + ::selection { 77 + background: var(--text); 78 + color: var(--bg); 79 + } 80 + 81 + :focus-visible { 82 + outline: 2px solid var(--text); 83 + outline-offset: 2px; 84 + } 85 + 86 + ::-webkit-scrollbar { 87 + width: 10px; 88 + height: 10px; 89 + } 90 + 91 + ::-webkit-scrollbar-track { 92 + background: var(--surface); 93 + } 94 + 95 + ::-webkit-scrollbar-thumb { 96 + background: color-mix(in oklch, var(--line-strong) 92%, transparent); 97 + border-radius: 999px; 98 + border: 2px solid var(--surface); 99 + } 100 + 101 + ::-webkit-scrollbar-thumb:hover { 102 + background: var(--text-muted); 42 103 } 43 104 44 105 a { 45 - color: var(--blue); 46 - text-decoration: none; 47 - } 48 - a:hover { 49 - text-decoration: underline; 106 + color: var(--text); 50 107 } 51 108 52 109 button { 53 110 font-family: inherit; 111 + cursor: pointer; 54 112 } 55 113 56 - ::selection { 57 - background: #bfdbfe; 58 - color: #0f172a; 114 + code, 115 + pre { 116 + font-family: var(--mono); 59 117 }
+5 -2
app/layout.tsx
··· 1 1 import type { Metadata } from 'next' 2 2 import './globals.css' 3 - import FollowPrompt from './components/FollowPrompt' 4 3 5 4 const BASE = 'https://bsky.md' 6 5 ··· 38 37 export default function RootLayout({ children }: { children: React.ReactNode }) { 39 38 return ( 40 39 <html lang="en"> 40 + <head> 41 + <link rel="preconnect" href="https://fonts.googleapis.com" /> 42 + <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" /> 43 + <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" /> 44 + </head> 41 45 <body> 42 46 {children} 43 - <FollowPrompt /> 44 47 </body> 45 48 </html> 46 49 )
+500 -488
app/page.module.css
··· 1 - /* ── Animations ───────────────────────────────────── */ 2 - @keyframes slideDown { 3 - from { opacity: 0; transform: translateY(-10px); } 4 - to { opacity: 1; transform: translateY(0); } 1 + @keyframes rise { 2 + from { 3 + opacity: 0; 4 + transform: translateY(14px); 5 + } 6 + to { 7 + opacity: 1; 8 + transform: translateY(0); 9 + } 5 10 } 6 11 7 - @keyframes fadeIn { 8 - from { opacity: 0; } 9 - to { opacity: 1; } 10 - } 11 - 12 - @keyframes shimmer { 13 - 0% { background-position: 0% center; } 14 - 100% { background-position: 300% center; } 12 + @keyframes pulseCursor { 13 + 0%, 14 + 45% { 15 + opacity: 1; 16 + } 17 + 55%, 18 + 100% { 19 + opacity: 0; 20 + } 15 21 } 16 22 17 23 @keyframes spin { 18 - to { transform: rotate(360deg); } 19 - } 20 - 21 - @keyframes pop { 22 - 0% { transform: scale(1); } 23 - 50% { transform: scale(0.94); } 24 - 100% { transform: scale(1); } 24 + to { 25 + transform: rotate(360deg); 26 + } 25 27 } 26 28 27 - /* ── Layout ───────────────────────────────────────── */ 28 29 .page { 29 30 min-height: 100vh; 30 31 display: flex; 31 32 flex-direction: column; 33 + position: relative; 32 34 } 33 35 34 - /* ── Header ───────────────────────────────────────── */ 36 + .page::before { 37 + content: ''; 38 + position: fixed; 39 + inset: 0; 40 + pointer-events: none; 41 + background-image: 42 + linear-gradient(to right, color-mix(in oklch, var(--line) 72%, transparent) 1px, transparent 1px), 43 + linear-gradient(to bottom, color-mix(in oklch, var(--line) 72%, transparent) 1px, transparent 1px); 44 + background-size: 30px 30px; 45 + opacity: 0.28; 46 + mask-image: radial-gradient(circle at 50% 36%, #000 38%, transparent 84%); 47 + } 48 + 35 49 .header { 36 50 position: sticky; 37 51 top: 0; 38 - z-index: 10; 39 - background: rgba(255, 255, 255, 0.85); 40 - backdrop-filter: blur(14px); 41 - -webkit-backdrop-filter: blur(14px); 42 - border-bottom: 1px solid var(--border); 43 - transition: background 0.2s; 44 - } 45 - 46 - @media (prefers-color-scheme: dark) { 47 - .header { 48 - background: rgba(13, 17, 23, 0.85); 49 - } 52 + z-index: 40; 53 + backdrop-filter: blur(9px); 54 + -webkit-backdrop-filter: blur(9px); 55 + background: color-mix(in oklch, var(--surface) 92%, transparent); 56 + border-bottom: 1px solid var(--line); 50 57 } 51 58 52 59 .nav { 53 - max-width: 900px; 60 + width: min(1120px, 100% - 48px); 54 61 margin: 0 auto; 55 - padding: 0 24px; 56 - height: 56px; 62 + min-height: 64px; 57 63 display: flex; 58 64 align-items: center; 59 65 justify-content: space-between; 66 + gap: 20px; 60 67 } 61 68 62 69 .logo { 63 - font-size: 1.05rem; 64 - font-weight: 700; 70 + display: inline-flex; 71 + align-items: center; 72 + gap: 8px; 65 73 color: var(--text); 66 - letter-spacing: -0.01em; 67 74 text-decoration: none; 68 - display: flex; 69 - align-items: center; 70 - gap: 8px; 75 + font-family: var(--display); 76 + font-size: 1rem; 77 + letter-spacing: 0.03em; 78 + text-transform: lowercase; 71 79 } 72 - .logo:hover { text-decoration: none; } 73 80 74 - .logoIcon { 75 - width: 22px; 76 - height: 22px; 77 - flex-shrink: 0; 81 + .logoMark { 82 + color: color-mix(in oklch, var(--text-muted) 70%, var(--blue-accent) 30%); 83 + font-family: var(--mono); 84 + font-size: 0.9rem; 78 85 } 79 86 80 87 .navLinks { 81 88 display: flex; 82 - gap: 24px; 89 + gap: clamp(12px, 2vw, 26px); 83 90 align-items: center; 84 91 } 85 92 86 93 .navLinks a { 87 - font-size: 0.875rem; 88 - color: var(--text-2); 94 + color: var(--text-dim); 89 95 text-decoration: none; 90 - transition: color 0.12s; 96 + font-size: 0.78rem; 97 + letter-spacing: 0.08em; 98 + text-transform: uppercase; 99 + transition: color 140ms ease; 91 100 } 92 101 93 102 .navLinks a:hover { 94 - color: var(--blue); 95 - text-decoration: none; 103 + color: var(--text); 96 104 } 97 105 98 - /* ── Hero ─────────────────────────────────────────── */ 99 106 .hero { 100 - padding: 72px 24px 56px; 101 - text-align: center; 102 - background: linear-gradient(180deg, #e8f3ff 0%, #f8fafc 50%, var(--bg) 100%); 103 - border-bottom: 1px solid var(--border); 107 + width: min(1120px, 100% - 48px); 108 + margin: 0 auto; 109 + padding: clamp(56px, 9vw, 100px) 0 18px; 110 + position: relative; 111 + animation: rise 360ms var(--ease-out); 104 112 } 105 113 106 - @media (prefers-color-scheme: dark) { 107 - .hero { 108 - background: linear-gradient(180deg, #0a1628 0%, #0d1520 50%, var(--bg) 100%); 109 - } 114 + .kicker { 115 + font-size: 0.72rem; 116 + color: color-mix(in oklch, var(--text-muted) 78%, var(--blue-accent) 22%); 117 + font-family: var(--mono); 118 + text-transform: uppercase; 119 + letter-spacing: 0.17em; 120 + margin-bottom: 18px; 110 121 } 111 122 112 123 .title { 113 - font-size: clamp(2.2rem, 6vw, 3.5rem); 114 - font-weight: 800; 124 + font-family: var(--display); 125 + font-size: clamp(2rem, 7vw, 4.2rem); 115 126 letter-spacing: -0.04em; 116 - line-height: 1.08; 117 - margin-bottom: 14px; 118 - background: linear-gradient( 119 - 90deg, 120 - var(--text) 0%, 121 - var(--blue) 40%, 122 - var(--text) 55%, 123 - var(--text) 100% 124 - ); 125 - background-size: 300% auto; 126 - -webkit-background-clip: text; 127 - background-clip: text; 128 - color: transparent; 129 - animation: shimmer 6s linear infinite; 127 + line-height: 0.94; 128 + max-width: 14ch; 129 + text-wrap: balance; 130 + margin-bottom: 18px; 131 + } 132 + 133 + .title::after { 134 + content: '_'; 135 + font-family: var(--mono); 136 + margin-left: 8px; 137 + color: var(--text-muted); 138 + animation: pulseCursor 1.15s steps(1, end) infinite; 130 139 } 131 140 132 141 .subtitle { 133 - font-size: 1.1rem; 134 - color: var(--text-2); 135 - max-width: 480px; 136 - margin: 0 auto 40px; 137 - line-height: 1.6; 142 + max-width: 64ch; 143 + color: var(--text-soft); 144 + line-height: 1.66; 145 + font-size: clamp(0.95rem, 1.55vw, 1.08rem); 146 + margin-bottom: 30px; 138 147 } 139 148 140 - /* ── Input row ────────────────────────────────────── */ 141 149 .inputWrapper { 150 + display: grid; 151 + grid-template-columns: minmax(0, 1fr) auto; 152 + gap: 12px; 153 + max-width: 900px; 142 154 position: relative; 143 - display: flex; 144 - gap: 8px; 145 - max-width: 620px; 146 - margin: 0 auto 14px; 155 + } 156 + 157 + .inputWrapper::before { 158 + content: '$'; 159 + position: absolute; 160 + top: 20px; 161 + left: 16px; 162 + color: var(--text-muted); 163 + font-family: var(--mono); 164 + font-size: 0.84rem; 165 + pointer-events: none; 147 166 } 148 167 149 168 .input { 150 - flex: 1; 151 - height: 52px; 152 - padding: 0 18px; 153 - border: 2px solid var(--border); 154 - border-radius: var(--radius); 155 - font-size: 0.95rem; 169 + width: 100%; 170 + min-width: 0; 171 + height: 56px; 172 + padding: 0 14px 0 36px; 173 + border: 1px solid var(--line); 174 + border-radius: var(--radius-md); 175 + background: var(--surface); 156 176 color: var(--text); 157 - background: var(--bg); 158 - outline: none; 159 - transition: border-color 0.15s, box-shadow 0.15s; 160 - min-width: 0; 177 + font-family: var(--mono); 178 + font-size: 0.86rem; 179 + letter-spacing: 0.02em; 180 + transition: border-color 160ms ease, background-color 160ms ease; 161 181 } 162 182 163 - .input::placeholder { color: var(--text-3); } 183 + .input::placeholder { 184 + color: var(--text-muted); 185 + } 164 186 165 - .input:focus { 166 - border-color: var(--blue); 167 - box-shadow: 0 0 0 3px rgba(0, 133, 255, 0.12); 187 + .input:focus-visible { 188 + outline: none; 189 + border-color: var(--text); 190 + background: var(--surface-2); 168 191 } 169 192 170 193 .detectedBadge { 171 194 position: absolute; 172 - right: 138px; 195 + right: 122px; 173 196 top: 50%; 174 197 transform: translateY(-50%); 175 - display: inline-flex; 176 - align-items: center; 177 - padding: 2px 8px; 178 - background: var(--blue-pale); 179 - color: var(--blue); 180 - border-radius: 4px; 181 - font-size: 0.68rem; 182 - font-weight: 700; 183 - letter-spacing: 0.05em; 198 + font-family: var(--mono); 199 + font-size: 0.66rem; 200 + letter-spacing: 0.12em; 184 201 text-transform: uppercase; 202 + border: 1px dashed color-mix(in oklch, var(--line-strong) 72%, var(--blue-accent) 28%); 203 + padding: 4px 10px; 204 + border-radius: 999px; 205 + background: color-mix(in oklch, var(--surface) 78%, transparent); 206 + color: color-mix(in oklch, var(--text-muted) 76%, var(--blue-accent) 24%); 185 207 pointer-events: none; 186 - animation: fadeIn 0.15s ease; 187 - white-space: nowrap; 188 208 } 189 209 190 210 .convertBtn { 191 - height: 52px; 192 - padding: 0 24px; 193 - background: var(--blue); 194 - color: #fff; 195 - border: none; 196 - border-radius: var(--radius); 197 - font-size: 0.95rem; 211 + min-width: 108px; 212 + height: 56px; 213 + padding: 0 22px; 214 + border: 1px solid var(--line-strong); 215 + border-radius: var(--radius-md); 216 + background: var(--text); 217 + color: var(--bg); 218 + font-family: var(--mono); 219 + font-size: 0.8rem; 198 220 font-weight: 600; 221 + letter-spacing: 0.07em; 222 + text-transform: uppercase; 199 223 cursor: pointer; 200 - white-space: nowrap; 201 - transition: background 0.15s, transform 0.1s, box-shadow 0.15s; 202 - flex-shrink: 0; 224 + box-shadow: 0 0 0 0 var(--blue-glow); 225 + transition: transform 120ms ease, opacity 120ms ease, box-shadow 180ms ease; 203 226 } 204 227 205 228 .convertBtn:hover { 206 - background: var(--blue-dark); 207 - box-shadow: 0 4px 14px rgba(0, 133, 255, 0.35); 229 + transform: translateY(-1px); 230 + box-shadow: 0 8px 24px -16px var(--blue-glow); 208 231 } 209 232 210 233 .convertBtn:active { 211 - animation: pop 0.15s ease; 234 + transform: translateY(0); 235 + opacity: 0.88; 212 236 } 213 237 214 - /* ── Pills ────────────────────────────────────────── */ 215 238 .pills { 239 + margin-top: 14px; 216 240 display: flex; 217 - gap: 8px; 218 - justify-content: center; 219 241 flex-wrap: wrap; 220 - margin-top: 4px; 242 + gap: 8px; 221 243 } 222 244 223 245 .pill { 224 - display: inline-flex; 225 - align-items: center; 226 - gap: 5px; 227 - padding: 6px 14px; 228 - background: var(--bg); 229 - border: 1px solid var(--border); 230 - border-radius: 100px; 231 - font-size: 0.82rem; 232 - color: var(--text-2); 233 - cursor: pointer; 234 - transition: border-color 0.12s, color 0.12s, background 0.12s, transform 0.1s; 235 - text-decoration: none; 236 - user-select: none; 246 + border: 1px solid var(--line); 247 + background: var(--surface); 248 + color: var(--text-dim); 249 + border-radius: 999px; 250 + height: 33px; 251 + padding: 0 13px; 252 + font-family: var(--mono); 253 + font-size: 0.72rem; 254 + letter-spacing: 0.04em; 255 + transition: border-color 150ms ease, color 150ms ease, background-color 150ms ease; 237 256 } 238 257 239 258 .pill:hover { 240 - border-color: var(--blue); 241 - color: var(--blue); 242 - background: var(--blue-pale); 243 - transform: translateY(-1px); 244 - text-decoration: none; 259 + color: var(--text); 260 + border-color: var(--line-strong); 261 + background: var(--surface-2); 245 262 } 246 263 247 - .pill:active { 248 - transform: translateY(0) scale(0.97); 264 + .resultSection, 265 + .infoStrip, 266 + .terminalSection, 267 + .endpointsSection, 268 + .agentSection, 269 + .footer { 270 + width: min(1120px, 100% - 48px); 271 + margin-inline: auto; 249 272 } 250 273 251 - /* ── Result ───────────────────────────────────────── */ 252 274 .resultSection { 253 - max-width: 900px; 254 - width: 100%; 255 - margin: 32px auto 0; 256 - padding: 0 24px; 257 - animation: slideDown 0.2s ease; 275 + margin-top: 28px; 258 276 } 259 277 260 278 .resultCard { 261 - border: 1px solid var(--border); 262 - border-radius: var(--radius); 279 + border: 1px solid var(--line-strong); 280 + border-radius: var(--radius-lg); 263 281 overflow: hidden; 264 - box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); 282 + background: var(--surface); 283 + box-shadow: 0 16px 44px color-mix(in oklch, var(--bg) 72%, transparent); 265 284 } 266 285 267 - @media (prefers-color-scheme: dark) { 268 - .resultCard { 269 - box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3); 270 - } 286 + .resultCard::before { 287 + content: ''; 288 + display: block; 289 + height: 1px; 290 + width: 100%; 291 + background: linear-gradient( 292 + 90deg, 293 + transparent, 294 + color-mix(in oklch, var(--blue-accent-soft) 50%, transparent), 295 + transparent 296 + ); 271 297 } 272 298 273 - /* Result top bar */ 274 299 .resultBar { 275 - background: var(--bg-2); 276 - border-bottom: 1px solid var(--border); 300 + min-height: 52px; 277 301 padding: 10px 14px; 302 + border-bottom: 1px solid var(--line); 303 + background: var(--surface-2); 278 304 display: flex; 305 + flex-wrap: wrap; 279 306 align-items: center; 280 - gap: 8px; 281 - min-height: 44px; 282 - flex-wrap: wrap; 307 + gap: 10px; 283 308 } 284 309 285 310 .resultLabel { 286 - display: inline-flex; 287 - align-items: center; 288 - padding: 2px 8px; 289 - background: var(--blue-pale); 290 - color: var(--blue); 291 - border-radius: 4px; 292 - font-size: 0.68rem; 293 - font-weight: 700; 294 - letter-spacing: 0.06em; 311 + border: 1px solid color-mix(in oklch, var(--line-strong) 72%, var(--blue-accent) 28%); 312 + border-radius: 999px; 313 + padding: 4px 10px; 314 + font-family: var(--mono); 315 + font-size: 0.66rem; 316 + letter-spacing: 0.11em; 295 317 text-transform: uppercase; 318 + color: color-mix(in oklch, var(--text-muted) 80%, var(--blue-accent) 20%); 296 319 white-space: nowrap; 297 - flex-shrink: 0; 298 320 } 299 321 300 322 .resultUrl { 301 323 flex: 1; 324 + min-width: 0; 325 + color: var(--text-dim); 326 + font-size: 0.76rem; 302 327 font-family: var(--mono); 303 - font-size: 0.78rem; 304 - color: var(--text-2); 328 + white-space: nowrap; 305 329 overflow: hidden; 306 330 text-overflow: ellipsis; 307 - white-space: nowrap; 308 - min-width: 0; 309 331 } 310 332 311 333 .resultActions { 312 334 display: flex; 313 - gap: 6px; 314 - flex-shrink: 0; 335 + gap: 8px; 336 + margin-left: auto; 315 337 } 316 338 317 339 .actionBtn { 340 + height: 30px; 318 341 display: inline-flex; 319 342 align-items: center; 320 - gap: 4px; 321 - padding: 5px 12px; 322 - font-size: 0.78rem; 323 - font-weight: 500; 324 - border-radius: 6px; 325 - border: 1px solid var(--border); 326 - background: var(--bg); 327 - cursor: pointer; 328 - color: var(--text-2); 343 + justify-content: center; 344 + border: 1px solid var(--line); 345 + border-radius: var(--radius-sm); 346 + background: var(--surface); 347 + color: var(--text-dim); 329 348 text-decoration: none; 330 - transition: background 0.1s, border-color 0.1s, color 0.1s, transform 0.1s; 331 - white-space: nowrap; 332 - user-select: none; 349 + padding: 0 11px; 350 + font-size: 0.69rem; 351 + letter-spacing: 0.05em; 352 + text-transform: uppercase; 353 + font-family: var(--mono); 354 + transition: border-color 140ms ease, color 140ms ease, background-color 140ms ease; 333 355 } 334 356 335 357 .actionBtn:hover { 336 - background: var(--bg-2); 337 - border-color: #cbd5e1; 358 + border-color: var(--line-strong); 338 359 color: var(--text); 339 - text-decoration: none; 340 - } 341 - 342 - .actionBtn:active { 343 - transform: scale(0.96); 360 + background: var(--surface-2); 344 361 } 345 362 346 363 .actionBtnSuccess { 347 - background: #dcfce7 !important; 348 - border-color: #86efac !important; 349 - color: #16a34a !important; 364 + background: var(--text); 365 + color: var(--bg); 366 + border-color: var(--text); 350 367 } 351 368 352 - @media (prefers-color-scheme: dark) { 353 - .actionBtnSuccess { 354 - background: #052e16 !important; 355 - border-color: #166534 !important; 356 - color: #4ade80 !important; 357 - } 358 - } 359 - 360 - /* Post/thread toggle */ 361 369 .toggle { 362 - padding: 8px 14px; 363 - border-bottom: 1px solid var(--border); 364 - background: var(--bg); 365 - display: flex; 366 - gap: 4px; 370 + padding: 8px 12px; 371 + border-bottom: 1px solid var(--line); 372 + display: inline-flex; 373 + gap: 8px; 367 374 } 368 375 369 376 .toggleBtn { 370 - padding: 5px 14px; 371 - font-size: 0.8rem; 372 - font-weight: 500; 373 - border-radius: 6px; 377 + height: 30px; 374 378 border: 1px solid transparent; 375 379 background: transparent; 376 - cursor: pointer; 377 - color: var(--text-2); 378 - transition: all 0.12s; 380 + color: var(--text-dim); 381 + border-radius: var(--radius-sm); 382 + padding: 0 12px; 383 + font-size: 0.72rem; 384 + font-family: var(--mono); 385 + letter-spacing: 0.02em; 379 386 } 380 387 381 388 .toggleBtn:hover { 382 - background: var(--bg-2); 389 + border-color: var(--line); 383 390 color: var(--text); 384 391 } 385 392 386 393 .toggleActive { 387 - background: var(--blue-pale); 388 - border-color: #bfdbfe; 389 - color: var(--blue); 390 - } 391 - 392 - @media (prefers-color-scheme: dark) { 393 - .toggleActive { 394 - border-color: #1e3a5f; 395 - } 394 + border-color: var(--line-strong); 395 + color: var(--text); 396 + background: var(--surface-2); 396 397 } 397 398 398 - /* Preview toolbar */ 399 399 .previewToolbar { 400 + border-bottom: 1px solid var(--line); 401 + background: var(--surface-2); 402 + min-height: 40px; 400 403 display: flex; 401 404 align-items: center; 402 405 justify-content: space-between; 403 - padding: 8px 14px; 404 - border-bottom: 1px solid var(--border); 405 - background: var(--bg-2); 406 + gap: 10px; 407 + padding: 5px 12px; 406 408 } 407 409 408 410 .charCount { 411 + color: var(--text-muted); 409 412 font-family: var(--mono); 410 - font-size: 0.72rem; 411 - color: var(--text-3); 413 + font-size: 0.68rem; 414 + letter-spacing: 0.06em; 415 + text-transform: uppercase; 412 416 } 413 417 414 - /* Preview content */ 415 418 .preview { 416 419 margin: 0; 417 - padding: 20px 22px; 418 - font-family: var(--mono); 419 - font-size: 0.8rem; 420 - line-height: 1.65; 421 - background: var(--bg); 422 - color: var(--text); 423 - overflow: auto; 424 420 max-height: 520px; 421 + overflow: auto; 422 + padding: 18px; 425 423 white-space: pre-wrap; 426 424 word-break: break-word; 427 - tab-size: 2; 425 + background: var(--bg); 426 + color: var(--text-soft); 427 + line-height: 1.72; 428 + font-family: var(--mono); 429 + font-size: 0.78rem; 428 430 } 429 431 430 432 .previewError { 431 - color: #dc2626; 432 - } 433 - 434 - @media (prefers-color-scheme: dark) { 435 - .previewError { color: #f87171; } 433 + color: color-mix(in oklch, var(--text) 58%, var(--danger)); 436 434 } 437 435 438 436 .previewLoading { 439 - padding: 40px; 440 - text-align: center; 441 - color: var(--text-3); 442 - font-size: 0.875rem; 443 - font-family: inherit; 444 - background: var(--bg); 437 + min-height: 170px; 445 438 display: flex; 446 439 align-items: center; 447 440 justify-content: center; 448 - gap: 8px; 441 + gap: 10px; 442 + color: var(--text-muted); 443 + background: var(--bg); 444 + font-family: var(--mono); 445 + font-size: 0.75rem; 446 + letter-spacing: 0.05em; 447 + text-transform: uppercase; 449 448 } 450 449 451 450 .spinner { 452 - display: inline-block; 453 - width: 16px; 454 - height: 16px; 455 - border: 2px solid var(--border); 456 - border-top-color: var(--blue); 451 + width: 17px; 452 + aspect-ratio: 1; 453 + border: 2px solid var(--line); 454 + border-top-color: var(--text); 457 455 border-radius: 50%; 458 - animation: spin 0.7s linear infinite; 459 - flex-shrink: 0; 456 + animation: spin 700ms linear infinite; 460 457 } 461 458 462 - /* ── Info strip ───────────────────────────────────── */ 463 459 .infoStrip { 464 - max-width: 900px; 465 - width: 100%; 466 - margin: 32px auto 0; 467 - padding: 0 24px; 468 - display: flex; 460 + margin-top: 40px; 461 + display: grid; 462 + grid-template-columns: repeat(4, minmax(0, 1fr)); 469 463 gap: 10px; 470 - flex-wrap: wrap; 471 464 } 472 465 473 466 .infoItem { 474 - display: flex; 475 - align-items: center; 476 - gap: 8px; 477 - padding: 10px 16px; 478 - background: var(--bg-2); 479 - border: 1px solid var(--border); 480 - border-radius: var(--radius); 481 - font-size: 0.82rem; 482 - color: var(--text-2); 483 - flex: 1; 484 - min-width: 170px; 485 - transition: border-color 0.15s, transform 0.15s; 467 + border: 1px solid var(--line); 468 + border-radius: var(--radius-md); 469 + background: var(--surface); 470 + padding: 14px; 471 + display: grid; 472 + gap: 7px; 486 473 } 487 474 488 - .infoItem:hover { 489 - border-color: var(--blue); 490 - transform: translateY(-1px); 475 + .infoKey { 476 + font-family: var(--mono); 477 + font-size: 0.64rem; 478 + letter-spacing: 0.12em; 479 + text-transform: uppercase; 480 + color: var(--text-muted); 491 481 } 492 482 493 - .infoIcon { 494 - font-size: 1rem; 495 - flex-shrink: 0; 483 + .infoValue { 484 + font-size: 0.87rem; 485 + color: var(--text-soft); 486 + line-height: 1.45; 496 487 } 497 488 498 - /* ── Terminal Section ─────────────────────────────── */ 499 - .terminalSection { 500 - max-width: 900px; 501 - width: 100%; 502 - margin: 48px auto 0; 503 - padding: 0 24px; 489 + .terminalSection, 490 + .endpointsSection, 491 + .agentSection { 492 + margin-top: 50px; 493 + } 494 + 495 + .sectionTitle { 496 + font-family: var(--mono); 497 + color: var(--text-muted); 498 + letter-spacing: 0.14em; 499 + text-transform: uppercase; 500 + font-size: 0.68rem; 501 + margin-bottom: 12px; 504 502 } 505 503 506 504 .terminalSubtitle { 507 - font-size: 0.85rem; 508 - color: var(--text-2); 509 - margin-bottom: 16px; 510 - line-height: 1.5; 505 + max-width: 65ch; 506 + color: var(--text-soft); 507 + line-height: 1.6; 508 + margin-bottom: 14px; 509 + } 510 + 511 + .terminalSubtitle code, 512 + .terminalHint code, 513 + .agentFooterHint code { 514 + border: 1px solid var(--line); 515 + border-radius: var(--radius-xs); 516 + background: var(--surface); 517 + color: var(--text); 518 + padding: 1px 6px; 511 519 } 512 520 513 521 .terminalBlock { 514 - border-radius: 12px; 522 + border: 1px solid var(--line-strong); 523 + border-radius: var(--radius-lg); 524 + background: var(--bg); 515 525 overflow: hidden; 516 - border: 1px solid var(--border); 517 - background: #0d1117; 518 - } 519 - 520 - @media (prefers-color-scheme: light) { 521 - .terminalBlock { 522 - background: #1a1f27; 523 - } 524 526 } 525 527 526 528 .terminalBar { 529 + height: 34px; 527 530 display: flex; 528 - gap: 6px; 529 531 align-items: center; 530 - padding: 10px 14px; 531 - background: rgba(255,255,255,0.05); 532 - border-bottom: 1px solid rgba(255,255,255,0.07); 532 + gap: 7px; 533 + border-bottom: 1px solid var(--line); 534 + background: var(--surface-2); 535 + padding: 0 12px; 533 536 } 534 537 535 538 .terminalDot { 536 - width: 10px; 537 - height: 10px; 539 + width: 9px; 540 + aspect-ratio: 1; 538 541 border-radius: 50%; 539 - background: rgba(255,255,255,0.15); 542 + border: 1px solid var(--line-strong); 543 + background: var(--surface); 540 544 } 541 545 542 - .terminalDot:nth-child(1) { background: #ff5f57; } 543 - .terminalDot:nth-child(2) { background: #febc2e; } 544 - .terminalDot:nth-child(3) { background: #28c840; } 545 - 546 546 .terminalCode { 547 547 margin: 0; 548 - padding: 20px 22px; 548 + padding: 16px; 549 + color: var(--text-soft); 549 550 font-family: var(--mono); 550 - font-size: 0.8rem; 551 - line-height: 1.7; 552 - color: #a3b5cc; 551 + font-size: 0.76rem; 552 + line-height: 1.8; 553 553 overflow-x: auto; 554 - white-space: pre; 555 554 } 556 555 557 556 .terminalHint { 558 - font-size: 0.78rem; 559 - color: var(--text-3); 560 557 margin-top: 10px; 558 + color: var(--text-muted); 559 + font-size: 0.8rem; 561 560 line-height: 1.5; 562 561 } 563 562 564 - /* ── Endpoints Grid ───────────────────────────────── */ 565 - .endpointsSection { 566 - max-width: 900px; 567 - width: 100%; 568 - margin: 48px auto 0; 569 - padding: 0 24px; 570 - } 571 - 572 - .sectionTitle { 573 - font-size: 0.72rem; 574 - font-weight: 700; 575 - letter-spacing: 0.08em; 576 - text-transform: uppercase; 577 - color: var(--text-3); 578 - margin-bottom: 14px; 579 - } 580 - 581 563 .grid { 582 564 display: grid; 583 - grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); 565 + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); 584 566 gap: 10px; 585 567 } 586 568 587 569 .card { 588 - padding: 16px; 589 - border: 1px solid var(--border); 590 - border-radius: var(--radius); 570 + border: 1px solid var(--line); 571 + background: var(--surface); 572 + border-radius: var(--radius-md); 573 + padding: 15px; 591 574 text-decoration: none; 592 575 color: inherit; 593 - transition: border-color 0.15s, box-shadow 0.15s, background 0.15s, transform 0.15s; 594 - display: block; 595 - background: var(--bg); 576 + min-height: 116px; 577 + transition: border-color 150ms ease, background-color 150ms ease; 596 578 } 597 579 598 580 .card:hover { 599 - border-color: var(--blue); 600 - box-shadow: 0 0 0 3px rgba(0, 133, 255, 0.08); 601 - text-decoration: none; 602 - transform: translateY(-2px); 603 - } 604 - 605 - .card:active { 606 - transform: translateY(0); 581 + border-color: var(--line-strong); 582 + background: var(--surface-2); 607 583 } 608 584 609 585 .cardBadge { 610 - display: inline-block; 611 - font-size: 0.62rem; 612 - font-weight: 700; 613 - letter-spacing: 0.06em; 614 - color: var(--blue); 615 - background: var(--blue-pale); 616 - padding: 2px 6px; 617 - border-radius: 4px; 618 - margin-bottom: 8px; 586 + display: inline-flex; 587 + min-width: 36px; 588 + height: 20px; 589 + align-items: center; 590 + justify-content: center; 591 + border: 1px solid color-mix(in oklch, var(--line-strong) 76%, var(--blue-accent) 24%); 592 + border-radius: 999px; 593 + font-family: var(--mono); 594 + font-size: 0.61rem; 595 + text-transform: uppercase; 596 + color: color-mix(in oklch, var(--text-muted) 82%, var(--blue-accent) 18%); 597 + letter-spacing: 0.08em; 619 598 } 620 599 621 600 .cardPath { 622 601 display: block; 623 - font-family: var(--mono); 624 - font-size: 0.78rem; 625 - font-weight: 600; 602 + margin-top: 10px; 626 603 color: var(--text); 627 - margin-bottom: 5px; 628 - word-break: break-all; 629 - } 630 - 631 - .cardDesc { 632 - font-size: 0.78rem; 633 - color: var(--text-2); 634 - margin: 0; 604 + font-family: var(--mono); 605 + font-size: 0.75rem; 635 606 line-height: 1.45; 636 607 } 637 608 638 - /* ── Agent install section ────────────────────────── */ 639 - .agentSection { 640 - max-width: 900px; 641 - width: 100%; 642 - margin: 56px auto 0; 643 - padding: 0 24px; 609 + .cardDesc { 610 + margin-top: 9px; 611 + color: var(--text-muted); 612 + font-size: 0.8rem; 613 + line-height: 1.5; 644 614 } 645 615 646 616 .agentCard { 647 - border: 1px solid var(--border); 648 - border-radius: var(--radius); 617 + border: 1px solid var(--line-strong); 618 + border-radius: var(--radius-lg); 649 619 overflow: hidden; 650 - background: var(--bg); 620 + background: var(--surface); 651 621 } 652 622 653 623 .agentHeader { 624 + padding: 16px; 625 + border-bottom: 1px solid var(--line); 654 626 display: flex; 627 + flex-wrap: wrap; 655 628 align-items: flex-start; 656 629 justify-content: space-between; 657 - gap: 16px; 658 - padding: 20px 20px 16px; 659 - flex-wrap: wrap; 630 + gap: 14px; 660 631 } 661 632 662 633 .agentDesc { 663 - font-size: 0.85rem; 664 - color: var(--text-2); 665 - margin-bottom: 8px; 666 - line-height: 1.5; 667 - max-width: 540px; 668 - } 669 - 670 - .agentSkillsLink { 671 - font-size: 0.78rem; 672 - color: var(--blue); 673 - text-decoration: none; 634 + max-width: 65ch; 635 + color: var(--text-soft); 636 + line-height: 1.6; 637 + font-size: 0.88rem; 674 638 } 675 639 676 - .agentSkillsLink:hover { 677 - text-decoration: underline; 678 - } 679 - 680 - .skillCopyBtn { 640 + .agentSkillsLink, 641 + .agentRawLink { 642 + margin-top: 9px; 681 643 display: inline-flex; 682 644 align-items: center; 683 - gap: 6px; 684 - padding: 9px 18px; 685 - font-size: 0.85rem; 686 - font-weight: 600; 687 - background: var(--blue); 688 - color: #fff; 689 - border: none; 690 - border-radius: 8px; 691 - cursor: pointer; 692 - white-space: nowrap; 693 - flex-shrink: 0; 694 - transition: background 0.15s, transform 0.1s; 645 + color: var(--text); 646 + text-decoration: none; 647 + border-bottom: 1px dashed color-mix(in oklch, var(--line-strong) 70%, var(--blue-accent) 30%); 648 + font-size: 0.78rem; 695 649 } 696 650 697 - .skillCopyBtn:hover:not(:disabled) { 698 - background: #006ad4; 651 + .agentSkillsLink:hover, 652 + .agentRawLink:hover { 653 + color: color-mix(in oklch, var(--text) 70%, var(--blue-accent) 30%); 699 654 } 700 655 701 - .skillCopyBtn:active:not(:disabled) { 702 - transform: scale(0.97); 656 + .skillCopyBtn { 657 + min-width: 134px; 658 + height: 37px; 659 + border: 1px solid var(--line-strong); 660 + border-radius: var(--radius-sm); 661 + background: var(--text); 662 + color: var(--bg); 663 + font-family: var(--mono); 664 + font-size: 0.72rem; 665 + text-transform: uppercase; 666 + letter-spacing: 0.08em; 667 + padding: 0 12px; 703 668 } 704 669 705 670 .skillCopyBtn:disabled { 706 - opacity: 0.5; 671 + opacity: 0.45; 707 672 cursor: default; 708 673 } 709 674 710 675 .skillCopyBtnDone { 711 - background: #16a34a !important; 676 + background: var(--surface-2); 677 + color: var(--text); 712 678 } 713 679 714 680 .skillEmbed { 715 681 margin: 0; 716 - padding: 18px 20px; 717 - font-family: var(--mono); 718 - font-size: 0.76rem; 719 - line-height: 1.65; 720 - color: var(--text-2); 721 - background: var(--bg-2); 722 - border-top: 1px solid var(--border); 723 - border-bottom: 1px solid var(--border); 682 + max-height: 280px; 724 683 overflow: auto; 725 - max-height: 320px; 684 + border-bottom: 1px solid var(--line); 685 + background: var(--bg); 686 + color: var(--text-soft); 687 + padding: 16px; 688 + font-family: var(--mono); 689 + font-size: 0.73rem; 690 + line-height: 1.66; 726 691 white-space: pre; 727 692 } 728 693 729 694 .agentFooter { 695 + min-height: 46px; 730 696 display: flex; 731 697 align-items: center; 732 - gap: 10px; 733 - padding: 12px 20px; 698 + gap: 12px; 734 699 flex-wrap: wrap; 735 - } 736 - 737 - .agentRawLink { 738 - font-size: 0.78rem; 739 - color: var(--blue); 740 - text-decoration: none; 741 - white-space: nowrap; 742 - } 743 - 744 - .agentRawLink:hover { 745 - text-decoration: underline; 700 + padding: 11px 16px; 746 701 } 747 702 748 703 .agentFooterHint { 749 - font-size: 0.75rem; 750 - color: var(--text-3); 751 - line-height: 1.5; 704 + color: var(--text-muted); 705 + font-size: 0.74rem; 752 706 } 753 707 754 - /* ── Footer ───────────────────────────────────────── */ 755 708 .footer { 756 - margin-top: auto; 757 - padding: 32px 24px; 758 - border-top: 1px solid var(--border); 759 - text-align: center; 709 + margin-top: 62px; 710 + border-top: 1px solid var(--line); 711 + padding: 30px 0 34px; 760 712 } 761 713 762 714 .footerLinks { 763 715 display: flex; 764 - gap: 20px; 765 - justify-content: center; 766 716 flex-wrap: wrap; 767 - margin-bottom: 10px; 717 + gap: 10px 24px; 718 + align-items: center; 768 719 } 769 720 770 721 .footerLinks a { 771 - color: var(--text-2); 772 - font-size: 0.85rem; 722 + color: var(--text-dim); 773 723 text-decoration: none; 774 - transition: color 0.12s; 724 + font-size: 0.76rem; 725 + text-transform: uppercase; 726 + letter-spacing: 0.08em; 775 727 } 776 728 777 729 .footerLinks a:hover { 778 - color: var(--blue); 730 + color: var(--text); 779 731 } 780 732 781 733 .footerNote { 782 - font-size: 0.78rem; 783 - color: var(--text-3); 734 + margin-top: 16px; 735 + color: var(--text-muted); 736 + font-family: var(--mono); 737 + font-size: 0.72rem; 738 + letter-spacing: 0.05em; 784 739 } 785 740 786 - /* ── Responsive ───────────────────────────────────── */ 787 - @media (max-width: 600px) { 788 - .hero { 789 - padding: 48px 16px 40px; 741 + @media (max-width: 980px) { 742 + .infoStrip { 743 + grid-template-columns: repeat(2, minmax(0, 1fr)); 744 + } 745 + 746 + .detectedBadge { 747 + display: none; 748 + } 749 + } 750 + 751 + @media (max-width: 760px) { 752 + .nav { 753 + width: min(1120px, 100% - 28px); 754 + min-height: 58px; 755 + } 756 + 757 + .navLinks { 758 + gap: 11px; 759 + } 760 + 761 + .hero, 762 + .resultSection, 763 + .infoStrip, 764 + .terminalSection, 765 + .endpointsSection, 766 + .agentSection, 767 + .footer { 768 + width: min(1120px, 100% - 28px); 769 + } 770 + 771 + .title { 772 + max-width: 12ch; 790 773 } 791 774 792 775 .inputWrapper { 793 - flex-direction: column; 776 + grid-template-columns: 1fr; 794 777 } 795 778 796 779 .convertBtn { 797 780 width: 100%; 798 781 } 799 782 800 - .detectedBadge { 801 - display: none; 783 + .resultActions { 784 + width: 100%; 785 + margin-left: 0; 786 + } 787 + 788 + .actionBtn { 789 + flex: 1; 790 + min-width: 0; 791 + } 792 + 793 + .agentHeader { 794 + align-items: stretch; 795 + } 796 + 797 + .skillCopyBtn { 798 + width: 100%; 802 799 } 800 + } 803 801 804 - .resultSection, 805 - .endpointsSection, 802 + @media (max-width: 540px) { 806 803 .infoStrip { 807 - padding: 0 16px; 804 + grid-template-columns: 1fr; 808 805 } 809 806 810 - .nav { 811 - padding: 0 16px; 807 + .pills { 808 + gap: 6px; 812 809 } 813 810 814 - .navLinks { 815 - gap: 16px; 811 + .pill { 812 + height: 31px; 813 + padding: 0 10px; 814 + font-size: 0.66rem; 816 815 } 817 816 818 - .resultActions { 819 - flex-wrap: wrap; 817 + .title { 818 + font-size: clamp(1.85rem, 10vw, 2.6rem); 819 + } 820 + } 821 + 822 + @media (prefers-reduced-motion: reduce) { 823 + .hero, 824 + .title::after, 825 + .spinner, 826 + .convertBtn, 827 + .pill, 828 + .actionBtn, 829 + .card { 830 + animation: none !important; 831 + transition: none !important; 820 832 } 821 833 }
+41 -38
app/page.tsx
··· 86 86 ] 87 87 88 88 const QUICK_LINKS = [ 89 - { label: '🔥 Trending', path: '/trending' }, 90 - { label: '👤 bsky.app', path: '/profile/bsky.app' }, 91 - { label: '🌐 What\'s Hot', path: '/profile/bsky.app/feed/whats-hot' }, 92 - { label: '#atproto', path: '/search?q=%23atproto' }, 93 - { label: '📰 Tech', path: '/search?q=tech' }, 89 + { label: '/trending', path: '/trending' }, 90 + { label: '@bsky.app', path: '/profile/bsky.app' }, 91 + { label: '/feed/whats-hot', path: '/profile/bsky.app/feed/whats-hot' }, 92 + { label: '#atproto', path: '/search?q=%23atproto' }, 93 + { label: 'search:tech', path: '/search?q=tech' }, 94 94 ] 95 95 96 96 // ── Component ───────────────────────────────────────────────────────────────── ··· 201 201 202 202 return ( 203 203 <div className={s.page}> 204 - {/* ── Header ── */} 205 204 <header className={s.header}> 206 205 <div className={s.nav}> 207 206 <a href="/" className={s.logo}> 208 - 🦋 bsky.md 207 + <span className={s.logoMark} aria-hidden="true">&gt;</span> 208 + bsky.md 209 209 </a> 210 210 <nav className={s.navLinks}> 211 211 <a href="/trending">Trending</a> 212 212 <a href="/llms.txt">llms.txt</a> 213 + <a href="/cli">CLI</a> 213 214 <a href="https://tangled.org/j4ck.xyz/bsky-md" target="_blank" rel="noopener noreferrer"> 214 215 Source 215 216 </a> ··· 217 218 </div> 218 219 </header> 219 220 220 - {/* ── Hero ── */} 221 221 <section className={s.hero}> 222 - <h1 className={s.title}>Bluesky, as Markdown.</h1> 222 + <p className={s.kicker}>Terminal-native Bluesky export</p> 223 + <h1 className={s.title}>Bluesky -&gt; Markdown</h1> 223 224 <p className={s.subtitle}> 224 - Paste any bsky.app URL — profile, post, feed, search, or hashtag — and get back clean, 225 - portable Markdown instantly. 225 + Paste any profile, post, feed, hashtag, or query and return clean plain-text Markdown ready 226 + for copy, curl, or coding agents. 226 227 </p> 227 228 228 229 <div className={s.inputWrapper}> 229 230 <input 230 231 className={s.input} 231 232 type="text" 232 - placeholder="bsky.app/profile/... · post URL · #hashtag · search term" 233 + placeholder="bsky.app/profile/... | post URL | #hashtag | search" 233 234 value={input} 234 235 onChange={(e) => setInput(e.target.value)} 235 236 onKeyDown={(e) => e.key === 'Enter' && handleConvert()} ··· 239 240 /> 240 241 {detected && <span className={s.detectedBadge}>{detected.label}</span>} 241 242 <button className={s.convertBtn} onClick={handleConvert}> 242 - Convert → 243 + Run 243 244 </button> 244 245 </div> 245 246 ··· 252 253 </div> 253 254 </section> 254 255 255 - {/* ── Result ── */} 256 256 {(loading || markdown !== null || error !== null) && parsed && ( 257 257 <section className={s.resultSection} ref={resultRef}> 258 258 <div className={s.resultCard}> 259 - 260 - {/* Top bar: label · url · copy url · open */} 261 259 <div className={s.resultBar}> 262 260 <span className={s.resultLabel}>{parsed.label}</span> 263 261 <code className={s.resultUrl}>{activePath}</code> ··· 279 277 </div> 280 278 </div> 281 279 282 - {/* Post / Thread toggle */} 283 280 {parsed.isPost && ( 284 281 <div className={s.toggle}> 285 282 <button ··· 297 294 </div> 298 295 )} 299 296 300 - {/* Preview toolbar */} 301 297 {!loading && markdown && ( 302 298 <div className={s.previewToolbar}> 303 299 <span className={s.charCount}>{fmtBytes(charCount)}</span> ··· 305 301 className={`${s.actionBtn} ${copiedMd ? s.actionBtnSuccess : ''}`} 306 302 onClick={copyMarkdown} 307 303 > 308 - {copiedMd ? '✓ Copied!' : '📋 Copy Markdown'} 304 + {copiedMd ? '✓ Copied' : 'Copy Markdown'} 309 305 </button> 310 306 </div> 311 307 )} 312 308 313 - {/* Content */} 314 309 {loading && ( 315 310 <div className={s.previewLoading}> 316 311 <span className={s.spinner} /> 317 - Fetching… 312 + Fetching... 318 313 </div> 319 314 )} 320 315 {!loading && error && ( ··· 327 322 </section> 328 323 )} 329 324 330 - {/* ── Info strip ── */} 331 - <div className={s.infoStrip} style={{ marginTop: 32 }}> 332 - <div className={s.infoItem}><span className={s.infoIcon}>🔓</span> No auth or API key</div> 333 - <div className={s.infoItem}><span className={s.infoIcon}>🌍</span> Open CORS from any origin</div> 334 - <div className={s.infoItem}><span className={s.infoIcon}>⚡</span> Edge-cached responses</div> 335 - <div className={s.infoItem}><span className={s.infoIcon}>🤖</span> LLM-friendly plain text</div> 325 + <div className={s.infoStrip}> 326 + <div className={s.infoItem}> 327 + <span className={s.infoKey}>Auth</span> 328 + <span className={s.infoValue}>No API key needed</span> 329 + </div> 330 + <div className={s.infoItem}> 331 + <span className={s.infoKey}>CORS</span> 332 + <span className={s.infoValue}>Open from any origin</span> 333 + </div> 334 + <div className={s.infoItem}> 335 + <span className={s.infoKey}>Cache</span> 336 + <span className={s.infoValue}>Fast edge responses</span> 337 + </div> 338 + <div className={s.infoItem}> 339 + <span className={s.infoKey}>Format</span> 340 + <span className={s.infoValue}>LLM-safe plain markdown</span> 341 + </div> 336 342 </div> 337 343 338 - {/* ── Terminal examples ── */} 339 344 <section className={s.terminalSection}> 340 - <p className={s.sectionTitle}>Works great in your terminal too</p> 345 + <p className={s.sectionTitle}>Terminal workflow</p> 341 346 <p className={s.terminalSubtitle}> 342 - <code>curl</code> any endpoint and get plain Markdown back — pipe it to <code>glow</code>, your agent, or just read it. 347 + Use <code>curl</code> directly and pipe output to any terminal renderer, script, or coding agent. 343 348 </p> 344 349 <div className={s.terminalBlock}> 345 350 <div className={s.terminalBar}> ··· 371 376 ].join('\n')}</pre> 372 377 </div> 373 378 <p className={s.terminalHint}> 374 - Tip: visiting this URL from <code>curl</code> or any terminal client automatically returns Markdown — no flags needed. 379 + Tip: requests from terminal clients automatically return Markdown with no additional flags. 375 380 </p> 376 381 </section> 377 382 378 - {/* ── Endpoints ── */} 379 - <section className={s.endpointsSection} style={{ marginTop: 48 }}> 383 + <section className={s.endpointsSection}> 380 384 <p className={s.sectionTitle}>All Endpoints</p> 381 385 <div className={s.grid}> 382 386 {ENDPOINTS.map((ep) => ( ··· 389 393 </div> 390 394 </section> 391 395 392 - {/* ── Agent install ── */} 393 396 <section className={s.agentSection}> 394 397 <p className={s.sectionTitle}>Add to your coding agent</p> 395 398 <div className={s.agentCard}> 396 399 <div className={s.agentHeader}> 397 400 <div> 398 401 <p className={s.agentDesc}> 399 - Copy this skill file and paste it into any coding agent — Claude, Cursor, Windsurf, Copilot, or any tool that accepts a system prompt or rules file. 402 + Copy this skill file into Claude, Cursor, Windsurf, Copilot, or any agent that accepts 403 + instruction files. 400 404 </p> 401 405 <a 402 406 className={s.agentSkillsLink} ··· 412 416 onClick={copySkill} 413 417 disabled={!skillMd} 414 418 > 415 - {copiedSkill ? '✓ Copied!' : '📋 Copy skill.md'} 419 + {copiedSkill ? '✓ Copied' : 'Copy skill.md'} 416 420 </button> 417 421 </div> 418 422 <pre className={s.skillEmbed}> ··· 429 433 </div> 430 434 </section> 431 435 432 - {/* ── Footer ── */} 433 - <footer className={s.footer} style={{ marginTop: 64 }}> 436 + <footer className={s.footer}> 434 437 <div className={s.footerLinks}> 435 438 <a href="/trending">Trending</a> 436 439 <a href="/llms.txt">llms.txt</a>
+184
test/generate_og_card.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + Generate a monochrome terminal-themed Open Graph image. 4 + 5 + Output: 6 + test/og-card.png 7 + """ 8 + 9 + from __future__ import annotations 10 + 11 + from pathlib import Path 12 + 13 + from PIL import Image, ImageDraw, ImageFilter, ImageFont 14 + 15 + 16 + WIDTH = 1200 17 + HEIGHT = 630 18 + 19 + 20 + def load_font(candidates: list[str], size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: 21 + for candidate in candidates: 22 + p = Path(candidate) 23 + if p.exists(): 24 + try: 25 + return ImageFont.truetype(str(p), size=size) 26 + except OSError: 27 + continue 28 + return ImageFont.load_default() 29 + 30 + 31 + def main() -> None: 32 + out_path = Path(__file__).resolve().parent / "og-card.png" 33 + 34 + bg = (14, 16, 20) 35 + surface = (21, 24, 30) 36 + surface_soft = (27, 31, 39) 37 + line = (58, 64, 76) 38 + text = (239, 242, 247) 39 + text_soft = (178, 187, 202) 40 + text_muted = (130, 139, 155) 41 + 42 + img = Image.new("RGB", (WIDTH, HEIGHT), bg) 43 + draw = ImageDraw.Draw(img) 44 + 45 + # Background radial glow 46 + glow = Image.new("RGBA", (WIDTH, HEIGHT), (0, 0, 0, 0)) 47 + gdraw = ImageDraw.Draw(glow) 48 + gdraw.ellipse((-180, -260, 760, 680), fill=(210, 220, 245, 20)) 49 + gdraw.ellipse((620, -220, 1420, 520), fill=(210, 220, 245, 16)) 50 + glow = glow.filter(ImageFilter.GaussianBlur(58)) 51 + img = Image.alpha_composite(img.convert("RGBA"), glow).convert("RGB") 52 + draw = ImageDraw.Draw(img) 53 + 54 + # Subtle grid 55 + step = 40 56 + for x in range(0, WIDTH, step): 57 + draw.line([(x, 0), (x, HEIGHT)], fill=(43, 47, 58), width=1) 58 + for y in range(0, HEIGHT, step): 59 + draw.line([(0, y), (WIDTH, y)], fill=(43, 47, 58), width=1) 60 + 61 + # Main frame 62 + card_x = 82 63 + card_y = 72 64 + card_w = WIDTH - (card_x * 2) 65 + card_h = HEIGHT - (card_y * 2) 66 + 67 + draw.rounded_rectangle( 68 + (card_x, card_y, card_x + card_w, card_y + card_h), 69 + radius=26, 70 + fill=surface, 71 + outline=line, 72 + width=2, 73 + ) 74 + 75 + # Top terminal bar 76 + top_h = 54 77 + draw.rounded_rectangle( 78 + (card_x + 1, card_y + 1, card_x + card_w - 1, card_y + top_h), 79 + radius=24, 80 + fill=surface_soft, 81 + outline=None, 82 + ) 83 + draw.line( 84 + [(card_x + 18, card_y + top_h), (card_x + card_w - 18, card_y + top_h)], 85 + fill=line, 86 + width=1, 87 + ) 88 + 89 + dot_y = card_y + 27 90 + for i in range(3): 91 + cx = card_x + 30 + (i * 18) 92 + draw.ellipse((cx - 4, dot_y - 4, cx + 4, dot_y + 4), fill=(158, 167, 183)) 93 + 94 + mono = load_font( 95 + [ 96 + "/System/Library/Fonts/Supplemental/Menlo.ttc", 97 + "/System/Library/Fonts/Supplemental/Courier New Bold.ttf", 98 + "/System/Library/Fonts/Supplemental/Andale Mono.ttf", 99 + ], 100 + size=22, 101 + ) 102 + mono_small = load_font( 103 + [ 104 + "/System/Library/Fonts/Supplemental/Menlo.ttc", 105 + "/System/Library/Fonts/Supplemental/Courier New.ttf", 106 + ], 107 + size=16, 108 + ) 109 + display = load_font( 110 + [ 111 + "/System/Library/Fonts/Supplemental/Avenir Next Condensed Bold.ttf", 112 + "/System/Library/Fonts/Supplemental/Arial Bold.ttf", 113 + ], 114 + size=78, 115 + ) 116 + 117 + draw.text((card_x + 95, card_y + 17), "bsky.md", fill=text_muted, font=mono_small) 118 + 119 + # Badge 120 + badge_x = card_x + card_w - 290 121 + badge_y = card_y + 15 122 + draw.rounded_rectangle( 123 + (badge_x, badge_y, badge_x + 210, badge_y + 26), 124 + radius=13, 125 + fill=surface, 126 + outline=(88, 95, 109), 127 + width=1, 128 + ) 129 + draw.text((badge_x + 14, badge_y + 5), "TEXT/MARKDOWN API", fill=text_soft, font=mono_small) 130 + 131 + # Headline 132 + head_x = card_x + 54 133 + head_y = card_y + 108 134 + draw.text((head_x, head_y), "Bluesky -> Markdown", fill=text, font=display) 135 + 136 + # Supporting copy 137 + body = ( 138 + "Minimal, terminal-native output for profiles, posts, threads, " 139 + "feeds, links, search, and trending topics." 140 + ) 141 + draw.text((head_x, head_y + 100), body, fill=text_soft, font=mono_small) 142 + 143 + # Command block 144 + block_x = head_x 145 + block_y = head_y + 162 146 + block_w = card_w - 108 147 + block_h = 160 148 + draw.rounded_rectangle( 149 + (block_x, block_y, block_x + block_w, block_y + block_h), 150 + radius=16, 151 + fill=(12, 14, 17), 152 + outline=(72, 79, 92), 153 + width=1, 154 + ) 155 + 156 + lines = [ 157 + "$ curl https://bsky.md/profile/bsky.app", 158 + "# clean markdown output, no auth required", 159 + "$ curl https://bsky.md/profile/bsky.app/post/3lhreomsy5k2x/thread", 160 + ] 161 + 162 + y = block_y + 20 163 + for i, line_text in enumerate(lines): 164 + color = text if i != 1 else text_muted 165 + draw.text((block_x + 20, y), line_text, fill=color, font=mono) 166 + y += 44 167 + 168 + # Footer strip 169 + foot_y = card_y + card_h - 56 170 + draw.line( 171 + [(card_x + 24, foot_y), (card_x + card_w - 24, foot_y)], 172 + fill=line, 173 + width=1, 174 + ) 175 + draw.text((card_x + 30, foot_y + 18), "Content-Type: text/markdown", fill=text_muted, font=mono_small) 176 + draw.text((card_x + card_w - 190, foot_y + 18), "https://bsky.md", fill=text_muted, font=mono_small) 177 + 178 + out_path.parent.mkdir(parents=True, exist_ok=True) 179 + img.save(out_path, format="PNG", optimize=True) 180 + print(f"Wrote {out_path}") 181 + 182 + 183 + if __name__ == "__main__": 184 + main()
test/og-card.png

This is a binary file and will not be displayed.