Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
778 changes: 742 additions & 36 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"styled-components": "^5.3.5"
},
"devDependencies": {
"@figma/plugin-typings": "^1.72.0",
"@figma/plugin-typings": "^1.121.0",
"@svgr/webpack": "^6.3.1",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
Expand All @@ -34,8 +34,8 @@
"css-loader": "^6.7.1",
"eslint": "^8.20.0",
"html-webpack-plugin": "^5.5.0",
"node-sass": "^8.0.0",
"prettier": "^2.7.1",
"sass": "^1.57.1",
"sass-loader": "^13.2.0",
"style-loader": "^3.3.1",
"ts-loader": "^9.3.1",
Expand Down
86 changes: 72 additions & 14 deletions src/controller/variables.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { OperationResult } from "shared/collab"
import { jsonColorToFigmaColor } from "utils/color"
import { extractFirstFontFamily } from "utils/fontFamily"
import { mapFontWeight } from "utils/lineWeight"
import { convertLineHeightPercentageToMultiplier } from "utils/lineHeight"
import { type JsonToken, type JsonTokenDocument, type JsonManifest, allTokenNodes } from "utils/tokens"
import type { JsonTokenType } from "utils/tokens/types"
import { getAliasTargetName } from "utils/tokens/utils"
Expand All @@ -9,18 +12,33 @@ function tokenNameToFigmaName(name: string): string {
return name.replaceAll(".", "/")
}

/** For a given token $type in the DTCG format, return the corresponding Figma token type, or null if there isn't one. */
function tokenTypeToFigmaType($type: JsonTokenType): VariableResolvedDataType | null {
switch ($type) {
/** Convert rem values to pixels (assuming 16px base font size). */
function convertRemToPx(value: any): number {
if (typeof value === "number") {
return value * 16
} else {
return parseFloat(value) * 16
}
}

/** For a given token "$type" or "type" in the DTCG format, return the corresponding Figma token type, or null if there isn't one. */
function tokenTypeToFigmaType(type: JsonTokenType): VariableResolvedDataType | null {
switch (type) {
case "color":
return "COLOR"
case "dimension":
case "duration":
case "number":
case "fontSize":
case "borderRadius":
case "lineHeight":
case "letterSpacing":
return "FLOAT"
case "boolean":
return "BOOLEAN"
case "string":
case "fontFamily":
case "fontWeight":
return "STRING"
default:
return null
Expand Down Expand Up @@ -114,17 +132,18 @@ export async function importTokens(files: Record<string, JsonTokenDocument>, man
keepGoing = false
const retryNextTime: typeof queuedUpdates = []
for (const update of queuedUpdates) {
const figmaType = tokenTypeToFigmaType(update.token.$type)
const tokenType = update.token.type || update.token.$type
const figmaType = tokenType ? tokenTypeToFigmaType(tokenType) : null
if (!figmaType) {
results.push({
result: "info",
text: `Unable to add ${update.figmaName} mode ${update.modeName} because ${update.token.$type} tokens arent supported.`,
text: `Unable to add ${update.figmaName} mode ${update.modeName} because ${tokenType || "unknown"} tokens aren't supported.`,
})
continue
}

// First, if this is an alias, see if the target exists already.
const targetName = getAliasTargetName(update.token.$value)
const targetName = getAliasTargetName(update.token.value || update.token.$value)
let targetVariable: Variable | LibraryVariable | undefined = undefined
if (targetName) {
const targetFigmaName = tokenNameToFigmaName(targetName)
Expand Down Expand Up @@ -196,23 +215,54 @@ export async function importTokens(files: Record<string, JsonTokenDocument>, man
}
variable.setValueForMode(modeId, figma.variables.createVariableAlias(targetVariable as Variable))
} else {
const value = update.token.$value
switch (update.token.$type) {
const value = update.token.value || update.token.$value
const tokenType = update.token.type || update.token.$type

//Update code syntax first if specified in $extensions
if (update.token.extensions && update.token.extensions["codeSyntax"] && update.token.extensions["codeSyntaxPlatform"] && typeof update.token.extensions["codeSyntax"] === "string") {
const codeSyntax = update.token.extensions["codeSyntax"]
const platform = update.token.extensions["codeSyntaxPlatform"]
variable.setVariableCodeSyntax(platform, codeSyntax);
}

switch (tokenType) {
case "color": {
const color = jsonColorToFigmaColor(value)
if (color) variable.setValueForMode(modeId, color)
else results.push({ result: "error", text: `Invalid color: ${update.figmaName} = ${JSON.stringify(value)}` })
break
}
case "fontSize": {
const fontSizeFloat = convertRemToPx(value)
if (!isNaN(fontSizeFloat)) variable.setValueForMode(modeId, fontSizeFloat)
else
results.push({
result: "error",
text: `Invalid ${tokenType}: ${update.figmaName} = ${JSON.stringify(value)}`,
})
break
}
case "lineHeight": {
const lineHeightFloat = convertLineHeightPercentageToMultiplier(value)
if (!isNaN(lineHeightFloat)) variable.setValueForMode(modeId, lineHeightFloat)
else
results.push({
result: "error",
text: `Invalid ${tokenType}: ${update.figmaName} = ${JSON.stringify(value)}`,
})
break
}
case "letterSpacing":
case "dimension":
case "duration":
case "number": {
case "number":
case "borderRadius": {
const float = typeof value === "number" ? value : parseFloat(value)
if (!isNaN(float)) variable.setValueForMode(modeId, float)
else
results.push({
result: "error",
text: `Invalid ${update.token.$type}: ${update.figmaName} = ${JSON.stringify(value)}`,
text: `Invalid ${tokenType}: ${update.figmaName} = ${JSON.stringify(value)}`,
})
break
}
Expand All @@ -221,15 +271,21 @@ export async function importTokens(files: Record<string, JsonTokenDocument>, man
else
results.push({
result: "error",
text: `Invalid ${update.token.$type}: ${update.figmaName} = ${JSON.stringify(value)}`,
text: `Invalid ${tokenType}: ${update.figmaName} = ${JSON.stringify(value)}`,
})
break
case "string":
variable.setValueForMode(modeId, value)
break
case "fontFamily":
variable.setValueForMode(modeId, extractFirstFontFamily(value))
break
case "fontWeight":
variable.setValueForMode(modeId, mapFontWeight(value))
break
default:
throw new Error(
`Failed to update a variable of type ${update.token.$type}. tokenTypeToFigmaType probably needs to be updated.`
`Failed to update a variable of type ${tokenType}. tokenTypeToFigmaType probably needs to be updated.`
)
}
}
Expand Down Expand Up @@ -265,8 +321,8 @@ export async function importTokens(files: Record<string, JsonTokenDocument>, man
results.push({
result: "error",
text: `Unable to add ${missing.figmaName} mode ${missing.modeName} because it is an alias of ${tokenNameToFigmaName(
getAliasTargetName(missing.token.$value) || "another token"
)} but ${isTeamLibraryAvailable ? "that doesnt exist" : "it wasnt found—it may be in a different file"}.`,
getAliasTargetName(missing.token.value || missing.token.$value) || "another token"
)} but ${isTeamLibraryAvailable ? "that doesn't exist" : "it wasn't found—it may be in a different file"}.`,
})
}
}
Expand All @@ -285,3 +341,5 @@ export async function importTokens(files: Record<string, JsonTokenDocument>, man

return results
}


19 changes: 19 additions & 0 deletions src/utils/fontFamily.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/** Extract the first font family name from a CSS font-family string. */
export function extractFirstFontFamily(value: any): string {
if (typeof value !== "string") {
return String(value)
}

// Check if split method exists
if (typeof value.split !== "function") {
return value.replace(/^['"]|['"]$/g, "")
}

// Split by comma to get the first font in the stack
const firstFont = value.split(",")[0].trim()
Copy link
Member

Choose a reason for hiding this comment

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

Is there a possibility that value.split doesn't have a valid return, which would cause this to crash?


// Remove surrounding quotes (single or double)
const withoutQuotes = firstFont.replace(/^['"]|['"]$/g, "")

return withoutQuotes
}
8 changes: 8 additions & 0 deletions src/utils/lineHeight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** Convert line height percentage values to multipliers. */
export function convertLineHeightPercentageToMultiplier(value: any): number {
if (typeof value === "number") {
return value / 100
} else {
return parseFloat(value) / 100
}
}
25 changes: 25 additions & 0 deletions src/utils/lineWeight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/** Map numeric font weight values to their string equivalents.
* This mapping is for Segoe Sans font weights (our default font). Figma has its own terminology for font weights and not numeric values. Mapping the value to a word helps designers accurately predict what font is going to have what weight.
*/
export function mapFontWeight(value: any): string | number {
const fontWeightMap: Record<number, string> = {
100: "hairline",
200: "thin",
300: "light",
400: "semilight",
500: "regular",
600: "semibold",
700: "bold",
800: "extrabold",
900: "black",
}

const numericValue = typeof value === "number" ? value : parseFloat(value)

if (!isNaN(numericValue) && fontWeightMap[numericValue]) {
return fontWeightMap[numericValue]
}

// Return original value if no mapping found
return value
}
2 changes: 1 addition & 1 deletion src/utils/tokens/aliases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function resolveAllAliases(tokens: Record<string, JsonToken>): OperationR
currentResults.push({ result: "error", text: `A token was an alias of "${name}" but that token doesn‘t exist.` })
return
}
const value = token.$value
const value = token.value || token.$value
if (!token) {
currentResults.push({ result: "error", text: `A token was an alias of "${name}" but that doesn‘t have a value.` })
return
Expand Down
9 changes: 6 additions & 3 deletions src/utils/tokens/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ export interface JsonTokenChildren {
export type JsonTokenValue = any

export interface JsonToken {
$type: JsonTokenType
$value: JsonTokenValue
type?: JsonTokenType
$type?: JsonTokenType
value?: JsonTokenValue
$value?: JsonTokenValue
$description?: string
$extensions?: Record<string, any>
extensions?: Record<string, any>
}

type JsonTokenPrimitiveType = "string" | "number" | "boolean" | "object" | "array" | "null"
type JsonTokenBasicType = "color" | "dimension" | "fontFamily" | "fontWeight" | "duration" | "cubicBezier"
type JsonTokenBasicType = "color" | "dimension" | "fontFamily" | "fontWeight" | "duration" | "cubicBezier" | "fontSize" | "borderRadius" | "lineHeight" | "letterSpacing"
type JsonTokenCompositeType = "strokeStyle" | "border" | "transition" | "shadow" | "gradient" | "typography"
export type JsonTokenType = JsonTokenPrimitiveType | JsonTokenBasicType | JsonTokenCompositeType
4 changes: 2 additions & 2 deletions src/utils/tokens/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ export function isChildName(name: string): boolean {
}

export function isToken(obj: JsonTokenGroup | JsonToken): obj is JsonToken {
return typeof obj === "object" && "$value" in obj
return typeof obj === "object" && ("value" in obj || "$value" in obj)
}

export function isTokenGroup(obj: JsonTokenGroup | JsonToken): obj is JsonTokenGroup {
return typeof obj === "object" && !("$value" in obj)
return typeof obj === "object" && !("value" in obj) && !("$value" in obj)
}

export function isAliasValue(value: string): boolean {
Expand Down
Loading