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