This repository has no description
0

Configure Feed

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

dark styling betgter

+376 -78
.DS_Store

This is a binary file and will not be displayed.

+49
app/src/app/api/bluesky/search/route.ts
··· 1 + import { NextRequest, NextResponse } from 'next/server'; 2 + import { BskyAgent } from '@atproto/api'; 3 + 4 + // Configure this route as dynamic to fix static generation issues 5 + export const dynamic = 'force-dynamic'; 6 + 7 + // This endpoint provides user search suggestions using Bluesky's searchActorsTypeahead API 8 + export async function GET(request: NextRequest) { 9 + try { 10 + // Get the query parameter from the URL 11 + const url = new URL(request.url); 12 + const term = url.searchParams.get('q'); 13 + 14 + if (!term) { 15 + return NextResponse.json({ suggestions: [] }, { status: 200 }); 16 + } 17 + 18 + // Create a Bluesky agent instance 19 + const agent = new BskyAgent({ 20 + service: 'https://bsky.social' 21 + }); 22 + 23 + // Make an unauthenticated request to the typeahead API 24 + const response = await agent.app.bsky.actor.searchActorsTypeahead({ 25 + term, 26 + limit: 5 27 + }); 28 + 29 + if (!response.success) { 30 + throw new Error('Failed to fetch user suggestions'); 31 + } 32 + 33 + // Format the response for the client 34 + const suggestions = response.data.actors.map(actor => ({ 35 + did: actor.did, 36 + handle: actor.handle, 37 + displayName: actor.displayName, 38 + avatar: actor.avatar || null 39 + })); 40 + 41 + return NextResponse.json({ suggestions }, { status: 200 }); 42 + } catch (error: any) { 43 + console.error('User search error:', error); 44 + return NextResponse.json( 45 + { error: 'Search failed', message: error.message }, 46 + { status: 500 } 47 + ); 48 + } 49 + }
+31 -28
app/src/app/profile/[handle]/profile.module.css
··· 29 29 30 30 .subtitle { 31 31 font-size: 1rem; 32 - color: #888; 32 + color: var(--timestamp-color); 33 33 margin: 0 0 0.5rem 0; 34 34 font-weight: normal; 35 35 word-wrap: break-word; ··· 37 37 38 38 .description { 39 39 font-size: 1.1rem; 40 - color: #666; 40 + color: var(--text-color); 41 41 margin: 0; 42 42 line-height: 1.5; 43 43 word-wrap: break-word; ··· 46 46 .profileHeader { 47 47 margin-bottom: 2rem; 48 48 padding-bottom: 1rem; 49 - border-bottom: 1px solid #e1e1e1; 49 + border-bottom: 1px solid var(--tile-border); 50 50 } 51 51 52 52 .profileInfo { ··· 64 64 65 65 .profileStats { 66 66 font-size: 1.1rem; 67 - color: #666; 67 + color: var(--text-color); 68 68 margin: 0; 69 69 } 70 70 ··· 84 84 85 85 /* Stats section and chart */ 86 86 .statsSection { 87 - background-color: white; 87 + background-color: var(--card-background); 88 88 border-radius: 8px; 89 89 padding: 1.5rem; 90 90 margin-bottom: 1.5rem; 91 - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); 92 - border: 1px solid #e1e1e1; 91 + box-shadow: 0 2px 5px var(--shadow-color); 92 + border: 1px solid var(--tile-border); 93 + background-image: repeating-linear-gradient(0deg, var(--tile-border), var(--tile-border) 1px, transparent 1px, transparent 20px); 93 94 } 94 95 95 96 .statsHeader { 96 97 font-size: 1.2rem; 97 98 font-weight: 500; 98 99 margin: 0 0 1rem 0; 99 - color: #444; 100 + color: var(--title-color); 100 101 } 101 102 102 103 .statDetails { 103 104 font-size: 1.1rem; 104 - color: #666; 105 + color: var(--text-color); 105 106 } 106 107 107 108 .chartContainer { ··· 109 110 display: flex; 110 111 align-items: flex-end; 111 112 gap: 2px; 112 - border-bottom: 1px solid #e1e1e1; 113 + border-bottom: 1px solid var(--tile-border); 113 114 margin-top: 1rem; 114 115 padding-top: 1rem; 115 116 position: relative; ··· 127 128 display: flex; 128 129 justify-content: space-between; 129 130 margin-top: 0.5rem; 130 - color: #999; 131 + color: var(--timestamp-color); 131 132 font-size: 0.8rem; 132 133 } 133 134 ··· 139 140 140 141 .chartLegendItem { 141 142 font-size: 0.75rem; 142 - color: #888; 143 + color: var(--timestamp-color); 143 144 } 144 145 145 146 .shareStatsButton { ··· 159 160 .shareStatsButton:hover { 160 161 background-color: var(--secondary-color); 161 162 transform: translateY(-2px); 162 - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 163 + box-shadow: 0 4px 8px var(--shadow-color); 163 164 } 164 165 165 166 .noDataMessage { 166 - color: #888; 167 + color: var(--timestamp-color); 167 168 text-align: center; 168 169 padding: 2rem 0; 169 170 font-style: italic; ··· 224 225 .backButton:hover { 225 226 background-color: var(--secondary-color); 226 227 transform: translateY(-2px); 227 - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 228 + box-shadow: 0 4px 8px var(--shadow-color); 228 229 } 229 230 230 231 .error { 231 - background-color: #ffebee; 232 - color: #c62828; 232 + background-color: var(--error-background); 233 + color: var(--error-color); 233 234 padding: 1rem; 234 235 border-radius: 4px; 235 236 margin-bottom: 1rem; ··· 245 246 } 246 247 247 248 .loader { 248 - border: 4px solid #f3f3f3; 249 + border: 4px solid var(--input-background); 249 250 border-top: 4px solid var(--primary-color); 250 251 border-radius: 50%; 251 252 width: 24px; ··· 267 268 } 268 269 269 270 .feedItem { 270 - background-color: white; 271 - border: 1px solid #e1e1e1; 271 + background-color: var(--card-background); 272 + border: 1px solid var(--tile-border); 272 273 border-radius: 8px; 273 274 padding: 1rem; 274 - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); 275 + box-shadow: 0 2px 5px var(--shadow-color); 275 276 transition: transform 0.2s, box-shadow 0.2s; 277 + background-image: repeating-linear-gradient(0deg, var(--tile-border), var(--tile-border) 1px, transparent 1px, transparent 20px); 276 278 } 277 279 278 280 .feedItem:hover { 279 281 transform: translateY(-2px); 280 - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 282 + box-shadow: 0 4px 8px var(--shadow-color); 281 283 } 282 284 283 285 @media (max-width: 600px) { ··· 285 287 padding: 0.75rem; 286 288 margin-bottom: 0.5rem; 287 289 border-radius: 6px; 288 - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03); 290 + box-shadow: 0 1px 3px var(--shadow-color); 289 291 } 290 292 291 293 .feedList { ··· 294 296 295 297 .feedItem:hover { 296 298 transform: none; 297 - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); 299 + box-shadow: 0 1px 3px var(--shadow-color); 298 300 } 299 301 300 302 .content { ··· 391 393 .text { 392 394 font-size: 1.1rem; 393 395 line-height: 1.4; 394 - color: #333; 396 + color: var(--text-color); 395 397 flex: 1; 396 398 min-width: 0; 397 399 word-wrap: break-word; ··· 401 403 402 404 .timestamp { 403 405 font-size: 0.85rem; 404 - color: #888; 406 + color: var(--timestamp-color); 405 407 margin-left: auto; 406 408 white-space: nowrap; 407 409 } ··· 427 429 margin-top: 0; 428 430 padding-left: 0.25rem; 429 431 font-size: 0.8rem; 432 + color: var(--timestamp-color); 430 433 } 431 434 432 435 .emoji { ··· 442 445 .emptyState { 443 446 text-align: center; 444 447 padding: 2rem; 445 - background-color: #f9f9f9; 448 + background-color: var(--background-color); 446 449 border-radius: 8px; 447 - border: 1px dashed #ccc; 450 + border: 1px dashed var(--tile-border); 448 451 }
+29 -26
app/src/app/stats/stats.module.css
··· 30 30 31 31 .subtitle { 32 32 font-size: 1rem; 33 - color: #888; 33 + color: var(--timestamp-color); 34 34 margin: 0 0 0.5rem 0; 35 35 font-weight: normal; 36 36 word-wrap: break-word; ··· 38 38 39 39 .description { 40 40 font-size: 1.1rem; 41 - color: #666; 41 + color: var(--text-color); 42 42 margin: 0; 43 43 line-height: 1.5; 44 44 word-wrap: break-word; ··· 48 48 display: block; 49 49 font-size: 0.85rem; 50 50 margin-top: 0.5rem; 51 - color: #888; 51 + color: var(--timestamp-color); 52 52 } 53 53 54 54 .kofiLink { ··· 68 68 } 69 69 70 70 .userInfo { 71 - color: #555; 71 + color: var(--text-color); 72 72 font-weight: 500; 73 73 text-decoration: none; 74 74 transition: color 0.2s; ··· 109 109 .loginButton:hover { 110 110 background-color: var(--secondary-color); 111 111 transform: translateY(-2px); 112 - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 112 + box-shadow: 0 4px 8px var(--shadow-color); 113 113 } 114 114 115 115 /* Stats Page Specific Styles */ ··· 125 125 } 126 126 127 127 .statsSubtitle { 128 - color: #666; 128 + color: var(--text-color); 129 129 font-size: 1.2rem; 130 130 margin: 0; 131 131 } ··· 182 182 } 183 183 184 184 .loader { 185 - border: 4px solid #f3f3f3; 185 + border: 4px solid var(--input-background); 186 186 border-top: 4px solid var(--primary-color); 187 187 border-radius: 50%; 188 188 width: 40px; ··· 197 197 } 198 198 199 199 .error { 200 - background-color: #ffebee; 201 - color: #c62828; 200 + background-color: var(--error-background); 201 + color: var(--error-color); 202 202 padding: 1rem; 203 203 border-radius: 4px; 204 204 margin-bottom: 1rem; ··· 207 207 .emptyState { 208 208 text-align: center; 209 209 padding: 3rem; 210 - color: #666; 210 + color: var(--text-color); 211 211 } 212 212 213 213 /* Stats Content Sections */ ··· 218 218 } 219 219 220 220 .overallStats, .chartSection, .leaderboardSection { 221 - background: white; 221 + background: var(--card-background); 222 222 border-radius: 8px; 223 223 padding: 1.5rem; 224 - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); 224 + box-shadow: 0 2px 8px var(--shadow-color); 225 + border: 1px solid var(--tile-border); 226 + background-image: repeating-linear-gradient(0deg, var(--tile-border), var(--tile-border) 1px, transparent 1px, transparent 20px); 225 227 } 226 228 227 229 .overallStats h2, .chartSection h2, .leaderboardSection h2 { ··· 239 241 } 240 242 241 243 .statCard { 242 - background: #f8f9fa; 244 + background: var(--input-background); 243 245 padding: 1.5rem; 244 246 border-radius: 8px; 245 247 text-align: center; 246 248 transition: transform 0.2s; 249 + border: 1px solid var(--tile-border); 247 250 } 248 251 249 252 .statCard:hover { 250 253 transform: translateY(-5px); 251 - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); 254 + box-shadow: 0 4px 12px var(--shadow-color); 252 255 } 253 256 254 257 .statValue { ··· 259 262 } 260 263 261 264 .statLabel { 262 - color: #666; 265 + color: var(--text-color); 263 266 font-size: 1.1rem; 264 267 } 265 268 ··· 285 288 .chartLegend { 286 289 display: flex; 287 290 justify-content: space-between; 288 - color: #666; 291 + color: var(--timestamp-color); 289 292 font-size: 0.9rem; 290 293 } 291 294 292 295 .noDataMessage { 293 296 text-align: center; 294 - color: #666; 297 + color: var(--timestamp-color); 295 298 font-style: italic; 296 299 padding: 2rem 0; 297 300 } 298 301 299 302 /* Leaderboard Styles */ 300 303 .leaderboard { 301 - border: 1px solid #eee; 304 + border: 1px solid var(--tile-border); 302 305 border-radius: 8px; 303 306 overflow: hidden; 304 307 } ··· 307 310 display: grid; 308 311 grid-template-columns: 80px 1fr 100px; 309 312 padding: 1rem; 310 - background: #f5f5f5; 313 + background: var(--input-background); 311 314 font-weight: bold; 312 - color: #333; 315 + color: var(--title-color); 313 316 } 314 317 315 318 .leaderboardItem { 316 319 display: grid; 317 320 grid-template-columns: 80px 1fr 100px; 318 321 padding: 1rem; 319 - border-top: 1px solid #eee; 322 + border-top: 1px solid var(--tile-border); 320 323 transition: background-color 0.2s; 321 324 } 322 325 323 326 .leaderboardItem:hover { 324 - background-color: #f9f9f9; 327 + background-color: var(--button-hover); 325 328 } 326 329 327 330 .topRank { 328 - background-color: #fff8e1; 331 + background-color: rgba(255, 193, 7, 0.1); 329 332 } 330 333 331 334 .rank { 332 335 font-weight: bold; 333 - color: #666; 336 + color: var(--text-color); 334 337 } 335 338 336 339 .user a { ··· 344 347 } 345 348 346 349 .unknownUser { 347 - color: #999; 350 + color: var(--timestamp-color); 348 351 font-style: italic; 349 352 } 350 353 ··· 374 377 .shareButton:hover { 375 378 background-color: var(--secondary-color); 376 379 transform: translateY(-2px); 377 - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 380 + box-shadow: 0 4px 8px var(--shadow-color); 378 381 } 379 382 380 383 /* Responsive Adjustments */
+114 -3
app/src/components/ProfileSearch.module.css
··· 1 + .searchContainer { 2 + position: relative; 3 + width: 260px; 4 + } 5 + 1 6 .searchForm { 2 7 display: flex; 3 8 align-items: center; ··· 5 10 border: 1px solid var(--input-border); 6 11 border-radius: 24px; 7 12 overflow: hidden; 8 - width: 260px; 13 + width: 100%; 9 14 transition: all 0.2s ease; 10 15 } 11 16 ··· 44 49 background-color: rgba(91, 173, 240, 0.1); 45 50 } 46 51 52 + /* Suggestions dropdown */ 53 + .suggestionsContainer { 54 + position: absolute; 55 + top: 100%; 56 + left: 0; 57 + right: 0; 58 + margin-top: 5px; 59 + background-color: var(--card-background); 60 + border: 1px solid var(--tile-border); 61 + border-radius: 8px; 62 + box-shadow: 0 4px 12px var(--shadow-color); 63 + max-height: 300px; 64 + overflow-y: auto; 65 + z-index: 10; 66 + } 67 + 68 + .suggestionsList { 69 + list-style: none; 70 + padding: 0; 71 + margin: 0; 72 + } 73 + 74 + .suggestionItem { 75 + padding: 0; 76 + margin: 0; 77 + border-bottom: 1px solid var(--tile-border); 78 + } 79 + 80 + .suggestionItem:last-child { 81 + border-bottom: none; 82 + } 83 + 84 + .suggestionButton { 85 + display: flex; 86 + align-items: center; 87 + width: 100%; 88 + text-align: left; 89 + padding: 0.75rem 1rem; 90 + background: none; 91 + border: none; 92 + cursor: pointer; 93 + transition: background-color 0.2s; 94 + color: var(--text-color); 95 + } 96 + 97 + .suggestionButton:hover { 98 + background-color: var(--button-hover); 99 + } 100 + 101 + .suggestionInfo { 102 + display: flex; 103 + flex-direction: column; 104 + } 105 + 106 + .displayName { 107 + font-weight: 600; 108 + font-size: 0.9rem; 109 + margin-bottom: 0.2rem; 110 + } 111 + 112 + .handle { 113 + font-size: 0.8rem; 114 + color: var(--timestamp-color); 115 + } 116 + 117 + .noResults { 118 + padding: 1rem; 119 + text-align: center; 120 + color: var(--timestamp-color); 121 + font-style: italic; 122 + } 123 + 124 + .loadingContainer { 125 + display: flex; 126 + justify-content: center; 127 + padding: 1rem; 128 + gap: 0.3rem; 129 + } 130 + 131 + .loadingDot { 132 + width: 8px; 133 + height: 8px; 134 + border-radius: 50%; 135 + background-color: var(--primary-color); 136 + animation: dotPulse 1.4s infinite ease-in-out; 137 + } 138 + 139 + .loadingDot:nth-child(2) { 140 + animation-delay: 0.2s; 141 + } 142 + 143 + .loadingDot:nth-child(3) { 144 + animation-delay: 0.4s; 145 + } 146 + 147 + @keyframes dotPulse { 148 + 0%, 80%, 100% { 149 + transform: scale(0.8); 150 + opacity: 0.5; 151 + } 152 + 40% { 153 + transform: scale(1.2); 154 + opacity: 1; 155 + } 156 + } 157 + 47 158 @media (max-width: 600px) { 48 - .searchForm { 159 + .searchContainer { 49 160 width: 180px; 50 161 } 51 162 ··· 56 167 } 57 168 58 169 @media (max-width: 450px) { 59 - .searchForm { 170 + .searchContainer { 60 171 width: 140px; 61 172 } 62 173
+136 -17
app/src/components/ProfileSearch.tsx
··· 1 1 'use client'; 2 2 3 - import React, { useState } from 'react'; 3 + import React, { useState, useEffect, useRef } from 'react'; 4 4 import { useRouter } from 'next/navigation'; 5 5 import styles from './ProfileSearch.module.css'; 6 6 7 + type UserSuggestion = { 8 + did: string; 9 + handle: string; 10 + displayName?: string; 11 + avatar?: string | null; 12 + }; 13 + 7 14 export default function ProfileSearch() { 8 15 const [query, setQuery] = useState(''); 16 + const [suggestions, setSuggestions] = useState<UserSuggestion[]>([]); 17 + const [loading, setLoading] = useState(false); 18 + const [showSuggestions, setShowSuggestions] = useState(false); 19 + const suggestionsRef = useRef<HTMLDivElement>(null); 20 + const inputRef = useRef<HTMLInputElement>(null); 9 21 const router = useRouter(); 22 + const debounceTimerRef = useRef<NodeJS.Timeout | null>(null); 23 + 24 + // Close suggestions when clicking outside 25 + useEffect(() => { 26 + const handleClickOutside = (event: MouseEvent) => { 27 + if ( 28 + suggestionsRef.current && 29 + !suggestionsRef.current.contains(event.target as Node) && 30 + !inputRef.current?.contains(event.target as Node) 31 + ) { 32 + setShowSuggestions(false); 33 + } 34 + }; 35 + 36 + document.addEventListener('mousedown', handleClickOutside); 37 + return () => { 38 + document.removeEventListener('mousedown', handleClickOutside); 39 + }; 40 + }, []); 41 + 42 + // Fetch suggestions when query changes 43 + useEffect(() => { 44 + // Clear previous timer 45 + if (debounceTimerRef.current) { 46 + clearTimeout(debounceTimerRef.current); 47 + } 48 + 49 + // Return early if query is too short 50 + if (query.trim().length < 2) { 51 + setSuggestions([]); 52 + setShowSuggestions(false); 53 + return; 54 + } 55 + 56 + // Set a new timer to fetch suggestions 57 + debounceTimerRef.current = setTimeout(async () => { 58 + setLoading(true); 59 + try { 60 + // Normalize query by removing @ if present 61 + const searchTerm = query.trim().startsWith('@') 62 + ? query.trim().substring(1) 63 + : query.trim(); 64 + 65 + const response = await fetch(`/api/bluesky/search?q=${encodeURIComponent(searchTerm)}`); 66 + 67 + if (response.ok) { 68 + const data = await response.json(); 69 + setSuggestions(data.suggestions); 70 + setShowSuggestions(true); 71 + } 72 + } catch (error) { 73 + console.error('Failed to fetch suggestions:', error); 74 + } finally { 75 + setLoading(false); 76 + } 77 + }, 300); // 300ms debounce delay 78 + 79 + return () => { 80 + if (debounceTimerRef.current) { 81 + clearTimeout(debounceTimerRef.current); 82 + } 83 + }; 84 + }, [query]); 10 85 11 86 const handleSearch = (e: React.FormEvent) => { 12 87 e.preventDefault(); ··· 17 92 : query.trim(); 18 93 19 94 router.push(`/profile/${handle}`); 95 + setShowSuggestions(false); 20 96 } 21 97 }; 22 98 99 + const handleSuggestionClick = (handle: string) => { 100 + router.push(`/profile/${handle}`); 101 + setQuery(`@${handle}`); 102 + setShowSuggestions(false); 103 + }; 104 + 23 105 return ( 24 - <form onSubmit={handleSearch} className={styles.searchForm}> 25 - <input 26 - type="text" 27 - value={query} 28 - onChange={(e) => setQuery(e.target.value)} 29 - placeholder="Search user @handle" 30 - className={styles.searchInput} 31 - aria-label="Search for a user profile" 32 - /> 33 - <button type="submit" className={styles.searchButton}> 34 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> 35 - <circle cx="11" cy="11" r="8"></circle> 36 - <line x1="21" y1="21" x2="16.65" y2="16.65"></line> 37 - </svg> 38 - </button> 39 - </form> 106 + <div className={styles.searchContainer}> 107 + <form onSubmit={handleSearch} className={styles.searchForm}> 108 + <input 109 + ref={inputRef} 110 + type="text" 111 + value={query} 112 + onChange={(e) => setQuery(e.target.value)} 113 + onFocus={() => query.trim().length >= 2 && setShowSuggestions(true)} 114 + placeholder="Search user @handle" 115 + className={styles.searchInput} 116 + aria-label="Search for a user profile" 117 + /> 118 + <button type="submit" className={styles.searchButton}> 119 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> 120 + <circle cx="11" cy="11" r="8"></circle> 121 + <line x1="21" y1="21" x2="16.65" y2="16.65"></line> 122 + </svg> 123 + </button> 124 + </form> 125 + 126 + {showSuggestions && ( 127 + <div ref={suggestionsRef} className={styles.suggestionsContainer}> 128 + {loading ? ( 129 + <div className={styles.loadingContainer}> 130 + <span className={styles.loadingDot}></span> 131 + <span className={styles.loadingDot}></span> 132 + <span className={styles.loadingDot}></span> 133 + </div> 134 + ) : suggestions.length > 0 ? ( 135 + <ul className={styles.suggestionsList}> 136 + {suggestions.map((suggestion) => ( 137 + <li key={suggestion.did} className={styles.suggestionItem}> 138 + <button 139 + type="button" 140 + onClick={() => handleSuggestionClick(suggestion.handle)} 141 + className={styles.suggestionButton} 142 + > 143 + <div className={styles.suggestionInfo}> 144 + {suggestion.displayName && ( 145 + <span className={styles.displayName}>{suggestion.displayName}</span> 146 + )} 147 + <span className={styles.handle}>@{suggestion.handle}</span> 148 + </div> 149 + </button> 150 + </li> 151 + ))} 152 + </ul> 153 + ) : ( 154 + <div className={styles.noResults}>No users found</div> 155 + )} 156 + </div> 157 + )} 158 + </div> 40 159 ); 41 160 }
+13
app/src/components/ThemeToggle.module.css
··· 10 10 cursor: pointer; 11 11 transition: all 0.2s; 12 12 margin-left: auto; 13 + white-space: nowrap; 13 14 } 14 15 15 16 .themeToggle:hover { 16 17 background-color: var(--button-hover); 18 + transform: translateY(-2px); 19 + box-shadow: 0 2px 4px var(--shadow-color); 17 20 } 18 21 19 22 .themeToggle svg { ··· 24 27 .themeLabel { 25 28 font-size: 0.9rem; 26 29 font-weight: 500; 30 + } 31 + 32 + @media (max-width: 768px) { 33 + .themeToggle { 34 + padding: 0.5rem 0.7rem; 35 + } 36 + 37 + .themeLabel { 38 + font-size: 0.8rem; 39 + } 27 40 } 28 41 29 42 @media (max-width: 600px) {
+4 -4
app/src/components/ThemeToggle.tsx
··· 81 81 }; 82 82 83 83 const getLabel = () => { 84 - if (themeState === 'light') return 'Light'; 85 - if (themeState === 'dark') return 'Dark'; 86 - return 'System'; 84 + if (themeState === 'light') return 'Lights On'; 85 + if (themeState === 'dark') return 'Lights Off'; 86 + return 'System Lights'; 87 87 }; 88 88 89 89 // During SSR or before mounting, render a placeholder that won't try to use the context ··· 91 91 return ( 92 92 <button className={styles.themeToggle} aria-label="Theme toggle"> 93 93 <LightIcon /> 94 - <span className={styles.themeLabel}>Theme</span> 94 + <span className={styles.themeLabel}>Lights On</span> 95 95 </button> 96 96 ); 97 97 }