├── .appcast.xml ├── .eslintrc.yml ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── assets └── icon.png ├── package-lock.json ├── package.json ├── prettier.config.js ├── src ├── __tests__ │ ├── init.test.js │ └── selection-changed.test.js ├── constants.js ├── init.js ├── manifest.json ├── open-preferences.js ├── selection-changed.js ├── test-utils.js └── utils.js └── xib-views ├── Info.plist ├── LayerStylePreferences.xib ├── TextStylePreferences.xib ├── xib-views.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── mathieudutour.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── mathieudutour.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist └── xib_views.h /.appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | root: true 3 | extends: 4 | - airbnb 5 | - plugin:import/errors 6 | - prettier 7 | plugins: 8 | - no-not-accumulator-reassign 9 | - prettier 10 | env: 11 | node: true 12 | rules: 13 | import/no-unresolved: 14 | - error 15 | - commonjs: true 16 | caseSensitive: true 17 | ignore: 18 | - '^sketch(\/|$)' 19 | no-param-reassign: 0 20 | import/no-extraneous-dependencies: 0 21 | jsx-a11y/accessible-emoji: 0 22 | no-underscore-dangle: 0 23 | react/no-array-index-key: 0 24 | react/require-default-props: 0 25 | import/prefer-default-export: 0 26 | no-plusplus: 0 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build artifacts 2 | sketch-styles-hierarchy.sketchplugin 3 | 4 | # npm 5 | node_modules 6 | .npm 7 | npm-debug.log 8 | 9 | # mac 10 | .DS_Store 11 | 12 | # WebStorm 13 | .idea 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shared Styles Hierarchy 2 | 3 | _Organize your shared styles using the layers list, create a hierarchy where children inherit properties from their parent._ 4 | 5 | ## Installation 6 | 7 | _Requires Sketch >= 53_ 8 | 9 | - [Download](https://github.com/mathieudutour/sketch-styles-hierarchy/releases/latest) the latest release of the plugin 10 | - Un-zip 11 | - Double-click on sketch-styles-hierarchy.sketchplugin 12 | 13 | ## Usage 14 | 15 | - You run the `setup` command which creates 2 new pages: `Text Styles` and `Layer Styles`. 16 | - Then you use the layer list and the inspector to manage the shared styles. 17 | 18 | Let's say you have 4 text styles: `Button/Primary`, `Button/Secondary`, `Header/H1` and `Header/H2`. 19 | You should have 2 groups: `Button` and `Header` with 3 text layers in each: `Primary` (or `H1`), `Secondary` (or `H2`) and `__default`. 20 | 21 | You now have multiple possible actions (each triggered after changing the selection): 22 | 23 | - rename a group to rename all the children (eg. renaming the `Header` group to `Heading` will change `Header/H1` to `Heading/H1` and `Header/H2` to `Heading/H2`) 24 | - rename a layer to rename the shared style 25 | - create a new Group (eg. creating a new Group called `Inverted` from `Button/Secondary` will rename `Button/Secondary` to `Button/Inverted/Secondary` and create a new `__default` layer) 26 | - drag and drop layers in the hierarchy to rename them 27 | - delete groups or layers to delete the Shared Styles 28 | - create new layers to create new shared styles 29 | 30 | Now you might wonder what the `__default` layers are for, and that's where the magic comes in :sparkles: 31 | 32 | - select a `__default` layer and press `cmd + shift + ,` to be able to select which style properties its children should inherit (the `__default` is linked to a group, so the children are the children of the parent group of the `__default` layer) 33 | - then, every time you change the `__default` style, the children will change as well 34 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathieudutour/sketch-styles-hierarchy/720a815b7ad2b64102333d5ca8d51a25bfd8179f/assets/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sketch-styles-hierarchy", 3 | "description": "Organize your shared styles using the layers list, create a hierarchy where children inherit properties from their parent.", 4 | "version": "0.1.1", 5 | "engines": { 6 | "sketch": ">=3.0" 7 | }, 8 | "skpm": { 9 | "name": "Shared Styles Hierarchy", 10 | "manifest": "src/manifest.json", 11 | "main": "sketch-styles-hierarchy.sketchplugin", 12 | "assets": [ 13 | "assets/**/*" 14 | ] 15 | }, 16 | "scripts": { 17 | "build": "skpm-build", 18 | "watch": "skpm-build --watch", 19 | "start": "skpm-build --watch --run", 20 | "postinstall": "npm run build && skpm-link", 21 | "lint-staged": "lint-staged", 22 | "lint": "eslint --ignore-path=.gitignore .", 23 | "prettier:base": "prettier --write", 24 | "test": "skpm-test" 25 | }, 26 | "devDependencies": { 27 | "@skpm/builder": "^0.5.11", 28 | "@skpm/test-runner": "^0.3.9", 29 | "eslint": "^5.13.0", 30 | "eslint-config-airbnb": "^17.1.0", 31 | "eslint-config-prettier": "^4.0.0", 32 | "eslint-plugin-import": "^2.16.0", 33 | "eslint-plugin-jsx-a11y": "^6.2.1", 34 | "eslint-plugin-no-not-accumulator-reassign": "^0.1.0", 35 | "eslint-plugin-prettier": "^3.0.1", 36 | "eslint-plugin-react": "^7.12.4", 37 | "lint-staged": "^8.1.3", 38 | "pre-commit": "^1.2.2", 39 | "prettier": "^1.16.4" 40 | }, 41 | "author": "Mathieu Dutour ", 42 | "dependencies": { 43 | "sketch-module-web-view": "^2.1.2" 44 | }, 45 | "pre-commit": [ 46 | "lint-staged" 47 | ], 48 | "lint-staged": { 49 | "*.js": [ 50 | "npm run prettier:base", 51 | "eslint --quiet --rule 'prettier/prettier: [\"error\", {\"trailingComma\": \"es5\", \"singleQuote\": true}]'", 52 | "git add" 53 | ] 54 | }, 55 | "repository": { 56 | "type": "git", 57 | "url": "git+https://github.com/mathieudutour/sketch-styles-hierarchy.git" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'es5', 4 | semi: false, 5 | } 6 | -------------------------------------------------------------------------------- /src/__tests__/init.test.js: -------------------------------------------------------------------------------- 1 | /* globals test, expect */ 2 | import { initDummyDocument } from '../test-utils' 3 | 4 | test('should create 2 pages', (_, document) => { 5 | expect(document.pages.length).toBe(1) 6 | initDummyDocument(document) 7 | expect(document.pages.length).toBe(3) 8 | }) 9 | -------------------------------------------------------------------------------- /src/__tests__/selection-changed.test.js: -------------------------------------------------------------------------------- 1 | /* globals test, expect */ 2 | import { initDummyDocument } from '../test-utils' 3 | import { onSelectionChanged } from '../selection-changed' 4 | 5 | test('should not do anything if there was no selection', (_, document) => { 6 | initDummyDocument(document) 7 | const context = { 8 | actionContext: { 9 | document: document.sketchObject, 10 | oldSelection: [], 11 | }, 12 | } 13 | expect(onSelectionChanged(context)).toBe('no old selection') 14 | }) 15 | 16 | test('should not do anything if there are more than one layer selected', (_, document) => { 17 | initDummyDocument(document) 18 | const context = { 19 | actionContext: { 20 | document: document.sketchObject, 21 | oldSelection: [ 22 | document.pages[1].layers[0].layers[0].sketchObject, 23 | document.pages[1].layers[0].layers[1].sketchObject, 24 | ], 25 | }, 26 | } 27 | expect(onSelectionChanged(context)).toBe('too big selection') 28 | }) 29 | 30 | test('should not do anything if the selection is from another page', (_, document) => { 31 | initDummyDocument(document) 32 | const context = { 33 | actionContext: { 34 | document: document.sketchObject, 35 | oldSelection: [document.pages[0].sketchObject], 36 | }, 37 | } 38 | expect(onSelectionChanged(context)).toBe('ignoring because wrong page') 39 | }) 40 | 41 | test('should not do anything if the selection is not the right type', (_, document) => { 42 | initDummyDocument(document) 43 | const context = { 44 | actionContext: { 45 | document: document.sketchObject, 46 | oldSelection: [document.pages[2].layers[0].sketchObject], 47 | }, 48 | } 49 | expect(onSelectionChanged(context)).toBe( 50 | 'ignoring because wrong type Artboard' 51 | ) 52 | }) 53 | 54 | test('should delete the shared style when deleting a layer', (_, document) => { 55 | initDummyDocument(document) 56 | let sharedStyles = ( 57 | document.sharedTextStyles || document.getSharedTextStyles() 58 | ).filter(s => !s.getLibrary()) 59 | expect(sharedStyles.length).toBe(4) 60 | document.pages[1].selected = true 61 | const layer = document.pages[1].layers[0].layers[1].layers[1] 62 | const sharedStyleName = layer.sharedStyle.name 63 | layer.parent = undefined 64 | const context = { 65 | actionContext: { 66 | document: document.sketchObject, 67 | oldSelection: [layer.sketchObject], 68 | }, 69 | } 70 | expect(onSelectionChanged(context)).toBe('handled deleted') 71 | sharedStyles = ( 72 | document.sharedTextStyles || document.getSharedTextStyles() 73 | ).filter(s => !s.getLibrary()) 74 | expect(sharedStyles.length).toBe(3) 75 | expect(sharedStyles.find(s => s.name === sharedStyleName)).toBe(undefined) 76 | }) 77 | 78 | test('should delete the shared styles when deleting a group', (_, document) => { 79 | initDummyDocument(document) 80 | let sharedStyles = ( 81 | document.sharedTextStyles || document.getSharedTextStyles() 82 | ).filter(s => !s.getLibrary()) 83 | expect(sharedStyles.length).toBe(4) 84 | document.pages[1].selected = true 85 | const group = document.pages[1].layers[0].layers[1] 86 | group.parent = undefined 87 | const context = { 88 | actionContext: { 89 | document: document.sketchObject, 90 | oldSelection: [group.sketchObject], 91 | }, 92 | } 93 | expect(onSelectionChanged(context)).toBe('handled deleted') 94 | sharedStyles = ( 95 | document.sharedTextStyles || document.getSharedTextStyles() 96 | ).filter(s => !s.getLibrary()) 97 | expect(sharedStyles.length).toBe(2) 98 | }) 99 | 100 | test('should ignore a group without style inside', (_, document) => { 101 | initDummyDocument(document) 102 | 103 | document.pages[1].layers[0].layers.push({ 104 | type: 'Group', 105 | }) 106 | 107 | const group = document.pages[1].layers[0].layers[2] 108 | const context = { 109 | actionContext: { 110 | document: document.sketchObject, 111 | oldSelection: [group.sketchObject], 112 | }, 113 | } 114 | expect(onSelectionChanged(context)).toBe( 115 | 'A group without styles inside is forbidden' 116 | ) 117 | }) 118 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const TEXT_STYLES_PAGE = 'Text styles' 2 | export const LAYER_STYLES_PAGE = 'Layer styles' 3 | 4 | export const ROOT_STYLE_NAME = '__default' 5 | 6 | // https://fonts.google.com 7 | export const TEXT_EXAMPLES = [ 8 | 'All their equipment and\ninstruments are alive.', 9 | 'A red flare silhouetted\nthe jagged edge of a wing.', 10 | 'I watched the storm, so\nbeautiful yet terrific.', 11 | 'Almost before we knew it,\nwe had left the ground.', 12 | 'A shining crescent far\nbeneath the flying vessel.', 13 | 'It was going to be a\nlonely trip back.', 14 | 'Mist enveloped the ship\nthree hours out from port.', 15 | 'My two natures had\nmemory in common.', 16 | 'Silver mist suffused\nthe deck of the ship.', 17 | 'The face of the moon\nwas in shadow.', 18 | 'She stared through\nthe window at the stars.', 19 | 'The recorded voice\nscratched in the speaker.', 20 | 'The sky was cloudless\nand of a deep dark blue.', 21 | 'The spectacle before\nus was indeed sublime.', 22 | 'Then came the night of\nthe first falling star.', 23 | 'Waves flung themselves\nat the blue evening.', 24 | ] 25 | -------------------------------------------------------------------------------- /src/init.js: -------------------------------------------------------------------------------- 1 | import sketch from 'sketch' 2 | import { TEXT_STYLES_PAGE, LAYER_STYLES_PAGE } from './constants' 3 | import { getHierarchy, updateLayerHierarchy } from './utils' 4 | 5 | const buildLayerHierarchy = (parent, name, hierarchy, isTextStyle) => { 6 | const artboard = new sketch.Artboard({ 7 | parent, 8 | name, 9 | frame: { 10 | x: 0, 11 | y: 0, 12 | width: 1000, 13 | height: 1000, 14 | }, 15 | }) 16 | 17 | updateLayerHierarchy(artboard, hierarchy, isTextStyle) 18 | } 19 | 20 | export function main(document) { 21 | const textStylesPage = 22 | document.pages.find(p => p.name === TEXT_STYLES_PAGE) || 23 | new sketch.Page({ 24 | name: TEXT_STYLES_PAGE, 25 | parent: document, 26 | }) 27 | 28 | const layerStylesPage = 29 | document.pages.find(p => p.name === LAYER_STYLES_PAGE) || 30 | new sketch.Page({ 31 | name: LAYER_STYLES_PAGE, 32 | parent: document, 33 | }) 34 | 35 | textStylesPage.layers = [] 36 | layerStylesPage.layers = [] 37 | 38 | const textStylesHierachy = getHierarchy(document, true) 39 | const layerStylesHierachy = getHierarchy(document, false) 40 | 41 | buildLayerHierarchy(textStylesPage, 'Text Styles', textStylesHierachy, true) 42 | buildLayerHierarchy( 43 | layerStylesPage, 44 | 'Layer Styles', 45 | layerStylesHierachy, 46 | false 47 | ) 48 | } 49 | 50 | export default function() { 51 | const document = sketch.getSelectedDocument() 52 | 53 | main(document) 54 | } 55 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "compatibleVersion": 3, 3 | "bundleVersion": 1, 4 | "icon": "icon.png", 5 | "commands": [ 6 | { 7 | "name": "Setup (once per file)", 8 | "identifier": "sketch-styles-hierarchy.init", 9 | "script": "./init.js" 10 | }, 11 | { 12 | "name": "Hierarchy preferences", 13 | "identifier": "sketch-styles-hierarchy.preferences", 14 | "script": "./open-preferences.js", 15 | "shortcut": "cmd + shift + ," 16 | }, 17 | { 18 | "name": "Handle Selection Change", 19 | "identifier": "sketch-styles-hierarchy.selection-changed", 20 | "script": "./selection-changed.js", 21 | "handlers": { 22 | "actions": { 23 | "SelectionChanged.finish": "onSelectionChanged" 24 | } 25 | } 26 | } 27 | ], 28 | "menu": { 29 | "title": "sketch-styles-hierarchy", 30 | "items": [ 31 | "sketch-styles-hierarchy.preferences", 32 | "-", 33 | "sketch-styles-hierarchy.init" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/open-preferences.js: -------------------------------------------------------------------------------- 1 | /* globals NSAlert, __command, NSAlertFirstButtonReturn, NSOnState, NSOffState */ 2 | import * as sketch from 'sketch' 3 | import { 4 | TEXT_STYLES_PAGE, 5 | LAYER_STYLES_PAGE, 6 | ROOT_STYLE_NAME, 7 | } from './constants' 8 | import { updateChildren, updateLayout, getHierarchy } from './utils' 9 | 10 | const TextStylePreferencesUI = require('../xib-views/TextStylePreferences.xib') 11 | const LayerStylePreferencesUI = require('../xib-views/LayerStylePreferences.xib') 12 | 13 | export default function() { 14 | const document = sketch.getSelectedDocument() 15 | 16 | const selection = document.selectedLayers 17 | 18 | if (!selection.length || selection.length > 1) { 19 | sketch.UI.message('You need to select 1 layer') 20 | return 21 | } 22 | 23 | const layer = selection.layers[0] 24 | const parentPage = layer.getParentPage() 25 | 26 | if ( 27 | !parentPage || 28 | (parentPage.name !== TEXT_STYLES_PAGE && 29 | parentPage.name !== LAYER_STYLES_PAGE) 30 | ) { 31 | sketch.UI.message('Only for layers in the right pages') 32 | return 33 | } 34 | 35 | const isTextStyle = parentPage.name === TEXT_STYLES_PAGE 36 | 37 | if ( 38 | (isTextStyle || layer.type !== 'ShapePath') && 39 | (!isTextStyle || layer.type !== 'Text') 40 | ) { 41 | sketch.UI.message('Only for layers of the right types') 42 | return 43 | } 44 | 45 | if (layer.name !== ROOT_STYLE_NAME) { 46 | sketch.UI.message('Only for default style layers') 47 | return 48 | } 49 | 50 | let preferencesUI 51 | 52 | if (isTextStyle) { 53 | preferencesUI = TextStylePreferencesUI() 54 | } else { 55 | preferencesUI = LayerStylePreferencesUI() 56 | } 57 | 58 | let inheritance = 59 | sketch.Settings.layerSettingForKey(layer, 'inheritance') || {} 60 | 61 | preferencesUI.opacity.state = inheritance.opacity ? NSOnState : NSOffState 62 | preferencesUI.blendingMode.state = inheritance.blendingMode 63 | ? NSOnState 64 | : NSOffState 65 | preferencesUI.borderOptions.state = inheritance.borderOptions 66 | ? NSOnState 67 | : NSOffState 68 | preferencesUI.blur.state = inheritance.blur ? NSOnState : NSOffState 69 | preferencesUI.fills.state = inheritance.fills ? NSOnState : NSOffState 70 | preferencesUI.borders.state = inheritance.borders ? NSOnState : NSOffState 71 | preferencesUI.shadows.state = inheritance.shadows ? NSOnState : NSOffState 72 | preferencesUI.innerShadows.state = inheritance.innerShadows 73 | ? NSOnState 74 | : NSOffState 75 | 76 | if (isTextStyle) { 77 | preferencesUI.alignment.state = inheritance.alignment 78 | ? NSOnState 79 | : NSOffState 80 | preferencesUI.verticalAlignment.state = inheritance.verticalAlignment 81 | ? NSOnState 82 | : NSOffState 83 | preferencesUI.kerning.state = inheritance.kerning ? NSOnState : NSOffState 84 | preferencesUI.lineHeight.state = inheritance.lineHeight 85 | ? NSOnState 86 | : NSOffState 87 | preferencesUI.paragraphSpacing.state = inheritance.paragraphSpacing 88 | ? NSOnState 89 | : NSOffState 90 | preferencesUI.textColor.state = inheritance.textColor 91 | ? NSOnState 92 | : NSOffState 93 | preferencesUI.fontSize.state = inheritance.fontSize ? NSOnState : NSOffState 94 | preferencesUI.textTransform.state = inheritance.textTransform 95 | ? NSOnState 96 | : NSOffState 97 | preferencesUI.fontFamily.state = inheritance.fontFamily 98 | ? NSOnState 99 | : NSOffState 100 | preferencesUI.fontWeight.state = inheritance.fontWeight 101 | ? NSOnState 102 | : NSOffState 103 | preferencesUI.fontStyle.state = inheritance.fontStyle 104 | ? NSOnState 105 | : NSOffState 106 | preferencesUI.fontVariant.state = inheritance.fontVariant 107 | ? NSOnState 108 | : NSOffState 109 | preferencesUI.fontStretch.state = inheritance.fontStretch 110 | ? NSOnState 111 | : NSOffState 112 | preferencesUI.textUnderline.state = inheritance.textUnderline 113 | ? NSOnState 114 | : NSOffState 115 | preferencesUI.textStrikethrough.state = inheritance.textStrikethrough 116 | ? NSOnState 117 | : NSOffState 118 | } 119 | 120 | const dialog = NSAlert.alloc().init() 121 | 122 | dialog.setMessageText('Style properties to pass down') 123 | dialog.setInformativeText( 124 | 'Select the properties of the style that the children will inherit.' 125 | ) 126 | dialog.addButtonWithTitle('Save') 127 | dialog.addButtonWithTitle('Cancel') 128 | dialog.setAccessoryView(preferencesUI.getRoot()) 129 | dialog.icon = __command.pluginBundle().alertIcon() 130 | 131 | const responseCode = dialog.runModal() 132 | 133 | if (responseCode !== NSAlertFirstButtonReturn) { 134 | return 135 | } 136 | 137 | inheritance = { 138 | opacity: preferencesUI.opacity.state() === NSOnState, 139 | blendingMode: preferencesUI.blendingMode.state() === NSOnState, 140 | borderOptions: preferencesUI.borderOptions.state() === NSOnState, 141 | blur: preferencesUI.blur.state() === NSOnState, 142 | fills: preferencesUI.fills.state() === NSOnState, 143 | borders: preferencesUI.borders.state() === NSOnState, 144 | shadows: preferencesUI.shadows.state() === NSOnState, 145 | innerShadows: preferencesUI.innerShadows.state() === NSOnState, 146 | } 147 | 148 | if (isTextStyle) { 149 | Object.assign(inheritance, { 150 | alignment: preferencesUI.alignment.state() === NSOnState, 151 | verticalAlignment: preferencesUI.verticalAlignment.state() === NSOnState, 152 | kerning: preferencesUI.kerning.state() === NSOnState, 153 | lineHeight: preferencesUI.lineHeight.state() === NSOnState, 154 | paragraphSpacing: preferencesUI.paragraphSpacing.state() === NSOnState, 155 | textColor: preferencesUI.textColor.state() === NSOnState, 156 | fontSize: preferencesUI.fontSize.state() === NSOnState, 157 | textTransform: preferencesUI.textTransform.state() === NSOnState, 158 | fontFamily: preferencesUI.fontFamily.state() === NSOnState, 159 | fontWeight: preferencesUI.fontWeight.state() === NSOnState, 160 | fontStyle: preferencesUI.fontStyle.state() === NSOnState, 161 | fontVariant: preferencesUI.fontVariant.state() === NSOnState, 162 | fontStretch: preferencesUI.fontStretch.state() === NSOnState, 163 | textUnderline: preferencesUI.textUnderline.state() === NSOnState, 164 | textStrikethrough: preferencesUI.textStrikethrough.state() === NSOnState, 165 | }) 166 | } 167 | 168 | sketch.Settings.setLayerSettingForKey(layer, 'inheritance', inheritance) 169 | 170 | // update the children 171 | updateChildren(layer.parent) 172 | 173 | updateLayout(document, getHierarchy(document, isTextStyle), isTextStyle) 174 | } 175 | -------------------------------------------------------------------------------- /src/selection-changed.js: -------------------------------------------------------------------------------- 1 | import * as sketch from 'sketch' 2 | import { 3 | TEXT_STYLES_PAGE, 4 | LAYER_STYLES_PAGE, 5 | ROOT_STYLE_NAME, 6 | } from './constants' 7 | import { 8 | getHierarchy, 9 | getParts, 10 | createSharedStyle, 11 | updateLayout, 12 | getTextExample, 13 | updateChildren, 14 | } from './utils' 15 | 16 | function updateStyleNames(hierarchy, newName) { 17 | if (hierarchy.style) { 18 | hierarchy.style.name = newName 19 | } 20 | hierarchy.children.forEach(c => { 21 | updateStyleNames(c, `${newName} / ${c.name}`) 22 | }) 23 | } 24 | 25 | function getPreviousParts(group) { 26 | const { layers } = group 27 | const layerWithSharedStyle = layers.find(l => l.sharedStyle) 28 | if (!layerWithSharedStyle) { 29 | return undefined 30 | } 31 | const parts = getParts(layerWithSharedStyle.sharedStyle) 32 | if (layerWithSharedStyle.name === ROOT_STYLE_NAME) { 33 | return parts 34 | } 35 | parts.pop() 36 | return parts 37 | } 38 | 39 | function deleteSharedStyles(isTextStyle, group, document) { 40 | if (group.sharedStyle) { 41 | const documentData = document._getMSDocumentData() 42 | const container = documentData.sharedObjectContainerOfType( 43 | isTextStyle ? 2 : 1 44 | ) 45 | container.removeSharedObject(group.sharedStyle.sketchObject) 46 | } 47 | 48 | if (group.layers) { 49 | group.layers.forEach(l => { 50 | deleteSharedStyles(isTextStyle, l, document) 51 | }) 52 | } 53 | } 54 | 55 | export function onSelectionChanged(context) { 56 | if (!context.actionContext || !context.actionContext.oldSelection) { 57 | return 'no old selection' 58 | } 59 | 60 | if (!context.actionContext.oldSelection.length) { 61 | return 'no old selection' 62 | } 63 | 64 | if (context.actionContext.oldSelection.length > 1) { 65 | return 'too big selection' 66 | } 67 | 68 | const oldSelection = sketch.fromNative(context.actionContext.oldSelection[0]) 69 | const wasDeleted = !oldSelection.getParentPage() 70 | const document = sketch.fromNative(context.actionContext.document) 71 | const parentPage = wasDeleted 72 | ? document.selectedPage 73 | : oldSelection.getParentPage() 74 | 75 | if ( 76 | !parentPage || 77 | (parentPage.name !== TEXT_STYLES_PAGE && 78 | parentPage.name !== LAYER_STYLES_PAGE) 79 | ) { 80 | return 'ignoring because wrong page' 81 | } 82 | 83 | const isTextStyle = parentPage.name === TEXT_STYLES_PAGE 84 | 85 | if ( 86 | oldSelection.type !== 'Group' && 87 | (isTextStyle || oldSelection.type !== 'ShapePath') && 88 | (!isTextStyle || oldSelection.type !== 'Text') 89 | ) { 90 | console.log(oldSelection.type) 91 | return `ignoring because wrong type ${oldSelection.type}` 92 | } 93 | 94 | if (wasDeleted) { 95 | deleteSharedStyles(isTextStyle, oldSelection, document) 96 | 97 | updateLayout(document, getHierarchy(document, isTextStyle), isTextStyle) 98 | return 'handled deleted' 99 | } 100 | 101 | const sharedStylesHierachy = getHierarchy(document, isTextStyle) 102 | 103 | const newParts = [oldSelection.name] 104 | let current = oldSelection.parent 105 | while (current && current.type === 'Group') { 106 | newParts.unshift(current.name) 107 | current = current.parent 108 | } 109 | 110 | if (oldSelection.type === 'Group') { 111 | const previousParts = getPreviousParts(oldSelection) 112 | 113 | if (!previousParts) { 114 | sketch.UI.message('A group without styles inside is forbidden') 115 | return 'A group without styles inside is forbidden' 116 | } 117 | 118 | if (!oldSelection.layers.find(l => l.name === ROOT_STYLE_NAME)) { 119 | // it's a new group. Let's create the default style 120 | oldSelection.layers.push({ 121 | name: ROOT_STYLE_NAME, 122 | type: isTextStyle ? 'Text' : 'ShapePath', 123 | frame: { 124 | x: 0, 125 | y: 0, 126 | width: 50, 127 | height: 50, 128 | }, 129 | }) 130 | 131 | const defaultLayer = oldSelection.layers[oldSelection.layers.length - 1] 132 | 133 | if (isTextStyle) { 134 | defaultLayer.text = getTextExample() 135 | defaultLayer.name = ROOT_STYLE_NAME 136 | defaultLayer.adjustToFit() 137 | defaultLayer.frame.x = 0 138 | defaultLayer.frame.y = 0 139 | } else { 140 | defaultLayer.style = { 141 | fills: ['#000'], 142 | } 143 | } 144 | 145 | // set the inheritance setting like the parent 146 | const parentDefaultStyle = oldSelection.parent 147 | ? oldSelection.parent.layers.find(l => l.name === ROOT_STYLE_NAME) 148 | : undefined 149 | if (parentDefaultStyle) { 150 | sketch.Settings.setLayerSettingForKey( 151 | defaultLayer, 152 | 'inheritance', 153 | sketch.Settings.layerSettingForKey( 154 | parentDefaultStyle, 155 | 'inheritance' 156 | ) || {} 157 | ) 158 | } 159 | 160 | // set the style depending on the parent 161 | updateChildren(oldSelection.parent) 162 | 163 | updateLayout(document, sharedStylesHierachy, isTextStyle) 164 | return 'handled new group' 165 | } 166 | 167 | if ( 168 | previousParts.length === newParts.length && 169 | previousParts.every((p, i) => p === newParts[i]) 170 | ) { 171 | // nothing changed 172 | return 'nothing changed' 173 | } 174 | 175 | // we might have changed the name 176 | const foundParts = [] 177 | let currentHierarchy = { children: sharedStylesHierachy } 178 | let index = currentHierarchy.children.findIndex( 179 | x => x.name === newParts[foundParts.length] 180 | ) 181 | while (index !== -1) { 182 | currentHierarchy = currentHierarchy.children[index] 183 | foundParts.push(currentHierarchy.name) 184 | index = currentHierarchy.children.findIndex( 185 | x => x.name === newParts[foundParts.length] 186 | ) 187 | } 188 | 189 | if (foundParts.length === newParts.length) { 190 | // nothing changed but should have 191 | sketch.UI.message( 192 | 'probably means that 2 groups are called the same, bad!' 193 | ) 194 | return 'probably means that 2 groups are called the same, bad!' 195 | } 196 | 197 | // we need to find the group that changed 198 | const siblings = oldSelection.parent.layers.filter( 199 | l => l.name !== ROOT_STYLE_NAME && l.id !== oldSelection.id 200 | ) 201 | const childLeft = currentHierarchy.children.filter( 202 | c => !siblings.find(s => s.name === c.name) 203 | ) 204 | if (childLeft.length > 1) { 205 | sketch.UI.message('too many children left!') 206 | return 'too many children left!' 207 | } 208 | // it's been moved or renamed 209 | updateStyleNames(childLeft[0], newParts.join(' / ')) 210 | 211 | updateLayout(document, getHierarchy(document, isTextStyle), isTextStyle) 212 | return 'handle renamed group' 213 | } 214 | 215 | // now we are dealing with a proper layer 216 | 217 | // a layer shared style should be up to date with its style at all time 218 | if ( 219 | oldSelection.sharedStyle && 220 | oldSelection.style.isOutOfSyncWithSharedStyle(oldSelection.sharedStyle) 221 | ) { 222 | oldSelection.sharedStyle.style = oldSelection.style 223 | } 224 | 225 | if (oldSelection.name === ROOT_STYLE_NAME) { 226 | newParts.pop() 227 | // update the children 228 | updateChildren(oldSelection.parent) 229 | } else if (!oldSelection.sharedStyle) { 230 | // we create a new layer so we need to transform it into a shared style 231 | const sharedStyle = createSharedStyle( 232 | isTextStyle, 233 | newParts.join(' / '), 234 | oldSelection.style, 235 | document 236 | ) 237 | oldSelection.sharedStyle = sharedStyle 238 | } 239 | 240 | if (oldSelection.sharedStyle) { 241 | oldSelection.sharedStyle.name = newParts.join(' / ') 242 | } 243 | 244 | updateLayout(document, getHierarchy(document, isTextStyle), isTextStyle) 245 | return 'handle changed layer' 246 | } 247 | -------------------------------------------------------------------------------- /src/test-utils.js: -------------------------------------------------------------------------------- 1 | import { createSharedStyle } from './utils' 2 | import { main as init } from './init' 3 | 4 | export function initDummyDocument(document) { 5 | document.pages[0].layers = [{ type: 'Artboard' }] 6 | createSharedStyle(true, 'Header/H1', { fontSize: 12 }, document) 7 | createSharedStyle(true, 'Header/H2', { fontSize: 12 }, document) 8 | createSharedStyle(true, 'Button/Primary', { fontSize: 12 }, document) 9 | createSharedStyle(true, 'Button/Secondary', { fontSize: 12 }, document) 10 | init(document) 11 | } 12 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import * as sketch from 'sketch' 2 | import { 3 | TEXT_EXAMPLES, 4 | ROOT_STYLE_NAME, 5 | TEXT_STYLES_PAGE, 6 | LAYER_STYLES_PAGE, 7 | } from './constants' 8 | 9 | let i = 0 10 | export function getTextExample() { 11 | return TEXT_EXAMPLES[i++ % TEXT_EXAMPLES.length] 12 | } 13 | 14 | export function getParts(sharedStyle) { 15 | return sharedStyle.name.split('/').map(p => p.trim()) 16 | } 17 | 18 | function buildHierarchy(sharedStyles) { 19 | return sharedStyles.reduce((prev, s) => { 20 | const parts = getParts(s) 21 | 22 | let currentParent = { children: prev } 23 | 24 | parts.forEach(p => { 25 | let index = currentParent.children.findIndex(x => x.name === p) 26 | if (index === -1) { 27 | index = currentParent.children.length 28 | currentParent.children.push({ 29 | name: p, 30 | children: [], 31 | }) 32 | } 33 | currentParent = currentParent.children[index] 34 | }) 35 | 36 | currentParent.style = s 37 | 38 | return prev 39 | }, []) 40 | } 41 | 42 | export function getHierarchy(document, isTextStyle) { 43 | const sharedStyles = (isTextStyle 44 | ? document.sharedTextStyles || document.getSharedTextStyles() 45 | : document.sharedLayerStyles || document.getSharedLayerStyles() 46 | ).filter(s => !s.getLibrary()) 47 | 48 | return buildHierarchy(sharedStyles) 49 | } 50 | 51 | export function createSharedStyle(isTextStyle, name, style, document) { 52 | if (document.sharedTextStyles) { 53 | const sharedStyles = 54 | document[isTextStyle ? 'sharedTextStyles' : 'sharedLayerStyles'] 55 | const newLength = sharedStyles.push({ 56 | name, 57 | style, 58 | }) 59 | return sharedStyles[newLength - 1] 60 | } 61 | 62 | return sketch.SharedStyle.fromStyle({ name, style, document }) 63 | } 64 | 65 | function _updateLayout({ name, style, children }, parent, isTextStyle, y) { 66 | if (!children.length && !style) { 67 | throw new Error('what') 68 | } 69 | 70 | const Primitive = isTextStyle ? sketch.Text : sketch.ShapePath 71 | 72 | if (!children.length) { 73 | let matchingLayer = parent.layers.find(l => l.name === name) 74 | if (!matchingLayer) { 75 | matchingLayer = new Primitive({ 76 | parent, 77 | name, 78 | frame: { 79 | x: 20, 80 | y, 81 | width: 50, 82 | height: 50, 83 | }, 84 | style: style.style, 85 | sharedStyle: style, 86 | }) 87 | 88 | if (isTextStyle) { 89 | matchingLayer.text = getTextExample() 90 | matchingLayer.name = name 91 | matchingLayer.adjustToFit() 92 | } 93 | } 94 | 95 | // reposition the layer 96 | matchingLayer.frame.x = 20 97 | matchingLayer.frame.y = y 98 | 99 | return matchingLayer.frame.height 100 | } 101 | 102 | let matchingGroup = parent.layers.find(l => l.name === name) 103 | if (!matchingGroup) { 104 | matchingGroup = new sketch.Group({ 105 | parent, 106 | name, 107 | frame: { 108 | x: 20, 109 | y, 110 | width: 1000, 111 | height: 1000, 112 | }, 113 | }) 114 | } 115 | 116 | // remove outdated layers 117 | matchingGroup.layers.forEach(l => { 118 | if (!children.find(c => c.name === l.name) && l.name !== ROOT_STYLE_NAME) { 119 | l.parent = undefined 120 | } 121 | }) 122 | 123 | let matchingDefault = matchingGroup.layers.find( 124 | l => l.name === ROOT_STYLE_NAME 125 | ) 126 | if (!matchingDefault) { 127 | matchingDefault = new Primitive({ 128 | parent: matchingGroup, 129 | name: ROOT_STYLE_NAME, 130 | frame: { 131 | x: 0, 132 | y: 0, 133 | width: 50, 134 | height: 50, 135 | }, 136 | }) 137 | if (style) { 138 | matchingDefault.style = style.style 139 | matchingDefault.sharedStyle = style 140 | } else if (!isTextStyle) { 141 | // need to set a default style 142 | matchingDefault.style = { 143 | fills: ['#000'], 144 | } 145 | } 146 | 147 | if (isTextStyle) { 148 | matchingDefault.text = getTextExample() 149 | matchingDefault.name = ROOT_STYLE_NAME 150 | matchingDefault.adjustToFit() 151 | } 152 | } 153 | 154 | matchingDefault.frame.x = 0 155 | matchingDefault.frame.y = 0 156 | 157 | let childrenY = matchingDefault.frame.height + 20 158 | 159 | children.forEach(c => { 160 | childrenY += _updateLayout(c, matchingGroup, isTextStyle, childrenY) + 20 161 | }) 162 | 163 | matchingGroup.adjustToFit() 164 | matchingGroup.frame.x = 20 165 | matchingGroup.frame.y = y 166 | 167 | return matchingGroup.frame.height 168 | } 169 | 170 | const adjustArtboard = artboard => { 171 | artboard.adjustToFit() 172 | artboard.frame.x = 0 173 | artboard.frame.y = 0 174 | artboard.frame.width += 40 175 | artboard.frame.height += 40 176 | artboard.layers.forEach(x => { 177 | x.frame.x += 20 178 | x.frame.y += 20 179 | }) 180 | } 181 | 182 | export const updateLayerHierarchy = (artboard, hierarchy, isTextStyle) => { 183 | let y = 20 184 | 185 | hierarchy.forEach(c => { 186 | y += _updateLayout(c, artboard, isTextStyle, y) + 20 187 | }) 188 | 189 | adjustArtboard(artboard) 190 | } 191 | 192 | export const getInheritedKeys = layer => { 193 | const inheritance = layer 194 | ? sketch.Settings.layerSettingForKey(layer, 'inheritance') || {} 195 | : {} 196 | return Object.keys(inheritance).filter(k => inheritance[k]) 197 | } 198 | 199 | export const updateChildren = parent => { 200 | if (!parent || !parent.layers) { 201 | return 202 | } 203 | const defaultStyle = parent.layers.find(l => l.name === ROOT_STYLE_NAME) 204 | const inheritance = getInheritedKeys(defaultStyle) 205 | parent.layers.forEach(l => { 206 | if (l.name === ROOT_STYLE_NAME) { 207 | return 208 | } 209 | if (l.sharedStyle) { 210 | inheritance.forEach(k => { 211 | l.style[k] = defaultStyle.style[k] 212 | }) 213 | l.sharedStyle.style = l.style 214 | } 215 | if (l.type === 'Group') { 216 | const child = l.layers.find(x => x.name === ROOT_STYLE_NAME) 217 | if (child) { 218 | inheritance.forEach(k => { 219 | child.style[k] = defaultStyle.style[k] 220 | }) 221 | if (child.sharedStyle) { 222 | child.sharedStyle.style = child.style 223 | } 224 | } 225 | } 226 | updateChildren(l) 227 | }) 228 | } 229 | 230 | export function updateLayout(document, hierarchy, isTextStyle) { 231 | const page = document.pages.find( 232 | p => p.name === (isTextStyle ? TEXT_STYLES_PAGE : LAYER_STYLES_PAGE) 233 | ) 234 | updateLayerHierarchy(page.layers[0], hierarchy, isTextStyle) 235 | } 236 | -------------------------------------------------------------------------------- /xib-views/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2019 Mathieu Dutour. All rights reserved. 23 | 24 | 25 | -------------------------------------------------------------------------------- /xib-views/LayerStylePreferences.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 23 | 31 | 39 | 47 | 55 | 63 | 71 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /xib-views/TextStylePreferences.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 23 | 31 | 39 | 47 | 55 | 63 | 71 | 79 | 87 | 95 | 103 | 111 | 119 | 127 | 135 | 143 | 151 | 159 | 167 | 175 | 183 | 191 | 199 | 200 | 201 | 202 | 203 | 204 | -------------------------------------------------------------------------------- /xib-views/xib-views.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0310D119220E1F13007B3546 /* xib_views.h in Headers */ = {isa = PBXBuildFile; fileRef = 0310D117220E1F13007B3546 /* xib_views.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | 0310D120220E2030007B3546 /* TextStylePreferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0310D11F220E2030007B3546 /* TextStylePreferences.xib */; }; 12 | 0310D122220E2389007B3546 /* LayerStylePreferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0310D121220E2389007B3546 /* LayerStylePreferences.xib */; }; 13 | /* End PBXBuildFile section */ 14 | 15 | /* Begin PBXFileReference section */ 16 | 0310D114220E1F13007B3546 /* xib_views.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = xib_views.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 17 | 0310D117220E1F13007B3546 /* xib_views.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = xib_views.h; sourceTree = ""; }; 18 | 0310D118220E1F13007B3546 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 19 | 0310D11F220E2030007B3546 /* TextStylePreferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TextStylePreferences.xib; sourceTree = ""; }; 20 | 0310D121220E2389007B3546 /* LayerStylePreferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LayerStylePreferences.xib; sourceTree = ""; }; 21 | /* End PBXFileReference section */ 22 | 23 | /* Begin PBXFrameworksBuildPhase section */ 24 | 0310D111220E1F13007B3546 /* Frameworks */ = { 25 | isa = PBXFrameworksBuildPhase; 26 | buildActionMask = 2147483647; 27 | files = ( 28 | ); 29 | runOnlyForDeploymentPostprocessing = 0; 30 | }; 31 | /* End PBXFrameworksBuildPhase section */ 32 | 33 | /* Begin PBXGroup section */ 34 | 0310D10A220E1F13007B3546 = { 35 | isa = PBXGroup; 36 | children = ( 37 | 0310D11F220E2030007B3546 /* TextStylePreferences.xib */, 38 | 0310D121220E2389007B3546 /* LayerStylePreferences.xib */, 39 | 0310D117220E1F13007B3546 /* xib_views.h */, 40 | 0310D118220E1F13007B3546 /* Info.plist */, 41 | 0310D115220E1F13007B3546 /* Products */, 42 | ); 43 | sourceTree = ""; 44 | }; 45 | 0310D115220E1F13007B3546 /* Products */ = { 46 | isa = PBXGroup; 47 | children = ( 48 | 0310D114220E1F13007B3546 /* xib_views.framework */, 49 | ); 50 | name = Products; 51 | sourceTree = ""; 52 | }; 53 | /* End PBXGroup section */ 54 | 55 | /* Begin PBXHeadersBuildPhase section */ 56 | 0310D10F220E1F13007B3546 /* Headers */ = { 57 | isa = PBXHeadersBuildPhase; 58 | buildActionMask = 2147483647; 59 | files = ( 60 | 0310D119220E1F13007B3546 /* xib_views.h in Headers */, 61 | ); 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | /* End PBXHeadersBuildPhase section */ 65 | 66 | /* Begin PBXNativeTarget section */ 67 | 0310D113220E1F13007B3546 /* xib-views */ = { 68 | isa = PBXNativeTarget; 69 | buildConfigurationList = 0310D11C220E1F13007B3546 /* Build configuration list for PBXNativeTarget "xib-views" */; 70 | buildPhases = ( 71 | 0310D10F220E1F13007B3546 /* Headers */, 72 | 0310D110220E1F13007B3546 /* Sources */, 73 | 0310D111220E1F13007B3546 /* Frameworks */, 74 | 0310D112220E1F13007B3546 /* Resources */, 75 | ); 76 | buildRules = ( 77 | ); 78 | dependencies = ( 79 | ); 80 | name = "xib-views"; 81 | productName = "xib-views"; 82 | productReference = 0310D114220E1F13007B3546 /* xib_views.framework */; 83 | productType = "com.apple.product-type.framework"; 84 | }; 85 | /* End PBXNativeTarget section */ 86 | 87 | /* Begin PBXProject section */ 88 | 0310D10B220E1F13007B3546 /* Project object */ = { 89 | isa = PBXProject; 90 | attributes = { 91 | LastUpgradeCheck = 1010; 92 | ORGANIZATIONNAME = "Mathieu Dutour"; 93 | TargetAttributes = { 94 | 0310D113220E1F13007B3546 = { 95 | CreatedOnToolsVersion = 10.1; 96 | }; 97 | }; 98 | }; 99 | buildConfigurationList = 0310D10E220E1F13007B3546 /* Build configuration list for PBXProject "xib-views" */; 100 | compatibilityVersion = "Xcode 9.3"; 101 | developmentRegion = en; 102 | hasScannedForEncodings = 0; 103 | knownRegions = ( 104 | en, 105 | ); 106 | mainGroup = 0310D10A220E1F13007B3546; 107 | productRefGroup = 0310D115220E1F13007B3546 /* Products */; 108 | projectDirPath = ""; 109 | projectRoot = ""; 110 | targets = ( 111 | 0310D113220E1F13007B3546 /* xib-views */, 112 | ); 113 | }; 114 | /* End PBXProject section */ 115 | 116 | /* Begin PBXResourcesBuildPhase section */ 117 | 0310D112220E1F13007B3546 /* Resources */ = { 118 | isa = PBXResourcesBuildPhase; 119 | buildActionMask = 2147483647; 120 | files = ( 121 | 0310D122220E2389007B3546 /* LayerStylePreferences.xib in Resources */, 122 | 0310D120220E2030007B3546 /* TextStylePreferences.xib in Resources */, 123 | ); 124 | runOnlyForDeploymentPostprocessing = 0; 125 | }; 126 | /* End PBXResourcesBuildPhase section */ 127 | 128 | /* Begin PBXSourcesBuildPhase section */ 129 | 0310D110220E1F13007B3546 /* Sources */ = { 130 | isa = PBXSourcesBuildPhase; 131 | buildActionMask = 2147483647; 132 | files = ( 133 | ); 134 | runOnlyForDeploymentPostprocessing = 0; 135 | }; 136 | /* End PBXSourcesBuildPhase section */ 137 | 138 | /* Begin XCBuildConfiguration section */ 139 | 0310D11A220E1F13007B3546 /* Debug */ = { 140 | isa = XCBuildConfiguration; 141 | buildSettings = { 142 | ALWAYS_SEARCH_USER_PATHS = NO; 143 | CLANG_ANALYZER_NONNULL = YES; 144 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 145 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 146 | CLANG_CXX_LIBRARY = "libc++"; 147 | CLANG_ENABLE_MODULES = YES; 148 | CLANG_ENABLE_OBJC_ARC = YES; 149 | CLANG_ENABLE_OBJC_WEAK = YES; 150 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 151 | CLANG_WARN_BOOL_CONVERSION = YES; 152 | CLANG_WARN_COMMA = YES; 153 | CLANG_WARN_CONSTANT_CONVERSION = YES; 154 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 155 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 156 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 157 | CLANG_WARN_EMPTY_BODY = YES; 158 | CLANG_WARN_ENUM_CONVERSION = YES; 159 | CLANG_WARN_INFINITE_RECURSION = YES; 160 | CLANG_WARN_INT_CONVERSION = YES; 161 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 162 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 163 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 164 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 165 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 166 | CLANG_WARN_STRICT_PROTOTYPES = YES; 167 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 168 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 169 | CLANG_WARN_UNREACHABLE_CODE = YES; 170 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 171 | CODE_SIGN_IDENTITY = "-"; 172 | COPY_PHASE_STRIP = NO; 173 | CURRENT_PROJECT_VERSION = 1; 174 | DEBUG_INFORMATION_FORMAT = dwarf; 175 | ENABLE_STRICT_OBJC_MSGSEND = YES; 176 | ENABLE_TESTABILITY = YES; 177 | GCC_C_LANGUAGE_STANDARD = gnu11; 178 | GCC_DYNAMIC_NO_PIC = NO; 179 | GCC_NO_COMMON_BLOCKS = YES; 180 | GCC_OPTIMIZATION_LEVEL = 0; 181 | GCC_PREPROCESSOR_DEFINITIONS = ( 182 | "DEBUG=1", 183 | "$(inherited)", 184 | ); 185 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 186 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 187 | GCC_WARN_UNDECLARED_SELECTOR = YES; 188 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 189 | GCC_WARN_UNUSED_FUNCTION = YES; 190 | GCC_WARN_UNUSED_VARIABLE = YES; 191 | MACOSX_DEPLOYMENT_TARGET = 10.14; 192 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 193 | MTL_FAST_MATH = YES; 194 | ONLY_ACTIVE_ARCH = YES; 195 | SDKROOT = macosx; 196 | VERSIONING_SYSTEM = "apple-generic"; 197 | VERSION_INFO_PREFIX = ""; 198 | }; 199 | name = Debug; 200 | }; 201 | 0310D11B220E1F13007B3546 /* Release */ = { 202 | isa = XCBuildConfiguration; 203 | buildSettings = { 204 | ALWAYS_SEARCH_USER_PATHS = NO; 205 | CLANG_ANALYZER_NONNULL = YES; 206 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 207 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 208 | CLANG_CXX_LIBRARY = "libc++"; 209 | CLANG_ENABLE_MODULES = YES; 210 | CLANG_ENABLE_OBJC_ARC = YES; 211 | CLANG_ENABLE_OBJC_WEAK = YES; 212 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 213 | CLANG_WARN_BOOL_CONVERSION = YES; 214 | CLANG_WARN_COMMA = YES; 215 | CLANG_WARN_CONSTANT_CONVERSION = YES; 216 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 217 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 218 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 219 | CLANG_WARN_EMPTY_BODY = YES; 220 | CLANG_WARN_ENUM_CONVERSION = YES; 221 | CLANG_WARN_INFINITE_RECURSION = YES; 222 | CLANG_WARN_INT_CONVERSION = YES; 223 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 224 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 225 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 226 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 227 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 228 | CLANG_WARN_STRICT_PROTOTYPES = YES; 229 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 230 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 231 | CLANG_WARN_UNREACHABLE_CODE = YES; 232 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 233 | CODE_SIGN_IDENTITY = "-"; 234 | COPY_PHASE_STRIP = NO; 235 | CURRENT_PROJECT_VERSION = 1; 236 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 237 | ENABLE_NS_ASSERTIONS = NO; 238 | ENABLE_STRICT_OBJC_MSGSEND = YES; 239 | GCC_C_LANGUAGE_STANDARD = gnu11; 240 | GCC_NO_COMMON_BLOCKS = YES; 241 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 242 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 243 | GCC_WARN_UNDECLARED_SELECTOR = YES; 244 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 245 | GCC_WARN_UNUSED_FUNCTION = YES; 246 | GCC_WARN_UNUSED_VARIABLE = YES; 247 | MACOSX_DEPLOYMENT_TARGET = 10.14; 248 | MTL_ENABLE_DEBUG_INFO = NO; 249 | MTL_FAST_MATH = YES; 250 | SDKROOT = macosx; 251 | VERSIONING_SYSTEM = "apple-generic"; 252 | VERSION_INFO_PREFIX = ""; 253 | }; 254 | name = Release; 255 | }; 256 | 0310D11D220E1F13007B3546 /* Debug */ = { 257 | isa = XCBuildConfiguration; 258 | buildSettings = { 259 | CODE_SIGN_IDENTITY = ""; 260 | CODE_SIGN_STYLE = Automatic; 261 | COMBINE_HIDPI_IMAGES = YES; 262 | DEFINES_MODULE = YES; 263 | DYLIB_COMPATIBILITY_VERSION = 1; 264 | DYLIB_CURRENT_VERSION = 1; 265 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 266 | FRAMEWORK_VERSION = A; 267 | INFOPLIST_FILE = "xib-views/Info.plist"; 268 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 269 | LD_RUNPATH_SEARCH_PATHS = ( 270 | "$(inherited)", 271 | "@executable_path/../Frameworks", 272 | "@loader_path/Frameworks", 273 | ); 274 | PRODUCT_BUNDLE_IDENTIFIER = "me.dutour.mathieu.xib-views"; 275 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 276 | SKIP_INSTALL = YES; 277 | }; 278 | name = Debug; 279 | }; 280 | 0310D11E220E1F13007B3546 /* Release */ = { 281 | isa = XCBuildConfiguration; 282 | buildSettings = { 283 | CODE_SIGN_IDENTITY = ""; 284 | CODE_SIGN_STYLE = Automatic; 285 | COMBINE_HIDPI_IMAGES = YES; 286 | DEFINES_MODULE = YES; 287 | DYLIB_COMPATIBILITY_VERSION = 1; 288 | DYLIB_CURRENT_VERSION = 1; 289 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 290 | FRAMEWORK_VERSION = A; 291 | INFOPLIST_FILE = "xib-views/Info.plist"; 292 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 293 | LD_RUNPATH_SEARCH_PATHS = ( 294 | "$(inherited)", 295 | "@executable_path/../Frameworks", 296 | "@loader_path/Frameworks", 297 | ); 298 | PRODUCT_BUNDLE_IDENTIFIER = "me.dutour.mathieu.xib-views"; 299 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 300 | SKIP_INSTALL = YES; 301 | }; 302 | name = Release; 303 | }; 304 | /* End XCBuildConfiguration section */ 305 | 306 | /* Begin XCConfigurationList section */ 307 | 0310D10E220E1F13007B3546 /* Build configuration list for PBXProject "xib-views" */ = { 308 | isa = XCConfigurationList; 309 | buildConfigurations = ( 310 | 0310D11A220E1F13007B3546 /* Debug */, 311 | 0310D11B220E1F13007B3546 /* Release */, 312 | ); 313 | defaultConfigurationIsVisible = 0; 314 | defaultConfigurationName = Release; 315 | }; 316 | 0310D11C220E1F13007B3546 /* Build configuration list for PBXNativeTarget "xib-views" */ = { 317 | isa = XCConfigurationList; 318 | buildConfigurations = ( 319 | 0310D11D220E1F13007B3546 /* Debug */, 320 | 0310D11E220E1F13007B3546 /* Release */, 321 | ); 322 | defaultConfigurationIsVisible = 0; 323 | defaultConfigurationName = Release; 324 | }; 325 | /* End XCConfigurationList section */ 326 | }; 327 | rootObject = 0310D10B220E1F13007B3546 /* Project object */; 328 | } 329 | -------------------------------------------------------------------------------- /xib-views/xib-views.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /xib-views/xib-views.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /xib-views/xib-views.xcodeproj/project.xcworkspace/xcuserdata/mathieudutour.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathieudutour/sketch-styles-hierarchy/720a815b7ad2b64102333d5ca8d51a25bfd8179f/xib-views/xib-views.xcodeproj/project.xcworkspace/xcuserdata/mathieudutour.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /xib-views/xib-views.xcodeproj/xcuserdata/mathieudutour.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | xib-views.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /xib-views/xib_views.h: -------------------------------------------------------------------------------- 1 | // 2 | // xib_views.h 3 | // xib-views 4 | // 5 | // Created by Mathieu Dutour on 08/02/2019. 6 | // Copyright © 2019 Mathieu Dutour. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for xib_views. 12 | FOUNDATION_EXPORT double xib_viewsVersionNumber; 13 | 14 | //! Project version string for xib_views. 15 | FOUNDATION_EXPORT const unsigned char xib_viewsVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | --------------------------------------------------------------------------------