From 722ebf338ce3a7cbb26eecc367fe5b949559d7fe Mon Sep 17 00:00:00 2001 From: Javi Aguilar <122741+itsjavi@users.noreply.github.com> Date: Tue, 4 Jun 2024 05:22:04 +0200 Subject: [PATCH] feat(colors): implement color creation form --- app/(components)/colors/page.tsx | 4 +- .../components/color-scale-creator.tsx | 41 ++++-- .../components/color-scale-editor.tsx | 88 ++++++++++-- .../components/color-system-lab.tsx | 132 ++++++++---------- modules/color-system/types.ts | 1 + modules/design-system/components/button.tsx | 11 -- 6 files changed, 164 insertions(+), 113 deletions(-) diff --git a/app/(components)/colors/page.tsx b/app/(components)/colors/page.tsx index 0ee60ed..1307ae9 100644 --- a/app/(components)/colors/page.tsx +++ b/app/(components)/colors/page.tsx @@ -31,8 +31,8 @@ export default async function () { the 600 level for all the rest.

- You can copy the token names by clicking on a color. For accent colors, you can also edit their hue. More - customization options are coming soon. + You can copy the token names by clicking on a color. For accent colors, you can also edit their hue and name + by clicking on the name. More customization options are coming soon.
diff --git a/modules/color-system/components/color-scale-creator.tsx b/modules/color-system/components/color-scale-creator.tsx index ef14ed2..348b10d 100644 --- a/modules/color-system/components/color-scale-creator.tsx +++ b/modules/color-system/components/color-scale-creator.tsx @@ -20,6 +20,7 @@ type ColorScaleCreatorProps = { initialColor?: ColorActionPayload onDelete?: (color: ColorActionPayload) => void onSave?: (color: ColorActionPayload) => void + onCancel?: () => void colorpickerSliders?: OklchEditorSlider[] } @@ -31,11 +32,11 @@ function _parseColor(color: string, fallback: ColorActionPayload['value']): Colo return parsed ?? fallback } -function _nearestColorName(value: string | Color | undefined | null, fallback: string): string { +function _nearestColorName(value: string | Color | undefined | null, fallback?: string): string { if (!value) { - return fallback + return fallback ?? '' } - return safeNearestColorNames(value, 1)[0] ?? fallback + return safeNearestColorNames(value, 1)[0] ?? fallback ?? '' } function _uniqueColorName(value: string, currentColorNames: string[]): string { @@ -46,7 +47,11 @@ function _uniqueColorName(value: string, currentColorNames: string[]): string { return value } -function _uniqueNearestColorName(value: string | Color, fallback: string, currentColorNames: string[]): string { +function _uniqueNearestColorName( + value: string | Color, + fallback: string | undefined, + currentColorNames: string[], +): string { return _uniqueColorName(_nearestColorName(value, fallback), currentColorNames) } @@ -57,12 +62,14 @@ export default function ColorScaleCreator({ colorpickerSliders = ['l', 'c', 'h', 'alpha'], onDelete, onSave, + onCancel, }: ColorScaleCreatorProps) { - const [editMode, setEditMode] = useState(false) + const [editMode, setEditMode] = useState(deletable === true) const [pickerOpen, setPickerOpen] = useState(false) const [color, setColor] = useState( initialColor ?? { name: 'black', + newName: 'black', alias: 'bg', valueCss: '#000', value: { @@ -81,6 +88,7 @@ export default function ColorScaleCreator({ if (onDelete) { onDelete(color) } + setEditMode(false) } @@ -88,7 +96,16 @@ export default function ColorScaleCreator({ if (onSave) { onSave(color) } - // setEditMode(false) + + setEditMode(false) + } + + function handleCancel() { + if (onCancel) { + onCancel() + } + + setEditMode(false) } if (editMode) { @@ -105,13 +122,13 @@ export default function ColorScaleCreator({ size="xs" as="div" > - {color.name || '(new)'} + {deletable ? '' : color.newName || '(new)'} setColor({ ...color, name: _uniqueColorName(e.target.value, currentColorNames) })} + value={color.newName ?? color.name} + onChange={(e) => setColor({ ...color, newName: _uniqueColorName(e.target.value, currentColorNames) })} /> {/* setColor({ ...color, - name: _uniqueNearestColorName(e.target.value, color.name, currentColorNames), + newName: color.newName ?? _uniqueNearestColorName(e.target.value, color.newName, currentColorNames), value: _parseColor(e.target.value, color.value), valueCss: e.target.value, }) @@ -142,7 +159,7 @@ export default function ColorScaleCreator({ onChange={(newColor, cssValue) => { setColor({ ...color, - name: _uniqueNearestColorName(cssValue, color.name, currentColorNames), + newName: color.newName ?? _uniqueNearestColorName(cssValue, color.newName, currentColorNames), value: newColor, valueCss: cssValue, }) @@ -168,7 +185,7 @@ export default function ColorScaleCreator({ Delete )} - setEditMode(false)}> + Cancel Save diff --git a/modules/color-system/components/color-scale-editor.tsx b/modules/color-system/components/color-scale-editor.tsx index bc6694a..034f138 100644 --- a/modules/color-system/components/color-scale-editor.tsx +++ b/modules/color-system/components/color-scale-editor.tsx @@ -1,7 +1,11 @@ 'use client' +import { PrimaryButton } from '@/modules/design-system/components/button' import useDarkMode from '@/modules/design-system/hooks/use-darkmode' +import { css } from '@/styled-system/css' +import { useState } from 'react' import type { ColorActionPayload, ColorConfig, ColorCssStrings, ColorLevelKey, ColorScheme } from '../types' +import ColorScaleCreator from './color-scale-creator' import ColorScaleGrid from './color-scale-grid' import ColorScaleLevelEditor from './color-scale-level-editor' @@ -9,28 +13,86 @@ type ColorScaleEditorProps = { fg: ColorCssStrings config: ColorConfig onChange?: (color: ColorActionPayload) => void + onDelete?: (color: ColorActionPayload) => void + currentColorNames: string[] + initialColor?: ColorActionPayload + editMode?: boolean + editor?: React.ReactNode } -export default function ColorScaleEditor({ fg, config, onChange }: ColorScaleEditorProps) { +export default function ColorScaleEditor({ + fg, + onDelete, + currentColorNames, + initialColor, + config, + editor, + onChange, +}: ColorScaleEditorProps) { const { isDarkMode } = useDarkMode() + const [editMode, setEditMode] = useState(false) - const colorTitle = config.name + const colorTitle = + config.group === 'accent' ? ( + setEditMode(true)} + > + {config.name} + + ) : ( + config.name + ) // const title = config.aliases.length > 0 ? config.aliases[0] : config.name const scheme: ColorScheme = isDarkMode ? 'dark' : 'light' const scaleLevels = Object.keys(config.scale) as ColorLevelKey[] return ( - - {scaleLevels.map((level) => ( - + + {scaleLevels.map((level) => ( + + ))} + + {editMode && ( + { + onDelete?.(color) + setEditMode(false) + }} + onCancel={() => { + setEditMode(false) + }} + onSave={(color) => { + onChange?.(color) + setEditMode(false) + }} /> - ))} - + )} + ) } diff --git a/modules/color-system/components/color-system-lab.tsx b/modules/color-system/components/color-system-lab.tsx index 5f28ed3..33d43dd 100644 --- a/modules/color-system/components/color-system-lab.tsx +++ b/modules/color-system/components/color-system-lab.tsx @@ -1,16 +1,19 @@ 'use client' import { PandaDiv } from '@/modules/design-system/components/panda' -import { buildSpectrum } from '@effective/color' +import useDarkMode from '@/modules/design-system/hooks/use-darkmode' +import { formatHex } from 'culori' +import { averageGeistChromaticity, averageGeistLuminance } from '../config' import { colorLevelAliases, colorLevels } from '../constants' import { useColorSystem } from '../global-state' import { formatColorConfig, parseColor } from '../lib/color-manipulation' -import type { ColorActionPayload, ColorConfig, ColorLevelKey } from '../types' +import type { ColorActionPayload, ColorConfig, ColorLevelKey, OklchColor } from '../types' import ColorScaleCreator from './color-scale-creator' import ColorScaleEditor from './color-scale-editor' import ColorScaleLegend from './color-scale-legend' export default function ColorSystemLab() { + const isDarkMode = useDarkMode() const [colorSystem, setColorSystem] = useColorSystem() const colors = colorSystem.colors @@ -35,15 +38,22 @@ export default function ColorSystemLab() { setColorSystem((draft) => { const colorIndex = draft.colors.findIndex((colorConfig) => colorConfig.name === color.name) const colorLevels = draft.colors[colorIndex].scale + draft.colors[colorIndex].name = color.newName ?? color.name for (const [level, levelData] of Object.entries(colorLevels)) { levelData.light = typeof levelData.light === 'string' ? parseColor(levelData.light) : levelData.light levelData.dark = typeof levelData.dark === 'string' ? parseColor(levelData.dark) : levelData.dark // console.log('changes:', { level, h: [levelData.light.h, levelData.dark.h, color.value.h] }) + levelData.light.l = averageGeistLuminance.light[level as ColorLevelKey] + levelData.light.c = (color.value.c + averageGeistChromaticity.light[level as ColorLevelKey]) / 2 levelData.light.h = color.value.h + + levelData.dark.l = averageGeistLuminance.dark[level as ColorLevelKey] + levelData.dark.c = (color.value.c + averageGeistChromaticity.dark[level as ColorLevelKey]) / 2 levelData.dark.h = color.value.h + draft.colors[colorIndex].name = color.newName ?? color.name draft.colors[colorIndex].scale[level as ColorLevelKey] = levelData } }) @@ -52,88 +62,43 @@ export default function ColorSystemLab() { function handleAddColor(color: ColorActionPayload) { setColorSystem((draft) => { const newColor: ColorConfig = { - name: color.name, + name: color.newName ?? color.name, scale: {}, defaultLevel: color.level, group: 'accent', aliases: color.alias ? [color.alias] : [], } - // @see https://www.npmjs.com/package/@effective/color - const spectrum = Object.entries( - buildSpectrum(color.value, { - outputSpace: 'oklch', - outputGamut: 'p3', - colorSteps: 5, - colorDifference: 8, - darkColorCompensation: 2, - }), - ) - - const spectrumColorScaleDark = [5, 4, 3, 2, 1, 7, 6, 0, 8, 9] - const spectrumColorScaleLight = Array.from(spectrumColorScaleDark).reverse() - - for (let i = 0; i < 10; i++) { - const levelKey = (i < 9 ? `${(i + 1) * 100}` : '950') as ColorLevelKey - const idx = spectrumColorScaleLight[i] - const idxDark = spectrumColorScaleDark[i] - const entry = spectrum[idx] - const entryDark = spectrum[idxDark] - - if (!entry || !entryDark) { - throw new Error('A color spectrum entry is undefined') - } - const [, color] = entry - const [, colorDark] = entryDark + let i = 0 + for (const colorLevel of colorLevels) { const levelAlias = colorLevelAliases[i] - newColor.scale[levelKey] = { - light: parseColor(color), - dark: parseColor(colorDark), + const lightColor: OklchColor = { + ...color.value, + l: averageGeistLuminance.light[colorLevel], + c: color.value.c > 0 ? (color.value.c + averageGeistChromaticity.light[colorLevel]) / 2 : 0, + } + + const darkColor: OklchColor = { + ...color.value, + l: averageGeistLuminance.dark[colorLevel], + c: color.value.c > 0 ? (color.value.c + averageGeistChromaticity.dark[colorLevel]) / 2 : 0, + } + + newColor.scale[colorLevel] = { + light: lightColor, + dark: darkColor, aliases: levelAlias ? [levelAlias] : [], } + i++ } - - // const spectrumIndexDark: Record = { - // 6: '100', - // 5: '200', - // 4: '300', - // 3: '400', - // 2: '500', - // 7: '600', - // 0: '700', - // 1: '800', - // 9: '900', - // 11: '950', - // } - - // const spectrumIndexLight: Record = { - // 6: '950', - // 5: '900', - // 4: '800', - // 3: '700', - // 2: '600', - // 7: '500', - // 0: '400', - // 1: '300', - // 9: '200', - // 11: '100', - // } - - console.log({ spectrum }) - - // console.log({ lightSpectrum, darkSpectrum }) draft.colors.push(newColor) }) } function handleDeleteColor(color: ColorActionPayload) { setColorSystem((draft) => { - const colorIndex = draft.colors.findIndex((colorConfig) => colorConfig.name === color.name) - if (colorIndex === -1) { - return - } - draft.colors.splice(colorIndex, 1) + draft.colors = draft.colors.filter((colorConfig) => colorConfig.name !== color.name) }) } @@ -142,14 +107,31 @@ export default function ColorSystemLab() { return ( - {colorSystem.colors.map((colorConfig) => ( - - ))} + {colorSystem.colors.map((colorConfig) => { + const baseColor = colorConfig.scale[600]?.[isDarkMode ? 'dark' : 'light'] + const baseColorPayload: ColorActionPayload | undefined = baseColor + ? { + name: colorConfig.name, + level: '600', + scheme: isDarkMode ? 'dark' : 'light', + value: parseColor(baseColor), + valueCss: formatHex(baseColor) ?? '#000', + alias: colorConfig.aliases?.[0], + } + : undefined + + return ( + + ) + })} diff --git a/modules/color-system/types.ts b/modules/color-system/types.ts index 2948a21..e046e6c 100644 --- a/modules/color-system/types.ts +++ b/modules/color-system/types.ts @@ -23,6 +23,7 @@ export type ColorConfig = { export type ColorActionPayload = { name: string + newName?: string level: ColorLevelKey scheme: ColorScheme value: OklchColor diff --git a/modules/design-system/components/button.tsx b/modules/design-system/components/button.tsx index 89d599f..a2590dc 100644 --- a/modules/design-system/components/button.tsx +++ b/modules/design-system/components/button.tsx @@ -186,17 +186,6 @@ export const PrimaryButtonSm = createVariantComponent(ark.button, buttonStyle, { // shadow: 'lg', size: 'sm', }) -export const AccentButton = createVariantComponent(ark.button, buttonStyle, { - variant: 'solid', - colorPalette: 'accent', - // shadow: 'lg', -}) -export const AccentButtonSm = createVariantComponent(ark.button, buttonStyle, { - variant: 'solid', - colorPalette: 'accent', - // shadow: 'lg', - size: 'sm', -}) export const DangerButton = createVariantComponent(ark.button, buttonStyle, { variant: 'solid', colorPalette: 'danger',