Skip to content

Commit 6019956

Browse files
Merge pull request #116 from lukasoppermann/copilot/fix-98
feat: Support updated $duration object format
2 parents 91b9aa6 + d8c02c1 commit 6019956

File tree

9 files changed

+467
-5
lines changed

9 files changed

+467
-5
lines changed

src/filter/isDuration.test.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,35 @@ import {isDuration} from './isDuration'
33

44
describe('Filter: isDuration', () => {
55
const items = [
6+
// Old string format
67
{
78
value: '300ms',
89
$type: 'duration',
910
},
1011
{
11-
value: '2rem',
12-
$type: 'dimension',
12+
value: '10ms',
13+
type: 'duration',
1314
},
15+
// New object format
1416
{
15-
value: '10ms',
17+
$value: {value: 2, unit: 's'},
18+
$type: 'duration',
19+
},
20+
{
21+
value: {value: 500, unit: 'ms'},
1622
type: 'duration',
1723
},
24+
// Non-duration tokens
25+
{
26+
value: '2rem',
27+
$type: 'dimension',
28+
},
1829
{
1930
value: 'string',
2031
},
2132
] as TransformedToken[]
22-
it('filters duration tokens', () => {
23-
expect(items.filter(isDuration)).toStrictEqual([items[0], items[2]])
33+
34+
it('filters duration tokens (both old and new formats)', () => {
35+
expect(items.filter(isDuration)).toStrictEqual([items[0], items[1], items[2], items[3]])
2436
})
2537
})

src/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ describe('index.ts', () => {
2828
expect(StyleDictionary.hooks.transforms['dimension/pixelToRem']).toBeDefined()
2929
expect(StyleDictionary.hooks.transforms['dimension/remToPixel']).toBeDefined()
3030
expect(StyleDictionary.hooks.transforms['dimension/pixelUnitless']).toBeDefined()
31+
expect(StyleDictionary.hooks.transforms['duration/toCss']).toBeDefined()
3132
})
3233

3334
it('all transformGroups are attached', () => {

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {cubicBezierCss} from './transformer/cubic-bezier-css.js'
3333
import {dimensionPixelToRem} from './transformer/dimension-pixel-to-rem.js'
3434
import {dimensionRemToPixel} from './transformer/dimension-rem-to-pixel.js'
3535
import {dimensionToPixelUnitless} from './transformer/dimension-to-pixelUnitless.js'
36+
import {durationToCss} from './transformer/durationToCss.js'
3637
import {fontCss} from './transformer/font-css.js'
3738
import {fontFamilyCss} from './transformer/font-family-css.js'
3839
import {fontWeightToNumber} from './transformer/font-weight-to-number.js'
@@ -142,6 +143,10 @@ OrigialStyleDictionary.registerTransform({
142143
...dimensionToPixelUnitless,
143144
})
144145

146+
OrigialStyleDictionary.registerTransform({
147+
...durationToCss,
148+
})
149+
145150
OrigialStyleDictionary.registerTransform({
146151
...borderCss,
147152
})
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import {describe, expect, it, vi} from 'vitest'
2+
import {TransformedToken} from 'style-dictionary/types'
3+
import {durationToCss} from './durationToCss.js'
4+
5+
// Mock console.error to capture deprecation warnings
6+
const mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
7+
8+
describe('transformer: duration to CSS', () => {
9+
beforeEach(() => {
10+
mockConsoleError.mockClear()
11+
})
12+
13+
describe('durationToCss', () => {
14+
describe('filter', () => {
15+
it('should match duration tokens (new object format with ms)', () => {
16+
const token: TransformedToken = {
17+
name: 'animation.fast',
18+
$value: {value: 300, unit: 'ms'},
19+
$type: 'duration',
20+
path: ['animation', 'fast'],
21+
original: {$value: {value: 300, unit: 'ms'}, $type: 'duration'},
22+
}
23+
expect(durationToCss.filter(token)).toBe(true)
24+
})
25+
26+
it('should match duration tokens (new object format with s)', () => {
27+
const token: TransformedToken = {
28+
name: 'animation.medium',
29+
$value: {value: 2, unit: 's'},
30+
$type: 'duration',
31+
path: ['animation', 'medium'],
32+
original: {$value: {value: 2, unit: 's'}, $type: 'duration'},
33+
}
34+
expect(durationToCss.filter(token)).toBe(true)
35+
})
36+
37+
it('should match duration tokens (old string format with ms)', () => {
38+
const token: TransformedToken = {
39+
name: 'animation.slow',
40+
$value: '500ms',
41+
$type: 'duration',
42+
path: ['animation', 'slow'],
43+
original: {$value: '500ms', $type: 'duration'},
44+
}
45+
expect(durationToCss.filter(token)).toBe(true)
46+
})
47+
48+
it('should match duration tokens (old string format with s)', () => {
49+
const token: TransformedToken = {
50+
name: 'animation.long',
51+
$value: '3s',
52+
$type: 'duration',
53+
path: ['animation', 'long'],
54+
original: {$value: '3s', $type: 'duration'},
55+
}
56+
expect(durationToCss.filter(token)).toBe(true)
57+
})
58+
59+
it('should not match non-duration tokens', () => {
60+
const token: TransformedToken = {
61+
name: 'spacing.large',
62+
$value: '32px',
63+
$type: 'dimension',
64+
path: ['spacing', 'large'],
65+
original: {$value: '32px', $type: 'dimension'},
66+
}
67+
expect(durationToCss.filter(token)).toBe(false)
68+
})
69+
})
70+
71+
describe('transform', () => {
72+
it('should preserve ms unit (new object format)', () => {
73+
const token: TransformedToken = {
74+
name: 'animation.fast',
75+
$value: {value: 300, unit: 'ms'},
76+
$type: 'duration',
77+
path: ['animation', 'fast'],
78+
original: {$value: {value: 300, unit: 'ms'}, $type: 'duration'},
79+
}
80+
expect(durationToCss.transform(token, {})).toBe('300ms')
81+
expect(mockConsoleError).not.toHaveBeenCalled()
82+
})
83+
84+
it('should preserve s unit (new object format)', () => {
85+
const token: TransformedToken = {
86+
name: 'animation.medium',
87+
$value: {value: 2, unit: 's'},
88+
$type: 'duration',
89+
path: ['animation', 'medium'],
90+
original: {$value: {value: 2, unit: 's'}, $type: 'duration'},
91+
}
92+
expect(durationToCss.transform(token, {})).toBe('2s')
93+
expect(mockConsoleError).not.toHaveBeenCalled()
94+
})
95+
96+
it('should preserve ms unit (old string format with deprecation warning)', () => {
97+
const token: TransformedToken = {
98+
name: 'animation.slow',
99+
$value: '1000ms',
100+
$type: 'duration',
101+
path: ['animation', 'slow'],
102+
original: {$value: '1000ms', $type: 'duration'},
103+
}
104+
expect(durationToCss.transform(token, {})).toBe('1000ms')
105+
expect(mockConsoleError).toHaveBeenCalledWith(
106+
expect.stringContaining('DEPRECATED: Token "animation.slow" uses the old string format')
107+
)
108+
})
109+
110+
it('should preserve s unit (old string format with deprecation warning)', () => {
111+
const token: TransformedToken = {
112+
name: 'animation.long',
113+
$value: '3s',
114+
$type: 'duration',
115+
path: ['animation', 'long'],
116+
original: {$value: '3s', $type: 'duration'},
117+
}
118+
expect(durationToCss.transform(token, {})).toBe('3s')
119+
expect(mockConsoleError).toHaveBeenCalledWith(
120+
expect.stringContaining('DEPRECATED: Token "animation.long" uses the old string format')
121+
)
122+
})
123+
124+
it('should handle zero values consistently', () => {
125+
const tokenMs: TransformedToken = {
126+
name: 'animation.none',
127+
$value: {value: 0, unit: 'ms'},
128+
$type: 'duration',
129+
path: ['animation', 'none'],
130+
original: {$value: {value: 0, unit: 'ms'}, $type: 'duration'},
131+
}
132+
expect(durationToCss.transform(tokenMs, {})).toBe('0s')
133+
134+
const tokenS: TransformedToken = {
135+
name: 'animation.none2',
136+
$value: {value: 0, unit: 's'},
137+
$type: 'duration',
138+
path: ['animation', 'none2'],
139+
original: {$value: {value: 0, unit: 's'}, $type: 'duration'},
140+
}
141+
expect(durationToCss.transform(tokenS, {})).toBe('0s')
142+
})
143+
144+
it('should handle decimal values', () => {
145+
const token: TransformedToken = {
146+
name: 'animation.quick',
147+
$value: {value: 0.5, unit: 's'},
148+
$type: 'duration',
149+
path: ['animation', 'quick'],
150+
original: {$value: {value: 0.5, unit: 's'}, $type: 'duration'},
151+
}
152+
expect(durationToCss.transform(token, {})).toBe('0.5s')
153+
})
154+
155+
it('should throw error for invalid unit', () => {
156+
const token: TransformedToken = {
157+
name: 'animation.invalid',
158+
$value: {value: 2, unit: 'px'},
159+
$type: 'duration',
160+
path: ['animation', 'invalid'],
161+
original: {$value: {value: 2, unit: 'px'}, $type: 'duration'},
162+
}
163+
expect(() => durationToCss.transform(token, {})).toThrow(
164+
"Invalid unit for duration/toCss: 'animation.invalid' has unit 'px', expected 'ms' or 's'"
165+
)
166+
})
167+
})
168+
})
169+
})

src/transformer/durationToCss.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {Transform, TransformedToken} from 'style-dictionary/types'
2+
import {isDuration} from '../filter/isDuration.js'
3+
import {getDurationValueAndUnit} from '../utilities/durationUtils.js'
4+
5+
/**
6+
* durationToCss
7+
* @description convert duration tokens to CSS-compatible format, preserving original units
8+
*/
9+
export const durationToCss: Transform = {
10+
name: 'duration/toCss',
11+
type: `value`,
12+
transitive: true,
13+
filter: isDuration,
14+
transform: (token: TransformedToken) => {
15+
const {value, unit} = getDurationValueAndUnit(token)
16+
17+
// Validate that the unit is supported
18+
if (unit !== 'ms' && unit !== 's') {
19+
throw new Error(`Invalid unit for duration/toCss: '${token.name}' has unit '${unit}', expected 'ms' or 's'`)
20+
}
21+
22+
// Handle zero values - always return "0s" for consistency with CSS
23+
if (value === 0) {
24+
return '0s'
25+
}
26+
27+
// Return the value with its original unit preserved
28+
return `${value}${unit}`
29+
},
30+
}

0 commit comments

Comments
 (0)