Skip to content

Instantly share code, notes, and snippets.

@pmark
Last active March 8, 2023 06:46
Show Gist options
  • Save pmark/f7f8c628543ddbecb64a7ceee953fb51 to your computer and use it in GitHub Desktop.
Save pmark/f7f8c628543ddbecb64a7ceee953fb51 to your computer and use it in GitHub Desktop.
A Figma API client for declaratively automating design token style sync and docs
class FigmaFile {
private fileId: string;
constructor(fileId: string) {
this.fileId = fileId;
}
async getColors(): Promise<FigmaColor[]> {
const response = await fetch(`https://api.figma.com/v1/files/${this.fileId}/`);
const json = await response.json();
return json.document.colors;
}
async getFonts(): Promise<FigmaFont[]> {
const response = await fetch(`https://api.figma.com/v1/files/${this.fileId}/`);
const json = await response.json();
return json.document.fonts;
}
async getEffects(): Promise<FigmaEffect[]> {
const response = await fetch(`https://api.figma.com/v1/files/${this.fileId}/`);
const json = await response.json();
return json.document.effects;
}
colors(): FigmaQueryBuilder<FigmaColor> {
return new FigmaQueryBuilder<FigmaColor>(this.getColors());
}
fonts(): FigmaQueryBuilder<FigmaFont> {
return new FigmaQueryBuilder<FigmaFont>(this.getFonts());
}
effects(): FigmaQueryBuilder<FigmaEffect> {
return new FigmaQueryBuilder<FigmaEffect>(this.getEffects());
}
}
class FigmaQueryBuilder<T> {
private items: Promise<T[]>;
constructor(items: Promise<T[]>) {
this.items = items;
}
filtered(predicate: (item: T) => boolean): FigmaQueryBuilder<T> {
this.items = this.items.then((items) => items.filter(predicate));
return this;
}
async result(): Promise<T[]> {
return await this.items;
}
}
async function findTokenStyleDifferences(fileId: string, tokens: DesignToken[]): Promise<TokenStyleDifference[]> {
const figmaFile = new FigmaFile(fileId);
// Get all styles in the file
const allStyles = await figmaFile.styles().result();
const differences: TokenStyleDifference[] = [];
for (const token of tokens) {
// Find the style that the token references
const referencedStyle = allStyles.find((style) => style.id === token.styleId);
if (!referencedStyle) {
// Style not found, skip this token
continue;
}
// Compare the token value to the style properties
const styleProps = referencedStyle.style;
const tokenValue = token.value;
const propsToCompare = ['color', 'fontSize', 'fontWeight', 'letterSpacing', 'lineHeight'];
const propertyDifferences = propsToCompare.filter((prop) => styleProps[prop] !== tokenValue[prop]);
if (propertyDifferences.length > 0) {
differences.push({
token: token.name,
style: referencedStyle.name,
differences: propertyDifferences,
});
}
}
return differences;
}
interface FigmaStyle {
id: string;
name: string;
type: string;
}
interface FigmaColor extends FigmaStyle {
r: number;
g: number;
b: number;
a: number;
visible: boolean;
description?: string;
mixMode: string;
opacity?: number;
blendMode?: string;
parent?: string;
gradientHandlePositions?: Vector[];
gradientStops?: GradientStop[];
scaleMode?: string;
imageRef?: string;
format?: string;
effects?: Effect[];
shortcut?: string;
shade?: number;
}
interface FigmaFont extends FigmaStyle {
family: string;
style: string;
postScriptName: string;
category: string;
fontFiles: {
[key: string]: {
url: string;
size: number;
};
};
fontWeight: number;
italic: boolean;
width?: string;
stretch?: string;
letterSpacing?: {
value: number;
unit: string;
};
lineHeightPx?: number;
lineHeightPercent?: number;
lineHeightUnit?: string;
fontSize?: {
value: number;
unit: string;
};
fontPostScriptName?: string;
fontVariationSettings?: {
[key: string]: number;
};
}
interface FigmaEffect {
type: string;
visible: boolean;
radius: number;
color: FigmaColor;
blendMode: string;
offset: Vector;
}
type FigmaItemType<T> = T extends FigmaColor ? 'FigmaColor' : T extends FigmaFont ? 'FigmaFont' : T extends FigmaEffect ? 'FigmaEffect' : never;
async function findTokenStyleDifferences(fileId: string, tokens: DesignToken[]): Promise<TokenStyleDifference[]> {
const figmaFile = new FigmaFile(fileId);
// Get all styles in the file
const allStyles = await figmaFile.styles().result();
const differences: TokenStyleDifference[] = [];
for (const token of tokens) {
// Find the style that the token references
const referencedStyle = allStyles.find((style) => style.id === token.styleId);
if (!referencedStyle) {
// Style not found, skip this token
continue;
}
// Compare the token value to the style properties
const styleProps = referencedStyle.style;
const tokenValue = token.value;
const propsToCompare = ['color', 'fontSize', 'fontWeight', 'letterSpacing', 'lineHeight'];
const propertyDifferences = propsToCompare.filter((prop) => styleProps[prop] !== tokenValue[prop]);
if (propertyDifferences.length > 0) {
differences.push({
token: token.name,
style: referencedStyle.name,
differences: propertyDifferences,
});
}
}
return differences;
}
async function updateTokenStyles(fileId: string, tokens: DesignToken[]): Promise<void> {
const figmaFile = new FigmaFile(fileId);
// Get all styles in the file
const allStyles = await figmaFile.styles().result();
// Create a mapping of style IDs to styles for fast lookups
const styleIdMap = new Map<string, FigmaStyle>();
allStyles.forEach((style) => {
styleIdMap.set(style.id, style);
});
// Update the styles that the tokens reference
for (const token of tokens) {
const referencedStyle = styleIdMap.get(token.styleId);
if (!referencedStyle) {
// Style not found, skip this token
continue;
}
// Update the properties of the referenced style to match the token value
const styleProps = referencedStyle.style;
const tokenValue = token.value;
const updatedStyle: FigmaStyle = {
...referencedStyle,
style: {
...styleProps,
color: tokenValue.color,
fontSize: tokenValue.fontSize,
fontWeight: tokenValue.fontWeight,
letterSpacing: tokenValue.letterSpacing,
lineHeight: tokenValue.lineHeight,
},
};
await figmaFile.styles().update(updatedStyle);
}
}
const figmaFile = new FigmaFile('fileId123')
// Select only colors in the primary tier and exclude any that start with 'background'
const primaryColors = figmaFile.colors.tier(1).filtered(color => !color.name.startsWith('background'))
// Select only fonts that are marked as display fonts
const displayFonts = figmaFile.fonts.filtered(font => font.category === 'display')
// Select only shadow effects that have a blur radius of 16 or greater
const largeShadows = figmaFile.effects.filtered(effect => effect.type === 'shadow' && effect.blurRadius >= 16)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment