Greenlens/constants/Colors.ts

355 lines
9.5 KiB
TypeScript

import { AppColorScheme, ColorPalette } from '../types';
interface SchemeColors {
text: string;
textSecondary: string;
textMuted: string;
textOnImage: string;
background: string;
surface: string;
surfaceMuted: string;
surfaceStrong: string;
border: string;
borderStrong: string;
overlay: string;
overlayStrong: string;
heroButton: string;
heroButtonBorder: string;
tabBarBg: string;
tabBarBorder: string;
inputBg: string;
inputBorder: string;
cardBg: string;
cardBorder: string;
cardShadow: string;
chipBg: string;
chipBorder: string;
pageBase: string;
pageGradientStart: string;
pageGradientEnd: string;
pageTexture: string;
}
interface PaletteColors {
primary: string;
primaryDark: string;
info: string;
warning: string;
danger: string;
success: string;
accent: string;
}
interface PaletteSurfaceTone {
background: string;
surface: string;
surfaceMuted: string;
surfaceStrong: string;
border: string;
borderStrong: string;
tabBarBg: string;
tabBarBorder: string;
pageBase: string;
pageGradientStart: string;
pageGradientEnd: string;
pageTexture: string;
}
interface PaletteThemeTone {
light: PaletteSurfaceTone;
dark: PaletteSurfaceTone;
}
export interface AppColors extends SchemeColors, PaletteColors {
onPrimary: string;
primarySoft: string;
infoSoft: string;
warningSoft: string;
dangerSoft: string;
successSoft: string;
primaryTint: string;
infoTint: string;
warningTint: string;
dangerTint: string;
successTint: string;
mutedChip: string;
fabBg: string;
fabShadow: string;
iconOnImage: string;
}
const textByScheme: Record<
AppColorScheme,
Pick<SchemeColors, 'text' | 'textSecondary' | 'textMuted' | 'textOnImage' | 'overlay' | 'overlayStrong'>
> = {
light: {
text: '#1f2520',
textSecondary: '#4d5650',
textMuted: '#6e7871',
textOnImage: '#f7f8f7',
overlay: 'rgba(16, 20, 17, 0.4)',
overlayStrong: 'rgba(13, 17, 14, 0.78)',
},
dark: {
text: '#f0f3f1',
textSecondary: '#c9d0cb',
textMuted: '#a1aba4',
textOnImage: '#f5f7f6',
overlay: 'rgba(9, 11, 10, 0.52)',
overlayStrong: 'rgba(8, 10, 9, 0.86)',
},
};
const paletteSurfaces: Record<ColorPalette, PaletteThemeTone> = {
forest: {
light: {
background: '#ecf3ed',
surface: '#f5faf6',
surfaceMuted: '#e4ede6',
surfaceStrong: '#d8e3d9',
border: '#c7d4c8',
borderStrong: '#b3c4b5',
tabBarBg: '#f1f7f2',
tabBarBorder: '#ccd9ce',
pageBase: '#e9f1ea',
pageGradientStart: '#dde9de',
pageGradientEnd: '#f1f5f0',
pageTexture: '#4b6a50',
},
dark: {
background: '#111813',
surface: '#17211a',
surfaceMuted: '#1d2920',
surfaceStrong: '#243128',
border: '#2b3a2f',
borderStrong: '#394c3d',
tabBarBg: '#141d17',
tabBarBorder: '#2f4033',
pageBase: '#111813',
pageGradientStart: '#15211a',
pageGradientEnd: '#0f1511',
pageTexture: '#91b196',
},
},
ocean: {
light: {
background: '#ebf1f8',
surface: '#f4f8fc',
surfaceMuted: '#e1e8f1',
surfaceStrong: '#d4deeb',
border: '#c2cfde',
borderStrong: '#acbdd1',
tabBarBg: '#eef4fa',
tabBarBorder: '#c8d6e6',
pageBase: '#e9f0f7',
pageGradientStart: '#d9e3f0',
pageGradientEnd: '#eef3f8',
pageTexture: '#4c6480',
},
dark: {
background: '#10171f',
surface: '#16202b',
surfaceMuted: '#1c2734',
surfaceStrong: '#243143',
border: '#2c3b4d',
borderStrong: '#3a4e64',
tabBarBg: '#131c26',
tabBarBorder: '#324357',
pageBase: '#10171f',
pageGradientStart: '#152230',
pageGradientEnd: '#0f151d',
pageTexture: '#96aac2',
},
},
sunset: {
light: {
background: '#f7eee8',
surface: '#fdf7f3',
surfaceMuted: '#f2e5da',
surfaceStrong: '#ead8c8',
border: '#dbc4b1',
borderStrong: '#cfb197',
tabBarBg: '#f9f0e8',
tabBarBorder: '#decaB9',
pageBase: '#f6ece4',
pageGradientStart: '#efdccd',
pageGradientEnd: '#f8f1eb',
pageTexture: '#8a644e',
},
dark: {
background: '#1b1410',
surface: '#271c16',
surfaceMuted: '#31231b',
surfaceStrong: '#3f2d22',
border: '#4a372a',
borderStrong: '#604633',
tabBarBg: '#231913',
tabBarBorder: '#553d2c',
pageBase: '#1a1410',
pageGradientStart: '#2a1d15',
pageGradientEnd: '#16110d',
pageTexture: '#c8a690',
},
},
mono: {
light: {
background: '#eff1f3',
surface: '#f7f8fa',
surfaceMuted: '#e5e8ec',
surfaceStrong: '#d8dde3',
border: '#c8ced6',
borderStrong: '#b4bdc8',
tabBarBg: '#f2f4f7',
tabBarBorder: '#cdd3db',
pageBase: '#eceff2',
pageGradientStart: '#dfe4ea',
pageGradientEnd: '#f2f4f6',
pageTexture: '#666f7b',
},
dark: {
background: '#131518',
surface: '#1b1f24',
surfaceMuted: '#232932',
surfaceStrong: '#2d3540',
border: '#343f4c',
borderStrong: '#455364',
tabBarBg: '#171b20',
tabBarBorder: '#3a4552',
pageBase: '#131518',
pageGradientStart: '#1a2028',
pageGradientEnd: '#111418',
pageTexture: '#a1adbc',
},
},
};
const palettes: Record<ColorPalette, PaletteColors> = {
forest: {
primary: '#5fa779',
primaryDark: '#3d7f57',
info: '#4e7fb3',
warning: '#bb8a36',
danger: '#be5d5d',
success: '#4f9767',
accent: '#4a8e7f',
},
ocean: {
primary: '#5a90be',
primaryDark: '#3d6f99',
info: '#4e79b1',
warning: '#bc8b37',
danger: '#be6464',
success: '#4f8b80',
accent: '#4f8fa0',
},
sunset: {
primary: '#c98965',
primaryDark: '#a36442',
info: '#6c89b4',
warning: '#bd8742',
danger: '#b8666d',
success: '#769f6e',
accent: '#b47453',
},
mono: {
primary: '#7b8796',
primaryDark: '#5b6574',
info: '#748498',
warning: '#9d8b5a',
danger: '#a86868',
success: '#6f8b75',
accent: '#6c7785',
},
};
const hexToRgb = (hex: string) => {
const cleaned = hex.replace('#', '');
const normalized =
cleaned.length === 3
? cleaned
.split('')
.map((char) => `${char}${char}`)
.join('')
: cleaned;
const int = Number.parseInt(normalized, 16);
return {
r: (int >> 16) & 255,
g: (int >> 8) & 255,
b: int & 255,
};
};
const withOpacity = (hex: string, opacity: number) => {
const { r, g, b } = hexToRgb(hex);
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
};
const mixColors = (baseHex: string, tintHex: string, tintWeight: number) => {
const base = hexToRgb(baseHex);
const tint = hexToRgb(tintHex);
const weight = Math.max(0, Math.min(1, tintWeight));
const r = Math.round(base.r + (tint.r - base.r) * weight);
const g = Math.round(base.g + (tint.g - base.g) * weight);
const b = Math.round(base.b + (tint.b - base.b) * weight);
return `rgb(${r}, ${g}, ${b})`;
};
const getOnColor = (hex: string) => {
const { r, g, b } = hexToRgb(hex);
const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
return luminance > 0.58 ? '#111111' : '#ffffff';
};
export const useColors = (
isDark: boolean,
palette: ColorPalette = 'forest'
): AppColors => {
const schemeKey: AppColorScheme = isDark ? 'dark' : 'light';
const textColors = textByScheme[schemeKey];
const paletteColors = palettes[palette];
const tone = paletteSurfaces[palette][schemeKey];
return {
...textColors,
...paletteColors,
background: tone.background,
surface: tone.surface,
surfaceMuted: tone.surfaceMuted,
surfaceStrong: tone.surfaceStrong,
border: tone.border,
borderStrong: tone.borderStrong,
tabBarBg: tone.tabBarBg,
tabBarBorder: tone.tabBarBorder,
inputBg: tone.surfaceMuted,
inputBorder: tone.borderStrong,
cardBg: withOpacity(tone.surface, isDark ? 0.9 : 0.92),
cardBorder: withOpacity(tone.borderStrong, isDark ? 0.7 : 0.82),
cardShadow: withOpacity('#000000', isDark ? 0.26 : 0.12),
chipBg: tone.surfaceStrong,
chipBorder: tone.border,
pageBase: tone.pageBase,
pageGradientStart: withOpacity(tone.pageGradientStart, isDark ? 0.44 : 0.52),
pageGradientEnd: withOpacity(tone.pageGradientEnd, isDark ? 0.34 : 0.44),
pageTexture: withOpacity(tone.pageTexture, isDark ? 0.09 : 0.07),
overlay: textColors.overlay,
overlayStrong: textColors.overlayStrong,
heroButton: withOpacity(tone.surface, isDark ? 0.9 : 0.94),
heroButtonBorder: withOpacity(tone.borderStrong, isDark ? 0.7 : 0.8),
onPrimary: getOnColor(paletteColors.primary),
primarySoft: withOpacity(paletteColors.primary, isDark ? 0.26 : 0.18),
infoSoft: withOpacity(paletteColors.info, isDark ? 0.24 : 0.16),
warningSoft: withOpacity(paletteColors.warning, isDark ? 0.24 : 0.18),
dangerSoft: withOpacity(paletteColors.danger, isDark ? 0.2 : 0.14),
successSoft: withOpacity(paletteColors.success, isDark ? 0.2 : 0.14),
primaryTint: mixColors(tone.surfaceStrong, paletteColors.primary, isDark ? 0.22 : 0.2),
infoTint: mixColors(tone.surfaceStrong, paletteColors.info, isDark ? 0.2 : 0.18),
warningTint: mixColors(tone.surfaceStrong, paletteColors.warning, isDark ? 0.22 : 0.2),
dangerTint: mixColors(tone.surfaceStrong, paletteColors.danger, isDark ? 0.2 : 0.18),
successTint: mixColors(tone.surfaceStrong, paletteColors.success, isDark ? 0.2 : 0.18),
mutedChip: tone.surfaceStrong,
fabBg: paletteColors.primary,
fabShadow: withOpacity(paletteColors.primaryDark, 0.75),
iconOnImage: '#ffffff',
};
};