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',