From 53c4023c33c134efdf76062d70825a1d88a72374 Mon Sep 17 00:00:00 2001 From: "whey.party" Date: Sun, 30 Nov 2025 18:07:42 +0100 Subject: [PATCH] color scheme selector --- src/alf/index.tsx | 36 +- src/alf/themes.ts | 712 ++++++++++++++++++++ src/alf/util/blackskyColorGeneration.ts | 49 ++ src/alf/util/deerColorGeneration.ts | 21 + src/alf/util/zeppelinColorGeneration.ts | 49 ++ src/lib/ThemeContext.tsx | 37 +- src/lib/themes.ts | 48 +- src/screens/Settings/AppearanceSettings.tsx | 41 +- src/state/persisted/schema.ts | 2 + src/state/shell/color-mode.tsx | 17 +- src/view/com/util/UserAvatar.tsx | 7 +- src/view/icons/Logo.tsx | 12 +- src/view/shell/index.web.tsx | 33 + 13 files changed, 1030 insertions(+), 34 deletions(-) create mode 100644 src/alf/util/blackskyColorGeneration.ts create mode 100644 src/alf/util/deerColorGeneration.ts create mode 100644 src/alf/util/zeppelinColorGeneration.ts diff --git a/src/alf/index.tsx b/src/alf/index.tsx index eed3fcbeb..193f1f949 100644 --- a/src/alf/index.tsx +++ b/src/alf/index.tsx @@ -1,6 +1,7 @@ import React from 'react' import {type Theme, type ThemeName} from '@bsky.app/alf' +import {useThemePrefs} from '#/state/shell/color-mode' import { computeFontScaleMultiplier, getFontFamily, @@ -8,7 +9,14 @@ import { setFontFamily as persistFontFamily, setFontScale as persistFontScale, } from '#/alf/fonts' -import {themes} from '#/alf/themes' +import { + blackskyscheme, + blueskyscheme, + deerscheme, + themes, + witchskyscheme, + zeppelinscheme, +} from '#/alf/themes' import {type Device} from '#/storage' export {type TextStyleProp, type Theme, type ViewStyleProp} from '@bsky.app/alf' @@ -56,10 +64,31 @@ export const Context = React.createContext({ }) Context.displayName = 'AlfContext' +export type SchemeType = typeof themes + +export function selectScheme(colorScheme: string | undefined): SchemeType { + switch (colorScheme) { + case 'witchsky': + return witchskyscheme + case 'bluesky': + return blueskyscheme + case 'blacksky': + return blackskyscheme + case 'deer': + return deerscheme + case 'zeppelin': + return zeppelinscheme + default: + return themes + } +} + export function ThemeProvider({ children, theme: themeName, }: React.PropsWithChildren<{theme: ThemeName}>) { + const {colorScheme} = useThemePrefs() + const currentScheme = selectScheme(colorScheme) const [fontScale, setFontScale] = React.useState(() => getFontScale(), ) @@ -91,9 +120,9 @@ export function ThemeProvider({ const value = React.useMemo( () => ({ - themes, + themes: currentScheme, themeName: themeName, - theme: themes[themeName], + theme: currentScheme[themeName], fonts: { scale: fontScale, scaleMultiplier: fontScaleMultiplier, @@ -104,6 +133,7 @@ export function ThemeProvider({ flags: {}, }), [ + currentScheme, themeName, fontScale, setFontScaleAndPersist, diff --git a/src/alf/themes.ts b/src/alf/themes.ts index 5fdfe22f5..f0219f888 100644 --- a/src/alf/themes.ts +++ b/src/alf/themes.ts @@ -5,6 +5,28 @@ import { // DEFAULT_SUBDUED_PALETTE, } from '@bsky.app/alf' +import { + BLUE_HUE as BLACKSKY_BLUE_HUE, + // defaultScale as BLACKSKY_defaultScale, + dimScale as BLACKSKY_dimScale, + GREEN_HUE as BLACKSKY_GREEN_HUE, + RED_HUE as BLACKSKY_RED_HUE, +} from '#/alf/util/blackskyColorGeneration' +import { + BLUE_HUE as ZEPPELIN_BLUE_HUE, + defaultScale as ZEPPELIN_defaultScale, + dimScale as ZEPPELIN_dimScale, + GREEN_HUE as ZEPPELIN_GREEN_HUE, + RED_HUE as ZEPPELIN_RED_HUE, +} from '#/alf/util/blackskyColorGeneration' +import { + BLUE_HUE as DEER_BLUE_HUE, + defaultScale as DEER_defaultScale, + // dimScale as DEER_dimScale, + GREEN_HUE as DEER_GREEN_HUE, + RED_HUE as DEER_RED_HUE, +} from '#/alf/util/deerColorGeneration' + export type Palette = { white: string black: string @@ -277,6 +299,696 @@ export const themes = { dim: DEFAULT_THEMES.dim, } +export const witchskyscheme = themes + +// const BLUESKY_THEMES = createThemes({ +// defaultPalette: BLUESKY_PALETTE, +// subduedPalette: BLUESKY_SUBDUED_PALETTE, +// }) + +// export const blueskyscheme = { +// lightPalette: BLUESKY_THEMES.light.palette, +// darkPalette: BLUESKY_THEMES.dark.palette, +// dimPalette: BLUESKY_THEMES.dim.palette, +// light: BLUESKY_THEMES.light, +// dark: BLUESKY_THEMES.dark, +// dim: BLUESKY_THEMES.dim, +// } +// export const YELLOW_PALETTE: Palette = { +// white: '#FEFBFB', +// black: '#000000', +// like: '#dd5e8f', + +// contrast_0: '#FEFBFB', +// contrast_25: '#F8F6EB', +// contrast_50: '#F2EDD8', +// contrast_100: '#E9E3C1', +// contrast_200: '#E0D9AA', +// contrast_300: '#D6CF94', +// contrast_400: '#CBC47F', +// contrast_500: '#C0B96B', +// contrast_600: '#A49D59', +// contrast_700: '#888249', +// contrast_800: '#6D683A', +// contrast_900: '#544F2C', +// contrast_950: '#3E391F', +// contrast_975: '#262413', +// contrast_1000: '#000000', + +// primary_25: `hsl(50, 70%, 97%)`, +// primary_50: `hsl(50, 70%, 94%)`, +// primary_100: `hsl(50, 70%, 88%)`, +// primary_200: `hsl(50, 75%, 78%)`, +// primary_300: `hsl(50, 78%, 68%)`, +// primary_400: `hsl(50, 82%, 58%)`, +// primary_500: `hsl(50, 85%, 52%)`, +// primary_600: `hsl(50, 80%, 46%)`, +// primary_700: `hsl(50, 60%, 33%)`, +// primary_800: `hsl(50, 48%, 26%)`, +// primary_900: `hsl(50, 45%, 18%)`, +// primary_950: `hsl(50, 40%, 10%)`, +// primary_975: `hsl(50, 38%, 7%)`, + +// positive_25: '#F3FCEB', +// positive_50: '#E8F9D5', +// positive_100: '#D4F4AE', +// positive_200: '#BEED81', +// positive_300: '#A4E34D', +// positive_400: '#8FD61E', +// positive_500: '#7AB815', +// positive_600: '#629412', +// positive_700: '#4E720F', +// positive_800: '#3C560C', +// positive_900: '#2D4109', +// positive_950: '#203006', +// positive_975: '#162204', + +// negative_25: '#FFF7EB', +// negative_50: '#FEEBD3', +// negative_100: '#FDDBB3', +// negative_200: '#FBC68B', +// negative_300: '#F7A44E', +// negative_400: '#EF8217', +// negative_500: '#D86E0F', +// negative_600: '#B55B0D', +// negative_700: '#8E480A', +// negative_800: '#6C3708', +// negative_900: '#4F2906', +// negative_950: '#371D04', +// negative_975: '#261403', +// } + +// export const YELLOW_SUBDUED_PALETTE: Palette = { +// white: '#FEFBFB', +// black: '#383434', +// like: '#dd5e8f', + +// contrast_0: '#FEFBFB', +// contrast_25: '#F8F6EB', +// contrast_50: '#F2EDD8', +// contrast_100: '#E9E3C1', +// contrast_200: '#E0D9AA', +// contrast_300: '#D6CF94', +// contrast_400: '#CBC47F', +// contrast_500: '#C0B96B', +// contrast_600: '#A49D59', +// contrast_700: '#888249', +// contrast_800: '#6D683A', +// contrast_900: '#544F2C', +// contrast_950: '#3E391F', +// contrast_975: '#312F1A', +// contrast_1000: '#262414', + +// primary_25: `hsl(50, 70%, 97%)`, +// primary_50: `hsl(50, 70%, 94%)`, +// primary_100: `hsl(50, 70%, 88%)`, +// primary_200: `hsl(50, 75%, 78%)`, +// primary_300: `hsl(50, 78%, 68%)`, +// primary_400: `hsl(50, 78%, 62%)`, +// primary_500: `hsl(50, 75%, 52%)`, +// primary_600: `hsl(50, 70%, 46%)`, +// primary_700: `hsl(50, 58%, 40%)`, +// primary_800: `hsl(50, 48%, 26%)`, +// primary_900: `hsl(50, 45%, 18%)`, +// primary_950: `hsl(50, 40%, 10%)`, +// primary_975: `hsl(50, 38%, 7%)`, + +// positive_25: '#F3FCEB', +// positive_50: '#EAF9DA', +// positive_100: '#D6F4B3', +// positive_200: '#C0ED87', +// positive_300: '#A8E45A', +// positive_400: '#94D93B', +// positive_500: '#7FBE2E', +// positive_600: '#659C25', +// positive_700: '#517B1D', +// positive_800: '#405F17', +// positive_900: '#314812', +// positive_950: '#23350C', +// positive_975: '#1A2709', + +// negative_25: '#FFF7EB', +// negative_50: '#FEEFE0', +// negative_100: '#FDE2C6', +// negative_200: '#FBCDA1', +// negative_300: '#F7AD68', +// negative_400: '#EF9140', +// negative_500: '#E0761F', +// negative_600: '#C06319', +// negative_700: '#995012', +// negative_800: '#75400E', +// negative_900: '#58300A', +// negative_950: '#3E2107', +// negative_975: '#2D1805', +// } + +// const YELLOW_THEMES = createThemes({ +// defaultPalette: YELLOW_PALETTE, +// subduedPalette: YELLOW_SUBDUED_PALETTE, +// }) + +// export const yellowscheme = { +// lightPalette: YELLOW_THEMES.light.palette, +// darkPalette: YELLOW_THEMES.dark.palette, +// dimPalette: YELLOW_THEMES.dim.palette, +// light: YELLOW_THEMES.light, +// dark: YELLOW_THEMES.dark, +// dim: YELLOW_THEMES.dim, +// } +export const BLACKSKY_BRAND = { + /* Neutrals */ + black: '#070C0C', + white: '#F8FAF9', + twilight: '#161E27', + gray300: '#C8CAC9', + gray400: '#9C9E9E', + gray600: '#6A6A6A', + + /* Primary / “Indigo‑violet” */ + primaryLight: '#6060E9', + primaryLightTint: '#EAEBFC', + primaryDark: '#8686FF', + primaryDarkTint: '#464985', + + /* Accent / Lime‑green (“success”) */ + secondary: '#D2FC51', + secondaryTint: '#F1FECB', + + /* Negative / Brand red */ + negative: '#F40B42', +} as const + +const scaleLight = (idx: number) => BLACKSKY_dimScale[idx] + +export const BLACKSKY_PALETTE: Palette = { + white: BLACKSKY_BRAND.white, + black: BLACKSKY_BRAND.black, + like: '#EC4899', + + // neutrals + contrast_0: BLACKSKY_BRAND.white, + contrast_25: BLACKSKY_BRAND.white, // Very Light + contrast_50: '#F0F2F2', + contrast_100: '#E6E8E8', + contrast_200: '#D1D3D3', + contrast_300: '#B6B8B8', + contrast_400: '#9C9E9E', + contrast_500: '#818383', + contrast_600: '#6A6A6A', + contrast_700: '#4F5050', + contrast_800: '#353636', + contrast_900: '#1F2020', + contrast_950: '#121313', + contrast_975: '#0B0C0C', + contrast_1000: BLACKSKY_BRAND.black, + + // primary (light scheme) + // Tuned to ensure _975 isn't too saturated so the inverted Dark Mode background is clean. + primary_25: BLACKSKY_BRAND.primaryLightTint, + primary_50: '#DCDDFA', + primary_100: '#C6C8F5', + primary_200: '#B0B3F0', + primary_300: '#989CED', + primary_400: '#8286E7', + primary_500: BLACKSKY_BRAND.primaryLight, + primary_600: '#5252C3', + primary_700: '#4545A8', + primary_800: '#38388D', + primary_900: '#2B2B71', + primary_950: '#151540', // Deepened and desaturated slightly + primary_975: '#0B0B24', // Almost black-blue, ensures Dark Mode BG isn't "muddy blue" + + // success + positive_25: BLACKSKY_BRAND.secondaryTint, + positive_50: '#EAFDD1', + positive_100: '#DAFCAB', + positive_200: '#C8FC80', + positive_300: '#BBFB66', + positive_400: '#AEFA59', + positive_500: BLACKSKY_BRAND.secondary, + positive_600: '#A0EC46', + positive_700: '#82C838', + positive_800: '#66942A', + positive_900: '#4A601C', + positive_950: '#2E3B0E', + positive_975: '#181F07', + + // error + negative_25: '#FFE5EC', + negative_50: '#FFD9E3', + negative_100: '#FFC1D1', + negative_200: '#FF9AB3', + negative_300: '#FF7396', + negative_400: '#FF4B78', + negative_500: BLACKSKY_BRAND.negative, + negative_600: '#C00A32', + negative_700: '#920826', + negative_800: '#630619', + negative_900: '#35030D', + negative_950: '#1B0206', + negative_975: '#0E0103', +} as const + +// The Subdued palette must be defined as a LIGHT palette. +// createThemes will then INVERT this to create the Dim (Dark Blue) theme. +// We map _25 to High Lightness and _975 to Low Lightness. +export const BLACKSKY_SUBDUED_PALETTE: Palette = { + ...BLACKSKY_PALETTE, + + // Override black to a softer twilight for the text in Light mode (optional) + // or primarily for the background color in the inverted Dim mode. + black: '#161E27', + + // Neutral / Contrast Scale (Blue-Tinted Grays) + // We utilize the dimScale in reverse: [14] is lightest, [1] is darkest. + contrast_0: '#FFFFFF', + contrast_25: `hsl(${BLACKSKY_BLUE_HUE}, 20%, ${scaleLight(14)}%)`, // Lightest + contrast_50: `hsl(${BLACKSKY_BLUE_HUE}, 20%, ${scaleLight(13)}%)`, + contrast_100: `hsl(${BLACKSKY_BLUE_HUE}, 20%, ${scaleLight(12)}%)`, + contrast_200: `hsl(${BLACKSKY_BLUE_HUE}, 20%, ${scaleLight(11)}%)`, + contrast_300: `hsl(${BLACKSKY_BLUE_HUE}, 15%, ${scaleLight(10)}%)`, + contrast_400: `hsl(${BLACKSKY_BLUE_HUE}, 15%, ${scaleLight(9)}%)`, + contrast_500: `hsl(${BLACKSKY_BLUE_HUE}, 15%, ${scaleLight(8)}%)`, + contrast_600: `hsl(${BLACKSKY_BLUE_HUE}, 15%, ${scaleLight(7)}%)`, + contrast_700: `hsl(${BLACKSKY_BLUE_HUE}, 15%, ${scaleLight(5)}%)`, + contrast_800: `hsl(${BLACKSKY_BLUE_HUE}, 20%, ${scaleLight(4)}%)`, + contrast_900: `hsl(${BLACKSKY_BLUE_HUE}, 24%, ${scaleLight(3)}%)`, + contrast_950: `hsl(${BLACKSKY_BLUE_HUE}, 28%, ${scaleLight(2)}%)`, + contrast_975: `hsl(${BLACKSKY_BLUE_HUE}, 30%, ${scaleLight(1)}%)`, // Darkest + contrast_1000: `hsl(${BLACKSKY_BLUE_HUE}, 30%, 8%)`, // Absolute Darkest + + // Subdued Primary + // Less saturation than the main palette to fit the "Subdued" vibe + primary_25: `hsl(240, 60%, 97%)`, + primary_50: `hsl(240, 60%, 95%)`, + primary_100: `hsl(240, 55%, 90%)`, + primary_200: `hsl(240, 50%, 80%)`, + primary_300: `hsl(240, 45%, 70%)`, + primary_400: `hsl(240, 40%, 60%)`, + primary_500: `hsl(240, 35%, 50%)`, // Midpoint + primary_600: `hsl(240, 40%, 45%)`, + primary_700: `hsl(240, 45%, 35%)`, + primary_800: `hsl(240, 50%, 25%)`, + primary_900: `hsl(240, 50%, 15%)`, + primary_950: `hsl(240, 50%, 10%)`, + primary_975: `hsl(240, 50%, 6%)`, + + // Subdued Success + positive_25: `hsl(${BLACKSKY_GREEN_HUE}, 60%, 96%)`, + positive_50: `hsl(${BLACKSKY_GREEN_HUE}, 60%, 93%)`, + positive_100: `hsl(${BLACKSKY_GREEN_HUE}, 55%, 88%)`, + positive_200: `hsl(${BLACKSKY_GREEN_HUE}, 50%, 80%)`, + positive_300: `hsl(${BLACKSKY_GREEN_HUE}, 50%, 70%)`, + positive_400: `hsl(${BLACKSKY_GREEN_HUE}, 50%, 60%)`, + positive_500: `hsl(${BLACKSKY_GREEN_HUE}, 50%, 50%)`, + positive_600: `hsl(${BLACKSKY_GREEN_HUE}, 55%, 40%)`, + positive_700: `hsl(${BLACKSKY_GREEN_HUE}, 60%, 30%)`, + positive_800: `hsl(${BLACKSKY_GREEN_HUE}, 60%, 20%)`, + positive_900: `hsl(${BLACKSKY_GREEN_HUE}, 60%, 15%)`, + positive_950: `hsl(${BLACKSKY_GREEN_HUE}, 60%, 10%)`, + positive_975: `hsl(${BLACKSKY_GREEN_HUE}, 60%, 5%)`, + + // Subdued Negative + negative_25: `hsl(${BLACKSKY_RED_HUE}, 70%, 97%)`, + negative_50: `hsl(${BLACKSKY_RED_HUE}, 70%, 95%)`, + negative_100: `hsl(${BLACKSKY_RED_HUE}, 65%, 90%)`, + negative_200: `hsl(${BLACKSKY_RED_HUE}, 60%, 80%)`, + negative_300: `hsl(${BLACKSKY_RED_HUE}, 55%, 70%)`, + negative_400: `hsl(${BLACKSKY_RED_HUE}, 55%, 60%)`, + negative_500: `hsl(${BLACKSKY_RED_HUE}, 60%, 50%)`, + negative_600: `hsl(${BLACKSKY_RED_HUE}, 60%, 45%)`, + negative_700: `hsl(${BLACKSKY_RED_HUE}, 65%, 35%)`, + negative_800: `hsl(${BLACKSKY_RED_HUE}, 65%, 25%)`, + negative_900: `hsl(${BLACKSKY_RED_HUE}, 70%, 15%)`, + negative_950: `hsl(${BLACKSKY_RED_HUE}, 70%, 10%)`, + negative_975: `hsl(${BLACKSKY_RED_HUE}, 70%, 5%)`, +} as const + +const BLACKSKY_THEMES = createThemes({ + defaultPalette: BLACKSKY_PALETTE, + subduedPalette: BLACKSKY_SUBDUED_PALETTE, +}) + +export const blackskyscheme = { + lightPalette: BLACKSKY_THEMES.light.palette, + darkPalette: BLACKSKY_THEMES.dark.palette, + dimPalette: BLACKSKY_THEMES.dim.palette, + light: BLACKSKY_THEMES.light, + dark: BLACKSKY_THEMES.dark, + dim: BLACKSKY_THEMES.dim, +} + +export const BLUESKY_PALETTE: Palette = { + white: '#FFFFFF', + black: '#000000', + like: '#EC4899', + + contrast_0: '#FFFFFF', + contrast_25: '#F9FAFB', + contrast_50: '#EFF2F6', + contrast_100: '#DCE2EA', + contrast_200: '#C0CAD8', + contrast_300: '#A5B2C5', + contrast_400: '#8798B0', + contrast_500: '#667B99', + contrast_600: '#526580', + contrast_700: '#405168', + contrast_800: '#313F54', + contrast_900: '#232E3E', + contrast_950: '#19222E', + contrast_975: '#111822', + contrast_1000: '#000000', + + primary_25: '#F5F9FF', + primary_50: '#E5F0FF', + primary_100: '#CCE1FF', + primary_200: '#A8CCFF', + primary_300: '#75AFFF', + primary_400: '#4291FF', + primary_500: '#006AFF', + primary_600: '#0059D6', + primary_700: '#0048AD', + primary_800: '#00398A', + primary_900: '#002861', + primary_950: '#001E47', + primary_975: '#001533', + + positive_25: '#ECFEF5', + positive_50: '#D3FDE8', + positive_100: '#A3FACF', + positive_200: '#6AF6B0', + positive_300: '#2CF28F', + positive_400: '#0DD370', + positive_500: '#09B35E', + positive_600: '#04904A', + positive_700: '#036D38', + positive_800: '#04522B', + positive_900: '#033F21', + positive_950: '#032A17', + positive_975: '#021D0F', + + negative_25: '#FFF5F7', + negative_50: '#FEE7EC', + negative_100: '#FDD3DD', + negative_200: '#FBBBCA', + negative_300: '#F891A9', + negative_400: '#F65A7F', + negative_500: '#E91646', + negative_600: '#CA123D', + negative_700: '#A71134', + negative_800: '#7F0B26', + negative_900: '#5F071C', + negative_950: '#430413', + negative_975: '#30030D', +} + +export const BLUESKY_SUBDUED_PALETTE: Palette = { + white: '#FFFFFF', + black: '#000000', + like: '#EC4899', + + contrast_0: '#FFFFFF', + contrast_25: '#F9FAFB', + contrast_50: '#F2F4F8', + contrast_100: '#E2E7EE', + contrast_200: '#C3CDDA', + contrast_300: '#ABB8C9', + contrast_400: '#8D9DB4', + contrast_500: '#6F839F', + contrast_600: '#586C89', + contrast_700: '#485B75', + contrast_800: '#394960', + contrast_900: '#2C3A4E', + contrast_950: '#222E3F', + contrast_975: '#1C2736', + contrast_1000: '#151D28', + + primary_25: '#F5F9FF', + primary_50: '#EBF3FF', + primary_100: '#D6E7FF', + primary_200: '#ADCFFF', + primary_300: '#80B5FF', + primary_400: '#4D97FF', + primary_500: '#0F73FF', + primary_600: '#0661E0', + primary_700: '#0A52B8', + primary_800: '#0E4490', + primary_900: '#123464', + primary_950: '#122949', + primary_975: '#122136', + + positive_25: '#ECFEF5', + positive_50: '#D8FDEB', + positive_100: '#A8FAD1', + positive_200: '#6FF6B3', + positive_300: '#31F291', + positive_400: '#0EDD75', + positive_500: '#0AC266', + positive_600: '#049F52', + positive_700: '#038142', + positive_800: '#056636', + positive_900: '#04522B', + positive_950: '#053D21', + positive_975: '#052917', + + negative_25: '#FFF5F7', + negative_50: '#FEEBEF', + negative_100: '#FDD8E1', + negative_200: '#FCC0CE', + negative_300: '#F99AB0', + negative_400: '#F76486', + negative_500: '#EB2452', + negative_600: '#D81341', + negative_700: '#BA1239', + negative_800: '#910D2C', + negative_900: '#6F0B22', + negative_950: '#500B1C', + negative_975: '#3E0915', +} + +const BLUESKY_THEMES = createThemes({ + defaultPalette: BLUESKY_PALETTE, + subduedPalette: BLUESKY_SUBDUED_PALETTE, +}) + +export const blueskyscheme = { + lightPalette: BLUESKY_THEMES.light.palette, + darkPalette: BLUESKY_THEMES.dark.palette, + dimPalette: BLUESKY_THEMES.dim.palette, + light: BLUESKY_THEMES.light, + dark: BLUESKY_THEMES.dark, + dim: BLUESKY_THEMES.dim, +} + +export const DEER_PALETTE: Palette = { + white: `hsl(${DEER_BLUE_HUE}, 20%, ${DEER_defaultScale[14]}%)`, + black: '#000000', + like: '#ec4899', + + contrast_0: `hsl(${DEER_BLUE_HUE}, 20%, ${DEER_defaultScale[14]}%)`, + contrast_25: `hsl(${DEER_BLUE_HUE}, 20%, ${DEER_defaultScale[13]}%)`, + contrast_50: `hsl(${DEER_BLUE_HUE}, 20%, ${DEER_defaultScale[12]}%)`, + contrast_100: `hsl(${DEER_BLUE_HUE}, 20%, ${DEER_defaultScale[11]}%)`, + contrast_200: `hsl(${DEER_BLUE_HUE}, 20%, ${DEER_defaultScale[10]}%)`, + contrast_300: `hsl(${DEER_BLUE_HUE}, 20%, ${DEER_defaultScale[9]}%)`, + contrast_400: `hsl(${DEER_BLUE_HUE}, 20%, ${DEER_defaultScale[8]}%)`, + contrast_500: `hsl(${DEER_BLUE_HUE}, 20%, ${DEER_defaultScale[7]}%)`, + contrast_600: `hsl(${DEER_BLUE_HUE}, 24%, ${DEER_defaultScale[6]}%)`, + contrast_700: `hsl(${DEER_BLUE_HUE}, 24%, ${DEER_defaultScale[5]}%)`, + contrast_800: `hsl(${DEER_BLUE_HUE}, 28%, ${DEER_defaultScale[4]}%)`, + contrast_900: `hsl(${DEER_BLUE_HUE}, 28%, ${DEER_defaultScale[3]}%)`, + contrast_950: `hsl(${DEER_BLUE_HUE}, 28%, ${DEER_defaultScale[2]}%)`, + contrast_975: `hsl(${DEER_BLUE_HUE}, 28%, ${DEER_defaultScale[1]}%)`, + contrast_1000: '#000000', + + primary_25: `hsl(145, 30%, 97%)`, + primary_50: `hsl(145, 30%, 95%)`, + primary_100: `hsl(145, 30%, 90%)`, + primary_200: `hsl(145, 32%, 80%)`, + primary_300: `hsl(145, 34%, 70%)`, + primary_400: `hsl(145, 35%, 58%)`, + primary_500: `hsl(145, 35%, 45%)`, + primary_600: `hsl(145, 38%, 38%)`, + primary_700: `hsl(145, 40%, 32%)`, + primary_800: `hsl(145, 42%, 25%)`, + primary_900: `hsl(145, 45%, 18%)`, + primary_950: `hsl(145, 48%, 10%)`, + primary_975: `hsl(145, 50%, 7%)`, + + positive_25: `hsl(${DEER_GREEN_HUE}, 82%, 97%)`, + positive_50: `hsl(${DEER_GREEN_HUE}, 82%, 95%)`, + positive_100: `hsl(${DEER_GREEN_HUE}, 82%, 90%)`, + positive_200: `hsl(${DEER_GREEN_HUE}, 82%, 80%)`, + positive_300: `hsl(${DEER_GREEN_HUE}, 82%, 70%)`, + positive_400: `hsl(${DEER_GREEN_HUE}, 82%, 60%)`, + positive_500: `hsl(${DEER_GREEN_HUE}, 82%, 50%)`, + positive_600: `hsl(${DEER_GREEN_HUE}, 82%, 42%)`, + positive_700: `hsl(${DEER_GREEN_HUE}, 82%, 34%)`, + positive_800: `hsl(${DEER_GREEN_HUE}, 82%, 26%)`, + positive_900: `hsl(${DEER_GREEN_HUE}, 82%, 18%)`, + positive_950: `hsl(${DEER_GREEN_HUE}, 82%, 10%)`, + positive_975: `hsl(${DEER_GREEN_HUE}, 82%, 7%)`, + + negative_25: `hsl(${DEER_RED_HUE}, 91%, 97%)`, + negative_50: `hsl(${DEER_RED_HUE}, 91%, 95%)`, + negative_100: `hsl(${DEER_RED_HUE}, 91%, 90%)`, + negative_200: `hsl(${DEER_RED_HUE}, 91%, 80%)`, + negative_300: `hsl(${DEER_RED_HUE}, 91%, 70%)`, + negative_400: `hsl(${DEER_RED_HUE}, 91%, 60%)`, + negative_500: `hsl(${DEER_RED_HUE}, 91%, 50%)`, + negative_600: `hsl(${DEER_RED_HUE}, 91%, 42%)`, + negative_700: `hsl(${DEER_RED_HUE}, 91%, 34%)`, + negative_800: `hsl(${DEER_RED_HUE}, 91%, 26%)`, + negative_900: `hsl(${DEER_RED_HUE}, 91%, 18%)`, + negative_950: `hsl(${DEER_RED_HUE}, 91%, 10%)`, + negative_975: `hsl(${DEER_RED_HUE}, 91%, 7%)`, +} + +export const DEER_SUBDUED_PALETTE: Palette = { + ...DEER_PALETTE, + primary_25: `hsl(140, 15%, 97%)`, + primary_50: `hsl(140, 18%, 95%)`, + primary_100: `hsl(140, 22%, 90%)`, + primary_200: `hsl(140, 25%, 80%)`, + primary_300: `hsl(140, 28%, 70%)`, + primary_400: `hsl(140, 32%, 58%)`, + primary_500: `hsl(140, 35%, 45%)`, + primary_600: `hsl(140, 38%, 38%)`, + primary_700: `hsl(140, 42%, 32%)`, + primary_800: `hsl(140, 45%, 25%)`, + primary_900: `hsl(140, 48%, 18%)`, + primary_950: `hsl(140, 50%, 10%)`, + primary_975: `hsl(140, 55%, 7%)`, + contrast_1000: '#151D28', +} + +const DEER_THEMES = createThemes({ + defaultPalette: DEER_PALETTE, + subduedPalette: DEER_SUBDUED_PALETTE, +}) + +export const deerscheme = { + lightPalette: DEER_THEMES.light.palette, + darkPalette: DEER_THEMES.dark.palette, + dimPalette: DEER_THEMES.dim.palette, + light: DEER_THEMES.light, + dark: DEER_THEMES.dark, + dim: DEER_THEMES.dim, +} + +export const ZEPPELIN_PALETTE: Palette = { + white: `hsl(${ZEPPELIN_BLUE_HUE}, 15%, ${ZEPPELIN_defaultScale[14]}%)`, + black: `hsl(${ZEPPELIN_BLUE_HUE}, 23%, ${ZEPPELIN_defaultScale[0]}%)`, + like: '#ec4899', + + contrast_0: `hsl(${ZEPPELIN_BLUE_HUE}, 15%, ${ZEPPELIN_defaultScale[14]}%)`, + contrast_25: `hsl(${ZEPPELIN_BLUE_HUE}, 15%, ${ZEPPELIN_defaultScale[13]}%)`, + contrast_50: `hsl(${ZEPPELIN_BLUE_HUE}, 15%, ${ZEPPELIN_defaultScale[12]}%)`, + contrast_100: `hsl(${ZEPPELIN_BLUE_HUE}, 15%, ${ZEPPELIN_defaultScale[11]}%)`, + contrast_200: `hsl(${ZEPPELIN_BLUE_HUE}, 15%, ${ZEPPELIN_defaultScale[10]}%)`, + contrast_300: `hsl(${ZEPPELIN_BLUE_HUE}, 15%, ${ZEPPELIN_defaultScale[9]}%)`, + contrast_400: `hsl(${ZEPPELIN_BLUE_HUE}, 15%, ${ZEPPELIN_defaultScale[8]}%)`, + contrast_500: `hsl(${ZEPPELIN_BLUE_HUE}, 15%, ${ZEPPELIN_defaultScale[7]}%)`, + contrast_600: `hsl(${ZEPPELIN_BLUE_HUE}, 19%, ${ZEPPELIN_defaultScale[6]}%)`, + contrast_700: `hsl(${ZEPPELIN_BLUE_HUE}, 19%, ${ZEPPELIN_defaultScale[5]}%)`, + contrast_800: `hsl(${ZEPPELIN_BLUE_HUE}, 23%, ${ZEPPELIN_defaultScale[4]}%)`, + contrast_900: `hsl(${ZEPPELIN_BLUE_HUE}, 23%, ${ZEPPELIN_defaultScale[3]}%)`, + contrast_950: `hsl(${ZEPPELIN_BLUE_HUE}, 23%, ${ZEPPELIN_defaultScale[2]}%)`, + contrast_975: `hsl(${ZEPPELIN_BLUE_HUE}, 23%, ${ZEPPELIN_defaultScale[1]}%)`, + contrast_1000: `hsl(${ZEPPELIN_BLUE_HUE}, 23%, ${ZEPPELIN_defaultScale[0]}%)`, + + primary_25: `hsl(47, 80%, 89%)`, + primary_50: `hsl(47, 80%, 87%)`, + primary_100: `hsl(47, 80%, 82%)`, + primary_200: `hsl(47, 72%, 72%)`, + primary_300: `hsl(47, 74%, 62%)`, + primary_400: `hsl(47, 75%, 50%)`, + primary_500: `hsl(47, 75%, 37%)`, + primary_600: `hsl(47, 78%, 30%)`, + primary_700: `hsl(47, 80%, 24%)`, + primary_800: `hsl(47, 82%, 17%)`, + primary_900: `hsl(47, 85%, 12%)`, + primary_950: `hsl(47, 88%, 5%)`, + primary_975: `hsl(47, 90%, 2%)`, + + positive_25: `hsl(${ZEPPELIN_GREEN_HUE}, 82%, 97%)`, + positive_50: `hsl(${ZEPPELIN_GREEN_HUE}, 82%, 95%)`, + positive_100: `hsl(${ZEPPELIN_GREEN_HUE}, 82%, 90%)`, + positive_200: `hsl(${ZEPPELIN_GREEN_HUE}, 82%, 80%)`, + positive_300: `hsl(${ZEPPELIN_GREEN_HUE}, 82%, 70%)`, + positive_400: `hsl(${ZEPPELIN_GREEN_HUE}, 82%, 60%)`, + positive_500: `hsl(${ZEPPELIN_GREEN_HUE}, 82%, 50%)`, + positive_600: `hsl(${ZEPPELIN_GREEN_HUE}, 82%, 42%)`, + positive_700: `hsl(${ZEPPELIN_GREEN_HUE}, 82%, 34%)`, + positive_800: `hsl(${ZEPPELIN_GREEN_HUE}, 82%, 26%)`, + positive_900: `hsl(${ZEPPELIN_GREEN_HUE}, 82%, 18%)`, + positive_950: `hsl(${ZEPPELIN_GREEN_HUE}, 82%, 10%)`, + positive_975: `hsl(${ZEPPELIN_GREEN_HUE}, 82%, 7%)`, + + negative_25: `hsl(${ZEPPELIN_RED_HUE}, 91%, 97%)`, + negative_50: `hsl(${ZEPPELIN_RED_HUE}, 91%, 95%)`, + negative_100: `hsl(${ZEPPELIN_RED_HUE}, 91%, 90%)`, + negative_200: `hsl(${ZEPPELIN_RED_HUE}, 91%, 80%)`, + negative_300: `hsl(${ZEPPELIN_RED_HUE}, 91%, 70%)`, + negative_400: `hsl(${ZEPPELIN_RED_HUE}, 91%, 60%)`, + negative_500: `hsl(${ZEPPELIN_RED_HUE}, 91%, 50%)`, + negative_600: `hsl(${ZEPPELIN_RED_HUE}, 91%, 42%)`, + negative_700: `hsl(${ZEPPELIN_RED_HUE}, 91%, 34%)`, + negative_800: `hsl(${ZEPPELIN_RED_HUE}, 91%, 26%)`, + negative_900: `hsl(${ZEPPELIN_RED_HUE}, 91%, 18%)`, + negative_950: `hsl(${ZEPPELIN_RED_HUE}, 91%, 10%)`, + negative_975: `hsl(${ZEPPELIN_RED_HUE}, 91%, 7%)`, +} + +export const ZEPPELIN_SUBDUED_PALETTE: Palette = { + ...ZEPPELIN_PALETTE, + black: `hsl(${ZEPPELIN_BLUE_HUE}, 3%, ${ZEPPELIN_dimScale[0]}%)`, + + contrast_0: `hsl(${ZEPPELIN_BLUE_HUE}, 20%, ${ZEPPELIN_dimScale[14]}%)`, + contrast_25: `hsl(${ZEPPELIN_BLUE_HUE}, 20%, ${ZEPPELIN_dimScale[13]}%)`, + contrast_50: `hsl(${ZEPPELIN_BLUE_HUE}, 20%, ${ZEPPELIN_dimScale[12]}%)`, + contrast_100: `hsl(${ZEPPELIN_BLUE_HUE}, 20%, ${ZEPPELIN_dimScale[11]}%)`, + contrast_200: `hsl(${ZEPPELIN_BLUE_HUE}, 20%, ${ZEPPELIN_dimScale[10]}%)`, + contrast_300: `hsl(${ZEPPELIN_BLUE_HUE}, 16%, ${ZEPPELIN_dimScale[9]}%)`, + contrast_400: `hsl(${ZEPPELIN_BLUE_HUE}, 16%, ${ZEPPELIN_dimScale[8]}%)`, + contrast_500: `hsl(${ZEPPELIN_BLUE_HUE}, 12%, ${ZEPPELIN_dimScale[7]}%)`, + contrast_600: `hsl(${ZEPPELIN_BLUE_HUE}, 12%, ${ZEPPELIN_dimScale[6]}%)`, + contrast_700: `hsl(${ZEPPELIN_BLUE_HUE}, 12%, ${ZEPPELIN_dimScale[5]}%)`, + contrast_800: `hsl(${ZEPPELIN_BLUE_HUE}, 12%, ${ZEPPELIN_dimScale[4]}%)`, + contrast_900: `hsl(${ZEPPELIN_BLUE_HUE}, 12%, ${ZEPPELIN_dimScale[3]}%)`, + contrast_950: `hsl(${ZEPPELIN_BLUE_HUE}, 12%, ${ZEPPELIN_dimScale[2]}%)`, + contrast_975: `hsl(${ZEPPELIN_BLUE_HUE}, 12%, ${ZEPPELIN_dimScale[1]}%)`, + contrast_1000: `hsl(${ZEPPELIN_BLUE_HUE}, 12%, ${ZEPPELIN_dimScale[0]}%)`, + + primary_25: `hsl(47, 60%, 97%)`, + primary_50: `hsl(47, 63%, 94%)`, + primary_100: `hsl(47, 65%, 88%)`, + primary_200: `hsl(47, 70%, 78%)`, + primary_300: `hsl(47, 73%, 68%)`, + primary_400: `hsl(47, 77%, 58%)`, + primary_500: `hsl(47, 80%, 45%)`, + primary_600: `hsl(47, 83%, 38%)`, + primary_700: `hsl(47, 87%, 30%)`, + primary_800: `hsl(47, 90%, 25%)`, + primary_900: `hsl(47, 93%, 18%)`, + primary_950: `hsl(47, 95%, 10%)`, + primary_975: `hsl(47, 98%, 7%)`, +} + +const ZEPPELIN_THEMES = createThemes({ + defaultPalette: ZEPPELIN_PALETTE, + subduedPalette: ZEPPELIN_SUBDUED_PALETTE, +}) + +export const zeppelinscheme = { + lightPalette: ZEPPELIN_THEMES.light.palette, + darkPalette: ZEPPELIN_THEMES.dark.palette, + dimPalette: ZEPPELIN_THEMES.dim.palette, + light: ZEPPELIN_THEMES.light, + dark: ZEPPELIN_THEMES.dark, + dim: ZEPPELIN_THEMES.dim, +} + /** * @deprecated use ALF and access palette from `useTheme()` */ diff --git a/src/alf/util/blackskyColorGeneration.ts b/src/alf/util/blackskyColorGeneration.ts new file mode 100644 index 000000000..9b0dce332 --- /dev/null +++ b/src/alf/util/blackskyColorGeneration.ts @@ -0,0 +1,49 @@ +import {logger} from '#/logger' + +export const BLUE_HUE = 240 +export const RED_HUE = 0 +export const GREEN_HUE = 80 + +/** + * Smooth progression of lightness "stops" for generating HSL colors. + */ +export const COLOR_STOPS = [ + 0, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.85, 0.9, 0.95, 1, +] + +export function generateScale(start: number, end: number) { + const range = end - start + return COLOR_STOPS.map(stop => { + return start + range * stop + }) +} + +export const defaultScale = generateScale(6, 100) +// dim shifted 6% lighter +export const dimScale = generateScale(12, 100) + +export function transparentifyColor(color: string, alpha: number) { + if (color.startsWith('hsl(')) { + return 'hsla(' + color.slice('hsl('.length, -1) + `, ${alpha})` + } else if (color.startsWith('rgb(')) { + return 'rgba(' + color.slice('rgb('.length, -1) + `, ${alpha})` + } else if (color.startsWith('#')) { + if (color.length === 7) { + const alphaHex = Math.round(alpha * 255).toString(16) + // Per MDN: If there is only one number, it is duplicated: e means ee + // https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color + return color.slice(0, 7) + alphaHex.padStart(2, alphaHex) + } else if (color.length === 4) { + // convert to 6-digit hex before adding alpha + const [r, g, b] = color.slice(1).split('') + const alphaHex = Math.round(alpha * 255).toString(16) + return `#${r.repeat(2)}${g.repeat(2)}${b.repeat(2)}${alphaHex.padStart( + 2, + alphaHex, + )}` + } + } else { + logger.warn(`Could not make '${color}' transparent`) + } + return color +} diff --git a/src/alf/util/deerColorGeneration.ts b/src/alf/util/deerColorGeneration.ts new file mode 100644 index 000000000..8d769b51b --- /dev/null +++ b/src/alf/util/deerColorGeneration.ts @@ -0,0 +1,21 @@ +export const BLUE_HUE = 211 +export const RED_HUE = 346 +export const GREEN_HUE = 152 + +/** + * Smooth progression of lightness "stops" for generating HSL colors. + */ +export const COLOR_STOPS = [ + 0, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.85, 0.9, 0.95, 1, +] + +export function generateScale(start: number, end: number) { + const range = end - start + return COLOR_STOPS.map(stop => { + return start + range * stop + }) +} + +export const defaultScale = generateScale(6, 100) +// dim shifted 6% lighter +export const dimScale = generateScale(12, 100) diff --git a/src/alf/util/zeppelinColorGeneration.ts b/src/alf/util/zeppelinColorGeneration.ts new file mode 100644 index 000000000..587cb5cbd --- /dev/null +++ b/src/alf/util/zeppelinColorGeneration.ts @@ -0,0 +1,49 @@ +import {logger} from '#/logger' + +export const BLUE_HUE = 47 +export const RED_HUE = 346 +export const GREEN_HUE = 152 + +/** + * Smooth progression of lightness "stops" for generating HSL colors. + */ +export const COLOR_STOPS = [ + 0, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.85, 0.9, 0.95, 1, +] + +export function generateScale(start: number, end: number) { + const range = end - start + return COLOR_STOPS.map(stop => { + return start + range * stop + }) +} + +export const defaultScale = generateScale(6, 100) +// dim shifted 6% lighter +export const dimScale = generateScale(11, 100) + +export function transparentifyColor(color: string, alpha: number) { + if (color.startsWith('hsl(')) { + return 'hsla(' + color.slice('hsl('.length, -1) + `, ${alpha})` + } else if (color.startsWith('rgb(')) { + return 'rgba(' + color.slice('rgb('.length, -1) + `, ${alpha})` + } else if (color.startsWith('#')) { + if (color.length === 7) { + const alphaHex = Math.round(alpha * 255).toString(16) + // Per MDN: If there is only one number, it is duplicated: e means ee + // https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color + return color.slice(0, 7) + alphaHex.padStart(2, alphaHex) + } else if (color.length === 4) { + // convert to 6-digit hex before adding alpha + const [r, g, b] = color.slice(1).split('') + const alphaHex = Math.round(alpha * 255).toString(16) + return `#${r.repeat(2)}${g.repeat(2)}${b.repeat(2)}${alphaHex.padStart( + 2, + alphaHex, + )}` + } + } else { + logger.warn(`Could not make '${color}' transparent`) + } + return color +} diff --git a/src/lib/ThemeContext.tsx b/src/lib/ThemeContext.tsx index d499910a7..e51f6a19d 100644 --- a/src/lib/ThemeContext.tsx +++ b/src/lib/ThemeContext.tsx @@ -1,8 +1,11 @@ -import {type ReactNode} from 'react' +import {type ReactNode, useMemo} from 'react' import {createContext, useContext} from 'react' import {type TextStyle, type ViewStyle} from 'react-native' import {type ThemeName} from '@bsky.app/alf' +import {useThemePrefs} from '#/state/shell/color-mode' +import {type SchemeType, selectScheme} from '#/alf' +import {themes} from '#/alf/themes' import {darkTheme, defaultTheme, dimTheme} from './themes' export type ColorScheme = 'light' | 'dark' @@ -88,21 +91,32 @@ export interface ThemeProviderProps { theme: ThemeName } -export const ThemeContext = createContext(defaultTheme) +export const ThemeContext = createContext( + defaultTheme({ + lightPalette: themes.lightPalette, + darkPalette: themes.darkPalette, + }), +) ThemeContext.displayName = 'ThemeContext' export const useTheme = () => useContext(ThemeContext) -function getTheme(theme: ThemeName) { - switch (theme) { +function getTheme(themeName: ThemeName, scheme: SchemeType) { + const paletteOptions = { + lightPalette: scheme.lightPalette, + darkPalette: scheme.darkPalette, + dimPalette: scheme.dimPalette, + } + + switch (themeName) { case 'light': - return defaultTheme + return defaultTheme(paletteOptions) case 'dim': - return dimTheme + return dimTheme(paletteOptions) case 'dark': - return darkTheme + return darkTheme(paletteOptions) default: - return defaultTheme + return defaultTheme(paletteOptions) } } @@ -110,7 +124,12 @@ export const ThemeProvider: React.FC = ({ theme, children, }) => { - const themeValue = getTheme(theme) + const {colorScheme} = useThemePrefs() + + const themeValue = useMemo(() => { + const currentScheme = selectScheme(colorScheme) + return getTheme(theme, currentScheme) + }, [theme, colorScheme]) return ( {children} diff --git a/src/lib/themes.ts b/src/lib/themes.ts index d99dd37b2..bc9fb7563 100644 --- a/src/lib/themes.ts +++ b/src/lib/themes.ts @@ -1,12 +1,18 @@ import {Platform} from 'react-native' import {tokens} from '#/alf' -import {darkPalette, dimPalette, lightPalette} from '#/alf/themes' +import {type Palette} from '#/alf/themes' import {fontWeight} from '#/alf/tokens' import {colors} from './styles' import {type Theme} from './ThemeContext' -export const defaultTheme: Theme = { +export const defaultTheme = ({ + lightPalette, + darkPalette, +}: { + lightPalette: Palette + darkPalette: Palette +}): Theme => ({ colorScheme: 'light', palette: { default: { @@ -288,13 +294,19 @@ export const defaultTheme: Theme = { fontFamily: Platform.OS === 'android' ? 'monospace' : 'Courier New', }, }, -} +}) -export const darkTheme: Theme = { - ...defaultTheme, +export const darkTheme = ({ + lightPalette, + darkPalette, +}: { + lightPalette: Palette + darkPalette: Palette +}): Theme => ({ + ...defaultTheme({lightPalette, darkPalette}), colorScheme: 'dark', palette: { - ...defaultTheme.palette, + ...defaultTheme({lightPalette, darkPalette}).palette, default: { background: darkPalette.contrast_0, backgroundLight: darkPalette.contrast_25, @@ -318,11 +330,11 @@ export const darkTheme: Theme = { borderLinkHover: darkPalette.contrast_300, }, primary: { - ...defaultTheme.palette.primary, + ...defaultTheme({lightPalette, darkPalette}).palette.primary, textInverted: colors.blue2, }, secondary: { - ...defaultTheme.palette.secondary, + ...defaultTheme({lightPalette, darkPalette}).palette.secondary, textInverted: colors.green2, }, inverted: { @@ -337,14 +349,22 @@ export const darkTheme: Theme = { icon: lightPalette.contrast_500, }, }, -} +}) -export const dimTheme: Theme = { - ...darkTheme, +export const dimTheme = ({ + lightPalette, + darkPalette, + dimPalette, +}: { + lightPalette: Palette + darkPalette: Palette + dimPalette: Palette +}): Theme => ({ + ...darkTheme({lightPalette, darkPalette}), palette: { - ...darkTheme.palette, + ...darkTheme({lightPalette, darkPalette}).palette, default: { - ...darkTheme.palette.default, + ...darkTheme({lightPalette, darkPalette}).palette.default, background: dimPalette.contrast_0, backgroundLight: dimPalette.contrast_25, text: dimPalette.white, @@ -367,4 +387,4 @@ export const dimTheme: Theme = { borderLinkHover: dimPalette.contrast_300, }, }, -} +}) diff --git a/src/screens/Settings/AppearanceSettings.tsx b/src/screens/Settings/AppearanceSettings.tsx index 658434c03..a6faeacfe 100644 --- a/src/screens/Settings/AppearanceSettings.tsx +++ b/src/screens/Settings/AppearanceSettings.tsx @@ -20,6 +20,7 @@ import * as SegmentedControl from '#/components/forms/SegmentedControl' import {type Props as SVGIconProps} from '#/components/icons/common' import {Moon_Stroke2_Corner0_Rounded as MoonIcon} from '#/components/icons/Moon' import {Phone_Stroke2_Corner0_Rounded as PhoneIcon} from '#/components/icons/Phone' +import {Pizza_Stroke2_Corner0_Rounded as PizzaIcon} from '#/components/icons/Pizza' import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/TextSize' import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase' import * as Layout from '#/components/Layout' @@ -32,8 +33,8 @@ export function AppearanceSettingsScreen({}: Props) { const {_} = useLingui() const {fonts} = useAlf() - const {colorMode, darkTheme} = useThemePrefs() - const {setColorMode, setDarkTheme} = useSetThemePrefs() + const {colorMode, colorScheme, darkTheme} = useThemePrefs() + const {setColorMode, setColorScheme, setDarkTheme} = useSetThemePrefs() const onChangeAppearance = useCallback( (value: 'light' | 'system' | 'dark') => { @@ -42,6 +43,13 @@ export function AppearanceSettingsScreen({}: Props) { [setColorMode], ) + const onChangeScheme = useCallback( + (value: 'witchsky' | 'bluesky' | 'blacksky' | 'deer' | 'zeppelin') => { + setColorScheme(value) + }, + [setColorScheme], + ) + const onChangeDarkTheme = useCallback( (value: 'dim' | 'dark') => { setDarkTheme(value) @@ -98,6 +106,35 @@ export function AppearanceSettingsScreen({}: Props) { onChange={onChangeAppearance} /> + + {colorMode !== 'light' && ( const schema = z.object({ colorMode: z.enum(['system', 'light', 'dark']), darkTheme: z.enum(['dim', 'dark']).optional(), + colorScheme: z.enum(['witchsky', 'bluesky', 'blacksky', 'deer', 'zeppelin']), session: z.object({ accounts: z.array(accountSchema), currentAccount: currentAccountSchema.optional(), @@ -164,6 +165,7 @@ export type Schema = z.infer export const defaults: Schema = { colorMode: 'system', darkTheme: 'dim', + colorScheme: 'witchsky', session: { accounts: [], currentAccount: undefined, diff --git a/src/state/shell/color-mode.tsx b/src/state/shell/color-mode.tsx index 14495f06d..05c7e7ee9 100644 --- a/src/state/shell/color-mode.tsx +++ b/src/state/shell/color-mode.tsx @@ -5,15 +5,18 @@ import * as persisted from '#/state/persisted' type StateContext = { colorMode: persisted.Schema['colorMode'] darkTheme: persisted.Schema['darkTheme'] + colorScheme: persisted.Schema['colorScheme'] } type SetContext = { setColorMode: (v: persisted.Schema['colorMode']) => void setDarkTheme: (v: persisted.Schema['darkTheme']) => void + setColorScheme: (v: persisted.Schema['colorScheme']) => void } const stateContext = React.createContext({ colorMode: 'system', darkTheme: 'dark', + colorScheme: 'witchsky', }) stateContext.displayName = 'ColorModeStateContext' const setContext = React.createContext({} as SetContext) @@ -22,13 +25,17 @@ setContext.displayName = 'ColorModeSetContext' export function Provider({children}: React.PropsWithChildren<{}>) { const [colorMode, setColorMode] = React.useState(persisted.get('colorMode')) const [darkTheme, setDarkTheme] = React.useState(persisted.get('darkTheme')) + const [colorScheme, setColorScheme] = React.useState( + persisted.get('colorScheme'), + ) const stateContextValue = React.useMemo( () => ({ colorMode, darkTheme, + colorScheme, }), - [colorMode, darkTheme], + [colorMode, darkTheme, colorScheme], ) const setContextValue = React.useMemo( @@ -41,6 +48,10 @@ export function Provider({children}: React.PropsWithChildren<{}>) { setDarkTheme(_darkTheme) persisted.write('darkTheme', _darkTheme) }, + setColorScheme: (_colorScheme: persisted.Schema['colorScheme']) => { + setColorScheme(_colorScheme) + persisted.write('colorScheme', _colorScheme) + }, }), [], ) @@ -52,9 +63,13 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const unsub2 = persisted.onUpdate('colorMode', nextColorMode => { setColorMode(nextColorMode) }) + const unsub3 = persisted.onUpdate('colorScheme', nextColorScheme => { + setColorScheme(nextColorScheme) + }) return () => { unsub1() unsub2() + unsub3() } }, []) diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 7b51b62d7..4332cd319 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -104,6 +104,7 @@ let DefaultAvatar = ({ size: number }): React.ReactNode => { const finalShape = overrideShape ?? (type === 'user' ? 'circle' : 'square') + const t = useTheme() const aviStyle = useMemo(() => { if (finalShape === 'square') { @@ -123,7 +124,7 @@ let DefaultAvatar = ({ fill="none" stroke="none" style={aviStyle}> - + - + - - + + )} diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 4b8b47acd..8ce65874e 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -46,6 +46,39 @@ function ShellInner() { const {state: policyUpdateState} = usePolicyUpdateContext() const welcomeModalControl = useWelcomeModal() + useLayoutEffect(() => { + const rootElement = document.documentElement + rootElement.className = `html` + rootElement.style.setProperty( + 'background', + `${t.atoms.bg.backgroundColor}`, + 'important', + ) + }, [t.atoms.bg.backgroundColor, t.name]) + + useLayoutEffect(() => { + const color = t.palette.primary_500 + + const styleId = 'prosemirror-mention-color' + let style = document.getElementById(styleId) as HTMLStyleElement | null + + if (!style) { + style = document.createElement('style') + style.id = styleId + document.head.appendChild(style) + } + + style.innerHTML = ` + .ProseMirror .mention { + color: ${color} !important; + } + .ProseMirror a, + .ProseMirror .autolink { + color: ${color} !important; + } + ` + }, [t.palette.primary_500]) + useLayoutEffect(() => { if (showDrawer !== showDrawerDelayedExit) { if (showDrawer) { -- 2.47.3