├── LICENSE ├── README.md ├── assets ├── Auto Theme Art.png ├── Auto Theme Logo.png └── auto-theme-example.gif ├── figma.d.ts ├── manifest.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── assets │ │ ├── arrow.svg │ │ ├── boolean_operation.svg │ │ ├── caret.svg │ │ ├── check.svg │ │ ├── component.svg │ │ ├── component_set.svg │ │ ├── confetti.svg │ │ ├── context.svg │ │ ├── effects.svg │ │ ├── ellipse.svg │ │ ├── fill.svg │ │ ├── frame.svg │ │ ├── group.svg │ │ ├── instance.svg │ │ ├── layers.svg │ │ ├── line.svg │ │ ├── logo.svg │ │ ├── placeholder-image.png │ │ ├── placeholder.svg │ │ ├── polygon.svg │ │ ├── radius.svg │ │ ├── rectangle.svg │ │ ├── refresh.svg │ │ ├── settings.svg │ │ ├── slice.svg │ │ ├── smile.svg │ │ ├── star.svg │ │ ├── stroke.svg │ │ ├── text.svg │ │ └── vector.svg │ ├── components │ │ ├── App.tsx │ │ └── ListItem.tsx │ ├── index.html │ ├── index.tsx │ └── styles │ │ ├── controls.css │ │ ├── empty-state.css │ │ ├── figma-plugin-ds.css │ │ ├── nav.css │ │ └── ui.css └── plugin │ ├── controller.ts │ ├── dark-to-light-theme.ts │ ├── example-theme.ts │ └── light-to-dark-theme.ts ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── yarn.lock /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Daniel Destefanis 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 | # Auto Theme 2 | 3 | ![alt text](https://github.com/destefanis/auto-theme/blob/master/assets/Auto%20Theme%20Art.png?raw=true "Auto Theme Cover Art") 4 | 5 | A figma plugin for automatically theming your designs from one color mapping to another. This was built specifically for use by the Discord design team. 6 | 7 | ## How to run locally 8 | * Run `yarn` to install dependencies. 9 | * Run `yarn build:watch` to start webpack in watch mode. 10 | 11 | ⭐ To change the UI of your plugin (the react code), start editing [App.tsx](./src/app/components/App.tsx). 12 | ⭐ To interact with the Figma API edit [controller.ts](./src/plugin/controller.ts). 13 | ⭐ Read more on the [Figma API Overview](https://www.figma.com/plugin-docs/api/api-overview/). 14 | 15 | ## How to use this plugin with your team 16 | * Follow the instructions for running locally 17 | * Set up your own themes, see the examples below in the Theme Object section. 18 | * In Figma, create a plugin and select this Auto Theme plugin manifest file. 19 | * Upload the plugin images from the asset directory, then hit publish internally. 20 | 21 | ## How it works 22 | * When a frame or multiple frames are selected the code loops through each layer. 23 | * During the loop, the layer checks to see what "type" the layer is (text, vector, rectangle etc). This allows us to skip certain nodes and handle mappings different for text and shapes. 24 | * If the layer has a fill, it fetches that nodes Style ID using `figma.getStyleById`. 25 | * It then imports that style from our main library using `figma.importStyleByKeyAsync` 26 | * Once we have that styles `key` then we check to see if it has a match in one of our theme objects, if it has a match we update that node with a new color. 27 | 28 | ![alt text](https://github.com/destefanis/auto-theme/blob/master/assets/auto-theme-example.gif?raw=true "Auto Theme Gif Example") 29 | 30 | ## Theme Object 31 | 32 | Themes are objects with key value pairs to handle how we map each color to another corresponding color. [See example theme](https://github.com/destefanis/auto-theme/blob/master/src/plugin/example-theme.ts). 33 | 34 | ``` 35 | '4b93d40f61be15e255e87948a715521c3ae957e6': { 36 | name: "Dark / Header / Primary (White)", 37 | mapsToName: "Light / Header / Primary (900)", 38 | mapsToKey: '3eddc15e90bbd7064aea7cc13dc13e23a712f0b0', 39 | }, 40 | ``` 41 | 42 | The first string of numbers is our `style.key` which in our design system is called "Dark / Header / Primary (White)". This color in light theme is "Light / Header / Primary (900)", so we replace our first key with the `mapsToKey` string. Swapping one style key for another. 43 | 44 | ``` 45 | "style_key_goes_here": { 46 | name: "", 47 | mapsToKey: "style_key_to_switch_with_goes_here", 48 | mapsToName: "", 49 | }, 50 | ``` 51 | 52 | This does mean you'll need to know the `keys` of each of your styles. 53 | 54 | ### How do I find my style keys? 55 | I built [Inspector Plugin](https://www.figma.com/community/plugin/760351147138040099) for this very reason. 56 | 57 | ### Instance Switching 58 | 59 | Some of your designs may use components like the status bar on iOS. In order to solve for this, the plugin allows you to swap instances of components. 60 | 61 | ``` 62 | "component_key_goes_here": { 63 | name: "", 64 | mapsToKey: "component_key_to_switch_with_goes_here", 65 | mapsToName: "", 66 | }, 67 | ``` 68 | 69 | This way if you'd like to switch `iPhone X Status Bar / Dark` with `iPhone X Status Bar / Light` rather than try and theme them, you can. Only instances will check to see if it's parent component is listed in the themes you've declared, otherwise it will be treated normally. 70 | 71 | ### Can I use multiple themes? 72 | Yes, create a new theme and import it, then hook up a button in the UI to send a message to the [controller.ts](https://github.com/destefanis/auto-theme/blob/master/src/plugin/controller.ts#L60) to 73 | call that theme. There are two examples of this in the code already. 74 | 75 | ## Toolings 76 | This repo is using: 77 | * React + Webpack 78 | * TypeScript 79 | * TSLint 80 | * Prettier precommit hook 81 | -------------------------------------------------------------------------------- /assets/Auto Theme Art.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/destefanis/auto-theme/9776a153cfde55e8788f702232ded89567dbd567/assets/Auto Theme Art.png -------------------------------------------------------------------------------- /assets/Auto Theme Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/destefanis/auto-theme/9776a153cfde55e8788f702232ded89567dbd567/assets/Auto Theme Logo.png -------------------------------------------------------------------------------- /assets/auto-theme-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/destefanis/auto-theme/9776a153cfde55e8788f702232ded89567dbd567/assets/auto-theme-example.gif -------------------------------------------------------------------------------- /figma.d.ts: -------------------------------------------------------------------------------- 1 | // Figma Plugin API version 1, update 10 2 | 3 | declare global { 4 | // Global variable with Figma's plugin API. 5 | const figma: PluginAPI; 6 | const __html__: string; 7 | 8 | interface PluginAPI { 9 | readonly apiVersion: "1.0.0"; 10 | readonly command: string; 11 | readonly viewport: ViewportAPI; 12 | closePlugin(message?: string): void; 13 | 14 | notify(message: string, options?: NotificationOptions): NotificationHandler; 15 | 16 | showUI(html: string, options?: ShowUIOptions): void; 17 | readonly ui: UIAPI; 18 | 19 | readonly clientStorage: ClientStorageAPI; 20 | 21 | getNodeById(id: string): BaseNode | null; 22 | getStyleById(id: string): BaseStyle | null; 23 | 24 | readonly root: DocumentNode; 25 | currentPage: PageNode; 26 | 27 | on( 28 | type: "selectionchange" | "currentpagechange" | "close", 29 | callback: () => void 30 | ): void; 31 | once( 32 | type: "selectionchange" | "currentpagechange" | "close", 33 | callback: () => void 34 | ): void; 35 | off( 36 | type: "selectionchange" | "currentpagechange" | "close", 37 | callback: () => void 38 | ): void; 39 | 40 | readonly mixed: unique symbol; 41 | 42 | createRectangle(): RectangleNode; 43 | createLine(): LineNode; 44 | createEllipse(): EllipseNode; 45 | createPolygon(): PolygonNode; 46 | createStar(): StarNode; 47 | createVector(): VectorNode; 48 | createText(): TextNode; 49 | createFrame(): FrameNode; 50 | createComponent(): ComponentNode; 51 | createPage(): PageNode; 52 | createSlice(): SliceNode; 53 | /** 54 | * [DEPRECATED]: This API often fails to create a valid boolean operation. Use figma.union, figma.subtract, figma.intersect and figma.exclude instead. 55 | */ 56 | createBooleanOperation(): BooleanOperationNode; 57 | 58 | createPaintStyle(): PaintStyle; 59 | createTextStyle(): TextStyle; 60 | createEffectStyle(): EffectStyle; 61 | createGridStyle(): GridStyle; 62 | 63 | // The styles are returned in the same order as displayed in the UI. Only 64 | // local styles are returned. Never styles from team library. 65 | getLocalPaintStyles(): PaintStyle[]; 66 | getLocalTextStyles(): TextStyle[]; 67 | getLocalEffectStyles(): EffectStyle[]; 68 | getLocalGridStyles(): GridStyle[]; 69 | 70 | importComponentByKeyAsync(key: string): Promise; 71 | importStyleByKeyAsync(key: string): Promise; 72 | 73 | listAvailableFontsAsync(): Promise; 74 | loadFontAsync(fontName: FontName): Promise; 75 | readonly hasMissingFont: boolean; 76 | 77 | createNodeFromSvg(svg: string): FrameNode; 78 | 79 | createImage(data: Uint8Array): Image; 80 | getImageByHash(hash: string): Image; 81 | 82 | group( 83 | nodes: ReadonlyArray, 84 | parent: BaseNode & ChildrenMixin, 85 | index?: number 86 | ): GroupNode; 87 | flatten( 88 | nodes: ReadonlyArray, 89 | parent?: BaseNode & ChildrenMixin, 90 | index?: number 91 | ): VectorNode; 92 | 93 | union( 94 | nodes: ReadonlyArray, 95 | parent: BaseNode & ChildrenMixin, 96 | index?: number 97 | ): BooleanOperationNode; 98 | subtract( 99 | nodes: ReadonlyArray, 100 | parent: BaseNode & ChildrenMixin, 101 | index?: number 102 | ): BooleanOperationNode; 103 | intersect( 104 | nodes: ReadonlyArray, 105 | parent: BaseNode & ChildrenMixin, 106 | index?: number 107 | ): BooleanOperationNode; 108 | exclude( 109 | nodes: ReadonlyArray, 110 | parent: BaseNode & ChildrenMixin, 111 | index?: number 112 | ): BooleanOperationNode; 113 | } 114 | 115 | interface ClientStorageAPI { 116 | getAsync(key: string): Promise; 117 | setAsync(key: string, value: any): Promise; 118 | } 119 | 120 | interface NotificationOptions { 121 | timeout?: number; 122 | } 123 | 124 | interface NotificationHandler { 125 | cancel: () => void; 126 | } 127 | 128 | interface ShowUIOptions { 129 | visible?: boolean; 130 | width?: number; 131 | height?: number; 132 | } 133 | 134 | interface UIPostMessageOptions { 135 | origin?: string; 136 | } 137 | 138 | interface OnMessageProperties { 139 | origin: string; 140 | } 141 | 142 | type MessageEventHandler = ( 143 | pluginMessage: any, 144 | props: OnMessageProperties 145 | ) => void; 146 | 147 | interface UIAPI { 148 | show(): void; 149 | hide(): void; 150 | resize(width: number, height: number): void; 151 | close(): void; 152 | 153 | postMessage(pluginMessage: any, options?: UIPostMessageOptions): void; 154 | onmessage: MessageEventHandler | undefined; 155 | on(type: "message", callback: MessageEventHandler): void; 156 | once(type: "message", callback: MessageEventHandler): void; 157 | off(type: "message", callback: MessageEventHandler): void; 158 | } 159 | 160 | interface ViewportAPI { 161 | center: { x: number; y: number }; 162 | zoom: number; 163 | scrollAndZoomIntoView(nodes: ReadonlyArray): void; 164 | } 165 | 166 | //////////////////////////////////////////////////////////////////////////////// 167 | // Datatypes 168 | 169 | type Transform = [[number, number, number], [number, number, number]]; 170 | 171 | interface Vector { 172 | readonly x: number; 173 | readonly y: number; 174 | } 175 | 176 | interface RGB { 177 | readonly r: number; 178 | readonly g: number; 179 | readonly b: number; 180 | } 181 | 182 | interface RGBA { 183 | readonly r: number; 184 | readonly g: number; 185 | readonly b: number; 186 | readonly a: number; 187 | } 188 | 189 | interface FontName { 190 | readonly family: string; 191 | readonly style: string; 192 | } 193 | 194 | type TextCase = "ORIGINAL" | "UPPER" | "LOWER" | "TITLE"; 195 | 196 | type TextDecoration = "NONE" | "UNDERLINE" | "STRIKETHROUGH"; 197 | 198 | interface ArcData { 199 | readonly startingAngle: number; 200 | readonly endingAngle: number; 201 | readonly innerRadius: number; 202 | } 203 | 204 | interface ShadowEffect { 205 | readonly type: "DROP_SHADOW" | "INNER_SHADOW"; 206 | readonly color: RGBA; 207 | readonly offset: Vector; 208 | readonly radius: number; 209 | readonly visible: boolean; 210 | readonly blendMode: BlendMode; 211 | } 212 | 213 | interface BlurEffect { 214 | readonly type: "LAYER_BLUR" | "BACKGROUND_BLUR"; 215 | readonly radius: number; 216 | readonly visible: boolean; 217 | } 218 | 219 | type Effect = ShadowEffect | BlurEffect; 220 | 221 | type ConstraintType = "MIN" | "CENTER" | "MAX" | "STRETCH" | "SCALE"; 222 | 223 | interface Constraints { 224 | readonly horizontal: ConstraintType; 225 | readonly vertical: ConstraintType; 226 | } 227 | 228 | interface ColorStop { 229 | readonly position: number; 230 | readonly color: RGBA; 231 | } 232 | 233 | interface ImageFilters { 234 | readonly exposure?: number; 235 | readonly contrast?: number; 236 | readonly saturation?: number; 237 | readonly temperature?: number; 238 | readonly tint?: number; 239 | readonly highlights?: number; 240 | readonly shadows?: number; 241 | } 242 | 243 | interface SolidPaint { 244 | readonly type: "SOLID"; 245 | readonly color: RGB; 246 | 247 | readonly visible?: boolean; 248 | readonly opacity?: number; 249 | readonly blendMode?: BlendMode; 250 | } 251 | 252 | interface GradientPaint { 253 | readonly type: 254 | | "GRADIENT_LINEAR" 255 | | "GRADIENT_RADIAL" 256 | | "GRADIENT_ANGULAR" 257 | | "GRADIENT_DIAMOND"; 258 | readonly gradientTransform: Transform; 259 | readonly gradientStops: ReadonlyArray; 260 | 261 | readonly visible?: boolean; 262 | readonly opacity?: number; 263 | readonly blendMode?: BlendMode; 264 | } 265 | 266 | interface ImagePaint { 267 | readonly type: "IMAGE"; 268 | readonly scaleMode: "FILL" | "FIT" | "CROP" | "TILE"; 269 | readonly imageHash: string | null; 270 | readonly imageTransform?: Transform; // setting for "CROP" 271 | readonly scalingFactor?: number; // setting for "TILE" 272 | readonly filters?: ImageFilters; 273 | 274 | readonly visible?: boolean; 275 | readonly opacity?: number; 276 | readonly blendMode?: BlendMode; 277 | } 278 | 279 | type Paint = SolidPaint | GradientPaint | ImagePaint; 280 | 281 | interface Guide { 282 | readonly axis: "X" | "Y"; 283 | readonly offset: number; 284 | } 285 | 286 | interface RowsColsLayoutGrid { 287 | readonly pattern: "ROWS" | "COLUMNS"; 288 | readonly alignment: "MIN" | "MAX" | "STRETCH" | "CENTER"; 289 | readonly gutterSize: number; 290 | 291 | readonly count: number; // Infinity when "Auto" is set in the UI 292 | readonly sectionSize?: number; // Not set for alignment: "STRETCH" 293 | readonly offset?: number; // Not set for alignment: "CENTER" 294 | 295 | readonly visible?: boolean; 296 | readonly color?: RGBA; 297 | } 298 | 299 | interface GridLayoutGrid { 300 | readonly pattern: "GRID"; 301 | readonly sectionSize: number; 302 | 303 | readonly visible?: boolean; 304 | readonly color?: RGBA; 305 | } 306 | 307 | type LayoutGrid = RowsColsLayoutGrid | GridLayoutGrid; 308 | 309 | interface ExportSettingsConstraints { 310 | readonly type: "SCALE" | "WIDTH" | "HEIGHT"; 311 | readonly value: number; 312 | } 313 | 314 | interface ExportSettingsImage { 315 | readonly format: "JPG" | "PNG"; 316 | readonly contentsOnly?: boolean; // defaults to true 317 | readonly suffix?: string; 318 | readonly constraint?: ExportSettingsConstraints; 319 | } 320 | 321 | interface ExportSettingsSVG { 322 | readonly format: "SVG"; 323 | readonly contentsOnly?: boolean; // defaults to true 324 | readonly suffix?: string; 325 | readonly svgOutlineText?: boolean; // defaults to true 326 | readonly svgIdAttribute?: boolean; // defaults to false 327 | readonly svgSimplifyStroke?: boolean; // defaults to true 328 | } 329 | 330 | interface ExportSettingsPDF { 331 | readonly format: "PDF"; 332 | readonly contentsOnly?: boolean; // defaults to true 333 | readonly suffix?: string; 334 | } 335 | 336 | type ExportSettings = 337 | | ExportSettingsImage 338 | | ExportSettingsSVG 339 | | ExportSettingsPDF; 340 | 341 | type WindingRule = "NONZERO" | "EVENODD"; 342 | 343 | interface VectorVertex { 344 | readonly x: number; 345 | readonly y: number; 346 | readonly strokeCap?: StrokeCap; 347 | readonly strokeJoin?: StrokeJoin; 348 | readonly cornerRadius?: number; 349 | readonly handleMirroring?: HandleMirroring; 350 | } 351 | 352 | interface VectorSegment { 353 | readonly start: number; 354 | readonly end: number; 355 | readonly tangentStart?: Vector; // Defaults to { x: 0, y: 0 } 356 | readonly tangentEnd?: Vector; // Defaults to { x: 0, y: 0 } 357 | } 358 | 359 | interface VectorRegion { 360 | readonly windingRule: WindingRule; 361 | readonly loops: ReadonlyArray>; 362 | } 363 | 364 | interface VectorNetwork { 365 | readonly vertices: ReadonlyArray; 366 | readonly segments: ReadonlyArray; 367 | readonly regions?: ReadonlyArray; // Defaults to [] 368 | } 369 | 370 | interface VectorPath { 371 | readonly windingRule: WindingRule | "NONE"; 372 | readonly data: string; 373 | } 374 | 375 | type VectorPaths = ReadonlyArray; 376 | 377 | interface LetterSpacing { 378 | readonly value: number; 379 | readonly unit: "PIXELS" | "PERCENT"; 380 | } 381 | 382 | type LineHeight = 383 | | { 384 | readonly value: number; 385 | readonly unit: "PIXELS" | "PERCENT"; 386 | } 387 | | { 388 | readonly unit: "AUTO"; 389 | }; 390 | 391 | type BlendMode = 392 | | "PASS_THROUGH" 393 | | "NORMAL" 394 | | "DARKEN" 395 | | "MULTIPLY" 396 | | "LINEAR_BURN" 397 | | "COLOR_BURN" 398 | | "LIGHTEN" 399 | | "SCREEN" 400 | | "LINEAR_DODGE" 401 | | "COLOR_DODGE" 402 | | "OVERLAY" 403 | | "SOFT_LIGHT" 404 | | "HARD_LIGHT" 405 | | "DIFFERENCE" 406 | | "EXCLUSION" 407 | | "HUE" 408 | | "SATURATION" 409 | | "COLOR" 410 | | "LUMINOSITY"; 411 | 412 | interface Font { 413 | fontName: FontName; 414 | } 415 | 416 | type Reaction = { action: Action; trigger: Trigger }; 417 | 418 | type Action = 419 | | { readonly type: "BACK" | "CLOSE" } 420 | | { readonly type: "URL"; url: string } 421 | | { 422 | readonly type: "NODE"; 423 | readonly destinationId: string | null; 424 | readonly navigation: Navigation; 425 | readonly transition: Transition | null; 426 | readonly preserveScrollPosition: boolean; 427 | 428 | // Only present if navigation == "OVERLAY" and the destination uses 429 | // overlay position type "RELATIVE" 430 | readonly overlayRelativePosition?: Vector; 431 | }; 432 | 433 | interface SimpleTransition { 434 | readonly type: "DISSOLVE" | "SMART_ANIMATE"; 435 | readonly easing: Easing; 436 | readonly duration: number; 437 | } 438 | 439 | interface DirectionalTransition { 440 | readonly type: "MOVE_IN" | "MOVE_OUT" | "PUSH" | "SLIDE_IN" | "SLIDE_OUT"; 441 | readonly direction: "LEFT" | "RIGHT" | "TOP" | "BOTTOM"; 442 | readonly matchLayers: boolean; 443 | 444 | readonly easing: Easing; 445 | readonly duration: number; 446 | } 447 | 448 | export type Transition = SimpleTransition | DirectionalTransition; 449 | 450 | type Trigger = 451 | | { readonly type: "ON_CLICK" | "ON_HOVER" | "ON_PRESS" | "ON_DRAG" } 452 | | { readonly type: "AFTER_TIMEOUT"; readonly timeout: number } 453 | | { 454 | readonly type: 455 | | "MOUSE_ENTER" 456 | | "MOUSE_LEAVE" 457 | | "MOUSE_UP" 458 | | "MOUSE_DOWN"; 459 | readonly delay: number; 460 | }; 461 | 462 | type Navigation = "NAVIGATE" | "SWAP" | "OVERLAY"; 463 | 464 | interface Easing { 465 | readonly type: "EASE_IN" | "EASE_OUT" | "EASE_IN_AND_OUT" | "LINEAR"; 466 | } 467 | 468 | type OverflowDirection = "NONE" | "HORIZONTAL" | "VERTICAL" | "BOTH"; 469 | 470 | type OverlayPositionType = 471 | | "CENTER" 472 | | "TOP_LEFT" 473 | | "TOP_CENTER" 474 | | "TOP_RIGHT" 475 | | "BOTTOM_LEFT" 476 | | "BOTTOM_CENTER" 477 | | "BOTTOM_RIGHT" 478 | | "MANUAL"; 479 | 480 | type OverlayBackground = 481 | | { readonly type: "NONE" } 482 | | { readonly type: "SOLID_COLOR"; readonly color: RGBA }; 483 | 484 | type OverlayBackgroundInteraction = "NONE" | "CLOSE_ON_CLICK_OUTSIDE"; 485 | 486 | //////////////////////////////////////////////////////////////////////////////// 487 | // Mixins 488 | 489 | interface BaseNodeMixin { 490 | readonly id: string; 491 | readonly parent: (BaseNode & ChildrenMixin) | null; 492 | name: string; // Note: setting this also sets \`autoRename\` to false on TextNodes 493 | readonly removed: boolean; 494 | toString(): string; 495 | remove(): void; 496 | 497 | getPluginData(key: string): string; 498 | setPluginData(key: string, value: string): void; 499 | 500 | // Namespace is a string that must be at least 3 alphanumeric characters, and should 501 | // be a name related to your plugin. Other plugins will be able to read this data. 502 | getSharedPluginData(namespace: string, key: string): string; 503 | setSharedPluginData(namespace: string, key: string, value: string): void; 504 | } 505 | 506 | interface SceneNodeMixin { 507 | visible: boolean; 508 | locked: boolean; 509 | } 510 | 511 | interface ChildrenMixin { 512 | readonly children: ReadonlyArray; 513 | 514 | appendChild(child: SceneNode): void; 515 | insertChild(index: number, child: SceneNode): void; 516 | 517 | findAll(callback?: (node: SceneNode) => boolean): SceneNode[]; 518 | findOne(callback: (node: SceneNode) => boolean): SceneNode | null; 519 | } 520 | 521 | interface ConstraintMixin { 522 | constraints: Constraints; 523 | } 524 | 525 | interface LayoutMixin { 526 | readonly absoluteTransform: Transform; 527 | relativeTransform: Transform; 528 | x: number; 529 | y: number; 530 | rotation: number; // In degrees 531 | 532 | readonly width: number; 533 | readonly height: number; 534 | 535 | layoutAlign: "MIN" | "CENTER" | "MAX"; // applicable only inside auto-layout frames 536 | 537 | resize(width: number, height: number): void; 538 | resizeWithoutConstraints(width: number, height: number): void; 539 | } 540 | 541 | interface BlendMixin { 542 | opacity: number; 543 | blendMode: BlendMode; 544 | isMask: boolean; 545 | effects: ReadonlyArray; 546 | effectStyleId: string; 547 | } 548 | 549 | interface ContainerMixin { 550 | backgrounds: ReadonlyArray; // DEPRECATED: use 'fills' instead 551 | layoutGrids: ReadonlyArray; 552 | clipsContent: boolean; 553 | guides: ReadonlyArray; 554 | gridStyleId: string; 555 | backgroundStyleId: string; // DEPRECATED: use 'fillStyleId' instead 556 | } 557 | 558 | type StrokeCap = 559 | | "NONE" 560 | | "ROUND" 561 | | "SQUARE" 562 | | "ARROW_LINES" 563 | | "ARROW_EQUILATERAL"; 564 | type StrokeJoin = "MITER" | "BEVEL" | "ROUND"; 565 | type HandleMirroring = "NONE" | "ANGLE" | "ANGLE_AND_LENGTH"; 566 | 567 | interface GeometryMixin { 568 | fills: ReadonlyArray | PluginAPI["mixed"]; 569 | strokes: ReadonlyArray; 570 | strokeWeight: number; 571 | strokeAlign: "CENTER" | "INSIDE" | "OUTSIDE"; 572 | strokeCap: StrokeCap | PluginAPI["mixed"]; 573 | strokeJoin: StrokeJoin | PluginAPI["mixed"]; 574 | dashPattern: ReadonlyArray; 575 | fillStyleId: string | PluginAPI["mixed"]; 576 | strokeStyleId: string; 577 | } 578 | 579 | interface CornerMixin { 580 | cornerRadius: number | PluginAPI["mixed"]; 581 | cornerSmoothing: number; 582 | } 583 | 584 | interface RectangleCornerMixin { 585 | topLeftRadius: number; 586 | topRightRadius: number; 587 | bottomLeftRadius: number; 588 | bottomRightRadius: number; 589 | } 590 | 591 | interface ExportMixin { 592 | exportSettings: ReadonlyArray; 593 | exportAsync(settings?: ExportSettings): Promise; // Defaults to PNG format 594 | } 595 | 596 | interface ReactionMixin { 597 | readonly reactions: ReadonlyArray; 598 | } 599 | 600 | interface DefaultShapeMixin 601 | extends BaseNodeMixin, 602 | SceneNodeMixin, 603 | ReactionMixin, 604 | BlendMixin, 605 | GeometryMixin, 606 | LayoutMixin, 607 | ExportMixin {} 608 | 609 | interface DefaultFrameMixin 610 | extends BaseNodeMixin, 611 | SceneNodeMixin, 612 | ReactionMixin, 613 | ChildrenMixin, 614 | ContainerMixin, 615 | GeometryMixin, 616 | CornerMixin, 617 | RectangleCornerMixin, 618 | BlendMixin, 619 | ConstraintMixin, 620 | LayoutMixin, 621 | ExportMixin { 622 | layoutMode: "NONE" | "HORIZONTAL" | "VERTICAL"; 623 | counterAxisSizingMode: "FIXED" | "AUTO"; // applicable only if layoutMode != "NONE" 624 | horizontalPadding: number; // applicable only if layoutMode != "NONE" 625 | verticalPadding: number; // applicable only if layoutMode != "NONE" 626 | itemSpacing: number; // applicable only if layoutMode != "NONE" 627 | 628 | overflowDirection: OverflowDirection; 629 | numberOfFixedChildren: number; 630 | 631 | readonly overlayPositionType: OverlayPositionType; 632 | readonly overlayBackground: OverlayBackground; 633 | readonly overlayBackgroundInteraction: OverlayBackgroundInteraction; 634 | } 635 | 636 | //////////////////////////////////////////////////////////////////////////////// 637 | // Nodes 638 | 639 | interface DocumentNode extends BaseNodeMixin { 640 | readonly type: "DOCUMENT"; 641 | 642 | readonly children: ReadonlyArray; 643 | 644 | appendChild(child: PageNode): void; 645 | insertChild(index: number, child: PageNode): void; 646 | 647 | findAll( 648 | callback?: (node: PageNode | SceneNode) => boolean 649 | ): Array; 650 | findOne( 651 | callback: (node: PageNode | SceneNode) => boolean 652 | ): PageNode | SceneNode | null; 653 | } 654 | 655 | interface PageNode extends BaseNodeMixin, ChildrenMixin, ExportMixin { 656 | readonly type: "PAGE"; 657 | clone(): PageNode; 658 | 659 | guides: ReadonlyArray; 660 | selection: ReadonlyArray; 661 | 662 | backgrounds: ReadonlyArray; 663 | 664 | readonly prototypeStartNode: 665 | | FrameNode 666 | | GroupNode 667 | | ComponentNode 668 | | InstanceNode 669 | | null; 670 | } 671 | 672 | interface FrameNode extends DefaultFrameMixin { 673 | readonly type: "FRAME"; 674 | clone(): FrameNode; 675 | } 676 | 677 | interface GroupNode 678 | extends BaseNodeMixin, 679 | SceneNodeMixin, 680 | ReactionMixin, 681 | ChildrenMixin, 682 | ContainerMixin, 683 | BlendMixin, 684 | LayoutMixin, 685 | ExportMixin { 686 | readonly type: "GROUP"; 687 | clone(): GroupNode; 688 | } 689 | 690 | interface SliceNode 691 | extends BaseNodeMixin, 692 | SceneNodeMixin, 693 | LayoutMixin, 694 | ExportMixin { 695 | readonly type: "SLICE"; 696 | clone(): SliceNode; 697 | } 698 | 699 | interface RectangleNode 700 | extends DefaultShapeMixin, 701 | ConstraintMixin, 702 | CornerMixin, 703 | RectangleCornerMixin { 704 | readonly type: "RECTANGLE"; 705 | clone(): RectangleNode; 706 | } 707 | 708 | interface LineNode extends DefaultShapeMixin, ConstraintMixin { 709 | readonly type: "LINE"; 710 | clone(): LineNode; 711 | } 712 | 713 | interface EllipseNode 714 | extends DefaultShapeMixin, 715 | ConstraintMixin, 716 | CornerMixin { 717 | readonly type: "ELLIPSE"; 718 | clone(): EllipseNode; 719 | arcData: ArcData; 720 | } 721 | 722 | interface PolygonNode 723 | extends DefaultShapeMixin, 724 | ConstraintMixin, 725 | CornerMixin { 726 | readonly type: "POLYGON"; 727 | clone(): PolygonNode; 728 | pointCount: number; 729 | } 730 | 731 | interface StarNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 732 | readonly type: "STAR"; 733 | clone(): StarNode; 734 | pointCount: number; 735 | innerRadius: number; 736 | } 737 | 738 | interface VectorNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 739 | readonly type: "VECTOR"; 740 | clone(): VectorNode; 741 | vectorNetwork: VectorNetwork; 742 | vectorPaths: VectorPaths; 743 | handleMirroring: HandleMirroring | PluginAPI["mixed"]; 744 | } 745 | 746 | interface TextNode extends DefaultShapeMixin, ConstraintMixin { 747 | readonly type: "TEXT"; 748 | clone(): TextNode; 749 | characters: string; 750 | readonly hasMissingFont: boolean; 751 | textAlignHorizontal: "LEFT" | "CENTER" | "RIGHT" | "JUSTIFIED"; 752 | textAlignVertical: "TOP" | "CENTER" | "BOTTOM"; 753 | textAutoResize: "NONE" | "WIDTH_AND_HEIGHT" | "HEIGHT"; 754 | paragraphIndent: number; 755 | paragraphSpacing: number; 756 | autoRename: boolean; 757 | 758 | textStyleId: string | PluginAPI["mixed"]; 759 | fontSize: number | PluginAPI["mixed"]; 760 | fontName: FontName | PluginAPI["mixed"]; 761 | textCase: TextCase | PluginAPI["mixed"]; 762 | textDecoration: TextDecoration | PluginAPI["mixed"]; 763 | letterSpacing: LetterSpacing | PluginAPI["mixed"]; 764 | lineHeight: LineHeight | PluginAPI["mixed"]; 765 | 766 | getRangeFontSize(start: number, end: number): number | PluginAPI["mixed"]; 767 | setRangeFontSize(start: number, end: number, value: number): void; 768 | getRangeFontName(start: number, end: number): FontName | PluginAPI["mixed"]; 769 | setRangeFontName(start: number, end: number, value: FontName): void; 770 | getRangeTextCase(start: number, end: number): TextCase | PluginAPI["mixed"]; 771 | setRangeTextCase(start: number, end: number, value: TextCase): void; 772 | getRangeTextDecoration( 773 | start: number, 774 | end: number 775 | ): TextDecoration | PluginAPI["mixed"]; 776 | setRangeTextDecoration( 777 | start: number, 778 | end: number, 779 | value: TextDecoration 780 | ): void; 781 | getRangeLetterSpacing( 782 | start: number, 783 | end: number 784 | ): LetterSpacing | PluginAPI["mixed"]; 785 | setRangeLetterSpacing( 786 | start: number, 787 | end: number, 788 | value: LetterSpacing 789 | ): void; 790 | getRangeLineHeight( 791 | start: number, 792 | end: number 793 | ): LineHeight | PluginAPI["mixed"]; 794 | setRangeLineHeight(start: number, end: number, value: LineHeight): void; 795 | getRangeFills(start: number, end: number): Paint[] | PluginAPI["mixed"]; 796 | setRangeFills(start: number, end: number, value: Paint[]): void; 797 | getRangeTextStyleId( 798 | start: number, 799 | end: number 800 | ): string | PluginAPI["mixed"]; 801 | setRangeTextStyleId(start: number, end: number, value: string): void; 802 | getRangeFillStyleId( 803 | start: number, 804 | end: number 805 | ): string | PluginAPI["mixed"]; 806 | setRangeFillStyleId(start: number, end: number, value: string): void; 807 | } 808 | 809 | interface ComponentNode extends DefaultFrameMixin { 810 | readonly type: "COMPONENT"; 811 | clone(): ComponentNode; 812 | 813 | createInstance(): InstanceNode; 814 | description: string; 815 | readonly remote: boolean; 816 | readonly key: string; // The key to use with "importComponentByKeyAsync" 817 | } 818 | 819 | interface InstanceNode extends DefaultFrameMixin { 820 | readonly type: "INSTANCE"; 821 | clone(): InstanceNode; 822 | masterComponent: ComponentNode; 823 | } 824 | 825 | interface BooleanOperationNode 826 | extends DefaultShapeMixin, 827 | ChildrenMixin, 828 | CornerMixin { 829 | readonly type: "BOOLEAN_OPERATION"; 830 | clone(): BooleanOperationNode; 831 | booleanOperation: "UNION" | "INTERSECT" | "SUBTRACT" | "EXCLUDE"; 832 | } 833 | 834 | type BaseNode = DocumentNode | PageNode | SceneNode; 835 | 836 | type SceneNode = 837 | | SliceNode 838 | | FrameNode 839 | | GroupNode 840 | | ComponentNode 841 | | InstanceNode 842 | | BooleanOperationNode 843 | | VectorNode 844 | | StarNode 845 | | LineNode 846 | | EllipseNode 847 | | PolygonNode 848 | | RectangleNode 849 | | TextNode; 850 | 851 | type NodeType = 852 | | "DOCUMENT" 853 | | "PAGE" 854 | | "SLICE" 855 | | "FRAME" 856 | | "GROUP" 857 | | "COMPONENT" 858 | | "INSTANCE" 859 | | "BOOLEAN_OPERATION" 860 | | "VECTOR" 861 | | "STAR" 862 | | "LINE" 863 | | "ELLIPSE" 864 | | "POLYGON" 865 | | "RECTANGLE" 866 | | "TEXT"; 867 | 868 | //////////////////////////////////////////////////////////////////////////////// 869 | // Styles 870 | type StyleType = "PAINT" | "TEXT" | "EFFECT" | "GRID"; 871 | 872 | interface BaseStyle { 873 | readonly id: string; 874 | readonly type: StyleType; 875 | name: string; 876 | description: string; 877 | remote: boolean; 878 | readonly key: string; // The key to use with "importStyleByKeyAsync" 879 | remove(): void; 880 | } 881 | 882 | interface PaintStyle extends BaseStyle { 883 | type: "PAINT"; 884 | paints: ReadonlyArray; 885 | } 886 | 887 | interface TextStyle extends BaseStyle { 888 | type: "TEXT"; 889 | fontSize: number; 890 | textDecoration: TextDecoration; 891 | fontName: FontName; 892 | letterSpacing: LetterSpacing; 893 | lineHeight: LineHeight; 894 | paragraphIndent: number; 895 | paragraphSpacing: number; 896 | textCase: TextCase; 897 | } 898 | 899 | interface EffectStyle extends BaseStyle { 900 | type: "EFFECT"; 901 | effects: ReadonlyArray; 902 | } 903 | 904 | interface GridStyle extends BaseStyle { 905 | type: "GRID"; 906 | layoutGrids: ReadonlyArray; 907 | } 908 | 909 | //////////////////////////////////////////////////////////////////////////////// 910 | // Other 911 | 912 | interface Image { 913 | readonly hash: string; 914 | getBytesAsync(): Promise; 915 | } 916 | } // declare global 917 | 918 | export {}; 919 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Auto Theme", 3 | "id": "816463256448925575", 4 | "api": "1.0.0", 5 | "main": "dist/code.js", 6 | "ui": "dist/ui.html", 7 | "editorType": ["figma"] 8 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "figma-plugin-react-template", 3 | "version": "1.0.0", 4 | "description": "This plugin template uses Typescript. If you are familiar with Javascript, Typescript will look very familiar. In fact, valid Javascript code is already valid Typescript code.", 5 | "license": "ISC", 6 | "scripts": { 7 | "build": "/usr/local/bin/node node_modules/.bin/webpack --mode=production", 8 | "build:watch": "/usr/local/bin/node node_modules/.bin/webpack --mode=development --watch", 9 | "prettier:format": "prettier --write 'src/**/*.{js,jsx,ts,tsx,css,json}' " 10 | }, 11 | "dependencies": { 12 | "classnames": "^2.2.6", 13 | "react": "^16.8.6", 14 | "react-dom": "^16.8.6" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^16.8.24", 18 | "@types/react-dom": "^16.8.5", 19 | "css-loader": "^3.1.0", 20 | "html-webpack-inline-source-plugin": "^0.0.10", 21 | "html-webpack-plugin": "^3.2.0", 22 | "husky": "^3.0.2", 23 | "lint-staged": "^9.2.1", 24 | "prettier": "^1.18.2", 25 | "style-loader": "^0.23.1", 26 | "ts-loader": "^6.0.4", 27 | "tslint": "^5.18.0", 28 | "tslint-react": "^4.0.0", 29 | "typescript": "^3.5.3", 30 | "url-loader": "^2.1.0", 31 | "webpack": "^4.39.1", 32 | "webpack-cli": "^3.3.6" 33 | }, 34 | "husky": { 35 | "hooks": { 36 | "pre-commit": "lint-staged" 37 | } 38 | }, 39 | "lint-staged": { 40 | "src/**/*.{js,jsx,ts,tsx,css,json}": [ 41 | "prettier --write", 42 | "git add" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/assets/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/assets/boolean_operation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/assets/caret.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/assets/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/assets/component.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/assets/component_set.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/assets/confetti.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/app/assets/context.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/assets/effects.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/app/assets/ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/assets/fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/assets/frame.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/assets/group.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/assets/instance.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/assets/layers.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/app/assets/line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/app/assets/placeholder-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/destefanis/auto-theme/9776a153cfde55e8788f702232ded89567dbd567/src/app/assets/placeholder-image.png -------------------------------------------------------------------------------- /src/app/assets/placeholder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/assets/polygon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/assets/radius.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/assets/rectangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/assets/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/app/assets/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/app/assets/slice.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/assets/smile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/app/assets/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/assets/stroke.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/assets/text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/assets/vector.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/components/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import "../styles/figma-plugin-ds.css"; 3 | import "../styles/ui.css"; 4 | import "../styles/nav.css"; 5 | import "../styles/controls.css"; 6 | import "../styles/empty-state.css"; 7 | 8 | import ListItem from "./ListItem"; 9 | 10 | declare function require(path: string): any; 11 | 12 | const App = ({}) => { 13 | const [selectedLayersLength, setSelectLayersLength] = React.useState(0); 14 | const [activeTab, setActiveTab] = React.useState("themes"); 15 | const [skippedLayers, setSkippedLayers] = React.useState([]); 16 | const [activeLayer, setActiveLayer] = React.useState(0); 17 | 18 | const onRunApp = React.useCallback(() => { 19 | const message = ""; 20 | parent.postMessage({ pluginMessage: { type: "run-app", message } }, "*"); 21 | }, []); 22 | 23 | // This tells the controller.ts file to theme 24 | // our selection from dark to light. 25 | const themeToLight = React.useCallback(() => { 26 | const message = "dark-to-light-theme"; 27 | parent.postMessage( 28 | { pluginMessage: { type: "theme-update", message } }, 29 | "*" 30 | ); 31 | }, []); 32 | 33 | // This tells the controller.ts file to theme 34 | // our selection from light to dark. 35 | const themeToDark = React.useCallback(() => { 36 | const message = "light-to-dark-theme"; 37 | parent.postMessage( 38 | { pluginMessage: { type: "theme-update", message } }, 39 | "*" 40 | ); 41 | }, []); 42 | 43 | function setThemesActive() { 44 | setActiveTab("themes"); 45 | } 46 | 47 | function setLayersActive() { 48 | setActiveTab("layers"); 49 | } 50 | 51 | // When the user selects a layer in the skipped layer list. 52 | const handleLayerSelect = id => { 53 | setActiveLayer(id); 54 | parent.postMessage( 55 | { pluginMessage: { type: "select-layer", id: id } }, 56 | "*" 57 | ); 58 | }; 59 | 60 | React.useEffect(() => { 61 | onRunApp(); 62 | 63 | window.onmessage = event => { 64 | const { type, message } = event.data.pluginMessage; 65 | 66 | if (type === "selection-updated") { 67 | let nodeArray = JSON.parse(message); 68 | setSelectLayersLength(nodeArray.length); 69 | } 70 | 71 | if (type === "layers-skipped") { 72 | let unthemedLayers = JSON.parse(message); 73 | setSkippedLayers(skippedLayers => [ 74 | ...skippedLayers, 75 | ...unthemedLayers 76 | ]); 77 | } 78 | }; 79 | }, []); 80 | 81 | const listItems = skippedLayers.map((node, index) => ( 82 | 88 | )); 89 | 90 | return ( 91 |
92 | {selectedLayersLength === 0 ? ( 93 |
94 |
95 | 96 |
97 |

98 | Select a layer to get started. 99 |

100 |
101 | ) : ( 102 | 103 | 124 | {activeTab === "themes" ? ( 125 |
126 |

127 | {selectedLayersLength} layers selected for themeing 128 |

129 | 135 | 141 |
142 | ) : ( 143 |
144 | {skippedLayers.length === 0 ? ( 145 |
146 |

147 | No layers have been skipped yet. 148 |

149 |
150 | ) : ( 151 |
    {listItems}
152 | )} 153 |
154 | )} 155 |
156 | )} 157 |
158 | ); 159 | }; 160 | 161 | export default App; 162 | -------------------------------------------------------------------------------- /src/app/components/ListItem.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import classNames from "classnames"; 3 | 4 | function ListItem(props) { 5 | const node = props.node; 6 | 7 | const handleClick = id => { 8 | props.onClick(id); 9 | }; 10 | 11 | return ( 12 |
  • handleClick(node.id)} 19 | > 20 |
    21 | 22 | 23 | 24 | 25 | 26 | {node.name} 27 | 28 |
    29 |
  • 30 | ); 31 | } 32 | 33 | export default ListItem; 34 | -------------------------------------------------------------------------------- /src/app/index.html: -------------------------------------------------------------------------------- 1 |
    2 | -------------------------------------------------------------------------------- /src/app/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | import App from "./components/App"; 4 | 5 | ReactDOM.render(, document.getElementById("react-page")); 6 | -------------------------------------------------------------------------------- /src/app/styles/controls.css: -------------------------------------------------------------------------------- 1 | .button-margin-bottom { 2 | margin-bottom: 12px; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/styles/empty-state.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | padding: 0; 3 | } 4 | 5 | .active-state { 6 | display: flex; 7 | flex-flow: column; 8 | align-content: center; 9 | justify-content: center; 10 | padding: 16px; 11 | } 12 | 13 | .active-state-title { 14 | width: 100%; 15 | padding: 84px 0; 16 | text-align: center; 17 | flex: 1 1 100%; 18 | border-radius: 4px; 19 | border: 1px solid #e5e5e5; 20 | margin-bottom: 16px; 21 | } 22 | 23 | .layer-empty-title { 24 | padding: 130px 0; 25 | } 26 | 27 | .empty-state-wrapper { 28 | margin: 16px; 29 | } 30 | 31 | .empty-state { 32 | height: calc(100% - 32px); 33 | width: 100%; 34 | border: 1px solid #e5e5e5; 35 | text-align: center; 36 | margin-bottom: 16px; 37 | padding: 40px 0; 38 | border-radius: 6px; 39 | display: flex; 40 | align-content: center; 41 | justify-content: center; 42 | flex-direction: column; 43 | } 44 | 45 | .empty-state__image { 46 | height: 64px; 47 | width: 64px; 48 | background-color: #18a0fb; 49 | border-radius: 100px; 50 | margin-bottom: 24px; 51 | margin-left: auto; 52 | margin-right: auto; 53 | display: flex; 54 | justify-content: center; 55 | align-content: center; 56 | } 57 | 58 | .empty-state__title { 59 | font-size: 13px; 60 | line-height: 1.3; 61 | color: #333333; 62 | margin: auto; 63 | padding-bottom: 16px; 64 | } 65 | -------------------------------------------------------------------------------- /src/app/styles/nav.css: -------------------------------------------------------------------------------- 1 | .nav { 2 | display: flex; 3 | flex-flow: row; 4 | border-bottom: 1px solid #e5e5e5; 5 | padding: 4px 8px; 6 | } 7 | 8 | .nav .section-title { 9 | cursor: pointer; 10 | } 11 | 12 | .active { 13 | color: #333; 14 | } 15 | 16 | .disabled { 17 | color: #b3b3b3; 18 | cursor: pointer; 19 | } 20 | 21 | .disabled:hover { 22 | color: #333; 23 | } 24 | 25 | .list { 26 | flex: 1; 27 | margin: 0; 28 | padding: 0; 29 | overflow-y: auto; 30 | overflow-x: hidden; 31 | position: relative; 32 | } 33 | 34 | .list-arrow { 35 | height: 32px; 36 | min-width: 16px; 37 | display: flex; 38 | align-content: center; 39 | justify-content: center; 40 | flex-flow: column; 41 | } 42 | 43 | .list-arrow-icon { 44 | opacity: 0; 45 | transition: opacity 50ms ease; 46 | transform: rotate(-90deg); 47 | } 48 | 49 | .list-item { 50 | width: 100%; 51 | list-style: none; 52 | padding: 0; 53 | font-family: "Inter"; 54 | } 55 | 56 | .list-item ul { 57 | display: none; 58 | } 59 | 60 | .list-item--selected > .list-flex-row { 61 | background: #daebf7; 62 | } 63 | 64 | .list-item--selected ul { 65 | background: #edf5fa; 66 | } 67 | 68 | .list-item--active > ul { 69 | display: block; 70 | } 71 | 72 | .list-item--active > div > .list-arrow > .list-arrow-icon { 73 | transform: rotate(0deg); 74 | } 75 | 76 | .list-flex-row { 77 | height: 32px; 78 | display: flex; 79 | flex-flow: row; 80 | align-items: center; 81 | justify-content: flex-start; 82 | border: 1px solid transparent; 83 | transition: border-color 100ms ease; 84 | cursor: pointer; 85 | position: relative; 86 | } 87 | 88 | .list-flex-row:hover { 89 | border-color: #18a0fb; 90 | } 91 | 92 | .list-flex-row:hover .list-arrow-icon { 93 | opacity: 1; 94 | } 95 | 96 | .list-icon { 97 | text-align: center; 98 | width: 16px; 99 | opacity: 0.6; 100 | } 101 | 102 | .list-icon img { 103 | height: 12px; 104 | width: 12px; 105 | } 106 | 107 | .list-name { 108 | margin: 0 8px; 109 | font-size: 12px; 110 | white-space: nowrap; 111 | overflow: hidden; 112 | text-overflow: ellipsis; 113 | margin-right: 48px; 114 | } 115 | 116 | .layer-count { 117 | padding-left: 2px; 118 | /* color: #b3b3b3; */ 119 | } 120 | -------------------------------------------------------------------------------- /src/app/styles/ui.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | div, 4 | span, 5 | applet, 6 | object, 7 | iframe, 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5, 13 | h6, 14 | p, 15 | blockquote, 16 | pre, 17 | a, 18 | abbr, 19 | acronym, 20 | address, 21 | big, 22 | cite, 23 | code, 24 | del, 25 | dfn, 26 | em, 27 | img, 28 | ins, 29 | kbd, 30 | q, 31 | s, 32 | samp, 33 | small, 34 | strike, 35 | strong, 36 | sub, 37 | sup, 38 | tt, 39 | var, 40 | b, 41 | u, 42 | i, 43 | center, 44 | dl, 45 | dt, 46 | dd, 47 | ol, 48 | ul, 49 | li, 50 | fieldset, 51 | form, 52 | label, 53 | legend, 54 | table, 55 | caption, 56 | tbody, 57 | tfoot, 58 | thead, 59 | tr, 60 | th, 61 | td, 62 | article, 63 | aside, 64 | canvas, 65 | details, 66 | embed, 67 | figure, 68 | figcaption, 69 | footer, 70 | header, 71 | hgroup, 72 | menu, 73 | nav, 74 | output, 75 | ruby, 76 | section, 77 | summary, 78 | time, 79 | mark, 80 | audio, 81 | video { 82 | margin: 0; 83 | padding: 0; 84 | border: 0; 85 | font-size: 100%; 86 | font: inherit; 87 | vertical-align: baseline; 88 | } 89 | 90 | /* make sure to set some focus styles for accessibility */ 91 | :focus { 92 | outline: 0; 93 | } 94 | 95 | /* HTML5 display-role reset for older browsers */ 96 | article, 97 | aside, 98 | details, 99 | figcaption, 100 | figure, 101 | footer, 102 | header, 103 | hgroup, 104 | menu, 105 | nav, 106 | section { 107 | display: block; 108 | } 109 | 110 | body { 111 | line-height: 1; 112 | } 113 | 114 | ol, 115 | ul { 116 | list-style: none; 117 | } 118 | 119 | blockquote, 120 | q { 121 | quotes: none; 122 | } 123 | 124 | blockquote:before, 125 | blockquote:after, 126 | q:before, 127 | q:after { 128 | content: ""; 129 | content: none; 130 | } 131 | 132 | table { 133 | border-collapse: collapse; 134 | border-spacing: 0; 135 | } 136 | 137 | input[type="search"]::-webkit-search-cancel-button, 138 | input[type="search"]::-webkit-search-decoration, 139 | input[type="search"]::-webkit-search-results-button, 140 | input[type="search"]::-webkit-search-results-decoration { 141 | -webkit-appearance: none; 142 | -moz-appearance: none; 143 | } 144 | 145 | input[type="search"] { 146 | -webkit-appearance: none; 147 | -moz-appearance: none; 148 | -webkit-box-sizing: content-box; 149 | -moz-box-sizing: content-box; 150 | box-sizing: content-box; 151 | } 152 | 153 | textarea { 154 | overflow: auto; 155 | vertical-align: top; 156 | resize: vertical; 157 | } 158 | 159 | /** 160 | * Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3. 161 | */ 162 | 163 | audio, 164 | canvas, 165 | video { 166 | display: inline-block; 167 | *display: inline; 168 | *zoom: 1; 169 | max-width: 100%; 170 | } 171 | 172 | /** 173 | * Prevent modern browsers from displaying `audio` without controls. 174 | * Remove excess height in iOS 5 devices. 175 | */ 176 | 177 | audio:not([controls]) { 178 | display: none; 179 | height: 0; 180 | } 181 | 182 | /** 183 | * Address styling not present in IE 7/8/9, Firefox 3, and Safari 4. 184 | * Known issue: no IE 6 support. 185 | */ 186 | 187 | [hidden] { 188 | display: none; 189 | } 190 | 191 | /** 192 | * 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using 193 | * `em` units. 194 | * 2. Prevent iOS text size adjust after orientation change, without disabling 195 | * user zoom. 196 | */ 197 | 198 | html { 199 | font-size: 100%; /* 1 */ 200 | -webkit-text-size-adjust: 100%; /* 2 */ 201 | -ms-text-size-adjust: 100%; /* 2 */ 202 | } 203 | 204 | /** 205 | * Address `outline` inconsistency between Chrome and other browsers. 206 | */ 207 | 208 | a:focus { 209 | outline: thin dotted; 210 | } 211 | 212 | /** 213 | * Improve readability when focused and also mouse hovered in all browsers. 214 | */ 215 | 216 | a:active, 217 | a:hover { 218 | outline: 0; 219 | } 220 | 221 | /** 222 | * 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3. 223 | * 2. Improve image quality when scaled in IE 7. 224 | */ 225 | 226 | img { 227 | border: 0; /* 1 */ 228 | -ms-interpolation-mode: bicubic; /* 2 */ 229 | } 230 | 231 | /** 232 | * Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11. 233 | */ 234 | 235 | figure { 236 | margin: 0; 237 | } 238 | 239 | /** 240 | * Correct margin displayed oddly in IE 6/7. 241 | */ 242 | 243 | form { 244 | margin: 0; 245 | } 246 | 247 | /** 248 | * Define consistent border, margin, and padding. 249 | */ 250 | 251 | fieldset { 252 | border: 1px solid #c0c0c0; 253 | margin: 0 2px; 254 | padding: 0.35em 0.625em 0.75em; 255 | } 256 | 257 | /** 258 | * 1. Correct color not being inherited in IE 6/7/8/9. 259 | * 2. Correct text not wrapping in Firefox 3. 260 | * 3. Correct alignment displayed oddly in IE 6/7. 261 | */ 262 | 263 | legend { 264 | border: 0; /* 1 */ 265 | padding: 0; 266 | white-space: normal; /* 2 */ 267 | *margin-left: -7px; /* 3 */ 268 | } 269 | 270 | /** 271 | * 1. Correct font size not being inherited in all browsers. 272 | * 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5, 273 | * and Chrome. 274 | * 3. Improve appearance and consistency in all browsers. 275 | */ 276 | 277 | button, 278 | input, 279 | select, 280 | textarea { 281 | font-size: 100%; /* 1 */ 282 | margin: 0; /* 2 */ 283 | vertical-align: baseline; /* 3 */ 284 | *vertical-align: middle; /* 3 */ 285 | } 286 | 287 | /** 288 | * Address Firefox 3+ setting `line-height` on `input` using `!important` in 289 | * the UA stylesheet. 290 | */ 291 | 292 | button, 293 | input { 294 | line-height: normal; 295 | } 296 | 297 | /** 298 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 299 | * All other form control elements do not inherit `text-transform` values. 300 | * Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+. 301 | * Correct `select` style inheritance in Firefox 4+ and Opera. 302 | */ 303 | 304 | button, 305 | select { 306 | text-transform: none; 307 | } 308 | 309 | /** 310 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 311 | * and `video` controls. 312 | * 2. Correct inability to style clickable `input` types in iOS. 313 | * 3. Improve usability and consistency of cursor style between image-type 314 | * `input` and others. 315 | * 4. Remove inner spacing in IE 7 without affecting normal text inputs. 316 | * Known issue: inner spacing remains in IE 6. 317 | */ 318 | 319 | button, 320 | html input[type="button"], /* 1 */ 321 | input[type="reset"], 322 | input[type="submit"] { 323 | -webkit-appearance: button; /* 2 */ 324 | cursor: pointer; /* 3 */ 325 | *overflow: visible; /* 4 */ 326 | } 327 | 328 | /** 329 | * Re-set default cursor for disabled elements. 330 | */ 331 | 332 | button[disabled], 333 | html input[disabled] { 334 | cursor: default; 335 | } 336 | 337 | /** 338 | * 1. Address box sizing set to content-box in IE 8/9. 339 | * 2. Remove excess padding in IE 8/9. 340 | * 3. Remove excess padding in IE 7. 341 | * Known issue: excess padding remains in IE 6. 342 | */ 343 | 344 | input[type="checkbox"], 345 | input[type="radio"] { 346 | box-sizing: border-box; /* 1 */ 347 | padding: 0; /* 2 */ 348 | *height: 13px; /* 3 */ 349 | *width: 13px; /* 3 */ 350 | } 351 | 352 | /** 353 | * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 354 | * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome 355 | * (include `-moz` to future-proof). 356 | */ 357 | 358 | input[type="search"] { 359 | -webkit-appearance: textfield; /* 1 */ 360 | -moz-box-sizing: content-box; 361 | -webkit-box-sizing: content-box; /* 2 */ 362 | box-sizing: content-box; 363 | } 364 | 365 | /** 366 | * Remove inner padding and search cancel button in Safari 5 and Chrome 367 | * on OS X. 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Remove inner padding and border in Firefox 3+. 377 | */ 378 | 379 | button::-moz-focus-inner, 380 | input::-moz-focus-inner { 381 | border: 0; 382 | padding: 0; 383 | } 384 | 385 | /** 386 | * 1. Remove default vertical scrollbar in IE 6/7/8/9. 387 | * 2. Improve readability and alignment in all browsers. 388 | */ 389 | 390 | textarea { 391 | overflow: auto; /* 1 */ 392 | vertical-align: top; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove most spacing between table cells. 397 | */ 398 | 399 | table { 400 | border-collapse: collapse; 401 | border-spacing: 0; 402 | } 403 | 404 | html, 405 | button, 406 | input, 407 | select, 408 | textarea { 409 | color: #222; 410 | } 411 | 412 | ::-moz-selection { 413 | background: #b3d4fc; 414 | text-shadow: none; 415 | } 416 | 417 | ::selection { 418 | background: #b3d4fc; 419 | text-shadow: none; 420 | } 421 | 422 | img { 423 | vertical-align: middle; 424 | } 425 | 426 | fieldset { 427 | border: 0; 428 | margin: 0; 429 | padding: 0; 430 | } 431 | 432 | textarea { 433 | resize: vertical; 434 | } 435 | 436 | .chromeframe { 437 | margin: 0.2em 0; 438 | background: #ccc; 439 | color: #000; 440 | padding: 0.2em 0; 441 | } 442 | -------------------------------------------------------------------------------- /src/plugin/controller.ts: -------------------------------------------------------------------------------- 1 | // Plugin window dimensions 2 | figma.showUI(__html__, { width: 320, height: 358 }); 3 | 4 | // Imported themes 5 | import { darkTheme } from "./dark-to-light-theme"; 6 | import { lightTheme } from "./light-to-dark-theme"; 7 | 8 | // Utility function for serializing nodes to pass back to the UI. 9 | function serializeNodes(nodes) { 10 | let serializedNodes = JSON.stringify(nodes, [ 11 | "name", 12 | "type", 13 | "children", 14 | "id" 15 | ]); 16 | 17 | return serializedNodes; 18 | } 19 | 20 | // Utility function for flattening the 21 | // selection of nodes in Figma into an array. 22 | const flatten = obj => { 23 | const array = Array.isArray(obj) ? obj : [obj]; 24 | return array.reduce((acc, value) => { 25 | acc.push(value); 26 | if (value.children) { 27 | acc = acc.concat(flatten(value.children)); 28 | delete value.children; 29 | } 30 | return acc; 31 | }, []); 32 | }; 33 | 34 | figma.ui.onmessage = msg => { 35 | let skippedLayers = []; 36 | 37 | if (msg.type === "run-app") { 38 | // If nothing's selected, we tell the UI to keep the empty state. 39 | if (figma.currentPage.selection.length === 0) { 40 | figma.ui.postMessage({ 41 | type: "selection-updated", 42 | message: 0 43 | }); 44 | } else { 45 | let selectedNodes = flatten(figma.currentPage.selection); 46 | 47 | // Update the UI with the number of selected nodes. 48 | // This will display our theming controls. 49 | figma.ui.postMessage({ 50 | type: "selection-updated", 51 | message: serializeNodes(selectedNodes) 52 | }); 53 | } 54 | } 55 | 56 | // When a theme is selected 57 | if (msg.type === "theme-update") { 58 | const nodesToTheme = figma.currentPage.selection; 59 | 60 | if (msg.message === "dark-to-light-theme") { 61 | // Update the layers with this theme, by passing in the 62 | // selected nodes and the theme object. 63 | nodesToTheme.map(selected => updateTheme(selected, darkTheme)); 64 | } 65 | 66 | if (msg.message === "light-to-dark-theme") { 67 | // Update the layers with this theme, by passing in the 68 | // selected nodes and the theme object. 69 | nodesToTheme.map(selected => updateTheme(selected, lightTheme)); 70 | } 71 | 72 | // Need to wait for some promises to resolve before 73 | // sending the skipped layers back to the UI. 74 | setTimeout(function() { 75 | figma.ui.postMessage({ 76 | type: "layers-skipped", 77 | message: serializeNodes(skippedLayers) 78 | }); 79 | }, 500); 80 | 81 | figma.notify(`Theming complete`, { timeout: 750 }); 82 | } 83 | 84 | // When a layer is selected from the skipped layers. 85 | if (msg.type === "select-layer") { 86 | let layer = figma.getNodeById(msg.id); 87 | let layerArray = []; 88 | 89 | // Using selection and viewport requires an array. 90 | layerArray.push(layer); 91 | 92 | // Moves the layer into focus and selects so the user can update it. 93 | figma.notify(`Layer ${layer.name} selected`, { timeout: 750 }); 94 | figma.currentPage.selection = layerArray; 95 | figma.viewport.scrollAndZoomIntoView(layerArray); 96 | } 97 | 98 | // Swap styles with the corresponding/mapped styles 99 | async function replaceStyles( 100 | node, 101 | style, 102 | mappings, 103 | applyStyle: (node, styleId) => void 104 | ) { 105 | // Find the style the ID corresponds to in the team library 106 | let importedStyle = await figma.importStyleByKeyAsync(style.key); 107 | 108 | // Once the promise is resolved, then see if the 109 | // key matches anything in the mappings object. 110 | if (mappings[importedStyle.key] !== undefined) { 111 | let mappingStyle = mappings[importedStyle.key]; 112 | 113 | // Use the mapping value to fetch the official style. 114 | let newStyle = await figma.importStyleByKeyAsync(mappingStyle.mapsToKey); 115 | 116 | // Update the node with the new color. 117 | applyStyle(node, newStyle.id); 118 | } else { 119 | skippedLayers.push(node); 120 | } 121 | } 122 | 123 | // Fix layers with no style attached to them, just hex colors 124 | async function fixStyles( 125 | node, 126 | nodeType, 127 | style, 128 | mappings, 129 | applyStyle: (node, styleId) => void 130 | ) { 131 | let styleName = nodeType.toLowerCase() + " " + style; 132 | console.log(styleName); 133 | // See if the key matches anything in the mappings object. 134 | if (mappings[styleName] !== undefined) { 135 | let mappingStyle = mappings[styleName]; 136 | 137 | // Use the mapping value to fetch the official style. 138 | let newStyle = await figma.importStyleByKeyAsync(mappingStyle.mapsToKey); 139 | 140 | // Update the node with the new color. 141 | applyStyle(node, newStyle.id); 142 | } else { 143 | skippedLayers.push(node); 144 | } 145 | } 146 | 147 | async function replaceComponent( 148 | node, 149 | key, 150 | mappings, 151 | applyComponent: (node, masterComponent) => void 152 | ) { 153 | let componentToSwitchWith = mappings[key]; 154 | let importedComponent = await figma.importComponentByKeyAsync( 155 | componentToSwitchWith.mapsToKey 156 | ); 157 | // Switch the existing component to a new component. 158 | applyComponent(node, importedComponent); 159 | } 160 | 161 | async function swapComponent(node, key, mappings) { 162 | await replaceComponent( 163 | node, 164 | key, 165 | mappings, 166 | (node, masterComponent) => (node.masterComponent = masterComponent) 167 | ); 168 | } 169 | 170 | async function replaceFills(node, style, mappings) { 171 | await replaceStyles( 172 | node, 173 | style, 174 | mappings, 175 | (node, styleId) => (node.fillStyleId = styleId) 176 | ); 177 | } 178 | 179 | async function replaceNoStyleFill(node, nodeType, style, mappings) { 180 | await fixStyles( 181 | node, 182 | nodeType, 183 | style, 184 | mappings, 185 | (node, styleId) => (node.fillStyleId = styleId) 186 | ); 187 | } 188 | 189 | async function replaceStrokes(node, style, mappings) { 190 | await replaceStyles( 191 | node, 192 | style, 193 | mappings, 194 | (node, styleId) => (node.strokeStyleId = styleId) 195 | ); 196 | } 197 | 198 | async function replaceEffects(node, style, mappings) { 199 | await replaceStyles( 200 | node, 201 | style, 202 | mappings, 203 | (node, styleId) => (node.effectStyleId = styleId) 204 | ); 205 | } 206 | 207 | // Updates the node with the new theme depending on 208 | // the type of the node. 209 | function updateTheme(node, theme) { 210 | switch (node.type) { 211 | case "COMPONENT": 212 | case "COMPONENT_SET": 213 | case "RECTANGLE": 214 | case "GROUP": 215 | case "ELLIPSE": 216 | case "POLYGON": 217 | case "STAR": 218 | case "LINE": 219 | case "BOOLEAN_OPERATION": 220 | case "FRAME": 221 | case "LINE": 222 | case "VECTOR": { 223 | if (node.children) { 224 | node.children.forEach(child => { 225 | updateTheme(child, theme); 226 | }); 227 | } 228 | if (node.fills) { 229 | if (node.fillStyleId && typeof node.fillStyleId !== "symbol") { 230 | let style = figma.getStyleById(node.fillStyleId); 231 | // Pass in the layer we want to change, the style ID the node is using. 232 | // and the set of mappings we want to check against. 233 | replaceFills(node, style, theme); 234 | } else if (node.fillStyleId === "") { 235 | // No style on the layer? Let's fix it for them. 236 | // First we need the fill type determined above ex:is it #ffffff?), then 237 | // we pass that hex into a new function. 238 | let style = determineFill(node.fills); 239 | let nodeType = node.type; 240 | replaceNoStyleFill(node, nodeType, style, theme); 241 | } else { 242 | skippedLayers.push(node); 243 | } 244 | } 245 | 246 | if (node.strokeStyleId) { 247 | replaceStrokes(node, figma.getStyleById(node.strokeStyleId), theme); 248 | } 249 | 250 | if (node.effectStyleId) { 251 | replaceEffects(node, figma.getStyleById(node.effectStyleId), theme); 252 | } 253 | 254 | break; 255 | } 256 | case "INSTANCE": { 257 | let componentKey = node.masterComponent.key; 258 | // If this instance is in mapping, then call it and skip it's children 259 | // otherwise check for the normal differences. 260 | if (theme[componentKey] !== undefined) { 261 | swapComponent(node, componentKey, theme); 262 | } else { 263 | if (node.fills) { 264 | if (node.fillStyleId && typeof node.fillStyleId !== "symbol") { 265 | let style = figma.getStyleById(node.fillStyleId); 266 | // Pass in the layer we want to change, the style ID the node is using. 267 | // and the set of mappings we want to check against. 268 | replaceFills(node, style, theme); 269 | } else if (node.fillStyleId === "") { 270 | // No style on the layer? Let's fix it for them. 271 | // First we need the fill type determined above ex:is it #ffffff?), then 272 | // we pass that hex into a new function. 273 | let style = determineFill(node.fills); 274 | let nodeType = node.type; 275 | replaceNoStyleFill(node, nodeType, style, theme); 276 | } else { 277 | skippedLayers.push(node); 278 | } 279 | } 280 | 281 | if (node.strokeStyleId) { 282 | replaceStrokes(node, figma.getStyleById(node.strokeStyleId), theme); 283 | } 284 | 285 | if (node.effectStyleId) { 286 | replaceEffects(node, figma.getStyleById(node.effectStyleId), theme); 287 | } 288 | 289 | if (node.children) { 290 | node.children.forEach(child => { 291 | updateTheme(child, theme); 292 | }); 293 | } 294 | } 295 | break; 296 | } 297 | case "TEXT": { 298 | if (node.fillStyleId && typeof node.fillStyleId !== "symbol") { 299 | replaceFills(node, figma.getStyleById(node.fillStyleId), theme); 300 | } else if (node.fillStyleId === "") { 301 | let style = determineFill(node.fills); 302 | let nodeType = node.type; 303 | replaceNoStyleFill(node, nodeType, style, theme); 304 | } else { 305 | skippedLayers.push(node); 306 | } 307 | } 308 | default: { 309 | // do nothing 310 | } 311 | } 312 | } 313 | 314 | // Determine a nodes fills 315 | function determineFill(fills) { 316 | let fillValues = []; 317 | let rgbObj; 318 | 319 | fills.forEach(fill => { 320 | if (fill.type === "SOLID" && fill.visible === true) { 321 | rgbObj = convertColor(fill.color); 322 | fillValues.push(RGBToHex(rgbObj["r"], rgbObj["g"], rgbObj["b"])); 323 | } 324 | }); 325 | 326 | return fillValues[0]; 327 | } 328 | 329 | // Utility functions for color conversion. 330 | function convertColor(color) { 331 | const colorObj = color; 332 | const figmaColor = {}; 333 | 334 | Object.entries(colorObj).forEach(cf => { 335 | const [key, value] = cf; 336 | 337 | if (["r", "g", "b"].includes(key)) { 338 | figmaColor[key] = (255 * (value as number)).toFixed(0); 339 | } 340 | if (key === "a") { 341 | figmaColor[key] = value; 342 | } 343 | }); 344 | return figmaColor; 345 | } 346 | 347 | function RGBToHex(r, g, b) { 348 | r = Number(r).toString(16); 349 | g = Number(g).toString(16); 350 | b = Number(b).toString(16); 351 | 352 | if (r.length == 1) r = "0" + r; 353 | if (g.length == 1) g = "0" + g; 354 | if (b.length == 1) b = "0" + b; 355 | 356 | return "#" + r + g + b; 357 | } 358 | }; 359 | -------------------------------------------------------------------------------- /src/plugin/dark-to-light-theme.ts: -------------------------------------------------------------------------------- 1 | // For mapping from dark to light theme. 2 | const darkTheme = { 3 | // Components (swaps instances rather ie Name / Dark to Name / Light) 4 | f0d4aa5e63fff4392e3b3c22884523369f5d0424: { 5 | componentName: "iPhone X Status Bar / Dark", 6 | mapsToKey: "33425bd93c1b8cea071df9b5297f0b19583a643b" 7 | }, 8 | "5b8dce7a790466da546d319a69f5de220e1a66f1": { 9 | componentName: "iPhone X Home Indicator / Dark", 10 | mapsToKey: "0489bde7fd0346a97eff3170167714838a8ffb9c" 11 | }, 12 | "3ee4cf479eefd5e181ff4abd1c982011438e692d": { 13 | componentName: "System (Dark) / Numeric Keyboard", 14 | mapsToKey: "867fa47defeb07293aa37e5467e4ca487019dd78" 15 | }, 16 | e4dcbeb8549332e4c969ef4d0d302e75a7932c25: { 17 | componentName: "System (Dark) / Keyboard", 18 | mapsToKey: "bca69f03db8e938cd78ca91c84e35553804ca8c1" 19 | }, 20 | e9622ab25248f31fb02b6faa00308b8faa4acb3e: { 21 | componentName: "Header / Guild / Dark", 22 | mapsToKey: "00230f03c08e00a787e9c2659c3165bcad7ae06b" 23 | }, 24 | "4592fb98edf78fdeea07d23445de948286e7c5f2": { 25 | componentName: "Header / DM", 26 | mapsToKey: "ff8b5c71a60fc212647c40e481c0bf1886a3ec85" 27 | }, 28 | "46d6bed4edd9482b1452afab5ab0292b516c9e09": { 29 | componentName: "Navigation Tab / Dark", 30 | mapsToKey: "64d16554561a7496b2d23854074ce923655918e0" 31 | }, 32 | "9e0a9f99024fb9baedcacbb123c84d7cc4b8f87a": { 33 | componentName: "Guild Selected / Dark", 34 | mapsToKey: "a9231a3d9fac5b7d26d56904c7b28d5d00bfff97" 35 | }, 36 | c25d89953041d095215c972fa55dc6f7776d9a54: { 37 | componentName: "Messages Selected / Dark", 38 | mapsToKey: "8c80d5bd7bdd80acec9793a719f6caaebba1c6ee" 39 | }, 40 | // Android System Components 41 | "3588fe4d5a302b2fca2be2b0cb5c12e2a2f41c05": { 42 | componentName: "Status Bar / Dark", 43 | mapsToKey: "790d7d3d884a6d3dadc79bf3c48a4918f4c16ba7" 44 | }, 45 | "43c14ca23834d2aa3bf1e027a0635c7393e87378": { 46 | componentName: "Navigation Tab / Dark", 47 | mapsToKey: "bd56c3e89162d8274c4ca591f2ca1e1064658570" 48 | }, 49 | // Desktop Components 50 | d31d651767116b73f9209c5362669782ff3a8a25: { 51 | componentName: "Windows Bar / Dark", 52 | mapsToKey: "05fc2a6b4207baa8f672dc9e8a5d750c5d60711b" 53 | }, 54 | // Headings 55 | "5c1691cbeaaf4270107d34f1a12f02fdd04afa02": { 56 | name: "Dark / Header / Primary (White)", 57 | mapsToName: "Light / Header / Primary (900)", 58 | mapsToKey: "b19a14675b8adeb1528ab5f84e57b2eeed10d46c" 59 | }, 60 | "text #ffffff": { 61 | name: "Unstyled Dark Header", 62 | mapsToName: "Light / Header / Primary (900)", 63 | mapsToKey: "b19a14675b8adeb1528ab5f84e57b2eeed10d46c" 64 | }, 65 | "text #b9bbbe": { 66 | name: "Unstyled Dark Secondary Header", 67 | mapsToName: "themes/light/header/header-secondary", 68 | mapsToKey: "608f2ea1aa64ff7f202e8c22cc4147a02be9d85b" 69 | }, 70 | "text #a3a6aa": { 71 | name: "Unstyled Dark Muted", 72 | mapsToName: "themes/light/text/text-muted", 73 | mapsToKey: "7d8703ec132ddaf6968f6d190d1e80031c559d7c" 74 | }, 75 | bc090cb3b1c7313ae276acbd791b5b87b478ec59: { 76 | name: "Dark / Header / Secondary (300)", 77 | mapsToName: "Light / Header / Secondary (600)", 78 | mapsToKey: "608f2ea1aa64ff7f202e8c22cc4147a02be9d85b" 79 | }, 80 | // Text 81 | "5c77a96137b698b5575557c069cabd6877d66e1e": { 82 | name: "Dark / Text / Normal (200)", 83 | mapsToName: "Light / Text / Normal (700)", 84 | mapsToKey: "546c7d46e754ac2b23b338783d72f206b77b6436" 85 | }, 86 | "5d84ad92f3ad152f196e2093a3c0542a08dfba11": { 87 | name: "Dark / Text / Muted (400)", 88 | mapsToName: "Light / Text / Muted (500)", 89 | mapsToKey: "7d8703ec132ddaf6968f6d190d1e80031c559d7c" 90 | }, 91 | bf03232753079bdd5bec6c55343b659876b5283f: { 92 | name: "Dark / Text / Link", 93 | mapsToName: "Light / Text / Link", 94 | mapsToKey: "64d3058dd508a4985670b2d19418a06a3503c9c2" 95 | }, 96 | "6e4aef7677e2ea82c87465276522da7ef5a07121": { 97 | name: "themes/dark/text/text-brand", 98 | mapsToName: "themes/light/text/text-brand", 99 | mapsToKey: "15320fd498dcd4e113c5bd587dca2d11d4492e84" 100 | }, 101 | "094cbaac0817be7bbfd8292cb98fc1e515e7ea0e": { 102 | name: "themes/dark/text/text-danger", 103 | mapsToName: "themes/light/text/text-danger", 104 | mapsToKey: "c8d237080d38671193403b49cdc6a5778a14bf45" 105 | }, 106 | df0622bb33232fe041c468e8d3dd37e5428b10e7: { 107 | name: "themes/dark/text/text-warning", 108 | mapsToName: "themes/light/text/text-warning", 109 | mapsToKey: "0d95a7d4d30ef99ebd04abd5b2dd4708913f765b" 110 | }, 111 | "7733117cf1ef570b77332c86ba783af6cb735fc1": { 112 | name: "themes/dark/text/text-positive", 113 | mapsToName: "themes/light/text/text-positive", 114 | mapsToKey: "71f64b08bdec4daf747a850b128e0994c4593c04" 115 | }, 116 | // Interactive Text & Icons 117 | "287463bade90c1eed5ea4cb0b5d63794daa8aec2": { 118 | name: "Dark / Interactive Text & Icons / Normal (300)", 119 | mapsToName: "Light / Interactive Text & Icons / Normal (600)", 120 | mapsToKey: "9c23a031773711e026394f4354661c37ee5b4682" 121 | }, 122 | "boolean_operation #b9bbbe": { 123 | name: "Dark / Interactive Text & Icons / Normal (300)", 124 | mapsToName: "Light / Interactive Text & Icons / Normal (600)", 125 | mapsToKey: "9c23a031773711e026394f4354661c37ee5b4682" 126 | }, 127 | "boolean_operation #757575": { 128 | name: "Dark / Interactive Text & Icons / Normal (300)", 129 | mapsToName: "Light / Interactive Text & Icons / Normal (600)", 130 | mapsToKey: "9c23a031773711e026394f4354661c37ee5b4682" 131 | }, 132 | "vector #757575": { 133 | name: "Dark / Interactive Text & Icons / Normal (300)", 134 | mapsToName: "Light / Interactive Text & Icons / Normal (600)", 135 | mapsToKey: "9c23a031773711e026394f4354661c37ee5b4682" 136 | }, 137 | "vector #b9bbbe": { 138 | name: "Dark / Interactive Text & Icons / Normal (300)", 139 | mapsToName: "Light / Interactive Text & Icons / Normal (600)", 140 | mapsToKey: "9c23a031773711e026394f4354661c37ee5b4682" 141 | }, 142 | "502dcdf04992818dcbaed125ad711b446dee4c68": { 143 | name: "Dark / Interactive Text & Icons / Hover (200)", 144 | mapsToName: "themes/light/interactive/interactive-hover", 145 | mapsToKey: "e9542e95adf3bbe74286c2cf279fee64f7ba3279" 146 | }, 147 | "3eddc15e90bbd7064aea7cc13dc13e23a712f0b0": { 148 | name: "Dark / Interactive Text & Icons / Active (White)", 149 | mapsToName: "Light / Interactive Text & Icons / Active (900)", 150 | mapsToKey: "620c98e8f9255a6107dee91745669e5b702b413c" 151 | }, 152 | "boolean_operation #ffffff": { 153 | name: "Dark / Interactive Text & Icons / Active (White)", 154 | mapsToName: "Light / Interactive Text & Icons / Active (900)", 155 | mapsToKey: "620c98e8f9255a6107dee91745669e5b702b413c" 156 | }, 157 | fa698aa2a724522a7c29efb0a662aec75a1be5a1: { 158 | name: "Dark / Interactive Text & Icons / Muted (500)", 159 | mapsToName: "Light / Interactive Text & Icons / Muted (300)", 160 | mapsToKey: "9328cd78a39149b070d68f98d9fe4df7a92bf67d" 161 | }, 162 | // Backgrounds 163 | "4b93d40f61be15e255e87948a715521c3ae957e6": { 164 | name: "Dark / Background / Primary (600)", 165 | mapsToName: "Light / Background / Primary (White)", 166 | mapsToKey: "2449a2983d43793d80baa20c6c60e8a48e7f3a0c" 167 | }, 168 | "frame #36393f": { 169 | name: "Dark Primary Background", 170 | mapsToName: "Light / Background / Primary (White)", 171 | mapsToKey: "2449a2983d43793d80baa20c6c60e8a48e7f3a0c" 172 | }, 173 | "rectangle #36393f": { 174 | name: "Dark Primary Background", 175 | mapsToName: "Dark / Background / Primary (600)", 176 | mapsToKey: "2449a2983d43793d80baa20c6c60e8a48e7f3a0c" 177 | }, 178 | "frame #5865f2": { 179 | name: "Unstyled Brand", 180 | mapsToName: "other/blurple (brand-500)", 181 | mapsToKey: "25b165222f45fd70dc3c8e68d1a25f8d379a597d" 182 | }, 183 | "rectangle #5865f2": { 184 | name: "Unstyled Brand", 185 | mapsToName: "other/blurple (brand-500)", 186 | mapsToKey: "25b165222f45fd70dc3c8e68d1a25f8d379a597d" 187 | }, 188 | fb1358e5bd6dec072801298238cf49ff77b79a4b: { 189 | name: "Dark / Background / Secondary (630)", 190 | mapsToName: "Light / Background / Primary (White)", 191 | mapsToKey: "83704278c845a6a7ceb1f837387972ccb6d41960" 192 | }, 193 | abf9ad88ae1ade1a4b945b012f0965c9cdc068c9: { 194 | name: "Dark / Background / Secondary Alternate", 195 | mapsToName: "Light / Background / Secondary Alternate", 196 | mapsToKey: "6acd84c794796d112d4e9d22c4c8a5cae940a61d" 197 | }, 198 | ef179b6abe6cb8779857e05a6333d33f7a2b9320: { 199 | name: "Dark / Background / Tertiary (700)", 200 | mapsToName: "Light / Background / Tertiary (200)", 201 | mapsToKey: "dbd02a76b7b77c1976114c04068f0fbc22015fab" 202 | }, 203 | "3dd0e30ce0a8287eb91ec1fbeff92031e634ed01": { 204 | name: "Dark / Background / Accent (500)", 205 | mapsToName: "Light / Background / Accent (500)", 206 | mapsToKey: "7a199ce029a847f3a361dfb6a6e0ee4e4ba84d4f" 207 | }, 208 | "11516f4b43f381afb5a6bdf2c34b9437f0eecde1": { 209 | name: "Dark / Background / Floating (800)", 210 | mapsToName: "Light / Background / Floating (White)", 211 | mapsToKey: "6c8b08a42f9614842e880bf7bb795014d8fbae94" 212 | }, 213 | bfcdf063eb2c1edb446ba5d7880da6a324cc9b4f: { 214 | name: "Dark / Override / Read Channels", 215 | mapsToName: "Light / Override / Read Channels 360", 216 | mapsToKey: "634ef95b53ab529a774f27ed16be07c0b3fb3a5f" 217 | }, 218 | b659c283950f8b335922f52e40cefd3cf679d297: { 219 | name: "themes/dark/background/status-danger-background", 220 | mapsToName: "themes/light/background/status-danger-background", 221 | mapsToKey: "c592ea0b26929cf1374f973b857027dbd21ffb12" 222 | }, 223 | "3dbd679897876b69bc9cc8fa38be83c525ac5ed5": { 224 | name: "themes/dark/background/status-warning-background", 225 | mapsToName: "themes/light/background/status-warning-background", 226 | mapsToKey: "45f2139348b50263fda4704d4a9accea74540dcc" 227 | }, 228 | "746e170ac6e7ba80d171f01313735a3ec5535ef8": { 229 | name: "themes/dark/background/status-positive-background", 230 | mapsToName: "themes/light/background/status-positive-background", 231 | mapsToKey: "2a135fa63c0cea473936ced51ccd767b2f156739" 232 | }, 233 | da21c08d5f887ae8d6195d7f8a7585219d670b93: { 234 | name: "themes/dark/background/background-mentioned", 235 | mapsToName: "themes/light/background/background-mentioned", 236 | mapsToKey: "30d44092c13231213143b50015907463dd1b6211" 237 | }, 238 | "39c91bf62536cb1c6f51087853c35afcc6462bac": { 239 | name: "themes/dark/background/background-mentioned-hover", 240 | mapsToName: "themes/light/background/background-mentioned-hover", 241 | mapsToKey: "4d15ee684eb9fd6cb114d7fb585c83c9b0a598fd" 242 | }, 243 | "1054e0c4bc3e52ae2c7c48aa0d0f95ed5d998587": { 244 | name: "themes/dark/background/background-message-hover", 245 | mapsToName: "themes/light/background/background-message-hover", 246 | mapsToKey: "440a2d66490b7162417c740e66355f39d7b9e41a" 247 | }, 248 | "72a70771ff2a268130e7352250f374722f4d8bfe": { 249 | name: "themes/dark/background/background-mobile-primary", 250 | mapsToName: "themes/light/background/background-mobile-primary", 251 | mapsToKey: "5747d5e2f1e6047746c77e9368e8d21324eb93d9" 252 | }, 253 | "251f85bc338c5411608c2dc141a538305ab6b4c1": { 254 | name: "themes/dark/background/background-mobile-secondary", 255 | mapsToName: "themes/light/background/background-mobile-secondary", 256 | mapsToKey: "de9f518c35096095c02c215543174a04900b07d7" 257 | }, 258 | de9f518c35096095c02c215543174a04900b07d7: { 259 | name: "themes/light/background/background-mobile-secondary", 260 | mapsToName: "themes/dark/background/background-mobile-secondary", 261 | mapsToKey: "251f85bc338c5411608c2dc141a538305ab6b4c1" 262 | }, 263 | "1e1caa8f31ed3bb7ce6e6ce20dfe3187b20766c8": { 264 | name: "themes/dark/background/background-nested-floating", 265 | mapsToName: "themes/light/background/background-nested-floating", 266 | mapsToKey: "1fae53b19be2fe85aa44529cd3243c7b280173f1" 267 | }, 268 | // Background Modifiers 269 | d6c9270834b11c99ee651f0f5072ad2c63701165: { 270 | name: "Dark / Background Mod / Hover", 271 | mapsToName: "Light / Background Mod / Hover", 272 | mapsToKey: "35307396ae29aaeb583ae65891c69ec689f0c41e" 273 | }, 274 | bcf890d7a215c65deef97fb3d3f5bcebc9869bab: { 275 | name: "Dark / Background Mod / Active", 276 | mapsToName: "Light / Background Mod / Active", 277 | mapsToKey: "ddadf76919d9bacb925242a024dc1e2f5f517a46" 278 | }, 279 | ce012db42f35fb58b4fe1d6d8b46c4905a8fad0a: { 280 | name: "Dark / Background Mod / Selected", 281 | mapsToName: "Light / Background Mod / Selected", 282 | mapsToKey: "5af2eaf14901472c26b641997796bdba76ee1794" 283 | }, 284 | a6a3dc153f0e589408186176ebf8f20ed2f9bda3: { 285 | name: "Dark / Background Mod / Accent", 286 | mapsToName: "Light / Background Mod / Accent", 287 | mapsToKey: "08c7091f8d6950dc3f616afe8ed45b086f9124c7" 288 | }, 289 | // Status 290 | "61c493d9d14f2a5ae52c2037149773f0cd7690a5": { 291 | name: "themes/dark/status/status-positive", 292 | mapsToName: "themes/light/status/status-positive", 293 | mapsToKey: "6faa6d09b47caeb32fa0f5f81c561dcb7d68e9b1" 294 | }, 295 | "0ff4d563aae53dd8012f78a67f9fd182693a0f21": { 296 | name: "themes/dark/status/status-danger", 297 | mapsToName: "themes/light/status/status-danger", 298 | mapsToKey: "0c9cfa27f153e6a5a9954242bb6ae3cac02d4468" 299 | }, 300 | f719fb8e7bf04342010ecb37165e55aa8a638d35: { 301 | name: "themes/dark/status/status-warning", 302 | mapsToName: "themes/light/status/status-warning", 303 | mapsToKey: "9fa2f99cffe7ba587f259e98fb4de12c0b893223" 304 | }, 305 | // Other 306 | "6c54be693a4bbdff6fa4c02f672bc5c9e4654f8b": { 307 | name: "themes/dark/other/channeltextarea-background", 308 | mapsToName: "themes/light/other/channeltextarea-background", 309 | mapsToKey: "3c098a8d09acbd25ef37e7fc0b657c2dc78f243e" 310 | }, 311 | a4d76cf75156ab760df1685a30dadab20724010e: { 312 | name: "themes/dark/other/focus-primary0", 313 | mapsToName: "themes/light/other/focus-primary", 314 | mapsToKey: "d1dbae483f4eefcf5adccfbba8e6d50dbef1ec27" 315 | }, 316 | "7337ac931b2c9b699d44e6e783637e5afac50298": { 317 | name: "themes/dark/other/control-brand-foreground", 318 | mapsToName: "themes/light/other/control-brand-foreground", 319 | mapsToKey: "bbdc5cb26595f77283b8dfe51e659c5bfdc6a2d0" 320 | }, 321 | a926774d558d0e70f505df697c21c12dc4270206: { 322 | name: "themes/dark/other/scrollbar-thin-thumb", 323 | mapsToName: "themes/light/other/scrollbar-thin-thumb", 324 | mapsToKey: "084969be9bfee752064df1c504b6ba07a8d727ad" 325 | }, 326 | "2ab24b1a3901fae7960deb8a36e49f0d6b1732af": { 327 | name: "themes/dark/other/scrollbar-auto-thumb", 328 | mapsToName: "themes/light/other/scrollbar-auto-thumb", 329 | mapsToKey: "6436d02f21d749b84cbd8736bd453dad1c4ac3ab" 330 | }, 331 | d509bf14b1c3aac55dc0fd6b822f628956ad80c3: { 332 | name: "themes/dark/other/scrollbar-auto-track", 333 | mapsToName: "themes/light/other/scrollbar-auto-track", 334 | mapsToKey: "54fb146609c07fba199d4066f8c2ce14829a0d0a" 335 | }, 336 | // Effects 337 | b7edafef4513a59a40c8ba7adb382a0b6d3313ff: { 338 | name: "Border Elevation / Dark", 339 | mapsToName: "Border Elevation / Light", 340 | mapsToKey: "bf64ca51f902a903935680f692618a5eba4ea894" 341 | }, 342 | "67aabb2beb8092e4c0094e0175657bb0758e6ba8": { 343 | name: "High Elevation / Dark", 344 | mapsToName: "High Elevation / Light", 345 | mapsToKey: "30f011bbe03506a59052d7f8435cc1ec3b743b19" 346 | }, 347 | // Deprecated 348 | d104f004f79d0e422c44d14efdd5e527d57a185f: { 349 | name: "BETA_DEPRECATED/header/header-primary", 350 | mapsToName: "themes/light/header/header-primary", 351 | mapsToKey: "b19a14675b8adeb1528ab5f84e57b2eeed10d46c" 352 | }, 353 | "1aee47626b0083fe2830fb8262d9ba2d1790949f": { 354 | name: "BETA_DEPRECATED/header/header-secondary", 355 | mapsToName: "themes/light/header/header-secondary", 356 | mapsToKey: "608f2ea1aa64ff7f202e8c22cc4147a02be9d85b" 357 | }, 358 | bd768f7dda36913ff061b1f82a273264e710e9e0: { 359 | name: "BETA_DEPRECATED/background/background-primary", 360 | mapsToName: "themes/light/background/background-primary", 361 | mapsToKey: "2449a2983d43793d80baa20c6c60e8a48e7f3a0c" 362 | }, 363 | e8c94a8857a45794172b8e7e1f4392b388403cfd: { 364 | name: "BETA_DEPRECATED/background/background-secondary", 365 | mapsToName: "themes/light/background/background-secondary", 366 | mapsToKey: "83704278c845a6a7ceb1f837387972ccb6d41960" 367 | }, 368 | "8ed7c2cbc95b1ef5dbd750e29446fb30f5e2c7d6": { 369 | name: "themes/light/text/text-normal", 370 | mapsToName: "themes/light/background/background-primary", 371 | mapsToKey: "546c7d46e754ac2b23b338783d72f206b77b6436" 372 | }, 373 | "7a18a8af03b002b7433560a024d0416017a927bd": { 374 | name: "BETA_DEPRECATED/text/text-muted", 375 | mapsToName: "themes/light/text/text-muted", 376 | mapsToKey: "7d8703ec132ddaf6968f6d190d1e80031c559d7c" 377 | } 378 | }; 379 | 380 | export { darkTheme }; 381 | -------------------------------------------------------------------------------- /src/plugin/example-theme.ts: -------------------------------------------------------------------------------- 1 | // Themes are objects. 2 | // Using the "Inspector" plugin we've made pubic makes this process easier! 3 | 4 | const sampleTheme = { 5 | // Within the object, we check for a *style key*, 6 | // Figma uses this key to refer to a specific style in your library 7 | "5c1691cbeaaf4270107d34f1a12f02fdd04afa02": { 8 | // Name isn't used, but is nice for reference. 9 | name: "Dark / Header / Primary (White)", 10 | // Within the object we check for the mapsToKey key value. 11 | // This is the style we'll swap the original style with. 12 | mapsToKey: "b19a14675b8adeb1528ab5f84e57b2eeed10d46c", 13 | mapsToName: "Light / Header / Primary (900)" 14 | }, 15 | style_key_goes_here: { 16 | name: "", 17 | mapsToKey: "style_key_to_switch_with_goes_here", 18 | mapsToName: "" 19 | }, 20 | // If you have two instances of a component in your library 21 | // ex: (Header/Dark and Header/Light) you can swap those instances 22 | // rather than simply retheming them. By adding a component key to your theme. 23 | f0d5aa5e63fff4392e3b3c22884523369f5d0424: { 24 | componentName: "iPhone X Status Bar / Dark", 25 | mapsToComponentName: "iPhone X Status Bar / Light", 26 | // This is key of the component I want to switch with. 27 | mapsToKey: "33425bd93c1b8cea071df9b5297f0b19583a643b" 28 | }, 29 | component_key_goes_here: { 30 | name: "", 31 | mapsToKey: "component_key_to_switch_with_goes_here", 32 | mapsToName: "" 33 | } 34 | }; 35 | 36 | // Don't know how to find a styles key? Use our inspector plugin 37 | // https://www.figma.com/community/plugin/760351147138040099 38 | 39 | export { sampleTheme }; 40 | -------------------------------------------------------------------------------- /src/plugin/light-to-dark-theme.ts: -------------------------------------------------------------------------------- 1 | // For mapping from light to dark theme. 2 | const lightTheme = { 3 | // Components 4 | "33425bd93c1b8cea071df9b5297f0b19583a643b": { 5 | componentName: "iPhone X Status Bar / Light", 6 | mapsToKey: "f0d4aa5e63fff4392e3b3c22884523369f5d0424" 7 | }, 8 | "0489bde7fd0346a97eff3170167714838a8ffb9c": { 9 | componentName: "iPhone X Home Indicator / Light", 10 | mapsToKey: "5b8dce7a790466da546d319a69f5de220e1a66f1" 11 | }, 12 | "867fa47defeb07293aa37e5467e4ca487019dd78": { 13 | componentName: "System (Light) / Numeric Keyboard", 14 | mapsToKey: "3ee4cf479eefd5e181ff4abd1c982011438e692d" 15 | }, 16 | bca69f03db8e938cd78ca91c84e35553804ca8c1: { 17 | componentName: "System (Light) / Keyboard", 18 | mapsToKey: "e4dcbeb8549332e4c969ef4d0d302e75a7932c25" 19 | }, 20 | "46d6bed4edd9482b1452afab5ab0292b516c9e09": { 21 | componentName: "Navigation Tab / Light", 22 | mapsToKey: "64d16554561a7496b2d23854074ce923655918e0" 23 | }, 24 | "00230f03c08e00a787e9c2659c3165bcad7ae06b": { 25 | componentName: "Header / Guild / Light", 26 | mapsToKey: "e9622ab25248f31fb02b6faa00308b8faa4acb3e" 27 | }, 28 | "4592fb98edf78fdeea07d23445de948286e7c5f2": { 29 | componentName: "Header / DM", 30 | mapsToKey: "ff8b5c71a60fc212647c40e481c0bf1886a3ec85" 31 | }, 32 | "790d7d3d884a6d3dadc79bf3c48a4918f4c16ba7": { 33 | componentName: "Status Bar / Light", 34 | mapsToKey: "3588fe4d5a302b2fca2be2b0cb5c12e2a2f41c05" 35 | }, 36 | a9231a3d9fac5b7d26d56904c7b28d5d00bfff97: { 37 | componentName: "Guild Selected / Light", 38 | mapsToKey: "9e0a9f99024fb9baedcacbb123c84d7cc4b8f87a" 39 | }, 40 | "8c80d5bd7bdd80acec9793a719f6caaebba1c6ee": { 41 | componentName: "Messages Selected / Light", 42 | mapsToKey: "c25d89953041d095215c972fa55dc6f7776d9a54" 43 | }, 44 | bd56c3e89162d8274c4ca591f2ca1e1064658570: { 45 | componentName: "Navigation Tab / Dark", 46 | mapsToKey: "43c14ca23834d2aa3bf1e027a0635c7393e87378" 47 | }, 48 | "05fc2a6b4207baa8f672dc9e8a5d750c5d60711b": { 49 | componentName: "Windows Bar / Light", 50 | mapsToKey: "d31d651767116b73f9209c5362669782ff3a8a25" 51 | }, 52 | // Headings 53 | b19a14675b8adeb1528ab5f84e57b2eeed10d46c: { 54 | name: "Light / Header / Primary (900)", 55 | mapsToName: "Dark / Header / Primary (White)", 56 | mapsToKey: "5c1691cbeaaf4270107d34f1a12f02fdd04afa02" 57 | }, 58 | "608f2ea1aa64ff7f202e8c22cc4147a02be9d85b": { 59 | name: "Light / Header / Secondary (600)", 60 | mapsToName: "Dark / Header / Secondary (300)", 61 | mapsToKey: "bc090cb3b1c7313ae276acbd791b5b87b478ec59" 62 | }, 63 | // Text 64 | "546c7d46e754ac2b23b338783d72f206b77b6436": { 65 | name: "Light / Text / Normal (700)", 66 | mapsToName: "Dark / Text / Normal (200)", 67 | mapsToKey: "5c77a96137b698b5575557c069cabd6877d66e1e" 68 | }, 69 | "7d8703ec132ddaf6968f6d190d1e80031c559d7c": { 70 | name: "Light / Text / Muted (500)", 71 | mapsToName: "Dark / Text / Muted (400)", 72 | mapsToKey: "5d84ad92f3ad152f196e2093a3c0542a08dfba11" 73 | }, 74 | "64d3058dd508a4985670b2d19418a06a3503c9c2": { 75 | name: "Light / Text / Link", 76 | mapsToName: "Dark / Text / Link", 77 | mapsToKey: "bf03232753079bdd5bec6c55343b659876b5283f" 78 | }, 79 | "15320fd498dcd4e113c5bd587dca2d11d4492e84": { 80 | name: "themes/light/text/text-brand", 81 | mapsToName: "themes/dark/text/text-brand", 82 | mapsToKey: "6e4aef7677e2ea82c87465276522da7ef5a07121" 83 | }, 84 | c8d237080d38671193403b49cdc6a5778a14bf45: { 85 | name: "themes/light/text/text-danger", 86 | mapsToName: "themes/dark/text/text-danger", 87 | mapsToKey: "094cbaac0817be7bbfd8292cb98fc1e515e7ea0e" 88 | }, 89 | "0d95a7d4d30ef99ebd04abd5b2dd4708913f765b": { 90 | name: "themes/light/text/text-warning", 91 | mapsToName: "themes/dark/text/text-warning", 92 | mapsToKey: "df0622bb33232fe041c468e8d3dd37e5428b10e7" 93 | }, 94 | "71f64b08bdec4daf747a850b128e0994c4593c04": { 95 | name: "themes/light/text/text-positive", 96 | mapsToName: "themes/dark/text/text-positive", 97 | mapsToKey: "7733117cf1ef570b77332c86ba783af6cb735fc1" 98 | }, 99 | // Interactive Text & Icons 100 | "9c23a031773711e026394f4354661c37ee5b4682": { 101 | name: "Light / Interactive Text & Icons / Normal (600)", 102 | mapsToName: "Dark / Interactive Text & Icons / Normal (300)", 103 | mapsToKey: "287463bade90c1eed5ea4cb0b5d63794daa8aec2" 104 | }, 105 | "vector #757575": { 106 | name: "Unstyled Icon", 107 | mapsToName: "Dark / Interactive Text & Icons / Normal (300)", 108 | mapsToKey: "287463bade90c1eed5ea4cb0b5d63794daa8aec2" 109 | }, 110 | "vector #4f5660": { 111 | name: "Unstyled Icon", 112 | mapsToName: "Dark / Interactive Text & Icons / Normal (300)", 113 | mapsToKey: "287463bade90c1eed5ea4cb0b5d63794daa8aec2" 114 | }, 115 | "boolean_operation #757575": { 116 | name: "Unstyled Icon", 117 | mapsToName: "Dark / Interactive Text & Icons / Normal (300)", 118 | mapsToKey: "287463bade90c1eed5ea4cb0b5d63794daa8aec2" 119 | }, 120 | "boolean_operation #4f5660": { 121 | name: "Unstyled Icon", 122 | mapsToName: "Dark / Interactive Text & Icons / Normal (300)", 123 | mapsToKey: "287463bade90c1eed5ea4cb0b5d63794daa8aec2" 124 | }, 125 | e9542e95adf3bbe74286c2cf279fee64f7ba3279: { 126 | name: "themes/light/interactive/interactive-hover", 127 | mapsToName: "themes/dark/interactive/interactive-hover", 128 | mapsToKey: "502dcdf04992818dcbaed125ad711b446dee4c68" 129 | }, 130 | "620c98e8f9255a6107dee91745669e5b702b413c": { 131 | name: "Light / Interactive Text & Icons / Active (900)", 132 | mapsToName: "Dark / Interactive Text & Icons / Active (White)", 133 | mapsToKey: "3eddc15e90bbd7064aea7cc13dc13e23a712f0b0" 134 | }, 135 | "9328cd78a39149b070d68f98d9fe4df7a92bf67d": { 136 | name: "Light / Interactive Text & Icons / Muted (300)", 137 | mapsToName: "Dark / Interactive Text & Icons / Muted (500)", 138 | mapsToKey: "fa698aa2a724522a7c29efb0a662aec75a1be5a1" 139 | }, 140 | // Backgrounds 141 | "2449a2983d43793d80baa20c6c60e8a48e7f3a0c": { 142 | name: "Light / Background / Primary (White)", 143 | mapsToName: "Dark / Background / Primary (600)", 144 | mapsToKey: "4b93d40f61be15e255e87948a715521c3ae957e6" 145 | }, 146 | "frame #ffffff": { 147 | name: "White Background", 148 | mapsToName: "Dark / Background / Primary (600)", 149 | mapsToKey: "4b93d40f61be15e255e87948a715521c3ae957e6" 150 | }, 151 | "frame #5865f2": { 152 | name: "Unstyled Brand", 153 | mapsToName: "other/blurple (brand-500)", 154 | mapsToKey: "25b165222f45fd70dc3c8e68d1a25f8d379a597d" 155 | }, 156 | "rectangle #5865f2": { 157 | name: "Unstyled Brand", 158 | mapsToName: "other/blurple (brand-500)", 159 | mapsToKey: "25b165222f45fd70dc3c8e68d1a25f8d379a597d" 160 | }, 161 | "83704278c845a6a7ceb1f837387972ccb6d41960": { 162 | name: "Light / Background / Secondary (130)", 163 | mapsToName: "Dark / Background / Secondary (630)", 164 | mapsToKey: "fb1358e5bd6dec072801298238cf49ff77b79a4b" 165 | }, 166 | "6acd84c794796d112d4e9d22c4c8a5cae940a61d": { 167 | name: "Light / Background / Secondary Alternate", 168 | mapsToName: "Dark / Background / Secondary Alternate", 169 | mapsToKey: "abf9ad88ae1ade1a4b945b012f0965c9cdc068c9" 170 | }, 171 | dbd02a76b7b77c1976114c04068f0fbc22015fab: { 172 | name: "Light / Background / Tertiary (200)", 173 | mapsToName: "Dark / Background / Tertiary (700)", 174 | mapsToKey: "ef179b6abe6cb8779857e05a6333d33f7a2b9320" 175 | }, 176 | "7a199ce029a847f3a361dfb6a6e0ee4e4ba84d4f": { 177 | name: "Light / Background / Accent (500)", 178 | mapsToName: "Dark / Background / Accent (500)", 179 | mapsToKey: "3dd0e30ce0a8287eb91ec1fbeff92031e634ed01" 180 | }, 181 | "634ef95b53ab529a774f27ed16be07c0b3fb3a5f": { 182 | name: "Light / Override / Read Channels 360", 183 | mapsToName: "Dark / Override / Read Channels", 184 | mapsToKey: "bfcdf063eb2c1edb446ba5d7880da6a324cc9b4f" 185 | }, 186 | "6c8b08a42f9614842e880bf7bb795014d8fbae94": { 187 | name: "Light / Background / Floating (White)", 188 | mapsToName: "Dark / Background / Floating (800)", 189 | mapsToKey: "11516f4b43f381afb5a6bdf2c34b9437f0eecde1" 190 | }, 191 | c592ea0b26929cf1374f973b857027dbd21ffb12: { 192 | name: "themes/light/background/status-danger-background", 193 | mapsToName: "themes/dark/background/status-danger-background", 194 | mapsToKey: "b659c283950f8b335922f52e40cefd3cf679d297" 195 | }, 196 | "45f2139348b50263fda4704d4a9accea74540dcc": { 197 | name: "themes/light/background/status-warning-background", 198 | mapsToName: "themes/dark/background/status-warning-background", 199 | mapsToKey: "3dbd679897876b69bc9cc8fa38be83c525ac5ed5" 200 | }, 201 | "2a135fa63c0cea473936ced51ccd767b2f156739": { 202 | name: "themes/light/background/status-positive-background", 203 | mapsToName: "themes/dark/background/status-positive-background", 204 | mapsToKey: "746e170ac6e7ba80d171f01313735a3ec5535ef8" 205 | }, 206 | "30d44092c13231213143b50015907463dd1b6211": { 207 | name: "themes/light/background/background-mentioned", 208 | mapsToName: "themes/dark/background/background-mentioned", 209 | mapsToKey: "da21c08d5f887ae8d6195d7f8a7585219d670b93" 210 | }, 211 | "4d15ee684eb9fd6cb114d7fb585c83c9b0a598fd": { 212 | name: "themes/light/background/background-mentioned-hover", 213 | mapsToName: "themes/dark/background/background-mentioned-hover", 214 | mapsToKey: "39c91bf62536cb1c6f51087853c35afcc6462bac" 215 | }, 216 | "440a2d66490b7162417c740e66355f39d7b9e41a": { 217 | name: "themes/light/background/background-message-hover", 218 | mapsToName: "themes/dark/background/background-message-hover", 219 | mapsToKey: "1054e0c4bc3e52ae2c7c48aa0d0f95ed5d998587" 220 | }, 221 | "5747d5e2f1e6047746c77e9368e8d21324eb93d9": { 222 | name: "themes/light/background/background-mobile-primary", 223 | mapsToName: "themes/dark/background/background-mobile-primary", 224 | mapsToKey: "72a70771ff2a268130e7352250f374722f4d8bfe" 225 | }, 226 | de9f518c35096095c02c215543174a04900b07d7: { 227 | name: "themes/light/background/background-mobile-secondary", 228 | mapsToName: "themes/dark/background/background-mobile-secondary", 229 | mapsToKey: "251f85bc338c5411608c2dc141a538305ab6b4c1" 230 | }, 231 | "1fae53b19be2fe85aa44529cd3243c7b280173f1": { 232 | name: "themes/light/background/background-nested-floating", 233 | mapsToName: "themes/dark/background/background-nested-floating", 234 | mapsToKey: "1e1caa8f31ed3bb7ce6e6ce20dfe3187b20766c8" 235 | }, 236 | // Background Modifiers 237 | "35307396ae29aaeb583ae65891c69ec689f0c41e": { 238 | name: "Light / Background Mod / Hover", 239 | mapsToName: "Dark / Background Mod / Hover", 240 | mapsToKey: "d6c9270834b11c99ee651f0f5072ad2c63701165" 241 | }, 242 | ddadf76919d9bacb925242a024dc1e2f5f517a46: { 243 | name: "Light / Background Mod / Active", 244 | mapsToName: "Dark / Background Mod / Active", 245 | mapsToKey: "bcf890d7a215c65deef97fb3d3f5bcebc9869bab" 246 | }, 247 | "5af2eaf14901472c26b641997796bdba76ee1794": { 248 | name: "Light / Background Mod / Selected", 249 | mapsToName: "Dark / Background Mod / Selected", 250 | mapsToKey: "ce012db42f35fb58b4fe1d6d8b46c4905a8fad0a" 251 | }, 252 | "08c7091f8d6950dc3f616afe8ed45b086f9124c7": { 253 | name: "Light / Background Mod / Accent", 254 | mapsToName: "Dark / Background Mod / Accent", 255 | mapsToKey: "a6a3dc153f0e589408186176ebf8f20ed2f9bda3" 256 | }, 257 | // Status 258 | "6faa6d09b47caeb32fa0f5f81c561dcb7d68e9b1": { 259 | name: "themes/light/status/status-positive", 260 | mapsToName: "themes/dark/status/status-positive", 261 | mapsToKey: "61c493d9d14f2a5ae52c2037149773f0cd7690a5" 262 | }, 263 | "0c9cfa27f153e6a5a9954242bb6ae3cac02d4468": { 264 | name: "themes/light/status/status-danger", 265 | mapsToName: "themes/dark/status/status-danger", 266 | mapsToKey: "0ff4d563aae53dd8012f78a67f9fd182693a0f21" 267 | }, 268 | "9fa2f99cffe7ba587f259e98fb4de12c0b893223": { 269 | name: "themes/light/status/status-warning", 270 | mapsToName: "themes/dark/status/status-warning", 271 | mapsToKey: "f719fb8e7bf04342010ecb37165e55aa8a638d35" 272 | }, 273 | // Other 274 | "3c098a8d09acbd25ef37e7fc0b657c2dc78f243e": { 275 | name: "themes/light/other/channeltextarea-background", 276 | mapsToName: "themes/dark/other/channeltextarea-background", 277 | mapsToKey: "6c54be693a4bbdff6fa4c02f672bc5c9e4654f8b" 278 | }, 279 | d1dbae483f4eefcf5adccfbba8e6d50dbef1ec27: { 280 | name: "themes/light/other/focus-primary", 281 | mapsToName: "themes/dark/other/focus-primary", 282 | mapsToKey: "a4d76cf75156ab760df1685a30dadab20724010e" 283 | }, 284 | bbdc5cb26595f77283b8dfe51e659c5bfdc6a2d0: { 285 | name: "themes/light/other/control-brand-foreground", 286 | mapsToName: "themes/dark/other/control-brand-foreground", 287 | mapsToKey: "7337ac931b2c9b699d44e6e783637e5afac50298" 288 | }, 289 | "084969be9bfee752064df1c504b6ba07a8d727ad": { 290 | name: "themes/light/other/scrollbar-thin-thumb", 291 | mapsToName: "themes/dark/other/scrollbar-thin-thumb", 292 | mapsToKey: "a926774d558d0e70f505df697c21c12dc4270206" 293 | }, 294 | "6436d02f21d749b84cbd8736bd453dad1c4ac3ab": { 295 | name: "themes/light/other/scrollbar-auto-thumb", 296 | mapsToName: "themes/dark/other/scrollbar-auto-thumb", 297 | mapsToKey: "2ab24b1a3901fae7960deb8a36e49f0d6b1732af" 298 | }, 299 | "54fb146609c07fba199d4066f8c2ce14829a0d0a": { 300 | name: "themes/light/other/scrollbar-auto-track", 301 | mapsToName: "themes/dark/other/scrollbar-auto-track", 302 | mapsToKey: "d509bf14b1c3aac55dc0fd6b822f628956ad80c3" 303 | }, 304 | // Effects 305 | bf64ca51f902a903935680f692618a5eba4ea894: { 306 | name: "Border Elevation / Light", 307 | mapsToName: "Border Elevation / Dark", 308 | mapsToKey: "b7edafef4513a59a40c8ba7adb382a0b6d3313ff" 309 | }, 310 | "30f011bbe03506a59052d7f8435cc1ec3b743b19": { 311 | name: "High Elevation / Light", 312 | mapsToName: "High Elevation / Dark", 313 | mapsToKey: "67aabb2beb8092e4c0094e0175657bb0758e6ba8" 314 | }, 315 | // Deprecated 316 | "5afa1524777579ea2eebc983f3210547c838fd3a": { 317 | name: "BETA_DEPRECATED/header/header-primary", 318 | mapsToName: "themes/dark/header/header-primary", 319 | mapsToKey: "5c1691cbeaaf4270107d34f1a12f02fdd04afa02" 320 | }, 321 | "206fc2ae47513da5db7cd705e758593221bb4b63": { 322 | name: "BETA_DEPRECATED/header/header-secondary", 323 | mapsToName: "themes/dark/header/header-secondary", 324 | mapsToKey: "bc090cb3b1c7313ae276acbd791b5b87b478ec59" 325 | }, 326 | ac344309d7e7d20a6b518d49d1501e3d134d996b: { 327 | name: "BETA_DEPRECATED/background/background-primary", 328 | mapsToName: "themes/dark/background/background-primary", 329 | mapsToKey: "4b93d40f61be15e255e87948a715521c3ae957e6" 330 | }, 331 | "5100d653a726bf86e3b43a3349c396474bd63950": { 332 | name: "BETA_DEPRECATED/background/background-secondary", 333 | mapsToName: "themes/dark/background/background-secondary", 334 | mapsToKey: "fb1358e5bd6dec072801298238cf49ff77b79a4b" 335 | }, 336 | "6e18949a990499bc0af852de9de4f2e378b1f954": { 337 | name: "BETA_DEPRECATED/text/text-normal", 338 | mapsToName: "themes/dark/text/text-normal", 339 | mapsToKey: "5c77a96137b698b5575557c069cabd6877d66e1e" 340 | }, 341 | "15d9230a1d41d9acd21b63012f86613f879cfaae": { 342 | name: "BETA_DEPRECATED/text/text-muted", 343 | mapsToName: "themes/dark/text/text-muted", 344 | mapsToKey: "5d84ad92f3ad152f196e2093a3c0542a08dfba11" 345 | } 346 | }; 347 | 348 | export { lightTheme }; 349 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "outDir": "dist", 5 | "jsx": "react", 6 | "noUnusedLocals": true, 7 | "noUnusedParameters": true, 8 | "experimentalDecorators": true, 9 | "removeComments": true, 10 | "noImplicitAny": false, 11 | "moduleResolution": "node" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended", 5 | "tslint-react" 6 | ], 7 | "jsRules": {}, 8 | "rules": { 9 | "ordered-imports": false, 10 | "object-literal-sort-keys": false, 11 | "member-ordering": false, 12 | "await-promise": false, 13 | "interface-name": false, 14 | "quotemark": false, 15 | "curly": false, 16 | "member-access": [true, "no-public"], 17 | "semicolon": false, 18 | "no-trailing-whitespace": false, 19 | "no-console": false, 20 | "jsx-wrap-multiline": false, 21 | "jsx-no-lambda": false 22 | }, 23 | "rulesDirectory": [] 24 | } -------------------------------------------------------------------------------- /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 | 5 | module.exports = (env, argv) => ({ 6 | mode: argv.mode === 'production' ? 'production' : 'development', 7 | 8 | // This is necessary because Figma's 'eval' works differently than normal eval 9 | devtool: argv.mode === 'production' ? false : 'inline-source-map', 10 | 11 | entry: { 12 | ui: './src/app/index.tsx', // The entry point for your UI code 13 | code: './src/plugin/controller.ts', // The entry point for your plugin code 14 | }, 15 | 16 | module: { 17 | rules: [ 18 | // Converts TypeScript code to JavaScript 19 | { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ }, 20 | 21 | // Enables including CSS by doing "import './file.css'" in your TypeScript code 22 | { test: /\.css$/, loader: [{ loader: 'style-loader' }, { loader: 'css-loader' }] }, 23 | 24 | // Allows you to use "<%= require('./file.svg') %>" in your HTML code to get a data URI 25 | { test: /\.(png|jpg|gif|webp|svg)$/, loader: [{ loader: 'url-loader' }] }, 26 | ], 27 | }, 28 | 29 | // Webpack tries these extensions for you if you omit the extension like "import './file'" 30 | resolve: { extensions: ['.tsx', '.ts', '.jsx', '.js'] }, 31 | 32 | output: { 33 | filename: '[name].js', 34 | path: path.resolve(__dirname, 'dist'), // Compile into a folder called "dist" 35 | }, 36 | 37 | // Tells Webpack to generate "ui.html" and to inline "ui.ts" into it 38 | plugins: [ 39 | new HtmlWebpackPlugin({ 40 | template: './src/app/index.html', 41 | filename: 'ui.html', 42 | inlineSource: '.(js)$', 43 | chunks: ['ui'], 44 | }), 45 | new HtmlWebpackInlineSourcePlugin(), 46 | ], 47 | }) --------------------------------------------------------------------------------