diff options
author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2024-12-12 01:31:29 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-12-11 16:31:29 +0000 |
commit | de15f8e2d3264b9ffb18c3a089612ef604eb87b8 (patch) | |
tree | ae5419e798d55b978596ca7a396fdb395d2cf4fc /bskyembed | |
parent | 48c5341644935b103fa81c91b23391f7b7c8a572 (diff) | |
download | voidsky-de15f8e2d3264b9ffb18c3a089612ef604eb87b8.tar.zst |
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
Diffstat (limited to 'bskyembed')
-rw-r--r-- | bskyembed/snippet/embed.ts | 4 | ||||
-rw-r--r-- | bskyembed/src/color-mode.ts | 17 | ||||
-rw-r--r-- | bskyembed/src/components/container.tsx | 2 | ||||
-rw-r--r-- | bskyembed/src/components/embed.tsx | 34 | ||||
-rw-r--r-- | bskyembed/src/components/post.tsx | 18 | ||||
-rw-r--r-- | bskyembed/src/index.css | 4 | ||||
-rw-r--r-- | bskyembed/src/screens/landing.tsx | 31 | ||||
-rw-r--r-- | bskyembed/src/screens/post.tsx | 9 | ||||
-rw-r--r-- | bskyembed/tailwind.config.cjs | 8 | ||||
-rw-r--r-- | bskyembed/tsconfig.json | 3 |
10 files changed, 88 insertions, 42 deletions
diff --git a/bskyembed/snippet/embed.ts b/bskyembed/snippet/embed.ts index 380cda5fb..3c1b14b95 100644 --- a/bskyembed/snippet/embed.ts +++ b/bskyembed/snippet/embed.ts @@ -20,6 +20,7 @@ window.addEventListener('message', event => { return } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const id = (event.data as {id: string}).id if (!id) { return @@ -33,6 +34,7 @@ window.addEventListener('message', event => { return } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const height = (event.data as {height: number}).height if (height) { embed.style.height = `${height}px` @@ -47,7 +49,7 @@ window.addEventListener('message', event => { * @returns */ function scan(node = document) { - const embeds = node.querySelectorAll('[data-bluesky-uri]') + const embeds = node.querySelectorAll<HTMLIFrameElement>('[data-bluesky-uri]') for (let i = 0; i < embeds.length; i++) { const id = String(Math.random()).slice(2) diff --git a/bskyembed/src/color-mode.ts b/bskyembed/src/color-mode.ts new file mode 100644 index 000000000..2b392c617 --- /dev/null +++ b/bskyembed/src/color-mode.ts @@ -0,0 +1,17 @@ +export function applyTheme(theme: 'light' | 'dark') { + document.documentElement.classList.remove('light', 'dark') + document.documentElement.classList.add(theme) +} + +export function initColorMode() { + applyTheme( + window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light', + ) + window + .matchMedia('(prefers-color-scheme: dark)') + .addEventListener('change', mql => { + applyTheme(mql.matches ? 'dark' : 'light') + }) +} diff --git a/bskyembed/src/components/container.tsx b/bskyembed/src/components/container.tsx index 5b1b2b7fb..8e142a25b 100644 --- a/bskyembed/src/components/container.tsx +++ b/bskyembed/src/components/container.tsx @@ -37,7 +37,7 @@ export function Container({ return ( <div ref={ref} - className="w-full bg-white hover:bg-neutral-50 relative transition-colors max-w-[600px] min-w-[300px] flex border rounded-xl" + 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" onClick={() => { if (ref.current && href) { // forwardRef requires preact/compat - let's keep it simple diff --git a/bskyembed/src/components/embed.tsx b/bskyembed/src/components/embed.tsx index 74eacf16d..20ffcb2b2 100644 --- a/bskyembed/src/components/embed.tsx +++ b/bskyembed/src/components/embed.tsx @@ -78,9 +78,9 @@ export function Embed({ return ( <Link href={`/profile/${record.author.did}/post/${getRkey(record)}`} - className="transition-colors hover:bg-neutral-100 border rounded-lg p-2 gap-1.5 w-full flex flex-col"> + 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"> <div className="flex gap-1.5 items-center"> - <div className="w-4 h-4 overflow-hidden rounded-full bg-neutral-300 shrink-0"> + <div className="w-4 h-4 overflow-hidden rounded-full bg-neutral-300 dark:bg-slate-700 shrink-0"> <img src={record.author.avatar} style={isAuthorLabeled ? {filter: 'blur(1.5px)'} : undefined} @@ -88,7 +88,7 @@ export function Embed({ </div> <p className="line-clamp-1 text-sm"> <span className="font-bold">{record.author.displayName}</span> - <span className="text-textLight ml-1"> + <span className="text-textLight dark:text-textDimmed ml-1"> @{record.author.handle} </span> </p> @@ -209,7 +209,7 @@ function Info({children}: {children: ComponentChildren}) { return ( <div className="w-full rounded-lg border py-2 px-2.5 flex-row flex gap-2 bg-neutral-50"> <img src={infoIcon} className="w-4 h-4 shrink-0 mt-0.5" /> - <p className="text-sm text-textLight">{children}</p> + <p className="text-sm text-textLight dark:text-textDimmed">{children}</p> </div> ) } @@ -308,7 +308,7 @@ function ExternalEmbed({ return ( <Link href={content.external.uri} - className="w-full rounded-lg overflow-hidden border flex flex-col items-stretch" + className="w-full rounded-lg overflow-hidden border dark:border-slate-600 flex flex-col items-stretch" disableTracking> {content.external.thumb && ( <img @@ -317,11 +317,11 @@ function ExternalEmbed({ /> )} <div className="py-3 px-4"> - <p className="text-sm text-textLight line-clamp-1"> + <p className="text-sm text-textLight dark:text-textDimmed line-clamp-1"> {toNiceDomain(content.external.uri)} </p> <p className="font-semibold line-clamp-3">{content.external.title}</p> - <p className="text-sm text-textLight line-clamp-2 mt-0.5"> + <p className="text-sm text-textLight dark:text-textDimmed line-clamp-2 mt-0.5"> {content.external.description} </p> </div> @@ -345,23 +345,29 @@ function GenericWithImageEmbed({ return ( <Link href={href} - className="w-full rounded-lg border py-2 px-3 flex flex-col gap-2"> + className="w-full rounded-lg border dark:border-slate-600 py-2 px-3 flex flex-col gap-2"> <div className="flex gap-2.5 items-center"> {image ? ( <img src={image} alt={title} - className="w-8 h-8 rounded-md bg-neutral-300 shrink-0" + className="w-8 h-8 rounded-md bg-neutral-300 dark:bg-slate-700 shrink-0" /> ) : ( <div className="w-8 h-8 rounded-md bg-brand shrink-0" /> )} <div className="flex-1"> <p className="font-bold text-sm">{title}</p> - <p className="text-textLight text-sm">{subtitle}</p> + <p className="text-textLight dark:text-textDimmed text-sm"> + {subtitle} + </p> </div> </div> - {description && <p className="text-textLight text-sm">{description}</p>} + {description && ( + <p className="text-textLight dark:text-textDimmed text-sm"> + {description} + </p> + )} </Link> ) } @@ -406,7 +412,7 @@ function StarterPackEmbed({ return ( <Link href={starterPackHref} - className="w-full rounded-lg overflow-hidden border flex flex-col items-stretch"> + className="w-full rounded-lg overflow-hidden border dark:border-slate-600 flex flex-col items-stretch"> <img src={imageUri} className="aspect-[1.91/1] object-cover" /> <div className="py-3 px-4"> <div className="flex space-x-2 items-center"> @@ -415,7 +421,7 @@ function StarterPackEmbed({ <p className="font-semibold leading-[21px]"> {content.record.name} </p> - <p className="text-sm text-textLight line-clamp-2 leading-[18px]"> + <p className="text-sm text-textLight dark:text-textDimmed line-clamp-2 leading-[18px]"> Starter pack by{' '} {content.creator.displayName || `@${content.creator.handle}`} </p> @@ -425,7 +431,7 @@ function StarterPackEmbed({ <p className="text-sm mt-1">{content.record.description}</p> )} {!!content.joinedAllTimeCount && content.joinedAllTimeCount > 50 && ( - <p className="text-sm font-semibold text-textLight mt-1"> + <p className="text-sm font-semibold text-textLight dark:text-textDimmed mt-1"> {content.joinedAllTimeCount} users have joined! </p> )} diff --git a/bskyembed/src/components/post.tsx b/bskyembed/src/components/post.tsx index 4db5eeb45..26945eb69 100644 --- a/bskyembed/src/components/post.tsx +++ b/bskyembed/src/components/post.tsx @@ -38,7 +38,7 @@ export function Post({thread}: Props) { <div className="flex-1 flex-col flex gap-2" lang={record?.langs?.[0]}> <div className="flex gap-2.5 items-center cursor-pointer"> <Link href={`/profile/${post.author.did}`} className="rounded-full"> - <div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-300 shrink-0"> + <div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-300 dark:bg-slate-700 shrink-0"> <img src={post.author.avatar} style={isAuthorLabeled ? {filter: 'blur(2.5px)'} : undefined} @@ -53,7 +53,7 @@ export function Post({thread}: Props) { </Link> <Link href={`/profile/${post.author.did}`} - className="text-[15px] text-textLight hover:underline line-clamp-1"> + className="text-[15px] text-textLight dark:text-textDimmed hover:underline line-clamp-1"> <p>@{post.author.handle}</p> </Link> </div> @@ -69,15 +69,15 @@ export function Post({thread}: Props) { <Link href={href}> <time datetime={new Date(post.indexedAt).toISOString()} - className="text-textLight mt-1 text-sm hover:underline"> + className="text-textLight dark:text-textDimmed mt-1 text-sm hover:underline"> {niceDate(post.indexedAt)} </time> </Link> - <div className="border-t w-full pt-2.5 flex items-center gap-5 text-sm cursor-pointer"> + <div className="border-t dark:border-slate-600 w-full pt-2.5 flex items-center gap-5 text-sm cursor-pointer"> {!!post.likeCount && ( <div className="flex items-center gap-2 cursor-pointer"> <img src={likeIcon} className="w-5 h-5" /> - <p className="font-bold text-neutral-500 mb-px"> + <p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px"> {prettyNumber(post.likeCount)} </p> </div> @@ -85,17 +85,19 @@ export function Post({thread}: Props) { {!!post.repostCount && ( <div className="flex items-center gap-2 cursor-pointer"> <img src={repostIcon} className="w-5 h-5" /> - <p className="font-bold text-neutral-500 mb-px"> + <p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px"> {prettyNumber(post.repostCount)} </p> </div> )} <div className="flex items-center gap-2 cursor-pointer"> <img src={replyIcon} className="w-5 h-5" /> - <p className="font-bold text-neutral-500 mb-px">Reply</p> + <p className="font-bold text-neutral-500 dark:text-neutral-300 mb-px"> + Reply + </p> </div> <div className="flex-1" /> - <p className="cursor-pointer text-brand font-bold hover:underline hidden min-[450px]:inline"> + <p className="cursor-pointer text-brand dark:text-brandLighten font-bold hover:underline hidden min-[450px]:inline"> {post.replyCount ? `Read ${prettyNumber(post.replyCount)} ${ post.replyCount > 1 ? 'replies' : 'reply' diff --git a/bskyembed/src/index.css b/bskyembed/src/index.css index 22b2b8be5..289e34cf0 100644 --- a/bskyembed/src/index.css +++ b/bskyembed/src/index.css @@ -5,3 +5,7 @@ .break-word { word-break: break-word; } + +:root { + color-scheme: light dark; +} diff --git a/bskyembed/src/screens/landing.tsx b/bskyembed/src/screens/landing.tsx index a9e08cd3f..a3448e90a 100644 --- a/bskyembed/src/screens/landing.tsx +++ b/bskyembed/src/screens/landing.tsx @@ -6,6 +6,7 @@ import {useEffect, useMemo, useRef, useState} from 'preact/hooks' import arrowBottom from '../../assets/arrowBottom_stroke2_corner0_rounded.svg' import logo from '../../assets/logo.svg' +import {initColorMode} from '../color-mode' import {Container} from '../components/container' import {Link} from '../components/link' import {Post} from '../components/post' @@ -21,6 +22,8 @@ export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js` const root = document.getElementById('app') if (!root) throw new Error('No root element') +initColorMode() + const agent = new BskyAgent({ service: 'https://public.api.bsky.app', }) @@ -108,7 +111,7 @@ function LandingPage() { }, [uri]) return ( - <main className="w-full min-h-screen flex flex-col items-center gap-8 py-14 px-4 md:pt-32"> + <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"> <Link href="https://bsky.social/about" className="transition-transform hover:scale-110"> @@ -121,20 +124,22 @@ function LandingPage() { type="text" value={uri} onInput={e => setUri(e.currentTarget.value)} - className="border rounded-lg py-3 w-full max-w-[600px] px-4" + className="border rounded-lg py-3 w-full max-w-[600px] px-4 dark:bg-dimmedBg dark:border-slate-500" placeholder={DEFAULT_POST} /> - <img src={arrowBottom} className="w-6" /> + <img src={arrowBottom} className="w-6 dark:invert" /> {loading ? ( - <Skeleton /> + <div className="w-full max-w-[600px]"> + <Skeleton /> + </div> ) : ( <div className="w-full max-w-[600px] gap-8 flex flex-col"> {!error && thread && uri && <Snippet thread={thread} />} {!error && thread && <Post thread={thread} key={thread.post.uri} />} {error && ( - <div className="w-full border border-red-500 bg-red-50 px-4 py-3 rounded-lg"> + <div className="w-full border border-red-500 bg-red-500/10 px-4 py-3 rounded-lg"> <p className="text-red-500 text-center">{error}</p> </div> )} @@ -149,15 +154,15 @@ function Skeleton() { <Container> <div className="flex-1 flex-col flex gap-2 pb-8"> <div className="flex gap-2.5 items-center"> - <div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-100 shrink-0 animate-pulse" /> + <div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-100 dark:bg-slate-700 shrink-0 animate-pulse" /> <div className="flex-1"> - <div className="bg-neutral-100 animate-pulse w-64 h-4 rounded" /> - <div className="bg-neutral-100 animate-pulse w-32 h-3 mt-1 rounded" /> + <div className="bg-neutral-100 dark:bg-slate-700 animate-pulse w-64 h-4 rounded" /> + <div className="bg-neutral-100 dark:bg-slate-700 animate-pulse w-32 h-3 mt-1 rounded" /> </div> </div> - <div className="w-full h-4 mt-2 bg-neutral-100 rounded animate-pulse" /> - <div className="w-5/6 h-4 bg-neutral-100 rounded animate-pulse" /> - <div className="w-3/4 h-4 bg-neutral-100 rounded animate-pulse" /> + <div className="w-full h-4 mt-2 bg-neutral-100 dark:bg-slate-700 rounded animate-pulse" /> + <div className="w-5/6 h-4 bg-neutral-100 dark:bg-slate-700 rounded animate-pulse" /> + <div className="w-3/4 h-4 bg-neutral-100 dark:bg-slate-700 rounded animate-pulse" /> </div> </Container> ) @@ -220,7 +225,7 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) { ref={ref} type="text" value={snippet} - className="border rounded-lg py-3 w-full px-4" + className="border rounded-lg py-3 w-full px-4 dark:bg-dimmedBg dark:border-slate-500" readOnly autoFocus onFocus={() => { @@ -228,7 +233,7 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) { }} /> <button - className="rounded-lg bg-brand text-white color-white py-3 px-4 whitespace-nowrap min-w-28" + className="rounded-lg bg-brand text-white py-3 px-4 whitespace-nowrap min-w-28" onClick={() => { ref.current?.focus() ref.current?.select() diff --git a/bskyembed/src/screens/post.tsx b/bskyembed/src/screens/post.tsx index 6ccf10a79..1764442b7 100644 --- a/bskyembed/src/screens/post.tsx +++ b/bskyembed/src/screens/post.tsx @@ -4,6 +4,7 @@ import {AppBskyFeedDefs, AtpAgent} from '@atproto/api' import {h, render} from 'preact' import logo from '../../assets/logo.svg' +import {initColorMode} from '../color-mode' import {Container} from '../components/container' import {Link} from '../components/link' import {Post} from '../components/post' @@ -21,6 +22,8 @@ if (!uri) { throw new Error('No uri in path') } +initColorMode() + agent .getPostThread({ uri, @@ -55,13 +58,13 @@ function PwiOptOut({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) { <img src={logo} className="h-6" /> </Link> <div className="w-full py-12 gap-4 flex flex-col items-center"> - <p className="max-w-80 text-center w-full text-textLight"> + <p className="max-w-80 text-center w-full text-textLight dark:text-textDimmed"> The author of this post has requested their posts not be displayed on external sites. </p> <Link href={href} - className="max-w-80 rounded-lg bg-brand text-white color-white text-center py-1 px-4 w-full mx-auto"> + className="max-w-80 rounded-lg bg-brand text-white text-center py-1 px-4 w-full mx-auto"> View on Bluesky </Link> </div> @@ -77,7 +80,7 @@ function ErrorMessage() { className="transition-transform hover:scale-110 absolute top-4 right-4"> <img src={logo} className="h-6" /> </Link> - <p className="my-16 text-center w-full text-textLight"> + <p className="my-16 text-center w-full text-textLight dark:text-textDimmed"> Post not found, it may have been deleted. </p> </Container> diff --git a/bskyembed/tailwind.config.cjs b/bskyembed/tailwind.config.cjs index 092e8c2cb..0e9f5b8ea 100644 --- a/bskyembed/tailwind.config.cjs +++ b/bskyembed/tailwind.config.cjs @@ -1,11 +1,19 @@ /** @type {import('tailwindcss').Config} */ module.exports = { content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + darkMode: ['variant', [ + '&:is(.dark *):not(:is(.dark .light *))', + ]], theme: { extend: { colors: { brand: 'rgb(10,122,255)', + brandLighten: 'rgb(32,139,254)', textLight: 'rgb(66,87,108)', + textDimmed: 'rgb(174,187,201)', + dimmedBgLighten: 'rgb(30,41,54)', + dimmedBg: 'rgb(22,30,39)', + dimmedBgDarken: 'rgb(18,25,32)', }, }, }, diff --git a/bskyembed/tsconfig.json b/bskyembed/tsconfig.json index b3b6055cc..f11db03e6 100644 --- a/bskyembed/tsconfig.json +++ b/bskyembed/tsconfig.json @@ -1,4 +1,3 @@ - { "compilerOptions": { "target": "ES5", @@ -20,5 +19,5 @@ "jsxFragmentFactory": "Fragment", "downlevelIteration": true }, - "include": ["src", "vite.config.ts"] + "include": ["src", "snippet", "vite.config.ts"] } |