From a4b22794f651625a19c8c7a0169bcc4e5c5fec1b Mon Sep 17 00:00:00 2001 From: Mehdi BHA Date: Fri, 25 Oct 2024 00:01:22 +0100 Subject: [PATCH] feat: generate color shades --- www/src/app/themes/theme-customizer.tsx | 29 +-- www/src/hooks/use-themes.ts | 28 +++ www/src/lib/colors.ts | 297 +++++++++++++++++------- www/src/lib/themes.ts | 4 +- www/src/types/theme.ts | 2 +- 5 files changed, 251 insertions(+), 109 deletions(-) diff --git a/www/src/app/themes/theme-customizer.tsx b/www/src/app/themes/theme-customizer.tsx index 4ba37692..45d52084 100644 --- a/www/src/app/themes/theme-customizer.tsx +++ b/www/src/app/themes/theme-customizer.tsx @@ -22,7 +22,6 @@ import { ColorPicker } from "@/registry/ui/default/core/color-picker"; import { Label } from "@/registry/ui/default/core/field"; import { Form } from "@/registry/ui/default/core/form"; import { InputProps } from "@/registry/ui/default/core/input"; -import { Link } from "@/registry/ui/default/core/link"; import { Item } from "@/registry/ui/default/core/list-box"; import { Select } from "@/registry/ui/default/core/select"; import { Skeleton } from "@/registry/ui/default/core/skeleton"; @@ -94,9 +93,6 @@ export const ThemeCustomizer = (
- {/*

- You can generate color scales using these base colors. -

*/}
{( [ @@ -156,20 +152,20 @@ export const ThemeCustomizer = ( handleColorConfigChange("lightness", value as number) } - size="lg" + size="sm" className="!w-full" /> handleColorConfigChange("saturation", value as number) @@ -180,14 +176,6 @@ export const ThemeCustomizer = (
- {/*

- There are 5 color scales in the color system. You can learn more - about it{" "} - - here - - . -

*/}
{( [ @@ -328,7 +316,6 @@ const ThemeName = ({ return () => document.removeEventListener("mousedown", handleClickOutside); }, [onDismiss]); - // Dismiss when edit mode when esc is pressed React.useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape" && editMode) { @@ -406,7 +393,6 @@ const ThemeName = ({ }; const Section = ({ - title, children, className, wrapperClassName, @@ -418,9 +404,6 @@ const Section = ({ }) => { return (
- {/*

- {title} -

*/}
{children}
); @@ -459,9 +442,7 @@ const AutoResizeInput = React.forwardRef( const [inputValue, setInputValue] = useControlledState( props.value, props.defaultValue ?? "", - () => { - // Do nothing - } + () => {} ); const inputRef = React.useRef(null); diff --git a/www/src/hooks/use-themes.ts b/www/src/hooks/use-themes.ts index 1b1cdad7..9560f1d6 100644 --- a/www/src/hooks/use-themes.ts +++ b/www/src/hooks/use-themes.ts @@ -2,6 +2,7 @@ import { useAtom } from "jotai"; import { withImmer } from "jotai-immer"; import { atomWithStorage } from "jotai/utils"; import { nanoid } from "nanoid"; +import { buildColorScales } from "@/lib/colors"; import { defaultTheme, dotUIThemes } from "@/lib/themes"; import { useMounted } from "@/registry/hooks/use-mounted"; import { BaseColor, Theme } from "@/types/theme"; @@ -29,6 +30,7 @@ export const useThemes = () => { const currentTheme = [...state.themes, ...dotUIThemes].find( (t) => t.id === state.currentThemeId ) as Theme; + const isCurrentThemeEditable = state.themes.some( (t) => t.id === state.currentThemeId ); @@ -38,11 +40,13 @@ export const useThemes = () => { draft.currentThemeId = themeId; }); }; + const setMode = (mode: Mode) => { setState((draft) => { draft.mode = mode; }); }; + const createTheme = ( themeProperties: Prettify< Omit, "id" | "name"> & { name: string } @@ -89,6 +93,18 @@ export const useThemes = () => { const theme = draft.themes.find((t) => t.id === draft.currentThemeId); if (theme) { theme.colors[state.mode][baseColor].baseColor = value; + const currentColors = theme.colors[state.mode]; + const modeConfig = buildColorScales({ + neutral: { baseColors: [currentColors.neutral.baseColor] }, + warning: { baseColors: [currentColors.warning.baseColor] }, + success: { baseColors: [currentColors.success.baseColor] }, + danger: { baseColors: [currentColors.danger.baseColor] }, + accent: { baseColors: [currentColors.accent.baseColor] }, + lightness: currentColors.lightness, + saturation: currentColors.saturation, + mode: state.mode, + }); + theme.colors[state.mode] = modeConfig; } }); }; @@ -102,6 +118,18 @@ export const useThemes = () => { const theme = draft.themes.find((t) => t.id === draft.currentThemeId); if (theme) { theme.colors[state.mode][config] = value; + const currentColors = theme.colors[state.mode]; + const modeConfig = buildColorScales({ + neutral: { baseColors: [currentColors.neutral.baseColor] }, + warning: { baseColors: [currentColors.warning.baseColor] }, + success: { baseColors: [currentColors.success.baseColor] }, + danger: { baseColors: [currentColors.danger.baseColor] }, + accent: { baseColors: [currentColors.accent.baseColor] }, + lightness: currentColors.lightness, + saturation: currentColors.saturation, + mode: state.mode, + }); + theme.colors[state.mode] = modeConfig; } }); }; diff --git a/www/src/lib/colors.ts b/www/src/lib/colors.ts index 9a45b317..b73a6561 100644 --- a/www/src/lib/colors.ts +++ b/www/src/lib/colors.ts @@ -7,6 +7,7 @@ import { BackgroundColor, type CssColor, } from "@adobe/leonardo-contrast-colors"; +import { ColorShade, ModeConfig } from "@/types/theme"; export const getContrastTextColor = (color: string): "white" | "black" => { const RGBColor = convertColorValue(color, "RGB", true) as unknown as { @@ -22,30 +23,6 @@ export const getContrastTextColor = (color: string): "white" | "black" => { return contrastText; }; -type ThemeMode = "light" | "dark"; - -type ColorConfig = - | { - baseColors: CssColor[]; - ratios?: number[]; - } - | CssColor[]; - -type Colors = { - neutral: ColorConfig; - accent: ColorConfig; - primary: ColorConfig; - success: ColorConfig; - warning: ColorConfig; - danger: ColorConfig; -}; - -type ThemeConfig = { - saturation: number; - lightness: number; - colors: Colors; -}; - const defaultConfig = { light: { saturation: 100, @@ -55,10 +32,6 @@ const defaultConfig = { baseColors: ["#000000"], ratios: [1, 1.5, 1.8, 2.23, 3.16, 4.78, 6.36, 8.28, 13.2, 15.2], }, - primary: { - baseColors: ["#ffffff"], - ratios: [1.25, 1.5, 1.8, 2.23, 3.16, 4.78, 6.36, 8.28, 13.2, 15.2], - }, success: { baseColors: ["#1A9338"], ratios: [1.25, 1.5, 1.8, 2.23, 3.16, 4.78, 6.36, 8.28, 13.2, 15.2], @@ -85,10 +58,6 @@ const defaultConfig = { baseColors: ["#ffffff"], ratios: [1.25, 1.5, 1.8, 2.23, 3.16, 4.78, 6.36, 8.28, 13.2, 15.2], }, - primary: { - baseColors: ["#000000"], - ratios: [1.25, 1.5, 1.8, 2.23, 3.16, 4.78, 6.36, 8.28, 13.2, 15.2], - }, success: { baseColors: ["#1A9338"], ratios: [1.25, 1.5, 1.8, 2.23, 3.16, 4.78, 6.36, 8.28, 13.2, 15.2], @@ -107,67 +76,231 @@ const defaultConfig = { }, }, }, -} as const satisfies Record; - -export const buildDotUIColorScales = ( - mode: ThemeMode, - colors: Colors, - options?: { - saturation?: number; - lightness?: number; - } -) => { - const defaultModeConfig = defaultConfig[mode]; - - const getColorKeys = (colorBase: keyof Colors) => { - const color = colors[colorBase]; - return Array.isArray(color) ? color : color.baseColors; - }; +}; - const getRatios = (colorBase: keyof Colors) => { - const color = colors[colorBase]; - return Array.isArray(color) - ? defaultModeConfig.colors[colorBase].ratios - : (color.ratios ?? defaultModeConfig.colors[colorBase].ratios); - }; +type ColorConfig = { + baseColors: string[]; + ratios?: number[]; +}; + +type BuildColorScalesProps = { + neutral: ColorConfig; + accent: ColorConfig; + warning: ColorConfig; + success: ColorConfig; + danger: ColorConfig; + lightness: number; + saturation: number; + mode: "light" | "dark"; +}; +export const buildColorScales = ({ + neutral: neutralConfig, + lightness, + saturation, + mode, + ...props +}: BuildColorScalesProps): ModeConfig => { const neutral = new BackgroundColor({ - name: "neutral-", - colorKeys: getColorKeys("neutral"), - ratios: getRatios("neutral"), + name: "neutral", + colorKeys: neutralConfig.baseColors as CssColor[], + ratios: neutralConfig.ratios ?? defaultConfig[mode].colors.neutral.ratios, }); - const colorScales = Object.entries(colors) - .map(([name]) => { - if (name === "neutral") return null; - - return new Color({ - name: `${name}-`, - colorKeys: getColorKeys(name as keyof Colors), - ratios: getRatios(name as keyof Colors), - }); - }) - .filter(Boolean) as Color[]; + const colorScales = Object.entries(props).map(([name, config]) => { + return new Color({ + name, + colorKeys: config.baseColors as CssColor[], + ratios: + config.ratios ?? + defaultConfig[mode].colors[ + name as keyof (typeof defaultConfig)[typeof mode]["colors"] + ].ratios, + }); + }); const theme = new Theme({ colors: [neutral, ...colorScales], backgroundColor: neutral, - lightness: options?.lightness ?? defaultModeConfig.lightness, - saturation: options?.saturation ?? defaultModeConfig.saturation, + lightness, + saturation, output: "HSL", }); - const colorVars = Object.entries(theme.contrastColorPairs).reduce( - (acc, [key, value]) => { - if (key === "background") { - return acc; - } - const [h, s, l] = value.match(/\d+(\.\d+)?/g) || []; - acc[key] = `hsl(${h} ${s}% ${l}%)`; - return acc; - }, - {} as Record - ); + const [_, ...contrastColors] = theme.contrastColors; - return colorVars; + return { + neutral: { + baseColor: neutralConfig.baseColors[0], + shades: contrastColors + .find((color) => color.name === "neutral") + ?.values.map((c) => c.value) as ColorShade, + }, + success: { + baseColor: props.success.baseColors[0], + shades: contrastColors + .find((color) => color.name === "success") + ?.values.map((c) => c.value) as ColorShade, + }, + warning: { + baseColor: props.warning.baseColors[0], + shades: contrastColors + .find((color) => color.name === "warning") + ?.values.map((c) => c.value) as ColorShade, + }, + danger: { + baseColor: props.danger.baseColors[0], + shades: contrastColors + .find((color) => color.name === "danger") + ?.values.map((c) => c.value) as ColorShade, + }, + accent: { + baseColor: props.accent.baseColors[0], + shades: contrastColors + .find((color) => color.name === "accent") + ?.values.map((c) => c.value) as ColorShade, + }, + lightness, + saturation, + }; }; + +// const neutralScale = new Color({ +// name: "neutral", +// colorKeys: ["#000000"], +// ratios: +// }); +// const theme = new Theme({ +// colors: [neutral, ...colorScales], +// backgroundColor: neutral, +// lightness: options?.lightness ?? defaultModeConfig.lightness, +// saturation: options?.saturation ?? defaultModeConfig.saturation, +// output: "HSL", +// }); +// }; + +// const defaultConfig = { +// light: { +// saturation: 100, +// lightness: 97, +// colors: { +// neutral: { +// baseColors: ["#000000"], +// ratios: [1, 1.5, 1.8, 2.23, 3.16, 4.78, 6.36, 8.28, 13.2, 15.2], +// }, +// primary: { +// baseColors: ["#ffffff"], +// ratios: [1.25, 1.5, 1.8, 2.23, 3.16, 4.78, 6.36, 8.28, 13.2, 15.2], +// }, +// success: { +// baseColors: ["#1A9338"], +// ratios: [1.25, 1.5, 1.8, 2.23, 3.16, 4.78, 6.36, 8.28, 13.2, 15.2], +// }, +// warning: { +// baseColors: ["#E79D13"], +// ratios: [1.25, 1.5, 1.8, 2.23, 3.16, 4.78, 6.36, 8.28, 13.2, 15.2], +// }, +// danger: { +// baseColors: ["#D93036"], +// ratios: [1.25, 1.5, 1.8, 2.23, 3.16, 4.78, 6.36, 8.28, 13.2, 15.2], +// }, +// accent: { +// baseColors: ["#0091FF"], +// ratios: [1.25, 1.5, 1.8, 2.23, 3.16, 4.78, 6.36, 8.28, 13.2, 15.2], +// }, +// }, +// }, +// dark: { +// saturation: 100, +// lightness: 0, +// colors: { +// neutral: { +// baseColors: ["#ffffff"], +// ratios: [1.25, 1.5, 1.8, 2.23, 3.16, 4.78, 6.36, 8.28, 13.2, 15.2], +// }, +// primary: { +// baseColors: ["#000000"], +// ratios: [1.25, 1.5, 1.8, 2.23, 3.16, 4.78, 6.36, 8.28, 13.2, 15.2], +// }, +// success: { +// baseColors: ["#1A9338"], +// ratios: [1.25, 1.5, 1.8, 2.23, 3.16, 4.78, 6.36, 8.28, 13.2, 15.2], +// }, +// warning: { +// baseColors: ["#E79D13"], +// ratios: [1.25, 1.5, 1.8, 2.23, 3.16, 4.78, 6.36, 8.28, 13.2, 15.2], +// }, +// danger: { +// baseColors: ["#D93036"], +// ratios: [1.25, 1.5, 1.8, 2.23, 3.16, 4.78, 6.36, 8.28, 13.2, 15.2], +// }, +// accent: { +// baseColors: ["#0091FF"], +// ratios: [1.25, 1.5, 1.8, 2.23, 3.16, 4.78, 6.36, 8.28, 13.2, 15.2], +// }, +// }, +// }, +// } as const satisfies Record; + +// export const buildDotUIColorScales = ( +// mode: ThemeMode, +// colors: Colors, +// options?: { +// saturation?: number; +// lightness?: number; +// } +// ) => { +// const defaultModeConfig = defaultConfig[mode]; + +// const getColorKeys = (colorBase: keyof Colors) => { +// const color = colors[colorBase]; +// return Array.isArray(color) ? color : color.baseColors; +// }; + +// const getRatios = (colorBase: keyof Colors) => { +// const color = colors[colorBase]; +// return Array.isArray(color) +// ? defaultModeConfig.colors[colorBase].ratios +// : (color.ratios ?? defaultModeConfig.colors[colorBase].ratios); +// }; + +// const neutral = new BackgroundColor({ +// name: "neutral-", +// colorKeys: getColorKeys("neutral"), +// ratios: getRatios("neutral"), +// }); + +// const colorScales = Object.entries(colors) +// .map(([name]) => { +// if (name === "neutral") return null; + +// return new Color({ +// name: `${name}-`, +// colorKeys: getColorKeys(name as keyof Colors), +// ratios: getRatios(name as keyof Colors), +// }); +// }) +// .filter(Boolean) as Color[]; + +// const theme = new Theme({ +// colors: [neutral, ...colorScales], +// backgroundColor: neutral, +// lightness: options?.lightness ?? defaultModeConfig.lightness, +// saturation: options?.saturation ?? defaultModeConfig.saturation, +// output: "HSL", +// }); + +// const colorVars = Object.entries(theme.contrastColorPairs).reduce( +// (acc, [key, value]) => { +// if (key === "background") { +// return acc; +// } +// const [h, s, l] = value.match(/\d+(\.\d+)?/g) || []; +// acc[key] = `hsl(${h} ${s}% ${l}%)`; +// return acc; +// }, +// {} as Record +// ); + +// return colorVars; +// }; diff --git a/www/src/lib/themes.ts b/www/src/lib/themes.ts index 283e1ce0..62018168 100644 --- a/www/src/lib/themes.ts +++ b/www/src/lib/themes.ts @@ -87,7 +87,7 @@ export const dotUIThemes: Theme[] = [ ], }, lightness: 100, - saturation: 0, + saturation: 100, }, dark: { neutral: { @@ -166,7 +166,7 @@ export const dotUIThemes: Theme[] = [ ], }, lightness: 0, - saturation: 0, + saturation: 100, }, }, }, diff --git a/www/src/types/theme.ts b/www/src/types/theme.ts index f3534c77..7fe8e033 100644 --- a/www/src/types/theme.ts +++ b/www/src/types/theme.ts @@ -1,4 +1,4 @@ -type ColorShade = [ +export type ColorShade = [ string, string, string,