diff --git a/.changeset/dull-falcons-compete.md b/.changeset/dull-falcons-compete.md new file mode 100644 index 0000000000..6c670d0666 --- /dev/null +++ b/.changeset/dull-falcons-compete.md @@ -0,0 +1,5 @@ +--- +'@channel.io/bezier-tokens': patch +--- + +Add hovered color tokens for alpha functional color. diff --git a/packages/bezier-react/src/stories/alpha-color.mdx b/packages/bezier-react/src/stories/alpha-color.mdx index b09e8a6dfc..6a6eeedfa8 100644 --- a/packages/bezier-react/src/stories/alpha-color.mdx +++ b/packages/bezier-react/src/stories/alpha-color.mdx @@ -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, @@ -9,60 +10,72 @@ import { HStack, VStack } from '~/src/components/Stack' -export const Color = ({ name, value, reference }) => { - return ( - -
- { + const [isHovered, setIsHovered] = useState(false) + const color = isHovered ? `var(--${name}-hovered, var(--${name}))` : `var(--${name})` + +return ( + + +
{ + setIsHovered(true) + }} + onMouseLeave={() => { + setIsHovered(false) + }} + /> + + {name} +
+      {isHoveredColor ? '' : reference ? 'var' : ''}
+      
-        {name}
-        
-          {reference ? 'var' : ''}
-          
-            {reference ? reference : value}
-          
-
- - - ) -} + {isHoveredColor ? value : reference ? reference : value} +
+
+
+ +)} export const Primary = () => ( - - + {Object.entries(tokens.global.color).map(([key, { value, ref }]) => ( ( - + {Object.entries(tokens.lightTheme.color).map(([key, { value, ref }]) => ( ( - + {Object.entries(tokens.darkTheme.color).map(([key, { value, ref }]) => ( !isHoveredTransformName(name) + ), +] +const HoveredTransforms = [ + ...Object.values(CSSTransforms).filter(({ name }) => + isHoveredTransformName(name) + ), +] const BUILD_PATH = { BASE: 'dist', @@ -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, @@ -65,6 +82,7 @@ interface DefineConfigOptions { reference?: string[] basePath: string destination: string + isForHovered?: boolean options?: Options & { cssSelector: string } @@ -72,12 +90,17 @@ interface DefineConfigOptions { 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: { @@ -91,6 +114,7 @@ function defineConfig({ source.some((src) => minimatch(filePath, src)), }, ], + transforms, }), 'web/esm': defineWebPlatform({ buildPath: `${basePath}/${BUILD_PATH.ESM}/`, @@ -102,6 +126,7 @@ function defineConfig({ source.some((src) => minimatch(filePath, src)), }, ], + transforms, }), 'web/css': defineWebPlatform({ buildPath: `${basePath}/${BUILD_PATH.CSS}/`, @@ -117,6 +142,7 @@ function defineConfig({ }, }, ], + transforms, }), }, } @@ -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}`, diff --git a/packages/bezier-tokens/scripts/lib/constants.ts b/packages/bezier-tokens/scripts/lib/constants.ts new file mode 100644 index 0000000000..6dec2d401f --- /dev/null +++ b/packages/bezier-tokens/scripts/lib/constants.ts @@ -0,0 +1 @@ +export const HOVERED = 'hovered' diff --git a/packages/bezier-tokens/scripts/lib/transform.ts b/packages/bezier-tokens/scripts/lib/transform.ts index f33d7c9fe1..46409c4ace 100644 --- a/packages/bezier-tokens/scripts/lib/transform.ts +++ b/packages/bezier-tokens/scripts/lib/transform.ts @@ -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> type Transforms = Record @@ -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 diff --git a/packages/bezier-tokens/scripts/lib/utils.ts b/packages/bezier-tokens/scripts/lib/utils.ts index d728b0ec2a..5727e8cfda 100644 --- a/packages/bezier-tokens/scripts/lib/utils.ts +++ b/packages/bezier-tokens/scripts/lib/utils.ts @@ -1,3 +1,5 @@ +import { HOVERED } from './constants' + export const toCamelCase = (str: string) => str .toLowerCase() @@ -8,3 +10,7 @@ export const extractNumber = (str: string) => export const toCSSDimension = (value: string) => /^0[a-zA-Z]+$/.test(value) ? 0 : value + +export const clip = (value: number) => Math.min(Math.max(value, 0), 1) + +export const isHoveredTransformName = (name: string) => name.includes(HOVERED) diff --git a/yarn.lock b/yarn.lock index 31c1a01ed8..0aca53141f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2211,9 +2211,11 @@ __metadata: version: 0.0.0-use.local resolution: "@channel.io/bezier-tokens@workspace:packages/bezier-tokens" dependencies: + "@types/tinycolor2": "npm:^1" eslint-config-bezier: "workspace:*" minimatch: "npm:^9.0.3" style-dictionary: "npm:^3.9.2" + tinycolor2: "npm:^1.6.0" tsconfig: "workspace:*" languageName: unknown linkType: soft @@ -6410,6 +6412,13 @@ __metadata: languageName: node linkType: hard +"@types/tinycolor2@npm:^1": + version: 1.4.6 + resolution: "@types/tinycolor2@npm:1.4.6" + checksum: 10/a662fd177135d27779b7ccba39881a85167f969787c71c7bf51903d288a519ad5b9526e0a4af7bde6444df6b7abe88bae893d569c6d804108c03dfec9509bdf6 + languageName: node + linkType: hard + "@types/tough-cookie@npm:*": version: 4.0.5 resolution: "@types/tough-cookie@npm:4.0.5" @@ -18584,7 +18593,7 @@ __metadata: languageName: node linkType: hard -"tinycolor2@npm:^1.4.1": +"tinycolor2@npm:^1.4.1, tinycolor2@npm:^1.6.0": version: 1.6.0 resolution: "tinycolor2@npm:1.6.0" checksum: 10/066c3acf4f82b81c58a0d3ab85f49407efe95ba87afc3c7a16b1d77625193dfbe10dd46c26d0a263c1137361dd5a6a68bff2fb71def5fb9b9aec940fb030bcd4