diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..43ff29a4 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,127 @@ +# Copilot Instructions for Packages + +This folder contains multiple Figma plugins and widgets for the DB UX Design System. Each package follows a consistent structure and pattern. + +## Package Structure + +Each package in this folder follows this structure: +``` +package-name/ +├── package.json # Main package configuration with workspaces +├── manifest.json # Figma plugin/widget manifest +├── index.html # Built UI file (generated) +├── index.js # Built plugin code (generated) +├── plugin/ # Plugin/widget logic +│ ├── package.json # Plugin-specific dependencies and build scripts +│ ├── tsconfig.json # TypeScript configuration +│ └── src/ +│ └── index.ts # Main plugin entry point +└── ui/ # React-based UI (if applicable) + ├── package.json # UI-specific dependencies and build scripts + ├── vite.config.ts # Vite configuration for UI build + ├── tsconfig.json # TypeScript configuration + └── src/ + └── index.tsx # Main UI entry point +``` + +## Existing Packages + +- **auto-sync**: Widget for synchronizing design tokens +- **codegen**: Plugin for generating code from designs +- **design-migration**: Plugin for migrating design system components +- **handover**: Widget for creating design handover documentation +- **inspect**: Plugin for inspecting design elements +- **shared**: Common utilities and types used across packages + +## Development Guidelines + +### Creating a New Package + +1. Follow the existing package structure pattern +2. Use the same build tools (esbuild for plugin, vite for UI) +3. Include proper TypeScript configurations +4. Add appropriate scripts to root package.json +5. Use shared utilities from the `shared` package when possible + +### Plugin Development (plugin/ folder) + +- Use TypeScript with `@figma/plugin-typings` +- Main entry point should handle `figma.showUI()` for UI-based plugins +- Use `figma.ui.onmessage` for UI communication +- Use esbuild for building with ES6 target +- Follow ESLint configuration with Figma plugin rules + +### UI Development (ui/ folder) + +- Use React with TypeScript +- Use Vite with singlefile plugin to generate inline HTML +- Use Tailwind CSS for styling with DB design tokens +- Use `@db-ux/react-core-components` for UI components +- Communication with plugin via `parent.postMessage()` + +### Build Process + +- Plugin builds to `../index.js` in package root +- UI builds to `../index.html` in package root +- Use `npm-run-all` for parallel builds +- Include watch modes for development + +### Message Communication Pattern + +Use the shared message pattern: +```typescript +// From UI to Plugin +parent.postMessage({ pluginMessage: { type: "action", data: payload } }, "*"); + +// From Plugin to UI +figma.ui.postMessage({ type: "response", data: result }); +``` + +### Code Quality + +- Use ESLint with TypeScript and Figma plugin rules +- Follow existing code patterns and conventions +- Use proper TypeScript types, especially for Figma API +- Include error handling for user interactions + +### Dependencies + +- Prefer shared dependencies in root package.json when possible +- Keep package-specific dependencies minimal +- Use exact versions for design system components +- Avoid duplicating build tools across packages + +## Common Patterns + +### Plugin Entry Point +```typescript +import { handlePluginLogic } from "./plugin-logic"; + +if (figma.editorType === "figma") { + handlePluginLogic(); +} +``` + +### UI Message Handling +```typescript +useEffect(() => { + onmessage = (event: MessageEvent) => { + const message = event.data.pluginMessage; + // Handle different message types + }; +}, []); +``` + +### Build Scripts +```json +{ + "scripts": { + "build": "npm-run-all --parallel build:*", + "build:code": "npm run build -w=package-name-code -- --minify", + "build:ui": "npm run build -w=package-name-ui", + "dev": "npm-run-all --parallel dev:*", + "dev:code": "npm run dev -w=package-name-code", + "dev:ui": "npm run dev -w=package-name-ui" + } +} +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1e9d18ee..2f27577c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -420,6 +420,10 @@ "resolved": "packages/handover", "link": true }, + "node_modules/@db-ux/import-custom-colors": { + "resolved": "packages/import-custom-colors", + "link": true + }, "node_modules/@db-ux/inspect": { "resolved": "packages/inspect", "link": true @@ -902,6 +906,16 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/core": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.0.tgz", @@ -3503,6 +3517,14 @@ "node": ">= 4" } }, + "node_modules/import-custom-colors-code": { + "resolved": "packages/import-custom-colors/plugin", + "link": true + }, + "node_modules/import-custom-colors-ui": { + "resolved": "packages/import-custom-colors/ui", + "link": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -6281,6 +6303,301 @@ "typescript": "^5.9.2" } }, + "packages/import-custom-colors": { + "name": "@db-ux/import-custom-colors", + "version": "0.0.0", + "license": "MIT", + "workspaces": [ + "plugin/*", + "ui/*" + ] + }, + "packages/import-custom-colors/node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "packages/import-custom-colors/node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "packages/import-custom-colors/node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "packages/import-custom-colors/node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/import-custom-colors/node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "packages/import-custom-colors/node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "packages/import-custom-colors/node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "packages/import-custom-colors/node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "packages/import-custom-colors/node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/import-custom-colors/node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "packages/import-custom-colors/plugin": { + "name": "import-custom-colors-code", + "version": "0.0.0", + "devDependencies": { + "@figma/eslint-plugin-figma-plugins": "*", + "@figma/plugin-typings": "*", + "@typescript-eslint/eslint-plugin": "^8.23.0", + "@typescript-eslint/parser": "^8.26.1", + "eslint": "^8.54.0", + "typescript": "^5.9.2" + } + }, + "packages/import-custom-colors/ui": { + "name": "import-custom-colors-ui", + "version": "0.0.0", + "dependencies": { + "@db-ux/db-theme": "1.0.2", + "@db-ux/react-core-components": "1.2.1", + "react": "^19.1.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/react": "^19.1.13", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.7.0", + "autoprefixer": "^10.4.21", + "eslint": "^9.13.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^15.15.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "typescript": "~5.9.2", + "typescript-eslint": "^8.44.0", + "vite": "^6.3.5", + "vite-plugin-singlefile": "^2.3.0" + } + }, + "packages/import-custom-colors/ui/node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "packages/import-custom-colors/ui/node_modules/eslint": { + "version": "9.36.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", + "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.36.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "packages/import-custom-colors/ui/node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "packages/inspect": { "name": "@db-ux/inspect", "version": "1.0.0", diff --git a/package.json b/package.json index 8f3d99f9..49b508a7 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,14 @@ "build:handover": "npm run build -w=@db-ux/handover", "build:auto-sync": "npm run build -w=@db-ux/auto-sync", "build:design-migration": "npm run build -w=@db-ux/design-migration", + "build:import-custom-colors": "npm run build -w=@db-ux/import-custom-colors", "dev": "npm-run-all --parallel dev:*", "dev:codegen": "npm run dev -w=@db-ux/codegen", "dev:inspect": "npm run dev -w=@db-ux/inspect", "dev:handover": "npm run dev -w=@db-ux/handover", "dev:auto-sync": "npm run dev -w=@db-ux/auto-sync", - "dev:design-migration": "npm run dev -w=@db-ux/design-migration" + "dev:design-migration": "npm run dev -w=@db-ux/design-migration", + "dev:import-custom-colors": "npm run dev -w=@db-ux/import-custom-colors" }, "author": "", "license": "MIT", diff --git a/packages/import-custom-colors/README.md b/packages/import-custom-colors/README.md new file mode 100644 index 00000000..5e74bb5f --- /dev/null +++ b/packages/import-custom-colors/README.md @@ -0,0 +1,118 @@ +# Import Custom Colors - Figma Plugin + +A Figma plugin for importing custom colors and design tokens into Figma with visual swatches, local paint styles, and variable collections. + +## Features + +- **Design Token JSON Import**: Upload JSON files containing design tokens with the W3C specification format +- **Variable Collections**: Creates Figma variable collections with light and dark modes for design tokens +- **Individual Color Import**: Add colors one by one using the color picker interface +- **Bulk Import**: Paste multiple colors in various text formats +- **Visual Color Swatches**: Creates organized frames with color swatches, names, and hex values +- **Paint Styles**: Automatically creates local paint styles for each imported color +- **Multiple Format Support**: Supports various text formats for bulk import + +## Usage + +### Design Token JSON Import +1. Open the plugin in Figma +2. Click "Select JSON File" and choose your design token JSON file +3. The plugin will: + - Parse the JSON and extract all color tokens + - Create a "Base Colors" frame with visual swatches + - Generate a variable collection with Base Colors (Default mode) + - Create a Mode collection with Light/Dark modes and semantic aliases + - Create local paint styles in the "Base Colors/" category + +#### Supported JSON Format +The plugin supports the following design token JSON structure: +```json +{ + "colors": { + "category-name": { + "token-name": { + "$type": "color", + "$value": "#FF5733" + }, + "another-token": { + "$type": "color", + "$value": "#3498DB" + } + }, + "another-category": { + "token-name": { + "$type": "color", + "$value": "#2ECC71" + } + } + } +} +``` + +## Output + +The plugin creates: + +1. **Base Colors Frame**: Visual representation of all design tokens organized by category +2. **Base Colors Collection**: Figma variable collection containing all raw color values with Default mode +3. **Mode Collection**: Semantic color variables with Light Mode and Dark Mode containing aliases to Base Colors +4. **Semantic Variables**: Automatically creates variables for: + - Background colors (`category/bg/basic/level-1`, `category/bg/basic/level-1/hovered`, etc.) + - Text colors (`category/text/basic/default`, `category/text/basic/hovered`, etc.) + - Border colors (`category/border/basic/default`, `category/border/basic/hovered`, etc.) +5. **Paint Styles**: Local paint styles in the "Base Colors/" category + +#### Variable Collection Structure +The plugin creates two collections following professional design system patterns: + +**Base Colors Collection (Default mode):** +- Contains raw color values from JSON (0-14, origin-*, transparent-*, etc.) +- Organized by category (e.g., `dibe-category1/0`, `dibe-category1/origin-light-default`) + +**Mode Collection (Light Mode / Dark Mode):** +- Semantic variables that alias to Base Colors +- Light mode uses high numbers (14, 13, 12) for backgrounds and low numbers (1, 2, 3) for text +- Dark mode uses low numbers (1, 2, 3) for backgrounds and high numbers (14, 13, 12) for text +- Enables automatic theme switching in Figma + +### Manual Import +1. **Custom Color Palette Frame**: Visual color swatches in a grid layout +2. **Paint Styles**: Local paint styles in the "Custom Colors/" category + +## Development + +### Building +```bash +npm run build:import-custom-colors +``` + +### Development Mode +```bash +npm run dev:import-custom-colors +``` + +## File Structure + +``` +import-custom-colors/ +├── manifest.json # Figma plugin manifest +├── package.json # Main package configuration +├── plugin/ # Plugin logic (TypeScript) +│ ├── src/index.ts # Main plugin file with JSON parsing +│ └── package.json # Plugin dependencies +└── ui/ # React UI + ├── src/App.tsx # Main UI with file upload + └── package.json # UI dependencies +``` + +## Plugin Capabilities + +- Parses W3C design token JSON format +- Creates Figma variable collections for scalable design systems +- Creates visual color swatches in organized frames +- Organizes colors in a grid layout (8 colors per row) +- Automatically focuses viewport on the created color palette +- Creates reusable paint styles for design work +- Handles invalid color formats gracefully +- Provides user feedback for all operations +- Supports design token workflows with professional variable collections \ No newline at end of file diff --git a/packages/import-custom-colors/design-token-schema.md b/packages/import-custom-colors/design-token-schema.md new file mode 100644 index 00000000..c03c7cb8 --- /dev/null +++ b/packages/import-custom-colors/design-token-schema.md @@ -0,0 +1,120 @@ +# Design Token Schema Documentation + +This document describes the expected schema for design token JSON files used with the Import Custom Colors Figma plugin. + +## Overview + +The plugin creates a sophisticated variable collection architecture in Figma based on the imported design tokens, following professional design system patterns. + +## JSON Schema + +### Root Structure + +```json +{ + "colors": { + "[category-name]": { + "[token-name]": { + "$type": "color", + "$value": "#hex-value" + } + } + } +} +``` + +### Token Naming Conventions + +Each color category should include the following token types for optimal variable collection generation: + +#### Numeric Scale (0-14) +- `0` - Darkest color in the scale +- `1` through `13` - Progressive lightness values +- `14` - Lightest color in the scale + +#### Semantic Tokens +- `origin-light-default` - Primary color for light theme +- `origin-light-hovered` - Hovered state for light theme +- `origin-light-pressed` - Pressed state for light theme +- `on-origin-light-default` - Contrast color for light theme +- `origin-dark-default` - Primary color for dark theme +- `origin-dark-hovered` - Hovered state for dark theme +- `origin-dark-pressed` - Pressed state for dark theme +- `on-origin-dark-default` - Contrast color for dark theme + +#### Transparency Variants +- `transparent-full-light-default` - Fully transparent in light theme +- `transparent-full-light-hovered` - Semi-transparent hovered state +- `transparent-full-light-pressed` - Semi-transparent pressed state +- `transparent-full-dark-default` - Fully transparent in dark theme +- `transparent-full-dark-hovered` - Semi-transparent hovered state +- `transparent-full-dark-pressed` - Semi-transparent pressed state +- `transparent-semi-light-default` - Semi-transparent in light theme +- `transparent-semi-light-hovered` - Semi-transparent hovered state +- `transparent-semi-light-pressed` - Semi-transparent pressed state +- `transparent-semi-dark-default` - Semi-transparent in dark theme +- `transparent-semi-dark-hovered` - Semi-transparent hovered state +- `transparent-semi-dark-pressed` - Semi-transparent pressed state + +## Generated Variable Collections + +The plugin creates two Figma variable collections: + +### 1. Base Colors Collection (Default mode) +Contains all raw color values exactly as defined in the JSON: +- `[category]/0` through `[category]/14` +- `[category]/origin-light-default` +- `[category]/transparent-full-light-default` +- etc. + +### 2. Mode Collection (Light Mode / Dark Mode) +Contains semantic variables that alias to Base Colors: + +#### Background Variables +- `[category]/bg/basic/level-1/default` +- `[category]/bg/basic/level-1/hovered` +- `[category]/bg/basic/level-1/pressed` + +#### Text Variables +- `[category]/text/basic/default` +- `[category]/text/basic/hovered` +- `[category]/text/basic/pressed` + +#### Border Variables +- `[category]/border/basic/default` +- `[category]/border/basic/hovered` +- `[category]/border/basic/pressed` + +## Alias Mapping Logic + +### Light Mode +- **Backgrounds**: Use high numbers (14, 13, 12) for light backgrounds +- **Text**: Use low numbers (1, 2, 3) for dark text on light backgrounds +- **Borders**: Use mid-range values (7) for visible borders + +### Dark Mode +- **Backgrounds**: Use low numbers (1, 2, 3) for dark backgrounds +- **Text**: Use high numbers (14, 13, 12) for light text on dark backgrounds +- **Borders**: Use mid-range values (7) for visible borders + +This creates an automatic theme switching system where semantic variables automatically resolve to appropriate colors based on the active mode. + +## Example Usage + +```json +{ + "colors": { + "primary": { + "0": {"$type": "color", "$value": "#1a0909"}, + "7": {"$type": "color", "$value": "#b06060"}, + "14": {"$type": "color", "$value": "#fbf9f9"}, + "origin-light-default": {"$type": "color", "$value": "#753e3e"}, + "on-origin-light-default": {"$type": "color", "$value": "#fbf9f9"} + } + } +} +``` + +This would generate: +- Base Colors: `primary/0`, `primary/7`, `primary/14`, etc. +- Semantic Colors: `primary/bg/basic/level-1/default` (aliasing to `primary/14` in light mode, `primary/1` in dark mode) \ No newline at end of file diff --git a/packages/import-custom-colors/example-design-tokens.json b/packages/import-custom-colors/example-design-tokens.json new file mode 100644 index 00000000..88268e7e --- /dev/null +++ b/packages/import-custom-colors/example-design-tokens.json @@ -0,0 +1,288 @@ +{ + "colors": { + "dibe-category1": { + "0": { + "$type": "color", + "$value": "#1a0909" + }, + "1": { + "$type": "color", + "$value": "#281111" + }, + "2": { + "$type": "color", + "$value": "#391b1b" + }, + "3": { + "$type": "color", + "$value": "#4b2626" + }, + "4": { + "$type": "color", + "$value": "#5e3131" + }, + "5": { + "$type": "color", + "$value": "#713c3c" + }, + "6": { + "$type": "color", + "$value": "#8c4c4c" + }, + "7": { + "$type": "color", + "$value": "#b06060" + }, + "8": { + "$type": "color", + "$value": "#c27f7f" + }, + "9": { + "$type": "color", + "$value": "#cea0a0" + }, + "10": { + "$type": "color", + "$value": "#dcc0c0" + }, + "11": { + "$type": "color", + "$value": "#eddfdf" + }, + "12": { + "$type": "color", + "$value": "#f4ecec" + }, + "13": { + "$type": "color", + "$value": "#f7f2f2" + }, + "14": { + "$type": "color", + "$value": "#fbf9f9" + }, + "origin-light-default": { + "$type": "color", + "$value": "#753e3e" + }, + "origin-light-hovered": { + "$type": "color", + "$value": "#281111" + }, + "origin-light-pressed": { + "$type": "color", + "$value": "#552b2b" + }, + "on-origin-light-default": { + "$type": "color", + "$value": "#fbf9f9" + }, + "origin-dark-default": { + "$type": "color", + "$value": "#753e3e" + }, + "origin-dark-hovered": { + "$type": "color", + "$value": "#281111" + }, + "origin-dark-pressed": { + "$type": "color", + "$value": "#552b2b" + }, + "on-origin-dark-default": { + "$type": "color", + "$value": "#fbf9f9" + }, + "transparent-full-light-default": { + "$type": "color", + "$value": "#8c4c4c00" + }, + "transparent-full-light-hovered": { + "$type": "color", + "$value": "#8c4c4c3d" + }, + "transparent-full-light-pressed": { + "$type": "color", + "$value": "#8c4c4c52" + }, + "transparent-full-dark-default": { + "$type": "color", + "$value": "#cea0a000" + }, + "transparent-full-dark-hovered": { + "$type": "color", + "$value": "#cea0a03d" + }, + "transparent-full-dark-pressed": { + "$type": "color", + "$value": "#cea0a052" + }, + "transparent-semi-light-default": { + "$type": "color", + "$value": "#8c4c4c14" + }, + "transparent-semi-light-hovered": { + "$type": "color", + "$value": "#8c4c4c3d" + }, + "transparent-semi-light-pressed": { + "$type": "color", + "$value": "#8c4c4c52" + }, + "transparent-semi-dark-default": { + "$type": "color", + "$value": "#cea0a029" + }, + "transparent-semi-dark-hovered": { + "$type": "color", + "$value": "#cea0a03d" + }, + "transparent-semi-dark-pressed": { + "$type": "color", + "$value": "#cea0a052" + } + }, + "dibe-category2": { + "0": { + "$type": "color", + "$value": "#110d0d" + }, + "1": { + "$type": "color", + "$value": "#1b1717" + }, + "2": { + "$type": "color", + "$value": "#282222" + }, + "3": { + "$type": "color", + "$value": "#362f2f" + }, + "4": { + "$type": "color", + "$value": "#443c3c" + }, + "5": { + "$type": "color", + "$value": "#534949" + }, + "6": { + "$type": "color", + "$value": "#685b5b" + }, + "7": { + "$type": "color", + "$value": "#837373" + }, + "8": { + "$type": "color", + "$value": "#9a8e8e" + }, + "9": { + "$type": "color", + "$value": "#b1a9a9" + }, + "10": { + "$type": "color", + "$value": "#cac5c5" + }, + "11": { + "$type": "color", + "$value": "#e4e2e2" + }, + "12": { + "$type": "color", + "$value": "#efeded" + }, + "13": { + "$type": "color", + "$value": "#f4f3f3" + }, + "14": { + "$type": "color", + "$value": "#faf9f9" + }, + "origin-light-default": { + "$type": "color", + "$value": "#473e3e" + }, + "origin-light-hovered": { + "$type": "color", + "$value": "#090707" + }, + "origin-light-pressed": { + "$type": "color", + "$value": "#2f2929" + }, + "on-origin-light-default": { + "$type": "color", + "$value": "#faf9f9" + }, + "origin-dark-default": { + "$type": "color", + "$value": "#473e3e" + }, + "origin-dark-hovered": { + "$type": "color", + "$value": "#090707" + }, + "origin-dark-pressed": { + "$type": "color", + "$value": "#2f2929" + }, + "on-origin-dark-default": { + "$type": "color", + "$value": "#faf9f9" + }, + "transparent-full-light-default": { + "$type": "color", + "$value": "#685b5b00" + }, + "transparent-full-light-hovered": { + "$type": "color", + "$value": "#685b5b3d" + }, + "transparent-full-light-pressed": { + "$type": "color", + "$value": "#685b5b52" + }, + "transparent-full-dark-default": { + "$type": "color", + "$value": "#b1a9a900" + }, + "transparent-full-dark-hovered": { + "$type": "color", + "$value": "#b1a9a93d" + }, + "transparent-full-dark-pressed": { + "$type": "color", + "$value": "#b1a9a952" + }, + "transparent-semi-light-default": { + "$type": "color", + "$value": "#685b5b14" + }, + "transparent-semi-light-hovered": { + "$type": "color", + "$value": "#685b5b3d" + }, + "transparent-semi-light-pressed": { + "$type": "color", + "$value": "#685b5b52" + }, + "transparent-semi-dark-default": { + "$type": "color", + "$value": "#b1a9a929" + }, + "transparent-semi-dark-hovered": { + "$type": "color", + "$value": "#b1a9a93d" + }, + "transparent-semi-dark-pressed": { + "$type": "color", + "$value": "#b1a9a952" + } + } + } +} \ No newline at end of file diff --git a/packages/import-custom-colors/manifest.json b/packages/import-custom-colors/manifest.json new file mode 100644 index 00000000..01355af5 --- /dev/null +++ b/packages/import-custom-colors/manifest.json @@ -0,0 +1,10 @@ +{ + "name": "import-custom-colors", + "id": "import-custom-colors", + "api": "0.0.0", + "main": "index.js", + "ui": "index.html", + "editorType": ["figma"], + "networkAccess": { "allowedDomains": ["none"] }, + "documentAccess": "dynamic-page" +} \ No newline at end of file diff --git a/packages/import-custom-colors/package.json b/packages/import-custom-colors/package.json new file mode 100644 index 00000000..ed5c2dd9 --- /dev/null +++ b/packages/import-custom-colors/package.json @@ -0,0 +1,21 @@ +{ + "name": "@db-ux/import-custom-colors", + "private": true, + "version": "0.0.0", + "description": "Figma plugin for importing custom colors into Figma", + "main": "index.js", + "workspaces": [ + "plugin/*", + "ui/*" + ], + "scripts": { + "build": "npm-run-all --parallel build:*", + "build:code": "npm run build -w=import-custom-colors-code -- --minify", + "build:ui": "npm run build -w=import-custom-colors-ui", + "dev": "npm-run-all --parallel dev:*", + "dev:code": "npm run dev -w=import-custom-colors-code", + "dev:ui": "npm run dev -w=import-custom-colors-ui" + }, + "author": "", + "license": "MIT" +} \ No newline at end of file diff --git a/packages/import-custom-colors/plugin/package.json b/packages/import-custom-colors/plugin/package.json new file mode 100644 index 00000000..393e1e3a --- /dev/null +++ b/packages/import-custom-colors/plugin/package.json @@ -0,0 +1,44 @@ +{ + "name": "import-custom-colors-code", + "version": "0.0.0", + "main": "import-custom-colors.js", + "type": "module", + "scripts": { + "build": "esbuild src/index.ts --bundle --outfile=../index.js --target=ES6", + "lint": "eslint --ext .ts,.tsx --ignore-pattern node_modules .", + "lint:fix": "eslint --ext .ts,.tsx --ignore-pattern node_modules --fix .", + "dev": "npm run build -- --watch" + }, + "author": "", + "license": "", + "devDependencies": { + "@figma/eslint-plugin-figma-plugins": "*", + "@figma/plugin-typings": "*", + "@typescript-eslint/eslint-plugin": "^8.23.0", + "@typescript-eslint/parser": "^8.26.1", + "eslint": "^8.54.0", + "typescript": "^5.9.2" + }, + "eslintConfig": { + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@figma/figma-plugins/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./tsconfig.json" + }, + "root": true, + "rules": { + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ] + } + } +} \ No newline at end of file diff --git a/packages/import-custom-colors/plugin/src/index.ts b/packages/import-custom-colors/plugin/src/index.ts new file mode 100644 index 00000000..f9276075 --- /dev/null +++ b/packages/import-custom-colors/plugin/src/index.ts @@ -0,0 +1,304 @@ +import { UiMessageImportColors } from "shared/data"; +import { sendMessage } from "shared/figma"; + +interface CustomColor { + name: string; + hex: string; +} + +interface DesignTokenColor { + $type: "color"; + $value: string; +} + +interface DesignTokensSchema { + colors: { + [category: string]: { + [tokenName: string]: DesignTokenColor; + }; + }; +} + +export const handleImportCustomColors = () => { + figma.showUI(__html__, { height: 600, width: 400 }); + + figma.ui.onmessage = async (msg: UiMessageImportColors) => { + try { + if (msg.type === "import-json") { + sendMessage({ type: "loading", data: "Processing design token JSON file..." }); + const jsonData = (msg.data as { jsonData: string }).jsonData; + const colors = parseDesignTokensJson(jsonData); + await importColorsFromTokens(colors); + sendMessage({ type: "success", data: `Imported ${colors.length} colors from design tokens!` }); + } + } catch (error) { + console.error("Error importing colors:", error); + sendMessage({ + type: "error", + data: error instanceof Error ? error.message : "Unknown error occurred" + }); + } + }; +}; + +const parseDesignTokensJson = (jsonString: string): CustomColor[] => { + try { + const tokens: DesignTokensSchema = JSON.parse(jsonString); + const colors: CustomColor[] = []; + + if (!tokens.colors) { + throw new Error("Invalid JSON format: 'colors' property not found"); + } + + Object.entries(tokens.colors).forEach(([category, categoryColors]) => { + Object.entries(categoryColors).forEach(([tokenName, token]) => { + if (token.$type === "color" && token.$value) { + colors.push({ + name: `${category}/${tokenName}`, + hex: token.$value + }); + } + }); + }); + + return colors; + } catch (error) { + throw new Error(`Failed to parse JSON: ${error instanceof Error ? error.message : 'Invalid format'}`); + } +}; + +const importColorsFromTokens = async (colors: CustomColor[]) => { + // Create base colors frame + await createColorFrame(colors, "Base Colors", true); + + // Create variable collection for light and dark modes + await createVariableCollection(colors); +}; + +const createVariableCollection = async (colors: CustomColor[]) => { + try { + // Group colors by category + const colorsByCategory = new Map(); + colors.forEach((color) => { + const parts = color.name.split('/'); + const category = parts[0]; + if (!colorsByCategory.has(category)) { + colorsByCategory.set(category, []); + } + colorsByCategory.get(category)!.push({ + name: parts[1] || parts[0], + hex: color.hex + }); + }); + + // Create Base Colors collection + const baseColorsCollection = figma.variables.createVariableCollection("Base Colors"); + const defaultMode = baseColorsCollection.modes[0]; + defaultMode.name = "Default"; + + // Create variables for each category in Base Colors collection + const baseColorVariables = new Map(); + + colorsByCategory.forEach((categoryColors, category) => { + categoryColors.forEach((color) => { + const variableName = `${category}/${color.name}`; + const variable = figma.variables.createVariable(variableName, baseColorsCollection, "COLOR"); + + const rgb = hexToRgb(color.hex); + if (rgb) { + const figmaColor = { r: rgb.r / 255, g: rgb.g / 255, b: rgb.b / 255 }; + variable.setValueForMode(defaultMode.modeId, figmaColor); + baseColorVariables.set(variableName, variable); + } + }); + }); + + // Create Mode collection with Light and Dark modes + const modeCollection = figma.variables.createVariableCollection("Mode"); + const lightMode = modeCollection.modes[0]; + lightMode.name = "Light Mode"; + const darkMode = modeCollection.addMode("Dark Mode"); + + // Create semantic color variables with aliases + colorsByCategory.forEach((categoryColors, category) => { + // Create bg semantic variables + const bgBasicLevel1Variable = figma.variables.createVariable(`${category}/bg/basic/level-1`, modeCollection, "COLOR"); + const bgBasicLevel1HoveredVariable = figma.variables.createVariable(`${category}/bg/basic/level-1/hovered`, modeCollection, "COLOR"); + const bgBasicLevel1PressedVariable = figma.variables.createVariable(`${category}/bg/basic/level-1/pressed`, modeCollection, "COLOR"); + + // Set up aliases for light mode (using high numbers for light backgrounds) + const lightBaseVariable = baseColorVariables.get(`${category}/14`) || baseColorVariables.get(`${category}/origin-light-default`); + const lightHoveredVariable = baseColorVariables.get(`${category}/13`) || baseColorVariables.get(`${category}/origin-light-hovered`); + const lightPressedVariable = baseColorVariables.get(`${category}/12`) || baseColorVariables.get(`${category}/origin-light-pressed`); + + // Set up aliases for dark mode (using low numbers for dark backgrounds) + const darkBaseVariable = baseColorVariables.get(`${category}/1`) || baseColorVariables.get(`${category}/origin-dark-default`); + const darkHoveredVariable = baseColorVariables.get(`${category}/3`) || baseColorVariables.get(`${category}/origin-dark-hovered`); + const darkPressedVariable = baseColorVariables.get(`${category}/2`) || baseColorVariables.get(`${category}/origin-dark-pressed`); + + if (lightBaseVariable && darkBaseVariable) { + // Create aliases to Base Colors collection variables + bgBasicLevel1Variable.setValueForMode(lightMode.modeId, figma.variables.createVariableAlias(lightBaseVariable)); + bgBasicLevel1Variable.setValueForMode(darkMode.modeId, figma.variables.createVariableAlias(darkBaseVariable)); + } + + if (lightHoveredVariable && darkHoveredVariable) { + bgBasicLevel1HoveredVariable.setValueForMode(lightMode.modeId, figma.variables.createVariableAlias(lightHoveredVariable)); + bgBasicLevel1HoveredVariable.setValueForMode(darkMode.modeId, figma.variables.createVariableAlias(darkHoveredVariable)); + } + + if (lightPressedVariable && darkPressedVariable) { + bgBasicLevel1PressedVariable.setValueForMode(lightMode.modeId, figma.variables.createVariableAlias(lightPressedVariable)); + bgBasicLevel1PressedVariable.setValueForMode(darkMode.modeId, figma.variables.createVariableAlias(darkPressedVariable)); + } + + // Create text color variables + const textBasicDefaultVariable = figma.variables.createVariable(`${category}/text/basic/default`, modeCollection, "COLOR"); + const textBasicHoveredVariable = figma.variables.createVariable(`${category}/text/basic/hovered`, modeCollection, "COLOR"); + const textBasicPressedVariable = figma.variables.createVariable(`${category}/text/basic/pressed`, modeCollection, "COLOR"); + + // Text colors use opposite logic - dark text on light backgrounds, light text on dark backgrounds + const lightTextVariable = baseColorVariables.get(`${category}/1`) || baseColorVariables.get(`${category}/on-origin-light-default`); + const darkTextVariable = baseColorVariables.get(`${category}/14`) || baseColorVariables.get(`${category}/on-origin-dark-default`); + + if (lightTextVariable && darkTextVariable) { + textBasicDefaultVariable.setValueForMode(lightMode.modeId, figma.variables.createVariableAlias(lightTextVariable)); + textBasicDefaultVariable.setValueForMode(darkMode.modeId, figma.variables.createVariableAlias(darkTextVariable)); + + textBasicHoveredVariable.setValueForMode(lightMode.modeId, figma.variables.createVariableAlias(lightTextVariable)); + textBasicHoveredVariable.setValueForMode(darkMode.modeId, figma.variables.createVariableAlias(darkTextVariable)); + + textBasicPressedVariable.setValueForMode(lightMode.modeId, figma.variables.createVariableAlias(lightTextVariable)); + textBasicPressedVariable.setValueForMode(darkMode.modeId, figma.variables.createVariableAlias(darkTextVariable)); + } + + // Create border color variables + const borderBasicDefaultVariable = figma.variables.createVariable(`${category}/border/basic/default`, modeCollection, "COLOR"); + const borderBasicHoveredVariable = figma.variables.createVariable(`${category}/border/basic/hovered`, modeCollection, "COLOR"); + const borderBasicPressedVariable = figma.variables.createVariable(`${category}/border/basic/pressed`, modeCollection, "COLOR"); + + // Border colors use mid-range values + const lightBorderVariable = baseColorVariables.get(`${category}/7`) || baseColorVariables.get(`${category}/origin-light-default`); + const darkBorderVariable = baseColorVariables.get(`${category}/7`) || baseColorVariables.get(`${category}/origin-dark-default`); + + if (lightBorderVariable && darkBorderVariable) { + borderBasicDefaultVariable.setValueForMode(lightMode.modeId, figma.variables.createVariableAlias(lightBorderVariable)); + borderBasicDefaultVariable.setValueForMode(darkMode.modeId, figma.variables.createVariableAlias(darkBorderVariable)); + + borderBasicHoveredVariable.setValueForMode(lightMode.modeId, figma.variables.createVariableAlias(lightBorderVariable)); + borderBasicHoveredVariable.setValueForMode(darkMode.modeId, figma.variables.createVariableAlias(darkBorderVariable)); + + borderBasicPressedVariable.setValueForMode(lightMode.modeId, figma.variables.createVariableAlias(lightBorderVariable)); + borderBasicPressedVariable.setValueForMode(darkMode.modeId, figma.variables.createVariableAlias(darkBorderVariable)); + } + }); + + sendMessage({ + type: "success", + data: `Created Base Colors collection with ${baseColorVariables.size} variables and Mode collection with semantic color aliases for ${colorsByCategory.size} categories` + }); + } catch (error) { + console.warn("Could not create variable collections:", error); + sendMessage({ + type: "error", + data: `Failed to create variable collections: ${error instanceof Error ? error.message : 'Unknown error'}` + }); + } +}; + +const createColorFrame = async (colors: CustomColor[], frameName: string, isBaseColors: boolean) => { + // Get the current page + const currentPage = figma.currentPage; + + // Create or find a color palette frame + let colorFrame = currentPage.findOne(node => + node.type === "FRAME" && node.name === frameName + ) as FrameNode; + + if (!colorFrame) { + colorFrame = figma.createFrame(); + colorFrame.name = frameName; + colorFrame.resize(800, Math.max(400, Math.ceil(colors.length / 8) * 100)); + colorFrame.x = 0; + colorFrame.y = 0; + colorFrame.fills = [{ type: "SOLID", color: { r: 0.98, g: 0.98, b: 0.98 } }]; + } + + // Clear existing colors in the frame + colorFrame.children.forEach(child => child.remove()); + + // Create color swatches + for (let i = 0; i < colors.length; i++) { + const color = colors[i]; + const col = i % 8; + const row = Math.floor(i / 8); + + // Convert hex to RGB + const rgb = hexToRgb(color.hex); + if (!rgb) { + console.warn(`Invalid hex color: ${color.hex}`); + continue; + } + + // Create color swatch + const swatch = figma.createRectangle(); + swatch.resize(80, 80); + swatch.x = col * 90 + 20; + swatch.y = row * 90 + 60; + swatch.fills = [{ + type: "SOLID", + color: { r: rgb.r / 255, g: rgb.g / 255, b: rgb.b / 255 } + }]; + swatch.name = color.name; + + // Add color name text + const text = figma.createText(); + await figma.loadFontAsync({ family: "Inter", style: "Regular" }); + text.characters = color.name; + text.fontSize = 10; + text.x = col * 90 + 20; + text.y = row * 90 + 145; + text.resize(80, 20); + text.textAlignHorizontal = "CENTER"; + + // Add hex value text + const hexText = figma.createText(); + hexText.characters = color.hex.toUpperCase(); + hexText.fontSize = 8; + hexText.x = col * 90 + 20; + hexText.y = row * 90 + 160; + hexText.resize(80, 16); + hexText.textAlignHorizontal = "CENTER"; + hexText.fills = [{ type: "SOLID", color: { r: 0.5, g: 0.5, b: 0.5 } }]; + + // Add to frame + colorFrame.appendChild(swatch); + colorFrame.appendChild(text); + colorFrame.appendChild(hexText); + + // Create local paint style + const paintStyle = figma.createPaintStyle(); + const styleCategory = isBaseColors ? "Base Colors" : "Custom Colors"; + paintStyle.name = `${styleCategory}/${color.name}`; + paintStyle.paints = [{ + type: "SOLID", + color: { r: rgb.r / 255, g: rgb.g / 255, b: rgb.b / 255 } + }]; + } + + // Focus on the color frame + figma.viewport.scrollAndZoomIntoView([colorFrame]); +}; + +const hexToRgb = (hex: string): { r: number; g: number; b: number } | null => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; +}; + +if (figma.editorType === "figma") { + handleImportCustomColors(); +} \ No newline at end of file diff --git a/packages/import-custom-colors/plugin/tsconfig.json b/packages/import-custom-colors/plugin/tsconfig.json new file mode 100644 index 00000000..57bd0b1f --- /dev/null +++ b/packages/import-custom-colors/plugin/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES6", + "lib": ["ES6"], + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/packages/import-custom-colors/ui/package.json b/packages/import-custom-colors/ui/package.json new file mode 100644 index 00000000..76bce78c --- /dev/null +++ b/packages/import-custom-colors/ui/package.json @@ -0,0 +1,34 @@ +{ + "name": "import-custom-colors-ui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite dev --host 0.0.0.0 --port 5173", + "build": "tsc -b && vite build --minify esbuild --emptyOutDir=false --target=ES6", + "lint": "eslint ." + }, + "dependencies": { + "@db-ux/react-core-components": "1.2.1", + "@db-ux/db-theme": "1.0.2", + "react": "^19.1.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/react": "^19.1.13", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.7.0", + "autoprefixer": "^10.4.21", + "eslint": "^9.13.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^15.15.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "typescript": "~5.9.2", + "typescript-eslint": "^8.44.0", + "vite": "^6.3.5", + "vite-plugin-singlefile": "^2.3.0" + } +} \ No newline at end of file diff --git a/packages/import-custom-colors/ui/src/App.tsx b/packages/import-custom-colors/ui/src/App.tsx new file mode 100644 index 00000000..dd676a29 --- /dev/null +++ b/packages/import-custom-colors/ui/src/App.tsx @@ -0,0 +1,133 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { DBButton, DBInfotext, DBBrand, DBHeader, DBPage, DBSection } from '@db-ux/react-core-components'; + +interface PluginMessage { + type: string; + data?: any; +} + +const ImportColorsPage: React.FC = () => { + const [loading, setLoading] = useState(false); + const [message, setMessage] = useState(''); + const [messageType, setMessageType] = useState<'success' | 'error' | 'info'>('info'); + const fileInputRef = useRef(null); + + useEffect(() => { + window.onmessage = (event: MessageEvent) => { + const pluginMessage: PluginMessage = event.data.pluginMessage; + + if (pluginMessage.type === 'loading') { + setLoading(true); + setMessage(pluginMessage.data || 'Processing...'); + setMessageType('info'); + } else if (pluginMessage.type === 'success') { + setLoading(false); + setMessage(pluginMessage.data || 'Success!'); + setMessageType('success'); + } else if (pluginMessage.type === 'error') { + setLoading(false); + setMessage(pluginMessage.data || 'An error occurred'); + setMessageType('error'); + } + }; + }, []); + + const handleFileUpload = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + if (!file.name.toLowerCase().endsWith('.json')) { + setMessage('Please select a JSON file'); + setMessageType('error'); + return; + } + + const reader = new FileReader(); + reader.onload = (e) => { + try { + const jsonData = e.target?.result as string; + setMessage(''); + parent.postMessage({ + pluginMessage: { + type: 'import-json', + data: { jsonData } + } + }, '*'); + } catch (error) { + setMessage('Failed to read file'); + setMessageType('error'); + } + }; + reader.readAsText(file); + }; + + return ( +
+

Import Design Token Colors

+ + {message && ( + + {message} + + )} + +
+
+

Upload Design Tokens JSON

+

+ Upload a JSON file containing design tokens with the following structure: +

+
+ {`{ + "colors": { + "category-name": { + "token-name": { + "$type": "color", + "$value": "#FF5733" + } + } + } +}`} +
+ +
+ + fileInputRef.current?.click()} + disabled={loading} + variant="primary" + > + {loading ? 'Processing...' : 'Select JSON File'} + +

+ Colors will be imported as "Base Colors" with variable collections and semantic aliases +

+
+
+
+
+ ); +}; + +const App: React.FC = () => { + return ( + Import Custom Colors}>} + > + + + + + ); +}; + +export default App; \ No newline at end of file diff --git a/packages/import-custom-colors/ui/src/index.css b/packages/import-custom-colors/ui/src/index.css new file mode 100644 index 00000000..bd6213e1 --- /dev/null +++ b/packages/import-custom-colors/ui/src/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/packages/import-custom-colors/ui/src/main.tsx b/packages/import-custom-colors/ui/src/main.tsx new file mode 100644 index 00000000..3d4bdea4 --- /dev/null +++ b/packages/import-custom-colors/ui/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) \ No newline at end of file diff --git a/packages/import-custom-colors/ui/tailwind.config.ts b/packages/import-custom-colors/ui/tailwind.config.ts new file mode 100644 index 00000000..3f5632c0 --- /dev/null +++ b/packages/import-custom-colors/ui/tailwind.config.ts @@ -0,0 +1,19 @@ +import type { Config } from "tailwindcss"; +import { CustomThemeConfig } from "tailwindcss/types/config"; +// @ts-ignore +import tokens from "@db-ux/core-foundations/build/tailwind/tailwind-tokens.json"; +const customThemeConfig: CustomThemeConfig = tokens as any; + +export default { + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + ...customThemeConfig, + gap: ({ theme }) => ({ + ...theme("spacing"), + }), + space: ({ theme }) => ({ + ...theme("spacing"), + }), + }, + plugins: [], +} satisfies Config; \ No newline at end of file diff --git a/packages/import-custom-colors/ui/tsconfig.json b/packages/import-custom-colors/ui/tsconfig.json new file mode 100644 index 00000000..a90ef5b2 --- /dev/null +++ b/packages/import-custom-colors/ui/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} \ No newline at end of file diff --git a/packages/import-custom-colors/ui/tsconfig.tsbuildinfo b/packages/import-custom-colors/ui/tsconfig.tsbuildinfo new file mode 100644 index 00000000..44870c70 --- /dev/null +++ b/packages/import-custom-colors/ui/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/App.tsx","./src/main.tsx"],"version":"5.9.2"} \ No newline at end of file diff --git a/packages/import-custom-colors/ui/vite.config.ts b/packages/import-custom-colors/ui/vite.config.ts new file mode 100644 index 00000000..cc54382a --- /dev/null +++ b/packages/import-custom-colors/ui/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import {viteSingleFile} from "vite-plugin-singlefile"; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react(),viteSingleFile()], + build: { + target: "esnext", + assetsInlineLimit: 100000000, + chunkSizeWarningLimit: 100000000, + cssCodeSplit: false, + outDir: "../", + rollupOptions: { + output: {}, + }, + }, +}) \ No newline at end of file diff --git a/packages/shared/data.ts b/packages/shared/data.ts index cff955e2..11b84498 100644 --- a/packages/shared/data.ts +++ b/packages/shared/data.ts @@ -8,7 +8,8 @@ export type PluginMessage = { | "loading" | "error" | "storage" - | "counter"; + | "counter" + | "success"; data: T; }; @@ -54,3 +55,10 @@ export type UiMessageDesignMigration = { type: "analyze"; data?: any; }; + +export type UiMessageImportColors = { + type: "import-json"; + data: { + jsonData: string; + }; +};