Skip to content

Commit

Permalink
Feat: Adds compiler for turning Figma nodes into Stylesheets (#200)
Browse files Browse the repository at this point in the history
* adding package for figma non-plugin integrations

* migrate designer core to adaptive-ui-designer-core package

* Update adaptive-uifigma-designer imports to source from adaptive-ui-designer-core

* Add REST node parsing to adaptive-ui-designer-figma

* rename plugin directory

* Add CLI to compile stylesheets

* Add JSON compiler to adaptive-ui and add readme

* refactor spinalCase to kebabCase

* remove company references

* fixing declaration path and schema url

* Update packages/adaptive-ui-designer-figma-plugin/src/core/code-gen.ts

* fixing typescript build

* Change files

* adds CI process to build all packages before running tests

---------

Co-authored-by: nicholasrice <nicholasrice@users.noreply.github.com>
  • Loading branch information
nicholasrice and nicholasrice authored May 23, 2024
1 parent d413592 commit ab3d387
Show file tree
Hide file tree
Showing 77 changed files with 4,625 additions and 1,679 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci-validate-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ jobs:

- name: Check for the presence of changed files inside ./change
run: npm run checkchange

- name: Build all packages
run: npm run build

- name: Validate workspaces
run: npm run test
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Adds stylesheet generation from JSON anatomy",
"packageName": "@adaptive-web/adaptive-ui",
"email": "nicholasrice@users.noreply.github.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Initial package creation",
"packageName": "@adaptive-web/adaptive-ui-designer-core",
"email": "nicholasrice@users.noreply.github.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Initial package creation",
"packageName": "@adaptive-web/adaptive-ui-designer-figma",
"email": "nicholasrice@users.noreply.github.com",
"dependentChangeType": "patch"
}
2,616 changes: 2,079 additions & 537 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
"workspaces": [
"./packages/adaptive-ui",
"./packages/adaptive-web-components",
"./packages/adaptive-ui-designer-core",
"./packages/adaptive-ui-explorer",
"./packages/adaptive-ui-figma-designer",
"./packages/adaptive-ui-designer-figma",
"./packages/adaptive-ui-designer-figma-plugin",
"./examples/*"
],
"engines": {
Expand Down
37 changes: 37 additions & 0 deletions packages/adaptive-ui-designer-core/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@adaptive-web/adaptive-ui-designer-core",
"version": "0.0.0",
"description": "Core infrastructure for the Adaptive UI Designer",
"main": "dist/esm/index.js",
"exports": {
".": {
"types": "./dist/dts/index.d.ts",
"default": "./dist/esm/index.js"
}
},
"type": "module",
"scripts": {
"build": "tsc",
"watch": "tsc --watch"
},
"repository": {
"type": "git",
"url": "git+https://github.com/adaptive-web-community/adaptive-web-components.git"
},
"author": {
"name": "Adaptive Web Community",
"url": "https://github.com/adaptive-web-community"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/adaptive-web-community/adaptive-web-components/issues"
},
"homepage": "https://github.com/adaptive-web-community/adaptive-web-components#readme",
"dependencies": {
"@adaptive-web/adaptive-ui": "^0.3.0",
"@microsoft/fast-foundation": "^3.0.0-alpha.31"
},
"devDependencies": {
"typescript": "^5.4.5"
}
}
19 changes: 19 additions & 0 deletions packages/adaptive-ui-designer-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export { Controller, PluginUIState, STYLE_REMOVE } from "./controller.js";
export {
PluginMessage,
PluginNodeData,
AppliedStyleModules,
AppliedStyleValues,
PluginUINodeData,
AdditionalDataKeys,
DesignTokenValue,
AppliedDesignToken,
AppliedStyleValue,
AdditionalData,
AppliedDesignTokens,
DesignTokenValues
} from "./model.js";
export { mapReplacer, mapReviver, deserializeMap } from "./serialization.js";
export { State, StatesState, PluginNode, focusIndicatorNodeName, } from "./node.js";
export { DesignTokenDefinition, DesignTokenRegistry, FormControlId } from "./registry/design-token-registry.js";
export { nameToTitle, registerAppliableTokens, registerTokens } from "./registry/recipes.js";
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ValuesOf } from "@microsoft/fast-foundation";
import { StyleProperty } from "@adaptive-web/adaptive-ui";
import { type Color, formatHex8 } from "culori/fn";
import {
Expand All @@ -14,6 +13,11 @@ import {
ReadonlyDesignTokenValues,
} from "./model.js";
import { deserializeMap, serializeMap } from "./serialization.js";
/**
* Helper for enumerating a type from a const object
* Example: export type Foo = ValuesOf\<typeof Foo\>
*/
type ValuesOf<T> = T[keyof T];

/**
* Layer name for special handling of the focus indicator in design tools.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
blackOrWhiteDiscernibleRecipe,
blackOrWhiteReadableRecipe,
fillColor
} from "@adaptive-web/adaptive-ui/reference"
} from "@adaptive-web/adaptive-ui/reference";
import { DesignTokenResolver } from "@microsoft/fast-foundation";

// Local recipes for use in documentation files.
Expand Down Expand Up @@ -66,7 +66,7 @@ export const blackOrWhiteDiscernibleRest = createTokenSwatch("black-or-white-dis
focus: fill,
disabled: fill,
}
return resolve(blackOrWhiteDiscernibleRecipe).evaluate(resolve, set).rest
return resolve(blackOrWhiteDiscernibleRecipe).evaluate(resolve, set).rest!
}
);

Expand All @@ -80,6 +80,6 @@ export const blackOrWhiteReadableRest = createTokenSwatch("black-or-white-readab
focus: fill,
disabled: fill,
}
return resolve(blackOrWhiteReadableRecipe).evaluate(resolve, set).rest
return resolve(blackOrWhiteReadableRecipe).evaluate(resolve, set).rest!
}
);
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DesignToken, ValuesOf } from "@microsoft/fast-foundation";
import { DesignToken, ValuesOf } from "@microsoft/fast-foundation";
import { StyleProperty } from "@adaptive-web/adaptive-ui";

export const FormControlId = {
Expand Down Expand Up @@ -104,4 +104,4 @@ export class DesignTokenRegistry {
public find(target: StyleProperty): DesignTokenDefinition[] {
return Object.values(this._entries).filter(value => value.intendedFor?.includes(target));
}
}
}
12 changes: 12 additions & 0 deletions packages/adaptive-ui-designer-core/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"importHelpers": true,
"lib": ["DOM", "ES2020"],
"skipLibCheck": true,
"declarationDir": "dist/dts",
"outDir": "dist/esm",
"rootDir": "src"
},
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@adaptive-web/adaptive-ui-figma-designer",
"name": "@adaptive-web/adaptive-ui-designer-figma-plugin",
"version": "0.0.1",
"description": "A designer for working with Adaptive UI within Figma",
"type": "module",
Expand Down Expand Up @@ -35,6 +35,8 @@
"test": "npm run lint && npm run compile"
},
"dependencies": {
"@adaptive-web/adaptive-ui-designer-core": "^0.0.0",
"@adaptive-web/adaptive-ui-designer-figma": "^0.0.0",
"@adaptive-web/adaptive-web-components": "0.5.0",
"change-case": "^5.4.4",
"culori": "^3.2.0"
Expand Down
148 changes: 148 additions & 0 deletions packages/adaptive-ui-designer-figma-plugin/src/core/code-gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/* eslint-disable max-len */
import { camelCase, kebabCase } from "change-case";
import { StyleNameMapping } from "@adaptive-web/adaptive-ui/reference";
import { Anatomy, BooleanCondition, Condition, StringCondition, StyleRule } from "@adaptive-web/adaptive-ui-designer-figma";

function makeClassName(value: string) {
return `.${kebabCase(value)}`;
}

/**
* Code generation capabilities related to component anatomy and styling.
*
* @remarks
* The functionality here is split into two parts to facilitate reuse and customization:
* - Design tool node parsing into a component anatomy definition
* - Generating AUI code based on the anatomy definition (functions named "generate" or "gen")
*
* The common flow for all output is:
* ``` ts
* const codeGen = new CodeGen();
* const anatomyOutput = codeGen.generateAnatomyCode(anatomy);
* const stylesOutput = codeGen.generateStylesCode(anatomy);
* ```
*/
export class CodeGen {
/**
* Generates anatomy code for the provided anatomy definition.
*
* @param anatomy - The provided anatomy definition
* @returns The generated anatomy code
*/
public generateAnatomyCode(anatomy: Anatomy): string {
const conditionsSet = new Array(...anatomy.conditions.entries())
.reduce((accumulated: Set<string>, current: [string, Condition]): Set<string> => {
if (current[1] instanceof BooleanCondition) {
accumulated.add(current[0]);
} else if (current[1] instanceof StringCondition) {
current[1].values.forEach(value => {
const currentValue = `${current[0]}${value}`;
accumulated.add(currentValue);
});
}

return accumulated;
}, new Set());
const conditionsOut = this.#genTypeCode(anatomy.name, "Conditions", conditionsSet);
const partsOut = this.#genTypeCode(anatomy.name, "Parts", anatomy.parts);

// TODO other states (json serialize?)
const interactivity = anatomy.interactivity ? `\n interactivity: {\n interactive: "${anatomy.interactivity.interactive}",\n disabled: "${anatomy.interactivity.disabled}",\n },` : "";
const conditionsValues = new Array(...conditionsSet).map(property => `\n ${camelCase(property)}: "${makeClassName(property)}",`).join("");
const partsValues = new Array(...anatomy.parts).map(property => `\n ${camelCase(property)}: "${makeClassName(property)}",`).join("");

const anatomyOut =
`export const ${anatomy.name}Anatomy: ComponentAnatomy<${anatomy.name}Conditions, ${anatomy.name}Parts> = {${interactivity}
conditions: {${conditionsValues}
},
parts: {${partsValues}
},
};
`;
// TODO:
// focus: Focus.contextFocused(),

const output = `${conditionsOut}\n${partsOut}\n${anatomyOut}\n`;
return output;
}

#genTypeCode(componentName: string, type: "Parts" | "Conditions", properties: Set<string>): string {
// const partsString = parts.map(part => ` ${camelCase(part)}: "${kebabCase(part)}"`).join("\n");
// const code = `export const ${componentName}Parts = {\n${partsString}\n};`;
const propertiesString = new Array(...properties).map(property => ` ${camelCase(property)}: string;`).join("\n");
return `export type ${componentName}${type} = {\n${propertiesString}\n};\n`;
}

/**
* Generates styling code for the provided anatomy definition.
*
* @param anatomy - The provided anatomy definition
* @returns The generated style code
*/
public generateStylesCode(anatomy: Anatomy): string {
const imported = new Array(...anatomy.styleRules).reduce<Array<string>>((accumulated, current) => {
current.styles.forEach(style => {
const varName = StyleNameMapping[style as keyof typeof StyleNameMapping];
accumulated.push(varName);
});
current.tokens.forEach(token => {
const varName = this.tokenIDMap(token.tokenID);
accumulated.push(varName);
});
return accumulated;
}, new Array<string>());
const importBase = `import { StyleRules } from "@adaptive-web/adaptive-ui";\n`;
const refImports = [...imported].sort().join(", ");
const importRef = `import { ${refImports} } from "@adaptive-web/adaptive-ui/reference";\n`;
const importAnatomy = `import { ${anatomy.name}Anatomy } from "./${kebabCase(anatomy.name)}.template.js";\n`
const styleRules = new Array(...anatomy.styleRules).map(styleRule => this.#genStyleRuleCode(anatomy.name, styleRule)).join("\n");
const genStyleRules = `\nexport const styleRules: StyleRules = [\n${styleRules}\n];\n`;

const output = `${importBase}${importRef}${importAnatomy}${genStyleRules}`;
return output;
}

#genStyleRuleCode(componentName: string, styleRule: StyleRule): string {
let targetOut = "";
const contextCondition = styleRule.contextCondition ? ` contextCondition: ${componentName}Anatomy.conditions.${camelCase(styleRule.contextCondition)},\n` : "";
const part = styleRule.part ? ` part: ${componentName}Anatomy.parts.${camelCase(styleRule.part)},\n` : "";
if (contextCondition || part) {
targetOut = ` target: {\n${part}${contextCondition} },\n`;
}

let stylesOut = "";
if (styleRule.styles.size > 0) {
stylesOut = ` styles: [\n ${new Array(...styleRule.styles).map(style => StyleNameMapping[style as keyof typeof StyleNameMapping] || style).join(",\n ")},\n ],\n`;
}

let propertiesOut = "";
if (styleRule.tokens.size > 0) {
// TODO Need to map tokens better
const tokens = new Array(...styleRule.tokens).map(token => `${token.target}: ${this.tokenIDMap(token.tokenID)}`);
propertiesOut = ` properties: {\n ${tokens.join(",\n ")},\n },\n`;
}

return ` {\n${targetOut}${stylesOut}${propertiesOut} },`;
}

private tokenIDMap(tokenID: string): string {
let adjustedID = tokenID;

// TODO: Clean up naming and grouping
const densityGroups = ["control", "item-container", "layer"];
if (tokenID.startsWith("density_")) {
for (let i = 0; i < densityGroups.length; i++) {
const group = densityGroups[i];
const testGroup = `density_${group}-`;
const adjustedGroup = `density-${group}_`;
if (tokenID.startsWith(testGroup)) {
adjustedID = tokenID.replace(testGroup, adjustedGroup);
continue;
}
}
}

const pieces = adjustedID.split("_");
return pieces.map(piece => camelCase(piece)).join(".");
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Controller, PluginUIState } from "../core/controller.js";
import type { PluginMessage } from "../core/model.js";
import { mapReplacer } from "../core/serialization.js";
import { Controller, PluginUIState } from "@adaptive-web/adaptive-ui-designer-core";
import type { PluginMessage } from "@adaptive-web/adaptive-ui-designer-core";
import { mapReplacer } from "@adaptive-web/adaptive-ui-designer-core";
import { FigmaPluginNode } from "./node.js";

export class FigmaController extends Controller {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PluginMessage } from "../core/model.js";
import { mapReviver } from "../core/serialization.js";
import { PluginMessage } from "@adaptive-web/adaptive-ui-designer-core";
import { mapReviver } from "@adaptive-web/adaptive-ui-designer-core";
import { FigmaController } from "./controller.js";

const controller = new FigmaController();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Shadow, StyleProperty } from "@adaptive-web/adaptive-ui";
import { type Color, modeLrgb, modeRgb, parse, type Rgb, useMode, wcagLuminance } from "culori/fn";
import { Controller, STYLE_REMOVE } from "../core/controller.js";
import { AppliedStyleModules, AppliedStyleValues, PluginNodeData } from "../core/model.js";
import { focusIndicatorNodeName, PluginNode, State, StatesState } from "../core/node.js";
import { Controller, STYLE_REMOVE } from "@adaptive-web/adaptive-ui-designer-core";
import { AppliedStyleModules, AppliedStyleValues, PluginNodeData } from "@adaptive-web/adaptive-ui-designer-core";
import { focusIndicatorNodeName, PluginNode, State, StatesState } from "@adaptive-web/adaptive-ui-designer-core";
import { colorToRgba, variantBooleanHelper } from "./utility.js";

const rgb = useMode(modeRgb);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
"compilerOptions": {
"outDir": "../../dist",
"moduleResolution": "Node16",
"module": "ESNext",
"module": "Node16",
"target": "ES2018",
"lib": ["ES2018"],
"strict": true,
"skipLibCheck": true,
"typeRoots": [
"../../../../node_modules/@figma"
]
"types": ["@figma/plugin-typings"],
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import {
neutralStrokeReadableRest,
neutralStrokeStrongRest
} from "@adaptive-web/adaptive-ui/reference";
import type { PluginMessage, PluginUINodeData } from "../core/model.js";
import { StatesState } from "../core/node.js";
import { DesignTokenDefinition } from "../core/registry/design-token-registry.js";
import type { PluginMessage, PluginUINodeData } from "@adaptive-web/adaptive-ui-designer-core";
import { StatesState } from "@adaptive-web/adaptive-ui-designer-core";
import { DesignTokenDefinition } from "@adaptive-web/adaptive-ui-designer-core";
import SubtractIcon from "./assets/subtract.svg";
import { UIController } from "./ui-controller.js";
import { AppliedDesignTokenItem, StyleModuleDisplay, StyleModuleDisplayList } from "./ui-controller-styles.js";
Expand Down
Loading

0 comments on commit ab3d387

Please sign in to comment.