├── .github └── FUNDING.yml ├── .gitignore ├── README.md ├── assets ├── banner.png ├── icon_dark.png ├── icon_dark_1x.png ├── icon_white.png ├── logo.png ├── logo_dark.png └── usage.gif ├── figma.d.ts ├── manifest.json ├── package-lock.json ├── package.json ├── src ├── code.js ├── ui.css ├── ui.html └── ui.js └── webpack.config.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | #github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | #patreon: # Replace with a single Patreon username 5 | #open_collective: # Replace with a single Open Collective username 6 | #ko_fi: # Replace with a single Ko-fi username 7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | #liberapay: # Replace with a single Liberapay username 10 | #issuehunt: # Replace with a single IssueHunt username 11 | #otechie: # Replace with a single Otechie username 12 | custom: ['paypal.me/nitinrgupta'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | logo 2 | 3 | 4 | A figma plugin to place elements into a grid with defined rows and columns to create a sick pattern or a texture! 5 | 6 | ![](https://github.com/nitinrgupta/figma-pattern-hero/blob/master/assets/banner.png) 7 | 8 | ![](https://github.com/nitinrgupta/figma-pattern-hero/blob/master/assets/usage.gif) 9 | 10 | 11 | 12 | ## Development 13 | 14 | Install the packages 15 | ``` 16 | npm i 17 | ``` 18 | 19 | 20 | Run the plugin in dev mode 21 | ``` 22 | npx webpack --mode=development --watch 23 | ``` 24 | 25 | 26 | Build for production 27 | ``` 28 | npx webpack --mode=production 29 | ``` 30 | -------------------------------------------------------------------------------- /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yenargy/figma-pattern-hero/6ff802bc26184e7f9c4d50d579d09c7ea4395125/assets/banner.png -------------------------------------------------------------------------------- /assets/icon_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yenargy/figma-pattern-hero/6ff802bc26184e7f9c4d50d579d09c7ea4395125/assets/icon_dark.png -------------------------------------------------------------------------------- /assets/icon_dark_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yenargy/figma-pattern-hero/6ff802bc26184e7f9c4d50d579d09c7ea4395125/assets/icon_dark_1x.png -------------------------------------------------------------------------------- /assets/icon_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yenargy/figma-pattern-hero/6ff802bc26184e7f9c4d50d579d09c7ea4395125/assets/icon_white.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yenargy/figma-pattern-hero/6ff802bc26184e7f9c4d50d579d09c7ea4395125/assets/logo.png -------------------------------------------------------------------------------- /assets/logo_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yenargy/figma-pattern-hero/6ff802bc26184e7f9c4d50d579d09c7ea4395125/assets/logo_dark.png -------------------------------------------------------------------------------- /assets/usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yenargy/figma-pattern-hero/6ff802bc26184e7f9c4d50d579d09c7ea4395125/assets/usage.gif -------------------------------------------------------------------------------- /figma.d.ts: -------------------------------------------------------------------------------- 1 | // Figma Plugin API version 1, update 11 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(type: "selectionchange" | "currentpagechange" | "close", callback: () => void): void 28 | once(type: "selectionchange" | "currentpagechange" | "close", callback: () => void): void 29 | off(type: "selectionchange" | "currentpagechange" | "close", callback: () => void): void 30 | 31 | readonly mixed: unique symbol 32 | 33 | createRectangle(): RectangleNode 34 | createLine(): LineNode 35 | createEllipse(): EllipseNode 36 | createPolygon(): PolygonNode 37 | createStar(): StarNode 38 | createVector(): VectorNode 39 | createText(): TextNode 40 | createFrame(): FrameNode 41 | createComponent(): ComponentNode 42 | createPage(): PageNode 43 | createSlice(): SliceNode 44 | /** 45 | * [DEPRECATED]: This API often fails to create a valid boolean operation. Use figma.union, figma.subtract, figma.intersect and figma.exclude instead. 46 | */ 47 | createBooleanOperation(): BooleanOperationNode 48 | 49 | createPaintStyle(): PaintStyle 50 | createTextStyle(): TextStyle 51 | createEffectStyle(): EffectStyle 52 | createGridStyle(): GridStyle 53 | 54 | // The styles are returned in the same order as displayed in the UI. Only 55 | // local styles are returned. Never styles from team library. 56 | getLocalPaintStyles(): PaintStyle[] 57 | getLocalTextStyles(): TextStyle[] 58 | getLocalEffectStyles(): EffectStyle[] 59 | getLocalGridStyles(): GridStyle[] 60 | 61 | importComponentByKeyAsync(key: string): Promise 62 | importStyleByKeyAsync(key: string): Promise 63 | 64 | listAvailableFontsAsync(): Promise 65 | loadFontAsync(fontName: FontName): Promise 66 | readonly hasMissingFont: boolean 67 | 68 | createNodeFromSvg(svg: string): FrameNode 69 | 70 | createImage(data: Uint8Array): Image 71 | getImageByHash(hash: string): Image 72 | 73 | group(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): GroupNode 74 | flatten(nodes: ReadonlyArray, parent?: BaseNode & ChildrenMixin, index?: number): VectorNode 75 | 76 | union(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 77 | subtract(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 78 | intersect(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 79 | exclude(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 80 | } 81 | 82 | interface ClientStorageAPI { 83 | getAsync(key: string): Promise 84 | setAsync(key: string, value: any): Promise 85 | } 86 | 87 | interface NotificationOptions { 88 | timeout?: number 89 | } 90 | 91 | interface NotificationHandler { 92 | cancel: () => void 93 | } 94 | 95 | interface ShowUIOptions { 96 | visible?: boolean 97 | width?: number 98 | height?: number 99 | } 100 | 101 | interface UIPostMessageOptions { 102 | origin?: string 103 | } 104 | 105 | interface OnMessageProperties { 106 | origin: string 107 | } 108 | 109 | type MessageEventHandler = (pluginMessage: any, props: OnMessageProperties) => void 110 | 111 | interface UIAPI { 112 | show(): void 113 | hide(): void 114 | resize(width: number, height: number): void 115 | close(): void 116 | 117 | postMessage(pluginMessage: any, options?: UIPostMessageOptions): void 118 | onmessage: MessageEventHandler | undefined 119 | on(type: "message", callback: MessageEventHandler): void 120 | once(type: "message", callback: MessageEventHandler): void 121 | off(type: "message", callback: MessageEventHandler): void 122 | } 123 | 124 | interface ViewportAPI { 125 | center: Vector 126 | zoom: number 127 | scrollAndZoomIntoView(nodes: ReadonlyArray): void 128 | readonly bounds: Rect 129 | } 130 | 131 | //////////////////////////////////////////////////////////////////////////////// 132 | // Datatypes 133 | 134 | type Transform = [ 135 | [number, number, number], 136 | [number, number, number] 137 | ] 138 | 139 | interface Vector { 140 | readonly x: number 141 | readonly y: number 142 | } 143 | 144 | interface Rect { 145 | readonly x: number 146 | readonly y: number 147 | readonly width: number 148 | readonly height: number 149 | } 150 | 151 | interface RGB { 152 | readonly r: number 153 | readonly g: number 154 | readonly b: number 155 | } 156 | 157 | interface RGBA { 158 | readonly r: number 159 | readonly g: number 160 | readonly b: number 161 | readonly a: number 162 | } 163 | 164 | interface FontName { 165 | readonly family: string 166 | readonly style: string 167 | } 168 | 169 | type TextCase = "ORIGINAL" | "UPPER" | "LOWER" | "TITLE" 170 | 171 | type TextDecoration = "NONE" | "UNDERLINE" | "STRIKETHROUGH" 172 | 173 | interface ArcData { 174 | readonly startingAngle: number 175 | readonly endingAngle: number 176 | readonly innerRadius: number 177 | } 178 | 179 | interface ShadowEffect { 180 | readonly type: "DROP_SHADOW" | "INNER_SHADOW" 181 | readonly color: RGBA 182 | readonly offset: Vector 183 | readonly radius: number 184 | readonly visible: boolean 185 | readonly blendMode: BlendMode 186 | } 187 | 188 | interface BlurEffect { 189 | readonly type: "LAYER_BLUR" | "BACKGROUND_BLUR" 190 | readonly radius: number 191 | readonly visible: boolean 192 | } 193 | 194 | type Effect = ShadowEffect | BlurEffect 195 | 196 | type ConstraintType = "MIN" | "CENTER" | "MAX" | "STRETCH" | "SCALE" 197 | 198 | interface Constraints { 199 | readonly horizontal: ConstraintType 200 | readonly vertical: ConstraintType 201 | } 202 | 203 | interface ColorStop { 204 | readonly position: number 205 | readonly color: RGBA 206 | } 207 | 208 | interface ImageFilters { 209 | readonly exposure?: number 210 | readonly contrast?: number 211 | readonly saturation?: number 212 | readonly temperature?: number 213 | readonly tint?: number 214 | readonly highlights?: number 215 | readonly shadows?: number 216 | } 217 | 218 | interface SolidPaint { 219 | readonly type: "SOLID" 220 | readonly color: RGB 221 | 222 | readonly visible?: boolean 223 | readonly opacity?: number 224 | readonly blendMode?: BlendMode 225 | } 226 | 227 | interface GradientPaint { 228 | readonly type: "GRADIENT_LINEAR" | "GRADIENT_RADIAL" | "GRADIENT_ANGULAR" | "GRADIENT_DIAMOND" 229 | readonly gradientTransform: Transform 230 | readonly gradientStops: ReadonlyArray 231 | 232 | readonly visible?: boolean 233 | readonly opacity?: number 234 | readonly blendMode?: BlendMode 235 | } 236 | 237 | interface ImagePaint { 238 | readonly type: "IMAGE" 239 | readonly scaleMode: "FILL" | "FIT" | "CROP" | "TILE" 240 | readonly imageHash: string | null 241 | readonly imageTransform?: Transform // setting for "CROP" 242 | readonly scalingFactor?: number // setting for "TILE" 243 | readonly filters?: ImageFilters 244 | 245 | readonly visible?: boolean 246 | readonly opacity?: number 247 | readonly blendMode?: BlendMode 248 | } 249 | 250 | type Paint = SolidPaint | GradientPaint | ImagePaint 251 | 252 | interface Guide { 253 | readonly axis: "X" | "Y" 254 | readonly offset: number 255 | } 256 | 257 | interface RowsColsLayoutGrid { 258 | readonly pattern: "ROWS" | "COLUMNS" 259 | readonly alignment: "MIN" | "MAX" | "STRETCH" | "CENTER" 260 | readonly gutterSize: number 261 | 262 | readonly count: number // Infinity when "Auto" is set in the UI 263 | readonly sectionSize?: number // Not set for alignment: "STRETCH" 264 | readonly offset?: number // Not set for alignment: "CENTER" 265 | 266 | readonly visible?: boolean 267 | readonly color?: RGBA 268 | } 269 | 270 | interface GridLayoutGrid { 271 | readonly pattern: "GRID" 272 | readonly sectionSize: number 273 | 274 | readonly visible?: boolean 275 | readonly color?: RGBA 276 | } 277 | 278 | type LayoutGrid = RowsColsLayoutGrid | GridLayoutGrid 279 | 280 | interface ExportSettingsConstraints { 281 | readonly type: "SCALE" | "WIDTH" | "HEIGHT" 282 | readonly value: number 283 | } 284 | 285 | interface ExportSettingsImage { 286 | readonly format: "JPG" | "PNG" 287 | readonly contentsOnly?: boolean // defaults to true 288 | readonly suffix?: string 289 | readonly constraint?: ExportSettingsConstraints 290 | } 291 | 292 | interface ExportSettingsSVG { 293 | readonly format: "SVG" 294 | readonly contentsOnly?: boolean // defaults to true 295 | readonly suffix?: string 296 | readonly svgOutlineText?: boolean // defaults to true 297 | readonly svgIdAttribute?: boolean // defaults to false 298 | readonly svgSimplifyStroke?: boolean // defaults to true 299 | } 300 | 301 | interface ExportSettingsPDF { 302 | readonly format: "PDF" 303 | readonly contentsOnly?: boolean // defaults to true 304 | readonly suffix?: string 305 | } 306 | 307 | type ExportSettings = ExportSettingsImage | ExportSettingsSVG | ExportSettingsPDF 308 | 309 | type WindingRule = "NONZERO" | "EVENODD" 310 | 311 | interface VectorVertex { 312 | readonly x: number 313 | readonly y: number 314 | readonly strokeCap?: StrokeCap 315 | readonly strokeJoin?: StrokeJoin 316 | readonly cornerRadius?: number 317 | readonly handleMirroring?: HandleMirroring 318 | } 319 | 320 | interface VectorSegment { 321 | readonly start: number 322 | readonly end: number 323 | readonly tangentStart?: Vector // Defaults to { x: 0, y: 0 } 324 | readonly tangentEnd?: Vector // Defaults to { x: 0, y: 0 } 325 | } 326 | 327 | interface VectorRegion { 328 | readonly windingRule: WindingRule 329 | readonly loops: ReadonlyArray> 330 | } 331 | 332 | interface VectorNetwork { 333 | readonly vertices: ReadonlyArray 334 | readonly segments: ReadonlyArray 335 | readonly regions?: ReadonlyArray // Defaults to [] 336 | } 337 | 338 | interface VectorPath { 339 | readonly windingRule: WindingRule | "NONE" 340 | readonly data: string 341 | } 342 | 343 | type VectorPaths = ReadonlyArray 344 | 345 | interface LetterSpacing { 346 | readonly value: number 347 | readonly unit: "PIXELS" | "PERCENT" 348 | } 349 | 350 | type LineHeight = { 351 | readonly value: number 352 | readonly unit: "PIXELS" | "PERCENT" 353 | } | { 354 | readonly unit: "AUTO" 355 | } 356 | 357 | type BlendMode = 358 | "PASS_THROUGH" | 359 | "NORMAL" | 360 | "DARKEN" | 361 | "MULTIPLY" | 362 | "LINEAR_BURN" | 363 | "COLOR_BURN" | 364 | "LIGHTEN" | 365 | "SCREEN" | 366 | "LINEAR_DODGE" | 367 | "COLOR_DODGE" | 368 | "OVERLAY" | 369 | "SOFT_LIGHT" | 370 | "HARD_LIGHT" | 371 | "DIFFERENCE" | 372 | "EXCLUSION" | 373 | "HUE" | 374 | "SATURATION" | 375 | "COLOR" | 376 | "LUMINOSITY" 377 | 378 | interface Font { 379 | fontName: FontName 380 | } 381 | 382 | type Reaction = { action: Action, trigger: Trigger } 383 | 384 | type Action = 385 | { readonly type: "BACK" | "CLOSE" } | 386 | { readonly type: "URL", url: string } | 387 | { readonly type: "NODE" 388 | readonly destinationId: string | null 389 | readonly navigation: Navigation 390 | readonly transition: Transition | null 391 | readonly preserveScrollPosition: boolean 392 | 393 | // Only present if navigation == "OVERLAY" and the destination uses 394 | // overlay position type "RELATIVE" 395 | readonly overlayRelativePosition?: Vector 396 | } 397 | 398 | interface SimpleTransition { 399 | readonly type: "DISSOLVE" | "SMART_ANIMATE" 400 | readonly easing: Easing 401 | readonly duration: number 402 | } 403 | 404 | interface DirectionalTransition { 405 | readonly type: "MOVE_IN" | "MOVE_OUT" | "PUSH" | "SLIDE_IN" | "SLIDE_OUT" 406 | readonly direction: "LEFT" | "RIGHT" | "TOP" | "BOTTOM" 407 | readonly matchLayers: boolean 408 | 409 | readonly easing: Easing 410 | readonly duration: number 411 | } 412 | 413 | type Transition = SimpleTransition | DirectionalTransition 414 | 415 | type Trigger = 416 | { readonly type: "ON_CLICK" | "ON_HOVER" | "ON_PRESS" | "ON_DRAG" } | 417 | { readonly type: "AFTER_TIMEOUT", readonly timeout: number } | 418 | { readonly type: "MOUSE_ENTER" | "MOUSE_LEAVE" | "MOUSE_UP" | "MOUSE_DOWN" 419 | readonly delay: number 420 | } 421 | 422 | type Navigation = "NAVIGATE" | "SWAP" | "OVERLAY" 423 | 424 | interface Easing { 425 | readonly type: "EASE_IN" | "EASE_OUT" | "EASE_IN_AND_OUT" | "LINEAR" 426 | } 427 | 428 | type OverflowDirection = "NONE" | "HORIZONTAL" | "VERTICAL" | "BOTH" 429 | 430 | type OverlayPositionType = "CENTER" | "TOP_LEFT" | "TOP_CENTER" | "TOP_RIGHT" | "BOTTOM_LEFT" | "BOTTOM_CENTER" | "BOTTOM_RIGHT" | "MANUAL" 431 | 432 | type OverlayBackground = 433 | { readonly type: "NONE" } | 434 | { readonly type: "SOLID_COLOR", readonly color: RGBA } 435 | 436 | type OverlayBackgroundInteraction = "NONE" | "CLOSE_ON_CLICK_OUTSIDE" 437 | 438 | //////////////////////////////////////////////////////////////////////////////// 439 | // Mixins 440 | 441 | interface BaseNodeMixin { 442 | readonly id: string 443 | readonly parent: (BaseNode & ChildrenMixin) | null 444 | name: string // Note: setting this also sets `autoRename` to false on TextNodes 445 | readonly removed: boolean 446 | toString(): string 447 | remove(): void 448 | 449 | getPluginData(key: string): string 450 | setPluginData(key: string, value: string): void 451 | 452 | // Namespace is a string that must be at least 3 alphanumeric characters, and should 453 | // be a name related to your plugin. Other plugins will be able to read this data. 454 | getSharedPluginData(namespace: string, key: string): string 455 | setSharedPluginData(namespace: string, key: string, value: string): void 456 | } 457 | 458 | interface SceneNodeMixin { 459 | visible: boolean 460 | locked: boolean 461 | } 462 | 463 | interface ChildrenMixin { 464 | readonly children: ReadonlyArray 465 | 466 | appendChild(child: SceneNode): void 467 | insertChild(index: number, child: SceneNode): void 468 | 469 | findChildren(callback?: (node: SceneNode) => boolean): SceneNode[] 470 | findChild(callback: (node: SceneNode) => boolean): SceneNode | null 471 | 472 | /** 473 | * If you only need to search immediate children, it is much faster 474 | * to call node.children.filter(callback) 475 | */ 476 | findAll(callback?: (node: SceneNode) => boolean): SceneNode[] 477 | 478 | /** 479 | * If you only need to search immediate children, it is much faster 480 | * to call node.children.find(callback) 481 | */ 482 | findOne(callback: (node: SceneNode) => boolean): SceneNode | null 483 | } 484 | 485 | interface ConstraintMixin { 486 | constraints: Constraints 487 | } 488 | 489 | interface LayoutMixin { 490 | readonly absoluteTransform: Transform 491 | relativeTransform: Transform 492 | x: number 493 | y: number 494 | rotation: number // In degrees 495 | 496 | readonly width: number 497 | readonly height: number 498 | constrainProportions: boolean 499 | 500 | layoutAlign: "MIN" | "CENTER" | "MAX" | "STRETCH" // applicable only inside auto-layout frames 501 | 502 | resize(width: number, height: number): void 503 | resizeWithoutConstraints(width: number, height: number): void 504 | } 505 | 506 | interface BlendMixin { 507 | opacity: number 508 | blendMode: BlendMode 509 | isMask: boolean 510 | effects: ReadonlyArray 511 | effectStyleId: string 512 | } 513 | 514 | interface ContainerMixin { 515 | expanded: boolean 516 | backgrounds: ReadonlyArray // DEPRECATED: use 'fills' instead 517 | layoutGrids: ReadonlyArray 518 | clipsContent: boolean 519 | guides: ReadonlyArray 520 | gridStyleId: string 521 | backgroundStyleId: string // DEPRECATED: use 'fillStyleId' instead 522 | } 523 | 524 | type StrokeCap = "NONE" | "ROUND" | "SQUARE" | "ARROW_LINES" | "ARROW_EQUILATERAL" 525 | type StrokeJoin = "MITER" | "BEVEL" | "ROUND" 526 | type HandleMirroring = "NONE" | "ANGLE" | "ANGLE_AND_LENGTH" 527 | 528 | interface GeometryMixin { 529 | fills: ReadonlyArray | PluginAPI['mixed'] 530 | strokes: ReadonlyArray 531 | strokeWeight: number 532 | strokeMiterLimit: number 533 | strokeAlign: "CENTER" | "INSIDE" | "OUTSIDE" 534 | strokeCap: StrokeCap | PluginAPI['mixed'] 535 | strokeJoin: StrokeJoin | PluginAPI['mixed'] 536 | dashPattern: ReadonlyArray 537 | fillStyleId: string | PluginAPI['mixed'] 538 | strokeStyleId: string 539 | outlineStroke(): VectorNode | null 540 | } 541 | 542 | interface CornerMixin { 543 | cornerRadius: number | PluginAPI['mixed'] 544 | cornerSmoothing: number 545 | } 546 | 547 | interface RectangleCornerMixin { 548 | topLeftRadius: number 549 | topRightRadius: number 550 | bottomLeftRadius: number 551 | bottomRightRadius: number 552 | } 553 | 554 | interface ExportMixin { 555 | exportSettings: ReadonlyArray 556 | exportAsync(settings?: ExportSettings): Promise // Defaults to PNG format 557 | } 558 | 559 | interface RelaunchableMixin { 560 | setRelaunchData(relaunchData: { [command: string]: /* description */ string }): void 561 | } 562 | 563 | interface ReactionMixin { 564 | readonly reactions: ReadonlyArray 565 | } 566 | 567 | interface DefaultShapeMixin extends 568 | BaseNodeMixin, SceneNodeMixin, ReactionMixin, 569 | BlendMixin, GeometryMixin, LayoutMixin, 570 | ExportMixin, RelaunchableMixin { 571 | } 572 | 573 | interface DefaultFrameMixin extends 574 | BaseNodeMixin, SceneNodeMixin, ReactionMixin, 575 | ChildrenMixin, ContainerMixin, 576 | GeometryMixin, CornerMixin, RectangleCornerMixin, 577 | BlendMixin, ConstraintMixin, LayoutMixin, 578 | ExportMixin, RelaunchableMixin { 579 | 580 | layoutMode: "NONE" | "HORIZONTAL" | "VERTICAL" 581 | counterAxisSizingMode: "FIXED" | "AUTO" // applicable only if layoutMode != "NONE" 582 | horizontalPadding: number // applicable only if layoutMode != "NONE" 583 | verticalPadding: number // applicable only if layoutMode != "NONE" 584 | itemSpacing: number // applicable only if layoutMode != "NONE" 585 | 586 | overflowDirection: OverflowDirection 587 | numberOfFixedChildren: number 588 | 589 | readonly overlayPositionType: OverlayPositionType 590 | readonly overlayBackground: OverlayBackground 591 | readonly overlayBackgroundInteraction: OverlayBackgroundInteraction 592 | } 593 | 594 | //////////////////////////////////////////////////////////////////////////////// 595 | // Nodes 596 | 597 | interface DocumentNode extends BaseNodeMixin { 598 | readonly type: "DOCUMENT" 599 | 600 | readonly children: ReadonlyArray 601 | 602 | appendChild(child: PageNode): void 603 | insertChild(index: number, child: PageNode): void 604 | findChildren(callback?: (node: PageNode) => boolean): Array 605 | findChild(callback: (node: PageNode) => boolean): PageNode | null 606 | 607 | /** 608 | * If you only need to search immediate children, it is much faster 609 | * to call node.children.filter(callback) 610 | */ 611 | findAll(callback?: (node: PageNode | SceneNode) => boolean): Array 612 | 613 | /** 614 | * If you only need to search immediate children, it is much faster 615 | * to call node.children.find(callback) 616 | */ 617 | findOne(callback: (node: PageNode | SceneNode) => boolean): PageNode | SceneNode | null 618 | } 619 | 620 | interface PageNode extends BaseNodeMixin, ChildrenMixin, ExportMixin, RelaunchableMixin { 621 | 622 | readonly type: "PAGE" 623 | clone(): PageNode 624 | 625 | guides: ReadonlyArray 626 | selection: ReadonlyArray 627 | 628 | backgrounds: ReadonlyArray 629 | 630 | readonly prototypeStartNode: FrameNode | GroupNode | ComponentNode | InstanceNode | null 631 | } 632 | 633 | interface FrameNode extends DefaultFrameMixin { 634 | readonly type: "FRAME" 635 | clone(): FrameNode 636 | } 637 | 638 | interface GroupNode extends 639 | BaseNodeMixin, SceneNodeMixin, ReactionMixin, 640 | ChildrenMixin, ContainerMixin, BlendMixin, 641 | LayoutMixin, ExportMixin, RelaunchableMixin { 642 | 643 | readonly type: "GROUP" 644 | clone(): GroupNode 645 | } 646 | 647 | interface SliceNode extends 648 | BaseNodeMixin, SceneNodeMixin, LayoutMixin, 649 | ExportMixin, RelaunchableMixin { 650 | 651 | readonly type: "SLICE" 652 | clone(): SliceNode 653 | } 654 | 655 | interface RectangleNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin, RectangleCornerMixin { 656 | readonly type: "RECTANGLE" 657 | clone(): RectangleNode 658 | } 659 | 660 | interface LineNode extends DefaultShapeMixin, ConstraintMixin { 661 | readonly type: "LINE" 662 | clone(): LineNode 663 | } 664 | 665 | interface EllipseNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 666 | readonly type: "ELLIPSE" 667 | clone(): EllipseNode 668 | arcData: ArcData 669 | } 670 | 671 | interface PolygonNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 672 | readonly type: "POLYGON" 673 | clone(): PolygonNode 674 | pointCount: number 675 | } 676 | 677 | interface StarNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 678 | readonly type: "STAR" 679 | clone(): StarNode 680 | pointCount: number 681 | innerRadius: number 682 | } 683 | 684 | interface VectorNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 685 | readonly type: "VECTOR" 686 | clone(): VectorNode 687 | vectorNetwork: VectorNetwork 688 | vectorPaths: VectorPaths 689 | handleMirroring: HandleMirroring | PluginAPI['mixed'] 690 | } 691 | 692 | interface TextNode extends DefaultShapeMixin, ConstraintMixin { 693 | readonly type: "TEXT" 694 | clone(): TextNode 695 | characters: string 696 | readonly hasMissingFont: boolean 697 | textAlignHorizontal: "LEFT" | "CENTER" | "RIGHT" | "JUSTIFIED" 698 | textAlignVertical: "TOP" | "CENTER" | "BOTTOM" 699 | textAutoResize: "NONE" | "WIDTH_AND_HEIGHT" | "HEIGHT" 700 | paragraphIndent: number 701 | paragraphSpacing: number 702 | autoRename: boolean 703 | 704 | textStyleId: string | PluginAPI['mixed'] 705 | fontSize: number | PluginAPI['mixed'] 706 | fontName: FontName | PluginAPI['mixed'] 707 | textCase: TextCase | PluginAPI['mixed'] 708 | textDecoration: TextDecoration | PluginAPI['mixed'] 709 | letterSpacing: LetterSpacing | PluginAPI['mixed'] 710 | lineHeight: LineHeight | PluginAPI['mixed'] 711 | 712 | getRangeFontSize(start: number, end: number): number | PluginAPI['mixed'] 713 | setRangeFontSize(start: number, end: number, value: number): void 714 | getRangeFontName(start: number, end: number): FontName | PluginAPI['mixed'] 715 | setRangeFontName(start: number, end: number, value: FontName): void 716 | getRangeTextCase(start: number, end: number): TextCase | PluginAPI['mixed'] 717 | setRangeTextCase(start: number, end: number, value: TextCase): void 718 | getRangeTextDecoration(start: number, end: number): TextDecoration | PluginAPI['mixed'] 719 | setRangeTextDecoration(start: number, end: number, value: TextDecoration): void 720 | getRangeLetterSpacing(start: number, end: number): LetterSpacing | PluginAPI['mixed'] 721 | setRangeLetterSpacing(start: number, end: number, value: LetterSpacing): void 722 | getRangeLineHeight(start: number, end: number): LineHeight | PluginAPI['mixed'] 723 | setRangeLineHeight(start: number, end: number, value: LineHeight): void 724 | getRangeFills(start: number, end: number): Paint[] | PluginAPI['mixed'] 725 | setRangeFills(start: number, end: number, value: Paint[]): void 726 | getRangeTextStyleId(start: number, end: number): string | PluginAPI['mixed'] 727 | setRangeTextStyleId(start: number, end: number, value: string): void 728 | getRangeFillStyleId(start: number, end: number): string | PluginAPI['mixed'] 729 | setRangeFillStyleId(start: number, end: number, value: string): void 730 | } 731 | 732 | interface ComponentNode extends DefaultFrameMixin { 733 | readonly type: "COMPONENT" 734 | clone(): ComponentNode 735 | 736 | createInstance(): InstanceNode 737 | description: string 738 | readonly remote: boolean 739 | readonly key: string // The key to use with "importComponentByKeyAsync" 740 | } 741 | 742 | interface InstanceNode extends DefaultFrameMixin { 743 | readonly type: "INSTANCE" 744 | clone(): InstanceNode 745 | masterComponent: ComponentNode 746 | scaleFactor: number 747 | } 748 | 749 | interface BooleanOperationNode extends DefaultShapeMixin, ChildrenMixin, CornerMixin { 750 | readonly type: "BOOLEAN_OPERATION" 751 | clone(): BooleanOperationNode 752 | booleanOperation: "UNION" | "INTERSECT" | "SUBTRACT" | "EXCLUDE" 753 | 754 | expanded: boolean 755 | } 756 | 757 | type BaseNode = 758 | DocumentNode | 759 | PageNode | 760 | SceneNode 761 | 762 | type SceneNode = 763 | SliceNode | 764 | FrameNode | 765 | GroupNode | 766 | ComponentNode | 767 | InstanceNode | 768 | BooleanOperationNode | 769 | VectorNode | 770 | StarNode | 771 | LineNode | 772 | EllipseNode | 773 | PolygonNode | 774 | RectangleNode | 775 | TextNode 776 | 777 | type NodeType = 778 | "DOCUMENT" | 779 | "PAGE" | 780 | "SLICE" | 781 | "FRAME" | 782 | "GROUP" | 783 | "COMPONENT" | 784 | "INSTANCE" | 785 | "BOOLEAN_OPERATION" | 786 | "VECTOR" | 787 | "STAR" | 788 | "LINE" | 789 | "ELLIPSE" | 790 | "POLYGON" | 791 | "RECTANGLE" | 792 | "TEXT" 793 | 794 | //////////////////////////////////////////////////////////////////////////////// 795 | // Styles 796 | type StyleType = "PAINT" | "TEXT" | "EFFECT" | "GRID" 797 | 798 | interface BaseStyle { 799 | readonly id: string 800 | readonly type: StyleType 801 | name: string 802 | description: string 803 | remote: boolean 804 | readonly key: string // The key to use with "importStyleByKeyAsync" 805 | remove(): void 806 | } 807 | 808 | interface PaintStyle extends BaseStyle { 809 | type: "PAINT" 810 | paints: ReadonlyArray 811 | } 812 | 813 | interface TextStyle extends BaseStyle { 814 | type: "TEXT" 815 | fontSize: number 816 | textDecoration: TextDecoration 817 | fontName: FontName 818 | letterSpacing: LetterSpacing 819 | lineHeight: LineHeight 820 | paragraphIndent: number 821 | paragraphSpacing: number 822 | textCase: TextCase 823 | } 824 | 825 | interface EffectStyle extends BaseStyle { 826 | type: "EFFECT" 827 | effects: ReadonlyArray 828 | } 829 | 830 | interface GridStyle extends BaseStyle { 831 | type: "GRID" 832 | layoutGrids: ReadonlyArray 833 | } 834 | 835 | //////////////////////////////////////////////////////////////////////////////// 836 | // Other 837 | 838 | interface Image { 839 | readonly hash: string 840 | getBytesAsync(): Promise 841 | } 842 | } // declare global 843 | 844 | export {} -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Pattern Hero", 3 | "id": "740556241021336678", 4 | "api": "1.0.0", 5 | "main": "dist/code.js", 6 | "ui": "dist/ui.html", 7 | "relaunchButtons": [ 8 | { 9 | "command": "openPlugin", 10 | "name": "Pattern Hero", 11 | "multipleSelection": true 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "figma-pattern-hero", 3 | "version": "1.2.2", 4 | "description": "Create patters, textures, grides seamlessly", 5 | "main": "code.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "figma", 11 | "grid", 12 | "maker", 13 | "plugin", 14 | "pattern hero" 15 | ], 16 | "author": "nitinrgupta", 17 | "license": "ISC", 18 | "devDependencies": { 19 | "css-loader": "^3.2.0", 20 | "html-webpack-inline-source-plugin": "0.0.10", 21 | "html-webpack-plugin": "^3.2.0", 22 | "style-loader": "^1.0.0", 23 | "url-loader": "^2.1.0", 24 | "vue-loader": "^15.7.1", 25 | "vue-template-compiler": "^2.6.10", 26 | "webpack": "^4.39.1", 27 | "webpack-cli": "^3.3.6" 28 | }, 29 | "dependencies": { 30 | "vue": "^2.6.10" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/code.js: -------------------------------------------------------------------------------- 1 | figma.showUI(__html__, { width: 230, height: 390 }); 2 | 3 | const key = 'SETTINGS'; 4 | 5 | figma.ui.onmessage = msg => { 6 | if (msg.type === 'INIT_PLUGIN') { 7 | figma.clientStorage.getAsync(key).then(settings => { 8 | figma.ui.postMessage({ data: settings, type: 'SETTINGS' }) 9 | }); 10 | } 11 | 12 | if (msg.type === 'CREATE_GRID') { 13 | const options = msg.options; 14 | const totalGridLength = options.rows * options.cols; 15 | 16 | // Get selection on current page 17 | let { selection } = figma.currentPage; 18 | 19 | if (!(selection.length > 0)) { 20 | console.log('no selection'); 21 | figma.notify('Please select atleast one node') 22 | figma.ui.postMessage({ data: {}, type: 'ERROR_EMPTY_SELECTION' }) 23 | return; 24 | } 25 | 26 | console.log(options); 27 | 28 | // Getting the position of the first selected node 29 | let x = figma.currentPage.selection[0].x * 2; 30 | let y = figma.currentPage.selection[0].y; 31 | let initialPosition = x; 32 | 33 | let parentNode = figma.currentPage.selection[0].parent; 34 | 35 | 36 | // Creating copy of selected items for repeating and shuffling 37 | let copies = []; 38 | selection.forEach(node => { 39 | if (node.type === 'COMPONENT') { 40 | copies.push(node.createInstance()) 41 | } else { 42 | copies.push(node.clone()) 43 | } 44 | }); 45 | 46 | 47 | // Do the repeat if the options is enabled 48 | if (options.repeat) { 49 | //caching the length 50 | const l = selection.length; 51 | while (copies.length < totalGridLength) { 52 | for(let i = 0; i < l; i++) { 53 | if (copies.length < totalGridLength) { 54 | if (selection[i].type === 'COMPONENT') { 55 | copies.push(selection[i].createInstance()); 56 | } else { 57 | copies.push(selection[i].clone()); 58 | } 59 | } 60 | } 61 | } 62 | } 63 | 64 | // Do the shuffling if the options is enabled 65 | let shuffledCopies = [] 66 | if (options.shuffle) { 67 | shuffledCopies = shuffleArray(copies); 68 | shuffledCopies.forEach(item => { 69 | figma.currentPage.appendChild(item); 70 | }) 71 | } 72 | 73 | 74 | // removing the previous selection 75 | selection.forEach(node => { 76 | // Not removing the original master component 77 | if (node.type === "COMPONENT") { 78 | return; 79 | } 80 | node.remove(); 81 | }); 82 | 83 | 84 | // Replacing the selection array with appropriate ones 85 | selection = options.shuffle ? shuffledCopies : copies; 86 | 87 | 88 | // Caching the length and counters 89 | let selectionCounter = 0; 90 | let selectionLength = selection.length; 91 | 92 | // Placing into a grid logic 93 | for (let i = 0; i < options.rows; i++) { 94 | if (selectionCounter < selectionLength) { 95 | for(let j = 0; j < options.cols; j++) { 96 | if (selectionCounter < selectionLength) { 97 | selection[selectionCounter].x = x; 98 | selection[selectionCounter].y = y; 99 | x = x + selection[selectionCounter].width + options.padding 100 | selectionCounter++; 101 | } 102 | } 103 | x = initialPosition; 104 | y = y + selection[i].height + options.padding 105 | } 106 | } 107 | 108 | 109 | const nodes = []; 110 | 111 | if (options.group) { 112 | //Creating a group with the selection 113 | const group = figma.group(selection, selection[0].parent); 114 | group.name = 'Pattern'; 115 | 116 | //Appending it to the parent of selection 117 | parentNode.appendChild(group); 118 | figma.currentPage.selection = [group]; 119 | nodes.push(group); 120 | group.setRelaunchData({ openPlugin: "Click to create more patterns" }) 121 | } else { 122 | //Appending all the selection to the parent 123 | for (const node of selection) { 124 | parentNode.appendChild(node); 125 | node.setRelaunchData({ openPlugin: "Click to create more patterns" }) 126 | } 127 | figma.currentPage.selection = selection; 128 | nodes.push(selection[0]); 129 | 130 | } 131 | 132 | //Saving user settings 133 | saveSettings(options); 134 | 135 | nodes.push(selection[0].parent); 136 | figma.viewport.scrollAndZoomIntoView(nodes); 137 | figma.ui.postMessage({ data: {}, type: 'DONE_LOADING' }) 138 | } 139 | } 140 | 141 | const saveSettings = options => { 142 | figma.clientStorage.setAsync(key, options).then(() => {}); 143 | } 144 | 145 | // Using Javascript implementation of Durstenfeld shuffle 146 | const shuffleArray = (array) => { 147 | let newArray = Array.from(array); 148 | for (let i = newArray.length - 1; i > 0; i--) { 149 | const j = Math.floor(Math.random() * (i + 1)); 150 | let temp = newArray[i]; 151 | newArray[i] = newArray[j]; 152 | newArray[j] = temp; 153 | } 154 | return newArray; 155 | } 156 | -------------------------------------------------------------------------------- /src/ui.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Inter", sans-serif; 3 | font-size: 12px; 4 | text-align: left; 5 | margin: 0; 6 | color: #191919; 7 | letter-spacing: 0.005em; 8 | overflow: hidden; 9 | } 10 | p { 11 | text-align: left; 12 | font-family: "Inter", sans-serif; 13 | font-size: 12px; 14 | letter-spacing: 0.005em; 15 | } 16 | .container { 17 | margin: 20px; 18 | } 19 | button { 20 | border-radius: 5px; 21 | background: white; 22 | color: black; 23 | border: none; 24 | padding: 8px 15px; 25 | box-shadow: inset 0 0 0 1px black; 26 | outline: none; 27 | width: 100%; 28 | cursor: pointer; 29 | } 30 | #create { 31 | box-shadow: none; 32 | background: #18A0FB; 33 | color: white; 34 | margin-top: 30px; 35 | height: 30px; 36 | display: flex; 37 | justify-content: center; 38 | align-items: center; 39 | } 40 | input { 41 | border: none; 42 | outline: none; 43 | padding: 8px; 44 | } 45 | input:hover { box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); } 46 | button:focus { box-shadow: inset 0 0 0 2px #18A0FB; } 47 | #create:focus { box-shadow: inset 0 0 0 2px rgba(0, 0, 0, 0.3); } 48 | input:focus { box-shadow: inset 0 0 0 2px #18A0FB; } 49 | 50 | 51 | .switch-container { 52 | position: relative; 53 | width: 24px; 54 | height: 12px; 55 | margin: 10px 0; 56 | } 57 | 58 | .switch-outer-container { 59 | width: 38px; 60 | } 61 | 62 | .switch-checkbox { 63 | width: 0; 64 | height: 0; 65 | opacity: 0; 66 | } 67 | 68 | .switch-checkbox:checked + .switch-slider { 69 | background-color: #000000; 70 | } 71 | 72 | .switch-checkbox:focus + .switch-slider { 73 | -webkit-box-shadow: 0 0 1px #2196f3; 74 | box-shadow: 0 0 1px #2196f3; 75 | } 76 | 77 | .switch-checkbox:checked + .switch-slider:before { 78 | -webkit-transform: translateX(12px); 79 | transform: translateX(12px); 80 | } 81 | 82 | .switch-slider { 83 | position: absolute; 84 | top: 0; 85 | right: 0; 86 | bottom: 0; 87 | left: 0; 88 | -webkit-transition: -webkit-transform .2s; 89 | transition: -webkit-transform .2s; 90 | transition: transform .2s; 91 | transition: transform .2s, -webkit-transform .2s; 92 | -webkit-transition: background-color 0 .2s; 93 | transition: background-color 0 .2s; 94 | border: 1px solid #000000; 95 | border-radius: 12px; 96 | background-color: #ffffff; 97 | } 98 | 99 | .switch-slider::before { 100 | position: absolute; 101 | top: -1px; 102 | left: -1px; 103 | width: 10px; 104 | height: 10px; 105 | content: ''; 106 | -webkit-transition: -webkit-transform .2s; 107 | transition: -webkit-transform .2s; 108 | transition: transform .2s; 109 | transition: transform .2s, -webkit-transform .2s; 110 | -webkit-transition: background-color 0 .2s; 111 | transition: background-color 0 .2s; 112 | border: 1px solid #000000; 113 | border-radius: 50%; 114 | background-color: white; 115 | } 116 | 117 | .options-row { 118 | display: flex; 119 | justify-content: space-between; 120 | } 121 | .options-row-container { 122 | display: flex; 123 | flex-direction: row; 124 | align-items: center; 125 | margin: 10px 0; 126 | justify-content: space-between; 127 | } 128 | .options-row-inner-container { 129 | display: flex; 130 | align-items: center; 131 | } 132 | .options-row-container p { 133 | margin: 0; 134 | } 135 | .options-row-container svg { 136 | margin-right: 10px; 137 | opacity: 0.4; 138 | width: 15px; 139 | } 140 | .container input { 141 | width: 45px; 142 | } 143 | 144 | .banner img { 145 | width: 100%; 146 | border-bottom: 1px solid #ececec; 147 | } 148 | 149 | .help-text { 150 | text-align: left; 151 | padding: 10px 20px; 152 | font-family: "Inter", sans-serif; 153 | font-size: 11px; 154 | margin: 0; 155 | background: #f7f7f7; 156 | border-bottom: 1px solid #ececec; 157 | } 158 | 159 | .error { 160 | position: absolute; 161 | width: 82%; 162 | text-align: center; 163 | color: #F24822; 164 | margin: 0; 165 | margin-bottom: 5px; 166 | } 167 | .fade-enter-active, .fade-leave-active { 168 | transition: opacity .5s; 169 | } 170 | .fade-enter, .fade-leave-to { 171 | opacity: 0; 172 | } 173 | .loading { 174 | display: inline-block; 175 | width: 15px; 176 | height: 15px; 177 | } 178 | .loading:after { 179 | content: " "; 180 | display: block; 181 | width: 13px; 182 | height: 13px; 183 | border-radius: 50%; 184 | border: 1px solid #fff; 185 | border-color: #fff transparent #fff transparent; 186 | animation: circle-loader 1.2s linear infinite; 187 | } 188 | @keyframes circle-loader { 189 | 0% { 190 | transform: rotate(0deg); 191 | } 192 | 100% { 193 | transform: rotate(360deg); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/ui.html: -------------------------------------------------------------------------------- 1 |
2 |

Select one or more elements and choose the appropriate options below to create grid or a pattern

3 |
4 |
5 |
6 | 7 | 8 | 9 | 10 |

Rows

11 |
12 | 13 |
14 |
15 |
16 | 17 | 18 | 19 | 20 |

Columns

21 |
22 | 23 |
24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 |

Padding

32 |
33 | 34 |
35 | 50 | 65 | 79 | 83 |
84 |
85 | -------------------------------------------------------------------------------- /src/ui.js: -------------------------------------------------------------------------------- 1 | import './ui.css' 2 | import Vue from "vue"; 3 | 4 | let app = new Vue({ 5 | el: '#app', 6 | data: { 7 | config: { 8 | rows: 10, 9 | cols: 10, 10 | padding: 5, 11 | shuffle: false, 12 | repeat: true, 13 | group: true, 14 | }, 15 | loading: false, 16 | }, 17 | mounted() { 18 | // Initing plugin for saved settings 19 | parent.postMessage({ pluginMessage: { type: 'INIT_PLUGIN' } }, '*') 20 | }, 21 | methods: { 22 | createPattern: function() { 23 | if (this.loading) return; 24 | 25 | this.loading = true; 26 | 27 | //Adding timeout to show the loader in button 28 | setTimeout(() => { 29 | parent.postMessage({ pluginMessage: { type: 'CREATE_GRID', options: this.config } }, '*'); 30 | },100); 31 | } 32 | } 33 | }) 34 | 35 | // Function to recieve events from figma 36 | onmessage = e => { 37 | if (!e.data) return; 38 | 39 | const data = e.data.pluginMessage.data; 40 | const type = e.data.pluginMessage.type; 41 | 42 | if (type === 'SETTINGS') { 43 | if (data) { 44 | Object.assign(app.config, data); 45 | } 46 | } 47 | if (type === 'DONE_LOADING') { 48 | app.loading = false; 49 | } 50 | if (type === 'ERROR_EMPTY_SELECTION') { 51 | app.loading = false; 52 | } 53 | }; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 4 | const path = require('path') 5 | 6 | module.exports = (env, argv) => ({ 7 | mode: argv.mode === 'production' ? 'production' : 'development', 8 | 9 | // This is necessary because Figma's 'eval' works differently than normal eval 10 | devtool: argv.mode === 'production' ? false : 'inline-source-map', 11 | 12 | entry: { 13 | ui: './src/ui.js', // The entry point for your UI code 14 | code: './src/code.js', // The entry point for your plugin code 15 | }, 16 | 17 | module: { 18 | rules: [ 19 | // Enables vue loader 20 | { test: /\.vue$/, loader: 'vue-loader' }, 21 | 22 | // Enables including CSS by doing "import './file.css'" in your TypeScript code 23 | { test: /\.css$/, loader: [{ loader: 'style-loader' }, { loader: 'css-loader' }] }, 24 | 25 | // Allows you to use "<%= require('./file.svg') %>" in your HTML code to get a data URI 26 | { test: /\.(png|jpg|gif|webp|svg)$/, loader: [{ loader: 'url-loader' }] }, 27 | ], 28 | }, 29 | 30 | // Webpack tries these extensions for you if you omit the extension like "import './file'" 31 | resolve: { 32 | extensions: ['.tsx', '.ts', '.jsx', '.js', '.vue'], 33 | alias: { vue: argv.mode === 'production' ? 'vue/dist/vue.min' : 'vue/dist/vue.js' } 34 | }, 35 | 36 | output: { 37 | filename: '[name].js', 38 | path: path.resolve(__dirname, 'dist'), // Compile into a folder called "dist" 39 | }, 40 | 41 | // Tells Webpack to generate "ui.html" and to inline "ui.ts" into it 42 | plugins: [ 43 | new VueLoaderPlugin(), 44 | new HtmlWebpackPlugin({ 45 | template: './src/ui.html', 46 | filename: 'ui.html', 47 | inlineSource: '.(js)$', 48 | chunks: ['ui'], 49 | }), 50 | new HtmlWebpackInlineSourcePlugin(), 51 | ], 52 | }) --------------------------------------------------------------------------------