Skip to content

Transition tokens transformer #1213

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/filters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export {isNumber} from './isNumber.js'
export {isShadow} from './isShadow.js'
export {isSource} from './isSource.js'
export {isTypography} from './isTypography.js'
export {isTransition} from './isTransition.js'
22 changes: 22 additions & 0 deletions src/filters/isTransition.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {getMockToken} from '../test-utilities/index.js'
import {isTransition} from './isTransition.js'

describe('Filter: isTransition', () => {
it('returns true if $type property is `transition`', () => {
expect(isTransition(getMockToken({$type: 'transition'}))).toStrictEqual(true)
})

it('returns false if $type property is not `transition`', () => {
expect(isTransition(getMockToken({$type: 'pumpkin'}))).toStrictEqual(false)
})

it('returns false if $type property is missing', () => {
expect(isTransition(getMockToken({alpha: 0.4}))).toStrictEqual(false)
})

it('returns false if $type property is falsy', () => {
expect(isTransition(getMockToken({$type: false}))).toStrictEqual(false)
expect(isTransition(getMockToken({$type: undefined}))).toStrictEqual(false)
expect(isTransition(getMockToken({$type: null}))).toStrictEqual(false)
})
})
10 changes: 10 additions & 0 deletions src/filters/isTransition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type {TransformedToken} from 'style-dictionary/types'

/**
* @description Checks if token is of $type `transition`
* @param token [TransformedToken](https://github.com/amzn/style-dictionary/blob/main/types/TransformedToken.d.ts)
* @returns boolean
*/
export const isTransition = (token: TransformedToken): boolean => {
return token.$type === 'transition'
}
1 change: 1 addition & 0 deletions src/platforms/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const css: PlatformInitializer = (outputFile, prefix, buildPath, options)
'shadow/css',
'border/css',
'typography/css',
'transition/css',
'fontFamily/css',
'fontWeight/number',
'gradient/css',
Expand Down
3 changes: 3 additions & 0 deletions src/primerStyleDictionary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
dimensionToRemPxArray,
floatToPixel,
floatToPixelUnitless,
transitionToCss,
} from './transformers/index.js'
import {
javascriptCommonJs,
Expand Down Expand Up @@ -152,6 +153,8 @@ PrimerStyleDictionary.registerTransform(borderToCss)

PrimerStyleDictionary.registerTransform(typographyToCss)

PrimerStyleDictionary.registerTransform(transitionToCss)

PrimerStyleDictionary.registerTransform(fontWeightToNumber)

PrimerStyleDictionary.registerTransform(fontFamilyToCss)
Expand Down
2 changes: 2 additions & 0 deletions src/schemas/designToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {shadowToken} from './shadowToken.js'
import {durationToken} from './durationToken.js'
import {cubicBezierToken} from './cubicBezierToken.js'
import {gradientToken} from './gradientToken.js'
import {transitionToken} from './transitionToken.js'

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: TODO: fix this
Expand All @@ -34,6 +35,7 @@ export const designToken = z.record(
numberToken,
durationToken,
stringToken,
transitionToken,
]),
designToken,
])
Expand Down
20 changes: 20 additions & 0 deletions src/schemas/transitionToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {z} from 'zod'
import {baseToken} from './baseToken.js'
import {referenceValue} from './referenceValue.js'
import {durationToken} from './durationToken.js'
import {cubicBezierToken} from './cubicBezierToken.js'
import {tokenType} from './tokenType.js'

export const transitionToken = baseToken
.extend({
$value: z.union([
z.object({
duration: z.union([durationToken.shape.$value, referenceValue]),
timingFunction: z.union([cubicBezierToken.shape.$value, referenceValue]),
delay: z.union([durationToken.shape.$value, referenceValue]).optional(),
}),
referenceValue,
]),
$type: tokenType('transition'),
})
.strict()
138 changes: 138 additions & 0 deletions src/schemas/transitionTokenSchema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import {transitionToken} from './transitionToken.js'

describe('Schema: transitionToken', () => {
const validToken = {
$value: {
duration: '300ms',
timingFunction: [0.4, 0, 0.2, 1],
delay: '0ms',
},
$type: 'transition',
$description: 'Standard transition',
}

it('parses valid transition tokens', () => {
expect(transitionToken.safeParse(validToken).success).toStrictEqual(true)
})

it('parses valid transition tokens without delay', () => {
const tokenWithoutDelay = {
$value: {
duration: '300ms',
timingFunction: [0.4, 0, 0.2, 1],
},
$type: 'transition',
$description: 'Standard transition without delay',
}
expect(transitionToken.safeParse(tokenWithoutDelay).success).toStrictEqual(true)
})

it('parses valid transition tokens with reference values', () => {
const tokenWithReferences = {
$value: {
duration: '{duration.medium}',
timingFunction: '{timing.easeInOut}',
delay: '{duration.small}',
},
$type: 'transition',
$description: 'Transition with reference values',
}
expect(transitionToken.safeParse(tokenWithReferences).success).toStrictEqual(true)
})

it('parses valid transition tokens with direct reference', () => {
const tokenWithDirectReference = {
$value: '{transition.standard}',
$type: 'transition',
$description: 'Transition with direct reference',
}
expect(transitionToken.safeParse(tokenWithDirectReference).success).toStrictEqual(true)
})

it('fails on invalid properties', () => {
// additional element
expect(
transitionToken.safeParse({
...validToken,
extra: 'value',
}).success,
).toStrictEqual(false)
// missing value
expect(
transitionToken.safeParse({
$type: 'transition',
}).success,
).toStrictEqual(false)
// missing type
expect(
transitionToken.safeParse({
$value: {
duration: '300ms',
timingFunction: [0.4, 0, 0.2, 1],
},
}).success,
).toStrictEqual(false)
})

it('fails on invalid duration value', () => {
expect(
transitionToken.safeParse({
...validToken,
$value: {
...validToken.$value,
duration: 'invalid',
},
}).success,
).toStrictEqual(false)
})

it('fails on invalid timing value', () => {
expect(
transitionToken.safeParse({
...validToken,
$value: {
...validToken.$value,
timingFunction: [0.4, 0, 0.2], // Missing one value
},
}).success,
).toStrictEqual(false)
})

it('fails on invalid delay value', () => {
expect(
transitionToken.safeParse({
...validToken,
$value: {
...validToken.$value,
delay: 'invalid',
},
}).success,
).toStrictEqual(false)
})

it('fails on wrong type', () => {
// invalid string
expect(
transitionToken.safeParse({
...validToken,
$type: 'motion',
}).success,
).toStrictEqual(false)
// undefined
expect(
transitionToken.safeParse({
...validToken,
$type: undefined,
}).success,
).toStrictEqual(false)
// no type
expect(
transitionToken.safeParse({
$value: {
duration: '300ms',
timingFunction: [0.4, 0, 0.2, 1],
},
}).success,
).toStrictEqual(false)
})
})
1 change: 1 addition & 0 deletions src/schemas/validTokenType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const validTypes = [
'gradient',
'number',
'string',
'transition',
'custom-viewportRange',
] as const

Expand Down
4 changes: 2 additions & 2 deletions src/test-utilities/getMockToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ export const getMockToken = (
[key: keyof TransformedToken]: unknown
},
options?: getMockTokenOptions,
) => {
): TransformedToken => {
return {
...removeProps(mockTokenDefaults, options?.remove),
...valueOverrides,
}
} as TransformedToken
}
20 changes: 12 additions & 8 deletions src/transformers/cubicBezierToCss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@ export const cubicBezierToCss: Transform = {
filter: isCubicBezier,
transform: (token: TransformedToken, _config: PlatformConfig) => {
const value = token.$value ?? token.value
// throw value of more or less than 4 items in array
if (value.length !== 4 || value.some((item: unknown) => typeof item !== 'number')) {
throw new Error(
`Invalid cubicBezier token ${token.path.join('.')}, must be an array with 4 numbers, but got this instead: ${JSON.stringify(value)}`,
)
}
// return value
return `cubic-bezier(${value.join(',')})`
return cubicBezierArrayToCss(value, token.path)
},
}

export const cubicBezierArrayToCss = (value: number[], path: string[]) => {
// throw value of more or less than 4 items in array
if (value.length !== 4 || value.some((item: unknown) => typeof item !== 'number')) {
throw new Error(
`Invalid cubicBezier token ${path.join('.')}, must be an array with 4 numbers, but got this instead: ${JSON.stringify(value)}`,
)
}
// return value
return `cubic-bezier(${value.join(',')})`
}
1 change: 1 addition & 0 deletions src/transformers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export {namePathToKebabCase} from './namePathToKebabCase.js'
export {namePathToSlashNotation} from './namePathToSlashNotation.js'
export {shadowToCss} from './shadowToCss.js'
export {typographyToCss} from './typographyToCss.js'
export {transitionToCss} from './transitionToCss.js'
85 changes: 85 additions & 0 deletions src/transformers/transitionToCss.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {getMockToken} from '../test-utilities/getMockToken.js'
import {transitionToCss} from './transitionToCss.js'

describe('transitionToCss', () => {
it('should transform basic transition token to CSS', () => {
const token = getMockToken({
$value: {
duration: '300ms',
timingFunction: [0.4, 0, 0.2, 1],
},
$type: 'transition',
})

expect(transitionToCss.transform(token, {}, {})).toBe('300ms cubic-bezier(0.4,0,0.2,1)')
})

it('should transform transition token with delay to CSS', () => {
const token = getMockToken({
$value: {
duration: '300ms',
timingFunction: [0.4, 0, 0.2, 1],
delay: '100ms',
},
$type: 'transition',
})

expect(transitionToCss.transform(token, {}, {})).toBe('300ms cubic-bezier(0.4,0,0.2,1) 100ms')
})

it('should transform transition token with resolved references', () => {
const token = {
$value: {
duration: '300ms',
timingFunction: [0.4, 0, 0.2, 1],
delay: '0ms',
},
$type: 'transition',
}
const resolvedRefs = {
duration: '300ms',
timingFunction: [0.4, 0, 0.2, 1],
delay: '0ms',
}

expect(transitionToCss.transform(token, resolvedRefs)).toBe('300ms cubic-bezier(0.4,0,0.2,1) 0ms')
})

it('should handle missing optional delay', () => {
const token = getMockToken({
$value: {
duration: '300ms',
timingFunction: [0.4, 0, 0.2, 1],
},
$type: 'transition',
})

expect(transitionToCss.transform(token, {}, {})).toBe('300ms cubic-bezier(0.4,0,0.2,1)')
})

it('should throw error for missing required properties', () => {
const token = getMockToken({
$value: {
duration: '300ms',
},
$type: 'transition',
})
expect(() => transitionToCss.transform(token, {}, {})).toThrow(
'Missing property: timingFunction on token with value {"duration":"300ms"}',
)
})

it('should throw error for invalid timing value', () => {
const token = getMockToken({
$value: {
duration: '300ms',
timingFunction: [0.4, 0, 0.2], // Missing one value
},
$type: 'transition',
})

expect(() => transitionToCss.transform(token, {}, {})).toThrow(
'Invalid cubicBezier token path, must be an array with 4 numbers, but got this instead: [0.4,0,0.2]',
)
})
})
Loading