From bcfe4f1016babbe6725035afa44605105f474c1a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Jun 2025 07:08:17 +0000 Subject: [PATCH 01/11] Initial plan for issue From ff17000a06b9cf997a8c4799beef165b9573c539 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Jun 2025 07:25:18 +0000 Subject: [PATCH 02/11] Update dimension schemas and transformers to support new object format Co-authored-by: lukasoppermann <813754+lukasoppermann@users.noreply.github.com> --- src/schemas/dimensionTokenSchema.test.ts | 38 +++++--- src/schemas/dimensionValue.ts | 14 ++- src/schemas/dimensionValueSchema.test.ts | 20 ++++- .../dimensionToPixelUnitless.test.ts | 25 ++++-- src/transformers/dimensionToPixelUnitless.ts | 88 +++++++++++++++---- src/transformers/dimensionToRem.test.ts | 25 +++++- src/transformers/dimensionToRem.ts | 75 +++++++++++++--- src/transformers/dimensionToRemPxArray.ts | 75 +++++++++++++--- 8 files changed, 295 insertions(+), 65 deletions(-) diff --git a/src/schemas/dimensionTokenSchema.test.ts b/src/schemas/dimensionTokenSchema.test.ts index 09e9e870b..c4ea14717 100644 --- a/src/schemas/dimensionTokenSchema.test.ts +++ b/src/schemas/dimensionTokenSchema.test.ts @@ -1,28 +1,42 @@ import {dimensionToken} from './dimensionToken.js' describe('Schema: dimensionToken', () => { - const validToken = { + const validTokenLegacy = { $value: '1px', $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) + const validTokenNew = { + $value: {value: 1, unit: 'px'}, + $type: 'dimension', + $description: 'a dimension token', + } + + it('passes on valid values (legacy string format)', () => { + expect(dimensionToken.safeParse(validTokenLegacy).success).toStrictEqual(true) + expect(dimensionToken.safeParse({...validTokenLegacy, $value: '1em'}).success).toStrictEqual(true) + expect(dimensionToken.safeParse({...validTokenLegacy, $value: '1rem'}).success).toStrictEqual(true) + }) + + it('passes on valid values (new object format)', () => { + expect(dimensionToken.safeParse(validTokenNew).success).toStrictEqual(true) + expect(dimensionToken.safeParse({...validTokenNew, $value: {value: 1, unit: 'em'}}).success).toStrictEqual(true) + expect(dimensionToken.safeParse({...validTokenNew, $value: {value: 1, unit: 'rem'}}).success).toStrictEqual(true) }) it('fails on invalid type', () => { - expect(dimensionToken.safeParse({...validToken, $type: 'stroke'}).success).toStrictEqual(false) + expect(dimensionToken.safeParse({...validTokenLegacy, $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: undefined}).success).toStrictEqual(false) - expect(dimensionToken.safeParse({...validToken, $value: ''}).success).toStrictEqual(false) - expect(dimensionToken.safeParse({...validToken, $value: false}).success).toStrictEqual(false) - expect(dimensionToken.safeParse({...validToken, $value: 1}).success).toStrictEqual(false) + expect(dimensionToken.safeParse({...validTokenLegacy, $value: 'wrong'}).success).toStrictEqual(false) + expect(dimensionToken.safeParse({...validTokenLegacy, $value: '1%'}).success).toStrictEqual(false) + expect(dimensionToken.safeParse({...validTokenLegacy, $value: undefined}).success).toStrictEqual(false) + expect(dimensionToken.safeParse({...validTokenLegacy, $value: ''}).success).toStrictEqual(false) + expect(dimensionToken.safeParse({...validTokenLegacy, $value: false}).success).toStrictEqual(false) + expect(dimensionToken.safeParse({...validTokenLegacy, $value: 1}).success).toStrictEqual(false) + expect(dimensionToken.safeParse({...validTokenNew, $value: {value: 1, unit: '%'}}).success).toStrictEqual(false) + expect(dimensionToken.safeParse({...validTokenNew, $value: {value: 'wrong', unit: 'px'}}).success).toStrictEqual(false) }) }) diff --git a/src/schemas/dimensionValue.ts b/src/schemas/dimensionValue.ts index b62733223..cf7f4a5f2 100644 --- a/src/schemas/dimensionValue.ts +++ b/src/schemas/dimensionValue.ts @@ -1,7 +1,14 @@ import {z} from 'zod' import {schemaErrorMessage} from '../utilities/index.js' -export const dimensionValue = z.union([ +// New object-based dimension format +const dimensionObjectValue = z.object({ + value: z.number(), + unit: z.enum(['px', 'rem', 'em']), +}) + +// Legacy string-based dimension format (for backward compatibility) +const dimensionStringValue = z.union([ z.string().refine( dim => /(^-?[0-9]+(px|rem)$|^-?[0-9]+\.?[0-9]*em$)/.test(dim), val => ({ @@ -14,3 +21,8 @@ export const dimensionValue = z.union([ z.literal('0'), z.literal(0), ]) + +export const dimensionValue = z.union([ + dimensionObjectValue, + dimensionStringValue, +]) diff --git a/src/schemas/dimensionValueSchema.test.ts b/src/schemas/dimensionValueSchema.test.ts index fd0cd7ff7..1b837ec1d 100644 --- a/src/schemas/dimensionValueSchema.test.ts +++ b/src/schemas/dimensionValueSchema.test.ts @@ -1,7 +1,7 @@ import {dimensionValue} from './dimensionValue.js' describe('Schema: dimensionValue', () => { - it('passes on valid values', () => { + it('passes on valid string values (legacy format)', () => { expect(dimensionValue.safeParse('1px').success).toStrictEqual(true) expect(dimensionValue.safeParse('-1px').success).toStrictEqual(true) expect(dimensionValue.safeParse('1em').success).toStrictEqual(true) @@ -10,7 +10,15 @@ describe('Schema: dimensionValue', () => { expect(dimensionValue.safeParse(0).success).toStrictEqual(true) }) - it('fails on invalid value', () => { + it('passes on valid object values (new format)', () => { + expect(dimensionValue.safeParse({value: 1, unit: 'px'}).success).toStrictEqual(true) + expect(dimensionValue.safeParse({value: -1, unit: 'px'}).success).toStrictEqual(true) + expect(dimensionValue.safeParse({value: 16, unit: 'rem'}).success).toStrictEqual(true) + expect(dimensionValue.safeParse({value: 1.5, unit: 'em'}).success).toStrictEqual(true) + expect(dimensionValue.safeParse({value: 0, unit: 'px'}).success).toStrictEqual(true) + }) + + it('fails on invalid string values', () => { expect(dimensionValue.safeParse('1%').success).toStrictEqual(false) expect(dimensionValue.safeParse(1).success).toStrictEqual(false) expect(dimensionValue.safeParse('small').success).toStrictEqual(false) @@ -18,4 +26,12 @@ describe('Schema: dimensionValue', () => { expect(dimensionValue.safeParse(false).success).toStrictEqual(false) expect(dimensionValue.safeParse(undefined).success).toStrictEqual(false) }) + + it('fails on invalid object values', () => { + expect(dimensionValue.safeParse({value: 1, unit: '%'}).success).toStrictEqual(false) + expect(dimensionValue.safeParse({value: 'small', unit: 'px'}).success).toStrictEqual(false) + expect(dimensionValue.safeParse({value: 1}).success).toStrictEqual(false) + expect(dimensionValue.safeParse({unit: 'px'}).success).toStrictEqual(false) + expect(dimensionValue.safeParse({}).success).toStrictEqual(false) + }) }) diff --git a/src/transformers/dimensionToPixelUnitless.test.ts b/src/transformers/dimensionToPixelUnitless.test.ts index 25e124cc5..398bff05c 100644 --- a/src/transformers/dimensionToPixelUnitless.test.ts +++ b/src/transformers/dimensionToPixelUnitless.test.ts @@ -7,8 +7,11 @@ describe('Transformer: dimensionToPixelUnitless', () => { getMockToken({ value: '16px', }), + getMockToken({ + value: {value: 16, unit: 'px'}, + }), ] - const expectedOutput = [16] + const expectedOutput = [16, 16] expect(input.map(item => dimensionToPixelUnitless.transform(item, {}, {}))).toStrictEqual(expectedOutput) }) @@ -30,8 +33,11 @@ describe('Transformer: dimensionToPixelUnitless', () => { getMockToken({ value: '1rem', }), + getMockToken({ + value: {value: 1, unit: 'rem'}, + }), ] - const expectedOutput = [16] + const expectedOutput = [16, 16] expect(input.map(item => dimensionToPixelUnitless.transform(item, {}, {}))).toStrictEqual(expectedOutput) }) @@ -40,8 +46,11 @@ describe('Transformer: dimensionToPixelUnitless', () => { getMockToken({ value: '2rem', }), + getMockToken({ + value: {value: 2, unit: 'rem'}, + }), ] - const expectedOutput = [20] + const expectedOutput = [20, 20] expect(input.map(item => dimensionToPixelUnitless.transform(item, {basePxFontSize: 10}, {}))).toStrictEqual( expectedOutput, ) @@ -52,8 +61,11 @@ describe('Transformer: dimensionToPixelUnitless', () => { getMockToken({ value: '1em', }), + getMockToken({ + value: {value: 1, unit: 'em'}, + }), ] - const expectedOutput = ['1em'] + const expectedOutput = ['1em', '1em'] expect(input.map(item => dimensionToPixelUnitless.transform(item, {}, {}))).toStrictEqual(expectedOutput) }) @@ -68,8 +80,11 @@ describe('Transformer: dimensionToPixelUnitless', () => { getMockToken({ value: '0', }), + getMockToken({ + value: {value: 0, unit: 'px'}, + }), ] - const expectedOutput = [0, 0, 0] + const expectedOutput = [0, 0, 0, 0] expect(input.map(item => dimensionToPixelUnitless.transform(item, {}, {}))).toStrictEqual(expectedOutput) }) diff --git a/src/transformers/dimensionToPixelUnitless.ts b/src/transformers/dimensionToPixelUnitless.ts index f6b082743..6a0265ac1 100644 --- a/src/transformers/dimensionToPixelUnitless.ts +++ b/src/transformers/dimensionToPixelUnitless.ts @@ -22,6 +22,47 @@ const hasUnit = (value: string | number, unit: string): boolean => { return value.indexOf(unit) > -1 } +/** + * @description extracts numeric value and unit from dimension token value + * @param value dimension token value (string or object format) + * @returns object with value and unit, or original value for special cases + */ +const parseDimensionValue = (value: string | number | {value: number; unit: string}): {value: number; unit: string} | {original: any} => { + // Handle invalid values + if (value === null || value === undefined || value === '') { + throw new Error(`Invalid dimension value: ${JSON.stringify(value)}`) + } + + // Handle new object format + if (typeof value === 'object' && value !== null && 'value' in value && 'unit' in value) { + return {value: value.value, unit: value.unit} + } + + // Handle legacy string/number format + if (typeof value === 'number') { + return {original: value} // Return original for pixelUnitless + } + + if (value === '0') { + return {value: 0, unit: 'px'} + } + + if (typeof value === 'string') { + // Handle pure number strings (return as original) + if (/^-?[0-9]+\.?[0-9]*$/.test(value)) { + return {original: value} + } + + const match = value.match(/^(-?[0-9]+\.?[0-9]*)(px|rem|em)$/) + if (match) { + return {value: parseFloat(match[1]), unit: match[2]} + } + } + + // Invalid values like 'rem', 'px' without numbers + throw new Error(`Invalid dimension value: ${JSON.stringify(value)}`) +} + /** * @description converts dimension tokens value to pixel value without unit, ignores `em` as they are relative to the font size of the parent element * @type value transformer — [StyleDictionary.ValueTransform](https://github.com/amzn/style-dictionary/blob/main/types/Transform.d.ts) @@ -36,25 +77,40 @@ export const dimensionToPixelUnitless: Transform = { transform: (token: TransformedToken, config: PlatformConfig, options: Config) => { const valueProp = options.usesDtcg ? '$value' : 'value' const baseFont = getBasePxFontSize(config) - const floatVal = parseFloat(token[valueProp]) - if (isNaN(floatVal)) { - throw new Error( - `Invalid dimension token: '${token.path.join('.')}: ${token[valueProp]}' is not valid and cannot be transform to 'float' \n`, - ) - } + + try { + const parsed = parseDimensionValue(token[valueProp]) + + if ('original' in parsed) { + // Return original value for numbers and em values + return parsed.original + } - if (floatVal === 0) { - return 0 - } + const {value, unit} = parsed - if (hasUnit(token[valueProp], 'rem')) { - return floatVal * baseFont - } + if (value === 0) { + return 0 + } - if (hasUnit(token[valueProp], 'px')) { - return floatVal - } + if (unit === 'rem') { + return value * baseFont + } + + if (unit === 'px') { + return value + } + + if (unit === 'em') { + // Return as string for em values (not converted) + return `${value}em` + } - return token[valueProp] + // Fallback for unexpected units + return parsed.original || value + } catch (error) { + throw new Error( + `Invalid dimension token: '${token.path.join('.')}: ${JSON.stringify(token[valueProp])}' is not valid and cannot be transform to 'float' \n`, + ) + } }, } diff --git a/src/transformers/dimensionToRem.test.ts b/src/transformers/dimensionToRem.test.ts index 21c06bb89..ca34ea55e 100644 --- a/src/transformers/dimensionToRem.test.ts +++ b/src/transformers/dimensionToRem.test.ts @@ -12,6 +12,16 @@ describe('Transformer: dimensionToRem', () => { expect(input.map(item => dimensionToRem.transform(item, {}, {}))).toStrictEqual(expectedOutput) }) + it('transforms pixel object tokens to rem (new format)', () => { + const input = [ + getMockToken({ + value: {value: 16, unit: 'px'}, + }), + ] + const expectedOutput = ['1rem'] + expect(input.map(item => dimensionToRem.transform(item, {}, {}))).toStrictEqual(expectedOutput) + }) + it('transforms number to rem', () => { const input = [ getMockToken({ @@ -30,8 +40,11 @@ describe('Transformer: dimensionToRem', () => { getMockToken({ value: '1rem', }), + getMockToken({ + value: {value: 1, unit: 'rem'}, + }), ] - const expectedOutput = ['1rem'] + const expectedOutput = ['1rem', '1rem'] expect(input.map(item => dimensionToRem.transform(item, {}, {}))).toStrictEqual(expectedOutput) }) @@ -40,8 +53,11 @@ describe('Transformer: dimensionToRem', () => { getMockToken({ value: '1em', }), + getMockToken({ + value: {value: 1, unit: 'em'}, + }), ] - const expectedOutput = ['1em'] + const expectedOutput = ['1em', '1em'] expect(input.map(item => dimensionToRem.transform(item, {}, {}))).toStrictEqual(expectedOutput) }) @@ -56,8 +72,11 @@ describe('Transformer: dimensionToRem', () => { getMockToken({ value: '0', }), + getMockToken({ + value: {value: 0, unit: 'px'}, + }), ] - const expectedOutput = ['0', '0', '0'] + const expectedOutput = ['0', '0', '0', '0'] expect(input.map(item => dimensionToRem.transform(item, {}, {}))).toStrictEqual(expectedOutput) }) diff --git a/src/transformers/dimensionToRem.ts b/src/transformers/dimensionToRem.ts index 5aaba8e70..b7279f39e 100644 --- a/src/transformers/dimensionToRem.ts +++ b/src/transformers/dimensionToRem.ts @@ -22,6 +22,47 @@ const hasUnit = (value: string | number, unit: string): boolean => { return value.indexOf(unit) > -1 } +/** + * @description extracts numeric value and unit from dimension token value + * @param value dimension token value (string or object format) + * @returns object with value and unit, or original value for special cases + */ +const parseDimensionValue = (value: string | number | {value: number; unit: string}): {value: number; unit: string} | {original: any} => { + // Handle invalid values + if (value === null || value === undefined || value === '') { + throw new Error(`Invalid dimension value: ${JSON.stringify(value)}`) + } + + // Handle new object format + if (typeof value === 'object' && value !== null && 'value' in value && 'unit' in value) { + return {value: value.value, unit: value.unit} + } + + // Handle legacy string/number format + if (typeof value === 'number') { + return {value, unit: 'px'} + } + + if (value === '0') { + return {value: 0, unit: 'px'} + } + + if (typeof value === 'string') { + // Handle pure number strings (for backward compatibility) + if (/^-?[0-9]+\.?[0-9]*$/.test(value)) { + return {value: parseFloat(value), unit: 'px'} + } + + const match = value.match(/^(-?[0-9]+\.?[0-9]*)(px|rem|em)$/) + if (match) { + return {value: parseFloat(match[1]), unit: match[2]} + } + } + + // Invalid values like 'rem', 'px' without numbers + throw new Error(`Invalid dimension value: ${JSON.stringify(value)}`) +} + /** * @description converts dimension tokens value to `rem`, ignores `em` as they are relative to the font size of the parent element * @type value transformer — [StyleDictionary.ValueTransform](https://github.com/amzn/style-dictionary/blob/main/types/Transform.d.ts) @@ -36,22 +77,30 @@ export const dimensionToRem: Transform = { transform: (token: TransformedToken, config: PlatformConfig, options: Config) => { const valueProp = options.usesDtcg ? '$value' : 'value' const baseFont = getBasePxFontSize(config) - const floatVal = parseFloat(token[valueProp]) + + try { + const parsed = parseDimensionValue(token[valueProp]) + + if ('original' in parsed) { + throw new Error(`Cannot parse dimension value`) + } - if (isNaN(floatVal)) { - throw new Error( - `Invalid dimension token: '${token.name}: ${token[valueProp]}' is not valid and cannot be transform to 'rem' \n`, - ) - } + const {value, unit} = parsed - if (floatVal === 0) { - return '0' - } + if (value === 0) { + return '0' + } - if (hasUnit(token[valueProp], 'rem') || hasUnit(token[valueProp], 'em')) { - return token[valueProp] - } + if (unit === 'rem' || unit === 'em') { + return `${value}${unit}` + } - return `${floatVal / baseFont}rem` + // Convert px to rem + return `${value / baseFont}rem` + } catch (error) { + throw new Error( + `Invalid dimension token: '${token.name}: ${JSON.stringify(token[valueProp])}' is not valid and cannot be transform to 'rem' \n`, + ) + } }, } diff --git a/src/transformers/dimensionToRemPxArray.ts b/src/transformers/dimensionToRemPxArray.ts index f5e0966ea..4d3576a35 100644 --- a/src/transformers/dimensionToRemPxArray.ts +++ b/src/transformers/dimensionToRemPxArray.ts @@ -26,6 +26,47 @@ const hasUnit = (value: string | number, unit: string): boolean => { return value.indexOf(unit) > -1 } +/** + * @description extracts numeric value and unit from dimension token value + * @param value dimension token value (string or object format) + * @returns object with value and unit, or original value for special cases + */ +const parseDimensionValue = (value: string | number | {value: number; unit: string}): {value: number; unit: string} | {original: any} => { + // Handle invalid values + if (value === null || value === undefined || value === '') { + throw new Error(`Invalid dimension value: ${JSON.stringify(value)}`) + } + + // Handle new object format + if (typeof value === 'object' && value !== null && 'value' in value && 'unit' in value) { + return {value: value.value, unit: value.unit} + } + + // Handle legacy string/number format + if (typeof value === 'number') { + return {value, unit: 'px'} + } + + if (value === '0') { + return {value: 0, unit: 'px'} + } + + if (typeof value === 'string') { + // Handle pure number strings (for backward compatibility) + if (/^-?[0-9]+\.?[0-9]*$/.test(value)) { + return {value: parseFloat(value), unit: 'px'} + } + + const match = value.match(/^(-?[0-9]+\.?[0-9]*)(px|rem|em)$/) + if (match) { + return {value: parseFloat(match[1]), unit: match[2]} + } + } + + // Invalid values like 'rem', 'px' without numbers + throw new Error(`Invalid dimension value: ${JSON.stringify(value)}`) +} + /** * @description converts dimension tokens value to `rem`, ignores `em` as they are relative to the font size of the parent element * @type value transformer — [StyleDictionary.ValueTransform](https://github.com/amzn/style-dictionary/blob/main/types/Transform.d.ts) @@ -40,22 +81,30 @@ export const dimensionToRemPxArray: Transform = { transform: (token: TransformedToken, config: PlatformConfig, options: Config): [SizeRem | SizeEm, SizePx] => { const valueProp = options.usesDtcg ? '$value' : 'value' const baseFont = getBasePxFontSize(config) - const floatVal = parseFloat(token[valueProp]) + + try { + const parsed = parseDimensionValue(token[valueProp]) + + if ('original' in parsed) { + throw new Error(`Cannot parse dimension value`) + } - if (isNaN(floatVal)) { - throw new Error( - `Invalid dimension token: '${token.name}: ${token[valueProp]}' is not valid and cannot be transform to 'rem' \n`, - ) - } + const {value, unit} = parsed - if (floatVal === 0) { - return ['0', '0'] - } + if (value === 0) { + return ['0', '0'] + } - if (hasUnit(token[valueProp], 'rem') || hasUnit(token[valueProp], 'em')) { - return [token.value, `${floatVal * baseFont}px`] - } + if (unit === 'rem' || unit === 'em') { + return [`${value}${unit}`, `${value * baseFont}px`] + } - return [`${floatVal / baseFont}rem`, `${floatVal}px`] + // Convert px to rem and keep px + return [`${value / baseFont}rem`, `${value}px`] + } catch (error) { + throw new Error( + `Invalid dimension token: '${token.name}: ${JSON.stringify(token[valueProp])}' is not valid and cannot be transform to 'rem' \n`, + ) + } }, } From e6982ac76fe0a0ecfc75e4da0005ed4c4cc59af0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Jun 2025 07:35:05 +0000 Subject: [PATCH 03/11] Update all dimension tokens to new object format Co-authored-by: lukasoppermann <813754+lukasoppermann@users.noreply.github.com> --- src/tokens/base/size/size.json5 | 38 +++++++++---------- src/tokens/functional/size/breakpoints.json5 | 12 +++--- src/tokens/functional/size/size.json5 | 30 +++++++-------- .../functional/typography/typography.json5 | 22 +++++------ 4 files changed, 51 insertions(+), 51 deletions(-) diff --git a/src/tokens/base/size/size.json5 b/src/tokens/base/size/size.json5 index f18cdd392..ceb57428c 100644 --- a/src/tokens/base/size/size.json5 +++ b/src/tokens/base/size/size.json5 @@ -2,7 +2,7 @@ "base": { "size": { "2": { - "$value": "2px", + "$value": {"value": 2, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -12,7 +12,7 @@ } }, "4": { - "$value": "4px", + "$value": {"value": 4, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -22,7 +22,7 @@ } }, "6": { - "$value": "6px", + "$value": {"value": 6, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -32,7 +32,7 @@ } }, "8": { - "$value": "8px", + "$value": {"value": 8, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -42,7 +42,7 @@ } }, "12": { - "$value": "12px", + "$value": {"value": 12, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -52,7 +52,7 @@ } }, "16": { - "$value": "16px", + "$value": {"value": 16, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -62,7 +62,7 @@ } }, "20": { - "$value": "20px", + "$value": {"value": 20, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -72,7 +72,7 @@ } }, "24": { - "$value": "24px", + "$value": {"value": 24, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -82,7 +82,7 @@ } }, "28": { - "$value": "28px", + "$value": {"value": 28, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -92,7 +92,7 @@ } }, "32": { - "$value": "32px", + "$value": {"value": 32, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -102,7 +102,7 @@ } }, "36": { - "$value": "36px", + "$value": {"value": 36, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -112,7 +112,7 @@ } }, "40": { - "$value": "40px", + "$value": {"value": 40, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -122,7 +122,7 @@ } }, "44": { - "$value": "44px", + "$value": {"value": 44, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -132,7 +132,7 @@ } }, "48": { - "$value": "48px", + "$value": {"value": 48, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -142,7 +142,7 @@ } }, "64": { - "$value": "64px", + "$value": {"value": 64, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -152,7 +152,7 @@ } }, "80": { - "$value": "80px", + "$value": {"value": 80, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -162,7 +162,7 @@ } }, "96": { - "$value": "96px", + "$value": {"value": 96, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -172,7 +172,7 @@ } }, "112": { - "$value": "112px", + "$value": {"value": 112, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -182,7 +182,7 @@ } }, "128": { - "$value": "128px", + "$value": {"value": 128, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { diff --git a/src/tokens/functional/size/breakpoints.json5 b/src/tokens/functional/size/breakpoints.json5 index e1458693a..b263ea220 100644 --- a/src/tokens/functional/size/breakpoints.json5 +++ b/src/tokens/functional/size/breakpoints.json5 @@ -1,7 +1,7 @@ { "breakpoint": { "xsmall": { - "$value": "320px", + "$value": {"value": 320, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -11,7 +11,7 @@ } }, "small": { - "$value": "544px", + "$value": {"value": 544, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -21,7 +21,7 @@ } }, "medium": { - "$value": "768px", + "$value": {"value": 768, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -31,7 +31,7 @@ } }, "large": { - "$value": "1012px", + "$value": {"value": 1012, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -41,7 +41,7 @@ } }, "xlarge": { - "$value": "1280px", + "$value": {"value": 1280, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -51,7 +51,7 @@ } }, "xxlarge": { - "$value": "1400px", + "$value": {"value": 1400, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { diff --git a/src/tokens/functional/size/size.json5 b/src/tokens/functional/size/size.json5 index 685831b9d..b203cff46 100644 --- a/src/tokens/functional/size/size.json5 +++ b/src/tokens/functional/size/size.json5 @@ -44,7 +44,7 @@ } }, "paddingBlock": { - "$value": "2px", + "$value": {"value": 2, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -182,7 +182,7 @@ } }, "paddingBlock": { - "$value": "6px", + "$value": {"value": 6, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -256,7 +256,7 @@ } }, "paddingBlock": { - "$value": "10px", + "$value": {"value": 10, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -320,7 +320,7 @@ } }, "paddingBlock": { - "$value": "14px", + "$value": {"value": 14, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -366,7 +366,7 @@ "spinner": { "strokeWidth": { "default": { - "$value": "2px", + "$value": {"value": 2, "unit": "px"}, "$type": "dimension" } }, @@ -580,7 +580,7 @@ "overlay": { "width": { "xsmall": { - "$value": "192px", + "$value": {"value": 192, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -590,7 +590,7 @@ } }, "small": { - "$value": "320px", + "$value": {"value": 320, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -600,7 +600,7 @@ } }, "medium": { - "$value": "480px", + "$value": {"value": 480, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -610,7 +610,7 @@ } }, "large": { - "$value": "640px", + "$value": {"value": 640, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -620,7 +620,7 @@ } }, "xlarge": { - "$value": "960px", + "$value": {"value": 960, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -632,7 +632,7 @@ }, "height": { "small": { - "$value": "256px", + "$value": {"value": 256, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -642,7 +642,7 @@ } }, "medium": { - "$value": "320px", + "$value": {"value": 320, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -652,7 +652,7 @@ } }, "large": { - "$value": "432px", + "$value": {"value": 432, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -662,7 +662,7 @@ } }, "xlarge": { - "$value": "600px", + "$value": {"value": 600, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { @@ -727,7 +727,7 @@ } }, "offset": { - "$value": "4px", + "$value": {"value": 4, "unit": "px"}, "$type": "dimension", "$extensions": { "org.primer.figma": { diff --git a/src/tokens/functional/typography/typography.json5 b/src/tokens/functional/typography/typography.json5 index bf7a9c038..7bd25e5ef 100644 --- a/src/tokens/functional/typography/typography.json5 +++ b/src/tokens/functional/typography/typography.json5 @@ -58,7 +58,7 @@ }, }, size: { - $value: '40px', + $value: {"value": 40, "unit": "px"}, $type: 'dimension', $extensions: { 'org.primer.figma': { @@ -104,7 +104,7 @@ title: { size: { large: { - $value: '32px', + $value: {"value": 32, "unit": "px"}, $type: 'dimension', $extensions: { 'org.primer.figma': { @@ -114,7 +114,7 @@ }, }, medium: { - $value: '20px', + $value: {"value": 20, "unit": "px"}, $type: 'dimension', $extensions: { 'org.primer.figma': { @@ -124,7 +124,7 @@ }, }, small: { - $value: '16px', + $value: {"value": 16, "unit": "px"}, $type: 'dimension', $extensions: { 'org.primer.figma': { @@ -242,7 +242,7 @@ }, subtitle: { size: { - $value: '20px', + $value: {"value": 20, "unit": "px"}, $type: 'dimension', $extensions: { 'org.primer.figma': { @@ -288,7 +288,7 @@ body: { size: { large: { - $value: '16px', + $value: {"value": 16, "unit": "px"}, $type: 'dimension', $extensions: { 'org.primer.figma': { @@ -298,7 +298,7 @@ }, }, medium: { - $value: '14px', + $value: {"value": 14, "unit": "px"}, $type: 'dimension', $extensions: { 'org.primer.figma': { @@ -308,7 +308,7 @@ }, }, small: { - $value: '12px', + $value: {"value": 12, "unit": "px"}, $type: 'dimension', $extensions: { 'org.primer.figma': { @@ -404,7 +404,7 @@ }, caption: { size: { - $value: '12px', + $value: {"value": 12, "unit": "px"}, $type: 'dimension', $extensions: { 'org.primer.figma': { @@ -449,7 +449,7 @@ }, codeBlock: { size: { - $value: '13px', + $value: {"value": 13, "unit": "px"}, $type: 'dimension', $extensions: { 'org.primer.figma': { @@ -494,7 +494,7 @@ }, codeInline: { size: { - $value: '0.9285em', + $value: {"value": 0.9285, "unit": "em"}, $type: 'dimension', $extensions: { 'org.primer.figma': { From fbd9bc5d025d9cdbd8e46159781d7c69c24ac466 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Jun 2025 08:07:32 +0000 Subject: [PATCH 04/11] Remove legacy dimension format support Co-authored-by: lukasoppermann <813754+lukasoppermann@users.noreply.github.com> --- src/schemas/borderTokenSchema.test.ts | 2 +- src/schemas/dimensionTokenSchema.test.ts | 41 +++---- src/schemas/dimensionValue.ts | 24 +--- src/schemas/dimensionValueSchema.test.ts | 32 +++--- src/schemas/shadowTokenSchema.test.ts | 18 ++- src/schemas/typographyTokenSchema.test.ts | 10 +- .../dimensionToPixelUnitless.test.ts | 65 +++++------ src/transformers/dimensionToPixelUnitless.ts | 106 ++++-------------- src/transformers/dimensionToRem.test.ts | 65 +++++------ src/transformers/dimensionToRem.ts | 90 +++------------ src/transformers/dimensionToRemPxArray.ts | 94 ++++------------ 11 files changed, 163 insertions(+), 384 deletions(-) diff --git a/src/schemas/borderTokenSchema.test.ts b/src/schemas/borderTokenSchema.test.ts index 03a7792ca..efce3a063 100644 --- a/src/schemas/borderTokenSchema.test.ts +++ b/src/schemas/borderTokenSchema.test.ts @@ -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', () => { diff --git a/src/schemas/dimensionTokenSchema.test.ts b/src/schemas/dimensionTokenSchema.test.ts index c4ea14717..09746ad1f 100644 --- a/src/schemas/dimensionTokenSchema.test.ts +++ b/src/schemas/dimensionTokenSchema.test.ts @@ -1,42 +1,31 @@ import {dimensionToken} from './dimensionToken.js' describe('Schema: dimensionToken', () => { - const validTokenLegacy = { - $value: '1px', - $type: 'dimension', - $description: 'a dimension token', - } - - const validTokenNew = { + const validToken = { $value: {value: 1, unit: 'px'}, $type: 'dimension', $description: 'a dimension token', } - it('passes on valid values (legacy string format)', () => { - expect(dimensionToken.safeParse(validTokenLegacy).success).toStrictEqual(true) - expect(dimensionToken.safeParse({...validTokenLegacy, $value: '1em'}).success).toStrictEqual(true) - expect(dimensionToken.safeParse({...validTokenLegacy, $value: '1rem'}).success).toStrictEqual(true) - }) - - it('passes on valid values (new object format)', () => { - expect(dimensionToken.safeParse(validTokenNew).success).toStrictEqual(true) - expect(dimensionToken.safeParse({...validTokenNew, $value: {value: 1, unit: 'em'}}).success).toStrictEqual(true) - expect(dimensionToken.safeParse({...validTokenNew, $value: {value: 1, unit: 'rem'}}).success).toStrictEqual(true) + it('passes on valid values', () => { + expect(dimensionToken.safeParse(validToken).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 invalid type', () => { - expect(dimensionToken.safeParse({...validTokenLegacy, $type: 'stroke'}).success).toStrictEqual(false) + expect(dimensionToken.safeParse({...validToken, $type: 'stroke'}).success).toStrictEqual(false) }) it('fails on invalid value', () => { - expect(dimensionToken.safeParse({...validTokenLegacy, $value: 'wrong'}).success).toStrictEqual(false) - expect(dimensionToken.safeParse({...validTokenLegacy, $value: '1%'}).success).toStrictEqual(false) - expect(dimensionToken.safeParse({...validTokenLegacy, $value: undefined}).success).toStrictEqual(false) - expect(dimensionToken.safeParse({...validTokenLegacy, $value: ''}).success).toStrictEqual(false) - expect(dimensionToken.safeParse({...validTokenLegacy, $value: false}).success).toStrictEqual(false) - expect(dimensionToken.safeParse({...validTokenLegacy, $value: 1}).success).toStrictEqual(false) - expect(dimensionToken.safeParse({...validTokenNew, $value: {value: 1, unit: '%'}}).success).toStrictEqual(false) - expect(dimensionToken.safeParse({...validTokenNew, $value: {value: 'wrong', unit: 'px'}}).success).toStrictEqual(false) + expect(dimensionToken.safeParse({...validToken, $value: 'wrong'}).success).toStrictEqual(false) + expect(dimensionToken.safeParse({...validToken, $value: '1px'}).success).toStrictEqual(false) + expect(dimensionToken.safeParse({...validToken, $value: '1%'}).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) + expect(dimensionToken.safeParse({...validToken, $value: 1}).success).toStrictEqual(false) + expect(dimensionToken.safeParse({...validToken, $value: {value: 1, unit: '%'}}).success).toStrictEqual(false) + expect(dimensionToken.safeParse({...validToken, $value: {value: 'wrong', unit: 'px'}}).success).toStrictEqual(false) }) }) diff --git a/src/schemas/dimensionValue.ts b/src/schemas/dimensionValue.ts index cf7f4a5f2..d05a5fc99 100644 --- a/src/schemas/dimensionValue.ts +++ b/src/schemas/dimensionValue.ts @@ -1,28 +1,6 @@ import {z} from 'zod' -import {schemaErrorMessage} from '../utilities/index.js' -// New object-based dimension format -const dimensionObjectValue = z.object({ +export const dimensionValue = z.object({ value: z.number(), unit: z.enum(['px', 'rem', 'em']), }) - -// Legacy string-based dimension format (for backward compatibility) -const dimensionStringValue = z.union([ - z.string().refine( - dim => /(^-?[0-9]+(px|rem)$|^-?[0-9]+\.?[0-9]*em$)/.test(dim), - val => ({ - message: schemaErrorMessage( - `Invalid dimension: "${val}"`, - `Dimension must be a string with a unit (px, rem or em) or 0`, - ), - }), - ), - z.literal('0'), - z.literal(0), -]) - -export const dimensionValue = z.union([ - dimensionObjectValue, - dimensionStringValue, -]) diff --git a/src/schemas/dimensionValueSchema.test.ts b/src/schemas/dimensionValueSchema.test.ts index 1b837ec1d..8bbe48921 100644 --- a/src/schemas/dimensionValueSchema.test.ts +++ b/src/schemas/dimensionValueSchema.test.ts @@ -1,16 +1,7 @@ import {dimensionValue} from './dimensionValue.js' describe('Schema: dimensionValue', () => { - it('passes on valid string values (legacy format)', () => { - 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 valid object values (new format)', () => { + it('passes on valid object values', () => { expect(dimensionValue.safeParse({value: 1, unit: 'px'}).success).toStrictEqual(true) expect(dimensionValue.safeParse({value: -1, unit: 'px'}).success).toStrictEqual(true) expect(dimensionValue.safeParse({value: 16, unit: 'rem'}).success).toStrictEqual(true) @@ -18,15 +9,6 @@ describe('Schema: dimensionValue', () => { expect(dimensionValue.safeParse({value: 0, unit: 'px'}).success).toStrictEqual(true) }) - it('fails on invalid string values', () => { - expect(dimensionValue.safeParse('1%').success).toStrictEqual(false) - expect(dimensionValue.safeParse(1).success).toStrictEqual(false) - expect(dimensionValue.safeParse('small').success).toStrictEqual(false) - expect(dimensionValue.safeParse('').success).toStrictEqual(false) - expect(dimensionValue.safeParse(false).success).toStrictEqual(false) - expect(dimensionValue.safeParse(undefined).success).toStrictEqual(false) - }) - it('fails on invalid object values', () => { expect(dimensionValue.safeParse({value: 1, unit: '%'}).success).toStrictEqual(false) expect(dimensionValue.safeParse({value: 'small', unit: 'px'}).success).toStrictEqual(false) @@ -34,4 +16,16 @@ describe('Schema: dimensionValue', () => { expect(dimensionValue.safeParse({unit: 'px'}).success).toStrictEqual(false) expect(dimensionValue.safeParse({}).success).toStrictEqual(false) }) + + it('fails on invalid values', () => { + expect(dimensionValue.safeParse('1px').success).toStrictEqual(false) + expect(dimensionValue.safeParse('1%').success).toStrictEqual(false) + expect(dimensionValue.safeParse(1).success).toStrictEqual(false) + expect(dimensionValue.safeParse('small').success).toStrictEqual(false) + expect(dimensionValue.safeParse('').success).toStrictEqual(false) + expect(dimensionValue.safeParse(false).success).toStrictEqual(false) + expect(dimensionValue.safeParse(undefined).success).toStrictEqual(false) + expect(dimensionValue.safeParse('0').success).toStrictEqual(false) + expect(dimensionValue.safeParse(0).success).toStrictEqual(false) + }) }) diff --git a/src/schemas/shadowTokenSchema.test.ts b/src/schemas/shadowTokenSchema.test.ts index 6b0760542..8736ba429 100644 --- a/src/schemas/shadowTokenSchema.test.ts +++ b/src/schemas/shadowTokenSchema.test.ts @@ -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, } @@ -15,8 +15,14 @@ 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) }) diff --git a/src/schemas/typographyTokenSchema.test.ts b/src/schemas/typographyTokenSchema.test.ts index cd9ed2bc7..7d9a0b712 100644 --- a/src/schemas/typographyTokenSchema.test.ts +++ b/src/schemas/typographyTokenSchema.test.ts @@ -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', } @@ -31,14 +31,16 @@ describe('Schema: typographyToken', () => { }) 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: '16px'}).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: '24px'}).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) diff --git a/src/transformers/dimensionToPixelUnitless.test.ts b/src/transformers/dimensionToPixelUnitless.test.ts index 398bff05c..3c812e7d6 100644 --- a/src/transformers/dimensionToPixelUnitless.test.ts +++ b/src/transformers/dimensionToPixelUnitless.test.ts @@ -2,55 +2,33 @@ import {getMockToken} from '../test-utilities/index.js' import {dimensionToPixelUnitless} from './dimensionToPixelUnitless.js' describe('Transformer: dimensionToPixelUnitless', () => { - it('transforms pixel string tokens', () => { + it('transforms pixel object tokens', () => { const input = [ - getMockToken({ - value: '16px', - }), getMockToken({ value: {value: 16, unit: 'px'}, }), ] - const expectedOutput = [16, 16] - expect(input.map(item => dimensionToPixelUnitless.transform(item, {}, {}))).toStrictEqual(expectedOutput) - }) - - it('does not transforms number or number string', () => { - const input = [ - getMockToken({ - value: '16', - }), - getMockToken({ - value: 16, - }), - ] - const expectedOutput = ['16', 16] + const expectedOutput = [16] expect(input.map(item => dimensionToPixelUnitless.transform(item, {}, {}))).toStrictEqual(expectedOutput) }) it('transforms rem', () => { const input = [ - getMockToken({ - value: '1rem', - }), getMockToken({ value: {value: 1, unit: 'rem'}, }), ] - const expectedOutput = [16, 16] + const expectedOutput = [16] expect(input.map(item => dimensionToPixelUnitless.transform(item, {}, {}))).toStrictEqual(expectedOutput) }) it('transforms rem with custom basePxFontSize', () => { const input = [ - getMockToken({ - value: '2rem', - }), getMockToken({ value: {value: 2, unit: 'rem'}, }), ] - const expectedOutput = [20, 20] + const expectedOutput = [20] expect(input.map(item => dimensionToPixelUnitless.transform(item, {basePxFontSize: 10}, {}))).toStrictEqual( expectedOutput, ) @@ -58,33 +36,27 @@ describe('Transformer: dimensionToPixelUnitless', () => { it('does not transforms em', () => { const input = [ - getMockToken({ - value: '1em', - }), getMockToken({ value: {value: 1, unit: 'em'}, }), ] - const expectedOutput = ['1em', '1em'] + const expectedOutput = ['1em'] expect(input.map(item => dimensionToPixelUnitless.transform(item, {}, {}))).toStrictEqual(expectedOutput) }) it('transforms 0 to 0', () => { const input = [ getMockToken({ - value: '0rem', - }), - getMockToken({ - value: '0px', + value: {value: 0, unit: 'rem'}, }), getMockToken({ - value: '0', + value: {value: 0, unit: 'px'}, }), getMockToken({ - value: {value: 0, unit: 'px'}, + value: {value: 0, unit: 'em'}, }), ] - const expectedOutput = [0, 0, 0, 0] + const expectedOutput = [0, 0, 0] expect(input.map(item => dimensionToPixelUnitless.transform(item, {}, {}))).toStrictEqual(expectedOutput) }) @@ -93,6 +65,12 @@ describe('Transformer: dimensionToPixelUnitless', () => { getMockToken({ value: 'rem', }), + getMockToken({ + value: '16px', + }), + getMockToken({ + value: 16, + }), getMockToken({ value: '', }), @@ -102,7 +80,18 @@ describe('Transformer: dimensionToPixelUnitless', () => { getMockToken({ value: null, }), + getMockToken({ + value: {}, + }), + getMockToken({ + value: {value: 16}, + }), + getMockToken({ + value: {unit: 'px'}, + }), ] - expect(() => input.map(item => dimensionToPixelUnitless.transform(item, {}, {}))).toThrow() + input.forEach(token => { + expect(() => dimensionToPixelUnitless.transform(token, {}, {})).toThrow() + }) }) }) diff --git a/src/transformers/dimensionToPixelUnitless.ts b/src/transformers/dimensionToPixelUnitless.ts index 6a0265ac1..faf814ded 100644 --- a/src/transformers/dimensionToPixelUnitless.ts +++ b/src/transformers/dimensionToPixelUnitless.ts @@ -8,61 +8,6 @@ import type {PlatformConfig, Transform, TransformedToken, Config} from 'style-di */ const getBasePxFontSize = (options?: PlatformConfig): number => (options && options.basePxFontSize) || 16 -/** - * @description checks if token value has a specific unit - * @param value token value - * @param unit unit string like px or value - * @returns boolean - */ -const hasUnit = (value: string | number, unit: string): boolean => { - if (typeof value === 'number') { - return false - } - - return value.indexOf(unit) > -1 -} - -/** - * @description extracts numeric value and unit from dimension token value - * @param value dimension token value (string or object format) - * @returns object with value and unit, or original value for special cases - */ -const parseDimensionValue = (value: string | number | {value: number; unit: string}): {value: number; unit: string} | {original: any} => { - // Handle invalid values - if (value === null || value === undefined || value === '') { - throw new Error(`Invalid dimension value: ${JSON.stringify(value)}`) - } - - // Handle new object format - if (typeof value === 'object' && value !== null && 'value' in value && 'unit' in value) { - return {value: value.value, unit: value.unit} - } - - // Handle legacy string/number format - if (typeof value === 'number') { - return {original: value} // Return original for pixelUnitless - } - - if (value === '0') { - return {value: 0, unit: 'px'} - } - - if (typeof value === 'string') { - // Handle pure number strings (return as original) - if (/^-?[0-9]+\.?[0-9]*$/.test(value)) { - return {original: value} - } - - const match = value.match(/^(-?[0-9]+\.?[0-9]*)(px|rem|em)$/) - if (match) { - return {value: parseFloat(match[1]), unit: match[2]} - } - } - - // Invalid values like 'rem', 'px' without numbers - throw new Error(`Invalid dimension value: ${JSON.stringify(value)}`) -} - /** * @description converts dimension tokens value to pixel value without unit, ignores `em` as they are relative to the font size of the parent element * @type value transformer — [StyleDictionary.ValueTransform](https://github.com/amzn/style-dictionary/blob/main/types/Transform.d.ts) @@ -77,40 +22,35 @@ export const dimensionToPixelUnitless: Transform = { transform: (token: TransformedToken, config: PlatformConfig, options: Config) => { const valueProp = options.usesDtcg ? '$value' : 'value' const baseFont = getBasePxFontSize(config) + const dimensionValue = token[valueProp] as {value: number; unit: string} - try { - const parsed = parseDimensionValue(token[valueProp]) - - if ('original' in parsed) { - // Return original value for numbers and em values - return parsed.original - } - - const {value, unit} = parsed + if (!dimensionValue || typeof dimensionValue !== 'object' || !('value' in dimensionValue) || !('unit' in dimensionValue)) { + throw new Error( + `Invalid dimension token: '${token.path.join('.')}: ${JSON.stringify(token[valueProp])}' must be an object with value and unit properties \n`, + ) + } - if (value === 0) { - return 0 - } + const {value, unit} = dimensionValue - if (unit === 'rem') { - return value * baseFont - } + if (value === 0) { + return 0 + } - if (unit === 'px') { - return value - } + if (unit === 'px') { + return value + } - if (unit === 'em') { - // Return as string for em values (not converted) - return `${value}em` - } + if (unit === 'rem') { + return value * baseFont + } - // Fallback for unexpected units - return parsed.original || value - } catch (error) { - throw new Error( - `Invalid dimension token: '${token.path.join('.')}: ${JSON.stringify(token[valueProp])}' is not valid and cannot be transform to 'float' \n`, - ) + if (unit === 'em') { + // Return as string for em values (not converted) + return `${value}em` } + + throw new Error( + `Invalid dimension token: '${token.path.join('.')}: ${JSON.stringify(token[valueProp])}' has unsupported unit '${unit}' \n`, + ) }, } diff --git a/src/transformers/dimensionToRem.test.ts b/src/transformers/dimensionToRem.test.ts index ca34ea55e..eb9b90b60 100644 --- a/src/transformers/dimensionToRem.test.ts +++ b/src/transformers/dimensionToRem.test.ts @@ -2,17 +2,7 @@ import {getMockToken} from '../test-utilities/index.js' import {dimensionToRem} from './dimensionToRem.js' describe('Transformer: dimensionToRem', () => { - it('transforms pixel string tokens to rem', () => { - const input = [ - getMockToken({ - value: '16px', - }), - ] - const expectedOutput = ['1rem'] - expect(input.map(item => dimensionToRem.transform(item, {}, {}))).toStrictEqual(expectedOutput) - }) - - it('transforms pixel object tokens to rem (new format)', () => { + it('transforms pixel object tokens to rem', () => { const input = [ getMockToken({ value: {value: 16, unit: 'px'}, @@ -22,61 +12,39 @@ describe('Transformer: dimensionToRem', () => { expect(input.map(item => dimensionToRem.transform(item, {}, {}))).toStrictEqual(expectedOutput) }) - it('transforms number to rem', () => { - const input = [ - getMockToken({ - value: '16', - }), - getMockToken({ - value: 16, - }), - ] - const expectedOutput = ['1rem', '1rem'] - expect(input.map(item => dimensionToRem.transform(item, {}, {}))).toStrictEqual(expectedOutput) - }) - it('transforms rem to rem', () => { const input = [ - getMockToken({ - value: '1rem', - }), getMockToken({ value: {value: 1, unit: 'rem'}, }), ] - const expectedOutput = ['1rem', '1rem'] + const expectedOutput = ['1rem'] expect(input.map(item => dimensionToRem.transform(item, {}, {}))).toStrictEqual(expectedOutput) }) it('does not transforms em to rem', () => { const input = [ - getMockToken({ - value: '1em', - }), getMockToken({ value: {value: 1, unit: 'em'}, }), ] - const expectedOutput = ['1em', '1em'] + const expectedOutput = ['1em'] expect(input.map(item => dimensionToRem.transform(item, {}, {}))).toStrictEqual(expectedOutput) }) it('transforms 0 to 0', () => { const input = [ getMockToken({ - value: '0rem', - }), - getMockToken({ - value: '0px', + value: {value: 0, unit: 'rem'}, }), getMockToken({ - value: '0', + value: {value: 0, unit: 'px'}, }), getMockToken({ - value: {value: 0, unit: 'px'}, + value: {value: 0, unit: 'em'}, }), ] - const expectedOutput = ['0', '0', '0', '0'] + const expectedOutput = ['0', '0', '0'] expect(input.map(item => dimensionToRem.transform(item, {}, {}))).toStrictEqual(expectedOutput) }) @@ -85,6 +53,12 @@ describe('Transformer: dimensionToRem', () => { getMockToken({ value: 'rem', }), + getMockToken({ + value: '16px', + }), + getMockToken({ + value: 16, + }), getMockToken({ value: '', }), @@ -94,7 +68,18 @@ describe('Transformer: dimensionToRem', () => { getMockToken({ value: null, }), + getMockToken({ + value: {}, + }), + getMockToken({ + value: {value: 16}, + }), + getMockToken({ + value: {unit: 'px'}, + }), ] - expect(() => input.map(item => dimensionToRem.transform(item, {}, {}))).toThrow() + input.forEach(token => { + expect(() => dimensionToRem.transform(token, {}, {})).toThrow() + }) }) }) diff --git a/src/transformers/dimensionToRem.ts b/src/transformers/dimensionToRem.ts index b7279f39e..990456b61 100644 --- a/src/transformers/dimensionToRem.ts +++ b/src/transformers/dimensionToRem.ts @@ -8,61 +8,6 @@ import type {Config, PlatformConfig, Transform, TransformedToken} from 'style-di */ const getBasePxFontSize = (options?: PlatformConfig): number => (options && options.basePxFontSize) || 16 -/** - * @description checks if token value has a specific unit - * @param value token value - * @param unit unit string like px or value - * @returns boolean - */ -const hasUnit = (value: string | number, unit: string): boolean => { - if (typeof value === 'number') { - return false - } - - return value.indexOf(unit) > -1 -} - -/** - * @description extracts numeric value and unit from dimension token value - * @param value dimension token value (string or object format) - * @returns object with value and unit, or original value for special cases - */ -const parseDimensionValue = (value: string | number | {value: number; unit: string}): {value: number; unit: string} | {original: any} => { - // Handle invalid values - if (value === null || value === undefined || value === '') { - throw new Error(`Invalid dimension value: ${JSON.stringify(value)}`) - } - - // Handle new object format - if (typeof value === 'object' && value !== null && 'value' in value && 'unit' in value) { - return {value: value.value, unit: value.unit} - } - - // Handle legacy string/number format - if (typeof value === 'number') { - return {value, unit: 'px'} - } - - if (value === '0') { - return {value: 0, unit: 'px'} - } - - if (typeof value === 'string') { - // Handle pure number strings (for backward compatibility) - if (/^-?[0-9]+\.?[0-9]*$/.test(value)) { - return {value: parseFloat(value), unit: 'px'} - } - - const match = value.match(/^(-?[0-9]+\.?[0-9]*)(px|rem|em)$/) - if (match) { - return {value: parseFloat(match[1]), unit: match[2]} - } - } - - // Invalid values like 'rem', 'px' without numbers - throw new Error(`Invalid dimension value: ${JSON.stringify(value)}`) -} - /** * @description converts dimension tokens value to `rem`, ignores `em` as they are relative to the font size of the parent element * @type value transformer — [StyleDictionary.ValueTransform](https://github.com/amzn/style-dictionary/blob/main/types/Transform.d.ts) @@ -77,30 +22,31 @@ export const dimensionToRem: Transform = { transform: (token: TransformedToken, config: PlatformConfig, options: Config) => { const valueProp = options.usesDtcg ? '$value' : 'value' const baseFont = getBasePxFontSize(config) + const dimensionValue = token[valueProp] as {value: number; unit: string} - try { - const parsed = parseDimensionValue(token[valueProp]) - - if ('original' in parsed) { - throw new Error(`Cannot parse dimension value`) - } + if (!dimensionValue || typeof dimensionValue !== 'object' || !('value' in dimensionValue) || !('unit' in dimensionValue)) { + throw new Error( + `Invalid dimension token: '${token.name}: ${JSON.stringify(token[valueProp])}' must be an object with value and unit properties \n`, + ) + } - const {value, unit} = parsed + const {value, unit} = dimensionValue - if (value === 0) { - return '0' - } + if (value === 0) { + return '0' + } - if (unit === 'rem' || unit === 'em') { - return `${value}${unit}` - } + if (unit === 'rem' || unit === 'em') { + return `${value}${unit}` + } + if (unit === 'px') { // Convert px to rem return `${value / baseFont}rem` - } catch (error) { - throw new Error( - `Invalid dimension token: '${token.name}: ${JSON.stringify(token[valueProp])}' is not valid and cannot be transform to 'rem' \n`, - ) } + + throw new Error( + `Invalid dimension token: '${token.name}: ${JSON.stringify(token[valueProp])}' has unsupported unit '${unit}' \n`, + ) }, } diff --git a/src/transformers/dimensionToRemPxArray.ts b/src/transformers/dimensionToRemPxArray.ts index 4d3576a35..99522163e 100644 --- a/src/transformers/dimensionToRemPxArray.ts +++ b/src/transformers/dimensionToRemPxArray.ts @@ -12,61 +12,6 @@ type SizeEm = '0' | `${number}em` */ const getBasePxFontSize = (options?: PlatformConfig): number => (options && options.basePxFontSize) || 16 -/** - * @description checks if token value has a specific unit - * @param value token value - * @param unit unit string like px or value - * @returns boolean - */ -const hasUnit = (value: string | number, unit: string): boolean => { - if (typeof value === 'number') { - return false - } - - return value.indexOf(unit) > -1 -} - -/** - * @description extracts numeric value and unit from dimension token value - * @param value dimension token value (string or object format) - * @returns object with value and unit, or original value for special cases - */ -const parseDimensionValue = (value: string | number | {value: number; unit: string}): {value: number; unit: string} | {original: any} => { - // Handle invalid values - if (value === null || value === undefined || value === '') { - throw new Error(`Invalid dimension value: ${JSON.stringify(value)}`) - } - - // Handle new object format - if (typeof value === 'object' && value !== null && 'value' in value && 'unit' in value) { - return {value: value.value, unit: value.unit} - } - - // Handle legacy string/number format - if (typeof value === 'number') { - return {value, unit: 'px'} - } - - if (value === '0') { - return {value: 0, unit: 'px'} - } - - if (typeof value === 'string') { - // Handle pure number strings (for backward compatibility) - if (/^-?[0-9]+\.?[0-9]*$/.test(value)) { - return {value: parseFloat(value), unit: 'px'} - } - - const match = value.match(/^(-?[0-9]+\.?[0-9]*)(px|rem|em)$/) - if (match) { - return {value: parseFloat(match[1]), unit: match[2]} - } - } - - // Invalid values like 'rem', 'px' without numbers - throw new Error(`Invalid dimension value: ${JSON.stringify(value)}`) -} - /** * @description converts dimension tokens value to `rem`, ignores `em` as they are relative to the font size of the parent element * @type value transformer — [StyleDictionary.ValueTransform](https://github.com/amzn/style-dictionary/blob/main/types/Transform.d.ts) @@ -81,30 +26,35 @@ export const dimensionToRemPxArray: Transform = { transform: (token: TransformedToken, config: PlatformConfig, options: Config): [SizeRem | SizeEm, SizePx] => { const valueProp = options.usesDtcg ? '$value' : 'value' const baseFont = getBasePxFontSize(config) + const dimensionValue = token[valueProp] as {value: number; unit: string} - try { - const parsed = parseDimensionValue(token[valueProp]) - - if ('original' in parsed) { - throw new Error(`Cannot parse dimension value`) - } + if (!dimensionValue || typeof dimensionValue !== 'object' || !('value' in dimensionValue) || !('unit' in dimensionValue)) { + throw new Error( + `Invalid dimension token: '${token.name}: ${JSON.stringify(token[valueProp])}' must be an object with value and unit properties \n`, + ) + } - const {value, unit} = parsed + const {value, unit} = dimensionValue - if (value === 0) { - return ['0', '0'] - } + if (value === 0) { + return ['0', '0'] + } - if (unit === 'rem' || unit === 'em') { - return [`${value}${unit}`, `${value * baseFont}px`] - } + if (unit === 'rem') { + return [`${value}rem`, `${value * baseFont}px`] + } + + if (unit === 'em') { + return [`${value}em`, `${value * baseFont}px`] + } + if (unit === 'px') { // Convert px to rem and keep px return [`${value / baseFont}rem`, `${value}px`] - } catch (error) { - throw new Error( - `Invalid dimension token: '${token.name}: ${JSON.stringify(token[valueProp])}' is not valid and cannot be transform to 'rem' \n`, - ) } + + throw new Error( + `Invalid dimension token: '${token.name}: ${JSON.stringify(token[valueProp])}' has unsupported unit '${unit}' \n`, + ) }, } From b93c2ff978447d8d387064858373e56fa6123b4b Mon Sep 17 00:00:00 2001 From: Lukas Oppermann Date: Tue, 17 Jun 2025 09:52:55 +0200 Subject: [PATCH 05/11] fix --- src/schemas/shadowTokenSchema.test.ts | 12 ++++++------ src/transformers/dimensionToPixelUnitless.ts | 4 ++-- src/transformers/dimensionToRem.ts | 4 ++-- src/transformers/dimensionToRemPxArray.ts | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/schemas/shadowTokenSchema.test.ts b/src/schemas/shadowTokenSchema.test.ts index 8736ba429..0e4536dce 100644 --- a/src/schemas/shadowTokenSchema.test.ts +++ b/src/schemas/shadowTokenSchema.test.ts @@ -16,12 +16,12 @@ describe('Schema: shadowValue', () => { // without inset 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'} + 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) }) diff --git a/src/transformers/dimensionToPixelUnitless.ts b/src/transformers/dimensionToPixelUnitless.ts index faf814ded..62ca2a428 100644 --- a/src/transformers/dimensionToPixelUnitless.ts +++ b/src/transformers/dimensionToPixelUnitless.ts @@ -23,8 +23,8 @@ export const dimensionToPixelUnitless: Transform = { const valueProp = options.usesDtcg ? '$value' : 'value' const baseFont = getBasePxFontSize(config) const dimensionValue = token[valueProp] as {value: number; unit: string} - - if (!dimensionValue || typeof dimensionValue !== 'object' || !('value' in dimensionValue) || !('unit' in dimensionValue)) { + + if (typeof dimensionValue !== 'object' || !('value' in dimensionValue) || !('unit' in dimensionValue)) { throw new Error( `Invalid dimension token: '${token.path.join('.')}: ${JSON.stringify(token[valueProp])}' must be an object with value and unit properties \n`, ) diff --git a/src/transformers/dimensionToRem.ts b/src/transformers/dimensionToRem.ts index 990456b61..de922df97 100644 --- a/src/transformers/dimensionToRem.ts +++ b/src/transformers/dimensionToRem.ts @@ -23,8 +23,8 @@ export const dimensionToRem: Transform = { const valueProp = options.usesDtcg ? '$value' : 'value' const baseFont = getBasePxFontSize(config) const dimensionValue = token[valueProp] as {value: number; unit: string} - - if (!dimensionValue || typeof dimensionValue !== 'object' || !('value' in dimensionValue) || !('unit' in dimensionValue)) { + + if (typeof dimensionValue !== 'object' || !('value' in dimensionValue) || !('unit' in dimensionValue)) { throw new Error( `Invalid dimension token: '${token.name}: ${JSON.stringify(token[valueProp])}' must be an object with value and unit properties \n`, ) diff --git a/src/transformers/dimensionToRemPxArray.ts b/src/transformers/dimensionToRemPxArray.ts index 99522163e..045c42098 100644 --- a/src/transformers/dimensionToRemPxArray.ts +++ b/src/transformers/dimensionToRemPxArray.ts @@ -27,8 +27,8 @@ export const dimensionToRemPxArray: Transform = { const valueProp = options.usesDtcg ? '$value' : 'value' const baseFont = getBasePxFontSize(config) const dimensionValue = token[valueProp] as {value: number; unit: string} - - if (!dimensionValue || typeof dimensionValue !== 'object' || !('value' in dimensionValue) || !('unit' in dimensionValue)) { + + if (typeof dimensionValue !== 'object' || !('value' in dimensionValue) || !('unit' in dimensionValue)) { throw new Error( `Invalid dimension token: '${token.name}: ${JSON.stringify(token[valueProp])}' must be an object with value and unit properties \n`, ) From d91c7afa024ad9dded155134d66f7407c302cab6 Mon Sep 17 00:00:00 2001 From: Lukas Oppermann Date: Tue, 17 Jun 2025 10:24:14 +0200 Subject: [PATCH 06/11] fix for of --- src/transformers/dimensionToPixelUnitless.test.ts | 4 ++-- src/transformers/dimensionToRem.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/transformers/dimensionToPixelUnitless.test.ts b/src/transformers/dimensionToPixelUnitless.test.ts index 3c812e7d6..bba4e8ae5 100644 --- a/src/transformers/dimensionToPixelUnitless.test.ts +++ b/src/transformers/dimensionToPixelUnitless.test.ts @@ -90,8 +90,8 @@ describe('Transformer: dimensionToPixelUnitless', () => { value: {unit: 'px'}, }), ] - input.forEach(token => { + for (const token of input) { expect(() => dimensionToPixelUnitless.transform(token, {}, {})).toThrow() - }) + } }) }) diff --git a/src/transformers/dimensionToRem.test.ts b/src/transformers/dimensionToRem.test.ts index eb9b90b60..b81ec2b08 100644 --- a/src/transformers/dimensionToRem.test.ts +++ b/src/transformers/dimensionToRem.test.ts @@ -78,8 +78,8 @@ describe('Transformer: dimensionToRem', () => { value: {unit: 'px'}, }), ] - input.forEach(token => { + for (const token of input) { expect(() => dimensionToRem.transform(token, {}, {})).toThrow() - }) + } }) }) From 0c98cd39514be55944e13e911c2b5e774a9328b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Jul 2025 12:23:17 +0000 Subject: [PATCH 07/11] Fix dimension format in all remaining files and update Figma formatter Co-authored-by: lukasoppermann <813754+lukasoppermann@users.noreply.github.com> --- src/formats/jsonFigma.ts | 44 ++- src/tokens/component/avatar.json5 | 16 +- src/tokens/component/button.json5 | 88 +++--- src/tokens/functional/border/border.json5 | 2 +- src/tokens/functional/shadow/shadow.json5 | 320 +++++++++++----------- src/tokens/functional/size/border.json5 | 18 +- 6 files changed, 256 insertions(+), 232 deletions(-) diff --git a/src/formats/jsonFigma.ts b/src/formats/jsonFigma.ts index 5a276a994..b34090a91 100644 --- a/src/formats/jsonFigma.ts +++ b/src/formats/jsonFigma.ts @@ -7,6 +7,9 @@ import type {RgbaFloat} from '../transformers/utilities/isRgbaFloat.js' import {isRgbaFloat} from '../transformers/utilities/isRgbaFloat.js' import {getReferences, sortByReference} from 'style-dictionary/utils' +// Type for dimension value that can be either string (legacy) or object (new format) +type DimensionValue = string | { value: number; unit: string } + const isReference = (string: string): boolean => /^\{([^\\]*)\}$/g.test(string) const getReference = (dictionary: Dictionary, refString: string, platform: PlatformConfig) => { @@ -32,19 +35,40 @@ const getFigmaType = (type: string): string => { const shadowToVariables = ( name: string, - values: Omit & {color: string | RgbaFloat}, + values: Omit & { + color: string | RgbaFloat; + offsetX: DimensionValue; + offsetY: DimensionValue; + blur: DimensionValue; + spread: DimensionValue; + }, token: TransformedToken, ) => { // floatValue - const floatValue = (property: 'offsetX' | 'offsetY' | 'blur' | 'spread') => ({ - name: `${name}/${property}`, - value: parseInt(values[property].replace('px', '')), - type: 'FLOAT', - scopes: ['EFFECT_FLOAT'], - mode, - collection, - group, - }) + const floatValue = (property: 'offsetX' | 'offsetY' | 'blur' | 'spread') => { + const dimValue = values[property]; + let numValue: number; + + if (typeof dimValue === 'string') { + // Legacy string format like "1px" + numValue = parseInt(dimValue.replace('px', '')); + } else if (typeof dimValue === 'object' && dimValue.value !== undefined) { + // New object format like {value: 1, unit: "px"} + numValue = dimValue.value; + } else { + throw new Error(`Invalid dimension value for ${property}: ${JSON.stringify(dimValue)}`); + } + + return { + name: `${name}/${property}`, + value: numValue, + type: 'FLOAT', + scopes: ['EFFECT_FLOAT'], + mode, + collection, + group, + }; + } const {attributes} = token const {mode, collection, group} = attributes || {} diff --git a/src/tokens/component/avatar.json5 b/src/tokens/component/avatar.json5 index d31b8c65c..ce911ba61 100644 --- a/src/tokens/component/avatar.json5 +++ b/src/tokens/component/avatar.json5 @@ -43,10 +43,10 @@ { color: '{base.color.neutral.0}', alpha: 0.8, - offsetX: '0px', - offsetY: '0px', - blur: '0px', - spread: '2px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 0, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 2, unit: 'px' }, }, ], $type: 'shadow', @@ -61,10 +61,10 @@ { color: '{base.color.neutral.1}', alpha: 1, - offsetX: '0px', - offsetY: '0px', - blur: '0px', - spread: '2px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 0, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 2, unit: 'px' }, }, ], }, diff --git a/src/tokens/component/button.json5 b/src/tokens/component/button.json5 index bd2e910a0..0b7d73f81 100644 --- a/src/tokens/component/button.json5 +++ b/src/tokens/component/button.json5 @@ -142,10 +142,10 @@ { color: '{base.color.neutral.13}', alpha: 0.04, - offsetX: '0px', - offsetY: '1px', - blur: '0px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 1, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 0, unit: 'px' }, inset: false, }, ], @@ -161,10 +161,10 @@ { color: '{base.color.transparent}', alpha: 0, - offsetX: '0px', - offsetY: '0px', - blur: '0px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 0, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 0, unit: 'px' }, inset: false, }, ], @@ -485,10 +485,10 @@ { color: '{base.color.green.9}', alpha: 0.3, - offsetX: '0px', - offsetY: '1px', - blur: '0px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 1, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 0, unit: 'px' }, inset: true, }, ], @@ -504,10 +504,10 @@ { color: '{base.color.blue.9}', alpha: 0.3, - offsetX: '0px', - offsetY: '1px', - blur: '0px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 1, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 0, unit: 'px' }, inset: true, }, ], @@ -517,10 +517,10 @@ { color: '{base.color.blue.9}', alpha: 0.3, - offsetX: '0px', - offsetY: '1px', - blur: '0px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 1, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 0, unit: 'px' }, inset: true, }, ], @@ -530,10 +530,10 @@ { color: '{base.color.transparent}', alpha: 0, - offsetX: '0px', - offsetY: '0px', - blur: '0px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 0, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 0, unit: 'px' }, inset: false, }, ], @@ -988,10 +988,10 @@ { color: '{base.color.blue.9}', alpha: 0.2, - offsetX: '0px', - offsetY: '1px', - blur: '0px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 1, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 0, unit: 'px' }, inset: true, }, ], @@ -1007,10 +1007,10 @@ { color: '{base.color.transparent}', alpha: 0, - offsetX: '0px', - offsetY: '0px', - blur: '0px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 0, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 0, unit: 'px' }, inset: false, }, ], @@ -1299,10 +1299,10 @@ { color: '{base.color.red.9}', alpha: 0.2, - offsetX: '0px', - offsetY: '1px', - blur: '0px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 1, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 0, unit: 'px' }, inset: true, }, ], @@ -1318,10 +1318,10 @@ { color: '{base.color.orange.9}', alpha: 0.2, - offsetX: '0px', - offsetY: '1px', - blur: '0px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 1, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 0, unit: 'px' }, inset: true, }, ], @@ -1331,10 +1331,10 @@ { color: '{base.color.transparent}', alpha: 0, - offsetX: '0px', - offsetY: '0px', - blur: '0px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 0, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 0, unit: 'px' }, inset: false, }, ], diff --git a/src/tokens/functional/border/border.json5 b/src/tokens/functional/border/border.json5 index c904d9132..e011990d5 100644 --- a/src/tokens/functional/border/border.json5 +++ b/src/tokens/functional/border/border.json5 @@ -4,7 +4,7 @@ $value: { color: '{focus.outlineColor}', style: 'solid', - width: '2px', + width: { value: 2, unit: 'px' }, }, $type: 'border', }, diff --git a/src/tokens/functional/shadow/shadow.json5 b/src/tokens/functional/shadow/shadow.json5 index cee07738d..8d2db2c17 100644 --- a/src/tokens/functional/shadow/shadow.json5 +++ b/src/tokens/functional/shadow/shadow.json5 @@ -4,10 +4,10 @@ $value: { color: '{base.color.neutral.13}', alpha: 0.04, - offsetX: '0px', - offsetY: '1px', - blur: '0px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 1, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 0, unit: 'px' }, inset: true, }, $type: 'shadow', @@ -21,10 +21,10 @@ $value: { color: '{base.color.neutral.0}', alpha: 0.24, - offsetX: '0px', - offsetY: '1px', - blur: '0px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 1, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 0, unit: 'px' }, inset: true, }, }, @@ -36,10 +36,10 @@ $value: { color: '{base.color.neutral.13}', alpha: 0.06, - offsetX: '0px', - offsetY: '1px', - blur: '1px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 1, unit: 'px' }, + blur: { value: 1, unit: 'px' }, + spread: { value: 0, unit: 'px' }, inset: false, }, $type: 'shadow', @@ -53,10 +53,10 @@ $value: { color: '{base.color.neutral.0}', alpha: 0.8, - offsetX: '0px', - offsetY: '1px', - blur: '1px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 1, unit: 'px' }, + blur: { value: 1, unit: 'px' }, + spread: { value: 0, unit: 'px' }, inset: false, }, }, @@ -68,19 +68,19 @@ { color: '{base.color.neutral.13}', alpha: 0.06, - offsetX: '0px', - offsetY: '1px', - blur: '1px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 1, unit: 'px' }, + blur: { value: 1, unit: 'px' }, + spread: { value: 0, unit: 'px' }, inset: false, }, { color: '{base.color.neutral.13}', alpha: 0.06, - offsetX: '0px', - offsetY: '1px', - blur: '3px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 1, unit: 'px' }, + blur: { value: 3, unit: 'px' }, + spread: { value: 0, unit: 'px' }, inset: false, }, ], @@ -96,19 +96,19 @@ { color: '{base.color.neutral.0}', alpha: 0.6, - offsetX: '0px', - offsetY: '1px', - blur: '1px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 1, unit: 'px' }, + blur: { value: 1, unit: 'px' }, + spread: { value: 0, unit: 'px' }, inset: false, }, { color: '{base.color.neutral.0}', alpha: 0.6, - offsetX: '0px', - offsetY: '1px', - blur: '3px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 1, unit: 'px' }, + blur: { value: 3, unit: 'px' }, + spread: { value: 0, unit: 'px' }, inset: false, }, ], @@ -121,18 +121,18 @@ { color: '{base.color.neutral.12}', alpha: 0.1, - offsetX: '0px', - offsetY: '1px', - blur: '1px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 1, unit: 'px' }, + blur: { value: 1, unit: 'px' }, + spread: { value: 0, unit: 'px' }, }, { color: '{base.color.neutral.12}', alpha: 0.12, - offsetX: '0px', - offsetY: '3px', - blur: '6px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 3, unit: 'px' }, + blur: { value: 6, unit: 'px' }, + spread: { value: 0, unit: 'px' }, }, ], $type: 'shadow', @@ -147,18 +147,18 @@ { color: '{base.color.neutral.0}', alpha: 0.4, - offsetX: '0px', - offsetY: '1px', - blur: '1px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 1, unit: 'px' }, + blur: { value: 1, unit: 'px' }, + spread: { value: 0, unit: 'px' }, }, { color: '{base.color.neutral.0}', alpha: 0.8, - offsetX: '0px', - offsetY: '3px', - blur: '6px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 3, unit: 'px' }, + blur: { value: 6, unit: 'px' }, + spread: { value: 0, unit: 'px' }, }, ], }, @@ -172,26 +172,26 @@ { color: '{overlay.borderColor}', alpha: 0.5, - offsetX: '0px', - offsetY: '0px', - blur: '0px', - spread: '1px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 0, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 1, unit: 'px' }, }, { color: '{base.color.neutral.12}', alpha: 0.04, - offsetX: '0px', - offsetY: '6px', - blur: '12px', - spread: '-3px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 6, unit: 'px' }, + blur: { value: 12, unit: 'px' }, + spread: { value: -3, unit: 'px' }, }, { color: '{base.color.neutral.12}', alpha: 0.12, - offsetX: '0px', - offsetY: '6px', - blur: '18px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 6, unit: 'px' }, + blur: { value: 18, unit: 'px' }, + spread: { value: 0, unit: 'px' }, }, ], $type: 'shadow', @@ -206,26 +206,26 @@ { color: '{overlay.borderColor}', alpha: 1, - offsetX: '0px', - offsetY: '0px', - blur: '0px', - spread: '1px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 0, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 1, unit: 'px' }, }, { color: '{base.color.neutral.0}', alpha: 0.4, - offsetX: '0px', - offsetY: '6px', - blur: '12px', - spread: '-3px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 6, unit: 'px' }, + blur: { value: 12, unit: 'px' }, + spread: { value: -3, unit: 'px' }, }, { color: '{base.color.neutral.0}', alpha: 0.4, - offsetX: '0px', - offsetY: '6px', - blur: '18px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 6, unit: 'px' }, + blur: { value: 18, unit: 'px' }, + spread: { value: 0, unit: 'px' }, }, ], }, @@ -237,42 +237,42 @@ { color: '{overlay.borderColor}', alpha: 0, - offsetX: '0px', - offsetY: '0px', - blur: '0px', - spread: '1px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 0, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 1, unit: 'px' }, }, { color: '{base.color.neutral.12}', alpha: 0.08, - offsetX: '0px', - offsetY: '8px', - blur: '16px', - spread: '-4px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 8, unit: 'px' }, + blur: { value: 16, unit: 'px' }, + spread: { value: -4, unit: 'px' }, }, { color: '{base.color.neutral.12}', alpha: 0.08, - offsetX: '0px', - offsetY: '4px', - blur: '32px', - spread: '-4px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 4, unit: 'px' }, + blur: { value: 32, unit: 'px' }, + spread: { value: -4, unit: 'px' }, }, { color: '{base.color.neutral.12}', alpha: 0.08, - offsetX: '0px', - offsetY: '24px', - blur: '48px', - spread: '-12px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 24, unit: 'px' }, + blur: { value: 48, unit: 'px' }, + spread: { value: -12, unit: 'px' }, }, { color: '{base.color.neutral.12}', alpha: 0.08, - offsetX: '0px', - offsetY: '48px', - blur: '96px', - spread: '-24px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 48, unit: 'px' }, + blur: { value: 96, unit: 'px' }, + spread: { value: -24, unit: 'px' }, }, ], $type: 'shadow', @@ -287,42 +287,42 @@ { color: '{overlay.borderColor}', alpha: 1, - offsetX: '0px', - offsetY: '0px', - blur: '0px', - spread: '1px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 0, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 1, unit: 'px' }, }, { color: '{base.color.neutral.0}', alpha: 0.4, - offsetX: '0px', - offsetY: '8px', - blur: '16px', - spread: '-4px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 8, unit: 'px' }, + blur: { value: 16, unit: 'px' }, + spread: { value: -4, unit: 'px' }, }, { color: '{base.color.neutral.0}', alpha: 0.4, - offsetX: '0px', - offsetY: '4px', - blur: '32px', - spread: '-4px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 4, unit: 'px' }, + blur: { value: 32, unit: 'px' }, + spread: { value: -4, unit: 'px' }, }, { color: '{base.color.neutral.0}', alpha: 0.4, - offsetX: '0px', - offsetY: '24px', - blur: '48px', - spread: '-12px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 24, unit: 'px' }, + blur: { value: 48, unit: 'px' }, + spread: { value: -12, unit: 'px' }, }, { color: '{base.color.neutral.0}', alpha: 0.4, - offsetX: '0px', - offsetY: '48px', - blur: '96px', - spread: '-24px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 48, unit: 'px' }, + blur: { value: 96, unit: 'px' }, + spread: { value: -24, unit: 'px' }, }, ], }, @@ -334,18 +334,18 @@ { color: '{overlay.borderColor}', alpha: 0, - offsetX: '0px', - offsetY: '0px', - blur: '0px', - spread: '1px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 0, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 1, unit: 'px' }, }, { color: '{base.color.neutral.12}', alpha: 0.24, - offsetX: '0px', - offsetY: '40px', - blur: '80px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 40, unit: 'px' }, + blur: { value: 80, unit: 'px' }, + spread: { value: 0, unit: 'px' }, }, ], $type: 'shadow', @@ -360,18 +360,18 @@ { color: '{overlay.borderColor}', alpha: 1, - offsetX: '0px', - offsetY: '0px', - blur: '0px', - spread: '1px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 0, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 1, unit: 'px' }, }, { color: '{base.color.neutral.0}', alpha: 1, - offsetX: '0px', - offsetY: '24px', - blur: '48px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 24, unit: 'px' }, + blur: { value: 48, unit: 'px' }, + spread: { value: 0, unit: 'px' }, }, ], }, @@ -383,18 +383,18 @@ { color: '{overlay.borderColor}', alpha: 0, - offsetX: '0px', - offsetY: '0px', - blur: '0px', - spread: '1px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 0, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 1, unit: 'px' }, }, { color: '{base.color.neutral.12}', alpha: 0.32, - offsetX: '0px', - offsetY: '56px', - blur: '112px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 56, unit: 'px' }, + blur: { value: 112, unit: 'px' }, + spread: { value: 0, unit: 'px' }, }, ], $type: 'shadow', @@ -409,18 +409,18 @@ { color: '{overlay.borderColor}', alpha: 1, - offsetX: '0px', - offsetY: '0px', - blur: '0px', - spread: '1px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 0, unit: 'px' }, + blur: { value: 0, unit: 'px' }, + spread: { value: 1, unit: 'px' }, }, { color: '{base.color.neutral.0}', alpha: 1, - offsetX: '0px', - offsetY: '32px', - blur: '64px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 32, unit: 'px' }, + blur: { value: 64, unit: 'px' }, + spread: { value: 0, unit: 'px' }, }, ], }, @@ -432,18 +432,18 @@ { color: '{base.color.neutral.12}', alpha: 0.04, - offsetX: '0px', - offsetY: '6px', - blur: '12px', - spread: '-3px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 6, unit: 'px' }, + blur: { value: 12, unit: 'px' }, + spread: { value: -3, unit: 'px' }, }, { color: '{base.color.neutral.12}', alpha: 0.12, - offsetX: '0px', - offsetY: '6px', - blur: '18px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 6, unit: 'px' }, + blur: { value: 18, unit: 'px' }, + spread: { value: 0, unit: 'px' }, }, ], $type: 'shadow', @@ -455,18 +455,18 @@ { color: '{base.color.neutral.0}', alpha: 0.4, - offsetX: '0px', - offsetY: '6px', - blur: '12px', - spread: '-3px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 6, unit: 'px' }, + blur: { value: 12, unit: 'px' }, + spread: { value: -3, unit: 'px' }, }, { color: '{base.color.neutral.0}', alpha: 0.4, - offsetX: '0px', - offsetY: '6px', - blur: '18px', - spread: '0px', + offsetX: { value: 0, unit: 'px' }, + offsetY: { value: 6, unit: 'px' }, + blur: { value: 18, unit: 'px' }, + spread: { value: 0, unit: 'px' }, }, ], }, diff --git a/src/tokens/functional/size/border.json5 b/src/tokens/functional/size/border.json5 index 37700f6cf..a147c9b95 100644 --- a/src/tokens/functional/size/border.json5 +++ b/src/tokens/functional/size/border.json5 @@ -26,7 +26,7 @@ }, }, thin: { - $value: '1px', + $value: { value: 1, unit: 'px' }, $type: 'dimension', $extensions: { 'org.primer.figma': { @@ -36,7 +36,7 @@ }, }, thick: { - $value: '2px', + $value: { value: 2, unit: 'px' }, $type: 'dimension', $extensions: { 'org.primer.figma': { @@ -46,7 +46,7 @@ }, }, thicker: { - $value: '4px', + $value: { value: 4, unit: 'px' }, $type: 'dimension', $extensions: { 'org.primer.figma': { @@ -58,7 +58,7 @@ }, borderRadius: { small: { - $value: '3px', + $value: { value: 3, unit: 'px' }, $type: 'dimension', $extensions: { 'org.primer.figma': { @@ -71,7 +71,7 @@ }, }, medium: { - $value: '6px', + $value: { value: 6, unit: 'px' }, $type: 'dimension', $extensions: { 'org.primer.figma': { @@ -84,7 +84,7 @@ }, }, large: { - $value: '12px', + $value: { value: 12, unit: 'px' }, $type: 'dimension', $extensions: { 'org.primer.figma': { @@ -97,7 +97,7 @@ }, }, full: { - $value: '9999px', + $value: { value: 9999, unit: 'px' }, $type: 'dimension', $description: 'Use this border radius for pill shaped elements', $extensions: { @@ -127,7 +127,7 @@ outline: { focus: { offset: { - $value: '-2px', + $value: { value: -2, unit: 'px' }, $type: 'dimension', $extensions: { 'org.primer.figma': { @@ -137,7 +137,7 @@ }, }, width: { - $value: '2px', + $value: { value: 2, unit: 'px' }, $type: 'dimension', $extensions: { 'org.primer.figma': { From eb62491fa397dc87cc60eeffa5634b4a9e59e3e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Jul 2025 12:41:38 +0000 Subject: [PATCH 08/11] Fix linting errors in jsonFigma formatter Co-authored-by: lukasoppermann <813754+lukasoppermann@users.noreply.github.com> --- src/formats/jsonFigma.ts | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/src/formats/jsonFigma.ts b/src/formats/jsonFigma.ts index b34090a91..0a266ed1a 100644 --- a/src/formats/jsonFigma.ts +++ b/src/formats/jsonFigma.ts @@ -7,8 +7,8 @@ import type {RgbaFloat} from '../transformers/utilities/isRgbaFloat.js' import {isRgbaFloat} from '../transformers/utilities/isRgbaFloat.js' import {getReferences, sortByReference} from 'style-dictionary/utils' -// Type for dimension value that can be either string (legacy) or object (new format) -type DimensionValue = string | { value: number; unit: string } +// Type for dimension value in new W3C object format +type DimensionValue = {value: number; unit: string} const isReference = (string: string): boolean => /^\{([^\\]*)\}$/g.test(string) @@ -36,29 +36,20 @@ const getFigmaType = (type: string): string => { const shadowToVariables = ( name: string, values: Omit & { - color: string | RgbaFloat; - offsetX: DimensionValue; - offsetY: DimensionValue; - blur: DimensionValue; - spread: DimensionValue; + color: string | RgbaFloat + offsetX: DimensionValue + offsetY: DimensionValue + blur: DimensionValue + spread: DimensionValue }, token: TransformedToken, ) => { // floatValue const floatValue = (property: 'offsetX' | 'offsetY' | 'blur' | 'spread') => { - const dimValue = values[property]; - let numValue: number; - - if (typeof dimValue === 'string') { - // Legacy string format like "1px" - numValue = parseInt(dimValue.replace('px', '')); - } else if (typeof dimValue === 'object' && dimValue.value !== undefined) { - // New object format like {value: 1, unit: "px"} - numValue = dimValue.value; - } else { - throw new Error(`Invalid dimension value for ${property}: ${JSON.stringify(dimValue)}`); - } - + const dimValue = values[property] + // New object format like {value: 1, unit: "px"} + const numValue = dimValue.value + return { name: `${name}/${property}`, value: numValue, @@ -67,7 +58,7 @@ const shadowToVariables = ( mode, collection, group, - }; + } } const {attributes} = token From 26e6da3f3e0e0d264a254dc3b929f597f933c92d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Jul 2025 12:54:06 +0000 Subject: [PATCH 09/11] Fix shadow transformer to use new W3C dimension format - Added dimensionToString utility to convert {value, unit} objects to CSS strings - Updated shadowToCss transformer to handle new dimension format for offsetX, offsetY, blur, and spread - Updated ShadowTokenValue type to support both string and object dimension formats - Updated all shadow transformer tests to use new format while maintaining backward compatibility - All tests pass and build completes successfully Co-authored-by: lukasoppermann <813754+lukasoppermann@users.noreply.github.com> --- src/transformers/shadowToCss.test.ts | 110 ++++++++++-------- src/transformers/shadowToCss.ts | 9 +- .../utilities/dimensionToString.test.ts | 25 ++++ .../utilities/dimensionToString.ts | 19 +++ src/types/shadowTokenValue.d.ts | 8 +- 5 files changed, 117 insertions(+), 54 deletions(-) create mode 100644 src/transformers/utilities/dimensionToString.test.ts create mode 100644 src/transformers/utilities/dimensionToString.ts diff --git a/src/transformers/shadowToCss.test.ts b/src/transformers/shadowToCss.test.ts index a738b0e88..1eaac14ff 100644 --- a/src/transformers/shadowToCss.test.ts +++ b/src/transformers/shadowToCss.test.ts @@ -7,14 +7,14 @@ describe('Transformer: shadowToCss', () => { getMockToken({ $value: { color: '#000000', - offsetX: '0px', - offsetY: '2px', - blur: '1px', - spread: '0', + offsetX: {value: 0, unit: 'px'}, + offsetY: {value: 2, unit: 'px'}, + blur: {value: 1, unit: 'px'}, + spread: {value: 0, unit: 'px'}, }, }), ] - const expectedOutput = ['0px 2px 1px 0 #000000'] + const expectedOutput = ['0px 2px 1px 0px #000000'] expect(input.map(item => shadowToCss.transform(item, {}, {}))).toStrictEqual(expectedOutput) }) @@ -23,20 +23,20 @@ describe('Transformer: shadowToCss', () => { getMockToken({ $value: { color: '#000000', - offsetX: '0px', - offsetY: '2px', - blur: '1px', - spread: '0px', + offsetX: {value: 0, unit: 'px'}, + offsetY: {value: 2, unit: 'px'}, + blur: {value: 1, unit: 'px'}, + spread: {value: 0, unit: 'px'}, inset: true, }, }), getMockToken({ $value: { color: '#000000', - offsetX: '0px', - offsetY: '2px', - blur: '1px', - spread: '0px', + offsetX: {value: 0, unit: 'px'}, + offsetY: {value: 2, unit: 'px'}, + blur: {value: 1, unit: 'px'}, + spread: {value: 0, unit: 'px'}, inset: false, }, }), @@ -52,9 +52,9 @@ describe('Transformer: shadowToCss', () => { getMockToken({ $value: { color: '#000000', - offsetX: '2px', - offsetY: '2px', - blur: '1px', + offsetX: {value: 2, unit: 'px'}, + offsetY: {value: 2, unit: 'px'}, + spread: {value: 0, unit: 'px'}, }, }), {}, @@ -68,9 +68,9 @@ describe('Transformer: shadowToCss', () => { getMockToken({ $value: { color: '#000000', - offsetX: '2px', - offsetY: '2px', - blur: '1px', + offsetX: {value: 2, unit: 'px'}, + offsetY: {value: 2, unit: 'px'}, + blur: {value: 1, unit: 'px'}, }, }), {}, @@ -84,9 +84,9 @@ describe('Transformer: shadowToCss', () => { getMockToken({ $value: { color: '#000000', - offsetX: '2px', - spread: '0px', - blur: '1px', + offsetX: {value: 2, unit: 'px'}, + spread: {value: 0, unit: 'px'}, + blur: {value: 1, unit: 'px'}, }, }), {}, @@ -99,9 +99,9 @@ describe('Transformer: shadowToCss', () => { getMockToken({ $value: { color: '#000000', - offsetY: '2px', - spread: '0px', - blur: '1px', + offsetY: {value: 2, unit: 'px'}, + spread: {value: 0, unit: 'px'}, + blur: {value: 1, unit: 'px'}, }, }), {}, @@ -113,10 +113,10 @@ describe('Transformer: shadowToCss', () => { shadowToCss.transform( getMockToken({ $value: { - offsetX: '0px', - offsetY: '2px', - spread: '0px', - blur: '1px', + offsetX: {value: 0, unit: 'px'}, + offsetY: {value: 2, unit: 'px'}, + spread: {value: 0, unit: 'px'}, + blur: {value: 1, unit: 'px'}, }, }), {}, @@ -130,25 +130,25 @@ describe('Transformer: shadowToCss', () => { getMockToken({ $value: { color: '#000000', - offsetX: '0px', - offsetY: '2px', - blur: '1px', - spread: '0', + offsetX: {value: 0, unit: 'px'}, + offsetY: {value: 2, unit: 'px'}, + blur: {value: 1, unit: 'px'}, + spread: {value: 0, unit: 'px'}, alpha: 0.5, }, }), getMockToken({ $value: { color: '#22222266', - offsetX: '0px', - offsetY: '2px', - blur: '1px', - spread: '0', + offsetX: {value: 0, unit: 'px'}, + offsetY: {value: 2, unit: 'px'}, + blur: {value: 1, unit: 'px'}, + spread: {value: 0, unit: 'px'}, alpha: 0.5, }, }), ] - const expectedOutput = ['0px 2px 1px 0 #00000080', '0px 2px 1px 0 #22222280'] + const expectedOutput = ['0px 2px 1px 0px #00000080', '0px 2px 1px 0px #22222280'] expect(input.map(item => shadowToCss.transform(item, {}, {}))).toStrictEqual(expectedOutput) }) @@ -157,24 +157,40 @@ describe('Transformer: shadowToCss', () => { $value: [ { color: '#000000', - offsetX: '0px', - offsetY: '2px', - blur: '1px', - spread: '0', + offsetX: {value: 0, unit: 'px'}, + offsetY: {value: 2, unit: 'px'}, + blur: {value: 1, unit: 'px'}, + spread: {value: 0, unit: 'px'}, alpha: 0.5, }, { color: '#22222266', - offsetX: '0px', - offsetY: '8px', - blur: '16px', - spread: '0', + offsetX: {value: 0, unit: 'px'}, + offsetY: {value: 8, unit: 'px'}, + blur: {value: 16, unit: 'px'}, + spread: {value: 0, unit: 'px'}, alpha: 0.2, }, ], }) - const expectedOutput = '0px 2px 1px 0 #00000080, 0px 8px 16px 0 #22222233' + const expectedOutput = '0px 2px 1px 0px #00000080, 0px 8px 16px 0px #22222233' expect(shadowToCss.transform(item, {}, {})).toStrictEqual(expectedOutput) }) + + it('maintains backward compatibility with string dimension values', () => { + const input = [ + getMockToken({ + $value: { + color: '#000000', + offsetX: '0px', + offsetY: '2px', + blur: '1px', + spread: '0px', + }, + }), + ] + const expectedOutput = ['0px 2px 1px 0px #000000'] + expect(input.map(item => shadowToCss.transform(item, {}, {}))).toStrictEqual(expectedOutput) + }) }) diff --git a/src/transformers/shadowToCss.ts b/src/transformers/shadowToCss.ts index e7ac6a1f0..eac66eb66 100644 --- a/src/transformers/shadowToCss.ts +++ b/src/transformers/shadowToCss.ts @@ -2,6 +2,7 @@ import {toHex} from 'color2k' import {isShadow} from '../filters/index.js' import {alpha} from './utilities/alpha.js' import {checkRequiredTokenProperties} from './utilities/checkRequiredTokenProperties.js' +import {dimensionToString} from './utilities/dimensionToString.js' import type {ShadowTokenValue} from '../types/shadowTokenValue.js' import {getTokenValue} from './utilities/getTokenValue.js' import type {PlatformConfig, Transform, TransformedToken} from 'style-dictionary/types' @@ -30,9 +31,11 @@ export const shadowToCss: Transform = { if (typeof shadow === 'string') return shadow checkRequiredTokenProperties(shadow, ['color', 'offsetX', 'offsetY', 'blur', 'spread']) /*css box shadow: inset? | offset-x | offset-y | blur-radius | spread-radius | color */ - return `${shadow.inset === true ? 'inset ' : ''}${shadow.offsetX} ${shadow.offsetY} ${shadow.blur} ${ - shadow.spread - } ${toHex(alpha(getTokenValue({...token, ...{[valueProp]: shadow}}, 'color'), shadow.alpha || 1, token, config))}` + return `${shadow.inset === true ? 'inset ' : ''}${dimensionToString(shadow.offsetX)} ${dimensionToString( + shadow.offsetY, + )} ${dimensionToString(shadow.blur)} ${dimensionToString( + shadow.spread, + )} ${toHex(alpha(getTokenValue({...token, ...{[valueProp]: shadow}}, 'color'), shadow.alpha || 1, token, config))}` }) .join(', ') }, diff --git a/src/transformers/utilities/dimensionToString.test.ts b/src/transformers/utilities/dimensionToString.test.ts new file mode 100644 index 000000000..12330a23e --- /dev/null +++ b/src/transformers/utilities/dimensionToString.test.ts @@ -0,0 +1,25 @@ +import {dimensionToString} from './dimensionToString.js' + +describe('Utility: dimensionToString', () => { + it('converts dimension object to string', () => { + expect(dimensionToString({value: 0, unit: 'px'})).toBe('0px') + expect(dimensionToString({value: 1, unit: 'px'})).toBe('1px') + expect(dimensionToString({value: 16, unit: 'px'})).toBe('16px') + expect(dimensionToString({value: 1.5, unit: 'rem'})).toBe('1.5rem') + expect(dimensionToString({value: 0, unit: 'em'})).toBe('0em') + }) + + it('handles string values for backward compatibility', () => { + expect(dimensionToString('0px')).toBe('0px') + expect(dimensionToString('1px')).toBe('1px') + expect(dimensionToString('16px')).toBe('16px') + expect(dimensionToString('1.5rem')).toBe('1.5rem') + }) + + it('throws error for invalid dimension values', () => { + expect(() => dimensionToString({value: 1} as {value: number; unit: string})).toThrowError() + expect(() => dimensionToString({unit: 'px'} as {value: number; unit: string})).toThrowError() + expect(() => dimensionToString(123 as unknown as string)).toThrowError() + expect(() => dimensionToString(null as unknown as string)).toThrowError() + }) +}) diff --git a/src/transformers/utilities/dimensionToString.ts b/src/transformers/utilities/dimensionToString.ts new file mode 100644 index 000000000..4ebd67838 --- /dev/null +++ b/src/transformers/utilities/dimensionToString.ts @@ -0,0 +1,19 @@ +/** + * Converts a dimension object with value and unit to a CSS string + * @param dimension - The dimension object with value and unit properties + * @returns CSS dimension string (e.g., "0px", "1rem") + */ +export const dimensionToString = (dimension: {value: number; unit: string} | string): string => { + // If it's already a string, return as is (backward compatibility) + if (typeof dimension === 'string') { + return dimension + } + + // Handle the new object format + if (typeof dimension === 'object' && 'value' in dimension && 'unit' in dimension) { + const {value, unit} = dimension + return `${value}${unit}` + } + + throw new Error(`Invalid dimension value: ${JSON.stringify(dimension)}`) +} diff --git a/src/types/shadowTokenValue.d.ts b/src/types/shadowTokenValue.d.ts index 89dbb5a57..9108c2696 100644 --- a/src/types/shadowTokenValue.d.ts +++ b/src/types/shadowTokenValue.d.ts @@ -4,10 +4,10 @@ */ export type ShadowTokenValue = { color: string - offsetX: string - offsetY: string - blur: string - spread: string + offsetX: string | {value: number; unit: string} + offsetY: string | {value: number; unit: string} + blur: string | {value: number; unit: string} + spread: string | {value: number; unit: string} // custom non w3c values inset?: boolean alpha?: number From 9ea3162699aa8da451c1d13c545825bdb7c05055 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Jul 2025 13:33:37 +0000 Subject: [PATCH 10/11] Fix border transformer to work with new W3C dimension format and ensure px values in styleLint output Co-authored-by: lukasoppermann <813754+lukasoppermann@users.noreply.github.com> --- package-lock.json | 4 +- src/transformers/borderToCss.test.ts | 59 ++++++++++++++++++++++++++-- src/transformers/borderToCss.ts | 31 ++++++++++++++- src/types/borderTokenValue.d.ts | 2 +- 4 files changed, 88 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4590a6716..be1d13e1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@primer/primitives", - "version": "10.7.0", + "version": "11.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@primer/primitives", - "version": "10.7.0", + "version": "11.0.0", "license": "MIT", "devDependencies": { "@actions/core": "^1.11.1", diff --git a/src/transformers/borderToCss.test.ts b/src/transformers/borderToCss.test.ts index 47be3348a..134523dcd 100644 --- a/src/transformers/borderToCss.test.ts +++ b/src/transformers/borderToCss.test.ts @@ -2,7 +2,7 @@ import {getMockToken} from '../test-utilities/index.js' import {borderToCss} from './borderToCss.js' describe('Transformer: borderToCss', () => { - it('transforms `border` token to css border string', () => { + it('transforms `border` token to css border string with string width', () => { const input = getMockToken({ $value: { color: '#000000', @@ -15,8 +15,43 @@ describe('Transformer: borderToCss', () => { expect(borderToCss.transform(input, {}, {})).toStrictEqual(expectedOutput) }) + it('transforms `border` token to css border string with dimension object width', () => { + const input = getMockToken({ + $value: { + color: '#000000', + style: 'solid', + width: {value: 2, unit: 'px'}, + }, + }) + + const expectedOutput = '2px solid #000000' + expect(borderToCss.transform(input, {}, {})).toStrictEqual(expectedOutput) + }) + + it('transforms `border` token to css border string with array width from dimension/remPxArray', () => { + const input = getMockToken({ + $value: { + color: '#000000', + style: 'solid', + width: ['0.125rem', '2px'], // Array from dimension/remPxArray transformer + }, + }) + + const expectedOutput = '2px solid #000000' // Should use px value for styleLint + expect(borderToCss.transform(input, {}, {})).toStrictEqual(expectedOutput) + }) + + it('returns already transformed string values as-is', () => { + const input = getMockToken({ + $value: '1px solid #000000', + }) + + const expectedOutput = '1px solid #000000' + expect(borderToCss.transform(input, {}, {})).toStrictEqual(expectedOutput) + }) + it('throws an error when required values are missing', () => { - // missing blur + // missing width expect(() => borderToCss.transform( getMockToken({ @@ -30,7 +65,7 @@ describe('Transformer: borderToCss', () => { ), ).toThrowError() - // missing spread + // missing style expect(() => borderToCss.transform( getMockToken({ @@ -44,7 +79,7 @@ describe('Transformer: borderToCss', () => { ), ).toThrowError() - // missing offsets + // missing color expect(() => borderToCss.transform( getMockToken({ @@ -58,4 +93,20 @@ describe('Transformer: borderToCss', () => { ), ).toThrowError() }) + + it('throws an error for invalid width values', () => { + expect(() => + borderToCss.transform( + getMockToken({ + $value: { + color: '#000000', + style: 'solid', + width: 123, // Invalid: number instead of string or object + }, + }), + {}, + {}, + ), + ).toThrowError('Invalid width value') + }) }) diff --git a/src/transformers/borderToCss.ts b/src/transformers/borderToCss.ts index 9c0edf635..7fe8d519b 100644 --- a/src/transformers/borderToCss.ts +++ b/src/transformers/borderToCss.ts @@ -1,6 +1,7 @@ import type {Transform, TransformedToken} from 'style-dictionary/types' import {isBorder} from '../filters/isBorder.js' import type {BorderTokenValue} from '../types/borderTokenValue.js' +import {dimensionToString} from './utilities/dimensionToString.js' /** * checks if all required properties exist on shadow token @@ -13,6 +14,31 @@ const checkForBorderTokenProperties = (border: Record): border } return false } + +/** + * Converts width value to string, handling different formats + * @param width - The width value (string, dimension object, or array from dimension/remPxArray) + * @returns CSS width string + */ +const getWidthString = (width: unknown): string => { + // If it's already a string, return as is + if (typeof width === 'string') { + return width + } + + // If it's an array from dimension/remPxArray transformer, use the px value (second element) + if (Array.isArray(width) && width.length === 2) { + return width[1] // Return the px value for styleLint compatibility + } + + // If it's a dimension object, convert to string + if (typeof width === 'object' && width !== null && 'value' in width && 'unit' in width) { + return dimensionToString(width as {value: number; unit: string}) + } + + throw new Error(`Invalid width value: ${JSON.stringify(width)}`) +} + /** * @description converts w3c border tokens in css border string * @type valueTransformer — [StyleDictionary.ValueTransform](https://github.com/amzn/style-dictionary/blob/main/types/Transform.d.ts) @@ -36,7 +62,10 @@ export const borderToCss: Transform = { `Invalid border token property ${JSON.stringify(value)}. Must be an object with color, width and style properties.`, ) } + + const widthString = getWidthString(value.width) + /* width | style | color */ - return `${value.width} ${value.style} ${value.color}` + return `${widthString} ${value.style} ${value.color}` }, } diff --git a/src/types/borderTokenValue.d.ts b/src/types/borderTokenValue.d.ts index 4b2f75625..f6838e9e9 100644 --- a/src/types/borderTokenValue.d.ts +++ b/src/types/borderTokenValue.d.ts @@ -5,6 +5,6 @@ export type StrokeStyleString = 'solid' | 'dashed' | 'dotted' | 'double' | 'groove' | 'ridge' | 'outset' | 'inset' export type BorderTokenValue = { color: string - width: string + width: string | {value: number; unit: string} | [string, string] // Support string, dimension object, or array from transformers style: StrokeStyleString } From fc9406fb6b53c0c0bc0ab8719e7c7e4e26b33471 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 9 Jul 2025 14:47:43 +0000 Subject: [PATCH 11/11] Fix styleLint output for all dimension values to be pixel strings instead of arrays Co-authored-by: lukasoppermann <813754+lukasoppermann@users.noreply.github.com> --- src/platforms/styleLint.ts | 2 +- src/primerStyleDictionary.ts | 3 + src/transformers/dimensionToPixel.test.ts | 78 +++++++++++++++++++++++ src/transformers/dimensionToPixel.ts | 57 +++++++++++++++++ src/transformers/index.ts | 1 + 5 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 src/transformers/dimensionToPixel.test.ts create mode 100644 src/transformers/dimensionToPixel.ts diff --git a/src/platforms/styleLint.ts b/src/platforms/styleLint.ts index 212e3fa1d..af77bb6dc 100644 --- a/src/platforms/styleLint.ts +++ b/src/platforms/styleLint.ts @@ -9,7 +9,7 @@ export const styleLint: PlatformInitializer = (outputFile, prefix, buildPath, op transforms: [ 'name/pathToKebabCase', 'color/hex', - 'dimension/remPxArray', + 'dimension/pixel', 'shadow/css', 'border/css', 'typography/css', diff --git a/src/primerStyleDictionary.ts b/src/primerStyleDictionary.ts index e6dbaabda..541eb8934 100644 --- a/src/primerStyleDictionary.ts +++ b/src/primerStyleDictionary.ts @@ -6,6 +6,7 @@ import { colorToRgbaFloat, cubicBezierToCss, dimensionToRem, + dimensionToPixel, dimensionToPixelUnitless, durationToCss, figmaAttributes, @@ -123,6 +124,8 @@ PrimerStyleDictionary.registerTransform(floatToPixelUnitless) PrimerStyleDictionary.registerTransform(dimensionToRem) +PrimerStyleDictionary.registerTransform(dimensionToPixel) + PrimerStyleDictionary.registerTransform(dimensionToRemPxArray) PrimerStyleDictionary.registerTransform(dimensionToPixelUnitless) diff --git a/src/transformers/dimensionToPixel.test.ts b/src/transformers/dimensionToPixel.test.ts new file mode 100644 index 000000000..5adb289bb --- /dev/null +++ b/src/transformers/dimensionToPixel.test.ts @@ -0,0 +1,78 @@ +import {dimensionToPixel} from './dimensionToPixel.js' +import {getMockToken} from '../test-utilities/index.js' + +describe('Transform: dimensionToPixel', () => { + it('transforms `px` dimension token', () => { + const input = [ + getMockToken({ + value: {value: 16, unit: 'px'}, + }), + ] + const expectedOutput = ['16px'] + expect(input.map(item => dimensionToPixel.transform(item, {basePxFontSize: 16}, {}))).toStrictEqual(expectedOutput) + }) + + it('transforms `rem` dimension token', () => { + const input = [ + getMockToken({ + value: {value: 1, unit: 'rem'}, + }), + ] + const expectedOutput = ['16px'] + expect(input.map(item => dimensionToPixel.transform(item, {basePxFontSize: 16}, {}))).toStrictEqual(expectedOutput) + }) + + it('transforms `em` dimension token', () => { + const input = [ + getMockToken({ + value: {value: 1.5, unit: 'em'}, + }), + ] + const expectedOutput = ['1.5em'] + expect(input.map(item => dimensionToPixel.transform(item, {basePxFontSize: 16}, {}))).toStrictEqual(expectedOutput) + }) + + it('transforms dimension token with `0` value', () => { + const input = [ + getMockToken({ + value: {value: 0, unit: 'px'}, + }), + ] + const expectedOutput = ['0'] + expect(input.map(item => dimensionToPixel.transform(item, {basePxFontSize: 16}, {}))).toStrictEqual(expectedOutput) + }) + + it('transforms dimension token with decimal value', () => { + const input = [ + getMockToken({ + value: {value: 1.5, unit: 'px'}, + }), + ] + const expectedOutput = ['1.5px'] + expect(input.map(item => dimensionToPixel.transform(item, {basePxFontSize: 16}, {}))).toStrictEqual(expectedOutput) + }) + + it('uses custom base font size', () => { + const input = [ + getMockToken({ + value: {value: 1, unit: 'rem'}, + }), + ] + const expectedOutput = ['20px'] + expect(input.map(item => dimensionToPixel.transform(item, {basePxFontSize: 20}, {}))).toStrictEqual(expectedOutput) + }) + + it('throws error for invalid dimension value', () => { + const input = getMockToken({ + value: 'invalid', + }) + expect(() => dimensionToPixel.transform(input, {basePxFontSize: 16}, {})).toThrow('Invalid dimension token') + }) + + it('throws error for unsupported unit', () => { + const input = getMockToken({ + value: {value: 16, unit: 'vh'}, + }) + expect(() => dimensionToPixel.transform(input, {basePxFontSize: 16}, {})).toThrow('Invalid dimension token') + }) +}) diff --git a/src/transformers/dimensionToPixel.ts b/src/transformers/dimensionToPixel.ts new file mode 100644 index 000000000..c68104c7b --- /dev/null +++ b/src/transformers/dimensionToPixel.ts @@ -0,0 +1,57 @@ +import {isDimension} from '../filters/index.js' +import type {Config, PlatformConfig, Transform, TransformedToken} from 'style-dictionary/types' + +/** + * @description base font size from options or 16 + * @param options + * @returns number + */ +const getBasePxFontSize = (options?: PlatformConfig): number => (options && options.basePxFontSize) || 16 + +/** + * @description converts dimension tokens value to `px` string, ignores `em` as they are relative to the font size of the parent element + * @type value transformer — [StyleDictionary.ValueTransform](https://github.com/amzn/style-dictionary/blob/main/types/Transform.d.ts) + * @matcher matches all tokens of $type `dimension` + * @transformer returns a `px` string + */ +export const dimensionToPixel: Transform = { + name: 'dimension/pixel', + type: 'value', + transitive: true, + filter: isDimension, + transform: (token: TransformedToken, config: PlatformConfig, options: Config) => { + const valueProp = options.usesDtcg ? '$value' : 'value' + const baseFont = getBasePxFontSize(config) + const dimensionValue = token[valueProp] as {value: number; unit: string} + + if (typeof dimensionValue !== 'object' || !('value' in dimensionValue) || !('unit' in dimensionValue)) { + throw new Error( + `Invalid dimension token: '${token.name}: ${JSON.stringify(token[valueProp])}' must be an object with value and unit properties \n`, + ) + } + + const {value, unit} = dimensionValue + + if (value === 0) { + return '0' + } + + if (unit === 'px') { + return `${value}px` + } + + if (unit === 'rem') { + // Convert rem to px + return `${value * baseFont}px` + } + + if (unit === 'em') { + // Keep em as is since it's relative + return `${value}em` + } + + throw new Error( + `Invalid dimension token: '${token.name}: ${JSON.stringify(token[valueProp])}' has unsupported unit '${unit}' \n`, + ) + }, +} diff --git a/src/transformers/index.ts b/src/transformers/index.ts index f84f2cfc6..c0aa5c064 100644 --- a/src/transformers/index.ts +++ b/src/transformers/index.ts @@ -8,6 +8,7 @@ export {floatToPixelUnitless} from './floatToPixel.js' export {gradientToCss} from './gradientToCss.js' export {dimensionToRem} from './dimensionToRem.js' export {dimensionToRemPxArray} from './dimensionToRemPxArray.js' +export {dimensionToPixel} from './dimensionToPixel.js' export {dimensionToPixelUnitless} from './dimensionToPixelUnitless.js' export {durationToCss} from './durationToCss.js' export {figmaAttributes} from './figmaAttributes.js'