├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── env.d.ts ├── manifest.json ├── package-lock.json ├── package.json ├── src ├── bridges │ ├── checks │ │ ├── checkConnectionStatus.ts │ │ ├── checkEditorType.ts │ │ ├── checkHighlightStatus.ts │ │ ├── checkPlanStatus.ts │ │ ├── checkUserConsent.ts │ │ └── checkUserPreferences.ts │ ├── creations │ │ ├── createLocalStyles.ts │ │ ├── createLocalVariables.ts │ │ └── createPalette.ts │ ├── enableTrial.ts │ ├── exports │ │ ├── exportCss.ts │ │ ├── exportCsv.ts │ │ ├── exportJson.ts │ │ ├── exportJsonAmznStyleDictionary.ts │ │ ├── exportJsonTokensStudio.ts │ │ ├── exportKt.ts │ │ ├── exportSwiftUI.ts │ │ ├── exportTailwind.ts │ │ ├── exportUIKit.ts │ │ └── exportXml.ts │ ├── getPalettesOnCurrentPage.ts │ ├── getProPlan.ts │ ├── loadParameters.ts │ ├── loadUI.ts │ ├── processSelection.ts │ ├── publication │ │ ├── authentication.ts │ │ ├── detachPalette.ts │ │ ├── publishPalette.ts │ │ ├── pullPalette.ts │ │ ├── pushPalette.ts │ │ ├── sharePalette.ts │ │ └── unpublishPalette.ts │ └── updates │ │ ├── updateColors.ts │ │ ├── updateGlobal.ts │ │ ├── updateLocalStyles.ts │ │ ├── updateLocalVariables.ts │ │ ├── updatePalette.ts │ │ ├── updateScale.ts │ │ ├── updateSettings.ts │ │ ├── updateThemes.ts │ │ └── updateView.ts ├── canvas │ ├── Colors.ts │ ├── Header.ts │ ├── LocalStyle.ts │ ├── LocalVariable.ts │ ├── Palette.ts │ ├── Paragraph.ts │ ├── Properties.ts │ ├── Property.ts │ ├── Sample.ts │ ├── Signature.ts │ ├── Status.ts │ ├── Tag.ts │ └── Title.ts ├── config.ts ├── content │ ├── images │ │ ├── choose_plan.webp │ │ ├── distribution_easing.gif │ │ ├── isb_product_thumbnail.webp │ │ ├── lock_source_colors.gif │ │ ├── pro_plan.webp │ │ ├── publication.webp │ │ ├── trial.webp │ │ └── uicp_product_thumbnail.webp │ └── locals.ts ├── index.ts ├── stores │ ├── features.ts │ ├── palette.ts │ ├── preferences.ts │ └── presets.ts ├── types │ ├── app.ts │ ├── configurations.ts │ ├── data.ts │ ├── events.ts │ ├── messages.ts │ ├── models.ts │ ├── nodes.ts │ └── user.ts ├── ui │ ├── App.tsx │ ├── app.html │ ├── components │ │ ├── Feature.tsx │ │ ├── Shade.tsx │ │ └── Slider.tsx │ ├── contexts │ │ ├── ColorSettings.tsx │ │ ├── Colors.tsx │ │ ├── CommunityPalettes.tsx │ │ ├── ContrastSettings.tsx │ │ ├── Explore.tsx │ │ ├── Export.tsx │ │ ├── GlobalSettings.tsx │ │ ├── InternalPalettes.tsx │ │ ├── Overview.tsx │ │ ├── Palettes.tsx │ │ ├── Scale.tsx │ │ ├── SelfPalettes.tsx │ │ ├── Settings.tsx │ │ ├── Source.tsx │ │ ├── SyncPreferences.tsx │ │ └── Themes.tsx │ ├── handlers │ │ ├── addStop.ts │ │ ├── deleteStop.ts │ │ ├── shiftLeftStop.ts │ │ └── shiftRightStop.ts │ ├── index.tsx │ ├── modules │ │ ├── About.tsx │ │ ├── Actions.tsx │ │ ├── Dispatcher.ts │ │ ├── Highlight.tsx │ │ ├── Icon.tsx │ │ ├── Onboarding.tsx │ │ ├── Preview.tsx │ │ ├── PriorityContainer.tsx │ │ ├── Publication.tsx │ │ ├── Shortcuts.tsx │ │ └── TrialControls.tsx │ ├── services │ │ ├── CreatePalette.tsx │ │ ├── EditPalette.tsx │ │ └── TransferPalette.tsx │ └── stylesheets │ │ ├── app-components.css │ │ └── app.css └── utils │ ├── Color.ts │ ├── Contrast.ts │ ├── doLightnessScale.ts │ ├── eventsTracker.ts │ ├── setContexts.ts │ ├── setData.ts │ ├── setPaletteMeta.ts │ ├── setPaletteMigration.ts │ ├── setPaletteName.ts │ └── userConsent.ts ├── tsconfig.json └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | # ESLint ignores 2 | node_modules/ 3 | dist/ 4 | webpack.config.js -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:@figma/figma-plugins/recommended" 12 | ], 13 | "overrides": [ 14 | ], 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "ecmaVersion": "latest", 18 | "sourceType": "module", 19 | "project": "./tsconfig.json" 20 | }, 21 | "plugins": [ 22 | "react", 23 | "@figma/figma-plugins", 24 | "@typescript-eslint" 25 | ], 26 | "rules": { 27 | "@typescript-eslint/no-explicit-any": [1], 28 | "eqeqeq": ["error", "always", { "null": "ignore" }], 29 | "@figma/figma-plugins/dynamic-page-find-method-advice": "off", 30 | "@figma/figma-plugins/ban-deprecated-id-params": "off", 31 | "@figma/figma-plugins/dynamic-page-documentchange-event-advice": "off", 32 | "curly": ["warn", "multi"], 33 | "prefer-const": "warn" 34 | }, 35 | "root": true, 36 | "settings" : { 37 | "react": { 38 | "version": "detect" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: a-ng-d 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | _A clear and concise description of what the bug is._ 12 | 13 | **To Reproduce** 14 | _Steps to reproduce the behavior:_ 15 | 1. _Go to '...'_ 16 | 2. _Click on '....'_ 17 | 3. _Scroll down to '....'_ 18 | 4. _See error_ 19 | 20 | **Expected behavior** 21 | _A clear and concise description of what you expected to happen._ 22 | 23 | **Screenshots** 24 | _If applicable, add screenshots to help explain your problem._ 25 | 26 | **Useful information (please complete the following information):** 27 | - _Browser [e.g. chrome, safari, firefox, desktop app]_ 28 | - _Link to a Figma document in which there is a copy of the UI Color Palette_ 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE REQUEST]" 5 | labels: enhancement 6 | assignees: a-ng-d 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | _A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]_ 12 | 13 | **Describe the solution you'd like** 14 | _A clear and concise description of what you want to happen._ 15 | 16 | **Describe alternatives you've considered** 17 | _A clear and concise description of any alternative solutions or features you've considered._ 18 | 19 | **Additional context** 20 | _Add any other context or screenshots about the feature request here._ 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Download UI Color Palette 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | build: 7 | name: Build with Node 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [20.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | 22 | - name: Create .env.local file 23 | run: | 24 | touch .env.local 25 | echo REACT_APP_SUPABASE_URL="${{ vars.REACT_APP_SUPABASE_URL }}" >> .env.local 26 | echo REACT_APP_SUPABASE_PUBLIC_ANON_KEY="${{ secrets.REACT_APP_SUPABASE_PUBLIC_ANON_KEY }}" >> .env.local 27 | echo REACT_APP_SENTRY_DSN="${{ vars.REACT_APP_SENTRY_DSN }}" >> .env.local 28 | echo SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}" >> .env.local 29 | echo REACT_APP_MIXPANEL_TOKEN="${{ secrets.REACT_APP_MIXPANEL_TOKEN }}" >> .env.local 30 | echo REACT_APP_AUTH_WORKER_URL="${{ vars.REACT_APP_AUTH_WORKER_URL }}" >> .env.local 31 | echo REACT_APP_ANNOUNCEMENTS_WORKER_URL="${{ vars.REACT_APP_ANNOUNCEMENTS_WORKER_URL }}" >> .env.local 32 | echo REACT_APP_NOTION_ANNOUNCEMENTS_ID="${{ vars.REACT_APP_NOTION_ANNOUNCEMENTS_ID }}" >> .env.local 33 | cat .env.local 34 | touch .env.sentry-build-plugin 35 | echo SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}" >> .env.sentry-build-plugin 36 | cat .env.sentry-build-plugin 37 | 38 | - name: Install and Build 39 | run: | 40 | npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }} 41 | npm install 42 | npm run build:prod 43 | 44 | - name: Archive production artifact 45 | uses: actions/upload-artifact@v4 46 | with: 47 | name: figma-ui-color-palette 48 | path: | 49 | dist 50 | manifest.json 51 | retention-days: 5 52 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release for Figma/FigJam/Dev mode 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | branches: 7 | - dev 8 | 9 | jobs: 10 | create-release: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Extract package version 21 | id: extract_version 22 | run: | 23 | package_version=$(jq -r '.version' package.json) 24 | echo "::set-output name=package_version::$package_version" 25 | 26 | - name: Create release 27 | id: create_release 28 | if: github.event.pull_request.merged == true && startsWith(github.head_ref, 'ui-color-palette-') 29 | uses: actions/create-release@v1 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | with: 33 | tag_name: v${{ steps.extract_version.outputs.package_version }} 34 | release_name: ${{ github.event.pull_request.title}} 35 | body: | 36 | ## What's Changed 37 | [Friendly release note](https://ui-color-palette.canny.io/changelog/${{ github.head_ref }})・[Full Changelog](https://github.com/a-ng-d/figma-ui-color-palette/commits/${{ github.head_ref }}) 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | *.local 5 | .env.sentry-build-plugin 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /src/content/images -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-organize-imports", "prettier-plugin-css-order"], 3 | "trailingComma": "es5", 4 | "tabWidth": 2, 5 | "semi": false, 6 | "singleQuote": true, 7 | "parser": "typescript", 8 | "singleAttributePerLine": true, 9 | "overrides": [ 10 | { 11 | "files": "*.css", 12 | "options": { 13 | "parser": "css" 14 | } 15 | }, 16 | { 17 | "files": "*.html", 18 | "options": { 19 | "parser": "html", 20 | "singleAttributePerLine": true 21 | } 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Aurélien Grimaud 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GitHub package.json version](https://img.shields.io/github/package-json/v/a-ng-d/figma-ui-color-palette?color=informational) ![GitHub last commit](https://img.shields.io/github/last-commit/a-ng-d/figma-ui-color-palette?color=informational) ![GitHub](https://img.shields.io/github/license/a-ng-d/figma-ui-color-palette?color=informational) 2 | 3 | # UI Color Palette 4 | UI Color Palette is a Figma and FigJam plugin that creates consistent and accessible color palettes specifically for UI. The plugin uses alternative color spaces, like `LCH`, `OKLCH`, `CIELAB`, `OKLAB`, and `HSLuv`, to create color shades based on the configured lightness scale. These spaces ensure [WCAG standards](https://www.w3.org/WAI/standards-guidelines/wcag/) compliance and sufficient contrast between information and background color. 5 | 6 | The idea to make this Figma plugin comes from the article: [Accessible Palette: stop using HSL for color systems](https://wildbit.com/blog/accessible-palette-stop-using-hsl-for-color-systems). 7 | 8 | This plugin will allow you to: 9 | - Create a complete palette from any existing color to help you build a color scaling (or Primitive colors). 10 | - Manage the color palette in real-time to control the contrast. 11 | - Sync the color shades with local styles, variables, and third-party plugins. 12 | - Generate code in various languages. 13 | - Publish the palette for reuse across multiple documents or add shared palettes from the community. 14 | 15 | ## Documentation 16 | The full documentation can be consulted on [docs.ui-color-palette.com](https://uicp.ylb.lt/docs). 17 | 18 | ## Contribution 19 | ### Community 20 | Ask questions, submit your ideas or requests on [Canny](https://uicp.ylb.lt/ideas). 21 | 22 | ### Issues 23 | Have you encountered a bug? Could a feature be improved? 24 | Go to the [Issues](https://uicp.ylb.lt/report) section and browse the existing tickets or create a new one. 25 | 26 | ### Development 27 | - Clone this repository (or fork it). 28 | - Install dependencies with `npm install`. 29 | - Run `npm run start` to watch in development mode. 30 | - Go to Figma, then `Plugins` > `Development` > `Import plugin from manifest…` and choose `manifest.json` in the repository. 31 | - Create a `Branch` and open a `Pull Request`. 32 | - _Let's do this._ 33 | 34 | ### Beta test 35 | - Go to the [Actions](https://github.com/a-ng-d/figma-ui-color-palette/actions) sections and access the `Build and Download UI Color Palette` tab. 36 | - Click `Run workflow`, then select a branch and confirm. 37 | - Wait a minute, and once finished, download the artifact (which is a ZIP file containing the plugin). 38 | - Go to Figma, then `Plugins` > `Development` > `Import plugin from manifest…` and choose `manifest.json` in the unzipped folder. 39 | - _Enjoy!_ 40 | 41 | ## Attribution 42 | - The colors are managed thanks to the [chroma.js](https://github.com/gka/chroma.js) library by [Gregor Aisch](https://github.com/gka). 43 | - The APCA algorithm is provided thanks to the [apca-w3](https://www.npmjs.com/package/apca-w3) module by [Andrew Somers](https://github.com/Myndex). 44 | - The Figma components are emulated thanks to the [Figma Plugin DS](https://github.com/thomas-lowry/figma-plugin-ds) stylesheet by [Tom Lowry](https://github.com/thomas-lowry). 45 | 46 | ## Support 47 | - [Follow the plugin LinkedIn page](https://uicp.ylb.lt/network). 48 | - [Connect to my Figma resources page](https://uicp.ylb.lt/author). 49 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.webp' { 2 | const value: string 3 | export = value 4 | } 5 | declare module '*.gif' { 6 | const value: string 7 | export = value 8 | } 9 | declare module 'JSZip' 10 | declare module 'react-dom/client' 11 | declare module 'apca-w3' 12 | declare module 'color-blind' -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "UI Color Palette・WCAG-Compliant Color Palette Suite", 3 | "id": "1063959496693642315", 4 | "api": "1.0.0", 5 | "editorType": [ 6 | "figma", 7 | "figjam", 8 | "dev" 9 | ], 10 | "capabilities": [ 11 | "inspect", 12 | "vscode" 13 | ], 14 | "relaunchButtons": [ 15 | { "command": "open", "name": "Open UI Color Palette" }, 16 | { "command": "create", "name": "Create a UI Color Palette", "multipleSelection": true }, 17 | { "command": "edit", "name": "Edit the UI Color Palette" } 18 | ], 19 | "permissions": [ 20 | "payments", 21 | "currentuser" 22 | ], 23 | "documentAccess": "dynamic-page", 24 | "networkAccess": { 25 | "allowedDomains": [ 26 | "https://figma.com", 27 | "https://*.figma.com", 28 | "https://rsms.me", 29 | "https://*.gravatar.com", 30 | "https://*.wp.com", 31 | "https://corsproxy.io", 32 | "https://zclweepgvqkrelyfwhma.supabase.co", 33 | "https://*.yelbolt.workers.dev", 34 | "https://www.colourlovers.com", 35 | "https://*.mixpanel.com", 36 | "https://asset.brandfetch.io", 37 | "https://*.sentry.io", 38 | "https://*.amazonaws.com" 39 | ], 40 | "devAllowedDomains": [ 41 | "http://localhost:3000", 42 | "http://localhost:8787", 43 | "http://localhost:8888" 44 | ] 45 | }, 46 | "parameters": [ 47 | { 48 | "name": "Preset", 49 | "key": "preset", 50 | "description": "Select a specific preset" 51 | }, 52 | { 53 | "name": "Color space", 54 | "key": "space", 55 | "description": "Select a specific color space" 56 | }, 57 | { 58 | "name": "Palette layout", 59 | "key": "view", 60 | "description": "Select a specific layout" 61 | }, 62 | { 63 | "name": "Palette name", 64 | "key": "name", 65 | "description": "Select source colors and type a name", 66 | "allowFreeform": true, 67 | "optional": true 68 | } 69 | ], 70 | "parameterOnly": false, 71 | "main": "dist/code.js", 72 | "ui": "dist/ui.html" 73 | } 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "figma-ui-color-palette", 3 | "version": "4.3.0", 4 | "description": "UI Color Palette is a Figma and FigJam plugin that creates, manages, deploys, and publishes consistent and accessible color palettes.", 5 | "main": "code.js", 6 | "scripts": { 7 | "start": "npm-run-all -p start:announcements_worker start:auth_lobby start:dev", 8 | "clone:announcements_worker": "git clone https://github.com/a-ng-d/announcements-yelbolt-worker.git && cd announcements-yelbolt-worker && npm install", 9 | "clone:auth_worker": "git clone https://github.com/a-ng-d/auth-yelbolt-worker.git && cd auth-yelbolt-worker && npm install", 10 | "clone:auth_lobby": "git clone https://github.com/a-ng-d/auth-yelbolt.git && cd auth-yelbolt && npm run install", 11 | "start:announcements_worker": "cd ../announcements-yelbolt-worker && npm run start:8888", 12 | "start:auth_lobby": "cd ../auth-yelbolt && npm run ui-color-palette", 13 | "start:dev": "npx webpack --mode=development --watch", 14 | "build": "npx webpack --mode=development", 15 | "build:prod": "npx webpack --mode=production", 16 | "typecheck": "npx tsc --noEmit", 17 | "typecheck:watch": "npx tsc --noEmit --watch", 18 | "lint": "npx eslint ./src/** --fix --ext .ts,.tsx .", 19 | "lint:watch": "esw ./src/** --fix --watch --ext .ts,.tsx .", 20 | "format": "npx prettier './src' --write", 21 | "update:figmug": "npm i @a_ng_d/figmug-ui@latest @a_ng_d/figmug-utils@latest" 22 | }, 23 | "keywords": [ 24 | "color", 25 | "color-scheme", 26 | "lch", 27 | "color-system", 28 | "ui-color", 29 | "figma-plugin" 30 | ], 31 | "author": "Aurélien Grimaud", 32 | "licence": "MIT", 33 | "repository": "https://github.com/a-ng-d/figma-ui-color-palette", 34 | "devDependencies": { 35 | "@figma/eslint-plugin-figma-plugins": "^0.15.0", 36 | "@figma/plugin-typings": "^1.89.0", 37 | "@types/chroma-js": "^2.4.0", 38 | "@types/file-saver": "^2.0.5", 39 | "@types/react": "^17.0.37", 40 | "@types/react-dom": "^17.0.11", 41 | "@typescript-eslint/eslint-plugin": "^5.62.0", 42 | "@typescript-eslint/parser": "^5.62.0", 43 | "css-loader": "^6.5.1", 44 | "dotenv-webpack": "^8.0.1", 45 | "eslint": "^8.57.0", 46 | "eslint-config-standard-with-typescript": "^34.0.1", 47 | "eslint-plugin-import": "^2.26.0", 48 | "eslint-plugin-n": "^15.7.0", 49 | "eslint-plugin-promise": "^6.1.1", 50 | "eslint-plugin-react": "^7.25.3", 51 | "eslint-watch": "^8.0.0", 52 | "html-webpack-inline-source-plugin": "^1.0.0-beta.2", 53 | "html-webpack-plugin": "^5.5.0", 54 | "npm-run-all": "^4.1.5", 55 | "postcss": "^8.4.31", 56 | "preact": "^10.6.4", 57 | "prettier": "^3.2.5", 58 | "prettier-plugin-css-order": "^2.1.2", 59 | "prettier-plugin-organize-imports": "^4.1.0", 60 | "sass": "^1.69.5", 61 | "sass-loader": "^13.3.2", 62 | "style-loader": "^3.3.1", 63 | "ts-loader": "^9.2.6", 64 | "typescript": "^5.4.3", 65 | "url-loader": "^4.1.1", 66 | "webpack": "^5.89.0", 67 | "webpack-cli": "^4.9.1" 68 | }, 69 | "dependencies": { 70 | "@a_ng_d/figmug-ui": "^1.6.15", 71 | "@a_ng_d/figmug-utils": "^0.2.2", 72 | "@nanostores/preact": "^0.5.2", 73 | "@sentry/react": "^8.8.0", 74 | "@sentry/webpack-plugin": "^2.22.6", 75 | "@supabase/supabase-js": "^2.44.2", 76 | "apca-w3": "^0.1.9", 77 | "chroma-js": "^2.4.2", 78 | "color-blind": "^0.1.3", 79 | "file-saver": "^2.0.5", 80 | "hsluv": "^1.0.1", 81 | "jszip": "^3.10.1", 82 | "mixpanel-figma": "^2.0.4", 83 | "react": "npm:@preact/compat", 84 | "react-dom": "npm:@preact/compat", 85 | "uid": "^2.0.2" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/bridges/checks/checkConnectionStatus.ts: -------------------------------------------------------------------------------- 1 | import { supabase } from '../publication/authentication' 2 | 3 | const checkConnectionStatus = async ( 4 | accessToken: string | undefined, 5 | refreshToken: string | undefined 6 | ) => { 7 | if (accessToken !== undefined && refreshToken !== undefined) { 8 | const { data, error } = await supabase.auth.setSession({ 9 | access_token: accessToken, 10 | refresh_token: refreshToken, 11 | }) 12 | 13 | if (!error) return data 14 | else throw error 15 | } 16 | } 17 | 18 | export default checkConnectionStatus 19 | -------------------------------------------------------------------------------- /src/bridges/checks/checkEditorType.ts: -------------------------------------------------------------------------------- 1 | const checkEditorType = () => 2 | figma.ui.postMessage({ 3 | type: 'CHECK_EDITOR_TYPE', 4 | id: figma.currentUser?.id, 5 | data: figma.vscode ? 'dev_vscode' : figma.editorType, 6 | }) 7 | 8 | export default checkEditorType 9 | -------------------------------------------------------------------------------- /src/bridges/checks/checkHighlightStatus.ts: -------------------------------------------------------------------------------- 1 | const checkHighlightStatus = async (remoteVersion: string) => { 2 | const localVersion = await figma.clientStorage.getAsync('highlight_version') 3 | const isOnboardingRead = 4 | await figma.clientStorage.getAsync('is_onboarding_read') 5 | 6 | if (localVersion === undefined && remoteVersion === undefined) 7 | return figma.ui.postMessage({ 8 | type: 'PUSH_HIGHLIGHT_STATUS', 9 | data: 'NO_HIGHLIGHT', 10 | }) 11 | else if (localVersion === undefined && isOnboardingRead === undefined) 12 | return figma.ui.postMessage({ 13 | type: 'PUSH_ONBOARDING_STATUS', 14 | data: 'DISPLAY_ONBOARDING_DIALOG', 15 | }) 16 | else if (localVersion === undefined) 17 | return figma.ui.postMessage({ 18 | type: 'PUSH_HIGHLIGHT_STATUS', 19 | data: 'DISPLAY_HIGHLIGHT_DIALOG', 20 | }) 21 | else { 22 | const remoteMajorVersion = remoteVersion.split('.')[0], 23 | remoteMinorVersion = remoteVersion.split('.')[1] 24 | 25 | const localMajorVersion = localVersion.split('.')[0], 26 | localMinorVersion = localVersion.split('.')[1] 27 | 28 | if (remoteMajorVersion !== localMajorVersion) 29 | return figma.ui.postMessage({ 30 | type: 'PUSH_HIGHLIGHT_STATUS', 31 | data: 'DISPLAY_HIGHLIGHT_DIALOG', 32 | }) 33 | 34 | if (remoteMinorVersion !== localMinorVersion) 35 | return figma.ui.postMessage({ 36 | type: 'PUSH_HIGHLIGHT_STATUS', 37 | data: 'DISPLAY_HIGHLIGHT_NOTIFICATION', 38 | }) 39 | } 40 | } 41 | 42 | export default checkHighlightStatus 43 | -------------------------------------------------------------------------------- /src/bridges/checks/checkPlanStatus.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isProEnabled, 3 | oldTrialTime, 4 | trialTime, 5 | trialVersion, 6 | } from '../../config' 7 | 8 | const checkPlanStatus = async (context = 'UI' as 'UI' | 'PARAMETERS') => { 9 | // figma.clientStorage.deleteAsync('trial_start_date') 10 | // figma.clientStorage.deleteAsync('trial_version') 11 | // figma.clientStorage.setAsync( 12 | // 'trial_start_date', 13 | // new Date().getTime() - 72 * 60 * 60 * 1000 14 | // ) 15 | // figma.payments?.setPaymentStatusInDevelopment({ 16 | // type: 'UNPAID', 17 | // }) 18 | 19 | const trialStartDate: number | undefined = 20 | await figma.clientStorage.getAsync('trial_start_date'), 21 | currentTrialVersion: string = 22 | (await figma.clientStorage.getAsync('trial_version')) ?? trialVersion 23 | 24 | let consumedTime = 0, 25 | trialStatus = 'UNUSED' 26 | 27 | if (trialStartDate !== undefined) { 28 | consumedTime = (new Date().getTime() - trialStartDate) / 1000 / (60 * 60) 29 | 30 | if (consumedTime <= oldTrialTime && currentTrialVersion !== trialVersion) 31 | trialStatus = 'PENDING' 32 | else if (consumedTime >= trialTime) trialStatus = 'EXPIRED' 33 | else trialStatus = 'PENDING' 34 | } 35 | 36 | if (context === 'UI') 37 | figma.ui.postMessage({ 38 | type: 'CHECK_PLAN_STATUS', 39 | data: { 40 | planStatus: 41 | trialStatus === 'PENDING' || !isProEnabled 42 | ? 'PAID' 43 | : figma.payments?.status.type, 44 | trialStatus: trialStatus, 45 | trialRemainingTime: Math.ceil( 46 | currentTrialVersion !== trialVersion 47 | ? oldTrialTime - consumedTime 48 | : trialTime - consumedTime 49 | ), 50 | }, 51 | }) 52 | 53 | return trialStatus === 'PENDING' || !isProEnabled 54 | ? 'PAID' 55 | : figma.payments?.status.type 56 | } 57 | 58 | export default checkPlanStatus 59 | -------------------------------------------------------------------------------- /src/bridges/checks/checkUserConsent.ts: -------------------------------------------------------------------------------- 1 | import { userConsentVersion } from '../../config' 2 | import { userConsent } from '../../utils/userConsent' 3 | 4 | const checkUserConsent = async () => { 5 | const currentUserConsentVersion = await figma.clientStorage.getAsync( 6 | 'user_consent_version' 7 | ) 8 | 9 | const userConsentData = await Promise.all( 10 | userConsent.map(async (consent) => { 11 | return { 12 | ...consent, 13 | isConsented: 14 | (await figma.clientStorage.getAsync(`${consent.id}_user_consent`)) ?? 15 | false, 16 | } 17 | }) 18 | ) 19 | 20 | figma.ui.postMessage({ 21 | type: 'CHECK_USER_CONSENT', 22 | mustUserConsent: 23 | currentUserConsentVersion !== userConsentVersion || 24 | currentUserConsentVersion === undefined, 25 | userConsent: userConsentData, 26 | }) 27 | } 28 | 29 | export default checkUserConsent 30 | -------------------------------------------------------------------------------- /src/bridges/checks/checkUserPreferences.ts: -------------------------------------------------------------------------------- 1 | const checkUserPreferences = async () => { 2 | const isWCAGDisplayed = 3 | await figma.clientStorage.getAsync('is_wcag_displayed') 4 | const isAPCADisplayed = 5 | await figma.clientStorage.getAsync('is_apca_displayed') 6 | const canDeepSyncPalette = await figma.clientStorage.getAsync( 7 | 'can_deep_sync_palette' 8 | ) 9 | const canDeepSyncVariables = await figma.clientStorage.getAsync( 10 | 'can_deep_sync_variables' 11 | ) 12 | const canDeepSyncStyles = await figma.clientStorage.getAsync( 13 | 'can_deep_sync_styles' 14 | ) 15 | const isVsCodeMessageDisplayed = await figma.clientStorage.getAsync( 16 | 'is_vs_code_displayed' 17 | ) 18 | 19 | if (isWCAGDisplayed === undefined) 20 | await figma.clientStorage.setAsync('is_wcag_displayed', true) 21 | 22 | if (isAPCADisplayed === undefined) 23 | await figma.clientStorage.setAsync('is_apca_displayed', true) 24 | 25 | if (canDeepSyncPalette === undefined) 26 | await figma.clientStorage.setAsync('can_deep_sync_palette', false) 27 | 28 | if (canDeepSyncVariables === undefined) 29 | await figma.clientStorage.setAsync('can_deep_sync_variables', false) 30 | 31 | if (canDeepSyncStyles === undefined) 32 | await figma.clientStorage.setAsync('can_deep_sync_styles', false) 33 | 34 | if (isVsCodeMessageDisplayed === undefined) 35 | await figma.clientStorage.setAsync('is_vs_code_displayed', true) 36 | 37 | figma.ui.postMessage({ 38 | type: 'CHECK_USER_PREFERENCES', 39 | data: { 40 | isWCAGDisplayed: isWCAGDisplayed ?? true, 41 | isAPCADisplayed: isAPCADisplayed ?? true, 42 | canDeepSyncPalette: canDeepSyncPalette ?? false, 43 | canDeepSyncVariables: canDeepSyncVariables ?? false, 44 | canDeepSyncStyles: canDeepSyncStyles ?? false, 45 | isVsCodeMessageDisplayed: isVsCodeMessageDisplayed ?? true, 46 | }, 47 | }) 48 | 49 | return true 50 | } 51 | 52 | export default checkUserPreferences 53 | -------------------------------------------------------------------------------- /src/bridges/creations/createLocalStyles.ts: -------------------------------------------------------------------------------- 1 | import LocalStyle from '../../canvas/LocalStyle' 2 | import { lang, locals } from '../../content/locals' 3 | import { PaletteData } from '../../types/data' 4 | 5 | const createLocalStyles = async (palette: FrameNode) => { 6 | const paletteData: PaletteData = JSON.parse(palette.getPluginData('data')), 7 | workingThemes = 8 | paletteData.themes.filter((theme) => theme.type === 'custom theme') 9 | .length === 0 10 | ? paletteData.themes.filter((theme) => theme.type === 'default theme') 11 | : paletteData.themes.filter((theme) => theme.type === 'custom theme') 12 | 13 | if (palette.children.length === 1) { 14 | const createdLocalStylesStatusMessage = figma 15 | .getLocalPaintStylesAsync() 16 | .then((localStyles) => { 17 | let i = 0 18 | workingThemes.forEach((theme) => { 19 | theme.colors.forEach((color) => { 20 | color.shades.forEach((shade) => { 21 | if ( 22 | localStyles.find( 23 | (localStyle) => localStyle.id === shade.styleId 24 | ) === undefined 25 | ) { 26 | const style = new LocalStyle( 27 | workingThemes[0].type === 'custom theme' 28 | ? `${ 29 | paletteData.name === '' ? '' : paletteData.name + '/' 30 | }${theme.name}/${color.name}/${shade.name}` 31 | : `${paletteData.name === '' ? '' : paletteData.name}/${ 32 | color.name 33 | }/${shade.name}`, 34 | color.description !== '' 35 | ? color.description + 36 | locals[lang].separator + 37 | shade.description 38 | : shade.description, 39 | { 40 | r: shade.gl[0], 41 | g: shade.gl[1], 42 | b: shade.gl[2], 43 | } 44 | ).makePaintStyle() 45 | shade.styleId = style.id 46 | i++ 47 | } 48 | }) 49 | }) 50 | }) 51 | palette.setPluginData('data', JSON.stringify(paletteData)) 52 | 53 | if (i > 1) return `${i} ${locals[lang].info.createdLocalStyles.plural}` 54 | else if (i === 1) return locals[lang].info.createdLocalStyle.single 55 | else return locals[lang].info.createdLocalStyles.none 56 | }) 57 | .catch(() => locals[lang].error.generic) 58 | 59 | return await createdLocalStylesStatusMessage 60 | } else locals[lang].error.corruption 61 | } 62 | 63 | export default createLocalStyles 64 | -------------------------------------------------------------------------------- /src/bridges/creations/createPalette.ts: -------------------------------------------------------------------------------- 1 | import Palette from '../../canvas/Palette' 2 | import { lang, locals } from '../../content/locals' 3 | import { 4 | MetaConfiguration, 5 | PaletteConfiguration, 6 | SourceColorConfiguration, 7 | ThemeConfiguration, 8 | } from '../../types/configurations' 9 | 10 | interface Msg { 11 | data: { 12 | sourceColors: Array 13 | palette: PaletteConfiguration 14 | themes?: Array 15 | isRemote?: boolean 16 | paletteMeta?: MetaConfiguration 17 | } 18 | } 19 | 20 | const createPalette = async (msg: Msg) => { 21 | const scene: SceneNode[] = [] 22 | 23 | const creatorAvatarImg = msg.data.paletteMeta?.creatorIdentity.creatorFullName 24 | ? await figma 25 | .createImageAsync(msg.data.paletteMeta.creatorIdentity.creatorAvatar) 26 | .then(async (image: Image) => image) 27 | .catch(() => null) 28 | : null 29 | 30 | const palette = new Palette( 31 | msg.data.sourceColors, 32 | msg.data.palette.name, 33 | msg.data.palette.description, 34 | msg.data.palette.preset, 35 | msg.data.palette.scale, 36 | msg.data.palette.shift, 37 | msg.data.palette.areSourceColorsLocked, 38 | msg.data.palette.colorSpace, 39 | msg.data.palette.visionSimulationMode, 40 | msg.data.palette.view, 41 | msg.data.palette.textColorsTheme, 42 | msg.data.palette.algorithmVersion, 43 | msg.data.themes, 44 | msg.data.isRemote, 45 | msg.data.paletteMeta, 46 | creatorAvatarImg 47 | ).makeNode() 48 | 49 | if (palette.children.length !== 0) { 50 | figma.currentPage.appendChild(palette) 51 | scene.push(palette) 52 | palette.x = figma.viewport.center.x - palette.width / 2 53 | palette.y = figma.viewport.center.y - palette.height / 2 54 | figma.currentPage.selection = scene 55 | figma.viewport.scrollAndZoomIntoView(scene) 56 | 57 | await figma.saveVersionHistoryAsync( 58 | locals[lang].info.paletteCreated.replace('$1', msg.data.palette.name) 59 | ) 60 | 61 | return true 62 | } else palette.remove() 63 | } 64 | 65 | export default createPalette 66 | -------------------------------------------------------------------------------- /src/bridges/enableTrial.ts: -------------------------------------------------------------------------------- 1 | import { trialTime, trialVersion } from '../config' 2 | 3 | const enableTrial = async () => { 4 | const date = new Date().getTime() 5 | 6 | await figma.clientStorage 7 | .setAsync('trial_start_date', date) 8 | .then(() => figma.clientStorage.setAsync('trial_version', trialVersion)) 9 | .then(() => 10 | figma.ui.postMessage({ 11 | type: 'ENABLE_TRIAL', 12 | id: figma.currentUser?.id ?? 'NC', 13 | date: date, 14 | trialTime: trialTime, 15 | }) 16 | ) 17 | } 18 | 19 | export default enableTrial 20 | -------------------------------------------------------------------------------- /src/bridges/exports/exportCss.ts: -------------------------------------------------------------------------------- 1 | import { Case } from '@a_ng_d/figmug-utils' 2 | import { lang, locals } from '../../content/locals' 3 | import { PaletteData, PaletteDataShadeItem } from '../../types/data' 4 | import { ActionsList } from '../../types/models' 5 | 6 | const exportCss = (palette: FrameNode, colorSpace: 'RGB' | 'LCH' | 'P3') => { 7 | const paletteData: PaletteData = JSON.parse(palette.getPluginData('data')), 8 | workingThemes = 9 | paletteData.themes.filter((theme) => theme.type === 'custom theme') 10 | .length === 0 11 | ? paletteData.themes.filter((theme) => theme.type === 'default theme') 12 | : paletteData.themes.filter((theme) => theme.type === 'custom theme'), 13 | css: Array = [] 14 | 15 | const setValueAccordingToColorSpace = (shade: PaletteDataShadeItem) => { 16 | const actions: ActionsList = { 17 | RGB: () => 18 | `rgb(${Math.floor(shade.rgb[0])}, ${Math.floor( 19 | shade.rgb[1] 20 | )}, ${Math.floor(shade.rgb[2])})`, 21 | HEX: () => shade.hex, 22 | HSL: () => 23 | `hsl(${Math.floor(shade.hsl[0])} ${Math.floor( 24 | shade.hsl[1] 25 | )}% ${Math.floor(shade.hsl[2])}%)`, 26 | LCH: () => 27 | `lch(${Math.floor(shade.lch[0])}% ${Math.floor( 28 | shade.lch[1] 29 | )} ${Math.floor(shade.lch[2])})`, 30 | P3: () => 31 | `color(display-p3 ${shade.gl[0].toFixed(3)} ${shade.gl[1].toFixed( 32 | 3 33 | )} ${shade.gl[2].toFixed(3)})`, 34 | } 35 | 36 | return actions[colorSpace ?? 'RGB']?.() 37 | } 38 | 39 | if (palette.children.length === 1) { 40 | workingThemes.forEach((theme) => { 41 | const rowCss: Array = [] 42 | theme.colors.forEach((color) => { 43 | rowCss.push(`/* ${color.name} */`) 44 | color.shades.forEach((shade) => { 45 | rowCss.push( 46 | `--${new Case(color.name).doKebabCase()}-${shade.name}: ${setValueAccordingToColorSpace(shade)};` 47 | ) 48 | }) 49 | rowCss.push('') 50 | }) 51 | rowCss.pop() 52 | css.push( 53 | `:root${ 54 | theme.type === 'custom theme' 55 | ? `[data-theme='${new Case(theme.name).doKebabCase()}']` 56 | : '' 57 | } {\n ${rowCss.join('\n ')}\n}` 58 | ) 59 | }) 60 | 61 | figma.ui.postMessage({ 62 | type: 'EXPORT_PALETTE_CSS', 63 | id: figma.currentUser?.id, 64 | context: 'CSS', 65 | colorSpace: colorSpace, 66 | data: css.join('\n\n'), 67 | }) 68 | } else figma.notify(locals[lang].error.corruption) 69 | } 70 | 71 | export default exportCss 72 | -------------------------------------------------------------------------------- /src/bridges/exports/exportCsv.ts: -------------------------------------------------------------------------------- 1 | import { lang, locals } from '../../content/locals' 2 | import { PaletteData } from '../../types/data' 3 | 4 | interface colorCsv { 5 | name: string 6 | csv: string 7 | } 8 | 9 | interface themeCsv { 10 | name: string 11 | colors: Array 12 | type: string 13 | } 14 | 15 | const exportCsv = (palette: FrameNode) => { 16 | const paletteData: PaletteData = JSON.parse(palette.getPluginData('data')), 17 | workingThemes = 18 | paletteData.themes.filter((theme) => theme.type === 'custom theme') 19 | .length === 0 20 | ? paletteData.themes.filter((theme) => theme.type === 'default theme') 21 | : paletteData.themes.filter((theme) => theme.type === 'custom theme'), 22 | colorCsv: Array = [], 23 | themeCsv: Array = [], 24 | lightness: Array = [], 25 | l: Array = [], 26 | c: Array = [], 27 | h: Array = [] 28 | 29 | if (palette.children.length === 1) { 30 | workingThemes.forEach((theme) => { 31 | theme.colors.forEach((color) => { 32 | color.shades.forEach((shade) => { 33 | lightness.push(shade.name) 34 | l.push(Math.floor(shade.lch[0])) 35 | c.push(Math.floor(shade.lch[1])) 36 | h.push(Math.floor(shade.lch[2])) 37 | }) 38 | colorCsv.push({ 39 | name: color.name, 40 | csv: `${color.name},Lightness,Chroma,Hue\n${lightness 41 | .map((stop, index) => `${stop},${l[index]},${c[index]},${h[index]}`) 42 | .join('\n')}`, 43 | }) 44 | lightness.splice(0, lightness.length) 45 | l.splice(0, l.length) 46 | c.splice(0, c.length) 47 | h.splice(0, h.length) 48 | }) 49 | themeCsv.push({ 50 | name: theme.name, 51 | colors: colorCsv.map((c) => { 52 | return c 53 | }), 54 | type: theme.type, 55 | }) 56 | colorCsv.splice(0, colorCsv.length) 57 | }) 58 | 59 | figma.ui.postMessage({ 60 | type: 'EXPORT_PALETTE_CSV', 61 | id: figma.currentUser?.id, 62 | context: 'CSV', 63 | data: 64 | paletteData.themes[0].colors.length === 0 65 | ? [ 66 | { 67 | name: 'empty', 68 | colors: [{ csv: locals[lang].warning.emptySourceColors }], 69 | }, 70 | ] 71 | : themeCsv, 72 | }) 73 | } else figma.notify(locals[lang].error.corruption) 74 | } 75 | 76 | export default exportCsv 77 | -------------------------------------------------------------------------------- /src/bridges/exports/exportJson.ts: -------------------------------------------------------------------------------- 1 | import { lang, locals } from '../../content/locals' 2 | import { PaletteData, PaletteDataShadeItem } from '../../types/data' 3 | 4 | const exportJson = (palette: FrameNode) => { 5 | const paletteData: PaletteData = JSON.parse(palette.getPluginData('data')), 6 | workingThemes = 7 | paletteData.themes.filter((theme) => theme.type === 'custom theme') 8 | .length === 0 9 | ? paletteData.themes.filter((theme) => theme.type === 'default theme') 10 | : paletteData.themes.filter((theme) => theme.type === 'custom theme'), 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | json: { [key: string]: any } = {} 13 | 14 | const model = (shade: PaletteDataShadeItem) => { 15 | return { 16 | rgb: { 17 | r: Math.floor(shade.rgb[0]), 18 | g: Math.floor(shade.rgb[1]), 19 | b: Math.floor(shade.rgb[2]), 20 | }, 21 | gl: { 22 | r: parseFloat(shade.gl[0].toFixed(3)), 23 | g: parseFloat(shade.gl[1].toFixed(3)), 24 | b: parseFloat(shade.gl[2].toFixed(3)), 25 | }, 26 | lch: { 27 | l: Math.floor(shade.lch[0]), 28 | c: Math.floor(shade.lch[1]), 29 | h: Math.floor(shade.lch[2]), 30 | }, 31 | oklch: { 32 | l: parseFloat(shade.oklch[0].toFixed(3)), 33 | c: parseFloat(shade.oklch[1].toFixed(3)), 34 | h: Math.floor(shade.oklch[2]), 35 | }, 36 | lab: { 37 | l: Math.floor(shade.lab[0]), 38 | a: Math.floor(shade.lab[1]), 39 | b: Math.floor(shade.lab[2]), 40 | }, 41 | oklab: { 42 | l: parseFloat(shade.oklab[0].toFixed(3)), 43 | a: parseFloat(shade.oklab[1].toFixed(3)), 44 | b: parseFloat(shade.oklab[2].toFixed(3)), 45 | }, 46 | hsl: { 47 | h: Math.floor(shade.hsl[0]), 48 | s: Math.floor(shade.hsl[1] * 100), 49 | l: Math.floor(shade.hsl[2] * 100), 50 | }, 51 | hsluv: { 52 | h: Math.floor(shade.hsluv[0]), 53 | s: Math.floor(shade.hsluv[1]), 54 | l: Math.floor(shade.hsluv[2]), 55 | }, 56 | hex: shade.hex, 57 | description: shade.description, 58 | type: 'color shade', 59 | } 60 | } 61 | 62 | if (palette.children.length === 1) { 63 | if (workingThemes[0].type === 'custom theme') 64 | workingThemes.forEach((theme) => { 65 | json[theme.name] = {} 66 | theme.colors.forEach((color) => { 67 | json[theme.name][color.name] = {} 68 | color.shades.reverse().forEach((shade) => { 69 | json[theme.name][color.name][shade.name] = model(shade) 70 | }) 71 | json[theme.name][color.name]['description'] = color.description 72 | json[theme.name][color.name]['type'] = 'color' 73 | }) 74 | json[theme.name]['description'] = theme.description 75 | json[theme.name]['type'] = 'color theme' 76 | }) 77 | else 78 | workingThemes.forEach((theme) => { 79 | theme.colors.forEach((color) => { 80 | json[color.name] = {} 81 | color.shades.sort().forEach((shade) => { 82 | json[color.name][shade.name] = model(shade) 83 | }) 84 | json[color.name]['description'] = color.description 85 | json[color.name]['type'] = 'color' 86 | }) 87 | }) 88 | 89 | json['descrption'] = paletteData.description 90 | json['type'] = 'color palette' 91 | 92 | figma.ui.postMessage({ 93 | type: 'EXPORT_PALETTE_JSON', 94 | id: figma.currentUser?.id, 95 | context: 'TOKENS_GLOBAL', 96 | data: JSON.stringify(json, null, ' '), 97 | }) 98 | } else figma.notify(locals[lang].error.corruption) 99 | } 100 | 101 | export default exportJson 102 | -------------------------------------------------------------------------------- /src/bridges/exports/exportJsonAmznStyleDictionary.ts: -------------------------------------------------------------------------------- 1 | import { lang, locals } from '../../content/locals' 2 | import { 3 | PaletteData, 4 | PaletteDataColorItem, 5 | PaletteDataShadeItem, 6 | } from '../../types/data' 7 | 8 | const exportJsonAmznStyleDictionary = (palette: FrameNode) => { 9 | const paletteData: PaletteData = JSON.parse(palette.getPluginData('data')), 10 | workingThemes = 11 | paletteData.themes.filter((theme) => theme.type === 'custom theme') 12 | .length === 0 13 | ? paletteData.themes.filter((theme) => theme.type === 'default theme') 14 | : paletteData.themes.filter((theme) => theme.type === 'custom theme'), 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | json: { [key: string]: any } = { 17 | color: {}, 18 | } 19 | 20 | const model = (color: PaletteDataColorItem, shade: PaletteDataShadeItem) => { 21 | return { 22 | value: shade.hex, 23 | comment: 24 | color.description !== '' 25 | ? color.description + locals[lang].separator + shade.description 26 | : shade.description, 27 | } 28 | } 29 | 30 | paletteData.themes[0].colors.forEach((color) => { 31 | json['color'][color.name] = {} 32 | }) 33 | 34 | if (palette.children.length === 1) { 35 | if (workingThemes[0].type === 'custom theme') 36 | workingThemes.forEach((theme) => { 37 | theme.colors.forEach((color) => { 38 | json['color'][color.name][theme.name] = {} 39 | color.shades.reverse().forEach((shade) => { 40 | json['color'][color.name][theme.name][shade.name] = model( 41 | color, 42 | shade 43 | ) 44 | }) 45 | }) 46 | }) 47 | else 48 | workingThemes.forEach((theme) => { 49 | theme.colors.forEach((color) => { 50 | json['color'][color.name] = {} 51 | color.shades.sort().forEach((shade) => { 52 | json['color'][color.name][shade.name] = model(color, shade) 53 | }) 54 | }) 55 | }) 56 | 57 | figma.ui.postMessage({ 58 | type: 'EXPORT_PALETTE_JSON', 59 | id: figma.currentUser?.id, 60 | context: 'TOKENS_AMZN_STYLE_DICTIONARY', 61 | data: JSON.stringify(json, null, ' '), 62 | }) 63 | } else figma.notify(locals[lang].error.corruption) 64 | } 65 | 66 | export default exportJsonAmznStyleDictionary 67 | -------------------------------------------------------------------------------- /src/bridges/exports/exportJsonTokensStudio.ts: -------------------------------------------------------------------------------- 1 | import { lang, locals } from '../../content/locals' 2 | import { 3 | PaletteData, 4 | PaletteDataColorItem, 5 | PaletteDataShadeItem, 6 | } from '../../types/data' 7 | 8 | const exportJsonTokensStudio = (palette: FrameNode) => { 9 | const paletteData: PaletteData = JSON.parse(palette.getPluginData('data')), 10 | workingThemes = 11 | paletteData.themes.filter((theme) => theme.type === 'custom theme') 12 | .length === 0 13 | ? paletteData.themes.filter((theme) => theme.type === 'default theme') 14 | : paletteData.themes.filter((theme) => theme.type === 'custom theme'), 15 | name: string = 16 | palette.getPluginData('name') === '' 17 | ? locals[lang].name 18 | : palette.getPluginData('name'), 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 20 | json: { [key: string]: any } = {} 21 | 22 | const model = (color: PaletteDataColorItem, shade: PaletteDataShadeItem) => { 23 | return { 24 | value: shade.hex, 25 | description: 26 | color.description !== '' 27 | ? color.description + locals[lang].separator + shade.description 28 | : shade.description, 29 | type: 'color', 30 | } 31 | } 32 | 33 | if (palette.children.length === 1) { 34 | if (workingThemes[0].type === 'custom theme') 35 | workingThemes.forEach((theme) => { 36 | json[name + ' - ' + theme.name] = {} 37 | theme.colors.forEach((color) => { 38 | json[name + ' - ' + theme.name][color.name] = {} 39 | color.shades.reverse().forEach((shade) => { 40 | json[name + ' - ' + theme.name][color.name][shade.name] = model( 41 | color, 42 | shade 43 | ) 44 | }) 45 | json[name + ' - ' + theme.name][color.name]['type'] = 'color' 46 | }) 47 | }) 48 | else 49 | workingThemes.forEach((theme) => { 50 | json[name] = {} 51 | theme.colors.forEach((color) => { 52 | json[name][color.name] = {} 53 | color.shades.sort().forEach((shade) => { 54 | json[name][color.name][shade.name] = model(color, shade) 55 | }) 56 | json[name][color.name]['type'] = 'color' 57 | }) 58 | }) 59 | 60 | figma.ui.postMessage({ 61 | type: 'EXPORT_PALETTE_JSON', 62 | id: figma.currentUser?.id, 63 | context: 'TOKENS_TOKENS_STUDIO', 64 | data: JSON.stringify(json, null, ' '), 65 | }) 66 | } else figma.notify(locals[lang].error.corruption) 67 | } 68 | 69 | export default exportJsonTokensStudio 70 | -------------------------------------------------------------------------------- /src/bridges/exports/exportKt.ts: -------------------------------------------------------------------------------- 1 | import { Case } from '@a_ng_d/figmug-utils' 2 | import { lang, locals } from '../../content/locals' 3 | import { PaletteData } from '../../types/data' 4 | 5 | const exportKt = (palette: FrameNode) => { 6 | const paletteData: PaletteData = JSON.parse(palette.getPluginData('data')), 7 | workingThemes = 8 | paletteData.themes.filter((theme) => theme.type === 'custom theme') 9 | .length === 0 10 | ? paletteData.themes.filter((theme) => theme.type === 'default theme') 11 | : paletteData.themes.filter((theme) => theme.type === 'custom theme'), 12 | kotlin: Array = [] 13 | 14 | if (palette.children.length === 1) { 15 | workingThemes.forEach((theme) => { 16 | theme.colors.forEach((color) => { 17 | const colors: Array = [] 18 | colors.unshift( 19 | `// ${ 20 | workingThemes[0].type === 'custom theme' ? theme.name + ' - ' : '' 21 | }${color.name}` 22 | ) 23 | color.shades.forEach((shade) => { 24 | colors.unshift( 25 | `val ${ 26 | workingThemes[0].type === 'custom theme' 27 | ? new Case(theme.name + ' ' + color.name).doSnakeCase() 28 | : new Case(color.name).doSnakeCase() 29 | }_${shade.name} = Color(${shade.hex 30 | .replace('#', '0xFF') 31 | .toUpperCase()})` 32 | ) 33 | }) 34 | colors.unshift('') 35 | colors.reverse().forEach((color) => kotlin.push(color)) 36 | }) 37 | }) 38 | 39 | kotlin.pop() 40 | 41 | figma.ui.postMessage({ 42 | type: 'EXPORT_PALETTE_KT', 43 | id: figma.currentUser?.id, 44 | context: 'ANDROID_COMPOSE', 45 | data: `import androidx.compose.ui.graphics.Color\n\n${kotlin.join('\n')}`, 46 | }) 47 | } else figma.notify(locals[lang].error.corruption) 48 | } 49 | 50 | export default exportKt 51 | -------------------------------------------------------------------------------- /src/bridges/exports/exportSwiftUI.ts: -------------------------------------------------------------------------------- 1 | import { Case } from '@a_ng_d/figmug-utils' 2 | import { lang, locals } from '../../content/locals' 3 | import { PaletteData } from '../../types/data' 4 | 5 | const exportSwiftUI = (palette: FrameNode) => { 6 | const paletteData: PaletteData = JSON.parse(palette.getPluginData('data')), 7 | workingThemes = 8 | paletteData.themes.filter((theme) => theme.type === 'custom theme') 9 | .length === 0 10 | ? paletteData.themes.filter((theme) => theme.type === 'default theme') 11 | : paletteData.themes.filter((theme) => theme.type === 'custom theme'), 12 | swift: Array = [] 13 | 14 | if (palette.children.length === 1) { 15 | workingThemes.forEach((theme) => { 16 | theme.colors.forEach((color) => { 17 | const Colors: Array = [] 18 | Colors.unshift( 19 | `// ${ 20 | workingThemes[0].type === 'custom theme' ? theme.name + ' - ' : '' 21 | }${color.name}` 22 | ) 23 | color.shades.forEach((shade) => { 24 | Colors.unshift( 25 | `public let ${ 26 | workingThemes[0].type === 'custom theme' 27 | ? new Case(theme.name + ' ' + color.name).doCamelCase() 28 | : new Case(color.name).doCamelCase() 29 | }${ 30 | shade.name === 'source' ? 'Source' : shade.name 31 | } = Color(red: ${shade.gl[0].toFixed( 32 | 3 33 | )}, green: ${shade.gl[1].toFixed(3)}, blue: ${shade.gl[2].toFixed( 34 | 3 35 | )})` 36 | ) 37 | }) 38 | Colors.unshift('') 39 | Colors.reverse().forEach((color) => swift.push(color)) 40 | }) 41 | }) 42 | 43 | swift.pop() 44 | 45 | figma.ui.postMessage({ 46 | type: 'EXPORT_PALETTE_SWIFTUI', 47 | id: figma.currentUser?.id, 48 | context: 'APPLE_SWIFTUI', 49 | data: `import SwiftUI\n\npublic extension Color {\n static let Token = Color.TokenColor()\n struct TokenColor {\n ${swift.join( 50 | '\n ' 51 | )}\n }\n}`, 52 | }) 53 | } else figma.notify(locals[lang].error.corruption) 54 | } 55 | 56 | export default exportSwiftUI 57 | -------------------------------------------------------------------------------- /src/bridges/exports/exportTailwind.ts: -------------------------------------------------------------------------------- 1 | import { Case } from '@a_ng_d/figmug-utils' 2 | import { lang, locals } from '../../content/locals' 3 | import { PaletteData } from '../../types/data' 4 | 5 | const exportTailwind = (palette: FrameNode) => { 6 | const paletteData: PaletteData = JSON.parse(palette.getPluginData('data')), 7 | workingThemes = 8 | paletteData.themes.filter((theme) => theme.type === 'custom theme') 9 | .length === 0 10 | ? paletteData.themes.filter((theme) => theme.type === 'default theme') 11 | : paletteData.themes.filter((theme) => theme.type === 'custom theme'), 12 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 13 | json: { [key: string]: any } = { 14 | theme: { 15 | colors: {}, 16 | }, 17 | } 18 | 19 | paletteData.themes[0].colors.forEach((color) => { 20 | json['theme']['colors'][new Case(color.name).doKebabCase()] = {} 21 | }) 22 | 23 | if (palette.children.length === 1) { 24 | if (workingThemes[0].type === 'custom theme') 25 | workingThemes.forEach((theme) => { 26 | theme.colors.forEach((color) => { 27 | json['theme']['colors'][new Case(color.name).doKebabCase()][ 28 | new Case(theme.name).doKebabCase() 29 | ] = {} 30 | color.shades.reverse().forEach((shade) => { 31 | json['theme']['colors'][new Case(color.name).doKebabCase()][ 32 | new Case(theme.name).doKebabCase() 33 | ][new Case(shade.name).doKebabCase()] = shade.hex 34 | }) 35 | }) 36 | }) 37 | else 38 | workingThemes.forEach((theme) => { 39 | theme.colors.forEach((color) => { 40 | json['theme']['colors'][new Case(color.name).doKebabCase()] = {} 41 | color.shades.sort().forEach((shade) => { 42 | json['theme']['colors'][new Case(color.name).doKebabCase()][ 43 | new Case(shade.name).doKebabCase() 44 | ] = shade.hex 45 | }) 46 | }) 47 | }) 48 | 49 | figma.ui.postMessage({ 50 | type: 'EXPORT_PALETTE_TAILWIND', 51 | id: figma.currentUser?.id, 52 | context: 'TAILWIND', 53 | data: json, 54 | }) 55 | } else figma.notify(locals[lang].error.corruption) 56 | } 57 | 58 | export default exportTailwind 59 | -------------------------------------------------------------------------------- /src/bridges/exports/exportUIKit.ts: -------------------------------------------------------------------------------- 1 | import { Case } from '@a_ng_d/figmug-utils' 2 | import { lang, locals } from '../../content/locals' 3 | import { PaletteData } from '../../types/data' 4 | 5 | const exportUIKit = (palette: FrameNode) => { 6 | const paletteData: PaletteData = JSON.parse(palette.getPluginData('data')), 7 | workingThemes = 8 | paletteData.themes.filter((theme) => theme.type === 'custom theme') 9 | .length === 0 10 | ? paletteData.themes.filter((theme) => theme.type === 'default theme') 11 | : paletteData.themes.filter((theme) => theme.type === 'custom theme'), 12 | swift: Array = [] 13 | 14 | if (palette.children.length === 1) { 15 | workingThemes.forEach((theme) => { 16 | const UIColors: Array = [] 17 | theme.colors.forEach((color) => { 18 | UIColors.unshift(`// ${color.name}`) 19 | color.shades.forEach((shade) => { 20 | UIColors.unshift( 21 | `static let ${new Case(color.name).doCamelCase()}${ 22 | shade.name === 'source' ? 'Source' : shade.name 23 | } = UIColor(red: ${shade.gl[0].toFixed( 24 | 3 25 | )}, green: ${shade.gl[1].toFixed(3)}, blue: ${shade.gl[2].toFixed( 26 | 3 27 | )})` 28 | ) 29 | }) 30 | UIColors.unshift('') 31 | }) 32 | UIColors.shift() 33 | if (workingThemes[0].type === 'custom theme') 34 | swift.push( 35 | `struct ${new Case(theme.name).doPascalCase()} {\n ${UIColors.reverse().join( 36 | '\n ' 37 | )}\n }` 38 | ) 39 | else swift.push(`${UIColors.reverse().join('\n ')}`) 40 | }) 41 | 42 | figma.ui.postMessage({ 43 | type: 'EXPORT_PALETTE_UIKIT', 44 | id: figma.currentUser?.id, 45 | context: 'APPLE_UIKIT', 46 | data: `import UIKit\n\nstruct Color {\n ${swift.join('\n\n ')}\n}`, 47 | }) 48 | } else figma.notify(locals[lang].error.corruption) 49 | } 50 | 51 | export default exportUIKit 52 | -------------------------------------------------------------------------------- /src/bridges/exports/exportXml.ts: -------------------------------------------------------------------------------- 1 | import { Case } from '@a_ng_d/figmug-utils' 2 | import { lang, locals } from '../../content/locals' 3 | import { PaletteData } from '../../types/data' 4 | 5 | const exportXml = (palette: FrameNode) => { 6 | const paletteData: PaletteData = JSON.parse(palette.getPluginData('data')), 7 | workingThemes = 8 | paletteData.themes.filter((theme) => theme.type === 'custom theme') 9 | .length === 0 10 | ? paletteData.themes.filter((theme) => theme.type === 'default theme') 11 | : paletteData.themes.filter((theme) => theme.type === 'custom theme'), 12 | resources: Array = [] 13 | 14 | if (palette.children.length === 1) { 15 | workingThemes.forEach((theme) => { 16 | theme.colors.forEach((color) => { 17 | const colors: Array = [] 18 | colors.unshift( 19 | `` 22 | ) 23 | color.shades.forEach((shade) => { 24 | colors.unshift( 25 | `${shade.hex}` 30 | ) 31 | }) 32 | colors.unshift('') 33 | colors.reverse().forEach((color) => resources.push(color)) 34 | }) 35 | }) 36 | 37 | resources.pop() 38 | 39 | figma.ui.postMessage({ 40 | type: 'EXPORT_PALETTE_XML', 41 | id: figma.currentUser?.id, 42 | context: 'ANDROID_XML', 43 | data: `\n\n ${resources.join( 44 | '\n ' 45 | )}\n`, 46 | }) 47 | } else figma.notify(locals[lang].error.corruption) 48 | } 49 | 50 | export default exportXml 51 | -------------------------------------------------------------------------------- /src/bridges/getPalettesOnCurrentPage.ts: -------------------------------------------------------------------------------- 1 | import { lang, locals } from '../content/locals' 2 | 3 | const getPalettesOnCurrentPage = async () => { 4 | const palettes = (await figma.currentPage 5 | .loadAsync() 6 | .then(() => 7 | figma.currentPage.findAllWithCriteria({ 8 | pluginData: {}, 9 | }) 10 | ) 11 | .catch(() => { 12 | figma.notify(locals[lang].error.palettesPicking) 13 | return [] 14 | })) as Array 15 | 16 | if (palettes.length !== 0) { 17 | const palettesList = async () => { 18 | const palettePromises = palettes.map(async (palette) => { 19 | const name = palette.getPluginData('name') 20 | const preset = palette.getPluginData('preset') 21 | const colors = palette.getPluginData('colors') 22 | const themes = palette.getPluginData('themes') 23 | 24 | if (preset === '' || colors === '' || themes === '') return null 25 | 26 | const bytes = await palette.exportAsync({ 27 | format: 'PNG', 28 | constraint: { type: 'SCALE', value: 0.25 }, 29 | }) 30 | return { 31 | id: palette.id, 32 | name: name, 33 | preset: JSON.parse(preset).name, 34 | colors: JSON.parse(colors), 35 | themes: JSON.parse(themes), 36 | screenshot: bytes, 37 | devStatus: palette.devStatus !== null && palette.devStatus.type, 38 | } 39 | }) 40 | const filteredPalettes = (await Promise.all(palettePromises)).filter( 41 | (palette) => palette !== null 42 | ) 43 | return filteredPalettes 44 | } 45 | 46 | figma.ui.postMessage({ 47 | type: 'EXPOSE_PALETTES', 48 | data: await palettesList().then((list) => { 49 | return list.sort((a, b) => { 50 | if ( 51 | a.devStatus === 'READY_FOR_DEV' && 52 | b.devStatus !== 'READY_FOR_DEV' 53 | ) 54 | return -1 55 | else if ( 56 | a.devStatus !== 'READY_FOR_DEV' && 57 | b.devStatus === 'READY_FOR_DEV' 58 | ) 59 | return 1 60 | else return 0 61 | }) 62 | }), 63 | }) 64 | } else 65 | figma.ui.postMessage({ 66 | type: 'EXPOSE_PALETTES', 67 | data: [], 68 | }) 69 | } 70 | 71 | export default getPalettesOnCurrentPage 72 | -------------------------------------------------------------------------------- /src/bridges/getProPlan.ts: -------------------------------------------------------------------------------- 1 | const getProPlan = async () => { 2 | await figma.payments 3 | ?.initiateCheckoutAsync({ 4 | interstitial: 'SKIP', 5 | }) 6 | .then(() => { 7 | if (figma.payments?.status.type === 'PAID') 8 | figma.ui.postMessage({ 9 | type: 'GET_PRO_PLAN', 10 | data: figma.payments.status.type, 11 | id: figma.currentUser?.id, 12 | }) 13 | }) 14 | } 15 | 16 | export default getProPlan 17 | -------------------------------------------------------------------------------- /src/bridges/loadParameters.ts: -------------------------------------------------------------------------------- 1 | import { FeatureStatus } from '@a_ng_d/figmug-utils' 2 | import features from '../config' 3 | import { lang, locals } from '../content/locals' 4 | import { presets } from '../stores/presets' 5 | import checkPlanStatus from './checks/checkPlanStatus' 6 | 7 | const loadParameters = async ({ key, result }: ParameterInputEvent) => { 8 | switch (key) { 9 | case 'preset': { 10 | const planStatus = (await checkPlanStatus('PARAMETERS')) ?? 'UNPAID' 11 | 12 | const filteredPresets = await Promise.all( 13 | presets.map(async (preset) => { 14 | const isBlocked = new FeatureStatus({ 15 | features: features, 16 | featureName: `PRESETS_${preset.id}`, 17 | planStatus: planStatus, 18 | }).isBlocked() 19 | return { preset, isBlocked } 20 | }) 21 | ) 22 | 23 | const suggestionsList = filteredPresets 24 | .filter(({ isBlocked }) => !isBlocked) 25 | .map(({ preset }) => preset.name) as Array 26 | 27 | result.setSuggestions(suggestionsList) 28 | break 29 | } 30 | 31 | case 'space': { 32 | const planStatus = (await checkPlanStatus('PARAMETERS')) ?? 'UNPAID' 33 | const suggestionsList = [ 34 | new FeatureStatus({ 35 | features: features, 36 | featureName: 'SETTINGS_COLOR_SPACE_LCH', 37 | planStatus: planStatus, 38 | suggestion: locals[lang].settings.color.colorSpace.lch, 39 | }).isAvailableAndBlocked(), 40 | new FeatureStatus({ 41 | features: features, 42 | featureName: 'SETTINGS_COLOR_SPACE_OKLCH', 43 | planStatus: planStatus, 44 | suggestion: locals[lang].settings.color.colorSpace.oklch, 45 | }).isAvailableAndBlocked(), 46 | new FeatureStatus({ 47 | features: features, 48 | featureName: 'SETTINGS_COLOR_SPACE_LAB', 49 | planStatus: planStatus, 50 | suggestion: locals[lang].settings.color.colorSpace.lab, 51 | }).isAvailableAndBlocked(), 52 | new FeatureStatus({ 53 | features: features, 54 | featureName: 'SETTINGS_COLOR_SPACE_OKLAB', 55 | planStatus: planStatus, 56 | suggestion: locals[lang].settings.color.colorSpace.oklab, 57 | }).isAvailableAndBlocked(), 58 | new FeatureStatus({ 59 | features: features, 60 | featureName: 'SETTINGS_COLOR_SPACE_HSL', 61 | planStatus: planStatus, 62 | suggestion: locals[lang].settings.color.colorSpace.hsl, 63 | }).isAvailableAndBlocked(), 64 | new FeatureStatus({ 65 | features: features, 66 | featureName: 'SETTINGS_COLOR_SPACE_HSLUV', 67 | planStatus: planStatus, 68 | suggestion: locals[lang].settings.color.colorSpace.hsluv, 69 | }).isAvailableAndBlocked(), 70 | ].filter((n) => n) as Array 71 | 72 | result.setSuggestions(suggestionsList) 73 | break 74 | } 75 | 76 | case 'view': { 77 | const planStatus = (await checkPlanStatus('PARAMETERS')) ?? 'UNPAID' 78 | const suggestionsList = [ 79 | new FeatureStatus({ 80 | features: features, 81 | featureName: 'VIEWS_PALETTE_WITH_PROPERTIES', 82 | planStatus: planStatus, 83 | suggestion: locals[lang].settings.global.views.detailed, 84 | }).isAvailableAndBlocked(), 85 | new FeatureStatus({ 86 | features: features, 87 | featureName: 'VIEWS_PALETTE', 88 | planStatus: planStatus, 89 | suggestion: locals[lang].settings.global.views.simple, 90 | }).isAvailableAndBlocked(), 91 | new FeatureStatus({ 92 | features: features, 93 | featureName: 'VIEWS_SHEET', 94 | planStatus: planStatus, 95 | suggestion: locals[lang].settings.global.views.sheet, 96 | }).isAvailableAndBlocked(), 97 | ].filter((n) => n) as Array 98 | 99 | result.setSuggestions(suggestionsList) 100 | break 101 | } 102 | 103 | default: 104 | return 105 | } 106 | } 107 | 108 | export default loadParameters 109 | -------------------------------------------------------------------------------- /src/bridges/processSelection.ts: -------------------------------------------------------------------------------- 1 | import { uid } from 'uid' 2 | 3 | import { lang, locals } from '../content/locals' 4 | import { 5 | SourceColorConfiguration, 6 | ThemeConfiguration, 7 | } from '../types/configurations' 8 | import { ActionsList } from '../types/models' 9 | import setPaletteMigration from '../utils/setPaletteMigration' 10 | 11 | export let currentSelection: ReadonlyArray 12 | export let previousSelection: ReadonlyArray | undefined 13 | export let isSelectionChanged = false 14 | 15 | const processSelection = () => { 16 | previousSelection = 17 | currentSelection === undefined ? undefined : currentSelection 18 | isSelectionChanged = true 19 | 20 | const selection: ReadonlyArray = figma.currentPage.selection 21 | currentSelection = figma.currentPage.selection 22 | 23 | const viableSelection: Array = [] 24 | 25 | const palette: FrameNode | InstanceNode = selection[0] as 26 | | FrameNode 27 | | InstanceNode 28 | const selectionHandler = ( 29 | state: string, 30 | element: FrameNode | null = null 31 | ) => { 32 | const actions: ActionsList = { 33 | PALETTE_SELECTED: async () => { 34 | figma.ui.postMessage({ 35 | type: 'PALETTE_SELECTED', 36 | data: { 37 | editorType: figma.editorType, 38 | id: palette.getPluginData('id'), 39 | name: palette.getPluginData('name'), 40 | description: palette.getPluginData('description'), 41 | preset: JSON.parse(palette.getPluginData('preset')), 42 | scale: JSON.parse(palette.getPluginData('themes')).find( 43 | (theme: ThemeConfiguration) => theme.isEnabled 44 | ).scale, 45 | shift: JSON.parse(palette.getPluginData('shift')), 46 | areSourceColorsLocked: 47 | palette.getPluginData('areSourceColorsLocked') === 'true', 48 | colors: JSON.parse(palette.getPluginData('colors')), 49 | colorSpace: palette.getPluginData('colorSpace'), 50 | visionSimulationMode: palette.getPluginData('visionSimulationMode'), 51 | themes: JSON.parse(palette.getPluginData('themes')), 52 | view: palette.getPluginData('view'), 53 | algorithmVersion: palette.getPluginData('algorithmVersion'), 54 | textColorsTheme: JSON.parse( 55 | palette.getPluginData('textColorsTheme') 56 | ), 57 | isPublished: palette.getPluginData('isPublished') === 'true', 58 | isShared: palette.getPluginData('isShared') === 'true', 59 | creatorFullName: palette.getPluginData('creatorFullName'), 60 | creatorAvatar: palette.getPluginData('creatorAvatar'), 61 | creatorId: palette.getPluginData('creatorId'), 62 | createdAt: palette.getPluginData('createdAt'), 63 | updatedAt: palette.getPluginData('updatedAt'), 64 | publishedAt: palette.getPluginData('publishedAt'), 65 | }, 66 | }) 67 | 68 | await palette 69 | .exportAsync({ 70 | format: 'PNG', 71 | constraint: { type: 'SCALE', value: 0.25 }, 72 | }) 73 | .then((image) => 74 | figma.ui.postMessage({ 75 | type: 'UPDATE_SCREENSHOT', 76 | data: image, 77 | }) 78 | ) 79 | .catch(() => 80 | figma.ui.postMessage({ 81 | type: 'UPDATE_SCREENSHOT', 82 | data: null, 83 | }) 84 | ) 85 | 86 | palette.setRelaunchData({ 87 | edit: locals[lang].relaunch.edit.description, 88 | }) 89 | }, 90 | EMPTY_SELECTION: () => 91 | figma.ui.postMessage({ 92 | type: 'EMPTY_SELECTION', 93 | data: {}, 94 | }), 95 | COLOR_SELECTED: () => { 96 | figma.ui.postMessage({ 97 | type: 'COLOR_SELECTED', 98 | data: { 99 | selection: viableSelection, 100 | }, 101 | }) 102 | element?.setRelaunchData({ 103 | create: locals[lang].relaunch.create.description, 104 | }) 105 | }, 106 | } 107 | 108 | return actions[state]?.() 109 | } 110 | 111 | if ( 112 | selection.length === 1 && 113 | palette.getPluginData('type') === 'UI_COLOR_PALETTE' && 114 | palette.type !== 'INSTANCE' 115 | ) { 116 | setPaletteMigration(palette) // Migration 117 | selectionHandler('PALETTE_SELECTED') 118 | } else if ( 119 | selection.length === 1 && 120 | palette.getPluginDataKeys().length > 0 && 121 | palette.type !== 'INSTANCE' 122 | ) { 123 | setPaletteMigration(palette) // Migration 124 | selectionHandler('PALETTE_SELECTED') 125 | } else if (selection.length === 0) selectionHandler('EMPTY_SELECTION') 126 | else if (selection.length > 1 && palette.getPluginDataKeys().length !== 0) 127 | selectionHandler('EMPTY_SELECTION') 128 | else if (selection[0].type === 'INSTANCE') selectionHandler('EMPTY_SELECTION') 129 | else if ((selection[0] as FrameNode).fills === undefined) 130 | selectionHandler('EMPTY_SELECTION') 131 | else if ( 132 | (selection[0] as FrameNode).fills && 133 | ((selection[0] as FrameNode).fills as readonly Paint[]).length === 0 134 | ) 135 | selectionHandler('EMPTY_SELECTION') 136 | 137 | selection.forEach((element) => { 138 | if ( 139 | element.type !== 'CONNECTOR' && 140 | element.type !== 'GROUP' && 141 | element.type !== 'EMBED' && 142 | element.type !== 'SLICE' 143 | ) 144 | if ( 145 | ((element as FrameNode).fills as readonly Paint[]).filter( 146 | (fill: Paint) => fill.type === 'SOLID' 147 | ).length !== 0 && 148 | element.getPluginDataKeys().length === 0 149 | ) { 150 | const solidFill = ((element as FrameNode).fills as Array).find( 151 | (fill: Paint) => fill.type === 'SOLID' 152 | ) as SolidPaint 153 | 154 | viableSelection.push({ 155 | name: (element as FrameNode).name, 156 | rgb: solidFill.color, 157 | source: 'CANVAS', 158 | id: uid(), 159 | isRemovable: false, 160 | }) 161 | selectionHandler('COLOR_SELECTED', element as FrameNode) 162 | } 163 | }) 164 | 165 | setTimeout(() => (isSelectionChanged = false), 1000) 166 | } 167 | 168 | export default processSelection 169 | -------------------------------------------------------------------------------- /src/bridges/publication/authentication.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js' 2 | 3 | import { authUrl, authWorkerUrl, databaseUrl } from '../../config' 4 | import { lang, locals } from '../../content/locals' 5 | import checkConnectionStatus from '../checks/checkConnectionStatus' 6 | 7 | let isAuthenticated = false 8 | 9 | export const supabase = createClient( 10 | databaseUrl, 11 | process.env.REACT_APP_SUPABASE_PUBLIC_ANON_KEY ?? '' 12 | ) 13 | 14 | export const signIn = async (disinctId: string) => { 15 | return new Promise((resolve, reject) => { 16 | fetch(authWorkerUrl, { 17 | method: 'GET', 18 | cache: 'no-cache', 19 | credentials: 'omit', 20 | headers: { 21 | type: 'GET_PASSKEY', 22 | 'distinct-id': disinctId, 23 | }, 24 | }) 25 | .then((response) => { 26 | if (response.ok) return response.json() 27 | else reject(new Error(locals[lang].error.badResponse)) 28 | }) 29 | .then((result) => { 30 | parent.postMessage( 31 | { 32 | pluginMessage: { 33 | type: 'OPEN_IN_BROWSER', 34 | url: `${authUrl}/?passkey=${result.passkey}`, 35 | }, 36 | pluginId: '1063959496693642315', 37 | }, 38 | 'https://www.figma.com' 39 | ) 40 | const poll = setInterval(async () => { 41 | fetch(authWorkerUrl, { 42 | method: 'GET', 43 | cache: 'no-cache', 44 | credentials: 'omit', 45 | headers: { 46 | type: 'GET_TOKENS', 47 | 'distinct-id': disinctId, 48 | passkey: result.passkey, 49 | }, 50 | }) 51 | .then((response) => { 52 | if (response.body) return response.json() 53 | else reject(new Error()) 54 | }) 55 | .then(async (result) => { 56 | //console.log(result) 57 | if (result.message !== 'No token found') { 58 | isAuthenticated = true 59 | parent.postMessage( 60 | { 61 | pluginMessage: { 62 | type: 'SET_ITEMS', 63 | items: [ 64 | { 65 | key: 'supabase_access_token', 66 | value: result.tokens.access_token, 67 | }, 68 | { 69 | key: 'supabase_refresh_token', 70 | value: result.tokens.refresh_token, 71 | }, 72 | ], 73 | }, 74 | pluginId: '1063959496693642315', 75 | }, 76 | 'https://www.figma.com' 77 | ) 78 | checkConnectionStatus( 79 | result.tokens.access_token, 80 | result.tokens.refresh_token 81 | ) 82 | .then(() => { 83 | clearInterval(poll) 84 | resolve(result) 85 | }) 86 | .catch((error) => { 87 | clearInterval(poll) 88 | reject(error) 89 | }) 90 | } 91 | }) 92 | .catch((error) => { 93 | clearInterval(poll) 94 | reject(error) 95 | }) 96 | }, 5000) 97 | setTimeout( 98 | () => { 99 | if (!isAuthenticated) { 100 | clearInterval(poll) 101 | reject(new Error('Authentication timeout')) 102 | } 103 | }, 104 | 2 * 60 * 1000 105 | ) 106 | }) 107 | .catch((error) => { 108 | reject(error) 109 | }) 110 | }) 111 | } 112 | 113 | export const signOut = async () => { 114 | parent.postMessage( 115 | { 116 | pluginMessage: { 117 | type: 'OPEN_IN_BROWSER', 118 | url: `${authUrl}/?action=sign_out`, 119 | }, 120 | pluginId: '1063959496693642315', 121 | }, 122 | 'https://www.figma.com' 123 | ) 124 | parent.postMessage( 125 | { 126 | pluginMessage: { 127 | type: 'DELETE_ITEMS', 128 | items: ['supabase_access_token'], 129 | }, 130 | }, 131 | '*' 132 | ) 133 | parent.postMessage( 134 | { 135 | pluginMessage: { 136 | type: 'SIGN_OUT', 137 | }, 138 | }, 139 | '*' 140 | ) 141 | 142 | setTimeout(async () => { 143 | await supabase.auth.signOut({ 144 | scope: 'local', 145 | }) 146 | }, 2000) 147 | } 148 | -------------------------------------------------------------------------------- /src/bridges/publication/detachPalette.ts: -------------------------------------------------------------------------------- 1 | import { lang, locals } from '../../content/locals' 2 | import type { AppStates } from '../../ui/App' 3 | 4 | const detachPalette = async ( 5 | rawData: AppStates 6 | ): Promise> => { 7 | const palettePublicationDetails = { 8 | id: '', 9 | dates: { 10 | publishedAt: '', 11 | createdAt: rawData.dates.createdAt, 12 | updatedAt: rawData.dates.updatedAt, 13 | }, 14 | publicationStatus: { 15 | isPublished: false, 16 | isShared: false, 17 | }, 18 | creatorIdentity: { 19 | creatorFullName: '', 20 | creatorAvatar: '', 21 | creatorId: '', 22 | }, 23 | } 24 | 25 | parent.postMessage( 26 | { 27 | pluginMessage: { 28 | type: 'SET_DATA', 29 | items: [ 30 | { 31 | key: 'id', 32 | value: palettePublicationDetails.id, 33 | }, 34 | ], 35 | }, 36 | }, 37 | '*' 38 | ) 39 | parent.postMessage( 40 | { 41 | pluginMessage: { 42 | type: 'SEND_MESSAGE', 43 | message: locals[lang].success.detachment, 44 | }, 45 | }, 46 | '*' 47 | ) 48 | parent.postMessage( 49 | { 50 | pluginMessage: { 51 | type: 'UPDATE_GLOBAL', 52 | data: { 53 | ...rawData, 54 | ...palettePublicationDetails, 55 | }, 56 | }, 57 | }, 58 | '*' 59 | ) 60 | 61 | return palettePublicationDetails 62 | } 63 | 64 | export default detachPalette 65 | -------------------------------------------------------------------------------- /src/bridges/publication/publishPalette.ts: -------------------------------------------------------------------------------- 1 | import { uid } from 'uid' 2 | 3 | import { 4 | databaseUrl, 5 | palettesDbTableName, 6 | palettesStorageName, 7 | } from '../../config' 8 | import type { AppStates } from '../../ui/App' 9 | import { supabase } from './authentication' 10 | 11 | const publishPalette = async ({ 12 | rawData, 13 | isShared = false, 14 | }: { 15 | rawData: AppStates 16 | isShared?: boolean 17 | }): Promise> => { 18 | let imageUrl = null 19 | const now = new Date().toISOString(), 20 | name = 21 | rawData.name === '' || rawData.name === 'Untitled' 22 | ? `${rawData.userSession.userFullName}'s UI COLOR PALETTE` 23 | : rawData.name, 24 | id = uid() 25 | 26 | if (rawData.screenshot !== null) { 27 | const { error } = await supabase.storage 28 | .from(palettesStorageName) 29 | .upload( 30 | `${rawData.userSession.userId}/${id}.png`, 31 | rawData.screenshot.buffer, 32 | { 33 | contentType: 'image/png', 34 | upsert: true, 35 | } 36 | ) 37 | 38 | if (!error) 39 | imageUrl = `${databaseUrl}/storage/v1/object/public/${palettesStorageName}/${rawData.userSession.userId}/${id}.png` 40 | else throw error 41 | } 42 | 43 | const { error } = await supabase 44 | .from(palettesDbTableName) 45 | .insert([ 46 | { 47 | palette_id: id, 48 | name: name, 49 | description: rawData.description, 50 | preset: rawData.preset, 51 | scale: rawData.scale, 52 | shift: rawData.shift, 53 | are_source_colors_locked: rawData.areSourceColorsLocked, 54 | colors: rawData.colors, 55 | color_space: rawData.colorSpace, 56 | vision_simulation_mode: rawData.visionSimulationMode, 57 | themes: rawData.themes, 58 | view: rawData.view, 59 | text_colors_theme: rawData.textColorsTheme, 60 | algorithm_version: rawData.algorithmVersion, 61 | is_shared: isShared, 62 | screenshot: imageUrl, 63 | creator_full_name: rawData.userSession.userFullName, 64 | creator_avatar: rawData.userSession.userAvatar, 65 | creator_id: rawData.userSession.userId, 66 | created_at: rawData.dates.createdAt, 67 | updated_at: now, 68 | published_at: now, 69 | }, 70 | ]) 71 | .select() 72 | 73 | if (!error) { 74 | const palettePublicationDetails = { 75 | id: id, 76 | name: name, 77 | dates: { 78 | publishedAt: now, 79 | createdAt: rawData.dates.createdAt, 80 | updatedAt: now, 81 | }, 82 | publicationStatus: { 83 | isPublished: true, 84 | isShared: isShared, 85 | }, 86 | creatorIdentity: { 87 | creatorFullName: rawData.userSession.userFullName, 88 | creatorAvatar: rawData.userSession.userAvatar, 89 | creatorId: rawData.userSession.userId ?? '', 90 | }, 91 | } 92 | 93 | parent.postMessage( 94 | { 95 | pluginMessage: { 96 | type: 'SET_DATA', 97 | items: [ 98 | { 99 | key: 'id', 100 | value: palettePublicationDetails.id, 101 | }, 102 | ], 103 | }, 104 | }, 105 | '*' 106 | ) 107 | 108 | parent.postMessage( 109 | { 110 | pluginMessage: { 111 | type: 'UPDATE_GLOBAL', 112 | data: { 113 | ...rawData, 114 | ...palettePublicationDetails, 115 | }, 116 | }, 117 | }, 118 | '*' 119 | ) 120 | 121 | return palettePublicationDetails 122 | } else throw error 123 | } 124 | 125 | export default publishPalette 126 | -------------------------------------------------------------------------------- /src/bridges/publication/pullPalette.ts: -------------------------------------------------------------------------------- 1 | import { palettesDbTableName } from '../../config' 2 | import type { AppStates } from '../../ui/App' 3 | import { supabase } from './authentication' 4 | 5 | const pullPalette = async (rawData: AppStates): Promise> => { 6 | const { data, error } = await supabase 7 | .from(palettesDbTableName) 8 | .select('*') 9 | .eq('palette_id', rawData.id) 10 | 11 | if (!error && data.length === 1) { 12 | const palettePublicationDetails: Partial = { 13 | name: data[0].name, 14 | description: data[0].description, 15 | preset: data[0].preset, 16 | scale: data[0].scale, 17 | shift: data[0].shift, 18 | areSourceColorsLocked: data[0].are_source_colors_locked, 19 | colors: data[0].colors, 20 | colorSpace: data[0].color_space, 21 | visionSimulationMode: data[0].vision_simulation_mode, 22 | themes: data[0].themes, 23 | view: data[0].view, 24 | textColorsTheme: data[0].text_colors_theme, 25 | algorithmVersion: data[0].algorithm_version, 26 | dates: { 27 | publishedAt: data[0].published_at, 28 | createdAt: data[0].created_at, 29 | updatedAt: data[0].updated_at, 30 | }, 31 | publicationStatus: { 32 | isPublished: true, 33 | isShared: data[0].is_shared, 34 | }, 35 | creatorIdentity: { 36 | creatorFullName: data[0].creator_full_name, 37 | creatorAvatar: data[0].creator_avatar, 38 | creatorId: data[0].creator_id, 39 | }, 40 | } 41 | 42 | parent.postMessage( 43 | { 44 | pluginMessage: { 45 | type: 'UPDATE_GLOBAL', 46 | data: palettePublicationDetails, 47 | }, 48 | }, 49 | '*' 50 | ) 51 | 52 | return palettePublicationDetails 53 | } else throw error 54 | } 55 | 56 | export default pullPalette 57 | -------------------------------------------------------------------------------- /src/bridges/publication/pushPalette.ts: -------------------------------------------------------------------------------- 1 | import { palettesDbTableName, palettesStorageName } from '../../config' 2 | import type { AppStates } from '../../ui/App' 3 | import { supabase } from './authentication' 4 | 5 | const pushPalette = async ({ 6 | rawData, 7 | isShared = false, 8 | }: { 9 | rawData: AppStates 10 | isShared?: boolean 11 | }): Promise> => { 12 | const now = new Date().toISOString() 13 | const name = 14 | rawData.name === '' || rawData.name === 'Untitled' 15 | ? `${rawData.userSession.userFullName}'s UI COLOR PALETTE` 16 | : rawData.name 17 | 18 | if (rawData.screenshot !== null) { 19 | const { error } = await supabase.storage 20 | .from(palettesStorageName) 21 | .update( 22 | `${rawData.userSession.userId}/${rawData.id}.png`, 23 | rawData.screenshot.buffer, 24 | { 25 | contentType: 'image/png', 26 | } 27 | ) 28 | 29 | if (error) throw error 30 | } 31 | 32 | const { error } = await supabase 33 | .from(palettesDbTableName) 34 | .update([ 35 | { 36 | name: name, 37 | description: rawData.description, 38 | preset: rawData.preset, 39 | scale: rawData.scale, 40 | shift: rawData.shift, 41 | are_source_colors_locked: rawData.areSourceColorsLocked, 42 | colors: rawData.colors, 43 | color_space: rawData.colorSpace, 44 | vision_simulation_mode: rawData.visionSimulationMode, 45 | themes: rawData.themes, 46 | view: rawData.view, 47 | text_colors_theme: rawData.textColorsTheme, 48 | algorithm_version: rawData.algorithmVersion, 49 | is_shared: isShared, 50 | creator_full_name: rawData.userSession.userFullName, 51 | creator_avatar: rawData.userSession.userAvatar, 52 | creator_id: rawData.userSession.userId, 53 | updated_at: rawData.dates.updatedAt, 54 | published_at: now, 55 | }, 56 | ]) 57 | .match({ palette_id: rawData.id }) 58 | 59 | if (!error) { 60 | const palettePublicationDetails = { 61 | name: name, 62 | dates: { 63 | publishedAt: now, 64 | createdAt: rawData.dates.createdAt, 65 | updatedAt: now, 66 | }, 67 | publicationStatus: { 68 | isPublished: true, 69 | isShared: isShared, 70 | }, 71 | creatorIdentity: { 72 | creatorFullName: rawData.userSession.userFullName, 73 | creatorAvatar: rawData.userSession.userAvatar, 74 | creatorId: rawData.userSession.userId ?? '', 75 | }, 76 | } 77 | 78 | parent.postMessage( 79 | { 80 | pluginMessage: { 81 | type: 'UPDATE_GLOBAL', 82 | data: { 83 | ...rawData, 84 | ...palettePublicationDetails, 85 | }, 86 | }, 87 | }, 88 | '*' 89 | ) 90 | 91 | return palettePublicationDetails 92 | } else throw error 93 | } 94 | 95 | export default pushPalette 96 | -------------------------------------------------------------------------------- /src/bridges/publication/sharePalette.ts: -------------------------------------------------------------------------------- 1 | import { palettesDbTableName } from '../../config' 2 | import { supabase } from './authentication' 3 | 4 | const sharePalette = async ({ 5 | id, 6 | isShared, 7 | }: { 8 | id: string 9 | isShared: boolean 10 | }): Promise => { 11 | const now = new Date().toISOString() 12 | 13 | const { error } = await supabase 14 | .from(palettesDbTableName) 15 | .update([ 16 | { 17 | is_shared: isShared, 18 | published_at: now, 19 | updated_at: now, 20 | }, 21 | ]) 22 | .match({ palette_id: id }) 23 | 24 | if (!error) return 25 | else throw error 26 | } 27 | 28 | export default sharePalette 29 | -------------------------------------------------------------------------------- /src/bridges/publication/unpublishPalette.ts: -------------------------------------------------------------------------------- 1 | import { palettesDbTableName, palettesStorageName } from '../../config' 2 | import type { AppStates } from '../../ui/App' 3 | import { supabase } from './authentication' 4 | 5 | const unpublishPalette = async ({ 6 | rawData, 7 | isRemote = false, 8 | }: { 9 | rawData: Partial 10 | isRemote?: boolean 11 | }): Promise> => { 12 | if (rawData.screenshot !== null || !isRemote) { 13 | const { error } = await supabase.storage 14 | .from(palettesStorageName) 15 | .remove([`${rawData.userSession?.userId}/${rawData.id}.png`]) 16 | 17 | if (error) throw error 18 | } 19 | 20 | const { error } = await supabase 21 | .from(palettesDbTableName) 22 | .delete() 23 | .match({ palette_id: rawData.id }) 24 | 25 | if (!error) { 26 | const palettePublicationDetails = { 27 | id: '', 28 | dates: { 29 | publishedAt: '', 30 | createdAt: rawData.dates?.createdAt ?? '', 31 | updatedAt: rawData.dates?.updatedAt ?? '', 32 | }, 33 | publicationStatus: { 34 | isPublished: false, 35 | isShared: false, 36 | }, 37 | creatorIdentity: { 38 | creatorFullName: '', 39 | creatorAvatar: '', 40 | creatorId: '', 41 | }, 42 | } 43 | 44 | if (!isRemote) { 45 | parent.postMessage( 46 | { 47 | pluginMessage: { 48 | type: 'SET_DATA', 49 | items: [ 50 | { 51 | key: 'id', 52 | value: palettePublicationDetails.id, 53 | }, 54 | ], 55 | }, 56 | }, 57 | '*' 58 | ) 59 | parent.postMessage( 60 | { 61 | pluginMessage: { 62 | type: 'UPDATE_GLOBAL', 63 | data: { 64 | ...rawData, 65 | ...palettePublicationDetails, 66 | }, 67 | }, 68 | }, 69 | '*' 70 | ) 71 | } 72 | 73 | return palettePublicationDetails 74 | } else throw error 75 | } 76 | 77 | export default unpublishPalette 78 | -------------------------------------------------------------------------------- /src/bridges/updates/updateColors.ts: -------------------------------------------------------------------------------- 1 | import { PaletteNode } from 'src/types/nodes' 2 | import Colors from '../../canvas/Colors' 3 | import { lang, locals } from '../../content/locals' 4 | import { ColorsMessage } from '../../types/messages' 5 | import setPaletteName from '../../utils/setPaletteName' 6 | import { 7 | currentSelection, 8 | isSelectionChanged, 9 | previousSelection, 10 | } from '../processSelection' 11 | 12 | const updateColors = async (msg: ColorsMessage) => { 13 | const palette = isSelectionChanged 14 | ? (previousSelection?.[0] as FrameNode) 15 | : (currentSelection[0] as FrameNode) 16 | 17 | if (palette.children.length === 1) { 18 | const keys = palette.getPluginDataKeys() 19 | const paletteData: [string, string | boolean | object][] = keys.map( 20 | (key) => { 21 | const value = palette.getPluginData(key) 22 | if (value === 'true' || value === 'false') 23 | return [key, value === 'true'] 24 | else if (value.includes('{')) 25 | return [key, JSON.parse(palette.getPluginData(key))] 26 | return [key, value] 27 | } 28 | ) 29 | const paletteObject = makePaletteNode(paletteData) 30 | const creatorAvatarImg = 31 | paletteObject.creatorAvatar !== '' 32 | ? await figma 33 | .createImageAsync(paletteObject.creatorAvatar ?? '') 34 | .then(async (image: Image) => image) 35 | .catch(() => null) 36 | : null 37 | 38 | palette.setPluginData('colors', JSON.stringify(msg.data)) 39 | 40 | if (JSON.stringify(msg.data) !== JSON.stringify(paletteObject.colors)) { 41 | palette.children[0].remove() 42 | palette.appendChild( 43 | new Colors( 44 | { 45 | ...paletteObject, 46 | colors: msg.data, 47 | name: paletteObject.name !== undefined ? paletteObject.name : '', 48 | description: 49 | paletteObject.description !== undefined 50 | ? paletteObject.description 51 | : '', 52 | view: 53 | msg.isEditedInRealTime && 54 | paletteObject.view === 'PALETTE_WITH_PROPERTIES' 55 | ? 'PALETTE' 56 | : msg.isEditedInRealTime && paletteObject.view === 'SHEET' 57 | ? 'SHEET_SAFE_MODE' 58 | : paletteObject.view, 59 | creatorAvatarImg: creatorAvatarImg, 60 | service: 'EDIT', 61 | }, 62 | palette 63 | ).makeNode() 64 | ) 65 | 66 | // Update 67 | const now = new Date().toISOString() 68 | palette.setPluginData('updatedAt', now) 69 | figma.ui.postMessage({ 70 | type: 'UPDATE_PALETTE_DATE', 71 | data: now, 72 | }) 73 | } 74 | 75 | // Palette migration 76 | palette.counterAxisSizingMode = 'AUTO' 77 | palette.name = setPaletteName( 78 | paletteObject.name !== undefined ? paletteObject.name : locals[lang].name, 79 | paletteObject.themes.find((theme) => theme.isEnabled)?.name, 80 | paletteObject.preset.name, 81 | paletteObject.colorSpace, 82 | paletteObject.visionSimulationMode 83 | ) 84 | } else figma.notify(locals[lang].error.corruption) 85 | } 86 | 87 | const makePaletteNode = (data: [string, string | boolean | object][]) => { 88 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 89 | const obj: { [key: string]: any } = {} 90 | 91 | data.forEach((d) => { 92 | obj[d[0]] = d[1] 93 | }) 94 | 95 | return obj as PaletteNode 96 | } 97 | 98 | export default updateColors 99 | -------------------------------------------------------------------------------- /src/bridges/updates/updateGlobal.ts: -------------------------------------------------------------------------------- 1 | import Colors from '../../canvas/Colors' 2 | import { lang, locals } from '../../content/locals' 3 | import { 4 | ColorConfiguration, 5 | CreatorConfiguration, 6 | DatesConfiguration, 7 | PaletteConfiguration, 8 | PublicationConfiguration, 9 | ThemeConfiguration, 10 | } from '../../types/configurations' 11 | import setPaletteName from '../../utils/setPaletteName' 12 | import { 13 | currentSelection, 14 | isSelectionChanged, 15 | previousSelection, 16 | } from '../processSelection' 17 | 18 | interface Msg { 19 | data: PaletteConfiguration & { 20 | creatorIdentity: CreatorConfiguration 21 | colors: Array 22 | themes: Array 23 | dates: DatesConfiguration 24 | publicationStatus: PublicationConfiguration 25 | } 26 | } 27 | 28 | const updateGlobal = async (msg: Msg) => { 29 | const palette = isSelectionChanged 30 | ? (previousSelection?.[0] as FrameNode) 31 | : (currentSelection[0] as FrameNode) 32 | 33 | const creatorAvatarImg = await figma 34 | .createImageAsync(msg.data.creatorIdentity.creatorAvatar) 35 | .then(async (image: Image) => image) 36 | .catch(() => null) 37 | 38 | if (palette.children.length === 1) { 39 | palette.children[0].remove() 40 | palette.appendChild( 41 | new Colors( 42 | { 43 | name: msg.data.name, 44 | description: msg.data.description, 45 | preset: msg.data.preset, 46 | scale: msg.data.scale, 47 | areSourceColorsLocked: msg.data.areSourceColorsLocked, 48 | colors: msg.data.colors, 49 | colorSpace: msg.data.colorSpace, 50 | visionSimulationMode: msg.data.visionSimulationMode, 51 | themes: msg.data.themes, 52 | view: msg.data.view, 53 | textColorsTheme: msg.data.textColorsTheme, 54 | algorithmVersion: msg.data.algorithmVersion, 55 | creatorFullName: msg.data.creatorIdentity.creatorFullName, 56 | creatorAvatarImg: creatorAvatarImg, 57 | service: 'EDIT', 58 | }, 59 | palette 60 | ).makeNode() 61 | ) 62 | 63 | palette.name = setPaletteName( 64 | msg.data.name, 65 | msg.data.themes.find((theme) => theme.isEnabled)?.name, 66 | msg.data.preset.name, 67 | msg.data.colorSpace, 68 | msg.data.visionSimulationMode 69 | ) 70 | 71 | palette.setPluginData('name', msg.data.name) 72 | palette.setPluginData('description', msg.data.description) 73 | palette.setPluginData('preset', JSON.stringify(msg.data.preset)) 74 | palette.setPluginData('scale', JSON.stringify(msg.data.scale)) 75 | palette.setPluginData( 76 | 'areSourceColorsLocked', 77 | msg.data.areSourceColorsLocked.toString() 78 | ) 79 | palette.setPluginData('colors', JSON.stringify(msg.data.colors)) 80 | palette.setPluginData('colorSpace', msg.data.colorSpace) 81 | palette.setPluginData('visionSimulationMode', msg.data.visionSimulationMode) 82 | palette.setPluginData('themes', JSON.stringify(msg.data.themes)) 83 | palette.setPluginData('view', msg.data.view) 84 | palette.setPluginData( 85 | 'textColorsTheme', 86 | JSON.stringify(msg.data.textColorsTheme) 87 | ) 88 | palette.setPluginData('algorithmVersion', msg.data.algorithmVersion) 89 | palette.setPluginData('createdAt', msg.data.dates.createdAt.toString()) 90 | palette.setPluginData('updatedAt', msg.data.dates.updatedAt.toString()) 91 | palette.setPluginData('publishedAt', msg.data.dates.publishedAt.toString()) 92 | palette.setPluginData( 93 | 'isPublished', 94 | msg.data.publicationStatus.isPublished.toString() 95 | ) 96 | palette.setPluginData( 97 | 'isShared', 98 | msg.data.publicationStatus.isShared.toString() 99 | ) 100 | palette.setPluginData( 101 | 'creatorFullName', 102 | msg.data.creatorIdentity.creatorFullName 103 | ) 104 | palette.setPluginData( 105 | 'creatorAvatar', 106 | msg.data.creatorIdentity.creatorAvatar 107 | ) 108 | palette.setPluginData('creatorId', msg.data.creatorIdentity.creatorId) 109 | 110 | figma.ui.postMessage({ 111 | type: 'UPDATE_SCREENSHOT', 112 | data: await palette.exportAsync({ 113 | format: 'PNG', 114 | constraint: { type: 'SCALE', value: 0.25 }, 115 | }), 116 | }) 117 | } else figma.notify(locals[lang].error.corruption) 118 | } 119 | 120 | export default updateGlobal 121 | -------------------------------------------------------------------------------- /src/bridges/updates/updatePalette.ts: -------------------------------------------------------------------------------- 1 | import { PaletteNode } from 'src/types/nodes' 2 | import Colors from '../../canvas/Colors' 3 | import { lang, locals } from '../../content/locals' 4 | import setPaletteName from '../../utils/setPaletteName' 5 | import { 6 | currentSelection, 7 | isSelectionChanged, 8 | previousSelection, 9 | } from '../processSelection' 10 | 11 | interface Item { 12 | key: string 13 | value: boolean | object | string 14 | } 15 | 16 | const updatePalette = async (msg: Array) => { 17 | const palette = isSelectionChanged 18 | ? (previousSelection?.[0] as FrameNode) 19 | : (currentSelection[0] as FrameNode) 20 | 21 | if (palette.children.length === 1) { 22 | msg.forEach((s: Item) => { 23 | if (typeof s.value === 'object') 24 | palette.setPluginData(s.key, JSON.stringify(s.value)) 25 | else if (typeof s.value === 'boolean') 26 | palette.setPluginData(s.key, s.value.toString()) 27 | else palette.setPluginData(s.key, s.value) 28 | }) 29 | 30 | const keys = palette.getPluginDataKeys() 31 | const paletteData: [string, string | boolean | object][] = keys.map( 32 | (key) => { 33 | const value = palette.getPluginData(key) 34 | if (value === 'true' || value === 'false') 35 | return [key, value === 'true'] 36 | else if (value.includes('{')) 37 | return [key, JSON.parse(palette.getPluginData(key))] 38 | return [key, value] 39 | } 40 | ) 41 | const paletteObject = makePaletteNode(paletteData) 42 | const creatorAvatarImg = 43 | paletteObject.creatorAvatar !== '' 44 | ? await figma 45 | .createImageAsync(paletteObject.creatorAvatar ?? '') 46 | .then(async (image: Image) => image) 47 | .catch(() => null) 48 | : null 49 | 50 | palette.children[0].remove() 51 | palette.appendChild( 52 | new Colors( 53 | { 54 | ...paletteObject, 55 | name: paletteObject.name !== undefined ? paletteObject.name : '', 56 | description: 57 | paletteObject.description !== undefined 58 | ? paletteObject.description 59 | : '', 60 | creatorAvatarImg: creatorAvatarImg, 61 | service: 'EDIT', 62 | }, 63 | palette 64 | ).makeNode() 65 | ) 66 | 67 | // Update 68 | const now = new Date().toISOString() 69 | palette.setPluginData('updatedAt', now) 70 | figma.ui.postMessage({ 71 | type: 'UPDATE_PALETTE_DATE', 72 | data: now, 73 | }) 74 | 75 | // Palette migration 76 | palette.counterAxisSizingMode = 'AUTO' 77 | palette.name = setPaletteName( 78 | paletteObject.name !== undefined ? paletteObject.name : locals[lang].name, 79 | paletteObject.themes.find((theme) => theme.isEnabled)?.name, 80 | paletteObject.preset.name, 81 | paletteObject.colorSpace, 82 | paletteObject.visionSimulationMode 83 | ) 84 | } else figma.notify(locals[lang].error.corruption) 85 | } 86 | 87 | const makePaletteNode = (data: [string, string | boolean | object][]) => { 88 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 89 | const obj: { [key: string]: any } = {} 90 | 91 | data.forEach((d) => { 92 | obj[d[0]] = d[1] 93 | }) 94 | 95 | return obj as PaletteNode 96 | } 97 | 98 | export default updatePalette 99 | -------------------------------------------------------------------------------- /src/bridges/updates/updateScale.ts: -------------------------------------------------------------------------------- 1 | import { PaletteNode } from 'src/types/nodes' 2 | import Colors from '../../canvas/Colors' 3 | import { lang, locals } from '../../content/locals' 4 | import { ScaleMessage } from '../../types/messages' 5 | import doLightnessScale from '../../utils/doLightnessScale' 6 | import setPaletteName from '../../utils/setPaletteName' 7 | import { 8 | currentSelection, 9 | isSelectionChanged, 10 | previousSelection, 11 | } from '../processSelection' 12 | 13 | const updateScale = async (msg: ScaleMessage) => { 14 | const palette = isSelectionChanged 15 | ? (previousSelection?.[0] as FrameNode) 16 | : (currentSelection[0] as FrameNode) 17 | 18 | if (palette.children.length === 1) { 19 | if (Object.keys(msg.data.preset).length !== 0) 20 | palette.setPluginData('preset', JSON.stringify(msg.data.preset)) 21 | 22 | const keys = palette.getPluginDataKeys() 23 | const paletteData: [string, string | boolean | object][] = keys.map( 24 | (key) => { 25 | const value = palette.getPluginData(key) 26 | if (value === 'true' || value === 'false') 27 | return [key, value === 'true'] 28 | else if (value.includes('{')) 29 | return [key, JSON.parse(palette.getPluginData(key))] 30 | return [key, value] 31 | } 32 | ) 33 | const paletteObject = makePaletteNode(paletteData) 34 | const creatorAvatarImg = 35 | paletteObject.creatorAvatar !== '' 36 | ? await figma 37 | .createImageAsync(paletteObject.creatorAvatar ?? '') 38 | .then(async (image: Image) => image) 39 | .catch(() => null) 40 | : null 41 | 42 | const theme = paletteObject.themes.find((theme) => theme.isEnabled) 43 | if (theme !== undefined) theme.scale = msg.data.scale 44 | 45 | if (msg.feature === 'ADD_STOP' || msg.feature === 'DELETE_STOP') 46 | paletteObject.themes.forEach((theme) => { 47 | if (!theme.isEnabled) 48 | theme.scale = doLightnessScale( 49 | Object.keys(msg.data.scale).map((stop) => { 50 | return parseFloat(stop.replace('lightness-', '')) 51 | }), 52 | theme.scale[ 53 | Object.keys(theme.scale)[Object.keys(theme.scale).length - 1] 54 | ], 55 | theme.scale[Object.keys(theme.scale)[0]] 56 | ) 57 | }) 58 | 59 | palette.setPluginData('scale', JSON.stringify(msg.data.scale)) 60 | palette.setPluginData('shift', JSON.stringify(msg.data.shift)) 61 | palette.setPluginData('themes', JSON.stringify(paletteObject.themes)) 62 | 63 | if ( 64 | JSON.stringify(msg.data.scale) !== JSON.stringify(paletteObject.scale) 65 | ) { 66 | palette.children[0].remove() 67 | palette.appendChild( 68 | new Colors( 69 | { 70 | ...paletteObject, 71 | scale: msg.data.scale, 72 | name: paletteObject.name !== undefined ? paletteObject.name : '', 73 | description: 74 | paletteObject.description !== undefined 75 | ? paletteObject.description 76 | : '', 77 | view: 78 | msg.isEditedInRealTime && 79 | paletteObject.view === 'PALETTE_WITH_PROPERTIES' 80 | ? 'PALETTE' 81 | : msg.isEditedInRealTime && paletteObject.view === 'SHEET' 82 | ? 'SHEET_SAFE_MODE' 83 | : paletteObject.view, 84 | creatorAvatarImg: creatorAvatarImg, 85 | service: 'EDIT', 86 | }, 87 | palette 88 | ).makeNode() 89 | ) 90 | 91 | // Update 92 | const now = new Date().toISOString() 93 | palette.setPluginData('updatedAt', now) 94 | figma.ui.postMessage({ 95 | type: 'UPDATE_PALETTE_DATE', 96 | data: now, 97 | }) 98 | } 99 | 100 | // Palette migration 101 | palette.counterAxisSizingMode = 'AUTO' 102 | palette.name = setPaletteName( 103 | paletteObject.name !== undefined ? paletteObject.name : locals[lang].name, 104 | paletteObject.themes.find((theme) => theme.isEnabled)?.name, 105 | msg.data.preset.name, 106 | paletteObject.colorSpace, 107 | paletteObject.visionSimulationMode 108 | ) 109 | } else figma.notify(locals[lang].error.corruption) 110 | } 111 | 112 | const makePaletteNode = (data: [string, string | boolean | object][]) => { 113 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 114 | const obj: { [key: string]: any } = {} 115 | 116 | data.forEach((d) => { 117 | obj[d[0]] = d[1] 118 | }) 119 | 120 | return obj as PaletteNode 121 | } 122 | 123 | export default updateScale 124 | -------------------------------------------------------------------------------- /src/bridges/updates/updateSettings.ts: -------------------------------------------------------------------------------- 1 | import { PaletteNode } from 'src/types/nodes' 2 | import Colors from '../../canvas/Colors' 3 | import { lang, locals } from '../../content/locals' 4 | import { SettingsMessage } from '../../types/messages' 5 | import setPaletteName from '../../utils/setPaletteName' 6 | import { 7 | currentSelection, 8 | isSelectionChanged, 9 | previousSelection, 10 | } from '../processSelection' 11 | 12 | const updateSettings = async (msg: SettingsMessage) => { 13 | const palette = isSelectionChanged 14 | ? (previousSelection?.[0] as FrameNode) 15 | : (currentSelection[0] as FrameNode) 16 | 17 | if (palette.children.length === 1) { 18 | const keys = palette.getPluginDataKeys() 19 | const paletteData: [string, string | boolean | object][] = keys.map( 20 | (key) => { 21 | const value = palette.getPluginData(key) 22 | if (value === 'true' || value === 'false') 23 | return [key, value === 'true'] 24 | else if (value.includes('{')) 25 | return [key, JSON.parse(palette.getPluginData(key))] 26 | return [key, value] 27 | } 28 | ) 29 | const paletteObject = makePaletteNode(paletteData) 30 | const creatorAvatarImg = 31 | paletteObject.creatorAvatar !== '' 32 | ? await figma 33 | .createImageAsync(paletteObject.creatorAvatar ?? '') 34 | .then(async (image: Image) => image) 35 | .catch(() => null) 36 | : null 37 | 38 | palette.setPluginData('name', msg.data.name) 39 | palette.setPluginData('description', msg.data.description) 40 | palette.setPluginData('colorSpace', msg.data.colorSpace) 41 | palette.setPluginData('visionSimulationMode', msg.data.visionSimulationMode) 42 | palette.setPluginData( 43 | 'textColorsTheme', 44 | JSON.stringify(msg.data.textColorsTheme) 45 | ) 46 | palette.setPluginData('algorithmVersion', msg.data.algorithmVersion) 47 | 48 | palette.children[0].remove() 49 | palette.appendChild( 50 | new Colors( 51 | { 52 | ...paletteObject, 53 | name: msg.data.name, 54 | description: msg.data.description, 55 | colorSpace: msg.data.colorSpace, 56 | visionSimulationMode: msg.data.visionSimulationMode, 57 | textColorsTheme: msg.data.textColorsTheme, 58 | algorithmVersion: msg.data.algorithmVersion, 59 | view: 60 | msg.isEditedInRealTime && 61 | paletteObject.view === 'PALETTE_WITH_PROPERTIES' 62 | ? 'PALETTE' 63 | : msg.isEditedInRealTime && paletteObject.view === 'SHEET' 64 | ? 'SHEET_SAFE_MODE' 65 | : paletteObject.view, 66 | creatorAvatarImg: creatorAvatarImg, 67 | service: 'EDIT', 68 | }, 69 | palette 70 | ).makeNode() 71 | ) 72 | 73 | // Update 74 | const now = new Date().toISOString() 75 | palette.setPluginData('updatedAt', now) 76 | figma.ui.postMessage({ 77 | type: 'UPDATE_PALETTE_DATE', 78 | data: now, 79 | }) 80 | 81 | // Palette migration 82 | palette.counterAxisSizingMode = 'AUTO' 83 | palette.name = setPaletteName( 84 | msg.data.name !== undefined ? msg.data.name : locals[lang].name, 85 | paletteObject.themes.find((theme) => theme.isEnabled)?.name, 86 | paletteObject.preset.name, 87 | msg.data.colorSpace, 88 | msg.data.visionSimulationMode 89 | ) 90 | } else figma.notify(locals[lang].error.corruption) 91 | } 92 | 93 | const makePaletteNode = (data: [string, string | boolean | object][]) => { 94 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 95 | const obj: { [key: string]: any } = {} 96 | 97 | data.forEach((d) => { 98 | obj[d[0]] = d[1] 99 | }) 100 | 101 | return obj as PaletteNode 102 | } 103 | 104 | export default updateSettings 105 | -------------------------------------------------------------------------------- /src/bridges/updates/updateThemes.ts: -------------------------------------------------------------------------------- 1 | import { PaletteNode } from 'src/types/nodes' 2 | import Colors from '../../canvas/Colors' 3 | import { lang, locals } from '../../content/locals' 4 | import { ThemesMessage } from '../../types/messages' 5 | import setPaletteName from '../../utils/setPaletteName' 6 | import { 7 | currentSelection, 8 | isSelectionChanged, 9 | previousSelection, 10 | } from '../processSelection' 11 | 12 | const updateThemes = async (msg: ThemesMessage) => { 13 | const palette = isSelectionChanged 14 | ? (previousSelection?.[0] as FrameNode) 15 | : (currentSelection[0] as FrameNode) 16 | 17 | if (palette.children.length === 1) { 18 | const keys = palette.getPluginDataKeys() 19 | const paletteData: [string, string | boolean | object][] = keys.map( 20 | (key) => { 21 | const value = palette.getPluginData(key) 22 | if (value === 'true' || value === 'false') 23 | return [key, value === 'true'] 24 | else if (value.includes('{')) 25 | return [key, JSON.parse(palette.getPluginData(key))] 26 | return [key, value] 27 | } 28 | ) 29 | const paletteObject = makePaletteNode(paletteData) 30 | const creatorAvatarImg = 31 | paletteObject.creatorAvatar !== '' 32 | ? await figma 33 | .createImageAsync(paletteObject.creatorAvatar ?? '') 34 | .then(async (image: Image) => image) 35 | .catch(() => null) 36 | : null 37 | 38 | palette.setPluginData('themes', JSON.stringify(msg.data)) 39 | 40 | palette.children[0].remove() 41 | palette.appendChild( 42 | new Colors( 43 | { 44 | ...paletteObject, 45 | themes: msg.data, 46 | name: paletteObject.name !== undefined ? paletteObject.name : '', 47 | description: 48 | paletteObject.description !== undefined 49 | ? paletteObject.description 50 | : '', 51 | view: 52 | msg.isEditedInRealTime && 53 | paletteObject.view === 'PALETTE_WITH_PROPERTIES' 54 | ? 'PALETTE' 55 | : msg.isEditedInRealTime && paletteObject.view === 'SHEET' 56 | ? 'SHEET_SAFE_MODE' 57 | : paletteObject.view, 58 | creatorAvatarImg: creatorAvatarImg, 59 | service: 'EDIT', 60 | }, 61 | palette 62 | ).makeNode() 63 | ) 64 | 65 | // Update 66 | const now = new Date().toISOString() 67 | palette.setPluginData('updatedAt', now) 68 | figma.ui.postMessage({ 69 | type: 'UPDATE_PALETTE_DATE', 70 | data: now, 71 | }) 72 | 73 | // Palette migration 74 | palette.counterAxisSizingMode = 'AUTO' 75 | palette.name = setPaletteName( 76 | paletteObject.name !== undefined ? paletteObject.name : locals[lang].name, 77 | msg.data.find((theme) => theme.isEnabled)?.name, 78 | paletteObject.preset.name, 79 | paletteObject.colorSpace, 80 | paletteObject.visionSimulationMode 81 | ) 82 | } else figma.notify(locals[lang].error.corruption) 83 | } 84 | 85 | const makePaletteNode = (data: [string, string | boolean | object][]) => { 86 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 87 | const obj: { [key: string]: any } = {} 88 | 89 | data.forEach((d) => { 90 | obj[d[0]] = d[1] 91 | }) 92 | 93 | return obj as PaletteNode 94 | } 95 | 96 | export default updateThemes 97 | -------------------------------------------------------------------------------- /src/bridges/updates/updateView.ts: -------------------------------------------------------------------------------- 1 | import { PaletteNode } from 'src/types/nodes' 2 | import Colors from '../../canvas/Colors' 3 | import { lang, locals } from '../../content/locals' 4 | import { ViewMessage } from '../../types/messages' 5 | import setPaletteName from '../../utils/setPaletteName' 6 | import { 7 | currentSelection, 8 | isSelectionChanged, 9 | previousSelection, 10 | } from '../processSelection' 11 | 12 | const updateView = async (msg: ViewMessage) => { 13 | const palette = isSelectionChanged 14 | ? (previousSelection?.[0] as FrameNode) 15 | : (currentSelection[0] as FrameNode) 16 | 17 | if (palette.children.length === 1) { 18 | const keys = palette.getPluginDataKeys() 19 | const paletteData: [string, string | boolean | object][] = keys.map( 20 | (key) => { 21 | const value = palette.getPluginData(key) 22 | if (value === 'true' || value === 'false') 23 | return [key, value === 'true'] 24 | else if (value.includes('{')) 25 | return [key, JSON.parse(palette.getPluginData(key))] 26 | return [key, value] 27 | } 28 | ) 29 | const paletteObject = makePaletteNode(paletteData) 30 | const creatorAvatarImg = 31 | paletteObject.creatorAvatar !== '' 32 | ? await figma 33 | .createImageAsync(paletteObject.creatorAvatar ?? '') 34 | .then(async (image: Image) => image) 35 | .catch(() => null) 36 | : null 37 | 38 | palette.setPluginData('view', msg.data.view) 39 | 40 | palette.children[0].remove() 41 | palette.appendChild( 42 | new Colors( 43 | { 44 | ...paletteObject, 45 | view: msg.data.view, 46 | name: paletteObject.name !== undefined ? paletteObject.name : '', 47 | description: 48 | paletteObject.description !== undefined 49 | ? paletteObject.description 50 | : '', 51 | creatorAvatarImg: creatorAvatarImg, 52 | service: 'EDIT', 53 | }, 54 | palette 55 | ).makeNode() 56 | ) 57 | 58 | // Update 59 | const now = new Date().toISOString() 60 | palette.setPluginData('updatedAt', now) 61 | figma.ui.postMessage({ 62 | type: 'UPDATE_PALETTE_DATE', 63 | data: now, 64 | }) 65 | 66 | // Palette migration 67 | palette.counterAxisSizingMode = 'AUTO' 68 | palette.name = setPaletteName( 69 | paletteObject.name !== undefined ? paletteObject.name : locals[lang].name, 70 | paletteObject.themes.find((theme) => theme.isEnabled)?.name, 71 | paletteObject.preset.name, 72 | paletteObject.colorSpace, 73 | paletteObject.visionSimulationMode 74 | ) 75 | } else figma.notify(locals[lang].error.corruption) 76 | } 77 | 78 | const makePaletteNode = (data: [string, string | boolean | object][]) => { 79 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 80 | const obj: { [key: string]: any } = {} 81 | 82 | data.forEach((d) => { 83 | obj[d[0]] = d[1] 84 | }) 85 | 86 | return obj as PaletteNode 87 | } 88 | 89 | export default updateView 90 | -------------------------------------------------------------------------------- /src/canvas/Header.ts: -------------------------------------------------------------------------------- 1 | import { lang, locals } from '../content/locals' 2 | import { ScaleConfiguration } from '../types/configurations' 3 | import { PaletteNode } from '../types/nodes' 4 | import Sample from './Sample' 5 | 6 | export default class Header { 7 | private parent: PaletteNode 8 | private currentScale: ScaleConfiguration 9 | private sampleSize: number 10 | private node: FrameNode | null 11 | 12 | constructor(parent: PaletteNode, size: number) { 13 | this.parent = parent 14 | this.currentScale = 15 | this.parent.themes.find((theme) => theme.isEnabled)?.scale ?? {} 16 | this.sampleSize = size 17 | this.node = null 18 | } 19 | 20 | makeNode = () => { 21 | // Base 22 | this.node = figma.createFrame() 23 | this.node.name = '_header' 24 | this.node.resize(100, this.sampleSize / 4) 25 | this.node.fills = [] 26 | 27 | // Layout 28 | this.node.layoutMode = 'HORIZONTAL' 29 | this.node.layoutSizingHorizontal = 'HUG' 30 | this.node.layoutSizingVertical = 'HUG' 31 | 32 | // Insert 33 | this.node.appendChild( 34 | new Sample( 35 | locals[lang].paletteProperties.sourceColors, 36 | null, 37 | null, 38 | [255, 255, 255], 39 | this.parent.colorSpace, 40 | this.parent.visionSimulationMode, 41 | this.parent.view, 42 | this.parent.textColorsTheme 43 | ).makeNodeName('FIXED', this.sampleSize, 48) 44 | ) 45 | if (this.parent.view.includes('PALETTE')) 46 | Object.values(this.currentScale) 47 | .reverse() 48 | .forEach((lightness) => { 49 | this.node?.appendChild( 50 | new Sample( 51 | Object.keys(this.currentScale) 52 | .find((key) => this.currentScale[key] === lightness) 53 | ?.substr(10) ?? '0', 54 | null, 55 | null, 56 | [255, 255, 255], 57 | this.parent.colorSpace, 58 | this.parent.visionSimulationMode, 59 | this.parent.view, 60 | this.parent.textColorsTheme 61 | ).makeNodeName('FIXED', this.sampleSize, 48) 62 | ) 63 | }) 64 | 65 | return this.node 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/canvas/LocalStyle.ts: -------------------------------------------------------------------------------- 1 | import { RgbModel } from '@a_ng_d/figmug-ui' 2 | 3 | export default class LocalStyle { 4 | private name: string 5 | private description: string 6 | private rgb: RgbModel 7 | private paintStyle: PaintStyle | null 8 | 9 | constructor(name: string, description: string, rgb: RgbModel) { 10 | this.name = name 11 | this.description = description 12 | this.rgb = rgb 13 | this.paintStyle = null 14 | } 15 | 16 | makePaintStyle = () => { 17 | this.paintStyle = figma.createPaintStyle() 18 | this.paintStyle.name = this.name 19 | this.paintStyle.description = this.description 20 | this.paintStyle.paints = [ 21 | { 22 | type: 'SOLID', 23 | color: this.rgb, 24 | }, 25 | ] 26 | 27 | return this.paintStyle 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/canvas/LocalVariable.ts: -------------------------------------------------------------------------------- 1 | export default class LocalVariable { 2 | private variable: Variable | undefined 3 | 4 | constructor(variable?: Variable) { 5 | this.variable = variable 6 | } 7 | 8 | makeCollection = (name: string) => { 9 | const collection = figma.variables.createVariableCollection(name) 10 | 11 | return collection 12 | } 13 | 14 | makeVariable = ( 15 | name: string, 16 | collection: VariableCollection, 17 | description: string 18 | ) => { 19 | this.variable = figma.variables.createVariable(name, collection, 'COLOR') 20 | this.variable.description = description 21 | 22 | return this.variable 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/canvas/Paragraph.ts: -------------------------------------------------------------------------------- 1 | export default class Paragraph { 2 | private name: string 3 | private content: string 4 | private fontSize: number 5 | private type: 'FILL' | 'FIXED' 6 | private width?: number 7 | private nodeText: TextNode | null 8 | private node: FrameNode | null 9 | 10 | constructor( 11 | name: string, 12 | content: string, 13 | type: 'FILL' | 'FIXED', 14 | width?: number, 15 | fontSize = 12 16 | ) { 17 | this.name = name 18 | this.content = content 19 | this.fontSize = fontSize 20 | this.type = type 21 | this.width = width 22 | this.nodeText = null 23 | this.node = null 24 | } 25 | 26 | makeNodeText = () => { 27 | // Base 28 | this.nodeText = figma.createText() 29 | this.nodeText.name = '_text' 30 | this.nodeText.characters = this.content 31 | this.nodeText.fontName = { 32 | family: 'Martian Mono', 33 | style: 'Medium', 34 | } 35 | this.nodeText.fontSize = this.fontSize 36 | this.nodeText.setRangeLineHeight(0, this.content.length, { 37 | value: 130, 38 | unit: 'PERCENT', 39 | }) 40 | this.nodeText.fills = [ 41 | { 42 | type: 'SOLID', 43 | color: { 44 | r: 16 / 255, 45 | g: 35 / 255, 46 | b: 37 / 255, 47 | }, 48 | }, 49 | ] 50 | 51 | // Layout 52 | this.nodeText.layoutGrow = 1 53 | 54 | return this.nodeText 55 | } 56 | 57 | makeNode() { 58 | // Base 59 | this.node = figma.createFrame() 60 | this.node.name = this.name 61 | this.node.fills = [ 62 | { 63 | type: 'SOLID', 64 | opacity: 0.5, 65 | color: { 66 | r: 1, 67 | g: 1, 68 | b: 1, 69 | }, 70 | }, 71 | ] 72 | this.node.cornerRadius = 16 73 | if (this.type === 'FIXED') this.node.resize(this.width ?? 100, 100) 74 | 75 | // Layout 76 | this.node.layoutMode = 'HORIZONTAL' 77 | if (this.type === 'FIXED') this.node.layoutSizingHorizontal = 'FIXED' 78 | else { 79 | this.node.primaryAxisSizingMode = 'FIXED' 80 | this.node.layoutAlign = 'STRETCH' 81 | } 82 | this.node.layoutSizingVertical = 'HUG' 83 | this.node.horizontalPadding = 8 84 | this.node.verticalPadding = 8 85 | 86 | // Insert 87 | this.node.appendChild(this.makeNodeText()) 88 | 89 | return this.node 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/canvas/Property.ts: -------------------------------------------------------------------------------- 1 | import Tag from './Tag' 2 | 3 | export default class Property { 4 | private name: string 5 | private content: string 6 | private size: number 7 | private node: FrameNode | null 8 | 9 | constructor(name: string, content: string, size: number) { 10 | this.name = name 11 | this.content = content 12 | this.size = size 13 | this.node = null 14 | } 15 | 16 | makeNode = () => { 17 | // Base 18 | this.node = figma.createFrame() 19 | this.node.name = '_property' 20 | this.node.fills = [] 21 | 22 | // Layout 23 | this.node.layoutMode = 'VERTICAL' 24 | this.node.counterAxisSizingMode = 'FIXED' 25 | this.node.layoutAlign = 'STRETCH' 26 | this.node.primaryAxisSizingMode = 'FIXED' 27 | this.node.layoutGrow = 1 28 | 29 | // Insert 30 | this.node.appendChild( 31 | new Tag({ 32 | name: this.name, 33 | content: this.content, 34 | fontSize: this.size, 35 | }).makeNodeTag() 36 | ) 37 | 38 | return this.node 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/canvas/Status.ts: -------------------------------------------------------------------------------- 1 | import { lang, locals } from '../content/locals' 2 | import Tag from './Tag' 3 | 4 | export default class Status { 5 | private status: { isClosestToRef: boolean; isLocked: boolean } 6 | private source: { [key: string]: number } 7 | private node: FrameNode | null 8 | 9 | constructor( 10 | status: { isClosestToRef: boolean; isLocked: boolean }, 11 | source: { [key: string]: number } 12 | ) { 13 | this.status = status 14 | this.source = source 15 | this.node = null 16 | } 17 | 18 | makeNode = () => { 19 | // Base 20 | this.node = figma.createFrame() 21 | this.node.name = '_status' 22 | this.node.fills = [] 23 | 24 | // Layout 25 | this.node.layoutMode = 'HORIZONTAL' 26 | this.node.primaryAxisSizingMode = 'FIXED' 27 | this.node.layoutAlign = 'STRETCH' 28 | this.node.layoutSizingVertical = 'HUG' 29 | 30 | if (this.status.isClosestToRef) 31 | this.node.appendChild( 32 | new Tag({ 33 | name: '_close', 34 | content: locals[lang].paletteProperties.closest, 35 | fontSize: 10, 36 | }).makeNodeTagwithIndicator([ 37 | this.source.r, 38 | this.source.g, 39 | this.source.b, 40 | 1, 41 | ]) 42 | ) 43 | 44 | if (this.status.isLocked) 45 | this.node.appendChild( 46 | new Tag({ 47 | name: '_lock', 48 | content: locals[lang].paletteProperties.locked, 49 | fontSize: 10, 50 | }).makeNodeTag() 51 | ) 52 | 53 | return this.node 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/canvas/Title.ts: -------------------------------------------------------------------------------- 1 | import { lang, locals } from '../content/locals' 2 | import { PaletteNode } from '../types/nodes' 3 | import Paragraph from './Paragraph' 4 | import Tag from './Tag' 5 | 6 | export default class Title { 7 | private parent: PaletteNode 8 | private nodeGlobalInfo: FrameNode | null 9 | private nodeDescriptions: FrameNode | null 10 | private nodeProps: FrameNode | null 11 | private node: FrameNode | null 12 | 13 | constructor(parent: PaletteNode) { 14 | this.parent = parent 15 | this.nodeGlobalInfo = null 16 | this.nodeDescriptions = null 17 | this.nodeProps = null 18 | this.node = null 19 | } 20 | 21 | makeNodeGlobalInfo = () => { 22 | // Base 23 | this.nodeGlobalInfo = figma.createFrame() 24 | this.nodeGlobalInfo.name = '_palette-global' 25 | this.nodeGlobalInfo.fills = [] 26 | 27 | // Layout 28 | this.nodeGlobalInfo.layoutMode = 'VERTICAL' 29 | this.nodeGlobalInfo.layoutSizingHorizontal = 'HUG' 30 | this.nodeGlobalInfo.layoutSizingVertical = 'HUG' 31 | this.nodeGlobalInfo.itemSpacing = 8 32 | 33 | // Insert 34 | this.nodeGlobalInfo.appendChild( 35 | new Tag({ 36 | name: '_name', 37 | content: this.parent.name === '' ? locals[lang].name : this.parent.name, 38 | fontSize: 20, 39 | }).makeNodeTag() 40 | ) 41 | if ( 42 | this.parent.description !== '' || 43 | this.parent.themes.find((theme) => theme.isEnabled)?.description !== '' 44 | ) 45 | this.nodeGlobalInfo.appendChild(this.makeNodeDescriptions()) 46 | 47 | return this.nodeGlobalInfo 48 | } 49 | 50 | makeNodeDescriptions = () => { 51 | // Base 52 | this.nodeDescriptions = figma.createFrame() 53 | this.nodeDescriptions.name = '_palette-description(s)' 54 | this.nodeDescriptions.fills = [] 55 | 56 | // Layout 57 | this.nodeDescriptions.layoutMode = 'HORIZONTAL' 58 | this.nodeDescriptions.layoutSizingHorizontal = 'HUG' 59 | this.nodeDescriptions.layoutSizingVertical = 'HUG' 60 | this.nodeDescriptions.itemSpacing = 8 61 | 62 | // Insert 63 | if (this.parent.description !== '') 64 | this.nodeDescriptions.appendChild( 65 | new Paragraph( 66 | '_palette-description', 67 | this.parent.description, 68 | 'FIXED', 69 | 644, 70 | 12 71 | ).makeNode() 72 | ) 73 | 74 | if (this.parent.themes.find((theme) => theme.isEnabled)?.description !== '') 75 | this.nodeDescriptions.appendChild( 76 | new Paragraph( 77 | '_theme-description', 78 | 'Theme description: ' + 79 | this.parent.themes.find((theme) => theme.isEnabled)?.description, 80 | 'FIXED', 81 | 644, 82 | 12 83 | ).makeNode() 84 | ) 85 | 86 | return this.nodeDescriptions 87 | } 88 | 89 | makeNodeProps = () => { 90 | // Base 91 | this.nodeProps = figma.createFrame() 92 | this.nodeProps.name = '_palette-props' 93 | this.nodeProps.fills = [] 94 | 95 | // Layout 96 | this.nodeProps.layoutMode = 'VERTICAL' 97 | this.nodeProps.layoutSizingHorizontal = 'HUG' 98 | this.nodeProps.layoutSizingVertical = 'HUG' 99 | this.nodeProps.counterAxisAlignItems = 'MAX' 100 | this.nodeProps.itemSpacing = 8 101 | 102 | // Insert 103 | if ( 104 | this.parent.creatorFullName !== undefined && 105 | this.parent.creatorFullName !== '' 106 | ) 107 | this.nodeProps.appendChild( 108 | new Tag({ 109 | name: '_creator_id', 110 | content: `${locals[lang].paletteProperties.provider}${this.parent.creatorFullName}`, 111 | fontSize: 12, 112 | }).makeNodeTagWithAvatar(this.parent.creatorAvatarImg) 113 | ) 114 | 115 | if ( 116 | this.parent.themes.find((theme) => theme.isEnabled)?.type !== 117 | 'default theme' 118 | ) 119 | this.nodeProps.appendChild( 120 | new Tag({ 121 | name: '_theme', 122 | content: `${locals[lang].paletteProperties.theme}${ 123 | this.parent.themes.find((theme) => theme.isEnabled)?.name 124 | }`, 125 | fontSize: 12, 126 | }).makeNodeTag() 127 | ) 128 | this.nodeProps.appendChild( 129 | new Tag({ 130 | name: '_preset', 131 | content: `${locals[lang].paletteProperties.preset}${this.parent.preset.name}`, 132 | fontSize: 12, 133 | }).makeNodeTag() 134 | ) 135 | this.nodeProps.appendChild( 136 | new Tag({ 137 | name: '_color-space', 138 | content: `${locals[lang].paletteProperties.colorSpace}${this.parent.colorSpace}`, 139 | fontSize: 12, 140 | }).makeNodeTag() 141 | ) 142 | if (this.parent.visionSimulationMode !== 'NONE') 143 | this.nodeProps.appendChild( 144 | new Tag({ 145 | name: '_vision-simulation', 146 | content: `${locals[lang].paletteProperties.visionSimulation}${ 147 | this.parent.visionSimulationMode.charAt(0) + 148 | this.parent.visionSimulationMode.toLocaleLowerCase().slice(1) 149 | }`, 150 | fontSize: 12, 151 | }).makeNodeTag() 152 | ) 153 | 154 | return this.nodeProps 155 | } 156 | 157 | makeNode = () => { 158 | // Base 159 | this.node = figma.createFrame() 160 | this.node.name = '_title' 161 | this.node.fills = [] 162 | 163 | // Layout 164 | this.node.layoutMode = 'HORIZONTAL' 165 | this.node.primaryAxisSizingMode = 'FIXED' 166 | this.node.layoutAlign = 'STRETCH' 167 | this.node.counterAxisSizingMode = 'AUTO' 168 | this.node.primaryAxisAlignItems = 'SPACE_BETWEEN' 169 | 170 | // Insert 171 | this.node.appendChild(this.makeNodeGlobalInfo()) 172 | this.node.appendChild(this.makeNodeProps()) 173 | 174 | return this.node 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { doSpecificMode, featuresScheme } from './stores/features' 2 | 3 | // Theme 4 | export const theme = 'figma-ui3' 5 | 6 | // Limitations 7 | export const isTrialEnabled = false 8 | export const isProEnabled = true 9 | export const trialTime = 48 10 | export const oldTrialTime = 72 11 | export const pageSize = 20 12 | 13 | // Versions 14 | export const userConsentVersion = '2024.01' 15 | export const trialVersion = '2024.03' 16 | export const algorithmVersion = 'v3' 17 | export const paletteDataVersion = '2025.03' 18 | 19 | // URLs 20 | export const authWorkerUrl = 21 | process.env.NODE_ENV === 'development' 22 | ? 'http://localhost:8787' 23 | : (process.env.REACT_APP_AUTH_WORKER_URL as string) 24 | export const announcementsWorkerUrl = 25 | process.env.NODE_ENV === 'development' 26 | ? 'http://localhost:8888' 27 | : (process.env.REACT_APP_ANNOUNCEMENTS_WORKER_URL as string) 28 | export const databaseUrl = process.env.REACT_APP_SUPABASE_URL as string 29 | export const authUrl = 30 | process.env.NODE_ENV === 'development' 31 | ? 'http://localhost:3000' 32 | : (process.env.REACT_APP_AUTH_URL as string) 33 | 34 | export const palettesDbTableName = 35 | process.env.NODE_ENV === 'development' ? 'sandbox.palettes' : 'palettes' 36 | export const palettesStorageName = 37 | process.env.NODE_ENV === 'development' 38 | ? 'palette.screenshots' 39 | : 'palette.screenshots' 40 | 41 | // External URLs 42 | export const documentationUrl = 'https://uicp.ylb.lt/docs' 43 | export const repositoryUrl = 'https://uicp.ylb.lt/repository' 44 | export const supportEmail = 'https://uicp.ylb.lt/contact' 45 | export const feedbackUrl = 'https://uicp.ylb.lt/feedback' 46 | export const trialFeedbackUrl = 'https://uicp.ylb.lt/feedback-trial' 47 | export const requestsUrl = 'https://uicp.ylb.lt/ideas' 48 | export const networkUrl = 'https://uicp.ylb.lt/network' 49 | export const authorUrl = 'https://uicp.ylb.lt/author' 50 | export const licenseUrl = 'https://uicp.ylb.lt/license' 51 | export const privacyUrl = 'https://uicp.ylb.lt/privacy' 52 | export const vsCodeFigmaPluginUrl = 53 | 'https://marketplace.visualstudio.com/items?itemName=figma.figma-vscode-extension' 54 | export const isbUrl = 'https://isb.ylb.lt/run' 55 | 56 | // Features modes 57 | const devMode = featuresScheme 58 | const prodMode = doSpecificMode( 59 | [ 60 | 'SYNC_LOCAL_STYLES', 61 | 'SYNC_LOCAL_VARIABLES', 62 | 'PREVIEW_LOCK_SOURCE_COLORS', 63 | 'SOURCE', 64 | 'PRESETS_MATERIAL_3', 65 | 'PRESETS_TAILWIND', 66 | 'PRESETS_ADS', 67 | 'PRESETS_ADS_NEUTRAL', 68 | 'PRESETS_CARBON', 69 | 'PRESETS_BASE', 70 | 'PRESETS_POLARIS', 71 | 'PRESETS_CUSTOM_ADD', 72 | 'SCALE_CHROMA', 73 | 'SCALE_HELPER_DISTRIBUTION', 74 | 'THEMES', 75 | 'THEMES_NAME', 76 | 'THEMES_PARAMS', 77 | 'THEMES_DESCRIPTION', 78 | 'COLORS', 79 | 'COLORS_HUE_SHIFTING', 80 | 'COLORS_CHROMA_SHIFTING', 81 | 'EXPORT_TOKENS_JSON_AMZN_STYLE_DICTIONARY', 82 | 'EXPORT_TOKENS_JSON_TOKENS_STUDIO', 83 | 'EXPORT_TAILWIND', 84 | 'EXPORT_APPLE_SWIFTUI', 85 | 'EXPORT_APPLE_UIKIT', 86 | 'EXPORT_ANDROID_COMPOSE', 87 | 'EXPORT_ANDROID_XML', 88 | 'EXPORT_CSV', 89 | 'VIEWS_SHEET', 90 | 'SETTINGS_VISION_SIMULATION_MODE_DEUTERANOMALY', 91 | 'SETTINGS_VISION_SIMULATION_MODE_DEUTERANOPIA', 92 | 'SETTINGS_VISION_SIMULATION_MODE_TRITANOMALY', 93 | 'SETTINGS_VISION_SIMULATION_MODE_TRITANOPIA', 94 | 'SETTINGS_VISION_SIMULATION_MODE_ACHROMATOMALY', 95 | 'SETTINGS_VISION_SIMULATION_MODE_ACHROMATOPSIA', 96 | ], 97 | [] 98 | ) 99 | 100 | export const features = 101 | process.env.NODE_ENV === 'development' ? devMode : prodMode 102 | 103 | export default features 104 | -------------------------------------------------------------------------------- /src/content/images/choose_plan.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-ng-d/figma-ui-color-palette/872a0020e4ac5bfc6cef68c2e5c2880b421e9530/src/content/images/choose_plan.webp -------------------------------------------------------------------------------- /src/content/images/distribution_easing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-ng-d/figma-ui-color-palette/872a0020e4ac5bfc6cef68c2e5c2880b421e9530/src/content/images/distribution_easing.gif -------------------------------------------------------------------------------- /src/content/images/isb_product_thumbnail.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-ng-d/figma-ui-color-palette/872a0020e4ac5bfc6cef68c2e5c2880b421e9530/src/content/images/isb_product_thumbnail.webp -------------------------------------------------------------------------------- /src/content/images/lock_source_colors.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-ng-d/figma-ui-color-palette/872a0020e4ac5bfc6cef68c2e5c2880b421e9530/src/content/images/lock_source_colors.gif -------------------------------------------------------------------------------- /src/content/images/pro_plan.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-ng-d/figma-ui-color-palette/872a0020e4ac5bfc6cef68c2e5c2880b421e9530/src/content/images/pro_plan.webp -------------------------------------------------------------------------------- /src/content/images/publication.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-ng-d/figma-ui-color-palette/872a0020e4ac5bfc6cef68c2e5c2880b421e9530/src/content/images/publication.webp -------------------------------------------------------------------------------- /src/content/images/trial.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-ng-d/figma-ui-color-palette/872a0020e4ac5bfc6cef68c2e5c2880b421e9530/src/content/images/trial.webp -------------------------------------------------------------------------------- /src/content/images/uicp_product_thumbnail.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a-ng-d/figma-ui-color-palette/872a0020e4ac5bfc6cef68c2e5c2880b421e9530/src/content/images/uicp_product_thumbnail.webp -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import checkPlanStatus from './bridges/checks/checkPlanStatus' 2 | import createPalette from './bridges/creations/createPalette' 3 | import loadParameters from './bridges/loadParameters' 4 | import loadUI from './bridges/loadUI' 5 | import processSelection from './bridges/processSelection' 6 | import { algorithmVersion } from './config' 7 | import { presets } from './stores/presets' 8 | import { PaletteConfiguration } from './types/configurations' 9 | import doLightnessScale from './utils/doLightnessScale' 10 | import setPaletteMigration from './utils/setPaletteMigration' 11 | 12 | // Fonts 13 | figma.loadFontAsync({ family: 'Inter', style: 'Regular' }) 14 | figma.loadFontAsync({ family: 'Inter', style: 'Medium' }) 15 | figma.loadFontAsync({ family: 'Martian Mono', style: 'Medium' }) 16 | 17 | // Parameters 18 | figma.parameters.on( 19 | 'input', 20 | ({ parameters, key, query, result }: ParameterInputEvent) => 21 | loadParameters({ parameters, key, query, result }) 22 | ) 23 | 24 | // Loader 25 | figma.on('run', async ({ parameters }: RunEvent) => { 26 | if (parameters === undefined) { 27 | figma.on('selectionchange', () => processSelection()) 28 | figma.on('selectionchange', async () => await checkPlanStatus()) 29 | loadUI() 30 | } else { 31 | const selectedPreset = presets.find( 32 | (preset) => preset.name === parameters.preset 33 | ) 34 | createPalette({ 35 | data: { 36 | sourceColors: figma.currentPage.selection 37 | .filter( 38 | (element) => 39 | element.type !== 'GROUP' && 40 | element.type !== 'EMBED' && 41 | element.type !== 'CONNECTOR' && 42 | element.getPluginDataKeys().length === 0 && 43 | ((element as FrameNode).fills as readonly SolidPaint[]).filter( 44 | (fill: Paint) => fill.type === 'SOLID' 45 | ).length !== 0 46 | ) 47 | .map((element) => { 48 | return { 49 | name: element.name, 50 | rgb: ((element as FrameNode).fills as readonly SolidPaint[])[0] 51 | .color, 52 | source: 'CANVAS', 53 | id: '', 54 | isRemovable: false, 55 | } 56 | }), 57 | palette: { 58 | name: 59 | parameters.name === undefined 60 | ? '' 61 | : parameters.name.substring(0, 64), 62 | description: '', 63 | preset: presets.find((preset) => preset.name === parameters.preset), 64 | scale: doLightnessScale( 65 | selectedPreset?.scale ?? [1, 2], 66 | selectedPreset?.min ?? 0, 67 | selectedPreset?.max ?? 100, 68 | selectedPreset?.easing ?? 'LINEAR' 69 | ), 70 | shift: { 71 | chroma: 100, 72 | }, 73 | areSourceColorsLocked: false, 74 | colorSpace: parameters.space.toUpperCase().replace(' ', '_'), 75 | visionSimulationMode: 'NONE', 76 | view: parameters.view.toUpperCase().split(' ').join('_'), 77 | algorithmVersion: algorithmVersion, 78 | textColorsTheme: { 79 | lightColor: '#FFFFFF', 80 | darkColor: '#000000', 81 | }, 82 | } as PaletteConfiguration, 83 | }, 84 | }) 85 | figma.closePlugin() 86 | } 87 | }) 88 | 89 | // Migration 90 | if (figma.editorType !== 'dev') 91 | figma.on('run', async () => { 92 | await figma.currentPage.loadAsync() 93 | figma.currentPage 94 | .findAllWithCriteria({ 95 | pluginData: {}, 96 | }) 97 | .forEach((palette) => { 98 | setPaletteMigration(palette) 99 | }) 100 | }) 101 | figma.on('currentpagechange', async () => { 102 | await figma.currentPage.loadAsync() 103 | figma.currentPage 104 | .findAllWithCriteria({ 105 | pluginData: {}, 106 | }) 107 | .forEach((palette) => { 108 | setPaletteMigration(palette) 109 | }) 110 | }) 111 | -------------------------------------------------------------------------------- /src/stores/palette.ts: -------------------------------------------------------------------------------- 1 | import { deepMap } from 'nanostores' 2 | import { PaletteConfiguration } from 'src/types/configurations' 3 | import { algorithmVersion } from '../config' 4 | import { lang, locals } from '../content/locals' 5 | import { presets } from './presets' 6 | 7 | export const $palette = deepMap({ 8 | name: locals[lang].settings.global.name.default, 9 | description: '', 10 | min: 0, 11 | max: 100, 12 | preset: presets[0], 13 | scale: {}, 14 | shift: { 15 | chroma: 100, 16 | }, 17 | areSourceColorsLocked: false, 18 | colorSpace: 'LCH', 19 | visionSimulationMode: 'NONE', 20 | view: 'PALETTE_WITH_PROPERTIES', 21 | algorithmVersion: algorithmVersion, 22 | textColorsTheme: { 23 | lightColor: '#FFFFFF', 24 | darkColor: '#000000', 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /src/stores/preferences.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'nanostores' 2 | 3 | export const $isWCAGDisplayed = atom(true) 4 | export const $isAPCADisplayed = atom(true) 5 | export const $canPaletteDeepSync = atom(false) 6 | export const $canVariablesDeepSync = atom(false) 7 | export const $canStylesDeepSync = atom(false) 8 | export const $isVsCodeMessageDisplayed = atom(true) 9 | -------------------------------------------------------------------------------- /src/stores/presets.ts: -------------------------------------------------------------------------------- 1 | import { lang, locals } from '../content/locals' 2 | import { PresetConfiguration } from '../types/configurations' 3 | 4 | export const presets: Array = [ 5 | { 6 | name: 'Material Design, 50-900', 7 | scale: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900], 8 | min: 24, 9 | max: 96, 10 | easing: 'LINEAR', 11 | family: 'Google', 12 | id: 'MATERIAL', 13 | }, 14 | { 15 | name: 'Material 3, 0-100', 16 | scale: [100, 99, 95, 90, 80, 70, 60, 50, 40, 30, 20, 10, 0], 17 | min: 0, 18 | max: 100, 19 | easing: 'NONE', 20 | family: 'Google', 21 | id: 'MATERIAL_3', 22 | }, 23 | { 24 | name: 'Tailwind, 50-950', 25 | scale: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950], 26 | min: 16, 27 | max: 96, 28 | easing: 'LINEAR', 29 | id: 'TAILWIND', 30 | }, 31 | { 32 | name: 'Ant Design, 1-10', 33 | scale: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 34 | min: 24, 35 | max: 96, 36 | easing: 'LINEAR', 37 | id: 'ANT', 38 | }, 39 | { 40 | name: 'ADS Foundations, 100-1000', 41 | scale: [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000], 42 | min: 24, 43 | max: 96, 44 | easing: 'LINEAR', 45 | family: 'Atlassian', 46 | id: 'ADS', 47 | }, 48 | { 49 | name: 'ADS Foundations, Neutral 0-1100', 50 | scale: [0, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100], 51 | min: 8, 52 | max: 100, 53 | easing: 'LINEAR', 54 | family: 'Atlassian', 55 | id: 'ADS_NEUTRAL', 56 | }, 57 | { 58 | name: 'Carbon, 10-100 (IBM)', 59 | scale: [10, 20, 30, 40, 50, 60, 70, 80, 90, 100], 60 | min: 24, 61 | max: 96, 62 | easing: 'LINEAR', 63 | family: locals[lang].scale.presets.more, 64 | id: 'CARBON', 65 | }, 66 | { 67 | name: 'Base, 50-700 (Uber)', 68 | scale: [50, 100, 200, 300, 400, 500, 600, 700], 69 | min: 24, 70 | max: 96, 71 | easing: 'LINEAR', 72 | family: locals[lang].scale.presets.more, 73 | id: 'BASE', 74 | }, 75 | { 76 | name: 'Polaris, 1-16 (Shopify)', 77 | scale: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], 78 | min: 16, 79 | max: 100, 80 | easing: 'EASE_OUT', 81 | family: locals[lang].scale.presets.more, 82 | id: 'POLARIS', 83 | }, 84 | 85 | { 86 | name: 'Custom', 87 | scale: [1, 2], 88 | min: 10, 89 | max: 90, 90 | easing: 'LINEAR', 91 | id: 'CUSTOM', 92 | }, 93 | ] 94 | 95 | export const defaultPreset = presets[0] 96 | -------------------------------------------------------------------------------- /src/types/app.ts: -------------------------------------------------------------------------------- 1 | export type Service = 'CREATE' | 'EDIT' | 'TRANSFER' 2 | 3 | export type Context = 4 | | 'PALETTES' 5 | | 'PALETTES_PAGE' 6 | | 'PALETTES_SELF' 7 | | 'PALETTES_COMMUNITY' 8 | | 'SOURCE' 9 | | 'SOURCE_OVERVIEW' 10 | | 'SOURCE_EXPLORE' 11 | | 'SCALE' 12 | | 'COLORS' 13 | | 'THEMES' 14 | | 'EXPORT' 15 | | 'SETTINGS' 16 | | 'SETTINGS_PALETTE' 17 | | 'SETTINGS_PREFERENCES' 18 | 19 | export type FilterOptions = 20 | | 'ANY' 21 | | 'YELLOW' 22 | | 'ORANGE' 23 | | 'RED' 24 | | 'GREEN' 25 | | 'VIOLET' 26 | | 'BLUE' 27 | 28 | export type EditorType = 'figma' | 'figjam' | 'dev' | 'dev_vscode' 29 | 30 | export type PlanStatus = 'UNPAID' | 'PAID' | 'NOT_SUPPORTED' 31 | 32 | export type TrialStatus = 'UNUSED' | 'PENDING' | 'EXPIRED' 33 | 34 | export type FetchStatus = 35 | | 'UNLOADED' 36 | | 'LOADING' 37 | | 'LOADED' 38 | | 'ERROR' 39 | | 'EMPTY' 40 | | 'COMPLETE' 41 | | 'SIGN_IN_FIRST' 42 | | 'NO_RESULT' 43 | 44 | export type HighlightStatus = 45 | | 'NO_HIGHLIGHT' 46 | | 'DISPLAY_HIGHLIGHT_NOTIFICATION' 47 | | 'DISPLAY_HIGHLIGHT_DIALOG' 48 | 49 | export type OnboardingStatus = 'NO_ONBOARDING' | 'DISPLAY_ONBOARDING_DIALOG' 50 | 51 | export type Language = 'en-US' 52 | 53 | export interface windowSize { 54 | w: number 55 | h: number 56 | } 57 | 58 | export interface HighlightDigest { 59 | version: string 60 | status: HighlightStatus 61 | } 62 | 63 | export type PriorityContext = 64 | | 'EMPTY' 65 | | 'PUBLICATION' 66 | | 'HIGHLIGHT' 67 | | 'ONBOARDING' 68 | | 'TRY' 69 | | 'WELCOME_TO_PRO' 70 | | 'WELCOME_TO_TRIAL' 71 | | 'REPORT' 72 | | 'STORE' 73 | | 'ABOUT' 74 | 75 | export type ThirdParty = 'COOLORS' | 'REALTIME_COLORS' | 'COLOUR_LOVERS' 76 | 77 | export type NamingConvention = 'ONES' | 'TENS' | 'HUNDREDS' 78 | 79 | export type Easing = 80 | | 'NONE' 81 | | 'LINEAR' 82 | | 'EASE_IN' 83 | | 'EASE_OUT' 84 | | 'EASE_IN_OUT' 85 | | 'FAST_EASE_IN' 86 | | 'FAST_EASE_OUT' 87 | | 'FAST_EASE_IN_OUT' 88 | | 'SLOW_EASE_IN' 89 | | 'SLOW_EASE_OUT' 90 | | 'SLOW_EASE_IN_OUT' 91 | 92 | export interface ImportUrl { 93 | value: string 94 | state: 'DEFAULT' | 'ERROR' | undefined 95 | canBeSubmitted: boolean 96 | helper: 97 | | { 98 | type: 'INFO' | 'ERROR' 99 | message: string 100 | } 101 | | undefined 102 | } 103 | 104 | export interface ContextItem { 105 | label: string 106 | id: Context 107 | isUpdated: boolean 108 | isActive: boolean 109 | } 110 | -------------------------------------------------------------------------------- /src/types/configurations.ts: -------------------------------------------------------------------------------- 1 | import { HexModel, RgbModel } from '@a_ng_d/figmug-ui' 2 | 3 | import { Easing, ThirdParty } from './app' 4 | import { TextColorsThemeHexModel } from './models' 5 | 6 | export interface SourceColorConfiguration { 7 | name: string 8 | rgb: RgbModel 9 | source: 'CANVAS' | 'REMOTE' | ThirdParty 10 | id: string 11 | isRemovable: boolean 12 | description?: string 13 | hue?: { 14 | shift: number 15 | isLocked: boolean 16 | } 17 | chroma?: { 18 | shift: number 19 | isLocked: boolean 20 | } 21 | hueShifting?: number 22 | chromaShifting?: number 23 | } 24 | 25 | export interface PaletteConfiguration { 26 | [key: string]: string | number | boolean | object | undefined 27 | name: string 28 | description: string 29 | min?: number 30 | max?: number 31 | preset: PresetConfiguration 32 | scale: ScaleConfiguration 33 | shift: ShiftConfiguration 34 | areSourceColorsLocked: LockedSourceColorsConfiguration 35 | colorSpace: ColorSpaceConfiguration 36 | visionSimulationMode: VisionSimulationModeConfiguration 37 | view: ViewConfiguration 38 | algorithmVersion: AlgorithmVersionConfiguration 39 | textColorsTheme: TextColorsThemeHexModel 40 | } 41 | 42 | export interface ExtractOfPaletteConfiguration { 43 | id: string 44 | name: string 45 | preset: string 46 | colors: Array 47 | themes: Array 48 | screenshot: Uint8Array | null 49 | devStatus: string | null 50 | } 51 | 52 | export interface PresetConfiguration { 53 | name: string 54 | scale: Array 55 | min: number 56 | max: number 57 | easing: Easing 58 | family?: string 59 | id: string 60 | } 61 | 62 | export type ScaleConfiguration = Record 63 | 64 | export interface ShiftConfiguration { 65 | chroma: number 66 | } 67 | 68 | export type LockedSourceColorsConfiguration = boolean 69 | 70 | export interface ColorConfiguration { 71 | name: string 72 | description: string 73 | rgb: RgbModel 74 | hue: { 75 | shift: number 76 | isLocked: boolean 77 | } 78 | chroma: { 79 | shift: number 80 | isLocked: boolean 81 | } 82 | hueShifting?: number 83 | chromaShifting?: number 84 | id: string 85 | } 86 | 87 | export interface ThemeConfiguration { 88 | name: string 89 | description: string 90 | scale: ScaleConfiguration 91 | paletteBackground: HexModel 92 | isEnabled: boolean 93 | id: string 94 | type: 'default theme' | 'custom theme' 95 | } 96 | 97 | export interface ExportConfiguration { 98 | format: 'JSON' | 'CSS' | 'TAILWIND' | 'SWIFT' | 'KT' | 'XML' | 'CSV' 99 | context: 100 | | 'TOKENS_GLOBAL' 101 | | 'TOKENS_AMZN_STYLE_DICTIONARY' 102 | | 'TOKENS_TOKENS_STUDIO' 103 | | 'CSS' 104 | | 'TAILWIND' 105 | | 'APPLE_SWIFTUI' 106 | | 'APPLE_UIKIT' 107 | | 'ANDROID_COMPOSE' 108 | | 'ANDROID_XML' 109 | | 'CSV' 110 | label: string 111 | colorSpace: ColorSpaceConfiguration 112 | mimeType: 113 | | 'application/json' 114 | | 'text/css' 115 | | 'text/javascript' 116 | | 'text/swift' 117 | | 'text/x-kotlin' 118 | | 'text/xml' 119 | | 'text/csv' 120 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 121 | data: any 122 | } 123 | 124 | export type ColorSpaceConfiguration = 125 | | 'LCH' 126 | | 'OKLCH' 127 | | 'LAB' 128 | | 'OKLAB' 129 | | 'HSL' 130 | | 'HSLUV' 131 | | 'RGB' 132 | | 'HEX' 133 | | 'P3' 134 | 135 | export type VisionSimulationModeConfiguration = 136 | | 'NONE' 137 | | 'PROTANOMALY' 138 | | 'PROTANOPIA' 139 | | 'DEUTERANOMALY' 140 | | 'DEUTERANOPIA' 141 | | 'TRITANOMALY' 142 | | 'TRITANOPIA' 143 | | 'ACHROMATOMALY' 144 | | 'ACHROMATOPSIA' 145 | 146 | export type ViewConfiguration = 147 | | 'PALETTE_WITH_PROPERTIES' 148 | | 'PALETTE' 149 | | 'SHEET' 150 | | 'SHEET_SAFE_MODE' 151 | 152 | export type AlgorithmVersionConfiguration = 'v1' | 'v2' | 'v3' 153 | 154 | export interface DatesConfiguration { 155 | createdAt: Date | string 156 | updatedAt: Date | string 157 | publishedAt: Date | string 158 | } 159 | 160 | export interface PublicationConfiguration { 161 | isPublished: boolean 162 | isShared: boolean 163 | } 164 | 165 | export interface CreatorConfiguration { 166 | creatorFullName: string 167 | creatorAvatar: string 168 | creatorId: string 169 | } 170 | 171 | export interface UserConfiguration { 172 | id: string 173 | fullName: string 174 | avatar: string 175 | } 176 | 177 | export interface MetaConfiguration { 178 | id: string 179 | dates: DatesConfiguration 180 | publicationStatus: PublicationConfiguration 181 | creatorIdentity: CreatorConfiguration 182 | } 183 | -------------------------------------------------------------------------------- /src/types/data.ts: -------------------------------------------------------------------------------- 1 | import { HexModel } from '@a_ng_d/figmug-ui' 2 | 3 | import { 4 | ColorConfiguration, 5 | PresetConfiguration, 6 | ThemeConfiguration, 7 | } from './configurations' 8 | 9 | export interface PaletteData { 10 | name: string 11 | description: string 12 | themes: Array 13 | collectionId: string 14 | version: string 15 | type: 'palette' 16 | } 17 | 18 | export interface PaletteDataThemeItem { 19 | name: string 20 | description: string 21 | colors: Array 22 | modeId: string 23 | id: string 24 | type: 'default theme' | 'custom theme' 25 | } 26 | 27 | export interface PaletteDataColorItem { 28 | name: string 29 | description: string 30 | shades: Array 31 | id: string 32 | type: 'color' 33 | } 34 | 35 | export interface PaletteDataShadeItem { 36 | name: string 37 | description: string 38 | hex: HexModel 39 | rgb: [number, number, number] 40 | gl: [number, number, number, number] 41 | lch: [number, number, number] 42 | oklch: [number, number, number] 43 | lab: [number, number, number] 44 | oklab: [number, number, number] 45 | hsl: [number, number, number] 46 | hsluv: [number, number, number] 47 | variableId: string 48 | styleId: string 49 | isClosestToRef?: boolean 50 | isSourceColorLocked?: boolean 51 | type: 'source color' | 'color shade' 52 | } 53 | 54 | export interface ColourLovers { 55 | apiUrl: string 56 | badgeUrl: string 57 | colors: Array 58 | dateCreated: Date | string 59 | description: string 60 | id: number 61 | imageUrl: string 62 | numComments: number 63 | numHearts: number 64 | numViews: number 65 | numVotes: number 66 | rank: number 67 | title: string 68 | url: string 69 | userName: string 70 | } 71 | 72 | export interface ExternalPalettes { 73 | palette_id: string 74 | screenshot: string 75 | name: string 76 | preset: PresetConfiguration 77 | colors: Array 78 | themes: Array 79 | creator_avatar: string 80 | creator_full_name: string 81 | creator_id: string 82 | is_shared: boolean 83 | } 84 | -------------------------------------------------------------------------------- /src/types/events.ts: -------------------------------------------------------------------------------- 1 | import { Easing, EditorType, NamingConvention } from './app' 2 | import { ColorSpaceConfiguration } from './configurations' 3 | 4 | export interface EditorEvent { 5 | editor: EditorType 6 | } 7 | 8 | export interface TrialEvent { 9 | date: number 10 | trialTime: number 11 | } 12 | 13 | export interface PublicationEvent { 14 | feature: 15 | | 'PUBLISH_PALETTE' 16 | | 'UNPUBLISH_PALETTE' 17 | | 'PUSH_PALETTE' 18 | | 'PULL_PALETTE' 19 | | 'REUSE_PALETTE' 20 | | 'SYNC_PALETTE' 21 | | 'REVERT_PALETTE' 22 | | 'DETACH_PALETTE' 23 | | 'ADD_PALETTE' 24 | | 'SHARE_PALETTE' 25 | } 26 | 27 | export interface ImportEvent { 28 | feature: 'IMPORT_COOLORS' | 'IMPORT_REALTIME_COLORS' | 'IMPORT_COLOUR_LOVERS' 29 | } 30 | 31 | export interface ScaleEvent { 32 | feature: 33 | | 'SWITCH_MATERIAL' 34 | | 'SWITCH_MATERIAL_3' 35 | | 'SWITCH_TAILWIND' 36 | | 'SWITCH_ANT' 37 | | 'SWITCH_ADS' 38 | | 'SWITCH_ADS_NEUTRAL' 39 | | 'SWITCH_CARBON' 40 | | 'SWITCH_BASE' 41 | | 'SWITCH_POLARIS' 42 | | 'SWITCH_CUSTOM' 43 | | 'OPEN_KEYBOARD_SHORTCUTS' 44 | | NamingConvention 45 | | Easing 46 | } 47 | 48 | export interface PreviewEvent { 49 | feature: 50 | | 'LOCK_SOURCE_COLORS' 51 | | 'UPDATE_COLOR_SPACE' 52 | | 'UPDATE_VISION_SIMULATION_MODE' 53 | } 54 | 55 | export interface SourceColorEvent { 56 | feature: 57 | | 'RENAME_COLOR' 58 | | 'REMOVE_COLOR' 59 | | 'ADD_COLOR' 60 | | 'UPDATE_HEX' 61 | | 'UPDATE_LCH' 62 | | 'SHIFT_HUE' 63 | | 'SHIFT_CHROMA' 64 | | 'RESET_HUE' 65 | | 'RESET_CHROMA' 66 | | 'DESCRIBE_COLOR' 67 | | 'REORDER_COLOR' 68 | } 69 | 70 | export interface ColorThemeEvent { 71 | feature: 72 | | 'RENAME_THEME' 73 | | 'REMOVE_THEME' 74 | | 'ADD_THEME' 75 | | 'ADD_THEME_FROM_DROPDOWN' 76 | | 'UPDATE_BACKGROUND' 77 | | 'DESCRIBE_THEME' 78 | | 'REORDER_THEME' 79 | } 80 | 81 | export interface ExportEvent { 82 | feature: 83 | | 'TOKENS_GLOBAL' 84 | | 'TOKENS_AMZN_STYLE_DICTIONARY' 85 | | 'TOKENS_TOKENS_STUDIO' 86 | | 'CSS' 87 | | 'TAILWIND' 88 | | 'APPLE_SWIFTUI' 89 | | 'APPLE_UIKIT' 90 | | 'ANDROID_COMPOSE' 91 | | 'ANDROID_XML' 92 | | 'CSV' 93 | colorSpace?: ColorSpaceConfiguration 94 | } 95 | 96 | export interface SettingEvent { 97 | feature: 98 | | 'RENAME_PALETTE' 99 | | 'DESCRIBE_PALETTE' 100 | | 'UPDATE_VIEW' 101 | | 'UPDATE_COLOR_SPACE' 102 | | 'UPDATE_VISION_SIMULATION_MODE' 103 | | 'UPDATE_ALGORITHM' 104 | | 'UPDATE_TEXT_COLORS_THEME' 105 | } 106 | 107 | export interface ActionEvent { 108 | feature: 'CREATE_PALETTE' | 'SYNC_STYLES' | 'SYNC_VARIABLES' 109 | colors?: number 110 | stops?: number 111 | } 112 | -------------------------------------------------------------------------------- /src/types/messages.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AlgorithmVersionConfiguration, 3 | ColorConfiguration, 4 | ColorSpaceConfiguration, 5 | PaletteConfiguration, 6 | ThemeConfiguration, 7 | VisionSimulationModeConfiguration, 8 | } from './configurations' 9 | import { TextColorsThemeHexModel } from './models' 10 | 11 | export interface ScaleMessage { 12 | type: 'UPDATE_SCALE' 13 | data: PaletteConfiguration 14 | isEditedInRealTime: boolean 15 | feature?: string 16 | } 17 | 18 | export interface ColorsMessage { 19 | type: 'UPDATE_COLORS' 20 | data: Array 21 | isEditedInRealTime: boolean 22 | } 23 | 24 | export interface ThemesMessage { 25 | type: 'UPDATE_THEMES' 26 | data: Array 27 | isEditedInRealTime: boolean 28 | } 29 | 30 | export interface ViewMessage { 31 | type: 'UPDATE_VIEW' 32 | data: PaletteConfiguration 33 | isEditedInRealTime: boolean 34 | } 35 | 36 | export interface SettingsMessage { 37 | type: 'UPDATE_SETTINGS' 38 | data: { 39 | name: string 40 | description: string 41 | colorSpace: ColorSpaceConfiguration 42 | visionSimulationMode: VisionSimulationModeConfiguration 43 | algorithmVersion: AlgorithmVersionConfiguration 44 | textColorsTheme: TextColorsThemeHexModel 45 | } 46 | isEditedInRealTime: boolean 47 | } 48 | 49 | export interface CollectionMessage { 50 | type: 'UPDATE_COLLECTION' 51 | data: { 52 | id: string 53 | } 54 | isEditedInRealTime: boolean 55 | } 56 | -------------------------------------------------------------------------------- /src/types/models.ts: -------------------------------------------------------------------------------- 1 | import { HexModel } from '@a_ng_d/figmug-ui' 2 | 3 | export interface RgbModel { 4 | r: number 5 | g: number 6 | b: number 7 | } 8 | 9 | export interface TextColorsThemeHexModel { 10 | lightColor: HexModel 11 | darkColor: HexModel 12 | } 13 | 14 | export interface TextColorsThemeGLModel { 15 | lightColor: Array 16 | darkColor: Array 17 | } 18 | 19 | export interface ActionsList { 20 | [action: string]: () => void 21 | } 22 | 23 | export interface DispatchProcess { 24 | time: number 25 | on: { 26 | active: boolean 27 | blocked: boolean 28 | interval: string 29 | send: () => void 30 | stop: () => void 31 | status: boolean 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/types/nodes.ts: -------------------------------------------------------------------------------- 1 | import { Service } from './app' 2 | import { 3 | AlgorithmVersionConfiguration, 4 | ColorConfiguration, 5 | ColorSpaceConfiguration, 6 | LockedSourceColorsConfiguration, 7 | PresetConfiguration, 8 | ScaleConfiguration, 9 | ThemeConfiguration, 10 | ViewConfiguration, 11 | VisionSimulationModeConfiguration, 12 | } from './configurations' 13 | import { TextColorsThemeHexModel } from './models' 14 | 15 | export interface PaletteNode { 16 | name: string 17 | description: string 18 | preset: PresetConfiguration 19 | scale: ScaleConfiguration 20 | areSourceColorsLocked: LockedSourceColorsConfiguration 21 | colors: Array 22 | colorSpace: ColorSpaceConfiguration 23 | visionSimulationMode: VisionSimulationModeConfiguration 24 | themes: Array 25 | view: ViewConfiguration 26 | algorithmVersion: AlgorithmVersionConfiguration 27 | textColorsTheme: TextColorsThemeHexModel 28 | creatorFullName?: string 29 | creatorAvatar?: string 30 | creatorAvatarImg?: Image | null 31 | service?: Service 32 | } 33 | -------------------------------------------------------------------------------- /src/types/user.ts: -------------------------------------------------------------------------------- 1 | export type ConnectionStatus = 'CONNECTED' | 'UNCONNECTED' 2 | 3 | export interface UserSession { 4 | connectionStatus: ConnectionStatus 5 | userFullName: string 6 | userAvatar: string 7 | userId: string | undefined 8 | accessToken: string | undefined 9 | refreshToken: string | undefined 10 | } 11 | 12 | export interface Identity { 13 | connectionStatus: ConnectionStatus 14 | userId: string | undefined 15 | creatorId: string 16 | } 17 | -------------------------------------------------------------------------------- /src/ui/app.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | -------------------------------------------------------------------------------- /src/ui/components/Feature.tsx: -------------------------------------------------------------------------------- 1 | import { PureComponent } from 'preact/compat' 2 | import React from 'react' 3 | 4 | interface FeatureProps { 5 | isActive: boolean 6 | children: React.ReactNode 7 | } 8 | 9 | export default class Feature extends PureComponent { 10 | static defaultProps = { 11 | isActive: false, 12 | isPro: false, 13 | } 14 | 15 | render() { 16 | return <>{this.props.isActive && this.props.children} 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ui/contexts/ContrastSettings.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FormItem, 3 | Input, 4 | Section, 5 | SectionTitle, 6 | SemanticMessage, 7 | SimpleItem, 8 | } from '@a_ng_d/figmug-ui' 9 | import { FeatureStatus } from '@a_ng_d/figmug-utils' 10 | import { PureComponent } from 'preact/compat' 11 | import React from 'react' 12 | 13 | import features from '../../config' 14 | import { locals } from '../../content/locals' 15 | import { Language, PlanStatus } from '../../types/app' 16 | import { TextColorsThemeHexModel } from '../../types/models' 17 | import Feature from '../components/Feature' 18 | 19 | interface ContrastSettingsProps { 20 | textColorsTheme: TextColorsThemeHexModel 21 | isLast?: boolean 22 | planStatus: PlanStatus 23 | lang: Language 24 | onChangeSettings: ( 25 | e: 26 | | React.ChangeEvent 27 | | React.KeyboardEvent 28 | ) => void 29 | } 30 | 31 | export default class ContrastSettings extends PureComponent { 32 | static features = (planStatus: PlanStatus) => ({ 33 | SETTINGS_TEXT_COLORS_THEME: new FeatureStatus({ 34 | features: features, 35 | featureName: 'SETTINGS_TEXT_COLORS_THEME', 36 | planStatus: planStatus, 37 | }), 38 | }) 39 | 40 | static defaultProps = { 41 | isLast: false, 42 | } 43 | 44 | // Templates 45 | LightTextColorsTheme = () => { 46 | return ( 47 | 52 | 61 | 76 | 77 | 78 | ) 79 | } 80 | 81 | DarkTextColorsTheme = () => { 82 | return ( 83 | 88 | 97 | 112 | 113 | 114 | ) 115 | } 116 | 117 | // Render 118 | render() { 119 | return ( 120 |
127 | } 128 | isListItem={false} 129 | alignment="CENTER" 130 | /> 131 | } 132 | body={[ 133 | { 134 | node: , 135 | }, 136 | { 137 | node: , 138 | }, 139 | { 140 | node: ( 141 | 148 | ), 149 | }, 150 | ]} 151 | border={!this.props.isLast ? ['BOTTOM'] : undefined} 152 | /> 153 | ) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/ui/contexts/InternalPalettes.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ActionsItem, 3 | Button, 4 | List, 5 | SemanticMessage, 6 | texts, 7 | } from '@a_ng_d/figmug-ui' 8 | import { PureComponent } from 'preact/compat' 9 | import React from 'react' 10 | 11 | import { doClassnames } from '@a_ng_d/figmug-utils' 12 | import { locals } from '../../content/locals' 13 | import { EditorType, Language } from '../../types/app' 14 | import { ExtractOfPaletteConfiguration } from '../../types/configurations' 15 | import { ActionsList } from '../../types/models' 16 | import getPaletteMeta from '../../utils/setPaletteMeta' 17 | 18 | interface InternalPalettesProps { 19 | editorType: EditorType 20 | lang: Language 21 | } 22 | 23 | interface InternalPalettesStates { 24 | paletteListsStatus: 'LOADING' | 'LOADED' | 'EMPTY' 25 | paletteLists: Array 26 | } 27 | 28 | export default class InternalPalettes extends PureComponent< 29 | InternalPalettesProps, 30 | InternalPalettesStates 31 | > { 32 | constructor(props: InternalPalettesProps) { 33 | super(props) 34 | this.state = { 35 | paletteListsStatus: 'LOADING', 36 | paletteLists: [], 37 | } 38 | } 39 | 40 | // Lifecycle 41 | componentDidMount = () => { 42 | parent.postMessage({ pluginMessage: { type: 'GET_PALETTES' } }, '*') 43 | 44 | window.addEventListener('message', this.handleMessage) 45 | } 46 | 47 | componentWillUnmount = () => { 48 | window.removeEventListener('message', this.handleMessage) 49 | } 50 | 51 | // Handlers 52 | handleMessage = (e: MessageEvent) => { 53 | const actions: ActionsList = { 54 | EXPOSE_PALETTES: () => 55 | this.setState({ 56 | paletteListsStatus: 57 | e.data.pluginMessage.data.length > 0 ? 'LOADED' : 'EMPTY', 58 | paletteLists: e.data.pluginMessage.data, 59 | }), 60 | LOAD_PALETTES: () => this.setState({ paletteListsStatus: 'LOADING' }), 61 | DEFAULT: () => null, 62 | } 63 | 64 | return actions[e.data.pluginMessage?.type ?? 'DEFAULT']?.() 65 | } 66 | 67 | // Direct Actions 68 | getImageSrc = (screenshot: Uint8Array | null) => { 69 | if (screenshot !== null) { 70 | const blob = new Blob([screenshot], { 71 | type: 'image/png', 72 | }) 73 | return URL.createObjectURL(blob) 74 | } else return '' 75 | } 76 | 77 | onSelectPalette = (id: string) => { 78 | parent.postMessage( 79 | { 80 | pluginMessage: { 81 | type: 'JUMP_TO_PALETTE', 82 | id: id, 83 | }, 84 | }, 85 | '*' 86 | ) 87 | } 88 | 89 | // Templates 90 | InternalPalettesList = () => { 91 | return ( 92 | 96 | {this.state.paletteListsStatus === 'LOADED' && ( 97 | <> 98 | {this.props.editorType === 'dev' && ( 99 |
107 | {locals[this.props.lang].palettes.devMode.title} 108 |
109 | )} 110 | {this.state.paletteLists.map((palette, index) => ( 111 | this.onSelectPalette(palette.id)} 140 | /> 141 | } 142 | /> 143 | ))} 144 | 145 | )} 146 | {this.state.paletteListsStatus === 'EMPTY' && ( 147 | 151 | )} 152 |
153 | ) 154 | } 155 | 156 | // Render 157 | render() { 158 | return 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/ui/contexts/Palettes.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Bar, 3 | ConsentConfiguration, 4 | HexModel, 5 | Layout, 6 | Tabs, 7 | } from '@a_ng_d/figmug-ui' 8 | import { PureComponent } from 'preact/compat' 9 | import React from 'react' 10 | 11 | import { 12 | ExtractOfPaletteConfiguration, 13 | UserConfiguration, 14 | } from 'src/types/configurations' 15 | import { 16 | Context, 17 | ContextItem, 18 | EditorType, 19 | FetchStatus, 20 | Language, 21 | PlanStatus, 22 | } from '../../types/app' 23 | import { ExternalPalettes } from '../../types/data' 24 | import { UserSession } from '../../types/user' 25 | import { setContexts } from '../../utils/setContexts' 26 | import CommunityPalettes from './CommunityPalettes' 27 | import InternalPalettes from './InternalPalettes' 28 | import SelfPalettes from './SelfPalettes' 29 | 30 | interface PalettesProps { 31 | userIdentity: UserConfiguration 32 | userSession: UserSession 33 | userConsent: Array 34 | palettesList: Array 35 | editorType: EditorType 36 | planStatus: PlanStatus 37 | lang: Language 38 | onConfigureExternalSourceColors: ( 39 | name: string, 40 | colors: Array 41 | ) => void 42 | } 43 | 44 | interface PalettesStates { 45 | context: Context | '' 46 | selfPalettesListStatus: FetchStatus 47 | communityPalettesListStatus: FetchStatus 48 | selfCurrentPage: number 49 | communityCurrentPage: number 50 | seftPalettesSearchQuery: string 51 | communityPalettesSearchQuery: string 52 | selfPalettesList: Array 53 | communityPalettesList: Array 54 | } 55 | 56 | export default class Palettes extends PureComponent< 57 | PalettesProps, 58 | PalettesStates 59 | > { 60 | private contexts: Array 61 | 62 | constructor(props: PalettesProps) { 63 | super(props) 64 | this.contexts = setContexts( 65 | ['PALETTES_PAGE', 'PALETTES_SELF', 'PALETTES_COMMUNITY'], 66 | props.planStatus 67 | ) 68 | this.state = { 69 | context: this.contexts[0] !== undefined ? this.contexts[0].id : '', 70 | selfPalettesListStatus: 'UNLOADED', 71 | communityPalettesListStatus: 'UNLOADED', 72 | selfCurrentPage: 1, 73 | communityCurrentPage: 1, 74 | selfPalettesList: [], 75 | communityPalettesList: [], 76 | seftPalettesSearchQuery: '', 77 | communityPalettesSearchQuery: '', 78 | } 79 | } 80 | 81 | // Lifecycle 82 | componentDidUpdate = (prevProps: Readonly): void => { 83 | if ( 84 | prevProps.userSession.connectionStatus !== 85 | this.props.userSession.connectionStatus && 86 | this.state.selfPalettesList.length === 0 87 | ) 88 | this.setState({ 89 | selfPalettesListStatus: 'LOADING', 90 | }) 91 | } 92 | 93 | // Handlers 94 | navHandler = (e: Event) => 95 | this.setState({ 96 | context: (e.target as HTMLElement).dataset.feature as Context, 97 | }) 98 | 99 | // Render 100 | render() { 101 | let fragment: React.ReactElement =
102 | 103 | switch (this.state.context) { 104 | case 'PALETTES_PAGE': { 105 | fragment = 106 | break 107 | } 108 | case 'PALETTES_SELF': { 109 | fragment = ( 110 | 122 | this.setState({ selfPalettesListStatus: status }) 123 | } 124 | onChangeCurrentPage={(page) => 125 | this.setState({ selfCurrentPage: page }) 126 | } 127 | onChangeSearchQuery={(query) => 128 | this.setState({ seftPalettesSearchQuery: query }) 129 | } 130 | onLoadPalettesList={(palettesList) => 131 | this.setState({ selfPalettesList: palettesList }) 132 | } 133 | /> 134 | ) 135 | break 136 | } 137 | case 'PALETTES_COMMUNITY': { 138 | fragment = ( 139 | 147 | this.setState({ communityPalettesListStatus: status }) 148 | } 149 | onChangeCurrentPage={(page) => 150 | this.setState({ communityCurrentPage: page }) 151 | } 152 | onChangeSearchQuery={(query) => 153 | this.setState({ communityPalettesSearchQuery: query }) 154 | } 155 | onLoadPalettesList={(palettesList) => 156 | this.setState({ communityPalettesList: palettesList }) 157 | } 158 | /> 159 | ) 160 | break 161 | } 162 | } 163 | return ( 164 | <> 165 | 172 | } 173 | border={['BOTTOM']} 174 | /> 175 | 185 | 186 | ) 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/ui/contexts/Source.tsx: -------------------------------------------------------------------------------- 1 | import { Bar, ConsentConfiguration, Tabs } from '@a_ng_d/figmug-ui' 2 | import { FeatureStatus } from '@a_ng_d/figmug-utils' 3 | import { PureComponent } from 'preact/compat' 4 | import React from 'react' 5 | 6 | import features from '../../config' 7 | import { 8 | Context, 9 | ContextItem, 10 | EditorType, 11 | FilterOptions, 12 | Language, 13 | PlanStatus, 14 | PriorityContext, 15 | ThirdParty, 16 | } from '../../types/app' 17 | import { 18 | SourceColorConfiguration, 19 | UserConfiguration, 20 | } from '../../types/configurations' 21 | import { ColourLovers } from '../../types/data' 22 | import { setContexts } from '../../utils/setContexts' 23 | import Explore from './Explore' 24 | import Overview from './Overview' 25 | 26 | interface SourceProps { 27 | sourceColors: Array 28 | userIdentity: UserConfiguration 29 | userConsent: Array 30 | planStatus: PlanStatus 31 | editorType?: EditorType 32 | lang: Language 33 | onChangeColorsFromImport: ( 34 | onChangeColorsFromImport: Array, 35 | source: ThirdParty 36 | ) => void 37 | onGetProPlan: (context: { priorityContainerContext: PriorityContext }) => void 38 | } 39 | 40 | interface SourceStates { 41 | context: Context | '' 42 | colourLoversPaletteList: Array 43 | activeFilters: Array 44 | } 45 | 46 | export default class Source extends PureComponent { 47 | private contexts: Array 48 | 49 | static features = (planStatus: PlanStatus) => ({ 50 | PREVIEW: new FeatureStatus({ 51 | features: features, 52 | featureName: 'PREVIEW', 53 | planStatus: planStatus, 54 | }), 55 | }) 56 | 57 | constructor(props: SourceProps) { 58 | super(props) 59 | this.contexts = setContexts( 60 | ['SOURCE_OVERVIEW', 'SOURCE_EXPLORE'], 61 | props.planStatus 62 | ) 63 | this.state = { 64 | context: this.contexts[0] !== undefined ? this.contexts[0].id : '', 65 | colourLoversPaletteList: [], 66 | activeFilters: ['ANY'], 67 | } 68 | } 69 | 70 | // Handlers 71 | navHandler = (e: Event) => 72 | this.setState({ 73 | context: (e.target as HTMLElement).dataset.feature as Context, 74 | }) 75 | 76 | // Render 77 | render() { 78 | let fragment 79 | 80 | switch (this.state.context) { 81 | case 'SOURCE_OVERVIEW': { 82 | fragment = ( 83 | 86 | this.setState({ context: 'SOURCE_EXPLORE' }) 87 | } 88 | /> 89 | ) 90 | break 91 | } 92 | case 'SOURCE_EXPLORE': { 93 | fragment = ( 94 | 99 | this.setState({ context: 'SOURCE_OVERVIEW' }) 100 | } 101 | onLoadColourLoversPalettesList={(e, shouldBeEmpty) => 102 | this.setState({ 103 | colourLoversPaletteList: !shouldBeEmpty 104 | ? this.state.colourLoversPaletteList.concat(e) 105 | : [], 106 | }) 107 | } 108 | onChangeFilters={(e) => this.setState({ activeFilters: e })} 109 | /> 110 | ) 111 | break 112 | } 113 | } 114 | 115 | return ( 116 | <> 117 | 124 | } 125 | border={['BOTTOM']} 126 | /> 127 | {fragment} 128 | 129 | ) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/ui/handlers/addStop.ts: -------------------------------------------------------------------------------- 1 | import { doMap } from '@a_ng_d/figmug-utils' 2 | 3 | import { $palette } from '../../stores/palette' 4 | import { ScaleConfiguration } from '../../types/configurations' 5 | 6 | const addStop = ( 7 | e: MouseEvent, 8 | scale: ScaleConfiguration, 9 | presetName: string, 10 | presetMin: number, 11 | presetMax: number 12 | ) => { 13 | const rangeWidth: number = (e.currentTarget as HTMLElement).offsetWidth, 14 | sliderPadding: number = parseFloat( 15 | window 16 | .getComputedStyle( 17 | (e.currentTarget as HTMLElement).parentNode as Element, 18 | null 19 | ) 20 | .getPropertyValue('padding-left') 21 | ), 22 | offset: number = doMap(e.clientX - sliderPadding, 0, rangeWidth, 0, 100), 23 | newLightnessScale: { [key: string]: number } = {}, 24 | factor = Math.min( 25 | ...Object.keys(scale).map((stop) => parseFloat(stop.split('-')[1])) 26 | ), 27 | palette = $palette 28 | 29 | let newScale: Array = [] 30 | 31 | newScale = Object.values(scale) 32 | newScale.length < 25 && newScale.push(parseFloat(offset.toFixed(1))) 33 | newScale.sort((a, b) => b - a) 34 | newScale.forEach( 35 | (scale, index) => 36 | (newLightnessScale[`lightness-${(index + 1) * factor}`] = scale) 37 | ) 38 | 39 | palette.setKey('scale', newLightnessScale) 40 | palette.setKey('preset', { 41 | name: presetName, 42 | scale: newScale.map((scale, index) => (index + 1) * factor), 43 | min: presetMin, 44 | max: presetMax, 45 | easing: 'NONE', 46 | id: 'CUSTOM', 47 | }) 48 | } 49 | 50 | export default addStop 51 | -------------------------------------------------------------------------------- /src/ui/handlers/deleteStop.ts: -------------------------------------------------------------------------------- 1 | import { $palette } from '../../stores/palette' 2 | import { ScaleConfiguration } from '../../types/configurations' 3 | 4 | const deleteStop = ( 5 | scale: ScaleConfiguration, 6 | selectedKnob: HTMLElement, 7 | presetName: string, 8 | presetMin: number, 9 | presetMax: number 10 | ) => { 11 | const newScale: Array = [], 12 | newLightnessScale: { [key: string]: number } = {}, 13 | factor = Math.min( 14 | ...Object.keys(scale).map((stop) => parseFloat(stop.split('-')[1])) 15 | ), 16 | palette = $palette 17 | 18 | Object.values(scale).forEach((scale) => { 19 | scale === parseFloat(selectedKnob.style.left) ? null : newScale.push(scale) 20 | }) 21 | newScale.forEach( 22 | (scale, index) => 23 | (newLightnessScale[`lightness-${(index + 1) * factor}`] = scale) 24 | ) 25 | 26 | palette.setKey('scale', newLightnessScale) 27 | palette.setKey('preset', { 28 | name: presetName, 29 | scale: Object.keys(palette.get().scale).map((key) => 30 | parseFloat(key.replace('lightness-', '')) 31 | ), 32 | min: presetMin, 33 | max: presetMax, 34 | easing: 'NONE', 35 | id: 'CUSTOM', 36 | }) 37 | } 38 | 39 | export default deleteStop 40 | -------------------------------------------------------------------------------- /src/ui/handlers/shiftLeftStop.ts: -------------------------------------------------------------------------------- 1 | import { $palette } from '../../stores/palette' 2 | import { ScaleConfiguration } from '../../types/configurations' 3 | 4 | const shiftLeftStop = ( 5 | scale: ScaleConfiguration, 6 | selectedKnob: HTMLElement, 7 | meta: boolean, 8 | ctrl: boolean, 9 | gap: number 10 | ) => { 11 | const stopsList: Array = [] 12 | const shiftValue = meta || ctrl ? 0.1 : 1 13 | const palette = $palette 14 | 15 | Object.keys(scale).forEach((stop) => { 16 | stopsList.push(stop) 17 | }) 18 | 19 | const selectedKnobIndex = stopsList.indexOf( 20 | selectedKnob.dataset.id as string 21 | ), 22 | newLightnessScale = scale, 23 | currentStopValue: number = newLightnessScale[stopsList[selectedKnobIndex]], 24 | nextStopValue: number = newLightnessScale[stopsList[selectedKnobIndex + 1]] 25 | 26 | if (currentStopValue + gap - shiftValue <= nextStopValue) nextStopValue + gap 27 | else if (currentStopValue <= 1 && (!meta || ctrl)) 28 | newLightnessScale[stopsList[selectedKnobIndex]] = 0 29 | else if (currentStopValue === 0 && (meta || ctrl)) 30 | newLightnessScale[stopsList[selectedKnobIndex]] = 0 31 | else 32 | newLightnessScale[stopsList[selectedKnobIndex]] = 33 | newLightnessScale[stopsList[selectedKnobIndex]] - shiftValue 34 | 35 | palette.setKey('scale', newLightnessScale) 36 | } 37 | 38 | export default shiftLeftStop 39 | -------------------------------------------------------------------------------- /src/ui/handlers/shiftRightStop.ts: -------------------------------------------------------------------------------- 1 | import { $palette } from '../../stores/palette' 2 | import { ScaleConfiguration } from '../../types/configurations' 3 | 4 | const shiftRightStop = ( 5 | scale: ScaleConfiguration, 6 | selectedKnob: HTMLElement, 7 | meta: boolean, 8 | ctrl: boolean, 9 | gap: number 10 | ) => { 11 | const stopsList: Array = [] 12 | const shiftValue = meta || ctrl ? 0.1 : 1 13 | const palette = $palette 14 | 15 | Object.keys(scale).forEach((stop) => { 16 | stopsList.push(stop) 17 | }) 18 | 19 | const selectedKnobIndex = stopsList.indexOf( 20 | selectedKnob.dataset.id as string 21 | ), 22 | newLightnessScale = scale, 23 | currentStopValue: number = newLightnessScale[stopsList[selectedKnobIndex]], 24 | nextStopValue: number = newLightnessScale[stopsList[selectedKnobIndex - 1]] 25 | 26 | if (currentStopValue - gap + shiftValue >= nextStopValue) nextStopValue - gap 27 | else if (currentStopValue >= 99 && (!meta || ctrl)) 28 | newLightnessScale[stopsList[selectedKnobIndex]] = 100 29 | else if (currentStopValue === 100 && (meta || ctrl)) 30 | newLightnessScale[stopsList[selectedKnobIndex]] = 100 31 | else 32 | newLightnessScale[stopsList[selectedKnobIndex]] = 33 | newLightnessScale[stopsList[selectedKnobIndex]] + shiftValue 34 | 35 | palette.setKey('scale', newLightnessScale) 36 | } 37 | 38 | export default shiftRightStop 39 | -------------------------------------------------------------------------------- /src/ui/index.tsx: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/react' 2 | import mixpanel from 'mixpanel-figma' 3 | import React from 'react' 4 | import { createRoot } from 'react-dom/client' 5 | import App from './App' 6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | import { theme } from '../config' 8 | 9 | const container = document.getElementById('app'), 10 | root = createRoot(container) 11 | 12 | document.documentElement.setAttribute('data-theme', theme) 13 | 14 | mixpanel.init(process.env.REACT_APP_MIXPANEL_TOKEN ?? '', { 15 | debug: process.env.NODE_ENV === 'development', 16 | disable_persistence: true, 17 | disable_cookie: true, 18 | opt_out_tracking_by_default: true, 19 | }) 20 | 21 | Sentry.init({ 22 | dsn: 23 | process.env.NODE_ENV === 'development' 24 | ? undefined 25 | : process.env.REACT_APP_SENTRY_DSN, 26 | integrations: [ 27 | Sentry.browserTracingIntegration(), 28 | Sentry.replayIntegration(), 29 | Sentry.feedbackIntegration({ 30 | colorScheme: 'system', 31 | autoInject: false, 32 | }), 33 | ], 34 | tracesSampleRate: 1.0, 35 | replaysSessionSampleRate: 0.1, 36 | replaysOnErrorSampleRate: 1.0, 37 | }) 38 | 39 | root.render() 40 | -------------------------------------------------------------------------------- /src/ui/modules/About.tsx: -------------------------------------------------------------------------------- 1 | import { Layout, layouts, texts } from '@a_ng_d/figmug-ui' 2 | import { doClassnames, FeatureStatus } from '@a_ng_d/figmug-utils' 3 | import { PureComponent } from 'preact/compat' 4 | import React from 'react' 5 | import features, { 6 | authorUrl, 7 | isProEnabled, 8 | licenseUrl, 9 | repositoryUrl, 10 | } from '../../config' 11 | import { locals } from '../../content/locals' 12 | import { Language, PlanStatus, TrialStatus } from '../../types/app' 13 | import Feature from '../components/Feature' 14 | import package_json from './../../../package.json' 15 | import Icon from './Icon' 16 | 17 | interface AboutProps { 18 | planStatus: PlanStatus 19 | trialStatus: TrialStatus 20 | lang: Language 21 | } 22 | 23 | export default class About extends PureComponent { 24 | static features = (planStatus: PlanStatus) => ({ 25 | GET_PRO_PLAN: new FeatureStatus({ 26 | features: features, 27 | featureName: 'GET_PRO_PLAN', 28 | planStatus: planStatus, 29 | }), 30 | }) 31 | 32 | render() { 33 | return ( 34 | 40 |
41 | 42 |
43 | 49 | {locals[this.props.lang].name} 50 | 51 |
52 | {`Version ${package_json.version}`} 55 | 62 | 63 | {locals[this.props.lang].separator} 64 | 65 | {process.env.NODE_ENV === 'development' ? ( 66 | 67 | {locals[this.props.lang].plan.dev} 68 | 69 | ) : ( 70 | 71 | {this.props.planStatus === 'UNPAID' 72 | ? locals[this.props.lang].plan.free 73 | : this.props.planStatus === 'PAID' && 74 | this.props.trialStatus === 'PENDING' 75 | ? locals[this.props.lang].plan.trial 76 | : locals[this.props.lang].plan.pro} 77 | 78 | )} 79 | 80 |
81 |
82 |
83 |
84 | 85 | {locals[this.props.lang].about.createdBy} 86 | 91 | {locals[this.props.lang].about.author} 92 | 93 | 94 | 95 | 100 | {locals[this.props.lang].about.sourceCode} 101 | 102 | {locals[this.props.lang].about.isLicensed} 103 | 108 | {locals[this.props.lang].about.license} 109 | 110 | 111 |
112 |
113 | ), 114 | typeModifier: 'CENTERED', 115 | }, 116 | ]} 117 | isFullWidth 118 | /> 119 | ) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/ui/modules/Dispatcher.ts: -------------------------------------------------------------------------------- 1 | export default class Dispatcher { 2 | time: number 3 | on: { 4 | active: boolean 5 | blocked: boolean 6 | interval: number | string 7 | send: () => void 8 | stop: () => void 9 | status: boolean 10 | } 11 | 12 | constructor(callback: () => void, time: number) { 13 | this.time = time 14 | this.on = { 15 | active: false, 16 | blocked: false, 17 | interval: 0, 18 | send() { 19 | this.interval = setInterval(callback, time) 20 | this.blocked = true 21 | }, 22 | stop() { 23 | callback() 24 | clearInterval(this.interval) 25 | this.blocked = false 26 | }, 27 | get status() { 28 | return this.active 29 | }, 30 | set status(bool) { 31 | if (!this.blocked && bool) this.send() 32 | else if (this.blocked && !bool) this.stop() 33 | this.active = bool 34 | }, 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ui/modules/Icon.tsx: -------------------------------------------------------------------------------- 1 | import { PureComponent } from 'preact/compat' 2 | import React from 'react' 3 | 4 | interface IconProps { 5 | size: number 6 | } 7 | 8 | export default class Icon extends PureComponent { 9 | render() { 10 | return ( 11 | 18 | 24 | 30 | 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ui/modules/TrialControls.tsx: -------------------------------------------------------------------------------- 1 | import { Button, layouts, texts } from '@a_ng_d/figmug-ui' 2 | import { doClassnames, FeatureStatus } from '@a_ng_d/figmug-utils' 3 | import React, { PureComponent } from 'react' 4 | import features, { trialFeedbackUrl } from '../../config' 5 | import { locals } from '../../content/locals' 6 | import { Language, PlanStatus, TrialStatus } from '../../types/app' 7 | import Feature from '../components/Feature' 8 | 9 | interface TrialControlsProps { 10 | planStatus: PlanStatus 11 | trialStatus: TrialStatus 12 | trialRemainingTime: number 13 | lang: Language 14 | onGetProPlan: () => void 15 | } 16 | 17 | export default class TrialControls extends PureComponent { 18 | static features = (planStatus: PlanStatus) => ({ 19 | ACTIVITIES_RUN: new FeatureStatus({ 20 | features: features, 21 | featureName: 'ACTIVITIES_RUN', 22 | planStatus: planStatus, 23 | }), 24 | SHORTCUTS_FEEDBACK: new FeatureStatus({ 25 | features: features, 26 | featureName: 'SHORTCUTS_FEEDBACK', 27 | planStatus: planStatus, 28 | }), 29 | }) 30 | 31 | constructor(props: TrialControlsProps) { 32 | super(props) 33 | this.state = { 34 | isUserMenuLoading: false, 35 | } 36 | } 37 | 38 | // Templates 39 | RemainingTime = () => ( 40 |
47 | {Math.ceil(this.props.trialRemainingTime) > 72 && ( 48 | 49 | {locals[this.props.lang].plan.trialTimeDays.plural.replace( 50 | '$1', 51 | Math.ceil(this.props.trialRemainingTime) > 72 52 | ? Math.ceil(this.props.trialRemainingTime / 24) 53 | : Math.ceil(this.props.trialRemainingTime) 54 | )} 55 | 56 | )} 57 | {Math.ceil(this.props.trialRemainingTime) <= 72 && 58 | Math.ceil(this.props.trialRemainingTime) > 1 && ( 59 | 60 | {locals[this.props.lang].plan.trialTimeHours.plural.replace( 61 | '$1', 62 | Math.ceil(this.props.trialRemainingTime) 63 | )} 64 | 65 | )} 66 | {Math.ceil(this.props.trialRemainingTime) <= 1 && ( 67 | {locals[this.props.lang].plan.trialTimeHours.single} 68 | )} 69 |
70 | ) 71 | 72 | FreePlan = () => ( 73 | <> 74 |