├── .gitignore ├── README.md ├── assets ├── avatar.png └── cover.png ├── figma.d.ts ├── figplug.d.ts ├── manifest.json ├── package-lock.json ├── package.json ├── src ├── createAndPlace.ts ├── plugin.ts └── sizes.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .DS_Store 3 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Figma Responsify](./assets/cover.png) 2 | 3 | # Figma Responsify ⚡️ 4 | 5 | A Figma plugin to quickly test your designs across multiple device sizes. 6 | 7 | [Install on Figma](https://www.figma.com/c/plugin/743654854885744527/Responsify-%E2%9A%A1%EF%B8%8F) 8 | 9 | ## Usage 10 | 11 | 1. Select any frame, component, or instance 12 | 2. Menu > Responsify️️ ⚡️ > Select device sizes to test 13 | 3. Command + Shift + P to re-run 14 | 15 | ## Installation and contributing 16 | 17 | 1. Clone the repository: `git clone https://github.com/brianlovin/figma-responsify.git` 18 | 1. Install [figplug](https://github.com/rsms/figplug) dependancy: `npm install -g figplug` 19 | 1. Go to the directory: `cd figma-responsify` 20 | 1. Build the plugin: `figplug build -w -o=./build` 21 | 1. Go to the `plugins` directory in Figma 22 | 1. Add a new development plugin 23 | 1. Select the `figma-responsify/build/manifest.json` file as the manifest 24 | -------------------------------------------------------------------------------- /assets/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianlovin/figma-responsify/abd1bc81c57139ab28fe505b8eb44e607dfb6750/assets/avatar.png -------------------------------------------------------------------------------- /assets/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianlovin/figma-responsify/abd1bc81c57139ab28fe505b8eb44e607dfb6750/assets/cover.png -------------------------------------------------------------------------------- /figma.d.ts: -------------------------------------------------------------------------------- 1 | // Global variable with Figma's plugin API. 2 | declare const figma: PluginAPI 3 | declare const __html__: string 4 | 5 | interface PluginAPI { 6 | readonly apiVersion: "1.0.0" 7 | readonly command: string 8 | readonly root: DocumentNode 9 | readonly viewport: ViewportAPI 10 | closePlugin(message?: string): void 11 | 12 | showUI(html: string, options?: ShowUIOptions): void 13 | readonly ui: UIAPI 14 | 15 | readonly clientStorage: ClientStorageAPI 16 | 17 | getNodeById(id: string): BaseNode | null 18 | getStyleById(id: string): BaseStyle | null 19 | 20 | currentPage: PageNode 21 | 22 | readonly mixed: symbol 23 | 24 | createRectangle(): RectangleNode 25 | createLine(): LineNode 26 | createEllipse(): EllipseNode 27 | createPolygon(): PolygonNode 28 | createStar(): StarNode 29 | createVector(): VectorNode 30 | createText(): TextNode 31 | createBooleanOperation(): BooleanOperationNode 32 | createFrame(): FrameNode 33 | createComponent(): ComponentNode 34 | createPage(): PageNode 35 | createSlice(): SliceNode 36 | 37 | createPaintStyle(): PaintStyle 38 | createTextStyle(): TextStyle 39 | createEffectStyle(): EffectStyle 40 | createGridStyle(): GridStyle 41 | 42 | importComponentByKeyAsync(key: string): Promise 43 | importStyleByKeyAsync(key: string): Promise 44 | 45 | listAvailableFontsAsync(): Promise 46 | loadFontAsync(fontName: FontName): Promise 47 | readonly hasMissingFont: boolean 48 | 49 | createNodeFromSvg(svg: string): FrameNode 50 | 51 | createImage(data: Uint8Array): Image 52 | getImageByHash(hash: string): Image 53 | 54 | group(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): FrameNode 55 | flatten(nodes: ReadonlyArray, parent?: BaseNode & ChildrenMixin, index?: number): VectorNode 56 | } 57 | 58 | interface ClientStorageAPI { 59 | getAsync(key: string): Promise 60 | setAsync(key: string, value: any): Promise 61 | } 62 | 63 | type ShowUIOptions = { 64 | visible?: boolean, 65 | width?: number, 66 | height?: number, 67 | } 68 | 69 | type UIPostMessageOptions = { 70 | targetOrigin?: string, 71 | } 72 | 73 | type OnMessageProperties = { 74 | sourceOrigin: string, 75 | } 76 | 77 | interface UIAPI { 78 | show(): void 79 | hide(): void 80 | resize(width: number, height: number): void 81 | close(): void 82 | 83 | postMessage(pluginMessage: any, options?: UIPostMessageOptions): void 84 | onmessage: ((pluginMessage: any, props: OnMessageProperties) => void) | undefined 85 | } 86 | 87 | interface ViewportAPI { 88 | center: { x: number, y: number } 89 | zoom: number 90 | scrollAndZoomIntoView(nodes: ReadonlyArray) 91 | } 92 | 93 | //////////////////////////////////////////////////////////////////////////////// 94 | // Datatypes 95 | 96 | type Transform = [ 97 | [number, number, number], 98 | [number, number, number] 99 | ] 100 | 101 | interface Vector { 102 | readonly x: number 103 | readonly y: number 104 | } 105 | 106 | interface RGB { 107 | readonly r: number 108 | readonly g: number 109 | readonly b: number 110 | } 111 | 112 | interface RGBA { 113 | readonly r: number 114 | readonly g: number 115 | readonly b: number 116 | readonly a: number 117 | } 118 | 119 | interface FontName { 120 | readonly family: string 121 | readonly style: string 122 | } 123 | 124 | type TextCase = "ORIGINAL" | "UPPER" | "LOWER" | "TITLE" 125 | 126 | type TextDecoration = "NONE" | "UNDERLINE" | "STRIKETHROUGH" 127 | 128 | interface ArcData { 129 | readonly startingAngle: number 130 | readonly endingAngle: number 131 | readonly innerRadius: number 132 | } 133 | 134 | interface ShadowEffect { 135 | readonly type: "DROP_SHADOW" | "INNER_SHADOW" 136 | readonly color: RGBA 137 | readonly offset: Vector 138 | readonly radius: number 139 | readonly visible: boolean 140 | readonly blendMode: BlendMode 141 | } 142 | 143 | interface BlurEffect { 144 | readonly type: "LAYER_BLUR" | "BACKGROUND_BLUR" 145 | readonly radius: number 146 | readonly visible: boolean 147 | } 148 | 149 | type Effect = ShadowEffect | BlurEffect 150 | 151 | type ConstraintType = "MIN" | "CENTER" | "MAX" | "STRETCH" | "SCALE" 152 | 153 | interface Constraints { 154 | readonly horizontal: ConstraintType 155 | readonly vertical: ConstraintType 156 | } 157 | 158 | interface ColorStop { 159 | readonly position: number 160 | readonly color: RGBA 161 | } 162 | 163 | interface ImageFilters { 164 | exposure?: number 165 | contrast?: number 166 | saturation?: number 167 | temperature?: number 168 | tint?: number 169 | highlights?: number 170 | shadows?: number 171 | } 172 | 173 | interface SolidPaint { 174 | readonly type: "SOLID" 175 | readonly color: RGB 176 | 177 | readonly visible?: boolean 178 | readonly opacity?: number 179 | readonly blendMode?: BlendMode 180 | } 181 | 182 | interface GradientPaint { 183 | readonly type: "GRADIENT_LINEAR" | "GRADIENT_RADIAL" | "GRADIENT_ANGULAR" | "GRADIENT_DIAMOND" 184 | readonly gradientTransform: Transform 185 | readonly gradientStops: ReadonlyArray 186 | 187 | readonly visible?: boolean 188 | readonly opacity?: number 189 | readonly blendMode?: BlendMode 190 | } 191 | 192 | interface ImagePaint { 193 | readonly type: "IMAGE" 194 | readonly scaleMode: "FILL" | "FIT" | "CROP" | "TILE" 195 | readonly imageHash: string | null 196 | readonly imageTransform?: Transform // setting for "CROP" 197 | readonly scalingFactor?: number // setting for "TILE" 198 | readonly filters?: ImageFilters 199 | 200 | readonly visible?: boolean 201 | readonly opacity?: number 202 | readonly blendMode?: BlendMode 203 | } 204 | 205 | type Paint = SolidPaint | GradientPaint | ImagePaint 206 | 207 | interface Guide { 208 | readonly axis: "X" | "Y" 209 | readonly offset: number 210 | } 211 | 212 | interface RowsColsLayoutGrid { 213 | readonly pattern: "ROWS" | "COLUMNS" 214 | readonly alignment: "MIN" | "MAX" | "STRETCH" | "CENTER" 215 | readonly gutterSize: number 216 | 217 | readonly count: number // Infinity when "Auto" is set in the UI 218 | readonly sectionSize?: number // Not set for alignment: "STRETCH" 219 | readonly offset?: number // Not set for alignment: "CENTER" 220 | 221 | readonly visible?: boolean 222 | readonly color?: RGBA 223 | } 224 | 225 | interface GridLayoutGrid { 226 | readonly pattern: "GRID" 227 | readonly sectionSize: number 228 | 229 | readonly visible?: boolean 230 | readonly color?: RGBA 231 | } 232 | 233 | type LayoutGrid = RowsColsLayoutGrid | GridLayoutGrid 234 | 235 | interface ExportSettingsConstraints { 236 | type: "SCALE" | "WIDTH" | "HEIGHT" 237 | value: number 238 | } 239 | 240 | interface ExportSettingsImage { 241 | format: "JPG" | "PNG" 242 | contentsOnly?: boolean // defaults to true 243 | suffix?: string 244 | constraint?: ExportSettingsConstraints 245 | } 246 | 247 | interface ExportSettingsSVG { 248 | format: "SVG" 249 | contentsOnly?: boolean // defaults to true 250 | suffix?: string 251 | svgOutlineText?: boolean // defaults to true 252 | svgIdAttribute?: boolean // defaults to false 253 | svgSimplifyStroke?: boolean // defaults to true 254 | } 255 | 256 | interface ExportSettingsPDF { 257 | format: "PDF" 258 | contentsOnly?: boolean // defaults to true 259 | suffix?: string 260 | } 261 | 262 | type ExportSettings = ExportSettingsImage | ExportSettingsSVG | ExportSettingsPDF 263 | 264 | type WindingRule = "NONZERO" | "EVENODD" 265 | 266 | interface VectorVertex { 267 | readonly x: number 268 | readonly y: number 269 | readonly strokeCap?: StrokeCap 270 | readonly strokeJoin?: StrokeJoin 271 | readonly cornerRadius?: number 272 | readonly handleMirroring?: HandleMirroring 273 | } 274 | 275 | interface VectorSegment { 276 | readonly start: number 277 | readonly end: number 278 | readonly tangentStart?: Vector // Defaults to { x: 0, y: 0 } 279 | readonly tangentEnd?: Vector // Defaults to { x: 0, y: 0 } 280 | } 281 | 282 | interface VectorRegion { 283 | readonly windingRule: WindingRule 284 | readonly loops: ReadonlyArray> 285 | } 286 | 287 | interface VectorNetwork { 288 | readonly vertices: ReadonlyArray 289 | readonly segments: ReadonlyArray 290 | readonly regions?: ReadonlyArray // Defaults to [] 291 | } 292 | 293 | interface VectorPath { 294 | readonly windingRule: WindingRule | "NONE" 295 | readonly data: string 296 | } 297 | 298 | type VectorPaths = ReadonlyArray 299 | 300 | type LetterSpacing = { 301 | readonly value: number 302 | readonly unit: "PIXELS" | "PERCENT" 303 | } 304 | 305 | type LineHeight = { 306 | readonly value: number 307 | readonly unit: "PIXELS" | "PERCENT" 308 | } | { 309 | readonly unit: "AUTO" 310 | } 311 | 312 | type BlendMode = 313 | "PASS_THROUGH" | 314 | "NORMAL" | 315 | "DARKEN" | 316 | "MULTIPLY" | 317 | "LINEAR_BURN" | 318 | "COLOR_BURN" | 319 | "LIGHTEN" | 320 | "SCREEN" | 321 | "LINEAR_DODGE" | 322 | "COLOR_DODGE" | 323 | "OVERLAY" | 324 | "SOFT_LIGHT" | 325 | "HARD_LIGHT" | 326 | "DIFFERENCE" | 327 | "EXCLUSION" | 328 | "HUE" | 329 | "SATURATION" | 330 | "COLOR" | 331 | "LUMINOSITY" 332 | 333 | interface Font { 334 | fontName: FontName 335 | } 336 | 337 | //////////////////////////////////////////////////////////////////////////////// 338 | // Mixins 339 | 340 | interface BaseNodeMixin { 341 | readonly id: string 342 | readonly parent: (BaseNode & ChildrenMixin) | null 343 | name: string // Note: setting this also sets `autoRename` to false on TextNodes 344 | readonly removed: boolean 345 | toString(): string 346 | remove(): void 347 | 348 | getPluginData(key: string): string 349 | setPluginData(key: string, value: string): void 350 | 351 | // Namespace is a string that must be at least 3 alphanumeric characters, and should 352 | // be a name related to your plugin. Other plugins will be able to read this data. 353 | getSharedPluginData(namespace: string, key: string): string 354 | setSharedPluginData(namespace: string, key: string, value: string): void 355 | } 356 | 357 | interface SceneNodeMixin { 358 | visible: boolean 359 | locked: boolean 360 | } 361 | 362 | interface ChildrenMixin { 363 | readonly children: ReadonlyArray 364 | 365 | appendChild(child: BaseNode): void 366 | insertChild(index: number, child: BaseNode): void 367 | 368 | findAll(callback?: (node: BaseNode) => boolean): ReadonlyArray 369 | findOne(callback: (node: BaseNode) => boolean): BaseNode | null 370 | } 371 | 372 | interface ConstraintMixin { 373 | constraints: Constraints 374 | } 375 | 376 | interface LayoutMixin { 377 | readonly absoluteTransform: Transform 378 | relativeTransform: Transform 379 | x: number 380 | y: number 381 | rotation: number // In degrees 382 | 383 | readonly width: number 384 | readonly height: number 385 | 386 | resize(width: number, height: number): void 387 | resizeWithoutConstraints(width: number, height: number): void 388 | } 389 | 390 | interface BlendMixin { 391 | opacity: number 392 | blendMode: BlendMode 393 | isMask: boolean 394 | effects: ReadonlyArray 395 | effectStyleId: string 396 | } 397 | 398 | interface FrameMixin { 399 | backgrounds: ReadonlyArray 400 | layoutGrids: ReadonlyArray 401 | clipsContent: boolean 402 | guides: ReadonlyArray 403 | gridStyleId: string 404 | backgroundStyleId: string 405 | } 406 | 407 | type StrokeCap = "NONE" | "ROUND" | "SQUARE" | "ARROW_LINES" | "ARROW_EQUILATERAL" 408 | type StrokeJoin = "MITER" | "BEVEL" | "ROUND" 409 | type HandleMirroring = "NONE" | "ANGLE" | "ANGLE_AND_LENGTH" 410 | 411 | interface GeometryMixin { 412 | fills: ReadonlyArray | symbol 413 | strokes: ReadonlyArray 414 | strokeWeight: number 415 | strokeAlign: "CENTER" | "INSIDE" | "OUTSIDE" 416 | strokeCap: StrokeCap | symbol 417 | strokeJoin: StrokeJoin | symbol 418 | dashPattern: ReadonlyArray 419 | fillStyleId: string | symbol 420 | strokeStyleId: string 421 | } 422 | 423 | interface CornerMixin { 424 | cornerRadius: number | symbol 425 | cornerSmoothing: number 426 | } 427 | 428 | interface ExportMixin { 429 | exportSettings: ExportSettings[] 430 | exportAsync(settings?: ExportSettings): Promise // Defaults to PNG format 431 | } 432 | 433 | interface DefaultShapeMixin extends 434 | BaseNodeMixin, SceneNodeMixin, 435 | BlendMixin, GeometryMixin, LayoutMixin, ExportMixin { 436 | } 437 | 438 | interface DefaultContainerMixin extends 439 | BaseNodeMixin, SceneNodeMixin, 440 | ChildrenMixin, FrameMixin, 441 | BlendMixin, ConstraintMixin, LayoutMixin, ExportMixin { 442 | } 443 | 444 | //////////////////////////////////////////////////////////////////////////////// 445 | // Nodes 446 | 447 | interface DocumentNode extends BaseNodeMixin, ChildrenMixin { 448 | readonly type: "DOCUMENT" 449 | } 450 | 451 | interface PageNode extends BaseNodeMixin, ChildrenMixin, ExportMixin { 452 | readonly type: "PAGE" 453 | clone(): PageNode 454 | 455 | guides: ReadonlyArray 456 | selection: ReadonlyArray 457 | } 458 | 459 | interface FrameNode extends DefaultContainerMixin { 460 | readonly type: "FRAME" | "GROUP" 461 | clone(): FrameNode 462 | } 463 | 464 | interface SliceNode extends BaseNodeMixin, SceneNodeMixin, LayoutMixin, ExportMixin { 465 | readonly type: "SLICE" 466 | clone(): SliceNode 467 | } 468 | 469 | interface RectangleNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 470 | readonly type: "RECTANGLE" 471 | clone(): RectangleNode 472 | topLeftRadius: number 473 | topRightRadius: number 474 | bottomLeftRadius: number 475 | bottomRightRadius: number 476 | } 477 | 478 | interface LineNode extends DefaultShapeMixin, ConstraintMixin { 479 | readonly type: "LINE" 480 | clone(): LineNode 481 | } 482 | 483 | interface EllipseNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 484 | readonly type: "ELLIPSE" 485 | clone(): EllipseNode 486 | arcData: ArcData 487 | } 488 | 489 | interface PolygonNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 490 | readonly type: "POLYGON" 491 | clone(): PolygonNode 492 | pointCount: number 493 | } 494 | 495 | interface StarNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 496 | readonly type: "STAR" 497 | clone(): StarNode 498 | pointCount: number 499 | innerRadius: number 500 | } 501 | 502 | interface VectorNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 503 | readonly type: "VECTOR" 504 | clone(): VectorNode 505 | vectorNetwork: VectorNetwork 506 | vectorPaths: VectorPaths 507 | handleMirroring: HandleMirroring | symbol 508 | } 509 | 510 | interface TextNode extends DefaultShapeMixin, ConstraintMixin { 511 | readonly type: "TEXT" 512 | clone(): TextNode 513 | characters: string 514 | readonly hasMissingFont: boolean 515 | textAlignHorizontal: "LEFT" | "CENTER" | "RIGHT" | "JUSTIFIED" 516 | textAlignVertical: "TOP" | "CENTER" | "BOTTOM" 517 | textAutoResize: "NONE" | "WIDTH_AND_HEIGHT" | "HEIGHT" 518 | paragraphIndent: number 519 | paragraphSpacing: number 520 | autoRename: boolean 521 | 522 | textStyleId: string | symbol 523 | fontSize: number | symbol 524 | fontName: FontName | symbol 525 | textCase: TextCase | symbol 526 | textDecoration: TextDecoration | symbol 527 | letterSpacing: LetterSpacing | symbol 528 | lineHeight: LineHeight | symbol 529 | 530 | getRangeFontSize(start: number, end: number): number | symbol 531 | setRangeFontSize(start: number, end: number, value: number): void 532 | getRangeFontName(start: number, end: number): FontName | symbol 533 | setRangeFontName(start: number, end: number, value: FontName): void 534 | getRangeTextCase(start: number, end: number): TextCase | symbol 535 | setRangeTextCase(start: number, end: number, value: TextCase): void 536 | getRangeTextDecoration(start: number, end: number): TextDecoration | symbol 537 | setRangeTextDecoration(start: number, end: number, value: TextDecoration): void 538 | getRangeLetterSpacing(start: number, end: number): LetterSpacing | symbol 539 | setRangeLetterSpacing(start: number, end: number, value: LetterSpacing): void 540 | getRangeLineHeight(start: number, end: number): LineHeight | symbol 541 | setRangeLineHeight(start: number, end: number, value: LineHeight): void 542 | getRangeFills(start: number, end: number): Paint[] | symbol 543 | setRangeFills(start: number, end: number, value: Paint[]): void 544 | getRangeTextStyleId(start: number, end: number): string | symbol 545 | setRangeTextStyleId(start: number, end: number, value: string): void 546 | getRangeFillStyleId(start: number, end: number): string | symbol 547 | setRangeFillStyleId(start: number, end: number, value: string): void 548 | } 549 | 550 | interface ComponentNode extends DefaultContainerMixin { 551 | readonly type: "COMPONENT" 552 | clone(): ComponentNode 553 | 554 | createInstance(): InstanceNode 555 | description: string 556 | readonly remote: boolean 557 | readonly key: string // The key to use with "importComponentByKeyAsync" 558 | } 559 | 560 | interface InstanceNode extends DefaultContainerMixin { 561 | readonly type: "INSTANCE" 562 | clone(): InstanceNode 563 | masterComponent: ComponentNode 564 | } 565 | 566 | interface BooleanOperationNode extends DefaultShapeMixin, ChildrenMixin, CornerMixin { 567 | readonly type: "BOOLEAN_OPERATION" 568 | clone(): BooleanOperationNode 569 | booleanOperation: "UNION" | "INTERSECT" | "SUBTRACT" | "EXCLUDE" 570 | } 571 | 572 | type BaseNode = 573 | DocumentNode | 574 | PageNode | 575 | SceneNode 576 | 577 | type SceneNode = 578 | SliceNode | 579 | FrameNode | 580 | ComponentNode | 581 | InstanceNode | 582 | BooleanOperationNode | 583 | VectorNode | 584 | StarNode | 585 | LineNode | 586 | EllipseNode | 587 | PolygonNode | 588 | RectangleNode | 589 | TextNode 590 | 591 | type NodeType = 592 | "DOCUMENT" | 593 | "PAGE" | 594 | "SLICE" | 595 | "FRAME" | 596 | "GROUP" | 597 | "COMPONENT" | 598 | "INSTANCE" | 599 | "BOOLEAN_OPERATION" | 600 | "VECTOR" | 601 | "STAR" | 602 | "LINE" | 603 | "ELLIPSE" | 604 | "POLYGON" | 605 | "RECTANGLE" | 606 | "TEXT" 607 | 608 | //////////////////////////////////////////////////////////////////////////////// 609 | // Styles 610 | type StyleType = "PAINT" | "TEXT" | "EFFECT" | "GRID" 611 | 612 | interface BaseStyle { 613 | readonly id: string 614 | readonly type: StyleType 615 | name: string 616 | description: string 617 | remote: boolean 618 | readonly key: string // The key to use with "importStyleByKeyAsync" 619 | remove(): void 620 | } 621 | 622 | interface PaintStyle extends BaseStyle { 623 | type: "PAINT" 624 | paints: ReadonlyArray 625 | } 626 | 627 | interface TextStyle extends BaseStyle { 628 | type: "TEXT" 629 | fontSize: number 630 | textDecoration: TextDecoration 631 | fontName: FontName 632 | letterSpacing: LetterSpacing 633 | lineHeight: LineHeight 634 | paragraphIndent: number 635 | paragraphSpacing: number 636 | textCase: TextCase 637 | } 638 | 639 | interface EffectStyle extends BaseStyle { 640 | type: "EFFECT" 641 | effects: ReadonlyArray 642 | } 643 | 644 | interface GridStyle extends BaseStyle { 645 | type: "GRID" 646 | layoutGrids: ReadonlyArray 647 | } 648 | 649 | //////////////////////////////////////////////////////////////////////////////// 650 | // Other 651 | 652 | interface Image { 653 | readonly hash: string 654 | getBytesAsync(): Promise 655 | } 656 | -------------------------------------------------------------------------------- /figplug.d.ts: -------------------------------------------------------------------------------- 1 | // Helpers provided automatically, as needed, by figplug. 2 | 3 | // symbolic type aliases 4 | type int = number 5 | type float = number 6 | type byte = number 7 | type bool = boolean 8 | 9 | // compile-time constants 10 | declare const DEBUG :boolean 11 | declare const VERSION :string 12 | 13 | // global namespace. Same as `window` in a regular web context. 14 | declare const global :{[k:string]:any} 15 | 16 | // panic prints a message, stack trace and exits the process 17 | // 18 | declare function panic(msg :any, ...v :any[]) :void 19 | 20 | // repr returns a detailed string representation of the input 21 | // 22 | declare function repr(obj :any) :string 23 | 24 | // print works just like console.log 25 | declare function print(msg :any, ...v :any[]) :void 26 | 27 | // dlog works just like console.log but is stripped out from non-debug builds 28 | declare function dlog(msg :any, ...v :any[]) :void 29 | 30 | // assert checks the condition for truth, and if false, prints an optional 31 | // message, stack trace and exits the process. 32 | // assert is removed in release builds 33 | declare var assert :AssertFun 34 | declare var AssertionError :ErrorConstructor 35 | declare interface AssertFun { 36 | (cond :any, msg? :string, cons? :Function) :void 37 | 38 | // throws can be set to true to cause assertions to be thrown as exceptions, 39 | // or set to false to cause the process to exit. 40 | // Only has an effect in Nodejs-like environments. 41 | // false by default. 42 | throws :bool 43 | } 44 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": "1.0.0", 3 | "id": "743654854885744527", 4 | "name": "Responsify ⚡️", 5 | "main": "src/plugin.ts", 6 | "menu": [ 7 | {"name": "iPhone", "command": "iphone"}, 8 | {"name": "Android", "command": "android"}, 9 | {"name": "Tablet", "command": "tablet"}, 10 | {"name": "Desktop", "command": "desktop"}, 11 | {"name": "Watch", "command": "watch"}, 12 | {"name": "All devices (for whatever reason)", "command": "all"} 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "responsify", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "tslib": { 8 | "version": "1.10.0", 9 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", 10 | "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", 11 | "dev": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "responsify", 3 | "version": "1.0.0", 4 | "description": "![Figma Responsify](./assets/cover.png)", 5 | "main": "index.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "tslib": "^1.10.0" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/brianlovin/figma-responsify.git" 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/brianlovin/figma-responsify/issues" 22 | }, 23 | "homepage": "https://github.com/brianlovin/figma-responsify#readme" 24 | } 25 | -------------------------------------------------------------------------------- /src/createAndPlace.ts: -------------------------------------------------------------------------------- 1 | interface Device { 2 | name: string; 3 | width: number; 4 | height: number 5 | } 6 | 7 | 8 | interface Props { 9 | device: Device; 10 | devices: Device[]; 11 | index: number; 12 | selectedNode: SceneNode 13 | } 14 | 15 | function place(frame, props: Props) { 16 | const { selectedNode, device, devices, index } = props 17 | 18 | // place the first container 100px to the right of the selected frame 19 | const startPos = selectedNode.x + selectedNode.width + 100 20 | // resize and name the container according to the device 21 | frame.resize(device.width, device.height) 22 | frame.name = device.name 23 | 24 | // for each subsequent device being tested, make sure it is always 25 | // placed 100px to the right of the previous device 26 | const widthOfAllPreviousFramesPlusGaps = devices 27 | .slice(0, index) 28 | .reduce((acc, curr) => acc += curr.width + 100, startPos) 29 | 30 | let x = widthOfAllPreviousFramesPlusGaps 31 | frame.x = x 32 | // top-align the containers to the selected node 33 | frame.y = selectedNode.y 34 | return frame 35 | } 36 | 37 | export function createAndPlace(props) { 38 | const { selectedNode } = props 39 | 40 | if (selectedNode.type === 'FRAME') { 41 | const frame = figma.createFrame() 42 | place(frame, props) 43 | return frame 44 | } 45 | 46 | if (selectedNode.type === 'COMPONENT') { 47 | const instance = selectedNode.createInstance() 48 | place(instance, props) 49 | return instance 50 | } 51 | 52 | if (selectedNode.type === 'INSTANCE') { 53 | const instance = selectedNode.clone() 54 | place(instance, props) 55 | return instance 56 | } 57 | } -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | import deviceSizes from './sizes' 2 | import { createAndPlace } from './createAndPlace' 3 | const { command, currentPage, closePlugin } = figma 4 | const { selection } = currentPage 5 | 6 | const all = [...Object.keys(deviceSizes).map(key => deviceSizes[key])] 7 | const devices = command === 'all' 8 | // flatten all devices to a single array 9 | ? [].concat.apply([], all) 10 | : deviceSizes[command] 11 | 12 | function hasValidSelection(nodes) { 13 | const oneSelected = nodes.length === 1 14 | if (!oneSelected) return false 15 | 16 | const frameTypes = ["FRAME", "COMPONENT", "INSTANCE"] 17 | const isFrame = frameTypes.indexOf(nodes[0].type) >= 0 18 | return isFrame 19 | } 20 | 21 | function main(nodes): Promise { 22 | if (!hasValidSelection(selection)) { 23 | return Promise.resolve('Select a single frame to test responsive sizes') 24 | } 25 | 26 | // the frame to clone 27 | const selectedNode = nodes[0] 28 | 29 | function generateContainerFrames() { 30 | let frames: FrameNode[] = [] 31 | 32 | for (let [index, device] of devices.entries()) { 33 | frames.push(createAndPlace({ device, devices, index, selectedNode })) 34 | } 35 | 36 | return frames 37 | } 38 | 39 | function insertSelectionAndResizeIntoContainerFrames(containerFrames) { 40 | for (let container of containerFrames) { 41 | const clone = selectedNode.clone() 42 | clone.x = 0 43 | clone.y = 0 44 | clone.resize(container.width, container.height) 45 | container.appendChild(clone) 46 | } 47 | } 48 | 49 | const containerFrames = generateContainerFrames() 50 | 51 | // if the selected node is a frame, we have to manually append it into 52 | // the newly generated containers. if the selected node is a component, 53 | // this is not needed because the instances don't necessarily need 54 | // to be housed in a parent frame. 55 | if (selectedNode.type === "FRAME") { 56 | insertSelectionAndResizeIntoContainerFrames(containerFrames) 57 | } 58 | 59 | figma.currentPage.selection = containerFrames 60 | figma.viewport.scrollAndZoomIntoView(containerFrames) 61 | return Promise.resolve('Responsified ⚡️') 62 | } 63 | 64 | main(selection).then((msg) => closePlugin(msg)) -------------------------------------------------------------------------------- /src/sizes.ts: -------------------------------------------------------------------------------- 1 | const iphone = [ 2 | { 3 | name: 'iPhone 12 Pro Max', 4 | width: 428, 5 | height: 926, 6 | }, 7 | { 8 | name: 'iPhone 12 Pro', 9 | width: 390, 10 | height: 844, 11 | }, 12 | { 13 | name: 'iPhone 12 mini', 14 | width: 360, 15 | height: 780, 16 | }, 17 | { 18 | name: 'iPhone SE', 19 | width: 320, 20 | height: 568, 21 | }, 22 | ]; 23 | 24 | const android = [ 25 | { 26 | name: 'Google Pixel', 27 | width: 411, 28 | height: 731, 29 | }, 30 | { 31 | name: 'Google Pixel XL', 32 | width: 411, 33 | height: 823, 34 | }, 35 | { 36 | name: 'Generic Android', 37 | width: 360, 38 | height: 640, 39 | }, 40 | ]; 41 | 42 | const tablet = [ 43 | { 44 | name: 'iPad Mini/iPad 9.7" Portrait', 45 | width: 768, 46 | height: 1024, 47 | }, 48 | { 49 | name: 'iPad Mini/iPad 9.7" Landsape', 50 | width: 1024, 51 | height: 768, 52 | }, 53 | { 54 | name: 'iPad Pro 10.5" Portrait', 55 | width: 834, 56 | height: 1112, 57 | }, 58 | { 59 | name: 'iPad Pro 10.5" Landscape', 60 | width: 1112, 61 | height: 834, 62 | }, 63 | { 64 | name: 'iPad Pro 12.9" Portrait', 65 | width: 1024, 66 | height: 1366, 67 | }, 68 | { 69 | name: 'iPad Pro 12.9" Landscape', 70 | width: 1366, 71 | height: 1024, 72 | }, 73 | { 74 | name: 'Surface Pro 3" Portrait', 75 | width: 990, 76 | height: 1440, 77 | }, 78 | { 79 | name: 'Surface Pro 3" Landscape', 80 | width: 1440, 81 | height: 990, 82 | }, 83 | { 84 | name: 'Surface Pro 4" Portrait', 85 | width: 912, 86 | height: 1368, 87 | }, 88 | { 89 | name: 'Surface Pro 4" Landscape', 90 | width: 1368, 91 | height: 912, 92 | }, 93 | ]; 94 | 95 | const desktop = [ 96 | { 97 | name: 'Generic Desktop', 98 | width: 1440, 99 | height: 1024, 100 | }, 101 | { 102 | name: 'MacBook', 103 | width: 1152, 104 | height: 700, 105 | }, 106 | { 107 | name: 'MacBook Pro', 108 | width: 1440, 109 | height: 900, 110 | }, 111 | { 112 | name: 'Surface Book', 113 | width: 1500, 114 | height: 1000, 115 | }, 116 | { 117 | name: 'iMac', 118 | width: 1280, 119 | height: 720, 120 | }, 121 | ]; 122 | 123 | const watch = [ 124 | { 125 | name: 'Apple Watch 42mm', 126 | width: 156, 127 | height: 195, 128 | }, 129 | { 130 | name: 'Apple Watch 38mm', 131 | width: 136, 132 | height: 170, 133 | }, 134 | ]; 135 | 136 | export default { 137 | iphone, 138 | android, 139 | tablet, 140 | desktop, 141 | watch, 142 | }; 143 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es2017", 5 | "lib": [ 6 | "es2017", 7 | "dom" 8 | ] 9 | } 10 | } 11 | --------------------------------------------------------------------------------