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

Export salt theme plugin #4

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
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
44 changes: 36 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions packages/export-salt-theme/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
![Plugin screenshot](docs/export-salt-theme-plugin-screenshot.png)

Figma plugin to export Salt DS library to JSON object, which can be previewed using code.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions packages/export-salt-theme/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "Export Salt Theme",
"id": "export-salt-theme",
"api": "1.0.0",
"editorType": ["figma"],
"permissions": [],
"main": "dist/code.js",
"ui": "dist/index.html"
}
39 changes: 39 additions & 0 deletions packages/export-salt-theme/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "figma-export-salt-theme",
"version": "0.0.1",
"description": "Figma plugin exports salt theme",
"main": "dist/code.js",
"scripts": {
"tsc": "npm run tsc:main && npm run tsc:ui",
"tsc:main": "tsc --noEmit -p plugin-src",
"tsc:ui": "tsc --noEmit -p ui-src",
"tsc:watch": "concurrently -n widget,iframe,tests \"npm run tsc:main -- --watch --preserveWatchOutput\" \"npm run tsc:ui -- --watch --preserveWatchOutput\" \"npm run tsc:tests -- --watch --preserveWatchOutput\"",
"build": "npm run build:ui && npm run build:main -- --minify",
"build:main": "esbuild plugin-src/code.ts --bundle --outfile=dist/code.js --target=es6",
"build:ui": "npx vite build --minify esbuild --emptyOutDir=false",
"build:watch": "concurrently -n widget,iframe \"npm run build:main -- --watch\" \"npm run build:ui -- --watch\"",
"dev": "concurrently -n tsc,build,vite 'npm:tsc:watch' 'npm:build:watch' 'vite'"
},
"repository": {
"type": "git",
"url": "git+https://github.com/jpmorganchase/Figma-Plugins-and-Widgets.git"
},
"keywords": [
"figma-plugin"
],
"author": "JPMC",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/jpmorganchase/Figma-Plugins-and-Widgets/issues"
},
"homepage": "https://github.com/jpmorganchase/Figma-Plugins-and-Widgets#readme",
"dependencies": {
"@salt-ds/core": "^1.7.1",
"@salt-ds/icons": "^1.3.1",
"@salt-ds/lab": "^1.0.0-alpha.9",
"@salt-ds/theme": "^1.5.0",
"lz-string": "^1.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
27 changes: 27 additions & 0 deletions packages/export-salt-theme/plugin-src/code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { PostToFigmaMessage, PostToUIMessage } from "../shared-src/messages";
import { generateThemeJson } from "./utils";

const WINDOW_MIN_WIDTH = 400;
const WINDOW_MIN_HEIGHT = 500;

figma.showUI(__html__, {
themeColors: true,
width: WINDOW_MIN_WIDTH,
height: WINDOW_MIN_HEIGHT,
});

figma.ui.onmessage = (msg: PostToFigmaMessage) => {
if (msg.type === "ui-ready") {

Choose a reason for hiding this comment

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

empty code branch

Choose a reason for hiding this comment

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

ah my bad .. pr still draft :)

} else if (msg.type === "generate-json") {
figma.ui.postMessage({
type: "generate-json-result",
data: generateThemeJson(),
} satisfies PostToUIMessage);
} else if (msg.type === "resize-window") {
const { width, height } = msg;
figma.ui.resize(
Math.max(width, WINDOW_MIN_WIDTH),
Math.max(height, WINDOW_MIN_HEIGHT)
);
}
};
16 changes: 16 additions & 0 deletions packages/export-salt-theme/plugin-src/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
transform: {
// Below matches default jest transform (from --debug)
// or a second transform would be applied and tsconfig would be overridden
"^.+\\.tsx?$": [
"ts-jest",
{
tsconfig: "plugin-src/__tests__/tsconfig.json",
},
],
},
setupFiles: ["<rootDir>/../../../jest/globals.js"],
};
10 changes: 10 additions & 0 deletions packages/export-salt-theme/plugin-src/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "es6",
"lib": ["es6"],
"strict": true,
"typeRoots": ["../../../node_modules/@figma"]
},
"include": ["./", "../shared-src"],
"exclude": ["__tests__"]
}
167 changes: 167 additions & 0 deletions packages/export-salt-theme/plugin-src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
type ThemeObject = any;
type TokenType = any;

export function setNestedKey(obj: any, path: string[], value: any): any {
if (path.length === 0) {
return obj;
} else if (path.length === 1) {
return {
...obj,
[path[0]]: value,
};
} else {
if (obj[path[0]]) {
return {
...obj,
[path[0]]: setNestedKey(obj[path[0]], path.slice(1), value),
};
} else {
return {
...obj,
[path[0]]: setNestedKey({}, path.slice(1), value),
};
}
}
}

const KEY_MAP = new Map([["border", "borderColor"]]);

/**
* https://stackoverflow.com/a/2970667
**/
function camelize(str: string) {
return str.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function (match, index) {
if (+match === 0) return ""; // or if (/\s+/.test(match)) for white spaces
return index === 0 ? match.toLowerCase() : match.toUpperCase();
});
}

/** Design side has different naming convention than code, need to modify the path to fit. */
export const fixPathForSalt = (path: string[]) => {
return path.reduce(
(prev, current, index, fullArray) => {
const key = current.toLowerCase();

const mappedKeys = key.split(" ").map((x) => KEY_MAP.get(x) || x);

// Ignore default when at last position
if (
mappedKeys.length === 1 &&
mappedKeys[0] === "default" &&
fullArray.length - 1 === index
) {
return prev;
} else {
return [...prev, ...mappedKeys];
}
},
// prefix salt as top level namespace if not existed
path[0] === "salt" ? [] : ["salt"]
);
};

export const updateTheme = (
theme: ThemeObject,
newToken: TokenType,
path: string[]
): ThemeObject => {
let newTheme = { ...theme };

const newPath = fixPathForSalt(path);

console.log({ newPath });

newTheme = setNestedKey(newTheme, newPath, newToken);

return newTheme;
};

// Refer to MDN font-weight page for more information.
// https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#common_weight_name_mapping
export const FONT_WEIGHT_MAPPING: { [key: string]: number } = {
Thin: 100,
"Extra Light": 200,
ExtraLight: 200,
Light: 300,
Regular: 400,
Medium: 500,
"Semi Bold": 600,
SemiBold: 600,
Semibold: 600,
Bold: 700,
"Extra Bold": 800,
ExtraBold: 800,
Extrabold: 800,
Black: 900,
};

// Try convert to valid CSS line height, see https://developer.mozilla.org/en-US/docs/Web/CSS/line-height
function extractLineHeight(lineHeight: LineHeight): string {
switch (lineHeight.unit) {
case "AUTO": {
return "normal";
}
case "PERCENT": {
return Math.round(lineHeight.value) + "%";
}
case "PIXELS": {
return Math.round(lineHeight.value) + "px";
}
}
return "normal";
}

export function generateThemeJson(): ThemeObject {
// Just support raw color without reference for now

let newTheme: ThemeObject = {};

const localPaintStyles = figma.getLocalPaintStyles();
for (let index = 0; index < localPaintStyles.length; index++) {
const paintStyle = localPaintStyles[index];
if (
paintStyle.paints.length === 1 &&
paintStyle.paints[0].type === "SOLID"
) {
const objPaths = paintStyle.name.split("/");

const { color, opacity } = paintStyle.paints[0];

const newColorToken = {
$type: "color",
$value: {
r: Math.round(color.r * 255),
g: Math.round(color.g * 255),
b: Math.round(color.b * 255),
a: opacity ? Math.round(opacity * 100) / 100 : undefined,
},
};

newTheme = updateTheme(newTheme, newColorToken as any, objPaths);
}
}

const localTextStyles = figma.getLocalTextStyles();
for (let index = 0; index < localTextStyles.length; index++) {
const textStyle = localTextStyles[index];

// NOTE: not all information is extracted
const { fontName, fontSize, lineHeight } = textStyle;
const { family, style } = fontName;

const newTypographyToken = {
$type: "typography",
$value: {
fontFamily: family,
fontWeight: FONT_WEIGHT_MAPPING[style] || 400, // default to 400 / regular
fontSize: fontSize + "px", // Figma fontSize is unit-less
lineHeight: extractLineHeight(lineHeight),
},
};

const objPaths = textStyle.name.split("/");
newTheme = updateTheme(newTheme, newTypographyToken as any, objPaths);
}

return newTheme;
}
1 change: 1 addition & 0 deletions packages/export-salt-theme/shared-src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./messages";
25 changes: 25 additions & 0 deletions packages/export-salt-theme/shared-src/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export type GenerateJsonResultToUIMessage = {
type: "generate-json-result";
data: any;
};

export type PostToUIMessage = GenerateJsonResultToUIMessage;

export type GenerateJsonToFigmaMessage = {
type: "generate-json";
};

export type UIRedayToFigmaMessage = {
type: "ui-ready";
};

export type ResizeWindowToFigmaMessage = {
type: "resize-window";
width: number;
height: number;
};

export type PostToFigmaMessage =
| GenerateJsonToFigmaMessage
| UIRedayToFigmaMessage
| ResizeWindowToFigmaMessage;
Loading
Loading