+1
package-lock.json
+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
+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
+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
+2
src/main.tsx
+6
-4
src/routes/__root.tsx
+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
+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
-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
+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
+
// }