feat: improve login page with internet handle terminology (#604)

- use "internet handle" label with link to internethandle.org
- collapsible FAQ sections for explanation
- simpler "sign in" button
- cleaner spacing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>

authored by zzstoatzz.io Claude and committed by GitHub b14db000 5f7db645

Changed files
+162 -79
frontend
src
routes
login
+162 -79
frontend/src/routes/login/+page.svelte
··· 1 1 <script lang="ts"> 2 - import { APP_NAME, APP_TAGLINE } from '$lib/branding'; 2 + import { APP_NAME } from '$lib/branding'; 3 3 import { API_URL } from '$lib/config'; 4 4 import HandleAutocomplete from '$lib/components/HandleAutocomplete.svelte'; 5 5 6 6 let handle = $state(''); 7 7 let loading = $state(false); 8 + let showHandleInfo = $state(false); 9 + let showPdsInfo = $state(false); 8 10 9 11 function startOAuth(e: SubmitEvent) { 10 12 e.preventDefault(); 11 13 if (!handle.trim()) return; 12 14 loading = true; 13 - // redirect to backend OAuth start endpoint 14 15 window.location.href = `${API_URL}/auth/start?handle=${encodeURIComponent(handle)}`; 15 16 } 16 17 ··· 21 22 22 23 <div class="container"> 23 24 <div class="login-card"> 24 - <h1>{APP_NAME}</h1> 25 - <p>{APP_TAGLINE}</p> 25 + <h1>sign in to {APP_NAME}</h1> 26 26 27 27 <form onsubmit={startOAuth}> 28 28 <div class="input-group"> 29 - <div class="label-row"> 30 - <label for="handle">atproto handle</label> 31 - <a 32 - href="https://atproto.com/specs/handle" 33 - target="_blank" 34 - rel="noopener noreferrer" 35 - class="help-link" 36 - title="learn about ATProto handles" 37 - > 38 - what's this? 39 - </a> 40 - </div> 29 + <label for="handle">internet handle</label> 41 30 <HandleAutocomplete 42 31 bind:value={handle} 43 32 onSelect={handleSelect} 44 - placeholder="yourname.bsky.social" 33 + placeholder="you.bsky.social" 45 34 disabled={loading} 46 35 /> 47 - <p class="input-help"> 48 - don't have one? 49 - <a href="https://bsky.app" target="_blank" rel="noopener noreferrer">create a free Bluesky account</a> 50 - to get your ATProto identity 51 - </p> 52 36 </div> 53 37 54 - <button type="submit" disabled={loading || !handle.trim()}> 55 - {loading ? 'redirecting...' : 'sign in with atproto'} 38 + <button type="submit" class="primary" disabled={loading || !handle.trim()}> 39 + {loading ? 'redirecting...' : 'sign in'} 56 40 </button> 57 41 </form> 42 + 43 + <div class="faq"> 44 + <button 45 + class="faq-toggle" 46 + onclick={() => (showHandleInfo = !showHandleInfo)} 47 + aria-expanded={showHandleInfo} 48 + > 49 + <span>what is an internet handle?</span> 50 + <svg 51 + class="chevron" 52 + class:open={showHandleInfo} 53 + width="16" 54 + height="16" 55 + viewBox="0 0 24 24" 56 + fill="none" 57 + stroke="currentColor" 58 + stroke-width="2" 59 + > 60 + <polyline points="6 9 12 15 18 9"></polyline> 61 + </svg> 62 + </button> 63 + {#if showHandleInfo} 64 + <div class="faq-content"> 65 + <p> 66 + your internet handle is a domain that identifies you across apps built on 67 + <a href="https://atproto.com" target="_blank" rel="noopener">AT Protocol</a>. 68 + if you signed up for Bluesky or another ATProto service, you already have one 69 + (like <code>yourname.bsky.social</code>). 70 + </p> 71 + <p> 72 + read more at <a href="https://internethandle.org" target="_blank" rel="noopener">internethandle.org</a>. 73 + </p> 74 + </div> 75 + {/if} 76 + 77 + <button 78 + class="faq-toggle" 79 + onclick={() => (showPdsInfo = !showPdsInfo)} 80 + aria-expanded={showPdsInfo} 81 + > 82 + <span>don't have one?</span> 83 + <svg 84 + class="chevron" 85 + class:open={showPdsInfo} 86 + width="16" 87 + height="16" 88 + viewBox="0 0 24 24" 89 + fill="none" 90 + stroke="currentColor" 91 + stroke-width="2" 92 + > 93 + <polyline points="6 9 12 15 18 9"></polyline> 94 + </svg> 95 + </button> 96 + {#if showPdsInfo} 97 + <div class="faq-content"> 98 + <p> 99 + the easiest way to get one is to sign up for <a href="https://bsky.app" target="_blank" rel="noopener">Bluesky</a>. 100 + once you have an account, you can use that handle here. 101 + </p> 102 + </div> 103 + {/if} 104 + </div> 58 105 </div> 59 106 </div> 60 107 ··· 71 118 .login-card { 72 119 background: var(--bg-tertiary); 73 120 border: 1px solid var(--border-subtle); 74 - border-radius: 8px; 75 - padding: 3rem; 76 - max-width: 400px; 121 + border-radius: 12px; 122 + padding: 2.5rem; 123 + max-width: 420px; 77 124 width: 100%; 78 125 } 79 126 80 127 h1 { 81 - font-size: 2.5rem; 82 - margin: 0 0 0.5rem 0; 128 + font-size: 1.75rem; 129 + margin: 0 0 2rem 0; 83 130 color: var(--text-primary); 84 131 text-align: center; 132 + font-weight: 600; 133 + white-space: nowrap; 85 134 } 86 135 87 - p { 88 - color: var(--text-tertiary); 89 - text-align: center; 90 - margin: 0 0 2rem 0; 136 + form { 137 + display: flex; 138 + flex-direction: column; 139 + gap: 1.5rem; 140 + } 141 + 142 + .input-group { 143 + display: flex; 144 + flex-direction: column; 145 + gap: 0.5rem; 146 + } 147 + 148 + label { 149 + color: var(--text-secondary); 150 + font-size: 0.9rem; 151 + } 152 + 153 + button.primary { 154 + width: 100%; 155 + padding: 0.85rem; 156 + background: var(--accent); 157 + color: white; 158 + border: none; 159 + border-radius: 8px; 91 160 font-size: 0.95rem; 161 + font-weight: 500; 162 + font-family: inherit; 163 + cursor: pointer; 164 + transition: all 0.15s; 92 165 } 93 166 94 - .input-group { 95 - margin-bottom: 1.5rem; 167 + button.primary:hover:not(:disabled) { 168 + opacity: 0.9; 169 + } 170 + 171 + button.primary:disabled { 172 + opacity: 0.5; 173 + cursor: not-allowed; 96 174 } 97 175 98 - .label-row { 176 + .faq { 177 + margin-top: 1.5rem; 178 + border-top: 1px solid var(--border-subtle); 179 + padding-top: 1rem; 180 + } 181 + 182 + .faq-toggle { 183 + width: 100%; 99 184 display: flex; 100 185 justify-content: space-between; 101 186 align-items: center; 102 - margin-bottom: 0.5rem; 103 - } 104 - 105 - label { 187 + padding: 0.75rem 0; 188 + background: none; 189 + border: none; 106 190 color: var(--text-secondary); 191 + font-family: inherit; 107 192 font-size: 0.9rem; 193 + cursor: pointer; 194 + text-align: left; 108 195 } 109 196 110 - .help-link { 111 - color: var(--accent); 112 - text-decoration: none; 113 - font-size: 0.85rem; 114 - transition: color 0.2s; 197 + .faq-toggle:hover { 198 + color: var(--text-primary); 115 199 } 116 200 117 - .help-link:hover { 118 - color: var(--accent-hover); 119 - text-decoration: underline; 201 + .chevron { 202 + transition: transform 0.2s; 203 + flex-shrink: 0; 120 204 } 121 205 122 - .input-help { 123 - margin: 0.5rem 0 0 0; 206 + .chevron.open { 207 + transform: rotate(180deg); 208 + } 209 + 210 + .faq-content { 211 + padding: 0 0 1rem 0; 212 + color: var(--text-tertiary); 124 213 font-size: 0.85rem; 125 - color: var(--text-tertiary); 214 + line-height: 1.6; 126 215 } 127 216 128 - .input-help a { 217 + .faq-content p { 218 + margin: 0 0 0.75rem 0; 219 + text-align: left; 220 + } 221 + 222 + .faq-content p:last-child { 223 + margin-bottom: 0; 224 + } 225 + 226 + .faq-content a { 129 227 color: var(--accent); 130 228 text-decoration: none; 131 - transition: color 0.2s; 132 229 } 133 230 134 - .input-help a:hover { 135 - color: var(--accent-hover); 231 + .faq-content a:hover { 136 232 text-decoration: underline; 137 233 } 138 234 139 - button { 140 - width: 100%; 141 - padding: 0.75rem; 142 - background: var(--accent); 143 - color: white; 144 - border: none; 235 + .faq-content code { 236 + background: var(--bg-secondary); 237 + padding: 0.15rem 0.4rem; 145 238 border-radius: 4px; 146 - font-size: 1rem; 147 - font-weight: 600; 148 - font-family: inherit; 149 - cursor: pointer; 150 - transition: all 0.2s; 151 - } 152 - 153 - button:hover:not(:disabled) { 154 - background: var(--accent-hover); 155 - transform: translateY(-1px); 156 - box-shadow: 0 4px 12px color-mix(in srgb, var(--accent) 30%, transparent); 239 + font-size: 0.85em; 157 240 } 158 241 159 - button:disabled { 160 - opacity: 0.5; 161 - cursor: not-allowed; 162 - transform: none; 163 - } 242 + @media (max-width: 480px) { 243 + .login-card { 244 + padding: 2rem 1.5rem; 245 + } 164 246 165 - button:active:not(:disabled) { 166 - transform: translateY(0); 247 + h1 { 248 + font-size: 1.5rem; 249 + } 167 250 } 168 251 </style>