Skip to content

Commit

Permalink
Add transform to style dictionary to get hovered color tokens (#2422)
Browse files Browse the repository at this point in the history
<!--
  How to write a good PR title:
- Follow [the Conventional Commits
specification](https://www.conventionalcommits.org/en/v1.0.0/).
  - Give as much context as necessary and as little as possible
  - Prefix it with [WIP] while it’s a work in progress
-->

## Self Checklist

- [x] I wrote a PR title in **English** and added an appropriate
**label** to the PR.
- [x] I wrote the commit message in **English** and to follow [**the
Conventional Commits
specification**](https://www.conventionalcommits.org/en/v1.0.0/).
- [x] I [added the
**changeset**](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md)
about the changes that needed to be released. (or didn't have to)
- [x] I wrote or updated **documentation** related to the changes. (or
didn't have to)
- [x] I wrote or updated **tests** related to the changes. (or didn't
have to)
- [x] I tested the changes in various browsers. (or didn't have to)
  - Windows: Chrome, Edge, (Optional) Firefox
  - macOS: Chrome, Edge, Safari, (Optional) Firefox

## Related Issue

<!-- Please link to issue if one exists -->

<!-- Fixes #0000 -->

- resolves #2413 

## Summary

<!-- Please brief explanation of the changes made -->

- alpha functional 토큰에 대해서 정해진 공식에 따라 hovered color 토큰을 만들기 위해 style
dictionary 에 transform 을 추가합니다.
- 스토리북 > Alpha-foundation > Color 에 hovered color 토큰을 추가합니다. 

## Details

<!-- Please elaborate description of the changes -->

- mergeCSS, mergeJsIndex 함수가 이미 있기 때문에, hovered color 토큰을 만들기 위해
builder를 하나 더 만들기만 하면 merge 함수가 알아서 파일을 합쳐줍니다.
- 변환 공식은 이슈를 참조해주세요. 추후에 바뀔 여지가 있습니다.

### Breaking change? (Yes/No)

<!-- If Yes, please describe the impact and migration path for users -->

-  No

## References

<!-- Please list any other resources or points the reviewer should be
aware of -->


- https://styledictionary.com/
  • Loading branch information
yangwooseong authored Sep 12, 2024
1 parent da327a3 commit 85bb261
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 61 deletions.
5 changes: 5 additions & 0 deletions .changeset/dull-falcons-compete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@channel.io/bezier-tokens': patch
---

Add hovered color tokens for alpha functional color.
115 changes: 64 additions & 51 deletions packages/bezier-react/src/stories/alpha-color.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Markdown, Meta } from '@storybook/blocks'
import { Markdown, Meta, DocsStory } from '@storybook/blocks'
import { tokens } from '@channel.io/bezier-tokens/alpha'
import { useLayoutEffect, useState } from 'react'

import {
LightThemeProvider,
Expand All @@ -9,60 +10,72 @@ import { HStack, VStack } from '~/src/components/Stack'

<Meta title="alpha-foundation/Color" />

export const Color = ({ name, value, reference }) => {
return (
<HStack spacing={20}>
<div
style={{
flex: 1,
boxShadow: value,
backgroundColor: `var(--${name})`,
height: 60,
}}
/>
<VStack
export const Color = ({ isHoveredColor, name, value, reference }) => {
const [isHovered, setIsHovered] = useState(false)
const color = isHovered ? `var(--${name}-hovered, var(--${name}))` : `var(--${name})`

return (

<HStack
spacing={10}
height={80}
align="center"
>
<div
style={{
flex: 1,
boxShadow: value,
backgroundColor: color,
height: '100%',
}}
onMouseEnter={() => {
setIsHovered(true)
}}
onMouseLeave={() => {
setIsHovered(false)
}}
/>
<VStack
style={{
flex: 1,
color: 'var(--alpha-color-fg-black-darkest)',
}}
spacing={4}
justify="center"
>
<span style={{ fontSize: 11 }}>{name}</span>
<pre
style={{
display: 'flex',
alignItems: 'center',
gap: 3,
fontSize: 9,
lineHeight: 1,
color: 'var(--alpha-color-fg-black-dark)',
}}
>
{isHoveredColor ? '' : reference ? 'var' : ''}
<pre
style={{
flex: 1,
color: 'var(--alpha-color-fg-black-darkest)',
display: 'inline-flex',
fontSize: 'inherit',
lineHeight: 'inherit',
padding: '1px 2px',
backgroundColor: 'var(--alpha-color-bg-grey-lighter)',
borderRadius: 3,
border: '1px solid var(--alpha-color-bg-black-lightest)',
}}
spacing={4}
justify="center"
>
<span style={{ fontSize: 11 }}>{name}</span>
<pre
style={{
display: 'flex',
alignItems: 'center',
gap: 3,
fontSize: 9,
lineHeight: 1,
color: 'var(--alpha-color-fg-black-dark)',
}}
>
{reference ? 'var' : ''}
<pre
style={{
display: 'inline-flex',
fontSize: 'inherit',
lineHeight: 'inherit',
padding: '1px 2px',
backgroundColor: 'var(--alpha-color-bg-grey-lighter)',
borderRadius: 3,
border: '1px solid var(--alpha-color-bg-black-lightest)',
}}
>
{reference ? reference : value}
</pre>
</pre>
</VStack>
</HStack>
)
}
{isHoveredColor ? value : reference ? reference : value}
</pre>
</pre>
</VStack>
</HStack>
)}

export const Primary = () => (
<HStack>

<VStack style={{ flex: 1, backgroundColor: 'var(--alpha-color-surface-normal)' }} padding={20}>
<VStack style={{ flex: 1, backgroundColor: 'var(--alpha-color-surface-normal)' }} padding={0}>
{Object.entries(tokens.global.color).map(([key, { value, ref }]) => (
<Color
key={key}
Expand All @@ -74,7 +87,7 @@ export const Primary = () => (
</VStack>

<LightThemeProvider>
<VStack style={{ flex: 1, backgroundColor: 'var(--alpha-color-surface-normal)' }} padding={20}>
<VStack style={{ flex: 1, backgroundColor: 'var(--alpha-color-surface-normal)' }} padding={0}>
{Object.entries(tokens.lightTheme.color).map(([key, { value, ref }]) => (
<Color
key={key}
Expand All @@ -87,7 +100,7 @@ export const Primary = () => (
</LightThemeProvider>

<DarkThemeProvider>
<VStack style={{ flex: 1, backgroundColor: 'var(--alpha-color-surface-normal)' }} padding={20}>
<VStack style={{ flex: 1, backgroundColor: 'var(--alpha-color-surface-normal)' }} padding={0}>
{Object.entries(tokens.darkTheme.color).map(([key, { value, ref }]) => (
<Color
key={key}
Expand Down
2 changes: 2 additions & 0 deletions packages/bezier-tokens/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,11 @@
"author": "Channel Corp.",
"license": "Apache-2.0",
"devDependencies": {
"@types/tinycolor2": "^1",
"eslint-config-bezier": "workspace:*",
"minimatch": "^9.0.3",
"style-dictionary": "^3.9.2",
"tinycolor2": "^1.6.0",
"tsconfig": "workspace:*"
}
}
68 changes: 60 additions & 8 deletions packages/bezier-tokens/scripts/build-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,19 @@ import {
customJsEsm,
} from './lib/format'
import { CSSTransforms } from './lib/transform'
import { isHoveredTransformName } from './lib/utils'
import { mergeCss } from './merge-css'

const CustomTransforms = [...Object.values(CSSTransforms)]
const CustomTransforms = [
...Object.values(CSSTransforms).filter(
({ name }) => !isHoveredTransformName(name)
),
]
const HoveredTransforms = [
...Object.values(CSSTransforms).filter(({ name }) =>
isHoveredTransformName(name)
),
]

const BUILD_PATH = {
BASE: 'dist',
Expand All @@ -39,13 +49,20 @@ const AlphaTokenBuilder = CustomTransforms.reduce(
.registerFormat(alphaCustomJsCjs)
.registerFormat(alphaCustomJsEsm)

function defineWebPlatform({ options, ...rest }: Platform): Platform {
const AlphaHoveredColorTokenBuilder = HoveredTransforms.reduce(
(builder, transform) => builder.registerTransform(transform),
StyleDictionary
)
.registerFormat(alphaCustomJsCjs)
.registerFormat(alphaCustomJsCjs)

function defineWebPlatform({
options,
transforms,
...rest
}: Platform): Platform {
return {
transforms: [
'attribute/cti',
'name/cti/kebab',
...CustomTransforms.map((transform) => transform.name),
],
transforms: ['attribute/cti', 'name/cti/kebab', ...(transforms ?? [])],
basePxFontSize: 10,
options: {
showFileHeader: false,
Expand All @@ -65,19 +82,25 @@ interface DefineConfigOptions {
reference?: string[]
basePath: string
destination: string
isForHovered?: boolean
options?: Options & {
cssSelector: string
}
}

function defineConfig({
useAlpha = false,
isForHovered = false,
source,
reference = [],
basePath,
destination,
options,
}: DefineConfigOptions): Config {
const transforms = isForHovered
? HoveredTransforms.map(({ name }) => name)
: CustomTransforms.map(({ name }) => name)

return {
source: [...source, ...reference],
platforms: {
Expand All @@ -91,6 +114,7 @@ function defineConfig({
source.some((src) => minimatch(filePath, src)),
},
],
transforms,
}),
'web/esm': defineWebPlatform({
buildPath: `${basePath}/${BUILD_PATH.ESM}/`,
Expand All @@ -102,6 +126,7 @@ function defineConfig({
source.some((src) => minimatch(filePath, src)),
},
],
transforms,
}),
'web/css': defineWebPlatform({
buildPath: `${basePath}/${BUILD_PATH.CSS}/`,
Expand All @@ -117,6 +142,7 @@ function defineConfig({
},
},
],
transforms,
}),
},
}
Expand Down Expand Up @@ -191,7 +217,33 @@ async function main() {
options: { cssSelector: '[data-bezier-theme="dark"]' },
})
),
].forEach((builder) => builder.buildAllPlatforms())
AlphaHoveredColorTokenBuilder.extend(
defineConfig({
useAlpha: true,
source: ['src/alpha/functional/dark-theme/*.json'],
reference: ['src/alpha/global/*.json'],
basePath: BUILD_PATH.BASE_ALPHA,
destination: 'darkThemeHovered',
options: { cssSelector: '[data-bezier-theme="dark"]' },
isForHovered: true,
})
),
AlphaHoveredColorTokenBuilder.extend(
defineConfig({
useAlpha: true,
source: ['src/alpha/functional/light-theme/*.json'],
reference: ['src/alpha/global/*.json'],
basePath: BUILD_PATH.BASE_ALPHA,
destination: 'lightThemeHovered',
options: {
cssSelector: ':where(:root, :host), [data-bezier-theme="light"]',
},
isForHovered: true,
})
),
].forEach((builder) => {
builder.buildAllPlatforms()
})

for (const buildPath of [
`${BUILD_PATH.BASE}/${BUILD_PATH.CSS}`,
Expand Down
1 change: 1 addition & 0 deletions packages/bezier-tokens/scripts/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const HOVERED = 'hovered'
83 changes: 82 additions & 1 deletion packages/bezier-tokens/scripts/lib/transform.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Named, Transform } from 'style-dictionary'
import tinycolor from 'tinycolor2'

import { extractNumber, toCSSDimension } from './utils'
import { HOVERED } from './constants'
import { clip, extractNumber, toCSSDimension } from './utils'

type CustomTransform = Named<Transform<unknown>>
type Transforms = Record<string, CustomTransform>
Expand Down Expand Up @@ -104,4 +106,83 @@ export const CSSTransforms = {
.map(({ color, position }) => `${color} ${position}`)
.join(', ')})`,
},
hoveredSuffix: {
name: `custom/css/${HOVERED}/namespace`,
type: 'name',
matcher: ({ type, filePath }) =>
filePath.startsWith('src/alpha') && type === 'color',
transformer: ({ name }) => {
return `alpha-${name}-${HOVERED}`
},
},
makeHoveredColor: {
name: `custom/css/${HOVERED}/functional-color`,
type: 'value',
transitive: true,
matcher: ({ type, filePath }) =>
type === 'color' && filePath.includes('functional'),
transformer: ({ value, filePath }) => {
function getHoveredColor(value: string, theme: 'dark' | 'light') {
const color = tinycolor(value)
const { h, s, l, a } = color.toHsl()

let alpha = a
let lightness = l
let saturation = s

if (a === 0) {
alpha = 0.1
} else if (a < 0.2) {
alpha = alpha * 1.5
}

if (theme === 'light') {
if (l <= 0.17) {
lightness = (l + 0.07) * 1.1
saturation += 0.05
} else {
lightness *= 0.93
saturation -= 0.03
}
} else {
if (l >= 0.83) {
lightness = (lightness - 0.2) * 0.98
saturation -= 0.03
} else {
lightness = (lightness + 0.04) * 1.005
saturation += 0.05
}
}

if (s <= 0.1 || s >= 0.9) {
saturation = s
}

const res = tinycolor.fromRatio({
h,
s: clip(saturation),
l: clip(lightness),
a: clip(alpha),
})

return res.toHex8String()
}

return filePath.includes('dark-theme')
? getHoveredColor(value, 'dark')
: getHoveredColor(value, 'light')
},
},
removeReference: {
name: `custom/css/${HOVERED}/remove-ref`,
type: 'attribute',
matcher: ({ type, filePath, name }) =>
filePath.startsWith('src/alpha') &&
type === 'color' &&
name.includes(`-${HOVERED}`),
transformer: (token) => {
token.original.value = null
return token
},
},
} satisfies Transforms
Loading

0 comments on commit 85bb261

Please sign in to comment.