about summary refs log tree commit diff
path: root/bskyembed
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2025-03-05 17:24:59 +0000
committerGitHub <noreply@github.com>2025-03-05 09:24:59 -0800
commit2d854091b9684ab253a2c117509bf95609a975a1 (patch)
tree965f049c7d8e13e61bd1ce05737113340fb3d991 /bskyembed
parent01a51c327505bc84f5755be82f15a855234a2750 (diff)
downloadvoidsky-2d854091b9684ab253a2c117509bf95609a975a1.tar.zst
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>
Diffstat (limited to 'bskyembed')
-rw-r--r--bskyembed/snippet/embed.ts1
-rw-r--r--bskyembed/src/color-mode.ts8
-rw-r--r--bskyembed/src/index.css11
-rw-r--r--bskyembed/src/screens/landing.tsx77
-rw-r--r--bskyembed/src/screens/post.tsx20
5 files changed, 95 insertions, 22 deletions
diff --git a/bskyembed/snippet/embed.ts b/bskyembed/snippet/embed.ts
index 3c1b14b95..7de7af1fe 100644
--- a/bskyembed/snippet/embed.ts
+++ b/bskyembed/snippet/embed.ts
@@ -68,6 +68,7 @@ function scan(node = document) {
     if (ref_url.startsWith('http')) {
       searchParams.set('ref_url', encodeURIComponent(ref_url))
     }
+    searchParams.set('colorMode', embed.dataset.blueskyColorMode || 'system')
 
     const iframe = document.createElement('iframe')
     iframe.setAttribute('data-bluesky-id', id)
diff --git a/bskyembed/src/color-mode.ts b/bskyembed/src/color-mode.ts
index 2b392c617..b34624e31 100644
--- a/bskyembed/src/color-mode.ts
+++ b/bskyembed/src/color-mode.ts
@@ -1,9 +1,15 @@
+export type ColorModeValues = 'system' | 'light' | 'dark'
+
+export function assertColorModeValues(value: string): value is ColorModeValues {
+  return ['system', 'light', 'dark'].includes(value)
+}
+
 export function applyTheme(theme: 'light' | 'dark') {
   document.documentElement.classList.remove('light', 'dark')
   document.documentElement.classList.add(theme)
 }
 
-export function initColorMode() {
+export function initSystemColorMode() {
   applyTheme(
     window.matchMedia('(prefers-color-scheme: dark)').matches
       ? 'dark'
diff --git a/bskyembed/src/index.css b/bskyembed/src/index.css
index 289e34cf0..efd9f4a4e 100644
--- a/bskyembed/src/index.css
+++ b/bskyembed/src/index.css
@@ -9,3 +9,14 @@
 :root {
   color-scheme: light dark;
 }
+
+select {
+  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>");
+  background-repeat: no-repeat;
+  background-position: calc(100% - 0.75rem) center;
+  padding-right: 2rem;
+
+  @media (prefers-color-scheme: dark) {
+    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>");
+  }
+}
diff --git a/bskyembed/src/screens/landing.tsx b/bskyembed/src/screens/landing.tsx
index a3448e90a..880b71337 100644
--- a/bskyembed/src/screens/landing.tsx
+++ b/bskyembed/src/screens/landing.tsx
@@ -1,12 +1,16 @@
 import '../index.css'
 
-import {AppBskyFeedDefs, AppBskyFeedPost, AtUri, BskyAgent} from '@atproto/api'
+import {AppBskyFeedDefs, AppBskyFeedPost, AtpAgent, AtUri} from '@atproto/api'
 import {h, render} from 'preact'
 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 {
+  assertColorModeValues,
+  ColorModeValues,
+  initSystemColorMode,
+} from '../color-mode'
 import {Container} from '../components/container'
 import {Link} from '../components/link'
 import {Post} from '../components/post'
@@ -22,9 +26,9 @@ export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js`
 const root = document.getElementById('app')
 if (!root) throw new Error('No root element')
 
-initColorMode()
+initSystemColorMode()
 
-const agent = new BskyAgent({
+const agent = new AtpAgent({
   service: 'https://public.api.bsky.app',
 })
 
@@ -32,6 +36,7 @@ render(<LandingPage />, root)
 
 function LandingPage() {
   const [uri, setUri] = useState('')
+  const [colorMode, setColorMode] = useState<ColorModeValues>('system')
   const [error, setError] = useState<string | null>(null)
   const [loading, setLoading] = useState(false)
   const [thread, setThread] = useState<AppBskyFeedDefs.ThreadViewPost | null>(
@@ -120,24 +125,50 @@ function LandingPage() {
 
       <h1 className="text-4xl font-bold text-center">Embed a Bluesky Post</h1>
 
-      <input
-        type="text"
-        value={uri}
-        onInput={e => setUri(e.currentTarget.value)}
-        className="border rounded-lg py-3 w-full max-w-[600px] px-4 dark:bg-dimmedBg dark:border-slate-500"
-        placeholder={DEFAULT_POST}
-      />
+      <div className="flex flex-col w-full max-w-[600px] gap-6">
+        <input
+          type="text"
+          value={uri}
+          onInput={e => setUri(e.currentTarget.value)}
+          className="border rounded-lg py-3 px-4 dark:bg-dimmedBg dark:border-slate-500"
+          placeholder={DEFAULT_POST}
+        />
+
+        <div className="flex flex-col gap-1.5">
+          <label className="text-sm font-medium" for="colorModeSelect">
+            Theme
+          </label>
+          <select
+            value={colorMode}
+            onChange={e => {
+              const value = e.currentTarget.value
+              if (assertColorModeValues(value)) {
+                setColorMode(value)
+              }
+            }}
+            id="colorModeSelect"
+            className="appearance-none bg-white border w-full rounded-lg text-sm px-3 py-2 dark:bg-dimmedBg dark:border-slate-500">
+            <option value="system">System</option>
+            <option value="light">Light</option>
+            <option value="dark">Dark</option>
+          </select>
+        </div>
+      </div>
 
       <img src={arrowBottom} className="w-6 dark:invert" />
 
       {loading ? (
-        <div className="w-full max-w-[600px]">
+        <div className={`${colorMode} 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 && thread && uri && (
+            <Snippet thread={thread} colorMode={colorMode} />
+          )}
+          <div className={colorMode}>
+            {!error && thread && <Post thread={thread} key={thread.post.uri} />}
+          </div>
           {error && (
             <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>
@@ -168,7 +199,13 @@ function Skeleton() {
   )
 }
 
-function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
+function Snippet({
+  thread,
+  colorMode,
+}: {
+  thread: AppBskyFeedDefs.ThreadViewPost
+  colorMode: ColorModeValues
+}) {
   const ref = useRef<HTMLInputElement>(null)
   const [copied, setCopied] = useState(false)
 
@@ -204,9 +241,11 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
     // 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
     return `<blockquote class="bluesky-embed" data-bluesky-uri="${escapeHtml(
       thread.post.uri,
-    )}" data-bluesky-cid="${escapeHtml(thread.post.cid)}"><p lang="${escapeHtml(
-      lang,
-    )}">${escapeHtml(record.text)}${
+    )}" data-bluesky-cid="${escapeHtml(
+      thread.post.cid,
+    )}" data-bluesky-embed-color-mode="${escapeHtml(
+      colorMode,
+    )}"><p lang="${escapeHtml(lang)}">${escapeHtml(record.text)}${
       record.embed
         ? `<br><br><a href="${escapeHtml(href)}">[image or embed]</a>`
         : ''
@@ -217,7 +256,7 @@ function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
     )}</a>) <a href="${escapeHtml(href)}">${escapeHtml(
       niceDate(thread.post.indexedAt),
     )}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>`
-  }, [thread])
+  }, [thread, colorMode])
 
   return (
     <div className="flex gap-2 w-full">
diff --git a/bskyembed/src/screens/post.tsx b/bskyembed/src/screens/post.tsx
index 1764442b7..4cd72b69b 100644
--- a/bskyembed/src/screens/post.tsx
+++ b/bskyembed/src/screens/post.tsx
@@ -4,7 +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 {applyTheme, initSystemColorMode} from '../color-mode'
 import {Container} from '../components/container'
 import {Link} from '../components/link'
 import {Post} from '../components/post'
@@ -22,7 +22,23 @@ if (!uri) {
   throw new Error('No uri in path')
 }
 
-initColorMode()
+const query = new URLSearchParams(window.location.search)
+
+// theme - default to light mode
+const colorMode = query.get('colorMode')
+
+switch (colorMode) {
+  case 'dark':
+    applyTheme('dark')
+    break
+  case 'system':
+    initSystemColorMode()
+    break
+  case 'light':
+  default:
+    applyTheme('light')
+    break
+}
 
 agent
   .getPostThread({