Skip to content
Open
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
10 changes: 10 additions & 0 deletions .changeset/kdzg-dyju-ilzm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@primer/primitives': patch
---

Update shadow tokens to use W3C DTCG object format for dimension values

- Shadow token dimension properties (`offsetX`, `offsetY`, `blur`, `spread`) now use object format `{ value: number, unit: "px" }` instead of legacy strings like `"1px"`
- Updated `shadowToCss` transformer to handle W3C dimension objects
- Updated `ShadowTokenValue` type to require `DimensionTokenValue` for dimension properties
- Legacy string format for shadow dimensions is no longer supported
10 changes: 9 additions & 1 deletion src/formats/jsonFigma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,18 @@ const shadowToVariables = (
values: Omit<ShadowTokenValue, 'color'> & {color: string | RgbaFloat},
token: TransformedToken,
) => {
// Helper to extract numeric value from W3C dimension object
const getDimensionValue = (dim: ShadowTokenValue['offsetX']): number => {
if (typeof dim === 'object' && 'value' in dim) {
return dim.value
}
throw new Error(`Invalid shadow dimension: expected W3C object format, got ${JSON.stringify(dim)}`)
}

// floatValue
const floatValue = (property: 'offsetX' | 'offsetY' | 'blur' | 'spread') => ({
name: `${name}/${property}`,
value: parseInt(values[property].replace('px', '')),
value: getDimensionValue(values[property]),
type: 'FLOAT',
scopes: ['EFFECT_FLOAT'],
mode,
Expand Down
9 changes: 6 additions & 3 deletions src/schemas/borderTokenSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {borderToken, borderValue} from './borderToken.js'
const validBorderValue = {
color: '#333',
style: 'solid',
width: '1px',
width: {value: 1, unit: 'px'},
}

describe('Schema: borderValue', () => {
Expand All @@ -22,11 +22,14 @@ describe('Schema: borderValue', () => {
expect(borderValue.safeParse({...validBorderValue, style: 'inset'}).success).toStrictEqual(true)
})

it('fails on legacy string format', () => {
expect(borderValue.safeParse({...validBorderValue, width: '1px'}).success).toStrictEqual(false)
})

it('fails on invalid values', () => {
expect(borderValue.safeParse({...validBorderValue, style: 'none'}).success).toStrictEqual(false)
expect(borderValue.safeParse({...validBorderValue, color: 'none'}).success).toStrictEqual(false)
expect(borderValue.safeParse({...validBorderValue, width: '1%'}).success).toStrictEqual(false)
expect(borderValue.safeParse({...validBorderValue, width: '1vw'}).success).toStrictEqual(false)
expect(borderValue.safeParse({...validBorderValue, width: {value: 1, unit: '%'}}).success).toStrictEqual(false)
})

it('fails on missing values', () => {
Expand Down
16 changes: 11 additions & 5 deletions src/schemas/dimensionTokenSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,30 @@ import {dimensionToken} from './dimensionToken.js'

describe('Schema: dimensionToken', () => {
const validToken = {
$value: '1px',
$value: {value: 1, unit: 'px'},
$type: 'dimension',
$description: 'a dimension token',
}

it('passes on valid values', () => {
expect(dimensionToken.safeParse(validToken).success).toStrictEqual(true)
expect(dimensionToken.safeParse({...validToken, $value: '1em'}).success).toStrictEqual(true)
expect(dimensionToken.safeParse({...validToken, $value: '1rem'}).success).toStrictEqual(true)
expect(dimensionToken.safeParse({...validToken, $value: {value: 1, unit: 'em'}}).success).toStrictEqual(true)
expect(dimensionToken.safeParse({...validToken, $value: {value: 1, unit: 'rem'}}).success).toStrictEqual(true)
})

it('fails on legacy string format', () => {
expect(dimensionToken.safeParse({...validToken, $value: '1px'}).success).toStrictEqual(false)
expect(dimensionToken.safeParse({...validToken, $value: '1em'}).success).toStrictEqual(false)
expect(dimensionToken.safeParse({...validToken, $value: '1rem'}).success).toStrictEqual(false)
})

it('fails on invalid type', () => {
expect(dimensionToken.safeParse({...validToken, $type: 'stroke'}).success).toStrictEqual(false)
})

it('fails on invalid value', () => {
expect(dimensionToken.safeParse({...validToken, $value: 'wrong'}).success).toStrictEqual(false)
expect(dimensionToken.safeParse({...validToken, $value: '1%'}).success).toStrictEqual(false)
expect(dimensionToken.safeParse({...validToken, $value: {value: 1, unit: 'pt'}}).success).toStrictEqual(false)
expect(dimensionToken.safeParse({...validToken, $value: {value: 1, unit: '%'}}).success).toStrictEqual(false)
expect(dimensionToken.safeParse({...validToken, $value: undefined}).success).toStrictEqual(false)
expect(dimensionToken.safeParse({...validToken, $value: ''}).success).toStrictEqual(false)
expect(dimensionToken.safeParse({...validToken, $value: false}).success).toStrictEqual(false)
Expand Down
29 changes: 1 addition & 28 deletions src/schemas/dimensionValue.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,12 @@
import {z} from 'zod'
import {schemaErrorMessage} from '../utilities/index.js'

/**
* W3C DTCG dimension value format
* @link https://www.designtokens.org/tr/drafts/format/#dimension
*/
export const dimensionValueObject = z
export const dimensionValue = z
.object({
value: z.number(),
unit: z.enum(['px', 'rem', 'em']),
})
.strict()

/**
* Legacy dimension value format (string with unit)
* @deprecated Use W3C DTCG object format instead
*/
const dimensionValueLegacy = z.union([
z.string().superRefine((dim, ctx) => {
if (!/(^-?[0-9]+\.?[0-9]*(px|rem)$|^-?[0-9]+\.?[0-9]*em$)/.test(dim)) {
ctx.addIssue({
code: 'custom',
message: schemaErrorMessage(
`Invalid dimension: "${dim}"`,
`Dimension must be a string with a unit (px, rem or em) or 0`,
),
})
}
}),
z.literal('0'),
z.literal(0),
])

/**
* Dimension value - supports both W3C DTCG object format and legacy string format
* Note: Transformers only accept W3C format, but schema validates both for migration period
*/
export const dimensionValue = z.union([dimensionValueObject, dimensionValueLegacy])
44 changes: 11 additions & 33 deletions src/schemas/dimensionValueSchema.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {dimensionValue, dimensionValueObject} from './dimensionValue.js'
import {dimensionValue} from './dimensionValue.js'

describe('Schema: dimensionValue', () => {
describe('W3C DTCG object format', () => {
Expand All @@ -17,45 +17,23 @@ describe('Schema: dimensionValue', () => {
expect(dimensionValue.safeParse({value: 16}).success).toStrictEqual(false)
expect(dimensionValue.safeParse({unit: 'px'}).success).toStrictEqual(false)
})
})

describe('dimensionValueObject (strict W3C format)', () => {
it('passes on valid W3C dimension objects', () => {
expect(dimensionValueObject.safeParse({value: 16, unit: 'px'}).success).toStrictEqual(true)
expect(dimensionValueObject.safeParse({value: 1, unit: 'rem'}).success).toStrictEqual(true)
expect(dimensionValueObject.safeParse({value: 0.9285, unit: 'em'}).success).toStrictEqual(true)
})

it('fails on extra properties', () => {
expect(dimensionValueObject.safeParse({value: 16, unit: 'px', extra: true}).success).toStrictEqual(false)
expect(dimensionValue.safeParse({value: 16, unit: 'px', extra: true}).success).toStrictEqual(false)
})
})

describe('legacy string format (deprecated)', () => {
it('passes on valid values', () => {
expect(dimensionValue.safeParse('1px').success).toStrictEqual(true)
expect(dimensionValue.safeParse('-1px').success).toStrictEqual(true)
expect(dimensionValue.safeParse('1em').success).toStrictEqual(true)
expect(dimensionValue.safeParse('1rem').success).toStrictEqual(true)
expect(dimensionValue.safeParse('0').success).toStrictEqual(true)
expect(dimensionValue.safeParse(0).success).toStrictEqual(true)
})

it('passes on decimal rem values', () => {
expect(dimensionValue.safeParse('0.75rem').success).toStrictEqual(true)
expect(dimensionValue.safeParse('0.875rem').success).toStrictEqual(true)
expect(dimensionValue.safeParse('1.25rem').success).toStrictEqual(true)
expect(dimensionValue.safeParse('2.5rem').success).toStrictEqual(true)
expect(dimensionValue.safeParse('-0.5rem').success).toStrictEqual(true)
})

it('passes on decimal px values', () => {
expect(dimensionValue.safeParse('2.5px').success).toStrictEqual(true)
expect(dimensionValue.safeParse('0.5px').success).toStrictEqual(true)
expect(dimensionValue.safeParse('-1.5px').success).toStrictEqual(true)
describe('legacy string format (rejected)', () => {
it('fails on legacy string values', () => {
expect(dimensionValue.safeParse('1px').success).toStrictEqual(false)
expect(dimensionValue.safeParse('-1px').success).toStrictEqual(false)
expect(dimensionValue.safeParse('1em').success).toStrictEqual(false)
expect(dimensionValue.safeParse('1rem').success).toStrictEqual(false)
expect(dimensionValue.safeParse('0').success).toStrictEqual(false)
expect(dimensionValue.safeParse(0).success).toStrictEqual(false)
})

it('fails on invalid value', () => {
it('fails on invalid value types', () => {
expect(dimensionValue.safeParse('1%').success).toStrictEqual(false)
expect(dimensionValue.safeParse(1).success).toStrictEqual(false)
expect(dimensionValue.safeParse('small').success).toStrictEqual(false)
Expand Down
97 changes: 71 additions & 26 deletions src/schemas/shadowTokenSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import {shadowValue, shadowToken} from './shadowToken.js'
const tokenValue = {
color: '#000000',
alpha: 0.5,
offsetX: '4px',
offsetY: '4px',
blur: '2px',
spread: '2px',
offsetX: {value: 4, unit: 'px'},
offsetY: {value: 4, unit: 'px'},
blur: {value: 2, unit: 'px'},
spread: {value: 2, unit: 'px'},
inset: false,
}

Expand All @@ -15,11 +15,56 @@ describe('Schema: shadowValue', () => {
expect(shadowValue.safeParse(tokenValue).success).toStrictEqual(true)
// without inset
expect(
shadowValue.safeParse({color: '#000000', alpha: 0.5, offsetX: '4px', offsetY: '4px', blur: '2px', spread: '2px'})
.success,
shadowValue.safeParse({
color: '#000000',
alpha: 0.5,
offsetX: {value: 4, unit: 'px'},
offsetY: {value: 4, unit: 'px'},
blur: {value: 2, unit: 'px'},
spread: {value: 2, unit: 'px'},
}).success,
).toStrictEqual(true)
})

it('parses shadow with zero values', () => {
expect(
shadowValue.safeParse({
color: '#000000',
alpha: 0.5,
offsetX: {value: 0, unit: 'px'},
offsetY: {value: 0, unit: 'px'},
blur: {value: 0, unit: 'px'},
spread: {value: 0, unit: 'px'},
}).success,
).toStrictEqual(true)
})

it('parses shadow with negative values', () => {
expect(
shadowValue.safeParse({
color: '#000000',
alpha: 0.5,
offsetX: {value: -4, unit: 'px'},
offsetY: {value: -4, unit: 'px'},
blur: {value: 2, unit: 'px'},
spread: {value: -2, unit: 'px'},
}).success,
).toStrictEqual(true)
})

it('fails on legacy string format', () => {
expect(
shadowValue.safeParse({
color: '#000000',
alpha: 0.5,
offsetX: '4px',
offsetY: '4px',
blur: '2px',
spread: '2px',
}).success,
).toStrictEqual(false)
})

it('fails on invalid properties', () => {
// additional element
expect(
Expand All @@ -34,55 +79,55 @@ describe('Schema: shadowValue', () => {
expect(
shadowValue.safeParse({
alpha: 0.5,
offsetX: '4px',
offsetY: '4px',
blur: '2px',
spread: '2px',
offsetX: {value: 4, unit: 'px'},
offsetY: {value: 4, unit: 'px'},
blur: {value: 2, unit: 'px'},
spread: {value: 2, unit: 'px'},
}).success,
).toStrictEqual(false)
expect(
shadowValue.safeParse({
color: '#000000',
offsetX: '4px',
offsetY: '4px',
blur: '2px',
spread: '2px',
offsetX: {value: 4, unit: 'px'},
offsetY: {value: 4, unit: 'px'},
blur: {value: 2, unit: 'px'},
spread: {value: 2, unit: 'px'},
}).success,
).toStrictEqual(false)
expect(
shadowValue.safeParse({
color: '#000000',
alpha: 0.5,
offsetY: '4px',
blur: '2px',
spread: '2px',
offsetY: {value: 4, unit: 'px'},
blur: {value: 2, unit: 'px'},
spread: {value: 2, unit: 'px'},
}).success,
).toStrictEqual(false)
expect(
shadowValue.safeParse({
color: '#000000',
alpha: 0.5,
offsetY: '4px',
blur: '2px',
spread: '2px',
offsetX: {value: 4, unit: 'px'},
blur: {value: 2, unit: 'px'},
spread: {value: 2, unit: 'px'},
}).success,
).toStrictEqual(false)
expect(
shadowValue.safeParse({
color: '#000000',
alpha: 0.5,
offsetX: '4px',
offsetY: '4px',
spread: '2px',
offsetX: {value: 4, unit: 'px'},
offsetY: {value: 4, unit: 'px'},
spread: {value: 2, unit: 'px'},
}).success,
).toStrictEqual(false)
expect(
shadowValue.safeParse({
color: '#000000',
alpha: 0.5,
offsetX: '4px',
offsetY: '4px',
blur: '2px',
offsetX: {value: 4, unit: 'px'},
offsetY: {value: 4, unit: 'px'},
blur: {value: 2, unit: 'px'},
}).success,
).toStrictEqual(false)
})
Expand Down
13 changes: 9 additions & 4 deletions src/schemas/typographyTokenSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import {typographyToken, typographyValue} from './typographyToken.js'

describe('Schema: typographyToken', () => {
const validValue = {
fontSize: '16px',
lineHeight: '24px',
fontSize: {value: 16, unit: 'px'},
lineHeight: {value: 24, unit: 'px'},
fontWeight: 600,
fontFamily: 'Helvetica',
}
Expand All @@ -24,21 +24,26 @@ describe('Schema: typographyToken', () => {
expect(typographyValue.safeParse({...validValue, lineHeight: undefined}).success).toStrictEqual(true)
})

it('fails on legacy string format', () => {
expect(typographyValue.safeParse({...validValue, fontSize: '16px'}).success).toStrictEqual(false)
expect(typographyValue.safeParse({...validValue, lineHeight: '24px'}).success).toStrictEqual(false)
})

it('it fails on missing property', () => {
expect(typographyValue.safeParse({...validValue, fontSize: undefined}).success).toStrictEqual(false)
expect(typographyValue.safeParse({...validValue, fontWeight: undefined}).success).toStrictEqual(false)
expect(typographyValue.safeParse({...validValue, fontFamily: undefined}).success).toStrictEqual(false)
})

it('it fails on invalid fontSize values', () => {
expect(typographyValue.safeParse({...validValue, fontSize: '100%'}).success).toStrictEqual(false)
expect(typographyValue.safeParse({...validValue, fontSize: {value: 100, unit: '%'}}).success).toStrictEqual(false)
expect(typographyValue.safeParse({...validValue, fontSize: '100'}).success).toStrictEqual(false)
expect(typographyValue.safeParse({...validValue, fontSize: ''}).success).toStrictEqual(false)
expect(typographyValue.safeParse({...validValue, fontSize: 10}).success).toStrictEqual(false)
})

it('it fails on invalid lineHeight values', () => {
expect(typographyValue.safeParse({...validValue, lineHeight: '100%'}).success).toStrictEqual(false)
expect(typographyValue.safeParse({...validValue, lineHeight: {value: 100, unit: '%'}}).success).toStrictEqual(false)
expect(typographyValue.safeParse({...validValue, lineHeight: '100'}).success).toStrictEqual(false)
expect(typographyValue.safeParse({...validValue, lineHeight: ''}).success).toStrictEqual(false)
expect(typographyValue.safeParse({...validValue, lineHeight: 10}).success).toStrictEqual(false)
Expand Down
Loading
Loading