355 lines
9.5 KiB
TypeScript
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',
|
|
};
|
|
};
|