├── .DS_Store ├── .gitignore ├── Plugin ├── .DS_Store ├── Debug.xcconfig ├── Images │ ├── dirty.png │ └── dirty@2x.png ├── LICENSE ├── README.md ├── Release.xcconfig ├── States.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ ├── States for Beta.xcscheme │ │ └── States.xcscheme ├── States │ ├── Info.plist │ ├── NSArray+HigherOrder.h │ ├── NSArray+HigherOrder.m │ ├── NSArray+Indexes.h │ ├── NSArray+Indexes.m │ ├── STArtboard.h │ ├── STColorFactory.h │ ├── STColorFactory.m │ ├── STCommand.h │ ├── STDocument.h │ ├── STHeaderView.h │ ├── STHeaderView.m │ ├── STLayer.h │ ├── STLayerState.h │ ├── STLayerState.m │ ├── STPage.h │ ├── STPlaceholderView.h │ ├── STPlaceholderView.m │ ├── STSketch.h │ ├── STSketch.m │ ├── STSketchPluginContext.h │ ├── STSketchPluginContext.m │ ├── STStateDescription.h │ ├── STStateDescription.m │ ├── STStatefulArtboard+Backend.h │ ├── STStatefulArtboard+Backend.m │ ├── STStatefulArtboard+Snapshots.h │ ├── STStatefulArtboard+Snapshots.m │ ├── STStatefulArtboard.h │ ├── STStatefulArtboard.m │ ├── STTableCellView.h │ ├── STTableCellView.m │ ├── STTableRowView.h │ ├── STTableRowView.m │ ├── STTableView.h │ ├── STTableView.m │ ├── STTextField.h │ ├── STTextField.m │ ├── STUpdateButton.h │ ├── STUpdateButton.m │ ├── STWindow.h │ ├── STWindow.m │ ├── StatesController+ContextMenu.h │ ├── StatesController+ContextMenu.m │ ├── StatesController+Decisions.h │ ├── StatesController+Decisions.m │ ├── StatesController+DragNDrop.h │ ├── StatesController+DragNDrop.m │ ├── StatesController+Naming.h │ ├── StatesController+Naming.m │ ├── StatesController.h │ ├── StatesController.m │ └── StatesWindow.xib ├── Versioning.xcconfig ├── lib │ └── runtime.js ├── manifest.json ├── plugin.js └── vendor │ ├── Aspects.h │ └── Aspects.m ├── States.sketchplugin └── Contents │ ├── Resources │ └── States.bundle │ │ └── Contents │ │ ├── Info.plist │ │ ├── MacOS │ │ └── States │ │ ├── Resources │ │ ├── StatesWindow.nib │ │ └── dirty.tiff │ │ └── _CodeSignature │ │ └── CodeResources │ └── Sketch │ ├── lib │ └── runtime.js │ ├── manifest.json │ └── plugin.js ├── css ├── normalize.css ├── states.webflow.css └── webflow.css ├── images ├── Animation2.gif ├── add.png ├── blk.png ├── f.png ├── favbig.png ├── favicon.png ├── git.png ├── logo.png ├── mate.gif ├── position.png ├── share-p-1080x565.png ├── share-p-500x262.png ├── share-p-800x418.png ├── share.png ├── sketch.png ├── t.png ├── update.png └── visible.png ├── index.html └── js └── webflow.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | States.sketchplugin.zip 3 | -------------------------------------------------------------------------------- /Plugin/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/Plugin/.DS_Store -------------------------------------------------------------------------------- /Plugin/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | // Debug.xcconfig 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #include "Versioning.xcconfig" 8 | 9 | OTHER_LDFLAGS = $(inherited) -Wl,-source_version -Wl,${IEXP_SOURCE_VERSION} 10 | -------------------------------------------------------------------------------- /Plugin/Images/dirty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/Plugin/Images/dirty.png -------------------------------------------------------------------------------- /Plugin/Images/dirty@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/Plugin/Images/dirty@2x.png -------------------------------------------------------------------------------- /Plugin/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Eden Vidal 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. -------------------------------------------------------------------------------- /Plugin/README.md: -------------------------------------------------------------------------------- 1 | # States of the artboard — Sketch Plugin 2 | 3 | Create different states and switch between them easily. Just like layer comps for Sketch. 4 | 5 | - Define different positions and toggle visibility of your layers. 6 | - Create new states and update changes. 7 | - Create pages with new artboards from your states. 8 | - Since symbols are artboards, you can create states for them too. 9 | - And yes — The states are saved on your file. Boom. 10 | 11 | ![How it works](https://daks2k3a4ib2z.cloudfront.net/574f0289c3c4633629a7737b/5766c49dc26632fe609656f1_Animation3_03.gif) 12 | -------------------------------------------------------------------------------- /Plugin/Release.xcconfig: -------------------------------------------------------------------------------- 1 | // Release.xcconfig 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #include "Versioning.xcconfig" 8 | 9 | OTHER_LDFLAGS = $(inherited) -Wl,-source_version -Wl,${IEXP_SOURCE_VERSION} 10 | -------------------------------------------------------------------------------- /Plugin/States.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Plugin/States.xcodeproj/xcshareddata/xcschemes/States for Beta.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 49 | 50 | 51 | 57 | 58 | 59 | 60 | 61 | 62 | 68 | 69 | 75 | 76 | 77 | 78 | 80 | 81 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /Plugin/States.xcodeproj/xcshareddata/xcschemes/States.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 49 | 50 | 51 | 57 | 58 | 59 | 60 | 61 | 62 | 68 | 69 | 75 | 76 | 77 | 78 | 80 | 81 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /Plugin/States/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | NSHumanReadableCopyright 24 | Copyright © 2016 Eden Vidal. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Plugin/States/NSArray+HigherOrder.h: -------------------------------------------------------------------------------- 1 | // NSArray+HigherOrder.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | @import Foundation; 8 | 9 | @interface NSArray (HigherOrder) 10 | 11 | - (nonnull NSArray *)st_map: (nonnull id _Nonnull (^)(id _Nonnull obj))mapper; 12 | 13 | - (nonnull NSArray *)st_filter: (nonnull BOOL (^)(id _Nonnull obj))block; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Plugin/States/NSArray+HigherOrder.m: -------------------------------------------------------------------------------- 1 | // NSArray+HigherOrder.m 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | 8 | #import "NSArray+HigherOrder.h" 9 | 10 | @implementation NSArray (HigherOrder) 11 | 12 | - (NSArray *)st_map: (nonnull id _Nonnull (^)(id _Nonnull obj))mapper 13 | { 14 | NSMutableArray *result = [NSMutableArray arrayWithCapacity: self.count]; 15 | [self enumerateObjectsUsingBlock: ^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 16 | [result addObject: mapper(obj)]; 17 | }]; 18 | return result; 19 | } 20 | 21 | - (NSArray *)st_filter: (BOOL (^)(id))block 22 | { 23 | NSMutableArray *new = [NSMutableArray array]; 24 | [self enumerateObjectsUsingBlock: ^(id obj, NSUInteger idx, BOOL *stop) { 25 | if (block(obj)) [new addObject: obj]; 26 | }]; 27 | return new; 28 | } 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /Plugin/States/NSArray+Indexes.h: -------------------------------------------------------------------------------- 1 | // NSArray+Indexes.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | @import Foundation; 8 | 9 | @interface NSArray (Indexes) 10 | 11 | - (nonnull NSIndexSet *)st_indexesOfObjects: (nonnull NSArray *)subarray; 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Plugin/States/NSArray+Indexes.m: -------------------------------------------------------------------------------- 1 | // NSArray+Indexes.m 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "NSArray+Indexes.h" 8 | 9 | @implementation NSArray (Indexes) 10 | 11 | - (nonnull NSIndexSet *)st_indexesOfObjects: (nonnull NSArray *)subarray 12 | { 13 | return [self indexesOfObjectsPassingTest: ^BOOL(id obj, NSUInteger idx, BOOL * stop) { 14 | return [subarray containsObject: obj]; 15 | }]; 16 | } 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /Plugin/States/STArtboard.h: -------------------------------------------------------------------------------- 1 | // STArtboard.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | @import Foundation; 8 | 9 | @protocol STLayer; 10 | 11 | @protocol STArtboard 12 | @optional 13 | 14 | - (NSArray >*)children; 15 | 16 | - (void)setName: (NSString *)name; 17 | - (NSString *)name; 18 | 19 | - (instancetype)copy; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /Plugin/States/STColorFactory.h: -------------------------------------------------------------------------------- 1 | // STColorFactory.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | @import Cocoa; 8 | 9 | /// Keeps all of the custom colors for this project 10 | @interface STColorFactory : NSObject 11 | 12 | // Table View Colors 13 | 14 | + (NSColor *)selectedTableViewRowColor; 15 | 16 | + (NSColor *)selectedInactiveTableViewRowColor; 17 | 18 | + (NSColor *)mainTableViewRowColor; 19 | 20 | + (NSColor *)secondaryTableViewRowColor; 21 | 22 | + (NSColor *)tableViewBackgroundColor; 23 | 24 | + (NSColor *)tableViewCellTextRegularColor; 25 | 26 | + (NSColor *)tableViewCellTextSelectedColorWithAlpha: (CGFloat)alpha; 27 | 28 | + (NSColor *)tableViewCellTextInactiveSelectedColorWithAlpha: (CGFloat)alpha; 29 | 30 | // Header Colors 31 | 32 | + (NSColor *)headerViewBackgroundColor; 33 | + (NSColor *)headerViewBorderColor; 34 | 35 | // Placeholder Colors 36 | 37 | + (NSColor *)placeholderViewBackground; 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /Plugin/States/STColorFactory.m: -------------------------------------------------------------------------------- 1 | // STColorFactory.m 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | // 7 | 8 | #import "STColorFactory.h" 9 | 10 | @implementation STColorFactory 11 | 12 | + (NSColor *)selectedTableViewRowColor 13 | { 14 | return [NSColor colorWithRed: 110.f/255.f green: 157.f/255.f blue: 228.f/255.f alpha: 1.0f]; 15 | } 16 | 17 | + (NSColor *)selectedInactiveTableViewRowColor 18 | { 19 | return [NSColor colorWithRed: 200.f/255.f green: 200.f/255.f blue: 200.f/255.f alpha: 1.0f]; 20 | } 21 | 22 | + (NSColor *)tableViewBackgroundColor 23 | { 24 | return [NSColor colorWithRed: 236.f/255.f green: 236.f/255.f blue: 236.f/255.f alpha: 1.0f]; 25 | } 26 | 27 | + (NSColor *)mainTableViewRowColor 28 | { 29 | return [NSColor colorWithRed: 240.f/255.f green: 240.f/255.f blue: 240.f/255.f alpha: 1.0f]; 30 | } 31 | 32 | + (NSColor *)secondaryTableViewRowColor 33 | { 34 | return [NSColor colorWithRed: 235.f/255.f green: 235.f/255.f blue: 235.f/255.f alpha: 1.0f]; 35 | } 36 | 37 | + (NSColor *)tableViewCellTextRegularColor 38 | { 39 | return [NSColor controlTextColor]; 40 | } 41 | 42 | + (NSColor *)tableViewCellTextSelectedColorWithAlpha: (CGFloat)alpha 43 | { 44 | return [NSColor colorWithWhite: 10 alpha: alpha]; 45 | } 46 | 47 | + (NSColor *)tableViewCellTextInactiveSelectedColorWithAlpha: (CGFloat)alpha 48 | { 49 | return [NSColor colorWithWhite: 5 alpha: alpha]; 50 | } 51 | 52 | + (NSColor *)headerViewBackgroundColor 53 | { 54 | return [NSColor colorWithRed: 243.f/255.f green: 243.f/255.f blue: 243.f/255.f alpha: 1.0f]; 55 | } 56 | 57 | + (NSColor *)headerViewBorderColor 58 | { 59 | return [NSColor colorWithRed: 184.f/255.f green: 184.f/255.f blue: 184.f/255.f alpha: 1.0f]; 60 | } 61 | 62 | + (NSColor *)placeholderViewBackground 63 | { 64 | return [NSColor colorWithRed: 236.f/255.f green: 236.f/255.f blue: 236.f/255.f alpha: 1.0f]; 65 | } 66 | 67 | @end 68 | -------------------------------------------------------------------------------- /Plugin/States/STCommand.h: -------------------------------------------------------------------------------- 1 | // STCommand.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | 8 | @import Foundation; 9 | #import "STLayer.h" 10 | 11 | @protocol STCommand 12 | @optional 13 | 14 | - (void)setValue: (id)value forKey: (id )key onLayer: (id )layer; 15 | - (id)valueForKey: (id )key onLayer: (id )layer; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /Plugin/States/STDocument.h: -------------------------------------------------------------------------------- 1 | // STDocument.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | @import Foundation; 8 | #import "STPage.h" 9 | 10 | @protocol STDocumentData 11 | 12 | - (void)addPage: (id )page; 13 | - (void)deselectAllLayers; 14 | 15 | @end 16 | 17 | @protocol STDocument 18 | @optional 19 | 20 | - (void)setCurrentPage: (id )page; 21 | - (id )currentPage; 22 | 23 | - (id)window; 24 | 25 | - (id )documentData; 26 | 27 | - (void)setSelectedLayers: (NSArray *)layers; 28 | 29 | @end 30 | 31 | 32 | -------------------------------------------------------------------------------- /Plugin/States/STHeaderView.h: -------------------------------------------------------------------------------- 1 | // STHeaderView.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | @import Cocoa; 8 | 9 | /// A header view with a custom background color 10 | @interface STHeaderView : NSView 11 | @end 12 | -------------------------------------------------------------------------------- /Plugin/States/STHeaderView.m: -------------------------------------------------------------------------------- 1 | // STHeaderView.m 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "STColorFactory.h" 8 | #import "STHeaderView.h" 9 | 10 | #define kHeaderViewBorderWidth (1.0f) 11 | 12 | @implementation STHeaderView 13 | 14 | - (void)awakeFromNib 15 | { 16 | self.wantsLayer = YES; 17 | } 18 | 19 | - (BOOL)wantsUpdateLayer 20 | { 21 | return YES; 22 | } 23 | 24 | - (void)updateLayer 25 | { 26 | // Setup a background 27 | self.layer.backgroundColor = [STColorFactory headerViewBackgroundColor].CGColor; 28 | // Draw a border at the buttom of the header 29 | CALayer *buttomBorder = [CALayer layer]; 30 | buttomBorder.borderColor = [STColorFactory headerViewBorderColor].CGColor; 31 | buttomBorder.borderWidth = kHeaderViewBorderWidth; 32 | buttomBorder.frame = CGRectMake(0, 0, CGRectGetWidth(self.frame), kHeaderViewBorderWidth); 33 | 34 | [self.layer addSublayer: buttomBorder]; 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /Plugin/States/STLayer.h: -------------------------------------------------------------------------------- 1 | // STLayer.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | @import Foundation; 8 | 9 | @protocol STAbsoluteRect; 10 | 11 | @protocol STLayer 12 | @optional 13 | 14 | - (BOOL)isVisible; 15 | - (void)setIsVisible: (BOOL)visible; 16 | - (id )absoluteRect; 17 | 18 | - (void)copyToLayer: (id )newParent beforeLayer: (id )sibling; 19 | 20 | @end 21 | 22 | @protocol STFrame 23 | @optional 24 | 25 | - (CGRect)rect; 26 | 27 | - (CGFloat)x; 28 | - (CGFloat)y; 29 | 30 | - (void)setX: (CGFloat)x; 31 | - (void)setY: (CGFloat)y; 32 | 33 | @end 34 | 35 | @protocol STAbsoluteRect 36 | @optional 37 | 38 | - (CGRect)absoluteRect; 39 | 40 | - (void)setX: (CGFloat)x; 41 | - (void)setY: (CGFloat)y; 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /Plugin/States/STLayerState.h: -------------------------------------------------------------------------------- 1 | // STLayerState.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | @import Foundation; 8 | 9 | @protocol STLayer; 10 | 11 | /// Incapsulates a state of a single layer: its frame and visibility status 12 | @interface STLayerState : NSObject 13 | 14 | @property (readonly) NSRect frame; 15 | @property (readonly) BOOL visible; 16 | 17 | - (instancetype)initWithFrame: (NSRect)aFrame visibilityStatus: (BOOL)visible; 18 | + (instancetype)stateWithFrame: (NSRect)aFrame visibilityStatus: (BOOL)visible; 19 | 20 | - (NSDictionary *)dictionaryRepresentation; 21 | - (instancetype)initWithDictionary: (NSDictionary *)dictionary; 22 | 23 | @end 24 | 25 | /// Applies the given layer state to the given layer 26 | @interface STLayerStateApplier : NSObject 27 | + (void)apply: (STLayerState *)state toLayer: (id )layer; 28 | @end 29 | 30 | /// Returns the current layer's state 31 | @interface STLayerStateFetcher : NSObject 32 | + (STLayerState *)fetchStateFromLayer: (id )layer; 33 | @end 34 | 35 | /// Verifies that the given layer conforms to the given state 36 | @interface STLayerStateExaminer : NSObject 37 | + (BOOL)layer: (id )layer conformsToState: (STLayerState *)state; 38 | @end 39 | -------------------------------------------------------------------------------- /Plugin/States/STLayerState.m: -------------------------------------------------------------------------------- 1 | // STLayerState.m 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "STLayer.h" 8 | #import "STLayerState.h" 9 | 10 | @implementation STLayerState 11 | 12 | - (instancetype)initWithFrame: (NSRect)aFrame visibilityStatus: (BOOL)visible 13 | { 14 | if ((self = [super init])) { 15 | _frame = aFrame; 16 | _visible = visible; 17 | } 18 | return self; 19 | } 20 | 21 | + (instancetype)stateWithFrame: (NSRect)aFrame visibilityStatus: (BOOL)visible 22 | { 23 | return [[[self class] alloc] initWithFrame: aFrame visibilityStatus: visible]; 24 | } 25 | 26 | - (NSDictionary *)dictionaryRepresentation 27 | { 28 | return @{ 29 | @"frame" : NSStringFromRect(_frame), 30 | @"visible" : @(_visible) 31 | }; 32 | } 33 | 34 | - (instancetype)initWithDictionary: (NSDictionary *)dictionary 35 | { 36 | NSParameterAssert(dictionary[@"frame"]); 37 | NSParameterAssert(dictionary[@"visible"]); 38 | 39 | return [self initWithFrame: NSRectFromString(dictionary[@"frame"]) 40 | visibilityStatus: [dictionary[@"visible"] boolValue]]; 41 | } 42 | 43 | - (BOOL)isEqual: (id)object 44 | { 45 | typeof(self) another = object; 46 | 47 | if (![another isKindOfClass: [self class]]) { 48 | return NO; 49 | } 50 | if (!NSEqualRects(_frame, another.frame)) { 51 | return NO; 52 | } 53 | if (_visible != another.visible) { 54 | return NO; 55 | } 56 | return YES; 57 | } 58 | 59 | - (NSUInteger)hash 60 | { 61 | return NSStringFromRect(_frame).hash + @(_visible).hash; 62 | } 63 | 64 | - (NSString *)description 65 | { 66 | return [NSString stringWithFormat: @"<%@: %p> (frame = %@, visible = %@)", 67 | NSStringFromClass([self class]), (void *)self, 68 | NSStringFromRect(_frame), _visible ? @"YES" : @"NO"]; 69 | } 70 | 71 | @end 72 | 73 | @implementation STLayerStateApplier 74 | 75 | + (void)apply: (STLayerState *)state toLayer: (id )layer 76 | { 77 | layer.isVisible = state.visible; 78 | 79 | id frame = [layer performSelector: @selector(frame)]; 80 | frame.x = state.frame.origin.x; 81 | frame.y = state.frame.origin.y; 82 | } 83 | 84 | @end 85 | 86 | @implementation STLayerStateFetcher : NSObject 87 | 88 | + (STLayerState *)fetchStateFromLayer: (id )layer 89 | { 90 | id frameObject = [layer performSelector: @selector(frame)]; 91 | return [[STLayerState alloc] initWithFrame: NSRectFromCGRect(frameObject.rect) 92 | visibilityStatus: layer.isVisible]; 93 | } 94 | 95 | @end 96 | 97 | @implementation STLayerStateExaminer : NSObject 98 | 99 | + (BOOL)layer: (id )layer conformsToState: (STLayerState *)state 100 | { 101 | id frameObject = [layer performSelector: @selector(frame)]; 102 | NSRect layerRect = NSRectFromCGRect(frameObject.rect); 103 | 104 | if (layer.isVisible != state.visible) { 105 | return NO; 106 | } 107 | if (!NSEqualPoints(layerRect.origin, state.frame.origin)) { 108 | return NO; 109 | } 110 | return YES; 111 | } 112 | 113 | @end 114 | -------------------------------------------------------------------------------- /Plugin/States/STPage.h: -------------------------------------------------------------------------------- 1 | // STPage.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | @import Foundation; 8 | #import "STArtboard.h" 9 | 10 | @protocol STPage 11 | @optional 12 | 13 | + (instancetype)page; 14 | - (instancetype)copy; 15 | 16 | - (id )currentArtboard; 17 | - (NSArray *)artboards; 18 | 19 | - (void)enumerateLayersWithOptions: (int)options block: (void(^)(id layer))block; 20 | 21 | - (void)addLayers: (NSArray *)layers; 22 | - (void)removeLayer: (id )layer; 23 | 24 | - (void)selectLayers: (NSArray *)layers; 25 | 26 | - (void)setName: (NSString *)name; 27 | - (NSString *)name; 28 | 29 | - (void)setPageDelegate: (id)pageDelegate; 30 | - (id)pageDelegate; 31 | 32 | - (void)setGrid: (id)grid; 33 | - (id)grid; 34 | 35 | - (void)setLayout: (id)layout; 36 | - (id)layout; 37 | 38 | - (void)setScrollOrigin: (CGPoint)scrollOrigin; 39 | - (CGPoint)scrollOrigin; 40 | 41 | - (void)setZoomValue: (CGFloat)zoomValue; 42 | - (CGFloat)zoomValue; 43 | 44 | @end 45 | -------------------------------------------------------------------------------- /Plugin/States/STPlaceholderView.h: -------------------------------------------------------------------------------- 1 | // STPlaceholderView.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | @import Cocoa; 8 | 9 | /// A simple placeholder view that covers the main table view when there's no artboard selected 10 | @interface STPlaceholderView : NSView 11 | @end 12 | -------------------------------------------------------------------------------- /Plugin/States/STPlaceholderView.m: -------------------------------------------------------------------------------- 1 | // STPlaceholderView.m 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | 8 | #import "STColorFactory.h" 9 | #import "STPlaceholderView.h" 10 | 11 | @implementation STPlaceholderView 12 | 13 | - (void)awakeFromNib 14 | { 15 | self.wantsLayer = YES; 16 | } 17 | 18 | - (BOOL)wantsUpdateLayer 19 | { 20 | return YES; 21 | } 22 | 23 | - (void)updateLayer 24 | { 25 | self.layer.backgroundColor = [STColorFactory placeholderViewBackground].CGColor; 26 | } 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /Plugin/States/STSketch.h: -------------------------------------------------------------------------------- 1 | // STSketch.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | @import Cocoa; 8 | #import "STStateDescription.h" 9 | #import "STDocument.h" 10 | #import "STSketchPluginContext.h" 11 | 12 | @protocol SketchNotificationsListener 13 | @required 14 | - (void)currentArtboardDidChange; 15 | - (void)currentArtboardUnselected; 16 | - (void)currentDocumentUpdated; 17 | @end 18 | 19 | /// The bridge between Sketch and our plugin. Provides info about current document as well 20 | /// as various notifications available for SketchNotificationsListener 21 | @interface STSketch : NSObject 22 | 23 | /// Information about the curent document: the document itself, current page and artboard 24 | + (id )currentDocument; 25 | + (id )currentPage; 26 | + (id )currentArtboard; 27 | 28 | /// Use this observer to subscribe to various Sketch notifications. See SketchNotificationsListener 29 | /// for more details 30 | + (instancetype)notificationObserver; 31 | - (void)addListener: (id )listener; 32 | 33 | /// We must save a plugin context in order to perform some layer modifications (i.e. use plugin command) 34 | /// IMPORTANT: You must set this context via -setPluginContext: method before calling any other methods 35 | /// of this class. 36 | + (void)setPluginContextDictionary: (NSDictionary *)contextDictionary; 37 | + (STSketchPluginContext *)pluginContext; 38 | 39 | /// Toggles the plugin's menu item's titles between "Show States" and "Hide States" 40 | + (void)toggleStatesPluginName; 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /Plugin/States/STSketch.m: -------------------------------------------------------------------------------- 1 | // STSketch.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import 8 | #import "Aspects.h" 9 | #import "STSketch.h" 10 | #import "STStatefulArtboard.h" 11 | 12 | @interface STSketch() 13 | @property (strong) NSHashTable *listeners; 14 | @end 15 | 16 | @implementation STSketch 17 | 18 | + (instancetype)notificationObserver 19 | { 20 | static STSketch *observer = nil; 21 | static dispatch_once_t onceToken; 22 | dispatch_once(&onceToken, ^{ 23 | observer = [STSketch new]; 24 | observer.listeners = [NSHashTable weakObjectsHashTable]; 25 | [observer injectIntoMSDocument]; 26 | }); 27 | return observer; 28 | } 29 | 30 | - (void)addListener: (id)listener 31 | { 32 | [_listeners addObject: listener]; 33 | } 34 | 35 | #pragma mark - 36 | 37 | + (id )currentDocument 38 | { 39 | return [NSClassFromString(@"MSDocument") currentDocument]; 40 | } 41 | 42 | + (id )currentPage 43 | { 44 | return [[self currentDocument] currentPage]; 45 | } 46 | 47 | + (id )currentArtboard 48 | { 49 | id raw = [[self currentPage] currentArtboard]; 50 | if (!raw) { 51 | return nil; 52 | } 53 | return [[STStatefulArtboard alloc] initWithArtboard: raw context: [self pluginContext]]; 54 | } 55 | 56 | #pragma mark - 57 | 58 | + (void)setPluginContextDictionary: (NSDictionary *)contextDictionary; 59 | { 60 | STSketchPluginContext *context = [[STSketchPluginContext alloc] initWithData: contextDictionary]; 61 | objc_setAssociatedObject(self, @selector(pluginContext), context, OBJC_ASSOCIATION_RETAIN); 62 | } 63 | 64 | + (instancetype)pluginContext 65 | { 66 | id context = objc_getAssociatedObject(self, @selector(pluginContext)); 67 | NSAssert(context != nil, @"You must set pluginContext via [%@ setPluginContext:] method before calling any other methods of this class", [self class]); 68 | return context; 69 | } 70 | 71 | #pragma mark - 72 | 73 | + (void)toggleStatesPluginName 74 | { 75 | NSMenu *pluginsMenu = [[NSApp menu] itemWithTitle: @"Plugins"].submenu; 76 | NSMenuItem *currentStatesItem = nil; 77 | if ((currentStatesItem = [pluginsMenu itemWithTitle: @"Show States"])) { 78 | currentStatesItem.title = @"Hide States"; 79 | } else if ((currentStatesItem = [pluginsMenu itemWithTitle: @"Hide States"])) { 80 | currentStatesItem.title = @"Show States"; 81 | } else { 82 | NSAssert(currentStatesItem, @"Could not find States plugin menu item inside Plugins menu"); 83 | } 84 | } 85 | 86 | #pragma mark - 87 | 88 | /// Inject ourselves into Sketch internals to receive notifications about artboard selection 89 | /// and document changes 90 | - (void)injectIntoMSDocument 91 | { 92 | Class MSDocument = NSClassFromString(@"MSDocument"); 93 | Class _MSLayer = NSClassFromString(@"_MSLayer"); 94 | Class MSPage = NSClassFromString(@"MSPage"); 95 | Class _MSImmutableLayer = NSClassFromString(@"_MSImmutableLayer"); 96 | 97 | [[NSNotificationCenter defaultCenter] addObserverForName: NSWindowWillCloseNotification 98 | object: [[STSketch currentDocument] window] 99 | queue: [NSOperationQueue mainQueue] 100 | usingBlock: ^(NSNotification * _Nonnull note) 101 | { 102 | for (id listener in [_listeners allObjects]) { 103 | [listener currentArtboardUnselected]; 104 | } 105 | }]; 106 | 107 | SEL currentArtboardDidChangeSelector = NSSelectorFromString(@"currentArtboardDidChange"); 108 | [MSDocument aspect_hookSelector: currentArtboardDidChangeSelector withOptions: AspectPositionAfter usingBlock: ^(id aspectInfo) 109 | { 110 | NSAssert(aspectInfo.instance == [STSketch currentDocument], 111 | @"Unexpected artboard selection update from a secondary document"); 112 | for (id listener in [_listeners allObjects]) { 113 | [listener currentArtboardDidChange]; 114 | } 115 | } error: NULL]; 116 | 117 | [MSDocument aspect_hookSelector: @selector(windowDidBecomeKey:) withOptions: AspectPositionAfter usingBlock: ^(id aspectInfo) 118 | { 119 | // Wait until the next run loop iteration to let Sketch switch to a new document 120 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 121 | for (id listener in [_listeners allObjects]) { 122 | [listener currentArtboardDidChange]; 123 | } 124 | }); 125 | } error: NULL]; 126 | 127 | /// XXX 128 | SEL setCurrentArtboard = NSSelectorFromString(@"setCurrentArtboard:"); 129 | [MSPage aspect_hookSelector: setCurrentArtboard withOptions: AspectPositionAfter usingBlock: ^(id aspectInfo) 130 | { 131 | id artboard = [[aspectInfo arguments] firstObject]; 132 | for (id listener in [_listeners allObjects]) { 133 | if (!artboard) { 134 | [listener currentArtboardUnselected]; 135 | } else { 136 | [listener currentArtboardDidChange]; 137 | } 138 | } 139 | } error: NULL]; 140 | 141 | Class MSDocumentData = NSClassFromString(@"MSDocumentData"); 142 | SEL changeSelectionTo = NSSelectorFromString(@"changeSelectionTo:"); 143 | [MSDocumentData aspect_hookSelector: changeSelectionTo withOptions: AspectPositionAfter usingBlock: ^(id aspectInfo) 144 | { 145 | NSArray *selection = [[aspectInfo arguments] firstObject]; 146 | if (![selection isKindOfClass: [NSArray class]]) { 147 | return; 148 | } 149 | for (id listener in [_listeners allObjects]) { 150 | if (selection.count != 0) { 151 | [listener currentArtboardDidChange]; 152 | } 153 | } 154 | } error: NULL]; 155 | 156 | 157 | /// XXX 158 | void (^documentUpdateHandler)(void) = ^(void) { 159 | for (id listener in [_listeners allObjects]) { 160 | [listener currentDocumentUpdated]; 161 | } 162 | }; 163 | 164 | // XXX 165 | SEL layerPositionPossiblyChanged = NSSelectorFromString(@"layerPositionPossiblyChanged"); 166 | [MSDocument aspect_hookSelector: layerPositionPossiblyChanged withOptions: AspectPositionAfter usingBlock: ^(id aspectInfo) 167 | { 168 | id doc = aspectInfo.instance; 169 | if ([[doc currentPage] currentArtboard] == [[STSketch currentPage] currentArtboard]) { 170 | documentUpdateHandler(); 171 | } 172 | } error: NULL]; 173 | 174 | // XXX 175 | [MSDocument aspect_hookSelector: NSSelectorFromString(@"undoAction:") withOptions: AspectPositionAfter usingBlock: ^(id aspectInfo) 176 | { 177 | id doc = aspectInfo.instance; 178 | if ([[doc currentPage] currentArtboard] == [[STSketch currentPage] currentArtboard]) { 179 | documentUpdateHandler(); 180 | } 181 | } error: NULL]; 182 | 183 | /// XXX 184 | SEL setIsVisible = NSSelectorFromString(@"setIsVisible:"); 185 | [_MSImmutableLayer aspect_hookSelector: setIsVisible withOptions: AspectPositionAfter usingBlock: ^(id aspectInfo) 186 | { 187 | documentUpdateHandler(); 188 | } error: NULL]; 189 | 190 | [_MSLayer aspect_hookSelector: setIsVisible withOptions: AspectPositionAfter usingBlock: ^(id aspectInfo) 191 | { 192 | documentUpdateHandler(); 193 | } error: NULL]; 194 | } 195 | 196 | @end 197 | -------------------------------------------------------------------------------- /Plugin/States/STSketchPluginContext.h: -------------------------------------------------------------------------------- 1 | // SketchPluginContext.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | @import Foundation; 8 | #import "STDocument.h" 9 | #import "STCommand.h" 10 | 11 | /// Encapsulate a Sketch plugin context dictionary 12 | @interface STSketchPluginContext : NSObject 13 | 14 | @property (readonly, strong) id pluginBundle; 15 | @property (readonly, strong) id document; 16 | @property (readonly, strong) id command; 17 | 18 | - (instancetype)initWithData: (NSDictionary *)data; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /Plugin/States/STSketchPluginContext.m: -------------------------------------------------------------------------------- 1 | // SketchPluginContext.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "STSketchPluginContext.h" 8 | 9 | @interface STSketchPluginContext() 10 | @property (readwrite, strong) id pluginBundle; 11 | @property (readwrite, strong) id document; 12 | @property (readwrite, strong) id command; 13 | @end 14 | 15 | @implementation STSketchPluginContext 16 | 17 | - (instancetype)initWithData: (NSDictionary *)data 18 | { 19 | if ((self = [super init])) { 20 | _pluginBundle = data[@"plugin"]; 21 | _document = data[@"document"]; 22 | _command = data[@"command"]; 23 | } 24 | return self; 25 | } 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /Plugin/States/STStateDescription.h: -------------------------------------------------------------------------------- 1 | // STStateDescription.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | @import Foundation; 8 | 9 | /// Represents a State model. Each state has a title and an unique identifier. 10 | @interface STStateDescription : NSObject 11 | 12 | @property (readonly, copy) NSString *title; 13 | @property (readonly, copy) NSUUID *UUID; 14 | 15 | /// Returns a new state description with the given title and random UUID 16 | - (instancetype)initWithTitle: (NSString *)title; 17 | /// Returns a new state description from the given dictionary. 18 | /// Expected keys: "title" and "UUID". 19 | - (instancetype)initWithDictionary: (NSDictionary *)dictionaryRepresentation; 20 | 21 | /// Returns a copy of the current state with the same UUID but different title. You're supposed 22 | /// to replace all copies of the old state with the new one. 23 | - (instancetype)stateByAlteringTitle: (NSString *)title; 24 | /// Returns a new state with random UUID and title equal to the current state's title with " Copy" suffix 25 | - (instancetype)duplicate; 26 | 27 | /// Returns a dictionary representation of this state model 28 | - (NSDictionary *)dictionaryRepresentation; 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /Plugin/States/STStateDescription.m: -------------------------------------------------------------------------------- 1 | // STStateDescription.m 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "STStateDescription.h" 8 | 9 | @interface STStateDescription() 10 | @property (readwrite, copy) NSString *title; 11 | @property (readwrite, copy) NSUUID *UUID; 12 | @end 13 | 14 | @implementation STStateDescription 15 | 16 | - (instancetype)initWithTitle: (NSString *)title 17 | { 18 | if ((self = [super init])) { 19 | self.UUID = [NSUUID UUID]; 20 | self.title = title; 21 | } 22 | return self; 23 | } 24 | 25 | - (instancetype)initWithTitle: (NSString *)title UUID: (NSUUID *)UUID 26 | { 27 | if ((self = [self initWithTitle: title])) { 28 | self.UUID = UUID; 29 | } 30 | return self; 31 | } 32 | 33 | - (instancetype)initWithDictionary: (NSDictionary *)dictionaryRepresentation 34 | { 35 | NSParameterAssert(dictionaryRepresentation[@"title"] != nil); 36 | NSParameterAssert(dictionaryRepresentation[@"UUID"] != nil); 37 | 38 | NSString *title = dictionaryRepresentation[@"title"]; 39 | NSUUID *UUID = [[NSUUID alloc] initWithUUIDString: dictionaryRepresentation[@"UUID"]]; 40 | 41 | return [self initWithTitle: title UUID: UUID]; 42 | } 43 | 44 | - (NSDictionary *)dictionaryRepresentation 45 | { 46 | return @{ 47 | @"title": self.title, 48 | @"UUID" : self.UUID.UUIDString 49 | }; 50 | } 51 | 52 | - (instancetype)stateByAlteringTitle: (NSString *)title 53 | { 54 | STStateDescription *newState = [[STStateDescription alloc] initWithTitle: title]; 55 | newState.UUID = self.UUID; 56 | return newState; 57 | } 58 | 59 | - (instancetype)duplicate 60 | { 61 | return [[STStateDescription alloc] initWithTitle: self.title]; 62 | } 63 | 64 | - (BOOL)isEqual: (id)object 65 | { 66 | typeof(self) another = object; 67 | 68 | if (![another isKindOfClass: [self class]]) { 69 | return NO; 70 | } 71 | 72 | if (![another.UUID isEqual: self.UUID]) { 73 | return NO; 74 | } 75 | 76 | if (![another.title isEqualToString: self.title]) { 77 | return NO; 78 | } 79 | return YES; 80 | } 81 | 82 | - (NSUInteger)hash 83 | { 84 | return self.UUID.hash + self.title.hash; 85 | } 86 | 87 | - (NSString *)description 88 | { 89 | return [NSString stringWithFormat: @"<%@: %p> (UUID = %@, title = \"%@\" @ %p)", 90 | NSStringFromClass([self class]), (void *)self, self.UUID.UUIDString, self.title, (void *)self.title]; 91 | } 92 | 93 | @end 94 | -------------------------------------------------------------------------------- /Plugin/States/STStatefulArtboard+Backend.h: -------------------------------------------------------------------------------- 1 | // STStatefulArtboard+Backend.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "STStatefulArtboard.h" 8 | 9 | /// STStatefulArtboard extension that allows to save data inside Sketch metadata 10 | @interface STStatefulArtboard (Backend) 11 | 12 | - (nonnull NSArray *)artboardStatesData; 13 | - (void)setArtboardStatesData: (nonnull NSArray *)newData; 14 | 15 | - (nonnull NSDictionary *)artboardCurrentStateData; 16 | - (void)setArtboardCurrentStateData: (nonnull NSDictionary *)newData; 17 | 18 | - (nonnull NSDictionary *)metadataForLayer: (nonnull id )layer; 19 | - (void)setMedatada: (nonnull NSDictionary *)newMetadata forLayer: (nonnull id )layer; 20 | 21 | - (nonnull NSDictionary *)artboardDefaultStateData; 22 | - (void)setArtboardDefaultStateData: (nonnull NSDictionary *)newData; 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /Plugin/States/STStatefulArtboard+Backend.m: -------------------------------------------------------------------------------- 1 | // STStatefulArtboard+Backend.m 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "STStatefulArtboard+Backend.h" 8 | 9 | static NSString const *const kSTStatefulArtboardStatesKey = @"x-states-states"; 10 | static NSString const *const kSTStatefulArtboardStateValuesKey = @"x-states-state-values"; 11 | static NSString const *const kSTStatefulArtboardCurrentStateKey = @"x-states-current-state"; 12 | static NSString const *const kSTStatefulArtboardDefaultStateKey = @"x-states-default-state"; 13 | 14 | @implementation STStatefulArtboard (Backend) 15 | 16 | - (NSArray *)artboardStatesData 17 | { 18 | return [[self.context command] valueForKey: kSTStatefulArtboardStatesKey onLayer: _internal] ?: @[]; 19 | } 20 | 21 | - (void)setArtboardStatesData: (NSArray *)newData 22 | { 23 | [[self.context command] setValue: newData forKey: kSTStatefulArtboardStatesKey onLayer: _internal]; 24 | } 25 | 26 | - (NSDictionary *)artboardCurrentStateData 27 | { 28 | return [[self.context command] valueForKey: kSTStatefulArtboardCurrentStateKey onLayer: _internal] ?: @{}; 29 | } 30 | 31 | - (void)setArtboardCurrentStateData: (NSDictionary *)newData 32 | { 33 | [[self.context command] setValue: newData forKey: kSTStatefulArtboardCurrentStateKey onLayer: _internal]; 34 | } 35 | 36 | - (NSDictionary *)metadataForLayer: (id )layer 37 | { 38 | return [[self.context command] valueForKey: kSTStatefulArtboardStateValuesKey onLayer: layer] ?: @{}; 39 | } 40 | 41 | - (void)setMedatada: (NSDictionary *)newMetadata forLayer: (id )layer 42 | { 43 | [[self.context command] setValue: newMetadata forKey: kSTStatefulArtboardStateValuesKey onLayer: layer]; 44 | } 45 | 46 | - (nonnull NSDictionary *)artboardDefaultStateData 47 | { 48 | return [[self.context command] valueForKey: kSTStatefulArtboardDefaultStateKey onLayer: _internal] ?: @{}; 49 | } 50 | 51 | - (void)setArtboardDefaultStateData: (nonnull NSDictionary *)newData 52 | { 53 | [[self.context command] setValue: newData forKey: kSTStatefulArtboardDefaultStateKey onLayer: _internal]; 54 | } 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /Plugin/States/STStatefulArtboard+Snapshots.h: -------------------------------------------------------------------------------- 1 | // STStatefulArtboard+Snapshots.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "STStatefulArtboard.h" 8 | 9 | @interface STStatefulArtboard (Snapshots) 10 | 11 | /// Returns a new artboard reflecting the given state. All states metadata will be lost (i.e. it 12 | /// will be "clean" snapshot) 13 | - (id )snapshotForState: (STStateDescription *)state; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Plugin/States/STStatefulArtboard+Snapshots.m: -------------------------------------------------------------------------------- 1 | // STStatefulArtboard+Snapshots.m 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "STLayerState.h" 8 | #import "NSArray+HigherOrder.h" 9 | #import "STStatefulArtboard+Backend.h" 10 | #import "STStatefulArtboard+Snapshots.h" 11 | 12 | @implementation STStatefulArtboard (Snapshots) 13 | 14 | - (id )snapshotForState: (STStateDescription *)state 15 | { 16 | NSParameterAssert([self.allStates containsObject: state]); 17 | 18 | id snapshotInternal = [_internal copy]; 19 | snapshotInternal.name = state.title; 20 | 21 | STStatefulArtboard *snapshot = [[STStatefulArtboard alloc] initWithArtboard: snapshotInternal 22 | context: self.context]; 23 | 24 | [snapshot applyState: state]; 25 | [snapshot removeAllStates]; 26 | 27 | return snapshotInternal; 28 | } 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /Plugin/States/STStatefulArtboard.h: -------------------------------------------------------------------------------- 1 | // StatefulArtboard.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | 8 | @import Foundation; 9 | #import "STStateDescription.h" 10 | #import "STSketchPluginContext.h" 11 | #import "STArtboard.h" 12 | 13 | /// A wrapper around Sketch's artboard which provides methods for manipulating its state 14 | @interface STStatefulArtboard : NSObject 15 | { 16 | @protected 17 | id _internal; 18 | } 19 | @property (readonly, strong) STSketchPluginContext *context; 20 | @property (readonly, strong) NSArray *allStates; 21 | @property (readonly, strong) STStateDescription *currentState; 22 | @property (readonly, strong) STStateDescription *defaultState; 23 | 24 | - (instancetype)initWithArtboard: (id )artboard context: (STSketchPluginContext *)context; 25 | 26 | /// Verifies that all of this artboard's child layers conforms to the given state model 27 | - (BOOL)conformsToState: (STStateDescription *)state; 28 | 29 | /// Restore artboard state from `state` 30 | - (void)applyState: (STStateDescription *)state; 31 | 32 | /// Save current artboard state 33 | - (void)updateCurrentState; 34 | 35 | /// Inserts a new state model into this artboard's metadata. This new state model will represent 36 | /// the current state of the artboard 37 | - (void)insertNewState: (STStateDescription *)newState; 38 | 39 | /// Rewrites all child layers attribites so that the `destination` state becomes equal to the `source` one 40 | - (void)copyState: (STStateDescription *)source toState: (STStateDescription *)destination; 41 | 42 | /// Update the given state's name in this artboard's metadata 43 | - (STStateDescription *)updateName: (NSString *)newName forState: (STStateDescription *)existingState; 44 | 45 | /// Changes the order of the states in this artboard. A passed array must include all of the states 46 | /// of this artboard and nothing else 47 | - (void)reorderStates: (NSArray *)allStatesInNewOrder; 48 | 49 | /// Completely removes the given state from this artboard 50 | - (void)removeState: (STStateDescription *)stateToRemove; 51 | 52 | /// Wipes all of the states 53 | - (void)removeAllStates; 54 | 55 | /// WARNING: you're not suppposed to call this method. It's here just so -[StatesContoller createNewState:] 56 | /// may call it and workaround a major performance issue with applying states on really big artboards. 57 | /// Eventually this method will go away. 58 | - (void)setCurrentState: (STStateDescription *)currentState; 59 | 60 | @end 61 | -------------------------------------------------------------------------------- /Plugin/States/STStatefulArtboard.m: -------------------------------------------------------------------------------- 1 | // StatefulArtboard.m 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "STStatefulArtboard.h" 8 | #import "STStatefulArtboard+Backend.h" 9 | #import "STLayerState.h" 10 | #import "NSArray+HigherOrder.h" 11 | 12 | #define kArtboardDefaultStateTitle @"Initial State" 13 | 14 | @implementation STStatefulArtboard 15 | 16 | - (instancetype)initWithArtboard: (id )artboard context: (STSketchPluginContext *)context 17 | { 18 | NSParameterAssert(artboard != nil); 19 | NSParameterAssert(context != nil); 20 | 21 | if ((self = [super init])) { 22 | _internal = artboard; 23 | _context = context; 24 | [self createDefaultStateIfNeeded]; 25 | } 26 | return self; 27 | } 28 | 29 | - (void)createDefaultStateIfNeeded 30 | { 31 | if (self.allStates.count > 0) { 32 | // Backwards compatibility 33 | if (!self.defaultState) { 34 | [self setDefaultState: self.allStates.firstObject]; 35 | } 36 | } else { 37 | STStateDescription *defaultState = [[STStateDescription alloc] initWithTitle: kArtboardDefaultStateTitle]; 38 | [self insertNewState: defaultState]; 39 | [self setCurrentState: defaultState]; 40 | [self setDefaultState: defaultState]; 41 | } 42 | } 43 | 44 | #pragma mark - STLayer 45 | 46 | - (NSArray > *)children 47 | { 48 | return [[_internal children] st_filter: ^BOOL(id child) { 49 | return [child class] != NSClassFromString(@"MSArtboardGroup"); 50 | }]; 51 | } 52 | 53 | #pragma mark - Actions 54 | 55 | - (BOOL)conformsToState: (STStateDescription *)state 56 | { 57 | if (![self.allStates containsObject: state]) { 58 | return NO; 59 | } 60 | 61 | __block BOOL result = YES; 62 | [[self children] enumerateObjectsUsingBlock: ^(id layer, NSUInteger idx, BOOL *stop) { 63 | NSDictionary *metadata = [self metadataForLayer: layer][state.UUID.UUIDString]; 64 | if (metadata.count == 0) { 65 | result = NO; *stop = YES; 66 | return; 67 | } 68 | STLayerState *layerState = [[STLayerState alloc] initWithDictionary: metadata]; 69 | if (!layerState || ![STLayerStateExaminer layer: layer conformsToState: layerState]) { 70 | result = NO; *stop = YES; 71 | } 72 | }]; 73 | return result; 74 | } 75 | 76 | - (void)removeAllStates 77 | { 78 | [self setArtboardStatesData: @[]]; 79 | [self setArtboardCurrentStateData: @{}]; 80 | [self setArtboardDefaultStateData: @{}]; 81 | [[self children] enumerateObjectsUsingBlock: ^(id layer, NSUInteger idx, BOOL *stop) { 82 | [self setMedatada: @{} forLayer: layer]; 83 | }]; 84 | // Re-create the initial state 85 | [self createDefaultStateIfNeeded]; 86 | } 87 | 88 | - (void)removeState: (STStateDescription *)stateToRemove 89 | { 90 | NSParameterAssert(stateToRemove != nil); 91 | 92 | if ([stateToRemove isEqual: self.currentState]) { 93 | NSInteger idx = [self.allStates indexOfObject: self.currentState]; 94 | NSInteger previousStateIdx = idx - 1; 95 | NSInteger nextStateIdx = idx + 1; 96 | if (previousStateIdx >= 0) { 97 | [self applyState: self.allStates[previousStateIdx]]; 98 | } else if (nextStateIdx < self.allStates.count) { 99 | [self applyState: self.allStates[nextStateIdx]]; 100 | } else { 101 | [self setArtboardCurrentStateData: @{}]; 102 | } 103 | } 104 | 105 | // 1) Remove from artboard state descriptions 106 | NSArray *statesToKeep = [[self artboardStatesData] st_filter: ^BOOL(NSDictionary *item) { 107 | return [item isNotEqualTo: stateToRemove.dictionaryRepresentation]; 108 | }]; 109 | [self setArtboardStatesData: statesToKeep]; 110 | // 2) Remove this state's metadata from layers 111 | [[self children] enumerateObjectsUsingBlock: ^(id layer, NSUInteger idx, BOOL *stop) { 112 | NSDictionary *metadata = [self metadataForLayer: layer]; 113 | NSArray *keysToKeep = [metadata.allKeys st_filter: ^BOOL(NSString *key) { 114 | return [key isNotEqualTo: stateToRemove.UUID.UUIDString]; 115 | }]; 116 | [self setMedatada: [metadata dictionaryWithValuesForKeys: keysToKeep] forLayer: layer]; 117 | }]; 118 | } 119 | 120 | - (void)applyState: (STStateDescription *)state 121 | { 122 | NSParameterAssert([self.allStates containsObject: state]); 123 | 124 | [[self children] enumerateObjectsUsingBlock: ^(id layer, NSUInteger idx, BOOL *stop) { 125 | NSDictionary *metadata = [self metadataForLayer: layer][state.UUID.UUIDString]; 126 | if (metadata.count == 0) { 127 | return; 128 | } 129 | STLayerState *layerState = [[STLayerState alloc] initWithDictionary: metadata]; 130 | NSAssert(layerState != nil, @"Requested state values are missing from layer's metadata"); 131 | [STLayerStateApplier apply: layerState toLayer: layer]; 132 | }]; 133 | 134 | self.currentState = state; 135 | } 136 | 137 | - (void)updateCurrentState 138 | { 139 | STStateDescription *state = self.currentState; 140 | NSParameterAssert(self.currentState != nil); 141 | 142 | [[self children] enumerateObjectsUsingBlock: ^(id layer, NSUInteger idx, BOOL *stop) { 143 | NSMutableDictionary *newMetadata = [[self metadataForLayer: layer] mutableCopy]; 144 | STLayerState *layerState = [STLayerStateFetcher fetchStateFromLayer: layer]; 145 | newMetadata[state.UUID.UUIDString] = [layerState dictionaryRepresentation]; 146 | [self setMedatada: newMetadata forLayer: layer]; 147 | }]; 148 | } 149 | 150 | - (void)copyState: (STStateDescription *)source toState: (STStateDescription *)destination 151 | { 152 | NSParameterAssert([self.allStates containsObject: source]); 153 | NSParameterAssert([self.allStates containsObject: destination]); 154 | 155 | // Copy all of the child layers metadata from `source` state to `destination` 156 | [[self children] enumerateObjectsUsingBlock: ^(id layer, NSUInteger idx, BOOL *stop) { 157 | NSMutableDictionary *newMetadata = [[self metadataForLayer: layer] mutableCopy]; 158 | NSAssert(newMetadata[source.UUID.UUIDString], @"The source state metadata doesn't exists on layer %@", layer); 159 | newMetadata[destination.UUID.UUIDString] = newMetadata[source.UUID.UUIDString]; 160 | [self setMedatada: newMetadata forLayer: layer]; 161 | }]; 162 | } 163 | 164 | - (void)insertNewState: (STStateDescription *)newState 165 | { 166 | NSParameterAssert(![self.allStates containsObject: newState]); 167 | 168 | // 1) insert this new state into the artboard's registry 169 | NSArray *oldRawStates = [self artboardStatesData]; 170 | [self setArtboardStatesData: [oldRawStates arrayByAddingObject: newState.dictionaryRepresentation]]; 171 | 172 | // 2) update all child layer with the new state: it will be a current layer snapshot 173 | [[self children] enumerateObjectsUsingBlock: ^(id layer, NSUInteger idx, BOOL *stop) { 174 | // TODO: this is the same code as in -updateCurrentState (just replace state <-> newState) 175 | NSMutableDictionary *newMetadata = [[self metadataForLayer: layer] mutableCopy]; 176 | STLayerState *layerState = [STLayerStateFetcher fetchStateFromLayer: layer]; 177 | newMetadata[newState.UUID.UUIDString] = [layerState dictionaryRepresentation]; 178 | [self setMedatada: newMetadata forLayer: layer]; 179 | }]; 180 | 181 | if (!self.currentState) { 182 | [self setCurrentState: newState]; 183 | } 184 | } 185 | 186 | - (STStateDescription *)updateName: (NSString *)newName forState: (STStateDescription *)oldState 187 | { 188 | NSParameterAssert([self.allStates containsObject: oldState]); 189 | 190 | STStateDescription *newState = [oldState stateByAlteringTitle: newName]; 191 | NSMutableArray *stateRegistry = [[self artboardStatesData] mutableCopy]; 192 | NSUInteger idx = [stateRegistry indexOfObject: oldState.dictionaryRepresentation]; 193 | NSAssert(idx != NSNotFound, @"Could not find the given state"); 194 | // Modify a states registry 195 | [stateRegistry replaceObjectAtIndex: idx withObject: newState.dictionaryRepresentation]; 196 | [self setArtboardStatesData: stateRegistry]; 197 | // What if we rename the default state? 198 | if ([oldState isEqual: self.defaultState]) { 199 | [self updateDefaultState: newState]; 200 | } 201 | // Also update the current state if needed 202 | if ([oldState isEqual: self.currentState]) { 203 | [self setCurrentState: newState]; 204 | } 205 | 206 | return newState; 207 | } 208 | 209 | - (void)reorderStates: (NSArray *)allStatesInNewOrder 210 | { 211 | NSAssert([[NSSet setWithArray: self.allStates] isEqualToSet: [NSSet setWithArray: allStatesInNewOrder]], 212 | @"Invalid argument"); 213 | [self setAllStates: allStatesInNewOrder]; 214 | } 215 | 216 | #pragma mark - Artboard State Metadata 217 | 218 | - (NSArray *)allStates 219 | { 220 | return [[self artboardStatesData] st_map: ^STStateDescription *(NSDictionary *model) { 221 | return [[STStateDescription alloc] initWithDictionary: model]; 222 | }]; 223 | } 224 | 225 | - (STStateDescription *)currentState 226 | { 227 | NSDictionary *currentStateData = [self artboardCurrentStateData]; 228 | if (currentStateData.count == 0) { 229 | return nil; 230 | } 231 | return [[STStateDescription alloc] initWithDictionary: currentStateData]; 232 | } 233 | 234 | - (STStateDescription *)defaultState 235 | { 236 | NSDictionary *defaultStateDictionary = [self artboardDefaultStateData]; 237 | if (defaultStateDictionary.count == 0) { 238 | return nil; 239 | } 240 | return [[STStateDescription alloc] initWithDictionary: defaultStateDictionary]; 241 | } 242 | 243 | #pragma mark - Internal Metadata 244 | 245 | - (void)setAllStates: (NSArray *)allStates 246 | { 247 | NSArray *rawStates = [allStates st_map: ^NSDictionary *(STStateDescription *state) { 248 | return [state dictionaryRepresentation]; 249 | }]; 250 | [self setArtboardStatesData: rawStates]; 251 | } 252 | 253 | - (void)setCurrentState: (STStateDescription *)newCurrentState 254 | { 255 | NSParameterAssert([self.allStates containsObject: newCurrentState]); 256 | NSDictionary *state = [newCurrentState dictionaryRepresentation]; 257 | [self setArtboardCurrentStateData: state]; 258 | } 259 | 260 | - (void)setDefaultState: (STStateDescription *)defaultState 261 | { 262 | NSParameterAssert(self.defaultState == nil); 263 | NSDictionary *stateDictionary = [defaultState dictionaryRepresentation]; 264 | [self setArtboardDefaultStateData: stateDictionary]; 265 | } 266 | 267 | - (void)updateDefaultState: (STStateDescription *)defaultState 268 | { 269 | NSDictionary *stateDictionary = [defaultState dictionaryRepresentation]; 270 | [self setArtboardDefaultStateData: stateDictionary]; 271 | } 272 | 273 | @end 274 | -------------------------------------------------------------------------------- /Plugin/States/STTableCellView.h: -------------------------------------------------------------------------------- 1 | // STTableCellView.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | 8 | @import Cocoa; 9 | 10 | #import "STUpdateButton.h" 11 | 12 | @class STTableCellView; 13 | 14 | @protocol STTableCellViewDelegate 15 | @required 16 | - (BOOL)cellViewRepresentsCurrentItem: (STTableCellView *)cellView; 17 | - (BOOL)isSingleRowSelected; 18 | @end 19 | 20 | /// A cell view that sets custom text field colors depending on whether it represents the current 21 | /// state model or not 22 | @interface STTableCellView : NSTableCellView 23 | 24 | @property (weak) id delegate; 25 | @property (weak) IBOutlet STUpdateButton *updateButton; 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /Plugin/States/STTableCellView.m: -------------------------------------------------------------------------------- 1 | // STTableCellView.m 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | 8 | #import "STColorFactory.h" 9 | #import "STTableCellView.h" 10 | 11 | @implementation STTableCellView 12 | 13 | - (void)setBackgroundStyle: (NSBackgroundStyle)backgroundStyle 14 | { 15 | [super setBackgroundStyle: backgroundStyle]; 16 | 17 | if (backgroundStyle == NSBackgroundStyleLight) { 18 | self.textField.textColor = [STColorFactory tableViewCellTextRegularColor]; 19 | } else { 20 | BOOL singleSelection = [self.delegate isSingleRowSelected]; 21 | 22 | if (singleSelection || [self.delegate cellViewRepresentsCurrentItem: self]) { 23 | self.textField.textColor = [STColorFactory tableViewCellTextSelectedColorWithAlpha: 1.0f]; 24 | } else { 25 | self.textField.textColor = [STColorFactory tableViewCellTextSelectedColorWithAlpha: 0.5f]; 26 | } 27 | } 28 | } 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /Plugin/States/STTableRowView.h: -------------------------------------------------------------------------------- 1 | // STTableRowView.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | @import Cocoa; 8 | 9 | /// A row view that draws custom background and selection rectangles 10 | @interface STTableRowView : NSTableRowView 11 | 12 | @property (readonly, weak) NSTableView *tableView; 13 | 14 | - (instancetype)initWithTableView: (NSTableView *)containingTableView; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Plugin/States/STTableRowView.m: -------------------------------------------------------------------------------- 1 | // STTableRowView.m 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "STColorFactory.h" 8 | #import "STTableRowView.h" 9 | #import "STTableCellView.h" 10 | 11 | @interface STTableRowView() 12 | @property (readwrite, weak) NSTableView *tableView; 13 | @end 14 | 15 | @implementation STTableRowView 16 | 17 | - (instancetype)initWithTableView: (NSTableView *)containingTableView 18 | { 19 | if ((self = [super initWithFrame: NSZeroRect])) { 20 | _tableView = containingTableView; 21 | } 22 | return self; 23 | } 24 | 25 | - (void)drawBackgroundInRect: (NSRect)dirtyRect 26 | { 27 | [super drawBackgroundInRect: dirtyRect]; 28 | NSInteger row = [self.tableView rowForView: self]; 29 | if (row % 2 == 0) { 30 | [[STColorFactory mainTableViewRowColor] setFill]; 31 | } else { 32 | [[STColorFactory secondaryTableViewRowColor] setFill]; 33 | } 34 | NSBezierPath *path = [NSBezierPath bezierPathWithRect: dirtyRect]; 35 | [path fill]; 36 | } 37 | 38 | - (void)drawSelectionInRect: (NSRect)dirtyRect 39 | { 40 | [super drawBackgroundInRect: dirtyRect]; 41 | if (self.emphasized) { 42 | [[STColorFactory selectedTableViewRowColor] setFill]; 43 | } else { 44 | [[STColorFactory selectedInactiveTableViewRowColor] setFill]; 45 | } 46 | NSBezierPath *path = [NSBezierPath bezierPathWithRect: dirtyRect]; 47 | [path fill]; 48 | } 49 | 50 | @end 51 | -------------------------------------------------------------------------------- /Plugin/States/STTableView.h: -------------------------------------------------------------------------------- 1 | // STTableView.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | @import Cocoa; 8 | 9 | /// A table view that disables navigation with arrow keys and draws a custom background 10 | @interface STTableView : NSTableView 11 | @end 12 | -------------------------------------------------------------------------------- /Plugin/States/STTableView.m: -------------------------------------------------------------------------------- 1 | // STTableView.m 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | 8 | #import "STColorFactory.h" 9 | #import "STTableView.h" 10 | 11 | @implementation STTableView 12 | 13 | - (void)keyDown: (NSEvent *)theEvent 14 | { 15 | NSString *characters = [theEvent charactersIgnoringModifiers]; 16 | unichar code = [characters characterAtIndex: 0]; 17 | // Disable arrow keys navigation 18 | switch (code) { 19 | case NSUpArrowFunctionKey: 20 | case NSDownArrowFunctionKey: 21 | case NSLeftArrowFunctionKey: 22 | case NSRightArrowFunctionKey: 23 | return; 24 | default: 25 | [super keyDown: theEvent]; 26 | } 27 | } 28 | 29 | - (void)drawBackgroundInClipRect: (NSRect)clipRect 30 | { 31 | [super drawBackgroundInClipRect: clipRect]; 32 | 33 | [[STColorFactory tableViewBackgroundColor] setFill]; 34 | NSBezierPath *path = [NSBezierPath bezierPathWithRect: clipRect]; 35 | [path fill]; 36 | } 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /Plugin/States/STTextField.h: -------------------------------------------------------------------------------- 1 | // STTextField.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | 8 | @import Cocoa; 9 | 10 | @protocol STTextFieldFirstResponderDelegate 11 | @optional 12 | - (void)textFieldBecomeFirstResponder: (NSTextField *)textField; 13 | @end 14 | 15 | /// A text field that notifies its delegate that it has became firt responder 16 | @interface STTextField : NSTextField 17 | 18 | @property (weak) id firstResponderDelegate; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /Plugin/States/STTextField.m: -------------------------------------------------------------------------------- 1 | // STTextField.m 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "STColorFactory.h" 8 | #import "STTextField.h" 9 | 10 | @implementation STTextField 11 | 12 | - (BOOL)becomeFirstResponder 13 | { 14 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 15 | self.textColor = [STColorFactory tableViewCellTextRegularColor]; 16 | }); 17 | 18 | BOOL result = [super becomeFirstResponder]; 19 | if (result && [self.delegate respondsToSelector: @selector(textFieldBecomeFirstResponder:)]) { 20 | [self.firstResponderDelegate textFieldBecomeFirstResponder: self]; 21 | } 22 | return result; 23 | } 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /Plugin/States/STUpdateButton.h: -------------------------------------------------------------------------------- 1 | // STUpdateButton.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | @import Cocoa; 8 | 9 | typedef void (^STUpdateButtonAnimationCompletion)(void); 10 | 11 | /// A simple button that may rotate its image clockwise 12 | @interface STUpdateButton : NSButton 13 | 14 | - (void)spinWithCompletion: (STUpdateButtonAnimationCompletion)completion; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Plugin/States/STUpdateButton.m: -------------------------------------------------------------------------------- 1 | // STUpdateButton.m 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | @import QuartzCore; 8 | 9 | #import "STUpdateButton.h" 10 | 11 | @interface STUpdateButton() 12 | { 13 | STUpdateButtonAnimationCompletion _completion; 14 | } 15 | @end 16 | 17 | @implementation STUpdateButton 18 | 19 | - (instancetype)init 20 | { 21 | if ((self = [super init])) { 22 | self.wantsLayer = YES; 23 | } 24 | return self; 25 | } 26 | 27 | 28 | - (void)spinWithCompletion: (STUpdateButtonAnimationCompletion)completion 29 | { 30 | if (!CGPointEqualToPoint(self.layer.anchorPoint, CGPointMake(0.5, 0.5))) { 31 | [self fixAnchorPoint]; 32 | } 33 | // Rotate 360° clockwise 34 | CABasicAnimation *spinningAnimation = [CABasicAnimation animationWithKeyPath: @"transform.rotation"]; 35 | spinningAnimation.fromValue = @(0.0f); 36 | spinningAnimation.toValue = @(-2 * M_PI); 37 | spinningAnimation.duration = 0.5f; 38 | spinningAnimation.delegate = self; 39 | _completion = (__bridge STUpdateButtonAnimationCompletion)(_Block_copy((__bridge const void *)(completion))); 40 | [self.layer addAnimation: spinningAnimation forKey: nil]; 41 | } 42 | 43 | - (void)animationDidStop: (CAAnimation *)animation finished: (BOOL)flag 44 | { 45 | if (_completion) { 46 | _completion(); 47 | _Block_release((__bridge const void *)(_completion)); 48 | } 49 | } 50 | 51 | - (void)fixAnchorPoint 52 | { 53 | CGRect frame = self.layer.frame; 54 | CGPoint center = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame)); 55 | self.layer.position = center; 56 | self.layer.anchorPoint = CGPointMake(0.5, 0.5); 57 | } 58 | 59 | @end 60 | -------------------------------------------------------------------------------- /Plugin/States/STWindow.h: -------------------------------------------------------------------------------- 1 | // STWindow.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | @import Cocoa; 8 | 9 | /// A panel which is movable by its background 10 | @interface STWindow : NSPanel 11 | @end 12 | -------------------------------------------------------------------------------- /Plugin/States/STWindow.m: -------------------------------------------------------------------------------- 1 | // STWindow.m 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "STWindow.h" 8 | 9 | @implementation STWindow 10 | 11 | - (BOOL)canBecomeKeyWindow 12 | { 13 | return YES; 14 | } 15 | 16 | - (BOOL)isMovableByWindowBackground 17 | { 18 | return YES; 19 | } 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /Plugin/States/StatesController+ContextMenu.h: -------------------------------------------------------------------------------- 1 | // StatesController+ContextMenu.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "StatesController.h" 8 | 9 | /// A category that builds a context menu for selected rows 10 | @interface StatesController (ContextMenu) 11 | 12 | - (void)menuNeedsUpdate: (NSMenu *)menu; 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /Plugin/States/StatesController+ContextMenu.m: -------------------------------------------------------------------------------- 1 | // StatesController+ContextMenu.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "STStateDescription.h" 8 | #import "STStatefulArtboard.h" 9 | #import "StatesController+ContextMenu.h" 10 | 11 | @implementation StatesController (ContextMenu) 12 | 13 | - (void)menuNeedsUpdate: (NSMenu *)menu 14 | { 15 | [menu removeAllItems]; 16 | 17 | NSInteger clickedRow = [self.tableView clickedRow]; 18 | if (clickedRow < 0 || clickedRow >= _artboard.allStates.count) { 19 | return; 20 | } 21 | 22 | NSArray *selectedStates = [_artboard.allStates objectsAtIndexes: 23 | [self.tableView selectedRowIndexes]]; 24 | if (selectedStates.count == 0) { 25 | return; 26 | } 27 | 28 | STStateDescription *clickedState = _artboard.allStates[clickedRow]; 29 | // We're clicking an a row that isn't part of current selection: show a menu just for this one row 30 | if (![selectedStates containsObject: clickedState]) { 31 | selectedStates = @[clickedState]; 32 | } 33 | 34 | // TODO?: add support for updating non-current states as well. Need to figure out 35 | // when "updating" them means though. Maybe just rewriting them to reflect current artboard properties? 36 | if (selectedStates.count == 1 && [clickedState isEqualTo: _artboard.currentState]) { 37 | [menu addItem: [self updateCurrentStateMenuItem]]; 38 | } 39 | 40 | [menu addItem: [self duplicateMenuItemForStates: selectedStates]]; 41 | [menu addItem: [NSMenuItem separatorItem]]; 42 | [menu addItem: [self createPageMenuItemForStates: selectedStates]]; 43 | if (selectedStates.count > 1 || [selectedStates.firstObject isNotEqualTo: _artboard.defaultState]) { 44 | [menu addItem: [NSMenuItem separatorItem]]; 45 | [menu addItem: [self deleteMenuItemForStates: selectedStates]]; 46 | } 47 | } 48 | 49 | #pragma mark Menu Items 50 | 51 | - (NSMenuItem *)updateCurrentStateMenuItem 52 | { 53 | NSMenuItem *item = [[NSMenuItem alloc] initWithTitle: @"Update" 54 | action: @selector(updateCurrentState:) 55 | keyEquivalent: @""]; 56 | item.target = self; 57 | return item; 58 | } 59 | 60 | - (NSMenuItem *)duplicateMenuItemForStates: (NSArray *)subjects 61 | { 62 | NSMenuItem *item = [[NSMenuItem alloc] initWithTitle: @"Duplicate" 63 | action: @selector(duplicateStates:) 64 | keyEquivalent: @""]; 65 | item.target = self; 66 | item.representedObject = subjects; 67 | return item; 68 | } 69 | 70 | - (NSMenuItem *)createPageMenuItemForStates: (NSArray *)subjects 71 | { 72 | NSMenuItem *item = [[NSMenuItem alloc] initWithTitle: @"Create Page" 73 | action: @selector(createPageFromStates:) 74 | keyEquivalent: @""]; 75 | item.target = self; 76 | item.representedObject = subjects; 77 | return item; 78 | } 79 | 80 | - (NSMenuItem *)deleteMenuItemForStates: (NSArray *)subjects 81 | { 82 | NSMenuItem *item = [[NSMenuItem alloc] initWithTitle: @"Delete" 83 | action: @selector(deleteStates:) 84 | keyEquivalent: @""]; 85 | item.target = self; 86 | item.representedObject = subjects; 87 | return item; 88 | } 89 | 90 | @end 91 | -------------------------------------------------------------------------------- /Plugin/States/StatesController+Decisions.h: -------------------------------------------------------------------------------- 1 | // StatesController+Decisions.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "StatesController.h" 8 | 9 | @class STStateDescription; 10 | 11 | /// A category that asks user's confirmation for (likely) destructive events 12 | @interface StatesController (Decisions) 13 | 14 | - (BOOL)shouldSwitchToState: (STStateDescription *)newState fromState: (STStateDescription *)oldState; 15 | 16 | - (BOOL)shoulRemoveStates: (NSArray *)states; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /Plugin/States/StatesController+Decisions.m: -------------------------------------------------------------------------------- 1 | // StatesController+Decisions.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "STStateDescription.h" 8 | #import "STStatefulArtboard.h" 9 | #import "NSArray+HigherOrder.h" 10 | #import "StatesController+Decisions.h" 11 | 12 | #define kNumberOfStatesToShowInDeleteAlert (10) 13 | 14 | @implementation StatesController (Decisions) 15 | 16 | - (BOOL)shouldSwitchToState: (STStateDescription *)newState fromState: (STStateDescription *)oldState 17 | { 18 | // If there aren't any changes then the switch is safe 19 | if ([_artboard conformsToState: oldState]) { 20 | return YES; 21 | } 22 | 23 | // When we're switching to the same state it means we're to reset all of the changes 24 | // made to this state 25 | if ([oldState isEqual: newState]) { 26 | NSAlert *alert = [[NSAlert alloc] init]; 27 | alert.messageText = [NSString stringWithFormat: 28 | @"Do you want to revert any changes made to state \"%@\"?", oldState.title]; 29 | [alert addButtonWithTitle: @"Revert changes"]; 30 | [alert addButtonWithTitle: @"Cancel"]; 31 | 32 | NSModalResponse response = [alert runModal]; 33 | switch (response) { 34 | case NSAlertFirstButtonReturn: 35 | // "Revert": allow to re-apply this state 36 | return YES; 37 | case NSAlertSecondButtonReturn: 38 | // "Cancel": do nothing 39 | return NO; 40 | default: 41 | return NO; 42 | } 43 | } else { 44 | // Otherwise it's just a regular switch between different states 45 | NSAlert *alert = [[NSAlert alloc] init]; 46 | alert.messageText = [NSString stringWithFormat: 47 | @"Update changes to state \"%@\" before switching to \"%@\"?", 48 | oldState.title, newState.title]; 49 | [alert addButtonWithTitle: @"Update"]; 50 | [alert addButtonWithTitle: @"Cancel"]; 51 | [alert addButtonWithTitle: @"Don’t Update"]; 52 | 53 | NSModalResponse response = [alert runModal]; 54 | switch (response) { 55 | case NSAlertFirstButtonReturn: 56 | // "Update": update the current state and switch to a new one 57 | [_artboard updateCurrentState]; 58 | return YES; 59 | case NSAlertSecondButtonReturn: 60 | // "Cancel": do nothing 61 | return NO; 62 | case NSAlertThirdButtonReturn: 63 | // "Do not update": so to say, just switch to the new state 64 | return YES; 65 | default: 66 | return NO; 67 | } 68 | } 69 | } 70 | 71 | - (BOOL)shoulRemoveStates: (NSArray *)states 72 | { 73 | NSParameterAssert(states.count > 0); 74 | 75 | NSArray *titles = [states st_map: ^NSString *(STStateDescription *state) { 76 | return [NSString stringWithFormat: @"\t• %@", state.title]; 77 | }]; 78 | 79 | if (titles.count > kNumberOfStatesToShowInDeleteAlert) { 80 | NSInteger total = titles.count; 81 | titles = [titles subarrayWithRange: NSMakeRange(0, kNumberOfStatesToShowInDeleteAlert)]; 82 | titles = [titles arrayByAddingObject: [NSString stringWithFormat: @"\t(and %ld more)", 83 | total-kNumberOfStatesToShowInDeleteAlert]]; 84 | } 85 | 86 | NSAlert *alert = [[NSAlert alloc] init]; 87 | if (titles.count == 1) { 88 | alert.messageText = [NSString stringWithFormat: 89 | @"Do you want to delete state \"%@\"?", states.firstObject.title]; 90 | } else { 91 | alert.messageText = [NSString stringWithFormat: 92 | @"Do you want to delete the following states:\n%@", 93 | [titles componentsJoinedByString: @"\n"]]; 94 | } 95 | 96 | alert.informativeText = @"All of the settings on this state will also be removed."; 97 | [alert addButtonWithTitle: @"Cancel"]; 98 | [alert addButtonWithTitle: @"Delete"]; 99 | 100 | NSModalResponse response = [alert runModal]; 101 | switch (response) { 102 | case NSAlertFirstButtonReturn: 103 | // "Cancel" 104 | return NO; 105 | case NSAlertSecondButtonReturn: 106 | // "Delete" 107 | return YES; 108 | default: 109 | return NO; 110 | } 111 | } 112 | 113 | @end 114 | -------------------------------------------------------------------------------- /Plugin/States/StatesController+DragNDrop.h: -------------------------------------------------------------------------------- 1 | // StatesController+DragNDrop.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "StatesController.h" 8 | 9 | @interface StatesController (DragNDrop) 10 | 11 | - (void)registerTableViewForDragNDrop; 12 | 13 | // This category also implements the following NSTableViewDataSources methods: 14 | 15 | - (BOOL)tableView: (NSTableView *)tableView writeRowsWithIndexes: (NSIndexSet *)rowIndexes toPasteboard: (NSPasteboard *)pboard; 16 | 17 | - (NSDragOperation)tableView: (NSTableView *)tableView validateDrop: (id )info proposedRow: (NSInteger)row proposedDropOperation: (NSTableViewDropOperation)dropOperation; 18 | 19 | - (BOOL)tableView: (NSTableView *)tableView acceptDrop: (id )info row: (NSInteger)row dropOperation: (NSTableViewDropOperation)dropOperation; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /Plugin/States/StatesController+DragNDrop.m: -------------------------------------------------------------------------------- 1 | // StatesController+DragNDrop.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "STStatefulArtboard.h" 8 | #import "StatesController+DragNDrop.h" 9 | 10 | NSString * const kStatesControllerDraggedType = @"StatesControllerDraggedType"; 11 | 12 | @implementation StatesController (DragNDrop) 13 | 14 | - (void)registerTableViewForDragNDrop; 15 | { 16 | [self.tableView registerForDraggedTypes: @[kStatesControllerDraggedType]]; 17 | } 18 | 19 | - (BOOL)tableView: (NSTableView *)tableView writeRowsWithIndexes: (NSIndexSet *)rowIndexes toPasteboard: (NSPasteboard *)pboard 20 | { 21 | NSData *indexesData = [NSKeyedArchiver archivedDataWithRootObject: rowIndexes]; 22 | [pboard declareTypes: @[kStatesControllerDraggedType] owner: self]; 23 | [pboard setData: indexesData forType: kStatesControllerDraggedType]; 24 | return YES; 25 | } 26 | 27 | - (NSDragOperation)tableView: (NSTableView *)tableView validateDrop: (id )info proposedRow: (NSInteger)row proposedDropOperation: (NSTableViewDropOperation)dropOperation 28 | { 29 | if (dropOperation == NSTableViewDropAbove) { 30 | [info setAnimatesToDestination: YES]; 31 | return NSDragOperationMove; 32 | } 33 | return NSDragOperationNone; 34 | } 35 | 36 | - (BOOL)tableView: (NSTableView *)tableView acceptDrop: (id )info row: (NSInteger)row dropOperation: (NSTableViewDropOperation)dropOperation 37 | { 38 | NSData *data = [[info draggingPasteboard] dataForType: kStatesControllerDraggedType]; 39 | NSIndexSet *sourceIndexes = [NSKeyedUnarchiver unarchiveObjectWithData: data]; 40 | // 41 | // FIXME: support dragging multiple items 42 | // 43 | NSUInteger destination = MIN(MAX(row, 0), _artboard.allStates.count-1); 44 | NSMutableArray *states = [_artboard.allStates mutableCopy]; 45 | NSUInteger source = sourceIndexes.firstIndex; 46 | 47 | // 1) model updates 48 | id draggedState = [states objectAtIndex: source]; 49 | [states removeObjectAtIndex: source]; 50 | [states insertObject: draggedState atIndex: destination]; 51 | [_artboard reorderStates: states]; 52 | // 2) table view updates 53 | [tableView moveRowAtIndex: source toIndex: destination]; 54 | 55 | return YES; 56 | } 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /Plugin/States/StatesController+Naming.h: -------------------------------------------------------------------------------- 1 | // StatesController+Naming.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "StatesController.h" 8 | @class STStateDescription; 9 | 10 | /// Naming things is the second hard thing in programming 11 | @interface StatesController (Naming) 12 | 13 | /// Enumerates names such as "State", "State 1", "State 2" etc and returns the first available one 14 | - (NSString *)newStateNameInStates: (NSArray *)existingStates; 15 | 16 | /// Returns a name for a new page containing shapshots of the given states 17 | - (NSString *)pageNameForStates: (NSArray *)states sourcePage: (id )sourcePage; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /Plugin/States/StatesController+Naming.m: -------------------------------------------------------------------------------- 1 | // StatesController+Naming.m 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "STPage.h" 8 | #import "STStateDescription.h" 9 | #import "NSArray+HigherOrder.h" 10 | #import "StatesController+Naming.h" 11 | 12 | @implementation StatesController (Naming) 13 | 14 | /// Enumerates names such as "State", "State 1", "State 2" etc and returns the first available one 15 | - (NSString *)newStateNameInStates: (NSArray *)existingStates 16 | { 17 | static NSString *template = @"State"; 18 | NSSet *matchedNames = [NSSet setWithArray: 19 | [existingStates st_map: ^NSString *(STStateDescription *state) { 20 | return state.title; 21 | }]]; 22 | 23 | NSInteger idx = 1; 24 | NSString *newName = template; 25 | while ([matchedNames containsObject: newName]) { 26 | newName = [NSString stringWithFormat: @"%@ %ld", template, idx++]; 27 | } 28 | 29 | return newName; 30 | } 31 | 32 | - (NSString *)pageNameForStates: (NSArray *)states sourcePage: (id )sourcePage; 33 | { 34 | NSString *titles = [[states st_map: ^NSString *(STStateDescription *state) { 35 | return state.title; 36 | }] componentsJoinedByString: @", "]; 37 | 38 | return [NSString stringWithFormat: @"%@ :: %@ [Snapshots for %@]", 39 | sourcePage.name, [sourcePage currentArtboard].name, titles]; 40 | } 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /Plugin/States/StatesController.h: -------------------------------------------------------------------------------- 1 | // StatesController.h 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | @import Cocoa; 8 | 9 | @class STStatefulArtboard; 10 | @class STUpdateButton; 11 | @class STTableCellView; 12 | 13 | /// So here you are, looking for a challenge. This one is responsible for managing the states 14 | /// table view and responding to user's actions by modifing current artboard. 15 | /// 16 | /// It's huge and ungly. But I tried my best to make this controller as stateless (such irony!) as 17 | /// possible so at least one could easily refactor different bits into separate classes 🌟 18 | @interface StatesController : NSWindowController 19 | 20 | { 21 | @protected 22 | STStatefulArtboard *_artboard; 23 | } 24 | @property (weak) IBOutlet NSTableView *tableView; 25 | @property (weak) IBOutlet STUpdateButton *addNewStateButton; 26 | @property (weak) IBOutlet NSView *placeholderView; 27 | 28 | + (instancetype)defaultController; 29 | 30 | /// Creates a new state 31 | - (void)createNewState: (id)sender; 32 | /// Update the current state: make it reflect current artboard attributes 33 | - (void)updateCurrentState: (NSMenuItem *)sender; 34 | /// Create duplicates for all selected states 35 | - (void)duplicateStates: (NSMenuItem *)sender; 36 | /// Create a one page containing as many artboards as selected states: each of them will contain 37 | /// a snapshot of the current artboard in a corresponding state 38 | - (void)createPageFromStates: (NSMenuItem *)sender; 39 | /// Delete all selected states 40 | - (void)deleteStates: (NSMenuItem *)sender; 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /Plugin/States/StatesController.m: -------------------------------------------------------------------------------- 1 | // StatesController.m 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | #import "STPage.h" 8 | #import "STSketch.h" 9 | #import "STTextField.h" 10 | #import "STTableRowView.h" 11 | #import "STColorFactory.h" 12 | #import "STTableCellView.h" 13 | #import "NSArray+Indexes.h" 14 | #import "STStatefulArtboard.h" 15 | #import "NSArray+HigherOrder.h" 16 | #import "STStatefulArtboard+Snapshots.h" 17 | #import "StatesController.h" 18 | #import "StatesController+Naming.h" 19 | #import "StatesController+Decisions.h" 20 | #import "StatesController+DragNDrop.h" 21 | #import "StatesController+ContextMenu.h" 22 | 23 | @interface StatesController() 24 | 25 | @end 26 | 27 | @implementation StatesController 28 | 29 | + (instancetype)defaultController 30 | { 31 | static StatesController *controller = nil; 32 | static dispatch_once_t onceToken; 33 | dispatch_once(&onceToken, ^{ 34 | controller = [[StatesController alloc] init]; 35 | [[STSketch notificationObserver] addListener: controller]; 36 | }); 37 | return controller; 38 | } 39 | 40 | - (NSString *)windowNibName 41 | { 42 | return @"StatesWindow"; 43 | } 44 | 45 | - (void)awakeFromNib 46 | { 47 | [(NSPanel *)self.window setWorksWhenModal: NO]; 48 | [(NSPanel *)self.window setFloatingPanel: YES]; 49 | 50 | /// NOTE: these two images are from Sketch 51 | self.addNewStateButton.image = [NSImage imageNamed: @"pages_add"]; 52 | self.addNewStateButton.alternateImage = [NSImage imageNamed: @"pages_add_pressed"]; 53 | self.addNewStateButton.toolTip = @"Add a new state which will reflect the current artboard parameters"; 54 | 55 | self.tableView.menu = [NSMenu new]; 56 | self.tableView.menu.delegate = self; 57 | self.tableView.action = @selector(singleClicked:); 58 | self.tableView.doubleAction = @selector(doubleClicked:); 59 | [self registerTableViewForDragNDrop]; 60 | 61 | [self resetArtboard: [STSketch currentArtboard]]; 62 | } 63 | 64 | - (void)resetArtboard: (STStatefulArtboard *)artboard 65 | { 66 | _artboard = artboard; 67 | [self.tableView reloadData]; 68 | 69 | if (!_artboard) { 70 | self.placeholderView.hidden = NO; 71 | self.addNewStateButton.enabled = NO; 72 | return; 73 | } 74 | 75 | self.placeholderView.hidden = YES; 76 | self.addNewStateButton.enabled = YES; 77 | 78 | // Pre-select the current state (if any) 79 | NSUInteger currentStateIndex = [_artboard.allStates indexOfObject: _artboard.currentState]; 80 | if (currentStateIndex != NSNotFound) { 81 | [self.tableView selectRowIndexes: [NSIndexSet indexSetWithIndex: currentStateIndex] 82 | byExtendingSelection: NO]; 83 | } 84 | } 85 | 86 | #pragma mark - SketchNotificationsListener 87 | 88 | - (void)currentArtboardDidChange 89 | { 90 | [self resetArtboard: [STSketch currentArtboard]]; 91 | } 92 | 93 | - (void)currentArtboardUnselected 94 | { 95 | [self resetArtboard: nil]; 96 | } 97 | 98 | - (void)currentDocumentUpdated 99 | { 100 | if (!_artboard.currentState) { 101 | return; 102 | } 103 | [self resetDirtyMarkOnStates]; 104 | } 105 | 106 | #pragma mark - Dirty States 107 | 108 | - (void)resetDirtyMarkOnStates 109 | { 110 | // Show or hide an update button depending on a situation 111 | [_artboard.allStates enumerateObjectsUsingBlock: ^(STStateDescription *state, NSUInteger idx, BOOL *stop) { 112 | STTableCellView *cell = [self.tableView viewAtColumn: 0 row: idx makeIfNecessary: NO]; 113 | if ([state isEqualTo: _artboard.currentState] && ([self.tableView editedRow] != idx)) { 114 | cell.updateButton.animator.hidden = [_artboard conformsToState: state]; 115 | } else { 116 | cell.updateButton.animator.hidden = YES; 117 | } 118 | }]; 119 | } 120 | 121 | #pragma mark - STTableCellViewDelegate 122 | 123 | - (BOOL)cellViewRepresentsCurrentItem: (STTableCellView *)cellView 124 | { 125 | NSInteger idx = [_artboard.allStates indexOfObject: _artboard.currentState]; 126 | if (!_artboard || idx == NSNotFound) { 127 | return NO; 128 | } 129 | return cellView == [self.tableView viewAtColumn: 0 row: idx makeIfNecessary: NO]; 130 | } 131 | 132 | - (BOOL)isSingleRowSelected 133 | { 134 | return [self.tableView selectedRowIndexes].count == 1; 135 | } 136 | 137 | #pragma mark - User Actions 138 | 139 | - (IBAction)createNewState: (id)sender 140 | { 141 | NSString *newStateName = [self newStateNameInStates: _artboard.allStates]; 142 | STStateDescription *state = [[STStateDescription alloc] initWithTitle: newStateName]; 143 | 144 | [_artboard insertNewState: state]; 145 | 146 | // Update the table view 147 | NSInteger newIndex = _artboard.allStates.count-1; 148 | [self.tableView insertRowsAtIndexes: [NSIndexSet indexSetWithIndex: newIndex] 149 | withAnimation: NSTableViewAnimationEffectFade]; 150 | // No need to ask user about switching, since the settings are already saved in this new state 151 | [self.tableView selectRowIndexes: [NSIndexSet indexSetWithIndex: newIndex] byExtendingSelection: NO]; 152 | // HACK: we avoid re-apply the same artboard properties again which can take a lot of time on big 153 | // artboards by setting the current state directly instead of calling -applyState:. 154 | // This is a workaround and should be removed as soon as we find a proper solution to our 155 | // performance issues 156 | [_artboard setCurrentState: state]; 157 | [self resetDirtyMarkOnStates]; 158 | // Move focus to the row to allow user to immdiately change the title value 159 | [self.tableView editColumn: 0 row: newIndex withEvent: nil select: YES]; 160 | } 161 | 162 | - (IBAction)updateCurrentState: (NSMenuItem *)sender 163 | { 164 | NSInteger idx = [_artboard.allStates indexOfObject: _artboard.currentState]; 165 | NSParameterAssert(idx != NSNotFound); 166 | 167 | STTableCellView *cell = [self.tableView viewAtColumn: 0 row: idx makeIfNecessary: NO]; 168 | 169 | // Animations first! 170 | __block BOOL animationCompleted = NO; 171 | [cell.updateButton spinWithCompletion: ^{ 172 | [self resetDirtyMarkOnStates]; 173 | animationCompleted = YES; 174 | }]; 175 | // Then actually update the model 176 | [_artboard updateCurrentState]; 177 | // Finally we double check that the update button may be hiden safely 178 | if (animationCompleted) { 179 | [self resetDirtyMarkOnStates]; 180 | } 181 | } 182 | 183 | - (IBAction)duplicateStates: (NSMenuItem *)sender 184 | { 185 | NSArray *originals = sender.representedObject; 186 | NSParameterAssert([originals isKindOfClass: [NSArray class]]); 187 | // Create a copy for every original state passed by sender 188 | [originals enumerateObjectsUsingBlock: ^(STStateDescription *state, NSUInteger idx, BOOL *stop) { 189 | NSString *duplicateTitle = [NSString stringWithFormat: @"%@ copy", state.title]; 190 | STStateDescription *duplicate = [[STStateDescription alloc] initWithTitle: duplicateTitle]; 191 | [_artboard insertNewState: duplicate]; 192 | [_artboard copyState: state toState: duplicate]; 193 | }]; 194 | // Update the table view to reveal this new states 195 | NSRange newStatesRange = NSMakeRange(_artboard.allStates.count-1, originals.count); 196 | NSIndexSet *newIndexes = [NSIndexSet indexSetWithIndexesInRange: newStatesRange]; 197 | [self.tableView insertRowsAtIndexes: newIndexes 198 | withAnimation: NSTableViewAnimationEffectFade]; 199 | } 200 | 201 | - (void)createPageFromStates: (NSMenuItem *)sender 202 | { 203 | NSArray *selectedStates = sender.representedObject; 204 | NSParameterAssert([selectedStates isKindOfClass: [NSArray class]]); 205 | 206 | // 1) Create a new page 207 | id currentPage = [STSketch currentPage]; 208 | id newPage = [NSClassFromString(@"MSPage") page]; 209 | NSAssert(newPage != nil, @"+[MSPage page] returned nil. Is this method still available?"); 210 | 211 | newPage.name = [self pageNameForStates: selectedStates sourcePage: currentPage]; 212 | // XXX: MSPage's pageDelegate property doesn't exist since 3.9 213 | if ([newPage respondsToSelector: @selector(pageDelegate)]) { 214 | newPage.pageDelegate = currentPage.pageDelegate; 215 | } 216 | newPage.grid = currentPage.grid; 217 | newPage.layout = currentPage.layout; 218 | 219 | // 2) for each selected state we create a "snapshot" artboard and copy it to this new page 220 | NSArray *artboards = [selectedStates st_map: ^id(STStateDescription *state) { 221 | return [_artboard snapshotForState: state]; 222 | }]; 223 | // 2.1) we want these artboards to be aligned in a line with a little space in between items 224 | CGFloat gap = 200.f; 225 | __block CGPoint location = CGPointZero; 226 | [artboards enumerateObjectsUsingBlock: ^(id artboard, NSUInteger idx, BOOL *stop) { 227 | if (idx == 0) { 228 | location = [[artboard absoluteRect] absoluteRect].origin; 229 | } 230 | [[artboard absoluteRect] setX: location.x]; 231 | location.x += [[artboard absoluteRect] absoluteRect].size.width + gap; 232 | }]; 233 | [newPage addLayers: artboards]; 234 | 235 | // 3) Insert this new page into the document 236 | [[[STSketch currentDocument] documentData] addPage: newPage]; 237 | // 3.1) adjust scroll and zoom to match the source page 238 | newPage.scrollOrigin = currentPage.scrollOrigin; 239 | newPage.zoomValue = currentPage.zoomValue; 240 | // 3.2) mark this new page as current 241 | [STSketch currentDocument].currentPage = newPage; 242 | 243 | // 4) Select the first available artboard on a new page 244 | if (newPage.artboards.count > 0) { 245 | [[[STSketch currentDocument] documentData] deselectAllLayers]; 246 | [newPage selectLayers: @[newPage.artboards.firstObject]]; 247 | } 248 | } 249 | 250 | - (IBAction)deleteStates: (NSMenuItem *)sender 251 | { 252 | NSMutableArray *statesToDelete = [sender.representedObject mutableCopy]; 253 | NSParameterAssert([statesToDelete isKindOfClass: [NSArray class]]); 254 | 255 | // We can not remove the default state so just remove if from the proposed set of states 256 | [statesToDelete removeObject: _artboard.defaultState]; 257 | 258 | if (![self shoulRemoveStates: statesToDelete]) { 259 | return; 260 | } 261 | NSIndexSet *indexesToDelete = [_artboard.allStates st_indexesOfObjects: statesToDelete]; 262 | // 1) remove states from data model 263 | [statesToDelete enumerateObjectsUsingBlock: ^(STStateDescription *state, NSUInteger idx, BOOL *stop) { 264 | [_artboard removeState: state]; 265 | }]; 266 | // 2) remove corresponding rows from table view 267 | [self.tableView removeRowsAtIndexes: indexesToDelete withAnimation: NSTableViewAnimationEffectFade]; 268 | // 3) update table view selection 269 | NSInteger newCurrentState = [_artboard.allStates indexOfObject: _artboard.currentState]; 270 | if (newCurrentState != NSNotFound) { 271 | [self.tableView selectRowIndexes: [NSIndexSet indexSetWithIndex: newCurrentState] 272 | byExtendingSelection: NO]; 273 | } 274 | } 275 | 276 | /// Single click switches current state 277 | - (void)singleClicked: (id)sender 278 | { 279 | // Ignore clicks when multiple rows are selected 280 | if ([self.tableView selectedRowIndexes].count > 1) { 281 | return; 282 | } 283 | 284 | NSInteger row = [self.tableView clickedRow]; 285 | if (row < 0 || row >= _artboard.allStates.count) { 286 | return; 287 | } 288 | STStateDescription *newState = _artboard.allStates[row]; 289 | if (!newState) { 290 | return; 291 | } 292 | // Clicking on the same state will drop any current changes so we ask user about it 293 | if ([newState isEqualTo: _artboard.currentState]) { 294 | if ([self shouldSwitchToState: _artboard.currentState fromState: _artboard.currentState]) { 295 | [_artboard applyState: newState]; 296 | } 297 | return; 298 | } 299 | // -tableView:shouldSelectRow: has been called already so we just check if the target row 300 | // is selected and apply the new state accordingly. 301 | if ([self.tableView isRowSelected: row]) { 302 | [_artboard applyState: newState]; 303 | [self resetDirtyMarkOnStates]; 304 | } 305 | } 306 | 307 | /// Double click makes a state title text fiels editable 308 | - (void)doubleClicked: (id)sender 309 | { 310 | // Ignore clicks when multiple rows are selected 311 | if ([self.tableView selectedRowIndexes].count > 1) { 312 | return; 313 | } 314 | 315 | NSInteger row = [self.tableView clickedRow]; 316 | if (row < 0 || row >= _artboard.allStates.count) { 317 | return; 318 | } 319 | [self.tableView editColumn: 0 row: row withEvent: nil select: YES]; 320 | } 321 | 322 | #pragma mark User Did Commit New State Title 323 | 324 | - (void)controlTextDidEndEditing: (NSNotification *)obj 325 | { 326 | NSTextView *editor = [obj.userInfo valueForKey: @"NSFieldEditor"]; 327 | NSInteger updatedRow = [self.tableView rowForView: editor]; 328 | if (updatedRow < 0 || updatedRow >= _artboard.allStates.count) { 329 | return; 330 | } 331 | 332 | NSString *newTitle = [[editor string] stringByTrimmingCharactersInSet: 333 | [NSCharacterSet whitespaceAndNewlineCharacterSet]]; 334 | // We either commit the change or just reset the row if user input is invalid 335 | if (newTitle.length > 0) { 336 | [_artboard updateName: newTitle forState: _artboard.allStates[updatedRow]]; 337 | } else { 338 | [self.tableView reloadDataForRowIndexes: [NSIndexSet indexSetWithIndex: updatedRow] 339 | columnIndexes: [NSIndexSet indexSetWithIndex: 0]]; 340 | } 341 | // Don't forget about the update button we've hidden 342 | [self resetDirtyMarkOnStates]; 343 | } 344 | 345 | /// Hide an update button when a state title is being edited 346 | - (void)textFieldBecomeFirstResponder: (NSTextField *)textField 347 | { 348 | NSInteger row = [self.tableView rowForView: textField]; 349 | if (row < 0 || row >= _artboard.allStates.count) { 350 | return; 351 | } 352 | STTableCellView *cell = [self.tableView viewAtColumn: 0 row: row makeIfNecessary:NO]; 353 | cell.updateButton.hidden = YES; 354 | } 355 | 356 | #pragma mark - NSTableViewDataSource & NSTableViewDelegate 357 | 358 | - (NSInteger)numberOfRowsInTableView: (NSTableView *)tableView 359 | { 360 | return _artboard.allStates.count; 361 | } 362 | 363 | - (NSView *)tableView: (NSTableView *)tableView viewForTableColumn: (NSTableColumn *)tableColumn row: (NSInteger)row 364 | { 365 | STStateDescription *state = _artboard.allStates[row]; 366 | if (!state) { 367 | return nil; 368 | } 369 | STTableCellView *cellView = [tableView makeViewWithIdentifier: @"StateCell" owner: nil]; 370 | if (!cellView) { 371 | return nil; 372 | } 373 | cellView.delegate = self; 374 | // Setup text field 375 | cellView.textField.stringValue = state.title; 376 | cellView.textField.delegate = self; 377 | ((STTextField *)cellView.textField).firstResponderDelegate = self; 378 | // Setup update button 379 | cellView.updateButton.action = @selector(updateCurrentState:); 380 | cellView.updateButton.target = self; 381 | // Toggle update button's visibility 382 | if ([[tableView selectedRowIndexes] containsIndex: row]) { 383 | cellView.updateButton.hidden = [_artboard conformsToState: state]; 384 | } else { 385 | cellView.updateButton.hidden = YES; 386 | } 387 | return cellView; 388 | } 389 | 390 | #pragma mark Selection Filter 391 | 392 | - (NSIndexSet *)tableView: (NSTableView *)tableView selectionIndexesForProposedSelection: (NSIndexSet *)proposedSelectionIndexes 393 | { 394 | // Don't allow table view to reset selection automatically from multiple rows to "nothing". In 395 | // this case it will select the last row which may not represent the current state 396 | if ([tableView selectedRowIndexes].count > 1 && proposedSelectionIndexes.count == 0) { 397 | NSInteger currentRow = [_artboard.allStates indexOfObject: _artboard.currentState]; 398 | if (currentRow != NSNotFound) { 399 | return [NSIndexSet indexSetWithIndex: currentRow]; 400 | } else { 401 | return [NSIndexSet indexSet]; 402 | } 403 | } 404 | // Redraw the already selected row when we're dropping multiselection to just this one row 405 | if ([tableView selectedRowIndexes].count > 1 && proposedSelectionIndexes.count == 1) { 406 | STStateDescription *newState = _artboard.allStates[proposedSelectionIndexes.firstIndex]; 407 | if (![self shouldSwitchToState: newState fromState: _artboard.currentState]) { 408 | return [NSIndexSet indexSet]; 409 | } 410 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 411 | (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), 412 | ^{ 413 | [self.tableView rowViewAtRow: proposedSelectionIndexes.firstIndex 414 | makeIfNecessary: NO].needsDisplay = YES; 415 | }); 416 | return proposedSelectionIndexes; 417 | } 418 | 419 | // Always allow to expand selection. Note that we don't switch states in this case 420 | if (proposedSelectionIndexes.count > 1) { 421 | return proposedSelectionIndexes; 422 | } 423 | // Always allow initial selection 424 | if (self.tableView.selectedRowIndexes.count == 0) { 425 | return proposedSelectionIndexes; 426 | } 427 | // Don't allow to drop selection from one row to zero 428 | if (proposedSelectionIndexes.count == 0) { 429 | return [NSIndexSet indexSet]; 430 | } 431 | // So we're switching from one state to another; ask user about this 432 | STStateDescription *oldState = _artboard.allStates[tableView.selectedRowIndexes.firstIndex]; 433 | STStateDescription *newState = _artboard.allStates[proposedSelectionIndexes.firstIndex]; 434 | if ([self shouldSwitchToState: newState fromState: oldState]) { 435 | return proposedSelectionIndexes; 436 | } 437 | 438 | return [NSIndexSet indexSet]; 439 | } 440 | 441 | - (void)tableViewSelectionDidChange:(NSNotification *)notification 442 | { 443 | // Update cell views for current selection state (e.g. set text color, etc) 444 | [_artboard.allStates enumerateObjectsUsingBlock: ^(id obj, NSUInteger idx, BOOL * stop) { 445 | NSTableCellView *view = [self.tableView viewAtColumn: 0 row: idx makeIfNecessary: NO]; 446 | view.backgroundStyle = view.backgroundStyle; 447 | }]; 448 | } 449 | 450 | #pragma mark Row Coloring 451 | 452 | - (NSTableRowView *)tableView: (NSTableView *)tableView rowViewForRow: (NSInteger)row 453 | { 454 | return [[STTableRowView alloc] initWithTableView: tableView]; 455 | } 456 | 457 | @end 458 | -------------------------------------------------------------------------------- /Plugin/States/StatesWindow.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 105 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /Plugin/Versioning.xcconfig: -------------------------------------------------------------------------------- 1 | IEXP_SOURCE_VERSION = 1.0.0 2 | -------------------------------------------------------------------------------- /Plugin/lib/runtime.js: -------------------------------------------------------------------------------- 1 | // runtime.js 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | (function(){ 8 | this.runtime = {}; 9 | /// This function fetches the plugin path from a current script path 10 | this.runtime.pluginPath = function() 11 | { 12 | var result = [NSString stringWithString: coscript.env().scriptURL.path()]; 13 | while(result.lastPathComponent().pathExtension() != "sketchplugin"){ 14 | result = result.stringByDeletingLastPathComponent(); 15 | } 16 | return result; 17 | } 18 | /// This function loads a bundle with the given name located in Resources directory 19 | /// of this plugin 20 | this.runtime.loadBundle = function(bundleName) 21 | { 22 | var bundlePath = runtime.pluginPath() + "/Contents/Resources/" + bundleName; 23 | var bundle = [NSBundle bundleWithPath: bundlePath]; 24 | bundle.load(); 25 | } 26 | })(); 27 | -------------------------------------------------------------------------------- /Plugin/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "States", 3 | "description": "Create different artboard states and switch between them easily", 4 | "author": "Eden Vidal", 5 | "homepage": "http://edenvidal.com", 6 | "version": "1.0.0", 7 | "identifier": "com.edenvidal.states-for-sketch", 8 | "compatibleVersion": 3, 9 | "bundleVersion": 1, 10 | "commands": [{ 11 | "name": "Show States", 12 | "identifier": "show-states", 13 | "script": "plugin.js", 14 | "handler": "showStatesWindow" 15 | }], 16 | "menu": { 17 | "items": ["show-states"], 18 | "isRoot": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Plugin/plugin.js: -------------------------------------------------------------------------------- 1 | // plugin.js 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | @import "lib/runtime.js" 8 | 9 | function showStatesWindow(context) 10 | { 11 | if (NSClassFromString("STStatesController") == null) { 12 | runtime.loadBundle("States.bundle"); 13 | [STSketch setPluginContextDictionary: context]; 14 | } 15 | 16 | var controller = [StatesController defaultController]; 17 | if ([[controller window] isVisible]) { 18 | [[controller window] close]; 19 | } else { 20 | [controller showWindow: nil]; 21 | } 22 | [STSketch toggleStatesPluginName]; 23 | } 24 | -------------------------------------------------------------------------------- /Plugin/vendor/Aspects.h: -------------------------------------------------------------------------------- 1 | // 2 | // Aspects.h 3 | // Aspects - A delightful, simple library for aspect oriented programming. 4 | // 5 | // Copyright (c) 2014 Peter Steinberger. Licensed under the MIT license. 6 | // 7 | 8 | #import 9 | 10 | typedef NS_OPTIONS(NSUInteger, AspectOptions) { 11 | AspectPositionAfter = 0, /// Called after the original implementation (default) 12 | AspectPositionInstead = 1, /// Will replace the original implementation. 13 | AspectPositionBefore = 2, /// Called before the original implementation. 14 | 15 | AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution. 16 | }; 17 | 18 | /// Opaque Aspect Token that allows to deregister the hook. 19 | @protocol AspectToken 20 | 21 | /// Deregisters an aspect. 22 | /// @return YES if deregistration is successful, otherwise NO. 23 | - (BOOL)remove; 24 | 25 | @end 26 | 27 | /// The AspectInfo protocol is the first parameter of our block syntax. 28 | @protocol AspectInfo 29 | 30 | /// The instance that is currently hooked. 31 | - (id)instance; 32 | 33 | /// The original invocation of the hooked method. 34 | - (NSInvocation *)originalInvocation; 35 | 36 | /// All method arguments, boxed. This is lazily evaluated. 37 | - (NSArray *)arguments; 38 | 39 | @end 40 | 41 | /** 42 | Aspects uses Objective-C message forwarding to hook into messages. This will create some overhead. Don't add aspects to methods that are called a lot. Aspects is meant for view/controller code that is not called a 1000 times per second. 43 | 44 | Adding aspects returns an opaque token which can be used to deregister again. All calls are thread safe. 45 | */ 46 | @interface NSObject (Aspects) 47 | 48 | /// Adds a block of code before/instead/after the current `selector` for a specific class. 49 | /// 50 | /// @param block Aspects replicates the type signature of the method being hooked. 51 | /// The first parameter will be `id`, followed by all parameters of the method. 52 | /// These parameters are optional and will be filled to match the block signature. 53 | /// You can even use an empty block, or one that simple gets `id`. 54 | /// 55 | /// @note Hooking static methods is not supported. 56 | /// @return A token which allows to later deregister the aspect. 57 | + (id)aspect_hookSelector:(SEL)selector 58 | withOptions:(AspectOptions)options 59 | usingBlock:(id)block 60 | error:(NSError **)error; 61 | 62 | /// Adds a block of code before/instead/after the current `selector` for a specific instance. 63 | - (id)aspect_hookSelector:(SEL)selector 64 | withOptions:(AspectOptions)options 65 | usingBlock:(id)block 66 | error:(NSError **)error; 67 | 68 | @end 69 | 70 | 71 | typedef NS_ENUM(NSUInteger, AspectErrorCode) { 72 | AspectErrorSelectorBlacklisted, /// Selectors like release, retain, autorelease are blacklisted. 73 | AspectErrorDoesNotRespondToSelector, /// Selector could not be found. 74 | AspectErrorSelectorDeallocPosition, /// When hooking dealloc, only AspectPositionBefore is allowed. 75 | AspectErrorSelectorAlreadyHookedInClassHierarchy, /// Statically hooking the same method in subclasses is not allowed. 76 | AspectErrorFailedToAllocateClassPair, /// The runtime failed creating a class pair. 77 | AspectErrorMissingBlockSignature, /// The block misses compile time signature info and can't be called. 78 | AspectErrorIncompatibleBlockSignature, /// The block signature does not match the method or is too large. 79 | 80 | AspectErrorRemoveObjectAlreadyDeallocated = 100 /// (for removing) The object hooked is already deallocated. 81 | }; 82 | 83 | extern NSString *const AspectErrorDomain; 84 | -------------------------------------------------------------------------------- /States.sketchplugin/Contents/Resources/States.bundle/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 15G31 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | States 11 | CFBundleIdentifier 12 | com.edenvidal.states-for-sketch 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | States 17 | CFBundlePackageType 18 | BNDL 19 | CFBundleShortVersionString 20 | 1.0.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleSupportedPlatforms 24 | 25 | MacOSX 26 | 27 | CFBundleVersion 28 | 1 29 | DTCompiler 30 | com.apple.compilers.llvm.clang.1_0 31 | DTPlatformBuild 32 | 7D1014 33 | DTPlatformVersion 34 | GM 35 | DTSDKBuild 36 | 15E60 37 | DTSDKName 38 | macosx10.11 39 | DTXcode 40 | 0731 41 | DTXcodeBuild 42 | 7D1014 43 | NSHumanReadableCopyright 44 | Copyright © 2016 Eden Vidal. All rights reserved. 45 | 46 | 47 | -------------------------------------------------------------------------------- /States.sketchplugin/Contents/Resources/States.bundle/Contents/MacOS/States: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/States.sketchplugin/Contents/Resources/States.bundle/Contents/MacOS/States -------------------------------------------------------------------------------- /States.sketchplugin/Contents/Resources/States.bundle/Contents/Resources/StatesWindow.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/States.sketchplugin/Contents/Resources/States.bundle/Contents/Resources/StatesWindow.nib -------------------------------------------------------------------------------- /States.sketchplugin/Contents/Resources/States.bundle/Contents/Resources/dirty.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/States.sketchplugin/Contents/Resources/States.bundle/Contents/Resources/dirty.tiff -------------------------------------------------------------------------------- /States.sketchplugin/Contents/Resources/States.bundle/Contents/_CodeSignature/CodeResources: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | files 6 | 7 | Resources/StatesWindow.nib 8 | 9 | tPNG8G23xzx2FF9fJkMeLlowPCg= 10 | 11 | Resources/dirty.tiff 12 | 13 | OigIS04hpst041DCQF9TZTP28HM= 14 | 15 | 16 | files2 17 | 18 | Resources/StatesWindow.nib 19 | 20 | hash 21 | 22 | tPNG8G23xzx2FF9fJkMeLlowPCg= 23 | 24 | hash2 25 | 26 | R+X7XpAk5/v35Io50Z1VeFKd+JD0rIEYCGO62q9luas= 27 | 28 | 29 | Resources/dirty.tiff 30 | 31 | hash 32 | 33 | OigIS04hpst041DCQF9TZTP28HM= 34 | 35 | hash2 36 | 37 | 7KCD71Cxnh0qc7BUXhpJv/M2LHesZc1jZI4/QyOsTC0= 38 | 39 | 40 | 41 | rules 42 | 43 | ^Resources/ 44 | 45 | ^Resources/.*\.lproj/ 46 | 47 | optional 48 | 49 | weight 50 | 1000 51 | 52 | ^Resources/.*\.lproj/locversion.plist$ 53 | 54 | omit 55 | 56 | weight 57 | 1100 58 | 59 | ^version.plist$ 60 | 61 | 62 | rules2 63 | 64 | .*\.dSYM($|/) 65 | 66 | weight 67 | 11 68 | 69 | ^(.*/)?\.DS_Store$ 70 | 71 | omit 72 | 73 | weight 74 | 2000 75 | 76 | ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ 77 | 78 | nested 79 | 80 | weight 81 | 10 82 | 83 | ^.* 84 | 85 | ^Info\.plist$ 86 | 87 | omit 88 | 89 | weight 90 | 20 91 | 92 | ^PkgInfo$ 93 | 94 | omit 95 | 96 | weight 97 | 20 98 | 99 | ^Resources/ 100 | 101 | weight 102 | 20 103 | 104 | ^Resources/.*\.lproj/ 105 | 106 | optional 107 | 108 | weight 109 | 1000 110 | 111 | ^Resources/.*\.lproj/locversion.plist$ 112 | 113 | omit 114 | 115 | weight 116 | 1100 117 | 118 | ^[^/]+$ 119 | 120 | nested 121 | 122 | weight 123 | 10 124 | 125 | ^embedded\.provisionprofile$ 126 | 127 | weight 128 | 20 129 | 130 | ^version\.plist$ 131 | 132 | weight 133 | 20 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /States.sketchplugin/Contents/Sketch/lib/runtime.js: -------------------------------------------------------------------------------- 1 | // runtime.js 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | (function(){ 8 | this.runtime = {}; 9 | /// This function fetches the plugin path from a current script path 10 | this.runtime.pluginPath = function() 11 | { 12 | var result = [NSString stringWithString: coscript.env().scriptURL.path()]; 13 | while(result.lastPathComponent().pathExtension() != "sketchplugin"){ 14 | result = result.stringByDeletingLastPathComponent(); 15 | } 16 | return result; 17 | } 18 | /// This function loads a bundle with the given name located in Resources directory 19 | /// of this plugin 20 | this.runtime.loadBundle = function(bundleName) 21 | { 22 | var bundlePath = runtime.pluginPath() + "/Contents/Resources/" + bundleName; 23 | var bundle = [NSBundle bundleWithPath: bundlePath]; 24 | bundle.load(); 25 | } 26 | })(); 27 | -------------------------------------------------------------------------------- /States.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "States", 3 | "description": "Create different artboard states and switch between them easily", 4 | "author": "Eden Vidal", 5 | "homepage": "http://edenvidal.com", 6 | "version": "1.0.0", 7 | "identifier": "com.edenvidal.states-for-sketch", 8 | "compatibleVersion": 3, 9 | "bundleVersion": 1, 10 | "commands": [{ 11 | "name": "Show States", 12 | "identifier": "show-states", 13 | "script": "plugin.js", 14 | "handler": "showStatesWindow" 15 | }], 16 | "menu": { 17 | "items": ["show-states"], 18 | "isRoot": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /States.sketchplugin/Contents/Sketch/plugin.js: -------------------------------------------------------------------------------- 1 | // plugin.js 2 | // Copyright (c) 2016 Eden Vidal 3 | // 4 | // This software may be modified and distributed under the terms 5 | // of the MIT license. See the LICENSE file for details. 6 | 7 | @import "lib/runtime.js" 8 | 9 | function showStatesWindow(context) 10 | { 11 | if (NSClassFromString("STStatesController") == null) { 12 | runtime.loadBundle("States.bundle"); 13 | [STSketch setPluginContextDictionary: context]; 14 | } 15 | 16 | var controller = [StatesController defaultController]; 17 | if ([[controller window] isVisible]) { 18 | [[controller window] close]; 19 | } else { 20 | [controller showWindow: nil]; 21 | } 22 | [STSketch toggleStatesPluginName]; 23 | } 24 | -------------------------------------------------------------------------------- /css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ 2 | /** 3 | * 1. Set default font family to sans-serif. 4 | * 2. Prevent iOS and IE text size adjust after device orientation change, 5 | * without disabling user zoom. 6 | */ 7 | html { 8 | font-family: sans-serif; 9 | /* 1 */ 10 | -ms-text-size-adjust: 100%; 11 | /* 2 */ 12 | -webkit-text-size-adjust: 100%; 13 | /* 2 */ 14 | } 15 | /** 16 | * Remove default margin. 17 | */ 18 | body { 19 | margin: 0; 20 | } 21 | /* HTML5 display definitions 22 | ========================================================================== */ 23 | /** 24 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 25 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 26 | * and Firefox. 27 | * Correct `block` display not defined for `main` in IE 11. 28 | */ 29 | article, 30 | aside, 31 | details, 32 | figcaption, 33 | figure, 34 | footer, 35 | header, 36 | hgroup, 37 | main, 38 | menu, 39 | nav, 40 | section, 41 | summary { 42 | display: block; 43 | } 44 | /** 45 | * 1. Correct `inline-block` display not defined in IE 8/9. 46 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 47 | */ 48 | audio, 49 | canvas, 50 | progress, 51 | video { 52 | display: inline-block; 53 | /* 1 */ 54 | vertical-align: baseline; 55 | /* 2 */ 56 | } 57 | /** 58 | * Prevent modern browsers from displaying `audio` without controls. 59 | * Remove excess height in iOS 5 devices. 60 | */ 61 | audio:not([controls]) { 62 | display: none; 63 | height: 0; 64 | } 65 | /** 66 | * Address `[hidden]` styling not present in IE 8/9/10. 67 | * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. 68 | */ 69 | [hidden], 70 | template { 71 | display: none; 72 | } 73 | /* Links 74 | ========================================================================== */ 75 | /** 76 | * Remove the gray background color from active links in IE 10. 77 | */ 78 | a { 79 | background-color: transparent; 80 | } 81 | /** 82 | * Improve readability of focused elements when they are also in an 83 | * active/hover state. 84 | */ 85 | a:active, 86 | a:hover { 87 | outline: 0; 88 | } 89 | /* Text-level semantics 90 | ========================================================================== */ 91 | /** 92 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 93 | */ 94 | abbr[title] { 95 | border-bottom: 1px dotted; 96 | } 97 | /** 98 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 99 | */ 100 | b, 101 | strong { 102 | font-weight: bold; 103 | } 104 | /** 105 | * Address styling not present in Safari and Chrome. 106 | */ 107 | dfn { 108 | font-style: italic; 109 | } 110 | /** 111 | * Address variable `h1` font-size and margin within `section` and `article` 112 | * contexts in Firefox 4+, Safari, and Chrome. 113 | */ 114 | h1 { 115 | font-size: 2em; 116 | margin: 0.67em 0; 117 | } 118 | /** 119 | * Address styling not present in IE 8/9. 120 | */ 121 | mark { 122 | background: #ff0; 123 | color: #000; 124 | } 125 | /** 126 | * Address inconsistent and variable font size in all browsers. 127 | */ 128 | small { 129 | font-size: 80%; 130 | } 131 | /** 132 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 133 | */ 134 | sub, 135 | sup { 136 | font-size: 75%; 137 | line-height: 0; 138 | position: relative; 139 | vertical-align: baseline; 140 | } 141 | sup { 142 | top: -0.5em; 143 | } 144 | sub { 145 | bottom: -0.25em; 146 | } 147 | /* Embedded content 148 | ========================================================================== */ 149 | /** 150 | * Remove border when inside `a` element in IE 8/9/10. 151 | */ 152 | img { 153 | border: 0; 154 | } 155 | /** 156 | * Correct overflow not hidden in IE 9/10/11. 157 | */ 158 | svg:not(:root) { 159 | overflow: hidden; 160 | } 161 | /* Grouping content 162 | ========================================================================== */ 163 | /** 164 | * Address margin not present in IE 8/9 and Safari. 165 | */ 166 | figure { 167 | margin: 1em 40px; 168 | } 169 | /** 170 | * Address differences between Firefox and other browsers. 171 | */ 172 | hr { 173 | box-sizing: content-box; 174 | height: 0; 175 | } 176 | /** 177 | * Contain overflow in all browsers. 178 | */ 179 | pre { 180 | overflow: auto; 181 | } 182 | /** 183 | * Address odd `em`-unit font size rendering in all browsers. 184 | */ 185 | code, 186 | kbd, 187 | pre, 188 | samp { 189 | font-family: monospace, monospace; 190 | font-size: 1em; 191 | } 192 | /* Forms 193 | ========================================================================== */ 194 | /** 195 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 196 | * styling of `select`, unless a `border` property is set. 197 | */ 198 | /** 199 | * 1. Correct color not being inherited. 200 | * Known issue: affects color of disabled elements. 201 | * 2. Correct font properties not being inherited. 202 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 203 | */ 204 | button, 205 | input, 206 | optgroup, 207 | select, 208 | textarea { 209 | color: inherit; 210 | /* 1 */ 211 | font: inherit; 212 | /* 2 */ 213 | margin: 0; 214 | /* 3 */ 215 | } 216 | /** 217 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 218 | */ 219 | button { 220 | overflow: visible; 221 | } 222 | /** 223 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 224 | * All other form control elements do not inherit `text-transform` values. 225 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 226 | * Correct `select` style inheritance in Firefox. 227 | */ 228 | button, 229 | select { 230 | text-transform: none; 231 | } 232 | /** 233 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 234 | * and `video` controls. 235 | * 2. Correct inability to style clickable `input` types in iOS. 236 | * 3. Improve usability and consistency of cursor style between image-type 237 | * `input` and others. 238 | * 4. CUSTOM FOR WEBFLOW: Removed the input[type="submit"] selector to reduce 239 | * specificity and defer to the .w-button selector 240 | */ 241 | button, 242 | html input[type="button"], 243 | input[type="reset"] { 244 | -webkit-appearance: button; 245 | /* 2 */ 246 | cursor: pointer; 247 | /* 3 */ 248 | } 249 | /** 250 | * Re-set default cursor for disabled elements. 251 | */ 252 | button[disabled], 253 | html input[disabled] { 254 | cursor: default; 255 | } 256 | /** 257 | * Remove inner padding and border in Firefox 4+. 258 | */ 259 | button::-moz-focus-inner, 260 | input::-moz-focus-inner { 261 | border: 0; 262 | padding: 0; 263 | } 264 | /** 265 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 266 | * the UA stylesheet. 267 | */ 268 | input { 269 | line-height: normal; 270 | } 271 | /** 272 | * It's recommended that you don't attempt to style these elements. 273 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 274 | * 275 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 276 | * 2. Remove excess padding in IE 8/9/10. 277 | */ 278 | input[type="checkbox"], 279 | input[type="radio"] { 280 | box-sizing: border-box; 281 | /* 1 */ 282 | padding: 0; 283 | /* 2 */ 284 | } 285 | /** 286 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 287 | * `font-size` values of the `input`, it causes the cursor style of the 288 | * decrement button to change from `default` to `text`. 289 | */ 290 | input[type="number"]::-webkit-inner-spin-button, 291 | input[type="number"]::-webkit-outer-spin-button { 292 | height: auto; 293 | } 294 | /** 295 | * 1. CUSTOM FOR WEBFLOW: changed from `textfield` to `none` to normalize iOS rounded input 296 | * 2. CUSTOM FOR WEBFLOW: box-sizing: content-box rule removed 297 | * (similar to normalize.css >=4.0.0) 298 | */ 299 | input[type="search"] { 300 | -webkit-appearance: none; 301 | /* 1 */ 302 | } 303 | /** 304 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 305 | * Safari (but not Chrome) clips the cancel button when the search input has 306 | * padding (and `textfield` appearance). 307 | */ 308 | input[type="search"]::-webkit-search-cancel-button, 309 | input[type="search"]::-webkit-search-decoration { 310 | -webkit-appearance: none; 311 | } 312 | /** 313 | * Define consistent border, margin, and padding. 314 | */ 315 | fieldset { 316 | border: 1px solid #c0c0c0; 317 | margin: 0 2px; 318 | padding: 0.35em 0.625em 0.75em; 319 | } 320 | /** 321 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 322 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 323 | */ 324 | legend { 325 | border: 0; 326 | /* 1 */ 327 | padding: 0; 328 | /* 2 */ 329 | } 330 | /** 331 | * Remove default vertical scrollbar in IE 8/9/10/11. 332 | */ 333 | textarea { 334 | overflow: auto; 335 | } 336 | /** 337 | * Don't inherit the `font-weight` (applied by a rule above). 338 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 339 | */ 340 | optgroup { 341 | font-weight: bold; 342 | } 343 | /* Tables 344 | ========================================================================== */ 345 | /** 346 | * Remove most spacing between table cells. 347 | */ 348 | table { 349 | border-collapse: collapse; 350 | border-spacing: 0; 351 | } 352 | td, 353 | th { 354 | padding: 0; 355 | } 356 | -------------------------------------------------------------------------------- /css/states.webflow.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #f2f2f2; 3 | color: #333; 4 | font-size: 16px; 5 | line-height: 20px; 6 | } 7 | 8 | h1 { 9 | margin-top: 20px; 10 | font-size: 74px; 11 | line-height: 72px; 12 | font-weight: 400; 13 | } 14 | 15 | h2 { 16 | margin-top: 20px; 17 | margin-bottom: 20px; 18 | font-family: 'Playfair Display', sans-serif; 19 | font-size: 23px; 20 | line-height: 32px; 21 | font-weight: 400; 22 | letter-spacing: 0.2px; 23 | } 24 | 25 | h3 { 26 | margin-top: 20px; 27 | margin-bottom: 10px; 28 | font-family: 'Playfair Display', sans-serif; 29 | font-size: 24px; 30 | line-height: 30px; 31 | } 32 | 33 | h5 { 34 | margin-top: 10px; 35 | margin-bottom: 10px; 36 | font-family: 'Playfair Display', sans-serif; 37 | font-size: 14px; 38 | line-height: 20px; 39 | font-style: italic; 40 | font-weight: 400; 41 | } 42 | 43 | p { 44 | margin-top: 20px; 45 | margin-bottom: 20px; 46 | color: rgba(0, 0, 0, .5); 47 | } 48 | 49 | a { 50 | clear: left; 51 | color: #fff; 52 | text-decoration: underline; 53 | } 54 | 55 | img { 56 | display: inline-block; 57 | max-width: 100%; 58 | } 59 | 60 | .section { 61 | display: -webkit-box; 62 | display: -webkit-flex; 63 | display: -ms-flexbox; 64 | display: flex; 65 | height: 100vh; 66 | padding: 8%; 67 | -webkit-box-orient: horizontal; 68 | -webkit-box-direction: normal; 69 | -webkit-flex-direction: row; 70 | -ms-flex-direction: row; 71 | flex-direction: row; 72 | -webkit-justify-content: space-around; 73 | -ms-flex-pack: distribute; 74 | justify-content: space-around; 75 | -webkit-flex-wrap: nowrap; 76 | -ms-flex-wrap: nowrap; 77 | flex-wrap: nowrap; 78 | -webkit-box-align: stretch; 79 | -webkit-align-items: stretch; 80 | -ms-flex-align: stretch; 81 | align-items: stretch; 82 | -webkit-align-content: center; 83 | -ms-flex-line-pack: center; 84 | align-content: center; 85 | font-weight: 400; 86 | } 87 | 88 | .section.black { 89 | background-color: #000; 90 | } 91 | 92 | .section.black.bg { 93 | background-color: #000; 94 | background-image: -webkit-linear-gradient(270deg, transparent, rgba(0, 0, 0, .5) 80%, #000), url('../images/mate.gif'); 95 | background-image: linear-gradient(180deg, transparent, rgba(0, 0, 0, .5) 80%, #000), url('../images/mate.gif'); 96 | background-position: 0px 0px, 50% 50%; 97 | background-repeat: repeat, no-repeat; 98 | background-attachment: scroll, fixed; 99 | } 100 | 101 | .section.black.bg2 { 102 | background-image: -webkit-linear-gradient(90deg, transparent, #000), url('../images/mate.gif'); 103 | background-image: linear-gradient(0deg, transparent, #000), url('../images/mate.gif'); 104 | background-position: 0px 0px, 50% 50%; 105 | background-repeat: repeat, no-repeat; 106 | background-attachment: scroll, fixed; 107 | } 108 | 109 | .section.artboard { 110 | background-image: url('../images/Animation2.gif'); 111 | background-position: 50% 50%; 112 | background-repeat: no-repeat; 113 | background-attachment: fixed; 114 | } 115 | 116 | ._33 { 117 | display: -webkit-box; 118 | display: -webkit-flex; 119 | display: -ms-flexbox; 120 | display: flex; 121 | height: 100%; 122 | -webkit-box-orient: vertical; 123 | -webkit-box-direction: normal; 124 | -webkit-flex-direction: column; 125 | -ms-flex-direction: column; 126 | flex-direction: column; 127 | -webkit-box-pack: center; 128 | -webkit-justify-content: center; 129 | -ms-flex-pack: center; 130 | justify-content: center; 131 | -webkit-flex-wrap: nowrap; 132 | -ms-flex-wrap: nowrap; 133 | flex-wrap: nowrap; 134 | -webkit-box-align: start; 135 | -webkit-align-items: flex-start; 136 | -ms-flex-align: start; 137 | align-items: flex-start; 138 | -webkit-align-content: flex-start; 139 | -ms-flex-line-pack: start; 140 | align-content: flex-start; 141 | -webkit-box-flex: 1; 142 | -webkit-flex: 1; 143 | -ms-flex: 1; 144 | flex: 1; 145 | } 146 | 147 | .button { 148 | border: 5px solid #000; 149 | background-color: transparent; 150 | color: #000; 151 | font-size: 18px; 152 | } 153 | 154 | .button.white { 155 | padding: 9px 15px; 156 | border-color: #fff; 157 | color: #fff; 158 | } 159 | 160 | .white { 161 | color: #fff; 162 | font-weight: 400; 163 | } 164 | 165 | .white.bold { 166 | font-weight: 700; 167 | } 168 | 169 | .white.space { 170 | margin-bottom: 36px; 171 | } 172 | 173 | .bold { 174 | color: #fff; 175 | font-weight: 400; 176 | } 177 | 178 | ._75 { 179 | width: 75%; 180 | -webkit-align-self: center; 181 | -ms-flex-item-align: center; 182 | align-self: center; 183 | } 184 | 185 | ._25 { 186 | display: -webkit-box; 187 | display: -webkit-flex; 188 | display: -ms-flexbox; 189 | display: flex; 190 | width: 25%; 191 | -webkit-box-orient: vertical; 192 | -webkit-box-direction: normal; 193 | -webkit-flex-direction: column; 194 | -ms-flex-direction: column; 195 | flex-direction: column; 196 | -webkit-box-align: start; 197 | -webkit-align-items: flex-start; 198 | -ms-flex-align: start; 199 | align-items: flex-start; 200 | -webkit-align-self: center; 201 | -ms-flex-item-align: center; 202 | align-self: center; 203 | } 204 | 205 | ._25.bottom { 206 | -webkit-align-self: flex-end; 207 | -ms-flex-item-align: end; 208 | align-self: flex-end; 209 | } 210 | 211 | ._25.long { 212 | display: -webkit-box; 213 | display: -webkit-flex; 214 | display: -ms-flexbox; 215 | display: flex; 216 | -webkit-box-orient: vertical; 217 | -webkit-box-direction: normal; 218 | -webkit-flex-direction: column; 219 | -ms-flex-direction: column; 220 | flex-direction: column; 221 | -webkit-box-pack: justify; 222 | -webkit-justify-content: space-between; 223 | -ms-flex-pack: justify; 224 | justify-content: space-between; 225 | -webkit-align-self: stretch; 226 | -ms-flex-item-align: stretch; 227 | align-self: stretch; 228 | } 229 | 230 | .social { 231 | position: fixed; 232 | top: 0px; 233 | right: 0px; 234 | display: -webkit-box; 235 | display: -webkit-flex; 236 | display: -ms-flexbox; 237 | display: flex; 238 | padding: 25px; 239 | opacity: 0.5; 240 | -webkit-transition: opacity 200ms ease; 241 | transition: opacity 200ms ease; 242 | } 243 | 244 | .social:hover { 245 | opacity: 1; 246 | } 247 | 248 | .icon { 249 | margin-right: 25px; 250 | } 251 | 252 | ._50 { 253 | -webkit-align-self: flex-end; 254 | -ms-flex-item-align: end; 255 | align-self: flex-end; 256 | -webkit-flex-basis: 50%; 257 | -ms-flex-preferred-size: 50%; 258 | flex-basis: 50%; 259 | text-align: center; 260 | } 261 | 262 | .center { 263 | float: none; 264 | text-align: center; 265 | } 266 | 267 | .red { 268 | background-color: red; 269 | } 270 | 271 | .link { 272 | color: #000; 273 | } 274 | 275 | .link-2 { 276 | border-bottom: 1px solid #000; 277 | } 278 | 279 | .none { 280 | margin-bottom: 20px; 281 | text-decoration: none; 282 | } 283 | 284 | html.w-mod-js *[data-ix="display-none"] { 285 | display: none; 286 | } 287 | 288 | @media (max-width: 991px) { 289 | .section { 290 | height: 100%; 291 | -webkit-box-orient: vertical; 292 | -webkit-box-direction: normal; 293 | -webkit-flex-direction: column; 294 | -ms-flex-direction: column; 295 | flex-direction: column; 296 | } 297 | .section.artboard { 298 | background-image: none; 299 | background-position: 0px 0px; 300 | background-repeat: repeat; 301 | background-attachment: scroll; 302 | } 303 | ._75 { 304 | width: 100%; 305 | } 306 | ._25 { 307 | width: 100%; 308 | } 309 | ._25.bottom { 310 | margin-bottom: 5%; 311 | } 312 | } 313 | 314 | -------------------------------------------------------------------------------- /images/Animation2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/images/Animation2.gif -------------------------------------------------------------------------------- /images/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/images/add.png -------------------------------------------------------------------------------- /images/blk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/images/blk.png -------------------------------------------------------------------------------- /images/f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/images/f.png -------------------------------------------------------------------------------- /images/favbig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/images/favbig.png -------------------------------------------------------------------------------- /images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/images/favicon.png -------------------------------------------------------------------------------- /images/git.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/images/git.png -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/images/logo.png -------------------------------------------------------------------------------- /images/mate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/images/mate.gif -------------------------------------------------------------------------------- /images/position.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/images/position.png -------------------------------------------------------------------------------- /images/share-p-1080x565.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/images/share-p-1080x565.png -------------------------------------------------------------------------------- /images/share-p-500x262.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/images/share-p-500x262.png -------------------------------------------------------------------------------- /images/share-p-800x418.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/images/share-p-800x418.png -------------------------------------------------------------------------------- /images/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/images/share.png -------------------------------------------------------------------------------- /images/sketch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/images/sketch.png -------------------------------------------------------------------------------- /images/t.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/images/t.png -------------------------------------------------------------------------------- /images/update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/images/update.png -------------------------------------------------------------------------------- /images/visible.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edenvidal/states/22c0aab87ec621daf678da24b8a9cabcc3ae6282/images/visible.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | States of the artboard - Sketch Plugin 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 23 | 24 | 31 | 32 | 33 | 43 | 50 | 51 | 52 | 80 |
81 |
82 |

States
of the 83 |
artboard

84 | 85 |

See how it works

86 |
87 |
88 |
89 |
90 |
91 |

Sketch Plugin

92 |

Create different states and switch between them easily. Just like layer comps for Sketch.

93 | 94 |
95 |
96 |
97 |
98 |
99 |

Define different positions and toggle visibility of your layers.

100 |
101 |
102 |

Create new states and update changes. Create pages with new artboards from your states.

103 |
104 |
105 |
106 |
107 |
108 |
109 |

Created with love for the design community and the good people of the earth.

Read the post on Medium

110 |
111 |
112 |
113 |

By Eden Vidal

114 |

Please let me know if this works for you and how – any feedback, ideas, or bugs.

115 |
116 |
117 | 118 | 119 | 120 | 121 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /js/webflow.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Webflow: Front-end site library 3 | * @license MIT 4 | * Inline scripts may access the api using an async handler: 5 | * var Webflow = Webflow || []; 6 | * Webflow.push(readyFunction); 7 | */!function(t){var e={};function n(i){if(e[i])return e[i].exports;var r=e[i]={i:i,l:!1,exports:{}};return t[i].call(r.exports,r,r.exports,n),r.l=!0,r.exports}n.m=t,n.c=e,n.d=function(t,e,i){n.o(t,e)||Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get:i})},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=2)}([function(t,e,n){var i={},r={},o=[],s=window.Webflow||[],a=window.jQuery,u=a(window),c=a(document),l=a.isFunction,f=i._=n(4),h=n(1)&&a.tram,d=!1,p=!1;function v(t){i.env()&&(l(t.design)&&u.on("__wf_design",t.design),l(t.preview)&&u.on("__wf_preview",t.preview)),l(t.destroy)&&u.on("__wf_destroy",t.destroy),t.ready&&l(t.ready)&&function(t){if(d)return void t.ready();if(f.contains(o,t.ready))return;o.push(t.ready)}(t)}function m(t){l(t.design)&&u.off("__wf_design",t.design),l(t.preview)&&u.off("__wf_preview",t.preview),l(t.destroy)&&u.off("__wf_destroy",t.destroy),t.ready&&l(t.ready)&&function(t){o=f.filter(o,function(e){return e!==t.ready})}(t)}h.config.hideBackface=!1,h.config.keepInherited=!0,i.define=function(t,e,n){r[t]&&m(r[t]);var i=r[t]=e(a,f,n)||{};return v(i),i},i.require=function(t){return r[t]},i.push=function(t){d?l(t)&&t():s.push(t)},i.env=function(t){var e=window.__wf_design,n=void 0!==e;return t?"design"===t?n&&e:"preview"===t?n&&!e:"slug"===t?n&&window.__wf_slug:"editor"===t?window.WebflowEditor:"test"===t?window.__wf_test:"frame"===t?window!==window.top:void 0:n};var w,g=navigator.userAgent.toLowerCase(),b=i.env.touch="ontouchstart"in window||window.DocumentTouch&&document instanceof window.DocumentTouch,y=i.env.chrome=/chrome/.test(g)&&/Google/.test(navigator.vendor)&&parseInt(g.match(/chrome\/(\d+)\./)[1],10),x=i.env.ios=/(ipod|iphone|ipad)/.test(g);i.env.safari=/safari/.test(g)&&!y&&!x,b&&c.on("touchstart mousedown",function(t){w=t.target}),i.validClick=b?function(t){return t===w||a.contains(t,w)}:function(){return!0};var _,k="resize.webflow orientationchange.webflow load.webflow";function z(t,e){var n=[],i={};return i.up=f.throttle(function(t){f.each(n,function(e){e(t)})}),t&&e&&t.on(e,i.up),i.on=function(t){"function"==typeof t&&(f.contains(n,t)||n.push(t))},i.off=function(t){n=arguments.length?f.filter(n,function(e){return e!==t}):[]},i}function T(t){l(t)&&t()}function E(){_&&(_.reject(),u.off("load",_.resolve)),_=new a.Deferred,u.on("load",_.resolve)}i.resize=z(u,k),i.scroll=z(u,"scroll.webflow resize.webflow orientationchange.webflow load.webflow"),i.redraw=z(),i.location=function(t){window.location=t},i.env()&&(i.location=function(){}),i.ready=function(){d=!0,p?(p=!1,f.each(r,v)):f.each(o,T),f.each(s,T),i.resize.up()},i.load=function(t){_.then(t)},i.destroy=function(t){t=t||{},p=!0,u.triggerHandler("__wf_destroy"),null!=t.domready&&(d=t.domready),f.each(r,m),i.resize.off(),i.scroll.off(),i.redraw.off(),o=[],s=[],"pending"===_.state()&&E()},a(i.ready),E(),t.exports=window.Webflow=i},function(t,e){var n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};window.tram=function(t){function e(t,e){return(new L.Bare).init(t,e)}function i(t){return t.replace(/[A-Z]/g,function(t){return"-"+t.toLowerCase()})}function r(t){var e=parseInt(t.slice(1),16);return[e>>16&255,e>>8&255,255&e]}function o(t,e,n){return"#"+(1<<24|t<<16|e<<8|n).toString(16).slice(1)}function s(){}function a(t,e,n){c("Units do not match ["+t+"]: "+e+", "+n)}function u(t,e,n){if(void 0!==e&&(n=e),void 0===t)return n;var i=n;return J.test(t)||!V.test(t)?i=parseInt(t,10):V.test(t)&&(i=1e3*parseFloat(t)),0>i&&(i=0),i==i?i:n}function c(t){X.debug&&window&&window.console.warn(t)}var l=function(t,e,i){function r(t){return"object"==(void 0===t?"undefined":n(t))}function o(t){return"function"==typeof t}function s(){}return function n(a,u){function c(){var t=new l;return o(t.init)&&t.init.apply(t,arguments),t}function l(){}u===i&&(u=a,a=Object),c.Bare=l;var f,h=s[t]=a[t],d=l[t]=c[t]=new s;return d.constructor=c,c.mixin=function(e){return l[t]=c[t]=n(c,e)[t],c},c.open=function(t){if(f={},o(t)?f=t.call(c,d,h,c,a):r(t)&&(f=t),r(f))for(var n in f)e.call(f,n)&&(d[n]=f[n]);return o(d.init)||(d.init=a),c},c.open(u)}}("prototype",{}.hasOwnProperty),f={ease:["ease",function(t,e,n,i){var r=(t/=i)*t,o=r*t;return e+n*(-2.75*o*r+11*r*r+-15.5*o+8*r+.25*t)}],"ease-in":["ease-in",function(t,e,n,i){var r=(t/=i)*t,o=r*t;return e+n*(-1*o*r+3*r*r+-3*o+2*r)}],"ease-out":["ease-out",function(t,e,n,i){var r=(t/=i)*t,o=r*t;return e+n*(.3*o*r+-1.6*r*r+2.2*o+-1.8*r+1.9*t)}],"ease-in-out":["ease-in-out",function(t,e,n,i){var r=(t/=i)*t,o=r*t;return e+n*(2*o*r+-5*r*r+2*o+2*r)}],linear:["linear",function(t,e,n,i){return n*t/i+e}],"ease-in-quad":["cubic-bezier(0.550, 0.085, 0.680, 0.530)",function(t,e,n,i){return n*(t/=i)*t+e}],"ease-out-quad":["cubic-bezier(0.250, 0.460, 0.450, 0.940)",function(t,e,n,i){return-n*(t/=i)*(t-2)+e}],"ease-in-out-quad":["cubic-bezier(0.455, 0.030, 0.515, 0.955)",function(t,e,n,i){return(t/=i/2)<1?n/2*t*t+e:-n/2*(--t*(t-2)-1)+e}],"ease-in-cubic":["cubic-bezier(0.550, 0.055, 0.675, 0.190)",function(t,e,n,i){return n*(t/=i)*t*t+e}],"ease-out-cubic":["cubic-bezier(0.215, 0.610, 0.355, 1)",function(t,e,n,i){return n*((t=t/i-1)*t*t+1)+e}],"ease-in-out-cubic":["cubic-bezier(0.645, 0.045, 0.355, 1)",function(t,e,n,i){return(t/=i/2)<1?n/2*t*t*t+e:n/2*((t-=2)*t*t+2)+e}],"ease-in-quart":["cubic-bezier(0.895, 0.030, 0.685, 0.220)",function(t,e,n,i){return n*(t/=i)*t*t*t+e}],"ease-out-quart":["cubic-bezier(0.165, 0.840, 0.440, 1)",function(t,e,n,i){return-n*((t=t/i-1)*t*t*t-1)+e}],"ease-in-out-quart":["cubic-bezier(0.770, 0, 0.175, 1)",function(t,e,n,i){return(t/=i/2)<1?n/2*t*t*t*t+e:-n/2*((t-=2)*t*t*t-2)+e}],"ease-in-quint":["cubic-bezier(0.755, 0.050, 0.855, 0.060)",function(t,e,n,i){return n*(t/=i)*t*t*t*t+e}],"ease-out-quint":["cubic-bezier(0.230, 1, 0.320, 1)",function(t,e,n,i){return n*((t=t/i-1)*t*t*t*t+1)+e}],"ease-in-out-quint":["cubic-bezier(0.860, 0, 0.070, 1)",function(t,e,n,i){return(t/=i/2)<1?n/2*t*t*t*t*t+e:n/2*((t-=2)*t*t*t*t+2)+e}],"ease-in-sine":["cubic-bezier(0.470, 0, 0.745, 0.715)",function(t,e,n,i){return-n*Math.cos(t/i*(Math.PI/2))+n+e}],"ease-out-sine":["cubic-bezier(0.390, 0.575, 0.565, 1)",function(t,e,n,i){return n*Math.sin(t/i*(Math.PI/2))+e}],"ease-in-out-sine":["cubic-bezier(0.445, 0.050, 0.550, 0.950)",function(t,e,n,i){return-n/2*(Math.cos(Math.PI*t/i)-1)+e}],"ease-in-expo":["cubic-bezier(0.950, 0.050, 0.795, 0.035)",function(t,e,n,i){return 0===t?e:n*Math.pow(2,10*(t/i-1))+e}],"ease-out-expo":["cubic-bezier(0.190, 1, 0.220, 1)",function(t,e,n,i){return t===i?e+n:n*(1-Math.pow(2,-10*t/i))+e}],"ease-in-out-expo":["cubic-bezier(1, 0, 0, 1)",function(t,e,n,i){return 0===t?e:t===i?e+n:(t/=i/2)<1?n/2*Math.pow(2,10*(t-1))+e:n/2*(2-Math.pow(2,-10*--t))+e}],"ease-in-circ":["cubic-bezier(0.600, 0.040, 0.980, 0.335)",function(t,e,n,i){return-n*(Math.sqrt(1-(t/=i)*t)-1)+e}],"ease-out-circ":["cubic-bezier(0.075, 0.820, 0.165, 1)",function(t,e,n,i){return n*Math.sqrt(1-(t=t/i-1)*t)+e}],"ease-in-out-circ":["cubic-bezier(0.785, 0.135, 0.150, 0.860)",function(t,e,n,i){return(t/=i/2)<1?-n/2*(Math.sqrt(1-t*t)-1)+e:n/2*(Math.sqrt(1-(t-=2)*t)+1)+e}],"ease-in-back":["cubic-bezier(0.600, -0.280, 0.735, 0.045)",function(t,e,n,i,r){return void 0===r&&(r=1.70158),n*(t/=i)*t*((r+1)*t-r)+e}],"ease-out-back":["cubic-bezier(0.175, 0.885, 0.320, 1.275)",function(t,e,n,i,r){return void 0===r&&(r=1.70158),n*((t=t/i-1)*t*((r+1)*t+r)+1)+e}],"ease-in-out-back":["cubic-bezier(0.680, -0.550, 0.265, 1.550)",function(t,e,n,i,r){return void 0===r&&(r=1.70158),(t/=i/2)<1?n/2*t*t*((1+(r*=1.525))*t-r)+e:n/2*((t-=2)*t*((1+(r*=1.525))*t+r)+2)+e}]},h={"ease-in-back":"cubic-bezier(0.600, 0, 0.735, 0.045)","ease-out-back":"cubic-bezier(0.175, 0.885, 0.320, 1)","ease-in-out-back":"cubic-bezier(0.680, 0, 0.265, 1)"},d=document,p=window,v="bkwld-tram",m=/[\-\.0-9]/g,w=/[A-Z]/,g="number",b=/^(rgb|#)/,y=/(em|cm|mm|in|pt|pc|px)$/,x=/(em|cm|mm|in|pt|pc|px|%)$/,_=/(deg|rad|turn)$/,k="unitless",z=/(all|none) 0s ease 0s/,T=/^(width|height)$/,E=" ",q=d.createElement("a"),O=["Webkit","Moz","O","ms"],A=["-webkit-","-moz-","-o-","-ms-"],S=function(t){if(t in q.style)return{dom:t,css:t};var e,n,i="",r=t.split("-");for(e=0;ec&&(c=t.span),t.stop(),t.animate(e)},function(t){"wait"in t&&(c=u(t.wait,0))}),h.call(this),c>0&&(this.timer=new N({duration:c,context:this}),this.active=!0,e&&(this.timer.complete=s));var p=this,v=!1,m={};B(function(){d.call(p,t,function(t){t.active&&(v=!0,m[t.name]=t.nextStyle)}),v&&p.$el.css(m)})}}}function s(){if(this.timer&&this.timer.destroy(),this.active=!1,this.queue.length){var t=this.queue.shift();o.call(this,t.options,!0,t.args)}}function a(t){var e;this.timer&&this.timer.destroy(),this.queue=[],this.active=!1,"string"==typeof t?(e={})[t]=1:e="object"==(void 0===t?"undefined":n(t))&&null!=t?t:this.props,d.call(this,e,p),h.call(this)}function l(){a.call(this),this.el.style.display="none"}function f(){this.el.offsetHeight}function h(){var t,e,n=[];for(t in this.upstream&&n.push(this.upstream),this.props)(e=this.props[t]).active&&n.push(e.string);n=n.join(","),this.style!==n&&(this.style=n,this.el.style[$.transition.dom]=n)}function d(t,e,n){var o,s,a,u,c=e!==p,l={};for(o in t)a=t[o],o in Q?(l.transform||(l.transform={}),l.transform[o]=a):(w.test(o)&&(o=i(o)),o in W?l[o]=a:(u||(u={}),u[o]=a));for(o in l){if(a=l[o],!(s=this.props[o])){if(!c)continue;s=r.call(this,o)}e.call(this,s,a)}n&&u&&n.call(this,u)}function p(t){t.stop()}function m(t,e){t.set(e)}function g(t){this.$el.css(t)}function b(t,n){e[t]=function(){return this.children?function(t,e){var n,i=this.children.length;for(n=0;i>n;n++)t.apply(this.children[n],e);return this}.call(this,n,arguments):(this.el&&n.apply(this,arguments),this)}}e.init=function(e){if(this.$el=t(e),this.el=this.$el[0],this.props={},this.queue=[],this.style="",this.active=!1,X.keepInherited&&!X.fallback){var n=G(this.el,"transition");n&&!z.test(n)&&(this.upstream=n)}$.backface&&X.hideBackface&&Y(this.el,$.backface.css,"hidden")},b("add",r),b("start",o),b("wait",function(t){t=u(t,0),this.active?this.queue.push({options:t}):(this.timer=new N({duration:t,context:this,complete:s}),this.active=!0)}),b("then",function(t){return this.active?(this.queue.push({options:t,args:arguments}),void(this.timer.complete=s)):c("No active transition timer. Use start() or wait() before then().")}),b("next",s),b("stop",a),b("set",function(t){a.call(this,t),d.call(this,t,m,g)}),b("show",function(t){"string"!=typeof t&&(t="block"),this.el.style.display=t}),b("hide",l),b("redraw",f),b("destroy",function(){a.call(this),t.removeData(this.el,v),this.$el=this.el=null})}),L=l(F,function(e){function n(e,n){var i=t.data(e,v)||t.data(e,v,new F.Bare);return i.el||i.init(e),n?i.start(n):i}e.init=function(e,i){var r=t(e);if(!r.length)return this;if(1===r.length)return n(r[0],i);var o=[];return r.each(function(t,e){o.push(n(e,i))}),this.children=o,this}}),D=l(function(t){function e(){var t=this.get();this.update("auto");var e=this.get();return this.update(t),e}function i(t){var e=/rgba?\((\d+),\s*(\d+),\s*(\d+)/.exec(t);return(e?o(e[1],e[2],e[3]):t).replace(/#(\w)(\w)(\w)$/,"#$1$1$2$2$3$3")}var r=500,s="ease",a=0;t.init=function(t,e,n,i){this.$el=t,this.el=t[0];var o=e[0];n[2]&&(o=n[2]),Z[o]&&(o=Z[o]),this.name=o,this.type=n[1],this.duration=u(e[1],this.duration,r),this.ease=function(t,e,n){return void 0!==e&&(n=e),t in f?t:n}(e[2],this.ease,s),this.delay=u(e[3],this.delay,a),this.span=this.duration+this.delay,this.active=!1,this.nextStyle=null,this.auto=T.test(this.name),this.unit=i.unit||this.unit||X.defaultUnit,this.angle=i.angle||this.angle||X.defaultAngle,X.fallback||i.fallback?this.animate=this.fallback:(this.animate=this.transition,this.string=this.name+E+this.duration+"ms"+("ease"!=this.ease?E+f[this.ease][0]:"")+(this.delay?E+this.delay+"ms":""))},t.set=function(t){t=this.convert(t,this.type),this.update(t),this.redraw()},t.transition=function(t){this.active=!0,t=this.convert(t,this.type),this.auto&&("auto"==this.el.style[this.name]&&(this.update(this.get()),this.redraw()),"auto"==t&&(t=e.call(this))),this.nextStyle=t},t.fallback=function(t){var n=this.el.style[this.name]||this.convert(this.get(),this.type);t=this.convert(t,this.type),this.auto&&("auto"==n&&(n=this.convert(this.get(),this.type)),"auto"==t&&(t=e.call(this))),this.tween=new C({from:n,to:t,duration:this.duration,delay:this.delay,ease:this.ease,update:this.update,context:this})},t.get=function(){return G(this.el,this.name)},t.update=function(t){Y(this.el,this.name,t)},t.stop=function(){(this.active||this.nextStyle)&&(this.active=!1,this.nextStyle=null,Y(this.el,this.name,this.get()));var t=this.tween;t&&t.context&&t.destroy()},t.convert=function(t,e){if("auto"==t&&this.auto)return t;var r,o="number"==typeof t,s="string"==typeof t;switch(e){case g:if(o)return t;if(s&&""===t.replace(m,""))return+t;r="number(unitless)";break;case b:if(s){if(""===t&&this.original)return this.original;if(e.test(t))return"#"==t.charAt(0)&&7==t.length?t:i(t)}r="hex or rgb string";break;case y:if(o)return t+this.unit;if(s&&e.test(t))return t;r="number(px) or string(unit)";break;case x:if(o)return t+this.unit;if(s&&e.test(t))return t;r="number(px) or string(unit or %)";break;case _:if(o)return t+this.angle;if(s&&e.test(t))return t;r="number(deg) or string(angle)";break;case k:if(o)return t;if(s&&x.test(t))return t;r="number(unitless) or string(unit or %)"}return function(t,e){c("Type warning: Expected: ["+t+"] Got: ["+(void 0===e?"undefined":n(e))+"] "+e)}(r,t),t},t.redraw=function(){this.el.offsetHeight}}),I=l(D,function(t,e){t.init=function(){e.init.apply(this,arguments),this.original||(this.original=this.convert(this.get(),b))}}),P=l(D,function(t,e){t.init=function(){e.init.apply(this,arguments),this.animate=this.fallback},t.get=function(){return this.$el[this.name]()},t.update=function(t){this.$el[this.name](t)}}),H=l(D,function(t,e){function n(t,e){var n,i,r,o,s;for(n in t)r=(o=Q[n])[0],i=o[1]||n,s=this.convert(t[n],r),e.call(this,i,s,r)}t.init=function(){e.init.apply(this,arguments),this.current||(this.current={},Q.perspective&&X.perspective&&(this.current.perspective=X.perspective,Y(this.el,this.name,this.style(this.current)),this.redraw()))},t.set=function(t){n.call(this,t,function(t,e){this.current[t]=e}),Y(this.el,this.name,this.style(this.current)),this.redraw()},t.transition=function(t){var e=this.values(t);this.tween=new U({current:this.current,values:e,duration:this.duration,delay:this.delay,ease:this.ease});var n,i={};for(n in this.current)i[n]=n in e?e[n]:this.current[n];this.active=!0,this.nextStyle=this.style(i)},t.fallback=function(t){var e=this.values(t);this.tween=new U({current:this.current,values:e,duration:this.duration,delay:this.delay,ease:this.ease,update:this.update,context:this})},t.update=function(){Y(this.el,this.name,this.style(this.current))},t.style=function(t){var e,n="";for(e in t)n+=e+"("+t[e]+") ";return n},t.values=function(t){var e,i={};return n.call(this,t,function(t,n,r){i[t]=n,void 0===this.current[t]&&(e=0,~t.indexOf("scale")&&(e=1),this.current[t]=this.convert(e,r))}),i}}),C=l(function(e){function n(){var t,e,i,r=u.length;if(r)for(B(n),e=R(),t=r;t--;)(i=u[t])&&i.render(e)}var i={ease:f.ease[1],from:0,to:1};e.init=function(t){this.duration=t.duration||0,this.delay=t.delay||0;var e=t.ease||i.ease;f[e]&&(e=f[e][1]),"function"!=typeof e&&(e=i.ease),this.ease=e,this.update=t.update||s,this.complete=t.complete||s,this.context=t.context||this,this.name=t.name;var n=t.from,r=t.to;void 0===n&&(n=i.from),void 0===r&&(r=i.to),this.unit=t.unit||"","number"==typeof n&&"number"==typeof r?(this.begin=n,this.change=r-n):this.format(r,n),this.value=this.begin+this.unit,this.start=R(),!1!==t.autoplay&&this.play()},e.play=function(){var t;this.active||(this.start||(this.start=R()),this.active=!0,t=this,1===u.push(t)&&B(n))},e.stop=function(){var e,n,i;this.active&&(this.active=!1,e=this,(i=t.inArray(e,u))>=0&&(n=u.slice(i+1),u.length=i,n.length&&(u=u.concat(n))))},e.render=function(t){var e,n=t-this.start;if(this.delay){if(n<=this.delay)return;n-=this.delay}if(n').attr("href","https://webflow.com?utm_campaign=brandjs"),i=t("").attr("src","https://d1otoma47x30pg.cloudfront.net/img/webflow-badge-icon.60efbf6ec9.svg").css({marginRight:"8px",width:"16px"}),s=t("").attr("src","https://d1otoma47x30pg.cloudfront.net/img/webflow-badge-text.6faa6a38cd.svg"),n.append(i,s),n[0]),h(),setTimeout(h,500),t(r).off(l,f).on(l,f))},n})},function(t,e,n){var i=window.$,r=n(1)&&i.tram; 8 | /*! 9 | * Webflow._ (aka) Underscore.js 1.6.0 (custom build) 10 | * _.each 11 | * _.map 12 | * _.find 13 | * _.filter 14 | * _.any 15 | * _.contains 16 | * _.delay 17 | * _.defer 18 | * _.throttle (webflow) 19 | * _.debounce 20 | * _.keys 21 | * _.has 22 | * _.now 23 | * 24 | * http://underscorejs.org 25 | * (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 26 | * Underscore may be freely distributed under the MIT license. 27 | * @license MIT 28 | */ 29 | t.exports=function(){var t={VERSION:"1.6.0-Webflow"},e={},n=Array.prototype,i=Object.prototype,o=Function.prototype,s=(n.push,n.slice),a=(n.concat,i.toString,i.hasOwnProperty),u=n.forEach,c=n.map,l=(n.reduce,n.reduceRight,n.filter),f=(n.every,n.some),h=n.indexOf,d=(n.lastIndexOf,Array.isArray,Object.keys),p=(o.bind,t.each=t.forEach=function(n,i,r){if(null==n)return n;if(u&&n.forEach===u)n.forEach(i,r);else if(n.length===+n.length){for(var o=0,s=n.length;o/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var m=/(.)^/,w={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},g=/\\|'|\r|\n|\u2028|\u2029/g,b=function(t){return"\\"+w[t]};return t.template=function(e,n,i){!n&&i&&(n=i),n=t.defaults({},n,t.templateSettings);var r=RegExp([(n.escape||m).source,(n.interpolate||m).source,(n.evaluate||m).source].join("|")+"|$","g"),o=0,s="__p+='";e.replace(r,function(t,n,i,r,a){return s+=e.slice(o,a).replace(g,b),o=a+t.length,n?s+="'+\n((__t=("+n+"))==null?'':_.escape(__t))+\n'":i?s+="'+\n((__t=("+i+"))==null?'':__t)+\n'":r&&(s+="';\n"+r+"\n__p+='"),t}),s+="';\n",n.variable||(s="with(obj||{}){\n"+s+"}\n"),s="var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};\n"+s+"return __p;\n";try{var a=new Function(n.variable||"obj","_",s)}catch(t){throw t.source=s,t}var u=function(e){return a.call(this,e,t)},c=n.variable||"obj";return u.source="function("+c+"){\n"+s+"}",u},t}()},function(t,e,n){var i=n(0),r=n(6);i.define("ix",t.exports=function(t,e){var n,o,s={},a=t(window),u=".w-ix",c=t.tram,l=i.env,f=l(),h=l.chrome&&l.chrome<35,d="none 0s ease 0s",p=t(),v={},m=[],w=[],g=[],b=1,y={tabs:".w-tab-link, .w-tab-pane",dropdown:".w-dropdown",slider:".w-slide",navbar:".w-nav"};function x(t){t&&(v={},e.each(t,function(t){v[t.slug]=t.value}),_())}function _(){!function(){var e=t("[data-ix]");if(!e.length)return;e.each(T),e.each(k),m.length&&(i.scroll.on(E),setTimeout(E,1));w.length&&i.load(q);g.length&&setTimeout(O,b)}(),r.init(),i.redraw.up()}function k(n,o){var a=t(o),c=a.attr("data-ix"),l=v[c];if(l){var h=l.triggers;h&&(s.style(a,l.style),e.each(h,function(t){var e={},n=t.type,o=t.stepsB&&t.stepsB.length;function s(){A(t,a,{group:"A"})}function c(){A(t,a,{group:"B"})}if("load"!==n){if("click"===n)return a.on("click"+u,function(n){i.validClick(n.currentTarget)&&("#"===a.attr("href")&&n.preventDefault(),A(t,a,{group:e.clicked?"B":"A"}),o&&(e.clicked=!e.clicked))}),void(p=p.add(a));if("hover"===n)return a.on("mouseenter"+u,s),a.on("mouseleave"+u,c),void(p=p.add(a));if("scroll"!==n){var l=y[n];if(l){var h=a.closest(l);return h.on(r.types.INTRO,s).on(r.types.OUTRO,c),void(p=p.add(h))}}else m.push({el:a,trigger:t,state:{active:!1},offsetTop:z(t.offsetTop),offsetBot:z(t.offsetBot)})}else t.preload&&!f?w.push(s):g.push(s)}))}}function z(t){if(!t)return 0;t=String(t);var e=parseInt(t,10);return e!=e?0:(t.indexOf("%")>0&&(e/=100)>=1&&(e=.999),e)}function T(e,n){t(n).off(u)}function E(){for(var t=a.scrollTop(),e=a.height(),n=m.length,i=0;i0&&(h*=e),d<1&&d>0&&(d*=e);var p=l+f-h>=t&&l+d<=t+e;p!==c.active&&((!1!==p||u)&&(c.active=p,A(s,o,{group:p?"A":"B"})))}}function q(){for(var t=w.length,e=0;e=0)){var s=t(e);if(0===i.indexOf("#")&&h.test(i)){var a=t(i);a.length&&r.push({link:s,sec:a,active:!1})}else if("#"!==i&&""!==i){var u=l.href===c.href||i===o||d.test(i)&&p.test(o);w(s,f,u)}}}function m(){var t=a.scrollTop(),n=a.height();e.each(r,function(e){var i=e.link,r=e.sec,o=r.offset().top,s=r.outerHeight(),a=.5*n,u=r.is(":visible")&&o+s-a>=t&&o+a<=t+n;e.active!==u&&(e.active=u,w(i,f,u))})}function w(t,e,n){var i=t.hasClass(e);n&&i||(n||i)&&(n?t.addClass(e):t.removeClass(e))}return s.ready=s.design=s.preview=function(){n=u&&i.env("design"),o=i.env("slug")||c.pathname||"",i.scroll.off(m),r=[];for(var t=document.links,e=0;e .header, "+c+" > .w-nav:not([data-no-scroll])"),f="fixed"===l.css("position")?l.outerHeight():0;n.setTimeout(function(){!function(e,i){var r=t(n).scrollTop(),o=e.offset().top-i;if("mid"===e.data("scroll")){var s=t(n).height()-i,a=e.outerHeight();a0)&&(u=e)}),Date.now||(Date.now=function(){return(new Date).getTime()});var c=Date.now(),l=n.requestAnimationFrame||n.mozRequestAnimationFrame||n.webkitRequestAnimationFrame||function(t){n.setTimeout(t,15)},f=(472.143*Math.log(Math.abs(r-o)+125)-2e3)*u;!function t(){var e=Date.now()-c;n.scroll(0,function(t,e,n,i){if(n>i)return e;return t+(e-t)*(r=n/i,r<.5?4*r*r*r:(r-1)*(2*r-2)*(2*r-2)+1);var r}(r,o,e,f)),e<=f&&l(t)}()}(u,f)},a?0:300)}}}return{ready:function(){r.hash&&a(r.hash.substring(1));var n=r.href.split("#")[0];e.on("click","a",function(e){if(!(i.env("design")||window.$.mobile&&t(e.currentTarget).hasClass("ui-link")))if("#"!==this.getAttribute("href")){var r=this.href.split("#"),o=r[0]===n?r[1]:null;o&&a(o,e)}else e.preventDefault()})}}})},function(t,e,n){n(0).define("touch",t.exports=function(t){var e={},n=!document.addEventListener,i=window.getSelection;function r(e,n,i){var r=t.Event(e,{originalEvent:n});t(n.target).trigger(r,i)}return n&&(t.event.special.tap={bindType:"click",delegateType:"click"}),e.init=function(e){return n?null:(e="string"==typeof e?t(e).get(0):e)?new function(t){var e,n,o,s=!1,a=!1,u=!1,c=Math.min(Math.round(.04*window.innerWidth),40);function l(t){var i=t.touches;i&&i.length>1||(s=!0,a=!1,i?(u=!0,e=i[0].clientX,n=i[0].clientY):(e=t.clientX,n=t.clientY),o=e)}function f(t){if(s){if(u&&"mousemove"===t.type)return t.preventDefault(),void t.stopPropagation();var l=t.touches,f=l?l[0].clientX:t.clientX,h=l?l[0].clientY:t.clientY,p=f-o;o=f,Math.abs(p)>c&&i&&""===String(i())&&(r("swipe",t,{direction:p>0?"right":"left"}),d()),(Math.abs(f-e)>10||Math.abs(h-n)>10)&&(a=!0)}}function h(t){if(s){if(s=!1,u&&"mouseup"===t.type)return t.preventDefault(),t.stopPropagation(),void(u=!1);a||r("tap",t)}}function d(){s=!1}t.addEventListener("touchstart",l,!1),t.addEventListener("touchmove",f,!1),t.addEventListener("touchend",h,!1),t.addEventListener("touchcancel",d,!1),t.addEventListener("mousedown",l,!1),t.addEventListener("mousemove",f,!1),t.addEventListener("mouseup",h,!1),t.addEventListener("mouseout",d,!1),this.destroy=function(){t.removeEventListener("touchstart",l,!1),t.removeEventListener("touchmove",f,!1),t.removeEventListener("touchend",h,!1),t.removeEventListener("touchcancel",d,!1),t.removeEventListener("mousedown",l,!1),t.removeEventListener("mousemove",f,!1),t.removeEventListener("mouseup",h,!1),t.removeEventListener("mouseout",d,!1),t=null}}(e):null},e.instance=e.init(document),e})}]);/** 30 | * ---------------------------------------------------------------------- 31 | * Webflow: Interactions: Init 32 | */ 33 | Webflow.require('ix').init([ 34 | {"slug":"new-interaction","name":"New Interaction","value":{"style":{},"triggers":[{"type":"click","stepsA":[{"display":"flex"}],"stepsB":[]}]}}, 35 | {"slug":"display-none","name":"Display None","value":{"style":{"display":"none"},"triggers":[]}} 36 | ]); 37 | --------------------------------------------------------------------------------