├── images ├── Compo.sketch ├── compo-icon@2x.png ├── compo-explanation@2x.png └── compo-promo-image@2x.png ├── .gitignore ├── Compo.sketchplugin └── Contents │ ├── Resources │ ├── Icon.png │ └── Screenshot.png │ └── Sketch │ ├── manifest.json │ └── Compo.cocoascript ├── appcast.xml ├── LICENSE └── README.md /images/Compo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romashamin/compo-sketch/HEAD/images/Compo.sketch -------------------------------------------------------------------------------- /images/compo-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romashamin/compo-sketch/HEAD/images/compo-icon@2x.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .config 3 | *.log 4 | *.swp 5 | hidden 6 | node_modules 7 | npm-debug.log 8 | tmp 9 | -------------------------------------------------------------------------------- /images/compo-explanation@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romashamin/compo-sketch/HEAD/images/compo-explanation@2x.png -------------------------------------------------------------------------------- /images/compo-promo-image@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romashamin/compo-sketch/HEAD/images/compo-promo-image@2x.png -------------------------------------------------------------------------------- /Compo.sketchplugin/Contents/Resources/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romashamin/compo-sketch/HEAD/Compo.sketchplugin/Contents/Resources/Icon.png -------------------------------------------------------------------------------- /Compo.sketchplugin/Contents/Resources/Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romashamin/compo-sketch/HEAD/Compo.sketchplugin/Contents/Resources/Screenshot.png -------------------------------------------------------------------------------- /appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Compo 5 | http://sparkle-project.org/files/sparkletestcast.xml 6 | Makes it easier to work with interface components 7 | en 8 | 9 | Version 1.6 10 | 11 | 13 |
  • Test update
  • 14 | 15 | ]]> 16 |
    17 | 18 |
    19 |
    20 |
    21 | -------------------------------------------------------------------------------- /Compo.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Compo", 3 | "description": "Makes it easier to work with interface components", 4 | "author": "Roman Shamin", 5 | "homepage": "https://github.com/romashamin/compo-sketch", 6 | "version": 1.6, 7 | "identifier": "com.github.romashamin.compo-sketch", 8 | "appcast": "https://raw.githubusercontent.com/romashamin/compo-sketch/master/appcast.xml", 9 | "compatibleVersion": 45, 10 | "bundleVersion": 1.0, 11 | "commands": [ 12 | { 13 | "name": "Create Component or Update Selected", 14 | "identifier": "update-component", 15 | "shortcut": "cmd j", 16 | "script": "Compo.cocoascript", 17 | "handler": "onRun" 18 | } 19 | ], 20 | "menu": { 21 | "items": [ 22 | "update-component" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Roman Shamin 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 | # Compo 2 | 3 | 4 | 5 | Compo is a Sketch plugin that makes it easier to work with interface components. With Compo, pressing ⌘J is all it takes to turn a text layer into a button or put an existing component in order. The plugin is especially effective if used jointly with [State Machine](https://github.com/romashamin/statemachine-sketch). 6 | 7 | 8 | 9 | Read more about [how Compo works](https://evilmartians.com/chronicles/compo-sketch). 10 | 11 | ### Install 12 | 13 | 1. Download and unzip: [compo-sketch-master.zip]. 14 | 2. Double click `Compo.sketchplugin`. 15 | 16 | [compo-sketch-master.zip]: https://github.com/romashamin/compo-sketch/archive/master.zip 17 | 18 | ### System Requirements 19 | 20 | Compo has been tested on Sketch 46 on macOS Sierra. If you have any problems, drop me a line: [@romanshamin]. 21 | 22 | [@romanshamin]: https://twitter.com/romanshamin 23 | 24 | ### Satisfied Pro? 25 | 26 | If you’re a professional web designer or a developer and Compo saves your time, buy me an espresso to say ‘thanks’: [pay $3 by PayPal]. 27 | 28 | [pay $3 by PayPal]: https://www.paypal.me/romanshamin/3 29 | 30 | ### Thanks 31 | 32 | 33 | Sponsored by Evil Martians 34 | -------------------------------------------------------------------------------- /Compo.sketchplugin/Contents/Sketch/Compo.cocoascript: -------------------------------------------------------------------------------- 1 | /** 2 | * Compo 1.6 3 | * 4 | * Copyright © 2016 Roman Shamin https://github.com/romashamin 5 | * and licenced under the MIT licence. All rights not explicitly 6 | * granted in the MIT license are reserved. See the included 7 | * LICENSE file for more details. 8 | * 9 | * https://github.com/romashamin/ 10 | * https://twitter.com/romanshamin 11 | */ 12 | 13 | 14 | 15 | var defaultNamePrefix = 'UI/' 16 | var defaultNamePostfix = ' Component' 17 | var defaultPaddingsName = '12:18:12:18' 18 | var masterLayerRE = /\d+:\d+:\d+:\d+/g 19 | var bgNames = ['BG', 'Background'] 20 | 21 | 22 | 23 | /** 24 | * @param {String} strColor 25 | * @param {CGRect} cgrect 26 | */ 27 | 28 | function createRect(nsColor, cgrect) { 29 | var shape = [[MSRectangleShape alloc] init] 30 | [shape setFrame:[MSRect rectWithRect:cgrect]] 31 | 32 | var shapeGroup = [MSShapeGroup shapeWithPath:shape] 33 | 34 | var fill = [[shapeGroup style] addStylePartOfType:0] 35 | var color = [MSColor colorWithNSColor:nsColor] 36 | [fill setColor:color] 37 | 38 | return shapeGroup 39 | } 40 | 41 | var createBackgroundRect = createRect.bind(null, NSColor.colorWithGray(0.8)) 42 | 43 | 44 | 45 | /** 46 | * @param {MSArray} layers 47 | * @param {Array} exceptions 48 | * @return {Array} 49 | */ 50 | 51 | function getLayersExcept(layers, exceptions) { 52 | var resultLayers = [] 53 | 54 | for (var i = 0; i < [layers count]; i++) { 55 | var currentLayer = [layers objectAtIndex:i] 56 | var isCurrentLayerInExceptionsList = false 57 | 58 | for (var j = 0; j < exceptions.length; j++) { 59 | var currentException = exceptions[j] 60 | 61 | if (currentLayer == currentException) { 62 | isCurrentLayerInExceptionsList = true 63 | break 64 | } 65 | } 66 | 67 | if (!isCurrentLayerInExceptionsList) resultLayers.push(currentLayer) 68 | } 69 | 70 | return resultLayers 71 | } 72 | 73 | 74 | 75 | /** 76 | * Parse layer’s name and return a bounding rectangle 77 | * @param {MSLayerGroup|MSShapeGroup|MSTextLayer} layer 78 | * @return {CGRect|undefined} 79 | */ 80 | 81 | function cgrectFromLayerName(layer) { 82 | var layerName = [layer name] 83 | 84 | var parseResult = layerName.match(masterLayerRE) 85 | 86 | if (parseResult) { 87 | var paddingsStrValues = parseResult[0].split(':') 88 | 89 | var paddingTop = parseInt(paddingsStrValues[0], 10) 90 | var paddingRight = parseInt(paddingsStrValues[1], 10) 91 | var paddingBottom = parseInt(paddingsStrValues[2], 10) 92 | var paddingLeft = parseInt(paddingsStrValues[3], 10) 93 | 94 | var layerCGRect = [layer rect] 95 | 96 | var x = layerCGRect.origin.x - paddingLeft 97 | var y = layerCGRect.origin.y - paddingTop 98 | var w = layerCGRect.size.width + paddingLeft + paddingRight 99 | var h = layerCGRect.size.height + paddingTop + paddingBottom 100 | 101 | return CGRectMake(x, y, w, h) 102 | } 103 | 104 | return undefined 105 | } 106 | 107 | 108 | 109 | /** 110 | * Creates MSLayerGroup 111 | * @param {String} name 112 | * @return {MSLayerGroup} 113 | */ 114 | 115 | function createGroup(name) { 116 | var group = [MSLayerGroup new] 117 | var groupFrame = [group frame] 118 | [groupFrame setConstrainProportions:false] 119 | [group setName:name] 120 | 121 | return group 122 | } 123 | 124 | 125 | 126 | /** 127 | * Creates a button group 128 | * @param {MSTextLayer|MSShapeGroup} layer 129 | */ 130 | 131 | function createButton(layer) { 132 | var name = [layer class] == [MSTextLayer class] ? [layerSelected stringValue] : [layerSelected name] 133 | 134 | var group = createGroup(defaultNamePrefix + name + defaultNamePostfix) 135 | var parentGroup = [layer parentGroup] 136 | 137 | var cgrectLayer = cgrectFromLayerName(layer) 138 | if (!cgrectLayer) { 139 | [layer setName:defaultPaddingsName] 140 | layer.nameIsFixed = 1 141 | cgrectLayer = cgrectFromLayerName(layer) 142 | } 143 | 144 | var bgRect = createBackgroundRect(cgrectLayer) 145 | [bgRect setName:bgNames[0]] 146 | 147 | [parentGroup addLayers:[group]] 148 | [parentGroup removeLayer:layer] 149 | [group addLayers:[bgRect, layer]] 150 | [group resizeToFitChildrenWithOption:1] 151 | } 152 | 153 | 154 | 155 | /** 156 | * Searches for the master layer 157 | * @param {MSArray} layers 158 | * @return {MSTextLayer|MSShapeGroup|undefined} 159 | */ 160 | 161 | function getMasterLayer(layers) { 162 | for (var i = 0; i < [layers count]; i++) { 163 | var layer = [layers objectAtIndex:i] 164 | 165 | if ([layer name].search(masterLayerRE) > -1) return layer 166 | } 167 | 168 | return undefined 169 | } 170 | 171 | 172 | 173 | /** 174 | * Searches for the background layer 175 | * @param {MSArray} layers 176 | * @return {MSTextLayer|MSShapeGroup|undefined} 177 | */ 178 | 179 | function getBackgroundLayer(layers) { 180 | for (var i = 0; i < [layers count]; i++) { 181 | var layer = [layers objectAtIndex:i] 182 | var strName = [layer name] 183 | 184 | for (var j = 0; j < bgNames.length; j++) { 185 | if (strName.toLowerCase().indexOf(bgNames[j].toLowerCase()) >= 0) { 186 | return layer 187 | } 188 | } 189 | } 190 | 191 | return undefined 192 | } 193 | 194 | 195 | 196 | /** 197 | * Resizes a background layer according master layer name 198 | * @param {MSShapeGroup} backgroundLayer 199 | */ 200 | 201 | function resizeBackground(backgroundLayer, masterLayer) { 202 | var cgrectMaster = cgrectFromLayerName(masterLayer) 203 | var bgFrame = [backgroundLayer frame] 204 | 205 | if (bgFrame.x != cgrectMaster.origin.x) [bgFrame setX:cgrectMaster.origin.x] 206 | if (bgFrame.y != cgrectMaster.origin.y) [bgFrame setY:cgrectMaster.origin.y] 207 | if (bgFrame.width != cgrectMaster.size.width) [bgFrame setWidth:cgrectMaster.size.width] 208 | if (bgFrame.height != cgrectMaster.size.height) [bgFrame setHeight:cgrectMaster.size.height] 209 | } 210 | 211 | 212 | 213 | /** 214 | * Searches for margin codes in the given name 215 | * @param {String} name 216 | * @return {} 217 | */ 218 | 219 | function getMarginsFromName(name) { 220 | 221 | name = name.toLowerCase(); 222 | 223 | var margins = { 224 | top: undefined, 225 | right: undefined, 226 | bottom: undefined, 227 | left: undefined 228 | } 229 | 230 | var re = { 231 | top: /t:\d+/g, 232 | right: /r:\d+/g, 233 | bottom: /b:\d+/g, 234 | left: /l:\d+/g 235 | } 236 | 237 | var arrTop = re.top.exec(name) 238 | if (arrTop) { 239 | var prefixAndValue = arrTop[0].split(':') 240 | margins.top = parseInt(prefixAndValue[1], 10) 241 | } 242 | 243 | var arrRight = re.right.exec(name) 244 | if (arrRight) { 245 | var prefixAndValue = arrRight[0].split(':') 246 | margins.right = parseInt(prefixAndValue[1], 10) 247 | } 248 | 249 | var arrBottom = re.bottom.exec(name) 250 | if (arrBottom) { 251 | var prefixAndValue = arrBottom[0].split(':') 252 | margins.bottom = parseInt(prefixAndValue[1], 10) 253 | } 254 | 255 | var arrLeft = re.left.exec(name) 256 | if (arrLeft) { 257 | var prefixAndValue = arrLeft[0].split(':') 258 | margins.left = parseInt(prefixAndValue[1], 10) 259 | } 260 | 261 | return margins 262 | } 263 | 264 | 265 | 266 | /** 267 | * @param {MSLayerGroup|MSShapeGroup|MSTextLayer} layer 268 | * @param {MSLayerGroup|MSShapeGroup} backgroundLayer 269 | */ 270 | 271 | function alignCenter(layer, background) { 272 | var bgFrame = [background frame] 273 | var layerFrame = [layer frame] 274 | 275 | [layerFrame setX: [bgFrame x] + Math.floor([bgFrame width] / 2 - [layerFrame width] / 2)] 276 | [layerFrame setY: [bgFrame y] + Math.floor([bgFrame height] / 2 - [layerFrame height] / 2)] 277 | } 278 | 279 | function alignTop(margin, layer, background) { 280 | var bgFrame = [background frame] 281 | var layerFrame = [layer frame] 282 | 283 | [layerFrame setY: margin + [bgFrame y]] 284 | } 285 | 286 | function alignBottom(margin, layer, background) { 287 | var bgFrame = [background frame] 288 | var layerFrame = [layer frame] 289 | 290 | [layerFrame setY: [bgFrame y] + [bgFrame height] - [layerFrame height] - margin] 291 | } 292 | 293 | function alignLeft(margin, layer, background) { 294 | var bgFrame = [background frame] 295 | var layerFrame = [layer frame] 296 | 297 | [layerFrame setX: margin + [bgFrame x]] 298 | } 299 | 300 | function alignRight(margin, layer, background) { 301 | var bgFrame = [background frame] 302 | var layerFrame = [layer frame] 303 | 304 | [layerFrame setX: [bgFrame x] + [bgFrame width] - [layerFrame width] - margin] 305 | } 306 | 307 | 308 | 309 | /** 310 | * Moves a layer according its settings 311 | * @param {MSLayerGroup|MSShapeGroup|MSTextLayer} layer 312 | * @param {MSShapeGroup|undefined} backgroundLayer 313 | */ 314 | 315 | function moveLayer(layer, backgroundLayer) { 316 | var margins = getMarginsFromName([layer name]) 317 | var background = (backgroundLayer ? backgroundLayer : [layer parentGroup]) 318 | 319 | alignCenter(layer, background) 320 | 321 | if ((margins.top != undefined && margins.bottom != undefined) || margins.top != undefined) { 322 | alignTop(margins.top, layer, background) 323 | } else if (margins.bottom != undefined) { 324 | alignBottom(margins.bottom, layer, background) 325 | } 326 | 327 | if ((margins.left != undefined && margins.right != undefined) || margins.left != undefined) { 328 | alignLeft(margins.left, layer, background) 329 | } else if (margins.right != undefined) { 330 | alignRight(margins.right, layer, background) 331 | } 332 | } 333 | 334 | 335 | 336 | /** 337 | * Distributes layers within a group 338 | * @param {Array} layers 339 | * @param {MSShapeGroup|undefined} backgroundLayer 340 | */ 341 | 342 | function distributeLayers(layers, backgroundLayer) { 343 | for (var i = 0; i < layers.length; i++) { 344 | moveLayer(layers[i], backgroundLayer) 345 | } 346 | } 347 | 348 | 349 | 350 | /** 351 | * Process the group 352 | * @param {MSLayerGroup} group 353 | */ 354 | 355 | function processGroup(group) { 356 | var layers = [group layers] 357 | 358 | var masterLayer = getMasterLayer(layers) 359 | log('masterLayer: ' + masterLayer) 360 | var backgroundLayer = getBackgroundLayer(layers) 361 | var exceptionList = [] 362 | 363 | if (masterLayer) { 364 | resizeBackground(backgroundLayer, masterLayer) 365 | [group resizeToFitChildrenWithOption:1] 366 | exceptionList = [masterLayer, backgroundLayer] 367 | } else { 368 | exceptionList = [backgroundLayer] 369 | } 370 | 371 | var layersExceptList = getLayersExcept(layers, exceptionList) 372 | distributeLayers(layersExceptList, backgroundLayer) 373 | [group resizeToFitChildrenWithOption:1] 374 | } 375 | 376 | 377 | 378 | /** 379 | * Creates a button group for text layers 380 | * and distributes layers within groups 381 | * @param {MSLayerGroup|MSShapeGroup|MSTextLayer} layer 382 | */ 383 | 384 | function processSelection(layer) { 385 | if ([layer class] == [MSLayerGroup class]) { 386 | processGroup(layer) 387 | } else { 388 | var parentGroup = [layer parentGroup] 389 | 390 | if ([parentGroup class] != [MSArtboardGroup class] && getBackgroundLayer([parentGroup layers])) { 391 | processGroup(parentGroup) 392 | } else { 393 | createButton(layer) 394 | } 395 | } 396 | } 397 | 398 | 399 | 400 | /** 401 | * Entry point 402 | * @param {NSDictionary} context 403 | */ 404 | 405 | function onRun(context) { 406 | var doc = context.document 407 | var selection = context.selection 408 | var pluginName = 'Compo' 409 | 410 | if ([selection count] > 0) { 411 | var loopSelection = [selection objectEnumerator] 412 | while (layerSelected = [loopSelection nextObject]) { 413 | processSelection(layerSelected) 414 | } 415 | } else { 416 | [doc showMessage: pluginName + ': select something'] 417 | } 418 | } 419 | 420 | //onRun(context) 421 | --------------------------------------------------------------------------------