this repo has no description
1import {createContext, useCallback, useContext, useMemo, useState} 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 = 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] = useState<Alf['fonts']['scale']>(() =>
185 getFontScale(),
186 )
187 const [fontScaleMultiplier, setFontScaleMultiplier] = useState(() =>
188 computeFontScaleMultiplier(fontScale),
189 )
190 const setFontScaleAndPersist = useCallback<Alf['fonts']['setFontScale']>(
191 fs => {
192 setFontScale(fs)
193 persistFontScale(fs)
194 setFontScaleMultiplier(computeFontScaleMultiplier(fs))
195 },
196 [setFontScale],
197 )
198 const [fontFamily, setFontFamily] = useState<Alf['fonts']['family']>(() =>
199 getFontFamily(),
200 )
201 const setFontFamilyAndPersist = useCallback<Alf['fonts']['setFontFamily']>(
202 ff => {
203 setFontFamily(ff)
204 persistFontFamily(ff)
205 },
206 [setFontFamily],
207 )
208
209 const value = useMemo<Alf>(() => {
210 const shiftedThemes = hueShifter(currentScheme, hue)
211
212 return {
213 themes: shiftedThemes,
214 themeName: themeName,
215 theme: shiftedThemes[themeName],
216 fonts: {
217 scale: fontScale,
218 scaleMultiplier: fontScaleMultiplier,
219 family: fontFamily,
220 setFontScale: setFontScaleAndPersist,
221 setFontFamily: setFontFamilyAndPersist,
222 },
223 flags: {},
224 }
225 }, [
226 currentScheme,
227 hue,
228 themeName,
229 fontScale,
230 fontScaleMultiplier,
231 fontFamily,
232 setFontScaleAndPersist,
233 setFontFamilyAndPersist,
234 ])
235
236 return <Context.Provider value={value}>{children}</Context.Provider>
237}
238
239export function useAlf() {
240 return useContext(Context)
241}
242
243export function useTheme(theme?: ThemeName) {
244 const alf = useAlf()
245 return useMemo(() => {
246 return theme ? alf.themes[theme] : alf.theme
247 }, [theme, alf])
248}