Bluesky's "Application Layout Framework"
1import {atoms} from './atoms'
2import {alpha} from './utils/alpha'
3
4import {
5 Palette,
6 DEFAULT_PALETTE,
7 DEFAULT_SUBDUED_PALETTE,
8 invertPalette,
9} from './palette'
10import {type ShadowStyle} from './atoms/types'
11
12export const themes = createThemes({
13 defaultPalette: DEFAULT_PALETTE,
14 subduedPalette: DEFAULT_SUBDUED_PALETTE,
15})
16
17export type ThemeAtoms = {
18 text: {
19 color: string
20 }
21 text_contrast_low: {
22 color: string
23 }
24 text_contrast_medium: {
25 color: string
26 }
27 text_contrast_high: {
28 color: string
29 }
30 text_inverted: {
31 color: string
32 }
33 bg: {
34 backgroundColor: string
35 }
36 bg_contrast_25: {
37 backgroundColor: string
38 }
39 bg_contrast_50: {
40 backgroundColor: string
41 }
42 bg_contrast_100: {
43 backgroundColor: string
44 }
45 bg_contrast_200: {
46 backgroundColor: string
47 }
48 bg_contrast_300: {
49 backgroundColor: string
50 }
51 bg_contrast_400: {
52 backgroundColor: string
53 }
54 bg_contrast_500: {
55 backgroundColor: string
56 }
57 bg_contrast_600: {
58 backgroundColor: string
59 }
60 bg_contrast_700: {
61 backgroundColor: string
62 }
63 bg_contrast_800: {
64 backgroundColor: string
65 }
66 bg_contrast_900: {
67 backgroundColor: string
68 }
69 bg_contrast_950: {
70 backgroundColor: string
71 }
72 bg_contrast_975: {
73 backgroundColor: string
74 }
75 border_contrast_low: {
76 borderColor: string
77 }
78 border_contrast_medium: {
79 borderColor: string
80 }
81 border_contrast_high: {
82 borderColor: string
83 }
84 shadow_sm: ShadowStyle
85 shadow_md: ShadowStyle
86 shadow_lg: ShadowStyle
87}
88
89/**
90 * Categorical representation of the theme
91 */
92export type ThemeScheme = 'light' | 'dark'
93
94/**
95 * Specific theme name, including low-contrast variants
96 */
97export type ThemeName = 'light' | 'dark' | 'dim'
98
99/**
100 * A theme object, containing the color palette and atoms for the theme
101 */
102export type Theme = {
103 scheme: ThemeScheme
104 name: ThemeName
105 palette: Palette
106 atoms: ThemeAtoms
107}
108
109export function createTheme({
110 scheme,
111 name,
112 palette,
113 options = {},
114}: {
115 scheme: ThemeScheme
116 name: ThemeName
117 palette: Palette
118 options?: {
119 shadowOpacity?: number
120 }
121}): Theme {
122 const shadowOpacity = options.shadowOpacity ?? 0.1
123 const shadowColor = alpha(palette.black, shadowOpacity)
124 return {
125 scheme,
126 name,
127 palette,
128 atoms: {
129 text: {
130 color: palette.contrast_1000,
131 },
132 text_contrast_low: {
133 color: palette.contrast_400,
134 },
135 text_contrast_medium: {
136 color: palette.contrast_700,
137 },
138 text_contrast_high: {
139 color: palette.contrast_900,
140 },
141 text_inverted: {
142 color: palette.contrast_0,
143 },
144 bg: {
145 backgroundColor: palette.contrast_0,
146 },
147 bg_contrast_25: {
148 backgroundColor: palette.contrast_25,
149 },
150 bg_contrast_50: {
151 backgroundColor: palette.contrast_50,
152 },
153 bg_contrast_100: {
154 backgroundColor: palette.contrast_100,
155 },
156 bg_contrast_200: {
157 backgroundColor: palette.contrast_200,
158 },
159 bg_contrast_300: {
160 backgroundColor: palette.contrast_300,
161 },
162 bg_contrast_400: {
163 backgroundColor: palette.contrast_400,
164 },
165 bg_contrast_500: {
166 backgroundColor: palette.contrast_500,
167 },
168 bg_contrast_600: {
169 backgroundColor: palette.contrast_600,
170 },
171 bg_contrast_700: {
172 backgroundColor: palette.contrast_700,
173 },
174 bg_contrast_800: {
175 backgroundColor: palette.contrast_800,
176 },
177 bg_contrast_900: {
178 backgroundColor: palette.contrast_900,
179 },
180 bg_contrast_950: {
181 backgroundColor: palette.contrast_950,
182 },
183 bg_contrast_975: {
184 backgroundColor: palette.contrast_975,
185 },
186 border_contrast_low: {
187 borderColor: palette.contrast_100,
188 },
189 border_contrast_medium: {
190 borderColor: palette.contrast_200,
191 },
192 border_contrast_high: {
193 borderColor: palette.contrast_300,
194 },
195 shadow_sm: {
196 ...atoms.shadow_sm,
197 shadowColor: palette.black,
198 boxShadow: `0 4px 6px -1px ${shadowColor}, 0 2px 4px -2px ${shadowColor}`,
199 },
200 shadow_md: {
201 ...atoms.shadow_md,
202 shadowColor: palette.black,
203 boxShadow: `0 10px 15px -3px ${shadowColor}, 0 4px 6px -4px ${shadowColor}`,
204 },
205 shadow_lg: {
206 ...atoms.shadow_lg,
207 shadowColor: palette.black,
208 boxShadow: `0 20px 25px -5px ${shadowColor}, 0 8px 10px -6px ${shadowColor}`,
209 },
210 },
211 }
212}
213
214export function createThemes({
215 defaultPalette,
216 subduedPalette,
217}: {
218 defaultPalette: Palette
219 subduedPalette: Palette
220}): {
221 light: Theme
222 dark: Theme
223 dim: Theme
224} {
225 const light = createTheme({
226 scheme: 'light',
227 name: 'light',
228 palette: defaultPalette,
229 })
230 const dark = createTheme({
231 scheme: 'dark',
232 name: 'dark',
233 palette: invertPalette(defaultPalette),
234 options: {
235 shadowOpacity: 0.4,
236 },
237 })
238 const dim = createTheme({
239 scheme: 'dark',
240 name: 'dim',
241 palette: invertPalette(subduedPalette),
242 options: {
243 shadowOpacity: 0.4,
244 },
245 })
246
247 return {
248 light,
249 dark,
250 dim,
251 }
252}