From 288278e336f33067c026f9760c1a25dd19239e8f Mon Sep 17 00:00:00 2001 From: Javi Aguilar <122741+itsjavi@users.noreply.github.com> Date: Thu, 11 Jul 2024 01:42:13 +0200 Subject: [PATCH] refactor(colors): scale generator closer to tints.dev with --- app/(components)/colors/_ui/client-state.ts | 31 ++- app/(components)/colors/_ui/color-scaler.ts | 57 ---- .../colors/_ui/palette-editor.tsx | 252 ++++++++++++++---- .../{generate-code.ts => preset-generator.ts} | 9 +- .../colors/_ui/scale-generator.ts | 93 +++++++ app/(components)/colors/page.tsx | 34 +-- 6 files changed, 331 insertions(+), 145 deletions(-) delete mode 100644 app/(components)/colors/_ui/color-scaler.ts rename app/(components)/colors/_ui/{generate-code.ts => preset-generator.ts} (88%) create mode 100644 app/(components)/colors/_ui/scale-generator.ts diff --git a/app/(components)/colors/_ui/client-state.ts b/app/(components)/colors/_ui/client-state.ts index a74ba79..35f0240 100644 --- a/app/(components)/colors/_ui/client-state.ts +++ b/app/(components)/colors/_ui/client-state.ts @@ -3,12 +3,13 @@ import type { Color, Hsl } from 'culori' import type { Draft } from 'immer' import { useAtom } from 'jotai' -import { atomWithImmer } from 'jotai-immer' +import { withImmer } from 'jotai-immer' +import { atomWithStorage } from 'jotai/utils' import { nanoid } from 'nanoid' -import { generateColorScale } from './color-scaler' +import { generateColorScale } from './scale-generator' const STORAGE_KEY = 'pandaColorSystem_v4' -const DEFAULT_MAX_STOPS = 10 +const DEFAULT_MAX_STOPS = 11 export type ColorData = Required export type ColorGroup = 'background' | 'foreground' | 'gray' | 'accent' | 'primary' | 'supporting' @@ -281,8 +282,8 @@ export const exampleColors = [ const initialState: ColorSystemState = { colors: exampleColors, } -const colorSystemAtom = atomWithImmer(initialState) -// const colorSystemAtom = withImmer(atomWithStorage(STORAGE_KEY, initialState)) +// const colorSystemAtom = atomWithImmer(initialState) +const colorSystemAtom = withImmer(atomWithStorage(STORAGE_KEY, initialState)) function _clearColors(draft: Draft) { draft.colors = [...initialState.colors] @@ -301,3 +302,23 @@ export function useColorSystem() { return [state, dispatch] as const } + +export function getColorStopFullId(colorName: string, index: number, maxStops: number): string { + return `${colorName}.${getColorStopShortId(index, maxStops)}` +} + +export function getColorStopShortId(index: number, maxStops: number): string { + if (index === 0) { + return '50' + } + + if (index === 10) { + return '950' + } + + if (index > 10) { + return `${index * 50 + 450}` + } + + return `${index * 100}` +} diff --git a/app/(components)/colors/_ui/color-scaler.ts b/app/(components)/colors/_ui/color-scaler.ts deleted file mode 100644 index a034f6a..0000000 --- a/app/(components)/colors/_ui/color-scaler.ts +++ /dev/null @@ -1,57 +0,0 @@ -import easing from 'bezier-easing' -import { type Hsl, interpolate, interpolatorPiecewise, lerp, samples } from 'culori' -import type { ColorSystemStateColorConfig } from './client-state' - -const bezierCurve = easing(1, 0.55, 0.6, 0.7) - -function piecewiseEasing() { - return interpolatorPiecewise((a, b, t) => lerp(a, b, bezierCurve(t))) -} - -const interpolator = { - mode: 'hsl', - h: piecewiseEasing(), - s: piecewiseEasing(), - l: piecewiseEasing(), - alpha: piecewiseEasing(), - // biome-ignore lint/suspicious/noExplicitAny: -} as any - -export function generateColorScale(color: ColorSystemStateColorConfig): Required[] { - const maxHueShift = color.hueShift * color.maxStops - const maxChromaShift = color.chromaShift * color.maxStops - - const startColor: Hsl = { - mode: 'hsl', - h: color.hue + maxHueShift, - s: (color.chroma + maxChromaShift) / 100, - l: color.luminanceMin / 100, - alpha: color.alpha / 100, - } - - const endColor: Hsl = { - mode: 'hsl', - h: color.hue, - s: color.chroma / 100, - l: color.luminanceMax / 100, - alpha: color.alpha / 100, - } - - return generateColorScaleFrom(startColor, endColor, color.maxStops) -} - -function generateColorScaleFrom(startColor: Hsl, endColor: Hsl, numStops: number): Required[] { - const colors = samples(numStops) - .map(interpolate([startColor, endColor], 'hsl', interpolator)) - .map((hsl): Required => { - return { - mode: 'hsl', - h: hsl.h || 0, - s: hsl.s, - l: hsl.l, - alpha: hsl.alpha || 1, - } - }) - - return colors -} diff --git a/app/(components)/colors/_ui/palette-editor.tsx b/app/(components)/colors/_ui/palette-editor.tsx index cf8639e..1f8f5fb 100644 --- a/app/(components)/colors/_ui/palette-editor.tsx +++ b/app/(components)/colors/_ui/palette-editor.tsx @@ -1,19 +1,51 @@ 'use client' +import { cn } from '@/lib/utils' import { GrayButton } from '@/modules/design-system/components/button' import { Input } from '@/modules/design-system/components/input' import { css } from '@/styled-system/css' -import { formatHex, formatHsl } from 'culori' -import { CodeIcon, TrashIcon } from 'lucide-react' +import { type Hsl, formatHex, formatHsl, wcagContrast } from 'culori' +import { CodeIcon, Edit3Icon, TrashIcon, XIcon } from 'lucide-react' import { nanoid } from 'nanoid' import { useState } from 'react' -import { type ColorSystemStateColorConfig, useColorSystem } from './client-state' -import { generatePandaColorsPreset, generatePandaPresetSnippet } from './generate-code' +import { type ColorSystemStateColorConfig, getColorStopFullId, useColorSystem } from './client-state' +import { generatePandaColorsPreset, generatePandaPresetSnippet } from './preset-generator' + +const checkerBoardBgClass = css({ + printColorAdjust: 'exact!', + backgroundSize: '100% 100%, 20px 20px', + backgroundImage: + 'linear-gradient(var(--bgcolor), var(--bgcolor)), repeating-conic-gradient(rgba(127,127,127,0.3) 0% 25%,transparent 0% 50%)', +}) + +function hasGoodContrast(color: Required): { + white: boolean + black: boolean +} { + const whiteContrast = wcagContrast({ mode: 'hsl', h: 0, s: 0, l: 1 }, color) + const blackContrast = wcagContrast({ mode: 'hsl', h: 0, s: 0, l: 0 }, color) + + return { + white: whiteContrast >= 4, + black: blackContrast >= 4, + } +} + +function getFgColor(color: Required): string { + const { white, black } = hasGoodContrast(color) + if (white) { + return '#fff' + } + if (black) { + return '#000' + } + return '#777' +} function ColorStop({ color, index }: { color: ColorSystemStateColorConfig; index: number }) { const stop = color.stops[index] if (!stop) return null - const stopName = `${color.name}.${index + 1}` + const stopName = getColorStopFullId(color.name, index, color.maxStops) const hsla = formatHsl(stop) return (
-
- {color.name} - {deletable && ( - { - if (window.confirm(`Are you sure you want to delete the '${color.name}' color?`)) - dispatch({ type: 'remove_color', payload: color.id }) - }} - > - - - )} -
+ const [isEditing, setIsEditing] = useState(false) + const editForm = ( + <>
+ + ) + const expandedPreviewer = ( +
+ {color.stops.map((_, index) => ( + + ))} +
+ ) + return ( +
- {color.stops.map((_, index) => ( - - ))} + {color.name} +
+ {isEditing && deletable && ( + { + if (window.confirm(`Are you sure you want to delete the '${color.name}' color?`)) + dispatch({ type: 'remove_color', payload: color.id }) + }} + > + + + )} + + { + setIsEditing(!isEditing) + }} + > + {isEditing ? : } + {isEditing ? 'Close' : 'Edit'} + +
+ + + {isEditing && editForm} + {isEditing && expandedPreviewer}
) } @@ -276,7 +328,7 @@ export function PaletteSwatches() { })} > {colorState.colors.map((color) => { - const midPoint = Math.floor(color.stops.length / 2) - 1 + const midPoint = Math.ceil(color.stops.length / 2) const stop = color.stops[midPoint] if (!stop) return null return ( @@ -290,6 +342,8 @@ export function PaletteSwatches() {