├── 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 |
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 |
--------------------------------------------------------------------------------