Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add support for HEX8 in HEXRGBa transform #341

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fair-poems-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tokens-studio/sd-transforms': patch
---

Fix support for HEX8 and shorthand HEX formats in the HEXRGBa transform.
67 changes: 56 additions & 11 deletions src/css/transformHEXRGBa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Color from 'colorjs.io';
import { DesignToken } from 'style-dictionary/types';

/**
* Helper: Transforms hex rgba colors used in figma tokens:
* Helper: Transforms hex to rgba colors used in figma tokens:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Helper: Transforms hex to rgba colors used in figma tokens:
* Helper: Transforms `rgba(hex, alpha)` format which is used in figma tokens, to valid CSS rgba colors:

* rgba(#ffffff, 0.5) =? rgba(255, 255, 255, 0.5).
* This is kind of like an alpha() function.
*/
Expand All @@ -11,17 +11,62 @@ export function transformHEXRGBaForCSS(token: DesignToken): DesignToken['value']
const type = token.$type ?? token.type;
if (val === undefined) return undefined;

const transformHEXRGBa = (val: string) => {
const regex = /rgba\(\s*(?<hex>#.+?)\s*,\s*(?<alpha>\d*(\.\d*|%)*)\s*\)/g;
return val.replace(regex, (match, hex, alpha) => {
try {
const [r, g, b] = new Color(hex).srgb;
return `rgba(${r * 255}, ${g * 255}, ${b * 255}, ${alpha})`;
} catch (e) {
console.warn(`Tried parsing "${hex}" as a hex value, but failed.`);
return match;
const transformHexColor = (hex: string) => {
try {
// Fast path for invalid hex
if (hex.length < 4) return hex;

// Determine format based on length
const hexLength = hex.length - 1; // subtract 1 for #
Comment on lines +16 to +20
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Fast path for invalid hex
if (hex.length < 4) return hex;
// Determine format based on length
const hexLength = hex.length - 1; // subtract 1 for #
// Determine format based on length
const hexLength = hex.length - 1; // subtract 1 for #
// Fast path for invalid hex
if (hexLength < 3) return hex;

This makes more sense, otherwise as a reader of the code I'd be wondering "what about hex3, 3 is less than 4"


// Only transform hex colors with alpha channel
const hasAlpha = hexLength === 4 || hexLength === 8;
if (!hasAlpha) return hex;

let hexColor = hex;
let alpha = '1';

// Convert shorthand to full format if necessary
if (hexLength === 4) {
const r = hex[1],
g = hex[2],
b = hex[3],
a = hex[4];
hexColor = `#${r}${r}${g}${g}${b}${b}`;
alpha = (parseInt(a + a, 16) / 255).toString();
} else if (hexLength === 8) {
alpha = (parseInt(hex.slice(7), 16) / 255).toString();
hexColor = hex.slice(0, 7);
Comment on lines +22 to +39
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be deleted entirely, as Colorjs.io can parse hex3/4/6/8 just fine.

}
});

const [r, g, b] = new Color(hexColor).srgb;
return `rgba(${r * 255}, ${g * 255}, ${b * 255}, ${alpha})`;
Comment on lines +42 to +43
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const [r, g, b] = new Color(hexColor).srgb;
return `rgba(${r * 255}, ${g * 255}, ${b * 255}, ${alpha})`;
const parsed = new Color(hexColor);
const [r, g, b] = parsed.srgb;
const alpha = parsed.alpha;
return `rgba(${r * 255}, ${g * 255}, ${b * 255}, ${alpha})`;

} catch (e) {
return hex;
}
};

const transformHEXRGBa = (val: string) => {
// Handle standalone hex colors
if (val.startsWith('#')) {
return transformHexColor(val);
}
Comment on lines +51 to +53
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be deleted right? If it's already hex format, there's no point in transforming it to rgba()?


// Handle rgba() with hex colors
if (val.includes('rgba(')) {
return val.replace(/rgba\(\s*#[A-Fa-f0-9]+\s*,\s*([0-9.%]+)\s*\)/g, (match, alpha) => {
const hex = match.substring(match.indexOf('#'), match.indexOf(','));
try {
const [r, g, b] = new Color(hex).srgb;
return `rgba(${r * 255}, ${g * 255}, ${b * 255}, ${alpha})`;
Comment on lines +60 to +61
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about rgba(hex8, alpha) or rgba(hex4, alpha) so basically which alpha do we take if they are both defined in the hex but also in the alpha component of the expression?

} catch (e) {
console.warn(`Tried parsing "${hex}" as a hex value, but failed.`);
return match;
}
});
}

return val;
};

const transformProp = (val: Record<string, unknown>, prop: string) => {
Expand Down
26 changes: 26 additions & 0 deletions test/spec/css/transformHEXRGBa.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,30 @@ describe('transform HEXRGBa', () => {
).to.eql({ width: '2px', style: 'solid', color: 'rgba(0, 0, 0, 0.5)' });
});
});

it('transforms shorthand hex formats correctly', () => {
// 3-digit hex (#RGB)
expect(transformHEXRGBaForCSS({ value: '#F00' })).to.equal('#F00');

// 4-digit hex (#RGBA)
expect(transformHEXRGBaForCSS({ value: '#F00F' })).to.equal('rgba(255, 0, 0, 1)');
expect(transformHEXRGBaForCSS({ value: '#F000' })).to.equal('rgba(255, 0, 0, 0)');
Comment on lines +112 to +113
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect these to have the same output as input, since it's valid hex format.


// Mixed formats in a single value
expect(
transformHEXRGBaForCSS({
value: 'linear-gradient(180deg, rgba(#000, 0.5), rgba(#F00F, 0.5))',
}),
).to.equal('linear-gradient(180deg, rgba(0, 0, 0, 0.5), rgba(255, 0, 0, 0.5))');
});

it('handles invalid hex values gracefully', () => {
expect(transformHEXRGBaForCSS({ value: 'rgba(#GGG, 0.5)' })).to.equal('rgba(#GGG, 0.5)');
expect(transformHEXRGBaForCSS({ value: 'rgba(#12, 0.5)' })).to.equal('rgba(#12, 0.5)');
});

it('transforms HEX8 format correctly', () => {
expect(transformHEXRGBaForCSS({ value: '#000000FF' })).to.equal('rgba(0, 0, 0, 1)');
expect(transformHEXRGBaForCSS({ value: '#00000000' })).to.equal('rgba(0, 0, 0, 0)');
Comment on lines +129 to +130
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, so far this transform only is there to handle rgba(hex, alpha), not transform hex4/8 to rgba. I'm not entirely against repurposing this transform to also do this, but it would definitely be a significant breaking change.

});
});
Loading