Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

feat(embed): Add support for dark mode (#6912)

* feat(embed): Support dark mode (wip)

* finishing up the implementation

* fix tailwind color selector

* tweak design

* refactor: unify types

* fix

* fix english grammar

* refactor: unify types

* tweak design

* remove the customization part

authored by

かっこかり and committed by
GitHub
de15f8e2 48c53416

+88 -42
+3 -1
bskyembed/snippet/embed.ts
··· 20 20 return 21 21 } 22 22 23 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 23 24 const id = (event.data as {id: string}).id 24 25 if (!id) { 25 26 return ··· 33 34 return 34 35 } 35 36 37 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 36 38 const height = (event.data as {height: number}).height 37 39 if (height) { 38 40 embed.style.height = `${height}px` ··· 47 49 * @returns 48 50 */ 49 51 function scan(node = document) { 50 - const embeds = node.querySelectorAll('[data-bluesky-uri]') 52 + const embeds = node.querySelectorAll<HTMLIFrameElement>('[data-bluesky-uri]') 51 53 52 54 for (let i = 0; i < embeds.length; i++) { 53 55 const id = String(Math.random()).slice(2)
+17
bskyembed/src/color-mode.ts
··· 1 + export function applyTheme(theme: 'light' | 'dark') { 2 + document.documentElement.classList.remove('light', 'dark') 3 + document.documentElement.classList.add(theme) 4 + } 5 + 6 + export function initColorMode() { 7 + applyTheme( 8 + window.matchMedia('(prefers-color-scheme: dark)').matches 9 + ? 'dark' 10 + : 'light', 11 + ) 12 + window 13 + .matchMedia('(prefers-color-scheme: dark)') 14 + .addEventListener('change', mql => { 15 + applyTheme(mql.matches ? 'dark' : 'light') 16 + }) 17 + }
+1 -1
bskyembed/src/components/container.tsx
··· 37 37 return ( 38 38 <div 39 39 ref={ref} 40 - className="w-full bg-white hover:bg-neutral-50 relative transition-colors max-w-[600px] min-w-[300px] flex border rounded-xl" 40 + className="w-full bg-white text-black hover:bg-neutral-50 dark:bg-dimmedBg dark:hover:bg-dimmedBgLighten relative transition-colors max-w-[600px] min-w-[300px] flex border dark:border-slate-600 dark:text-slate-200 rounded-xl" 41 41 onClick={() => { 42 42 if (ref.current && href) { 43 43 // forwardRef requires preact/compat - let's keep it simple
+20 -14
bskyembed/src/components/embed.tsx
··· 78 78 return ( 79 79 <Link 80 80 href={`/profile/${record.author.did}/post/${getRkey(record)}`} 81 - className="transition-colors hover:bg-neutral-100 border rounded-lg p-2 gap-1.5 w-full flex flex-col"> 81 + className="transition-colors hover:bg-neutral-100 dark:hover:bg-slate-700 border dark:border-slate-600 rounded-lg p-2 gap-1.5 w-full flex flex-col"> 82 82 <div className="flex gap-1.5 items-center"> 83 - <div className="w-4 h-4 overflow-hidden rounded-full bg-neutral-300 shrink-0"> 83 + <div className="w-4 h-4 overflow-hidden rounded-full bg-neutral-300 dark:bg-slate-700 shrink-0"> 84 84 <img 85 85 src={record.author.avatar} 86 86 style={isAuthorLabeled ? {filter: 'blur(1.5px)'} : undefined} ··· 88 88 </div> 89 89 <p className="line-clamp-1 text-sm"> 90 90 <span className="font-bold">{record.author.displayName}</span> 91 - <span className="text-textLight ml-1"> 91 + <span className="text-textLight dark:text-textDimmed ml-1"> 92 92 @{record.author.handle} 93 93 </span> 94 94 </p> ··· 209 209 return ( 210 210 <div className="w-full rounded-lg border py-2 px-2.5 flex-row flex gap-2 bg-neutral-50"> 211 211 <img src={infoIcon} className="w-4 h-4 shrink-0 mt-0.5" /> 212 - <p className="text-sm text-textLight">{children}</p> 212 + <p className="text-sm text-textLight dark:text-textDimmed">{children}</p> 213 213 </div> 214 214 ) 215 215 } ··· 308 308 return ( 309 309 <Link 310 310 href={content.external.uri} 311 - className="w-full rounded-lg overflow-hidden border flex flex-col items-stretch" 311 + className="w-full rounded-lg overflow-hidden border dark:border-slate-600 flex flex-col items-stretch" 312 312 disableTracking> 313 313 {content.external.thumb && ( 314 314 <img ··· 317 317 /> 318 318 )} 319 319 <div className="py-3 px-4"> 320 - <p className="text-sm text-textLight line-clamp-1"> 320 + <p className="text-sm text-textLight dark:text-textDimmed line-clamp-1"> 321 321 {toNiceDomain(content.external.uri)} 322 322 </p> 323 323 <p className="font-semibold line-clamp-3">{content.external.title}</p> 324 - <p className="text-sm text-textLight line-clamp-2 mt-0.5"> 324 + <p className="text-sm text-textLight dark:text-textDimmed line-clamp-2 mt-0.5"> 325 325 {content.external.description} 326 326 </p> 327 327 </div> ··· 345 345 return ( 346 346 <Link 347 347 href={href} 348 - className="w-full rounded-lg border py-2 px-3 flex flex-col gap-2"> 348 + className="w-full rounded-lg border dark:border-slate-600 py-2 px-3 flex flex-col gap-2"> 349 349 <div className="flex gap-2.5 items-center"> 350 350 {image ? ( 351 351 <img 352 352 src={image} 353 353 alt={title} 354 - className="w-8 h-8 rounded-md bg-neutral-300 shrink-0" 354 + className="w-8 h-8 rounded-md bg-neutral-300 dark:bg-slate-700 shrink-0" 355 355 /> 356 356 ) : ( 357 357 <div className="w-8 h-8 rounded-md bg-brand shrink-0" /> 358 358 )} 359 359 <div className="flex-1"> 360 360 <p className="font-bold text-sm">{title}</p> 361 - <p className="text-textLight text-sm">{subtitle}</p> 361 + <p className="text-textLight dark:text-textDimmed text-sm"> 362 + {subtitle} 363 + </p> 362 364 </div> 363 365 </div> 364 - {description && <p className="text-textLight text-sm">{description}</p>} 366 + {description && ( 367 + <p className="text-textLight dark:text-textDimmed text-sm"> 368 + {description} 369 + </p> 370 + )} 365 371 </Link> 366 372 ) 367 373 } ··· 406 412 return ( 407 413 <Link 408 414 href={starterPackHref} 409 - className="w-full rounded-lg overflow-hidden border flex flex-col items-stretch"> 415 + className="w-full rounded-lg overflow-hidden border dark:border-slate-600 flex flex-col items-stretch"> 410 416 <img src={imageUri} className="aspect-[1.91/1] object-cover" /> 411 417 <div className="py-3 px-4"> 412 418 <div className="flex space-x-2 items-center"> ··· 415 421 <p className="font-semibold leading-[21px]"> 416 422 {content.record.name} 417 423 </p> 418 - <p className="text-sm text-textLight line-clamp-2 leading-[18px]"> 424 + <p className="text-sm text-textLight dark:text-textDimmed line-clamp-2 leading-[18px]"> 419 425 Starter pack by{' '} 420 426 {content.creator.displayName || `@${content.creator.handle}`} 421 427 </p> ··· 425 431 <p className="text-sm mt-1">{content.record.description}</p> 426 432 )} 427 433 {!!content.joinedAllTimeCount && content.joinedAllTimeCount > 50 && ( 428 - <p className="text-sm font-semibold text-textLight mt-1"> 434 + <p className="text-sm font-semibold text-textLight dark:text-textDimmed mt-1"> 429 435 {content.joinedAllTimeCount} users have joined! 430 436 </p> 431 437 )}
+10 -8
bskyembed/src/components/post.tsx
··· 38 38 <div className="flex-1 flex-col flex gap-2" lang={record?.langs?.[0]}> 39 39 <div className="flex gap-2.5 items-center cursor-pointer"> 40 40 <Link href={`/profile/${post.author.did}`} className="rounded-full"> 41 - <div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-300 shrink-0"> 41 + <div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-300 dark:bg-slate-700 shrink-0"> 42 42 <img 43 43 src={post.author.avatar} 44 44 style={isAuthorLabeled ? {filter: 'blur(2.5px)'} : undefined} ··· 53 53 </Link> 54 54 <Link 55 55 href={`/profile/${post.author.did}`} 56 - className="text-[15px] text-textLight hover:underline line-clamp-1"> 56 + className="text-[15px] text-textLight dark:text-textDimmed hover:underline line-clamp-1"> 57 57 <p>@{post.author.handle}</p> 58 58 </Link> 59 59 </div> ··· 69 69 <Link href={href}> 70 70 <time 71 71 datetime={new Date(post.indexedAt).toISOString()} 72 - className="text-textLight mt-1 text-sm hover:underline"> 72 + className="text-textLight dark:text-textDimmed mt-1 text-sm hover:underline"> 73 73 {niceDate(post.indexedAt)} 74 74 </time> 75 75 </Link> 76 - <div className="border-t w-full pt-2.5 flex items-center gap-5 text-sm cursor-pointer"> 76 + <div className="border-t dark:border-slate-600 w-full pt-2.5 flex items-center gap-5 text-sm cursor-pointer"> 77 77 {!!post.likeCount && ( 78 78 <div className="flex items-center gap-2 cursor-pointer"> 79 79 <img src={likeIcon} className="w-5 h-5" /> 80 - <p className="font-bold text-neutral-500 mb-px"> 80 + <p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px"> 81 81 {prettyNumber(post.likeCount)} 82 82 </p> 83 83 </div> ··· 85 85 {!!post.repostCount && ( 86 86 <div className="flex items-center gap-2 cursor-pointer"> 87 87 <img src={repostIcon} className="w-5 h-5" /> 88 - <p className="font-bold text-neutral-500 mb-px"> 88 + <p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px"> 89 89 {prettyNumber(post.repostCount)} 90 90 </p> 91 91 </div> 92 92 )} 93 93 <div className="flex items-center gap-2 cursor-pointer"> 94 94 <img src={replyIcon} className="w-5 h-5" /> 95 - <p className="font-bold text-neutral-500 mb-px">Reply</p> 95 + <p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px"> 96 + Reply 97 + </p> 96 98 </div> 97 99 <div className="flex-1" /> 98 - <p className="cursor-pointer text-brand font-bold hover:underline hidden min-[450px]:inline"> 100 + <p className="cursor-pointer text-brand dark:text-brandLighten font-bold hover:underline hidden min-[450px]:inline"> 99 101 {post.replyCount 100 102 ? `Read ${prettyNumber(post.replyCount)} ${ 101 103 post.replyCount > 1 ? 'replies' : 'reply'
+4
bskyembed/src/index.css
··· 5 5 .break-word { 6 6 word-break: break-word; 7 7 } 8 + 9 + :root { 10 + color-scheme: light dark; 11 + }
+18 -13
bskyembed/src/screens/landing.tsx
··· 6 6 7 7 import arrowBottom from '../../assets/arrowBottom_stroke2_corner0_rounded.svg' 8 8 import logo from '../../assets/logo.svg' 9 + import {initColorMode} from '../color-mode' 9 10 import {Container} from '../components/container' 10 11 import {Link} from '../components/link' 11 12 import {Post} from '../components/post' ··· 20 21 21 22 const root = document.getElementById('app') 22 23 if (!root) throw new Error('No root element') 24 + 25 + initColorMode() 23 26 24 27 const agent = new BskyAgent({ 25 28 service: 'https://public.api.bsky.app', ··· 108 111 }, [uri]) 109 112 110 113 return ( 111 - <main className="w-full min-h-screen flex flex-col items-center gap-8 py-14 px-4 md:pt-32"> 114 + <main className="w-full min-h-screen flex flex-col items-center gap-8 py-14 px-4 md:pt-32 dark:bg-dimmedBgDarken dark:text-slate-200"> 112 115 <Link 113 116 href="https://bsky.social/about" 114 117 className="transition-transform hover:scale-110"> ··· 121 124 type="text" 122 125 value={uri} 123 126 onInput={e => setUri(e.currentTarget.value)} 124 - className="border rounded-lg py-3 w-full max-w-[600px] px-4" 127 + className="border rounded-lg py-3 w-full max-w-[600px] px-4 dark:bg-dimmedBg dark:border-slate-500" 125 128 placeholder={DEFAULT_POST} 126 129 /> 127 130 128 - <img src={arrowBottom} className="w-6" /> 131 + <img src={arrowBottom} className="w-6 dark:invert" /> 129 132 130 133 {loading ? ( 131 - <Skeleton /> 134 + <div className="w-full max-w-[600px]"> 135 + <Skeleton /> 136 + </div> 132 137 ) : ( 133 138 <div className="w-full max-w-[600px] gap-8 flex flex-col"> 134 139 {!error && thread && uri && <Snippet thread={thread} />} 135 140 {!error && thread && <Post thread={thread} key={thread.post.uri} />} 136 141 {error && ( 137 - <div className="w-full border border-red-500 bg-red-50 px-4 py-3 rounded-lg"> 142 + <div className="w-full border border-red-500 bg-red-500/10 px-4 py-3 rounded-lg"> 138 143 <p className="text-red-500 text-center">{error}</p> 139 144 </div> 140 145 )} ··· 149 154 <Container> 150 155 <div className="flex-1 flex-col flex gap-2 pb-8"> 151 156 <div className="flex gap-2.5 items-center"> 152 - <div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-100 shrink-0 animate-pulse" /> 157 + <div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-100 dark:bg-slate-700 shrink-0 animate-pulse" /> 153 158 <div className="flex-1"> 154 - <div className="bg-neutral-100 animate-pulse w-64 h-4 rounded" /> 155 - <div className="bg-neutral-100 animate-pulse w-32 h-3 mt-1 rounded" /> 159 + <div className="bg-neutral-100 dark:bg-slate-700 animate-pulse w-64 h-4 rounded" /> 160 + <div className="bg-neutral-100 dark:bg-slate-700 animate-pulse w-32 h-3 mt-1 rounded" /> 156 161 </div> 157 162 </div> 158 - <div className="w-full h-4 mt-2 bg-neutral-100 rounded animate-pulse" /> 159 - <div className="w-5/6 h-4 bg-neutral-100 rounded animate-pulse" /> 160 - <div className="w-3/4 h-4 bg-neutral-100 rounded animate-pulse" /> 163 + <div className="w-full h-4 mt-2 bg-neutral-100 dark:bg-slate-700 rounded animate-pulse" /> 164 + <div className="w-5/6 h-4 bg-neutral-100 dark:bg-slate-700 rounded animate-pulse" /> 165 + <div className="w-3/4 h-4 bg-neutral-100 dark:bg-slate-700 rounded animate-pulse" /> 161 166 </div> 162 167 </Container> 163 168 ) ··· 220 225 ref={ref} 221 226 type="text" 222 227 value={snippet} 223 - className="border rounded-lg py-3 w-full px-4" 228 + className="border rounded-lg py-3 w-full px-4 dark:bg-dimmedBg dark:border-slate-500" 224 229 readOnly 225 230 autoFocus 226 231 onFocus={() => { ··· 228 233 }} 229 234 /> 230 235 <button 231 - className="rounded-lg bg-brand text-white color-white py-3 px-4 whitespace-nowrap min-w-28" 236 + className="rounded-lg bg-brand text-white py-3 px-4 whitespace-nowrap min-w-28" 232 237 onClick={() => { 233 238 ref.current?.focus() 234 239 ref.current?.select()
+6 -3
bskyembed/src/screens/post.tsx
··· 4 4 import {h, render} from 'preact' 5 5 6 6 import logo from '../../assets/logo.svg' 7 + import {initColorMode} from '../color-mode' 7 8 import {Container} from '../components/container' 8 9 import {Link} from '../components/link' 9 10 import {Post} from '../components/post' ··· 20 21 if (!uri) { 21 22 throw new Error('No uri in path') 22 23 } 24 + 25 + initColorMode() 23 26 24 27 agent 25 28 .getPostThread({ ··· 55 58 <img src={logo} className="h-6" /> 56 59 </Link> 57 60 <div className="w-full py-12 gap-4 flex flex-col items-center"> 58 - <p className="max-w-80 text-center w-full text-textLight"> 61 + <p className="max-w-80 text-center w-full text-textLight dark:text-textDimmed"> 59 62 The author of this post has requested their posts not be displayed on 60 63 external sites. 61 64 </p> 62 65 <Link 63 66 href={href} 64 - className="max-w-80 rounded-lg bg-brand text-white color-white text-center py-1 px-4 w-full mx-auto"> 67 + className="max-w-80 rounded-lg bg-brand text-white text-center py-1 px-4 w-full mx-auto"> 65 68 View on Bluesky 66 69 </Link> 67 70 </div> ··· 77 80 className="transition-transform hover:scale-110 absolute top-4 right-4"> 78 81 <img src={logo} className="h-6" /> 79 82 </Link> 80 - <p className="my-16 text-center w-full text-textLight"> 83 + <p className="my-16 text-center w-full text-textLight dark:text-textDimmed"> 81 84 Post not found, it may have been deleted. 82 85 </p> 83 86 </Container>
+8
bskyembed/tailwind.config.cjs
··· 1 1 /** @type {import('tailwindcss').Config} */ 2 2 module.exports = { 3 3 content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], 4 + darkMode: ['variant', [ 5 + '&:is(.dark *):not(:is(.dark .light *))', 6 + ]], 4 7 theme: { 5 8 extend: { 6 9 colors: { 7 10 brand: 'rgb(10,122,255)', 11 + brandLighten: 'rgb(32,139,254)', 8 12 textLight: 'rgb(66,87,108)', 13 + textDimmed: 'rgb(174,187,201)', 14 + dimmedBgLighten: 'rgb(30,41,54)', 15 + dimmedBg: 'rgb(22,30,39)', 16 + dimmedBgDarken: 'rgb(18,25,32)', 9 17 }, 10 18 }, 11 19 },
+1 -2
bskyembed/tsconfig.json
··· 1 - 2 1 { 3 2 "compilerOptions": { 4 3 "target": "ES5", ··· 20 19 "jsxFragmentFactory": "Fragment", 21 20 "downlevelIteration": true 22 21 }, 23 - "include": ["src", "vite.config.ts"] 22 + "include": ["src", "snippet", "vite.config.ts"] 24 23 }