import { MaskOptions, addChildren, applyMask, createStrengthenMask, createTheme, createWeakenMask, } from '@tamagui/create-theme' import { mauve, slate, mauveDark, slateDark } from '@tamagui/colors' import { colorTokens } from '@tamagui/themes' import { keys } from '@beatgig/helpers/object' export const darkColors = { ...colorTokens.dark.blue, ...colorTokens.dark.gray, ...colorTokens.dark.green, ...colorTokens.dark.orange, ...colorTokens.dark.pink, ...colorTokens.dark.purple, ...colorTokens.dark.red, ...colorTokens.dark.yellow, } export const lightColors = { ...colorTokens.light.blue, ...colorTokens.light.gray, ...colorTokens.light.green, ...colorTokens.light.orange, ...colorTokens.light.pink, ...colorTokens.light.purple, ...colorTokens.light.red, ...colorTokens.light.yellow, } type ColorName = keyof typeof colorTokens.dark const lightTransparent = 'rgba(255,255,255,0)' const darkTransparent = 'rgba(10,10,10,0)' // background => foreground const palettes = { light: [ lightTransparent, // transparent mauve.mauve1, mauve.mauve2, slate.slate3, slate.slate4, slate.slate5, slate.slate6, slate.slate7, slate.slate8, slate.slate9, slate.slate10, slate.slate11, slate.slate12, darkTransparent, 'black', // contrast foreground 'white', // background strong ], dark: [ darkTransparent, mauveDark.mauve1, mauveDark.mauve2, slateDark.slate3, slateDark.slate4, slateDark.slate5, slateDark.slate6, slateDark.slate7, slateDark.slate8, slateDark.slate9, slateDark.slate10, slateDark.slate11, slateDark.slate12, lightTransparent, 'white', 'black', ], } const templateColors = { color1: 1, color2: 2, color3: 3, color4: 4, color5: 5, color6: 6, color7: 7, color8: 8, color9: 9, color10: 10, color11: 11, color12: 12, } const templateShadows = { shadowColor: 1, shadowColorHover: 1, shadowColorPress: 2, shadowColorFocus: 2, } // we can use subset of our template as a "skip" so it doesn't get adjusted with masks const skip = { ...templateColors, ...templateShadows, } // templates use the palette and specify index // negative goes backwards from end so -1 is the last item const template = { ...skip, // the background, color, etc keys here work like generics - they make it so you // can publish components for others to use without mandating a specific color scale // the @tamagui/button Button component looks for `$background`, so you set the // dark_red_Button theme to have a stronger background than the dark_red theme. background: 2, backgroundHover: 3, backgroundPress: 1, backgroundFocus: 2, backgroundStrong: -0, backgroundTransparent: 0, color: -3, colorHover: -4, colorPress: -3, colorFocus: -4, colorTransparent: -2, borderColor: 4, borderColorHover: 5, borderColorPress: 3, borderColorFocus: 4, placeholderColor: -7, contrastColor: -1, } const lightShadowColor = 'rgba(0,0,0,0.02)' const lightShadowColorStrong = 'rgba(0,0,0,0.066)' const darkShadowColor = 'rgba(0,0,0,0.2)' const darkShadowColorStrong = 'rgba(0,0,0,0.3)' const lightShadows = { shadowColor: lightShadowColorStrong, shadowColorHover: lightShadowColorStrong, shadowColorPress: lightShadowColor, shadowColorFocus: lightShadowColor, } const darkShadows = { shadowColor: darkShadowColorStrong, shadowColorHover: darkShadowColorStrong, shadowColorPress: darkShadowColor, shadowColorFocus: darkShadowColor, } const lightTemplate = { ...template, // our light color palette is... a bit unique borderColor: 6, borderColorHover: 7, borderColorFocus: 5, borderColorPress: 6, ...lightShadows, } const darkTemplate = { ...template, ...darkShadows } const light = createTheme(palettes.light, lightTemplate) const dark = createTheme(palettes.dark, darkTemplate) type SubTheme = typeof light const baseThemes: { light: SubTheme dark: SubTheme } = { light, dark, } const masks = { weaker: createWeakenMask(), stronger: createStrengthenMask(), } // default mask options for most uses const maskOptions: MaskOptions = { skip, // avoids the transparent ends max: palettes.light.length - 2, min: 1, } const allThemes = addChildren(baseThemes, (name, theme) => { const isLight = name === 'light' const inverseName = isLight ? 'dark' : 'light' const inverseTheme = baseThemes[inverseName] const transparent = (hsl: string, opacity = 0) => hsl.replace(`%)`, `%, ${opacity})`).replace(`hsl(`, `hsla(`) // setup colorThemes and their inverses const [colorThemes, inverseColorThemes] = [ colorTokens[name], colorTokens[inverseName], ].map((colorSet) => { return Object.fromEntries( keys(colorSet).map((color) => { const colorPalette = Object.values(colorSet[color]) as string[] // were re-ordering these const [head, tail] = [colorPalette.slice(0, 6), colorPalette.slice(6)] const contrasts: Record = { blue: 'white', gray: 'white', green: 'white', orange: 'white', pink: 'white', purple: 'white', red: 'white', yellow: 'black', } // add our transparent colors first/last // and make sure the last (foreground) color is white/black rather than colorful // this is mostly for consistency with the older theme-base const palette = [ transparent(colorPalette[0]), ...head, ...tail, theme.color, transparent(colorPalette[colorPalette.length - 1]), contrasts[color], // contrastColor isLight ? 'white' : 'black', // backgroundStrong ] const colorTheme = createTheme( palette, isLight ? { ...lightTemplate, // light color themes are a bit less sensitive borderColor: 4, borderColorHover: 5, borderColorFocus: 4, borderColorPress: 4, } : darkTemplate ) return [color, colorTheme] }) ) as Record }) const allColorThemes = addChildren(colorThemes, (colorName, colorTheme) => { const inverse = inverseColorThemes[colorName] return { ...getAltThemes(colorTheme, inverse), ...getComponentThemes(colorTheme, inverse), } }) const baseActiveTheme = applyMask(colorThemes.blue, masks.weaker, { ...maskOptions, strength: 4, }) const baseSubThemes = { ...getAltThemes(theme, inverseTheme, baseActiveTheme), ...getComponentThemes(theme, inverseTheme), } return { ...baseSubThemes, ...allColorThemes, } function getAltThemes( theme: SubTheme, inverse: SubTheme, activeTheme?: SubTheme ) { const maskOptionsAlt = { ...maskOptions, skip: templateShadows, } const alt1 = applyMask(theme, masks.weaker, maskOptionsAlt) const alt2 = applyMask(alt1, masks.weaker, maskOptionsAlt) const active = activeTheme ?? applyMask(theme, masks.weaker, { ...maskOptions, strength: 4, }) return addChildren({ alt1, alt2, active }, (_, subTheme) => { return getComponentThemes( subTheme, subTheme === inverse ? theme : inverse ) }) } function getComponentThemes(theme: SubTheme, inverse: SubTheme) { const weaker1 = applyMask(theme, masks.weaker, maskOptions) const weaker2 = applyMask(weaker1, masks.weaker, maskOptions) const stronger1 = applyMask(theme, masks.stronger, maskOptions) const inverse1 = applyMask(inverse, masks.weaker, maskOptions) const inverse2 = applyMask(inverse1, masks.weaker, maskOptions) const strongerBorderLighterBackground: SubTheme = isLight ? { ...stronger1, borderColor: weaker1.borderColor, borderColorHover: weaker1.borderColorHover, borderColorPress: weaker1.borderColorPress, borderColorFocus: weaker1.borderColorFocus, } : { ...theme, borderColor: weaker1.borderColor, borderColorHover: weaker1.borderColorHover, borderColorPress: weaker1.borderColorPress, borderColorFocus: weaker1.borderColorFocus, } const overlayTheme = { background: isLight ? 'rgba(0,0,0,0.5)' : 'rgba(255,255,255,0.9)', } return { Card: weaker1, Button: weaker2, Checkbox: weaker2, DrawerFrame: weaker1, SliderTrack: stronger1, SliderTrackActive: weaker2, SliderThumb: inverse1, Progress: weaker1, ProgressIndicator: inverse, Switch: weaker2, SwitchThumb: inverse2, TooltipArrow: weaker1, TooltipContent: weaker2, Input: strongerBorderLighterBackground, TextArea: strongerBorderLighterBackground, Tooltip: inverse1, SheetOverlay: overlayTheme, DialogOverlay: overlayTheme, ModalOverlay: overlayTheme, } } }) export const themesNew = { ...allThemes, // bring back the full type, the rest use a subset to avoid clogging up ts, // tamagui will be smart and use the top level themes as the type for useTheme() etc light: createTheme(palettes.light, lightTemplate, { nonInheritedValues: lightColors, }), dark: createTheme(palettes.dark, darkTemplate, { nonInheritedValues: darkColors, }), } // if (process.env.NODE_ENV === 'development') { // console.log(JSON.stringify(themes).length) // }