An ATproto social media client -- with an independent Appview.

enhance(embed): add ability to pin color mode (#7186)

* enhance(embed): add ability to pin color mode

* fix: Move color mode dropdown to the root section

* auto -> system

* style tweaks

* default to light theme

* try and fix eslint

* fix dropdown styles on other browsers

* rm unnecessary eslintrc change

* more explicit color mode select

* make light explicit

---------

Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>

authored by samuel.fm

kakkokari-gtyih and committed by
GitHub
2d854091 01a51c32

+96 -23
+1 -1
.eslintrc.js
··· 92 92 '*.lock', 93 93 '.husky', 94 94 'patches', 95 - 'bskyweb', 96 95 '*.html', 97 96 'bskyweb', 97 + 'bskyembed', 98 98 'src/locale/locales/_build/', 99 99 'src/locale/locales/**/*.js', 100 100 ],
+1
bskyembed/snippet/embed.ts
··· 68 68 if (ref_url.startsWith('http')) { 69 69 searchParams.set('ref_url', encodeURIComponent(ref_url)) 70 70 } 71 + searchParams.set('colorMode', embed.dataset.blueskyColorMode || 'system') 71 72 72 73 const iframe = document.createElement('iframe') 73 74 iframe.setAttribute('data-bluesky-id', id)
+7 -1
bskyembed/src/color-mode.ts
··· 1 + export type ColorModeValues = 'system' | 'light' | 'dark' 2 + 3 + export function assertColorModeValues(value: string): value is ColorModeValues { 4 + return ['system', 'light', 'dark'].includes(value) 5 + } 6 + 1 7 export function applyTheme(theme: 'light' | 'dark') { 2 8 document.documentElement.classList.remove('light', 'dark') 3 9 document.documentElement.classList.add(theme) 4 10 } 5 11 6 - export function initColorMode() { 12 + export function initSystemColorMode() { 7 13 applyTheme( 8 14 window.matchMedia('(prefers-color-scheme: dark)').matches 9 15 ? 'dark'
+11
bskyembed/src/index.css
··· 9 9 :root { 10 10 color-scheme: light dark; 11 11 } 12 + 13 + select { 14 + background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' height='14px' width='14px' fill='none' viewBox='0 0 24 24'><path fill='black' fill-rule='evenodd' d='M3.293 8.293a1 1 0 0 1 1.414 0L12 15.586l7.293-7.293a1 1 0 1 1 1.414 1.414l-8 8a1 1 0 0 1-1.414 0l-8-8a1 1 0 0 1 0-1.414Z' clip-rule='evenodd'/></svg>"); 15 + background-repeat: no-repeat; 16 + background-position: calc(100% - 0.75rem) center; 17 + padding-right: 2rem; 18 + 19 + @media (prefers-color-scheme: dark) { 20 + background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' height='14px' width='14px' fill='none' viewBox='0 0 24 24'><path fill='white' fill-rule='evenodd' d='M3.293 8.293a1 1 0 0 1 1.414 0L12 15.586l7.293-7.293a1 1 0 1 1 1.414 1.414l-8 8a1 1 0 0 1-1.414 0l-8-8a1 1 0 0 1 0-1.414Z' clip-rule='evenodd'/></svg>"); 21 + } 22 + }
+58 -19
bskyembed/src/screens/landing.tsx
··· 1 1 import '../index.css' 2 2 3 - import {AppBskyFeedDefs, AppBskyFeedPost, AtUri, BskyAgent} from '@atproto/api' 3 + import {AppBskyFeedDefs, AppBskyFeedPost, AtpAgent, AtUri} from '@atproto/api' 4 4 import {h, render} from 'preact' 5 5 import {useEffect, useMemo, useRef, useState} from 'preact/hooks' 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 + import { 10 + assertColorModeValues, 11 + ColorModeValues, 12 + initSystemColorMode, 13 + } from '../color-mode' 10 14 import {Container} from '../components/container' 11 15 import {Link} from '../components/link' 12 16 import {Post} from '../components/post' ··· 22 26 const root = document.getElementById('app') 23 27 if (!root) throw new Error('No root element') 24 28 25 - initColorMode() 29 + initSystemColorMode() 26 30 27 - const agent = new BskyAgent({ 31 + const agent = new AtpAgent({ 28 32 service: 'https://public.api.bsky.app', 29 33 }) 30 34 ··· 32 36 33 37 function LandingPage() { 34 38 const [uri, setUri] = useState('') 39 + const [colorMode, setColorMode] = useState<ColorModeValues>('system') 35 40 const [error, setError] = useState<string | null>(null) 36 41 const [loading, setLoading] = useState(false) 37 42 const [thread, setThread] = useState<AppBskyFeedDefs.ThreadViewPost | null>( ··· 120 125 121 126 <h1 className="text-4xl font-bold text-center">Embed a Bluesky Post</h1> 122 127 123 - <input 124 - type="text" 125 - value={uri} 126 - onInput={e => setUri(e.currentTarget.value)} 127 - className="border rounded-lg py-3 w-full max-w-[600px] px-4 dark:bg-dimmedBg dark:border-slate-500" 128 - placeholder={DEFAULT_POST} 129 - /> 128 + <div className="flex flex-col w-full max-w-[600px] gap-6"> 129 + <input 130 + type="text" 131 + value={uri} 132 + onInput={e => setUri(e.currentTarget.value)} 133 + className="border rounded-lg py-3 px-4 dark:bg-dimmedBg dark:border-slate-500" 134 + placeholder={DEFAULT_POST} 135 + /> 136 + 137 + <div className="flex flex-col gap-1.5"> 138 + <label className="text-sm font-medium" for="colorModeSelect"> 139 + Theme 140 + </label> 141 + <select 142 + value={colorMode} 143 + onChange={e => { 144 + const value = e.currentTarget.value 145 + if (assertColorModeValues(value)) { 146 + setColorMode(value) 147 + } 148 + }} 149 + id="colorModeSelect" 150 + className="appearance-none bg-white border w-full rounded-lg text-sm px-3 py-2 dark:bg-dimmedBg dark:border-slate-500"> 151 + <option value="system">System</option> 152 + <option value="light">Light</option> 153 + <option value="dark">Dark</option> 154 + </select> 155 + </div> 156 + </div> 130 157 131 158 <img src={arrowBottom} className="w-6 dark:invert" /> 132 159 133 160 {loading ? ( 134 - <div className="w-full max-w-[600px]"> 161 + <div className={`${colorMode} w-full max-w-[600px]`}> 135 162 <Skeleton /> 136 163 </div> 137 164 ) : ( 138 165 <div className="w-full max-w-[600px] gap-8 flex flex-col"> 139 - {!error && thread && uri && <Snippet thread={thread} />} 140 - {!error && thread && <Post thread={thread} key={thread.post.uri} />} 166 + {!error && thread && uri && ( 167 + <Snippet thread={thread} colorMode={colorMode} /> 168 + )} 169 + <div className={colorMode}> 170 + {!error && thread && <Post thread={thread} key={thread.post.uri} />} 171 + </div> 141 172 {error && ( 142 173 <div className="w-full border border-red-500 bg-red-500/10 px-4 py-3 rounded-lg"> 143 174 <p className="text-red-500 text-center">{error}</p> ··· 168 199 ) 169 200 } 170 201 171 - function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) { 202 + function Snippet({ 203 + thread, 204 + colorMode, 205 + }: { 206 + thread: AppBskyFeedDefs.ThreadViewPost 207 + colorMode: ColorModeValues 208 + }) { 172 209 const ref = useRef<HTMLInputElement>(null) 173 210 const [copied, setCopied] = useState(false) 174 211 ··· 204 241 // x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x 205 242 return `<blockquote class="bluesky-embed" data-bluesky-uri="${escapeHtml( 206 243 thread.post.uri, 207 - )}" data-bluesky-cid="${escapeHtml(thread.post.cid)}"><p lang="${escapeHtml( 208 - lang, 209 - )}">${escapeHtml(record.text)}${ 244 + )}" data-bluesky-cid="${escapeHtml( 245 + thread.post.cid, 246 + )}" data-bluesky-embed-color-mode="${escapeHtml( 247 + colorMode, 248 + )}"><p lang="${escapeHtml(lang)}">${escapeHtml(record.text)}${ 210 249 record.embed 211 250 ? `<br><br><a href="${escapeHtml(href)}">[image or embed]</a>` 212 251 : '' ··· 217 256 )}</a>) <a href="${escapeHtml(href)}">${escapeHtml( 218 257 niceDate(thread.post.indexedAt), 219 258 )}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>` 220 - }, [thread]) 259 + }, [thread, colorMode]) 221 260 222 261 return ( 223 262 <div className="flex gap-2 w-full">
+18 -2
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 + import {applyTheme, initSystemColorMode} from '../color-mode' 8 8 import {Container} from '../components/container' 9 9 import {Link} from '../components/link' 10 10 import {Post} from '../components/post' ··· 22 22 throw new Error('No uri in path') 23 23 } 24 24 25 - initColorMode() 25 + const query = new URLSearchParams(window.location.search) 26 + 27 + // theme - default to light mode 28 + const colorMode = query.get('colorMode') 29 + 30 + switch (colorMode) { 31 + case 'dark': 32 + applyTheme('dark') 33 + break 34 + case 'system': 35 + initSystemColorMode() 36 + break 37 + case 'light': 38 + default: 39 + applyTheme('light') 40 + break 41 + } 26 42 27 43 agent 28 44 .getPostThread({