Skip to content

Commit

Permalink
feat: figma plugin for palette local variables
Browse files Browse the repository at this point in the history
  • Loading branch information
vscaiceanu-1a committed Feb 20, 2025
1 parent b1c84ac commit 939b4a6
Show file tree
Hide file tree
Showing 31 changed files with 1,354 additions and 46 deletions.
3 changes: 2 additions & 1 deletion .github/ISSUE_TEMPLATE/4-APPLICATION-ISSUE-FORM.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ body:
Chrome Devtools,
VsCode Extension,
IntelliJ extension,
Cascading bot
Cascading bot,
Palette generator Figma plugin
]
validations:
required: true
Expand Down
71 changes: 71 additions & 0 deletions apps/palette-generator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Otter Figma plugin for color palettes in local variables (V1) - User guide

## Introduction
The Otter Figma plugin helps you manage color palettes in local variables. Follow these steps to use the plugin effectively.

## Prerequisites
- You'll need the [Color Shades](https://www.figma.com/community/plugin/929607085343688745/color-shades) plugin to generate initial palettes.

## Steps to use the Otter plugin

1. **Generate color palettes**:
- Use the Color Shades plugin to create your color palettes.

![color shades](docs/color-shades.png)
- Copy the generated UI elements from the Figma page.

![copy shades](docs/copy-shades.png)
The output should have this format:
```
0
#F4F7F7
0.5
#E4EAEA
1
#D3DDDC
1.5
#C3D0CF
2
#B2C3C1
3
#91A8A7
4
#708E8C
5
#4F7471
6
#3F5D5A
7
#2F4644
8
#202E2D
8.5
#182322
9
#101717
9.5
#080C0B
10
#000000
```
2. **Open the Otter plugin**:
- Launch the Otter plugin in Figma.

![Otter plugin](docs/otter-plugin.png)

3. **Configure the plugin**:
- Select values from the dropdowns for **Collections**, **Palette name**, and **Mode**.
- Paste the copied values into the **Shades** input area.
- Adjust the filter values if needed (default: **15, 85, 95**).

4. **Add Variables**:
- Click the **"Add variables"** button to update the local variables with the new shades.

### Current Limitations
- **Update Only**: Currently, the plugin only supports updating existing variables. Creating new variables is not yet supported.
- **Future Enhancements**: Future versions will support creating new variables and generating palettes directly within the Otter plugin.

## Conclusion
By following these steps, you can efficiently manage your color palettes in local variables using the Otter Figma plugin. Happy designing!

If you have any questions or need further assistance, feel free to [reach out](https://github.com/AmadeusITGroup/otter/issues).
Binary file added apps/palette-generator/docs/color-shades.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/palette-generator/docs/copy-shades.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/palette-generator/docs/otter-plugin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions apps/palette-generator/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import shared from '../../eslint.shared.config.mjs';
import local from './eslint.local.config.mjs';

export default [
...shared,
...local
];
45 changes: 45 additions & 0 deletions apps/palette-generator/eslint.local.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
dirname,
} from 'node:path';
import {
fileURLToPath,
} from 'node:url';
import globals from 'globals';

const __filename = fileURLToPath(import.meta.url);
// __dirname is not defined in ES module scope
const __dirname = dirname(__filename);

export default [
{
name: '@o3r/palette-generator/projects',
languageOptions: {
sourceType: 'module',
parserOptions: {
tsconfigRootDir: __dirname,
project: [
'tsconfig.build.json',
'tsconfig.build.plugin.json',
'tsconfig.spec.json',
'tsconfig.eslint.json'
]
},
globals: {
// TODO: support for flat config https://github.com/figma/eslint-plugin-figma-plugins/issues/28
figma: 'readonly',
__html__: 'readonly'
}
}
},
{
name: '@o3r/palette-generator/webpack',
files: ['webpack.config.js'],
languageOptions: {
globals: {
...globals.node,
NodeJS: true,
globalThis: true
}
}
}
];
9 changes: 9 additions & 0 deletions apps/palette-generator/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const getJestGlobalConfig = require('../../jest.config.ut').getJestGlobalConfig;

/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */
module.exports = {
...getJestGlobalConfig(__dirname),
projects: [
'<rootDir>/testing/jest.config.ut.js'
]
};
18 changes: 18 additions & 0 deletions apps/palette-generator/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "Otter Palette Generator",
"id": "1471168859479115202",
"api": "1.0.0",
"main": "src/plugin/code.js",
"capabilities": [],
"enableProposedApi": false,
"documentAccess": "dynamic-page",
"editorType": [
"figma"
],
"ui": "src/ui/ui.html",
"networkAccess": {
"allowedDomains": [
"none"
]
}
}
68 changes: 68 additions & 0 deletions apps/palette-generator/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"name": "@o3r/palette-generator",
"version": "0.0.0-placeholder",
"publishConfig": {
"access": "public"
},
"description": "Figma Plugin for generating palettes and storing them in Local Variables",
"sideEffects": false,
"scripts": {
"nx": "nx",
"ng": "yarn nx",
"build": "yarn nx build o3r-palette-generator",
"build:plugin": "esbuild src/plugin/code.ts --bundle --outfile=dist/src/plugin/code.js",
"build:ui": "tsc -p tsconfig.build.json && webpack",
"postbuild": "yarn cpy './manifest.json' dist"
},
"dependencies": {
"tslib": "^2.6.2"
},
"devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "^4.4.0",
"@figma/eslint-plugin-figma-plugins": "^0.15.0",
"@figma/plugin-typings": "^1.107.0",
"@nx/eslint-plugin": "~20.4.0",
"@nx/jest": "~20.4.0",
"@o3r/build-helpers": "workspace:^",
"@o3r/eslint-plugin": "workspace:^",
"@schematics/angular": "~19.1.0",
"@stylistic/eslint-plugin": "~3.1.0",
"@stylistic/eslint-plugin-ts": "~3.1.0",
"@types/jest": "~29.5.2",
"@types/node": "^20.0.0",
"@typescript-eslint/parser": "~8.24.0",
"angular-eslint": "~19.1.0",
"cpy-cli": "^5.0.0",
"esbuild": "~0.25.0",
"eslint": "~9.20.0",
"eslint-import-resolver-node": "^0.3.9",
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-import-newlines": "^1.4.0",
"eslint-plugin-jest": "~28.11.0",
"eslint-plugin-jsdoc": "~50.6.0",
"eslint-plugin-prefer-arrow": "~1.2.3",
"eslint-plugin-unicorn": "^56.0.0",
"eslint-plugin-unused-imports": "^4.1.4",
"globals": "^15.9.0",
"html-inline-script-webpack-plugin": "^3.1.0",
"html-webpack-plugin": "^5.3.2",
"jest": "~29.7.0",
"jest-environment-jsdom": "~29.7.0",
"jest-junit": "~16.0.0",
"jsonc-eslint-parser": "~2.4.0",
"style-loader": "^3.2.1",
"ts-jest": "~29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "~10.9.2",
"type-fest": "^4.30.1",
"typescript": "~5.7.3",
"typescript-eslint": "~8.24.0",
"url-loader": "^4.1.1",
"webpack": "~5.98.0",
"webpack-cli": "~6.0.0"
},
"engines": {
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
}
}
43 changes: 43 additions & 0 deletions apps/palette-generator/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "o3r-palette-generator",
"$schema": "https://raw.githubusercontent.com/nrwl/nx/master/packages/nx/schemas/project-schema.json",
"projectType": "application",
"sourceRoot": "apps/palette-generator/src",
"prefix": "o3r",
"targets": {
"build": {
"executor": "nx:run-script",
"outputs": ["{projectRoot}/dist/package.json"],
"options": {
"script": "postbuild"
},
"dependsOn": [
"build-plugin"
]
},
"build-plugin": {
"executor": "nx:run-script",
"options": {
"script": "build:plugin"
},
"outputs": ["{projectRoot}/dist/src"],
"dependsOn": [
"build-ui"
]
},
"build-ui": {
"executor": "nx:run-script",
"options": {
"script": "build:ui"
},
"outputs": ["{projectRoot}/dist/src"]
},
"lint": {},
"test": {
"options": {
"passWithNoTests": true
}
}
},
"tags": []
}
39 changes: 39 additions & 0 deletions apps/palette-generator/src/contract/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Represents a mode with an ID and a name.
*/
interface Mode {
/** The unique identifier for the mode. */
modeId: string;
/** The name of the mode. */
name: string;
}

/**
* Represents the data for variables.
*/
export interface VariablesData {
/** The name of the palette. */
paletteName: string;
/** The input for shades. */
shadesInput: string;
/** The values that will be filtered out, comma separated. */
filter: string;
/** The ID of the collection. */
collectionId: string;
/** The ID of the mode. */
modeId: string;
}

/**
* Represents a collection with an ID, name, modes, and palettes.
*/
export interface Collection {
/** The unique identifier for the collection. */
id: string;
/** The name of the collection. */
name: string;
/** The modes available in the collection. */
modes: Mode[];
/** The palettes available in the collection. */
palettes: string[];
}
57 changes: 57 additions & 0 deletions apps/palette-generator/src/plugin/code.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type {
Collection,
VariablesData,
} from '../contract';

/**
* Initializes the plugin by fetching collections and color variables,
* then sends the data to the UI.
*/
async function initialize() {
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const colorVars = await figma.variables.getLocalVariablesAsync('COLOR');
const collectionsForUi: Collection[] = collections.map((collection) => {
const collectionPalettes = Array.from(new Set(colorVars
.filter((v) => v.variableCollectionId === collection.id && v.name.startsWith('color/'))
.map((variable) => variable.name.split('/')[1])));
return { id: collection.id, name: collection.name, modes: collection.modes, palettes: collectionPalettes };
});
figma.showUI(__html__, { width: 400, height: 670 });
if (collectionsForUi.length > 0) {
figma.ui.postMessage({ type: 'collections', data: collectionsForUi });
} else {
figma.ui.postMessage({ type: 'no-collections' });
}
}

// eslint-disable-next-line unicorn/prefer-add-event-listener -- Figma API
figma.ui.onmessage = async (msg: { type: string; data: any }) => {
const colorVars: { key: string; value: string }[] = [];
if (msg.type === 'store-vars') {
const varData = msg.data as VariablesData;
const excludedKeys = new Set(varData.filter.split(','));
// shades input come as line seprated values, even lines represent the palette name (0, 0.5 etc) and odd ones the colors in hex format
const shadesInput = varData.shadesInput.split('\n');
for (let i = 0; i < shadesInput.length; i += 2) {
const key = (Number.parseFloat(shadesInput[i]) * 10).toString().padStart(2, '0');
if (!excludedKeys.has(key)) {
const value = shadesInput[i + 1];
colorVars.push({ key, value });
}
}
const collection = await figma.variables.getVariableCollectionByIdAsync(varData.collectionId);
const variables = await figma.variables.getLocalVariablesAsync();
for (const colorVar of colorVars) {
const varName = `color/${varData.paletteName}/${colorVar.key}`;
const existingVar = variables.find((variable) => variable.name === varName);
if (existingVar) {
existingVar.setValueForMode(varData.modeId, figma.util.rgb(colorVar.value));
} else if (collection) {
const newVar = figma.variables.createVariable(varName, collection, 'COLOR');
newVar.setValueForMode(varData.modeId, figma.util.rgb(colorVar.value));
}
}
}
};

void initialize();
Loading

0 comments on commit 939b4a6

Please sign in to comment.