{
+ 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