Skip to content

Commit

Permalink
feat(colors): add hue editor - wip
Browse files Browse the repository at this point in the history
  • Loading branch information
itsjavi committed May 31, 2024
1 parent 7968090 commit 57a173f
Show file tree
Hide file tree
Showing 15 changed files with 600 additions and 153 deletions.
7 changes: 2 additions & 5 deletions app/(components)/colors/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import ColorScaleViewer from '@/components/colorsystem/color-scale-viewer'
import ColorSystemEditor from '@/components/colorsystem/color-system-editor'
import PandaPresetRenderer from '@/components/colorsystem/panda-preset-renderer'
import ContentSection from '@/components/layout/content-section'
import Heading from '@/components/layout/heading'
import Subtitle from '@/components/layout/subtitle'
import { geistColorsConfig } from '@/lib/colorsystem/config'

export default async function () {
const solidColors = ['shade', 'gray', 'blue', 'red', 'yellow', 'green', 'teal', 'purple', 'pink'] as const
Expand All @@ -24,9 +23,7 @@ export default async function () {
supported browsers and displays. Default color is at the 600 level.
</Subtitle>
<br />
{Object.entries(geistColorsConfig).map(([name, colorConfig], i) => {
return <ColorScaleViewer withLegend={i === 0} key={name} colorConfig={colorConfig} />
})}
<ColorSystemEditor />
</ContentSection>
<ContentSection>
<Heading size="lg">Preset</Heading>
Expand Down
209 changes: 191 additions & 18 deletions components/colorsystem/color-scale-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,72 @@

import { monoFontClass } from '@/app/fonts'
import { bfgColorLevelToAlias, colorLevelToAlias, colorLevels } from '@/lib/colorsystem/constants'
import { parseColor } from '@/lib/colorsystem/create-color'
import type { ColorConfig, ColorLevel, ColorLevelAlias, ColorLevelBfg } from '@/lib/colorsystem/types'
import useDarkMode from '@/lib/hooks/use-darkmode'
import { cn } from '@/lib/utils'
import { css } from '@/styled-system/css'
import { Box, Stack, VStack } from '@/styled-system/jsx'
import { Popover } from '@ark-ui/react'
import { useState } from 'react'
import { useCopyToClipboard } from 'usehooks-ts'
import Heading from '../layout/heading'
import { InlineTextButton } from '../ui/button'

type ColorScaleViewerProps = {
withLegend?: boolean
colorConfig: ColorConfig
onHueChange?: (newHue: number, level: string, color: ColorConfig, isDarkMode: boolean) => void
}

export const popoverCss = {
positioner: css({
position: 'relative',
}),
content: css({
minWidth: '360px',
borderWidth: '1px',
borderStyle: 'solid',
borderColor: 'gray.border1',
background: 'bg.base',
borderRadius: 'sm',
boxShadow: 'lg',
display: 'flex',
flexDirection: 'column',
maxWidth: 'sm',
zIndex: 'popover',
p: '4',
_open: {
animation: 'fadeIn 0.25s ease-out',
},
_closed: {
animation: 'fadeOut 0.2s ease-out',
},
_hidden: {
display: 'none',
},
}),
title: css({
fontWeight: 'medium',
textStyle: 'sm',
}),
description: css({
color: 'fg.muted',
textStyle: 'sm',
}),
closeTrigger: css({
color: 'fg.muted',
}),
arrow: css({
'--arrow-size': 'var(--sizes-3)',
'--arrow-background': 'var(--colors-bg)',
zIndex: 'z1',
}),
arrowTip: css({
borderTopWidth: '1px',
borderLeftWidth: '1px',
borderColor: 'gray.border1',
}),
}

const gridCss = css({
Expand All @@ -37,18 +93,95 @@ const gridCss = css({
fontSize: 'xxs',
textAlign: 'center',
},
'& [data-level="600"], & [data-level="border3"]': {
'& [data-is-default], & [data-level="600"], & [data-level="border3"]': {
fontWeight: 'black',
fontSize: 'lg',
borderRadius: 'sm',
borderWidth: '2px',
borderWidth: '1.5px',
borderColor: 'fg.200',
},
})

function ColorCell({ level, colorConfig }: { colorConfig: ColorConfig; level?: ColorLevel | ColorLevelAlias }) {
type ColorEditorProps = {
isOpen: boolean
hue: number
onHueChange?: (newHue: number) => void
children: React.ReactNode
onClose?: () => void
}

function ColorEditor({ onHueChange, onClose, isOpen, children, hue }: ColorEditorProps) {
if (!isOpen) {
return children
}

return (
<Popover.Root
portalled
open={isOpen}
onOpenChange={(details) => {
if (!details.open) {
onClose?.()
}
}}
closeOnEscape={true}
closeOnInteractOutside={true}
>
<Popover.Trigger asChild>{children}</Popover.Trigger>
<Popover.Positioner className={popoverCss.positioner}>
<Popover.Content className={popoverCss.content}>
<Popover.Arrow className={popoverCss.arrow}>
<Popover.ArrowTip className={popoverCss.arrowTip} />
</Popover.Arrow>
<Stack gap="1">
<Popover.Title className={popoverCss.title}>OKCLH Color Editor</Popover.Title>
<Popover.Description className={popoverCss.description}>
<VStack gap="1">
<strong>Hue</strong>
<input
className={css({ width: 'full', display: 'inline-flex' })}
onChange={(e) => {
onHueChange?.(Number.parseFloat(e.target.value))
}}
type="range"
min="0"
max="360"
defaultValue={hue ?? 0}
step="0.01"
/>
</VStack>
</Popover.Description>
</Stack>
<Box position="absolute" top="1" right="1">
<Popover.CloseTrigger
asChild
className={popoverCss.closeTrigger}
onClick={() => {
// console.log('close....')
onClose?.()
}}
>
<InlineTextButton aria-label="Close Popover" variant="ghost" size="sm">
X
</InlineTextButton>
</Popover.CloseTrigger>
</Box>
</Popover.Content>
</Popover.Positioner>
</Popover.Root>
)
}

type ColorCellProps = {
colorConfig: ColorConfig
level?: ColorLevel | ColorLevelAlias
onHueChange?: (newHue: number, level: string, color: ColorConfig, isDarkMode: boolean) => void
}
function ColorCell({ level, colorConfig, onHueChange }: ColorCellProps) {
const [copiedText, copyToClipboard] = useCopyToClipboard()
const [isEditorOpen, setIsEditorOpen] = useState(false)
const [copied, setCopied] = useState(false)
const { isDarkMode } = useDarkMode()

const isBgFg = ['bg', 'fg'].includes(colorConfig.type)
const colorName = colorConfig.name
Expand All @@ -57,16 +190,18 @@ function ColorCell({ level, colorConfig }: { colorConfig: ColorConfig; level?: C
const levelAlias =
(isBgFg ? bfgColorLevelToAlias[levelValue as ColorLevelBfg] : colorLevelToAlias[levelValue as ColorLevel]) ??
levelValue
const token = level ? `${colorName}.${levelAlias}` : colorName
const firstAlias = colorConfig.aliases.length > 0 ? colorConfig.aliases[0] : colorName
const token = level ? `${firstAlias}.${levelAlias}` : colorName
const colorPreviewCssVars = {
'--bg': colorConfig.light[levelValue]?.srgb,
'--bg-p3': colorConfig.light[levelValue]?.oklch ?? colorConfig.light[levelValue]?.srgb,
'--bg-dark': colorConfig.dark[levelValue]?.srgb,
'--bg-dark-p3': colorConfig.dark[levelValue]?.oklch ?? colorConfig.dark[levelValue]?.srgb,
} as React.CSSProperties
}
const showCopiedText = copied && copiedText === token

const handleCopy = (text: string) => () => {
const handleBtnClick = (text: string) => () => {
setIsEditorOpen(!isEditorOpen)
copyToClipboard(text)
.then(() => {
setCopied(true)
Expand Down Expand Up @@ -137,26 +272,64 @@ function ColorCell({ level, colorConfig }: { colorConfig: ColorConfig; level?: C
monoFontClass,
)

const lightColorObj = parseColor(
colorConfig.light[levelValue]?.oklch ?? colorConfig.light[levelValue]?.srgb ?? 'transparent',
)

if (!lightColorObj) {
console.error('lightColorObj is undefined', { colorConfig })
throw new Error('lightColorObj is undefined')
}

const darkColorObj = parseColor(
colorConfig.dark[levelValue]?.oklch ?? colorConfig.dark[levelValue]?.srgb ?? 'transparent',
)

if (!darkColorObj) {
console.error('darkColorObj is undefined', { colorConfig })
throw new Error('darkColorObj is undefined')
}

const currentHue = isDarkMode ? darkColorObj.h : lightColorObj.h

return (
<button
type="button"
data-level={level}
className={classNames}
onClick={handleCopy(token)}
style={colorPreviewCssVars}
<ColorEditor
hue={currentHue ?? 0}
isOpen={isEditorOpen && defaultLevel === level}
onClose={() => setIsEditorOpen(false)}
onHueChange={(hue) => {
if (!onHueChange) {
return
}

onHueChange?.(hue, levelValue, colorConfig, isDarkMode)
}}
>
{showCopiedText === false && <span>{levelAlias}</span>}
{showCopiedText && <span data-copied>Copied!</span>}
</button>
<button
type="button"
data-level={level}
data-is-default={defaultLevel === level ? true : undefined}
className={classNames}
onClick={handleBtnClick(token)}
style={colorPreviewCssVars as React.CSSProperties}
data-color-light={colorPreviewCssVars['--bg']}
data-color-light-p3={colorPreviewCssVars['--bg-p3']}
data-color-dark={colorPreviewCssVars['--bg-dark']}
data-color-dark-p3={colorPreviewCssVars['--bg-dark-p3']}
>
{showCopiedText === false && <span>{levelAlias}</span>}
{showCopiedText && <span data-copied>Copied!</span>}
</button>
</ColorEditor>
)
}

export default function ColorScaleViewer({ colorConfig, withLegend }: ColorScaleViewerProps) {
export default function ColorScaleViewer({ colorConfig, onHueChange, withLegend }: ColorScaleViewerProps) {
const aliasesLegend = withLegend ? (
<div className={cn(gridCss, 'legend', monoFontClass)}>
<div />
{colorLevels.map((level) => (
<div className="label" key={level}>
<div className="label" key={level} data-level={level}>
{level}
</div>
))}
Expand All @@ -178,7 +351,7 @@ export default function ColorScaleViewer({ colorConfig, withLegend }: ColorScale
</Heading>
{/* <ColorCell colorName={name} label="DEFAULT" /> */}
{levelsSlice.map((level) => (
<ColorCell key={level} colorConfig={colorConfig} level={level} />
<ColorCell onHueChange={onHueChange} key={level} colorConfig={colorConfig} level={level} />
))}
</div>
</>
Expand Down
39 changes: 39 additions & 0 deletions components/colorsystem/color-system-editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use client'

import ColorScaleViewer from '@/components/colorsystem/color-scale-viewer'
import { generateColorSystemPresetCode } from '@/lib/colorsystem/generate-panda-preset'
import modifyPaletteHue from '@/lib/colorsystem/modify-palette-hue'
import { useAppState } from '@/lib/colorsystem/state'

export default function ColorSystemEditor() {
const [appState, setAppState] = useAppState()

return (
<>
{Object.entries(appState.colorSystemConfig).map(([name, colorConfig], i) => {
if (colorConfig.name === 'blue') {
// console.log(colorConfig.modifiedAt)
}
return (
<ColorScaleViewer
onHueChange={(newHue, level, cfg, isDarkMode) => {
// console.log({ newHue })
const newColorSystemConfig = {
...appState.colorSystemConfig,
[name]: modifyPaletteHue(newHue, colorConfig),
}
setAppState({
...appState,
colorSystemConfig: newColorSystemConfig,
colorSystemPresetCode: generateColorSystemPresetCode(newColorSystemConfig),
})
}}
withLegend={i === 0}
key={name}
colorConfig={colorConfig}
/>
)
})}
</>
)
}
22 changes: 6 additions & 16 deletions components/colorsystem/panda-preset-renderer.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
'use client'

import { monoFontClass } from '@/app/fonts'
import { geistColorsConfig } from '@/lib/colorsystem/config'
import generatePandaPreset from '@/lib/colorsystem/generate-panda-preset'
import { useAppState } from '@/lib/colorsystem/state'
import { css } from '@/styled-system/css'
import { outdent } from 'outdent'
import { useState } from 'react'
import { useCopyToClipboard } from 'usehooks-ts'
import { PrimaryButtonSm } from '../ui/button'

export default function PandaPresetRenderer() {
const [_, copyToClipboard] = useCopyToClipboard()
const [copied, setCopied] = useState(false)
const [appState] = useAppState()

const handleCopy = (text: string) => () => {
copyToClipboard(text)
Expand All @@ -26,27 +25,18 @@ export default function PandaPresetRenderer() {
})
}

// TODO: pass colors from global state
const configSubset = generatePandaPreset(geistColorsConfig)

const jsCode = outdent`
import { definePreset } from "@pandacss/dev";
export const colorSystemPreset = definePreset(
${JSON.stringify(configSubset, null, 2)}
);
`

return (
<>
<PrimaryButtonSm onClick={handleCopy(jsCode)}>{copied ? 'Copied!' : 'Copy to clipboard'}</PrimaryButtonSm>
<PrimaryButtonSm onClick={handleCopy(appState.colorSystemPresetCode)}>
{copied ? 'Copied!' : 'Copy to clipboard'}
</PrimaryButtonSm>
<div
className={css({
maxHeight: '500px',
overflowY: 'auto',
})}
>
<pre className={monoFontClass}>{jsCode}</pre>
<pre className={monoFontClass}>{appState.colorSystemPresetCode}</pre>
</div>
</>
)
Expand Down
Loading

0 comments on commit 57a173f

Please sign in to comment.