Bluesky app fork with some witchin' additions 馃挮
witchsky.app
bluesky
fork
client
1import React from 'react'
2import {createTheme, type Theme, type ThemeName} from '@bsky.app/alf'
3import chroma from 'chroma-js'
4
5import {useThemePrefs} from '#/state/shell/color-mode'
6import {
7 computeFontScaleMultiplier,
8 getFontFamily,
9 getFontScale,
10 setFontFamily as persistFontFamily,
11 setFontScale as persistFontScale,
12} from '#/alf/fonts'
13import {
14 blackskyscheme,
15 blueskyscheme,
16 catppuccinscheme,
17 deerscheme,
18 evergardenscheme,
19 kittyscheme,
20 type Palette,
21 reddwarfscheme,
22 themes,
23 witchskyscheme,
24 zeppelinscheme,
25} from '#/alf/themes'
26import {type Device} from '#/storage'
27
28export {
29 type TextStyleProp,
30 type Theme,
31 utils,
32 type ViewStyleProp,
33} from '@bsky.app/alf'
34export {atoms} from '#/alf/atoms'
35export * from '#/alf/breakpoints'
36export * from '#/alf/fonts'
37export * as tokens from '#/alf/tokens'
38export * from '#/alf/util/flatten'
39export * from '#/alf/util/platform'
40export * from '#/alf/util/themeSelector'
41export * from '#/alf/util/useGutters'
42
43export type Alf = {
44 themeName: ThemeName
45 theme: Theme
46 themes: typeof themes
47 fonts: {
48 scale: Exclude<Device['fontScale'], undefined>
49 scaleMultiplier: number
50 family: Device['fontFamily']
51 setFontScale: (fontScale: Exclude<Device['fontScale'], undefined>) => void
52 setFontFamily: (fontFamily: Device['fontFamily']) => void
53 }
54 /**
55 * Feature flags or other gated options
56 */
57 flags: {}
58}
59
60/*
61 * Context
62 */
63export const Context = React.createContext<Alf>({
64 themeName: 'light',
65 theme: themes.light,
66 themes,
67 fonts: {
68 scale: getFontScale(),
69 scaleMultiplier: computeFontScaleMultiplier(getFontScale()),
70 family: getFontFamily(),
71 setFontScale: () => {},
72 setFontFamily: () => {},
73 },
74 flags: {},
75})
76Context.displayName = 'AlfContext'
77
78export type SchemeType = typeof themes
79
80export function changeHue(colorStr: string, hueShift: number) {
81 if (!hueShift || hueShift === 0) return colorStr
82
83 const color = chroma(colorStr).oklch()
84
85 const newHue = (color[2] + hueShift + 360) % 360
86
87 return chroma.oklch(color[0], color[1], newHue).hex()
88}
89
90export function shiftPalette(palette: Palette, hueShift: number): Palette {
91 const newPalette = {...palette}
92 const keys = Object.keys(newPalette) as Array<keyof Palette>
93
94 keys.forEach(key => {
95 if (
96 key.startsWith('positive_') ||
97 key.startsWith('negative_') ||
98 key === 'like' ||
99 key === 'pink' ||
100 key === 'yellow'
101 ) {
102 return
103 }
104 newPalette[key] = changeHue(newPalette[key], hueShift)
105 })
106
107 return newPalette
108}
109
110export function hueShifter(scheme: SchemeType, hueShift: number): SchemeType {
111 if (!hueShift || hueShift === 0) {
112 return scheme
113 }
114
115 const lightPalette = shiftPalette(scheme.lightPalette, hueShift)
116 const darkPalette = shiftPalette(scheme.darkPalette, hueShift)
117 const dimPalette = shiftPalette(scheme.dimPalette, hueShift)
118
119 const light = createTheme({
120 scheme: 'light',
121 name: 'light',
122 palette: lightPalette,
123 })
124
125 const dark = createTheme({
126 scheme: 'dark',
127 name: 'dark',
128 palette: darkPalette,
129 options: {
130 shadowOpacity: 0.4,
131 },
132 })
133
134 const dim = createTheme({
135 scheme: 'dark',
136 name: 'dim',
137 palette: dimPalette,
138 options: {
139 shadowOpacity: 0.4,
140 },
141 })
142
143 return {
144 lightPalette,
145 darkPalette,
146 dimPalette,
147 light,
148 dark,
149 dim,
150 }
151}
152
153export function selectScheme(colorScheme: string | undefined): SchemeType {
154 switch (colorScheme) {
155 case 'witchsky':
156 return witchskyscheme
157 case 'bluesky':
158 return blueskyscheme
159 case 'blacksky':
160 return blackskyscheme
161 case 'deer':
162 return deerscheme
163 case 'zeppelin':
164 return zeppelinscheme
165 case 'kitty':
166 return kittyscheme
167 case 'reddwarf':
168 return reddwarfscheme
169 case 'catppuccin':
170 return catppuccinscheme
171 case 'evergarden':
172 return evergardenscheme
173 default:
174 return themes
175 }
176}
177
178export function ThemeProvider({
179 children,
180 theme: themeName,
181}: React.PropsWithChildren<{theme: ThemeName}>) {
182 const {colorScheme, hue} = useThemePrefs()
183 const currentScheme = selectScheme(colorScheme)
184 const [fontScale, setFontScale] = React.useState<Alf['fonts']['scale']>(() =>
185 getFontScale(),
186 )
187 const [fontScaleMultiplier, setFontScaleMultiplier] = React.useState(() =>
188 computeFontScaleMultiplier(fontScale),
189 )
190 const setFontScaleAndPersist = React.useCallback<
191 Alf['fonts']['setFontScale']
192 >(
193 fs => {
194 setFontScale(fs)
195 persistFontScale(fs)
196 setFontScaleMultiplier(computeFontScaleMultiplier(fs))
197 },
198 [setFontScale],
199 )
200 const [fontFamily, setFontFamily] = React.useState<Alf['fonts']['family']>(
201 () => getFontFamily(),
202 )
203 const setFontFamilyAndPersist = React.useCallback<
204 Alf['fonts']['setFontFamily']
205 >(
206 ff => {
207 setFontFamily(ff)
208 persistFontFamily(ff)
209 },
210 [setFontFamily],
211 )
212
213 const value = React.useMemo<Alf>(() => {
214 const shiftedThemes = hueShifter(currentScheme, hue)
215
216 return {
217 themes: shiftedThemes,
218 themeName: themeName,
219 theme: shiftedThemes[themeName],
220 fonts: {
221 scale: fontScale,
222 scaleMultiplier: fontScaleMultiplier,
223 family: fontFamily,
224 setFontScale: setFontScaleAndPersist,
225 setFontFamily: setFontFamilyAndPersist,
226 },
227 flags: {},
228 }
229 }, [
230 currentScheme,
231 hue,
232 themeName,
233 fontScale,
234 fontScaleMultiplier,
235 fontFamily,
236 setFontScaleAndPersist,
237 setFontFamilyAndPersist,
238 ])
239
240 return <Context.Provider value={value}>{children}</Context.Provider>
241}
242
243export function useAlf() {
244 return React.useContext(Context)
245}
246
247export function useTheme(theme?: ThemeName) {
248 const alf = useAlf()
249 return React.useMemo(() => {
250 return theme ? alf.themes[theme] : alf.theme
251 }, [theme, alf])
252}