an independent Bluesky client using Constellation, PDS Queries, and other services reddwarf.app
frontend spa bluesky reddwarf microcosm client app
99
fork

Configure Feed

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

hueshift

rimar1337 83082d78 074047b5

+152 -44
+1
package-lock.json
··· 9 9 "@atproto/api": "^0.16.6", 10 10 "@atproto/oauth-client-browser": "^0.3.33", 11 11 "@radix-ui/react-dropdown-menu": "^2.1.16", 12 + "@radix-ui/react-slider": "^1.3.6", 12 13 "@tailwindcss/vite": "^4.0.6", 13 14 "@tanstack/query-sync-storage-persister": "^5.85.6", 14 15 "@tanstack/react-devtools": "^0.2.2",
+1
package.json
··· 13 13 "@atproto/api": "^0.16.6", 14 14 "@atproto/oauth-client-browser": "^0.3.33", 15 15 "@radix-ui/react-dropdown-menu": "^2.1.16", 16 + "@radix-ui/react-slider": "^1.3.6", 16 17 "@tailwindcss/vite": "^4.0.6", 17 18 "@tanstack/query-sync-storage-persister": "^5.85.6", 18 19 "@tanstack/react-devtools": "^0.2.2",
+6
src/components/Star.tsx
··· 1 + import type { SVGProps } from 'react'; 2 + import React from 'react'; 3 + 4 + export function FluentEmojiHighContrastGlowingStar(props: SVGProps<SVGSVGElement>) { 5 + return (<svg xmlns="http://www.w3.org/2000/svg" width={32} height={32} viewBox="0 0 32 32" {...props}><g fill="currentColor"><path d="m28.979 17.003l-3.108.214c-.834.06-1.178 1.079-.542 1.608l2.388 1.955c.521.428 1.314.204 1.523-.428l.709-2.127c.219-.632-.292-1.273-.97-1.222M21.75 2.691l-.72 2.9c-.2.78.66 1.41 1.34.98l2.54-1.58c.55-.34.58-1.14.05-1.52l-1.78-1.29a.912.912 0 0 0-1.43.51M6.43 4.995l2.53 1.58c.68.43 1.54-.19 1.35-.98l-.72-2.9a.92.92 0 0 0-1.43-.52l-1.78 1.29c-.53.4-.5 1.19.05 1.53M4.185 20.713l2.29-1.92c.62-.52.29-1.53-.51-1.58l-2.98-.21a.92.92 0 0 0-.94 1.2l.68 2.09c.2.62.97.84 1.46.42m13.61 7.292l-1.12-2.77c-.3-.75-1.36-.75-1.66 0l-1.12 2.77c-.24.6.2 1.26.85 1.26h2.2a.92.92 0 0 0 .85-1.26"></path><path d="m17.565 3.324l1.726 3.72c.326.694.967 1.18 1.717 1.29l4.056.624c1.835.278 2.575 2.53 1.293 3.859L23.268 16a2.28 2.28 0 0 0-.612 1.964l.71 4.374c.307 1.885-1.687 3.293-3.354 2.37l-3.405-1.894a2.25 2.25 0 0 0-2.21 0l-3.404 1.895c-1.668.922-3.661-.486-3.355-2.37l.71-4.375A2.28 2.28 0 0 0 7.736 16l-3.088-3.184c-1.293-1.34-.543-3.581 1.293-3.859l4.055-.625a2.3 2.3 0 0 0 1.717-1.29l1.727-3.719c.819-1.765 3.306-1.765 4.124 0"></path></g></svg>); 6 + }
+2
src/main.tsx
··· 14 14 import { routeTree } from "./routeTree.gen"; 15 15 import { isAtTopAtom } from "./utils/atoms.ts"; 16 16 17 + //initAtomToCssVar(hueAtom, "--tw-gray-hue") 18 + 17 19 const queryClient = new QueryClient({ 18 20 defaultOptions: { 19 21 queries: {
+6 -4
src/routes/__root.tsx
··· 20 20 import { DefaultCatchBoundary } from "~/components/DefaultCatchBoundary"; 21 21 import Login from "~/components/Login"; 22 22 import { NotFound } from "~/components/NotFound"; 23 + import { FluentEmojiHighContrastGlowingStar } from "~/components/Star"; 23 24 import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider"; 24 - import { composerAtom } from "~/utils/atoms"; 25 + import { composerAtom, hueAtom, useAtomCssVar } from "~/utils/atoms"; 25 26 import { seo } from "~/utils/seo"; 26 27 27 28 export const Route = createRootRouteWithContext<{ ··· 87 88 } 88 89 89 90 function RootDocument({ children }: { children: React.ReactNode }) { 91 + useAtomCssVar(hueAtom, "--tw-gray-hue"); 90 92 const location = useLocation(); 91 93 const navigate = useNavigate(); 92 94 const { agent } = useAuth(); ··· 128 130 <div className="min-h-screen flex justify-center bg-gray-50 dark:bg-gray-950"> 129 131 <nav className="hidden lg:flex h-screen w-[250px] flex-col gap-0 p-4 dark:border-gray-800 sticky top-0 self-start"> 130 132 <div className="flex items-center gap-3 mb-4"> 131 - <img src="/redstar.png" alt="Red Dwarf Logo" className="w-8 h-8" /> 133 + <FluentEmojiHighContrastGlowingStar className="h-8 w-8" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} /> 132 134 <span className="font-extrabold text-2xl tracking-tight text-gray-900 dark:text-gray-100"> 133 135 Red Dwarf{" "} 134 136 {/* <span className="text-gray-500 dark:text-gray-400 text-sm"> ··· 367 369 368 370 <nav className="hidden sm:flex items-center lg:hidden h-screen flex-col gap-2 p-4 dark:border-gray-800 sticky top-0 self-start"> 369 371 <div className="flex items-center gap-3 mb-4"> 370 - <img src="/redstar.png" alt="Red Dwarf Logo" className="w-8 h-8" /> 372 + <FluentEmojiHighContrastGlowingStar className="h-8 w-8" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} /> 371 373 </div> 372 374 <MaterialNavItem 373 375 small ··· 676 678 ) : ( 677 679 <div className="lg:hidden flex items-center fixed bottom-0 left-0 right-0 justify-between px-4 py-3 border-0 shadow border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 z-10"> 678 680 <div className="flex items-center gap-2"> 679 - <img src="/redstar.png" alt="Red Dwarf Logo" className="w-6 h-6" /> 681 + <FluentEmojiHighContrastGlowingStar className="h-6 w-6" style={{color: "oklch(0.6616 0.2249 calc(25.88 + (var(--safe-hue) - 28))"}} /> 680 682 <span className="font-bold text-lg text-gray-900 dark:text-gray-100"> 681 683 Red Dwarf{" "} 682 684 {/* <span className="text-gray-500 dark:text-gray-400 text-sm">
+73 -4
src/routes/settings.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 2 import { useAtom } from "jotai"; 3 + import { Slider } from "radix-ui"; 3 4 4 5 import { Header } from "~/components/Header"; 5 6 import Login from "~/components/Login"; 6 7 import { 7 8 constellationURLAtom, 8 9 defaultconstellationURL, 10 + defaulthue, 9 11 defaultImgCDN, 10 12 defaultslingshotURL, 11 13 defaultVideoCDN, 14 + hueAtom, 12 15 imgCDNAtom, 13 16 slingshotURLAtom, 14 17 videoCDNAtom, ··· 31 34 } 32 35 }} 33 36 /> 34 - <div className="lg:hidden"><Login /></div> 37 + <div className="lg:hidden"> 38 + <Login /> 39 + </div> 35 40 <div className="h-4" /> 36 41 <TextInputSetting 37 42 atom={constellationURLAtom} ··· 61 66 description={"Customize the Slingshot instance to be used by Red Dwarf"} 62 67 init={defaultVideoCDN} 63 68 /> 64 - <p className="text-gray-500 dark:text-gray-400 py-4 px-6 text-sm">please restart/refresh the app if changes arent applying correctly</p> 69 + 70 + <Hue /> 71 + <p className="text-gray-500 dark:text-gray-400 py-4 px-6 text-sm"> 72 + please restart/refresh the app if changes arent applying correctly 73 + </p> 65 74 </> 66 75 ); 67 76 } 77 + function Hue() { 78 + const [hue, setHue] = useAtom(hueAtom); 79 + return ( 80 + <div className="flex flex-col px-4 mt-4 "> 81 + <span className="z-10">Hue</span> 82 + <div className="flex flex-row items-center gap-4"> 83 + <SliderComponent 84 + atom={hueAtom} 85 + max={360} 86 + /> 87 + <button 88 + onClick={() => setHue(defaulthue ?? 28)} 89 + className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800 90 + text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition" 91 + > 92 + Reset 93 + </button> 94 + </div> 95 + </div> 96 + ); 97 + } 68 98 69 99 export function TextInputSetting({ 70 100 atom, ··· 95 125 96 126 <div className="flex flex-row gap-2 items-center"> 97 127 <div className="m3input-field m3input-label m3input-border size-md flex-1"> 98 - <input type="text" placeholder=" " value={value} onChange={(e) => setValue(e.target.value)}/> 128 + <input 129 + type="text" 130 + placeholder=" " 131 + value={value} 132 + onChange={(e) => setValue(e.target.value)} 133 + /> 99 134 <label>{title}</label> 100 135 </div> 101 136 {/* <input ··· 117 152 </div> 118 153 </div> 119 154 ); 120 - } 155 + } 156 + 157 + 158 + interface SliderProps { 159 + atom: typeof hueAtom; 160 + min?: number; 161 + max?: number; 162 + step?: number; 163 + } 164 + 165 + export const SliderComponent: React.FC<SliderProps> = ({ 166 + atom, 167 + min = 0, 168 + max = 100, 169 + step = 1, 170 + }) => { 171 + 172 + const [value, setValue] = useAtom(atom) 173 + 174 + return ( 175 + <Slider.Root 176 + className="relative flex items-center w-full h-4" 177 + value={[value]} 178 + min={min} 179 + max={max} 180 + step={step} 181 + onValueChange={(v: number[]) => setValue(v[0])} 182 + > 183 + <Slider.Track className="relative flex-grow h-4 bg-gray-300 dark:bg-gray-700 rounded-full"> 184 + <Slider.Range className="absolute h-full bg-gray-500 dark:bg-gray-400 rounded-l-full rounded-r-none" /> 185 + </Slider.Track> 186 + <Slider.Thumb className="shadow-[0_0_0_8px_var(--color-white)] dark:shadow-[0_0_0_8px_var(--color-gray-950)] block w-[3px] h-12 bg-gray-500 dark:bg-gray-400 rounded-md focus:outline-none" /> 187 + </Slider.Root> 188 + ); 189 + };
+15 -11
src/styles/app.css
··· 15 15 --color-gray-950: oklch(0.129 0.050 222.000); 16 16 } */ 17 17 18 + :root { 19 + --safe-hue: var(--tw-gray-hue, 28) 20 + } 21 + 18 22 @theme { 19 - --color-gray-50: oklch(0.984 0.012 28); 20 - --color-gray-100: oklch(0.968 0.017 28); 21 - --color-gray-200: oklch(0.929 0.025 28); 22 - --color-gray-300: oklch(0.869 0.035 28); 23 - --color-gray-400: oklch(0.704 0.05 28); 24 - --color-gray-500: oklch(0.554 0.06 28); 25 - --color-gray-600: oklch(0.446 0.058 28); 26 - --color-gray-700: oklch(0.372 0.058 28); 27 - --color-gray-800: oklch(0.279 0.055 28); 28 - --color-gray-900: oklch(0.208 0.055 28); 29 - --color-gray-950: oklch(0.129 0.055 28); 23 + --color-gray-50: oklch(0.984 0.012 var(--safe-hue)); 24 + --color-gray-100: oklch(0.968 0.017 var(--safe-hue)); 25 + --color-gray-200: oklch(0.929 0.025 var(--safe-hue)); 26 + --color-gray-300: oklch(0.869 0.035 var(--safe-hue)); 27 + --color-gray-400: oklch(0.704 0.05 var(--safe-hue)); 28 + --color-gray-500: oklch(0.554 0.06 var(--safe-hue)); 29 + --color-gray-600: oklch(0.446 0.058 var(--safe-hue)); 30 + --color-gray-700: oklch(0.372 0.058 var(--safe-hue)); 31 + --color-gray-800: oklch(0.279 0.055 var(--safe-hue)); 32 + --color-gray-900: oklch(0.208 0.055 var(--safe-hue)); 33 + --color-gray-950: oklch(0.129 0.055 var(--safe-hue)); 30 34 } 31 35 32 36 @layer base {
+48 -25
src/utils/atoms.ts
··· 1 1 import type Agent from "@atproto/api"; 2 - import { atom, createStore } from "jotai"; 3 - import { atomWithStorage } from 'jotai/utils'; 2 + import { atom, createStore, useAtomValue } from "jotai"; 3 + import { atomWithStorage } from "jotai/utils"; 4 + import { useEffect } from "react"; 4 5 5 6 export const store = createStore(); 6 7 7 8 export const selectedFeedUriAtom = atomWithStorage<string | null>( 8 - 'selectedFeedUri', 9 + "selectedFeedUri", 9 10 null 10 11 ); 11 12 12 13 //export const feedScrollPositionsAtom = atom<Record<string, number>>({}); 13 14 14 15 export const feedScrollPositionsAtom = atomWithStorage<Record<string, number>>( 15 - 'feedscrollpositions', 16 + "feedscrollpositions", 16 17 {} 17 18 ); 18 19 19 20 export const likedPostsAtom = atomWithStorage<Record<string, string>>( 20 - 'likedPosts', 21 + "likedPosts", 21 22 {} 22 23 ); 23 24 24 - export const defaultconstellationURL = 'constellation.microcosm.blue' 25 + export const defaultconstellationURL = "constellation.microcosm.blue"; 25 26 export const constellationURLAtom = atomWithStorage<string>( 26 - 'constellationURL', 27 + "constellationURL", 27 28 defaultconstellationURL 28 - ) 29 - export const defaultslingshotURL = 'slingshot.microcosm.blue' 29 + ); 30 + export const defaultslingshotURL = "slingshot.microcosm.blue"; 30 31 export const slingshotURLAtom = atomWithStorage<string>( 31 - 'slingshotURL', 32 + "slingshotURL", 32 33 defaultslingshotURL 33 - ) 34 - export const defaultImgCDN = 'cdn.bsky.app' 35 - export const imgCDNAtom = atomWithStorage<string>( 36 - 'imgcdnurl', 37 - defaultImgCDN 38 - ) 39 - export const defaultVideoCDN = 'video.bsky.app' 34 + ); 35 + export const defaultImgCDN = "cdn.bsky.app"; 36 + export const imgCDNAtom = atomWithStorage<string>("imgcdnurl", defaultImgCDN); 37 + export const defaultVideoCDN = "video.bsky.app"; 40 38 export const videoCDNAtom = atomWithStorage<string>( 41 - 'videocdnurl', 39 + "videocdnurl", 42 40 defaultVideoCDN 43 - ) 41 + ); 42 + 43 + export const defaulthue = 28; 44 + export const hueAtom = atomWithStorage<number>("hue", defaulthue); 44 45 45 46 export const isAtTopAtom = atom<boolean>(true); 46 47 47 48 type ComposerState = 48 - | { kind: 'closed' } 49 - | { kind: 'root' } 50 - | { kind: 'reply'; parent: string } 51 - | { kind: 'quote'; subject: string }; 52 - export const composerAtom = atom<ComposerState>({ kind: 'closed' }); 49 + | { kind: "closed" } 50 + | { kind: "root" } 51 + | { kind: "reply"; parent: string } 52 + | { kind: "quote"; subject: string }; 53 + export const composerAtom = atom<ComposerState>({ kind: "closed" }); 53 54 54 - export const agentAtom = atom<Agent|null>(null); 55 + export const agentAtom = atom<Agent | null>(null); 55 56 export const authedAtom = atom<boolean>(false); 57 + 58 + export function useAtomCssVar(atom: typeof hueAtom, cssVar: string) { 59 + const value = useAtomValue(atom); 60 + 61 + useEffect(() => { 62 + document.documentElement.style.setProperty(cssVar, value.toString()); 63 + }, [value, cssVar]); 64 + 65 + useEffect(() => { 66 + document.documentElement.style.setProperty(cssVar, value.toString()); 67 + }, []); 68 + } 69 + 70 + hueAtom.onMount = (setAtom) => { 71 + const stored = localStorage.getItem("hue"); 72 + if (stored != null) setAtom(Number(stored)); 73 + }; 74 + // export function initAtomToCssVar(atom: typeof hueAtom, cssVar: string) { 75 + // const initial = store.get(atom); 76 + // console.log("atom get ", initial); 77 + // document.documentElement.style.setProperty(cssVar, initial.toString()); 78 + // }