├── .all-contributorsrc ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── LICENSE ├── README.md ├── applescript.sh ├── images ├── banner.png └── icon.png ├── jest.config.js ├── manifest.json ├── modd.conf ├── package.json ├── src ├── @types │ ├── core.d.ts │ ├── figma.d.ts │ └── tailwind.d.ts ├── App.tsx ├── __tests__ │ ├── colors.test.ts │ └── config.test.ts ├── code.ts ├── components │ ├── ColorView.tsx │ ├── FileUpload.tsx │ └── Footer.tsx ├── core │ ├── colors.ts │ ├── config.ts │ └── fonts.ts ├── generated │ └── tw.min.json ├── models │ └── Root.ts ├── resources │ ├── css │ │ └── ui.css │ └── data │ │ └── .gitkeep └── ui.html ├── tailwind.config.js ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "KA95DEV", 10 | "name": "KA95DEV", 11 | "avatar_url": "https://avatars1.githubusercontent.com/u/32483834?v=4", 12 | "profile": "https://github.com/KA95DEV", 13 | "contributions": [ 14 | "code" 15 | ] 16 | }, 17 | { 18 | "login": "leinardi", 19 | "name": "Roberto Leinardi", 20 | "avatar_url": "https://avatars2.githubusercontent.com/u/273338?v=4", 21 | "profile": "https://github.com/leinardi", 22 | "contributions": [ 23 | "ideas", 24 | "code" 25 | ] 26 | } 27 | ], 28 | "contributorsPerLine": 7, 29 | "projectName": "tailwindcss-figma-plugin", 30 | "projectOwner": "impulse", 31 | "repoType": "github", 32 | "repoHost": "https://github.com", 33 | "skipCi": true 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Figma Plugin CI 2 | 3 | on: 4 | push: 5 | # Run against main branch only. 6 | branches: [main] 7 | pull_request: 8 | # Include events emitted on code change only. 9 | types: [opened, synchronize] 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [14.x] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - name: yarn install, build, and test 26 | run: | 27 | yarn install 28 | yarn build 29 | yarn test 30 | env: 31 | CI: true 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | /coverage.data 5 | /coverage/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ecklf 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tailwind CSS Figma Plugin 2 | 3 | ![Figma Plugin CI](https://github.com/impulse/tailwindcss-figma-plugin/workflows/Figma%20Plugin%20CI/badge.svg) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/impulse/tailwindcss-figma-plugin/blob/master/LICENSE) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg) 4 | 5 | 6 | 7 | [![All Contributors](https://img.shields.io/badge/all_contributors-2-orange.svg?style=flat-square)](#contributors-) 8 | 9 | 10 | 11 | Making your life with Tailwind CSS and Figma easier. 12 | 13 | ![plugin banner](./images/banner.png) 14 | 15 | ## Usage 16 | 17 | 1. Generate a resolved TailwindCSS config (make sure dependencies are installed) 18 | 19 | ```sh 20 | $ cd yourtailwindproject/ 21 | $ npx tailwind.json 22 | ``` 23 | 24 | 2. Drag the generated `tailwind.json` in the plugin. 25 | 3. Done! 26 | 27 | ## Setup 28 | 29 | This Plugin will only work when running within Figma since it relies on its API. 30 | 31 | Go to the Plugins tab and add this projects `manifest.json`. For more info read the [Figma docs](https://www.figma.com/plugin-docs/intro/) 32 | 33 | 1. Then, install your dependencies: 34 | 35 | ```sh 36 | $ yarn install 37 | ``` 38 | 39 | 2. The config offers either a dev or build script 40 | 41 | ```sh 42 | $ yarn dev 43 | $ yarn build 44 | ``` 45 | 46 | ## Hot Reloading (macOS only) 47 | 48 | Not the smoothest experience, but way better than needing to press a hotkey. In case you use the Figma Beta change App name in `applescript.sh`. 49 | 50 | ```sh 51 | $ brew install modd 52 | $ cd tailwindcss-figma-plugin/ 53 | $ modd 54 | ``` 55 | 56 | ## License 57 | 58 | MIT 59 | 60 | ## Contributors ✨ 61 | 62 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |

KA95DEV

💻

Roberto Leinardi

🤔 💻
73 | 74 | 75 | 76 | 77 | 78 | 79 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 80 | -------------------------------------------------------------------------------- /applescript.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | osascript <<'EOF' 4 | tell application "Figma" to activate 5 | tell application "System Events" to tell process "Figma" 6 | keystroke "p" using {command down, option down} 7 | end tell 8 | EOF -------------------------------------------------------------------------------- /images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecklf/tailwindcss-figma-plugin/114e846993c33f714e0dd4699af7b67fc36d635b/images/banner.png -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecklf/tailwindcss-figma-plugin/114e846993c33f714e0dd4699af7b67fc36d635b/images/icon.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tailwind CSS", 3 | "id": "738806869514947558", 4 | "api": "1.0.0", 5 | "main": "dist/code.js", 6 | "ui": "dist/ui.html" 7 | } 8 | -------------------------------------------------------------------------------- /modd.conf: -------------------------------------------------------------------------------- 1 | ** !dist/** !lib/** !node_modules/** { 2 | prep: yarn build:dev 3 | prep: ./applescript.sh 4 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailwindcss", 3 | "version": "2.1.0", 4 | "description": "Make your life with Tailwind CSS and Figma easier", 5 | "main": "code.ts", 6 | "typings": "./src/@types", 7 | "scripts": { 8 | "test": "jest", 9 | "test:watch": "jest --watchAll", 10 | "coverage": "jest --coverage", 11 | "dev": "webpack --mode=development --watch", 12 | "build:dev": "webpack --mode=development", 13 | "build": "webpack --mode=production" 14 | }, 15 | "author": "impulse", 16 | "license": "MIT", 17 | "dependencies": { 18 | "autoprefixer": "^10.3.4", 19 | "classnames": "^2.2.6", 20 | "css-loader": "^3.6.0", 21 | "html-webpack-inline-source-plugin": "0.0.10", 22 | "html-webpack-plugin": "^3.2.0", 23 | "lodash": "^4.17.20", 24 | "mobx": "^6.0.4", 25 | "mobx-react-lite": "^3.1.6", 26 | "mobx-state-tree": "^4.0.2", 27 | "parse-color": "^1.0.0", 28 | "postcss-loader": "^4.1.0", 29 | "react": "^17.0.1", 30 | "react-dom": "^17.0.1", 31 | "react-dropzone": "^11.2.4", 32 | "style-loader": "^1.2.1", 33 | "tailwindcss": "^2.2.15", 34 | "ts-loader": "^7.0.5", 35 | "typescript": "^3.9.6", 36 | "url-loader": "^4.1.0", 37 | "webpack": "^4.43.0", 38 | "webpack-cli": "^3.3.12" 39 | }, 40 | "devDependencies": { 41 | "@fullhuman/postcss-purgecss": "4.0.3", 42 | "@tailwindcss/forms": "^0.3.3", 43 | "@types/jest": "27.0.1", 44 | "@types/node": "14.0.14", 45 | "@types/react": "^17.0.0", 46 | "@types/react-dom": "^17.0.0", 47 | "cssnano": "^5.0.8", 48 | "jest": "26.1.0", 49 | "postcss": "^8.3.6", 50 | "ts-jest": "26.1.1", 51 | "webpack-dev-server": "3.11.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/@types/core.d.ts: -------------------------------------------------------------------------------- 1 | interface PluginMessage { 2 | type: string; 3 | payload: object; 4 | } 5 | 6 | interface TailwindColor { 7 | name: string; 8 | value: RGBA; 9 | } 10 | interface AddColorsPayload { 11 | prefix: string; 12 | config: Array; 13 | overrideStyles: boolean; 14 | addSpaces: boolean; 15 | } 16 | -------------------------------------------------------------------------------- /src/@types/figma.d.ts: -------------------------------------------------------------------------------- 1 | // Global variable with Figma's plugin API. 2 | declare const figma: PluginAPI; 3 | declare const __html__: string; 4 | 5 | interface PluginAPI { 6 | readonly apiVersion: "1.0.0"; 7 | readonly command: string; 8 | readonly root: DocumentNode; 9 | readonly viewport: ViewportAPI; 10 | closePlugin(message?: string): void; 11 | 12 | showUI(html: string, options?: ShowUIOptions): void; 13 | readonly ui: UIAPI; 14 | 15 | readonly clientStorage: ClientStorageAPI; 16 | 17 | getNodeById(id: string): BaseNode | null; 18 | getStyleById(id: string): BaseStyle | null; 19 | 20 | currentPage: PageNode; 21 | 22 | readonly mixed: symbol; 23 | 24 | createRectangle(): RectangleNode; 25 | createLine(): LineNode; 26 | createEllipse(): EllipseNode; 27 | createPolygon(): PolygonNode; 28 | createStar(): StarNode; 29 | createVector(): VectorNode; 30 | createText(): TextNode; 31 | createBooleanOperation(): BooleanOperationNode; 32 | createFrame(): FrameNode; 33 | createComponent(): ComponentNode; 34 | createPage(): PageNode; 35 | createSlice(): SliceNode; 36 | 37 | createPaintStyle(): PaintStyle; 38 | createTextStyle(): TextStyle; 39 | createEffectStyle(): EffectStyle; 40 | createGridStyle(): GridStyle; 41 | 42 | getLocalPaintStyles(): PaintStyle[]; 43 | 44 | importComponentByKeyAsync(key: string): Promise; 45 | importStyleByKeyAsync(key: string): Promise; 46 | 47 | listAvailableFontsAsync(): Promise; 48 | loadFontAsync(fontName: FontName): Promise; 49 | readonly hasMissingFont: boolean; 50 | 51 | createNodeFromSvg(svg: string): FrameNode; 52 | 53 | createImage(data: Uint8Array): Image; 54 | getImageByHash(hash: string): Image; 55 | 56 | group( 57 | nodes: ReadonlyArray, 58 | parent: BaseNode & ChildrenMixin, 59 | index?: number 60 | ): FrameNode; 61 | flatten( 62 | nodes: ReadonlyArray, 63 | parent?: BaseNode & ChildrenMixin, 64 | index?: number 65 | ): VectorNode; 66 | } 67 | 68 | interface ClientStorageAPI { 69 | getAsync(key: string): Promise; 70 | setAsync(key: string, value: any): Promise; 71 | } 72 | 73 | type ShowUIOptions = { 74 | visible?: boolean; 75 | width?: number; 76 | height?: number; 77 | }; 78 | 79 | type UIPostMessageOptions = { 80 | targetOrigin?: string; 81 | }; 82 | 83 | type OnMessageProperties = { 84 | sourceOrigin: string; 85 | }; 86 | 87 | interface UIAPI { 88 | show(): void; 89 | hide(): void; 90 | resize(width: number, height: number): void; 91 | close(): void; 92 | 93 | postMessage(pluginMessage: any, options?: UIPostMessageOptions): void; 94 | onmessage: 95 | | ((pluginMessage: any, props: OnMessageProperties) => void) 96 | | undefined; 97 | } 98 | 99 | interface ViewportAPI { 100 | center: { x: number; y: number }; 101 | zoom: number; 102 | scrollAndZoomIntoView(nodes: ReadonlyArray); 103 | } 104 | 105 | //////////////////////////////////////////////////////////////////////////////// 106 | // Datatypes 107 | 108 | type Transform = [[number, number, number], [number, number, number]]; 109 | 110 | interface Vector { 111 | readonly x: number; 112 | readonly y: number; 113 | } 114 | 115 | interface RGB { 116 | readonly r: number; 117 | readonly g: number; 118 | readonly b: number; 119 | } 120 | 121 | interface RGBA { 122 | readonly r: number; 123 | readonly g: number; 124 | readonly b: number; 125 | readonly a: number; 126 | } 127 | 128 | interface FontName { 129 | readonly family: string; 130 | readonly style: string; 131 | } 132 | 133 | type TextCase = "ORIGINAL" | "UPPER" | "LOWER" | "TITLE"; 134 | 135 | type TextDecoration = "NONE" | "UNDERLINE" | "STRIKETHROUGH"; 136 | 137 | interface ArcData { 138 | readonly startingAngle: number; 139 | readonly endingAngle: number; 140 | readonly innerRadius: number; 141 | } 142 | 143 | interface ShadowEffect { 144 | readonly type: "DROP_SHADOW" | "INNER_SHADOW"; 145 | readonly color: RGBA; 146 | readonly offset: Vector; 147 | readonly radius: number; 148 | readonly visible: boolean; 149 | readonly blendMode: BlendMode; 150 | } 151 | 152 | interface BlurEffect { 153 | readonly type: "LAYER_BLUR" | "BACKGROUND_BLUR"; 154 | readonly radius: number; 155 | readonly visible: boolean; 156 | } 157 | 158 | type Effect = ShadowEffect | BlurEffect; 159 | 160 | type ConstraintType = "MIN" | "CENTER" | "MAX" | "STRETCH" | "SCALE"; 161 | 162 | interface Constraints { 163 | readonly horizontal: ConstraintType; 164 | readonly vertical: ConstraintType; 165 | } 166 | 167 | interface ColorStop { 168 | readonly position: number; 169 | readonly color: RGBA; 170 | } 171 | 172 | interface ImageFilters { 173 | exposure?: number; 174 | contrast?: number; 175 | saturation?: number; 176 | temperature?: number; 177 | tint?: number; 178 | highlights?: number; 179 | shadows?: number; 180 | } 181 | 182 | interface SolidPaint { 183 | readonly type: "SOLID"; 184 | readonly color: RGB; 185 | 186 | readonly visible?: boolean; 187 | readonly opacity?: number; 188 | readonly blendMode?: BlendMode; 189 | } 190 | 191 | interface GradientPaint { 192 | readonly type: 193 | | "GRADIENT_LINEAR" 194 | | "GRADIENT_RADIAL" 195 | | "GRADIENT_ANGULAR" 196 | | "GRADIENT_DIAMOND"; 197 | readonly gradientTransform: Transform; 198 | readonly gradientStops: ReadonlyArray; 199 | 200 | readonly visible?: boolean; 201 | readonly opacity?: number; 202 | readonly blendMode?: BlendMode; 203 | } 204 | 205 | interface ImagePaint { 206 | readonly type: "IMAGE"; 207 | readonly scaleMode: "FILL" | "FIT" | "CROP" | "TILE"; 208 | readonly imageHash: string | null; 209 | readonly imageTransform?: Transform; // setting for "CROP" 210 | readonly scalingFactor?: number; // setting for "TILE" 211 | readonly filters?: ImageFilters; 212 | 213 | readonly visible?: boolean; 214 | readonly opacity?: number; 215 | readonly blendMode?: BlendMode; 216 | } 217 | 218 | type Paint = SolidPaint | GradientPaint | ImagePaint; 219 | 220 | interface Guide { 221 | readonly axis: "X" | "Y"; 222 | readonly offset: number; 223 | } 224 | 225 | interface RowsColsLayoutGrid { 226 | readonly pattern: "ROWS" | "COLUMNS"; 227 | readonly alignment: "MIN" | "MAX" | "STRETCH" | "CENTER"; 228 | readonly gutterSize: number; 229 | 230 | readonly count: number; // Infinity when "Auto" is set in the UI 231 | readonly sectionSize?: number; // Not set for alignment: "STRETCH" 232 | readonly offset?: number; // Not set for alignment: "CENTER" 233 | 234 | readonly visible?: boolean; 235 | readonly color?: RGBA; 236 | } 237 | 238 | interface GridLayoutGrid { 239 | readonly pattern: "GRID"; 240 | readonly sectionSize: number; 241 | 242 | readonly visible?: boolean; 243 | readonly color?: RGBA; 244 | } 245 | 246 | type LayoutGrid = RowsColsLayoutGrid | GridLayoutGrid; 247 | 248 | interface ExportSettingsConstraints { 249 | type: "SCALE" | "WIDTH" | "HEIGHT"; 250 | value: number; 251 | } 252 | 253 | interface ExportSettingsImage { 254 | format: "JPG" | "PNG"; 255 | contentsOnly?: boolean; // defaults to true 256 | suffix?: string; 257 | constraint?: ExportSettingsConstraints; 258 | } 259 | 260 | interface ExportSettingsSVG { 261 | format: "SVG"; 262 | contentsOnly?: boolean; // defaults to true 263 | suffix?: string; 264 | svgOutlineText?: boolean; // defaults to true 265 | svgIdAttribute?: boolean; // defaults to false 266 | svgSimplifyStroke?: boolean; // defaults to true 267 | } 268 | 269 | interface ExportSettingsPDF { 270 | format: "PDF"; 271 | contentsOnly?: boolean; // defaults to true 272 | suffix?: string; 273 | } 274 | 275 | type ExportSettings = 276 | | ExportSettingsImage 277 | | ExportSettingsSVG 278 | | ExportSettingsPDF; 279 | 280 | type WindingRule = "NONZERO" | "EVENODD"; 281 | 282 | interface VectorVertex { 283 | readonly x: number; 284 | readonly y: number; 285 | readonly strokeCap?: StrokeCap; 286 | readonly strokeJoin?: StrokeJoin; 287 | readonly cornerRadius?: number; 288 | readonly handleMirroring?: HandleMirroring; 289 | } 290 | 291 | interface VectorSegment { 292 | readonly start: number; 293 | readonly end: number; 294 | readonly tangentStart?: Vector; // Defaults to { x: 0, y: 0 } 295 | readonly tangentEnd?: Vector; // Defaults to { x: 0, y: 0 } 296 | } 297 | 298 | interface VectorRegion { 299 | readonly windingRule: WindingRule; 300 | readonly loops: ReadonlyArray>; 301 | } 302 | 303 | interface VectorNetwork { 304 | readonly vertices: ReadonlyArray; 305 | readonly segments: ReadonlyArray; 306 | readonly regions?: ReadonlyArray; // Defaults to [] 307 | } 308 | 309 | interface VectorPath { 310 | readonly windingRule: WindingRule | "NONE"; 311 | readonly data: string; 312 | } 313 | 314 | type VectorPaths = ReadonlyArray; 315 | 316 | type LetterSpacing = { 317 | readonly value: number; 318 | readonly unit: "PIXELS" | "PERCENT"; 319 | }; 320 | 321 | type LineHeight = 322 | | { 323 | readonly value: number; 324 | readonly unit: "PIXELS" | "PERCENT"; 325 | } 326 | | { 327 | readonly unit: "AUTO"; 328 | }; 329 | 330 | type BlendMode = 331 | | "PASS_THROUGH" 332 | | "NORMAL" 333 | | "DARKEN" 334 | | "MULTIPLY" 335 | | "LINEAR_BURN" 336 | | "COLOR_BURN" 337 | | "LIGHTEN" 338 | | "SCREEN" 339 | | "LINEAR_DODGE" 340 | | "COLOR_DODGE" 341 | | "OVERLAY" 342 | | "SOFT_LIGHT" 343 | | "HARD_LIGHT" 344 | | "DIFFERENCE" 345 | | "EXCLUSION" 346 | | "HUE" 347 | | "SATURATION" 348 | | "COLOR" 349 | | "LUMINOSITY"; 350 | 351 | interface Font { 352 | fontName: FontName; 353 | } 354 | 355 | //////////////////////////////////////////////////////////////////////////////// 356 | // Mixins 357 | 358 | interface BaseNodeMixin { 359 | readonly id: string; 360 | readonly parent: (BaseNode & ChildrenMixin) | null; 361 | name: string; // Note: setting this also sets \`autoRename\` to false on TextNodes 362 | readonly removed: boolean; 363 | toString(): string; 364 | remove(): void; 365 | 366 | getPluginData(key: string): string; 367 | setPluginData(key: string, value: string): void; 368 | 369 | // Namespace is a string that must be at least 3 alphanumeric characters, and should 370 | // be a name related to your plugin. Other plugins will be able to read this data. 371 | getSharedPluginData(namespace: string, key: string): string; 372 | setSharedPluginData(namespace: string, key: string, value: string): void; 373 | } 374 | 375 | interface SceneNodeMixin { 376 | visible: boolean; 377 | locked: boolean; 378 | } 379 | 380 | interface ChildrenMixin { 381 | readonly children: ReadonlyArray; 382 | 383 | appendChild(child: BaseNode): void; 384 | insertChild(index: number, child: BaseNode): void; 385 | 386 | findAll(callback?: (node: BaseNode) => boolean): ReadonlyArray; 387 | findOne(callback: (node: BaseNode) => boolean): BaseNode | null; 388 | } 389 | 390 | interface ConstraintMixin { 391 | constraints: Constraints; 392 | } 393 | 394 | interface LayoutMixin { 395 | readonly absoluteTransform: Transform; 396 | relativeTransform: Transform; 397 | x: number; 398 | y: number; 399 | rotation: number; // In degrees 400 | 401 | readonly width: number; 402 | readonly height: number; 403 | 404 | resize(width: number, height: number): void; 405 | resizeWithoutConstraints(width: number, height: number): void; 406 | } 407 | 408 | interface BlendMixin { 409 | opacity: number; 410 | blendMode: BlendMode; 411 | isMask: boolean; 412 | effects: ReadonlyArray; 413 | effectStyleId: string; 414 | } 415 | 416 | interface FrameMixin { 417 | backgrounds: ReadonlyArray; 418 | layoutGrids: ReadonlyArray; 419 | clipsContent: boolean; 420 | guides: ReadonlyArray; 421 | gridStyleId: string; 422 | backgroundStyleId: string; 423 | } 424 | 425 | type StrokeCap = 426 | | "NONE" 427 | | "ROUND" 428 | | "SQUARE" 429 | | "ARROW_LINES" 430 | | "ARROW_EQUILATERAL"; 431 | type StrokeJoin = "MITER" | "BEVEL" | "ROUND"; 432 | type HandleMirroring = "NONE" | "ANGLE" | "ANGLE_AND_LENGTH"; 433 | 434 | interface GeometryMixin { 435 | fills: ReadonlyArray | symbol; 436 | strokes: ReadonlyArray; 437 | strokeWeight: number; 438 | strokeAlign: "CENTER" | "INSIDE" | "OUTSIDE"; 439 | strokeCap: StrokeCap | symbol; 440 | strokeJoin: StrokeJoin | symbol; 441 | dashPattern: ReadonlyArray; 442 | fillStyleId: string | symbol; 443 | strokeStyleId: string; 444 | } 445 | 446 | interface CornerMixin { 447 | cornerRadius: number | symbol; 448 | cornerSmoothing: number; 449 | } 450 | 451 | interface ExportMixin { 452 | exportSettings: ExportSettings[]; 453 | exportAsync(settings?: ExportSettings): Promise; // Defaults to PNG format 454 | } 455 | 456 | interface DefaultShapeMixin 457 | extends BaseNodeMixin, 458 | SceneNodeMixin, 459 | BlendMixin, 460 | GeometryMixin, 461 | LayoutMixin, 462 | ExportMixin {} 463 | 464 | interface DefaultContainerMixin 465 | extends BaseNodeMixin, 466 | SceneNodeMixin, 467 | ChildrenMixin, 468 | FrameMixin, 469 | BlendMixin, 470 | ConstraintMixin, 471 | LayoutMixin, 472 | ExportMixin {} 473 | 474 | //////////////////////////////////////////////////////////////////////////////// 475 | // Nodes 476 | 477 | interface DocumentNode extends BaseNodeMixin, ChildrenMixin { 478 | readonly type: "DOCUMENT"; 479 | } 480 | 481 | interface PageNode extends BaseNodeMixin, ChildrenMixin, ExportMixin { 482 | readonly type: "PAGE"; 483 | clone(): PageNode; 484 | 485 | guides: ReadonlyArray; 486 | selection: ReadonlyArray; 487 | } 488 | 489 | interface FrameNode extends DefaultContainerMixin { 490 | readonly type: "FRAME" | "GROUP"; 491 | clone(): FrameNode; 492 | } 493 | 494 | interface SliceNode 495 | extends BaseNodeMixin, 496 | SceneNodeMixin, 497 | LayoutMixin, 498 | ExportMixin { 499 | readonly type: "SLICE"; 500 | clone(): SliceNode; 501 | } 502 | 503 | interface RectangleNode 504 | extends DefaultShapeMixin, 505 | ConstraintMixin, 506 | CornerMixin { 507 | readonly type: "RECTANGLE"; 508 | clone(): RectangleNode; 509 | topLeftRadius: number; 510 | topRightRadius: number; 511 | bottomLeftRadius: number; 512 | bottomRightRadius: number; 513 | } 514 | 515 | interface LineNode extends DefaultShapeMixin, ConstraintMixin { 516 | readonly type: "LINE"; 517 | clone(): LineNode; 518 | } 519 | 520 | interface EllipseNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 521 | readonly type: "ELLIPSE"; 522 | clone(): EllipseNode; 523 | arcData: ArcData; 524 | } 525 | 526 | interface PolygonNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 527 | readonly type: "POLYGON"; 528 | clone(): PolygonNode; 529 | pointCount: number; 530 | } 531 | 532 | interface StarNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 533 | readonly type: "STAR"; 534 | clone(): StarNode; 535 | pointCount: number; 536 | innerRadius: number; 537 | } 538 | 539 | interface VectorNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 540 | readonly type: "VECTOR"; 541 | clone(): VectorNode; 542 | vectorNetwork: VectorNetwork; 543 | vectorPaths: VectorPaths; 544 | handleMirroring: HandleMirroring | symbol; 545 | } 546 | 547 | interface TextNode extends DefaultShapeMixin, ConstraintMixin { 548 | readonly type: "TEXT"; 549 | clone(): TextNode; 550 | characters: string; 551 | readonly hasMissingFont: boolean; 552 | textAlignHorizontal: "LEFT" | "CENTER" | "RIGHT" | "JUSTIFIED"; 553 | textAlignVertical: "TOP" | "CENTER" | "BOTTOM"; 554 | textAutoResize: "NONE" | "WIDTH_AND_HEIGHT" | "HEIGHT"; 555 | paragraphIndent: number; 556 | paragraphSpacing: number; 557 | autoRename: boolean; 558 | 559 | textStyleId: string | symbol; 560 | fontSize: number | symbol; 561 | fontName: FontName | symbol; 562 | textCase: TextCase | symbol; 563 | textDecoration: TextDecoration | symbol; 564 | letterSpacing: LetterSpacing | symbol; 565 | lineHeight: LineHeight | symbol; 566 | 567 | getRangeFontSize(start: number, end: number): number | symbol; 568 | setRangeFontSize(start: number, end: number, value: number): void; 569 | getRangeFontName(start: number, end: number): FontName | symbol; 570 | setRangeFontName(start: number, end: number, value: FontName): void; 571 | getRangeTextCase(start: number, end: number): TextCase | symbol; 572 | setRangeTextCase(start: number, end: number, value: TextCase): void; 573 | getRangeTextDecoration(start: number, end: number): TextDecoration | symbol; 574 | setRangeTextDecoration( 575 | start: number, 576 | end: number, 577 | value: TextDecoration 578 | ): void; 579 | getRangeLetterSpacing(start: number, end: number): LetterSpacing | symbol; 580 | setRangeLetterSpacing(start: number, end: number, value: LetterSpacing): void; 581 | getRangeLineHeight(start: number, end: number): LineHeight | symbol; 582 | setRangeLineHeight(start: number, end: number, value: LineHeight): void; 583 | getRangeFills(start: number, end: number): Paint[] | symbol; 584 | setRangeFills(start: number, end: number, value: Paint[]): void; 585 | getRangeTextStyleId(start: number, end: number): string | symbol; 586 | setRangeTextStyleId(start: number, end: number, value: string): void; 587 | getRangeFillStyleId(start: number, end: number): string | symbol; 588 | setRangeFillStyleId(start: number, end: number, value: string): void; 589 | } 590 | 591 | interface ComponentNode extends DefaultContainerMixin { 592 | readonly type: "COMPONENT"; 593 | clone(): ComponentNode; 594 | 595 | createInstance(): InstanceNode; 596 | description: string; 597 | readonly remote: boolean; 598 | readonly key: string; // The key to use with "importComponentByKeyAsync" 599 | } 600 | 601 | interface InstanceNode extends DefaultContainerMixin { 602 | readonly type: "INSTANCE"; 603 | clone(): InstanceNode; 604 | masterComponent: ComponentNode; 605 | } 606 | 607 | interface BooleanOperationNode 608 | extends DefaultShapeMixin, 609 | ChildrenMixin, 610 | CornerMixin { 611 | readonly type: "BOOLEAN_OPERATION"; 612 | clone(): BooleanOperationNode; 613 | booleanOperation: "UNION" | "INTERSECT" | "SUBTRACT" | "EXCLUDE"; 614 | } 615 | 616 | type BaseNode = DocumentNode | PageNode | SceneNode; 617 | 618 | type SceneNode = 619 | | SliceNode 620 | | FrameNode 621 | | ComponentNode 622 | | InstanceNode 623 | | BooleanOperationNode 624 | | VectorNode 625 | | StarNode 626 | | LineNode 627 | | EllipseNode 628 | | PolygonNode 629 | | RectangleNode 630 | | TextNode; 631 | 632 | type NodeType = 633 | | "DOCUMENT" 634 | | "PAGE" 635 | | "SLICE" 636 | | "FRAME" 637 | | "GROUP" 638 | | "COMPONENT" 639 | | "INSTANCE" 640 | | "BOOLEAN_OPERATION" 641 | | "VECTOR" 642 | | "STAR" 643 | | "LINE" 644 | | "ELLIPSE" 645 | | "POLYGON" 646 | | "RECTANGLE" 647 | | "TEXT"; 648 | 649 | //////////////////////////////////////////////////////////////////////////////// 650 | // Styles 651 | type StyleType = "PAINT" | "TEXT" | "EFFECT" | "GRID"; 652 | 653 | interface BaseStyle { 654 | readonly id: string; 655 | readonly type: StyleType; 656 | name: string; 657 | description: string; 658 | remote: boolean; 659 | readonly key: string; // The key to use with "importStyleByKeyAsync" 660 | remove(): void; 661 | } 662 | 663 | interface PaintStyle extends BaseStyle { 664 | type: "PAINT"; 665 | paints: ReadonlyArray; 666 | } 667 | 668 | interface TextStyle extends BaseStyle { 669 | type: "TEXT"; 670 | fontSize: number; 671 | textDecoration: TextDecoration; 672 | fontName: FontName; 673 | letterSpacing: LetterSpacing; 674 | lineHeight: LineHeight; 675 | paragraphIndent: number; 676 | paragraphSpacing: number; 677 | textCase: TextCase; 678 | } 679 | 680 | interface EffectStyle extends BaseStyle { 681 | type: "EFFECT"; 682 | effects: ReadonlyArray; 683 | } 684 | 685 | interface GridStyle extends BaseStyle { 686 | type: "GRID"; 687 | layoutGrids: ReadonlyArray; 688 | } 689 | 690 | //////////////////////////////////////////////////////////////////////////////// 691 | // Other 692 | 693 | interface Image { 694 | readonly hash: string; 695 | getBytesAsync(): Promise; 696 | } 697 | -------------------------------------------------------------------------------- /src/@types/tailwind.d.ts: -------------------------------------------------------------------------------- 1 | export interface TailwindConfig { 2 | theme: Theme; 3 | variants: { [K in keyof Theme]?: string[] }; 4 | purge?: any[]; 5 | plugins: any[]; 6 | target?: string; 7 | prefix?: string; 8 | important?: boolean; 9 | separator?: string; 10 | corePlugins?: string[]; 11 | darkMode: boolean; 12 | presets: any[]; 13 | variantOrder: string[]; 14 | } 15 | export interface Theme { 16 | extend?: Theme; 17 | screens?: KeyConfig; 18 | colors?: KeyConfig; 19 | spacing?: KeyConfig; 20 | backgroundColor?: KeyConfig; 21 | backgroundOpacity?: KeyConfig; 22 | backgroundPosition?: KeyConfig; 23 | backgroundSize?: KeyConfig; 24 | borderColor?: KeyConfig; 25 | borderOpacity?: KeyConfig; 26 | borderRadius?: KeyConfig; 27 | borderWidth?: KeyConfig; 28 | boxShadow?: KeyConfig; 29 | container?: KeyConfig; 30 | cursor?: KeyConfig; 31 | divideColor?: KeyConfig; 32 | divideOpacity?: KeyConfig; 33 | divideWidth?: KeyConfig; 34 | fill?: KeyConfig; 35 | flex?: KeyConfig; 36 | flexGrow?: KeyConfig; 37 | flexShrink?: KeyConfig; 38 | fontFamily?: KeyConfig; 39 | fontSize?: KeyConfig; 40 | fontWeight?: KeyConfig; 41 | height?: KeyConfig; 42 | inset?: KeyConfig; 43 | letterSpacing?: KeyConfig; 44 | lineHeight?: KeyConfig; 45 | listStyleType?: KeyConfig; 46 | margin?: KeyConfig; 47 | maxHeight?: KeyConfig; 48 | maxWidth?: KeyConfig; 49 | minHeight?: KeyConfig; 50 | minWidth?: KeyConfig; 51 | objectPosition?: KeyConfig; 52 | opacity?: KeyConfig; 53 | order?: KeyConfig; 54 | padding?: KeyConfig; 55 | placeholderColor?: KeyConfig; 56 | placeholderOpacity?: KeyConfig; 57 | space?: KeyConfig; 58 | stroke?: KeyConfig; 59 | strokeWidth?: KeyConfig; 60 | textColor?: KeyConfig; 61 | textOpacity?: KeyConfig; 62 | width?: KeyConfig; 63 | zIndex?: KeyConfig; 64 | gap?: KeyConfig; 65 | gridTemplateColumns?: KeyConfig; 66 | gridColumn?: KeyConfig; 67 | gridColumnStart?: KeyConfig; 68 | gridColumnEnd?: KeyConfig; 69 | gridTemplateRows?: KeyConfig; 70 | gridRow?: KeyConfig; 71 | gridRowStart?: KeyConfig; 72 | gridRowEnd?: KeyConfig; 73 | transformOrigin?: KeyConfig; 74 | scale?: KeyConfig; 75 | rotate?: KeyConfig; 76 | translate?: KeyConfig; 77 | skew?: KeyConfig; 78 | transitionProperty?: KeyConfig; 79 | transitionTimingFunction?: KeyConfig; 80 | transitionDuration?: KeyConfig; 81 | transitionDelay?: KeyConfig; 82 | } 83 | export interface KeyConfig { 84 | [key: string]: string | { [key: string]: any }; 85 | } 86 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import ColorView from "./components/ColorView"; 4 | import FileUpload from "./components/FileUpload"; 5 | import Footer from "./components/Footer"; 6 | import { Provider, rootStore } from "./models/Root"; 7 | import "./resources/css/ui.css"; 8 | 9 | interface Props {} 10 | 11 | const App = (props: Props) => { 12 | return ( 13 | 14 |
15 | 16 |
17 | 18 |
19 |
20 |
21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | ReactDOM.render(, document.getElementById("react-page")); 28 | -------------------------------------------------------------------------------- /src/__tests__/colors.test.ts: -------------------------------------------------------------------------------- 1 | import * as colors from "../core/colors"; 2 | import { fetchConfigColors } from "../core/config"; 3 | import defaultConfig from "../generated/tw.min.json"; 4 | 5 | let fetchedColors = fetchConfigColors(defaultConfig); 6 | 7 | describe("When addColors is called", () => { 8 | it("should return true", () => { 9 | const handleSolidColorMock = jest.spyOn(colors, "handleSolidColor"); 10 | handleSolidColorMock.mockImplementation(() => "figma api"); 11 | expect( 12 | colors.addColors( 13 | { 14 | prefix: "", 15 | config: fetchedColors, 16 | overrideStyles: false, 17 | addSpaces: false, 18 | }, 19 | [] 20 | ) 21 | ).toBe(true); 22 | // Would not be called if Figma API returns error 23 | expect(handleSolidColorMock).toHaveBeenCalledTimes(fetchedColors.length); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/__tests__/config.test.ts: -------------------------------------------------------------------------------- 1 | import { convertConfigColor, fetchConfigColors } from "../core/config"; 2 | import defaultConfig from "../generated/tw.min.json"; 3 | 4 | const colors = { 5 | transparent: "transparent", 6 | hex6: "#E53E3E", 7 | hex8: "#E53E3E80", 8 | rgb: "rgb(56,161,105)", 9 | rgba: "rgba(49,151,149, 0.5)", 10 | hsl: "hsl(170, 45%, 45%)", 11 | hsla: "hsla(170, 45%, 45%, 0.5)", 12 | }; 13 | 14 | describe("fetchConfigColor", () => { 15 | describe("reading a valid config", () => { 16 | it("should return an object with color name and value", () => { 17 | const fetchedColors = fetchConfigColors(defaultConfig); 18 | expect(fetchedColors[0]).toHaveProperty("name"); 19 | expect(fetchedColors[0]).toHaveProperty("value"); 20 | }); 21 | }); 22 | }); 23 | 24 | describe("convertConfigColor", () => { 25 | describe("Passing a valid config color", () => { 26 | let currentColor: RGBA; 27 | it("should return object from transparent string", () => { 28 | currentColor = convertConfigColor(colors["transparent"]); 29 | // Check if valid object 30 | expect(currentColor).toHaveProperty("r"); 31 | expect(currentColor).toHaveProperty("g"); 32 | expect(currentColor).toHaveProperty("b"); 33 | expect(currentColor).toHaveProperty("a"); 34 | // Check if valid values 35 | expect(currentColor["r"]).toBeGreaterThanOrEqual(0); 36 | expect(currentColor["r"]).toBeLessThanOrEqual(1); 37 | expect(currentColor["g"]).toBeGreaterThanOrEqual(0); 38 | expect(currentColor["g"]).toBeLessThanOrEqual(1); 39 | expect(currentColor["b"]).toBeGreaterThanOrEqual(0); 40 | expect(currentColor["b"]).toBeLessThanOrEqual(1); 41 | expect(currentColor["a"]).toBeGreaterThanOrEqual(0); 42 | expect(currentColor["a"]).toBeLessThanOrEqual(1); 43 | }); 44 | 45 | it("should return object from hex6", () => { 46 | currentColor = convertConfigColor(colors["hex6"]); 47 | // Check if valid object 48 | expect(currentColor).toHaveProperty("r"); 49 | expect(currentColor).toHaveProperty("g"); 50 | expect(currentColor).toHaveProperty("b"); 51 | expect(currentColor).toHaveProperty("a"); 52 | // Check if valid values 53 | expect(currentColor["r"]).toBeGreaterThanOrEqual(0); 54 | expect(currentColor["r"]).toBeLessThanOrEqual(1); 55 | expect(currentColor["g"]).toBeGreaterThanOrEqual(0); 56 | expect(currentColor["g"]).toBeLessThanOrEqual(1); 57 | expect(currentColor["b"]).toBeGreaterThanOrEqual(0); 58 | expect(currentColor["b"]).toBeLessThanOrEqual(1); 59 | expect(currentColor["a"]).toBeGreaterThanOrEqual(0); 60 | expect(currentColor["a"]).toBeLessThanOrEqual(1); 61 | }); 62 | 63 | it("should return object from hex8", () => { 64 | currentColor = convertConfigColor(colors["hex8"]); 65 | // Check if valid object 66 | expect(currentColor).toHaveProperty("r"); 67 | expect(currentColor).toHaveProperty("g"); 68 | expect(currentColor).toHaveProperty("b"); 69 | expect(currentColor).toHaveProperty("a"); 70 | // Check if valid values 71 | expect(currentColor["r"]).toBeGreaterThanOrEqual(0); 72 | expect(currentColor["r"]).toBeLessThanOrEqual(1); 73 | expect(currentColor["g"]).toBeGreaterThanOrEqual(0); 74 | expect(currentColor["g"]).toBeLessThanOrEqual(1); 75 | expect(currentColor["b"]).toBeGreaterThanOrEqual(0); 76 | expect(currentColor["b"]).toBeLessThanOrEqual(1); 77 | expect(currentColor["a"]).toBeGreaterThanOrEqual(0); 78 | expect(currentColor["a"]).toBeLessThanOrEqual(1); 79 | }); 80 | 81 | it("should return object from rgb", () => { 82 | currentColor = convertConfigColor(colors["rgb"]); 83 | // Check if valid object 84 | expect(currentColor).toHaveProperty("r"); 85 | expect(currentColor).toHaveProperty("g"); 86 | expect(currentColor).toHaveProperty("b"); 87 | expect(currentColor).toHaveProperty("a"); 88 | // Check if valid values 89 | expect(currentColor["r"]).toBeGreaterThanOrEqual(0); 90 | expect(currentColor["r"]).toBeLessThanOrEqual(1); 91 | expect(currentColor["g"]).toBeGreaterThanOrEqual(0); 92 | expect(currentColor["g"]).toBeLessThanOrEqual(1); 93 | expect(currentColor["b"]).toBeGreaterThanOrEqual(0); 94 | expect(currentColor["b"]).toBeLessThanOrEqual(1); 95 | expect(currentColor["a"]).toBeGreaterThanOrEqual(0); 96 | expect(currentColor["a"]).toBeLessThanOrEqual(1); 97 | }); 98 | 99 | it("should return object from rgba", () => { 100 | currentColor = convertConfigColor(colors["rgba"]); 101 | // Check if valid object 102 | expect(currentColor).toHaveProperty("r"); 103 | expect(currentColor).toHaveProperty("g"); 104 | expect(currentColor).toHaveProperty("b"); 105 | expect(currentColor).toHaveProperty("a"); 106 | // Check if valid values 107 | expect(currentColor["r"]).toBeGreaterThanOrEqual(0); 108 | expect(currentColor["r"]).toBeLessThanOrEqual(1); 109 | expect(currentColor["g"]).toBeGreaterThanOrEqual(0); 110 | expect(currentColor["g"]).toBeLessThanOrEqual(1); 111 | expect(currentColor["b"]).toBeGreaterThanOrEqual(0); 112 | expect(currentColor["b"]).toBeLessThanOrEqual(1); 113 | expect(currentColor["a"]).toBeGreaterThanOrEqual(0); 114 | expect(currentColor["a"]).toBeLessThanOrEqual(1); 115 | }); 116 | 117 | it("should return object from hsl", () => { 118 | currentColor = convertConfigColor(colors["hsl"]); 119 | // Check if valid object 120 | expect(currentColor).toHaveProperty("r"); 121 | expect(currentColor).toHaveProperty("g"); 122 | expect(currentColor).toHaveProperty("b"); 123 | expect(currentColor).toHaveProperty("a"); 124 | // Check if valid values 125 | expect(currentColor["r"]).toBeGreaterThanOrEqual(0); 126 | expect(currentColor["r"]).toBeLessThanOrEqual(1); 127 | expect(currentColor["g"]).toBeGreaterThanOrEqual(0); 128 | expect(currentColor["g"]).toBeLessThanOrEqual(1); 129 | expect(currentColor["b"]).toBeGreaterThanOrEqual(0); 130 | expect(currentColor["b"]).toBeLessThanOrEqual(1); 131 | expect(currentColor["a"]).toBeGreaterThanOrEqual(0); 132 | expect(currentColor["a"]).toBeLessThanOrEqual(1); 133 | }); 134 | 135 | it("should return object from hsla", () => { 136 | currentColor = convertConfigColor(colors["hsla"]); 137 | // Check if valid object 138 | expect(currentColor).toHaveProperty("r"); 139 | expect(currentColor).toHaveProperty("g"); 140 | expect(currentColor).toHaveProperty("b"); 141 | expect(currentColor).toHaveProperty("a"); 142 | // Check if valid values 143 | expect(currentColor["r"]).toBeGreaterThanOrEqual(0); 144 | expect(currentColor["r"]).toBeLessThanOrEqual(1); 145 | expect(currentColor["g"]).toBeGreaterThanOrEqual(0); 146 | expect(currentColor["g"]).toBeLessThanOrEqual(1); 147 | expect(currentColor["b"]).toBeGreaterThanOrEqual(0); 148 | expect(currentColor["b"]).toBeLessThanOrEqual(1); 149 | expect(currentColor["a"]).toBeGreaterThanOrEqual(0); 150 | expect(currentColor["a"]).toBeLessThanOrEqual(1); 151 | }); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /src/code.ts: -------------------------------------------------------------------------------- 1 | import { addColors } from "./core/colors"; 2 | 3 | let windowSize = { 4 | width: 300, 5 | height: 290, 6 | }; 7 | figma.showUI(__html__, windowSize); 8 | 9 | interface AddColorsMessage { 10 | type: "ADD_COLORS"; 11 | payload: AddColorsPayload; 12 | } 13 | 14 | let styles: Array = []; 15 | 16 | styles = figma.getLocalPaintStyles().map((style) => style); 17 | 18 | export type PluginMessage = AddColorsMessage; 19 | 20 | figma.ui.onmessage = (pluginMessage: PluginMessage) => { 21 | const { type, payload } = pluginMessage; 22 | if (type === "ADD_COLORS") { 23 | console.log("adding color"); 24 | addColors(payload, styles); 25 | } 26 | figma.closePlugin(); 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/ColorView.tsx: -------------------------------------------------------------------------------- 1 | import cx from "classnames"; 2 | import { observer } from "mobx-react-lite"; 3 | import React, { useState } from "react"; 4 | import { useMst } from "../models/Root"; 5 | 6 | interface Props {} 7 | 8 | const ColorView = observer(({}: Props) => { 9 | const { 10 | addColorStyles, 11 | loadedTailwindConfig, 12 | loadDefaultStub, 13 | addSpaces, 14 | setAddSpaces, 15 | overrideStyles, 16 | setOverrideStyles, 17 | } = useMst(); 18 | const [prefix, setPrefix] = useState(""); 19 | 20 | const hasAvailableColorStyles = () => { 21 | return ( 22 | loadedTailwindConfig.theme.colors || loadedTailwindConfig.theme.extend 23 | ); 24 | }; 25 | 26 | return ( 27 | 28 |
29 | setPrefix(e.target.value)} 33 | className="block w-full mt-2 border-gray-300 rounded-md shadow-sm focus:border-teal-300 focus:ring focus:ring-teal-200 focus:ring-opacity-50" 34 | placeholder="Style name prefix (optional)" 35 | /> 36 |
37 | 38 |
39 | 52 | 53 | 65 |
66 | 67 |
68 | 83 | 92 |
93 |
94 | ); 95 | }); 96 | 97 | export default ColorView; 98 | -------------------------------------------------------------------------------- /src/components/FileUpload.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from "mobx-react-lite"; 2 | import React, { useCallback } from "react"; 3 | import { useDropzone } from "react-dropzone"; 4 | import { useMst } from "../models/Root"; 5 | 6 | interface Props {} 7 | 8 | const FileUpload = observer(({}: Props) => { 9 | const { configName, errorMessage, readFile } = useMst(); 10 | 11 | const onDrop = useCallback((acceptedFiles) => { 12 | acceptedFiles.forEach((file) => readFile(acceptedFiles[0].name, file)); 13 | }, []); 14 | const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop }); 15 | 16 | return ( 17 |
28 |
33 | 34 |

35 | {isDragActive 36 | ? "Drag your file here 🎉" 37 | : errorMessage ?? configName ?? "Import tailwind.json"} 38 |

39 |
40 |
41 | ); 42 | }); 43 | 44 | export default FileUpload; 45 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface Props {} 4 | 5 | const Footer = ({}: Props) => { 6 | return ( 7 |
8 | 25 |
26 | 27 | with 28 | 36 | 42 | 43 | by ecklf 44 | 45 | 50 | 58 | 64 | 65 | 66 |
67 |
68 | ); 69 | }; 70 | 71 | export default Footer; 72 | -------------------------------------------------------------------------------- /src/core/colors.ts: -------------------------------------------------------------------------------- 1 | export const addColors = (payload: AddColorsPayload, styles: Array): boolean => { 2 | let payloadStyles: Array = payload.config; 3 | const overrideStyles: boolean = payload.overrideStyles; 4 | const addSpaces: boolean = payload.addSpaces; 5 | const prefix = payload.prefix !== "" ? addSpaces ? `${payload.prefix} / ` : `${payload.prefix}/` : ""; 6 | try { 7 | if (styles.length > 0) { 8 | if (overrideStyles) 9 | styles.forEach(style => { 10 | for (const payloadStyle of payloadStyles) { 11 | const styleName = addSpaces ? payloadStyle.name.split('/').join(' / ') : payloadStyle.name; 12 | const payloadStyleName = `${prefix}${styleName}`; 13 | 14 | if (style.name == payloadStyleName) { 15 | handleSolidColor(style.name, payloadStyle.value, false, style); 16 | const index = payloadStyles.indexOf(payloadStyle, 0); 17 | payloadStyles.splice(index, 1); 18 | } 19 | } 20 | }); 21 | createSolidColors(prefix, payloadStyles, addSpaces); 22 | } else { 23 | createSolidColors(prefix, payloadStyles, addSpaces); 24 | } 25 | return true; 26 | } catch (error) { 27 | alert(error); 28 | return false; 29 | } 30 | }; 31 | 32 | export const createSolidColors = (prefix: String, payloadStyles: Array, addSpaces: boolean) => { 33 | for (const { name, value } of payloadStyles) { 34 | const styleName = addSpaces ? name.split('/').join(' / ') : name; 35 | const styleNameWithPrefix = `${prefix}${styleName}`; 36 | handleSolidColor(styleNameWithPrefix, value); 37 | } 38 | } 39 | 40 | export const handleSolidColor = (name: string, color: RGBA, create: boolean = true, paintStyle?: PaintStyle) => { 41 | const style = create ? figma.createPaintStyle() : paintStyle; 42 | 43 | if (create) { 44 | figma.createPaintStyle; 45 | style.name = name; 46 | } 47 | 48 | const { r, g, b, a } = color; 49 | 50 | const rgbColor: RGB = { r, g, b }; 51 | const alpha: number = a; 52 | 53 | const solidPaint: SolidPaint = { 54 | type: "SOLID", 55 | color: rgbColor, 56 | opacity: alpha 57 | }; 58 | 59 | style.paints = [solidPaint]; 60 | }; 61 | -------------------------------------------------------------------------------- /src/core/config.ts: -------------------------------------------------------------------------------- 1 | const parse = require("parse-color"); 2 | 3 | const looseJsonParse = (obj) => { 4 | return Function('"use strict";return (' + obj + ")")(); 5 | }; 6 | 7 | export const parseConfig = (config: string): object => { 8 | try { 9 | // Remove any whitespace 10 | config = config.replace(/\s/g, ""); 11 | // Remove anything before module.exports and module.exports itself 12 | config = config.replace(/^(.*)(module.exports=?)/gi, ""); 13 | // Remove semicolon after brace 14 | config = config.replace("};", "}"); 15 | // Remove plugins section 16 | config = config.replace(/,plugins(.*?)(?!.*\])(?=})/gi, ""); 17 | return looseJsonParse(config); 18 | } catch (error) { 19 | console.error(error); 20 | throw new Error(error); 21 | } 22 | }; 23 | 24 | export const convertConfigColor = (color: string): RGBA => { 25 | if (color === "transparent") { 26 | color = "rgba(0, 0, 0, 0.0)"; 27 | } 28 | const { rgba } = parse(color); 29 | 30 | const alpha = color.length < 8 ? rgba[3] : rgba[3] / 255; 31 | return { 32 | r: rgba[0] / 255, 33 | g: rgba[1] / 255, 34 | b: rgba[2] / 255, 35 | a: alpha, 36 | }; 37 | }; 38 | 39 | export const fetchConfigColors = (config): Array => { 40 | let foundColors: Array = []; 41 | 42 | if (config.theme) { 43 | const { theme } = config; 44 | let mergedColors = {}; 45 | 46 | if (theme.colors) { 47 | mergedColors = { ...mergedColors, ...theme.colors }; 48 | } 49 | 50 | if (theme.extend && theme.extend.colors) { 51 | mergedColors = { ...mergedColors, ...theme.extend.colors }; 52 | } 53 | 54 | // Looping through all overwritten and extended colors 55 | for (const color of Object.keys(mergedColors)) { 56 | const colorData = mergedColors[color]; 57 | // Determine if the current color has nested shades 58 | if (typeof colorData === "object") { 59 | for (const colorShade of Object.keys(colorData)) { 60 | foundColors.push({ 61 | name: `${color}/${colorShade}`, 62 | value: convertConfigColor(colorData[colorShade]), 63 | }); 64 | } 65 | } else { 66 | if (colorData === "currentColor") { 67 | continue; 68 | } 69 | foundColors.push({ 70 | name: color, 71 | value: convertConfigColor(mergedColors[color]), 72 | }); 73 | } 74 | } 75 | } 76 | return foundColors; 77 | }; 78 | -------------------------------------------------------------------------------- /src/core/fonts.ts: -------------------------------------------------------------------------------- 1 | export interface TailwindFont { 2 | family: string; 3 | style: string; 4 | } 5 | 6 | export const createFontStyle = async (payload: TailwindFont) => { 7 | try { 8 | const { family, style } = payload; 9 | const desiredFont: FontName = { family, style }; 10 | const fontData = await figma.listAvailableFontsAsync(); 11 | const availableStyles = fontData 12 | .filter(font => font.fontName.family === family) 13 | .map(font => { 14 | return font.fontName.style; 15 | }); 16 | await figma.loadFontAsync(desiredFont); 17 | const fontStyle = figma.createTextStyle(); 18 | fontStyle.name = "text-xs"; 19 | fontStyle.fontName = desiredFont; 20 | } catch (error) { 21 | alert(error); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/generated/tw.min.json: -------------------------------------------------------------------------------- 1 | {"theme":{"typography":{"DEFAULT":{"css":[{"color":"#374151","maxWidth":"65ch","[class~=\"lead\"]":{"color":"#4b5563"},"a":{"color":"#111827","textDecoration":"underline","fontWeight":"500"},"strong":{"color":"#111827","fontWeight":"600"},"ol":{"counterReset":"list-counter"},"ol > li":{"position":"relative","counterIncrement":"list-counter"},"ol > li::before":{"content":"counter(list-counter) \".\"","position":"absolute","fontWeight":"400","color":"#6b7280"},"ul > li":{"position":"relative"},"ul > li::before":{"content":"\"\"","position":"absolute","backgroundColor":"#d1d5db","borderRadius":"50%"},"hr":{"borderColor":"#e5e7eb","borderTopWidth":1},"blockquote":{"fontWeight":"500","fontStyle":"italic","color":"#111827","borderLeftWidth":"0.25rem","borderLeftColor":"#e5e7eb","quotes":"\"\\201C\"\"\\201D\"\"\\2018\"\"\\2019\""},"blockquote p:first-of-type::before":{"content":"open-quote"},"blockquote p:last-of-type::after":{"content":"close-quote"},"h1":{"color":"#111827","fontWeight":"800"},"h2":{"color":"#111827","fontWeight":"700"},"h3":{"color":"#111827","fontWeight":"600"},"h4":{"color":"#111827","fontWeight":"600"},"figure figcaption":{"color":"#6b7280"},"code":{"color":"#111827","fontWeight":"600"},"code::before":{"content":"\"`\""},"code::after":{"content":"\"`\""},"a code":{"color":"#111827"},"pre":{"color":"#e5e7eb","backgroundColor":"#1f2937","overflowX":"auto"},"pre code":{"backgroundColor":"transparent","borderWidth":"0","borderRadius":"0","padding":"0","fontWeight":"400","color":"inherit","fontSize":"inherit","fontFamily":"inherit","lineHeight":"inherit"},"pre code::before":{"content":"\"\""},"pre code::after":{"content":"\"\""},"table":{"width":"100%","tableLayout":"auto","textAlign":"left","marginTop":"2em","marginBottom":"2em"},"thead":{"color":"#111827","fontWeight":"600","borderBottomWidth":"1px","borderBottomColor":"#d1d5db"},"thead th":{"verticalAlign":"bottom"},"tbody tr":{"borderBottomWidth":"1px","borderBottomColor":"#e5e7eb"},"tbody tr:last-child":{"borderBottomWidth":"0"},"tbody td":{"verticalAlign":"top"}},{"fontSize":"1rem","lineHeight":"1.75","p":{"marginTop":"1.25em","marginBottom":"1.25em"},"[class~=\"lead\"]":{"fontSize":"1.25em","lineHeight":"1.6","marginTop":"1.2em","marginBottom":"1.2em"},"blockquote":{"marginTop":"1.6em","marginBottom":"1.6em","paddingLeft":"1em"},"h1":{"fontSize":"2.25em","marginTop":"0","marginBottom":"0.8888889em","lineHeight":"1.1111111"},"h2":{"fontSize":"1.5em","marginTop":"2em","marginBottom":"1em","lineHeight":"1.3333333"},"h3":{"fontSize":"1.25em","marginTop":"1.6em","marginBottom":"0.6em","lineHeight":"1.6"},"h4":{"marginTop":"1.5em","marginBottom":"0.5em","lineHeight":"1.5"},"img":{"marginTop":"2em","marginBottom":"2em"},"video":{"marginTop":"2em","marginBottom":"2em"},"figure":{"marginTop":"2em","marginBottom":"2em"},"figure > *":{"marginTop":"0","marginBottom":"0"},"figure figcaption":{"fontSize":"0.875em","lineHeight":"1.4285714","marginTop":"0.8571429em"},"code":{"fontSize":"0.875em"},"h2 code":{"fontSize":"0.875em"},"h3 code":{"fontSize":"0.9em"},"pre":{"fontSize":"0.875em","lineHeight":"1.7142857","marginTop":"1.7142857em","marginBottom":"1.7142857em","borderRadius":"0.375rem","paddingTop":"0.8571429em","paddingRight":"1.1428571em","paddingBottom":"0.8571429em","paddingLeft":"1.1428571em"},"ol":{"marginTop":"1.25em","marginBottom":"1.25em"},"ul":{"marginTop":"1.25em","marginBottom":"1.25em"},"li":{"marginTop":"0.5em","marginBottom":"0.5em"},"ol > li":{"paddingLeft":"1.75em"},"ol > li::before":{"left":"0"},"ul > li":{"paddingLeft":"1.75em"},"ul > li::before":{"width":"0.375em","height":"0.375em","top":"calc(0.875em - 0.1875em)","left":"0.25em"},"> ul > li p":{"marginTop":"0.75em","marginBottom":"0.75em"},"> ul > li > *:first-child":{"marginTop":"1.25em"},"> ul > li > *:last-child":{"marginBottom":"1.25em"},"> ol > li > *:first-child":{"marginTop":"1.25em"},"> ol > li > *:last-child":{"marginBottom":"1.25em"},"ul ul, ul ol, ol ul, ol ol":{"marginTop":"0.75em","marginBottom":"0.75em"},"hr":{"marginTop":"3em","marginBottom":"3em"},"hr + *":{"marginTop":"0"},"h2 + *":{"marginTop":"0"},"h3 + *":{"marginTop":"0"},"h4 + *":{"marginTop":"0"},"table":{"fontSize":"0.875em","lineHeight":"1.7142857"},"thead th":{"paddingRight":"0.5714286em","paddingBottom":"0.5714286em","paddingLeft":"0.5714286em"},"thead th:first-child":{"paddingLeft":"0"},"thead th:last-child":{"paddingRight":"0"},"tbody td":{"paddingTop":"0.5714286em","paddingRight":"0.5714286em","paddingBottom":"0.5714286em","paddingLeft":"0.5714286em"},"tbody td:first-child":{"paddingLeft":"0"},"tbody td:last-child":{"paddingRight":"0"}},{"> :first-child":{"marginTop":"0"},"> :last-child":{"marginBottom":"0"}}]},"sm":{"css":[{"fontSize":"0.875rem","lineHeight":"1.7142857","p":{"marginTop":"1.1428571em","marginBottom":"1.1428571em"},"[class~=\"lead\"]":{"fontSize":"1.2857143em","lineHeight":"1.5555556","marginTop":"0.8888889em","marginBottom":"0.8888889em"},"blockquote":{"marginTop":"1.3333333em","marginBottom":"1.3333333em","paddingLeft":"1.1111111em"},"h1":{"fontSize":"2.1428571em","marginTop":"0","marginBottom":"0.8em","lineHeight":"1.2"},"h2":{"fontSize":"1.4285714em","marginTop":"1.6em","marginBottom":"0.8em","lineHeight":"1.4"},"h3":{"fontSize":"1.2857143em","marginTop":"1.5555556em","marginBottom":"0.4444444em","lineHeight":"1.5555556"},"h4":{"marginTop":"1.4285714em","marginBottom":"0.5714286em","lineHeight":"1.4285714"},"img":{"marginTop":"1.7142857em","marginBottom":"1.7142857em"},"video":{"marginTop":"1.7142857em","marginBottom":"1.7142857em"},"figure":{"marginTop":"1.7142857em","marginBottom":"1.7142857em"},"figure > *":{"marginTop":"0","marginBottom":"0"},"figure figcaption":{"fontSize":"0.8571429em","lineHeight":"1.3333333","marginTop":"0.6666667em"},"code":{"fontSize":"0.8571429em"},"h2 code":{"fontSize":"0.9em"},"h3 code":{"fontSize":"0.8888889em"},"pre":{"fontSize":"0.8571429em","lineHeight":"1.6666667","marginTop":"1.6666667em","marginBottom":"1.6666667em","borderRadius":"0.25rem","paddingTop":"0.6666667em","paddingRight":"1em","paddingBottom":"0.6666667em","paddingLeft":"1em"},"ol":{"marginTop":"1.1428571em","marginBottom":"1.1428571em"},"ul":{"marginTop":"1.1428571em","marginBottom":"1.1428571em"},"li":{"marginTop":"0.2857143em","marginBottom":"0.2857143em"},"ol > li":{"paddingLeft":"1.5714286em"},"ol > li::before":{"left":"0"},"ul > li":{"paddingLeft":"1.5714286em"},"ul > li::before":{"height":"0.3571429em","width":"0.3571429em","top":"calc(0.8571429em - 0.1785714em)","left":"0.2142857em"},"> ul > li p":{"marginTop":"0.5714286em","marginBottom":"0.5714286em"},"> ul > li > *:first-child":{"marginTop":"1.1428571em"},"> ul > li > *:last-child":{"marginBottom":"1.1428571em"},"> ol > li > *:first-child":{"marginTop":"1.1428571em"},"> ol > li > *:last-child":{"marginBottom":"1.1428571em"},"ul ul, ul ol, ol ul, ol ol":{"marginTop":"0.5714286em","marginBottom":"0.5714286em"},"hr":{"marginTop":"2.8571429em","marginBottom":"2.8571429em"},"hr + *":{"marginTop":"0"},"h2 + *":{"marginTop":"0"},"h3 + *":{"marginTop":"0"},"h4 + *":{"marginTop":"0"},"table":{"fontSize":"0.8571429em","lineHeight":"1.5"},"thead th":{"paddingRight":"1em","paddingBottom":"0.6666667em","paddingLeft":"1em"},"thead th:first-child":{"paddingLeft":"0"},"thead th:last-child":{"paddingRight":"0"},"tbody td":{"paddingTop":"0.6666667em","paddingRight":"1em","paddingBottom":"0.6666667em","paddingLeft":"1em"},"tbody td:first-child":{"paddingLeft":"0"},"tbody td:last-child":{"paddingRight":"0"}},{"> :first-child":{"marginTop":"0"},"> :last-child":{"marginBottom":"0"}}]},"lg":{"css":[{"fontSize":"1.125rem","lineHeight":"1.7777778","p":{"marginTop":"1.3333333em","marginBottom":"1.3333333em"},"[class~=\"lead\"]":{"fontSize":"1.2222222em","lineHeight":"1.4545455","marginTop":"1.0909091em","marginBottom":"1.0909091em"},"blockquote":{"marginTop":"1.6666667em","marginBottom":"1.6666667em","paddingLeft":"1em"},"h1":{"fontSize":"2.6666667em","marginTop":"0","marginBottom":"0.8333333em","lineHeight":"1"},"h2":{"fontSize":"1.6666667em","marginTop":"1.8666667em","marginBottom":"1.0666667em","lineHeight":"1.3333333"},"h3":{"fontSize":"1.3333333em","marginTop":"1.6666667em","marginBottom":"0.6666667em","lineHeight":"1.5"},"h4":{"marginTop":"1.7777778em","marginBottom":"0.4444444em","lineHeight":"1.5555556"},"img":{"marginTop":"1.7777778em","marginBottom":"1.7777778em"},"video":{"marginTop":"1.7777778em","marginBottom":"1.7777778em"},"figure":{"marginTop":"1.7777778em","marginBottom":"1.7777778em"},"figure > *":{"marginTop":"0","marginBottom":"0"},"figure figcaption":{"fontSize":"0.8888889em","lineHeight":"1.5","marginTop":"1em"},"code":{"fontSize":"0.8888889em"},"h2 code":{"fontSize":"0.8666667em"},"h3 code":{"fontSize":"0.875em"},"pre":{"fontSize":"0.8888889em","lineHeight":"1.75","marginTop":"2em","marginBottom":"2em","borderRadius":"0.375rem","paddingTop":"1em","paddingRight":"1.5em","paddingBottom":"1em","paddingLeft":"1.5em"},"ol":{"marginTop":"1.3333333em","marginBottom":"1.3333333em"},"ul":{"marginTop":"1.3333333em","marginBottom":"1.3333333em"},"li":{"marginTop":"0.6666667em","marginBottom":"0.6666667em"},"ol > li":{"paddingLeft":"1.6666667em"},"ol > li::before":{"left":"0"},"ul > li":{"paddingLeft":"1.6666667em"},"ul > li::before":{"width":"0.3333333em","height":"0.3333333em","top":"calc(0.8888889em - 0.1666667em)","left":"0.2222222em"},"> ul > li p":{"marginTop":"0.8888889em","marginBottom":"0.8888889em"},"> ul > li > *:first-child":{"marginTop":"1.3333333em"},"> ul > li > *:last-child":{"marginBottom":"1.3333333em"},"> ol > li > *:first-child":{"marginTop":"1.3333333em"},"> ol > li > *:last-child":{"marginBottom":"1.3333333em"},"ul ul, ul ol, ol ul, ol ol":{"marginTop":"0.8888889em","marginBottom":"0.8888889em"},"hr":{"marginTop":"3.1111111em","marginBottom":"3.1111111em"},"hr + *":{"marginTop":"0"},"h2 + *":{"marginTop":"0"},"h3 + *":{"marginTop":"0"},"h4 + *":{"marginTop":"0"},"table":{"fontSize":"0.8888889em","lineHeight":"1.5"},"thead th":{"paddingRight":"0.75em","paddingBottom":"0.75em","paddingLeft":"0.75em"},"thead th:first-child":{"paddingLeft":"0"},"thead th:last-child":{"paddingRight":"0"},"tbody td":{"paddingTop":"0.75em","paddingRight":"0.75em","paddingBottom":"0.75em","paddingLeft":"0.75em"},"tbody td:first-child":{"paddingLeft":"0"},"tbody td:last-child":{"paddingRight":"0"}},{"> :first-child":{"marginTop":"0"},"> :last-child":{"marginBottom":"0"}}]},"xl":{"css":[{"fontSize":"1.25rem","lineHeight":"1.8","p":{"marginTop":"1.2em","marginBottom":"1.2em"},"[class~=\"lead\"]":{"fontSize":"1.2em","lineHeight":"1.5","marginTop":"1em","marginBottom":"1em"},"blockquote":{"marginTop":"1.6em","marginBottom":"1.6em","paddingLeft":"1.0666667em"},"h1":{"fontSize":"2.8em","marginTop":"0","marginBottom":"0.8571429em","lineHeight":"1"},"h2":{"fontSize":"1.8em","marginTop":"1.5555556em","marginBottom":"0.8888889em","lineHeight":"1.1111111"},"h3":{"fontSize":"1.5em","marginTop":"1.6em","marginBottom":"0.6666667em","lineHeight":"1.3333333"},"h4":{"marginTop":"1.8em","marginBottom":"0.6em","lineHeight":"1.6"},"img":{"marginTop":"2em","marginBottom":"2em"},"video":{"marginTop":"2em","marginBottom":"2em"},"figure":{"marginTop":"2em","marginBottom":"2em"},"figure > *":{"marginTop":"0","marginBottom":"0"},"figure figcaption":{"fontSize":"0.9em","lineHeight":"1.5555556","marginTop":"1em"},"code":{"fontSize":"0.9em"},"h2 code":{"fontSize":"0.8611111em"},"h3 code":{"fontSize":"0.9em"},"pre":{"fontSize":"0.9em","lineHeight":"1.7777778","marginTop":"2em","marginBottom":"2em","borderRadius":"0.5rem","paddingTop":"1.1111111em","paddingRight":"1.3333333em","paddingBottom":"1.1111111em","paddingLeft":"1.3333333em"},"ol":{"marginTop":"1.2em","marginBottom":"1.2em"},"ul":{"marginTop":"1.2em","marginBottom":"1.2em"},"li":{"marginTop":"0.6em","marginBottom":"0.6em"},"ol > li":{"paddingLeft":"1.8em"},"ol > li::before":{"left":"0"},"ul > li":{"paddingLeft":"1.8em"},"ul > li::before":{"width":"0.35em","height":"0.35em","top":"calc(0.9em - 0.175em)","left":"0.25em"},"> ul > li p":{"marginTop":"0.8em","marginBottom":"0.8em"},"> ul > li > *:first-child":{"marginTop":"1.2em"},"> ul > li > *:last-child":{"marginBottom":"1.2em"},"> ol > li > *:first-child":{"marginTop":"1.2em"},"> ol > li > *:last-child":{"marginBottom":"1.2em"},"ul ul, ul ol, ol ul, ol ol":{"marginTop":"0.8em","marginBottom":"0.8em"},"hr":{"marginTop":"2.8em","marginBottom":"2.8em"},"hr + *":{"marginTop":"0"},"h2 + *":{"marginTop":"0"},"h3 + *":{"marginTop":"0"},"h4 + *":{"marginTop":"0"},"table":{"fontSize":"0.9em","lineHeight":"1.5555556"},"thead th":{"paddingRight":"0.6666667em","paddingBottom":"0.8888889em","paddingLeft":"0.6666667em"},"thead th:first-child":{"paddingLeft":"0"},"thead th:last-child":{"paddingRight":"0"},"tbody td":{"paddingTop":"0.8888889em","paddingRight":"0.6666667em","paddingBottom":"0.8888889em","paddingLeft":"0.6666667em"},"tbody td:first-child":{"paddingLeft":"0"},"tbody td:last-child":{"paddingRight":"0"}},{"> :first-child":{"marginTop":"0"},"> :last-child":{"marginBottom":"0"}}]},"2xl":{"css":[{"fontSize":"1.5rem","lineHeight":"1.6666667","p":{"marginTop":"1.3333333em","marginBottom":"1.3333333em"},"[class~=\"lead\"]":{"fontSize":"1.25em","lineHeight":"1.4666667","marginTop":"1.0666667em","marginBottom":"1.0666667em"},"blockquote":{"marginTop":"1.7777778em","marginBottom":"1.7777778em","paddingLeft":"1.1111111em"},"h1":{"fontSize":"2.6666667em","marginTop":"0","marginBottom":"0.875em","lineHeight":"1"},"h2":{"fontSize":"2em","marginTop":"1.5em","marginBottom":"0.8333333em","lineHeight":"1.0833333"},"h3":{"fontSize":"1.5em","marginTop":"1.5555556em","marginBottom":"0.6666667em","lineHeight":"1.2222222"},"h4":{"marginTop":"1.6666667em","marginBottom":"0.6666667em","lineHeight":"1.5"},"img":{"marginTop":"2em","marginBottom":"2em"},"video":{"marginTop":"2em","marginBottom":"2em"},"figure":{"marginTop":"2em","marginBottom":"2em"},"figure > *":{"marginTop":"0","marginBottom":"0"},"figure figcaption":{"fontSize":"0.8333333em","lineHeight":"1.6","marginTop":"1em"},"code":{"fontSize":"0.8333333em"},"h2 code":{"fontSize":"0.875em"},"h3 code":{"fontSize":"0.8888889em"},"pre":{"fontSize":"0.8333333em","lineHeight":"1.8","marginTop":"2em","marginBottom":"2em","borderRadius":"0.5rem","paddingTop":"1.2em","paddingRight":"1.6em","paddingBottom":"1.2em","paddingLeft":"1.6em"},"ol":{"marginTop":"1.3333333em","marginBottom":"1.3333333em"},"ul":{"marginTop":"1.3333333em","marginBottom":"1.3333333em"},"li":{"marginTop":"0.5em","marginBottom":"0.5em"},"ol > li":{"paddingLeft":"1.6666667em"},"ol > li::before":{"left":"0"},"ul > li":{"paddingLeft":"1.6666667em"},"ul > li::before":{"width":"0.3333333em","height":"0.3333333em","top":"calc(0.8333333em - 0.1666667em)","left":"0.25em"},"> ul > li p":{"marginTop":"0.8333333em","marginBottom":"0.8333333em"},"> ul > li > *:first-child":{"marginTop":"1.3333333em"},"> ul > li > *:last-child":{"marginBottom":"1.3333333em"},"> ol > li > *:first-child":{"marginTop":"1.3333333em"},"> ol > li > *:last-child":{"marginBottom":"1.3333333em"},"ul ul, ul ol, ol ul, ol ol":{"marginTop":"0.6666667em","marginBottom":"0.6666667em"},"hr":{"marginTop":"3em","marginBottom":"3em"},"hr + *":{"marginTop":"0"},"h2 + *":{"marginTop":"0"},"h3 + *":{"marginTop":"0"},"h4 + *":{"marginTop":"0"},"table":{"fontSize":"0.8333333em","lineHeight":"1.4"},"thead th":{"paddingRight":"0.6em","paddingBottom":"0.8em","paddingLeft":"0.6em"},"thead th:first-child":{"paddingLeft":"0"},"thead th:last-child":{"paddingRight":"0"},"tbody td":{"paddingTop":"0.8em","paddingRight":"0.6em","paddingBottom":"0.8em","paddingLeft":"0.6em"},"tbody td:first-child":{"paddingLeft":"0"},"tbody td:last-child":{"paddingRight":"0"}},{"> :first-child":{"marginTop":"0"},"> :last-child":{"marginBottom":"0"}}]},"red":{"css":[{"a":{"color":"#dc2626"},"a code":{"color":"#dc2626"}}]},"yellow":{"css":[{"a":{"color":"#d97706"},"a code":{"color":"#d97706"}}]},"green":{"css":[{"a":{"color":"#059669"},"a code":{"color":"#059669"}}]},"blue":{"css":[{"a":{"color":"#2563eb"},"a code":{"color":"#2563eb"}}]},"indigo":{"css":[{"a":{"color":"#4f46e5"},"a code":{"color":"#4f46e5"}}]},"purple":{"css":[{"a":{"color":"#7c3aed"},"a code":{"color":"#7c3aed"}}]},"pink":{"css":[{"a":{"color":"#db2777"},"a code":{"color":"#db2777"}}]}},"aspectRatio":{"1":"1","2":"2","3":"3","4":"4","5":"5","6":"6","7":"7","8":"8","9":"9","10":"10","11":"11","12":"12","13":"13","14":"14","15":"15","16":"16"},"screens":{"sm":"640px","md":"768px","lg":"1024px","xl":"1280px","2xl":"1536px"},"colors":{"transparent":"transparent","current":"currentColor","black":"#000","white":"#fff","gray":{"50":"#f9fafb","100":"#f3f4f6","200":"#e5e7eb","300":"#d1d5db","400":"#9ca3af","500":"#6b7280","600":"#4b5563","700":"#374151","800":"#1f2937","900":"#111827"},"red":{"50":"#fef2f2","100":"#fee2e2","200":"#fecaca","300":"#fca5a5","400":"#f87171","500":"#ef4444","600":"#dc2626","700":"#b91c1c","800":"#991b1b","900":"#7f1d1d"},"yellow":{"50":"#fffbeb","100":"#fef3c7","200":"#fde68a","300":"#fcd34d","400":"#fbbf24","500":"#f59e0b","600":"#d97706","700":"#b45309","800":"#92400e","900":"#78350f"},"green":{"50":"#ecfdf5","100":"#d1fae5","200":"#a7f3d0","300":"#6ee7b7","400":"#34d399","500":"#10b981","600":"#059669","700":"#047857","800":"#065f46","900":"#064e3b"},"blue":{"50":"#eff6ff","100":"#dbeafe","200":"#bfdbfe","300":"#93c5fd","400":"#60a5fa","500":"#3b82f6","600":"#2563eb","700":"#1d4ed8","800":"#1e40af","900":"#1e3a8a"},"indigo":{"50":"#eef2ff","100":"#e0e7ff","200":"#c7d2fe","300":"#a5b4fc","400":"#818cf8","500":"#6366f1","600":"#4f46e5","700":"#4338ca","800":"#3730a3","900":"#312e81"},"purple":{"50":"#f5f3ff","100":"#ede9fe","200":"#ddd6fe","300":"#c4b5fd","400":"#a78bfa","500":"#8b5cf6","600":"#7c3aed","700":"#6d28d9","800":"#5b21b6","900":"#4c1d95"},"pink":{"50":"#fdf2f8","100":"#fce7f3","200":"#fbcfe8","300":"#f9a8d4","400":"#f472b6","500":"#ec4899","600":"#db2777","700":"#be185d","800":"#9d174d","900":"#831843"}},"spacing":{"0":"0px","1":"0.25rem","2":"0.5rem","3":"0.75rem","4":"1rem","5":"1.25rem","6":"1.5rem","7":"1.75rem","8":"2rem","9":"2.25rem","10":"2.5rem","11":"2.75rem","12":"3rem","14":"3.5rem","16":"4rem","20":"5rem","24":"6rem","28":"7rem","32":"8rem","36":"9rem","40":"10rem","44":"11rem","48":"12rem","52":"13rem","56":"14rem","60":"15rem","64":"16rem","72":"18rem","80":"20rem","96":"24rem","px":"1px","0.5":"0.125rem","1.5":"0.375rem","2.5":"0.625rem","3.5":"0.875rem"},"animation":{"none":"none","spin":"spin 1s linear infinite","ping":"ping 1s cubic-bezier(0, 0, 0.2, 1) infinite","pulse":"pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite","bounce":"bounce 1s infinite"},"backgroundColor":{"transparent":"transparent","current":"currentColor","black":"#000","white":"#fff","gray":{"50":"#f9fafb","100":"#f3f4f6","200":"#e5e7eb","300":"#d1d5db","400":"#9ca3af","500":"#6b7280","600":"#4b5563","700":"#374151","800":"#1f2937","900":"#111827"},"red":{"50":"#fef2f2","100":"#fee2e2","200":"#fecaca","300":"#fca5a5","400":"#f87171","500":"#ef4444","600":"#dc2626","700":"#b91c1c","800":"#991b1b","900":"#7f1d1d"},"yellow":{"50":"#fffbeb","100":"#fef3c7","200":"#fde68a","300":"#fcd34d","400":"#fbbf24","500":"#f59e0b","600":"#d97706","700":"#b45309","800":"#92400e","900":"#78350f"},"green":{"50":"#ecfdf5","100":"#d1fae5","200":"#a7f3d0","300":"#6ee7b7","400":"#34d399","500":"#10b981","600":"#059669","700":"#047857","800":"#065f46","900":"#064e3b"},"blue":{"50":"#eff6ff","100":"#dbeafe","200":"#bfdbfe","300":"#93c5fd","400":"#60a5fa","500":"#3b82f6","600":"#2563eb","700":"#1d4ed8","800":"#1e40af","900":"#1e3a8a"},"indigo":{"50":"#eef2ff","100":"#e0e7ff","200":"#c7d2fe","300":"#a5b4fc","400":"#818cf8","500":"#6366f1","600":"#4f46e5","700":"#4338ca","800":"#3730a3","900":"#312e81"},"purple":{"50":"#f5f3ff","100":"#ede9fe","200":"#ddd6fe","300":"#c4b5fd","400":"#a78bfa","500":"#8b5cf6","600":"#7c3aed","700":"#6d28d9","800":"#5b21b6","900":"#4c1d95"},"pink":{"50":"#fdf2f8","100":"#fce7f3","200":"#fbcfe8","300":"#f9a8d4","400":"#f472b6","500":"#ec4899","600":"#db2777","700":"#be185d","800":"#9d174d","900":"#831843"}},"backgroundImage":{"none":"none","gradient-to-t":"linear-gradient(to top, var(--tw-gradient-stops))","gradient-to-tr":"linear-gradient(to top right, var(--tw-gradient-stops))","gradient-to-r":"linear-gradient(to right, var(--tw-gradient-stops))","gradient-to-br":"linear-gradient(to bottom right, var(--tw-gradient-stops))","gradient-to-b":"linear-gradient(to bottom, var(--tw-gradient-stops))","gradient-to-bl":"linear-gradient(to bottom left, var(--tw-gradient-stops))","gradient-to-l":"linear-gradient(to left, var(--tw-gradient-stops))","gradient-to-tl":"linear-gradient(to top left, var(--tw-gradient-stops))"},"backgroundOpacity":{"0":"0","5":"0.05","10":"0.1","20":"0.2","25":"0.25","30":"0.3","40":"0.4","50":"0.5","60":"0.6","70":"0.7","75":"0.75","80":"0.8","90":"0.9","95":"0.95","100":"1"},"backgroundPosition":{"bottom":"bottom","center":"center","left":"left","left-bottom":"left bottom","left-top":"left top","right":"right","right-bottom":"right bottom","right-top":"right top","top":"top"},"backgroundSize":{"auto":"auto","cover":"cover","contain":"contain"},"borderColor":{"transparent":"transparent","current":"currentColor","black":"#000","white":"#fff","gray":{"50":"#f9fafb","100":"#f3f4f6","200":"#e5e7eb","300":"#d1d5db","400":"#9ca3af","500":"#6b7280","600":"#4b5563","700":"#374151","800":"#1f2937","900":"#111827"},"red":{"50":"#fef2f2","100":"#fee2e2","200":"#fecaca","300":"#fca5a5","400":"#f87171","500":"#ef4444","600":"#dc2626","700":"#b91c1c","800":"#991b1b","900":"#7f1d1d"},"yellow":{"50":"#fffbeb","100":"#fef3c7","200":"#fde68a","300":"#fcd34d","400":"#fbbf24","500":"#f59e0b","600":"#d97706","700":"#b45309","800":"#92400e","900":"#78350f"},"green":{"50":"#ecfdf5","100":"#d1fae5","200":"#a7f3d0","300":"#6ee7b7","400":"#34d399","500":"#10b981","600":"#059669","700":"#047857","800":"#065f46","900":"#064e3b"},"blue":{"50":"#eff6ff","100":"#dbeafe","200":"#bfdbfe","300":"#93c5fd","400":"#60a5fa","500":"#3b82f6","600":"#2563eb","700":"#1d4ed8","800":"#1e40af","900":"#1e3a8a"},"indigo":{"50":"#eef2ff","100":"#e0e7ff","200":"#c7d2fe","300":"#a5b4fc","400":"#818cf8","500":"#6366f1","600":"#4f46e5","700":"#4338ca","800":"#3730a3","900":"#312e81"},"purple":{"50":"#f5f3ff","100":"#ede9fe","200":"#ddd6fe","300":"#c4b5fd","400":"#a78bfa","500":"#8b5cf6","600":"#7c3aed","700":"#6d28d9","800":"#5b21b6","900":"#4c1d95"},"pink":{"50":"#fdf2f8","100":"#fce7f3","200":"#fbcfe8","300":"#f9a8d4","400":"#f472b6","500":"#ec4899","600":"#db2777","700":"#be185d","800":"#9d174d","900":"#831843"},"DEFAULT":"#e5e7eb"},"borderOpacity":{"0":"0","5":"0.05","10":"0.1","20":"0.2","25":"0.25","30":"0.3","40":"0.4","50":"0.5","60":"0.6","70":"0.7","75":"0.75","80":"0.8","90":"0.9","95":"0.95","100":"1"},"borderRadius":{"none":"0px","sm":"0.125rem","DEFAULT":"0.25rem","md":"0.375rem","lg":"0.5rem","xl":"0.75rem","2xl":"1rem","3xl":"1.5rem","full":"9999px"},"borderWidth":{"0":"0px","2":"2px","4":"4px","8":"8px","DEFAULT":"1px"},"boxShadow":{"sm":"0 1px 2px 0 rgba(0, 0, 0, 0.05)","DEFAULT":"0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)","md":"0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)","lg":"0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)","xl":"0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)","2xl":"0 25px 50px -12px rgba(0, 0, 0, 0.25)","inner":"inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)","none":"none"},"container":{},"cursor":{"auto":"auto","default":"default","pointer":"pointer","wait":"wait","text":"text","move":"move","not-allowed":"not-allowed"},"divideColor":{"transparent":"transparent","current":"currentColor","black":"#000","white":"#fff","gray":{"50":"#f9fafb","100":"#f3f4f6","200":"#e5e7eb","300":"#d1d5db","400":"#9ca3af","500":"#6b7280","600":"#4b5563","700":"#374151","800":"#1f2937","900":"#111827"},"red":{"50":"#fef2f2","100":"#fee2e2","200":"#fecaca","300":"#fca5a5","400":"#f87171","500":"#ef4444","600":"#dc2626","700":"#b91c1c","800":"#991b1b","900":"#7f1d1d"},"yellow":{"50":"#fffbeb","100":"#fef3c7","200":"#fde68a","300":"#fcd34d","400":"#fbbf24","500":"#f59e0b","600":"#d97706","700":"#b45309","800":"#92400e","900":"#78350f"},"green":{"50":"#ecfdf5","100":"#d1fae5","200":"#a7f3d0","300":"#6ee7b7","400":"#34d399","500":"#10b981","600":"#059669","700":"#047857","800":"#065f46","900":"#064e3b"},"blue":{"50":"#eff6ff","100":"#dbeafe","200":"#bfdbfe","300":"#93c5fd","400":"#60a5fa","500":"#3b82f6","600":"#2563eb","700":"#1d4ed8","800":"#1e40af","900":"#1e3a8a"},"indigo":{"50":"#eef2ff","100":"#e0e7ff","200":"#c7d2fe","300":"#a5b4fc","400":"#818cf8","500":"#6366f1","600":"#4f46e5","700":"#4338ca","800":"#3730a3","900":"#312e81"},"purple":{"50":"#f5f3ff","100":"#ede9fe","200":"#ddd6fe","300":"#c4b5fd","400":"#a78bfa","500":"#8b5cf6","600":"#7c3aed","700":"#6d28d9","800":"#5b21b6","900":"#4c1d95"},"pink":{"50":"#fdf2f8","100":"#fce7f3","200":"#fbcfe8","300":"#f9a8d4","400":"#f472b6","500":"#ec4899","600":"#db2777","700":"#be185d","800":"#9d174d","900":"#831843"},"DEFAULT":"#e5e7eb"},"divideOpacity":{"0":"0","5":"0.05","10":"0.1","20":"0.2","25":"0.25","30":"0.3","40":"0.4","50":"0.5","60":"0.6","70":"0.7","75":"0.75","80":"0.8","90":"0.9","95":"0.95","100":"1"},"divideWidth":{"0":"0px","2":"2px","4":"4px","8":"8px","DEFAULT":"1px"},"fill":{"current":"currentColor"},"flex":{"1":"1 1 0%","auto":"1 1 auto","initial":"0 1 auto","none":"none"},"flexGrow":{"0":"0","DEFAULT":"1"},"flexShrink":{"0":"0","DEFAULT":"1"},"fontFamily":{"sans":["ui-sans-serif","system-ui","-apple-system","BlinkMacSystemFont","\"Segoe UI\"","Roboto","\"Helvetica Neue\"","Arial","\"Noto Sans\"","sans-serif","\"Apple Color Emoji\"","\"Segoe UI Emoji\"","\"Segoe UI Symbol\"","\"Noto Color Emoji\""],"serif":["ui-serif","Georgia","Cambria","\"Times New Roman\"","Times","serif"],"mono":["ui-monospace","SFMono-Regular","Menlo","Monaco","Consolas","\"Liberation Mono\"","\"Courier New\"","monospace"]},"fontSize":{"xs":["0.75rem",{"lineHeight":"1rem"}],"sm":["0.875rem",{"lineHeight":"1.25rem"}],"base":["1rem",{"lineHeight":"1.5rem"}],"lg":["1.125rem",{"lineHeight":"1.75rem"}],"xl":["1.25rem",{"lineHeight":"1.75rem"}],"2xl":["1.5rem",{"lineHeight":"2rem"}],"3xl":["1.875rem",{"lineHeight":"2.25rem"}],"4xl":["2.25rem",{"lineHeight":"2.5rem"}],"5xl":["3rem",{"lineHeight":"1"}],"6xl":["3.75rem",{"lineHeight":"1"}],"7xl":["4.5rem",{"lineHeight":"1"}],"8xl":["6rem",{"lineHeight":"1"}],"9xl":["8rem",{"lineHeight":"1"}]},"fontWeight":{"thin":"100","extralight":"200","light":"300","normal":"400","medium":"500","semibold":"600","bold":"700","extrabold":"800","black":"900"},"gap":{"0":"0px","1":"0.25rem","2":"0.5rem","3":"0.75rem","4":"1rem","5":"1.25rem","6":"1.5rem","7":"1.75rem","8":"2rem","9":"2.25rem","10":"2.5rem","11":"2.75rem","12":"3rem","14":"3.5rem","16":"4rem","20":"5rem","24":"6rem","28":"7rem","32":"8rem","36":"9rem","40":"10rem","44":"11rem","48":"12rem","52":"13rem","56":"14rem","60":"15rem","64":"16rem","72":"18rem","80":"20rem","96":"24rem","px":"1px","0.5":"0.125rem","1.5":"0.375rem","2.5":"0.625rem","3.5":"0.875rem"},"gradientColorStops":{"transparent":"transparent","current":"currentColor","black":"#000","white":"#fff","gray":{"50":"#f9fafb","100":"#f3f4f6","200":"#e5e7eb","300":"#d1d5db","400":"#9ca3af","500":"#6b7280","600":"#4b5563","700":"#374151","800":"#1f2937","900":"#111827"},"red":{"50":"#fef2f2","100":"#fee2e2","200":"#fecaca","300":"#fca5a5","400":"#f87171","500":"#ef4444","600":"#dc2626","700":"#b91c1c","800":"#991b1b","900":"#7f1d1d"},"yellow":{"50":"#fffbeb","100":"#fef3c7","200":"#fde68a","300":"#fcd34d","400":"#fbbf24","500":"#f59e0b","600":"#d97706","700":"#b45309","800":"#92400e","900":"#78350f"},"green":{"50":"#ecfdf5","100":"#d1fae5","200":"#a7f3d0","300":"#6ee7b7","400":"#34d399","500":"#10b981","600":"#059669","700":"#047857","800":"#065f46","900":"#064e3b"},"blue":{"50":"#eff6ff","100":"#dbeafe","200":"#bfdbfe","300":"#93c5fd","400":"#60a5fa","500":"#3b82f6","600":"#2563eb","700":"#1d4ed8","800":"#1e40af","900":"#1e3a8a"},"indigo":{"50":"#eef2ff","100":"#e0e7ff","200":"#c7d2fe","300":"#a5b4fc","400":"#818cf8","500":"#6366f1","600":"#4f46e5","700":"#4338ca","800":"#3730a3","900":"#312e81"},"purple":{"50":"#f5f3ff","100":"#ede9fe","200":"#ddd6fe","300":"#c4b5fd","400":"#a78bfa","500":"#8b5cf6","600":"#7c3aed","700":"#6d28d9","800":"#5b21b6","900":"#4c1d95"},"pink":{"50":"#fdf2f8","100":"#fce7f3","200":"#fbcfe8","300":"#f9a8d4","400":"#f472b6","500":"#ec4899","600":"#db2777","700":"#be185d","800":"#9d174d","900":"#831843"}},"gridAutoColumns":{"auto":"auto","min":"min-content","max":"max-content","fr":"minmax(0, 1fr)"},"gridAutoRows":{"auto":"auto","min":"min-content","max":"max-content","fr":"minmax(0, 1fr)"},"gridColumn":{"auto":"auto","span-1":"span 1 / span 1","span-2":"span 2 / span 2","span-3":"span 3 / span 3","span-4":"span 4 / span 4","span-5":"span 5 / span 5","span-6":"span 6 / span 6","span-7":"span 7 / span 7","span-8":"span 8 / span 8","span-9":"span 9 / span 9","span-10":"span 10 / span 10","span-11":"span 11 / span 11","span-12":"span 12 / span 12","span-full":"1 / -1"},"gridColumnEnd":{"1":"1","2":"2","3":"3","4":"4","5":"5","6":"6","7":"7","8":"8","9":"9","10":"10","11":"11","12":"12","13":"13","auto":"auto"},"gridColumnStart":{"1":"1","2":"2","3":"3","4":"4","5":"5","6":"6","7":"7","8":"8","9":"9","10":"10","11":"11","12":"12","13":"13","auto":"auto"},"gridRow":{"auto":"auto","span-1":"span 1 / span 1","span-2":"span 2 / span 2","span-3":"span 3 / span 3","span-4":"span 4 / span 4","span-5":"span 5 / span 5","span-6":"span 6 / span 6","span-full":"1 / -1"},"gridRowStart":{"1":"1","2":"2","3":"3","4":"4","5":"5","6":"6","7":"7","auto":"auto"},"gridRowEnd":{"1":"1","2":"2","3":"3","4":"4","5":"5","6":"6","7":"7","auto":"auto"},"transformOrigin":{"center":"center","top":"top","top-right":"top right","right":"right","bottom-right":"bottom right","bottom":"bottom","bottom-left":"bottom left","left":"left","top-left":"top left"},"gridTemplateColumns":{"1":"repeat(1, minmax(0, 1fr))","2":"repeat(2, minmax(0, 1fr))","3":"repeat(3, minmax(0, 1fr))","4":"repeat(4, minmax(0, 1fr))","5":"repeat(5, minmax(0, 1fr))","6":"repeat(6, minmax(0, 1fr))","7":"repeat(7, minmax(0, 1fr))","8":"repeat(8, minmax(0, 1fr))","9":"repeat(9, minmax(0, 1fr))","10":"repeat(10, minmax(0, 1fr))","11":"repeat(11, minmax(0, 1fr))","12":"repeat(12, minmax(0, 1fr))","none":"none"},"gridTemplateRows":{"1":"repeat(1, minmax(0, 1fr))","2":"repeat(2, minmax(0, 1fr))","3":"repeat(3, minmax(0, 1fr))","4":"repeat(4, minmax(0, 1fr))","5":"repeat(5, minmax(0, 1fr))","6":"repeat(6, minmax(0, 1fr))","none":"none"},"height":{"0":"0px","1":"0.25rem","2":"0.5rem","3":"0.75rem","4":"1rem","5":"1.25rem","6":"1.5rem","7":"1.75rem","8":"2rem","9":"2.25rem","10":"2.5rem","11":"2.75rem","12":"3rem","14":"3.5rem","16":"4rem","20":"5rem","24":"6rem","28":"7rem","32":"8rem","36":"9rem","40":"10rem","44":"11rem","48":"12rem","52":"13rem","56":"14rem","60":"15rem","64":"16rem","72":"18rem","80":"20rem","96":"24rem","auto":"auto","px":"1px","0.5":"0.125rem","1.5":"0.375rem","2.5":"0.625rem","3.5":"0.875rem","1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%","1/5":"20%","2/5":"40%","3/5":"60%","4/5":"80%","1/6":"16.666667%","2/6":"33.333333%","3/6":"50%","4/6":"66.666667%","5/6":"83.333333%","full":"100%","screen":"100vh"},"inset":{"0":"0px","1":"0.25rem","2":"0.5rem","3":"0.75rem","4":"1rem","5":"1.25rem","6":"1.5rem","7":"1.75rem","8":"2rem","9":"2.25rem","10":"2.5rem","11":"2.75rem","12":"3rem","14":"3.5rem","16":"4rem","20":"5rem","24":"6rem","28":"7rem","32":"8rem","36":"9rem","40":"10rem","44":"11rem","48":"12rem","52":"13rem","56":"14rem","60":"15rem","64":"16rem","72":"18rem","80":"20rem","96":"24rem","auto":"auto","px":"1px","0.5":"0.125rem","1.5":"0.375rem","2.5":"0.625rem","3.5":"0.875rem","-0":"0px","-1":"-0.25rem","-2":"-0.5rem","-3":"-0.75rem","-4":"-1rem","-5":"-1.25rem","-6":"-1.5rem","-7":"-1.75rem","-8":"-2rem","-9":"-2.25rem","-10":"-2.5rem","-11":"-2.75rem","-12":"-3rem","-14":"-3.5rem","-16":"-4rem","-20":"-5rem","-24":"-6rem","-28":"-7rem","-32":"-8rem","-36":"-9rem","-40":"-10rem","-44":"-11rem","-48":"-12rem","-52":"-13rem","-56":"-14rem","-60":"-15rem","-64":"-16rem","-72":"-18rem","-80":"-20rem","-96":"-24rem","-px":"-1px","-0.5":"-0.125rem","-1.5":"-0.375rem","-2.5":"-0.625rem","-3.5":"-0.875rem","1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%","full":"100%","-1/2":"-50%","-1/3":"-33.333333%","-2/3":"-66.666667%","-1/4":"-25%","-2/4":"-50%","-3/4":"-75%","-full":"-100%"},"keyframes":{"spin":{"to":{"transform":"rotate(360deg)"}},"ping":{"75%, 100%":{"transform":"scale(2)","opacity":"0"}},"pulse":{"50%":{"opacity":".5"}},"bounce":{"0%, 100%":{"transform":"translateY(-25%)","animationTimingFunction":"cubic-bezier(0.8,0,1,1)"},"50%":{"transform":"none","animationTimingFunction":"cubic-bezier(0,0,0.2,1)"}}},"letterSpacing":{"tighter":"-0.05em","tight":"-0.025em","normal":"0em","wide":"0.025em","wider":"0.05em","widest":"0.1em"},"lineHeight":{"3":".75rem","4":"1rem","5":"1.25rem","6":"1.5rem","7":"1.75rem","8":"2rem","9":"2.25rem","10":"2.5rem","none":"1","tight":"1.25","snug":"1.375","normal":"1.5","relaxed":"1.625","loose":"2"},"listStyleType":{"none":"none","disc":"disc","decimal":"decimal"},"margin":{"0":"0px","1":"0.25rem","2":"0.5rem","3":"0.75rem","4":"1rem","5":"1.25rem","6":"1.5rem","7":"1.75rem","8":"2rem","9":"2.25rem","10":"2.5rem","11":"2.75rem","12":"3rem","14":"3.5rem","16":"4rem","20":"5rem","24":"6rem","28":"7rem","32":"8rem","36":"9rem","40":"10rem","44":"11rem","48":"12rem","52":"13rem","56":"14rem","60":"15rem","64":"16rem","72":"18rem","80":"20rem","96":"24rem","auto":"auto","px":"1px","0.5":"0.125rem","1.5":"0.375rem","2.5":"0.625rem","3.5":"0.875rem","-0":"0px","-1":"-0.25rem","-2":"-0.5rem","-3":"-0.75rem","-4":"-1rem","-5":"-1.25rem","-6":"-1.5rem","-7":"-1.75rem","-8":"-2rem","-9":"-2.25rem","-10":"-2.5rem","-11":"-2.75rem","-12":"-3rem","-14":"-3.5rem","-16":"-4rem","-20":"-5rem","-24":"-6rem","-28":"-7rem","-32":"-8rem","-36":"-9rem","-40":"-10rem","-44":"-11rem","-48":"-12rem","-52":"-13rem","-56":"-14rem","-60":"-15rem","-64":"-16rem","-72":"-18rem","-80":"-20rem","-96":"-24rem","-px":"-1px","-0.5":"-0.125rem","-1.5":"-0.375rem","-2.5":"-0.625rem","-3.5":"-0.875rem"},"maxHeight":{"0":"0px","1":"0.25rem","2":"0.5rem","3":"0.75rem","4":"1rem","5":"1.25rem","6":"1.5rem","7":"1.75rem","8":"2rem","9":"2.25rem","10":"2.5rem","11":"2.75rem","12":"3rem","14":"3.5rem","16":"4rem","20":"5rem","24":"6rem","28":"7rem","32":"8rem","36":"9rem","40":"10rem","44":"11rem","48":"12rem","52":"13rem","56":"14rem","60":"15rem","64":"16rem","72":"18rem","80":"20rem","96":"24rem","px":"1px","0.5":"0.125rem","1.5":"0.375rem","2.5":"0.625rem","3.5":"0.875rem","full":"100%","screen":"100vh"},"maxWidth":{"0":"0rem","none":"none","xs":"20rem","sm":"24rem","md":"28rem","lg":"32rem","xl":"36rem","2xl":"42rem","3xl":"48rem","4xl":"56rem","5xl":"64rem","6xl":"72rem","7xl":"80rem","full":"100%","min":"min-content","max":"max-content","prose":"65ch","screen-sm":"640px","screen-md":"768px","screen-lg":"1024px","screen-xl":"1280px","screen-2xl":"1536px"},"minHeight":{"0":"0px","full":"100%","screen":"100vh"},"minWidth":{"0":"0px","full":"100%","min":"min-content","max":"max-content"},"objectPosition":{"bottom":"bottom","center":"center","left":"left","left-bottom":"left bottom","left-top":"left top","right":"right","right-bottom":"right bottom","right-top":"right top","top":"top"},"opacity":{"0":"0","5":"0.05","10":"0.1","20":"0.2","25":"0.25","30":"0.3","40":"0.4","50":"0.5","60":"0.6","70":"0.7","75":"0.75","80":"0.8","90":"0.9","95":"0.95","100":"1"},"order":{"1":"1","2":"2","3":"3","4":"4","5":"5","6":"6","7":"7","8":"8","9":"9","10":"10","11":"11","12":"12","first":"-9999","last":"9999","none":"0"},"outline":{"none":["2px solid transparent","2px"],"white":["2px dotted white","2px"],"black":["2px dotted black","2px"]},"padding":{"0":"0px","1":"0.25rem","2":"0.5rem","3":"0.75rem","4":"1rem","5":"1.25rem","6":"1.5rem","7":"1.75rem","8":"2rem","9":"2.25rem","10":"2.5rem","11":"2.75rem","12":"3rem","14":"3.5rem","16":"4rem","20":"5rem","24":"6rem","28":"7rem","32":"8rem","36":"9rem","40":"10rem","44":"11rem","48":"12rem","52":"13rem","56":"14rem","60":"15rem","64":"16rem","72":"18rem","80":"20rem","96":"24rem","px":"1px","0.5":"0.125rem","1.5":"0.375rem","2.5":"0.625rem","3.5":"0.875rem"},"placeholderColor":{"transparent":"transparent","current":"currentColor","black":"#000","white":"#fff","gray":{"50":"#f9fafb","100":"#f3f4f6","200":"#e5e7eb","300":"#d1d5db","400":"#9ca3af","500":"#6b7280","600":"#4b5563","700":"#374151","800":"#1f2937","900":"#111827"},"red":{"50":"#fef2f2","100":"#fee2e2","200":"#fecaca","300":"#fca5a5","400":"#f87171","500":"#ef4444","600":"#dc2626","700":"#b91c1c","800":"#991b1b","900":"#7f1d1d"},"yellow":{"50":"#fffbeb","100":"#fef3c7","200":"#fde68a","300":"#fcd34d","400":"#fbbf24","500":"#f59e0b","600":"#d97706","700":"#b45309","800":"#92400e","900":"#78350f"},"green":{"50":"#ecfdf5","100":"#d1fae5","200":"#a7f3d0","300":"#6ee7b7","400":"#34d399","500":"#10b981","600":"#059669","700":"#047857","800":"#065f46","900":"#064e3b"},"blue":{"50":"#eff6ff","100":"#dbeafe","200":"#bfdbfe","300":"#93c5fd","400":"#60a5fa","500":"#3b82f6","600":"#2563eb","700":"#1d4ed8","800":"#1e40af","900":"#1e3a8a"},"indigo":{"50":"#eef2ff","100":"#e0e7ff","200":"#c7d2fe","300":"#a5b4fc","400":"#818cf8","500":"#6366f1","600":"#4f46e5","700":"#4338ca","800":"#3730a3","900":"#312e81"},"purple":{"50":"#f5f3ff","100":"#ede9fe","200":"#ddd6fe","300":"#c4b5fd","400":"#a78bfa","500":"#8b5cf6","600":"#7c3aed","700":"#6d28d9","800":"#5b21b6","900":"#4c1d95"},"pink":{"50":"#fdf2f8","100":"#fce7f3","200":"#fbcfe8","300":"#f9a8d4","400":"#f472b6","500":"#ec4899","600":"#db2777","700":"#be185d","800":"#9d174d","900":"#831843"}},"placeholderOpacity":{"0":"0","5":"0.05","10":"0.1","20":"0.2","25":"0.25","30":"0.3","40":"0.4","50":"0.5","60":"0.6","70":"0.7","75":"0.75","80":"0.8","90":"0.9","95":"0.95","100":"1"},"ringColor":{"DEFAULT":"#3b82f6","transparent":"transparent","current":"currentColor","black":"#000","white":"#fff","gray":{"50":"#f9fafb","100":"#f3f4f6","200":"#e5e7eb","300":"#d1d5db","400":"#9ca3af","500":"#6b7280","600":"#4b5563","700":"#374151","800":"#1f2937","900":"#111827"},"red":{"50":"#fef2f2","100":"#fee2e2","200":"#fecaca","300":"#fca5a5","400":"#f87171","500":"#ef4444","600":"#dc2626","700":"#b91c1c","800":"#991b1b","900":"#7f1d1d"},"yellow":{"50":"#fffbeb","100":"#fef3c7","200":"#fde68a","300":"#fcd34d","400":"#fbbf24","500":"#f59e0b","600":"#d97706","700":"#b45309","800":"#92400e","900":"#78350f"},"green":{"50":"#ecfdf5","100":"#d1fae5","200":"#a7f3d0","300":"#6ee7b7","400":"#34d399","500":"#10b981","600":"#059669","700":"#047857","800":"#065f46","900":"#064e3b"},"blue":{"50":"#eff6ff","100":"#dbeafe","200":"#bfdbfe","300":"#93c5fd","400":"#60a5fa","500":"#3b82f6","600":"#2563eb","700":"#1d4ed8","800":"#1e40af","900":"#1e3a8a"},"indigo":{"50":"#eef2ff","100":"#e0e7ff","200":"#c7d2fe","300":"#a5b4fc","400":"#818cf8","500":"#6366f1","600":"#4f46e5","700":"#4338ca","800":"#3730a3","900":"#312e81"},"purple":{"50":"#f5f3ff","100":"#ede9fe","200":"#ddd6fe","300":"#c4b5fd","400":"#a78bfa","500":"#8b5cf6","600":"#7c3aed","700":"#6d28d9","800":"#5b21b6","900":"#4c1d95"},"pink":{"50":"#fdf2f8","100":"#fce7f3","200":"#fbcfe8","300":"#f9a8d4","400":"#f472b6","500":"#ec4899","600":"#db2777","700":"#be185d","800":"#9d174d","900":"#831843"}},"ringOffsetColor":{"transparent":"transparent","current":"currentColor","black":"#000","white":"#fff","gray":{"50":"#f9fafb","100":"#f3f4f6","200":"#e5e7eb","300":"#d1d5db","400":"#9ca3af","500":"#6b7280","600":"#4b5563","700":"#374151","800":"#1f2937","900":"#111827"},"red":{"50":"#fef2f2","100":"#fee2e2","200":"#fecaca","300":"#fca5a5","400":"#f87171","500":"#ef4444","600":"#dc2626","700":"#b91c1c","800":"#991b1b","900":"#7f1d1d"},"yellow":{"50":"#fffbeb","100":"#fef3c7","200":"#fde68a","300":"#fcd34d","400":"#fbbf24","500":"#f59e0b","600":"#d97706","700":"#b45309","800":"#92400e","900":"#78350f"},"green":{"50":"#ecfdf5","100":"#d1fae5","200":"#a7f3d0","300":"#6ee7b7","400":"#34d399","500":"#10b981","600":"#059669","700":"#047857","800":"#065f46","900":"#064e3b"},"blue":{"50":"#eff6ff","100":"#dbeafe","200":"#bfdbfe","300":"#93c5fd","400":"#60a5fa","500":"#3b82f6","600":"#2563eb","700":"#1d4ed8","800":"#1e40af","900":"#1e3a8a"},"indigo":{"50":"#eef2ff","100":"#e0e7ff","200":"#c7d2fe","300":"#a5b4fc","400":"#818cf8","500":"#6366f1","600":"#4f46e5","700":"#4338ca","800":"#3730a3","900":"#312e81"},"purple":{"50":"#f5f3ff","100":"#ede9fe","200":"#ddd6fe","300":"#c4b5fd","400":"#a78bfa","500":"#8b5cf6","600":"#7c3aed","700":"#6d28d9","800":"#5b21b6","900":"#4c1d95"},"pink":{"50":"#fdf2f8","100":"#fce7f3","200":"#fbcfe8","300":"#f9a8d4","400":"#f472b6","500":"#ec4899","600":"#db2777","700":"#be185d","800":"#9d174d","900":"#831843"}},"ringOffsetWidth":{"0":"0px","1":"1px","2":"2px","4":"4px","8":"8px"},"ringOpacity":{"0":"0","5":"0.05","10":"0.1","20":"0.2","25":"0.25","30":"0.3","40":"0.4","50":"0.5","60":"0.6","70":"0.7","75":"0.75","80":"0.8","90":"0.9","95":"0.95","100":"1","DEFAULT":"0.5"},"ringWidth":{"0":"0px","1":"1px","2":"2px","4":"4px","8":"8px","DEFAULT":"3px"},"rotate":{"0":"0deg","1":"1deg","2":"2deg","3":"3deg","6":"6deg","12":"12deg","45":"45deg","90":"90deg","180":"180deg","-180":"-180deg","-90":"-90deg","-45":"-45deg","-12":"-12deg","-6":"-6deg","-3":"-3deg","-2":"-2deg","-1":"-1deg"},"scale":{"0":"0","50":".5","75":".75","90":".9","95":".95","100":"1","105":"1.05","110":"1.1","125":"1.25","150":"1.5"},"skew":{"0":"0deg","1":"1deg","2":"2deg","3":"3deg","6":"6deg","12":"12deg","-12":"-12deg","-6":"-6deg","-3":"-3deg","-2":"-2deg","-1":"-1deg"},"space":{"0":"0px","1":"0.25rem","2":"0.5rem","3":"0.75rem","4":"1rem","5":"1.25rem","6":"1.5rem","7":"1.75rem","8":"2rem","9":"2.25rem","10":"2.5rem","11":"2.75rem","12":"3rem","14":"3.5rem","16":"4rem","20":"5rem","24":"6rem","28":"7rem","32":"8rem","36":"9rem","40":"10rem","44":"11rem","48":"12rem","52":"13rem","56":"14rem","60":"15rem","64":"16rem","72":"18rem","80":"20rem","96":"24rem","px":"1px","0.5":"0.125rem","1.5":"0.375rem","2.5":"0.625rem","3.5":"0.875rem","-0":"0px","-1":"-0.25rem","-2":"-0.5rem","-3":"-0.75rem","-4":"-1rem","-5":"-1.25rem","-6":"-1.5rem","-7":"-1.75rem","-8":"-2rem","-9":"-2.25rem","-10":"-2.5rem","-11":"-2.75rem","-12":"-3rem","-14":"-3.5rem","-16":"-4rem","-20":"-5rem","-24":"-6rem","-28":"-7rem","-32":"-8rem","-36":"-9rem","-40":"-10rem","-44":"-11rem","-48":"-12rem","-52":"-13rem","-56":"-14rem","-60":"-15rem","-64":"-16rem","-72":"-18rem","-80":"-20rem","-96":"-24rem","-px":"-1px","-0.5":"-0.125rem","-1.5":"-0.375rem","-2.5":"-0.625rem","-3.5":"-0.875rem"},"stroke":{"current":"currentColor"},"strokeWidth":{"0":"0","1":"1","2":"2"},"textColor":{"transparent":"transparent","current":"currentColor","black":"#000","white":"#fff","gray":{"50":"#f9fafb","100":"#f3f4f6","200":"#e5e7eb","300":"#d1d5db","400":"#9ca3af","500":"#6b7280","600":"#4b5563","700":"#374151","800":"#1f2937","900":"#111827"},"red":{"50":"#fef2f2","100":"#fee2e2","200":"#fecaca","300":"#fca5a5","400":"#f87171","500":"#ef4444","600":"#dc2626","700":"#b91c1c","800":"#991b1b","900":"#7f1d1d"},"yellow":{"50":"#fffbeb","100":"#fef3c7","200":"#fde68a","300":"#fcd34d","400":"#fbbf24","500":"#f59e0b","600":"#d97706","700":"#b45309","800":"#92400e","900":"#78350f"},"green":{"50":"#ecfdf5","100":"#d1fae5","200":"#a7f3d0","300":"#6ee7b7","400":"#34d399","500":"#10b981","600":"#059669","700":"#047857","800":"#065f46","900":"#064e3b"},"blue":{"50":"#eff6ff","100":"#dbeafe","200":"#bfdbfe","300":"#93c5fd","400":"#60a5fa","500":"#3b82f6","600":"#2563eb","700":"#1d4ed8","800":"#1e40af","900":"#1e3a8a"},"indigo":{"50":"#eef2ff","100":"#e0e7ff","200":"#c7d2fe","300":"#a5b4fc","400":"#818cf8","500":"#6366f1","600":"#4f46e5","700":"#4338ca","800":"#3730a3","900":"#312e81"},"purple":{"50":"#f5f3ff","100":"#ede9fe","200":"#ddd6fe","300":"#c4b5fd","400":"#a78bfa","500":"#8b5cf6","600":"#7c3aed","700":"#6d28d9","800":"#5b21b6","900":"#4c1d95"},"pink":{"50":"#fdf2f8","100":"#fce7f3","200":"#fbcfe8","300":"#f9a8d4","400":"#f472b6","500":"#ec4899","600":"#db2777","700":"#be185d","800":"#9d174d","900":"#831843"}},"textOpacity":{"0":"0","5":"0.05","10":"0.1","20":"0.2","25":"0.25","30":"0.3","40":"0.4","50":"0.5","60":"0.6","70":"0.7","75":"0.75","80":"0.8","90":"0.9","95":"0.95","100":"1"},"transitionDuration":{"75":"75ms","100":"100ms","150":"150ms","200":"200ms","300":"300ms","500":"500ms","700":"700ms","1000":"1000ms","DEFAULT":"150ms"},"transitionDelay":{"75":"75ms","100":"100ms","150":"150ms","200":"200ms","300":"300ms","500":"500ms","700":"700ms","1000":"1000ms"},"transitionProperty":{"none":"none","all":"all","DEFAULT":"background-color, border-color, color, fill, stroke, opacity, box-shadow, transform","colors":"background-color, border-color, color, fill, stroke","opacity":"opacity","shadow":"box-shadow","transform":"transform"},"transitionTimingFunction":{"DEFAULT":"cubic-bezier(0.4, 0, 0.2, 1)","linear":"linear","in":"cubic-bezier(0.4, 0, 1, 1)","out":"cubic-bezier(0, 0, 0.2, 1)","in-out":"cubic-bezier(0.4, 0, 0.2, 1)"},"translate":{"0":"0px","1":"0.25rem","2":"0.5rem","3":"0.75rem","4":"1rem","5":"1.25rem","6":"1.5rem","7":"1.75rem","8":"2rem","9":"2.25rem","10":"2.5rem","11":"2.75rem","12":"3rem","14":"3.5rem","16":"4rem","20":"5rem","24":"6rem","28":"7rem","32":"8rem","36":"9rem","40":"10rem","44":"11rem","48":"12rem","52":"13rem","56":"14rem","60":"15rem","64":"16rem","72":"18rem","80":"20rem","96":"24rem","px":"1px","0.5":"0.125rem","1.5":"0.375rem","2.5":"0.625rem","3.5":"0.875rem","-0":"0px","-1":"-0.25rem","-2":"-0.5rem","-3":"-0.75rem","-4":"-1rem","-5":"-1.25rem","-6":"-1.5rem","-7":"-1.75rem","-8":"-2rem","-9":"-2.25rem","-10":"-2.5rem","-11":"-2.75rem","-12":"-3rem","-14":"-3.5rem","-16":"-4rem","-20":"-5rem","-24":"-6rem","-28":"-7rem","-32":"-8rem","-36":"-9rem","-40":"-10rem","-44":"-11rem","-48":"-12rem","-52":"-13rem","-56":"-14rem","-60":"-15rem","-64":"-16rem","-72":"-18rem","-80":"-20rem","-96":"-24rem","-px":"-1px","-0.5":"-0.125rem","-1.5":"-0.375rem","-2.5":"-0.625rem","-3.5":"-0.875rem","1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%","full":"100%","-1/2":"-50%","-1/3":"-33.333333%","-2/3":"-66.666667%","-1/4":"-25%","-2/4":"-50%","-3/4":"-75%","-full":"-100%"},"width":{"0":"0px","1":"0.25rem","2":"0.5rem","3":"0.75rem","4":"1rem","5":"1.25rem","6":"1.5rem","7":"1.75rem","8":"2rem","9":"2.25rem","10":"2.5rem","11":"2.75rem","12":"3rem","14":"3.5rem","16":"4rem","20":"5rem","24":"6rem","28":"7rem","32":"8rem","36":"9rem","40":"10rem","44":"11rem","48":"12rem","52":"13rem","56":"14rem","60":"15rem","64":"16rem","72":"18rem","80":"20rem","96":"24rem","auto":"auto","px":"1px","0.5":"0.125rem","1.5":"0.375rem","2.5":"0.625rem","3.5":"0.875rem","1/2":"50%","1/3":"33.333333%","2/3":"66.666667%","1/4":"25%","2/4":"50%","3/4":"75%","1/5":"20%","2/5":"40%","3/5":"60%","4/5":"80%","1/6":"16.666667%","2/6":"33.333333%","3/6":"50%","4/6":"66.666667%","5/6":"83.333333%","1/12":"8.333333%","2/12":"16.666667%","3/12":"25%","4/12":"33.333333%","5/12":"41.666667%","6/12":"50%","7/12":"58.333333%","8/12":"66.666667%","9/12":"75%","10/12":"83.333333%","11/12":"91.666667%","full":"100%","screen":"100vw","min":"min-content","max":"max-content"},"zIndex":{"0":"0","10":"10","20":"20","30":"30","40":"40","50":"50","auto":"auto"}},"variants":{"accessibility":["responsive","focus-within","focus"],"alignContent":["responsive"],"alignItems":["responsive"],"alignSelf":["responsive"],"animation":["responsive"],"appearance":["responsive"],"backgroundAttachment":["responsive"],"backgroundClip":["responsive"],"backgroundColor":["responsive","dark","group-hover","focus-within","hover","focus"],"backgroundImage":["responsive"],"backgroundOpacity":["responsive","group-hover","focus-within","hover","focus"],"backgroundPosition":["responsive"],"backgroundRepeat":["responsive"],"backgroundSize":["responsive"],"borderCollapse":["responsive"],"borderColor":["responsive","dark","group-hover","focus-within","hover","focus"],"borderOpacity":["responsive","group-hover","focus-within","hover","focus"],"borderRadius":["responsive"],"borderStyle":["responsive"],"borderWidth":["responsive"],"boxShadow":["responsive","group-hover","focus-within","hover","focus"],"boxSizing":["responsive"],"clear":["responsive"],"container":["responsive"],"cursor":["responsive"],"display":["responsive"],"divideColor":["responsive","dark"],"divideOpacity":["responsive"],"divideStyle":["responsive"],"divideWidth":["responsive"],"fill":["responsive"],"flex":["responsive"],"flexDirection":["responsive"],"flexGrow":["responsive"],"flexShrink":["responsive"],"flexWrap":["responsive"],"float":["responsive"],"fontFamily":["responsive"],"fontSize":["responsive"],"fontSmoothing":["responsive"],"fontStyle":["responsive"],"fontVariantNumeric":["responsive"],"fontWeight":["responsive"],"gap":["responsive"],"gradientColorStops":["responsive","dark","hover","focus"],"gridAutoColumns":["responsive"],"gridAutoFlow":["responsive"],"gridAutoRows":["responsive"],"gridColumn":["responsive"],"gridColumnEnd":["responsive"],"gridColumnStart":["responsive"],"gridRow":["responsive"],"gridRowEnd":["responsive"],"gridRowStart":["responsive"],"gridTemplateColumns":["responsive"],"gridTemplateRows":["responsive"],"height":["responsive"],"inset":["responsive"],"justifyContent":["responsive"],"justifyItems":["responsive"],"justifySelf":["responsive"],"letterSpacing":["responsive"],"lineHeight":["responsive"],"listStylePosition":["responsive"],"listStyleType":["responsive"],"margin":["responsive"],"maxHeight":["responsive"],"maxWidth":["responsive"],"minHeight":["responsive"],"minWidth":["responsive"],"objectFit":["responsive"],"objectPosition":["responsive"],"opacity":["responsive","group-hover","focus-within","hover","focus"],"order":["responsive"],"outline":["responsive","focus-within","focus"],"overflow":["responsive"],"overscrollBehavior":["responsive"],"padding":["responsive"],"placeContent":["responsive"],"placeItems":["responsive"],"placeSelf":["responsive"],"placeholderColor":["responsive","dark","focus"],"placeholderOpacity":["responsive","focus"],"pointerEvents":["responsive"],"position":["responsive"],"resize":["responsive"],"ringColor":["responsive","dark","focus-within","focus"],"ringOffsetColor":["responsive","dark","focus-within","focus"],"ringOffsetWidth":["responsive","focus-within","focus"],"ringOpacity":["responsive","focus-within","focus"],"ringWidth":["responsive","focus-within","focus"],"rotate":["responsive","hover","focus"],"scale":["responsive","hover","focus"],"skew":["responsive","hover","focus"],"space":["responsive"],"stroke":["responsive"],"strokeWidth":["responsive"],"tableLayout":["responsive"],"textAlign":["responsive"],"textColor":["responsive","dark","group-hover","focus-within","hover","focus"],"textDecoration":["responsive","group-hover","focus-within","hover","focus"],"textOpacity":["responsive","group-hover","focus-within","hover","focus"],"textOverflow":["responsive"],"textTransform":["responsive"],"transform":["responsive"],"transformOrigin":["responsive"],"transitionDelay":["responsive"],"transitionDuration":["responsive"],"transitionProperty":["responsive"],"transitionTimingFunction":["responsive"],"translate":["responsive","hover","focus"],"userSelect":["responsive"],"verticalAlign":["responsive"],"visibility":["responsive"],"whitespace":["responsive"],"width":["responsive"],"wordBreak":["responsive"],"zIndex":["responsive","focus-within","focus"],"aspectRatio":["responsive"],"typography":["responsive"]},"corePlugins":["preflight","container","space","divideWidth","divideColor","divideStyle","divideOpacity","accessibility","appearance","backgroundAttachment","backgroundClip","backgroundColor","backgroundImage","gradientColorStops","backgroundOpacity","backgroundPosition","backgroundRepeat","backgroundSize","borderCollapse","borderColor","borderOpacity","borderRadius","borderStyle","borderWidth","boxSizing","cursor","display","flexDirection","flexWrap","placeItems","placeContent","placeSelf","alignItems","alignContent","alignSelf","justifyItems","justifyContent","justifySelf","flex","flexGrow","flexShrink","order","float","clear","fontFamily","fontWeight","height","fontSize","lineHeight","listStylePosition","listStyleType","margin","maxHeight","maxWidth","minHeight","minWidth","objectFit","objectPosition","opacity","outline","overflow","overscrollBehavior","padding","placeholderColor","placeholderOpacity","pointerEvents","position","inset","resize","boxShadow","ringWidth","ringOffsetColor","ringOffsetWidth","ringColor","ringOpacity","fill","stroke","strokeWidth","tableLayout","textAlign","textColor","textOpacity","textOverflow","fontStyle","textTransform","textDecoration","fontSmoothing","fontVariantNumeric","letterSpacing","userSelect","verticalAlign","visibility","whitespace","wordBreak","width","zIndex","gap","gridAutoFlow","gridTemplateColumns","gridAutoColumns","gridColumn","gridColumnStart","gridColumnEnd","gridTemplateRows","gridAutoRows","gridRow","gridRowStart","gridRowEnd","transform","transformOrigin","scale","rotate","translate","skew","transitionProperty","transitionTimingFunction","transitionDuration","transitionDelay","animation"],"plugins":[{},null,{"config":{"theme":{"aspectRatio":{"1":"1","2":"2","3":"3","4":"4","5":"5","6":"6","7":"7","8":"8","9":"9","10":"10","11":"11","12":"12","13":"13","14":"14","15":"15","16":"16"}},"variants":{"aspectRatio":["responsive"]}}},{}],"purge":[],"darkMode":false,"presets":[],"variantOrder":["first","last","odd","even","visited","checked","group-hover","group-focus","focus-within","hover","focus","focus-visible","active","disabled"],"prefix":"","important":false,"separator":":"} -------------------------------------------------------------------------------- /src/models/Root.ts: -------------------------------------------------------------------------------- 1 | import "mobx-react-lite/batchingForReactDom"; 2 | import { Instance, onSnapshot, types } from "mobx-state-tree"; 3 | import { createContext, useContext } from "react"; 4 | import { TailwindConfig } from "../@types/tailwind"; 5 | import { PluginMessage } from "../code"; 6 | import { fetchConfigColors } from "../core/config"; 7 | import defaultConfig from "../generated/tw.min.json"; 8 | 9 | const RootModel = types 10 | .model({ 11 | twPrefix: types.optional(types.string, ""), 12 | addSpaces: types.optional(types.boolean, false), 13 | overrideStyles: types.optional(types.boolean, false), 14 | configName: types.optional(types.maybeNull(types.string), null), 15 | errorMessage: types.optional(types.maybeNull(types.string), null), 16 | }) 17 | .volatile(() => { 18 | const reader = new FileReader(); 19 | const config: TailwindConfig = { 20 | theme: {}, 21 | variants: {}, 22 | plugins: [], 23 | darkMode: false, 24 | presets: [], 25 | variantOrder: [ 26 | "first", 27 | "last", 28 | "odd", 29 | "even", 30 | "visited", 31 | "checked", 32 | "group-hover", 33 | "group-focus", 34 | "focus-within", 35 | "hover", 36 | "focus", 37 | "focus-visible", 38 | "active", 39 | "disabled", 40 | ], 41 | }; 42 | return { 43 | reader, 44 | config, 45 | }; 46 | }) 47 | .views((self) => ({ 48 | get loadedTailwindConfig(): TailwindConfig { 49 | return self.config; 50 | }, 51 | })) 52 | .actions((self) => ({ 53 | handleReadAbort: () => { 54 | alert("Read was aborted"); 55 | }, 56 | handleReadError: () => { 57 | alert("Read failed"); 58 | }, 59 | setAddSpaces: (value: boolean) => { 60 | self.addSpaces = value; 61 | }, 62 | setOverrideStyles: (value: boolean) => { 63 | self.overrideStyles = value; 64 | }, 65 | setTwPrefix: (prefix: string) => { 66 | self.twPrefix = prefix; 67 | }, 68 | setConfig: (config: TailwindConfig) => { 69 | self.config = config; 70 | }, 71 | setConfigName: (name: string | null) => { 72 | self.configName = name; 73 | }, 74 | setErrorMessage: (msg: string | null) => { 75 | self.errorMessage = msg; 76 | }, 77 | sendPluginMessage: (pluginMessage: PluginMessage) => { 78 | parent.postMessage( 79 | { 80 | pluginMessage, 81 | }, 82 | "*" 83 | ); 84 | }, 85 | })) 86 | .actions((self) => ({ 87 | afterCreate: () => { 88 | self.reader.onabort = (e) => self.handleReadAbort; 89 | self.reader.onerror = (e) => self.handleReadError; 90 | self.reader.onload = () => { 91 | const binaryString = self.reader.result as string; 92 | try { 93 | self.setConfig(JSON.parse(binaryString)); 94 | } catch (error) { 95 | new Error(error); 96 | self.setErrorMessage("Parsing failed!"); 97 | } 98 | }; 99 | }, 100 | readFile: (fileName: string, file: File) => { 101 | self.setErrorMessage(null); 102 | self.setConfigName(fileName); 103 | self.reader.readAsBinaryString(file); 104 | }, 105 | loadDefaultStub: () => { 106 | self.setErrorMessage(null); 107 | self.setConfigName("Default Config"); 108 | self.setConfig(defaultConfig); 109 | }, 110 | addColorStyles: (prefix: string) => { 111 | self.sendPluginMessage({ 112 | type: "ADD_COLORS", 113 | payload: { 114 | prefix: prefix, 115 | config: fetchConfigColors(self.config), 116 | overrideStyles: self.overrideStyles, 117 | addSpaces: self.addSpaces, 118 | }, 119 | }); 120 | }, 121 | })); 122 | 123 | export const rootStore = RootModel.create({}); 124 | 125 | onSnapshot(rootStore, (snapshot) => console.log("Snapshot: ", snapshot)); 126 | 127 | export type RootInstance = Instance; 128 | const RootStoreContext = createContext(null); 129 | 130 | export const Provider = RootStoreContext.Provider; 131 | export function useMst() { 132 | const store = useContext(RootStoreContext); 133 | if (store === null) { 134 | throw new Error("Store cannot be null, please add a context provider"); 135 | } 136 | return store as RootInstance; 137 | } 138 | -------------------------------------------------------------------------------- /src/resources/css/ui.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @tailwind components; 4 | 5 | .link { 6 | transition: all 0.27s cubic-bezier(0.3, 0.1, 0.58, 1) 0s; 7 | } 8 | 9 | button { 10 | @apply outline-none; 11 | } 12 | 13 | button:focus { 14 | @apply outline-none; 15 | } 16 | 17 | button { 18 | transition: all 0.27s cubic-bezier(0.3, 0.1, 0.58, 1) 0s; 19 | } 20 | 21 | @tailwind utilities; 22 | -------------------------------------------------------------------------------- /src/resources/data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecklf/tailwindcss-figma-plugin/114e846993c33f714e0dd4699af7b67fc36d635b/src/resources/data/.gitkeep -------------------------------------------------------------------------------- /src/ui.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require("tailwindcss/colors"); 2 | 3 | module.exports = { 4 | mode: "jit", 5 | purge: ["./src/**/*.{js,ts,tsx,html}"], 6 | darkMode: false, // or 'media' or 'class' 7 | theme: { 8 | extend: { 9 | colors: { 10 | teal: colors.teal, 11 | }, 12 | }, 13 | }, 14 | variants: { 15 | extend: {}, 16 | }, 17 | plugins: [require("@tailwindcss/forms")], 18 | }; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "es2016", "es2017.object"], 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "target": "es6", 7 | "jsx": "react", 8 | "allowSyntheticDefaultImports": true, 9 | "module": "commonjs", 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "moduleResolution": "node" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackInlineSourcePlugin = require("html-webpack-inline-source-plugin"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const path = require("path"); 4 | const trimEnd = require("lodash/trimEnd"); 5 | 6 | module.exports = (env, argv) => ({ 7 | mode: argv.mode === "production" ? "production" : "development", 8 | 9 | // This is necessary because Figma's 'eval' works differently than normal eval 10 | devtool: argv.mode === "production" ? false : "inline-source-map", 11 | 12 | entry: { 13 | ui: "./src/App.tsx", // The entry point for your UI code 14 | code: "./src/code.ts", // The entry point for your plugin code 15 | }, 16 | 17 | module: { 18 | rules: [ 19 | // Converts TypeScript code to JavaScript 20 | { test: /\.tsx?$/, use: "ts-loader", exclude: /node_modules/ }, 21 | // Enables including CSS by doing "import './file.css'" in your TypeScript code 22 | { 23 | test: /\.css$/, 24 | use: [ 25 | "style-loader", 26 | { loader: "css-loader", options: { importLoaders: 1 } }, 27 | { 28 | loader: "postcss-loader", 29 | options: { 30 | postcssOptions: { 31 | plugins: [ 32 | require("tailwindcss")("./tailwind.config.js"), 33 | require("autoprefixer"), 34 | require("cssnano"), 35 | ], 36 | }, 37 | }, 38 | }, 39 | ], 40 | }, 41 | // Allows you to use "<%= require('./file.svg') %>" in your HTML code to get a data URI 42 | { 43 | test: /\.(png|jpg|gif|webp|svg|zip)$/, 44 | loader: [{ loader: "url-loader" }], 45 | }, 46 | ], 47 | }, 48 | 49 | // Webpack tries these extensions for you if you omit the extension like "import './file'" 50 | resolve: { extensions: [".ts", ".tsx", ".js"] }, 51 | 52 | output: { 53 | filename: "[name].js", 54 | path: path.resolve(__dirname, "dist"), // Compile into a folder called "dist" 55 | }, 56 | 57 | // Tells Webpack to generate "ui.html" and to inline "ui.ts" into it 58 | plugins: [ 59 | new HtmlWebpackPlugin({ 60 | template: "./src/ui.html", 61 | filename: "ui.html", 62 | inlineSource: ".(js)$", 63 | chunks: ["ui"], 64 | }), 65 | new HtmlWebpackInlineSourcePlugin(), 66 | ], 67 | }); 68 | --------------------------------------------------------------------------------